Commit 31a81957 authored by Simon Knox's avatar Simon Knox Committed by Kushal Pandya

Add basic iteration report view

Filtering iterations by ID, and display just info
No edit view for now
parent d30d93cb
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.
"""
......@@ -9153,6 +9168,11 @@ type Project {
"""
iids: [String!]
"""
Iterations applied to the issue
"""
iterationId: [ID]
"""
Labels applied to this issue
"""
......@@ -9248,6 +9268,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.",
......@@ -27282,6 +27332,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": {
......@@ -27462,6 +27526,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
......
......@@ -3226,6 +3226,9 @@ msgstr ""
msgid "Assigned Merge Requests"
msgstr ""
msgid "Assigned to %{assigneeName}"
msgstr ""
msgid "Assigned to %{assignee_name}"
msgstr ""
......@@ -9345,6 +9348,9 @@ msgstr ""
msgid "Error loading file viewer."
msgstr ""
msgid "Error loading issues"
msgstr ""
msgid "Error loading last commit."
msgstr ""
......@@ -15691,6 +15697,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