tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

browser_parsable_css.js (22873B)


      1 /* Any copyright is dedicated to the Public Domain.
      2 * http://creativecommons.org/publicdomain/zero/1.0/ */
      3 
      4 /* This list allows pre-existing or 'unfixable' CSS issues to remain, while we
      5 * detect newly occurring issues in shipping CSS. It is a list of objects
      6 * specifying conditions under which an error should be ignored.
      7 *
      8 * Every property of the objects in it needs to consist of a regular expression
      9 * matching the offending error. If an object has multiple regex criteria, they
     10 * ALL need to match an error in order for that error not to cause a test
     11 * failure. */
     12 let ignoreList = [
     13  // CodeMirror is imported as-is, see bug 1004423.
     14  { sourceName: /codemirror\.css$/i, isFromDevTools: true },
     15  // UA-only media features.
     16  {
     17    sourceName:
     18      /\b(contenteditable|EditorOverride|svg|forms|html|mathml|ua|scrollbars|xul)\.css$/i,
     19    errorMessage: /Unknown pseudo-class.*-moz-/i,
     20    isFromDevTools: false,
     21  },
     22  {
     23    sourceName:
     24      /\b(scrollbars|xul|html|mathml|ua|EditorOverride|contenteditable|forms|svg|manageDialog|formautofill)\.css$/i,
     25    errorMessage: /Unknown property.*-moz-/i,
     26    isFromDevTools: false,
     27  },
     28  // content: -moz-alt-content is UA-only.
     29  {
     30    sourceName: /\b(html)\.css$/i,
     31    errorMessage: /Error in parsing value for ‘content’/i,
     32    isFromDevTools: false,
     33  },
     34  // These variables are declared somewhere else, and error when we load the
     35  // files directly. They're all marked intermittent because their appearance
     36  // in the error console seems to not be consistent.
     37  {
     38    sourceName: /jsonview\/css\/general\.css$/i,
     39    intermittent: true,
     40    errorMessage: /Property contained reference to invalid variable.*color/i,
     41    isFromDevTools: true,
     42  },
     43  {
     44    sourceName: /web\/viewer\.css$/i,
     45    errorMessage:
     46      /Unknown property ‘text-size-adjust’\. {2}Declaration dropped\./i,
     47    isFromDevTools: false,
     48  },
     49 ];
     50 
     51 if (AppConstants.platform != "macosx") {
     52  ignoreList.push({
     53    errorMessage: /Unknown property.*-moz-osx-font-smoothing/i,
     54    isFromDevTools: false,
     55  });
     56 }
     57 
     58 if (!Services.prefs.getBoolPref("layout.css.zoom.enabled")) {
     59  ignoreList.push({
     60    sourceName: /\bscrollbars\.css$/i,
     61    errorMessage: /Error in parsing value for ‘zoom’/i,
     62    isFromDevTools: false,
     63  });
     64 }
     65 
     66 if (!Services.prefs.getBoolPref("layout.css.scroll-anchoring.enabled")) {
     67  ignoreList.push({
     68    sourceName: /webconsole\.css$/i,
     69    errorMessage: /Unknown property .*\boverflow-anchor\b/i,
     70    isFromDevTools: true,
     71  });
     72 }
     73 
     74 if (!Services.prefs.getBoolPref("layout.css.text-decoration-inset.enabled")) {
     75  ignoreList.push({
     76    sourceName: /html\.css$/i,
     77    errorMessage: /Unknown property .*text-decoration-inset/i,
     78    isFromDevTools: false,
     79  });
     80  ignoreList.push({
     81    sourceName: /ua\.css$/i,
     82    errorMessage: /Unknown property .*text-decoration-inset/i,
     83    isFromDevTools: false,
     84  });
     85 }
     86 
     87 if (!Services.prefs.getBoolPref("dom.viewTransitions.enabled")) {
     88  // view-transition selectors
     89  ignoreList.push({
     90    sourceName: /\b(ua)\.css$/i,
     91    errorMessage: /Unknown pseudo-class.*view-transition/i,
     92    isFromDevTools: false,
     93  });
     94  ignoreList.push({
     95    sourceName: /\b(ua)\.css$/i,
     96    errorMessage: /Unknown property.*view-transition/i,
     97    isFromDevTools: false,
     98  });
     99 }
    100 
    101 if (!Services.prefs.getBoolPref("mathml.math_shift.enabled")) {
    102  ignoreList.push({
    103    sourceName: /\bmathml\.css$/i,
    104    errorMessage: /Unknown property.*math-shift/i,
    105    isFromDevTools: false,
    106  });
    107 }
    108 
    109 let propNameAllowlist = [
    110  // These custom properties are retrieved directly from CSSOM
    111  // in videocontrols.xml to get pre-defined style instead of computed
    112  // dimensions, which is why they are not referenced by CSS.
    113  { propName: "--clickToPlay-width", isFromDevTools: false },
    114  { propName: "--playButton-width", isFromDevTools: false },
    115  { propName: "--muteButton-width", isFromDevTools: false },
    116  { propName: "--castingButton-width", isFromDevTools: false },
    117  { propName: "--closedCaptionButton-width", isFromDevTools: false },
    118  { propName: "--fullscreenButton-width", isFromDevTools: false },
    119  { propName: "--durationSpan-width", isFromDevTools: false },
    120  { propName: "--durationSpan-width-long", isFromDevTools: false },
    121  { propName: "--positionDurationBox-width", isFromDevTools: false },
    122  { propName: "--positionDurationBox-width-long", isFromDevTools: false },
    123 
    124  // These variables are used in a shorthand, but the CSS parser deletes the values
    125  // when expanding the shorthands. See https://github.com/w3c/csswg-drafts/issues/2515
    126  { propName: "--bezier-diagonal-color", isFromDevTools: true },
    127  { propName: "--highlighter-font-family", isFromDevTools: true },
    128 
    129  // This variable is used from CSS embedded in JS in adjustableTitle.js
    130  { propName: "--icon-url", isFromDevTools: false },
    131 
    132  // These are referenced from devtools files.
    133  {
    134    propName: "--browser-stack-z-index-devtools-splitter",
    135    isFromDevTools: false,
    136  },
    137  { propName: "--browser-stack-z-index-rdm-toolbar", isFromDevTools: false },
    138 
    139  // These variables are specified from devtools but read from non-devtools
    140  // styles, which confuses the test.
    141  { propName: "--panel-border-radius", isFromDevTools: true },
    142  { propName: "--panel-padding", isFromDevTools: true },
    143  { propName: "--panel-background", isFromDevTools: true },
    144  { propName: "--panel-border-color", isFromDevTools: true },
    145  { propName: "--panel-shadow", isFromDevTools: true },
    146  { propName: "--panel-shadow-margin", isFromDevTools: true },
    147 
    148  // These variables are set in host CSS but consumed in shadow DOM CSS
    149  // (content-search-handoff-ui component), which confuses the test.
    150  { propName: /^--content-search-handoff-ui-/, isFromDevTools: false },
    151 
    152  // These variables are used in JS in viewer.mjs (PDF.js).
    153  {
    154    propName: "--scale-round-x",
    155    isFromDevTools: false,
    156  },
    157  {
    158    propName: "--scale-round-y",
    159    isFromDevTools: false,
    160  },
    161 
    162  // These variables define accent colors for tab group chrome
    163  // and are used in JS in tabgroup.js
    164  { propName: "--tab-group-color-blue", isFromDevTools: false },
    165  { propName: "--tab-group-color-blue-invert", isFromDevTools: false },
    166  { propName: "--tab-group-color-blue-pale", isFromDevTools: false },
    167 
    168  { propName: "--tab-group-color-purple", isFromDevTools: false },
    169  { propName: "--tab-group-color-purple-invert", isFromDevTools: false },
    170  { propName: "--tab-group-color-purple-pale", isFromDevTools: false },
    171 
    172  { propName: "--tab-group-color-cyan", isFromDevTools: false },
    173  { propName: "--tab-group-color-cyan-invert", isFromDevTools: false },
    174  { propName: "--tab-group-color-cyan-pale", isFromDevTools: false },
    175 
    176  { propName: "--tab-group-color-orange", isFromDevTools: false },
    177  { propName: "--tab-group-color-orange-invert", isFromDevTools: false },
    178  { propName: "--tab-group-color-orange-pale", isFromDevTools: false },
    179 
    180  { propName: "--tab-group-color-yellow", isFromDevTools: false },
    181  { propName: "--tab-group-color-yellow-invert", isFromDevTools: false },
    182  { propName: "--tab-group-color-yellow-pale", isFromDevTools: false },
    183 
    184  { propName: "--tab-group-color-pink", isFromDevTools: false },
    185  { propName: "--tab-group-color-pink-invert", isFromDevTools: false },
    186  { propName: "--tab-group-color-pink-pale", isFromDevTools: false },
    187 
    188  { propName: "--tab-group-color-green", isFromDevTools: false },
    189  { propName: "--tab-group-color-green-invert", isFromDevTools: false },
    190  { propName: "--tab-group-color-green-pale", isFromDevTools: false },
    191 
    192  { propName: "--tab-group-color-red", isFromDevTools: false },
    193  { propName: "--tab-group-color-red-invert", isFromDevTools: false },
    194  { propName: "--tab-group-color-red-pale", isFromDevTools: false },
    195 
    196  { propName: "--tab-group-color-gray", isFromDevTools: false },
    197  { propName: "--tab-group-color-gray-invert", isFromDevTools: false },
    198  { propName: "--tab-group-color-gray-pale", isFromDevTools: false },
    199 
    200  /* Allow design tokens in devtools without all variables being used there */
    201  { sourceName: /\/design-system\/tokens-.*\.css$/, isFromDevTools: true },
    202 
    203  // Ignore token properties that follow the patterns --color-[name], --color-[name]-[number], or --color-[name]-alpha-[number]
    204  // This enables us to provide our full color palette for developers.
    205  { propName: /--color-[a-z]+(-alpha)?(-\d+)?/, isFromDevTools: false },
    206 ];
    207 
    208 // Add suffix to stylesheets' URI so that we always load them here and
    209 // have them parsed. Add a random number so that even if we run this
    210 // test multiple times, it would be unlikely to affect each other.
    211 const kPathSuffix = "?always-parse-css-" + Math.random();
    212 
    213 function dumpAllowlistItem(item) {
    214  return JSON.stringify(item, (key, value) => {
    215    return value instanceof RegExp ? value.toString() : value;
    216  });
    217 }
    218 
    219 /**
    220 * Check if an error should be ignored due to matching one of the allowlist
    221 * objects.
    222 *
    223 * @param aErrorObject the error to check
    224 * @return true if the error should be ignored, false otherwise.
    225 */
    226 function ignoredError(aErrorObject) {
    227  for (let allowlistItem of ignoreList) {
    228    let matches = true;
    229    let catchAll = true;
    230    for (let prop of ["sourceName", "errorMessage"]) {
    231      if (allowlistItem.hasOwnProperty(prop)) {
    232        catchAll = false;
    233        if (!allowlistItem[prop].test(aErrorObject[prop] || "")) {
    234          matches = false;
    235          break;
    236        }
    237      }
    238    }
    239    if (catchAll) {
    240      ok(
    241        false,
    242        "An allowlist item is catching all errors. " +
    243          dumpAllowlistItem(allowlistItem)
    244      );
    245      continue;
    246    }
    247    if (matches) {
    248      allowlistItem.used = true;
    249      let { sourceName, errorMessage } = aErrorObject;
    250      info(
    251        `Ignored error "${errorMessage}" on ${sourceName} ` +
    252          "because of allowlist item " +
    253          dumpAllowlistItem(allowlistItem)
    254      );
    255      return true;
    256    }
    257  }
    258  return false;
    259 }
    260 
    261 var gChromeReg = Cc["@mozilla.org/chrome/chrome-registry;1"].getService(
    262  Ci.nsIChromeRegistry
    263 );
    264 var gChromeMap = new Map();
    265 
    266 var resHandler = Services.io
    267  .getProtocolHandler("resource")
    268  .QueryInterface(Ci.nsIResProtocolHandler);
    269 var gResourceMap = [];
    270 function trackResourcePrefix(prefix) {
    271  let uri = Services.io.newURI("resource://" + prefix + "/");
    272  gResourceMap.unshift([prefix, resHandler.resolveURI(uri)]);
    273 }
    274 trackResourcePrefix("gre");
    275 trackResourcePrefix("app");
    276 
    277 function getBaseUriForChromeUri(chromeUri) {
    278  let chromeFile = chromeUri + "nonexistentfile.reallynothere";
    279  let uri = Services.io.newURI(chromeFile);
    280  let fileUri = gChromeReg.convertChromeURL(uri);
    281  return fileUri.resolve(".");
    282 }
    283 
    284 function parseManifest(manifestUri) {
    285  return fetchFile(manifestUri.spec).then(data => {
    286    for (let line of data.split("\n")) {
    287      let [type, ...argv] = line.split(/\s+/);
    288      if (type == "content" || type == "skin") {
    289        let chromeUri = `chrome://${argv[0]}/${type}/`;
    290        gChromeMap.set(getBaseUriForChromeUri(chromeUri), chromeUri);
    291      } else if (type == "resource") {
    292        trackResourcePrefix(argv[0]);
    293      }
    294    }
    295  });
    296 }
    297 
    298 function convertToCodeURI(fileUri) {
    299  let baseUri = fileUri;
    300  let path = "";
    301  while (true) {
    302    let slashPos = baseUri.lastIndexOf("/", baseUri.length - 2);
    303    if (slashPos <= 0) {
    304      // File not accessible from chrome protocol, try resource://
    305      for (let res of gResourceMap) {
    306        if (fileUri.startsWith(res[1])) {
    307          return fileUri.replace(res[1], "resource://" + res[0] + "/");
    308        }
    309      }
    310      // Give up and return the original URL.
    311      return fileUri;
    312    }
    313    path = baseUri.slice(slashPos + 1) + path;
    314    baseUri = baseUri.slice(0, slashPos + 1);
    315    if (gChromeMap.has(baseUri)) {
    316      return gChromeMap.get(baseUri) + path;
    317    }
    318  }
    319 }
    320 
    321 function messageIsCSSError(msg) {
    322  // Only care about CSS errors generated by our iframe:
    323  if (
    324    msg instanceof Ci.nsIScriptError &&
    325    msg.category.includes("CSS") &&
    326    msg.sourceName.endsWith(kPathSuffix)
    327  ) {
    328    let sourceName = msg.sourceName.slice(0, -kPathSuffix.length);
    329    let msgInfo = { sourceName, errorMessage: msg.errorMessage };
    330    // Check if this error is allowlisted in allowlist
    331    if (!ignoredError(msgInfo)) {
    332      ok(false, `Got error message for ${sourceName}: ${msg.errorMessage}`);
    333      return true;
    334    }
    335  }
    336  return false;
    337 }
    338 
    339 let imageURIsToReferencesMap = new Map();
    340 let customPropsToReferencesMap = new Map();
    341 let customPropsDefinitionFileMap = new Map();
    342 
    343 function neverMatches(mediaList) {
    344  const perPlatformMediaQueryMap = {
    345    macosx: ["(-moz-platform: macos)"],
    346    win: ["(-moz-platform: windows)"],
    347    linux: ["(-moz-platform: linux)"],
    348    android: ["(-moz-platform: android)"],
    349  };
    350  for (let platform in perPlatformMediaQueryMap) {
    351    const inThisPlatform = platform === AppConstants.platform;
    352    for (const media of perPlatformMediaQueryMap[platform]) {
    353      if (inThisPlatform && mediaList.mediaText == "not " + media) {
    354        // This query can't match on this platform.
    355        return true;
    356      }
    357      if (!inThisPlatform && mediaList.mediaText == media) {
    358        // This query only matches on another platform that isn't ours.
    359        return true;
    360      }
    361    }
    362  }
    363  return false;
    364 }
    365 
    366 function processCSSRules(container) {
    367  for (let rule of container.cssRules) {
    368    if (rule.media && neverMatches(rule.media)) {
    369      continue;
    370    }
    371    if (rule.styleSheet) {
    372      processCSSRules(rule.styleSheet); // @import
    373      continue;
    374    }
    375    if (rule.cssRules) {
    376      processCSSRules(rule); // @supports, @media, @layer (block), @keyframes, style rules with nested rules.
    377    }
    378    if (!rule.style) {
    379      continue; // @layer (statement), @font-feature-values, @counter-style
    380    }
    381    // Extract urls from the css text.
    382    // Note: CSSRule.style.cssText always has double quotes around URLs even
    383    //       when the original CSS file didn't.
    384    let cssText = rule.style.cssText;
    385    let urls = cssText.match(/url\("[^"]*"\)/g);
    386    // Extract props by searching all "--" preceded by "var(" or a non-word
    387    // character.
    388    let props = cssText.match(/(var\(\s*|\W|^)(--[\w\-]+)/g);
    389    if (!urls && !props) {
    390      continue;
    391    }
    392 
    393    for (let url of urls || []) {
    394      // Remove the url(" prefix and the ") suffix.
    395      url = url.replace(/url\("(.*)"\)/, "$1");
    396      if (url.startsWith("data:")) {
    397        continue;
    398      }
    399 
    400      // Make the url absolute and remove the ref.
    401      let baseURI = Services.io.newURI(rule.parentStyleSheet.href);
    402      url = Services.io.newURI(url, null, baseURI).specIgnoringRef;
    403 
    404      // Store the image url along with the css file referencing it.
    405      let baseUrl = baseURI.spec.split("?always-parse-css")[0];
    406      if (!imageURIsToReferencesMap.has(url)) {
    407        imageURIsToReferencesMap.set(url, new Set([baseUrl]));
    408      } else {
    409        imageURIsToReferencesMap.get(url).add(baseUrl);
    410      }
    411    }
    412 
    413    for (let prop of props || []) {
    414      if (prop.startsWith("var(")) {
    415        prop = prop.substring(4).trim();
    416        let prevValue = customPropsToReferencesMap.get(prop) || 0;
    417        customPropsToReferencesMap.set(prop, prevValue + 1);
    418      } else {
    419        // Remove the extra non-word character captured by the regular
    420        // expression if needed.
    421        if (prop[0] != "-") {
    422          prop = prop.substring(1);
    423        }
    424        if (!customPropsToReferencesMap.has(prop)) {
    425          customPropsToReferencesMap.set(prop, undefined);
    426          if (!customPropsDefinitionFileMap.has(prop)) {
    427            customPropsDefinitionFileMap.set(prop, new Set());
    428          }
    429          customPropsDefinitionFileMap
    430            .get(prop)
    431            .add(container.href || container.parentStyleSheet.href);
    432        }
    433      }
    434    }
    435  }
    436 }
    437 
    438 function chromeFileExists(aURI) {
    439  let available = 0;
    440  try {
    441    let channel = NetUtil.newChannel({
    442      uri: aURI,
    443      loadUsingSystemPrincipal: true,
    444    });
    445    let stream = channel.open();
    446    let sstream = Cc["@mozilla.org/scriptableinputstream;1"].createInstance(
    447      Ci.nsIScriptableInputStream
    448    );
    449    sstream.init(stream);
    450    available = sstream.available();
    451    sstream.close();
    452  } catch (e) {
    453    if (e.result != Cr.NS_ERROR_FILE_NOT_FOUND) {
    454      dump("Checking " + aURI + ": " + e + "\n");
    455      console.error(e);
    456    }
    457  }
    458  return available > 0;
    459 }
    460 
    461 function shouldIgnorePropSource(item, prop) {
    462  if (!item.sourceName || !customPropsDefinitionFileMap.has(prop)) {
    463    return false;
    464  }
    465  return customPropsDefinitionFileMap
    466    .get(prop)
    467    .values()
    468    .some(f => item.sourceName.test(f));
    469 }
    470 
    471 function shouldIgnorePropPattern(item, prop) {
    472  if (!item.propName || !(item.propName instanceof RegExp)) {
    473    return false;
    474  }
    475  return item.propName.test(prop);
    476 }
    477 
    478 add_task(async function checkAllTheCSS() {
    479  // Since we later in this test use Services.console.getMessageArray(),
    480  // better to not have some messages from previous tests in the array.
    481  Services.console.reset();
    482 
    483  let appDir = Services.dirsvc.get("GreD", Ci.nsIFile);
    484  // This asynchronously produces a list of URLs (sadly, mostly sync on our
    485  // test infrastructure because it runs against jarfiles there, and
    486  // our zipreader APIs are all sync)
    487  let uris = await generateURIsFromDirTree(appDir, [".css", ".manifest"]);
    488 
    489  // Create a clean iframe to load all the files into. This needs to live at a
    490  // chrome URI so that it's allowed to load and parse any styles.
    491  let testFile = getRootDirectory(gTestPath) + "dummy_page.html";
    492  let { HiddenFrame } = ChromeUtils.importESModule(
    493    "resource://gre/modules/HiddenFrame.sys.mjs"
    494  );
    495  let hiddenFrame = new HiddenFrame();
    496  let win = await hiddenFrame.get();
    497  let iframe = win.document.createElementNS(
    498    "http://www.w3.org/1999/xhtml",
    499    "html:iframe"
    500  );
    501  win.document.documentElement.appendChild(iframe);
    502  let iframeLoaded = BrowserTestUtils.waitForEvent(iframe, "load", true);
    503  iframe.contentWindow.location = testFile;
    504  await iframeLoaded;
    505  let doc = iframe.contentWindow.document;
    506  iframe.contentWindow.docShell.cssErrorReportingEnabled = true;
    507 
    508  // Parse and remove all manifests from the list.
    509  // NOTE that this must be done before filtering out devtools paths
    510  // so that all chrome paths can be recorded.
    511  let manifestURIs = [];
    512  uris = uris.filter(uri => {
    513    if (uri.pathQueryRef.endsWith(".manifest")) {
    514      manifestURIs.push(uri);
    515      return false;
    516    }
    517    return true;
    518  });
    519  // Wait for all manifest to be parsed
    520  await PerfTestHelpers.throttledMapPromises(manifestURIs, parseManifest);
    521 
    522  // filter out either the devtools paths or the non-devtools paths:
    523  let isDevtools = SimpleTest.harnessParameters.subsuite == "devtools";
    524  let devtoolsPathBits = ["devtools"];
    525  uris = uris.filter(
    526    uri => isDevtools == devtoolsPathBits.some(path => uri.spec.includes(path))
    527  );
    528 
    529  let loadCSS = chromeUri =>
    530    new Promise(resolve => {
    531      let linkEl, onLoad, onError;
    532      onLoad = () => {
    533        processCSSRules(linkEl.sheet);
    534        resolve();
    535        linkEl.removeEventListener("load", onLoad);
    536        linkEl.removeEventListener("error", onError);
    537      };
    538      onError = () => {
    539        ok(
    540          false,
    541          "Loading " + linkEl.getAttribute("href") + " threw an error!"
    542        );
    543        resolve();
    544        linkEl.removeEventListener("load", onLoad);
    545        linkEl.removeEventListener("error", onError);
    546      };
    547      linkEl = doc.createElement("link");
    548      linkEl.setAttribute("rel", "stylesheet");
    549      linkEl.setAttribute("type", "text/css");
    550      linkEl.addEventListener("load", onLoad);
    551      linkEl.addEventListener("error", onError);
    552      linkEl.setAttribute("href", chromeUri + kPathSuffix);
    553      doc.head.appendChild(linkEl);
    554    });
    555 
    556  // We build a list of promises that get resolved when their respective
    557  // files have loaded and produced no errors.
    558  const kInContentCommonCSS = "chrome://global/skin/in-content/common.css";
    559  let allPromises = uris
    560    .map(uri => convertToCodeURI(uri.spec))
    561    .filter(uri => uri !== kInContentCommonCSS);
    562 
    563  // Make sure chrome://global/skin/in-content/common.css is loaded before other
    564  // stylesheets in order to guarantee the --in-content variables can be
    565  // correctly referenced.
    566  if (allPromises.length !== uris.length) {
    567    await loadCSS(kInContentCommonCSS);
    568  }
    569 
    570  // Wait for all the files to have actually loaded:
    571  await PerfTestHelpers.throttledMapPromises(allPromises, loadCSS);
    572 
    573  // Check if all the files referenced from CSS actually exist.
    574  // Files in browser/ should never be referenced outside browser/.
    575  for (let [image, references] of imageURIsToReferencesMap) {
    576    if (!chromeFileExists(image)) {
    577      for (let ref of references) {
    578        ok(false, "missing " + image + " referenced from " + ref);
    579      }
    580    }
    581 
    582    let imageHost = image.split("/")[2];
    583    if (imageHost == "browser") {
    584      for (let ref of references) {
    585        let refHost = ref.split("/")[2];
    586        if (!["builtin-addons", "newtab", "browser"].includes(refHost)) {
    587          ok(
    588            false,
    589            "browser file " + image + " referenced outside browser in " + ref
    590          );
    591        }
    592      }
    593    }
    594  }
    595 
    596  // Check if all the properties that are defined are referenced.
    597  for (let [prop, refCount] of customPropsToReferencesMap) {
    598    if (!refCount) {
    599      let ignored = false;
    600      for (let item of propNameAllowlist) {
    601        if (
    602          isDevtools == item.isFromDevTools &&
    603          (item.propName == prop ||
    604            shouldIgnorePropPattern(item, prop) ||
    605            shouldIgnorePropSource(item, prop))
    606        ) {
    607          item.used = true;
    608          if (
    609            !item.platforms ||
    610            item.platforms.includes(AppConstants.platform)
    611          ) {
    612            ignored = true;
    613          }
    614          break;
    615        }
    616      }
    617      if (!ignored) {
    618        ok(false, "custom property `" + prop + "` is not referenced");
    619      }
    620    }
    621  }
    622 
    623  let messages = Services.console.getMessageArray();
    624  // Count errors (the test output will list actual issues for us, as well
    625  // as the ok(false) in messageIsCSSError.
    626  let errors = messages.filter(messageIsCSSError);
    627  is(
    628    errors.length,
    629    0,
    630    "All the styles (" + allPromises.length + ") loaded without errors."
    631  );
    632 
    633  // Confirm that all allowlist rules have been used.
    634  function checkAllowlist(list) {
    635    for (let item of list) {
    636      if (
    637        !item.used &&
    638        isDevtools == item.isFromDevTools &&
    639        (!item.platforms || item.platforms.includes(AppConstants.platform)) &&
    640        !item.intermittent
    641      ) {
    642        ok(false, "Unused allowlist item: " + dumpAllowlistItem(item));
    643      }
    644    }
    645  }
    646  checkAllowlist(ignoreList);
    647  checkAllowlist(propNameAllowlist);
    648 
    649  // Clean up to avoid leaks:
    650  doc.head.innerHTML = "";
    651  doc = null;
    652  iframe.remove();
    653  iframe = null;
    654  win = null;
    655  hiddenFrame.destroy();
    656  hiddenFrame = null;
    657  imageURIsToReferencesMap = null;
    658  customPropsToReferencesMap = null;
    659 });