Source code

Revision control

Copy as Markdown

Other Tools

/* Any copyright is dedicated to the Public Domain.
"use strict";
// Exit-outcome event for the search CTA (bug 2055717). Exactly one
// search_cta_exit event per CTA-eligible online dnsNotFound page, with a
// content-free reason (clicked-cta / clicked-reload / clicked-suggestion /
// navigated-or-closed) plus cta_shown and suggestion_shown booleans. Recorded
// at the click for the three click-driven reasons, at pagehide otherwise.
// Non-eligible pages (pref off, etc.) record nothing.
const { SearchService } = ChromeUtils.importESModule(
"moz-src:///toolkit/components/search/SearchService.sys.mjs"
);
const CTA_PREF = "browser.netError.searchCTA.enabled";
// Empty path -> registrable-domain (host) query, so the CTA shows with a
// default engine present.
const FAILED_URL = "https://foo.exit-cta.com/";
add_setup(async function () {
stubSearchCTASupportedEngine();
await SearchTestUtils.installSearchExtension(
{
name: "MozSearchCTAExit",
search_url: "https://example.com/",
search_url_get_params: "q={searchTerms}",
},
{ setAsDefault: true }
);
await SpecialPowers.pushPrefEnv({
set: [
[CTA_PREF, true],
// Treat the connectivity reading as always fresh so the bug 2055712 guard
// is a no-op and the CTA renders deterministically.
["browser.netError.searchCTA.connectivityFreshnessMs", 2147483647],
],
});
});
const exitEvents = () =>
Glean.securityUiNeterror.searchCtaExit.testGetValue() ?? [];
// The CTA exit event fires on every eligible page, so tab closes in other
// searchCTA test files can leave events buffered in content. Ship those to the
// parent, then clear, so each task starts from a known-empty state.
async function drainAndResetExitEvents() {
await Services.fog.testFlushAllChildren();
Services.fog.testResetFOG();
}
/**
* Navigate the error page away to fire pagehide, then flush content telemetry.
* Kept local because this is the only file that has to force a pagehide.
*
* @param {MozBrowser} browser The browser showing the error page.
*/
async function navigateAwayAndFlush(browser) {
const loaded = BrowserTestUtils.browserLoaded(
browser,
false,
url => url === "about:blank"
);
BrowserTestUtils.startLoadingURIString(browser, "about:blank");
await loaded;
await Services.fog.testFlushAllChildren();
}
add_task(async function test_navigatedOrClosed() {
await drainAndResetExitEvents();
const { tab, browser } = await loadDnsNotFoundPage(FAILED_URL);
await waitForSettledNetErrorCard(browser);
await navigateAwayAndFlush(browser);
const events = exitEvents();
is(events.length, 1, "one exit event for a qualifying page");
is(
events[0].extra.reason,
"navigated-or-closed",
"merged away/closed reason"
);
is(String(events[0].extra.cta_shown), "true", "cta_shown true when shown");
is(
String(events[0].extra.suggestion_shown),
"false",
"suggestion_shown false when no suggestion was offered"
);
Assert.deepEqual(
Object.keys(events[0].extra).sort(),
["cta_shown", "reason", "suggestion_shown"],
"no content fields recorded"
);
BrowserTestUtils.removeTab(tab);
});
add_task(async function test_clickedCta() {
await drainAndResetExitEvents();
const { tab, browser } = await loadDnsNotFoundPage(FAILED_URL);
const newTabPromise = BrowserTestUtils.waitForNewTab(gBrowser, null, true);
await waitForSettledNetErrorCard(browser, {
clickQuery: "searchCTAButton",
});
const searchTab = await newTabPromise;
BrowserTestUtils.removeTab(searchTab);
// The error page stays open after the search; navigating it away exits it.
await navigateAwayAndFlush(browser);
const events = exitEvents();
is(events.length, 1, "one exit event");
is(events[0].extra.reason, "clicked-cta", "clicked-cta recorded");
is(String(events[0].extra.cta_shown), "true", "cta_shown true");
BrowserTestUtils.removeTab(tab);
});
// Clicking Search opens a separate tab and leaves this page loaded, so the
// event must not wait for pagehide: it would otherwise be delayed for as long
// as the tab stays open, and lost entirely if pagehide never fires — losing
// precisely the clicks the CTA worked for.
add_task(async function test_clickedCtaRecordsWithoutPagehide() {
await drainAndResetExitEvents();
const { tab, browser } = await loadDnsNotFoundPage(FAILED_URL);
const newTabPromise = BrowserTestUtils.waitForNewTab(gBrowser, null, true);
await waitForSettledNetErrorCard(browser, {
clickQuery: "searchCTAButton",
});
const searchTab = await newTabPromise;
// Deliberately no navigation: the error page is still open and has not fired
// pagehide, yet the outcome must already be recorded.
await Services.fog.testFlushAllChildren();
const events = exitEvents();
is(events.length, 1, "exit recorded at click time, with no pagehide");
is(events[0].extra.reason, "clicked-cta", "clicked-cta recorded");
is(String(events[0].extra.cta_shown), "true", "cta_shown true");
BrowserTestUtils.removeTab(searchTab);
BrowserTestUtils.removeTab(tab);
});
add_task(async function test_clickedReload() {
await drainAndResetExitEvents();
const { tab, browser } = await loadDnsNotFoundPage(FAILED_URL);
const reloaded = BrowserTestUtils.waitForErrorPage(browser);
await waitForSettledNetErrorCard(browser, { clickQuery: "reloadButton" });
await reloaded;
await Services.fog.testFlushAllChildren();
const events = exitEvents();
is(events.length, 1, "reload fires one exit event");
is(events[0].extra.reason, "clicked-reload", "clicked-reload recorded");
// Navigate away before closing so the still-live reloaded error page doesn't
// fire a stray exit event on tab close that races into the next task.
await navigateAwayAndFlush(browser);
BrowserTestUtils.removeTab(tab);
});
add_task(async function test_clickedSuggestion() {
await drainAndResetExitEvents();
const { tab, browser } = await loadDnsNotFoundPage(FAILED_URL);
await waitForSettledNetErrorCard(browser);
await SpecialPowers.spawn(browser, [], async () => {
const card =
content.document.querySelector("net-error-card").wrappedJSObject;
// Simulate the NetErrorChild-injected "did you mean" link and click it.
const span = content.document.createElement("span");
span.id = "dns-suggestion";
const link = content.document.createElement("a");
link.textContent = "did you mean";
span.appendChild(link);
card.errorIntro.after(span);
link.click();
});
await navigateAwayAndFlush(browser);
const events = exitEvents();
is(events.length, 1, "one exit event");
is(
events[0].extra.reason,
"clicked-suggestion",
"clicked-suggestion recorded"
);
is(
String(events[0].extra.suggestion_shown),
"true",
"suggestion_shown true when the suggestion was on the page"
);
BrowserTestUtils.removeTab(tab);
});
add_task(async function test_ctaShownFalseWhenSuppressed() {
await drainAndResetExitEvents();
const sandbox = sinon.createSandbox();
sandbox.stub(SearchService, "getDefault").resolves(null);
try {
const { tab, browser } = await loadDnsNotFoundPage(FAILED_URL);
await waitForSettledNetErrorCard(browser);
await SpecialPowers.spawn(browser, [], async () => {
const card =
content.document.querySelector("net-error-card").wrappedJSObject;
is(card.searchCTAButton, null, "Search button suppressed with no engine");
});
await navigateAwayAndFlush(browser);
const events = exitEvents();
is(events.length, 1, "exit event still fires on a qualifying page");
is(events[0].extra.reason, "navigated-or-closed", "reason recorded");
is(
String(events[0].extra.cta_shown),
"false",
"cta_shown false when the Search button was suppressed"
);
BrowserTestUtils.removeTab(tab);
} finally {
sandbox.restore();
}
});
add_task(async function test_noEventWhenNotEligible() {
await drainAndResetExitEvents();
await SpecialPowers.pushPrefEnv({ set: [[CTA_PREF, false]] });
const { tab, browser } = await loadDnsNotFoundPage(FAILED_URL);
await waitForSettledNetErrorCard(browser);
await SpecialPowers.spawn(browser, [], async () => {
const card =
content.document.querySelector("net-error-card").wrappedJSObject;
ok(card.tryAgainButton, "The standard dnsNotFound page is shown");
});
await navigateAwayAndFlush(browser);
is(exitEvents().length, 0, "no exit event on a non-eligible page");
BrowserTestUtils.removeTab(tab);
await SpecialPowers.popPrefEnv();
});