gitlab-org--gitlab-foss/spec/frontend/snippets/components/edit_spec.js

371 lines
13 KiB
JavaScript

import VueApollo, { ApolloMutation } from 'vue-apollo';
import { GlLoadingIcon } from '@gitlab/ui';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import waitForPromises from 'helpers/wait_for_promises';
import createMockApollo from 'jest/helpers/mock_apollo_helper';
import GetSnippetQuery from 'shared_queries/snippet/snippet.query.graphql';
import { deprecatedCreateFlash as Flash } from '~/flash';
import * as urlUtils 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 SnippetBlobActionsEdit from '~/snippets/components/snippet_blob_actions_edit.vue';
import TitleField from '~/vue_shared/components/form/title.vue';
import FormFooterActions from '~/vue_shared/components/form/form_footer_actions.vue';
import {
SNIPPET_VISIBILITY_PRIVATE,
SNIPPET_VISIBILITY_INTERNAL,
SNIPPET_VISIBILITY_PUBLIC,
} from '~/snippets/constants';
import UpdateSnippetMutation from '~/snippets/mutations/updateSnippet.mutation.graphql';
import CreateSnippetMutation from '~/snippets/mutations/createSnippet.mutation.graphql';
import { testEntries } from '../test_utils';
jest.mock('~/flash');
const TEST_UPLOADED_FILES = ['foo/bar.txt', 'alpha/beta.js'];
const TEST_API_ERROR = 'Ufff';
const TEST_MUTATION_ERROR = 'Bummer';
const TEST_ACTIONS = {
NO_CONTENT: {
...testEntries.created.diff,
content: '',
},
NO_PATH: {
...testEntries.created.diff,
filePath: '',
},
VALID: {
...testEntries.created.diff,
},
};
const TEST_WEB_URL = '/snippets/7';
const createTestSnippet = () => ({
webUrl: TEST_WEB_URL,
id: 7,
title: 'Snippet Title',
description: 'Lorem ipsum snippet desc',
visibilityLevel: SNIPPET_VISIBILITY_PRIVATE,
});
describe('Snippet Edit app', () => {
let wrapper;
let fakeApollo;
const relativeUrlRoot = '/foo/';
const originalRelativeUrlRoot = gon.relative_url_root;
const GetSnippetQuerySpy = jest.fn().mockResolvedValue({
data: { snippets: { nodes: [createTestSnippet()] } },
});
const mutationTypes = {
RESOLVE: jest.fn().mockResolvedValue({
data: {
updateSnippet: {
errors: [],
snippet: createTestSnippet(),
},
},
}),
RESOLVE_WITH_ERRORS: jest.fn().mockResolvedValue({
data: {
updateSnippet: {
errors: [TEST_MUTATION_ERROR],
snippet: createTestSnippet(),
},
createSnippet: {
errors: [TEST_MUTATION_ERROR],
snippet: null,
},
},
}),
REJECT: jest.fn().mockRejectedValue(TEST_API_ERROR),
};
function createComponent({
props = {},
loading = false,
mutationRes = mutationTypes.RESOLVE,
selectedLevel = SNIPPET_VISIBILITY_PRIVATE,
withApollo = false,
} = {}) {
let componentData = {
mocks: {
$apollo: {
queries: {
snippet: { loading },
},
mutate: mutationRes,
},
},
};
if (withApollo) {
const localVue = createLocalVue();
localVue.use(VueApollo);
const requestHandlers = [[GetSnippetQuery, GetSnippetQuerySpy]];
fakeApollo = createMockApollo(requestHandlers);
componentData = {
localVue,
apolloProvider: fakeApollo,
};
}
wrapper = shallowMount(SnippetEditApp, {
...componentData,
stubs: {
ApolloMutation,
FormFooterActions,
},
provide: {
selectedLevel,
},
propsData: {
snippetGid: 'gid://gitlab/PersonalSnippet/42',
markdownPreviewPath: 'http://preview.foo.bar',
markdownDocsPath: 'http://docs.foo.bar',
...props,
},
});
}
beforeEach(() => {
gon.relative_url_root = relativeUrlRoot;
jest.spyOn(urlUtils, 'redirectTo').mockImplementation();
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
gon.relative_url_root = originalRelativeUrlRoot;
});
const findBlobActions = () => wrapper.find(SnippetBlobActionsEdit);
const findSubmitButton = () => wrapper.find('[data-testid="snippet-submit-btn"]');
const findCancelButton = () => wrapper.find('[data-testid="snippet-cancel-btn"]');
const hasDisabledSubmit = () => Boolean(findSubmitButton().attributes('disabled'));
const clickSubmitBtn = () => wrapper.find('[data-testid="snippet-edit-form"]').trigger('submit');
const triggerBlobActions = actions => findBlobActions().vm.$emit('actions', actions);
const setUploadFilesHtml = paths => {
wrapper.vm.$el.innerHTML = paths.map(path => `<input name="files[]" value="${path}">`).join('');
};
const getApiData = ({
id,
title = '',
description = '',
visibilityLevel = SNIPPET_VISIBILITY_PRIVATE,
} = {}) => ({
id,
title,
description,
visibilityLevel,
blobActions: [],
});
// Ideally we wouldn't call this method directly, but we don't have a way to trigger
// apollo responses yet.
const loadSnippet = (...nodes) => {
if (nodes.length) {
wrapper.setData({
snippet: nodes[0],
newSnippet: false,
});
} else {
wrapper.setData({
newSnippet: true,
});
}
};
describe('rendering', () => {
it('renders loader while the query is in flight', () => {
createComponent({ loading: true });
expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
});
it.each([[{}], [{ snippetGid: '' }]])(
'should render all required components with %s',
props => {
createComponent(props);
expect(wrapper.find(TitleField).exists()).toBe(true);
expect(wrapper.find(SnippetDescriptionEdit).exists()).toBe(true);
expect(wrapper.find(SnippetVisibilityEdit).exists()).toBe(true);
expect(wrapper.find(FormFooterActions).exists()).toBe(true);
expect(findBlobActions().exists()).toBe(true);
},
);
it.each`
title | actions | shouldDisable
${''} | ${[]} | ${true}
${''} | ${[TEST_ACTIONS.VALID]} | ${true}
${'foo'} | ${[]} | ${false}
${'foo'} | ${[TEST_ACTIONS.VALID]} | ${false}
${'foo'} | ${[TEST_ACTIONS.VALID, TEST_ACTIONS.NO_CONTENT]} | ${true}
${'foo'} | ${[TEST_ACTIONS.VALID, TEST_ACTIONS.NO_PATH]} | ${false}
`(
'should handle submit disable (title=$title, actions=$actions, shouldDisable=$shouldDisable)',
async ({ title, actions, shouldDisable }) => {
createComponent();
loadSnippet({ title });
triggerBlobActions(actions);
await wrapper.vm.$nextTick();
expect(hasDisabledSubmit()).toBe(shouldDisable);
},
);
it.each`
projectPath | snippetArg | expectation
${''} | ${[]} | ${urlUtils.joinPaths('/', relativeUrlRoot, '-', 'snippets')}
${'project/path'} | ${[]} | ${urlUtils.joinPaths('/', relativeUrlRoot, 'project/path/-', 'snippets')}
${''} | ${[createTestSnippet()]} | ${TEST_WEB_URL}
${'project/path'} | ${[createTestSnippet()]} | ${TEST_WEB_URL}
`(
'should set cancel href when (projectPath=$projectPath, snippet=$snippetArg)',
async ({ projectPath, snippetArg, expectation }) => {
createComponent({
props: { projectPath },
});
loadSnippet(...snippetArg);
await wrapper.vm.$nextTick();
expect(findCancelButton().attributes('href')).toBe(expectation);
},
);
});
describe('functionality', () => {
it('does not fetch snippet when create a new snippet', async () => {
createComponent({ props: { snippetGid: '' }, withApollo: true });
jest.runOnlyPendingTimers();
await wrapper.vm.$nextTick();
expect(GetSnippetQuerySpy).not.toHaveBeenCalled();
});
describe('default visibility', () => {
it.each([SNIPPET_VISIBILITY_PRIVATE, SNIPPET_VISIBILITY_INTERNAL, SNIPPET_VISIBILITY_PUBLIC])(
'marks %s visibility by default',
async visibility => {
createComponent({
props: { snippetGid: '' },
selectedLevel: visibility,
});
expect(wrapper.vm.snippet.visibilityLevel).toEqual(visibility);
},
);
});
describe('form submission handling', () => {
it.each`
snippetArg | projectPath | uploadedFiles | input | mutation
${[]} | ${'project/path'} | ${[]} | ${{ ...getApiData(), projectPath: 'project/path', uploadedFiles: [] }} | ${CreateSnippetMutation}
${[]} | ${''} | ${[]} | ${{ ...getApiData(), projectPath: '', uploadedFiles: [] }} | ${CreateSnippetMutation}
${[]} | ${''} | ${TEST_UPLOADED_FILES} | ${{ ...getApiData(), projectPath: '', uploadedFiles: TEST_UPLOADED_FILES }} | ${CreateSnippetMutation}
${[createTestSnippet()]} | ${'project/path'} | ${[]} | ${getApiData(createTestSnippet())} | ${UpdateSnippetMutation}
${[createTestSnippet()]} | ${''} | ${[]} | ${getApiData(createTestSnippet())} | ${UpdateSnippetMutation}
`(
'should submit mutation with (snippet=$snippetArg, projectPath=$projectPath, uploadedFiles=$uploadedFiles)',
async ({ snippetArg, projectPath, uploadedFiles, mutation, input }) => {
createComponent({
props: {
projectPath,
},
});
loadSnippet(...snippetArg);
setUploadFilesHtml(uploadedFiles);
await wrapper.vm.$nextTick();
clickSubmitBtn();
expect(mutationTypes.RESOLVE).toHaveBeenCalledWith({
mutation,
variables: {
input,
},
});
},
);
it('should redirect to snippet view on successful mutation', async () => {
createComponent();
loadSnippet(createTestSnippet());
clickSubmitBtn();
await waitForPromises();
expect(urlUtils.redirectTo).toHaveBeenCalledWith(TEST_WEB_URL);
});
it.each`
snippetArg | projectPath | mutationRes | expectMessage
${[]} | ${'project/path'} | ${mutationTypes.RESOLVE_WITH_ERRORS} | ${`Can't create snippet: ${TEST_MUTATION_ERROR}`}
${[]} | ${''} | ${mutationTypes.RESOLVE_WITH_ERRORS} | ${`Can't create snippet: ${TEST_MUTATION_ERROR}`}
${[]} | ${''} | ${mutationTypes.REJECT} | ${`Can't create snippet: ${TEST_API_ERROR}`}
${[createTestSnippet()]} | ${'project/path'} | ${mutationTypes.RESOLVE_WITH_ERRORS} | ${`Can't update snippet: ${TEST_MUTATION_ERROR}`}
${[createTestSnippet()]} | ${''} | ${mutationTypes.RESOLVE_WITH_ERRORS} | ${`Can't update snippet: ${TEST_MUTATION_ERROR}`}
`(
'should flash error with (snippet=$snippetArg, projectPath=$projectPath)',
async ({ snippetArg, projectPath, mutationRes, expectMessage }) => {
createComponent({
props: {
projectPath,
},
mutationRes,
});
loadSnippet(...snippetArg);
clickSubmitBtn();
await waitForPromises();
expect(urlUtils.redirectTo).not.toHaveBeenCalled();
expect(Flash).toHaveBeenCalledWith(expectMessage);
},
);
});
describe('on before unload', () => {
it.each`
condition | expectPrevented | action
${'there are no actions'} | ${false} | ${() => triggerBlobActions([])}
${'there are actions'} | ${true} | ${() => triggerBlobActions([testEntries.updated.diff])}
${'the snippet is being saved'} | ${false} | ${() => clickSubmitBtn()}
`(
'handles before unload prevent when $condition (expectPrevented=$expectPrevented)',
({ expectPrevented, action }) => {
createComponent();
loadSnippet();
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();
}
},
);
});
});
});