Source code
Revision control
Copy as Markdown
Other Tools
import { _ToolbarBadgeHub } from "modules/ToolbarBadgeHub.sys.mjs";
import { GlobalOverrider } from "test/unit/utils";
import { OnboardingMessageProvider } from "modules/OnboardingMessageProvider.sys.mjs";
describe("ToolbarBadgeHub", () => {
let sandbox;
let instance;
let fakeAddImpression;
let fakeSendTelemetry;
let isBrowserPrivateStub;
let fxaMessage;
let fakeElement;
let globals;
let everyWindowStub;
let clearTimeoutStub;
let setTimeoutStub;
let addObserverStub;
let removeObserverStub;
let getStringPrefStub;
let clearUserPrefStub;
let setStringPrefStub;
let requestIdleCallbackStub;
let fakeWindow;
beforeEach(async () => {
globals = new GlobalOverrider();
sandbox = sinon.createSandbox();
instance = new _ToolbarBadgeHub();
fakeAddImpression = sandbox.stub();
fakeSendTelemetry = sandbox.stub();
isBrowserPrivateStub = sandbox.stub();
const onboardingMsgs =
await OnboardingMessageProvider.getUntranslatedMessages();
fxaMessage = onboardingMsgs.find(({ id }) => id === "FXA_ACCOUNTS_BADGE");
fakeElement = {
classList: {
add: sandbox.stub(),
remove: sandbox.stub(),
},
setAttribute: sandbox.stub(),
removeAttribute: sandbox.stub(),
querySelector: sandbox.stub(),
addEventListener: sandbox.stub(),
remove: sandbox.stub(),
appendChild: sandbox.stub(),
};
// Share the same element when selecting child nodes
fakeElement.querySelector.returns(fakeElement);
everyWindowStub = {
registerCallback: sandbox.stub(),
unregisterCallback: sandbox.stub(),
};
clearTimeoutStub = sandbox.stub();
setTimeoutStub = sandbox.stub();
fakeWindow = {
MozXULElement: { insertFTLIfNeeded: sandbox.stub() },
ownerGlobal: {
gBrowser: {
selectedBrowser: "browser",
},
},
};
addObserverStub = sandbox.stub();
removeObserverStub = sandbox.stub();
getStringPrefStub = sandbox.stub();
clearUserPrefStub = sandbox.stub();
setStringPrefStub = sandbox.stub();
requestIdleCallbackStub = sandbox.stub().callsFake(fn => fn());
globals.set({
requestIdleCallback: requestIdleCallbackStub,
EveryWindow: everyWindowStub,
PrivateBrowsingUtils: { isBrowserPrivate: isBrowserPrivateStub },
setTimeout: setTimeoutStub,
clearTimeout: clearTimeoutStub,
Services: {
wm: {
getMostRecentWindow: () => fakeWindow,
},
prefs: {
addObserver: addObserverStub,
removeObserver: removeObserverStub,
getStringPref: getStringPrefStub,
clearUserPref: clearUserPrefStub,
setStringPref: setStringPrefStub,
},
},
});
});
afterEach(() => {
sandbox.restore();
globals.restore();
});
it("should create an instance", () => {
assert.ok(instance);
});
describe("#init", () => {
it("should make a single messageRequest on init", async () => {
sandbox.stub(instance, "messageRequest");
const waitForInitialized = sandbox.stub().resolves();
await instance.init(waitForInitialized, {});
await instance.init(waitForInitialized, {});
assert.calledOnce(instance.messageRequest);
assert.calledWithExactly(instance.messageRequest, {
template: "toolbar_badge",
triggerId: "toolbarBadgeUpdate",
});
instance.uninit();
await instance.init(waitForInitialized, {});
assert.calledTwice(instance.messageRequest);
});
});
describe("#uninit", () => {
beforeEach(async () => {
await instance.init(sandbox.stub().resolves(), {});
});
it("should clear any setTimeout cbs", async () => {
await instance.init(sandbox.stub().resolves(), {});
instance.state.showBadgeTimeoutId = 2;
instance.uninit();
assert.calledOnce(clearTimeoutStub);
assert.calledWithExactly(clearTimeoutStub, 2);
});
});
describe("messageRequest", () => {
let handleMessageRequestStub;
beforeEach(() => {
handleMessageRequestStub = sandbox.stub().returns(fxaMessage);
sandbox
.stub(instance, "_handleMessageRequest")
.value(handleMessageRequestStub);
sandbox.stub(instance, "registerBadgeNotificationListener");
});
it("should fetch a message with the provided trigger and template", async () => {
await instance.messageRequest({
triggerId: "trigger",
template: "template",
});
assert.calledOnce(handleMessageRequestStub);
assert.calledWithExactly(handleMessageRequestStub, {
triggerId: "trigger",
template: "template",
});
});
it("should call addToolbarNotification with browser window and message", async () => {
await instance.messageRequest("trigger");
assert.calledOnce(instance.registerBadgeNotificationListener);
assert.calledWithExactly(
instance.registerBadgeNotificationListener,
fxaMessage
);
});
it("shouldn't do anything if no message is provided", async () => {
handleMessageRequestStub.resolves(null);
await instance.messageRequest({ triggerId: "trigger" });
assert.notCalled(instance.registerBadgeNotificationListener);
});
it("should record telemetry events", async () => {
const startTelemetryStopwatch = sandbox.stub(
global.TelemetryStopwatch,
"start"
);
const finishTelemetryStopwatch = sandbox.stub(
global.TelemetryStopwatch,
"finish"
);
handleMessageRequestStub.returns(null);
await instance.messageRequest({ triggerId: "trigger" });
assert.calledOnce(startTelemetryStopwatch);
assert.calledWithExactly(
startTelemetryStopwatch,
"MS_MESSAGE_REQUEST_TIME_MS",
{ triggerId: "trigger" }
);
assert.calledOnce(finishTelemetryStopwatch);
assert.calledWithExactly(
finishTelemetryStopwatch,
"MS_MESSAGE_REQUEST_TIME_MS",
{ triggerId: "trigger" }
);
});
});
describe("addToolbarNotification", () => {
let target;
let fakeDocument;
beforeEach(async () => {
await instance.init(sandbox.stub().resolves(), {
addImpression: fakeAddImpression,
sendTelemetry: fakeSendTelemetry,
});
fakeDocument = {
getElementById: sandbox.stub().returns(fakeElement),
createElement: sandbox.stub().returns(fakeElement),
l10n: { setAttributes: sandbox.stub() },
};
target = { ...fakeWindow, browser: { ownerDocument: fakeDocument } };
});
afterEach(() => {
instance.uninit();
});
it("shouldn't do anything if target element is not found", () => {
fakeDocument.getElementById.returns(null);
instance.addToolbarNotification(target, fxaMessage);
assert.notCalled(fakeElement.setAttribute);
});
it("should target the element specified in the message", () => {
instance.addToolbarNotification(target, fxaMessage);
assert.calledOnce(fakeDocument.getElementById);
assert.calledWithExactly(
fakeDocument.getElementById,
fxaMessage.content.target
);
});
it("should show a notification", () => {
instance.addToolbarNotification(target, fxaMessage);
assert.calledTwice(fakeElement.setAttribute);
assert.calledWithExactly(fakeElement.setAttribute, "badged", true);
assert.calledWithExactly(
fakeElement.setAttribute,
"showing-callout",
true
);
assert.calledWithExactly(fakeElement.classList.add, "feature-callout");
});
it("should attach a cb on the notification", () => {
instance.addToolbarNotification(target, fxaMessage);
assert.calledTwice(fakeElement.addEventListener);
assert.calledWithExactly(
fakeElement.addEventListener,
"mousedown",
instance.removeAllNotifications
);
assert.calledWithExactly(
fakeElement.addEventListener,
"keypress",
instance.removeAllNotifications
);
});
});
describe("registerBadgeNotificationListener", () => {
let msg_no_delay;
beforeEach(async () => {
await instance.init(sandbox.stub().resolves(), {
addImpression: fakeAddImpression,
sendTelemetry: fakeSendTelemetry,
});
sandbox.stub(instance, "addToolbarNotification").returns(fakeElement);
sandbox.stub(instance, "removeToolbarNotification");
msg_no_delay = {
...fxaMessage,
content: {
...fxaMessage.content,
delay: 0,
},
};
});
afterEach(() => {
instance.uninit();
});
it("should register a callback that adds/removes the notification", () => {
instance.registerBadgeNotificationListener(msg_no_delay);
assert.calledOnce(everyWindowStub.registerCallback);
assert.calledWithExactly(
everyWindowStub.registerCallback,
instance.id,
sinon.match.func,
sinon.match.func
);
const [, initFn, uninitFn] =
everyWindowStub.registerCallback.firstCall.args;
initFn(window);
// Test that it doesn't try to add a second notification
initFn(window);
assert.calledOnce(instance.addToolbarNotification);
assert.calledWithExactly(
instance.addToolbarNotification,
window,
msg_no_delay
);
uninitFn(window);
assert.calledOnce(instance.removeToolbarNotification);
assert.calledWithExactly(instance.removeToolbarNotification, fakeElement);
});
it("should unregister notifications when forcing a badge via devtools", () => {
instance.registerBadgeNotificationListener(msg_no_delay, { force: true });
assert.calledOnce(everyWindowStub.unregisterCallback);
assert.calledWithExactly(everyWindowStub.unregisterCallback, instance.id);
});
});
describe("removeToolbarNotification", () => {
it("should remove the notification", () => {
instance.removeToolbarNotification(fakeElement);
assert.callCount(fakeElement.removeAttribute, 4);
assert.calledWithExactly(fakeElement.removeAttribute, "badged");
assert.calledWithExactly(fakeElement.removeAttribute, "aria-labelledby");
assert.calledWithExactly(fakeElement.removeAttribute, "aria-describedby");
assert.calledWithExactly(fakeElement.removeAttribute, "showing-callout");
assert.calledOnce(fakeElement.classList.remove);
assert.calledWithExactly(fakeElement.classList.remove, "feature-callout");
assert.calledOnce(fakeElement.remove);
});
});
describe("removeAllNotifications", () => {
let blockMessageByIdStub;
let fakeEvent;
beforeEach(async () => {
await instance.init(sandbox.stub().resolves(), {
sendTelemetry: fakeSendTelemetry,
});
blockMessageByIdStub = sandbox.stub();
sandbox.stub(instance, "_blockMessageById").value(blockMessageByIdStub);
instance.state = { notification: { id: fxaMessage.id } };
fakeEvent = { target: { removeEventListener: sandbox.stub() } };
});
it("should call to block the message", () => {
instance.removeAllNotifications();
assert.calledOnce(blockMessageByIdStub);
assert.calledWithExactly(blockMessageByIdStub, fxaMessage.id);
});
it("should remove the window listener", () => {
instance.removeAllNotifications();
assert.calledOnce(everyWindowStub.unregisterCallback);
assert.calledWithExactly(everyWindowStub.unregisterCallback, instance.id);
});
it("should ignore right mouse button (mousedown event)", () => {
fakeEvent.type = "mousedown";
fakeEvent.button = 1; // not left click
instance.removeAllNotifications(fakeEvent);
assert.notCalled(fakeEvent.target.removeEventListener);
assert.notCalled(everyWindowStub.unregisterCallback);
});
it("should ignore right mouse button (click event)", () => {
fakeEvent.type = "click";
fakeEvent.button = 1; // not left click
instance.removeAllNotifications(fakeEvent);
assert.notCalled(fakeEvent.target.removeEventListener);
assert.notCalled(everyWindowStub.unregisterCallback);
});
it("should ignore keypresses that are not meant to focus the target", () => {
fakeEvent.type = "keypress";
fakeEvent.key = "\t"; // not enter
instance.removeAllNotifications(fakeEvent);
assert.notCalled(fakeEvent.target.removeEventListener);
assert.notCalled(everyWindowStub.unregisterCallback);
});
it("should remove the event listeners after succesfully focusing the element", () => {
fakeEvent.type = "click";
fakeEvent.button = 0;
instance.removeAllNotifications(fakeEvent);
assert.calledTwice(fakeEvent.target.removeEventListener);
assert.calledWithExactly(
fakeEvent.target.removeEventListener,
"mousedown",
instance.removeAllNotifications
);
assert.calledWithExactly(
fakeEvent.target.removeEventListener,
"keypress",
instance.removeAllNotifications
);
});
it("should send telemetry", () => {
fakeEvent.type = "click";
fakeEvent.button = 0;
sandbox.stub(instance, "sendUserEventTelemetry");
instance.removeAllNotifications(fakeEvent);
assert.calledOnce(instance.sendUserEventTelemetry);
assert.calledWithExactly(instance.sendUserEventTelemetry, "CLICK", {
id: "FXA_ACCOUNTS_BADGE",
});
});
it("should remove the event listeners after succesfully focusing the element", () => {
fakeEvent.type = "keypress";
fakeEvent.key = "Enter";
instance.removeAllNotifications(fakeEvent);
assert.calledTwice(fakeEvent.target.removeEventListener);
assert.calledWithExactly(
fakeEvent.target.removeEventListener,
"mousedown",
instance.removeAllNotifications
);
assert.calledWithExactly(
fakeEvent.target.removeEventListener,
"keypress",
instance.removeAllNotifications
);
});
});
describe("message with delay", () => {
let msg_with_delay;
beforeEach(async () => {
await instance.init(sandbox.stub().resolves(), {
addImpression: fakeAddImpression,
});
msg_with_delay = {
...fxaMessage,
content: {
...fxaMessage.content,
delay: 500,
},
};
sandbox.stub(instance, "registerBadgeToAllWindows");
});
afterEach(() => {
instance.uninit();
});
it("should register a cb to fire after msg.content.delay ms", () => {
instance.registerBadgeNotificationListener(msg_with_delay);
assert.calledOnce(setTimeoutStub);
assert.calledWithExactly(
setTimeoutStub,
sinon.match.func,
msg_with_delay.content.delay
);
const [cb] = setTimeoutStub.firstCall.args;
assert.notCalled(instance.registerBadgeToAllWindows);
cb();
assert.calledOnce(instance.registerBadgeToAllWindows);
assert.calledWithExactly(
instance.registerBadgeToAllWindows,
msg_with_delay
);
// Delayed actions should be executed inside requestIdleCallback
assert.calledOnce(requestIdleCallbackStub);
});
});
describe("#sendUserEventTelemetry", () => {
beforeEach(async () => {
await instance.init(sandbox.stub().resolves(), {
sendTelemetry: fakeSendTelemetry,
});
});
it("should check for private window and not send", () => {
isBrowserPrivateStub.returns(true);
instance.sendUserEventTelemetry("CLICK", { id: fxaMessage });
assert.notCalled(instance._sendTelemetry);
});
it("should check for private window and send", () => {
isBrowserPrivateStub.returns(false);
instance.sendUserEventTelemetry("CLICK", { id: fxaMessage });
assert.calledOnce(fakeSendTelemetry);
const [ping] = instance._sendTelemetry.firstCall.args;
assert.propertyVal(ping, "type", "TOOLBAR_BADGE_TELEMETRY");
assert.propertyVal(ping.data, "event", "CLICK");
});
});
});