diff options
Diffstat (limited to 'spec/frontend/vue_merge_request_widget/components/approvals/approvals_spec.js')
-rw-r--r-- | spec/frontend/vue_merge_request_widget/components/approvals/approvals_spec.js | 424 |
1 files changed, 424 insertions, 0 deletions
diff --git a/spec/frontend/vue_merge_request_widget/components/approvals/approvals_spec.js b/spec/frontend/vue_merge_request_widget/components/approvals/approvals_spec.js new file mode 100644 index 00000000000..05cd1bb5b3d --- /dev/null +++ b/spec/frontend/vue_merge_request_widget/components/approvals/approvals_spec.js @@ -0,0 +1,424 @@ +import { nextTick } from 'vue'; +import { GlButton, GlSprintf } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import createFlash from '~/flash'; +import Approvals from '~/vue_merge_request_widget/components/approvals/approvals.vue'; +import ApprovalsSummary from '~/vue_merge_request_widget/components/approvals/approvals_summary.vue'; +import ApprovalsSummaryOptional from '~/vue_merge_request_widget/components/approvals/approvals_summary_optional.vue'; +import { + FETCH_LOADING, + FETCH_ERROR, + APPROVE_ERROR, + UNAPPROVE_ERROR, +} from '~/vue_merge_request_widget/components/approvals/messages'; +import eventHub from '~/vue_merge_request_widget/event_hub'; + +jest.mock('~/flash'); + +const RULE_NAME = 'first_rule'; +const TEST_HELP_PATH = 'help/path'; +const testApprovedBy = () => [1, 7, 10].map((id) => ({ id })); +const testApprovals = () => ({ + approved: false, + approved_by: testApprovedBy().map((user) => ({ user })), + approval_rules_left: [], + approvals_left: 4, + suggested_approvers: [], + user_can_approve: true, + user_has_approved: true, + require_password_to_approve: false, + invalid_approvers_rules: [], +}); +const testApprovalRulesResponse = () => ({ rules: [{ id: 2 }] }); + +describe('MRWidget approvals', () => { + let wrapper; + let service; + let mr; + + const createComponent = (props = {}) => { + wrapper = shallowMount(Approvals, { + propsData: { + mr, + service, + ...props, + }, + stubs: { + GlSprintf, + }, + }); + }; + + const findAction = () => wrapper.find(GlButton); + const findActionData = () => { + const action = findAction(); + + return !action.exists() + ? null + : { + variant: action.props('variant'), + category: action.props('category'), + text: action.text(), + }; + }; + const findSummary = () => wrapper.find(ApprovalsSummary); + const findOptionalSummary = () => wrapper.find(ApprovalsSummaryOptional); + const findInvalidRules = () => wrapper.find('[data-testid="invalid-rules"]'); + + beforeEach(() => { + service = { + ...{ + fetchApprovals: jest.fn().mockReturnValue(Promise.resolve(testApprovals())), + fetchApprovalSettings: jest + .fn() + .mockReturnValue(Promise.resolve(testApprovalRulesResponse())), + approveMergeRequest: jest.fn().mockReturnValue(Promise.resolve(testApprovals())), + unapproveMergeRequest: jest.fn().mockReturnValue(Promise.resolve(testApprovals())), + approveMergeRequestWithAuth: jest.fn().mockReturnValue(Promise.resolve(testApprovals())), + }, + }; + mr = { + ...{ + setApprovals: jest.fn(), + setApprovalRules: jest.fn(), + }, + approvalsHelpPath: TEST_HELP_PATH, + approvals: testApprovals(), + approvalRules: [], + isOpen: true, + state: 'open', + }; + + jest.spyOn(eventHub, '$emit').mockImplementation(() => {}); + }); + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + describe('when created', () => { + beforeEach(() => { + createComponent(); + }); + + it('shows loading message', () => { + // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details + // eslint-disable-next-line no-restricted-syntax + wrapper.setData({ fetchingApprovals: true }); + + return nextTick().then(() => { + expect(wrapper.text()).toContain(FETCH_LOADING); + }); + }); + + it('fetches approvals', () => { + expect(service.fetchApprovals).toHaveBeenCalled(); + }); + }); + + describe('when fetch approvals error', () => { + beforeEach(() => { + jest.spyOn(service, 'fetchApprovals').mockReturnValue(Promise.reject()); + createComponent(); + return nextTick(); + }); + + it('still shows loading message', () => { + expect(wrapper.text()).toContain(FETCH_LOADING); + }); + + it('flashes error', () => { + expect(createFlash).toHaveBeenCalledWith({ message: FETCH_ERROR }); + }); + }); + + describe('action button', () => { + describe('when mr is closed', () => { + beforeEach(() => { + mr.isOpen = false; + mr.approvals.user_has_approved = false; + mr.approvals.user_can_approve = true; + + createComponent(); + return nextTick(); + }); + + it('action is not rendered', () => { + expect(findActionData()).toBe(null); + }); + }); + + describe('when user cannot approve', () => { + beforeEach(() => { + mr.approvals.user_has_approved = false; + mr.approvals.user_can_approve = false; + + createComponent(); + return nextTick(); + }); + + it('action is not rendered', () => { + expect(findActionData()).toBe(null); + }); + }); + + describe('when user can approve', () => { + beforeEach(() => { + mr.approvals.user_has_approved = false; + mr.approvals.user_can_approve = true; + }); + + describe('and MR is unapproved', () => { + beforeEach(() => { + createComponent(); + return nextTick(); + }); + + it('approve action is rendered', () => { + expect(findActionData()).toEqual({ + variant: 'confirm', + text: 'Approve', + category: 'primary', + }); + }); + }); + + describe('and MR is approved', () => { + beforeEach(() => { + mr.approvals.approved = true; + }); + + describe('with no approvers', () => { + beforeEach(() => { + mr.approvals.approved_by = []; + createComponent(); + return nextTick(); + }); + + it('approve action (with inverted style) is rendered', () => { + expect(findActionData()).toEqual({ + variant: 'confirm', + text: 'Approve', + category: 'secondary', + }); + }); + }); + + describe('with approvers', () => { + beforeEach(() => { + mr.approvals.approved_by = [{ user: { id: 7 } }]; + createComponent(); + return nextTick(); + }); + + it('approve additionally action is rendered', () => { + expect(findActionData()).toEqual({ + variant: 'confirm', + text: 'Approve additionally', + category: 'secondary', + }); + }); + }); + }); + + describe('when approve action is clicked', () => { + beforeEach(() => { + createComponent(); + return nextTick(); + }); + + it('shows loading icon', () => { + jest.spyOn(service, 'approveMergeRequest').mockReturnValue(new Promise(() => {})); + const action = findAction(); + + expect(action.props('loading')).toBe(false); + + action.vm.$emit('click'); + + return nextTick().then(() => { + expect(action.props('loading')).toBe(true); + }); + }); + + describe('and after loading', () => { + beforeEach(() => { + findAction().vm.$emit('click'); + return nextTick(); + }); + + it('calls service approve', () => { + expect(service.approveMergeRequest).toHaveBeenCalled(); + }); + + it('emits to eventHub', () => { + expect(eventHub.$emit).toHaveBeenCalledWith('MRWidgetUpdateRequested'); + }); + + it('calls store setApprovals', () => { + expect(mr.setApprovals).toHaveBeenCalledWith(testApprovals()); + }); + }); + + describe('and error', () => { + beforeEach(() => { + jest.spyOn(service, 'approveMergeRequest').mockReturnValue(Promise.reject()); + findAction().vm.$emit('click'); + return nextTick(); + }); + + it('flashes error message', () => { + expect(createFlash).toHaveBeenCalledWith({ message: APPROVE_ERROR }); + }); + }); + }); + }); + + describe('when user has approved', () => { + beforeEach(() => { + mr.approvals.user_has_approved = true; + mr.approvals.user_can_approve = false; + + createComponent(); + return nextTick(); + }); + + it('revoke action is rendered', () => { + expect(findActionData()).toEqual({ + category: 'primary', + variant: 'default', + text: 'Revoke approval', + }); + }); + + describe('when revoke action is clicked', () => { + describe('and successful', () => { + beforeEach(() => { + findAction().vm.$emit('click'); + return nextTick(); + }); + + it('calls service unapprove', () => { + expect(service.unapproveMergeRequest).toHaveBeenCalled(); + }); + + it('emits to eventHub', () => { + expect(eventHub.$emit).toHaveBeenCalledWith('MRWidgetUpdateRequested'); + }); + + it('calls store setApprovals', () => { + expect(mr.setApprovals).toHaveBeenCalledWith(testApprovals()); + }); + }); + + describe('and error', () => { + beforeEach(() => { + jest.spyOn(service, 'unapproveMergeRequest').mockReturnValue(Promise.reject()); + findAction().vm.$emit('click'); + return nextTick(); + }); + + it('flashes error message', () => { + expect(createFlash).toHaveBeenCalledWith({ message: UNAPPROVE_ERROR }); + }); + }); + }); + }); + }); + + describe('approvals optional summary', () => { + describe('when no approvals required and no approvers', () => { + beforeEach(() => { + mr.approvals.approved_by = []; + mr.approvals.approvals_required = 0; + mr.approvals.user_has_approved = false; + }); + + describe('and can approve', () => { + beforeEach(() => { + mr.approvals.user_can_approve = true; + + createComponent(); + return nextTick(); + }); + + it('is shown', () => { + expect(findSummary().exists()).toBe(false); + expect(findOptionalSummary().props()).toEqual({ + canApprove: true, + helpPath: TEST_HELP_PATH, + }); + }); + }); + + describe('and cannot approve', () => { + beforeEach(() => { + mr.approvals.user_can_approve = false; + + createComponent(); + return nextTick(); + }); + + it('is shown', () => { + expect(findSummary().exists()).toBe(false); + expect(findOptionalSummary().props()).toEqual({ + canApprove: false, + helpPath: TEST_HELP_PATH, + }); + }); + }); + }); + }); + + describe('approvals summary', () => { + beforeEach(() => { + createComponent(); + return nextTick(); + }); + + it('is rendered with props', () => { + const expected = testApprovals(); + const summary = findSummary(); + + expect(findOptionalSummary().exists()).toBe(false); + expect(summary.exists()).toBe(true); + expect(summary.props()).toMatchObject({ + approvalsLeft: expected.approvals_left, + rulesLeft: expected.approval_rules_left, + approvers: testApprovedBy(), + }); + }); + }); + + describe('invalid rules', () => { + beforeEach(() => { + mr.approvals.merge_request_approvers_available = true; + createComponent(); + }); + + it('does not render related components', () => { + expect(findInvalidRules().exists()).toBe(false); + }); + + describe('when invalid rules are present', () => { + beforeEach(() => { + mr.approvals.invalid_approvers_rules = [{ name: RULE_NAME }]; + createComponent(); + }); + + it('renders related components', () => { + const invalidRules = findInvalidRules(); + + expect(invalidRules.exists()).toBe(true); + + const invalidRulesText = invalidRules.text(); + + expect(invalidRulesText).toContain(RULE_NAME); + expect(invalidRulesText).toContain( + 'GitLab has approved this rule automatically to unblock the merge request.', + ); + expect(invalidRulesText).toContain('Learn more.'); + }); + }); + }); +}); |