Commit b2f22b3f authored by Phil Hughes's avatar Phil Hughes

Merge branch '2122-transform-linked-into-button' into 'master'

Updates linked pipelines to render inline

Closes #2122

See merge request gitlab-org/gitlab-ee!8607
parents 60bb9eb4 738d27e8
<script>
import _ from 'underscore';
import { GlLoadingIcon } from '@gitlab/ui';
import LinkedPipelinesColumn from 'ee/pipelines/components/graph/linked_pipelines_column.vue';
import EEGraphMixin from 'ee/pipelines/mixins/graph_component_mixin';
import StageColumnComponent from './stage_column_component.vue';
import LinkedPipelinesColumn from 'ee/pipelines/components/graph/linked_pipelines_column.vue'; // eslint-disable-line import/order
export default {
components: {
......@@ -10,6 +11,7 @@ export default {
StageColumnComponent,
GlLoadingIcon,
},
mixins: [EEGraphMixin],
props: {
isLoading: {
type: Boolean,
......@@ -20,36 +22,19 @@ export default {
required: true,
},
},
computed: {
graph() {
return this.pipeline.details && this.pipeline.details.stages;
},
triggered() {
return this.pipeline.triggered || [];
},
triggeredBy() {
const response = this.pipeline.triggered_by;
return response ? [response] : [];
},
hasTriggered() {
return !!this.triggered.length;
},
hasTriggeredBy() {
return !!this.triggeredBy.length;
},
},
methods: {
capitalizeStageName(name) {
const escapedName = _.escape(name);
return escapedName.charAt(0).toUpperCase() + escapedName.slice(1);
},
isFirstColumn(index) {
return index === 0;
},
stageConnectorClass(index, stage) {
let className;
......@@ -63,10 +48,12 @@ export default {
return className;
},
refreshPipelineGraph() {
this.$emit('refreshPipelineGraph');
},
hasOnlyOneJob(stage) {
return stage.groups.length === 1;
},
},
};
</script>
......@@ -75,11 +62,27 @@ export default {
<div class="pipeline-visualization pipeline-graph pipeline-tab-content">
<div class="text-center"><gl-loading-icon v-if="isLoading" :size="3" /></div>
<ul v-if="shouldRenderTriggeredByPipeline" class="d-inline-block upstream-pipeline align-top">
<stage-column-component
v-for="(stage, indexUpstream) in triggeredByGraph"
:key="stage.name"
:class="{
'has-only-one-job': hasOnlyOneJob(stage),
}"
:title="capitalizeStageName(stage.name)"
:groups="stage.groups"
:stage-connector-class="stageConnectorClass(indexUpstream, stage)"
:is-first-column="isFirstColumn(indexUpstream)"
@refreshPipelineGraph="refreshTriggeredByPipelineGraph"
/>
</ul>
<linked-pipelines-column
v-if="hasTriggeredBy"
:linked-pipelines="triggeredBy"
column-title="Upstream"
:linked-pipelines="triggeredByPipelines"
:column-title="__('Upstream')"
graph-position="left"
@linkedPipelineClick="pipeline => $emit('onClickTriggeredBy', pipeline)"
/>
<ul
......@@ -87,7 +90,7 @@ export default {
:class="{
'has-linked-pipelines': hasTriggered || hasTriggeredBy,
}"
class="stage-column-list"
class="stage-column-list align-top"
>
<stage-column-component
v-for="(stage, index) in graph"
......@@ -95,7 +98,7 @@ export default {
:class="{
'has-upstream': index === 0 && hasTriggeredBy,
'has-downstream': index === graph.length - 1 && hasTriggered,
'has-only-one-job': stage.groups.length === 1,
'has-only-one-job': hasOnlyOneJob(stage),
}"
:title="capitalizeStageName(stage.name)"
:groups="stage.groups"
......@@ -108,10 +111,30 @@ export default {
<linked-pipelines-column
v-if="hasTriggered"
:linked-pipelines="triggered"
column-title="Downstream"
:linked-pipelines="triggeredPipelines"
:column-title="__('Downstream')"
graph-position="right"
@linkedPipelineClick="handleClickedDownstream"
/>
<ul
v-if="shouldRenderTriggeredPipeline"
class="d-inline-block downstream-pipeline position-relative align-top"
:style="{ 'margin-top': marginTop }"
>
<stage-column-component
v-for="(stage, indexDownstream) in triggeredGraph"
:key="stage.name"
:class="{
'has-only-one-job': hasOnlyOneJob(stage),
}"
:title="capitalizeStageName(stage.name)"
:groups="stage.groups"
:stage-connector-class="stageConnectorClass(indexDownstream, stage)"
:is-first-column="isFirstColumn(indexDownstream)"
@refreshPipelineGraph="refreshTriggeredPipelineGraph"
/>
</ul>
</div>
</div>
</template>
......@@ -27,10 +27,10 @@ export default {
required: false,
default: '',
},
hasTriggeredBy: {
type: Boolean,
required: true,
required: false,
default: false,
},
},
methods: {
......
......@@ -2,10 +2,11 @@ import Vue from 'vue';
import Flash from '~/flash';
import Translate from '~/vue_shared/translate';
import { __ } from '~/locale';
import PipelinesMediator from './pipeline_details_mediator';
import PipelinesMediator from 'ee/pipelines/pipeline_details_mediator';
import pipelineGraph from './components/graph/graph_component.vue';
import pipelineHeader from './components/header_component.vue';
import eventHub from './event_hub';
import GraphEEMixin from 'ee/pipelines/mixins/graph_pipeline_bundle_mixin'; // eslint-disable-line import/order
Vue.use(Translate);
......@@ -22,6 +23,7 @@ export default () => {
components: {
pipelineGraph,
},
mixins: [GraphEEMixin],
data() {
return {
mediator,
......@@ -41,9 +43,21 @@ export default () => {
props: {
isLoading: this.mediator.state.isLoading,
pipeline: this.mediator.store.state.pipeline,
// EE-only start
triggeredPipelines: this.mediator.store.state.triggeredPipelines,
triggered: this.mediator.store.state.triggered,
triggeredByPipelines: this.mediator.store.state.triggeredByPipelines,
triggeredBy: this.mediator.store.state.triggeredBy,
// EE-only end
},
on: {
refreshPipelineGraph: this.requestRefreshPipelineGraph,
// EE-only start
refreshTriggeredPipelineGraph: this.mediator.refreshTriggeredByPipelineGraph,
refreshTriggeredByPipelineGraph: this.mediator.refreshTriggeredByPipelineGraph,
onClickTriggeredBy: pipeline => this.clickTriggeredBy(pipeline),
onClickTriggered: pipeline => this.clickTriggered(pipeline),
// EE-only end
},
});
},
......
......@@ -2,8 +2,8 @@ import Visibility from 'visibilityjs';
import Flash from '../flash';
import Poll from '../lib/utils/poll';
import { __ } from '../locale';
import PipelineStore from './stores/pipeline_store';
import PipelineService from './services/pipeline_service';
import PipelineStore from 'ee/pipelines/stores/pipeline_store'; // eslint-disable-line import/order
import PipelineService from 'ee/pipelines/services/pipeline_service'; // eslint-disable-line import/order
export default class pipelinesMediator {
constructor(options = {}) {
......
<script>
import { GlLoadingIcon, GlTooltipDirective, GlLink } from '@gitlab/ui';
import { GlLoadingIcon, GlTooltipDirective, GlButton } from '@gitlab/ui';
import CiStatus from '~/vue_shared/components/ci_icon.vue';
export default {
......@@ -9,17 +9,13 @@ export default {
components: {
CiStatus,
GlLoadingIcon,
GlLink,
GlButton,
},
props: {
pipelineId: {
type: Number,
required: true,
},
pipelinePath: {
type: String,
required: true,
},
pipelineStatus: {
type: Object,
required: true,
......@@ -38,6 +34,15 @@ export default {
tooltipText() {
return `${this.projectName} - ${this.pipelineStatus.label}`;
},
buttonId() {
return `js-linked-pipeline-${this.pipelineId}`;
},
},
methods: {
onClickLinkedPipeline() {
this.$root.$emit('bv::hide::tooltip', this.buttonId);
this.$emit('pipelineClicked');
},
},
};
</script>
......@@ -45,21 +50,17 @@ export default {
<template>
<li class="linked-pipeline build">
<div class="curve"></div>
<div>
<gl-link
v-gl-tooltip
:href="pipelinePath"
:title="tooltipText"
class="js-linked-pipeline-content linked-pipeline-content"
>
<span class="js-linked-pipeline-status ci-status-text">
<gl-loading-icon v-if="isLoading" class="js-linked-pipeline-loading" />
<ci-status v-else :status="pipelineStatus" class="js-linked-pipeline-status" />
</span>
<span class="linked-pipeline-project-name">{{ projectName }}</span>
<span class="project-name-pipeline-id-separator">&#8226;</span>
<span class="js-linked-pipeline-id">#{{ pipelineId }}</span>
</gl-link>
</div>
<gl-button
:id="buttonId"
v-gl-tooltip
:title="tooltipText"
class="js-linked-pipeline-content linked-pipeline-content"
@click="onClickLinkedPipeline"
>
<gl-loading-icon v-if="isLoading" class="js-linked-pipeline-loading d-inline" />
<ci-status v-else :status="pipelineStatus" class="js-linked-pipeline-status" />
<span class="str-truncated align-bottom"> {{ projectName }} &#8226; #{{ pipelineId }} </span>
</gl-button>
</li>
</template>
<script>
import linkedPipeline from './linked_pipeline.vue';
import LinkedPipeline from './linked_pipeline.vue';
export default {
components: {
linkedPipeline,
LinkedPipeline,
},
props: {
columnTitle: {
......@@ -19,7 +19,6 @@ export default {
required: true,
},
},
computed: {
columnClass() {
return `graph-position-${this.graphPosition}`;
......@@ -38,11 +37,15 @@ export default {
:key="pipeline.id"
:class="{
'flat-connector-before': index === 0 && graphPosition === 'right',
active: !pipeline.isCollapsed || pipeline.isLoading,
'left-connector': !pipeline.isCollapsed && graphPosition === 'left',
}"
:pipeline-id="pipeline.id"
:project-name="pipeline.project.name"
:pipeline-status="pipeline.details.status"
:pipeline-path="pipeline.path"
:is-loading="pipeline.isLoading"
@pipelineClicked="$emit('linkedPipelineClick', pipeline, index);"
/>
</ul>
</div>
......
import _ from 'underscore';
export default {
props: {
triggered: {
type: Object,
required: false,
default: () => ({}),
},
triggeredBy: {
type: Object,
required: false,
default: () => ({}),
},
triggeredByPipelines: {
type: Array,
required: false,
default: () => [],
},
triggeredPipelines: {
type: Array,
required: false,
default: () => [],
},
},
data() {
return {
triggeredTopIndex: 1,
};
},
computed: {
triggeredGraph() {
return this.triggered && this.triggered.details && this.triggered.details.stages;
},
triggeredByGraph() {
return this.triggeredBy && this.triggeredBy.details && this.triggeredBy.details.stages;
},
hasTriggered() {
return this.triggeredPipelines.length > 0;
},
hasTriggeredBy() {
return this.triggeredByPipelines.length > 0;
},
shouldRenderTriggeredPipeline() {
return !this.isLoading && !_.isEmpty(this.triggered);
},
shouldRenderTriggeredByPipeline() {
return !this.isLoading && !_.isEmpty(this.triggeredBy);
},
/**
* Calculates the margin top of the clicked downstream pipeline by
* adding the height of each linked pipeline and the margin
*/
marginTop() {
return `${this.triggeredTopIndex * 52}px`;
},
},
methods: {
refreshTriggeredPipelineGraph() {
this.$emit('refreshTriggeredPipelineGraph');
},
refreshTriggeredByPipelineGraph() {
this.$emit('refreshTriggeredByPipelineGraph');
},
handleClickedDownstream(pipeline, clickedIndex) {
this.triggeredTopIndex = clickedIndex;
this.$emit('onClickTriggered', pipeline);
},
},
};
import pipelinesKeys from 'ee/pipelines/constants';
export default {
methods: {
/**
* Called when a linked pipeline is clicked.
*
* If the pipeline is collapsed we will start polling it & we will reset the other pipelines.
* If the pipeline is expanded we will close it.
*
* @param {String} method Method to fetch the pipeline
* @param {String} storeKey Store property that will be updates
* @param {String} resetStoreKey Store key for the visible pipeline that will need to be reset
* @param {Object} pipeline The clicked pipeline
*/
clickPipeline(method, storeKey, resetStoreKey, pipeline, pollKey) {
if (pipeline.isCollapsed) {
this.mediator[method](pipeline);
} else {
this.mediator.resetPipeline(storeKey, pipeline, resetStoreKey, pollKey);
}
},
clickTriggered(triggered) {
this.clickPipeline(
'fetchTriggeredPipeline',
pipelinesKeys.triggeredPipelines,
pipelinesKeys.triggered,
triggered,
'pollTriggered',
);
},
clickTriggeredBy(triggeredBy) {
this.clickPipeline(
'fetchTriggeredByPipeline',
pipelinesKeys.triggeredByPipelines,
pipelinesKeys.triggeredBy,
triggeredBy,
'pollTriggeredBy',
);
},
},
};
import CePipelineMediator from '~/pipelines/pipeline_details_mediator';
import createFlash from '~/flash';
import Poll from '~/lib/utils/poll';
import { __ } from '~/locale';
import PipelineService from 'ee/pipelines/services/pipeline_service';
/**
* Extends CE mediator with the logic to handle the upstream/downstream pipelines
*/
export default class EePipelineMediator extends CePipelineMediator {
/**
* Requests the clicked downstream pipeline pipeline
*
* @param {Object} pipeline
*/
fetchTriggeredPipeline(pipeline) {
if (this.pollTriggered) {
this.pollTriggered.stop();
this.pollTriggered = null;
}
this.store.requestTriggeredPipeline(pipeline);
this.pollTriggered = new Poll({
resource: PipelineService,
method: 'getUpstreamDownstream',
data: pipeline.path,
successCallback: ({ data }) => this.store.receiveTriggeredPipelineSuccess(pipeline, data),
errorCallback: () => {
this.store.receiveTriggeredPipelineError(pipeline);
createFlash(
__('An error occured while fetching this downstream pipeline. Please try again'),
);
},
});
this.pollTriggered.makeRequest();
}
refreshTriggeredPipelineGraph() {
this.pollTriggered.stop();
this.pollTriggered.restart();
}
/**
* Requests the clicked upstream pipeline pipeline
* @param {*} pipeline
*/
fetchTriggeredByPipeline(pipeline) {
if (this.pollTriggeredBy) {
this.pollTriggeredBy.stop();
this.pollTriggeredBy = null;
}
this.store.requestTriggeredByPipeline(pipeline);
this.pollTriggeredBy = new Poll({
resource: PipelineService,
method: 'getUpstreamDownstream',
data: pipeline.path,
successCallback: ({ data }) => this.store.receiveTriggeredByPipelineSuccess(pipeline, data),
errorCallback: () => {
this.store.receiveTriggeredByPipelineError(pipeline);
createFlash(__('An error occured while fetching this upstream pipeline. Please try again'));
},
});
this.pollTriggeredBy.makeRequest();
}
refreshTriggeredByPipelineGraph() {
this.pollTriggeredBy.stop();
this.pollTriggeredBy.restart();
}
resetPipeline(storeKey, pipeline, resetStoreKey, pollKey) {
this[pollKey].stop();
this.store.closePipeline(storeKey, pipeline, resetStoreKey);
}
}
......@@ -33,13 +33,30 @@ export default class PipelineStore extends CePipelineStore {
super.storePipeline(pipeline);
if (pipeline.triggered && pipeline.triggered.length) {
this.state.triggeredPipelines = pipeline.triggered.map(triggered =>
PipelineStore.parsePipeline(triggered),
);
this.state.triggeredPipelines = pipeline.triggered.map(triggered => {
// because we are polling we need to make sure we do not hijack user's clicks.
const oldPipeline = this.state.triggeredPipelines.find(
oldValue => oldValue.id === triggered.id,
);
return Object.assign({}, triggered, {
isCollapsed: oldPipeline ? oldPipeline.isCollapsed : true,
isLoading: oldPipeline ? oldPipeline.isLoading : false,
});
});
}
if (pipeline.triggered_by) {
this.state.triggeredByPipelines = [PipelineStore.parsePipeline(pipeline.triggered_by)];
this.state.triggeredByPipelines = [
Object.assign({}, pipeline.triggered_by, {
isCollapsed: this.state.triggeredByPipelines.length
? this.state.triggeredByPipelines[0].isCollapsed
: true,
isLoading: this.state.triggeredByPipelines.length
? this.state.triggeredByPipelines[0].isLoading
: false,
}),
];
}
}
......@@ -72,7 +89,7 @@ export default class PipelineStore extends CePipelineStore {
this.updatePipeline(
pipelinesKeys.triggeredPipelines,
pipeline,
{ isLoading: false },
{ isLoading: false, isCollapsed: false },
pipelinesKeys.triggered,
response,
);
......@@ -121,7 +138,7 @@ export default class PipelineStore extends CePipelineStore {
this.updatePipeline(
pipelinesKeys.triggeredByPipelines,
pipeline,
{ isLoading: false },
{ isLoading: false, isCollapsed: false },
pipelinesKeys.triggeredBy,
response,
);
......@@ -183,7 +200,6 @@ export default class PipelineStore extends CePipelineStore {
if (triggered.id === pipeline.id) {
return Object.assign({}, triggered, { isLoading: true, isCollapsed: false });
}
// reset the others, in case another was one opened
return PipelineStore.parsePipeline(triggered);
});
......
......@@ -98,6 +98,10 @@
display: inline-block;
}
.upstream-pipeline {
margin-right: 84px;
}
.linked-pipelines-column.stage-column {
position: relative;
......@@ -119,6 +123,16 @@
.cross-project-triangle {
left: -64px;
}
// reset connectors for the downstream pipeline
.linked-pipeline.build {
.curve::before,
&::after {
content: '';
width: 0;
border: 0;
}
}
}
.linked-pipeline.build {
......@@ -129,16 +143,36 @@
@include flat-connector-before($linked-project-column-margin);
}
&::after {
right: -$linked-project-column-margin;
width: $linked-project-column-margin;
&.active, {
.linked-pipeline-content,
.linked-pipeline-content:hover,
.linked-pipeline-content:focus, {
background-color: $blue-100;
}
&.left-connector {
@include flat-connector-before(88px)
}
&::after {
content: '';
position: absolute;
top: 48%;
right: -88px;
border-top: 2px solid $border-color;
width: 88px;
height: 1px;
}
}
.linked-pipeline-content {
@include build-content(0);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
text-align: inherit;
min-height: 42px;
svg {
top: 0;
}
}
}
}
......
---
title: Renders upstream and downstream pipelines in the main pipeline graph
merge_request: 8607
author:
type: fixed
......@@ -48,43 +48,26 @@ describe('Linked pipeline', () => {
expect(vm.$el.tagName).toBe('LI');
});
it('should render a link', () => {
it('should render a button', () => {
const linkElement = vm.$el.querySelector('.js-linked-pipeline-content');
expect(linkElement).not.toBeNull();
});
it('should link to the correct path', () => {
const linkElement = vm.$el.querySelector('.js-linked-pipeline-content');
expect(linkElement.getAttribute('href')).toBe(props.pipelinePath);
});
it('should render the project name', () => {
const projectNameElement = vm.$el.querySelector('.linked-pipeline-project-name');
expect(projectNameElement.innerText).toContain(props.projectName);
expect(vm.$el.innerText).toContain(props.projectName);
});
it('should render an svg within the status container', () => {
console.log(vm.$el);
const pipelineStatusElement = vm.$el.querySelector('.js-linked-pipeline-status');
expect(pipelineStatusElement.querySelector('svg')).not.toBeNull();
});
it('should render the pipeline status icon svg', () => {
const pipelineStatusElement = vm.$el.querySelector('.js-linked-pipeline-status');
expect(pipelineStatusElement.querySelector('.ci-status-icon-running')).not.toBeNull();
expect(pipelineStatusElement.innerHTML).toContain('<svg');
});
it('should render the correct pipeline status icon style selector', () => {
const pipelineStatusElement = vm.$el.querySelector('.js-linked-pipeline-status');
expect(pipelineStatusElement.firstChild.classList.contains('ci-status-icon-running')).toBe(
true,
);
expect(vm.$el.querySelector('.js-ci-status-icon-running')).not.toBeNull();
expect(vm.$el.querySelector('.js-ci-status-icon-running').innerHTML).toContain('<svg');
});
it('should have a ci-status child component', () => {
......@@ -92,9 +75,7 @@ describe('Linked pipeline', () => {
});
it('should render the pipeline id', () => {
const pipelineIdElement = vm.$el.querySelector('.js-linked-pipeline-id');
expect(pipelineIdElement.innerText).toContain(`#${props.pipelineId}`);
expect(vm.$el.innerText).toContain(`#${props.pipelineId}`);
});
it('should correctly compute the tooltip text', () => {
......@@ -110,4 +91,35 @@ describe('Linked pipeline', () => {
expect(titleAttr).toContain(mockPipeline.details.status.label);
});
});
describe('on click', () => {
const props = {
pipelineId: mockPipeline.id,
pipelinePath: mockPipeline.path,
pipelineStatus: mockPipeline.details.status,
projectName: mockPipeline.project.name,
isLoading: false,
};
beforeEach(() => {
vm = mountComponent(Component, props);
});
it('emits `pipelineClicked` event', () => {
spyOn(vm, '$emit');
vm.$el.querySelector('button').click();
expect(vm.$emit).toHaveBeenCalledWith('pipelineClicked');
});
it('should emit `bv::hide::tooltip` to close the tooltip', () => {
spyOn(vm.$root, '$emit');
vm.$el.querySelector('button').click();
expect(vm.$root.$emit).toHaveBeenCalledWith(
'bv::hide::tooltip',
`js-linked-pipeline-${props.pipelineId}`,
);
});
});
});
import Vue from 'vue';
import LinkedPipelinesColumn from 'ee/pipelines/components/graph/linked_pipelines_column.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import mockData from './linked_pipelines_mock_data';
const LinkedPipelinesColumnComponent = Vue.extend(LinkedPipelinesColumn);
describe('Linked Pipelines Column', () => {
const Component = Vue.extend(LinkedPipelinesColumn);
const props = {
columnTitle: 'Upstream',
linkedPipelines: mockData.triggered,
graphPosition: 'right',
};
let vm;
describe('Linked Pipelines Column', function() {
beforeEach(() => {
this.propsData = {
columnTitle: 'Upstream',
linkedPipelines: mockData.triggered,
graphPosition: 'right',
};
this.linkedPipelinesColumn = new LinkedPipelinesColumnComponent({
propsData: this.propsData,
}).$mount();
vm = mountComponent(Component, props);
});
it('instantiates a defined Vue component', () => {
expect(this.linkedPipelinesColumn).toBeDefined();
afterEach(() => {
vm.$destroy();
});
it('renders the pipeline orientation', () => {
const titleElement = this.linkedPipelinesColumn.$el.querySelector(
'.linked-pipelines-column-title',
);
const titleElement = vm.$el.querySelector('.linked-pipelines-column-title');
expect(titleElement.innerText).toContain(this.propsData.columnTitle);
expect(titleElement.innerText).toContain(props.columnTitle);
});
it('has the correct number of linked pipeline child components', () => {
expect(this.linkedPipelinesColumn.$children.length).toBe(this.propsData.linkedPipelines.length);
expect(vm.$children.length).toBe(props.linkedPipelines.length);
});
it('renders the correct number of linked pipelines', () => {
const linkedPipelineElements = this.linkedPipelinesColumn.$el.querySelectorAll(
'.linked-pipeline',
);
const linkedPipelineElements = vm.$el.querySelectorAll('.linked-pipeline');
expect(linkedPipelineElements.length).toBe(this.propsData.linkedPipelines.length);
expect(linkedPipelineElements.length).toBe(props.linkedPipelines.length);
});
});
......@@ -608,6 +608,12 @@ msgstr ""
msgid "An error has occurred"
msgstr ""
msgid "An error occured while fetching this downstream pipeline. Please try again"
msgstr ""
msgid "An error occured while fetching this upstream pipeline. Please try again"
msgstr ""
msgid "An error occurred adding a draft to the discussion."
msgstr ""
......@@ -2998,6 +3004,9 @@ msgstr ""
msgid "DownloadSource|Download"
msgstr ""
msgid "Downstream"
msgstr ""
msgid "Downvotes"
msgstr ""
......@@ -9043,6 +9052,9 @@ msgstr ""
msgid "UploadLink|click to upload"
msgstr ""
msgid "Upstream"
msgstr ""
msgid "Upvotes"
msgstr ""
......
......@@ -28,28 +28,30 @@ describe('graph component', () => {
});
});
describe('when linked pipelines are present', function() {
beforeEach(function() {
describe('when linked pipelines are present', () => {
beforeEach(() => {
component = mountComponent(GraphComponent, {
isLoading: false,
pipeline: graphJSON,
triggeredByPipelines: [linkedPipelineJSON.triggered_by],
triggeredPipelines: linkedPipelineJSON.triggered,
});
});
describe('rendered output', function() {
it('should include the pipelines graph', function() {
describe('rendered output', () => {
it('should include the pipelines graph', () => {
expect(component.$el.classList.contains('js-pipeline-graph')).toEqual(true);
});
it('should not include the loading icon', function() {
it('should not include the loading icon', () => {
expect(component.$el.querySelector('.fa-spinner')).toBeNull();
});
it('should include the stage column list', function() {
it('should include the stage column list', () => {
expect(component.$el.querySelector('.stage-column-list')).not.toBeNull();
});
it('should include the no-margin class on the first child', function() {
it('should include the no-margin class on the first child', () => {
const firstStageColumnElement = component.$el.querySelector(
'.stage-column-list .stage-column',
);
......@@ -57,7 +59,7 @@ describe('graph component', () => {
expect(firstStageColumnElement.classList.contains('no-margin')).toEqual(true);
});
it('should include the has-only-one-job class on the first child', function() {
it('should include the has-only-one-job class on the first child', () => {
const firstStageColumnElement = component.$el.querySelector(
'.stage-column-list .stage-column',
);
......@@ -65,7 +67,7 @@ describe('graph component', () => {
expect(firstStageColumnElement.classList.contains('has-only-one-job')).toEqual(true);
});
it('should include the left-margin class on the second child', function() {
it('should include the left-margin class on the second child', () => {
const firstStageColumnElement = component.$el.querySelector(
'.stage-column-list .stage-column:last-child',
);
......@@ -73,44 +75,96 @@ describe('graph component', () => {
expect(firstStageColumnElement.classList.contains('left-margin')).toEqual(true);
});
it('should include the has-linked-pipelines flag', function() {
it('should include the has-linked-pipelines flag', () => {
expect(component.$el.querySelector('.has-linked-pipelines')).not.toBeNull();
});
});
describe('computeds and methods', function() {
describe('capitalizeStageName', function() {
it('it capitalizes the stage name', function() {
describe('computeds and methods', () => {
describe('capitalizeStageName', () => {
it('it capitalizes the stage name', () => {
expect(component.capitalizeStageName('mystage')).toBe('Mystage');
});
});
describe('stageConnectorClass', function() {
it('it returns left-margin when there is a triggerer', function() {
describe('stageConnectorClass', () => {
it('it returns left-margin when there is a triggerer', () => {
expect(component.stageConnectorClass(0, { groups: ['job'] })).toBe('no-margin');
});
});
});
describe('linked pipelines components', function() {
it('should coerce triggeredBy into a collection', function() {
expect(component.triggeredBy.length).toBe(1);
});
it('should render an upstream pipelines column', function() {
describe('linked pipelines components', () => {
it('should render an upstream pipelines column', () => {
expect(component.$el.querySelector('.linked-pipelines-column')).not.toBeNull();
expect(component.$el.innerHTML).toContain('Upstream');
});
it('should render a downstream pipelines column', function() {
it('should render a downstream pipelines column', () => {
expect(component.$el.querySelector('.linked-pipelines-column')).not.toBeNull();
expect(component.$el.innerHTML).toContain('Downstream');
});
describe('triggered by', () => {
it('should emit `onClickTriggeredBy` when triggered by linked pipeline is clicked', () => {
spyOn(component, '$emit');
component.$el.querySelector('#js-linked-pipeline-129').click();
expect(component.$emit).toHaveBeenCalledWith(
'onClickTriggeredBy',
linkedPipelineJSON.triggered_by,
);
});
describe('with expanded triggered by pipeline', () => {
it('should render expanded upstream pipeline', () => {
component = mountComponent(GraphComponent, {
isLoading: false,
pipeline: graphJSON,
triggeredByPipelines: [
Object.assign({}, linkedPipelineJSON.triggered_by, { isCollapsed: false }),
],
triggeredPipelines: linkedPipelineJSON.triggered,
triggeredBy: linkedPipelineJSON.triggered_by,
});
expect(component.$el.querySelector('.upstream-pipeline')).not.toBeNull();
});
});
});
describe('triggered ', () => {
it('should emit `onClickTriggered` when triggered linked pipeline is clicked', () => {
spyOn(component, '$emit');
component.$el.querySelector('#js-linked-pipeline-132').click();
expect(component.$emit).toHaveBeenCalledWith(
'onClickTriggered',
linkedPipelineJSON.triggered[0],
);
});
describe('with expanded triggered pipeline', () => {
it('should render expanded downstream pipeline', () => {
component = mountComponent(GraphComponent, {
isLoading: false,
pipeline: graphJSON,
triggeredByPipelines: [linkedPipelineJSON.triggered_by],
triggeredPipelines: [
Object.assign({}, linkedPipelineJSON.triggered[0], { isCollapsed: false }),
],
triggered: linkedPipelineJSON.triggered[0],
});
expect(component.$el.querySelector('.downstream-pipeline')).not.toBeNull();
});
});
});
});
});
describe('when linked pipelines are not present', function() {
beforeEach(function() {
describe('when linked pipelines are not present', () => {
beforeEach(() => {
const pipeline = Object.assign(graphJSON, { triggered: null, triggered_by: null });
component = mountComponent(GraphComponent, {
isLoading: false,
......@@ -118,24 +172,24 @@ describe('graph component', () => {
});
});
describe('rendered output', function() {
it('should include the first column with a no margin', function() {
describe('rendered output', () => {
it('should include the first column with a no margin', () => {
const firstColumn = component.$el.querySelector('.stage-column:first-child');
expect(firstColumn.classList.contains('no-margin')).toEqual(true);
});
it('should not render a linked pipelines column', function() {
it('should not render a linked pipelines column', () => {
expect(component.$el.querySelector('.linked-pipelines-column')).toBeNull();
});
});
describe('stageConnectorClass', function() {
it('it returns left-margin when no triggerer and there is one job', function() {
describe('stageConnectorClass', () => {
it('it returns left-margin when no triggerer and there is one job', () => {
expect(component.stageConnectorClass(0, { groups: ['job'] })).toBe('no-margin');
});
it('it returns left-margin when no triggerer and not the first stage', function() {
it('it returns left-margin when no triggerer and not the first stage', () => {
expect(component.stageConnectorClass(99, { groups: ['job'] })).toBe('left-margin');
});
});
......
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