Commit 831e5f4d authored by Eulyeon Ko's avatar Eulyeon Ko Committed by Nicolò Maria Mezzopera

Allow marking a requirement as (un)satisfied

1. Add a checkbox within the edit form to mark -
a requirement as satisfied or unsatisfied

2. Use "lastTestReportState" field in query and mutation.

Test the child components over parent props when testing watcher props.
parent 05ef2590
...@@ -40,13 +40,15 @@ list is sorted by creation date in descending order. ...@@ -40,13 +40,15 @@ list is sorted by creation date in descending order.
## Edit a requirement ## Edit a requirement
> - [Added](https://gitlab.com/gitlab-org/gitlab/-/issues/218607) ability to mark a requirement as Satisfied in [GitLab Ultimate](https://about.gitlab.com/pricing/) 13.5.
You can edit a requirement (if you have the necessary privileges) from the requirements You can edit a requirement (if you have the necessary privileges) from the requirements
list page. list page.
To edit a requirement: To edit a requirement:
1. From the requirements list, click **Edit** (**{pencil}**). 1. From the requirements list, click **Edit** (**{pencil}**).
1. Update the title in text input field. 1. Update the title in text input field. You can also mark (and unmark) a requirement as satisfied in the edit form by using the checkbox labeled "Satisfied".
1. Click **Save changes**. 1. Click **Save changes**.
## Archive a requirement ## Archive a requirement
...@@ -97,7 +99,7 @@ You can also sort the requirements list by: ...@@ -97,7 +99,7 @@ You can also sort the requirements list by:
GitLab supports [requirements test GitLab supports [requirements test
reports](../../../ci/pipelines/job_artifacts.md#artifactsreportsrequirements) now. reports](../../../ci/pipelines/job_artifacts.md#artifactsreportsrequirements) now.
You can add a job to your CI pipeline that, when triggered, marks all existing You can add a job to your CI pipeline that, when triggered, marks all existing
requirements as Satisfied. requirements as Satisfied (you may manually satisfy a requirement in the edit form [edit a requirement](#edit-a-requirement)).
### Add the manual job to CI ### Add the manual job to CI
......
<script> <script>
import { GlDrawer, GlFormGroup, GlFormTextarea, GlButton } from '@gitlab/ui'; import { GlDrawer, GlFormGroup, GlFormTextarea, GlFormCheckbox, GlButton } from '@gitlab/ui';
import { isEmpty } from 'lodash'; import { isEmpty } from 'lodash';
import { __, sprintf } from '~/locale'; import { __, sprintf } from '~/locale';
import { MAX_TITLE_LENGTH } from '../constants'; import { MAX_TITLE_LENGTH, TestReportStatus } from '../constants';
export default { export default {
titleInvalidMessage: sprintf(__('Requirement title cannot have more than %{limit} characters.'), { titleInvalidMessage: sprintf(__('Requirement title cannot have more than %{limit} characters.'), {
...@@ -13,6 +13,7 @@ export default { ...@@ -13,6 +13,7 @@ export default {
GlDrawer, GlDrawer,
GlFormGroup, GlFormGroup,
GlFormTextarea, GlFormTextarea,
GlFormCheckbox,
GlButton, GlButton,
}, },
props: { props: {
...@@ -33,6 +34,7 @@ export default { ...@@ -33,6 +34,7 @@ export default {
data() { data() {
return { return {
title: this.requirement?.title || '', title: this.requirement?.title || '',
satisfied: this.requirement?.satisfied || false,
}; };
}, },
computed: { computed: {
...@@ -59,13 +61,15 @@ export default { ...@@ -59,13 +61,15 @@ export default {
requirement: { requirement: {
handler(value) { handler(value) {
this.title = value?.title || ''; this.title = value?.title || '';
this.satisfied = value?.satisfied || false;
}, },
deep: true, deep: true,
}, },
drawerOpen(value) { drawerOpen(value) {
// Clear `title` value on drawer close. // Clear `title` and `satisfied` value on drawer close.
if (!value) { if (!value) {
this.title = ''; this.title = '';
this.satisfied = false;
} }
}, },
}, },
...@@ -79,6 +83,23 @@ export default { ...@@ -79,6 +83,23 @@ export default {
return ''; return '';
}, },
newLastTestReportState() {
// lastTestReportState determines whether a requirement is satisfied or not.
// Only create a new test report when manually marking/unmarking a requirement as satisfied:
// when 1) manually marking a requirement as satisfied for the first time.
const updateCondition1 = this.requirement.lastTestReportState === null && this.satisfied;
// or when 2) overriding the status in the latest test report.
const updateCondition2 =
this.requirement.lastTestReportState !== null &&
this.satisfied !== this.requirement.satisfied;
if (updateCondition1 || updateCondition2) {
return this.satisfied ? TestReportStatus.Passed : TestReportStatus.Failed;
}
return null;
},
handleSave() { handleSave() {
if (this.isCreate) { if (this.isCreate) {
this.$emit('save', this.title); this.$emit('save', this.title);
...@@ -86,6 +107,7 @@ export default { ...@@ -86,6 +107,7 @@ export default {
this.$emit('save', { this.$emit('save', {
iid: this.requirement.iid, iid: this.requirement.iid,
title: this.title, title: this.title,
lastTestReportState: this.newLastTestReportState(),
}); });
} }
}, },
...@@ -96,12 +118,12 @@ export default { ...@@ -96,12 +118,12 @@ export default {
<template> <template>
<gl-drawer :open="drawerOpen" :header-height="getDrawerHeaderHeight()" @close="$emit('cancel')"> <gl-drawer :open="drawerOpen" :header-height="getDrawerHeaderHeight()" @close="$emit('cancel')">
<template #header> <template #header>
<h4 class="m-0">{{ fieldLabel }}</h4> <h4 class="gl-m-0">{{ fieldLabel }}</h4>
</template> </template>
<template> <template>
<div class="requirement-form"> <div class="requirement-form">
<span v-if="!isCreate" class="text-muted">{{ reference }}</span> <span v-if="!isCreate" class="text-muted">{{ reference }}</span>
<div class="requirement-form-container" :class="{ 'flex-grow-1 mt-1': !isCreate }"> <div class="requirement-form-container" :class="{ 'gl-flex-grow-1 gl-mt-2': !isCreate }">
<gl-form-group <gl-form-group
:label="__('Title')" :label="__('Title')"
:invalid-feedback="$options.titleInvalidMessage" :invalid-feedback="$options.titleInvalidMessage"
...@@ -121,14 +143,17 @@ export default { ...@@ -121,14 +143,17 @@ export default {
:class="{ 'gl-field-error-outline': titleInvalid }" :class="{ 'gl-field-error-outline': titleInvalid }"
@keyup.escape.exact="$emit('cancel')" @keyup.escape.exact="$emit('cancel')"
/> />
<gl-form-checkbox v-if="!isCreate" v-model="satisfied" class="gl-mt-6">{{
__('Satisfied')
}}</gl-form-checkbox>
</gl-form-group> </gl-form-group>
<div class="d-flex requirement-form-actions"> <div class="gl-display-flex requirement-form-actions gl-mt-6">
<gl-button <gl-button
:disabled="disableSaveButton" :disabled="disableSaveButton"
:loading="requirementRequestActive" :loading="requirementRequestActive"
variant="success" variant="success"
category="primary" category="primary"
class="mr-auto js-requirement-save" class="gl-mr-auto js-requirement-save"
@click="handleSave" @click="handleSave"
> >
{{ saveButtonLabel }} {{ saveButtonLabel }}
......
...@@ -136,6 +136,7 @@ export default { ...@@ -136,6 +136,7 @@ export default {
<requirement-status-badge <requirement-status-badge
v-if="testReport" v-if="testReport"
:test-report="testReport" :test-report="testReport"
:last-test-report-manually-created="requirement.lastTestReportManuallyCreated"
class="d-block d-sm-none" class="d-block d-sm-none"
/> />
</div> </div>
...@@ -144,6 +145,7 @@ export default { ...@@ -144,6 +145,7 @@ export default {
<requirement-status-badge <requirement-status-badge
v-if="testReport" v-if="testReport"
:test-report="testReport" :test-report="testReport"
:last-test-report-manually-created="requirement.lastTestReportManuallyCreated"
element-type="li" element-type="li"
class="d-none d-sm-block" class="d-none d-sm-block"
/> />
......
...@@ -17,6 +17,10 @@ export default { ...@@ -17,6 +17,10 @@ export default {
type: Object, type: Object,
required: true, required: true,
}, },
lastTestReportManuallyCreated: {
type: Boolean,
required: true,
},
elementType: { elementType: {
type: String, type: String,
required: false, required: false,
...@@ -47,6 +51,15 @@ export default { ...@@ -47,6 +51,15 @@ export default {
tooltipTitle: '', tooltipTitle: '',
}; };
}, },
hideTestReportBadge() {
// User can manually indicate that a requirement has not been satisfied
// Internally, we create a test report with FAILED state with null build_id
// (this type of test report with null build id is said to be manually created).
// In this case, we do not show 'failed' badge.
return (
this.testReport.state === TestReportStatus.Failed && this.lastTestReportManuallyCreated
);
},
}, },
methods: { methods: {
getTestReportBadgeTarget() { getTestReportBadgeTarget() {
...@@ -57,7 +70,7 @@ export default { ...@@ -57,7 +70,7 @@ export default {
</script> </script>
<template> <template>
<component :is="elementType" class="requirement-status-badge"> <component :is="elementType" v-if="!hideTestReportBadge" class="requirement-status-badge">
<gl-badge ref="testReportBadge" :variant="testReportBadge.variant"> <gl-badge ref="testReportBadge" :variant="testReportBadge.variant">
<gl-icon :name="testReportBadge.icon" class="mr-1" /> <gl-icon :name="testReportBadge.icon" class="mr-1" />
{{ testReportBadge.text }} {{ testReportBadge.text }}
......
...@@ -22,7 +22,12 @@ import projectRequirementsCount from '../queries/projectRequirementsCount.query. ...@@ -22,7 +22,12 @@ import projectRequirementsCount from '../queries/projectRequirementsCount.query.
import createRequirement from '../queries/createRequirement.mutation.graphql'; import createRequirement from '../queries/createRequirement.mutation.graphql';
import updateRequirement from '../queries/updateRequirement.mutation.graphql'; import updateRequirement from '../queries/updateRequirement.mutation.graphql';
import { FilterState, AvailableSortOptions, DEFAULT_PAGE_SIZE } from '../constants'; import {
FilterState,
AvailableSortOptions,
TestReportStatus,
DEFAULT_PAGE_SIZE,
} from '../constants';
export default { export default {
DEFAULT_PAGE_SIZE, DEFAULT_PAGE_SIZE,
...@@ -136,8 +141,15 @@ export default { ...@@ -136,8 +141,15 @@ export default {
update(data) { update(data) {
const requirementsRoot = data.project?.requirements; const requirementsRoot = data.project?.requirements;
const list = requirementsRoot?.nodes.map(node => {
return {
...node,
satisfied: node.lastTestReportState === TestReportStatus.Passed,
};
});
return { return {
list: requirementsRoot?.nodes || [], list: list || [],
pageInfo: requirementsRoot?.pageInfo || {}, pageInfo: requirementsRoot?.pageInfo || {},
}; };
}, },
...@@ -325,7 +337,7 @@ export default { ...@@ -325,7 +337,7 @@ export default {
replace: true, replace: true,
}); });
}, },
updateRequirement({ iid, title, state, errorFlashMessage }) { updateRequirement({ iid, title, state, lastTestReportState, errorFlashMessage }) {
const updateRequirementInput = { const updateRequirementInput = {
projectPath: this.projectPath, projectPath: this.projectPath,
iid, iid,
...@@ -337,6 +349,9 @@ export default { ...@@ -337,6 +349,9 @@ export default {
if (state) { if (state) {
updateRequirementInput.state = state; updateRequirementInput.state = state;
} }
if (lastTestReportState) {
updateRequirementInput.lastTestReportState = lastTestReportState;
}
return this.$apollo return this.$apollo
.mutate({ .mutate({
......
...@@ -26,6 +26,8 @@ query projectRequirementsEE( ...@@ -26,6 +26,8 @@ query projectRequirementsEE(
createdAt createdAt
updatedAt updatedAt
state state
lastTestReportState
lastTestReportManuallyCreated
testReports(first: 1, sort: created_desc) { testReports(first: 1, sort: created_desc) {
nodes { nodes {
id id
......
...@@ -7,6 +7,14 @@ mutation updateRequirement($updateRequirementInput: UpdateRequirementInput!) { ...@@ -7,6 +7,14 @@ mutation updateRequirement($updateRequirementInput: UpdateRequirementInput!) {
title title
state state
updatedAt updatedAt
lastTestReportState
testReports(first: 1, sort: created_desc) {
nodes {
id
state
createdAt
}
}
} }
} }
} }
---
title: Provide ability to mark a requirement as Satisfied
merge_request: 43583
author:
type: added
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import { GlDrawer, GlFormGroup, GlFormTextarea } from '@gitlab/ui'; import { GlDrawer, GlFormCheckbox, GlFormGroup, GlFormTextarea } from '@gitlab/ui';
import RequirementForm from 'ee/requirements/components/requirement_form.vue'; import RequirementForm from 'ee/requirements/components/requirement_form.vue';
import { MAX_TITLE_LENGTH } from 'ee/requirements/constants'; import { TestReportStatus, MAX_TITLE_LENGTH } from 'ee/requirements/constants';
import { mockRequirementsOpen } from '../mock_data'; import { mockRequirementsOpen } from '../mock_data';
...@@ -19,6 +19,9 @@ const createComponent = ({ ...@@ -19,6 +19,9 @@ const createComponent = ({
}, },
}); });
const findGlFormTextArea = wrapper => wrapper.find(GlFormTextarea);
const findGlFormCheckbox = wrapper => wrapper.find(GlFormCheckbox);
describe('RequirementForm', () => { describe('RequirementForm', () => {
let wrapper; let wrapper;
let wrapperWithRequirement; let wrapperWithRequirement;
...@@ -94,24 +97,48 @@ describe('RequirementForm', () => { ...@@ -94,24 +97,48 @@ describe('RequirementForm', () => {
describe('watchers', () => { describe('watchers', () => {
describe('requirement', () => { describe('requirement', () => {
it('sets `title` to the value of `requirement.title` when requirement is not null', async () => { describe('when requirement is not null', () => {
wrapper.setProps({ it('renders the value of `requirement.title` as title', async () => {
requirement: mockRequirementsOpen[0], wrapper.setProps({
}); requirement: mockRequirementsOpen[0],
});
await wrapper.vm.$nextTick(); await wrapper.vm.$nextTick();
expect(findGlFormTextArea(wrapper).attributes('value')).toBe(
mockRequirementsOpen[0].title,
);
});
expect(wrapper.vm.title).toBe(mockRequirementsOpen[0].title); it.each`
requirement | satisfied
${mockRequirementsOpen[0]} | ${true}
${mockRequirementsOpen[1]} | ${false}
`(
`renders the satisfied checkbox according to the value of \`requirement.satisfied\`=$satisfied`,
async ({ requirement, satisfied }) => {
wrapper = createComponent();
wrapper.setProps({ requirement });
await wrapper.vm.$nextTick();
expect(findGlFormCheckbox(wrapper).vm.$attrs.checked).toBe(satisfied);
},
);
}); });
it('sets `title` to empty string when requirement is null', async () => { describe('when requirement is null', () => {
wrapperWithRequirement.setProps({ beforeEach(() => {
requirement: null, wrapperWithRequirement.setProps({
requirement: null,
});
}); });
await wrapperWithRequirement.vm.$nextTick(); it('renders empty string as title', async () => {
await wrapperWithRequirement.vm.$nextTick();
expect(wrapperWithRequirement.vm.title).toBe(''); expect(findGlFormTextArea(wrapperWithRequirement).attributes('value')).toBe('');
});
}); });
}); });
...@@ -133,6 +160,33 @@ describe('RequirementForm', () => { ...@@ -133,6 +160,33 @@ describe('RequirementForm', () => {
}); });
describe('methods', () => { describe('methods', () => {
describe.each`
lastTestReportState | requirement | newLastTestReportState
${TestReportStatus.Passed} | ${mockRequirementsOpen[0]} | ${TestReportStatus.Failed}
${TestReportStatus.Failed} | ${mockRequirementsOpen[1]} | ${TestReportStatus.Passed}
${'null'} | ${mockRequirementsOpen[2]} | ${TestReportStatus.Passed}
`('newLastTestReportState', ({ lastTestReportState, requirement, newLastTestReportState }) => {
describe(`when \`lastTestReportState\` is ${lastTestReportState}`, () => {
beforeEach(() => {
wrapperWithRequirement = createComponent({ requirement });
});
it("returns null when `satisfied` hasn't changed", () => {
expect(wrapperWithRequirement.vm.newLastTestReportState()).toBe(null);
});
it(`returns ${newLastTestReportState} when \`satisfied\` has changed from ${
requirement.satisfied
} to ${!requirement.satisfied}`, () => {
wrapperWithRequirement.setData({
satisfied: !requirement.satisfied,
});
expect(wrapperWithRequirement.vm.newLastTestReportState()).toBe(newLastTestReportState);
});
});
});
describe('handleSave', () => { describe('handleSave', () => {
it('emits `save` event on component with `title` as param when form is in create mode', () => { it('emits `save` event on component with `title` as param when form is in create mode', () => {
wrapper.setData({ wrapper.setData({
...@@ -147,7 +201,7 @@ describe('RequirementForm', () => { ...@@ -147,7 +201,7 @@ describe('RequirementForm', () => {
}); });
}); });
it('emits `save` event on component with object as param containing `iid` & `title` when form is in update mode', () => { it('emits `save` event on component with object as param containing `iid` & `title` & `lastTestReportState` when form is in update mode', () => {
wrapperWithRequirement.vm.handleSave(); wrapperWithRequirement.vm.handleSave();
return wrapperWithRequirement.vm.$nextTick(() => { return wrapperWithRequirement.vm.$nextTick(() => {
...@@ -156,6 +210,7 @@ describe('RequirementForm', () => { ...@@ -156,6 +210,7 @@ describe('RequirementForm', () => {
{ {
iid: mockRequirementsOpen[0].iid, iid: mockRequirementsOpen[0].iid,
title: mockRequirementsOpen[0].title, title: mockRequirementsOpen[0].title,
lastTestReportState: wrapperWithRequirement.vm.newLastTestReportState(),
}, },
]); ]);
}); });
...@@ -172,6 +227,14 @@ describe('RequirementForm', () => { ...@@ -172,6 +227,14 @@ describe('RequirementForm', () => {
expect(wrapperWithRequirement.find('span').text()).toBe(`REQ-${mockRequirementsOpen[0].iid}`); expect(wrapperWithRequirement.find('span').text()).toBe(`REQ-${mockRequirementsOpen[0].iid}`);
}); });
it('does not render gl-form-checkbox when form is in create mode', () => {
expect(findGlFormCheckbox(wrapper).exists()).toBe(false);
});
it('renders gl-form-checkbox when form is in edit mode', () => {
expect(findGlFormCheckbox(wrapperWithRequirement).exists()).toBe(true);
});
it('renders gl-form-group component', () => { it('renders gl-form-group component', () => {
const glFormGroup = wrapper.find(GlFormGroup); const glFormGroup = wrapper.find(GlFormGroup);
...@@ -185,7 +248,7 @@ describe('RequirementForm', () => { ...@@ -185,7 +248,7 @@ describe('RequirementForm', () => {
}); });
it('renders gl-form-textarea component', () => { it('renders gl-form-textarea component', () => {
const glFormTextarea = wrapper.find(GlFormTextarea); const glFormTextarea = findGlFormTextArea(wrapper);
expect(glFormTextarea.exists()).toBe(true); expect(glFormTextarea.exists()).toBe(true);
expect(glFormTextarea.attributes('id')).toBe('requirementTitle'); expect(glFormTextarea.attributes('id')).toBe('requirementTitle');
...@@ -194,7 +257,7 @@ describe('RequirementForm', () => { ...@@ -194,7 +257,7 @@ describe('RequirementForm', () => {
}); });
it('renders gl-form-textarea component populated with `requirement.title` when `requirement` prop is defined', () => { it('renders gl-form-textarea component populated with `requirement.title` when `requirement` prop is defined', () => {
expect(wrapperWithRequirement.find(GlFormTextarea).attributes('value')).toBe( expect(findGlFormTextArea(wrapperWithRequirement).attributes('value')).toBe(
mockRequirementsOpen[0].title, mockRequirementsOpen[0].title,
); );
}); });
......
...@@ -2,16 +2,36 @@ import { shallowMount } from '@vue/test-utils'; ...@@ -2,16 +2,36 @@ import { shallowMount } from '@vue/test-utils';
import { GlBadge, GlIcon, GlTooltip } from '@gitlab/ui'; import { GlBadge, GlIcon, GlTooltip } from '@gitlab/ui';
import RequirementStatusBadge from 'ee/requirements/components/requirement_status_badge.vue'; import RequirementStatusBadge from 'ee/requirements/components/requirement_status_badge.vue';
import { mockTestReport, mockTestReportFailed, mockTestReportMissing } from '../mock_data'; import { mockTestReport, mockTestReportFailed, mockTestReportMissing } from '../mock_data';
const createComponent = (testReport = mockTestReport) => const createComponent = ({
testReport = mockTestReport,
lastTestReportManuallyCreated = false,
} = {}) =>
shallowMount(RequirementStatusBadge, { shallowMount(RequirementStatusBadge, {
propsData: { propsData: {
testReport, testReport,
lastTestReportManuallyCreated,
}, },
}); });
const findGlBadge = wrapper => wrapper.find(GlBadge);
const findGlTooltip = wrapper => wrapper.find(GlTooltip);
const successBadgeProps = {
variant: 'success',
icon: 'status_success',
text: 'satisfied',
tooltipTitle: 'Passed on',
};
const failedBadgeProps = {
variant: 'danger',
icon: 'status_failed',
text: 'failed',
tooltipTitle: 'Failed on',
};
describe('RequirementStatusBadge', () => { describe('RequirementStatusBadge', () => {
let wrapper; let wrapper;
...@@ -26,12 +46,7 @@ describe('RequirementStatusBadge', () => { ...@@ -26,12 +46,7 @@ describe('RequirementStatusBadge', () => {
describe('computed', () => { describe('computed', () => {
describe('testReportBadge', () => { describe('testReportBadge', () => {
it('returns object containing variant, icon, text and tooltipTitle when status is "PASSED"', () => { it('returns object containing variant, icon, text and tooltipTitle when status is "PASSED"', () => {
expect(wrapper.vm.testReportBadge).toEqual({ expect(wrapper.vm.testReportBadge).toEqual(successBadgeProps);
variant: 'success',
icon: 'status_success',
text: 'satisfied',
tooltipTitle: 'Passed on',
});
}); });
it('returns object containing variant, icon, text and tooltipTitle when status is "FAILED"', () => { it('returns object containing variant, icon, text and tooltipTitle when status is "FAILED"', () => {
...@@ -40,12 +55,7 @@ describe('RequirementStatusBadge', () => { ...@@ -40,12 +55,7 @@ describe('RequirementStatusBadge', () => {
}); });
return wrapper.vm.$nextTick(() => { return wrapper.vm.$nextTick(() => {
expect(wrapper.vm.testReportBadge).toEqual({ expect(wrapper.vm.testReportBadge).toEqual(failedBadgeProps);
variant: 'danger',
icon: 'status_failed',
text: 'failed',
tooltipTitle: 'Failed on',
});
}); });
}); });
...@@ -67,22 +77,55 @@ describe('RequirementStatusBadge', () => { ...@@ -67,22 +77,55 @@ describe('RequirementStatusBadge', () => {
}); });
describe('template', () => { describe('template', () => {
it('renders GlBadge component', () => { describe.each`
const badgeEl = wrapper.find(GlBadge); testReport | badgeProps
${mockTestReport} | ${successBadgeProps}
expect(badgeEl.exists()).toBe(true); ${mockTestReportFailed} | ${failedBadgeProps}
expect(badgeEl.props('variant')).toBe('success'); `(`when the last test report's been automatically created`, ({ testReport, badgeProps }) => {
expect(badgeEl.text()).toBe('satisfied'); beforeEach(() => {
expect(badgeEl.find(GlIcon).exists()).toBe(true); wrapper = createComponent({
expect(badgeEl.find(GlIcon).props('name')).toBe('status_success'); testReport,
lastTestReportManuallyCreated: false,
});
});
describe(`when test report status is ${testReport.state}`, () => {
it(`renders GlBadge component`, () => {
const badgeEl = findGlBadge(wrapper);
expect(badgeEl.exists()).toBe(true);
expect(badgeEl.props('variant')).toBe(badgeProps.variant);
expect(badgeEl.text()).toBe(badgeProps.text);
expect(badgeEl.find(GlIcon).exists()).toBe(true);
expect(badgeEl.find(GlIcon).props('name')).toBe(badgeProps.icon);
});
it('renders GlTooltip component', () => {
const tooltipEl = findGlTooltip(wrapper);
expect(tooltipEl.exists()).toBe(true);
expect(tooltipEl.find('b').text()).toBe(badgeProps.tooltipTitle);
expect(tooltipEl.find('div').text()).toBe('Jun 4, 2020 10:55am GMT+0000');
});
});
}); });
it('renders GlTooltip component', () => { describe(`when the last test report's been manually created`, () => {
const tooltipEl = wrapper.find(GlTooltip); it('renders GlBadge component when status is "PASSED"', () => {
wrapper = createComponent({ lastTestReportManuallyCreated: true });
expect(tooltipEl.exists()).toBe(true); expect(findGlBadge(wrapper).exists()).toBe(true);
expect(tooltipEl.find('b').text()).toBe('Passed on'); expect(findGlBadge(wrapper).text()).toBe('satisfied');
expect(tooltipEl.find('div').text()).toBe('Jun 4, 2020 10:55am GMT+0000'); });
it('does not render GlBadge component when status is "FAILED"', () => {
wrapper = createComponent({
testReport: mockTestReportFailed,
lastTestReportManuallyCreated: true,
});
expect(findGlBadge(wrapper).exists()).toBe(false);
});
}); });
}); });
}); });
...@@ -351,6 +351,51 @@ describe('RequirementsRoot', () => { ...@@ -351,6 +351,51 @@ describe('RequirementsRoot', () => {
); );
}); });
describe('when `lastTestReportState` is included in object param', () => {
beforeEach(() => {
jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(mockUpdateMutationResult);
});
it('calls `$apollo.mutate` with `lastTestReportState` when it is not null', () => {
wrapper.vm.updateRequirement({
iid: '1',
lastTestReportState: 'PASSED',
});
expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith(
expect.objectContaining({
mutation: updateRequirement,
variables: {
updateRequirementInput: {
projectPath: 'gitlab-org/gitlab-shell',
iid: '1',
lastTestReportState: 'PASSED',
},
},
}),
);
});
it('calls `$apollo.mutate` without `lastTestReportState` when it is null', () => {
wrapper.vm.updateRequirement({
iid: '1',
lastTestReportState: null,
});
expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith(
expect.objectContaining({
mutation: updateRequirement,
variables: {
updateRequirementInput: {
projectPath: 'gitlab-org/gitlab-shell',
iid: '1',
},
},
}),
);
});
});
it('calls `createFlash` with provided `errorFlashMessage` param and `Sentry.captureException` when request fails', () => { it('calls `createFlash` with provided `errorFlashMessage` param and `Sentry.captureException` when request fails', () => {
jest.spyOn(wrapper.vm.$apollo, 'mutate').mockRejectedValue(new Error()); jest.spyOn(wrapper.vm.$apollo, 'mutate').mockRejectedValue(new Error());
jest.spyOn(Sentry, 'captureException').mockImplementation(); jest.spyOn(Sentry, 'captureException').mockImplementation();
......
...@@ -39,6 +39,9 @@ export const requirement1 = { ...@@ -39,6 +39,9 @@ export const requirement1 = {
state: 'OPENED', state: 'OPENED',
userPermissions: mockUserPermissions, userPermissions: mockUserPermissions,
author: mockAuthor, author: mockAuthor,
lastTestReportState: 'PASSED',
lastTestReportManuallyCreated: false,
satisfied: true,
testReports: { testReports: {
nodes: [mockTestReport], nodes: [mockTestReport],
}, },
...@@ -52,6 +55,9 @@ export const requirement2 = { ...@@ -52,6 +55,9 @@ export const requirement2 = {
state: 'OPENED', state: 'OPENED',
userPermissions: mockUserPermissions, userPermissions: mockUserPermissions,
author: mockAuthor, author: mockAuthor,
lastTestReportState: 'FAILED',
lastTestReportManuallyCreated: true,
satisfied: false,
testReports: { testReports: {
nodes: [mockTestReport], nodes: [mockTestReport],
}, },
...@@ -65,6 +71,9 @@ export const requirement3 = { ...@@ -65,6 +71,9 @@ export const requirement3 = {
state: 'OPENED', state: 'OPENED',
userPermissions: mockUserPermissions, userPermissions: mockUserPermissions,
author: mockAuthor, author: mockAuthor,
lastTestReportState: null,
lastTestReportManuallyCreated: true,
satisfied: false,
testReports: { testReports: {
nodes: [mockTestReport], nodes: [mockTestReport],
}, },
...@@ -78,6 +87,9 @@ export const requirementArchived = { ...@@ -78,6 +87,9 @@ export const requirementArchived = {
state: 'ARCHIVED', state: 'ARCHIVED',
userPermissions: mockUserPermissions, userPermissions: mockUserPermissions,
author: mockAuthor, author: mockAuthor,
lastTestReportState: null,
lastTestReportManuallyCreated: true,
satisfied: false,
testReports: { testReports: {
nodes: [mockTestReport], nodes: [mockTestReport],
}, },
......
...@@ -22606,6 +22606,9 @@ msgstr "" ...@@ -22606,6 +22606,9 @@ msgstr ""
msgid "SSL Verification:" msgid "SSL Verification:"
msgstr "" msgstr ""
msgid "Satisfied"
msgstr ""
msgid "Saturday" msgid "Saturday"
msgstr "" 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