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.calledOnce(fakeElement.setAttribute); assert.calledWithExactly(fakeElement.setAttribute, "badged", 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.calledThrice(fakeElement.removeAttribute); assert.calledWithExactly(fakeElement.removeAttribute, "badged"); assert.calledWithExactly(fakeElement.removeAttribute, "aria-labelledby"); assert.calledWithExactly(fakeElement.removeAttribute, "aria-describedby"); 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"); }); }); });