Commit baa64d59 authored by Phil Hughes's avatar Phil Hughes

Merge branch '38395-mr-widget-ci' into 'master'

Resolve "Merge request widget - CI information has different margins"

Closes #38395

See merge request gitlab-org/gitlab-ce!15237
parents 6f045671 eca2418b
import PipelineStage from '../../pipelines/components/stage.vue';
import ciIcon from '../../vue_shared/components/ci_icon.vue';
import icon from '../../vue_shared/components/icon.vue';
export default {
name: 'MRWidgetPipeline',
props: {
mr: { type: Object, required: true },
},
components: {
'pipeline-stage': PipelineStage,
ciIcon,
icon,
},
computed: {
hasPipeline() {
return this.mr.pipeline && Object.keys(this.mr.pipeline).length > 0;
},
hasCIError() {
const { hasCI, ciStatus } = this.mr;
return hasCI && !ciStatus;
},
stageText() {
return this.mr.pipeline.details.stages.length > 1 ? 'stages' : 'stage';
},
status() {
return this.mr.pipeline.details.status || {};
},
},
template: `
<div
v-if="hasPipeline || hasCIError"
class="mr-widget-heading">
<div class="ci-widget media">
<template v-if="hasCIError">
<div class="ci-status-icon ci-status-icon-failed ci-error js-ci-error append-right-10">
<span
aria-hidden="true">
<icon
name="status_failed"/>
</span>
</div>
<div class="media-body">
Could not connect to the CI server. Please check your settings and try again
</div>
</template>
<template v-else-if="hasPipeline">
<div class="ci-status-icon append-right-10">
<a
class="icon-link"
:href="this.status.details_path">
<ci-icon :status="status" />
</a>
</div>
<div class="media-body">
<span>
Pipeline
<a
:href="mr.pipeline.path"
class="pipeline-id">#{{mr.pipeline.id}}</a>
</span>
<span class="mr-widget-pipeline-graph">
<span class="stage-cell">
<div
v-if="mr.pipeline.details.stages.length > 0"
v-for="stage in mr.pipeline.details.stages"
class="stage-container dropdown js-mini-pipeline-graph">
<pipeline-stage :stage="stage" />
</div>
</span>
</span>
<span>
{{mr.pipeline.details.status.label}} for
<a
:href="mr.pipeline.commit.commit_path"
class="commit-sha js-commit-link">
{{mr.pipeline.commit.short_id}}</a>.
</span>
<span
v-if="mr.pipeline.coverage"
class="js-mr-coverage">
Coverage {{mr.pipeline.coverage}}%
</span>
</div>
</template>
</div>
</div>
`,
};
<script>
import pipelineStage from '../../pipelines/components/stage.vue';
import ciIcon from '../../vue_shared/components/ci_icon.vue';
import icon from '../../vue_shared/components/icon.vue';
export default {
name: 'MRWidgetPipeline',
props: {
pipeline: {
type: Object,
required: true,
},
// This prop needs to be camelCase, html attributes are case insensive
// https://vuejs.org/v2/guide/components.html#camelCase-vs-kebab-case
hasCi: {
type: Boolean,
required: false,
},
ciStatus: {
type: String,
required: false,
},
},
components: {
pipelineStage,
ciIcon,
icon,
},
computed: {
hasPipeline() {
return this.pipeline && Object.keys(this.pipeline).length > 0;
},
hasCIError() {
return this.hasCi && !this.ciStatus;
},
status() {
return this.pipeline.details &&
this.pipeline.details.status ? this.pipeline.details.status : {};
},
hasStages() {
return this.pipeline.details &&
this.pipeline.details.stages &&
this.pipeline.details.stages.length;
},
},
};
</script>
<template>
<div
v-if="hasPipeline || hasCIError"
class="mr-widget-heading">
<div class="ci-widget media">
<template v-if="hasCIError">
<div class="ci-status-icon ci-status-icon-failed ci-error js-ci-error append-right-10">
<icon name="status_failed" />
</div>
<div class="media-body">
Could not connect to the CI server. Please check your settings and try again
</div>
</template>
<template v-else-if="hasPipeline">
<a
class="append-right-10"
:href="this.status.details_path">
<ci-icon :status="status" />
</a>
<div class="media-body">
Pipeline
<a
:href="pipeline.path"
class="pipeline-id">
#{{pipeline.id}}
</a>
{{pipeline.details.status.label}} for
<a
:href="pipeline.commit.commit_path"
class="commit-sha js-commit-link">
{{pipeline.commit.short_id}}</a>.
<span class="mr-widget-pipeline-graph">
<span class="stage-cell">
<div
v-if="hasStages"
v-for="(stage, i) in pipeline.details.stages"
:key="i"
class="stage-container dropdown js-mini-pipeline-graph">
<pipeline-stage :stage="stage" />
</div>
</span>
</span>
<template v-if="pipeline.coverage">
Coverage {{pipeline.coverage}}%
</template>
</div>
</template>
</div>
</div>
</template>
...@@ -13,7 +13,7 @@ export { default as Vue } from 'vue'; ...@@ -13,7 +13,7 @@ export { default as Vue } from 'vue';
export { default as SmartInterval } from '~/smart_interval'; export { default as SmartInterval } from '~/smart_interval';
export { default as WidgetHeader } from './components/mr_widget_header'; export { default as WidgetHeader } from './components/mr_widget_header';
export { default as WidgetMergeHelp } from './components/mr_widget_merge_help'; export { default as WidgetMergeHelp } from './components/mr_widget_merge_help';
export { default as WidgetPipeline } from './components/mr_widget_pipeline'; export { default as WidgetPipeline } from './components/mr_widget_pipeline.vue';
export { default as WidgetDeployment } from './components/mr_widget_deployment'; export { default as WidgetDeployment } from './components/mr_widget_deployment';
export { default as WidgetRelatedLinks } from './components/mr_widget_related_links'; export { default as WidgetRelatedLinks } from './components/mr_widget_related_links';
export { default as MergedState } from './components/states/mr_widget_merged'; export { default as MergedState } from './components/states/mr_widget_merged';
......
...@@ -236,7 +236,10 @@ export default { ...@@ -236,7 +236,10 @@ export default {
<mr-widget-header :mr="mr" /> <mr-widget-header :mr="mr" />
<mr-widget-pipeline <mr-widget-pipeline
v-if="shouldRenderPipelines" v-if="shouldRenderPipelines"
:mr="mr" /> :pipeline="mr.pipeline"
:ci-status="mr.ciStatus"
:has-ci="mr.hasCI"
/>
<mr-widget-deployment <mr-widget-deployment
v-if="shouldRenderDeployments" v-if="shouldRenderDeployments"
:mr="mr" :mr="mr"
......
---
title: Moves mini graph of pipeline to the end of sentence in MR widget. Cleans HTML
and tests
merge_request:
author:
type: fixed
import Vue from 'vue'; import Vue from 'vue';
import pipelineComponent from '~/vue_merge_request_widget/components/mr_widget_pipeline'; import pipelineComponent from '~/vue_merge_request_widget/components/mr_widget_pipeline.vue';
import mountComponent from '../../helpers/vue_mount_component_helper';
import mockData from '../mock_data'; import mockData from '../mock_data';
const createComponent = (mr) => {
const Component = Vue.extend(pipelineComponent);
return new Component({
el: document.createElement('div'),
propsData: { mr },
});
};
describe('MRWidgetPipeline', () => { describe('MRWidgetPipeline', () => {
describe('props', () => { let vm;
it('should have props', () => { let Component;
const { mr } = pipelineComponent.props;
expect(mr.type instanceof Object).toBeTruthy(); beforeEach(() => {
expect(mr.required).toBeTruthy(); Component = Vue.extend(pipelineComponent);
});
}); });
describe('components', () => { afterEach(() => {
it('should have components added', () => { vm.$destroy();
expect(pipelineComponent.components['pipeline-stage']).toBeDefined();
expect(pipelineComponent.components.ciIcon).toBeDefined();
});
}); });
describe('computed', () => { describe('computed', () => {
describe('hasPipeline', () => { describe('hasPipeline', () => {
it('should return true when there is a pipeline', () => { it('should return true when there is a pipeline', () => {
expect(Object.keys(mockData.pipeline).length).toBeGreaterThan(0); vm = mountComponent(Component, {
const vm = createComponent({
pipeline: mockData.pipeline, pipeline: mockData.pipeline,
ciStatus: 'success',
hasCi: true,
}); });
expect(vm.hasPipeline).toBeTruthy(); expect(vm.hasPipeline).toEqual(true);
}); });
it('should return false when there is no pipeline', () => { it('should return false when there is no pipeline', () => {
const vm = createComponent({ vm = mountComponent(Component, {
pipeline: null, pipeline: {},
}); });
expect(vm.hasPipeline).toBeFalsy(); expect(vm.hasPipeline).toEqual(false);
}); });
}); });
describe('hasCIError', () => { describe('hasCIError', () => {
it('should return false when there is no CI error', () => { it('should return false when there is no CI error', () => {
const vm = createComponent({ vm = mountComponent(Component, {
pipeline: mockData.pipeline, pipeline: mockData.pipeline,
hasCI: true, hasCi: true,
ciStatus: 'success', ciStatus: 'success',
}); });
expect(vm.hasCIError).toBeFalsy(); expect(vm.hasCIError).toEqual(false);
}); });
it('should return true when there is a CI error', () => { it('should return true when there is a CI error', () => {
const vm = createComponent({ vm = mountComponent(Component, {
pipeline: mockData.pipeline, pipeline: mockData.pipeline,
hasCI: true, hasCi: true,
ciStatus: null, ciStatus: null,
}); });
expect(vm.hasCIError).toBeTruthy(); expect(vm.hasCIError).toEqual(true);
}); });
}); });
}); });
describe('template', () => { describe('rendered output', () => {
let vm; it('should render CI error', () => {
let el; vm = mountComponent(Component, {
const { pipeline } = mockData; pipeline: mockData.pipeline,
const mr = { hasCi: true,
hasCI: true, ciStatus: null,
ciStatus: 'success', });
pipelineDetailedStatus: pipeline.details.status,
pipeline,
};
expect(
vm.$el.querySelector('.media-body').textContent.trim(),
).toEqual('Could not connect to the CI server. Please check your settings and try again');
});
describe('with a pipeline', () => {
beforeEach(() => { beforeEach(() => {
vm = createComponent(mr); vm = mountComponent(Component, {
el = vm.$el; pipeline: mockData.pipeline,
hasCi: true,
ciStatus: 'success',
});
}); });
it('should render template elements correctly', () => { it('should render pipeline ID', () => {
expect(el.classList.contains('mr-widget-heading')).toBeTruthy(); expect(
expect(el.querySelectorAll('.ci-status-icon.ci-status-icon-success').length).toEqual(1); vm.$el.querySelector('.pipeline-id').textContent.trim(),
expect(el.querySelector('.pipeline-id').textContent).toContain(`#${pipeline.id}`); ).toEqual(`#${mockData.pipeline.id}`);
expect(el.innerText).toContain('passed');
expect(el.querySelector('.pipeline-id').getAttribute('href')).toEqual(pipeline.path);
expect(el.querySelectorAll('.stage-container').length).toEqual(2);
expect(el.querySelector('.js-ci-error')).toEqual(null);
expect(el.querySelector('.js-commit-link').getAttribute('href')).toEqual(pipeline.commit.commit_path);
expect(el.querySelector('.js-commit-link').textContent).toContain(pipeline.commit.short_id);
expect(el.querySelector('.js-mr-coverage').textContent).toContain(`Coverage ${pipeline.coverage}%`);
}); });
it('should list single stage', (done) => { it('should render pipeline status and commit id', () => {
pipeline.details.stages.splice(0, 1); expect(
vm.$el.querySelector('.media-body').textContent.trim(),
).toContain(mockData.pipeline.details.status.label);
Vue.nextTick(() => { expect(
expect(el.querySelectorAll('.stage-container button').length).toEqual(1); vm.$el.querySelector('.js-commit-link').textContent.trim(),
done(); ).toEqual(mockData.pipeline.commit.short_id);
});
expect(
vm.$el.querySelector('.js-commit-link').getAttribute('href'),
).toEqual(mockData.pipeline.commit.commit_path);
}); });
it('should not have stages when there is no stage', (done) => { it('should render pipeline graph', () => {
vm.mr.pipeline.details.stages = []; expect(vm.$el.querySelector('.mr-widget-pipeline-graph')).toBeDefined();
expect(vm.$el.querySelectorAll('.stage-container').length).toEqual(mockData.pipeline.details.stages.length);
});
Vue.nextTick(() => { it('should render coverage information', () => {
expect(el.querySelectorAll('.stage-container button').length).toEqual(0); expect(
done(); vm.$el.querySelector('.media-body').textContent,
).toContain(`Coverage ${mockData.pipeline.coverage}`);
}); });
}); });
it('should not have coverage text when pipeline has no coverage info', (done) => { describe('without coverage', () => {
vm.mr.pipeline.coverage = null; it('should not render a coverage', () => {
const mockCopy = Object.assign({}, mockData);
delete mockCopy.pipeline.coverage;
Vue.nextTick(() => { vm = mountComponent(Component, {
expect(el.querySelector('.js-mr-coverage')).toEqual(null); pipeline: mockCopy.pipeline,
done(); hasCi: true,
ciStatus: 'success',
});
expect(
vm.$el.querySelector('.media-body').textContent,
).not.toContain('Coverage');
}); });
}); });
it('should show CI error when there is a CI error', (done) => { describe('without a pipeline graph', () => {
vm.mr.ciStatus = null; it('should not render a pipeline graph', () => {
const mockCopy = Object.assign({}, mockData);
delete mockCopy.pipeline.details.stages;
vm = mountComponent(Component, {
pipeline: mockCopy.pipeline,
hasCi: true,
ciStatus: 'success',
});
Vue.nextTick(() => { expect(vm.$el.querySelector('.js-mini-pipeline-graph')).toEqual(null);
expect(el.querySelectorAll('.js-ci-error').length).toEqual(1);
expect(el.innerText).toContain('Could not connect to the CI server');
expect(el.querySelector('.ci-status-icon svg use').getAttribute('xlink:href')).toContain('status_failed');
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