PrefsFeed.test.js (17180B)
1 import { actionCreators as ac, actionTypes as at } from "common/Actions.mjs"; 2 import { GlobalOverrider } from "test/unit/utils"; 3 import { PrefsFeed } from "lib/PrefsFeed.sys.mjs"; 4 5 let overrider = new GlobalOverrider(); 6 7 describe("PrefsFeed", () => { 8 let feed; 9 let FAKE_PREFS; 10 let sandbox; 11 let ServicesStub; 12 beforeEach(() => { 13 sandbox = sinon.createSandbox(); 14 FAKE_PREFS = new Map([ 15 ["foo", 1], 16 ["bar", 2], 17 ["baz", { value: 1, skipBroadcast: true }], 18 ["qux", { value: 1, skipBroadcast: true, alsoToPreloaded: true }], 19 ]); 20 feed = new PrefsFeed(FAKE_PREFS); 21 ServicesStub = { 22 prefs: { 23 clearUserPref: sinon.spy(), 24 getStringPref: sinon.spy(), 25 getIntPref: sinon.spy(), 26 getBoolPref: sinon.spy(), 27 }, 28 obs: { 29 removeObserver: sinon.spy(), 30 addObserver: sinon.spy(), 31 }, 32 }; 33 sinon.spy(feed, "_setPref"); 34 feed.store = { 35 dispatch: sinon.spy(), 36 getState() { 37 return this.state; 38 }, 39 }; 40 // Setup for tests that don't call `init` 41 feed._prefs = { 42 get: sinon.spy(item => FAKE_PREFS.get(item)), 43 set: sinon.spy((name, value) => FAKE_PREFS.set(name, value)), 44 observe: sinon.spy(), 45 observeBranch: sinon.spy(), 46 ignore: sinon.spy(), 47 ignoreBranch: sinon.spy(), 48 reset: sinon.stub(), 49 _branchStr: "branch.str.", 50 }; 51 overrider.set({ 52 PrivateBrowsingUtils: { enabled: true }, 53 Services: ServicesStub, 54 }); 55 }); 56 afterEach(() => { 57 overrider.restore(); 58 sandbox.restore(); 59 }); 60 61 it("should set a pref when a SET_PREF action is received", () => { 62 feed.onAction(ac.SetPref("foo", 2)); 63 assert.calledWith(feed._prefs.set, "foo", 2); 64 }); 65 it("should call clearUserPref with action CLEAR_PREF", () => { 66 feed.onAction({ type: at.CLEAR_PREF, data: { name: "pref.test" } }); 67 assert.calledWith(ServicesStub.prefs.clearUserPref, "branch.str.pref.test"); 68 }); 69 it("should dispatch PREFS_INITIAL_VALUES on init with pref values and .isPrivateBrowsingEnabled", () => { 70 feed.onAction({ type: at.INIT }); 71 assert.calledOnce(feed.store.dispatch); 72 assert.equal( 73 feed.store.dispatch.firstCall.args[0].type, 74 at.PREFS_INITIAL_VALUES 75 ); 76 const [{ data }] = feed.store.dispatch.firstCall.args; 77 assert.equal(data.foo, 1); 78 assert.equal(data.bar, 2); 79 assert.isTrue(data.isPrivateBrowsingEnabled); 80 }); 81 it("should dispatch PREFS_INITIAL_VALUES with a .featureConfig", () => { 82 sandbox.stub(global.NimbusFeatures.newtab, "getAllVariables").returns({ 83 prefsButtonIcon: "icon-foo", 84 }); 85 feed.onAction({ type: at.INIT }); 86 assert.equal( 87 feed.store.dispatch.firstCall.args[0].type, 88 at.PREFS_INITIAL_VALUES 89 ); 90 const [{ data }] = feed.store.dispatch.firstCall.args; 91 assert.deepEqual(data.featureConfig, { prefsButtonIcon: "icon-foo" }); 92 }); 93 it("should dispatch PREFS_INITIAL_VALUES with trainhopConfig", () => { 94 const testObject = { 95 meta: { isRollout: false }, 96 value: { 97 type: "testExperiment", 98 payload: { enabled: true }, 99 }, 100 }; 101 sandbox 102 .stub(global.NimbusFeatures.newtabTrainhop, "getAllEnrollments") 103 .returns([testObject]); 104 105 feed.onAction({ type: at.INIT }); 106 107 assert.equal( 108 feed.store.dispatch.firstCall.args[0].type, 109 at.PREFS_INITIAL_VALUES 110 ); 111 const [{ data }] = feed.store.dispatch.firstCall.args; 112 assert.deepEqual(data.trainhopConfig, { 113 testExperiment: { enabled: true }, 114 }); 115 }); 116 it("should dispatch PREFS_INITIAL_VALUES with an empty object if no experiment is returned", () => { 117 sandbox.stub(global.NimbusFeatures.newtab, "getAllVariables").returns(null); 118 feed.onAction({ type: at.INIT }); 119 assert.equal( 120 feed.store.dispatch.firstCall.args[0].type, 121 at.PREFS_INITIAL_VALUES 122 ); 123 const [{ data }] = feed.store.dispatch.firstCall.args; 124 assert.deepEqual(data.featureConfig, {}); 125 }); 126 it("should add one branch observer on init", () => { 127 feed.onAction({ type: at.INIT }); 128 assert.calledOnce(feed._prefs.observeBranch); 129 assert.calledWith(feed._prefs.observeBranch, feed); 130 }); 131 it("should handle region on init", () => { 132 feed.init(); 133 assert.equal(feed.geo, "US"); 134 }); 135 it("should add region observer on init", () => { 136 sandbox.stub(global.Region, "home").get(() => ""); 137 feed.init(); 138 assert.equal(feed.geo, ""); 139 assert.calledWith( 140 ServicesStub.obs.addObserver, 141 feed, 142 global.Region.REGION_TOPIC 143 ); 144 }); 145 it("should remove the branch observer on uninit", () => { 146 feed.onAction({ type: at.UNINIT }); 147 assert.calledOnce(feed._prefs.ignoreBranch); 148 assert.calledWith(feed._prefs.ignoreBranch, feed); 149 }); 150 it("should call removeObserver", () => { 151 feed.geo = ""; 152 feed.uninit(); 153 assert.calledWith( 154 ServicesStub.obs.removeObserver, 155 feed, 156 global.Region.REGION_TOPIC 157 ); 158 }); 159 it("should send a PREF_CHANGED action when onPrefChanged is called", () => { 160 feed.onPrefChanged("foo", 2); 161 assert.calledWith( 162 feed.store.dispatch, 163 ac.BroadcastToContent({ 164 type: at.PREF_CHANGED, 165 data: { name: "foo", value: 2 }, 166 }) 167 ); 168 }); 169 it("should send a PREF_CHANGED actions when onPocketExperimentUpdated is called", () => { 170 sandbox 171 .stub(global.NimbusFeatures.pocketNewtab, "getAllVariables") 172 .returns({ 173 prefsButtonIcon: "icon-new", 174 }); 175 feed.onPocketExperimentUpdated(); 176 assert.calledWith( 177 feed.store.dispatch, 178 ac.BroadcastToContent({ 179 type: at.PREF_CHANGED, 180 data: { 181 name: "pocketConfig", 182 value: { 183 prefsButtonIcon: "icon-new", 184 }, 185 }, 186 }) 187 ); 188 }); 189 it("should not send a PREF_CHANGED actions when onPocketExperimentUpdated is called during startup", () => { 190 sandbox 191 .stub(global.NimbusFeatures.pocketNewtab, "getAllVariables") 192 .returns({ 193 prefsButtonIcon: "icon-new", 194 }); 195 feed.onPocketExperimentUpdated({}, "feature-experiment-loaded"); 196 assert.notCalled(feed.store.dispatch); 197 feed.onPocketExperimentUpdated({}, "feature-rollout-loaded"); 198 assert.notCalled(feed.store.dispatch); 199 }); 200 it("should send a PREF_CHANGED actions when onExperimentUpdated is called", () => { 201 sandbox.stub(global.NimbusFeatures.newtab, "getAllVariables").returns({ 202 prefsButtonIcon: "icon-new", 203 }); 204 feed.onExperimentUpdated(); 205 assert.calledWith( 206 feed.store.dispatch, 207 ac.BroadcastToContent({ 208 type: at.PREF_CHANGED, 209 data: { 210 name: "featureConfig", 211 value: { 212 prefsButtonIcon: "icon-new", 213 }, 214 }, 215 }) 216 ); 217 }); 218 describe("newtabTrainhop", () => { 219 it("should send a PREF_CHANGED actions when onTrainhopExperimentUpdated is called", () => { 220 const testObject = { 221 meta: { 222 isRollout: false, 223 }, 224 value: { 225 type: "testExperiment", 226 payload: { 227 enabled: true, 228 }, 229 }, 230 }; 231 sandbox 232 .stub(global.NimbusFeatures.newtabTrainhop, "getAllEnrollments") 233 .returns([testObject]); 234 feed.onTrainhopExperimentUpdated(); 235 assert.calledWith( 236 feed.store.dispatch, 237 ac.BroadcastToContent({ 238 type: at.PREF_CHANGED, 239 data: { 240 name: "trainhopConfig", 241 value: { 242 testExperiment: testObject.value.payload, 243 }, 244 }, 245 }) 246 ); 247 }); 248 it("should handle and dedupe multiple experiments and rollouts", () => { 249 const testObject1 = { 250 meta: { 251 isRollout: false, 252 }, 253 value: { 254 type: "testExperiment1", 255 payload: { 256 enabled: true, 257 }, 258 }, 259 }; 260 const testObject2 = { 261 meta: { 262 isRollout: false, 263 }, 264 value: { 265 type: "testExperiment1", 266 payload: { 267 enabled: false, 268 }, 269 }, 270 }; 271 const testObject3 = { 272 meta: { 273 isRollout: true, 274 }, 275 value: { 276 type: "testExperiment2", 277 payload: { 278 enabled: true, 279 }, 280 }, 281 }; 282 const testObject4 = { 283 meta: { 284 isRollout: false, 285 }, 286 value: { 287 type: "testExperiment2", 288 payload: { 289 enabled: false, 290 }, 291 }, 292 }; 293 sandbox 294 .stub(global.NimbusFeatures.newtabTrainhop, "getAllEnrollments") 295 .returns([testObject1, testObject2, testObject3, testObject4]); 296 feed.onTrainhopExperimentUpdated(); 297 assert.calledWith( 298 feed.store.dispatch, 299 ac.BroadcastToContent({ 300 type: at.PREF_CHANGED, 301 data: { 302 name: "trainhopConfig", 303 value: { 304 testExperiment1: testObject1.value.payload, 305 testExperiment2: testObject4.value.payload, 306 }, 307 }, 308 }) 309 ); 310 }); 311 it("should handle multi-payload format with single enrollment", () => { 312 const testObject = { 313 meta: { 314 isRollout: false, 315 }, 316 value: { 317 type: "multi-payload", 318 payload: [ 319 { 320 type: "testExperiment", 321 payload: { 322 enabled: true, 323 name: "test-name", 324 }, 325 }, 326 ], 327 }, 328 }; 329 sandbox 330 .stub(global.NimbusFeatures.newtabTrainhop, "getAllEnrollments") 331 .returns([testObject]); 332 feed.onTrainhopExperimentUpdated(); 333 assert.calledWith( 334 feed.store.dispatch, 335 ac.BroadcastToContent({ 336 type: at.PREF_CHANGED, 337 data: { 338 name: "trainhopConfig", 339 value: { 340 testExperiment: { 341 enabled: true, 342 name: "test-name", 343 }, 344 }, 345 }, 346 }) 347 ); 348 }); 349 it("should handle multi-payload format with multiple items in single enrollment", () => { 350 const testObject = { 351 meta: { 352 isRollout: false, 353 }, 354 value: { 355 type: "multi-payload", 356 payload: [ 357 { 358 type: "testExperiment1", 359 payload: { 360 enabled: true, 361 }, 362 }, 363 { 364 type: "testExperiment2", 365 payload: { 366 enabled: false, 367 }, 368 }, 369 ], 370 }, 371 }; 372 sandbox 373 .stub(global.NimbusFeatures.newtabTrainhop, "getAllEnrollments") 374 .returns([testObject]); 375 feed.onTrainhopExperimentUpdated(); 376 assert.calledWith( 377 feed.store.dispatch, 378 ac.BroadcastToContent({ 379 type: at.PREF_CHANGED, 380 data: { 381 name: "trainhopConfig", 382 value: { 383 testExperiment1: { 384 enabled: true, 385 }, 386 testExperiment2: { 387 enabled: false, 388 }, 389 }, 390 }, 391 }) 392 ); 393 }); 394 it("should dedupe multi-payload format with experiment taking precedence over rollout", () => { 395 const rollout = { 396 meta: { 397 isRollout: true, 398 }, 399 value: { 400 type: "multi-payload", 401 payload: [ 402 { 403 type: "testExperiment", 404 payload: { 405 enabled: false, 406 name: "rollout-name", 407 }, 408 }, 409 ], 410 }, 411 }; 412 const experiment = { 413 meta: { 414 isRollout: false, 415 }, 416 value: { 417 type: "multi-payload", 418 payload: [ 419 { 420 type: "testExperiment", 421 payload: { 422 enabled: true, 423 name: "experiment-name", 424 }, 425 }, 426 ], 427 }, 428 }; 429 sandbox 430 .stub(global.NimbusFeatures.newtabTrainhop, "getAllEnrollments") 431 .returns([rollout, experiment]); 432 feed.onTrainhopExperimentUpdated(); 433 assert.calledWith( 434 feed.store.dispatch, 435 ac.BroadcastToContent({ 436 type: at.PREF_CHANGED, 437 data: { 438 name: "trainhopConfig", 439 value: { 440 testExperiment: { 441 enabled: true, 442 name: "experiment-name", 443 }, 444 }, 445 }, 446 }) 447 ); 448 }); 449 }); 450 it("should dispatch PREF_CHANGED when onWidgetsUpdated is called", () => { 451 sandbox 452 .stub(global.NimbusFeatures.newtabWidgets, "getAllVariables") 453 .returns({ 454 enabled: true, 455 listsEnabled: true, 456 timerEnabled: false, 457 }); 458 459 feed.onWidgetsUpdated(); 460 461 assert.calledWith( 462 feed.store.dispatch, 463 ac.BroadcastToContent({ 464 type: at.PREF_CHANGED, 465 data: { 466 name: "widgetsConfig", 467 value: { 468 enabled: true, 469 listsEnabled: true, 470 timerEnabled: false, 471 }, 472 }, 473 }) 474 ); 475 }); 476 it("should remove all events on removeListeners", () => { 477 feed.geo = ""; 478 sandbox.spy(global.NimbusFeatures.pocketNewtab, "offUpdate"); 479 sandbox.spy(global.NimbusFeatures.newtab, "offUpdate"); 480 sandbox.spy(global.NimbusFeatures.newtabTrainhop, "offUpdate"); 481 feed.removeListeners(); 482 assert.calledWith( 483 global.NimbusFeatures.pocketNewtab.offUpdate, 484 feed.onPocketExperimentUpdated 485 ); 486 assert.calledWith( 487 global.NimbusFeatures.newtab.offUpdate, 488 feed.onExperimentUpdated 489 ); 490 assert.calledWith( 491 global.NimbusFeatures.newtabTrainhop.offUpdate, 492 feed.onTrainhopExperimentUpdated 493 ); 494 assert.calledWith( 495 ServicesStub.obs.removeObserver, 496 feed, 497 global.Region.REGION_TOPIC 498 ); 499 }); 500 it("should send OnlyToMain pref update if config for pref has skipBroadcast: true", async () => { 501 feed.onPrefChanged("baz", { value: 2, skipBroadcast: true }); 502 assert.calledWith( 503 feed.store.dispatch, 504 ac.OnlyToMain({ 505 type: at.PREF_CHANGED, 506 data: { name: "baz", value: { value: 2, skipBroadcast: true } }, 507 }) 508 ); 509 }); 510 it("should send AlsoToPreloaded pref update if config for pref has skipBroadcast: true and alsoToPreloaded: true", async () => { 511 feed.onPrefChanged("qux", { 512 value: 2, 513 skipBroadcast: true, 514 alsoToPreloaded: true, 515 }); 516 assert.calledWith( 517 feed.store.dispatch, 518 ac.AlsoToPreloaded({ 519 type: at.PREF_CHANGED, 520 data: { 521 name: "qux", 522 value: { value: 2, skipBroadcast: true, alsoToPreloaded: true }, 523 }, 524 }) 525 ); 526 }); 527 describe("#observe", () => { 528 it("should call dispatch from observe", () => { 529 feed.observe(undefined, global.Region.REGION_TOPIC); 530 assert.calledOnce(feed.store.dispatch); 531 }); 532 }); 533 describe("#_setStringPref", () => { 534 it("should call _setPref and getStringPref from _setStringPref", () => { 535 feed._setStringPref({}, "fake.pref", "default"); 536 assert.calledOnce(feed._setPref); 537 assert.calledWith( 538 feed._setPref, 539 { "fake.pref": undefined }, 540 "fake.pref", 541 "default" 542 ); 543 assert.calledOnce(ServicesStub.prefs.getStringPref); 544 assert.calledWith( 545 ServicesStub.prefs.getStringPref, 546 "browser.newtabpage.activity-stream.fake.pref", 547 "default" 548 ); 549 }); 550 }); 551 describe("#_setBoolPref", () => { 552 it("should call _setPref and getBoolPref from _setBoolPref", () => { 553 feed._setBoolPref({}, "fake.pref", false); 554 assert.calledOnce(feed._setPref); 555 assert.calledWith( 556 feed._setPref, 557 { "fake.pref": undefined }, 558 "fake.pref", 559 false 560 ); 561 assert.calledOnce(ServicesStub.prefs.getBoolPref); 562 assert.calledWith( 563 ServicesStub.prefs.getBoolPref, 564 "browser.newtabpage.activity-stream.fake.pref", 565 false 566 ); 567 }); 568 }); 569 describe("#_setIntPref", () => { 570 it("should call _setPref and getIntPref from _setIntPref", () => { 571 feed._setIntPref({}, "fake.pref", 1); 572 assert.calledOnce(feed._setPref); 573 assert.calledWith( 574 feed._setPref, 575 { "fake.pref": undefined }, 576 "fake.pref", 577 1 578 ); 579 assert.calledOnce(ServicesStub.prefs.getIntPref); 580 assert.calledWith( 581 ServicesStub.prefs.getIntPref, 582 "browser.newtabpage.activity-stream.fake.pref", 583 1 584 ); 585 }); 586 }); 587 describe("#_setPref", () => { 588 it("should set pref value with _setPref", () => { 589 const getPrefFunctionSpy = sinon.spy(); 590 const values = {}; 591 feed._setPref(values, "fake.pref", "default", getPrefFunctionSpy); 592 assert.deepEqual(values, { "fake.pref": undefined }); 593 assert.calledOnce(getPrefFunctionSpy); 594 assert.calledWith( 595 getPrefFunctionSpy, 596 "browser.newtabpage.activity-stream.fake.pref", 597 "default" 598 ); 599 }); 600 }); 601 });