Project import generated by Copybara.

GitOrigin-RevId: d9e9e3fb4e31372ec1fb43b178994ca78fa8fe70
diff --git a/static_src/elements/issue-list/mr-list-page/mr-list-page.test.js b/static_src/elements/issue-list/mr-list-page/mr-list-page.test.js
new file mode 100644
index 0000000..0f1d4ac
--- /dev/null
+++ b/static_src/elements/issue-list/mr-list-page/mr-list-page.test.js
@@ -0,0 +1,615 @@
+// 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 sinon from 'sinon';
+import {assert} from 'chai';
+import {prpcClient} from 'prpc-client-instance.js';
+import {MrListPage, DEFAULT_ISSUES_PER_PAGE} from './mr-list-page.js';
+import {SERVER_LIST_ISSUES_LIMIT} from 'shared/consts/index.js';
+import {store, resetState} from 'reducers/base.js';
+
+let element;
+
+describe('mr-list-page', () => {
+  beforeEach(() => {
+    store.dispatch(resetState());
+    element = document.createElement('mr-list-page');
+    document.body.appendChild(element);
+    sinon.stub(prpcClient, 'call');
+  });
+
+  afterEach(() => {
+    document.body.removeChild(element);
+    prpcClient.call.restore();
+  });
+
+  it('initializes', () => {
+    assert.instanceOf(element, MrListPage);
+  });
+
+  it('shows loading page when issues not loaded yet', async () => {
+    element._issueListLoaded = false;
+
+    await element.updateComplete;
+
+    const loading = element.shadowRoot.querySelector('.container-loading');
+    const noIssues = element.shadowRoot.querySelector('.container-no-issues');
+    const issueList = element.shadowRoot.querySelector('mr-issue-list');
+
+    assert.equal(loading.textContent.trim(), 'Loading...');
+    assert.isNull(noIssues);
+    assert.isNull(issueList);
+  });
+
+  it('does not clear existing issue list when loading new issues', async () => {
+    element._fetchingIssueList = true;
+    element._issueListLoaded = true;
+
+    element.totalIssues = 1;
+    element.issues = [{localId: 1, projectName: 'chromium'}];
+
+    await element.updateComplete;
+
+    const loading = element.shadowRoot.querySelector('.container-loading');
+    const noIssues = element.shadowRoot.querySelector('.container-no-issues');
+    const issueList = element.shadowRoot.querySelector('mr-issue-list');
+
+    assert.isNull(loading);
+    assert.isNull(noIssues);
+    assert.isNotNull(issueList);
+    // TODO(crbug.com/monorail/6560): We intend for the snackbar to be shown,
+    // but it is hidden because the store thinks we have 0 total issues.
+  });
+
+  it('shows list when done loading', async () => {
+    element._fetchingIssueList = false;
+    element._issueListLoaded = true;
+
+    element.totalIssues = 100;
+
+    await element.updateComplete;
+
+    const loading = element.shadowRoot.querySelector('.container-loading');
+    const noIssues = element.shadowRoot.querySelector('.container-no-issues');
+    const issueList = element.shadowRoot.querySelector('mr-issue-list');
+
+    assert.isNull(loading);
+    assert.isNull(noIssues);
+    assert.isNotNull(issueList);
+  });
+
+  describe('issue loading snackbar', () => {
+    beforeEach(() => {
+      sinon.spy(store, 'dispatch');
+    });
+
+    afterEach(() => {
+      store.dispatch.restore();
+    });
+
+    it('shows snackbar when loading new list of issues', async () => {
+      sinon.stub(element, 'stateChanged');
+      sinon.stub(element, '_showIssueLoadingSnackbar');
+
+      element._fetchingIssueList = true;
+      element.totalIssues = 1;
+      element.issues = [{localId: 1, projectName: 'chromium'}];
+
+      await element.updateComplete;
+
+      sinon.assert.calledOnce(element._showIssueLoadingSnackbar);
+    });
+
+    it('hides snackbar when issues are done loading', async () => {
+      element._fetchingIssueList = true;
+      element.totalIssues = 1;
+      element.issues = [{localId: 1, projectName: 'chromium'}];
+
+      await element.updateComplete;
+
+      sinon.assert.neverCalledWith(store.dispatch,
+          {type: 'HIDE_SNACKBAR', id: 'FETCH_ISSUE_LIST'});
+
+      element._fetchingIssueList = false;
+
+      await element.updateComplete;
+
+      sinon.assert.calledWith(store.dispatch,
+          {type: 'HIDE_SNACKBAR', id: 'FETCH_ISSUE_LIST'});
+    });
+
+    it('hides snackbar when <mr-list-page> disconnects', async () => {
+      document.body.removeChild(element);
+
+      sinon.assert.calledWith(store.dispatch,
+          {type: 'HIDE_SNACKBAR', id: 'FETCH_ISSUE_LIST'});
+
+      document.body.appendChild(element);
+    });
+
+    it('shows snackbar on issue loading error', async () => {
+      sinon.stub(element, 'stateChanged');
+      sinon.stub(element, '_showIssueErrorSnackbar');
+
+      element._fetchIssueListError = 'Something went wrong';
+
+      await element.updateComplete;
+
+      sinon.assert.calledWith(element._showIssueErrorSnackbar,
+          'Something went wrong');
+    });
+  });
+
+  it('shows no issues when no search results', async () => {
+    element._fetchingIssueList = false;
+    element._issueListLoaded = true;
+
+    element.totalIssues = 0;
+    element._queryParams = {q: 'owner:me'};
+
+    await element.updateComplete;
+
+    const loading = element.shadowRoot.querySelector('.container-loading');
+    const noIssues = element.shadowRoot.querySelector('.container-no-issues');
+    const issueList = element.shadowRoot.querySelector('mr-issue-list');
+
+    assert.isNull(loading);
+    assert.isNotNull(noIssues);
+    assert.isNull(issueList);
+
+    assert.equal(noIssues.querySelector('strong').textContent.trim(),
+        'owner:me');
+  });
+
+  it('offers consider closed issues when no open results', async () => {
+    element._fetchingIssueList = false;
+    element._issueListLoaded = true;
+
+    element.totalIssues = 0;
+    element._queryParams = {q: 'owner:me', can: '2'};
+
+    await element.updateComplete;
+
+    const considerClosed = element.shadowRoot.querySelector('.consider-closed');
+
+    assert.isFalse(considerClosed.hidden);
+
+    element._queryParams = {q: 'owner:me', can: '1'};
+    element._fetchingIssueList = false;
+    element._issueListLoaded = true;
+
+    await element.updateComplete;
+
+    assert.isTrue(considerClosed.hidden);
+  });
+
+  it('refreshes when _queryParams.sort changes', async () => {
+    sinon.stub(element, 'refresh');
+
+    element._queryParams = {q: ''};
+    await element.updateComplete;
+    sinon.assert.callCount(element.refresh, 1);
+
+    element._queryParams = {q: '', colspec: 'Summary+ID'};
+
+    await element.updateComplete;
+    sinon.assert.callCount(element.refresh, 1);
+
+    element._queryParams = {q: '', sort: '-Summary'};
+    await element.updateComplete;
+    sinon.assert.callCount(element.refresh, 2);
+
+    element.refresh.restore();
+  });
+
+  it('refreshes when currentQuery changes', async () => {
+    sinon.stub(element, 'refresh');
+
+    element._queryParams = {q: ''};
+    await element.updateComplete;
+    sinon.assert.callCount(element.refresh, 1);
+
+    element.currentQuery = 'some query term';
+
+    await element.updateComplete;
+    sinon.assert.callCount(element.refresh, 2);
+
+    element.refresh.restore();
+  });
+
+  it('does not refresh when presentation config not fetched', async () => {
+    sinon.stub(element, 'refresh');
+
+    element._presentationConfigLoaded = false;
+    element.currentQuery = 'some query term';
+
+    await element.updateComplete;
+    sinon.assert.callCount(element.refresh, 0);
+
+    element.refresh.restore();
+  });
+
+  it('refreshes if presentation config fetch finishes last', async () => {
+    sinon.stub(element, 'refresh');
+
+    element._presentationConfigLoaded = false;
+
+    await element.updateComplete;
+    sinon.assert.callCount(element.refresh, 0);
+
+    element._presentationConfigLoaded = true;
+    element.currentQuery = 'some query term';
+
+    await element.updateComplete;
+    sinon.assert.callCount(element.refresh, 1);
+
+    element.refresh.restore();
+  });
+
+  it('startIndex parses _queryParams for value', () => {
+    // Default value.
+    element._queryParams = {};
+    assert.equal(element.startIndex, 0);
+
+    // Int.
+    element._queryParams = {start: 2};
+    assert.equal(element.startIndex, 2);
+
+    // String.
+    element._queryParams = {start: '5'};
+    assert.equal(element.startIndex, 5);
+
+    // Negative value.
+    element._queryParams = {start: -5};
+    assert.equal(element.startIndex, 0);
+
+    // NaN
+    element._queryParams = {start: 'lol'};
+    assert.equal(element.startIndex, 0);
+  });
+
+  it('maxItems parses _queryParams for value', () => {
+    // Default value.
+    element._queryParams = {};
+    assert.equal(element.maxItems, DEFAULT_ISSUES_PER_PAGE);
+
+    // Int.
+    element._queryParams = {num: 50};
+    assert.equal(element.maxItems, 50);
+
+    // String.
+    element._queryParams = {num: '33'};
+    assert.equal(element.maxItems, 33);
+
+    // NaN
+    element._queryParams = {num: 'lol'};
+    assert.equal(element.maxItems, DEFAULT_ISSUES_PER_PAGE);
+  });
+
+  it('parses groupby parameter correctly', () => {
+    element._queryParams = {groupby: 'Priority+Status'};
+
+    assert.deepEqual(element.groups,
+        ['Priority', 'Status']);
+  });
+
+  it('groupby parsing preserves dashed parameters', () => {
+    element._queryParams = {groupby: 'Priority+Custom-Status'};
+
+    assert.deepEqual(element.groups,
+        ['Priority', 'Custom-Status']);
+  });
+
+  describe('pagination', () => {
+    beforeEach(() => {
+      // Stop Redux from overriding values being tested.
+      sinon.stub(element, 'stateChanged');
+    });
+
+    it('issue count hidden when no issues', async () => {
+      element._queryParams = {num: 10, start: 0};
+      element.totalIssues = 0;
+
+      await element.updateComplete;
+
+      const count = element.shadowRoot.querySelector('.issue-count');
+
+      assert.isTrue(count.hidden);
+    });
+
+    it('issue count renders on first page', async () => {
+      element._queryParams = {num: 10, start: 0};
+      element.totalIssues = 100;
+
+      await element.updateComplete;
+
+      const count = element.shadowRoot.querySelector('.issue-count');
+
+      assert.equal(count.textContent.trim(), '1 - 10 of 100');
+    });
+
+    it('issue count renders on middle page', async () => {
+      element._queryParams = {num: 10, start: 50};
+      element.totalIssues = 100;
+
+      await element.updateComplete;
+
+      const count = element.shadowRoot.querySelector('.issue-count');
+
+      assert.equal(count.textContent.trim(), '51 - 60 of 100');
+    });
+
+    it('issue count renders on last page', async () => {
+      element._queryParams = {num: 10, start: 95};
+      element.totalIssues = 100;
+
+      await element.updateComplete;
+
+      const count = element.shadowRoot.querySelector('.issue-count');
+
+      assert.equal(count.textContent.trim(), '96 - 100 of 100');
+    });
+
+    it('issue count renders on single page', async () => {
+      element._queryParams = {num: 100, start: 0};
+      element.totalIssues = 33;
+
+      await element.updateComplete;
+
+      const count = element.shadowRoot.querySelector('.issue-count');
+
+      assert.equal(count.textContent.trim(), '1 - 33 of 33');
+    });
+
+    it('total issue count shows backend limit of 100,000', () => {
+      element.totalIssues = SERVER_LIST_ISSUES_LIMIT;
+      assert.equal(element.totalIssuesDisplay, '100,000+');
+    });
+
+    it('next and prev hidden on single page', async () => {
+      element._queryParams = {num: 500, start: 0};
+      element.totalIssues = 10;
+
+      await element.updateComplete;
+
+      const next = element.shadowRoot.querySelector('.next-link');
+      const prev = element.shadowRoot.querySelector('.prev-link');
+
+      assert.isNull(next);
+      assert.isNull(prev);
+    });
+
+    it('prev hidden on first page', async () => {
+      element._queryParams = {num: 10, start: 0};
+      element.totalIssues = 30;
+
+      await element.updateComplete;
+
+      const next = element.shadowRoot.querySelector('.next-link');
+      const prev = element.shadowRoot.querySelector('.prev-link');
+
+      assert.isNotNull(next);
+      assert.isNull(prev);
+    });
+
+    it('next hidden on last page', async () => {
+      element._queryParams = {num: 10, start: 9};
+      element.totalIssues = 5;
+
+      await element.updateComplete;
+
+      const next = element.shadowRoot.querySelector('.next-link');
+      const prev = element.shadowRoot.querySelector('.prev-link');
+
+      assert.isNull(next);
+      assert.isNotNull(prev);
+    });
+
+    it('next and prev shown on middle page', async () => {
+      element._queryParams = {num: 10, start: 50};
+      element.totalIssues = 100;
+
+      await element.updateComplete;
+
+      const next = element.shadowRoot.querySelector('.next-link');
+      const prev = element.shadowRoot.querySelector('.prev-link');
+
+      assert.isNotNull(next);
+      assert.isNotNull(prev);
+    });
+  });
+
+  describe('edit actions', () => {
+    beforeEach(() => {
+      sinon.stub(window, 'alert');
+
+      // Give the test user edit privileges.
+      element._isLoggedIn = true;
+      element._currentUser = {isSiteAdmin: true};
+    });
+
+    afterEach(() => {
+      window.alert.restore();
+    });
+
+    it('edit actions hidden when user is logged out', async () => {
+      element._isLoggedIn = false;
+
+      await element.updateComplete;
+
+      assert.isNull(element.shadowRoot.querySelector('mr-button-bar'));
+    });
+
+    it('edit actions hidden when user is not a project member', async () => {
+      element._isLoggedIn = true;
+      element._currentUser = {displayName: 'regular@user.com'};
+
+      await element.updateComplete;
+
+      assert.isNull(element.shadowRoot.querySelector('mr-button-bar'));
+    });
+
+    it('edit actions shown when user is a project member', async () => {
+      element.projectName = 'chromium';
+      element._isLoggedIn = true;
+      element._currentUser = {isSiteAdmin: false, userId: '123'};
+      element._usersProjects = new Map([['123', {ownerOf: ['chromium']}]]);
+
+      await element.updateComplete;
+
+      assert.isNotNull(element.shadowRoot.querySelector('mr-button-bar'));
+
+      element.projectName = 'nonmember-project';
+      await element.updateComplete;
+
+      assert.isNull(element.shadowRoot.querySelector('mr-button-bar'));
+    });
+
+    it('edit actions shown when user is a site admin', async () => {
+      element._isLoggedIn = true;
+      element._currentUser = {isSiteAdmin: true};
+
+      await element.updateComplete;
+
+      assert.isNotNull(element.shadowRoot.querySelector('mr-button-bar'));
+    });
+
+    it('bulk edit stops when no issues selected', () => {
+      element.selectedIssues = [];
+      element.projectName = 'test';
+
+      element.bulkEdit();
+
+      sinon.assert.calledWith(window.alert,
+          'Please select some issues to edit.');
+    });
+
+    it('bulk edit redirects to bulk edit page', () => {
+      element.page = sinon.stub();
+      element.selectedIssues = [
+        {localId: 1},
+        {localId: 2},
+      ];
+      element.projectName = 'test';
+
+      element.bulkEdit();
+
+      sinon.assert.calledWith(element.page,
+          '/p/test/issues/bulkedit?ids=1%2C2');
+    });
+
+    it('flag issue as spam stops when no issues selected', () => {
+      element.selectedIssues = [];
+
+      element._flagIssues(true);
+
+      sinon.assert.calledWith(window.alert,
+          'Please select some issues to flag as spam.');
+    });
+
+    it('un-flag issue as spam stops when no issues selected', () => {
+      element.selectedIssues = [];
+
+      element._flagIssues(false);
+
+      sinon.assert.calledWith(window.alert,
+          'Please select some issues to un-flag as spam.');
+    });
+
+    it('flagging issues as spam sends pRPC request', async () => {
+      element.page = sinon.stub();
+      element.selectedIssues = [
+        {localId: 1, projectName: 'test'},
+        {localId: 2, projectName: 'test'},
+      ];
+
+      await element._flagIssues(true);
+
+      sinon.assert.calledWith(prpcClient.call, 'monorail.Issues',
+          'FlagIssues', {
+            issueRefs: [
+              {localId: 1, projectName: 'test'},
+              {localId: 2, projectName: 'test'},
+            ],
+            flag: true,
+          });
+    });
+
+    it('un-flagging issues as spam sends pRPC request', async () => {
+      element.page = sinon.stub();
+      element.selectedIssues = [
+        {localId: 1, projectName: 'test'},
+        {localId: 2, projectName: 'test'},
+      ];
+
+      await element._flagIssues(false);
+
+      sinon.assert.calledWith(prpcClient.call, 'monorail.Issues',
+          'FlagIssues', {
+            issueRefs: [
+              {localId: 1, projectName: 'test'},
+              {localId: 2, projectName: 'test'},
+            ],
+            flag: false,
+          });
+    });
+
+    it('clicking change columns opens dialog', async () => {
+      await element.updateComplete;
+      const dialog = element.shadowRoot.querySelector('mr-change-columns');
+      sinon.stub(dialog, 'open');
+
+      element.openColumnsDialog();
+
+      sinon.assert.calledOnce(dialog.open);
+    });
+
+    it('add to hotlist stops when no issues selected', () => {
+      element.selectedIssues = [];
+      element.projectName = 'test';
+
+      element.addToHotlist();
+
+      sinon.assert.calledWith(window.alert,
+          'Please select some issues to add to hotlists.');
+    });
+
+    it('add to hotlist dialog opens', async () => {
+      element.selectedIssues = [
+        {localId: 1, projectName: 'test'},
+        {localId: 2, projectName: 'test'},
+      ];
+      element.projectName = 'test';
+
+      await element.updateComplete;
+
+      const dialog = element.shadowRoot.querySelector(
+          'mr-update-issue-hotlists-dialog');
+
+      sinon.stub(dialog, 'open');
+
+      element.addToHotlist();
+
+      sinon.assert.calledOnce(dialog.open);
+    });
+
+    it('hotlist update triggers snackbar', async () => {
+      element.selectedIssues = [
+        {localId: 1, projectName: 'test'},
+        {localId: 2, projectName: 'test'},
+      ];
+      element.projectName = 'test';
+      sinon.stub(element, '_showHotlistSaveSnackbar');
+
+      await element.updateComplete;
+
+      const dialog = element.shadowRoot.querySelector(
+          'mr-update-issue-hotlists-dialog');
+
+      element.addToHotlist();
+      dialog.dispatchEvent(new Event('saveSuccess'));
+
+      sinon.assert.calledOnce(element._showHotlistSaveSnackbar);
+    });
+  });
+});