diff options
Diffstat (limited to 'spec/frontend/snippets/components')
8 files changed, 362 insertions, 264 deletions
diff --git a/spec/frontend/snippets/components/edit_spec.js b/spec/frontend/snippets/components/edit_spec.js index b818f98efb1..2b6d3ca8c2a 100644 --- a/spec/frontend/snippets/components/edit_spec.js +++ b/spec/frontend/snippets/components/edit_spec.js @@ -1,138 +1,120 @@ -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 { merge } from 'lodash'; +import { nextTick } from 'vue'; +import VueApollo, { ApolloMutation } from 'vue-apollo'; +import { useFakeDate } from 'helpers/fake_date'; import createMockApollo from 'helpers/mock_apollo_helper'; +import { stubComponent } from 'helpers/stub_component'; +import waitForPromises from 'helpers/wait_for_promises'; import GetSnippetQuery from 'shared_queries/snippet/snippet.query.graphql'; +import CaptchaModal from '~/captcha/captcha_modal.vue'; import { deprecatedCreateFlash as Flash } from '~/flash'; import * as urlUtils from '~/lib/utils/url_utility'; import SnippetEditApp from '~/snippets/components/edit.vue'; +import SnippetBlobActionsEdit from '~/snippets/components/snippet_blob_actions_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'; +import UpdateSnippetMutation from '~/snippets/mutations/updateSnippet.mutation.graphql'; +import FormFooterActions from '~/vue_shared/components/form/form_footer_actions.vue'; +import TitleField from '~/vue_shared/components/form/title.vue'; +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 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 createTestSnippet = () => ({ - webUrl: TEST_WEB_URL, - id: 7, - title: 'Snippet Title', - description: 'Lorem ipsum snippet desc', - visibilityLevel: SNIPPET_VISIBILITY_PRIVATE, +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; + 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(), - }, - }, - }), - 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, - }; - } + beforeEach(() => { + getSpy = jest.fn().mockResolvedValue(createQueryResponse()); - 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, - }, - }); - } + // See `mutateSpy` declaration comment for why we send a key + mutateSpy = jest.fn().mockImplementation((key) => Promise.resolve(createMutationResponse(key))); - beforeEach(() => { gon.relative_url_root = relativeUrlRoot; jest.spyOn(urlUtils, 'redirectTo').mockImplementation(); }); @@ -144,10 +126,10 @@ describe('Snippet Edit app', () => { }); const findBlobActions = () => wrapper.find(SnippetBlobActionsEdit); + const findCaptchaModal = () => wrapper.find(CaptchaModal); 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) => { @@ -155,53 +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, + }, + }); }; - describe('rendering', () => { - it('renders loader while the query is in flight', () => { - createComponent({ loading: true }); + // Creates comopnent and waits for gql load + const createComponentAndLoad = async (...args) => { + createComponent(...args); + + await waitForPromises(); + }; + + // 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); + describe.each` + snippetGid | expectedQueries + ${TEST_SNIPPET_GID} | ${[[{ ids: [TEST_SNIPPET_GID] }]]} + ${''} | ${[]} + `('when loaded with snippetGid=$snippetGid', ({ snippetGid, expectedQueries }) => { + beforeEach(() => createComponentAndLoad({ props: { snippetGid } })); - 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(`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} @@ -211,163 +232,241 @@ 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 wrapper.vm.$nextTick(); + await 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} + 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 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(); - }); + 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 wrapper.vm.$nextTick(); + 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('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(); + describe('with apollo network error', () => { + beforeEach(async () => { + jest.spyOn(console, 'error').mockImplementation(); + mutateSpy.mockRejectedValue(TEST_API_ERROR); - action(); + await createComponentAndSubmit(); + }); - const event = new Event('beforeunload'); - const returnValueSetter = jest.spyOn(event, 'returnValue', 'set'); + it('should not redirect', () => { + expect(urlUtils.redirectTo).not.toHaveBeenCalled(); + }); - window.dispatchEvent(event); + 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}`, + ); + }); - if (expectPrevented) { - expect(returnValueSetter).toHaveBeenCalledWith( - 'Are you sure you want to lose unsaved changes?', - ); - } else { - expect(returnValueSetter).not.toHaveBeenCalled(); - } - }, - ); + 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; + + beforeEach(async () => { + mutateSpy + .mockResolvedValueOnce(createMutationResponseWithRecaptcha('updateSnippet')) + .mockResolvedValueOnce(createMutationResponseWithErrors('updateSnippet')); + + await createComponentAndSubmit(); + + modal = findCaptchaModal(); + + mutateSpy.mockClear(); + }); + + it('should display captcha modal', () => { + expect(urlUtils.redirectTo).not.toHaveBeenCalled(); + expect(modal.props()).toEqual({ + needsCaptchaResponse: true, + captchaSiteKey: TEST_CAPTCHA_SITE_KEY, + }); + }); + + 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); + + await nextTick(); + }); + + it('sets needsCaptchaResponse to false', () => { + expect(modal.props('needsCaptchaResponse')).toEqual(false); + }); + + it(`expected to call times = ${expectedCalls.length}`, () => { + expect(mutateSpy.mock.calls).toEqual(expectedCalls); + }); + }); + }); }); }); + + 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(); + + 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(); + } + }, + ); + }); }); diff --git a/spec/frontend/snippets/components/embed_dropdown_spec.js b/spec/frontend/snippets/components/embed_dropdown_spec.js index f1eb7d43409..389b1c618a3 100644 --- a/spec/frontend/snippets/components/embed_dropdown_spec.js +++ b/spec/frontend/snippets/components/embed_dropdown_spec.js @@ -1,6 +1,6 @@ -import { escape as esc } from 'lodash'; -import { mount } from '@vue/test-utils'; import { GlFormInputGroup } from '@gitlab/ui'; +import { mount } from '@vue/test-utils'; +import { escape as esc } from 'lodash'; import { TEST_HOST } from 'helpers/test_constants'; import EmbedDropdown from '~/snippets/components/embed_dropdown.vue'; diff --git a/spec/frontend/snippets/components/show_spec.js b/spec/frontend/snippets/components/show_spec.js index b5ab7def753..e6162c6aad2 100644 --- a/spec/frontend/snippets/components/show_spec.js +++ b/spec/frontend/snippets/components/show_spec.js @@ -1,18 +1,17 @@ import { GlLoadingIcon } from '@gitlab/ui'; -import { Blob, BinaryBlob } from 'jest/blob/components/mock_data'; import { shallowMount } from '@vue/test-utils'; -import SnippetApp from '~/snippets/components/show.vue'; +import { Blob, BinaryBlob } from 'jest/blob/components/mock_data'; import EmbedDropdown from '~/snippets/components/embed_dropdown.vue'; +import SnippetApp from '~/snippets/components/show.vue'; +import SnippetBlob from '~/snippets/components/snippet_blob_view.vue'; import SnippetHeader from '~/snippets/components/snippet_header.vue'; import SnippetTitle from '~/snippets/components/snippet_title.vue'; -import SnippetBlob from '~/snippets/components/snippet_blob_view.vue'; -import CloneDropdownButton from '~/vue_shared/components/clone_dropdown.vue'; - import { SNIPPET_VISIBILITY_INTERNAL, SNIPPET_VISIBILITY_PRIVATE, SNIPPET_VISIBILITY_PUBLIC, } from '~/snippets/constants'; +import CloneDropdownButton from '~/vue_shared/components/clone_dropdown.vue'; describe('Snippet view app', () => { let wrapper; diff --git a/spec/frontend/snippets/components/snippet_blob_actions_edit_spec.js b/spec/frontend/snippets/components/snippet_blob_actions_edit_spec.js index 08056e788de..2693b26aeae 100644 --- a/spec/frontend/snippets/components/snippet_blob_actions_edit_spec.js +++ b/spec/frontend/snippets/components/snippet_blob_actions_edit_spec.js @@ -1,5 +1,5 @@ -import { times } from 'lodash'; import { shallowMount } from '@vue/test-utils'; +import { times } from 'lodash'; import SnippetBlobActionsEdit from '~/snippets/components/snippet_blob_actions_edit.vue'; import SnippetBlobEdit from '~/snippets/components/snippet_blob_edit.vue'; import { diff --git a/spec/frontend/snippets/components/snippet_blob_edit_spec.js b/spec/frontend/snippets/components/snippet_blob_edit_spec.js index 9d0311fd682..a7ab205ca7b 100644 --- a/spec/frontend/snippets/components/snippet_blob_edit_spec.js +++ b/spec/frontend/snippets/components/snippet_blob_edit_spec.js @@ -1,14 +1,14 @@ import { GlLoadingIcon } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import AxiosMockAdapter from 'axios-mock-adapter'; -import waitForPromises from 'helpers/wait_for_promises'; import { TEST_HOST } from 'helpers/test_constants'; -import SnippetBlobEdit from '~/snippets/components/snippet_blob_edit.vue'; +import waitForPromises from 'helpers/wait_for_promises'; import BlobHeaderEdit from '~/blob/components/blob_edit_header.vue'; -import EditorLite from '~/vue_shared/components/editor_lite.vue'; +import { deprecatedCreateFlash as createFlash } from '~/flash'; import axios from '~/lib/utils/axios_utils'; import { joinPaths } from '~/lib/utils/url_utility'; -import { deprecatedCreateFlash as createFlash } from '~/flash'; +import SnippetBlobEdit from '~/snippets/components/snippet_blob_edit.vue'; +import EditorLite from '~/vue_shared/components/editor_lite.vue'; jest.mock('~/flash'); diff --git a/spec/frontend/snippets/components/snippet_blob_view_spec.js b/spec/frontend/snippets/components/snippet_blob_view_spec.js index 1ccecd7b5ba..b92c1907980 100644 --- a/spec/frontend/snippets/components/snippet_blob_view_spec.js +++ b/spec/frontend/snippets/components/snippet_blob_view_spec.js @@ -1,5 +1,5 @@ -import { nextTick } from 'vue'; import { mount } from '@vue/test-utils'; +import { nextTick } from 'vue'; import { Blob as BlobMock, SimpleViewerMock, @@ -7,16 +7,16 @@ import { RichBlobContentMock, SimpleBlobContentMock, } from 'jest/blob/components/mock_data'; -import SnippetBlobView from '~/snippets/components/snippet_blob_view.vue'; -import BlobHeader from '~/blob/components/blob_header.vue'; import BlobContent from '~/blob/components/blob_content.vue'; +import BlobHeader from '~/blob/components/blob_header.vue'; import { BLOB_RENDER_EVENT_LOAD, BLOB_RENDER_EVENT_SHOW_SOURCE, BLOB_RENDER_ERRORS, } from '~/blob/components/constants'; -import { RichViewer, SimpleViewer } from '~/vue_shared/components/blob_viewers'; +import SnippetBlobView from '~/snippets/components/snippet_blob_view.vue'; import { SNIPPET_VISIBILITY_PUBLIC } from '~/snippets/constants'; +import { RichViewer, SimpleViewer } from '~/vue_shared/components/blob_viewers'; describe('Blob Embeddable', () => { let wrapper; diff --git a/spec/frontend/snippets/components/snippet_header_spec.js b/spec/frontend/snippets/components/snippet_header_spec.js index 93a66db32c6..585614a6b79 100644 --- a/spec/frontend/snippets/components/snippet_header_spec.js +++ b/spec/frontend/snippets/components/snippet_header_spec.js @@ -1,11 +1,11 @@ -import { ApolloMutation } from 'vue-apollo'; import { GlButton, GlModal } from '@gitlab/ui'; import { mount } from '@vue/test-utils'; -import { Blob, BinaryBlob } from 'jest/blob/components/mock_data'; +import { ApolloMutation } from 'vue-apollo'; import waitForPromises from 'helpers/wait_for_promises'; -import DeleteSnippetMutation from '~/snippets/mutations/deleteSnippet.mutation.graphql'; -import SnippetHeader from '~/snippets/components/snippet_header.vue'; +import { Blob, BinaryBlob } from 'jest/blob/components/mock_data'; import { differenceInMilliseconds } from '~/lib/utils/datetime_utility'; +import SnippetHeader from '~/snippets/components/snippet_header.vue'; +import DeleteSnippetMutation from '~/snippets/mutations/deleteSnippet.mutation.graphql'; describe('Snippet header component', () => { let wrapper; diff --git a/spec/frontend/snippets/components/snippet_title_spec.js b/spec/frontend/snippets/components/snippet_title_spec.js index f201cfb19b7..48fb51ce703 100644 --- a/spec/frontend/snippets/components/snippet_title_spec.js +++ b/spec/frontend/snippets/components/snippet_title_spec.js @@ -1,7 +1,7 @@ import { GlSprintf } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; -import SnippetTitle from '~/snippets/components/snippet_title.vue'; import SnippetDescription from '~/snippets/components/snippet_description_view.vue'; +import SnippetTitle from '~/snippets/components/snippet_title.vue'; describe('Snippet header component', () => { let wrapper; |