Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'spec/frontend/pipelines/graph')
-rw-r--r--spec/frontend/pipelines/graph/graph_component_legacy_spec.js306
-rw-r--r--spec/frontend/pipelines/graph/graph_component_spec.js312
-rw-r--r--spec/frontend/pipelines/graph/graph_component_wrapper_spec.js124
-rw-r--r--spec/frontend/pipelines/graph/linked_pipeline_spec.js22
-rw-r--r--spec/frontend/pipelines/graph/linked_pipelines_column_legacy_spec.js40
-rw-r--r--spec/frontend/pipelines/graph/linked_pipelines_column_spec.js120
-rw-r--r--spec/frontend/pipelines/graph/mock_data.js896
-rw-r--r--spec/frontend/pipelines/graph/mock_data_legacy.js261
-rw-r--r--spec/frontend/pipelines/graph/stage_column_component_legacy_spec.js135
-rw-r--r--spec/frontend/pipelines/graph/stage_column_component_spec.js164
10 files changed, 1776 insertions, 604 deletions
diff --git a/spec/frontend/pipelines/graph/graph_component_legacy_spec.js b/spec/frontend/pipelines/graph/graph_component_legacy_spec.js
new file mode 100644
index 00000000000..3b1909b6564
--- /dev/null
+++ b/spec/frontend/pipelines/graph/graph_component_legacy_spec.js
@@ -0,0 +1,306 @@
+import Vue from 'vue';
+import { mount } from '@vue/test-utils';
+import { GlLoadingIcon } from '@gitlab/ui';
+import { setHTMLFixture } from 'helpers/fixtures';
+import PipelineStore from '~/pipelines/stores/pipeline_store';
+import GraphComponentLegacy from '~/pipelines/components/graph/graph_component_legacy.vue';
+import StageColumnComponentLegacy from '~/pipelines/components/graph/stage_column_component_legacy.vue';
+import LinkedPipelinesColumnLegacy from '~/pipelines/components/graph/linked_pipelines_column_legacy.vue';
+import graphJSON from './mock_data_legacy';
+import linkedPipelineJSON from './linked_pipelines_mock_data';
+import PipelinesMediator from '~/pipelines/pipeline_details_mediator';
+
+describe('graph component', () => {
+ let store;
+ let mediator;
+ let wrapper;
+
+ 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);
+
+ beforeEach(() => {
+ mediator = new PipelinesMediator({ endpoint: '' });
+ store = new PipelineStore();
+ store.storePipeline(linkedPipelineJSON);
+
+ setHTMLFixture('<div class="layout-page"></div>');
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe('while is loading', () => {
+ it('should render a loading icon', () => {
+ wrapper = mount(GraphComponentLegacy, {
+ propsData: {
+ isLoading: true,
+ pipeline: {},
+ mediator,
+ },
+ });
+
+ expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
+ });
+ });
+
+ describe('with data', () => {
+ beforeEach(() => {
+ wrapper = mount(GraphComponentLegacy, {
+ propsData: {
+ isLoading: false,
+ pipeline: graphJSON,
+ mediator,
+ },
+ });
+ });
+
+ it('renders the graph', () => {
+ expect(wrapper.find('.js-pipeline-graph').exists()).toBe(true);
+ expect(wrapper.find('.loading-icon').exists()).toBe(false);
+ expect(wrapper.find('.stage-column-list').exists()).toBe(true);
+ });
+
+ it('renders columns in the graph', () => {
+ expect(findStageColumns()).toHaveLength(graphJSON.details.stages.length);
+ });
+ });
+
+ describe('when linked pipelines are present', () => {
+ beforeEach(() => {
+ wrapper = mount(GraphComponentLegacy, {
+ propsData: {
+ isLoading: false,
+ pipeline: store.state.pipeline,
+ mediator,
+ },
+ });
+ });
+
+ describe('rendered output', () => {
+ it('should include the pipelines graph', () => {
+ expect(wrapper.find('.js-pipeline-graph').exists()).toBe(true);
+ });
+
+ it('should not include the loading icon', () => {
+ expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
+ });
+
+ it('should include the stage column', () => {
+ expect(findStageColumnAt(0).exists()).toBe(true);
+ });
+
+ it('stage column should have no-margin, gl-mr-26, has-only-one-job classes if there is only one job', () => {
+ expect(findStageColumnAt(0).classes()).toEqual(
+ expect.arrayContaining(['no-margin', 'gl-mr-26', 'has-only-one-job']),
+ );
+ });
+
+ it('should include the left-margin class on the second child', () => {
+ expect(findStageColumnAt(1).classes('left-margin')).toBe(true);
+ });
+
+ it('should include the left-connector class in the build of the second child', () => {
+ expect(
+ findStageColumnAt(1)
+ .find('.build:nth-child(1)')
+ .classes('left-connector'),
+ ).toBe(true);
+ });
+
+ it('should include the js-has-linked-pipelines flag', () => {
+ expect(wrapper.find('.js-has-linked-pipelines').exists()).toBe(true);
+ });
+ });
+
+ describe('computeds and methods', () => {
+ describe('capitalizeStageName', () => {
+ it('it capitalizes the stage name', () => {
+ expect(
+ wrapper
+ .findAll('.stage-column .stage-name')
+ .at(1)
+ .text(),
+ ).toBe('Prebuild');
+ });
+ });
+
+ describe('stageConnectorClass', () => {
+ it('it returns left-margin when there is a triggerer', () => {
+ expect(findStageColumnAt(1).classes('left-margin')).toBe(true);
+ });
+ });
+ });
+
+ describe('linked pipelines components', () => {
+ beforeEach(() => {
+ wrapper = mount(GraphComponentLegacy, {
+ propsData: {
+ isLoading: false,
+ pipeline: store.state.pipeline,
+ mediator,
+ },
+ });
+ });
+
+ it('should render an upstream pipelines column at first position', () => {
+ expect(wrapper.find(LinkedPipelinesColumnLegacy).exists()).toBe(true);
+ expect(wrapper.find('.stage-column .stage-name').text()).toBe('Upstream');
+ });
+
+ it('should render a downstream pipelines column at last position', () => {
+ const stageColumnNames = wrapper.findAll('.stage-column .stage-name');
+
+ expect(wrapper.find(LinkedPipelinesColumnLegacy).exists()).toBe(true);
+ expect(stageColumnNames.at(stageColumnNames.length - 1).text()).toBe('Downstream');
+ });
+
+ describe('triggered by', () => {
+ describe('on click', () => {
+ it('should emit `onClickUpstreamPipeline` when triggered by linked pipeline is clicked', () => {
+ const btnWrapper = findExpandPipelineBtn();
+
+ btnWrapper.trigger('click');
+
+ btnWrapper.vm.$nextTick(() => {
+ expect(wrapper.emitted().onClickUpstreamPipeline).toEqual([
+ store.state.pipeline.triggered_by,
+ ]);
+ });
+ });
+ });
+
+ describe('with expanded pipeline', () => {
+ it('should render expanded pipeline', done => {
+ // expand the pipeline
+ store.state.pipeline.triggered_by[0].isExpanded = true;
+
+ wrapper = mount(GraphComponentLegacy, {
+ propsData: {
+ isLoading: false,
+ pipeline: store.state.pipeline,
+ mediator,
+ },
+ });
+
+ Vue.nextTick()
+ .then(() => {
+ expect(wrapper.find('.js-upstream-pipeline-12').exists()).toBe(true);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+ });
+
+ describe('triggered', () => {
+ describe('on click', () => {
+ it('should emit `onClickTriggered`', () => {
+ // We have to mock this method since we do both style change and
+ // emit and event, not mocking returns an error.
+ wrapper.setMethods({
+ handleClickedDownstream: jest.fn(() =>
+ wrapper.vm.$emit('onClickTriggered', ...store.state.pipeline.triggered),
+ ),
+ });
+
+ const btnWrappers = findAllExpandPipelineBtns();
+ const downstreamBtnWrapper = btnWrappers.at(btnWrappers.length - 1);
+
+ downstreamBtnWrapper.trigger('click');
+
+ downstreamBtnWrapper.vm.$nextTick(() => {
+ expect(wrapper.emitted().onClickTriggered).toEqual([store.state.pipeline.triggered]);
+ });
+ });
+ });
+
+ describe('with expanded pipeline', () => {
+ it('should render expanded pipeline', done => {
+ // expand the pipeline
+ store.state.pipeline.triggered[0].isExpanded = true;
+
+ wrapper = mount(GraphComponentLegacy, {
+ propsData: {
+ isLoading: false,
+ pipeline: store.state.pipeline,
+ mediator,
+ },
+ });
+
+ Vue.nextTick()
+ .then(() => {
+ expect(wrapper.find('.js-downstream-pipeline-34993051')).not.toBeNull();
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+
+ describe('when column requests a refresh', () => {
+ beforeEach(() => {
+ findStageColumnAt(0).vm.$emit('refreshPipelineGraph');
+ });
+
+ it('refreshPipelineGraph is emitted', () => {
+ expect(wrapper.emitted().refreshPipelineGraph).toHaveLength(1);
+ });
+ });
+ });
+ });
+ });
+
+ describe('when linked pipelines are not present', () => {
+ beforeEach(() => {
+ const pipeline = Object.assign(linkedPipelineJSON, { triggered: null, triggered_by: null });
+ wrapper = mount(GraphComponentLegacy, {
+ propsData: {
+ isLoading: false,
+ pipeline,
+ mediator,
+ },
+ });
+ });
+
+ describe('rendered output', () => {
+ it('should include the first column with a no margin', () => {
+ const firstColumn = wrapper.find('.stage-column');
+
+ expect(firstColumn.classes('no-margin')).toBe(true);
+ });
+
+ it('should not render a linked pipelines column', () => {
+ expect(wrapper.find('.linked-pipelines-column').exists()).toBe(false);
+ });
+ });
+
+ describe('stageConnectorClass', () => {
+ it('it returns no-margin when no triggerer and there is one job', () => {
+ expect(findStageColumnAt(0).classes('no-margin')).toBe(true);
+ });
+
+ it('it returns left-margin when no triggerer and not the first stage', () => {
+ expect(findStageColumnAt(1).classes('left-margin')).toBe(true);
+ });
+ });
+ });
+
+ describe('capitalizeStageName', () => {
+ it('capitalizes and escapes stage name', () => {
+ wrapper = mount(GraphComponentLegacy, {
+ propsData: {
+ isLoading: false,
+ pipeline: graphJSON,
+ mediator,
+ },
+ });
+
+ expect(findStageColumnAt(1).props('title')).toEqual(
+ 'Deploy &lt;img src=x onerror=alert(document.domain)&gt;',
+ );
+ });
+ });
+});
diff --git a/spec/frontend/pipelines/graph/graph_component_spec.js b/spec/frontend/pipelines/graph/graph_component_spec.js
index 5a17be1af23..7572dd83798 100644
--- a/spec/frontend/pipelines/graph/graph_component_spec.js
+++ b/spec/frontend/pipelines/graph/graph_component_spec.js
@@ -1,305 +1,83 @@
-import Vue from 'vue';
-import { mount } from '@vue/test-utils';
-import { setHTMLFixture } from 'helpers/fixtures';
-import PipelineStore from '~/pipelines/stores/pipeline_store';
-import graphComponent from '~/pipelines/components/graph/graph_component.vue';
+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 graphJSON from './mock_data';
-import linkedPipelineJSON from './linked_pipelines_mock_data';
-import PipelinesMediator from '~/pipelines/pipeline_details_mediator';
+import LinkedPipelinesColumn from '~/pipelines/components/graph/linked_pipelines_column.vue';
+import { GRAPHQL } from '~/pipelines/components/graph/constants';
+import {
+ generateResponse,
+ mockPipelineResponse,
+ pipelineWithUpstreamDownstream,
+} from './mock_data';
describe('graph component', () => {
- let store;
- let mediator;
let wrapper;
- const findExpandPipelineBtn = () => wrapper.find('[data-testid="expandPipelineButton"]');
- const findAllExpandPipelineBtns = () => wrapper.findAll('[data-testid="expandPipelineButton"]');
+ const findLinkedColumns = () => wrapper.findAll(LinkedPipelinesColumn);
const findStageColumns = () => wrapper.findAll(StageColumnComponent);
- const findStageColumnAt = i => findStageColumns().at(i);
- beforeEach(() => {
- mediator = new PipelinesMediator({ endpoint: '' });
- store = new PipelineStore();
- store.storePipeline(linkedPipelineJSON);
-
- setHTMLFixture('<div class="layout-page"></div>');
- });
+ const defaultProps = {
+ pipeline: generateResponse(mockPipelineResponse, 'root/fungi-xoxo'),
+ };
+
+ const createComponent = ({ mountFn = shallowMount, props = {} } = {}) => {
+ wrapper = mountFn(PipelineGraph, {
+ propsData: {
+ ...defaultProps,
+ ...props,
+ },
+ provide: {
+ dataMethod: GRAPHQL,
+ },
+ });
+ };
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
- describe('while is loading', () => {
- it('should render a loading icon', () => {
- wrapper = mount(graphComponent, {
- propsData: {
- isLoading: true,
- pipeline: {},
- mediator,
- },
- });
-
- expect(wrapper.find('.gl-spinner').exists()).toBe(true);
- });
- });
-
describe('with data', () => {
beforeEach(() => {
- wrapper = mount(graphComponent, {
- propsData: {
- isLoading: false,
- pipeline: graphJSON,
- mediator,
- },
- });
+ createComponent({ mountFn: mount });
});
- it('renders the graph', () => {
- expect(wrapper.find('.js-pipeline-graph').exists()).toBe(true);
- expect(wrapper.find('.loading-icon').exists()).toBe(false);
- expect(wrapper.find('.stage-column-list').exists()).toBe(true);
- });
-
- it('renders columns in the graph', () => {
- expect(findStageColumns()).toHaveLength(graphJSON.details.stages.length);
- });
- });
-
- describe('when linked pipelines are present', () => {
- beforeEach(() => {
- wrapper = mount(graphComponent, {
- propsData: {
- isLoading: false,
- pipeline: store.state.pipeline,
- mediator,
- },
- });
- });
-
- describe('rendered output', () => {
- it('should include the pipelines graph', () => {
- expect(wrapper.find('.js-pipeline-graph').exists()).toBe(true);
- });
-
- it('should not include the loading icon', () => {
- expect(wrapper.find('.fa-spinner').exists()).toBe(false);
- });
-
- it('should include the stage column', () => {
- expect(findStageColumnAt(0).exists()).toBe(true);
- });
-
- it('stage column should have no-margin, gl-mr-26, has-only-one-job classes if there is only one job', () => {
- expect(findStageColumnAt(0).classes()).toEqual(
- expect.arrayContaining(['no-margin', 'gl-mr-26', 'has-only-one-job']),
- );
- });
-
- it('should include the left-margin class on the second child', () => {
- expect(findStageColumnAt(1).classes('left-margin')).toBe(true);
- });
-
- it('should include the left-connector class in the build of the second child', () => {
- expect(
- findStageColumnAt(1)
- .find('.build:nth-child(1)')
- .classes('left-connector'),
- ).toBe(true);
- });
-
- it('should include the js-has-linked-pipelines flag', () => {
- expect(wrapper.find('.js-has-linked-pipelines').exists()).toBe(true);
- });
- });
-
- describe('computeds and methods', () => {
- describe('capitalizeStageName', () => {
- it('it capitalizes the stage name', () => {
- expect(
- wrapper
- .findAll('.stage-column .stage-name')
- .at(1)
- .text(),
- ).toBe('Prebuild');
- });
- });
-
- describe('stageConnectorClass', () => {
- it('it returns left-margin when there is a triggerer', () => {
- expect(findStageColumnAt(1).classes('left-margin')).toBe(true);
- });
- });
+ it('renders the main columns in the graph', () => {
+ expect(findStageColumns()).toHaveLength(defaultProps.pipeline.stages.length);
});
- describe('linked pipelines components', () => {
+ describe('when column requests a refresh', () => {
beforeEach(() => {
- wrapper = mount(graphComponent, {
- propsData: {
- isLoading: false,
- pipeline: store.state.pipeline,
- mediator,
- },
- });
+ findStageColumns()
+ .at(0)
+ .vm.$emit('refreshPipelineGraph');
});
- it('should render an upstream pipelines column at first position', () => {
- expect(wrapper.find(linkedPipelinesColumn).exists()).toBe(true);
- expect(wrapper.find('.stage-column .stage-name').text()).toBe('Upstream');
- });
-
- it('should render a downstream pipelines column at last position', () => {
- const stageColumnNames = wrapper.findAll('.stage-column .stage-name');
-
- expect(wrapper.find(linkedPipelinesColumn).exists()).toBe(true);
- expect(stageColumnNames.at(stageColumnNames.length - 1).text()).toBe('Downstream');
- });
-
- describe('triggered by', () => {
- describe('on click', () => {
- it('should emit `onClickUpstreamPipeline` when triggered by linked pipeline is clicked', () => {
- const btnWrapper = findExpandPipelineBtn();
-
- btnWrapper.trigger('click');
-
- btnWrapper.vm.$nextTick(() => {
- expect(wrapper.emitted().onClickUpstreamPipeline).toEqual([
- store.state.pipeline.triggered_by,
- ]);
- });
- });
- });
-
- describe('with expanded pipeline', () => {
- it('should render expanded pipeline', done => {
- // expand the pipeline
- store.state.pipeline.triggered_by[0].isExpanded = true;
-
- wrapper = mount(graphComponent, {
- propsData: {
- isLoading: false,
- pipeline: store.state.pipeline,
- mediator,
- },
- });
-
- Vue.nextTick()
- .then(() => {
- expect(wrapper.find('.js-upstream-pipeline-12').exists()).toBe(true);
- })
- .then(done)
- .catch(done.fail);
- });
- });
- });
-
- describe('triggered', () => {
- describe('on click', () => {
- it('should emit `onClickTriggered`', () => {
- // We have to mock this method since we do both style change and
- // emit and event, not mocking returns an error.
- wrapper.setMethods({
- handleClickedDownstream: jest.fn(() =>
- wrapper.vm.$emit('onClickTriggered', ...store.state.pipeline.triggered),
- ),
- });
-
- const btnWrappers = findAllExpandPipelineBtns();
- const downstreamBtnWrapper = btnWrappers.at(btnWrappers.length - 1);
-
- downstreamBtnWrapper.trigger('click');
-
- downstreamBtnWrapper.vm.$nextTick(() => {
- expect(wrapper.emitted().onClickTriggered).toEqual([store.state.pipeline.triggered]);
- });
- });
- });
-
- describe('with expanded pipeline', () => {
- it('should render expanded pipeline', done => {
- // expand the pipeline
- store.state.pipeline.triggered[0].isExpanded = true;
-
- wrapper = mount(graphComponent, {
- propsData: {
- isLoading: false,
- pipeline: store.state.pipeline,
- mediator,
- },
- });
-
- Vue.nextTick()
- .then(() => {
- expect(wrapper.find('.js-downstream-pipeline-34993051')).not.toBeNull();
- })
- .then(done)
- .catch(done.fail);
- });
- });
-
- describe('when column requests a refresh', () => {
- beforeEach(() => {
- findStageColumnAt(0).vm.$emit('refreshPipelineGraph');
- });
-
- it('refreshPipelineGraph is emitted', () => {
- expect(wrapper.emitted().refreshPipelineGraph).toHaveLength(1);
- });
- });
+ it('refreshPipelineGraph is emitted', () => {
+ expect(wrapper.emitted().refreshPipelineGraph).toHaveLength(1);
});
});
});
describe('when linked pipelines are not present', () => {
beforeEach(() => {
- const pipeline = Object.assign(linkedPipelineJSON, { triggered: null, triggered_by: null });
- wrapper = mount(graphComponent, {
- propsData: {
- isLoading: false,
- pipeline,
- mediator,
- },
- });
+ createComponent({ mountFn: mount });
});
- describe('rendered output', () => {
- it('should include the first column with a no margin', () => {
- const firstColumn = wrapper.find('.stage-column');
-
- expect(firstColumn.classes('no-margin')).toBe(true);
- });
-
- it('should not render a linked pipelines column', () => {
- expect(wrapper.find('.linked-pipelines-column').exists()).toBe(false);
- });
- });
-
- describe('stageConnectorClass', () => {
- it('it returns no-margin when no triggerer and there is one job', () => {
- expect(findStageColumnAt(0).classes('no-margin')).toBe(true);
- });
-
- it('it returns left-margin when no triggerer and not the first stage', () => {
- expect(findStageColumnAt(1).classes('left-margin')).toBe(true);
- });
+ it('should not render a linked pipelines column', () => {
+ expect(findLinkedColumns()).toHaveLength(0);
});
});
- describe('capitalizeStageName', () => {
- it('capitalizes and escapes stage name', () => {
- wrapper = mount(graphComponent, {
- propsData: {
- isLoading: false,
- pipeline: graphJSON,
- mediator,
- },
+ describe('when linked pipelines are present', () => {
+ beforeEach(() => {
+ createComponent({
+ mountFn: mount,
+ props: { pipeline: pipelineWithUpstreamDownstream(mockPipelineResponse) },
});
+ });
- expect(findStageColumnAt(1).props('title')).toEqual(
- 'Deploy &lt;img src=x onerror=alert(document.domain)&gt;',
- );
+ it('should render linked pipelines columns', () => {
+ expect(findLinkedColumns()).toHaveLength(2);
});
});
});
diff --git a/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js b/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js
new file mode 100644
index 00000000000..875aaa48037
--- /dev/null
+++ b/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js
@@ -0,0 +1,124 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import { shallowMount } from '@vue/test-utils';
+import { GlAlert, GlLoadingIcon } from '@gitlab/ui';
+import createMockApollo from 'jest/helpers/mock_apollo_helper';
+import PipelineGraphWrapper from '~/pipelines/components/graph/graph_component_wrapper.vue';
+import PipelineGraph from '~/pipelines/components/graph/graph_component.vue';
+import getPipelineDetails from '~/pipelines/graphql/queries/get_pipeline_details.query.graphql';
+import { mockPipelineResponse } from './mock_data';
+
+const defaultProvide = {
+ pipelineProjectPath: 'frog/amphibirama',
+ pipelineIid: '22',
+};
+
+describe('Pipeline graph wrapper', () => {
+ Vue.use(VueApollo);
+
+ let wrapper;
+ const getAlert = () => wrapper.find(GlAlert);
+ const getLoadingIcon = () => wrapper.find(GlLoadingIcon);
+ const getGraph = () => wrapper.find(PipelineGraph);
+
+ const createComponent = ({
+ apolloProvider,
+ data = {},
+ provide = defaultProvide,
+ mountFn = shallowMount,
+ } = {}) => {
+ wrapper = mountFn(PipelineGraphWrapper, {
+ provide,
+ apolloProvider,
+ data() {
+ return {
+ ...data,
+ };
+ },
+ });
+ };
+
+ const createComponentWithApollo = (
+ getPipelineDetailsHandler = jest.fn().mockResolvedValue(mockPipelineResponse),
+ ) => {
+ const requestHandlers = [[getPipelineDetails, getPipelineDetailsHandler]];
+
+ const apolloProvider = createMockApollo(requestHandlers);
+ createComponent({ apolloProvider });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe('when data is loading', () => {
+ it('displays the loading icon', () => {
+ createComponentWithApollo();
+ expect(getLoadingIcon().exists()).toBe(true);
+ });
+
+ it('does not display the alert', () => {
+ createComponentWithApollo();
+ expect(getAlert().exists()).toBe(false);
+ });
+
+ it('does not display the graph', () => {
+ createComponentWithApollo();
+ expect(getGraph().exists()).toBe(false);
+ });
+ });
+
+ describe('when data has loaded', () => {
+ beforeEach(async () => {
+ createComponentWithApollo();
+ jest.runOnlyPendingTimers();
+ await wrapper.vm.$nextTick();
+ });
+
+ it('does not display the loading icon', () => {
+ expect(getLoadingIcon().exists()).toBe(false);
+ });
+
+ it('does not display the alert', () => {
+ expect(getAlert().exists()).toBe(false);
+ });
+
+ it('displays the graph', () => {
+ expect(getGraph().exists()).toBe(true);
+ });
+ });
+
+ describe('when there is an error', () => {
+ beforeEach(async () => {
+ createComponentWithApollo(jest.fn().mockRejectedValue(new Error('GraphQL error')));
+ jest.runOnlyPendingTimers();
+ await wrapper.vm.$nextTick();
+ });
+
+ it('does not display the loading icon', () => {
+ expect(getLoadingIcon().exists()).toBe(false);
+ });
+
+ it('displays the alert', () => {
+ expect(getAlert().exists()).toBe(true);
+ });
+
+ it('does not display the graph', () => {
+ expect(getGraph().exists()).toBe(false);
+ });
+ });
+
+ describe('when refresh action is emitted', () => {
+ beforeEach(async () => {
+ createComponentWithApollo();
+ jest.spyOn(wrapper.vm.$apollo.queries.pipeline, 'refetch');
+ await wrapper.vm.$nextTick();
+ getGraph().vm.$emit('refreshPipelineGraph');
+ });
+
+ it('calls refetch', () => {
+ expect(wrapper.vm.$apollo.queries.pipeline.refetch).toHaveBeenCalled();
+ });
+ });
+});
diff --git a/spec/frontend/pipelines/graph/linked_pipeline_spec.js b/spec/frontend/pipelines/graph/linked_pipeline_spec.js
index 67986ca7739..fb005d628a9 100644
--- a/spec/frontend/pipelines/graph/linked_pipeline_spec.js
+++ b/spec/frontend/pipelines/graph/linked_pipeline_spec.js
@@ -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', () => {
diff --git a/spec/frontend/pipelines/graph/linked_pipelines_column_legacy_spec.js b/spec/frontend/pipelines/graph/linked_pipelines_column_legacy_spec.js
new file mode 100644
index 00000000000..b6c700c65d2
--- /dev/null
+++ b/spec/frontend/pipelines/graph/linked_pipelines_column_legacy_spec.js
@@ -0,0 +1,40 @@
+import { shallowMount } from '@vue/test-utils';
+import LinkedPipelinesColumnLegacy from '~/pipelines/components/graph/linked_pipelines_column_legacy.vue';
+import LinkedPipeline from '~/pipelines/components/graph/linked_pipeline.vue';
+import { UPSTREAM } from '~/pipelines/components/graph/constants';
+import mockData from './linked_pipelines_mock_data';
+
+describe('Linked Pipelines Column', () => {
+ const propsData = {
+ columnTitle: 'Upstream',
+ linkedPipelines: mockData.triggered,
+ graphPosition: 'right',
+ projectId: 19,
+ type: UPSTREAM,
+ };
+ let wrapper;
+
+ beforeEach(() => {
+ wrapper = shallowMount(LinkedPipelinesColumnLegacy, { propsData });
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders the pipeline orientation', () => {
+ const titleElement = wrapper.find('.linked-pipelines-column-title');
+
+ expect(titleElement.text()).toBe(propsData.columnTitle);
+ });
+
+ it('renders the correct number of linked pipelines', () => {
+ const linkedPipelineElements = wrapper.findAll(LinkedPipeline);
+
+ expect(linkedPipelineElements.length).toBe(propsData.linkedPipelines.length);
+ });
+
+ it('renders cross project triangle when column is upstream', () => {
+ expect(wrapper.find('.cross-project-triangle').exists()).toBe(true);
+ });
+});
diff --git a/spec/frontend/pipelines/graph/linked_pipelines_column_spec.js b/spec/frontend/pipelines/graph/linked_pipelines_column_spec.js
index e6ae3154d1d..37eb5f900dd 100644
--- a/spec/frontend/pipelines/graph/linked_pipelines_column_spec.js
+++ b/spec/frontend/pipelines/graph/linked_pipelines_column_spec.js
@@ -1,40 +1,120 @@
-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);
+ });
+ });
});
});
diff --git a/spec/frontend/pipelines/graph/mock_data.js b/spec/frontend/pipelines/graph/mock_data.js
index a4a5d78f906..d53a11eea0e 100644
--- a/spec/frontend/pipelines/graph/mock_data.js
+++ b/spec/frontend/pipelines/graph/mock_data.js
@@ -1,261 +1,665 @@
-export default {
- id: 123,
- user: {
- name: 'Root',
- username: 'root',
- id: 1,
- state: 'active',
- avatar_url: null,
- web_url: 'http://localhost:3000/root',
- },
- active: false,
- coverage: null,
- path: '/root/ci-mock/pipelines/123',
- details: {
- status: {
- icon: 'status_success',
- text: 'passed',
- label: 'passed',
- group: 'success',
- has_details: true,
- details_path: '/root/ci-mock/pipelines/123',
- favicon:
- '/assets/ci_favicons/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.png',
- },
- duration: 9,
- finished_at: '2017-04-19T14:30:27.542Z',
- stages: [
- {
- name: 'test',
- title: 'test: passed',
- groups: [
- {
- name: 'test',
- size: 1,
- status: {
- icon: 'status_success',
- text: 'passed',
- label: 'passed',
- group: 'success',
- has_details: true,
- details_path: '/root/ci-mock/builds/4153',
- favicon:
- '/assets/ci_favicons/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.png',
- action: {
- icon: 'retry',
- title: 'Retry',
- path: '/root/ci-mock/builds/4153/retry',
- method: 'post',
+import { unwrapPipelineData } from '~/pipelines/components/graph/utils';
+
+export const mockPipelineResponse = {
+ data: {
+ project: {
+ __typename: 'Project',
+ pipeline: {
+ __typename: 'Pipeline',
+ id: 163,
+ iid: '22',
+ downstream: null,
+ upstream: null,
+ stages: {
+ __typename: 'CiStageConnection',
+ nodes: [
+ {
+ __typename: 'CiStage',
+ name: 'build',
+ status: {
+ __typename: 'DetailedStatus',
+ action: null,
},
- },
- jobs: [
- {
- id: 4153,
- name: 'test',
- build_path: '/root/ci-mock/builds/4153',
- retry_path: '/root/ci-mock/builds/4153/retry',
- playable: false,
- created_at: '2017-04-13T09:25:18.959Z',
- updated_at: '2017-04-13T09:25:23.118Z',
- status: {
- icon: 'status_success',
- text: 'passed',
- label: 'passed',
- group: 'success',
- has_details: true,
- details_path: '/root/ci-mock/builds/4153',
- favicon:
- '/assets/ci_favicons/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.png',
- action: {
- icon: 'retry',
- title: 'Retry',
- path: '/root/ci-mock/builds/4153/retry',
- method: 'post',
+ groups: {
+ __typename: 'CiGroupConnection',
+ nodes: [
+ {
+ __typename: 'CiGroup',
+ name: 'build_a_nlfjkdnlvskfnksvjknlfdjvlvnjdkjdf_nvjkenjkrlngjeknjkl',
+ size: 1,
+ status: {
+ __typename: 'DetailedStatus',
+ label: 'passed',
+ group: 'success',
+ icon: 'status_success',
+ },
+ jobs: {
+ __typename: 'CiJobConnection',
+ nodes: [
+ {
+ __typename: 'CiJob',
+ name: 'build_a_nlfjkdnlvskfnksvjknlfdjvlvnjdkjdf_nvjkenjkrlngjeknjkl',
+ scheduledAt: null,
+ status: {
+ __typename: 'DetailedStatus',
+ icon: 'status_success',
+ tooltip: 'passed',
+ hasDetails: true,
+ detailsPath: '/root/abcd-dag/-/jobs/1482',
+ group: 'success',
+ action: {
+ __typename: 'StatusAction',
+ buttonTitle: 'Retry this job',
+ icon: 'retry',
+ path: '/root/abcd-dag/-/jobs/1482/retry',
+ title: 'Retry',
+ },
+ },
+ needs: {
+ __typename: 'CiJobConnection',
+ nodes: [],
+ },
+ },
+ ],
+ },
},
- },
- },
- ],
- },
- ],
- status: {
- icon: 'status_success',
- text: 'passed',
- label: 'passed',
- group: 'success',
- has_details: true,
- details_path: '/root/ci-mock/pipelines/123#test',
- favicon:
- '/assets/ci_favicons/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.png',
- },
- path: '/root/ci-mock/pipelines/123#test',
- dropdown_path: '/root/ci-mock/pipelines/123/stage.json?stage=test',
- },
- {
- name: 'deploy <img src=x onerror=alert(document.domain)>',
- title: 'deploy: passed',
- groups: [
- {
- name: 'deploy to production',
- size: 1,
- status: {
- icon: 'status_success',
- text: 'passed',
- label: 'passed',
- group: 'success',
- has_details: true,
- details_path: '/root/ci-mock/builds/4166',
- favicon:
- '/assets/ci_favicons/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.png',
- action: {
- icon: 'retry',
- title: 'Retry',
- path: '/root/ci-mock/builds/4166/retry',
- method: 'post',
- },
- },
- jobs: [
- {
- id: 4166,
- name: 'deploy to production',
- build_path: '/root/ci-mock/builds/4166',
- retry_path: '/root/ci-mock/builds/4166/retry',
- playable: false,
- created_at: '2017-04-19T14:29:46.463Z',
- updated_at: '2017-04-19T14:30:27.498Z',
- status: {
- icon: 'status_success',
- text: 'passed',
- label: 'passed',
- group: 'success',
- has_details: true,
- details_path: '/root/ci-mock/builds/4166',
- favicon:
- '/assets/ci_favicons/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.png',
- action: {
- icon: 'retry',
- title: 'Retry',
- path: '/root/ci-mock/builds/4166/retry',
- method: 'post',
+ {
+ __typename: 'CiGroup',
+ name: 'build_b',
+ size: 1,
+ status: {
+ __typename: 'DetailedStatus',
+ label: 'passed',
+ group: 'success',
+ icon: 'status_success',
+ },
+ jobs: {
+ __typename: 'CiJobConnection',
+ nodes: [
+ {
+ __typename: 'CiJob',
+ name: 'build_b',
+ scheduledAt: null,
+ status: {
+ __typename: 'DetailedStatus',
+ icon: 'status_success',
+ tooltip: 'passed',
+ hasDetails: true,
+ detailsPath: '/root/abcd-dag/-/jobs/1515',
+ group: 'success',
+ action: {
+ __typename: 'StatusAction',
+ buttonTitle: 'Retry this job',
+ icon: 'retry',
+ path: '/root/abcd-dag/-/jobs/1515/retry',
+ title: 'Retry',
+ },
+ },
+ needs: {
+ __typename: 'CiJobConnection',
+ nodes: [],
+ },
+ },
+ ],
+ },
},
- },
- },
- ],
- },
- {
- name: 'deploy to staging',
- size: 1,
- status: {
- icon: 'status_success',
- text: 'passed',
- label: 'passed',
- group: 'success',
- has_details: true,
- details_path: '/root/ci-mock/builds/4159',
- favicon:
- '/assets/ci_favicons/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.png',
- action: {
- icon: 'retry',
- title: 'Retry',
- path: '/root/ci-mock/builds/4159/retry',
- method: 'post',
+ {
+ __typename: 'CiGroup',
+ name: 'build_c',
+ size: 1,
+ status: {
+ __typename: 'DetailedStatus',
+ label: 'passed',
+ group: 'success',
+ icon: 'status_success',
+ },
+ jobs: {
+ __typename: 'CiJobConnection',
+ nodes: [
+ {
+ __typename: 'CiJob',
+ name: 'build_c',
+ scheduledAt: null,
+ status: {
+ __typename: 'DetailedStatus',
+ icon: 'status_success',
+ tooltip: 'passed',
+ hasDetails: true,
+ detailsPath: '/root/abcd-dag/-/jobs/1484',
+ group: 'success',
+ action: {
+ __typename: 'StatusAction',
+ buttonTitle: 'Retry this job',
+ icon: 'retry',
+ path: '/root/abcd-dag/-/jobs/1484/retry',
+ title: 'Retry',
+ },
+ },
+ needs: {
+ __typename: 'CiJobConnection',
+ nodes: [],
+ },
+ },
+ ],
+ },
+ },
+ {
+ __typename: 'CiGroup',
+ name: 'build_d',
+ size: 3,
+ status: {
+ __typename: 'DetailedStatus',
+ label: 'passed',
+ group: 'success',
+ icon: 'status_success',
+ },
+ jobs: {
+ __typename: 'CiJobConnection',
+ nodes: [
+ {
+ __typename: 'CiJob',
+ name: 'build_d 1/3',
+ scheduledAt: null,
+ status: {
+ __typename: 'DetailedStatus',
+ icon: 'status_success',
+ tooltip: 'passed',
+ hasDetails: true,
+ detailsPath: '/root/abcd-dag/-/jobs/1485',
+ group: 'success',
+ action: {
+ __typename: 'StatusAction',
+ buttonTitle: 'Retry this job',
+ icon: 'retry',
+ path: '/root/abcd-dag/-/jobs/1485/retry',
+ title: 'Retry',
+ },
+ },
+ needs: {
+ __typename: 'CiJobConnection',
+ nodes: [],
+ },
+ },
+ {
+ __typename: 'CiJob',
+ name: 'build_d 2/3',
+ scheduledAt: null,
+ status: {
+ __typename: 'DetailedStatus',
+ icon: 'status_success',
+ tooltip: 'passed',
+ hasDetails: true,
+ detailsPath: '/root/abcd-dag/-/jobs/1486',
+ group: 'success',
+ action: {
+ __typename: 'StatusAction',
+ buttonTitle: 'Retry this job',
+ icon: 'retry',
+ path: '/root/abcd-dag/-/jobs/1486/retry',
+ title: 'Retry',
+ },
+ },
+ needs: {
+ __typename: 'CiJobConnection',
+ nodes: [],
+ },
+ },
+ {
+ __typename: 'CiJob',
+ name: 'build_d 3/3',
+ scheduledAt: null,
+ status: {
+ __typename: 'DetailedStatus',
+ icon: 'status_success',
+ tooltip: 'passed',
+ hasDetails: true,
+ detailsPath: '/root/abcd-dag/-/jobs/1487',
+ group: 'success',
+ action: {
+ __typename: 'StatusAction',
+ buttonTitle: 'Retry this job',
+ icon: 'retry',
+ path: '/root/abcd-dag/-/jobs/1487/retry',
+ title: 'Retry',
+ },
+ },
+ needs: {
+ __typename: 'CiJobConnection',
+ nodes: [],
+ },
+ },
+ ],
+ },
+ },
+ ],
},
},
- jobs: [
- {
- id: 4159,
- name: 'deploy to staging',
- build_path: '/root/ci-mock/builds/4159',
- retry_path: '/root/ci-mock/builds/4159/retry',
- playable: false,
- created_at: '2017-04-18T16:32:08.420Z',
- updated_at: '2017-04-18T16:32:12.631Z',
- status: {
- icon: 'status_success',
- text: 'passed',
- label: 'passed',
- group: 'success',
- has_details: true,
- details_path: '/root/ci-mock/builds/4159',
- favicon:
- '/assets/ci_favicons/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.png',
- action: {
- icon: 'retry',
- title: 'Retry',
- path: '/root/ci-mock/builds/4159/retry',
- method: 'post',
+ {
+ __typename: 'CiStage',
+ name: 'test',
+ status: {
+ __typename: 'DetailedStatus',
+ action: null,
+ },
+ groups: {
+ __typename: 'CiGroupConnection',
+ nodes: [
+ {
+ __typename: 'CiGroup',
+ name: 'test_a',
+ size: 1,
+ status: {
+ __typename: 'DetailedStatus',
+ label: 'passed',
+ group: 'success',
+ icon: 'status_success',
+ },
+ jobs: {
+ __typename: 'CiJobConnection',
+ nodes: [
+ {
+ __typename: 'CiJob',
+ name: 'test_a',
+ scheduledAt: null,
+ status: {
+ __typename: 'DetailedStatus',
+ icon: 'status_success',
+ tooltip: 'passed',
+ hasDetails: true,
+ detailsPath: '/root/abcd-dag/-/jobs/1514',
+ group: 'success',
+ action: {
+ __typename: 'StatusAction',
+ buttonTitle: 'Retry this job',
+ icon: 'retry',
+ path: '/root/abcd-dag/-/jobs/1514/retry',
+ title: 'Retry',
+ },
+ },
+ needs: {
+ __typename: 'CiJobConnection',
+ nodes: [
+ {
+ __typename: 'CiJob',
+ name: 'build_c',
+ },
+ {
+ __typename: 'CiJob',
+ name: 'build_b',
+ },
+ {
+ __typename: 'CiJob',
+ name:
+ 'build_a_nlfjkdnlvskfnksvjknlfdjvlvnjdkjdf_nvjkenjkrlngjeknjkl',
+ },
+ ],
+ },
+ },
+ ],
+ },
+ },
+ {
+ __typename: 'CiGroup',
+ name: 'test_b',
+ size: 2,
+ status: {
+ __typename: 'DetailedStatus',
+ label: 'passed',
+ group: 'success',
+ icon: 'status_success',
+ },
+ jobs: {
+ __typename: 'CiJobConnection',
+ nodes: [
+ {
+ __typename: 'CiJob',
+ name: 'test_b 1/2',
+ scheduledAt: null,
+ status: {
+ __typename: 'DetailedStatus',
+ icon: 'status_success',
+ tooltip: 'passed',
+ hasDetails: true,
+ detailsPath: '/root/abcd-dag/-/jobs/1489',
+ group: 'success',
+ action: {
+ __typename: 'StatusAction',
+ buttonTitle: 'Retry this job',
+ icon: 'retry',
+ path: '/root/abcd-dag/-/jobs/1489/retry',
+ title: 'Retry',
+ },
+ },
+ needs: {
+ __typename: 'CiJobConnection',
+ nodes: [
+ {
+ __typename: 'CiJob',
+ name: 'build_d 3/3',
+ },
+ {
+ __typename: 'CiJob',
+ name: 'build_d 2/3',
+ },
+ {
+ __typename: 'CiJob',
+ name: 'build_d 1/3',
+ },
+ {
+ __typename: 'CiJob',
+ name: 'build_b',
+ },
+ {
+ __typename: 'CiJob',
+ name:
+ 'build_a_nlfjkdnlvskfnksvjknlfdjvlvnjdkjdf_nvjkenjkrlngjeknjkl',
+ },
+ ],
+ },
+ },
+ {
+ __typename: 'CiJob',
+ name: 'test_b 2/2',
+ scheduledAt: null,
+ status: {
+ __typename: 'DetailedStatus',
+ icon: 'status_success',
+ tooltip: 'passed',
+ hasDetails: true,
+ detailsPath: '/root/abcd-dag/-/jobs/1490',
+ group: 'success',
+ action: {
+ __typename: 'StatusAction',
+ buttonTitle: 'Retry this job',
+ icon: 'retry',
+ path: '/root/abcd-dag/-/jobs/1490/retry',
+ title: 'Retry',
+ },
+ },
+ needs: {
+ __typename: 'CiJobConnection',
+ nodes: [
+ {
+ __typename: 'CiJob',
+ name: 'build_d 3/3',
+ },
+ {
+ __typename: 'CiJob',
+ name: 'build_d 2/3',
+ },
+ {
+ __typename: 'CiJob',
+ name: 'build_d 1/3',
+ },
+ {
+ __typename: 'CiJob',
+ name: 'build_b',
+ },
+ {
+ __typename: 'CiJob',
+ name:
+ 'build_a_nlfjkdnlvskfnksvjknlfdjvlvnjdkjdf_nvjkenjkrlngjeknjkl',
+ },
+ ],
+ },
+ },
+ ],
+ },
+ },
+ {
+ __typename: 'CiGroup',
+ name: 'test_c',
+ size: 1,
+ status: {
+ __typename: 'DetailedStatus',
+ label: null,
+ group: 'success',
+ icon: 'status_success',
+ },
+ jobs: {
+ __typename: 'CiJobConnection',
+ nodes: [
+ {
+ __typename: 'CiJob',
+ name: 'test_c',
+ scheduledAt: null,
+ status: {
+ __typename: 'DetailedStatus',
+ icon: 'status_success',
+ tooltip: null,
+ hasDetails: true,
+ detailsPath: '/root/kinder-pipe/-/pipelines/154',
+ group: 'success',
+ action: null,
+ },
+ needs: {
+ __typename: 'CiJobConnection',
+ nodes: [
+ {
+ __typename: 'CiJob',
+ name: 'build_c',
+ },
+ {
+ __typename: 'CiJob',
+ name: 'build_b',
+ },
+ {
+ __typename: 'CiJob',
+ name:
+ 'build_a_nlfjkdnlvskfnksvjknlfdjvlvnjdkjdf_nvjkenjkrlngjeknjkl',
+ },
+ ],
+ },
+ },
+ ],
+ },
+ },
+ {
+ __typename: 'CiGroup',
+ name: 'test_d',
+ size: 1,
+ status: {
+ __typename: 'DetailedStatus',
+ label: null,
+ group: 'success',
+ icon: 'status_success',
+ },
+ jobs: {
+ __typename: 'CiJobConnection',
+ nodes: [
+ {
+ __typename: 'CiJob',
+ name: 'test_d',
+ scheduledAt: null,
+ status: {
+ __typename: 'DetailedStatus',
+ icon: 'status_success',
+ tooltip: null,
+ hasDetails: true,
+ detailsPath: '/root/abcd-dag/-/pipelines/153',
+ group: 'success',
+ action: null,
+ },
+ needs: {
+ __typename: 'CiJobConnection',
+ nodes: [
+ {
+ __typename: 'CiJob',
+ name: 'build_b',
+ },
+ ],
+ },
+ },
+ ],
+ },
},
- },
+ ],
},
- ],
- },
- ],
- status: {
- icon: 'status_success',
- text: 'passed',
- label: 'passed',
- group: 'success',
- has_details: true,
- details_path: '/root/ci-mock/pipelines/123#deploy',
- favicon:
- '/assets/ci_favicons/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.png',
+ },
+ ],
},
- path: '/root/ci-mock/pipelines/123#deploy',
- dropdown_path: '/root/ci-mock/pipelines/123/stage.json?stage=deploy',
- },
- ],
- artifacts: [],
- manual_actions: [
- {
- name: 'deploy to production',
- path: '/root/ci-mock/builds/4166/play',
- playable: false,
},
- ],
+ },
},
- flags: {
- latest: true,
- triggered: false,
- stuck: false,
- yaml_errors: false,
- retryable: false,
- cancelable: false,
+};
+
+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',
},
- ref: {
- name: 'master',
- path: '/root/ci-mock/tree/master',
- tag: false,
- branch: true,
+ sourceJob: null,
+ project: {
+ id: 'gid://gitlab/Project/23',
+ name: 'abcd-dag',
+ fullPath: 'root/abcd-dag',
+ __typename: 'Project',
},
- commit: {
- id: '798e5f902592192afaba73f4668ae30e56eae492',
- short_id: '798e5f90',
- title: "Merge branch 'new-branch' into 'master'\r",
- created_at: '2017-04-13T10:25:17.000+01:00',
- parent_ids: [
- '54d483b1ed156fbbf618886ddf7ab023e24f8738',
- 'c8e2d38a6c538822e81c57022a6e3a0cfedebbcc',
- ],
- message:
- "Merge branch 'new-branch' into 'master'\r\n\r\nAdd new file\r\n\r\nSee merge request !1",
- author_name: 'Root',
- author_email: 'admin@example.com',
- authored_date: '2017-04-13T10:25:17.000+01:00',
- committer_name: 'Root',
- committer_email: 'admin@example.com',
- committed_date: '2017-04-13T10:25:17.000+01:00',
- author: {
- name: 'Root',
- username: 'root',
- id: 1,
- state: 'active',
- avatar_url: null,
- web_url: 'http://localhost:3000/root',
+ __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',
+ },
+ },
+ },
+ ],
+ },
+ },
+ ],
+ },
+ },
+ ],
+ },
+ },
},
- author_gravatar_url: null,
- commit_url:
- 'http://localhost:3000/root/ci-mock/commit/798e5f902592192afaba73f4668ae30e56eae492',
- commit_path: '/root/ci-mock/commit/798e5f902592192afaba73f4668ae30e56eae492',
},
- created_at: '2017-04-13T09:25:18.881Z',
- updated_at: '2017-04-19T14:30:27.561Z',
+};
+
+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');
};
diff --git a/spec/frontend/pipelines/graph/mock_data_legacy.js b/spec/frontend/pipelines/graph/mock_data_legacy.js
new file mode 100644
index 00000000000..a4a5d78f906
--- /dev/null
+++ b/spec/frontend/pipelines/graph/mock_data_legacy.js
@@ -0,0 +1,261 @@
+export default {
+ id: 123,
+ user: {
+ name: 'Root',
+ username: 'root',
+ id: 1,
+ state: 'active',
+ avatar_url: null,
+ web_url: 'http://localhost:3000/root',
+ },
+ active: false,
+ coverage: null,
+ path: '/root/ci-mock/pipelines/123',
+ details: {
+ status: {
+ icon: 'status_success',
+ text: 'passed',
+ label: 'passed',
+ group: 'success',
+ has_details: true,
+ details_path: '/root/ci-mock/pipelines/123',
+ favicon:
+ '/assets/ci_favicons/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.png',
+ },
+ duration: 9,
+ finished_at: '2017-04-19T14:30:27.542Z',
+ stages: [
+ {
+ name: 'test',
+ title: 'test: passed',
+ groups: [
+ {
+ name: 'test',
+ size: 1,
+ status: {
+ icon: 'status_success',
+ text: 'passed',
+ label: 'passed',
+ group: 'success',
+ has_details: true,
+ details_path: '/root/ci-mock/builds/4153',
+ favicon:
+ '/assets/ci_favicons/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.png',
+ action: {
+ icon: 'retry',
+ title: 'Retry',
+ path: '/root/ci-mock/builds/4153/retry',
+ method: 'post',
+ },
+ },
+ jobs: [
+ {
+ id: 4153,
+ name: 'test',
+ build_path: '/root/ci-mock/builds/4153',
+ retry_path: '/root/ci-mock/builds/4153/retry',
+ playable: false,
+ created_at: '2017-04-13T09:25:18.959Z',
+ updated_at: '2017-04-13T09:25:23.118Z',
+ status: {
+ icon: 'status_success',
+ text: 'passed',
+ label: 'passed',
+ group: 'success',
+ has_details: true,
+ details_path: '/root/ci-mock/builds/4153',
+ favicon:
+ '/assets/ci_favicons/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.png',
+ action: {
+ icon: 'retry',
+ title: 'Retry',
+ path: '/root/ci-mock/builds/4153/retry',
+ method: 'post',
+ },
+ },
+ },
+ ],
+ },
+ ],
+ status: {
+ icon: 'status_success',
+ text: 'passed',
+ label: 'passed',
+ group: 'success',
+ has_details: true,
+ details_path: '/root/ci-mock/pipelines/123#test',
+ favicon:
+ '/assets/ci_favicons/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.png',
+ },
+ path: '/root/ci-mock/pipelines/123#test',
+ dropdown_path: '/root/ci-mock/pipelines/123/stage.json?stage=test',
+ },
+ {
+ name: 'deploy <img src=x onerror=alert(document.domain)>',
+ title: 'deploy: passed',
+ groups: [
+ {
+ name: 'deploy to production',
+ size: 1,
+ status: {
+ icon: 'status_success',
+ text: 'passed',
+ label: 'passed',
+ group: 'success',
+ has_details: true,
+ details_path: '/root/ci-mock/builds/4166',
+ favicon:
+ '/assets/ci_favicons/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.png',
+ action: {
+ icon: 'retry',
+ title: 'Retry',
+ path: '/root/ci-mock/builds/4166/retry',
+ method: 'post',
+ },
+ },
+ jobs: [
+ {
+ id: 4166,
+ name: 'deploy to production',
+ build_path: '/root/ci-mock/builds/4166',
+ retry_path: '/root/ci-mock/builds/4166/retry',
+ playable: false,
+ created_at: '2017-04-19T14:29:46.463Z',
+ updated_at: '2017-04-19T14:30:27.498Z',
+ status: {
+ icon: 'status_success',
+ text: 'passed',
+ label: 'passed',
+ group: 'success',
+ has_details: true,
+ details_path: '/root/ci-mock/builds/4166',
+ favicon:
+ '/assets/ci_favicons/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.png',
+ action: {
+ icon: 'retry',
+ title: 'Retry',
+ path: '/root/ci-mock/builds/4166/retry',
+ method: 'post',
+ },
+ },
+ },
+ ],
+ },
+ {
+ name: 'deploy to staging',
+ size: 1,
+ status: {
+ icon: 'status_success',
+ text: 'passed',
+ label: 'passed',
+ group: 'success',
+ has_details: true,
+ details_path: '/root/ci-mock/builds/4159',
+ favicon:
+ '/assets/ci_favicons/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.png',
+ action: {
+ icon: 'retry',
+ title: 'Retry',
+ path: '/root/ci-mock/builds/4159/retry',
+ method: 'post',
+ },
+ },
+ jobs: [
+ {
+ id: 4159,
+ name: 'deploy to staging',
+ build_path: '/root/ci-mock/builds/4159',
+ retry_path: '/root/ci-mock/builds/4159/retry',
+ playable: false,
+ created_at: '2017-04-18T16:32:08.420Z',
+ updated_at: '2017-04-18T16:32:12.631Z',
+ status: {
+ icon: 'status_success',
+ text: 'passed',
+ label: 'passed',
+ group: 'success',
+ has_details: true,
+ details_path: '/root/ci-mock/builds/4159',
+ favicon:
+ '/assets/ci_favicons/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.png',
+ action: {
+ icon: 'retry',
+ title: 'Retry',
+ path: '/root/ci-mock/builds/4159/retry',
+ method: 'post',
+ },
+ },
+ },
+ ],
+ },
+ ],
+ status: {
+ icon: 'status_success',
+ text: 'passed',
+ label: 'passed',
+ group: 'success',
+ has_details: true,
+ details_path: '/root/ci-mock/pipelines/123#deploy',
+ favicon:
+ '/assets/ci_favicons/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.png',
+ },
+ path: '/root/ci-mock/pipelines/123#deploy',
+ dropdown_path: '/root/ci-mock/pipelines/123/stage.json?stage=deploy',
+ },
+ ],
+ artifacts: [],
+ manual_actions: [
+ {
+ name: 'deploy to production',
+ path: '/root/ci-mock/builds/4166/play',
+ playable: false,
+ },
+ ],
+ },
+ flags: {
+ latest: true,
+ triggered: false,
+ stuck: false,
+ yaml_errors: false,
+ retryable: false,
+ cancelable: false,
+ },
+ ref: {
+ name: 'master',
+ path: '/root/ci-mock/tree/master',
+ tag: false,
+ branch: true,
+ },
+ commit: {
+ id: '798e5f902592192afaba73f4668ae30e56eae492',
+ short_id: '798e5f90',
+ title: "Merge branch 'new-branch' into 'master'\r",
+ created_at: '2017-04-13T10:25:17.000+01:00',
+ parent_ids: [
+ '54d483b1ed156fbbf618886ddf7ab023e24f8738',
+ 'c8e2d38a6c538822e81c57022a6e3a0cfedebbcc',
+ ],
+ message:
+ "Merge branch 'new-branch' into 'master'\r\n\r\nAdd new file\r\n\r\nSee merge request !1",
+ author_name: 'Root',
+ author_email: 'admin@example.com',
+ authored_date: '2017-04-13T10:25:17.000+01:00',
+ committer_name: 'Root',
+ committer_email: 'admin@example.com',
+ committed_date: '2017-04-13T10:25:17.000+01:00',
+ author: {
+ name: 'Root',
+ username: 'root',
+ id: 1,
+ state: 'active',
+ avatar_url: null,
+ web_url: 'http://localhost:3000/root',
+ },
+ author_gravatar_url: null,
+ commit_url:
+ 'http://localhost:3000/root/ci-mock/commit/798e5f902592192afaba73f4668ae30e56eae492',
+ commit_path: '/root/ci-mock/commit/798e5f902592192afaba73f4668ae30e56eae492',
+ },
+ created_at: '2017-04-13T09:25:18.881Z',
+ updated_at: '2017-04-19T14:30:27.561Z',
+};
diff --git a/spec/frontend/pipelines/graph/stage_column_component_legacy_spec.js b/spec/frontend/pipelines/graph/stage_column_component_legacy_spec.js
new file mode 100644
index 00000000000..463e4c12c7d
--- /dev/null
+++ b/spec/frontend/pipelines/graph/stage_column_component_legacy_spec.js
@@ -0,0 +1,135 @@
+import { shallowMount } from '@vue/test-utils';
+import StageColumnComponentLegacy from '~/pipelines/components/graph/stage_column_component_legacy.vue';
+
+describe('stage column component', () => {
+ const mockJob = {
+ id: 4250,
+ name: 'test',
+ status: {
+ icon: 'status_success',
+ text: 'passed',
+ label: 'passed',
+ group: 'success',
+ details_path: '/root/ci-mock/builds/4250',
+ action: {
+ icon: 'retry',
+ title: 'Retry',
+ path: '/root/ci-mock/builds/4250/retry',
+ method: 'post',
+ },
+ },
+ };
+
+ let wrapper;
+
+ beforeEach(() => {
+ const mockGroups = [];
+ for (let i = 0; i < 3; i += 1) {
+ const mockedJob = { ...mockJob };
+ mockedJob.id += i;
+ mockGroups.push(mockedJob);
+ }
+
+ wrapper = shallowMount(StageColumnComponentLegacy, {
+ propsData: {
+ title: 'foo',
+ groups: mockGroups,
+ hasTriggeredBy: false,
+ },
+ });
+ });
+
+ it('should render provided title', () => {
+ expect(
+ wrapper
+ .find('.stage-name')
+ .text()
+ .trim(),
+ ).toBe('foo');
+ });
+
+ it('should render the provided groups', () => {
+ expect(wrapper.findAll('.builds-container > ul > li').length).toBe(
+ wrapper.props('groups').length,
+ );
+ });
+
+ describe('jobId', () => {
+ it('escapes job name', () => {
+ wrapper = shallowMount(StageColumnComponentLegacy, {
+ propsData: {
+ groups: [
+ {
+ id: 4259,
+ name: '<img src=x onerror=alert(document.domain)>',
+ status: {
+ icon: 'status_success',
+ label: 'success',
+ tooltip: '<img src=x onerror=alert(document.domain)>',
+ },
+ },
+ ],
+ title: 'test',
+ hasTriggeredBy: false,
+ },
+ });
+
+ expect(wrapper.find('.builds-container li').attributes('id')).toBe(
+ 'ci-badge-&lt;img src=x onerror=alert(document.domain)&gt;',
+ );
+ });
+ });
+
+ describe('with action', () => {
+ it('renders action button', () => {
+ wrapper = shallowMount(StageColumnComponentLegacy, {
+ propsData: {
+ groups: [
+ {
+ id: 4259,
+ name: '<img src=x onerror=alert(document.domain)>',
+ status: {
+ icon: 'status_success',
+ label: 'success',
+ tooltip: '<img src=x onerror=alert(document.domain)>',
+ },
+ },
+ ],
+ title: 'test',
+ hasTriggeredBy: false,
+ action: {
+ icon: 'play',
+ title: 'Play all',
+ path: 'action',
+ },
+ },
+ });
+
+ expect(wrapper.find('.js-stage-action').exists()).toBe(true);
+ });
+ });
+
+ describe('without action', () => {
+ it('does not render action button', () => {
+ wrapper = shallowMount(StageColumnComponentLegacy, {
+ propsData: {
+ groups: [
+ {
+ id: 4259,
+ name: '<img src=x onerror=alert(document.domain)>',
+ status: {
+ icon: 'status_success',
+ label: 'success',
+ tooltip: '<img src=x onerror=alert(document.domain)>',
+ },
+ },
+ ],
+ title: 'test',
+ hasTriggeredBy: false,
+ },
+ });
+
+ expect(wrapper.find('.js-stage-action').exists()).toBe(false);
+ });
+ });
+});
diff --git a/spec/frontend/pipelines/graph/stage_column_component_spec.js b/spec/frontend/pipelines/graph/stage_column_component_spec.js
index d32534326c5..44803929f6d 100644
--- a/spec/frontend/pipelines/graph/stage_column_component_spec.js
+++ b/spec/frontend/pipelines/graph/stage_column_component_spec.js
@@ -1,64 +1,101 @@
-import { shallowMount } from '@vue/test-utils';
+import { mount, shallowMount } from '@vue/test-utils';
+import ActionComponent from '~/pipelines/components/graph/action_component.vue';
+import JobItem from '~/pipelines/components/graph/job_item.vue';
+import StageColumnComponent from '~/pipelines/components/graph/stage_column_component.vue';
-import stageColumnComponent from '~/pipelines/components/graph/stage_column_component.vue';
-
-describe('stage column component', () => {
- const mockJob = {
- id: 4250,
- name: 'test',
- status: {
- icon: 'status_success',
- text: 'passed',
- label: 'passed',
- group: 'success',
- details_path: '/root/ci-mock/builds/4250',
- action: {
- icon: 'retry',
- title: 'Retry',
- path: '/root/ci-mock/builds/4250/retry',
- method: 'post',
- },
+const mockJob = {
+ id: 4250,
+ name: 'test',
+ status: {
+ icon: 'status_success',
+ text: 'passed',
+ label: 'passed',
+ group: 'success',
+ details_path: '/root/ci-mock/builds/4250',
+ action: {
+ icon: 'retry',
+ title: 'Retry',
+ path: '/root/ci-mock/builds/4250/retry',
+ method: 'post',
},
- };
+ },
+};
+const mockGroups = Array(4)
+ .fill(0)
+ .map((item, idx) => {
+ return { ...mockJob, id: idx, name: `fish-${idx}` };
+ });
+
+const defaultProps = {
+ title: 'Fish',
+ groups: mockGroups,
+};
+
+describe('stage column component', () => {
let wrapper;
- beforeEach(() => {
- const mockGroups = [];
- for (let i = 0; i < 3; i += 1) {
- const mockedJob = { ...mockJob };
- mockedJob.id += i;
- mockGroups.push(mockedJob);
- }
+ const findStageColumnTitle = () => wrapper.find('[data-testid="stage-column-title"]');
+ const findStageColumnGroup = () => wrapper.find('[data-testid="stage-column-group"]');
+ const findAllStageColumnGroups = () => wrapper.findAll('[data-testid="stage-column-group"]');
+ const findJobItem = () => wrapper.find(JobItem);
+ const findActionComponent = () => wrapper.find(ActionComponent);
- wrapper = shallowMount(stageColumnComponent, {
+ const createComponent = ({ method = shallowMount, props = {} } = {}) => {
+ wrapper = method(StageColumnComponent, {
propsData: {
- title: 'foo',
- groups: mockGroups,
- hasTriggeredBy: false,
+ ...defaultProps,
+ ...props,
},
});
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
});
- it('should render provided title', () => {
- expect(
- wrapper
- .find('.stage-name')
- .text()
- .trim(),
- ).toBe('foo');
+ describe('when mounted', () => {
+ beforeEach(() => {
+ createComponent({ method: mount });
+ });
+
+ it('should render provided title', () => {
+ expect(findStageColumnTitle().text()).toBe(defaultProps.title);
+ });
+
+ it('should render the provided groups', () => {
+ expect(findAllStageColumnGroups().length).toBe(mockGroups.length);
+ });
});
- it('should render the provided groups', () => {
- expect(wrapper.findAll('.builds-container > ul > li').length).toBe(
- wrapper.props('groups').length,
- );
+ describe('when job notifies action is complete', () => {
+ beforeEach(() => {
+ createComponent({
+ method: mount,
+ props: {
+ groups: [
+ {
+ title: 'Fish',
+ size: 1,
+ jobs: [mockJob],
+ },
+ ],
+ },
+ });
+ findJobItem().vm.$emit('pipelineActionRequestComplete');
+ });
+
+ it('emits refreshPipelineGraph', () => {
+ expect(wrapper.emitted().refreshPipelineGraph).toHaveLength(1);
+ });
});
- describe('jobId', () => {
- it('escapes job name', () => {
- wrapper = shallowMount(stageColumnComponent, {
- propsData: {
+ describe('job', () => {
+ beforeEach(() => {
+ createComponent({
+ method: mount,
+ props: {
groups: [
{
id: 4259,
@@ -70,21 +107,29 @@ describe('stage column component', () => {
},
},
],
- title: 'test',
- hasTriggeredBy: false,
+ title: 'test <img src=x onerror=alert(document.domain)>',
},
});
+ });
- expect(wrapper.find('.builds-container li').attributes('id')).toBe(
+ it('capitalizes and escapes name', () => {
+ expect(findStageColumnTitle().text()).toBe(
+ 'Test &lt;img src=x onerror=alert(document.domain)&gt;',
+ );
+ });
+
+ it('escapes id', () => {
+ expect(findStageColumnGroup().attributes('id')).toBe(
'ci-badge-&lt;img src=x onerror=alert(document.domain)&gt;',
);
});
});
describe('with action', () => {
- it('renders action button', () => {
- wrapper = shallowMount(stageColumnComponent, {
- propsData: {
+ beforeEach(() => {
+ createComponent({
+ method: mount,
+ props: {
groups: [
{
id: 4259,
@@ -105,15 +150,18 @@ describe('stage column component', () => {
},
},
});
+ });
- expect(wrapper.find('.js-stage-action').exists()).toBe(true);
+ it('renders action button', () => {
+ expect(findActionComponent().exists()).toBe(true);
});
});
describe('without action', () => {
- it('does not render action button', () => {
- wrapper = shallowMount(stageColumnComponent, {
- propsData: {
+ beforeEach(() => {
+ createComponent({
+ method: mount,
+ props: {
groups: [
{
id: 4259,
@@ -129,8 +177,10 @@ describe('stage column component', () => {
hasTriggeredBy: false,
},
});
+ });
- expect(wrapper.find('.js-stage-action').exists()).toBe(false);
+ it('does not render action button', () => {
+ expect(findActionComponent().exists()).toBe(false);
});
});
});