2021-04-29 21:17:54 +05:30
|
|
|
import { GlAlert } from '@gitlab/ui';
|
2021-02-22 17:27:13 +05:30
|
|
|
import { mount, shallowMount } from '@vue/test-utils';
|
2019-12-26 22:10:19 +05:30
|
|
|
import Autosize from 'autosize';
|
2021-03-11 19:13:27 +05:30
|
|
|
import MockAdapter from 'axios-mock-adapter';
|
2021-04-17 20:07:23 +05:30
|
|
|
import Vue, { nextTick } from 'vue';
|
|
|
|
import Vuex from 'vuex';
|
2021-03-11 19:13:27 +05:30
|
|
|
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
|
2023-06-20 00:43:36 +05:30
|
|
|
import { useLocalStorageSpy } from 'helpers/local_storage_helper';
|
2021-04-29 21:17:54 +05:30
|
|
|
import batchComments from '~/batch_comments/stores/modules/batch_comments';
|
2021-03-11 19:13:27 +05:30
|
|
|
import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests';
|
2023-05-27 22:25:52 +05:30
|
|
|
import { createAlert } from '~/alert';
|
|
|
|
import { STATUS_CLOSED, STATUS_OPEN } from '~/issues/constants';
|
2019-12-26 22:10:19 +05:30
|
|
|
import axios from '~/lib/utils/axios_utils';
|
2023-05-27 22:25:52 +05:30
|
|
|
import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue';
|
2023-04-23 21:23:45 +05:30
|
|
|
import { HTTP_STATUS_UNPROCESSABLE_ENTITY } from '~/lib/utils/http_status';
|
2019-12-26 22:10:19 +05:30
|
|
|
import CommentForm from '~/notes/components/comment_form.vue';
|
2021-11-11 11:23:49 +05:30
|
|
|
import CommentTypeDropdown from '~/notes/components/comment_type_dropdown.vue';
|
2019-12-26 22:10:19 +05:30
|
|
|
import * as constants from '~/notes/constants';
|
2021-02-22 17:27:13 +05:30
|
|
|
import eventHub from '~/notes/event_hub';
|
2021-04-17 20:07:23 +05:30
|
|
|
import { COMMENT_FORM } from '~/notes/i18n';
|
|
|
|
import notesModule from '~/notes/stores/modules';
|
2020-05-24 23:13:21 +05:30
|
|
|
import { loggedOutnoteableData, notesDataMock, userDataMock, noteableDataMock } from '../mock_data';
|
2019-12-26 22:10:19 +05:30
|
|
|
|
|
|
|
jest.mock('autosize');
|
|
|
|
jest.mock('~/commons/nav/user_merge_requests');
|
2023-05-27 22:25:52 +05:30
|
|
|
jest.mock('~/alert');
|
2019-12-26 22:10:19 +05:30
|
|
|
|
2021-04-17 20:07:23 +05:30
|
|
|
Vue.use(Vuex);
|
|
|
|
|
2019-12-26 22:10:19 +05:30
|
|
|
describe('issue_comment_form component', () => {
|
2023-06-20 00:43:36 +05:30
|
|
|
useLocalStorageSpy();
|
|
|
|
|
2019-12-26 22:10:19 +05:30
|
|
|
let store;
|
|
|
|
let wrapper;
|
|
|
|
let axiosMock;
|
|
|
|
|
2021-03-11 19:13:27 +05:30
|
|
|
const findCloseReopenButton = () => wrapper.findByTestId('close-reopen-button');
|
2023-05-27 22:25:52 +05:30
|
|
|
const findMarkdownEditor = () => wrapper.findComponent(MarkdownEditor);
|
|
|
|
const findMarkdownEditorTextarea = () => findMarkdownEditor().find('textarea');
|
2021-04-29 21:17:54 +05:30
|
|
|
const findAddToReviewButton = () => wrapper.findByTestId('add-to-review-button');
|
|
|
|
const findAddCommentNowButton = () => wrapper.findByTestId('add-comment-now-button');
|
2022-07-23 23:45:48 +05:30
|
|
|
const findConfidentialNoteCheckbox = () => wrapper.findByTestId('internal-note-checkbox');
|
2021-11-11 11:23:49 +05:30
|
|
|
const findCommentTypeDropdown = () => wrapper.findComponent(CommentTypeDropdown);
|
|
|
|
const findCommentButton = () => findCommentTypeDropdown().find('button');
|
2021-04-17 20:07:23 +05:30
|
|
|
const findErrorAlerts = () => wrapper.findAllComponents(GlAlert).wrappers;
|
|
|
|
|
|
|
|
async function clickCommentButton({ waitForComponent = true, waitForNetwork = true } = {}) {
|
|
|
|
findCommentButton().trigger('click');
|
|
|
|
|
|
|
|
if (waitForComponent || waitForNetwork) {
|
|
|
|
// Wait for the click to bubble out and trigger the handler
|
|
|
|
await nextTick();
|
|
|
|
|
|
|
|
if (waitForNetwork) {
|
|
|
|
// Wait for the network request promise to resolve
|
|
|
|
await nextTick();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function createStore({ actions = {} } = {}) {
|
|
|
|
const baseModule = notesModule();
|
|
|
|
|
|
|
|
return new Vuex.Store({
|
|
|
|
...baseModule,
|
|
|
|
actions: {
|
|
|
|
...baseModule.actions,
|
|
|
|
...actions,
|
|
|
|
},
|
|
|
|
});
|
|
|
|
}
|
2021-02-22 17:27:13 +05:30
|
|
|
|
2021-03-11 19:13:27 +05:30
|
|
|
const createNotableDataMock = (data = {}) => {
|
|
|
|
return {
|
|
|
|
...noteableDataMock,
|
|
|
|
...data,
|
|
|
|
};
|
|
|
|
};
|
2021-02-22 17:27:13 +05:30
|
|
|
|
2021-03-11 19:13:27 +05:30
|
|
|
const notableDataMockCanUpdateIssuable = createNotableDataMock({
|
2022-11-25 23:54:43 +05:30
|
|
|
current_user: { can_update: true, can_create_note: true, can_create_confidential_note: true },
|
2021-03-11 19:13:27 +05:30
|
|
|
});
|
|
|
|
|
|
|
|
const notableDataMockCannotUpdateIssuable = createNotableDataMock({
|
2022-11-25 23:54:43 +05:30
|
|
|
current_user: {
|
|
|
|
can_update: false,
|
|
|
|
can_create_note: false,
|
|
|
|
can_create_confidential_note: false,
|
|
|
|
},
|
|
|
|
});
|
|
|
|
|
|
|
|
const notableDataMockCannotCreateConfidentialNote = createNotableDataMock({
|
|
|
|
current_user: { can_update: false, can_create_note: true, can_create_confidential_note: false },
|
2021-03-11 19:13:27 +05:30
|
|
|
});
|
2021-02-22 17:27:13 +05:30
|
|
|
|
|
|
|
const mountComponent = ({
|
|
|
|
initialData = {},
|
|
|
|
noteableType = 'Issue',
|
|
|
|
noteableData = noteableDataMock,
|
|
|
|
notesData = notesDataMock,
|
|
|
|
userData = userDataMock,
|
2021-03-11 19:13:27 +05:30
|
|
|
features = {},
|
2021-02-22 17:27:13 +05:30
|
|
|
mountFunction = shallowMount,
|
|
|
|
} = {}) => {
|
2019-12-26 22:10:19 +05:30
|
|
|
store.dispatch('setNoteableData', noteableData);
|
2021-02-22 17:27:13 +05:30
|
|
|
store.dispatch('setNotesData', notesData);
|
|
|
|
store.dispatch('setUserData', userData);
|
2019-12-26 22:10:19 +05:30
|
|
|
|
2021-03-11 19:13:27 +05:30
|
|
|
wrapper = extendedWrapper(
|
|
|
|
mountFunction(CommentForm, {
|
|
|
|
propsData: {
|
|
|
|
noteableType,
|
|
|
|
},
|
|
|
|
data() {
|
|
|
|
return {
|
|
|
|
...initialData,
|
|
|
|
};
|
|
|
|
},
|
|
|
|
store,
|
|
|
|
provide: {
|
|
|
|
glFeatures: features,
|
|
|
|
},
|
|
|
|
}),
|
|
|
|
);
|
2019-12-26 22:10:19 +05:30
|
|
|
};
|
|
|
|
|
|
|
|
beforeEach(() => {
|
|
|
|
axiosMock = new MockAdapter(axios);
|
|
|
|
store = createStore();
|
|
|
|
});
|
|
|
|
|
|
|
|
afterEach(() => {
|
|
|
|
axiosMock.restore();
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('user is logged in', () => {
|
|
|
|
describe('handleSave', () => {
|
|
|
|
it('should request to save note when note is entered', () => {
|
2021-02-22 17:27:13 +05:30
|
|
|
mountComponent({ mountFunction: mount, initialData: { note: 'hello world' } });
|
|
|
|
|
|
|
|
jest.spyOn(wrapper.vm, 'saveNote').mockResolvedValue();
|
2019-12-26 22:10:19 +05:30
|
|
|
jest.spyOn(wrapper.vm, 'stopPolling');
|
|
|
|
|
2021-02-22 17:27:13 +05:30
|
|
|
findCloseReopenButton().trigger('click');
|
2019-12-26 22:10:19 +05:30
|
|
|
|
2021-02-22 17:27:13 +05:30
|
|
|
expect(wrapper.vm.isSubmitting).toBe(true);
|
|
|
|
expect(wrapper.vm.note).toBe('');
|
2019-12-26 22:10:19 +05:30
|
|
|
expect(wrapper.vm.saveNote).toHaveBeenCalled();
|
|
|
|
expect(wrapper.vm.stopPolling).toHaveBeenCalled();
|
|
|
|
});
|
|
|
|
|
2021-04-17 20:07:23 +05:30
|
|
|
it('does not report errors in the UI when the save succeeds', async () => {
|
|
|
|
mountComponent({ mountFunction: mount, initialData: { note: '/label ~sdfghj' } });
|
|
|
|
|
|
|
|
jest.spyOn(wrapper.vm, 'saveNote').mockResolvedValue();
|
|
|
|
|
|
|
|
await clickCommentButton();
|
|
|
|
|
|
|
|
// findErrorAlerts().exists returns false if *any* wrapper is empty,
|
|
|
|
// not necessarily that there aren't any at all.
|
|
|
|
// We want to check here that there are none found, so we use the
|
|
|
|
// raw wrapper array length instead.
|
|
|
|
expect(findErrorAlerts().length).toBe(0);
|
|
|
|
});
|
|
|
|
|
|
|
|
it.each`
|
2023-04-23 21:23:45 +05:30
|
|
|
httpStatus | errors
|
|
|
|
${400} | ${[COMMENT_FORM.GENERIC_UNSUBMITTABLE_NETWORK]}
|
|
|
|
${HTTP_STATUS_UNPROCESSABLE_ENTITY} | ${['error 1']}
|
|
|
|
${HTTP_STATUS_UNPROCESSABLE_ENTITY} | ${['error 1', 'error 2']}
|
|
|
|
${HTTP_STATUS_UNPROCESSABLE_ENTITY} | ${['error 1', 'error 2', 'error 3']}
|
2021-04-17 20:07:23 +05:30
|
|
|
`(
|
|
|
|
'displays the correct errors ($errors) for a $httpStatus network response',
|
|
|
|
async ({ errors, httpStatus }) => {
|
|
|
|
store = createStore({
|
|
|
|
actions: {
|
|
|
|
saveNote: jest.fn().mockRejectedValue({
|
|
|
|
response: { status: httpStatus, data: { errors: { commands_only: errors } } },
|
|
|
|
}),
|
|
|
|
},
|
|
|
|
});
|
|
|
|
|
|
|
|
mountComponent({ mountFunction: mount, initialData: { note: '/label ~sdfghj' } });
|
|
|
|
|
|
|
|
await clickCommentButton();
|
|
|
|
|
|
|
|
const errorAlerts = findErrorAlerts();
|
|
|
|
|
|
|
|
expect(errorAlerts.length).toBe(errors.length);
|
|
|
|
errors.forEach((msg, index) => {
|
|
|
|
const alert = errorAlerts[index];
|
|
|
|
|
|
|
|
expect(alert.text()).toBe(msg);
|
|
|
|
});
|
|
|
|
},
|
|
|
|
);
|
|
|
|
|
|
|
|
it('should remove the correct error from the list when it is dismissed', async () => {
|
|
|
|
const commandErrors = ['1', '2', '3'];
|
|
|
|
store = createStore({
|
|
|
|
actions: {
|
|
|
|
saveNote: jest.fn().mockRejectedValue({
|
2023-04-23 21:23:45 +05:30
|
|
|
response: {
|
|
|
|
status: HTTP_STATUS_UNPROCESSABLE_ENTITY,
|
|
|
|
data: { errors: { commands_only: [...commandErrors] } },
|
|
|
|
},
|
2021-04-17 20:07:23 +05:30
|
|
|
}),
|
|
|
|
},
|
|
|
|
});
|
|
|
|
|
|
|
|
mountComponent({ mountFunction: mount, initialData: { note: '/label ~sdfghj' } });
|
|
|
|
|
|
|
|
await clickCommentButton();
|
|
|
|
|
|
|
|
let errorAlerts = findErrorAlerts();
|
|
|
|
|
|
|
|
expect(errorAlerts.length).toBe(commandErrors.length);
|
|
|
|
|
|
|
|
// dismiss the second error
|
|
|
|
extendedWrapper(errorAlerts[1]).findByTestId('close-icon').trigger('click');
|
|
|
|
// Wait for the dismissal to bubble out of the Alert component and be handled in this component
|
|
|
|
await nextTick();
|
|
|
|
// Refresh the list of alerts
|
|
|
|
errorAlerts = findErrorAlerts();
|
|
|
|
|
|
|
|
expect(errorAlerts.length).toBe(commandErrors.length - 1);
|
|
|
|
// We want to know that the *correct* error was dismissed, not just that any one is gone
|
|
|
|
expect(errorAlerts[0].text()).toBe(commandErrors[0]);
|
|
|
|
expect(errorAlerts[1].text()).toBe(commandErrors[2]);
|
|
|
|
});
|
|
|
|
|
2019-12-26 22:10:19 +05:30
|
|
|
it('should toggle issue state when no note', () => {
|
2021-02-22 17:27:13 +05:30
|
|
|
mountComponent({ mountFunction: mount });
|
|
|
|
|
2019-12-26 22:10:19 +05:30
|
|
|
jest.spyOn(wrapper.vm, 'toggleIssueState');
|
|
|
|
|
2021-02-22 17:27:13 +05:30
|
|
|
findCloseReopenButton().trigger('click');
|
2019-12-26 22:10:19 +05:30
|
|
|
|
|
|
|
expect(wrapper.vm.toggleIssueState).toHaveBeenCalled();
|
|
|
|
});
|
|
|
|
|
2021-02-22 17:27:13 +05:30
|
|
|
it('should disable action button while submitting', async () => {
|
|
|
|
mountComponent({ mountFunction: mount, initialData: { note: 'hello world' } });
|
|
|
|
|
2019-12-26 22:10:19 +05:30
|
|
|
const saveNotePromise = Promise.resolve();
|
2021-02-22 17:27:13 +05:30
|
|
|
|
2019-12-26 22:10:19 +05:30
|
|
|
jest.spyOn(wrapper.vm, 'saveNote').mockReturnValue(saveNotePromise);
|
|
|
|
jest.spyOn(wrapper.vm, 'stopPolling');
|
|
|
|
|
2021-02-22 17:27:13 +05:30
|
|
|
const actionButton = findCloseReopenButton();
|
|
|
|
|
|
|
|
await actionButton.trigger('click');
|
|
|
|
|
|
|
|
expect(actionButton.props('disabled')).toBe(true);
|
|
|
|
|
|
|
|
await saveNotePromise;
|
|
|
|
|
|
|
|
await nextTick();
|
|
|
|
|
|
|
|
expect(actionButton.props('disabled')).toBe(false);
|
2019-12-26 22:10:19 +05:30
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2023-05-27 22:25:52 +05:30
|
|
|
it('hides content editor switcher if feature flag content_editor_on_issues is off', () => {
|
|
|
|
mountComponent({ mountFunction: mount, features: { contentEditorOnIssues: false } });
|
|
|
|
|
2023-07-09 08:55:56 +05:30
|
|
|
expect(wrapper.text()).not.toContain('Switch to rich text');
|
2023-05-27 22:25:52 +05:30
|
|
|
});
|
|
|
|
|
|
|
|
it('shows content editor switcher if feature flag content_editor_on_issues is on', () => {
|
|
|
|
mountComponent({ mountFunction: mount, features: { contentEditorOnIssues: true } });
|
|
|
|
|
2023-07-09 08:55:56 +05:30
|
|
|
expect(wrapper.text()).toContain('Switch to rich text');
|
2023-05-27 22:25:52 +05:30
|
|
|
});
|
|
|
|
|
2019-12-26 22:10:19 +05:30
|
|
|
describe('textarea', () => {
|
2021-02-22 17:27:13 +05:30
|
|
|
describe('general', () => {
|
2022-07-16 23:28:13 +05:30
|
|
|
it.each`
|
2022-07-23 23:45:48 +05:30
|
|
|
noteType | noteIsInternal | placeholder
|
|
|
|
${'comment'} | ${false} | ${'Write a comment or drag your files here…'}
|
|
|
|
${'internal note'} | ${true} | ${'Write an internal note or drag your files here…'}
|
2022-07-16 23:28:13 +05:30
|
|
|
`(
|
|
|
|
'should render textarea with placeholder for $noteType',
|
2023-05-27 22:25:52 +05:30
|
|
|
async ({ noteIsInternal, placeholder }) => {
|
|
|
|
mountComponent();
|
|
|
|
|
|
|
|
wrapper.vm.noteIsInternal = noteIsInternal;
|
|
|
|
await nextTick();
|
2019-12-26 22:10:19 +05:30
|
|
|
|
2023-05-27 22:25:52 +05:30
|
|
|
expect(findMarkdownEditor().props('formFieldProps').placeholder).toBe(placeholder);
|
2022-07-16 23:28:13 +05:30
|
|
|
},
|
|
|
|
);
|
2019-12-26 22:10:19 +05:30
|
|
|
|
2021-02-22 17:27:13 +05:30
|
|
|
it('should make textarea disabled while requesting', async () => {
|
|
|
|
mountComponent({ mountFunction: mount });
|
2019-12-26 22:10:19 +05:30
|
|
|
|
2021-02-22 17:27:13 +05:30
|
|
|
jest.spyOn(wrapper.vm, 'stopPolling');
|
|
|
|
jest.spyOn(wrapper.vm, 'saveNote').mockResolvedValue();
|
|
|
|
|
2022-03-02 08:16:31 +05:30
|
|
|
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
|
|
|
|
// eslint-disable-next-line no-restricted-syntax
|
2021-02-22 17:27:13 +05:30
|
|
|
await wrapper.setData({ note: 'hello world' });
|
|
|
|
|
|
|
|
await findCommentButton().trigger('click');
|
|
|
|
|
2023-07-09 08:55:56 +05:30
|
|
|
expect(findMarkdownEditor().find('textarea').attributes('disabled')).toBeDefined();
|
2019-12-26 22:10:19 +05:30
|
|
|
});
|
|
|
|
|
2021-02-22 17:27:13 +05:30
|
|
|
it('should support quick actions', () => {
|
|
|
|
mountComponent({ mountFunction: mount });
|
2019-12-26 22:10:19 +05:30
|
|
|
|
2023-05-27 22:25:52 +05:30
|
|
|
expect(findMarkdownEditor().props('supportsQuickActions')).toBe(true);
|
2021-02-22 17:27:13 +05:30
|
|
|
});
|
2019-12-26 22:10:19 +05:30
|
|
|
|
2021-02-22 17:27:13 +05:30
|
|
|
it('should link to markdown docs', () => {
|
|
|
|
mountComponent({ mountFunction: mount });
|
2019-12-26 22:10:19 +05:30
|
|
|
|
2021-02-22 17:27:13 +05:30
|
|
|
const { markdownDocsPath } = notesDataMock;
|
2019-12-26 22:10:19 +05:30
|
|
|
|
2021-02-22 17:27:13 +05:30
|
|
|
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' } });
|
2019-12-26 22:10:19 +05:30
|
|
|
|
2021-02-22 17:27:13 +05:30
|
|
|
jest.spyOn(wrapper.vm, 'discard');
|
2019-12-26 22:10:19 +05:30
|
|
|
|
2021-02-22 17:27:13 +05:30
|
|
|
wrapper.vm.discard();
|
|
|
|
|
|
|
|
await nextTick();
|
2019-12-26 22:10:19 +05:30
|
|
|
|
|
|
|
expect(Autosize.update).toHaveBeenCalled();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('edit mode', () => {
|
2021-02-22 17:27:13 +05:30
|
|
|
beforeEach(() => {
|
2021-03-08 18:12:59 +05:30
|
|
|
mountComponent({ mountFunction: mount });
|
2021-02-22 17:27:13 +05:30
|
|
|
});
|
|
|
|
|
2019-12-26 22:10:19 +05:30
|
|
|
it('should enter edit mode when arrow up is pressed', () => {
|
|
|
|
jest.spyOn(wrapper.vm, 'editCurrentUserLastNote');
|
2021-02-22 17:27:13 +05:30
|
|
|
|
2023-05-27 22:25:52 +05:30
|
|
|
findMarkdownEditorTextarea().trigger('keydown.up');
|
2019-12-26 22:10:19 +05:30
|
|
|
|
|
|
|
expect(wrapper.vm.editCurrentUserLastNote).toHaveBeenCalled();
|
|
|
|
});
|
|
|
|
|
2023-05-27 22:25:52 +05:30
|
|
|
describe('event enter', () => {
|
|
|
|
describe('when no draft exists', () => {
|
|
|
|
it('should save note when cmd+enter is pressed', () => {
|
|
|
|
jest.spyOn(wrapper.vm, 'handleSave');
|
2019-12-26 22:10:19 +05:30
|
|
|
|
2023-05-27 22:25:52 +05:30
|
|
|
findMarkdownEditorTextarea().trigger('keydown.enter', { metaKey: true });
|
2019-12-26 22:10:19 +05:30
|
|
|
|
2023-05-27 22:25:52 +05:30
|
|
|
expect(wrapper.vm.handleSave).toHaveBeenCalledWith();
|
|
|
|
});
|
2021-09-04 01:27:46 +05:30
|
|
|
|
2023-05-27 22:25:52 +05:30
|
|
|
it('should save note when ctrl+enter is pressed', () => {
|
|
|
|
jest.spyOn(wrapper.vm, 'handleSave');
|
2021-09-04 01:27:46 +05:30
|
|
|
|
2023-05-27 22:25:52 +05:30
|
|
|
findMarkdownEditorTextarea().trigger('keydown.enter', { ctrlKey: true });
|
2021-09-04 01:27:46 +05:30
|
|
|
|
2023-05-27 22:25:52 +05:30
|
|
|
expect(wrapper.vm.handleSave).toHaveBeenCalledWith();
|
|
|
|
});
|
2021-09-04 01:27:46 +05:30
|
|
|
});
|
2019-12-26 22:10:19 +05:30
|
|
|
|
2023-05-27 22:25:52 +05:30
|
|
|
describe('when a draft exists', () => {
|
|
|
|
beforeEach(() => {
|
|
|
|
store.registerModule('batchComments', batchComments());
|
|
|
|
store.state.batchComments.drafts = [{ note: 'A' }];
|
|
|
|
});
|
2021-09-04 01:27:46 +05:30
|
|
|
|
2023-05-27 22:25:52 +05:30
|
|
|
it('should save note draft when cmd+enter is pressed', () => {
|
|
|
|
jest.spyOn(wrapper.vm, 'handleSaveDraft');
|
2021-09-04 01:27:46 +05:30
|
|
|
|
2023-05-27 22:25:52 +05:30
|
|
|
findMarkdownEditorTextarea().trigger('keydown.enter', { metaKey: true });
|
2021-09-04 01:27:46 +05:30
|
|
|
|
2023-05-27 22:25:52 +05:30
|
|
|
expect(wrapper.vm.handleSaveDraft).toHaveBeenCalledWith();
|
|
|
|
});
|
2021-09-04 01:27:46 +05:30
|
|
|
|
2023-05-27 22:25:52 +05:30
|
|
|
it('should save note draft when ctrl+enter is pressed', () => {
|
|
|
|
jest.spyOn(wrapper.vm, 'handleSaveDraft');
|
2021-02-22 17:27:13 +05:30
|
|
|
|
2023-05-27 22:25:52 +05:30
|
|
|
findMarkdownEditorTextarea().trigger('keydown.enter', { ctrlKey: true });
|
2019-12-26 22:10:19 +05:30
|
|
|
|
2023-05-27 22:25:52 +05:30
|
|
|
expect(wrapper.vm.handleSaveDraft).toHaveBeenCalledWith();
|
|
|
|
});
|
2021-09-04 01:27:46 +05:30
|
|
|
});
|
2019-12-26 22:10:19 +05:30
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('actions', () => {
|
|
|
|
it('should be possible to close the issue', () => {
|
2021-02-22 17:27:13 +05:30
|
|
|
mountComponent();
|
|
|
|
|
|
|
|
expect(findCloseReopenButton().text()).toBe('Close issue');
|
2019-12-26 22:10:19 +05:30
|
|
|
});
|
|
|
|
|
2022-07-16 23:28:13 +05:30
|
|
|
it.each`
|
2022-07-23 23:45:48 +05:30
|
|
|
noteIsInternal | buttonText
|
|
|
|
${false} | ${'Comment'}
|
|
|
|
${true} | ${'Add internal note'}
|
|
|
|
`('renders comment button with text "$buttonText"', ({ noteIsInternal, buttonText }) => {
|
2022-07-16 23:28:13 +05:30
|
|
|
mountComponent({
|
|
|
|
mountFunction: mount,
|
2022-07-23 23:45:48 +05:30
|
|
|
noteableData: createNotableDataMock({ confidential: noteIsInternal }),
|
|
|
|
initialData: { noteIsInternal },
|
2022-07-16 23:28:13 +05:30
|
|
|
});
|
|
|
|
|
|
|
|
expect(findCommentButton().text()).toBe(buttonText);
|
|
|
|
});
|
|
|
|
|
2019-12-26 22:10:19 +05:30
|
|
|
it('should render comment button as disabled', () => {
|
2021-02-22 17:27:13 +05:30
|
|
|
mountComponent();
|
|
|
|
|
2021-11-11 11:23:49 +05:30
|
|
|
expect(findCommentTypeDropdown().props('disabled')).toBe(true);
|
2019-12-26 22:10:19 +05:30
|
|
|
});
|
|
|
|
|
2021-02-22 17:27:13 +05:30
|
|
|
it('should enable comment button if it has note', async () => {
|
|
|
|
mountComponent();
|
|
|
|
|
2022-03-02 08:16:31 +05:30
|
|
|
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
|
|
|
|
// eslint-disable-next-line no-restricted-syntax
|
2021-02-22 17:27:13 +05:30
|
|
|
await wrapper.setData({ note: 'Foo' });
|
|
|
|
|
2021-11-11 11:23:49 +05:30
|
|
|
expect(findCommentTypeDropdown().props('disabled')).toBe(false);
|
2019-12-26 22:10:19 +05:30
|
|
|
});
|
|
|
|
|
2021-02-22 17:27:13 +05:30
|
|
|
it('should update buttons texts when it has note', () => {
|
|
|
|
mountComponent({ initialData: { note: 'Foo' } });
|
|
|
|
|
|
|
|
expect(findCloseReopenButton().text()).toBe('Comment & close issue');
|
2019-12-26 22:10:19 +05:30
|
|
|
});
|
|
|
|
|
2021-02-22 17:27:13 +05:30
|
|
|
it('updates button text with noteable type', () => {
|
|
|
|
mountComponent({ noteableType: constants.MERGE_REQUEST_NOTEABLE_TYPE });
|
|
|
|
|
|
|
|
expect(findCloseReopenButton().text()).toBe('Close merge request');
|
2019-12-26 22:10:19 +05:30
|
|
|
});
|
|
|
|
|
|
|
|
describe('when clicking close/reopen button', () => {
|
2021-02-22 17:27:13 +05:30
|
|
|
it('should show a loading spinner', async () => {
|
|
|
|
mountComponent({
|
|
|
|
noteableType: constants.MERGE_REQUEST_NOTEABLE_TYPE,
|
|
|
|
mountFunction: mount,
|
|
|
|
});
|
2019-12-26 22:10:19 +05:30
|
|
|
|
2021-02-22 17:27:13 +05:30
|
|
|
await findCloseReopenButton().trigger('click');
|
2019-12-26 22:10:19 +05:30
|
|
|
|
2021-02-22 17:27:13 +05:30
|
|
|
expect(findCloseReopenButton().props('loading')).toBe(true);
|
2019-12-26 22:10:19 +05:30
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('when toggling state', () => {
|
2021-02-22 17:27:13 +05:30
|
|
|
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,
|
2023-05-27 22:25:52 +05:30
|
|
|
noteableData: { ...noteableDataMock, state: STATUS_OPEN },
|
2021-02-22 17:27:13 +05:30
|
|
|
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,
|
2023-05-27 22:25:52 +05:30
|
|
|
noteableData: { ...noteableDataMock, state: STATUS_OPEN },
|
2021-02-22 17:27:13 +05:30
|
|
|
mountFunction: mount,
|
|
|
|
});
|
|
|
|
|
|
|
|
jest.spyOn(wrapper.vm, 'closeIssuable').mockRejectedValue();
|
|
|
|
|
|
|
|
await findCloseReopenButton().trigger('click');
|
|
|
|
|
2022-07-23 23:45:48 +05:30
|
|
|
await nextTick();
|
|
|
|
await nextTick();
|
2021-02-22 17:27:13 +05:30
|
|
|
|
2022-11-25 23:54:43 +05:30
|
|
|
expect(createAlert).toHaveBeenCalledWith({
|
2021-09-30 23:02:18 +05:30
|
|
|
message: `Something went wrong while closing the ${type}. Please try again later.`,
|
|
|
|
});
|
2021-02-22 17:27:13 +05:30
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('when closed', () => {
|
|
|
|
it('makes an API call to close it', () => {
|
|
|
|
mountComponent({
|
|
|
|
noteableType,
|
2023-05-27 22:25:52 +05:30
|
|
|
noteableData: { ...noteableDataMock, state: STATUS_CLOSED },
|
2021-02-22 17:27:13 +05:30
|
|
|
mountFunction: mount,
|
|
|
|
});
|
|
|
|
|
|
|
|
jest.spyOn(wrapper.vm, 'reopenIssuable').mockResolvedValue();
|
2019-12-26 22:10:19 +05:30
|
|
|
|
2021-02-22 17:27:13 +05:30
|
|
|
findCloseReopenButton().trigger('click');
|
2019-12-26 22:10:19 +05:30
|
|
|
|
2021-02-22 17:27:13 +05:30
|
|
|
expect(wrapper.vm.reopenIssuable).toHaveBeenCalled();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
it(`shows an error when the API call fails`, async () => {
|
|
|
|
mountComponent({
|
|
|
|
noteableType,
|
2023-05-27 22:25:52 +05:30
|
|
|
noteableData: { ...noteableDataMock, state: STATUS_CLOSED },
|
2021-02-22 17:27:13 +05:30
|
|
|
mountFunction: mount,
|
|
|
|
});
|
|
|
|
|
|
|
|
jest.spyOn(wrapper.vm, 'reopenIssuable').mockRejectedValue();
|
|
|
|
|
|
|
|
await findCloseReopenButton().trigger('click');
|
2019-12-26 22:10:19 +05:30
|
|
|
|
2022-07-23 23:45:48 +05:30
|
|
|
await nextTick();
|
|
|
|
await nextTick();
|
2021-02-22 17:27:13 +05:30
|
|
|
|
2022-11-25 23:54:43 +05:30
|
|
|
expect(createAlert).toHaveBeenCalledWith({
|
2021-09-30 23:02:18 +05:30
|
|
|
message: `Something went wrong while reopening the ${type}. Please try again later.`,
|
|
|
|
});
|
2019-12-26 22:10:19 +05:30
|
|
|
});
|
|
|
|
});
|
2021-02-22 17:27:13 +05:30
|
|
|
|
|
|
|
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');
|
|
|
|
|
2022-04-04 11:22:00 +05:30
|
|
|
await nextTick();
|
2021-06-08 01:23:25 +05:30
|
|
|
|
2021-02-22 17:27:13 +05:30
|
|
|
expect(refreshUserMergeRequestCounts).toHaveBeenCalled();
|
|
|
|
});
|
2019-12-26 22:10:19 +05:30
|
|
|
});
|
|
|
|
});
|
2021-03-11 19:13:27 +05:30
|
|
|
|
|
|
|
describe('confidential notes checkbox', () => {
|
2022-08-13 15:12:31 +05:30
|
|
|
it('should render checkbox as unchecked by default', () => {
|
|
|
|
mountComponent({
|
|
|
|
mountFunction: mount,
|
|
|
|
initialData: { note: 'confidential note' },
|
|
|
|
noteableData: { ...notableDataMockCanUpdateIssuable },
|
|
|
|
});
|
2021-03-11 19:13:27 +05:30
|
|
|
|
2022-08-13 15:12:31 +05:30
|
|
|
const checkbox = findConfidentialNoteCheckbox();
|
|
|
|
expect(checkbox.exists()).toBe(true);
|
|
|
|
expect(checkbox.element.checked).toBe(false);
|
|
|
|
});
|
|
|
|
|
2022-11-25 23:54:43 +05:30
|
|
|
it('should not render checkbox if user is not at least a reporter', () => {
|
|
|
|
mountComponent({
|
|
|
|
mountFunction: mount,
|
|
|
|
initialData: { note: 'confidential note' },
|
|
|
|
noteableData: { ...notableDataMockCannotCreateConfidentialNote },
|
|
|
|
});
|
|
|
|
|
|
|
|
const checkbox = findConfidentialNoteCheckbox();
|
|
|
|
expect(checkbox.exists()).toBe(false);
|
|
|
|
});
|
|
|
|
|
2022-08-13 15:12:31 +05:30
|
|
|
it.each`
|
|
|
|
noteableType | rendered | message
|
|
|
|
${'Issue'} | ${true} | ${'render'}
|
|
|
|
${'Epic'} | ${true} | ${'render'}
|
|
|
|
${'MergeRequest'} | ${false} | ${'not render'}
|
|
|
|
`(
|
|
|
|
'should $message checkbox when noteableType is $noteableType',
|
|
|
|
({ noteableType, rendered }) => {
|
2021-03-11 19:13:27 +05:30
|
|
|
mountComponent({
|
|
|
|
mountFunction: mount,
|
2022-08-13 15:12:31 +05:30
|
|
|
noteableType,
|
|
|
|
initialData: { note: 'internal note' },
|
|
|
|
noteableData: { ...notableDataMockCanUpdateIssuable, noteableType },
|
2021-03-11 19:13:27 +05:30
|
|
|
});
|
|
|
|
|
2022-08-13 15:12:31 +05:30
|
|
|
expect(findConfidentialNoteCheckbox().exists()).toBe(rendered);
|
|
|
|
},
|
|
|
|
);
|
2021-03-11 19:13:27 +05:30
|
|
|
|
2022-08-13 15:12:31 +05:30
|
|
|
describe.each`
|
|
|
|
shouldCheckboxBeChecked
|
|
|
|
${true}
|
|
|
|
${false}
|
|
|
|
`('when checkbox value is `$shouldCheckboxBeChecked`', ({ shouldCheckboxBeChecked }) => {
|
2022-10-11 01:57:18 +05:30
|
|
|
it(`sets \`internal\` to \`${shouldCheckboxBeChecked}\``, async () => {
|
2021-03-11 19:13:27 +05:30
|
|
|
mountComponent({
|
|
|
|
mountFunction: mount,
|
2022-10-11 01:57:18 +05:30
|
|
|
initialData: { note: 'internal note' },
|
2021-03-11 19:13:27 +05:30
|
|
|
noteableData: { ...notableDataMockCanUpdateIssuable },
|
|
|
|
});
|
|
|
|
|
2022-08-13 15:12:31 +05:30
|
|
|
jest.spyOn(wrapper.vm, 'saveNote').mockResolvedValue({});
|
2021-03-11 19:13:27 +05:30
|
|
|
|
2022-08-13 15:12:31 +05:30
|
|
|
const checkbox = findConfidentialNoteCheckbox();
|
2021-03-11 19:13:27 +05:30
|
|
|
|
2022-08-13 15:12:31 +05:30
|
|
|
// check checkbox
|
|
|
|
checkbox.element.checked = shouldCheckboxBeChecked;
|
|
|
|
checkbox.trigger('change');
|
|
|
|
await nextTick();
|
2021-03-11 19:13:27 +05:30
|
|
|
|
2022-08-13 15:12:31 +05:30
|
|
|
// submit comment
|
|
|
|
findCommentButton().trigger('click');
|
2021-03-11 19:13:27 +05:30
|
|
|
|
2022-08-13 15:12:31 +05:30
|
|
|
const [providedData] = wrapper.vm.saveNote.mock.calls[0];
|
2022-10-11 01:57:18 +05:30
|
|
|
expect(providedData.data.note.internal).toBe(shouldCheckboxBeChecked);
|
2021-03-11 19:13:27 +05:30
|
|
|
});
|
2022-08-13 15:12:31 +05:30
|
|
|
});
|
2021-03-11 19:13:27 +05:30
|
|
|
|
2022-08-13 15:12:31 +05:30
|
|
|
describe('when user cannot update issuable', () => {
|
|
|
|
it('should not render checkbox', () => {
|
|
|
|
mountComponent({
|
|
|
|
mountFunction: mount,
|
|
|
|
noteableData: { ...notableDataMockCannotUpdateIssuable },
|
2021-03-11 19:13:27 +05:30
|
|
|
});
|
2022-08-13 15:12:31 +05:30
|
|
|
|
|
|
|
expect(findConfidentialNoteCheckbox().exists()).toBe(false);
|
2021-03-11 19:13:27 +05:30
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
2019-12-26 22:10:19 +05:30
|
|
|
});
|
|
|
|
|
2023-06-20 00:43:36 +05:30
|
|
|
describe('check sensitive tokens', () => {
|
|
|
|
const sensitiveMessage = 'token: glpat-1234567890abcdefghij';
|
|
|
|
const nonSensitiveMessage = 'text';
|
|
|
|
|
|
|
|
it('should not save note when it contains sensitive token', () => {
|
|
|
|
mountComponent({
|
|
|
|
mountFunction: mount,
|
|
|
|
initialData: { note: sensitiveMessage },
|
|
|
|
});
|
|
|
|
|
|
|
|
jest.spyOn(wrapper.vm, 'saveNote').mockResolvedValue();
|
|
|
|
|
|
|
|
clickCommentButton();
|
|
|
|
|
|
|
|
expect(wrapper.vm.saveNote).not.toHaveBeenCalled();
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should save note it does not contain sensitive token', () => {
|
|
|
|
mountComponent({
|
|
|
|
mountFunction: mount,
|
|
|
|
initialData: { note: nonSensitiveMessage },
|
|
|
|
});
|
|
|
|
|
|
|
|
jest.spyOn(wrapper.vm, 'saveNote').mockResolvedValue();
|
|
|
|
|
|
|
|
clickCommentButton();
|
|
|
|
|
|
|
|
expect(wrapper.vm.saveNote).toHaveBeenCalled();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2019-12-26 22:10:19 +05:30
|
|
|
describe('user is not logged in', () => {
|
|
|
|
beforeEach(() => {
|
2021-02-22 17:27:13 +05:30
|
|
|
mountComponent({ userData: null, noteableData: loggedOutnoteableData, mountFunction: mount });
|
2019-12-26 22:10:19 +05:30
|
|
|
});
|
|
|
|
|
|
|
|
it('should render signed out widget', () => {
|
2021-02-22 17:27:13 +05:30
|
|
|
expect(wrapper.text()).toBe('Please register or sign in to reply');
|
2019-12-26 22:10:19 +05:30
|
|
|
});
|
|
|
|
|
|
|
|
it('should not render submission form', () => {
|
2023-05-27 22:25:52 +05:30
|
|
|
expect(findMarkdownEditor().exists()).toBe(false);
|
2019-12-26 22:10:19 +05:30
|
|
|
});
|
|
|
|
});
|
2021-04-29 21:17:54 +05:30
|
|
|
|
|
|
|
describe('with batchComments in store', () => {
|
|
|
|
beforeEach(() => {
|
|
|
|
store.registerModule('batchComments', batchComments());
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('add to review and comment now buttons', () => {
|
|
|
|
it('when no drafts exist, should not render', () => {
|
|
|
|
mountComponent();
|
|
|
|
|
2021-11-11 11:23:49 +05:30
|
|
|
expect(findCommentTypeDropdown().exists()).toBe(true);
|
2021-04-29 21:17:54 +05:30
|
|
|
expect(findAddToReviewButton().exists()).toBe(false);
|
|
|
|
expect(findAddCommentNowButton().exists()).toBe(false);
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('when drafts exist', () => {
|
|
|
|
beforeEach(() => {
|
|
|
|
store.state.batchComments.drafts = [{ note: 'A' }];
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should render', () => {
|
|
|
|
mountComponent();
|
|
|
|
|
2021-11-11 11:23:49 +05:30
|
|
|
expect(findCommentTypeDropdown().exists()).toBe(false);
|
2021-04-29 21:17:54 +05:30
|
|
|
expect(findAddToReviewButton().exists()).toBe(true);
|
|
|
|
expect(findAddCommentNowButton().exists()).toBe(true);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('clicking `add to review`, should call draft endpoint, set `isDraft` true', () => {
|
|
|
|
mountComponent({ mountFunction: mount, initialData: { note: 'a draft note' } });
|
|
|
|
|
|
|
|
jest.spyOn(store, 'dispatch').mockResolvedValue();
|
|
|
|
findAddToReviewButton().trigger('click');
|
|
|
|
|
|
|
|
expect(store.dispatch).toHaveBeenCalledWith(
|
|
|
|
'saveNote',
|
|
|
|
expect.objectContaining({
|
|
|
|
endpoint: notesDataMock.draftsPath,
|
|
|
|
isDraft: true,
|
|
|
|
}),
|
|
|
|
);
|
|
|
|
});
|
|
|
|
|
2022-10-11 01:57:18 +05:30
|
|
|
it('clicking `add comment now`, should call note endpoint, set `isDraft` false', () => {
|
2021-04-29 21:17:54 +05:30
|
|
|
mountComponent({ mountFunction: mount, initialData: { note: 'a comment' } });
|
|
|
|
|
|
|
|
jest.spyOn(store, 'dispatch').mockResolvedValue();
|
|
|
|
findAddCommentNowButton().trigger('click');
|
|
|
|
|
|
|
|
expect(store.dispatch).toHaveBeenCalledWith(
|
|
|
|
'saveNote',
|
|
|
|
expect.objectContaining({
|
|
|
|
endpoint: noteableDataMock.create_note_path,
|
|
|
|
isDraft: false,
|
|
|
|
}),
|
|
|
|
);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
2019-12-26 22:10:19 +05:30
|
|
|
});
|