summaryrefslogtreecommitdiff
path: root/spec/frontend/notes/components/comment_form_spec.js
diff options
context:
space:
mode:
Diffstat (limited to 'spec/frontend/notes/components/comment_form_spec.js')
-rw-r--r--spec/frontend/notes/components/comment_form_spec.js443
1 files changed, 250 insertions, 193 deletions
diff --git a/spec/frontend/notes/components/comment_form_spec.js b/spec/frontend/notes/components/comment_form_spec.js
index 59fa7b372ed..fca1beca999 100644
--- a/spec/frontend/notes/components/comment_form_spec.js
+++ b/spec/frontend/notes/components/comment_form_spec.js
@@ -1,18 +1,20 @@
-import $ from 'jquery';
-import { mount } from '@vue/test-utils';
+import { nextTick } from 'vue';
+import { mount, shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import Autosize from 'autosize';
-import { trimText } from 'helpers/text_helper';
+import { deprecatedCreateFlash as flash } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import createStore from '~/notes/stores';
import CommentForm from '~/notes/components/comment_form.vue';
import * as constants from '~/notes/constants';
+import eventHub from '~/notes/event_hub';
import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests';
-import { keyboardDownEvent } from '../../issue_show/helpers';
+import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
import { loggedOutnoteableData, notesDataMock, userDataMock, noteableDataMock } from '../mock_data';
jest.mock('autosize');
jest.mock('~/commons/nav/user_merge_requests');
+jest.mock('~/flash');
jest.mock('~/gl_form');
describe('issue_comment_form component', () => {
@@ -20,17 +22,33 @@ describe('issue_comment_form component', () => {
let wrapper;
let axiosMock;
- const setupStore = (userData, noteableData) => {
- store.dispatch('setUserData', userData);
+ const findCloseReopenButton = () => wrapper.find('[data-testid="close-reopen-button"]');
+
+ const findCommentButton = () => wrapper.find('[data-testid="comment-button"]');
+
+ const findTextArea = () => wrapper.find('[data-testid="comment-field"]');
+
+ const mountComponent = ({
+ initialData = {},
+ noteableType = 'Issue',
+ noteableData = noteableDataMock,
+ notesData = notesDataMock,
+ userData = userDataMock,
+ mountFunction = shallowMount,
+ } = {}) => {
store.dispatch('setNoteableData', noteableData);
- store.dispatch('setNotesData', notesDataMock);
- };
+ store.dispatch('setNotesData', notesData);
+ store.dispatch('setUserData', userData);
- const mountComponent = (noteableType = 'issue') => {
- wrapper = mount(CommentForm, {
+ wrapper = mountFunction(CommentForm, {
propsData: {
noteableType,
},
+ data() {
+ return {
+ ...initialData,
+ };
+ },
store,
});
};
@@ -46,168 +64,157 @@ describe('issue_comment_form component', () => {
});
describe('user is logged in', () => {
- beforeEach(() => {
- setupStore(userDataMock, noteableDataMock);
-
- mountComponent();
- });
+ describe('avatar', () => {
+ it('should render user avatar with link', () => {
+ mountComponent({ mountFunction: mount });
- it('should render user avatar with link', () => {
- expect(wrapper.find('.timeline-icon .user-avatar-link').attributes('href')).toEqual(
- userDataMock.path,
- );
+ expect(wrapper.find(UserAvatarLink).attributes('href')).toBe(userDataMock.path);
+ });
});
describe('handleSave', () => {
it('should request to save note when note is entered', () => {
- wrapper.vm.note = 'hello world';
- jest.spyOn(wrapper.vm, 'saveNote').mockReturnValue(new Promise(() => {}));
+ mountComponent({ mountFunction: mount, initialData: { note: 'hello world' } });
+
+ jest.spyOn(wrapper.vm, 'saveNote').mockResolvedValue();
jest.spyOn(wrapper.vm, 'resizeTextarea');
jest.spyOn(wrapper.vm, 'stopPolling');
- wrapper.vm.handleSave();
+ findCloseReopenButton().trigger('click');
- expect(wrapper.vm.isSubmitting).toEqual(true);
- expect(wrapper.vm.note).toEqual('');
+ expect(wrapper.vm.isSubmitting).toBe(true);
+ expect(wrapper.vm.note).toBe('');
expect(wrapper.vm.saveNote).toHaveBeenCalled();
expect(wrapper.vm.stopPolling).toHaveBeenCalled();
expect(wrapper.vm.resizeTextarea).toHaveBeenCalled();
});
it('should toggle issue state when no note', () => {
+ mountComponent({ mountFunction: mount });
+
jest.spyOn(wrapper.vm, 'toggleIssueState');
- wrapper.vm.handleSave();
+ findCloseReopenButton().trigger('click');
expect(wrapper.vm.toggleIssueState).toHaveBeenCalled();
});
- it('should disable action button while submitting', done => {
+ it('should disable action button while submitting', async () => {
+ mountComponent({ mountFunction: mount, initialData: { note: 'hello world' } });
+
const saveNotePromise = Promise.resolve();
- wrapper.vm.note = 'hello world';
+
jest.spyOn(wrapper.vm, 'saveNote').mockReturnValue(saveNotePromise);
jest.spyOn(wrapper.vm, 'stopPolling');
- const actionButton = wrapper.find('.js-action-button');
-
- wrapper.vm.handleSave();
-
- wrapper.vm
- .$nextTick()
- .then(() => {
- expect(actionButton.vm.disabled).toBeTruthy();
- })
- .then(saveNotePromise)
- .then(wrapper.vm.$nextTick)
- .then(() => {
- expect(actionButton.vm.disabled).toBeFalsy();
- })
- .then(done)
- .catch(done.fail);
+ const actionButton = findCloseReopenButton();
+
+ await actionButton.trigger('click');
+
+ expect(actionButton.props('disabled')).toBe(true);
+
+ await saveNotePromise;
+
+ await nextTick();
+
+ expect(actionButton.props('disabled')).toBe(false);
});
});
describe('textarea', () => {
- it('should render textarea with placeholder', () => {
- expect(wrapper.find('.js-main-target-form textarea').attributes('placeholder')).toEqual(
- 'Write a comment or drag your files here…',
- );
- });
+ describe('general', () => {
+ it('should render textarea with placeholder', () => {
+ mountComponent({ mountFunction: mount });
- it('should make textarea disabled while requesting', done => {
- const $submitButton = $(wrapper.find('.js-comment-submit-button').element);
- wrapper.vm.note = 'hello world';
- jest.spyOn(wrapper.vm, 'stopPolling');
- jest.spyOn(wrapper.vm, 'saveNote').mockReturnValue(new Promise(() => {}));
+ expect(findTextArea().attributes('placeholder')).toBe(
+ 'Write a comment or drag your files here…',
+ );
+ });
- wrapper.vm.$nextTick(() => {
- // Wait for wrapper.vm.note change triggered. It should enable $submitButton.
- $submitButton.trigger('click');
+ it('should make textarea disabled while requesting', async () => {
+ mountComponent({ mountFunction: mount });
- wrapper.vm.$nextTick(() => {
- // Wait for wrapper.isSubmitting triggered. It should disable textarea.
- expect(wrapper.find('.js-main-target-form textarea').attributes('disabled')).toBe(
- 'disabled',
- );
- done();
- });
+ jest.spyOn(wrapper.vm, 'stopPolling');
+ jest.spyOn(wrapper.vm, 'saveNote').mockResolvedValue();
+
+ await wrapper.setData({ note: 'hello world' });
+
+ await findCommentButton().trigger('click');
+
+ expect(findTextArea().attributes('disabled')).toBe('disabled');
});
- });
- it('should support quick actions', () => {
- expect(
- wrapper.find('.js-main-target-form textarea').attributes('data-supports-quick-actions'),
- ).toBe('true');
- });
+ it('should support quick actions', () => {
+ mountComponent({ mountFunction: mount });
- it('should link to markdown docs', () => {
- const { markdownDocsPath } = notesDataMock;
+ expect(findTextArea().attributes('data-supports-quick-actions')).toBe('true');
+ });
- expect(
- wrapper
- .find(`a[href="${markdownDocsPath}"]`)
- .text()
- .trim(),
- ).toEqual('Markdown');
- });
+ it('should link to markdown docs', () => {
+ mountComponent({ mountFunction: mount });
- it('should link to quick actions docs', () => {
- const { quickActionsDocsPath } = notesDataMock;
+ const { markdownDocsPath } = notesDataMock;
- expect(
- wrapper
- .find(`a[href="${quickActionsDocsPath}"]`)
- .text()
- .trim(),
- ).toEqual('quick actions');
- });
+ expect(wrapper.find(`a[href="${markdownDocsPath}"]`).text()).toBe('Markdown');
+ });
+
+ it('should link to quick actions docs', () => {
+ mountComponent({ mountFunction: mount });
+
+ const { quickActionsDocsPath } = notesDataMock;
+
+ expect(wrapper.find(`a[href="${quickActionsDocsPath}"]`).text()).toBe('quick actions');
+ });
+
+ it('should resize textarea after note discarded', async () => {
+ mountComponent({ mountFunction: mount, initialData: { note: 'foo' } });
- it('should resize textarea after note discarded', done => {
- jest.spyOn(wrapper.vm, 'discard');
+ jest.spyOn(wrapper.vm, 'discard');
- wrapper.vm.note = 'foo';
- wrapper.vm.discard();
+ wrapper.vm.discard();
+
+ await nextTick();
- wrapper.vm.$nextTick(() => {
expect(Autosize.update).toHaveBeenCalled();
- done();
});
});
describe('edit mode', () => {
+ beforeEach(() => {
+ mountComponent();
+ });
+
it('should enter edit mode when arrow up is pressed', () => {
jest.spyOn(wrapper.vm, 'editCurrentUserLastNote');
- wrapper.find('.js-main-target-form textarea').value = 'Foo';
- wrapper
- .find('.js-main-target-form textarea')
- .element.dispatchEvent(keyboardDownEvent(38, true));
+
+ findTextArea().trigger('keydown.up');
expect(wrapper.vm.editCurrentUserLastNote).toHaveBeenCalled();
});
it('inits autosave', () => {
expect(wrapper.vm.autosave).toBeDefined();
- expect(wrapper.vm.autosave.key).toEqual(`autosave/Note/Issue/${noteableDataMock.id}`);
+ expect(wrapper.vm.autosave.key).toBe(`autosave/Note/Issue/${noteableDataMock.id}`);
});
});
describe('event enter', () => {
+ beforeEach(() => {
+ mountComponent();
+ });
+
it('should save note when cmd+enter is pressed', () => {
jest.spyOn(wrapper.vm, 'handleSave');
- wrapper.find('.js-main-target-form textarea').value = 'Foo';
- wrapper
- .find('.js-main-target-form textarea')
- .element.dispatchEvent(keyboardDownEvent(13, true));
+
+ findTextArea().trigger('keydown.enter', { metaKey: true });
expect(wrapper.vm.handleSave).toHaveBeenCalled();
});
it('should save note when ctrl+enter is pressed', () => {
jest.spyOn(wrapper.vm, 'handleSave');
- wrapper.find('.js-main-target-form textarea').value = 'Foo';
- wrapper
- .find('.js-main-target-form textarea')
- .element.dispatchEvent(keyboardDownEvent(13, false, true));
+
+ findTextArea().trigger('keydown.enter', { ctrlKey: true });
expect(wrapper.vm.handleSave).toHaveBeenCalled();
});
@@ -216,137 +223,187 @@ describe('issue_comment_form component', () => {
describe('actions', () => {
it('should be possible to close the issue', () => {
- expect(
- wrapper
- .find('.btn-comment-and-close')
- .text()
- .trim(),
- ).toEqual('Close issue');
+ mountComponent();
+
+ expect(findCloseReopenButton().text()).toBe('Close issue');
});
it('should render comment button as disabled', () => {
- expect(wrapper.find('.js-comment-submit-button').attributes('disabled')).toEqual(
- 'disabled',
- );
+ mountComponent();
+
+ expect(findCommentButton().props('disabled')).toBe(true);
});
- it('should enable comment button if it has note', done => {
- wrapper.vm.note = 'Foo';
- wrapper.vm.$nextTick(() => {
- expect(wrapper.find('.js-comment-submit-button').attributes('disabled')).toBeFalsy();
- done();
- });
+ it('should enable comment button if it has note', async () => {
+ mountComponent();
+
+ await wrapper.setData({ note: 'Foo' });
+
+ expect(findCommentButton().props('disabled')).toBe(false);
});
- it('should update buttons texts when it has note', done => {
- wrapper.vm.note = 'Foo';
- wrapper.vm.$nextTick(() => {
- expect(
- wrapper
- .find('.btn-comment-and-close')
- .text()
- .trim(),
- ).toEqual('Comment & close issue');
-
- done();
- });
+ it('should update buttons texts when it has note', () => {
+ mountComponent({ initialData: { note: 'Foo' } });
+
+ expect(findCloseReopenButton().text()).toBe('Comment & close issue');
});
- it('updates button text with noteable type', done => {
- wrapper.setProps({ noteableType: constants.MERGE_REQUEST_NOTEABLE_TYPE });
-
- wrapper.vm.$nextTick(() => {
- expect(
- wrapper
- .find('.btn-comment-and-close')
- .text()
- .trim(),
- ).toEqual('Close merge request');
- done();
- });
+ it('updates button text with noteable type', () => {
+ mountComponent({ noteableType: constants.MERGE_REQUEST_NOTEABLE_TYPE });
+
+ expect(findCloseReopenButton().text()).toBe('Close merge request');
});
describe('when clicking close/reopen button', () => {
- it('should disable button and show a loading spinner', () => {
- const toggleStateButton = wrapper.find('.js-action-button');
+ it('should show a loading spinner', async () => {
+ mountComponent({
+ noteableType: constants.MERGE_REQUEST_NOTEABLE_TYPE,
+ mountFunction: mount,
+ });
- toggleStateButton.trigger('click');
+ await findCloseReopenButton().trigger('click');
- return wrapper.vm.$nextTick().then(() => {
- expect(toggleStateButton.element.disabled).toEqual(true);
- expect(toggleStateButton.props('loading')).toBe(true);
- });
+ expect(findCloseReopenButton().props('loading')).toBe(true);
});
});
describe('when toggling state', () => {
- it('should update MR count', done => {
- jest.spyOn(wrapper.vm, 'closeIssue').mockResolvedValue();
+ describe('when issue', () => {
+ it('emits event to toggle state', () => {
+ mountComponent({ mountFunction: mount });
+
+ jest.spyOn(eventHub, '$emit');
+
+ findCloseReopenButton().trigger('click');
+
+ expect(eventHub.$emit).toHaveBeenCalledWith('toggle.issuable.state');
+ });
+ });
+
+ describe.each`
+ type | noteableType
+ ${'merge request'} | ${'MergeRequest'}
+ ${'epic'} | ${'Epic'}
+ `('when $type', ({ type, noteableType }) => {
+ describe('when open', () => {
+ it(`makes an API call to open it`, () => {
+ mountComponent({
+ noteableType,
+ noteableData: { ...noteableDataMock, state: constants.OPENED },
+ mountFunction: mount,
+ });
+
+ jest.spyOn(wrapper.vm, 'closeIssuable').mockResolvedValue();
+
+ findCloseReopenButton().trigger('click');
+
+ expect(wrapper.vm.closeIssuable).toHaveBeenCalled();
+ });
+
+ it(`shows an error when the API call fails`, async () => {
+ mountComponent({
+ noteableType,
+ noteableData: { ...noteableDataMock, state: constants.OPENED },
+ mountFunction: mount,
+ });
+
+ jest.spyOn(wrapper.vm, 'closeIssuable').mockRejectedValue();
+
+ await findCloseReopenButton().trigger('click');
+
+ await wrapper.vm.$nextTick;
+
+ expect(flash).toHaveBeenCalledWith(
+ `Something went wrong while closing the ${type}. Please try again later.`,
+ );
+ });
+ });
+
+ describe('when closed', () => {
+ it('makes an API call to close it', () => {
+ mountComponent({
+ noteableType,
+ noteableData: { ...noteableDataMock, state: constants.CLOSED },
+ mountFunction: mount,
+ });
+
+ jest.spyOn(wrapper.vm, 'reopenIssuable').mockResolvedValue();
- wrapper.vm.toggleIssueState();
+ findCloseReopenButton().trigger('click');
- wrapper.vm.$nextTick(() => {
- expect(refreshUserMergeRequestCounts).toHaveBeenCalled();
+ expect(wrapper.vm.reopenIssuable).toHaveBeenCalled();
+ });
+ });
+
+ it(`shows an error when the API call fails`, async () => {
+ mountComponent({
+ noteableType,
+ noteableData: { ...noteableDataMock, state: constants.CLOSED },
+ mountFunction: mount,
+ });
+
+ jest.spyOn(wrapper.vm, 'reopenIssuable').mockRejectedValue();
+
+ await findCloseReopenButton().trigger('click');
- done();
+ await wrapper.vm.$nextTick;
+
+ expect(flash).toHaveBeenCalledWith(
+ `Something went wrong while reopening the ${type}. Please try again later.`,
+ );
});
});
+
+ it('when merge request, should update MR count', async () => {
+ mountComponent({
+ noteableType: constants.MERGE_REQUEST_NOTEABLE_TYPE,
+ mountFunction: mount,
+ });
+
+ jest.spyOn(wrapper.vm, 'closeIssuable').mockResolvedValue();
+
+ await findCloseReopenButton().trigger('click');
+
+ expect(refreshUserMergeRequestCounts).toHaveBeenCalled();
+ });
});
});
describe('issue is confidential', () => {
- it('shows information warning', done => {
- store.dispatch('setNoteableData', Object.assign(noteableDataMock, { confidential: true }));
- wrapper.vm.$nextTick(() => {
- expect(wrapper.find('.confidential-issue-warning')).toBeDefined();
- done();
+ it('shows information warning', () => {
+ mountComponent({
+ noteableData: { ...noteableDataMock, confidential: true },
+ mountFunction: mount,
});
+
+ expect(wrapper.find('[data-testid="confidential-warning"]').exists()).toBe(true);
});
});
});
describe('user is not logged in', () => {
beforeEach(() => {
- setupStore(null, loggedOutnoteableData);
-
- mountComponent();
+ mountComponent({ userData: null, noteableData: loggedOutnoteableData, mountFunction: mount });
});
it('should render signed out widget', () => {
- expect(trimText(wrapper.text())).toEqual('Please register or sign in to reply');
+ expect(wrapper.text()).toBe('Please register or sign in to reply');
});
it('should not render submission form', () => {
- expect(wrapper.find('textarea').exists()).toBe(false);
+ expect(findTextArea().exists()).toBe(false);
});
});
- describe('when issuable is open', () => {
- beforeEach(() => {
- setupStore(userDataMock, noteableDataMock);
- });
-
- it.each([['opened', 'warning'], ['reopened', 'warning']])(
- 'when %i, it changes the variant of the btn to %i',
- (a, expected) => {
- store.state.noteableData.state = a;
-
- mountComponent();
-
- expect(wrapper.find('.js-action-button').props('variant')).toBe(expected);
- },
- );
- });
-
- describe('when issuable is not open', () => {
- beforeEach(() => {
- setupStore(userDataMock, noteableDataMock);
-
- mountComponent();
- });
+ describe('close/reopen button variants', () => {
+ it.each([
+ [constants.OPENED, 'warning'],
+ [constants.REOPENED, 'warning'],
+ [constants.CLOSED, 'default'],
+ ])('when %s, the variant of the btn is %s', (state, expected) => {
+ mountComponent({ noteableData: { ...noteableDataMock, state } });
- it('should render the "default" variant of the button', () => {
- expect(wrapper.find('.js-action-button').props('variant')).toBe('warning');
+ expect(findCloseReopenButton().props('variant')).toBe(expected);
});
});
});