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]'); const findCancellButton = () => wrapper.find('[data-testid="snippet-cancel-btn"]'); 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); }, ); 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('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); }); }); }); }); });