summaryrefslogtreecommitdiff
path: root/spec/frontend/sidebar
diff options
context:
space:
mode:
Diffstat (limited to 'spec/frontend/sidebar')
-rw-r--r--spec/frontend/sidebar/__snapshots__/confidential_issue_sidebar_spec.js.snap12
-rw-r--r--spec/frontend/sidebar/assignees_realtime_spec.js102
-rw-r--r--spec/frontend/sidebar/components/time_tracking/time_tracker_spec.js279
-rw-r--r--spec/frontend/sidebar/confidential/edit_form_buttons_spec.js41
-rw-r--r--spec/frontend/sidebar/confidential/edit_form_spec.js45
-rw-r--r--spec/frontend/sidebar/confidential_edit_buttons_spec.js35
-rw-r--r--spec/frontend/sidebar/confidential_edit_form_buttons_spec.js35
-rw-r--r--spec/frontend/sidebar/confidential_issue_sidebar_spec.js25
-rw-r--r--spec/frontend/sidebar/lock/edit_form_buttons_spec.js31
-rw-r--r--spec/frontend/sidebar/lock/lock_issue_sidebar_spec.js99
-rw-r--r--spec/frontend/sidebar/participants_spec.js206
-rw-r--r--spec/frontend/sidebar/sidebar_assignees_spec.js46
-rw-r--r--spec/frontend/sidebar/sidebar_mediator_spec.js135
-rw-r--r--spec/frontend/sidebar/sidebar_move_issue_spec.js167
-rw-r--r--spec/frontend/sidebar/sidebar_subscriptions_spec.js36
-rw-r--r--spec/frontend/sidebar/subscriptions_spec.js106
16 files changed, 1308 insertions, 92 deletions
diff --git a/spec/frontend/sidebar/__snapshots__/confidential_issue_sidebar_spec.js.snap b/spec/frontend/sidebar/__snapshots__/confidential_issue_sidebar_spec.js.snap
index 1f93336e755..cf7832f3948 100644
--- a/spec/frontend/sidebar/__snapshots__/confidential_issue_sidebar_spec.js.snap
+++ b/spec/frontend/sidebar/__snapshots__/confidential_issue_sidebar_spec.js.snap
@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`Confidential Issue Sidebar Block renders for isConfidential = false and isEditable = false 1`] = `
+exports[`Confidential Issue Sidebar Block renders for confidential = false and isEditable = false 1`] = `
<div
class="block issuable-sidebar-item confidentiality"
>
@@ -52,7 +52,7 @@ exports[`Confidential Issue Sidebar Block renders for isConfidential = false and
</div>
`;
-exports[`Confidential Issue Sidebar Block renders for isConfidential = false and isEditable = true 1`] = `
+exports[`Confidential Issue Sidebar Block renders for confidential = false and isEditable = true 1`] = `
<div
class="block issuable-sidebar-item confidentiality"
>
@@ -84,9 +84,7 @@ exports[`Confidential Issue Sidebar Block renders for isConfidential = false and
data-track-property="confidentiality"
href="#"
>
-
Edit
-
</a>
</div>
@@ -114,7 +112,7 @@ exports[`Confidential Issue Sidebar Block renders for isConfidential = false and
</div>
`;
-exports[`Confidential Issue Sidebar Block renders for isConfidential = true and isEditable = false 1`] = `
+exports[`Confidential Issue Sidebar Block renders for confidential = true and isEditable = false 1`] = `
<div
class="block issuable-sidebar-item confidentiality"
>
@@ -166,7 +164,7 @@ exports[`Confidential Issue Sidebar Block renders for isConfidential = true and
</div>
`;
-exports[`Confidential Issue Sidebar Block renders for isConfidential = true and isEditable = true 1`] = `
+exports[`Confidential Issue Sidebar Block renders for confidential = true and isEditable = true 1`] = `
<div
class="block issuable-sidebar-item confidentiality"
>
@@ -198,9 +196,7 @@ exports[`Confidential Issue Sidebar Block renders for isConfidential = true and
data-track-property="confidentiality"
href="#"
>
-
Edit
-
</a>
</div>
diff --git a/spec/frontend/sidebar/assignees_realtime_spec.js b/spec/frontend/sidebar/assignees_realtime_spec.js
new file mode 100644
index 00000000000..1c62c52dc67
--- /dev/null
+++ b/spec/frontend/sidebar/assignees_realtime_spec.js
@@ -0,0 +1,102 @@
+import { shallowMount } from '@vue/test-utils';
+import ActionCable from '@rails/actioncable';
+import AssigneesRealtime from '~/sidebar/components/assignees/assignees_realtime.vue';
+import SidebarMediator from '~/sidebar/sidebar_mediator';
+import Mock from './mock_data';
+import query from '~/issuable_sidebar/queries/issue_sidebar.query.graphql';
+
+jest.mock('@rails/actioncable', () => {
+ const mockConsumer = {
+ subscriptions: { create: jest.fn().mockReturnValue({ unsubscribe: jest.fn() }) },
+ };
+ return {
+ createConsumer: jest.fn().mockReturnValue(mockConsumer),
+ };
+});
+
+describe('Assignees Realtime', () => {
+ let wrapper;
+ let mediator;
+
+ const createComponent = () => {
+ wrapper = shallowMount(AssigneesRealtime, {
+ propsData: {
+ issuableIid: '1',
+ mediator,
+ projectPath: 'path/to/project',
+ },
+ mocks: {
+ $apollo: {
+ query,
+ queries: {
+ project: {
+ refetch: jest.fn(),
+ },
+ },
+ },
+ },
+ });
+ };
+
+ beforeEach(() => {
+ mediator = new SidebarMediator(Mock.mediator);
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ SidebarMediator.singleton = null;
+ });
+
+ describe('when handleFetchResult is called from smart query', () => {
+ it('sets assignees to the store', () => {
+ const data = {
+ project: {
+ issue: {
+ assignees: {
+ nodes: [{ id: 'gid://gitlab/Environments/123', avatarUrl: 'url' }],
+ },
+ },
+ },
+ };
+ const expected = [{ id: 123, avatar_url: 'url', avatarUrl: 'url' }];
+ createComponent();
+
+ wrapper.vm.handleFetchResult({ data });
+
+ expect(mediator.store.assignees).toEqual(expected);
+ });
+ });
+
+ describe('when mounted', () => {
+ it('calls create subscription', () => {
+ const cable = ActionCable.createConsumer();
+
+ createComponent();
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(cable.subscriptions.create).toHaveBeenCalledTimes(1);
+ expect(cable.subscriptions.create).toHaveBeenCalledWith(
+ {
+ channel: 'IssuesChannel',
+ iid: wrapper.props('issuableIid'),
+ project_path: wrapper.props('projectPath'),
+ },
+ { received: wrapper.vm.received },
+ );
+ });
+ });
+ });
+
+ describe('when subscription is recieved', () => {
+ it('refetches the GraphQL project query', () => {
+ createComponent();
+
+ wrapper.vm.received({ event: 'updated' });
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(wrapper.vm.$apollo.queries.project.refetch).toHaveBeenCalledTimes(1);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/sidebar/components/time_tracking/time_tracker_spec.js b/spec/frontend/sidebar/components/time_tracking/time_tracker_spec.js
new file mode 100644
index 00000000000..1f028f74423
--- /dev/null
+++ b/spec/frontend/sidebar/components/time_tracking/time_tracker_spec.js
@@ -0,0 +1,279 @@
+import Vue from 'vue';
+
+import mountComponent from 'helpers/vue_mount_component_helper';
+import TimeTracker from '~/sidebar/components/time_tracking/time_tracker.vue';
+
+describe('Issuable Time Tracker', () => {
+ let initialData;
+ let vm;
+
+ const initTimeTrackingComponent = ({
+ timeEstimate,
+ timeSpent,
+ timeEstimateHumanReadable,
+ timeSpentHumanReadable,
+ limitToHours,
+ }) => {
+ setFixtures(`
+ <div>
+ <div id="mock-container"></div>
+ </div>
+ `);
+
+ initialData = {
+ timeEstimate,
+ timeSpent,
+ humanTimeEstimate: timeEstimateHumanReadable,
+ humanTimeSpent: timeSpentHumanReadable,
+ limitToHours: Boolean(limitToHours),
+ rootPath: '/',
+ };
+
+ const TimeTrackingComponent = Vue.extend({
+ ...TimeTracker,
+ components: {
+ ...TimeTracker.components,
+ transition: {
+ // disable animations
+ render(h) {
+ return h('div', this.$slots.default);
+ },
+ },
+ },
+ });
+ vm = mountComponent(TimeTrackingComponent, initialData, '#mock-container');
+ };
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('Initialization', () => {
+ beforeEach(() => {
+ initTimeTrackingComponent({
+ timeEstimate: 10000, // 2h 46m
+ timeSpent: 5000, // 1h 23m
+ timeEstimateHumanReadable: '2h 46m',
+ timeSpentHumanReadable: '1h 23m',
+ });
+ });
+
+ it('should return something defined', () => {
+ expect(vm).toBeDefined();
+ });
+
+ it('should correctly set timeEstimate', done => {
+ Vue.nextTick(() => {
+ expect(vm.timeEstimate).toBe(initialData.timeEstimate);
+ done();
+ });
+ });
+
+ it('should correctly set time_spent', done => {
+ Vue.nextTick(() => {
+ expect(vm.timeSpent).toBe(initialData.timeSpent);
+ done();
+ });
+ });
+ });
+
+ describe('Content Display', () => {
+ describe('Panes', () => {
+ describe('Comparison pane', () => {
+ beforeEach(() => {
+ initTimeTrackingComponent({
+ timeEstimate: 100000, // 1d 3h
+ timeSpent: 5000, // 1h 23m
+ timeEstimateHumanReadable: '1d 3h',
+ timeSpentHumanReadable: '1h 23m',
+ });
+ });
+
+ it('should show the "Comparison" pane when timeEstimate and time_spent are truthy', done => {
+ Vue.nextTick(() => {
+ expect(vm.showComparisonState).toBe(true);
+ const $comparisonPane = vm.$el.querySelector('.time-tracking-comparison-pane');
+
+ expect($comparisonPane).toBeVisible();
+ done();
+ });
+ });
+
+ it('should show full times when the sidebar is collapsed', done => {
+ Vue.nextTick(() => {
+ const timeTrackingText = vm.$el.querySelector('.time-tracking-collapsed-summary span')
+ .textContent;
+
+ expect(timeTrackingText.trim()).toBe('1h 23m / 1d 3h');
+ done();
+ });
+ });
+
+ describe('Remaining meter', () => {
+ it('should display the remaining meter with the correct width', done => {
+ Vue.nextTick(() => {
+ expect(
+ vm.$el.querySelector('.time-tracking-comparison-pane .progress[value="5"]'),
+ ).not.toBeNull();
+ done();
+ });
+ });
+
+ it('should display the remaining meter with the correct background color when within estimate', done => {
+ Vue.nextTick(() => {
+ expect(
+ vm.$el.querySelector('.time-tracking-comparison-pane .progress[variant="primary"]'),
+ ).not.toBeNull();
+ done();
+ });
+ });
+
+ it('should display the remaining meter with the correct background color when over estimate', done => {
+ vm.timeEstimate = 10000; // 2h 46m
+ vm.timeSpent = 20000000; // 231 days
+ Vue.nextTick(() => {
+ expect(
+ vm.$el.querySelector('.time-tracking-comparison-pane .progress[variant="danger"]'),
+ ).not.toBeNull();
+ done();
+ });
+ });
+ });
+ });
+
+ describe('Comparison pane when limitToHours is true', () => {
+ beforeEach(() => {
+ initTimeTrackingComponent({
+ timeEstimate: 100000, // 1d 3h
+ timeSpent: 5000, // 1h 23m
+ timeEstimateHumanReadable: '',
+ timeSpentHumanReadable: '',
+ limitToHours: true,
+ });
+ });
+
+ it('should show the correct tooltip text', done => {
+ Vue.nextTick(() => {
+ expect(vm.showComparisonState).toBe(true);
+ const $title = vm.$el.querySelector('.time-tracking-content .compare-meter').dataset
+ .originalTitle;
+
+ expect($title).toBe('Time remaining: 26h 23m');
+ done();
+ });
+ });
+ });
+
+ describe('Estimate only pane', () => {
+ beforeEach(() => {
+ initTimeTrackingComponent({
+ timeEstimate: 10000, // 2h 46m
+ timeSpent: 0,
+ timeEstimateHumanReadable: '2h 46m',
+ timeSpentHumanReadable: '',
+ });
+ });
+
+ it('should display the human readable version of time estimated', done => {
+ Vue.nextTick(() => {
+ const estimateText = vm.$el.querySelector('.time-tracking-estimate-only-pane')
+ .textContent;
+ const correctText = 'Estimated: 2h 46m';
+
+ expect(estimateText.trim()).toBe(correctText);
+ done();
+ });
+ });
+ });
+
+ describe('Spent only pane', () => {
+ beforeEach(() => {
+ initTimeTrackingComponent({
+ timeEstimate: 0,
+ timeSpent: 5000, // 1h 23m
+ timeEstimateHumanReadable: '2h 46m',
+ timeSpentHumanReadable: '1h 23m',
+ });
+ });
+
+ it('should display the human readable version of time spent', done => {
+ Vue.nextTick(() => {
+ const spentText = vm.$el.querySelector('.time-tracking-spend-only-pane').textContent;
+ const correctText = 'Spent: 1h 23m';
+
+ expect(spentText).toBe(correctText);
+ done();
+ });
+ });
+ });
+
+ describe('No time tracking pane', () => {
+ beforeEach(() => {
+ initTimeTrackingComponent({
+ timeEstimate: 0,
+ timeSpent: 0,
+ timeEstimateHumanReadable: '',
+ timeSpentHumanReadable: '',
+ });
+ });
+
+ it('should only show the "No time tracking" pane when both timeEstimate and time_spent are falsey', done => {
+ Vue.nextTick(() => {
+ const $noTrackingPane = vm.$el.querySelector('.time-tracking-no-tracking-pane');
+ const noTrackingText = $noTrackingPane.textContent;
+ const correctText = 'No estimate or time spent';
+
+ expect(vm.showNoTimeTrackingState).toBe(true);
+ expect($noTrackingPane).toBeVisible();
+ expect(noTrackingText.trim()).toBe(correctText);
+ done();
+ });
+ });
+ });
+
+ describe('Help pane', () => {
+ const helpButton = () => vm.$el.querySelector('.help-button');
+ const closeHelpButton = () => vm.$el.querySelector('.close-help-button');
+ const helpPane = () => vm.$el.querySelector('.time-tracking-help-state');
+
+ beforeEach(() => {
+ initTimeTrackingComponent({ timeEstimate: 0, timeSpent: 0 });
+
+ return vm.$nextTick();
+ });
+
+ it('should not show the "Help" pane by default', () => {
+ expect(vm.showHelpState).toBe(false);
+ expect(helpPane()).toBeNull();
+ });
+
+ it('should show the "Help" pane when help button is clicked', () => {
+ helpButton().click();
+
+ return vm.$nextTick().then(() => {
+ expect(vm.showHelpState).toBe(true);
+
+ // let animations run
+ jest.advanceTimersByTime(500);
+
+ expect(helpPane()).toBeVisible();
+ });
+ });
+
+ it('should not show the "Help" pane when help button is clicked and then closed', done => {
+ helpButton().click();
+
+ Vue.nextTick()
+ .then(() => closeHelpButton().click())
+ .then(() => Vue.nextTick())
+ .then(() => {
+ expect(vm.showHelpState).toBe(false);
+ expect(helpPane()).toBeNull();
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/sidebar/confidential/edit_form_buttons_spec.js b/spec/frontend/sidebar/confidential/edit_form_buttons_spec.js
new file mode 100644
index 00000000000..acdfb5139bf
--- /dev/null
+++ b/spec/frontend/sidebar/confidential/edit_form_buttons_spec.js
@@ -0,0 +1,41 @@
+import { shallowMount } from '@vue/test-utils';
+import EditFormButtons from '~/sidebar/components/confidential/edit_form_buttons.vue';
+
+describe('Edit Form Buttons', () => {
+ let wrapper;
+ const findConfidentialToggle = () => wrapper.find('[data-testid="confidential-toggle"]');
+
+ const createComponent = props => {
+ wrapper = shallowMount(EditFormButtons, {
+ propsData: {
+ updateConfidentialAttribute: () => {},
+ ...props,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe('when not confidential', () => {
+ it('renders Turn On in the ', () => {
+ createComponent({
+ isConfidential: false,
+ });
+
+ expect(findConfidentialToggle().text()).toBe('Turn On');
+ });
+ });
+
+ describe('when confidential', () => {
+ it('renders on or off text based on confidentiality', () => {
+ createComponent({
+ isConfidential: true,
+ });
+
+ expect(findConfidentialToggle().text()).toBe('Turn Off');
+ });
+ });
+});
diff --git a/spec/frontend/sidebar/confidential/edit_form_spec.js b/spec/frontend/sidebar/confidential/edit_form_spec.js
new file mode 100644
index 00000000000..137019a1e1b
--- /dev/null
+++ b/spec/frontend/sidebar/confidential/edit_form_spec.js
@@ -0,0 +1,45 @@
+import { shallowMount } from '@vue/test-utils';
+import EditForm from '~/sidebar/components/confidential/edit_form.vue';
+
+describe('Edit Form Dropdown', () => {
+ let wrapper;
+ const toggleForm = () => {};
+ const updateConfidentialAttribute = () => {};
+
+ const createComponent = props => {
+ wrapper = shallowMount(EditForm, {
+ propsData: {
+ ...props,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe('when not confidential', () => {
+ it('renders "You are going to turn off the confidentiality." in the ', () => {
+ createComponent({
+ isConfidential: false,
+ toggleForm,
+ updateConfidentialAttribute,
+ });
+
+ expect(wrapper.find('p').text()).toContain('You are going to turn on the confidentiality.');
+ });
+ });
+
+ describe('when confidential', () => {
+ it('renders on or off text based on confidentiality', () => {
+ createComponent({
+ isConfidential: true,
+ toggleForm,
+ updateConfidentialAttribute,
+ });
+
+ expect(wrapper.find('p').text()).toContain('You are going to turn off the confidentiality.');
+ });
+ });
+});
diff --git a/spec/frontend/sidebar/confidential_edit_buttons_spec.js b/spec/frontend/sidebar/confidential_edit_buttons_spec.js
deleted file mode 100644
index 32da9f83112..00000000000
--- a/spec/frontend/sidebar/confidential_edit_buttons_spec.js
+++ /dev/null
@@ -1,35 +0,0 @@
-import Vue from 'vue';
-import editFormButtons from '~/sidebar/components/confidential/edit_form_buttons.vue';
-
-describe('Edit Form Buttons', () => {
- let vm1;
- let vm2;
-
- beforeEach(() => {
- const Component = Vue.extend(editFormButtons);
- const toggleForm = () => {};
- const updateConfidentialAttribute = () => {};
-
- vm1 = new Component({
- propsData: {
- isConfidential: true,
- toggleForm,
- updateConfidentialAttribute,
- },
- }).$mount();
-
- vm2 = new Component({
- propsData: {
- isConfidential: false,
- toggleForm,
- updateConfidentialAttribute,
- },
- }).$mount();
- });
-
- it('renders on or off text based on confidentiality', () => {
- expect(vm1.$el.innerHTML.includes('Turn Off')).toBe(true);
-
- expect(vm2.$el.innerHTML.includes('Turn On')).toBe(true);
- });
-});
diff --git a/spec/frontend/sidebar/confidential_edit_form_buttons_spec.js b/spec/frontend/sidebar/confidential_edit_form_buttons_spec.js
deleted file mode 100644
index 369088cb258..00000000000
--- a/spec/frontend/sidebar/confidential_edit_form_buttons_spec.js
+++ /dev/null
@@ -1,35 +0,0 @@
-import Vue from 'vue';
-import editForm from '~/sidebar/components/confidential/edit_form.vue';
-
-describe('Edit Form Dropdown', () => {
- let vm1;
- let vm2;
-
- beforeEach(() => {
- const Component = Vue.extend(editForm);
- const toggleForm = () => {};
- const updateConfidentialAttribute = () => {};
-
- vm1 = new Component({
- propsData: {
- isConfidential: true,
- toggleForm,
- updateConfidentialAttribute,
- },
- }).$mount();
-
- vm2 = new Component({
- propsData: {
- isConfidential: false,
- toggleForm,
- updateConfidentialAttribute,
- },
- }).$mount();
- });
-
- it('renders on the appropriate warning text', () => {
- expect(vm1.$el.innerHTML.includes('You are going to turn off the confidentiality.')).toBe(true);
-
- expect(vm2.$el.innerHTML.includes('You are going to turn on the confidentiality.')).toBe(true);
- });
-});
diff --git a/spec/frontend/sidebar/confidential_issue_sidebar_spec.js b/spec/frontend/sidebar/confidential_issue_sidebar_spec.js
index 4853d9795b1..e7a64ec5ed9 100644
--- a/spec/frontend/sidebar/confidential_issue_sidebar_spec.js
+++ b/spec/frontend/sidebar/confidential_issue_sidebar_spec.js
@@ -5,6 +5,7 @@ import EditForm from '~/sidebar/components/confidential/edit_form.vue';
import SidebarService from '~/sidebar/services/sidebar_service';
import createFlash from '~/flash';
import RecaptchaModal from '~/vue_shared/components/recaptcha_modal.vue';
+import createStore from '~/notes/stores';
jest.mock('~/flash');
jest.mock('~/sidebar/services/sidebar_service');
@@ -31,8 +32,10 @@ describe('Confidential Issue Sidebar Block', () => {
};
const createComponent = propsData => {
+ const store = createStore();
const service = new SidebarService();
wrapper = shallowMount(ConfidentialIssueSidebar, {
+ store,
propsData: {
service,
...propsData,
@@ -49,29 +52,31 @@ describe('Confidential Issue Sidebar Block', () => {
});
it.each`
- isConfidential | isEditable
- ${false} | ${false}
- ${false} | ${true}
- ${true} | ${false}
- ${true} | ${true}
+ confidential | isEditable
+ ${false} | ${false}
+ ${false} | ${true}
+ ${true} | ${false}
+ ${true} | ${true}
`(
- 'renders for isConfidential = $isConfidential and isEditable = $isEditable',
- ({ isConfidential, isEditable }) => {
+ 'renders for confidential = $confidential and isEditable = $isEditable',
+ ({ confidential, isEditable }) => {
createComponent({
- isConfidential,
isEditable,
});
+ wrapper.vm.$store.state.noteableData.confidential = confidential;
- expect(wrapper.element).toMatchSnapshot();
+ return wrapper.vm.$nextTick().then(() => {
+ expect(wrapper.element).toMatchSnapshot();
+ });
},
);
describe('if editable', () => {
beforeEach(() => {
createComponent({
- isConfidential: true,
isEditable: true,
});
+ wrapper.vm.$store.state.noteableData.confidential = true;
});
it('displays the edit form when editable', () => {
diff --git a/spec/frontend/sidebar/lock/edit_form_buttons_spec.js b/spec/frontend/sidebar/lock/edit_form_buttons_spec.js
new file mode 100644
index 00000000000..66f9237ce97
--- /dev/null
+++ b/spec/frontend/sidebar/lock/edit_form_buttons_spec.js
@@ -0,0 +1,31 @@
+import { shallowMount } from '@vue/test-utils';
+import EditFormButtons from '~/sidebar/components/lock/edit_form_buttons.vue';
+
+describe('EditFormButtons', () => {
+ let wrapper;
+
+ const mountComponent = propsData => shallowMount(EditFormButtons, { propsData });
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ it('displays "Unlock" when locked', () => {
+ wrapper = mountComponent({
+ isLocked: true,
+ updateLockedAttribute: () => {},
+ });
+
+ expect(wrapper.text()).toContain('Unlock');
+ });
+
+ it('displays "Lock" when unlocked', () => {
+ wrapper = mountComponent({
+ isLocked: false,
+ updateLockedAttribute: () => {},
+ });
+
+ expect(wrapper.text()).toContain('Lock');
+ });
+});
diff --git a/spec/frontend/sidebar/lock/lock_issue_sidebar_spec.js b/spec/frontend/sidebar/lock/lock_issue_sidebar_spec.js
new file mode 100644
index 00000000000..00997326d87
--- /dev/null
+++ b/spec/frontend/sidebar/lock/lock_issue_sidebar_spec.js
@@ -0,0 +1,99 @@
+import Vue from 'vue';
+import { mockTracking, triggerEvent } from 'helpers/tracking_helper';
+import lockIssueSidebar from '~/sidebar/components/lock/lock_issue_sidebar.vue';
+
+describe('LockIssueSidebar', () => {
+ let vm1;
+ let vm2;
+
+ beforeEach(() => {
+ const Component = Vue.extend(lockIssueSidebar);
+
+ const mediator = {
+ service: {
+ update: Promise.resolve(true),
+ },
+
+ store: {
+ isLockDialogOpen: false,
+ },
+ };
+
+ vm1 = new Component({
+ propsData: {
+ isLocked: true,
+ isEditable: true,
+ mediator,
+ issuableType: 'issue',
+ },
+ }).$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',
+ });
+ });
+
+ 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();
+ });
+ });
+
+ 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);
+ });
+});
diff --git a/spec/frontend/sidebar/participants_spec.js b/spec/frontend/sidebar/participants_spec.js
new file mode 100644
index 00000000000..ebe94582588
--- /dev/null
+++ b/spec/frontend/sidebar/participants_spec.js
@@ -0,0 +1,206 @@
+import { GlLoadingIcon } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
+import Participants from '~/sidebar/components/participants/participants.vue';
+
+const PARTICIPANT = {
+ id: 1,
+ state: 'active',
+ username: 'marcene',
+ name: 'Allie Will',
+ web_url: 'foo.com',
+ avatar_url: 'gravatar.com/avatar/xxx',
+};
+
+const PARTICIPANT_LIST = [PARTICIPANT, { ...PARTICIPANT, id: 2 }, { ...PARTICIPANT, id: 3 }];
+
+describe('Participants', () => {
+ let wrapper;
+
+ const getMoreParticipantsButton = () => wrapper.find('button');
+
+ const getCollapsedParticipantsCount = () => wrapper.find('[data-testid="collapsed-count"]');
+
+ const mountComponent = propsData =>
+ shallowMount(Participants, {
+ propsData,
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe('collapsed sidebar state', () => {
+ it('shows loading spinner when loading', () => {
+ wrapper = mountComponent({
+ loading: true,
+ });
+
+ expect(wrapper.contains(GlLoadingIcon)).toBe(true);
+ });
+
+ it('does not show loading spinner not loading', () => {
+ wrapper = mountComponent({
+ loading: false,
+ });
+
+ expect(wrapper.contains(GlLoadingIcon)).toBe(false);
+ });
+
+ it('shows participant count when given', () => {
+ wrapper = mountComponent({
+ loading: false,
+ participants: PARTICIPANT_LIST,
+ });
+
+ expect(getCollapsedParticipantsCount().text()).toBe(`${PARTICIPANT_LIST.length}`);
+ });
+
+ it('shows full participant count when there are hidden participants', () => {
+ wrapper = mountComponent({
+ loading: false,
+ participants: PARTICIPANT_LIST,
+ numberOfLessParticipants: 1,
+ });
+
+ expect(getCollapsedParticipantsCount().text()).toBe(`${PARTICIPANT_LIST.length}`);
+ });
+ });
+
+ describe('expanded sidebar state', () => {
+ it('shows loading spinner when loading', () => {
+ wrapper = mountComponent({
+ loading: true,
+ });
+
+ expect(wrapper.contains(GlLoadingIcon)).toBe(true);
+ });
+
+ it('when only showing visible participants, shows an avatar only for each participant under the limit', () => {
+ const numberOfLessParticipants = 2;
+ wrapper = mountComponent({
+ loading: false,
+ participants: PARTICIPANT_LIST,
+ numberOfLessParticipants,
+ });
+
+ wrapper.setData({
+ isShowingMoreParticipants: false,
+ });
+
+ return Vue.nextTick().then(() => {
+ expect(wrapper.findAll('.participants-author')).toHaveLength(numberOfLessParticipants);
+ });
+ });
+
+ it('when only showing all participants, each has an avatar', () => {
+ wrapper = mountComponent({
+ loading: false,
+ participants: PARTICIPANT_LIST,
+ numberOfLessParticipants: 2,
+ });
+
+ wrapper.setData({
+ isShowingMoreParticipants: true,
+ });
+
+ return Vue.nextTick().then(() => {
+ expect(wrapper.findAll('.participants-author')).toHaveLength(PARTICIPANT_LIST.length);
+ });
+ });
+
+ it('does not have more participants link when they can all be shown', () => {
+ const numberOfLessParticipants = 100;
+ wrapper = mountComponent({
+ loading: false,
+ participants: PARTICIPANT_LIST,
+ numberOfLessParticipants,
+ });
+
+ expect(PARTICIPANT_LIST.length).toBeLessThan(numberOfLessParticipants);
+ expect(getMoreParticipantsButton().exists()).toBe(false);
+ });
+
+ it('when too many participants, has more participants link to show more', () => {
+ wrapper = mountComponent({
+ loading: false,
+ participants: PARTICIPANT_LIST,
+ numberOfLessParticipants: 2,
+ });
+
+ wrapper.setData({
+ isShowingMoreParticipants: false,
+ });
+
+ return Vue.nextTick().then(() => {
+ expect(getMoreParticipantsButton().text()).toBe('+ 1 more');
+ });
+ });
+
+ it('when too many participants and already showing them, has more participants link to show less', () => {
+ wrapper = mountComponent({
+ loading: false,
+ participants: PARTICIPANT_LIST,
+ numberOfLessParticipants: 2,
+ });
+
+ wrapper.setData({
+ isShowingMoreParticipants: true,
+ });
+
+ return Vue.nextTick().then(() => {
+ expect(getMoreParticipantsButton().text()).toBe('- show less');
+ });
+ });
+
+ it('clicking more participants link emits event', () => {
+ wrapper = mountComponent({
+ loading: false,
+ participants: PARTICIPANT_LIST,
+ numberOfLessParticipants: 2,
+ });
+
+ expect(wrapper.vm.isShowingMoreParticipants).toBe(false);
+
+ getMoreParticipantsButton().trigger('click');
+
+ expect(wrapper.vm.isShowingMoreParticipants).toBe(true);
+ });
+
+ it('clicking on participants icon emits `toggleSidebar` event', () => {
+ wrapper = mountComponent({
+ loading: false,
+ participants: PARTICIPANT_LIST,
+ numberOfLessParticipants: 2,
+ });
+
+ const spy = jest.spyOn(wrapper.vm, '$emit');
+
+ wrapper.find('.sidebar-collapsed-icon').trigger('click');
+
+ return Vue.nextTick(() => {
+ expect(spy).toHaveBeenCalledWith('toggleSidebar');
+
+ spy.mockRestore();
+ });
+ });
+ });
+
+ describe('when not showing participants label', () => {
+ beforeEach(() => {
+ wrapper = mountComponent({
+ participants: PARTICIPANT_LIST,
+ showParticipantLabel: false,
+ });
+ });
+
+ it('does not show sidebar collapsed icon', () => {
+ expect(wrapper.contains('.sidebar-collapsed-icon')).toBe(false);
+ });
+
+ it('does not show participants label title', () => {
+ expect(wrapper.contains('.title')).toBe(false);
+ });
+ });
+});
diff --git a/spec/frontend/sidebar/sidebar_assignees_spec.js b/spec/frontend/sidebar/sidebar_assignees_spec.js
index c1876066a21..88e2d2c9514 100644
--- a/spec/frontend/sidebar/sidebar_assignees_spec.js
+++ b/spec/frontend/sidebar/sidebar_assignees_spec.js
@@ -3,6 +3,7 @@ import AxiosMockAdapter from 'axios-mock-adapter';
import axios from 'axios';
import SidebarAssignees from '~/sidebar/components/assignees/sidebar_assignees.vue';
import Assigness from '~/sidebar/components/assignees/assignees.vue';
+import AssigneesRealtime from '~/sidebar/components/assignees/assignees_realtime.vue';
import SidebarMediator from '~/sidebar/sidebar_mediator';
import SidebarService from '~/sidebar/services/sidebar_service';
import SidebarStore from '~/sidebar/stores/sidebar_store';
@@ -12,12 +13,19 @@ describe('sidebar assignees', () => {
let wrapper;
let mediator;
let axiosMock;
-
- const createComponent = () => {
+ const createComponent = (realTimeIssueSidebar = false, props) => {
wrapper = shallowMount(SidebarAssignees, {
propsData: {
+ issuableIid: '1',
mediator,
field: '',
+ projectPath: 'projectPath',
+ ...props,
+ },
+ provide: {
+ glFeatures: {
+ realTimeIssueSidebar,
+ },
},
// Attaching to document is required because this component emits something from the parent element :/
attachToDocument: true,
@@ -30,8 +38,6 @@ describe('sidebar assignees', () => {
jest.spyOn(mediator, 'saveAssignees');
jest.spyOn(mediator, 'assignYourself');
-
- createComponent();
});
afterEach(() => {
@@ -45,6 +51,8 @@ describe('sidebar assignees', () => {
});
it('calls the mediator when saves the assignees', () => {
+ createComponent();
+
expect(mediator.saveAssignees).not.toHaveBeenCalled();
wrapper.vm.saveAssignees();
@@ -53,6 +61,8 @@ describe('sidebar assignees', () => {
});
it('calls the mediator when "assignSelf" method is called', () => {
+ createComponent();
+
expect(mediator.assignYourself).not.toHaveBeenCalled();
expect(mediator.store.assignees.length).toBe(0);
@@ -63,6 +73,8 @@ describe('sidebar assignees', () => {
});
it('hides assignees until fetched', () => {
+ createComponent();
+
expect(wrapper.find(Assigness).exists()).toBe(false);
wrapper.vm.store.isFetching.assignees = false;
@@ -71,4 +83,30 @@ describe('sidebar assignees', () => {
expect(wrapper.find(Assigness).exists()).toBe(true);
});
});
+
+ describe('when realTimeIssueSidebar is turned on', () => {
+ describe('when issuableType is issue', () => {
+ it('finds AssigneesRealtime componeont', () => {
+ createComponent(true);
+
+ expect(wrapper.find(AssigneesRealtime).exists()).toBe(true);
+ });
+ });
+
+ describe('when issuableType is MR', () => {
+ it('does not find AssigneesRealtime componeont', () => {
+ createComponent(true, { issuableType: 'MR' });
+
+ expect(wrapper.find(AssigneesRealtime).exists()).toBe(false);
+ });
+ });
+ });
+
+ describe('when realTimeIssueSidebar is turned off', () => {
+ it('does not find AssigneesRealtime', () => {
+ createComponent(false, { issuableType: 'issue' });
+
+ expect(wrapper.find(AssigneesRealtime).exists()).toBe(false);
+ });
+ });
});
diff --git a/spec/frontend/sidebar/sidebar_mediator_spec.js b/spec/frontend/sidebar/sidebar_mediator_spec.js
new file mode 100644
index 00000000000..0892d452966
--- /dev/null
+++ b/spec/frontend/sidebar/sidebar_mediator_spec.js
@@ -0,0 +1,135 @@
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
+import * as urlUtility from '~/lib/utils/url_utility';
+import SidebarService, { gqClient } from '~/sidebar/services/sidebar_service';
+import SidebarMediator from '~/sidebar/sidebar_mediator';
+import SidebarStore from '~/sidebar/stores/sidebar_store';
+import Mock from './mock_data';
+
+describe('Sidebar mediator', () => {
+ const { mediator: mediatorMockData } = Mock;
+ let mock;
+ let mediator;
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ mediator = new SidebarMediator(mediatorMockData);
+ });
+
+ afterEach(() => {
+ SidebarService.singleton = null;
+ SidebarStore.singleton = null;
+ SidebarMediator.singleton = null;
+ mock.restore();
+ });
+
+ it('assigns yourself ', () => {
+ mediator.assignYourself();
+
+ expect(mediator.store.currentUser).toEqual(mediatorMockData.currentUser);
+ expect(mediator.store.assignees[0]).toEqual(mediatorMockData.currentUser);
+ });
+
+ it('saves assignees', () => {
+ mock.onPut(mediatorMockData.endpoint).reply(200, {});
+
+ return mediator.saveAssignees('issue[assignee_ids]').then(resp => {
+ expect(resp.status).toEqual(200);
+ });
+ });
+
+ it('fetches the data', () => {
+ const mockData = Mock.responseMap.GET[mediatorMockData.endpoint];
+ mock.onGet(mediatorMockData.endpoint).reply(200, mockData);
+
+ const mockGraphQlData = Mock.graphQlResponseData;
+ const graphQlSpy = jest.spyOn(gqClient, 'query').mockReturnValue({
+ data: mockGraphQlData,
+ });
+ const spy = jest.spyOn(mediator, 'processFetchedData').mockReturnValue(Promise.resolve());
+
+ return mediator.fetch().then(() => {
+ expect(spy).toHaveBeenCalledWith(mockData, mockGraphQlData);
+
+ spy.mockRestore();
+ graphQlSpy.mockRestore();
+ });
+ });
+
+ it('processes fetched data', () => {
+ const mockData = Mock.responseMap.GET[mediatorMockData.endpoint];
+ mediator.processFetchedData(mockData);
+
+ expect(mediator.store.assignees).toEqual(mockData.assignees);
+ expect(mediator.store.humanTimeEstimate).toEqual(mockData.human_time_estimate);
+ expect(mediator.store.humanTotalTimeSpent).toEqual(mockData.human_total_time_spent);
+ expect(mediator.store.participants).toEqual(mockData.participants);
+ expect(mediator.store.subscribed).toEqual(mockData.subscribed);
+ expect(mediator.store.timeEstimate).toEqual(mockData.time_estimate);
+ expect(mediator.store.totalTimeSpent).toEqual(mockData.total_time_spent);
+ });
+
+ it('sets moveToProjectId', () => {
+ const projectId = 7;
+ const spy = jest.spyOn(mediator.store, 'setMoveToProjectId').mockReturnValue(Promise.resolve());
+
+ mediator.setMoveToProjectId(projectId);
+
+ expect(spy).toHaveBeenCalledWith(projectId);
+
+ spy.mockRestore();
+ });
+
+ it('fetches autocomplete projects', () => {
+ const searchTerm = 'foo';
+ mock.onGet(mediatorMockData.projectsAutocompleteEndpoint).reply(200, {});
+ const getterSpy = jest
+ .spyOn(mediator.service, 'getProjectsAutocomplete')
+ .mockReturnValue(Promise.resolve({ data: {} }));
+ const setterSpy = jest
+ .spyOn(mediator.store, 'setAutocompleteProjects')
+ .mockReturnValue(Promise.resolve());
+
+ return mediator.fetchAutocompleteProjects(searchTerm).then(() => {
+ expect(getterSpy).toHaveBeenCalledWith(searchTerm);
+ expect(setterSpy).toHaveBeenCalled();
+
+ getterSpy.mockRestore();
+ setterSpy.mockRestore();
+ });
+ });
+
+ it('moves issue', () => {
+ const mockData = Mock.responseMap.POST[mediatorMockData.moveIssueEndpoint];
+ const moveToProjectId = 7;
+ mock.onPost(mediatorMockData.moveIssueEndpoint).reply(200, mockData);
+ mediator.store.setMoveToProjectId(moveToProjectId);
+ const moveIssueSpy = jest
+ .spyOn(mediator.service, 'moveIssue')
+ .mockReturnValue(Promise.resolve({ data: { web_url: mockData.web_url } }));
+ const urlSpy = jest.spyOn(urlUtility, 'visitUrl').mockReturnValue({});
+
+ return mediator.moveIssue().then(() => {
+ expect(moveIssueSpy).toHaveBeenCalledWith(moveToProjectId);
+ expect(urlSpy).toHaveBeenCalledWith(mockData.web_url);
+
+ moveIssueSpy.mockRestore();
+ urlSpy.mockRestore();
+ });
+ });
+
+ it('toggle subscription', () => {
+ mediator.store.setSubscribedState(false);
+ mock.onPost(mediatorMockData.toggleSubscriptionEndpoint).reply(200, {});
+ const spy = jest
+ .spyOn(mediator.service, 'toggleSubscription')
+ .mockReturnValue(Promise.resolve());
+
+ return mediator.toggleSubscription().then(() => {
+ expect(spy).toHaveBeenCalled();
+ expect(mediator.store.subscribed).toEqual(true);
+
+ spy.mockRestore();
+ });
+ });
+});
diff --git a/spec/frontend/sidebar/sidebar_move_issue_spec.js b/spec/frontend/sidebar/sidebar_move_issue_spec.js
new file mode 100644
index 00000000000..db0d3e06272
--- /dev/null
+++ b/spec/frontend/sidebar/sidebar_move_issue_spec.js
@@ -0,0 +1,167 @@
+import $ from 'jquery';
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
+import SidebarMediator from '~/sidebar/sidebar_mediator';
+import SidebarStore from '~/sidebar/stores/sidebar_store';
+import SidebarService from '~/sidebar/services/sidebar_service';
+import SidebarMoveIssue from '~/sidebar/lib/sidebar_move_issue';
+import Mock from './mock_data';
+
+describe('SidebarMoveIssue', () => {
+ let mock;
+ const test = {};
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ const mockData = Mock.responseMap.GET['/autocomplete/projects?project_id=15'];
+ mock.onGet('/autocomplete/projects?project_id=15').reply(200, mockData);
+ test.mediator = new SidebarMediator(Mock.mediator);
+ test.$content = $(`
+ <div class="dropdown">
+ <div class="js-toggle"></div>
+ <div class="dropdown-menu">
+ <div class="dropdown-content"></div>
+ </div>
+ <div class="js-confirm-button"></div>
+ </div>
+ `);
+ test.$toggleButton = test.$content.find('.js-toggle');
+ test.$confirmButton = test.$content.find('.js-confirm-button');
+
+ test.sidebarMoveIssue = new SidebarMoveIssue(
+ test.mediator,
+ test.$toggleButton,
+ test.$confirmButton,
+ );
+ test.sidebarMoveIssue.init();
+ });
+
+ afterEach(() => {
+ SidebarService.singleton = null;
+ SidebarStore.singleton = null;
+ SidebarMediator.singleton = null;
+
+ test.sidebarMoveIssue.destroy();
+ mock.restore();
+ });
+
+ describe('init', () => {
+ it('should initialize the dropdown and listeners', () => {
+ jest.spyOn(test.sidebarMoveIssue, 'initDropdown').mockImplementation(() => {});
+ jest.spyOn(test.sidebarMoveIssue, 'addEventListeners').mockImplementation(() => {});
+
+ test.sidebarMoveIssue.init();
+
+ expect(test.sidebarMoveIssue.initDropdown).toHaveBeenCalled();
+ expect(test.sidebarMoveIssue.addEventListeners).toHaveBeenCalled();
+ });
+ });
+
+ describe('destroy', () => {
+ it('should remove the listeners', () => {
+ jest.spyOn(test.sidebarMoveIssue, 'removeEventListeners').mockImplementation(() => {});
+
+ test.sidebarMoveIssue.destroy();
+
+ expect(test.sidebarMoveIssue.removeEventListeners).toHaveBeenCalled();
+ });
+ });
+
+ describe('initDropdown', () => {
+ it('should initialize the gl_dropdown', () => {
+ jest.spyOn($.fn, 'glDropdown').mockImplementation(() => {});
+
+ test.sidebarMoveIssue.initDropdown();
+
+ expect($.fn.glDropdown).toHaveBeenCalled();
+ });
+
+ it('escapes html from project name', done => {
+ test.$toggleButton.dropdown('toggle');
+
+ setImmediate(() => {
+ expect(test.$content.find('.js-move-issue-dropdown-item')[1].innerHTML.trim()).toEqual(
+ '&lt;img src=x onerror=alert(document.domain)&gt; foo / bar',
+ );
+ done();
+ });
+ });
+ });
+
+ describe('onConfirmClicked', () => {
+ it('should move the issue with valid project ID', () => {
+ jest.spyOn(test.mediator, 'moveIssue').mockReturnValue(Promise.resolve());
+ test.mediator.setMoveToProjectId(7);
+
+ test.sidebarMoveIssue.onConfirmClicked();
+
+ expect(test.mediator.moveIssue).toHaveBeenCalled();
+ expect(test.$confirmButton.prop('disabled')).toBeTruthy();
+ expect(test.$confirmButton.hasClass('is-loading')).toBe(true);
+ });
+
+ it('should remove loading state from confirm button on failure', done => {
+ jest.spyOn(window, 'Flash').mockImplementation(() => {});
+ jest.spyOn(test.mediator, 'moveIssue').mockReturnValue(Promise.reject());
+ test.mediator.setMoveToProjectId(7);
+
+ test.sidebarMoveIssue.onConfirmClicked();
+
+ expect(test.mediator.moveIssue).toHaveBeenCalled();
+ // Wait for the move issue request to fail
+ setImmediate(() => {
+ expect(window.Flash).toHaveBeenCalled();
+ expect(test.$confirmButton.prop('disabled')).toBeFalsy();
+ expect(test.$confirmButton.hasClass('is-loading')).toBe(false);
+ done();
+ });
+ });
+
+ it('should not move the issue with id=0', () => {
+ jest.spyOn(test.mediator, 'moveIssue').mockImplementation(() => {});
+ test.mediator.setMoveToProjectId(0);
+
+ test.sidebarMoveIssue.onConfirmClicked();
+
+ expect(test.mediator.moveIssue).not.toHaveBeenCalled();
+ });
+ });
+
+ it('should set moveToProjectId on dropdown item "No project" click', done => {
+ jest.spyOn(test.mediator, 'setMoveToProjectId').mockImplementation(() => {});
+
+ // Open the dropdown
+ test.$toggleButton.dropdown('toggle');
+
+ // Wait for the autocomplete request to finish
+ setImmediate(() => {
+ test.$content
+ .find('.js-move-issue-dropdown-item')
+ .eq(0)
+ .trigger('click');
+
+ expect(test.mediator.setMoveToProjectId).toHaveBeenCalledWith(0);
+ expect(test.$confirmButton.prop('disabled')).toBeTruthy();
+ done();
+ });
+ });
+
+ it('should set moveToProjectId on dropdown item click', done => {
+ jest.spyOn(test.mediator, 'setMoveToProjectId').mockImplementation(() => {});
+
+ // Open the dropdown
+ test.$toggleButton.dropdown('toggle');
+
+ // Wait for the autocomplete request to finish
+ setImmediate(() => {
+ test.$content
+ .find('.js-move-issue-dropdown-item')
+ .eq(1)
+ .trigger('click');
+
+ expect(test.mediator.setMoveToProjectId).toHaveBeenCalledWith(20);
+ expect(test.$confirmButton.attr('disabled')).toBe(undefined);
+ done();
+ });
+ });
+});
diff --git a/spec/frontend/sidebar/sidebar_subscriptions_spec.js b/spec/frontend/sidebar/sidebar_subscriptions_spec.js
new file mode 100644
index 00000000000..18aaeabe3dd
--- /dev/null
+++ b/spec/frontend/sidebar/sidebar_subscriptions_spec.js
@@ -0,0 +1,36 @@
+import { shallowMount } from '@vue/test-utils';
+import SidebarSubscriptions from '~/sidebar/components/subscriptions/sidebar_subscriptions.vue';
+import SidebarMediator from '~/sidebar/sidebar_mediator';
+import SidebarService from '~/sidebar/services/sidebar_service';
+import SidebarStore from '~/sidebar/stores/sidebar_store';
+import Mock from './mock_data';
+
+describe('Sidebar Subscriptions', () => {
+ let wrapper;
+ let mediator;
+
+ beforeEach(() => {
+ mediator = new SidebarMediator(Mock.mediator);
+ wrapper = shallowMount(SidebarSubscriptions, {
+ propsData: {
+ mediator,
+ },
+ });
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ SidebarService.singleton = null;
+ SidebarStore.singleton = null;
+ SidebarMediator.singleton = null;
+ });
+
+ it('calls the mediator toggleSubscription on event', () => {
+ const spy = jest.spyOn(mediator, 'toggleSubscription').mockReturnValue(Promise.resolve());
+
+ wrapper.vm.onToggleSubscription();
+
+ expect(spy).toHaveBeenCalled();
+ spy.mockRestore();
+ });
+});
diff --git a/spec/frontend/sidebar/subscriptions_spec.js b/spec/frontend/sidebar/subscriptions_spec.js
new file mode 100644
index 00000000000..cce35666985
--- /dev/null
+++ b/spec/frontend/sidebar/subscriptions_spec.js
@@ -0,0 +1,106 @@
+import { shallowMount } from '@vue/test-utils';
+import Subscriptions from '~/sidebar/components/subscriptions/subscriptions.vue';
+import eventHub from '~/sidebar/event_hub';
+import ToggleButton from '~/vue_shared/components/toggle_button.vue';
+
+describe('Subscriptions', () => {
+ let wrapper;
+
+ const findToggleButton = () => wrapper.find(ToggleButton);
+
+ const mountComponent = propsData =>
+ shallowMount(Subscriptions, {
+ propsData,
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ it('shows loading spinner when loading', () => {
+ wrapper = mountComponent({
+ loading: true,
+ subscribed: undefined,
+ });
+
+ expect(findToggleButton().attributes('isloading')).toBe('true');
+ });
+
+ it('is toggled "off" when currently not subscribed', () => {
+ wrapper = mountComponent({
+ subscribed: false,
+ });
+
+ expect(findToggleButton().attributes('value')).toBeFalsy();
+ });
+
+ it('is toggled "on" when currently subscribed', () => {
+ wrapper = mountComponent({
+ subscribed: true,
+ });
+
+ expect(findToggleButton().attributes('value')).toBe('true');
+ });
+
+ it('toggleSubscription method emits `toggleSubscription` event on eventHub and Component', () => {
+ const id = 42;
+ wrapper = mountComponent({ subscribed: true, id });
+ const eventHubSpy = jest.spyOn(eventHub, '$emit');
+ const wrapperEmitSpy = jest.spyOn(wrapper.vm, '$emit');
+
+ wrapper.vm.toggleSubscription();
+
+ expect(eventHubSpy).toHaveBeenCalledWith('toggleSubscription', id);
+ expect(wrapperEmitSpy).toHaveBeenCalledWith('toggleSubscription', id);
+ eventHubSpy.mockRestore();
+ wrapperEmitSpy.mockRestore();
+ });
+
+ it('tracks the event when toggled', () => {
+ wrapper = mountComponent({ subscribed: true });
+
+ const wrapperTrackSpy = jest.spyOn(wrapper.vm, 'track');
+
+ wrapper.vm.toggleSubscription();
+
+ expect(wrapperTrackSpy).toHaveBeenCalledWith('toggle_button', {
+ property: 'notifications',
+ value: 0,
+ });
+ wrapperTrackSpy.mockRestore();
+ });
+
+ it('onClickCollapsedIcon method emits `toggleSidebar` event on component', () => {
+ wrapper = mountComponent({ subscribed: true });
+ const spy = jest.spyOn(wrapper.vm, '$emit');
+
+ wrapper.vm.onClickCollapsedIcon();
+
+ expect(spy).toHaveBeenCalledWith('toggleSidebar');
+ spy.mockRestore();
+ });
+
+ describe('given project emails are disabled', () => {
+ const subscribeDisabledDescription = 'Notifications have been disabled';
+
+ beforeEach(() => {
+ wrapper = mountComponent({
+ subscribed: false,
+ projectEmailsDisabled: true,
+ subscribeDisabledDescription,
+ });
+ });
+
+ it('sets the correct display text', () => {
+ expect(wrapper.find('.issuable-header-text').text()).toContain(subscribeDisabledDescription);
+ expect(wrapper.find({ ref: 'tooltip' }).attributes('data-original-title')).toBe(
+ subscribeDisabledDescription,
+ );
+ });
+
+ it('does not render the toggle button', () => {
+ expect(wrapper.contains('.js-issuable-subscribe-button')).toBe(false);
+ });
+ });
+});