Commit a39c97e3 authored by mfluharty's avatar mfluharty

Use new vuex store for code quality MR widget

Make GroupedCodequalityReportsApp to replace report-section
Hook it up to the vuex store
Fix import for parseCodeclimateMetrics in full code quality report

Move out of ee directory:
- endpoints passed in via haml
- endpoints passed through the mr widget store
- issue body component
- codeclimate mock data for the mr widget
parent c795c32e
<script>
import { mapState, mapActions, mapGetters } from 'vuex';
import { componentNames } from '~/reports/components/issue_body';
import ReportSection from '~/reports/components/report_section.vue';
import createStore from './store';
export default {
name: 'GroupedCodequalityReportsApp',
store: createStore(),
components: {
ReportSection,
},
props: {
headPath: {
type: String,
required: true,
},
headBlobPath: {
type: String,
required: true,
},
basePath: {
type: String,
required: false,
default: null,
},
baseBlobPath: {
type: String,
required: false,
default: null,
},
codequalityHelpPath: {
type: String,
required: true,
},
},
componentNames,
computed: {
...mapState(['newIssues', 'resolvedIssues']),
...mapGetters([
'hasCodequalityIssues',
'codequalityStatus',
'codequalityText',
'codequalityPopover',
]),
},
created() {
this.setPaths({
basePath: this.basePath,
headPath: this.headPath,
baseBlobPath: this.baseBlobPath,
headBlobPath: this.headBlobPath,
helpPath: this.codequalityHelpPath,
});
this.fetchReports();
},
methods: {
...mapActions(['fetchReports', 'setPaths']),
},
};
</script>
<template>
<report-section
:status="codequalityStatus"
:loading-text="
sprintf(s__('ciReport|Loading %{reportName} report'), {
reportName: 'codeclimate',
})
"
:error-text="
sprintf(s__('ciReport|Failed to load %{reportName} report'), {
reportName: 'codeclimate',
})
"
:success-text="codequalityText"
:unresolved-issues="newIssues"
:resolved-issues="resolvedIssues"
:has-issues="hasCodequalityIssues"
:component="$options.componentNames.CodequalityIssueBody"
:popover-options="codequalityPopover"
class="js-codequality-widget mr-widget-border-top mr-report"
/>
</template>
import TestIssueBody from './test_issue_body.vue';
import AccessibilityIssueBody from '../accessibility_report/components/accessibility_issue_body.vue';
import CodequalityIssueBody from '../codequality_report/components/codequality_issue_body.vue';
export const components = {
AccessibilityIssueBody,
CodequalityIssueBody,
TestIssueBody,
};
export const componentNames = {
AccessibilityIssueBody: AccessibilityIssueBody.name,
CodequalityIssueBody: CodequalityIssueBody.name,
TestIssueBody: TestIssueBody.name,
};
......@@ -37,6 +37,7 @@ import eventHub from './event_hub';
import notify from '~/lib/utils/notify';
import SourceBranchRemovalStatus from './components/source_branch_removal_status.vue';
import TerraformPlan from './components/terraform/mr_widget_terraform_container.vue';
import GroupedCodequalityReportsApp from '../reports/codequality_report/grouped_codequality_reports_app.vue';
import GroupedTestReportsApp from '../reports/components/grouped_test_reports_app.vue';
import { setFaviconOverlay } from '../lib/utils/common_utils';
import GroupedAccessibilityReportsApp from '../reports/accessibility_report/grouped_accessibility_reports_app.vue';
......@@ -75,6 +76,7 @@ export default {
'mr-widget-auto-merge-failed': AutoMergeFailed,
'mr-widget-rebase': RebaseState,
SourceBranchRemovalStatus,
GroupedCodequalityReportsApp,
GroupedTestReportsApp,
TerraformPlan,
GroupedAccessibilityReportsApp,
......@@ -111,6 +113,9 @@ export default {
shouldSuggestPipelines() {
return gon.features?.suggestPipeline && !this.mr.hasCI && this.mr.mergeRequestAddCiConfigPath;
},
shouldRenderCodeQuality() {
return this.mr?.codeclimate?.head_path;
},
shouldRenderRelatedLinks() {
return Boolean(this.mr.relatedLinks) && !this.mr.isNothingToMergeState;
},
......@@ -380,6 +385,15 @@ export default {
:mr="mr"
/>
<div class="mr-section-container mr-widget-workflow">
<grouped-codequality-reports-app
v-if="shouldRenderCodeQuality"
:base-path="mr.codeclimate.base_path"
:head-path="mr.codeclimate.head_path"
:head-blob-path="mr.headBlobPath"
:base-blob-path="mr.baseBlobPath"
:codequality-help-path="mr.codequalityHelpPath"
/>
<grouped-test-reports-app
v-if="mr.testResultsPath"
class="js-reports-container"
......
......@@ -185,6 +185,13 @@ export default class MergeRequestStore {
this.pipelinesEmptySvgPath = data.pipelines_empty_svg_path;
this.humanAccess = data.human_access;
this.newPipelinePath = data.new_project_pipeline_path;
// codeclimate
const blobPath = data.blob_path || {};
this.headBlobPath = blobPath.head_path || '';
this.baseBlobPath = blobPath.base_path || '';
this.codequalityHelpPath = data.codequality_help_path;
this.codeclimate = data.codeclimate;
}
get isNothingToMergeState() {
......
......@@ -13,5 +13,6 @@
window.gl.mrWidgetData.security_approvals_help_page_path = '#{help_page_path('user/application_security/index.md', anchor: 'security-approvals-in-merge-requests-ultimate')}';
window.gl.mrWidgetData.eligible_approvers_docs_path = '#{help_page_path('user/project/merge_requests/merge_request_approvals', anchor: 'eligible-approvers')}';
window.gl.mrWidgetData.pipelines_empty_svg_path = '#{image_path('illustrations/pipelines_empty.svg')}';
window.gl.mrWidgetData.codequality_help_path = '#{help_page_path("user/project/merge_requests/code_quality", anchor: "code-quality-reports")}';
#js-vue-mr-widget.mr-widget
......@@ -3,13 +3,13 @@ import * as types from './mutation_types';
import createFlash from '~/flash';
import { s__ } from '~/locale';
import MergeRequestStore from 'ee/vue_merge_request_widget/stores/mr_widget_store';
import { parseCodeclimateMetrics } from '~/reports/codequality_report/store/utils/codequality_comparison';
export const setPage = ({ commit }, page) => commit(types.SET_PAGE, page);
export const requestReport = ({ commit }) => commit(types.REQUEST_REPORT);
export const receiveReportSuccess = ({ state, commit }, data) => {
const parsedIssues = MergeRequestStore.parseCodeclimateMetrics(data, state.blobPath);
const parsedIssues = parseCodeclimateMetrics(data, state.blobPath);
commit(types.RECEIVE_REPORT_SUCCESS, parsedIssues);
};
export const receiveReportError = ({ commit }, error) => commit(types.RECEIVE_REPORT_ERROR, error);
......
import PerformanceIssueBody from 'ee/vue_merge_request_widget/components/performance_issue_body.vue';
import CodequalityIssueBody from 'ee/vue_merge_request_widget/components/codequality_issue_body.vue';
import BlockingMergeRequestsBody from 'ee/vue_merge_request_widget/components/blocking_merge_requests/blocking_merge_request_body.vue';
import LicenseIssueBody from 'ee/vue_shared/license_compliance/components/license_issue_body.vue';
import SecurityIssueBody from 'ee/vue_shared/security_reports/components/security_issue_body.vue';
......@@ -12,7 +11,6 @@ import {
export const components = {
...componentsCE,
PerformanceIssueBody,
CodequalityIssueBody,
LicenseIssueBody,
SecurityIssueBody,
MetricsReportsIssueBody,
......@@ -22,7 +20,6 @@ export const components = {
export const componentNames = {
...componentNamesCE,
PerformanceIssueBody: PerformanceIssueBody.name,
CodequalityIssueBody: CodequalityIssueBody.name,
LicenseIssueBody: LicenseIssueBody.name,
SecurityIssueBody: SecurityIssueBody.name,
MetricsReportsIssueBody: MetricsReportsIssueBody.name,
......
......@@ -8,7 +8,7 @@ import MrWidgetLicenses from 'ee/vue_shared/license_compliance/mr_widget_license
import ReportSection from '~/reports/components/report_section.vue';
import BlockingMergeRequestsReport from './components/blocking_merge_requests/blocking_merge_requests_report.vue';
import { n__, s__, __, sprintf } from '~/locale';
import { s__, __, sprintf } from '~/locale';
import CEWidgetOptions from '~/vue_merge_request_widget/mr_widget_options.vue';
import MrWidgetApprovals from './components/approvals/approvals.vue';
import MrWidgetGeoSecondaryNode from './components/states/mr_widget_secondary_geo_node.vue';
......@@ -33,10 +33,8 @@ export default {
componentNames,
data() {
return {
isLoadingCodequality: false,
isLoadingBrowserPerformance: false,
isLoadingLoadPerformance: false,
loadingCodequalityFailed: false,
loadingBrowserPerformanceFailed: false,
loadingLoadPerformanceFailed: false,
loadingLicenseReportFailed: false,
......@@ -46,22 +44,9 @@ export default {
shouldRenderApprovals() {
return this.mr.hasApprovalsAvailable && this.mr.state !== 'nothingToMerge';
},
shouldRenderCodeQuality() {
const { codeclimate } = this.mr || {};
return codeclimate && codeclimate.head_path;
},
shouldRenderLicenseReport() {
return this.mr.enabledReports?.licenseScanning;
},
hasCodequalityIssues() {
return (
this.mr.codeclimateMetrics &&
((this.mr.codeclimateMetrics.newIssues &&
this.mr.codeclimateMetrics.newIssues.length > 0) ||
(this.mr.codeclimateMetrics.resolvedIssues &&
this.mr.codeclimateMetrics.resolvedIssues.length > 0))
);
},
hasBrowserPerformanceMetrics() {
return (
this.mr.browserPerformanceMetrics?.degraded?.length > 0 ||
......@@ -112,47 +97,6 @@ export default {
this.$options.securityReportTypes.some(reportType => enabledReports[reportType])
);
},
codequalityText() {
const { newIssues, resolvedIssues } = this.mr.codeclimateMetrics;
const text = [];
if (!newIssues.length && !resolvedIssues.length) {
text.push(s__('ciReport|No changes to code quality'));
} else if (newIssues.length || resolvedIssues.length) {
text.push(s__('ciReport|Code quality'));
if (resolvedIssues.length) {
text.push(n__(' improved on %d point', ' improved on %d points', resolvedIssues.length));
}
if (newIssues.length > 0 && resolvedIssues.length > 0) {
text.push(__(' and'));
}
if (newIssues.length) {
text.push(n__(' degraded on %d point', ' degraded on %d points', newIssues.length));
}
}
return text.join('');
},
codequalityPopover() {
const { codeclimate } = this.mr || {};
if (codeclimate && !codeclimate.base_path) {
return {
title: s__('ciReport|Base pipeline codequality artifact not found'),
content: sprintf(
s__('ciReport|%{linkStartTag}Learn more about codequality reports %{linkEndTag}'),
{
linkStartTag: `<a href="${this.mr.codequalityHelpPath}" target="_blank" rel="noopener noreferrer">`,
linkEndTag: '<i class="fa fa-external-link" aria-hidden="true"></i></a>',
},
false,
),
};
}
return {};
},
browserPerformanceText() {
const { improved, degraded, same } = this.mr.browserPerformanceMetrics;
......@@ -204,10 +148,6 @@ export default {
return [...text, ...reportNumbers.join(', ')].join('');
},
codequalityStatus() {
return this.checkReportStatus(this.isLoadingCodequality, this.loadingCodequalityFailed);
},
browserPerformanceStatus() {
return this.checkReportStatus(
this.isLoadingBrowserPerformance,
......@@ -236,11 +176,6 @@ export default {
},
},
watch: {
shouldRenderCodeQuality(newVal) {
if (newVal) {
this.fetchCodeQuality();
}
},
hasBrowserPerformancePaths(newVal) {
if (newVal) {
this.fetchBrowserPerformance();
......@@ -264,37 +199,6 @@ export default {
apiUnapprovePath: store.apiUnapprovePath,
};
},
fetchCodeQuality() {
const { codeclimate } = this.mr || {};
if (!codeclimate.base_path) {
this.isLoadingCodequality = false;
this.loadingCodequalityFailed = true;
return;
}
this.isLoadingCodequality = true;
Promise.all([
this.service.fetchReport(codeclimate.head_path),
this.service.fetchReport(codeclimate.base_path),
])
.then(values =>
this.mr.compareCodeclimateMetrics(
values[0],
values[1],
this.mr.headBlobPath,
this.mr.baseBlobPath,
),
)
.then(() => {
this.isLoadingCodequality = false;
})
.catch(() => {
this.isLoadingCodequality = false;
this.loadingCodequalityFailed = true;
});
},
fetchBrowserPerformance() {
const { head_path, base_path } = this.mr.browserPerformance;
......@@ -367,18 +271,13 @@ export default {
/>
<div class="mr-section-container mr-widget-workflow">
<blocking-merge-requests-report :mr="mr" />
<report-section
<grouped-codequality-reports-app
v-if="shouldRenderCodeQuality"
:status="codequalityStatus"
:loading-text="translateText('codeclimate').loading"
:error-text="translateText('codeclimate').error"
:success-text="codequalityText"
:unresolved-issues="mr.codeclimateMetrics.newIssues"
:resolved-issues="mr.codeclimateMetrics.resolvedIssues"
:has-issues="hasCodequalityIssues"
:component="$options.componentNames.CodequalityIssueBody"
:popover-options="codequalityPopover"
class="js-codequality-widget mr-widget-border-top mr-report"
:base-path="mr.codeclimate.base_path"
:head-path="mr.codeclimate.head_path"
:head-blob-path="mr.headBlobPath"
:base-blob-path="mr.baseBlobPath"
:codequality-help-path="mr.codequalityHelpPath"
/>
<report-section
v-if="shouldRenderBrowserPerformance"
......
import CEMergeRequestStore from '~/vue_merge_request_widget/stores/mr_widget_store';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { mapApprovalsResponse, mapApprovalRulesResponse } from '../mappers';
import CodeQualityComparisonWorker from '../workers/code_quality_comparison_worker';
import { s__ } from '~/locale';
export default class MergeRequestStore extends CEMergeRequestStore {
constructor(data) {
super(data);
const blobPath = data.blob_path || {};
this.headBlobPath = blobPath.head_path || '';
this.baseBlobPath = blobPath.base_path || '';
this.sastHelp = data.sast_help_path;
this.containerScanningHelp = data.container_scanning_help_path;
this.dastHelp = data.dast_help_path;
......@@ -20,7 +16,6 @@ export default class MergeRequestStore extends CEMergeRequestStore {
this.canReadVulnerabilityFeedback = data.can_read_vulnerability_feedback;
this.vulnerabilityFeedbackHelpPath = data.vulnerability_feedback_help_path;
this.approvalsHelpPath = data.approvals_help_path;
this.codequalityHelpPath = data.codequality_help_path;
this.securityReportsPipelineId = data.pipeline_id;
this.securityReportsPipelineIid = data.pipeline_iid;
this.createVulnerabilityFeedbackIssuePath = data.create_vulnerability_feedback_issue_path;
......@@ -31,7 +26,6 @@ export default class MergeRequestStore extends CEMergeRequestStore {
this.visualReviewAppAvailable = Boolean(data.visual_review_app_available);
this.appUrl = gon && gon.gitlab_url;
this.initCodeclimate(data);
this.initBrowserPerformanceReport(data);
this.initLoadPerformanceReport(data);
this.licenseScanning = data.license_scanning;
......@@ -81,14 +75,6 @@ export default class MergeRequestStore extends CEMergeRequestStore {
this.approvalRules = mapApprovalRulesResponse(data.rules, this.approvals);
}
initCodeclimate(data) {
this.codeclimate = data.codeclimate;
this.codeclimateMetrics = {
newIssues: [],
resolvedIssues: [],
};
}
initBrowserPerformanceReport(data) {
this.browserPerformance = data.browser_performance;
this.browserPerformanceMetrics = {
......@@ -107,32 +93,6 @@ export default class MergeRequestStore extends CEMergeRequestStore {
};
}
static doCodeClimateComparison(headIssues, baseIssues) {
// Do these comparisons in worker threads to avoid blocking the main thread
return new Promise((resolve, reject) => {
const worker = new CodeQualityComparisonWorker();
worker.addEventListener('message', ({ data }) =>
data.newIssues && data.resolvedIssues ? resolve(data) : reject(data),
);
worker.postMessage({
headIssues,
baseIssues,
});
});
}
compareCodeclimateMetrics(headIssues, baseIssues, headBlobPath, baseBlobPath) {
const parsedHeadIssues = MergeRequestStore.parseCodeclimateMetrics(headIssues, headBlobPath);
const parsedBaseIssues = MergeRequestStore.parseCodeclimateMetrics(baseIssues, baseBlobPath);
return MergeRequestStore.doCodeClimateComparison(parsedHeadIssues, parsedBaseIssues).then(
response => {
this.codeclimateMetrics.newIssues = response.newIssues;
this.codeclimateMetrics.resolvedIssues = response.resolvedIssues;
},
);
}
compareBrowserPerformanceMetrics(headMetrics, baseMetrics) {
const headMetricsIndexed = MergeRequestStore.normalizeBrowserPerformanceMetrics(headMetrics);
const baseMetricsIndexed = MergeRequestStore.normalizeBrowserPerformanceMetrics(baseMetrics);
......@@ -254,38 +214,4 @@ export default class MergeRequestStore extends CEMergeRequestStore {
return indexedMetrics;
}
static parseCodeclimateMetrics(issues = [], path = '') {
return issues.map(issue => {
const parsedIssue = {
...issue,
name: issue.description,
};
if (issue.location) {
let parseCodeQualityUrl;
if (issue.location.path) {
parseCodeQualityUrl = `${path}/${issue.location.path}`;
parsedIssue.path = issue.location.path;
if (issue.location.lines && issue.location.lines.begin) {
parsedIssue.line = issue.location.lines.begin;
parseCodeQualityUrl += `#L${issue.location.lines.begin}`;
} else if (
issue.location.positions &&
issue.location.positions.begin &&
issue.location.positions.begin.line
) {
parsedIssue.line = issue.location.positions.begin.line;
parseCodeQualityUrl += `#L${issue.location.positions.begin.line}`;
}
parsedIssue.urlPath = parseCodeQualityUrl;
}
}
return parsedIssue;
});
}
}
import filterByKey from 'ee/vue_shared/security_reports/store/utils/filter_by_key';
const keyToFilterBy = 'fingerprint';
// eslint-disable-next-line no-restricted-globals
self.addEventListener('message', e => {
const { data } = e;
if (data === undefined) {
return null;
}
const { headIssues, baseIssues } = data;
if (!headIssues || !baseIssues) {
// eslint-disable-next-line no-restricted-globals
return self.postMessage({});
}
// eslint-disable-next-line no-restricted-globals
self.postMessage({
newIssues: filterByKey(headIssues, baseIssues, keyToFilterBy),
resolvedIssues: filterByKey(baseIssues, headIssues, keyToFilterBy),
});
// eslint-disable-next-line no-restricted-globals
return self.close();
});
/**
* Compares two arrays by the given key and returns the difference
*
* @param {Array} firstArray
* @param {Array} secondArray
* @param {String} key
* @returns {Array}
*/
const filterByKey = (firstArray = [], secondArray = [], key = '') =>
firstArray.filter(item => !secondArray.find(el => el[key] === item[key]));
export default filterByKey;
......@@ -14,7 +14,6 @@
window.gl.mrWidgetData.secret_scanning_help_path = '#{help_page_path('user/application_security/sast/index', anchor: 'secret-detection')}';
window.gl.mrWidgetData.vulnerability_feedback_help_path = '#{help_page_path("user/application_security/index")}';
window.gl.mrWidgetData.approvals_help_path = '#{help_page_path("user/project/merge_requests/merge_request_approvals")}';
window.gl.mrWidgetData.codequality_help_path = '#{help_page_path("user/project/merge_requests/code_quality", anchor: "code-quality-reports")}';
window.gl.mrWidgetData.visual_review_app_available = '#{@project.feature_available?(:visual_review_app)}' === 'true';
window.gl.mrWidgetData.license_scanning_comparison_path = '#{license_scanning_reports_project_merge_request_path(@project, @merge_request) if @project.feature_available?(:license_scanning)}'
window.gl.mrWidgetData.container_scanning_comparison_path = '#{container_scanning_reports_project_merge_request_path(@project, @merge_request) if @project.feature_available?(:container_scanning)}'
......
import Vue from 'vue';
import MockAdapter from 'axios-mock-adapter';
import mrWidgetOptions from 'ee/vue_merge_request_widget/mr_widget_options.vue';
import MRWidgetStore from 'ee/vue_merge_request_widget/stores/mr_widget_store';
import filterByKey from 'ee/vue_shared/security_reports/store/utils/filter_by_key';
import mountComponent from 'helpers/vue_mount_component_helper';
import { TEST_HOST } from 'helpers/test_constants';
import waitForPromises from 'helpers/wait_for_promises';
import { trimText } from 'helpers/text_helper';
import mockData, {
baseIssues,
headIssues,
baseBrowserPerformance,
headBrowserPerformance,
baseLoadPerformance,
headLoadPerformance,
parsedBaseIssues,
parsedHeadIssues,
} from './mock_data';
import { SUCCESS } from '~/vue_merge_request_widget/components/deployment/constants';
......@@ -303,204 +297,6 @@ describe('ee merge request widget options', () => {
});
});
describe('code quality', () => {
beforeEach(() => {
gl.mrWidgetData = {
...mockData,
codeclimate: {},
};
});
describe('when it is loading', () => {
it('should render loading indicator', done => {
mock.onGet('head.json').reply(200, headIssues);
mock.onGet('base.json').reply(200, baseIssues);
vm = mountComponent(Component, { mrData: gl.mrWidgetData });
vm.mr.codeclimate = {
head_path: 'head.json',
base_path: 'base.json',
};
vm.$nextTick(() => {
expect(trimText(vm.$el.querySelector('.js-codequality-widget').textContent)).toContain(
'Loading codeclimate report',
);
done();
});
});
});
describe('with successful request', () => {
beforeEach(() => {
mock.onGet('head.json').reply(200, headIssues);
mock.onGet('base.json').reply(200, baseIssues);
vm = mountComponent(Component, { mrData: gl.mrWidgetData });
gl.mrWidgetData.codeclimate = {
head_path: 'head.json',
base_path: 'base.json',
};
vm.mr.codeclimate = gl.mrWidgetData.codeclimate;
// mock worker response
jest.spyOn(MRWidgetStore, 'doCodeClimateComparison').mockResolvedValue({
newIssues: filterByKey(parsedHeadIssues, parsedBaseIssues, 'fingerprint'),
resolvedIssues: filterByKey(parsedBaseIssues, parsedHeadIssues, 'fingerprint'),
});
});
it('should render provided data', done => {
setImmediate(() => {
expect(
trimText(vm.$el.querySelector('.js-codequality-widget .js-code-text').textContent),
).toEqual('Code quality improved on 1 point and degraded on 1 point');
done();
});
});
describe('text connector', () => {
it('should only render information about fixed issues', done => {
setImmediate(() => {
vm.mr.codeclimateMetrics.newIssues = [];
Vue.nextTick(() => {
expect(
trimText(vm.$el.querySelector('.js-codequality-widget .js-code-text').textContent),
).toEqual('Code quality improved on 1 point');
done();
});
});
});
it('should only render information about added issues', done => {
setImmediate(() => {
vm.mr.codeclimateMetrics.resolvedIssues = [];
Vue.nextTick(() => {
expect(
trimText(vm.$el.querySelector('.js-codequality-widget .js-code-text').textContent),
).toEqual('Code quality degraded on 1 point');
done();
});
});
});
});
});
describe('with empty successful request', () => {
beforeEach(() => {
mock.onGet('head.json').reply(200, []);
mock.onGet('base.json').reply(200, []);
vm = mountComponent(Component, { mrData: gl.mrWidgetData });
gl.mrWidgetData.codeclimate = {
head_path: 'head.json',
base_path: 'base.json',
};
vm.mr.codeclimate = gl.mrWidgetData.codeclimate;
// mock worker response
jest.spyOn(MRWidgetStore, 'doCodeClimateComparison').mockResolvedValue({
newIssues: filterByKey([], [], 'fingerprint'),
resolvedIssues: filterByKey([], [], 'fingerprint'),
});
});
afterEach(() => {
mock.restore();
});
it('should render provided data', done => {
setImmediate(() => {
expect(
trimText(vm.$el.querySelector('.js-codequality-widget .js-code-text').textContent),
).toEqual('No changes to code quality');
done();
});
});
});
describe('with a head_path but no base_path', () => {
beforeEach(() => {
vm = mountComponent(Component, { mrData: gl.mrWidgetData });
gl.mrWidgetData.codeclimate = {
head_path: 'head.json',
base_path: null,
};
vm.mr.codeclimate = gl.mrWidgetData.codeclimate;
});
it('should render error indicator', done => {
setImmediate(() => {
expect(
trimText(vm.$el.querySelector('.js-codequality-widget .js-code-text').textContent),
).toContain('Failed to load codeclimate report');
done();
});
});
it('should render a help icon with more information', done => {
setImmediate(() => {
expect(vm.$el.querySelector('.js-codequality-widget .btn-help')).not.toBeNull();
expect(vm.codequalityPopover.title).toBe('Base pipeline codequality artifact not found');
done();
});
});
});
describe('with codeclimate comparison worker rejection', () => {
beforeEach(() => {
mock.onGet('head.json').reply(200, headIssues);
mock.onGet('base.json').reply(200, baseIssues);
vm = mountComponent(Component, { mrData: gl.mrWidgetData });
gl.mrWidgetData.codeclimate = {
head_path: 'head.json',
base_path: 'base.json',
};
vm.mr.codeclimate = gl.mrWidgetData.codeclimate;
// mock worker rejection
jest.spyOn(MRWidgetStore, 'doCodeClimateComparison').mockRejectedValue();
});
it('should render error indicator', done => {
setImmediate(() => {
expect(
trimText(vm.$el.querySelector('.js-codequality-widget .js-code-text').textContent),
).toEqual('Failed to load codeclimate report');
done();
});
});
});
describe('with failed request', () => {
beforeEach(() => {
mock.onGet('head.json').reply(500, []);
mock.onGet('base.json').reply(500, []);
vm = mountComponent(Component, { mrData: gl.mrWidgetData });
gl.mrWidgetData.codeclimate = {
head_path: 'head.json',
base_path: 'base.json',
};
vm.mr.codeclimate = gl.mrWidgetData.codeclimate;
});
it('should render error indicator', done => {
setImmediate(() => {
expect(
trimText(vm.$el.querySelector('.js-codequality-widget .js-code-text').textContent),
).toContain('Failed to load codeclimate report');
done();
});
});
});
});
describe('browser_performance', () => {
beforeEach(() => {
gl.mrWidgetData = {
......
......@@ -2,14 +2,6 @@ import mockData, { mockStore } from 'jest/vue_mr_widget/mock_data';
export default {
...mockData,
codeclimate: {
head_path: 'head.json',
base_path: 'base.json',
},
blob_path: {
base_path: 'blob_path',
head_path: 'blob_path',
},
vulnerability_feedback_help_path: '/help/user/application_security/index',
enabled_reports: {
sast: false,
......@@ -21,84 +13,6 @@ export default {
},
};
// Codeclimate
export const headIssues = [
{
check_name: 'Rubocop/Lint/UselessAssignment',
description: 'Insecure Dependency',
location: {
path: 'lib/six.rb',
lines: {
begin: 6,
end: 7,
},
},
fingerprint: 'e879dd9bbc0953cad5037cde7ff0f627',
},
{
categories: ['Security'],
check_name: 'Insecure Dependency',
description: 'Insecure Dependency',
location: {
path: 'Gemfile.lock',
lines: {
begin: 22,
end: 22,
},
},
fingerprint: 'ca2e59451e98ae60ba2f54e3857c50e5',
},
];
// Codeclimate
export const parsedHeadIssues = [
{
...headIssues[1],
name: 'Insecure Dependency',
path: 'lib/six.rb',
urlPath: 'headPath/lib/six.rb#L6',
line: 6,
},
];
export const baseIssues = [
{
categories: ['Security'],
check_name: 'Insecure Dependency',
description: 'Insecure Dependency',
location: {
path: 'Gemfile.lock',
lines: {
begin: 22,
end: 22,
},
},
fingerprint: 'ca2e59451e98ae60ba2f54e3857c50e5',
},
{
categories: ['Security'],
check_name: 'Insecure Dependency',
description: 'Insecure Dependency',
location: {
path: 'Gemfile.lock',
lines: {
begin: 21,
end: 21,
},
},
fingerprint: 'ca2354534dee94ae60ba2f54e3857c50e5',
},
];
export const parsedBaseIssues = [
{
...baseIssues[1],
name: 'Insecure Dependency',
path: 'Gemfile.lock',
line: 21,
urlPath: 'basePath/Gemfile.lock#L21',
},
];
// Browser Performance Testing
export const headBrowserPerformance = [
{
......
import MergeRequestStore from 'ee/vue_merge_request_widget/stores/mr_widget_store';
import filterByKey from 'ee/vue_shared/security_reports/store/utils/filter_by_key';
import mockData, {
headIssues,
baseIssues,
parsedBaseIssues,
parsedHeadIssues,
} from 'ee_jest/vue_mr_widget/mock_data';
import mockData from 'ee_jest/vue_mr_widget/mock_data';
import { stateKey } from '~/vue_merge_request_widget/stores/state_maps';
describe('MergeRequestStore', () => {
......@@ -15,38 +9,6 @@ describe('MergeRequestStore', () => {
store = new MergeRequestStore(mockData);
});
describe('compareCodeclimateMetrics', () => {
beforeEach(() => {
// mock worker response
jest.spyOn(MergeRequestStore, 'doCodeClimateComparison').mockImplementation(() =>
Promise.resolve({
newIssues: filterByKey(parsedHeadIssues, parsedBaseIssues, 'fingerprint'),
resolvedIssues: filterByKey(parsedBaseIssues, parsedHeadIssues, 'fingerprint'),
}),
);
return store.compareCodeclimateMetrics(headIssues, baseIssues, 'headPath', 'basePath');
});
it('should return the new issues', () => {
expect(store.codeclimateMetrics.newIssues[0]).toEqual(parsedHeadIssues[0]);
});
it('should return the resolved issues', () => {
expect(store.codeclimateMetrics.resolvedIssues[0]).toEqual(parsedBaseIssues[0]);
});
});
describe('parseCodeclimateMetrics', () => {
it('should parse the received issues', () => {
const codequality = MergeRequestStore.parseCodeclimateMetrics(baseIssues, 'path')[0];
expect(codequality.name).toEqual(baseIssues[0].check_name);
expect(codequality.path).toEqual(baseIssues[0].location.path);
expect(codequality.line).toEqual(baseIssues[0].location.lines.begin);
});
});
describe('isNothingToMergeState', () => {
it('returns true when nothingToMerge', () => {
store.state = stateKey.nothingToMerge;
......
......@@ -6,7 +6,6 @@ import {
groupedReportText,
} from 'ee/vue_shared/security_reports/store/utils';
import convertReportType from 'ee/vue_shared/security_reports/store/utils/convert_report_type';
import filterByKey from 'ee/vue_shared/security_reports/store/utils/filter_by_key';
import getFileLocation from 'ee/vue_shared/security_reports/store/utils/get_file_location';
import {
CRITICAL,
......@@ -63,15 +62,6 @@ describe('security reports utils', () => {
);
});
describe('filterByKey', () => {
it('filters the array with the provided key', () => {
const array1 = [{ id: '1234' }, { id: 'abg543' }, { id: '214swfA' }];
const array2 = [{ id: '1234' }, { id: 'abg543' }, { id: '453OJKs' }];
expect(filterByKey(array1, array2, 'id')).toEqual([{ id: '214swfA' }]);
});
});
describe('getFileLocation', () => {
const hostname = 'https://hostna.me';
const path = '/deeply/nested/route';
......
import { shallowMount } from '@vue/test-utils';
import component from 'ee/vue_merge_request_widget/components/codequality_issue_body.vue';
import component from '~/reports/codequality_report/components/codequality_issue_body.vue';
import { STATUS_FAILED, STATUS_NEUTRAL, STATUS_SUCCESS } from '~/reports/constants';
describe('code quality issue body issue body', () => {
......
import { mount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
import GroupedCodequalityReportsApp from '~/reports/codequality_report/grouped_codequality_reports_app.vue';
import CodequalityIssueBody from '~/reports/codequality_report/components/codequality_issue_body.vue';
import store from '~/reports/codequality_report/store';
import { mockParsedHeadIssues, mockParsedBaseIssues } from './mock_data';
const localVue = createLocalVue();
localVue.use(Vuex);
describe('Grouped code quality reports app', () => {
const Component = localVue.extend(GroupedCodequalityReportsApp);
let wrapper;
let mockStore;
const mountComponent = (props = {}) => {
wrapper = mount(Component, {
store: mockStore,
localVue,
propsData: {
basePath: 'base.json',
headPath: 'head.json',
baseBlobPath: 'base/blob/path/',
headBlobPath: 'head/blob/path/',
codequalityHelpPath: 'codequality_help.html',
...props,
},
methods: {
fetchReports: () => {},
},
});
};
const findWidget = () => wrapper.find('.js-codequality-widget');
const findIssueBody = () => wrapper.find(CodequalityIssueBody);
beforeEach(() => {
mockStore = store();
mountComponent();
});
afterEach(() => {
wrapper.destroy();
});
describe('when it is loading reports', () => {
beforeEach(() => {
mockStore.state.isLoading = true;
});
it('should render loading text', () => {
expect(findWidget().text()).toEqual('Loading codeclimate report');
});
});
describe('when base and head reports are loaded and compared', () => {
describe('with no issues', () => {
beforeEach(() => {
mockStore.state.newIssues = [];
mockStore.state.resolvedIssues = [];
});
it('renders no changes text', () => {
expect(findWidget().text()).toEqual('No changes to code quality');
});
});
describe('with issues', () => {
describe('with new issues', () => {
beforeEach(() => {
mockStore.state.newIssues = [mockParsedHeadIssues[0]];
mockStore.state.resolvedIssues = [];
});
it('renders summary text', () => {
expect(findWidget().text()).toContain('Code quality degraded on 1 point');
});
it('renders custom codequality issue body', () => {
expect(findIssueBody().props('issue')).toEqual(mockParsedHeadIssues[0]);
});
});
describe('with resolved issues', () => {
beforeEach(() => {
mockStore.state.newIssues = [];
mockStore.state.resolvedIssues = [mockParsedBaseIssues[0]];
});
it('renders summary text', () => {
expect(findWidget().text()).toContain('Code quality improved on 1 point');
});
it('renders custom codequality issue body', () => {
expect(findIssueBody().props('issue')).toEqual(mockParsedBaseIssues[0]);
});
});
describe('with new and resolved issues', () => {
beforeEach(() => {
mockStore.state.newIssues = [mockParsedHeadIssues[0]];
mockStore.state.resolvedIssues = [mockParsedBaseIssues[0]];
});
it('renders summary text', () => {
expect(findWidget().text()).toContain(
'Code quality improved on 1 point and degraded on 1 point',
);
});
it('renders custom codequality issue body', () => {
expect(findIssueBody().props('issue')).toEqual(mockParsedHeadIssues[0]);
});
});
});
});
describe('when there is a head report but no base report', () => {
beforeEach(() => {
mockStore.state.basePath = null;
mockStore.state.hasError = true;
});
it('renders error text', () => {
expect(findWidget().text()).toEqual('Failed to load codeclimate report');
});
it('renders a help icon with more information', () => {
expect(findWidget().html()).toContain('ic-question');
});
});
describe('on error', () => {
beforeEach(() => {
mockStore.state.hasError = true;
});
it('renders error text', () => {
expect(findWidget().text()).toContain('Failed to load codeclimate report');
});
it('does not render a help icon', () => {
expect(findWidget().html()).not.toContain('ic-question');
});
});
});
......@@ -211,6 +211,15 @@ export default {
can_revert_on_current_merge_request: true,
can_cherry_pick_on_current_merge_request: true,
},
codeclimate: {
head_path: 'head.json',
base_path: 'base.json',
},
blob_path: {
base_path: 'blob_path',
head_path: 'blob_path',
},
codequality_help_path: 'code_quality.html',
target_branch_path: '/root/acets-app/branches/master',
source_branch_path: '/root/acets-app/branches/daaaa',
conflict_resolution_ui_path: '/root/acets-app/-/merge_requests/22/conflicts',
......
......@@ -609,6 +609,12 @@ describe('mrWidgetOptions', () => {
});
});
describe('code quality widget', () => {
it('renders the component', () => {
expect(vm.$el.querySelector('.js-codequality-widget')).toExist();
});
});
describe('pipeline for target branch after merge', () => {
describe('with information for target branch pipeline', () => {
beforeEach(done => {
......
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