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 });