blob: 0f1d4aced574ac720649030e8d54aeca53fd45f9 [file] [log] [blame]
// 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);
});
});
});