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 });