Commit 58b322f6 authored by Natalia Tepluhina's avatar Natalia Tepluhina

Merge branch '19411-toggling-on-off-confidentiality-and-locking-of-issues-mr' into 'master'

Toggling on/off locking of issues/mr

See merge request gitlab-org/gitlab!36773
parents fe0cd041 a51457a3
......@@ -20,6 +20,11 @@ document.addEventListener('DOMContentLoaded', () => {
noteableData.noteableType = notesDataset.noteableType;
noteableData.targetType = notesDataset.targetType;
if (noteableData.discussion_locked === null) {
// discussion_locked has never been set for this issuable.
// set to `false` for safety.
noteableData.discussion_locked = false;
}
if (parsedUserData) {
currentUserData = {
......
......@@ -13,7 +13,9 @@ import sidebarTimeTrackingEventHub from '../../sidebar/event_hub';
import { isInViewport, scrollToElement, isInMRPage } from '../../lib/utils/common_utils';
import { mergeUrlParams } from '../../lib/utils/url_utility';
import mrWidgetEventHub from '../../vue_merge_request_widget/event_hub';
import updateIssueConfidentialMutation from '~/sidebar/components/confidential/queries/update_issue_confidential.mutation.graphql';
import updateIssueConfidentialMutation from '~/sidebar/components/confidential/mutations/update_issue_confidential.mutation.graphql';
import updateMergeRequestLockMutation from '~/sidebar/components/lock/mutations/update_merge_request_lock.mutation.graphql';
import updateIssueLockMutation from '~/sidebar/components/lock/mutations/update_issue_lock.mutation.graphql';
import { __, sprintf } from '~/locale';
import Api from '~/api';
......@@ -42,6 +44,30 @@ export const updateConfidentialityOnIssue = ({ commit, getters }, { confidential
});
};
export const updateLockedAttribute = ({ commit, getters }, { locked, fullPath }) => {
const { iid, targetType } = getters.getNoteableData;
return utils.gqClient
.mutate({
mutation: targetType === 'issue' ? updateIssueLockMutation : updateMergeRequestLockMutation,
variables: {
input: {
projectPath: fullPath,
iid: String(iid),
locked,
},
},
})
.then(({ data }) => {
const discussionLocked =
targetType === 'issue'
? data.issueSetLocked.issue.discussionLocked
: data.mergeRequestSetLocked.mergeRequest.discussionLocked;
commit(types.SET_ISSUABLE_LOCK, discussionLocked);
});
};
export const expandDiscussion = ({ commit, dispatch }, data) => {
if (data.discussionId) {
dispatch('diffs/renderFileForDiscussionId', data.discussionId, { root: true });
......
......@@ -42,6 +42,7 @@ export const REOPEN_ISSUE = 'REOPEN_ISSUE';
export const TOGGLE_STATE_BUTTON_LOADING = 'TOGGLE_STATE_BUTTON_LOADING';
export const TOGGLE_BLOCKED_ISSUE_WARNING = 'TOGGLE_BLOCKED_ISSUE_WARNING';
export const SET_ISSUE_CONFIDENTIAL = 'SET_ISSUE_CONFIDENTIAL';
export const SET_ISSUABLE_LOCK = 'SET_ISSUABLE_LOCK';
// Description version
export const REQUEST_DESCRIPTION_VERSION = 'REQUEST_DESCRIPTION_VERSION';
......
......@@ -99,6 +99,10 @@ export default {
state.noteableData.confidential = data;
},
[types.SET_ISSUABLE_LOCK](state, locked) {
state.noteableData.discussion_locked = locked;
},
[types.SET_USER_DATA](state, data) {
Object.assign(state, { userData: data });
},
......
<script>
import editFormButtons from './edit_form_buttons.vue';
import issuableMixin from '../../../vue_shared/mixins/issuable';
import { __, sprintf } from '../../../locale';
export default {
components: {
editFormButtons,
},
mixins: [issuableMixin],
props: {
isLocked: {
required: true,
type: Boolean,
},
updateLockedAttribute: {
issuableDisplayName: {
required: true,
type: Function,
type: String,
},
},
computed: {
......@@ -42,12 +39,12 @@ export default {
<template>
<div class="dropdown show">
<div class="dropdown-menu sidebar-item-warning-message">
<div class="dropdown-menu sidebar-item-warning-message" data-testid="warning-text">
<p v-if="isLocked" class="text" v-html="unlockWarning"></p>
<p v-else class="text" v-html="lockWarning"></p>
<edit-form-buttons :is-locked="isLocked" :update-locked-attribute="updateLockedAttribute" />
<edit-form-buttons :is-locked="isLocked" :issuable-display-name="issuableDisplayName" />
</div>
</div>
</template>
<script>
import $ from 'jquery';
import { __ } from '~/locale';
import { GlLoadingIcon } from '@gitlab/ui';
import { __, sprintf } from '../../../locale';
import Flash from '~/flash';
import eventHub from '../../event_hub';
import { mapActions } from 'vuex';
export default {
components: {
GlLoadingIcon,
},
inject: ['fullPath'],
props: {
isLocked: {
required: true,
type: Boolean,
},
updateLockedAttribute: {
issuableDisplayName: {
required: true,
type: Function,
type: String,
},
},
data() {
return {
isLoading: false,
};
},
computed: {
buttonText() {
return this.isLocked ? __('Unlock') : __('Lock');
},
if (this.isLoading) {
return __('Applying');
}
toggleLock() {
return !this.isLocked;
return this.isLocked ? __('Unlock') : __('Lock');
},
},
methods: {
...mapActions(['updateLockedAttribute']),
closeForm() {
eventHub.$emit('closeLockForm');
$(this.$el).trigger('hidden.gl.dropdown');
},
submitForm() {
this.closeForm();
this.updateLockedAttribute(this.toggleLock);
this.isLoading = true;
this.updateLockedAttribute({
locked: !this.isLocked,
fullPath: this.fullPath,
})
.catch(() => {
const flashMessage = __(
'Something went wrong trying to change the locked state of this %{issuableDisplayName}',
);
Flash(sprintf(flashMessage, { issuableDisplayName: this.issuableDisplayName }));
})
.finally(() => {
this.closeForm();
this.isLoading = false;
});
},
},
};
......@@ -45,7 +69,14 @@ export default {
{{ __('Cancel') }}
</button>
<button type="button" class="btn btn-close" @click.prevent="submitForm">
<button
type="button"
data-testid="lock-toggle"
class="btn btn-close"
:disabled="isLoading"
@click.prevent="submitForm"
>
<gl-loading-icon v-if="isLoading" inline />
{{ buttonText }}
</button>
</div>
......
<script>
import { __, sprintf } from '~/locale';
import Flash from '~/flash';
import { __ } from '~/locale';
import tooltip from '~/vue_shared/directives/tooltip';
import issuableMixin from '~/vue_shared/mixins/issuable';
import Icon from '~/vue_shared/components/icon.vue';
import eventHub from '~/sidebar/event_hub';
import editForm from './edit_form.vue';
import { mapGetters } from 'vuex';
export default {
issue: 'issue',
locked: {
icon: 'lock',
class: 'value',
iconClass: 'is-active',
displayText: __('Locked'),
},
unlocked: {
class: ['no-value hide-collapsed'],
icon: 'lock-open',
iconClass: '',
displayText: __('Unlocked'),
},
components: {
editForm,
Icon,
......@@ -17,35 +29,28 @@ export default {
tooltip,
},
mixins: [issuableMixin],
props: {
isLocked: {
required: true,
type: Boolean,
},
isEditable: {
required: true,
type: Boolean,
},
mediator: {
required: true,
type: Object,
validator(mediatorObject) {
return mediatorObject.service && mediatorObject.service.update && mediatorObject.store;
},
},
},
data() {
return {
isLockDialogOpen: false,
};
},
computed: {
lockIcon() {
return this.isLocked ? 'lock' : 'lock-open';
...mapGetters(['getNoteableData']),
issuableDisplayName() {
const isInIssuePage = this.getNoteableData.targetType === this.$options.issue;
return isInIssuePage ? __('issue') : __('merge request');
},
isLockDialogOpen() {
return this.mediator.store.isLockDialogOpen;
isLocked() {
return this.getNoteableData.discussion_locked;
},
lockStatus() {
return this.isLocked ? this.$options.locked : this.$options.unlocked;
},
tooltipLabel() {
......@@ -64,28 +69,9 @@ export default {
methods: {
toggleForm() {
if (this.isEditable) {
this.mediator.store.isLockDialogOpen = !this.mediator.store.isLockDialogOpen;
this.isLockDialogOpen = !this.isLockDialogOpen;
}
},
updateLockedAttribute(locked) {
this.mediator.service
.update(this.issuableType, {
discussion_locked: locked,
})
.then(() => window.location.reload())
.catch(() =>
Flash(
sprintf(
__(
'Something went wrong trying to change the locked state of this %{issuableDisplayName}',
),
{
issuableDisplayName: this.issuableDisplayName,
},
),
),
);
},
},
};
</script>
......@@ -96,12 +82,13 @@ export default {
v-tooltip
:title="tooltipLabel"
class="sidebar-collapsed-icon"
data-testid="sidebar-collapse-icon"
data-container="body"
data-placement="left"
data-boundary="viewport"
@click="toggleForm"
>
<icon :name="lockIcon" class="sidebar-item-icon is-active" />
<icon :name="lockStatus.icon" class="sidebar-item-icon is-active" />
</div>
<div class="title hide-collapsed">
......@@ -110,6 +97,7 @@ export default {
v-if="isEditable"
class="float-right lock-edit"
href="#"
data-testid="edit-link"
data-track-event="click_edit_button"
data-track-label="right_sidebar"
data-track-property="lock_issue"
......@@ -122,18 +110,19 @@ export default {
<div class="value sidebar-item-value hide-collapsed">
<edit-form
v-if="isLockDialogOpen"
data-testid="edit-form"
:is-locked="isLocked"
:update-locked-attribute="updateLockedAttribute"
:issuable-type="issuableType"
:issuable-display-name="issuableDisplayName"
/>
<div v-if="isLocked" class="value sidebar-item-value">
<icon :size="16" name="lock" class="sidebar-item-icon inline is-active" />
{{ __('Locked') }}
</div>
<div v-else class="no-value sidebar-item-value hide-collapsed">
<icon :size="16" name="lock-open" class="sidebar-item-icon inline" /> {{ __('Unlocked') }}
<div data-testid="lock-status" class="sidebar-item-value" :class="lockStatus.class">
<icon
:size="16"
:name="lockStatus.icon"
class="sidebar-item-icon"
:class="lockStatus.iconClass"
/>
{{ lockStatus.displayText }}
</div>
</div>
</div>
......
mutation updateIssueLocked($input: IssueSetLockedInput!) {
issueSetLocked(input: $input) {
issue {
discussionLocked
}
errors
}
}
mutation updateMergeRequestLocked($input: MergeRequestSetLockedInput!) {
mergeRequestSetLocked(input: $input) {
mergeRequest {
discussionLocked
}
errors
}
}
......@@ -12,6 +12,7 @@ import Translate from '../vue_shared/translate';
import createDefaultClient from '~/lib/graphql';
import { store } from '~/notes/stores';
import { isInIssuePage } from '~/lib/utils/common_utils';
import mergeRequestStore from '~/mr_notes/stores';
Vue.use(Translate);
Vue.use(VueApollo);
......@@ -79,24 +80,28 @@ function mountConfidentialComponent(mediator) {
});
}
function mountLockComponent(mediator) {
function mountLockComponent() {
const el = document.getElementById('js-lock-entry-point');
if (!el) return;
const { fullPath } = getSidebarOptions();
const dataNode = document.getElementById('js-lock-issue-data');
const initialData = JSON.parse(dataNode.innerHTML);
const LockComp = Vue.extend(LockIssueSidebar);
new LockComp({
propsData: {
isLocked: initialData.is_locked,
isEditable: initialData.is_editable,
mediator,
issuableType: isInIssuePage() ? 'issue' : 'merge_request',
},
}).$mount(el);
return el
? new Vue({
el,
store: isInIssuePage() ? store : mergeRequestStore,
provide: {
fullPath,
},
render: createElement =>
createElement(LockIssueSidebar, {
props: {
isEditable: initialData.is_editable,
},
}),
})
: undefined;
}
function mountParticipantsComponent(mediator) {
......
---
title: Allow an issue or MR to be locked and unlocked without page refresh
merge_request: 36773
author:
type: changed
......@@ -19,7 +19,9 @@ import {
} from '../mock_data';
import axios from '~/lib/utils/axios_utils';
import * as utils from '~/notes/stores/utils';
import updateIssueConfidentialMutation from '~/sidebar/components/confidential/queries/update_issue_confidential.mutation.graphql';
import updateIssueConfidentialMutation from '~/sidebar/components/confidential/mutations/update_issue_confidential.mutation.graphql';
import updateMergeRequestLockMutation from '~/sidebar/components/lock/mutations/update_merge_request_lock.mutation.graphql';
import updateIssueLockMutation from '~/sidebar/components/lock/mutations/update_issue_lock.mutation.graphql';
const TEST_ERROR_MESSAGE = 'Test error message';
jest.mock('~/flash');
......@@ -1263,4 +1265,61 @@ describe('Actions Notes Store', () => {
});
});
});
describe.each`
issuableType
${'issue'} | ${'merge_request'}
`('updateLockedAttribute for issuableType=$issuableType', ({ issuableType }) => {
// Payload for mutation query
state = { noteableData: { discussion_locked: false } };
const targetType = issuableType;
const getters = { getNoteableData: { iid: '1', targetType } };
// Target state after mutation
const locked = true;
const actionArgs = { fullPath: 'full/path', locked };
const input = { iid: '1', projectPath: 'full/path', locked: true };
// Helper functions
const targetMutation = () => {
return targetType === 'issue' ? updateIssueLockMutation : updateMergeRequestLockMutation;
};
const mockResolvedValue = () => {
return targetType === 'issue'
? { data: { issueSetLocked: { issue: { discussionLocked: locked } } } }
: { data: { mergeRequestSetLocked: { mergeRequest: { discussionLocked: locked } } } };
};
beforeEach(() => {
jest.spyOn(utils.gqClient, 'mutate').mockResolvedValue(mockResolvedValue());
});
it('calls gqClient mutation one time', () => {
actions.updateLockedAttribute({ commit: () => {}, state, getters }, actionArgs);
expect(utils.gqClient.mutate).toHaveBeenCalledTimes(1);
});
it('calls gqClient mutation with the correct values', () => {
actions.updateLockedAttribute({ commit: () => {}, state, getters }, actionArgs);
expect(utils.gqClient.mutate).toHaveBeenCalledWith({
mutation: targetMutation(),
variables: { input },
});
});
describe('on success of mutation', () => {
it('calls commit with the correct values', () => {
const commitSpy = jest.fn();
return actions
.updateLockedAttribute({ commit: commitSpy, state, getters }, actionArgs)
.then(() => {
expect(commitSpy).toHaveBeenCalledWith(mutationTypes.SET_ISSUABLE_LOCK, locked);
});
});
});
});
});
......@@ -833,13 +833,27 @@ describe('Notes Store mutations', () => {
state = { noteableData: { confidential: false } };
});
it('sets sort order', () => {
it('should set issuable as confidential', () => {
mutations.SET_ISSUE_CONFIDENTIAL(state, true);
expect(state.noteableData.confidential).toBe(true);
});
});
describe('SET_ISSUABLE_LOCK', () => {
let state;
beforeEach(() => {
state = { noteableData: { discussion_locked: false } };
});
it('should set issuable as locked', () => {
mutations.SET_ISSUABLE_LOCK(state, true);
expect(state.noteableData.discussion_locked).toBe(true);
});
});
describe('UPDATE_ASSIGNEES', () => {
it('should update assignees', () => {
const state = {
......
export const ISSUABLE_TYPE_ISSUE = 'issue';
export const ISSUABLE_TYPE_MR = 'merge request';
import { shallowMount } from '@vue/test-utils';
import { GlLoadingIcon } from '@gitlab/ui';
import EditFormButtons from '~/sidebar/components/lock/edit_form_buttons.vue';
import eventHub from '~/sidebar/event_hub';
import flash from '~/flash';
import createStore from '~/notes/stores';
import { createStore as createMrStore } from '~/mr_notes/stores';
import { ISSUABLE_TYPE_ISSUE, ISSUABLE_TYPE_MR } from './constants';
jest.mock('~/sidebar/event_hub', () => ({ $emit: jest.fn() }));
jest.mock('~/flash');
describe('EditFormButtons', () => {
let wrapper;
let store;
let issuableType;
let issuableDisplayName;
const setIssuableType = pageType => {
issuableType = pageType;
issuableDisplayName = issuableType.replace(/_/g, ' ');
};
const findLockToggle = () => wrapper.find('[data-testid="lock-toggle"]');
const findGlLoadingIcon = () => wrapper.find(GlLoadingIcon);
const mountComponent = propsData => shallowMount(EditFormButtons, { propsData });
const createComponent = ({ props = {}, data = {}, resolved = true }) => {
store = issuableType === ISSUABLE_TYPE_ISSUE ? createStore() : createMrStore();
if (resolved) {
jest.spyOn(store, 'dispatch').mockResolvedValue();
} else {
jest.spyOn(store, 'dispatch').mockRejectedValue();
}
wrapper = shallowMount(EditFormButtons, {
store,
provide: {
fullPath: '',
},
propsData: {
isLocked: false,
issuableDisplayName,
...props,
},
data() {
return {
isLoading: false,
...data,
};
},
});
};
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
it('displays "Unlock" when locked', () => {
wrapper = mountComponent({
isLocked: true,
updateLockedAttribute: () => {},
describe.each`
pageType
${ISSUABLE_TYPE_ISSUE} | ${ISSUABLE_TYPE_MR}
`('In $pageType page', ({ pageType }) => {
beforeEach(() => {
setIssuableType(pageType);
});
expect(wrapper.text()).toContain('Unlock');
});
describe('when isLoading', () => {
beforeEach(() => {
createComponent({ data: { isLoading: true } });
});
it('renders "Applying" in the toggle button', () => {
expect(findLockToggle().text()).toBe('Applying');
});
it('disables the toggle button', () => {
expect(findLockToggle().attributes('disabled')).toBe('disabled');
});
it('displays "Lock" when unlocked', () => {
wrapper = mountComponent({
isLocked: false,
updateLockedAttribute: () => {},
it('displays the GlLoadingIcon', () => {
expect(findGlLoadingIcon().exists()).toBe(true);
});
});
expect(wrapper.text()).toContain('Lock');
describe.each`
isLocked | toggleText | statusText
${false} | ${'Lock'} | ${'unlocked'}
${true} | ${'Unlock'} | ${'locked'}
`('when $statusText', ({ isLocked, toggleText }) => {
beforeEach(() => {
createComponent({
props: {
isLocked,
},
});
});
it(`toggle button displays "${toggleText}"`, () => {
expect(findLockToggle().text()).toContain(toggleText);
});
describe('when toggled', () => {
describe(`when resolved`, () => {
beforeEach(() => {
createComponent({
props: {
isLocked,
},
resolved: true,
});
findLockToggle().trigger('click');
});
it('dispatches the correct action', () => {
expect(store.dispatch).toHaveBeenCalledWith('updateLockedAttribute', {
locked: !isLocked,
fullPath: '',
});
});
it('resets loading', async () => {
await wrapper.vm.$nextTick().then(() => {
expect(findGlLoadingIcon().exists()).toBe(false);
});
});
it('emits close form', () => {
return wrapper.vm.$nextTick().then(() => {
expect(eventHub.$emit).toHaveBeenCalledWith('closeLockForm');
});
});
it('does not flash an error message', () => {
expect(flash).not.toHaveBeenCalled();
});
});
describe(`when not resolved`, () => {
beforeEach(() => {
createComponent({
props: {
isLocked,
},
resolved: false,
});
findLockToggle().trigger('click');
});
it('dispatches the correct action', () => {
expect(store.dispatch).toHaveBeenCalledWith('updateLockedAttribute', {
locked: !isLocked,
fullPath: '',
});
});
it('resets loading', async () => {
await wrapper.vm.$nextTick().then(() => {
expect(findGlLoadingIcon().exists()).toBe(false);
});
});
it('emits close form', () => {
return wrapper.vm.$nextTick().then(() => {
expect(eventHub.$emit).toHaveBeenCalledWith('closeLockForm');
});
});
it('calls flash with the correct message', () => {
expect(flash).toHaveBeenCalledWith(
`Something went wrong trying to change the locked state of this ${issuableDisplayName}`,
);
});
});
});
});
});
});
import Vue from 'vue';
import editForm from '~/sidebar/components/lock/edit_form.vue';
import { shallowMount } from '@vue/test-utils';
import EditForm from '~/sidebar/components/lock/edit_form.vue';
import { ISSUABLE_TYPE_ISSUE, ISSUABLE_TYPE_MR } from './constants';
describe('EditForm', () => {
let vm1;
let vm2;
describe('Edit Form Dropdown', () => {
let wrapper;
let issuableType; // Either ISSUABLE_TYPE_ISSUE or ISSUABLE_TYPE_MR
let issuableDisplayName;
beforeEach(() => {
const Component = Vue.extend(editForm);
const toggleForm = () => {};
const updateLockedAttribute = () => {};
const setIssuableType = pageType => {
issuableType = pageType;
issuableDisplayName = issuableType.replace(/_/g, ' ');
};
vm1 = new Component({
propsData: {
isLocked: true,
toggleForm,
updateLockedAttribute,
issuableType: 'issue',
},
}).$mount();
const findWarningText = () => wrapper.find('[data-testid="warning-text"]');
vm2 = new Component({
const createComponent = ({ props }) => {
wrapper = shallowMount(EditForm, {
propsData: {
isLocked: false,
toggleForm,
updateLockedAttribute,
issuableType: 'merge_request',
issuableDisplayName,
...props,
},
}).$mount();
});
};
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
it('renders on the appropriate warning text', () => {
expect(vm1.$el.innerHTML.includes('Unlock this issue?')).toBe(true);
describe.each`
pageType
${ISSUABLE_TYPE_ISSUE} | ${ISSUABLE_TYPE_MR}
`('In $pageType page', ({ pageType }) => {
beforeEach(() => {
setIssuableType(pageType);
});
describe.each`
isLocked | lockStatusText | lockAction | warningText
${false} | ${'unlocked'} | ${'Lock'} | ${'Only project members will be able to comment.'}
${true} | ${'locked'} | ${'Unlock'} | ${'Everyone will be able to comment.'}
`('when $lockStatusText', ({ isLocked, lockAction, warningText }) => {
beforeEach(() => {
createComponent({ props: { isLocked } });
});
expect(vm2.$el.innerHTML.includes('Lock this merge request?')).toBe(true);
it(`the appropriate warning text is rendered`, () => {
expect(findWarningText().text()).toContain(
`${lockAction} this ${issuableDisplayName}? ${warningText}`,
);
});
});
});
});
import Vue from 'vue';
import { shallowMount } from '@vue/test-utils';
import { mockTracking, triggerEvent } from 'helpers/tracking_helper';
import lockIssueSidebar from '~/sidebar/components/lock/lock_issue_sidebar.vue';
import LockIssueSidebar from '~/sidebar/components/lock/lock_issue_sidebar.vue';
import EditForm from '~/sidebar/components/lock/edit_form.vue';
import createStore from '~/notes/stores';
import { createStore as createMrStore } from '~/mr_notes/stores';
import { ISSUABLE_TYPE_ISSUE, ISSUABLE_TYPE_MR } from './constants';
describe('LockIssueSidebar', () => {
let vm1;
let vm2;
beforeEach(() => {
const Component = Vue.extend(lockIssueSidebar);
const mediator = {
let wrapper;
let store;
let mediator;
let issuableType; // Either ISSUABLE_TYPE_ISSUE or ISSUABLE_TYPE_MR
const setIssuableType = pageType => {
issuableType = pageType;
};
const findSidebarCollapseIcon = () => wrapper.find('[data-testid="sidebar-collapse-icon"]');
const findLockStatus = () => wrapper.find('[data-testid="lock-status"]');
const findEditLink = () => wrapper.find('[data-testid="edit-link"]');
const findEditForm = () => wrapper.find(EditForm);
const initMediator = () => {
mediator = {
service: {
update: Promise.resolve(true),
},
store: {
isLockDialogOpen: false,
},
store: {},
};
vm1 = new Component({
};
const initStore = isLocked => {
if (issuableType === ISSUABLE_TYPE_ISSUE) {
store = createStore();
store.getters.getNoteableData.targetType = 'issue';
} else {
store = createMrStore();
}
store.getters.getNoteableData.discussion_locked = isLocked;
};
const createComponent = ({ props = {} }) => {
wrapper = shallowMount(LockIssueSidebar, {
store,
propsData: {
isLocked: true,
isEditable: true,
mediator,
issuableType: 'issue',
...props,
},
}).$mount();
vm2 = new Component({
propsData: {
isLocked: false,
isEditable: false,
mediator,
issuableType: 'merge_request',
},
}).$mount();
});
it('shows if locked and/or editable', () => {
expect(vm1.$el.innerHTML.includes('Edit')).toBe(true);
expect(vm1.$el.innerHTML.includes('Locked')).toBe(true);
expect(vm2.$el.innerHTML.includes('Unlocked')).toBe(true);
});
it('displays the edit form when editable', done => {
expect(vm1.isLockDialogOpen).toBe(false);
vm1.$el.querySelector('.lock-edit').click();
expect(vm1.isLockDialogOpen).toBe(true);
vm1.$nextTick(() => {
expect(vm1.$el.innerHTML.includes('Unlock this issue?')).toBe(true);
done();
});
});
it('tracks an event when "Edit" is clicked', () => {
const spy = mockTracking('_category_', vm1.$el, jest.spyOn);
triggerEvent('.lock-edit');
};
expect(spy).toHaveBeenCalledWith('_category_', 'click_edit_button', {
label: 'right_sidebar',
property: 'lock_issue',
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
it('displays the edit form when opened from collapsed state', done => {
expect(vm1.isLockDialogOpen).toBe(false);
vm1.$el.querySelector('.sidebar-collapsed-icon').click();
expect(vm1.isLockDialogOpen).toBe(true);
setImmediate(() => {
expect(vm1.$el.innerHTML.includes('Unlock this issue?')).toBe(true);
done();
describe.each`
pageType
${ISSUABLE_TYPE_ISSUE} | ${ISSUABLE_TYPE_MR}
`('In $pageType page', ({ pageType }) => {
beforeEach(() => {
setIssuableType(pageType);
initMediator();
});
});
it('does not display the edit form when opened from collapsed state if not editable', done => {
expect(vm2.isLockDialogOpen).toBe(false);
vm2.$el.querySelector('.sidebar-collapsed-icon').click();
Vue.nextTick()
.then(() => {
expect(vm2.isLockDialogOpen).toBe(false);
})
.then(done)
.catch(done.fail);
describe.each`
isLocked
${false} | ${true}
`(`renders for isLocked = $isLocked`, ({ isLocked }) => {
beforeEach(() => {
initStore(isLocked);
createComponent({});
});
it('shows the lock status', () => {
expect(findLockStatus().text()).toBe(isLocked ? 'Locked' : 'Unlocked');
});
describe('edit form', () => {
let isEditable;
beforeEach(() => {
isEditable = false;
createComponent({ props: { isEditable } });
});
describe('when not editable', () => {
it('does not display the edit form when opened if not editable', () => {
expect(findEditForm().exists()).toBe(false);
findSidebarCollapseIcon().trigger('click');
return wrapper.vm.$nextTick().then(() => {
expect(findEditForm().exists()).toBe(false);
});
});
});
describe('when editable', () => {
beforeEach(() => {
isEditable = true;
createComponent({ props: { isEditable } });
});
it('shows the editable status', () => {
expect(findEditLink().exists()).toBe(isEditable);
expect(findEditLink().text()).toBe('Edit');
});
describe("when 'Edit' is clicked", () => {
it('displays the edit form when editable', () => {
expect(findEditForm().exists()).toBe(false);
findEditLink().trigger('click');
return wrapper.vm.$nextTick().then(() => {
expect(findEditForm().exists()).toBe(true);
});
});
it('tracks the event ', () => {
const spy = mockTracking('_category_', wrapper.element, jest.spyOn);
triggerEvent(findEditLink().element);
expect(spy).toHaveBeenCalledWith('_category_', 'click_edit_button', {
label: 'right_sidebar',
property: 'lock_issue',
});
});
});
describe('When sidebar is collapsed', () => {
it('displays the edit form when opened', () => {
expect(findEditForm().exists()).toBe(false);
findSidebarCollapseIcon().trigger('click');
return wrapper.vm.$nextTick().then(() => {
expect(findEditForm().exists()).toBe(true);
});
});
});
});
});
});
});
});
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment