Commit f7783545 authored by Fabio Huser's avatar Fabio Huser Committed by Grzegorz Bizon

feat(merge-request): visualize coverage delta on MR widget

parent 050f57d9
......@@ -28,6 +28,10 @@ export default {
type: Object,
required: true,
},
pipelineCoverageDelta: {
type: String,
required: false,
},
// This prop needs to be camelCase, html attributes are case insensive
// https://vuejs.org/v2/guide/components.html#camelCase-vs-kebab-case
hasCi: {
......@@ -92,6 +96,16 @@ export default {
showSourceBranch() {
return Boolean(this.pipeline.ref.branch);
},
coverageDeltaClass() {
const delta = this.pipelineCoverageDelta;
if (delta && parseFloat(delta) > 0) {
return 'text-success';
}
if (delta && parseFloat(delta) < 0) {
return 'text-danger';
}
return '';
},
},
};
</script>
......@@ -142,6 +156,14 @@ export default {
</div>
<div v-if="pipeline.coverage" class="coverage">
{{ s__('Pipeline|Coverage') }} {{ pipeline.coverage }}%
<span
v-if="pipelineCoverageDelta"
class="js-pipeline-coverage-delta"
:class="coverageDeltaClass"
>
({{ pipelineCoverageDelta }}%)
</span>
</div>
</div>
</div>
......
......@@ -76,6 +76,7 @@ export default {
<mr-widget-container>
<mr-widget-pipeline
:pipeline="pipeline"
:pipeline-coverage-delta="mr.pipelineCoverageDelta"
:ci-status="mr.ciStatus"
:has-ci="mr.hasCI"
:source-branch="branch"
......
......@@ -42,6 +42,7 @@ export default class MergeRequestStore {
this.commitsCount = data.commits_count;
this.divergedCommitsCount = data.diverged_commits_count;
this.pipeline = data.pipeline || {};
this.pipelineCoverageDelta = data.pipeline_coverage_delta;
this.mergePipeline = data.merge_pipeline || {};
this.deployments = this.deployments || data.deployments || [];
this.postMergeDeployments = this.postMergeDeployments || [];
......
......@@ -1423,6 +1423,12 @@ class MergeRequest < ApplicationRecord
true
end
def pipeline_coverage_delta
if base_pipeline&.coverage && head_pipeline&.coverage
'%.2f' % (head_pipeline.coverage.to_f - base_pipeline.coverage.to_f)
end
end
def base_pipeline
@base_pipeline ||= project.ci_pipelines
.order(id: :desc)
......
......@@ -57,6 +57,10 @@ class MergeRequestPollWidgetEntity < Grape::Entity
presenter(merge_request).ci_status
end
expose :pipeline_coverage_delta do |merge_request|
presenter(merge_request).pipeline_coverage_delta
end
expose :cancel_auto_merge_path do |merge_request|
presenter(merge_request).cancel_auto_merge_path
end
......
---
title: Add coverage difference visualization to merge request page
merge_request: 20676
author: Fabio Huser
type: added
......@@ -34,6 +34,7 @@ describe('MrWidgetPipelineContainer', () => {
expect(wrapper.find(MrWidgetPipeline).props()).toEqual(
jasmine.objectContaining({
pipeline: mockStore.pipeline,
pipelineCoverageDelta: mockStore.pipelineCoverageDelta,
ciStatus: mockStore.ciStatus,
hasCi: mockStore.hasCI,
sourceBranch: mockStore.sourceBranch,
......@@ -68,6 +69,7 @@ describe('MrWidgetPipelineContainer', () => {
expect(wrapper.find(MrWidgetPipeline).props()).toEqual(
jasmine.objectContaining({
pipeline: mockStore.mergePipeline,
pipelineCoverageDelta: mockStore.pipelineCoverageDelta,
ciStatus: mockStore.ciStatus,
hasCi: mockStore.hasCI,
sourceBranch: mockStore.targetBranch,
......
......@@ -62,6 +62,38 @@ describe('MRWidgetPipeline', () => {
expect(vm.hasCIError).toEqual(true);
});
});
describe('coverageDeltaClass', () => {
it('should return no class if there is no coverage change', () => {
vm = mountComponent(Component, {
pipeline: mockData.pipeline,
pipelineCoverageDelta: '0',
troubleshootingDocsPath: 'help',
});
expect(vm.coverageDeltaClass).toEqual('');
});
it('should return text-success if the coverage increased', () => {
vm = mountComponent(Component, {
pipeline: mockData.pipeline,
pipelineCoverageDelta: '10',
troubleshootingDocsPath: 'help',
});
expect(vm.coverageDeltaClass).toEqual('text-success');
});
it('should return text-danger if the coverage decreased', () => {
vm = mountComponent(Component, {
pipeline: mockData.pipeline,
pipelineCoverageDelta: '-12',
troubleshootingDocsPath: 'help',
});
expect(vm.coverageDeltaClass).toEqual('text-danger');
});
});
});
describe('rendered output', () => {
......@@ -96,6 +128,7 @@ describe('MRWidgetPipeline', () => {
pipeline: mockData.pipeline,
hasCi: true,
ciStatus: 'success',
pipelineCoverageDelta: mockData.pipelineCoverageDelta,
troubleshootingDocsPath: 'help',
});
});
......@@ -132,6 +165,13 @@ describe('MRWidgetPipeline', () => {
`Coverage ${mockData.pipeline.coverage}`,
);
});
it('should render pipeline coverage delta information', () => {
expect(vm.$el.querySelector('.js-pipeline-coverage-delta.text-danger')).toBeDefined();
expect(vm.$el.querySelector('.js-pipeline-coverage-delta').textContent).toContain(
`(${mockData.pipelineCoverageDelta}%)`,
);
});
});
describe('without commit path', () => {
......
......@@ -185,6 +185,7 @@ export default {
created_at: '2017-04-07T12:27:19.520Z',
updated_at: '2017-04-07T15:28:44.800Z',
},
pipelineCoverageDelta: '15.25',
work_in_progress: false,
source_branch_exists: false,
mergeable_discussions_state: true,
......
......@@ -2821,6 +2821,63 @@ describe MergeRequest do
end
end
describe '#pipeline_coverage_delta' do
let!(:project) { create(:project, :repository) }
let!(:merge_request) { create(:merge_request, source_project: project) }
let!(:source_pipeline) do
create(:ci_pipeline,
project: project,
ref: merge_request.source_branch,
sha: merge_request.diff_head_sha
)
end
let!(:target_pipeline) do
create(:ci_pipeline,
project: project,
ref: merge_request.target_branch,
sha: merge_request.diff_base_sha
)
end
def create_build(pipeline, coverage, name)
create(:ci_build, :success, pipeline: pipeline, coverage: coverage, name: name)
merge_request.update_head_pipeline
end
context 'when both source and target branches have coverage information' do
it 'returns the appropriate coverage delta' do
create_build(source_pipeline, 60.2, 'test:1')
create_build(target_pipeline, 50, 'test:2')
expect(merge_request.pipeline_coverage_delta).to eq('10.20')
end
end
context 'when target branch does not have coverage information' do
it 'returns nil' do
create_build(source_pipeline, 50, 'test:1')
expect(merge_request.pipeline_coverage_delta).to be_nil
end
end
context 'when source branch does not have coverage information' do
it 'returns nil for coverage_delta' do
create_build(target_pipeline, 50, 'test:1')
expect(merge_request.pipeline_coverage_delta).to be_nil
end
end
context 'neither source nor target branch has coverage information' do
it 'returns nil for coverage_delta' do
expect(merge_request.pipeline_coverage_delta).to be_nil
end
end
end
describe '#base_pipeline' do
let(:pipeline_arguments) do
{
......
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