Source code
Revision control
Copy as Markdown
Other Tools
/* Any copyright is dedicated to the Public Domain.
"use strict";
const {
FX_MONITOR_OAUTH_CLIENT_ID,
FX_RELAY_OAUTH_CLIENT_ID,
VPN_OAUTH_CLIENT_ID,
} = ChromeUtils.importESModule(
"resource://gre/modules/FxAccountsCommon.sys.mjs"
);
const UTM =
"utm_medium=referral&utm_source=firefox-desktop&utm_campaign=toolbar";
const NEW_USER = `${UTM}&utm_content=new-user-global`;
const EXISTING_USER = `${UTM}&utm_content=existing-user-global`;
// The exact destinations the growth team measures. These are asserted as whole
// strings so a dropped or reordered campaign param fails the test.
const PRODUCTS = [
{
name: "monitor",
method: "openMonitorLink",
clientId: FX_MONITOR_OAUTH_CLIENT_ID,
},
{
name: "relay",
method: "openRelayLink",
clientId: FX_RELAY_OAUTH_CLIENT_ID,
},
{
name: "vpn",
method: "openVPNLink",
clientId: VPN_OAUTH_CLIENT_ID,
},
];
/**
* Invokes a product CTA with a stubbed account state and returns the URL that
* would have been opened.
*
* @param {object} product
* An entry from PRODUCTS.
* @param {object} options
* @param {string} options.status
* A UIState.STATUS_* value.
* @param {?string[]} options.attachedClients
* OAuth client Ids attached to the account, or null if never fetched.
* @returns {string}
*/
function ctaURLFor(product, { status, attachedClients }) {
const sandbox = sinon.createSandbox();
const origClients = gSync._attachedClients;
try {
sandbox.stub(UIState, "get").returns({ status, syncEnabled: true });
// Guards against the real network fetch replacing our injected list.
sandbox.stub(gSync, "fetchListOfOAuthClients").resolves(true);
sandbox.stub(gSync, "emitFxaToolbarTelemetry");
const openLink = sandbox.stub(gSync, "openLink");
const hide = sandbox.stub(PanelUI, "hide");
gSync._attachedClients = attachedClients?.map(id => ({ id })) ?? null;
gSync[product.method](null);
Assert.ok(
openLink.calledOnce,
`${product.name}: opened exactly one link, and did so synchronously`
);
Assert.ok(hide.calledOnce, `${product.name}: closed the panel`);
return openLink.firstCall.args[0].href;
} finally {
gSync._attachedClients = origClients;
sandbox.restore();
}
}
add_setup(async function () {
gSync.init();
});
add_task(async function test_cta_new_user_urls() {
for (const product of PRODUCTS) {
const url = ctaURLFor(product, {
status: UIState.STATUS_NOT_CONFIGURED,
attachedClients: null,
});
Assert.equal(
url,
product.newUserURL,
`${product.name}: signed-out users go to the landing page`
);
}
});
add_task(async function test_cta_signed_in_without_the_product() {
for (const product of PRODUCTS) {
const url = ctaURLFor(product, {
status: UIState.STATUS_SIGNED_IN,
attachedClients: [],
});
Assert.equal(
url,
product.newUserURL,
`${product.name}: signed in but not subscribed still gets the landing page`
);
}
});
add_task(async function test_cta_existing_user_urls() {
for (const product of PRODUCTS) {
const url = ctaURLFor(product, {
status: UIState.STATUS_SIGNED_IN,
attachedClients: [product.clientId],
});
Assert.equal(
url,
product.existingUserURL,
`${product.name}: subscribers deep-link into the product`
);
}
});
// Each CTA must key off its own client Id: having Relay attached must not
// promote the Monitor or VPN link to its existing-user destination.
add_task(async function test_cta_client_ids_do_not_cross_talk() {
for (const attached of PRODUCTS) {
for (const product of PRODUCTS) {
const url = ctaURLFor(product, {
status: UIState.STATUS_SIGNED_IN,
attachedClients: [attached.clientId],
});
Assert.equal(
url,
product === attached ? product.existingUserURL : product.newUserURL,
`${product.name}: correct destination when only ${attached.name} is attached`
);
}
}
});
add_task(async function test_cta_all_products_attached() {
const allClientIds = PRODUCTS.map(p => p.clientId);
for (const product of PRODUCTS) {
const url = ctaURLFor(product, {
status: UIState.STATUS_SIGNED_IN,
attachedClients: allClientIds,
});
Assert.equal(
url,
product.existingUserURL,
`${product.name}: deep-links when every product is attached`
);
}
});
// The signed-in check is not redundant with the attached-client check.
// _attachedClients is only cleared on an explicit sign-out notification, so a
// stale list can outlive the signed-in state. In every non-signed-in status we
// must fall back to the landing page rather than deep-link an account the user
// can no longer access.
add_task(async function test_cta_stale_clients_in_non_signed_in_states() {
const NON_SIGNED_IN = [
UIState.STATUS_NOT_CONFIGURED,
UIState.STATUS_NOT_VERIFIED,
UIState.STATUS_LOGIN_FAILED,
];
for (const status of NON_SIGNED_IN) {
for (const product of PRODUCTS) {
const url = ctaURLFor(product, {
status,
attachedClients: [product.clientId],
});
Assert.equal(
url,
product.newUserURL,
`${product.name}: stale attached client is ignored in ${status}`
);
}
}
});
add_task(async function test_cta_emits_telemetry() {
const EVENTS = {
openMonitorLink: "monitor_cta",
openRelayLink: "relay_cta",
openVPNLink: "vpn_cta",
};
for (const product of PRODUCTS) {
const sandbox = sinon.createSandbox();
try {
sandbox
.stub(UIState, "get")
.returns({ status: UIState.STATUS_SIGNED_IN, syncEnabled: true });
sandbox.stub(gSync, "openLink");
sandbox.stub(PanelUI, "hide");
const telemetry = sandbox.stub(gSync, "emitFxaToolbarTelemetry");
const sourceElement = document.getElementById("fxa-toolbar-menu-button");
gSync[product.method](sourceElement);
Assert.ok(
telemetry.calledOnceWith(EVENTS[product.method], sourceElement),
`${product.name}: records the CTA click with its source element`
);
} finally {
sandbox.restore();
}
}
});