tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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