diff options
Diffstat (limited to 'spec/frontend/design_management')
26 files changed, 713 insertions, 429 deletions
diff --git a/spec/frontend/design_management/components/delete_button_spec.js b/spec/frontend/design_management/components/delete_button_spec.js index cd4ef1f0ccd..961f5bdd2ae 100644 --- a/spec/frontend/design_management/components/delete_button_spec.js +++ b/spec/frontend/design_management/components/delete_button_spec.js @@ -8,7 +8,7 @@ describe('Batch delete button component', () => { const findButton = () => wrapper.find(GlButton); const findModal = () => wrapper.find(GlModal); - function createComponent(isDeleting = false) { + function createComponent({ isDeleting = false } = {}, { slots = {} } = {}) { wrapper = shallowMount(BatchDeleteButton, { propsData: { isDeleting, @@ -16,6 +16,7 @@ describe('Batch delete button component', () => { directives: { GlModalDirective, }, + slots, }); } @@ -31,7 +32,7 @@ describe('Batch delete button component', () => { }); it('renders disabled button when design is deleting', () => { - createComponent(true); + createComponent({ isDeleting: true }); expect(findButton().attributes('disabled')).toBeTruthy(); }); @@ -48,4 +49,18 @@ describe('Batch delete button component', () => { expect(wrapper.emitted().deleteSelectedDesigns).toBeTruthy(); }); }); + + it('renders slot content', () => { + const testText = 'Archive selected'; + createComponent( + {}, + { + slots: { + default: testText, + }, + }, + ); + + expect(findButton().text()).toBe(testText); + }); }); diff --git a/spec/frontend/design_management/components/design_notes/__snapshots__/design_note_spec.js.snap b/spec/frontend/design_management/components/design_notes/__snapshots__/design_note_spec.js.snap index b55bacb6fc5..084a7e5d712 100644 --- a/spec/frontend/design_management/components/design_notes/__snapshots__/design_note_spec.js.snap +++ b/spec/frontend/design_management/components/design_notes/__snapshots__/design_note_spec.js.snap @@ -17,15 +17,15 @@ exports[`Design note component should match the snapshot 1`] = ` /> <div - class="d-flex justify-content-between" + class="gl-display-flex gl-justify-content-space-between" > <div> - <a + <gl-link-stub class="js-user-link" data-user-id="author-id" > <span - class="note-header-author-name bold" + class="note-header-author-name gl-font-weight-bold" > </span> @@ -37,7 +37,7 @@ exports[`Design note component should match the snapshot 1`] = ` > @ </span> - </a> + </gl-link-stub> <span class="note-headline-light note-headline-meta" @@ -46,12 +46,21 @@ exports[`Design note component should match the snapshot 1`] = ` class="system-note-message" /> - <!----> + <gl-link-stub + class="note-timestamp system-note-separator gl-display-block gl-mb-2" + href="#note_123" + > + <time-ago-tooltip-stub + cssclass="" + time="2019-07-26T15:02:20Z" + tooltipplacement="bottom" + /> + </gl-link-stub> </span> </div> <div - class="gl-display-flex" + class="gl-display-flex gl-align-items-baseline" > <!----> diff --git a/spec/frontend/design_management/components/design_notes/__snapshots__/design_reply_form_spec.js.snap b/spec/frontend/design_management/components/design_notes/__snapshots__/design_reply_form_spec.js.snap index e01c79e3520..f8c68ca4c83 100644 --- a/spec/frontend/design_management/components/design_notes/__snapshots__/design_reply_form_spec.js.snap +++ b/spec/frontend/design_management/components/design_notes/__snapshots__/design_reply_form_spec.js.snap @@ -1,15 +1,17 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Design reply form component renders button text as "Comment" when creating a comment 1`] = ` -"<button data-track-event=\\"click_button\\" data-qa-selector=\\"save_comment_button\\" type=\\"submit\\" disabled=\\"disabled\\" class=\\"btn btn-success btn-md disabled\\"> +"<button data-track-event=\\"click_button\\" data-qa-selector=\\"save_comment_button\\" type=\\"submit\\" disabled=\\"disabled\\" class=\\"btn btn-success btn-md disabled gl-button\\"> <!----> - Comment -</button>" + <!----> <span class=\\"gl-button-text\\"> + Comment + </span></button>" `; exports[`Design reply form component renders button text as "Save comment" when creating a comment 1`] = ` -"<button data-track-event=\\"click_button\\" data-qa-selector=\\"save_comment_button\\" type=\\"submit\\" disabled=\\"disabled\\" class=\\"btn btn-success btn-md disabled\\"> +"<button data-track-event=\\"click_button\\" data-qa-selector=\\"save_comment_button\\" type=\\"submit\\" disabled=\\"disabled\\" class=\\"btn btn-success btn-md disabled gl-button\\"> <!----> - Save comment -</button>" + <!----> <span class=\\"gl-button-text\\"> + Save comment + </span></button>" `; diff --git a/spec/frontend/design_management/components/design_notes/design_discussion_spec.js b/spec/frontend/design_management/components/design_notes/design_discussion_spec.js index 176c10ea584..9fbd9b2c2a3 100644 --- a/spec/frontend/design_management/components/design_notes/design_discussion_spec.js +++ b/spec/frontend/design_management/components/design_notes/design_discussion_spec.js @@ -8,8 +8,9 @@ import createNoteMutation from '~/design_management/graphql/mutations/create_not import toggleResolveDiscussionMutation from '~/design_management/graphql/mutations/toggle_resolve_discussion.mutation.graphql'; import ReplyPlaceholder from '~/notes/components/discussion_reply_placeholder.vue'; import ToggleRepliesWidget from '~/design_management/components/design_notes/toggle_replies_widget.vue'; +import mockDiscussion from '../../mock_data/discussion'; -const discussion = { +const defaultMockDiscussion = { id: '0', resolved: false, resolvable: true, @@ -31,7 +32,6 @@ describe('Design discussions component', () => { const mutationVariables = { mutation: createNoteMutation, - update: expect.anything(), variables: { input: { noteableId: 'noteable-id', @@ -40,7 +40,7 @@ describe('Design discussions component', () => { }, }, }; - const mutate = jest.fn(() => Promise.resolve()); + const mutate = jest.fn().mockResolvedValue({ data: { createNote: { errors: [] } } }); const $apollo = { mutate, }; @@ -49,7 +49,7 @@ describe('Design discussions component', () => { wrapper = mount(DesignDiscussion, { propsData: { resolvedDiscussionsExpanded: true, - discussion, + discussion: defaultMockDiscussion, noteableId: 'noteable-id', designId: 'design-id', discussionIndex: 1, @@ -82,7 +82,7 @@ describe('Design discussions component', () => { beforeEach(() => { createComponent({ discussion: { - ...discussion, + ...defaultMockDiscussion, resolvable: false, }, }); @@ -93,7 +93,7 @@ describe('Design discussions component', () => { }); it('does not render a checkbox in reply form', () => { - findReplyPlaceholder().vm.$emit('onMouseDown'); + findReplyPlaceholder().vm.$emit('onClick'); return wrapper.vm.$nextTick().then(() => { expect(findResolveCheckbox().exists()).toBe(false); @@ -125,7 +125,7 @@ describe('Design discussions component', () => { it('renders a checkbox with Resolve thread text in reply form', () => { findReplyPlaceholder().vm.$emit('onClick'); - wrapper.setProps({ discussionWithOpenForm: discussion.id }); + wrapper.setProps({ discussionWithOpenForm: defaultMockDiscussion.id }); return wrapper.vm.$nextTick().then(() => { expect(findResolveCheckbox().text()).toBe('Resolve thread'); @@ -141,7 +141,7 @@ describe('Design discussions component', () => { beforeEach(() => { createComponent({ discussion: { - ...discussion, + ...defaultMockDiscussion, resolved: true, resolvedBy: notes[0].author, resolvedAt: '2020-05-08T07:10:45Z', @@ -206,7 +206,7 @@ describe('Design discussions component', () => { it('renders a checkbox with Unresolve thread text in reply form', () => { findReplyPlaceholder().vm.$emit('onClick'); - wrapper.setProps({ discussionWithOpenForm: discussion.id }); + wrapper.setProps({ discussionWithOpenForm: defaultMockDiscussion.id }); return wrapper.vm.$nextTick().then(() => { expect(findResolveCheckbox().text()).toBe('Unresolve thread'); @@ -218,7 +218,7 @@ describe('Design discussions component', () => { it('hides reply placeholder and opens form on placeholder click', () => { createComponent(); findReplyPlaceholder().vm.$emit('onClick'); - wrapper.setProps({ discussionWithOpenForm: discussion.id }); + wrapper.setProps({ discussionWithOpenForm: defaultMockDiscussion.id }); return wrapper.vm.$nextTick().then(() => { expect(findReplyPlaceholder().exists()).toBe(false); @@ -226,34 +226,31 @@ describe('Design discussions component', () => { }); }); - it('calls mutation on submitting form and closes the form', () => { + it('calls mutation on submitting form and closes the form', async () => { createComponent( - { discussionWithOpenForm: discussion.id }, + { discussionWithOpenForm: defaultMockDiscussion.id }, { discussionComment: 'test', isFormRendered: true }, ); - findReplyForm().vm.$emit('submitForm'); + findReplyForm().vm.$emit('submit-form'); expect(mutate).toHaveBeenCalledWith(mutationVariables); - return mutate() - .then(() => { - return wrapper.vm.$nextTick(); - }) - .then(() => { - expect(findReplyForm().exists()).toBe(false); - }); + await mutate(); + await wrapper.vm.$nextTick(); + + expect(findReplyForm().exists()).toBe(false); }); it('clears the discussion comment on closing comment form', () => { createComponent( - { discussionWithOpenForm: discussion.id }, + { discussionWithOpenForm: defaultMockDiscussion.id }, { discussionComment: 'test', isFormRendered: true }, ); return wrapper.vm .$nextTick() .then(() => { - findReplyForm().vm.$emit('cancelForm'); + findReplyForm().vm.$emit('cancel-form'); expect(wrapper.vm.discussionComment).toBe(''); return wrapper.vm.$nextTick(); @@ -263,19 +260,26 @@ describe('Design discussions component', () => { }); }); - it('applies correct class to design notes when discussion is highlighted', () => { - createComponent( - {}, - { - activeDiscussion: { - id: notes[0].id, - source: 'pin', - }, - }, - ); + describe('when any note from a discussion is active', () => { + it.each([notes[0], notes[0].discussion.notes.nodes[1]])( + 'applies correct class to all notes in the active discussion', + note => { + createComponent( + { discussion: mockDiscussion }, + { + activeDiscussion: { + id: note.id, + source: 'pin', + }, + }, + ); - expect(wrapper.findAll(DesignNote).wrappers.every(note => note.classes('gl-bg-blue-50'))).toBe( - true, + expect( + wrapper + .findAll(DesignNote) + .wrappers.every(designNote => designNote.classes('gl-bg-blue-50')), + ).toBe(true); + }, ); }); @@ -285,7 +289,7 @@ describe('Design discussions component', () => { expect(mutate).toHaveBeenCalledWith({ mutation: toggleResolveDiscussionMutation, variables: { - id: discussion.id, + id: defaultMockDiscussion.id, resolve: true, }, }); @@ -296,7 +300,7 @@ describe('Design discussions component', () => { it('calls toggleResolveDiscussion mutation after adding a note if checkbox was checked', () => { createComponent( - { discussionWithOpenForm: discussion.id }, + { discussionWithOpenForm: defaultMockDiscussion.id }, { discussionComment: 'test', isFormRendered: true }, ); findResolveButton().trigger('click'); @@ -306,7 +310,7 @@ describe('Design discussions component', () => { expect(mutate).toHaveBeenCalledWith({ mutation: toggleResolveDiscussionMutation, variables: { - id: discussion.id, + id: defaultMockDiscussion.id, resolve: true, }, }); @@ -317,6 +321,6 @@ describe('Design discussions component', () => { createComponent(); findReplyPlaceholder().vm.$emit('onClick'); - expect(wrapper.emitted('openForm')).toBeTruthy(); + expect(wrapper.emitted('open-form')).toBeTruthy(); }); }); diff --git a/spec/frontend/design_management/components/design_notes/design_note_spec.js b/spec/frontend/design_management/components/design_notes/design_note_spec.js index 8b32d3022ee..043091e3dc2 100644 --- a/spec/frontend/design_management/components/design_notes/design_note_spec.js +++ b/spec/frontend/design_management/components/design_notes/design_note_spec.js @@ -15,6 +15,7 @@ const note = { userPermissions: { adminNote: false, }, + createdAt: '2019-07-26T15:02:20Z', }; HTMLElement.prototype.scrollIntoView = scrollIntoViewMock; @@ -79,21 +80,10 @@ describe('Design note component', () => { it('should render a time ago tooltip if note has createdAt property', () => { createComponent({ - note: { - ...note, - createdAt: '2019-07-26T15:02:20Z', - }, - }); - - expect(wrapper.find(TimeAgoTooltip).exists()).toBe(true); - }); - - it('should trigger a scrollIntoView method', () => { - createComponent({ note, }); - expect(scrollIntoViewMock).toHaveBeenCalled(); + expect(wrapper.find(TimeAgoTooltip).exists()).toBe(true); }); it('should not render edit icon when user does not have a permission', () => { @@ -143,8 +133,8 @@ describe('Design note component', () => { expect(findReplyForm().exists()).toBe(true); }); - it('hides the form on hideForm event', () => { - findReplyForm().vm.$emit('cancelForm'); + it('hides the form on cancel-form event', () => { + findReplyForm().vm.$emit('cancel-form'); return wrapper.vm.$nextTick().then(() => { expect(findReplyForm().exists()).toBe(false); @@ -152,8 +142,8 @@ describe('Design note component', () => { }); }); - it('calls a mutation on submitForm event and hides a form', () => { - findReplyForm().vm.$emit('submitForm'); + it('calls a mutation on submit-form event and hides a form', () => { + findReplyForm().vm.$emit('submit-form'); expect(mutate).toHaveBeenCalled(); return mutate() diff --git a/spec/frontend/design_management/components/design_notes/design_reply_form_spec.js b/spec/frontend/design_management/components/design_notes/design_reply_form_spec.js index 16b34f150b8..1a80fc4e761 100644 --- a/spec/frontend/design_management/components/design_notes/design_reply_form_spec.js +++ b/spec/frontend/design_management/components/design_notes/design_reply_form_spec.js @@ -70,7 +70,7 @@ describe('Design reply form component', () => { }); return wrapper.vm.$nextTick().then(() => { - expect(wrapper.emitted('submitForm')).toBeFalsy(); + expect(wrapper.emitted('submit-form')).toBeFalsy(); }); }); @@ -80,20 +80,20 @@ describe('Design reply form component', () => { }); return wrapper.vm.$nextTick().then(() => { - expect(wrapper.emitted('submitForm')).toBeFalsy(); + expect(wrapper.emitted('submit-form')).toBeFalsy(); }); }); it('emits cancelForm event on pressing escape button on textarea', () => { findTextarea().trigger('keyup.esc'); - expect(wrapper.emitted('cancelForm')).toBeTruthy(); + expect(wrapper.emitted('cancel-form')).toBeTruthy(); }); it('emits cancelForm event on clicking Cancel button', () => { findCancelButton().vm.$emit('click'); - expect(wrapper.emitted('cancelForm')).toHaveLength(1); + expect(wrapper.emitted('cancel-form')).toHaveLength(1); }); }); @@ -112,7 +112,7 @@ describe('Design reply form component', () => { findSubmitButton().vm.$emit('click'); return wrapper.vm.$nextTick().then(() => { - expect(wrapper.emitted('submitForm')).toBeTruthy(); + expect(wrapper.emitted('submit-form')).toBeTruthy(); }); }); @@ -122,7 +122,7 @@ describe('Design reply form component', () => { }); return wrapper.vm.$nextTick().then(() => { - expect(wrapper.emitted('submitForm')).toBeTruthy(); + expect(wrapper.emitted('submit-form')).toBeTruthy(); }); }); @@ -132,7 +132,7 @@ describe('Design reply form component', () => { }); return wrapper.vm.$nextTick().then(() => { - expect(wrapper.emitted('submitForm')).toBeTruthy(); + expect(wrapper.emitted('submit-form')).toBeTruthy(); }); }); @@ -147,7 +147,7 @@ describe('Design reply form component', () => { it('emits cancelForm event on Escape key if text was not changed', () => { findTextarea().trigger('keyup.esc'); - expect(wrapper.emitted('cancelForm')).toBeTruthy(); + expect(wrapper.emitted('cancel-form')).toBeTruthy(); }); it('opens confirmation modal on Escape key when text has changed', () => { @@ -162,7 +162,7 @@ describe('Design reply form component', () => { it('emits cancelForm event on Cancel button click if text was not changed', () => { findCancelButton().trigger('click'); - expect(wrapper.emitted('cancelForm')).toBeTruthy(); + expect(wrapper.emitted('cancel-form')).toBeTruthy(); }); it('opens confirmation modal on Cancel button click when text has changed', () => { @@ -178,7 +178,7 @@ describe('Design reply form component', () => { findTextarea().trigger('keyup.esc'); findModal().vm.$emit('ok'); - expect(wrapper.emitted('cancelForm')).toBeTruthy(); + expect(wrapper.emitted('cancel-form')).toBeTruthy(); }); }); }); diff --git a/spec/frontend/design_management/components/design_overlay_spec.js b/spec/frontend/design_management/components/design_overlay_spec.js index f243323b162..673a09320e5 100644 --- a/spec/frontend/design_management/components/design_overlay_spec.js +++ b/spec/frontend/design_management/components/design_overlay_spec.js @@ -11,11 +11,11 @@ describe('Design overlay component', () => { const mockDimensions = { width: 100, height: 100 }; - const findOverlay = () => wrapper.find('.image-diff-overlay'); const findAllNotes = () => wrapper.findAll('.js-image-badge'); const findCommentBadge = () => wrapper.find('.comment-indicator'); - const findFirstBadge = () => findAllNotes().at(0); - const findSecondBadge = () => findAllNotes().at(1); + const findBadgeAtIndex = noteIndex => findAllNotes().at(noteIndex); + const findFirstBadge = () => findBadgeAtIndex(0); + const findSecondBadge = () => findBadgeAtIndex(1); const clickAndDragBadge = (elem, fromPoint, toPoint) => { elem.trigger('mousedown', { clientX: fromPoint.x, clientY: fromPoint.y }); @@ -56,9 +56,7 @@ describe('Design overlay component', () => { it('should have correct inline style', () => { createComponent(); - expect(wrapper.find('.image-diff-overlay').attributes().style).toBe( - 'width: 100px; height: 100px; top: 0px; left: 0px;', - ); + expect(wrapper.attributes().style).toBe('width: 100px; height: 100px; top: 0px; left: 0px;'); }); it('should emit `openCommentForm` when clicking on overlay', () => { @@ -69,7 +67,7 @@ describe('Design overlay component', () => { }; wrapper - .find('.image-diff-overlay-add-comment') + .find('[data-qa-selector="design_image_button"]') .trigger('mouseup', { offsetX: newCoordinates.x, offsetY: newCoordinates.y }); return wrapper.vm.$nextTick().then(() => { expect(wrapper.emitted('openCommentForm')).toEqual([ @@ -107,16 +105,43 @@ describe('Design overlay component', () => { expect(findSecondBadge().classes()).toContain('resolved'); }); - it('when there is an active discussion, should apply inactive class to all pins besides the active one', () => { - wrapper.setData({ - activeDiscussion: { - id: notes[0].id, - source: 'discussion', - }, + describe('when no discussion is active', () => { + it('should not apply inactive class to any pins', () => { + expect( + findAllNotes(0).wrappers.every(designNote => designNote.classes('gl-bg-blue-50')), + ).toBe(false); }); + }); + + describe('when a discussion is active', () => { + it.each([notes[0].discussion.notes.nodes[1], notes[0].discussion.notes.nodes[0]])( + 'should not apply inactive class to the pin for the active discussion', + note => { + wrapper.setData({ + activeDiscussion: { + id: note.id, + source: 'discussion', + }, + }); + + return wrapper.vm.$nextTick().then(() => { + expect(findBadgeAtIndex(0).classes()).not.toContain('inactive'); + }); + }, + ); + + it('should apply inactive class to all pins besides the active one', () => { + wrapper.setData({ + activeDiscussion: { + id: notes[0].id, + source: 'discussion', + }, + }); - return wrapper.vm.$nextTick().then(() => { - expect(findSecondBadge().classes()).toContain('inactive'); + return wrapper.vm.$nextTick().then(() => { + expect(findSecondBadge().classes()).toContain('inactive'); + expect(findFirstBadge().classes()).not.toContain('inactive'); + }); }); }); }); @@ -309,7 +334,7 @@ describe('Design overlay component', () => { it.each` element | getElementFunc | event - ${'overlay'} | ${findOverlay} | ${'mouseleave'} + ${'overlay'} | ${() => wrapper} | ${'mouseleave'} ${'comment badge'} | ${findCommentBadge} | ${'mouseup'} `( 'should emit `openCommentForm` event when $event fired on $element element', diff --git a/spec/frontend/design_management/components/design_presentation_spec.js b/spec/frontend/design_management/components/design_presentation_spec.js index 7e513182589..d633d00f2ed 100644 --- a/spec/frontend/design_management/components/design_presentation_spec.js +++ b/spec/frontend/design_management/components/design_presentation_spec.js @@ -42,7 +42,7 @@ describe('Design management design presentation component', () => { wrapper.element.scrollTo = jest.fn(); } - const findOverlayCommentButton = () => wrapper.find('.image-diff-overlay-add-comment'); + const findOverlayCommentButton = () => wrapper.find('[data-qa-selector="design_image_button"]'); /** * Spy on $refs and mock given values diff --git a/spec/frontend/design_management/components/design_sidebar_spec.js b/spec/frontend/design_management/components/design_sidebar_spec.js index e098e7de867..700faa8a70f 100644 --- a/spec/frontend/design_management/components/design_sidebar_spec.js +++ b/spec/frontend/design_management/components/design_sidebar_spec.js @@ -6,6 +6,10 @@ import Participants from '~/sidebar/components/participants/participants.vue'; import DesignDiscussion from '~/design_management/components/design_notes/design_discussion.vue'; import design from '../mock_data/design'; import updateActiveDiscussionMutation from '~/design_management/graphql/mutations/update_active_discussion.mutation.graphql'; +import DesignTodoButton from '~/design_management/components/design_todo_button.vue'; + +const scrollIntoViewMock = jest.fn(); +HTMLElement.prototype.scrollIntoView = scrollIntoViewMock; const updateActiveDiscussionMutationVariables = { mutation: updateActiveDiscussionMutation, @@ -39,7 +43,7 @@ describe('Design management design sidebar component', () => { const findNewDiscussionDisclaimer = () => wrapper.find('[data-testid="new-discussion-disclaimer"]'); - function createComponent(props = {}) { + function createComponent(props = {}, { enableTodoButton } = {}) { wrapper = shallowMount(DesignSidebar, { propsData: { design, @@ -53,6 +57,10 @@ describe('Design management design sidebar component', () => { mutate, }, }, + stubs: { GlPopover }, + provide: { + glFeatures: { designManagementTodoButton: enableTodoButton }, + }, }); } @@ -146,22 +154,22 @@ describe('Design management design sidebar component', () => { }); it('emits correct event on discussion create note error', () => { - findFirstDiscussion().vm.$emit('createNoteError', 'payload'); + findFirstDiscussion().vm.$emit('create-note-error', 'payload'); expect(wrapper.emitted('onDesignDiscussionError')).toEqual([['payload']]); }); it('emits correct event on discussion update note error', () => { - findFirstDiscussion().vm.$emit('updateNoteError', 'payload'); + findFirstDiscussion().vm.$emit('update-note-error', 'payload'); expect(wrapper.emitted('updateNoteError')).toEqual([['payload']]); }); it('emits correct event on discussion resolve error', () => { - findFirstDiscussion().vm.$emit('resolveDiscussionError', 'payload'); + findFirstDiscussion().vm.$emit('resolve-discussion-error', 'payload'); expect(wrapper.emitted('resolveDiscussionError')).toEqual([['payload']]); }); it('changes prop correctly on opening discussion form', () => { - findFirstDiscussion().vm.$emit('openForm', 'some-id'); + findFirstDiscussion().vm.$emit('open-form', 'some-id'); return wrapper.vm.$nextTick().then(() => { expect(findFirstDiscussion().props('discussionWithOpenForm')).toBe('some-id'); @@ -220,6 +228,10 @@ describe('Design management design sidebar component', () => { expect(findPopover().exists()).toBe(true); }); + it('scrolls to resolved threads link', () => { + expect(scrollIntoViewMock).toHaveBeenCalled(); + }); + it('dismisses a popover on the outside click', () => { wrapper.trigger('click'); return wrapper.vm.$nextTick(() => { @@ -233,4 +245,23 @@ describe('Design management design sidebar component', () => { expect(Cookies.set).toHaveBeenCalledWith(cookieKey, 'true', { expires: 365 * 10 }); }); }); + + it('does not render To-Do button by default', () => { + createComponent(); + expect(wrapper.find(DesignTodoButton).exists()).toBe(false); + }); + + describe('when `design_management_todo_button` feature flag is enabled', () => { + beforeEach(() => { + createComponent({}, { enableTodoButton: true }); + }); + + it('renders sidebar root element with no top padding', () => { + expect(wrapper.classes()).toContain('gl-pt-0'); + }); + + it('renders To-Do button', () => { + expect(wrapper.find(DesignTodoButton).exists()).toBe(true); + }); + }); }); diff --git a/spec/frontend/design_management/components/design_todo_button_spec.js b/spec/frontend/design_management/components/design_todo_button_spec.js new file mode 100644 index 00000000000..451c23f0fea --- /dev/null +++ b/spec/frontend/design_management/components/design_todo_button_spec.js @@ -0,0 +1,158 @@ +import { shallowMount, mount } from '@vue/test-utils'; +import TodoButton from '~/vue_shared/components/todo_button.vue'; +import DesignTodoButton from '~/design_management/components/design_todo_button.vue'; +import createDesignTodoMutation from '~/design_management/graphql/mutations/create_design_todo.mutation.graphql'; +import todoMarkDoneMutation from '~/graphql_shared/mutations/todo_mark_done.mutation.graphql'; +import mockDesign from '../mock_data/design'; + +const mockDesignWithPendingTodos = { + ...mockDesign, + currentUserTodos: { + nodes: [ + { + id: 'todo-id', + }, + ], + }, +}; + +const mutate = jest.fn().mockResolvedValue(); + +describe('Design management design todo button', () => { + let wrapper; + + function createComponent(props = {}, { mountFn = shallowMount } = {}) { + wrapper = mountFn(DesignTodoButton, { + propsData: { + design: mockDesign, + ...props, + }, + provide: { + projectPath: 'project-path', + issueIid: '10', + }, + mocks: { + $route: { + params: { + id: 'my-design.jpg', + }, + query: {}, + }, + $apollo: { + mutate, + }, + }, + }); + } + + beforeEach(() => { + createComponent(); + }); + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + jest.clearAllMocks(); + }); + + it('renders TodoButton component', () => { + expect(wrapper.find(TodoButton).exists()).toBe(true); + }); + + describe('when design has a pending todo', () => { + beforeEach(() => { + createComponent({ design: mockDesignWithPendingTodos }, { mountFn: mount }); + }); + + it('renders correct button text', () => { + expect(wrapper.text()).toBe('Mark as done'); + }); + + describe('when clicked', () => { + let dispatchEventSpy; + + beforeEach(() => { + dispatchEventSpy = jest.spyOn(document, 'dispatchEvent'); + jest.spyOn(document, 'querySelector').mockReturnValue({ + innerText: 2, + }); + + createComponent({ design: mockDesignWithPendingTodos }, { mountFn: mount }); + wrapper.trigger('click'); + return wrapper.vm.$nextTick(); + }); + + it('calls `$apollo.mutate` with the `todoMarkDone` mutation and variables containing `id`', async () => { + const todoMarkDoneMutationVariables = { + mutation: todoMarkDoneMutation, + update: expect.anything(), + variables: { + id: 'todo-id', + }, + }; + + expect(mutate).toHaveBeenCalledTimes(1); + expect(mutate).toHaveBeenCalledWith(todoMarkDoneMutationVariables); + }); + + it('calls dispatchDocumentEvent to update global To-Do counter correctly', () => { + const dispatchedEvent = dispatchEventSpy.mock.calls[0][0]; + + expect(dispatchEventSpy).toHaveBeenCalledTimes(1); + expect(dispatchedEvent.detail).toEqual({ count: 1 }); + expect(dispatchedEvent.type).toBe('todo:toggle'); + }); + }); + }); + + describe('when design has no pending todos', () => { + beforeEach(() => { + createComponent({}, { mountFn: mount }); + }); + + it('renders correct button text', () => { + expect(wrapper.text()).toBe('Add a To-Do'); + }); + + describe('when clicked', () => { + let dispatchEventSpy; + + beforeEach(() => { + dispatchEventSpy = jest.spyOn(document, 'dispatchEvent'); + jest.spyOn(document, 'querySelector').mockReturnValue({ + innerText: 2, + }); + + createComponent({}, { mountFn: mount }); + wrapper.trigger('click'); + return wrapper.vm.$nextTick(); + }); + + it('calls `$apollo.mutate` with the `createDesignTodoMutation` mutation and variables containing `issuable_id`, `issue_id`, & `projectPath`', async () => { + const createDesignTodoMutationVariables = { + mutation: createDesignTodoMutation, + update: expect.anything(), + variables: { + atVersion: null, + filenames: ['my-design.jpg'], + designId: '1', + issueId: '1', + issueIid: '10', + projectPath: 'project-path', + }, + }; + + expect(mutate).toHaveBeenCalledTimes(1); + expect(mutate).toHaveBeenCalledWith(createDesignTodoMutationVariables); + }); + + it('calls dispatchDocumentEvent to update global To-Do counter correctly', () => { + const dispatchedEvent = dispatchEventSpy.mock.calls[0][0]; + + expect(dispatchEventSpy).toHaveBeenCalledTimes(1); + expect(dispatchedEvent.detail).toEqual({ count: 3 }); + expect(dispatchedEvent.type).toBe('todo:toggle'); + }); + }); + }); +}); diff --git a/spec/frontend/design_management/components/list/__snapshots__/item_spec.js.snap b/spec/frontend/design_management/components/list/__snapshots__/item_spec.js.snap index d76b6e712fe..822df1f6472 100644 --- a/spec/frontend/design_management/components/list/__snapshots__/item_spec.js.snap +++ b/spec/frontend/design_management/components/list/__snapshots__/item_spec.js.snap @@ -10,11 +10,11 @@ exports[`Design management list item component when item appears in view after i exports[`Design management list item component with notes renders item with multiple comments 1`] = ` <router-link-stub - class="card cursor-pointer text-plain js-design-list-item design-list-item design-list-item-new" + class="card gl-cursor-pointer text-plain js-design-list-item design-list-item design-list-item-new" to="[object Object]" > <div - class="card-body p-0 d-flex-center overflow-hidden position-relative" + class="card-body gl-p-0 gl-display-flex gl-align-items-center gl-justify-content-center gl-overflow-hidden gl-relative" > <!----> @@ -23,7 +23,7 @@ exports[`Design management list item component with notes renders item with mult <img alt="test" - class="block mx-auto mw-100 mh-100 design-img" + class="gl-display-block gl-mx-auto gl-max-w-full mh-100 design-img" data-qa-selector="design_image" src="" /> @@ -31,13 +31,13 @@ exports[`Design management list item component with notes renders item with mult </div> <div - class="card-footer d-flex w-100" + class="card-footer gl-display-flex gl-w-full" > <div - class="d-flex flex-column str-truncated-100" + class="gl-display-flex gl-flex-direction-column str-truncated-100" > <span - class="bold str-truncated-100" + class="gl-font-weight-bold str-truncated-100" data-qa-selector="design_file_name" > test @@ -57,17 +57,17 @@ exports[`Design management list item component with notes renders item with mult </div> <div - class="ml-auto d-flex align-items-center text-secondary" + class="gl-ml-auto gl-display-flex gl-align-items-center gl-text-gray-500" > - <icon-stub - class="ml-1" + <gl-icon-stub + class="gl-ml-2" name="comments" size="16" /> <span aria-label="2 comments" - class="ml-1" + class="gl-ml-2" > 2 @@ -80,11 +80,11 @@ exports[`Design management list item component with notes renders item with mult exports[`Design management list item component with notes renders item with single comment 1`] = ` <router-link-stub - class="card cursor-pointer text-plain js-design-list-item design-list-item design-list-item-new" + class="card gl-cursor-pointer text-plain js-design-list-item design-list-item design-list-item-new" to="[object Object]" > <div - class="card-body p-0 d-flex-center overflow-hidden position-relative" + class="card-body gl-p-0 gl-display-flex gl-align-items-center gl-justify-content-center gl-overflow-hidden gl-relative" > <!----> @@ -93,7 +93,7 @@ exports[`Design management list item component with notes renders item with sing <img alt="test" - class="block mx-auto mw-100 mh-100 design-img" + class="gl-display-block gl-mx-auto gl-max-w-full mh-100 design-img" data-qa-selector="design_image" src="" /> @@ -101,13 +101,13 @@ exports[`Design management list item component with notes renders item with sing </div> <div - class="card-footer d-flex w-100" + class="card-footer gl-display-flex gl-w-full" > <div - class="d-flex flex-column str-truncated-100" + class="gl-display-flex gl-flex-direction-column str-truncated-100" > <span - class="bold str-truncated-100" + class="gl-font-weight-bold str-truncated-100" data-qa-selector="design_file_name" > test @@ -127,17 +127,17 @@ exports[`Design management list item component with notes renders item with sing </div> <div - class="ml-auto d-flex align-items-center text-secondary" + class="gl-ml-auto gl-display-flex gl-align-items-center gl-text-gray-500" > - <icon-stub - class="ml-1" + <gl-icon-stub + class="gl-ml-2" name="comments" size="16" /> <span aria-label="1 comment" - class="ml-1" + class="gl-ml-2" > 1 diff --git a/spec/frontend/design_management/components/list/item_spec.js b/spec/frontend/design_management/components/list/item_spec.js index d1c90bd57b0..55c6ecbc26b 100644 --- a/spec/frontend/design_management/components/list/item_spec.js +++ b/spec/frontend/design_management/components/list/item_spec.js @@ -1,7 +1,6 @@ import { createLocalVue, shallowMount } from '@vue/test-utils'; import { GlIcon, GlLoadingIcon, GlIntersectionObserver } from '@gitlab/ui'; import VueRouter from 'vue-router'; -import Icon from '~/vue_shared/components/icon.vue'; import Item from '~/design_management/components/list/item.vue'; const localVue = createLocalVue(); @@ -20,7 +19,7 @@ describe('Design management list item component', () => { let wrapper; const findDesignEvent = () => wrapper.find('[data-testid="designEvent"]'); - const findEventIcon = () => findDesignEvent().find(Icon); + const findEventIcon = () => findDesignEvent().find(GlIcon); const findLoadingIcon = () => wrapper.find(GlLoadingIcon); function createComponent({ diff --git a/spec/frontend/design_management/components/upload/__snapshots__/design_version_dropdown_spec.js.snap b/spec/frontend/design_management/components/upload/__snapshots__/design_version_dropdown_spec.js.snap index d6fd09eb698..1e94e90c3b0 100644 --- a/spec/frontend/design_management/components/upload/__snapshots__/design_version_dropdown_spec.js.snap +++ b/spec/frontend/design_management/components/upload/__snapshots__/design_version_dropdown_spec.js.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Design management design version dropdown component renders design version dropdown button 1`] = ` -<gl-new-dropdown-stub +<gl-dropdown-stub category="tertiary" headertext="" issueiid="" @@ -10,7 +10,7 @@ exports[`Design management design version dropdown component renders design vers text="Showing latest version" variant="default" > - <gl-new-dropdown-item-stub + <gl-dropdown-item-stub avatarurl="" iconcolor="" iconname="" @@ -22,8 +22,8 @@ exports[`Design management design version dropdown component renders design vers Version 2 (latest) - </gl-new-dropdown-item-stub> - <gl-new-dropdown-item-stub + </gl-dropdown-item-stub> + <gl-dropdown-item-stub avatarurl="" iconcolor="" iconname="" @@ -34,12 +34,12 @@ exports[`Design management design version dropdown component renders design vers Version 1 - </gl-new-dropdown-item-stub> -</gl-new-dropdown-stub> + </gl-dropdown-item-stub> +</gl-dropdown-stub> `; exports[`Design management design version dropdown component renders design version list 1`] = ` -<gl-new-dropdown-stub +<gl-dropdown-stub category="tertiary" headertext="" issueiid="" @@ -48,7 +48,7 @@ exports[`Design management design version dropdown component renders design vers text="Showing latest version" variant="default" > - <gl-new-dropdown-item-stub + <gl-dropdown-item-stub avatarurl="" iconcolor="" iconname="" @@ -60,8 +60,8 @@ exports[`Design management design version dropdown component renders design vers Version 2 (latest) - </gl-new-dropdown-item-stub> - <gl-new-dropdown-item-stub + </gl-dropdown-item-stub> + <gl-dropdown-item-stub avatarurl="" iconcolor="" iconname="" @@ -72,6 +72,6 @@ exports[`Design management design version dropdown component renders design vers Version 1 - </gl-new-dropdown-item-stub> -</gl-new-dropdown-stub> + </gl-dropdown-item-stub> +</gl-dropdown-stub> `; diff --git a/spec/frontend/design_management/components/upload/design_version_dropdown_spec.js b/spec/frontend/design_management/components/upload/design_version_dropdown_spec.js index f4206cdaeb3..4ef787ac754 100644 --- a/spec/frontend/design_management/components/upload/design_version_dropdown_spec.js +++ b/spec/frontend/design_management/components/upload/design_version_dropdown_spec.js @@ -1,5 +1,5 @@ import { shallowMount } from '@vue/test-utils'; -import { GlNewDropdown, GlNewDropdownItem, GlSprintf } from '@gitlab/ui'; +import { GlDropdown, GlDropdownItem, GlSprintf } from '@gitlab/ui'; import DesignVersionDropdown from '~/design_management/components/upload/design_version_dropdown.vue'; import mockAllVersions from './mock_data/all_versions'; @@ -42,7 +42,7 @@ describe('Design management design version dropdown component', () => { wrapper.destroy(); }); - const findVersionLink = index => wrapper.findAll(GlNewDropdownItem).at(index); + const findVersionLink = index => wrapper.findAll(GlDropdownItem).at(index); it('renders design version dropdown button', () => { createComponent(); @@ -75,7 +75,7 @@ describe('Design management design version dropdown component', () => { createComponent(); return wrapper.vm.$nextTick().then(() => { - expect(wrapper.find(GlNewDropdown).attributes('text')).toBe('Showing latest version'); + expect(wrapper.find(GlDropdown).attributes('text')).toBe('Showing latest version'); }); }); @@ -83,7 +83,7 @@ describe('Design management design version dropdown component', () => { createComponent({ maxVersions: 1 }); return wrapper.vm.$nextTick().then(() => { - expect(wrapper.find(GlNewDropdown).attributes('text')).toBe('Showing latest version'); + expect(wrapper.find(GlDropdown).attributes('text')).toBe('Showing latest version'); }); }); @@ -91,7 +91,7 @@ describe('Design management design version dropdown component', () => { createComponent({ $route: designRouteFactory(PREVIOUS_VERSION_ID) }); return wrapper.vm.$nextTick().then(() => { - expect(wrapper.find(GlNewDropdown).attributes('text')).toBe(`Showing version #1`); + expect(wrapper.find(GlDropdown).attributes('text')).toBe(`Showing version #1`); }); }); @@ -99,7 +99,7 @@ describe('Design management design version dropdown component', () => { createComponent({ $route: designRouteFactory(LATEST_VERSION_ID) }); return wrapper.vm.$nextTick().then(() => { - expect(wrapper.find(GlNewDropdown).attributes('text')).toBe('Showing latest version'); + expect(wrapper.find(GlDropdown).attributes('text')).toBe('Showing latest version'); }); }); @@ -107,7 +107,7 @@ describe('Design management design version dropdown component', () => { createComponent(); return wrapper.vm.$nextTick().then(() => { - expect(wrapper.findAll(GlNewDropdownItem)).toHaveLength(wrapper.vm.allVersions.length); + expect(wrapper.findAll(GlDropdownItem)).toHaveLength(wrapper.vm.allVersions.length); }); }); }); diff --git a/spec/frontend/design_management/mock_data/apollo_mock.js b/spec/frontend/design_management/mock_data/apollo_mock.js index 5e2df3877a5..1c7806c292f 100644 --- a/spec/frontend/design_management/mock_data/apollo_mock.js +++ b/spec/frontend/design_management/mock_data/apollo_mock.js @@ -13,6 +13,9 @@ export const designListQueryResponse = { notesCount: 3, image: 'image-1', imageV432x230: 'image-1', + currentUserTodos: { + nodes: [], + }, }, { id: '2', @@ -21,6 +24,9 @@ export const designListQueryResponse = { notesCount: 2, image: 'image-2', imageV432x230: 'image-2', + currentUserTodos: { + nodes: [], + }, }, { id: '3', @@ -29,6 +35,9 @@ export const designListQueryResponse = { notesCount: 1, image: 'image-3', imageV432x230: 'image-3', + currentUserTodos: { + nodes: [], + }, }, ], }, @@ -60,6 +69,9 @@ export const reorderedDesigns = [ notesCount: 2, image: 'image-2', imageV432x230: 'image-2', + currentUserTodos: { + nodes: [], + }, }, { id: '1', @@ -68,6 +80,9 @@ export const reorderedDesigns = [ notesCount: 3, image: 'image-1', imageV432x230: 'image-1', + currentUserTodos: { + nodes: [], + }, }, { id: '3', @@ -76,6 +91,9 @@ export const reorderedDesigns = [ notesCount: 1, image: 'image-3', imageV432x230: 'image-3', + currentUserTodos: { + nodes: [], + }, }, ]; diff --git a/spec/frontend/design_management/mock_data/design.js b/spec/frontend/design_management/mock_data/design.js index 72be33fef1d..f2a3a800969 100644 --- a/spec/frontend/design_management/mock_data/design.js +++ b/spec/frontend/design_management/mock_data/design.js @@ -1,5 +1,5 @@ export default { - id: 'design-id', + id: 'gid::/gitlab/Design/1', filename: 'test.jpg', fullPath: 'full-design-path', image: 'test.jpg', @@ -8,6 +8,7 @@ export default { name: 'test', }, issue: { + id: 'gid::/gitlab/Issue/1', title: 'My precious issue', webPath: 'full-issue-path', webUrl: 'full-issue-url', diff --git a/spec/frontend/design_management/mock_data/discussion.js b/spec/frontend/design_management/mock_data/discussion.js new file mode 100644 index 00000000000..fbf9a2fdcc1 --- /dev/null +++ b/spec/frontend/design_management/mock_data/discussion.js @@ -0,0 +1,45 @@ +export default { + id: 'discussion-id-1', + resolved: false, + resolvable: true, + notes: [ + { + id: 'note-id-1', + index: 1, + position: { + height: 100, + width: 100, + x: 10, + y: 15, + }, + author: { + name: 'John', + webUrl: 'link-to-john-profile', + }, + createdAt: '2020-05-08T07:10:45Z', + userPermissions: { + adminNote: true, + }, + resolved: false, + }, + { + id: 'note-id-3', + index: 3, + position: { + height: 50, + width: 50, + x: 25, + y: 25, + }, + author: { + name: 'Mary', + webUrl: 'link-to-mary-profile', + }, + createdAt: '2020-05-08T07:10:45Z', + userPermissions: { + adminNote: true, + }, + resolved: false, + }, + ], +}; diff --git a/spec/frontend/design_management/mock_data/notes.js b/spec/frontend/design_management/mock_data/notes.js index 80cb3944786..41cefaca05b 100644 --- a/spec/frontend/design_management/mock_data/notes.js +++ b/spec/frontend/design_management/mock_data/notes.js @@ -1,46 +1,44 @@ +import DISCUSSION_1 from './discussion'; + +const DISCUSSION_2 = { + id: 'discussion-id-2', + notes: { + nodes: [ + { + id: 'note-id-2', + index: 2, + position: { + height: 50, + width: 50, + x: 25, + y: 25, + }, + author: { + name: 'Mary', + webUrl: 'link-to-mary-profile', + }, + createdAt: '2020-05-08T07:10:45Z', + userPermissions: { + adminNote: true, + }, + resolved: true, + }, + ], + }, +}; + export default [ { - id: 'note-id-1', - index: 1, - position: { - height: 100, - width: 100, - x: 10, - y: 15, - }, - author: { - name: 'John', - webUrl: 'link-to-john-profile', - }, - createdAt: '2020-05-08T07:10:45Z', - userPermissions: { - adminNote: true, - }, + ...DISCUSSION_1.notes[0], discussion: { - id: 'discussion-id-1', + id: DISCUSSION_1.id, + notes: { + nodes: DISCUSSION_1.notes, + }, }, - resolved: false, }, { - id: 'note-id-2', - index: 2, - position: { - height: 50, - width: 50, - x: 25, - y: 25, - }, - author: { - name: 'Mary', - webUrl: 'link-to-mary-profile', - }, - createdAt: '2020-05-08T07:10:45Z', - userPermissions: { - adminNote: true, - }, - discussion: { - id: 'discussion-id-2', - }, - resolved: true, + ...DISCUSSION_2.notes.nodes[0], + discussion: DISCUSSION_2, }, ]; diff --git a/spec/frontend/design_management/pages/__snapshots__/index_spec.js.snap b/spec/frontend/design_management/pages/__snapshots__/index_spec.js.snap index 3881b2d7679..b80b7fdb43e 100644 --- a/spec/frontend/design_management/pages/__snapshots__/index_spec.js.snap +++ b/spec/frontend/design_management/pages/__snapshots__/index_spec.js.snap @@ -111,7 +111,7 @@ exports[`Design management index page designs renders designs list and header wi > <gl-button-stub category="primary" - class="gl-mr-3 js-select-all" + class="gl-mr-4 js-select-all" icon="" size="small" variant="link" diff --git a/spec/frontend/design_management/pages/design/__snapshots__/index_spec.js.snap b/spec/frontend/design_management/pages/design/__snapshots__/index_spec.js.snap index 823294efc38..c849e4d4ed6 100644 --- a/spec/frontend/design_management/pages/design/__snapshots__/index_spec.js.snap +++ b/spec/frontend/design_management/pages/design/__snapshots__/index_spec.js.snap @@ -32,6 +32,8 @@ exports[`Design management design index page renders design index 1`] = ` <div class="image-notes" > + <!----> + <h2 class="gl-font-weight-bold gl-mt-0" > @@ -57,11 +59,11 @@ exports[`Design management design index page renders design index 1`] = ` <design-discussion-stub data-testid="unresolved-discussion" - designid="test" + designid="gid::/gitlab/Design/1" discussion="[object Object]" discussionwithopenform="" markdownpreviewpath="/project-path/preview_markdown?target_type=Issue" - noteableid="design-id" + noteableid="gid::/gitlab/Design/1" /> <gl-button-stub @@ -92,7 +94,7 @@ exports[`Design management design index page renders design index 1`] = ` </p> <a - href="#" + href="https://docs.gitlab.com/ee/user/project/issues/design_management.html#resolve-design-threads" rel="noopener noreferrer" target="_blank" > @@ -105,11 +107,11 @@ exports[`Design management design index page renders design index 1`] = ` > <design-discussion-stub data-testid="resolved-discussion" - designid="test" + designid="gid::/gitlab/Design/1" discussion="[object Object]" discussionwithopenform="" markdownpreviewpath="/project-path/preview_markdown?target_type=Issue" - noteableid="design-id" + noteableid="gid::/gitlab/Design/1" /> </gl-collapse-stub> @@ -179,6 +181,8 @@ exports[`Design management design index page with error GlAlert is rendered in c <div class="image-notes" > + <!----> + <h2 class="gl-font-weight-bold gl-mt-0" > diff --git a/spec/frontend/design_management/pages/design/index_spec.js b/spec/frontend/design_management/pages/design/index_spec.js index 369c8667f4d..d9f7146d258 100644 --- a/spec/frontend/design_management/pages/design/index_spec.js +++ b/spec/frontend/design_management/pages/design/index_spec.js @@ -7,24 +7,21 @@ import DesignIndex from '~/design_management/pages/design/index.vue'; import DesignSidebar from '~/design_management/components/design_sidebar.vue'; import DesignPresentation from '~/design_management/components/design_presentation.vue'; import createImageDiffNoteMutation from '~/design_management/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 updateActiveDiscussion from '~/design_management/graphql/mutations/update_active_discussion.mutation.graphql'; import { DESIGN_NOT_FOUND_ERROR, DESIGN_VERSION_NOT_EXIST_ERROR, } from '~/design_management/utils/error_messages'; -import { DESIGNS_ROUTE_NAME } from '~/design_management/router/constants'; +import { DESIGNS_ROUTE_NAME, DESIGN_ROUTE_NAME } from '~/design_management/router/constants'; import createRouter from '~/design_management/router'; import * as utils from '~/design_management/utils/design_management_utils'; import { DESIGN_DETAIL_LAYOUT_CLASSLIST } from '~/design_management/constants'; +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'; jest.mock('~/flash'); -jest.mock('mousetrap', () => ({ - bind: jest.fn(), - unbind: jest.fn(), -})); const focusInput = jest.fn(); @@ -34,6 +31,12 @@ const DesignReplyForm = { focusInput, }, }; +const mockDesignNoDiscussions = { + ...design, + discussions: { + nodes: [], + }, +}; const localVue = createLocalVue(); localVue.use(VueRouter); @@ -75,7 +78,7 @@ describe('Design management design index page', () => { const findSidebar = () => wrapper.find(DesignSidebar); const findDesignPresentation = () => wrapper.find(DesignPresentation); - function createComponent(loading = false, data = {}) { + function createComponent({ loading = false } = {}, { data = {}, intialRouteOptions = {} } = {}) { const $apollo = { queries: { design: { @@ -87,6 +90,8 @@ describe('Design management design index page', () => { router = createRouter(); + router.push({ name: DESIGN_ROUTE_NAME, params: { id: design.id }, ...intialRouteOptions }); + wrapper = shallowMount(DesignIndex, { propsData: { id: '1' }, mocks: { $apollo }, @@ -126,29 +131,28 @@ describe('Design management design index page', () => { }, }; jest.spyOn(utils, 'getPageLayoutElement').mockReturnValue(mockEl); - createComponent(true); + createComponent({ loading: 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); + createComponent({ loading: true }); expect(wrapper.element).toMatchSnapshot(); }); it('renders design index', () => { - createComponent(false, { design }); + createComponent({ loading: false }, { data: { design } }); expect(wrapper.element).toMatchSnapshot(); expect(wrapper.find(GlAlert).exists()).toBe(false); }); it('passes correct props to sidebar component', () => { - createComponent(false, { design }); + createComponent({ loading: false }, { data: { design } }); expect(findSidebar().props()).toEqual({ design, @@ -158,14 +162,14 @@ describe('Design management design index page', () => { }); it('opens a new discussion form', () => { - createComponent(false, { - design: { - ...design, - discussions: { - nodes: [], + createComponent( + { loading: false }, + { + data: { + design, }, }, - }); + ); findDesignPresentation().vm.$emit('openCommentForm', { x: 0, y: 0 }); @@ -175,15 +179,15 @@ describe('Design management design index page', () => { }); it('keeps new discussion form focused', () => { - createComponent(false, { - design: { - ...design, - discussions: { - nodes: [], + createComponent( + { loading: false }, + { + data: { + design, + annotationCoordinates, }, }, - annotationCoordinates, - }); + ); findDesignPresentation().vm.$emit('openCommentForm', { x: 10, y: 10 }); @@ -191,18 +195,18 @@ describe('Design management design index page', () => { }); it('sends a mutation on submitting form and closes form', () => { - createComponent(false, { - design: { - ...design, - discussions: { - nodes: [], + createComponent( + { loading: false }, + { + data: { + design, + annotationCoordinates, + comment: newComment, }, }, - annotationCoordinates, - comment: newComment, - }); + ); - findDiscussionForm().vm.$emit('submitForm'); + findDiscussionForm().vm.$emit('submit-form'); expect(mutate).toHaveBeenCalledWith(createDiscussionMutationVariables); return wrapper.vm @@ -216,18 +220,18 @@ describe('Design management design index page', () => { }); it('closes the form and clears the comment on canceling form', () => { - createComponent(false, { - design: { - ...design, - discussions: { - nodes: [], + createComponent( + { loading: false }, + { + data: { + design, + annotationCoordinates, + comment: newComment, }, }, - annotationCoordinates, - comment: newComment, - }); + ); - findDiscussionForm().vm.$emit('cancelForm'); + findDiscussionForm().vm.$emit('cancel-form'); expect(wrapper.vm.comment).toBe(''); @@ -238,15 +242,15 @@ describe('Design management design index page', () => { describe('with error', () => { beforeEach(() => { - createComponent(false, { - design: { - ...design, - discussions: { - nodes: [], + createComponent( + { loading: false }, + { + data: { + design: mockDesignNoDiscussions, + errorMessage: 'woops', }, }, - errorMessage: 'woops', - }); + ); }); it('GlAlert is rendered in correct position with correct content', () => { @@ -257,7 +261,7 @@ describe('Design management design index page', () => { describe('onDesignQueryResult', () => { describe('with no designs', () => { it('redirects to /designs', () => { - createComponent(true); + createComponent({ loading: true }); router.push = jest.fn(); wrapper.vm.onDesignQueryResult({ data: mockResponseNoDesigns, loading: false }); @@ -272,7 +276,7 @@ describe('Design management design index page', () => { describe('when no design exists for given version', () => { it('redirects to /designs', () => { - createComponent(true); + createComponent({ loading: true }); wrapper.setData({ allVersions: mockAllVersions, }); @@ -291,4 +295,24 @@ describe('Design management design index page', () => { }); }); }); + + describe('when hash present in current route', () => { + it('calls updateActiveDiscussion mutation', () => { + createComponent( + { loading: false }, + { + data: { + design, + }, + intialRouteOptions: { hash: '#note_123' }, + }, + ); + + expect(mutate).toHaveBeenCalledTimes(1); + expect(mutate).toHaveBeenCalledWith({ + mutation: updateActiveDiscussion, + variables: { id: 'gid://gitlab/DiffNote/123', source: 'url' }, + }); + }); + }); }); diff --git a/spec/frontend/design_management/pages/index_apollo_spec.js b/spec/frontend/design_management/pages/index_apollo_spec.js deleted file mode 100644 index 3ea711c2cfa..00000000000 --- a/spec/frontend/design_management/pages/index_apollo_spec.js +++ /dev/null @@ -1,162 +0,0 @@ -import { shallowMount, createLocalVue } from '@vue/test-utils'; -import { createMockClient } from 'mock-apollo-client'; -import VueApollo from 'vue-apollo'; -import VueRouter from 'vue-router'; -import VueDraggable from 'vuedraggable'; -import { InMemoryCache } from 'apollo-cache-inmemory'; -import Design from '~/design_management/components/list/item.vue'; -import createRouter from '~/design_management/router'; -import getDesignListQuery from '~/design_management/graphql/queries/get_design_list.query.graphql'; -import permissionsQuery from '~/design_management/graphql/queries/design_permissions.query.graphql'; -import moveDesignMutation from '~/design_management/graphql/mutations/move_design.mutation.graphql'; -import { deprecatedCreateFlash as createFlash } from '~/flash'; -import Index from '~/design_management/pages/index.vue'; -import { - designListQueryResponse, - permissionsQueryResponse, - moveDesignMutationResponse, - reorderedDesigns, - moveDesignMutationResponseWithErrors, -} from '../mock_data/apollo_mock'; - -jest.mock('~/flash'); - -const localVue = createLocalVue(); -localVue.use(VueApollo); - -const router = createRouter(); -localVue.use(VueRouter); - -const designToMove = { - __typename: 'Design', - id: '2', - event: 'NONE', - filename: 'fox_2.jpg', - notesCount: 2, - image: 'image-2', - imageV432x230: 'image-2', -}; - -describe('Design management index page with Apollo mock', () => { - let wrapper; - let mockClient; - let apolloProvider; - let moveDesignHandler; - - async function moveDesigns(localWrapper) { - await jest.runOnlyPendingTimers(); - await localWrapper.vm.$nextTick(); - - localWrapper.find(VueDraggable).vm.$emit('input', reorderedDesigns); - localWrapper.find(VueDraggable).vm.$emit('change', { - moved: { - newIndex: 0, - element: designToMove, - }, - }); - } - - const fragmentMatcher = { match: () => true }; - - const cache = new InMemoryCache({ - fragmentMatcher, - addTypename: false, - }); - - const findDesigns = () => wrapper.findAll(Design); - - function createComponent({ - moveHandler = jest.fn().mockResolvedValue(moveDesignMutationResponse), - }) { - mockClient = createMockClient({ cache }); - - mockClient.setRequestHandler( - getDesignListQuery, - jest.fn().mockResolvedValue(designListQueryResponse), - ); - - mockClient.setRequestHandler( - permissionsQuery, - jest.fn().mockResolvedValue(permissionsQueryResponse), - ); - - moveDesignHandler = moveHandler; - - mockClient.setRequestHandler(moveDesignMutation, moveDesignHandler); - - apolloProvider = new VueApollo({ - defaultClient: mockClient, - }); - - wrapper = shallowMount(Index, { - localVue, - apolloProvider, - router, - stubs: { VueDraggable }, - }); - } - - afterEach(() => { - wrapper.destroy(); - wrapper = null; - mockClient = null; - apolloProvider = null; - }); - - it('has a design with id 1 as a first one', async () => { - createComponent({}); - - await jest.runOnlyPendingTimers(); - await wrapper.vm.$nextTick(); - - expect(findDesigns()).toHaveLength(3); - expect( - findDesigns() - .at(0) - .props('id'), - ).toBe('1'); - }); - - it('calls a mutation with correct parameters and reorders designs', async () => { - createComponent({}); - - await moveDesigns(wrapper); - - expect(moveDesignHandler).toHaveBeenCalled(); - - await wrapper.vm.$nextTick(); - - expect( - findDesigns() - .at(0) - .props('id'), - ).toBe('2'); - }); - - it('displays flash if mutation had a recoverable error', async () => { - createComponent({ - moveHandler: jest.fn().mockResolvedValue(moveDesignMutationResponseWithErrors), - }); - - await moveDesigns(wrapper); - - await wrapper.vm.$nextTick(); - - expect(createFlash).toHaveBeenCalledWith('Houston, we have a problem'); - }); - - it('displays flash if mutation had a non-recoverable error', async () => { - createComponent({ - moveHandler: jest.fn().mockRejectedValue('Error'), - }); - - await moveDesigns(wrapper); - - await jest.runOnlyPendingTimers(); - await wrapper.vm.$nextTick(); - - expect(createFlash).toHaveBeenCalledWith( - 'Something went wrong when reordering designs. Please try again', - ); - }); -}); diff --git a/spec/frontend/design_management/pages/index_spec.js b/spec/frontend/design_management/pages/index_spec.js index 093fa155d2e..661717d29a3 100644 --- a/spec/frontend/design_management/pages/index_spec.js +++ b/spec/frontend/design_management/pages/index_spec.js @@ -1,13 +1,15 @@ import { createLocalVue, shallowMount } from '@vue/test-utils'; -import { ApolloMutation } from 'vue-apollo'; +import VueApollo, { ApolloMutation } from 'vue-apollo'; import VueDraggable from 'vuedraggable'; import VueRouter from 'vue-router'; import { GlEmptyState } from '@gitlab/ui'; +import createMockApollo from 'jest/helpers/mock_apollo_helper'; import Index from '~/design_management/pages/index.vue'; import uploadDesignQuery from '~/design_management/graphql/mutations/upload_design.mutation.graphql'; import DesignDestroyer from '~/design_management/components/design_destroyer.vue'; import DesignDropzone from '~/design_management/components/upload/design_dropzone.vue'; import DeleteButton from '~/design_management/components/delete_button.vue'; +import Design from '~/design_management/components/list/item.vue'; import { DESIGNS_ROUTE_NAME } from '~/design_management/router/constants'; import { EXISTING_DESIGN_DROP_MANY_FILES_MESSAGE, @@ -17,6 +19,16 @@ import { deprecatedCreateFlash as createFlash } from '~/flash'; import createRouter from '~/design_management/router'; import * as utils from '~/design_management/utils/design_management_utils'; import { DESIGN_DETAIL_LAYOUT_CLASSLIST } from '~/design_management/constants'; +import { + designListQueryResponse, + permissionsQueryResponse, + moveDesignMutationResponse, + reorderedDesigns, + moveDesignMutationResponseWithErrors, +} from '../mock_data/apollo_mock'; +import getDesignListQuery from '~/design_management/graphql/queries/get_design_list.query.graphql'; +import permissionsQuery from '~/design_management/graphql/queries/design_permissions.query.graphql'; +import moveDesignMutation from '~/design_management/graphql/mutations/move_design.mutation.graphql'; jest.mock('~/flash.js'); const mockPageEl = { @@ -61,9 +73,21 @@ const mockVersion = { id: 'gid://gitlab/DesignManagement::Version/1', }; +const designToMove = { + __typename: 'Design', + id: '2', + event: 'NONE', + filename: 'fox_2.jpg', + notesCount: 2, + image: 'image-2', + imageV432x230: 'image-2', +}; + describe('Design management index page', () => { let mutate; let wrapper; + let fakeApollo; + let moveDesignHandler; const findDesignCheckboxes = () => wrapper.findAll('.design-checkbox'); const findSelectAllButton = () => wrapper.find('.js-select-all'); @@ -74,6 +98,20 @@ describe('Design management index page', () => { const findDropzoneWrapper = () => wrapper.find('[data-testid="design-dropzone-wrapper"]'); const findFirstDropzoneWithDesign = () => wrapper.findAll(DesignDropzone).at(1); const findDesignsWrapper = () => wrapper.find('[data-testid="designs-root"]'); + const findDesigns = () => wrapper.findAll(Design); + + async function moveDesigns(localWrapper) { + await jest.runOnlyPendingTimers(); + await localWrapper.vm.$nextTick(); + + localWrapper.find(VueDraggable).vm.$emit('input', reorderedDesigns); + localWrapper.find(VueDraggable).vm.$emit('change', { + moved: { + newIndex: 0, + element: designToMove, + }, + }); + } function createComponent({ loading = false, @@ -118,8 +156,30 @@ describe('Design management index page', () => { }); } + function createComponentWithApollo({ + moveHandler = jest.fn().mockResolvedValue(moveDesignMutationResponse), + }) { + localVue.use(VueApollo); + moveDesignHandler = moveHandler; + + const requestHandlers = [ + [getDesignListQuery, jest.fn().mockResolvedValue(designListQueryResponse)], + [permissionsQuery, jest.fn().mockResolvedValue(permissionsQueryResponse)], + [moveDesignMutation, moveDesignHandler], + ]; + + fakeApollo = createMockApollo(requestHandlers); + wrapper = shallowMount(Index, { + localVue, + apolloProvider: fakeApollo, + router, + stubs: { VueDraggable }, + }); + } + afterEach(() => { wrapper.destroy(); + wrapper = null; }); describe('designs', () => { @@ -478,16 +538,15 @@ describe('Design management index page', () => { describe('on non-latest version', () => { beforeEach(() => { createComponent({ designs: mockDesigns, allVersions: [mockVersion] }); + }); - router.replace({ + it('does not render design checkboxes', async () => { + await router.replace({ name: DESIGNS_ROUTE_NAME, query: { version: '2', }, }); - }); - - it('does not render design checkboxes', () => { expect(findDesignCheckboxes()).toHaveLength(0); }); @@ -514,13 +573,6 @@ describe('Design management index page', () => { files: [{ name: 'image.png', type: 'image/png' }], getData: () => 'test.png', }; - - router.replace({ - name: DESIGNS_ROUTE_NAME, - query: { - version: '2', - }, - }); }); it('does not call paste event if designs wrapper is not hovered', () => { @@ -587,7 +639,69 @@ describe('Design management index page', () => { }); createComponent(true); - expect(scrollIntoViewMock).toHaveBeenCalled(); + return wrapper.vm.$nextTick().then(() => { + expect(scrollIntoViewMock).toHaveBeenCalled(); + }); + }); + }); + + describe('with mocked Apollo client', () => { + it('has a design with id 1 as a first one', async () => { + createComponentWithApollo({}); + + await jest.runOnlyPendingTimers(); + await wrapper.vm.$nextTick(); + + expect(findDesigns()).toHaveLength(3); + expect( + findDesigns() + .at(0) + .props('id'), + ).toBe('1'); + }); + + it('calls a mutation with correct parameters and reorders designs', async () => { + createComponentWithApollo({}); + + await moveDesigns(wrapper); + + expect(moveDesignHandler).toHaveBeenCalled(); + + await wrapper.vm.$nextTick(); + + expect( + findDesigns() + .at(0) + .props('id'), + ).toBe('2'); + }); + + it('displays flash if mutation had a recoverable error', async () => { + createComponentWithApollo({ + moveHandler: jest.fn().mockResolvedValue(moveDesignMutationResponseWithErrors), + }); + + await moveDesigns(wrapper); + + await wrapper.vm.$nextTick(); + + expect(createFlash).toHaveBeenCalledWith('Houston, we have a problem'); + }); + + it('displays flash if mutation had a non-recoverable error', async () => { + createComponentWithApollo({ + moveHandler: jest.fn().mockRejectedValue('Error'), + }); + + await moveDesigns(wrapper); + + await wrapper.vm.$nextTick(); // kick off the DOM update + await jest.runOnlyPendingTimers(); // kick off the mocked GQL stuff (promises) + await wrapper.vm.$nextTick(); // kick off the DOM update for flash + + expect(createFlash).toHaveBeenCalledWith( + 'Something went wrong when reordering designs. Please try again', + ); }); }); }); diff --git a/spec/frontend/design_management/router_spec.js b/spec/frontend/design_management/router_spec.js index 2b8c7ee959b..d4cb9f75a77 100644 --- a/spec/frontend/design_management/router_spec.js +++ b/spec/frontend/design_management/router_spec.js @@ -35,11 +35,6 @@ function factory(routeArg) { }); } -jest.mock('mousetrap', () => ({ - bind: jest.fn(), - unbind: jest.fn(), -})); - describe('Design management router', () => { afterEach(() => { window.location.hash = ''; diff --git a/spec/frontend/design_management/utils/cache_update_spec.js b/spec/frontend/design_management/utils/cache_update_spec.js index e8a5cf3949d..6c859e8c3e8 100644 --- a/spec/frontend/design_management/utils/cache_update_spec.js +++ b/spec/frontend/design_management/utils/cache_update_spec.js @@ -1,14 +1,12 @@ import { InMemoryCache } from 'apollo-cache-inmemory'; import { updateStoreAfterDesignsDelete, - updateStoreAfterAddDiscussionComment, updateStoreAfterAddImageDiffNote, updateStoreAfterUploadDesign, updateStoreAfterUpdateImageDiffNote, } from '~/design_management/utils/cache_update'; import { designDeletionError, - ADD_DISCUSSION_COMMENT_ERROR, ADD_IMAGE_DIFF_NOTE_ERROR, UPDATE_IMAGE_DIFF_NOTE_ERROR, } from '~/design_management/utils/error_messages'; @@ -28,12 +26,11 @@ describe('Design Management cache update', () => { describe('error handling', () => { it.each` - fnName | subject | errorMessage | extraArgs - ${'updateStoreAfterDesignsDelete'} | ${updateStoreAfterDesignsDelete} | ${designDeletionError({ singular: true })} | ${[[design]]} - ${'updateStoreAfterAddDiscussionComment'} | ${updateStoreAfterAddDiscussionComment} | ${ADD_DISCUSSION_COMMENT_ERROR} | ${[]} - ${'updateStoreAfterAddImageDiffNote'} | ${updateStoreAfterAddImageDiffNote} | ${ADD_IMAGE_DIFF_NOTE_ERROR} | ${[]} - ${'updateStoreAfterUploadDesign'} | ${updateStoreAfterUploadDesign} | ${mockErrors[0]} | ${[]} - ${'updateStoreAfterUpdateImageDiffNote'} | ${updateStoreAfterUpdateImageDiffNote} | ${UPDATE_IMAGE_DIFF_NOTE_ERROR} | ${[]} + fnName | subject | errorMessage | extraArgs + ${'updateStoreAfterDesignsDelete'} | ${updateStoreAfterDesignsDelete} | ${designDeletionError({ singular: true })} | ${[[design]]} + ${'updateStoreAfterAddImageDiffNote'} | ${updateStoreAfterAddImageDiffNote} | ${ADD_IMAGE_DIFF_NOTE_ERROR} | ${[]} + ${'updateStoreAfterUploadDesign'} | ${updateStoreAfterUploadDesign} | ${mockErrors[0]} | ${[]} + ${'updateStoreAfterUpdateImageDiffNote'} | ${updateStoreAfterUpdateImageDiffNote} | ${UPDATE_IMAGE_DIFF_NOTE_ERROR} | ${[]} `('$fnName handles errors in response', ({ subject, extraArgs, errorMessage }) => { expect(createFlash).not.toHaveBeenCalled(); expect(() => subject(mockStore, { errors: mockErrors }, {}, ...extraArgs)).toThrow(); diff --git a/spec/frontend/design_management/utils/design_management_utils_spec.js b/spec/frontend/design_management/utils/design_management_utils_spec.js index e6d836b9157..7e857d08d25 100644 --- a/spec/frontend/design_management/utils/design_management_utils_spec.js +++ b/spec/frontend/design_management/utils/design_management_utils_spec.js @@ -6,6 +6,7 @@ import { updateImageDiffNoteOptimisticResponse, isValidDesignFile, extractDesign, + extractDesignNoteId, } from '~/design_management/utils/design_management_utils'; import mockResponseNoDesigns from '../mock_data/no_designs'; import mockResponseWithDesigns from '../mock_data/designs'; @@ -171,3 +172,19 @@ describe('extractDesign', () => { }); }); }); + +describe('extractDesignNoteId', () => { + it.each` + hash | expectedNoteId + ${'#note_0'} | ${'0'} + ${'#note_1'} | ${'1'} + ${'#note_23'} | ${'23'} + ${'#note_456'} | ${'456'} + ${'note_1'} | ${null} + ${'#note_'} | ${null} + ${'#note_asd'} | ${null} + ${'#note_1asd'} | ${null} + `('returns $expectedNoteId when hash is $hash', ({ hash, expectedNoteId }) => { + expect(extractDesignNoteId(hash)).toBe(expectedNoteId); + }); +}); |