diff options
Diffstat (limited to 'spec/frontend/design_management_new/pages')
4 files changed, 1398 insertions, 0 deletions
diff --git a/spec/frontend/design_management_new/pages/__snapshots__/index_spec.js.snap b/spec/frontend/design_management_new/pages/__snapshots__/index_spec.js.snap new file mode 100644 index 00000000000..3d1fe143ac3 --- /dev/null +++ b/spec/frontend/design_management_new/pages/__snapshots__/index_spec.js.snap @@ -0,0 +1,317 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Design management index page designs does not render toolbar when there is no permission 1`] = ` +<div + class="gl-mt-5" + data-testid="designs-root" +> + <!----> + + <div + class="mt-4" + > + <ol + class="list-unstyled row" + > + <!----> + + <li + class="gl-flex-direction-column col-md-6 col-lg-3 gl-mb-3" + data-testid="design-dropzone-wrapper" + > + <design-dropzone-stub + class="design-list-item design-list-item-new" + hasdesigns="true" + /> + </li> + + <li + class="col-md-6 col-lg-3 gl-mb-3" + > + <design-dropzone-stub + hasdesigns="true" + > + <design-stub + event="NONE" + filename="design-1-name" + id="design-1" + image="design-1-image" + notescount="0" + /> + </design-dropzone-stub> + + <!----> + </li> + <li + class="col-md-6 col-lg-3 gl-mb-3" + > + <design-dropzone-stub + hasdesigns="true" + > + <design-stub + event="NONE" + filename="design-2-name" + id="design-2" + image="design-2-image" + notescount="1" + /> + </design-dropzone-stub> + + <!----> + </li> + <li + class="col-md-6 col-lg-3 gl-mb-3" + > + <design-dropzone-stub + hasdesigns="true" + > + <design-stub + event="NONE" + filename="design-3-name" + id="design-3" + image="design-3-image" + notescount="0" + /> + </design-dropzone-stub> + + <!----> + </li> + </ol> + </div> + + <router-view-stub + name="default" + /> +</div> +`; + +exports[`Design management index page designs renders designs list and header with upload button 1`] = ` +<div + class="gl-mt-5" + data-testid="designs-root" +> + <header + class="row-content-block border-top-0 p-2 d-flex" + > + <div + class="gl-display-flex gl-justify-content-space-between gl-align-items-center gl-w-full" + > + <div> + <span + class="gl-font-weight-bold gl-mr-3" + > + Designs + </span> + + <design-version-dropdown-stub /> + </div> + + <div + class="qa-selector-toolbar gl-display-flex" + > + <gl-button-stub + category="tertiary" + class="gl-mr-2 js-select-all" + icon="" + size="small" + variant="link" + > + Select all + + </gl-button-stub> + + <div> + <delete-button-stub + buttonclass="gl-mr-4" + buttonsize="small" + buttonvariant="danger" + > + + Delete selected + + <!----> + </delete-button-stub> + </div> + + <upload-button-stub /> + </div> + </div> + </header> + + <div + class="mt-4" + > + <ol + class="list-unstyled row" + > + <!----> + + <li + class="gl-flex-direction-column col-md-6 col-lg-3 gl-mb-3" + data-testid="design-dropzone-wrapper" + > + <design-dropzone-stub + class="design-list-item design-list-item-new" + hasdesigns="true" + /> + </li> + + <li + class="col-md-6 col-lg-3 gl-mb-3" + > + <design-dropzone-stub + hasdesigns="true" + > + <design-stub + event="NONE" + filename="design-1-name" + id="design-1" + image="design-1-image" + notescount="0" + /> + </design-dropzone-stub> + + <input + class="design-checkbox" + type="checkbox" + /> + </li> + <li + class="col-md-6 col-lg-3 gl-mb-3" + > + <design-dropzone-stub + hasdesigns="true" + > + <design-stub + event="NONE" + filename="design-2-name" + id="design-2" + image="design-2-image" + notescount="1" + /> + </design-dropzone-stub> + + <input + class="design-checkbox" + type="checkbox" + /> + </li> + <li + class="col-md-6 col-lg-3 gl-mb-3" + > + <design-dropzone-stub + hasdesigns="true" + > + <design-stub + event="NONE" + filename="design-3-name" + id="design-3" + image="design-3-image" + notescount="0" + /> + </design-dropzone-stub> + + <input + class="design-checkbox" + type="checkbox" + /> + </li> + </ol> + </div> + + <router-view-stub + name="default" + /> +</div> +`; + +exports[`Design management index page designs renders error 1`] = ` +<div + class="gl-mt-5" + data-testid="designs-root" +> + <!----> + + <div + class="mt-4" + > + <gl-alert-stub + dismisslabel="Dismiss" + primarybuttonlink="" + primarybuttontext="" + secondarybuttonlink="" + secondarybuttontext="" + title="" + variant="danger" + > + + An error occurred while loading designs. Please try again. + + </gl-alert-stub> + </div> + + <router-view-stub + name="default" + /> +</div> +`; + +exports[`Design management index page designs renders loading icon 1`] = ` +<div + class="gl-mt-5" + data-testid="designs-root" +> + <!----> + + <div + class="mt-4" + > + <gl-loading-icon-stub + color="orange" + label="Loading" + size="md" + /> + </div> + + <router-view-stub + name="default" + /> +</div> +`; + +exports[`Design management index page when has no designs renders design dropzone 1`] = ` +<div + class="gl-mt-5" + data-testid="designs-root" +> + <!----> + + <div + class="mt-4" + > + <ol + class="list-unstyled row" + > + <span + class="gl-font-weight-bold gl-font-weight-bold gl-ml-5 gl-mb-4" + > + Designs + </span> + + <li + class="col-12" + data-testid="design-dropzone-wrapper" + > + <design-dropzone-stub + class="" + /> + </li> + + </ol> + </div> + + <router-view-stub + name="default" + /> +</div> +`; diff --git a/spec/frontend/design_management_new/pages/design/__snapshots__/index_spec.js.snap b/spec/frontend/design_management_new/pages/design/__snapshots__/index_spec.js.snap new file mode 100644 index 00000000000..83bcebd513e --- /dev/null +++ b/spec/frontend/design_management_new/pages/design/__snapshots__/index_spec.js.snap @@ -0,0 +1,216 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Design management design index page renders design index 1`] = ` +<div + class="design-detail js-design-detail fixed-top w-100 position-bottom-0 d-flex justify-content-center flex-column flex-lg-row" +> + <div + class="d-flex overflow-hidden flex-grow-1 flex-column position-relative" + > + <design-destroyer-stub + filenames="test.jpg" + iid="1" + project-path="project-path" + /> + + <!----> + + <design-presentation-stub + discussions="[object Object],[object Object]" + image="test.jpg" + imagename="test.jpg" + scale="1" + /> + + <div + class="design-scaler-wrapper position-absolute mb-4 d-flex-center" + > + <design-scaler-stub /> + </div> + </div> + + <div + class="image-notes" + > + <h2 + class="gl-font-weight-bold gl-mt-0" + > + + My precious issue + + </h2> + + <a + class="gl-text-gray-600 gl-text-decoration-none gl-mb-6 gl-display-block" + href="full-issue-url" + > + ull-issue-path + </a> + + <participants-stub + class="gl-mb-4" + numberoflessparticipants="7" + participants="[object Object]" + /> + + <!----> + + <design-discussion-stub + data-testid="unresolved-discussion" + designid="test" + discussion="[object Object]" + discussionwithopenform="" + markdownpreviewpath="/project-path/preview_markdown?target_type=Issue" + noteableid="design-id" + /> + + <gl-button-stub + category="tertiary" + class="link-inherit-color gl-text-black-normal gl-text-decoration-none gl-font-weight-bold gl-mb-4" + data-testid="resolved-comments" + icon="chevron-right" + id="resolved-comments" + size="medium" + variant="link" + > + Resolved Comments (1) + + </gl-button-stub> + + <gl-popover-stub + container="popovercontainer" + cssclasses="" + placement="top" + show="true" + target="resolved-comments" + title="Resolved Comments" + > + <p> + + Comments you resolve can be viewed and unresolved by going to the "Resolved Comments" section below + + </p> + + <a + href="#" + rel="noopener noreferrer" + target="_blank" + > + Learn more about resolving comments + </a> + </gl-popover-stub> + + <gl-collapse-stub + class="gl-mt-3" + > + <design-discussion-stub + data-testid="resolved-discussion" + designid="test" + discussion="[object Object]" + discussionwithopenform="" + markdownpreviewpath="/project-path/preview_markdown?target_type=Issue" + noteableid="design-id" + /> + </gl-collapse-stub> + + </div> +</div> +`; + +exports[`Design management design index page sets loading state 1`] = ` +<div + class="design-detail js-design-detail fixed-top w-100 position-bottom-0 d-flex justify-content-center flex-column flex-lg-row" +> + <gl-loading-icon-stub + class="align-self-center" + color="orange" + label="Loading" + size="xl" + /> +</div> +`; + +exports[`Design management design index page with error GlAlert is rendered in correct position with correct content 1`] = ` +<div + class="design-detail js-design-detail fixed-top w-100 position-bottom-0 d-flex justify-content-center flex-column flex-lg-row" +> + <div + class="d-flex overflow-hidden flex-grow-1 flex-column position-relative" + > + <design-destroyer-stub + filenames="test.jpg" + iid="1" + project-path="project-path" + /> + + <div + class="p-3" + > + <gl-alert-stub + dismissible="true" + dismisslabel="Dismiss" + primarybuttonlink="" + primarybuttontext="" + secondarybuttonlink="" + secondarybuttontext="" + title="" + variant="danger" + > + + woops + + </gl-alert-stub> + </div> + + <design-presentation-stub + discussions="" + image="test.jpg" + imagename="test.jpg" + scale="1" + /> + + <div + class="design-scaler-wrapper position-absolute mb-4 d-flex-center" + > + <design-scaler-stub /> + </div> + </div> + + <div + class="image-notes" + > + <h2 + class="gl-font-weight-bold gl-mt-0" + > + + My precious issue + + </h2> + + <a + class="gl-text-gray-600 gl-text-decoration-none gl-mb-6 gl-display-block" + href="full-issue-url" + > + ull-issue-path + </a> + + <participants-stub + class="gl-mb-4" + numberoflessparticipants="7" + participants="[object Object]" + /> + + <h2 + class="new-discussion-disclaimer gl-font-base gl-m-0 gl-mb-4" + data-testid="new-discussion-disclaimer" + > + + Click the image where you'd like to start a new discussion + + </h2> + + <!----> + + </div> +</div> +`; diff --git a/spec/frontend/design_management_new/pages/design/index_spec.js b/spec/frontend/design_management_new/pages/design/index_spec.js new file mode 100644 index 00000000000..3822b0b3b71 --- /dev/null +++ b/spec/frontend/design_management_new/pages/design/index_spec.js @@ -0,0 +1,294 @@ +import { shallowMount, createLocalVue } from '@vue/test-utils'; +import VueRouter from 'vue-router'; +import { GlAlert } from '@gitlab/ui'; +import { ApolloMutation } from 'vue-apollo'; +import createFlash from '~/flash'; +import DesignIndex from '~/design_management_new/pages/design/index.vue'; +import DesignSidebar from '~/design_management_new/components/design_sidebar.vue'; +import DesignPresentation from '~/design_management_new/components/design_presentation.vue'; +import createImageDiffNoteMutation from '~/design_management_new/graphql/mutations/create_image_diff_note.mutation.graphql'; +import design from '../../mock_data/design'; +import mockResponseWithDesigns from '../../mock_data/designs'; +import mockResponseNoDesigns from '../../mock_data/no_designs'; +import mockAllVersions from '../../mock_data/all_versions'; +import { + DESIGN_NOT_FOUND_ERROR, + DESIGN_VERSION_NOT_EXIST_ERROR, +} from '~/design_management_new/utils/error_messages'; +import { DESIGNS_ROUTE_NAME } from '~/design_management_new/router/constants'; +import createRouter from '~/design_management_new/router'; +import * as utils from '~/design_management_new/utils/design_management_utils'; +import { DESIGN_DETAIL_LAYOUT_CLASSLIST } from '~/design_management_new/constants'; + +jest.mock('~/flash'); +jest.mock('mousetrap', () => ({ + bind: jest.fn(), + unbind: jest.fn(), +})); + +const focusInput = jest.fn(); + +const DesignReplyForm = { + template: '<div><textarea ref="textarea"></textarea></div>', + methods: { + focusInput, + }, +}; + +const localVue = createLocalVue(); +localVue.use(VueRouter); + +describe('Design management design index page', () => { + let wrapper; + let router; + + const newComment = 'new comment'; + const annotationCoordinates = { + x: 10, + y: 10, + width: 100, + height: 100, + }; + const createDiscussionMutationVariables = { + mutation: createImageDiffNoteMutation, + update: expect.anything(), + variables: { + input: { + body: newComment, + noteableId: design.id, + position: { + headSha: 'headSha', + baseSha: 'baseSha', + startSha: 'startSha', + paths: { + newPath: 'full-design-path', + }, + ...annotationCoordinates, + }, + }, + }, + }; + + const mutate = jest.fn().mockResolvedValue(); + + const findDiscussionForm = () => wrapper.find(DesignReplyForm); + const findSidebar = () => wrapper.find(DesignSidebar); + const findDesignPresentation = () => wrapper.find(DesignPresentation); + + function createComponent(loading = false, data = {}) { + const $apollo = { + queries: { + design: { + loading, + }, + }, + mutate, + }; + + router = createRouter(); + + wrapper = shallowMount(DesignIndex, { + propsData: { id: '1' }, + mocks: { $apollo }, + stubs: { + ApolloMutation, + DesignSidebar, + DesignReplyForm, + }, + provide: { + issueIid: '1', + projectPath: 'project-path', + }, + data() { + return { + activeDiscussion: { + id: null, + source: null, + }, + ...data, + }; + }, + localVue, + router, + }); + } + + afterEach(() => { + wrapper.destroy(); + }); + + describe('when navigating', () => { + it('applies fullscreen layout', () => { + const mockEl = { + classList: { + add: jest.fn(), + remove: jest.fn(), + }, + }; + jest.spyOn(utils, 'getPageLayoutElement').mockReturnValue(mockEl); + createComponent(true); + + wrapper.vm.$router.push('/designs/test'); + expect(mockEl.classList.add).toHaveBeenCalledTimes(1); + expect(mockEl.classList.add).toHaveBeenCalledWith(...DESIGN_DETAIL_LAYOUT_CLASSLIST); + }); + }); + + it('sets loading state', () => { + createComponent(true); + + expect(wrapper.element).toMatchSnapshot(); + }); + + it('renders design index', () => { + createComponent(false, { design }); + + expect(wrapper.element).toMatchSnapshot(); + expect(wrapper.find(GlAlert).exists()).toBe(false); + }); + + it('passes correct props to sidebar component', () => { + createComponent(false, { design }); + + expect(findSidebar().props()).toEqual({ + design, + markdownPreviewPath: '/project-path/preview_markdown?target_type=Issue', + resolvedDiscussionsExpanded: false, + }); + }); + + it('opens a new discussion form', () => { + createComponent(false, { + design: { + ...design, + discussions: { + nodes: [], + }, + }, + }); + + findDesignPresentation().vm.$emit('openCommentForm', { x: 0, y: 0 }); + + return wrapper.vm.$nextTick().then(() => { + expect(findDiscussionForm().exists()).toBe(true); + }); + }); + + it('keeps new discussion form focused', () => { + createComponent(false, { + design: { + ...design, + discussions: { + nodes: [], + }, + }, + annotationCoordinates, + }); + + findDesignPresentation().vm.$emit('openCommentForm', { x: 10, y: 10 }); + + expect(focusInput).toHaveBeenCalled(); + }); + + it('sends a mutation on submitting form and closes form', () => { + createComponent(false, { + design: { + ...design, + discussions: { + nodes: [], + }, + }, + annotationCoordinates, + comment: newComment, + }); + + findDiscussionForm().vm.$emit('submitForm'); + expect(mutate).toHaveBeenCalledWith(createDiscussionMutationVariables); + + return wrapper.vm + .$nextTick() + .then(() => { + return mutate({ variables: createDiscussionMutationVariables }); + }) + .then(() => { + expect(findDiscussionForm().exists()).toBe(false); + }); + }); + + it('closes the form and clears the comment on canceling form', () => { + createComponent(false, { + design: { + ...design, + discussions: { + nodes: [], + }, + }, + annotationCoordinates, + comment: newComment, + }); + + findDiscussionForm().vm.$emit('cancelForm'); + + expect(wrapper.vm.comment).toBe(''); + + return wrapper.vm.$nextTick().then(() => { + expect(findDiscussionForm().exists()).toBe(false); + }); + }); + + describe('with error', () => { + beforeEach(() => { + createComponent(false, { + design: { + ...design, + discussions: { + nodes: [], + }, + }, + errorMessage: 'woops', + }); + }); + + it('GlAlert is rendered in correct position with correct content', () => { + expect(wrapper.element).toMatchSnapshot(); + }); + }); + + describe('onDesignQueryResult', () => { + describe('with no designs', () => { + it('redirects to /designs', () => { + createComponent(true); + router.push = jest.fn(); + + wrapper.vm.onDesignQueryResult({ data: mockResponseNoDesigns, loading: false }); + return wrapper.vm.$nextTick().then(() => { + expect(createFlash).toHaveBeenCalledTimes(1); + expect(createFlash).toHaveBeenCalledWith(DESIGN_NOT_FOUND_ERROR); + expect(router.push).toHaveBeenCalledTimes(1); + expect(router.push).toHaveBeenCalledWith({ name: DESIGNS_ROUTE_NAME }); + }); + }); + }); + + describe('when no design exists for given version', () => { + it('redirects to /designs', () => { + createComponent(true); + wrapper.setData({ + allVersions: mockAllVersions, + }); + + // attempt to query for a version of the design that doesn't exist + router.push({ query: { version: '999' } }); + router.push = jest.fn(); + + wrapper.vm.onDesignQueryResult({ data: mockResponseWithDesigns, loading: false }); + return wrapper.vm.$nextTick().then(() => { + expect(createFlash).toHaveBeenCalledTimes(1); + expect(createFlash).toHaveBeenCalledWith(DESIGN_VERSION_NOT_EXIST_ERROR); + expect(router.push).toHaveBeenCalledTimes(1); + expect(router.push).toHaveBeenCalledWith({ name: DESIGNS_ROUTE_NAME }); + }); + }); + }); + }); +}); diff --git a/spec/frontend/design_management_new/pages/index_spec.js b/spec/frontend/design_management_new/pages/index_spec.js new file mode 100644 index 00000000000..40a462eabb8 --- /dev/null +++ b/spec/frontend/design_management_new/pages/index_spec.js @@ -0,0 +1,571 @@ +import { createLocalVue, shallowMount } from '@vue/test-utils'; +import { ApolloMutation } from 'vue-apollo'; +import VueRouter from 'vue-router'; +import { GlEmptyState } from '@gitlab/ui'; +import Index from '~/design_management_new/pages/index.vue'; +import uploadDesignQuery from '~/design_management_new/graphql/mutations/upload_design.mutation.graphql'; +import DesignDestroyer from '~/design_management_new/components/design_destroyer.vue'; +import DesignDropzone from '~/design_management_new/components/upload/design_dropzone.vue'; +import DeleteButton from '~/design_management_new/components/delete_button.vue'; +import { DESIGNS_ROUTE_NAME } from '~/design_management_new/router/constants'; +import { + EXISTING_DESIGN_DROP_MANY_FILES_MESSAGE, + EXISTING_DESIGN_DROP_INVALID_FILENAME_MESSAGE, +} from '~/design_management_new/utils/error_messages'; +import createFlash from '~/flash'; +import createRouter from '~/design_management_new/router'; +import * as utils from '~/design_management_new/utils/design_management_utils'; +import { DESIGN_DETAIL_LAYOUT_CLASSLIST } from '~/design_management_new/constants'; + +jest.mock('~/flash.js'); +const mockPageEl = { + classList: { + remove: jest.fn(), + }, +}; +jest.spyOn(utils, 'getPageLayoutElement').mockReturnValue(mockPageEl); + +const localVue = createLocalVue(); +const router = createRouter(); +localVue.use(VueRouter); + +const mockDesigns = [ + { + id: 'design-1', + image: 'design-1-image', + filename: 'design-1-name', + event: 'NONE', + notesCount: 0, + }, + { + id: 'design-2', + image: 'design-2-image', + filename: 'design-2-name', + event: 'NONE', + notesCount: 1, + }, + { + id: 'design-3', + image: 'design-3-image', + filename: 'design-3-name', + event: 'NONE', + notesCount: 0, + }, +]; + +const mockVersion = { + node: { + id: 'gid://gitlab/DesignManagement::Version/1', + }, +}; + +describe('Design management index page', () => { + let mutate; + let wrapper; + + const findDesignCheckboxes = () => wrapper.findAll('.design-checkbox'); + const findSelectAllButton = () => wrapper.find('.js-select-all'); + const findToolbar = () => wrapper.find('.qa-selector-toolbar'); + const findDeleteButton = () => wrapper.find(DeleteButton); + const findDropzone = () => wrapper.findAll(DesignDropzone).at(0); + const dropzoneClasses = () => findDropzone().classes(); + const findDropzoneWrapper = () => wrapper.find('[data-testid="design-dropzone-wrapper"]'); + const findFirstDropzoneWithDesign = () => wrapper.findAll(DesignDropzone).at(1); + + function createComponent({ + loading = false, + designs = [], + allVersions = [], + createDesign = true, + stubs = {}, + mockMutate = jest.fn().mockResolvedValue(), + } = {}) { + mutate = mockMutate; + const $apollo = { + queries: { + designs: { + loading, + }, + permissions: { + loading, + }, + }, + mutate, + }; + + wrapper = shallowMount(Index, { + data() { + return { + designs, + allVersions, + permissions: { + createDesign, + }, + }; + }, + mocks: { $apollo }, + localVue, + router, + stubs: { DesignDestroyer, ApolloMutation, ...stubs }, + attachToDocument: true, + provide: { + projectPath: 'project-path', + issueIid: '1', + }, + }); + } + + afterEach(() => { + wrapper.destroy(); + }); + + describe('designs', () => { + it('renders loading icon', () => { + createComponent({ loading: true }); + + expect(wrapper.element).toMatchSnapshot(); + }); + + it('renders error', () => { + createComponent(); + + wrapper.setData({ error: true }); + + return wrapper.vm.$nextTick().then(() => { + expect(wrapper.element).toMatchSnapshot(); + }); + }); + + it('renders a toolbar with buttons when there are designs', () => { + createComponent({ designs: mockDesigns, allVersions: [mockVersion] }); + + expect(findToolbar().exists()).toBe(true); + }); + + it('renders designs list and header with upload button', () => { + createComponent({ designs: mockDesigns, allVersions: [mockVersion] }); + + expect(wrapper.element).toMatchSnapshot(); + }); + + it('does not render toolbar when there is no permission', () => { + createComponent({ designs: mockDesigns, allVersions: [mockVersion], createDesign: false }); + + expect(wrapper.element).toMatchSnapshot(); + }); + + it('has correct classes applied to design dropzone', () => { + createComponent({ designs: mockDesigns, allVersions: [mockVersion] }); + expect(dropzoneClasses()).toContain('design-list-item'); + expect(dropzoneClasses()).toContain('design-list-item-new'); + }); + + it('has correct classes applied to dropzone wrapper', () => { + createComponent({ designs: mockDesigns, allVersions: [mockVersion] }); + expect(findDropzoneWrapper().classes()).toEqual([ + 'gl-flex-direction-column', + 'col-md-6', + 'col-lg-3', + 'gl-mb-3', + ]); + }); + }); + + describe('when has no designs', () => { + beforeEach(() => { + createComponent(); + }); + + it('renders design dropzone', () => + wrapper.vm.$nextTick().then(() => { + expect(wrapper.element).toMatchSnapshot(); + })); + + it('has correct classes applied to design dropzone', () => { + expect(dropzoneClasses()).not.toContain('design-list-item'); + expect(dropzoneClasses()).not.toContain('design-list-item-new'); + }); + + it('has correct classes applied to dropzone wrapper', () => { + expect(findDropzoneWrapper().classes()).toEqual(['col-12']); + }); + + it('does not render a toolbar with buttons', () => + wrapper.vm.$nextTick().then(() => { + expect(findToolbar().exists()).toBe(false); + })); + }); + + describe('uploading designs', () => { + it('calls mutation on upload', () => { + createComponent({ stubs: { GlEmptyState } }); + + const mutationVariables = { + update: expect.anything(), + context: { + hasUpload: true, + }, + mutation: uploadDesignQuery, + variables: { + files: [{ name: 'test' }], + projectPath: 'project-path', + iid: '1', + }, + optimisticResponse: { + __typename: 'Mutation', + designManagementUpload: { + __typename: 'DesignManagementUploadPayload', + designs: [ + { + __typename: 'Design', + id: expect.anything(), + image: '', + imageV432x230: '', + filename: 'test', + fullPath: '', + event: 'NONE', + notesCount: 0, + diffRefs: { + __typename: 'DiffRefs', + baseSha: '', + startSha: '', + headSha: '', + }, + discussions: { + __typename: 'DesignDiscussion', + nodes: [], + }, + versions: { + __typename: 'DesignVersionConnection', + edges: { + __typename: 'DesignVersionEdge', + node: { + __typename: 'DesignVersion', + id: expect.anything(), + sha: expect.anything(), + }, + }, + }, + }, + ], + skippedDesigns: [], + errors: [], + }, + }, + }; + + return wrapper.vm + .$nextTick() + .then(() => { + findDropzone().vm.$emit('change', [{ name: 'test' }]); + expect(mutate).toHaveBeenCalledWith(mutationVariables); + expect(wrapper.vm.filesToBeSaved).toEqual([{ name: 'test' }]); + expect(wrapper.vm.isSaving).toBeTruthy(); + }) + .then(() => { + expect(dropzoneClasses()).toContain('design-list-item'); + expect(dropzoneClasses()).toContain('design-list-item-new'); + }); + }); + + it('sets isSaving', () => { + createComponent(); + + const uploadDesign = wrapper.vm.onUploadDesign([ + { + name: 'test', + }, + ]); + + expect(wrapper.vm.isSaving).toBe(true); + + return uploadDesign.then(() => { + expect(wrapper.vm.isSaving).toBe(false); + }); + }); + + it('updates state appropriately after upload complete', () => { + createComponent({ stubs: { GlEmptyState } }); + wrapper.setData({ filesToBeSaved: [{ name: 'test' }] }); + + wrapper.vm.onUploadDesignDone(); + return wrapper.vm.$nextTick().then(() => { + expect(wrapper.vm.filesToBeSaved).toEqual([]); + expect(wrapper.vm.isSaving).toBeFalsy(); + expect(wrapper.vm.isLatestVersion).toBe(true); + }); + }); + + it('updates state appropriately after upload error', () => { + createComponent({ stubs: { GlEmptyState } }); + wrapper.setData({ filesToBeSaved: [{ name: 'test' }] }); + + wrapper.vm.onUploadDesignError(); + return wrapper.vm.$nextTick().then(() => { + expect(wrapper.vm.filesToBeSaved).toEqual([]); + expect(wrapper.vm.isSaving).toBeFalsy(); + expect(createFlash).toHaveBeenCalled(); + + createFlash.mockReset(); + }); + }); + + it('does not call mutation if createDesign is false', () => { + createComponent({ createDesign: false }); + + wrapper.vm.onUploadDesign([]); + + expect(mutate).not.toHaveBeenCalled(); + }); + + describe('upload count limit', () => { + const MAXIMUM_FILE_UPLOAD_LIMIT = 10; + + afterEach(() => { + createFlash.mockReset(); + }); + + it('does not warn when the max files are uploaded', () => { + createComponent(); + + wrapper.vm.onUploadDesign(new Array(MAXIMUM_FILE_UPLOAD_LIMIT).fill(mockDesigns[0])); + + expect(createFlash).not.toHaveBeenCalled(); + }); + + it('warns when too many files are uploaded', () => { + createComponent(); + + wrapper.vm.onUploadDesign(new Array(MAXIMUM_FILE_UPLOAD_LIMIT + 1).fill(mockDesigns[0])); + + expect(createFlash).toHaveBeenCalled(); + }); + }); + + it('flashes warning if designs are skipped', () => { + createComponent({ + mockMutate: () => + Promise.resolve({ + data: { designManagementUpload: { skippedDesigns: [{ filename: 'test.jpg' }] } }, + }), + }); + + const uploadDesign = wrapper.vm.onUploadDesign([ + { + name: 'test', + }, + ]); + + return uploadDesign.then(() => { + expect(createFlash).toHaveBeenCalledTimes(1); + expect(createFlash).toHaveBeenCalledWith( + 'Upload skipped. test.jpg did not change.', + 'warning', + ); + }); + }); + + describe('dragging onto an existing design', () => { + beforeEach(() => { + createComponent({ designs: mockDesigns, allVersions: [mockVersion] }); + }); + + it('calls onUploadDesign with valid upload', () => { + wrapper.setMethods({ + onUploadDesign: jest.fn(), + }); + + const mockUploadPayload = [ + { + name: mockDesigns[0].filename, + }, + ]; + + const designDropzone = findFirstDropzoneWithDesign(); + designDropzone.vm.$emit('change', mockUploadPayload); + + expect(wrapper.vm.onUploadDesign).toHaveBeenCalledTimes(1); + expect(wrapper.vm.onUploadDesign).toHaveBeenCalledWith(mockUploadPayload); + }); + + it.each` + description | eventPayload | message + ${'> 1 file'} | ${[{ name: 'test' }, { name: 'test-2' }]} | ${EXISTING_DESIGN_DROP_MANY_FILES_MESSAGE} + ${'different filename'} | ${[{ name: 'wrong-name' }]} | ${EXISTING_DESIGN_DROP_INVALID_FILENAME_MESSAGE} + `('calls createFlash when upload has $description', ({ eventPayload, message }) => { + const designDropzone = findFirstDropzoneWithDesign(); + designDropzone.vm.$emit('change', eventPayload); + + expect(createFlash).toHaveBeenCalledTimes(1); + expect(createFlash).toHaveBeenCalledWith(message); + }); + }); + }); + + describe('on latest version when has designs', () => { + beforeEach(() => { + createComponent({ designs: mockDesigns, allVersions: [mockVersion] }); + }); + + it('renders design checkboxes', () => { + expect(findDesignCheckboxes()).toHaveLength(mockDesigns.length); + }); + + it('renders toolbar buttons', () => { + expect(findToolbar().exists()).toBe(true); + expect(findToolbar().isVisible()).toBe(true); + }); + + it('adds two designs to selected designs when their checkboxes are checked', () => { + findDesignCheckboxes() + .at(0) + .trigger('click'); + + return wrapper.vm + .$nextTick() + .then(() => { + findDesignCheckboxes() + .at(1) + .trigger('click'); + + return wrapper.vm.$nextTick(); + }) + .then(() => { + expect(findDeleteButton().exists()).toBe(true); + expect(findSelectAllButton().text()).toBe('Deselect all'); + findDeleteButton().vm.$emit('deleteSelectedDesigns'); + const [{ variables }] = mutate.mock.calls[0]; + expect(variables.filenames).toStrictEqual([ + mockDesigns[0].filename, + mockDesigns[1].filename, + ]); + }); + }); + + it('adds all designs to selected designs when Select All button is clicked', () => { + findSelectAllButton().vm.$emit('click'); + + return wrapper.vm.$nextTick().then(() => { + expect(findDeleteButton().props().hasSelectedDesigns).toBe(true); + expect(findSelectAllButton().text()).toBe('Deselect all'); + expect(wrapper.vm.selectedDesigns).toEqual(mockDesigns.map(design => design.filename)); + }); + }); + + it('removes all designs from selected designs when at least one design was selected', () => { + findDesignCheckboxes() + .at(0) + .trigger('click'); + + return wrapper.vm + .$nextTick() + .then(() => { + findSelectAllButton().vm.$emit('click'); + }) + .then(() => { + expect(findDeleteButton().props().hasSelectedDesigns).toBe(false); + expect(findSelectAllButton().text()).toBe('Select all'); + expect(wrapper.vm.selectedDesigns).toEqual([]); + }); + }); + }); + + it('on latest version when has no designs toolbar buttons are invisible', () => { + createComponent({ designs: [], allVersions: [mockVersion] }); + expect(findToolbar().isVisible()).toBe(false); + }); + + describe('on non-latest version', () => { + beforeEach(() => { + createComponent({ designs: mockDesigns, allVersions: [mockVersion] }); + + router.replace({ + name: DESIGNS_ROUTE_NAME, + query: { + version: '2', + }, + }); + }); + + it('does not render design checkboxes', () => { + expect(findDesignCheckboxes()).toHaveLength(0); + }); + + it('does not render Delete selected button', () => { + expect(findDeleteButton().exists()).toBe(false); + }); + + it('does not render Select All button', () => { + expect(findSelectAllButton().exists()).toBe(false); + }); + }); + + describe('pasting a design', () => { + let event; + beforeEach(() => { + createComponent({ designs: mockDesigns, allVersions: [mockVersion] }); + + wrapper.setMethods({ + onUploadDesign: jest.fn(), + }); + + event = new Event('paste'); + + router.replace({ + name: DESIGNS_ROUTE_NAME, + query: { + version: '2', + }, + }); + }); + + it('calls onUploadDesign with valid paste', () => { + event.clipboardData = { + files: [{ name: 'image.png', type: 'image/png' }], + getData: () => 'test.png', + }; + + document.dispatchEvent(event); + + expect(wrapper.vm.onUploadDesign).toHaveBeenCalledTimes(1); + expect(wrapper.vm.onUploadDesign).toHaveBeenCalledWith([ + new File([{ name: 'image.png' }], 'test.png'), + ]); + }); + + it('renames a design if it has an image.png filename', () => { + event.clipboardData = { + files: [{ name: 'image.png', type: 'image/png' }], + getData: () => 'image.png', + }; + + document.dispatchEvent(event); + + expect(wrapper.vm.onUploadDesign).toHaveBeenCalledTimes(1); + expect(wrapper.vm.onUploadDesign).toHaveBeenCalledWith([ + new File([{ name: 'image.png' }], `design_${Date.now()}.png`), + ]); + }); + + it('does not call onUploadDesign with invalid paste', () => { + event.clipboardData = { + items: [{ type: 'text/plain' }, { type: 'text' }], + files: [], + }; + + document.dispatchEvent(event); + + expect(wrapper.vm.onUploadDesign).not.toHaveBeenCalled(); + }); + }); + + describe('when navigating', () => { + it('ensures fullscreen layout is not applied', () => { + createComponent(true); + + wrapper.vm.$router.push('/'); + expect(mockPageEl.classList.remove).toHaveBeenCalledTimes(1); + expect(mockPageEl.classList.remove).toHaveBeenCalledWith(...DESIGN_DETAIL_LAYOUT_CLASSLIST); + }); + }); +}); |