debian-mirror-gitlab/spec/frontend/snippets/components/edit_spec.js

485 lines
16 KiB
JavaScript
Raw Normal View History

2022-07-16 23:28:13 +05:30
import { GlFormGroup, GlLoadingIcon } from '@gitlab/ui';
2022-04-04 11:22:00 +05:30
import Vue, { nextTick } from 'vue';
2021-03-11 19:13:27 +05:30
import { merge } from 'lodash';
2022-04-04 11:22:00 +05:30
2021-03-11 19:13:27 +05:30
import VueApollo, { ApolloMutation } from 'vue-apollo';
import { useFakeDate } from 'helpers/fake_date';
2021-03-08 18:12:59 +05:30
import createMockApollo from 'helpers/mock_apollo_helper';
2022-08-13 15:12:31 +05:30
import { stubPerformanceWebAPI } from 'helpers/performance';
2021-03-11 19:13:27 +05:30
import waitForPromises from 'helpers/wait_for_promises';
2022-07-16 23:28:13 +05:30
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
2021-01-29 00:20:46 +05:30
import GetSnippetQuery from 'shared_queries/snippet/snippet.query.graphql';
2022-11-25 23:54:43 +05:30
import { createAlert } from '~/flash';
2020-10-24 23:57:45 +05:30
import * as urlUtils from '~/lib/utils/url_utility';
2020-04-22 19:07:51 +05:30
import SnippetEditApp from '~/snippets/components/edit.vue';
2021-03-11 19:13:27 +05:30
import SnippetBlobActionsEdit from '~/snippets/components/snippet_blob_actions_edit.vue';
2020-04-22 19:07:51 +05:30
import SnippetDescriptionEdit from '~/snippets/components/snippet_description_edit.vue';
import SnippetVisibilityEdit from '~/snippets/components/snippet_visibility_edit.vue';
2021-01-29 00:20:46 +05:30
import {
2022-10-11 01:57:18 +05:30
VISIBILITY_LEVEL_PRIVATE_STRING,
VISIBILITY_LEVEL_INTERNAL_STRING,
VISIBILITY_LEVEL_PUBLIC_STRING,
} from '~/visibility_level/constants';
2022-06-21 17:19:12 +05:30
import CreateSnippetMutation from '~/snippets/mutations/create_snippet.mutation.graphql';
import UpdateSnippetMutation from '~/snippets/mutations/update_snippet.mutation.graphql';
2021-03-11 19:13:27 +05:30
import FormFooterActions from '~/vue_shared/components/form/form_footer_actions.vue';
import { testEntries, createGQLSnippetsQueryResponse, createGQLSnippet } from '../test_utils';
2020-04-22 19:07:51 +05:30
2020-06-23 00:09:42 +05:30
jest.mock('~/flash');
2020-10-24 23:57:45 +05:30
const TEST_UPLOADED_FILES = ['foo/bar.txt', 'alpha/beta.js'];
2021-03-11 19:13:27 +05:30
const TEST_API_ERROR = new Error('TEST_API_ERROR');
const TEST_MUTATION_ERROR = 'Test mutation error';
2020-10-24 23:57:45 +05:30
const TEST_ACTIONS = {
2021-03-11 19:13:27 +05:30
NO_CONTENT: merge({}, testEntries.created.diff, { content: '' }),
NO_PATH: merge({}, testEntries.created.diff, { filePath: '' }),
VALID: merge({}, testEntries.created.diff),
2020-07-28 23:09:34 +05:30
};
2020-10-24 23:57:45 +05:30
const TEST_WEB_URL = '/snippets/7';
2021-03-11 19:13:27 +05:30
const TEST_SNIPPET_GID = 'gid://gitlab/PersonalSnippet/42';
const createSnippet = () =>
merge(createGQLSnippet(), {
webUrl: TEST_WEB_URL,
2022-10-11 01:57:18 +05:30
visibilityLevel: VISIBILITY_LEVEL_PRIVATE_STRING,
2021-03-11 19:13:27 +05:30
});
const createQueryResponse = (obj = {}) =>
createGQLSnippetsQueryResponse([merge(createSnippet(), obj)]);
const createMutationResponse = (key, obj = {}) => ({
data: {
[key]: merge(
{
errors: [],
snippet: {
__typename: 'Snippet',
2022-01-26 12:08:38 +05:30
id: 1,
2021-03-11 19:13:27 +05:30
webUrl: TEST_WEB_URL,
},
},
obj,
),
},
});
const createMutationResponseWithErrors = (key) =>
createMutationResponse(key, { errors: [TEST_MUTATION_ERROR] });
const getApiData = ({
id,
title = '',
description = '',
2022-10-11 01:57:18 +05:30
visibilityLevel = VISIBILITY_LEVEL_PRIVATE_STRING,
2021-03-11 19:13:27 +05:30
} = {}) => ({
id,
title,
description,
visibilityLevel,
blobActions: [],
2020-10-24 23:57:45 +05:30
});
2022-04-04 11:22:00 +05:30
Vue.use(VueApollo);
2021-03-11 19:13:27 +05:30
2020-04-22 19:07:51 +05:30
describe('Snippet Edit app', () => {
2021-03-11 19:13:27 +05:30
useFakeDate();
2020-04-22 19:07:51 +05:30
let wrapper;
2021-03-11 19:13:27 +05:30
let getSpy;
// Mutate spy receives a "key" so that we can:
// - Use the same spy whether we are creating or updating.
// - Build the correct response object
// - Assert which mutation was sent
let mutateSpy;
2020-11-24 15:15:51 +05:30
const relativeUrlRoot = '/foo/';
const originalRelativeUrlRoot = gon.relative_url_root;
2020-04-22 19:07:51 +05:30
2021-03-11 19:13:27 +05:30
beforeEach(() => {
2022-08-13 15:12:31 +05:30
stubPerformanceWebAPI();
2021-03-11 19:13:27 +05:30
getSpy = jest.fn().mockResolvedValue(createQueryResponse());
2021-01-29 00:20:46 +05:30
2021-03-11 19:13:27 +05:30
// See `mutateSpy` declaration comment for why we send a key
mutateSpy = jest.fn().mockImplementation((key) => Promise.resolve(createMutationResponse(key)));
2020-04-22 19:07:51 +05:30
2020-11-24 15:15:51 +05:30
gon.relative_url_root = relativeUrlRoot;
2020-10-24 23:57:45 +05:30
jest.spyOn(urlUtils, 'redirectTo').mockImplementation();
});
2020-04-22 19:07:51 +05:30
afterEach(() => {
wrapper.destroy();
2020-10-24 23:57:45 +05:30
wrapper = null;
2020-11-24 15:15:51 +05:30
gon.relative_url_root = originalRelativeUrlRoot;
2020-04-22 19:07:51 +05:30
});
2022-07-16 23:28:13 +05:30
const findBlobActions = () => wrapper.findComponent(SnippetBlobActionsEdit);
const findCancelButton = () => wrapper.findByTestId('snippet-cancel-btn');
const clickSubmitBtn = () => wrapper.findByTestId('snippet-edit-form').trigger('submit');
2021-03-08 18:12:59 +05:30
const triggerBlobActions = (actions) => findBlobActions().vm.$emit('actions', actions);
const setUploadFilesHtml = (paths) => {
wrapper.vm.$el.innerHTML = paths
.map((path) => `<input name="files[]" value="${path}">`)
.join('');
2020-10-24 23:57:45 +05:30
};
2022-07-16 23:28:13 +05:30
const setTitle = (val) => wrapper.findByTestId('snippet-title-input').vm.$emit('input', val);
const setDescription = (val) =>
wrapper.findComponent(SnippetDescriptionEdit).vm.$emit('input', val);
2020-10-24 23:57:45 +05:30
2022-10-11 01:57:18 +05:30
const createComponent = ({
props = {},
selectedLevel = VISIBILITY_LEVEL_PRIVATE_STRING,
} = {}) => {
2021-03-11 19:13:27 +05:30
if (wrapper) {
throw new Error('wrapper already created');
2020-10-24 23:57:45 +05:30
}
2021-03-11 19:13:27 +05:30
const requestHandlers = [
[GetSnippetQuery, getSpy],
// See `mutateSpy` declaration comment for why we send a key
[UpdateSnippetMutation, (...args) => mutateSpy('updateSnippet', ...args)],
[CreateSnippetMutation, (...args) => mutateSpy('createSnippet', ...args)],
];
const apolloProvider = createMockApollo(requestHandlers);
2022-07-16 23:28:13 +05:30
wrapper = shallowMountExtended(SnippetEditApp, {
2021-03-11 19:13:27 +05:30
apolloProvider,
stubs: {
ApolloMutation,
FormFooterActions,
},
provide: {
selectedLevel,
},
propsData: {
snippetGid: TEST_SNIPPET_GID,
markdownPreviewPath: 'http://preview.foo.bar',
markdownDocsPath: 'http://docs.foo.bar',
...props,
},
});
2020-10-24 23:57:45 +05:30
};
2020-04-22 19:07:51 +05:30
2021-03-11 19:13:27 +05:30
// Creates comopnent and waits for gql load
const createComponentAndLoad = async (...args) => {
createComponent(...args);
await waitForPromises();
};
// Creates loaded component and submits form
const createComponentAndSubmit = async (...args) => {
await createComponentAndLoad(...args);
clickSubmitBtn();
await waitForPromises();
};
describe('when loading', () => {
it('renders loader', () => {
createComponent();
2022-07-16 23:28:13 +05:30
expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
2020-04-22 19:07:51 +05:30
});
2021-03-11 19:13:27 +05:30
});
2020-04-22 19:07:51 +05:30
2021-03-11 19:13:27 +05:30
describe.each`
snippetGid | expectedQueries
${TEST_SNIPPET_GID} | ${[[{ ids: [TEST_SNIPPET_GID] }]]}
${''} | ${[]}
`('when loaded with snippetGid=$snippetGid', ({ snippetGid, expectedQueries }) => {
beforeEach(() => createComponentAndLoad({ props: { snippetGid } }));
2020-04-22 19:07:51 +05:30
2021-03-11 19:13:27 +05:30
it(`queries with ${JSON.stringify(expectedQueries)}`, () => {
expect(getSpy.mock.calls).toEqual(expectedQueries);
});
2020-05-24 23:13:21 +05:30
2021-03-11 19:13:27 +05:30
it('should render components', () => {
2022-07-16 23:28:13 +05:30
expect(wrapper.findComponent(GlFormGroup).attributes('label')).toEqual('Title');
expect(wrapper.findComponent(SnippetDescriptionEdit).exists()).toBe(true);
expect(wrapper.findComponent(SnippetVisibilityEdit).exists()).toBe(true);
expect(wrapper.findComponent(FormFooterActions).exists()).toBe(true);
2021-03-11 19:13:27 +05:30
expect(findBlobActions().exists()).toBe(true);
});
it('should hide loader', () => {
2022-11-25 23:54:43 +05:30
expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(false);
2021-03-11 19:13:27 +05:30
});
});
describe('default', () => {
2020-05-24 23:13:21 +05:30
it.each`
2022-07-16 23:28:13 +05:30
title | actions | titleHasErrors | blobActionsHasErrors
${''} | ${[]} | ${true} | ${false}
${''} | ${[TEST_ACTIONS.VALID]} | ${true} | ${false}
${'foo'} | ${[]} | ${false} | ${false}
${'foo'} | ${[TEST_ACTIONS.VALID]} | ${false} | ${false}
${'foo'} | ${[TEST_ACTIONS.VALID, TEST_ACTIONS.NO_CONTENT]} | ${false} | ${true}
${'foo'} | ${[TEST_ACTIONS.VALID, TEST_ACTIONS.NO_PATH]} | ${false} | ${false}
2020-10-24 23:57:45 +05:30
`(
2022-07-16 23:28:13 +05:30
'validates correctly (title="$title", actions="$actions", titleHasErrors="$titleHasErrors", blobActionsHasErrors="$blobActionsHasErrors")',
async ({ title, actions, titleHasErrors, blobActionsHasErrors }) => {
2021-03-11 19:13:27 +05:30
getSpy.mockResolvedValue(createQueryResponse({ title }));
await createComponentAndLoad();
2020-05-24 23:13:21 +05:30
2020-10-24 23:57:45 +05:30
triggerBlobActions(actions);
2020-04-22 19:07:51 +05:30
2022-07-16 23:28:13 +05:30
clickSubmitBtn();
2021-03-11 19:13:27 +05:30
await nextTick();
2020-04-22 19:07:51 +05:30
2022-07-16 23:28:13 +05:30
expect(wrapper.findComponent(GlFormGroup).exists()).toBe(true);
expect(Boolean(wrapper.findComponent(GlFormGroup).attributes('state'))).toEqual(
!titleHasErrors,
);
2022-11-25 23:54:43 +05:30
expect(wrapper.findComponent(SnippetBlobActionsEdit).props('isValid')).toEqual(
2022-07-16 23:28:13 +05:30
!blobActionsHasErrors,
);
2020-10-24 23:57:45 +05:30
},
);
2020-04-22 19:07:51 +05:30
2020-10-24 23:57:45 +05:30
it.each`
2021-03-11 19:13:27 +05:30
projectPath | snippetGid | expectation
${''} | ${''} | ${urlUtils.joinPaths('/', relativeUrlRoot, '-', 'snippets')}
${'project/path'} | ${''} | ${urlUtils.joinPaths('/', relativeUrlRoot, 'project/path/-', 'snippets')}
${''} | ${TEST_SNIPPET_GID} | ${TEST_WEB_URL}
${'project/path'} | ${TEST_SNIPPET_GID} | ${TEST_WEB_URL}
2020-10-24 23:57:45 +05:30
`(
2021-03-11 19:13:27 +05:30
'should set cancel href (projectPath="$projectPath", snippetGid="$snippetGid")',
async ({ projectPath, snippetGid, expectation }) => {
await createComponentAndLoad({
props: {
projectPath,
snippetGid,
},
2020-04-22 19:07:51 +05:30
});
2020-10-24 23:57:45 +05:30
expect(findCancelButton().attributes('href')).toBe(expectation);
},
);
2021-01-29 00:20:46 +05:30
2022-10-11 01:57:18 +05:30
it.each([
VISIBILITY_LEVEL_PRIVATE_STRING,
VISIBILITY_LEVEL_INTERNAL_STRING,
VISIBILITY_LEVEL_PUBLIC_STRING,
])('marks %s visibility by default', async (visibility) => {
createComponent({
props: { snippetGid: '' },
selectedLevel: visibility,
});
2022-11-25 23:54:43 +05:30
expect(wrapper.findComponent(SnippetVisibilityEdit).props('value')).toBe(visibility);
2022-10-11 01:57:18 +05:30
});
2021-01-29 00:20:46 +05:30
2020-10-24 23:57:45 +05:30
describe('form submission handling', () => {
2022-07-16 23:28:13 +05:30
describe('when creating a new snippet', () => {
it.each`
projectPath | uploadedFiles | input
${''} | ${TEST_UPLOADED_FILES} | ${{ ...getApiData({ title: 'Title' }), projectPath: '', uploadedFiles: TEST_UPLOADED_FILES }}
${'project/path'} | ${TEST_UPLOADED_FILES} | ${{ ...getApiData({ title: 'Title' }), projectPath: 'project/path', uploadedFiles: TEST_UPLOADED_FILES }}
`(
'should submit a createSnippet mutation (projectPath=$projectPath, uploadedFiles=$uploadedFiles)',
async ({ projectPath, uploadedFiles, input }) => {
await createComponentAndLoad({
props: {
snippetGid: '',
projectPath,
},
});
setTitle(input.title);
setUploadFilesHtml(uploadedFiles);
await nextTick();
clickSubmitBtn();
expect(mutateSpy).toHaveBeenCalledTimes(1);
expect(mutateSpy).toHaveBeenCalledWith('createSnippet', {
input,
});
},
);
});
2020-06-23 00:09:42 +05:30
2022-07-16 23:28:13 +05:30
describe('when updating a snippet', () => {
it.each`
projectPath | uploadedFiles | input
${''} | ${[]} | ${getApiData(createSnippet())}
${'project/path'} | ${[]} | ${getApiData(createSnippet())}
`(
'should submit an updateSnippet mutation (projectPath=$projectPath, uploadedFiles=$uploadedFiles)',
async ({ projectPath, uploadedFiles, input }) => {
await createComponentAndLoad({
props: {
snippetGid: TEST_SNIPPET_GID,
projectPath,
},
});
setUploadFilesHtml(uploadedFiles);
await nextTick();
clickSubmitBtn();
expect(mutateSpy).toHaveBeenCalledTimes(1);
expect(mutateSpy).toHaveBeenCalledWith('updateSnippet', {
input,
});
},
);
});
2020-06-23 00:09:42 +05:30
2020-10-24 23:57:45 +05:30
it('should redirect to snippet view on successful mutation', async () => {
2021-03-11 19:13:27 +05:30
await createComponentAndSubmit();
2020-10-24 23:57:45 +05:30
expect(urlUtils.redirectTo).toHaveBeenCalledWith(TEST_WEB_URL);
2020-04-22 19:07:51 +05:30
});
2020-06-23 00:09:42 +05:30
2022-07-16 23:28:13 +05:30
describe('when there are errors after creating a new snippet', () => {
it.each`
projectPath
${'project/path'}
${''}
`('should flash error (projectPath=$projectPath)', async ({ projectPath }) => {
mutateSpy.mockResolvedValue(createMutationResponseWithErrors('createSnippet'));
await createComponentAndLoad({
props: { projectPath, snippetGid: '' },
2020-06-23 00:09:42 +05:30
});
2020-10-24 23:57:45 +05:30
2022-07-16 23:28:13 +05:30
setTitle('Title');
clickSubmitBtn();
await waitForPromises();
2020-10-24 23:57:45 +05:30
expect(urlUtils.redirectTo).not.toHaveBeenCalled();
2022-11-25 23:54:43 +05:30
expect(createAlert).toHaveBeenCalledWith({
2022-07-16 23:28:13 +05:30
message: `Can't create snippet: ${TEST_MUTATION_ERROR}`,
2021-09-30 23:02:18 +05:30
});
2022-07-16 23:28:13 +05:30
});
});
describe('when there are errors after updating a snippet', () => {
it.each`
projectPath
${'project/path'}
${''}
`(
'should flash error with (snippet=$snippetGid, projectPath=$projectPath)',
async ({ projectPath }) => {
mutateSpy.mockResolvedValue(createMutationResponseWithErrors('updateSnippet'));
await createComponentAndSubmit({
props: {
projectPath,
snippetGid: TEST_SNIPPET_GID,
},
});
expect(urlUtils.redirectTo).not.toHaveBeenCalled();
2022-11-25 23:54:43 +05:30
expect(createAlert).toHaveBeenCalledWith({
2022-07-16 23:28:13 +05:30
message: `Can't update snippet: ${TEST_MUTATION_ERROR}`,
});
},
);
});
2020-06-23 00:09:42 +05:30
2021-09-30 23:02:18 +05:30
describe('with apollo network error', () => {
2021-03-11 19:13:27 +05:30
beforeEach(async () => {
jest.spyOn(console, 'error').mockImplementation();
2021-09-30 23:02:18 +05:30
mutateSpy.mockRejectedValue(TEST_API_ERROR);
2020-07-28 23:09:34 +05:30
2021-03-11 19:13:27 +05:30
await createComponentAndSubmit();
2022-04-04 11:22:00 +05:30
await nextTick();
2021-03-11 19:13:27 +05:30
});
2020-07-28 23:09:34 +05:30
2021-03-11 19:13:27 +05:30
it('should not redirect', () => {
expect(urlUtils.redirectTo).not.toHaveBeenCalled();
});
2020-07-28 23:09:34 +05:30
2021-03-11 19:13:27 +05:30
it('should flash', () => {
// Apollo automatically wraps the resolver's error in a NetworkError
2022-11-25 23:54:43 +05:30
expect(createAlert).toHaveBeenCalledWith({
2022-04-04 11:22:00 +05:30
message: `Can't update snippet: ${TEST_API_ERROR.message}`,
2021-09-30 23:02:18 +05:30
});
2021-03-11 19:13:27 +05:30
});
2020-07-28 23:09:34 +05:30
2021-03-11 19:13:27 +05:30
it('should console error', () => {
// eslint-disable-next-line no-console
expect(console.error).toHaveBeenCalledTimes(1);
// eslint-disable-next-line no-console
expect(console.error).toHaveBeenCalledWith(
'[gitlab] unexpected error while updating snippet',
2022-04-04 11:22:00 +05:30
expect.objectContaining({ message: `${TEST_API_ERROR.message}` }),
2021-03-11 19:13:27 +05:30
);
});
});
2020-07-28 23:09:34 +05:30
});
2020-04-22 19:07:51 +05:30
});
2021-03-11 19:13:27 +05:30
describe('on before unload', () => {
it.each([
['there are no actions', false, () => triggerBlobActions([])],
['there is an empty action', false, () => triggerBlobActions([testEntries.empty.diff])],
['there are actions', true, () => triggerBlobActions([testEntries.updated.diff])],
[
'the title is set',
true,
() => {
triggerBlobActions([testEntries.empty.diff]);
setTitle('test');
},
],
[
'the description is set',
true,
() => {
triggerBlobActions([testEntries.empty.diff]);
setDescription('test');
},
],
[
'the snippet is being saved',
false,
() => {
triggerBlobActions([testEntries.updated.diff]);
2022-07-16 23:28:13 +05:30
setTitle('test');
2021-03-11 19:13:27 +05:30
clickSubmitBtn();
},
],
])(
'handles before unload prevent when %s (expectPrevented=%s)',
async (_, expectPrevented, action) => {
await createComponentAndLoad({
props: {
snippetGid: '',
},
});
action();
const event = new Event('beforeunload');
const returnValueSetter = jest.spyOn(event, 'returnValue', 'set');
window.dispatchEvent(event);
if (expectPrevented) {
expect(returnValueSetter).toHaveBeenCalledWith(
'Are you sure you want to lose unsaved changes?',
);
} else {
expect(returnValueSetter).not.toHaveBeenCalled();
}
},
);
});
2020-04-22 19:07:51 +05:30
});