diff options
Diffstat (limited to 'spec/frontend/vue_shared/issuable/show')
7 files changed, 983 insertions, 0 deletions
diff --git a/spec/frontend/vue_shared/issuable/show/components/issuable_body_spec.js b/spec/frontend/vue_shared/issuable/show/components/issuable_body_spec.js new file mode 100644 index 00000000000..41bacf18a68 --- /dev/null +++ b/spec/frontend/vue_shared/issuable/show/components/issuable_body_spec.js @@ -0,0 +1,236 @@ +import { shallowMount } from '@vue/test-utils'; +import { useFakeDate } from 'helpers/fake_date'; + +import IssuableBody from '~/vue_shared/issuable/show/components/issuable_body.vue'; + +import IssuableDescription from '~/vue_shared/issuable/show/components/issuable_description.vue'; +import IssuableEditForm from '~/vue_shared/issuable/show/components/issuable_edit_form.vue'; +import IssuableTitle from '~/vue_shared/issuable/show/components/issuable_title.vue'; +import TaskList from '~/task_list'; +import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; + +import { mockIssuableShowProps, mockIssuable } from '../mock_data'; + +jest.mock('~/autosave'); +jest.mock('~/flash'); + +const issuableBodyProps = { + ...mockIssuableShowProps, + issuable: mockIssuable, +}; + +const createComponent = (propsData = issuableBodyProps) => + shallowMount(IssuableBody, { + propsData, + stubs: { + IssuableTitle, + IssuableDescription, + IssuableEditForm, + TimeAgoTooltip, + }, + slots: { + 'status-badge': 'Open', + 'edit-form-actions': ` + <button class="js-save">Save changes</button> + <button class="js-cancel">Cancel</button> + `, + }, + }); + +describe('IssuableBody', () => { + // Some assertions expect a date later than our default + useFakeDate(2020, 11, 11); + + let wrapper; + + beforeEach(() => { + wrapper = createComponent(); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + describe('computed', () => { + describe('isUpdated', () => { + it.each` + updatedAt | returnValue + ${mockIssuable.updatedAt} | ${true} + ${null} | ${false} + ${''} | ${false} + `( + 'returns $returnValue when value of `updateAt` prop is `$updatedAt`', + async ({ updatedAt, returnValue }) => { + wrapper.setProps({ + issuable: { + ...mockIssuable, + updatedAt, + }, + }); + + await wrapper.vm.$nextTick(); + + expect(wrapper.vm.isUpdated).toBe(returnValue); + }, + ); + }); + + describe('updatedBy', () => { + it('returns value of `issuable.updatedBy`', () => { + expect(wrapper.vm.updatedBy).toBe(mockIssuable.updatedBy); + }); + }); + }); + + describe('watchers', () => { + describe('editFormVisible', () => { + it('calls initTaskList in nextTick', async () => { + jest.spyOn(wrapper.vm, 'initTaskList'); + wrapper.setProps({ + editFormVisible: true, + }); + + await wrapper.vm.$nextTick(); + + wrapper.setProps({ + editFormVisible: false, + }); + + await wrapper.vm.$nextTick(); + + expect(wrapper.vm.initTaskList).toHaveBeenCalled(); + }); + }); + }); + + describe('mounted', () => { + it('initializes TaskList instance when enabledEdit and enableTaskList props are true', () => { + expect(wrapper.vm.taskList instanceof TaskList).toBe(true); + expect(wrapper.vm.taskList).toMatchObject({ + dataType: 'issue', + fieldName: 'description', + lockVersion: issuableBodyProps.taskListLockVersion, + selector: '.js-detail-page-description', + onSuccess: expect.any(Function), + onError: expect.any(Function), + }); + }); + + it('does not initialize TaskList instance when either enabledEdit or enableTaskList prop is false', () => { + const wrapperNoTaskList = createComponent({ + ...issuableBodyProps, + enableTaskList: false, + }); + + expect(wrapperNoTaskList.vm.taskList).not.toBeDefined(); + + wrapperNoTaskList.destroy(); + }); + }); + + describe('methods', () => { + describe('handleTaskListUpdateSuccess', () => { + it('emits `task-list-update-success` event on component', () => { + const updatedIssuable = { + foo: 'bar', + }; + + wrapper.vm.handleTaskListUpdateSuccess(updatedIssuable); + + expect(wrapper.emitted('task-list-update-success')).toBeTruthy(); + expect(wrapper.emitted('task-list-update-success')[0]).toEqual([updatedIssuable]); + }); + }); + + describe('handleTaskListUpdateFailure', () => { + it('emits `task-list-update-failure` event on component', () => { + wrapper.vm.handleTaskListUpdateFailure(); + + expect(wrapper.emitted('task-list-update-failure')).toBeTruthy(); + }); + }); + }); + + describe('template', () => { + it('renders issuable-title component', () => { + const titleEl = wrapper.find(IssuableTitle); + + expect(titleEl.exists()).toBe(true); + expect(titleEl.props()).toMatchObject({ + issuable: issuableBodyProps.issuable, + statusBadgeClass: issuableBodyProps.statusBadgeClass, + statusIcon: issuableBodyProps.statusIcon, + enableEdit: issuableBodyProps.enableEdit, + }); + }); + + it('renders issuable-description component', () => { + const descriptionEl = wrapper.find(IssuableDescription); + + expect(descriptionEl.exists()).toBe(true); + expect(descriptionEl.props('issuable')).toEqual(issuableBodyProps.issuable); + }); + + it('renders issuable edit info', () => { + const editedEl = wrapper.find('small'); + + expect(editedEl.text()).toMatchInterpolatedText('Edited 3 months ago by Administrator'); + }); + + it('renders issuable-edit-form when `editFormVisible` prop is true', async () => { + wrapper.setProps({ + editFormVisible: true, + }); + + await wrapper.vm.$nextTick(); + + const editFormEl = wrapper.find(IssuableEditForm); + expect(editFormEl.exists()).toBe(true); + expect(editFormEl.props()).toMatchObject({ + issuable: issuableBodyProps.issuable, + enableAutocomplete: issuableBodyProps.enableAutocomplete, + descriptionPreviewPath: issuableBodyProps.descriptionPreviewPath, + descriptionHelpPath: issuableBodyProps.descriptionHelpPath, + }); + expect(editFormEl.find('button.js-save').exists()).toBe(true); + expect(editFormEl.find('button.js-cancel').exists()).toBe(true); + }); + + describe('events', () => { + it('component emits `edit-issuable` event bubbled via issuable-title', () => { + const issuableTitle = wrapper.find(IssuableTitle); + + issuableTitle.vm.$emit('edit-issuable'); + + expect(wrapper.emitted('edit-issuable')).toBeTruthy(); + }); + + it.each(['keydown-title', 'keydown-description'])( + 'component emits `%s` event with event object and issuableMeta params via issuable-edit-form', + async (eventName) => { + const eventObj = { + preventDefault: jest.fn(), + stopPropagation: jest.fn(), + }; + const issuableMeta = { + issuableTitle: 'foo', + issuableDescription: 'foobar', + }; + + wrapper.setProps({ + editFormVisible: true, + }); + + await wrapper.vm.$nextTick(); + + const issuableEditForm = wrapper.find(IssuableEditForm); + + issuableEditForm.vm.$emit(eventName, eventObj, issuableMeta); + + expect(wrapper.emitted(eventName)).toBeTruthy(); + expect(wrapper.emitted(eventName)[0]).toMatchObject([eventObj, issuableMeta]); + }, + ); + }); + }); +}); diff --git a/spec/frontend/vue_shared/issuable/show/components/issuable_description_spec.js b/spec/frontend/vue_shared/issuable/show/components/issuable_description_spec.js new file mode 100644 index 00000000000..f2211e5b2bb --- /dev/null +++ b/spec/frontend/vue_shared/issuable/show/components/issuable_description_spec.js @@ -0,0 +1,69 @@ +import { shallowMount } from '@vue/test-utils'; +import $ from 'jquery'; + +import IssuableDescription from '~/vue_shared/issuable/show/components/issuable_description.vue'; + +import { mockIssuable } from '../mock_data'; + +const createComponent = ({ + issuable = mockIssuable, + enableTaskList = true, + canEdit = true, + taskListUpdatePath = `${mockIssuable.webUrl}.json`, +} = {}) => + shallowMount(IssuableDescription, { + propsData: { issuable, enableTaskList, canEdit, taskListUpdatePath }, + }); + +describe('IssuableDescription', () => { + let renderGFMSpy; + let wrapper; + + beforeEach(() => { + renderGFMSpy = jest.spyOn($.fn, 'renderGFM'); + wrapper = createComponent(); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + describe('mounted', () => { + it('calls `renderGFM`', () => { + expect(renderGFMSpy).toHaveBeenCalledTimes(1); + }); + }); + + describe('methods', () => { + describe('renderGFM', () => { + it('calls `renderGFM` on container element', () => { + wrapper.vm.renderGFM(); + + expect(renderGFMSpy).toHaveBeenCalled(); + }); + }); + }); + + describe('templates', () => { + it('renders container element with class `js-task-list-container` when canEdit and enableTaskList props are true', () => { + expect(wrapper.classes()).toContain('js-task-list-container'); + }); + + it('renders container element without class `js-task-list-container` when canEdit and enableTaskList props are true', () => { + const wrapperNoTaskList = createComponent({ + enableTaskList: false, + }); + + expect(wrapperNoTaskList.classes()).not.toContain('js-task-list-container'); + + wrapperNoTaskList.destroy(); + }); + + it('renders hidden textarea element when issuable.description is present and enableTaskList prop is true', () => { + const textareaEl = wrapper.find('textarea.gl-display-none.js-task-list-field'); + + expect(textareaEl.exists()).toBe(true); + expect(textareaEl.attributes('data-update-url')).toBe(`${mockIssuable.webUrl}.json`); + }); + }); +}); diff --git a/spec/frontend/vue_shared/issuable/show/components/issuable_edit_form_spec.js b/spec/frontend/vue_shared/issuable/show/components/issuable_edit_form_spec.js new file mode 100644 index 00000000000..051ffd27af4 --- /dev/null +++ b/spec/frontend/vue_shared/issuable/show/components/issuable_edit_form_spec.js @@ -0,0 +1,193 @@ +import { GlFormInput } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; + +import IssuableEditForm from '~/vue_shared/issuable/show/components/issuable_edit_form.vue'; +import IssuableEventHub from '~/vue_shared/issuable/show/event_hub'; +import MarkdownField from '~/vue_shared/components/markdown/field.vue'; + +import { mockIssuableShowProps, mockIssuable } from '../mock_data'; + +const issuableEditFormProps = { + issuable: mockIssuable, + ...mockIssuableShowProps, +}; + +const createComponent = ({ propsData = issuableEditFormProps } = {}) => + shallowMount(IssuableEditForm, { + propsData, + stubs: { + MarkdownField, + }, + slots: { + 'edit-form-actions': ` + <button class="js-save">Save changes</button> + <button class="js-cancel">Cancel</button> + `, + }, + }); + +describe('IssuableEditForm', () => { + let wrapper; + const assertEvent = (eventSpy) => { + expect(eventSpy).toHaveBeenNthCalledWith(1, 'update.issuable', expect.any(Function)); + expect(eventSpy).toHaveBeenNthCalledWith(2, 'close.form', expect.any(Function)); + }; + + beforeEach(() => { + wrapper = createComponent(); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + describe('watch', () => { + describe('issuable', () => { + it('sets title and description to `issuable.title` and `issuable.description` when those values are available', async () => { + wrapper.setProps({ + issuable: { + ...issuableEditFormProps.issuable, + title: 'Foo', + description: 'Foobar', + }, + }); + + await wrapper.vm.$nextTick(); + + expect(wrapper.vm.title).toBe('Foo'); + expect(wrapper.vm.description).toBe('Foobar'); + }); + + it('sets title and description to empty string when `issuable.title` and `issuable.description` is unavailable', async () => { + wrapper.setProps({ + issuable: { + ...issuableEditFormProps.issuable, + title: null, + description: null, + }, + }); + + await wrapper.vm.$nextTick(); + + expect(wrapper.vm.title).toBe(''); + expect(wrapper.vm.description).toBe(''); + }); + }); + }); + + describe('created', () => { + it('binds `update.issuable` and `close.form` event listeners', () => { + const eventOnSpy = jest.spyOn(IssuableEventHub, '$on'); + const wrapperTemp = createComponent(); + + assertEvent(eventOnSpy); + + wrapperTemp.destroy(); + }); + }); + + describe('beforeDestroy', () => { + it('unbinds `update.issuable` and `close.form` event listeners', () => { + const wrapperTemp = createComponent(); + const eventOffSpy = jest.spyOn(IssuableEventHub, '$off'); + + wrapperTemp.destroy(); + + assertEvent(eventOffSpy); + }); + }); + + describe('methods', () => { + describe('initAutosave', () => { + it('initializes `autosaveTitle` and `autosaveDescription` props', () => { + expect(wrapper.vm.autosaveTitle).toBeDefined(); + expect(wrapper.vm.autosaveDescription).toBeDefined(); + }); + }); + + describe('resetAutosave', () => { + it('calls `reset` on `autosaveTitle` and `autosaveDescription` props', () => { + jest.spyOn(wrapper.vm.autosaveTitle, 'reset').mockImplementation(jest.fn); + jest.spyOn(wrapper.vm.autosaveDescription, 'reset').mockImplementation(jest.fn); + + wrapper.vm.resetAutosave(); + + expect(wrapper.vm.autosaveTitle.reset).toHaveBeenCalled(); + expect(wrapper.vm.autosaveDescription.reset).toHaveBeenCalled(); + }); + }); + }); + + describe('template', () => { + it('renders title input field', () => { + const titleInputEl = wrapper.find('[data-testid="title"]'); + + expect(titleInputEl.exists()).toBe(true); + expect(titleInputEl.find(GlFormInput).attributes()).toMatchObject({ + 'aria-label': 'Title', + placeholder: 'Title', + }); + }); + + it('renders description textarea field', () => { + const descriptionEl = wrapper.find('[data-testid="description"]'); + + expect(descriptionEl.exists()).toBe(true); + expect(descriptionEl.find(MarkdownField).props()).toMatchObject({ + markdownPreviewPath: issuableEditFormProps.descriptionPreviewPath, + markdownDocsPath: issuableEditFormProps.descriptionHelpPath, + enableAutocomplete: issuableEditFormProps.enableAutocomplete, + textareaValue: mockIssuable.description, + }); + expect(descriptionEl.find('textarea').attributes()).toMatchObject({ + 'data-supports-quick-actions': 'true', + 'aria-label': 'Description', + placeholder: 'Write a comment or drag your files hereā¦', + }); + }); + + it('renders form actions', () => { + const actionsEl = wrapper.find('[data-testid="actions"]'); + + expect(actionsEl.find('button.js-save').exists()).toBe(true); + expect(actionsEl.find('button.js-cancel').exists()).toBe(true); + }); + + describe('events', () => { + const eventObj = { + preventDefault: jest.fn(), + stopPropagation: jest.fn(), + }; + + it('component emits `keydown-title` event with event object and issuableMeta params via gl-form-input', async () => { + const titleInputEl = wrapper.find(GlFormInput); + + titleInputEl.vm.$emit('keydown', eventObj, 'title'); + + expect(wrapper.emitted('keydown-title')).toBeTruthy(); + expect(wrapper.emitted('keydown-title')[0]).toMatchObject([ + eventObj, + { + issuableTitle: wrapper.vm.title, + issuableDescription: wrapper.vm.description, + }, + ]); + }); + + it('component emits `keydown-description` event with event object and issuableMeta params via textarea', async () => { + const descriptionInputEl = wrapper.find('[data-testid="description"] textarea'); + + descriptionInputEl.trigger('keydown', eventObj, 'description'); + + expect(wrapper.emitted('keydown-description')).toBeTruthy(); + expect(wrapper.emitted('keydown-description')[0]).toMatchObject([ + eventObj, + { + issuableTitle: wrapper.vm.title, + issuableDescription: wrapper.vm.description, + }, + ]); + }); + }); + }); +}); diff --git a/spec/frontend/vue_shared/issuable/show/components/issuable_header_spec.js b/spec/frontend/vue_shared/issuable/show/components/issuable_header_spec.js new file mode 100644 index 00000000000..41735923957 --- /dev/null +++ b/spec/frontend/vue_shared/issuable/show/components/issuable_header_spec.js @@ -0,0 +1,182 @@ +import { GlIcon, GlAvatarLabeled } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import { extendedWrapper } from 'helpers/vue_test_utils_helper'; + +import IssuableHeader from '~/vue_shared/issuable/show/components/issuable_header.vue'; + +import { mockIssuableShowProps, mockIssuable } from '../mock_data'; + +const issuableHeaderProps = { + ...mockIssuable, + ...mockIssuableShowProps, +}; + +const createComponent = (propsData = issuableHeaderProps, { stubs } = {}) => + extendedWrapper( + shallowMount(IssuableHeader, { + propsData, + slots: { + 'status-badge': 'Open', + 'header-actions': ` + <button class="js-close">Close issuable</button> + <a class="js-new" href="/gitlab-org/gitlab-shell/-/issues/new">New issuable</a> + `, + }, + stubs, + }), + ); + +describe('IssuableHeader', () => { + let wrapper; + + beforeEach(() => { + wrapper = createComponent(); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + describe('computed', () => { + describe('authorId', () => { + it('returns numeric ID from GraphQL ID of `author` prop', () => { + expect(wrapper.vm.authorId).toBe(1); + }); + }); + }); + + describe('handleRightSidebarToggleClick', () => { + beforeEach(() => { + setFixtures('<button class="js-toggle-right-sidebar-button">Collapse sidebar</button>'); + }); + + it('dispatches `click` event on sidebar toggle button', () => { + wrapper.vm.toggleSidebarButtonEl = document.querySelector('.js-toggle-right-sidebar-button'); + jest.spyOn(wrapper.vm.toggleSidebarButtonEl, 'dispatchEvent').mockImplementation(jest.fn); + + wrapper.vm.handleRightSidebarToggleClick(); + + expect(wrapper.vm.toggleSidebarButtonEl.dispatchEvent).toHaveBeenCalledWith( + expect.objectContaining({ + type: 'click', + }), + ); + }); + }); + + describe('template', () => { + it('renders issuable status icon and text', () => { + const statusBoxEl = wrapper.findByTestId('status'); + + expect(statusBoxEl.exists()).toBe(true); + expect(statusBoxEl.find(GlIcon).props('name')).toBe(mockIssuableShowProps.statusIcon); + expect(statusBoxEl.text()).toContain('Open'); + }); + + it('renders blocked icon when issuable is blocked', async () => { + wrapper.setProps({ + blocked: true, + }); + + await wrapper.vm.$nextTick(); + + const blockedEl = wrapper.findByTestId('blocked'); + + expect(blockedEl.exists()).toBe(true); + expect(blockedEl.find(GlIcon).props('name')).toBe('lock'); + }); + + it('renders confidential icon when issuable is confidential', async () => { + wrapper.setProps({ + confidential: true, + }); + + await wrapper.vm.$nextTick(); + + const confidentialEl = wrapper.findByTestId('confidential'); + + expect(confidentialEl.exists()).toBe(true); + expect(confidentialEl.find(GlIcon).props('name')).toBe('eye-slash'); + }); + + it('renders issuable author avatar', () => { + const { username, name, webUrl, avatarUrl } = mockIssuable.author; + const avatarElAttrs = { + 'data-user-id': '1', + 'data-username': username, + 'data-name': name, + href: webUrl, + target: '_blank', + }; + const avatarEl = wrapper.findByTestId('avatar'); + expect(avatarEl.exists()).toBe(true); + expect(avatarEl.attributes()).toMatchObject(avatarElAttrs); + expect(avatarEl.find(GlAvatarLabeled).attributes()).toMatchObject({ + size: '24', + src: avatarUrl, + label: name, + }); + expect(avatarEl.find(GlAvatarLabeled).find(GlIcon).exists()).toBe(false); + }); + + it('renders tast status text when `taskCompletionStatus` prop is defined', () => { + let taskStatusEl = wrapper.findByTestId('task-status'); + + expect(taskStatusEl.exists()).toBe(true); + expect(taskStatusEl.text()).toContain('0 of 5 tasks completed'); + + const wrapperSingleTask = createComponent({ + ...issuableHeaderProps, + taskCompletionStatus: { + completedCount: 0, + count: 1, + }, + }); + + taskStatusEl = wrapperSingleTask.findByTestId('task-status'); + + expect(taskStatusEl.text()).toContain('0 of 1 task completed'); + + wrapperSingleTask.destroy(); + }); + + it('renders sidebar toggle button', () => { + const toggleButtonEl = wrapper.findByTestId('sidebar-toggle'); + + expect(toggleButtonEl.exists()).toBe(true); + expect(toggleButtonEl.props('icon')).toBe('chevron-double-lg-left'); + }); + + it('renders header actions', () => { + const actionsEl = wrapper.findByTestId('header-actions'); + + expect(actionsEl.find('button.js-close').exists()).toBe(true); + expect(actionsEl.find('a.js-new').exists()).toBe(true); + }); + + describe('when author exists outside of GitLab', () => { + it("renders 'external-link' icon in avatar label", () => { + wrapper = createComponent( + { + ...issuableHeaderProps, + author: { + ...issuableHeaderProps.author, + webUrl: 'https://jira.com/test-user/author.jpg', + }, + }, + { + stubs: { + GlAvatarLabeled, + }, + }, + ); + + const avatarEl = wrapper.findComponent(GlAvatarLabeled); + const icon = avatarEl.find(GlIcon); + + expect(icon.exists()).toBe(true); + expect(icon.props('name')).toBe('external-link'); + }); + }); + }); +}); diff --git a/spec/frontend/vue_shared/issuable/show/components/issuable_show_root_spec.js b/spec/frontend/vue_shared/issuable/show/components/issuable_show_root_spec.js new file mode 100644 index 00000000000..d1eb1366225 --- /dev/null +++ b/spec/frontend/vue_shared/issuable/show/components/issuable_show_root_spec.js @@ -0,0 +1,158 @@ +import { shallowMount } from '@vue/test-utils'; + +import IssuableBody from '~/vue_shared/issuable/show/components/issuable_body.vue'; +import IssuableHeader from '~/vue_shared/issuable/show/components/issuable_header.vue'; +import IssuableShowRoot from '~/vue_shared/issuable/show/components/issuable_show_root.vue'; + +import IssuableSidebar from '~/vue_shared/issuable/sidebar/components/issuable_sidebar_root.vue'; + +import { mockIssuableShowProps, mockIssuable } from '../mock_data'; + +const createComponent = (propsData = mockIssuableShowProps) => + shallowMount(IssuableShowRoot, { + propsData, + stubs: { + IssuableHeader, + IssuableBody, + IssuableSidebar, + }, + slots: { + 'status-badge': 'Open', + 'header-actions': ` + <button class="js-close">Close issuable</button> + <a class="js-new" href="/gitlab-org/gitlab-shell/-/issues/new">New issuable</a> + `, + 'edit-form-actions': ` + <button class="js-save">Save changes</button> + <button class="js-cancel">Cancel</button> + `, + 'right-sidebar-items': ` + <div class="js-todo"> + To Do <button class="js-add-todo">Add a To Do</button> + </div> + `, + }, + }); + +describe('IssuableShowRoot', () => { + let wrapper; + + beforeEach(() => { + wrapper = createComponent(); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + describe('template', () => { + const { + statusBadgeClass, + statusIcon, + enableEdit, + enableAutocomplete, + editFormVisible, + descriptionPreviewPath, + descriptionHelpPath, + taskCompletionStatus, + } = mockIssuableShowProps; + const { blocked, confidential, createdAt, author } = mockIssuable; + + it('renders component container element with class `issuable-show-container`', () => { + expect(wrapper.classes()).toContain('issuable-show-container'); + }); + + it('renders issuable-header component', () => { + const issuableHeader = wrapper.find(IssuableHeader); + + expect(issuableHeader.exists()).toBe(true); + expect(issuableHeader.props()).toMatchObject({ + statusBadgeClass, + statusIcon, + blocked, + confidential, + createdAt, + author, + taskCompletionStatus, + }); + expect(issuableHeader.find('.issuable-status-box').text()).toContain('Open'); + expect(issuableHeader.find('.detail-page-header-actions button.js-close').exists()).toBe( + true, + ); + expect(issuableHeader.find('.detail-page-header-actions a.js-new').exists()).toBe(true); + }); + + it('renders issuable-body component', () => { + const issuableBody = wrapper.find(IssuableBody); + + expect(issuableBody.exists()).toBe(true); + expect(issuableBody.props()).toMatchObject({ + issuable: mockIssuable, + statusBadgeClass, + statusIcon, + enableEdit, + enableAutocomplete, + editFormVisible, + descriptionPreviewPath, + descriptionHelpPath, + }); + }); + + it('renders issuable-sidebar component', () => { + const issuableSidebar = wrapper.find(IssuableSidebar); + + expect(issuableSidebar.exists()).toBe(true); + }); + + describe('events', () => { + it('component emits `edit-issuable` event bubbled via issuable-body', () => { + const issuableBody = wrapper.find(IssuableBody); + + issuableBody.vm.$emit('edit-issuable'); + + expect(wrapper.emitted('edit-issuable')).toBeTruthy(); + }); + + it('component emits `task-list-update-success` event bubbled via issuable-body', () => { + const issuableBody = wrapper.find(IssuableBody); + const eventParam = { + foo: 'bar', + }; + + issuableBody.vm.$emit('task-list-update-success', eventParam); + + expect(wrapper.emitted('task-list-update-success')).toBeTruthy(); + expect(wrapper.emitted('task-list-update-success')[0]).toEqual([eventParam]); + }); + + it('component emits `task-list-update-failure` event bubbled via issuable-body', () => { + const issuableBody = wrapper.find(IssuableBody); + + issuableBody.vm.$emit('task-list-update-failure'); + + expect(wrapper.emitted('task-list-update-failure')).toBeTruthy(); + }); + + it.each(['keydown-title', 'keydown-description'])( + 'component emits `%s` event with event object and issuableMeta params via issuable-body', + (eventName) => { + const eventObj = { + preventDefault: jest.fn(), + stopPropagation: jest.fn(), + }; + const issuableMeta = { + issuableTitle: 'foo', + issuableDescription: 'foobar', + }; + + const issuableBody = wrapper.find(IssuableBody); + + issuableBody.vm.$emit(eventName, eventObj, issuableMeta); + + expect(wrapper.emitted(eventName)).toBeTruthy(); + expect(wrapper.emitted(eventName)[0]).toMatchObject([eventObj, issuableMeta]); + }, + ); + }); + }); +}); diff --git a/spec/frontend/vue_shared/issuable/show/components/issuable_title_spec.js b/spec/frontend/vue_shared/issuable/show/components/issuable_title_spec.js new file mode 100644 index 00000000000..1fcf37a0477 --- /dev/null +++ b/spec/frontend/vue_shared/issuable/show/components/issuable_title_spec.js @@ -0,0 +1,100 @@ +import { GlIcon, GlButton, GlIntersectionObserver } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; + +import IssuableTitle from '~/vue_shared/issuable/show/components/issuable_title.vue'; + +import { mockIssuableShowProps, mockIssuable } from '../mock_data'; + +const issuableTitleProps = { + issuable: mockIssuable, + ...mockIssuableShowProps, +}; + +const createComponent = (propsData = issuableTitleProps) => + shallowMount(IssuableTitle, { + propsData, + stubs: { + transition: true, + }, + slots: { + 'status-badge': 'Open', + }, + directives: { + GlTooltip: createMockDirective(), + }, + }); + +describe('IssuableTitle', () => { + let wrapper; + + beforeEach(() => { + wrapper = createComponent(); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + describe('methods', () => { + describe('handleTitleAppear', () => { + it('sets value of `stickyTitleVisible` prop to false', () => { + wrapper.find(GlIntersectionObserver).vm.$emit('appear'); + + expect(wrapper.vm.stickyTitleVisible).toBe(false); + }); + }); + + describe('handleTitleDisappear', () => { + it('sets value of `stickyTitleVisible` prop to true', () => { + wrapper.find(GlIntersectionObserver).vm.$emit('disappear'); + + expect(wrapper.vm.stickyTitleVisible).toBe(true); + }); + }); + }); + + describe('template', () => { + it('renders issuable title', async () => { + const wrapperWithTitle = createComponent({ + ...mockIssuableShowProps, + issuable: { + ...mockIssuable, + titleHtml: '<b>Sample</b> title', + }, + }); + + await wrapperWithTitle.vm.$nextTick(); + const titleEl = wrapperWithTitle.find('h2'); + + expect(titleEl.exists()).toBe(true); + expect(titleEl.html()).toBe('<h2 dir="auto" class="title qa-title"><b>Sample</b> title</h2>'); + + wrapperWithTitle.destroy(); + }); + + it('renders edit button', () => { + const editButtonEl = wrapper.find(GlButton); + const tooltip = getBinding(editButtonEl.element, 'gl-tooltip'); + + expect(editButtonEl.exists()).toBe(true); + expect(editButtonEl.props('icon')).toBe('pencil'); + expect(editButtonEl.attributes('title')).toBe('Edit title and description'); + expect(tooltip).toBeDefined(); + }); + + it('renders sticky header when `stickyTitleVisible` prop is true', async () => { + wrapper.setData({ + stickyTitleVisible: true, + }); + + await wrapper.vm.$nextTick(); + const stickyHeaderEl = wrapper.find('[data-testid="header"]'); + + expect(stickyHeaderEl.exists()).toBe(true); + expect(stickyHeaderEl.find(GlIcon).props('name')).toBe(issuableTitleProps.statusIcon); + expect(stickyHeaderEl.text()).toContain('Open'); + expect(stickyHeaderEl.text()).toContain(issuableTitleProps.issuable.title); + }); + }); +}); diff --git a/spec/frontend/vue_shared/issuable/show/mock_data.js b/spec/frontend/vue_shared/issuable/show/mock_data.js new file mode 100644 index 00000000000..f5f3ed58655 --- /dev/null +++ b/spec/frontend/vue_shared/issuable/show/mock_data.js @@ -0,0 +1,45 @@ +import { mockIssuable as issuable } from 'jest/vue_shared/issuable/list/mock_data'; + +export const mockIssuable = { + ...issuable, + id: 'gid://gitlab/Issue/30', + title: 'Sample title', + titleHtml: 'Sample title', + description: '# Summary', + descriptionHtml: + '<h1 data-sourcepos="1:1-1:25" dir="auto">
<a id="user-content-magnoque-it-lurida-deus" class="anchor" href="#magnoque-it-lurida-deus" aria-hidden="true"></a>Summary</h1>', + state: 'opened', + blocked: false, + confidential: false, + updatedBy: issuable.author, + type: 'ISSUE', + currentUserTodos: { + nodes: [ + { + id: 'gid://gitlab/Todo/489', + state: 'done', + }, + ], + }, +}; + +export const mockIssuableShowProps = { + issuable: mockIssuable, + descriptionHelpPath: '/help/user/markdown', + descriptionPreviewPath: '/gitlab-org/gitlab-shell/preview_markdown', + taskListUpdatePath: `${mockIssuable.webUrl}.json`, + taskListLockVersion: 1, + editFormVisible: false, + enableAutocomplete: true, + enableAutosave: true, + enableZenMode: true, + enableTaskList: true, + enableEdit: true, + showFieldTitle: false, + statusBadgeClass: 'status-box-open', + statusIcon: 'issue-open-m', + taskCompletionStatus: { + completedCount: 0, + count: 5, + }, +}; |