Commit 8f0c9efc authored by Nathan Friend's avatar Nathan Friend

Merge branch '224622-requirement-description-support' into 'master'

Add Requirement description support

See merge request gitlab-org/gitlab!44902
parents 1fb57db4 52a83481
<script>
import { GlDrawer, GlFormGroup, GlFormTextarea, GlFormCheckbox, GlButton } from '@gitlab/ui';
import '~/behaviors/markdown/render_gfm';
import $ from 'jquery';
import {
GlDrawer,
GlFormGroup,
GlFormTextarea,
GlButton,
GlFormCheckbox,
GlTooltipDirective,
GlSafeHtmlDirective as SafeHtml,
} from '@gitlab/ui';
import { isEmpty } from 'lodash';
import { __, sprintf } from '~/locale';
import ZenMode from '~/zen_mode';
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
import RequirementStatusBadge from './requirement_status_badge.vue';
import RequirementMeta from '../mixins/requirement_meta';
import { MAX_TITLE_LENGTH, TestReportStatus } from '../constants';
export default {
events: {
drawerClose: 'drawer-close',
disableEdit: 'disable-edit',
enableEdit: 'enable-edit',
},
titleInvalidMessage: sprintf(__('Requirement title cannot have more than %{limit} characters.'), {
limit: MAX_TITLE_LENGTH,
}),
......@@ -15,7 +35,15 @@ export default {
GlFormTextarea,
GlFormCheckbox,
GlButton,
MarkdownField,
RequirementStatusBadge,
},
directives: {
GlTooltip: GlTooltipDirective,
SafeHtml,
},
mixins: [RequirementMeta],
inject: ['descriptionPreviewPath', 'descriptionHelpPath'],
props: {
drawerOpen: {
type: Boolean,
......@@ -26,6 +54,11 @@ export default {
required: false,
default: null,
},
enableRequirementEdit: {
type: Boolean,
required: false,
default: false,
},
requirementRequestActive: {
type: Boolean,
required: true,
......@@ -33,8 +66,10 @@ export default {
},
data() {
return {
zenModeEnabled: false,
title: this.requirement?.title || '',
satisfied: this.requirement?.satisfied || false,
description: this.requirement?.description || '',
};
},
computed: {
......@@ -48,19 +83,17 @@ export default {
return this.isCreate ? __('Create requirement') : __('Save changes');
},
titleInvalid() {
return this.title.length > MAX_TITLE_LENGTH;
return this.title?.length > MAX_TITLE_LENGTH;
},
disableSaveButton() {
return this.title === '' || this.titleInvalid || this.requirementRequestActive;
},
reference() {
return `REQ-${this.requirement?.iid}`;
},
},
watch: {
requirement: {
handler(value) {
this.title = value?.title || '';
this.description = value?.description || '';
this.satisfied = value?.satisfied || false;
},
deep: true,
......@@ -69,10 +102,25 @@ export default {
// Clear `title` and `satisfied` value on drawer close.
if (!value) {
this.title = '';
this.description = '';
this.satisfied = false;
}
},
},
mounted() {
this.zenMode = new ZenMode();
$(this.$refs.gfmContainer).renderGFM();
$(document).on('zen_mode:enter', () => {
this.zenModeEnabled = true;
});
$(document).on('zen_mode:leave', () => {
this.zenModeEnabled = false;
});
},
beforeDestroy() {
$(document).off('zen_mode:enter');
$(document).off('zen_mode:leave');
},
methods: {
getDrawerHeaderHeight() {
const wrapperEl = document.querySelector('.js-requirements-container-wrapper');
......@@ -100,31 +148,83 @@ export default {
return null;
},
handleSave() {
if (this.isCreate) {
this.$emit('save', this.title);
handleFormInputKeyDown() {
if (this.zenModeEnabled) {
// Exit Zen mode, don't close the drawer.
this.zenModeEnabled = false;
this.zenMode.exit();
} else {
this.$emit('save', {
iid: this.requirement.iid,
title: this.title,
lastTestReportState: this.newLastTestReportState(),
});
this.$emit(this.$options.events.disableEdit);
}
},
handleSave() {
const { title, description } = this;
const eventParams = {
title,
description,
};
if (!this.isCreate) {
eventParams.iid = this.requirement.iid;
eventParams.lastTestReportState = this.newLastTestReportState();
}
this.$emit('save', eventParams);
},
handleCancel() {
this.$emit(
this.isCreate ? this.$options.events.drawerClose : this.$options.events.disableEdit,
);
},
},
};
</script>
<template>
<gl-drawer :open="drawerOpen" :header-height="getDrawerHeaderHeight()" @close="$emit('cancel')">
<gl-drawer
:open="drawerOpen"
:header-height="getDrawerHeaderHeight()"
:class="{ 'zen-mode gl-absolute': zenModeEnabled }"
class="requirement-form-drawer"
@close="$emit($options.events.drawerClose)"
>
<template #header>
<h4 class="gl-m-0">{{ fieldLabel }}</h4>
<h4 v-if="isCreate" class="gl-m-0">{{ __('New Requirement') }}</h4>
<div v-else class="gl-display-flex gl-align-items-center">
<strong class="gl-text-gray-500">{{ reference }}</strong>
<requirement-status-badge
v-if="testReport"
:test-report="testReport"
:last-test-report-manually-created="requirement.lastTestReportManuallyCreated"
class="gl-ml-3"
/>
</div>
</template>
<template>
<div class="requirement-form">
<span v-if="!isCreate" class="text-muted">{{ reference }}</span>
<div v-if="!enableRequirementEdit && !isCreate" class="requirement-details">
<div
class="title-container gl-display-flex gl-border-b-1 gl-border-b-solid gl-border-gray-100"
>
<h3 v-safe-html="titleHtml" class="title qa-title gl-flex-grow-1 gl-m-0 gl-mb-3"></h3>
<gl-button
v-if="canUpdate && !isArchived"
v-gl-tooltip.bottom
data-testid="edit"
:title="__('Edit title and description')"
icon="pencil"
class="btn-edit gl-align-self-start"
@click="$emit($options.events.enableEdit, $event)"
/>
</div>
<div data-testid="descriptionContainer" class="description-container gl-mt-3">
<div ref="gfmContainer" v-safe-html="descriptionHtml" class="md"></div>
</div>
</div>
<div v-else class="requirement-form">
<div class="requirement-form-container" :class="{ 'gl-flex-grow-1 gl-mt-2': !isCreate }">
<div data-testid="form-error-container" class="flash-container"></div>
<gl-form-group
data-testid="title"
:label="__('Title')"
:invalid-feedback="$options.titleInvalidMessage"
:state="!titleInvalid"
......@@ -137,12 +237,39 @@ export default {
autofocus
resize
:disabled="requirementRequestActive"
:placeholder="__('Describe the requirement here')"
:placeholder="__('Requirement title')"
max-rows="25"
class="requirement-form-textarea"
:class="{ 'gl-field-error-outline': titleInvalid }"
@keyup.escape.exact="$emit('cancel')"
@keydown.escape.exact.stop="handleFormInputKeyDown"
@keydown.meta.enter="handleSave"
@keydown.ctrl.enter="handleSave"
/>
</gl-form-group>
<gl-form-group data-testid="description" class="common-note-form">
<label for="requirementDescription" class="d-block col-form-label gl-pb-0!">
{{ __('Description') }}
</label>
<markdown-field
:markdown-preview-path="descriptionPreviewPath"
:markdown-docs-path="descriptionHelpPath"
:enable-autocomplete="false"
:textarea-value="description"
>
<template #textarea>
<textarea
id="requirementDescription"
v-model="description"
:data-supports-quick-actions="false"
:aria-label="__('Description')"
:placeholder="__('Describe the requirement here')"
class="note-textarea js-gfm-input js-autosize markdown-area qa-description-textarea"
@keydown.escape.exact.stop="handleFormInputKeyDown"
@keydown.meta.enter="handleSave"
@keydown.ctrl.enter="handleSave"
></textarea>
</template>
</markdown-field>
<gl-form-checkbox v-if="!isCreate" v-model="satisfied" class="gl-mt-6">{{
__('Satisfied')
}}</gl-form-checkbox>
......@@ -162,7 +289,7 @@ export default {
variant="default"
category="primary"
class="js-requirement-cancel"
@click="$emit('cancel')"
@click="handleCancel"
>
{{ __('Cancel') }}
</gl-button>
......
<script>
import { escape } from 'lodash';
import { GlPopover, GlLink, GlAvatar, GlButton, GlTooltipDirective } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
import { getTimeago } from '~/lib/utils/datetime_utility';
import timeagoMixin from '~/vue_shared/mixins/timeago';
import RequirementStatusBadge from './requirement_status_badge.vue';
import RequirementMeta from '../mixins/requirement_meta';
import { FilterState } from '../constants';
export default {
......@@ -20,7 +18,7 @@ export default {
directives: {
GlTooltip: GlTooltipDirective,
},
mixins: [timeagoMixin],
mixins: [RequirementMeta, timeagoMixin],
props: {
requirement: {
type: Object,
......@@ -42,36 +40,13 @@ export default {
required: false,
default: false,
},
active: {
type: Boolean,
required: false,
default: false,
},
computed: {
reference() {
return `REQ-${this.requirement.iid}`;
},
canUpdate() {
return this.requirement.userPermissions.updateRequirement;
},
canArchive() {
return this.requirement.userPermissions.adminRequirement;
},
createdAt() {
return sprintf(__('created %{timeAgo}'), {
timeAgo: escape(getTimeago().format(this.requirement.createdAt)),
});
},
updatedAt() {
return sprintf(__('updated %{timeAgo}'), {
timeAgo: escape(getTimeago().format(this.requirement.updatedAt)),
});
},
isArchived() {
return this.requirement?.state === FilterState.archived;
},
author() {
return this.requirement.author;
},
testReport() {
return this.requirement.testReports.nodes[0];
},
computed: {
showIssuableMetaActions() {
return Boolean(this.canUpdate || this.canArchive || this.testReport);
},
......@@ -105,7 +80,11 @@ export default {
</script>
<template>
<li class="issue requirement" :class="{ 'disabled-content': stateChangeRequestActive }">
<li
class="issue requirement gl-cursor-pointer"
:class="{ 'disabled-content': stateChangeRequestActive, 'gl-bg-blue-50': active }"
@click="$emit('show-click', requirement)"
>
<div class="issue-box">
<div class="issuable-info-container">
<span class="issuable-reference text-muted d-none d-sm-block mr-2">{{ reference }}</span>
......@@ -119,7 +98,7 @@ export default {
<span
v-gl-tooltip:tooltipcontainer.bottom
:title="tooltipTitle(requirement.createdAt)"
>{{ createdAt }}</span
>{{ createdAtFormatted }}</span
>
{{ __('by') }}
<gl-link ref="authorLink" class="author-link js-user-link" :href="author.webUrl">
......@@ -130,7 +109,7 @@ export default {
v-gl-tooltip:tooltipcontainer.bottom
:title="tooltipTitle(requirement.updatedAt)"
class="issuable-updated-at"
>&middot; {{ updatedAt }}</span
>&middot; {{ updatedAtFormatted }}</span
>
</div>
<requirement-status-badge
......@@ -154,7 +133,7 @@ export default {
v-gl-tooltip
icon="pencil"
:title="__('Edit')"
@click="$emit('editClick', requirement)"
@click="$emit('edit-click', requirement)"
/>
</li>
<li v-if="canArchive && !isArchived" class="requirement-archive d-sm-block">
......@@ -164,7 +143,7 @@ export default {
icon="archive"
:loading="stateChangeRequestActive"
:title="__('Archive')"
@click="handleArchiveClick"
@click.stop="handleArchiveClick"
/>
</li>
<li v-if="canArchive && isArchived" class="requirement-reopen d-sm-block">
......
......@@ -53,7 +53,7 @@ export default {
:description="emptyStateDescription"
>
<template v-if="emptyStateDescription && canCreateRequirement" #actions>
<gl-button category="primary" variant="success" @click="$emit('clickNewRequirement')">{{
<gl-button category="primary" variant="success" @click="$emit('click-new-requirement')">{{
__('New requirement')
}}</gl-button>
</template>
......
......@@ -50,7 +50,7 @@ export default {
id="state-opened"
data-state="opened"
:title="__('Filter by requirements that are currently opened.')"
@click="$emit('clickTab', { filterBy: $options.FilterState.opened })"
@click="$emit('click-tab', { filterBy: $options.FilterState.opened })"
>
{{ __('Open') }}
<gl-badge class="badge-pill">{{ requirementsCount.OPENED }}</gl-badge>
......@@ -61,7 +61,7 @@ export default {
id="state-archived"
data-state="archived"
:title="__('Filter by requirements that are currently archived.')"
@click="$emit('clickTab', { filterBy: $options.FilterState.archived })"
@click="$emit('click-tab', { filterBy: $options.FilterState.archived })"
>
{{ __('Archived') }}
<gl-badge class="badge-pill">{{ requirementsCount.ARCHIVED }}</gl-badge>
......@@ -72,7 +72,7 @@ export default {
id="state-all"
data-state="all"
:title="__('Show all requirements.')"
@click="$emit('clickTab', { filterBy: $options.FilterState.all })"
@click="$emit('click-tab', { filterBy: $options.FilterState.all })"
>
{{ __('All') }}
<gl-badge class="badge-pill">{{ requirementsCount.ALL }}</gl-badge>
......@@ -85,7 +85,7 @@ export default {
variant="success"
class="js-new-requirement qa-new-requirement-button"
:disabled="showCreateForm"
@click="$emit('clickNewRequirement')"
@click="$emit('click-new-requirement')"
>{{ __('New requirement') }}</gl-button
>
</div>
......
import { __, sprintf } from '~/locale';
import { getTimeago } from '~/lib/utils/datetime_utility';
import { FilterState } from '../constants';
export default {
computed: {
reference() {
return `REQ-${this.requirement?.iid}`;
},
titleHtml() {
return this.requirement?.titleHtml;
},
descriptionHtml() {
return this.requirement?.descriptionHtml;
},
isArchived() {
return this.requirement?.state === FilterState.archived;
},
author() {
return this.requirement?.author;
},
createdAtFormatted() {
return sprintf(__('created %{timeAgo}'), {
timeAgo: getTimeago().format(this.requirement?.createdAt),
});
},
updatedAtFormatted() {
return sprintf(__('updated %{timeAgo}'), {
timeAgo: getTimeago().format(this.requirement?.updatedAt),
});
},
testReport() {
return this.requirement?.testReports.nodes[0];
},
canUpdate() {
return this.requirement?.userPermissions.updateRequirement;
},
canArchive() {
return this.requirement?.userPermissions.adminRequirement;
},
},
};
#import "~/graphql_shared/fragments/pageInfo.fragment.graphql"
#import "./requirement.fragment.graphql"
query projectRequirementsEE(
$projectPath: ID!
$state: RequirementState
......@@ -7,7 +10,7 @@ query projectRequirementsEE(
$nextPageCursor: String = ""
$authorUsernames: [String!] = []
$search: String = ""
$sortBy: Sort = created_desc
$sortBy: Sort = CREATED_DESC
) {
project(fullPath: $projectPath) {
requirements(
......@@ -21,36 +24,10 @@ query projectRequirementsEE(
sort: $sortBy
) {
nodes {
iid
title
createdAt
updatedAt
state
lastTestReportState
lastTestReportManuallyCreated
testReports(first: 1, sort: created_desc) {
nodes {
id
state
createdAt
}
}
userPermissions {
updateRequirement
adminRequirement
}
author {
name
username
avatarUrl
webUrl
}
...Requirement
}
pageInfo {
hasPreviousPage
hasNextPage
startCursor
endCursor
...PageInfo
}
}
}
......
#import "~/graphql_shared/fragments/author.fragment.graphql"
fragment Requirement on Requirement {
iid
title
titleHtml
description
descriptionHtml
createdAt
updatedAt
state
lastTestReportState
lastTestReportManuallyCreated
testReports(first: 1, sort: CREATED_DESC) {
nodes {
id
state
createdAt
}
}
userPermissions {
updateRequirement
adminRequirement
}
author {
...Author
}
}
#import "./requirement.fragment.graphql"
mutation updateRequirement($updateRequirementInput: UpdateRequirementInput!) {
updateRequirement(input: $updateRequirementInput) {
clientMutationId
errors
requirement {
iid
title
state
updatedAt
lastTestReportState
testReports(first: 1, sort: created_desc) {
nodes {
id
state
createdAt
}
}
...Requirement
}
}
}
......@@ -38,6 +38,10 @@ export default () => {
components: {
RequirementsRoot,
},
provide: {
descriptionPreviewPath: el.dataset.descriptionPreviewPath,
descriptionHelpPath: el.dataset.descriptionHelpPath,
},
data() {
const {
filterBy,
......
......@@ -20,6 +20,12 @@
overflow-y: auto !important;
}
}
.requirement-form-drawer.zen-mode {
// We need to override `z-index` provided to GlDrawer
// in Zen mode to enable full-screen editing.
z-index: auto !important;
}
}
.requirements-list-container {
......@@ -73,7 +79,11 @@
}
.gl-drawer {
width: 480px;
// Both width & min-width
// are defined as per Pajamas
// See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/44902#note_429056182
width: 28%;
min-width: 400px;
padding-left: $gl-padding;
padding-right: $gl-padding;
box-shadow: none;
......
......@@ -31,6 +31,8 @@
all: total_requirements,
requirements_web_url: project_requirements_management_requirements_path(@project),
can_create_requirement: "#{can?(current_user, :create_requirement, @project)}",
description_preview_path: preview_markdown_path(@project),
description_help_path: help_page_path('user/markdown'),
empty_state_path: image_path('illustrations/empty-state/empty-requirements-lg.svg') } }
- if current_tab_count == 0
-# Show regular spinner only when there will be no
......
---
title: Add support for providing requirement description.
merge_request: 44902
author:
type: added
......@@ -6,10 +6,10 @@ RSpec.describe 'Requirements list', :js do
let_it_be(:user) { create(:user) }
let_it_be(:user_guest) { create(:user) }
let_it_be(:project) { create(:project, :repository) }
let_it_be(:requirement1) { create(:requirement, project: project, title: 'Some requirement-1', author: user, created_at: 5.days.ago, updated_at: 2.days.ago) }
let_it_be(:requirement2) { create(:requirement, project: project, title: 'Some requirement-2', author: user, created_at: 6.days.ago, updated_at: 2.days.ago) }
let_it_be(:requirement3) { create(:requirement, project: project, title: 'Some requirement-3', author: user, created_at: 7.days.ago, updated_at: 2.days.ago) }
let_it_be(:requirement_archived) { create(:requirement, project: project, title: 'Some requirement-3', state: :archived, author: user, created_at: 8.days.ago, updated_at: 2.days.ago) }
let_it_be(:requirement1) { create(:requirement, project: project, title: 'Some requirement-1', description: 'Sample description', author: user, created_at: 5.days.ago, updated_at: 2.days.ago) }
let_it_be(:requirement2) { create(:requirement, project: project, title: 'Some requirement-2', description: 'Sample description', author: user, created_at: 6.days.ago, updated_at: 2.days.ago) }
let_it_be(:requirement3) { create(:requirement, project: project, title: 'Some requirement-3', description: 'Sample description', author: user, created_at: 7.days.ago, updated_at: 2.days.ago) }
let_it_be(:requirement_archived) { create(:requirement, project: project, title: 'Some requirement-3', description: 'Sample description', state: :archived, author: user, created_at: 8.days.ago, updated_at: 2.days.ago) }
def create_requirement(title)
page.within('.nav-controls') do
......@@ -131,34 +131,51 @@ RSpec.describe 'Requirements list', :js do
end
end
it 'shows title and description along with edit button in drawer' do
find('.requirements-list li.requirement', match: :first).click
page.within('.requirement-form-drawer') do
expect(page.find('.title-container')).to have_content(requirement1.title)
expect(page.find('.title-container')).to have_selector('button.btn-edit')
expect(page.find('.description-container')).to have_content(requirement1.description)
end
end
it 'shows edit form when edit button is clicked for a requirement' do
page.within('.requirements-list li.requirement', match: :first) do
find('li.requirement-edit button[title="Edit"]').click
end
page.within('.requirement-form-drawer') do
expect(page.find('.gl-drawer-header span', match: :first)).to have_content("REQ-#{requirement1.iid}")
expect(page.find('textarea#requirementTitle')['value']).to have_content("#{requirement1.title}")
expect(page.find('textarea#requirementDescription')['value']).to have_content("#{requirement1.description}")
expect(page.find('input[type="checkbox"]')['checked']).to eq(requirement1.last_test_report_state)
expect(page.find('.js-requirement-save')).to have_content('Save changes')
end
end
it 'updates requirement using edit form' do
requirement_title = 'Foobar'
requirement_description = 'Baz'
page.within('.requirements-list li.requirement', match: :first) do
find('li.requirement-edit button[title="Edit"]').click
end
page.within('.requirement-form') do
page.within('.requirement-form-drawer') do
find('textarea#requirementTitle').native.send_keys requirement_title
find('button.js-requirement-save').click
find('textarea#requirementDescription').native.send_keys requirement_description
find('input[type="checkbox"]').click
click_button 'Save changes'
wait_for_all_requests
end
page.within('.requirements-list li.requirement', match: :first) do
expect(page.find('.issue-title-text')).to have_content(requirement_title)
end
end
it 'saves updated title for requirement using edit form' do
page.within('.requirements-list li.requirement', match: :first) do
find('li.requirement-edit button[title="Edit"]').click
end
page.within('.requirement-form') do
expect(page.find('span', match: :first)).to have_content("REQ-#{requirement1.iid}")
expect(page.find('textarea#requirementTitle')['value']).to have_content("#{requirement1.title}")
expect(page.find('.js-requirement-save')).to have_content('Save changes')
expect(page.find('.requirement-status-badge')).to have_content('satisfied')
end
end
......
......@@ -32,64 +32,6 @@ describe('RequirementItem', () => {
wrapperArchived.destroy();
});
describe('computed', () => {
describe('reference', () => {
it('returns string containing `requirement.iid` prefixed with "REQ-"', () => {
expect(wrapper.vm.reference).toBe(`REQ-${requirement1.iid}`);
});
});
describe('canUpdate', () => {
it('returns value of `requirement.userPermissions.updateRequirement`', () => {
expect(wrapper.vm.canUpdate).toBe(requirement1.userPermissions.updateRequirement);
});
});
describe('canArchive', () => {
it('returns value of `requirement.userPermissions.updateRequirement`', () => {
expect(wrapper.vm.canArchive).toBe(requirement1.userPermissions.adminRequirement);
});
});
describe('createdAt', () => {
it('returns timeago-style string representing `requirement.createdAt`', () => {
// We don't have to be accurate here as it is already covered in rspecs
expect(wrapper.vm.createdAt).toContain('created');
expect(wrapper.vm.createdAt).toContain('ago');
});
});
describe('updatedAt', () => {
it('returns timeago-style string representing `requirement.updatedAt`', () => {
// We don't have to be accurate here as it is already covered in rspecs
expect(wrapper.vm.updatedAt).toContain('updated');
expect(wrapper.vm.updatedAt).toContain('ago');
});
});
describe('isArchived', () => {
it('returns `true` when current requirement is archived', () => {
expect(wrapperArchived.vm.isArchived).toBe(true);
});
it('returns `false` when current requirement is archived', () => {
expect(wrapper.vm.isArchived).toBe(false);
});
});
describe('author', () => {
it('returns value of `requirement.author`', () => {
expect(wrapper.vm.author).toBe(requirement1.author);
});
});
describe('testReport', () => {
it('returns testReport object from reports array within `requirement`', () => {
expect(wrapper.vm.testReport).toBe(mockTestReport);
});
});
});
describe('methods', () => {
describe('handleArchiveClick', () => {
it('emits `archiveClick` event on component with object containing `requirement.iid` & `state` as "ARCHIVED" as param', () => {
......@@ -139,6 +81,13 @@ describe('RequirementItem', () => {
});
});
it('emits `show-click` event with requirement as param', () => {
wrapper.trigger('click');
expect(wrapper.emitted('show-click')).toBeTruthy();
expect(wrapper.emitted('show-click')[0]).toEqual([requirement1]);
});
it('renders element containing requirement reference', () => {
expect(wrapper.find('.issuable-reference').text()).toBe(`REQ-${requirement1.iid}`);
});
......@@ -186,6 +135,11 @@ describe('RequirementItem', () => {
expect(editButtonEl.exists()).toBe(true);
expect(editButtonEl.attributes('title')).toBe('Edit');
editButtonEl.vm.$emit('click');
expect(wrapper.emitted('edit-click')).toBeTruthy();
expect(wrapper.emitted('edit-click')[0]).toEqual([wrapper.vm.requirement]);
});
it('does not render element containing requirement `Edit` button when `requirement.userPermissions.updateRequirement` is false', () => {
......
import { shallowMount } from '@vue/test-utils';
import RequirementItem from 'ee/requirements/components/requirement_item.vue';
import { FilterState } from 'ee/requirements/constants';
import { mockAuthor, mockTestReport, requirement1 as mockRequirement } from '../mock_data';
const createComponent = (requirement = mockRequirement) =>
shallowMount(RequirementItem, {
propsData: {
requirement,
},
});
describe('RequirementMeta Mixin', () => {
let wrapper;
beforeEach(() => {
wrapper = createComponent();
});
afterEach(() => {
wrapper.destroy();
});
describe('computed', () => {
describe('reference', () => {
it('returns string containing `requirement.iid` prefixed with `REQ-`', () => {
expect(wrapper.vm.reference).toBe(`REQ-${mockRequirement.iid}`);
});
});
describe('titleHtml', () => {
it('returns value of `requirement.titleHtml`', () => {
expect(wrapper.vm.titleHtml).toBe(mockRequirement.titleHtml);
});
});
describe('descriptionHtml', () => {
it('returns value of `requirement.descriptionHtml`', () => {
expect(wrapper.vm.descriptionHtml).toBe(mockRequirement.descriptionHtml);
});
});
describe('isArchived', () => {
it('returns true when `requirement.state` is "ARCHIVED"', async () => {
wrapper.setProps({
requirement: {
...mockRequirement,
state: FilterState.archived,
},
});
await wrapper.vm.$nextTick();
expect(wrapper.vm.isArchived).toBe(true);
});
it('returns false when `requirement.state` is "OPENED"', () => {
expect(wrapper.vm.isArchived).toBe(false);
});
});
describe('author', () => {
it('returns value of `requirement.author`', () => {
expect(wrapper.vm.author).toBe(mockAuthor);
});
});
describe('createdAtFormatted', () => {
it('returns timeago-style string representing `requirement.createdAtFormatted`', () => {
// We don't have to be accurate here as it is already covered in rspecs
expect(wrapper.vm.createdAtFormatted).toContain('created');
expect(wrapper.vm.createdAtFormatted).toContain('ago');
});
});
describe('updatedAtFormatted', () => {
it('returns timeago-style string representing `requirement.updatedAtFormatted`', () => {
// We don't have to be accurate here as it is already covered in rspecs
expect(wrapper.vm.updatedAtFormatted).toContain('updated');
expect(wrapper.vm.updatedAtFormatted).toContain('ago');
});
});
describe('testReport', () => {
it('returns testReport object from reports array within `requirement`', () => {
expect(wrapper.vm.testReport).toBe(mockTestReport);
});
});
describe('canUpdate', () => {
it('returns value of `requirement.userPermissions.updateRequirement`', () => {
expect(wrapper.vm.canUpdate).toBe(mockRequirement.userPermissions.updateRequirement);
});
});
describe('canArchive', () => {
it('returns value of `requirement.userPermissions.updateRequirement`', () => {
expect(wrapper.vm.canArchive).toBe(mockRequirement.userPermissions.adminRequirement);
});
});
});
});
......@@ -34,6 +34,10 @@ export const mockTestReportMissing = {
export const requirement1 = {
iid: '1',
title: 'Virtutis, magnitudinis animi, patientiae, fortitudinis fomentis dolor mitigari solet.',
titleHtml:
'Virtutis, magnitudinis animi, patientiae, fortitudinis fomentis dolor mitigari solet.',
description: 'fortitudinis _fomentis_ dolor mitigari solet.',
descriptionHtml: 'fortitudinis <i>fomentis</i> dolor mitigari solet.',
createdAt: '2020-03-19T08:09:09Z',
updatedAt: '2020-03-20T08:09:09Z',
state: 'OPENED',
......@@ -50,6 +54,10 @@ export const requirement1 = {
export const requirement2 = {
iid: '2',
title: 'Est autem officium, quod ita factum est, ut eius facti probabilis ratio reddi possit.',
titleHtml:
'Est autem officium, quod ita factum est, ut eius facti probabilis ratio reddi possit.',
description: 'ut eius facti _probabilis_ ratio reddi possit.',
descriptionHtml: 'ut eius facti <i>probabilis</i> ratio reddi possit.',
createdAt: '2020-03-19T08:08:14Z',
updatedAt: '2020-03-20T08:08:14Z',
state: 'OPENED',
......@@ -66,6 +74,9 @@ export const requirement2 = {
export const requirement3 = {
iid: '3',
title: 'Non modo carum sibi quemque, verum etiam vehementer carum esse',
titleHtml: 'Non modo carum sibi quemque, verum etiam vehementer carum esse',
description: 'verum etiam _vehementer_ carum esse.',
descriptionHtml: 'verum etiam <i>vehementer</i> carum esse.',
createdAt: '2020-03-19T08:08:25Z',
updatedAt: '2020-03-20T08:08:25Z',
state: 'OPENED',
......@@ -82,6 +93,9 @@ export const requirement3 = {
export const requirementArchived = {
iid: '23',
title: 'Cuius quidem, quoniam Stoicus fuit',
titleHtml: 'Cuius quidem, quoniam Stoicus fuit',
description: 'quoniam _Stoicus_ fuit.',
descriptionHtml: 'quoniam <i>Stoicus</i> fuit.',
createdAt: '2020-03-31T13:31:40Z',
updatedAt: '2020-03-31T13:31:40Z',
state: 'ARCHIVED',
......
......@@ -22312,6 +22312,9 @@ msgstr ""
msgid "Requirement %{reference} has been updated"
msgstr ""
msgid "Requirement title"
msgstr ""
msgid "Requirement title cannot have more than %{limit} characters."
msgstr ""
......
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