diff options
Diffstat (limited to 'spec/frontend/issue_show/components')
6 files changed, 241 insertions, 85 deletions
diff --git a/spec/frontend/issue_show/components/app_spec.js b/spec/frontend/issue_show/components/app_spec.js index b8860e93a22..4c06f2dca1b 100644 --- a/spec/frontend/issue_show/components/app_spec.js +++ b/spec/frontend/issue_show/components/app_spec.js @@ -1,6 +1,7 @@ import { GlIntersectionObserver } from '@gitlab/ui'; import { mount } from '@vue/test-utils'; import MockAdapter from 'axios-mock-adapter'; +import { nextTick } from 'vue'; import { useMockIntersectionObserver } from 'helpers/mock_dom_observer'; import '~/behaviors/markdown/render_gfm'; import IssuableApp from '~/issue_show/components/app.vue'; @@ -17,7 +18,7 @@ import { publishedIncidentUrl, secondRequest, zoomMeetingUrl, -} from '../mock_data'; +} from '../mock_data/mock_data'; function formatText(text) { return text.trim().replace(/\s\s+/g, ' '); @@ -36,12 +37,11 @@ describe('Issuable output', () => { let wrapper; const findStickyHeader = () => wrapper.find('[data-testid="issue-sticky-header"]'); - const findLockedBadge = () => wrapper.find('[data-testid="locked"]'); - const findConfidentialBadge = () => wrapper.find('[data-testid="confidential"]'); + const findAlert = () => wrapper.find('.alert'); - const mountComponent = (props = {}, options = {}) => { + const mountComponent = (props = {}, options = {}, data = {}) => { wrapper = mount(IssuableApp, { propsData: { ...appProps, ...props }, provide: { @@ -53,6 +53,11 @@ describe('Issuable output', () => { HighlightBar: true, IncidentTabs: true, }, + data() { + return { + ...data, + }; + }, ...options, }); }; @@ -91,10 +96,8 @@ describe('Issuable output', () => { afterEach(() => { mock.restore(); realtimeRequestCount = 0; - wrapper.vm.poll.stop(); wrapper.destroy(); - wrapper = null; }); it('should render a title/description/edited and update title/description/edited on update', () => { @@ -115,7 +118,7 @@ describe('Issuable output', () => { expect(formatText(editedText.text())).toMatch(/Edited[\s\S]+?by Some User/); expect(editedText.find('.author-link').attributes('href')).toMatch(/\/some_user$/); expect(editedText.find('time').text()).toBeTruthy(); - expect(wrapper.vm.state.lock_version).toEqual(1); + expect(wrapper.vm.state.lock_version).toBe(initialRequest.lock_version); }) .then(() => { wrapper.vm.poll.makeRequest(); @@ -133,7 +136,9 @@ describe('Issuable output', () => { expect(editedText.find('.author-link').attributes('href')).toMatch(/\/other_user$/); expect(editedText.find('time').text()).toBeTruthy(); - expect(wrapper.vm.state.lock_version).toEqual(2); + // As the lock_version value does not differ from the server, + // we should not see an alert + expect(findAlert().exists()).toBe(false); }); }); @@ -172,7 +177,7 @@ describe('Issuable output', () => { ${'zoomMeetingUrl'} | ${zoomMeetingUrl} ${'publishedIncidentUrl'} | ${publishedIncidentUrl} `('sets the $prop correctly on underlying pinned links', ({ prop, value }) => { - expect(wrapper.vm[prop]).toEqual(value); + expect(wrapper.vm[prop]).toBe(value); expect(wrapper.find(`[data-testid="${prop}"]`).attributes('href')).toBe(value); }); }); @@ -374,9 +379,9 @@ describe('Issuable output', () => { }); }) .then(() => { - expect(wrapper.vm.formState.lockedWarningVisible).toEqual(true); - expect(wrapper.vm.formState.lock_version).toEqual(1); - expect(wrapper.find('.alert').exists()).toBe(true); + expect(wrapper.vm.formState.lockedWarningVisible).toBe(true); + expect(wrapper.vm.formState.lock_version).toBe(1); + expect(findAlert().exists()).toBe(true); }); }); }); @@ -530,7 +535,7 @@ describe('Issuable output', () => { `('$title', async ({ state }) => { wrapper.setProps({ issuableStatus: state }); - await wrapper.vm.$nextTick(); + await nextTick(); expect(findStickyHeader().text()).toContain(IssuableStatusText[state]); }); @@ -542,7 +547,7 @@ describe('Issuable output', () => { `('$title', async ({ isConfidential }) => { wrapper.setProps({ isConfidential }); - await wrapper.vm.$nextTick(); + await nextTick(); expect(findConfidentialBadge().exists()).toBe(isConfidential); }); @@ -554,7 +559,7 @@ describe('Issuable output', () => { `('$title', async ({ isLocked }) => { wrapper.setProps({ isLocked }); - await wrapper.vm.$nextTick(); + await nextTick(); expect(findLockedBadge().exists()).toBe(isLocked); }); @@ -562,9 +567,9 @@ describe('Issuable output', () => { }); describe('Composable description component', () => { - const findIncidentTabs = () => wrapper.find(IncidentTabs); - const findDescriptionComponent = () => wrapper.find(DescriptionComponent); - const findPinnedLinks = () => wrapper.find(PinnedLinks); + const findIncidentTabs = () => wrapper.findComponent(IncidentTabs); + const findDescriptionComponent = () => wrapper.findComponent(DescriptionComponent); + const findPinnedLinks = () => wrapper.findComponent(PinnedLinks); const borderClass = 'gl-border-b-1 gl-border-b-gray-100 gl-border-b-solid gl-mb-6'; describe('when using description component', () => { diff --git a/spec/frontend/issue_show/components/description_spec.js b/spec/frontend/issue_show/components/description_spec.js index 70c04280675..cdf06ecc31f 100644 --- a/spec/frontend/issue_show/components/description_spec.js +++ b/spec/frontend/issue_show/components/description_spec.js @@ -5,7 +5,7 @@ import { TEST_HOST } from 'helpers/test_constants'; import mountComponent from 'helpers/vue_mount_component_helper'; import Description from '~/issue_show/components/description.vue'; import TaskList from '~/task_list'; -import { descriptionProps as props } from '../mock_data'; +import { descriptionProps as props } from '../mock_data/mock_data'; jest.mock('~/task_list'); diff --git a/spec/frontend/issue_show/components/edit_actions_spec.js b/spec/frontend/issue_show/components/edit_actions_spec.js index 54707879f63..50c27cb5bda 100644 --- a/spec/frontend/issue_show/components/edit_actions_spec.js +++ b/spec/frontend/issue_show/components/edit_actions_spec.js @@ -1,113 +1,163 @@ -import Vue from 'vue'; -import editActions from '~/issue_show/components/edit_actions.vue'; +import { GlButton, GlModal } from '@gitlab/ui'; +import { createLocalVue } from '@vue/test-utils'; +import VueApollo from 'vue-apollo'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import waitForPromises from 'helpers/wait_for_promises'; +import IssuableEditActions from '~/issue_show/components/edit_actions.vue'; import eventHub from '~/issue_show/event_hub'; -import Store from '~/issue_show/stores'; -describe('Edit Actions components', () => { - let vm; +import { + getIssueStateQueryResponse, + updateIssueStateQueryResponse, +} from '../mock_data/apollo_mock'; + +const localVue = createLocalVue(); +localVue.use(VueApollo); + +describe('Edit Actions component', () => { + let wrapper; + let fakeApollo; + let mockIssueStateData; + + const mockResolvers = { + Query: { + issueState() { + return { + __typename: 'IssueState', + rawData: mockIssueStateData(), + }; + }, + }, + }; - beforeEach((done) => { - const Component = Vue.extend(editActions); - const store = new Store({ - titleHtml: '', - descriptionHtml: '', - issuableRef: '', - }); - store.formState.title = 'test'; + const modalId = 'delete-issuable-modal-1'; - jest.spyOn(eventHub, '$emit').mockImplementation(() => {}); + const createComponent = ({ props, data } = {}) => { + fakeApollo = createMockApollo([], mockResolvers); - vm = new Component({ + wrapper = shallowMountExtended(IssuableEditActions, { + apolloProvider: fakeApollo, propsData: { + formState: { + title: 'GitLab Issue', + }, canDestroy: true, - formState: store.formState, issuableType: 'issue', + ...props, }, - }).$mount(); + data() { + return { + issueState: {}, + modalId, + ...data, + }; + }, + }); + }; - Vue.nextTick(done); - }); + async function deleteIssuable(localWrapper) { + localWrapper.findComponent(GlModal).vm.$emit('primary'); + } - it('renders all buttons as enabled', () => { - expect(vm.$el.querySelectorAll('.disabled').length).toBe(0); + const findModal = () => wrapper.findComponent(GlModal); + const findEditButtons = () => wrapper.findAllComponents(GlButton); + const findDeleteButton = () => wrapper.findByTestId('issuable-delete-button'); + const findSaveButton = () => wrapper.findByTestId('issuable-save-button'); + const findCancelButton = () => wrapper.findByTestId('issuable-cancel-button'); - expect(vm.$el.querySelectorAll('[disabled]').length).toBe(0); + beforeEach(() => { + mockIssueStateData = jest.fn(); + createComponent(); }); - it('does not render delete button if canUpdate is false', (done) => { - vm.canDestroy = false; - - Vue.nextTick(() => { - expect(vm.$el.querySelector('.btn-danger')).toBeNull(); + afterEach(() => { + wrapper.destroy(); + }); - done(); + it('renders all buttons as enabled', () => { + const buttons = findEditButtons().wrappers; + buttons.forEach((button) => { + expect(button.attributes('disabled')).toBeFalsy(); }); }); - it('disables submit button when title is blank', (done) => { - vm.formState.title = ''; + it('does not render the delete button if canDestroy is false', () => { + createComponent({ props: { canDestroy: false } }); + expect(findDeleteButton().exists()).toBe(false); + }); - Vue.nextTick(() => { - expect(vm.$el.querySelector('.btn-confirm').getAttribute('disabled')).toBe('disabled'); + it('disables save button when title is blank', () => { + createComponent({ props: { formState: { title: '', issue_type: '' } } }); - done(); - }); + expect(findSaveButton().attributes('disabled')).toBe('true'); }); - it('should not show delete button if showDeleteButton is false', (done) => { - vm.showDeleteButton = false; + it('does not render the delete button if showDeleteButton is false', () => { + createComponent({ props: { showDeleteButton: false } }); - Vue.nextTick(() => { - expect(vm.$el.querySelector('.btn-danger')).toBeNull(); - done(); - }); + expect(findDeleteButton().exists()).toBe(false); }); describe('updateIssuable', () => { - it('sends update.issauble event when clicking save button', () => { - vm.$el.querySelector('.btn-confirm').click(); - - expect(eventHub.$emit).toHaveBeenCalledWith('update.issuable'); + beforeEach(() => { + jest.spyOn(eventHub, '$emit').mockImplementation(() => {}); }); - it('disabled button after clicking save button', (done) => { - vm.$el.querySelector('.btn-confirm').click(); - - Vue.nextTick(() => { - expect(vm.$el.querySelector('.btn-confirm').getAttribute('disabled')).toBe('disabled'); + it('sends update.issauble event when clicking save button', () => { + findSaveButton().vm.$emit('click', { preventDefault: jest.fn() }); - done(); - }); + expect(eventHub.$emit).toHaveBeenCalledWith('update.issuable'); }); }); describe('closeForm', () => { + beforeEach(() => { + jest.spyOn(eventHub, '$emit').mockImplementation(() => {}); + }); + it('emits close.form when clicking cancel', () => { - vm.$el.querySelector('.btn-default').click(); + findCancelButton().vm.$emit('click'); expect(eventHub.$emit).toHaveBeenCalledWith('close.form'); }); }); - describe('deleteIssuable', () => { - it('sends delete.issuable event when clicking save button', () => { - jest.spyOn(window, 'confirm').mockReturnValue(true); - vm.$el.querySelector('.btn-danger').click(); + describe('renders create modal with the correct information', () => { + it('renders correct modal id', () => { + expect(findModal().attributes('modalid')).toBe(modalId); + }); + }); - expect(eventHub.$emit).toHaveBeenCalledWith('delete.issuable', { destroy_confirm: true }); + describe('deleteIssuable', () => { + beforeEach(() => { + jest.spyOn(eventHub, '$emit').mockImplementation(() => {}); }); - it('does no actions when confirm is false', (done) => { - jest.spyOn(window, 'confirm').mockReturnValue(false); - vm.$el.querySelector('.btn-danger').click(); + it('does not send the `delete.issuable` event when clicking delete button', () => { + findDeleteButton().vm.$emit('click'); + expect(eventHub.$emit).not.toHaveBeenCalled(); + }); - Vue.nextTick(() => { - expect(eventHub.$emit).not.toHaveBeenCalledWith('delete.issuable'); + it('sends the `delete.issuable` event when clicking the delete confirm button', async () => { + expect(eventHub.$emit).toHaveBeenCalledTimes(0); + await deleteIssuable(wrapper); + expect(eventHub.$emit).toHaveBeenCalledWith('delete.issuable', { destroy_confirm: true }); + expect(eventHub.$emit).toHaveBeenCalledTimes(1); + }); + }); - expect(vm.$el.querySelector('.btn-danger .fa')).toBeNull(); + describe('with Apollo cache mock', () => { + it('renders the right delete button text per apollo cache type', async () => { + mockIssueStateData.mockResolvedValue(getIssueStateQueryResponse); + await waitForPromises(); + expect(findDeleteButton().text()).toBe('Delete issue'); + }); - done(); - }); + it('should not change the delete button text per apollo cache mutation', async () => { + mockIssueStateData.mockResolvedValue(updateIssueStateQueryResponse); + await waitForPromises(); + expect(findDeleteButton().text()).toBe('Delete issue'); }); }); }); diff --git a/spec/frontend/issue_show/components/fields/type_spec.js b/spec/frontend/issue_show/components/fields/type_spec.js new file mode 100644 index 00000000000..0c8af60d50d --- /dev/null +++ b/spec/frontend/issue_show/components/fields/type_spec.js @@ -0,0 +1,84 @@ +import { GlFormGroup, GlDropdown, GlDropdownItem } from '@gitlab/ui'; +import { shallowMount, createLocalVue } from '@vue/test-utils'; +import VueApollo from 'vue-apollo'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import waitForPromises from 'helpers/wait_for_promises'; +import IssueTypeField, { i18n } from '~/issue_show/components/fields/type.vue'; +import { IssuableTypes } from '~/issue_show/constants'; +import { + getIssueStateQueryResponse, + updateIssueStateQueryResponse, +} from '../../mock_data/apollo_mock'; + +const localVue = createLocalVue(); +localVue.use(VueApollo); + +describe('Issue type field component', () => { + let wrapper; + let fakeApollo; + let mockIssueStateData; + + const mockResolvers = { + Query: { + issueState() { + return { + __typename: 'IssueState', + rawData: mockIssueStateData(), + }; + }, + }, + Mutation: { + updateIssueState: jest.fn().mockResolvedValue(updateIssueStateQueryResponse), + }, + }; + + const findTypeFromGroup = () => wrapper.findComponent(GlFormGroup); + const findTypeFromDropDown = () => wrapper.findComponent(GlDropdown); + const findTypeFromDropDownItems = () => wrapper.findAllComponents(GlDropdownItem); + + const createComponent = ({ data } = {}) => { + fakeApollo = createMockApollo([], mockResolvers); + + wrapper = shallowMount(IssueTypeField, { + localVue, + apolloProvider: fakeApollo, + data() { + return { + issueState: {}, + ...data, + }; + }, + }); + }; + + beforeEach(() => { + mockIssueStateData = jest.fn(); + createComponent(); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('renders a form group with the correct label', () => { + expect(findTypeFromGroup().attributes('label')).toBe(i18n.label); + }); + + it('renders a form select with the `issue_type` value', () => { + expect(findTypeFromDropDown().attributes('value')).toBe(IssuableTypes.issue); + }); + + describe('with Apollo cache mock', () => { + it('renders the selected issueType', async () => { + mockIssueStateData.mockResolvedValue(getIssueStateQueryResponse); + await waitForPromises(); + expect(findTypeFromDropDown().attributes('value')).toBe(IssuableTypes.issue); + }); + + it('updates the `issue_type` in the apollo cache when the value is changed', async () => { + findTypeFromDropDownItems().at(1).vm.$emit('click', IssuableTypes.incident); + await wrapper.vm.$nextTick(); + expect(findTypeFromDropDown().attributes('value')).toBe(IssuableTypes.incident); + }); + }); +}); diff --git a/spec/frontend/issue_show/components/form_spec.js b/spec/frontend/issue_show/components/form_spec.js index 6d4807c4261..28498cb90ec 100644 --- a/spec/frontend/issue_show/components/form_spec.js +++ b/spec/frontend/issue_show/components/form_spec.js @@ -2,6 +2,7 @@ import { GlAlert } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import Autosave from '~/autosave'; import DescriptionTemplate from '~/issue_show/components/fields/description_template.vue'; +import IssueTypeField from '~/issue_show/components/fields/type.vue'; import formComponent from '~/issue_show/components/form.vue'; import LockedWarning from '~/issue_show/components/locked_warning.vue'; import eventHub from '~/issue_show/event_hub'; @@ -39,6 +40,7 @@ describe('Inline edit form component', () => { }; const findDescriptionTemplate = () => wrapper.findComponent(DescriptionTemplate); + const findIssuableTypeField = () => wrapper.findComponent(IssueTypeField); const findLockedWarning = () => wrapper.findComponent(LockedWarning); const findAlert = () => wrapper.findComponent(GlAlert); @@ -68,6 +70,21 @@ describe('Inline edit form component', () => { expect(findDescriptionTemplate().exists()).toBe(true); }); + it.each` + issuableType | value + ${'issue'} | ${true} + ${'epic'} | ${false} + `( + 'when `issue_type` is set to "$issuableType" rendering the type select will be "$value"', + ({ issuableType, value }) => { + createComponent({ + issuableType, + }); + + expect(findIssuableTypeField().exists()).toBe(value); + }, + ); + it('hides locked warning by default', () => { createComponent(); diff --git a/spec/frontend/issue_show/components/incidents/incident_tabs_spec.js b/spec/frontend/issue_show/components/incidents/incident_tabs_spec.js index f46b6ba6f54..6b9f5b17e99 100644 --- a/spec/frontend/issue_show/components/incidents/incident_tabs_spec.js +++ b/spec/frontend/issue_show/components/incidents/incident_tabs_spec.js @@ -9,7 +9,7 @@ import IncidentTabs from '~/issue_show/components/incidents/incident_tabs.vue'; import INVALID_URL from '~/lib/utils/invalid_url'; import Tracking from '~/tracking'; import AlertDetailsTable from '~/vue_shared/components/alert_details_table.vue'; -import { descriptionProps } from '../../mock_data'; +import { descriptionProps } from '../../mock_data/mock_data'; const mockAlert = { __typename: 'AlertManagementAlert', |