2022-04-13 21:09:57 +00:00
|
|
|
import { nextTick } from 'vue';
|
2022-06-16 21:09:48 +00:00
|
|
|
import { GlButton, GlSprintf } from '@gitlab/ui';
|
2021-02-14 18:09:20 +00:00
|
|
|
import { shallowMount } from '@vue/test-utils';
|
2022-10-17 12:10:08 +00:00
|
|
|
import { createAlert } from '~/flash';
|
2020-07-17 06:09:11 +00:00
|
|
|
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');
|
|
|
|
|
2022-06-16 21:09:48 +00:00
|
|
|
const RULE_NAME = 'first_rule';
|
2020-07-17 06:09:11 +00:00
|
|
|
const TEST_HELP_PATH = 'help/path';
|
2020-12-23 21:10:24 +00:00
|
|
|
const testApprovedBy = () => [1, 7, 10].map((id) => ({ id }));
|
2020-07-17 06:09:11 +00:00
|
|
|
const testApprovals = () => ({
|
|
|
|
approved: false,
|
2020-12-23 21:10:24 +00:00
|
|
|
approved_by: testApprovedBy().map((user) => ({ user })),
|
2020-07-17 06:09:11 +00:00
|
|
|
approval_rules_left: [],
|
|
|
|
approvals_left: 4,
|
|
|
|
suggested_approvers: [],
|
|
|
|
user_can_approve: true,
|
|
|
|
user_has_approved: true,
|
|
|
|
require_password_to_approve: false,
|
2022-06-16 21:09:48 +00:00
|
|
|
invalid_approvers_rules: [],
|
2020-07-17 06:09:11 +00:00
|
|
|
});
|
|
|
|
const testApprovalRulesResponse = () => ({ rules: [{ id: 2 }] });
|
|
|
|
|
|
|
|
describe('MRWidget approvals', () => {
|
|
|
|
let wrapper;
|
|
|
|
let service;
|
|
|
|
let mr;
|
|
|
|
|
|
|
|
const createComponent = (props = {}) => {
|
|
|
|
wrapper = shallowMount(Approvals, {
|
|
|
|
propsData: {
|
|
|
|
mr,
|
|
|
|
service,
|
|
|
|
...props,
|
|
|
|
},
|
2022-06-16 21:09:48 +00:00
|
|
|
stubs: {
|
|
|
|
GlSprintf,
|
|
|
|
},
|
2020-07-17 06:09:11 +00:00
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2022-09-21 15:14:09 +00:00
|
|
|
const findAction = () => wrapper.findComponent(GlButton);
|
2020-07-17 06:09:11 +00:00
|
|
|
const findActionData = () => {
|
|
|
|
const action = findAction();
|
|
|
|
|
|
|
|
return !action.exists()
|
|
|
|
? null
|
|
|
|
: {
|
|
|
|
variant: action.props('variant'),
|
|
|
|
category: action.props('category'),
|
|
|
|
text: action.text(),
|
|
|
|
};
|
|
|
|
};
|
2022-09-21 15:14:09 +00:00
|
|
|
const findSummary = () => wrapper.findComponent(ApprovalsSummary);
|
|
|
|
const findOptionalSummary = () => wrapper.findComponent(ApprovalsSummaryOptional);
|
2022-06-16 21:09:48 +00:00
|
|
|
const findInvalidRules = () => wrapper.find('[data-testid="invalid-rules"]');
|
2020-07-17 06:09:11 +00:00
|
|
|
|
|
|
|
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', () => {
|
2021-12-29 15:10:45 +00:00
|
|
|
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
|
|
|
|
// eslint-disable-next-line no-restricted-syntax
|
2020-07-17 06:09:11 +00:00
|
|
|
wrapper.setData({ fetchingApprovals: true });
|
|
|
|
|
2022-04-13 21:09:57 +00:00
|
|
|
return nextTick().then(() => {
|
2020-07-17 06:09:11 +00:00
|
|
|
expect(wrapper.text()).toContain(FETCH_LOADING);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
it('fetches approvals', () => {
|
|
|
|
expect(service.fetchApprovals).toHaveBeenCalled();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('when fetch approvals error', () => {
|
2022-04-13 21:09:57 +00:00
|
|
|
beforeEach(() => {
|
2020-07-17 06:09:11 +00:00
|
|
|
jest.spyOn(service, 'fetchApprovals').mockReturnValue(Promise.reject());
|
|
|
|
createComponent();
|
2022-04-13 21:09:57 +00:00
|
|
|
return nextTick();
|
2020-07-17 06:09:11 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
it('still shows loading message', () => {
|
|
|
|
expect(wrapper.text()).toContain(FETCH_LOADING);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('flashes error', () => {
|
2022-10-17 12:10:08 +00:00
|
|
|
expect(createAlert).toHaveBeenCalledWith({ message: FETCH_ERROR });
|
2020-07-17 06:09:11 +00:00
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('action button', () => {
|
|
|
|
describe('when mr is closed', () => {
|
2022-04-13 21:09:57 +00:00
|
|
|
beforeEach(() => {
|
2020-07-17 06:09:11 +00:00
|
|
|
mr.isOpen = false;
|
|
|
|
mr.approvals.user_has_approved = false;
|
|
|
|
mr.approvals.user_can_approve = true;
|
|
|
|
|
|
|
|
createComponent();
|
2022-04-13 21:09:57 +00:00
|
|
|
return nextTick();
|
2020-07-17 06:09:11 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
it('action is not rendered', () => {
|
|
|
|
expect(findActionData()).toBe(null);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('when user cannot approve', () => {
|
2022-04-13 21:09:57 +00:00
|
|
|
beforeEach(() => {
|
2020-07-17 06:09:11 +00:00
|
|
|
mr.approvals.user_has_approved = false;
|
|
|
|
mr.approvals.user_can_approve = false;
|
|
|
|
|
|
|
|
createComponent();
|
2022-04-13 21:09:57 +00:00
|
|
|
return nextTick();
|
2020-07-17 06:09:11 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
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', () => {
|
2022-04-13 21:09:57 +00:00
|
|
|
beforeEach(() => {
|
2020-07-17 06:09:11 +00:00
|
|
|
createComponent();
|
2022-04-13 21:09:57 +00:00
|
|
|
return nextTick();
|
2020-07-17 06:09:11 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
it('approve action is rendered', () => {
|
|
|
|
expect(findActionData()).toEqual({
|
2022-06-13 21:09:12 +00:00
|
|
|
variant: 'confirm',
|
2020-07-17 06:09:11 +00:00
|
|
|
text: 'Approve',
|
|
|
|
category: 'primary',
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('and MR is approved', () => {
|
|
|
|
beforeEach(() => {
|
|
|
|
mr.approvals.approved = true;
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('with no approvers', () => {
|
2022-04-13 21:09:57 +00:00
|
|
|
beforeEach(() => {
|
2020-07-17 06:09:11 +00:00
|
|
|
mr.approvals.approved_by = [];
|
|
|
|
createComponent();
|
2022-04-13 21:09:57 +00:00
|
|
|
return nextTick();
|
2020-07-17 06:09:11 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
it('approve action (with inverted style) is rendered', () => {
|
|
|
|
expect(findActionData()).toEqual({
|
2022-06-13 21:09:12 +00:00
|
|
|
variant: 'confirm',
|
2020-07-17 06:09:11 +00:00
|
|
|
text: 'Approve',
|
|
|
|
category: 'secondary',
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('with approvers', () => {
|
2022-04-13 21:09:57 +00:00
|
|
|
beforeEach(() => {
|
2020-07-17 06:09:11 +00:00
|
|
|
mr.approvals.approved_by = [{ user: { id: 7 } }];
|
|
|
|
createComponent();
|
2022-04-13 21:09:57 +00:00
|
|
|
return nextTick();
|
2020-07-17 06:09:11 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
it('approve additionally action is rendered', () => {
|
|
|
|
expect(findActionData()).toEqual({
|
2022-06-13 21:09:12 +00:00
|
|
|
variant: 'confirm',
|
2020-07-17 06:09:11 +00:00
|
|
|
text: 'Approve additionally',
|
|
|
|
category: 'secondary',
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('when approve action is clicked', () => {
|
2022-04-13 21:09:57 +00:00
|
|
|
beforeEach(() => {
|
2020-07-17 06:09:11 +00:00
|
|
|
createComponent();
|
2022-04-13 21:09:57 +00:00
|
|
|
return nextTick();
|
2020-07-17 06:09:11 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
it('shows loading icon', () => {
|
|
|
|
jest.spyOn(service, 'approveMergeRequest').mockReturnValue(new Promise(() => {}));
|
|
|
|
const action = findAction();
|
|
|
|
|
|
|
|
expect(action.props('loading')).toBe(false);
|
|
|
|
|
|
|
|
action.vm.$emit('click');
|
|
|
|
|
2022-04-13 21:09:57 +00:00
|
|
|
return nextTick().then(() => {
|
2020-07-17 06:09:11 +00:00
|
|
|
expect(action.props('loading')).toBe(true);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('and after loading', () => {
|
2022-04-13 21:09:57 +00:00
|
|
|
beforeEach(() => {
|
2020-07-17 06:09:11 +00:00
|
|
|
findAction().vm.$emit('click');
|
2022-04-13 21:09:57 +00:00
|
|
|
return nextTick();
|
2020-07-17 06:09:11 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
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', () => {
|
2022-04-13 21:09:57 +00:00
|
|
|
beforeEach(() => {
|
2020-07-17 06:09:11 +00:00
|
|
|
jest.spyOn(service, 'approveMergeRequest').mockReturnValue(Promise.reject());
|
|
|
|
findAction().vm.$emit('click');
|
2022-04-13 21:09:57 +00:00
|
|
|
return nextTick();
|
2020-07-17 06:09:11 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
it('flashes error message', () => {
|
2022-10-17 12:10:08 +00:00
|
|
|
expect(createAlert).toHaveBeenCalledWith({ message: APPROVE_ERROR });
|
2020-07-17 06:09:11 +00:00
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('when user has approved', () => {
|
2022-04-13 21:09:57 +00:00
|
|
|
beforeEach(() => {
|
2020-07-17 06:09:11 +00:00
|
|
|
mr.approvals.user_has_approved = true;
|
|
|
|
mr.approvals.user_can_approve = false;
|
|
|
|
|
|
|
|
createComponent();
|
2022-04-13 21:09:57 +00:00
|
|
|
return nextTick();
|
2020-07-17 06:09:11 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
it('revoke action is rendered', () => {
|
|
|
|
expect(findActionData()).toEqual({
|
2022-05-30 12:08:23 +00:00
|
|
|
category: 'primary',
|
|
|
|
variant: 'default',
|
2020-07-17 06:09:11 +00:00
|
|
|
text: 'Revoke approval',
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('when revoke action is clicked', () => {
|
|
|
|
describe('and successful', () => {
|
2022-04-13 21:09:57 +00:00
|
|
|
beforeEach(() => {
|
2020-07-17 06:09:11 +00:00
|
|
|
findAction().vm.$emit('click');
|
2022-04-13 21:09:57 +00:00
|
|
|
return nextTick();
|
2020-07-17 06:09:11 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
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', () => {
|
2022-04-13 21:09:57 +00:00
|
|
|
beforeEach(() => {
|
2020-07-17 06:09:11 +00:00
|
|
|
jest.spyOn(service, 'unapproveMergeRequest').mockReturnValue(Promise.reject());
|
|
|
|
findAction().vm.$emit('click');
|
2022-04-13 21:09:57 +00:00
|
|
|
return nextTick();
|
2020-07-17 06:09:11 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
it('flashes error message', () => {
|
2022-10-17 12:10:08 +00:00
|
|
|
expect(createAlert).toHaveBeenCalledWith({ message: UNAPPROVE_ERROR });
|
2020-07-17 06:09:11 +00:00
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
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', () => {
|
2022-04-13 21:09:57 +00:00
|
|
|
beforeEach(() => {
|
2020-07-17 06:09:11 +00:00
|
|
|
mr.approvals.user_can_approve = true;
|
|
|
|
|
|
|
|
createComponent();
|
2022-04-13 21:09:57 +00:00
|
|
|
return nextTick();
|
2020-07-17 06:09:11 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
it('is shown', () => {
|
|
|
|
expect(findSummary().exists()).toBe(false);
|
|
|
|
expect(findOptionalSummary().props()).toEqual({
|
|
|
|
canApprove: true,
|
|
|
|
helpPath: TEST_HELP_PATH,
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('and cannot approve', () => {
|
2022-04-13 21:09:57 +00:00
|
|
|
beforeEach(() => {
|
2020-07-17 06:09:11 +00:00
|
|
|
mr.approvals.user_can_approve = false;
|
|
|
|
|
|
|
|
createComponent();
|
2022-04-13 21:09:57 +00:00
|
|
|
return nextTick();
|
2020-07-17 06:09:11 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
it('is shown', () => {
|
|
|
|
expect(findSummary().exists()).toBe(false);
|
|
|
|
expect(findOptionalSummary().props()).toEqual({
|
|
|
|
canApprove: false,
|
|
|
|
helpPath: TEST_HELP_PATH,
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('approvals summary', () => {
|
2022-04-13 21:09:57 +00:00
|
|
|
beforeEach(() => {
|
2020-07-17 06:09:11 +00:00
|
|
|
createComponent();
|
2022-04-13 21:09:57 +00:00
|
|
|
return nextTick();
|
2020-07-17 06:09:11 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
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(),
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
2022-06-16 21:09:48 +00:00
|
|
|
|
|
|
|
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.');
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
2020-07-17 06:09:11 +00:00
|
|
|
});
|