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