/* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; js-indent-level: 2 -*- */ requestLongerTimeout(2); const gHttpTestRoot = "https://example.com/browser/dom/base/test/"; add_setup(async function test_initialize() { await SpecialPowers.pushPrefEnv({ set: [ ["layout.css.use-counters.enabled", true], ["layout.css.use-counters-unimplemented.enabled", true], ], }); }); async function grabCounters(counters, before) { let result = { sentinel: await ensureData(before?.sentinel) }; await Services.fog.testFlushAllChildren(); result.gleanPage = Object.fromEntries( counters.map(c => [ c.name, Glean[`useCounter${c.glean[0]}Page`][c.glean[1]].testGetValue() ?? 0, ]) ); result.gleanDoc = Object.fromEntries( counters.map(c => [ c.name, Glean[`useCounter${c.glean[0]}Doc`][c.glean[1]].testGetValue() ?? 0, ]) ); result.glean_docs_destroyed = Glean.useCounter.contentDocumentsDestroyed.testGetValue(); result.glean_toplevel_destroyed = Glean.useCounter.topLevelContentDocumentsDestroyed.testGetValue(); return result; } function assertRange(before, after, key, range) { before = before[key]; after = after[key]; let desc = key + " are correct"; if (Array.isArray(range)) { let [min, max] = range; Assert.greaterOrEqual(after, before + min, desc); Assert.lessOrEqual(after, before + max, desc); } else { Assert.equal(after, before + range, desc); } } async function test_once( { counters, toplevel_docs, docs, ignore_sentinel }, callback ) { // Hold on to the current values of the data we're interested in. // Opening an about:blank tab shouldn't change those. let before = await grabCounters(counters); await callback(); let after = await grabCounters(counters, ignore_sentinel ? null : before); // Compare before and after. for (let counter of counters) { let name = counter.name; let value = counter.value ?? 1; if (!counter.xfail) { is( after.gleanPage[name], before.gleanPage[name] + value, `Glean page counts for ${name} are correct` ); is( after.gleanDoc[name], before.gleanDoc[name] + value, `Glean document counts for ${name} are correct` ); } } assertRange(before, after, "glean_toplevel_destroyed", toplevel_docs); assertRange(before, after, "glean_docs_destroyed", docs); } add_task(async function test_page_counters() { const TESTS = [ // Check that use counters are incremented by SVGs loaded directly in iframes. { type: "iframe", filename: "file_use_counter_svg_getElementById.svg", counters: [ { name: "SVGSVGELEMENT_GETELEMENTBYID", glean: ["", "svgsvgelementGetelementbyid"], }, ], }, { type: "iframe", filename: "file_use_counter_svg_currentScale.svg", counters: [ { name: "SVGSVGELEMENT_CURRENTSCALE_getter", glean: ["", "svgsvgelementCurrentscaleGetter"], }, { name: "SVGSVGELEMENT_CURRENTSCALE_setter", glean: ["", "svgsvgelementCurrentscaleSetter"], }, ], }, { type: "iframe", filename: "file_use_counter_style.html", counters: [ // Check for longhands. { name: "CSS_PROPERTY_BackgroundImage", glean: ["Css", "cssBackgroundImage"], }, // Check for shorthands. { name: "CSS_PROPERTY_Padding", glean: ["Css", "cssPadding"] }, // Check for aliases. { name: "CSS_PROPERTY_MozAppearance", glean: ["Css", "cssMozAppearance"], }, // Check for counted unknown properties. { name: "CSS_PROPERTY_WebkitPaddingStart", glean: ["Css", "webkitPaddingStart"], }, ], }, // Check that even loads from the imglib cache update use counters. The // images should still be there, because we just loaded them in the last // set of tests. But we won't get updated counts for the document // counters, because we won't be re-parsing the SVG documents. { type: "iframe", filename: "file_use_counter_svg_getElementById.svg", counters: [ { name: "SVGSVGELEMENT_GETELEMENTBYID", glean: ["", "svgsvgelementGetelementbyid"], }, ], }, { type: "iframe", filename: "file_use_counter_svg_currentScale.svg", counters: [ { name: "SVGSVGELEMENT_CURRENTSCALE_getter", glean: ["", "svgsvgelementCurrentscaleGetter"], }, { name: "SVGSVGELEMENT_CURRENTSCALE_setter", glean: ["", "svgsvgelementCurrentscaleSetter"], }, ], }, // Check that use counters are incremented by SVGs loaded as images. // Note that SVG images are not permitted to execute script, so we can only // check for properties here. { type: "img", filename: "file_use_counter_svg_getElementById.svg", counters: [{ name: "CSS_PROPERTY_Fill", glean: ["Css", "cssFill"] }], }, { type: "img", filename: "file_use_counter_svg_currentScale.svg", counters: [{ name: "CSS_PROPERTY_Fill", glean: ["Css", "cssFill"] }], }, // Check that use counters are incremented by directly loading SVGs // that reference patterns defined in another SVG file. { type: "direct", filename: "file_use_counter_svg_fill_pattern.svg", counters: [ { name: "CSS_PROPERTY_FillOpacity", glean: ["Css", "cssFillOpacity"], xfail: true, }, ], }, // Check that use counters are incremented by directly loading SVGs // that reference patterns defined in the same file or in data: URLs. { type: "direct", filename: "file_use_counter_svg_fill_pattern_internal.svg", counters: [ { name: "CSS_PROPERTY_FillOpacity", glean: ["Css", "cssFillOpacity"] }, ], }, // Check that use counters are incremented in a display:none iframe. { type: "undisplayed-iframe", filename: "file_use_counter_svg_currentScale.svg", counters: [ { name: "SVGSVGELEMENT_CURRENTSCALE_getter", glean: ["", "svgsvgelementCurrentscaleGetter"], }, ], }, // Check that a document that comes out of the bfcache reports any new use // counters recorded on it. { type: "direct", filename: "file_use_counter_bfcache.html", waitForExplicitFinish: true, counters: [ { name: "SVGSVGELEMENT_GETELEMENTBYID", glean: ["", "svgsvgelementGetelementbyid"], }, ], }, // // data: URLs don't correctly propagate to their referring document yet. // { // type: "direct", // filename: "file_use_counter_svg_fill_pattern_data.svg", // counters: [ // { name: "PROPERTY_FILL_OPACITY" }, // ], // }, ]; for (let test of TESTS) { let file = test.filename; info(`checking ${file} (${test.type})`); let options = { counters: test.counters, // bfcache test navigates a bunch of times and thus creates multiple top // level document entries, as expected. Whether the last document is // destroyed is a bit racy, see bug 1842800, so for now we allow it // with +/- 1. toplevel_docs: file == "file_use_counter_bfcache.html" ? [5, 6] : 1, docs: [test.type == "img" ? 2 : 1, Infinity], }; await test_once(options, async function () { // Load the test file in the new tab, either directly or via // file_use_counter_outer{,_display_none}.html, depending on the test type. let url, targetElement; switch (test.type) { case "iframe": url = gHttpTestRoot + "file_use_counter_outer.html"; targetElement = "content"; break; case "undisplayed-iframe": url = gHttpTestRoot + "file_use_counter_outer_display_none.html"; targetElement = "content"; break; case "img": url = gHttpTestRoot + "file_use_counter_outer.html"; targetElement = "display"; break; case "direct": url = gHttpTestRoot + file; targetElement = null; break; default: throw `unexpected type ${test.type}`; } let waitForFinish = null; if (test.waitForExplicitFinish) { is( test.type, "direct", `cannot use waitForExplicitFinish with test type ${test.type}` ); // Wait until the tab changes its hash to indicate it has finished. waitForFinish = BrowserTestUtils.waitForLocationChange( gBrowser, url + "#finished" ); } let newTab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url); if (waitForFinish) { await waitForFinish; } if (targetElement) { // Inject our desired file into the target element of the newly-loaded page. await SpecialPowers.spawn( gBrowser.selectedBrowser, [{ file, targetElement }], function (opts) { let target = content.document.getElementById(opts.targetElement); target.src = opts.file; return new Promise(resolve => { let listener = event => { event.target.removeEventListener("load", listener, true); resolve(); }; target.addEventListener("load", listener, true); }); } ); } // Tear down the page. await BrowserTestUtils.removeTab(newTab); }); } }); add_task(async function test_extension_counters() { let options = { counters: [], docs: 0, toplevel_docs: 0, ignore_sentinel: true, }; await test_once(options, async function () { let extension = ExtensionTestUtils.loadExtension({ manifest: { page_action: { default_popup: "page.html", browser_style: false, }, }, async background() { let [tab] = await browser.tabs.query({ active: true, currentWindow: true, }); await browser.pageAction.show(tab.id); browser.test.sendMessage("ready"); }, files: { "page.html": ` `, }, }); await extension.startup(); info("Extension started up"); await extension.awaitMessage("ready"); await extension.unload(); info("Extension unloaded"); }); }); async function ensureData(prevSentinelValue = null) { ok( !prevSentinelValue || ("page" in prevSentinelValue && "doc" in prevSentinelValue), `Sentinel's valid: ${JSON.stringify(prevSentinelValue)}` ); // Unfortunately, document destruction (when use counter reporting happens) // happens at some time later than the removal of the tab. // To wait for the use counters to be reported, we repeatedly flush IPC and // check for a change in the "sentinel" use counters // `use.counter.css.{page|doc}.css_marker_mid`. return BrowserTestUtils.waitForCondition( async () => { await Services.fog.testFlushAllChildren(); return ( !prevSentinelValue || (prevSentinelValue?.page != Glean.useCounterCssPage.cssMarkerMid.testGetValue() && prevSentinelValue?.doc != Glean.useCounterCssDoc.cssMarkerMid.testGetValue()) ); }, "ensureData", 100, Infinity ).then( () => ({ doc: Glean.useCounterCssPage.cssMarkerMid.testGetValue(), page: Glean.useCounterCssDoc.cssMarkerMid.testGetValue(), }), msg => { throw msg; } ); }