From a09983ae35713f5a2bbb100981116d31ce99826e Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Mon, 20 Jul 2020 12:26:25 +0000 Subject: Add latest changes from gitlab-org/gitlab@13-2-stable-ee --- spec/frontend/snippets/components/edit_spec.js | 179 +++++++++++---------- spec/frontend/snippets/components/show_spec.js | 35 +++- .../snippets/components/snippet_blob_edit_spec.js | 137 ++++++++++++---- .../snippets/components/snippet_blob_view_spec.js | 38 ++--- .../snippets/components/snippet_header_spec.js | 19 +-- 5 files changed, 265 insertions(+), 143 deletions(-) (limited to 'spec/frontend/snippets') diff --git a/spec/frontend/snippets/components/edit_spec.js b/spec/frontend/snippets/components/edit_spec.js index 83f46dd347f..d2265dfd506 100644 --- a/spec/frontend/snippets/components/edit_spec.js +++ b/spec/frontend/snippets/components/edit_spec.js @@ -1,9 +1,8 @@ import { shallowMount } from '@vue/test-utils'; -import axios from '~/lib/utils/axios_utils'; import Flash from '~/flash'; import { GlLoadingIcon } from '@gitlab/ui'; -import { joinPaths, redirectTo } from '~/lib/utils/url_utility'; +import { redirectTo } from '~/lib/utils/url_utility'; import SnippetEditApp from '~/snippets/components/edit.vue'; import SnippetDescriptionEdit from '~/snippets/components/snippet_description_edit.vue'; @@ -16,25 +15,17 @@ import { SNIPPET_CREATE_MUTATION_ERROR, SNIPPET_UPDATE_MUTATION_ERROR } from '~/ 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'), })); jest.mock('~/flash'); let flashSpy; -const contentMock = 'Foo Bar'; -const rawPathMock = '/foo/bar'; const rawProjectPathMock = '/project/path'; const newlyEditedSnippetUrl = 'http://foo.bar'; const apiError = { message: 'Ufff' }; @@ -43,15 +34,27 @@ const mutationError = 'Bummer'; const attachedFilePath1 = 'foo/bar'; const attachedFilePath2 = 'alpha/beta'; +const actionWithContent = { + content: 'Foo Bar', +}; +const actionWithoutContent = { + content: '', +}; + const defaultProps = { snippetGid: 'gid://gitlab/PersonalSnippet/42', markdownPreviewPath: 'http://preview.foo.bar', markdownDocsPath: 'http://docs.foo.bar', }; +const defaultData = { + blobsActions: { + ...actionWithContent, + action: '', + }, +}; describe('Snippet Edit app', () => { let wrapper; - let axiosMock; const resolveMutate = jest.fn().mockResolvedValue({ data: { @@ -156,18 +159,21 @@ describe('Snippet Edit app', () => { }); it.each` - title | content | expectation - ${''} | ${''} | ${true} - ${'foo'} | ${''} | ${true} - ${''} | ${'foo'} | ${true} - ${'foo'} | ${'bar'} | ${false} + 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 are present', - ({ title, content, expectation }) => { + 'disables submit button unless both title and content for all blobs are present', + ({ title, blobsActions, expectation }) => { createComponent({ data: { snippet: { title }, - content, + blobsActions, }, }); const isBtnDisabled = Boolean(findSubmitButton().attributes('disabled')); @@ -192,83 +198,31 @@ describe('Snippet Edit app', () => { }); describe('functionality', () => { - describe('handling of the data from GraphQL response', () => { - const snippet = { - blob: { - rawPath: rawPathMock, - }, - }; - const getResSchema = newSnippet => { - return { - data: { - snippets: { - edges: newSnippet ? [] : [snippet], - }, - }, + describe('form submission handling', () => { + it('does not submit unchanged blobs', () => { + const foo = { + action: '', + }; + const bar = { + action: 'update', }; - }; - - const bootstrapForExistingSnippet = resp => { createComponent({ data: { - snippet, + blobsActions: { + foo, + bar, + }, }, }); - - 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(); + clickSubmitBtn(); 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); + expect(resolveMutate).toHaveBeenCalledWith( + expect.objectContaining({ variables: { input: { files: [bar] } } }), + ); }); }); - }); - describe('form submission handling', () => { it.each` newSnippet | projectPath | mutation | mutationName ${true} | ${rawProjectPathMock} | ${CreateSnippetMutation} | ${'CreateSnippetMutation with projectPath'} @@ -279,6 +233,7 @@ describe('Snippet Edit app', () => { createComponent({ data: { newSnippet, + ...defaultData, }, props: { ...defaultProps, @@ -419,5 +374,57 @@ describe('Snippet Edit app', () => { 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); + + expect(returnValueSetter).not.toHaveBeenCalled(); + }); + + it('prevents page navigation if there are some changes in the snippet content', () => { + bootstrap({ + blobsActions: { + foo: { + ...actionWithContent, + action: 'update', + }, + }, + }); + + window.dispatchEvent(event); + + expect(returnValueSetter).toHaveBeenCalledWith( + 'Are you sure you want to lose unsaved changes?', + ); + }); + }); }); }); diff --git a/spec/frontend/snippets/components/show_spec.js b/spec/frontend/snippets/components/show_spec.js index 33608df8cf2..b5446e70028 100644 --- a/spec/frontend/snippets/components/show_spec.js +++ b/spec/frontend/snippets/components/show_spec.js @@ -1,10 +1,13 @@ import SnippetApp from '~/snippets/components/show.vue'; +import BlobEmbeddable from '~/blob/components/blob_embeddable.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 { GlLoadingIcon } from '@gitlab/ui'; +import { Blob, BinaryBlob } from 'jest/blob/components/mock_data'; import { shallowMount } from '@vue/test-utils'; +import { SNIPPET_VISIBILITY_PUBLIC } from '~/snippets/constants'; describe('Snippet view app', () => { let wrapper; @@ -12,7 +15,7 @@ describe('Snippet view app', () => { snippetGid: 'gid://gitlab/PersonalSnippet/42', }; - function createComponent({ props = defaultProps, loading = false } = {}) { + function createComponent({ props = defaultProps, data = {}, loading = false } = {}) { const $apollo = { queries: { snippet: { @@ -26,6 +29,9 @@ describe('Snippet view app', () => { propsData: { ...props, }, + data() { + return data; + }, }); } afterEach(() => { @@ -37,10 +43,33 @@ describe('Snippet view app', () => { expect(wrapper.find(GlLoadingIcon).exists()).toBe(true); }); - it('renders all components after the query is finished', () => { + it('renders all simple components after the query is finished', () => { createComponent(); expect(wrapper.find(SnippetHeader).exists()).toBe(true); expect(wrapper.find(SnippetTitle).exists()).toBe(true); - expect(wrapper.find(SnippetBlob).exists()).toBe(true); + }); + + it('renders embeddable component if visibility allows', () => { + createComponent({ + data: { + snippet: { + visibilityLevel: SNIPPET_VISIBILITY_PUBLIC, + webUrl: 'http://foo.bar', + }, + }, + }); + expect(wrapper.contains(BlobEmbeddable)).toBe(true); + }); + + it('renders correct snippet-blob components', () => { + createComponent({ + data: { + blobs: [Blob, BinaryBlob], + }, + }); + const blobs = wrapper.findAll(SnippetBlob); + expect(blobs.length).toBe(2); + expect(blobs.at(0).props('blob')).toEqual(Blob); + expect(blobs.at(1).props('blob')).toEqual(BinaryBlob); }); }); diff --git a/spec/frontend/snippets/components/snippet_blob_edit_spec.js b/spec/frontend/snippets/components/snippet_blob_edit_spec.js index 75688e61892..009074b4558 100644 --- a/spec/frontend/snippets/components/snippet_blob_edit_spec.js +++ b/spec/frontend/snippets/components/snippet_blob_edit_spec.js @@ -4,78 +4,161 @@ import BlobContentEdit from '~/blob/components/blob_edit_content.vue'; import { GlLoadingIcon } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import { nextTick } from 'vue'; +import AxiosMockAdapter from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; +import { joinPaths } from '~/lib/utils/url_utility'; +import waitForPromises from 'helpers/wait_for_promises'; jest.mock('~/blob/utils', () => jest.fn()); +jest.mock('~/lib/utils/url_utility', () => ({ + getBaseURL: jest.fn().mockReturnValue('foo/'), + joinPaths: jest + .fn() + .mockName('joinPaths') + .mockReturnValue('contentApiURL'), +})); + +jest.mock('~/flash'); + +let flashSpy; + describe('Snippet Blob Edit component', () => { let wrapper; - const value = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.'; - const fileName = 'lorem.txt'; - const findHeader = () => wrapper.find(BlobHeaderEdit); - const findContent = () => wrapper.find(BlobContentEdit); + let axiosMock; + const contentMock = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.'; + const pathMock = 'lorem.txt'; + const rawPathMock = 'foo/bar'; + const blob = { + path: pathMock, + content: contentMock, + rawPath: rawPathMock, + }; + const findComponent = component => wrapper.find(component); - function createComponent(props = {}) { + function createComponent(props = {}, data = { isContentLoading: false }) { wrapper = shallowMount(SnippetBlobEdit, { propsData: { - value, - fileName, - isLoading: false, ...props, }, + data() { + return { + ...data, + }; + }, }); + flashSpy = jest.spyOn(wrapper.vm, 'flashAPIFailure'); } beforeEach(() => { + axiosMock = new AxiosMockAdapter(axios); createComponent(); }); afterEach(() => { + axiosMock.restore(); wrapper.destroy(); }); describe('rendering', () => { it('matches the snapshot', () => { + createComponent({ blob }); expect(wrapper.element).toMatchSnapshot(); }); it('renders required components', () => { - expect(findHeader().exists()).toBe(true); - expect(findContent().exists()).toBe(true); + expect(findComponent(BlobHeaderEdit).exists()).toBe(true); + expect(findComponent(BlobContentEdit).exists()).toBe(true); }); - it('renders loader if isLoading equals true', () => { - createComponent({ isLoading: true }); + it('renders loader if existing blob is supplied but no content is fetched yet', () => { + createComponent({ blob }, { isContentLoading: true }); expect(wrapper.contains(GlLoadingIcon)).toBe(true); - expect(findContent().exists()).toBe(false); + expect(findComponent(BlobContentEdit).exists()).toBe(false); + }); + + it('does not render loader if when blob is not supplied', () => { + createComponent(); + expect(wrapper.contains(GlLoadingIcon)).toBe(false); + expect(findComponent(BlobContentEdit).exists()).toBe(true); }); }); describe('functionality', () => { - it('does not fail without content', () => { + it('does not fail without blob', () => { const spy = jest.spyOn(global.console, 'error'); - createComponent({ value: undefined }); + createComponent({ blob: undefined }); expect(spy).not.toHaveBeenCalled(); - expect(findContent().exists()).toBe(true); + expect(findComponent(BlobContentEdit).exists()).toBe(true); }); - it('emits "name-change" event when the file name gets changed', () => { - expect(wrapper.emitted('name-change')).toBeUndefined(); - const newFilename = 'foo.bar'; - findHeader().vm.$emit('input', newFilename); + it.each` + emitter | prop + ${BlobHeaderEdit} | ${'filePath'} + ${BlobContentEdit} | ${'content'} + `('emits "blob-updated" event when the $prop gets changed', ({ emitter, prop }) => { + expect(wrapper.emitted('blob-updated')).toBeUndefined(); + const newValue = 'foo.bar'; + findComponent(emitter).vm.$emit('input', newValue); return nextTick().then(() => { - expect(wrapper.emitted('name-change')[0]).toEqual([newFilename]); + expect(wrapper.emitted('blob-updated')[0]).toEqual([ + expect.objectContaining({ + [prop]: newValue, + }), + ]); }); }); - it('emits "input" event when the file content gets changed', () => { - expect(wrapper.emitted('input')).toBeUndefined(); - const newValue = 'foo.bar'; - findContent().vm.$emit('input', newValue); + describe('fetching blob content', () => { + const bootstrapForExistingSnippet = resp => { + createComponent({ + blob: { + ...blob, + content: '', + }, + }); - return nextTick().then(() => { - expect(wrapper.emitted('input')[0]).toEqual([newValue]); + if (resp === 500) { + axiosMock.onGet('contentApiURL').reply(500); + } else { + axiosMock.onGet('contentApiURL').reply(200, contentMock); + } + }; + + const bootstrapForNewSnippet = () => { + createComponent(); + }; + + it('fetches blob content with the additional query', () => { + bootstrapForExistingSnippet(); + + return waitForPromises().then(() => { + expect(joinPaths).toHaveBeenCalledWith('foo/', rawPathMock); + expect(findComponent(BlobHeaderEdit).props('value')).toBe(pathMock); + expect(findComponent(BlobContentEdit).props('value')).toBe(contentMock); + }); + }); + + it('flashes the error message if fetching content fails', () => { + bootstrapForExistingSnippet(500); + + return waitForPromises().then(() => { + expect(flashSpy).toHaveBeenCalled(); + expect(findComponent(BlobContentEdit).props('value')).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(findComponent(BlobHeaderEdit).props('value')).toBe(''); + expect(findComponent(BlobContentEdit).props('value')).toBe(''); + expect(joinPaths).not.toHaveBeenCalled(); + }); }); }); }); diff --git a/spec/frontend/snippets/components/snippet_blob_view_spec.js b/spec/frontend/snippets/components/snippet_blob_view_spec.js index e4d8ee9b7df..c8f1c8fc8a9 100644 --- a/spec/frontend/snippets/components/snippet_blob_view_spec.js +++ b/spec/frontend/snippets/components/snippet_blob_view_spec.js @@ -23,13 +23,17 @@ describe('Blob Embeddable', () => { id: 'gid://foo.bar/snippet', webUrl: 'https://foo.bar', visibilityLevel: SNIPPET_VISIBILITY_PUBLIC, - blob: BlobMock, }; const dataMock = { activeViewerType: SimpleViewerMock.type, }; - function createComponent(props = {}, data = dataMock, contentLoading = false) { + function createComponent({ + snippetProps = {}, + data = dataMock, + blob = BlobMock, + contentLoading = false, + } = {}) { const $apollo = { queries: { blobContent: { @@ -44,8 +48,9 @@ describe('Blob Embeddable', () => { propsData: { snippet: { ...snippet, - ...props, + ...snippetProps, }, + blob, }, data() { return { @@ -63,7 +68,6 @@ describe('Blob Embeddable', () => { describe('rendering', () => { it('renders correct components', () => { createComponent(); - expect(wrapper.find(BlobEmbeddable).exists()).toBe(true); expect(wrapper.find(BlobHeader).exists()).toBe(true); expect(wrapper.find(BlobContent).exists()).toBe(true); }); @@ -72,19 +76,14 @@ describe('Blob Embeddable', () => { 'does not render blob-embeddable by default', visibilityLevel => { createComponent({ - visibilityLevel, + snippetProps: { + visibilityLevel, + }, }); expect(wrapper.find(BlobEmbeddable).exists()).toBe(false); }, ); - it('does render blob-embeddable for public snippet', () => { - createComponent({ - visibilityLevel: SNIPPET_VISIBILITY_PUBLIC, - }); - expect(wrapper.find(BlobEmbeddable).exists()).toBe(true); - }); - it('sets simple viewer correctly', () => { createComponent(); expect(wrapper.find(SimpleViewer).exists()).toBe(true); @@ -92,7 +91,9 @@ describe('Blob Embeddable', () => { it('sets rich viewer correctly', () => { const data = { ...dataMock, activeViewerType: RichViewerMock.type }; - createComponent({}, data); + createComponent({ + data, + }); expect(wrapper.find(RichViewer).exists()).toBe(true); }); @@ -137,7 +138,9 @@ describe('Blob Embeddable', () => { }); it('renders simple viewer by default if URL contains hash', () => { - createComponent({}, {}); + createComponent({ + data: {}, + }); expect(wrapper.vm.activeViewerType).toBe(SimpleViewerMock.type); expect(wrapper.find(SimpleViewer).exists()).toBe(true); @@ -183,12 +186,11 @@ describe('Blob Embeddable', () => { }); it(`sets '${SimpleViewerMock.type}' as active on ${BLOB_RENDER_EVENT_SHOW_SOURCE} event`, () => { - createComponent( - {}, - { + createComponent({ + data: { activeViewerType: RichViewerMock.type, }, - ); + }); findContentEl().vm.$emit(BLOB_RENDER_EVENT_SHOW_SOURCE); expect(wrapper.vm.activeViewerType).toEqual(SimpleViewerMock.type); diff --git a/spec/frontend/snippets/components/snippet_header_spec.js b/spec/frontend/snippets/components/snippet_header_spec.js index 5230910b6f5..0825da92118 100644 --- a/spec/frontend/snippets/components/snippet_header_spec.js +++ b/spec/frontend/snippets/components/snippet_header_spec.js @@ -3,6 +3,7 @@ import DeleteSnippetMutation from '~/snippets/mutations/deleteSnippet.mutation.g import { ApolloMutation } from 'vue-apollo'; import { GlButton, GlModal } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; +import { Blob, BinaryBlob } from 'jest/blob/components/mock_data'; describe('Snippet header component', () => { let wrapper; @@ -20,9 +21,7 @@ describe('Snippet header component', () => { author: { name: 'Thor Odinson', }, - blob: { - binary: false, - }, + blobs: [Blob], }; const mutationVariables = { mutation: DeleteSnippetMutation, @@ -49,7 +48,6 @@ describe('Snippet header component', () => { mutationRes = mutationTypes.RESOLVE, snippetProps = {}, } = {}) { - // const defaultProps = Object.assign({}, snippet, snippetProps); const defaultProps = Object.assign(snippet, snippetProps); if (permissions) { Object.assign(defaultProps.userPermissions, { @@ -131,15 +129,18 @@ describe('Snippet header component', () => { expect(wrapper.find(GlModal).exists()).toBe(true); }); - it('renders Edit button as disabled for binary snippets', () => { + it.each` + blobs | isDisabled | condition + ${[Blob]} | ${false} | ${'no binary'} + ${[Blob, BinaryBlob]} | ${true} | ${'several blobs. incl. a binary'} + ${[BinaryBlob]} | ${true} | ${'binary'} + `('renders Edit button when snippet contains $condition file', ({ blobs, isDisabled }) => { createComponent({ snippetProps: { - blob: { - binary: true, - }, + blobs, }, }); - expect(wrapper.find('[href*="edit"]').props('disabled')).toBe(true); + expect(wrapper.find('[href*="edit"]').props('disabled')).toBe(isDisabled); }); describe('Delete mutation', () => { -- cgit v1.2.3