Commit 5afd00e1 authored by Sarah Groff Hennigh-Palermo's avatar Sarah Groff Hennigh-Palermo Committed by Natalia Tepluhina

Add new sections and apply to component

Adds query and UI elements for new linked pipeline display
Some changes to existing components to accomodate
parent 1243e528
<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