Commit 2946e918 authored by Kushal Pandya's avatar Kushal Pandya

Move tabs within Requirements Vue app

- Moves Requirements tabs into Vue app.
- Makes Requirements count more reliable by refetching it from backend
on mutation.
parent 7da7c18c
......@@ -4,14 +4,16 @@ import { GlPagination } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
import createFlash from '~/flash';
import { urlParamsToObject } from '~/lib/utils/common_utils';
import { updateHistory, setUrlParams, visitUrl } from '~/lib/utils/url_utility';
import { updateHistory, setUrlParams } from '~/lib/utils/url_utility';
import RequirementsTabs from './requirements_tabs.vue';
import RequirementsLoading from './requirements_loading.vue';
import RequirementsEmptyState from './requirements_empty_state.vue';
import RequirementItem from './requirement_item.vue';
import RequirementForm from './requirement_form.vue';
import projectRequirements from '../queries/projectRequirements.query.graphql';
import projectRequirementsCount from '../queries/projectRequirementsCount.query.graphql';
import createRequirement from '../queries/createRequirement.mutation.graphql';
import updateRequirement from '../queries/updateRequirement.mutation.graphql';
......@@ -20,6 +22,7 @@ import { FilterState, DEFAULT_PAGE_SIZE } from '../constants';
export default {
DEFAULT_PAGE_SIZE,
components: {
RequirementsTabs,
GlPagination,
RequirementsLoading,
RequirementsEmptyState,
......@@ -31,11 +34,11 @@ export default {
type: String,
required: true,
},
filterBy: {
initialFilterBy: {
type: String,
required: true,
},
requirementsCount: {
initialRequirementsCount: {
type: Object,
required: true,
validator: value =>
......@@ -87,6 +90,8 @@ export default {
queryVariables.firstPageSize = DEFAULT_PAGE_SIZE;
}
// Include `state` only if `filterBy` is not `ALL`.
// as Grqph query only supports `OPEN` and `ARCHIVED`.
if (this.filterBy !== FilterState.all) {
queryVariables.state = this.filterBy;
}
......@@ -95,29 +100,42 @@ export default {
},
update(data) {
const requirementsRoot = data.project?.requirements;
const { opened = 0, archived = 0 } = data.project?.requirementStatesCount;
return {
list: requirementsRoot?.nodes || [],
pageInfo: requirementsRoot?.pageInfo || {},
count: {
};
},
error: e => {
createFlash(__('Something went wrong while fetching requirements list.'));
Sentry.captureException(e);
},
},
requirementsCount: {
query: projectRequirementsCount,
variables() {
return {
projectPath: this.projectPath,
};
},
update({ project = {} }) {
const { opened = 0, archived = 0 } = project.requirementStatesCount;
return {
OPENED: opened,
ARCHIVED: archived,
ALL: opened + archived,
},
};
},
error: e => {
createFlash(__('Something went wrong while fetching requirements list.'));
createFlash(__('Something went wrong while fetching requirements count.'));
Sentry.captureException(e);
},
},
},
data() {
const tabsContainerEl = document.querySelector('.js-requirements-state-filters');
return {
newRequirementEl: null,
filterBy: this.initialFilterBy,
showCreateForm: false,
showUpdateFormForRequirement: 0,
createRequirementRequestActive: false,
......@@ -127,15 +145,12 @@ export default {
nextPageCursor: this.next,
requirements: {
list: [],
count: {},
pageInfo: {},
},
openedCount: this.requirementsCount[FilterState.opened],
archivedCount: this.requirementsCount[FilterState.archived],
countEls: {
opened: tabsContainerEl.querySelector('.js-opened-count'),
archived: tabsContainerEl.querySelector('.js-archived-count'),
all: tabsContainerEl.querySelector('.js-all-count'),
requirementsCount: {
OPENED: this.initialRequirementsCount[FilterState.opened],
ARCHIVED: this.initialRequirementsCount[FilterState.archived],
ALL: this.initialRequirementsCount[FilterState.all],
},
};
},
......@@ -149,31 +164,17 @@ export default {
return this.$apollo.queries.requirements.loading;
},
requirementsListEmpty() {
return !this.$apollo.queries.requirements.loading && !this.requirements.list.length;
return (
!this.$apollo.queries.requirements.loading &&
!this.requirements.list.length &&
this.requirementsCount[this.filterBy] === 0
);
},
/**
* We want to ensure that count `0` is prioritized
* over `this.requirements.count` (GraphQL) or `this.requirementsCount` (HAML prop)
* as both of them are invalid once user does archive/reopen actions.
* this is a technical debt that we want to clean up once mutations support
* `requirementStatesCount` connection.
*/
totalRequirementsForCurrentTab() {
if (this.filterBy === FilterState.opened) {
return this.openedCount === 0
? 0
: this.requirements.count.OPENED || this.requirementsCount.OPENED;
} else if (this.filterBy === FilterState.archived) {
return this.archivedCount === 0
? 0
: this.requirements.count.ARCHIVED || this.requirementsCount.ARCHIVED;
}
return this.requirements.count[this.filterBy] || this.requirementsCount[this.filterBy];
return this.requirementsCount[this.filterBy];
},
showEmptyState() {
return (
(this.requirementsListEmpty && !this.showCreateForm) || !this.totalRequirementsForCurrentTab
);
return this.requirementsListEmpty && !this.showCreateForm;
},
showPaginationControls() {
return this.totalRequirementsForCurrentTab > DEFAULT_PAGE_SIZE && !this.requirementsListEmpty;
......@@ -188,36 +189,6 @@ export default {
: nextPage;
},
},
watch: {
showCreateForm(value) {
this.enableOrDisableNewRequirement({
disable: value,
});
},
requirements() {
const totalCount = this.requirements.count.ALL;
this.countEls.all.innerText = totalCount;
},
openedCount(value) {
this.countEls.opened.innerText = value;
},
archivedCount(value) {
this.countEls.archived.innerText = value;
},
},
mounted() {
if (this.filterBy === FilterState.opened) {
this.newRequirementEl = document.querySelector('.js-new-requirement');
this.newRequirementEl.addEventListener('click', this.handleNewRequirementClick);
}
},
beforeDestroy() {
if (this.filterBy === FilterState.opened) {
this.newRequirementEl.removeEventListener('click', this.handleNewRequirementClick);
}
},
methods: {
/**
* Update browser URL with updated query-param values
......@@ -273,21 +244,21 @@ export default {
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');
}
}
handleTabClick({ filterBy }) {
this.filterBy = filterBy;
this.prevPageCursor = '';
this.nextPageCursor = '';
// Update browser URL
updateHistory({
url: setUrlParams({ state: filterBy.toLowerCase() }, window.location.href, true),
title: document.title,
replace: true,
});
// Wait for changes to propagate in component
// and then fetch again.
this.$nextTick(() => this.$apollo.queries.requirements.refetch());
},
handleNewRequirementClick() {
this.showCreateForm = true;
......@@ -296,7 +267,6 @@ export default {
this.showUpdateFormForRequirement = iid;
},
handleNewRequirementSave(title) {
const reloadPage = this.totalRequirementsForCurrentTab === 0;
this.createRequirementRequestActive = true;
return this.$apollo
.mutate({
......@@ -310,18 +280,14 @@ export default {
})
.then(({ data }) => {
if (!data.createRequirement.errors.length) {
if (reloadPage) {
visitUrl(this.requirementsWebUrl);
} else {
this.showCreateForm = false;
this.$apollo.queries.requirementsCount.refetch();
this.$apollo.queries.requirements.refetch();
this.openedCount += 1;
this.$toast.show(
sprintf(__('Requirement %{reference} has been added'), {
reference: `REQ-${data.createRequirement.requirement.iid}`,
}),
);
}
this.showCreateForm = false;
} else {
throw new Error(`Error creating a requirement`);
}
......@@ -369,17 +335,14 @@ export default {
: __('Something went wrong while archiving a requirement.'),
}).then(({ data }) => {
if (!data.updateRequirement.errors.length) {
this.$apollo.queries.requirementsCount.refetch();
this.stateChangeRequestActiveFor = 0;
let toastMessage;
if (params.state === FilterState.opened) {
this.openedCount += 1;
this.archivedCount -= 1;
toastMessage = sprintf(__('Requirement %{reference} has been reopened'), {
reference: `REQ-${data.updateRequirement.requirement.iid}`,
});
} else {
this.openedCount -= 1;
this.archivedCount += 1;
toastMessage = sprintf(__('Requirement %{reference} has been archived'), {
reference: `REQ-${data.updateRequirement.requirement.iid}`,
});
......@@ -418,6 +381,14 @@ export default {
<template>
<div class="requirements-list-container">
<requirements-tabs
:filter-by="filterBy"
:requirements-count="requirementsCount"
:show-create-form="showCreateForm"
:can-create-requirement="canCreateRequirement"
@clickTab="handleTabClick"
@clickNewRequirement="handleNewRequirementClick"
/>
<requirement-form
v-if="showCreateForm"
:requirement-request-active="createRequirementRequestActive"
......
<script>
import { GlLink, GlBadge, GlButton } from '@gitlab/ui';
import { FilterState } from '../constants';
export default {
FilterState,
components: {
GlLink,
GlBadge,
GlButton,
},
props: {
filterBy: {
type: String,
required: true,
},
requirementsCount: {
type: Object,
required: true,
},
showCreateForm: {
type: Boolean,
required: true,
},
canCreateRequirement: {
type: Boolean,
required: false,
},
},
computed: {
isOpenTab() {
return this.filterBy === FilterState.opened;
},
isArchivedTab() {
return this.filterBy === FilterState.archived;
},
isAllTab() {
return this.filterBy === FilterState.all;
},
},
};
</script>
<template>
<div class="top-area">
<ul class="nav-links mobile-separator requirements-state-filters js-requirements-state-filters">
<li :class="{ active: isOpenTab }">
<gl-link
id="state-opened"
data-state="opened"
:title="__('Filter by requirements that are currently opened.')"
@click="$emit('clickTab', { filterBy: $options.FilterState.opened })"
>
{{ __('Open') }}
<gl-badge class="badge-pill">{{ requirementsCount.OPENED }}</gl-badge>
</gl-link>
</li>
<li :class="{ active: isArchivedTab }">
<gl-link
id="state-archived"
data-state="archived"
:title="__('Filter by requirements that are currently archived.')"
@click="$emit('clickTab', { filterBy: $options.FilterState.archived })"
>
{{ __('Archived') }}
<gl-badge class="badge-pill">{{ requirementsCount.ARCHIVED }}</gl-badge>
</gl-link>
</li>
<li :class="{ active: isAllTab }">
<gl-link
id="state-all"
data-state="all"
:title="__('Show all requirements.')"
@click="$emit('clickTab', { filterBy: $options.FilterState.all })"
>
{{ __('All') }}
<gl-badge class="badge-pill">{{ requirementsCount.ALL }}</gl-badge>
</gl-link>
</li>
</ul>
<div v-if="isOpenTab && canCreateRequirement" class="nav-controls">
<gl-button
category="primary"
variant="success"
class="js-new-requirement qa-new-requirement-button"
:disabled="showCreateForm"
@click="$emit('clickNewRequirement')"
>{{ __('New requirement') }}</gl-button
>
</div>
</div>
</template>
......@@ -7,10 +7,6 @@ query projectRequirements(
$nextPageCursor: String = ""
) {
project(fullPath: $projectPath) {
requirementStatesCount {
opened
archived
}
requirements(
first: $firstPageSize
last: $lastPageSize
......
query projectRequirements($projectPath: ID!) {
project(fullPath: $projectPath) {
requirementStatesCount {
opened
archived
}
}
}
......@@ -3,6 +3,7 @@ import VueApollo from 'vue-apollo';
import { GlToast } from '@gitlab/ui';
import { defaultDataIdFromObject } from 'apollo-cache-inmemory';
import createDefaultClient from '~/lib/graphql';
import { parseBoolean } from '~/lib/utils/common_utils';
import RequirementsRoot from './components/requirements_root.vue';
......@@ -58,8 +59,8 @@ export default () => {
const ALL = parseInt(all, 10);
return {
filterBy: stateFilterBy,
requirementsCount: {
initialFilterBy: stateFilterBy,
initialRequirementsCount: {
OPENED,
ARCHIVED,
ALL,
......@@ -77,13 +78,13 @@ export default () => {
return createElement('requirements-root', {
props: {
projectPath: this.projectPath,
filterBy: this.filterBy,
requirementsCount: this.requirementsCount,
initialFilterBy: this.initialFilterBy,
initialRequirementsCount: this.initialRequirementsCount,
page: parseInt(this.page, 10) || 1,
prev: this.prev,
next: this.next,
emptyStatePath: this.emptyStatePath,
canCreateRequirement: this.canCreateRequirement,
canCreateRequirement: parseBoolean(this.canCreateRequirement),
requirementsWebUrl: this.requirementsWebUrl,
},
});
......
- page_title _('Requirements')
- type = :requirements
- page_context_word = type.to_s.humanize(capitalize: false)
- @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']
- 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'
......@@ -19,28 +16,6 @@
- else
- current_tab_count = total_requirements > page_size ? page_size : total_requirements
.top-area
%ul.nav-links.mobile-separator.requirements-state-filters.js-requirements-state-filters
%li{ class: active_when(is_open_tab) }>
= link_to page_filter_path(state: 'opened', without: ignore_page_params), id: 'state-opened', title: (_("Filter by %{issuable_type} that are currently opened.") % { issuable_type: page_context_word }), data: { state: 'opened' } do
= _('Open')
%span.badge.badge-pill.js-opened-count= requirements_count['opened']
%li{ class: active_when(params[:state] == 'archived') }>
= link_to page_filter_path(state: 'archived', without: ignore_page_params), id: 'state-archived', title: (_("Filter by %{issuable_type} that are currently archived.") % { issuable_type: page_context_word }), data: { state: 'archived' } do
= _('Archived')
%span.badge.badge-pill.js-archived-count= requirements_count['archived']
%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
= _('All')
%span.badge.badge-pill.js-all-count= total_requirements
.nav-controls
- if is_open_tab && can?(current_user, :create_requirement, @project)
%button.btn.btn-success.js-new-requirement.qa-new-requirement-button{ type: 'button' }
= _('New requirement')
#js-requirements-app{ data: { filter_by: params[:state],
page: params[:page],
prev: params[:prev],
......@@ -50,7 +25,7 @@
archived: requirements_count['archived'],
all: total_requirements,
requirements_web_url: project_requirements_management_requirements_path(@project),
can_create_requirement: can?(current_user, :create_requirement, @project),
can_create_requirement: "#{can?(current_user, :create_requirement, @project)}",
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
......
......@@ -184,9 +184,7 @@ describe 'Requirements list', :js do
end
it 'does not show button "New requirement"' do
page.within('.nav-controls') do
expect(page).not_to have_selector('button.js-new-requirement')
end
expect(page).not_to have_selector('.nav-controls button.js-new-requirement')
end
it 'shows list of all archived requirements' do
......@@ -229,9 +227,7 @@ describe 'Requirements list', :js do
end
it 'does not show button "New requirement"' do
page.within('.nav-controls') do
expect(page).not_to have_selector('button.js-new-requirement')
end
expect(page).not_to have_selector('.nav-controls button.js-new-requirement')
end
it 'shows list of all requirements' do
......@@ -251,9 +247,7 @@ describe 'Requirements list', :js do
end
it 'open tab does not show button "New requirement"' do
page.within('.nav-controls') do
expect(page).not_to have_selector('button.js-new-requirement')
end
expect(page).not_to have_selector('.nav-controls button.js-new-requirement')
end
end
end
......@@ -3,9 +3,9 @@ import { shallowMount } from '@vue/test-utils';
import { GlPagination } from '@gitlab/ui';
import * as Sentry from '@sentry/browser';
import createFlash from '~/flash';
import { visitUrl } from '~/lib/utils/url_utility';
import RequirementsRoot from 'ee/requirements/components/requirements_root.vue';
import RequirementsTabs from 'ee/requirements/components/requirements_tabs.vue';
import RequirementsLoading from 'ee/requirements/components/requirements_loading.vue';
import RequirementsEmptyState from 'ee/requirements/components/requirements_empty_state.vue';
import RequirementItem from 'ee/requirements/components/requirement_item.vue';
......@@ -31,10 +31,6 @@ jest.mock('ee/requirements/constants', () => ({
}));
jest.mock('~/flash');
jest.mock('~/lib/utils/url_utility', () => ({
...jest.requireActual('~/lib/utils/url_utility'),
visitUrl: jest.fn(),
}));
const $toast = {
show: jest.fn(),
......@@ -42,8 +38,8 @@ const $toast = {
const createComponent = ({
projectPath = 'gitlab-org/gitlab-shell',
filterBy = FilterState.opened,
requirementsCount = mockRequirementsCount,
initialFilterBy = FilterState.opened,
initialRequirementsCount = mockRequirementsCount,
showCreateRequirement = false,
emptyStatePath = '/assets/illustrations/empty-state/requirements.svg',
loading = false,
......@@ -53,8 +49,8 @@ const createComponent = ({
shallowMount(RequirementsRoot, {
propsData: {
projectPath,
filterBy,
requirementsCount,
initialFilterBy,
initialRequirementsCount,
showCreateRequirement,
emptyStatePath,
canCreateRequirement,
......@@ -67,7 +63,10 @@ const createComponent = ({
loading,
list: [],
pageInfo: {},
count: {},
refetch: jest.fn(),
},
requirementsCount: {
...initialRequirementsCount,
refetch: jest.fn(),
},
},
......@@ -81,16 +80,6 @@ describe('RequirementsRoot', () => {
let wrapper;
beforeEach(() => {
setFixtures(`
<div class="js-nav-requirements-count"></div>
<div class="js-nav-requirements-count-fly-out"></div>
<div class="js-requirements-state-filters">
<span class="js-opened-count"></span>
<span class="js-archived-count"></span>
<span class="js-all-count"></span>
</div>
<button class="js-new-requirement">New requirement</button>
`);
wrapper = createComponent();
});
......@@ -99,21 +88,64 @@ describe('RequirementsRoot', () => {
});
describe('computed', () => {
describe('requirementsListEmpty', () => {
it('returns `false` when `$apollo.queries.requirements.loading` is true', () => {
const wrapperLoading = createComponent({ loading: true });
expect(wrapperLoading.vm.requirementsListEmpty).toBe(false);
wrapperLoading.destroy();
});
it('returns `false` when `requirements.list` is empty', () => {
wrapper.setData({
requirements: {
list: [],
},
});
return wrapper.vm.$nextTick(() => {
expect(wrapper.vm.requirementsListEmpty).toBe(false);
});
});
it('returns `true` when `requirementsCount` for current filterBy value is 0', () => {
wrapper.setData({
filterBy: FilterState.opened,
requirementsCount: {
OPENED: 0,
},
});
return wrapper.vm.$nextTick(() => {
expect(wrapper.vm.requirementsListEmpty).toBe(true);
});
});
});
describe('totalRequirementsForCurrentTab', () => {
it('returns number representing total requirements for current tab', () => {
expect(wrapper.vm.totalRequirementsForCurrentTab).toBe(mockRequirementsCount.OPENED);
wrapper.setData({
filterBy: FilterState.opened,
requirementsCount: {
OPENED: mockRequirementsCount.OPENED,
},
});
it('returns 0 when `openedCount` is 0 and filterBy represents opened tab', () => {
wrapper.setProps({
filterBy: FilterState.opened,
return wrapper.vm.$nextTick(() => {
expect(wrapper.vm.totalRequirementsForCurrentTab).toBe(mockRequirementsCount.OPENED);
});
});
});
describe('showEmptyState', () => {
it('returns `false` when `showCreateForm` is true', () => {
wrapper.setData({
openedCount: 0,
showCreateForm: true,
});
return wrapper.vm.$nextTick(() => {
expect(wrapper.vm.totalRequirementsForCurrentTab).toBe(0);
expect(wrapper.vm.showEmptyState).toBe(false);
});
});
});
......@@ -123,9 +155,9 @@ describe('RequirementsRoot', () => {
wrapper.setData({
requirements: {
list: mockRequirementsOpen,
count: mockRequirementsCount,
pageInfo: mockPageInfo,
},
requirementsCount: mockRequirementsCount,
});
return wrapper.vm.$nextTick(() => {
......@@ -137,12 +169,12 @@ describe('RequirementsRoot', () => {
wrapper.setData({
requirements: {
list: [mockRequirementsOpen[0]],
count: {
pageInfo: mockPageInfo,
},
requirementsCount: {
...mockRequirementsCount,
OPENED: 1,
},
pageInfo: mockPageInfo,
},
});
return wrapper.vm.$nextTick(() => {
......@@ -294,34 +326,6 @@ 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', () => {
it('sets `showCreateForm` prop to `true`', () => {
wrapper.vm.handleNewRequirementClick();
......@@ -380,33 +384,21 @@ 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 refetches requirements count and list when request is successful', () => {
jest
.spyOn(wrapper.vm.$apollo, 'mutate')
.mockReturnValue(Promise.resolve(mockMutationResult));
jest
.spyOn(wrapper.vm.$apollo.queries.requirementsCount, 'refetch')
.mockImplementation(jest.fn());
jest
.spyOn(wrapper.vm.$apollo.queries.requirements, 'refetch')
.mockImplementation(jest.fn());
return wrapper.vm.handleNewRequirementSave('foo').then(() => {
expect(wrapper.vm.showCreateForm).toBe(false);
expect(wrapper.vm.$apollo.queries.requirementsCount.refetch).toHaveBeenCalled();
expect(wrapper.vm.$apollo.queries.requirements.refetch).toHaveBeenCalled();
expect(wrapper.vm.showCreateForm).toBe(false);
expect(wrapper.vm.createRequirementRequestActive).toBe(false);
});
});
......@@ -571,11 +563,10 @@ describe('RequirementsRoot', () => {
});
});
it('increments `openedCount` by 1 and decrements `archivedCount` by 1 when `params.state` is "OPENED"', () => {
wrapper.setData({
openedCount: 1,
archivedCount: 1,
});
it('refetches requirementsCount query when request is successful', () => {
jest
.spyOn(wrapper.vm.$apollo.queries.requirementsCount, 'refetch')
.mockImplementation(jest.fn());
return wrapper.vm
.handleRequirementStateChange({
......@@ -583,8 +574,7 @@ describe('RequirementsRoot', () => {
state: FilterState.opened,
})
.then(() => {
expect(wrapper.vm.openedCount).toBe(2);
expect(wrapper.vm.archivedCount).toBe(0);
expect(wrapper.vm.$apollo.queries.requirementsCount.refetch).toHaveBeenCalled();
});
});
......@@ -601,23 +591,6 @@ describe('RequirementsRoot', () => {
});
});
it('decrements `openedCount` by 1 and increments `archivedCount` by 1 when `params.state` is "ARCHIVED"', () => {
wrapper.setData({
openedCount: 1,
archivedCount: 1,
});
return wrapper.vm
.handleRequirementStateChange({
iid: '1',
state: FilterState.archived,
})
.then(() => {
expect(wrapper.vm.openedCount).toBe(0);
expect(wrapper.vm.archivedCount).toBe(2);
});
});
it('calls `$toast.show` with string "Requirement has been archived" when `params.state` is "ARCHIVED" and request is successful', () => {
return wrapper.vm
.handleRequirementStateChange({
......@@ -647,9 +620,9 @@ describe('RequirementsRoot', () => {
wrapper.setData({
requirements: {
list: mockRequirementsOpen,
count: mockRequirementsCount,
pageInfo: mockPageInfo,
},
requirementsCount: mockRequirementsCount,
});
return wrapper.vm.$nextTick();
......@@ -688,9 +661,24 @@ describe('RequirementsRoot', () => {
expect(wrapper.classes()).toContain('requirements-list-container');
});
it('renders requirements-tabs component', () => {
expect(wrapper.find(RequirementsTabs).exists()).toBe(true);
});
it('renders empty state when query results are empty', () => {
wrapper.setData({
requirements: {
list: [],
},
requirementsCount: {
OPENED: 0,
},
});
return wrapper.vm.$nextTick(() => {
expect(wrapper.find(RequirementsEmptyState).exists()).toBe(true);
});
});
it('renders requirements-loading component when query results are still being loaded', () => {
const wrapperLoading = createComponent({ loading: true });
......@@ -724,9 +712,9 @@ describe('RequirementsRoot', () => {
wrapper.setData({
requirements: {
list: mockRequirementsOpen,
count: mockRequirementsCount,
pageInfo: mockPageInfo,
},
requirementsCount: mockRequirementsCount,
});
return wrapper.vm.$nextTick(() => {
......@@ -741,9 +729,9 @@ describe('RequirementsRoot', () => {
wrapper.setData({
requirements: {
list: mockRequirementsOpen,
count: mockRequirementsCount,
pageInfo: mockPageInfo,
},
requirementsCount: mockRequirementsCount,
});
return wrapper.vm.$nextTick(() => {
......
import { shallowMount } from '@vue/test-utils';
import { GlLink, GlBadge, GlButton } from '@gitlab/ui';
import RequirementsTabs from 'ee/requirements/components/requirements_tabs.vue';
import { FilterState } from 'ee/requirements/constants';
import { mockRequirementsCount } from '../mock_data';
const createComponent = ({
filterBy = FilterState.opened,
requirementsCount = mockRequirementsCount,
showCreateForm = false,
canCreateRequirement = true,
} = {}) =>
shallowMount(RequirementsTabs, {
propsData: {
filterBy,
requirementsCount,
showCreateForm,
canCreateRequirement,
},
});
describe('RequirementsTabs', () => {
let wrapper;
beforeEach(() => {
wrapper = createComponent();
});
afterEach(() => {
wrapper.destroy();
});
describe('template', () => {
it('renders "Open" tab', () => {
const tabEl = wrapper.findAll(GlLink).at(0);
expect(tabEl.attributes('id')).toBe('state-opened');
expect(tabEl.attributes('data-state')).toBe('opened');
expect(tabEl.attributes('title')).toBe('Filter by requirements that are currently opened.');
expect(tabEl.text()).toContain('Open');
expect(tabEl.find(GlBadge).text()).toBe(`${mockRequirementsCount.OPENED}`);
});
it('renders "Archived" tab', () => {
const tabEl = wrapper.findAll(GlLink).at(1);
expect(tabEl.attributes('id')).toBe('state-archived');
expect(tabEl.attributes('data-state')).toBe('archived');
expect(tabEl.attributes('title')).toBe('Filter by requirements that are currently archived.');
expect(tabEl.text()).toContain('Archived');
expect(tabEl.find(GlBadge).text()).toBe(`${mockRequirementsCount.ARCHIVED}`);
});
it('renders "All" tab', () => {
const tabEl = wrapper.findAll(GlLink).at(2);
expect(tabEl.attributes('id')).toBe('state-all');
expect(tabEl.attributes('data-state')).toBe('all');
expect(tabEl.attributes('title')).toBe('Show all requirements.');
expect(tabEl.text()).toContain('All');
expect(tabEl.find(GlBadge).text()).toBe(`${mockRequirementsCount.ALL}`);
});
it('renders class `active` on currently selected tab', () => {
const tabEl = wrapper.findAll('li').at(0);
expect(tabEl.classes()).toContain('active');
});
it('renders "New requirement" button when current tab is "Open" tab', () => {
wrapper.setProps({
filterBy: FilterState.opened,
});
return wrapper.vm.$nextTick(() => {
const buttonEl = wrapper.find(GlButton);
expect(buttonEl.exists()).toBe(true);
expect(buttonEl.text()).toBe('New requirement');
});
});
it('does not render "New requirement" button when current tab is not "Open" tab', () => {
wrapper.setProps({
filterBy: FilterState.closed,
});
return wrapper.vm.$nextTick(() => {
const buttonEl = wrapper.find(GlButton);
expect(buttonEl.exists()).toBe(false);
});
});
it('does not render "New requirement" button when `canCreateRequirement` prop is false', () => {
wrapper.setProps({
filterBy: FilterState.opened,
canCreateRequirement: false,
});
return wrapper.vm.$nextTick(() => {
const buttonEl = wrapper.find(GlButton);
expect(buttonEl.exists()).toBe(false);
});
});
it('disables "New requirement" button when `showCreateForm` is true', () => {
wrapper.setProps({
showCreateForm: true,
});
return wrapper.vm.$nextTick(() => {
const buttonEl = wrapper.find(GlButton);
expect(buttonEl.props('disabled')).toBe(true);
});
});
});
});
......@@ -9308,9 +9308,6 @@ msgstr ""
msgid "Filter"
msgstr ""
msgid "Filter by %{issuable_type} that are currently archived."
msgstr ""
msgid "Filter by %{issuable_type} that are currently closed."
msgstr ""
......@@ -9326,6 +9323,12 @@ msgstr ""
msgid "Filter by name"
msgstr ""
msgid "Filter by requirements that are currently archived."
msgstr ""
msgid "Filter by requirements that are currently opened."
msgstr ""
msgid "Filter by status"
msgstr ""
......@@ -19152,15 +19155,15 @@ msgstr ""
msgid "Should you ever lose your phone or access to your one time password secret, each of these recovery codes can be used one time each to regain access to your account. Please save them in a safe place, or you %{b_start}will%{b_end} lose access to your account."
msgstr ""
msgid "Show all %{issuable_type}."
msgstr ""
msgid "Show all activity"
msgstr ""
msgid "Show all members"
msgstr ""
msgid "Show all requirements."
msgstr ""
msgid "Show archived projects"
msgstr ""
......@@ -19535,6 +19538,9 @@ msgstr ""
msgid "Something went wrong while fetching related merge requests."
msgstr ""
msgid "Something went wrong while fetching requirements count."
msgstr ""
msgid "Something went wrong while fetching requirements list."
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