summaryrefslogtreecommitdiffstats
path: root/browser/base/content/test/static
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
commit36d22d82aa202bb199967e9512281e9a53db42c9 (patch)
tree105e8c98ddea1c1e4784a60a5a6410fa416be2de /browser/base/content/test/static
parentInitial commit. (diff)
downloadfirefox-esr-upstream.tar.xz
firefox-esr-upstream.zip
Adding upstream version 115.7.0esr.upstream/115.7.0esrupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'browser/base/content/test/static')
-rw-r--r--browser/base/content/test/static/browser.ini22
-rw-r--r--browser/base/content/test/static/browser_all_files_referenced.js1093
-rw-r--r--browser/base/content/test/static/browser_misused_characters_in_strings.js276
-rw-r--r--browser/base/content/test/static/browser_parsable_css.js590
-rw-r--r--browser/base/content/test/static/browser_parsable_script.js167
-rw-r--r--browser/base/content/test/static/browser_sentence_case_strings.js279
-rw-r--r--browser/base/content/test/static/browser_title_case_menus.js158
-rw-r--r--browser/base/content/test/static/bug1262648_string_with_newlines.dtd3
-rw-r--r--browser/base/content/test/static/dummy_page.html9
-rw-r--r--browser/base/content/test/static/head.js177
10 files changed, 2774 insertions, 0 deletions
diff --git a/browser/base/content/test/static/browser.ini b/browser/base/content/test/static/browser.ini
new file mode 100644
index 0000000000..69f2a71723
--- /dev/null
+++ b/browser/base/content/test/static/browser.ini
@@ -0,0 +1,22 @@
+[DEFAULT]
+# These tests can be prone to intermittent failures on slower systems.
+# Since the specific flavor doesn't matter from a correctness standpoint,
+# just skip the tests on sanitizer, debug and OS X verify builds.
+skip-if = (asan || tsan || debug || (verify && os == 'mac'))
+support-files =
+ head.js
+
+[browser_all_files_referenced.js]
+skip-if = verify && bits == 32 # Causes OOMs when run repeatedly
+[browser_misused_characters_in_strings.js]
+support-files =
+ bug1262648_string_with_newlines.dtd
+skip-if = os == 'win' && msix # Permafail on MSIX packages due to it running on files it shouldn't.
+[browser_parsable_css.js]
+support-files =
+ dummy_page.html
+skip-if = os == 'win' && msix # Permafail on MSIX packages due to it running on files it shouldn't.
+[browser_parsable_script.js]
+skip-if = ccov && os == 'linux' # https://bugzilla.mozilla.org/show_bug.cgi?id=1608081
+[browser_sentence_case_strings.js]
+[browser_title_case_menus.js]
diff --git a/browser/base/content/test/static/browser_all_files_referenced.js b/browser/base/content/test/static/browser_all_files_referenced.js
new file mode 100644
index 0000000000..df8a1997a7
--- /dev/null
+++ b/browser/base/content/test/static/browser_all_files_referenced.js
@@ -0,0 +1,1093 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Note to run this test similar to try server, you need to run:
+// ./mach package
+// ./mach mochitest --appname dist <path to test>
+
+// Slow on asan builds.
+requestLongerTimeout(5);
+
+var isDevtools = SimpleTest.harnessParameters.subsuite == "devtools";
+
+// This list should contain only path prefixes. It is meant to stop the test
+// from reporting things that *are* referenced, but for which the test can't
+// find any reference because the URIs are constructed programatically.
+// If you need to whitelist specific files, please use the 'whitelist' object.
+var gExceptionPaths = [
+ "resource://app/defaults/settings/blocklists/",
+ "resource://app/defaults/settings/security-state/",
+ "resource://app/defaults/settings/main/",
+ "resource://app/defaults/preferences/",
+ "resource://gre/modules/commonjs/",
+ "resource://gre/defaults/pref/",
+
+ // These chrome resources are referenced using relative paths from JS files.
+ "chrome://global/content/certviewer/components/",
+
+ // https://github.com/mozilla/activity-stream/issues/3053
+ "chrome://activity-stream/content/data/content/tippytop/images/",
+ "chrome://activity-stream/content/data/content/tippytop/favicons/",
+ // These resources are referenced by messages delivered through Remote Settings
+ "chrome://activity-stream/content/data/content/assets/remote/",
+ "chrome://activity-stream/content/data/content/assets/mobile-download-qr-new-user-cn.svg",
+ "chrome://activity-stream/content/data/content/assets/mobile-download-qr-existing-user-cn.svg",
+ "chrome://activity-stream/content/data/content/assets/person-typing.svg",
+ "chrome://browser/content/assets/moz-vpn.svg",
+ "chrome://browser/content/assets/vpn-logo.svg",
+ "chrome://browser/content/assets/focus-promo.png",
+ "chrome://browser/content/assets/klar-qr-code.svg",
+
+ // toolkit/components/pdfjs/content/build/pdf.js
+ "resource://pdf.js/web/images/",
+
+ // Exclude the form autofill path that has been moved out of the extensions to
+ // toolkit, see bug 1691821.
+ "resource://gre-resources/autofill/",
+
+ // Exclude all search-extensions because they aren't referenced by filename
+ "resource://search-extensions/",
+
+ // Exclude all services-automation because they are used through webdriver
+ "resource://gre/modules/services-automation/",
+ "resource://services-automation/ServicesAutomation.jsm",
+
+ // Paths from this folder are constructed in NetErrorParent.sys.mjs based on
+ // the type of cert or net error the user is encountering.
+ "chrome://global/content/neterror/supportpages/",
+
+ // Points to theme preview images, which are defined in browser/ but only used
+ // in toolkit/mozapps/extensions/content/aboutaddons.js.
+ "resource://usercontext-content/builtin-themes/",
+
+ // Page data schemas are referenced programmatically.
+ "chrome://browser/content/pagedata/schemas/",
+
+ // Nimbus schemas are referenced programmatically.
+ "resource://nimbus/schemas/",
+
+ // Activity stream schemas are referenced programmatically.
+ "resource://activity-stream/schemas",
+
+ // Localization file added programatically in featureCallout.jsm
+ "resource://app/localization/en-US/browser/featureCallout.ftl",
+];
+
+// These are not part of the omni.ja file, so we find them only when running
+// the test on a non-packaged build.
+if (AppConstants.platform == "macosx") {
+ gExceptionPaths.push("resource://gre/res/cursors/");
+ gExceptionPaths.push("resource://gre/res/touchbar/");
+}
+
+if (AppConstants.MOZ_BACKGROUNDTASKS) {
+ // These preferences are active only when we're in background task mode.
+ gExceptionPaths.push("resource://gre/defaults/backgroundtasks/");
+ gExceptionPaths.push("resource://app/defaults/backgroundtasks/");
+ // `BackgroundTask_id.jsm` is loaded at runtime by `app --backgroundtask id ...`.
+ gExceptionPaths.push("resource://gre/modules/backgroundtasks/");
+ gExceptionPaths.push("resource://app/modules/backgroundtasks/");
+}
+
+// Bug 1710546 https://bugzilla.mozilla.org/show_bug.cgi?id=1710546
+if (AppConstants.NIGHTLY_BUILD) {
+ gExceptionPaths.push("resource://builtin-addons/translations/");
+}
+
+if (AppConstants.NIGHTLY_BUILD) {
+ // This is nightly-only debug tool.
+ gExceptionPaths.push(
+ "chrome://browser/content/places/interactionsViewer.html"
+ );
+}
+
+// Each whitelist entry should have a comment indicating which file is
+// referencing the whitelisted file in a way that the test can't detect, or a
+// bug number to remove or use the file if it is indeed currently unreferenced.
+var whitelist = [
+ // toolkit/components/pdfjs/content/PdfStreamConverter.jsm
+ { file: "chrome://pdf.js/locale/chrome.properties" },
+ { file: "chrome://pdf.js/locale/viewer.properties" },
+
+ // security/manager/pki/resources/content/device_manager.js
+ { file: "chrome://pippki/content/load_device.xhtml" },
+
+ // The l10n build system can't package string files only for some platforms.
+ // See bug 1339424 for why this is hard to fix.
+ {
+ file: "chrome://global/locale/fallbackMenubar.properties",
+ platforms: ["linux", "win"],
+ },
+ {
+ file: "resource://gre/localization/en-US/toolkit/printing/printDialogs.ftl",
+ platforms: ["linux", "macosx"],
+ },
+
+ // This file is referenced by the build system to generate the
+ // Firefox .desktop entry. See bug 1824327 (and perhaps bug 1526672)
+ {
+ file: "resource://app/localization/en-US/browser/linuxDesktopEntry.ftl",
+ },
+
+ // toolkit/content/aboutRights-unbranded.xhtml doesn't use aboutRights.css
+ { file: "chrome://global/skin/aboutRights.css", skipUnofficial: true },
+
+ // devtools/client/inspector/bin/dev-server.js
+ {
+ file: "chrome://devtools/content/inspector/markup/markup.xhtml",
+ isFromDevTools: true,
+ },
+
+ // used by devtools/client/memory/index.xhtml
+ { file: "chrome://global/content/third_party/d3/d3.js" },
+
+ // SpiderMonkey parser API, currently unused in browser/ and toolkit/
+ { file: "resource://gre/modules/reflect.sys.mjs" },
+
+ // extensions/pref/autoconfig/src/nsReadConfig.cpp
+ { file: "resource://gre/defaults/autoconfig/prefcalls.js" },
+
+ // browser/components/preferences/moreFromMozilla.js
+ // These files URLs are constructed programatically at run time.
+ {
+ file: "chrome://browser/content/preferences/more-from-mozilla-qr-code-simple.svg",
+ },
+ {
+ file: "chrome://browser/content/preferences/more-from-mozilla-qr-code-simple-cn.svg",
+ },
+
+ { file: "resource://gre/greprefs.js" },
+
+ // layout/mathml/nsMathMLChar.cpp
+ { file: "resource://gre/res/fonts/mathfontSTIXGeneral.properties" },
+ { file: "resource://gre/res/fonts/mathfontUnicode.properties" },
+
+ // toolkit/mozapps/extensions/AddonContentPolicy.cpp
+ { file: "resource://gre/localization/en-US/toolkit/global/cspErrors.ftl" },
+
+ // The l10n build system can't package string files only for some platforms.
+ {
+ file: "resource://gre/chrome/en-US/locale/en-US/global-platform/mac/accessible.properties",
+ platforms: ["linux", "win"],
+ },
+ {
+ file: "resource://gre/chrome/en-US/locale/en-US/global-platform/mac/intl.properties",
+ platforms: ["linux", "win"],
+ },
+ {
+ file: "resource://gre/chrome/en-US/locale/en-US/global-platform/mac/platformKeys.properties",
+ platforms: ["linux", "win"],
+ },
+ {
+ file: "resource://gre/chrome/en-US/locale/en-US/global-platform/unix/accessible.properties",
+ platforms: ["macosx", "win"],
+ },
+ {
+ file: "resource://gre/chrome/en-US/locale/en-US/global-platform/unix/intl.properties",
+ platforms: ["macosx", "win"],
+ },
+ {
+ file: "resource://gre/chrome/en-US/locale/en-US/global-platform/unix/platformKeys.properties",
+ platforms: ["macosx", "win"],
+ },
+ {
+ file: "resource://gre/chrome/en-US/locale/en-US/global-platform/win/accessible.properties",
+ platforms: ["linux", "macosx"],
+ },
+ {
+ file: "resource://gre/chrome/en-US/locale/en-US/global-platform/win/intl.properties",
+ platforms: ["linux", "macosx"],
+ },
+ {
+ file: "resource://gre/chrome/en-US/locale/en-US/global-platform/win/platformKeys.properties",
+ platforms: ["linux", "macosx"],
+ },
+
+ // Files from upstream library
+ { file: "resource://pdf.js/web/debugger.js" },
+ { file: "resource://pdf.js/web/debugger.css" },
+
+ // resource://app/modules/translation/TranslationContentHandler.jsm
+ { file: "resource://app/modules/translation/BingTranslator.jsm" },
+ { file: "resource://app/modules/translation/GoogleTranslator.jsm" },
+ { file: "resource://app/modules/translation/YandexTranslator.jsm" },
+
+ // Starting from here, files in the whitelist are bugs that need fixing.
+ // Bug 1339424 (wontfix?)
+ {
+ file: "chrome://browser/locale/taskbar.properties",
+ platforms: ["linux", "macosx"],
+ },
+ // Bug 1344267
+ { file: "chrome://remote/content/marionette/test_dialog.properties" },
+ { file: "chrome://remote/content/marionette/test_dialog.xhtml" },
+ { file: "chrome://remote/content/marionette/test_menupopup.xhtml" },
+ { file: "chrome://remote/content/marionette/test_no_xul.xhtml" },
+ { file: "chrome://remote/content/marionette/test.xhtml" },
+ // Bug 1348559
+ { file: "chrome://pippki/content/resetpassword.xhtml" },
+ // Bug 1337345
+ { file: "resource://gre/modules/Manifest.sys.mjs" },
+ // Bug 1494170
+ // (The references to these files are dynamically generated, so the test can't
+ // find the references)
+ {
+ file: "chrome://devtools/skin/images/aboutdebugging-firefox-aurora.svg",
+ isFromDevTools: true,
+ },
+ {
+ file: "chrome://devtools/skin/images/aboutdebugging-firefox-beta.svg",
+ isFromDevTools: true,
+ },
+ {
+ file: "chrome://devtools/skin/images/aboutdebugging-firefox-release.svg",
+ isFromDevTools: true,
+ },
+ { file: "chrome://devtools/skin/images/next.svg", isFromDevTools: true },
+
+ // Bug 1526672
+ {
+ file: "resource://app/localization/en-US/browser/touchbar/touchbar.ftl",
+ platforms: ["linux", "win"],
+ },
+ // Referenced by the webcompat system addon for localization
+ { file: "resource://gre/localization/en-US/toolkit/about/aboutCompat.ftl" },
+
+ // dom/media/mediacontrol/MediaControlService.cpp
+ { file: "resource://gre/localization/en-US/dom/media.ftl" },
+
+ // dom/xml/nsXMLPrettyPrinter.cpp
+ { file: "resource://gre/localization/en-US/dom/XMLPrettyPrint.ftl" },
+
+ // tookit/mozapps/update/BackgroundUpdate.jsm
+ {
+ file: "resource://gre/localization/en-US/toolkit/updates/backgroundupdate.ftl",
+ },
+ // Bug 1713242 - referenced by aboutThirdParty.html which is only for Windows
+ {
+ file: "resource://gre/localization/en-US/toolkit/about/aboutThirdParty.ftl",
+ platforms: ["linux", "macosx"],
+ },
+ // Bug 1973834 - referenced by aboutWindowsMessages.html which is only for Windows
+ {
+ file: "resource://gre/localization/en-US/toolkit/about/aboutWindowsMessages.ftl",
+ platforms: ["linux", "macosx"],
+ },
+ // Bug 1721741:
+ // (The references to these files are dynamically generated, so the test can't
+ // find the references)
+ { file: "chrome://browser/content/screenshots/copied-notification.svg" },
+
+ // toolkit/xre/MacRunFromDmgUtils.mm
+ { file: "resource://gre/localization/en-US/toolkit/global/run-from-dmg.ftl" },
+
+ // Referenced by screenshots extension
+ { file: "chrome://browser/content/screenshots/cancel.svg" },
+ { file: "chrome://browser/content/screenshots/copy.svg" },
+ { file: "chrome://browser/content/screenshots/download.svg" },
+ { file: "chrome://browser/content/screenshots/download-white.svg" },
+
+ // Bug 1824826 - Implement a view of history in Firefox View
+ { file: "resource://gre/modules/PlacesQuery.sys.mjs" },
+
+ // Should be removed in bug 1824826 when fxview-tab-list is used in Firefox View
+ { file: "resource://app/localization/en-US/browser/fxviewTabList.ftl" },
+ { file: "chrome://browser/content/firefoxview/fxview-tab-list.css" },
+ { file: "chrome://browser/content/firefoxview/fxview-tab-list.mjs" },
+ { file: "chrome://browser/content/firefoxview/fxview-tab-row.css" },
+
+ // Bug 1834176 - Imports of NetUtil can't be converted until hostutils is
+ // updated.
+ { file: "resource://gre/modules/NetUtil.sys.mjs" },
+];
+
+if (AppConstants.NIGHTLY_BUILD && AppConstants.platform != "win") {
+ // This path is refereneced in nsFxrCommandLineHandler.cpp, which is only
+ // compiled in Windows. Whitelisted this path so that non-Windows builds
+ // can access the FxR UI via --chrome rather than --fxr (which includes VR-
+ // specific functionality)
+ whitelist.push({ file: "chrome://fxr/content/fxrui.html" });
+}
+
+if (AppConstants.platform == "android") {
+ // The l10n build system can't package string files only for some platforms.
+ // Referenced by aboutGlean.html
+ whitelist.push({
+ file: "resource://gre/localization/en-US/toolkit/about/aboutGlean.ftl",
+ });
+}
+
+if (AppConstants.MOZ_UPDATE_AGENT && !AppConstants.MOZ_BACKGROUNDTASKS) {
+ // Task scheduling is only used for background updates right now.
+ whitelist.push({
+ file: "resource://gre/modules/TaskScheduler.jsm",
+ });
+}
+
+whitelist = new Set(
+ whitelist
+ .filter(
+ item =>
+ "isFromDevTools" in item == isDevtools &&
+ (!item.skipUnofficial || !AppConstants.MOZILLA_OFFICIAL) &&
+ (!item.platforms || item.platforms.includes(AppConstants.platform))
+ )
+ .map(item => item.file)
+);
+
+const ignorableWhitelist = new Set([
+ // The following files are outside of the omni.ja file, so we only catch them
+ // when testing on a non-packaged build.
+
+ // toolkit/mozapps/extensions/nsBlocklistService.js
+ "resource://app/blocklist.xml",
+
+ // dom/media/gmp/GMPParent.cpp
+ "resource://gre/gmp-clearkey/0.1/manifest.json",
+
+ // Bug 1351669 - obsolete test file
+ "resource://gre/res/test.properties",
+]);
+for (let entry of ignorableWhitelist) {
+ whitelist.add(entry);
+}
+
+if (!isDevtools) {
+ // services/sync/modules/service.sys.mjs
+ for (let module of [
+ "addons.sys.mjs",
+ "bookmarks.sys.mjs",
+ "forms.sys.mjs",
+ "history.sys.mjs",
+ "passwords.sys.mjs",
+ "prefs.sys.mjs",
+ "tabs.sys.mjs",
+ "extension-storage.sys.mjs",
+ ]) {
+ whitelist.add("resource://services-sync/engines/" + module);
+ }
+ // resource://devtools/shared/worker/loader.js,
+ // resource://devtools/shared/loader/builtin-modules.js
+ if (!AppConstants.ENABLE_WEBDRIVER) {
+ whitelist.add("resource://gre/modules/jsdebugger.sys.mjs");
+ }
+}
+
+if (AppConstants.MOZ_CODE_COVERAGE) {
+ whitelist.add(
+ "chrome://remote/content/marionette/PerTestCoverageUtils.sys.mjs"
+ );
+}
+
+const gInterestingCategories = new Set([
+ "agent-style-sheets",
+ "addon-provider-module",
+ "webextension-modules",
+ "webextension-scripts",
+ "webextension-schemas",
+ "webextension-scripts-addon",
+ "webextension-scripts-content",
+ "webextension-scripts-devtools",
+]);
+
+var gChromeReg = Cc["@mozilla.org/chrome/chrome-registry;1"].getService(
+ Ci.nsIChromeRegistry
+);
+var gChromeMap = new Map();
+var gOverrideMap = new Map();
+var gComponentsSet = new Set();
+
+// In this map when the value is a Set of URLs, the file is referenced if any
+// of the files in the Set is referenced.
+// When the value is null, the file is referenced unconditionally.
+// When the value is a string, "whitelist-direct" means that we have not found
+// any reference in the code, but have a matching whitelist entry for this file.
+// "whitelist" means that the file is indirectly whitelisted, ie. a whitelisted
+// file causes this file to be referenced.
+var gReferencesFromCode = 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 + "nonexistentfile.reallynothere";
+ let uri = Services.io.newURI(chromeFile);
+ let fileUri = gChromeReg.convertChromeURL(uri);
+ return fileUri.resolve(".");
+}
+
+function trackChromeUri(uri) {
+ gChromeMap.set(getBaseUriForChromeUri(uri), uri);
+}
+
+// formautofill registers resource://formautofill/ and
+// chrome://formautofill/content/ dynamically at runtime.
+// Bug 1480276 is about addressing this without this hard-coding.
+trackResourcePrefix("autofill");
+trackChromeUri("chrome://formautofill/content/");
+
+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" || type == "locale") {
+ let chromeUri = `chrome://${argv[0]}/${type}/`;
+ // The webcompat reporter's locale directory may not exist if
+ // the addon is preffed-off, and since it's a hack until we
+ // get bz1425104 landed, we'll just skip it for now.
+ if (chromeUri === "chrome://report-site-issue/locale/") {
+ gChromeMap.set("chrome://report-site-issue/locale/", true);
+ } else {
+ trackChromeUri(chromeUri);
+ }
+ } else if (type == "override" || type == "overlay") {
+ // Overlays aren't really overrides, but behave the same in
+ // that the overlay is only referenced if the original xul
+ // file is referenced somewhere.
+ let os = "os=" + Services.appinfo.OS;
+ if (!argv.some(s => s.startsWith("os=") && s != os)) {
+ gOverrideMap.set(
+ Services.io.newURI(argv[1]).specIgnoringRef,
+ Services.io.newURI(argv[0]).specIgnoringRef
+ );
+ }
+ } else if (type == "category" && gInterestingCategories.has(argv[0])) {
+ gReferencesFromCode.set(argv[2], null);
+ } else if (type == "resource") {
+ trackResourcePrefix(argv[0]);
+ } else if (type == "component") {
+ gComponentsSet.add(argv[1]);
+ }
+ }
+ });
+}
+
+// If the given URI is a webextension manifest, extract files used by
+// any of its APIs (scripts, icons, style sheets, theme images).
+// Returns the passed in URI if the manifest is not a webextension
+// manifest, null otherwise.
+async function parseJsonManifest(uri) {
+ uri = Services.io.newURI(convertToCodeURI(uri.spec));
+
+ let raw = await fetchFile(uri.spec);
+ let data;
+ try {
+ data = JSON.parse(raw);
+ } catch (ex) {
+ return uri;
+ }
+
+ // Simplistic test for whether this is a webextension manifest:
+ if (data.manifest_version !== 2) {
+ return uri;
+ }
+
+ if (data.background?.scripts) {
+ for (let bgscript of data.background.scripts) {
+ gReferencesFromCode.set(uri.resolve(bgscript), null);
+ }
+ }
+
+ if (data.icons) {
+ for (let icon of Object.values(data.icons)) {
+ gReferencesFromCode.set(uri.resolve(icon), null);
+ }
+ }
+
+ if (data.experiment_apis) {
+ for (let api of Object.values(data.experiment_apis)) {
+ if (api.parent && api.parent.script) {
+ let script = uri.resolve(api.parent.script);
+ gReferencesFromCode.set(script, null);
+ }
+
+ if (api.schema) {
+ gReferencesFromCode.set(uri.resolve(api.schema), null);
+ }
+ }
+ }
+
+ if (data.theme_experiment && data.theme_experiment.stylesheet) {
+ let stylesheet = uri.resolve(data.theme_experiment.stylesheet);
+ gReferencesFromCode.set(stylesheet, null);
+ }
+
+ for (let themeKey of ["theme", "dark_theme"]) {
+ if (data?.[themeKey]?.images?.additional_backgrounds) {
+ for (let background of data[themeKey].images.additional_backgrounds) {
+ gReferencesFromCode.set(uri.resolve(background), null);
+ }
+ }
+ }
+
+ return null;
+}
+
+function addCodeReference(url, fromURI) {
+ let from = convertToCodeURI(fromURI.spec);
+
+ // Ignore self references.
+ if (url == from) {
+ return;
+ }
+
+ let ref;
+ if (gReferencesFromCode.has(url)) {
+ ref = gReferencesFromCode.get(url);
+ if (ref === null) {
+ return;
+ }
+ } else {
+ ref = new Set();
+ gReferencesFromCode.set(url, ref);
+ }
+ ref.add(from);
+}
+
+function listCodeReferences(refs) {
+ let refList = [];
+ if (refs) {
+ for (let ref of refs) {
+ refList.push(ref);
+ }
+ }
+ return refList.join(",");
+}
+
+function parseCSSFile(fileUri) {
+ return fetchFile(fileUri.spec).then(data => {
+ for (let line of data.split("\n")) {
+ let urls = line.match(/url\([^()]+\)/g);
+ if (!urls) {
+ // @import rules can take a string instead of a url.
+ let importMatch = line.match(/@import ['"]?([^'"]*)['"]?/);
+ if (importMatch && importMatch[1]) {
+ let url = Services.io.newURI(importMatch[1], null, fileUri).spec;
+ addCodeReference(convertToCodeURI(url), fileUri);
+ }
+ continue;
+ }
+
+ for (let url of urls) {
+ // Remove the url(" prefix and the ") suffix.
+ url = url
+ .replace(/url\(([^)]*)\)/, "$1")
+ .replace(/^"(.*)"$/, "$1")
+ .replace(/^'(.*)'$/, "$1");
+ if (url.startsWith("data:")) {
+ continue;
+ }
+
+ try {
+ url = Services.io.newURI(url, null, fileUri).specIgnoringRef;
+ addCodeReference(convertToCodeURI(url), fileUri);
+ } catch (e) {
+ ok(false, "unexpected error while resolving this URI: " + url);
+ }
+ }
+ }
+ });
+}
+
+function parseCodeFile(fileUri) {
+ return fetchFile(fileUri.spec).then(data => {
+ let baseUri;
+ for (let line of data.split("\n")) {
+ let urls = line.match(
+ /["'`]chrome:\/\/[a-zA-Z0-9-]+\/(content|skin|locale)\/[^"'` ]*["'`]/g
+ );
+
+ if (!urls) {
+ urls = line.match(/["']resource:\/\/[^"']+["']/g);
+ if (
+ urls &&
+ isDevtools &&
+ /baseURI: "resource:\/\/devtools\//.test(line)
+ ) {
+ baseUri = Services.io.newURI(urls[0].slice(1, -1));
+ continue;
+ }
+ }
+
+ if (!urls) {
+ urls = line.match(/[a-z0-9_\/-]+\.ftl/i);
+ if (urls) {
+ urls = urls[0];
+ let grePrefix = Services.io.newURI(
+ "resource://gre/localization/en-US/"
+ );
+ let appPrefix = Services.io.newURI(
+ "resource://app/localization/en-US/"
+ );
+
+ let grePrefixUrl = Services.io.newURI(urls, null, grePrefix).spec;
+ let appPrefixUrl = Services.io.newURI(urls, null, appPrefix).spec;
+
+ addCodeReference(grePrefixUrl, fileUri);
+ addCodeReference(appPrefixUrl, fileUri);
+ continue;
+ }
+ }
+
+ if (!urls) {
+ // If there's no absolute chrome URL, look for relative ones in
+ // src and href attributes.
+ let match = line.match("(?:src|href)=[\"']([^$&\"']+)");
+ if (match && match[1]) {
+ let url = Services.io.newURI(match[1], null, fileUri).spec;
+ addCodeReference(convertToCodeURI(url), fileUri);
+ }
+
+ // This handles `import` lines which may be multi-line.
+ // We have an ESLint rule, `import/no-unassigned-import` which prevents
+ // using bare `import "foo.js"`, so we don't need to handle that case
+ // here.
+ match = line.match(/from\W*['"](.*?)['"]/);
+ if (match?.[1]) {
+ let url = match[1];
+ url = Services.io.newURI(url, null, baseUri || fileUri).spec;
+ url = convertToCodeURI(url);
+ addCodeReference(url, fileUri);
+ }
+
+ if (isDevtools) {
+ let rules = [
+ ["devtools/client/locales", "chrome://devtools/locale"],
+ ["devtools/shared/locales", "chrome://devtools-shared/locale"],
+ [
+ "devtools/shared/platform",
+ "resource://devtools/shared/platform/chrome",
+ ],
+ ["devtools", "resource://devtools"],
+ ];
+
+ match = line.match(/["']((?:devtools)\/[^\\#"']+)["']/);
+ if (match && match[1]) {
+ let path = match[1];
+ for (let rule of rules) {
+ if (path.startsWith(rule[0] + "/")) {
+ path = path.replace(rule[0], rule[1]);
+ if (!/\.(properties|js|jsm|mjs|json|css)$/.test(path)) {
+ path += ".js";
+ }
+ addCodeReference(path, fileUri);
+ break;
+ }
+ }
+ }
+
+ match = line.match(/require\(['"](\.[^'"]+)['"]\)/);
+ if (match && match[1]) {
+ let url = match[1];
+ url = Services.io.newURI(url, null, baseUri || fileUri).spec;
+ url = convertToCodeURI(url);
+ if (!/\.(properties|js|jsm|mjs|json|css)$/.test(url)) {
+ url += ".js";
+ }
+ if (url.startsWith("resource://")) {
+ addCodeReference(url, fileUri);
+ } else {
+ // if we end up with a chrome:// url here, it's likely because
+ // a baseURI to a resource:// path has been defined in another
+ // .js file that is loaded in the same scope, we can't detect it.
+ }
+ }
+ }
+ continue;
+ }
+
+ for (let url of urls) {
+ // Remove quotes.
+ url = url.slice(1, -1);
+ // Remove ? or \ trailing characters.
+ if (url.endsWith("\\")) {
+ url = url.slice(0, -1);
+ }
+
+ let pos = url.indexOf("?");
+ if (pos != -1) {
+ url = url.slice(0, pos);
+ }
+
+ // Make urls like chrome://browser/skin/ point to an actual file,
+ // and remove the ref if any.
+ try {
+ url = Services.io.newURI(url).specIgnoringRef;
+ } catch (e) {
+ continue;
+ }
+
+ if (
+ isDevtools &&
+ line.includes("require(") &&
+ !/\.(properties|js|jsm|mjs|json|css)$/.test(url)
+ ) {
+ url += ".js";
+ }
+
+ addCodeReference(url, fileUri);
+ }
+ }
+ });
+}
+
+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;
+ }
+ }
+}
+
+async function chromeFileExists(aURI) {
+ try {
+ return await PerfTestHelpers.checkURIExists(aURI);
+ } catch (e) {
+ todo(false, `Failed to check if ${aURI} exists: ${e}`);
+ return false;
+ }
+}
+
+function findChromeUrlsFromArray(array, prefix) {
+ // Find the first character of the prefix...
+ for (
+ let index = 0;
+ (index = array.indexOf(prefix.charCodeAt(0), index)) != -1;
+ ++index
+ ) {
+ // Then ensure we actually have the whole prefix.
+ let found = true;
+ for (let i = 1; i < prefix.length; ++i) {
+ if (array[index + i] != prefix.charCodeAt(i)) {
+ found = false;
+ break;
+ }
+ }
+ if (!found) {
+ continue;
+ }
+
+ // C strings are null terminated, but " also terminates urls
+ // (nsIndexedToHTML.cpp contains an HTML fragment with several chrome urls)
+ // Let's also terminate the string on the # character to skip references.
+ let end = Math.min(
+ array.indexOf(0, index),
+ array.indexOf('"'.charCodeAt(0), index),
+ array.indexOf(")".charCodeAt(0), index),
+ array.indexOf("#".charCodeAt(0), index)
+ );
+ let string = "";
+ for (; index < end; ++index) {
+ string += String.fromCharCode(array[index]);
+ }
+
+ // Only keep strings that look like real chrome or resource urls.
+ if (
+ /chrome:\/\/[a-zA-Z09-]+\/(content|skin|locale)\//.test(string) ||
+ /resource:\/\/[a-zA-Z09-]*\/.*\.[a-z]+/.test(string)
+ ) {
+ gReferencesFromCode.set(string, null);
+ }
+ }
+}
+
+add_task(async function checkAllTheFiles() {
+ TestUtils.assertPackagedBuild();
+
+ const libxul = await IOUtils.read(PathUtils.xulLibraryPath);
+ findChromeUrlsFromArray(libxul, "chrome://");
+ findChromeUrlsFromArray(libxul, "resource://");
+ // Handle NS_LITERAL_STRING.
+ let uint16 = new Uint16Array(libxul.buffer);
+ findChromeUrlsFromArray(uint16, "chrome://");
+ findChromeUrlsFromArray(uint16, "resource://");
+
+ const kCodeExtensions = [
+ ".xml",
+ ".xsl",
+ ".mjs",
+ ".js",
+ ".jsm",
+ ".json",
+ ".html",
+ ".xhtml",
+ ];
+
+ 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",
+ ".jpg",
+ ".png",
+ ".gif",
+ ".svg",
+ ".ftl",
+ ".dtd",
+ ".properties",
+ ].concat(kCodeExtensions)
+ );
+
+ // 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 = [];
+ let jsonManifests = [];
+ uris = uris.filter(uri => {
+ let path = uri.pathQueryRef;
+ if (path.endsWith(".manifest")) {
+ manifestURIs.push(uri);
+ return false;
+ } else if (path.endsWith("/manifest.json")) {
+ jsonManifests.push(uri);
+ return false;
+ }
+
+ return true;
+ });
+
+ // Wait for all manifest to be parsed
+ await PerfTestHelpers.throttledMapPromises(manifestURIs, parseManifest);
+
+ for (let jsm of Components.manager.getComponentJSMs()) {
+ gReferencesFromCode.set(jsm, null);
+ }
+ for (let esModule of Components.manager.getComponentESModules()) {
+ gReferencesFromCode.set(esModule, null);
+ }
+
+ // manifest.json is a common name, it is used for WebExtension manifests
+ // but also for other things. To tell them apart, we have to actually
+ // read the contents. This will populate gExtensionRoots with all
+ // embedded extension APIs, and return any manifest.json files that aren't
+ // webextensions.
+ let nonWebextManifests = (
+ await Promise.all(jsonManifests.map(parseJsonManifest))
+ ).filter(uri => !!uri);
+ uris.push(...nonWebextManifests);
+
+ // We build a list of promises that get resolved when their respective
+ // files have loaded and produced no errors.
+ let allPromises = [];
+
+ for (let uri of uris) {
+ let path = uri.pathQueryRef;
+ if (path.endsWith(".css")) {
+ allPromises.push([parseCSSFile, uri]);
+ } else if (kCodeExtensions.some(ext => path.endsWith(ext))) {
+ allPromises.push([parseCodeFile, uri]);
+ }
+ }
+
+ // Wait for all the files to have actually loaded:
+ await PerfTestHelpers.throttledMapPromises(allPromises, ([task, uri]) =>
+ task(uri)
+ );
+
+ // Keep only chrome:// files, and filter out either the devtools paths or
+ // the non-devtools paths:
+ let devtoolsPrefixes = [
+ "chrome://devtools",
+ "resource://devtools/",
+ "resource://devtools-client-jsonview/",
+ "resource://devtools-client-shared/",
+ "resource://app/modules/devtools",
+ "resource://gre/modules/devtools",
+ "resource://app/localization/en-US/startup/aboutDevTools.ftl",
+ "resource://app/localization/en-US/devtools/",
+ ];
+ let hasDevtoolsPrefix = uri =>
+ devtoolsPrefixes.some(prefix => uri.startsWith(prefix));
+ let chromeFiles = [];
+ for (let uri of uris) {
+ uri = convertToCodeURI(uri.spec);
+ if (
+ (uri.startsWith("chrome://") || uri.startsWith("resource://")) &&
+ isDevtools == hasDevtoolsPrefix(uri)
+ ) {
+ chromeFiles.push(uri);
+ }
+ }
+
+ if (isDevtools) {
+ // chrome://devtools/skin/devtools-browser.css is included from browser.xhtml
+ gReferencesFromCode.set(AppConstants.BROWSER_CHROME_URL, null);
+ // devtools' css is currently included from browser.css, see bug 1204810.
+ gReferencesFromCode.set("chrome://browser/skin/browser.css", null);
+ }
+
+ let isUnreferenced = file => {
+ if (gExceptionPaths.some(e => file.startsWith(e))) {
+ return false;
+ }
+ if (gReferencesFromCode.has(file)) {
+ let refs = gReferencesFromCode.get(file);
+ if (refs === null) {
+ return false;
+ }
+ for (let ref of refs) {
+ if (isDevtools) {
+ if (
+ ref.startsWith("resource://app/components/") ||
+ (file.startsWith("chrome://") && ref.startsWith("resource://"))
+ ) {
+ return false;
+ }
+ }
+
+ if (gReferencesFromCode.has(ref)) {
+ let refType = gReferencesFromCode.get(ref);
+ if (
+ refType === null || // unconditionally referenced
+ refType == "whitelist" ||
+ refType == "whitelist-direct"
+ ) {
+ return false;
+ }
+ }
+ }
+ }
+ return !gOverrideMap.has(file) || isUnreferenced(gOverrideMap.get(file));
+ };
+
+ let unreferencedFiles = chromeFiles;
+
+ let removeReferenced = useWhitelist => {
+ let foundReference = false;
+ unreferencedFiles = unreferencedFiles.filter(f => {
+ let rv = isUnreferenced(f);
+ if (rv && f.startsWith("resource://app/")) {
+ rv = isUnreferenced(f.replace("resource://app/", "resource:///"));
+ }
+ if (rv && /^resource:\/\/(?:app|gre)\/components\/[^/]+\.js$/.test(f)) {
+ rv = !gComponentsSet.has(f.replace(/.*\//, ""));
+ }
+ if (!rv) {
+ foundReference = true;
+ if (useWhitelist) {
+ info(
+ "indirectly whitelisted file: " +
+ f +
+ " used from " +
+ listCodeReferences(gReferencesFromCode.get(f))
+ );
+ }
+ gReferencesFromCode.set(f, useWhitelist ? "whitelist" : null);
+ }
+ return rv;
+ });
+ return foundReference;
+ };
+ // First filter out the files that are referenced.
+ while (removeReferenced(false)) {
+ // As long as removeReferenced returns true, some files have been marked
+ // as referenced, so we need to run it again.
+ }
+ // Marked as referenced the files that have been explicitly whitelisted.
+ unreferencedFiles = unreferencedFiles.filter(file => {
+ if (whitelist.has(file)) {
+ whitelist.delete(file);
+ gReferencesFromCode.set(file, "whitelist-direct");
+ return false;
+ }
+ return true;
+ });
+ // Run the process again, this time when more files are marked as referenced,
+ // it's a consequence of the whitelist.
+ while (removeReferenced(true)) {
+ // As long as removeReferenced returns true, we need to run it again.
+ }
+
+ unreferencedFiles.sort();
+
+ if (isDevtools) {
+ // Bug 1351878 - handle devtools resource files
+ unreferencedFiles = unreferencedFiles.filter(file => {
+ if (file.startsWith("resource://")) {
+ info("unreferenced devtools resource file: " + file);
+ return false;
+ }
+ return true;
+ });
+ }
+
+ is(unreferencedFiles.length, 0, "there should be no unreferenced files");
+ for (let file of unreferencedFiles) {
+ let refs = gReferencesFromCode.get(file);
+ if (refs === undefined) {
+ ok(false, "unreferenced file: " + file);
+ } else {
+ let refList = listCodeReferences(refs);
+ let msg = "file only referenced from unreferenced files: " + file;
+ if (refList) {
+ msg += " referenced from " + refList;
+ }
+ ok(false, msg);
+ }
+ }
+
+ for (let file of whitelist) {
+ if (ignorableWhitelist.has(file)) {
+ info("ignored unused whitelist entry: " + file);
+ } else {
+ ok(false, "unused whitelist entry: " + file);
+ }
+ }
+
+ for (let [file, refs] of gReferencesFromCode) {
+ if (
+ isDevtools != devtoolsPrefixes.some(prefix => file.startsWith(prefix))
+ ) {
+ continue;
+ }
+
+ if (
+ (file.startsWith("chrome://") || file.startsWith("resource://")) &&
+ !(await chromeFileExists(file))
+ ) {
+ // Ignore chrome prefixes that have been automatically expanded.
+ let pathParts =
+ file.match("chrome://([^/]+)/content/([^/.]+).xul") ||
+ file.match("chrome://([^/]+)/skin/([^/.]+).css");
+ if (pathParts && pathParts[1] == pathParts[2]) {
+ continue;
+ }
+
+ // TODO: bug 1349010 - add a whitelist and make this reliable enough
+ // that we could make the test fail when this catches something new.
+ let refList = listCodeReferences(refs);
+ let msg = "missing file: " + file;
+ if (refList) {
+ msg += " referenced from " + refList;
+ }
+ info(msg);
+ }
+ }
+});
diff --git a/browser/base/content/test/static/browser_misused_characters_in_strings.js b/browser/base/content/test/static/browser_misused_characters_in_strings.js
new file mode 100644
index 0000000000..42be3b4392
--- /dev/null
+++ b/browser/base/content/test/static/browser_misused_characters_in_strings.js
@@ -0,0 +1,276 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* This list allows pre-existing or 'unfixable' issues to remain, while we
+ * detect newly occurring issues in shipping files. It is a list of objects
+ * specifying conditions under which an error should be ignored.
+ *
+ * As each issue is found in the exceptions list, it is removed from the list.
+ * At the end of the test, there is an assertion that all items have been
+ * removed from the exceptions list, thus ensuring there are no stale
+ * entries. */
+let gExceptionsList = [
+ {
+ file: "layout_errors.properties",
+ key: "ImageMapRectBoundsError",
+ type: "double-quote",
+ },
+ {
+ file: "layout_errors.properties",
+ key: "ImageMapCircleWrongNumberOfCoords",
+ type: "double-quote",
+ },
+ {
+ file: "layout_errors.properties",
+ key: "ImageMapCircleNegativeRadius",
+ type: "double-quote",
+ },
+ {
+ file: "layout_errors.properties",
+ key: "ImageMapPolyWrongNumberOfCoords",
+ type: "double-quote",
+ },
+ {
+ file: "layout_errors.properties",
+ key: "ImageMapPolyOddNumberOfCoords",
+ type: "double-quote",
+ },
+ {
+ file: "dom.properties",
+ key: "PatternAttributeCompileFailure",
+ type: "single-quote",
+ },
+ // dom.properties is packaged twice so we need to have two exceptions for this string.
+ {
+ file: "dom.properties",
+ key: "PatternAttributeCompileFailure",
+ type: "single-quote",
+ },
+ {
+ file: "dom.properties",
+ key: "ImportMapExternalNotSupported",
+ type: "single-quote",
+ },
+ // dom.properties is packaged twice so we need to have two exceptions for this string.
+ {
+ file: "dom.properties",
+ key: "ImportMapExternalNotSupported",
+ type: "single-quote",
+ },
+];
+
+/**
+ * Check if an error should be ignored due to matching one of the exceptions
+ * defined in gExceptionsList.
+ *
+ * @param filepath The URI spec of the locale file
+ * @param key The key of the entity that is being checked
+ * @param type The type of error that has been found
+ * @return true if the error should be ignored, false otherwise.
+ */
+function ignoredError(filepath, key, type) {
+ for (let index in gExceptionsList) {
+ let exceptionItem = gExceptionsList[index];
+ if (
+ filepath.endsWith(exceptionItem.file) &&
+ key == exceptionItem.key &&
+ type == exceptionItem.type
+ ) {
+ gExceptionsList.splice(index, 1);
+ return true;
+ }
+ }
+ return false;
+}
+
+function testForError(filepath, key, str, pattern, type, helpText) {
+ if (str.match(pattern) && !ignoredError(filepath, key, type)) {
+ ok(false, `${filepath} with key=${key} has a misused ${type}. ${helpText}`);
+ }
+}
+
+function testForErrors(filepath, key, str) {
+ testForError(
+ filepath,
+ key,
+ str,
+ /(\w|^)'\w/,
+ "apostrophe",
+ "Strings with apostrophes should use foo\u2019s instead of foo's."
+ );
+ testForError(
+ filepath,
+ key,
+ str,
+ /\w\u2018\w/,
+ "incorrect-apostrophe",
+ "Strings with apostrophes should use foo\u2019s instead of foo\u2018s."
+ );
+ testForError(
+ filepath,
+ key,
+ str,
+ /'.+'/,
+ "single-quote",
+ "Single-quoted strings should use Unicode \u2018foo\u2019 instead of 'foo'."
+ );
+ testForError(
+ filepath,
+ key,
+ str,
+ /"/,
+ "double-quote",
+ 'Double-quoted strings should use Unicode \u201cfoo\u201d instead of "foo".'
+ );
+ testForError(
+ filepath,
+ key,
+ str,
+ /\.\.\./,
+ "ellipsis",
+ "Strings with an ellipsis should use the Unicode \u2026 character instead of three periods."
+ );
+}
+
+async function getAllTheFiles(extension) {
+ let appDirGreD = Services.dirsvc.get("GreD", Ci.nsIFile);
+ let appDirXCurProcD = Services.dirsvc.get("XCurProcD", Ci.nsIFile);
+ if (appDirGreD.contains(appDirXCurProcD)) {
+ return generateURIsFromDirTree(appDirGreD, [extension]);
+ }
+ if (appDirXCurProcD.contains(appDirGreD)) {
+ return generateURIsFromDirTree(appDirXCurProcD, [extension]);
+ }
+ let urisGreD = await generateURIsFromDirTree(appDirGreD, [extension]);
+ let urisXCurProcD = await generateURIsFromDirTree(appDirXCurProcD, [
+ extension,
+ ]);
+ return Array.from(new Set(urisGreD.concat(urisXCurProcD)));
+}
+
+add_task(async function checkAllTheProperties() {
+ // 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 getAllTheFiles(".properties");
+ ok(
+ uris.length,
+ `Found ${uris.length} .properties files to scan for misused characters`
+ );
+
+ for (let uri of uris) {
+ let bundle = Services.strings.createBundle(uri.spec);
+
+ for (let entity of bundle.getSimpleEnumeration()) {
+ testForErrors(uri.spec, entity.key, entity.value);
+ }
+ }
+});
+
+var checkDTD = async function (aURISpec) {
+ let rawContents = await fetchFile(aURISpec);
+ // The regular expression below is adapted from:
+ // https://hg.mozilla.org/mozilla-central/file/68c0b7d6f16ce5bb023e08050102b5f2fe4aacd8/python/compare-locales/compare_locales/parser.py#l233
+ let entities = rawContents.match(
+ /<!ENTITY\s+([\w\.]*)\s+("[^"]*"|'[^']*')\s*>/g
+ );
+ if (!entities) {
+ // Some files have no entities defined.
+ return;
+ }
+ for (let entity of entities) {
+ let [, key, str] = entity.match(
+ /<!ENTITY\s+([\w\.]*)\s+("[^"]*"|'[^']*')\s*>/
+ );
+ // The matched string includes the enclosing quotation marks,
+ // we need to slice them off.
+ str = str.slice(1, -1);
+ testForErrors(aURISpec, key, str);
+ }
+};
+
+add_task(async function checkAllTheDTDs() {
+ let uris = await getAllTheFiles(".dtd");
+ ok(
+ uris.length,
+ `Found ${uris.length} .dtd files to scan for misused characters`
+ );
+ for (let uri of uris) {
+ await checkDTD(uri.spec);
+ }
+
+ // This support DTD file supplies a string with a newline to make sure
+ // the regex in checkDTD works correctly for that case.
+ let dtdLocation = gTestPath.replace(
+ /\/[^\/]*$/i,
+ "/bug1262648_string_with_newlines.dtd"
+ );
+ await checkDTD(dtdLocation);
+});
+
+add_task(async function checkAllTheFluents() {
+ let uris = await getAllTheFiles(".ftl");
+ let { FluentParser, Visitor } = ChromeUtils.import(
+ "resource://testing-common/FluentSyntax.jsm"
+ );
+
+ class TextElementVisitor extends Visitor {
+ constructor() {
+ super();
+ let domParser = new DOMParser();
+ domParser.forceEnableDTD();
+
+ this.domParser = domParser;
+ this.uri = null;
+ this.id = null;
+ this.attr = null;
+ }
+
+ visitMessage(node) {
+ this.id = node.id.name;
+ this.attr = null;
+ this.genericVisit(node);
+ }
+
+ visitTerm(node) {
+ this.id = node.id.name;
+ this.attr = null;
+ this.genericVisit(node);
+ }
+
+ visitAttribute(node) {
+ this.attr = node.id.name;
+ this.genericVisit(node);
+ }
+
+ get key() {
+ if (this.attr) {
+ return `${this.id}.${this.attr}`;
+ }
+ return this.id;
+ }
+
+ visitTextElement(node) {
+ const stripped_val = this.domParser.parseFromString(
+ "<!DOCTYPE html>" + node.value,
+ "text/html"
+ ).documentElement.textContent;
+ testForErrors(this.uri, this.key, stripped_val);
+ }
+ }
+
+ const ftlParser = new FluentParser({ withSpans: false });
+ const visitor = new TextElementVisitor();
+
+ for (let uri of uris) {
+ let rawContents = await fetchFile(uri.spec);
+ let ast = ftlParser.parse(rawContents);
+
+ visitor.uri = uri.spec;
+ visitor.visit(ast);
+ }
+});
+
+add_task(async function ensureExceptionsListIsEmpty() {
+ is(gExceptionsList.length, 0, "No remaining exceptions exist");
+});
diff --git a/browser/base/content/test/static/browser_parsable_css.js b/browser/base/content/test/static/browser_parsable_css.js
new file mode 100644
index 0000000000..6ff480fddc
--- /dev/null
+++ b/browser/base/content/test/static/browser_parsable_css.js
@@ -0,0 +1,590 @@
+/* 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 },
+ {
+ 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,
+ },
+ {
+ 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,
+ });
+}
+
+if (!Services.prefs.getBoolPref("layout.css.forced-color-adjust.enabled")) {
+ // PDF.js uses a property that is currently not enabled.
+ whitelist.push({
+ sourceName: /web\/viewer\.css$/i,
+ errorMessage:
+ /Unknown property ā€˜forced-color-adjustā€™\. {2}Declaration dropped\./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 },
+
+ // This variable is used from CSS embedded in JS in adjustableTitle.js
+ { propName: "--icon-url", isFromDevTools: false },
+
+ // These are referenced from devtools files.
+ {
+ propName: "--browser-stack-z-index-devtools-splitter",
+ isFromDevTools: false,
+ },
+ { propName: "--browser-stack-z-index-rdm-toolbar", 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 whitelistItem of whitelist) {
+ 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 + "nonexistentfile.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) {
+ const inThisPlatform = platform === AppConstants.platform;
+ for (const media of perPlatformMediaQueryMap[platform]) {
+ if (inThisPlatform && mediaList.mediaText == "not " + media) {
+ // This query can't match on this platform.
+ return true;
+ }
+ if (!inThisPlatform && mediaList.mediaText == media) {
+ // This query only matches on another platform that isn't ours.
+ return true;
+ }
+ }
+ }
+ return false;
+}
+
+function processCSSRules(container) {
+ for (let rule of container.cssRules) {
+ if (rule.media && neverMatches(rule.media)) {
+ continue;
+ }
+ if (rule.styleSheet) {
+ processCSSRules(rule.styleSheet); // @import
+ continue;
+ }
+ if (rule.cssRules) {
+ processCSSRules(rule); // @supports, @media, @layer (block), @keyframes
+ continue;
+ }
+ if (!rule.style) {
+ continue; // @layer (statement), @font-feature-values, @counter-style
+ }
+ // Extract urls from the css text.
+ // Note: CSSRule.style.cssText always has double quotes around URLs even
+ // when the original CSS file didn't.
+ let cssText = rule.style.cssText;
+ let urls = cssText.match(/url\("[^"]*"\)/g);
+ // Extract props by searching all "--" preceded by "var(" or a non-word
+ // character.
+ let props = 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 if needed.
+ if (prop[0] != "-") {
+ 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 PerfTestHelpers.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 PerfTestHelpers.throttledMapPromises(allPromises, loadCSS);
+
+ // Check if all the files referenced from CSS actually exist.
+ // Files in browser/ should never be referenced outside browser/.
+ for (let [image, references] of imageURIsToReferencesMap) {
+ if (!chromeFileExists(image)) {
+ for (let ref of references) {
+ ok(false, "missing " + image + " referenced from " + ref);
+ }
+ }
+
+ let imageHost = image.split("/")[2];
+ if (imageHost == "browser") {
+ for (let ref of references) {
+ let refHost = ref.split("/")[2];
+ if (!["activity-stream", "browser"].includes(refHost)) {
+ ok(
+ false,
+ "browser file " + image + " referenced outside browser in " + 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) {
+ ok(false, "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(whitelist);
+ checkWhitelist(propNameWhitelist);
+
+ // 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/browser/base/content/test/static/browser_parsable_script.js b/browser/base/content/test/static/browser_parsable_script.js
new file mode 100644
index 0000000000..982ef2f91a
--- /dev/null
+++ b/browser/base/content/test/static/browser_parsable_script.js
@@ -0,0 +1,167 @@
+/* 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);
+
+const kWhitelist = new Set([
+ /browser\/content\/browser\/places\/controller.js$/,
+]);
+
+const kESModuleList = new Set([
+ /browser\/lockwise-card.js$/,
+ /browser\/monitor-card.js$/,
+ /browser\/proxy-card.js$/,
+ /browser\/vpn-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
+ * @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) {
+ if (uri.filePath.endsWith(".mjs")) {
+ return true;
+ }
+
+ 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", ".mjs"]);
+ 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 PerfTestHelpers.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/browser/base/content/test/static/browser_sentence_case_strings.js b/browser/base/content/test/static/browser_sentence_case_strings.js
new file mode 100644
index 0000000000..e995f76b1a
--- /dev/null
+++ b/browser/base/content/test/static/browser_sentence_case_strings.js
@@ -0,0 +1,279 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * This test file checks that our en-US builds use sentence case strings
+ * where appropriate. It's not exhaustive - some panels will show different
+ * items in different states, and this test doesn't iterate all of them.
+ */
+
+/* global PanelUI */
+
+const { CustomizableUITestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/CustomizableUITestUtils.sys.mjs"
+);
+
+const { AppMenuNotifications } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppMenuNotifications.sys.mjs"
+);
+
+// These are brand names, proper names, or other things that we expect to
+// not abide exactly to sentence case. NAMES is for single words, and PHRASES
+// is for words in a specific order.
+const NAMES = new Set(["Mozilla", "Nightly", "Firefox"]);
+const PHRASES = new Set(["Troubleshoot Modeā€¦"]);
+
+let gCUITestUtils = new CustomizableUITestUtils(window);
+let gLocalization = new Localization(["browser/newtab/asrouter.ftl"], true);
+
+/**
+ * This recursive function will take the current main or subview, find all of
+ * the buttons that navigate to subviews inside it, and click each one
+ * individually. Upon entering the new view, we recurse. When the subviews
+ * within a view have been exhausted, we go back up a level.
+ *
+ * @generator
+ * @param {<xul:panelview>} parentView The view to start scanning for
+ * subviews.
+ * @yields {<xul:panelview>} Each found <xul:panelview>, in depth-first search
+ * order.
+ */
+async function* iterateSubviews(parentView) {
+ let navButtons = Array.from(
+ // Ensure that only enabled buttons are tested
+ parentView.querySelectorAll(".subviewbutton-nav:not([disabled])")
+ );
+ if (!navButtons) {
+ return;
+ }
+
+ for (let button of navButtons) {
+ info("Click " + button.id);
+ let panel = parentView.closest("panel");
+ let panelmultiview = parentView.closest("panelmultiview");
+ let promiseViewShown = BrowserTestUtils.waitForEvent(panel, "ViewShown");
+ button.click();
+ let viewShownEvent = await promiseViewShown;
+
+ yield viewShownEvent.originalTarget;
+
+ info("Shown " + viewShownEvent.originalTarget.id);
+ yield* iterateSubviews(viewShownEvent.originalTarget);
+ promiseViewShown = BrowserTestUtils.waitForEvent(parentView, "ViewShown");
+ panelmultiview.goBack();
+ await promiseViewShown;
+ }
+}
+
+/**
+ * Given a <xul:panelview>, look for <xul:toolbarbutton> descendants, extract
+ * any relevant strings from them, and check to see if they are in sentence
+ * case. By default, labels, textContent, and toolTipText (including dynamic
+ * toolTipText) are checked.
+ *
+ * @param {<xul:panelview>} view The <xul:panelview> to check.
+ */
+function checkToolbarButtons(view) {
+ let toolbarbuttons = view.querySelectorAll("toolbarbutton");
+ info("Checking toolbarbuttons in subview with id " + view.id);
+
+ for (let toolbarbutton of toolbarbuttons) {
+ let strings = [
+ toolbarbutton.label,
+ toolbarbutton.textContent,
+ toolbarbutton.toolTipText,
+ GetDynamicShortcutTooltipText(toolbarbutton.id),
+ ];
+ info("Checking toolbarbutton " + toolbarbutton.id);
+ for (let string of strings) {
+ checkSentenceCase(string, toolbarbutton.id);
+ }
+ }
+}
+
+function checkSubheaders(view) {
+ let subheaders = view.querySelectorAll("h2");
+ info("Checking subheaders in subview with id " + view.id);
+
+ for (let subheader of subheaders) {
+ checkSentenceCase(subheader.textContent, subheader.id);
+ }
+}
+
+async function checkUpdateBanner(view) {
+ let banner = view.querySelector("#appMenu-proton-update-banner");
+
+ const notifications = [
+ "update-downloading",
+ "update-available",
+ "update-manual",
+ "update-unsupported",
+ "update-restart",
+ ];
+
+ for (const notification of notifications) {
+ // Forcibly remove the label in order to wait for the new label.
+ banner.removeAttribute("label");
+
+ let labelPromise = BrowserTestUtils.waitForMutationCondition(
+ banner,
+ { attributes: true, attributeFilter: ["label"] },
+ () => !!banner.getAttribute("label")
+ );
+
+ AppMenuNotifications.showNotification(notification);
+
+ await labelPromise;
+
+ checkSentenceCase(banner.label, banner.id);
+
+ AppMenuNotifications.removeNotification(/.*/);
+ }
+}
+
+/**
+ * Asserts whether or not a string matches sentence case.
+ *
+ * @param {String} string The string to check for sentence case.
+ * @param {String} elementID The ID of the element being tested. This is
+ * mainly used for the assertion message to make it easier to debug
+ * failures, but items without IDs will not be checked (as these are
+ * likely using dynamic strings, like bookmarked page titles).
+ */
+function checkSentenceCase(string, elementID) {
+ if (!string || !elementID) {
+ return;
+ }
+
+ info("Testing string: " + string);
+
+ let words = string.trim().split(/\s+/);
+
+ // We expect that the first word is always capitalized. If it isn't,
+ // there's no need to keep checking the rest of the string, since we're
+ // going to fail the assertion.
+ let result = hasExpectedCapitalization(words[0], true);
+ if (result) {
+ for (let wordIndex = 1; wordIndex < words.length; ++wordIndex) {
+ let word = words[wordIndex];
+
+ if (word) {
+ if (isPartOfPhrase(words, wordIndex)) {
+ result = hasExpectedCapitalization(word, true);
+ } else {
+ let isName = NAMES.has(word);
+ result = hasExpectedCapitalization(word, isName);
+ }
+ if (!result) {
+ break;
+ }
+ }
+ }
+ }
+
+ Assert.ok(result, `${string} for ${elementID} should have sentence casing.`);
+}
+
+/**
+ * Returns true if a word is part of a phrase defined in the PHRASES set.
+ * The function will see if the word is contained within any of the defined
+ * PHRASES, and will then scan back and forward within the words array to
+ * to see if the word is indeed part of the phrase in context.
+ *
+ * @param {Array} words The full array of words being checked by the caller.
+ * @param {Number} wordIndex The index of the word being checked within the
+ * words array.
+ * @return {Boolean}
+ */
+function isPartOfPhrase(words, wordIndex) {
+ let word = words[wordIndex];
+
+ info(`Checking if ${word} is part of a phrase`);
+
+ for (let phrase of PHRASES) {
+ let phraseFragments = phrase.split(" ");
+ let fragmentIndex = phraseFragments.indexOf(word);
+
+ // If we didn't find the word within this phrase, the candidate phrase
+ // has more words than what we're analyzing, or the word doesn't have
+ // enough words before it to match the candidate phrase, then move on.
+ if (
+ fragmentIndex == -1 ||
+ words.length - phraseFragments.length < 0 ||
+ fragmentIndex > wordIndex
+ ) {
+ continue;
+ }
+
+ let wordsSlice = words.slice(
+ wordIndex - fragmentIndex,
+ wordIndex + phraseFragments.length
+ );
+ let matches = wordsSlice.every((w, index) => {
+ return phraseFragments[index] === w;
+ });
+
+ if (matches) {
+ info(`${word} is part of phrase ${phrase}`);
+ return true;
+ }
+ }
+
+ return false;
+}
+
+/**
+ * Tests that the strings under the AppMenu are in sentence case.
+ */
+add_task(async function test_sentence_case_appmenu() {
+ // Some of these panels are lazy, so it's necessary to open them in
+ // order for them to be inserted into the DOM.
+ await gCUITestUtils.openMainMenu();
+ registerCleanupFunction(async () => {
+ await gCUITestUtils.hideMainMenu();
+ });
+
+ checkToolbarButtons(PanelUI.mainView);
+ checkSubheaders(PanelUI.mainView);
+
+ for await (const view of iterateSubviews(PanelUI.mainView)) {
+ checkToolbarButtons(view);
+ checkSubheaders(view);
+ }
+
+ await checkUpdateBanner(PanelUI.mainView);
+});
+
+/**
+ * Tests that the strings under the All Tabs panel are in sentence case.
+ */
+add_task(async function test_sentence_case_all_tabs_panel() {
+ gTabsPanel.init();
+
+ const allTabsView = document.getElementById("allTabsMenu-allTabsView");
+ let allTabsPopupShownPromise = BrowserTestUtils.waitForEvent(
+ allTabsView,
+ "ViewShown"
+ );
+ gTabsPanel.showAllTabsPanel();
+ await allTabsPopupShownPromise;
+
+ registerCleanupFunction(async () => {
+ let allTabsPopupHiddenPromise = BrowserTestUtils.waitForEvent(
+ allTabsView.panelMultiView,
+ "PanelMultiViewHidden"
+ );
+ gTabsPanel.hideAllTabsPanel();
+ await allTabsPopupHiddenPromise;
+ });
+
+ checkToolbarButtons(gTabsPanel.allTabsView);
+ checkSubheaders(gTabsPanel.allTabsView);
+
+ for await (const view of iterateSubviews(gTabsPanel.allTabsView)) {
+ checkToolbarButtons(view);
+ checkSubheaders(view);
+ }
+});
diff --git a/browser/base/content/test/static/browser_title_case_menus.js b/browser/base/content/test/static/browser_title_case_menus.js
new file mode 100644
index 0000000000..9251db057b
--- /dev/null
+++ b/browser/base/content/test/static/browser_title_case_menus.js
@@ -0,0 +1,158 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * This test file checks that our en-US builds use APA-style Title Case strings
+ * where appropriate.
+ */
+
+// MINOR_WORDS are words that are okay to not be capitalized when they're
+// mid-string.
+//
+// Source: https://apastyle.apa.org/style-grammar-guidelines/capitalization/title-case
+const MINOR_WORDS = [
+ "a",
+ "an",
+ "and",
+ "as",
+ "at",
+ "but",
+ "by",
+ "for",
+ "if",
+ "in",
+ "nor",
+ "of",
+ "off",
+ "on",
+ "or",
+ "per",
+ "so",
+ "the",
+ "to",
+ "up",
+ "via",
+ "yet",
+];
+
+/**
+ * Returns a generator that will yield all of the <xul:menupopups>
+ * beneath <xul:menu> elements within a given <xul:menubar>. Each
+ * <xul:menupopup> will have the "popupshowing" and "popupshown"
+ * event fired on them to give them an opportunity to fully populate
+ * themselves before being yielded.
+ *
+ * @generator
+ * @param {<xul:menubar>} menubar The <xul:menubar> to get <xul:menupopup>s
+ * for.
+ * @yields {<xul:menupopup>} The next <xul:menupopup> under the <xul:menubar>.
+ */
+async function* iterateMenuPopups(menubar) {
+ let menus = menubar.querySelectorAll("menu");
+
+ for (let menu of menus) {
+ for (let menupopup of menu.querySelectorAll("menupopup")) {
+ // We fake the popupshowing and popupshown events to give the menupopups
+ // an opportunity to fully populate themselves. We don't actually open
+ // the menupopups because this is not possible on macOS.
+ menupopup.dispatchEvent(
+ new MouseEvent("popupshowing", { bubbles: true })
+ );
+ menupopup.dispatchEvent(new MouseEvent("popupshown", { bubbles: true }));
+
+ yield menupopup;
+
+ // Just for good measure, we'll fire the popuphiding/popuphidden events
+ // after we close the menupopups.
+ menupopup.dispatchEvent(new MouseEvent("popuphiding", { bubbles: true }));
+ menupopup.dispatchEvent(new MouseEvent("popuphidden", { bubbles: true }));
+ }
+ }
+}
+
+/**
+ * Given a <xul:menupopup>, checks all of the child elements with label
+ * properties to see if those labels are Title Cased. Skips any elements that
+ * have an empty or undefined label property.
+ *
+ * @param {<xul:menupopup>} menupopup The <xul:menupopup> to check.
+ */
+function checkMenuItems(menupopup) {
+ info("Checking menupopup with id " + menupopup.id);
+ for (let child of menupopup.children) {
+ if (child.label) {
+ info("Checking menupopup child with id " + child.id);
+ checkTitleCase(child.label, child.id);
+ }
+ }
+}
+
+/**
+ * Given a string, checks that the string is in Title Case.
+ *
+ * @param {String} string The string to check.
+ * @param {String} elementID The ID of the element associated with the string.
+ * This is included in the assertion message.
+ */
+function checkTitleCase(string, elementID) {
+ if (!string || !elementID /* document this */) {
+ return;
+ }
+
+ let words = string.trim().split(/\s+/);
+
+ // We extract the first word, and always expect it to be capitalized,
+ // even if it's a short word like one of MINOR_WORDS.
+ let firstWord = words.shift();
+ let result = hasExpectedCapitalization(firstWord, true);
+ if (result) {
+ for (let word of words) {
+ if (word) {
+ let expectCapitalized = !MINOR_WORDS.includes(word);
+ result = hasExpectedCapitalization(word, expectCapitalized);
+ if (!result) {
+ break;
+ }
+ }
+ }
+ }
+
+ Assert.ok(result, `${string} for ${elementID} should have Title Casing.`);
+}
+
+/**
+ * On Windows, macOS and GTK/KDE Linux, menubars are expected to be in Title
+ * Case in order to feel native. This test iterates the menuitem labels of the
+ * main menubar to ensure the en-US strings are all in Title Case.
+ *
+ * We use APA-style Title Case for the menubar, rather than Photon-style Title
+ * Case (https://design.firefox.com/photon/copy/capitalization.html) to match
+ * the native platform conventions.
+ */
+add_task(async function apa_test_title_case_menubar() {
+ let newWin = await BrowserTestUtils.openNewBrowserWindow();
+ let menuToolbar = newWin.document.getElementById("main-menubar");
+
+ for await (const menupopup of iterateMenuPopups(menuToolbar)) {
+ checkMenuItems(menupopup);
+ }
+
+ await BrowserTestUtils.closeWindow(newWin);
+});
+
+/**
+ * This test iterates the menuitem labels of the macOS dock menu for the
+ * application to ensure the en-US strings are all in Title Case.
+ */
+add_task(async function apa_test_title_case_macos_dock_menu() {
+ if (AppConstants.platform != "macosx") {
+ return;
+ }
+
+ let hiddenWindow = Services.appShell.hiddenDOMWindow;
+ Assert.ok(hiddenWindow, "Could get at hidden window");
+ let menupopup = hiddenWindow.document.getElementById("menu_mac_dockmenu");
+ checkMenuItems(menupopup);
+});
diff --git a/browser/base/content/test/static/bug1262648_string_with_newlines.dtd b/browser/base/content/test/static/bug1262648_string_with_newlines.dtd
new file mode 100644
index 0000000000..86cbefa5bd
--- /dev/null
+++ b/browser/base/content/test/static/bug1262648_string_with_newlines.dtd
@@ -0,0 +1,3 @@
+<!ENTITY foo.bar "This string
+contains
+newlines!">
diff --git a/browser/base/content/test/static/dummy_page.html b/browser/base/content/test/static/dummy_page.html
new file mode 100644
index 0000000000..1a87e28408
--- /dev/null
+++ b/browser/base/content/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/browser/base/content/test/static/head.js b/browser/base/content/test/static/head.js
new file mode 100644
index 0000000000..d9b978e853
--- /dev/null
+++ b/browser/base/content/test/static/head.js
@@ -0,0 +1,177 @@
+/* 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"
+);
+
+const IS_ALPHA = /^[a-z]+$/i;
+
+var { PerfTestHelpers } = ChromeUtils.importESModule(
+ "resource://testing-common/PerfTestHelpers.sys.mjs"
+);
+
+/**
+ * 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;
+ })();
+}
+
+/**
+ * Iterate over the children of |path| and find subdirectories and files with
+ * the given extension.
+ *
+ * This function recurses into ZIP and JAR archives as well.
+ *
+ * @param {string} path The path to check.
+ * @param {string[]} extensions The file extensions we're interested in.
+ *
+ * @returns {Promise<object>}
+ * A promise that resolves to an object containing the following
+ * 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)
+ */
+async function iterateOverPath(path, extensions) {
+ const children = await IOUtils.getChildren(path);
+
+ const files = [];
+ const subdirs = [];
+
+ for (const entry of children) {
+ let stat;
+ try {
+ stat = await IOUtils.stat(entry);
+ } catch (error) {
+ if (error.name === "NotFoundError") {
+ // Ignore symlinks from prior builds to subsequently removed files
+ continue;
+ }
+ throw error;
+ }
+
+ if (stat.type === "directory") {
+ subdirs.push(entry);
+ } else if (extensions.some(extension => entry.endsWith(extension))) {
+ if (await IOUtils.exists(entry)) {
+ const spec = PathUtils.toFileURI(entry);
+ files.push(Services.io.newURI(spec));
+ }
+ } else if (
+ entry.endsWith(".ja") ||
+ entry.endsWith(".jar") ||
+ entry.endsWith(".zip") ||
+ entry.endsWith(".xpi")
+ ) {
+ const file = new LocalFile(entry);
+ for (const extension of extensions) {
+ files.push(...generateEntriesFromJarFile(file, extension));
+ }
+ }
+ }
+
+ 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);
+ });
+}
+
+/**
+ * Returns whether or not a word (presumably in en-US) is capitalized per
+ * expectations.
+ *
+ * @param {String} word The single word to check.
+ * @param {boolean} expectCapitalized True if the word should be capitalized.
+ * @returns {boolean} True if the word matches the expected capitalization.
+ */
+function hasExpectedCapitalization(word, expectCapitalized) {
+ let firstChar = word[0];
+ if (!IS_ALPHA.test(firstChar)) {
+ return true;
+ }
+
+ let isCapitalized = firstChar == firstChar.toLocaleUpperCase("en-US");
+ return isCapitalized == expectCapitalized;
+}