blob: 33b63c1b4cbd5d70a4a674a085a1df4ab03396fa [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
Adrià Vilanova Martínezde942802022-07-15 14:06:55 +0200302 it('migratedId', () => {
303 assert.equal(issueV0.migratedId(wrapIssue()), '');
304 assert.equal(issueV0.migratedId(wrapIssue({labelRefs: []})), '');
305
306 assert.equal(issueV0.migratedId(wrapIssue({labelRefs: [
307 {label: 'IgnoreThis'},
308 {label: 'IgnoreThis2'},
309 ]})), '');
310
311 assert.equal(issueV0.migratedId(wrapIssue({labelRefs: [
312 {label: 'IgnoreThis'},
313 {label: 'IgnoreThis2'},
314 {label: 'migrated-to-b-6789'},
315 ]})), '6789');
316
317 assert.equal(issueV0.migratedId(wrapIssue({labelRefs: [
318 {label: 'migrated-to-b-1234'},
319 ]})), '1234');
320
321 // We assume there's only one migrated-to-b-* label.
322 assert.equal(issueV0.migratedId(wrapIssue({labelRefs: [
323 {label: 'migrated-to-b-1234'},
324 {label: 'migrated-to-b-6789'},
325 ]})), '1234');
326 });
327
328
Copybara854996b2021-09-07 19:36:02 +0000329 it('isOpen', () => {
330 assert.isFalse(issueV0.isOpen(wrapIssue()));
331 assert.isTrue(issueV0.isOpen(wrapIssue({statusRef: {meansOpen: true}})));
332 assert.isFalse(issueV0.isOpen(wrapIssue({statusRef: {meansOpen: false}})));
333 });
334
335 it('issueListPhaseNames', () => {
336 const stateWithEmptyIssueList = {issue: {
337 issueList: [],
338 }};
339 assert.deepEqual(issueV0.issueListPhaseNames(stateWithEmptyIssueList), []);
340 const stateWithIssueList = {issue: {
341 issuesByRefString: {
342 '1': {localId: 1, phases: [{phaseRef: {phaseName: 'chicken-phase'}}]},
343 '2': {localId: 2, phases: [
344 {phaseRef: {phaseName: 'chicken-Phase'}},
345 {phaseRef: {phaseName: 'cow-phase'}}],
346 },
347 '3': {localId: 3, phases: [
348 {phaseRef: {phaseName: 'cow-Phase'}},
349 {phaseRef: {phaseName: 'DOG-phase'}}],
350 },
351 '4': {localId: 4, phases: [
352 {phaseRef: {phaseName: 'dog-phase'}},
353 ]},
354 },
355 issueList: {
356 issueRefs: ['1', '2', '3', '4'],
357 }}};
358 assert.deepEqual(issueV0.issueListPhaseNames(stateWithIssueList),
359 ['chicken-phase', 'cow-phase', 'dog-phase']);
360 });
361
362 describe('blockingIssues', () => {
363 const relatedIssues = {
364 ['proj:1']: {
365 localId: 1,
366 projectName: 'proj',
367 labelRefs: [{label: 'label'}],
368 },
369 ['proj:3']: {
370 localId: 3,
371 projectName: 'proj',
372 labelRefs: [],
373 },
374 ['chromium:332']: {
375 localId: 332,
376 projectName: 'chromium',
377 labelRefs: [],
378 },
379 };
380
381 it('returns references when no issue data', () => {
382 const stateNoReferences = wrapIssue(
383 {
384 projectName: 'project',
385 localId: 123,
386 blockingIssueRefs: [{localId: 1, projectName: 'proj'}],
387 },
388 {relatedIssues: {}},
389 );
390 assert.deepEqual(issueV0.blockingIssues(stateNoReferences),
391 [{localId: 1, projectName: 'proj'}],
392 );
393 });
394
395 it('returns empty when no blocking issues', () => {
396 const stateNoIssues = wrapIssue(
397 {
398 projectName: 'project',
399 localId: 123,
400 blockingIssueRefs: [],
401 },
402 {relatedIssues},
403 );
404 assert.deepEqual(issueV0.blockingIssues(stateNoIssues), []);
405 });
406
407 it('returns full issues when deferenced data present', () => {
408 const stateIssuesWithReferences = wrapIssue(
409 {
410 projectName: 'project',
411 localId: 123,
412 blockingIssueRefs: [
413 {localId: 1, projectName: 'proj'},
414 {localId: 332, projectName: 'chromium'},
415 ],
416 },
417 {relatedIssues},
418 );
419 assert.deepEqual(issueV0.blockingIssues(stateIssuesWithReferences),
420 [
421 {localId: 1, projectName: 'proj', labelRefs: [{label: 'label'}]},
422 {localId: 332, projectName: 'chromium', labelRefs: []},
423 ]);
424 });
425
426 it('returns federated references', () => {
427 const stateIssuesWithFederatedReferences = wrapIssue(
428 {
429 projectName: 'project',
430 localId: 123,
431 blockingIssueRefs: [
432 {localId: 1, projectName: 'proj'},
433 {extIdentifier: 'b/1234'},
434 ],
435 },
436 {relatedIssues},
437 );
438 assert.deepEqual(
439 issueV0.blockingIssues(stateIssuesWithFederatedReferences), [
440 {localId: 1, projectName: 'proj', labelRefs: [{label: 'label'}]},
441 {extIdentifier: 'b/1234'},
442 ]);
443 });
444 });
445
446 describe('blockedOnIssues', () => {
447 const relatedIssues = {
448 ['proj:1']: {
449 localId: 1,
450 projectName: 'proj',
451 labelRefs: [{label: 'label'}],
452 },
453 ['proj:3']: {
454 localId: 3,
455 projectName: 'proj',
456 labelRefs: [],
457 },
458 ['chromium:332']: {
459 localId: 332,
460 projectName: 'chromium',
461 labelRefs: [],
462 },
463 };
464
465 it('returns references when no issue data', () => {
466 const stateNoReferences = wrapIssue(
467 {
468 projectName: 'project',
469 localId: 123,
470 blockedOnIssueRefs: [{localId: 1, projectName: 'proj'}],
471 },
472 {relatedIssues: {}},
473 );
474 assert.deepEqual(issueV0.blockedOnIssues(stateNoReferences),
475 [{localId: 1, projectName: 'proj'}],
476 );
477 });
478
479 it('returns empty when no blocking issues', () => {
480 const stateNoIssues = wrapIssue(
481 {
482 projectName: 'project',
483 localId: 123,
484 blockedOnIssueRefs: [],
485 },
486 {relatedIssues},
487 );
488 assert.deepEqual(issueV0.blockedOnIssues(stateNoIssues), []);
489 });
490
491 it('returns full issues when deferenced data present', () => {
492 const stateIssuesWithReferences = wrapIssue(
493 {
494 projectName: 'project',
495 localId: 123,
496 blockedOnIssueRefs: [
497 {localId: 1, projectName: 'proj'},
498 {localId: 332, projectName: 'chromium'},
499 ],
500 },
501 {relatedIssues},
502 );
503 assert.deepEqual(issueV0.blockedOnIssues(stateIssuesWithReferences),
504 [
505 {localId: 1, projectName: 'proj', labelRefs: [{label: 'label'}]},
506 {localId: 332, projectName: 'chromium', labelRefs: []},
507 ]);
508 });
509
510 it('returns federated references', () => {
511 const stateIssuesWithFederatedReferences = wrapIssue(
512 {
513 projectName: 'project',
514 localId: 123,
515 blockedOnIssueRefs: [
516 {localId: 1, projectName: 'proj'},
517 {extIdentifier: 'b/1234'},
518 ],
519 },
520 {relatedIssues},
521 );
522 assert.deepEqual(
523 issueV0.blockedOnIssues(stateIssuesWithFederatedReferences),
524 [
525 {localId: 1, projectName: 'proj', labelRefs: [{label: 'label'}]},
526 {extIdentifier: 'b/1234'},
527 ]);
528 });
529 });
530
531 describe('sortedBlockedOn', () => {
532 const relatedIssues = {
533 ['proj:1']: {
534 localId: 1,
535 projectName: 'proj',
536 statusRef: {meansOpen: true},
537 },
538 ['proj:3']: {
539 localId: 3,
540 projectName: 'proj',
541 statusRef: {meansOpen: false},
542 },
543 ['proj:4']: {
544 localId: 4,
545 projectName: 'proj',
546 statusRef: {meansOpen: false},
547 },
548 ['proj:5']: {
549 localId: 5,
550 projectName: 'proj',
551 statusRef: {meansOpen: false},
552 },
553 ['chromium:332']: {
554 localId: 332,
555 projectName: 'chromium',
556 statusRef: {meansOpen: true},
557 },
558 };
559
560 it('does not sort references when no issue data', () => {
561 const stateNoReferences = wrapIssue(
562 {
563 projectName: 'project',
564 localId: 123,
565 blockedOnIssueRefs: [
566 {localId: 3, projectName: 'proj'},
567 {localId: 1, projectName: 'proj'},
568 ],
569 },
570 {relatedIssues: {}},
571 );
572 assert.deepEqual(issueV0.sortedBlockedOn(stateNoReferences), [
573 {localId: 3, projectName: 'proj'},
574 {localId: 1, projectName: 'proj'},
575 ]);
576 });
577
578 it('sorts open issues first when issue data available', () => {
579 const stateReferences = wrapIssue(
580 {
581 projectName: 'project',
582 localId: 123,
583 blockedOnIssueRefs: [
584 {localId: 3, projectName: 'proj'},
585 {localId: 1, projectName: 'proj'},
586 ],
587 },
588 {relatedIssues},
589 );
590 assert.deepEqual(issueV0.sortedBlockedOn(stateReferences), [
591 {localId: 1, projectName: 'proj', statusRef: {meansOpen: true}},
592 {localId: 3, projectName: 'proj', statusRef: {meansOpen: false}},
593 ]);
594 });
595
596 it('preserves original order on ties', () => {
597 const statePreservesArrayOrder = wrapIssue(
598 {
599 projectName: 'project',
600 localId: 123,
601 blockedOnIssueRefs: [
602 {localId: 5, projectName: 'proj'}, // Closed
603 {localId: 1, projectName: 'proj'}, // Open
604 {localId: 4, projectName: 'proj'}, // Closed
605 {localId: 3, projectName: 'proj'}, // Closed
606 {localId: 332, projectName: 'chromium'}, // Open
607 ],
608 },
609 {relatedIssues},
610 );
611 assert.deepEqual(issueV0.sortedBlockedOn(statePreservesArrayOrder),
612 [
613 {localId: 1, projectName: 'proj', statusRef: {meansOpen: true}},
614 {localId: 332, projectName: 'chromium',
615 statusRef: {meansOpen: true}},
616 {localId: 5, projectName: 'proj', statusRef: {meansOpen: false}},
617 {localId: 4, projectName: 'proj', statusRef: {meansOpen: false}},
618 {localId: 3, projectName: 'proj', statusRef: {meansOpen: false}},
619 ],
620 );
621 });
622 });
623
624 describe('mergedInto', () => {
625 it('empty', () => {
626 assert.deepEqual(issueV0.mergedInto(wrapIssue()), {});
627 });
628
629 it('gets mergedInto ref for viewed issue', () => {
630 const state = issueV0.mergedInto(wrapIssue({
631 projectName: 'project',
632 localId: 123,
633 mergedIntoIssueRef: {localId: 22, projectName: 'proj'},
634 }));
635 assert.deepEqual(state, {
636 localId: 22,
637 projectName: 'proj',
638 });
639 });
640
641 it('gets full mergedInto issue data when it exists in the store', () => {
642 const state = wrapIssue(
643 {
644 projectName: 'project',
645 localId: 123,
646 mergedIntoIssueRef: {localId: 22, projectName: 'proj'},
647 }, {
648 relatedIssues: {
649 ['proj:22']: {localId: 22, projectName: 'proj', summary: 'test'},
650 },
651 });
652 assert.deepEqual(issueV0.mergedInto(state), {
653 localId: 22,
654 projectName: 'proj',
655 summary: 'test',
656 });
657 });
658 });
659
660 it('fieldValueMap', () => {
661 assert.deepEqual(issueV0.fieldValueMap(wrapIssue()), new Map());
662 assert.deepEqual(issueV0.fieldValueMap(wrapIssue({
663 fieldValues: [],
664 })), new Map());
665 assert.deepEqual(issueV0.fieldValueMap(wrapIssue({
666 fieldValues: [
667 {fieldRef: {fieldName: 'hello'}, value: 'v3'},
668 {fieldRef: {fieldName: 'hello'}, value: 'v2'},
669 {fieldRef: {fieldName: 'world'}, value: 'v3'},
670 ],
671 })), new Map([
672 ['hello', ['v3', 'v2']],
673 ['world', ['v3']],
674 ]));
675 });
676
677 it('fieldDefs filters fields by applicable type', () => {
678 assert.deepEqual(issueV0.fieldDefs({
679 projectV0: {},
680 ...wrapIssue(),
681 }), []);
682
683 assert.deepEqual(issueV0.fieldDefs({
684 projectV0: {
685 name: 'chromium',
686 configs: {
687 chromium: {
688 fieldDefs: [
689 {fieldRef: {fieldName: 'intyInt', type: fieldTypes.INT_TYPE}},
690 {fieldRef: {fieldName: 'enum', type: fieldTypes.ENUM_TYPE}},
691 {
692 fieldRef:
693 {fieldName: 'nonApplicable', type: fieldTypes.STR_TYPE},
694 applicableType: 'None',
695 },
696 {fieldRef: {fieldName: 'defectsOnly', type: fieldTypes.STR_TYPE},
697 applicableType: 'Defect'},
698 ],
699 },
700 },
701 },
702 ...wrapIssue({
703 fieldValues: [
704 {fieldRef: {fieldName: 'Type'}, value: 'Defect'},
705 ],
706 }),
707 }), [
708 {fieldRef: {fieldName: 'intyInt', type: fieldTypes.INT_TYPE}},
709 {fieldRef: {fieldName: 'enum', type: fieldTypes.ENUM_TYPE}},
710 {fieldRef: {fieldName: 'defectsOnly', type: fieldTypes.STR_TYPE},
711 applicableType: 'Defect'},
712 ]);
713 });
714
715 it('fieldDefs skips approval fields for all issues', () => {
716 assert.deepEqual(issueV0.fieldDefs({
717 projectV0: {
718 name: 'chromium',
719 configs: {
720 chromium: {
721 fieldDefs: [
722 {fieldRef: {fieldName: 'test', type: fieldTypes.INT_TYPE}},
723 {fieldRef:
724 {fieldName: 'ignoreMe', type: fieldTypes.APPROVAL_TYPE}},
725 {fieldRef:
726 {fieldName: 'LookAway', approvalName: 'ThisIsAnApproval'}},
727 {fieldRef: {fieldName: 'phaseField'}, isPhaseField: true},
728 ],
729 },
730 },
731 },
732 ...wrapIssue(),
733 }), [
734 {fieldRef: {fieldName: 'test', type: fieldTypes.INT_TYPE}},
735 ]);
736 });
737
738 it('fieldDefs includes non applicable fields when values defined', () => {
739 assert.deepEqual(issueV0.fieldDefs({
740 projectV0: {
741 name: 'chromium',
742 configs: {
743 chromium: {
744 fieldDefs: [
745 {
746 fieldRef:
747 {fieldName: 'nonApplicable', type: fieldTypes.STR_TYPE},
748 applicableType: 'None',
749 },
750 ],
751 },
752 },
753 },
754 ...wrapIssue({
755 fieldValues: [
756 {fieldRef: {fieldName: 'nonApplicable'}, value: 'v3'},
757 ],
758 }),
759 }), [
760 {fieldRef: {fieldName: 'nonApplicable', type: fieldTypes.STR_TYPE},
761 applicableType: 'None'},
762 ]);
763 });
764
765 describe('action creators', () => {
766 beforeEach(() => {
767 prpcCall = sinon.stub(prpcClient, 'call');
768 });
769
770 afterEach(() => {
771 prpcCall.restore();
772 });
773
774 it('viewIssue creates action with issueRef', () => {
775 assert.deepEqual(
776 issueV0.viewIssue({projectName: 'proj', localId: 123}),
777 {
778 type: issueV0.VIEW_ISSUE,
779 issueRef: {projectName: 'proj', localId: 123},
780 },
781 );
782 });
783
784
785 describe('updateApproval', async () => {
786 const APPROVAL = {
787 fieldRef: {fieldName: 'Privacy', type: 'APPROVAL_TYPE'},
788 approverRefs: [{userId: 1234, displayName: 'test@example.com'}],
789 status: 'APPROVED',
790 };
791
792 it('approval update success', async () => {
793 const dispatch = sinon.stub();
794
795 prpcCall.returns({approval: APPROVAL});
796
797 const action = issueV0.updateApproval({
798 issueRef: {projectName: 'chromium', localId: 1234},
799 fieldRef: {fieldName: 'Privacy', type: 'APPROVAL_TYPE'},
800 approvalDelta: {status: 'APPROVED'},
801 sendEmail: true,
802 });
803
804 await action(dispatch);
805
806 sinon.assert.calledOnce(prpcCall);
807
808 sinon.assert.calledWith(prpcCall, 'monorail.Issues',
809 'UpdateApproval', {
810 issueRef: {projectName: 'chromium', localId: 1234},
811 fieldRef: {fieldName: 'Privacy', type: 'APPROVAL_TYPE'},
812 approvalDelta: {status: 'APPROVED'},
813 sendEmail: true,
814 });
815
816 sinon.assert.calledWith(dispatch, {type: 'UPDATE_APPROVAL_START'});
817 sinon.assert.calledWith(dispatch, {
818 type: 'UPDATE_APPROVAL_SUCCESS',
819 approval: APPROVAL,
820 issueRef: {projectName: 'chromium', localId: 1234},
821 });
822 });
823
824 it('approval survey update success', async () => {
825 const dispatch = sinon.stub();
826
827 prpcCall.returns({approval: APPROVAL});
828
829 const action = issueV0.updateApproval({
830 issueRef: {projectName: 'chromium', localId: 1234},
831 fieldRef: {fieldName: 'Privacy', type: 'APPROVAL_TYPE'},
832 commentContent: 'new survey',
833 sendEmail: false,
834 isDescription: true,
835 });
836
837 await action(dispatch);
838
839 sinon.assert.calledOnce(prpcCall);
840
841 sinon.assert.calledWith(prpcCall, 'monorail.Issues',
842 'UpdateApproval', {
843 issueRef: {projectName: 'chromium', localId: 1234},
844 fieldRef: {fieldName: 'Privacy', type: 'APPROVAL_TYPE'},
845 commentContent: 'new survey',
846 isDescription: true,
847 });
848
849 sinon.assert.calledWith(dispatch, {type: 'UPDATE_APPROVAL_START'});
850 sinon.assert.calledWith(dispatch, {
851 type: 'UPDATE_APPROVAL_SUCCESS',
852 approval: APPROVAL,
853 issueRef: {projectName: 'chromium', localId: 1234},
854 });
855 });
856
857 it('attachment upload success', async () => {
858 const dispatch = sinon.stub();
859
860 prpcCall.returns({approval: APPROVAL});
861
862 const action = issueV0.updateApproval({
863 issueRef: {projectName: 'chromium', localId: 1234},
864 fieldRef: {fieldName: 'Privacy', type: 'APPROVAL_TYPE'},
865 uploads: '78f17a020cbf39e90e344a842cd19911',
866 });
867
868 await action(dispatch);
869
870 sinon.assert.calledOnce(prpcCall);
871
872 sinon.assert.calledWith(prpcCall, 'monorail.Issues',
873 'UpdateApproval', {
874 issueRef: {projectName: 'chromium', localId: 1234},
875 fieldRef: {fieldName: 'Privacy', type: 'APPROVAL_TYPE'},
876 uploads: '78f17a020cbf39e90e344a842cd19911',
877 });
878
879 sinon.assert.calledWith(dispatch, {type: 'UPDATE_APPROVAL_START'});
880 sinon.assert.calledWith(dispatch, {
881 type: 'UPDATE_APPROVAL_SUCCESS',
882 approval: APPROVAL,
883 issueRef: {projectName: 'chromium', localId: 1234},
884 });
885 });
886 });
887
888 describe('fetchIssues', () => {
889 it('success', async () => {
890 const response = {
891 openRefs: [example.ISSUE],
892 closedRefs: [example.ISSUE_OTHER_PROJECT],
893 };
894 prpcClient.call.returns(Promise.resolve(response));
895 const dispatch = sinon.stub();
896
897 await issueV0.fetchIssues([example.ISSUE_REF])(dispatch);
898
899 sinon.assert.calledWith(dispatch, {type: issueV0.FETCH_ISSUES_START});
900
901 const args = {issueRefs: [example.ISSUE_REF]};
902 sinon.assert.calledWith(
903 prpcClient.call, 'monorail.Issues', 'ListReferencedIssues', args);
904
905 const action = {
906 type: issueV0.FETCH_ISSUES_SUCCESS,
907 issues: [example.ISSUE, example.ISSUE_OTHER_PROJECT],
908 };
909 sinon.assert.calledWith(dispatch, action);
910 });
911
912 it('failure', async () => {
913 prpcClient.call.throws();
914 const dispatch = sinon.stub();
915
916 await issueV0.fetchIssues([example.ISSUE_REF])(dispatch);
917
918 const action = {
919 type: issueV0.FETCH_ISSUES_FAILURE,
920 error: sinon.match.any,
921 };
922 sinon.assert.calledWith(dispatch, action);
923 });
924 });
925
926 it('fetchIssueList calls ListIssues', async () => {
927 prpcCall.callsFake(() => {
928 return {
929 issues: [{localId: 1}, {localId: 2}, {localId: 3}],
930 totalResults: 6,
931 };
932 });
933
934 store.dispatch(issueV0.fetchIssueList('chromium',
935 {q: 'owner:me', can: '4'}));
936
937 sinon.assert.calledWith(prpcCall, 'monorail.Issues', 'ListIssues', {
938 query: 'owner:me',
939 cannedQuery: 4,
940 projectNames: ['chromium'],
941 pagination: {},
942 groupBySpec: undefined,
943 sortSpec: undefined,
944 });
945 });
946
947 it('fetchIssueList does not set can when can is NaN', async () => {
948 prpcCall.callsFake(() => ({}));
949
950 store.dispatch(issueV0.fetchIssueList('chromium', {q: 'owner:me',
951 can: 'four-leaf-clover'}));
952
953 sinon.assert.calledWith(prpcCall, 'monorail.Issues', 'ListIssues', {
954 query: 'owner:me',
955 cannedQuery: undefined,
956 projectNames: ['chromium'],
957 pagination: {},
958 groupBySpec: undefined,
959 sortSpec: undefined,
960 });
961 });
962
963 it('fetchIssueList makes several calls to ListIssues', async () => {
964 prpcCall.callsFake(() => {
965 return {
966 issues: [{localId: 1}, {localId: 2}, {localId: 3}],
967 totalResults: 6,
968 };
969 });
970
971 const dispatch = sinon.stub();
972 const action = issueV0.fetchIssueList('chromium',
973 {maxItems: 3, maxCalls: 2});
974 await action(dispatch);
975
976 sinon.assert.calledTwice(prpcCall);
977 sinon.assert.calledWith(dispatch, {
978 type: 'FETCH_ISSUE_LIST_UPDATE',
979 issues:
980 [{localId: 1}, {localId: 2}, {localId: 3},
981 {localId: 1}, {localId: 2}, {localId: 3}],
982 progress: 1,
983 totalResults: 6,
984 });
985 sinon.assert.calledWith(dispatch, {type: 'FETCH_ISSUE_LIST_SUCCESS'});
986 });
987
988 it('fetchIssueList orders issues correctly', async () => {
989 prpcCall.onFirstCall().returns({issues: [{localId: 1}], totalResults: 6});
990 prpcCall.onSecondCall().returns({
991 issues: [{localId: 2}],
992 totalResults: 6});
993 prpcCall.onThirdCall().returns({issues: [{localId: 3}], totalResults: 6});
994
995 const dispatch = sinon.stub();
996 const action = issueV0.fetchIssueList('chromium',
997 {maxItems: 1, maxCalls: 3});
998 await action(dispatch);
999
1000 sinon.assert.calledWith(dispatch, {
1001 type: 'FETCH_ISSUE_LIST_UPDATE',
1002 issues: [{localId: 1}, {localId: 2}, {localId: 3}],
1003 progress: 1,
1004 totalResults: 6,
1005 });
1006 sinon.assert.calledWith(dispatch, {type: 'FETCH_ISSUE_LIST_SUCCESS'});
1007 });
1008
1009 it('returns progress of 1 when no totalIssues', async () => {
1010 prpcCall.onFirstCall().returns({issues: [], totalResults: 0});
1011
1012 const dispatch = sinon.stub();
1013 const action = issueV0.fetchIssueList('chromium',
1014 {maxItems: 1, maxCalls: 1});
1015 await action(dispatch);
1016
1017 sinon.assert.calledWith(dispatch, {
1018 type: 'FETCH_ISSUE_LIST_UPDATE',
1019 issues: [],
1020 progress: 1,
1021 totalResults: 0,
1022 });
1023 sinon.assert.calledWith(dispatch, {type: 'FETCH_ISSUE_LIST_SUCCESS'});
1024 });
1025
1026 it('returns progress of 1 when totalIssues undefined', async () => {
1027 prpcCall.onFirstCall().returns({issues: []});
1028
1029 const dispatch = sinon.stub();
1030 const action = issueV0.fetchIssueList('chromium',
1031 {maxItems: 1, maxCalls: 1});
1032 await action(dispatch);
1033
1034 sinon.assert.calledWith(dispatch, {
1035 type: 'FETCH_ISSUE_LIST_UPDATE',
1036 issues: [],
1037 progress: 1,
1038 });
1039 sinon.assert.calledWith(dispatch, {type: 'FETCH_ISSUE_LIST_SUCCESS'});
1040 });
1041
1042 // TODO(kweng@) remove once crbug.com/monorail/6641 is fixed
1043 it('has expected default for empty response', async () => {
1044 prpcCall.onFirstCall().returns({});
1045
1046 const dispatch = sinon.stub();
1047 const action = issueV0.fetchIssueList('chromium',
1048 {maxItems: 1, maxCalls: 1});
1049 await action(dispatch);
1050
1051 sinon.assert.calledWith(dispatch, {
1052 type: 'FETCH_ISSUE_LIST_UPDATE',
1053 issues: [],
1054 progress: 1,
1055 totalResults: 0,
1056 });
1057 sinon.assert.calledWith(dispatch, {type: 'FETCH_ISSUE_LIST_SUCCESS'});
1058 });
1059
1060 describe('federated references', () => {
1061 beforeEach(() => {
1062 // Preload signinImpl with a fake for testing.
1063 getSigninInstance({
1064 init: sinon.stub(),
1065 getUserProfileAsync: () => (
1066 Promise.resolve({
1067 getEmail: sinon.stub().returns('rutabaga@google.com'),
1068 })
1069 ),
1070 });
1071 window.CS_env = {gapi_client_id: 'rutabaga'};
1072 const getStub = sinon.stub().returns({
1073 execute: (cb) => cb(response),
1074 });
1075 const response = {
1076 result: {
1077 resolvedTime: 12345,
1078 issueState: {
1079 title: 'Rutabaga title',
1080 },
1081 },
1082 };
1083 window.gapi = {
1084 client: {
1085 load: (_url, _version, cb) => cb(),
1086 corp_issuetracker: {issues: {get: getStub}},
1087 },
1088 };
1089 });
1090
1091 afterEach(() => {
1092 delete window.CS_env;
1093 delete window.gapi;
1094 });
1095
1096 describe('fetchFederatedReferences', () => {
1097 it('returns an empty map if no fedrefs found', async () => {
1098 const dispatch = sinon.stub();
1099 const testIssue = {};
1100 const action = issueV0.fetchFederatedReferences(testIssue);
1101 const result = await action(dispatch);
1102
1103 assert.equal(dispatch.getCalls().length, 1);
1104 sinon.assert.calledWith(dispatch, {
1105 type: 'FETCH_FEDERATED_REFERENCES_START',
1106 });
1107 assert.isUndefined(result);
1108 });
1109
1110 it('fetches from Buganizer API', async () => {
1111 const dispatch = sinon.stub();
1112 const testIssue = {
1113 danglingBlockingRefs: [
1114 {extIdentifier: 'b/123456'},
1115 ],
1116 danglingBlockedOnRefs: [
1117 {extIdentifier: 'b/654321'},
1118 ],
1119 mergedIntoIssueRef: {
1120 extIdentifier: 'b/987654',
1121 },
1122 };
1123 const action = issueV0.fetchFederatedReferences(testIssue);
1124 await action(dispatch);
1125
1126 sinon.assert.calledWith(dispatch, {
1127 type: 'FETCH_FEDERATED_REFERENCES_START',
1128 });
1129 sinon.assert.calledWith(dispatch, {
1130 type: 'GAPI_LOGIN_SUCCESS',
1131 email: 'rutabaga@google.com',
1132 });
1133 sinon.assert.calledWith(dispatch, {
1134 type: 'FETCH_FEDERATED_REFERENCES_SUCCESS',
1135 fedRefIssueRefs: [
1136 {
1137 extIdentifier: 'b/123456',
1138 statusRef: {meansOpen: false},
1139 summary: 'Rutabaga title',
1140 },
1141 {
1142 extIdentifier: 'b/654321',
1143 statusRef: {meansOpen: false},
1144 summary: 'Rutabaga title',
1145 },
1146 {
1147 extIdentifier: 'b/987654',
1148 statusRef: {meansOpen: false},
1149 summary: 'Rutabaga title',
1150 },
1151 ],
1152 });
1153 });
1154 });
1155
1156 describe('fetchRelatedIssues', () => {
1157 it('calls fetchFederatedReferences for mergedinto', async () => {
1158 const dispatch = sinon.stub();
1159 prpcCall.returns(Promise.resolve({openRefs: [], closedRefs: []}));
1160 const testIssue = {
1161 mergedIntoIssueRef: {
1162 extIdentifier: 'b/987654',
1163 },
1164 };
1165 const action = issueV0.fetchRelatedIssues(testIssue);
1166 await action(dispatch);
1167
1168 // Important: mergedinto fedref is not passed to ListReferencedIssues.
1169 const expectedMessage = {issueRefs: []};
1170 sinon.assert.calledWith(prpcClient.call, 'monorail.Issues',
1171 'ListReferencedIssues', expectedMessage);
1172
1173 sinon.assert.calledWith(dispatch, {
1174 type: 'FETCH_RELATED_ISSUES_START',
1175 });
1176 // No mergedInto refs returned, they're handled by
1177 // fetchFederatedReferences.
1178 sinon.assert.calledWith(dispatch, {
1179 type: 'FETCH_RELATED_ISSUES_SUCCESS',
1180 relatedIssues: {},
1181 });
1182 });
1183 });
1184 });
1185 });
1186
1187 describe('starring issues', () => {
1188 describe('reducers', () => {
1189 it('FETCH_IS_STARRED_SUCCESS updates the starredIssues object', () => {
1190 const state = {};
1191 const newState = issueV0.starredIssuesReducer(state,
1192 {
1193 type: issueV0.FETCH_IS_STARRED_SUCCESS,
1194 starred: false,
1195 issueRef: {
1196 projectName: 'proj',
1197 localId: 1,
1198 },
1199 },
1200 );
1201 assert.deepEqual(newState, {'proj:1': false});
1202 });
1203
1204 it('FETCH_ISSUES_STARRED_SUCCESS updates the starredIssues object',
1205 () => {
1206 const state = {};
1207 const starredIssueRefs = [{projectName: 'proj', localId: 1},
1208 {projectName: 'proj', localId: 2}];
1209 const newState = issueV0.starredIssuesReducer(state,
1210 {type: issueV0.FETCH_ISSUES_STARRED_SUCCESS, starredIssueRefs},
1211 );
1212 assert.deepEqual(newState, {'proj:1': true, 'proj:2': true});
1213 });
1214
1215 it('FETCH_ISSUES_STARRED_SUCCESS does not time out with 10,000 stars',
1216 () => {
1217 const state = {};
1218 const starredIssueRefs = [];
1219 const expected = {};
1220 for (let i = 1; i <= 10000; i++) {
1221 starredIssueRefs.push({projectName: 'proj', localId: i});
1222 expected[`proj:${i}`] = true;
1223 }
1224 const newState = issueV0.starredIssuesReducer(state,
1225 {type: issueV0.FETCH_ISSUES_STARRED_SUCCESS, starredIssueRefs},
1226 );
1227 assert.deepEqual(newState, expected);
1228 });
1229
1230 it('STAR_SUCCESS updates the starredIssues object', () => {
1231 const state = {'proj:1': true, 'proj:2': false};
1232 const newState = issueV0.starredIssuesReducer(state,
1233 {
1234 type: issueV0.STAR_SUCCESS,
1235 starred: true,
1236 issueRef: {projectName: 'proj', localId: 2},
1237 });
1238 assert.deepEqual(newState, {'proj:1': true, 'proj:2': true});
1239 });
1240 });
1241
1242 describe('selectors', () => {
1243 describe('issue', () => {
1244 const selector = issueV0.issue(wrapIssue(example.ISSUE));
1245 assert.deepEqual(selector(example.NAME), example.ISSUE);
1246 });
1247
1248 describe('issueForRefString', () => {
1249 const noIssues = issueV0.issueForRefString(wrapIssue({}));
1250 const withIssue = issueV0.issueForRefString(wrapIssue({
1251 projectName: 'test',
1252 localId: 1,
1253 summary: 'hello world',
1254 }));
1255
1256 it('returns issue ref when no issue data', () => {
1257 assert.deepEqual(noIssues('1', 'chromium'), {
1258 localId: 1,
1259 projectName: 'chromium',
1260 });
1261
1262 assert.deepEqual(noIssues('chromium:2', 'ignore'), {
1263 localId: 2,
1264 projectName: 'chromium',
1265 });
1266
1267 assert.deepEqual(noIssues('other:3'), {
1268 localId: 3,
1269 projectName: 'other',
1270 });
1271
1272 assert.deepEqual(withIssue('other:3'), {
1273 localId: 3,
1274 projectName: 'other',
1275 });
1276 });
1277
1278 it('returns full issue data when available', () => {
1279 assert.deepEqual(withIssue('1', 'test'), {
1280 projectName: 'test',
1281 localId: 1,
1282 summary: 'hello world',
1283 });
1284
1285 assert.deepEqual(withIssue('test:1', 'other'), {
1286 projectName: 'test',
1287 localId: 1,
1288 summary: 'hello world',
1289 });
1290
1291 assert.deepEqual(withIssue('test:1'), {
1292 projectName: 'test',
1293 localId: 1,
1294 summary: 'hello world',
1295 });
1296 });
1297 });
1298
1299 it('starredIssues', () => {
1300 const state = {issue:
1301 {starredIssues: {'proj:1': true, 'proj:2': false}}};
1302 assert.deepEqual(issueV0.starredIssues(state), new Set(['proj:1']));
1303 });
1304
1305 it('starringIssues', () => {
1306 const state = {issue: {
1307 requests: {
1308 starringIssues: {
1309 'proj:1': {requesting: true},
1310 'proj:2': {requestin: false, error: 'unknown error'},
1311 },
1312 },
1313 }};
1314 assert.deepEqual(issueV0.starringIssues(state), new Map([
1315 ['proj:1', {requesting: true}],
1316 ['proj:2', {requestin: false, error: 'unknown error'}],
1317 ]));
1318 });
1319 });
1320
1321 describe('action creators', () => {
1322 beforeEach(() => {
1323 prpcCall = sinon.stub(prpcClient, 'call');
1324
1325 dispatch = sinon.stub();
1326 });
1327
1328 afterEach(() => {
1329 prpcCall.restore();
1330 });
1331
1332 it('fetching if an issue is starred', async () => {
1333 const issueRef = {projectName: 'proj', localId: 1};
1334 const action = issueV0.fetchIsStarred(issueRef);
1335
1336 prpcCall.returns(Promise.resolve({isStarred: true}));
1337
1338 await action(dispatch);
1339
1340 sinon.assert.calledWith(dispatch,
1341 {type: issueV0.FETCH_IS_STARRED_START});
1342
1343 sinon.assert.calledWith(
1344 prpcClient.call, 'monorail.Issues',
1345 'IsIssueStarred', {issueRef},
1346 );
1347
1348 sinon.assert.calledWith(dispatch, {
1349 type: issueV0.FETCH_IS_STARRED_SUCCESS,
1350 starred: true,
1351 issueRef,
1352 });
1353 });
1354
1355 it('fetching starred issues', async () => {
1356 const returnedIssueRef = {projectName: 'proj', localId: 1};
1357 const starredIssueRefs = [returnedIssueRef];
1358 const action = issueV0.fetchStarredIssues();
1359
1360 prpcCall.returns(Promise.resolve({starredIssueRefs}));
1361
1362 await action(dispatch);
1363
1364 sinon.assert.calledWith(dispatch, {type: 'FETCH_ISSUES_STARRED_START'});
1365
1366 sinon.assert.calledWith(
1367 prpcClient.call, 'monorail.Issues',
1368 'ListStarredIssues', {},
1369 );
1370
1371 sinon.assert.calledWith(dispatch, {
1372 type: issueV0.FETCH_ISSUES_STARRED_SUCCESS,
1373 starredIssueRefs,
1374 });
1375 });
1376
1377 it('star', async () => {
1378 const testIssue = {projectName: 'proj', localId: 1, starCount: 1};
1379 const issueRef = issueToIssueRef(testIssue);
1380 const action = issueV0.star(issueRef, false);
1381
1382 prpcCall.returns(Promise.resolve(testIssue));
1383
1384 await action(dispatch);
1385
1386 sinon.assert.calledWith(dispatch, {
1387 type: issueV0.STAR_START,
1388 requestKey: 'proj:1',
1389 });
1390
1391 sinon.assert.calledWith(
1392 prpcClient.call,
1393 'monorail.Issues', 'StarIssue',
1394 {issueRef, starred: false},
1395 );
1396
1397 sinon.assert.calledWith(dispatch, {
1398 type: issueV0.STAR_SUCCESS,
1399 starCount: 1,
1400 issueRef,
1401 starred: false,
1402 requestKey: 'proj:1',
1403 });
1404 });
1405 });
1406 });
1407});
1408
1409/**
1410 * Return an initial Redux state with a given viewed
1411 * @param {Issue=} viewedIssue The viewed issue.
1412 * @param {Object=} otherValues Any other state values that need
1413 * to be initialized.
1414 * @return {Object}
1415 */
1416function wrapIssue(viewedIssue, otherValues = {}) {
1417 if (!viewedIssue) {
1418 return {
1419 issue: {
1420 issuesByRefString: {},
1421 ...otherValues,
1422 },
1423 };
1424 }
1425
1426 const ref = issueRefToString(viewedIssue);
1427 return {
1428 issue: {
1429 viewedIssueRef: ref,
1430 issuesByRefString: {
1431 [ref]: {...viewedIssue},
1432 },
1433 ...otherValues,
1434 },
1435 };
1436}