const URL_BASE = "https://example.com/browser/dom/tests/browser/"; const URL_BASE2 = "https://example.net/browser/dom/tests/browser/"; function testFields( entry, { hasBodyAccess, hasTimingAccess, isCacheOf }, desc ) { Assert.equal(entry.entryType, "resource", "entryType should be available"); Assert.equal( entry.initiatorType, "script", "initiatorType should be available" ); if (hasTimingAccess) { Assert.equal( entry.nextHopProtocol, "http/1.1", `nextHopProtocol should be available for ${desc}` ); } else { Assert.equal( entry.nextHopProtocol, "", `nextHopProtocol should be hidden for ${desc}` ); } if (hasBodyAccess) { Assert.equal( entry.responseStatus, 200, `responseStatus should be available for ${desc}` ); } else { Assert.equal( entry.responseStatus, 0, `responseStatus should be hidden for ${desc}` ); } if (hasBodyAccess) { Assert.equal( entry.contentType, "text/javascript", `contentType should be available for ${desc}` ); } else { Assert.equal( entry.contentType, "", `contentType should be hidden for ${desc}` ); } Assert.greater( entry.startTime, 0, `startTime should be non-zero for ${desc}` ); Assert.greater( entry.responseEnd, 0, `responseEnd should be non-zero for ${desc}` ); Assert.lessOrEqual( entry.startTime, entry.responseEnd, `startTime <= responseEnd for ${desc}` ); if (hasTimingAccess) { Assert.deepEqual( entry.serverTiming, [ { name: "name1", duration: 0, description: "" }, { name: "name2", duration: 20, description: "" }, { name: "name3", duration: 30, description: "desc3" }, ], `serverTiming should be available for ${desc}` ); } else { Assert.deepEqual( entry.serverTiming, [], `serverTiming should be hidden for ${desc}` ); } if (hasBodyAccess) { Assert.greater( entry.encodedBodySize, 0, `encodedBodySize should be available for ${desc}` ); } else { Assert.equal( entry.encodedBodySize, 0, `encodedBodySize should be hidden for ${desc}` ); } if (isCacheOf) { Assert.equal( entry.encodedBodySize, isCacheOf.encodedBodySize, `encodedBodySize should equal to non-cache case for ${desc}` ); } if (hasBodyAccess) { Assert.greater( entry.decodedBodySize, 0, `decodedBodySize should be available for ${desc}` ); } else { Assert.equal( entry.decodedBodySize, 0, `decodedBodySize should be hidden for ${desc}` ); } if (isCacheOf) { Assert.equal( entry.decodedBodySize, isCacheOf.decodedBodySize, `decodedBodySize should equal to non-cache case for ${desc}` ); } if (hasTimingAccess) { if (isCacheOf) { Assert.equal( entry.transferSize, 0, `transferSize should be zero for ${desc}` ); } else if (hasBodyAccess) { Assert.greater( entry.transferSize, 300, `transferSize should be non-zero +300 for ${desc}` ); } else { Assert.equal( entry.transferSize, 300, `transferSize should be zero +300 for ${desc}` ); } } else { Assert.equal( entry.transferSize, 0, `transferSize should be hidden for ${desc}` ); } } async function doTestCompleteCacheAfterReload(type) { await SpecialPowers.pushPrefEnv({ set: [["dom.script_loader.navigation_cache", true]], }); registerCleanupFunction(() => SpecialPowers.popPrefEnv()); ChromeUtils.clearResourceCache(); Services.cache2.clear(); await BrowserTestUtils.withNewTab( { gBrowser, url: URL_BASE + "dummy.html", }, async function (browser) { const JS_URL = URL_BASE + "perf_server.sjs?cacheable"; const task = async (url, type) => { await new Promise(resolve => { const script = content.document.createElement("script"); script.src = url; if (type === "module") { script.type = "module"; } script.addEventListener("load", resolve); content.document.head.append(script); }); const entries = content.performance .getEntriesByType("resource") .filter(entry => entry.name.includes("perf_server.sjs")); if (entries.length != 1) { throw new Error(`Expect one entry, got ${entries.length} entries`); } // NOTE: entries[0].toJSON() doesn't convert serverTiming items. return JSON.parse(JSON.stringify(entries[0])); }; const entry = await SpecialPowers.spawn(browser, [JS_URL, type], task); Assert.equal(entry.name, JS_URL); testFields( entry, { hasBodyAccess: true, hasTimingAccess: true, }, "same origin (non-cached)" ); await BrowserTestUtils.reloadTab(gBrowser.selectedTab); const cacheEntry = await SpecialPowers.spawn( browser, [JS_URL, type], task ); Assert.equal(cacheEntry.name, JS_URL); testFields( cacheEntry, { hasBodyAccess: true, hasTimingAccess: true, isCacheOf: entry, }, "same origin (cached)" ); } ); } add_task(async function testScriptCompleteCacheAfterReload() { await doTestCompleteCacheAfterReload("script"); }); add_task(async function testModuleCompleteCacheAfterReload() { await doTestCompleteCacheAfterReload("module"); }); async function doTestCompleteCacheInSameDocument(type) { await SpecialPowers.pushPrefEnv({ set: [["dom.script_loader.navigation_cache", true]], }); registerCleanupFunction(() => SpecialPowers.popPrefEnv()); ChromeUtils.clearResourceCache(); Services.cache2.clear(); await BrowserTestUtils.withNewTab( { gBrowser, url: URL_BASE + "dummy.html", }, async function (browser) { const JS_URL = URL_BASE + "perf_server.sjs?cacheable"; const task = async (url, type) => { // Before reload: // * The first load is not cache // * The second load is complete cache // After reload: // * Both loads are complete cache for (let i = 0; i < 2; i++) { await new Promise(resolve => { const script = content.document.createElement("script"); script.src = url; if (type === "module") { script.type = "module"; } script.addEventListener("load", () => { resolve(); }); content.document.head.append(script); }); } const entries = content.performance .getEntriesByType("resource") .filter(entry => entry.name.includes("perf_server.sjs")); // In contrast to CSS, classic scripts perform "fetch" for both. // Module scripts become singleton, and only one "fetch" happens. if (type === "script") { if (entries.length != 2) { throw new Error( `Expect two entries, got ${entries.length} entries` ); } } else if (entries.length != 1) { throw new Error(`Expect one entry, got ${entries.length} entries`); } return JSON.parse(JSON.stringify(entries)); }; const entries = await SpecialPowers.spawn(browser, [JS_URL, type], task); Assert.equal(entries[0].name, JS_URL); if (type === "script") { Assert.equal(entries[1].name, JS_URL); } testFields( entries[0], { hasBodyAccess: true, hasTimingAccess: true, }, "same origin (non-cached)" ); if (type === "script") { testFields( entries[1], { hasBodyAccess: true, hasTimingAccess: true, isCacheOf: entries[0], }, "same origin (cached)" ); } await BrowserTestUtils.reloadTab(gBrowser.selectedTab); const cacheEntries = await SpecialPowers.spawn( browser, [JS_URL, type], task ); Assert.equal(cacheEntries[0].name, JS_URL); if (type === "script") { Assert.equal(cacheEntries[1].name, JS_URL); } testFields( cacheEntries[0], { hasBodyAccess: true, hasTimingAccess: true, isCacheOf: entries[0], }, "same origin (cached)" ); if (type === "script") { testFields( cacheEntries[1], { hasBodyAccess: true, hasTimingAccess: true, isCacheOf: entries[0], }, "same origin (cached)" ); } } ); } add_task(async function testScriptCompleteCacheInSameDocument() { await doTestCompleteCacheInSameDocument("script"); }); add_task(async function testModuleCompleteCacheInSameDocument() { await doTestCompleteCacheInSameDocument("module"); }); async function doTestNoCacheReload(type) { await SpecialPowers.pushPrefEnv({ set: [["dom.script_loader.navigation_cache", true]], }); registerCleanupFunction(() => SpecialPowers.popPrefEnv()); ChromeUtils.clearResourceCache(); Services.cache2.clear(); await BrowserTestUtils.withNewTab( { gBrowser, url: URL_BASE + "dummy.html", }, async function (browser) { const JS_URL = URL_BASE + "perf_server.sjs?"; const task = async (url, type) => { await new Promise(resolve => { const script = content.document.createElement("script"); script.src = url; if (type === "module") { script.type = "module"; } script.addEventListener("load", resolve); content.document.head.append(script); }); const entries = content.performance .getEntriesByType("resource") .filter(entry => entry.name.includes("perf_server.sjs")); if (entries.length != 1) { throw new Error(`Expect one entry, got ${entries.length} entries`); } return JSON.parse(JSON.stringify(entries[0])); }; const entry = await SpecialPowers.spawn(browser, [JS_URL, type], task); Assert.equal(entry.name, JS_URL); testFields( entry, { hasBodyAccess: true, hasTimingAccess: true, }, "same origin (non-cached)" ); await BrowserTestUtils.reloadTab(gBrowser.selectedTab); // Reloading the JS shouldn't hit any cache. const reloadEntry = await SpecialPowers.spawn( browser, [JS_URL, type], task ); Assert.equal(reloadEntry.name, JS_URL); testFields( reloadEntry, { hasBodyAccess: true, hasTimingAccess: true, }, "same origin (non-cached)" ); } ); } add_task(async function testScriptNoCacheReload() { await doTestNoCacheReload("script"); }); add_task(async function testModuleNoCacheReload() { await doTestNoCacheReload("module"); }); async function doTest_NoCORS(type) { await SpecialPowers.pushPrefEnv({ set: [["dom.script_loader.navigation_cache", true]], }); registerCleanupFunction(() => SpecialPowers.popPrefEnv()); ChromeUtils.clearResourceCache(); Services.cache2.clear(); await BrowserTestUtils.withNewTab( { gBrowser, url: URL_BASE + "dummy.html", }, async function (browser) { const JS_URL = URL_BASE2 + "perf_server.sjs?cacheable"; const task = async (url, type) => { await new Promise(resolve => { const script = content.document.createElement("script"); script.src = url; if (type === "module") { script.type = "module"; } script.addEventListener("load", resolve); content.document.head.append(script); }); const entries = content.performance .getEntriesByType("resource") .filter(entry => entry.name.includes("perf_server.sjs")); if (entries.length != 1) { throw new Error(`Expect one entry, got ${entries.length} entries`); } return JSON.parse(JSON.stringify(entries[0])); }; const entry = await SpecialPowers.spawn(browser, [JS_URL, type], task); Assert.equal(entry.name, JS_URL); testFields( entry, { hasBodyAccess: false, hasTimingAccess: false, }, "cross origin (non-cached)" ); await BrowserTestUtils.reloadTab(gBrowser.selectedTab); const cacheEntry = await SpecialPowers.spawn( browser, [JS_URL, type], task ); Assert.equal(cacheEntry.name, JS_URL); testFields( cacheEntry, { hasBodyAccess: false, hasTimingAccess: false, isCacheOf: entry, }, "cross origin (cached)" ); } ); } add_task(async function testScript_NoCORS() { await doTest_NoCORS("script"); }); // Modules cannot be loaded with no CORS. async function doTest_NoCORS_TAO(type) { await SpecialPowers.pushPrefEnv({ set: [["dom.script_loader.navigation_cache", true]], }); registerCleanupFunction(() => SpecialPowers.popPrefEnv()); ChromeUtils.clearResourceCache(); Services.cache2.clear(); await BrowserTestUtils.withNewTab( { gBrowser, url: URL_BASE + "dummy.html", }, async function (browser) { const JS_URL = URL_BASE2 + "perf_server.sjs?cacheable,tao"; const task = async (url, type) => { await new Promise(resolve => { const script = content.document.createElement("script"); script.src = url; if (type === "module") { script.type = "module"; } script.addEventListener("load", resolve); content.document.head.append(script); }); const entries = content.performance .getEntriesByType("resource") .filter(entry => entry.name.includes("perf_server.sjs")); if (entries.length != 1) { throw new Error(`Expect one entry, got ${entries.length} entries`); } return JSON.parse(JSON.stringify(entries[0])); }; const entry = await SpecialPowers.spawn(browser, [JS_URL, type], task); Assert.equal(entry.name, JS_URL); testFields( entry, { hasBodyAccess: false, hasTimingAccess: true, }, "cross origin with Timing-Allow-Origin (non-cached)" ); await BrowserTestUtils.reloadTab(gBrowser.selectedTab); const cacheEntry = await SpecialPowers.spawn( browser, [JS_URL, type], task ); Assert.equal(cacheEntry.name, JS_URL); testFields( cacheEntry, { hasBodyAccess: false, hasTimingAccess: true, isCacheOf: entry, }, "cross origin with Timing-Allow-Origin (cached)" ); } ); } add_task(async function testScript_NoCORS_TAO() { await doTest_NoCORS_TAO("script"); }); // Modules cannot be loaded with no CORS. async function doTest_CORS(type) { await SpecialPowers.pushPrefEnv({ set: [["dom.script_loader.navigation_cache", true]], }); registerCleanupFunction(() => SpecialPowers.popPrefEnv()); ChromeUtils.clearResourceCache(); Services.cache2.clear(); await BrowserTestUtils.withNewTab( { gBrowser, url: URL_BASE + "dummy.html", }, async function (browser) { const JS_URL = URL_BASE2 + "perf_server.sjs?cacheable,cors"; const task = async (url, type) => { await new Promise(resolve => { const script = content.document.createElement("script"); script.setAttribute("crossorigin", "anonymous"); script.src = url; if (type === "module") { script.type = "module"; } script.addEventListener("load", resolve); content.document.head.append(script); }); const entries = content.performance .getEntriesByType("resource") .filter(entry => entry.name.includes("perf_server.sjs")); if (entries.length != 1) { throw new Error(`Expect one entry, got ${entries.length} entries`); } return JSON.parse(JSON.stringify(entries[0])); }; const entry = await SpecialPowers.spawn(browser, [JS_URL, type], task); Assert.equal(entry.name, JS_URL); testFields( entry, { hasBodyAccess: true, hasTimingAccess: false, }, "CORS (non-cached)" ); await BrowserTestUtils.reloadTab(gBrowser.selectedTab); const cacheEntry = await SpecialPowers.spawn( browser, [JS_URL, type], task ); Assert.equal(cacheEntry.name, JS_URL); testFields( cacheEntry, { hasBodyAccess: true, hasTimingAccess: false, isCacheOf: entry, }, "CORS (cached)" ); } ); } add_task(async function testScript_CORS() { await doTest_CORS("script"); }); add_task(async function testModule_CORS() { await doTest_CORS("module"); }); async function doTest_CORS_TAO(type) { await SpecialPowers.pushPrefEnv({ set: [["dom.script_loader.navigation_cache", true]], }); registerCleanupFunction(() => SpecialPowers.popPrefEnv()); ChromeUtils.clearResourceCache(); Services.cache2.clear(); await BrowserTestUtils.withNewTab( { gBrowser, url: URL_BASE + "dummy.html", }, async function (browser) { const JS_URL = URL_BASE2 + "perf_server.sjs?cacheable,cors,tao"; const task = async (url, type) => { await new Promise(resolve => { const script = content.document.createElement("script"); script.setAttribute("crossorigin", "anonymous"); script.src = url; if (type === "module") { script.type = "module"; } script.addEventListener("load", resolve); content.document.head.append(script); }); const entries = content.performance .getEntriesByType("resource") .filter(entry => entry.name.includes("perf_server.sjs")); if (entries.length != 1) { throw new Error(`Expect one entry, got ${entries.length} entries`); } return JSON.parse(JSON.stringify(entries[0])); }; const entry = await SpecialPowers.spawn(browser, [JS_URL, type], task); Assert.equal(entry.name, JS_URL); testFields( entry, { hasBodyAccess: true, hasTimingAccess: true, }, "CORS with Timing-Allow-Origin (non-cached)" ); await BrowserTestUtils.reloadTab(gBrowser.selectedTab); const cacheEntry = await SpecialPowers.spawn( browser, [JS_URL, type], task ); Assert.equal(cacheEntry.name, JS_URL); testFields( cacheEntry, { hasBodyAccess: true, hasTimingAccess: true, isCacheOf: entry, }, "CORS with Timing-Allow-Origin (cached)" ); } ); } add_task(async function testScript_CORS_TAO() { await doTest_CORS_TAO("script"); }); add_task(async function testModule_CORS_TAO() { await doTest_CORS_TAO("module"); });