diff options
Diffstat (limited to 'spec/frontend/issues/show')
7 files changed, 206 insertions, 191 deletions
diff --git a/spec/frontend/issues/show/components/app_spec.js b/spec/frontend/issues/show/components/app_spec.js index ac2717a5028..5ab64d8e9ca 100644 --- a/spec/frontend/issues/show/components/app_spec.js +++ b/spec/frontend/issues/show/components/app_spec.js @@ -2,11 +2,14 @@ import { GlIntersectionObserver } from '@gitlab/ui'; import MockAdapter from 'axios-mock-adapter'; import { nextTick } from 'vue'; import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; -import { mountExtended } from 'helpers/vue_test_utils_helper'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import '~/behaviors/markdown/render_gfm'; import { IssuableStatus, IssuableStatusText } 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 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'; import { POLLING_DELAY } from '~/issues/show/constants'; @@ -21,10 +24,6 @@ import { zoomMeetingUrl, } from '../mock_data/mock_data'; -function formatText(text) { - return text.trim().replace(/\s\s+/g, ' '); -} - jest.mock('~/lib/utils/url_utility'); jest.mock('~/issues/show/event_hub'); @@ -39,10 +38,15 @@ describe('Issuable output', () => { const findLockedBadge = () => wrapper.findByTestId('locked'); const findConfidentialBadge = () => wrapper.findByTestId('confidential'); const findHiddenBadge = () => wrapper.findByTestId('hidden'); - const findAlert = () => wrapper.find('.alert'); + + 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 mountComponent = (props = {}, options = {}, data = {}) => { - wrapper = mountExtended(IssuableApp, { + wrapper = shallowMountExtended(IssuableApp, { directives: { GlTooltip: createMockDirective(), }, @@ -104,23 +108,15 @@ describe('Issuable output', () => { }); it('should render a title/description/edited and update title/description/edited on update', () => { - let editedText; return axios .waitForAll() .then(() => { - editedText = wrapper.find('.edited-text'); - }) - .then(() => { - expect(document.querySelector('title').innerText).toContain('this is a title (#1)'); - 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(findTitle().props('titleText')).toContain('this is a title'); + expect(findDescription().props('descriptionText')).toContain('this is a description'); - 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(findEdited().exists()).toBe(true); + expect(findEdited().props('updatedByPath')).toMatch(/\/some_user$/); + expect(findEdited().props('updatedAt')).toBeTruthy(); expect(wrapper.vm.state.lock_version).toBe(initialRequest.lock_version); }) .then(() => { @@ -128,20 +124,13 @@ describe('Issuable output', () => { return axios.waitForAll(); }) .then(() => { - expect(document.querySelector('title').innerText).toContain('2 (#1)'); - 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(findTitle().props('titleText')).toContain('2'); + expect(findDescription().props('descriptionText')).toContain('42'); - expect(editedText.find('.author-link').attributes('href')).toMatch(/\/other_user$/); - expect(editedText.find('time').text()).toBeTruthy(); - // As the lock_version value does not differ from the server, - // we should not see an alert - expect(findAlert().exists()).toBe(false); + expect(findEdited().exists()).toBe(true); + expect(findEdited().props('updatedByName')).toBe('Other User'); + expect(findEdited().props('updatedByPath')).toMatch(/\/other_user$/); + expect(findEdited().props('updatedAt')).toBeTruthy(); }); }); @@ -149,7 +138,7 @@ describe('Issuable output', () => { wrapper.vm.showForm = true; await nextTick(); - expect(wrapper.find('.markdown-selector').exists()).toBe(true); + expect(findForm().exists()).toBe(true); }); it('does not show actions if permissions are incorrect', async () => { @@ -157,7 +146,7 @@ describe('Issuable output', () => { wrapper.setProps({ canUpdate: false }); await nextTick(); - expect(wrapper.find('.markdown-selector').exists()).toBe(false); + expect(findForm().exists()).toBe(false); }); it('does not update formState if form is already open', async () => { @@ -177,8 +166,7 @@ describe('Issuable output', () => { ${'zoomMeetingUrl'} | ${zoomMeetingUrl} ${'publishedIncidentUrl'} | ${publishedIncidentUrl} `('sets the $prop correctly on underlying pinned links', ({ prop, value }) => { - expect(wrapper.vm[prop]).toBe(value); - expect(wrapper.find(`[data-testid="${prop}"]`).attributes('href')).toBe(value); + expect(findPinnedLinks().props(prop)).toBe(value); }); }); @@ -327,7 +315,6 @@ describe('Issuable output', () => { expect(wrapper.vm.formState.lockedWarningVisible).toBe(true); expect(wrapper.vm.formState.lock_version).toBe(1); - expect(findAlert().exists()).toBe(true); }); }); @@ -374,15 +361,22 @@ describe('Issuable output', () => { }); describe('show inline edit button', () => { - it('should not render by default', () => { - expect(wrapper.find('.btn-edit').exists()).toBe(true); + it('should render by default', () => { + expect(findTitle().props('showInlineEditButton')).toBe(true); }); it('should render if showInlineEditButton', async () => { wrapper.setProps({ showInlineEditButton: true }); await nextTick(); - expect(wrapper.find('.btn-edit').exists()).toBe(true); + expect(findTitle().props('showInlineEditButton')).toBe(true); + }); + + it('should not render if showInlineEditButton is false', async () => { + wrapper.setProps({ showInlineEditButton: false }); + + await nextTick(); + expect(findTitle().props('showInlineEditButton')).toBe(false); }); }); @@ -533,13 +527,11 @@ describe('Issuable output', () => { describe('Composable description component', () => { 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', () => { it('renders the description component', () => { - expect(findDescriptionComponent().exists()).toBe(true); + expect(findDescription().exists()).toBe(true); }); it('does not render incident tabs', () => { @@ -572,8 +564,8 @@ describe('Issuable output', () => { ); }); - it('renders the description component', () => { - expect(findDescriptionComponent().exists()).toBe(true); + it('does not the description component', () => { + expect(findDescription().exists()).toBe(false); }); it('renders incident tabs', () => { diff --git a/spec/frontend/issues/show/components/description_spec.js b/spec/frontend/issues/show/components/description_spec.js index 08f8996de6f..0b3daadae1d 100644 --- a/spec/frontend/issues/show/components/description_spec.js +++ b/spec/frontend/issues/show/components/description_spec.js @@ -1,26 +1,35 @@ import $ from 'jquery'; import { nextTick } from 'vue'; import '~/behaviors/markdown/render_gfm'; -import { GlPopover, GlModal } from '@gitlab/ui'; +import { GlTooltip, GlModal } from '@gitlab/ui'; +import setWindowLocation from 'helpers/set_window_location_helper'; import { stubComponent } from 'helpers/stub_component'; import { TEST_HOST } from 'helpers/test_constants'; import { mockTracking } from 'helpers/tracking_helper'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; -import createFlash from '~/flash'; import Description from '~/issues/show/components/description.vue'; +import { updateHistory } from '~/lib/utils/url_utility'; import TaskList from '~/task_list'; import WorkItemDetailModal from '~/work_items/components/work_item_detail_modal.vue'; import CreateWorkItem from '~/work_items/pages/create_work_item.vue'; import { descriptionProps as initialProps, descriptionHtmlWithCheckboxes, + descriptionHtmlWithTask, } from '../mock_data/mock_data'; jest.mock('~/flash'); +jest.mock('~/lib/utils/url_utility', () => ({ + ...jest.requireActual('~/lib/utils/url_utility'), + updateHistory: jest.fn(), +})); jest.mock('~/task_list'); const showModal = jest.fn(); const hideModal = jest.fn(); +const $toast = { + show: jest.fn(), +}; describe('Description component', () => { let wrapper; @@ -28,10 +37,9 @@ describe('Description component', () => { const findGfmContent = () => wrapper.find('[data-testid="gfm-content"]'); const findTextarea = () => wrapper.find('[data-testid="textarea"]'); const findTaskActionButtons = () => wrapper.findAll('.js-add-task'); - const findConvertToTaskButton = () => wrapper.find('[data-testid="convert-to-task"]'); - const findTaskSvg = () => wrapper.find('[data-testid="issue-open-m-icon"]'); + const findConvertToTaskButton = () => wrapper.find('.js-add-task'); - const findPopovers = () => wrapper.findAllComponents(GlPopover); + const findTooltips = () => wrapper.findAllComponents(GlTooltip); const findModal = () => wrapper.findComponent(GlModal); const findCreateWorkItem = () => wrapper.findComponent(CreateWorkItem); const findWorkItemDetailModal = () => wrapper.findComponent(WorkItemDetailModal); @@ -39,10 +47,14 @@ describe('Description component', () => { function createComponent({ props = {}, provide = {} } = {}) { wrapper = shallowMountExtended(Description, { propsData: { + issueId: 1, ...initialProps, ...props, }, provide, + mocks: { + $toast, + }, stubs: { GlModal: stubComponent(GlModal, { methods: { @@ -50,12 +62,13 @@ describe('Description component', () => { hide: hideModal, }, }), - GlPopover, }, }); } beforeEach(() => { + setWindowLocation(TEST_HOST); + if (!document.querySelector('.issuable-meta')) { const metaData = document.createElement('div'); metaData.classList.add('issuable-meta'); @@ -253,9 +266,9 @@ describe('Description component', () => { expect(findTaskActionButtons()).toHaveLength(3); }); - it('renders a list of popovers corresponding to checkboxes in description HTML', () => { - expect(findPopovers()).toHaveLength(3); - expect(findPopovers().at(0).props('target')).toBe( + it('renders a list of tooltips corresponding to checkboxes in description HTML', () => { + expect(findTooltips()).toHaveLength(3); + expect(findTooltips().at(0).props('target')).toBe( findTaskActionButtons().at(0).attributes('id'), ); }); @@ -264,92 +277,113 @@ describe('Description component', () => { expect(findModal().props('visible')).toBe(false); }); - it('opens a modal when a button on popover is clicked and displays correct title', async () => { - findConvertToTaskButton().vm.$emit('click'); - expect(showModal).toHaveBeenCalled(); - await nextTick(); + it('opens a modal when a button is clicked and displays correct title', async () => { + await findConvertToTaskButton().trigger('click'); expect(findCreateWorkItem().props('initialTitle').trim()).toBe('todo 1'); }); - it('closes the modal on `closeCreateTaskModal` event', () => { - findConvertToTaskButton().vm.$emit('click'); + it('closes the modal on `closeCreateTaskModal` event', async () => { + await findConvertToTaskButton().trigger('click'); findCreateWorkItem().vm.$emit('closeModal'); expect(hideModal).toHaveBeenCalled(); }); - it('updates description HTML on `onCreate` event', async () => { - const newTitle = 'New title'; - findConvertToTaskButton().vm.$emit('click'); - findCreateWorkItem().vm.$emit('onCreate', { title: newTitle }); + it('emits `updateDescription` on `onCreate` event', () => { + const newDescription = `<p>New description</p>`; + findCreateWorkItem().vm.$emit('onCreate', newDescription); expect(hideModal).toHaveBeenCalled(); - await nextTick(); + expect(wrapper.emitted('updateDescription')).toEqual([[newDescription]]); + }); + + it('shows toast after delete success', async () => { + findWorkItemDetailModal().vm.$emit('workItemDeleted'); - expect(findTaskSvg().exists()).toBe(true); - expect(wrapper.text()).toContain(newTitle); + expect($toast.show).toHaveBeenCalledWith('Work item deleted'); }); }); describe('work items detail', () => { - const id = '1'; - const title = 'my first task'; - const type = 'task'; + const findTaskLink = () => wrapper.find('a.gfm-issue'); - const createThenClickOnTask = () => { - findConvertToTaskButton().vm.$emit('click'); - findCreateWorkItem().vm.$emit('onCreate', { id, title, type }); - return wrapper.findByRole('button', { name: title }).trigger('click'); - }; - - beforeEach(() => { - createComponent({ - props: { - descriptionHtml: descriptionHtmlWithCheckboxes, - }, - provide: { - glFeatures: { workItems: true }, - }, + describe('when opening and closing', () => { + beforeEach(() => { + createComponent({ + props: { + descriptionHtml: descriptionHtmlWithTask, + }, + provide: { + glFeatures: { workItems: true }, + }, + }); + return nextTick(); }); - return nextTick(); - }); - it('opens when task button is clicked', async () => { - expect(findWorkItemDetailModal().props('visible')).toBe(false); + it('opens when task button is clicked', async () => { + expect(findWorkItemDetailModal().props('visible')).toBe(false); - await createThenClickOnTask(); + await findTaskLink().trigger('click'); - expect(findWorkItemDetailModal().props('visible')).toBe(true); - }); + expect(findWorkItemDetailModal().props('visible')).toBe(true); + expect(updateHistory).toHaveBeenCalledWith({ + url: `${TEST_HOST}/?work_item_id=2`, + replace: true, + }); + }); - it('closes from an open state', async () => { - await createThenClickOnTask(); + it('closes from an open state', async () => { + await findTaskLink().trigger('click'); - expect(findWorkItemDetailModal().props('visible')).toBe(true); + expect(findWorkItemDetailModal().props('visible')).toBe(true); - findWorkItemDetailModal().vm.$emit('close'); - await nextTick(); + findWorkItemDetailModal().vm.$emit('close'); + await nextTick(); - expect(findWorkItemDetailModal().props('visible')).toBe(false); - }); + expect(findWorkItemDetailModal().props('visible')).toBe(false); + expect(updateHistory).toHaveBeenLastCalledWith({ + url: `${TEST_HOST}/`, + replace: true, + }); + }); - it('shows error on error', async () => { - const message = 'I am error'; + it('tracks when opened', async () => { + const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn); - await createThenClickOnTask(); - findWorkItemDetailModal().vm.$emit('error', message); + await findTaskLink().trigger('click'); - expect(createFlash).toHaveBeenCalledWith({ message }); + expect(trackingSpy).toHaveBeenCalledWith( + 'workItems:show', + 'viewed_work_item_from_modal', + { + category: 'workItems:show', + label: 'work_item_view', + property: 'type_task', + }, + ); + }); }); - it('tracks when opened', async () => { - const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn); - - await createThenClickOnTask(); - - expect(trackingSpy).toHaveBeenCalledWith('workItems:show', 'viewed_work_item_from_modal', { - category: 'workItems:show', - label: 'work_item_view', - property: 'type_task', - }); + describe('when url query `work_item_id` exists', () => { + it.each` + behavior | workItemId | visible + ${'opens'} | ${'123'} | ${true} + ${'does not open'} | ${'123e'} | ${false} + ${'does not open'} | ${'12e3'} | ${false} + ${'does not open'} | ${'1e23'} | ${false} + ${'does not open'} | ${'x'} | ${false} + ${'does not open'} | ${'undefined'} | ${false} + `( + '$behavior when url contains `work_item_id=$workItemId`', + async ({ workItemId, visible }) => { + setWindowLocation(`?work_item_id=${workItemId}`); + + createComponent({ + props: { descriptionHtml: descriptionHtmlWithTask }, + provide: { glFeatures: { workItems: true } }, + }); + + expect(findWorkItemDetailModal().props('visible')).toBe(visible); + }, + ); }); }); }); diff --git a/spec/frontend/issues/show/components/fields/description_spec.js b/spec/frontend/issues/show/components/fields/description_spec.js index dd511c3945c..0dcd70ac19b 100644 --- a/spec/frontend/issues/show/components/fields/description_spec.js +++ b/spec/frontend/issues/show/components/fields/description_spec.js @@ -14,9 +14,8 @@ describe('Description field component', () => { propsData: { markdownPreviewPath: '/', markdownDocsPath: '/', - formState: { - description, - }, + quickActionsDocsPath: '/', + value: description, }, stubs: { MarkdownField, diff --git a/spec/frontend/issues/show/components/fields/description_template_spec.js b/spec/frontend/issues/show/components/fields/description_template_spec.js index abe2805e5b2..79a3bfa9840 100644 --- a/spec/frontend/issues/show/components/fields/description_template_spec.js +++ b/spec/frontend/issues/show/components/fields/description_template_spec.js @@ -1,74 +1,65 @@ -import Vue from 'vue'; +import { shallowMount } from '@vue/test-utils'; import descriptionTemplate from '~/issues/show/components/fields/description_template.vue'; describe('Issue description template component with templates as hash', () => { - let vm; - let formState; + let wrapper; + const defaultOptions = { + propsData: { + value: 'test', + issuableTemplates: { + test: [{ name: 'test', id: 'test', project_path: '/', namespace_path: '/' }], + }, + projectId: 1, + projectPath: '/', + namespacePath: '/', + projectNamespace: '/', + }, + }; - beforeEach(() => { - const Component = Vue.extend(descriptionTemplate); - formState = { - description: 'test', - }; + const findIssuableSelector = () => wrapper.find('.js-issuable-selector'); - vm = new Component({ - propsData: { - formState, - issuableTemplates: { - test: [{ name: 'test', id: 'test', project_path: '/', namespace_path: '/' }], - }, - projectId: 1, - projectPath: '/', - namespacePath: '/', - projectNamespace: '/', - }, - }).$mount(); + const createComponent = (options = defaultOptions) => { + wrapper = shallowMount(descriptionTemplate, options); + }; + + afterEach(() => { + wrapper.destroy(); }); it('renders templates as JSON hash in data attribute', () => { - expect(vm.$el.querySelector('.js-issuable-selector').getAttribute('data-data')).toBe( + createComponent(); + expect(findIssuableSelector().attributes('data-data')).toBe( '{"test":[{"name":"test","id":"test","project_path":"/","namespace_path":"/"}]}', ); }); - it('updates formState when changing template', () => { - vm.issuableTemplate.editor.setValue('test new template'); + it('emits input event', () => { + createComponent(); + wrapper.vm.issuableTemplate.editor.setValue('test new template'); - expect(formState.description).toBe('test new template'); + expect(wrapper.emitted('input')).toEqual([['test new template']]); }); - it('returns formState description with editor getValue', () => { - formState.description = 'testing new template'; - - expect(vm.issuableTemplate.editor.getValue()).toBe('testing new template'); + it('returns value with editor getValue', () => { + createComponent(); + expect(wrapper.vm.issuableTemplate.editor.getValue()).toBe('test'); }); -}); - -describe('Issue description template component with templates as array', () => { - let vm; - let formState; - beforeEach(() => { - const Component = Vue.extend(descriptionTemplate); - formState = { - description: 'test', - }; - - vm = new Component({ - propsData: { - formState, - issuableTemplates: [{ name: 'test', id: 'test', project_path: '/', namespace_path: '/' }], - projectId: 1, - projectPath: '/', - namespacePath: '/', - projectNamespace: '/', - }, - }).$mount(); - }); - - it('renders templates as JSON array in data attribute', () => { - expect(vm.$el.querySelector('.js-issuable-selector').getAttribute('data-data')).toBe( - '[{"name":"test","id":"test","project_path":"/","namespace_path":"/"}]', - ); + describe('Issue description template component with templates as array', () => { + it('renders templates as JSON array in data attribute', () => { + createComponent({ + propsData: { + value: 'test', + issuableTemplates: [{ name: 'test', id: 'test', project_path: '/', namespace_path: '/' }], + projectId: 1, + projectPath: '/', + namespacePath: '/', + projectNamespace: '/', + }, + }); + expect(findIssuableSelector().attributes('data-data')).toBe( + '[{"name":"test","id":"test","project_path":"/","namespace_path":"/"}]', + ); + }); }); }); diff --git a/spec/frontend/issues/show/components/fields/title_spec.js b/spec/frontend/issues/show/components/fields/title_spec.js index efd0b6fbd30..de04405d89b 100644 --- a/spec/frontend/issues/show/components/fields/title_spec.js +++ b/spec/frontend/issues/show/components/fields/title_spec.js @@ -12,9 +12,7 @@ describe('Title field component', () => { wrapper = shallowMount(TitleField, { propsData: { - formState: { - title: 'test', - }, + value: 'test', }, }); }); 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 20c6cda33d4..35acca60de7 100644 --- a/spec/frontend/issues/show/components/incidents/incident_tabs_spec.js +++ b/spec/frontend/issues/show/components/incidents/incident_tabs_spec.js @@ -34,8 +34,9 @@ describe('Incident Tabs component', () => { provide: { fullPath: '', iid: '', + projectId: '', uploadMetricsFeatureAvailable: true, - glFeatures: { incidentTimelineEventTab: true, incidentTimelineEvents: true }, + glFeatures: { incidentTimeline: true, incidentTimelineEvents: true }, }, data() { return { alert: mockAlert, ...data }; @@ -57,7 +58,6 @@ describe('Incident Tabs component', () => { const findTabs = () => wrapper.findAll(GlTab); const findSummaryTab = () => findTabs().at(0); - const findMetricsTab = () => wrapper.find('[data-testid="metrics-tab"]'); const findAlertDetailsTab = () => wrapper.find('[data-testid="alert-details-tab"]'); const findAlertDetailsComponent = () => wrapper.find(AlertDetailsTable); const findDescriptionComponent = () => wrapper.find(DescriptionComponent); @@ -111,20 +111,6 @@ describe('Incident Tabs component', () => { }); }); - describe('upload metrics feature available', () => { - it('shows the metric tab when metrics are available', () => { - mountComponent({}, { provide: { uploadMetricsFeatureAvailable: true } }); - - expect(findMetricsTab().exists()).toBe(true); - }); - - it('hides the tab when metrics are not available', () => { - mountComponent({}, { provide: { uploadMetricsFeatureAvailable: false } }); - - expect(findMetricsTab().exists()).toBe(false); - }); - }); - describe('Snowplow tracking', () => { beforeEach(() => { jest.spyOn(Tracking, 'event'); diff --git a/spec/frontend/issues/show/mock_data/mock_data.js b/spec/frontend/issues/show/mock_data/mock_data.js index 89653ff82b2..7b0b8ca686a 100644 --- a/spec/frontend/issues/show/mock_data/mock_data.js +++ b/spec/frontend/issues/show/mock_data/mock_data.js @@ -72,3 +72,18 @@ export const descriptionHtmlWithCheckboxes = ` </li> </ul> `; + +export const descriptionHtmlWithTask = ` + <ul data-sourcepos="1:1-3:7" class="task-list" dir="auto"> + <li data-sourcepos="1:1-1:10" class="task-list-item"> + <input type="checkbox" class="task-list-item-checkbox" disabled> + <a href="/gitlab-org/gitlab-test/-/issues/48" data-original="#48+" data-link="false" data-link-reference="false" data-project="1" data-issue="2" data-reference-format="+" data-reference-type="task" data-container="body" data-placement="top" title="1" class="gfm gfm-issue has-tooltip">1 (#48)</a> + </li> + <li data-sourcepos="2:1-2:7" class="task-list-item"> + <input type="checkbox" class="task-list-item-checkbox" disabled> 2 + </li> + <li data-sourcepos="3:1-3:7" class="task-list-item"> + <input type="checkbox" class="task-list-item-checkbox" disabled> 3 + </li> + </ul> +`; |