blob: 0f1d4aced574ac720649030e8d54aeca53fd45f9 [file] [log] [blame]
Copybara854996b2021-09-07 19:36:02 +00001// Copyright 2019 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4import sinon from 'sinon';
5import {assert} from 'chai';
6import {prpcClient} from 'prpc-client-instance.js';
7import {MrListPage, DEFAULT_ISSUES_PER_PAGE} from './mr-list-page.js';
8import {SERVER_LIST_ISSUES_LIMIT} from 'shared/consts/index.js';
9import {store, resetState} from 'reducers/base.js';
10
11let element;
12
13describe('mr-list-page', () => {
14 beforeEach(() => {
15 store.dispatch(resetState());
16 element = document.createElement('mr-list-page');
17 document.body.appendChild(element);
18 sinon.stub(prpcClient, 'call');
19 });
20
21 afterEach(() => {
22 document.body.removeChild(element);
23 prpcClient.call.restore();
24 });
25
26 it('initializes', () => {
27 assert.instanceOf(element, MrListPage);
28 });
29
30 it('shows loading page when issues not loaded yet', async () => {
31 element._issueListLoaded = false;
32
33 await element.updateComplete;
34
35 const loading = element.shadowRoot.querySelector('.container-loading');
36 const noIssues = element.shadowRoot.querySelector('.container-no-issues');
37 const issueList = element.shadowRoot.querySelector('mr-issue-list');
38
39 assert.equal(loading.textContent.trim(), 'Loading...');
40 assert.isNull(noIssues);
41 assert.isNull(issueList);
42 });
43
44 it('does not clear existing issue list when loading new issues', async () => {
45 element._fetchingIssueList = true;
46 element._issueListLoaded = true;
47
48 element.totalIssues = 1;
49 element.issues = [{localId: 1, projectName: 'chromium'}];
50
51 await element.updateComplete;
52
53 const loading = element.shadowRoot.querySelector('.container-loading');
54 const noIssues = element.shadowRoot.querySelector('.container-no-issues');
55 const issueList = element.shadowRoot.querySelector('mr-issue-list');
56
57 assert.isNull(loading);
58 assert.isNull(noIssues);
59 assert.isNotNull(issueList);
60 // TODO(crbug.com/monorail/6560): We intend for the snackbar to be shown,
61 // but it is hidden because the store thinks we have 0 total issues.
62 });
63
64 it('shows list when done loading', async () => {
65 element._fetchingIssueList = false;
66 element._issueListLoaded = true;
67
68 element.totalIssues = 100;
69
70 await element.updateComplete;
71
72 const loading = element.shadowRoot.querySelector('.container-loading');
73 const noIssues = element.shadowRoot.querySelector('.container-no-issues');
74 const issueList = element.shadowRoot.querySelector('mr-issue-list');
75
76 assert.isNull(loading);
77 assert.isNull(noIssues);
78 assert.isNotNull(issueList);
79 });
80
81 describe('issue loading snackbar', () => {
82 beforeEach(() => {
83 sinon.spy(store, 'dispatch');
84 });
85
86 afterEach(() => {
87 store.dispatch.restore();
88 });
89
90 it('shows snackbar when loading new list of issues', async () => {
91 sinon.stub(element, 'stateChanged');
92 sinon.stub(element, '_showIssueLoadingSnackbar');
93
94 element._fetchingIssueList = true;
95 element.totalIssues = 1;
96 element.issues = [{localId: 1, projectName: 'chromium'}];
97
98 await element.updateComplete;
99
100 sinon.assert.calledOnce(element._showIssueLoadingSnackbar);
101 });
102
103 it('hides snackbar when issues are done loading', async () => {
104 element._fetchingIssueList = true;
105 element.totalIssues = 1;
106 element.issues = [{localId: 1, projectName: 'chromium'}];
107
108 await element.updateComplete;
109
110 sinon.assert.neverCalledWith(store.dispatch,
111 {type: 'HIDE_SNACKBAR', id: 'FETCH_ISSUE_LIST'});
112
113 element._fetchingIssueList = false;
114
115 await element.updateComplete;
116
117 sinon.assert.calledWith(store.dispatch,
118 {type: 'HIDE_SNACKBAR', id: 'FETCH_ISSUE_LIST'});
119 });
120
121 it('hides snackbar when <mr-list-page> disconnects', async () => {
122 document.body.removeChild(element);
123
124 sinon.assert.calledWith(store.dispatch,
125 {type: 'HIDE_SNACKBAR', id: 'FETCH_ISSUE_LIST'});
126
127 document.body.appendChild(element);
128 });
129
130 it('shows snackbar on issue loading error', async () => {
131 sinon.stub(element, 'stateChanged');
132 sinon.stub(element, '_showIssueErrorSnackbar');
133
134 element._fetchIssueListError = 'Something went wrong';
135
136 await element.updateComplete;
137
138 sinon.assert.calledWith(element._showIssueErrorSnackbar,
139 'Something went wrong');
140 });
141 });
142
143 it('shows no issues when no search results', async () => {
144 element._fetchingIssueList = false;
145 element._issueListLoaded = true;
146
147 element.totalIssues = 0;
148 element._queryParams = {q: 'owner:me'};
149
150 await element.updateComplete;
151
152 const loading = element.shadowRoot.querySelector('.container-loading');
153 const noIssues = element.shadowRoot.querySelector('.container-no-issues');
154 const issueList = element.shadowRoot.querySelector('mr-issue-list');
155
156 assert.isNull(loading);
157 assert.isNotNull(noIssues);
158 assert.isNull(issueList);
159
160 assert.equal(noIssues.querySelector('strong').textContent.trim(),
161 'owner:me');
162 });
163
164 it('offers consider closed issues when no open results', async () => {
165 element._fetchingIssueList = false;
166 element._issueListLoaded = true;
167
168 element.totalIssues = 0;
169 element._queryParams = {q: 'owner:me', can: '2'};
170
171 await element.updateComplete;
172
173 const considerClosed = element.shadowRoot.querySelector('.consider-closed');
174
175 assert.isFalse(considerClosed.hidden);
176
177 element._queryParams = {q: 'owner:me', can: '1'};
178 element._fetchingIssueList = false;
179 element._issueListLoaded = true;
180
181 await element.updateComplete;
182
183 assert.isTrue(considerClosed.hidden);
184 });
185
186 it('refreshes when _queryParams.sort changes', async () => {
187 sinon.stub(element, 'refresh');
188
189 element._queryParams = {q: ''};
190 await element.updateComplete;
191 sinon.assert.callCount(element.refresh, 1);
192
193 element._queryParams = {q: '', colspec: 'Summary+ID'};
194
195 await element.updateComplete;
196 sinon.assert.callCount(element.refresh, 1);
197
198 element._queryParams = {q: '', sort: '-Summary'};
199 await element.updateComplete;
200 sinon.assert.callCount(element.refresh, 2);
201
202 element.refresh.restore();
203 });
204
205 it('refreshes when currentQuery changes', async () => {
206 sinon.stub(element, 'refresh');
207
208 element._queryParams = {q: ''};
209 await element.updateComplete;
210 sinon.assert.callCount(element.refresh, 1);
211
212 element.currentQuery = 'some query term';
213
214 await element.updateComplete;
215 sinon.assert.callCount(element.refresh, 2);
216
217 element.refresh.restore();
218 });
219
220 it('does not refresh when presentation config not fetched', async () => {
221 sinon.stub(element, 'refresh');
222
223 element._presentationConfigLoaded = false;
224 element.currentQuery = 'some query term';
225
226 await element.updateComplete;
227 sinon.assert.callCount(element.refresh, 0);
228
229 element.refresh.restore();
230 });
231
232 it('refreshes if presentation config fetch finishes last', async () => {
233 sinon.stub(element, 'refresh');
234
235 element._presentationConfigLoaded = false;
236
237 await element.updateComplete;
238 sinon.assert.callCount(element.refresh, 0);
239
240 element._presentationConfigLoaded = true;
241 element.currentQuery = 'some query term';
242
243 await element.updateComplete;
244 sinon.assert.callCount(element.refresh, 1);
245
246 element.refresh.restore();
247 });
248
249 it('startIndex parses _queryParams for value', () => {
250 // Default value.
251 element._queryParams = {};
252 assert.equal(element.startIndex, 0);
253
254 // Int.
255 element._queryParams = {start: 2};
256 assert.equal(element.startIndex, 2);
257
258 // String.
259 element._queryParams = {start: '5'};
260 assert.equal(element.startIndex, 5);
261
262 // Negative value.
263 element._queryParams = {start: -5};
264 assert.equal(element.startIndex, 0);
265
266 // NaN
267 element._queryParams = {start: 'lol'};
268 assert.equal(element.startIndex, 0);
269 });
270
271 it('maxItems parses _queryParams for value', () => {
272 // Default value.
273 element._queryParams = {};
274 assert.equal(element.maxItems, DEFAULT_ISSUES_PER_PAGE);
275
276 // Int.
277 element._queryParams = {num: 50};
278 assert.equal(element.maxItems, 50);
279
280 // String.
281 element._queryParams = {num: '33'};
282 assert.equal(element.maxItems, 33);
283
284 // NaN
285 element._queryParams = {num: 'lol'};
286 assert.equal(element.maxItems, DEFAULT_ISSUES_PER_PAGE);
287 });
288
289 it('parses groupby parameter correctly', () => {
290 element._queryParams = {groupby: 'Priority+Status'};
291
292 assert.deepEqual(element.groups,
293 ['Priority', 'Status']);
294 });
295
296 it('groupby parsing preserves dashed parameters', () => {
297 element._queryParams = {groupby: 'Priority+Custom-Status'};
298
299 assert.deepEqual(element.groups,
300 ['Priority', 'Custom-Status']);
301 });
302
303 describe('pagination', () => {
304 beforeEach(() => {
305 // Stop Redux from overriding values being tested.
306 sinon.stub(element, 'stateChanged');
307 });
308
309 it('issue count hidden when no issues', async () => {
310 element._queryParams = {num: 10, start: 0};
311 element.totalIssues = 0;
312
313 await element.updateComplete;
314
315 const count = element.shadowRoot.querySelector('.issue-count');
316
317 assert.isTrue(count.hidden);
318 });
319
320 it('issue count renders on first page', async () => {
321 element._queryParams = {num: 10, start: 0};
322 element.totalIssues = 100;
323
324 await element.updateComplete;
325
326 const count = element.shadowRoot.querySelector('.issue-count');
327
328 assert.equal(count.textContent.trim(), '1 - 10 of 100');
329 });
330
331 it('issue count renders on middle page', async () => {
332 element._queryParams = {num: 10, start: 50};
333 element.totalIssues = 100;
334
335 await element.updateComplete;
336
337 const count = element.shadowRoot.querySelector('.issue-count');
338
339 assert.equal(count.textContent.trim(), '51 - 60 of 100');
340 });
341
342 it('issue count renders on last page', async () => {
343 element._queryParams = {num: 10, start: 95};
344 element.totalIssues = 100;
345
346 await element.updateComplete;
347
348 const count = element.shadowRoot.querySelector('.issue-count');
349
350 assert.equal(count.textContent.trim(), '96 - 100 of 100');
351 });
352
353 it('issue count renders on single page', async () => {
354 element._queryParams = {num: 100, start: 0};
355 element.totalIssues = 33;
356
357 await element.updateComplete;
358
359 const count = element.shadowRoot.querySelector('.issue-count');
360
361 assert.equal(count.textContent.trim(), '1 - 33 of 33');
362 });
363
364 it('total issue count shows backend limit of 100,000', () => {
365 element.totalIssues = SERVER_LIST_ISSUES_LIMIT;
366 assert.equal(element.totalIssuesDisplay, '100,000+');
367 });
368
369 it('next and prev hidden on single page', async () => {
370 element._queryParams = {num: 500, start: 0};
371 element.totalIssues = 10;
372
373 await element.updateComplete;
374
375 const next = element.shadowRoot.querySelector('.next-link');
376 const prev = element.shadowRoot.querySelector('.prev-link');
377
378 assert.isNull(next);
379 assert.isNull(prev);
380 });
381
382 it('prev hidden on first page', async () => {
383 element._queryParams = {num: 10, start: 0};
384 element.totalIssues = 30;
385
386 await element.updateComplete;
387
388 const next = element.shadowRoot.querySelector('.next-link');
389 const prev = element.shadowRoot.querySelector('.prev-link');
390
391 assert.isNotNull(next);
392 assert.isNull(prev);
393 });
394
395 it('next hidden on last page', async () => {
396 element._queryParams = {num: 10, start: 9};
397 element.totalIssues = 5;
398
399 await element.updateComplete;
400
401 const next = element.shadowRoot.querySelector('.next-link');
402 const prev = element.shadowRoot.querySelector('.prev-link');
403
404 assert.isNull(next);
405 assert.isNotNull(prev);
406 });
407
408 it('next and prev shown on middle page', async () => {
409 element._queryParams = {num: 10, start: 50};
410 element.totalIssues = 100;
411
412 await element.updateComplete;
413
414 const next = element.shadowRoot.querySelector('.next-link');
415 const prev = element.shadowRoot.querySelector('.prev-link');
416
417 assert.isNotNull(next);
418 assert.isNotNull(prev);
419 });
420 });
421
422 describe('edit actions', () => {
423 beforeEach(() => {
424 sinon.stub(window, 'alert');
425
426 // Give the test user edit privileges.
427 element._isLoggedIn = true;
428 element._currentUser = {isSiteAdmin: true};
429 });
430
431 afterEach(() => {
432 window.alert.restore();
433 });
434
435 it('edit actions hidden when user is logged out', async () => {
436 element._isLoggedIn = false;
437
438 await element.updateComplete;
439
440 assert.isNull(element.shadowRoot.querySelector('mr-button-bar'));
441 });
442
443 it('edit actions hidden when user is not a project member', async () => {
444 element._isLoggedIn = true;
445 element._currentUser = {displayName: 'regular@user.com'};
446
447 await element.updateComplete;
448
449 assert.isNull(element.shadowRoot.querySelector('mr-button-bar'));
450 });
451
452 it('edit actions shown when user is a project member', async () => {
453 element.projectName = 'chromium';
454 element._isLoggedIn = true;
455 element._currentUser = {isSiteAdmin: false, userId: '123'};
456 element._usersProjects = new Map([['123', {ownerOf: ['chromium']}]]);
457
458 await element.updateComplete;
459
460 assert.isNotNull(element.shadowRoot.querySelector('mr-button-bar'));
461
462 element.projectName = 'nonmember-project';
463 await element.updateComplete;
464
465 assert.isNull(element.shadowRoot.querySelector('mr-button-bar'));
466 });
467
468 it('edit actions shown when user is a site admin', async () => {
469 element._isLoggedIn = true;
470 element._currentUser = {isSiteAdmin: true};
471
472 await element.updateComplete;
473
474 assert.isNotNull(element.shadowRoot.querySelector('mr-button-bar'));
475 });
476
477 it('bulk edit stops when no issues selected', () => {
478 element.selectedIssues = [];
479 element.projectName = 'test';
480
481 element.bulkEdit();
482
483 sinon.assert.calledWith(window.alert,
484 'Please select some issues to edit.');
485 });
486
487 it('bulk edit redirects to bulk edit page', () => {
488 element.page = sinon.stub();
489 element.selectedIssues = [
490 {localId: 1},
491 {localId: 2},
492 ];
493 element.projectName = 'test';
494
495 element.bulkEdit();
496
497 sinon.assert.calledWith(element.page,
498 '/p/test/issues/bulkedit?ids=1%2C2');
499 });
500
501 it('flag issue as spam stops when no issues selected', () => {
502 element.selectedIssues = [];
503
504 element._flagIssues(true);
505
506 sinon.assert.calledWith(window.alert,
507 'Please select some issues to flag as spam.');
508 });
509
510 it('un-flag issue as spam stops when no issues selected', () => {
511 element.selectedIssues = [];
512
513 element._flagIssues(false);
514
515 sinon.assert.calledWith(window.alert,
516 'Please select some issues to un-flag as spam.');
517 });
518
519 it('flagging issues as spam sends pRPC request', async () => {
520 element.page = sinon.stub();
521 element.selectedIssues = [
522 {localId: 1, projectName: 'test'},
523 {localId: 2, projectName: 'test'},
524 ];
525
526 await element._flagIssues(true);
527
528 sinon.assert.calledWith(prpcClient.call, 'monorail.Issues',
529 'FlagIssues', {
530 issueRefs: [
531 {localId: 1, projectName: 'test'},
532 {localId: 2, projectName: 'test'},
533 ],
534 flag: true,
535 });
536 });
537
538 it('un-flagging issues as spam sends pRPC request', async () => {
539 element.page = sinon.stub();
540 element.selectedIssues = [
541 {localId: 1, projectName: 'test'},
542 {localId: 2, projectName: 'test'},
543 ];
544
545 await element._flagIssues(false);
546
547 sinon.assert.calledWith(prpcClient.call, 'monorail.Issues',
548 'FlagIssues', {
549 issueRefs: [
550 {localId: 1, projectName: 'test'},
551 {localId: 2, projectName: 'test'},
552 ],
553 flag: false,
554 });
555 });
556
557 it('clicking change columns opens dialog', async () => {
558 await element.updateComplete;
559 const dialog = element.shadowRoot.querySelector('mr-change-columns');
560 sinon.stub(dialog, 'open');
561
562 element.openColumnsDialog();
563
564 sinon.assert.calledOnce(dialog.open);
565 });
566
567 it('add to hotlist stops when no issues selected', () => {
568 element.selectedIssues = [];
569 element.projectName = 'test';
570
571 element.addToHotlist();
572
573 sinon.assert.calledWith(window.alert,
574 'Please select some issues to add to hotlists.');
575 });
576
577 it('add to hotlist dialog opens', async () => {
578 element.selectedIssues = [
579 {localId: 1, projectName: 'test'},
580 {localId: 2, projectName: 'test'},
581 ];
582 element.projectName = 'test';
583
584 await element.updateComplete;
585
586 const dialog = element.shadowRoot.querySelector(
587 'mr-update-issue-hotlists-dialog');
588
589 sinon.stub(dialog, 'open');
590
591 element.addToHotlist();
592
593 sinon.assert.calledOnce(dialog.open);
594 });
595
596 it('hotlist update triggers snackbar', async () => {
597 element.selectedIssues = [
598 {localId: 1, projectName: 'test'},
599 {localId: 2, projectName: 'test'},
600 ];
601 element.projectName = 'test';
602 sinon.stub(element, '_showHotlistSaveSnackbar');
603
604 await element.updateComplete;
605
606 const dialog = element.shadowRoot.querySelector(
607 'mr-update-issue-hotlists-dialog');
608
609 element.addToHotlist();
610 dialog.dispatchEvent(new Event('saveSuccess'));
611
612 sinon.assert.calledOnce(element._showHotlistSaveSnackbar);
613 });
614 });
615});