From 6bf0a5cb5034a7e684dcc3500e841785237ce2dd Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 19:32:43 +0200 Subject: Adding upstream version 1:115.7.0. Signed-off-by: Daniel Baumann --- .../newtab/test/unit/lib/ToolbarBadgeHub.test.js | 649 +++++++++++++++++++++ 1 file changed, 649 insertions(+) create mode 100644 browser/components/newtab/test/unit/lib/ToolbarBadgeHub.test.js (limited to 'browser/components/newtab/test/unit/lib/ToolbarBadgeHub.test.js') diff --git a/browser/components/newtab/test/unit/lib/ToolbarBadgeHub.test.js b/browser/components/newtab/test/unit/lib/ToolbarBadgeHub.test.js new file mode 100644 index 0000000000..12e70557f6 --- /dev/null +++ b/browser/components/newtab/test/unit/lib/ToolbarBadgeHub.test.js @@ -0,0 +1,649 @@ +import { _ToolbarBadgeHub } from "lib/ToolbarBadgeHub.jsm"; +import { GlobalOverrider } from "test/unit/utils"; +import { OnboardingMessageProvider } from "lib/OnboardingMessageProvider.jsm"; +import { _ToolbarPanelHub, ToolbarPanelHub } from "lib/ToolbarPanelHub.jsm"; + +describe("ToolbarBadgeHub", () => { + let sandbox; + let instance; + let fakeAddImpression; + let fakeSendTelemetry; + let isBrowserPrivateStub; + let fxaMessage; + let whatsnewMessage; + 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"); + whatsnewMessage = { + id: `WHATS_NEW_BADGE_71`, + template: "toolbar_badge", + content: { + delay: 1000, + target: "whats-new-menu-button", + action: { id: "show-whatsnew-button" }, + badgeDescription: { string_id: "cfr-badge-reader-label-newfeature" }, + }, + priority: 1, + trigger: { id: "toolbarBadgeUpdate" }, + frequency: { + // Makes it so that we track impressions for this message while at the + // same time it can have unlimited impressions + lifetime: Infinity, + }, + // Never saw this message or saw it in the past 4 days or more recent + targeting: `isWhatsNewPanelEnabled && + (!messageImpressions['WHATS_NEW_BADGE_71'] || + (messageImpressions['WHATS_NEW_BADGE_71']|length >= 1 && + currentDate|date - messageImpressions['WHATS_NEW_BADGE_71'][0] <= 4 * 24 * 3600 * 1000))`, + }; + 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({ + ToolbarPanelHub, + 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); + }); + it("should add a pref observer", async () => { + await instance.init(sandbox.stub().resolves(), {}); + + assert.calledOnce(addObserverStub); + assert.calledWithExactly( + addObserverStub, + instance.prefs.WHATSNEW_TOOLBAR_PANEL, + instance + ); + }); + }); + 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); + }); + it("should remove the pref observer", () => { + instance.uninit(); + + assert.calledOnce(removeObserverStub); + assert.calledWithExactly( + removeObserverStub, + instance.prefs.WHATSNEW_TOOLBAR_PANEL, + instance + ); + }); + }); + 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 + ); + }); + it("should execute actions if they exist", () => { + sandbox.stub(instance, "executeAction"); + instance.addToolbarNotification(target, whatsnewMessage); + + assert.calledOnce(instance.executeAction); + assert.calledWithExactly(instance.executeAction, { + ...whatsnewMessage.content.action, + message_id: whatsnewMessage.id, + }); + }); + it("should create a description element", () => { + sandbox.stub(instance, "executeAction"); + instance.addToolbarNotification(target, whatsnewMessage); + + assert.calledOnce(fakeDocument.createElement); + assert.calledWithExactly(fakeDocument.createElement, "span"); + }); + it("should set description id to element and to button", () => { + sandbox.stub(instance, "executeAction"); + instance.addToolbarNotification(target, whatsnewMessage); + + assert.calledWithExactly( + fakeElement.setAttribute, + "id", + "toolbarbutton-notification-description" + ); + assert.calledWithExactly( + fakeElement.setAttribute, + "aria-labelledby", + `toolbarbutton-notification-description ${whatsnewMessage.content.target}` + ); + }); + it("should attach fluent id to description", () => { + sandbox.stub(instance, "executeAction"); + instance.addToolbarNotification(target, whatsnewMessage); + + assert.calledOnce(fakeDocument.l10n.setAttributes); + assert.calledWithExactly( + fakeDocument.l10n.setAttributes, + fakeElement, + whatsnewMessage.content.badgeDescription.string_id + ); + }); + it("should add an impression for the message", () => { + instance.addToolbarNotification(target, whatsnewMessage); + + assert.calledOnce(instance._addImpression); + assert.calledWithExactly(instance._addImpression, whatsnewMessage); + }); + it("should send an impression ping", async () => { + sandbox.stub(instance, "sendUserEventTelemetry"); + instance.addToolbarNotification(target, whatsnewMessage); + + assert.calledOnce(instance.sendUserEventTelemetry); + assert.calledWithExactly( + instance.sendUserEventTelemetry, + "IMPRESSION", + whatsnewMessage + ); + }); + }); + 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); + }); + it("should only call executeAction for 'update_action' messages", () => { + const stub = sandbox.stub(instance, "executeAction"); + const updateActionMsg = { ...msg_no_delay, template: "update_action" }; + + instance.registerBadgeNotificationListener(updateActionMsg); + + assert.notCalled(everyWindowStub.registerCallback); + assert.calledOnce(stub); + }); + }); + describe("executeAction", () => { + let blockMessageByIdStub; + beforeEach(async () => { + blockMessageByIdStub = sandbox.stub(); + await instance.init(sandbox.stub().resolves(), { + blockMessageById: blockMessageByIdStub, + }); + }); + it("should call ToolbarPanelHub.enableToolbarButton", () => { + const stub = sandbox.stub( + _ToolbarPanelHub.prototype, + "enableToolbarButton" + ); + + instance.executeAction({ id: "show-whatsnew-button" }); + + assert.calledOnce(stub); + }); + it("should call ToolbarPanelHub.enableAppmenuButton", () => { + const stub = sandbox.stub( + _ToolbarPanelHub.prototype, + "enableAppmenuButton" + ); + + instance.executeAction({ id: "show-whatsnew-button" }); + + assert.calledOnce(stub); + }); + }); + 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"); + }); + }); + describe("#observe", () => { + it("should make a message request when the whats new pref is changed", () => { + sandbox.stub(instance, "messageRequest"); + + instance.observe("", "", instance.prefs.WHATSNEW_TOOLBAR_PANEL); + + assert.calledOnce(instance.messageRequest); + assert.calledWithExactly(instance.messageRequest, { + template: "toolbar_badge", + triggerId: "toolbarBadgeUpdate", + }); + }); + it("should not react to other pref changes", () => { + sandbox.stub(instance, "messageRequest"); + + instance.observe("", "", "foo"); + + assert.notCalled(instance.messageRequest); + }); + }); +}); -- cgit v1.2.3