Project import generated by Copybara.

GitOrigin-RevId: d9e9e3fb4e31372ec1fb43b178994ca78fa8fe70
diff --git a/static_src/reducers/hotlists.test.js b/static_src/reducers/hotlists.test.js
new file mode 100644
index 0000000..4aa42a2
--- /dev/null
+++ b/static_src/reducers/hotlists.test.js
@@ -0,0 +1,568 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import {assert} from 'chai';
+import sinon from 'sinon';
+
+import * as hotlists from './hotlists.js';
+import * as example from 'shared/test/constants-hotlists.js';
+import * as exampleIssues from 'shared/test/constants-issueV0.js';
+import * as exampleUsers from 'shared/test/constants-users.js';
+
+import {prpcClient} from 'prpc-client-instance.js';
+
+let dispatch;
+
+describe('hotlist reducers', () => {
+  it('root reducer initial state', () => {
+    const actual = hotlists.reducer(undefined, {type: null});
+    const expected = {
+      name: null,
+      byName: {},
+      hotlistItems: {},
+      requests: {
+        deleteHotlist: {error: null, requesting: false},
+        fetch: {error: null, requesting: false},
+        fetchItems: {error: null, requesting: false},
+        removeEditors: {error: null, requesting: false},
+        removeItems: {error: null, requesting: false},
+        rerankItems: {error: null, requesting: false},
+        update: {error: null, requesting: false},
+      },
+    };
+    assert.deepEqual(actual, expected);
+  });
+
+  it('name updates on SELECT', () => {
+    const action = {type: hotlists.SELECT, name: example.NAME};
+    const actual = hotlists.nameReducer(null, action);
+    assert.deepEqual(actual, example.NAME);
+  });
+
+  it('byName updates on RECEIVE_HOTLIST', () => {
+    const action = {type: hotlists.RECEIVE_HOTLIST, hotlist: example.HOTLIST};
+    const actual = hotlists.byNameReducer({}, action);
+    assert.deepEqual(actual, example.BY_NAME);
+  });
+
+  it('byName fills in missing fields on RECEIVE_HOTLIST', () => {
+    const action = {
+      type: hotlists.RECEIVE_HOTLIST,
+      hotlist: {name: example.NAME},
+    };
+    const actual = hotlists.byNameReducer({}, action);
+
+    const hotlist = {name: example.NAME, defaultColumns: [], editors: []};
+    assert.deepEqual(actual, {[example.NAME]: hotlist});
+  });
+
+  it('hotlistItems updates on FETCH_ITEMS_SUCCESS', () => {
+    const action = {
+      type: hotlists.FETCH_ITEMS_SUCCESS,
+      name: example.NAME,
+      items: [example.HOTLIST_ITEM],
+    };
+    const actual = hotlists.hotlistItemsReducer({}, action);
+    assert.deepEqual(actual, example.HOTLIST_ITEMS);
+  });
+});
+
+describe('hotlist selectors', () => {
+  it('name', () => {
+    const state = {hotlists: {name: example.NAME}};
+    assert.deepEqual(hotlists.name(state), example.NAME);
+  });
+
+  it('byName', () => {
+    const state = {hotlists: {byName: example.BY_NAME}};
+    assert.deepEqual(hotlists.byName(state), example.BY_NAME);
+  });
+
+  it('hotlistItems', () => {
+    const state = {hotlists: {hotlistItems: example.HOTLIST_ITEMS}};
+    assert.deepEqual(hotlists.hotlistItems(state), example.HOTLIST_ITEMS);
+  });
+
+  describe('viewedHotlist', () => {
+    it('normal case', () => {
+      const state = {hotlists: {name: example.NAME, byName: example.BY_NAME}};
+      assert.deepEqual(hotlists.viewedHotlist(state), example.HOTLIST);
+    });
+
+    it('no name', () => {
+      const state = {hotlists: {name: null, byName: example.BY_NAME}};
+      assert.deepEqual(hotlists.viewedHotlist(state), null);
+    });
+
+    it('hotlist not found', () => {
+      const state = {hotlists: {name: example.NAME, byName: {}}};
+      assert.deepEqual(hotlists.viewedHotlist(state), null);
+    });
+  });
+
+  describe('viewedHotlistOwner', () => {
+    it('normal case', () => {
+      const state = {
+        hotlists: {name: example.NAME, byName: example.BY_NAME},
+        users: {byName: exampleUsers.BY_NAME},
+      };
+      assert.deepEqual(hotlists.viewedHotlistOwner(state), exampleUsers.USER);
+    });
+
+    it('no hotlist', () => {
+      const state = {hotlists: {}, users: {}};
+      assert.deepEqual(hotlists.viewedHotlistOwner(state), null);
+    });
+  });
+
+  describe('viewedHotlistEditors', () => {
+    it('normal case', () => {
+      const state = {
+        hotlists: {
+          name: example.NAME,
+          byName: {[example.NAME]: {
+            ...example.HOTLIST,
+            editors: [exampleUsers.NAME, exampleUsers.NAME_2],
+          }},
+        },
+        users: {byName: exampleUsers.BY_NAME},
+      };
+
+      const editors = [exampleUsers.USER, exampleUsers.USER_2];
+      assert.deepEqual(hotlists.viewedHotlistEditors(state), editors);
+    });
+
+    it('no user data', () => {
+      const editors = [exampleUsers.NAME, exampleUsers.NAME_2];
+      const state = {
+        hotlists: {
+          name: example.NAME,
+          byName: {[example.NAME]: {...example.HOTLIST, editors}},
+        },
+        users: {byName: {}},
+      };
+      assert.deepEqual(hotlists.viewedHotlistEditors(state), [null, null]);
+    });
+
+    it('no hotlist', () => {
+      const state = {hotlists: {}, users: {}};
+      assert.deepEqual(hotlists.viewedHotlistEditors(state), null);
+    });
+  });
+
+  describe('viewedHotlistItems', () => {
+    it('normal case', () => {
+      const state = {hotlists: {
+        name: example.NAME,
+        hotlistItems: example.HOTLIST_ITEMS,
+      }};
+      const actual = hotlists.viewedHotlistItems(state);
+      assert.deepEqual(actual, [example.HOTLIST_ITEM]);
+    });
+
+    it('no name', () => {
+      const state = {hotlists: {
+        name: null,
+        hotlistItems: example.HOTLIST_ITEMS,
+      }};
+      assert.deepEqual(hotlists.viewedHotlistItems(state), []);
+    });
+
+    it('hotlist not found', () => {
+      const state = {hotlists: {name: example.NAME, hotlistItems: {}}};
+      assert.deepEqual(hotlists.viewedHotlistItems(state), []);
+    });
+  });
+
+  describe('viewedHotlistIssues', () => {
+    it('normal case', () => {
+      const state = {
+        hotlists: {
+          name: example.NAME,
+          hotlistItems: example.HOTLIST_ITEMS,
+        },
+        issue: {
+          issuesByRefString: {
+            [exampleIssues.ISSUE_REF_STRING]: exampleIssues.ISSUE,
+          },
+        },
+        users: {byName: {[exampleUsers.NAME]: exampleUsers.USER}},
+      };
+      const actual = hotlists.viewedHotlistIssues(state);
+      assert.deepEqual(actual, [example.HOTLIST_ISSUE]);
+    });
+
+    it('no issue', () => {
+      const state = {
+        hotlists: {
+          name: example.NAME,
+          hotlistItems: example.HOTLIST_ITEMS,
+        },
+        issue: {
+          issuesByRefString: {
+            [exampleIssues.ISSUE_OTHER_PROJECT_REF_STRING]: exampleIssues.ISSUE,
+          },
+        },
+        users: {byName: {}},
+      };
+      assert.deepEqual(hotlists.viewedHotlistIssues(state), []);
+    });
+  });
+
+  describe('viewedHotlistColumns', () => {
+    it('sitewide currentColumns overrides hotlist defaultColumns', () => {
+      const state = {
+        sitewide: {queryParams: {colspec: 'Summary+ColumnName'}},
+        hotlists: {},
+      };
+      const actual = hotlists.viewedHotlistColumns(state);
+      assert.deepEqual(actual, ['Summary', 'ColumnName']);
+    });
+
+    it('uses DEFAULT_COLUMNS when no hotlist', () => {
+      const actual = hotlists.viewedHotlistColumns({hotlists: {}});
+      assert.deepEqual(actual, hotlists.DEFAULT_COLUMNS);
+    });
+
+    it('uses DEFAULT_COLUMNS when hotlist has empty defaultColumns', () => {
+      const state = {hotlists: {
+        name: example.HOTLIST.name,
+        byName: {
+          [example.HOTLIST.name]: {...example.HOTLIST, defaultColumns: []},
+        },
+      }};
+      const actual = hotlists.viewedHotlistColumns(state);
+      assert.deepEqual(actual, hotlists.DEFAULT_COLUMNS);
+    });
+
+    it('uses hotlist defaultColumns', () => {
+      const state = {hotlists: {
+        name: example.HOTLIST.name,
+        byName: {[example.HOTLIST.name]: {
+          ...example.HOTLIST,
+          defaultColumns: [{column: 'ID'}, {column: 'ColumnName'}],
+        }},
+      }};
+      const actual = hotlists.viewedHotlistColumns(state);
+      assert.deepEqual(actual, ['ID', 'ColumnName']);
+    });
+  });
+
+  describe('viewedHotlistPermissions', () => {
+    it('normal case', () => {
+      const permissions = [hotlists.ADMINISTER, hotlists.EDIT];
+      const state = {
+        hotlists: {name: example.NAME, byName: example.BY_NAME},
+        permissions: {byName: {[example.NAME]: {permissions}}},
+      };
+      assert.deepEqual(hotlists.viewedHotlistPermissions(state), permissions);
+    });
+
+    it('no issue', () => {
+      const state = {hotlists: {}, permissions: {}};
+      assert.deepEqual(hotlists.viewedHotlistPermissions(state), []);
+    });
+  });
+});
+
+describe('hotlist action creators', () => {
+  beforeEach(() => {
+    sinon.stub(prpcClient, 'call');
+    dispatch = sinon.stub();
+  });
+
+  afterEach(() => {
+    prpcClient.call.restore();
+  });
+
+  it('select', () => {
+    const actual = hotlists.select(example.NAME);
+    const expected = {type: hotlists.SELECT, name: example.NAME};
+    assert.deepEqual(actual, expected);
+  });
+
+  describe('deleteHotlist', () => {
+    it('success', async () => {
+      prpcClient.call.returns(Promise.resolve({}));
+
+      await hotlists.deleteHotlist(example.NAME)(dispatch);
+
+      sinon.assert.calledWith(dispatch, {type: hotlists.DELETE_START});
+
+      const args = {name: example.NAME};
+      sinon.assert.calledWith(
+          prpcClient.call, 'monorail.v3.Hotlists', 'DeleteHotlist', args);
+
+      sinon.assert.calledWith(dispatch, {type: hotlists.DELETE_SUCCESS});
+    });
+
+    it('failure', async () => {
+      prpcClient.call.throws();
+
+      await hotlists.deleteHotlist(example.NAME)(dispatch);
+
+      const action = {
+        type: hotlists.DELETE_FAILURE,
+        error: sinon.match.any,
+      };
+      sinon.assert.calledWith(dispatch, action);
+    });
+  });
+
+  describe('fetch', () => {
+    it('success', async () => {
+      const hotlist = example.HOTLIST;
+      prpcClient.call.returns(Promise.resolve(hotlist));
+
+      await hotlists.fetch(example.NAME)(dispatch);
+
+      const args = {name: example.NAME};
+      sinon.assert.calledWith(
+          prpcClient.call, 'monorail.v3.Hotlists', 'GetHotlist', args);
+
+      sinon.assert.calledWith(dispatch, {type: hotlists.FETCH_START});
+      sinon.assert.calledWith(dispatch, {type: hotlists.FETCH_SUCCESS});
+      sinon.assert.calledWith(
+          dispatch, {type: hotlists.RECEIVE_HOTLIST, hotlist});
+    });
+
+    it('failure', async () => {
+      prpcClient.call.throws();
+
+      await hotlists.fetch(example.NAME)(dispatch);
+
+      const action = {type: hotlists.FETCH_FAILURE, error: sinon.match.any};
+      sinon.assert.calledWith(dispatch, action);
+    });
+  });
+
+  describe('fetchItems', () => {
+    it('success', async () => {
+      const response = {items: [example.HOTLIST_ITEM]};
+      prpcClient.call.returns(Promise.resolve(response));
+
+      const returnValue = await hotlists.fetchItems(example.NAME)(dispatch);
+      assert.deepEqual(returnValue, [{...example.HOTLIST_ITEM, rank: 0}]);
+
+      const args = {parent: example.NAME, orderBy: 'rank'};
+      sinon.assert.calledWith(
+          prpcClient.call, 'monorail.v3.Hotlists', 'ListHotlistItems', args);
+
+      sinon.assert.calledWith(dispatch, {type: hotlists.FETCH_ITEMS_START});
+      const action = {
+        type: hotlists.FETCH_ITEMS_SUCCESS,
+        name: example.NAME,
+        items: [{...example.HOTLIST_ITEM, rank: 0}],
+      };
+      sinon.assert.calledWith(dispatch, action);
+    });
+
+    it('failure', async () => {
+      prpcClient.call.throws();
+
+      await hotlists.fetchItems(example.NAME)(dispatch);
+
+      const action = {
+        type: hotlists.FETCH_ITEMS_FAILURE,
+        error: sinon.match.any,
+      };
+      sinon.assert.calledWith(dispatch, action);
+    });
+
+    it('success with empty hotlist', async () => {
+      const response = {items: []};
+      prpcClient.call.returns(Promise.resolve(response));
+
+      const returnValue = await hotlists.fetchItems(example.NAME)(dispatch);
+      assert.deepEqual(returnValue, []);
+
+      sinon.assert.calledWith(dispatch, {type: hotlists.FETCH_ITEMS_START});
+
+      const args = {parent: example.NAME, orderBy: 'rank'};
+      sinon.assert.calledWith(
+          prpcClient.call, 'monorail.v3.Hotlists', 'ListHotlistItems', args);
+
+      const action = {
+        type: hotlists.FETCH_ITEMS_SUCCESS,
+        name: example.NAME,
+        items: [],
+      };
+      sinon.assert.calledWith(dispatch, action);
+    });
+  });
+
+  describe('removeEditors', () => {
+    it('success', async () => {
+      prpcClient.call.returns(Promise.resolve({}));
+
+      const editors = [exampleUsers.NAME];
+      await hotlists.removeEditors(example.NAME, editors)(dispatch);
+
+      const args = {name: example.NAME, editors};
+      sinon.assert.calledWith(
+          prpcClient.call, 'monorail.v3.Hotlists',
+          'RemoveHotlistEditors', args);
+
+      sinon.assert.calledWith(dispatch, {type: hotlists.REMOVE_EDITORS_START});
+      const action = {type: hotlists.REMOVE_EDITORS_SUCCESS};
+      sinon.assert.calledWith(dispatch, action);
+    });
+
+    it('failure', async () => {
+      prpcClient.call.throws();
+
+      await hotlists.removeEditors(example.NAME, [])(dispatch);
+
+      const action = {
+        type: hotlists.REMOVE_EDITORS_FAILURE,
+        error: sinon.match.any,
+      };
+      sinon.assert.calledWith(dispatch, action);
+    });
+  });
+
+  describe('removeItems', () => {
+    it('success', async () => {
+      prpcClient.call.returns(Promise.resolve({}));
+
+      const issues = [exampleIssues.NAME];
+      await hotlists.removeItems(example.NAME, issues)(dispatch);
+
+      const args = {parent: example.NAME, issues};
+      sinon.assert.calledWith(
+          prpcClient.call, 'monorail.v3.Hotlists',
+          'RemoveHotlistItems', args);
+
+      sinon.assert.calledWith(dispatch, {type: hotlists.REMOVE_ITEMS_START});
+      sinon.assert.calledWith(dispatch, {type: hotlists.REMOVE_ITEMS_SUCCESS});
+    });
+
+    it('failure', async () => {
+      prpcClient.call.throws();
+
+      await hotlists.removeItems(example.NAME, [])(dispatch);
+
+      const action = {
+        type: hotlists.REMOVE_ITEMS_FAILURE,
+        error: sinon.match.any,
+      };
+      sinon.assert.calledWith(dispatch, action);
+    });
+  });
+
+  describe('rerankItems', () => {
+    it('success', async () => {
+      prpcClient.call.returns(Promise.resolve({}));
+
+      const items = [example.HOTLIST_ITEM_NAME];
+      await hotlists.rerankItems(example.NAME, items, 0)(dispatch);
+
+      const args = {
+        name: example.NAME,
+        hotlistItems: items,
+        targetPosition: 0,
+      };
+      sinon.assert.calledWith(
+          prpcClient.call, 'monorail.v3.Hotlists',
+          'RerankHotlistItems', args);
+
+      sinon.assert.calledWith(dispatch, {type: hotlists.RERANK_ITEMS_START});
+      sinon.assert.calledWith(dispatch, {type: hotlists.RERANK_ITEMS_SUCCESS});
+    });
+
+    it('failure', async () => {
+      prpcClient.call.throws();
+
+      await hotlists.rerankItems(example.NAME, [], 0)(dispatch);
+
+      const action = {
+        type: hotlists.RERANK_ITEMS_FAILURE,
+        error: sinon.match.any,
+      };
+      sinon.assert.calledWith(dispatch, action);
+    });
+  });
+
+  describe('update', () => {
+    it('success', async () => {
+      const hotlistOnlyWithUpdates = {
+        displayName: example.HOTLIST.displayName + 'foo',
+        summary: example.HOTLIST.summary + 'abc',
+      };
+      const hotlist = {...example.HOTLIST, ...hotlistOnlyWithUpdates};
+      prpcClient.call.returns(Promise.resolve(hotlist));
+
+      await hotlists.update(
+          example.HOTLIST.name, hotlistOnlyWithUpdates)(dispatch);
+
+      const hotlistArg = {
+        ...hotlistOnlyWithUpdates,
+        name: example.HOTLIST.name,
+      };
+      const fieldMask = 'displayName,summary';
+      const args = {hotlist: hotlistArg, updateMask: fieldMask};
+      sinon.assert.calledWith(
+          prpcClient.call, 'monorail.v3.Hotlists', 'UpdateHotlist', args);
+
+      sinon.assert.calledWith(dispatch, {type: hotlists.UPDATE_START});
+      sinon.assert.calledWith(dispatch, {type: hotlists.UPDATE_SUCCESS});
+      sinon.assert.calledWith(
+          dispatch, {type: hotlists.RECEIVE_HOTLIST, hotlist});
+    });
+
+    it('failure', async () => {
+      prpcClient.call.throws();
+      const hotlistOnlyWithUpdates = {
+        displayName: example.HOTLIST.displayName + 'foo',
+        summary: example.HOTLIST.summary + 'abc',
+      };
+      try {
+        // TODO(crbug.com/monorail/7883): Use Chai Promises plugin
+        // to assert promise rejected.
+        await hotlists.update(
+            example.HOTLIST.name, hotlistOnlyWithUpdates)(dispatch);
+      } catch (e) {}
+
+      const action = {
+        type: hotlists.UPDATE_FAILURE,
+        error: sinon.match.any,
+      };
+      sinon.assert.calledWith(dispatch, action);
+    });
+  });
+});
+
+describe('helpers', () => {
+  beforeEach(() => {
+    sinon.stub(prpcClient, 'call');
+    dispatch = sinon.stub();
+  });
+
+  afterEach(() => {
+    prpcClient.call.restore();
+  });
+
+  describe('getHotlistName', () => {
+    it('success', async () => {
+      const response = {hotlistId: '1234'};
+      prpcClient.call.returns(Promise.resolve(response));
+
+      const name = await hotlists.getHotlistName('foo@bar.com', 'hotlist');
+      assert.deepEqual(name, 'hotlists/1234');
+
+      const args = {hotlistRef: {
+        owner: {displayName: 'foo@bar.com'},
+        name: 'hotlist',
+      }};
+      sinon.assert.calledWith(
+          prpcClient.call, 'monorail.Features', 'GetHotlistID', args);
+    });
+
+    it('failure', async () => {
+      prpcClient.call.throws();
+
+      assert.isNull(await hotlists.getHotlistName('foo@bar.com', 'hotlist'));
+    });
+  });
+});