diff options
Diffstat (limited to 'spec/frontend/design_management_new/components/design_notes')
6 files changed, 856 insertions, 0 deletions
diff --git a/spec/frontend/design_management_new/components/design_notes/__snapshots__/design_note_spec.js.snap b/spec/frontend/design_management_new/components/design_notes/__snapshots__/design_note_spec.js.snap new file mode 100644 index 00000000000..b55bacb6fc5 --- /dev/null +++ b/spec/frontend/design_management_new/components/design_notes/__snapshots__/design_note_spec.js.snap @@ -0,0 +1,67 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Design note component should match the snapshot 1`] = ` +<timeline-entry-item-stub + class="design-note note-form" + id="note_123" +> + <user-avatar-link-stub + imgalt="" + imgcssclasses="" + imgsize="40" + imgsrc="" + linkhref="" + tooltipplacement="top" + tooltiptext="" + username="" + /> + + <div + class="d-flex justify-content-between" + > + <div> + <a + class="js-user-link" + data-user-id="author-id" + > + <span + class="note-header-author-name bold" + > + + </span> + + <!----> + + <span + class="note-headline-light" + > + @ + </span> + </a> + + <span + class="note-headline-light note-headline-meta" + > + <span + class="system-note-message" + /> + + <!----> + </span> + </div> + + <div + class="gl-display-flex" + > + + <!----> + </div> + </div> + + <div + class="note-text js-note-text md" + data-qa-selector="note_content" + /> + +</timeline-entry-item-stub> +`; diff --git a/spec/frontend/design_management_new/components/design_notes/__snapshots__/design_reply_form_spec.js.snap b/spec/frontend/design_management_new/components/design_notes/__snapshots__/design_reply_form_spec.js.snap new file mode 100644 index 00000000000..e01c79e3520 --- /dev/null +++ b/spec/frontend/design_management_new/components/design_notes/__snapshots__/design_reply_form_spec.js.snap @@ -0,0 +1,15 @@ +// 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\\"> + <!----> + Comment +</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\\"> + <!----> + Save comment +</button>" +`; diff --git a/spec/frontend/design_management_new/components/design_notes/design_discussion_spec.js b/spec/frontend/design_management_new/components/design_notes/design_discussion_spec.js new file mode 100644 index 00000000000..401ce64e859 --- /dev/null +++ b/spec/frontend/design_management_new/components/design_notes/design_discussion_spec.js @@ -0,0 +1,322 @@ +import { mount } from '@vue/test-utils'; +import { GlLoadingIcon } from '@gitlab/ui'; +import notes from '../../mock_data/notes'; +import DesignDiscussion from '~/design_management_new/components/design_notes/design_discussion.vue'; +import DesignNote from '~/design_management_new/components/design_notes/design_note.vue'; +import DesignReplyForm from '~/design_management_new/components/design_notes/design_reply_form.vue'; +import createNoteMutation from '~/design_management_new/graphql/mutations/create_note.mutation.graphql'; +import toggleResolveDiscussionMutation from '~/design_management_new/graphql/mutations/toggle_resolve_discussion.mutation.graphql'; +import ReplyPlaceholder from '~/notes/components/discussion_reply_placeholder.vue'; +import ToggleRepliesWidget from '~/design_management_new/components/design_notes/toggle_replies_widget.vue'; + +const discussion = { + id: '0', + resolved: false, + resolvable: true, + notes, +}; + +describe('Design discussions component', () => { + let wrapper; + + const findDesignNotes = () => wrapper.findAll(DesignNote); + const findReplyPlaceholder = () => wrapper.find(ReplyPlaceholder); + const findReplyForm = () => wrapper.find(DesignReplyForm); + const findRepliesWidget = () => wrapper.find(ToggleRepliesWidget); + const findResolveButton = () => wrapper.find('[data-testid="resolve-button"]'); + const findResolveIcon = () => wrapper.find('[data-testid="resolve-icon"]'); + const findResolvedMessage = () => wrapper.find('[data-testid="resolved-message"]'); + const findResolveLoadingIcon = () => wrapper.find(GlLoadingIcon); + const findResolveCheckbox = () => wrapper.find('[data-testid="resolve-checkbox"]'); + + const mutationVariables = { + mutation: createNoteMutation, + update: expect.anything(), + variables: { + input: { + noteableId: 'noteable-id', + body: 'test', + discussionId: '0', + }, + }, + }; + const mutate = jest.fn(() => Promise.resolve()); + const $apollo = { + mutate, + }; + + function createComponent(props = {}, data = {}) { + wrapper = mount(DesignDiscussion, { + propsData: { + resolvedDiscussionsExpanded: true, + discussion, + noteableId: 'noteable-id', + designId: 'design-id', + discussionIndex: 1, + discussionWithOpenForm: '', + ...props, + }, + data() { + return { + ...data, + }; + }, + provide: { + projectPath: 'project-path', + issueIid: '1', + }, + mocks: { + $apollo, + $route: { + hash: '#note_1', + }, + }, + }); + } + + afterEach(() => { + wrapper.destroy(); + }); + + describe('when discussion is not resolvable', () => { + beforeEach(() => { + createComponent({ + discussion: { + ...discussion, + resolvable: false, + }, + }); + }); + + it('does not render an icon to resolve a thread', () => { + expect(findResolveIcon().exists()).toBe(false); + }); + + it('does not render a checkbox in reply form', () => { + findReplyPlaceholder().vm.$emit('onMouseDown'); + + return wrapper.vm.$nextTick().then(() => { + expect(findResolveCheckbox().exists()).toBe(false); + }); + }); + }); + + describe('when discussion is unresolved', () => { + beforeEach(() => { + createComponent(); + }); + + it('renders correct amount of discussion notes', () => { + expect(findDesignNotes()).toHaveLength(2); + expect(findDesignNotes().wrappers.every(w => w.isVisible())).toBe(true); + }); + + it('renders reply placeholder', () => { + expect(findReplyPlaceholder().isVisible()).toBe(true); + }); + + it('does not render toggle replies widget', () => { + expect(findRepliesWidget().exists()).toBe(false); + }); + + it('renders a correct icon to resolve a thread', () => { + expect(findResolveIcon().props('name')).toBe('check-circle'); + }); + + it('renders a checkbox with Resolve thread text in reply form', () => { + findReplyPlaceholder().vm.$emit('onClick'); + wrapper.setProps({ discussionWithOpenForm: discussion.id }); + + return wrapper.vm.$nextTick().then(() => { + expect(findResolveCheckbox().text()).toBe('Resolve thread'); + }); + }); + + it('does not render resolved message', () => { + expect(findResolvedMessage().exists()).toBe(false); + }); + }); + + describe('when discussion is resolved', () => { + beforeEach(() => { + createComponent({ + discussion: { + ...discussion, + resolved: true, + resolvedBy: notes[0].author, + resolvedAt: '2020-05-08T07:10:45Z', + }, + }); + }); + + it('shows only the first note', () => { + expect( + findDesignNotes() + .at(0) + .isVisible(), + ).toBe(true); + expect( + findDesignNotes() + .at(1) + .isVisible(), + ).toBe(false); + }); + + it('renders resolved message', () => { + expect(findResolvedMessage().exists()).toBe(true); + }); + + it('does not show renders reply placeholder', () => { + expect(findReplyPlaceholder().isVisible()).toBe(false); + }); + + it('renders toggle replies widget with correct props', () => { + expect(findRepliesWidget().exists()).toBe(true); + expect(findRepliesWidget().props()).toEqual({ + collapsed: true, + replies: notes.slice(1), + }); + }); + + it('renders a correct icon to resolve a thread', () => { + expect(findResolveIcon().props('name')).toBe('check-circle-filled'); + }); + + describe('when replies are expanded', () => { + beforeEach(() => { + findRepliesWidget().vm.$emit('toggle'); + return wrapper.vm.$nextTick(); + }); + + it('renders replies widget with collapsed prop equal to false', () => { + expect(findRepliesWidget().props('collapsed')).toBe(false); + }); + + it('renders the second note', () => { + expect( + findDesignNotes() + .at(1) + .isVisible(), + ).toBe(true); + }); + + it('renders a reply placeholder', () => { + expect(findReplyPlaceholder().isVisible()).toBe(true); + }); + + it('renders a checkbox with Unresolve thread text in reply form', () => { + findReplyPlaceholder().vm.$emit('onClick'); + wrapper.setProps({ discussionWithOpenForm: discussion.id }); + + return wrapper.vm.$nextTick().then(() => { + expect(findResolveCheckbox().text()).toBe('Unresolve thread'); + }); + }); + }); + }); + + it('hides reply placeholder and opens form on placeholder click', () => { + createComponent(); + findReplyPlaceholder().vm.$emit('onClick'); + wrapper.setProps({ discussionWithOpenForm: discussion.id }); + + return wrapper.vm.$nextTick().then(() => { + expect(findReplyPlaceholder().exists()).toBe(false); + expect(findReplyForm().exists()).toBe(true); + }); + }); + + it('calls mutation on submitting form and closes the form', () => { + createComponent( + { discussionWithOpenForm: discussion.id }, + { discussionComment: 'test', isFormRendered: true }, + ); + + findReplyForm().vm.$emit('submitForm'); + expect(mutate).toHaveBeenCalledWith(mutationVariables); + + return mutate() + .then(() => { + return wrapper.vm.$nextTick(); + }) + .then(() => { + expect(findReplyForm().exists()).toBe(false); + }); + }); + + it('clears the discussion comment on closing comment form', () => { + createComponent( + { discussionWithOpenForm: discussion.id }, + { discussionComment: 'test', isFormRendered: true }, + ); + + return wrapper.vm + .$nextTick() + .then(() => { + findReplyForm().vm.$emit('cancelForm'); + + expect(wrapper.vm.discussionComment).toBe(''); + return wrapper.vm.$nextTick(); + }) + .then(() => { + expect(findReplyForm().exists()).toBe(false); + }); + }); + + it('applies correct class to design notes when discussion is highlighted', () => { + createComponent( + {}, + { + activeDiscussion: { + id: notes[0].id, + source: 'pin', + }, + }, + ); + + expect(wrapper.findAll(DesignNote).wrappers.every(note => note.classes('gl-bg-blue-50'))).toBe( + true, + ); + }); + + it('calls toggleResolveDiscussion mutation on resolve thread button click', () => { + createComponent(); + findResolveButton().trigger('click'); + expect(mutate).toHaveBeenCalledWith({ + mutation: toggleResolveDiscussionMutation, + variables: { + id: discussion.id, + resolve: true, + }, + }); + return wrapper.vm.$nextTick(() => { + expect(findResolveLoadingIcon().exists()).toBe(true); + }); + }); + + it('calls toggleResolveDiscussion mutation after adding a note if checkbox was checked', () => { + createComponent( + { discussionWithOpenForm: discussion.id }, + { discussionComment: 'test', isFormRendered: true }, + ); + findResolveButton().trigger('click'); + findReplyForm().vm.$emit('submitForm'); + + return mutate().then(() => { + expect(mutate).toHaveBeenCalledWith({ + mutation: toggleResolveDiscussionMutation, + variables: { + id: discussion.id, + resolve: true, + }, + }); + }); + }); + + it('emits openForm event on opening the form', () => { + createComponent(); + findReplyPlaceholder().vm.$emit('onClick'); + + expect(wrapper.emitted('openForm')).toBeTruthy(); + }); +}); diff --git a/spec/frontend/design_management_new/components/design_notes/design_note_spec.js b/spec/frontend/design_management_new/components/design_notes/design_note_spec.js new file mode 100644 index 00000000000..b0e3e85b9c6 --- /dev/null +++ b/spec/frontend/design_management_new/components/design_notes/design_note_spec.js @@ -0,0 +1,170 @@ +import { shallowMount } from '@vue/test-utils'; +import { ApolloMutation } from 'vue-apollo'; +import DesignNote from '~/design_management_new/components/design_notes/design_note.vue'; +import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue'; +import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; +import DesignReplyForm from '~/design_management_new/components/design_notes/design_reply_form.vue'; + +const scrollIntoViewMock = jest.fn(); +const note = { + id: 'gid://gitlab/DiffNote/123', + author: { + id: 'author-id', + }, + body: 'test', + userPermissions: { + adminNote: false, + }, +}; +HTMLElement.prototype.scrollIntoView = scrollIntoViewMock; + +const $route = { + hash: '#note_123', +}; + +const mutate = jest.fn().mockResolvedValue({ data: { updateNote: {} } }); + +describe('Design note component', () => { + let wrapper; + + const findUserAvatar = () => wrapper.find(UserAvatarLink); + const findUserLink = () => wrapper.find('.js-user-link'); + const findReplyForm = () => wrapper.find(DesignReplyForm); + const findEditButton = () => wrapper.find('.js-note-edit'); + const findNoteContent = () => wrapper.find('.js-note-text'); + + function createComponent(props = {}, data = { isEditing: false }) { + wrapper = shallowMount(DesignNote, { + propsData: { + note: {}, + ...props, + }, + data() { + return { + ...data, + }; + }, + mocks: { + $route, + $apollo: { + mutate, + }, + }, + stubs: { + ApolloMutation, + }, + }); + } + + afterEach(() => { + wrapper.destroy(); + }); + + it('should match the snapshot', () => { + createComponent({ + note, + }); + + expect(wrapper.element).toMatchSnapshot(); + }); + + it('should render an author', () => { + createComponent({ + note, + }); + + expect(findUserAvatar().exists()).toBe(true); + expect(findUserLink().exists()).toBe(true); + }); + + 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(); + }); + + it('should not render edit icon when user does not have a permission', () => { + createComponent({ + note, + }); + + expect(findEditButton().exists()).toBe(false); + }); + + describe('when user has a permission to edit note', () => { + it('should open an edit form on edit button click', () => { + createComponent({ + note: { + ...note, + userPermissions: { + adminNote: true, + }, + }, + }); + + findEditButton().trigger('click'); + + return wrapper.vm.$nextTick().then(() => { + expect(findReplyForm().exists()).toBe(true); + expect(findNoteContent().exists()).toBe(false); + }); + }); + + describe('when edit form is rendered', () => { + beforeEach(() => { + createComponent( + { + note: { + ...note, + userPermissions: { + adminNote: true, + }, + }, + }, + { isEditing: true }, + ); + }); + + it('should not render note content and should render reply form', () => { + expect(findNoteContent().exists()).toBe(false); + expect(findReplyForm().exists()).toBe(true); + }); + + it('hides the form on hideForm event', () => { + findReplyForm().vm.$emit('cancelForm'); + + return wrapper.vm.$nextTick().then(() => { + expect(findReplyForm().exists()).toBe(false); + expect(findNoteContent().exists()).toBe(true); + }); + }); + + it('calls a mutation on submitForm event and hides a form', () => { + findReplyForm().vm.$emit('submitForm'); + expect(mutate).toHaveBeenCalled(); + + return mutate() + .then(() => { + return wrapper.vm.$nextTick(); + }) + .then(() => { + expect(findReplyForm().exists()).toBe(false); + expect(findNoteContent().exists()).toBe(true); + }); + }); + }); + }); +}); diff --git a/spec/frontend/design_management_new/components/design_notes/design_reply_form_spec.js b/spec/frontend/design_management_new/components/design_notes/design_reply_form_spec.js new file mode 100644 index 00000000000..9c1d6154516 --- /dev/null +++ b/spec/frontend/design_management_new/components/design_notes/design_reply_form_spec.js @@ -0,0 +1,184 @@ +import { mount } from '@vue/test-utils'; +import DesignReplyForm from '~/design_management_new/components/design_notes/design_reply_form.vue'; + +const showModal = jest.fn(); + +const GlModal = { + template: '<div><slot name="modal-title"></slot><slot></slot><slot name="modal-ok"></slot></div>', + methods: { + show: showModal, + }, +}; + +describe('Design reply form component', () => { + let wrapper; + + const findTextarea = () => wrapper.find('textarea'); + const findSubmitButton = () => wrapper.find({ ref: 'submitButton' }); + const findCancelButton = () => wrapper.find({ ref: 'cancelButton' }); + const findModal = () => wrapper.find({ ref: 'cancelCommentModal' }); + + function createComponent(props = {}, mountOptions = {}) { + wrapper = mount(DesignReplyForm, { + propsData: { + value: '', + isSaving: false, + ...props, + }, + stubs: { GlModal }, + ...mountOptions, + }); + } + + afterEach(() => { + wrapper.destroy(); + }); + + it('textarea has focus after component mount', () => { + // We need to attach to document, so that `document.activeElement` is properly set in jsdom + createComponent({}, { attachToDocument: true }); + + expect(findTextarea().element).toEqual(document.activeElement); + }); + + it('renders button text as "Comment" when creating a comment', () => { + createComponent(); + + expect(findSubmitButton().html()).toMatchSnapshot(); + }); + + it('renders button text as "Save comment" when creating a comment', () => { + createComponent({ isNewComment: false }); + + expect(findSubmitButton().html()).toMatchSnapshot(); + }); + + describe('when form has no text', () => { + beforeEach(() => { + createComponent({ + value: '', + }); + }); + + it('submit button is disabled', () => { + expect(findSubmitButton().attributes().disabled).toBeTruthy(); + }); + + it('does not emit submitForm event on textarea ctrl+enter keydown', () => { + findTextarea().trigger('keydown.enter', { + ctrlKey: true, + }); + + return wrapper.vm.$nextTick().then(() => { + expect(wrapper.emitted('submitForm')).toBeFalsy(); + }); + }); + + it('does not emit submitForm event on textarea meta+enter keydown', () => { + findTextarea().trigger('keydown.enter', { + metaKey: true, + }); + + return wrapper.vm.$nextTick().then(() => { + expect(wrapper.emitted('submitForm')).toBeFalsy(); + }); + }); + + it('emits cancelForm event on pressing escape button on textarea', () => { + findTextarea().trigger('keyup.esc'); + + expect(wrapper.emitted('cancelForm')).toBeTruthy(); + }); + + it('emits cancelForm event on clicking Cancel button', () => { + findCancelButton().vm.$emit('click'); + + expect(wrapper.emitted('cancelForm')).toHaveLength(1); + }); + }); + + describe('when form has text', () => { + beforeEach(() => { + createComponent({ + value: 'test', + }); + }); + + it('submit button is enabled', () => { + expect(findSubmitButton().attributes().disabled).toBeFalsy(); + }); + + it('emits submitForm event on Comment button click', () => { + findSubmitButton().vm.$emit('click'); + + return wrapper.vm.$nextTick().then(() => { + expect(wrapper.emitted('submitForm')).toBeTruthy(); + }); + }); + + it('emits submitForm event on textarea ctrl+enter keydown', () => { + findTextarea().trigger('keydown.enter', { + ctrlKey: true, + }); + + return wrapper.vm.$nextTick().then(() => { + expect(wrapper.emitted('submitForm')).toBeTruthy(); + }); + }); + + it('emits submitForm event on textarea meta+enter keydown', () => { + findTextarea().trigger('keydown.enter', { + metaKey: true, + }); + + return wrapper.vm.$nextTick().then(() => { + expect(wrapper.emitted('submitForm')).toBeTruthy(); + }); + }); + + it('emits input event on changing textarea content', () => { + findTextarea().setValue('test2'); + + return wrapper.vm.$nextTick().then(() => { + expect(wrapper.emitted('input')).toBeTruthy(); + }); + }); + + it('emits cancelForm event on Escape key if text was not changed', () => { + findTextarea().trigger('keyup.esc'); + + expect(wrapper.emitted('cancelForm')).toBeTruthy(); + }); + + it('opens confirmation modal on Escape key when text has changed', () => { + wrapper.setProps({ value: 'test2' }); + + return wrapper.vm.$nextTick().then(() => { + findTextarea().trigger('keyup.esc'); + expect(showModal).toHaveBeenCalled(); + }); + }); + + it('emits cancelForm event on Cancel button click if text was not changed', () => { + findCancelButton().trigger('click'); + + expect(wrapper.emitted('cancelForm')).toBeTruthy(); + }); + + it('opens confirmation modal on Cancel button click when text has changed', () => { + wrapper.setProps({ value: 'test2' }); + + return wrapper.vm.$nextTick().then(() => { + findCancelButton().trigger('click'); + expect(showModal).toHaveBeenCalled(); + }); + }); + + it('emits cancelForm event on modal Ok button click', () => { + findTextarea().trigger('keyup.esc'); + findModal().vm.$emit('ok'); + + expect(wrapper.emitted('cancelForm')).toBeTruthy(); + }); + }); +}); diff --git a/spec/frontend/design_management_new/components/design_notes/toggle_replies_widget_spec.js b/spec/frontend/design_management_new/components/design_notes/toggle_replies_widget_spec.js new file mode 100644 index 00000000000..d3c89075a24 --- /dev/null +++ b/spec/frontend/design_management_new/components/design_notes/toggle_replies_widget_spec.js @@ -0,0 +1,98 @@ +import { shallowMount } from '@vue/test-utils'; +import { GlIcon, GlButton, GlLink } from '@gitlab/ui'; +import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; +import ToggleRepliesWidget from '~/design_management_new/components/design_notes/toggle_replies_widget.vue'; +import notes from '../../mock_data/notes'; + +describe('Toggle replies widget component', () => { + let wrapper; + + const findToggleWrapper = () => wrapper.find('[data-testid="toggle-comments-wrapper"]'); + const findIcon = () => wrapper.find(GlIcon); + const findButton = () => wrapper.find(GlButton); + const findAuthorLink = () => wrapper.find(GlLink); + const findTimeAgo = () => wrapper.find(TimeAgoTooltip); + + function createComponent(props = {}) { + wrapper = shallowMount(ToggleRepliesWidget, { + propsData: { + collapsed: true, + replies: notes, + ...props, + }, + }); + } + + afterEach(() => { + wrapper.destroy(); + }); + + describe('when replies are collapsed', () => { + beforeEach(() => { + createComponent(); + }); + + it('should not have expanded class', () => { + expect(findToggleWrapper().classes()).not.toContain('expanded'); + }); + + it('should render chevron-right icon', () => { + expect(findIcon().props('name')).toBe('chevron-right'); + }); + + it('should have replies length on button', () => { + expect(findButton().text()).toBe('2 replies'); + }); + + it('should render a link to the last reply author', () => { + expect(findAuthorLink().exists()).toBe(true); + expect(findAuthorLink().text()).toBe(notes[1].author.name); + expect(findAuthorLink().attributes('href')).toBe(notes[1].author.webUrl); + }); + + it('should render correct time ago tooltip', () => { + expect(findTimeAgo().exists()).toBe(true); + expect(findTimeAgo().props('time')).toBe(notes[1].createdAt); + }); + }); + + describe('when replies are expanded', () => { + beforeEach(() => { + createComponent({ collapsed: false }); + }); + + it('should have expanded class', () => { + expect(findToggleWrapper().classes()).toContain('expanded'); + }); + + it('should render chevron-down icon', () => { + expect(findIcon().props('name')).toBe('chevron-down'); + }); + + it('should have Collapse replies text on button', () => { + expect(findButton().text()).toBe('Collapse replies'); + }); + + it('should not have a link to the last reply author', () => { + expect(findAuthorLink().exists()).toBe(false); + }); + + it('should not render time ago tooltip', () => { + expect(findTimeAgo().exists()).toBe(false); + }); + }); + + it('should emit toggle event on icon click', () => { + createComponent(); + findIcon().vm.$emit('click', new MouseEvent('click')); + + expect(wrapper.emitted('toggle')).toHaveLength(1); + }); + + it('should emit toggle event on button click', () => { + createComponent(); + findButton().vm.$emit('click', new MouseEvent('click')); + + expect(wrapper.emitted('toggle')).toHaveLength(1); + }); +}); |