| // Copyright 2019 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import {assert} from 'chai'; |
| import sinon from 'sinon'; |
| import {createSelector} from 'reselect'; |
| import {store, resetState} from './base.js'; |
| import * as issueV0 from './issueV0.js'; |
| import * as example from 'shared/test/constants-issueV0.js'; |
| import {fieldTypes} from 'shared/issue-fields.js'; |
| import {issueToIssueRef, issueRefToString} from 'shared/convertersV0.js'; |
| import {prpcClient} from 'prpc-client-instance.js'; |
| import {getSigninInstance} from 'shared/gapi-loader.js'; |
| |
| let prpcCall; |
| let dispatch; |
| |
| describe('issue', () => { |
| beforeEach(() => { |
| store.dispatch(resetState()); |
| }); |
| |
| describe('reducers', () => { |
| describe('issueByRefReducer', () => { |
| it('no-op on unmatching action', () => { |
| const action = { |
| type: 'FAKE_ACTION', |
| issues: [example.ISSUE_OTHER_PROJECT], |
| }; |
| assert.deepEqual(issueV0.issuesByRefStringReducer({}, action), {}); |
| |
| const state = {[example.ISSUE_REF_STRING]: example.ISSUE}; |
| assert.deepEqual(issueV0.issuesByRefStringReducer(state, action), |
| state); |
| }); |
| |
| it('handles FETCH_ISSUE_LIST_UPDATE', () => { |
| const newState = issueV0.issuesByRefStringReducer({}, { |
| type: issueV0.FETCH_ISSUE_LIST_UPDATE, |
| issues: [example.ISSUE, example.ISSUE_OTHER_PROJECT], |
| totalResults: 2, |
| progress: 1, |
| }); |
| assert.deepEqual(newState, { |
| [example.ISSUE_REF_STRING]: example.ISSUE, |
| [example.ISSUE_OTHER_PROJECT_REF_STRING]: example.ISSUE_OTHER_PROJECT, |
| }); |
| }); |
| |
| it('handles FETCH_ISSUES_SUCCESS', () => { |
| const newState = issueV0.issuesByRefStringReducer({}, { |
| type: issueV0.FETCH_ISSUES_SUCCESS, |
| issues: [example.ISSUE, example.ISSUE_OTHER_PROJECT], |
| }); |
| assert.deepEqual(newState, { |
| [example.ISSUE_REF_STRING]: example.ISSUE, |
| [example.ISSUE_OTHER_PROJECT_REF_STRING]: example.ISSUE_OTHER_PROJECT, |
| }); |
| }); |
| }); |
| |
| describe('issueListReducer', () => { |
| it('no-op on unmatching action', () => { |
| const action = { |
| type: 'FETCH_ISSUE_LIST_FAKE_ACTION', |
| issues: [ |
| {localId: 1, projectName: 'chromium', summary: 'hello-world'}, |
| ], |
| }; |
| assert.deepEqual(issueV0.issueListReducer({}, action), {}); |
| |
| assert.deepEqual(issueV0.issueListReducer({ |
| issueRefs: ['chromium:1'], |
| totalResults: 1, |
| progress: 1, |
| }, action), { |
| issueRefs: ['chromium:1'], |
| totalResults: 1, |
| progress: 1, |
| }); |
| }); |
| |
| it('handles FETCH_ISSUE_LIST_UPDATE', () => { |
| const newState = issueV0.issueListReducer({}, { |
| type: 'FETCH_ISSUE_LIST_UPDATE', |
| issues: [ |
| {localId: 1, projectName: 'chromium', summary: 'hello-world'}, |
| {localId: 2, projectName: 'monorail', summary: 'Test'}, |
| ], |
| totalResults: 2, |
| progress: 1, |
| }); |
| assert.deepEqual(newState, { |
| issueRefs: ['chromium:1', 'monorail:2'], |
| totalResults: 2, |
| progress: 1, |
| }); |
| }); |
| }); |
| |
| describe('relatedIssuesReducer', () => { |
| it('handles FETCH_RELATED_ISSUES_SUCCESS', () => { |
| const newState = issueV0.relatedIssuesReducer({}, { |
| type: 'FETCH_RELATED_ISSUES_SUCCESS', |
| relatedIssues: {'rutabaga:1234': {}}, |
| }); |
| assert.deepEqual(newState, {'rutabaga:1234': {}}); |
| }); |
| |
| describe('FETCH_FEDERATED_REFERENCES_SUCCESS', () => { |
| it('returns early if data is missing', () => { |
| const newState = issueV0.relatedIssuesReducer({'b/123': {}}, { |
| type: 'FETCH_FEDERATED_REFERENCES_SUCCESS', |
| }); |
| assert.deepEqual(newState, {'b/123': {}}); |
| }); |
| |
| it('returns early if data is empty', () => { |
| const newState = issueV0.relatedIssuesReducer({'b/123': {}}, { |
| type: 'FETCH_FEDERATED_REFERENCES_SUCCESS', |
| fedRefIssueRefs: [], |
| }); |
| assert.deepEqual(newState, {'b/123': {}}); |
| }); |
| |
| it('assigns each FedRef to the state', () => { |
| const state = { |
| 'rutabaga:123': {}, |
| 'rutabaga:345': {}, |
| }; |
| const newState = issueV0.relatedIssuesReducer(state, { |
| type: 'FETCH_FEDERATED_REFERENCES_SUCCESS', |
| fedRefIssueRefs: [ |
| { |
| extIdentifier: 'b/987', |
| summary: 'What is up', |
| statusRef: {meansOpen: true}, |
| }, |
| { |
| extIdentifier: 'b/765', |
| summary: 'Rutabaga', |
| statusRef: {meansOpen: false}, |
| }, |
| ], |
| }); |
| assert.deepEqual(newState, { |
| 'rutabaga:123': {}, |
| 'rutabaga:345': {}, |
| 'b/987': { |
| extIdentifier: 'b/987', |
| summary: 'What is up', |
| statusRef: {meansOpen: true}, |
| }, |
| 'b/765': { |
| extIdentifier: 'b/765', |
| summary: 'Rutabaga', |
| statusRef: {meansOpen: false}, |
| }, |
| }); |
| }); |
| }); |
| }); |
| }); |
| |
| it('viewedIssue', () => { |
| assert.deepEqual(issueV0.viewedIssue(wrapIssue()), {}); |
| assert.deepEqual( |
| issueV0.viewedIssue(wrapIssue({projectName: 'proj', localId: 100})), |
| {projectName: 'proj', localId: 100}, |
| ); |
| }); |
| |
| describe('issueList', () => { |
| it('issueList', () => { |
| const stateWithEmptyIssueList = {issue: { |
| issueList: {}, |
| }}; |
| assert.deepEqual(issueV0.issueList(stateWithEmptyIssueList), []); |
| |
| const stateWithIssueList = {issue: { |
| issuesByRefString: { |
| 'chromium:1': {localId: 1, projectName: 'chromium', summary: 'test'}, |
| 'monorail:2': {localId: 2, projectName: 'monorail', |
| summary: 'hello world'}, |
| }, |
| issueList: { |
| issueRefs: ['chromium:1', 'monorail:2'], |
| }}}; |
| assert.deepEqual(issueV0.issueList(stateWithIssueList), |
| [ |
| {localId: 1, projectName: 'chromium', summary: 'test'}, |
| {localId: 2, projectName: 'monorail', summary: 'hello world'}, |
| ]); |
| }); |
| |
| it('is a selector', () => { |
| issueV0.issueList.constructor === createSelector; |
| }); |
| |
| it('memoizes results: returns same reference', () => { |
| const stateWithIssueList = {issue: { |
| issuesByRefString: { |
| 'chromium:1': {localId: 1, projectName: 'chromium', summary: 'test'}, |
| 'monorail:2': {localId: 2, projectName: 'monorail', |
| summary: 'hello world'}, |
| }, |
| issueList: { |
| issueRefs: ['chromium:1', 'monorail:2'], |
| }}}; |
| const reference1 = issueV0.issueList(stateWithIssueList); |
| const reference2 = issueV0.issueList(stateWithIssueList); |
| |
| assert.equal(typeof reference1, 'object'); |
| assert.equal(typeof reference2, 'object'); |
| assert.equal(reference1, reference2); |
| }); |
| }); |
| |
| describe('issueListLoaded', () => { |
| const stateWithEmptyIssueList = {issue: { |
| issueList: {}, |
| }}; |
| |
| it('false when no issue list', () => { |
| assert.isFalse(issueV0.issueListLoaded(stateWithEmptyIssueList)); |
| }); |
| |
| it('true after issues loaded, even when empty', () => { |
| const issueList = issueV0.issueListReducer({}, { |
| type: issueV0.FETCH_ISSUE_LIST_UPDATE, |
| issues: [], |
| progress: 1, |
| totalResults: 0, |
| }); |
| assert.isTrue(issueV0.issueListLoaded({issue: {issueList}})); |
| }); |
| }); |
| |
| it('fieldValues', () => { |
| assert.isUndefined(issueV0.fieldValues(wrapIssue())); |
| assert.deepEqual(issueV0.fieldValues(wrapIssue({ |
| fieldValues: [{value: 'v'}], |
| })), [{value: 'v'}]); |
| }); |
| |
| it('type computes type from custom field', () => { |
| assert.isUndefined(issueV0.type(wrapIssue())); |
| assert.isUndefined(issueV0.type(wrapIssue({ |
| fieldValues: [{value: 'v'}], |
| }))); |
| assert.deepEqual(issueV0.type(wrapIssue({ |
| fieldValues: [ |
| {fieldRef: {fieldName: 'IgnoreMe'}, value: 'v'}, |
| {fieldRef: {fieldName: 'Type'}, value: 'Defect'}, |
| ], |
| })), 'Defect'); |
| }); |
| |
| it('type computes type from label', () => { |
| assert.deepEqual(issueV0.type(wrapIssue({ |
| labelRefs: [ |
| {label: 'Test'}, |
| {label: 'tYpE-FeatureRequest'}, |
| ], |
| })), 'FeatureRequest'); |
| |
| assert.deepEqual(issueV0.type(wrapIssue({ |
| fieldValues: [ |
| {fieldRef: {fieldName: 'IgnoreMe'}, value: 'v'}, |
| ], |
| labelRefs: [ |
| {label: 'Test'}, |
| {label: 'Type-Defect'}, |
| ], |
| })), 'Defect'); |
| }); |
| |
| it('restrictions', () => { |
| assert.deepEqual(issueV0.restrictions(wrapIssue()), {}); |
| assert.deepEqual(issueV0.restrictions(wrapIssue({labelRefs: []})), {}); |
| |
| assert.deepEqual(issueV0.restrictions(wrapIssue({labelRefs: [ |
| {label: 'IgnoreThis'}, |
| {label: 'IgnoreThis2'}, |
| ]})), {}); |
| |
| assert.deepEqual(issueV0.restrictions(wrapIssue({labelRefs: [ |
| {label: 'IgnoreThis'}, |
| {label: 'IgnoreThis2'}, |
| {label: 'Restrict-View-Google'}, |
| {label: 'Restrict-EditIssue-hello'}, |
| {label: 'Restrict-EditIssue-test'}, |
| {label: 'Restrict-AddIssueComment-HELLO'}, |
| ]})), { |
| 'view': ['Google'], |
| 'edit': ['hello', 'test'], |
| 'comment': ['HELLO'], |
| }); |
| }); |
| |
| it('isOpen', () => { |
| assert.isFalse(issueV0.isOpen(wrapIssue())); |
| assert.isTrue(issueV0.isOpen(wrapIssue({statusRef: {meansOpen: true}}))); |
| assert.isFalse(issueV0.isOpen(wrapIssue({statusRef: {meansOpen: false}}))); |
| }); |
| |
| it('issueListPhaseNames', () => { |
| const stateWithEmptyIssueList = {issue: { |
| issueList: [], |
| }}; |
| assert.deepEqual(issueV0.issueListPhaseNames(stateWithEmptyIssueList), []); |
| const stateWithIssueList = {issue: { |
| issuesByRefString: { |
| '1': {localId: 1, phases: [{phaseRef: {phaseName: 'chicken-phase'}}]}, |
| '2': {localId: 2, phases: [ |
| {phaseRef: {phaseName: 'chicken-Phase'}}, |
| {phaseRef: {phaseName: 'cow-phase'}}], |
| }, |
| '3': {localId: 3, phases: [ |
| {phaseRef: {phaseName: 'cow-Phase'}}, |
| {phaseRef: {phaseName: 'DOG-phase'}}], |
| }, |
| '4': {localId: 4, phases: [ |
| {phaseRef: {phaseName: 'dog-phase'}}, |
| ]}, |
| }, |
| issueList: { |
| issueRefs: ['1', '2', '3', '4'], |
| }}}; |
| assert.deepEqual(issueV0.issueListPhaseNames(stateWithIssueList), |
| ['chicken-phase', 'cow-phase', 'dog-phase']); |
| }); |
| |
| describe('blockingIssues', () => { |
| const relatedIssues = { |
| ['proj:1']: { |
| localId: 1, |
| projectName: 'proj', |
| labelRefs: [{label: 'label'}], |
| }, |
| ['proj:3']: { |
| localId: 3, |
| projectName: 'proj', |
| labelRefs: [], |
| }, |
| ['chromium:332']: { |
| localId: 332, |
| projectName: 'chromium', |
| labelRefs: [], |
| }, |
| }; |
| |
| it('returns references when no issue data', () => { |
| const stateNoReferences = wrapIssue( |
| { |
| projectName: 'project', |
| localId: 123, |
| blockingIssueRefs: [{localId: 1, projectName: 'proj'}], |
| }, |
| {relatedIssues: {}}, |
| ); |
| assert.deepEqual(issueV0.blockingIssues(stateNoReferences), |
| [{localId: 1, projectName: 'proj'}], |
| ); |
| }); |
| |
| it('returns empty when no blocking issues', () => { |
| const stateNoIssues = wrapIssue( |
| { |
| projectName: 'project', |
| localId: 123, |
| blockingIssueRefs: [], |
| }, |
| {relatedIssues}, |
| ); |
| assert.deepEqual(issueV0.blockingIssues(stateNoIssues), []); |
| }); |
| |
| it('returns full issues when deferenced data present', () => { |
| const stateIssuesWithReferences = wrapIssue( |
| { |
| projectName: 'project', |
| localId: 123, |
| blockingIssueRefs: [ |
| {localId: 1, projectName: 'proj'}, |
| {localId: 332, projectName: 'chromium'}, |
| ], |
| }, |
| {relatedIssues}, |
| ); |
| assert.deepEqual(issueV0.blockingIssues(stateIssuesWithReferences), |
| [ |
| {localId: 1, projectName: 'proj', labelRefs: [{label: 'label'}]}, |
| {localId: 332, projectName: 'chromium', labelRefs: []}, |
| ]); |
| }); |
| |
| it('returns federated references', () => { |
| const stateIssuesWithFederatedReferences = wrapIssue( |
| { |
| projectName: 'project', |
| localId: 123, |
| blockingIssueRefs: [ |
| {localId: 1, projectName: 'proj'}, |
| {extIdentifier: 'b/1234'}, |
| ], |
| }, |
| {relatedIssues}, |
| ); |
| assert.deepEqual( |
| issueV0.blockingIssues(stateIssuesWithFederatedReferences), [ |
| {localId: 1, projectName: 'proj', labelRefs: [{label: 'label'}]}, |
| {extIdentifier: 'b/1234'}, |
| ]); |
| }); |
| }); |
| |
| describe('blockedOnIssues', () => { |
| const relatedIssues = { |
| ['proj:1']: { |
| localId: 1, |
| projectName: 'proj', |
| labelRefs: [{label: 'label'}], |
| }, |
| ['proj:3']: { |
| localId: 3, |
| projectName: 'proj', |
| labelRefs: [], |
| }, |
| ['chromium:332']: { |
| localId: 332, |
| projectName: 'chromium', |
| labelRefs: [], |
| }, |
| }; |
| |
| it('returns references when no issue data', () => { |
| const stateNoReferences = wrapIssue( |
| { |
| projectName: 'project', |
| localId: 123, |
| blockedOnIssueRefs: [{localId: 1, projectName: 'proj'}], |
| }, |
| {relatedIssues: {}}, |
| ); |
| assert.deepEqual(issueV0.blockedOnIssues(stateNoReferences), |
| [{localId: 1, projectName: 'proj'}], |
| ); |
| }); |
| |
| it('returns empty when no blocking issues', () => { |
| const stateNoIssues = wrapIssue( |
| { |
| projectName: 'project', |
| localId: 123, |
| blockedOnIssueRefs: [], |
| }, |
| {relatedIssues}, |
| ); |
| assert.deepEqual(issueV0.blockedOnIssues(stateNoIssues), []); |
| }); |
| |
| it('returns full issues when deferenced data present', () => { |
| const stateIssuesWithReferences = wrapIssue( |
| { |
| projectName: 'project', |
| localId: 123, |
| blockedOnIssueRefs: [ |
| {localId: 1, projectName: 'proj'}, |
| {localId: 332, projectName: 'chromium'}, |
| ], |
| }, |
| {relatedIssues}, |
| ); |
| assert.deepEqual(issueV0.blockedOnIssues(stateIssuesWithReferences), |
| [ |
| {localId: 1, projectName: 'proj', labelRefs: [{label: 'label'}]}, |
| {localId: 332, projectName: 'chromium', labelRefs: []}, |
| ]); |
| }); |
| |
| it('returns federated references', () => { |
| const stateIssuesWithFederatedReferences = wrapIssue( |
| { |
| projectName: 'project', |
| localId: 123, |
| blockedOnIssueRefs: [ |
| {localId: 1, projectName: 'proj'}, |
| {extIdentifier: 'b/1234'}, |
| ], |
| }, |
| {relatedIssues}, |
| ); |
| assert.deepEqual( |
| issueV0.blockedOnIssues(stateIssuesWithFederatedReferences), |
| [ |
| {localId: 1, projectName: 'proj', labelRefs: [{label: 'label'}]}, |
| {extIdentifier: 'b/1234'}, |
| ]); |
| }); |
| }); |
| |
| describe('sortedBlockedOn', () => { |
| const relatedIssues = { |
| ['proj:1']: { |
| localId: 1, |
| projectName: 'proj', |
| statusRef: {meansOpen: true}, |
| }, |
| ['proj:3']: { |
| localId: 3, |
| projectName: 'proj', |
| statusRef: {meansOpen: false}, |
| }, |
| ['proj:4']: { |
| localId: 4, |
| projectName: 'proj', |
| statusRef: {meansOpen: false}, |
| }, |
| ['proj:5']: { |
| localId: 5, |
| projectName: 'proj', |
| statusRef: {meansOpen: false}, |
| }, |
| ['chromium:332']: { |
| localId: 332, |
| projectName: 'chromium', |
| statusRef: {meansOpen: true}, |
| }, |
| }; |
| |
| it('does not sort references when no issue data', () => { |
| const stateNoReferences = wrapIssue( |
| { |
| projectName: 'project', |
| localId: 123, |
| blockedOnIssueRefs: [ |
| {localId: 3, projectName: 'proj'}, |
| {localId: 1, projectName: 'proj'}, |
| ], |
| }, |
| {relatedIssues: {}}, |
| ); |
| assert.deepEqual(issueV0.sortedBlockedOn(stateNoReferences), [ |
| {localId: 3, projectName: 'proj'}, |
| {localId: 1, projectName: 'proj'}, |
| ]); |
| }); |
| |
| it('sorts open issues first when issue data available', () => { |
| const stateReferences = wrapIssue( |
| { |
| projectName: 'project', |
| localId: 123, |
| blockedOnIssueRefs: [ |
| {localId: 3, projectName: 'proj'}, |
| {localId: 1, projectName: 'proj'}, |
| ], |
| }, |
| {relatedIssues}, |
| ); |
| assert.deepEqual(issueV0.sortedBlockedOn(stateReferences), [ |
| {localId: 1, projectName: 'proj', statusRef: {meansOpen: true}}, |
| {localId: 3, projectName: 'proj', statusRef: {meansOpen: false}}, |
| ]); |
| }); |
| |
| it('preserves original order on ties', () => { |
| const statePreservesArrayOrder = wrapIssue( |
| { |
| projectName: 'project', |
| localId: 123, |
| blockedOnIssueRefs: [ |
| {localId: 5, projectName: 'proj'}, // Closed |
| {localId: 1, projectName: 'proj'}, // Open |
| {localId: 4, projectName: 'proj'}, // Closed |
| {localId: 3, projectName: 'proj'}, // Closed |
| {localId: 332, projectName: 'chromium'}, // Open |
| ], |
| }, |
| {relatedIssues}, |
| ); |
| assert.deepEqual(issueV0.sortedBlockedOn(statePreservesArrayOrder), |
| [ |
| {localId: 1, projectName: 'proj', statusRef: {meansOpen: true}}, |
| {localId: 332, projectName: 'chromium', |
| statusRef: {meansOpen: true}}, |
| {localId: 5, projectName: 'proj', statusRef: {meansOpen: false}}, |
| {localId: 4, projectName: 'proj', statusRef: {meansOpen: false}}, |
| {localId: 3, projectName: 'proj', statusRef: {meansOpen: false}}, |
| ], |
| ); |
| }); |
| }); |
| |
| describe('mergedInto', () => { |
| it('empty', () => { |
| assert.deepEqual(issueV0.mergedInto(wrapIssue()), {}); |
| }); |
| |
| it('gets mergedInto ref for viewed issue', () => { |
| const state = issueV0.mergedInto(wrapIssue({ |
| projectName: 'project', |
| localId: 123, |
| mergedIntoIssueRef: {localId: 22, projectName: 'proj'}, |
| })); |
| assert.deepEqual(state, { |
| localId: 22, |
| projectName: 'proj', |
| }); |
| }); |
| |
| it('gets full mergedInto issue data when it exists in the store', () => { |
| const state = wrapIssue( |
| { |
| projectName: 'project', |
| localId: 123, |
| mergedIntoIssueRef: {localId: 22, projectName: 'proj'}, |
| }, { |
| relatedIssues: { |
| ['proj:22']: {localId: 22, projectName: 'proj', summary: 'test'}, |
| }, |
| }); |
| assert.deepEqual(issueV0.mergedInto(state), { |
| localId: 22, |
| projectName: 'proj', |
| summary: 'test', |
| }); |
| }); |
| }); |
| |
| it('fieldValueMap', () => { |
| assert.deepEqual(issueV0.fieldValueMap(wrapIssue()), new Map()); |
| assert.deepEqual(issueV0.fieldValueMap(wrapIssue({ |
| fieldValues: [], |
| })), new Map()); |
| assert.deepEqual(issueV0.fieldValueMap(wrapIssue({ |
| fieldValues: [ |
| {fieldRef: {fieldName: 'hello'}, value: 'v3'}, |
| {fieldRef: {fieldName: 'hello'}, value: 'v2'}, |
| {fieldRef: {fieldName: 'world'}, value: 'v3'}, |
| ], |
| })), new Map([ |
| ['hello', ['v3', 'v2']], |
| ['world', ['v3']], |
| ])); |
| }); |
| |
| it('fieldDefs filters fields by applicable type', () => { |
| assert.deepEqual(issueV0.fieldDefs({ |
| projectV0: {}, |
| ...wrapIssue(), |
| }), []); |
| |
| assert.deepEqual(issueV0.fieldDefs({ |
| projectV0: { |
| name: 'chromium', |
| configs: { |
| chromium: { |
| fieldDefs: [ |
| {fieldRef: {fieldName: 'intyInt', type: fieldTypes.INT_TYPE}}, |
| {fieldRef: {fieldName: 'enum', type: fieldTypes.ENUM_TYPE}}, |
| { |
| fieldRef: |
| {fieldName: 'nonApplicable', type: fieldTypes.STR_TYPE}, |
| applicableType: 'None', |
| }, |
| {fieldRef: {fieldName: 'defectsOnly', type: fieldTypes.STR_TYPE}, |
| applicableType: 'Defect'}, |
| ], |
| }, |
| }, |
| }, |
| ...wrapIssue({ |
| fieldValues: [ |
| {fieldRef: {fieldName: 'Type'}, value: 'Defect'}, |
| ], |
| }), |
| }), [ |
| {fieldRef: {fieldName: 'intyInt', type: fieldTypes.INT_TYPE}}, |
| {fieldRef: {fieldName: 'enum', type: fieldTypes.ENUM_TYPE}}, |
| {fieldRef: {fieldName: 'defectsOnly', type: fieldTypes.STR_TYPE}, |
| applicableType: 'Defect'}, |
| ]); |
| }); |
| |
| it('fieldDefs skips approval fields for all issues', () => { |
| assert.deepEqual(issueV0.fieldDefs({ |
| projectV0: { |
| name: 'chromium', |
| configs: { |
| chromium: { |
| fieldDefs: [ |
| {fieldRef: {fieldName: 'test', type: fieldTypes.INT_TYPE}}, |
| {fieldRef: |
| {fieldName: 'ignoreMe', type: fieldTypes.APPROVAL_TYPE}}, |
| {fieldRef: |
| {fieldName: 'LookAway', approvalName: 'ThisIsAnApproval'}}, |
| {fieldRef: {fieldName: 'phaseField'}, isPhaseField: true}, |
| ], |
| }, |
| }, |
| }, |
| ...wrapIssue(), |
| }), [ |
| {fieldRef: {fieldName: 'test', type: fieldTypes.INT_TYPE}}, |
| ]); |
| }); |
| |
| it('fieldDefs includes non applicable fields when values defined', () => { |
| assert.deepEqual(issueV0.fieldDefs({ |
| projectV0: { |
| name: 'chromium', |
| configs: { |
| chromium: { |
| fieldDefs: [ |
| { |
| fieldRef: |
| {fieldName: 'nonApplicable', type: fieldTypes.STR_TYPE}, |
| applicableType: 'None', |
| }, |
| ], |
| }, |
| }, |
| }, |
| ...wrapIssue({ |
| fieldValues: [ |
| {fieldRef: {fieldName: 'nonApplicable'}, value: 'v3'}, |
| ], |
| }), |
| }), [ |
| {fieldRef: {fieldName: 'nonApplicable', type: fieldTypes.STR_TYPE}, |
| applicableType: 'None'}, |
| ]); |
| }); |
| |
| describe('action creators', () => { |
| beforeEach(() => { |
| prpcCall = sinon.stub(prpcClient, 'call'); |
| }); |
| |
| afterEach(() => { |
| prpcCall.restore(); |
| }); |
| |
| it('viewIssue creates action with issueRef', () => { |
| assert.deepEqual( |
| issueV0.viewIssue({projectName: 'proj', localId: 123}), |
| { |
| type: issueV0.VIEW_ISSUE, |
| issueRef: {projectName: 'proj', localId: 123}, |
| }, |
| ); |
| }); |
| |
| |
| describe('updateApproval', async () => { |
| const APPROVAL = { |
| fieldRef: {fieldName: 'Privacy', type: 'APPROVAL_TYPE'}, |
| approverRefs: [{userId: 1234, displayName: 'test@example.com'}], |
| status: 'APPROVED', |
| }; |
| |
| it('approval update success', async () => { |
| const dispatch = sinon.stub(); |
| |
| prpcCall.returns({approval: APPROVAL}); |
| |
| const action = issueV0.updateApproval({ |
| issueRef: {projectName: 'chromium', localId: 1234}, |
| fieldRef: {fieldName: 'Privacy', type: 'APPROVAL_TYPE'}, |
| approvalDelta: {status: 'APPROVED'}, |
| sendEmail: true, |
| }); |
| |
| await action(dispatch); |
| |
| sinon.assert.calledOnce(prpcCall); |
| |
| sinon.assert.calledWith(prpcCall, 'monorail.Issues', |
| 'UpdateApproval', { |
| issueRef: {projectName: 'chromium', localId: 1234}, |
| fieldRef: {fieldName: 'Privacy', type: 'APPROVAL_TYPE'}, |
| approvalDelta: {status: 'APPROVED'}, |
| sendEmail: true, |
| }); |
| |
| sinon.assert.calledWith(dispatch, {type: 'UPDATE_APPROVAL_START'}); |
| sinon.assert.calledWith(dispatch, { |
| type: 'UPDATE_APPROVAL_SUCCESS', |
| approval: APPROVAL, |
| issueRef: {projectName: 'chromium', localId: 1234}, |
| }); |
| }); |
| |
| it('approval survey update success', async () => { |
| const dispatch = sinon.stub(); |
| |
| prpcCall.returns({approval: APPROVAL}); |
| |
| const action = issueV0.updateApproval({ |
| issueRef: {projectName: 'chromium', localId: 1234}, |
| fieldRef: {fieldName: 'Privacy', type: 'APPROVAL_TYPE'}, |
| commentContent: 'new survey', |
| sendEmail: false, |
| isDescription: true, |
| }); |
| |
| await action(dispatch); |
| |
| sinon.assert.calledOnce(prpcCall); |
| |
| sinon.assert.calledWith(prpcCall, 'monorail.Issues', |
| 'UpdateApproval', { |
| issueRef: {projectName: 'chromium', localId: 1234}, |
| fieldRef: {fieldName: 'Privacy', type: 'APPROVAL_TYPE'}, |
| commentContent: 'new survey', |
| isDescription: true, |
| }); |
| |
| sinon.assert.calledWith(dispatch, {type: 'UPDATE_APPROVAL_START'}); |
| sinon.assert.calledWith(dispatch, { |
| type: 'UPDATE_APPROVAL_SUCCESS', |
| approval: APPROVAL, |
| issueRef: {projectName: 'chromium', localId: 1234}, |
| }); |
| }); |
| |
| it('attachment upload success', async () => { |
| const dispatch = sinon.stub(); |
| |
| prpcCall.returns({approval: APPROVAL}); |
| |
| const action = issueV0.updateApproval({ |
| issueRef: {projectName: 'chromium', localId: 1234}, |
| fieldRef: {fieldName: 'Privacy', type: 'APPROVAL_TYPE'}, |
| uploads: '78f17a020cbf39e90e344a842cd19911', |
| }); |
| |
| await action(dispatch); |
| |
| sinon.assert.calledOnce(prpcCall); |
| |
| sinon.assert.calledWith(prpcCall, 'monorail.Issues', |
| 'UpdateApproval', { |
| issueRef: {projectName: 'chromium', localId: 1234}, |
| fieldRef: {fieldName: 'Privacy', type: 'APPROVAL_TYPE'}, |
| uploads: '78f17a020cbf39e90e344a842cd19911', |
| }); |
| |
| sinon.assert.calledWith(dispatch, {type: 'UPDATE_APPROVAL_START'}); |
| sinon.assert.calledWith(dispatch, { |
| type: 'UPDATE_APPROVAL_SUCCESS', |
| approval: APPROVAL, |
| issueRef: {projectName: 'chromium', localId: 1234}, |
| }); |
| }); |
| }); |
| |
| describe('fetchIssues', () => { |
| it('success', async () => { |
| const response = { |
| openRefs: [example.ISSUE], |
| closedRefs: [example.ISSUE_OTHER_PROJECT], |
| }; |
| prpcClient.call.returns(Promise.resolve(response)); |
| const dispatch = sinon.stub(); |
| |
| await issueV0.fetchIssues([example.ISSUE_REF])(dispatch); |
| |
| sinon.assert.calledWith(dispatch, {type: issueV0.FETCH_ISSUES_START}); |
| |
| const args = {issueRefs: [example.ISSUE_REF]}; |
| sinon.assert.calledWith( |
| prpcClient.call, 'monorail.Issues', 'ListReferencedIssues', args); |
| |
| const action = { |
| type: issueV0.FETCH_ISSUES_SUCCESS, |
| issues: [example.ISSUE, example.ISSUE_OTHER_PROJECT], |
| }; |
| sinon.assert.calledWith(dispatch, action); |
| }); |
| |
| it('failure', async () => { |
| prpcClient.call.throws(); |
| const dispatch = sinon.stub(); |
| |
| await issueV0.fetchIssues([example.ISSUE_REF])(dispatch); |
| |
| const action = { |
| type: issueV0.FETCH_ISSUES_FAILURE, |
| error: sinon.match.any, |
| }; |
| sinon.assert.calledWith(dispatch, action); |
| }); |
| }); |
| |
| it('fetchIssueList calls ListIssues', async () => { |
| prpcCall.callsFake(() => { |
| return { |
| issues: [{localId: 1}, {localId: 2}, {localId: 3}], |
| totalResults: 6, |
| }; |
| }); |
| |
| store.dispatch(issueV0.fetchIssueList('chromium', |
| {q: 'owner:me', can: '4'})); |
| |
| sinon.assert.calledWith(prpcCall, 'monorail.Issues', 'ListIssues', { |
| query: 'owner:me', |
| cannedQuery: 4, |
| projectNames: ['chromium'], |
| pagination: {}, |
| groupBySpec: undefined, |
| sortSpec: undefined, |
| }); |
| }); |
| |
| it('fetchIssueList does not set can when can is NaN', async () => { |
| prpcCall.callsFake(() => ({})); |
| |
| store.dispatch(issueV0.fetchIssueList('chromium', {q: 'owner:me', |
| can: 'four-leaf-clover'})); |
| |
| sinon.assert.calledWith(prpcCall, 'monorail.Issues', 'ListIssues', { |
| query: 'owner:me', |
| cannedQuery: undefined, |
| projectNames: ['chromium'], |
| pagination: {}, |
| groupBySpec: undefined, |
| sortSpec: undefined, |
| }); |
| }); |
| |
| it('fetchIssueList makes several calls to ListIssues', async () => { |
| prpcCall.callsFake(() => { |
| return { |
| issues: [{localId: 1}, {localId: 2}, {localId: 3}], |
| totalResults: 6, |
| }; |
| }); |
| |
| const dispatch = sinon.stub(); |
| const action = issueV0.fetchIssueList('chromium', |
| {maxItems: 3, maxCalls: 2}); |
| await action(dispatch); |
| |
| sinon.assert.calledTwice(prpcCall); |
| sinon.assert.calledWith(dispatch, { |
| type: 'FETCH_ISSUE_LIST_UPDATE', |
| issues: |
| [{localId: 1}, {localId: 2}, {localId: 3}, |
| {localId: 1}, {localId: 2}, {localId: 3}], |
| progress: 1, |
| totalResults: 6, |
| }); |
| sinon.assert.calledWith(dispatch, {type: 'FETCH_ISSUE_LIST_SUCCESS'}); |
| }); |
| |
| it('fetchIssueList orders issues correctly', async () => { |
| prpcCall.onFirstCall().returns({issues: [{localId: 1}], totalResults: 6}); |
| prpcCall.onSecondCall().returns({ |
| issues: [{localId: 2}], |
| totalResults: 6}); |
| prpcCall.onThirdCall().returns({issues: [{localId: 3}], totalResults: 6}); |
| |
| const dispatch = sinon.stub(); |
| const action = issueV0.fetchIssueList('chromium', |
| {maxItems: 1, maxCalls: 3}); |
| await action(dispatch); |
| |
| sinon.assert.calledWith(dispatch, { |
| type: 'FETCH_ISSUE_LIST_UPDATE', |
| issues: [{localId: 1}, {localId: 2}, {localId: 3}], |
| progress: 1, |
| totalResults: 6, |
| }); |
| sinon.assert.calledWith(dispatch, {type: 'FETCH_ISSUE_LIST_SUCCESS'}); |
| }); |
| |
| it('returns progress of 1 when no totalIssues', async () => { |
| prpcCall.onFirstCall().returns({issues: [], totalResults: 0}); |
| |
| const dispatch = sinon.stub(); |
| const action = issueV0.fetchIssueList('chromium', |
| {maxItems: 1, maxCalls: 1}); |
| await action(dispatch); |
| |
| sinon.assert.calledWith(dispatch, { |
| type: 'FETCH_ISSUE_LIST_UPDATE', |
| issues: [], |
| progress: 1, |
| totalResults: 0, |
| }); |
| sinon.assert.calledWith(dispatch, {type: 'FETCH_ISSUE_LIST_SUCCESS'}); |
| }); |
| |
| it('returns progress of 1 when totalIssues undefined', async () => { |
| prpcCall.onFirstCall().returns({issues: []}); |
| |
| const dispatch = sinon.stub(); |
| const action = issueV0.fetchIssueList('chromium', |
| {maxItems: 1, maxCalls: 1}); |
| await action(dispatch); |
| |
| sinon.assert.calledWith(dispatch, { |
| type: 'FETCH_ISSUE_LIST_UPDATE', |
| issues: [], |
| progress: 1, |
| }); |
| sinon.assert.calledWith(dispatch, {type: 'FETCH_ISSUE_LIST_SUCCESS'}); |
| }); |
| |
| // TODO(kweng@) remove once crbug.com/monorail/6641 is fixed |
| it('has expected default for empty response', async () => { |
| prpcCall.onFirstCall().returns({}); |
| |
| const dispatch = sinon.stub(); |
| const action = issueV0.fetchIssueList('chromium', |
| {maxItems: 1, maxCalls: 1}); |
| await action(dispatch); |
| |
| sinon.assert.calledWith(dispatch, { |
| type: 'FETCH_ISSUE_LIST_UPDATE', |
| issues: [], |
| progress: 1, |
| totalResults: 0, |
| }); |
| sinon.assert.calledWith(dispatch, {type: 'FETCH_ISSUE_LIST_SUCCESS'}); |
| }); |
| |
| describe('federated references', () => { |
| beforeEach(() => { |
| // Preload signinImpl with a fake for testing. |
| getSigninInstance({ |
| init: sinon.stub(), |
| getUserProfileAsync: () => ( |
| Promise.resolve({ |
| getEmail: sinon.stub().returns('rutabaga@google.com'), |
| }) |
| ), |
| }); |
| window.CS_env = {gapi_client_id: 'rutabaga'}; |
| const getStub = sinon.stub().returns({ |
| execute: (cb) => cb(response), |
| }); |
| const response = { |
| result: { |
| resolvedTime: 12345, |
| issueState: { |
| title: 'Rutabaga title', |
| }, |
| }, |
| }; |
| window.gapi = { |
| client: { |
| load: (_url, _version, cb) => cb(), |
| corp_issuetracker: {issues: {get: getStub}}, |
| }, |
| }; |
| }); |
| |
| afterEach(() => { |
| delete window.CS_env; |
| delete window.gapi; |
| }); |
| |
| describe('fetchFederatedReferences', () => { |
| it('returns an empty map if no fedrefs found', async () => { |
| const dispatch = sinon.stub(); |
| const testIssue = {}; |
| const action = issueV0.fetchFederatedReferences(testIssue); |
| const result = await action(dispatch); |
| |
| assert.equal(dispatch.getCalls().length, 1); |
| sinon.assert.calledWith(dispatch, { |
| type: 'FETCH_FEDERATED_REFERENCES_START', |
| }); |
| assert.isUndefined(result); |
| }); |
| |
| it('fetches from Buganizer API', async () => { |
| const dispatch = sinon.stub(); |
| const testIssue = { |
| danglingBlockingRefs: [ |
| {extIdentifier: 'b/123456'}, |
| ], |
| danglingBlockedOnRefs: [ |
| {extIdentifier: 'b/654321'}, |
| ], |
| mergedIntoIssueRef: { |
| extIdentifier: 'b/987654', |
| }, |
| }; |
| const action = issueV0.fetchFederatedReferences(testIssue); |
| await action(dispatch); |
| |
| sinon.assert.calledWith(dispatch, { |
| type: 'FETCH_FEDERATED_REFERENCES_START', |
| }); |
| sinon.assert.calledWith(dispatch, { |
| type: 'GAPI_LOGIN_SUCCESS', |
| email: 'rutabaga@google.com', |
| }); |
| sinon.assert.calledWith(dispatch, { |
| type: 'FETCH_FEDERATED_REFERENCES_SUCCESS', |
| fedRefIssueRefs: [ |
| { |
| extIdentifier: 'b/123456', |
| statusRef: {meansOpen: false}, |
| summary: 'Rutabaga title', |
| }, |
| { |
| extIdentifier: 'b/654321', |
| statusRef: {meansOpen: false}, |
| summary: 'Rutabaga title', |
| }, |
| { |
| extIdentifier: 'b/987654', |
| statusRef: {meansOpen: false}, |
| summary: 'Rutabaga title', |
| }, |
| ], |
| }); |
| }); |
| }); |
| |
| describe('fetchRelatedIssues', () => { |
| it('calls fetchFederatedReferences for mergedinto', async () => { |
| const dispatch = sinon.stub(); |
| prpcCall.returns(Promise.resolve({openRefs: [], closedRefs: []})); |
| const testIssue = { |
| mergedIntoIssueRef: { |
| extIdentifier: 'b/987654', |
| }, |
| }; |
| const action = issueV0.fetchRelatedIssues(testIssue); |
| await action(dispatch); |
| |
| // Important: mergedinto fedref is not passed to ListReferencedIssues. |
| const expectedMessage = {issueRefs: []}; |
| sinon.assert.calledWith(prpcClient.call, 'monorail.Issues', |
| 'ListReferencedIssues', expectedMessage); |
| |
| sinon.assert.calledWith(dispatch, { |
| type: 'FETCH_RELATED_ISSUES_START', |
| }); |
| // No mergedInto refs returned, they're handled by |
| // fetchFederatedReferences. |
| sinon.assert.calledWith(dispatch, { |
| type: 'FETCH_RELATED_ISSUES_SUCCESS', |
| relatedIssues: {}, |
| }); |
| }); |
| }); |
| }); |
| }); |
| |
| describe('starring issues', () => { |
| describe('reducers', () => { |
| it('FETCH_IS_STARRED_SUCCESS updates the starredIssues object', () => { |
| const state = {}; |
| const newState = issueV0.starredIssuesReducer(state, |
| { |
| type: issueV0.FETCH_IS_STARRED_SUCCESS, |
| starred: false, |
| issueRef: { |
| projectName: 'proj', |
| localId: 1, |
| }, |
| }, |
| ); |
| assert.deepEqual(newState, {'proj:1': false}); |
| }); |
| |
| it('FETCH_ISSUES_STARRED_SUCCESS updates the starredIssues object', |
| () => { |
| const state = {}; |
| const starredIssueRefs = [{projectName: 'proj', localId: 1}, |
| {projectName: 'proj', localId: 2}]; |
| const newState = issueV0.starredIssuesReducer(state, |
| {type: issueV0.FETCH_ISSUES_STARRED_SUCCESS, starredIssueRefs}, |
| ); |
| assert.deepEqual(newState, {'proj:1': true, 'proj:2': true}); |
| }); |
| |
| it('FETCH_ISSUES_STARRED_SUCCESS does not time out with 10,000 stars', |
| () => { |
| const state = {}; |
| const starredIssueRefs = []; |
| const expected = {}; |
| for (let i = 1; i <= 10000; i++) { |
| starredIssueRefs.push({projectName: 'proj', localId: i}); |
| expected[`proj:${i}`] = true; |
| } |
| const newState = issueV0.starredIssuesReducer(state, |
| {type: issueV0.FETCH_ISSUES_STARRED_SUCCESS, starredIssueRefs}, |
| ); |
| assert.deepEqual(newState, expected); |
| }); |
| |
| it('STAR_SUCCESS updates the starredIssues object', () => { |
| const state = {'proj:1': true, 'proj:2': false}; |
| const newState = issueV0.starredIssuesReducer(state, |
| { |
| type: issueV0.STAR_SUCCESS, |
| starred: true, |
| issueRef: {projectName: 'proj', localId: 2}, |
| }); |
| assert.deepEqual(newState, {'proj:1': true, 'proj:2': true}); |
| }); |
| }); |
| |
| describe('selectors', () => { |
| describe('issue', () => { |
| const selector = issueV0.issue(wrapIssue(example.ISSUE)); |
| assert.deepEqual(selector(example.NAME), example.ISSUE); |
| }); |
| |
| describe('issueForRefString', () => { |
| const noIssues = issueV0.issueForRefString(wrapIssue({})); |
| const withIssue = issueV0.issueForRefString(wrapIssue({ |
| projectName: 'test', |
| localId: 1, |
| summary: 'hello world', |
| })); |
| |
| it('returns issue ref when no issue data', () => { |
| assert.deepEqual(noIssues('1', 'chromium'), { |
| localId: 1, |
| projectName: 'chromium', |
| }); |
| |
| assert.deepEqual(noIssues('chromium:2', 'ignore'), { |
| localId: 2, |
| projectName: 'chromium', |
| }); |
| |
| assert.deepEqual(noIssues('other:3'), { |
| localId: 3, |
| projectName: 'other', |
| }); |
| |
| assert.deepEqual(withIssue('other:3'), { |
| localId: 3, |
| projectName: 'other', |
| }); |
| }); |
| |
| it('returns full issue data when available', () => { |
| assert.deepEqual(withIssue('1', 'test'), { |
| projectName: 'test', |
| localId: 1, |
| summary: 'hello world', |
| }); |
| |
| assert.deepEqual(withIssue('test:1', 'other'), { |
| projectName: 'test', |
| localId: 1, |
| summary: 'hello world', |
| }); |
| |
| assert.deepEqual(withIssue('test:1'), { |
| projectName: 'test', |
| localId: 1, |
| summary: 'hello world', |
| }); |
| }); |
| }); |
| |
| it('starredIssues', () => { |
| const state = {issue: |
| {starredIssues: {'proj:1': true, 'proj:2': false}}}; |
| assert.deepEqual(issueV0.starredIssues(state), new Set(['proj:1'])); |
| }); |
| |
| it('starringIssues', () => { |
| const state = {issue: { |
| requests: { |
| starringIssues: { |
| 'proj:1': {requesting: true}, |
| 'proj:2': {requestin: false, error: 'unknown error'}, |
| }, |
| }, |
| }}; |
| assert.deepEqual(issueV0.starringIssues(state), new Map([ |
| ['proj:1', {requesting: true}], |
| ['proj:2', {requestin: false, error: 'unknown error'}], |
| ])); |
| }); |
| }); |
| |
| describe('action creators', () => { |
| beforeEach(() => { |
| prpcCall = sinon.stub(prpcClient, 'call'); |
| |
| dispatch = sinon.stub(); |
| }); |
| |
| afterEach(() => { |
| prpcCall.restore(); |
| }); |
| |
| it('fetching if an issue is starred', async () => { |
| const issueRef = {projectName: 'proj', localId: 1}; |
| const action = issueV0.fetchIsStarred(issueRef); |
| |
| prpcCall.returns(Promise.resolve({isStarred: true})); |
| |
| await action(dispatch); |
| |
| sinon.assert.calledWith(dispatch, |
| {type: issueV0.FETCH_IS_STARRED_START}); |
| |
| sinon.assert.calledWith( |
| prpcClient.call, 'monorail.Issues', |
| 'IsIssueStarred', {issueRef}, |
| ); |
| |
| sinon.assert.calledWith(dispatch, { |
| type: issueV0.FETCH_IS_STARRED_SUCCESS, |
| starred: true, |
| issueRef, |
| }); |
| }); |
| |
| it('fetching starred issues', async () => { |
| const returnedIssueRef = {projectName: 'proj', localId: 1}; |
| const starredIssueRefs = [returnedIssueRef]; |
| const action = issueV0.fetchStarredIssues(); |
| |
| prpcCall.returns(Promise.resolve({starredIssueRefs})); |
| |
| await action(dispatch); |
| |
| sinon.assert.calledWith(dispatch, {type: 'FETCH_ISSUES_STARRED_START'}); |
| |
| sinon.assert.calledWith( |
| prpcClient.call, 'monorail.Issues', |
| 'ListStarredIssues', {}, |
| ); |
| |
| sinon.assert.calledWith(dispatch, { |
| type: issueV0.FETCH_ISSUES_STARRED_SUCCESS, |
| starredIssueRefs, |
| }); |
| }); |
| |
| it('star', async () => { |
| const testIssue = {projectName: 'proj', localId: 1, starCount: 1}; |
| const issueRef = issueToIssueRef(testIssue); |
| const action = issueV0.star(issueRef, false); |
| |
| prpcCall.returns(Promise.resolve(testIssue)); |
| |
| await action(dispatch); |
| |
| sinon.assert.calledWith(dispatch, { |
| type: issueV0.STAR_START, |
| requestKey: 'proj:1', |
| }); |
| |
| sinon.assert.calledWith( |
| prpcClient.call, |
| 'monorail.Issues', 'StarIssue', |
| {issueRef, starred: false}, |
| ); |
| |
| sinon.assert.calledWith(dispatch, { |
| type: issueV0.STAR_SUCCESS, |
| starCount: 1, |
| issueRef, |
| starred: false, |
| requestKey: 'proj:1', |
| }); |
| }); |
| }); |
| }); |
| }); |
| |
| /** |
| * Return an initial Redux state with a given viewed |
| * @param {Issue=} viewedIssue The viewed issue. |
| * @param {Object=} otherValues Any other state values that need |
| * to be initialized. |
| * @return {Object} |
| */ |
| function wrapIssue(viewedIssue, otherValues = {}) { |
| if (!viewedIssue) { |
| return { |
| issue: { |
| issuesByRefString: {}, |
| ...otherValues, |
| }, |
| }; |
| } |
| |
| const ref = issueRefToString(viewedIssue); |
| return { |
| issue: { |
| viewedIssueRef: ref, |
| issuesByRefString: { |
| [ref]: {...viewedIssue}, |
| }, |
| ...otherValues, |
| }, |
| }; |
| } |