Commit 9849f50f authored by Kushal Pandya's avatar Kushal Pandya

Merge branch 'psi-iteration-report-issues' into 'master'

Show issues on iteration report page

See merge request gitlab-org/gitlab!34379
parents 27bdfb89 31a81957
fragment User on User {
id
avatarUrl
name
username
webUrl
}
......@@ -92,3 +92,5 @@ module Resolvers
end
end
end
Resolvers::IssuesResolver.prepend_if_ee('::EE::Resolvers::IssuesResolver')
# frozen_string_literal: true
module Types
# rubocop: disable Graphql/AuthorizeTypes
class IssueConnectionType < GraphQL::Types::Relay::BaseConnection
field :count, Integer, null: false,
description: 'Total count of collection'
def count
object.items.size
end
end
end
......@@ -4,6 +4,8 @@ module Types
class IssueType < BaseObject
graphql_name 'Issue'
connection_type_class(Types::IssueConnectionType)
implements(Types::Notes::NoteableType)
authorize :read_issue
......
......@@ -4370,6 +4370,11 @@ type EpicIssue implements Noteable {
The connection type for EpicIssue.
"""
type EpicIssueConnection {
"""
Total count of collection
"""
count: Int!
"""
A list of edges.
"""
......@@ -5043,6 +5048,11 @@ type Group {
"""
iids: [String!]
"""
Iterations applied to the issue
"""
iterationId: [ID]
"""
Labels applied to this issue
"""
......@@ -5967,6 +5977,11 @@ type Issue implements Noteable {
The connection type for Issue.
"""
type IssueConnection {
"""
Total count of collection
"""
count: Int!
"""
A list of edges.
"""
......@@ -9199,6 +9214,11 @@ type Project {
"""
iids: [String!]
"""
Iterations applied to the issue
"""
iterationId: [ID]
"""
Labels applied to this issue
"""
......@@ -9294,6 +9314,11 @@ type Project {
"""
iids: [String!]
"""
Iterations applied to the issue
"""
iterationId: [ID]
"""
Labels applied to this issue
"""
......
......@@ -12216,6 +12216,24 @@
"name": "EpicIssueConnection",
"description": "The connection type for EpicIssue.",
"fields": [
{
"name": "count",
"description": "Total count of collection",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "edges",
"description": "A list of edges.",
......@@ -14041,6 +14059,20 @@
},
"defaultValue": "created_desc"
},
{
"name": "iterationId",
"description": "Iterations applied to the issue",
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "after",
"description": "Returns the elements in the list that come after the specified cursor.",
......@@ -16428,6 +16460,24 @@
"name": "IssueConnection",
"description": "The connection type for Issue.",
"fields": [
{
"name": "count",
"description": "Total count of collection",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "edges",
"description": "A list of edges.",
......@@ -27439,6 +27489,20 @@
"ofType": null
},
"defaultValue": "created_desc"
},
{
"name": "iterationId",
"description": "Iterations applied to the issue",
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"defaultValue": null
}
],
"type": {
......@@ -27619,6 +27683,20 @@
},
"defaultValue": "created_desc"
},
{
"name": "iterationId",
"description": "Iterations applied to the issue",
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "after",
"description": "Returns the elements in the list that come after the specified cursor.",
......@@ -11,6 +11,7 @@ import {
import { formatDate } from '~/lib/utils/datetime_utility';
import { __ } from '~/locale';
import IterationForm from './iteration_form.vue';
import IterationReportTabs from './iteration_report_tabs.vue';
import query from '../queries/group_iteration.query.graphql';
const iterationStates = {
......@@ -29,6 +30,7 @@ export default {
GlNewDropdown,
GlNewDropdownItem,
IterationForm,
IterationReportTabs,
},
apollo: {
group: {
......@@ -154,6 +156,7 @@ export default {
</div>
<h3 ref="title" class="page-title">{{ iteration.title }}</h3>
<div ref="description" v-html="iteration.description"></div>
<iteration-report-tabs :group-path="groupPath" :iteration-id="iteration.id" />
</template>
</div>
</template>
<script>
import {
GlAlert,
GlAvatar,
GlBadge,
GlLink,
GlLoadingIcon,
GlPagination,
GlTab,
GlTabs,
GlTable,
GlTooltipDirective,
} from '@gitlab/ui';
import { __, sprintf } from '~/locale';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import query from '../queries/iteration_issues.query.graphql';
const states = {
opened: 'opened',
closed: 'closed',
};
const pageSize = 20;
export default {
fields: [
{
key: 'title',
label: __('Title'),
class: 'gl-bg-transparent! gl-border-b-1',
},
{
key: 'status',
label: __('Status'),
class: 'gl-bg-transparent! gl-text-truncate',
thClass: 'gl-w-eighth',
},
{
key: 'assignees',
label: __('Assignees'),
class: 'gl-bg-transparent! gl-text-right',
thClass: 'gl-w-eighth',
},
],
components: {
GlAlert,
GlAvatar,
GlBadge,
GlLink,
GlLoadingIcon,
GlPagination,
GlTab,
GlTabs,
GlTable,
},
directives: {
GlTooltip: GlTooltipDirective,
},
apollo: {
issues: {
query,
variables() {
return this.queryVariables;
},
update(data) {
const { nodes: issues = [], count, pageInfo = {} } = data?.group?.issues || {};
const list = issues.map(issue => ({
...issue,
labels: issue?.labels?.nodes || [],
assignees: issue?.assignees?.nodes || [],
}));
return {
pageInfo,
list,
count,
};
},
error() {
this.error = __('Error loading issues');
},
},
},
props: {
groupPath: {
type: String,
required: true,
},
iterationId: {
type: String,
required: true,
},
},
data() {
return {
issues: {
list: [],
pageInfo: {
hasNextPage: true,
hasPreviousPage: false,
},
},
error: '',
pagination: {
currentPage: 1,
},
};
},
computed: {
queryVariables() {
const vars = {
groupPath: this.groupPath,
id: getIdFromGraphQLId(this.iterationId),
};
if (this.pagination.beforeCursor) {
vars.beforeCursor = this.pagination.beforeCursor;
vars.lastPageSize = pageSize;
} else {
vars.afterCursor = this.pagination.afterCursor;
vars.firstPageSize = pageSize;
}
return vars;
},
prevPage() {
return Number(this.issues.pageInfo.hasPreviousPage);
},
nextPage() {
return Number(this.issues.pageInfo.hasNextPage);
},
},
methods: {
tooltipText(assignee) {
return sprintf(__('Assigned to %{assigneeName}'), {
assigneeName: assignee.name,
});
},
issueState(state, assigneeCount) {
if (state === states.opened && assigneeCount === 0) {
return __('Open');
}
if (state === states.opened && assigneeCount > 0) {
return __('In progress');
}
return __('Closed');
},
handlePageChange(page) {
const { startCursor, endCursor } = this.issues.pageInfo;
if (page > this.pagination.currentPage) {
this.pagination = {
afterCursor: endCursor,
currentPage: page,
};
} else {
this.pagination = {
beforeCursor: startCursor,
currentPage: page,
};
}
},
},
};
</script>
<template>
<gl-tabs>
<gl-alert v-if="error" variant="danger" @dismiss="error = ''">
{{ error }}
</gl-alert>
<gl-tab title="Issues">
<template #title>
<span>{{ __('Issues') }}</span
><gl-badge class="ml-2" variant="neutral">{{ issues.count }}</gl-badge>
</template>
<gl-loading-icon v-if="$apollo.queries.issues.loading" class="gl-my-9" size="md" />
<gl-table
v-else
:items="issues.list"
:fields="$options.fields"
:empty-text="__('No iterations found')"
:show-empty="true"
fixed
stacked="sm"
>
<template #cell(title)="{ item: { iid, title, webUrl } }">
<div class="gl-text-truncate">
<gl-link class="gl-text-gray-900 gl-font-weight-bold" :href="webUrl">{{
title
}}</gl-link>
<!-- TODO: add references.relative (project name) -->
<!-- Depends on https://gitlab.com/gitlab-org/gitlab/-/issues/222763 -->
<div class="gl-text-secondary">#{{ iid }}</div>
</div>
</template>
<template #cell(status)="{ item: { state, assignees = [] } }">
<span class="gl-w-6 gl-flex-shrink-0">{{ issueState(state, assignees.length) }}</span>
</template>
<template #cell(assignees)="{ item: { assignees } }">
<span class="assignee-icon gl-w-6">
<span
v-for="assignee in assignees"
:key="assignee.username"
v-gl-tooltip="tooltipText(assignee)"
>
<gl-avatar :src="assignee.avatarUrl" :size="16" />
</span>
</span>
</template>
</gl-table>
<div class="mt-3">
<gl-pagination
:value="pagination.currentPage"
:prev-page="prevPage"
:next-page="nextPage"
align="center"
class="gl-pagination gl-mt-3"
@input="handlePageChange"
/>
</div>
</gl-tab>
</gl-tabs>
</template>
#import "~/graphql_shared/fragments/user.fragment.graphql"
#import "~/graphql_shared/fragments/pageInfo.fragment.graphql"
query GroupIteration(
$groupPath: ID!
$id: ID!
$beforeCursor: String = ""
$afterCursor: String = ""
$firstPageSize: Int
$lastPageSize: Int
) {
group(fullPath: $groupPath) {
issues(
iterationId: [$id]
before: $beforeCursor
after: $afterCursor
first: $firstPageSize
last: $lastPageSize
) {
count
pageInfo {
...PageInfo
}
nodes {
iid
title
webUrl
state
assignees {
nodes {
...User
}
}
}
}
}
}
......@@ -18,7 +18,8 @@ module EE
override :filter_items
def filter_items(items)
issues = by_weight(super)
by_epic(issues)
issues = by_epic(issues)
by_iteration(issues)
end
private
......@@ -61,5 +62,18 @@ module EE
items.in_epics(params.epics)
end
end
def by_iteration(items)
return items unless params.iterations
case params.iterations.to_s.downcase
when ::IssuableFinder::Params::FILTER_NONE
items.no_iteration
when ::IssuableFinder::Params::FILTER_ANY
items.any_iteration
else
items.in_iterations(params.iterations)
end
end
end
end
......@@ -50,6 +50,10 @@ module EE
params[:epic_id]
end
end
def iterations
params[:iteration_id]
end
end
end
end
# frozen_string_literal: true
module EE
module Resolvers
module IssuesResolver
extend ActiveSupport::Concern
prepended do
argument :iteration_id, ::GraphQL::ID_TYPE.to_list_type,
required: false,
description: 'Iterations applied to the issue'
end
end
end
end
......@@ -25,6 +25,9 @@ module EE
issue_ids = EpicIssue.where(epic_id: epics).select(:issue_id)
id_in(issue_ids)
end
scope :no_iteration, -> { where(sprint_id: nil) }
scope :any_iteration, -> { where.not(sprint_id: nil) }
scope :in_iterations, ->(iterations) { where(sprint_id: iterations) }
scope :on_status_page, -> do
joins(project: :status_page_setting)
.where(status_page_settings: { enabled: true })
......
......@@ -5,11 +5,19 @@ require 'spec_helper'
RSpec.describe 'User views iteration' do
let_it_be(:now) { Time.now }
let_it_be(:group) { create(:group) }
let_it_be(:iteration) { create(:iteration, :skip_future_date_validation, group: group, start_date: now - 1.day, due_date: now) }
let_it_be(:project) { create(:project, group: group) }
let_it_be(:user) { create(:group_member, :maintainer, user: create(:user), group: group ).user }
let_it_be(:iteration) { create(:iteration, :skip_future_date_validation, iid: 1, id: 2, group: group, title: 'Correct Iteration', start_date: now - 1.day, due_date: now) }
let_it_be(:other_iteration) { create(:iteration, :skip_future_date_validation, iid: 2, id: 1, group: group, title: 'Wrong Iteration', start_date: now - 4.days, due_date: now - 3.days) }
let_it_be(:issue) { create(:issue, project: project, iteration: iteration) }
let_it_be(:assigned_issue) { create(:issue, project: project, iteration: iteration, assignees: [user]) }
let_it_be(:closed_issue) { create(:closed_issue, project: project, iteration: iteration) }
let_it_be(:other_issue) { create(:issue, project: project, iteration: other_iteration) }
context 'with license' do
before do
stub_licensed_features(iterations: true)
sign_in(user)
end
context 'view an iteration', :js do
......@@ -23,6 +31,13 @@ RSpec.describe 'User views iteration' do
expect(page).to have_content(iteration.start_date.strftime('%b %-d, %Y'))
expect(page).to have_content(iteration.due_date.strftime('%b %-d, %Y'))
end
it 'shows correct issues for issue' do
expect(page).to have_content(issue.title)
expect(page).to have_content(assigned_issue.title)
expect(page).to have_content(closed_issue.title)
expect(page).not_to have_content(other_issue.title)
end
end
end
end
......@@ -8,6 +8,8 @@ RSpec.describe IssuesFinder do
include_context 'IssuesFinder#execute context'
context 'scope: all' do
let_it_be(:group) { create(:group) }
let(:scope) { 'all' }
describe 'filter by weight' do
......@@ -76,8 +78,6 @@ RSpec.describe IssuesFinder do
end
context 'filter by epic' do
let_it_be(:group) { create(:group) }
let_it_be(:epic_1) { create(:epic, group: group) }
let_it_be(:epic_2) { create(:epic, group: group) }
let_it_be(:sub_epic) { create(:epic, group: group, parent: epic_1) }
......@@ -122,6 +122,54 @@ RSpec.describe IssuesFinder do
end
end
end
context 'filter by iteration' do
let_it_be(:iteration_1) { create(:iteration, group: group) }
let_it_be(:iteration_2) { create(:iteration, group: group) }
let_it_be(:iteration_1_issue) { create(:issue, project: project1, iteration: iteration_1) }
let_it_be(:iteration_2_issue) { create(:issue, project: project1, iteration: iteration_2) }
context 'filter issues with no iteration' do
let(:params) { { iteration_id: ::IssuableFinder::Params::FILTER_NONE } }
it 'returns all issues without iterations' do
expect(issues).to contain_exactly(issue1, issue2, issue3, issue4)
end
end
context 'filter issues with any iteration' do
let(:params) { { iteration_id: ::IssuableFinder::Params::FILTER_ANY } }
it 'returns filtered issues' do
expect(issues).to contain_exactly(iteration_1_issue, iteration_2_issue)
end
end
context 'filter issues by iteration' do
let(:params) { { iteration_id: iteration_1.id } }
it 'returns all issues with the iteration' do
expect(issues).to contain_exactly(iteration_1_issue)
end
end
context 'filter issues by multiple iterations' do
let(:params) { { iteration_id: [iteration_1.id, iteration_2.id] } }
it 'returns all issues with the iteration' do
expect(issues).to contain_exactly(iteration_1_issue, iteration_2_issue)
end
end
context 'without iteration_id param' do
let(:params) { { iteration_id: nil } }
it 'returns unfiltered issues' do
expect(issues).to contain_exactly(issue1, issue2, issue3, issue4, iteration_1_issue, iteration_2_issue)
end
end
end
end
end
......
......@@ -58,6 +58,7 @@ describe('Iterations tabs', () => {
describe('item loaded', () => {
const iteration = {
title: 'June week 1',
id: 'gid://gitlab/Iteration/2',
description: 'The first week of June',
startDate: '2020-06-02',
dueDate: '2020-06-08',
......
import IterationReportTabs from 'ee/iterations/components/iteration_report_tabs.vue';
import { mount } from '@vue/test-utils';
import { GlAlert, GlAvatar, GlLoadingIcon, GlPagination, GlTable, GlTab } from '@gitlab/ui';
describe('Iterations report tabs', () => {
let wrapper;
const id = 3;
const groupPath = 'gitlab-org';
const defaultProps = {
groupPath,
iterationId: `gid://gitlab/Iteration/${id}`,
};
const mountComponent = ({ props = defaultProps, loading = false, data = {} } = {}) => {
wrapper = mount(IterationReportTabs, {
propsData: props,
data() {
return data;
},
mocks: {
$apollo: {
queries: { issues: { loading } },
},
},
stubs: {
GlAvatar,
GlTab,
GlTable,
},
});
};
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
it('shows spinner while loading', () => {
mountComponent({
loading: true,
});
expect(wrapper.contains(GlLoadingIcon)).toBe(true);
expect(wrapper.contains(GlTable)).toBe(false);
});
it('shows iterations list when not loading', () => {
mountComponent({
loading: false,
});
expect(wrapper.contains(GlLoadingIcon)).toBe(false);
expect(wrapper.contains(GlTable)).toBe(true);
expect(wrapper.text()).toContain('No iterations found');
});
it('shows error in a gl-alert', () => {
const error = 'Oh no!';
mountComponent({
data: {
error,
},
});
expect(wrapper.find(GlAlert).text()).toContain(error);
});
describe('with issues', () => {
const pageSize = 20;
const totalIssues = pageSize + 1;
const assignees = Array(totalIssues)
.fill(null)
.map((_, i) => ({
id: i,
name: `User ${i}`,
username: `user${i}`,
state: 'active',
avatarUrl: 'http://invalid/avatar.png',
webUrl: `https://localhost:3000/user${i}`,
}));
const issues = Array(totalIssues)
.fill(null)
.map((_, i) => ({
id: i,
title: `Issue ${i}`,
assignees: assignees.slice(0, i),
}));
const findIssues = () => wrapper.findAll('table tbody tr');
const findAssigneesForIssue = index =>
findIssues()
.at(index)
.findAll(GlAvatar);
beforeEach(() => {
mountComponent();
wrapper.setData({
issues: {
list: issues,
pageInfo: {
hasNextPage: true,
hasPreviousPage: false,
startCursor: 'first-item',
endCursor: 'last-item',
},
count: issues.length,
},
});
});
it('shows issue list in table', () => {
expect(wrapper.contains(GlTable)).toBe(true);
expect(findIssues()).toHaveLength(issues.length);
});
it('shows assignees', () => {
expect(findAssigneesForIssue(0)).toHaveLength(0);
expect(findAssigneesForIssue(1)).toHaveLength(1);
expect(findAssigneesForIssue(10)).toHaveLength(10);
});
describe('pagination', () => {
const findPagination = () => wrapper.find(GlPagination);
const setPage = page => {
findPagination().vm.$emit('input', page);
return findPagination().vm.$nextTick();
};
it('passes prev, next, and current page props', () => {
expect(findPagination().exists()).toBe(true);
expect(findPagination().props()).toEqual(
expect.objectContaining({
value: wrapper.vm.pagination.currentPage,
prevPage: wrapper.vm.prevPage,
nextPage: wrapper.vm.nextPage,
}),
);
});
it('updates query variables when going to previous page', () => {
return setPage(1).then(() => {
expect(wrapper.vm.queryVariables).toEqual({
beforeCursor: 'first-item',
groupPath,
id,
lastPageSize: 20,
});
});
});
it('updates query variables when going to next page', () => {
return setPage(2).then(() => {
expect(wrapper.vm.queryVariables).toEqual({
afterCursor: 'last-item',
groupPath,
id,
firstPageSize: 20,
});
});
});
});
});
});
......@@ -6,10 +6,15 @@ RSpec.describe Resolvers::IssuesResolver do
include GraphqlHelpers
let_it_be(:current_user) { create(:user) }
let_it_be(:project) { create(:project) }
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, namespace: group) }
context "with a project" do
describe '#resolve' do
before do
project.add_developer(current_user)
end
describe 'sorting' do
context 'when sorting by weight' do
let_it_be(:weight_issue1) { create(:issue, project: project, weight: 5) }
......@@ -17,10 +22,6 @@ RSpec.describe Resolvers::IssuesResolver do
let_it_be(:weight_issue3) { create(:issue, project: project, weight: 1) }
let_it_be(:weight_issue4) { create(:issue, project: project, weight: nil) }
before do
project.add_developer(current_user)
end
it 'sorts issues ascending' do
expect(resolve_issues(sort: :weight_asc)).to eq [weight_issue3, weight_issue1, weight_issue4, weight_issue2]
end
......@@ -30,6 +31,16 @@ RSpec.describe Resolvers::IssuesResolver do
end
end
end
describe 'filtering by iteration' do
let_it_be(:iteration1) { create(:iteration, group: group) }
let_it_be(:issue_with_iteration) { create(:issue, project: project, iteration: iteration1) }
let_it_be(:issue_without_iteration) { create(:issue, project: project) }
it 'returns issues with iteration' do
expect(resolve_issues(iteration_id: iteration1.id)).to eq [issue_with_iteration]
end
end
end
end
......
......@@ -130,6 +130,39 @@ RSpec.describe Issue do
end
end
end
context 'iterations' do
let_it_be(:iteration1) { create(:iteration) }
let_it_be(:iteration2) { create(:iteration) }
let_it_be(:iteration1_issue) { create(:issue, iteration: iteration1) }
let_it_be(:iteration2_issue) { create(:issue, iteration: iteration2) }
let_it_be(:issue_no_iteration) { create(:issue) }
before do
stub_licensed_features(iterations: true)
end
describe '.no_iteration' do
it 'returns only issues without an iteration assigned' do
expect(described_class.count).to eq 3
expect(described_class.no_iteration).to eq [issue_no_iteration]
end
end
describe '.any_iteration' do
it 'returns only issues with an iteration assigned' do
expect(described_class.count).to eq 3
expect(described_class.any_iteration).to eq [iteration1_issue, iteration2_issue]
end
end
describe '.in_iterations' do
it 'returns only issues in selected iterations' do
expect(described_class.count).to eq 3
expect(described_class.in_iterations([iteration1])).to eq [iteration1_issue]
end
end
end
end
describe 'validations' do
......
......@@ -3229,6 +3229,9 @@ msgstr ""
msgid "Assigned Merge Requests"
msgstr ""
msgid "Assigned to %{assigneeName}"
msgstr ""
msgid "Assigned to %{assignee_name}"
msgstr ""
......@@ -9366,6 +9369,9 @@ msgstr ""
msgid "Error loading file viewer."
msgstr ""
msgid "Error loading issues"
msgstr ""
msgid "Error loading last commit."
msgstr ""
......@@ -15715,6 +15721,9 @@ msgstr ""
msgid "No iteration"
msgstr ""
msgid "No iterations found"
msgstr ""
msgid "No iterations to show"
msgstr ""
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe GitlabSchema.types['IssueConnection'] do
it 'has the expected fields' do
expected_fields = %i[count page_info edges nodes]
expect(described_class).to have_graphql_fields(*expected_fields)
end
end
......@@ -22,6 +22,104 @@ RSpec.describe GitlabSchema.types['Issue'] do
end
end
describe 'pagination and count' do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :public) }
let_it_be(:now) { Time.now.change(usec: 0) }
let_it_be(:issues) { create_list(:issue, 10, project: project, created_at: now) }
let(:count_path) { %w(data project issues count) }
let(:page_size) { 3 }
let(:query) do
<<~GRAPHQL
query project($fullPath: ID!, $first: Int, $after: String) {
project(fullPath: $fullPath) {
issues(first: $first, after: $after) {
count
edges {
node {
iid
}
}
pageInfo {
endCursor
hasNextPage
}
}
}
}
GRAPHQL
end
subject do
GitlabSchema.execute(
query,
context: { current_user: user },
variables: {
fullPath: project.full_path,
first: page_size
}
).to_h
end
context 'when user does not have the permission' do
it 'returns no data' do
allow(Ability).to receive(:allowed?).with(user, :read_project, project).and_return(false)
expect(subject.dig(:data, :project)).to eq(nil)
end
end
context 'count' do
let(:end_cursor) { %w(data project issues pageInfo endCursor) }
let(:issues_edges) { %w(data project issues edges) }
it 'returns total count' do
expect(subject.dig(*count_path)).to eq(issues.count)
end
it 'total count does not change between pages' do
old_count = subject.dig(*count_path)
new_cursor = subject.dig(*end_cursor)
new_page = GitlabSchema.execute(
query,
context: { current_user: user },
variables: {
fullPath: project.full_path,
first: page_size,
after: new_cursor
}
).to_h
new_count = new_page.dig(*count_path)
expect(old_count).to eq(new_count)
end
context 'pagination' do
let(:page_size) { 9 }
it 'returns new ids during pagination' do
old_edges = subject.dig(*issues_edges)
new_cursor = subject.dig(*end_cursor)
new_edges = GitlabSchema.execute(
query,
context: { current_user: user },
variables: {
fullPath: project.full_path,
first: page_size,
after: new_cursor
}
).to_h.dig(*issues_edges)
expect(old_edges.count).to eq(9)
expect(new_edges.count).to eq(1)
end
end
end
end
describe "issue notes" do
let(:user) { create(:user) }
let(:project) { create(:project, :public) }
......
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