Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'spec/frontend/design_management_new/pages')
-rw-r--r--spec/frontend/design_management_new/pages/__snapshots__/index_spec.js.snap317
-rw-r--r--spec/frontend/design_management_new/pages/design/__snapshots__/index_spec.js.snap216
-rw-r--r--spec/frontend/design_management_new/pages/design/index_spec.js294
-rw-r--r--spec/frontend/design_management_new/pages/index_spec.js571
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);
+ });
+ });
+});