Reducers.test.js (44201B)
1 import { INITIAL_STATE, reducers } from "common/Reducers.sys.mjs"; 2 const { 3 TopSites, 4 App, 5 Prefs, 6 Dialog, 7 Sections, 8 Pocket, 9 Personalization, 10 DiscoveryStream, 11 Search, 12 ExternalComponents, 13 } = reducers; 14 import { actionTypes as at } from "common/Actions.mjs"; 15 16 describe("Reducers", () => { 17 describe("App", () => { 18 it("should return the initial state", () => { 19 const nextState = App(undefined, { type: "FOO" }); 20 assert.equal(nextState, INITIAL_STATE.App); 21 }); 22 it("should set initialized to true on INIT", () => { 23 const nextState = App(undefined, { type: "INIT" }); 24 25 assert.propertyVal(nextState, "initialized", true); 26 }); 27 }); 28 describe("TopSites", () => { 29 it("should return the initial state", () => { 30 const nextState = TopSites(undefined, { type: "FOO" }); 31 assert.equal(nextState, INITIAL_STATE.TopSites); 32 }); 33 it("should add top sites on TOP_SITES_UPDATED", () => { 34 const newRows = [{ url: "foo.com" }, { url: "bar.com" }]; 35 const nextState = TopSites(undefined, { 36 type: at.TOP_SITES_UPDATED, 37 data: { links: newRows }, 38 }); 39 assert.equal(nextState.rows, newRows); 40 }); 41 it("should not update state for empty action.data on TOP_SITES_UPDATED", () => { 42 const nextState = TopSites(undefined, { type: at.TOP_SITES_UPDATED }); 43 assert.equal(nextState, INITIAL_STATE.TopSites); 44 }); 45 it("should initialize prefs on TOP_SITES_UPDATED", () => { 46 const nextState = TopSites(undefined, { 47 type: at.TOP_SITES_UPDATED, 48 data: { links: [], pref: "foo" }, 49 }); 50 51 assert.equal(nextState.pref, "foo"); 52 }); 53 it("should pass prevState.prefs if not present in TOP_SITES_UPDATED", () => { 54 const nextState = TopSites( 55 { prefs: "foo" }, 56 { type: at.TOP_SITES_UPDATED, data: { links: [] } } 57 ); 58 59 assert.equal(nextState.prefs, "foo"); 60 }); 61 it("should set editForm.site to action.data on TOP_SITES_EDIT", () => { 62 const data = { index: 7 }; 63 const nextState = TopSites(undefined, { type: at.TOP_SITES_EDIT, data }); 64 assert.equal(nextState.editForm.index, data.index); 65 }); 66 it("should set editForm to null on TOP_SITES_CANCEL_EDIT", () => { 67 const nextState = TopSites(undefined, { type: at.TOP_SITES_CANCEL_EDIT }); 68 assert.isNull(nextState.editForm); 69 }); 70 it("should preserve the editForm.index", () => { 71 const actionTypes = [ 72 at.PREVIEW_RESPONSE, 73 at.PREVIEW_REQUEST, 74 at.PREVIEW_REQUEST_CANCEL, 75 ]; 76 actionTypes.forEach(type => { 77 const oldState = { editForm: { index: 0, previewUrl: "foo" } }; 78 const action = { type, data: { url: "foo" } }; 79 const nextState = TopSites(oldState, action); 80 assert.equal(nextState.editForm.index, 0); 81 }); 82 }); 83 it("should set previewResponse on PREVIEW_RESPONSE", () => { 84 const oldState = { editForm: { previewUrl: "url" } }; 85 const action = { 86 type: at.PREVIEW_RESPONSE, 87 data: { preview: "data:123", url: "url" }, 88 }; 89 const nextState = TopSites(oldState, action); 90 assert.propertyVal(nextState.editForm, "previewResponse", "data:123"); 91 }); 92 it("should return previous state if action url does not match expected", () => { 93 const oldState = { editForm: { previewUrl: "foo" } }; 94 const action = { type: at.PREVIEW_RESPONSE, data: { url: "bar" } }; 95 const nextState = TopSites(oldState, action); 96 assert.equal(nextState, oldState); 97 }); 98 it("should return previous state if editForm is not set", () => { 99 const actionTypes = [ 100 at.PREVIEW_RESPONSE, 101 at.PREVIEW_REQUEST, 102 at.PREVIEW_REQUEST_CANCEL, 103 ]; 104 actionTypes.forEach(type => { 105 const oldState = { editForm: null }; 106 const action = { type, data: { url: "bar" } }; 107 const nextState = TopSites(oldState, action); 108 assert.equal(nextState, oldState, type); 109 }); 110 }); 111 it("should set previewResponse to null on PREVIEW_REQUEST", () => { 112 const oldState = { editForm: { previewResponse: "foo" } }; 113 const action = { type: at.PREVIEW_REQUEST, data: {} }; 114 const nextState = TopSites(oldState, action); 115 assert.propertyVal(nextState.editForm, "previewResponse", null); 116 }); 117 it("should set previewUrl on PREVIEW_REQUEST", () => { 118 const oldState = { editForm: {} }; 119 const action = { type: at.PREVIEW_REQUEST, data: { url: "bar" } }; 120 const nextState = TopSites(oldState, action); 121 assert.propertyVal(nextState.editForm, "previewUrl", "bar"); 122 }); 123 it("should add screenshots for SCREENSHOT_UPDATED", () => { 124 const oldState = { rows: [{ url: "foo.com" }, { url: "bar.com" }] }; 125 const action = { 126 type: at.SCREENSHOT_UPDATED, 127 data: { url: "bar.com", screenshot: "data:123" }, 128 }; 129 const nextState = TopSites(oldState, action); 130 assert.deepEqual(nextState.rows, [ 131 { url: "foo.com" }, 132 { url: "bar.com", screenshot: "data:123" }, 133 ]); 134 }); 135 it("should not modify rows if nothing matches the url for SCREENSHOT_UPDATED", () => { 136 const oldState = { rows: [{ url: "foo.com" }, { url: "bar.com" }] }; 137 const action = { 138 type: at.SCREENSHOT_UPDATED, 139 data: { url: "baz.com", screenshot: "data:123" }, 140 }; 141 const nextState = TopSites(oldState, action); 142 assert.deepEqual(nextState, oldState); 143 }); 144 it("should bookmark an item on PLACES_BOOKMARK_ADDED", () => { 145 const oldState = { rows: [{ url: "foo.com" }, { url: "bar.com" }] }; 146 const action = { 147 type: at.PLACES_BOOKMARK_ADDED, 148 data: { 149 url: "bar.com", 150 bookmarkGuid: "bookmark123", 151 bookmarkTitle: "Title for bar.com", 152 dateAdded: 1234567, 153 }, 154 }; 155 const nextState = TopSites(oldState, action); 156 const [, newRow] = nextState.rows; 157 // new row has bookmark data 158 assert.equal(newRow.url, action.data.url); 159 assert.equal(newRow.bookmarkGuid, action.data.bookmarkGuid); 160 assert.equal(newRow.bookmarkTitle, action.data.bookmarkTitle); 161 assert.equal(newRow.bookmarkDateCreated, action.data.dateAdded); 162 163 // old row is unchanged 164 assert.equal(nextState.rows[0], oldState.rows[0]); 165 }); 166 it("should not update state for empty action.data on PLACES_BOOKMARK_ADDED", () => { 167 const nextState = TopSites(undefined, { type: at.PLACES_BOOKMARK_ADDED }); 168 assert.equal(nextState, INITIAL_STATE.TopSites); 169 }); 170 it("should remove a bookmark on PLACES_BOOKMARKS_REMOVED", () => { 171 const oldState = { 172 rows: [ 173 { url: "foo.com" }, 174 { 175 url: "bar.com", 176 bookmarkGuid: "bookmark123", 177 bookmarkTitle: "Title for bar.com", 178 dateAdded: 123456, 179 }, 180 ], 181 }; 182 const action = { 183 type: at.PLACES_BOOKMARKS_REMOVED, 184 data: { urls: ["bar.com"] }, 185 }; 186 const nextState = TopSites(oldState, action); 187 const [, newRow] = nextState.rows; 188 // new row no longer has bookmark data 189 assert.equal(newRow.url, oldState.rows[1].url); 190 assert.isUndefined(newRow.bookmarkGuid); 191 assert.isUndefined(newRow.bookmarkTitle); 192 assert.isUndefined(newRow.bookmarkDateCreated); 193 194 // old row is unchanged 195 assert.deepEqual(nextState.rows[0], oldState.rows[0]); 196 }); 197 it("should not update state for empty action.data on PLACES_BOOKMARKS_REMOVED", () => { 198 const nextState = TopSites(undefined, { 199 type: at.PLACES_BOOKMARKS_REMOVED, 200 }); 201 assert.equal(nextState, INITIAL_STATE.TopSites); 202 }); 203 it("should update prefs on TOP_SITES_PREFS_UPDATED", () => { 204 const state = TopSites( 205 {}, 206 { type: at.TOP_SITES_PREFS_UPDATED, data: { pref: "foo" } } 207 ); 208 209 assert.equal(state.pref, "foo"); 210 }); 211 it("should not update state for empty action.data on PLACES_LINKS_DELETED", () => { 212 const nextState = TopSites(undefined, { type: at.PLACES_LINKS_DELETED }); 213 assert.equal(nextState, INITIAL_STATE.TopSites); 214 }); 215 it("should remove the site on PLACES_LINKS_DELETED", () => { 216 const oldState = { rows: [{ url: "foo.com" }, { url: "bar.com" }] }; 217 const deleteAction = { 218 type: at.PLACES_LINKS_DELETED, 219 data: { urls: ["foo.com"] }, 220 }; 221 const nextState = TopSites(oldState, deleteAction); 222 assert.deepEqual(nextState.rows, [{ url: "bar.com" }]); 223 }); 224 it("should set showSearchShortcutsForm to true on TOP_SITES_OPEN_SEARCH_SHORTCUTS_MODAL", () => { 225 const data = { index: 7 }; 226 const nextState = TopSites(undefined, { 227 type: at.TOP_SITES_OPEN_SEARCH_SHORTCUTS_MODAL, 228 data, 229 }); 230 assert.isTrue(nextState.showSearchShortcutsForm); 231 }); 232 it("should set showSearchShortcutsForm to false on TOP_SITES_CLOSE_SEARCH_SHORTCUTS_MODAL", () => { 233 const nextState = TopSites(undefined, { 234 type: at.TOP_SITES_CLOSE_SEARCH_SHORTCUTS_MODAL, 235 }); 236 assert.isFalse(nextState.showSearchShortcutsForm); 237 }); 238 it("should update searchShortcuts on UPDATE_SEARCH_SHORTCUTS", () => { 239 const shortcuts = [ 240 { 241 keyword: "@google", 242 shortURL: "google", 243 url: "https://google.com", 244 searchIdentifier: /^google/, 245 }, 246 { 247 keyword: "@baidu", 248 shortURL: "baidu", 249 url: "https://baidu.com", 250 searchIdentifier: /^baidu/, 251 }, 252 ]; 253 const nextState = TopSites(undefined, { 254 type: at.UPDATE_SEARCH_SHORTCUTS, 255 data: { searchShortcuts: shortcuts }, 256 }); 257 assert.deepEqual(shortcuts, nextState.searchShortcuts); 258 }); 259 it("should set sov positions and state", () => { 260 const positions = [ 261 { position: 0, assignedPartner: "amp" }, 262 { position: 1, assignedPartner: "moz-sales" }, 263 ]; 264 const nextState = TopSites(undefined, { 265 type: at.SOV_UPDATED, 266 data: { ready: true, positions }, 267 }); 268 assert.equal(nextState.sov.ready, true); 269 assert.equal(nextState.sov.positions, positions); 270 }); 271 }); 272 describe("Prefs", () => { 273 function prevState(custom = {}) { 274 return Object.assign({}, INITIAL_STATE.Prefs, custom); 275 } 276 it("should have the correct initial state", () => { 277 const state = Prefs(undefined, {}); 278 assert.deepEqual(state, INITIAL_STATE.Prefs); 279 }); 280 describe("PREFS_INITIAL_VALUES", () => { 281 it("should return a new object", () => { 282 const state = Prefs(undefined, { 283 type: at.PREFS_INITIAL_VALUES, 284 data: {}, 285 }); 286 assert.notEqual( 287 INITIAL_STATE.Prefs, 288 state, 289 "should not modify INITIAL_STATE" 290 ); 291 }); 292 it("should set initalized to true", () => { 293 const state = Prefs(undefined, { 294 type: at.PREFS_INITIAL_VALUES, 295 data: {}, 296 }); 297 assert.isTrue(state.initialized); 298 }); 299 it("should set .values", () => { 300 const newValues = { foo: 1, bar: 2 }; 301 const state = Prefs(undefined, { 302 type: at.PREFS_INITIAL_VALUES, 303 data: newValues, 304 }); 305 assert.equal(state.values, newValues); 306 }); 307 }); 308 describe("PREF_CHANGED", () => { 309 it("should return a new Prefs object", () => { 310 const state = Prefs(undefined, { 311 type: at.PREF_CHANGED, 312 data: { name: "foo", value: 2 }, 313 }); 314 assert.notEqual( 315 INITIAL_STATE.Prefs, 316 state, 317 "should not modify INITIAL_STATE" 318 ); 319 }); 320 it("should set the changed pref", () => { 321 const state = Prefs(prevState({ foo: 1 }), { 322 type: at.PREF_CHANGED, 323 data: { name: "foo", value: 2 }, 324 }); 325 assert.equal(state.values.foo, 2); 326 }); 327 it("should return a new .pref object instead of mutating", () => { 328 const oldState = prevState({ foo: 1 }); 329 const state = Prefs(oldState, { 330 type: at.PREF_CHANGED, 331 data: { name: "foo", value: 2 }, 332 }); 333 assert.notEqual(oldState.values, state.values); 334 }); 335 }); 336 }); 337 describe("Dialog", () => { 338 it("should return INITIAL_STATE by default", () => { 339 assert.equal( 340 INITIAL_STATE.Dialog, 341 Dialog(undefined, { type: "non_existent" }) 342 ); 343 }); 344 it("should toggle visible to true on DIALOG_OPEN", () => { 345 const action = { type: at.DIALOG_OPEN }; 346 const nextState = Dialog(INITIAL_STATE.Dialog, action); 347 assert.isTrue(nextState.visible); 348 }); 349 it("should pass url data on DIALOG_OPEN", () => { 350 const action = { type: at.DIALOG_OPEN, data: "some url" }; 351 const nextState = Dialog(INITIAL_STATE.Dialog, action); 352 assert.equal(nextState.data, action.data); 353 }); 354 it("should toggle visible to false on DIALOG_CANCEL", () => { 355 const action = { type: at.DIALOG_CANCEL, data: "some url" }; 356 const nextState = Dialog(INITIAL_STATE.Dialog, action); 357 assert.isFalse(nextState.visible); 358 }); 359 it("should return inital state on DELETE_HISTORY_URL", () => { 360 const action = { type: at.DELETE_HISTORY_URL }; 361 const nextState = Dialog(INITIAL_STATE.Dialog, action); 362 363 assert.deepEqual(INITIAL_STATE.Dialog, nextState); 364 }); 365 }); 366 describe("Sections", () => { 367 let oldState; 368 369 beforeEach(() => { 370 oldState = new Array(5).fill(null).map((v, i) => ({ 371 id: `foo_bar_${i}`, 372 title: `Foo Bar ${i}`, 373 initialized: false, 374 rows: [ 375 { url: "www.foo.bar", pocket_id: 123 }, 376 { url: "www.other.url" }, 377 ], 378 order: i, 379 type: "history", 380 })); 381 }); 382 383 it("should return INITIAL_STATE by default", () => { 384 assert.equal( 385 INITIAL_STATE.Sections, 386 Sections(undefined, { type: "non_existent" }) 387 ); 388 }); 389 it("should remove the correct section on SECTION_DEREGISTER", () => { 390 const newState = Sections(oldState, { 391 type: at.SECTION_DEREGISTER, 392 data: "foo_bar_2", 393 }); 394 assert.lengthOf(newState, 4); 395 const expectedNewState = oldState.splice(2, 1) && oldState; 396 assert.deepEqual(newState, expectedNewState); 397 }); 398 it("should add a section on SECTION_REGISTER if it doesn't already exist", () => { 399 const action = { 400 type: at.SECTION_REGISTER, 401 data: { id: "foo_bar_5", title: "Foo Bar 5" }, 402 }; 403 const newState = Sections(oldState, action); 404 assert.lengthOf(newState, 6); 405 const insertedSection = newState.find( 406 section => section.id === "foo_bar_5" 407 ); 408 assert.propertyVal(insertedSection, "title", action.data.title); 409 }); 410 it("should set newSection.rows === [] if no rows are provided on SECTION_REGISTER", () => { 411 const action = { 412 type: at.SECTION_REGISTER, 413 data: { id: "foo_bar_5", title: "Foo Bar 5" }, 414 }; 415 const newState = Sections(oldState, action); 416 const insertedSection = newState.find( 417 section => section.id === "foo_bar_5" 418 ); 419 assert.deepEqual(insertedSection.rows, []); 420 }); 421 it("should update a section on SECTION_REGISTER if it already exists", () => { 422 const NEW_TITLE = "New Title"; 423 const action = { 424 type: at.SECTION_REGISTER, 425 data: { id: "foo_bar_2", title: NEW_TITLE }, 426 }; 427 const newState = Sections(oldState, action); 428 assert.lengthOf(newState, 5); 429 const updatedSection = newState.find( 430 section => section.id === "foo_bar_2" 431 ); 432 assert.ok(updatedSection && updatedSection.title === NEW_TITLE); 433 }); 434 it("should set initialized to false on SECTION_REGISTER if there are no rows", () => { 435 const NEW_TITLE = "New Title"; 436 const action = { 437 type: at.SECTION_REGISTER, 438 data: { id: "bloop", title: NEW_TITLE }, 439 }; 440 const newState = Sections(oldState, action); 441 const updatedSection = newState.find(section => section.id === "bloop"); 442 assert.propertyVal(updatedSection, "initialized", false); 443 }); 444 it("should set initialized to true on SECTION_REGISTER if there are rows", () => { 445 const NEW_TITLE = "New Title"; 446 const action = { 447 type: at.SECTION_REGISTER, 448 data: { id: "bloop", title: NEW_TITLE, rows: [{}, {}] }, 449 }; 450 const newState = Sections(oldState, action); 451 const updatedSection = newState.find(section => section.id === "bloop"); 452 assert.propertyVal(updatedSection, "initialized", true); 453 }); 454 it("should have no effect on SECTION_UPDATE if the id doesn't exist", () => { 455 const action = { 456 type: at.SECTION_UPDATE, 457 data: { id: "fake_id", data: "fake_data" }, 458 }; 459 const newState = Sections(oldState, action); 460 assert.deepEqual(oldState, newState); 461 }); 462 it("should update the section with the correct data on SECTION_UPDATE", () => { 463 const FAKE_DATA = { rows: ["some", "fake", "data"], foo: "bar" }; 464 const action = { 465 type: at.SECTION_UPDATE, 466 data: Object.assign(FAKE_DATA, { id: "foo_bar_2" }), 467 }; 468 const newState = Sections(oldState, action); 469 const updatedSection = newState.find( 470 section => section.id === "foo_bar_2" 471 ); 472 assert.include(updatedSection, FAKE_DATA); 473 }); 474 it("should set initialized to true on SECTION_UPDATE if rows is defined on action.data", () => { 475 const data = { rows: [], id: "foo_bar_2" }; 476 const action = { type: at.SECTION_UPDATE, data }; 477 const newState = Sections(oldState, action); 478 const updatedSection = newState.find( 479 section => section.id === "foo_bar_2" 480 ); 481 assert.propertyVal(updatedSection, "initialized", true); 482 }); 483 it("should retain pinned cards on SECTION_UPDATE", () => { 484 const ROW = { id: "row" }; 485 let newState = Sections(oldState, { 486 type: at.SECTION_UPDATE, 487 data: Object.assign({ rows: [ROW] }, { id: "foo_bar_2" }), 488 }); 489 let updatedSection = newState.find(section => section.id === "foo_bar_2"); 490 assert.deepEqual(updatedSection.rows, [ROW]); 491 492 const PINNED_ROW = { id: "pinned", pinned: true, guid: "pinned" }; 493 newState = Sections(newState, { 494 type: at.SECTION_UPDATE, 495 data: Object.assign({ rows: [PINNED_ROW] }, { id: "foo_bar_2" }), 496 }); 497 updatedSection = newState.find(section => section.id === "foo_bar_2"); 498 assert.deepEqual(updatedSection.rows, [PINNED_ROW]); 499 500 // Updating the section again should not duplicate pinned cards 501 newState = Sections(newState, { 502 type: at.SECTION_UPDATE, 503 data: Object.assign({ rows: [PINNED_ROW] }, { id: "foo_bar_2" }), 504 }); 505 updatedSection = newState.find(section => section.id === "foo_bar_2"); 506 assert.deepEqual(updatedSection.rows, [PINNED_ROW]); 507 508 // Updating the section should retain pinned card at its index 509 newState = Sections(newState, { 510 type: at.SECTION_UPDATE, 511 data: Object.assign({ rows: [ROW] }, { id: "foo_bar_2" }), 512 }); 513 updatedSection = newState.find(section => section.id === "foo_bar_2"); 514 assert.deepEqual(updatedSection.rows, [PINNED_ROW, ROW]); 515 516 // Clearing/Resetting the section should clear pinned cards 517 newState = Sections(newState, { 518 type: at.SECTION_UPDATE, 519 data: Object.assign({ rows: [] }, { id: "foo_bar_2" }), 520 }); 521 updatedSection = newState.find(section => section.id === "foo_bar_2"); 522 assert.deepEqual(updatedSection.rows, []); 523 }); 524 it("should have no effect on SECTION_UPDATE_CARD if the id or url doesn't exist", () => { 525 const noIdAction = { 526 type: at.SECTION_UPDATE_CARD, 527 data: { 528 id: "non-existent", 529 url: "www.foo.bar", 530 options: { title: "New title" }, 531 }, 532 }; 533 const noIdState = Sections(oldState, noIdAction); 534 const noUrlAction = { 535 type: at.SECTION_UPDATE_CARD, 536 data: { 537 id: "foo_bar_2", 538 url: "www.non-existent.url", 539 options: { title: "New title" }, 540 }, 541 }; 542 const noUrlState = Sections(oldState, noUrlAction); 543 assert.deepEqual(noIdState, oldState); 544 assert.deepEqual(noUrlState, oldState); 545 }); 546 it("should update the card with the correct data on SECTION_UPDATE_CARD", () => { 547 const action = { 548 type: at.SECTION_UPDATE_CARD, 549 data: { 550 id: "foo_bar_2", 551 url: "www.other.url", 552 options: { title: "Fake new title" }, 553 }, 554 }; 555 const newState = Sections(oldState, action); 556 const updatedSection = newState.find( 557 section => section.id === "foo_bar_2" 558 ); 559 const updatedCard = updatedSection.rows.find( 560 card => card.url === "www.other.url" 561 ); 562 assert.propertyVal(updatedCard, "title", "Fake new title"); 563 }); 564 it("should only update the cards belonging to the right section on SECTION_UPDATE_CARD", () => { 565 const action = { 566 type: at.SECTION_UPDATE_CARD, 567 data: { 568 id: "foo_bar_2", 569 url: "www.other.url", 570 options: { title: "Fake new title" }, 571 }, 572 }; 573 const newState = Sections(oldState, action); 574 newState.forEach((section, i) => { 575 if (section.id !== "foo_bar_2") { 576 assert.deepEqual(section, oldState[i]); 577 } 578 }); 579 }); 580 it("should allow action.data to set .initialized", () => { 581 const data = { rows: [], initialized: false, id: "foo_bar_2" }; 582 const action = { type: at.SECTION_UPDATE, data }; 583 const newState = Sections(oldState, action); 584 const updatedSection = newState.find( 585 section => section.id === "foo_bar_2" 586 ); 587 assert.propertyVal(updatedSection, "initialized", false); 588 }); 589 it("should dedupe based on dedupeConfigurations", () => { 590 const site = { url: "foo.com" }; 591 const highlights = { rows: [site], id: "highlights" }; 592 const topstories = { rows: [site], id: "topstories" }; 593 const dedupeConfigurations = [ 594 { id: "topstories", dedupeFrom: ["highlights"] }, 595 ]; 596 const action = { data: { dedupeConfigurations }, type: "SECTION_UPDATE" }; 597 const state = [highlights, topstories]; 598 599 const nextState = Sections(state, action); 600 601 assert.equal(nextState.find(s => s.id === "highlights").rows.length, 1); 602 assert.equal(nextState.find(s => s.id === "topstories").rows.length, 0); 603 }); 604 it("should remove blocked and deleted urls from all rows in all sections", () => { 605 const blockAction = { 606 type: at.PLACES_LINK_BLOCKED, 607 data: { url: "www.foo.bar" }, 608 }; 609 const deleteAction = { 610 type: at.PLACES_LINKS_DELETED, 611 data: { urls: ["www.foo.bar"] }, 612 }; 613 const newBlockState = Sections(oldState, blockAction); 614 const newDeleteState = Sections(oldState, deleteAction); 615 newBlockState.concat(newDeleteState).forEach(section => { 616 assert.deepEqual(section.rows, [{ url: "www.other.url" }]); 617 }); 618 }); 619 it("should not update state for empty action.data on PLACES_LINK_BLOCKED", () => { 620 const nextState = Sections(undefined, { type: at.PLACES_LINK_BLOCKED }); 621 assert.equal(nextState, INITIAL_STATE.Sections); 622 }); 623 it("should not update state for empty action.data on PLACES_LINKS_DELETED", () => { 624 const nextState = Sections(undefined, { type: at.PLACES_LINKS_DELETED }); 625 assert.equal(nextState, INITIAL_STATE.Sections); 626 }); 627 it("should not update state for empty action.data on PLACES_BOOKMARK_ADDED", () => { 628 const nextState = Sections(undefined, { type: at.PLACES_BOOKMARK_ADDED }); 629 assert.equal(nextState, INITIAL_STATE.Sections); 630 }); 631 it("should bookmark an item when PLACES_BOOKMARK_ADDED is received", () => { 632 const action = { 633 type: at.PLACES_BOOKMARK_ADDED, 634 data: { 635 url: "www.foo.bar", 636 bookmarkGuid: "bookmark123", 637 bookmarkTitle: "Title for bar.com", 638 dateAdded: 1234567, 639 }, 640 }; 641 const nextState = Sections(oldState, action); 642 // check a section to ensure the correct url was bookmarked 643 const [newRow, oldRow] = nextState[0].rows; 644 645 // new row has bookmark data 646 assert.equal(newRow.url, action.data.url); 647 assert.equal(newRow.type, "bookmark"); 648 assert.equal(newRow.bookmarkGuid, action.data.bookmarkGuid); 649 assert.equal(newRow.bookmarkTitle, action.data.bookmarkTitle); 650 assert.equal(newRow.bookmarkDateCreated, action.data.dateAdded); 651 652 // old row is unchanged 653 assert.equal(oldRow, oldState[0].rows[1]); 654 }); 655 it("should not update state for empty action.data on PLACES_BOOKMARKS_REMOVED", () => { 656 const nextState = Sections(undefined, { 657 type: at.PLACES_BOOKMARKS_REMOVED, 658 }); 659 assert.equal(nextState, INITIAL_STATE.Sections); 660 }); 661 it("should remove the bookmark when PLACES_BOOKMARKS_REMOVED is received", () => { 662 const action = { 663 type: at.PLACES_BOOKMARKS_REMOVED, 664 data: { 665 urls: ["www.foo.bar"], 666 bookmarkGuid: "bookmark123", 667 }, 668 }; 669 // add some bookmark data for the first url in rows 670 oldState.forEach(item => { 671 item.rows[0].bookmarkGuid = "bookmark123"; 672 item.rows[0].bookmarkTitle = "Title for bar.com"; 673 item.rows[0].bookmarkDateCreated = 1234567; 674 item.rows[0].type = "bookmark"; 675 }); 676 const nextState = Sections(oldState, action); 677 // check a section to ensure the correct bookmark was removed 678 const [newRow, oldRow] = nextState[0].rows; 679 680 // new row isn't a bookmark 681 assert.equal(newRow.url, action.data.urls[0]); 682 assert.equal(newRow.type, "history"); 683 assert.isUndefined(newRow.bookmarkGuid); 684 assert.isUndefined(newRow.bookmarkTitle); 685 assert.isUndefined(newRow.bookmarkDateCreated); 686 687 // old row is unchanged 688 assert.equal(oldRow, oldState[0].rows[1]); 689 }); 690 }); 691 describe("Pocket", () => { 692 it("should return INITIAL_STATE by default", () => { 693 assert.equal( 694 Pocket(undefined, { type: "some_action" }), 695 INITIAL_STATE.Pocket 696 ); 697 }); 698 it("should set waitingForSpoc on a POCKET_WAITING_FOR_SPOC action", () => { 699 const state = Pocket(undefined, { 700 type: at.POCKET_WAITING_FOR_SPOC, 701 data: false, 702 }); 703 assert.isFalse(state.waitingForSpoc); 704 }); 705 it("should set pocketCta with correct object on a POCKET_CTA", () => { 706 const data = { 707 cta_button: "cta button", 708 cta_text: "cta text", 709 cta_url: "https://cta-url.com", 710 use_cta: true, 711 }; 712 const state = Pocket(undefined, { type: at.POCKET_CTA, data }); 713 assert.equal(state.pocketCta.ctaButton, data.cta_button); 714 assert.equal(state.pocketCta.ctaText, data.cta_text); 715 assert.equal(state.pocketCta.ctaUrl, data.cta_url); 716 assert.equal(state.pocketCta.useCta, data.use_cta); 717 }); 718 }); 719 describe("Personalization", () => { 720 it("should return INITIAL_STATE by default", () => { 721 assert.equal( 722 Personalization(undefined, { type: "some_action" }), 723 INITIAL_STATE.Personalization 724 ); 725 }); 726 it("should set lastUpdated with DISCOVERY_STREAM_PERSONALIZATION_LAST_UPDATED", () => { 727 const state = Personalization(undefined, { 728 type: at.DISCOVERY_STREAM_PERSONALIZATION_LAST_UPDATED, 729 data: { 730 lastUpdated: 123, 731 }, 732 }); 733 assert.equal(state.lastUpdated, 123); 734 }); 735 it("should set initialized to true with DISCOVERY_STREAM_PERSONALIZATION_INIT", () => { 736 const state = Personalization(undefined, { 737 type: at.DISCOVERY_STREAM_PERSONALIZATION_INIT, 738 }); 739 assert.equal(state.initialized, true); 740 }); 741 }); 742 describe("DiscoveryStream", () => { 743 it("should return INITIAL_STATE by default", () => { 744 assert.equal( 745 DiscoveryStream(undefined, { type: "some_action" }), 746 INITIAL_STATE.DiscoveryStream 747 ); 748 }); 749 it("should set layout data with DISCOVERY_STREAM_LAYOUT_UPDATE", () => { 750 const state = DiscoveryStream(undefined, { 751 type: at.DISCOVERY_STREAM_LAYOUT_UPDATE, 752 data: { layout: ["test"] }, 753 }); 754 assert.equal(state.layout[0], "test"); 755 }); 756 it("should reset layout data with DISCOVERY_STREAM_LAYOUT_RESET", () => { 757 const layoutData = { layout: ["test"], lastUpdated: 123 }; 758 const feedsData = { 759 "https://foo.com/feed1": { lastUpdated: 123, data: [1, 2, 3] }, 760 }; 761 const spocsData = { 762 lastUpdated: 123, 763 spocs: [1, 2, 3], 764 }; 765 let state = DiscoveryStream(undefined, { 766 type: at.DISCOVERY_STREAM_LAYOUT_UPDATE, 767 data: layoutData, 768 }); 769 state = DiscoveryStream(state, { 770 type: at.DISCOVERY_STREAM_FEEDS_UPDATE, 771 data: feedsData, 772 }); 773 state = DiscoveryStream(state, { 774 type: at.DISCOVERY_STREAM_SPOCS_UPDATE, 775 data: spocsData, 776 }); 777 state = DiscoveryStream(state, { 778 type: at.DISCOVERY_STREAM_LAYOUT_RESET, 779 }); 780 781 assert.deepEqual(state, INITIAL_STATE.DiscoveryStream); 782 }); 783 it("should set config data with DISCOVERY_STREAM_CONFIG_CHANGE", () => { 784 const state = DiscoveryStream(undefined, { 785 type: at.DISCOVERY_STREAM_CONFIG_CHANGE, 786 data: { enabled: true }, 787 }); 788 assert.deepEqual(state.config, { enabled: true }); 789 }); 790 it("should set feeds as loaded with DISCOVERY_STREAM_FEEDS_UPDATE", () => { 791 const state = DiscoveryStream(undefined, { 792 type: at.DISCOVERY_STREAM_FEEDS_UPDATE, 793 }); 794 assert.isTrue(state.feeds.loaded); 795 }); 796 it("should set spoc_endpoint with DISCOVERY_STREAM_SPOCS_ENDPOINT", () => { 797 const state = DiscoveryStream(undefined, { 798 type: at.DISCOVERY_STREAM_SPOCS_ENDPOINT, 799 data: { url: "foo.com" }, 800 }); 801 assert.equal(state.spocs.spocs_endpoint, "foo.com"); 802 }); 803 it("should use initial state with DISCOVERY_STREAM_SPOCS_PLACEMENTS", () => { 804 const state = DiscoveryStream(undefined, { 805 type: at.DISCOVERY_STREAM_SPOCS_PLACEMENTS, 806 data: {}, 807 }); 808 assert.deepEqual(state.spocs.placements, []); 809 }); 810 it("should set placements with DISCOVERY_STREAM_SPOCS_PLACEMENTS", () => { 811 const state = DiscoveryStream(undefined, { 812 type: at.DISCOVERY_STREAM_SPOCS_PLACEMENTS, 813 data: { 814 placements: [1, 2, 3], 815 }, 816 }); 817 assert.deepEqual(state.spocs.placements, [1, 2, 3]); 818 }); 819 it("should set spocs with DISCOVERY_STREAM_SPOCS_UPDATE", () => { 820 const data = { 821 lastUpdated: 123, 822 spocs: [1, 2, 3], 823 spocsCacheUpdateTime: 10 * 60 * 1000, 824 spocsOnDemand: true, 825 }; 826 const state = DiscoveryStream(undefined, { 827 type: at.DISCOVERY_STREAM_SPOCS_UPDATE, 828 data, 829 }); 830 assert.deepEqual(state.spocs, { 831 spocs_endpoint: "", 832 data: data.spocs, 833 lastUpdated: data.lastUpdated, 834 loaded: true, 835 frequency_caps: [], 836 blocked: [], 837 placements: [], 838 cacheUpdateTime: data.spocsCacheUpdateTime, 839 onDemand: { 840 enabled: data.spocsOnDemand, 841 loaded: false, 842 }, 843 }); 844 }); 845 it("should default to a single spoc placement", () => { 846 const deleteAction = { 847 type: at.DISCOVERY_STREAM_LINK_BLOCKED, 848 data: { url: "https://foo.com" }, 849 }; 850 const oldState = { 851 spocs: { 852 data: { 853 spocs: { 854 items: [ 855 { 856 url: "test-spoc.com", 857 }, 858 ], 859 }, 860 }, 861 loaded: true, 862 }, 863 feeds: { 864 data: {}, 865 loaded: true, 866 }, 867 }; 868 869 const newState = DiscoveryStream(oldState, deleteAction); 870 871 assert.equal(newState.spocs.data.spocs.items.length, 1); 872 }); 873 it("should handle no data from DISCOVERY_STREAM_SPOCS_UPDATE", () => { 874 const data = null; 875 const state = DiscoveryStream(undefined, { 876 type: at.DISCOVERY_STREAM_SPOCS_UPDATE, 877 data, 878 }); 879 assert.deepEqual(state.spocs, INITIAL_STATE.DiscoveryStream.spocs); 880 }); 881 it("should add blocked spocs to blocked array with DISCOVERY_STREAM_SPOC_BLOCKED", () => { 882 const firstState = DiscoveryStream(undefined, { 883 type: at.DISCOVERY_STREAM_SPOC_BLOCKED, 884 data: { url: "https://foo.com" }, 885 }); 886 const secondState = DiscoveryStream(firstState, { 887 type: at.DISCOVERY_STREAM_SPOC_BLOCKED, 888 data: { url: "https://bar.com" }, 889 }); 890 assert.deepEqual(firstState.spocs.blocked, ["https://foo.com"]); 891 assert.deepEqual(secondState.spocs.blocked, [ 892 "https://foo.com", 893 "https://bar.com", 894 ]); 895 }); 896 it("should not update state for empty action.data on DISCOVERY_STREAM_LINK_BLOCKED", () => { 897 const newState = DiscoveryStream(undefined, { 898 type: at.DISCOVERY_STREAM_LINK_BLOCKED, 899 }); 900 assert.equal(newState, INITIAL_STATE.DiscoveryStream); 901 }); 902 it("should not update state if feeds are not loaded", () => { 903 const deleteAction = { 904 type: at.DISCOVERY_STREAM_LINK_BLOCKED, 905 data: { url: "foo.com" }, 906 }; 907 const newState = DiscoveryStream(undefined, deleteAction); 908 assert.equal(newState, INITIAL_STATE.DiscoveryStream); 909 }); 910 it("should not update state if spocs and feeds data is undefined", () => { 911 const deleteAction = { 912 type: at.DISCOVERY_STREAM_LINK_BLOCKED, 913 data: { url: "foo.com" }, 914 }; 915 const oldState = { 916 spocs: { 917 data: {}, 918 loaded: true, 919 placements: [{ name: "spocs" }], 920 }, 921 feeds: { 922 data: {}, 923 loaded: true, 924 }, 925 }; 926 const newState = DiscoveryStream(oldState, deleteAction); 927 assert.deepEqual(newState, oldState); 928 }); 929 it("should remove the site on DISCOVERY_STREAM_LINK_BLOCKED from spocs if feeds data is empty", () => { 930 const deleteAction = { 931 type: at.DISCOVERY_STREAM_LINK_BLOCKED, 932 data: { url: "https://foo.com" }, 933 }; 934 const oldState = { 935 spocs: { 936 data: { 937 spocs: { 938 items: [{ url: "https://foo.com" }, { url: "test-spoc.com" }], 939 }, 940 }, 941 loaded: true, 942 placements: [{ name: "spocs" }], 943 }, 944 feeds: { 945 data: {}, 946 loaded: true, 947 }, 948 }; 949 const newState = DiscoveryStream(oldState, deleteAction); 950 assert.deepEqual(newState.spocs.data.spocs.items, [ 951 { url: "test-spoc.com" }, 952 ]); 953 }); 954 it("should remove the site on DISCOVERY_STREAM_LINK_BLOCKED from feeds if spocs data is empty", () => { 955 const deleteAction = { 956 type: at.DISCOVERY_STREAM_LINK_BLOCKED, 957 data: { url: "https://foo.com" }, 958 }; 959 const oldState = { 960 spocs: { 961 data: {}, 962 loaded: true, 963 placements: [{ name: "spocs" }], 964 }, 965 feeds: { 966 data: { 967 "https://foo.com/feed1": { 968 data: { 969 recommendations: [ 970 { url: "https://foo.com" }, 971 { url: "test.com" }, 972 ], 973 }, 974 }, 975 }, 976 loaded: true, 977 }, 978 }; 979 const newState = DiscoveryStream(oldState, deleteAction); 980 assert.deepEqual( 981 newState.feeds.data["https://foo.com/feed1"].data.recommendations, 982 [{ url: "test.com" }] 983 ); 984 }); 985 it("should remove the site on DISCOVERY_STREAM_LINK_BLOCKED from both feeds and spocs", () => { 986 const oldState = { 987 feeds: { 988 data: { 989 "https://foo.com/feed1": { 990 data: { 991 recommendations: [ 992 { url: "https://foo.com" }, 993 { url: "test.com" }, 994 ], 995 }, 996 }, 997 }, 998 loaded: true, 999 }, 1000 spocs: { 1001 data: { 1002 spocs: { 1003 items: [{ url: "https://foo.com" }, { url: "test-spoc.com" }], 1004 }, 1005 }, 1006 loaded: true, 1007 placements: [{ name: "spocs" }], 1008 }, 1009 }; 1010 const deleteAction = { 1011 type: at.DISCOVERY_STREAM_LINK_BLOCKED, 1012 data: { url: "https://foo.com" }, 1013 }; 1014 const newState = DiscoveryStream(oldState, deleteAction); 1015 assert.deepEqual(newState.spocs.data.spocs.items, [ 1016 { url: "test-spoc.com" }, 1017 ]); 1018 assert.deepEqual( 1019 newState.feeds.data["https://foo.com/feed1"].data.recommendations, 1020 [{ url: "test.com" }] 1021 ); 1022 }); 1023 it("should add boookmark details on PLACES_BOOKMARK_ADDED in both feeds and spocs", () => { 1024 const oldState = { 1025 feeds: { 1026 data: { 1027 "https://foo.com/feed1": { 1028 data: { 1029 recommendations: [ 1030 { url: "https://foo.com" }, 1031 { url: "test.com" }, 1032 ], 1033 }, 1034 }, 1035 }, 1036 loaded: true, 1037 }, 1038 spocs: { 1039 data: { 1040 spocs: { 1041 items: [{ url: "https://foo.com" }, { url: "test-spoc.com" }], 1042 }, 1043 }, 1044 loaded: true, 1045 placements: [{ name: "spocs" }], 1046 }, 1047 }; 1048 const bookmarkAction = { 1049 type: at.PLACES_BOOKMARK_ADDED, 1050 data: { 1051 url: "https://foo.com", 1052 bookmarkGuid: "bookmark123", 1053 bookmarkTitle: "Title for bar.com", 1054 dateAdded: 1234567, 1055 }, 1056 }; 1057 1058 const newState = DiscoveryStream(oldState, bookmarkAction); 1059 1060 assert.lengthOf(newState.spocs.data.spocs.items, 2); 1061 assert.equal( 1062 newState.spocs.data.spocs.items[0].bookmarkGuid, 1063 bookmarkAction.data.bookmarkGuid 1064 ); 1065 assert.equal( 1066 newState.spocs.data.spocs.items[0].bookmarkTitle, 1067 bookmarkAction.data.bookmarkTitle 1068 ); 1069 assert.isUndefined(newState.spocs.data.spocs.items[1].bookmarkGuid); 1070 1071 assert.lengthOf( 1072 newState.feeds.data["https://foo.com/feed1"].data.recommendations, 1073 2 1074 ); 1075 assert.equal( 1076 newState.feeds.data["https://foo.com/feed1"].data.recommendations[0] 1077 .bookmarkGuid, 1078 bookmarkAction.data.bookmarkGuid 1079 ); 1080 assert.equal( 1081 newState.feeds.data["https://foo.com/feed1"].data.recommendations[0] 1082 .bookmarkTitle, 1083 bookmarkAction.data.bookmarkTitle 1084 ); 1085 assert.isUndefined( 1086 newState.feeds.data["https://foo.com/feed1"].data.recommendations[1] 1087 .bookmarkGuid 1088 ); 1089 }); 1090 1091 it("should remove boookmark details on PLACES_BOOKMARKS_REMOVED in both feeds and spocs", () => { 1092 const oldState = { 1093 feeds: { 1094 data: { 1095 "https://foo.com/feed1": { 1096 data: { 1097 recommendations: [ 1098 { 1099 url: "https://foo.com", 1100 bookmarkGuid: "bookmark123", 1101 bookmarkTitle: "Title for bar.com", 1102 }, 1103 { url: "test.com" }, 1104 ], 1105 }, 1106 }, 1107 }, 1108 loaded: true, 1109 }, 1110 spocs: { 1111 data: { 1112 spocs: { 1113 items: [ 1114 { 1115 url: "https://foo.com", 1116 bookmarkGuid: "bookmark123", 1117 bookmarkTitle: "Title for bar.com", 1118 }, 1119 { url: "test-spoc.com" }, 1120 ], 1121 }, 1122 }, 1123 loaded: true, 1124 placements: [{ name: "spocs" }], 1125 }, 1126 }; 1127 const action = { 1128 type: at.PLACES_BOOKMARKS_REMOVED, 1129 data: { 1130 urls: ["https://foo.com"], 1131 }, 1132 }; 1133 1134 const newState = DiscoveryStream(oldState, action); 1135 1136 assert.lengthOf(newState.spocs.data.spocs.items, 2); 1137 assert.isUndefined(newState.spocs.data.spocs.items[0].bookmarkGuid); 1138 assert.isUndefined(newState.spocs.data.spocs.items[0].bookmarkTitle); 1139 1140 assert.lengthOf( 1141 newState.feeds.data["https://foo.com/feed1"].data.recommendations, 1142 2 1143 ); 1144 assert.isUndefined( 1145 newState.feeds.data["https://foo.com/feed1"].data.recommendations[0] 1146 .bookmarkGuid 1147 ); 1148 assert.isUndefined( 1149 newState.feeds.data["https://foo.com/feed1"].data.recommendations[0] 1150 .bookmarkTitle 1151 ); 1152 }); 1153 }); 1154 describe("Search", () => { 1155 it("should return INITIAL_STATE by default", () => { 1156 assert.equal( 1157 Search(undefined, { type: "some_action" }), 1158 INITIAL_STATE.Search 1159 ); 1160 }); 1161 it("should set disable to true on DISABLE_SEARCH", () => { 1162 const nextState = Search(undefined, { type: "DISABLE_SEARCH" }); 1163 assert.propertyVal(nextState, "disable", true); 1164 }); 1165 it("should set focus to true on FAKE_FOCUS_SEARCH", () => { 1166 const nextState = Search(undefined, { type: "FAKE_FOCUS_SEARCH" }); 1167 assert.propertyVal(nextState, "fakeFocus", true); 1168 }); 1169 it("should set focus and disable to false on SHOW_SEARCH", () => { 1170 const nextState = Search(undefined, { type: "SHOW_SEARCH" }); 1171 assert.propertyVal(nextState, "fakeFocus", false); 1172 assert.propertyVal(nextState, "disable", false); 1173 }); 1174 }); 1175 describe("ExternalComponents", () => { 1176 it("should return INITIAL_STATE by default", () => { 1177 const nextState = ExternalComponents(undefined, { type: "some_action" }); 1178 assert.equal(nextState, INITIAL_STATE.ExternalComponents); 1179 }); 1180 it("should return initial state with empty components array", () => { 1181 const nextState = ExternalComponents(undefined, { type: "some_action" }); 1182 assert.deepEqual(nextState.components, []); 1183 }); 1184 it("should update components on REFRESH_EXTERNAL_COMPONENTS", () => { 1185 const testComponents = [ 1186 { 1187 type: "SEARCH", 1188 componentURL: "chrome://test/content/component.mjs", 1189 tagName: "test-component", 1190 l10nURLs: [], 1191 }, 1192 ]; 1193 const nextState = ExternalComponents(undefined, { 1194 type: at.REFRESH_EXTERNAL_COMPONENTS, 1195 data: testComponents, 1196 }); 1197 assert.deepEqual(nextState.components, testComponents); 1198 }); 1199 it("should preserve other state when updating components", () => { 1200 const testComponents = [ 1201 { 1202 type: "SEARCH", 1203 componentURL: "chrome://test/content/component.mjs", 1204 tagName: "test-component", 1205 l10nURLs: [], 1206 }, 1207 ]; 1208 const prevState = { components: [], otherProp: "value" }; 1209 const nextState = ExternalComponents(prevState, { 1210 type: at.REFRESH_EXTERNAL_COMPONENTS, 1211 data: testComponents, 1212 }); 1213 assert.deepEqual(nextState.components, testComponents); 1214 assert.propertyVal(nextState, "otherProp", "value"); 1215 }); 1216 it("should replace existing components on REFRESH_EXTERNAL_COMPONENTS", () => { 1217 const oldComponents = [ 1218 { 1219 type: "OLD", 1220 componentURL: "chrome://old/content/component.mjs", 1221 tagName: "old-component", 1222 l10nURLs: [], 1223 }, 1224 ]; 1225 const newComponents = [ 1226 { 1227 type: "NEW", 1228 componentURL: "chrome://new/content/component.mjs", 1229 tagName: "new-component", 1230 l10nURLs: [], 1231 }, 1232 ]; 1233 const prevState = { components: oldComponents }; 1234 const nextState = ExternalComponents(prevState, { 1235 type: at.REFRESH_EXTERNAL_COMPONENTS, 1236 data: newComponents, 1237 }); 1238 assert.deepEqual(nextState.components, newComponents); 1239 assert.notDeepEqual(nextState.components, oldComponents); 1240 }); 1241 }); 1242 });