tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

ToolbarBadgeHub.test.js (16098B)


      1 import { _ToolbarBadgeHub } from "modules/ToolbarBadgeHub.sys.mjs";
      2 import { GlobalOverrider } from "tests/unit/utils";
      3 import { OnboardingMessageProvider } from "modules/OnboardingMessageProvider.sys.mjs";
      4 
      5 describe("ToolbarBadgeHub", () => {
      6  let sandbox;
      7  let instance;
      8  let fakeAddImpression;
      9  let fakeSendTelemetry;
     10  let isBrowserPrivateStub;
     11  let fxaMessage;
     12  let fakeElement;
     13  let globals;
     14  let everyWindowStub;
     15  let clearTimeoutStub;
     16  let setTimeoutStub;
     17  let addObserverStub;
     18  let removeObserverStub;
     19  let getStringPrefStub;
     20  let clearUserPrefStub;
     21  let setStringPrefStub;
     22  let requestIdleCallbackStub;
     23  let fakeWindow;
     24  beforeEach(async () => {
     25    globals = new GlobalOverrider();
     26    sandbox = sinon.createSandbox();
     27    instance = new _ToolbarBadgeHub();
     28    fakeAddImpression = sandbox.stub();
     29    fakeSendTelemetry = sandbox.stub();
     30    isBrowserPrivateStub = sandbox.stub();
     31    const onboardingMsgs =
     32      await OnboardingMessageProvider.getUntranslatedMessages();
     33    fxaMessage = onboardingMsgs.find(({ id }) => id === "FXA_ACCOUNTS_BADGE");
     34    fakeElement = {
     35      classList: {
     36        add: sandbox.stub(),
     37        remove: sandbox.stub(),
     38      },
     39      setAttribute: sandbox.stub(),
     40      removeAttribute: sandbox.stub(),
     41      querySelector: sandbox.stub(),
     42      addEventListener: sandbox.stub(),
     43      remove: sandbox.stub(),
     44      appendChild: sandbox.stub(),
     45    };
     46    // Share the same element when selecting child nodes
     47    fakeElement.querySelector.returns(fakeElement);
     48    everyWindowStub = {
     49      registerCallback: sandbox.stub(),
     50      unregisterCallback: sandbox.stub(),
     51    };
     52    clearTimeoutStub = sandbox.stub();
     53    setTimeoutStub = sandbox.stub();
     54    fakeWindow = {
     55      MozXULElement: { insertFTLIfNeeded: sandbox.stub() },
     56      ownerGlobal: {
     57        gBrowser: {
     58          selectedBrowser: "browser",
     59        },
     60      },
     61    };
     62    addObserverStub = sandbox.stub();
     63    removeObserverStub = sandbox.stub();
     64    getStringPrefStub = sandbox.stub();
     65    clearUserPrefStub = sandbox.stub();
     66    setStringPrefStub = sandbox.stub();
     67    requestIdleCallbackStub = sandbox.stub().callsFake(fn => fn());
     68    globals.set({
     69      requestIdleCallback: requestIdleCallbackStub,
     70      EveryWindow: everyWindowStub,
     71      PrivateBrowsingUtils: { isBrowserPrivate: isBrowserPrivateStub },
     72      setTimeout: setTimeoutStub,
     73      clearTimeout: clearTimeoutStub,
     74      Services: {
     75        wm: {
     76          getMostRecentWindow: () => fakeWindow,
     77        },
     78        prefs: {
     79          addObserver: addObserverStub,
     80          removeObserver: removeObserverStub,
     81          getStringPref: getStringPrefStub,
     82          clearUserPref: clearUserPrefStub,
     83          setStringPref: setStringPrefStub,
     84        },
     85      },
     86    });
     87  });
     88  afterEach(() => {
     89    sandbox.restore();
     90    globals.restore();
     91  });
     92  it("should create an instance", () => {
     93    assert.ok(instance);
     94  });
     95  describe("#init", () => {
     96    it("should make a single messageRequest on init", async () => {
     97      sandbox.stub(instance, "messageRequest");
     98      const waitForInitialized = sandbox.stub().resolves();
     99 
    100      await instance.init(waitForInitialized, {});
    101      await instance.init(waitForInitialized, {});
    102      assert.calledOnce(instance.messageRequest);
    103      assert.calledWithExactly(instance.messageRequest, {
    104        template: "toolbar_badge",
    105        triggerId: "toolbarBadgeUpdate",
    106      });
    107 
    108      instance.uninit();
    109 
    110      await instance.init(waitForInitialized, {});
    111 
    112      assert.calledTwice(instance.messageRequest);
    113    });
    114  });
    115  describe("#uninit", () => {
    116    beforeEach(async () => {
    117      await instance.init(sandbox.stub().resolves(), {});
    118    });
    119    it("should clear any setTimeout cbs", async () => {
    120      await instance.init(sandbox.stub().resolves(), {});
    121 
    122      instance.state.showBadgeTimeoutId = 2;
    123 
    124      instance.uninit();
    125 
    126      assert.calledOnce(clearTimeoutStub);
    127      assert.calledWithExactly(clearTimeoutStub, 2);
    128    });
    129  });
    130  describe("messageRequest", () => {
    131    let handleMessageRequestStub;
    132    beforeEach(() => {
    133      handleMessageRequestStub = sandbox.stub().returns(fxaMessage);
    134      sandbox
    135        .stub(instance, "_handleMessageRequest")
    136        .value(handleMessageRequestStub);
    137      sandbox.stub(instance, "registerBadgeNotificationListener");
    138    });
    139    it("should fetch a message with the provided trigger and template", async () => {
    140      await instance.messageRequest({
    141        triggerId: "trigger",
    142        template: "template",
    143      });
    144 
    145      assert.calledOnce(handleMessageRequestStub);
    146      assert.calledWithExactly(handleMessageRequestStub, {
    147        triggerId: "trigger",
    148        template: "template",
    149      });
    150    });
    151    it("should call addToolbarNotification with browser window and message", async () => {
    152      await instance.messageRequest("trigger");
    153 
    154      assert.calledOnce(instance.registerBadgeNotificationListener);
    155      assert.calledWithExactly(
    156        instance.registerBadgeNotificationListener,
    157        fxaMessage
    158      );
    159    });
    160    it("shouldn't do anything if no message is provided", async () => {
    161      handleMessageRequestStub.resolves(null);
    162      await instance.messageRequest({ triggerId: "trigger" });
    163 
    164      assert.notCalled(instance.registerBadgeNotificationListener);
    165    });
    166    it("should record a message request time", async () => {
    167      const fakeTimerId = 42;
    168      const start = sandbox
    169        .stub(global.Glean.messagingSystem.messageRequestTime, "start")
    170        .returns(fakeTimerId);
    171      const stopAndAccumulate = sandbox.stub(
    172        global.Glean.messagingSystem.messageRequestTime,
    173        "stopAndAccumulate"
    174      );
    175      handleMessageRequestStub.returns(null);
    176 
    177      await instance.messageRequest({ triggerId: "trigger" });
    178 
    179      assert.calledOnce(start);
    180      assert.calledWithExactly(start);
    181      assert.calledOnce(stopAndAccumulate);
    182      assert.calledWithExactly(stopAndAccumulate, fakeTimerId);
    183    });
    184  });
    185  describe("addToolbarNotification", () => {
    186    let target;
    187    let fakeDocument;
    188    beforeEach(async () => {
    189      await instance.init(sandbox.stub().resolves(), {
    190        addImpression: fakeAddImpression,
    191        sendTelemetry: fakeSendTelemetry,
    192      });
    193      fakeDocument = {
    194        getElementById: sandbox.stub().returns(fakeElement),
    195        createElement: sandbox.stub().returns(fakeElement),
    196        l10n: { setAttributes: sandbox.stub() },
    197      };
    198      target = { ...fakeWindow, browser: { ownerDocument: fakeDocument } };
    199    });
    200    afterEach(() => {
    201      instance.uninit();
    202    });
    203    it("shouldn't do anything if target element is not found", () => {
    204      fakeDocument.getElementById.returns(null);
    205      instance.addToolbarNotification(target, fxaMessage);
    206 
    207      assert.notCalled(fakeElement.setAttribute);
    208    });
    209    it("should target the element specified in the message", () => {
    210      instance.addToolbarNotification(target, fxaMessage);
    211 
    212      assert.calledOnce(fakeDocument.getElementById);
    213      assert.calledWithExactly(
    214        fakeDocument.getElementById,
    215        fxaMessage.content.target
    216      );
    217    });
    218    it("should show a notification", () => {
    219      instance.addToolbarNotification(target, fxaMessage);
    220 
    221      assert.calledTwice(fakeElement.setAttribute);
    222      assert.calledWithExactly(fakeElement.setAttribute, "badged", true);
    223      assert.calledWithExactly(
    224        fakeElement.setAttribute,
    225        "showing-callout",
    226        true
    227      );
    228      assert.calledWithExactly(fakeElement.classList.add, "feature-callout");
    229    });
    230    it("should attach a cb on the notification", () => {
    231      instance.addToolbarNotification(target, fxaMessage);
    232 
    233      assert.calledTwice(fakeElement.addEventListener);
    234      assert.calledWithExactly(
    235        fakeElement.addEventListener,
    236        "mousedown",
    237        instance.removeAllNotifications
    238      );
    239      assert.calledWithExactly(
    240        fakeElement.addEventListener,
    241        "keypress",
    242        instance.removeAllNotifications
    243      );
    244    });
    245  });
    246  describe("registerBadgeNotificationListener", () => {
    247    let msg_no_delay;
    248    beforeEach(async () => {
    249      await instance.init(sandbox.stub().resolves(), {
    250        addImpression: fakeAddImpression,
    251        sendTelemetry: fakeSendTelemetry,
    252      });
    253      sandbox.stub(instance, "addToolbarNotification").returns(fakeElement);
    254      sandbox.stub(instance, "removeToolbarNotification");
    255      msg_no_delay = {
    256        ...fxaMessage,
    257        content: {
    258          ...fxaMessage.content,
    259          delay: 0,
    260        },
    261      };
    262    });
    263    afterEach(() => {
    264      instance.uninit();
    265    });
    266    it("should register a callback that adds/removes the notification", () => {
    267      instance.registerBadgeNotificationListener(msg_no_delay);
    268 
    269      assert.calledOnce(everyWindowStub.registerCallback);
    270      assert.calledWithExactly(
    271        everyWindowStub.registerCallback,
    272        instance.id,
    273        sinon.match.func,
    274        sinon.match.func
    275      );
    276 
    277      const [, initFn, uninitFn] =
    278        everyWindowStub.registerCallback.firstCall.args;
    279 
    280      initFn(window);
    281      // Test that it doesn't try to add a second notification
    282      initFn(window);
    283 
    284      assert.calledOnce(instance.addToolbarNotification);
    285      assert.calledWithExactly(
    286        instance.addToolbarNotification,
    287        window,
    288        msg_no_delay
    289      );
    290 
    291      uninitFn(window);
    292 
    293      assert.calledOnce(instance.removeToolbarNotification);
    294      assert.calledWithExactly(instance.removeToolbarNotification, fakeElement);
    295    });
    296    it("should unregister notifications when forcing a badge via devtools", () => {
    297      instance.registerBadgeNotificationListener(msg_no_delay, { force: true });
    298 
    299      assert.calledOnce(everyWindowStub.unregisterCallback);
    300      assert.calledWithExactly(everyWindowStub.unregisterCallback, instance.id);
    301    });
    302  });
    303  describe("removeToolbarNotification", () => {
    304    it("should remove the notification", () => {
    305      instance.removeToolbarNotification(fakeElement);
    306 
    307      assert.callCount(fakeElement.removeAttribute, 4);
    308      assert.calledWithExactly(fakeElement.removeAttribute, "badged");
    309      assert.calledWithExactly(fakeElement.removeAttribute, "aria-labelledby");
    310      assert.calledWithExactly(fakeElement.removeAttribute, "aria-describedby");
    311      assert.calledWithExactly(fakeElement.removeAttribute, "showing-callout");
    312      assert.calledOnce(fakeElement.classList.remove);
    313      assert.calledWithExactly(fakeElement.classList.remove, "feature-callout");
    314      assert.calledOnce(fakeElement.remove);
    315    });
    316  });
    317  describe("removeAllNotifications", () => {
    318    let blockMessageByIdStub;
    319    let fakeEvent;
    320    beforeEach(async () => {
    321      await instance.init(sandbox.stub().resolves(), {
    322        sendTelemetry: fakeSendTelemetry,
    323      });
    324      blockMessageByIdStub = sandbox.stub();
    325      sandbox.stub(instance, "_blockMessageById").value(blockMessageByIdStub);
    326      instance.state = { notification: { id: fxaMessage.id } };
    327      fakeEvent = { target: { removeEventListener: sandbox.stub() } };
    328    });
    329    it("should call to block the message", () => {
    330      instance.removeAllNotifications();
    331 
    332      assert.calledOnce(blockMessageByIdStub);
    333      assert.calledWithExactly(blockMessageByIdStub, fxaMessage.id);
    334    });
    335    it("should remove the window listener", () => {
    336      instance.removeAllNotifications();
    337 
    338      assert.calledOnce(everyWindowStub.unregisterCallback);
    339      assert.calledWithExactly(everyWindowStub.unregisterCallback, instance.id);
    340    });
    341    it("should ignore right mouse button (mousedown event)", () => {
    342      fakeEvent.type = "mousedown";
    343      fakeEvent.button = 1; // not left click
    344 
    345      instance.removeAllNotifications(fakeEvent);
    346 
    347      assert.notCalled(fakeEvent.target.removeEventListener);
    348      assert.notCalled(everyWindowStub.unregisterCallback);
    349    });
    350    it("should ignore right mouse button (click event)", () => {
    351      fakeEvent.type = "click";
    352      fakeEvent.button = 1; // not left click
    353 
    354      instance.removeAllNotifications(fakeEvent);
    355 
    356      assert.notCalled(fakeEvent.target.removeEventListener);
    357      assert.notCalled(everyWindowStub.unregisterCallback);
    358    });
    359    it("should ignore keypresses that are not meant to focus the target", () => {
    360      fakeEvent.type = "keypress";
    361      fakeEvent.key = "\t"; // not enter
    362 
    363      instance.removeAllNotifications(fakeEvent);
    364 
    365      assert.notCalled(fakeEvent.target.removeEventListener);
    366      assert.notCalled(everyWindowStub.unregisterCallback);
    367    });
    368    it("should remove the event listeners after succesfully focusing the element", () => {
    369      fakeEvent.type = "click";
    370      fakeEvent.button = 0;
    371 
    372      instance.removeAllNotifications(fakeEvent);
    373 
    374      assert.calledTwice(fakeEvent.target.removeEventListener);
    375      assert.calledWithExactly(
    376        fakeEvent.target.removeEventListener,
    377        "mousedown",
    378        instance.removeAllNotifications
    379      );
    380      assert.calledWithExactly(
    381        fakeEvent.target.removeEventListener,
    382        "keypress",
    383        instance.removeAllNotifications
    384      );
    385    });
    386    it("should send telemetry", () => {
    387      fakeEvent.type = "click";
    388      fakeEvent.button = 0;
    389      sandbox.stub(instance, "sendUserEventTelemetry");
    390 
    391      instance.removeAllNotifications(fakeEvent);
    392 
    393      assert.calledOnce(instance.sendUserEventTelemetry);
    394      assert.calledWithExactly(instance.sendUserEventTelemetry, "CLICK", {
    395        id: "FXA_ACCOUNTS_BADGE",
    396      });
    397    });
    398    it("should remove the event listeners after succesfully focusing the element", () => {
    399      fakeEvent.type = "keypress";
    400      fakeEvent.key = "Enter";
    401 
    402      instance.removeAllNotifications(fakeEvent);
    403 
    404      assert.calledTwice(fakeEvent.target.removeEventListener);
    405      assert.calledWithExactly(
    406        fakeEvent.target.removeEventListener,
    407        "mousedown",
    408        instance.removeAllNotifications
    409      );
    410      assert.calledWithExactly(
    411        fakeEvent.target.removeEventListener,
    412        "keypress",
    413        instance.removeAllNotifications
    414      );
    415    });
    416  });
    417  describe("message with delay", () => {
    418    let msg_with_delay;
    419    beforeEach(async () => {
    420      await instance.init(sandbox.stub().resolves(), {
    421        addImpression: fakeAddImpression,
    422      });
    423      msg_with_delay = {
    424        ...fxaMessage,
    425        content: {
    426          ...fxaMessage.content,
    427          delay: 500,
    428        },
    429      };
    430      sandbox.stub(instance, "registerBadgeToAllWindows");
    431    });
    432    afterEach(() => {
    433      instance.uninit();
    434    });
    435    it("should register a cb to fire after msg.content.delay ms", () => {
    436      instance.registerBadgeNotificationListener(msg_with_delay);
    437 
    438      assert.calledOnce(setTimeoutStub);
    439      assert.calledWithExactly(
    440        setTimeoutStub,
    441        sinon.match.func,
    442        msg_with_delay.content.delay
    443      );
    444 
    445      const [cb] = setTimeoutStub.firstCall.args;
    446 
    447      assert.notCalled(instance.registerBadgeToAllWindows);
    448 
    449      cb();
    450 
    451      assert.calledOnce(instance.registerBadgeToAllWindows);
    452      assert.calledWithExactly(
    453        instance.registerBadgeToAllWindows,
    454        msg_with_delay
    455      );
    456      // Delayed actions should be executed inside requestIdleCallback
    457      assert.calledOnce(requestIdleCallbackStub);
    458    });
    459  });
    460  describe("#sendUserEventTelemetry", () => {
    461    beforeEach(async () => {
    462      await instance.init(sandbox.stub().resolves(), {
    463        sendTelemetry: fakeSendTelemetry,
    464      });
    465    });
    466    it("should check for private window and not send", () => {
    467      isBrowserPrivateStub.returns(true);
    468 
    469      instance.sendUserEventTelemetry("CLICK", { id: fxaMessage });
    470 
    471      assert.notCalled(instance._sendTelemetry);
    472    });
    473    it("should check for private window and send", () => {
    474      isBrowserPrivateStub.returns(false);
    475 
    476      instance.sendUserEventTelemetry("CLICK", { id: fxaMessage });
    477 
    478      assert.calledOnce(fakeSendTelemetry);
    479      const [ping] = instance._sendTelemetry.firstCall.args;
    480      assert.propertyVal(ping, "type", "TOOLBAR_BADGE_TELEMETRY");
    481      assert.propertyVal(ping.data, "event", "CLICK");
    482    });
    483  });
    484 });