Source code

Revision control

Copy as Markdown

Other Tools

Test Info:

"use strict";
const NAVIGATIONAL_PREFETCH = 1 << 1;
const NAVIGATIONAL_PREFETCH_ELIGIBLE = 1 << 2;
let gCollectPageloadEvents = true;
let gSubmittedPageloadEvents = [];
function keepSubmittedPageloadEvents() {
if (!gCollectPageloadEvents) {
return;
}
GleanPings.pageload.testBeforeNextSubmit(() => {
if (!gCollectPageloadEvents) {
return;
}
gSubmittedPageloadEvents.push(
...(Glean.perf.pageLoad.testGetValue() ?? [])
);
Promise.resolve().then(keepSubmittedPageloadEvents);
});
}
async function resetPageloadTelemetry() {
await Services.fog.testFlushAllChildren();
Services.fog.testResetFOG();
gSubmittedPageloadEvents = [];
keepSubmittedPageloadEvents();
}
async function waitForPageloadEvent() {
let records;
await TestUtils.waitForCondition(() => {
records = [
...gSubmittedPageloadEvents,
...(Glean.perf.pageLoad.testGetValue() ?? []),
];
return records.length;
}, "A pageload event was recorded.");
Assert.equal(records.length, 1, "Only the expected document was reported.");
return records;
}
function documentFeatures(records) {
return records.map(r => parseInt(r.extra.document_features || "0", 10));
}
function targetURL(token, { noStore = false, large = false } = {}) {
// A fresh URL per navigation, so tests cannot share a cache entry.
return (
`${BASE}file_pageload_prefetch_target.sjs?v=${token}-${Date.now()}` +
(noStore ? "&nostore=1" : "") +
(large ? "&large=1" : "")
);
}
function sourceURL(file, target) {
return `${BASE}${file}?target=${encodeURIComponent(target)}`;
}
// Navigates source -> target; returns the target's pageload event.
async function pageloadEventFor(source, target, expectPrefetch) {
// The prefetch can finish before the source page fires load.
const prefetched = expectPrefetch
? TestUtils.topicObserved(
"http-on-stop-request",
subject => subject.QueryInterface(Ci.nsIHttpChannel).URI.spec === target
)
: Promise.resolve();
await resetPageloadTelemetry();
const tab = await BrowserTestUtils.openNewForegroundTab({
gBrowser,
url: source,
waitForLoad: true,
});
try {
await prefetched;
if (!expectPrefetch) {
await TestUtils.waitForTick();
}
const loaded = BrowserTestUtils.browserLoaded(
tab.linkedBrowser,
false,
target
);
BrowserTestUtils.startLoadingURIString(tab.linkedBrowser, target);
await loaded;
// Wait for the source event before clearing it, leaving only the target's.
await waitForPageloadEvent();
await resetPageloadTelemetry();
} finally {
await BrowserTestUtils.removeTab(tab);
}
return waitForPageloadEvent();
}
async function withPageloadEvent(name, trial, check) {
check(await trial(name));
}
add_setup(async function () {
registerCleanupFunction(() => {
gCollectPageloadEvents = false;
GleanPings.pageload.testBeforeNextSubmit(() => {});
gSubmittedPageloadEvents = [];
});
await SpecialPowers.pushPrefEnv({
set: [
["dom.speculation_rules.enabled", true],
["dom.pageload_event.testing.always_send_normal", true],
],
});
});
add_task(async function test_navigation_served_from_prefetch() {
if (Services.prefs.getBoolPref("telemetry.fog.artifact_build", false)) {
Assert.ok(true, "Test skipped in artifact builds. See bug 1836686.");
return;
}
await withPageloadEvent(
"hit",
token => {
const target = targetURL(token);
return pageloadEventFor(
sourceURL("file_pageload_prefetch_source.html", target),
target,
true
);
},
records =>
Assert.ok(
documentFeatures(records).some(
f => f & NAVIGATIONAL_PREFETCH && f & NAVIGATIONAL_PREFETCH_ELIGIBLE
),
"A navigation served from a prefetch is both eligible and served."
)
);
});
// Bug 2070841 skips cache validation for an activated record, so even a
// no-store response serves the navigation it was prefetched for.
add_task(async function test_nostore_prefetch_served_from_cache() {
if (Services.prefs.getBoolPref("telemetry.fog.artifact_build", false)) {
Assert.ok(true, "Test skipped in artifact builds. See bug 1836686.");
return;
}
await withPageloadEvent(
"nostore",
token => {
const target = targetURL(token, { noStore: true });
return pageloadEventFor(
sourceURL("file_pageload_prefetch_source.html", target),
target,
true
);
},
records => {
const features = documentFeatures(records);
Assert.ok(
features.some(f => f & NAVIGATIONAL_PREFETCH_ELIGIBLE),
"A no-store prefetch is eligible."
);
Assert.ok(
features.some(f => f & NAVIGATIONAL_PREFETCH),
"A navigation served from a no-store prefetch is marked served."
);
}
);
});
add_task(async function test_canceled_prefetch_is_still_eligible() {
if (Services.prefs.getBoolPref("telemetry.fog.artifact_build", false)) {
Assert.ok(true, "Test skipped in artifact builds. See bug 1836686.");
return;
}
await SpecialPowers.pushPrefEnv({
set: [["dom.speculation_rules.max_body_bytes", 64]],
});
try {
await withPageloadEvent(
"canceled",
token => {
const target = targetURL(token, { large: true });
return pageloadEventFor(
sourceURL("file_pageload_prefetch_source.html", target),
target,
true
);
},
records => {
const features = documentFeatures(records);
Assert.ok(
features.some(f => f & NAVIGATIONAL_PREFETCH_ELIGIBLE),
"A candidate stays eligible after a treatment-side fetch failure."
);
Assert.ok(
features.every(f => !(f & NAVIGATIONAL_PREFETCH)),
"A canceled prefetch is not marked served."
);
}
);
} finally {
await SpecialPowers.popPrefEnv();
}
});
// The experiment control branch: rules are processed and the candidate is
// recorded, but nothing is fetched, so the navigation is eligible and not
// served.
add_task(async function test_dry_run_is_eligible_but_not_served() {
if (Services.prefs.getBoolPref("telemetry.fog.artifact_build", false)) {
Assert.ok(true, "Test skipped in artifact builds. See bug 1836686.");
return;
}
await SpecialPowers.pushPrefEnv({
set: [["dom.speculation_rules.prefetch.dry_run", true]],
});
let sawPrefetchRequest = false;
try {
await withPageloadEvent(
"dryrun",
async token => {
const target = targetURL(token);
const observer = subject => {
const channel = subject.QueryInterface(Ci.nsIHttpChannel);
if (channel.URI.spec !== target) {
return;
}
try {
sawPrefetchRequest ||= channel
.getRequestHeader("Sec-Purpose")
.includes("prefetch");
} catch (e) {
// Navigation requests do not carry Sec-Purpose.
}
};
Services.obs.addObserver(observer, "http-on-modify-request");
try {
return await pageloadEventFor(
sourceURL("file_pageload_prefetch_source.html", target),
target,
false
);
} finally {
Services.obs.removeObserver(observer, "http-on-modify-request");
}
},
records => {
const features = documentFeatures(records);
Assert.ok(
features.some(f => f & NAVIGATIONAL_PREFETCH_ELIGIBLE),
"A dry-run candidate still marks the navigation eligible."
);
Assert.ok(
features.every(f => !(f & NAVIGATIONAL_PREFETCH)),
"A dry-run candidate is never served."
);
Assert.ok(!sawPrefetchRequest, "Dry run does not issue a request.");
}
);
} finally {
await SpecialPowers.popPrefEnv();
}
});
add_task(async function test_navigation_without_prefetch() {
if (Services.prefs.getBoolPref("telemetry.fog.artifact_build", false)) {
Assert.ok(true, "Test skipped in artifact builds. See bug 1836686.");
return;
}
await withPageloadEvent(
"miss",
token => {
const target = targetURL(token);
// Same flow, but the source page carries no speculation rules.
return pageloadEventFor(
sourceURL("file_pageload_prefetch_plain.html", target),
target,
false
);
},
records =>
Assert.ok(
documentFeatures(records).every(
f => !(f & (NAVIGATIONAL_PREFETCH | NAVIGATIONAL_PREFETCH_ELIGIBLE))
),
"A navigation with no rules is neither eligible nor served."
)
);
});