tor-browser

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

CFRPageActions.test.js (53198B)


      1 /* eslint max-nested-callbacks: ["error", 100] */
      2 
      3 import { CFRPageActions, PageAction } from "modules/CFRPageActions.sys.mjs";
      4 import { FAKE_RECOMMENDATION } from "./constants";
      5 import { GlobalOverrider } from "tests/unit/utils";
      6 import { CFRMessageProvider } from "modules/CFRMessageProvider.sys.mjs";
      7 
      8 describe("CFRPageActions", () => {
      9  let sandbox;
     10  let clock;
     11  let fakeRecommendation;
     12  let fakeHost;
     13  let fakeBrowser;
     14  let dispatchStub;
     15  let globals;
     16  let containerElem;
     17  let elements;
     18  let announceStub;
     19  let fakeRemoteL10n;
     20  let isElmVisibleStub;
     21  let getWidgetStub;
     22 
     23  const elementIDs = [
     24    "urlbar",
     25    "urlbar-input",
     26    "contextual-feature-recommendation",
     27    "cfr-button",
     28    "cfr-label",
     29    "contextual-feature-recommendation-notification",
     30    "cfr-notification-header-label",
     31    "cfr-notification-header-link",
     32    "cfr-notification-header-image",
     33    "cfr-notification-author",
     34    "cfr-notification-footer",
     35    "cfr-notification-footer-text",
     36    "cfr-notification-footer-filled-stars",
     37    "cfr-notification-footer-empty-stars",
     38    "cfr-notification-footer-users",
     39    "cfr-notification-footer-spacer",
     40    "cfr-notification-footer-learn-more-link",
     41  ];
     42  const elementClassNames = ["popup-notification-body-container"];
     43 
     44  beforeEach(() => {
     45    sandbox = sinon.createSandbox();
     46    clock = sandbox.useFakeTimers();
     47    isElmVisibleStub = sandbox.stub().returns(true);
     48    getWidgetStub = sandbox.stub();
     49 
     50    announceStub = sandbox.stub();
     51    const A11yUtils = { announce: announceStub };
     52    fakeRecommendation = { ...FAKE_RECOMMENDATION };
     53    fakeHost = "mozilla.org";
     54    fakeBrowser = {
     55      documentURI: {
     56        scheme: "https",
     57        host: fakeHost,
     58      },
     59      ownerGlobal: window,
     60    };
     61    dispatchStub = sandbox.stub();
     62 
     63    fakeRemoteL10n = {
     64      l10n: {},
     65      reloadL10n: sandbox.stub(),
     66      createElement: sandbox.stub().returns(document.createElement("div")),
     67    };
     68 
     69    const gURLBar = document.createElement("div");
     70    gURLBar.inputField = document.createElement("input");
     71 
     72    globals = new GlobalOverrider();
     73    globals.set({
     74      RemoteL10n: fakeRemoteL10n,
     75      promiseDocumentFlushed: sandbox
     76        .stub()
     77        .callsFake(fn => Promise.resolve(fn())),
     78      PopupNotifications: {
     79        show: sandbox.stub(),
     80        remove: sandbox.stub(),
     81      },
     82      PrivateBrowsingUtils: { isWindowPrivate: sandbox.stub().returns(false) },
     83      gBrowser: { selectedBrowser: fakeBrowser },
     84      A11yUtils,
     85      gURLBar,
     86      isElementVisible: isElmVisibleStub,
     87      CustomizableUI: { getWidget: getWidgetStub },
     88    });
     89    document.createXULElement = document.createElement;
     90 
     91    elements = {};
     92    const [body] = document.getElementsByTagName("body");
     93    containerElem = document.createElement("div");
     94    body.appendChild(containerElem);
     95    for (const id of elementIDs) {
     96      const elem = document.createElement("div");
     97      elem.setAttribute("id", id);
     98      containerElem.appendChild(elem);
     99      elements[id] = elem;
    100    }
    101    for (const className of elementClassNames) {
    102      const elem = document.createElement("div");
    103      elem.setAttribute("class", className);
    104      containerElem.appendChild(elem);
    105      elements[className] = elem;
    106    }
    107  });
    108 
    109  afterEach(() => {
    110    CFRPageActions.clearRecommendations();
    111    containerElem.remove();
    112    sandbox.restore();
    113    globals.restore();
    114  });
    115 
    116  describe("PageAction", () => {
    117    let pageAction;
    118 
    119    beforeEach(() => {
    120      pageAction = new PageAction(window, dispatchStub);
    121    });
    122 
    123    describe("#addImpression", () => {
    124      it("should call _sendTelemetry with the impression payload", () => {
    125        const recommendation = {
    126          id: "foo",
    127          content: { bucket_id: "bar" },
    128        };
    129        sandbox.spy(pageAction, "_sendTelemetry");
    130 
    131        pageAction.addImpression(recommendation);
    132 
    133        assert.calledWith(pageAction._sendTelemetry, {
    134          message_id: "foo",
    135          bucket_id: "bar",
    136          event: "IMPRESSION",
    137        });
    138      });
    139    });
    140 
    141    describe("#showAddressBarNotifier", () => {
    142      it("should un-hideAddressBarNotifier the element and set the right label value", async () => {
    143        await pageAction.showAddressBarNotifier(fakeRecommendation);
    144        assert.isFalse(pageAction.container.hidden);
    145        assert.equal(
    146          pageAction.label.value,
    147          fakeRecommendation.content.notification_text
    148        );
    149      });
    150      it("should wait for the document layout to flush", async () => {
    151        sandbox.spy(pageAction.label, "getClientRects");
    152        await pageAction.showAddressBarNotifier(fakeRecommendation);
    153        assert.calledOnce(global.promiseDocumentFlushed);
    154        assert.callOrder(
    155          global.promiseDocumentFlushed,
    156          pageAction.label.getClientRects
    157        );
    158      });
    159      it("should set the CSS variable --cfr-label-width correctly", async () => {
    160        await pageAction.showAddressBarNotifier(fakeRecommendation);
    161        const expectedWidth = pageAction.label.getClientRects()[0].width;
    162        assert.equal(
    163          pageAction.urlbar.style.getPropertyValue("--cfr-label-width"),
    164          `${expectedWidth}px`
    165        );
    166      });
    167      it("should cause an expansion, and dispatch an impression if `expand` is true", async () => {
    168        sandbox.spy(pageAction, "_clearScheduledStateChanges");
    169        sandbox.spy(pageAction, "_expand");
    170        sandbox.spy(pageAction, "_dispatchImpression");
    171 
    172        await pageAction.showAddressBarNotifier(fakeRecommendation);
    173        assert.notCalled(pageAction._dispatchImpression);
    174        clock.tick(1001);
    175        assert.notEqual(
    176          pageAction.urlbar.getAttribute("cfr-recommendation-state"),
    177          "expanded"
    178        );
    179 
    180        await pageAction.showAddressBarNotifier(fakeRecommendation, true);
    181        assert.calledOnce(pageAction._clearScheduledStateChanges);
    182        clock.tick(1001);
    183        assert.equal(
    184          pageAction.urlbar.getAttribute("cfr-recommendation-state"),
    185          "expanded"
    186        );
    187        assert.calledOnce(pageAction._dispatchImpression);
    188        assert.calledWith(pageAction._dispatchImpression, fakeRecommendation);
    189      });
    190      it("should send telemetry if `expand` is true and the id and bucket_id are provided", async () => {
    191        await pageAction.showAddressBarNotifier(fakeRecommendation, true);
    192        assert.calledWith(dispatchStub, {
    193          type: "DOORHANGER_TELEMETRY",
    194          data: {
    195            action: "cfr_user_event",
    196            source: "CFR",
    197            message_id: fakeRecommendation.id,
    198            bucket_id: fakeRecommendation.content.bucket_id,
    199            event: "IMPRESSION",
    200          },
    201        });
    202      });
    203    });
    204 
    205    describe("#hideAddressBarNotifier", () => {
    206      it("should hideAddressBarNotifier the container, cancel any state changes, and remove the state attribute", () => {
    207        sandbox.spy(pageAction, "_clearScheduledStateChanges");
    208        pageAction.hideAddressBarNotifier();
    209        assert.isTrue(pageAction.container.hidden);
    210        assert.calledOnce(pageAction._clearScheduledStateChanges);
    211        assert.isNull(
    212          pageAction.urlbar.getAttribute("cfr-recommendation-state")
    213        );
    214      });
    215      it("should remove the `currentNotification`", () => {
    216        const notification = {};
    217        pageAction.currentNotification = notification;
    218        pageAction.hideAddressBarNotifier();
    219        assert.calledWith(global.PopupNotifications.remove, notification);
    220      });
    221    });
    222 
    223    describe("#_expand", () => {
    224      beforeEach(() => {
    225        pageAction._clearScheduledStateChanges();
    226        pageAction.urlbar.removeAttribute("cfr-recommendation-state");
    227      });
    228      it("without a delay, should clear other state changes and set the state to 'expanded'", () => {
    229        sandbox.spy(pageAction, "_clearScheduledStateChanges");
    230        pageAction._expand();
    231        assert.calledOnce(pageAction._clearScheduledStateChanges);
    232        assert.equal(
    233          pageAction.urlbar.getAttribute("cfr-recommendation-state"),
    234          "expanded"
    235        );
    236      });
    237      it("with a delay, should set the expanded state after the correct amount of time", () => {
    238        const delay = 1234;
    239        pageAction._expand(delay);
    240        // We expect that an expansion has been scheduled
    241        assert.lengthOf(pageAction.stateTransitionTimeoutIDs, 1);
    242        clock.tick(delay + 1);
    243        assert.equal(
    244          pageAction.urlbar.getAttribute("cfr-recommendation-state"),
    245          "expanded"
    246        );
    247      });
    248    });
    249 
    250    describe("#_collapse", () => {
    251      beforeEach(() => {
    252        pageAction._clearScheduledStateChanges();
    253        pageAction.urlbar.removeAttribute("cfr-recommendation-state");
    254      });
    255      it("without a delay, should clear other state changes and set the state to collapsed only if it's already expanded", () => {
    256        sandbox.spy(pageAction, "_clearScheduledStateChanges");
    257        pageAction._collapse();
    258        assert.calledOnce(pageAction._clearScheduledStateChanges);
    259        assert.isNull(
    260          pageAction.urlbar.getAttribute("cfr-recommendation-state")
    261        );
    262        pageAction.urlbar.setAttribute("cfr-recommendation-state", "expanded");
    263        pageAction._collapse();
    264        assert.equal(
    265          pageAction.urlbar.getAttribute("cfr-recommendation-state"),
    266          "collapsed"
    267        );
    268      });
    269      it("with a delay, should set the collapsed state after the correct amount of time", () => {
    270        const delay = 1234;
    271        pageAction._collapse(delay);
    272        clock.tick(delay + 1);
    273        // The state was _not_ "expanded" and so should not have been set to "collapsed"
    274        assert.isNull(
    275          pageAction.urlbar.getAttribute("cfr-recommendation-state")
    276        );
    277 
    278        pageAction._expand();
    279        pageAction._collapse(delay);
    280        // We expect that a collapse has been scheduled
    281        assert.lengthOf(pageAction.stateTransitionTimeoutIDs, 1);
    282        clock.tick(delay + 1);
    283        // This time it was "expanded" so should now (after the delay) be "collapsed"
    284        assert.equal(
    285          pageAction.urlbar.getAttribute("cfr-recommendation-state"),
    286          "collapsed"
    287        );
    288      });
    289    });
    290 
    291    describe("#_clearScheduledStateChanges", () => {
    292      it("should call .clearTimeout on all stored timeoutIDs", () => {
    293        pageAction.stateTransitionTimeoutIDs = [42, 73, 1997];
    294        sandbox.spy(pageAction.window, "clearTimeout");
    295        pageAction._clearScheduledStateChanges();
    296        assert.calledThrice(pageAction.window.clearTimeout);
    297        assert.calledWith(pageAction.window.clearTimeout, 42);
    298        assert.calledWith(pageAction.window.clearTimeout, 73);
    299        assert.calledWith(pageAction.window.clearTimeout, 1997);
    300      });
    301    });
    302 
    303    describe("#_popupStateChange", () => {
    304      it("should collapse the notification and send dismiss telemetry on 'dismissed'", () => {
    305        pageAction._expand();
    306 
    307        sandbox.spy(pageAction, "_sendTelemetry");
    308 
    309        pageAction._popupStateChange("dismissed");
    310        assert.equal(
    311          pageAction.urlbar.getAttribute("cfr-recommendation-state"),
    312          "collapsed"
    313        );
    314 
    315        assert.equal(
    316          pageAction._sendTelemetry.lastCall.args[0].event,
    317          "DISMISS"
    318        );
    319      });
    320      it("should remove the notification on 'removed'", () => {
    321        pageAction._expand();
    322        const fakeNotification = {};
    323 
    324        pageAction.currentNotification = fakeNotification;
    325        pageAction._popupStateChange("removed");
    326        assert.calledOnce(global.PopupNotifications.remove);
    327        assert.calledWith(global.PopupNotifications.remove, fakeNotification);
    328      });
    329      it("should do nothing for other states", () => {
    330        pageAction._popupStateChange("opened");
    331        assert.notCalled(global.PopupNotifications.remove);
    332      });
    333    });
    334 
    335    describe("#dispatchUserAction", () => {
    336      it("should call ._dispatchCFRAction with the right action", () => {
    337        const fakeAction = {};
    338        pageAction.dispatchUserAction(fakeAction);
    339        assert.calledOnce(dispatchStub);
    340        assert.calledWith(
    341          dispatchStub,
    342          { type: "USER_ACTION", data: fakeAction },
    343          fakeBrowser
    344        );
    345      });
    346    });
    347 
    348    describe("#_dispatchImpression", () => {
    349      it("should call ._dispatchCFRAction with the right action", () => {
    350        pageAction._dispatchImpression("fake impression");
    351        assert.calledWith(dispatchStub, {
    352          type: "IMPRESSION",
    353          data: "fake impression",
    354        });
    355      });
    356    });
    357 
    358    describe("#_sendTelemetry", () => {
    359      it("should call ._dispatchCFRAction with the right action", () => {
    360        const fakePing = { message_id: 42 };
    361        pageAction._sendTelemetry(fakePing);
    362        assert.calledWith(dispatchStub, {
    363          type: "DOORHANGER_TELEMETRY",
    364          data: {
    365            action: "cfr_user_event",
    366            source: "CFR",
    367            message_id: 42,
    368          },
    369        });
    370      });
    371    });
    372 
    373    describe("#_blockMessage", () => {
    374      it("should call ._dispatchCFRAction with the right action", () => {
    375        pageAction._blockMessage("fake id");
    376        assert.calledOnce(dispatchStub);
    377        assert.calledWith(dispatchStub, {
    378          type: "BLOCK_MESSAGE_BY_ID",
    379          data: { id: "fake id" },
    380        });
    381      });
    382    });
    383 
    384    describe("#getStrings", () => {
    385      let formatMessagesStub;
    386      const localeStrings = [
    387        {
    388          value: "你好世界",
    389          attributes: [
    390            { name: "first_attr", value: 42 },
    391            { name: "second_attr", value: "some string" },
    392            { name: "third_attr", value: [1, 2, 3] },
    393          ],
    394        },
    395      ];
    396 
    397      beforeEach(() => {
    398        formatMessagesStub = sandbox
    399          .stub()
    400          .withArgs({ id: "hello_world" })
    401          .resolves(localeStrings);
    402        global.RemoteL10n.l10n.formatMessages = formatMessagesStub;
    403      });
    404 
    405      it("should return the argument if a string_id is not defined", async () => {
    406        assert.deepEqual(await pageAction.getStrings({}), {});
    407        assert.equal(await pageAction.getStrings("some string"), "some string");
    408      });
    409      it("should get the right locale string", async () => {
    410        assert.equal(
    411          await pageAction.getStrings({ string_id: "hello_world" }),
    412          localeStrings[0].value
    413        );
    414      });
    415      it("should return the right sub-attribute if specified", async () => {
    416        assert.equal(
    417          await pageAction.getStrings(
    418            { string_id: "hello_world" },
    419            "second_attr"
    420          ),
    421          "some string"
    422        );
    423      });
    424      it("should attach attributes to string overrides", async () => {
    425        const fromJson = { value: "Add Now", attributes: { accesskey: "A" } };
    426 
    427        const result = await pageAction.getStrings(fromJson);
    428 
    429        assert.equal(result, fromJson.value);
    430        assert.propertyVal(result.attributes, "accesskey", "A");
    431      });
    432      it("should return subAttributes when doing string overrides", async () => {
    433        const fromJson = { value: "Add Now", attributes: { accesskey: "A" } };
    434 
    435        const result = await pageAction.getStrings(fromJson, "accesskey");
    436 
    437        assert.equal(result, "A");
    438      });
    439      it("should resolve ftl strings and attach subAttributes", async () => {
    440        const fromFtl = { string_id: "cfr-doorhanger-extension-ok-button" };
    441        formatMessagesStub.resolves([
    442          { value: "Add Now", attributes: [{ name: "accesskey", value: "A" }] },
    443        ]);
    444 
    445        const result = await pageAction.getStrings(fromFtl);
    446 
    447        assert.equal(result, "Add Now");
    448        assert.propertyVal(result.attributes, "accesskey", "A");
    449      });
    450      it("should return subAttributes from ftl ids", async () => {
    451        const fromFtl = { string_id: "cfr-doorhanger-extension-ok-button" };
    452        formatMessagesStub.resolves([
    453          { value: "Add Now", attributes: [{ name: "accesskey", value: "A" }] },
    454        ]);
    455 
    456        const result = await pageAction.getStrings(fromFtl, "accesskey");
    457 
    458        assert.equal(result, "A");
    459      });
    460      it("should report an error when no attributes are present but subAttribute is requested", async () => {
    461        const fromJson = { value: "Foo" };
    462        const stub = sandbox.stub(global.console, "error");
    463 
    464        await pageAction.getStrings(fromJson, "accesskey");
    465 
    466        assert.calledOnce(stub);
    467        stub.restore();
    468      });
    469    });
    470 
    471    describe("#_cfrUrlbarButtonClick", () => {
    472      let translateElementsStub;
    473      let setAttributesStub;
    474      let getStringsStub;
    475      beforeEach(async () => {
    476        CFRPageActions.PageActionMap.set(fakeBrowser.ownerGlobal, pageAction);
    477        await CFRPageActions.addRecommendation(
    478          fakeBrowser,
    479          fakeHost,
    480          fakeRecommendation,
    481          dispatchStub
    482        );
    483        getStringsStub = sandbox.stub(pageAction, "getStrings").resolves("");
    484        getStringsStub
    485          .callsFake(async a => a) // eslint-disable-line max-nested-callbacks
    486          .withArgs({ string_id: "primary_button_id" })
    487          .resolves({ value: "Primary Button", attributes: { accesskey: "p" } })
    488          .withArgs({ string_id: "secondary_button_id" })
    489          .resolves({
    490            value: "Secondary Button",
    491            attributes: { accesskey: "s" },
    492          })
    493          .withArgs({ string_id: "secondary_button_id_2" })
    494          .resolves({
    495            value: "Secondary Button 2",
    496            attributes: { accesskey: "a" },
    497          })
    498          .withArgs({ string_id: "secondary_button_id_3" })
    499          .resolves({
    500            value: "Secondary Button 3",
    501            attributes: { accesskey: "g" },
    502          })
    503          .withArgs(
    504            sinon.match({
    505              string_id: "cfr-doorhanger-extension-learn-more-link",
    506            })
    507          )
    508          .resolves("Learn more")
    509          .withArgs(
    510            sinon.match({ string_id: "cfr-doorhanger-extension-total-users" })
    511          )
    512          .callsFake(async ({ args }) => `${args.total} users`); // eslint-disable-line max-nested-callbacks
    513 
    514        translateElementsStub = sandbox.stub().resolves();
    515        setAttributesStub = sandbox.stub();
    516        global.RemoteL10n.l10n.setAttributes = setAttributesStub;
    517        global.RemoteL10n.l10n.translateElements = translateElementsStub;
    518      });
    519 
    520      it("should call `.hideAddressBarNotifier` and do nothing if there is no recommendation for the selected browser", async () => {
    521        sandbox.spy(pageAction, "hideAddressBarNotifier");
    522        CFRPageActions.RecommendationMap.delete(fakeBrowser);
    523        await pageAction._cfrUrlbarButtonClick({});
    524        assert.calledOnce(pageAction.hideAddressBarNotifier);
    525        assert.notCalled(global.PopupNotifications.show);
    526      });
    527      it("should cancel any planned state changes", async () => {
    528        sandbox.spy(pageAction, "_clearScheduledStateChanges");
    529        assert.notCalled(pageAction._clearScheduledStateChanges);
    530        await pageAction._cfrUrlbarButtonClick({});
    531        assert.calledOnce(pageAction._clearScheduledStateChanges);
    532      });
    533      it("should set the right text values", async () => {
    534        await pageAction._cfrUrlbarButtonClick({});
    535        const headerLabel = elements["cfr-notification-header-label"];
    536        const headerLink = elements["cfr-notification-header-link"];
    537        const headerImage = elements["cfr-notification-header-image"];
    538        const footerLink = elements["cfr-notification-footer-learn-more-link"];
    539        assert.equal(
    540          headerLabel.value,
    541          fakeRecommendation.content.heading_text
    542        );
    543        assert.isTrue(
    544          headerLink
    545            .getAttribute("href")
    546            .endsWith(fakeRecommendation.content.info_icon.sumo_path)
    547        );
    548        assert.equal(
    549          headerImage.getAttribute("tooltiptext"),
    550          fakeRecommendation.content.info_icon.label
    551        );
    552        const htmlFooterEl = fakeRemoteL10n.createElement.args.find(
    553          /* eslint-disable-next-line max-nested-callbacks */
    554          ([, , args]) =>
    555            args && args.content === fakeRecommendation.content.text
    556        );
    557        assert.ok(htmlFooterEl);
    558        assert.equal(footerLink.value, "Learn more");
    559        assert.equal(
    560          footerLink.getAttribute("href"),
    561          fakeRecommendation.content.addon.amo_url
    562        );
    563      });
    564      it("should add the rating correctly", async () => {
    565        await pageAction._cfrUrlbarButtonClick();
    566        const footerFilledStars =
    567          elements["cfr-notification-footer-filled-stars"];
    568        const footerEmptyStars =
    569          elements["cfr-notification-footer-empty-stars"];
    570        // .toFixed to sort out some floating precision errors
    571        assert.equal(
    572          footerFilledStars.style.width,
    573          `${(4.2 * 16).toFixed(1)}px`
    574        );
    575        assert.equal(
    576          footerEmptyStars.style.width,
    577          `${(0.8 * 16).toFixed(1)}px`
    578        );
    579      });
    580      it("should add the number of users correctly", async () => {
    581        await pageAction._cfrUrlbarButtonClick();
    582        const footerUsers = elements["cfr-notification-footer-users"];
    583        assert.isNull(footerUsers.getAttribute("hidden"));
    584        assert.equal(
    585          footerUsers.getAttribute("value"),
    586          `${fakeRecommendation.content.addon.users}`
    587        );
    588      });
    589      it("should send the right telemetry", async () => {
    590        await pageAction._cfrUrlbarButtonClick();
    591        assert.calledWith(dispatchStub, {
    592          type: "DOORHANGER_TELEMETRY",
    593          data: {
    594            action: "cfr_user_event",
    595            source: "CFR",
    596            message_id: fakeRecommendation.id,
    597            bucket_id: fakeRecommendation.content.bucket_id,
    598            event: "CLICK_DOORHANGER",
    599          },
    600        });
    601      });
    602      it("should set the main action correctly", async () => {
    603        sinon
    604          .stub(CFRPageActions, "_fetchLatestAddonVersion")
    605          .resolves("latest-addon.xpi");
    606        await pageAction._cfrUrlbarButtonClick();
    607        const mainAction = global.PopupNotifications.show.firstCall.args[4]; // eslint-disable-line prefer-destructuring
    608        assert.deepEqual(mainAction.label, {
    609          value: "Primary Button",
    610          attributes: { accesskey: "p" },
    611        });
    612        sandbox.spy(pageAction, "hideAddressBarNotifier");
    613        await mainAction.callback();
    614        assert.calledOnce(pageAction.hideAddressBarNotifier);
    615        // Should block the message
    616        assert.calledWith(dispatchStub, {
    617          type: "BLOCK_MESSAGE_BY_ID",
    618          data: { id: fakeRecommendation.id },
    619        });
    620        // Should trigger the action
    621        assert.calledWith(
    622          dispatchStub,
    623          {
    624            type: "USER_ACTION",
    625            data: { id: "primary_action", data: { url: "latest-addon.xpi" } },
    626          },
    627          fakeBrowser
    628        );
    629        // Should send telemetry
    630        assert.calledWith(dispatchStub, {
    631          type: "DOORHANGER_TELEMETRY",
    632          data: {
    633            action: "cfr_user_event",
    634            source: "CFR",
    635            message_id: fakeRecommendation.id,
    636            bucket_id: fakeRecommendation.content.bucket_id,
    637            event: "INSTALL",
    638          },
    639        });
    640        // Should remove the recommendation
    641        assert.isFalse(CFRPageActions.RecommendationMap.has(fakeBrowser));
    642      });
    643      it("should set the secondary action correctly", async () => {
    644        await pageAction._cfrUrlbarButtonClick();
    645        // eslint-disable-next-line prefer-destructuring
    646        const [secondaryAction] =
    647          global.PopupNotifications.show.firstCall.args[5];
    648 
    649        assert.deepEqual(secondaryAction.label, {
    650          value: "Secondary Button",
    651          attributes: { accesskey: "s" },
    652        });
    653        sandbox.spy(pageAction, "hideAddressBarNotifier");
    654        CFRPageActions.RecommendationMap.set(fakeBrowser, {});
    655        secondaryAction.callback();
    656        // Should send telemetry
    657        assert.calledWith(dispatchStub, {
    658          type: "DOORHANGER_TELEMETRY",
    659          data: {
    660            action: "cfr_user_event",
    661            source: "CFR",
    662            message_id: fakeRecommendation.id,
    663            bucket_id: fakeRecommendation.content.bucket_id,
    664            event: "DISMISS",
    665          },
    666        });
    667        // Don't remove the recommendation on `DISMISS` action
    668        assert.isTrue(CFRPageActions.RecommendationMap.has(fakeBrowser));
    669        assert.notCalled(pageAction.hideAddressBarNotifier);
    670      });
    671      it("should send right telemetry for BLOCK secondary action", async () => {
    672        await pageAction._cfrUrlbarButtonClick();
    673        // eslint-disable-next-line prefer-destructuring
    674        const blockAction = global.PopupNotifications.show.firstCall.args[5][1];
    675 
    676        assert.deepEqual(blockAction.label, {
    677          value: "Secondary Button 2",
    678          attributes: { accesskey: "a" },
    679        });
    680        sandbox.spy(pageAction, "hideAddressBarNotifier");
    681        sandbox.spy(pageAction, "_blockMessage");
    682        CFRPageActions.RecommendationMap.set(fakeBrowser, {});
    683        blockAction.callback();
    684        assert.calledOnce(pageAction.hideAddressBarNotifier);
    685        assert.calledOnce(pageAction._blockMessage);
    686        // Should send telemetry
    687        assert.calledWith(dispatchStub, {
    688          type: "DOORHANGER_TELEMETRY",
    689          data: {
    690            action: "cfr_user_event",
    691            source: "CFR",
    692            message_id: fakeRecommendation.id,
    693            bucket_id: fakeRecommendation.content.bucket_id,
    694            event: "BLOCK",
    695          },
    696        });
    697        // Should remove the recommendation
    698        assert.isFalse(CFRPageActions.RecommendationMap.has(fakeBrowser));
    699      });
    700      it("should send right telemetry for MANAGE secondary action", async () => {
    701        await pageAction._cfrUrlbarButtonClick();
    702        // eslint-disable-next-line prefer-destructuring
    703        const manageAction =
    704          global.PopupNotifications.show.firstCall.args[5][2];
    705 
    706        assert.deepEqual(manageAction.label, {
    707          value: "Secondary Button 3",
    708          attributes: { accesskey: "g" },
    709        });
    710        sandbox.spy(pageAction, "hideAddressBarNotifier");
    711        CFRPageActions.RecommendationMap.set(fakeBrowser, {});
    712        manageAction.callback();
    713        // Should send telemetry
    714        assert.calledWith(dispatchStub, {
    715          type: "DOORHANGER_TELEMETRY",
    716          data: {
    717            action: "cfr_user_event",
    718            source: "CFR",
    719            message_id: fakeRecommendation.id,
    720            bucket_id: fakeRecommendation.content.bucket_id,
    721            event: "MANAGE",
    722          },
    723        });
    724        // Don't remove the recommendation on `MANAGE` action
    725        assert.isTrue(CFRPageActions.RecommendationMap.has(fakeBrowser));
    726        assert.notCalled(pageAction.hideAddressBarNotifier);
    727      });
    728      it("should call PopupNotifications.show with the right arguments", async () => {
    729        await pageAction._cfrUrlbarButtonClick();
    730        assert.calledWith(
    731          global.PopupNotifications.show,
    732          fakeBrowser,
    733          "contextual-feature-recommendation",
    734          fakeRecommendation.content.addon.title,
    735          "cfr",
    736          sinon.match.any, // Corresponds to the main action, tested above
    737          sinon.match.any, // Corresponds to the secondary action, tested above
    738          {
    739            popupIconURL: fakeRecommendation.content.addon.icon,
    740            hideClose: true,
    741            eventCallback: pageAction._popupStateChange,
    742            persistent: false,
    743            persistWhileVisible: false,
    744            popupIconClass: fakeRecommendation.content.icon_class,
    745            recordTelemetryInPrivateBrowsing:
    746              fakeRecommendation.content.show_in_private_browsing,
    747            name: {
    748              string_id: "cfr-doorhanger-extension-author",
    749              args: { name: fakeRecommendation.content.addon.author },
    750            },
    751          }
    752        );
    753      });
    754    });
    755    describe("#_cfrUrlbarButtonClick/cfr_urlbar_chiclet", () => {
    756      let heartbeatRecommendation;
    757      beforeEach(async () => {
    758        heartbeatRecommendation = (await CFRMessageProvider.getMessages()).find(
    759          m => m.template === "cfr_urlbar_chiclet"
    760        );
    761        CFRPageActions.PageActionMap.set(fakeBrowser.ownerGlobal, pageAction);
    762        await CFRPageActions.addRecommendation(
    763          fakeBrowser,
    764          fakeHost,
    765          heartbeatRecommendation,
    766          dispatchStub
    767        );
    768      });
    769      it("should dispatch a click event", async () => {
    770        await pageAction._cfrUrlbarButtonClick({});
    771 
    772        assert.calledWith(dispatchStub, {
    773          type: "DOORHANGER_TELEMETRY",
    774          data: {
    775            action: "cfr_user_event",
    776            source: "CFR",
    777            message_id: heartbeatRecommendation.id,
    778            bucket_id: heartbeatRecommendation.content.bucket_id,
    779            event: "CLICK_DOORHANGER",
    780          },
    781        });
    782      });
    783      it("should dispatch a USER_ACTION for chiclet_open_url layout", async () => {
    784        await pageAction._cfrUrlbarButtonClick({});
    785 
    786        assert.calledWith(dispatchStub, {
    787          type: "USER_ACTION",
    788          data: {
    789            data: {
    790              args: heartbeatRecommendation.content.action.url,
    791              where: heartbeatRecommendation.content.action.where,
    792            },
    793            type: "OPEN_URL",
    794          },
    795        });
    796      });
    797      it("should block the message after the click", async () => {
    798        await pageAction._cfrUrlbarButtonClick({});
    799 
    800        assert.calledWith(dispatchStub, {
    801          type: "BLOCK_MESSAGE_BY_ID",
    802          data: { id: heartbeatRecommendation.id },
    803        });
    804      });
    805      it("should remove the button and browser entry", async () => {
    806        sandbox.spy(pageAction, "hideAddressBarNotifier");
    807 
    808        await pageAction._cfrUrlbarButtonClick({});
    809 
    810        assert.calledOnce(pageAction.hideAddressBarNotifier);
    811        assert.isFalse(CFRPageActions.RecommendationMap.has(fakeBrowser));
    812      });
    813    });
    814 
    815    describe("#showMilestonePopup", () => {
    816      let milestoneRecommendation;
    817      let fakeTrackingDBService;
    818      beforeEach(async () => {
    819        fakeTrackingDBService = {
    820          sumAllEvents: sandbox.stub(),
    821        };
    822        globals.set({ TrackingDBService: fakeTrackingDBService });
    823        CFRPageActions.PageActionMap.set(fakeBrowser.ownerGlobal, pageAction);
    824        sandbox
    825          .stub(pageAction, "getStrings")
    826          .callsFake(async a => a) // eslint-disable-line max-nested-callbacks
    827          .resolves({ value: "element", attributes: { accesskey: "e" } });
    828 
    829        milestoneRecommendation = (await CFRMessageProvider.getMessages()).find(
    830          m => m.template === "milestone_message"
    831        );
    832      });
    833 
    834      afterEach(() => {
    835        sandbox.restore();
    836        globals.restore();
    837      });
    838 
    839      it("Set current date in header when earliest date undefined", async () => {
    840        fakeTrackingDBService.getEarliestRecordedDate = sandbox.stub();
    841        await CFRPageActions.addRecommendation(
    842          fakeBrowser,
    843          fakeHost,
    844          milestoneRecommendation,
    845          dispatchStub
    846        );
    847        const [, , headerElementArgs] = fakeRemoteL10n.createElement.args.find(
    848          /* eslint-disable-next-line max-nested-callbacks */
    849          ([, , args]) => args && args.content && args.attributes
    850        );
    851        assert.equal(
    852          headerElementArgs.content.string_id,
    853          milestoneRecommendation.content.heading_text.string_id
    854        );
    855        assert.equal(headerElementArgs.attributes.date, new Date().getTime());
    856        assert.calledOnce(global.PopupNotifications.show);
    857      });
    858 
    859      it("Set date in header to earliest date timestamp by default", async () => {
    860        let earliestDateTimeStamp = 1705601996435;
    861        fakeTrackingDBService.getEarliestRecordedDate = sandbox
    862          .stub()
    863          .returns(earliestDateTimeStamp);
    864        await CFRPageActions.addRecommendation(
    865          fakeBrowser,
    866          fakeHost,
    867          milestoneRecommendation,
    868          dispatchStub
    869        );
    870        const [, , headerElementArgs] = fakeRemoteL10n.createElement.args.find(
    871          /* eslint-disable-next-line max-nested-callbacks */
    872          ([, , args]) => args && args.content && args.attributes
    873        );
    874        assert.equal(
    875          headerElementArgs.content.string_id,
    876          milestoneRecommendation.content.heading_text.string_id
    877        );
    878        assert.equal(headerElementArgs.attributes.date, earliestDateTimeStamp);
    879        assert.calledOnce(global.PopupNotifications.show);
    880      });
    881    });
    882  });
    883 
    884  describe("CFRPageActions", () => {
    885    beforeEach(() => {
    886      // Spy on the prototype methods to inspect calls for any PageAction instance
    887      sandbox.spy(PageAction.prototype, "showAddressBarNotifier");
    888      sandbox.spy(PageAction.prototype, "hideAddressBarNotifier");
    889    });
    890 
    891    describe("updatePageActions", () => {
    892      let savedRec;
    893 
    894      beforeEach(() => {
    895        const win = fakeBrowser.ownerGlobal;
    896        CFRPageActions.PageActionMap.set(
    897          win,
    898          new PageAction(win, dispatchStub)
    899        );
    900        const { id, content } = fakeRecommendation;
    901        savedRec = {
    902          id,
    903          host: fakeHost,
    904          content,
    905        };
    906        CFRPageActions.RecommendationMap.set(fakeBrowser, savedRec);
    907      });
    908 
    909      it("should do nothing if a pageAction doesn't exist for the window", () => {
    910        const win = fakeBrowser.ownerGlobal;
    911        CFRPageActions.PageActionMap.delete(win);
    912        CFRPageActions.updatePageActions(fakeBrowser);
    913        assert.notCalled(PageAction.prototype.showAddressBarNotifier);
    914        assert.notCalled(PageAction.prototype.hideAddressBarNotifier);
    915      });
    916      it("should do nothing if the browser is not the `selectedBrowser`", () => {
    917        const someOtherFakeBrowser = {};
    918        CFRPageActions.updatePageActions(someOtherFakeBrowser);
    919        assert.notCalled(PageAction.prototype.showAddressBarNotifier);
    920        assert.notCalled(PageAction.prototype.hideAddressBarNotifier);
    921      });
    922      it("should hideAddressBarNotifier the pageAction if a recommendation doesn't exist for the given browser", () => {
    923        CFRPageActions.RecommendationMap.delete(fakeBrowser);
    924        CFRPageActions.updatePageActions(fakeBrowser);
    925        assert.calledOnce(PageAction.prototype.hideAddressBarNotifier);
    926      });
    927      it("should show the pageAction if a recommendation exists and the host matches", () => {
    928        CFRPageActions.updatePageActions(fakeBrowser);
    929        assert.calledOnce(PageAction.prototype.showAddressBarNotifier);
    930        assert.calledWith(
    931          PageAction.prototype.showAddressBarNotifier,
    932          savedRec
    933        );
    934      });
    935      it("should show the pageAction if a recommendation exists and it doesn't have a host defined", () => {
    936        const recNoHost = { ...savedRec, host: undefined };
    937        CFRPageActions.RecommendationMap.set(fakeBrowser, recNoHost);
    938        CFRPageActions.updatePageActions(fakeBrowser);
    939        assert.calledOnce(PageAction.prototype.showAddressBarNotifier);
    940        assert.calledWith(
    941          PageAction.prototype.showAddressBarNotifier,
    942          recNoHost
    943        );
    944      });
    945      it("should hideAddressBarNotifier the pageAction and delete the recommendation if the recommendation exists but the host doesn't match", () => {
    946        const someOtherFakeHost = "subdomain.mozilla.com";
    947        fakeBrowser.documentURI.host = someOtherFakeHost;
    948        assert.isTrue(CFRPageActions.RecommendationMap.has(fakeBrowser));
    949        CFRPageActions.updatePageActions(fakeBrowser);
    950        assert.calledOnce(PageAction.prototype.hideAddressBarNotifier);
    951        assert.isFalse(CFRPageActions.RecommendationMap.has(fakeBrowser));
    952      });
    953      it("should not call `delete` if retain is true", () => {
    954        savedRec.retain = true;
    955        fakeBrowser.documentURI.host = "subdomain.mozilla.com";
    956        assert.isTrue(CFRPageActions.RecommendationMap.has(fakeBrowser));
    957 
    958        CFRPageActions.updatePageActions(fakeBrowser);
    959        assert.propertyVal(savedRec, "retain", false);
    960        assert.calledOnce(PageAction.prototype.hideAddressBarNotifier);
    961        assert.isTrue(CFRPageActions.RecommendationMap.has(fakeBrowser));
    962      });
    963      it("should call `delete` if retain is false", () => {
    964        savedRec.retain = false;
    965        fakeBrowser.documentURI.host = "subdomain.mozilla.com";
    966        assert.isTrue(CFRPageActions.RecommendationMap.has(fakeBrowser));
    967 
    968        CFRPageActions.updatePageActions(fakeBrowser);
    969        assert.propertyVal(savedRec, "retain", false);
    970        assert.calledOnce(PageAction.prototype.hideAddressBarNotifier);
    971        assert.isFalse(CFRPageActions.RecommendationMap.has(fakeBrowser));
    972      });
    973    });
    974 
    975    describe("forceRecommendation", () => {
    976      it("should succeed and add an element to the RecommendationMap", async () => {
    977        assert.isTrue(
    978          await CFRPageActions.forceRecommendation(
    979            fakeBrowser,
    980            fakeRecommendation,
    981            dispatchStub
    982          )
    983        );
    984        assert.deepInclude(CFRPageActions.RecommendationMap.get(fakeBrowser), {
    985          id: fakeRecommendation.id,
    986          content: fakeRecommendation.content,
    987        });
    988      });
    989      it("should create a PageAction if one doesn't exist for the window, save it in the PageActionMap, and call `show`", async () => {
    990        const win = fakeBrowser.ownerGlobal;
    991        assert.isFalse(CFRPageActions.PageActionMap.has(win));
    992        await CFRPageActions.forceRecommendation(
    993          fakeBrowser,
    994          fakeRecommendation,
    995          dispatchStub
    996        );
    997        const pageAction = CFRPageActions.PageActionMap.get(win);
    998        assert.equal(win, pageAction.window);
    999        assert.equal(dispatchStub, pageAction._dispatchCFRAction);
   1000        assert.calledOnce(PageAction.prototype.showAddressBarNotifier);
   1001      });
   1002    });
   1003 
   1004    describe("showPopup", () => {
   1005      let savedRec;
   1006      let pageAction;
   1007      let fakeAnchorId = "fake_anchor_id";
   1008      let fakeAltAnchorId = "fake_alt_anchor_id";
   1009      let TEST_MESSAGE;
   1010      let getElmStub;
   1011      let getStyleStub;
   1012      let isCustomizingStub;
   1013      beforeEach(() => {
   1014        TEST_MESSAGE = {
   1015          id: "fake_id",
   1016          template: "cfr_doorhanger",
   1017          content: {
   1018            skip_address_bar_notifier: true,
   1019            heading_text: "Fake Heading Text",
   1020            anchor_id: fakeAnchorId,
   1021          },
   1022        };
   1023        getElmStub = sandbox
   1024          .stub(window.document, "getElementById")
   1025          .callsFake(id => ({ id }));
   1026        getStyleStub = sandbox
   1027          .stub(window, "getComputedStyle")
   1028          .returns({ display: "block", visibility: "visible" });
   1029 
   1030        isCustomizingStub = sandbox.stub().returns(false);
   1031        globals.set({
   1032          CustomizationHandler: { isCustomizing: isCustomizingStub },
   1033        });
   1034 
   1035        savedRec = {
   1036          id: TEST_MESSAGE.id,
   1037          host: fakeHost,
   1038          content: TEST_MESSAGE.content,
   1039        };
   1040        CFRPageActions.RecommendationMap.set(fakeBrowser, savedRec);
   1041        pageAction = new PageAction(window, dispatchStub);
   1042        sandbox.stub(pageAction, "_renderPopup");
   1043      });
   1044      afterEach(() => {
   1045        sandbox.restore();
   1046        globals.restore();
   1047      });
   1048 
   1049      it("should use anchor_id if element exists and is not a customizable widget", async () => {
   1050        await pageAction.showPopup();
   1051        assert.equal(fakeBrowser.cfrpopupnotificationanchor.id, fakeAnchorId);
   1052      });
   1053 
   1054      it("should use anchor_id if element exists and is in the toolbar", async () => {
   1055        getWidgetStub.withArgs(fakeAnchorId).returns({ areaType: "toolbar" });
   1056        await pageAction.showPopup();
   1057        assert.equal(fakeBrowser.cfrpopupnotificationanchor.id, fakeAnchorId);
   1058      });
   1059 
   1060      it("should use the cfr button if element exists but is in the widget overflow panel", async () => {
   1061        getWidgetStub
   1062          .withArgs(fakeAnchorId)
   1063          .returns({ areaType: "menu-panel" });
   1064        await pageAction.showPopup();
   1065        assert.equal(
   1066          fakeBrowser.cfrpopupnotificationanchor.id,
   1067          pageAction.button.id
   1068        );
   1069      });
   1070 
   1071      it("should use the cfr button if element exists but is in the customization palette", async () => {
   1072        getWidgetStub.withArgs(fakeAnchorId).returns({ areaType: null });
   1073        isCustomizingStub.returns(true);
   1074        await pageAction.showPopup();
   1075        assert.equal(
   1076          fakeBrowser.cfrpopupnotificationanchor.id,
   1077          pageAction.button.id
   1078        );
   1079      });
   1080 
   1081      it("should use alt_anchor_id if one has been provided and the anchor_id element cannot be found", async () => {
   1082        TEST_MESSAGE.content.alt_anchor_id = fakeAltAnchorId;
   1083        getElmStub.withArgs(fakeAnchorId).returns(null);
   1084        await pageAction.showPopup();
   1085        assert.equal(
   1086          fakeBrowser.cfrpopupnotificationanchor.id,
   1087          fakeAltAnchorId
   1088        );
   1089      });
   1090 
   1091      it("should use alt_anchor_id if one has been provided and the anchor_id element is hidden by CSS", async () => {
   1092        TEST_MESSAGE.content.alt_anchor_id = fakeAltAnchorId;
   1093        getStyleStub
   1094          .withArgs(sandbox.match({ id: fakeAnchorId }))
   1095          .returns({ display: "none", visibility: "visible" });
   1096        await pageAction.showPopup();
   1097        assert.equal(
   1098          fakeBrowser.cfrpopupnotificationanchor.id,
   1099          fakeAltAnchorId
   1100        );
   1101      });
   1102 
   1103      it("should use alt_anchor_id if one has been provided and the anchor_id element has no height/width", async () => {
   1104        TEST_MESSAGE.content.alt_anchor_id = fakeAltAnchorId;
   1105        isElmVisibleStub
   1106          .withArgs(sandbox.match({ id: fakeAnchorId }))
   1107          .returns(false);
   1108        await pageAction.showPopup();
   1109        assert.equal(
   1110          fakeBrowser.cfrpopupnotificationanchor.id,
   1111          fakeAltAnchorId
   1112        );
   1113      });
   1114 
   1115      it("should use the button if the anchor_id and alt_anchor_id are both not visible", async () => {
   1116        TEST_MESSAGE.content.alt_anchor_id = fakeAltAnchorId;
   1117        getStyleStub
   1118          .withArgs(sandbox.match({ id: fakeAnchorId }))
   1119          .returns({ display: "none", visibility: "visible" });
   1120        getWidgetStub.withArgs(fakeAltAnchorId).returns({ areaType: null });
   1121        isCustomizingStub.returns(true);
   1122        await pageAction.showPopup();
   1123        assert.equal(
   1124          fakeBrowser.cfrpopupnotificationanchor.id,
   1125          pageAction.button.id
   1126        );
   1127      });
   1128 
   1129      it("should use the default container if the anchor_id, alt_anchor_id, and cfr button are not visible", async () => {
   1130        TEST_MESSAGE.content.alt_anchor_id = fakeAltAnchorId;
   1131        getStyleStub
   1132          .withArgs(sandbox.match({ id: fakeAnchorId }))
   1133          .returns({ display: "none", visibility: "visible" });
   1134        getStyleStub
   1135          .withArgs(sandbox.match({ id: "cfr-button" }))
   1136          .returns({ display: "none", visibility: "visible" });
   1137        getWidgetStub.withArgs(fakeAltAnchorId).returns({ areaType: null });
   1138        isCustomizingStub.returns(true);
   1139        await pageAction.showPopup();
   1140        assert.equal(
   1141          fakeBrowser.cfrpopupnotificationanchor.id,
   1142          pageAction.container.id
   1143        );
   1144      });
   1145    });
   1146 
   1147    describe("addRecommendation", () => {
   1148      it("should fail and not add a recommendation if the browser is part of a private window", async () => {
   1149        global.PrivateBrowsingUtils.isWindowPrivate.returns(true);
   1150        assert.isFalse(
   1151          await CFRPageActions.addRecommendation(
   1152            fakeBrowser,
   1153            fakeHost,
   1154            fakeRecommendation,
   1155            dispatchStub
   1156          )
   1157        );
   1158        assert.isFalse(CFRPageActions.RecommendationMap.has(fakeBrowser));
   1159      });
   1160      it("should successfully add a private browsing recommendation and send correct telemetry", async () => {
   1161        global.PrivateBrowsingUtils.isWindowPrivate.returns(true);
   1162        fakeRecommendation.content.show_in_private_browsing = true;
   1163        assert.isTrue(
   1164          await CFRPageActions.addRecommendation(
   1165            fakeBrowser,
   1166            fakeHost,
   1167            fakeRecommendation,
   1168            dispatchStub
   1169          )
   1170        );
   1171        assert.isTrue(CFRPageActions.RecommendationMap.has(fakeBrowser));
   1172 
   1173        const pageAction = CFRPageActions.PageActionMap.get(
   1174          fakeBrowser.ownerGlobal
   1175        );
   1176        await pageAction.showAddressBarNotifier(fakeRecommendation, true);
   1177        assert.calledWith(dispatchStub, {
   1178          type: "DOORHANGER_TELEMETRY",
   1179          data: {
   1180            action: "cfr_user_event",
   1181            source: "CFR",
   1182            is_private: true,
   1183            message_id: fakeRecommendation.id,
   1184            bucket_id: fakeRecommendation.content.bucket_id,
   1185            event: "IMPRESSION",
   1186          },
   1187        });
   1188      });
   1189      it("should fail and not add a recommendation if the browser is not the selected browser", async () => {
   1190        global.gBrowser.selectedBrowser = {}; // Some other browser
   1191        assert.isFalse(
   1192          await CFRPageActions.addRecommendation(
   1193            fakeBrowser,
   1194            fakeHost,
   1195            fakeRecommendation,
   1196            dispatchStub
   1197          )
   1198        );
   1199      });
   1200      it("should fail and not add a recommendation if the browser does not exist", async () => {
   1201        assert.isFalse(
   1202          await CFRPageActions.addRecommendation(
   1203            undefined,
   1204            fakeHost,
   1205            fakeRecommendation,
   1206            dispatchStub
   1207          )
   1208        );
   1209        assert.isFalse(CFRPageActions.RecommendationMap.has(fakeBrowser));
   1210      });
   1211      it("should fail and not add a recommendation if the host doesn't match", async () => {
   1212        const someOtherFakeHost = "subdomain.mozilla.com";
   1213        assert.isFalse(
   1214          await CFRPageActions.addRecommendation(
   1215            fakeBrowser,
   1216            someOtherFakeHost,
   1217            fakeRecommendation,
   1218            dispatchStub
   1219          )
   1220        );
   1221      });
   1222      it("should otherwise succeed and add an element to the RecommendationMap", async () => {
   1223        assert.isTrue(
   1224          await CFRPageActions.addRecommendation(
   1225            fakeBrowser,
   1226            fakeHost,
   1227            fakeRecommendation,
   1228            dispatchStub
   1229          )
   1230        );
   1231        assert.deepInclude(CFRPageActions.RecommendationMap.get(fakeBrowser), {
   1232          id: fakeRecommendation.id,
   1233          host: fakeHost,
   1234          content: fakeRecommendation.content,
   1235        });
   1236      });
   1237      it("should create a PageAction if one doesn't exist for the window, save it in the PageActionMap, and call `show`", async () => {
   1238        const win = fakeBrowser.ownerGlobal;
   1239        assert.isFalse(CFRPageActions.PageActionMap.has(win));
   1240        await CFRPageActions.addRecommendation(
   1241          fakeBrowser,
   1242          fakeHost,
   1243          fakeRecommendation,
   1244          dispatchStub
   1245        );
   1246        const pageAction = CFRPageActions.PageActionMap.get(win);
   1247        assert.equal(win, pageAction.window);
   1248        assert.equal(dispatchStub, pageAction._dispatchCFRAction);
   1249        assert.calledOnce(PageAction.prototype.showAddressBarNotifier);
   1250      });
   1251      it("should add the right url if we fetched and addon install URL", async () => {
   1252        fakeRecommendation.template = "cfr_doorhanger";
   1253        await CFRPageActions.addRecommendation(
   1254          fakeBrowser,
   1255          fakeHost,
   1256          fakeRecommendation,
   1257          dispatchStub
   1258        );
   1259        const recommendation =
   1260          CFRPageActions.RecommendationMap.get(fakeBrowser);
   1261 
   1262        // sanity check - just go through some of the rest of the attributes to make sure they were untouched
   1263        assert.equal(recommendation.id, fakeRecommendation.id);
   1264        assert.equal(
   1265          recommendation.content.heading_text,
   1266          fakeRecommendation.content.heading_text
   1267        );
   1268        assert.equal(
   1269          recommendation.content.addon,
   1270          fakeRecommendation.content.addon
   1271        );
   1272        assert.equal(
   1273          recommendation.content.text,
   1274          fakeRecommendation.content.text
   1275        );
   1276        assert.equal(
   1277          recommendation.content.buttons.secondary,
   1278          fakeRecommendation.content.buttons.secondary
   1279        );
   1280        assert.equal(
   1281          recommendation.content.buttons.primary.action.id,
   1282          fakeRecommendation.content.buttons.primary.action.id
   1283        );
   1284 
   1285        delete fakeRecommendation.template;
   1286      });
   1287      it("should prevent a second message if one is currently displayed", async () => {
   1288        const secondMessage = { ...fakeRecommendation, id: "second_message" };
   1289        let messageAdded = await CFRPageActions.addRecommendation(
   1290          fakeBrowser,
   1291          fakeHost,
   1292          fakeRecommendation,
   1293          dispatchStub
   1294        );
   1295 
   1296        assert.isTrue(messageAdded);
   1297        assert.deepInclude(CFRPageActions.RecommendationMap.get(fakeBrowser), {
   1298          id: fakeRecommendation.id,
   1299          host: fakeHost,
   1300          content: fakeRecommendation.content,
   1301        });
   1302 
   1303        messageAdded = await CFRPageActions.addRecommendation(
   1304          fakeBrowser,
   1305          fakeHost,
   1306          secondMessage,
   1307          dispatchStub
   1308        );
   1309        // Adding failed
   1310        assert.isFalse(messageAdded);
   1311        // First message is still there
   1312        assert.deepInclude(CFRPageActions.RecommendationMap.get(fakeBrowser), {
   1313          id: fakeRecommendation.id,
   1314          host: fakeHost,
   1315          content: fakeRecommendation.content,
   1316        });
   1317      });
   1318      it("should send impressions just for the first message", async () => {
   1319        const secondMessage = { ...fakeRecommendation, id: "second_message" };
   1320        await CFRPageActions.addRecommendation(
   1321          fakeBrowser,
   1322          fakeHost,
   1323          fakeRecommendation,
   1324          dispatchStub
   1325        );
   1326        await CFRPageActions.addRecommendation(
   1327          fakeBrowser,
   1328          fakeHost,
   1329          secondMessage,
   1330          dispatchStub
   1331        );
   1332 
   1333        // Doorhanger telemetry + Impression for just 1 message
   1334        assert.calledTwice(dispatchStub);
   1335        const [firstArgs] = dispatchStub.firstCall.args;
   1336        const [secondArgs] = dispatchStub.secondCall.args;
   1337        assert.equal(firstArgs.data.id, secondArgs.data.message_id);
   1338      });
   1339    });
   1340 
   1341    describe("clearRecommendations", () => {
   1342      const createFakePageAction = () => ({
   1343        hideAddressBarNotifier: sandbox.stub(),
   1344      });
   1345      const windows = [{}, {}, { closed: true }];
   1346      const browsers = [{}, {}, {}, {}];
   1347 
   1348      beforeEach(() => {
   1349        CFRPageActions.PageActionMap.set(windows[0], createFakePageAction());
   1350        CFRPageActions.PageActionMap.set(windows[2], createFakePageAction());
   1351        for (const browser of browsers) {
   1352          CFRPageActions.RecommendationMap.set(browser, {});
   1353        }
   1354        globals.set({ Services: { wm: { getEnumerator: () => windows } } });
   1355      });
   1356 
   1357      it("should hideAddressBarNotifier the PageActions of any existing, non-closed windows", () => {
   1358        const pageActions = windows.map(win =>
   1359          CFRPageActions.PageActionMap.get(win)
   1360        );
   1361        CFRPageActions.clearRecommendations();
   1362 
   1363        // Only the first window had a PageAction and wasn't closed
   1364        assert.calledOnce(pageActions[0].hideAddressBarNotifier);
   1365        assert.isUndefined(pageActions[1]);
   1366        assert.notCalled(pageActions[2].hideAddressBarNotifier);
   1367      });
   1368      it("should clear the PageActionMap and the RecommendationMap", () => {
   1369        CFRPageActions.clearRecommendations();
   1370 
   1371        // Both are WeakMaps and so are not iterable, cannot be cleared, and
   1372        // cannot have their length queried directly, so we have to check
   1373        // whether previous elements still exist
   1374        assert.lengthOf(windows, 3);
   1375        for (const win of windows) {
   1376          assert.isFalse(CFRPageActions.PageActionMap.has(win));
   1377        }
   1378        assert.lengthOf(browsers, 4);
   1379        for (const browser of browsers) {
   1380          assert.isFalse(CFRPageActions.RecommendationMap.has(browser));
   1381        }
   1382      });
   1383    });
   1384 
   1385    describe("reloadL10n", () => {
   1386      const createFakePageAction = () => ({
   1387        hideAddressBarNotifier() {},
   1388        reloadL10n: sandbox.stub(),
   1389      });
   1390      const windows = [{}, {}, { closed: true }];
   1391 
   1392      beforeEach(() => {
   1393        CFRPageActions.PageActionMap.set(windows[0], createFakePageAction());
   1394        CFRPageActions.PageActionMap.set(windows[2], createFakePageAction());
   1395        globals.set({ Services: { wm: { getEnumerator: () => windows } } });
   1396      });
   1397 
   1398      it("should call reloadL10n for all the PageActions of any existing, non-closed windows", () => {
   1399        const pageActions = windows.map(win =>
   1400          CFRPageActions.PageActionMap.get(win)
   1401        );
   1402        CFRPageActions.reloadL10n();
   1403 
   1404        // Only the first window had a PageAction and wasn't closed
   1405        assert.calledOnce(pageActions[0].reloadL10n);
   1406        assert.isUndefined(pageActions[1]);
   1407        assert.notCalled(pageActions[2].reloadL10n);
   1408      });
   1409    });
   1410  });
   1411 });