Commit 38bc21e3 authored by Miguel Rincon's avatar Miguel Rincon

Merge branch '328257-try-to-improve-loading-and-caching' into 'master'

Improve graphql full code quality report UX

See merge request gitlab-org/gitlab!72036
parents 7304f86c f5b268ee
<script>
import { mapActions, mapState, mapGetters } from 'vuex';
import { GlPagination, GlSkeletonLoader } from '@gitlab/ui';
import { componentNames } from 'ee/reports/components/issue_body';
import reportsMixin from 'ee/vue_shared/security_reports/mixins/reports_mixin';
import { n__, s__, sprintf } from '~/locale';
import ReportSection from '~/reports/components/report_section.vue';
import PaginationLinks from '~/vue_shared/components/pagination_links.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default {
components: {
ReportSection,
PaginationLinks,
GlSkeletonLoader,
GlPagination,
},
mixins: [reportsMixin, glFeatureFlagsMixin()],
mixins: [reportsMixin],
componentNames,
computed: {
...mapState(['isLoadingCodequality', 'loadingCodequalityFailed', 'pageInfo']),
...mapGetters(['codequalityIssues', 'codequalityIssueTotal']),
prevPage() {
return Math.max(this.pageInfo.currentPage - 1, 0);
},
nextPage() {
return this.pageInfo?.hasNextPage ? this.pageInfo.currentPage + 1 : null;
},
hasCodequalityIssues() {
return this.codequalityIssueTotal > 0;
},
codequalityText() {
const text = [];
const count = this.codequalityIssueTotal;
const { codequalityIssueTotal } = this;
if (count === 0) {
if (codequalityIssueTotal === 0) {
return s__('ciReport|No code quality issues found');
} else if (count > 0) {
} else if (codequalityIssueTotal > 0) {
return sprintf(s__('ciReport|Found %{issuesWithCount}'), {
issuesWithCount: n__('%d code quality issue', '%d code quality issues', count),
issuesWithCount: n__(
'%d code quality issue',
'%d code quality issues',
codequalityIssueTotal,
),
});
}
......@@ -76,27 +70,13 @@ export default {
:success-text="codequalityText"
:unresolved-issues="codequalityIssues"
:resolved-issues="[]"
:has-issues="hasCodequalityIssues && !isLoadingCodequality"
:has-issues="hasCodequalityIssues"
:component="$options.componentNames.CodequalityIssueBody"
class="codequality-report"
>
<template v-if="hasCodequalityIssues" #sub-heading>{{ $options.i18n.subHeading }}</template>
</report-section>
<div v-if="isLoadingCodequality" class="gl-p-4">
<gl-skeleton-loader :lines="50" />
</div>
<gl-pagination
v-if="glFeatures.graphqlCodeQualityFullReport"
:disabled="isLoadingCodequality"
:value="pageInfo.currentPage"
:prev-page="prevPage"
:next-page="nextPage"
align="center"
class="gl-mt-3"
@input="setPage"
/>
<pagination-links
v-else
:change="setPage"
:page-info="pageInfo"
class="d-flex justify-content-center gl-mt-3"
......
<script>
import { GlSkeletonLoader, GlInfiniteScroll, GlLoadingIcon, GlSprintf } from '@gitlab/ui';
import { once } from 'lodash';
import produce from 'immer';
import api from '~/api';
import { componentNames } from 'ee/reports/components/issue_body';
import reportsMixin from 'ee/vue_shared/security_reports/mixins/reports_mixin';
import { n__, s__, sprintf } from '~/locale';
import ReportSection from '~/reports/components/report_section.vue';
import CodequalityIssueBody from '~/reports/codequality_report/components/codequality_issue_body.vue';
import { parseCodeclimateMetrics } from '~/reports/codequality_report/store/utils/codequality_parser';
import getCodeQualityViolations from './graphql/queries/get_code_quality_violations.query.graphql';
import { PAGE_SIZE, VIEW_EVENT_NAME } from './store/constants';
export default {
components: {
ReportSection,
CodequalityIssueBody,
GlSkeletonLoader,
GlInfiniteScroll,
GlLoadingIcon,
GlSprintf,
},
mixins: [reportsMixin],
componentNames,
inject: ['projectPath', 'pipelineIid', 'blobPath'],
apollo: {
codequalityViolations: {
query: getCodeQualityViolations,
variables() {
return {
projectPath: this.projectPath,
iid: this.pipelineIid,
first: PAGE_SIZE,
};
},
update({
project: {
pipeline: { codeQualityReports: { nodes = [], pageInfo = {}, count = 0 } = {} } = {},
} = {},
}) {
return {
nodes,
parsedList: parseCodeclimateMetrics(nodes, this.blobPath),
count,
pageInfo,
};
},
error() {
this.errored = true;
},
watchLoading(isLoading) {
if (isLoading) {
this.trackViewEvent();
}
},
},
},
data() {
return {
codequalityViolations: {
nodes: [],
parsedList: [],
count: 0,
pageInfo: {},
},
errored: false,
};
},
computed: {
isLoading() {
return this.$apollo.queries.codequalityViolations.loading;
},
hasCodequalityViolations() {
return this.codequalityViolations.count > 0;
},
trackViewEvent() {
return once(() => {
api.trackRedisHllUserEvent(VIEW_EVENT_NAME);
});
},
codequalityText() {
const text = [];
const { count } = this.codequalityViolations;
if (count === 0) {
return s__('ciReport|No code quality issues found');
} else if (count > 0) {
return sprintf(s__('ciReport|Found %{issuesWithCount}'), {
issuesWithCount: n__('%d code quality issue', '%d code quality issues', count),
});
}
return text.join('');
},
codequalityStatus() {
return this.checkReportStatus(this.isLoading && !this.hasCodequalityViolations, this.errored);
},
},
i18n: {
subHeading: s__('ciReport|This report contains all Code Quality issues in the source branch.'),
loadingText: s__('ciReport|Loading Code Quality report'),
errorText: s__('ciReport|Failed to load Code Quality report'),
showingCount: s__('ciReport|Showing %{fetchedItems} of %{totalItems} items'),
},
methods: {
fetchMoreViolations() {
this.$apollo.queries.codequalityViolations
.fetchMore({
variables: {
first: PAGE_SIZE,
after: this.codequalityViolations.pageInfo.endCursor,
},
updateQuery: (previousResult, { fetchMoreResult }) => {
return produce(fetchMoreResult, (draftData) => {
draftData.project.pipeline.codeQualityReports.nodes = [
...previousResult.project.pipeline.codeQualityReports.nodes,
...draftData.project.pipeline.codeQualityReports.nodes,
];
});
},
})
.catch(() => {
this.errored = true;
});
},
},
};
</script>
<template>
<div>
<report-section
always-open
:status="codequalityStatus"
:loading-text="$options.i18n.loadingText"
:error-text="$options.i18n.errorText"
:success-text="codequalityText"
:unresolved-issues="codequalityViolations.parsedList"
:resolved-issues="[]"
:has-issues="hasCodequalityViolations"
:component="$options.componentNames.CodequalityIssueBody"
class="codequality-report"
>
<template v-if="hasCodequalityViolations" #sub-heading>{{
$options.i18n.subHeading
}}</template>
<template #body>
<gl-infinite-scroll
:max-list-height="500"
:fetched-items="codequalityViolations.parsedList.length"
:total-items="codequalityViolations.count"
@bottomReached="fetchMoreViolations"
>
<template #items>
<div class="report-block-container">
<template v-for="(issue, index) in codequalityViolations.parsedList">
<codequality-issue-body
:key="index"
:issue="issue"
class="report-block-list-issue"
/>
</template>
</div>
</template>
<template #default>
<div class="gl-mt-3">
<gl-loading-icon v-if="isLoading" />
<gl-sprintf v-else :message="$options.i18n.showingCount"
><template #fetchedItems>{{ codequalityViolations.parsedList.length }}</template
><template #totalItems>{{ codequalityViolations.count }}</template></gl-sprintf
>
</div>
</template>
</gl-infinite-scroll>
</template>
</report-section>
<div v-if="isLoading && !hasCodequalityViolations" class="report-block-container">
<gl-skeleton-loader :lines="36" />
</div>
</div>
</template>
#import "~/graphql_shared/fragments/pageInfo.fragment.graphql"
query getCodeQualityViolations($projectPath: ID!, $iid: ID!, $first: Int, $after: String) {
project(fullPath: $projectPath) {
pipeline(iid: $iid) {
codeQualityReports(first: $first, after: $after) {
count
edges {
node {
line
description
path
fingerprint
severity
}
nodes {
line
description
path
fingerprint
severity
}
pageInfo {
startCursor
endCursor
hasNextPage
...PageInfo
}
}
}
......
......@@ -4,30 +4,10 @@ import axios from '~/lib/utils/axios_utils';
import { s__ } from '~/locale';
import { parseCodeclimateMetrics } from '~/reports/codequality_report/store/utils/codequality_parser';
import getCodeQualityViolations from '../graphql/queries/get_code_quality_violations.query.graphql';
import { VIEW_EVENT_NAME } from './constants';
import * as types from './mutation_types';
import { gqClient } from './utils';
export const setPage = ({ state, commit, dispatch }, page) => {
if (gon.features?.graphqlCodeQualityFullReport) {
const { currentPage, startCursor, endCursor } = state.pageInfo;
if (page > currentPage) {
commit(types.SET_PAGE, {
after: endCursor,
currentPage: page,
});
} else {
commit(types.SET_PAGE, {
after: startCursor,
currentPage: page,
});
}
return dispatch('fetchReport');
}
return commit(types.SET_PAGE, { page });
};
export const setPage = ({ commit }, page) => commit(types.SET_PAGE, page);
export const requestReport = ({ commit }) => {
commit(types.REQUEST_REPORT);
......@@ -35,49 +15,24 @@ export const requestReport = ({ commit }) => {
Api.trackRedisHllUserEvent(VIEW_EVENT_NAME);
};
export const receiveReportSuccess = ({ state, commit }, data) => {
if (gon.features?.graphqlCodeQualityFullReport) {
const parsedIssues = parseCodeclimateMetrics(
data.edges.map((edge) => edge.node),
state.blobPath,
);
return commit(types.RECEIVE_REPORT_SUCCESS_GRAPHQL, { data, parsedIssues });
}
const parsedIssues = parseCodeclimateMetrics(data, state.blobPath);
return commit(types.RECEIVE_REPORT_SUCCESS, parsedIssues);
commit(types.RECEIVE_REPORT_SUCCESS, parsedIssues);
};
export const receiveReportError = ({ commit }, error) => commit(types.RECEIVE_REPORT_ERROR, error);
export const fetchReport = async ({ state, dispatch }) => {
try {
dispatch('requestReport');
if (!state.blobPath) throw new Error();
if (gon.features?.graphqlCodeQualityFullReport) {
const { projectPath, pipelineIid, pageInfo } = state;
const variables = {
projectPath,
iid: pipelineIid,
first: pageInfo.first,
after: pageInfo.after,
};
await gqClient
.query({
query: getCodeQualityViolations,
variables,
})
.then(({ data }) => {
dispatch('receiveReportSuccess', data.project?.pipeline?.codeQualityReports);
});
} else {
await axios.get(state.endpoint).then(({ data }) => {
dispatch('receiveReportSuccess', data);
export const fetchReport = ({ state, dispatch }) => {
dispatch('requestReport');
axios
.get(state.endpoint)
.then(({ data }) => {
if (!state.blobPath) throw new Error();
dispatch('receiveReportSuccess', data);
})
.catch((error) => {
dispatch('receiveReportError', error);
createFlash({
message: s__('ciReport|There was an error fetching the codequality report.'),
});
}
} catch (error) {
dispatch('receiveReportError', error);
createFlash({
message: s__('ciReport|There was an error fetching the codequality report.'),
});
}
};
export const codequalityIssues = (state) => {
if (gon.features?.graphqlCodeQualityFullReport) {
return state.codequalityIssues;
}
const { page, perPage } = state.pageInfo;
const start = (page - 1) * perPage;
return state.allCodequalityIssues.slice(start, start + perPage);
};
export const codequalityIssueTotal = (state) => {
if (gon.features?.graphqlCodeQualityFullReport) {
return state.pageInfo.count;
}
return state.allCodequalityIssues.length;
};
export const codequalityIssueTotal = (state) => state.allCodequalityIssues.length;
export const SET_PAGE = 'SET_PAGE';
export const REQUEST_REPORT = 'REQUEST_REPORT';
export const RECEIVE_REPORT_SUCCESS_GRAPHQL = 'RECEIVE_REPORT_SUCCESS_GRAPHQL';
export const RECEIVE_REPORT_SUCCESS = 'RECEIVE_REPORT_SUCCESS';
export const RECEIVE_REPORT_ERROR = 'RECEIVE_REPORT_ERROR';
......@@ -2,25 +2,16 @@ import { SEVERITY_SORT_ORDER } from './constants';
import * as types from './mutation_types';
export default {
[types.SET_PAGE](state, pageInfo) {
[types.SET_PAGE](state, page) {
Object.assign(state, {
pageInfo: Object.assign(state.pageInfo, pageInfo),
pageInfo: Object.assign(state.pageInfo, {
page,
}),
});
},
[types.REQUEST_REPORT](state) {
Object.assign(state, { isLoadingCodequality: true });
},
[types.RECEIVE_REPORT_SUCCESS_GRAPHQL](state, { data, parsedIssues }) {
Object.assign(state, {
isLoadingCodequality: false,
codequalityIssues: parsedIssues,
loadingCodequalityFailed: false,
pageInfo: Object.assign(state.pageInfo, {
count: data.count,
...data.pageInfo,
}),
});
},
[types.RECEIVE_REPORT_SUCCESS](state, allCodequalityIssues) {
Object.assign(state, {
isLoadingCodequality: false,
......@@ -38,12 +29,10 @@ export default {
Object.assign(state, {
isLoadingCodequality: false,
allCodequalityIssues: [],
codequalityIssues: [],
loadingCodequalityFailed: true,
codeQualityError,
pageInfo: Object.assign(state.pageInfo, {
total: 0,
count: 0,
}),
});
},
......
import { PAGE_SIZE } from './constants';
export default () => ({
projectPath: null,
pipelineIid: null,
endpoint: '',
allCodequalityIssues: [],
codequalityIssues: [],
isLoadingCodequality: false,
loadingCodequalityFailed: false,
codeQualityError: null,
......@@ -13,12 +10,5 @@ export default () => ({
page: 1,
perPage: PAGE_SIZE,
total: 0,
count: 0,
currentPage: 1,
startCursor: '',
endCursor: '',
first: PAGE_SIZE,
after: '',
hasNextPage: false,
},
});
import createGqClient from '~/lib/graphql';
export const gqClient = createGqClient();
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
import CodequalityReportApp from 'ee/codequality_report/codequality_report.vue';
import CodequalityReportAppGraphQL from 'ee/codequality_report/codequality_report_graphql.vue';
import createStore from 'ee/codequality_report/store';
import Translate from '~/vue_shared/translate';
Vue.use(Translate);
Vue.use(VueApollo);
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(),
});
export default () => {
const tabsElement = document.querySelector('.pipelines-tabs');
const codequalityTab = document.getElementById('js-pipeline-codequality-report');
const isGraphqlFeatureFlagEnabled = gon.features?.graphqlCodeQualityFullReport;
if (tabsElement && codequalityTab) {
const fetchReportAction = 'fetchReport';
......@@ -17,38 +26,68 @@ export default () => {
projectPath,
pipelineIid,
} = codequalityTab.dataset;
const store = createStore({
endpoint: codequalityReportDownloadPath,
blobPath,
projectPath,
pipelineIid,
});
const isCodequalityTabActive = Boolean(
document.querySelector('.pipelines-tabs > li > a.codequality-tab.active'),
);
if (isCodequalityTabActive) {
store.dispatch(fetchReportAction);
} else {
const tabClickHandler = (e) => {
if (e.target.className === 'codequality-tab') {
store.dispatch(fetchReportAction);
tabsElement.removeEventListener('click', tabClickHandler);
}
if (isGraphqlFeatureFlagEnabled) {
const vueOptions = {
el: codequalityTab,
apolloProvider,
components: {
CodequalityReportApp: CodequalityReportAppGraphQL,
},
provide: {
projectPath,
pipelineIid,
blobPath,
},
render: (createElement) => createElement('codequality-report-app'),
};
tabsElement.addEventListener('click', tabClickHandler);
}
if (isCodequalityTabActive) {
// eslint-disable-next-line no-new
new Vue(vueOptions);
} else {
const tabClickHandler = (e) => {
if (e.target.className === 'codequality-tab') {
// eslint-disable-next-line no-new
new Vue(vueOptions);
tabsElement.removeEventListener('click', tabClickHandler);
}
};
tabsElement.addEventListener('click', tabClickHandler);
}
} else {
const store = createStore({
endpoint: codequalityReportDownloadPath,
blobPath,
projectPath,
pipelineIid,
});
if (isCodequalityTabActive) {
store.dispatch(fetchReportAction);
} else {
const tabClickHandler = (e) => {
if (e.target.className === 'codequality-tab') {
store.dispatch(fetchReportAction);
tabsElement.removeEventListener('click', tabClickHandler);
}
};
// eslint-disable-next-line no-new
new Vue({
el: codequalityTab,
components: {
CodequalityReportApp,
},
store,
render: (createElement) => createElement('codequality-report-app'),
});
tabsElement.addEventListener('click', tabClickHandler);
}
// eslint-disable-next-line no-new
new Vue({
el: codequalityTab,
components: {
CodequalityReportApp,
},
store,
render: (createElement) => createElement('codequality-report-app'),
});
}
}
};
......@@ -214,18 +214,33 @@ RSpec.describe 'Pipeline', :js do
context 'with code quality artifact' do
before do
create(:ee_ci_build, :codequality, pipeline: pipeline)
visit codequality_report_project_pipeline_path(project, pipeline)
end
it 'shows code quality tab pane as active, quality issue with link to file, and events for data tracking' do
expect(page).to have_content('Code Quality')
expect(page).to have_css('#js-tab-codequality')
context 'when navigating directly to the code quality tab' do
before do
visit codequality_report_project_pipeline_path(project, pipeline)
end
expect(page).to have_content('Method `new_array` has 12 arguments (exceeds 4 allowed). Consider refactoring.')
expect(find_link('foo.rb:10')[:href]).to end_with(project_blob_path(project, File.join(pipeline.commit.id, 'foo.rb')) + '#L10')
it_behaves_like 'an active code quality tab'
end
context 'when starting from the pipeline tab' do
before do
visit project_pipeline_path(project, pipeline)
end
it 'shows the code quality tab as inactive' do
expect(page).to have_content('Code Quality')
expect(page).not_to have_css('#js-tab-codequality')
end
context 'when the code quality tab is clicked' do
before do
click_link 'Code Quality'
end
expect(page).to have_selector('[data-track-action="click_button"]')
expect(page).to have_selector('[data-track-label="get_codequality_report"]')
it_behaves_like 'an active code quality tab'
end
end
end
......@@ -257,6 +272,19 @@ RSpec.describe 'Pipeline', :js do
end
end
shared_examples_for 'an active code quality tab' do
it 'shows code quality tab pane as active, quality issue with link to file, and events for data tracking' do
expect(page).to have_content('Code Quality')
expect(page).to have_css('#js-tab-codequality')
expect(page).to have_content('Method `new_array` has 12 arguments (exceeds 4 allowed). Consider refactoring.')
expect(find_link('foo.rb:10')[:href]).to end_with(project_blob_path(project, File.join(pipeline.commit.id, 'foo.rb')) + '#L10')
expect(page).to have_selector('[data-track-action="click_button"]')
expect(page).to have_selector('[data-track-label="get_codequality_report"]')
end
end
context 'for a branch pipeline' do
let(:pipeline) { create(:ci_pipeline, project: project, ref: 'master', sha: project.commit.id) }
......@@ -278,6 +306,14 @@ RSpec.describe 'Pipeline', :js do
it_behaves_like 'full codequality report'
end
context 'with graphql feature flag disabled' do
let(:pipeline) { create(:ci_pipeline, project: project, ref: 'master', sha: project.commit.id) }
stub_feature_flags(graphql_code_quality_full_report: false)
it_behaves_like 'full codequality report'
end
end
private
......
import { GlInfiniteScroll } from '@gitlab/ui';
import { mount, createLocalVue } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import waitForPromises from 'helpers/wait_for_promises';
import createMockApollo from 'helpers/mock_apollo_helper';
import CodequalityReportApp from 'ee/codequality_report/codequality_report_graphql.vue';
import getCodeQualityViolations from 'ee/codequality_report/graphql/queries/get_code_quality_violations.query.graphql';
import { mockGetCodeQualityViolationsResponse, codeQualityViolations } from './mock_data';
const localVue = createLocalVue();
localVue.use(VueApollo);
describe('Codequality report app', () => {
let wrapper;
const createComponent = (
mockReturnValue = jest.fn().mockResolvedValue(mockGetCodeQualityViolationsResponse),
mountFn = mount,
) => {
const apolloProvider = createMockApollo([[getCodeQualityViolations, mockReturnValue]]);
wrapper = mountFn(CodequalityReportApp, {
localVue,
apolloProvider,
provide: {
projectPath: 'project-path',
pipelineIid: 'pipeline-iid',
blobPath: '/blob/path',
},
});
};
const findStatus = () => wrapper.find('.js-code-text');
const findSuccessIcon = () => wrapper.find('.js-ci-status-icon-success');
const findWarningIcon = () => wrapper.find('.js-ci-status-icon-warning');
const findInfiniteScroll = () => wrapper.find(GlInfiniteScroll);
afterEach(() => {
wrapper.destroy();
});
describe('when loading', () => {
beforeEach(() => {
createComponent(jest.fn().mockReturnValueOnce(new Promise(() => {})));
});
it('shows a loading state', () => {
expect(findStatus().text()).toBe('Loading Code Quality report');
});
});
describe('on error', () => {
beforeEach(() => {
createComponent(jest.fn().mockRejectedValueOnce(new Error('Error!')));
});
it('shows a warning icon and error message', () => {
expect(findWarningIcon().exists()).toBe(true);
expect(findStatus().text()).toBe('Failed to load Code Quality report');
});
});
describe('when there are codequality issues', () => {
beforeEach(() => {
createComponent(jest.fn().mockResolvedValue(mockGetCodeQualityViolationsResponse));
});
it('renders the codequality issues', () => {
const expectedIssueTotal = codeQualityViolations.count;
expect(findWarningIcon().exists()).toBe(true);
expect(findInfiniteScroll().exists()).toBe(true);
expect(findStatus().text()).toContain(`Found ${expectedIssueTotal} code quality issues`);
expect(findStatus().text()).toContain(
`This report contains all Code Quality issues in the source branch.`,
);
expect(wrapper.findAll('.report-block-list-issue')).toHaveLength(expectedIssueTotal);
});
it('renders a link to the line where the issue was found', () => {
const issueLink = wrapper.find('.report-block-list-issue a');
expect(issueLink.text()).toBe('foo.rb:10');
expect(issueLink.attributes('href')).toBe('/blob/path/foo.rb#L10');
});
it('loads the next page when the end of the list is reached', async () => {
jest
.spyOn(wrapper.vm.$apollo.queries.codequalityViolations, 'fetchMore')
.mockResolvedValue({});
findInfiniteScroll().vm.$emit('bottomReached');
await waitForPromises();
expect(wrapper.vm.$apollo.queries.codequalityViolations.fetchMore).toHaveBeenCalledWith(
expect.objectContaining({
variables: expect.objectContaining({
after: codeQualityViolations.pageInfo.endCursor,
}),
}),
);
});
});
describe('when there are no codequality issues', () => {
beforeEach(() => {
const emptyResponse = {
data: {
project: {
pipeline: {
codeQualityReports: {
...codeQualityViolations,
nodes: [],
count: 0,
},
},
},
},
};
createComponent(jest.fn().mockResolvedValue(emptyResponse));
});
it('shows a message that no codequality issues were found', () => {
expect(findSuccessIcon().exists()).toBe(true);
expect(findStatus().text()).toBe('No code quality issues found');
expect(wrapper.findAll('.report-block-list-issue')).toHaveLength(0);
});
});
});
import { GlPagination, GlSkeletonLoader } from '@gitlab/ui';
import { mount, shallowMount, createLocalVue } from '@vue/test-utils';
import { mount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
import CodequalityReportApp from 'ee/codequality_report/codequality_report.vue';
import PaginationLinks from '~/vue_shared/components/pagination_links.vue';
......@@ -39,9 +38,7 @@ describe('Codequality report app', () => {
const findStatus = () => wrapper.find('.js-code-text');
const findSuccessIcon = () => wrapper.find('.js-ci-status-icon-success');
const findWarningIcon = () => wrapper.find('.js-ci-status-icon-warning');
const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader);
const findOldPagination = () => wrapper.findComponent(PaginationLinks);
const findPagination = () => wrapper.findComponent(GlPagination);
const findPagination = () => wrapper.findComponent(PaginationLinks);
afterEach(() => {
wrapper.destroy();
......@@ -54,7 +51,6 @@ describe('Codequality report app', () => {
it('shows a loading state', () => {
expect(findStatus().text()).toBe('Loading Code Quality report');
expect(findSkeletonLoader().exists()).toBe(true);
});
});
......@@ -93,40 +89,8 @@ describe('Codequality report app', () => {
'/root/test-codequality/blob/feature-branch/ee/spec/features/admin/geo/admin_geo_projects_spec.rb#L152',
);
});
});
describe('with graphql feature flag disabled', () => {
beforeEach(() => {
createComponent(
{},
parsedIssues,
{
graphqlCodeQualityFullReport: false,
},
shallowMount,
);
});
it('renders the old pagination component', () => {
expect(findOldPagination().exists()).toBe(true);
expect(findPagination().exists()).toBe(false);
});
});
describe('with graphql feature flag enabled', () => {
beforeEach(() => {
createComponent(
{},
parsedIssues,
{
graphqlCodeQualityFullReport: true,
},
shallowMount,
);
});
it('renders the pagination component', () => {
expect(findOldPagination().exists()).toBe(false);
expect(findPagination().exists()).toBe(true);
});
});
......
import mockGetCodeQualityViolationsResponse from 'test_fixtures/graphql/codequality_report/graphql/queries/get_code_quality_violations.query.graphql.json';
export { mockGetCodeQualityViolationsResponse };
export const codeQualityViolations =
mockGetCodeQualityViolationsResponse.data.project.pipeline.codeQualityReports;
export const unparsedIssues = [
{
type: 'issue',
......@@ -180,53 +187,3 @@ export const parsedIssues = [
'/root/test-codequality/blob/feature-branch/ee/spec/finders/geo/lfs_object_registry_finder_spec.rb#L512',
},
];
export const mockGraphqlResponse = {
data: {
project: {
pipeline: {
codeQualityReports: {
count: 3,
edges: [
{
node: {
line: 33,
description:
'Function `addToImport` has 54 lines of code (exceeds 25 allowed). Consider refactoring.',
path: 'app/assets/javascripts/importer_status.js',
fingerprint: 'f5c4a1a17a8903f8c6dd885142d0e5a7',
severity: 'MAJOR',
},
},
{
node: {
line: 170,
description: 'Avoid too many `return` statements within this function.',
path: 'app/assets/javascripts/ide/stores/utils.js',
fingerprint: '75e7e39f5b8ea0aadff2470a9b44ca68',
severity: 'MINOR',
},
},
{
node: {
line: 44,
description: 'Similar blocks of code found in 3 locations. Consider refactoring.',
path: 'ee/spec/features/projects/feature_flags/user_sees_feature_flag_list_spec.rb',
fingerprint: '99c054a8b1a7270b193b0a03a6c69cfc',
severity: 'INFO',
},
},
],
},
},
},
},
};
export const mockGraphqlPagination = {
hasNextPage: true,
hasPreviousPage: true,
startCursor: 'abc123',
endCursor: 'abc124',
page: 1,
};
import MockAdapter from 'axios-mock-adapter';
import getCodeQualityViolations from 'ee/codequality_report/graphql/queries/get_code_quality_violations.query.graphql';
import * as actions from 'ee/codequality_report/store/actions';
import { VIEW_EVENT_NAME } from 'ee/codequality_report/store/constants';
import * as types from 'ee/codequality_report/store/mutation_types';
import initialState from 'ee/codequality_report/store/state';
import { gqClient } from 'ee/codequality_report/store/utils';
import { TEST_HOST } from 'helpers/test_constants';
import testAction from 'helpers/vuex_action_helper';
import Api from '~/api';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import {
unparsedIssues,
parsedIssues,
mockGraphqlResponse,
mockGraphqlPagination,
} from '../mock_data';
import { unparsedIssues, parsedIssues } from '../mock_data';
jest.mock('~/api.js');
jest.mock('~/flash');
......@@ -41,60 +34,16 @@ describe('Codequality report actions', () => {
});
describe('setPage', () => {
it('sets the page number with feature flag disabled', (done) => {
it('sets the page number', (done) => {
return testAction(
actions.setPage,
12,
state,
[{ type: types.SET_PAGE, payload: { page: 12 } }],
[{ type: types.SET_PAGE, payload: 12 }],
[],
done,
);
});
describe('with the feature flag enabled', () => {
let mockPageInfo;
beforeEach(() => {
window.gon = { features: { graphqlCodeQualityFullReport: true } };
mockPageInfo = {
...mockGraphqlPagination,
currentPage: 11,
};
});
it('sets the next page number', (done) => {
return testAction(
actions.setPage,
12,
{ ...state, pageInfo: mockPageInfo },
[
{
type: types.SET_PAGE,
payload: { after: mockGraphqlPagination.endCursor, currentPage: 12 },
},
],
[{ type: 'fetchReport' }],
done,
);
});
it('sets the previous page number', (done) => {
return testAction(
actions.setPage,
10,
{ ...state, pageInfo: mockPageInfo },
[
{
type: types.SET_PAGE,
payload: { after: mockGraphqlPagination.startCursor, currentPage: 10 },
},
],
[{ type: 'fetchReport' }],
done,
);
});
});
});
describe('requestReport', () => {
......@@ -110,7 +59,7 @@ describe('Codequality report actions', () => {
});
describe('receiveReportSuccess', () => {
it('parses the list of issues from the report with feature flag disabled', (done) => {
it('parses the list of issues from the report', (done) => {
return testAction(
actions.receiveReportSuccess,
unparsedIssues,
......@@ -120,25 +69,6 @@ describe('Codequality report actions', () => {
done,
);
});
it('parses the list of issues from the report with feature flag enabled', (done) => {
window.gon = { features: { graphqlCodeQualityFullReport: true } };
const data = {
edges: unparsedIssues.map((issue) => {
return { node: issue };
}),
};
return testAction(
actions.receiveReportSuccess,
data,
{ blobPath: '/root/test-codequality/blob/feature-branch', ...state },
[{ type: types.RECEIVE_REPORT_SUCCESS_GRAPHQL, payload: { data, parsedIssues } }],
[],
done,
);
});
});
describe('receiveReportError', () => {
......@@ -155,83 +85,19 @@ describe('Codequality report actions', () => {
});
describe('fetchReport', () => {
describe('with graphql feature flag disabled', () => {
beforeEach(() => {
mock.onGet(endpoint).replyOnce(200, unparsedIssues);
});
it('fetches the report', (done) => {
return testAction(
actions.fetchReport,
null,
{ blobPath: 'blah', ...state },
[],
[{ type: 'requestReport' }, { type: 'receiveReportSuccess', payload: unparsedIssues }],
done,
);
});
it('shows a flash message when there is an error', (done) => {
testAction(
actions.fetchReport,
'error',
state,
[],
[{ type: 'requestReport' }, { type: 'receiveReportError', payload: expect.any(Error) }],
() => {
expect(createFlash).toHaveBeenCalledWith({
message: 'There was an error fetching the codequality report.',
});
done();
},
);
});
it('shows an error when blob path is missing', (done) => {
testAction(
actions.fetchReport,
null,
state,
[],
[{ type: 'requestReport' }, { type: 'receiveReportError', payload: expect.any(Error) }],
() => {
expect(createFlash).toHaveBeenCalledWith({
message: 'There was an error fetching the codequality report.',
});
done();
},
);
});
beforeEach(() => {
mock.onGet(endpoint).replyOnce(200, unparsedIssues);
});
describe('with graphql feature flag enabled', () => {
beforeEach(() => {
jest.spyOn(gqClient, 'query').mockResolvedValue(mockGraphqlResponse);
state.paginationData = mockGraphqlPagination;
window.gon = { features: { graphqlCodeQualityFullReport: true } };
});
it('fetches the report', () => {
return testAction(
actions.fetchReport,
null,
{ blobPath: 'blah', ...state },
[],
[
{ type: 'requestReport' },
{
type: 'receiveReportSuccess',
payload: mockGraphqlResponse.data.project.pipeline.codeQualityReports,
},
],
() => {
expect(gqClient.query).toHaveBeenCalledWith({
query: getCodeQualityViolations,
variables: { after: '', first: 25, iid: null, projectPath: null },
});
},
);
});
it('fetches the report', (done) => {
return testAction(
actions.fetchReport,
null,
{ blobPath: 'blah', ...state },
[],
[{ type: 'requestReport' }, { type: 'receiveReportSuccess', payload: unparsedIssues }],
done,
);
});
it('shows a flash message when there is an error', (done) => {
......
......@@ -15,7 +15,7 @@ describe('Codequality report mutations', () => {
describe('set page', () => {
it('should set page', () => {
mutations[types.SET_PAGE](state, { page: 4 });
mutations[types.SET_PAGE](state, 4);
expect(state.pageInfo.page).toBe(4);
});
});
......@@ -27,17 +27,7 @@ describe('Codequality report mutations', () => {
});
});
describe('receive report success with graphql', () => {
it('should set issue info and clear the loading flag', () => {
mutations[types.RECEIVE_REPORT_SUCCESS_GRAPHQL](state, { data: { count: 42 }, parsedIssues });
expect(state.isLoadingCodequality).toBe(false);
expect(state.codequalityIssues).toBe(parsedIssues);
expect(state.pageInfo.count).toBe(42);
});
});
describe('receive report success without graphql', () => {
describe('receive report success', () => {
it('should set issue info and clear the loading flag', () => {
mutations[types.RECEIVE_REPORT_SUCCESS](state, parsedIssues);
......@@ -72,10 +62,8 @@ describe('Codequality report mutations', () => {
expect(state.isLoadingCodequality).toBe(false);
expect(state.loadingCodequalityFailed).toBe(true);
expect(state.allCodequalityIssues).toEqual([]);
expect(state.codequalityIssues).toEqual([]);
expect(state.codeQualityError).toEqual(new Error());
expect(state.pageInfo.total).toBe(0);
expect(state.pageInfo.count).toBe(0);
});
});
});
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Code Quality Report (GraphQL fixtures)' do
describe GraphQL::Query, type: :request do
include ApiHelpers
include GraphqlHelpers
include JavaScriptFixturesHelpers
let_it_be(:project) { create(:project, :repository) }
let_it_be(:current_user) { create(:user) }
let_it_be(:pipeline) { create(:ci_pipeline, :success, :with_codequality_reports, project: project) }
codequality_report_query_path = 'codequality_report/graphql/queries/get_code_quality_violations.query.graphql'
it "graphql/#{codequality_report_query_path}.json" do
project.add_developer(current_user)
query = get_graphql_query_as_string(codequality_report_query_path, ee: true)
post_graphql(query, current_user: current_user, variables: { projectPath: project.full_path, iid: pipeline.iid })
expect_graphql_errors_to_be_empty
end
end
end
......@@ -40307,6 +40307,9 @@ msgstr ""
msgid "ciReport|Failed to load %{reportName} report"
msgstr ""
msgid "ciReport|Failed to load Code Quality report"
msgstr ""
msgid "ciReport|Fixed"
msgstr ""
......@@ -40339,6 +40342,9 @@ msgstr ""
msgid "ciReport|Loading %{reportName} report"
msgstr ""
msgid "ciReport|Loading Code Quality report"
msgstr ""
msgid "ciReport|Manage licenses"
msgstr ""
......@@ -40375,6 +40381,9 @@ msgstr ""
msgid "ciReport|Security scanning failed loading any results"
msgstr ""
msgid "ciReport|Showing %{fetchedItems} of %{totalItems} items"
msgstr ""
msgid "ciReport|Solution"
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