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.experimental.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.experimental.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.experimental.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.experimental.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.experimental.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.experimental.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.experimental.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");
});