diff options
Diffstat (limited to 'spec/frontend/snippets/components/edit_spec.js')
-rw-r--r-- | spec/frontend/snippets/components/edit_spec.js | 279 |
1 files changed, 279 insertions, 0 deletions
diff --git a/spec/frontend/snippets/components/edit_spec.js b/spec/frontend/snippets/components/edit_spec.js new file mode 100644 index 00000000000..21a4ccf5a74 --- /dev/null +++ b/spec/frontend/snippets/components/edit_spec.js @@ -0,0 +1,279 @@ +import { shallowMount } from '@vue/test-utils'; +import axios from '~/lib/utils/axios_utils'; + +import { GlLoadingIcon } from '@gitlab/ui'; +import { joinPaths, 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 UpdateSnippetMutation from '~/snippets/mutations/updateSnippet.mutation.graphql'; +import CreateSnippetMutation from '~/snippets/mutations/createSnippet.mutation.graphql'; + +import AxiosMockAdapter from 'axios-mock-adapter'; +import waitForPromises from 'helpers/wait_for_promises'; +import { ApolloMutation } from 'vue-apollo'; + +jest.mock('~/lib/utils/url_utility', () => ({ + getBaseURL: jest.fn().mockReturnValue('foo/'), + redirectTo: jest.fn().mockName('redirectTo'), + joinPaths: jest + .fn() + .mockName('joinPaths') + .mockReturnValue('contentApiURL'), +})); + +let flashSpy; + +const contentMock = 'Foo Bar'; +const rawPathMock = '/foo/bar'; +const rawProjectPathMock = '/project/path'; +const newlyEditedSnippetUrl = 'http://foo.bar'; +const apiError = { message: 'Ufff' }; + +const defaultProps = { + snippetGid: 'gid://gitlab/PersonalSnippet/42', + markdownPreviewPath: 'http://preview.foo.bar', + markdownDocsPath: 'http://docs.foo.bar', +}; + +describe('Snippet Edit app', () => { + let wrapper; + let axiosMock; + + const resolveMutate = jest.fn().mockResolvedValue({ + data: { + updateSnippet: { + errors: [], + snippet: { + webUrl: newlyEditedSnippetUrl, + }, + }, + }, + }); + + const rejectMutation = jest.fn().mockRejectedValue(apiError); + + const mutationTypes = { + RESOLVE: resolveMutate, + 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('[type=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 | content | expectation + ${''} | ${''} | ${true} + ${'foo'} | ${''} | ${true} + ${''} | ${'foo'} | ${true} + ${'foo'} | ${'bar'} | ${false} + `( + 'disables submit button unless both title and content are present', + ({ title, content, expectation }) => { + createComponent({ + data: { + snippet: { title }, + content, + }, + }); + const isBtnDisabled = Boolean(findSubmitButton().attributes('disabled')); + expect(isBtnDisabled).toBe(expectation); + }, + ); + }); + + describe('functionality', () => { + describe('handling of the data from GraphQL response', () => { + const snippet = { + blob: { + rawPath: rawPathMock, + }, + }; + const getResSchema = newSnippet => { + return { + data: { + snippets: { + edges: newSnippet ? [] : [snippet], + }, + }, + }; + }; + + const bootstrapForExistingSnippet = resp => { + createComponent({ + data: { + snippet, + }, + }); + + if (resp === 500) { + axiosMock.onGet('contentApiURL').reply(500); + } else { + axiosMock.onGet('contentApiURL').reply(200, contentMock); + } + wrapper.vm.onSnippetFetch(getResSchema()); + }; + + const bootstrapForNewSnippet = () => { + createComponent(); + wrapper.vm.onSnippetFetch(getResSchema(true)); + }; + + beforeEach(() => { + axiosMock = new AxiosMockAdapter(axios); + }); + + afterEach(() => { + axiosMock.restore(); + }); + + it('fetches blob content with the additional query', () => { + bootstrapForExistingSnippet(); + + return waitForPromises().then(() => { + expect(joinPaths).toHaveBeenCalledWith('foo/', rawPathMock); + expect(wrapper.vm.newSnippet).toBe(false); + expect(wrapper.vm.content).toBe(contentMock); + }); + }); + + it('flashes the error message if fetching content fails', () => { + bootstrapForExistingSnippet(500); + + return waitForPromises().then(() => { + expect(flashSpy).toHaveBeenCalled(); + expect(wrapper.vm.content).toBe(''); + }); + }); + + it('does not fetch content for new snippet', () => { + bootstrapForNewSnippet(); + + return waitForPromises().then(() => { + // we keep using waitForPromises to make sure we do not run failed test + expect(wrapper.vm.newSnippet).toBe(true); + expect(wrapper.vm.content).toBe(''); + expect(joinPaths).not.toHaveBeenCalled(); + expect(wrapper.vm.snippet).toEqual(wrapper.vm.$options.newSnippetSchema); + }); + }); + }); + + describe('form submission handling', () => { + 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, + }, + props: { + ...defaultProps, + projectPath, + }, + }); + + const mutationPayload = { + mutation, + variables: { + input: newSnippet ? expect.objectContaining({ projectPath }) : expect.any(Object), + }, + }; + + wrapper.vm.handleFormSubmit(); + expect(resolveMutate).toHaveBeenCalledWith(mutationPayload); + }); + + it('redirects to snippet view on successful mutation', () => { + createComponent(); + wrapper.vm.handleFormSubmit(); + return waitForPromises().then(() => { + expect(redirectTo).toHaveBeenCalledWith(newlyEditedSnippetUrl); + }); + }); + + it('flashes an error if mutation failed', () => { + createComponent({ + mutationRes: mutationTypes.REJECT, + }); + wrapper.vm.handleFormSubmit(); + return waitForPromises().then(() => { + expect(redirectTo).not.toHaveBeenCalled(); + expect(flashSpy).toHaveBeenCalledWith(apiError); + }); + }); + }); + }); +}); |