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