diff options
Diffstat (limited to 'spec/frontend/issue_show/components/app_spec.js')
-rw-r--r-- | spec/frontend/issue_show/components/app_spec.js | 335 |
1 files changed, 203 insertions, 132 deletions
diff --git a/spec/frontend/issue_show/components/app_spec.js b/spec/frontend/issue_show/components/app_spec.js index a59d6d35ded..d970fd349e7 100644 --- a/spec/frontend/issue_show/components/app_spec.js +++ b/spec/frontend/issue_show/components/app_spec.js @@ -1,10 +1,11 @@ -import Vue from 'vue'; +import { GlIntersectionObserver } from '@gitlab/ui'; +import { mount } from '@vue/test-utils'; import MockAdapter from 'axios-mock-adapter'; import { TEST_HOST } from 'helpers/test_constants'; import axios from '~/lib/utils/axios_utils'; import { visitUrl } from '~/lib/utils/url_utility'; import '~/behaviors/markdown/render_gfm'; -import issuableApp from '~/issue_show/components/app.vue'; +import IssuableApp from '~/issue_show/components/app.vue'; import eventHub from '~/issue_show/event_hub'; import { initialRequest, secondRequest } from '../mock_data'; @@ -17,10 +18,15 @@ jest.mock('~/issue_show/event_hub'); const REALTIME_REQUEST_STACK = [initialRequest, secondRequest]; +const zoomMeetingUrl = 'https://gitlab.zoom.us/j/95919234811'; +const publishedIncidentUrl = 'https://status.com/'; + describe('Issuable output', () => { let mock; let realtimeRequestCount = 0; - let vm; + let wrapper; + + const findStickyHeader = () => wrapper.find('[data-testid="issue-sticky-header"]'); beforeEach(() => { setFixtures(` @@ -39,7 +45,10 @@ describe('Issuable output', () => { </div> `); - const IssuableDescriptionComponent = Vue.extend(issuableApp); + window.IntersectionObserver = class { + disconnect = jest.fn(); + observe = jest.fn(); + }; mock = new MockAdapter(axios); mock @@ -50,13 +59,14 @@ describe('Issuable output', () => { return res; }); - vm = new IssuableDescriptionComponent({ + wrapper = mount(IssuableApp, { propsData: { canUpdate: true, canDestroy: true, endpoint: '/gitlab-org/gitlab-shell/-/issues/9/realtime_changes', updateEndpoint: TEST_HOST, issuableRef: '#1', + issuableStatus: 'opened', initialTitleHtml: '', initialTitleText: '', initialDescriptionHtml: 'test', @@ -67,16 +77,20 @@ describe('Issuable output', () => { projectNamespace: '/', projectPath: '/', issuableTemplateNamesPath: '/issuable-templates-path', + zoomMeetingUrl, + publishedIncidentUrl, }, - }).$mount(); + }); }); afterEach(() => { + delete window.IntersectionObserver; mock.restore(); realtimeRequestCount = 0; - vm.poll.stop(); - vm.$destroy(); + wrapper.vm.poll.stop(); + wrapper.destroy(); + wrapper = null; }); it('should render a title/description/edited and update title/description/edited on update', () => { @@ -84,196 +98,209 @@ describe('Issuable output', () => { return axios .waitForAll() .then(() => { - editedText = vm.$el.querySelector('.edited-text'); + editedText = wrapper.find('.edited-text'); }) .then(() => { expect(document.querySelector('title').innerText).toContain('this is a title (#1)'); - expect(vm.$el.querySelector('.title').innerHTML).toContain('<p>this is a title</p>'); - expect(vm.$el.querySelector('.md').innerHTML).toContain('<p>this is a description!</p>'); - expect(vm.$el.querySelector('.js-task-list-field').value).toContain( + expect(wrapper.find('.title').text()).toContain('this is a title'); + expect(wrapper.find('.md').text()).toContain('this is a description!'); + expect(wrapper.find('.js-task-list-field').element.value).toContain( 'this is a description', ); - expect(formatText(editedText.innerText)).toMatch(/Edited[\s\S]+?by Some User/); - expect(editedText.querySelector('.author-link').href).toMatch(/\/some_user$/); - expect(editedText.querySelector('time')).toBeTruthy(); - expect(vm.state.lock_version).toEqual(1); + 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); }) .then(() => { - vm.poll.makeRequest(); + wrapper.vm.poll.makeRequest(); return axios.waitForAll(); }) .then(() => { expect(document.querySelector('title').innerText).toContain('2 (#1)'); - expect(vm.$el.querySelector('.title').innerHTML).toContain('<p>2</p>'); - expect(vm.$el.querySelector('.md').innerHTML).toContain('<p>42</p>'); - expect(vm.$el.querySelector('.js-task-list-field').value).toContain('42'); - expect(vm.$el.querySelector('.edited-text')).toBeTruthy(); - expect(formatText(vm.$el.querySelector('.edited-text').innerText)).toMatch( + expect(wrapper.find('.title').text()).toContain('2'); + expect(wrapper.find('.md').text()).toContain('42'); + expect(wrapper.find('.js-task-list-field').element.value).toContain('42'); + expect(wrapper.find('.edited-text').text()).toBeTruthy(); + expect(formatText(wrapper.find('.edited-text').text())).toMatch( /Edited[\s\S]+?by Other User/, ); - expect(editedText.querySelector('.author-link').href).toMatch(/\/other_user$/); - expect(editedText.querySelector('time')).toBeTruthy(); - expect(vm.state.lock_version).toEqual(2); + expect(editedText.find('.author-link').attributes('href')).toMatch(/\/other_user$/); + expect(editedText.find('time').text()).toBeTruthy(); + expect(wrapper.vm.state.lock_version).toEqual(2); }); }); it('shows actions if permissions are correct', () => { - vm.showForm = true; + wrapper.vm.showForm = true; - return vm.$nextTick().then(() => { - expect(vm.$el.querySelector('.btn')).not.toBeNull(); + return wrapper.vm.$nextTick().then(() => { + expect(wrapper.contains('.markdown-selector')).toBe(true); }); }); it('does not show actions if permissions are incorrect', () => { - vm.showForm = true; - vm.canUpdate = false; + wrapper.vm.showForm = true; + wrapper.setProps({ canUpdate: false }); - return vm.$nextTick().then(() => { - expect(vm.$el.querySelector('.btn')).toBeNull(); + return wrapper.vm.$nextTick().then(() => { + expect(wrapper.contains('.markdown-selector')).toBe(false); }); }); it('does not update formState if form is already open', () => { - vm.updateAndShowForm(); + wrapper.vm.updateAndShowForm(); - vm.state.titleText = 'testing 123'; + wrapper.vm.state.titleText = 'testing 123'; - vm.updateAndShowForm(); + wrapper.vm.updateAndShowForm(); - return vm.$nextTick().then(() => { - expect(vm.store.formState.title).not.toBe('testing 123'); + return wrapper.vm.$nextTick().then(() => { + expect(wrapper.vm.store.formState.title).not.toBe('testing 123'); }); }); it('opens reCAPTCHA modal if update rejected as spam', () => { let modal; - jest.spyOn(vm.service, 'updateIssuable').mockResolvedValue({ + jest.spyOn(wrapper.vm.service, 'updateIssuable').mockResolvedValue({ data: { recaptcha_html: '<div class="g-recaptcha">recaptcha_html</div>', }, }); - vm.canUpdate = true; - vm.showForm = true; + wrapper.vm.canUpdate = true; + wrapper.vm.showForm = true; - return vm + return wrapper.vm .$nextTick() .then(() => { - vm.$refs.recaptchaModal.scriptSrc = '//scriptsrc'; - return vm.updateIssuable(); + wrapper.vm.$refs.recaptchaModal.scriptSrc = '//scriptsrc'; + return wrapper.vm.updateIssuable(); }) .then(() => { - modal = vm.$el.querySelector('.js-recaptcha-modal'); - - expect(modal.style.display).not.toEqual('none'); - expect(modal.querySelector('.g-recaptcha').textContent).toEqual('recaptcha_html'); + modal = wrapper.find('.js-recaptcha-modal'); + expect(modal.isVisible()).toBe(true); + expect(modal.find('.g-recaptcha').text()).toEqual('recaptcha_html'); expect(document.body.querySelector('.js-recaptcha-script').src).toMatch('//scriptsrc'); }) .then(() => { - modal.querySelector('.close').click(); - return vm.$nextTick(); + modal.find('.close').trigger('click'); + return wrapper.vm.$nextTick(); }) .then(() => { - expect(modal.style.display).toEqual('none'); + expect(modal.isVisible()).toBe(false); expect(document.body.querySelector('.js-recaptcha-script')).toBeNull(); }); }); + describe('Pinned links propagated', () => { + it.each` + prop | value + ${'zoomMeetingUrl'} | ${zoomMeetingUrl} + ${'publishedIncidentUrl'} | ${publishedIncidentUrl} + `('sets the $prop correctly on underlying pinned links', ({ prop, value }) => { + expect(wrapper.vm[prop]).toEqual(value); + expect(wrapper.find(`[data-testid="${prop}"]`).attributes('href')).toBe(value); + }); + }); + describe('updateIssuable', () => { it('fetches new data after update', () => { - const updateStoreSpy = jest.spyOn(vm, 'updateStoreState'); - const getDataSpy = jest.spyOn(vm.service, 'getData'); - jest.spyOn(vm.service, 'updateIssuable').mockResolvedValue({ + const updateStoreSpy = jest.spyOn(wrapper.vm, 'updateStoreState'); + const getDataSpy = jest.spyOn(wrapper.vm.service, 'getData'); + jest.spyOn(wrapper.vm.service, 'updateIssuable').mockResolvedValue({ data: { web_url: window.location.pathname }, }); - return vm.updateIssuable().then(() => { + return wrapper.vm.updateIssuable().then(() => { expect(updateStoreSpy).toHaveBeenCalled(); expect(getDataSpy).toHaveBeenCalled(); }); }); it('correctly updates issuable data', () => { - const spy = jest.spyOn(vm.service, 'updateIssuable').mockResolvedValue({ + const spy = jest.spyOn(wrapper.vm.service, 'updateIssuable').mockResolvedValue({ data: { web_url: window.location.pathname }, }); - return vm.updateIssuable().then(() => { - expect(spy).toHaveBeenCalledWith(vm.formState); + return wrapper.vm.updateIssuable().then(() => { + expect(spy).toHaveBeenCalledWith(wrapper.vm.formState); expect(eventHub.$emit).toHaveBeenCalledWith('close.form'); }); }); it('does not redirect if issue has not moved', () => { - jest.spyOn(vm.service, 'updateIssuable').mockResolvedValue({ + jest.spyOn(wrapper.vm.service, 'updateIssuable').mockResolvedValue({ data: { web_url: window.location.pathname, - confidential: vm.isConfidential, + confidential: wrapper.vm.isConfidential, }, }); - return vm.updateIssuable().then(() => { + return wrapper.vm.updateIssuable().then(() => { expect(visitUrl).not.toHaveBeenCalled(); }); }); it('does not redirect if issue has not moved and user has switched tabs', () => { - jest.spyOn(vm.service, 'updateIssuable').mockResolvedValue({ + jest.spyOn(wrapper.vm.service, 'updateIssuable').mockResolvedValue({ data: { web_url: '', - confidential: vm.isConfidential, + confidential: wrapper.vm.isConfidential, }, }); - return vm.updateIssuable().then(() => { + return wrapper.vm.updateIssuable().then(() => { expect(visitUrl).not.toHaveBeenCalled(); }); }); it('redirects if returned web_url has changed', () => { - jest.spyOn(vm.service, 'updateIssuable').mockResolvedValue({ + jest.spyOn(wrapper.vm.service, 'updateIssuable').mockResolvedValue({ data: { web_url: '/testing-issue-move', - confidential: vm.isConfidential, + confidential: wrapper.vm.isConfidential, }, }); - vm.updateIssuable(); + wrapper.vm.updateIssuable(); - return vm.updateIssuable().then(() => { + return wrapper.vm.updateIssuable().then(() => { expect(visitUrl).toHaveBeenCalledWith('/testing-issue-move'); }); }); describe('shows dialog when issue has unsaved changed', () => { it('confirms on title change', () => { - vm.showForm = true; - vm.state.titleText = 'title has changed'; + wrapper.vm.showForm = true; + wrapper.vm.state.titleText = 'title has changed'; const e = { returnValue: null }; - vm.handleBeforeUnloadEvent(e); - return vm.$nextTick().then(() => { + wrapper.vm.handleBeforeUnloadEvent(e); + + return wrapper.vm.$nextTick().then(() => { expect(e.returnValue).not.toBeNull(); }); }); it('confirms on description change', () => { - vm.showForm = true; - vm.state.descriptionText = 'description has changed'; + wrapper.vm.showForm = true; + wrapper.vm.state.descriptionText = 'description has changed'; const e = { returnValue: null }; - vm.handleBeforeUnloadEvent(e); - return vm.$nextTick().then(() => { + wrapper.vm.handleBeforeUnloadEvent(e); + + return wrapper.vm.$nextTick().then(() => { expect(e.returnValue).not.toBeNull(); }); }); it('does nothing when nothing has changed', () => { const e = { returnValue: null }; - vm.handleBeforeUnloadEvent(e); - return vm.$nextTick().then(() => { + wrapper.vm.handleBeforeUnloadEvent(e); + + return wrapper.vm.$nextTick().then(() => { expect(e.returnValue).toBeNull(); }); }); @@ -281,8 +308,9 @@ describe('Issuable output', () => { describe('error when updating', () => { it('closes form on error', () => { - jest.spyOn(vm.service, 'updateIssuable').mockRejectedValue(); - return vm.updateIssuable().then(() => { + jest.spyOn(wrapper.vm.service, 'updateIssuable').mockRejectedValue(); + + return wrapper.vm.updateIssuable().then(() => { expect(eventHub.$emit).not.toHaveBeenCalledWith('close.form'); expect(document.querySelector('.flash-container .flash-text').innerText.trim()).toBe( `Error updating issue`, @@ -291,12 +319,12 @@ describe('Issuable output', () => { }); it('returns the correct error message for issuableType', () => { - jest.spyOn(vm.service, 'updateIssuable').mockRejectedValue(); - vm.issuableType = 'merge request'; + jest.spyOn(wrapper.vm.service, 'updateIssuable').mockRejectedValue(); + wrapper.setProps({ issuableType: 'merge request' }); - return vm + return wrapper.vm .$nextTick() - .then(vm.updateIssuable) + .then(wrapper.vm.updateIssuable) .then(() => { expect(eventHub.$emit).not.toHaveBeenCalledWith('close.form'); expect(document.querySelector('.flash-container .flash-text').innerText.trim()).toBe( @@ -308,12 +336,12 @@ describe('Issuable output', () => { it('shows error message from backend if exists', () => { const msg = 'Custom error message from backend'; jest - .spyOn(vm.service, 'updateIssuable') + .spyOn(wrapper.vm.service, 'updateIssuable') .mockRejectedValue({ response: { data: { errors: [msg] } } }); - return vm.updateIssuable().then(() => { + return wrapper.vm.updateIssuable().then(() => { expect(document.querySelector('.flash-container .flash-text').innerText.trim()).toBe( - `${vm.defaultErrorMessage}. ${msg}`, + `${wrapper.vm.defaultErrorMessage}. ${msg}`, ); }); }); @@ -322,34 +350,34 @@ describe('Issuable output', () => { describe('deleteIssuable', () => { it('changes URL when deleted', () => { - jest.spyOn(vm.service, 'deleteIssuable').mockResolvedValue({ + jest.spyOn(wrapper.vm.service, 'deleteIssuable').mockResolvedValue({ data: { web_url: '/test', }, }); - return vm.deleteIssuable().then(() => { + return wrapper.vm.deleteIssuable().then(() => { expect(visitUrl).toHaveBeenCalledWith('/test'); }); }); it('stops polling when deleting', () => { - const spy = jest.spyOn(vm.poll, 'stop'); - jest.spyOn(vm.service, 'deleteIssuable').mockResolvedValue({ + const spy = jest.spyOn(wrapper.vm.poll, 'stop'); + jest.spyOn(wrapper.vm.service, 'deleteIssuable').mockResolvedValue({ data: { web_url: '/test', }, }); - return vm.deleteIssuable().then(() => { + return wrapper.vm.deleteIssuable().then(() => { expect(spy).toHaveBeenCalledWith(); }); }); it('closes form on error', () => { - jest.spyOn(vm.service, 'deleteIssuable').mockRejectedValue(); + jest.spyOn(wrapper.vm.service, 'deleteIssuable').mockRejectedValue(); - return vm.deleteIssuable().then(() => { + return wrapper.vm.deleteIssuable().then(() => { expect(eventHub.$emit).not.toHaveBeenCalledWith('close.form'); expect(document.querySelector('.flash-container .flash-text').innerText.trim()).toBe( 'Error deleting issue', @@ -360,23 +388,25 @@ describe('Issuable output', () => { describe('updateAndShowForm', () => { it('shows locked warning if form is open & data is different', () => { - return vm + return wrapper.vm .$nextTick() .then(() => { - vm.updateAndShowForm(); + wrapper.vm.updateAndShowForm(); - vm.poll.makeRequest(); + wrapper.vm.poll.makeRequest(); return new Promise(resolve => { - vm.$watch('formState.lockedWarningVisible', value => { - if (value) resolve(); + wrapper.vm.$watch('formState.lockedWarningVisible', value => { + if (value) { + resolve(); + } }); }); }) .then(() => { - expect(vm.formState.lockedWarningVisible).toEqual(true); - expect(vm.formState.lock_version).toEqual(1); - expect(vm.$el.querySelector('.alert')).not.toBeNull(); + expect(wrapper.vm.formState.lockedWarningVisible).toEqual(true); + expect(wrapper.vm.formState.lock_version).toEqual(1); + expect(wrapper.contains('.alert')).toBe(true); }); }); }); @@ -385,14 +415,14 @@ describe('Issuable output', () => { let formSpy; beforeEach(() => { - formSpy = jest.spyOn(vm, 'updateAndShowForm'); + formSpy = jest.spyOn(wrapper.vm, 'updateAndShowForm'); }); it('shows the form if template names request is successful', () => { const mockData = [{ name: 'Bug' }]; mock.onGet('/issuable-templates-path').reply(() => Promise.resolve([200, mockData])); - return vm.requestTemplatesAndShowForm().then(() => { + return wrapper.vm.requestTemplatesAndShowForm().then(() => { expect(formSpy).toHaveBeenCalledWith(mockData); }); }); @@ -402,7 +432,7 @@ describe('Issuable output', () => { .onGet('/issuable-templates-path') .reply(() => Promise.reject(new Error('something went wrong'))); - return vm.requestTemplatesAndShowForm().then(() => { + return wrapper.vm.requestTemplatesAndShowForm().then(() => { expect(document.querySelector('.flash-container .flash-text').textContent).toContain( 'Error updating issue', ); @@ -414,35 +444,39 @@ describe('Issuable output', () => { describe('show inline edit button', () => { it('should not render by default', () => { - expect(vm.$el.querySelector('.title-container .note-action-button')).toBeDefined(); + expect(wrapper.contains('.btn-edit')).toBe(true); }); it('should render if showInlineEditButton', () => { - vm.showInlineEditButton = true; + wrapper.setProps({ showInlineEditButton: true }); - expect(vm.$el.querySelector('.title-container .note-action-button')).toBeDefined(); + return wrapper.vm.$nextTick(() => { + expect(wrapper.contains('.btn-edit')).toBe(true); + }); }); }); describe('updateStoreState', () => { it('should make a request and update the state of the store', () => { const data = { foo: 1 }; - const getDataSpy = jest.spyOn(vm.service, 'getData').mockResolvedValue({ data }); - const updateStateSpy = jest.spyOn(vm.store, 'updateState').mockImplementation(jest.fn); + const getDataSpy = jest.spyOn(wrapper.vm.service, 'getData').mockResolvedValue({ data }); + const updateStateSpy = jest + .spyOn(wrapper.vm.store, 'updateState') + .mockImplementation(jest.fn); - return vm.updateStoreState().then(() => { + return wrapper.vm.updateStoreState().then(() => { expect(getDataSpy).toHaveBeenCalled(); expect(updateStateSpy).toHaveBeenCalledWith(data); }); }); it('should show error message if store update fails', () => { - jest.spyOn(vm.service, 'getData').mockRejectedValue(); - vm.issuableType = 'merge request'; + jest.spyOn(wrapper.vm.service, 'getData').mockRejectedValue(); + wrapper.setProps({ issuableType: 'merge request' }); - return vm.updateStoreState().then(() => { + return wrapper.vm.updateStoreState().then(() => { expect(document.querySelector('.flash-container .flash-text').innerText.trim()).toBe( - `Error updating ${vm.issuableType}`, + `Error updating ${wrapper.vm.issuableType}`, ); }); }); @@ -450,48 +484,85 @@ describe('Issuable output', () => { describe('issueChanged', () => { beforeEach(() => { - vm.store.formState.title = ''; - vm.store.formState.description = ''; - vm.initialDescriptionText = ''; - vm.initialTitleText = ''; + wrapper.vm.store.formState.title = ''; + wrapper.vm.store.formState.description = ''; + wrapper.setProps({ + initialDescriptionText: '', + initialTitleText: '', + }); }); it('returns true when title is changed', () => { - vm.store.formState.title = 'RandomText'; + wrapper.vm.store.formState.title = 'RandomText'; - expect(vm.issueChanged).toBe(true); + expect(wrapper.vm.issueChanged).toBe(true); }); it('returns false when title is empty null', () => { - vm.store.formState.title = null; + wrapper.vm.store.formState.title = null; - expect(vm.issueChanged).toBe(false); + expect(wrapper.vm.issueChanged).toBe(false); }); it('returns false when `initialTitleText` is null and `formState.title` is empty string', () => { - vm.store.formState.title = ''; - vm.initialTitleText = null; + wrapper.vm.store.formState.title = ''; + wrapper.setProps({ initialTitleText: null }); - expect(vm.issueChanged).toBe(false); + expect(wrapper.vm.issueChanged).toBe(false); }); it('returns true when description is changed', () => { - vm.store.formState.description = 'RandomText'; + wrapper.vm.store.formState.description = 'RandomText'; - expect(vm.issueChanged).toBe(true); + expect(wrapper.vm.issueChanged).toBe(true); }); it('returns false when description is empty null', () => { - vm.store.formState.title = null; + wrapper.vm.store.formState.description = null; - expect(vm.issueChanged).toBe(false); + expect(wrapper.vm.issueChanged).toBe(false); }); it('returns false when `initialDescriptionText` is null and `formState.description` is empty string', () => { - vm.store.formState.description = ''; - vm.initialDescriptionText = null; + wrapper.vm.store.formState.description = ''; + wrapper.setProps({ initialDescriptionText: null }); - expect(vm.issueChanged).toBe(false); + expect(wrapper.vm.issueChanged).toBe(false); + }); + }); + + describe('sticky header', () => { + describe('when title is in view', () => { + it('is not shown', () => { + expect(wrapper.contains('.issue-sticky-header')).toBe(false); + }); + }); + + describe('when title is not in view', () => { + beforeEach(() => { + wrapper.vm.state.titleText = 'Sticky header title'; + wrapper.find(GlIntersectionObserver).vm.$emit('disappear'); + }); + + it('is shown with title', () => { + expect(findStickyHeader().text()).toContain('Sticky header title'); + }); + + it('is shown with Open when status is opened', () => { + wrapper.setProps({ issuableStatus: 'opened' }); + + return wrapper.vm.$nextTick(() => { + expect(findStickyHeader().text()).toContain('Open'); + }); + }); + + it('is shown with Closed when status is closed', () => { + wrapper.setProps({ issuableStatus: 'closed' }); + + return wrapper.vm.$nextTick(() => { + expect(findStickyHeader().text()).toContain('Closed'); + }); + }); }); }); }); |