diff options
Diffstat (limited to 'spec/frontend/issues/show')
-rw-r--r-- | spec/frontend/issues/show/components/app_spec.js | 154 | ||||
-rw-r--r-- | spec/frontend/issues/show/components/sticky_header_spec.js | 135 | ||||
-rw-r--r-- | spec/frontend/issues/show/components/task_list_item_actions_spec.js | 52 | ||||
-rw-r--r-- | spec/frontend/issues/show/issue_spec.js | 43 | ||||
-rw-r--r-- | spec/frontend/issues/show/mock_data/mock_data.js | 3 | ||||
-rw-r--r-- | spec/frontend/issues/show/store_spec.js | 39 |
6 files changed, 202 insertions, 224 deletions
diff --git a/spec/frontend/issues/show/components/app_spec.js b/spec/frontend/issues/show/components/app_spec.js index de183f94277..8999952c54c 100644 --- a/spec/frontend/issues/show/components/app_spec.js +++ b/spec/frontend/issues/show/components/app_spec.js @@ -1,23 +1,14 @@ -import { GlIcon, GlIntersectionObserver } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; import MockAdapter from 'axios-mock-adapter'; import { nextTick } from 'vue'; -import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; -import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import waitForPromises from 'helpers/wait_for_promises'; import { createAlert } from '~/alert'; -import { - issuableStatusText, - STATUS_CLOSED, - STATUS_OPEN, - STATUS_REOPENED, - TYPE_EPIC, - TYPE_INCIDENT, - TYPE_ISSUE, -} from '~/issues/constants'; +import { TYPE_EPIC, TYPE_INCIDENT, TYPE_ISSUE } from '~/issues/constants'; import IssuableApp from '~/issues/show/components/app.vue'; import DescriptionComponent from '~/issues/show/components/description.vue'; import EditedComponent from '~/issues/show/components/edited.vue'; import FormComponent from '~/issues/show/components/form.vue'; +import StickyHeader from '~/issues/show/components/sticky_header.vue'; import TitleComponent from '~/issues/show/components/title.vue'; import IncidentTabs from '~/issues/show/components/incidents/incident_tabs.vue'; import PinnedLinks from '~/issues/show/components/pinned_links.vue'; @@ -44,22 +35,15 @@ describe('Issuable output', () => { let axiosMock; let wrapper; - const findStickyHeader = () => wrapper.findByTestId('issue-sticky-header'); - const findLockedBadge = () => wrapper.findByTestId('locked'); - const findConfidentialBadge = () => wrapper.findByTestId('confidential'); - const findHiddenBadge = () => wrapper.findByTestId('hidden'); - + const findStickyHeader = () => wrapper.findComponent(StickyHeader); const findTitle = () => wrapper.findComponent(TitleComponent); const findDescription = () => wrapper.findComponent(DescriptionComponent); const findEdited = () => wrapper.findComponent(EditedComponent); const findForm = () => wrapper.findComponent(FormComponent); const findPinnedLinks = () => wrapper.findComponent(PinnedLinks); - const createComponent = ({ props = {}, options = {}, data = {} } = {}) => { - wrapper = shallowMountExtended(IssuableApp, { - directives: { - GlTooltip: createMockDirective('gl-tooltip'), - }, + const createComponent = ({ props = {}, options = {} } = {}) => { + wrapper = shallowMount(IssuableApp, { propsData: { ...appProps, ...props }, provide: { fullPath: 'gitlab-org/incidents', @@ -69,11 +53,6 @@ describe('Issuable output', () => { HighlightBar: true, IncidentTabs: true, }, - data() { - return { - ...data, - }; - }, ...options, }); @@ -81,13 +60,6 @@ describe('Issuable output', () => { return waitForPromises(); }; - const createComponentAndScroll = async (props) => { - await createComponent({ props }); - global.pageYOffset = 100; - wrapper.findComponent(GlIntersectionObserver).vm.$emit('disappear'); - await nextTick(); - }; - const emitHubEvent = (event) => { eventHub.$emit(event); return waitForPromises(); @@ -332,104 +304,36 @@ describe('Issuable output', () => { describe('when title is in view', () => { it('is not shown', async () => { await createComponent(); - wrapper.findComponent(GlIntersectionObserver).vm.$emit('disappear'); - expect(findStickyHeader().exists()).toBe(false); + wrapper.findComponent(StickyHeader).vm.$emit('show'); + + expect(findStickyHeader().props('show')).toBe(false); }); }); - describe('when title is not in view', () => { - it.each([TYPE_INCIDENT, TYPE_ISSUE, TYPE_EPIC])( - 'shows with title when issuableType="%s"', - async (issuableType) => { - await createComponentAndScroll({ issuableType }); - - expect(findStickyHeader().text()).toContain('this is a title'); - }, - ); - - it.each` - issuableType | issuableStatus | statusIcon - ${TYPE_INCIDENT} | ${STATUS_OPEN} | ${'issues'} - ${TYPE_INCIDENT} | ${STATUS_CLOSED} | ${'issue-closed'} - ${TYPE_ISSUE} | ${STATUS_OPEN} | ${'issues'} - ${TYPE_ISSUE} | ${STATUS_CLOSED} | ${'issue-closed'} - ${TYPE_EPIC} | ${STATUS_OPEN} | ${'epic'} - ${TYPE_EPIC} | ${STATUS_CLOSED} | ${'epic-closed'} - `( - 'shows with state icon "$statusIcon" for $issuableType when status is $issuableStatus', - async ({ issuableType, issuableStatus, statusIcon }) => { - await createComponentAndScroll({ issuableType, issuableStatus }); - - expect(findStickyHeader().findComponent(GlIcon).props('name')).toBe(statusIcon); - }, - ); - - it.each` - title | issuableStatus - ${'shows with Open when status is opened'} | ${STATUS_OPEN} - ${'shows with Closed when status is closed'} | ${STATUS_CLOSED} - ${'shows with Open when status is reopened'} | ${STATUS_REOPENED} - `('$title', async ({ issuableStatus }) => { - await createComponentAndScroll({ issuableStatus }); - - expect(findStickyHeader().text()).toContain(issuableStatusText[issuableStatus]); - }); + describe.each([TYPE_INCIDENT, TYPE_ISSUE, TYPE_EPIC])( + 'when title is not in view', + (issuableType) => { + beforeEach(async () => { + await createComponent({ props: { issuableType } }); - it.each` - title | isConfidential - ${'does not show confidential badge when issue is not confidential'} | ${false} - ${'shows confidential badge when issue is confidential'} | ${true} - `('$title', async ({ isConfidential }) => { - await createComponentAndScroll({ isConfidential }); - const confidentialEl = findConfidentialBadge(); - - expect(confidentialEl.exists()).toBe(isConfidential); - - if (isConfidential) { - expect(confidentialEl.props()).toMatchObject({ - workspaceType: 'project', - issuableType: 'issue', - }); - } - }); + global.pageYOffset = 100; + wrapper.findComponent(StickyHeader).vm.$emit('show'); + await nextTick(); + }); - it.each` - title | isLocked - ${'does not show locked badge when issue is not locked'} | ${false} - ${'shows locked badge when issue is locked'} | ${true} - `('$title', async ({ isLocked }) => { - await createComponentAndScroll({ isLocked }); - const lockedBadge = findLockedBadge(); - - expect(lockedBadge.exists()).toBe(isLocked); - - if (isLocked) { - expect(lockedBadge.attributes('title')).toBe( - 'This issue is locked. Only project members can comment.', - ); - expect(getBinding(lockedBadge.element, 'gl-tooltip')).not.toBeUndefined(); - } - }); + it(`shows when issuableType=${issuableType}`, () => { + expect(findStickyHeader().props('show')).toBe(true); + }); - it.each` - title | isHidden - ${'does not show hidden badge when issue is not hidden'} | ${false} - ${'shows hidden badge when issue is hidden'} | ${true} - `('$title', async ({ isHidden }) => { - await createComponentAndScroll({ isHidden }); - const hiddenBadge = findHiddenBadge(); - - expect(hiddenBadge.exists()).toBe(isHidden); - - if (isHidden) { - expect(hiddenBadge.attributes('title')).toBe( - 'This issue is hidden because its author has been banned', - ); - expect(getBinding(hiddenBadge.element, 'gl-tooltip')).not.toBeUndefined(); - } - }); - }); + it('hides again when title is back in view', async () => { + wrapper.findComponent(StickyHeader).vm.$emit('hide'); + await nextTick(); + + expect(findStickyHeader().props('show')).toBe(false); + }); + }, + ); }); describe('Composable description component', () => { diff --git a/spec/frontend/issues/show/components/sticky_header_spec.js b/spec/frontend/issues/show/components/sticky_header_spec.js new file mode 100644 index 00000000000..0c54ae45e70 --- /dev/null +++ b/spec/frontend/issues/show/components/sticky_header_spec.js @@ -0,0 +1,135 @@ +import { GlIcon } from '@gitlab/ui'; +import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import { + issuableStatusText, + STATUS_CLOSED, + STATUS_OPEN, + STATUS_REOPENED, + TYPE_EPIC, + TYPE_INCIDENT, + TYPE_ISSUE, +} from '~/issues/constants'; +import StickyHeader from '~/issues/show/components/sticky_header.vue'; +import ConfidentialityBadge from '~/vue_shared/components/confidentiality_badge.vue'; + +describe('StickyHeader component', () => { + let wrapper; + + const findConfidentialBadge = () => wrapper.findComponent(ConfidentialityBadge); + const findHiddenBadge = () => wrapper.findByTestId('hidden'); + const findLockedBadge = () => wrapper.findByTestId('locked'); + + const createComponent = (props = {}) => { + wrapper = shallowMountExtended(StickyHeader, { + directives: { + GlTooltip: createMockDirective('gl-tooltip'), + }, + propsData: { + issuableStatus: STATUS_OPEN, + issuableType: TYPE_ISSUE, + show: true, + title: 'A sticky issue', + titleHtml: '', + ...props, + }, + }); + }; + + it.each` + issuableType | issuableStatus | statusIcon + ${TYPE_INCIDENT} | ${STATUS_OPEN} | ${'issues'} + ${TYPE_INCIDENT} | ${STATUS_CLOSED} | ${'issue-closed'} + ${TYPE_ISSUE} | ${STATUS_OPEN} | ${'issues'} + ${TYPE_ISSUE} | ${STATUS_CLOSED} | ${'issue-closed'} + ${TYPE_EPIC} | ${STATUS_OPEN} | ${'epic'} + ${TYPE_EPIC} | ${STATUS_CLOSED} | ${'epic-closed'} + `( + 'shows with state icon "$statusIcon" for $issuableType when status is $issuableStatus', + ({ issuableType, issuableStatus, statusIcon }) => { + createComponent({ issuableType, issuableStatus }); + + expect(wrapper.findComponent(GlIcon).props('name')).toBe(statusIcon); + }, + ); + + it.each` + title | issuableStatus + ${'shows with Open when status is opened'} | ${STATUS_OPEN} + ${'shows with Closed when status is closed'} | ${STATUS_CLOSED} + ${'shows with Open when status is reopened'} | ${STATUS_REOPENED} + `('$title', ({ issuableStatus }) => { + createComponent({ issuableStatus }); + + expect(wrapper.text()).toContain(issuableStatusText[issuableStatus]); + }); + + it.each` + title | isConfidential + ${'does not show confidential badge when issue is not confidential'} | ${false} + ${'shows confidential badge when issue is confidential'} | ${true} + `('$title', ({ isConfidential }) => { + createComponent({ isConfidential }); + const confidentialBadge = findConfidentialBadge(); + + expect(confidentialBadge.exists()).toBe(isConfidential); + + if (isConfidential) { + expect(confidentialBadge.props()).toMatchObject({ + workspaceType: 'project', + issuableType: 'issue', + }); + } + }); + + it.each` + title | isLocked + ${'does not show locked badge when issue is not locked'} | ${false} + ${'shows locked badge when issue is locked'} | ${true} + `('$title', ({ isLocked }) => { + createComponent({ isLocked }); + const lockedBadge = findLockedBadge(); + + expect(lockedBadge.exists()).toBe(isLocked); + + if (isLocked) { + expect(lockedBadge.attributes('title')).toBe( + 'This issue is locked. Only project members can comment.', + ); + expect(getBinding(lockedBadge.element, 'gl-tooltip')).not.toBeUndefined(); + } + }); + + it.each` + title | isHidden + ${'does not show hidden badge when issue is not hidden'} | ${false} + ${'shows hidden badge when issue is hidden'} | ${true} + `('$title', ({ isHidden }) => { + createComponent({ isHidden }); + const hiddenBadge = findHiddenBadge(); + + expect(hiddenBadge.exists()).toBe(isHidden); + + if (isHidden) { + expect(hiddenBadge.attributes('title')).toBe( + 'This issue is hidden because its author has been banned', + ); + expect(getBinding(hiddenBadge.element, 'gl-tooltip')).not.toBeUndefined(); + } + }); + + it('shows with title', () => { + createComponent(); + const title = wrapper.find('a'); + + expect(title.text()).toContain('A sticky issue'); + expect(title.attributes('href')).toBe('#top'); + }); + + it('shows title containing markup', () => { + const titleHtml = '<b>A sticky issue</b>'; + createComponent({ titleHtml }); + + expect(wrapper.find('a').html()).toContain(titleHtml); + }); +}); diff --git a/spec/frontend/issues/show/components/task_list_item_actions_spec.js b/spec/frontend/issues/show/components/task_list_item_actions_spec.js index 93cb7b5ae16..b2e57bf49d0 100644 --- a/spec/frontend/issues/show/components/task_list_item_actions_spec.js +++ b/spec/frontend/issues/show/components/task_list_item_actions_spec.js @@ -1,5 +1,6 @@ -import { GlDisclosureDropdown, GlDisclosureDropdownItem } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; +import { GlDisclosureDropdown } from '@gitlab/ui'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import { TYPE_EPIC, TYPE_INCIDENT, TYPE_ISSUE } from '~/issues/constants'; import TaskListItemActions from '~/issues/show/components/task_list_item_actions.vue'; import eventHub from '~/issues/show/event_hub'; @@ -9,26 +10,24 @@ describe('TaskListItemActions component', () => { let wrapper; const findGlDisclosureDropdown = () => wrapper.findComponent(GlDisclosureDropdown); - const findConvertToTaskItem = () => wrapper.findAllComponents(GlDisclosureDropdownItem).at(0); - const findDeleteItem = () => wrapper.findAllComponents(GlDisclosureDropdownItem).at(1); + const findConvertToTaskItem = () => wrapper.findByTestId('convert'); + const findDeleteItem = () => wrapper.findByTestId('delete'); - const mountComponent = () => { + const mountComponent = ({ issuableType = TYPE_ISSUE } = {}) => { const li = document.createElement('li'); li.dataset.sourcepos = '3:1-3:10'; li.appendChild(document.createElement('div')); document.body.appendChild(li); - wrapper = shallowMount(TaskListItemActions, { - provide: { canUpdate: true }, + wrapper = shallowMountExtended(TaskListItemActions, { + provide: { canUpdate: true, issuableType }, attachTo: document.querySelector('div'), }); }; - beforeEach(() => { + it('renders dropdown', () => { mountComponent(); - }); - it('renders dropdown', () => { expect(findGlDisclosureDropdown().props()).toMatchObject({ category: 'tertiary', icon: 'ellipsis_v', @@ -38,15 +37,36 @@ describe('TaskListItemActions component', () => { }); }); - it('emits event when `Convert to task` dropdown item is clicked', () => { - findConvertToTaskItem().vm.$emit('action'); + describe('"Convert to task" dropdown item', () => { + describe.each` + issuableType | exists + ${TYPE_EPIC} | ${false} + ${TYPE_INCIDENT} | ${true} + ${TYPE_ISSUE} | ${true} + `(`when $issuableType`, ({ issuableType, exists }) => { + it(`${exists ? 'renders' : 'does not render'}`, () => { + mountComponent({ issuableType }); - expect(eventHub.$emit).toHaveBeenCalledWith('convert-task-list-item', '3:1-3:10'); + expect(findConvertToTaskItem().exists()).toBe(exists); + }); + }); }); - it('emits event when `Delete` dropdown item is clicked', () => { - findDeleteItem().vm.$emit('action'); + describe('events', () => { + beforeEach(() => { + mountComponent(); + }); + + it('emits event when `Convert to task` dropdown item is clicked', () => { + findConvertToTaskItem().vm.$emit('action'); + + expect(eventHub.$emit).toHaveBeenCalledWith('convert-task-list-item', '3:1-3:10'); + }); - expect(eventHub.$emit).toHaveBeenCalledWith('delete-task-list-item', '3:1-3:10'); + it('emits event when `Delete` dropdown item is clicked', () => { + findDeleteItem().vm.$emit('action'); + + expect(eventHub.$emit).toHaveBeenCalledWith('delete-task-list-item', '3:1-3:10'); + }); }); }); diff --git a/spec/frontend/issues/show/issue_spec.js b/spec/frontend/issues/show/issue_spec.js deleted file mode 100644 index 561035242eb..00000000000 --- a/spec/frontend/issues/show/issue_spec.js +++ /dev/null @@ -1,43 +0,0 @@ -import MockAdapter from 'axios-mock-adapter'; -import waitForPromises from 'helpers/wait_for_promises'; -import { initIssueApp } from '~/issues/show'; -import * as parseData from '~/issues/show/utils/parse_data'; -import axios from '~/lib/utils/axios_utils'; -import { HTTP_STATUS_OK } from '~/lib/utils/http_status'; -import createStore from '~/notes/stores'; -import { appProps } from './mock_data/mock_data'; - -const mock = new MockAdapter(axios); -mock.onGet().reply(HTTP_STATUS_OK); - -jest.mock('~/lib/utils/poll'); - -const setupHTML = (initialData) => { - document.body.innerHTML = `<div id="js-issuable-app"></div>`; - document.getElementById('js-issuable-app').dataset.initial = JSON.stringify(initialData); -}; - -describe('Issue show index', () => { - describe('initIssueApp', () => { - // quarantine: https://gitlab.com/gitlab-org/gitlab/-/issues/390368 - // eslint-disable-next-line jest/no-disabled-tests - it.skip('should initialize app with no potential XSS attack', async () => { - const alertSpy = jest.spyOn(window, 'alert').mockImplementation(() => {}); - const parseDataSpy = jest.spyOn(parseData, 'parseIssuableData'); - - setupHTML({ - ...appProps, - initialDescriptionHtml: '<svg onload=window.alert(1)>', - }); - - const initialDataEl = document.getElementById('js-issuable-app'); - const issuableData = parseData.parseIssuableData(initialDataEl); - initIssueApp(issuableData, createStore()); - - await waitForPromises(); - - expect(parseDataSpy).toHaveBeenCalled(); - expect(alertSpy).not.toHaveBeenCalled(); - }); - }); -}); diff --git a/spec/frontend/issues/show/mock_data/mock_data.js b/spec/frontend/issues/show/mock_data/mock_data.js index ed969a08ac5..37aa18ced8d 100644 --- a/spec/frontend/issues/show/mock_data/mock_data.js +++ b/spec/frontend/issues/show/mock_data/mock_data.js @@ -1,8 +1,9 @@ import { TEST_HOST } from 'helpers/test_constants'; export const initialRequest = { - title: '<p>this is a title</p>', + title: '<gl-emoji title="party-parrot"></gl-emoji>this is a title', title_text: 'this is a title', + title_html: '<gl-emoji title="party-parrot"></gl-emoji>this is a title', description: '<p>this is a description!</p>', description_text: 'this is a description', task_completion_status: { completed_count: 2, count: 4 }, diff --git a/spec/frontend/issues/show/store_spec.js b/spec/frontend/issues/show/store_spec.js deleted file mode 100644 index 20d3a6cdaae..00000000000 --- a/spec/frontend/issues/show/store_spec.js +++ /dev/null @@ -1,39 +0,0 @@ -import Store from '~/issues/show/stores'; -import updateDescription from '~/issues/show/utils/update_description'; - -jest.mock('~/issues/show/utils/update_description'); - -describe('Store', () => { - let store; - - beforeEach(() => { - store = new Store({ - descriptionHtml: '<p>This is a description</p>', - }); - }); - - describe('updateState', () => { - beforeEach(() => { - document.body.innerHTML = ` - <div class="detail-page-description content-block"> - <details open> - <summary>One</summary> - </details> - <details> - <summary>Two</summary> - </details> - </div> - `; - }); - - afterEach(() => { - document.getElementsByTagName('html')[0].innerHTML = ''; - }); - - it('calls updateDetailsState', () => { - store.updateState({ description: '' }); - - expect(updateDescription).toHaveBeenCalledTimes(1); - }); - }); -}); |