diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-02-10 18:09:02 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-02-10 18:09:02 +0000 |
commit | 577bb49691b11bc8ebae3a4966153ed39af60d87 (patch) | |
tree | c34970de0f1fc58463448da0f34be13a2f3f47f9 /spec/frontend/snippets | |
parent | 6cffe9ea21d0974ebd3c544a3b711ffcd35649e2 (diff) | |
download | gitlab-ce-577bb49691b11bc8ebae3a4966153ed39af60d87.tar.gz |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec/frontend/snippets')
-rw-r--r-- | spec/frontend/snippets/components/edit_spec.js | 657 | ||||
-rw-r--r-- | spec/frontend/snippets/test_utils.js | 46 | ||||
-rw-r--r-- | spec/frontend/snippets/utils/error_spec.js | 16 |
3 files changed, 368 insertions, 351 deletions
diff --git a/spec/frontend/snippets/components/edit_spec.js b/spec/frontend/snippets/components/edit_spec.js index 1c06465907a..7a1c6e64614 100644 --- a/spec/frontend/snippets/components/edit_spec.js +++ b/spec/frontend/snippets/components/edit_spec.js @@ -2,6 +2,8 @@ import VueApollo, { ApolloMutation } from 'vue-apollo'; import { GlLoadingIcon } from '@gitlab/ui'; import { shallowMount, createLocalVue } from '@vue/test-utils'; import { nextTick } from 'vue'; +import { merge } from 'lodash'; +import { useFakeDate } from 'helpers/fake_date'; import waitForPromises from 'helpers/wait_for_promises'; import createMockApollo from 'helpers/mock_apollo_helper'; import { stubComponent } from 'helpers/stub_component'; @@ -22,162 +24,97 @@ import { } from '~/snippets/constants'; import UpdateSnippetMutation from '~/snippets/mutations/updateSnippet.mutation.graphql'; import CreateSnippetMutation from '~/snippets/mutations/createSnippet.mutation.graphql'; -import { testEntries } from '../test_utils'; +import { testEntries, createGQLSnippetsQueryResponse, createGQLSnippet } 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_API_ERROR = new Error('TEST_API_ERROR'); +const TEST_MUTATION_ERROR = 'Test mutation error'; +const TEST_CAPTCHA_RESPONSE = 'i-got-a-captcha'; +const TEST_CAPTCHA_SITE_KEY = 'abc123'; const TEST_ACTIONS = { - NO_CONTENT: { - ...testEntries.created.diff, - content: '', - }, - NO_PATH: { - ...testEntries.created.diff, - filePath: '', - }, - VALID: { - ...testEntries.created.diff, - }, + NO_CONTENT: merge({}, testEntries.created.diff, { content: '' }), + NO_PATH: merge({}, testEntries.created.diff, { filePath: '' }), + VALID: merge({}, testEntries.created.diff), }; - const TEST_WEB_URL = '/snippets/7'; +const TEST_SNIPPET_GID = 'gid://gitlab/PersonalSnippet/42'; -const createTestSnippet = () => ({ - webUrl: TEST_WEB_URL, - id: 7, - title: 'Snippet Title', - description: 'Lorem ipsum snippet desc', - visibilityLevel: SNIPPET_VISIBILITY_PRIVATE, +const createSnippet = () => + merge(createGQLSnippet(), { + webUrl: TEST_WEB_URL, + visibilityLevel: SNIPPET_VISIBILITY_PRIVATE, + }); + +const createQueryResponse = (obj = {}) => + createGQLSnippetsQueryResponse([merge(createSnippet(), obj)]); + +const createMutationResponse = (key, obj = {}) => ({ + data: { + [key]: merge( + { + errors: [], + snippet: { + __typename: 'Snippet', + webUrl: TEST_WEB_URL, + }, + spamLogId: null, + needsCaptchaResponse: false, + captchaSiteKey: null, + }, + obj, + ), + }, }); +const createMutationResponseWithErrors = (key) => + createMutationResponse(key, { errors: [TEST_MUTATION_ERROR] }); + +const createMutationResponseWithRecaptcha = (key) => + createMutationResponse(key, { + errors: ['ignored captcha error message'], + needsCaptchaResponse: true, + captchaSiteKey: TEST_CAPTCHA_SITE_KEY, + }); + +const getApiData = ({ + id, + title = '', + description = '', + visibilityLevel = SNIPPET_VISIBILITY_PRIVATE, +} = {}) => ({ + id, + title, + description, + visibilityLevel, + blobActions: [], +}); + +const localVue = createLocalVue(); +localVue.use(VueApollo); + describe('Snippet Edit app', () => { + useFakeDate(); + let wrapper; - let fakeApollo; - const captchaSiteKey = 'abc123'; + let getSpy; + + // Mutate spy receives a "key" so that we can: + // - Use the same spy whether we are creating or updating. + // - Build the correct response object + // - Assert which mutation was sent + let mutateSpy; + 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(), - needsCaptchaResponse: null, - captchaSiteKey: null, - }, - }, - }), - RESOLVE_WITH_ERRORS: jest.fn().mockResolvedValue({ - data: { - updateSnippet: { - errors: [TEST_MUTATION_ERROR], - snippet: createTestSnippet(), - needsCaptchaResponse: null, - captchaSiteKey: null, - }, - createSnippet: { - errors: [TEST_MUTATION_ERROR], - snippet: null, - needsCaptchaResponse: null, - captchaSiteKey: null, - }, - }, - }), - // TODO: QUESTION - This has to be wrapped in a factory function in order for the mock to have - // the `mockResolvedValueOnce` counter properly cleared/reset between test `it` examples, by - // ensuring each one gets a fresh mock instance. It's apparently impossible/hard to manually - // clear/reset them (see https://github.com/facebook/jest/issues/7136). So, should - // we convert all the others to factory functions too, to be consistent? And/or move the whole - // `mutationTypes` declaration into a `beforeEach`? (not sure if that will still solve the - // mock reset problem though). - RESOLVE_WITH_NEEDS_CAPTCHA_RESPONSE: () => - jest - .fn() - // NOTE: There may be a captcha-related error, but it is not used in the GraphQL/Vue flow, - // only a truthy 'needsCaptchaResponse' value is used to trigger the captcha modal showing. - .mockResolvedValueOnce({ - data: { - createSnippet: { - errors: ['ignored captcha error message'], - snippet: null, - needsCaptchaResponse: true, - captchaSiteKey, - }, - }, - }) - // After the captcha is solved and the modal is closed, the second form submission should - // be successful and return needsCaptchaResponse = false. - .mockResolvedValueOnce({ - data: { - createSnippet: { - errors: ['ignored captcha error message'], - snippet: createTestSnippet(), - needsCaptchaResponse: false, - captchaSiteKey: null, - }, - }, - }), - REJECT: jest.fn().mockRejectedValue(TEST_API_ERROR), - }; + beforeEach(() => { + getSpy = jest.fn().mockResolvedValue(createQueryResponse()); - 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, - }; - } + // See `mutateSpy` declaration comment for why we send a key + mutateSpy = jest.fn().mockImplementation((key) => Promise.resolve(createMutationResponse(key))); - wrapper = shallowMount(SnippetEditApp, { - ...componentData, - stubs: { - ApolloMutation, - FormFooterActions, - CaptchaModal: stubComponent(CaptchaModal), - }, - 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(); }); @@ -193,7 +130,6 @@ describe('Snippet Edit app', () => { 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) => { @@ -201,56 +137,92 @@ describe('Snippet Edit app', () => { .map((path) => `<input name="files[]" value="${path}">`) .join(''); }; - const getApiData = ({ - id, - title = '', - description = '', - visibilityLevel = SNIPPET_VISIBILITY_PRIVATE, - } = {}) => ({ - id, - title, - description, - visibilityLevel, - blobActions: [], - }); const setTitle = (val) => wrapper.find(TitleField).vm.$emit('input', val); const setDescription = (val) => wrapper.find(SnippetDescriptionEdit).vm.$emit('input', val); - // 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, - }); + const createComponent = ({ props = {}, selectedLevel = SNIPPET_VISIBILITY_PRIVATE } = {}) => { + if (wrapper) { + throw new Error('wrapper already created'); } + + const requestHandlers = [ + [GetSnippetQuery, getSpy], + // See `mutateSpy` declaration comment for why we send a key + [UpdateSnippetMutation, (...args) => mutateSpy('updateSnippet', ...args)], + [CreateSnippetMutation, (...args) => mutateSpy('createSnippet', ...args)], + ]; + const apolloProvider = createMockApollo(requestHandlers); + + wrapper = shallowMount(SnippetEditApp, { + apolloProvider, + localVue, + stubs: { + ApolloMutation, + FormFooterActions, + CaptchaModal: stubComponent(CaptchaModal), + }, + provide: { + selectedLevel, + }, + propsData: { + snippetGid: TEST_SNIPPET_GID, + markdownPreviewPath: 'http://preview.foo.bar', + markdownDocsPath: 'http://docs.foo.bar', + ...props, + }, + }); + }; + + // Creates comopnent and waits for gql load + const createComponentAndLoad = async (...args) => { + createComponent(...args); + + await waitForPromises(); }; - describe('rendering', () => { - it('renders loader while the query is in flight', () => { - createComponent({ loading: true }); + // Creates loaded component and submits form + const createComponentAndSubmit = async (...args) => { + await createComponentAndLoad(...args); + + clickSubmitBtn(); + + await waitForPromises(); + }; + + describe('when loading', () => { + it('renders loader', () => { + createComponent(); + expect(wrapper.find(GlLoadingIcon).exists()).toBe(true); }); + }); - it.each([[{}], [{ snippetGid: '' }]])( - 'should render all required components with %s', - (props) => { - createComponent(props); - - expect(wrapper.find(CaptchaModal).exists()).toBe(true); - 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); - }, - ); + describe.each` + snippetGid | expectedQueries + ${TEST_SNIPPET_GID} | ${[[{ ids: [TEST_SNIPPET_GID] }]]} + ${''} | ${[]} + `('when loaded with snippetGid=$snippetGid', ({ snippetGid, expectedQueries }) => { + beforeEach(() => createComponentAndLoad({ props: { snippetGid } })); + it(`queries with ${JSON.stringify(expectedQueries)}`, () => { + expect(getSpy.mock.calls).toEqual(expectedQueries); + }); + + it('should render components', () => { + expect(wrapper.find(CaptchaModal).exists()).toBe(true); + 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('should hide loader', () => { + expect(wrapper.find(GlLoadingIcon).exists()).toBe(false); + }); + }); + + describe('default', () => { it.each` title | actions | shouldDisable ${''} | ${[]} | ${true} @@ -260,11 +232,12 @@ describe('Snippet Edit app', () => { ${'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)', + 'should handle submit disable (title="$title", actions="$actions", shouldDisable="$shouldDisable")', async ({ title, actions, shouldDisable }) => { - createComponent(); + getSpy.mockResolvedValue(createQueryResponse({ title })); + + await createComponentAndLoad(); - loadSnippet({ title }); triggerBlobActions(actions); await nextTick(); @@ -274,244 +247,226 @@ describe('Snippet Edit app', () => { ); 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} + projectPath | snippetGid | expectation + ${''} | ${''} | ${urlUtils.joinPaths('/', relativeUrlRoot, '-', 'snippets')} + ${'project/path'} | ${''} | ${urlUtils.joinPaths('/', relativeUrlRoot, 'project/path/-', 'snippets')} + ${''} | ${TEST_SNIPPET_GID} | ${TEST_WEB_URL} + ${'project/path'} | ${TEST_SNIPPET_GID} | ${TEST_WEB_URL} `( - 'should set cancel href when (projectPath=$projectPath, snippet=$snippetArg)', - async ({ projectPath, snippetArg, expectation }) => { - createComponent({ - props: { projectPath }, + 'should set cancel href (projectPath="$projectPath", snippetGid="$snippetGid")', + async ({ projectPath, snippetGid, expectation }) => { + await createComponentAndLoad({ + props: { + projectPath, + snippetGid, + }, }); - loadSnippet(...snippetArg); - - await 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 nextTick(); - expect(GetSnippetQuerySpy).not.toHaveBeenCalled(); - }); + it.each([SNIPPET_VISIBILITY_PRIVATE, SNIPPET_VISIBILITY_INTERNAL, SNIPPET_VISIBILITY_PUBLIC])( + 'marks %s visibility by default', + async (visibility) => { + createComponent({ + props: { snippetGid: '' }, + selectedLevel: visibility, + }); - 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); - }, - ); - }); + expect(wrapper.find(SnippetVisibilityEdit).props('value')).toBe(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} + snippetGid | projectPath | uploadedFiles | input | mutationType + ${''} | ${'project/path'} | ${[]} | ${{ ...getApiData(), projectPath: 'project/path', uploadedFiles: [] }} | ${'createSnippet'} + ${''} | ${''} | ${[]} | ${{ ...getApiData(), projectPath: '', uploadedFiles: [] }} | ${'createSnippet'} + ${''} | ${''} | ${TEST_UPLOADED_FILES} | ${{ ...getApiData(), projectPath: '', uploadedFiles: TEST_UPLOADED_FILES }} | ${'createSnippet'} + ${TEST_SNIPPET_GID} | ${'project/path'} | ${[]} | ${getApiData(createSnippet())} | ${'updateSnippet'} + ${TEST_SNIPPET_GID} | ${''} | ${[]} | ${getApiData(createSnippet())} | ${'updateSnippet'} `( - 'should submit mutation with (snippet=$snippetArg, projectPath=$projectPath, uploadedFiles=$uploadedFiles)', - async ({ snippetArg, projectPath, uploadedFiles, mutation, input }) => { - createComponent({ + 'should submit mutation $mutationType (snippetGid=$snippetGid, projectPath=$projectPath, uploadedFiles=$uploadedFiles)', + async ({ snippetGid, projectPath, uploadedFiles, mutationType, input }) => { + await createComponentAndLoad({ props: { + snippetGid, projectPath, }, }); - loadSnippet(...snippetArg); + setUploadFilesHtml(uploadedFiles); await nextTick(); clickSubmitBtn(); - expect(mutationTypes.RESOLVE).toHaveBeenCalledWith({ - mutation, - variables: { - input, - }, + expect(mutateSpy).toHaveBeenCalledTimes(1); + expect(mutateSpy).toHaveBeenCalledWith(mutationType, { + input, }); }, ); it('should redirect to snippet view on successful mutation', async () => { - createComponent(); - loadSnippet(createTestSnippet()); - - clickSubmitBtn(); - - await waitForPromises(); + await createComponentAndSubmit(); 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}`} + snippetGid | projectPath | mutationRes | expectMessage + ${''} | ${'project/path'} | ${createMutationResponseWithErrors('createSnippet')} | ${`Can't create snippet: ${TEST_MUTATION_ERROR}`} + ${''} | ${''} | ${createMutationResponseWithErrors('createSnippet')} | ${`Can't create snippet: ${TEST_MUTATION_ERROR}`} + ${TEST_SNIPPET_GID} | ${'project/path'} | ${createMutationResponseWithErrors('updateSnippet')} | ${`Can't update snippet: ${TEST_MUTATION_ERROR}`} + ${TEST_SNIPPET_GID} | ${''} | ${createMutationResponseWithErrors('updateSnippet')} | ${`Can't update snippet: ${TEST_MUTATION_ERROR}`} `( - 'should flash error with (snippet=$snippetArg, projectPath=$projectPath)', - async ({ snippetArg, projectPath, mutationRes, expectMessage }) => { - createComponent({ + 'should flash error with (snippet=$snippetGid, projectPath=$projectPath)', + async ({ snippetGid, projectPath, mutationRes, expectMessage }) => { + mutateSpy.mockResolvedValue(mutationRes); + + await createComponentAndSubmit({ props: { projectPath, + snippetGid, }, - mutationRes, }); - loadSnippet(...snippetArg); - - clickSubmitBtn(); - - await waitForPromises(); expect(urlUtils.redirectTo).not.toHaveBeenCalled(); expect(Flash).toHaveBeenCalledWith(expectMessage); }, ); + describe('with apollo network error', () => { + beforeEach(async () => { + jest.spyOn(console, 'error').mockImplementation(); + mutateSpy.mockRejectedValue(TEST_API_ERROR); + + await createComponentAndSubmit(); + }); + + it('should not redirect', () => { + expect(urlUtils.redirectTo).not.toHaveBeenCalled(); + }); + + it('should flash', () => { + // Apollo automatically wraps the resolver's error in a NetworkError + expect(Flash).toHaveBeenCalledWith( + `Can't update snippet: Network error: ${TEST_API_ERROR.message}`, + ); + }); + + it('should console error', () => { + // eslint-disable-next-line no-console + expect(console.error).toHaveBeenCalledTimes(1); + // eslint-disable-next-line no-console + expect(console.error).toHaveBeenCalledWith( + '[gitlab] unexpected error while updating snippet', + expect.objectContaining({ message: `Network error: ${TEST_API_ERROR.message}` }), + ); + }); + }); + describe('when needsCaptchaResponse is true', () => { let modal; - let captchaResponse; - let mutationRes; beforeEach(async () => { - mutationRes = mutationTypes.RESOLVE_WITH_NEEDS_CAPTCHA_RESPONSE(); - createComponent({ - props: { - snippetGid: '', - projectPath: '', - }, - mutationRes, - }); - // await waitForPromises(); - modal = findCaptchaModal(); + mutateSpy + .mockResolvedValueOnce(createMutationResponseWithRecaptcha('updateSnippet')) + .mockResolvedValueOnce(createMutationResponseWithErrors('updateSnippet')); - loadSnippet(); + await createComponentAndSubmit(); - clickSubmitBtn(); - await waitForPromises(); + modal = findCaptchaModal(); + + mutateSpy.mockClear(); }); it('should display captcha modal', () => { expect(urlUtils.redirectTo).not.toHaveBeenCalled(); - expect(modal.props('needsCaptchaResponse')).toEqual(true); - expect(modal.props('captchaSiteKey')).toEqual(captchaSiteKey); - }); - - describe('when a non-empty captcha response is received', () => { - beforeEach(() => { - captchaResponse = 'xyz123'; + expect(modal.props()).toEqual({ + needsCaptchaResponse: true, + captchaSiteKey: TEST_CAPTCHA_SITE_KEY, }); + }); - it('sets needsCaptchaResponse to false', async () => { - modal.vm.$emit('receivedCaptchaResponse', captchaResponse); - await nextTick(); - expect(modal.props('needsCaptchaResponse')).toEqual(false); - }); + describe.each` + response | expectedCalls + ${null} | ${[]} + ${TEST_CAPTCHA_RESPONSE} | ${[['updateSnippet', { input: { ...getApiData(createSnippet()), captchaResponse: TEST_CAPTCHA_RESPONSE } }]]} + `('when captcha response is $response', ({ response, expectedCalls }) => { + beforeEach(async () => { + modal.vm.$emit('receivedCaptchaResponse', response); - it('resubmits form with captchaResponse', async () => { - modal.vm.$emit('receivedCaptchaResponse', captchaResponse); await nextTick(); - expect(mutationRes.mock.calls[1][0]).toEqual({ - mutation: CreateSnippetMutation, - variables: { - input: { - ...getApiData(), - captchaResponse, - projectPath: '', - uploadedFiles: [], - }, - }, - }); - }); - }); - - describe('when an empty captcha response is received ', () => { - beforeEach(() => { - captchaResponse = ''; }); - it('sets needsCaptchaResponse to false', async () => { - modal.vm.$emit('receivedCaptchaResponse', captchaResponse); - await nextTick(); + it('sets needsCaptchaResponse to false', () => { expect(modal.props('needsCaptchaResponse')).toEqual(false); }); - it('does not resubmit form', async () => { - modal.vm.$emit('receivedCaptchaResponse', captchaResponse); - await nextTick(); - expect(mutationRes.mock.calls.length).toEqual(1); + it(`expected to call times = ${expectedCalls.length}`, () => { + expect(mutateSpy.mock.calls).toEqual(expectedCalls); }); }); }); }); + }); - describe('on before unload', () => { - const caseNoActions = () => triggerBlobActions([]); - const caseEmptyAction = () => triggerBlobActions([testEntries.empty.diff]); - const caseSomeActions = () => triggerBlobActions([testEntries.updated.diff]); - const caseTitleIsSet = () => { - caseEmptyAction(); - setTitle('test'); - }; - const caseDescriptionIsSet = () => { - caseEmptyAction(); - setDescription('test'); - }; - const caseClickSubmitBtn = () => { - caseSomeActions(); - clickSubmitBtn(); - }; - - it.each` - condition | expectPrevented | action - ${'there are no actions'} | ${false} | ${caseNoActions} - ${'there is an empty action'} | ${false} | ${caseEmptyAction} - ${'there are actions'} | ${true} | ${caseSomeActions} - ${'the title is set'} | ${true} | ${caseTitleIsSet} - ${'the description is set'} | ${true} | ${caseDescriptionIsSet} - ${'the snippet is being saved'} | ${false} | ${caseClickSubmitBtn} - `( - 'handles before unload prevent when $condition (expectPrevented=$expectPrevented)', - ({ expectPrevented, action }) => { - createComponent(); - loadSnippet(); + describe('on before unload', () => { + it.each([ + ['there are no actions', false, () => triggerBlobActions([])], + ['there is an empty action', false, () => triggerBlobActions([testEntries.empty.diff])], + ['there are actions', true, () => triggerBlobActions([testEntries.updated.diff])], + [ + 'the title is set', + true, + () => { + triggerBlobActions([testEntries.empty.diff]); + setTitle('test'); + }, + ], + [ + 'the description is set', + true, + () => { + triggerBlobActions([testEntries.empty.diff]); + setDescription('test'); + }, + ], + [ + 'the snippet is being saved', + false, + () => { + triggerBlobActions([testEntries.updated.diff]); + clickSubmitBtn(); + }, + ], + ])( + 'handles before unload prevent when %s (expectPrevented=%s)', + async (_, expectPrevented, action) => { + await createComponentAndLoad({ + props: { + snippetGid: '', + }, + }); - action(); + action(); - const event = new Event('beforeunload'); - const returnValueSetter = jest.spyOn(event, 'returnValue', 'set'); + const event = new Event('beforeunload'); + const returnValueSetter = jest.spyOn(event, 'returnValue', 'set'); - window.dispatchEvent(event); + window.dispatchEvent(event); - if (expectPrevented) { - expect(returnValueSetter).toHaveBeenCalledWith( - 'Are you sure you want to lose unsaved changes?', - ); - } else { - expect(returnValueSetter).not.toHaveBeenCalled(); - } - }, - ); - }); + if (expectPrevented) { + expect(returnValueSetter).toHaveBeenCalledWith( + 'Are you sure you want to lose unsaved changes?', + ); + } else { + expect(returnValueSetter).not.toHaveBeenCalled(); + } + }, + ); }); }); diff --git a/spec/frontend/snippets/test_utils.js b/spec/frontend/snippets/test_utils.js index fd389620d35..8ba5a2fe5dc 100644 --- a/spec/frontend/snippets/test_utils.js +++ b/spec/frontend/snippets/test_utils.js @@ -1,3 +1,4 @@ +import { TEST_HOST } from 'helpers/test_constants'; import { SNIPPET_BLOB_ACTION_CREATE, SNIPPET_BLOB_ACTION_UPDATE, @@ -8,6 +9,51 @@ import { const CONTENT_1 = 'Lorem ipsum dolar\nSit amit\n\nGoodbye!\n'; const CONTENT_2 = 'Lorem ipsum dolar sit amit.\n\nGoodbye!\n'; +export const createGQLSnippet = () => ({ + __typename: 'Snippet', + id: 7, + title: 'Snippet Title', + description: 'Lorem ipsum snippet desc', + descriptionHtml: '<p>Lorem ipsum snippet desc</p>', + createdAt: new Date(Date.now() - 1e6), + updatedAt: new Date(Date.now() - 1e3), + httpUrlToRepo: `${TEST_HOST}/repo`, + sshUrlToRepo: 'ssh://ssh.test/repo', + blobs: [], + userPermissions: { + __typename: 'SnippetPermissions', + adminSnippet: true, + updateSnippet: true, + }, + project: { + __typename: 'Project', + fullPath: 'group/project', + webUrl: `${TEST_HOST}/group/project`, + }, + author: { + __typename: 'User', + id: 1, + avatarUrl: `${TEST_HOST}/avatar.png`, + name: 'root', + username: 'root', + webUrl: `${TEST_HOST}/root`, + status: { + __typename: 'UserStatus', + emoji: '', + message: '', + }, + }, +}); + +export const createGQLSnippetsQueryResponse = (snippets) => ({ + data: { + snippets: { + __typename: 'SnippetConnection', + nodes: snippets, + }, + }, +}); + export const testEntries = { created: { id: 'blob_1', diff --git a/spec/frontend/snippets/utils/error_spec.js b/spec/frontend/snippets/utils/error_spec.js new file mode 100644 index 00000000000..385554568db --- /dev/null +++ b/spec/frontend/snippets/utils/error_spec.js @@ -0,0 +1,16 @@ +import { getErrorMessage, UNEXPECTED_ERROR } from '~/snippets/utils/error'; + +describe('~/snippets/utils/error', () => { + describe('getErrorMessage', () => { + it.each` + input | output + ${null} | ${UNEXPECTED_ERROR} + ${'message'} | ${'message'} + ${new Error('test message')} | ${'test message'} + ${{ networkError: 'Network error: test message' }} | ${'Network error: test message'} + ${{}} | ${UNEXPECTED_ERROR} + `('with $input, should return "$output"', ({ input, output }) => { + expect(getErrorMessage(input)).toBe(output); + }); + }); +}); |