ASRouterPreferences.test.js (17743B)
1 import { 2 _ASRouterPreferences, 3 ASRouterPreferences as ASRouterPreferencesSingleton, 4 TEST_PROVIDERS, 5 } from "modules/ASRouterPreferences.sys.mjs"; 6 const FAKE_PROVIDERS = [{ id: "foo" }, { id: "bar" }]; 7 8 const PROVIDER_PREF_BRANCH = 9 "browser.newtabpage.activity-stream.asrouter.providers."; 10 const DEVTOOLS_PREF = 11 "browser.newtabpage.activity-stream.asrouter.devtoolsEnabled"; 12 const CFR_USER_PREF_ADDONS = 13 "browser.newtabpage.activity-stream.asrouter.userprefs.cfr.addons"; 14 const CFR_USER_PREF_FEATURES = 15 "browser.newtabpage.activity-stream.asrouter.userprefs.cfr.features"; 16 17 /** 18 * NUMBER_OF_PREFS_TO_OBSERVE includes: 19 * 1. asrouter.providers. pref branch 20 * 2. asrouter.devtoolsEnabled 21 * 3. browser.newtabpage.activity-stream.asrouter.userprefs.cfr.addons (user preference - cfr) 22 * 4. browser.newtabpage.activity-stream.asrouter.userprefs.cfr.features (user preference - cfr) 23 * 5. services.sync.username 24 */ 25 const NUMBER_OF_PREFS_TO_OBSERVE = 5; 26 27 describe("ASRouterPreferences", () => { 28 let ASRouterPreferences; 29 let sandbox; 30 let addObserverStub; 31 let stringPrefStub; 32 let boolPrefStub; 33 let resetStub; 34 let hasUserValueStub; 35 let childListStub; 36 let setStringPrefStub; 37 38 beforeEach(() => { 39 ASRouterPreferences = new _ASRouterPreferences(); 40 41 sandbox = sinon.createSandbox(); 42 addObserverStub = sandbox.stub(global.Services.prefs, "addObserver"); 43 stringPrefStub = sandbox.stub(global.Services.prefs, "getStringPref"); 44 resetStub = sandbox.stub(global.Services.prefs, "clearUserPref"); 45 setStringPrefStub = sandbox.stub(global.Services.prefs, "setStringPref"); 46 FAKE_PROVIDERS.forEach(provider => { 47 stringPrefStub 48 .withArgs(`${PROVIDER_PREF_BRANCH}${provider.id}`) 49 .returns(JSON.stringify(provider)); 50 }); 51 52 boolPrefStub = sandbox 53 .stub(global.Services.prefs, "getBoolPref") 54 .returns(false); 55 56 hasUserValueStub = sandbox 57 .stub(global.Services.prefs, "prefHasUserValue") 58 .returns(false); 59 60 childListStub = sandbox.stub(global.Services.prefs, "getChildList"); 61 childListStub 62 .withArgs(PROVIDER_PREF_BRANCH) 63 .returns( 64 FAKE_PROVIDERS.map(provider => `${PROVIDER_PREF_BRANCH}${provider.id}`) 65 ); 66 }); 67 68 afterEach(() => { 69 sandbox.restore(); 70 }); 71 72 function getPrefNameForProvider(providerId) { 73 return `${PROVIDER_PREF_BRANCH}${providerId}`; 74 } 75 76 function setPrefForProvider(providerId, value) { 77 stringPrefStub 78 .withArgs(getPrefNameForProvider(providerId)) 79 .returns(JSON.stringify(value)); 80 } 81 82 it("ASRouterPreferences should be an instance of _ASRouterPreferences", () => { 83 assert.instanceOf(ASRouterPreferencesSingleton, _ASRouterPreferences); 84 }); 85 describe("#init", () => { 86 it("should set ._initialized to true", () => { 87 ASRouterPreferences.init(); 88 assert.isTrue(ASRouterPreferences._initialized); 89 }); 90 it("should migrate the provider prefs", () => { 91 ASRouterPreferences.uninit(); 92 // Should be migrated because they contain bucket and not collection 93 const MIGRATE_PROVIDERS = [ 94 { id: "baz", bucket: "buk" }, 95 { id: "qux", bucket: "buk" }, 96 ]; 97 // Should be cleared to defaults because it throws on setStringPref 98 const ERROR_PROVIDER = { id: "err", bucket: "buk" }; 99 // Should not be migrated because, although modified, it lacks bucket 100 const MODIFIED_SAFE_PROVIDER = { id: "safe" }; 101 const ALL_PROVIDERS = [ 102 ...MIGRATE_PROVIDERS, 103 ...FAKE_PROVIDERS, // Should not be migrated because they're unmodified 104 MODIFIED_SAFE_PROVIDER, 105 ERROR_PROVIDER, 106 ]; 107 // The migrator should attempt to read prefs for all of these providers 108 const TRY_PROVIDERS = [ 109 ...MIGRATE_PROVIDERS, 110 MODIFIED_SAFE_PROVIDER, 111 ERROR_PROVIDER, 112 ]; 113 114 // Update the full list of provider prefs 115 childListStub 116 .withArgs(PROVIDER_PREF_BRANCH) 117 .returns( 118 ALL_PROVIDERS.map(provider => getPrefNameForProvider(provider.id)) 119 ); 120 // Stub the pref values so the migrator can read them 121 ALL_PROVIDERS.forEach(provider => { 122 stringPrefStub 123 .withArgs(getPrefNameForProvider(provider.id)) 124 .returns(JSON.stringify(provider)); 125 }); 126 127 // Consider these providers' prefs "modified" 128 TRY_PROVIDERS.forEach(provider => { 129 hasUserValueStub 130 .withArgs(`${PROVIDER_PREF_BRANCH}${provider.id}`) 131 .returns(true); 132 }); 133 // Spoof an error when trying to set the pref for this provider so we can 134 // test that the pref is gracefully reset on error 135 setStringPrefStub 136 .withArgs(getPrefNameForProvider(ERROR_PROVIDER.id)) 137 .throws(); 138 139 ASRouterPreferences.init(); 140 141 // The migrator should have tried to check each pref for user modification 142 ALL_PROVIDERS.forEach(provider => 143 assert.calledWith(hasUserValueStub, getPrefNameForProvider(provider.id)) 144 ); 145 // Test that we don't call getStringPref for providers that don't have a 146 // user-defined value 147 FAKE_PROVIDERS.forEach(provider => 148 assert.neverCalledWith( 149 stringPrefStub, 150 getPrefNameForProvider(provider.id) 151 ) 152 ); 153 // But we do call it for providers that do have a user-defined value 154 TRY_PROVIDERS.forEach(provider => 155 assert.calledWith(stringPrefStub, getPrefNameForProvider(provider.id)) 156 ); 157 158 // Test that we don't call setStringPref to migrate providers that don't 159 // have a bucket property 160 assert.neverCalledWith( 161 setStringPrefStub, 162 getPrefNameForProvider(MODIFIED_SAFE_PROVIDER.id) 163 ); 164 165 /** 166 * For a given provider, return a sinon matcher that matches if the value 167 * looks like a migrated version of the original provider. Requires that: 168 * its id matches the original provider's id; it has no bucket; and its 169 * collection is set to the value of the original provider's bucket. 170 * 171 * @param {object} provider the provider object to compare to 172 * @returns {object} custom matcher object for sinon 173 */ 174 function providerJsonMatches(provider) { 175 return sandbox.match(migrated => { 176 const parsed = JSON.parse(migrated); 177 return ( 178 parsed.id === provider.id && 179 !("bucket" in parsed) && 180 parsed.collection === provider.bucket 181 ); 182 }); 183 } 184 185 // Test that we call setStringPref to migrate providers that have a bucket 186 // property and don't have a collection property 187 MIGRATE_PROVIDERS.forEach(provider => 188 assert.calledWith( 189 setStringPrefStub, 190 getPrefNameForProvider(provider.id), 191 providerJsonMatches(provider) // Verify the migrated pref value 192 ) 193 ); 194 195 // Test that we clear the pref for providers that throw when we try to 196 // read or write them 197 assert.calledWith(resetStub, getPrefNameForProvider(ERROR_PROVIDER.id)); 198 }); 199 it(`should set ${NUMBER_OF_PREFS_TO_OBSERVE} observers and not re-initialize if already initialized`, () => { 200 ASRouterPreferences.init(); 201 assert.callCount(addObserverStub, NUMBER_OF_PREFS_TO_OBSERVE); 202 ASRouterPreferences.init(); 203 ASRouterPreferences.init(); 204 assert.callCount(addObserverStub, NUMBER_OF_PREFS_TO_OBSERVE); 205 }); 206 }); 207 describe("#uninit", () => { 208 it("should set ._initialized to false", () => { 209 ASRouterPreferences.init(); 210 ASRouterPreferences.uninit(); 211 assert.isFalse(ASRouterPreferences._initialized); 212 }); 213 it("should clear cached values for ._initialized, .devtoolsEnabled", () => { 214 ASRouterPreferences.init(); 215 // trigger caching 216 // eslint-disable-next-line no-unused-vars 217 const result = [ 218 ASRouterPreferences.providers, 219 ASRouterPreferences.devtoolsEnabled, 220 ]; 221 assert.isNotNull( 222 ASRouterPreferences._providers, 223 "providers should not be null" 224 ); 225 assert.isNotNull( 226 ASRouterPreferences._devtoolsEnabled, 227 "devtolosEnabled should not be null" 228 ); 229 230 ASRouterPreferences.uninit(); 231 assert.isNull(ASRouterPreferences._providers); 232 assert.isNull(ASRouterPreferences._devtoolsEnabled); 233 }); 234 it("should clear all listeners and remove observers (only once)", () => { 235 const removeStub = sandbox.stub(global.Services.prefs, "removeObserver"); 236 ASRouterPreferences.init(); 237 ASRouterPreferences.addListener(() => {}); 238 ASRouterPreferences.addListener(() => {}); 239 assert.equal(ASRouterPreferences._callbacks.size, 2); 240 ASRouterPreferences.uninit(); 241 // Tests to make sure we don't remove observers that weren't set 242 ASRouterPreferences.uninit(); 243 244 assert.callCount(removeStub, NUMBER_OF_PREFS_TO_OBSERVE); 245 assert.calledWith(removeStub, PROVIDER_PREF_BRANCH); 246 assert.calledWith(removeStub, DEVTOOLS_PREF); 247 assert.isEmpty(ASRouterPreferences._callbacks); 248 }); 249 }); 250 describe(".providers", () => { 251 it("should return the value the first time .providers is accessed", () => { 252 ASRouterPreferences.init(); 253 254 const result = ASRouterPreferences.providers; 255 assert.deepEqual(result, FAKE_PROVIDERS); 256 // once per pref 257 assert.calledTwice(stringPrefStub); 258 }); 259 it("should return the cached value the second time .providers is accessed", () => { 260 ASRouterPreferences.init(); 261 const [, secondCall] = [ 262 ASRouterPreferences.providers, 263 ASRouterPreferences.providers, 264 ]; 265 266 assert.deepEqual(secondCall, FAKE_PROVIDERS); 267 // once per pref 268 assert.calledTwice(stringPrefStub); 269 }); 270 it("should just parse the pref each time if ASRouterPreferences hasn't been initialized yet", () => { 271 // Intentionally not initialized 272 const [firstCall, secondCall] = [ 273 ASRouterPreferences.providers, 274 ASRouterPreferences.providers, 275 ]; 276 277 assert.deepEqual(firstCall, FAKE_PROVIDERS); 278 assert.deepEqual(secondCall, FAKE_PROVIDERS); 279 assert.callCount(stringPrefStub, 4); 280 }); 281 it("should skip the pref without throwing if a pref is not parsable", () => { 282 stringPrefStub.withArgs(`${PROVIDER_PREF_BRANCH}foo`).returns("not json"); 283 ASRouterPreferences.init(); 284 285 assert.deepEqual(ASRouterPreferences.providers, [{ id: "bar" }]); 286 }); 287 it("should include TEST_PROVIDERS if devtools is turned on", () => { 288 boolPrefStub.withArgs(DEVTOOLS_PREF).returns(true); 289 ASRouterPreferences.init(); 290 291 assert.deepEqual(ASRouterPreferences.providers, [ 292 ...TEST_PROVIDERS, 293 ...FAKE_PROVIDERS, 294 ]); 295 }); 296 }); 297 describe(".devtoolsEnabled", () => { 298 it("should read the pref the first time .devtoolsEnabled is accessed", () => { 299 ASRouterPreferences.init(); 300 301 const result = ASRouterPreferences.devtoolsEnabled; 302 assert.deepEqual(result, false); 303 assert.calledOnce(boolPrefStub); 304 }); 305 it("should return the cached value the second time .devtoolsEnabled is accessed", () => { 306 ASRouterPreferences.init(); 307 const [, secondCall] = [ 308 ASRouterPreferences.devtoolsEnabled, 309 ASRouterPreferences.devtoolsEnabled, 310 ]; 311 312 assert.deepEqual(secondCall, false); 313 assert.calledOnce(boolPrefStub); 314 }); 315 it("should just parse the pref each time if ASRouterPreferences hasn't been initialized yet", () => { 316 // Intentionally not initialized 317 const [firstCall, secondCall] = [ 318 ASRouterPreferences.devtoolsEnabled, 319 ASRouterPreferences.devtoolsEnabled, 320 ]; 321 322 assert.deepEqual(firstCall, false); 323 assert.deepEqual(secondCall, false); 324 assert.calledTwice(boolPrefStub); 325 }); 326 }); 327 describe("#getAllUserPreferences", () => { 328 it("should return all user preferences", () => { 329 boolPrefStub.withArgs(CFR_USER_PREF_ADDONS).returns(false); 330 boolPrefStub.withArgs(CFR_USER_PREF_FEATURES).returns(true); 331 const result = ASRouterPreferences.getAllUserPreferences(); 332 assert.deepEqual(result, { 333 cfrAddons: false, 334 cfrFeatures: true, 335 }); 336 }); 337 }); 338 describe("#enableOrDisableProvider", () => { 339 it("should enable an existing provider if second param is true", () => { 340 setPrefForProvider("foo", { id: "foo", enabled: false }); 341 assert.isFalse(ASRouterPreferences.providers[0].enabled); 342 343 ASRouterPreferences.enableOrDisableProvider("foo", true); 344 345 assert.calledWith( 346 setStringPrefStub, 347 getPrefNameForProvider("foo"), 348 JSON.stringify({ id: "foo", enabled: true }) 349 ); 350 }); 351 it("should disable an existing provider if second param is false", () => { 352 setPrefForProvider("foo", { id: "foo", enabled: true }); 353 assert.isTrue(ASRouterPreferences.providers[0].enabled); 354 355 ASRouterPreferences.enableOrDisableProvider("foo", false); 356 357 assert.calledWith( 358 setStringPrefStub, 359 getPrefNameForProvider("foo"), 360 JSON.stringify({ id: "foo", enabled: false }) 361 ); 362 }); 363 it("should not throw if the id does not exist", () => { 364 assert.doesNotThrow(() => { 365 ASRouterPreferences.enableOrDisableProvider("does_not_exist", true); 366 }); 367 }); 368 it("should not throw if pref is not parseable", () => { 369 stringPrefStub 370 .withArgs(getPrefNameForProvider("foo")) 371 .returns("not valid"); 372 assert.doesNotThrow(() => { 373 ASRouterPreferences.enableOrDisableProvider("foo", true); 374 }); 375 }); 376 }); 377 describe("#setUserPreference", () => { 378 it("should do nothing if the pref doesn't exist", () => { 379 ASRouterPreferences.setUserPreference("foo", true); 380 assert.notCalled(boolPrefStub); 381 }); 382 it("should set the given pref", () => { 383 const setStub = sandbox.stub(global.Services.prefs, "setBoolPref"); 384 ASRouterPreferences.setUserPreference("cfrAddons", true); 385 assert.calledWith(setStub, CFR_USER_PREF_ADDONS, true); 386 }); 387 }); 388 describe("#resetProviderPref", () => { 389 it("should reset the pref and user prefs", () => { 390 ASRouterPreferences.resetProviderPref(); 391 FAKE_PROVIDERS.forEach(provider => { 392 assert.calledWith(resetStub, getPrefNameForProvider(provider.id)); 393 }); 394 assert.calledWith(resetStub, CFR_USER_PREF_ADDONS); 395 assert.calledWith(resetStub, CFR_USER_PREF_FEATURES); 396 }); 397 }); 398 describe("observer, listeners", () => { 399 it("should invalidate .providers when the pref is changed", () => { 400 const testProvider = { id: "newstuff" }; 401 const newProviders = [...FAKE_PROVIDERS, testProvider]; 402 403 ASRouterPreferences.init(); 404 405 assert.deepEqual(ASRouterPreferences.providers, FAKE_PROVIDERS); 406 stringPrefStub 407 .withArgs(getPrefNameForProvider(testProvider.id)) 408 .returns(JSON.stringify(testProvider)); 409 childListStub 410 .withArgs(PROVIDER_PREF_BRANCH) 411 .returns( 412 newProviders.map(provider => getPrefNameForProvider(provider.id)) 413 ); 414 ASRouterPreferences.observe( 415 null, 416 null, 417 getPrefNameForProvider(testProvider.id) 418 ); 419 420 // Cache should be invalidated so we access the new value of the pref now 421 assert.deepEqual(ASRouterPreferences.providers, newProviders); 422 }); 423 it("should invalidate .devtoolsEnabled and .providers when the pref is changed", () => { 424 ASRouterPreferences.init(); 425 426 assert.isFalse(ASRouterPreferences.devtoolsEnabled); 427 boolPrefStub.withArgs(DEVTOOLS_PREF).returns(true); 428 childListStub.withArgs(PROVIDER_PREF_BRANCH).returns([]); 429 ASRouterPreferences.observe(null, null, DEVTOOLS_PREF); 430 431 // Cache should be invalidated so we access the new value of the pref now 432 // Note that providers needs to be invalidated because devtools adds test content to it. 433 assert.isTrue(ASRouterPreferences.devtoolsEnabled); 434 assert.deepEqual(ASRouterPreferences.providers, TEST_PROVIDERS); 435 }); 436 it("should call listeners added with .addListener", () => { 437 const callback1 = sinon.stub(); 438 const callback2 = sinon.stub(); 439 ASRouterPreferences.init(); 440 ASRouterPreferences.addListener(callback1); 441 ASRouterPreferences.addListener(callback2); 442 443 ASRouterPreferences.observe(null, null, getPrefNameForProvider("foo")); 444 assert.calledWith(callback1, getPrefNameForProvider("foo")); 445 446 ASRouterPreferences.observe(null, null, DEVTOOLS_PREF); 447 assert.calledWith(callback2, DEVTOOLS_PREF); 448 }); 449 it("should not call listeners after they are removed with .removeListeners", () => { 450 const callback = sinon.stub(); 451 ASRouterPreferences.init(); 452 ASRouterPreferences.addListener(callback); 453 454 ASRouterPreferences.observe(null, null, getPrefNameForProvider("foo")); 455 assert.calledWith(callback, getPrefNameForProvider("foo")); 456 457 callback.reset(); 458 ASRouterPreferences.removeListener(callback); 459 460 ASRouterPreferences.observe(null, null, DEVTOOLS_PREF); 461 assert.notCalled(callback); 462 }); 463 }); 464 describe("#_transformPersonalizedCfrScores", () => { 465 it("should report JSON.parse errors", () => { 466 sandbox.stub(global.console, "error"); 467 468 ASRouterPreferences._transformPersonalizedCfrScores(""); 469 470 assert.calledOnce(global.console.error); 471 }); 472 it("should return an object parsed from a string", () => { 473 const scores = { FOO: 3000, BAR: 4000 }; 474 assert.deepEqual( 475 ASRouterPreferences._transformPersonalizedCfrScores( 476 JSON.stringify(scores) 477 ), 478 scores 479 ); 480 }); 481 }); 482 });