diff options
Diffstat (limited to 'spec/frontend/issues/show')
5 files changed, 151 insertions, 43 deletions
diff --git a/spec/frontend/issues/show/components/edited_spec.js b/spec/frontend/issues/show/components/edited_spec.js index 8a240c38b5f..aa6e0a9dceb 100644 --- a/spec/frontend/issues/show/components/edited_spec.js +++ b/spec/frontend/issues/show/components/edited_spec.js @@ -1,7 +1,10 @@ -import { shallowMount } from '@vue/test-utils'; +import { mount } from '@vue/test-utils'; +import { getTimeago } from '~/lib/utils/datetime_utility'; import Edited from '~/issues/show/components/edited.vue'; import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; +const timeago = getTimeago(); + describe('Edited component', () => { let wrapper; @@ -9,7 +12,8 @@ describe('Edited component', () => { const findTimeAgoTooltip = () => wrapper.findComponent(TimeAgoTooltip); const formatText = (text) => text.trim().replace(/\s\s+/g, ' '); - const mountComponent = (propsData) => shallowMount(Edited, { propsData }); + const mountComponent = (propsData) => mount(Edited, { propsData }); + const updatedAt = '2017-05-15T12:31:04.428Z'; afterEach(() => { wrapper.destroy(); @@ -17,12 +21,12 @@ describe('Edited component', () => { it('renders an edited at+by string', () => { wrapper = mountComponent({ - updatedAt: '2017-05-15T12:31:04.428Z', + updatedAt, updatedByName: 'Some User', updatedByPath: '/some_user', }); - expect(formatText(wrapper.text())).toBe('Edited by Some User'); + expect(formatText(wrapper.text())).toBe(`Edited ${timeago.format(updatedAt)} by Some User`); expect(findAuthorLink().attributes('href')).toBe('/some_user'); expect(findTimeAgoTooltip().exists()).toBe(true); }); @@ -40,10 +44,10 @@ describe('Edited component', () => { it('if no updatedByName and updatedByPath is provided, no user element will be rendered', () => { wrapper = mountComponent({ - updatedAt: '2017-05-15T12:31:04.428Z', + updatedAt, }); - expect(formatText(wrapper.text())).toBe('Edited'); + expect(formatText(wrapper.text())).toBe(`Edited ${timeago.format(updatedAt)}`); expect(findAuthorLink().exists()).toBe(false); expect(findTimeAgoTooltip().exists()).toBe(true); }); diff --git a/spec/frontend/issues/show/components/fields/description_spec.js b/spec/frontend/issues/show/components/fields/description_spec.js index 61433607a2b..cd4d422583b 100644 --- a/spec/frontend/issues/show/components/fields/description_spec.js +++ b/spec/frontend/issues/show/components/fields/description_spec.js @@ -2,13 +2,15 @@ import { shallowMount } from '@vue/test-utils'; import DescriptionField from '~/issues/show/components/fields/description.vue'; import eventHub from '~/issues/show/event_hub'; import MarkdownField from '~/vue_shared/components/markdown/field.vue'; +import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue'; describe('Description field component', () => { let wrapper; const findTextarea = () => wrapper.findComponent({ ref: 'textarea' }); + const findMarkdownEditor = () => wrapper.findComponent(MarkdownEditor); - const mountComponent = (description = 'test') => + const mountComponent = ({ description = 'test', contentEditorOnIssues = false } = {}) => shallowMount(DescriptionField, { attachTo: document.body, propsData: { @@ -17,6 +19,11 @@ describe('Description field component', () => { quickActionsDocsPath: '/', value: description, }, + provide: { + glFeatures: { + contentEditorOnIssues, + }, + }, stubs: { MarkdownField, }, @@ -40,7 +47,7 @@ describe('Description field component', () => { it('renders markdown field with a markdown description', () => { const markdown = '**test**'; - wrapper = mountComponent(markdown); + wrapper = mountComponent({ description: markdown }); expect(findTextarea().element.value).toBe(markdown); }); @@ -66,4 +73,52 @@ describe('Description field component', () => { expect(eventHub.$emit).toHaveBeenCalledWith('update.issuable'); }); + + describe('when contentEditorOnIssues feature flag is on', () => { + beforeEach(() => { + wrapper = mountComponent({ contentEditorOnIssues: true }); + }); + + it('uses the MarkdownEditor component to edit markdown', () => { + expect(findMarkdownEditor().props()).toEqual( + expect.objectContaining({ + value: 'test', + renderMarkdownPath: '/', + markdownDocsPath: '/', + quickActionsDocsPath: expect.any(String), + initOnAutofocus: true, + supportsQuickActions: true, + enableAutocomplete: true, + }), + ); + }); + + it('triggers update with meta+enter', () => { + findMarkdownEditor().vm.$emit('keydown', { + type: 'keydown', + keyCode: 13, + metaKey: true, + }); + + expect(eventHub.$emit).toHaveBeenCalledWith('update.issuable'); + }); + + it('triggers update with ctrl+enter', () => { + findMarkdownEditor().vm.$emit('keydown', { + type: 'keydown', + keyCode: 13, + ctrlKey: true, + }); + + expect(eventHub.$emit).toHaveBeenCalledWith('update.issuable'); + }); + + it('emits input event when MarkdownEditor emits input event', () => { + const markdown = 'markdown'; + + findMarkdownEditor().vm.$emit('input', markdown); + + expect(wrapper.emitted('input')).toEqual([[markdown]]); + }); + }); }); diff --git a/spec/frontend/issues/show/components/form_spec.js b/spec/frontend/issues/show/components/form_spec.js index 5c0fe991b22..aedb974cbd0 100644 --- a/spec/frontend/issues/show/components/form_spec.js +++ b/spec/frontend/issues/show/components/form_spec.js @@ -1,14 +1,16 @@ import { GlAlert } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import { nextTick } from 'vue'; -import Autosave from '~/autosave'; +import { getDraft, updateDraft, clearDraft, getLockVersion } from '~/lib/utils/autosave'; import DescriptionTemplate from '~/issues/show/components/fields/description_template.vue'; +import IssuableTitleField from '~/issues/show/components/fields/title.vue'; +import DescriptionField from '~/issues/show/components/fields/description.vue'; import IssueTypeField from '~/issues/show/components/fields/type.vue'; import formComponent from '~/issues/show/components/form.vue'; import LockedWarning from '~/issues/show/components/locked_warning.vue'; import eventHub from '~/issues/show/event_hub'; -jest.mock('~/autosave'); +jest.mock('~/lib/utils/autosave'); describe('Inline edit form component', () => { let wrapper; @@ -38,9 +40,14 @@ describe('Inline edit form component', () => { ...defaultProps, ...props, }, + stubs: { + DescriptionField, + }, }); }; + const findTitleField = () => wrapper.findComponent(IssuableTitleField); + const findDescriptionField = () => wrapper.findComponent(DescriptionField); const findDescriptionTemplate = () => wrapper.findComponent(DescriptionTemplate); const findIssuableTypeField = () => wrapper.findComponent(IssueTypeField); const findLockedWarning = () => wrapper.findComponent(LockedWarning); @@ -108,16 +115,34 @@ describe('Inline edit form component', () => { }); describe('autosave', () => { - let spy; - beforeEach(() => { - spy = jest.spyOn(Autosave.prototype, 'reset'); + getDraft.mockImplementation((autosaveKey) => { + return autosaveKey[autosaveKey.length - 1]; + }); }); - it('initialized Autosave on mount', () => { + it('initializes title and description fields with saved drafts', () => { createComponent(); - expect(Autosave).toHaveBeenCalledTimes(2); + expect(findTitleField().props().value).toBe('title'); + expect(findDescriptionField().props().value).toBe('description'); + }); + + it('updates local storage drafts when title and description change', () => { + const updatedTitle = 'updated title'; + const updatedDescription = 'updated description'; + + createComponent(); + + findTitleField().vm.$emit('input', updatedTitle); + findDescriptionField().vm.$emit('input', updatedDescription); + + expect(updateDraft).toHaveBeenCalledWith(expect.any(Array), updatedTitle); + expect(updateDraft).toHaveBeenCalledWith( + expect.any(Array), + updatedDescription, + defaultProps.formState.lock_version, + ); }); it('calls reset on autosave when eventHub emits appropriate events', () => { @@ -125,33 +150,60 @@ describe('Inline edit form component', () => { eventHub.$emit('close.form'); - expect(spy).toHaveBeenCalledTimes(2); + expect(clearDraft).toHaveBeenCalledTimes(2); eventHub.$emit('delete.issuable'); - expect(spy).toHaveBeenCalledTimes(4); + expect(clearDraft).toHaveBeenCalledTimes(4); eventHub.$emit('update.issuable'); - expect(spy).toHaveBeenCalledTimes(6); + expect(clearDraft).toHaveBeenCalledTimes(6); }); describe('outdated description', () => { + const clientSideMockVersion = 'lock version from local storage'; + const serverSideMockVersion = 'lock version from server'; + + const mockGetLockVersion = () => getLockVersion.mockResolvedValue(clientSideMockVersion); + it('does not show warning if lock version from server is the same as the local lock version', () => { createComponent(); expect(findAlert().exists()).toBe(false); }); it('shows warning if lock version from server differs than the local lock version', async () => { - Autosave.prototype.getSavedLockVersion.mockResolvedValue('lock version from local storage'); + mockGetLockVersion(); createComponent({ - formState: { ...defaultProps.formState, lock_version: 'lock version from server' }, + formState: { ...defaultProps.formState, lock_version: serverSideMockVersion }, }); await nextTick(); expect(findAlert().exists()).toBe(true); }); + + describe('when saved draft is discarded', () => { + beforeEach(async () => { + mockGetLockVersion(); + + createComponent({ + formState: { ...defaultProps.formState, lock_version: serverSideMockVersion }, + }); + + await nextTick(); + + findAlert().vm.$emit('secondaryAction'); + }); + + it('hides the warning alert', () => { + expect(findAlert().exists()).toBe(false); + }); + + it('clears the description draft', () => { + expect(clearDraft).toHaveBeenCalledWith(expect.any(Array)); + }); + }); }); }); }); diff --git a/spec/frontend/issues/show/components/incidents/incident_tabs_spec.js b/spec/frontend/issues/show/components/incidents/incident_tabs_spec.js index d92aeabba0f..458c1c3f858 100644 --- a/spec/frontend/issues/show/components/incidents/incident_tabs_spec.js +++ b/spec/frontend/issues/show/components/incidents/incident_tabs_spec.js @@ -5,7 +5,6 @@ import { trackIncidentDetailsViewsOptions } from '~/incidents/constants'; import DescriptionComponent from '~/issues/show/components/description.vue'; import HighlightBar from '~/issues/show/components/incidents/highlight_bar.vue'; import IncidentTabs from '~/issues/show/components/incidents/incident_tabs.vue'; -import TimelineTab from '~/issues/show/components/incidents/timeline_events_tab.vue'; import INVALID_URL from '~/lib/utils/invalid_url'; import Tracking from '~/tracking'; import AlertDetailsTable from '~/vue_shared/components/alert_details_table.vue'; @@ -38,7 +37,6 @@ describe('Incident Tabs component', () => { projectId: '', issuableId: '', uploadMetricsFeatureAvailable: true, - glFeatures: { incidentTimeline: true }, }, data() { return { alert: mockAlert, ...data }; @@ -67,7 +65,6 @@ describe('Incident Tabs component', () => { const findAlertDetailsComponent = () => wrapper.findComponent(AlertDetailsTable); const findDescriptionComponent = () => wrapper.findComponent(DescriptionComponent); const findHighlightBarComponent = () => wrapper.findComponent(HighlightBar); - const findTimelineTab = () => wrapper.findComponent(TimelineTab); describe('empty state', () => { beforeEach(() => { @@ -128,20 +125,4 @@ describe('Incident Tabs component', () => { expect(Tracking.event).toHaveBeenCalledWith(category, action); }); }); - - describe('incident timeline tab', () => { - beforeEach(() => { - mountComponent(); - }); - - it('renders the timeline tab when feature flag is enabled', () => { - expect(findTimelineTab().exists()).toBe(true); - }); - - it('does not render timeline tab when feature flag is disabled', () => { - mountComponent({}, { provide: { glFeatures: { incidentTimeline: false } } }); - - expect(findTimelineTab().exists()).toBe(false); - }); - }); }); diff --git a/spec/frontend/issues/show/components/incidents/timeline_events_form_spec.js b/spec/frontend/issues/show/components/incidents/timeline_events_form_spec.js index 7f086a276f7..2e7449974e5 100644 --- a/spec/frontend/issues/show/components/incidents/timeline_events_form_spec.js +++ b/spec/frontend/issues/show/components/incidents/timeline_events_form_spec.js @@ -22,12 +22,15 @@ describe('Timeline events form', () => { useFakeDate(fakeDate); let wrapper; - const mountComponent = ({ mountMethod = shallowMountExtended }) => { + const mountComponent = ({ mountMethod = shallowMountExtended } = {}) => { wrapper = mountMethod(TimelineEventsForm, { propsData: { showSaveAndAdd: true, isEventProcessed: false, }, + stubs: { + GlButton: true, + }, }); }; @@ -48,17 +51,18 @@ describe('Timeline events form', () => { findHourInput().setValue(5); findMinuteInput().setValue(45); }; + const findTextarea = () => wrapper.findByTestId('input-note'); const submitForm = async () => { - findSubmitButton().trigger('click'); + findSubmitButton().vm.$emit('click'); await waitForPromises(); }; const submitFormAndAddAnother = async () => { - findSubmitAndAddButton().trigger('click'); + findSubmitAndAddButton().vm.$emit('click'); await waitForPromises(); }; const cancelForm = async () => { - findCancelButton().trigger('click'); + findCancelButton().vm.$emit('click'); await waitForPromises(); }; @@ -118,5 +122,17 @@ describe('Timeline events form', () => { expect(findHourInput().element.value).toBe('0'); expect(findMinuteInput().element.value).toBe('0'); }); + + it('should disable the save buttons when event content does not exist', async () => { + expect(findSubmitButton().props('disabled')).toBe(true); + expect(findSubmitAndAddButton().props('disabled')).toBe(true); + }); + + it('should enable the save buttons when event content exists', async () => { + await findTextarea().setValue('hello'); + + expect(findSubmitButton().props('disabled')).toBe(false); + expect(findSubmitAndAddButton().props('disabled')).toBe(false); + }); }); }); |