diff options
Diffstat (limited to 'spec/frontend/issue_show')
6 files changed, 969 insertions, 0 deletions
diff --git a/spec/frontend/issue_show/components/app_spec.js b/spec/frontend/issue_show/components/app_spec.js new file mode 100644 index 00000000000..a59d6d35ded --- /dev/null +++ b/spec/frontend/issue_show/components/app_spec.js @@ -0,0 +1,497 @@ +import Vue from 'vue'; +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 eventHub from '~/issue_show/event_hub'; +import { initialRequest, secondRequest } from '../mock_data'; + +function formatText(text) { + return text.trim().replace(/\s\s+/g, ' '); +} + +jest.mock('~/lib/utils/url_utility'); +jest.mock('~/issue_show/event_hub'); + +const REALTIME_REQUEST_STACK = [initialRequest, secondRequest]; + +describe('Issuable output', () => { + let mock; + let realtimeRequestCount = 0; + let vm; + + beforeEach(() => { + setFixtures(` + <div> + <title>Title</title> + <div class="detail-page-description content-block"> + <details open> + <summary>One</summary> + </details> + <details> + <summary>Two</summary> + </details> + </div> + <div class="flash-container"></div> + <span id="task_status"></span> + </div> + `); + + const IssuableDescriptionComponent = Vue.extend(issuableApp); + + mock = new MockAdapter(axios); + mock + .onGet('/gitlab-org/gitlab-shell/-/issues/9/realtime_changes/realtime_changes') + .reply(() => { + const res = Promise.resolve([200, REALTIME_REQUEST_STACK[realtimeRequestCount]]); + realtimeRequestCount += 1; + return res; + }); + + vm = new IssuableDescriptionComponent({ + propsData: { + canUpdate: true, + canDestroy: true, + endpoint: '/gitlab-org/gitlab-shell/-/issues/9/realtime_changes', + updateEndpoint: TEST_HOST, + issuableRef: '#1', + initialTitleHtml: '', + initialTitleText: '', + initialDescriptionHtml: 'test', + initialDescriptionText: 'test', + lockVersion: 1, + markdownPreviewPath: '/', + markdownDocsPath: '/', + projectNamespace: '/', + projectPath: '/', + issuableTemplateNamesPath: '/issuable-templates-path', + }, + }).$mount(); + }); + + afterEach(() => { + mock.restore(); + realtimeRequestCount = 0; + + vm.poll.stop(); + vm.$destroy(); + }); + + it('should render a title/description/edited and update title/description/edited on update', () => { + let editedText; + return axios + .waitForAll() + .then(() => { + editedText = vm.$el.querySelector('.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( + '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); + }) + .then(() => { + 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( + /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); + }); + }); + + it('shows actions if permissions are correct', () => { + vm.showForm = true; + + return vm.$nextTick().then(() => { + expect(vm.$el.querySelector('.btn')).not.toBeNull(); + }); + }); + + it('does not show actions if permissions are incorrect', () => { + vm.showForm = true; + vm.canUpdate = false; + + return vm.$nextTick().then(() => { + expect(vm.$el.querySelector('.btn')).toBeNull(); + }); + }); + + it('does not update formState if form is already open', () => { + vm.updateAndShowForm(); + + vm.state.titleText = 'testing 123'; + + vm.updateAndShowForm(); + + return vm.$nextTick().then(() => { + expect(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({ + data: { + recaptcha_html: '<div class="g-recaptcha">recaptcha_html</div>', + }, + }); + + vm.canUpdate = true; + vm.showForm = true; + + return vm + .$nextTick() + .then(() => { + vm.$refs.recaptchaModal.scriptSrc = '//scriptsrc'; + return 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'); + expect(document.body.querySelector('.js-recaptcha-script').src).toMatch('//scriptsrc'); + }) + .then(() => { + modal.querySelector('.close').click(); + return vm.$nextTick(); + }) + .then(() => { + expect(modal.style.display).toEqual('none'); + expect(document.body.querySelector('.js-recaptcha-script')).toBeNull(); + }); + }); + + 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({ + data: { web_url: window.location.pathname }, + }); + + return vm.updateIssuable().then(() => { + expect(updateStoreSpy).toHaveBeenCalled(); + expect(getDataSpy).toHaveBeenCalled(); + }); + }); + + it('correctly updates issuable data', () => { + const spy = jest.spyOn(vm.service, 'updateIssuable').mockResolvedValue({ + data: { web_url: window.location.pathname }, + }); + + return vm.updateIssuable().then(() => { + expect(spy).toHaveBeenCalledWith(vm.formState); + expect(eventHub.$emit).toHaveBeenCalledWith('close.form'); + }); + }); + + it('does not redirect if issue has not moved', () => { + jest.spyOn(vm.service, 'updateIssuable').mockResolvedValue({ + data: { + web_url: window.location.pathname, + confidential: vm.isConfidential, + }, + }); + + return 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({ + data: { + web_url: '', + confidential: vm.isConfidential, + }, + }); + + return vm.updateIssuable().then(() => { + expect(visitUrl).not.toHaveBeenCalled(); + }); + }); + + it('redirects if returned web_url has changed', () => { + jest.spyOn(vm.service, 'updateIssuable').mockResolvedValue({ + data: { + web_url: '/testing-issue-move', + confidential: vm.isConfidential, + }, + }); + + vm.updateIssuable(); + + return 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'; + const e = { returnValue: null }; + vm.handleBeforeUnloadEvent(e); + return vm.$nextTick().then(() => { + expect(e.returnValue).not.toBeNull(); + }); + }); + + it('confirms on description change', () => { + vm.showForm = true; + vm.state.descriptionText = 'description has changed'; + const e = { returnValue: null }; + vm.handleBeforeUnloadEvent(e); + return 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(() => { + expect(e.returnValue).toBeNull(); + }); + }); + }); + + describe('error when updating', () => { + it('closes form on error', () => { + jest.spyOn(vm.service, 'updateIssuable').mockRejectedValue(); + return vm.updateIssuable().then(() => { + expect(eventHub.$emit).not.toHaveBeenCalledWith('close.form'); + expect(document.querySelector('.flash-container .flash-text').innerText.trim()).toBe( + `Error updating issue`, + ); + }); + }); + + it('returns the correct error message for issuableType', () => { + jest.spyOn(vm.service, 'updateIssuable').mockRejectedValue(); + vm.issuableType = 'merge request'; + + return vm + .$nextTick() + .then(vm.updateIssuable) + .then(() => { + expect(eventHub.$emit).not.toHaveBeenCalledWith('close.form'); + expect(document.querySelector('.flash-container .flash-text').innerText.trim()).toBe( + `Error updating merge request`, + ); + }); + }); + + it('shows error message from backend if exists', () => { + const msg = 'Custom error message from backend'; + jest + .spyOn(vm.service, 'updateIssuable') + .mockRejectedValue({ response: { data: { errors: [msg] } } }); + + return vm.updateIssuable().then(() => { + expect(document.querySelector('.flash-container .flash-text').innerText.trim()).toBe( + `${vm.defaultErrorMessage}. ${msg}`, + ); + }); + }); + }); + }); + + describe('deleteIssuable', () => { + it('changes URL when deleted', () => { + jest.spyOn(vm.service, 'deleteIssuable').mockResolvedValue({ + data: { + web_url: '/test', + }, + }); + + return 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({ + data: { + web_url: '/test', + }, + }); + + return vm.deleteIssuable().then(() => { + expect(spy).toHaveBeenCalledWith(); + }); + }); + + it('closes form on error', () => { + jest.spyOn(vm.service, 'deleteIssuable').mockRejectedValue(); + + return vm.deleteIssuable().then(() => { + expect(eventHub.$emit).not.toHaveBeenCalledWith('close.form'); + expect(document.querySelector('.flash-container .flash-text').innerText.trim()).toBe( + 'Error deleting issue', + ); + }); + }); + }); + + describe('updateAndShowForm', () => { + it('shows locked warning if form is open & data is different', () => { + return vm + .$nextTick() + .then(() => { + vm.updateAndShowForm(); + + vm.poll.makeRequest(); + + return new Promise(resolve => { + 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(); + }); + }); + }); + + describe('requestTemplatesAndShowForm', () => { + let formSpy; + + beforeEach(() => { + formSpy = jest.spyOn(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(() => { + expect(formSpy).toHaveBeenCalledWith(mockData); + }); + }); + + it('shows the form if template names request failed', () => { + mock + .onGet('/issuable-templates-path') + .reply(() => Promise.reject(new Error('something went wrong'))); + + return vm.requestTemplatesAndShowForm().then(() => { + expect(document.querySelector('.flash-container .flash-text').textContent).toContain( + 'Error updating issue', + ); + + expect(formSpy).toHaveBeenCalledWith(); + }); + }); + }); + + describe('show inline edit button', () => { + it('should not render by default', () => { + expect(vm.$el.querySelector('.title-container .note-action-button')).toBeDefined(); + }); + + it('should render if showInlineEditButton', () => { + vm.showInlineEditButton = true; + + expect(vm.$el.querySelector('.title-container .note-action-button')).toBeDefined(); + }); + }); + + 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); + + return 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'; + + return vm.updateStoreState().then(() => { + expect(document.querySelector('.flash-container .flash-text').innerText.trim()).toBe( + `Error updating ${vm.issuableType}`, + ); + }); + }); + }); + + describe('issueChanged', () => { + beforeEach(() => { + vm.store.formState.title = ''; + vm.store.formState.description = ''; + vm.initialDescriptionText = ''; + vm.initialTitleText = ''; + }); + + it('returns true when title is changed', () => { + vm.store.formState.title = 'RandomText'; + + expect(vm.issueChanged).toBe(true); + }); + + it('returns false when title is empty null', () => { + vm.store.formState.title = null; + + expect(vm.issueChanged).toBe(false); + }); + + it('returns false when `initialTitleText` is null and `formState.title` is empty string', () => { + vm.store.formState.title = ''; + vm.initialTitleText = null; + + expect(vm.issueChanged).toBe(false); + }); + + it('returns true when description is changed', () => { + vm.store.formState.description = 'RandomText'; + + expect(vm.issueChanged).toBe(true); + }); + + it('returns false when description is empty null', () => { + vm.store.formState.title = null; + + expect(vm.issueChanged).toBe(false); + }); + + it('returns false when `initialDescriptionText` is null and `formState.description` is empty string', () => { + vm.store.formState.description = ''; + vm.initialDescriptionText = null; + + expect(vm.issueChanged).toBe(false); + }); + }); +}); diff --git a/spec/frontend/issue_show/components/description_spec.js b/spec/frontend/issue_show/components/description_spec.js new file mode 100644 index 00000000000..0053475dd13 --- /dev/null +++ b/spec/frontend/issue_show/components/description_spec.js @@ -0,0 +1,188 @@ +import $ from 'jquery'; +import Vue from 'vue'; +import '~/behaviors/markdown/render_gfm'; +import mountComponent from 'helpers/vue_mount_component_helper'; +import { TEST_HOST } from 'helpers/test_constants'; +import Description from '~/issue_show/components/description.vue'; +import TaskList from '~/task_list'; + +jest.mock('~/task_list'); + +describe('Description component', () => { + let vm; + let DescriptionComponent; + const props = { + canUpdate: true, + descriptionHtml: 'test', + descriptionText: 'test', + updatedAt: new Date().toString(), + taskStatus: '', + updateUrl: TEST_HOST, + }; + + beforeEach(() => { + DescriptionComponent = Vue.extend(Description); + + if (!document.querySelector('.issuable-meta')) { + const metaData = document.createElement('div'); + metaData.classList.add('issuable-meta'); + metaData.innerHTML = + '<div class="flash-container"></div><span id="task_status"></span><span id="task_status_short"></span>'; + + document.body.appendChild(metaData); + } + + vm = mountComponent(DescriptionComponent, props); + }); + + afterEach(() => { + vm.$destroy(); + }); + + afterAll(() => { + $('.issuable-meta .flash-container').remove(); + }); + + it('animates description changes', () => { + vm.descriptionHtml = 'changed'; + + return vm + .$nextTick() + .then(() => { + expect( + vm.$el.querySelector('.md').classList.contains('issue-realtime-pre-pulse'), + ).toBeTruthy(); + jest.runAllTimers(); + return vm.$nextTick(); + }) + .then(() => { + expect( + vm.$el.querySelector('.md').classList.contains('issue-realtime-trigger-pulse'), + ).toBeTruthy(); + }); + }); + + it('opens reCAPTCHA dialog if update rejected as spam', () => { + let modal; + const recaptchaChild = vm.$children.find( + // eslint-disable-next-line no-underscore-dangle + child => child.$options._componentTag === 'recaptcha-modal', + ); + + recaptchaChild.scriptSrc = '//scriptsrc'; + + vm.taskListUpdateSuccess({ + recaptcha_html: '<div class="g-recaptcha">recaptcha_html</div>', + }); + + return vm + .$nextTick() + .then(() => { + modal = vm.$el.querySelector('.js-recaptcha-modal'); + + expect(modal.style.display).not.toEqual('none'); + expect(modal.querySelector('.g-recaptcha').textContent).toEqual('recaptcha_html'); + expect(document.body.querySelector('.js-recaptcha-script').src).toMatch('//scriptsrc'); + }) + .then(() => modal.querySelector('.close').click()) + .then(() => vm.$nextTick()) + .then(() => { + expect(modal.style.display).toEqual('none'); + expect(document.body.querySelector('.js-recaptcha-script')).toBeNull(); + }); + }); + + it('applies syntax highlighting and math when description changed', () => { + const vmSpy = jest.spyOn(vm, 'renderGFM'); + const prototypeSpy = jest.spyOn($.prototype, 'renderGFM'); + vm.descriptionHtml = 'changed'; + + return vm.$nextTick().then(() => { + expect(vm.$refs['gfm-content']).toBeDefined(); + expect(vmSpy).toHaveBeenCalled(); + expect(prototypeSpy).toHaveBeenCalled(); + expect($.prototype.renderGFM).toHaveBeenCalled(); + }); + }); + + it('sets data-update-url', () => { + expect(vm.$el.querySelector('textarea').dataset.updateUrl).toEqual(TEST_HOST); + }); + + describe('TaskList', () => { + beforeEach(() => { + vm.$destroy(); + TaskList.mockClear(); + vm = mountComponent(DescriptionComponent, { ...props, issuableType: 'issuableType' }); + }); + + it('re-inits the TaskList when description changed', () => { + vm.descriptionHtml = 'changed'; + + expect(TaskList).toHaveBeenCalled(); + }); + + it('does not re-init the TaskList when canUpdate is false', () => { + vm.canUpdate = false; + vm.descriptionHtml = 'changed'; + + expect(TaskList).toHaveBeenCalledTimes(1); + }); + + it('calls with issuableType dataType', () => { + vm.descriptionHtml = 'changed'; + + expect(TaskList).toHaveBeenCalledWith({ + dataType: 'issuableType', + fieldName: 'description', + selector: '.detail-page-description', + onSuccess: expect.any(Function), + onError: expect.any(Function), + lockVersion: 0, + }); + }); + }); + + describe('taskStatus', () => { + it('adds full taskStatus', () => { + vm.taskStatus = '1 of 1'; + + return vm.$nextTick().then(() => { + expect(document.querySelector('.issuable-meta #task_status').textContent.trim()).toBe( + '1 of 1', + ); + }); + }); + + it('adds short taskStatus', () => { + vm.taskStatus = '1 of 1'; + + return vm.$nextTick().then(() => { + expect(document.querySelector('.issuable-meta #task_status_short').textContent.trim()).toBe( + '1/1 task', + ); + }); + }); + + it('clears task status text when no tasks are present', () => { + vm.taskStatus = '0 of 0'; + + return vm.$nextTick().then(() => { + expect(document.querySelector('.issuable-meta #task_status').textContent.trim()).toBe(''); + }); + }); + }); + + describe('taskListUpdateError', () => { + it('should create flash notification and emit an event to parent', () => { + const msg = + 'Someone edited this issue at the same time you did. The description has been updated and you will need to make your changes again.'; + const spy = jest.spyOn(vm, '$emit'); + + vm.taskListUpdateError(); + + expect(document.querySelector('.flash-container .flash-text').innerText.trim()).toBe(msg); + expect(spy).toHaveBeenCalledWith('taskListUpdateFailed'); + }); + }); +}); diff --git a/spec/frontend/issue_show/components/edited_spec.js b/spec/frontend/issue_show/components/edited_spec.js new file mode 100644 index 00000000000..a1683f060c0 --- /dev/null +++ b/spec/frontend/issue_show/components/edited_spec.js @@ -0,0 +1,49 @@ +import Vue from 'vue'; +import edited from '~/issue_show/components/edited.vue'; + +function formatText(text) { + return text.trim().replace(/\s\s+/g, ' '); +} + +describe('edited', () => { + const EditedComponent = Vue.extend(edited); + + it('should render an edited at+by string', () => { + const editedComponent = new EditedComponent({ + propsData: { + updatedAt: '2017-05-15T12:31:04.428Z', + updatedByName: 'Some User', + updatedByPath: '/some_user', + }, + }).$mount(); + + expect(formatText(editedComponent.$el.innerText)).toMatch(/Edited[\s\S]+?by Some User/); + expect(editedComponent.$el.querySelector('.author-link').href).toMatch(/\/some_user$/); + expect(editedComponent.$el.querySelector('time')).toBeTruthy(); + }); + + it('if no updatedAt is provided, no time element will be rendered', () => { + const editedComponent = new EditedComponent({ + propsData: { + updatedByName: 'Some User', + updatedByPath: '/some_user', + }, + }).$mount(); + + expect(formatText(editedComponent.$el.innerText)).toMatch(/Edited by Some User/); + expect(editedComponent.$el.querySelector('.author-link').href).toMatch(/\/some_user$/); + expect(editedComponent.$el.querySelector('time')).toBeFalsy(); + }); + + it('if no updatedByName and updatedByPath is provided, no user element will be rendered', () => { + const editedComponent = new EditedComponent({ + propsData: { + updatedAt: '2017-05-15T12:31:04.428Z', + }, + }).$mount(); + + expect(formatText(editedComponent.$el.innerText)).not.toMatch(/by Some User/); + expect(editedComponent.$el.querySelector('.author-link')).toBeFalsy(); + expect(editedComponent.$el.querySelector('time')).toBeTruthy(); + }); +}); diff --git a/spec/frontend/issue_show/components/fields/description_template_spec.js b/spec/frontend/issue_show/components/fields/description_template_spec.js new file mode 100644 index 00000000000..9ebab31f1ad --- /dev/null +++ b/spec/frontend/issue_show/components/fields/description_template_spec.js @@ -0,0 +1,41 @@ +import Vue from 'vue'; +import descriptionTemplate from '~/issue_show/components/fields/description_template.vue'; + +describe('Issue description template component', () => { + let vm; + let formState; + + beforeEach(() => { + const Component = Vue.extend(descriptionTemplate); + formState = { + description: 'test', + }; + + vm = new Component({ + propsData: { + formState, + issuableTemplates: [{ name: 'test' }], + projectPath: '/', + projectNamespace: '/', + }, + }).$mount(); + }); + + it('renders templates as JSON array in data attribute', () => { + expect(vm.$el.querySelector('.js-issuable-selector').getAttribute('data-data')).toBe( + '[{"name":"test"}]', + ); + }); + + it('updates formState when changing template', () => { + vm.issuableTemplate.editor.setValue('test new template'); + + expect(formState.description).toBe('test new template'); + }); + + it('returns formState description with editor getValue', () => { + formState.description = 'testing new template'; + + expect(vm.issuableTemplate.editor.getValue()).toBe('testing new template'); + }); +}); diff --git a/spec/frontend/issue_show/components/form_spec.js b/spec/frontend/issue_show/components/form_spec.js new file mode 100644 index 00000000000..b06a3a89d3b --- /dev/null +++ b/spec/frontend/issue_show/components/form_spec.js @@ -0,0 +1,99 @@ +import Vue from 'vue'; +import mountComponent from 'helpers/vue_mount_component_helper'; +import formComponent from '~/issue_show/components/form.vue'; +import Autosave from '~/autosave'; +import eventHub from '~/issue_show/event_hub'; + +jest.mock('~/autosave'); + +describe('Inline edit form component', () => { + let vm; + const defaultProps = { + canDestroy: true, + formState: { + title: 'b', + description: 'a', + lockedWarningVisible: false, + }, + issuableType: 'issue', + markdownPreviewPath: '/', + markdownDocsPath: '/', + projectPath: '/', + projectNamespace: '/', + }; + + afterEach(() => { + vm.$destroy(); + }); + + const createComponent = props => { + const Component = Vue.extend(formComponent); + + vm = mountComponent(Component, { + ...defaultProps, + ...props, + }); + }; + + it('does not render template selector if no templates exist', () => { + createComponent(); + + expect(vm.$el.querySelector('.js-issuable-selector-wrap')).toBeNull(); + }); + + it('renders template selector when templates exists', () => { + createComponent({ issuableTemplates: ['test'] }); + + expect(vm.$el.querySelector('.js-issuable-selector-wrap')).not.toBeNull(); + }); + + it('hides locked warning by default', () => { + createComponent(); + + expect(vm.$el.querySelector('.alert')).toBeNull(); + }); + + it('shows locked warning if formState is different', () => { + createComponent({ formState: { ...defaultProps.formState, lockedWarningVisible: true } }); + + expect(vm.$el.querySelector('.alert')).not.toBeNull(); + }); + + it('hides locked warning when currently saving', () => { + createComponent({ + formState: { ...defaultProps.formState, updateLoading: true, lockedWarningVisible: true }, + }); + + expect(vm.$el.querySelector('.alert')).toBeNull(); + }); + + describe('autosave', () => { + let spy; + + beforeEach(() => { + spy = jest.spyOn(Autosave.prototype, 'reset'); + }); + + it('initialized Autosave on mount', () => { + createComponent(); + + expect(Autosave).toHaveBeenCalledTimes(2); + }); + + it('calls reset on autosave when eventHub emits appropriate events', () => { + createComponent(); + + eventHub.$emit('close.form'); + + expect(spy).toHaveBeenCalledTimes(2); + + eventHub.$emit('delete.issuable'); + + expect(spy).toHaveBeenCalledTimes(4); + + eventHub.$emit('update.issuable'); + + expect(spy).toHaveBeenCalledTimes(6); + }); + }); +}); diff --git a/spec/frontend/issue_show/components/title_spec.js b/spec/frontend/issue_show/components/title_spec.js new file mode 100644 index 00000000000..c274048fdd5 --- /dev/null +++ b/spec/frontend/issue_show/components/title_spec.js @@ -0,0 +1,95 @@ +import Vue from 'vue'; +import Store from '~/issue_show/stores'; +import titleComponent from '~/issue_show/components/title.vue'; +import eventHub from '~/issue_show/event_hub'; + +describe('Title component', () => { + let vm; + beforeEach(() => { + setFixtures(`<title />`); + + const Component = Vue.extend(titleComponent); + const store = new Store({ + titleHtml: '', + descriptionHtml: '', + issuableRef: '', + }); + vm = new Component({ + propsData: { + issuableRef: '#1', + titleHtml: 'Testing <img />', + titleText: 'Testing', + showForm: false, + formState: store.formState, + }, + }).$mount(); + }); + + it('renders title HTML', () => { + expect(vm.$el.querySelector('.title').innerHTML.trim()).toBe('Testing <img>'); + }); + + it('updates page title when changing titleHtml', () => { + const spy = jest.spyOn(vm, 'setPageTitle'); + vm.titleHtml = 'test'; + + return vm.$nextTick().then(() => { + expect(spy).toHaveBeenCalled(); + }); + }); + + it('animates title changes', () => { + vm.titleHtml = 'test'; + return vm + .$nextTick() + .then(() => { + expect(vm.$el.querySelector('.title').classList).toContain('issue-realtime-pre-pulse'); + jest.runAllTimers(); + return vm.$nextTick(); + }) + .then(() => { + expect(vm.$el.querySelector('.title').classList).toContain('issue-realtime-trigger-pulse'); + }); + }); + + it('updates page title after changing title', () => { + vm.titleHtml = 'changed'; + vm.titleText = 'changed'; + + return vm.$nextTick().then(() => { + expect(document.querySelector('title').textContent.trim()).toContain('changed'); + }); + }); + + describe('inline edit button', () => { + it('should not show by default', () => { + expect(vm.$el.querySelector('.btn-edit')).toBeNull(); + }); + + it('should not show if canUpdate is false', () => { + vm.showInlineEditButton = true; + vm.canUpdate = false; + + expect(vm.$el.querySelector('.btn-edit')).toBeNull(); + }); + + it('should show if showInlineEditButton and canUpdate', () => { + vm.showInlineEditButton = true; + vm.canUpdate = true; + + expect(vm.$el.querySelector('.btn-edit')).toBeDefined(); + }); + + it('should trigger open.form event when clicked', () => { + jest.spyOn(eventHub, '$emit').mockImplementation(() => {}); + vm.showInlineEditButton = true; + vm.canUpdate = true; + + Vue.nextTick(() => { + vm.$el.querySelector('.btn-edit').click(); + + expect(eventHub.$emit).toHaveBeenCalledWith('open.form'); + }); + }); + }); +}); |