diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-08-20 21:42:06 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-08-20 21:42:06 +0300 |
commit | 6e4e1050d9dba2b7b2523fdd1768823ab85feef4 (patch) | |
tree | 78be5963ec075d80116a932011d695dd33910b4e /spec/frontend/snippets/components/edit_spec.js | |
parent | 1ce776de4ae122aba3f349c02c17cebeaa8ecf07 (diff) |
Add latest changes from gitlab-org/gitlab@13-3-stable-ee
Diffstat (limited to 'spec/frontend/snippets/components/edit_spec.js')
-rw-r--r-- | spec/frontend/snippets/components/edit_spec.js | 539 |
1 files changed, 215 insertions, 324 deletions
diff --git a/spec/frontend/snippets/components/edit_spec.js b/spec/frontend/snippets/components/edit_spec.js index d2265dfd506..980855a0615 100644 --- a/spec/frontend/snippets/components/edit_spec.js +++ b/spec/frontend/snippets/components/edit_spec.js @@ -1,134 +1,157 @@ -import { shallowMount } from '@vue/test-utils'; -import Flash from '~/flash'; - +import { ApolloMutation } from 'vue-apollo'; import { GlLoadingIcon } from '@gitlab/ui'; -import { redirectTo } from '~/lib/utils/url_utility'; - +import { shallowMount } from '@vue/test-utils'; +import waitForPromises from 'helpers/wait_for_promises'; +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 SnippetBlobEdit from '~/snippets/components/snippet_blob_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_CREATE_MUTATION_ERROR, SNIPPET_UPDATE_MUTATION_ERROR } from '~/snippets/constants'; - +import { SNIPPET_VISIBILITY_PRIVATE } from '~/snippets/constants'; 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'), -})); +import { testEntries } from '../test_utils'; jest.mock('~/flash'); -let flashSpy; - -const rawProjectPathMock = '/project/path'; -const newlyEditedSnippetUrl = 'http://foo.bar'; -const apiError = { message: 'Ufff' }; -const mutationError = 'Bummer'; - -const attachedFilePath1 = 'foo/bar'; -const attachedFilePath2 = 'alpha/beta'; - -const actionWithContent = { - content: 'Foo Bar', -}; -const actionWithoutContent = { - content: '', -}; +const TEST_UPLOADED_FILES = ['foo/bar.txt', 'alpha/beta.js']; +const TEST_API_ERROR = 'Ufff'; +const TEST_MUTATION_ERROR = 'Bummer'; -const defaultProps = { - snippetGid: 'gid://gitlab/PersonalSnippet/42', - markdownPreviewPath: 'http://preview.foo.bar', - markdownDocsPath: 'http://docs.foo.bar', -}; -const defaultData = { - blobsActions: { - ...actionWithContent, - action: '', +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; - const resolveMutate = jest.fn().mockResolvedValue({ - data: { - updateSnippet: { - errors: [], - snippet: { - webUrl: newlyEditedSnippetUrl, + const mutationTypes = { + RESOLVE: jest.fn().mockResolvedValue({ + data: { + updateSnippet: { + errors: [], + snippet: createTestSnippet(), }, }, - }, - }); - - const resolveMutateWithErrors = jest.fn().mockResolvedValue({ - data: { - updateSnippet: { - errors: [mutationError], - snippet: { - webUrl: newlyEditedSnippetUrl, + }), + RESOLVE_WITH_ERRORS: jest.fn().mockResolvedValue({ + data: { + updateSnippet: { + errors: [TEST_MUTATION_ERROR], + snippet: createTestSnippet(), + }, + createSnippet: { + errors: [TEST_MUTATION_ERROR], + snippet: null, }, }, - createSnippet: { - errors: [mutationError], - snippet: null, - }, - }, - }); - - const rejectMutation = jest.fn().mockRejectedValue(apiError); - - const mutationTypes = { - RESOLVE: resolveMutate, - RESOLVE_WITH_ERRORS: resolveMutateWithErrors, - REJECT: rejectMutation, + }), + REJECT: jest.fn().mockRejectedValue(TEST_API_ERROR), }; function createComponent({ - props = defaultProps, - data = {}, + props = {}, loading = false, mutationRes = mutationTypes.RESOLVE, } = {}) { - const $apollo = { - queries: { - snippet: { - loading, - }, - }, - mutate: mutationRes, - }; + if (wrapper) { + throw new Error('wrapper already exists'); + } wrapper = shallowMount(SnippetEditApp, { - mocks: { $apollo }, + mocks: { + $apollo: { + queries: { + snippet: { loading }, + }, + mutate: mutationRes, + }, + }, stubs: { - FormFooterActions, ApolloMutation, + FormFooterActions, }, propsData: { + snippetGid: 'gid://gitlab/PersonalSnippet/42', + markdownPreviewPath: 'http://preview.foo.bar', + markdownDocsPath: 'http://docs.foo.bar', ...props, }, - data() { - return data; - }, }); - - flashSpy = jest.spyOn(wrapper.vm, 'flashAPIFailure'); } + beforeEach(() => { + jest.spyOn(urlUtils, 'redirectTo').mockImplementation(); + }); + afterEach(() => { wrapper.destroy(); + wrapper = null; }); + const findBlobActions = () => wrapper.find(SnippetBlobActionsEdit); const findSubmitButton = () => wrapper.find('[data-testid="snippet-submit-btn"]'); - const findCancellButton = () => wrapper.find('[data-testid="snippet-cancel-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 = (...edges) => { + if (edges.length) { + wrapper.setData({ + snippet: edges[0], + }); + } + + wrapper.vm.onSnippetFetch({ + data: { + snippets: { + edges, + }, + }, + }); + }; describe('rendering', () => { it('renders loader while the query is in flight', () => { @@ -136,295 +159,163 @@ describe('Snippet Edit app', () => { 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([[{}], [{ snippetGid: '' }]])( + 'should render all required components with %s', + props => { + createComponent(props); - it.each` - title | blobsActions | expectation - ${''} | ${{}} | ${true} - ${''} | ${{ actionWithContent }} | ${true} - ${''} | ${{ actionWithoutContent }} | ${true} - ${'foo'} | ${{}} | ${true} - ${'foo'} | ${{ actionWithoutContent }} | ${true} - ${'foo'} | ${{ actionWithoutContent, actionWithContent }} | ${true} - ${'foo'} | ${{ actionWithContent }} | ${false} - `( - 'disables submit button unless both title and content for all blobs are present', - ({ title, blobsActions, expectation }) => { - createComponent({ - data: { - snippet: { title }, - blobsActions, - }, - }); - const isBtnDisabled = Boolean(findSubmitButton().attributes('disabled')); - expect(isBtnDisabled).toBe(expectation); + expect(wrapper.contains(TitleField)).toBe(true); + expect(wrapper.contains(SnippetDescriptionEdit)).toBe(true); + expect(wrapper.contains(SnippetVisibilityEdit)).toBe(true); + expect(wrapper.contains(FormFooterActions)).toBe(true); + expect(findBlobActions().exists()).toBe(true); }, ); 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, - }, - }); + 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]} | ${true} + `( + 'should handle submit disable (title=$title, actions=$actions, shouldDisable=$shouldDisable)', + async ({ title, actions, shouldDisable }) => { + createComponent(); - expect(findCancellButton().attributes('href')).toBe(expectation); - }); - }); + loadSnippet({ title }); + triggerBlobActions(actions); - describe('functionality', () => { - describe('form submission handling', () => { - it('does not submit unchanged blobs', () => { - const foo = { - action: '', - }; - const bar = { - action: 'update', - }; - createComponent({ - data: { - blobsActions: { - foo, - bar, - }, - }, - }); - clickSubmitBtn(); + await wrapper.vm.$nextTick(); - return waitForPromises().then(() => { - expect(resolveMutate).toHaveBeenCalledWith( - expect.objectContaining({ variables: { input: { files: [bar] } } }), - ); - }); - }); + expect(hasDisabledSubmit()).toBe(shouldDisable); + }, + ); - 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 }) => { + it.each` + projectPath | snippetArg | expectation + ${''} | ${[]} | ${'/-/snippets'} + ${'project/path'} | ${[]} | ${'/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({ - data: { - newSnippet, - ...defaultData, - }, - props: { - ...defaultProps, - projectPath, - }, + props: { projectPath }, }); - const mutationPayload = { - mutation, - variables: { - input: newSnippet ? expect.objectContaining({ projectPath }) : expect.any(Object), - }, - }; - - clickSubmitBtn(); - - expect(resolveMutate).toHaveBeenCalledWith(mutationPayload); - }); + loadSnippet(...snippetArg); - it('redirects to snippet view on successful mutation', () => { - createComponent(); - clickSubmitBtn(); + await wrapper.vm.$nextTick(); - return waitForPromises().then(() => { - expect(redirectTo).toHaveBeenCalledWith(newlyEditedSnippetUrl); - }); - }); + expect(findCancelButton().attributes('href')).toBe(expectation); + }, + ); + }); + describe('functionality', () => { + describe('form submission handling', () => { it.each` - newSnippet | projectPath | mutationName - ${true} | ${rawProjectPathMock} | ${'CreateSnippetMutation with projectPath'} - ${true} | ${''} | ${'CreateSnippetMutation without projectPath'} - ${false} | ${rawProjectPathMock} | ${'UpdateSnippetMutation with projectPath'} - ${false} | ${''} | ${'UpdateSnippetMutation without projectPath'} + 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} `( - 'does not redirect to snippet view if the seemingly successful' + - ' $mutationName response contains errors', - ({ newSnippet, projectPath }) => { + 'should submit mutation with (snippet=$snippetArg, projectPath=$projectPath, uploadedFiles=$uploadedFiles)', + async ({ snippetArg, projectPath, uploadedFiles, mutation, input }) => { createComponent({ - data: { - newSnippet, - }, props: { - ...defaultProps, projectPath, }, - mutationRes: mutationTypes.RESOLVE_WITH_ERRORS, }); + loadSnippet(...snippetArg); + setUploadFilesHtml(uploadedFiles); + + await wrapper.vm.$nextTick(); clickSubmitBtn(); - return waitForPromises().then(() => { - expect(redirectTo).not.toHaveBeenCalled(); - expect(flashSpy).toHaveBeenCalledWith(mutationError); + expect(mutationTypes.RESOLVE).toHaveBeenCalledWith({ + mutation, + variables: { + input, + }, }); }, ); - it('flashes an error if mutation failed', () => { - createComponent({ - mutationRes: mutationTypes.REJECT, - }); + it('should redirect to snippet view on successful mutation', async () => { + createComponent(); + loadSnippet(createTestSnippet()); clickSubmitBtn(); - return waitForPromises().then(() => { - expect(redirectTo).not.toHaveBeenCalled(); - expect(flashSpy).toHaveBeenCalledWith(apiError); - }); + await waitForPromises(); + + expect(urlUtils.redirectTo).toHaveBeenCalledWith(TEST_WEB_URL); }); it.each` - isNew | status | expectation - ${true} | ${`new`} | ${SNIPPET_CREATE_MUTATION_ERROR.replace('%{err}', '')} - ${false} | ${`existing`} | ${SNIPPET_UPDATE_MUTATION_ERROR.replace('%{err}', '')} + 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}`} `( - `renders the correct error message if mutation fails for $status snippet`, - ({ isNew, expectation }) => { + 'should flash error with (snippet=$snippetArg, projectPath=$projectPath)', + async ({ snippetArg, projectPath, mutationRes, expectMessage }) => { createComponent({ - data: { - newSnippet: isNew, + props: { + projectPath, }, - mutationRes: mutationTypes.REJECT, + mutationRes, }); + loadSnippet(...snippetArg); clickSubmitBtn(); - return waitForPromises().then(() => { - expect(Flash).toHaveBeenCalledWith(expect.stringContaining(expectation)); - }); + await waitForPromises(); + + expect(urlUtils.redirectTo).not.toHaveBeenCalled(); + expect(Flash).toHaveBeenCalledWith(expectMessage); }, ); }); - 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()); - }); - }); - describe('on before unload', () => { - let event; - let returnValueSetter; - - const bootstrap = data => { - createComponent({ - data, - }); - - event = new Event('beforeunload'); - returnValueSetter = jest.spyOn(event, 'returnValue', 'set'); - }; - - it('does not prevent page navigation if there are no blobs', () => { - bootstrap(); - window.dispatchEvent(event); - - expect(returnValueSetter).not.toHaveBeenCalled(); - }); - - it('does not prevent page navigation if there are no changes to the blobs content', () => { - bootstrap({ - blobsActions: { - foo: { - ...actionWithContent, - action: '', - }, - }, - }); - window.dispatchEvent(event); + 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(); - expect(returnValueSetter).not.toHaveBeenCalled(); - }); + action(); - it('prevents page navigation if there are some changes in the snippet content', () => { - bootstrap({ - blobsActions: { - foo: { - ...actionWithContent, - action: 'update', - }, - }, - }); + const event = new Event('beforeunload'); + const returnValueSetter = jest.spyOn(event, 'returnValue', 'set'); - window.dispatchEvent(event); + window.dispatchEvent(event); - expect(returnValueSetter).toHaveBeenCalledWith( - 'Are you sure you want to lose unsaved changes?', - ); - }); + if (expectPrevented) { + expect(returnValueSetter).toHaveBeenCalledWith( + 'Are you sure you want to lose unsaved changes?', + ); + } else { + expect(returnValueSetter).not.toHaveBeenCalled(); + } + }, + ); }); }); }); |