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