Commit 85cf0320 authored by Martin Wortschack's avatar Martin Wortschack

Merge branch '207981-requirements-list-pagination-support' into 'master'

[Part-3] Add pagination support in Requirements list

Closes #207981

See merge request gitlab-org/gitlab!28508
parents 914cd9ee 50afa65e
......@@ -58,6 +58,19 @@ export default {
return this.requirement.author;
},
},
methods: {
/**
* This is needed as an independent method since
* when user changes current page, `$refs.authorLink`
* will be null until next page results are loaded & rendered.
*/
getAuthorPopoverTarget() {
if (this.$refs.authorLink) {
return this.$refs.authorLink.$el;
}
return '';
},
},
};
</script>
......@@ -108,7 +121,7 @@ export default {
</div>
</div>
</div>
<gl-popover :target="() => $refs.authorLink.$el" triggers="hover focus" placement="top">
<gl-popover :target="getAuthorPopoverTarget()" triggers="hover focus" placement="top">
<div class="user-popover p-0 d-flex">
<div class="p-1 flex-shrink-1">
<gl-avatar :entity-name="author.name" :alt="author.name" :src="author.avatarUrl" />
......
<script>
import { GlSkeletonLoading } from '@gitlab/ui';
import { DEFAULT_PAGE_SIZE } from '../constants';
export default {
components: {
GlSkeletonLoading,
},
props: {
filterBy: {
type: String,
required: true,
},
currentTabCount: {
type: Number,
required: true,
},
currentPage: {
type: Number,
required: true,
},
},
computed: {
lastPage() {
return Math.ceil(this.currentTabCount / DEFAULT_PAGE_SIZE);
},
loaderCount() {
if (this.currentTabCount > DEFAULT_PAGE_SIZE && this.currentPage !== this.lastPage) {
return DEFAULT_PAGE_SIZE;
}
return this.currentTabCount % DEFAULT_PAGE_SIZE || DEFAULT_PAGE_SIZE;
},
},
};
</script>
<template>
<ul class="content-list issuable-list issues-list requirements-list-loading">
<li v-for="(i, index) in Array(loaderCount).fill()" :key="index" class="issue requirement">
<gl-skeleton-loading :lines="2" class="pt-2" />
</li>
</ul>
</template>
<script>
import * as Sentry from '@sentry/browser';
import { GlLoadingIcon } from '@gitlab/ui';
import { GlPagination } from '@gitlab/ui';
import { __ } from '~/locale';
import createFlash from '~/flash';
import { urlParamsToObject } from '~/lib/utils/common_utils';
import { updateHistory, setUrlParams } from '~/lib/utils/url_utility';
import RequirementsLoading from './requirements_loading.vue';
import RequirementsEmptyState from './requirements_empty_state.vue';
import RequirementItem from './requirement_item.vue';
import projectRequirements from '../queries/projectRequirements.query.graphql';
import { FilterState } from '../constants';
import { FilterState, DEFAULT_PAGE_SIZE } from '../constants';
export default {
DEFAULT_PAGE_SIZE,
components: {
GlLoadingIcon,
GlPagination,
RequirementsLoading,
RequirementsEmptyState,
RequirementItem,
},
......@@ -25,6 +30,26 @@ export default {
type: String,
required: true,
},
requirementsCount: {
type: Object,
required: true,
validator: value => ['OPENED', 'ARCHIVED', 'ALL'].every(prop => value[prop]),
},
page: {
type: Number,
required: false,
default: 1,
},
prev: {
type: String,
required: false,
default: '',
},
next: {
type: String,
required: false,
default: '',
},
showCreateRequirement: {
type: Boolean,
required: true,
......@@ -42,13 +67,36 @@ export default {
projectPath: this.projectPath,
};
if (this.prevPageCursor) {
queryVariables.prevPageCursor = this.prevPageCursor;
queryVariables.lastPageSize = DEFAULT_PAGE_SIZE;
} else if (this.nextPageCursor) {
queryVariables.nextPageCursor = this.nextPageCursor;
queryVariables.firstPageSize = DEFAULT_PAGE_SIZE;
} else {
queryVariables.firstPageSize = DEFAULT_PAGE_SIZE;
}
if (this.filterBy !== FilterState.all) {
queryVariables.state = this.filterBy;
}
return queryVariables;
},
update: data => data.project?.requirements?.nodes || [],
update(data) {
const requirementsRoot = data.project?.requirements;
const count = data.project?.requirementStatesCount;
return {
list: requirementsRoot?.nodes || [],
pageInfo: requirementsRoot?.pageInfo || {},
count: {
OPENED: count.opened,
ARCHIVED: count.archived,
ALL: count.opened + count.archived,
},
};
},
error: e => {
createFlash(__('Something went wrong while fetching requirements list.'));
Sentry.captureException(e);
......@@ -57,7 +105,14 @@ export default {
},
data() {
return {
requirements: [],
currentPage: this.page,
prevPageCursor: this.prev,
nextPageCursor: this.next,
requirements: {
list: [],
count: {},
pageInfo: {},
},
};
},
computed: {
......@@ -65,7 +120,70 @@ export default {
return this.$apollo.queries.requirements.loading;
},
requirementsListEmpty() {
return !this.$apollo.queries.requirements.loading && !this.requirements.length;
return !this.$apollo.queries.requirements.loading && !this.requirements.list.length;
},
totalRequirements() {
return this.requirements.count[this.filterBy] || this.requirementsCount[this.filterBy];
},
showPaginationControls() {
return this.totalRequirements > DEFAULT_PAGE_SIZE && !this.requirementsListEmpty;
},
prevPage() {
return Math.max(this.currentPage - 1, 0);
},
nextPage() {
const nextPage = this.currentPage + 1;
return nextPage > Math.ceil(this.totalRequirements / DEFAULT_PAGE_SIZE) ? null : nextPage;
},
},
methods: {
/**
* Update browser URL with updated query-param values
* based on current page details.
*/
updateUrl({ page, prev, next }) {
const { href, search } = window.location;
const queryParams = urlParamsToObject(search);
queryParams.page = page || 1;
// Only keep params that have any values.
if (prev) {
queryParams.prev = prev;
} else {
delete queryParams.prev;
}
if (next) {
queryParams.next = next;
} else {
delete queryParams.next;
}
// We want to replace the history state so that back button
// correctly reloads the page with previous URL.
updateHistory({
url: setUrlParams(queryParams, href, true),
title: document.title,
replace: true,
});
},
handlePageChange(page) {
const { startCursor, endCursor } = this.requirements.pageInfo;
if (page > this.currentPage) {
this.prevPageCursor = '';
this.nextPageCursor = endCursor;
} else {
this.prevPageCursor = startCursor;
this.nextPageCursor = '';
}
this.currentPage = page;
this.updateUrl({
page,
prev: this.prevPageCursor,
next: this.nextPageCursor,
});
},
},
};
......@@ -78,13 +196,31 @@ export default {
:filter-by="filterBy"
:empty-state-path="emptyStatePath"
/>
<gl-loading-icon v-if="requirementsListLoading" class="mt-3" size="md" />
<ul v-else class="content-list issuable-list issues-list requirements-list">
<requirements-loading
v-show="requirementsListLoading"
:filter-by="filterBy"
:current-tab-count="totalRequirements"
:current-page="currentPage"
/>
<ul
v-if="!requirementsListLoading && !requirementsListEmpty"
class="content-list issuable-list issues-list requirements-list"
>
<requirement-item
v-for="requirement in requirements"
v-for="requirement in requirements.list"
:key="requirement.iid"
:requirement="requirement"
/>
</ul>
<gl-pagination
v-if="showPaginationControls"
:value="currentPage"
:per-page="$options.DEFAULT_PAGE_SIZE"
:prev-page="prevPage"
:next-page="nextPage"
align="center"
class="gl-pagination prepend-top-default"
@input="handlePageChange"
/>
</div>
</template>
......@@ -10,3 +10,5 @@ export const FilterStateEmptyMessage = {
OPENED: __('There are no open requirements'),
ARCHIVED: __('There are no archived requirements'),
};
export const DEFAULT_PAGE_SIZE = 20;
query projectRequirements($projectPath: ID!, $state: RequirementState) {
query projectRequirements(
$projectPath: ID!
$state: RequirementState
$firstPageSize: Int
$lastPageSize: Int
$prevPageCursor: String = ""
$nextPageCursor: String = ""
) {
project(fullPath: $projectPath) {
requirements(sort: created_desc, state: $state) {
requirementStatesCount {
opened
archived
}
requirements(
first: $firstPageSize
last: $lastPageSize
after: $nextPageCursor
before: $prevPageCursor
sort: created_desc
state: $state
) {
nodes {
iid
title
......@@ -18,6 +36,10 @@ query projectRequirements($projectPath: ID!, $state: RequirementState) {
webUrl
}
}
pageInfo {
startCursor
endCursor
}
}
}
}
......@@ -27,12 +27,32 @@ export default () => {
RequirementsRoot,
},
data() {
const { filterBy, projectPath, emptyStatePath } = el.dataset;
const {
filterBy,
page,
next,
prev,
projectPath,
emptyStatePath,
opened,
archived,
} = el.dataset;
const stateFilterBy = filterBy ? FilterState[filterBy] : FilterState.opened;
const OPENED = parseInt(opened, 10);
const ARCHIVED = parseInt(archived, 10);
return {
showCreateRequirement: false,
filterBy: stateFilterBy,
requirementsCount: {
OPENED,
ARCHIVED,
ALL: OPENED + ARCHIVED,
},
page,
prev,
next,
emptyStatePath,
projectPath,
};
......@@ -53,6 +73,10 @@ export default () => {
props: {
projectPath: this.projectPath,
filterBy: this.filterBy,
requirementsCount: this.requirementsCount,
page: parseInt(this.page, 10) || 1,
prev: this.prev,
next: this.next,
showCreateRequirement: this.showCreateRequirement,
emptyStatePath: this.emptyStatePath,
},
......
......@@ -26,4 +26,21 @@
padding: $gl-padding-4 $gl-vert-padding;
}
}
.requirements-list-loading {
.animation-container {
// This absolute height ensures that
// animation container takes up average height
// similar to a rendered requirement item.
height: 51px;
.skeleton-line-1 {
width: 70%;
}
.skeleton-line-2 {
width: 60%;
}
}
}
}
- return unless Feature.enabled?(:requirements_management, project)
- requirements_count = Hash.new(0).merge(project.requirements.counts_by_state)
- total_count = requirements_count['opened'] + requirements_count['archived']
= nav_link(path: 'requirements#index') do
= link_to project_requirements_path(project), class: 'qa-project-requirements-link' do
.nav-icon-container
= sprite_icon('requirements')
%span.nav-item-name
= _('Requirements')
%span.badge.badge-pill.count= number_with_delimiter(total_count)
%ul.sidebar-sub-level-items
= nav_link(path: 'requirements#index', html_options: { class: "fly-out-top-item" } ) do
= link_to project_requirements_path(project) do
%strong.fly-out-top-item-name= _('Requirements')
%span.badge.badge-pill.count.requirements_counter.fly-out-badge
%span.badge.badge-pill.count.requirements_counter.fly-out-badge= number_with_delimiter(total_count)
%li.divider.fly-out-top-item
= nav_link(path: 'requirements#index', html_options: { class: 'home' }) do
= link_to project_requirements_path(project), title: 'List' do
......
......@@ -3,24 +3,36 @@
- page_context_word = type.to_s.humanize(capitalize: false)
- @content_class = 'requirements-container'
- requirements_count = Hash.new(0).merge(@project.requirements.counts_by_state)
.top-area
%ul.nav-links.mobile-separator.requirements-state-filters
%li{ class: active_when(params[:state].nil? || params[:state] == 'opened') }>
= link_to page_filter_path(state: 'opened'), 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= requirements_count['opened']
%li{ class: active_when(params[:state] == 'archived') }>
= link_to page_filter_path(state: 'archived'), 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= requirements_count['archived']
= render 'shared/issuable/nav_links/all', page_context_word: page_context_word, counter: _('All')
%li{ class: active_when(params[:state] == 'all') }>
= link_to page_filter_path(state: 'all'), id: 'state-all', title: (_("Show all %{issuable_type}.") % { issuable_type: page_context_word }), data: { state: 'all' } do
= _('All')
%span.badge.badge-pill= requirements_count['opened'] + requirements_count['archived']
.nav-controls
%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],
next: params[:next],
project_path: @project.full_path,
opened: requirements_count['opened'],
archived: requirements_count['archived'],
empty_state_path: image_path('illustrations/empty-state/empty-requirements-lg.svg') } }
.gl-spinner-container.mt-3
%span.align-text-bottom.gl-spinner.gl-spinner-orange.gl-spinner-md{ aria: { label: _('Loading'), hidden: 'true' } }
......@@ -8,6 +8,7 @@ describe 'Requirements list', :js do
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) }
before do
stub_licensed_features(requirements: true)
......@@ -25,18 +26,22 @@ describe 'Requirements list', :js do
it 'shows the requirements in the navigation sidebar' do
expect(first('.nav-sidebar .active a .nav-item-name')).to have_content('Requirements')
expect(first('.nav-sidebar .active a .count')).to have_content('4')
end
it 'shows requirements tabs for each status type' do
page.within('.requirements-state-filters') do
expect(page).to have_selector('li > a#state-opened')
expect(find('li > a#state-opened')[:title]).to eq('Filter by requirements that are currently opened.')
expect(find('li > a#state-opened .badge')).to have_content('3')
expect(page).to have_selector('li > a#state-archived')
expect(find('li > a#state-archived')[:title]).to eq('Filter by requirements that are currently archived.')
expect(find('li > a#state-archived .badge')).to have_content('1')
expect(page).to have_selector('li > a#state-all')
expect(find('li > a#state-all')[:title]).to eq('Show all requirements.')
expect(find('li > a#state-all .badge')).to have_content('4')
end
end
......@@ -47,21 +52,61 @@ describe 'Requirements list', :js do
end
end
it 'shows list of all available requirements' do
page.within('.requirements-list-container .requirements-list') do
expect(page).to have_selector('li.requirement', count: 3)
context 'open tab' do
it 'shows list of all open requirements' do
page.within('.requirements-list-container .requirements-list') do
expect(page).to have_selector('li.requirement', count: 3)
end
end
it 'shows title, metadata and actions within each requirement item' do
page.within('.requirements-list li.requirement', match: :first) do
expect(page.find('.issuable-reference')).to have_content("REQ-#{requirement1.iid}")
expect(page.find('.issue-title-text')).to have_content(requirement1.title)
expect(page.find('.issuable-authored')).to have_content('created 5 days ago by')
expect(page.find('.author')).to have_content(user.name)
expect(page.find('.controls')).to have_selector('li.requirement-edit button[title="Edit"]')
expect(page.find('.controls')).to have_selector('li.requirement-archive button[title="Archive"]')
expect(page.find('.issuable-updated-at')).to have_content('updated 2 days ago')
end
end
end
it 'shows title, metadata and actions within each requirement item' do
page.within('.requirements-list li.requirement', match: :first) do
expect(page.find('.issuable-reference')).to have_content("REQ-#{requirement1.iid}")
expect(page.find('.issue-title-text')).to have_content(requirement1.title)
expect(page.find('.issuable-authored')).to have_content('created 5 days ago by')
expect(page.find('.author')).to have_content(user.name)
expect(page.find('.controls')).to have_selector('li.requirement-edit button[title="Edit"]')
expect(page.find('.controls')).to have_selector('li.requirement-archive button[title="Archive"]')
expect(page.find('.issuable-updated-at')).to have_content('updated 2 days ago')
context 'archived tab' do
before do
find('li > a#state-archived').click
wait_for_requests
end
it 'shows list of all archived requirements' do
page.within('.requirements-list-container .requirements-list') do
expect(page).to have_selector('li.requirement', count: 1)
end
end
it 'shows title, metadata and actions within each requirement item' do
page.within('.requirements-list li.requirement', match: :first) do
expect(page.find('.issuable-reference')).to have_content("REQ-#{requirement_archived.iid}")
expect(page.find('.issue-title-text')).to have_content(requirement_archived.title)
expect(page.find('.issuable-authored')).to have_content('created 1 week ago by')
expect(page.find('.author')).to have_content(user.name)
expect(page.find('.issuable-updated-at')).to have_content('updated 2 days ago')
end
end
end
context 'archived tab' do
before do
find('li > a#state-all').click
wait_for_requests
end
it 'shows list of all requirements' do
page.within('.requirements-list-container .requirements-list') do
expect(page).to have_selector('li.requirement', count: 4)
end
end
end
end
......
import { shallowMount } from '@vue/test-utils';
import { GlSkeletonLoading } from '@gitlab/ui';
import RequirementsLoading from 'ee/requirements/components/requirements_loading.vue';
import { FilterState, mockRequirementsCount } from '../mock_data';
jest.mock('ee/requirements/constants', () => ({
DEFAULT_PAGE_SIZE: 2,
}));
const createComponent = ({
filterBy = FilterState.opened,
currentTabCount = mockRequirementsCount.OPENED,
currentPage = 1,
} = {}) =>
shallowMount(RequirementsLoading, {
propsData: {
filterBy,
currentTabCount,
currentPage,
},
});
describe('RequirementsLoading', () => {
let wrapper;
beforeEach(() => {
wrapper = createComponent();
});
afterEach(() => {
wrapper.destroy();
});
describe('computed', () => {
describe('lastPage', () => {
it('returns number representing last page of the list', () => {
expect(wrapper.vm.lastPage).toBe(2);
});
});
describe('loaderCount', () => {
it('returns value of DEFAULT_PAGE_SIZE when current page is not the last page total requirements are more than DEFAULT_PAGE_SIZE', () => {
expect(wrapper.vm.loaderCount).toBe(2);
});
it('returns value of remainder requirements for last page when current page is the last page total requirements are more than DEFAULT_PAGE_SIZE', () => {
wrapper.setProps({
currentPage: 2,
});
return wrapper.vm.$nextTick(() => {
expect(wrapper.vm.loaderCount).toBe(1);
});
});
it('returns value DEFAULT_PAGE_SIZE when current page is the last page total requirements are less than DEFAULT_PAGE_SIZE', () => {
wrapper.setProps({
currentPage: 1,
currentTabCount: 1,
});
return wrapper.vm.$nextTick(() => {
expect(wrapper.vm.loaderCount).toBe(1);
});
});
});
});
describe('template', () => {
it('renders gl-skeleton-loading component based on loaderCount', () => {
const loaders = wrapper.find('.requirements-list-loading').findAll(GlSkeletonLoading);
expect(loaders.length).toBe(2);
expect(loaders.at(0).props('lines')).toBe(2);
});
});
});
import { shallowMount } from '@vue/test-utils';
import { GlLoadingIcon } from '@gitlab/ui';
import { GlPagination } from '@gitlab/ui';
import RequirementsRoot from 'ee/requirements/components/requirements_root.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';
import { FilterState } from 'ee/requirements/constants';
import { mockRequirements } from '../mock_data';
import {
FilterState,
mockRequirementsOpen,
mockRequirementsCount,
mockPageInfo,
} from '../mock_data';
jest.mock('ee/requirements/constants', () => ({
DEFAULT_PAGE_SIZE: 2,
}));
const createComponent = ({
projectPath = 'gitlab-org/gitlab-shell',
filterBy = FilterState.opened,
requirementsCount = mockRequirementsCount,
showCreateRequirement = false,
emptyStatePath = '/assets/illustrations/empty-state/requirements.svg',
loading = false,
......@@ -19,6 +29,7 @@ const createComponent = ({
propsData: {
projectPath,
filterBy,
requirementsCount,
showCreateRequirement,
emptyStatePath,
},
......@@ -27,6 +38,9 @@ const createComponent = ({
queries: {
requirements: {
loading,
list: [],
pageInfo: {},
count: {},
},
},
},
......@@ -44,33 +58,189 @@ describe('RequirementsRoot', () => {
wrapper.destroy();
});
describe('computed', () => {
describe('totalRequirements', () => {
it('returns number representing total requirements for current tab', () => {
expect(wrapper.vm.totalRequirements).toBe(mockRequirementsCount.OPENED);
});
});
describe('showPaginationControls', () => {
it('returns `true` when totalRequirements is more than default page size', () => {
wrapper.setData({
requirements: {
list: mockRequirementsOpen,
count: mockRequirementsCount,
pageInfo: mockPageInfo,
},
});
return wrapper.vm.$nextTick(() => {
expect(wrapper.vm.showPaginationControls).toBe(true);
});
});
it('returns `false` when totalRequirements is less than default page size', () => {
wrapper.setData({
requirements: {
list: [mockRequirementsOpen[0]],
count: {
...mockRequirementsCount,
OPENED: 1,
},
pageInfo: mockPageInfo,
},
});
return wrapper.vm.$nextTick(() => {
expect(wrapper.vm.showPaginationControls).toBe(false);
});
});
});
describe('prevPage', () => {
it('returns number representing previous page based on currentPage value', () => {
wrapper.setData({
currentPage: 3,
});
return wrapper.vm.$nextTick(() => {
expect(wrapper.vm.prevPage).toBe(2);
});
});
});
describe('nextPage', () => {
it('returns number representing next page based on currentPage value', () => {
expect(wrapper.vm.nextPage).toBe(2);
});
it('returns `null` when currentPage is already last page', () => {
wrapper.setData({
currentPage: 2,
});
return wrapper.vm.$nextTick(() => {
expect(wrapper.vm.nextPage).toBeNull();
});
});
});
});
describe('methods', () => {
describe('updateUrl', () => {
it('updates window URL with query params `page` and `prev`', () => {
wrapper.vm.updateUrl({
page: 2,
prev: mockPageInfo.startCursor,
});
expect(global.window.location.href).toContain(`?page=2&prev=${mockPageInfo.startCursor}`);
});
it('updates window URL with query params `page` and `next`', () => {
wrapper.vm.updateUrl({
page: 1,
next: mockPageInfo.endCursor,
});
expect(global.window.location.href).toContain(`?page=1&next=${mockPageInfo.endCursor}`);
});
});
describe('handlePageChange', () => {
beforeEach(() => {
jest.spyOn(wrapper.vm, 'updateUrl').mockImplementation(jest.fn());
wrapper.setData({
requirements: {
list: mockRequirementsOpen,
count: mockRequirementsCount,
pageInfo: mockPageInfo,
},
});
return wrapper.vm.$nextTick();
});
it('calls `updateUrl` with `page` and `next` params when value of page is `2`', () => {
wrapper.vm.handlePageChange(2);
expect(wrapper.vm.updateUrl).toHaveBeenCalledWith({
page: 2,
prev: '',
next: mockPageInfo.endCursor,
});
});
it('calls `updateUrl` with `page` and `next` params when value of page is `1`', () => {
wrapper.setData({
currentPage: 2,
});
return wrapper.vm.$nextTick(() => {
wrapper.vm.handlePageChange(1);
expect(wrapper.vm.updateUrl).toHaveBeenCalledWith({
page: 1,
prev: mockPageInfo.startCursor,
next: '',
});
});
});
});
});
describe('template', () => {
it('renders component container element with class `requirements-list-container`', () => {
expect(wrapper.classes()).toContain('requirements-list-container');
});
it('renders empty state query results are empty', () => {
it('renders empty state when query results are empty', () => {
expect(wrapper.find(RequirementsEmptyState).exists()).toBe(true);
});
it('renders loading icon when query results are still being loaded', () => {
it('renders requirements-loading component when query results are still being loaded', () => {
const wrapperLoading = createComponent({ loading: true });
expect(wrapperLoading.find(GlLoadingIcon).exists()).toBe(true);
expect(wrapperLoading.find(RequirementsLoading).isVisible()).toBe(true);
wrapperLoading.destroy();
});
it('renders requirement items for all the requirements', () => {
wrapper.setData({
requirements: mockRequirements,
requirements: {
list: mockRequirementsOpen,
count: mockRequirementsCount,
pageInfo: mockPageInfo,
},
});
return wrapper.vm.$nextTick(() => {
const itemsContainer = wrapper.find('ul.requirements-list');
expect(itemsContainer.exists()).toBe(true);
expect(itemsContainer.findAll(RequirementItem).length).toBe(mockRequirements.length);
expect(itemsContainer.findAll(RequirementItem)).toHaveLength(mockRequirementsOpen.length);
});
});
it('renders pagination controls', () => {
wrapper.setData({
requirements: {
list: mockRequirementsOpen,
count: mockRequirementsCount,
pageInfo: mockPageInfo,
},
});
return wrapper.vm.$nextTick(() => {
const pagination = wrapper.find(GlPagination);
expect(pagination.exists()).toBe(true);
expect(pagination.props('value')).toBe(1);
expect(pagination.props('perPage')).toBe(2); // We're mocking this page size
expect(pagination.props('align')).toBe('center');
});
});
});
......
......@@ -40,4 +40,35 @@ export const requirement3 = {
author: mockAuthor,
};
export const mockRequirements = [requirement1, requirement2, requirement3];
export const requirementArchived = {
iid: '23',
title: 'Cuius quidem, quoniam Stoicus fuit',
createdAt: '2020-03-31T13:31:40Z',
updatedAt: '2020-03-31T13:31:40Z',
state: 'ARCHIVED',
userPermissions: mockUserPermissions,
author: mockAuthor,
};
export const mockRequirementsOpen = [requirement1, requirement2, requirement3];
export const mockRequirementsArchived = [requirementArchived];
export const mockRequirementsAll = [...mockRequirementsOpen, ...mockRequirementsArchived];
export const mockRequirementsCount = {
OPENED: 3,
ARCHIVED: 1,
ALL: 4,
};
export const FilterState = {
opened: 'OPENED',
archived: 'ARCHIVED',
all: 'ALL',
};
export const mockPageInfo = {
startCursor: 'eyJpZCI6IjI1IiwiY3JlYXRlZF9hdCI6IjIwMjAtMDMtMzEgMTM6MzI6MTQgVVRDIn0',
endCursor: 'eyJpZCI6IjIxIiwiY3JlYXRlZF9hdCI6IjIwMjAtMDMtMzEgMTM6MzE6MTUgVVRDIn0',
};
......@@ -18342,6 +18342,9 @@ 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 ""
......
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