debian-mirror-gitlab/spec/frontend/snippets/components/edit_spec.js
2020-08-09 17:41:57 +05:30

430 lines
13 KiB
JavaScript

import { shallowMount } from '@vue/test-utils';
import Flash from '~/flash';
import { GlLoadingIcon } from '@gitlab/ui';
import { redirectTo } from '~/lib/utils/url_utility';
import SnippetEditApp from '~/snippets/components/edit.vue';
import SnippetDescriptionEdit from '~/snippets/components/snippet_description_edit.vue';
import SnippetVisibilityEdit from '~/snippets/components/snippet_visibility_edit.vue';
import SnippetBlobEdit from '~/snippets/components/snippet_blob_edit.vue';
import TitleField from '~/vue_shared/components/form/title.vue';
import FormFooterActions from '~/vue_shared/components/form/form_footer_actions.vue';
import { SNIPPET_CREATE_MUTATION_ERROR, SNIPPET_UPDATE_MUTATION_ERROR } from '~/snippets/constants';
import UpdateSnippetMutation from '~/snippets/mutations/updateSnippet.mutation.graphql';
import CreateSnippetMutation from '~/snippets/mutations/createSnippet.mutation.graphql';
import waitForPromises from 'helpers/wait_for_promises';
import { ApolloMutation } from 'vue-apollo';
jest.mock('~/lib/utils/url_utility', () => ({
redirectTo: jest.fn().mockName('redirectTo'),
}));
jest.mock('~/flash');
let flashSpy;
const rawProjectPathMock = '/project/path';
const newlyEditedSnippetUrl = 'http://foo.bar';
const apiError = { message: 'Ufff' };
const mutationError = 'Bummer';
const attachedFilePath1 = 'foo/bar';
const attachedFilePath2 = 'alpha/beta';
const actionWithContent = {
content: 'Foo Bar',
};
const actionWithoutContent = {
content: '',
};
const defaultProps = {
snippetGid: 'gid://gitlab/PersonalSnippet/42',
markdownPreviewPath: 'http://preview.foo.bar',
markdownDocsPath: 'http://docs.foo.bar',
};
const defaultData = {
blobsActions: {
...actionWithContent,
action: '',
},
};
describe('Snippet Edit app', () => {
let wrapper;
const resolveMutate = jest.fn().mockResolvedValue({
data: {
updateSnippet: {
errors: [],
snippet: {
webUrl: newlyEditedSnippetUrl,
},
},
},
});
const resolveMutateWithErrors = jest.fn().mockResolvedValue({
data: {
updateSnippet: {
errors: [mutationError],
snippet: {
webUrl: newlyEditedSnippetUrl,
},
},
createSnippet: {
errors: [mutationError],
snippet: null,
},
},
});
const rejectMutation = jest.fn().mockRejectedValue(apiError);
const mutationTypes = {
RESOLVE: resolveMutate,
RESOLVE_WITH_ERRORS: resolveMutateWithErrors,
REJECT: rejectMutation,
};
function createComponent({
props = defaultProps,
data = {},
loading = false,
mutationRes = mutationTypes.RESOLVE,
} = {}) {
const $apollo = {
queries: {
snippet: {
loading,
},
},
mutate: mutationRes,
};
wrapper = shallowMount(SnippetEditApp, {
mocks: { $apollo },
stubs: {
FormFooterActions,
ApolloMutation,
},
propsData: {
...props,
},
data() {
return data;
},
});
flashSpy = jest.spyOn(wrapper.vm, 'flashAPIFailure');
}
afterEach(() => {
wrapper.destroy();
});
const findSubmitButton = () => wrapper.find('[data-testid="snippet-submit-btn"]');
const findCancellButton = () => wrapper.find('[data-testid="snippet-cancel-btn"]');
const clickSubmitBtn = () => wrapper.find('[data-testid="snippet-edit-form"]').trigger('submit');
describe('rendering', () => {
it('renders loader while the query is in flight', () => {
createComponent({ loading: true });
expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
});
it('renders all required components', () => {
createComponent();
expect(wrapper.contains(TitleField)).toBe(true);
expect(wrapper.contains(SnippetDescriptionEdit)).toBe(true);
expect(wrapper.contains(SnippetBlobEdit)).toBe(true);
expect(wrapper.contains(SnippetVisibilityEdit)).toBe(true);
expect(wrapper.contains(FormFooterActions)).toBe(true);
});
it('does not fail if there is no snippet yet (new snippet creation)', () => {
const snippetGid = '';
createComponent({
props: {
...defaultProps,
snippetGid,
},
});
expect(wrapper.props('snippetGid')).toBe(snippetGid);
});
it.each`
title | blobsActions | expectation
${''} | ${{}} | ${true}
${''} | ${{ actionWithContent }} | ${true}
${''} | ${{ actionWithoutContent }} | ${true}
${'foo'} | ${{}} | ${true}
${'foo'} | ${{ actionWithoutContent }} | ${true}
${'foo'} | ${{ actionWithoutContent, actionWithContent }} | ${true}
${'foo'} | ${{ actionWithContent }} | ${false}
`(
'disables submit button unless both title and content for all blobs are present',
({ title, blobsActions, expectation }) => {
createComponent({
data: {
snippet: { title },
blobsActions,
},
});
const isBtnDisabled = Boolean(findSubmitButton().attributes('disabled'));
expect(isBtnDisabled).toBe(expectation);
},
);
it.each`
isNew | status | expectation
${true} | ${`new`} | ${`/snippets`}
${false} | ${`existing`} | ${newlyEditedSnippetUrl}
`('sets correct href for the cancel button on a $status snippet', ({ isNew, expectation }) => {
createComponent({
data: {
snippet: { webUrl: newlyEditedSnippetUrl },
newSnippet: isNew,
},
});
expect(findCancellButton().attributes('href')).toBe(expectation);
});
});
describe('functionality', () => {
describe('form submission handling', () => {
it('does not submit unchanged blobs', () => {
const foo = {
action: '',
};
const bar = {
action: 'update',
};
createComponent({
data: {
blobsActions: {
foo,
bar,
},
},
});
clickSubmitBtn();
return waitForPromises().then(() => {
expect(resolveMutate).toHaveBeenCalledWith(
expect.objectContaining({ variables: { input: { files: [bar] } } }),
);
});
});
it.each`
newSnippet | projectPath | mutation | mutationName
${true} | ${rawProjectPathMock} | ${CreateSnippetMutation} | ${'CreateSnippetMutation with projectPath'}
${true} | ${''} | ${CreateSnippetMutation} | ${'CreateSnippetMutation without projectPath'}
${false} | ${rawProjectPathMock} | ${UpdateSnippetMutation} | ${'UpdateSnippetMutation with projectPath'}
${false} | ${''} | ${UpdateSnippetMutation} | ${'UpdateSnippetMutation without projectPath'}
`('should submit $mutationName correctly', ({ newSnippet, projectPath, mutation }) => {
createComponent({
data: {
newSnippet,
...defaultData,
},
props: {
...defaultProps,
projectPath,
},
});
const mutationPayload = {
mutation,
variables: {
input: newSnippet ? expect.objectContaining({ projectPath }) : expect.any(Object),
},
};
clickSubmitBtn();
expect(resolveMutate).toHaveBeenCalledWith(mutationPayload);
});
it('redirects to snippet view on successful mutation', () => {
createComponent();
clickSubmitBtn();
return waitForPromises().then(() => {
expect(redirectTo).toHaveBeenCalledWith(newlyEditedSnippetUrl);
});
});
it.each`
newSnippet | projectPath | mutationName
${true} | ${rawProjectPathMock} | ${'CreateSnippetMutation with projectPath'}
${true} | ${''} | ${'CreateSnippetMutation without projectPath'}
${false} | ${rawProjectPathMock} | ${'UpdateSnippetMutation with projectPath'}
${false} | ${''} | ${'UpdateSnippetMutation without projectPath'}
`(
'does not redirect to snippet view if the seemingly successful' +
' $mutationName response contains errors',
({ newSnippet, projectPath }) => {
createComponent({
data: {
newSnippet,
},
props: {
...defaultProps,
projectPath,
},
mutationRes: mutationTypes.RESOLVE_WITH_ERRORS,
});
clickSubmitBtn();
return waitForPromises().then(() => {
expect(redirectTo).not.toHaveBeenCalled();
expect(flashSpy).toHaveBeenCalledWith(mutationError);
});
},
);
it('flashes an error if mutation failed', () => {
createComponent({
mutationRes: mutationTypes.REJECT,
});
clickSubmitBtn();
return waitForPromises().then(() => {
expect(redirectTo).not.toHaveBeenCalled();
expect(flashSpy).toHaveBeenCalledWith(apiError);
});
});
it.each`
isNew | status | expectation
${true} | ${`new`} | ${SNIPPET_CREATE_MUTATION_ERROR.replace('%{err}', '')}
${false} | ${`existing`} | ${SNIPPET_UPDATE_MUTATION_ERROR.replace('%{err}', '')}
`(
`renders the correct error message if mutation fails for $status snippet`,
({ isNew, expectation }) => {
createComponent({
data: {
newSnippet: isNew,
},
mutationRes: mutationTypes.REJECT,
});
clickSubmitBtn();
return waitForPromises().then(() => {
expect(Flash).toHaveBeenCalledWith(expect.stringContaining(expectation));
});
},
);
});
describe('correctly includes attached files into the mutation', () => {
const createMutationPayload = expectation => {
return expect.objectContaining({
variables: {
input: expect.objectContaining({ uploadedFiles: expectation }),
},
});
};
const updateMutationPayload = () => {
return expect.objectContaining({
variables: {
input: expect.not.objectContaining({ uploadedFiles: expect.anything() }),
},
});
};
it.each`
paths | expectation
${[attachedFilePath1]} | ${[attachedFilePath1]}
${[attachedFilePath1, attachedFilePath2]} | ${[attachedFilePath1, attachedFilePath2]}
${[]} | ${[]}
`(`correctly sends paths for $paths.length files`, ({ paths, expectation }) => {
createComponent({
data: {
newSnippet: true,
},
});
const fixtures = paths.map(path => {
return path ? `<input name="files[]" value="${path}">` : undefined;
});
wrapper.vm.$el.innerHTML += fixtures.join('');
clickSubmitBtn();
expect(resolveMutate).toHaveBeenCalledWith(createMutationPayload(expectation));
});
it(`neither fails nor sends 'uploadedFiles' to update mutation`, () => {
createComponent();
clickSubmitBtn();
expect(resolveMutate).toHaveBeenCalledWith(updateMutationPayload());
});
});
describe('on before unload', () => {
let event;
let returnValueSetter;
const bootstrap = data => {
createComponent({
data,
});
event = new Event('beforeunload');
returnValueSetter = jest.spyOn(event, 'returnValue', 'set');
};
it('does not prevent page navigation if there are no blobs', () => {
bootstrap();
window.dispatchEvent(event);
expect(returnValueSetter).not.toHaveBeenCalled();
});
it('does not prevent page navigation if there are no changes to the blobs content', () => {
bootstrap({
blobsActions: {
foo: {
...actionWithContent,
action: '',
},
},
});
window.dispatchEvent(event);
expect(returnValueSetter).not.toHaveBeenCalled();
});
it('prevents page navigation if there are some changes in the snippet content', () => {
bootstrap({
blobsActions: {
foo: {
...actionWithContent,
action: 'update',
},
},
});
window.dispatchEvent(event);
expect(returnValueSetter).toHaveBeenCalledWith(
'Are you sure you want to lose unsaved changes?',
);
});
});
});
});