Commit 249d54e4 authored by Martin Wortschack's avatar Martin Wortschack

Merge branch '207981-refine-empty-state-and-loading-animations' into 'master'

[Part-4] Refine empty state & loading animation

Closes #207981

See merge request gitlab-org/gitlab!29320
parents ccbf3cf6 2fc9a630
<script> <script>
import { GlEmptyState } from '@gitlab/ui'; import { GlEmptyState, GlDeprecatedButton } from '@gitlab/ui';
import { __ } from '~/locale';
import { FilterStateEmptyMessage } from '../constants'; import { FilterState, FilterStateEmptyMessage } from '../constants';
export default { export default {
components: { components: {
GlEmptyState, GlEmptyState,
GlDeprecatedButton,
}, },
props: { props: {
filterBy: { filterBy: {
...@@ -16,10 +18,24 @@ export default { ...@@ -16,10 +18,24 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
requirementsCount: {
type: Object,
required: true,
},
}, },
computed: { computed: {
emptyStateTitle() { emptyStateTitle() {
return FilterStateEmptyMessage[this.filterBy]; return this.requirementsCount[FilterState.all]
? FilterStateEmptyMessage[this.filterBy]
: __('Requirements allow you to create criteria to check your products against.');
},
emptyStateDescription() {
return !this.requirementsCount[FilterState.all]
? __(
`Requirements can be based on users, stakeholders, system, software
or anything else you find important to capture.`,
)
: null;
}, },
}, },
}; };
...@@ -27,6 +43,19 @@ export default { ...@@ -27,6 +43,19 @@ export default {
<template> <template>
<div class="requirements-empty-state-container"> <div class="requirements-empty-state-container">
<gl-empty-state :title="emptyStateTitle" :svg-path="emptyStatePath" /> <gl-empty-state
:svg-path="emptyStatePath"
:title="emptyStateTitle"
:description="emptyStateDescription"
>
<template v-if="emptyStateDescription" #actions>
<gl-deprecated-button
category="primary"
variant="success"
@click="$emit('clickNewRequirement')"
>{{ __('New requirement') }}</gl-deprecated-button
>
</template>
</gl-empty-state>
</div> </div>
</template> </template>
<script> <script>
import { GlSkeletonLoading } from '@gitlab/ui'; import { GlSkeletonLoading, GlLoadingIcon } from '@gitlab/ui';
import { DEFAULT_PAGE_SIZE } from '../constants'; import { DEFAULT_PAGE_SIZE, FilterState } from '../constants';
export default { export default {
components: { components: {
GlSkeletonLoading, GlSkeletonLoading,
GlLoadingIcon,
}, },
props: { props: {
filterBy: { filterBy: {
type: String, type: String,
required: true, required: true,
}, },
currentTabCount: { currentPage: {
type: Number, type: Number,
required: true, required: true,
}, },
currentPage: { requirementsCount: {
type: Number, type: Object,
required: true, required: true,
}, },
}, },
computed: { computed: {
currentTabCount() {
return this.requirementsCount[this.filterBy];
},
totalRequirements() {
return this.requirementsCount[FilterState.all];
},
lastPage() { lastPage() {
return Math.ceil(this.currentTabCount / DEFAULT_PAGE_SIZE); return Math.ceil(this.currentTabCount / DEFAULT_PAGE_SIZE);
}, },
...@@ -36,9 +43,13 @@ export default { ...@@ -36,9 +43,13 @@ export default {
</script> </script>
<template> <template>
<ul class="content-list issuable-list issues-list requirements-list-loading"> <ul
v-if="totalRequirements && currentTabCount"
class="content-list issuable-list issues-list requirements-list-loading"
>
<li v-for="(i, index) in Array(loaderCount).fill()" :key="index" class="issue requirement"> <li v-for="(i, index) in Array(loaderCount).fill()" :key="index" class="issue requirement">
<gl-skeleton-loading :lines="2" class="pt-2" /> <gl-skeleton-loading :lines="2" class="pt-2" />
</li> </li>
</ul> </ul>
<gl-loading-icon v-else size="md" class="mt-3" />
</template> </template>
...@@ -4,7 +4,7 @@ import { GlPagination } from '@gitlab/ui'; ...@@ -4,7 +4,7 @@ import { GlPagination } from '@gitlab/ui';
import { __ } from '~/locale'; import { __ } from '~/locale';
import createFlash from '~/flash'; import createFlash from '~/flash';
import { urlParamsToObject } from '~/lib/utils/common_utils'; import { urlParamsToObject } from '~/lib/utils/common_utils';
import { updateHistory, setUrlParams } from '~/lib/utils/url_utility'; import { updateHistory, setUrlParams, visitUrl } from '~/lib/utils/url_utility';
import RequirementsLoading from './requirements_loading.vue'; import RequirementsLoading from './requirements_loading.vue';
import RequirementsEmptyState from './requirements_empty_state.vue'; import RequirementsEmptyState from './requirements_empty_state.vue';
...@@ -38,7 +38,8 @@ export default { ...@@ -38,7 +38,8 @@ export default {
requirementsCount: { requirementsCount: {
type: Object, type: Object,
required: true, required: true,
validator: value => ['OPENED', 'ARCHIVED', 'ALL'].every(prop => value[prop]), validator: value =>
['OPENED', 'ARCHIVED', 'ALL'].every(prop => typeof value[prop] === 'number'),
}, },
page: { page: {
type: Number, type: Number,
...@@ -59,6 +60,10 @@ export default { ...@@ -59,6 +60,10 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
requirementsWebUrl: {
type: String,
required: true,
},
}, },
apollo: { apollo: {
requirements: { requirements: {
...@@ -108,6 +113,7 @@ export default { ...@@ -108,6 +113,7 @@ export default {
const tabsContainerEl = document.querySelector('.js-requirements-state-filters'); const tabsContainerEl = document.querySelector('.js-requirements-state-filters');
return { return {
newRequirementEl: null,
showCreateForm: false, showCreateForm: false,
showUpdateFormForRequirement: 0, showUpdateFormForRequirement: 0,
createRequirementRequestActive: false, createRequirementRequestActive: false,
...@@ -158,6 +164,11 @@ export default { ...@@ -158,6 +164,11 @@ export default {
}, },
}, },
watch: { watch: {
showCreateForm(value) {
this.enableOrDisableNewRequirement({
disable: value,
});
},
requirements() { requirements() {
const totalCount = this.requirements.count.ALL; const totalCount = this.requirements.count.ALL;
...@@ -174,16 +185,14 @@ export default { ...@@ -174,16 +185,14 @@ export default {
}, },
mounted() { mounted() {
if (this.filterBy === FilterState.opened) { if (this.filterBy === FilterState.opened) {
document this.newRequirementEl = document.querySelector('.js-new-requirement');
.querySelector('.js-new-requirement')
.addEventListener('click', this.handleNewRequirementClick); this.newRequirementEl.addEventListener('click', this.handleNewRequirementClick);
} }
}, },
beforeDestroy() { beforeDestroy() {
if (this.filterBy === FilterState.opened) { if (this.filterBy === FilterState.opened) {
document this.newRequirementEl.removeEventListener('click', this.handleNewRequirementClick);
.querySelector('.js-new-requirement')
.removeEventListener('click', this.handleNewRequirementClick);
} }
}, },
methods: { methods: {
...@@ -241,6 +250,22 @@ export default { ...@@ -241,6 +250,22 @@ export default {
Sentry.captureException(e); Sentry.captureException(e);
}); });
}, },
/**
* This method is only needed until we move Requirements page
* tabs and button into this Vue app instead of rendering it
* using HAML.
*/
enableOrDisableNewRequirement({ disable = true }) {
if (this.newRequirementEl) {
if (disable) {
this.newRequirementEl.setAttribute('disabled', 'disabled');
this.newRequirementEl.classList.add('disabled');
} else {
this.newRequirementEl.removeAttribute('disabled');
this.newRequirementEl.classList.remove('disabled');
}
}
},
handleNewRequirementClick() { handleNewRequirementClick() {
this.showCreateForm = true; this.showCreateForm = true;
}, },
...@@ -248,6 +273,7 @@ export default { ...@@ -248,6 +273,7 @@ export default {
this.showUpdateFormForRequirement = iid; this.showUpdateFormForRequirement = iid;
}, },
handleNewRequirementSave(title) { handleNewRequirementSave(title) {
const reloadPage = this.totalRequirements === 0;
this.createRequirementRequestActive = true; this.createRequirementRequestActive = true;
return this.$apollo return this.$apollo
.mutate({ .mutate({
...@@ -261,9 +287,13 @@ export default { ...@@ -261,9 +287,13 @@ export default {
}) })
.then(({ data }) => { .then(({ data }) => {
if (!data.createRequirement.errors.length) { if (!data.createRequirement.errors.length) {
this.showCreateForm = false; if (reloadPage) {
this.$apollo.queries.requirements.refetch(); visitUrl(this.requirementsWebUrl);
this.openedCount += 1; } else {
this.showCreateForm = false;
this.$apollo.queries.requirements.refetch();
this.openedCount += 1;
}
} else { } else {
throw new Error(`Error creating a requirement`); throw new Error(`Error creating a requirement`);
} }
...@@ -348,22 +378,24 @@ export default { ...@@ -348,22 +378,24 @@ export default {
<template> <template>
<div class="requirements-list-container"> <div class="requirements-list-container">
<requirement-form
v-if="showCreateForm"
:requirement-request-active="createRequirementRequestActive"
@save="handleNewRequirementSave"
@cancel="handleNewRequirementCancel"
/>
<requirements-empty-state <requirements-empty-state
v-if="requirementsListEmpty" v-if="requirementsListEmpty && !showCreateForm"
:filter-by="filterBy" :filter-by="filterBy"
:empty-state-path="emptyStatePath" :empty-state-path="emptyStatePath"
:requirements-count="requirementsCount"
@clickNewRequirement="handleNewRequirementClick"
/> />
<requirements-loading <requirements-loading
v-show="requirementsListLoading" v-show="requirementsListLoading"
:filter-by="filterBy" :filter-by="filterBy"
:current-tab-count="totalRequirements"
:current-page="currentPage" :current-page="currentPage"
/> :requirements-count="requirementsCount"
<requirement-form
v-if="showCreateForm"
:requirement-request-active="createRequirementRequestActive"
@save="handleNewRequirementSave"
@cancel="handleNewRequirementCancel"
/> />
<ul <ul
v-if="!requirementsListLoading && !requirementsListEmpty" v-if="!requirementsListLoading && !requirementsListEmpty"
......
...@@ -45,24 +45,28 @@ export default () => { ...@@ -45,24 +45,28 @@ export default () => {
emptyStatePath, emptyStatePath,
opened, opened,
archived, archived,
all,
requirementsWebUrl,
} = el.dataset; } = el.dataset;
const stateFilterBy = filterBy ? FilterState[filterBy] : FilterState.opened; const stateFilterBy = filterBy ? FilterState[filterBy] : FilterState.opened;
const OPENED = parseInt(opened, 10); const OPENED = parseInt(opened, 10);
const ARCHIVED = parseInt(archived, 10); const ARCHIVED = parseInt(archived, 10);
const ALL = parseInt(all, 10);
return { return {
filterBy: stateFilterBy, filterBy: stateFilterBy,
requirementsCount: { requirementsCount: {
OPENED, OPENED,
ARCHIVED, ARCHIVED,
ALL: OPENED + ARCHIVED, ALL,
}, },
page, page,
prev, prev,
next, next,
emptyStatePath, emptyStatePath,
projectPath, projectPath,
requirementsWebUrl,
}; };
}, },
render(createElement) { render(createElement) {
...@@ -75,6 +79,7 @@ export default () => { ...@@ -75,6 +79,7 @@ export default () => {
prev: this.prev, prev: this.prev,
next: this.next, next: this.next,
emptyStatePath: this.emptyStatePath, emptyStatePath: this.emptyStatePath,
requirementsWebUrl: this.requirementsWebUrl,
}, },
}); });
}, },
......
...@@ -3,10 +3,22 @@ ...@@ -3,10 +3,22 @@
- page_context_word = type.to_s.humanize(capitalize: false) - page_context_word = type.to_s.humanize(capitalize: false)
- @content_class = 'requirements-container' - @content_class = 'requirements-container'
-# We'd prefer to have following declarations be part of
-# helpers in some way but given that they're very frontend-centeric,
-# keeping them in HAML view makes more sense.
- page_size = 20
- ignore_page_params = ['next', 'prev', 'page'] - ignore_page_params = ['next', 'prev', 'page']
- requirements_count = Hash.new(0).merge(@project.requirements.counts_by_state) - requirements_count = Hash.new(0).merge(@project.requirements.counts_by_state)
- total_requirements = requirements_count['opened'] + requirements_count['archived']
- is_open_tab = params[:state].nil? || params[:state] == 'opened' - is_open_tab = params[:state].nil? || params[:state] == 'opened'
- if is_open_tab
- current_tab_count = requirements_count['opened'] > page_size ? page_size : requirements_count['opened']
- elsif params[:state] == 'archived'
- current_tab_count = requirements_count['archived'] > page_size ? page_size : requirements_count['archived']
- else
- current_tab_count = total_requirements > page_size ? page_size : total_requirements
.top-area .top-area
%ul.nav-links.mobile-separator.requirements-state-filters.js-requirements-state-filters %ul.nav-links.mobile-separator.requirements-state-filters.js-requirements-state-filters
%li{ class: active_when(is_open_tab) }> %li{ class: active_when(is_open_tab) }>
...@@ -22,7 +34,7 @@ ...@@ -22,7 +34,7 @@
%li{ class: active_when(params[:state] == 'all') }> %li{ class: active_when(params[:state] == 'all') }>
= link_to page_filter_path(state: 'all', without: ignore_page_params), id: 'state-all', title: (_("Show all %{issuable_type}.") % { issuable_type: page_context_word }), data: { state: 'all' } do = link_to page_filter_path(state: 'all', without: ignore_page_params), id: 'state-all', title: (_("Show all %{issuable_type}.") % { issuable_type: page_context_word }), data: { state: 'all' } do
= _('All') = _('All')
%span.badge.badge-pill.js-all-count= requirements_count['opened'] + requirements_count['archived'] %span.badge.badge-pill.js-all-count= total_requirements
.nav-controls .nav-controls
- if is_open_tab - if is_open_tab
...@@ -36,6 +48,21 @@ ...@@ -36,6 +48,21 @@
project_path: @project.full_path, project_path: @project.full_path,
opened: requirements_count['opened'], opened: requirements_count['opened'],
archived: requirements_count['archived'], archived: requirements_count['archived'],
all: total_requirements,
requirements_web_url: project_requirements_path(@project),
empty_state_path: image_path('illustrations/empty-state/empty-requirements-lg.svg') } } empty_state_path: image_path('illustrations/empty-state/empty-requirements-lg.svg') } }
.gl-spinner-container.mt-3 - if current_tab_count == 0
%span.align-text-bottom.gl-spinner.gl-spinner-orange.gl-spinner-md{ aria: { label: _('Loading'), hidden: 'true' } } -# Show regular spinner only when there will be no
-# requirements to show for current tab.
.gl-spinner-container.mt-3
%span.align-text-bottom.gl-spinner.gl-spinner-orange.gl-spinner-md{ aria: { label: _('Loading'), hidden: 'true' } }
- else
-# Following block shows skeleton loading same as mounted Vue app so while
-# app is being loaded and initialized, user continues to see skeleton loading.
.requirements-list-container
%ul.content-list.issuable-list.issues-list.requirements-list-loading
- Array.new(current_tab_count).each do |i|
%li.issue.requirement
.animation-container.pt-2
.skeleton-line-1
.skeleton-line-2
...@@ -69,6 +69,13 @@ describe 'Requirements list', :js do ...@@ -69,6 +69,13 @@ describe 'Requirements list', :js do
end end
end end
it 'disables new requirement button while create form is open' do
page.within('.nav-controls') do
find('button.js-new-requirement').click
expect(find('button.js-new-requirement')[:disabled]).to eq "true"
end
end
it 'creates new requirement' do it 'creates new requirement' do
requirement_title = 'Foobar' requirement_title = 'Foobar'
......
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import { GlEmptyState } from '@gitlab/ui'; import { GlEmptyState, GlDeprecatedButton } from '@gitlab/ui';
import RequirementsEmptyState from 'ee/requirements/components/requirements_empty_state.vue'; import RequirementsEmptyState from 'ee/requirements/components/requirements_empty_state.vue';
import { FilterState } from 'ee/requirements/constants'; import { FilterState } from 'ee/requirements/constants';
...@@ -12,7 +12,13 @@ const createComponent = ( ...@@ -12,7 +12,13 @@ const createComponent = (
propsData: { propsData: {
filterBy, filterBy,
emptyStatePath, emptyStatePath,
requirementsCount: {
OPENED: 0,
ARCHIVED: 0,
ALL: 0,
},
}, },
stubs: { GlEmptyState },
}); });
describe('RequirementsEmptyState', () => { describe('RequirementsEmptyState', () => {
...@@ -28,31 +34,99 @@ describe('RequirementsEmptyState', () => { ...@@ -28,31 +34,99 @@ describe('RequirementsEmptyState', () => {
describe('computed', () => { describe('computed', () => {
describe('emptyStateTitle', () => { describe('emptyStateTitle', () => {
it('returns string "There are no open requirements" when value of `filterBy` prop is "OPENED"', () => { it('returns string "There are no open requirements" when value of `filterBy` prop is "OPENED" and project has some requirements', () => {
expect(wrapper.vm.emptyStateTitle).toBe('There are no open requirements'); wrapper.setProps({
requirementsCount: {
OPENED: 0,
ARCHIVED: 2,
ALL: 2,
},
});
return wrapper.vm.$nextTick(() => {
expect(wrapper.vm.emptyStateTitle).toBe('There are no open requirements');
});
}); });
it('returns string "There are no archived requirements" when value of `filterBy` prop is "ARCHIVED"', () => { it('returns string "There are no archived requirements" when value of `filterBy` prop is "ARCHIVED" and project has some requirements', () => {
wrapper.setProps({ wrapper.setProps({
filterBy: FilterState.archived, filterBy: FilterState.archived,
requirementsCount: {
OPENED: 2,
ARCHIVED: 0,
ALL: 2,
},
}); });
return wrapper.vm.$nextTick(() => { return wrapper.vm.$nextTick(() => {
expect(wrapper.vm.emptyStateTitle).toBe('There are no archived requirements'); expect(wrapper.vm.emptyStateTitle).toBe('There are no archived requirements');
}); });
}); });
it('returns a generic string when project has no requirements', () => {
expect(wrapper.vm.emptyStateTitle).toBe(
'Requirements allow you to create criteria to check your products against.',
);
});
});
describe('emptyStateDescription', () => {
it('returns a generic string when project has no requirements', () => {
expect(wrapper.vm.emptyStateDescription).toBe(
'Requirements can be based on users, stakeholders, system, software or anything else you find important to capture.',
);
});
it('returns a null when project has some requirements', () => {
wrapper.setProps({
requirementsCount: {
OPENED: 2,
ARCHIVED: 0,
ALL: 2,
},
});
return wrapper.vm.$nextTick(() => {
expect(wrapper.vm.emptyStateDescription).toBeNull();
});
});
}); });
}); });
describe('template', () => { describe('template', () => {
it('renders empty state element', () => { it('renders empty state element', () => {
const emptyStateEl = wrapper.find(GlEmptyState); const emptyStateEl = wrapper.find('.empty-state .svg-content img');
expect(emptyStateEl.exists()).toBe(true); expect(emptyStateEl.exists()).toBe(true);
expect(emptyStateEl.props('title')).toBe('There are no open requirements'); expect(emptyStateEl.attributes('alt')).toBe(
expect(emptyStateEl.attributes('svgpath')).toBe( 'Requirements allow you to create criteria to check your products against.',
);
expect(emptyStateEl.attributes('src')).toBe(
'/assets/illustrations/empty-state/requirements.svg', '/assets/illustrations/empty-state/requirements.svg',
); );
}); });
it('renders new requirement button when project has no requirements', () => {
const newReqButton = wrapper.find(GlDeprecatedButton);
expect(newReqButton.exists()).toBe(true);
expect(newReqButton.text()).toBe('New requirement');
});
it('does not render new requirement button when project some requirements', () => {
wrapper.setProps({
requirementsCount: {
OPENED: 2,
ARCHIVED: 0,
ALL: 2,
},
});
return wrapper.vm.$nextTick(() => {
const newReqButton = wrapper.find(GlDeprecatedButton);
expect(newReqButton.exists()).toBe(false);
});
});
}); });
}); });
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import { GlSkeletonLoading } from '@gitlab/ui'; import { GlSkeletonLoading, GlLoadingIcon } from '@gitlab/ui';
import RequirementsLoading from 'ee/requirements/components/requirements_loading.vue'; import RequirementsLoading from 'ee/requirements/components/requirements_loading.vue';
import { FilterState, mockRequirementsCount } from '../mock_data'; import { FilterState, mockRequirementsCount } from '../mock_data';
jest.mock('ee/requirements/constants', () => ({ jest.mock('ee/requirements/constants', () => ({
DEFAULT_PAGE_SIZE: 2, DEFAULT_PAGE_SIZE: 2,
FilterState: {
opened: 'OPENED',
archived: 'ARCHIVED',
all: 'ALL',
},
})); }));
const createComponent = ({ const createComponent = ({
filterBy = FilterState.opened, filterBy = FilterState.opened,
currentTabCount = mockRequirementsCount.OPENED, requirementsCount = mockRequirementsCount,
currentPage = 1, currentPage = 1,
} = {}) => } = {}) =>
shallowMount(RequirementsLoading, { shallowMount(RequirementsLoading, {
propsData: { propsData: {
filterBy, filterBy,
currentTabCount,
currentPage, currentPage,
requirementsCount,
}, },
}); });
...@@ -58,7 +63,11 @@ describe('RequirementsLoading', () => { ...@@ -58,7 +63,11 @@ describe('RequirementsLoading', () => {
it('returns value DEFAULT_PAGE_SIZE when current page is the last page total requirements are less than DEFAULT_PAGE_SIZE', () => { it('returns value DEFAULT_PAGE_SIZE when current page is the last page total requirements are less than DEFAULT_PAGE_SIZE', () => {
wrapper.setProps({ wrapper.setProps({
currentPage: 1, currentPage: 1,
currentTabCount: 1, requirementsCount: {
OPENED: 1,
ARCHIVED: 0,
ALL: 2,
},
}); });
return wrapper.vm.$nextTick(() => { return wrapper.vm.$nextTick(() => {
...@@ -69,11 +78,26 @@ describe('RequirementsLoading', () => { ...@@ -69,11 +78,26 @@ describe('RequirementsLoading', () => {
}); });
describe('template', () => { describe('template', () => {
it('renders gl-skeleton-loading component based on loaderCount', () => { it('renders gl-skeleton-loading component project has some requirements and current tab has requirements to show', () => {
const loaders = wrapper.find('.requirements-list-loading').findAll(GlSkeletonLoading); const loaders = wrapper.find('.requirements-list-loading').findAll(GlSkeletonLoading);
expect(loaders.length).toBe(2); expect(loaders.length).toBe(2);
expect(loaders.at(0).props('lines')).toBe(2); expect(loaders.at(0).props('lines')).toBe(2);
}); });
it('renders gl-loading-icon component project has no requirements and current tab has nothing to show', () => {
wrapper.setProps({
requirementsCount: {
OPENED: 0,
ARCHIVED: 0,
ALL: 0,
},
});
return wrapper.vm.$nextTick(() => {
expect(wrapper.find('.requirements-list-loading').exists()).toBe(false);
expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
});
});
}); });
}); });
...@@ -3,6 +3,7 @@ import { shallowMount } from '@vue/test-utils'; ...@@ -3,6 +3,7 @@ import { shallowMount } from '@vue/test-utils';
import { GlPagination } from '@gitlab/ui'; import { GlPagination } from '@gitlab/ui';
import * as Sentry from '@sentry/browser'; import * as Sentry from '@sentry/browser';
import createFlash from '~/flash'; import createFlash from '~/flash';
import { visitUrl } from '~/lib/utils/url_utility';
import RequirementsRoot from 'ee/requirements/components/requirements_root.vue'; import RequirementsRoot from 'ee/requirements/components/requirements_root.vue';
import RequirementsLoading from 'ee/requirements/components/requirements_loading.vue'; import RequirementsLoading from 'ee/requirements/components/requirements_loading.vue';
...@@ -30,6 +31,10 @@ jest.mock('ee/requirements/constants', () => ({ ...@@ -30,6 +31,10 @@ jest.mock('ee/requirements/constants', () => ({
})); }));
jest.mock('~/flash'); jest.mock('~/flash');
jest.mock('~/lib/utils/url_utility', () => ({
...jest.requireActual('~/lib/utils/url_utility'),
visitUrl: jest.fn(),
}));
const createComponent = ({ const createComponent = ({
projectPath = 'gitlab-org/gitlab-shell', projectPath = 'gitlab-org/gitlab-shell',
...@@ -38,6 +43,7 @@ const createComponent = ({ ...@@ -38,6 +43,7 @@ const createComponent = ({
showCreateRequirement = false, showCreateRequirement = false,
emptyStatePath = '/assets/illustrations/empty-state/requirements.svg', emptyStatePath = '/assets/illustrations/empty-state/requirements.svg',
loading = false, loading = false,
requirementsWebUrl = '/gitlab-org/gitlab-shell/-/requirements',
} = {}) => } = {}) =>
shallowMount(RequirementsRoot, { shallowMount(RequirementsRoot, {
propsData: { propsData: {
...@@ -46,6 +52,7 @@ const createComponent = ({ ...@@ -46,6 +52,7 @@ const createComponent = ({
requirementsCount, requirementsCount,
showCreateRequirement, showCreateRequirement,
emptyStatePath, emptyStatePath,
requirementsWebUrl,
}, },
mocks: { mocks: {
$apollo: { $apollo: {
...@@ -267,6 +274,34 @@ describe('RequirementsRoot', () => { ...@@ -267,6 +274,34 @@ describe('RequirementsRoot', () => {
}); });
}); });
describe('enableOrDisableNewRequirement', () => {
it('disables new requirement button when called with param `{ disable: true }`', () => {
wrapper.vm.enableOrDisableNewRequirement({
disable: true,
});
return wrapper.vm.$nextTick(() => {
const newReqButton = document.querySelector('.js-new-requirement');
expect(newReqButton.getAttribute('disabled')).toBe('disabled');
expect(newReqButton.classList.contains('disabled')).toBe(true);
});
});
it('enables new requirement button when called with param `{ disable: false }`', () => {
wrapper.vm.enableOrDisableNewRequirement({
disable: false,
});
return wrapper.vm.$nextTick(() => {
const newReqButton = document.querySelector('.js-new-requirement');
expect(newReqButton.getAttribute('disabled')).toBeNull();
expect(newReqButton.classList.contains('disabled')).toBe(false);
});
});
});
describe('handleNewRequirementClick', () => { describe('handleNewRequirementClick', () => {
it('sets `showCreateForm` prop to `true`', () => { it('sets `showCreateForm` prop to `true`', () => {
wrapper.vm.handleNewRequirementClick(); wrapper.vm.handleNewRequirementClick();
...@@ -322,6 +357,22 @@ describe('RequirementsRoot', () => { ...@@ -322,6 +357,22 @@ describe('RequirementsRoot', () => {
); );
}); });
it('calls `visitUrl` when project has no requirements and request is successful', () => {
jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(mockMutationResult);
wrapper.setProps({
requirementsCount: {
OPENED: 0,
ARCHIVED: 0,
ALL: 0,
},
});
return wrapper.vm.handleNewRequirementSave('foo').then(() => {
expect(visitUrl).toHaveBeenCalledWith('/gitlab-org/gitlab-shell/-/requirements');
});
});
it('sets `showCreateForm` and `createRequirementRequestActive` props to `false` and calls `$apollo.queries.requirements.refetch()` when request is successful', () => { it('sets `showCreateForm` and `createRequirementRequestActive` props to `false` and calls `$apollo.queries.requirements.refetch()` when request is successful', () => {
jest jest
.spyOn(wrapper.vm.$apollo, 'mutate') .spyOn(wrapper.vm.$apollo, 'mutate')
...@@ -587,6 +638,16 @@ describe('RequirementsRoot', () => { ...@@ -587,6 +638,16 @@ describe('RequirementsRoot', () => {
}); });
}); });
it('does not render requirement-empty-state component when `showCreateForm` prop is `true`', () => {
wrapper.setData({
showCreateForm: true,
});
return wrapper.vm.$nextTick(() => {
expect(wrapper.find(RequirementsEmptyState).exists()).toBe(false);
});
});
it('renders requirement items for all the requirements', () => { it('renders requirement items for all the requirements', () => {
wrapper.setData({ wrapper.setData({
requirements: { requirements: {
......
...@@ -17184,6 +17184,12 @@ msgstr "" ...@@ -17184,6 +17184,12 @@ msgstr ""
msgid "Requirements" msgid "Requirements"
msgstr "" msgstr ""
msgid "Requirements allow you to create criteria to check your products against."
msgstr ""
msgid "Requirements can be based on users, stakeholders, system, software or anything else you find important to capture."
msgstr ""
msgid "Requires approval from %{names}." msgid "Requires approval from %{names}."
msgid_plural "Requires %{count} more approvals from %{names}." msgid_plural "Requires %{count} more approvals from %{names}."
msgstr[0] "" msgstr[0] ""
......
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