Commit 87f8d02d authored by Natalia Tepluhina's avatar Natalia Tepluhina

Merge branch '301192-add-tasklist-support-in-test-cases' into 'master'

Add task list support in Issuable Show app

See merge request gitlab-org/gitlab!56196
parents e1cda699 928c49b1
<script> <script>
import { GlLink } from '@gitlab/ui'; import { GlLink } from '@gitlab/ui';
import TaskList from '~/task_list';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import IssuableDescription from './issuable_description.vue'; import IssuableDescription from './issuable_description.vue';
...@@ -40,6 +42,11 @@ export default { ...@@ -40,6 +42,11 @@ export default {
type: Boolean, type: Boolean,
required: true, required: true,
}, },
enableTaskList: {
type: Boolean,
required: false,
default: false,
},
editFormVisible: { editFormVisible: {
type: Boolean, type: Boolean,
required: true, required: true,
...@@ -56,6 +63,16 @@ export default { ...@@ -56,6 +63,16 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
taskListUpdatePath: {
type: String,
required: false,
default: '',
},
taskListLockVersion: {
type: Number,
required: false,
default: 0,
},
}, },
computed: { computed: {
isUpdated() { isUpdated() {
...@@ -65,7 +82,50 @@ export default { ...@@ -65,7 +82,50 @@ export default {
return this.issuable.updatedBy; return this.issuable.updatedBy;
}, },
}, },
watch: {
/**
* When user switches between view and edit modes,
* taskList instance becomes invalid so whenever
* view mode is rendered, we need to re-initialize
* taskList to ensure the behaviour functional.
*/
editFormVisible(value) {
if (!value) {
this.$nextTick(() => {
this.initTaskList();
});
}
},
},
mounted() {
if (this.enableEdit && this.enableTaskList) {
this.initTaskList();
}
},
methods: { methods: {
initTaskList() {
this.taskList = new TaskList({
/**
* We have hard-coded dataType to `issue`
* as currently only `issue` types can handle
* task-lists, however, we can still use
* task lists in Issue, Test Cases and Incidents
* as all of those are derived from `issue`.
*/
dataType: 'issue',
fieldName: 'description',
lockVersion: this.taskListLockVersion,
selector: '.js-detail-page-description',
onSuccess: this.handleTaskListUpdateSuccess.bind(this),
onError: this.handleTaskListUpdateFailure.bind(this),
});
},
handleTaskListUpdateSuccess(updatedIssuable) {
this.$emit('task-list-update-success', updatedIssuable);
},
handleTaskListUpdateFailure() {
this.$emit('task-list-update-failure');
},
handleKeydownTitle(e, issuableMeta) { handleKeydownTitle(e, issuableMeta) {
this.$emit('keydown-title', e, issuableMeta); this.$emit('keydown-title', e, issuableMeta);
}, },
...@@ -78,7 +138,7 @@ export default { ...@@ -78,7 +138,7 @@ export default {
<template> <template>
<div class="issue-details issuable-details"> <div class="issue-details issuable-details">
<div class="detail-page-description content-block"> <div class="detail-page-description js-detail-page-description content-block">
<issuable-edit-form <issuable-edit-form
v-if="editFormVisible" v-if="editFormVisible"
:issuable="issuable" :issuable="issuable"
...@@ -106,7 +166,13 @@ export default { ...@@ -106,7 +166,13 @@ export default {
<slot name="status-badge"></slot> <slot name="status-badge"></slot>
</template> </template>
</issuable-title> </issuable-title>
<issuable-description v-if="issuable.descriptionHtml" :issuable="issuable" /> <issuable-description
v-if="issuable.descriptionHtml"
:issuable="issuable"
:enable-task-list="enableTaskList"
:can-edit="enableEdit"
:task-list-update-path="taskListUpdatePath"
/>
<small v-if="isUpdated" class="edited-text gl-font-sm!"> <small v-if="isUpdated" class="edited-text gl-font-sm!">
{{ __('Edited') }} {{ __('Edited') }}
<time-ago-tooltip :time="issuable.updatedAt" tooltip-placement="bottom" /> <time-ago-tooltip :time="issuable.updatedAt" tooltip-placement="bottom" />
......
...@@ -12,6 +12,18 @@ export default { ...@@ -12,6 +12,18 @@ export default {
type: Object, type: Object,
required: true, required: true,
}, },
enableTaskList: {
type: Boolean,
required: true,
},
canEdit: {
type: Boolean,
required: true,
},
taskListUpdatePath: {
type: String,
required: true,
},
}, },
mounted() { mounted() {
this.renderGFM(); this.renderGFM();
...@@ -25,7 +37,16 @@ export default { ...@@ -25,7 +37,16 @@ export default {
</script> </script>
<template> <template>
<div class="description"> <div class="description" :class="{ 'js-task-list-container': canEdit && enableTaskList }">
<div ref="gfmContainer" v-safe-html="issuable.descriptionHtml" class="md"></div> <div ref="gfmContainer" v-safe-html="issuable.descriptionHtml" class="md"></div>
<textarea
v-if="issuable.description && enableTaskList"
ref="textarea"
:value="issuable.description"
:data-update-url="taskListUpdatePath"
class="gl-display-none js-task-list-field"
dir="auto"
>
</textarea>
</div> </div>
</template> </template>
...@@ -3,6 +3,7 @@ import { GlIcon, GlButton, GlTooltipDirective, GlAvatarLink, GlAvatarLabeled } f ...@@ -3,6 +3,7 @@ import { GlIcon, GlButton, GlTooltipDirective, GlAvatarLink, GlAvatarLabeled } f
import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { isExternal } from '~/lib/utils/url_utility'; import { isExternal } from '~/lib/utils/url_utility';
import { n__, sprintf } from '~/locale';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
export default { export default {
...@@ -45,6 +46,11 @@ export default { ...@@ -45,6 +46,11 @@ export default {
required: false, required: false,
default: false, default: false,
}, },
taskCompletionStatus: {
type: Object,
required: false,
default: null,
},
}, },
computed: { computed: {
authorId() { authorId() {
...@@ -53,6 +59,18 @@ export default { ...@@ -53,6 +59,18 @@ export default {
isAuthorExternal() { isAuthorExternal() {
return isExternal(this.author.webUrl); return isExternal(this.author.webUrl);
}, },
taskStatusString() {
const { count, completedCount } = this.taskCompletionStatus;
return sprintf(
n__(
'%{completedCount} of %{count} task completed',
'%{completedCount} of %{count} tasks completed',
count,
),
{ completedCount, count },
);
},
}, },
mounted() { mounted() {
this.toggleSidebarButtonEl = document.querySelector('.js-toggle-right-sidebar-button'); this.toggleSidebarButtonEl = document.querySelector('.js-toggle-right-sidebar-button');
...@@ -74,8 +92,8 @@ export default { ...@@ -74,8 +92,8 @@ export default {
<gl-icon v-if="statusIcon" :name="statusIcon" class="d-block d-sm-none" /> <gl-icon v-if="statusIcon" :name="statusIcon" class="d-block d-sm-none" />
<span class="d-none d-sm-block"><slot name="status-badge"></slot></span> <span class="d-none d-sm-block"><slot name="status-badge"></slot></span>
</div> </div>
<div class="issuable-meta gl-display-flex gl-align-items-center"> <div class="issuable-meta gl-display-flex gl-align-items-center d-md-inline-block">
<div class="gl-display-inline-block"> <div v-if="blocked || confidential" class="gl-display-inline-block">
<div v-if="blocked" data-testid="blocked" class="issuable-warning-icon inline"> <div v-if="blocked" data-testid="blocked" class="issuable-warning-icon inline">
<gl-icon name="lock" :aria-label="__('Blocked')" /> <gl-icon name="lock" :aria-label="__('Blocked')" />
</div> </div>
...@@ -95,13 +113,13 @@ export default { ...@@ -95,13 +113,13 @@ export default {
:data-name="author.name" :data-name="author.name"
:href="author.webUrl" :href="author.webUrl"
target="_blank" target="_blank"
class="js-user-link gl-ml-2" class="js-user-link gl-vertical-align-middle gl-ml-2"
> >
<gl-avatar-labeled <gl-avatar-labeled
:size="24" :size="24"
:src="author.avatarUrl" :src="author.avatarUrl"
:label="author.name" :label="author.name"
class="d-none d-sm-inline-flex gl-ml-1" class="d-none d-sm-inline-flex gl-mx-1"
> >
<template #meta> <template #meta>
<gl-icon v-if="isAuthorExternal" name="external-link" /> <gl-icon v-if="isAuthorExternal" name="external-link" />
...@@ -109,6 +127,12 @@ export default { ...@@ -109,6 +127,12 @@ export default {
</gl-avatar-labeled> </gl-avatar-labeled>
<strong class="author d-sm-none d-inline">@{{ author.username }}</strong> <strong class="author d-sm-none d-inline">@{{ author.username }}</strong>
</gl-avatar-link> </gl-avatar-link>
<span
v-if="taskCompletionStatus"
data-testid="task-status"
class="gl-display-none gl-md-display-block gl-lg-display-inline-block"
>{{ taskStatusString }}</span
>
</div> </div>
<gl-button <gl-button
data-testid="sidebar-toggle" data-testid="sidebar-toggle"
......
...@@ -42,6 +42,11 @@ export default { ...@@ -42,6 +42,11 @@ export default {
required: false, required: false,
default: true, default: true,
}, },
enableTaskList: {
type: Boolean,
required: false,
default: false,
},
editFormVisible: { editFormVisible: {
type: Boolean, type: Boolean,
required: false, required: false,
...@@ -62,6 +67,21 @@ export default { ...@@ -62,6 +67,21 @@ export default {
required: false, required: false,
default: '', default: '',
}, },
taskCompletionStatus: {
type: Object,
required: false,
default: null,
},
taskListUpdatePath: {
type: String,
required: false,
default: '',
},
taskListLockVersion: {
type: Number,
required: false,
default: 0,
},
}, },
methods: { methods: {
handleKeydownTitle(e, issuableMeta) { handleKeydownTitle(e, issuableMeta) {
...@@ -83,6 +103,7 @@ export default { ...@@ -83,6 +103,7 @@ export default {
:confidential="issuable.confidential" :confidential="issuable.confidential"
:created-at="issuable.createdAt" :created-at="issuable.createdAt"
:author="issuable.author" :author="issuable.author"
:task-completion-status="taskCompletionStatus"
> >
<template #status-badge> <template #status-badge>
<slot name="status-badge"></slot> <slot name="status-badge"></slot>
...@@ -99,11 +120,16 @@ export default { ...@@ -99,11 +120,16 @@ export default {
:enable-edit="enableEdit" :enable-edit="enableEdit"
:enable-autocomplete="enableAutocomplete" :enable-autocomplete="enableAutocomplete"
:enable-autosave="enableAutosave" :enable-autosave="enableAutosave"
:enable-task-list="enableTaskList"
:edit-form-visible="editFormVisible" :edit-form-visible="editFormVisible"
:show-field-title="showFieldTitle" :show-field-title="showFieldTitle"
:description-preview-path="descriptionPreviewPath" :description-preview-path="descriptionPreviewPath"
:description-help-path="descriptionHelpPath" :description-help-path="descriptionHelpPath"
:task-list-update-path="taskListUpdatePath"
:task-list-lock-version="taskListLockVersion"
@edit-issuable="$emit('edit-issuable', $event)" @edit-issuable="$emit('edit-issuable', $event)"
@task-list-update-success="$emit('task-list-update-success', $event)"
@task-list-update-failure="$emit('task-list-update-failure')"
@keydown-title="handleKeydownTitle" @keydown-title="handleKeydownTitle"
@keydown-description="handleKeydownDescription" @keydown-description="handleKeydownDescription"
> >
......
...@@ -7,6 +7,7 @@ import { ...@@ -7,6 +7,7 @@ import {
GlButton, GlButton,
GlSprintf, GlSprintf,
GlLink, GlLink,
GlAlert,
} from '@gitlab/ui'; } from '@gitlab/ui';
import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import { getIdFromGraphQLId } from '~/graphql_shared/utils';
...@@ -31,6 +32,7 @@ export default { ...@@ -31,6 +32,7 @@ export default {
GlButton, GlButton,
GlSprintf, GlSprintf,
GlLink, GlLink,
GlAlert,
IssuableShow, IssuableShow,
TestCaseSidebar, TestCaseSidebar,
}, },
...@@ -39,6 +41,8 @@ export default { ...@@ -39,6 +41,8 @@ export default {
'projectFullPath', 'projectFullPath',
'testCaseNewPath', 'testCaseNewPath',
'testCaseId', 'testCaseId',
'updatePath',
'lockVersion',
'canEditTestCase', 'canEditTestCase',
'descriptionPreviewPath', 'descriptionPreviewPath',
'descriptionHelpPath', 'descriptionHelpPath',
...@@ -49,6 +53,7 @@ export default { ...@@ -49,6 +53,7 @@ export default {
editTestCaseFormVisible: false, editTestCaseFormVisible: false,
testCaseSaveInProgress: false, testCaseSaveInProgress: false,
testCaseStateChangeInProgress: false, testCaseStateChangeInProgress: false,
taskListUpdateFailed: false,
}; };
}, },
computed: { computed: {
...@@ -98,6 +103,9 @@ export default { ...@@ -98,6 +103,9 @@ export default {
this.testCaseStateChangeInProgress = false; this.testCaseStateChangeInProgress = false;
}); });
}, },
handleTaskListUpdateFailure() {
this.taskListUpdateFailed = true;
},
handleEditTestCase() { handleEditTestCase() {
this.editTestCaseFormVisible = true; this.editTestCaseFormVisible = true;
}, },
...@@ -132,6 +140,13 @@ export default { ...@@ -132,6 +140,13 @@ export default {
<template> <template>
<div class="test-case-container"> <div class="test-case-container">
<gl-alert v-if="taskListUpdateFailed" variant="danger" @dismiss="taskListUpdateFailed = false">
{{
__(
'Someone edited this test case at the same time you did. The description has been updated and you will need to make your changes again.',
)
}}
</gl-alert>
<gl-loading-icon v-if="testCaseLoading" size="md" class="gl-mt-3" /> <gl-loading-icon v-if="testCaseLoading" size="md" class="gl-mt-3" />
<issuable-show <issuable-show
v-if="!testCaseLoading && !testCaseLoadFailed" v-if="!testCaseLoading && !testCaseLoadFailed"
...@@ -140,10 +155,15 @@ export default { ...@@ -140,10 +155,15 @@ export default {
:status-icon="statusIcon" :status-icon="statusIcon"
:enable-edit="canEditTestCase" :enable-edit="canEditTestCase"
:enable-autocomplete="true" :enable-autocomplete="true"
:enable-task-list="true"
:edit-form-visible="editTestCaseFormVisible" :edit-form-visible="editTestCaseFormVisible"
:description-preview-path="descriptionPreviewPath" :description-preview-path="descriptionPreviewPath"
:description-help-path="descriptionHelpPath" :description-help-path="descriptionHelpPath"
:task-completion-status="testCase.taskCompletionStatus"
:task-list-update-path="updatePath"
:task-list-lock-version="lockVersion"
@edit-issuable="handleEditTestCase" @edit-issuable="handleEditTestCase"
@task-list-update-failure="handleTaskListUpdateFailure"
> >
<template #status-badge> <template #status-badge>
<gl-sprintf <gl-sprintf
......
...@@ -8,6 +8,7 @@ fragment TestCase on Issue { ...@@ -8,6 +8,7 @@ fragment TestCase on Issue {
description description
descriptionHtml descriptionHtml
state state
type
createdAt createdAt
updatedAt updatedAt
updatedBy { updatedBy {
...@@ -34,4 +35,8 @@ fragment TestCase on Issue { ...@@ -34,4 +35,8 @@ fragment TestCase on Issue {
state state
} }
} }
taskCompletionStatus {
count
completedCount
}
} }
...@@ -28,6 +28,7 @@ export default function initTestCaseShow({ mountPointSelector }) { ...@@ -28,6 +28,7 @@ export default function initTestCaseShow({ mountPointSelector }) {
...el.dataset, ...el.dataset,
projectsFetchPath: sidebarOptions.projectsAutocompleteEndpoint, projectsFetchPath: sidebarOptions.projectsAutocompleteEndpoint,
canEditTestCase: parseBoolean(el.dataset.canEditTestCase), canEditTestCase: parseBoolean(el.dataset.canEditTestCase),
lockVersion: parseInt(el.dataset.lockVersion, 10),
}, },
render: (createElement) => createElement(TestCaseShowApp), render: (createElement) => createElement(TestCaseShowApp),
}); });
......
...@@ -6,6 +6,8 @@ ...@@ -6,6 +6,8 @@
- page_description @test_case.description - page_description @test_case.description
#js-issuable-app{ data: { initial: issuable_initial_data(@test_case).to_json, #js-issuable-app{ data: { initial: issuable_initial_data(@test_case).to_json,
update_path: issuable_path(@test_case, format: :json),
lock_version: @test_case.lock_version,
can_edit_test_case: can?(current_user, :admin_issue, @project).to_s, can_edit_test_case: can?(current_user, :admin_issue, @project).to_s,
can_move_test_case: @issuable_sidebar.dig(:current_user, :can_move).to_s, can_move_test_case: @issuable_sidebar.dig(:current_user, :can_move).to_s,
description_preview_path: preview_markdown_path(@project), description_preview_path: preview_markdown_path(@project),
......
---
title: Add support for task lists within Test Case description
merge_request: 56196
author:
type: added
import { GlLink, GlLoadingIcon, GlSprintf } from '@gitlab/ui'; import { GlLink, GlLoadingIcon, GlSprintf, GlAlert } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import TestCaseShowRoot from 'ee/test_case_show/components/test_case_show_root.vue'; import TestCaseShowRoot from 'ee/test_case_show/components/test_case_show_root.vue';
...@@ -268,9 +268,26 @@ describe('TestCaseShowRoot', () => { ...@@ -268,9 +268,26 @@ describe('TestCaseShowRoot', () => {
expect(wrapper.find(GlLoadingIcon).exists()).toBe(true); expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
}); });
it('renders gl-alert when issuable-show component emits `task-list-update-failure` event', async () => {
await wrapper.find(IssuableShow).vm.$emit('task-list-update-failure');
const alertEl = wrapper.find(GlAlert);
expect(alertEl.exists()).toBe(true);
expect(alertEl.text()).toBe(
'Someone edited this test case at the same time you did. The description has been updated and you will need to make your changes again.',
);
});
it('renders issuable-show when `testCaseLoading` prop is false', () => { it('renders issuable-show when `testCaseLoading` prop is false', () => {
const { statusBadgeClass, statusIcon, editTestCaseFormVisible } = wrapper.vm; const { statusBadgeClass, statusIcon, editTestCaseFormVisible } = wrapper.vm;
const { canEditTestCase, descriptionPreviewPath, descriptionHelpPath } = mockProvide; const {
canEditTestCase,
descriptionPreviewPath,
descriptionHelpPath,
updatePath,
lockVersion,
} = mockProvide;
const issuableShowEl = wrapper.find(IssuableShow); const issuableShowEl = wrapper.find(IssuableShow);
expect(issuableShowEl.exists()).toBe(true); expect(issuableShowEl.exists()).toBe(true);
...@@ -280,9 +297,13 @@ describe('TestCaseShowRoot', () => { ...@@ -280,9 +297,13 @@ describe('TestCaseShowRoot', () => {
descriptionPreviewPath, descriptionPreviewPath,
descriptionHelpPath, descriptionHelpPath,
enableAutocomplete: true, enableAutocomplete: true,
enableTaskList: true,
issuable: mockTestCase, issuable: mockTestCase,
enableEdit: canEditTestCase, enableEdit: canEditTestCase,
editFormVisible: editTestCaseFormVisible, editFormVisible: editTestCaseFormVisible,
taskCompletionStatus: mockTestCase.taskCompletionStatus,
taskListUpdatePath: updatePath,
taskListLockVersion: lockVersion,
}); });
}); });
......
...@@ -6,6 +6,10 @@ export const mockTestCase = { ...@@ -6,6 +6,10 @@ export const mockTestCase = {
currentUserTodos: { currentUserTodos: {
nodes: [mockCurrentUserTodo], nodes: [mockCurrentUserTodo],
}, },
taskCompletionStatus: {
completedCount: 0,
count: 5,
},
}; };
export const mockProvide = { export const mockProvide = {
...@@ -19,4 +23,6 @@ export const mockProvide = { ...@@ -19,4 +23,6 @@ export const mockProvide = {
labelsFetchPath: '/gitlab-org/gitlab-test/-/labels.json', labelsFetchPath: '/gitlab-org/gitlab-test/-/labels.json',
labelsManagePath: '/gitlab-org/gitlab-shell/-/labels', labelsManagePath: '/gitlab-org/gitlab-shell/-/labels',
projectsFetchPath: '/-/autocomplete/projects?project_id=1', projectsFetchPath: '/-/autocomplete/projects?project_id=1',
updatePath: `${mockIssuable.webUrl}.json`,
lockVersion: 1,
}; };
...@@ -28134,6 +28134,9 @@ msgstr "" ...@@ -28134,6 +28134,9 @@ msgstr ""
msgid "Someone edited this merge request at the same time you did. Please refresh the page to see changes." msgid "Someone edited this merge request at the same time you did. Please refresh the page to see changes."
msgstr "" msgstr ""
msgid "Someone edited this test case at the same time you did. The description has been updated and you will need to make your changes again."
msgstr ""
msgid "Something went wrong" msgid "Something went wrong"
msgstr "" msgstr ""
......
...@@ -6,11 +6,13 @@ import IssuableBody from '~/issuable_show/components/issuable_body.vue'; ...@@ -6,11 +6,13 @@ import IssuableBody from '~/issuable_show/components/issuable_body.vue';
import IssuableDescription from '~/issuable_show/components/issuable_description.vue'; import IssuableDescription from '~/issuable_show/components/issuable_description.vue';
import IssuableEditForm from '~/issuable_show/components/issuable_edit_form.vue'; import IssuableEditForm from '~/issuable_show/components/issuable_edit_form.vue';
import IssuableTitle from '~/issuable_show/components/issuable_title.vue'; import IssuableTitle from '~/issuable_show/components/issuable_title.vue';
import TaskList from '~/task_list';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import { mockIssuableShowProps, mockIssuable } from '../mock_data'; import { mockIssuableShowProps, mockIssuable } from '../mock_data';
jest.mock('~/autosave'); jest.mock('~/autosave');
jest.mock('~/flash');
const issuableBodyProps = { const issuableBodyProps = {
...mockIssuableShowProps, ...mockIssuableShowProps,
...@@ -80,6 +82,75 @@ describe('IssuableBody', () => { ...@@ -80,6 +82,75 @@ describe('IssuableBody', () => {
}); });
}); });
describe('watchers', () => {
describe('editFormVisible', () => {
it('calls initTaskList in nextTick', async () => {
jest.spyOn(wrapper.vm, 'initTaskList');
wrapper.setProps({
editFormVisible: true,
});
await wrapper.vm.$nextTick();
wrapper.setProps({
editFormVisible: false,
});
await wrapper.vm.$nextTick();
expect(wrapper.vm.initTaskList).toHaveBeenCalled();
});
});
});
describe('mounted', () => {
it('initializes TaskList instance when enabledEdit and enableTaskList props are true', () => {
expect(wrapper.vm.taskList instanceof TaskList).toBe(true);
expect(wrapper.vm.taskList).toMatchObject({
dataType: 'issue',
fieldName: 'description',
lockVersion: issuableBodyProps.taskListLockVersion,
selector: '.js-detail-page-description',
onSuccess: expect.any(Function),
onError: expect.any(Function),
});
});
it('does not initialize TaskList instance when either enabledEdit or enableTaskList prop is false', () => {
const wrapperNoTaskList = createComponent({
...issuableBodyProps,
enableTaskList: false,
});
expect(wrapperNoTaskList.vm.taskList).not.toBeDefined();
wrapperNoTaskList.destroy();
});
});
describe('methods', () => {
describe('handleTaskListUpdateSuccess', () => {
it('emits `task-list-update-success` event on component', () => {
const updatedIssuable = {
foo: 'bar',
};
wrapper.vm.handleTaskListUpdateSuccess(updatedIssuable);
expect(wrapper.emitted('task-list-update-success')).toBeTruthy();
expect(wrapper.emitted('task-list-update-success')[0]).toEqual([updatedIssuable]);
});
});
describe('handleTaskListUpdateFailure', () => {
it('emits `task-list-update-failure` event on component', () => {
wrapper.vm.handleTaskListUpdateFailure();
expect(wrapper.emitted('task-list-update-failure')).toBeTruthy();
});
});
});
describe('template', () => { describe('template', () => {
it('renders issuable-title component', () => { it('renders issuable-title component', () => {
const titleEl = wrapper.find(IssuableTitle); const titleEl = wrapper.find(IssuableTitle);
......
...@@ -5,9 +5,14 @@ import IssuableDescription from '~/issuable_show/components/issuable_description ...@@ -5,9 +5,14 @@ import IssuableDescription from '~/issuable_show/components/issuable_description
import { mockIssuable } from '../mock_data'; import { mockIssuable } from '../mock_data';
const createComponent = (issuable = mockIssuable) => const createComponent = ({
issuable = mockIssuable,
enableTaskList = true,
canEdit = true,
taskListUpdatePath = `${mockIssuable.webUrl}.json`,
} = {}) =>
shallowMount(IssuableDescription, { shallowMount(IssuableDescription, {
propsData: { issuable }, propsData: { issuable, enableTaskList, canEdit, taskListUpdatePath },
}); });
describe('IssuableDescription', () => { describe('IssuableDescription', () => {
...@@ -38,4 +43,27 @@ describe('IssuableDescription', () => { ...@@ -38,4 +43,27 @@ describe('IssuableDescription', () => {
}); });
}); });
}); });
describe('templates', () => {
it('renders container element with class `js-task-list-container` when canEdit and enableTaskList props are true', () => {
expect(wrapper.classes()).toContain('js-task-list-container');
});
it('renders container element without class `js-task-list-container` when canEdit and enableTaskList props are true', () => {
const wrapperNoTaskList = createComponent({
enableTaskList: false,
});
expect(wrapperNoTaskList.classes()).not.toContain('js-task-list-container');
wrapperNoTaskList.destroy();
});
it('renders hidden textarea element when issuable.description is present and enableTaskList prop is true', () => {
const textareaEl = wrapper.find('textarea.gl-display-none.js-task-list-field');
expect(textareaEl.exists()).toBe(true);
expect(textareaEl.attributes('data-update-url')).toBe(`${mockIssuable.webUrl}.json`);
});
});
}); });
...@@ -119,6 +119,27 @@ describe('IssuableHeader', () => { ...@@ -119,6 +119,27 @@ describe('IssuableHeader', () => {
expect(avatarEl.find(GlAvatarLabeled).find(GlIcon).exists()).toBe(false); expect(avatarEl.find(GlAvatarLabeled).find(GlIcon).exists()).toBe(false);
}); });
it('renders tast status text when `taskCompletionStatus` prop is defined', () => {
let taskStatusEl = wrapper.findByTestId('task-status');
expect(taskStatusEl.exists()).toBe(true);
expect(taskStatusEl.text()).toContain('0 of 5 tasks completed');
const wrapperSingleTask = createComponent({
...issuableHeaderProps,
taskCompletionStatus: {
completedCount: 0,
count: 1,
},
});
taskStatusEl = wrapperSingleTask.findByTestId('task-status');
expect(taskStatusEl.text()).toContain('0 of 1 task completed');
wrapperSingleTask.destroy();
});
it('renders sidebar toggle button', () => { it('renders sidebar toggle button', () => {
const toggleButtonEl = wrapper.findByTestId('sidebar-toggle'); const toggleButtonEl = wrapper.findByTestId('sidebar-toggle');
......
...@@ -54,6 +54,7 @@ describe('IssuableShowRoot', () => { ...@@ -54,6 +54,7 @@ describe('IssuableShowRoot', () => {
editFormVisible, editFormVisible,
descriptionPreviewPath, descriptionPreviewPath,
descriptionHelpPath, descriptionHelpPath,
taskCompletionStatus,
} = mockIssuableShowProps; } = mockIssuableShowProps;
const { blocked, confidential, createdAt, author } = mockIssuable; const { blocked, confidential, createdAt, author } = mockIssuable;
...@@ -72,6 +73,7 @@ describe('IssuableShowRoot', () => { ...@@ -72,6 +73,7 @@ describe('IssuableShowRoot', () => {
confidential, confidential,
createdAt, createdAt,
author, author,
taskCompletionStatus,
}); });
expect(issuableHeader.find('.issuable-status-box').text()).toContain('Open'); expect(issuableHeader.find('.issuable-status-box').text()).toContain('Open');
expect(issuableHeader.find('.detail-page-header-actions button.js-close').exists()).toBe( expect(issuableHeader.find('.detail-page-header-actions button.js-close').exists()).toBe(
...@@ -111,6 +113,26 @@ describe('IssuableShowRoot', () => { ...@@ -111,6 +113,26 @@ describe('IssuableShowRoot', () => {
expect(wrapper.emitted('edit-issuable')).toBeTruthy(); expect(wrapper.emitted('edit-issuable')).toBeTruthy();
}); });
it('component emits `task-list-update-success` event bubbled via issuable-body', () => {
const issuableBody = wrapper.find(IssuableBody);
const eventParam = {
foo: 'bar',
};
issuableBody.vm.$emit('task-list-update-success', eventParam);
expect(wrapper.emitted('task-list-update-success')).toBeTruthy();
expect(wrapper.emitted('task-list-update-success')[0]).toEqual([eventParam]);
});
it('component emits `task-list-update-failure` event bubbled via issuable-body', () => {
const issuableBody = wrapper.find(IssuableBody);
issuableBody.vm.$emit('task-list-update-failure');
expect(wrapper.emitted('task-list-update-failure')).toBeTruthy();
});
it('component emits `sidebar-toggle` event bubbled via issuable-sidebar', () => { it('component emits `sidebar-toggle` event bubbled via issuable-sidebar', () => {
const issuableSidebar = wrapper.find(IssuableSidebar); const issuableSidebar = wrapper.find(IssuableSidebar);
......
...@@ -12,6 +12,7 @@ export const mockIssuable = { ...@@ -12,6 +12,7 @@ export const mockIssuable = {
blocked: false, blocked: false,
confidential: false, confidential: false,
updatedBy: issuable.author, updatedBy: issuable.author,
type: 'ISSUE',
currentUserTodos: { currentUserTodos: {
nodes: [ nodes: [
{ {
...@@ -26,11 +27,18 @@ export const mockIssuableShowProps = { ...@@ -26,11 +27,18 @@ export const mockIssuableShowProps = {
issuable: mockIssuable, issuable: mockIssuable,
descriptionHelpPath: '/help/user/markdown', descriptionHelpPath: '/help/user/markdown',
descriptionPreviewPath: '/gitlab-org/gitlab-shell/preview_markdown', descriptionPreviewPath: '/gitlab-org/gitlab-shell/preview_markdown',
taskListUpdatePath: `${mockIssuable.webUrl}.json`,
taskListLockVersion: 1,
editFormVisible: false, editFormVisible: false,
enableAutocomplete: true, enableAutocomplete: true,
enableAutosave: true, enableAutosave: true,
enableTaskList: true,
enableEdit: true, enableEdit: true,
showFieldTitle: false, showFieldTitle: false,
statusBadgeClass: 'status-box-open', statusBadgeClass: 'status-box-open',
statusIcon: 'issue-open-m', statusIcon: 'issue-open-m',
taskCompletionStatus: {
completedCount: 0,
count: 5,
},
}; };
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment