browser_use_counters.js (11986B)
1 /* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; js-indent-level: 2 -*- */ 2 3 requestLongerTimeout(2); 4 5 const gHttpTestRoot = "https://example.com/browser/dom/base/test/"; 6 7 add_setup(async function test_initialize() { 8 await SpecialPowers.pushPrefEnv({ 9 set: [ 10 ["layout.css.use-counters.enabled", true], 11 ["layout.css.use-counters-unimplemented.enabled", true], 12 ], 13 }); 14 }); 15 16 async function grabCounters(counters, before) { 17 let result = { sentinel: await ensureData(before?.sentinel) }; 18 await Services.fog.testFlushAllChildren(); 19 result.gleanPage = Object.fromEntries( 20 counters.map(c => [ 21 c.name, 22 Glean[`useCounter${c.glean[0]}Page`][c.glean[1]].testGetValue() ?? 0, 23 ]) 24 ); 25 result.gleanDoc = Object.fromEntries( 26 counters.map(c => [ 27 c.name, 28 Glean[`useCounter${c.glean[0]}Doc`][c.glean[1]].testGetValue() ?? 0, 29 ]) 30 ); 31 result.glean_docs_destroyed = 32 Glean.useCounter.contentDocumentsDestroyed.testGetValue(); 33 result.glean_toplevel_destroyed = 34 Glean.useCounter.topLevelContentDocumentsDestroyed.testGetValue(); 35 return result; 36 } 37 38 function assertRange(before, after, key, range) { 39 before = before[key]; 40 after = after[key]; 41 let desc = key + " are correct"; 42 if (Array.isArray(range)) { 43 let [min, max] = range; 44 Assert.greaterOrEqual(after, before + min, desc); 45 Assert.lessOrEqual(after, before + max, desc); 46 } else { 47 Assert.equal(after, before + range, desc); 48 } 49 } 50 51 async function test_once( 52 { counters, toplevel_docs, docs, ignore_sentinel }, 53 callback 54 ) { 55 // Hold on to the current values of the data we're interested in. 56 // Opening an about:blank tab shouldn't change those. 57 let before = await grabCounters(counters); 58 59 await callback(); 60 61 let after = await grabCounters(counters, ignore_sentinel ? null : before); 62 63 // Compare before and after. 64 for (let counter of counters) { 65 let name = counter.name; 66 let value = counter.value ?? 1; 67 if (!counter.xfail) { 68 is( 69 after.gleanPage[name], 70 before.gleanPage[name] + value, 71 `Glean page counts for ${name} are correct` 72 ); 73 is( 74 after.gleanDoc[name], 75 before.gleanDoc[name] + value, 76 `Glean document counts for ${name} are correct` 77 ); 78 } 79 } 80 81 assertRange(before, after, "glean_toplevel_destroyed", toplevel_docs); 82 assertRange(before, after, "glean_docs_destroyed", docs); 83 } 84 85 add_task(async function test_page_counters() { 86 const TESTS = [ 87 // Check that use counters are incremented by SVGs loaded directly in iframes. 88 { 89 type: "iframe", 90 filename: "file_use_counter_svg_getElementById.svg", 91 counters: [ 92 { 93 name: "SVGSVGELEMENT_GETELEMENTBYID", 94 glean: ["", "svgsvgelementGetelementbyid"], 95 }, 96 ], 97 }, 98 { 99 type: "iframe", 100 filename: "file_use_counter_svg_currentScale.svg", 101 counters: [ 102 { 103 name: "SVGSVGELEMENT_CURRENTSCALE_getter", 104 glean: ["", "svgsvgelementCurrentscaleGetter"], 105 }, 106 { 107 name: "SVGSVGELEMENT_CURRENTSCALE_setter", 108 glean: ["", "svgsvgelementCurrentscaleSetter"], 109 }, 110 ], 111 }, 112 113 { 114 type: "iframe", 115 filename: "file_use_counter_style.html", 116 counters: [ 117 // Check for longhands. 118 { 119 name: "CSS_PROPERTY_BackgroundImage", 120 glean: ["Css", "cssBackgroundImage"], 121 }, 122 // Check for shorthands. 123 { name: "CSS_PROPERTY_Padding", glean: ["Css", "cssPadding"] }, 124 // Check for aliases. 125 { 126 name: "CSS_PROPERTY_MozAppearance", 127 glean: ["Css", "cssMozAppearance"], 128 }, 129 // Check for counted unknown properties. 130 { 131 name: "CSS_PROPERTY_WebkitPaddingStart", 132 glean: ["Css", "webkitPaddingStart"], 133 }, 134 ], 135 }, 136 137 // Check that even loads from the imglib cache update use counters. The 138 // images should still be there, because we just loaded them in the last 139 // set of tests. But we won't get updated counts for the document 140 // counters, because we won't be re-parsing the SVG documents. 141 { 142 type: "iframe", 143 filename: "file_use_counter_svg_getElementById.svg", 144 counters: [ 145 { 146 name: "SVGSVGELEMENT_GETELEMENTBYID", 147 glean: ["", "svgsvgelementGetelementbyid"], 148 }, 149 ], 150 }, 151 { 152 type: "iframe", 153 filename: "file_use_counter_svg_currentScale.svg", 154 counters: [ 155 { 156 name: "SVGSVGELEMENT_CURRENTSCALE_getter", 157 glean: ["", "svgsvgelementCurrentscaleGetter"], 158 }, 159 { 160 name: "SVGSVGELEMENT_CURRENTSCALE_setter", 161 glean: ["", "svgsvgelementCurrentscaleSetter"], 162 }, 163 ], 164 }, 165 166 // Check that use counters are incremented by SVGs loaded as images. 167 // Note that SVG images are not permitted to execute script, so we can only 168 // check for properties here. 169 { 170 type: "img", 171 filename: "file_use_counter_svg_getElementById.svg", 172 counters: [{ name: "CSS_PROPERTY_Fill", glean: ["Css", "cssFill"] }], 173 }, 174 { 175 type: "img", 176 filename: "file_use_counter_svg_currentScale.svg", 177 counters: [{ name: "CSS_PROPERTY_Fill", glean: ["Css", "cssFill"] }], 178 }, 179 180 // Check that use counters are incremented by directly loading SVGs 181 // that reference patterns defined in another SVG file. 182 { 183 type: "direct", 184 filename: "file_use_counter_svg_fill_pattern.svg", 185 counters: [ 186 { 187 name: "CSS_PROPERTY_FillOpacity", 188 glean: ["Css", "cssFillOpacity"], 189 xfail: true, 190 }, 191 ], 192 }, 193 194 // Check that use counters are incremented by directly loading SVGs 195 // that reference patterns defined in the same file or in data: URLs. 196 { 197 type: "direct", 198 filename: "file_use_counter_svg_fill_pattern_internal.svg", 199 counters: [ 200 { name: "CSS_PROPERTY_FillOpacity", glean: ["Css", "cssFillOpacity"] }, 201 ], 202 }, 203 204 // Check that use counters are incremented in a display:none iframe. 205 { 206 type: "undisplayed-iframe", 207 filename: "file_use_counter_svg_currentScale.svg", 208 counters: [ 209 { 210 name: "SVGSVGELEMENT_CURRENTSCALE_getter", 211 glean: ["", "svgsvgelementCurrentscaleGetter"], 212 }, 213 ], 214 }, 215 216 // Check that a document that comes out of the bfcache reports any new use 217 // counters recorded on it. 218 { 219 type: "direct", 220 filename: "file_use_counter_bfcache.html", 221 waitForExplicitFinish: true, 222 counters: [ 223 { 224 name: "SVGSVGELEMENT_GETELEMENTBYID", 225 glean: ["", "svgsvgelementGetelementbyid"], 226 }, 227 ], 228 }, 229 230 // // data: URLs don't correctly propagate to their referring document yet. 231 // { 232 // type: "direct", 233 // filename: "file_use_counter_svg_fill_pattern_data.svg", 234 // counters: [ 235 // { name: "PROPERTY_FILL_OPACITY" }, 236 // ], 237 // }, 238 ]; 239 240 for (let test of TESTS) { 241 let file = test.filename; 242 info(`checking ${file} (${test.type})`); 243 244 let options = { 245 counters: test.counters, 246 // bfcache test navigates a bunch of times and thus creates multiple top 247 // level document entries, as expected. Whether the last document is 248 // destroyed is a bit racy, see bug 1842800, so for now we allow it 249 // with +/- 1. 250 toplevel_docs: file == "file_use_counter_bfcache.html" ? [5, 6] : 1, 251 docs: [test.type == "img" ? 2 : 1, Infinity], 252 }; 253 254 await test_once(options, async function () { 255 // Load the test file in the new tab, either directly or via 256 // file_use_counter_outer{,_display_none}.html, depending on the test type. 257 let url, targetElement; 258 switch (test.type) { 259 case "iframe": 260 url = gHttpTestRoot + "file_use_counter_outer.html"; 261 targetElement = "content"; 262 break; 263 case "undisplayed-iframe": 264 url = gHttpTestRoot + "file_use_counter_outer_display_none.html"; 265 targetElement = "content"; 266 break; 267 case "img": 268 url = gHttpTestRoot + "file_use_counter_outer.html"; 269 targetElement = "display"; 270 break; 271 case "direct": 272 url = gHttpTestRoot + file; 273 targetElement = null; 274 break; 275 default: 276 throw `unexpected type ${test.type}`; 277 } 278 279 let waitForFinish = null; 280 if (test.waitForExplicitFinish) { 281 is( 282 test.type, 283 "direct", 284 `cannot use waitForExplicitFinish with test type ${test.type}` 285 ); 286 // Wait until the tab changes its hash to indicate it has finished. 287 waitForFinish = BrowserTestUtils.waitForLocationChange( 288 gBrowser, 289 url + "#finished" 290 ); 291 } 292 293 let newTab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url); 294 if (waitForFinish) { 295 await waitForFinish; 296 } 297 298 if (targetElement) { 299 // Inject our desired file into the target element of the newly-loaded page. 300 await SpecialPowers.spawn( 301 gBrowser.selectedBrowser, 302 [{ file, targetElement }], 303 function (opts) { 304 let target = content.document.getElementById(opts.targetElement); 305 target.src = opts.file; 306 307 return new Promise(resolve => { 308 let listener = event => { 309 event.target.removeEventListener("load", listener, true); 310 resolve(); 311 }; 312 target.addEventListener("load", listener, true); 313 }); 314 } 315 ); 316 } 317 318 // Tear down the page. 319 await BrowserTestUtils.removeTab(newTab); 320 }); 321 } 322 }); 323 324 add_task(async function test_extension_counters() { 325 let options = { 326 counters: [], 327 docs: 0, 328 toplevel_docs: 0, 329 ignore_sentinel: true, 330 }; 331 await test_once(options, async function () { 332 let extension = ExtensionTestUtils.loadExtension({ 333 manifest: { 334 page_action: { 335 default_popup: "page.html", 336 browser_style: false, 337 }, 338 }, 339 async background() { 340 let [tab] = await browser.tabs.query({ 341 active: true, 342 currentWindow: true, 343 }); 344 await browser.pageAction.show(tab.id); 345 browser.test.sendMessage("ready"); 346 }, 347 files: { 348 "page.html": `<!DOCTYPE html> 349 <meta charset="utf-8"> 350 <!-- Enough to trigger the use counter --> 351 <style>:root { opacity: .5 }</style> 352 `, 353 }, 354 }); 355 356 await extension.startup(); 357 info("Extension started up"); 358 359 await extension.awaitMessage("ready"); 360 361 await extension.unload(); 362 info("Extension unloaded"); 363 }); 364 }); 365 366 async function ensureData(prevSentinelValue = null) { 367 ok( 368 !prevSentinelValue || 369 ("page" in prevSentinelValue && "doc" in prevSentinelValue), 370 `Sentinel's valid: ${JSON.stringify(prevSentinelValue)}` 371 ); 372 // Unfortunately, document destruction (when use counter reporting happens) 373 // happens at some time later than the removal of the tab. 374 // To wait for the use counters to be reported, we repeatedly flush IPC and 375 // check for a change in the "sentinel" use counters 376 // `use.counter.css.{page|doc}.css_marker_mid`. 377 return BrowserTestUtils.waitForCondition( 378 async () => { 379 await Services.fog.testFlushAllChildren(); 380 return ( 381 !prevSentinelValue || 382 (prevSentinelValue?.page != 383 Glean.useCounterCssPage.cssMarkerMid.testGetValue() && 384 prevSentinelValue?.doc != 385 Glean.useCounterCssDoc.cssMarkerMid.testGetValue()) 386 ); 387 }, 388 "ensureData", 389 100, 390 Infinity 391 ).then( 392 () => ({ 393 doc: Glean.useCounterCssPage.cssMarkerMid.testGetValue(), 394 page: Glean.useCounterCssDoc.cssMarkerMid.testGetValue(), 395 }), 396 msg => { 397 throw msg; 398 } 399 ); 400 }