Commit da5814e1 authored by Andrew Fontaine's avatar Andrew Fontaine

Merge branch '298973-needs-selector-segment-and-links' into 'master'

Pipeline Graph: Update graph state selection

See merge request gitlab-org/gitlab!59685
parents 103901de c8431dcb
...@@ -25,6 +25,10 @@ export default { ...@@ -25,6 +25,10 @@ export default {
type: Object, type: Object,
required: true, required: true,
}, },
showLinks: {
type: Boolean,
required: true,
},
viewType: { viewType: {
type: String, type: String,
required: true, required: true,
...@@ -91,8 +95,8 @@ export default { ...@@ -91,8 +95,8 @@ export default {
collectMetrics: true, collectMetrics: true,
}; };
}, },
shouldHideLinks() { showJobLinks() {
return this.isStageView; return !this.isStageView && this.showLinks;
}, },
shouldShowStageName() { shouldShowStageName() {
return !this.isStageView; return !this.isStageView;
...@@ -188,6 +192,7 @@ export default { ...@@ -188,6 +192,7 @@ export default {
:config-paths="configPaths" :config-paths="configPaths"
:linked-pipelines="upstreamPipelines" :linked-pipelines="upstreamPipelines"
:column-title="__('Upstream')" :column-title="__('Upstream')"
:show-links="showJobLinks"
:type="$options.pipelineTypeConstants.UPSTREAM" :type="$options.pipelineTypeConstants.UPSTREAM"
:view-type="viewType" :view-type="viewType"
@error="onError" @error="onError"
...@@ -202,9 +207,8 @@ export default { ...@@ -202,9 +207,8 @@ export default {
:container-measurements="measurements" :container-measurements="measurements"
:highlighted-job="hoveredJobName" :highlighted-job="hoveredJobName"
:metrics-config="metricsConfig" :metrics-config="metricsConfig"
:never-show-links="shouldHideLinks" :show-links="showJobLinks"
:view-type="viewType" :view-type="viewType"
default-link-color="gl-stroke-transparent"
@error="onError" @error="onError"
@highlightedJobsChange="updateHighlightedJobs" @highlightedJobsChange="updateHighlightedJobs"
> >
...@@ -234,6 +238,7 @@ export default { ...@@ -234,6 +238,7 @@ export default {
:config-paths="configPaths" :config-paths="configPaths"
:linked-pipelines="downstreamPipelines" :linked-pipelines="downstreamPipelines"
:column-title="__('Downstream')" :column-title="__('Downstream')"
:show-links="showJobLinks"
:type="$options.pipelineTypeConstants.DOWNSTREAM" :type="$options.pipelineTypeConstants.DOWNSTREAM"
:view-type="viewType" :view-type="viewType"
@downstreamHovered="setSourceJob" @downstreamHovered="setSourceJob"
......
...@@ -48,6 +48,7 @@ export default { ...@@ -48,6 +48,7 @@ export default {
pipeline: null, pipeline: null,
pipelineLayers: null, pipelineLayers: null,
showAlert: false, showAlert: false,
showLinks: false,
}; };
}, },
errorTexts: { errorTexts: {
...@@ -182,6 +183,9 @@ export default { ...@@ -182,6 +183,9 @@ export default {
} }
}, },
/* eslint-enable @gitlab/require-i18n-strings */ /* eslint-enable @gitlab/require-i18n-strings */
updateShowLinksState(val) {
this.showLinks = val;
},
updateViewType(type) { updateViewType(type) {
this.currentViewType = type; this.currentViewType = type;
}, },
...@@ -202,7 +206,9 @@ export default { ...@@ -202,7 +206,9 @@ export default {
<graph-view-selector <graph-view-selector
v-if="showGraphViewSelector" v-if="showGraphViewSelector"
:type="currentViewType" :type="currentViewType"
:show-links="showLinks"
@updateViewType="updateViewType" @updateViewType="updateViewType"
@updateShowLinksState="updateShowLinksState"
/> />
</local-storage-sync> </local-storage-sync>
<gl-loading-icon v-if="showLoadingIcon" class="gl-mx-auto gl-my-4" size="lg" /> <gl-loading-icon v-if="showLoadingIcon" class="gl-mx-auto gl-my-4" size="lg" />
...@@ -211,6 +217,7 @@ export default { ...@@ -211,6 +217,7 @@ export default {
:config-paths="configPaths" :config-paths="configPaths"
:pipeline="pipeline" :pipeline="pipeline"
:pipeline-layers="getPipelineLayers()" :pipeline-layers="getPipelineLayers()"
:show-links="showLinks"
:view-type="currentViewType" :view-type="currentViewType"
@error="reportFailure" @error="reportFailure"
@refreshPipelineGraph="refreshPipelineGraph" @refreshPipelineGraph="refreshPipelineGraph"
......
<script> <script>
import { GlDropdown, GlDropdownItem, GlIcon, GlSprintf } from '@gitlab/ui'; import { GlLoadingIcon, GlSegmentedControl, GlToggle } from '@gitlab/ui';
import { __ } from '~/locale'; import { __ } from '~/locale';
import { STAGE_VIEW, LAYER_VIEW } from './constants'; import { STAGE_VIEW, LAYER_VIEW } from './constants';
export default { export default {
name: 'GraphViewSelector', name: 'GraphViewSelector',
components: { components: {
GlDropdown, GlLoadingIcon,
GlDropdownItem, GlSegmentedControl,
GlIcon, GlToggle,
GlSprintf,
}, },
props: { props: {
showLinks: {
type: Boolean,
required: true,
},
type: { type: {
type: String, type: String,
required: true, required: true,
...@@ -19,67 +22,119 @@ export default { ...@@ -19,67 +22,119 @@ export default {
}, },
data() { data() {
return { return {
currentViewType: STAGE_VIEW, currentViewType: this.type,
showLinksActive: false,
isToggleLoading: false,
isSwitcherLoading: false,
}; };
}, },
i18n: { i18n: {
labelText: __('Order jobs by'), viewLabelText: __('Group jobs by'),
linksLabelText: __('Show dependencies'),
}, },
views: { views: {
[STAGE_VIEW]: { [STAGE_VIEW]: {
type: STAGE_VIEW, type: STAGE_VIEW,
text: { text: {
primary: __('Stage'), primary: __('Stage'),
secondary: __('View the jobs grouped into stages'),
}, },
}, },
[LAYER_VIEW]: { [LAYER_VIEW]: {
type: LAYER_VIEW, type: LAYER_VIEW,
text: { text: {
primary: __('%{codeStart}needs:%{codeEnd} relationships'), primary: __('Job dependencies'),
secondary: __('View what jobs are needed for a job to run'),
}, },
}, },
}, },
computed: { computed: {
currentDropdownText() { showLinksToggle() {
return this.$options.views[this.type].text.primary; return this.currentViewType === LAYER_VIEW;
},
viewTypesList() {
return Object.keys(this.$options.views).map((key) => {
return {
value: key,
text: this.$options.views[key].text.primary,
};
});
},
},
watch: {
/*
How does this reset the loading? As we note in the methods comment below,
the loader is set to on before the update work is undertaken (in the parent).
Once the work is complete, one of these values will change, since that's the
point of the work. When that happens, the related value will update and we are done.
The bonus for this approach is that it works the same whichever "direction"
the work goes in.
*/
showLinks() {
this.isToggleLoading = false;
},
type() {
this.isSwitcherLoading = false;
}, },
}, },
methods: { methods: {
itemClick(type) { /*
this.$emit('updateViewType', type); In both toggle methods, we use setTimeout so that the loading indicator displays,
then the work is done to update the DOM. The process is:
→ user clicks
→ call stack: set loading to true
→ render: the loading icon appears on the screen
→ callback queue: now do the work to calculate the new view / links
(note: this work is done in the parent after the event is emitted)
setTimeout is how we move the work to the callback queue.
We can't use nextTick because that is called before the render loop.
See https://www.hesselinkwebdesign.nl/2019/nexttick-vs-settimeout-in-vue/ for more details.
*/
toggleView(type) {
this.isSwitcherLoading = true;
setTimeout(() => {
this.$emit('updateViewType', type);
});
},
toggleShowLinksActive(val) {
this.isToggleLoading = true;
setTimeout(() => {
this.$emit('updateShowLinksState', val);
});
}, },
}, },
}; };
</script> </script>
<template> <template>
<div class="gl-display-flex gl-align-items-center gl-my-4"> <div class="gl-relative gl-display-flex gl-align-items-center gl-w-max-content gl-my-4">
<span>{{ $options.i18n.labelText }}</span> <gl-loading-icon
<gl-dropdown data-testid="pipeline-view-selector" class="gl-ml-4"> v-if="isSwitcherLoading"
<template #button-content> data-testid="switcher-loading-state"
<gl-sprintf :message="currentDropdownText"> class="gl-absolute gl-w-full gl-bg-white gl-opacity-5 gl-z-index-2"
<template #code="{ content }"> size="lg"
<code> {{ content }} </code> />
</template> <span class="gl-font-weight-bold">{{ $options.i18n.viewLabelText }}</span>
</gl-sprintf> <gl-segmented-control
<gl-icon class="gl-px-2" name="angle-down" :size="16" /> v-model="currentViewType"
</template> :options="viewTypesList"
<gl-dropdown-item :disabled="isSwitcherLoading"
v-for="view in $options.views" data-testid="pipeline-view-selector"
:key="view.type" class="gl-mx-4"
:secondary-text="view.text.secondary" @input="toggleView"
@click="itemClick(view.type)" />
>
<b> <div v-if="showLinksToggle">
<gl-sprintf :message="view.text.primary"> <gl-toggle
<template #code="{ content }"> v-model="showLinksActive"
<code> {{ content }} </code> data-testid="show-links-toggle"
</template> class="gl-mx-4"
</gl-sprintf> :label="$options.i18n.linksLabelText"
</b> :is-loading="isToggleLoading"
</gl-dropdown-item> label-position="left"
</gl-dropdown> @change="toggleShowLinksActive"
/>
</div>
</div> </div>
</template> </template>
...@@ -32,6 +32,10 @@ export default { ...@@ -32,6 +32,10 @@ export default {
type: Array, type: Array,
required: true, required: true,
}, },
showLinks: {
type: Boolean,
required: true,
},
type: { type: {
type: String, type: String,
required: true, required: true,
...@@ -217,6 +221,7 @@ export default { ...@@ -217,6 +221,7 @@ export default {
:config-paths="configPaths" :config-paths="configPaths"
:pipeline="currentPipeline" :pipeline="currentPipeline"
:pipeline-layers="getPipelineLayers(pipeline.id)" :pipeline-layers="getPipelineLayers(pipeline.id)"
:show-links="showLinks"
:is-linked-pipeline="true" :is-linked-pipeline="true"
:view-type="viewType" :view-type="viewType"
/> />
......
<script> <script>
import { GlAlert } from '@gitlab/ui';
import { isEmpty } from 'lodash'; import { isEmpty } from 'lodash';
import { __ } from '~/locale'; import { __ } from '~/locale';
import { import {
...@@ -19,10 +18,8 @@ import LinksInner from './links_inner.vue'; ...@@ -19,10 +18,8 @@ import LinksInner from './links_inner.vue';
export default { export default {
name: 'LinksLayer', name: 'LinksLayer',
components: { components: {
GlAlert,
LinksInner, LinksInner,
}, },
MAX_GROUPS: 200,
props: { props: {
containerMeasurements: { containerMeasurements: {
type: Object, type: Object,
...@@ -37,10 +34,10 @@ export default { ...@@ -37,10 +34,10 @@ export default {
required: false, required: false,
default: () => ({}), default: () => ({}),
}, },
neverShowLinks: { showLinks: {
type: Boolean, type: Boolean,
required: false, required: false,
default: false, default: true,
}, },
}, },
data() { data() {
...@@ -67,29 +64,8 @@ export default { ...@@ -67,29 +64,8 @@ export default {
shouldCollectMetrics() { shouldCollectMetrics() {
return this.metricsConfig.collectMetrics && this.metricsConfig.path; return this.metricsConfig.collectMetrics && this.metricsConfig.path;
}, },
showAlert() {
/*
This is a hard override that allows us to turn off the links without
needing to remove the component entirely for iteration or based on graph type.
*/
if (this.neverShowLinks) {
return false;
}
return !this.containerZero && !this.showLinkedLayers && !this.alertDismissed;
},
showLinkedLayers() { showLinkedLayers() {
/* return this.showLinks && !this.containerZero;
This is a hard override that allows us to turn off the links without
needing to remove the component entirely for iteration or based on graph type.
*/
if (this.neverShowLinks) {
return false;
}
return (
!this.containerZero && (this.showLinksOverride || this.numGroups < this.$options.MAX_GROUPS)
);
}, },
}, },
errorCaptured(err, _vm, info) { errorCaptured(err, _vm, info) {
...@@ -103,7 +79,7 @@ export default { ...@@ -103,7 +79,7 @@ export default {
is closed and functionality is enabled by default. is closed and functionality is enabled by default.
*/ */
if (this.neverShowLinks && !isEmpty(this.pipelineData)) { if (!this.showLinks && !isEmpty(this.pipelineData)) {
window.requestAnimationFrame(() => { window.requestAnimationFrame(() => {
this.prepareLinkData(); this.prepareLinkData();
}); });
...@@ -151,13 +127,6 @@ export default { ...@@ -151,13 +127,6 @@ export default {
reportPerformance(this.metricsConfig.path, data); reportPerformance(this.metricsConfig.path, data);
}); });
}, },
dismissAlert() {
this.alertDismissed = true;
},
overrideShowLinks() {
this.dismissAlert();
this.showLinksOverride = true;
},
prepareLinkData() { prepareLinkData() {
this.beginPerfMeasure(); this.beginPerfMeasure();
let numLinks; let numLinks;
...@@ -185,15 +154,6 @@ export default { ...@@ -185,15 +154,6 @@ export default {
<slot></slot> <slot></slot>
</links-inner> </links-inner>
<div v-else> <div v-else>
<gl-alert
v-if="showAlert"
class="gl-ml-4 gl-mb-4"
:primary-button-text="$options.i18n.showLinksAnyways"
@primaryAction="overrideShowLinks"
@dismiss="dismissAlert"
>
{{ $options.i18n.tooManyJobs }}
</gl-alert>
<div class="gl-display-flex gl-relative"> <div class="gl-display-flex gl-relative">
<slot></slot> <slot></slot>
</div> </div>
......
...@@ -416,9 +416,6 @@ msgstr "" ...@@ -416,9 +416,6 @@ msgstr ""
msgid "%{board_target} not found" msgid "%{board_target} not found"
msgstr "" msgstr ""
msgid "%{codeStart}needs:%{codeEnd} relationships"
msgstr ""
msgid "%{code_open}Masked:%{code_close} Hidden in job logs. Must match masking requirements." msgid "%{code_open}Masked:%{code_close} Hidden in job logs. Must match masking requirements."
msgstr "" msgstr ""
...@@ -15217,6 +15214,9 @@ msgstr "" ...@@ -15217,6 +15214,9 @@ msgstr ""
msgid "Group is required when cluster_type is :group" msgid "Group is required when cluster_type is :group"
msgstr "" msgstr ""
msgid "Group jobs by"
msgstr ""
msgid "Group maintainers can register group runners in the %{link}" msgid "Group maintainers can register group runners in the %{link}"
msgstr "" msgstr ""
...@@ -18199,6 +18199,9 @@ msgstr "" ...@@ -18199,6 +18199,9 @@ msgstr ""
msgid "Job artifacts" msgid "Job artifacts"
msgstr "" msgstr ""
msgid "Job dependencies"
msgstr ""
msgid "Job has been erased" msgid "Job has been erased"
msgstr "" msgstr ""
...@@ -22505,9 +22508,6 @@ msgstr "" ...@@ -22505,9 +22508,6 @@ msgstr ""
msgid "Or you can choose one of the suggested colors below" msgid "Or you can choose one of the suggested colors below"
msgstr "" msgstr ""
msgid "Order jobs by"
msgstr ""
msgid "Orphaned member" msgid "Orphaned member"
msgstr "" msgstr ""
...@@ -29054,6 +29054,9 @@ msgstr "" ...@@ -29054,6 +29054,9 @@ msgstr ""
msgid "Show complete raw log" msgid "Show complete raw log"
msgstr "" msgstr ""
msgid "Show dependencies"
msgstr ""
msgid "Show details" msgid "Show details"
msgstr "" msgstr ""
...@@ -34822,9 +34825,6 @@ msgstr "" ...@@ -34822,9 +34825,6 @@ msgstr ""
msgid "View the documentation" msgid "View the documentation"
msgstr "" msgstr ""
msgid "View the jobs grouped into stages"
msgstr ""
msgid "View the latest successful deployment to this environment" msgid "View the latest successful deployment to this environment"
msgstr "" msgstr ""
...@@ -34837,9 +34837,6 @@ msgstr "" ...@@ -34837,9 +34837,6 @@ msgstr ""
msgid "View users statistics" msgid "View users statistics"
msgstr "" msgstr ""
msgid "View what jobs are needed for a job to run"
msgstr ""
msgid "Viewed" msgid "Viewed"
msgstr "" msgstr ""
......
...@@ -22,6 +22,7 @@ describe('graph component', () => { ...@@ -22,6 +22,7 @@ describe('graph component', () => {
const defaultProps = { const defaultProps = {
pipeline: generateResponse(mockPipelineResponse, 'root/fungi-xoxo'), pipeline: generateResponse(mockPipelineResponse, 'root/fungi-xoxo'),
showLinks: false,
viewType: STAGE_VIEW, viewType: STAGE_VIEW,
configPaths: { configPaths: {
metricsPath: '', metricsPath: '',
......
...@@ -15,6 +15,7 @@ import PipelineGraph from '~/pipelines/components/graph/graph_component.vue'; ...@@ -15,6 +15,7 @@ import PipelineGraph from '~/pipelines/components/graph/graph_component.vue';
import PipelineGraphWrapper from '~/pipelines/components/graph/graph_component_wrapper.vue'; import PipelineGraphWrapper from '~/pipelines/components/graph/graph_component_wrapper.vue';
import GraphViewSelector from '~/pipelines/components/graph/graph_view_selector.vue'; import GraphViewSelector from '~/pipelines/components/graph/graph_view_selector.vue';
import StageColumnComponent from '~/pipelines/components/graph/stage_column_component.vue'; import StageColumnComponent from '~/pipelines/components/graph/stage_column_component.vue';
import LinksLayer from '~/pipelines/components/graph_shared/links_layer.vue';
import * as parsingUtils from '~/pipelines/components/parsing_utils'; import * as parsingUtils from '~/pipelines/components/parsing_utils';
import { mockPipelineResponse } from './mock_data'; import { mockPipelineResponse } from './mock_data';
...@@ -31,7 +32,9 @@ describe('Pipeline graph wrapper', () => { ...@@ -31,7 +32,9 @@ describe('Pipeline graph wrapper', () => {
let wrapper; let wrapper;
const getAlert = () => wrapper.find(GlAlert); const getAlert = () => wrapper.find(GlAlert);
const getDependenciesToggle = () => wrapper.find('[data-testid="show-links-toggle"]');
const getLoadingIcon = () => wrapper.find(GlLoadingIcon); const getLoadingIcon = () => wrapper.find(GlLoadingIcon);
const getLinksLayer = () => wrapper.findComponent(LinksLayer);
const getGraph = () => wrapper.find(PipelineGraph); const getGraph = () => wrapper.find(PipelineGraph);
const getStageColumnTitle = () => wrapper.find('[data-testid="stage-column-title"]'); const getStageColumnTitle = () => wrapper.find('[data-testid="stage-column-title"]');
const getAllStageColumnGroupsInColumn = () => const getAllStageColumnGroupsInColumn = () =>
...@@ -59,6 +62,7 @@ describe('Pipeline graph wrapper', () => { ...@@ -59,6 +62,7 @@ describe('Pipeline graph wrapper', () => {
}; };
const createComponentWithApollo = ({ const createComponentWithApollo = ({
data = {},
getPipelineDetailsHandler = jest.fn().mockResolvedValue(mockPipelineResponse), getPipelineDetailsHandler = jest.fn().mockResolvedValue(mockPipelineResponse),
mountFn = shallowMount, mountFn = shallowMount,
provide = {}, provide = {},
...@@ -66,7 +70,7 @@ describe('Pipeline graph wrapper', () => { ...@@ -66,7 +70,7 @@ describe('Pipeline graph wrapper', () => {
const requestHandlers = [[getPipelineDetails, getPipelineDetailsHandler]]; const requestHandlers = [[getPipelineDetails, getPipelineDetailsHandler]];
const apolloProvider = createMockApollo(requestHandlers); const apolloProvider = createMockApollo(requestHandlers);
createComponent({ apolloProvider, provide, mountFn }); createComponent({ apolloProvider, data, provide, mountFn });
}; };
afterEach(() => { afterEach(() => {
...@@ -74,6 +78,15 @@ describe('Pipeline graph wrapper', () => { ...@@ -74,6 +78,15 @@ describe('Pipeline graph wrapper', () => {
wrapper = null; wrapper = null;
}); });
beforeAll(() => {
jest.useFakeTimers();
});
afterAll(() => {
jest.runOnlyPendingTimers();
jest.useRealTimers();
});
describe('when data is loading', () => { describe('when data is loading', () => {
it('displays the loading icon', () => { it('displays the loading icon', () => {
createComponentWithApollo(); createComponentWithApollo();
...@@ -282,6 +295,36 @@ describe('Pipeline graph wrapper', () => { ...@@ -282,6 +295,36 @@ describe('Pipeline graph wrapper', () => {
}); });
}); });
describe('when pipelineGraphLayersView feature flag is on and layers view is selected', () => {
beforeEach(async () => {
createComponentWithApollo({
provide: {
glFeatures: {
pipelineGraphLayersView: true,
},
},
data: {
currentViewType: LAYER_VIEW,
},
mountFn: mount,
});
jest.runOnlyPendingTimers();
await wrapper.vm.$nextTick();
});
it('sets showLinks to true', async () => {
/* This spec uses .props for performance reasons. */
expect(getLinksLayer().exists()).toBe(true);
expect(getLinksLayer().props('showLinks')).toBe(false);
expect(getViewSelector().props('type')).toBe(LAYER_VIEW);
await getDependenciesToggle().trigger('click');
jest.runOnlyPendingTimers();
await wrapper.vm.$nextTick();
expect(wrapper.findComponent(LinksLayer).props('showLinks')).toBe(true);
});
});
describe('when feature flag is on and local storage is set', () => { describe('when feature flag is on and local storage is set', () => {
beforeEach(async () => { beforeEach(async () => {
localStorage.setItem(VIEW_TYPE_KEY, LAYER_VIEW); localStorage.setItem(VIEW_TYPE_KEY, LAYER_VIEW);
...@@ -299,10 +342,15 @@ describe('Pipeline graph wrapper', () => { ...@@ -299,10 +342,15 @@ describe('Pipeline graph wrapper', () => {
await wrapper.vm.$nextTick(); await wrapper.vm.$nextTick();
}); });
afterEach(() => {
localStorage.clear();
});
it('reads the view type from localStorage when available', () => { it('reads the view type from localStorage when available', () => {
expect(wrapper.find('[data-testid="pipeline-view-selector"] code').text()).toContain( const viewSelectorNeedsSegment = wrapper
'needs:', .findAll('[data-testid="pipeline-view-selector"] > label')
); .at(1);
expect(viewSelectorNeedsSegment.classes()).toContain('active');
}); });
}); });
......
import { GlLoadingIcon, GlSegmentedControl } from '@gitlab/ui';
import { mount, shallowMount } from '@vue/test-utils';
import { LAYER_VIEW, STAGE_VIEW } from '~/pipelines/components/graph/constants';
import GraphViewSelector from '~/pipelines/components/graph/graph_view_selector.vue';
describe('the graph view selector component', () => {
let wrapper;
const findDependenciesToggle = () => wrapper.find('[data-testid="show-links-toggle"]');
const findViewTypeSelector = () => wrapper.findComponent(GlSegmentedControl);
const findStageViewLabel = () => findViewTypeSelector().findAll('label').at(0);
const findLayersViewLabel = () => findViewTypeSelector().findAll('label').at(1);
const findSwitcherLoader = () => wrapper.find('[data-testid="switcher-loading-state"]');
const findToggleLoader = () => findDependenciesToggle().find(GlLoadingIcon);
const defaultProps = {
showLinks: false,
type: STAGE_VIEW,
};
const defaultData = {
showLinksActive: false,
isToggleLoading: false,
isSwitcherLoading: false,
};
const createComponent = ({ data = {}, mountFn = shallowMount, props = {} } = {}) => {
wrapper = mountFn(GraphViewSelector, {
propsData: {
...defaultProps,
...props,
},
data() {
return {
...defaultData,
...data,
};
},
});
};
afterEach(() => {
wrapper.destroy();
});
describe('when showing stage view', () => {
beforeEach(() => {
createComponent({ mountFn: mount });
});
it('shows the Stage view label as active in the selector', () => {
expect(findStageViewLabel().classes()).toContain('active');
});
it('does not show the Job dependencies (links) toggle', () => {
expect(findDependenciesToggle().exists()).toBe(false);
});
});
describe('when showing Job dependencies view', () => {
beforeEach(() => {
createComponent({
mountFn: mount,
props: {
type: LAYER_VIEW,
},
});
});
it('shows the Job dependencies view label as active in the selector', () => {
expect(findLayersViewLabel().classes()).toContain('active');
});
it('shows the Job dependencies (links) toggle', () => {
expect(findDependenciesToggle().exists()).toBe(true);
});
});
describe('events', () => {
beforeEach(() => {
jest.useFakeTimers();
createComponent({
mountFn: mount,
props: {
type: LAYER_VIEW,
},
});
});
it('shows loading state and emits updateViewType when view type toggled', async () => {
expect(wrapper.emitted().updateViewType).toBeUndefined();
expect(findSwitcherLoader().exists()).toBe(false);
await findStageViewLabel().trigger('click');
/*
Loading happens before the event is emitted or timers are run.
Then we run the timer because the event is emitted in setInterval
which is what gives the loader a chace to show up.
*/
expect(findSwitcherLoader().exists()).toBe(true);
jest.runOnlyPendingTimers();
expect(wrapper.emitted().updateViewType).toHaveLength(1);
expect(wrapper.emitted().updateViewType).toEqual([[STAGE_VIEW]]);
});
it('shows loading state and emits updateShowLinks when show links toggle is clicked', async () => {
expect(wrapper.emitted().updateShowLinksState).toBeUndefined();
expect(findToggleLoader().exists()).toBe(false);
await findDependenciesToggle().trigger('click');
/*
Loading happens before the event is emitted or timers are run.
Then we run the timer because the event is emitted in setInterval
which is what gives the loader a chace to show up.
*/
expect(findToggleLoader().exists()).toBe(true);
jest.runOnlyPendingTimers();
expect(wrapper.emitted().updateShowLinksState).toHaveLength(1);
expect(wrapper.emitted().updateShowLinksState).toEqual([[true]]);
});
});
});
...@@ -26,6 +26,7 @@ describe('Linked Pipelines Column', () => { ...@@ -26,6 +26,7 @@ describe('Linked Pipelines Column', () => {
const defaultProps = { const defaultProps = {
columnTitle: 'Downstream', columnTitle: 'Downstream',
linkedPipelines: processedPipeline.downstream, linkedPipelines: processedPipeline.downstream,
showLinks: false,
type: DOWNSTREAM, type: DOWNSTREAM,
viewType: STAGE_VIEW, viewType: STAGE_VIEW,
configPaths: { configPaths: {
......
import { GlAlert } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils';
import { fireEvent, within } from '@testing-library/dom';
import { mount, shallowMount } from '@vue/test-utils';
import LinksInner from '~/pipelines/components/graph_shared/links_inner.vue'; import LinksInner from '~/pipelines/components/graph_shared/links_inner.vue';
import LinksLayer from '~/pipelines/components/graph_shared/links_layer.vue'; import LinksLayer from '~/pipelines/components/graph_shared/links_layer.vue';
import { generateResponse, mockPipelineResponse } from '../graph/mock_data'; import { generateResponse, mockPipelineResponse } from '../graph/mock_data';
...@@ -8,25 +6,18 @@ import { generateResponse, mockPipelineResponse } from '../graph/mock_data'; ...@@ -8,25 +6,18 @@ import { generateResponse, mockPipelineResponse } from '../graph/mock_data';
describe('links layer component', () => { describe('links layer component', () => {
let wrapper; let wrapper;
const withinComponent = () => within(wrapper.element);
const findAlert = () => wrapper.find(GlAlert);
const findShowAnyways = () =>
withinComponent().getByText(wrapper.vm.$options.i18n.showLinksAnyways);
const findLinksInner = () => wrapper.find(LinksInner); const findLinksInner = () => wrapper.find(LinksInner);
const pipeline = generateResponse(mockPipelineResponse, 'root/fungi-xoxo'); const pipeline = generateResponse(mockPipelineResponse, 'root/fungi-xoxo');
const containerId = `pipeline-links-container-${pipeline.id}`; const containerId = `pipeline-links-container-${pipeline.id}`;
const slotContent = "<div>Ceci n'est pas un graphique</div>"; const slotContent = "<div>Ceci n'est pas un graphique</div>";
const tooManyStages = Array(101)
.fill(0)
.flatMap(() => pipeline.stages);
const defaultProps = { const defaultProps = {
containerId, containerId,
containerMeasurements: { width: 400, height: 400 }, containerMeasurements: { width: 400, height: 400 },
pipelineId: pipeline.id, pipelineId: pipeline.id,
pipelineData: pipeline.stages, pipelineData: pipeline.stages,
showLinks: false,
}; };
const createComponent = ({ mountFn = shallowMount, props = {} } = {}) => { const createComponent = ({ mountFn = shallowMount, props = {} } = {}) => {
...@@ -49,7 +40,7 @@ describe('links layer component', () => { ...@@ -49,7 +40,7 @@ describe('links layer component', () => {
wrapper = null; wrapper = null;
}); });
describe('with data under max stages', () => { describe('with show links off', () => {
beforeEach(() => { beforeEach(() => {
createComponent(); createComponent();
}); });
...@@ -58,63 +49,40 @@ describe('links layer component', () => { ...@@ -58,63 +49,40 @@ describe('links layer component', () => {
expect(wrapper.html()).toContain(slotContent); expect(wrapper.html()).toContain(slotContent);
}); });
it('renders the inner links component', () => { it('does not render inner links component', () => {
expect(findLinksInner().exists()).toBe(true); expect(findLinksInner().exists()).toBe(false);
}); });
}); });
describe('with more than the max number of stages', () => { describe('with show links on', () => {
describe('rendering', () => { beforeEach(() => {
beforeEach(() => { createComponent({
createComponent({ props: { pipelineData: tooManyStages } }); props: {
}); showLinks: true,
},
it('renders the default slot', () => {
expect(wrapper.html()).toContain(slotContent);
});
it('renders the alert component', () => {
expect(findAlert().exists()).toBe(true);
});
it('does not render the inner links component', () => {
expect(findLinksInner().exists()).toBe(false);
}); });
}); });
describe('with width or height measurement at 0', () => { it('renders the default slot', () => {
beforeEach(() => { expect(wrapper.html()).toContain(slotContent);
createComponent({ props: { containerMeasurements: { width: 0, height: 100 } } }); });
});
it('renders the default slot', () => {
expect(wrapper.html()).toContain(slotContent);
});
it('does not render the alert component', () => {
expect(findAlert().exists()).toBe(false);
});
it('does not render the inner links component', () => { it('renders the inner links component', () => {
expect(findLinksInner().exists()).toBe(false); expect(findLinksInner().exists()).toBe(true);
});
}); });
});
describe('interactions', () => { describe('with width or height measurement at 0', () => {
beforeEach(() => { beforeEach(() => {
createComponent({ mountFn: mount, props: { pipelineData: tooManyStages } }); createComponent({ props: { containerMeasurements: { width: 0, height: 100 } } });
}); });
it('renders the disable button', () => { it('renders the default slot', () => {
expect(findShowAnyways()).not.toBe(null); expect(wrapper.html()).toContain(slotContent);
}); });
it('shows links when override is clicked', async () => { it('does not render the inner links component', () => {
expect(findLinksInner().exists()).toBe(false); expect(findLinksInner().exists()).toBe(false);
fireEvent(findShowAnyways(), new MouseEvent('click', { bubbles: true }));
await wrapper.vm.$nextTick();
expect(findLinksInner().exists()).toBe(true);
});
}); });
}); });
}); });
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