diff --git a/ee/app/assets/javascripts/codequality_report/codequality_report.vue b/ee/app/assets/javascripts/codequality_report/codequality_report.vue index 3dbee2320b91d13858789a3724bcfe4f7fe04f78..0f67ec88f5dbc4f01f75f54cdbcd6652611805ff 100644 --- a/ee/app/assets/javascripts/codequality_report/codequality_report.vue +++ b/ee/app/assets/javascripts/codequality_report/codequality_report.vue @@ -1,43 +1,37 @@ + + diff --git a/ee/app/assets/javascripts/codequality_report/graphql/queries/get_code_quality_violations.query.graphql b/ee/app/assets/javascripts/codequality_report/graphql/queries/get_code_quality_violations.query.graphql index 86c0b30612b58d643c7fc8017ec4261feb79c074..febfcbd77b76de40b42ee6009e1298ed7c559493 100644 --- a/ee/app/assets/javascripts/codequality_report/graphql/queries/get_code_quality_violations.query.graphql +++ b/ee/app/assets/javascripts/codequality_report/graphql/queries/get_code_quality_violations.query.graphql @@ -1,21 +1,19 @@ +#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 } } } diff --git a/ee/app/assets/javascripts/codequality_report/store/actions.js b/ee/app/assets/javascripts/codequality_report/store/actions.js index b56c6e74ed3d0626c0261a0807017cd3881ef4bc..171ba816c2c53a037a4eaf6fa1af1c80ba9b7416 100644 --- a/ee/app/assets/javascripts/codequality_report/store/actions.js +++ b/ee/app/assets/javascripts/codequality_report/store/actions.js @@ -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.'), }); - } }; diff --git a/ee/app/assets/javascripts/codequality_report/store/getters.js b/ee/app/assets/javascripts/codequality_report/store/getters.js index 8ef04e4da3eccf7360c86d11c05d5c5179c17285..a7f3f1eb2a661d485eb44e8a7c2fcdd7bd04d032 100644 --- a/ee/app/assets/javascripts/codequality_report/store/getters.js +++ b/ee/app/assets/javascripts/codequality_report/store/getters.js @@ -1,15 +1,7 @@ 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; diff --git a/ee/app/assets/javascripts/codequality_report/store/mutation_types.js b/ee/app/assets/javascripts/codequality_report/store/mutation_types.js index 208999feff1fddbd2547e5892914146772a397ec..d66afa5e95c6dc51b32d66bdcad8d4c3c4711f9a 100644 --- a/ee/app/assets/javascripts/codequality_report/store/mutation_types.js +++ b/ee/app/assets/javascripts/codequality_report/store/mutation_types.js @@ -1,5 +1,4 @@ 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'; diff --git a/ee/app/assets/javascripts/codequality_report/store/mutations.js b/ee/app/assets/javascripts/codequality_report/store/mutations.js index ba89fb5ba13738e82303aef53d87f2eaed69805e..5949082fbc6f84a7a05f9ce1b7ea4e38528b18c5 100644 --- a/ee/app/assets/javascripts/codequality_report/store/mutations.js +++ b/ee/app/assets/javascripts/codequality_report/store/mutations.js @@ -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, }), }); }, diff --git a/ee/app/assets/javascripts/codequality_report/store/state.js b/ee/app/assets/javascripts/codequality_report/store/state.js index e1149ceea08fc40528ec5be5db99171eef4f6f74..b07cf8dd448dd4f22240721109f4f0b22d6eb564 100644 --- a/ee/app/assets/javascripts/codequality_report/store/state.js +++ b/ee/app/assets/javascripts/codequality_report/store/state.js @@ -1,11 +1,8 @@ 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, }, }); diff --git a/ee/app/assets/javascripts/codequality_report/store/utils.js b/ee/app/assets/javascripts/codequality_report/store/utils.js deleted file mode 100644 index 796884592376ff60efea3a30d03f09987e29256e..0000000000000000000000000000000000000000 --- a/ee/app/assets/javascripts/codequality_report/store/utils.js +++ /dev/null @@ -1,3 +0,0 @@ -import createGqClient from '~/lib/graphql'; - -export const gqClient = createGqClient(); diff --git a/ee/app/assets/javascripts/pages/projects/pipelines/show/codequality_report.js b/ee/app/assets/javascripts/pages/projects/pipelines/show/codequality_report.js index 604bde065589f635c08148dd25e47e325f1db72d..5d6ea62d0bf2ec0603c2a4893249f33f073866ca 100644 --- a/ee/app/assets/javascripts/pages/projects/pipelines/show/codequality_report.js +++ b/ee/app/assets/javascripts/pages/projects/pipelines/show/codequality_report.js @@ -1,13 +1,22 @@ 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'), + }); + } } }; diff --git a/ee/spec/features/projects/pipelines/pipeline_spec.rb b/ee/spec/features/projects/pipelines/pipeline_spec.rb index fe8771a1987bbf703d5d3bc7c78b0e4484187e21..240863424a79812f1d14913304e79d402676b720 100644 --- a/ee/spec/features/projects/pipelines/pipeline_spec.rb +++ b/ee/spec/features/projects/pipelines/pipeline_spec.rb @@ -214,18 +214,33 @@ 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 @@ 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 @@ 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 diff --git a/ee/spec/frontend/codequality_report/codequality_report_graphql_spec.js b/ee/spec/frontend/codequality_report/codequality_report_graphql_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..8113c2f8fec90d685c774f446c6c3980fb823537 --- /dev/null +++ b/ee/spec/frontend/codequality_report/codequality_report_graphql_spec.js @@ -0,0 +1,131 @@ +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); + }); + }); +}); diff --git a/ee/spec/frontend/codequality_report/codequality_report_spec.js b/ee/spec/frontend/codequality_report/codequality_report_spec.js index d27959508e36ce9877f34ba1b2ad65752407b8c4..8116d6c243efa62911bfc43e8bcf70e515324055 100644 --- a/ee/spec/frontend/codequality_report/codequality_report_spec.js +++ b/ee/spec/frontend/codequality_report/codequality_report_spec.js @@ -1,5 +1,4 @@ -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); }); }); diff --git a/ee/spec/frontend/codequality_report/mock_data.js b/ee/spec/frontend/codequality_report/mock_data.js index f0f70b36268c47cf95312d103786a56a1dc5b333..87da5a847635f6b0ee3aa5d96a648e86a854e1c1 100644 --- a/ee/spec/frontend/codequality_report/mock_data.js +++ b/ee/spec/frontend/codequality_report/mock_data.js @@ -1,3 +1,10 @@ +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, -}; diff --git a/ee/spec/frontend/codequality_report/store/actions_spec.js b/ee/spec/frontend/codequality_report/store/actions_spec.js index d156e412fab283171d672ba1f2febb083eda5665..4d46405fc2ad485b76e45b1688774a6d69780f08 100644 --- a/ee/spec/frontend/codequality_report/store/actions_spec.js +++ b/ee/spec/frontend/codequality_report/store/actions_spec.js @@ -1,21 +1,14 @@ 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) => { diff --git a/ee/spec/frontend/codequality_report/store/mutations_spec.js b/ee/spec/frontend/codequality_report/store/mutations_spec.js index 61fd2a1ea5007c89f593be9ae5ddce63436731e8..f31317d025bbfe1d6ac788ff7d1a0fca6bedf28e 100644 --- a/ee/spec/frontend/codequality_report/store/mutations_spec.js +++ b/ee/spec/frontend/codequality_report/store/mutations_spec.js @@ -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); }); }); }); diff --git a/ee/spec/frontend/fixtures/codequality_report.rb b/ee/spec/frontend/fixtures/codequality_report.rb new file mode 100644 index 0000000000000000000000000000000000000000..847354459b2c4c3fbb914e43c09f378dc90a1bf0 --- /dev/null +++ b/ee/spec/frontend/fixtures/codequality_report.rb @@ -0,0 +1,27 @@ +# 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 diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 3841c7bcbeca7995c4753f6eb7eb98995632d7a8..2ff256c5d92b94aa8e26ecf142cc6c75ae8fe21f 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -40214,6 +40214,9 @@ msgstr "" msgid "ciReport|Failed to load %{reportName} report" msgstr "" +msgid "ciReport|Failed to load Code Quality report" +msgstr "" + msgid "ciReport|Fixed" msgstr "" @@ -40246,6 +40249,9 @@ msgstr "" msgid "ciReport|Loading %{reportName} report" msgstr "" +msgid "ciReport|Loading Code Quality report" +msgstr "" + msgid "ciReport|Manage licenses" msgstr "" @@ -40282,6 +40288,9 @@ msgstr "" msgid "ciReport|Security scanning failed loading any results" msgstr "" +msgid "ciReport|Showing %{fetchedItems} of %{totalItems} items" +msgstr "" + msgid "ciReport|Solution" msgstr ""