Commit e50538f4 authored by Natalia Tepluhina's avatar Natalia Tepluhina

Merge branch '276949-pipeline-restructure-6' into 'master'

Pipeline Graph Structural Update: Add Linked Pipeline Interactions

See merge request gitlab-org/gitlab!49164
parents 18ec1870 5afd00e1
<script>
import LinkedGraphWrapper from '../graph_shared/linked_graph_wrapper.vue';
import LinkedPipelinesColumn from './linked_pipelines_column.vue';
import StageColumnComponent from './stage_column_component.vue';
import { MAIN } from './constants';
import { DOWNSTREAM, MAIN, UPSTREAM } from './constants';
export default {
name: 'PipelineGraph',
components: {
LinkedGraphWrapper,
LinkedPipelinesColumn,
StageColumnComponent,
},
props: {
......@@ -23,10 +27,60 @@ export default {
default: MAIN,
},
},
pipelineTypeConstants: {
DOWNSTREAM,
UPSTREAM,
},
data() {
return {
hoveredJobName: '',
pipelineExpanded: {
jobName: '',
expanded: false,
},
};
},
computed: {
downstreamPipelines() {
return this.hasDownstreamPipelines ? this.pipeline.downstream : [];
},
graph() {
return this.pipeline.stages;
},
hasDownstreamPipelines() {
return Boolean(this.pipeline?.downstream?.length > 0);
},
hasUpstreamPipelines() {
return Boolean(this.pipeline?.upstream?.length > 0);
},
// The two show checks prevent upstream / downstream from showing redundant linked columns
showDownstreamPipelines() {
return (
this.hasDownstreamPipelines && this.type !== this.$options.pipelineTypeConstants.UPSTREAM
);
},
showUpstreamPipelines() {
return (
this.hasUpstreamPipelines && this.type !== this.$options.pipelineTypeConstants.DOWNSTREAM
);
},
upstreamPipelines() {
return this.hasUpstreamPipelines ? this.pipeline.upstream : [];
},
},
methods: {
handleError(errorType) {
this.$emit('error', errorType);
},
setJob(jobName) {
this.hoveredJobName = jobName;
},
togglePipelineExpanded(jobName, expanded) {
this.pipelineExpanded = {
expanded,
jobName: expanded ? jobName : '',
};
},
},
};
</script>
......@@ -36,13 +90,39 @@ export default {
class="gl-pipeline-min-h gl-display-flex gl-position-relative gl-overflow-auto gl-bg-gray-10 gl-white-space-nowrap"
:class="{ 'gl-py-5': !isLinkedPipeline }"
>
<stage-column-component
v-for="stage in graph"
:key="stage.name"
:title="stage.name"
:groups="stage.groups"
:action="stage.status.action"
/>
<linked-graph-wrapper>
<template #upstream>
<linked-pipelines-column
v-if="showUpstreamPipelines"
:linked-pipelines="upstreamPipelines"
:column-title="__('Upstream')"
:type="$options.pipelineTypeConstants.UPSTREAM"
@error="handleError"
/>
</template>
<template #main>
<stage-column-component
v-for="stage in graph"
:key="stage.name"
:title="stage.name"
:groups="stage.groups"
:action="stage.status.action"
:job-hovered="hoveredJobName"
:pipeline-expanded="pipelineExpanded"
/>
</template>
<template #downstream>
<linked-pipelines-column
v-if="showDownstreamPipelines"
:linked-pipelines="downstreamPipelines"
:column-title="__('Downstream')"
:type="$options.pipelineTypeConstants.DOWNSTREAM"
@downstreamHovered="setJob"
@pipelineExpandToggle="togglePipelineExpanded"
@error="handleError"
/>
</template>
</linked-graph-wrapper>
</div>
</div>
</template>
......@@ -42,7 +42,7 @@ export default {
};
},
update(data) {
return unwrapPipelineData(this.pipelineIid, data);
return unwrapPipelineData(this.pipelineProjectPath, data);
},
error() {
this.reportFailure(LOAD_FAILURE);
......@@ -77,13 +77,11 @@ export default {
};
</script>
<template>
<gl-alert v-if="showAlert" :variant="alert.variant" @dismiss="hideAlert">
{{ alert.text }}
</gl-alert>
<gl-loading-icon
v-else-if="$apollo.queries.pipeline.loading"
class="gl-mx-auto gl-my-4"
size="lg"
/>
<pipeline-graph v-else :pipeline="pipeline" />
<div>
<gl-alert v-if="showAlert" :variant="alert.variant" @dismiss="hideAlert">
{{ alert.text }}
</gl-alert>
<gl-loading-icon v-if="$apollo.queries.pipeline.loading" class="gl-mx-auto gl-my-4" size="lg" />
<pipeline-graph v-if="pipeline" :pipeline="pipeline" @error="reportFailure" />
</div>
</template>
......@@ -25,23 +25,33 @@ export default {
type: String,
required: true,
},
pipeline: {
type: Object,
expanded: {
type: Boolean,
required: true,
},
projectId: {
type: Number,
pipeline: {
type: Object,
required: true,
},
type: {
type: String,
required: true,
},
},
data() {
return {
expanded: false,
};
/*
The next two props will be removed or required
once the graph transition is done.
See: https://gitlab.com/gitlab-org/gitlab/-/issues/291043
*/
isLoading: {
type: Boolean,
required: false,
default: false,
},
projectId: {
type: Number,
required: false,
default: -1,
},
},
computed: {
tooltipText() {
......@@ -74,6 +84,9 @@ export default {
}
return __('Multi-project');
},
pipelineIsLoading() {
return Boolean(this.isLoading || this.pipeline.isLoading);
},
isDownstream() {
return this.type === DOWNSTREAM;
},
......@@ -81,7 +94,9 @@ export default {
return this.type === UPSTREAM;
},
isSameProject() {
return this.projectId === this.pipeline.project.id;
return this.projectId > -1
? this.projectId === this.pipeline.project.id
: !this.pipeline.multiproject;
},
sourceJobName() {
return accessValue(this.dataMethod, 'sourceJob', this.pipeline);
......@@ -101,16 +116,15 @@ export default {
},
methods: {
onClickLinkedPipeline() {
this.$root.$emit('bv::hide::tooltip', this.buttonId);
this.expanded = !this.expanded;
this.hideTooltips();
this.$emit('pipelineClicked', this.$refs.linkedPipeline);
this.$emit('pipelineExpandToggle', this.pipeline.source_job.name, this.expanded);
this.$emit('pipelineExpandToggle', this.sourceJobName, !this.expanded);
},
hideTooltips() {
this.$root.$emit('bv::hide::tooltip');
},
onDownstreamHovered() {
this.$emit('downstreamHovered', this.pipeline.source_job.name);
this.$emit('downstreamHovered', this.sourceJobName);
},
onDownstreamHoverLeave() {
this.$emit('downstreamHovered', '');
......@@ -120,10 +134,10 @@ export default {
</script>
<template>
<li
<div
ref="linkedPipeline"
v-gl-tooltip
class="linked-pipeline build"
class="linked-pipeline build gl-pipeline-job-width"
:title="tooltipText"
:class="{ 'downstream-pipeline': isDownstream }"
data-qa-selector="child_pipeline"
......@@ -136,8 +150,9 @@ export default {
>
<div class="gl-display-flex">
<ci-status
v-if="!pipeline.isLoading"
v-if="!pipelineIsLoading"
:status="pipelineStatus"
:size="24"
css-classes="gl-top-0 gl-pr-2"
/>
<div v-else class="gl-pr-2"><gl-loading-icon inline /></div>
......@@ -160,10 +175,10 @@ export default {
class="gl-absolute gl-top-0 gl-bottom-0 gl-shadow-none! gl-rounded-0!"
:class="`js-pipeline-expand-${pipeline.id} ${expandButtonPosition}`"
:icon="expandedIcon"
data-testid="expandPipelineButton"
data-testid="expand-pipeline-button"
data-qa-selector="expand_pipeline_button"
@click="onClickLinkedPipeline"
/>
</div>
</li>
</div>
</template>
<script>
import getPipelineDetails from '../../graphql/queries/get_pipeline_details.query.graphql';
import LinkedPipeline from './linked_pipeline.vue';
import { LOAD_FAILURE } from '../../constants';
import { UPSTREAM } from './constants';
import { unwrapPipelineData } from './utils';
export default {
components: {
LinkedPipeline,
PipelineGraph: () => import('./graph_component.vue'),
},
props: {
columnTitle: {
......@@ -19,11 +23,22 @@ export default {
type: String,
required: true,
},
projectId: {
type: Number,
required: true,
},
},
data() {
return {
currentPipeline: null,
loadingPipelineId: null,
pipelineExpanded: false,
};
},
titleClasses: [
'gl-font-weight-bold',
'gl-pipeline-job-width',
'gl-text-truncate',
'gl-line-height-36',
'gl-pl-3',
'gl-mb-5',
],
computed: {
columnClass() {
const positionValues = {
......@@ -35,14 +50,66 @@ export default {
graphPosition() {
return this.isUpstream ? 'left' : 'right';
},
// Refactor string match when BE returns Upstream/Downstream indicators
isUpstream() {
return this.type === UPSTREAM;
},
computedTitleClasses() {
const positionalClasses = this.isUpstream
? ['gl-w-full', 'gl-text-right', 'gl-linked-pipeline-padding']
: [];
return [...this.$options.titleClasses, ...positionalClasses];
},
},
methods: {
onPipelineClick(downstreamNode, pipeline, index) {
this.$emit('linkedPipelineClick', pipeline, index, downstreamNode);
getPipelineData(pipeline) {
const projectPath = pipeline.project.fullPath;
this.$apollo.addSmartQuery('currentPipeline', {
query: getPipelineDetails,
variables() {
return {
projectPath,
iid: pipeline.iid,
};
},
update(data) {
return unwrapPipelineData(projectPath, data);
},
result() {
this.loadingPipelineId = null;
},
error() {
this.$emit('error', LOAD_FAILURE);
},
});
},
isExpanded(id) {
return Boolean(this.currentPipeline?.id && id === this.currentPipeline.id);
},
isLoadingPipeline(id) {
return this.loadingPipelineId === id;
},
onPipelineClick(pipeline) {
/* If the clicked pipeline has been expanded already, close it, clear, exit */
if (this.currentPipeline?.id === pipeline.id) {
this.pipelineExpanded = false;
this.currentPipeline = null;
return;
}
/* Set the loading id */
this.loadingPipelineId = pipeline.id;
/*
Expand the pipeline.
If this was not a toggle close action, and
it was already showing a different pipeline, then
this will be a no-op, but that doesn't matter.
*/
this.pipelineExpanded = true;
this.getPipelineData(pipeline);
},
onDownstreamHovered(jobName) {
this.$emit('downstreamHovered', jobName);
......@@ -60,25 +127,40 @@ export default {
</script>
<template>
<div :class="columnClass" class="stage-column linked-pipelines-column">
<div class="stage-name linked-pipelines-column-title">{{ columnTitle }}</div>
<div v-if="isUpstream" class="cross-project-triangle"></div>
<ul>
<linked-pipeline
v-for="(pipeline, index) in linkedPipelines"
:key="pipeline.id"
:class="{
active: pipeline.isExpanded,
'left-connector': pipeline.isExpanded && graphPosition === 'left',
}"
:pipeline="pipeline"
:column-title="columnTitle"
:project-id="projectId"
:type="type"
@pipelineClicked="onPipelineClick($event, pipeline, index)"
@downstreamHovered="onDownstreamHovered"
@pipelineExpandToggle="onPipelineExpandToggle"
/>
</ul>
<div class="gl-display-flex">
<div :class="columnClass" class="linked-pipelines-column">
<div data-testid="linked-column-title" class="stage-name" :class="computedTitleClasses">
{{ columnTitle }}
</div>
<ul class="gl-pl-0">
<li
v-for="pipeline in linkedPipelines"
:key="pipeline.id"
class="gl-display-flex gl-mb-4"
:class="{ 'gl-flex-direction-row-reverse': isUpstream }"
>
<linked-pipeline
class="gl-display-inline-block"
:is-loading="isLoadingPipeline(pipeline.id)"
:pipeline="pipeline"
:column-title="columnTitle"
:type="type"
:expanded="isExpanded(pipeline.id)"
@downstreamHovered="onDownstreamHovered"
@pipelineClicked="onPipelineClick(pipeline)"
@pipelineExpandToggle="onPipelineExpandToggle"
/>
<div v-if="isExpanded(pipeline.id)" class="gl-display-inline-block">
<pipeline-graph
v-if="currentPipeline"
:type="type"
class="d-inline-block gl-mt-n2"
:pipeline="currentPipeline"
:is-linked-pipeline="true"
/>
</div>
</li>
</ul>
</div>
</div>
</template>
......@@ -35,7 +35,9 @@ export default {
graphPosition() {
return this.isUpstream ? 'left' : 'right';
},
// Refactor string match when BE returns Upstream/Downstream indicators
isExpanded() {
return this.pipeline?.isExpanded || false;
},
isUpstream() {
return this.type === UPSTREAM;
},
......@@ -64,21 +66,22 @@ export default {
<div class="stage-name linked-pipelines-column-title">{{ columnTitle }}</div>
<div v-if="isUpstream" class="cross-project-triangle"></div>
<ul>
<linked-pipeline
v-for="(pipeline, index) in linkedPipelines"
:key="pipeline.id"
:class="{
active: pipeline.isExpanded,
'left-connector': pipeline.isExpanded && graphPosition === 'left',
}"
:pipeline="pipeline"
:column-title="columnTitle"
:project-id="projectId"
:type="type"
@pipelineClicked="onPipelineClick($event, pipeline, index)"
@downstreamHovered="onDownstreamHovered"
@pipelineExpandToggle="onPipelineExpandToggle"
/>
<li v-for="(pipeline, index) in linkedPipelines" :key="pipeline.id">
<linked-pipeline
:class="{
active: pipeline.isExpanded,
'left-connector': pipeline.isExpanded && graphPosition === 'left',
}"
:pipeline="pipeline"
:column-title="columnTitle"
:project-id="projectId"
:type="type"
:expanded="isExpanded"
@pipelineClicked="onPipelineClick($event, pipeline, index)"
@downstreamHovered="onDownstreamHovered"
@pipelineExpandToggle="onPipelineExpandToggle"
/>
</li>
</ul>
</div>
</template>
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { unwrapStagesWithNeeds } from '../unwrapping_utils';
const addMulti = (mainId, pipeline) => {
return { ...pipeline, multiproject: mainId !== pipeline.id };
const addMulti = (mainPipelineProjectPath, linkedPipeline) => {
return {
...linkedPipeline,
multiproject: mainPipelineProjectPath !== linkedPipeline.project.fullPath,
};
};
const unwrapPipelineData = (mainPipelineId, data) => {
const transformId = linkedPipeline => {
return { ...linkedPipeline, id: getIdFromGraphQLId(linkedPipeline.id) };
};
const unwrapPipelineData = (mainPipelineProjectPath, data) => {
if (!data?.project?.pipeline) {
return null;
}
const { pipeline } = data.project;
const {
id,
upstream,
downstream,
stages: { nodes: stages },
} = data.project.pipeline;
} = pipeline;
const nodes = unwrapStagesWithNeeds(stages);
return {
id,
...pipeline,
id: getIdFromGraphQLId(pipeline.id),
stages: nodes,
upstream: upstream ? [upstream].map(addMulti.bind(null, mainPipelineId)) : [],
downstream: downstream ? downstream.map(addMulti.bind(null, mainPipelineId)) : [],
upstream: upstream
? [upstream].map(addMulti.bind(null, mainPipelineProjectPath)).map(transformId)
: [],
downstream: downstream
? downstream.nodes.map(addMulti.bind(null, mainPipelineProjectPath)).map(transformId)
: [],
};
};
......
<template>
<div class="gl-display-flex">
<slot name="upstream"></slot>
<slot name="main"></slot>
<slot name="downstream"></slot>
</div>
</template>
......@@ -17,7 +17,7 @@ export default {
<template>
<div>
<div
class="gl-display-flex gl-align-items-center gl-w-full gl-px-8 gl-py-4 gl-mb-5"
class="gl-display-flex gl-align-items-center gl-w-full gl-px-8 gl-mb-5"
:class="stageClasses"
>
<slot name="stages"> </slot>
......
fragment LinkedPipelineData on Pipeline {
id
iid
path
status: detailedStatus {
group
label
icon
}
sourceJob {
name
}
project {
name
fullPath
}
}
#import "../fragments/linked_pipelines.fragment.graphql"
query getPipelineDetails($projectPath: ID!, $iid: ID!) {
project(fullPath: $projectPath) {
pipeline(iid: $iid) {
id: iid
id
iid
downstream {
nodes {
...LinkedPipelineData
}
}
upstream {
...LinkedPipelineData
}
stages {
nodes {
name
......
......@@ -2,6 +2,7 @@ query getPipelineHeaderData($fullPath: ID!, $iid: ID!) {
project(fullPath: $fullPath) {
pipeline(iid: $iid) {
id
iid
status
retryable
cancelable
......
......@@ -139,6 +139,10 @@
width: 186px;
}
.gl-linked-pipeline-padding {
padding-right: 120px;
}
.gl-build-content {
@include build-content();
}
......
......@@ -15,8 +15,8 @@ describe('graph component', () => {
let mediator;
let wrapper;
const findExpandPipelineBtn = () => wrapper.find('[data-testid="expandPipelineButton"]');
const findAllExpandPipelineBtns = () => wrapper.findAll('[data-testid="expandPipelineButton"]');
const findExpandPipelineBtn = () => wrapper.find('[data-testid="expand-pipeline-button"]');
const findAllExpandPipelineBtns = () => wrapper.findAll('[data-testid="expand-pipeline-button"]');
const findStageColumns = () => wrapper.findAll(StageColumnComponentLegacy);
const findStageColumnAt = i => findStageColumns().at(i);
......
import { shallowMount } from '@vue/test-utils';
import { mount, shallowMount } from '@vue/test-utils';
import PipelineGraph from '~/pipelines/components/graph/graph_component.vue';
import StageColumnComponent from '~/pipelines/components/graph/stage_column_component.vue';
import LinkedPipelinesColumn from '~/pipelines/components/graph/linked_pipelines_column.vue';
import { unwrapPipelineData } from '~/pipelines/components/graph/utils';
import { mockPipelineResponse } from './mock_data';
import { GRAPHQL } from '~/pipelines/components/graph/constants';
import {
generateResponse,
mockPipelineResponse,
pipelineWithUpstreamDownstream,
} from './mock_data';
describe('graph component', () => {
let wrapper;
......@@ -11,10 +15,8 @@ describe('graph component', () => {
const findLinkedColumns = () => wrapper.findAll(LinkedPipelinesColumn);
const findStageColumns = () => wrapper.findAll(StageColumnComponent);
const generateResponse = raw => unwrapPipelineData(raw.data.project.pipeline.id, raw.data);
const defaultProps = {
pipeline: generateResponse(mockPipelineResponse),
pipeline: generateResponse(mockPipelineResponse, 'root/fungi-xoxo'),
};
const createComponent = ({ mountFn = shallowMount, props = {} } = {}) => {
......@@ -23,6 +25,9 @@ describe('graph component', () => {
...defaultProps,
...props,
},
provide: {
dataMethod: GRAPHQL,
},
});
};
......@@ -33,7 +38,7 @@ describe('graph component', () => {
describe('with data', () => {
beforeEach(() => {
createComponent();
createComponent({ mountFn: mount });
});
it('renders the main columns in the graph', () => {
......@@ -43,11 +48,24 @@ describe('graph component', () => {
describe('when linked pipelines are not present', () => {
beforeEach(() => {
createComponent();
createComponent({ mountFn: mount });
});
it('should not render a linked pipelines column', () => {
expect(findLinkedColumns()).toHaveLength(0);
});
});
describe('when linked pipelines are present', () => {
beforeEach(() => {
createComponent({
mountFn: mount,
props: { pipeline: pipelineWithUpstreamDownstream(mockPipelineResponse) },
});
});
it('should render linked pipelines columns', () => {
expect(findLinkedColumns()).toHaveLength(2);
});
});
});
......@@ -17,7 +17,7 @@ describe('Linked pipeline', () => {
const findLinkedPipeline = () => wrapper.find({ ref: 'linkedPipeline' });
const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
const findPipelineLink = () => wrapper.find('[data-testid="pipelineLink"]');
const findExpandButton = () => wrapper.find('[data-testid="expandPipelineButton"]');
const findExpandButton = () => wrapper.find('[data-testid="expand-pipeline-button"]');
const createWrapper = (propsData, data = []) => {
wrapper = mount(LinkedPipelineComponent, {
......@@ -40,20 +40,13 @@ describe('Linked pipeline', () => {
projectId: invalidTriggeredPipelineId,
columnTitle: 'Downstream',
type: DOWNSTREAM,
expanded: false,
};
beforeEach(() => {
createWrapper(props);
});
it('should render a list item as the containing element', () => {
expect(wrapper.element.tagName).toBe('LI');
});
it('should render a button', () => {
expect(findButton().exists()).toBe(true);
});
it('should render the project name', () => {
expect(wrapper.text()).toContain(props.pipeline.project.name);
});
......@@ -105,12 +98,14 @@ describe('Linked pipeline', () => {
projectId: validTriggeredPipelineId,
columnTitle: 'Downstream',
type: DOWNSTREAM,
expanded: false,
};
const upstreamProps = {
...downstreamProps,
columnTitle: 'Upstream',
type: UPSTREAM,
expanded: false,
};
it('parent/child label container should exist', () => {
......@@ -173,7 +168,7 @@ describe('Linked pipeline', () => {
`(
'$pipelineType.columnTitle pipeline button icon should be $anglePosition if expanded state is $expanded',
({ pipelineType, anglePosition, expanded }) => {
createWrapper(pipelineType, { expanded });
createWrapper({ ...pipelineType, expanded });
expect(findExpandButton().props('icon')).toBe(anglePosition);
},
);
......@@ -185,6 +180,7 @@ describe('Linked pipeline', () => {
projectId: invalidTriggeredPipelineId,
columnTitle: 'Downstream',
type: DOWNSTREAM,
expanded: false,
};
beforeEach(() => {
......@@ -202,6 +198,7 @@ describe('Linked pipeline', () => {
projectId: validTriggeredPipelineId,
columnTitle: 'Downstream',
type: DOWNSTREAM,
expanded: false,
};
beforeEach(() => {
......@@ -219,10 +216,7 @@ describe('Linked pipeline', () => {
jest.spyOn(wrapper.vm.$root, '$emit');
findButton().trigger('click');
expect(wrapper.vm.$root.$emit.mock.calls[0]).toEqual([
'bv::hide::tooltip',
'js-linked-pipeline-34993051',
]);
expect(wrapper.vm.$root.$emit.mock.calls[0]).toEqual(['bv::hide::tooltip']);
});
it('should emit downstreamHovered with job name on mouseover', () => {
......
import { shallowMount } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import { mount, shallowMount, createLocalVue } from '@vue/test-utils';
import createMockApollo from 'jest/helpers/mock_apollo_helper';
import PipelineGraph from '~/pipelines/components/graph/graph_component.vue';
import LinkedPipelinesColumn from '~/pipelines/components/graph/linked_pipelines_column.vue';
import LinkedPipeline from '~/pipelines/components/graph/linked_pipeline.vue';
import { UPSTREAM } from '~/pipelines/components/graph/constants';
import mockData from './linked_pipelines_mock_data';
import getPipelineDetails from '~/pipelines/graphql/queries/get_pipeline_details.query.graphql';
import { DOWNSTREAM, GRAPHQL } from '~/pipelines/components/graph/constants';
import { LOAD_FAILURE } from '~/pipelines/constants';
import {
mockPipelineResponse,
pipelineWithUpstreamDownstream,
wrappedPipelineReturn,
} from './mock_data';
const processedPipeline = pipelineWithUpstreamDownstream(mockPipelineResponse);
describe('Linked Pipelines Column', () => {
const propsData = {
const defaultProps = {
columnTitle: 'Upstream',
linkedPipelines: mockData.triggered,
graphPosition: 'right',
projectId: 19,
type: UPSTREAM,
linkedPipelines: processedPipeline.downstream,
type: DOWNSTREAM,
};
let wrapper;
const findLinkedColumnTitle = () => wrapper.find('[data-testid="linked-column-title"]');
const findLinkedPipelineElements = () => wrapper.findAll(LinkedPipeline);
const findPipelineGraph = () => wrapper.find(PipelineGraph);
const findExpandButton = () => wrapper.find('[data-testid="expand-pipeline-button"]');
beforeEach(() => {
wrapper = shallowMount(LinkedPipelinesColumn, { propsData });
});
const localVue = createLocalVue();
localVue.use(VueApollo);
const createComponent = ({ apolloProvider, mountFn = shallowMount, props = {} } = {}) => {
wrapper = mountFn(LinkedPipelinesColumn, {
apolloProvider,
localVue,
propsData: {
...defaultProps,
...props,
},
provide: {
dataMethod: GRAPHQL,
},
});
};
const createComponentWithApollo = (
mountFn = shallowMount,
getPipelineDetailsHandler = jest.fn().mockResolvedValue(wrappedPipelineReturn),
) => {
const requestHandlers = [[getPipelineDetails, getPipelineDetailsHandler]];
const apolloProvider = createMockApollo(requestHandlers);
createComponent({ apolloProvider, mountFn });
};
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
it('renders the pipeline orientation', () => {
const titleElement = wrapper.find('.linked-pipelines-column-title');
describe('it renders correctly', () => {
beforeEach(() => {
createComponent();
});
it('renders the pipeline title', () => {
expect(findLinkedColumnTitle().text()).toBe(defaultProps.columnTitle);
});
expect(titleElement.text()).toBe(propsData.columnTitle);
it('renders the correct number of linked pipelines', () => {
expect(findLinkedPipelineElements()).toHaveLength(defaultProps.linkedPipelines.length);
});
});
it('renders the correct number of linked pipelines', () => {
const linkedPipelineElements = wrapper.findAll(LinkedPipeline);
describe('click action', () => {
const clickExpandButton = async () => {
await findExpandButton().trigger('click');
await wrapper.vm.$nextTick();
};
expect(linkedPipelineElements.length).toBe(propsData.linkedPipelines.length);
});
const clickExpandButtonAndAwaitTimers = async () => {
await clickExpandButton();
jest.runOnlyPendingTimers();
await wrapper.vm.$nextTick();
};
describe('when successful', () => {
beforeEach(() => {
createComponentWithApollo(mount);
});
it('toggles the pipeline visibility', async () => {
expect(findPipelineGraph().exists()).toBe(false);
await clickExpandButtonAndAwaitTimers();
expect(findPipelineGraph().exists()).toBe(true);
await clickExpandButton();
expect(findPipelineGraph().exists()).toBe(false);
});
});
describe('on error', () => {
beforeEach(() => {
createComponentWithApollo(mount, jest.fn().mockRejectedValue(new Error('GraphQL error')));
});
it('emits the error', async () => {
await clickExpandButton();
expect(wrapper.emitted().error).toEqual([[LOAD_FAILURE]]);
});
it('renders cross project triangle when column is upstream', () => {
expect(wrapper.find('.cross-project-triangle').exists()).toBe(true);
it('does not show the pipeline', async () => {
expect(findPipelineGraph().exists()).toBe(false);
await clickExpandButtonAndAwaitTimers();
expect(findPipelineGraph().exists()).toBe(false);
});
});
});
});
import { unwrapPipelineData } from '~/pipelines/components/graph/utils';
export const mockPipelineResponse = {
data: {
project: {
__typename: 'Project',
pipeline: {
__typename: 'Pipeline',
id: '22',
id: 163,
iid: '22',
downstream: null,
upstream: null,
stages: {
__typename: 'CiStageConnection',
nodes: [
......@@ -497,3 +502,164 @@ export const mockPipelineResponse = {
},
},
};
export const downstream = {
nodes: [
{
id: 175,
iid: '31',
path: '/root/elemenohpee/-/pipelines/175',
status: {
group: 'success',
label: 'passed',
icon: 'status_success',
__typename: 'DetailedStatus',
},
sourceJob: {
name: 'test_c',
__typename: 'CiJob',
},
project: {
id: 'gid://gitlab/Project/25',
name: 'elemenohpee',
fullPath: 'root/elemenohpee',
__typename: 'Project',
},
__typename: 'Pipeline',
multiproject: true,
},
{
id: 181,
iid: '27',
path: '/root/abcd-dag/-/pipelines/181',
status: {
group: 'success',
label: 'passed',
icon: 'status_success',
__typename: 'DetailedStatus',
},
sourceJob: {
name: 'test_d',
__typename: 'CiJob',
},
project: {
id: 'gid://gitlab/Project/23',
name: 'abcd-dag',
fullPath: 'root/abcd-dag',
__typename: 'Project',
},
__typename: 'Pipeline',
multiproject: false,
},
],
};
export const upstream = {
id: 161,
iid: '24',
path: '/root/abcd-dag/-/pipelines/161',
status: {
group: 'success',
label: 'passed',
icon: 'status_success',
__typename: 'DetailedStatus',
},
sourceJob: null,
project: {
id: 'gid://gitlab/Project/23',
name: 'abcd-dag',
fullPath: 'root/abcd-dag',
__typename: 'Project',
},
__typename: 'Pipeline',
multiproject: true,
};
export const wrappedPipelineReturn = {
data: {
project: {
pipeline: {
id: 'gid://gitlab/Ci::Pipeline/175',
iid: '38',
downstream: {
nodes: [],
},
upstream: {
id: 'gid://gitlab/Ci::Pipeline/174',
iid: '37',
path: '/root/elemenohpee/-/pipelines/174',
status: {
group: 'success',
label: 'passed',
icon: 'status_success',
},
sourceJob: {
name: 'test_c',
},
project: {
id: 'gid://gitlab/Project/25',
name: 'elemenohpee',
fullPath: 'root/elemenohpee',
},
},
stages: {
nodes: [
{
name: 'build',
status: {
action: null,
},
groups: {
nodes: [
{
status: {
label: 'passed',
group: 'success',
icon: 'status_success',
},
name: 'build_n',
size: 1,
jobs: {
nodes: [
{
name: 'build_n',
scheduledAt: null,
needs: {
nodes: [],
},
status: {
icon: 'status_success',
tooltip: 'passed',
hasDetails: true,
detailsPath: '/root/elemenohpee/-/jobs/1662',
group: 'success',
action: {
buttonTitle: 'Retry this job',
icon: 'retry',
path: '/root/elemenohpee/-/jobs/1662/retry',
title: 'Retry',
},
},
},
],
},
},
],
},
},
],
},
},
},
},
};
export const generateResponse = (raw, mockPath) => unwrapPipelineData(mockPath, raw.data);
export const pipelineWithUpstreamDownstream = base => {
const pip = { ...base };
pip.data.project.pipeline.downstream = downstream;
pip.data.project.pipeline.upstream = upstream;
return generateResponse(pip, 'root/abcd-dag');
};
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