blob: 0c7a0f55812e8110563d35c72b2cccd42730ffff [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.
4
5import {assert} from 'chai';
6import sinon from 'sinon';
7import {createSelector} from 'reselect';
8import {store, resetState} from './base.js';
9import * as issueV0 from './issueV0.js';
10import * as example from 'shared/test/constants-issueV0.js';
11import {fieldTypes} from 'shared/issue-fields.js';
12import {issueToIssueRef, issueRefToString} from 'shared/convertersV0.js';
13import {prpcClient} from 'prpc-client-instance.js';
14import {getSigninInstance} from 'shared/gapi-loader.js';
15
16let prpcCall;
17let dispatch;
18
19describe('issue', () => {
20 beforeEach(() => {
21 store.dispatch(resetState());
22 });
23
24 describe('reducers', () => {
25 describe('issueByRefReducer', () => {
26 it('no-op on unmatching action', () => {
27 const action = {
28 type: 'FAKE_ACTION',
29 issues: [example.ISSUE_OTHER_PROJECT],
30 };
31 assert.deepEqual(issueV0.issuesByRefStringReducer({}, action), {});
32
33 const state = {[example.ISSUE_REF_STRING]: example.ISSUE};
34 assert.deepEqual(issueV0.issuesByRefStringReducer(state, action),
35 state);
36 });
37
38 it('handles FETCH_ISSUE_LIST_UPDATE', () => {
39 const newState = issueV0.issuesByRefStringReducer({}, {
40 type: issueV0.FETCH_ISSUE_LIST_UPDATE,
41 issues: [example.ISSUE, example.ISSUE_OTHER_PROJECT],
42 totalResults: 2,
43 progress: 1,
44 });
45 assert.deepEqual(newState, {
46 [example.ISSUE_REF_STRING]: example.ISSUE,
47 [example.ISSUE_OTHER_PROJECT_REF_STRING]: example.ISSUE_OTHER_PROJECT,
48 });
49 });
50
51 it('handles FETCH_ISSUES_SUCCESS', () => {
52 const newState = issueV0.issuesByRefStringReducer({}, {
53 type: issueV0.FETCH_ISSUES_SUCCESS,
54 issues: [example.ISSUE, example.ISSUE_OTHER_PROJECT],
55 });
56 assert.deepEqual(newState, {
57 [example.ISSUE_REF_STRING]: example.ISSUE,
58 [example.ISSUE_OTHER_PROJECT_REF_STRING]: example.ISSUE_OTHER_PROJECT,
59 });
60 });
61 });
62
63 describe('issueListReducer', () => {
64 it('no-op on unmatching action', () => {
65 const action = {
66 type: 'FETCH_ISSUE_LIST_FAKE_ACTION',
67 issues: [
68 {localId: 1, projectName: 'chromium', summary: 'hello-world'},
69 ],
70 };
71 assert.deepEqual(issueV0.issueListReducer({}, action), {});
72
73 assert.deepEqual(issueV0.issueListReducer({
74 issueRefs: ['chromium:1'],
75 totalResults: 1,
76 progress: 1,
77 }, action), {
78 issueRefs: ['chromium:1'],
79 totalResults: 1,
80 progress: 1,
81 });
82 });
83
84 it('handles FETCH_ISSUE_LIST_UPDATE', () => {
85 const newState = issueV0.issueListReducer({}, {
86 type: 'FETCH_ISSUE_LIST_UPDATE',
87 issues: [
88 {localId: 1, projectName: 'chromium', summary: 'hello-world'},
89 {localId: 2, projectName: 'monorail', summary: 'Test'},
90 ],
91 totalResults: 2,
92 progress: 1,
93 });
94 assert.deepEqual(newState, {
95 issueRefs: ['chromium:1', 'monorail:2'],
96 totalResults: 2,
97 progress: 1,
98 });
99 });
100 });
101
102 describe('relatedIssuesReducer', () => {
103 it('handles FETCH_RELATED_ISSUES_SUCCESS', () => {
104 const newState = issueV0.relatedIssuesReducer({}, {
105 type: 'FETCH_RELATED_ISSUES_SUCCESS',
106 relatedIssues: {'rutabaga:1234': {}},
107 });
108 assert.deepEqual(newState, {'rutabaga:1234': {}});
109 });
110
111 describe('FETCH_FEDERATED_REFERENCES_SUCCESS', () => {
112 it('returns early if data is missing', () => {
113 const newState = issueV0.relatedIssuesReducer({'b/123': {}}, {
114 type: 'FETCH_FEDERATED_REFERENCES_SUCCESS',
115 });
116 assert.deepEqual(newState, {'b/123': {}});
117 });
118
119 it('returns early if data is empty', () => {
120 const newState = issueV0.relatedIssuesReducer({'b/123': {}}, {
121 type: 'FETCH_FEDERATED_REFERENCES_SUCCESS',
122 fedRefIssueRefs: [],
123 });
124 assert.deepEqual(newState, {'b/123': {}});
125 });
126
127 it('assigns each FedRef to the state', () => {
128 const state = {
129 'rutabaga:123': {},
130 'rutabaga:345': {},
131 };
132 const newState = issueV0.relatedIssuesReducer(state, {
133 type: 'FETCH_FEDERATED_REFERENCES_SUCCESS',
134 fedRefIssueRefs: [
135 {
136 extIdentifier: 'b/987',
137 summary: 'What is up',
138 statusRef: {meansOpen: true},
139 },
140 {
141 extIdentifier: 'b/765',
142 summary: 'Rutabaga',
143 statusRef: {meansOpen: false},
144 },
145 ],
146 });
147 assert.deepEqual(newState, {
148 'rutabaga:123': {},
149 'rutabaga:345': {},
150 'b/987': {
151 extIdentifier: 'b/987',
152 summary: 'What is up',
153 statusRef: {meansOpen: true},
154 },
155 'b/765': {
156 extIdentifier: 'b/765',
157 summary: 'Rutabaga',
158 statusRef: {meansOpen: false},
159 },
160 });
161 });
162 });
163 });
164 });
165
166 it('viewedIssue', () => {
167 assert.deepEqual(issueV0.viewedIssue(wrapIssue()), {});
168 assert.deepEqual(
169 issueV0.viewedIssue(wrapIssue({projectName: 'proj', localId: 100})),
170 {projectName: 'proj', localId: 100},
171 );
172 });
173
174 describe('issueList', () => {
175 it('issueList', () => {
176 const stateWithEmptyIssueList = {issue: {
177 issueList: {},
178 }};
179 assert.deepEqual(issueV0.issueList(stateWithEmptyIssueList), []);
180
181 const stateWithIssueList = {issue: {
182 issuesByRefString: {
183 'chromium:1': {localId: 1, projectName: 'chromium', summary: 'test'},
184 'monorail:2': {localId: 2, projectName: 'monorail',
185 summary: 'hello world'},
186 },
187 issueList: {
188 issueRefs: ['chromium:1', 'monorail:2'],
189 }}};
190 assert.deepEqual(issueV0.issueList(stateWithIssueList),
191 [
192 {localId: 1, projectName: 'chromium', summary: 'test'},
193 {localId: 2, projectName: 'monorail', summary: 'hello world'},
194 ]);
195 });
196
197 it('is a selector', () => {
198 issueV0.issueList.constructor === createSelector;
199 });
200
201 it('memoizes results: returns same reference', () => {
202 const stateWithIssueList = {issue: {
203 issuesByRefString: {
204 'chromium:1': {localId: 1, projectName: 'chromium', summary: 'test'},
205 'monorail:2': {localId: 2, projectName: 'monorail',
206 summary: 'hello world'},
207 },
208 issueList: {
209 issueRefs: ['chromium:1', 'monorail:2'],
210 }}};
211 const reference1 = issueV0.issueList(stateWithIssueList);
212 const reference2 = issueV0.issueList(stateWithIssueList);
213
214 assert.equal(typeof reference1, 'object');
215 assert.equal(typeof reference2, 'object');
216 assert.equal(reference1, reference2);
217 });
218 });
219
220 describe('issueListLoaded', () => {
221 const stateWithEmptyIssueList = {issue: {
222 issueList: {},
223 }};
224
225 it('false when no issue list', () => {
226 assert.isFalse(issueV0.issueListLoaded(stateWithEmptyIssueList));
227 });
228
229 it('true after issues loaded, even when empty', () => {
230 const issueList = issueV0.issueListReducer({}, {
231 type: issueV0.FETCH_ISSUE_LIST_UPDATE,
232 issues: [],
233 progress: 1,
234 totalResults: 0,
235 });
236 assert.isTrue(issueV0.issueListLoaded({issue: {issueList}}));
237 });
238 });
239
240 it('fieldValues', () => {
241 assert.isUndefined(issueV0.fieldValues(wrapIssue()));
242 assert.deepEqual(issueV0.fieldValues(wrapIssue({
243 fieldValues: [{value: 'v'}],
244 })), [{value: 'v'}]);
245 });
246
247 it('type computes type from custom field', () => {
248 assert.isUndefined(issueV0.type(wrapIssue()));
249 assert.isUndefined(issueV0.type(wrapIssue({
250 fieldValues: [{value: 'v'}],
251 })));
252 assert.deepEqual(issueV0.type(wrapIssue({
253 fieldValues: [
254 {fieldRef: {fieldName: 'IgnoreMe'}, value: 'v'},
255 {fieldRef: {fieldName: 'Type'}, value: 'Defect'},
256 ],
257 })), 'Defect');
258 });
259
260 it('type computes type from label', () => {
261 assert.deepEqual(issueV0.type(wrapIssue({
262 labelRefs: [
263 {label: 'Test'},
264 {label: 'tYpE-FeatureRequest'},
265 ],
266 })), 'FeatureRequest');
267
268 assert.deepEqual(issueV0.type(wrapIssue({
269 fieldValues: [
270 {fieldRef: {fieldName: 'IgnoreMe'}, value: 'v'},
271 ],
272 labelRefs: [
273 {label: 'Test'},
274 {label: 'Type-Defect'},
275 ],
276 })), 'Defect');
277 });
278
279 it('restrictions', () => {
280 assert.deepEqual(issueV0.restrictions(wrapIssue()), {});
281 assert.deepEqual(issueV0.restrictions(wrapIssue({labelRefs: []})), {});
282
283 assert.deepEqual(issueV0.restrictions(wrapIssue({labelRefs: [
284 {label: 'IgnoreThis'},
285 {label: 'IgnoreThis2'},
286 ]})), {});
287
288 assert.deepEqual(issueV0.restrictions(wrapIssue({labelRefs: [
289 {label: 'IgnoreThis'},
290 {label: 'IgnoreThis2'},
291 {label: 'Restrict-View-Google'},
292 {label: 'Restrict-EditIssue-hello'},
293 {label: 'Restrict-EditIssue-test'},
294 {label: 'Restrict-AddIssueComment-HELLO'},
295 ]})), {
296 'view': ['Google'],
297 'edit': ['hello', 'test'],
298 'comment': ['HELLO'],
299 });
300 });
301
302 it('isOpen', () => {
303 assert.isFalse(issueV0.isOpen(wrapIssue()));
304 assert.isTrue(issueV0.isOpen(wrapIssue({statusRef: {meansOpen: true}})));
305 assert.isFalse(issueV0.isOpen(wrapIssue({statusRef: {meansOpen: false}})));
306 });
307
308 it('issueListPhaseNames', () => {
309 const stateWithEmptyIssueList = {issue: {
310 issueList: [],
311 }};
312 assert.deepEqual(issueV0.issueListPhaseNames(stateWithEmptyIssueList), []);
313 const stateWithIssueList = {issue: {
314 issuesByRefString: {
315 '1': {localId: 1, phases: [{phaseRef: {phaseName: 'chicken-phase'}}]},
316 '2': {localId: 2, phases: [
317 {phaseRef: {phaseName: 'chicken-Phase'}},
318 {phaseRef: {phaseName: 'cow-phase'}}],
319 },
320 '3': {localId: 3, phases: [
321 {phaseRef: {phaseName: 'cow-Phase'}},
322 {phaseRef: {phaseName: 'DOG-phase'}}],
323 },
324 '4': {localId: 4, phases: [
325 {phaseRef: {phaseName: 'dog-phase'}},
326 ]},
327 },
328 issueList: {
329 issueRefs: ['1', '2', '3', '4'],
330 }}};
331 assert.deepEqual(issueV0.issueListPhaseNames(stateWithIssueList),
332 ['chicken-phase', 'cow-phase', 'dog-phase']);
333 });
334
335 describe('blockingIssues', () => {
336 const relatedIssues = {
337 ['proj:1']: {
338 localId: 1,
339 projectName: 'proj',
340 labelRefs: [{label: 'label'}],
341 },
342 ['proj:3']: {
343 localId: 3,
344 projectName: 'proj',
345 labelRefs: [],
346 },
347 ['chromium:332']: {
348 localId: 332,
349 projectName: 'chromium',
350 labelRefs: [],
351 },
352 };
353
354 it('returns references when no issue data', () => {
355 const stateNoReferences = wrapIssue(
356 {
357 projectName: 'project',
358 localId: 123,
359 blockingIssueRefs: [{localId: 1, projectName: 'proj'}],
360 },
361 {relatedIssues: {}},
362 );
363 assert.deepEqual(issueV0.blockingIssues(stateNoReferences),
364 [{localId: 1, projectName: 'proj'}],
365 );
366 });
367
368 it('returns empty when no blocking issues', () => {
369 const stateNoIssues = wrapIssue(
370 {
371 projectName: 'project',
372 localId: 123,
373 blockingIssueRefs: [],
374 },
375 {relatedIssues},
376 );
377 assert.deepEqual(issueV0.blockingIssues(stateNoIssues), []);
378 });
379
380 it('returns full issues when deferenced data present', () => {
381 const stateIssuesWithReferences = wrapIssue(
382 {
383 projectName: 'project',
384 localId: 123,
385 blockingIssueRefs: [
386 {localId: 1, projectName: 'proj'},
387 {localId: 332, projectName: 'chromium'},
388 ],
389 },
390 {relatedIssues},
391 );
392 assert.deepEqual(issueV0.blockingIssues(stateIssuesWithReferences),
393 [
394 {localId: 1, projectName: 'proj', labelRefs: [{label: 'label'}]},
395 {localId: 332, projectName: 'chromium', labelRefs: []},
396 ]);
397 });
398
399 it('returns federated references', () => {
400 const stateIssuesWithFederatedReferences = wrapIssue(
401 {
402 projectName: 'project',
403 localId: 123,
404 blockingIssueRefs: [
405 {localId: 1, projectName: 'proj'},
406 {extIdentifier: 'b/1234'},
407 ],
408 },
409 {relatedIssues},
410 );
411 assert.deepEqual(
412 issueV0.blockingIssues(stateIssuesWithFederatedReferences), [
413 {localId: 1, projectName: 'proj', labelRefs: [{label: 'label'}]},
414 {extIdentifier: 'b/1234'},
415 ]);
416 });
417 });
418
419 describe('blockedOnIssues', () => {
420 const relatedIssues = {
421 ['proj:1']: {
422 localId: 1,
423 projectName: 'proj',
424 labelRefs: [{label: 'label'}],
425 },
426 ['proj:3']: {
427 localId: 3,
428 projectName: 'proj',
429 labelRefs: [],
430 },
431 ['chromium:332']: {
432 localId: 332,
433 projectName: 'chromium',
434 labelRefs: [],
435 },
436 };
437
438 it('returns references when no issue data', () => {
439 const stateNoReferences = wrapIssue(
440 {
441 projectName: 'project',
442 localId: 123,
443 blockedOnIssueRefs: [{localId: 1, projectName: 'proj'}],
444 },
445 {relatedIssues: {}},
446 );
447 assert.deepEqual(issueV0.blockedOnIssues(stateNoReferences),
448 [{localId: 1, projectName: 'proj'}],
449 );
450 });
451
452 it('returns empty when no blocking issues', () => {
453 const stateNoIssues = wrapIssue(
454 {
455 projectName: 'project',
456 localId: 123,
457 blockedOnIssueRefs: [],
458 },
459 {relatedIssues},
460 );
461 assert.deepEqual(issueV0.blockedOnIssues(stateNoIssues), []);
462 });
463
464 it('returns full issues when deferenced data present', () => {
465 const stateIssuesWithReferences = wrapIssue(
466 {
467 projectName: 'project',
468 localId: 123,
469 blockedOnIssueRefs: [
470 {localId: 1, projectName: 'proj'},
471 {localId: 332, projectName: 'chromium'},
472 ],
473 },
474 {relatedIssues},
475 );
476 assert.deepEqual(issueV0.blockedOnIssues(stateIssuesWithReferences),
477 [
478 {localId: 1, projectName: 'proj', labelRefs: [{label: 'label'}]},
479 {localId: 332, projectName: 'chromium', labelRefs: []},
480 ]);
481 });
482
483 it('returns federated references', () => {
484 const stateIssuesWithFederatedReferences = wrapIssue(
485 {
486 projectName: 'project',
487 localId: 123,
488 blockedOnIssueRefs: [
489 {localId: 1, projectName: 'proj'},
490 {extIdentifier: 'b/1234'},
491 ],
492 },
493 {relatedIssues},
494 );
495 assert.deepEqual(
496 issueV0.blockedOnIssues(stateIssuesWithFederatedReferences),
497 [
498 {localId: 1, projectName: 'proj', labelRefs: [{label: 'label'}]},
499 {extIdentifier: 'b/1234'},
500 ]);
501 });
502 });
503
504 describe('sortedBlockedOn', () => {
505 const relatedIssues = {
506 ['proj:1']: {
507 localId: 1,
508 projectName: 'proj',
509 statusRef: {meansOpen: true},
510 },
511 ['proj:3']: {
512 localId: 3,
513 projectName: 'proj',
514 statusRef: {meansOpen: false},
515 },
516 ['proj:4']: {
517 localId: 4,
518 projectName: 'proj',
519 statusRef: {meansOpen: false},
520 },
521 ['proj:5']: {
522 localId: 5,
523 projectName: 'proj',
524 statusRef: {meansOpen: false},
525 },
526 ['chromium:332']: {
527 localId: 332,
528 projectName: 'chromium',
529 statusRef: {meansOpen: true},
530 },
531 };
532
533 it('does not sort references when no issue data', () => {
534 const stateNoReferences = wrapIssue(
535 {
536 projectName: 'project',
537 localId: 123,
538 blockedOnIssueRefs: [
539 {localId: 3, projectName: 'proj'},
540 {localId: 1, projectName: 'proj'},
541 ],
542 },
543 {relatedIssues: {}},
544 );
545 assert.deepEqual(issueV0.sortedBlockedOn(stateNoReferences), [
546 {localId: 3, projectName: 'proj'},
547 {localId: 1, projectName: 'proj'},
548 ]);
549 });
550
551 it('sorts open issues first when issue data available', () => {
552 const stateReferences = wrapIssue(
553 {
554 projectName: 'project',
555 localId: 123,
556 blockedOnIssueRefs: [
557 {localId: 3, projectName: 'proj'},
558 {localId: 1, projectName: 'proj'},
559 ],
560 },
561 {relatedIssues},
562 );
563 assert.deepEqual(issueV0.sortedBlockedOn(stateReferences), [
564 {localId: 1, projectName: 'proj', statusRef: {meansOpen: true}},
565 {localId: 3, projectName: 'proj', statusRef: {meansOpen: false}},
566 ]);
567 });
568
569 it('preserves original order on ties', () => {
570 const statePreservesArrayOrder = wrapIssue(
571 {
572 projectName: 'project',
573 localId: 123,
574 blockedOnIssueRefs: [
575 {localId: 5, projectName: 'proj'}, // Closed
576 {localId: 1, projectName: 'proj'}, // Open
577 {localId: 4, projectName: 'proj'}, // Closed
578 {localId: 3, projectName: 'proj'}, // Closed
579 {localId: 332, projectName: 'chromium'}, // Open
580 ],
581 },
582 {relatedIssues},
583 );
584 assert.deepEqual(issueV0.sortedBlockedOn(statePreservesArrayOrder),
585 [
586 {localId: 1, projectName: 'proj', statusRef: {meansOpen: true}},
587 {localId: 332, projectName: 'chromium',
588 statusRef: {meansOpen: true}},
589 {localId: 5, projectName: 'proj', statusRef: {meansOpen: false}},
590 {localId: 4, projectName: 'proj', statusRef: {meansOpen: false}},
591 {localId: 3, projectName: 'proj', statusRef: {meansOpen: false}},
592 ],
593 );
594 });
595 });
596
597 describe('mergedInto', () => {
598 it('empty', () => {
599 assert.deepEqual(issueV0.mergedInto(wrapIssue()), {});
600 });
601
602 it('gets mergedInto ref for viewed issue', () => {
603 const state = issueV0.mergedInto(wrapIssue({
604 projectName: 'project',
605 localId: 123,
606 mergedIntoIssueRef: {localId: 22, projectName: 'proj'},
607 }));
608 assert.deepEqual(state, {
609 localId: 22,
610 projectName: 'proj',
611 });
612 });
613
614 it('gets full mergedInto issue data when it exists in the store', () => {
615 const state = wrapIssue(
616 {
617 projectName: 'project',
618 localId: 123,
619 mergedIntoIssueRef: {localId: 22, projectName: 'proj'},
620 }, {
621 relatedIssues: {
622 ['proj:22']: {localId: 22, projectName: 'proj', summary: 'test'},
623 },
624 });
625 assert.deepEqual(issueV0.mergedInto(state), {
626 localId: 22,
627 projectName: 'proj',
628 summary: 'test',
629 });
630 });
631 });
632
633 it('fieldValueMap', () => {
634 assert.deepEqual(issueV0.fieldValueMap(wrapIssue()), new Map());
635 assert.deepEqual(issueV0.fieldValueMap(wrapIssue({
636 fieldValues: [],
637 })), new Map());
638 assert.deepEqual(issueV0.fieldValueMap(wrapIssue({
639 fieldValues: [
640 {fieldRef: {fieldName: 'hello'}, value: 'v3'},
641 {fieldRef: {fieldName: 'hello'}, value: 'v2'},
642 {fieldRef: {fieldName: 'world'}, value: 'v3'},
643 ],
644 })), new Map([
645 ['hello', ['v3', 'v2']],
646 ['world', ['v3']],
647 ]));
648 });
649
650 it('fieldDefs filters fields by applicable type', () => {
651 assert.deepEqual(issueV0.fieldDefs({
652 projectV0: {},
653 ...wrapIssue(),
654 }), []);
655
656 assert.deepEqual(issueV0.fieldDefs({
657 projectV0: {
658 name: 'chromium',
659 configs: {
660 chromium: {
661 fieldDefs: [
662 {fieldRef: {fieldName: 'intyInt', type: fieldTypes.INT_TYPE}},
663 {fieldRef: {fieldName: 'enum', type: fieldTypes.ENUM_TYPE}},
664 {
665 fieldRef:
666 {fieldName: 'nonApplicable', type: fieldTypes.STR_TYPE},
667 applicableType: 'None',
668 },
669 {fieldRef: {fieldName: 'defectsOnly', type: fieldTypes.STR_TYPE},
670 applicableType: 'Defect'},
671 ],
672 },
673 },
674 },
675 ...wrapIssue({
676 fieldValues: [
677 {fieldRef: {fieldName: 'Type'}, value: 'Defect'},
678 ],
679 }),
680 }), [
681 {fieldRef: {fieldName: 'intyInt', type: fieldTypes.INT_TYPE}},
682 {fieldRef: {fieldName: 'enum', type: fieldTypes.ENUM_TYPE}},
683 {fieldRef: {fieldName: 'defectsOnly', type: fieldTypes.STR_TYPE},
684 applicableType: 'Defect'},
685 ]);
686 });
687
688 it('fieldDefs skips approval fields for all issues', () => {
689 assert.deepEqual(issueV0.fieldDefs({
690 projectV0: {
691 name: 'chromium',
692 configs: {
693 chromium: {
694 fieldDefs: [
695 {fieldRef: {fieldName: 'test', type: fieldTypes.INT_TYPE}},
696 {fieldRef:
697 {fieldName: 'ignoreMe', type: fieldTypes.APPROVAL_TYPE}},
698 {fieldRef:
699 {fieldName: 'LookAway', approvalName: 'ThisIsAnApproval'}},
700 {fieldRef: {fieldName: 'phaseField'}, isPhaseField: true},
701 ],
702 },
703 },
704 },
705 ...wrapIssue(),
706 }), [
707 {fieldRef: {fieldName: 'test', type: fieldTypes.INT_TYPE}},
708 ]);
709 });
710
711 it('fieldDefs includes non applicable fields when values defined', () => {
712 assert.deepEqual(issueV0.fieldDefs({
713 projectV0: {
714 name: 'chromium',
715 configs: {
716 chromium: {
717 fieldDefs: [
718 {
719 fieldRef:
720 {fieldName: 'nonApplicable', type: fieldTypes.STR_TYPE},
721 applicableType: 'None',
722 },
723 ],
724 },
725 },
726 },
727 ...wrapIssue({
728 fieldValues: [
729 {fieldRef: {fieldName: 'nonApplicable'}, value: 'v3'},
730 ],
731 }),
732 }), [
733 {fieldRef: {fieldName: 'nonApplicable', type: fieldTypes.STR_TYPE},
734 applicableType: 'None'},
735 ]);
736 });
737
738 describe('action creators', () => {
739 beforeEach(() => {
740 prpcCall = sinon.stub(prpcClient, 'call');
741 });
742
743 afterEach(() => {
744 prpcCall.restore();
745 });
746
747 it('viewIssue creates action with issueRef', () => {
748 assert.deepEqual(
749 issueV0.viewIssue({projectName: 'proj', localId: 123}),
750 {
751 type: issueV0.VIEW_ISSUE,
752 issueRef: {projectName: 'proj', localId: 123},
753 },
754 );
755 });
756
757
758 describe('updateApproval', async () => {
759 const APPROVAL = {
760 fieldRef: {fieldName: 'Privacy', type: 'APPROVAL_TYPE'},
761 approverRefs: [{userId: 1234, displayName: 'test@example.com'}],
762 status: 'APPROVED',
763 };
764
765 it('approval update success', async () => {
766 const dispatch = sinon.stub();
767
768 prpcCall.returns({approval: APPROVAL});
769
770 const action = issueV0.updateApproval({
771 issueRef: {projectName: 'chromium', localId: 1234},
772 fieldRef: {fieldName: 'Privacy', type: 'APPROVAL_TYPE'},
773 approvalDelta: {status: 'APPROVED'},
774 sendEmail: true,
775 });
776
777 await action(dispatch);
778
779 sinon.assert.calledOnce(prpcCall);
780
781 sinon.assert.calledWith(prpcCall, 'monorail.Issues',
782 'UpdateApproval', {
783 issueRef: {projectName: 'chromium', localId: 1234},
784 fieldRef: {fieldName: 'Privacy', type: 'APPROVAL_TYPE'},
785 approvalDelta: {status: 'APPROVED'},
786 sendEmail: true,
787 });
788
789 sinon.assert.calledWith(dispatch, {type: 'UPDATE_APPROVAL_START'});
790 sinon.assert.calledWith(dispatch, {
791 type: 'UPDATE_APPROVAL_SUCCESS',
792 approval: APPROVAL,
793 issueRef: {projectName: 'chromium', localId: 1234},
794 });
795 });
796
797 it('approval survey update success', async () => {
798 const dispatch = sinon.stub();
799
800 prpcCall.returns({approval: APPROVAL});
801
802 const action = issueV0.updateApproval({
803 issueRef: {projectName: 'chromium', localId: 1234},
804 fieldRef: {fieldName: 'Privacy', type: 'APPROVAL_TYPE'},
805 commentContent: 'new survey',
806 sendEmail: false,
807 isDescription: true,
808 });
809
810 await action(dispatch);
811
812 sinon.assert.calledOnce(prpcCall);
813
814 sinon.assert.calledWith(prpcCall, 'monorail.Issues',
815 'UpdateApproval', {
816 issueRef: {projectName: 'chromium', localId: 1234},
817 fieldRef: {fieldName: 'Privacy', type: 'APPROVAL_TYPE'},
818 commentContent: 'new survey',
819 isDescription: true,
820 });
821
822 sinon.assert.calledWith(dispatch, {type: 'UPDATE_APPROVAL_START'});
823 sinon.assert.calledWith(dispatch, {
824 type: 'UPDATE_APPROVAL_SUCCESS',
825 approval: APPROVAL,
826 issueRef: {projectName: 'chromium', localId: 1234},
827 });
828 });
829
830 it('attachment upload success', async () => {
831 const dispatch = sinon.stub();
832
833 prpcCall.returns({approval: APPROVAL});
834
835 const action = issueV0.updateApproval({
836 issueRef: {projectName: 'chromium', localId: 1234},
837 fieldRef: {fieldName: 'Privacy', type: 'APPROVAL_TYPE'},
838 uploads: '78f17a020cbf39e90e344a842cd19911',
839 });
840
841 await action(dispatch);
842
843 sinon.assert.calledOnce(prpcCall);
844
845 sinon.assert.calledWith(prpcCall, 'monorail.Issues',
846 'UpdateApproval', {
847 issueRef: {projectName: 'chromium', localId: 1234},
848 fieldRef: {fieldName: 'Privacy', type: 'APPROVAL_TYPE'},
849 uploads: '78f17a020cbf39e90e344a842cd19911',
850 });
851
852 sinon.assert.calledWith(dispatch, {type: 'UPDATE_APPROVAL_START'});
853 sinon.assert.calledWith(dispatch, {
854 type: 'UPDATE_APPROVAL_SUCCESS',
855 approval: APPROVAL,
856 issueRef: {projectName: 'chromium', localId: 1234},
857 });
858 });
859 });
860
861 describe('fetchIssues', () => {
862 it('success', async () => {
863 const response = {
864 openRefs: [example.ISSUE],
865 closedRefs: [example.ISSUE_OTHER_PROJECT],
866 };
867 prpcClient.call.returns(Promise.resolve(response));
868 const dispatch = sinon.stub();
869
870 await issueV0.fetchIssues([example.ISSUE_REF])(dispatch);
871
872 sinon.assert.calledWith(dispatch, {type: issueV0.FETCH_ISSUES_START});
873
874 const args = {issueRefs: [example.ISSUE_REF]};
875 sinon.assert.calledWith(
876 prpcClient.call, 'monorail.Issues', 'ListReferencedIssues', args);
877
878 const action = {
879 type: issueV0.FETCH_ISSUES_SUCCESS,
880 issues: [example.ISSUE, example.ISSUE_OTHER_PROJECT],
881 };
882 sinon.assert.calledWith(dispatch, action);
883 });
884
885 it('failure', async () => {
886 prpcClient.call.throws();
887 const dispatch = sinon.stub();
888
889 await issueV0.fetchIssues([example.ISSUE_REF])(dispatch);
890
891 const action = {
892 type: issueV0.FETCH_ISSUES_FAILURE,
893 error: sinon.match.any,
894 };
895 sinon.assert.calledWith(dispatch, action);
896 });
897 });
898
899 it('fetchIssueList calls ListIssues', async () => {
900 prpcCall.callsFake(() => {
901 return {
902 issues: [{localId: 1}, {localId: 2}, {localId: 3}],
903 totalResults: 6,
904 };
905 });
906
907 store.dispatch(issueV0.fetchIssueList('chromium',
908 {q: 'owner:me', can: '4'}));
909
910 sinon.assert.calledWith(prpcCall, 'monorail.Issues', 'ListIssues', {
911 query: 'owner:me',
912 cannedQuery: 4,
913 projectNames: ['chromium'],
914 pagination: {},
915 groupBySpec: undefined,
916 sortSpec: undefined,
917 });
918 });
919
920 it('fetchIssueList does not set can when can is NaN', async () => {
921 prpcCall.callsFake(() => ({}));
922
923 store.dispatch(issueV0.fetchIssueList('chromium', {q: 'owner:me',
924 can: 'four-leaf-clover'}));
925
926 sinon.assert.calledWith(prpcCall, 'monorail.Issues', 'ListIssues', {
927 query: 'owner:me',
928 cannedQuery: undefined,
929 projectNames: ['chromium'],
930 pagination: {},
931 groupBySpec: undefined,
932 sortSpec: undefined,
933 });
934 });
935
936 it('fetchIssueList makes several calls to ListIssues', async () => {
937 prpcCall.callsFake(() => {
938 return {
939 issues: [{localId: 1}, {localId: 2}, {localId: 3}],
940 totalResults: 6,
941 };
942 });
943
944 const dispatch = sinon.stub();
945 const action = issueV0.fetchIssueList('chromium',
946 {maxItems: 3, maxCalls: 2});
947 await action(dispatch);
948
949 sinon.assert.calledTwice(prpcCall);
950 sinon.assert.calledWith(dispatch, {
951 type: 'FETCH_ISSUE_LIST_UPDATE',
952 issues:
953 [{localId: 1}, {localId: 2}, {localId: 3},
954 {localId: 1}, {localId: 2}, {localId: 3}],
955 progress: 1,
956 totalResults: 6,
957 });
958 sinon.assert.calledWith(dispatch, {type: 'FETCH_ISSUE_LIST_SUCCESS'});
959 });
960
961 it('fetchIssueList orders issues correctly', async () => {
962 prpcCall.onFirstCall().returns({issues: [{localId: 1}], totalResults: 6});
963 prpcCall.onSecondCall().returns({
964 issues: [{localId: 2}],
965 totalResults: 6});
966 prpcCall.onThirdCall().returns({issues: [{localId: 3}], totalResults: 6});
967
968 const dispatch = sinon.stub();
969 const action = issueV0.fetchIssueList('chromium',
970 {maxItems: 1, maxCalls: 3});
971 await action(dispatch);
972
973 sinon.assert.calledWith(dispatch, {
974 type: 'FETCH_ISSUE_LIST_UPDATE',
975 issues: [{localId: 1}, {localId: 2}, {localId: 3}],
976 progress: 1,
977 totalResults: 6,
978 });
979 sinon.assert.calledWith(dispatch, {type: 'FETCH_ISSUE_LIST_SUCCESS'});
980 });
981
982 it('returns progress of 1 when no totalIssues', async () => {
983 prpcCall.onFirstCall().returns({issues: [], totalResults: 0});
984
985 const dispatch = sinon.stub();
986 const action = issueV0.fetchIssueList('chromium',
987 {maxItems: 1, maxCalls: 1});
988 await action(dispatch);
989
990 sinon.assert.calledWith(dispatch, {
991 type: 'FETCH_ISSUE_LIST_UPDATE',
992 issues: [],
993 progress: 1,
994 totalResults: 0,
995 });
996 sinon.assert.calledWith(dispatch, {type: 'FETCH_ISSUE_LIST_SUCCESS'});
997 });
998
999 it('returns progress of 1 when totalIssues undefined', async () => {
1000 prpcCall.onFirstCall().returns({issues: []});
1001
1002 const dispatch = sinon.stub();
1003 const action = issueV0.fetchIssueList('chromium',
1004 {maxItems: 1, maxCalls: 1});
1005 await action(dispatch);
1006
1007 sinon.assert.calledWith(dispatch, {
1008 type: 'FETCH_ISSUE_LIST_UPDATE',
1009 issues: [],
1010 progress: 1,
1011 });
1012 sinon.assert.calledWith(dispatch, {type: 'FETCH_ISSUE_LIST_SUCCESS'});
1013 });
1014
1015 // TODO(kweng@) remove once crbug.com/monorail/6641 is fixed
1016 it('has expected default for empty response', async () => {
1017 prpcCall.onFirstCall().returns({});
1018
1019 const dispatch = sinon.stub();
1020 const action = issueV0.fetchIssueList('chromium',
1021 {maxItems: 1, maxCalls: 1});
1022 await action(dispatch);
1023
1024 sinon.assert.calledWith(dispatch, {
1025 type: 'FETCH_ISSUE_LIST_UPDATE',
1026 issues: [],
1027 progress: 1,
1028 totalResults: 0,
1029 });
1030 sinon.assert.calledWith(dispatch, {type: 'FETCH_ISSUE_LIST_SUCCESS'});
1031 });
1032
1033 describe('federated references', () => {
1034 beforeEach(() => {
1035 // Preload signinImpl with a fake for testing.
1036 getSigninInstance({
1037 init: sinon.stub(),
1038 getUserProfileAsync: () => (
1039 Promise.resolve({
1040 getEmail: sinon.stub().returns('rutabaga@google.com'),
1041 })
1042 ),
1043 });
1044 window.CS_env = {gapi_client_id: 'rutabaga'};
1045 const getStub = sinon.stub().returns({
1046 execute: (cb) => cb(response),
1047 });
1048 const response = {
1049 result: {
1050 resolvedTime: 12345,
1051 issueState: {
1052 title: 'Rutabaga title',
1053 },
1054 },
1055 };
1056 window.gapi = {
1057 client: {
1058 load: (_url, _version, cb) => cb(),
1059 corp_issuetracker: {issues: {get: getStub}},
1060 },
1061 };
1062 });
1063
1064 afterEach(() => {
1065 delete window.CS_env;
1066 delete window.gapi;
1067 });
1068
1069 describe('fetchFederatedReferences', () => {
1070 it('returns an empty map if no fedrefs found', async () => {
1071 const dispatch = sinon.stub();
1072 const testIssue = {};
1073 const action = issueV0.fetchFederatedReferences(testIssue);
1074 const result = await action(dispatch);
1075
1076 assert.equal(dispatch.getCalls().length, 1);
1077 sinon.assert.calledWith(dispatch, {
1078 type: 'FETCH_FEDERATED_REFERENCES_START',
1079 });
1080 assert.isUndefined(result);
1081 });
1082
1083 it('fetches from Buganizer API', async () => {
1084 const dispatch = sinon.stub();
1085 const testIssue = {
1086 danglingBlockingRefs: [
1087 {extIdentifier: 'b/123456'},
1088 ],
1089 danglingBlockedOnRefs: [
1090 {extIdentifier: 'b/654321'},
1091 ],
1092 mergedIntoIssueRef: {
1093 extIdentifier: 'b/987654',
1094 },
1095 };
1096 const action = issueV0.fetchFederatedReferences(testIssue);
1097 await action(dispatch);
1098
1099 sinon.assert.calledWith(dispatch, {
1100 type: 'FETCH_FEDERATED_REFERENCES_START',
1101 });
1102 sinon.assert.calledWith(dispatch, {
1103 type: 'GAPI_LOGIN_SUCCESS',
1104 email: 'rutabaga@google.com',
1105 });
1106 sinon.assert.calledWith(dispatch, {
1107 type: 'FETCH_FEDERATED_REFERENCES_SUCCESS',
1108 fedRefIssueRefs: [
1109 {
1110 extIdentifier: 'b/123456',
1111 statusRef: {meansOpen: false},
1112 summary: 'Rutabaga title',
1113 },
1114 {
1115 extIdentifier: 'b/654321',
1116 statusRef: {meansOpen: false},
1117 summary: 'Rutabaga title',
1118 },
1119 {
1120 extIdentifier: 'b/987654',
1121 statusRef: {meansOpen: false},
1122 summary: 'Rutabaga title',
1123 },
1124 ],
1125 });
1126 });
1127 });
1128
1129 describe('fetchRelatedIssues', () => {
1130 it('calls fetchFederatedReferences for mergedinto', async () => {
1131 const dispatch = sinon.stub();
1132 prpcCall.returns(Promise.resolve({openRefs: [], closedRefs: []}));
1133 const testIssue = {
1134 mergedIntoIssueRef: {
1135 extIdentifier: 'b/987654',
1136 },
1137 };
1138 const action = issueV0.fetchRelatedIssues(testIssue);
1139 await action(dispatch);
1140
1141 // Important: mergedinto fedref is not passed to ListReferencedIssues.
1142 const expectedMessage = {issueRefs: []};
1143 sinon.assert.calledWith(prpcClient.call, 'monorail.Issues',
1144 'ListReferencedIssues', expectedMessage);
1145
1146 sinon.assert.calledWith(dispatch, {
1147 type: 'FETCH_RELATED_ISSUES_START',
1148 });
1149 // No mergedInto refs returned, they're handled by
1150 // fetchFederatedReferences.
1151 sinon.assert.calledWith(dispatch, {
1152 type: 'FETCH_RELATED_ISSUES_SUCCESS',
1153 relatedIssues: {},
1154 });
1155 });
1156 });
1157 });
1158 });
1159
1160 describe('starring issues', () => {
1161 describe('reducers', () => {
1162 it('FETCH_IS_STARRED_SUCCESS updates the starredIssues object', () => {
1163 const state = {};
1164 const newState = issueV0.starredIssuesReducer(state,
1165 {
1166 type: issueV0.FETCH_IS_STARRED_SUCCESS,
1167 starred: false,
1168 issueRef: {
1169 projectName: 'proj',
1170 localId: 1,
1171 },
1172 },
1173 );
1174 assert.deepEqual(newState, {'proj:1': false});
1175 });
1176
1177 it('FETCH_ISSUES_STARRED_SUCCESS updates the starredIssues object',
1178 () => {
1179 const state = {};
1180 const starredIssueRefs = [{projectName: 'proj', localId: 1},
1181 {projectName: 'proj', localId: 2}];
1182 const newState = issueV0.starredIssuesReducer(state,
1183 {type: issueV0.FETCH_ISSUES_STARRED_SUCCESS, starredIssueRefs},
1184 );
1185 assert.deepEqual(newState, {'proj:1': true, 'proj:2': true});
1186 });
1187
1188 it('FETCH_ISSUES_STARRED_SUCCESS does not time out with 10,000 stars',
1189 () => {
1190 const state = {};
1191 const starredIssueRefs = [];
1192 const expected = {};
1193 for (let i = 1; i <= 10000; i++) {
1194 starredIssueRefs.push({projectName: 'proj', localId: i});
1195 expected[`proj:${i}`] = true;
1196 }
1197 const newState = issueV0.starredIssuesReducer(state,
1198 {type: issueV0.FETCH_ISSUES_STARRED_SUCCESS, starredIssueRefs},
1199 );
1200 assert.deepEqual(newState, expected);
1201 });
1202
1203 it('STAR_SUCCESS updates the starredIssues object', () => {
1204 const state = {'proj:1': true, 'proj:2': false};
1205 const newState = issueV0.starredIssuesReducer(state,
1206 {
1207 type: issueV0.STAR_SUCCESS,
1208 starred: true,
1209 issueRef: {projectName: 'proj', localId: 2},
1210 });
1211 assert.deepEqual(newState, {'proj:1': true, 'proj:2': true});
1212 });
1213 });
1214
1215 describe('selectors', () => {
1216 describe('issue', () => {
1217 const selector = issueV0.issue(wrapIssue(example.ISSUE));
1218 assert.deepEqual(selector(example.NAME), example.ISSUE);
1219 });
1220
1221 describe('issueForRefString', () => {
1222 const noIssues = issueV0.issueForRefString(wrapIssue({}));
1223 const withIssue = issueV0.issueForRefString(wrapIssue({
1224 projectName: 'test',
1225 localId: 1,
1226 summary: 'hello world',
1227 }));
1228
1229 it('returns issue ref when no issue data', () => {
1230 assert.deepEqual(noIssues('1', 'chromium'), {
1231 localId: 1,
1232 projectName: 'chromium',
1233 });
1234
1235 assert.deepEqual(noIssues('chromium:2', 'ignore'), {
1236 localId: 2,
1237 projectName: 'chromium',
1238 });
1239
1240 assert.deepEqual(noIssues('other:3'), {
1241 localId: 3,
1242 projectName: 'other',
1243 });
1244
1245 assert.deepEqual(withIssue('other:3'), {
1246 localId: 3,
1247 projectName: 'other',
1248 });
1249 });
1250
1251 it('returns full issue data when available', () => {
1252 assert.deepEqual(withIssue('1', 'test'), {
1253 projectName: 'test',
1254 localId: 1,
1255 summary: 'hello world',
1256 });
1257
1258 assert.deepEqual(withIssue('test:1', 'other'), {
1259 projectName: 'test',
1260 localId: 1,
1261 summary: 'hello world',
1262 });
1263
1264 assert.deepEqual(withIssue('test:1'), {
1265 projectName: 'test',
1266 localId: 1,
1267 summary: 'hello world',
1268 });
1269 });
1270 });
1271
1272 it('starredIssues', () => {
1273 const state = {issue:
1274 {starredIssues: {'proj:1': true, 'proj:2': false}}};
1275 assert.deepEqual(issueV0.starredIssues(state), new Set(['proj:1']));
1276 });
1277
1278 it('starringIssues', () => {
1279 const state = {issue: {
1280 requests: {
1281 starringIssues: {
1282 'proj:1': {requesting: true},
1283 'proj:2': {requestin: false, error: 'unknown error'},
1284 },
1285 },
1286 }};
1287 assert.deepEqual(issueV0.starringIssues(state), new Map([
1288 ['proj:1', {requesting: true}],
1289 ['proj:2', {requestin: false, error: 'unknown error'}],
1290 ]));
1291 });
1292 });
1293
1294 describe('action creators', () => {
1295 beforeEach(() => {
1296 prpcCall = sinon.stub(prpcClient, 'call');
1297
1298 dispatch = sinon.stub();
1299 });
1300
1301 afterEach(() => {
1302 prpcCall.restore();
1303 });
1304
1305 it('fetching if an issue is starred', async () => {
1306 const issueRef = {projectName: 'proj', localId: 1};
1307 const action = issueV0.fetchIsStarred(issueRef);
1308
1309 prpcCall.returns(Promise.resolve({isStarred: true}));
1310
1311 await action(dispatch);
1312
1313 sinon.assert.calledWith(dispatch,
1314 {type: issueV0.FETCH_IS_STARRED_START});
1315
1316 sinon.assert.calledWith(
1317 prpcClient.call, 'monorail.Issues',
1318 'IsIssueStarred', {issueRef},
1319 );
1320
1321 sinon.assert.calledWith(dispatch, {
1322 type: issueV0.FETCH_IS_STARRED_SUCCESS,
1323 starred: true,
1324 issueRef,
1325 });
1326 });
1327
1328 it('fetching starred issues', async () => {
1329 const returnedIssueRef = {projectName: 'proj', localId: 1};
1330 const starredIssueRefs = [returnedIssueRef];
1331 const action = issueV0.fetchStarredIssues();
1332
1333 prpcCall.returns(Promise.resolve({starredIssueRefs}));
1334
1335 await action(dispatch);
1336
1337 sinon.assert.calledWith(dispatch, {type: 'FETCH_ISSUES_STARRED_START'});
1338
1339 sinon.assert.calledWith(
1340 prpcClient.call, 'monorail.Issues',
1341 'ListStarredIssues', {},
1342 );
1343
1344 sinon.assert.calledWith(dispatch, {
1345 type: issueV0.FETCH_ISSUES_STARRED_SUCCESS,
1346 starredIssueRefs,
1347 });
1348 });
1349
1350 it('star', async () => {
1351 const testIssue = {projectName: 'proj', localId: 1, starCount: 1};
1352 const issueRef = issueToIssueRef(testIssue);
1353 const action = issueV0.star(issueRef, false);
1354
1355 prpcCall.returns(Promise.resolve(testIssue));
1356
1357 await action(dispatch);
1358
1359 sinon.assert.calledWith(dispatch, {
1360 type: issueV0.STAR_START,
1361 requestKey: 'proj:1',
1362 });
1363
1364 sinon.assert.calledWith(
1365 prpcClient.call,
1366 'monorail.Issues', 'StarIssue',
1367 {issueRef, starred: false},
1368 );
1369
1370 sinon.assert.calledWith(dispatch, {
1371 type: issueV0.STAR_SUCCESS,
1372 starCount: 1,
1373 issueRef,
1374 starred: false,
1375 requestKey: 'proj:1',
1376 });
1377 });
1378 });
1379 });
1380});
1381
1382/**
1383 * Return an initial Redux state with a given viewed
1384 * @param {Issue=} viewedIssue The viewed issue.
1385 * @param {Object=} otherValues Any other state values that need
1386 * to be initialized.
1387 * @return {Object}
1388 */
1389function wrapIssue(viewedIssue, otherValues = {}) {
1390 if (!viewedIssue) {
1391 return {
1392 issue: {
1393 issuesByRefString: {},
1394 ...otherValues,
1395 },
1396 };
1397 }
1398
1399 const ref = issueRefToString(viewedIssue);
1400 return {
1401 issue: {
1402 viewedIssueRef: ref,
1403 issuesByRefString: {
1404 [ref]: {...viewedIssue},
1405 },
1406 ...otherValues,
1407 },
1408 };
1409}