| // Copyright 2019 The Chromium Authors |
| // 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); |
| }); |
| }); |
| }); |