Commit 99ba5a1a authored by Filipa Lacerda's avatar Filipa Lacerda Committed by Phil Hughes

Port of 38395-mr-widget-ci to EE

parent ce57217b
import PipelineStage from '../../pipelines/components/stage.vue';
import ciIcon from '../../vue_shared/components/ci_icon.vue';
import icon from '../../vue_shared/components/icon.vue';
import linkedPipelinesMiniList from '../../vue_shared/components/linked_pipelines_mini_list.vue';
export default {
name: 'MRWidgetPipeline',
props: {
mr: { type: Object, required: true },
},
components: {
'pipeline-stage': PipelineStage,
ciIcon,
icon,
linkedPipelinesMiniList,
},
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 || {};
},
/* We typically set defaults ([]) in the store or prop declarations, but because triggered
* and triggeredBy are appended to `pipeline`, we can't set defaults in the store, and we
* need to check their length here to prevent initializing linked-pipeline-mini-lists
* unneccessarily. */
triggered() {
return this.mr.pipeline.triggered || [];
},
triggeredBy() {
const response = this.mr.pipeline.triggered_by;
return response ? [response] : [];
},
},
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">
<linked-pipelines-mini-list
v-if="triggeredBy.length"
:triggered-by="triggeredBy"
/>
<div
v-if="mr.pipeline.details.stages.length > 0"
v-for="(stage, index) in mr.pipeline.details.stages"
class="stage-container dropdown js-mini-pipeline-graph"
:class="{
'has-downstream': index === mr.pipeline.details.stages.length - 1 && triggered.length
}">
<pipeline-stage :stage="stage" />
</div>
<linked-pipelines-mini-list
v-if="triggered.length"
:triggered="triggered"
/>
</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';
import linkedPipelinesMiniList from '../../vue_shared/components/linked_pipelines_mini_list.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,
linkedPipelinesMiniList,
},
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;
},
/* We typically set defaults ([]) in the store or prop declarations, but because triggered
* and triggeredBy are appended to `pipeline`, we can't set defaults in the store, and we
* need to check their length here to prevent initializing linked-pipeline-mini-lists
* unneccessarily. */
triggered() {
return this.pipeline.triggered || [];
},
triggeredBy() {
const response = this.pipeline.triggered_by;
return response ? [response] : [];
},
},
};
</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">
<linked-pipelines-mini-list
v-if="triggeredBy.length"
:triggered-by="triggeredBy"
/>
<div
v-if="hasStages"
v-for="(stage, i) in pipeline.details.stages"
:key="i"
class="stage-container dropdown js-mini-pipeline-graph"
:class="{
'has-downstream': i === pipeline.details.stages.length - 1 && triggered.length
}">
<pipeline-stage :stage="stage" />
</div>
<linked-pipelines-mini-list
v-if="triggered.length"
:triggered="triggered"
/>
</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';
......
...@@ -233,7 +233,10 @@ export default { ...@@ -233,7 +233,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"
......
...@@ -26,7 +26,10 @@ export default { ...@@ -26,7 +26,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"
......
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 mockData from '../mock_data'; import mockData from '../mock_data';
import mockLinkedPipelines from '../../pipelines/graph/linked_pipelines_mock_data'; import mockLinkedPipelines from '../../pipelines/graph/linked_pipelines_mock_data';
import mountComponent from '../../helpers/vue_mount_component_helper';
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,
};
beforeEach(() => {
vm = createComponent(mr);
el = vm.$el;
});
it('should render template elements correctly', () => { expect(
// TODO: Break this into separate specs vm.$el.querySelector('.media-body').textContent.trim(),
expect(el.classList.contains('mr-widget-heading')).toBeTruthy(); ).toEqual('Could not connect to the CI server. Please check your settings and try again');
expect(el.querySelectorAll('.ci-status-icon.ci-status-icon-success').length).toEqual(1);
expect(el.querySelector('.pipeline-id').textContent).toContain(`#${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) => { describe('with a pipeline', () => {
pipeline.details.stages.splice(0, 1); beforeEach(() => {
vm = mountComponent(Component, {
pipeline: mockData.pipeline,
hasCi: true,
ciStatus: 'success',
});
});
Vue.nextTick(() => { it('should render pipeline ID', () => {
expect(el.querySelectorAll('.stage-container button').length).toEqual(1); expect(
done(); vm.$el.querySelector('.pipeline-id').textContent.trim(),
).toEqual(`#${mockData.pipeline.id}`);
}); });
});
it('should not have stages when there is no stage', (done) => { it('should render pipeline status and commit id', () => {
vm.mr.pipeline.details.stages = []; expect(
vm.$el.querySelector('.media-body').textContent.trim(),
).toContain(mockData.pipeline.details.status.label);
expect(
vm.$el.querySelector('.js-commit-link').textContent.trim(),
).toEqual(mockData.pipeline.commit.short_id);
Vue.nextTick(() => { expect(
expect(el.querySelectorAll('.stage-container button').length).toEqual(0); vm.$el.querySelector('.js-commit-link').getAttribute('href'),
done(); ).toEqual(mockData.pipeline.commit.commit_path);
}); });
});
it('should not have coverage text when pipeline has no coverage info', (done) => { it('should render pipeline graph', () => {
vm.mr.pipeline.coverage = null; 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.querySelector('.js-mr-coverage')).toEqual(null); expect(
done(); vm.$el.querySelector('.media-body').textContent,
).toContain(`Coverage ${mockData.pipeline.coverage}`);
}); });
}); });
it('should show CI error when there is a CI error', (done) => { describe('without coverage', () => {
vm.mr.ciStatus = null; it('should not render a coverage', () => {
const mockCopy = Object.assign({}, mockData);
delete mockCopy.pipeline.coverage;
Vue.nextTick(() => { vm = mountComponent(Component, {
expect(el.querySelectorAll('.js-ci-error').length).toEqual(1); pipeline: mockCopy.pipeline,
expect(el.innerText).toContain('Could not connect to the CI server'); hasCi: true,
expect(el.querySelector('.ci-status-icon svg use').getAttribute('xlink:href')).toContain('status_failed'); ciStatus: 'success',
done(); });
expect(
vm.$el.querySelector('.media-body').textContent,
).not.toContain('Coverage');
}); });
}); });
it('should set triggered to an empty array', () => { describe('without a pipeline graph', () => {
expect(vm.triggered.length).toBe(0); it('should not render a pipeline graph', () => {
}); const mockCopy = Object.assign({}, mockData);
delete mockCopy.pipeline.details.stages;
it('should set triggeredBy to an empty array', () => { vm = mountComponent(Component, {
expect(vm.triggeredBy.length).toBe(0); pipeline: mockCopy.pipeline,
}); hasCi: true,
ciStatus: 'success',
});
it('should not render upstream or downstream pipelines', () => { expect(vm.$el.querySelector('.js-mini-pipeline-graph')).toEqual(null);
expect(el.querySelector('.linked-pipeline-mini-list')).toBeNull(); });
}); });
}); });
describe('when upstream pipelines are passed', function () { describe('when upstream pipelines are passed', () => {
beforeEach(function () { beforeEach(() => {
const pipeline = Object.assign({}, mockData.pipeline, { vm = mountComponent(Component, {
triggered_by: mockLinkedPipelines.triggered_by, pipeline: Object.assign({}, mockData.pipeline, {
}); triggered_by: mockLinkedPipelines.triggered_by,
}),
this.vm = createComponent({ hasCi: true,
pipeline,
pipelineDetailedStatus: mockData.pipeline.details.status,
hasCI: true,
ciStatus: 'success', ciStatus: 'success',
}).$mount(); });
}); });
it('should coerce triggeredBy into a collection', function () { it('should coerce triggeredBy into a collection', () => {
expect(this.vm.triggeredBy.length).toBe(1); expect(vm.triggeredBy.length).toBe(1);
}); });
it('should render the linked pipelines mini list', function (done) { it('should render the linked pipelines mini list', () => {
Vue.nextTick(() => { expect(vm.$el.querySelector('.linked-pipeline-mini-list.is-upstream')).not.toBeNull();
expect(this.vm.$el.querySelector('.linked-pipeline-mini-list.is-upstream')).not.toBeNull();
done();
});
}); });
}); });
describe('when downstream pipelines are passed', function () { describe('when downstream pipelines are passed', () => {
beforeEach(function () { beforeEach(() => {
const pipeline = Object.assign({}, mockData.pipeline, { vm = mountComponent(Component, {
triggered: mockLinkedPipelines.triggered, pipeline: Object.assign({}, mockData.pipeline, {
}); triggered: mockLinkedPipelines.triggered,
}),
this.vm = createComponent({ hasCi: true,
pipeline,
pipelineDetailedStatus: mockData.pipeline.details.status,
hasCI: true,
ciStatus: 'success', ciStatus: 'success',
}).$mount(); });
}); });
it('should render the linked pipelines mini list', function (done) { it('should render the linked pipelines mini list', () => {
Vue.nextTick(() => { expect(vm.$el.querySelector('.linked-pipeline-mini-list.is-downstream')).not.toBeNull();
expect(this.vm.$el.querySelector('.linked-pipeline-mini-list.is-downstream')).not.toBeNull();
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