2020-04-21 15:21:10 +00:00
|
|
|
import { shallowMount } from '@vue/test-utils';
|
2020-06-02 06:08:01 +00:00
|
|
|
import Flash from '~/flash';
|
2020-04-21 15:21:10 +00:00
|
|
|
|
|
|
|
import { GlLoadingIcon } from '@gitlab/ui';
|
2020-07-17 09:09:43 +00:00
|
|
|
import { redirectTo } from '~/lib/utils/url_utility';
|
2020-04-21 15:21:10 +00:00
|
|
|
|
|
|
|
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';
|
2020-06-02 06:08:01 +00:00
|
|
|
import { SNIPPET_CREATE_MUTATION_ERROR, SNIPPET_UPDATE_MUTATION_ERROR } from '~/snippets/constants';
|
2020-04-21 15:21:10 +00:00
|
|
|
|
|
|
|
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'),
|
|
|
|
}));
|
|
|
|
|
2020-06-02 06:08:01 +00:00
|
|
|
jest.mock('~/flash');
|
|
|
|
|
2020-04-21 15:21:10 +00:00
|
|
|
let flashSpy;
|
|
|
|
|
|
|
|
const rawProjectPathMock = '/project/path';
|
|
|
|
const newlyEditedSnippetUrl = 'http://foo.bar';
|
|
|
|
const apiError = { message: 'Ufff' };
|
2020-06-04 09:08:01 +00:00
|
|
|
const mutationError = 'Bummer';
|
2020-04-21 15:21:10 +00:00
|
|
|
|
2020-06-15 12:08:44 +00:00
|
|
|
const attachedFilePath1 = 'foo/bar';
|
|
|
|
const attachedFilePath2 = 'alpha/beta';
|
|
|
|
|
2020-07-17 09:09:43 +00:00
|
|
|
const actionWithContent = {
|
|
|
|
content: 'Foo Bar',
|
|
|
|
};
|
|
|
|
const actionWithoutContent = {
|
|
|
|
content: '',
|
|
|
|
};
|
|
|
|
|
2020-04-21 15:21:10 +00:00
|
|
|
const defaultProps = {
|
|
|
|
snippetGid: 'gid://gitlab/PersonalSnippet/42',
|
|
|
|
markdownPreviewPath: 'http://preview.foo.bar',
|
|
|
|
markdownDocsPath: 'http://docs.foo.bar',
|
|
|
|
};
|
2020-07-17 09:09:43 +00:00
|
|
|
const defaultData = {
|
|
|
|
blobsActions: {
|
|
|
|
...actionWithContent,
|
|
|
|
action: '',
|
|
|
|
},
|
|
|
|
};
|
2020-04-21 15:21:10 +00:00
|
|
|
|
|
|
|
describe('Snippet Edit app', () => {
|
|
|
|
let wrapper;
|
|
|
|
|
|
|
|
const resolveMutate = jest.fn().mockResolvedValue({
|
|
|
|
data: {
|
|
|
|
updateSnippet: {
|
|
|
|
errors: [],
|
|
|
|
snippet: {
|
|
|
|
webUrl: newlyEditedSnippetUrl,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
});
|
|
|
|
|
2020-06-04 09:08:01 +00:00
|
|
|
const resolveMutateWithErrors = jest.fn().mockResolvedValue({
|
|
|
|
data: {
|
|
|
|
updateSnippet: {
|
|
|
|
errors: [mutationError],
|
|
|
|
snippet: {
|
|
|
|
webUrl: newlyEditedSnippetUrl,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
createSnippet: {
|
|
|
|
errors: [mutationError],
|
|
|
|
snippet: null,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
});
|
|
|
|
|
2020-04-21 15:21:10 +00:00
|
|
|
const rejectMutation = jest.fn().mockRejectedValue(apiError);
|
|
|
|
|
|
|
|
const mutationTypes = {
|
|
|
|
RESOLVE: resolveMutate,
|
2020-06-04 09:08:01 +00:00
|
|
|
RESOLVE_WITH_ERRORS: resolveMutateWithErrors,
|
2020-04-21 15:21:10 +00:00
|
|
|
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();
|
|
|
|
});
|
|
|
|
|
2020-06-15 12:08:44 +00:00
|
|
|
const findSubmitButton = () => wrapper.find('[data-testid="snippet-submit-btn"]');
|
2020-04-21 15:21:10 +00:00
|
|
|
const findCancellButton = () => wrapper.find('[data-testid="snippet-cancel-btn"]');
|
2020-06-15 12:08:44 +00:00
|
|
|
const clickSubmitBtn = () => wrapper.find('[data-testid="snippet-edit-form"]').trigger('submit');
|
2020-04-21 15:21:10 +00:00
|
|
|
|
|
|
|
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`
|
2020-07-17 09:09:43 +00:00
|
|
|
title | blobsActions | expectation
|
|
|
|
${''} | ${{}} | ${true}
|
|
|
|
${''} | ${{ actionWithContent }} | ${true}
|
|
|
|
${''} | ${{ actionWithoutContent }} | ${true}
|
|
|
|
${'foo'} | ${{}} | ${true}
|
|
|
|
${'foo'} | ${{ actionWithoutContent }} | ${true}
|
|
|
|
${'foo'} | ${{ actionWithoutContent, actionWithContent }} | ${true}
|
|
|
|
${'foo'} | ${{ actionWithContent }} | ${false}
|
2020-04-21 15:21:10 +00:00
|
|
|
`(
|
2020-07-17 09:09:43 +00:00
|
|
|
'disables submit button unless both title and content for all blobs are present',
|
|
|
|
({ title, blobsActions, expectation }) => {
|
2020-04-21 15:21:10 +00:00
|
|
|
createComponent({
|
|
|
|
data: {
|
|
|
|
snippet: { title },
|
2020-07-17 09:09:43 +00:00
|
|
|
blobsActions,
|
2020-04-21 15:21:10 +00:00
|
|
|
},
|
|
|
|
});
|
|
|
|
const isBtnDisabled = Boolean(findSubmitButton().attributes('disabled'));
|
|
|
|
expect(isBtnDisabled).toBe(expectation);
|
|
|
|
},
|
|
|
|
);
|
|
|
|
|
|
|
|
it.each`
|
|
|
|
isNew | status | expectation
|
2020-07-23 15:09:28 +00:00
|
|
|
${true} | ${`new`} | ${`/-/snippets`}
|
2020-04-21 15:21:10 +00:00
|
|
|
${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', () => {
|
2020-07-17 09:09:43 +00:00
|
|
|
describe('form submission handling', () => {
|
|
|
|
it('does not submit unchanged blobs', () => {
|
|
|
|
const foo = {
|
|
|
|
action: '',
|
|
|
|
};
|
|
|
|
const bar = {
|
|
|
|
action: 'update',
|
2020-04-21 15:21:10 +00:00
|
|
|
};
|
|
|
|
createComponent({
|
|
|
|
data: {
|
2020-07-17 09:09:43 +00:00
|
|
|
blobsActions: {
|
|
|
|
foo,
|
|
|
|
bar,
|
|
|
|
},
|
2020-04-21 15:21:10 +00:00
|
|
|
},
|
|
|
|
});
|
2020-07-17 09:09:43 +00:00
|
|
|
clickSubmitBtn();
|
2020-04-21 15:21:10 +00:00
|
|
|
|
|
|
|
return waitForPromises().then(() => {
|
2020-07-17 09:09:43 +00:00
|
|
|
expect(resolveMutate).toHaveBeenCalledWith(
|
2020-07-17 18:09:20 +00:00
|
|
|
expect.objectContaining({ variables: { input: { blobActions: [bar] } } }),
|
2020-07-17 09:09:43 +00:00
|
|
|
);
|
2020-04-21 15:21:10 +00:00
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
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,
|
2020-07-17 09:09:43 +00:00
|
|
|
...defaultData,
|
2020-04-21 15:21:10 +00:00
|
|
|
},
|
|
|
|
props: {
|
|
|
|
...defaultProps,
|
|
|
|
projectPath,
|
|
|
|
},
|
|
|
|
});
|
|
|
|
|
|
|
|
const mutationPayload = {
|
|
|
|
mutation,
|
|
|
|
variables: {
|
|
|
|
input: newSnippet ? expect.objectContaining({ projectPath }) : expect.any(Object),
|
|
|
|
},
|
|
|
|
};
|
|
|
|
|
2020-06-15 12:08:44 +00:00
|
|
|
clickSubmitBtn();
|
|
|
|
|
2020-04-21 15:21:10 +00:00
|
|
|
expect(resolveMutate).toHaveBeenCalledWith(mutationPayload);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('redirects to snippet view on successful mutation', () => {
|
|
|
|
createComponent();
|
2020-06-15 12:08:44 +00:00
|
|
|
clickSubmitBtn();
|
|
|
|
|
2020-04-21 15:21:10 +00:00
|
|
|
return waitForPromises().then(() => {
|
|
|
|
expect(redirectTo).toHaveBeenCalledWith(newlyEditedSnippetUrl);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2020-06-04 09:08:01 +00:00
|
|
|
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,
|
|
|
|
});
|
|
|
|
|
2020-06-15 12:08:44 +00:00
|
|
|
clickSubmitBtn();
|
|
|
|
|
2020-06-04 09:08:01 +00:00
|
|
|
return waitForPromises().then(() => {
|
|
|
|
expect(redirectTo).not.toHaveBeenCalled();
|
|
|
|
expect(flashSpy).toHaveBeenCalledWith(mutationError);
|
|
|
|
});
|
|
|
|
},
|
|
|
|
);
|
|
|
|
|
2020-04-21 15:21:10 +00:00
|
|
|
it('flashes an error if mutation failed', () => {
|
|
|
|
createComponent({
|
|
|
|
mutationRes: mutationTypes.REJECT,
|
|
|
|
});
|
2020-06-15 12:08:44 +00:00
|
|
|
|
|
|
|
clickSubmitBtn();
|
|
|
|
|
2020-04-21 15:21:10 +00:00
|
|
|
return waitForPromises().then(() => {
|
|
|
|
expect(redirectTo).not.toHaveBeenCalled();
|
|
|
|
expect(flashSpy).toHaveBeenCalledWith(apiError);
|
|
|
|
});
|
|
|
|
});
|
2020-06-02 06:08:01 +00:00
|
|
|
|
|
|
|
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,
|
|
|
|
});
|
2020-06-15 12:08:44 +00:00
|
|
|
|
|
|
|
clickSubmitBtn();
|
|
|
|
|
2020-06-02 06:08:01 +00:00
|
|
|
return waitForPromises().then(() => {
|
|
|
|
expect(Flash).toHaveBeenCalledWith(expect.stringContaining(expectation));
|
|
|
|
});
|
|
|
|
},
|
|
|
|
);
|
2020-04-21 15:21:10 +00:00
|
|
|
});
|
2020-06-15 12:08:44 +00:00
|
|
|
|
|
|
|
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());
|
|
|
|
});
|
|
|
|
});
|
2020-06-17 15:08:36 +00:00
|
|
|
|
|
|
|
describe('on before unload', () => {
|
|
|
|
let event;
|
|
|
|
let returnValueSetter;
|
|
|
|
|
2020-07-17 09:09:43 +00:00
|
|
|
const bootstrap = data => {
|
|
|
|
createComponent({
|
|
|
|
data,
|
|
|
|
});
|
2020-06-17 15:08:36 +00:00
|
|
|
|
|
|
|
event = new Event('beforeunload');
|
|
|
|
returnValueSetter = jest.spyOn(event, 'returnValue', 'set');
|
2020-07-17 09:09:43 +00:00
|
|
|
};
|
|
|
|
|
2020-07-20 21:09:23 +00:00
|
|
|
const actionsWithoutAction = {
|
|
|
|
blobsActions: {
|
|
|
|
foo: {
|
|
|
|
...actionWithContent,
|
|
|
|
action: '',
|
2020-07-17 09:09:43 +00:00
|
|
|
},
|
2020-07-20 21:09:23 +00:00
|
|
|
},
|
|
|
|
};
|
|
|
|
const actionsWithUpdate = {
|
|
|
|
blobsActions: {
|
|
|
|
foo: {
|
|
|
|
...actionWithContent,
|
|
|
|
action: 'update',
|
2020-07-17 09:09:43 +00:00
|
|
|
},
|
2020-07-20 21:09:23 +00:00
|
|
|
},
|
|
|
|
};
|
|
|
|
const actionsWithUpdateWhileSaving = {
|
|
|
|
blobsActions: {
|
|
|
|
foo: {
|
|
|
|
...actionWithContent,
|
|
|
|
action: 'update',
|
|
|
|
},
|
|
|
|
},
|
|
|
|
isUpdating: true,
|
|
|
|
};
|
2020-06-17 15:08:36 +00:00
|
|
|
|
2020-07-20 21:09:23 +00:00
|
|
|
it.each`
|
|
|
|
bool | expectToBePrevented | data | condition
|
|
|
|
${'does not prevent'} | ${false} | ${undefined} | ${'there are no blobs'}
|
|
|
|
${'does not prevent'} | ${false} | ${actionsWithoutAction} | ${'there are no changes to the blobs content'}
|
|
|
|
${'prevents'} | ${true} | ${actionsWithUpdate} | ${'there are changes to the blobs content'}
|
|
|
|
${'does not prevent'} | ${false} | ${actionsWithUpdateWhileSaving} | ${'the snippet is being saved'}
|
|
|
|
`('$bool page navigation if $condition', ({ expectToBePrevented, data }) => {
|
|
|
|
bootstrap(data);
|
2020-06-17 15:08:36 +00:00
|
|
|
window.dispatchEvent(event);
|
|
|
|
|
2020-07-20 21:09:23 +00:00
|
|
|
if (expectToBePrevented) {
|
|
|
|
expect(returnValueSetter).toHaveBeenCalledWith(
|
|
|
|
'Are you sure you want to lose unsaved changes?',
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
expect(returnValueSetter).not.toHaveBeenCalled();
|
|
|
|
}
|
2020-06-17 15:08:36 +00:00
|
|
|
});
|
|
|
|
});
|
2020-04-21 15:21:10 +00:00
|
|
|
});
|
|
|
|
});
|