diff options
Diffstat (limited to 'spec/frontend/issuable_show/components')
6 files changed, 658 insertions, 0 deletions
diff --git a/spec/frontend/issuable_show/components/issuable_body_spec.js b/spec/frontend/issuable_show/components/issuable_body_spec.js new file mode 100644 index 00000000000..0e4475e8103 --- /dev/null +++ b/spec/frontend/issuable_show/components/issuable_body_spec.js @@ -0,0 +1,140 @@ +import { shallowMount } from '@vue/test-utils'; + +import IssuableBody from '~/issuable_show/components/issuable_body.vue'; + +import IssuableTitle from '~/issuable_show/components/issuable_title.vue'; +import IssuableDescription from '~/issuable_show/components/issuable_description.vue'; +import IssuableEditForm from '~/issuable_show/components/issuable_edit_form.vue'; +import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; + +import { mockIssuableShowProps, mockIssuable } from '../mock_data'; + +jest.mock('~/autosave'); + +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', () => { + 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('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'); + const sanitizedText = editedEl + .text() + .replace(/\n/g, ' ') + .replace(/\s+/g, ' '); + + expect(sanitizedText).toContain('Edited'); + expect(sanitizedText).toContain('ago'); + expect(sanitizedText).toContain(`by ${mockIssuable.updatedBy.name}`); + }); + + 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(); + }); + }); + }); +}); diff --git a/spec/frontend/issuable_show/components/issuable_description_spec.js b/spec/frontend/issuable_show/components/issuable_description_spec.js new file mode 100644 index 00000000000..1dd8348b098 --- /dev/null +++ b/spec/frontend/issuable_show/components/issuable_description_spec.js @@ -0,0 +1,41 @@ +import $ from 'jquery'; +import { shallowMount } from '@vue/test-utils'; + +import IssuableDescription from '~/issuable_show/components/issuable_description.vue'; + +import { mockIssuable } from '../mock_data'; + +const createComponent = (issuable = mockIssuable) => + shallowMount(IssuableDescription, { + propsData: { issuable }, + }); + +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(); + }); + }); + }); +}); diff --git a/spec/frontend/issuable_show/components/issuable_edit_form_spec.js b/spec/frontend/issuable_show/components/issuable_edit_form_spec.js new file mode 100644 index 00000000000..352e66cdffe --- /dev/null +++ b/spec/frontend/issuable_show/components/issuable_edit_form_spec.js @@ -0,0 +1,122 @@ +import { shallowMount } from '@vue/test-utils'; +import { GlFormInput } from '@gitlab/ui'; +import MarkdownField from '~/vue_shared/components/markdown/field.vue'; + +import IssuableEditForm from '~/issuable_show/components/issuable_edit_form.vue'; +import IssuableEventHub from '~/issuable_show/event_hub'; + +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('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); + }); + }); +}); diff --git a/spec/frontend/issuable_show/components/issuable_header_spec.js b/spec/frontend/issuable_show/components/issuable_header_spec.js new file mode 100644 index 00000000000..fad8ec8a891 --- /dev/null +++ b/spec/frontend/issuable_show/components/issuable_header_spec.js @@ -0,0 +1,132 @@ +import { shallowMount } from '@vue/test-utils'; +import { GlIcon, GlAvatarLabeled } from '@gitlab/ui'; + +import IssuableHeader from '~/issuable_show/components/issuable_header.vue'; + +import { mockIssuableShowProps, mockIssuable } from '../mock_data'; + +const issuableHeaderProps = { + ...mockIssuable, + ...mockIssuableShowProps, +}; + +const createComponent = (propsData = issuableHeaderProps) => + 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> + `, + }, + }); + +describe('IssuableHeader', () => { + let wrapper; + const findByTestId = testId => wrapper.find(`[data-testid="${testId}"]`); + + 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 = 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 = 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 = 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 = findByTestId('avatar'); + expect(avatarEl.exists()).toBe(true); + expect(avatarEl.attributes()).toMatchObject(avatarElAttrs); + expect(avatarEl.find(GlAvatarLabeled).attributes()).toMatchObject({ + size: '24', + src: avatarUrl, + label: name, + }); + }); + + it('renders sidebar toggle button', () => { + const toggleButtonEl = findByTestId('sidebar-toggle'); + + expect(toggleButtonEl.exists()).toBe(true); + expect(toggleButtonEl.props('icon')).toBe('chevron-double-lg-left'); + }); + + it('renders header actions', () => { + const actionsEl = findByTestId('header-actions'); + + expect(actionsEl.find('button.js-close').exists()).toBe(true); + expect(actionsEl.find('a.js-new').exists()).toBe(true); + }); + }); +}); diff --git a/spec/frontend/issuable_show/components/issuable_show_root_spec.js b/spec/frontend/issuable_show/components/issuable_show_root_spec.js new file mode 100644 index 00000000000..112e4ccd340 --- /dev/null +++ b/spec/frontend/issuable_show/components/issuable_show_root_spec.js @@ -0,0 +1,123 @@ +import { shallowMount } from '@vue/test-utils'; + +import IssuableShowRoot from '~/issuable_show/components/issuable_show_root.vue'; + +import IssuableHeader from '~/issuable_show/components/issuable_header.vue'; +import IssuableBody from '~/issuable_show/components/issuable_body.vue'; +import IssuableSidebar from '~/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, + } = 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, + }); + 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 `sidebar-toggle` event bubbled via issuable-sidebar', () => { + const issuableSidebar = wrapper.find(IssuableSidebar); + + issuableSidebar.vm.$emit('sidebar-toggle', true); + + expect(wrapper.emitted('sidebar-toggle')).toBeTruthy(); + }); + }); + }); +}); diff --git a/spec/frontend/issuable_show/components/issuable_title_spec.js b/spec/frontend/issuable_show/components/issuable_title_spec.js new file mode 100644 index 00000000000..e8621c763b3 --- /dev/null +++ b/spec/frontend/issuable_show/components/issuable_title_spec.js @@ -0,0 +1,100 @@ +import { shallowMount } from '@vue/test-utils'; +import { GlIcon, GlButton, GlIntersectionObserver } from '@gitlab/ui'; +import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; + +import IssuableTitle from '~/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); + }); + }); +}); |