diff options
Diffstat (limited to 'spec/frontend/pipelines')
83 files changed, 0 insertions, 18536 deletions
diff --git a/spec/frontend/pipelines/components/dag/__snapshots__/dag_graph_spec.js.snap b/spec/frontend/pipelines/components/dag/__snapshots__/dag_graph_spec.js.snap deleted file mode 100644 index cb5f6ff5307..00000000000 --- a/spec/frontend/pipelines/components/dag/__snapshots__/dag_graph_spec.js.snap +++ /dev/null @@ -1,230 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`The DAG graph in the basic case renders the graph svg 1`] = ` -"<svg viewBox=\\"0,0,1000,540\\" width=\\"1000\\" height=\\"540\\"> - <g fill=\\"none\\" stroke-opacity=\\"0.8\\"> - <g id=\\"dag-link43\\" class=\\"dag-link gl-transition-property-stroke-opacity gl-cursor-pointer gl-transition-duration-slow gl-transition-timing-function-ease\\"> - <linearGradient id=\\"dag-grad53\\" gradientUnits=\\"userSpaceOnUse\\" x1=\\"116\\" x2=\\"361.3333333333333\\"> - <stop offset=\\"0%\\" stop-color=\\"#e17223\\"></stop> - <stop offset=\\"100%\\" stop-color=\\"#83ab4a\\"></stop> - </linearGradient> - <clipPath id=\\"dag-clip63\\"> - <path d=\\" - M100, 129 - V158 - H377.3333333333333 - V100 - H100 - Z - \\"></path> - </clipPath> - <path d=\\"M108,129L190,129L190,129L369.3333333333333,129\\" stroke=\\"url(#dag-grad53)\\" style=\\"stroke-linejoin: round;\\" stroke-width=\\"56\\" clip-path=\\"url(#dag-clip63)\\"></path> - </g> - <g id=\\"dag-link44\\" class=\\"dag-link gl-transition-property-stroke-opacity gl-cursor-pointer gl-transition-duration-slow gl-transition-timing-function-ease\\"> - <linearGradient id=\\"dag-grad54\\" gradientUnits=\\"userSpaceOnUse\\" x1=\\"377.3333333333333\\" x2=\\"622.6666666666666\\"> - <stop offset=\\"0%\\" stop-color=\\"#83ab4a\\"></stop> - <stop offset=\\"100%\\" stop-color=\\"#6f3500\\"></stop> - </linearGradient> - <clipPath id=\\"dag-clip64\\"> - <path d=\\" - M361.3333333333333, 129.0000000000002 - V158.0000000000002 - H638.6666666666666 - V100 - H361.3333333333333 - Z - \\"></path> - </clipPath> - <path d=\\"M369.3333333333333,129L509.3333333333333,129L509.3333333333333,129.0000000000002L630.6666666666666,129.0000000000002\\" stroke=\\"url(#dag-grad54)\\" style=\\"stroke-linejoin: round;\\" stroke-width=\\"56\\" clip-path=\\"url(#dag-clip64)\\"></path> - </g> - <g id=\\"dag-link45\\" class=\\"dag-link gl-transition-property-stroke-opacity gl-cursor-pointer gl-transition-duration-slow gl-transition-timing-function-ease\\"> - <linearGradient id=\\"dag-grad55\\" gradientUnits=\\"userSpaceOnUse\\" x1=\\"116\\" x2=\\"622.6666666666666\\"> - <stop offset=\\"0%\\" stop-color=\\"#5772ff\\"></stop> - <stop offset=\\"100%\\" stop-color=\\"#6f3500\\"></stop> - </linearGradient> - <clipPath id=\\"dag-clip65\\"> - <path d=\\" - M100, 187.0000000000002 - V241.00000000000003 - H638.6666666666666 - V158.0000000000002 - H100 - Z - \\"></path> - </clipPath> - <path d=\\"M108,212.00000000000003L306,212.00000000000003L306,187.0000000000002L630.6666666666666,187.0000000000002\\" stroke=\\"url(#dag-grad55)\\" style=\\"stroke-linejoin: round;\\" stroke-width=\\"56\\" clip-path=\\"url(#dag-clip65)\\"></path> - </g> - <g id=\\"dag-link46\\" class=\\"dag-link gl-transition-property-stroke-opacity gl-cursor-pointer gl-transition-duration-slow gl-transition-timing-function-ease\\"> - <linearGradient id=\\"dag-grad56\\" gradientUnits=\\"userSpaceOnUse\\" x1=\\"116\\" x2=\\"361.3333333333333\\"> - <stop offset=\\"0%\\" stop-color=\\"#b24800\\"></stop> - <stop offset=\\"100%\\" stop-color=\\"#006887\\"></stop> - </linearGradient> - <clipPath id=\\"dag-clip66\\"> - <path d=\\" - M100, 269.9999999999998 - V324 - H377.3333333333333 - V240.99999999999977 - H100 - Z - \\"></path> - </clipPath> - <path d=\\"M108,295L338.93333333333334,295L338.93333333333334,269.9999999999998L369.3333333333333,269.9999999999998\\" stroke=\\"url(#dag-grad56)\\" style=\\"stroke-linejoin: round;\\" stroke-width=\\"56\\" clip-path=\\"url(#dag-clip66)\\"></path> - </g> - <g id=\\"dag-link47\\" class=\\"dag-link gl-transition-property-stroke-opacity gl-cursor-pointer gl-transition-duration-slow gl-transition-timing-function-ease\\"> - <linearGradient id=\\"dag-grad57\\" gradientUnits=\\"userSpaceOnUse\\" x1=\\"116\\" x2=\\"361.3333333333333\\"> - <stop offset=\\"0%\\" stop-color=\\"#25d2d2\\"></stop> - <stop offset=\\"100%\\" stop-color=\\"#487900\\"></stop> - </linearGradient> - <clipPath id=\\"dag-clip67\\"> - <path d=\\" - M100, 352.99999999999994 - V407.00000000000006 - H377.3333333333333 - V323.99999999999994 - H100 - Z - \\"></path> - </clipPath> - <path d=\\"M108,378.00000000000006L144.66666666666669,378.00000000000006L144.66666666666669,352.99999999999994L369.3333333333333,352.99999999999994\\" stroke=\\"url(#dag-grad57)\\" style=\\"stroke-linejoin: round;\\" stroke-width=\\"56\\" clip-path=\\"url(#dag-clip67)\\"></path> - </g> - <g id=\\"dag-link48\\" class=\\"dag-link gl-transition-property-stroke-opacity gl-cursor-pointer gl-transition-duration-slow gl-transition-timing-function-ease\\"> - <linearGradient id=\\"dag-grad58\\" gradientUnits=\\"userSpaceOnUse\\" x1=\\"377.3333333333333\\" x2=\\"622.6666666666666\\"> - <stop offset=\\"0%\\" stop-color=\\"#006887\\"></stop> - <stop offset=\\"100%\\" stop-color=\\"#d84280\\"></stop> - </linearGradient> - <clipPath id=\\"dag-clip68\\"> - <path d=\\" - M361.3333333333333, 270.0000000000001 - V299.0000000000001 - H638.6666666666666 - V240.99999999999977 - H361.3333333333333 - Z - \\"></path> - </clipPath> - <path d=\\"M369.3333333333333,269.9999999999998L464,269.9999999999998L464,270.0000000000001L630.6666666666666,270.0000000000001\\" stroke=\\"url(#dag-grad58)\\" style=\\"stroke-linejoin: round;\\" stroke-width=\\"56\\" clip-path=\\"url(#dag-clip68)\\"></path> - </g> - <g id=\\"dag-link49\\" class=\\"dag-link gl-transition-property-stroke-opacity gl-cursor-pointer gl-transition-duration-slow gl-transition-timing-function-ease\\"> - <linearGradient id=\\"dag-grad59\\" gradientUnits=\\"userSpaceOnUse\\" x1=\\"377.3333333333333\\" x2=\\"622.6666666666666\\"> - <stop offset=\\"0%\\" stop-color=\\"#487900\\"></stop> - <stop offset=\\"100%\\" stop-color=\\"#d84280\\"></stop> - </linearGradient> - <clipPath id=\\"dag-clip69\\"> - <path d=\\" - M361.3333333333333, 328.0000000000001 - V381.99999999999994 - H638.6666666666666 - V299.0000000000001 - H361.3333333333333 - Z - \\"></path> - </clipPath> - <path d=\\"M369.3333333333333,352.99999999999994L522,352.99999999999994L522,328.0000000000001L630.6666666666666,328.0000000000001\\" stroke=\\"url(#dag-grad59)\\" style=\\"stroke-linejoin: round;\\" stroke-width=\\"56\\" clip-path=\\"url(#dag-clip69)\\"></path> - </g> - <g id=\\"dag-link50\\" class=\\"dag-link gl-transition-property-stroke-opacity gl-cursor-pointer gl-transition-duration-slow gl-transition-timing-function-ease\\"> - <linearGradient id=\\"dag-grad60\\" gradientUnits=\\"userSpaceOnUse\\" x1=\\"377.3333333333333\\" x2=\\"622.6666666666666\\"> - <stop offset=\\"0%\\" stop-color=\\"#487900\\"></stop> - <stop offset=\\"100%\\" stop-color=\\"#3547de\\"></stop> - </linearGradient> - <clipPath id=\\"dag-clip70\\"> - <path d=\\" - M361.3333333333333, 411 - V440 - H638.6666666666666 - V381.99999999999994 - H361.3333333333333 - Z - \\"></path> - </clipPath> - <path d=\\"M369.3333333333333,410.99999999999994L580,410.99999999999994L580,411L630.6666666666666,411\\" stroke=\\"url(#dag-grad60)\\" style=\\"stroke-linejoin: round;\\" stroke-width=\\"56\\" clip-path=\\"url(#dag-clip70)\\"></path> - </g> - <g id=\\"dag-link51\\" class=\\"dag-link gl-transition-property-stroke-opacity gl-cursor-pointer gl-transition-duration-slow gl-transition-timing-function-ease\\"> - <linearGradient id=\\"dag-grad61\\" gradientUnits=\\"userSpaceOnUse\\" x1=\\"638.6666666666666\\" x2=\\"884\\"> - <stop offset=\\"0%\\" stop-color=\\"#d84280\\"></stop> - <stop offset=\\"100%\\" stop-color=\\"#006887\\"></stop> - </linearGradient> - <clipPath id=\\"dag-clip71\\"> - <path d=\\" - M622.6666666666666, 270.1890725105691 - V299.1890725105691 - H900 - V241.0000000000001 - H622.6666666666666 - Z - \\"></path> - </clipPath> - <path d=\\"M630.6666666666666,270.0000000000001L861.6,270.0000000000001L861.6,270.1890725105691L892,270.1890725105691\\" stroke=\\"url(#dag-grad61)\\" style=\\"stroke-linejoin: round;\\" stroke-width=\\"56\\" clip-path=\\"url(#dag-clip71)\\"></path> - </g> - <g id=\\"dag-link52\\" class=\\"dag-link gl-transition-property-stroke-opacity gl-cursor-pointer gl-transition-duration-slow gl-transition-timing-function-ease\\"> - <linearGradient id=\\"dag-grad62\\" gradientUnits=\\"userSpaceOnUse\\" x1=\\"638.6666666666666\\" x2=\\"884\\"> - <stop offset=\\"0%\\" stop-color=\\"#3547de\\"></stop> - <stop offset=\\"100%\\" stop-color=\\"#275600\\"></stop> - </linearGradient> - <clipPath id=\\"dag-clip72\\"> - <path d=\\" - M622.6666666666666, 411 - V440 - H900 - V382 - H622.6666666666666 - Z - \\"></path> - </clipPath> - <path d=\\"M630.6666666666666,411L679.9999999999999,411L679.9999999999999,411L892,411\\" stroke=\\"url(#dag-grad62)\\" style=\\"stroke-linejoin: round;\\" stroke-width=\\"56\\" clip-path=\\"url(#dag-clip72)\\"></path> - </g> - </g> - <g> - <line class=\\"dag-node gl-transition-property-stroke gl-cursor-pointer gl-transition-duration-slow gl-transition-timing-function-ease\\" id=\\"dag-node73\\" stroke=\\"#e17223\\" stroke-width=\\"16\\" stroke-linecap=\\"round\\" x1=\\"108\\" x2=\\"108\\" y1=\\"104\\" y2=\\"154.00000000000003\\"></line> - <line class=\\"dag-node gl-transition-property-stroke gl-cursor-pointer gl-transition-duration-slow gl-transition-timing-function-ease\\" id=\\"dag-node74\\" stroke=\\"#83ab4a\\" stroke-width=\\"16\\" stroke-linecap=\\"round\\" x1=\\"369\\" x2=\\"369\\" y1=\\"104\\" y2=\\"154\\"></line> - <line class=\\"dag-node gl-transition-property-stroke gl-cursor-pointer gl-transition-duration-slow gl-transition-timing-function-ease\\" id=\\"dag-node75\\" stroke=\\"#5772ff\\" stroke-width=\\"16\\" stroke-linecap=\\"round\\" x1=\\"108\\" x2=\\"108\\" y1=\\"187.00000000000003\\" y2=\\"237.00000000000003\\"></line> - <line class=\\"dag-node gl-transition-property-stroke gl-cursor-pointer gl-transition-duration-slow gl-transition-timing-function-ease\\" id=\\"dag-node76\\" stroke=\\"#b24800\\" stroke-width=\\"16\\" stroke-linecap=\\"round\\" x1=\\"108\\" x2=\\"108\\" y1=\\"270\\" y2=\\"320.00000000000006\\"></line> - <line class=\\"dag-node gl-transition-property-stroke gl-cursor-pointer gl-transition-duration-slow gl-transition-timing-function-ease\\" id=\\"dag-node77\\" stroke=\\"#25d2d2\\" stroke-width=\\"16\\" stroke-linecap=\\"round\\" x1=\\"108\\" x2=\\"108\\" y1=\\"353.00000000000006\\" y2=\\"403.0000000000001\\"></line> - <line class=\\"dag-node gl-transition-property-stroke gl-cursor-pointer gl-transition-duration-slow gl-transition-timing-function-ease\\" id=\\"dag-node78\\" stroke=\\"#6f3500\\" stroke-width=\\"16\\" stroke-linecap=\\"round\\" x1=\\"630\\" x2=\\"630\\" y1=\\"104.0000000000002\\" y2=\\"212.00000000000009\\"></line> - <line class=\\"dag-node gl-transition-property-stroke gl-cursor-pointer gl-transition-duration-slow gl-transition-timing-function-ease\\" id=\\"dag-node79\\" stroke=\\"#006887\\" stroke-width=\\"16\\" stroke-linecap=\\"round\\" x1=\\"369\\" x2=\\"369\\" y1=\\"244.99999999999977\\" y2=\\"294.99999999999994\\"></line> - <line class=\\"dag-node gl-transition-property-stroke gl-cursor-pointer gl-transition-duration-slow gl-transition-timing-function-ease\\" id=\\"dag-node80\\" stroke=\\"#487900\\" stroke-width=\\"16\\" stroke-linecap=\\"round\\" x1=\\"369\\" x2=\\"369\\" y1=\\"327.99999999999994\\" y2=\\"436\\"></line> - <line class=\\"dag-node gl-transition-property-stroke gl-cursor-pointer gl-transition-duration-slow gl-transition-timing-function-ease\\" id=\\"dag-node81\\" stroke=\\"#d84280\\" stroke-width=\\"16\\" stroke-linecap=\\"round\\" x1=\\"630\\" x2=\\"630\\" y1=\\"245.00000000000009\\" y2=\\"353\\"></line> - <line class=\\"dag-node gl-transition-property-stroke gl-cursor-pointer gl-transition-duration-slow gl-transition-timing-function-ease\\" id=\\"dag-node82\\" stroke=\\"#3547de\\" stroke-width=\\"16\\" stroke-linecap=\\"round\\" x1=\\"630\\" x2=\\"630\\" y1=\\"386\\" y2=\\"436\\"></line> - <line class=\\"dag-node gl-transition-property-stroke gl-cursor-pointer gl-transition-duration-slow gl-transition-timing-function-ease\\" id=\\"dag-node83\\" stroke=\\"#006887\\" stroke-width=\\"16\\" stroke-linecap=\\"round\\" x1=\\"892\\" x2=\\"892\\" y1=\\"245.18907251056908\\" y2=\\"295.1890725105691\\"></line> - <line class=\\"dag-node gl-transition-property-stroke gl-cursor-pointer gl-transition-duration-slow gl-transition-timing-function-ease\\" id=\\"dag-node84\\" stroke=\\"#275600\\" stroke-width=\\"16\\" stroke-linecap=\\"round\\" x1=\\"892\\" x2=\\"892\\" y1=\\"386\\" y2=\\"436\\"></line> - </g> - <g class=\\"gl-font-sm\\"> - <foreignObject requiredFeatures=\\"http://www.w3.org/TR/SVG11/feature#Extensibility\\" height=\\"58.00000000000003px\\" width=\\"84\\" x=\\"8\\" y=\\"100\\" class=\\"gl-overflow-visible\\"> - <div class=\\"gl-display-flex gl-pointer-events-none gl-flex-direction-column gl-justify-content-center gl-overflow-wrap-break\\" style=\\"height: 58.00000000000003px; text-align: right;\\">build_a</div> - </foreignObject> - <foreignObject requiredFeatures=\\"http://www.w3.org/TR/SVG11/feature#Extensibility\\" height=\\"25px\\" width=\\"84\\" x=\\"369.3333333333333\\" y=\\"75\\" class=\\"gl-overflow-visible\\"> - <div class=\\"gl-display-flex gl-pointer-events-none gl-flex-direction-column gl-justify-content-center gl-overflow-wrap-break\\" style=\\"height: 25px; text-align: left;\\">test_a</div> - </foreignObject> - <foreignObject requiredFeatures=\\"http://www.w3.org/TR/SVG11/feature#Extensibility\\" height=\\"58px\\" width=\\"84\\" x=\\"8\\" y=\\"183.00000000000003\\" class=\\"gl-overflow-visible\\"> - <div class=\\"gl-display-flex gl-pointer-events-none gl-flex-direction-column gl-justify-content-center gl-overflow-wrap-break\\" style=\\"height: 58px; text-align: right;\\">test_b</div> - </foreignObject> - <foreignObject requiredFeatures=\\"http://www.w3.org/TR/SVG11/feature#Extensibility\\" height=\\"58.00000000000006px\\" width=\\"84\\" x=\\"8\\" y=\\"266\\" class=\\"gl-overflow-visible\\"> - <div class=\\"gl-display-flex gl-pointer-events-none gl-flex-direction-column gl-justify-content-center gl-overflow-wrap-break\\" style=\\"height: 58.00000000000006px; text-align: right;\\">post_test_a</div> - </foreignObject> - <foreignObject requiredFeatures=\\"http://www.w3.org/TR/SVG11/feature#Extensibility\\" height=\\"58.00000000000006px\\" width=\\"84\\" x=\\"8\\" y=\\"349.00000000000006\\" class=\\"gl-overflow-visible\\"> - <div class=\\"gl-display-flex gl-pointer-events-none gl-flex-direction-column gl-justify-content-center gl-overflow-wrap-break\\" style=\\"height: 58.00000000000006px; text-align: right;\\">post_test_b</div> - </foreignObject> - <foreignObject requiredFeatures=\\"http://www.w3.org/TR/SVG11/feature#Extensibility\\" height=\\"25px\\" width=\\"84\\" x=\\"630.6666666666666\\" y=\\"75.0000000000002\\" class=\\"gl-overflow-visible\\"> - <div class=\\"gl-display-flex gl-pointer-events-none gl-flex-direction-column gl-justify-content-center gl-overflow-wrap-break\\" style=\\"height: 25px; text-align: right;\\">post_test_c</div> - </foreignObject> - <foreignObject requiredFeatures=\\"http://www.w3.org/TR/SVG11/feature#Extensibility\\" height=\\"25px\\" width=\\"84\\" x=\\"369.3333333333333\\" y=\\"215.99999999999977\\" class=\\"gl-overflow-visible\\"> - <div class=\\"gl-display-flex gl-pointer-events-none gl-flex-direction-column gl-justify-content-center gl-overflow-wrap-break\\" style=\\"height: 25px; text-align: left;\\">staging_a</div> - </foreignObject> - <foreignObject requiredFeatures=\\"http://www.w3.org/TR/SVG11/feature#Extensibility\\" height=\\"25px\\" width=\\"84\\" x=\\"369.3333333333333\\" y=\\"298.99999999999994\\" class=\\"gl-overflow-visible\\"> - <div class=\\"gl-display-flex gl-pointer-events-none gl-flex-direction-column gl-justify-content-center gl-overflow-wrap-break\\" style=\\"height: 25px; text-align: left;\\">staging_b</div> - </foreignObject> - <foreignObject requiredFeatures=\\"http://www.w3.org/TR/SVG11/feature#Extensibility\\" height=\\"25px\\" width=\\"84\\" x=\\"630.6666666666666\\" y=\\"216.00000000000009\\" class=\\"gl-overflow-visible\\"> - <div class=\\"gl-display-flex gl-pointer-events-none gl-flex-direction-column gl-justify-content-center gl-overflow-wrap-break\\" style=\\"height: 25px; text-align: right;\\">canary_a</div> - </foreignObject> - <foreignObject requiredFeatures=\\"http://www.w3.org/TR/SVG11/feature#Extensibility\\" height=\\"25px\\" width=\\"84\\" x=\\"630.6666666666666\\" y=\\"357\\" class=\\"gl-overflow-visible\\"> - <div class=\\"gl-display-flex gl-pointer-events-none gl-flex-direction-column gl-justify-content-center gl-overflow-wrap-break\\" style=\\"height: 25px; text-align: right;\\">canary_c</div> - </foreignObject> - <foreignObject requiredFeatures=\\"http://www.w3.org/TR/SVG11/feature#Extensibility\\" height=\\"58px\\" width=\\"84\\" x=\\"908\\" y=\\"241.18907251056908\\" class=\\"gl-overflow-visible\\"> - <div class=\\"gl-display-flex gl-pointer-events-none gl-flex-direction-column gl-justify-content-center gl-overflow-wrap-break\\" style=\\"height: 58px; text-align: left;\\">production_a</div> - </foreignObject> - <foreignObject requiredFeatures=\\"http://www.w3.org/TR/SVG11/feature#Extensibility\\" height=\\"58px\\" width=\\"84\\" x=\\"908\\" y=\\"382\\" class=\\"gl-overflow-visible\\"> - <div class=\\"gl-display-flex gl-pointer-events-none gl-flex-direction-column gl-justify-content-center gl-overflow-wrap-break\\" style=\\"height: 58px; text-align: left;\\">production_d</div> - </foreignObject> - </g> -</svg>" -`; diff --git a/spec/frontend/pipelines/components/dag/dag_annotations_spec.js b/spec/frontend/pipelines/components/dag/dag_annotations_spec.js deleted file mode 100644 index 124f02bcec7..00000000000 --- a/spec/frontend/pipelines/components/dag/dag_annotations_spec.js +++ /dev/null @@ -1,98 +0,0 @@ -import { GlButton } from '@gitlab/ui'; -import { shallowMount, mount } from '@vue/test-utils'; -import { nextTick } from 'vue'; -import DagAnnotations from '~/pipelines/components/dag/dag_annotations.vue'; -import { singleNote, multiNote } from './mock_data'; - -describe('The DAG annotations', () => { - let wrapper; - - const getColorBlock = () => wrapper.find('[data-testid="dag-color-block"]'); - const getAllColorBlocks = () => wrapper.findAll('[data-testid="dag-color-block"]'); - const getTextBlock = () => wrapper.find('[data-testid="dag-note-text"]'); - const getAllTextBlocks = () => wrapper.findAll('[data-testid="dag-note-text"]'); - const getToggleButton = () => wrapper.findComponent(GlButton); - - const createComponent = (propsData = {}, method = shallowMount) => { - wrapper = method(DagAnnotations, { - propsData, - data() { - return { - showList: true, - }; - }, - }); - }; - - describe('when there is one annotation', () => { - const currentNote = singleNote['dag-link103']; - - beforeEach(() => { - createComponent({ annotations: singleNote }); - }); - - it('displays the color block', () => { - expect(getColorBlock().exists()).toBe(true); - }); - - it('displays the text block', () => { - expect(getTextBlock().exists()).toBe(true); - expect(getTextBlock().text()).toBe(`${currentNote.source.name} → ${currentNote.target.name}`); - }); - - it('does not display the list toggle link', () => { - expect(getToggleButton().exists()).toBe(false); - }); - }); - - describe('when there are multiple annoataions', () => { - beforeEach(() => { - createComponent({ annotations: multiNote }); - }); - - it('displays a color block for each link', () => { - expect(getAllColorBlocks().length).toBe(Object.keys(multiNote).length); - }); - - it('displays a text block for each link', () => { - expect(getAllTextBlocks().length).toBe(Object.keys(multiNote).length); - - Object.values(multiNote).forEach((item, idx) => { - expect(getAllTextBlocks().at(idx).text()).toBe(`${item.source.name} → ${item.target.name}`); - }); - }); - - it('displays the list toggle link', () => { - expect(getToggleButton().exists()).toBe(true); - expect(getToggleButton().text()).toBe('Hide list'); - }); - }); - - describe('the list toggle', () => { - beforeEach(() => { - createComponent({ annotations: multiNote }, mount); - }); - - describe('clicking hide', () => { - it('hides listed items and changes text to show', async () => { - expect(getAllTextBlocks().length).toBe(Object.keys(multiNote).length); - expect(getToggleButton().text()).toBe('Hide list'); - getToggleButton().trigger('click'); - await nextTick(); - expect(getAllTextBlocks().length).toBe(0); - expect(getToggleButton().text()).toBe('Show list'); - }); - }); - - describe('clicking show', () => { - it('shows listed items and changes text to hide', async () => { - getToggleButton().trigger('click'); - getToggleButton().trigger('click'); - - await nextTick(); - expect(getAllTextBlocks().length).toBe(Object.keys(multiNote).length); - expect(getToggleButton().text()).toBe('Hide list'); - }); - }); - }); -}); diff --git a/spec/frontend/pipelines/components/dag/dag_graph_spec.js b/spec/frontend/pipelines/components/dag/dag_graph_spec.js deleted file mode 100644 index 6b46be3dd49..00000000000 --- a/spec/frontend/pipelines/components/dag/dag_graph_spec.js +++ /dev/null @@ -1,209 +0,0 @@ -import { shallowMount } from '@vue/test-utils'; -import { IS_HIGHLIGHTED, LINK_SELECTOR, NODE_SELECTOR } from '~/pipelines/components/dag/constants'; -import DagGraph from '~/pipelines/components/dag/dag_graph.vue'; -import { createSankey } from '~/pipelines/components/dag/drawing_utils'; -import { highlightIn, highlightOut } from '~/pipelines/components/dag/interactions'; -import { removeOrphanNodes } from '~/pipelines/components/parsing_utils'; -import { parsedData } from './mock_data'; - -describe('The DAG graph', () => { - let wrapper; - - const getGraph = () => wrapper.find('.dag-graph-container > svg'); - const getAllLinks = () => wrapper.findAll(`.${LINK_SELECTOR}`); - const getAllNodes = () => wrapper.findAll(`.${NODE_SELECTOR}`); - const getAllLabels = () => wrapper.findAll('foreignObject'); - - const createComponent = (propsData = {}) => { - if (wrapper?.destroy) { - wrapper.destroy(); - } - - wrapper = shallowMount(DagGraph, { - attachTo: document.body, - propsData, - data() { - return { - color: () => {}, - width: 0, - height: 0, - }; - }, - }); - }; - - beforeEach(() => { - createComponent({ graphData: parsedData }); - }); - - describe('in the basic case', () => { - beforeEach(() => { - /* - The graph uses random to offset links. To keep the snapshot consistent, - we mock Math.random. Wheeeee! - */ - const randomNumber = jest.spyOn(global.Math, 'random'); - randomNumber.mockImplementation(() => 0.2); - createComponent({ graphData: parsedData }); - }); - - it('renders the graph svg', () => { - expect(getGraph().exists()).toBe(true); - expect(getGraph().html()).toMatchSnapshot(); - }); - }); - - describe('links', () => { - it('renders the expected number of links', () => { - expect(getAllLinks()).toHaveLength(parsedData.links.length); - }); - - it('renders the expected number of gradients', () => { - expect(wrapper.findAll('linearGradient')).toHaveLength(parsedData.links.length); - }); - - it('renders the expected number of clip paths', () => { - expect(wrapper.findAll('clipPath')).toHaveLength(parsedData.links.length); - }); - }); - - describe('nodes and labels', () => { - const sankeyNodes = createSankey()(parsedData).nodes; - const processedNodes = removeOrphanNodes(sankeyNodes); - - describe('nodes', () => { - it('renders the expected number of nodes', () => { - expect(getAllNodes()).toHaveLength(processedNodes.length); - }); - }); - - describe('labels', () => { - it('renders the expected number of labels as foreignObjects', () => { - expect(getAllLabels()).toHaveLength(processedNodes.length); - }); - - it('renders the title as text', () => { - expect(getAllLabels().at(0).text()).toBe(parsedData.nodes[0].name); - }); - }); - }); - - describe('interactions', () => { - const strokeOpacity = (opacity) => `stroke-opacity: ${opacity};`; - const baseOpacity = () => wrapper.vm.$options.viewOptions.baseOpacity; - - describe('links', () => { - const liveLink = () => getAllLinks().at(4); - const otherLink = () => getAllLinks().at(1); - - describe('on hover', () => { - it('sets the link opacity to baseOpacity and background links to 0.2', () => { - liveLink().trigger('mouseover'); - expect(liveLink().attributes('style')).toBe(strokeOpacity(highlightIn)); - expect(otherLink().attributes('style')).toBe(strokeOpacity(highlightOut)); - }); - - it('reverts the styles on mouseout', () => { - liveLink().trigger('mouseover'); - liveLink().trigger('mouseout'); - expect(liveLink().attributes('style')).toBe(strokeOpacity(baseOpacity())); - expect(otherLink().attributes('style')).toBe(strokeOpacity(baseOpacity())); - }); - }); - - describe('on click', () => { - describe('toggles link liveness', () => { - it('turns link on', () => { - liveLink().trigger('click'); - expect(liveLink().attributes('style')).toBe(strokeOpacity(highlightIn)); - expect(otherLink().attributes('style')).toBe(strokeOpacity(highlightOut)); - }); - - it('turns link off on second click', () => { - liveLink().trigger('click'); - liveLink().trigger('click'); - expect(liveLink().attributes('style')).toBe(strokeOpacity(baseOpacity())); - expect(otherLink().attributes('style')).toBe(strokeOpacity(baseOpacity())); - }); - }); - - it('the link remains live even after mouseout', () => { - liveLink().trigger('click'); - liveLink().trigger('mouseout'); - expect(liveLink().attributes('style')).toBe(strokeOpacity(highlightIn)); - expect(otherLink().attributes('style')).toBe(strokeOpacity(highlightOut)); - }); - - it('preserves state when multiple links are toggled on and off', () => { - const anotherLiveLink = () => getAllLinks().at(2); - - liveLink().trigger('click'); - anotherLiveLink().trigger('click'); - expect(liveLink().attributes('style')).toBe(strokeOpacity(highlightIn)); - expect(anotherLiveLink().attributes('style')).toBe(strokeOpacity(highlightIn)); - expect(otherLink().attributes('style')).toBe(strokeOpacity(highlightOut)); - - anotherLiveLink().trigger('click'); - expect(liveLink().attributes('style')).toBe(strokeOpacity(highlightIn)); - expect(anotherLiveLink().attributes('style')).toBe(strokeOpacity(highlightOut)); - expect(otherLink().attributes('style')).toBe(strokeOpacity(highlightOut)); - - liveLink().trigger('click'); - expect(liveLink().attributes('style')).toBe(strokeOpacity(baseOpacity())); - expect(anotherLiveLink().attributes('style')).toBe(strokeOpacity(baseOpacity())); - expect(otherLink().attributes('style')).toBe(strokeOpacity(baseOpacity())); - }); - }); - }); - - describe('nodes', () => { - const liveNode = () => getAllNodes().at(10); - const anotherLiveNode = () => getAllNodes().at(5); - const nodesNotHighlighted = () => getAllNodes().filter((n) => !n.classes(IS_HIGHLIGHTED)); - const linksNotHighlighted = () => getAllLinks().filter((n) => !n.classes(IS_HIGHLIGHTED)); - const nodesHighlighted = () => getAllNodes().filter((n) => n.classes(IS_HIGHLIGHTED)); - const linksHighlighted = () => getAllLinks().filter((n) => n.classes(IS_HIGHLIGHTED)); - - describe('on click', () => { - it('highlights the clicked node and predecessors', () => { - liveNode().trigger('click'); - - expect(nodesNotHighlighted().length < getAllNodes().length).toBe(true); - expect(linksNotHighlighted().length < getAllLinks().length).toBe(true); - - linksHighlighted().wrappers.forEach((link) => { - expect(link.attributes('style')).toBe(strokeOpacity(highlightIn)); - }); - - nodesHighlighted().wrappers.forEach((node) => { - expect(node.attributes('stroke')).not.toBe('#f2f2f2'); - }); - - linksNotHighlighted().wrappers.forEach((link) => { - expect(link.attributes('style')).toBe(strokeOpacity(highlightOut)); - }); - - nodesNotHighlighted().wrappers.forEach((node) => { - expect(node.attributes('stroke')).toBe('#f2f2f2'); - }); - }); - - it('toggles path off on second click', () => { - liveNode().trigger('click'); - liveNode().trigger('click'); - - expect(nodesNotHighlighted().length).toBe(getAllNodes().length); - expect(linksNotHighlighted().length).toBe(getAllLinks().length); - }); - - it('preserves state when multiple nodes are toggled on and off', () => { - anotherLiveNode().trigger('click'); - liveNode().trigger('click'); - anotherLiveNode().trigger('click'); - expect(nodesNotHighlighted().length < getAllNodes().length).toBe(true); - expect(linksNotHighlighted().length < getAllLinks().length).toBe(true); - }); - }); - }); - }); -}); diff --git a/spec/frontend/pipelines/components/dag/dag_spec.js b/spec/frontend/pipelines/components/dag/dag_spec.js deleted file mode 100644 index 53719065611..00000000000 --- a/spec/frontend/pipelines/components/dag/dag_spec.js +++ /dev/null @@ -1,168 +0,0 @@ -import { GlAlert, GlEmptyState } from '@gitlab/ui'; -import { mount, shallowMount } from '@vue/test-utils'; -import { nextTick } from 'vue'; -import { ADD_NOTE, REMOVE_NOTE, REPLACE_NOTES } from '~/pipelines/components/dag/constants'; -import Dag from '~/pipelines/components/dag/dag.vue'; -import DagAnnotations from '~/pipelines/components/dag/dag_annotations.vue'; -import DagGraph from '~/pipelines/components/dag/dag_graph.vue'; - -import { PARSE_FAILURE, UNSUPPORTED_DATA } from '~/pipelines/constants'; -import { - mockParsedGraphQLNodes, - tooSmallGraph, - unparseableGraph, - graphWithoutDependencies, - singleNote, - multiNote, -} from './mock_data'; - -describe('Pipeline DAG graph wrapper', () => { - let wrapper; - const getAlert = () => wrapper.findComponent(GlAlert); - const getAllAlerts = () => wrapper.findAllComponents(GlAlert); - const getGraph = () => wrapper.findComponent(DagGraph); - const getNotes = () => wrapper.findComponent(DagAnnotations); - const getErrorText = (type) => wrapper.vm.$options.errorTexts[type]; - const getEmptyState = () => wrapper.findComponent(GlEmptyState); - - const createComponent = ({ - graphData = mockParsedGraphQLNodes, - provideOverride = {}, - method = shallowMount, - } = {}) => { - wrapper = method(Dag, { - provide: { - pipelineProjectPath: 'root/abc-dag', - pipelineIid: '1', - emptySvgPath: '/my-svg', - dagDocPath: '/my-doc', - ...provideOverride, - }, - data() { - return { - graphData, - showFailureAlert: false, - }; - }, - }); - }; - - describe('when a query argument is undefined', () => { - beforeEach(() => { - createComponent({ - provideOverride: { pipelineProjectPath: undefined }, - graphData: null, - }); - }); - - it('does not render the graph', () => { - expect(getGraph().exists()).toBe(false); - }); - - it('does not render the empty state', () => { - expect(getEmptyState().exists()).toBe(false); - }); - }); - - describe('when all query variables are defined', () => { - describe('but the parse fails', () => { - beforeEach(() => { - createComponent({ - graphData: unparseableGraph, - }); - }); - - it('shows the PARSE_FAILURE alert and not the graph', () => { - expect(getAlert().exists()).toBe(true); - expect(getAlert().text()).toBe(getErrorText(PARSE_FAILURE)); - expect(getGraph().exists()).toBe(false); - }); - - it('does not render the empty state', () => { - expect(getEmptyState().exists()).toBe(false); - }); - }); - - describe('parse succeeds', () => { - beforeEach(() => { - createComponent({ method: mount }); - }); - - it('shows the graph', () => { - expect(getGraph().exists()).toBe(true); - }); - - it('does not render the empty state', () => { - expect(getEmptyState().exists()).toBe(false); - }); - }); - - describe('parse succeeds, but the resulting graph is too small', () => { - beforeEach(() => { - createComponent({ - graphData: tooSmallGraph, - }); - }); - - it('shows the UNSUPPORTED_DATA alert and not the graph', () => { - expect(getAlert().exists()).toBe(true); - expect(getAlert().text()).toBe(getErrorText(UNSUPPORTED_DATA)); - expect(getGraph().exists()).toBe(false); - }); - - it('does not show the empty dag graph state', () => { - expect(getEmptyState().exists()).toBe(false); - }); - }); - - describe('the returned data is empty', () => { - beforeEach(() => { - createComponent({ - method: mount, - graphData: graphWithoutDependencies, - }); - }); - - it('does not render an error alert or the graph', () => { - expect(getAllAlerts().length).toBe(0); - expect(getGraph().exists()).toBe(false); - }); - - it('shows the empty dag graph state', () => { - expect(getEmptyState().exists()).toBe(true); - }); - }); - }); - - describe('annotations', () => { - beforeEach(() => { - createComponent(); - }); - - it('toggles on link mouseover and mouseout', async () => { - const currentNote = singleNote['dag-link103']; - - expect(getNotes().exists()).toBe(false); - - getGraph().vm.$emit('update-annotation', { type: ADD_NOTE, data: currentNote }); - await nextTick(); - expect(getNotes().exists()).toBe(true); - - getGraph().vm.$emit('update-annotation', { type: REMOVE_NOTE, data: currentNote }); - await nextTick(); - expect(getNotes().exists()).toBe(false); - }); - - it('toggles on node and link click', async () => { - expect(getNotes().exists()).toBe(false); - - getGraph().vm.$emit('update-annotation', { type: REPLACE_NOTES, data: multiNote }); - await nextTick(); - expect(getNotes().exists()).toBe(true); - - getGraph().vm.$emit('update-annotation', { type: REPLACE_NOTES, data: {} }); - await nextTick(); - expect(getNotes().exists()).toBe(false); - }); - }); -}); diff --git a/spec/frontend/pipelines/components/dag/drawing_utils_spec.js b/spec/frontend/pipelines/components/dag/drawing_utils_spec.js deleted file mode 100644 index 095ded01298..00000000000 --- a/spec/frontend/pipelines/components/dag/drawing_utils_spec.js +++ /dev/null @@ -1,57 +0,0 @@ -import { createSankey } from '~/pipelines/components/dag/drawing_utils'; -import { parseData } from '~/pipelines/components/parsing_utils'; -import { mockParsedGraphQLNodes } from './mock_data'; - -describe('DAG visualization drawing utilities', () => { - const parsed = parseData(mockParsedGraphQLNodes); - - const layoutSettings = { - width: 200, - height: 200, - nodeWidth: 10, - nodePadding: 20, - paddingForLabels: 100, - }; - - const sankeyLayout = createSankey(layoutSettings)(parsed); - - describe('createSankey', () => { - it('returns a nodes data structure with expected d3-added properties', () => { - const exampleNode = sankeyLayout.nodes[0]; - expect(exampleNode).toHaveProperty('sourceLinks'); - expect(exampleNode).toHaveProperty('targetLinks'); - expect(exampleNode).toHaveProperty('depth'); - expect(exampleNode).toHaveProperty('layer'); - expect(exampleNode).toHaveProperty('x0'); - expect(exampleNode).toHaveProperty('x1'); - expect(exampleNode).toHaveProperty('y0'); - expect(exampleNode).toHaveProperty('y1'); - }); - - it('returns a links data structure with expected d3-added properties', () => { - const exampleLink = sankeyLayout.links[0]; - expect(exampleLink).toHaveProperty('source'); - expect(exampleLink).toHaveProperty('target'); - expect(exampleLink).toHaveProperty('width'); - expect(exampleLink).toHaveProperty('y0'); - expect(exampleLink).toHaveProperty('y1'); - }); - - describe('data structure integrity', () => { - const newObject = { name: 'bad-actor' }; - - beforeEach(() => { - sankeyLayout.nodes.unshift(newObject); - }); - - it('sankey does not propagate changes back to the original', () => { - expect(sankeyLayout.nodes[0]).toBe(newObject); - expect(parsed.nodes[0]).not.toBe(newObject); - }); - - afterEach(() => { - sankeyLayout.nodes.shift(); - }); - }); - }); -}); diff --git a/spec/frontend/pipelines/components/dag/mock_data.js b/spec/frontend/pipelines/components/dag/mock_data.js deleted file mode 100644 index f27e7cf3d6b..00000000000 --- a/spec/frontend/pipelines/components/dag/mock_data.js +++ /dev/null @@ -1,674 +0,0 @@ -export const tooSmallGraph = [ - { - category: 'test', - name: 'jest', - size: 2, - jobs: [{ name: 'jest 1/2' }, { name: 'jest 2/2' }], - }, - { - category: 'test', - name: 'rspec', - size: 1, - jobs: [{ name: 'rspec', needs: ['frontend fixtures'] }], - }, - { - category: 'fixtures', - name: 'frontend fixtures', - size: 1, - jobs: [{ name: 'frontend fixtures' }], - }, - { - category: 'un-needed', - name: 'un-needed', - size: 1, - jobs: [{ name: 'un-needed' }], - }, -]; - -export const graphWithoutDependencies = [ - { - category: 'test', - name: 'jest', - size: 2, - jobs: [{ name: 'jest 1/2' }, { name: 'jest 2/2' }], - }, - { - category: 'test', - name: 'rspec', - size: 1, - jobs: [{ name: 'rspec' }], - }, - { - category: 'fixtures', - name: 'frontend fixtures', - size: 1, - jobs: [{ name: 'frontend fixtures' }], - }, - { - category: 'un-needed', - name: 'un-needed', - size: 1, - jobs: [{ name: 'un-needed' }], - }, -]; - -export const unparseableGraph = [ - { - name: 'test', - groups: [ - { - name: 'jest', - size: 2, - jobs: [{ name: 'jest 1/2', needs: ['frontend fixtures'] }, { name: 'jest 2/2' }], - }, - { - name: 'rspec', - size: 1, - jobs: [{ name: 'rspec', needs: ['frontend fixtures'] }], - }, - ], - }, - { - name: 'un-needed', - groups: [ - { - name: 'un-needed', - size: 1, - jobs: [{ name: 'un-needed' }], - }, - ], - }, -]; - -/* - This represents data that has been parsed by the wrapper -*/ -export const parsedData = { - nodes: [ - { - name: 'build_a', - size: 1, - jobs: [ - { - name: 'build_a', - }, - ], - category: 'build', - }, - { - name: 'build_b', - size: 1, - jobs: [ - { - name: 'build_b', - }, - ], - category: 'build', - }, - { - name: 'test_a', - size: 1, - jobs: [ - { - name: 'test_a', - needs: ['build_a'], - }, - ], - category: 'test', - }, - { - name: 'test_b', - size: 1, - jobs: [ - { - name: 'test_b', - }, - ], - category: 'test', - }, - { - name: 'test_c', - size: 1, - jobs: [ - { - name: 'test_c', - }, - ], - category: 'test', - }, - { - name: 'test_d', - size: 1, - jobs: [ - { - name: 'test_d', - }, - ], - category: 'test', - }, - { - name: 'post_test_a', - size: 1, - jobs: [ - { - name: 'post_test_a', - }, - ], - category: 'post-test', - }, - { - name: 'post_test_b', - size: 1, - jobs: [ - { - name: 'post_test_b', - }, - ], - category: 'post-test', - }, - { - name: 'post_test_c', - size: 1, - jobs: [ - { - name: 'post_test_c', - needs: ['test_a', 'test_b'], - }, - ], - category: 'post-test', - }, - { - name: 'staging_a', - size: 1, - jobs: [ - { - name: 'staging_a', - needs: ['post_test_a'], - }, - ], - category: 'staging', - }, - { - name: 'staging_b', - size: 1, - jobs: [ - { - name: 'staging_b', - needs: ['post_test_b'], - }, - ], - category: 'staging', - }, - { - name: 'staging_c', - size: 1, - jobs: [ - { - name: 'staging_c', - }, - ], - category: 'staging', - }, - { - name: 'staging_d', - size: 1, - jobs: [ - { - name: 'staging_d', - }, - ], - category: 'staging', - }, - { - name: 'staging_e', - size: 1, - jobs: [ - { - name: 'staging_e', - }, - ], - category: 'staging', - }, - { - name: 'canary_a', - size: 1, - jobs: [ - { - name: 'canary_a', - needs: ['staging_a', 'staging_b'], - }, - ], - category: 'canary', - }, - { - name: 'canary_b', - size: 1, - jobs: [ - { - name: 'canary_b', - }, - ], - category: 'canary', - }, - { - name: 'canary_c', - size: 1, - jobs: [ - { - name: 'canary_c', - needs: ['staging_b'], - }, - ], - category: 'canary', - }, - { - name: 'production_a', - size: 1, - jobs: [ - { - name: 'production_a', - needs: ['canary_a'], - }, - ], - category: 'production', - }, - { - name: 'production_b', - size: 1, - jobs: [ - { - name: 'production_b', - }, - ], - category: 'production', - }, - { - name: 'production_c', - size: 1, - jobs: [ - { - name: 'production_c', - }, - ], - category: 'production', - }, - { - name: 'production_d', - size: 1, - jobs: [ - { - name: 'production_d', - needs: ['canary_c'], - }, - ], - category: 'production', - }, - ], - links: [ - { - source: 'build_a', - target: 'test_a', - value: 10, - }, - { - source: 'test_a', - target: 'post_test_c', - value: 10, - }, - { - source: 'test_b', - target: 'post_test_c', - value: 10, - }, - { - source: 'post_test_a', - target: 'staging_a', - value: 10, - }, - { - source: 'post_test_b', - target: 'staging_b', - value: 10, - }, - { - source: 'staging_a', - target: 'canary_a', - value: 10, - }, - { - source: 'staging_b', - target: 'canary_a', - value: 10, - }, - { - source: 'staging_b', - target: 'canary_c', - value: 10, - }, - { - source: 'canary_a', - target: 'production_a', - value: 10, - }, - { - source: 'canary_c', - target: 'production_d', - value: 10, - }, - ], -}; - -export const singleNote = { - 'dag-link103': { - uid: 'dag-link103', - source: { - name: 'canary_a', - color: '#b31756', - }, - target: { - name: 'production_a', - color: '#b24800', - }, - }, -}; - -export const multiNote = { - ...singleNote, - 'dag-link104': { - uid: 'dag-link104', - source: { - name: 'build_a', - color: '#e17223', - }, - target: { - name: 'test_c', - color: '#006887', - }, - }, - 'dag-link105': { - uid: 'dag-link105', - source: { - name: 'test_c', - color: '#006887', - }, - target: { - name: 'post_test_c', - color: '#3547de', - }, - }, -}; - -export const missingJob = 'missing_job'; - -/* - It is important that the base include parallel jobs - as well as non-parallel jobs with spaces in the name to prevent - us relying on spaces as an indicator. -*/ - -export const mockParsedGraphQLNodes = [ - { - category: 'build', - name: 'build_a', - size: 1, - jobs: [ - { - name: 'build_a', - needs: [], - }, - ], - __typename: 'CiGroup', - }, - { - category: 'build', - name: 'build_b', - size: 1, - jobs: [ - { - name: 'build_b', - needs: [], - }, - ], - __typename: 'CiGroup', - }, - { - category: 'test', - name: 'test_a', - size: 1, - jobs: [ - { - name: 'test_a', - needs: ['build_a'], - }, - ], - __typename: 'CiGroup', - }, - { - category: 'test', - name: 'test_b', - size: 1, - jobs: [ - { - name: 'test_b', - needs: [], - }, - ], - __typename: 'CiGroup', - }, - { - category: 'test', - name: 'test_c', - size: 1, - jobs: [ - { - name: 'test_c', - needs: [], - }, - ], - __typename: 'CiGroup', - }, - { - category: 'test', - name: 'test_d', - size: 1, - jobs: [ - { - name: 'test_d', - needs: [], - }, - ], - __typename: 'CiGroup', - }, - { - category: 'post-test', - name: 'post_test_a', - size: 1, - jobs: [ - { - name: 'post_test_a', - needs: [], - }, - ], - __typename: 'CiGroup', - }, - { - category: 'post-test', - name: 'post_test_b', - size: 1, - jobs: [ - { - name: 'post_test_b', - needs: [], - }, - ], - __typename: 'CiGroup', - }, - { - category: 'post-test', - name: 'post_test_c', - size: 1, - jobs: [ - { - name: 'post_test_c', - needs: ['test_b', 'test_a'], - }, - ], - __typename: 'CiGroup', - }, - { - category: 'staging', - name: 'staging_a', - size: 1, - jobs: [ - { - name: 'staging_a', - needs: ['post_test_a'], - }, - ], - __typename: 'CiGroup', - }, - { - category: 'staging', - name: 'staging_b', - size: 1, - jobs: [ - { - name: 'staging_b', - needs: ['post_test_b'], - }, - ], - __typename: 'CiGroup', - }, - { - category: 'staging', - name: 'staging_c', - size: 1, - jobs: [ - { - name: 'staging_c', - needs: [], - }, - ], - __typename: 'CiGroup', - }, - { - category: 'staging', - name: 'staging_d', - size: 1, - jobs: [ - { - name: 'staging_d', - needs: [], - }, - ], - __typename: 'CiGroup', - }, - { - category: 'staging', - name: 'staging_e', - size: 1, - jobs: [ - { - name: 'staging_e', - needs: [], - }, - ], - __typename: 'CiGroup', - }, - { - category: 'canary', - name: 'canary_a', - size: 1, - jobs: [ - { - name: 'canary_a', - needs: ['staging_b', 'staging_a'], - }, - ], - __typename: 'CiGroup', - }, - { - category: 'canary', - name: 'canary_b', - size: 1, - jobs: [ - { - name: 'canary_b', - needs: [], - }, - ], - __typename: 'CiGroup', - }, - { - category: 'canary', - name: 'canary_c', - size: 1, - jobs: [ - { - name: 'canary_c', - needs: ['staging_b'], - }, - ], - __typename: 'CiGroup', - }, - { - category: 'production', - name: 'production_a', - size: 1, - jobs: [ - { - name: 'production_a', - needs: ['canary_a'], - }, - ], - __typename: 'CiGroup', - }, - { - category: 'production', - name: 'production_b', - size: 1, - jobs: [ - { - name: 'production_b', - needs: [], - }, - ], - __typename: 'CiGroup', - }, - { - category: 'production', - name: 'production_c', - size: 1, - jobs: [ - { - name: 'production_c', - needs: [], - }, - ], - __typename: 'CiGroup', - }, - { - category: 'production', - name: 'production_d', - size: 1, - jobs: [ - { - name: 'production_d', - needs: ['canary_c'], - }, - ], - __typename: 'CiGroup', - }, - { - category: 'production', - name: 'production_e', - size: 1, - jobs: [ - { - name: 'production_e', - needs: [missingJob], - }, - ], - __typename: 'CiGroup', - }, -]; diff --git a/spec/frontend/pipelines/components/jobs/failed_jobs_app_spec.js b/spec/frontend/pipelines/components/jobs/failed_jobs_app_spec.js deleted file mode 100644 index 6a2453704db..00000000000 --- a/spec/frontend/pipelines/components/jobs/failed_jobs_app_spec.js +++ /dev/null @@ -1,80 +0,0 @@ -import { GlLoadingIcon } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; -import Vue from 'vue'; -import VueApollo from 'vue-apollo'; -import createMockApollo from 'helpers/mock_apollo_helper'; -import waitForPromises from 'helpers/wait_for_promises'; -import { createAlert } from '~/alert'; -import FailedJobsApp from '~/pipelines/components/jobs/failed_jobs_app.vue'; -import FailedJobsTable from '~/pipelines/components/jobs/failed_jobs_table.vue'; -import GetFailedJobsQuery from '~/pipelines/graphql/queries/get_failed_jobs.query.graphql'; -import { mockFailedJobsQueryResponse } from '../../mock_data'; - -Vue.use(VueApollo); - -jest.mock('~/alert'); - -describe('Failed Jobs App', () => { - let wrapper; - let resolverSpy; - - const findLoadingSpinner = () => wrapper.findComponent(GlLoadingIcon); - const findJobsTable = () => wrapper.findComponent(FailedJobsTable); - - const createMockApolloProvider = (resolver) => { - const requestHandlers = [[GetFailedJobsQuery, resolver]]; - - return createMockApollo(requestHandlers); - }; - - const createComponent = (resolver) => { - wrapper = shallowMount(FailedJobsApp, { - provide: { - fullPath: 'root/ci-project', - pipelineIid: 1, - }, - apolloProvider: createMockApolloProvider(resolver), - }); - }; - - beforeEach(() => { - resolverSpy = jest.fn().mockResolvedValue(mockFailedJobsQueryResponse); - }); - - describe('loading spinner', () => { - it('displays loading spinner when fetching failed jobs', () => { - createComponent(resolverSpy); - - expect(findLoadingSpinner().exists()).toBe(true); - }); - - it('hides loading spinner after the failed jobs have been fetched', async () => { - createComponent(resolverSpy); - - await waitForPromises(); - - expect(findLoadingSpinner().exists()).toBe(false); - }); - }); - - it('displays the failed jobs table', async () => { - createComponent(resolverSpy); - - await waitForPromises(); - - expect(findJobsTable().exists()).toBe(true); - expect(createAlert).not.toHaveBeenCalled(); - }); - - it('handles query fetch error correctly', async () => { - resolverSpy = jest.fn().mockRejectedValue(new Error('GraphQL error')); - - createComponent(resolverSpy); - - await waitForPromises(); - - expect(createAlert).toHaveBeenCalledWith({ - message: 'There was a problem fetching the failed jobs.', - }); - }); -}); diff --git a/spec/frontend/pipelines/components/jobs/failed_jobs_table_spec.js b/spec/frontend/pipelines/components/jobs/failed_jobs_table_spec.js deleted file mode 100644 index 99a178120cc..00000000000 --- a/spec/frontend/pipelines/components/jobs/failed_jobs_table_spec.js +++ /dev/null @@ -1,141 +0,0 @@ -import { GlButton, GlLink, GlTableLite } from '@gitlab/ui'; -import Vue from 'vue'; -import VueApollo from 'vue-apollo'; -import createMockApollo from 'helpers/mock_apollo_helper'; -import { mockTracking, unmockTracking } from 'helpers/tracking_helper'; -import waitForPromises from 'helpers/wait_for_promises'; -import { mountExtended } from 'helpers/vue_test_utils_helper'; -import { createAlert } from '~/alert'; -import { redirectTo } from '~/lib/utils/url_utility'; // eslint-disable-line import/no-deprecated -import FailedJobsTable from '~/pipelines/components/jobs/failed_jobs_table.vue'; -import RetryFailedJobMutation from '~/pipelines/graphql/mutations/retry_failed_job.mutation.graphql'; -import { TRACKING_CATEGORIES } from '~/pipelines/constants'; -import { - successRetryMutationResponse, - failedRetryMutationResponse, - mockFailedJobsData, - mockFailedJobsDataNoPermission, -} from '../../mock_data'; - -jest.mock('~/alert'); -jest.mock('~/lib/utils/url_utility'); - -Vue.use(VueApollo); - -describe('Failed Jobs Table', () => { - let wrapper; - - const successRetryMutationHandler = jest.fn().mockResolvedValue(successRetryMutationResponse); - const failedRetryMutationHandler = jest.fn().mockResolvedValue(failedRetryMutationResponse); - - const findJobsTable = () => wrapper.findComponent(GlTableLite); - const findRetryButton = () => wrapper.findComponent(GlButton); - const findJobLink = () => wrapper.findComponent(GlLink); - const findJobLog = () => wrapper.findByTestId('job-log'); - const findSummary = (index) => wrapper.findAllByTestId('job-trace-summary').at(index); - const findFirstFailureMessage = () => wrapper.findAllByTestId('job-failure-message').at(0); - - const createMockApolloProvider = (resolver) => { - const requestHandlers = [[RetryFailedJobMutation, resolver]]; - return createMockApollo(requestHandlers); - }; - - const createComponent = (resolver, failedJobsData = mockFailedJobsData) => { - wrapper = mountExtended(FailedJobsTable, { - propsData: { - failedJobs: failedJobsData, - }, - apolloProvider: createMockApolloProvider(resolver), - }); - }; - - it('displays the failed jobs table', () => { - createComponent(); - - expect(findJobsTable().exists()).toBe(true); - }); - - it('displays failed job summary', () => { - createComponent(); - - expect(findSummary(0).text()).toBe('Html Summary'); - }); - - it('displays no job log when no trace', () => { - createComponent(); - - expect(findSummary(1).text()).toBe('No job log'); - }); - - it('displays failure reason', () => { - createComponent(); - - expect(findFirstFailureMessage().text()).toBe('Job failed'); - }); - - it('calls the retry failed job mutation and tracks the click', () => { - const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn); - - createComponent(successRetryMutationHandler); - - findRetryButton().trigger('click'); - - expect(successRetryMutationHandler).toHaveBeenCalledWith({ - id: mockFailedJobsData[0].id, - }); - expect(trackingSpy).toHaveBeenCalledTimes(1); - expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_retry', { - label: TRACKING_CATEGORIES.failed, - }); - - unmockTracking(); - }); - - it('redirects to the new job after the mutation', async () => { - const { - data: { - jobRetry: { job }, - }, - } = successRetryMutationResponse; - - createComponent(successRetryMutationHandler); - - findRetryButton().trigger('click'); - - await waitForPromises(); - - expect(redirectTo).toHaveBeenCalledWith(job.detailedStatus.detailsPath); // eslint-disable-line import/no-deprecated - }); - - it('shows error message if the retry failed job mutation fails', async () => { - createComponent(failedRetryMutationHandler); - - findRetryButton().trigger('click'); - - await waitForPromises(); - - expect(createAlert).toHaveBeenCalledWith({ - message: 'There was a problem retrying the failed job.', - }); - }); - - it('hides the job log and retry button if a user does not have permission', () => { - createComponent([[]], mockFailedJobsDataNoPermission); - - expect(findJobLog().exists()).toBe(false); - expect(findRetryButton().exists()).toBe(false); - }); - - it('displays the job log and retry button if a user has permission', () => { - createComponent(); - - expect(findJobLog().exists()).toBe(true); - expect(findRetryButton().exists()).toBe(true); - }); - - it('job name links to the correct job', () => { - createComponent(); - - expect(findJobLink().attributes('href')).toBe(mockFailedJobsData[0].detailedStatus.detailsPath); - }); -}); diff --git a/spec/frontend/pipelines/components/jobs/jobs_app_spec.js b/spec/frontend/pipelines/components/jobs/jobs_app_spec.js deleted file mode 100644 index 39475788fe2..00000000000 --- a/spec/frontend/pipelines/components/jobs/jobs_app_spec.js +++ /dev/null @@ -1,127 +0,0 @@ -import { GlIntersectionObserver, GlSkeletonLoader, GlLoadingIcon } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; -import Vue from 'vue'; -import VueApollo from 'vue-apollo'; -import createMockApollo from 'helpers/mock_apollo_helper'; -import waitForPromises from 'helpers/wait_for_promises'; -import { createAlert } from '~/alert'; -import JobsApp from '~/pipelines/components/jobs/jobs_app.vue'; -import JobsTable from '~/jobs/components/table/jobs_table.vue'; -import getPipelineJobsQuery from '~/pipelines/graphql/queries/get_pipeline_jobs.query.graphql'; -import { mockPipelineJobsQueryResponse } from '../../mock_data'; - -Vue.use(VueApollo); - -jest.mock('~/alert'); - -describe('Jobs app', () => { - let wrapper; - let resolverSpy; - - const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader); - const findLoadingSpinner = () => wrapper.findComponent(GlLoadingIcon); - const findJobsTable = () => wrapper.findComponent(JobsTable); - - const triggerInfiniteScroll = () => - wrapper.findComponent(GlIntersectionObserver).vm.$emit('appear'); - - const createMockApolloProvider = (resolver) => { - const requestHandlers = [[getPipelineJobsQuery, resolver]]; - - return createMockApollo(requestHandlers); - }; - - const createComponent = (resolver) => { - wrapper = shallowMount(JobsApp, { - provide: { - projectPath: 'root/ci-project', - pipelineIid: 1, - }, - apolloProvider: createMockApolloProvider(resolver), - }); - }; - - beforeEach(() => { - resolverSpy = jest.fn().mockResolvedValue(mockPipelineJobsQueryResponse); - }); - - describe('loading spinner', () => { - const setup = async () => { - createComponent(resolverSpy); - - await waitForPromises(); - - triggerInfiniteScroll(); - }; - - it('displays loading spinner when fetching more jobs', async () => { - await setup(); - - expect(findLoadingSpinner().exists()).toBe(true); - expect(findSkeletonLoader().exists()).toBe(false); - }); - - it('hides loading spinner after jobs have been fetched', async () => { - await setup(); - await waitForPromises(); - - expect(findLoadingSpinner().exists()).toBe(false); - expect(findSkeletonLoader().exists()).toBe(false); - }); - }); - - it('displays the skeleton loader', () => { - createComponent(resolverSpy); - - expect(findSkeletonLoader().exists()).toBe(true); - expect(findJobsTable().exists()).toBe(false); - }); - - it('displays the jobs table', async () => { - createComponent(resolverSpy); - - await waitForPromises(); - - expect(findJobsTable().exists()).toBe(true); - expect(findSkeletonLoader().exists()).toBe(false); - expect(createAlert).not.toHaveBeenCalled(); - }); - - it('handles job fetch error correctly', async () => { - resolverSpy = jest.fn().mockRejectedValue(new Error('GraphQL error')); - - createComponent(resolverSpy); - - await waitForPromises(); - - expect(createAlert).toHaveBeenCalledWith({ - message: 'An error occurred while fetching the pipelines jobs.', - }); - }); - - it('handles infinite scrolling by calling fetchMore', async () => { - createComponent(resolverSpy); - await waitForPromises(); - - triggerInfiniteScroll(); - await waitForPromises(); - - expect(resolverSpy).toHaveBeenCalledWith({ - after: 'eyJpZCI6Ijg0NyJ9', - fullPath: 'root/ci-project', - iid: 1, - }); - }); - - it('does not display skeleton loader again after fetchMore', async () => { - createComponent(resolverSpy); - - expect(findSkeletonLoader().exists()).toBe(true); - await waitForPromises(); - - triggerInfiniteScroll(); - await waitForPromises(); - - expect(findSkeletonLoader().exists()).toBe(false); - }); -}); diff --git a/spec/frontend/pipelines/components/pipeline_mini_graph/job_item_spec.js b/spec/frontend/pipelines/components/pipeline_mini_graph/job_item_spec.js deleted file mode 100644 index b89f27e5c05..00000000000 --- a/spec/frontend/pipelines/components/pipeline_mini_graph/job_item_spec.js +++ /dev/null @@ -1,29 +0,0 @@ -import { shallowMount } from '@vue/test-utils'; -import JobItem from '~/pipelines/components/pipeline_mini_graph/job_item.vue'; - -describe('JobItem', () => { - let wrapper; - - const defaultProps = { - job: { id: '3' }, - }; - - const createComponent = ({ props = {} } = {}) => { - wrapper = shallowMount(JobItem, { - propsData: { - ...defaultProps, - ...props, - }, - }); - }; - - describe('when mounted', () => { - beforeEach(() => { - createComponent(); - }); - - it('renders the received HTML', () => { - expect(wrapper.html()).toContain(defaultProps.job.id); - }); - }); -}); diff --git a/spec/frontend/pipelines/components/pipeline_mini_graph/legacy_pipeline_mini_graph_spec.js b/spec/frontend/pipelines/components/pipeline_mini_graph/legacy_pipeline_mini_graph_spec.js deleted file mode 100644 index 6661bb079d2..00000000000 --- a/spec/frontend/pipelines/components/pipeline_mini_graph/legacy_pipeline_mini_graph_spec.js +++ /dev/null @@ -1,122 +0,0 @@ -import { mount } from '@vue/test-utils'; -import { pipelines } from 'test_fixtures/pipelines/pipelines.json'; -import LegacyPipelineMiniGraph from '~/pipelines/components/pipeline_mini_graph/legacy_pipeline_mini_graph.vue'; -import PipelineStages from '~/pipelines/components/pipeline_mini_graph/pipeline_stages.vue'; -import mockLinkedPipelines from './linked_pipelines_mock_data'; - -const mockStages = pipelines[0].details.stages; - -describe('Legacy Pipeline Mini Graph', () => { - let wrapper; - - const findLegacyPipelineMiniGraph = () => wrapper.findComponent(LegacyPipelineMiniGraph); - const findPipelineStages = () => wrapper.findComponent(PipelineStages); - - const findLinkedPipelineUpstream = () => - wrapper.findComponent('[data-testid="pipeline-mini-graph-upstream"]'); - const findLinkedPipelineDownstream = () => - wrapper.findComponent('[data-testid="pipeline-mini-graph-downstream"]'); - const findDownstreamArrowIcon = () => wrapper.find('[data-testid="downstream-arrow-icon"]'); - const findUpstreamArrowIcon = () => wrapper.find('[data-testid="upstream-arrow-icon"]'); - - const createComponent = (props = {}) => { - wrapper = mount(LegacyPipelineMiniGraph, { - propsData: { - stages: mockStages, - ...props, - }, - }); - }; - - describe('rendered state without upstream or downstream pipelines', () => { - beforeEach(() => { - createComponent(); - }); - - it('should render the pipeline stages', () => { - expect(findPipelineStages().exists()).toBe(true); - }); - - it('should have the correct props', () => { - expect(findLegacyPipelineMiniGraph().props()).toMatchObject({ - downstreamPipelines: [], - isMergeTrain: false, - pipelinePath: '', - stages: expect.any(Array), - updateDropdown: false, - upstreamPipeline: undefined, - }); - }); - - it('should have no linked pipelines', () => { - expect(findLinkedPipelineDownstream().exists()).toBe(false); - expect(findLinkedPipelineUpstream().exists()).toBe(false); - }); - - it('should not render arrow icons', () => { - expect(findUpstreamArrowIcon().exists()).toBe(false); - expect(findDownstreamArrowIcon().exists()).toBe(false); - }); - }); - - describe('rendered state with upstream pipeline', () => { - beforeEach(() => { - createComponent({ - upstreamPipeline: mockLinkedPipelines.triggered_by, - }); - }); - - it('should have the correct props', () => { - expect(findLegacyPipelineMiniGraph().props()).toMatchObject({ - downstreamPipelines: [], - isMergeTrain: false, - pipelinePath: '', - stages: expect.any(Array), - updateDropdown: false, - upstreamPipeline: expect.any(Object), - }); - }); - - it('should render the upstream linked pipelines mini list only', () => { - expect(findLinkedPipelineUpstream().exists()).toBe(true); - expect(findLinkedPipelineDownstream().exists()).toBe(false); - }); - - it('should render an upstream arrow icon only', () => { - expect(findDownstreamArrowIcon().exists()).toBe(false); - expect(findUpstreamArrowIcon().exists()).toBe(true); - expect(findUpstreamArrowIcon().props('name')).toBe('long-arrow'); - }); - }); - - describe('rendered state with downstream pipelines', () => { - beforeEach(() => { - createComponent({ - downstreamPipelines: mockLinkedPipelines.triggered, - pipelinePath: 'my/pipeline/path', - }); - }); - - it('should have the correct props', () => { - expect(findLegacyPipelineMiniGraph().props()).toMatchObject({ - downstreamPipelines: expect.any(Array), - isMergeTrain: false, - pipelinePath: 'my/pipeline/path', - stages: expect.any(Array), - updateDropdown: false, - upstreamPipeline: undefined, - }); - }); - - it('should render the downstream linked pipelines mini list only', () => { - expect(findLinkedPipelineDownstream().exists()).toBe(true); - expect(findLinkedPipelineUpstream().exists()).toBe(false); - }); - - it('should render a downstream arrow icon only', () => { - expect(findUpstreamArrowIcon().exists()).toBe(false); - expect(findDownstreamArrowIcon().exists()).toBe(true); - expect(findDownstreamArrowIcon().props('name')).toBe('long-arrow'); - }); - }); -}); diff --git a/spec/frontend/pipelines/components/pipeline_mini_graph/legacy_pipeline_stage_spec.js b/spec/frontend/pipelines/components/pipeline_mini_graph/legacy_pipeline_stage_spec.js deleted file mode 100644 index 3697eaeea1a..00000000000 --- a/spec/frontend/pipelines/components/pipeline_mini_graph/legacy_pipeline_stage_spec.js +++ /dev/null @@ -1,247 +0,0 @@ -import { GlDropdown } from '@gitlab/ui'; -import { nextTick } from 'vue'; -import { mount } from '@vue/test-utils'; -import MockAdapter from 'axios-mock-adapter'; -import CiIcon from '~/vue_shared/components/ci_icon.vue'; -import axios from '~/lib/utils/axios_utils'; -import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status'; -import LegacyPipelineStage from '~/pipelines/components/pipeline_mini_graph/legacy_pipeline_stage.vue'; -import eventHub from '~/pipelines/event_hub'; -import waitForPromises from 'helpers/wait_for_promises'; -import { stageReply } from '../../mock_data'; - -const dropdownPath = 'path.json'; - -describe('Pipelines stage component', () => { - let wrapper; - let mock; - let glTooltipDirectiveMock; - - const createComponent = (props = {}) => { - glTooltipDirectiveMock = jest.fn(); - wrapper = mount(LegacyPipelineStage, { - attachTo: document.body, - directives: { - GlTooltip: glTooltipDirectiveMock, - }, - propsData: { - stage: { - status: { - group: 'success', - icon: 'status_success', - title: 'success', - }, - dropdown_path: dropdownPath, - }, - updateDropdown: false, - ...props, - }, - }); - }; - - beforeEach(() => { - mock = new MockAdapter(axios); - jest.spyOn(eventHub, '$emit'); - }); - - afterEach(() => { - eventHub.$emit.mockRestore(); - mock.restore(); - // eslint-disable-next-line @gitlab/vtu-no-explicit-wrapper-destroy - wrapper.destroy(); - }); - - const findCiActionBtn = () => wrapper.find('.js-ci-action'); - const findCiIcon = () => wrapper.findComponent(CiIcon); - const findDropdown = () => wrapper.findComponent(GlDropdown); - const findDropdownToggle = () => wrapper.find('button.dropdown-toggle'); - const findDropdownMenu = () => - wrapper.find('[data-testid="mini-pipeline-graph-dropdown-menu-list"]'); - const findDropdownMenuTitle = () => - wrapper.find('[data-testid="pipeline-stage-dropdown-menu-title"]'); - const findMergeTrainWarning = () => wrapper.find('[data-testid="warning-message-merge-trains"]'); - const findLoadingState = () => wrapper.find('[data-testid="pipeline-stage-loading-state"]'); - - const openStageDropdown = async () => { - await findDropdownToggle().trigger('click'); - await waitForPromises(); - await nextTick(); - }; - - describe('loading state', () => { - beforeEach(async () => { - createComponent({ updateDropdown: true }); - - mock.onGet(dropdownPath).reply(HTTP_STATUS_OK, stageReply); - - await openStageDropdown(); - }); - - it('displays loading state while jobs are being fetched', async () => { - jest.runOnlyPendingTimers(); - await nextTick(); - - expect(findLoadingState().exists()).toBe(true); - expect(findLoadingState().text()).toBe(LegacyPipelineStage.i18n.loadingText); - }); - - it('does not display loading state after jobs have been fetched', async () => { - await waitForPromises(); - - expect(findLoadingState().exists()).toBe(false); - }); - }); - - describe('default appearance', () => { - beforeEach(() => { - createComponent(); - }); - - it('sets up the tooltip to not have a show delay animation', () => { - expect(glTooltipDirectiveMock.mock.calls[0][1].modifiers.ds0).toBe(true); - }); - - it('renders a dropdown with the status icon', () => { - expect(findDropdown().exists()).toBe(true); - expect(findDropdownToggle().exists()).toBe(true); - expect(findCiIcon().exists()).toBe(true); - }); - - it('renders a borderless ci-icon', () => { - expect(findCiIcon().exists()).toBe(true); - expect(findCiIcon().props('isBorderless')).toBe(true); - expect(findCiIcon().classes('borderless')).toBe(true); - }); - - it('renders a ci-icon with a custom border class', () => { - expect(findCiIcon().exists()).toBe(true); - expect(findCiIcon().classes('gl-border')).toBe(true); - }); - }); - - describe('when user opens dropdown and stage request is successful', () => { - beforeEach(async () => { - mock.onGet(dropdownPath).reply(HTTP_STATUS_OK, stageReply); - createComponent(); - - await openStageDropdown(); - await jest.runAllTimers(); - await axios.waitForAll(); - }); - - it('renders the received data and emits the correct events', () => { - expect(findDropdownMenu().text()).toContain(stageReply.latest_statuses[0].name); - expect(findDropdownMenuTitle().text()).toContain(stageReply.name); - expect(eventHub.$emit).toHaveBeenCalledWith('clickedDropdown'); - expect(wrapper.emitted('miniGraphStageClick')).toEqual([[]]); - }); - - it('refreshes when updateDropdown is set to true', async () => { - expect(mock.history.get).toHaveLength(1); - - wrapper.setProps({ updateDropdown: true }); - await axios.waitForAll(); - - expect(mock.history.get).toHaveLength(2); - }); - }); - - describe('when user opens dropdown and stage request fails', () => { - it('should close the dropdown', async () => { - mock.onGet(dropdownPath).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR); - createComponent(); - - await openStageDropdown(); - await axios.waitForAll(); - await waitForPromises(); - - expect(findDropdown().classes('show')).toBe(false); - }); - }); - - describe('update endpoint correctly', () => { - beforeEach(async () => { - const copyStage = { ...stageReply }; - copyStage.latest_statuses[0].name = 'this is the updated content'; - mock.onGet('bar.json').reply(HTTP_STATUS_OK, copyStage); - createComponent({ - stage: { - status: { - group: 'running', - icon: 'status_running', - title: 'running', - }, - dropdown_path: 'bar.json', - }, - }); - await axios.waitForAll(); - }); - - it('should update the stage to request the new endpoint provided', async () => { - await openStageDropdown(); - jest.runOnlyPendingTimers(); - await waitForPromises(); - - expect(findDropdownMenu().text()).toContain('this is the updated content'); - }); - }); - - describe('job update in dropdown', () => { - beforeEach(async () => { - mock.onGet(dropdownPath).reply(HTTP_STATUS_OK, stageReply); - mock.onPost(`${stageReply.latest_statuses[0].status.action.path}.json`).reply(HTTP_STATUS_OK); - - createComponent(); - await waitForPromises(); - await nextTick(); - }); - - const clickCiAction = async () => { - await openStageDropdown(); - jest.runOnlyPendingTimers(); - await waitForPromises(); - - await findCiActionBtn().trigger('click'); - }; - - it('keeps dropdown open when job item action is clicked', async () => { - await clickCiAction(); - await waitForPromises(); - - expect(findDropdown().classes('show')).toBe(true); - }); - }); - - describe('With merge trains enabled', () => { - it('shows a warning on the dropdown', async () => { - mock.onGet(dropdownPath).reply(HTTP_STATUS_OK, stageReply); - createComponent({ - isMergeTrain: true, - }); - - await openStageDropdown(); - jest.runOnlyPendingTimers(); - await waitForPromises(); - - const warning = findMergeTrainWarning(); - - expect(warning.text()).toBe('Merge train pipeline jobs can not be retried'); - }); - }); - - describe('With merge trains disabled', () => { - beforeEach(async () => { - mock.onGet(dropdownPath).reply(HTTP_STATUS_OK, stageReply); - createComponent(); - - await openStageDropdown(); - await axios.waitForAll(); - }); - - it('does not show a warning on the dropdown', () => { - const warning = findMergeTrainWarning(); - - expect(warning.exists()).toBe(false); - }); - }); -}); diff --git a/spec/frontend/pipelines/components/pipeline_mini_graph/linked_pipelines_mini_list_spec.js b/spec/frontend/pipelines/components/pipeline_mini_graph/linked_pipelines_mini_list_spec.js deleted file mode 100644 index a4ecb9041c9..00000000000 --- a/spec/frontend/pipelines/components/pipeline_mini_graph/linked_pipelines_mini_list_spec.js +++ /dev/null @@ -1,166 +0,0 @@ -import { mount } from '@vue/test-utils'; -import CiIcon from '~/vue_shared/components/ci_icon.vue'; -import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; -import LinkedPipelinesMiniList from '~/pipelines/components/pipeline_mini_graph/linked_pipelines_mini_list.vue'; -import mockData from './linked_pipelines_mock_data'; - -describe('Linked pipeline mini list', () => { - let wrapper; - - const findCiIcon = () => wrapper.findComponent(CiIcon); - const findCiIcons = () => wrapper.findAllComponents(CiIcon); - const findLinkedPipelineCounter = () => wrapper.find('[data-testid="linked-pipeline-counter"]'); - const findLinkedPipelineMiniItem = () => - wrapper.find('[data-testid="linked-pipeline-mini-item"]'); - const findLinkedPipelineMiniItems = () => - wrapper.findAll('[data-testid="linked-pipeline-mini-item"]'); - const findLinkedPipelineMiniList = () => wrapper.findComponent(LinkedPipelinesMiniList); - - const createComponent = (props = {}) => { - wrapper = mount(LinkedPipelinesMiniList, { - directives: { - GlTooltip: createMockDirective('gl-tooltip'), - }, - propsData: { - ...props, - }, - }); - }; - - describe('when passed an upstream pipeline as prop', () => { - beforeEach(() => { - createComponent({ - triggeredBy: [mockData.triggered_by], - }); - }); - - it('should render one linked pipeline item', () => { - expect(findLinkedPipelineMiniItem().exists()).toBe(true); - }); - - it('should render a linked pipeline with the correct href', () => { - expect(findLinkedPipelineMiniItem().exists()).toBe(true); - - expect(findLinkedPipelineMiniItem().attributes('href')).toBe( - '/gitlab-org/gitlab-foss/-/pipelines/129', - ); - }); - - it('should render one ci status icon', () => { - expect(findCiIcon().exists()).toBe(true); - }); - - it('should render a borderless ci-icon', () => { - expect(findCiIcon().exists()).toBe(true); - - expect(findCiIcon().props('isBorderless')).toBe(true); - expect(findCiIcon().classes('borderless')).toBe(true); - }); - - it('should render a ci-icon with a custom border class', () => { - expect(findCiIcon().exists()).toBe(true); - - expect(findCiIcon().classes('gl-border')).toBe(true); - }); - - it('should render the correct ci status icon', () => { - expect(findCiIcon().classes('ci-status-icon-running')).toBe(true); - }); - - it('should have an activated tooltip', () => { - expect(findLinkedPipelineMiniItem().exists()).toBe(true); - const tooltip = getBinding(findLinkedPipelineMiniItem().element, 'gl-tooltip'); - - expect(tooltip.value.title).toBe('GitLabCE - running'); - }); - - it('should correctly set is-upstream', () => { - expect(findLinkedPipelineMiniList().exists()).toBe(true); - - expect(findLinkedPipelineMiniList().classes('is-upstream')).toBe(true); - }); - - it('should correctly compute shouldRenderCounter', () => { - expect(findLinkedPipelineMiniList().vm.shouldRenderCounter).toBe(false); - }); - - it('should not render the pipeline counter', () => { - expect(findLinkedPipelineCounter().exists()).toBe(false); - }); - }); - - describe('when passed downstream pipelines as props', () => { - beforeEach(() => { - createComponent({ - triggered: mockData.triggered, - pipelinePath: 'my/pipeline/path', - }); - }); - - it('should render three linked pipeline items', () => { - expect(findLinkedPipelineMiniItems().exists()).toBe(true); - expect(findLinkedPipelineMiniItems().length).toBe(3); - }); - - it('should render three ci status icons', () => { - expect(findCiIcons().exists()).toBe(true); - expect(findCiIcons().length).toBe(3); - }); - - it('should render the correct ci status icon', () => { - expect(findCiIcon().classes('ci-status-icon-running')).toBe(true); - }); - - it('should have an activated tooltip', () => { - expect(findLinkedPipelineMiniItem().exists()).toBe(true); - const tooltip = getBinding(findLinkedPipelineMiniItem().element, 'gl-tooltip'); - - expect(tooltip.value.title).toBe('GitLabCE - running'); - }); - - it('should correctly set is-downstream', () => { - expect(findLinkedPipelineMiniList().exists()).toBe(true); - - expect(findLinkedPipelineMiniList().classes('is-downstream')).toBe(true); - }); - - it('should render a borderless ci-icon', () => { - expect(findCiIcon().exists()).toBe(true); - - expect(findCiIcon().props('isBorderless')).toBe(true); - expect(findCiIcon().classes('borderless')).toBe(true); - }); - - it('should render a ci-icon with a custom border class', () => { - expect(findCiIcon().exists()).toBe(true); - - expect(findCiIcon().classes('gl-border')).toBe(true); - }); - - it('should render the pipeline counter', () => { - expect(findLinkedPipelineCounter().exists()).toBe(true); - }); - - it('should correctly compute shouldRenderCounter', () => { - expect(findLinkedPipelineMiniList().vm.shouldRenderCounter).toBe(true); - }); - - it('should correctly trim linkedPipelines', () => { - expect(findLinkedPipelineMiniList().props('triggered').length).toBe(6); - expect(findLinkedPipelineMiniList().vm.linkedPipelinesTrimmed.length).toBe(3); - }); - - it('should set the correct pipeline path', () => { - expect(findLinkedPipelineCounter().exists()).toBe(true); - - expect(findLinkedPipelineCounter().attributes('href')).toBe('my/pipeline/path'); - }); - - it('should render the correct counterTooltipText', () => { - expect(findLinkedPipelineCounter().exists()).toBe(true); - const tooltip = getBinding(findLinkedPipelineCounter().element, 'gl-tooltip'); - - expect(tooltip.value.title).toBe(findLinkedPipelineMiniList().vm.counterTooltipText); - }); - }); -}); diff --git a/spec/frontend/pipelines/components/pipeline_mini_graph/linked_pipelines_mock_data.js b/spec/frontend/pipelines/components/pipeline_mini_graph/linked_pipelines_mock_data.js deleted file mode 100644 index 117c7f2ae52..00000000000 --- a/spec/frontend/pipelines/components/pipeline_mini_graph/linked_pipelines_mock_data.js +++ /dev/null @@ -1,407 +0,0 @@ -export default { - triggered_by: { - id: 129, - active: true, - path: '/gitlab-org/gitlab-foss/-/pipelines/129', - project: { - name: 'GitLabCE', - }, - details: { - status: { - icon: 'status_running', - text: 'running', - label: 'running', - group: 'running', - has_details: true, - details_path: '/gitlab-org/gitlab-foss/-/pipelines/129', - favicon: - '/assets/ci_favicons/dev/favicon_status_running-c3ad2fc53ea6079c174e5b6c1351ff349e99ec3af5a5622fb77b0fe53ea279c1.ico', - }, - }, - flags: { - latest: false, - triggered: false, - stuck: false, - yaml_errors: false, - retryable: true, - cancelable: true, - }, - ref: { - name: '7-5-stable', - path: '/gitlab-org/gitlab-foss/commits/7-5-stable', - tag: false, - branch: true, - }, - commit: { - id: '23433d4d8b20d7e45c103d0b6048faad38a130ab', - short_id: '23433d4d', - title: 'Version 7.5.0.rc1', - created_at: '2014-11-17T15:44:14.000+01:00', - parent_ids: ['30ac909f30f58d319b42ed1537664483894b18cd'], - message: 'Version 7.5.0.rc1\n', - author_name: 'Jacob Vosmaer', - author_email: 'contact@jacobvosmaer.nl', - authored_date: '2014-11-17T15:44:14.000+01:00', - committer_name: 'Jacob Vosmaer', - committer_email: 'contact@jacobvosmaer.nl', - committed_date: '2014-11-17T15:44:14.000+01:00', - author_gravatar_url: - 'http://www.gravatar.com/avatar/e66d11c0eedf8c07b3b18fca46599807?s=80&d=identicon', - commit_url: - 'http://localhost:3000/gitlab-org/gitlab-foss/commit/23433d4d8b20d7e45c103d0b6048faad38a130ab', - commit_path: '/gitlab-org/gitlab-foss/commit/23433d4d8b20d7e45c103d0b6048faad38a130ab', - }, - retry_path: '/gitlab-org/gitlab-foss/-/pipelines/129/retry', - cancel_path: '/gitlab-org/gitlab-foss/-/pipelines/129/cancel', - created_at: '2017-05-24T14:46:20.090Z', - updated_at: '2017-05-24T14:46:29.906Z', - }, - triggered: [ - { - id: 132, - active: true, - path: '/gitlab-org/gitlab-foss/-/pipelines/132', - project: { - name: 'GitLabCE', - }, - details: { - status: { - icon: 'status_running', - text: 'running', - label: 'running', - group: 'running', - has_details: true, - details_path: '/gitlab-org/gitlab-foss/-/pipelines/132', - favicon: - '/assets/ci_favicons/dev/favicon_status_running-c3ad2fc53ea6079c174e5b6c1351ff349e99ec3af5a5622fb77b0fe53ea279c1.ico', - }, - }, - flags: { - latest: false, - triggered: false, - stuck: false, - yaml_errors: false, - retryable: true, - cancelable: true, - }, - ref: { - name: 'crowd', - path: '/gitlab-org/gitlab-foss/commits/crowd', - tag: false, - branch: true, - }, - commit: { - id: 'b9d58c4cecd06be74c3cc32ccfb522b31544ab2e', - short_id: 'b9d58c4c', - title: 'getting user keys publically through http without any authentication, the github…', - created_at: '2013-10-03T12:50:33.000+05:30', - parent_ids: ['e219cf7246c6a0495e4507deaffeba11e79f13b8'], - message: - 'getting user keys publically through http without any authentication, the github way. E.g: http://github.com/devaroop.keys\n\nchangelog updated to include ssh key retrieval feature update\n', - author_name: 'devaroop', - author_email: 'devaroop123@yahoo.co.in', - authored_date: '2013-10-02T20:39:29.000+05:30', - committer_name: 'devaroop', - committer_email: 'devaroop123@yahoo.co.in', - committed_date: '2013-10-03T12:50:33.000+05:30', - author_gravatar_url: - 'http://www.gravatar.com/avatar/35df4b155ec66a3127d53459941cf8a2?s=80&d=identicon', - commit_url: - 'http://localhost:3000/gitlab-org/gitlab-foss/commit/b9d58c4cecd06be74c3cc32ccfb522b31544ab2e', - commit_path: '/gitlab-org/gitlab-foss/commit/b9d58c4cecd06be74c3cc32ccfb522b31544ab2e', - }, - retry_path: '/gitlab-org/gitlab-foss/-/pipelines/132/retry', - cancel_path: '/gitlab-org/gitlab-foss/-/pipelines/132/cancel', - created_at: '2017-05-24T14:46:24.644Z', - updated_at: '2017-05-24T14:48:55.226Z', - }, - { - id: 133, - active: true, - path: '/gitlab-org/gitlab-foss/-/pipelines/133', - project: { - name: 'GitLabCE', - }, - details: { - status: { - icon: 'status_running', - text: 'running', - label: 'running', - group: 'running', - has_details: true, - details_path: '/gitlab-org/gitlab-foss/-/pipelines/133', - favicon: - '/assets/ci_favicons/dev/favicon_status_running-c3ad2fc53ea6079c174e5b6c1351ff349e99ec3af5a5622fb77b0fe53ea279c1.ico', - }, - }, - flags: { - latest: false, - triggered: false, - stuck: false, - yaml_errors: false, - retryable: true, - cancelable: true, - }, - ref: { - name: 'crowd', - path: '/gitlab-org/gitlab-foss/commits/crowd', - tag: false, - branch: true, - }, - commit: { - id: 'b6bd4856a33df3d144be66c4ed1f1396009bb08b', - short_id: 'b6bd4856', - title: 'getting user keys publically through http without any authentication, the github…', - created_at: '2013-10-02T20:39:29.000+05:30', - parent_ids: ['e219cf7246c6a0495e4507deaffeba11e79f13b8'], - message: - 'getting user keys publically through http without any authentication, the github way. E.g: http://github.com/devaroop.keys\n', - author_name: 'devaroop', - author_email: 'devaroop123@yahoo.co.in', - authored_date: '2013-10-02T20:39:29.000+05:30', - committer_name: 'devaroop', - committer_email: 'devaroop123@yahoo.co.in', - committed_date: '2013-10-02T20:39:29.000+05:30', - author_gravatar_url: - 'http://www.gravatar.com/avatar/35df4b155ec66a3127d53459941cf8a2?s=80&d=identicon', - commit_url: - 'http://localhost:3000/gitlab-org/gitlab-foss/commit/b6bd4856a33df3d144be66c4ed1f1396009bb08b', - commit_path: '/gitlab-org/gitlab-foss/commit/b6bd4856a33df3d144be66c4ed1f1396009bb08b', - }, - retry_path: '/gitlab-org/gitlab-foss/-/pipelines/133/retry', - cancel_path: '/gitlab-org/gitlab-foss/-/pipelines/133/cancel', - created_at: '2017-05-24T14:46:24.648Z', - updated_at: '2017-05-24T14:48:59.673Z', - }, - { - id: 130, - active: true, - path: '/gitlab-org/gitlab-foss/-/pipelines/130', - project: { - name: 'GitLabCE', - }, - details: { - status: { - icon: 'status_running', - text: 'running', - label: 'running', - group: 'running', - has_details: true, - details_path: '/gitlab-org/gitlab-foss/-/pipelines/130', - favicon: - '/assets/ci_favicons/dev/favicon_status_running-c3ad2fc53ea6079c174e5b6c1351ff349e99ec3af5a5622fb77b0fe53ea279c1.ico', - }, - }, - flags: { - latest: false, - triggered: false, - stuck: false, - yaml_errors: false, - retryable: true, - cancelable: true, - }, - ref: { - name: 'crowd', - path: '/gitlab-org/gitlab-foss/commits/crowd', - tag: false, - branch: true, - }, - commit: { - id: '6d7ced4a2311eeff037c5575cca1868a6d3f586f', - short_id: '6d7ced4a', - title: 'Whitespace fixes to patch', - created_at: '2013-10-08T13:53:22.000-05:00', - parent_ids: ['1875141a963a4238bda29011d8f7105839485253'], - message: 'Whitespace fixes to patch\n', - author_name: 'Dale Hamel', - author_email: 'dale.hamel@srvthe.net', - authored_date: '2013-10-08T13:53:22.000-05:00', - committer_name: 'Dale Hamel', - committer_email: 'dale.hamel@invenia.ca', - committed_date: '2013-10-08T13:53:22.000-05:00', - author_gravatar_url: - 'http://www.gravatar.com/avatar/cd08930e69fa5ad1a669206e7bafe476?s=80&d=identicon', - commit_url: - 'http://localhost:3000/gitlab-org/gitlab-foss/commit/6d7ced4a2311eeff037c5575cca1868a6d3f586f', - commit_path: '/gitlab-org/gitlab-foss/commit/6d7ced4a2311eeff037c5575cca1868a6d3f586f', - }, - retry_path: '/gitlab-org/gitlab-foss/-/pipelines/130/retry', - cancel_path: '/gitlab-org/gitlab-foss/-/pipelines/130/cancel', - created_at: '2017-05-24T14:46:24.630Z', - updated_at: '2017-05-24T14:49:45.091Z', - }, - { - id: 131, - active: true, - path: '/gitlab-org/gitlab-foss/-/pipelines/132', - project: { - name: 'GitLabCE', - }, - details: { - status: { - icon: 'status_running', - text: 'running', - label: 'running', - group: 'running', - has_details: true, - details_path: '/gitlab-org/gitlab-foss/-/pipelines/132', - favicon: - '/assets/ci_favicons/dev/favicon_status_running-c3ad2fc53ea6079c174e5b6c1351ff349e99ec3af5a5622fb77b0fe53ea279c1.ico', - }, - }, - flags: { - latest: false, - triggered: false, - stuck: false, - yaml_errors: false, - retryable: true, - cancelable: true, - }, - ref: { - name: 'crowd', - path: '/gitlab-org/gitlab-foss/commits/crowd', - tag: false, - branch: true, - }, - commit: { - id: 'b9d58c4cecd06be74c3cc32ccfb522b31544ab2e', - short_id: 'b9d58c4c', - title: 'getting user keys publically through http without any authentication, the github…', - created_at: '2013-10-03T12:50:33.000+05:30', - parent_ids: ['e219cf7246c6a0495e4507deaffeba11e79f13b8'], - message: - 'getting user keys publically through http without any authentication, the github way. E.g: http://github.com/devaroop.keys\n\nchangelog updated to include ssh key retrieval feature update\n', - author_name: 'devaroop', - author_email: 'devaroop123@yahoo.co.in', - authored_date: '2013-10-02T20:39:29.000+05:30', - committer_name: 'devaroop', - committer_email: 'devaroop123@yahoo.co.in', - committed_date: '2013-10-03T12:50:33.000+05:30', - author_gravatar_url: - 'http://www.gravatar.com/avatar/35df4b155ec66a3127d53459941cf8a2?s=80&d=identicon', - commit_url: - 'http://localhost:3000/gitlab-org/gitlab-foss/commit/b9d58c4cecd06be74c3cc32ccfb522b31544ab2e', - commit_path: '/gitlab-org/gitlab-foss/commit/b9d58c4cecd06be74c3cc32ccfb522b31544ab2e', - }, - retry_path: '/gitlab-org/gitlab-foss/-/pipelines/132/retry', - cancel_path: '/gitlab-org/gitlab-foss/-/pipelines/132/cancel', - created_at: '2017-05-24T14:46:24.644Z', - updated_at: '2017-05-24T14:48:55.226Z', - }, - { - id: 134, - active: true, - path: '/gitlab-org/gitlab-foss/-/pipelines/133', - project: { - name: 'GitLabCE', - }, - details: { - status: { - icon: 'status_running', - text: 'running', - label: 'running', - group: 'running', - has_details: true, - details_path: '/gitlab-org/gitlab-foss/-/pipelines/133', - favicon: - '/assets/ci_favicons/dev/favicon_status_running-c3ad2fc53ea6079c174e5b6c1351ff349e99ec3af5a5622fb77b0fe53ea279c1.ico', - }, - }, - flags: { - latest: false, - triggered: false, - stuck: false, - yaml_errors: false, - retryable: true, - cancelable: true, - }, - ref: { - name: 'crowd', - path: '/gitlab-org/gitlab-foss/commits/crowd', - tag: false, - branch: true, - }, - commit: { - id: 'b6bd4856a33df3d144be66c4ed1f1396009bb08b', - short_id: 'b6bd4856', - title: 'getting user keys publically through http without any authentication, the github…', - created_at: '2013-10-02T20:39:29.000+05:30', - parent_ids: ['e219cf7246c6a0495e4507deaffeba11e79f13b8'], - message: - 'getting user keys publically through http without any authentication, the github way. E.g: http://github.com/devaroop.keys\n', - author_name: 'devaroop', - author_email: 'devaroop123@yahoo.co.in', - authored_date: '2013-10-02T20:39:29.000+05:30', - committer_name: 'devaroop', - committer_email: 'devaroop123@yahoo.co.in', - committed_date: '2013-10-02T20:39:29.000+05:30', - author_gravatar_url: - 'http://www.gravatar.com/avatar/35df4b155ec66a3127d53459941cf8a2?s=80&d=identicon', - commit_url: - 'http://localhost:3000/gitlab-org/gitlab-foss/commit/b6bd4856a33df3d144be66c4ed1f1396009bb08b', - commit_path: '/gitlab-org/gitlab-foss/commit/b6bd4856a33df3d144be66c4ed1f1396009bb08b', - }, - retry_path: '/gitlab-org/gitlab-foss/-/pipelines/133/retry', - cancel_path: '/gitlab-org/gitlab-foss/-/pipelines/133/cancel', - created_at: '2017-05-24T14:46:24.648Z', - updated_at: '2017-05-24T14:48:59.673Z', - }, - { - id: 135, - active: true, - path: '/gitlab-org/gitlab-foss/-/pipelines/130', - project: { - name: 'GitLabCE', - }, - details: { - status: { - icon: 'status_running', - text: 'running', - label: 'running', - group: 'running', - has_details: true, - details_path: '/gitlab-org/gitlab-foss/-/pipelines/130', - favicon: - '/assets/ci_favicons/dev/favicon_status_running-c3ad2fc53ea6079c174e5b6c1351ff349e99ec3af5a5622fb77b0fe53ea279c1.ico', - }, - }, - flags: { - latest: false, - triggered: false, - stuck: false, - yaml_errors: false, - retryable: true, - cancelable: true, - }, - ref: { - name: 'crowd', - path: '/gitlab-org/gitlab-foss/commits/crowd', - tag: false, - branch: true, - }, - commit: { - id: '6d7ced4a2311eeff037c5575cca1868a6d3f586f', - short_id: '6d7ced4a', - title: 'Whitespace fixes to patch', - created_at: '2013-10-08T13:53:22.000-05:00', - parent_ids: ['1875141a963a4238bda29011d8f7105839485253'], - message: 'Whitespace fixes to patch\n', - author_name: 'Dale Hamel', - author_email: 'dale.hamel@srvthe.net', - authored_date: '2013-10-08T13:53:22.000-05:00', - committer_name: 'Dale Hamel', - committer_email: 'dale.hamel@invenia.ca', - committed_date: '2013-10-08T13:53:22.000-05:00', - author_gravatar_url: - 'http://www.gravatar.com/avatar/cd08930e69fa5ad1a669206e7bafe476?s=80&d=identicon', - commit_url: - 'http://localhost:3000/gitlab-org/gitlab-foss/commit/6d7ced4a2311eeff037c5575cca1868a6d3f586f', - commit_path: '/gitlab-org/gitlab-foss/commit/6d7ced4a2311eeff037c5575cca1868a6d3f586f', - }, - retry_path: '/gitlab-org/gitlab-foss/-/pipelines/130/retry', - cancel_path: '/gitlab-org/gitlab-foss/-/pipelines/130/cancel', - created_at: '2017-05-24T14:46:24.630Z', - updated_at: '2017-05-24T14:49:45.091Z', - }, - ], -}; diff --git a/spec/frontend/pipelines/components/pipeline_mini_graph/mock_data.js b/spec/frontend/pipelines/components/pipeline_mini_graph/mock_data.js deleted file mode 100644 index 1c13e9eb62b..00000000000 --- a/spec/frontend/pipelines/components/pipeline_mini_graph/mock_data.js +++ /dev/null @@ -1,150 +0,0 @@ -export const mockDownstreamPipelinesGraphql = ({ includeSourceJobRetried = true } = {}) => ({ - nodes: [ - { - id: 'gid://gitlab/Ci::Pipeline/612', - path: '/root/job-log-sections/-/pipelines/612', - project: { - id: 'gid://gitlab/Project/21', - name: 'job-log-sections', - __typename: 'Project', - }, - detailedStatus: { - id: 'success-612-612', - group: 'success', - icon: 'status_success', - label: 'passed', - __typename: 'DetailedStatus', - }, - sourceJob: { - id: 'gid://gitlab/Ci::Bridge/532', - retried: includeSourceJobRetried ? false : null, - }, - __typename: 'Pipeline', - }, - { - id: 'gid://gitlab/Ci::Pipeline/611', - path: '/root/job-log-sections/-/pipelines/611', - project: { - id: 'gid://gitlab/Project/21', - name: 'job-log-sections', - __typename: 'Project', - }, - detailedStatus: { - id: 'success-611-611', - group: 'success', - icon: 'status_success', - label: 'passed', - __typename: 'DetailedStatus', - }, - sourceJob: { - id: 'gid://gitlab/Ci::Bridge/531', - retried: includeSourceJobRetried ? true : null, - }, - __typename: 'Pipeline', - }, - { - id: 'gid://gitlab/Ci::Pipeline/609', - path: '/root/job-log-sections/-/pipelines/609', - project: { - id: 'gid://gitlab/Project/21', - name: 'job-log-sections', - __typename: 'Project', - }, - detailedStatus: { - id: 'success-609-609', - group: 'success', - icon: 'status_success', - label: 'passed', - __typename: 'DetailedStatus', - }, - sourceJob: { - id: 'gid://gitlab/Ci::Bridge/530', - retried: includeSourceJobRetried ? true : null, - }, - __typename: 'Pipeline', - }, - ], - __typename: 'PipelineConnection', -}); - -const upstream = { - id: 'gid://gitlab/Ci::Pipeline/610', - path: '/root/trigger-downstream/-/pipelines/610', - project: { - id: 'gid://gitlab/Project/21', - name: 'trigger-downstream', - __typename: 'Project', - }, - detailedStatus: { - id: 'success-610-610', - group: 'success', - icon: 'status_success', - label: 'passed', - __typename: 'DetailedStatus', - }, - __typename: 'Pipeline', -}; - -export const mockPipelineStagesQueryResponse = { - data: { - project: { - id: 'gid://gitlab/Project/20', - pipeline: { - id: 'gid://gitlab/Ci::Pipeline/320', - stages: { - nodes: [ - { - __typename: 'CiStage', - id: 'gid://gitlab/Ci::Stage/409', - name: 'build', - detailedStatus: { - __typename: 'DetailedStatus', - id: 'success-409-409', - icon: 'status_success', - group: 'success', - }, - }, - ], - }, - }, - }, - }, -}; - -export const mockPipelineStatusResponse = { - data: { - project: { - id: 'gid://gitlab/Project/20', - pipeline: { - id: 'gid://gitlab/Ci::Pipeline/320', - detailedStatus: { - id: 'pending-320-320', - detailsPath: '/root/ci-project/-/pipelines/320', - icon: 'status_pending', - group: 'pending', - __typename: 'DetailedStatus', - }, - __typename: 'Pipeline', - }, - __typename: 'Project', - }, - }, -}; - -export const mockUpstreamDownstreamQueryResponse = { - data: { - project: { - id: '1', - pipeline: { - id: 'pipeline-1', - path: '/root/ci-project/-/pipelines/790', - downstream: mockDownstreamPipelinesGraphql(), - upstream, - }, - __typename: 'Project', - }, - }, -}; - -export const linkedPipelinesFetchError = 'There was a problem fetching linked pipelines.'; -export const stagesFetchError = 'There was a problem fetching the pipeline stages.'; diff --git a/spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_mini_graph_spec.js b/spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_mini_graph_spec.js deleted file mode 100644 index b3e157f75f6..00000000000 --- a/spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_mini_graph_spec.js +++ /dev/null @@ -1,123 +0,0 @@ -import Vue from 'vue'; -import VueApollo from 'vue-apollo'; -import { GlLoadingIcon } from '@gitlab/ui'; - -import { createAlert } from '~/alert'; -import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; -import waitForPromises from 'helpers/wait_for_promises'; -import createMockApollo from 'helpers/mock_apollo_helper'; - -import getLinkedPipelinesQuery from '~/pipelines/graphql/queries/get_linked_pipelines.query.graphql'; -import getPipelineStagesQuery from '~/pipelines/graphql/queries/get_pipeline_stages.query.graphql'; -import LegacyPipelineMiniGraph from '~/pipelines/components/pipeline_mini_graph/legacy_pipeline_mini_graph.vue'; -import PipelineMiniGraph from '~/pipelines/components/pipeline_mini_graph/pipeline_mini_graph.vue'; -import * as sharedGraphQlUtils from '~/graphql_shared/utils'; - -import { - linkedPipelinesFetchError, - stagesFetchError, - mockPipelineStagesQueryResponse, - mockUpstreamDownstreamQueryResponse, -} from './mock_data'; - -Vue.use(VueApollo); -jest.mock('~/alert'); - -describe('PipelineMiniGraph', () => { - let wrapper; - let linkedPipelinesResponse; - let pipelineStagesResponse; - - const fullPath = 'gitlab-org/gitlab'; - const iid = '315'; - const pipelineEtag = '/api/graphql:pipelines/id/315'; - - const createComponent = ({ - pipelineStagesHandler = pipelineStagesResponse, - linkedPipelinesHandler = linkedPipelinesResponse, - } = {}) => { - const handlers = [ - [getLinkedPipelinesQuery, linkedPipelinesHandler], - [getPipelineStagesQuery, pipelineStagesHandler], - ]; - const mockApollo = createMockApollo(handlers); - - wrapper = shallowMountExtended(PipelineMiniGraph, { - propsData: { - fullPath, - iid, - pipelineEtag, - }, - apolloProvider: mockApollo, - }); - - return waitForPromises(); - }; - - const findLegacyPipelineMiniGraph = () => wrapper.findComponent(LegacyPipelineMiniGraph); - const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); - - beforeEach(() => { - linkedPipelinesResponse = jest.fn().mockResolvedValue(mockUpstreamDownstreamQueryResponse); - pipelineStagesResponse = jest.fn().mockResolvedValue(mockPipelineStagesQueryResponse); - }); - - describe('when initial queries are loading', () => { - beforeEach(() => { - createComponent(); - }); - - it('shows a loading icon and no mini graph', () => { - expect(findLoadingIcon().exists()).toBe(true); - expect(findLegacyPipelineMiniGraph().exists()).toBe(false); - }); - }); - - describe('when queries have loaded', () => { - it('does not show a loading icon', async () => { - await createComponent(); - - expect(findLoadingIcon().exists()).toBe(false); - }); - - it('renders the Pipeline Mini Graph', async () => { - await createComponent(); - - expect(findLegacyPipelineMiniGraph().exists()).toBe(true); - }); - - it('fires the queries', async () => { - await createComponent(); - - expect(linkedPipelinesResponse).toHaveBeenCalledWith({ iid, fullPath }); - expect(pipelineStagesResponse).toHaveBeenCalledWith({ iid, fullPath }); - }); - }); - - describe('polling', () => { - it('toggles query polling with visibility check', async () => { - jest.spyOn(sharedGraphQlUtils, 'toggleQueryPollingByVisibility'); - - createComponent(); - - await waitForPromises(); - - expect(sharedGraphQlUtils.toggleQueryPollingByVisibility).toHaveBeenCalledTimes(2); - }); - }); - - describe('when pipeline queries are unsuccessful', () => { - const failedHandler = jest.fn().mockRejectedValue(new Error('GraphQL error')); - it.each` - query | handlerName | errorMessage - ${'pipeline stages'} | ${'pipelineStagesHandler'} | ${stagesFetchError} - ${'linked pipelines'} | ${'linkedPipelinesHandler'} | ${linkedPipelinesFetchError} - `('throws an error for the $query query', async ({ errorMessage, handlerName }) => { - await createComponent({ [handlerName]: failedHandler }); - - await waitForPromises(); - - expect(createAlert).toHaveBeenCalledWith({ message: errorMessage }); - }); - }); -}); diff --git a/spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_stage_spec.js b/spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_stage_spec.js deleted file mode 100644 index 1989aad12b0..00000000000 --- a/spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_stage_spec.js +++ /dev/null @@ -1,46 +0,0 @@ -import Vue from 'vue'; -import VueApollo from 'vue-apollo'; -import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; -import waitForPromises from 'helpers/wait_for_promises'; -import createMockApollo from 'helpers/mock_apollo_helper'; - -import getPipelineStageQuery from '~/pipelines/graphql/queries/get_pipeline_stage.query.graphql'; -import PipelineStage from '~/pipelines/components/pipeline_mini_graph/pipeline_stage.vue'; - -Vue.use(VueApollo); - -describe('PipelineStage', () => { - let wrapper; - let pipelineStageResponse; - - const defaultProps = { - pipelineEtag: '/etag', - stageId: '1', - }; - - const createComponent = ({ pipelineStageHandler = pipelineStageResponse } = {}) => { - const handlers = [[getPipelineStageQuery, pipelineStageHandler]]; - const mockApollo = createMockApollo(handlers); - - wrapper = shallowMountExtended(PipelineStage, { - propsData: { - ...defaultProps, - }, - apolloProvider: mockApollo, - }); - - return waitForPromises(); - }; - - const findPipelineStage = () => wrapper.findComponent(PipelineStage); - - describe('when mounted', () => { - beforeEach(() => { - createComponent(); - }); - - it('renders job item', () => { - expect(findPipelineStage().exists()).toBe(true); - }); - }); -}); diff --git a/spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_stages_spec.js b/spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_stages_spec.js deleted file mode 100644 index c212087b7e3..00000000000 --- a/spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_stages_spec.js +++ /dev/null @@ -1,63 +0,0 @@ -import { shallowMount } from '@vue/test-utils'; -import { pipelines } from 'test_fixtures/pipelines/pipelines.json'; -import LegacyPipelineStage from '~/pipelines/components/pipeline_mini_graph/legacy_pipeline_stage.vue'; -import PipelineStages from '~/pipelines/components/pipeline_mini_graph/pipeline_stages.vue'; - -const mockStages = pipelines[0].details.stages; - -describe('Pipeline Stages', () => { - let wrapper; - - const findLegacyPipelineStages = () => wrapper.findAllComponents(LegacyPipelineStage); - const findPipelineStagesAt = (i) => findLegacyPipelineStages().at(i); - - const createComponent = (props = {}) => { - wrapper = shallowMount(PipelineStages, { - propsData: { - stages: mockStages, - ...props, - }, - }); - }; - - it('renders stages', () => { - createComponent(); - - expect(findLegacyPipelineStages()).toHaveLength(mockStages.length); - }); - - it('does not fail when stages are empty', () => { - createComponent({ stages: [] }); - - expect(wrapper.exists()).toBe(true); - expect(findLegacyPipelineStages()).toHaveLength(0); - }); - - it('update dropdown is false by default', () => { - createComponent(); - - expect(findPipelineStagesAt(0).props('updateDropdown')).toBe(false); - expect(findPipelineStagesAt(1).props('updateDropdown')).toBe(false); - }); - - it('update dropdown is set to true', () => { - createComponent({ updateDropdown: true }); - - expect(findPipelineStagesAt(0).props('updateDropdown')).toBe(true); - expect(findPipelineStagesAt(1).props('updateDropdown')).toBe(true); - }); - - it('is merge train is false by default', () => { - createComponent(); - - expect(findPipelineStagesAt(0).props('isMergeTrain')).toBe(false); - expect(findPipelineStagesAt(1).props('isMergeTrain')).toBe(false); - }); - - it('is merge train is set to true', () => { - createComponent({ isMergeTrain: true }); - - expect(findPipelineStagesAt(0).props('isMergeTrain')).toBe(true); - expect(findPipelineStagesAt(1).props('isMergeTrain')).toBe(true); - }); -}); diff --git a/spec/frontend/pipelines/components/pipeline_tabs_spec.js b/spec/frontend/pipelines/components/pipeline_tabs_spec.js deleted file mode 100644 index 0951e1ffb46..00000000000 --- a/spec/frontend/pipelines/components/pipeline_tabs_spec.js +++ /dev/null @@ -1,114 +0,0 @@ -import { GlTab } from '@gitlab/ui'; -import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; -import { mockTracking, unmockTracking } from 'helpers/tracking_helper'; -import PipelineTabs from '~/pipelines/components/pipeline_tabs.vue'; -import { TRACKING_CATEGORIES } from '~/pipelines/constants'; - -describe('The Pipeline Tabs', () => { - let wrapper; - let trackingSpy; - - const $router = { push: jest.fn() }; - - const findDagTab = () => wrapper.findByTestId('dag-tab'); - const findFailedJobsTab = () => wrapper.findByTestId('failed-jobs-tab'); - const findJobsTab = () => wrapper.findByTestId('jobs-tab'); - const findPipelineTab = () => wrapper.findByTestId('pipeline-tab'); - const findTestsTab = () => wrapper.findByTestId('tests-tab'); - - const findFailedJobsBadge = () => wrapper.findByTestId('failed-builds-counter'); - const findJobsBadge = () => wrapper.findByTestId('builds-counter'); - const findTestsBadge = () => wrapper.findByTestId('tests-counter'); - - const defaultProvide = { - defaultTabValue: '', - failedJobsCount: 1, - totalJobCount: 10, - testsCount: 123, - }; - - const createComponent = (provide = {}) => { - wrapper = shallowMountExtended(PipelineTabs, { - provide: { - ...defaultProvide, - ...provide, - }, - stubs: { - GlTab, - RouterView: true, - }, - mocks: { - $router, - }, - }); - }; - - describe('Tabs', () => { - it.each` - tabName | tabComponent - ${'Pipeline'} | ${findPipelineTab} - ${'Dag'} | ${findDagTab} - ${'Jobs'} | ${findJobsTab} - ${'Failed Jobs'} | ${findFailedJobsTab} - ${'Tests'} | ${findTestsTab} - `('shows $tabName tab', ({ tabComponent }) => { - createComponent(); - - expect(tabComponent().exists()).toBe(true); - }); - - describe('with no failed jobs', () => { - beforeEach(() => { - createComponent({ failedJobsCount: 0 }); - }); - - it('hides the failed jobs tab', () => { - expect(findFailedJobsTab().exists()).toBe(false); - }); - }); - }); - - describe('Tabs badges', () => { - it.each` - tabName | badgeComponent | badgeText - ${'Jobs'} | ${findJobsBadge} | ${String(defaultProvide.totalJobCount)} - ${'Failed Jobs'} | ${findFailedJobsBadge} | ${String(defaultProvide.failedJobsCount)} - ${'Tests'} | ${findTestsBadge} | ${String(defaultProvide.testsCount)} - `('shows badge for $tabName with the correct text', ({ badgeComponent, badgeText }) => { - createComponent(); - - expect(badgeComponent().exists()).toBe(true); - expect(badgeComponent().text()).toBe(badgeText); - }); - }); - - describe('Tab tracking', () => { - beforeEach(() => { - createComponent(); - - trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn); - }); - - afterEach(() => { - unmockTracking(); - }); - - it('tracks failed jobs tab click', () => { - findFailedJobsTab().vm.$emit('click'); - - expect(trackingSpy).toHaveBeenCalledTimes(1); - expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_tab', { - label: TRACKING_CATEGORIES.failed, - }); - }); - - it('tracks tests tab click', () => { - findTestsTab().vm.$emit('click'); - - expect(trackingSpy).toHaveBeenCalledTimes(1); - expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_tab', { - label: TRACKING_CATEGORIES.tests, - }); - }); - }); -}); diff --git a/spec/frontend/pipelines/components/pipelines_filtered_search_spec.js b/spec/frontend/pipelines/components/pipelines_filtered_search_spec.js deleted file mode 100644 index 51a4487a3ef..00000000000 --- a/spec/frontend/pipelines/components/pipelines_filtered_search_spec.js +++ /dev/null @@ -1,199 +0,0 @@ -import { GlFilteredSearch } from '@gitlab/ui'; -import { mount } from '@vue/test-utils'; -import MockAdapter from 'axios-mock-adapter'; -import { nextTick } from 'vue'; -import { mockTracking, unmockTracking } from 'helpers/tracking_helper'; -import Api from '~/api'; -import axios from '~/lib/utils/axios_utils'; -import PipelinesFilteredSearch from '~/pipelines/components/pipelines_list/pipelines_filtered_search.vue'; -import { - FILTERED_SEARCH_TERM, - OPERATORS_IS, -} from '~/vue_shared/components/filtered_search_bar/constants'; -import { TRACKING_CATEGORIES } from '~/pipelines/constants'; -import { users, mockSearch, branches, tags } from '../mock_data'; - -describe('Pipelines filtered search', () => { - let wrapper; - let mock; - - const findFilteredSearch = () => wrapper.findComponent(GlFilteredSearch); - const getSearchToken = (type) => - findFilteredSearch() - .props('availableTokens') - .find((token) => token.type === type); - const findBranchToken = () => getSearchToken('ref'); - const findTagToken = () => getSearchToken('tag'); - const findUserToken = () => getSearchToken('username'); - const findStatusToken = () => getSearchToken('status'); - const findSourceToken = () => getSearchToken('source'); - - const createComponent = (params = {}) => { - wrapper = mount(PipelinesFilteredSearch, { - propsData: { - projectId: '21', - defaultBranchName: 'main', - params, - }, - attachTo: document.body, - }); - }; - - beforeEach(() => { - mock = new MockAdapter(axios); - - jest.spyOn(Api, 'projectUsers').mockResolvedValue(users); - jest.spyOn(Api, 'branches').mockResolvedValue({ data: branches }); - jest.spyOn(Api, 'tags').mockResolvedValue({ data: tags }); - - createComponent(); - }); - - afterEach(() => { - mock.restore(); - }); - - it('displays UI elements', () => { - expect(findFilteredSearch().exists()).toBe(true); - }); - - it('displays search tokens', () => { - expect(findUserToken()).toMatchObject({ - type: 'username', - icon: 'user', - title: 'Trigger author', - unique: true, - projectId: '21', - operators: OPERATORS_IS, - }); - - expect(findBranchToken()).toMatchObject({ - type: 'ref', - icon: 'branch', - title: 'Branch name', - unique: true, - projectId: '21', - defaultBranchName: 'main', - operators: OPERATORS_IS, - }); - - expect(findSourceToken()).toMatchObject({ - type: 'source', - icon: 'trigger-source', - title: 'Source', - unique: true, - operators: OPERATORS_IS, - }); - - expect(findStatusToken()).toMatchObject({ - type: 'status', - icon: 'status', - title: 'Status', - unique: true, - operators: OPERATORS_IS, - }); - - expect(findTagToken()).toMatchObject({ - type: 'tag', - icon: 'tag', - title: 'Tag name', - unique: true, - operators: OPERATORS_IS, - }); - }); - - it('emits filterPipelines on submit with correct filter', () => { - findFilteredSearch().vm.$emit('submit', mockSearch); - - expect(wrapper.emitted('filterPipelines')).toHaveLength(1); - expect(wrapper.emitted('filterPipelines')[0]).toEqual([mockSearch]); - }); - - it('disables tag name token when branch name token is active', async () => { - findFilteredSearch().vm.$emit('input', [ - { type: 'ref', value: { data: 'branch-1', operator: '=' } }, - { type: FILTERED_SEARCH_TERM, value: { data: '' } }, - ]); - - await nextTick(); - expect(findBranchToken().disabled).toBe(false); - expect(findTagToken().disabled).toBe(true); - }); - - it('disables branch name token when tag name token is active', async () => { - findFilteredSearch().vm.$emit('input', [ - { type: 'tag', value: { data: 'tag-1', operator: '=' } }, - { type: FILTERED_SEARCH_TERM, value: { data: '' } }, - ]); - - await nextTick(); - expect(findBranchToken().disabled).toBe(true); - expect(findTagToken().disabled).toBe(false); - }); - - it('resets tokens disabled state on clear', async () => { - findFilteredSearch().vm.$emit('clearInput'); - - await nextTick(); - expect(findBranchToken().disabled).toBe(false); - expect(findTagToken().disabled).toBe(false); - }); - - it('resets tokens disabled state when clearing tokens by backspace', async () => { - findFilteredSearch().vm.$emit('input', [{ type: FILTERED_SEARCH_TERM, value: { data: '' } }]); - - await nextTick(); - expect(findBranchToken().disabled).toBe(false); - expect(findTagToken().disabled).toBe(false); - }); - - describe('Url query params', () => { - const params = { - username: 'deja.green', - ref: 'main', - }; - - beforeEach(() => { - createComponent(params); - }); - - it('sets default value if url query params', () => { - const expectedValueProp = [ - { - type: 'username', - value: { - data: params.username, - operator: '=', - }, - }, - { - type: 'ref', - value: { - data: params.ref, - operator: '=', - }, - }, - { type: FILTERED_SEARCH_TERM, value: { data: '' } }, - ]; - - expect(findFilteredSearch().props('value')).toMatchObject(expectedValueProp); - expect(findFilteredSearch().props('value')).toHaveLength(expectedValueProp.length); - }); - }); - - describe('tracking', () => { - afterEach(() => { - unmockTracking(); - }); - - it('tracks filtered search click', () => { - const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn); - - findFilteredSearch().vm.$emit('submit', mockSearch); - - expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_filtered_search', { - label: TRACKING_CATEGORIES.search, - }); - }); - }); -}); diff --git a/spec/frontend/pipelines/components/pipelines_list/empty_state/ci_templates_spec.js b/spec/frontend/pipelines/components/pipelines_list/empty_state/ci_templates_spec.js deleted file mode 100644 index b560eea4882..00000000000 --- a/spec/frontend/pipelines/components/pipelines_list/empty_state/ci_templates_spec.js +++ /dev/null @@ -1,107 +0,0 @@ -import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; -import { mockTracking, unmockTracking } from 'helpers/tracking_helper'; -import CiTemplates from '~/pipelines/components/pipelines_list/empty_state/ci_templates.vue'; - -const pipelineEditorPath = '/-/ci/editor'; -const suggestedCiTemplates = [ - { name: 'Android', logo: '/assets/illustrations/logos/android.svg' }, - { name: 'Bash', logo: '/assets/illustrations/logos/bash.svg' }, - { name: 'C++', logo: '/assets/illustrations/logos/c_plus_plus.svg' }, -]; - -describe('CI Templates', () => { - let wrapper; - let trackingSpy; - - const createWrapper = (propsData = {}) => { - wrapper = shallowMountExtended(CiTemplates, { - provide: { - pipelineEditorPath, - suggestedCiTemplates, - }, - propsData, - }); - }; - - const findTemplateDescription = () => wrapper.findByTestId('template-description'); - const findTemplateLink = () => wrapper.findByTestId('template-link'); - const findTemplateNames = () => wrapper.findAllByTestId('template-name'); - const findTemplateName = () => wrapper.findByTestId('template-name'); - const findTemplateLogo = () => wrapper.findByTestId('template-logo'); - - describe('renders template list', () => { - beforeEach(() => { - createWrapper(); - }); - - it('renders all suggested templates', () => { - expect(findTemplateNames().length).toBe(3); - expect(wrapper.text()).toContain('Android', 'Bash', 'C++'); - }); - - it('has the correct template name', () => { - expect(findTemplateName().text()).toBe('Android'); - }); - - it('links to the correct template', () => { - expect(findTemplateLink().attributes('href')).toBe( - pipelineEditorPath.concat('?template=Android'), - ); - }); - - it('has the link button enabled', () => { - expect(findTemplateLink().props('disabled')).toBe(false); - }); - - it('has the description of the template', () => { - expect(findTemplateDescription().text()).toBe( - 'Continuous integration and deployment template to test and deploy your Android project.', - ); - }); - - it('has the right logo of the template', () => { - expect(findTemplateLogo().attributes('src')).toBe('/assets/illustrations/logos/android.svg'); - }); - }); - - describe('filtering the templates', () => { - beforeEach(() => { - createWrapper({ filterTemplates: ['Bash'] }); - }); - - it('renders only the filtered templates', () => { - expect(findTemplateNames()).toHaveLength(1); - expect(findTemplateName().text()).toBe('Bash'); - }); - }); - - describe('disabling the templates', () => { - beforeEach(() => { - createWrapper({ disabled: true }); - }); - - it('has the link button disabled', () => { - expect(findTemplateLink().props('disabled')).toBe(true); - }); - }); - - describe('tracking', () => { - beforeEach(() => { - createWrapper(); - trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn); - }); - - afterEach(() => { - unmockTracking(); - }); - - it('sends an event when template is clicked', () => { - findTemplateLink().vm.$emit('click'); - - expect(trackingSpy).toHaveBeenCalledTimes(1); - expect(trackingSpy).toHaveBeenCalledWith(undefined, 'template_clicked', { - label: 'Android', - }); - }); - }); -}); diff --git a/spec/frontend/pipelines/components/pipelines_list/empty_state/ios_templates_spec.js b/spec/frontend/pipelines/components/pipelines_list/empty_state/ios_templates_spec.js deleted file mode 100644 index 700be076e0c..00000000000 --- a/spec/frontend/pipelines/components/pipelines_list/empty_state/ios_templates_spec.js +++ /dev/null @@ -1,133 +0,0 @@ -import '~/commons'; -import { nextTick } from 'vue'; -import { GlPopover, GlButton } from '@gitlab/ui'; -import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; -import RunnerInstructionsModal from '~/vue_shared/components/runner_instructions/runner_instructions_modal.vue'; -import IosTemplates from '~/pipelines/components/pipelines_list/empty_state/ios_templates.vue'; -import CiTemplates from '~/pipelines/components/pipelines_list/empty_state/ci_templates.vue'; - -const pipelineEditorPath = '/-/ci/editor'; -const registrationToken = 'SECRET_TOKEN'; -const iOSTemplateName = 'iOS-Fastlane'; - -describe('iOS Templates', () => { - let wrapper; - - const createWrapper = (providedPropsData = {}) => { - return shallowMountExtended(IosTemplates, { - provide: { - pipelineEditorPath, - iosRunnersAvailable: true, - ...providedPropsData, - }, - propsData: { - registrationToken, - }, - stubs: { - GlButton, - }, - }); - }; - - const findIosTemplate = () => wrapper.findComponent(CiTemplates); - const findRunnerInstructionsModal = () => wrapper.findComponent(RunnerInstructionsModal); - const findRunnerInstructionsPopover = () => wrapper.findComponent(GlPopover); - const findRunnerSetupTodoEmoji = () => wrapper.findByTestId('runner-setup-marked-todo'); - const findRunnerSetupCompletedEmoji = () => wrapper.findByTestId('runner-setup-marked-completed'); - const findSetupRunnerLink = () => wrapper.findByText('Set up a runner'); - const configurePipelineLink = () => wrapper.findByTestId('configure-pipeline-link'); - - describe('when ios runners are not available', () => { - beforeEach(() => { - wrapper = createWrapper({ iosRunnersAvailable: false }); - }); - - describe('the runner setup section', () => { - it('marks the section as todo', () => { - expect(findRunnerSetupTodoEmoji().isVisible()).toBe(true); - expect(findRunnerSetupCompletedEmoji().isVisible()).toBe(false); - }); - - it('renders the setup runner link', () => { - expect(findSetupRunnerLink().exists()).toBe(true); - }); - - it('renders the runner instructions modal with a popover once clicked', async () => { - findSetupRunnerLink().element.parentElement.click(); - - await nextTick(); - - expect(findRunnerInstructionsModal().exists()).toBe(true); - expect(findRunnerInstructionsModal().props('registrationToken')).toBe(registrationToken); - expect(findRunnerInstructionsModal().props('defaultPlatformName')).toBe('osx'); - - findRunnerInstructionsModal().vm.$emit('shown'); - - await nextTick(); - - expect(findRunnerInstructionsPopover().exists()).toBe(true); - }); - }); - - describe('the configure pipeline section', () => { - it('has a disabled link button', () => { - expect(configurePipelineLink().props('disabled')).toBe(true); - }); - }); - - describe('the ios-Fastlane template', () => { - it('renders the template', () => { - expect(findIosTemplate().props('filterTemplates')).toStrictEqual([iOSTemplateName]); - }); - - it('has a disabled link button', () => { - expect(findIosTemplate().props('disabled')).toBe(true); - }); - }); - }); - - describe('when ios runners are available', () => { - beforeEach(() => { - wrapper = createWrapper(); - }); - - describe('the runner setup section', () => { - it('marks the section as completed', () => { - expect(findRunnerSetupTodoEmoji().isVisible()).toBe(false); - expect(findRunnerSetupCompletedEmoji().isVisible()).toBe(true); - }); - - it('does not render the setup runner link', () => { - expect(findSetupRunnerLink().exists()).toBe(false); - }); - }); - - describe('the configure pipeline section', () => { - it('has an enabled link button', () => { - expect(configurePipelineLink().props('disabled')).toBe(false); - }); - - it('links to the pipeline editor with the right template', () => { - expect(configurePipelineLink().attributes('href')).toBe( - `${pipelineEditorPath}?template=${iOSTemplateName}`, - ); - }); - }); - - describe('the ios-Fastlane template', () => { - it('renders the template', () => { - expect(findIosTemplate().props('filterTemplates')).toStrictEqual([iOSTemplateName]); - }); - - it('has an enabled link button', () => { - expect(findIosTemplate().props('disabled')).toBe(false); - }); - - it('links to the pipeline editor with the right template', () => { - expect(configurePipelineLink().attributes('href')).toBe( - `${pipelineEditorPath}?template=${iOSTemplateName}`, - ); - }); - }); - }); -}); diff --git a/spec/frontend/pipelines/components/pipelines_list/empty_state/pipelines_ci_templates_spec.js b/spec/frontend/pipelines/components/pipelines_list/empty_state/pipelines_ci_templates_spec.js deleted file mode 100644 index 4bf4257f462..00000000000 --- a/spec/frontend/pipelines/components/pipelines_list/empty_state/pipelines_ci_templates_spec.js +++ /dev/null @@ -1,58 +0,0 @@ -import '~/commons'; -import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; -import { mockTracking, unmockTracking } from 'helpers/tracking_helper'; -import PipelinesCiTemplates from '~/pipelines/components/pipelines_list/empty_state/pipelines_ci_templates.vue'; -import CiTemplates from '~/pipelines/components/pipelines_list/empty_state/ci_templates.vue'; - -const pipelineEditorPath = '/-/ci/editor'; - -describe('Pipelines CI Templates', () => { - let wrapper; - let trackingSpy; - - const createWrapper = (propsData = {}, stubs = {}) => { - return shallowMountExtended(PipelinesCiTemplates, { - provide: { - pipelineEditorPath, - ...propsData, - }, - stubs, - }); - }; - - const findTestTemplateLink = () => wrapper.findByTestId('test-template-link'); - const findCiTemplates = () => wrapper.findComponent(CiTemplates); - - describe('templates', () => { - beforeEach(() => { - wrapper = createWrapper(); - }); - - it('renders test template and Ci templates', () => { - expect(findTestTemplateLink().attributes('href')).toBe( - pipelineEditorPath.concat('?template=Getting-Started'), - ); - expect(findCiTemplates().exists()).toBe(true); - }); - }); - - describe('tracking', () => { - beforeEach(() => { - wrapper = createWrapper(); - trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn); - }); - - afterEach(() => { - unmockTracking(); - }); - - it('sends an event when Getting-Started template is clicked', () => { - findTestTemplateLink().vm.$emit('click'); - - expect(trackingSpy).toHaveBeenCalledTimes(1); - expect(trackingSpy).toHaveBeenCalledWith(undefined, 'template_clicked', { - label: 'Getting-Started', - }); - }); - }); -}); diff --git a/spec/frontend/pipelines/components/pipelines_list/failure_widget/failed_job_details_spec.js b/spec/frontend/pipelines/components/pipelines_list/failure_widget/failed_job_details_spec.js deleted file mode 100644 index 479ee854ecf..00000000000 --- a/spec/frontend/pipelines/components/pipelines_list/failure_widget/failed_job_details_spec.js +++ /dev/null @@ -1,254 +0,0 @@ -import Vue from 'vue'; -import VueApollo from 'vue-apollo'; -import { GlIcon, GlLink } from '@gitlab/ui'; -import createMockApollo from 'helpers/mock_apollo_helper'; -import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; -import waitForPromises from 'helpers/wait_for_promises'; -import { createAlert } from '~/alert'; -import { getIdFromGraphQLId } from '~/graphql_shared/utils'; -import FailedJobDetails from '~/pipelines/components/pipelines_list/failure_widget/failed_job_details.vue'; -import RetryMrFailedJobMutation from '~/pipelines/graphql/mutations/retry_mr_failed_job.mutation.graphql'; -import { BRIDGE_KIND } from '~/pipelines/components/graph/constants'; -import { job } from './mock'; - -Vue.use(VueApollo); -jest.mock('~/alert'); - -const createFakeEvent = () => ({ stopPropagation: jest.fn() }); - -describe('FailedJobDetails component', () => { - let wrapper; - let mockRetryResponse; - - const retrySuccessResponse = { - data: { - jobRetry: { - errors: [], - }, - }, - }; - - const defaultProps = { - job, - }; - - const createComponent = ({ props = {} } = {}) => { - const handlers = [[RetryMrFailedJobMutation, mockRetryResponse]]; - - wrapper = shallowMountExtended(FailedJobDetails, { - propsData: { - ...defaultProps, - ...props, - }, - apolloProvider: createMockApollo(handlers), - }); - }; - - const findArrowIcon = () => wrapper.findComponent(GlIcon); - const findJobId = () => wrapper.findComponent(GlLink); - const findJobLog = () => wrapper.findByTestId('job-log'); - const findJobName = () => wrapper.findByText(defaultProps.job.name); - const findRetryButton = () => wrapper.findByLabelText('Retry'); - const findRow = () => wrapper.findByTestId('widget-row'); - const findStageName = () => wrapper.findByText(defaultProps.job.stage.name); - - beforeEach(() => { - mockRetryResponse = jest.fn(); - mockRetryResponse.mockResolvedValue(retrySuccessResponse); - }); - - describe('ui', () => { - beforeEach(() => { - createComponent(); - }); - - it('renders the job name', () => { - expect(findJobName().exists()).toBe(true); - }); - - it('renders the stage name', () => { - expect(findStageName().exists()).toBe(true); - }); - - it('renders the job id as a link', () => { - const jobId = getIdFromGraphQLId(defaultProps.job.id); - - expect(findJobId().exists()).toBe(true); - expect(findJobId().text()).toContain(String(jobId)); - }); - - it('does not renders the job lob', () => { - expect(findJobLog().exists()).toBe(false); - }); - }); - - describe('Retry action', () => { - describe('when the job is not retryable', () => { - beforeEach(() => { - createComponent({ props: { job: { ...job, retryable: false } } }); - }); - - it('disables the retry button', () => { - expect(findRetryButton().props().disabled).toBe(true); - }); - }); - - describe('when the job is a bridge', () => { - beforeEach(() => { - createComponent({ props: { job: { ...job, kind: BRIDGE_KIND } } }); - }); - - it('disables the retry button', () => { - expect(findRetryButton().props().disabled).toBe(true); - }); - }); - - describe('when the job is retryable', () => { - describe('and user has permission to update the build', () => { - beforeEach(() => { - createComponent(); - }); - - it('enables the retry button', () => { - expect(findRetryButton().props().disabled).toBe(false); - }); - - describe('when clicking on the retry button', () => { - it('passes the loading state to the button', async () => { - await findRetryButton().vm.$emit('click', createFakeEvent()); - - expect(findRetryButton().props().loading).toBe(true); - }); - - describe('and it succeeds', () => { - beforeEach(async () => { - findRetryButton().vm.$emit('click', createFakeEvent()); - await waitForPromises(); - }); - - it('is no longer loading', () => { - expect(findRetryButton().props().loading).toBe(false); - }); - - it('calls the retry mutation', () => { - expect(mockRetryResponse).toHaveBeenCalled(); - expect(mockRetryResponse).toHaveBeenCalledWith({ - id: job.id, - }); - }); - - it('emits the `retried-job` event', () => { - expect(wrapper.emitted('job-retried')).toStrictEqual([[job.name]]); - }); - }); - - describe('and it fails', () => { - const customErrorMsg = 'Custom error message from API'; - - beforeEach(async () => { - mockRetryResponse.mockResolvedValue({ - data: { jobRetry: { errors: [customErrorMsg] } }, - }); - findRetryButton().vm.$emit('click', createFakeEvent()); - - await waitForPromises(); - }); - - it('shows an error message', () => { - expect(createAlert).toHaveBeenCalledWith({ message: customErrorMsg }); - }); - - it('does not emits the `refetch-jobs` event', () => { - expect(wrapper.emitted('refetch-jobs')).toBeUndefined(); - }); - }); - }); - }); - - describe('and user does not have permission to update the build', () => { - beforeEach(() => { - createComponent({ - props: { job: { ...job, retryable: true, userPermissions: { updateBuild: false } } }, - }); - }); - - it('disables the retry button', () => { - expect(findRetryButton().props().disabled).toBe(true); - }); - }); - }); - }); - - describe('Job log', () => { - describe('without permissions', () => { - beforeEach(async () => { - createComponent({ props: { job: { ...job, userPermissions: { readBuild: false } } } }); - await findRow().trigger('click'); - }); - - it('does not renders the received html of the job log', () => { - expect(findJobLog().html()).not.toContain(defaultProps.job.trace.htmlSummary); - }); - - it('shows a permission error message', () => { - expect(findJobLog().text()).toBe("You do not have permission to read this job's log."); - }); - }); - - describe('with permissions', () => { - beforeEach(() => { - createComponent(); - }); - - describe('when clicking on the row', () => { - beforeEach(async () => { - await findRow().trigger('click'); - }); - - describe('while collapsed', () => { - it('expands the job log', () => { - expect(findJobLog().exists()).toBe(true); - }); - - it('renders the down arrow', () => { - expect(findArrowIcon().props().name).toBe('chevron-down'); - }); - - it('renders the received html of the job log', () => { - expect(findJobLog().html()).toContain(defaultProps.job.trace.htmlSummary); - }); - }); - - describe('while expanded', () => { - it('collapes the job log', async () => { - expect(findJobLog().exists()).toBe(true); - - await findRow().trigger('click'); - - expect(findJobLog().exists()).toBe(false); - }); - - it('renders the right arrow', async () => { - expect(findArrowIcon().props().name).toBe('chevron-down'); - - await findRow().trigger('click'); - - expect(findArrowIcon().props().name).toBe('chevron-right'); - }); - }); - }); - - describe('when clicking on a link element within the row', () => { - it('does not expands/collapse the job log', async () => { - expect(findJobLog().exists()).toBe(false); - expect(findArrowIcon().props().name).toBe('chevron-right'); - - await findJobId().vm.$emit('click'); - - expect(findJobLog().exists()).toBe(false); - expect(findArrowIcon().props().name).toBe('chevron-right'); - }); - }); - }); - }); -}); diff --git a/spec/frontend/pipelines/components/pipelines_list/failure_widget/failed_jobs_list_spec.js b/spec/frontend/pipelines/components/pipelines_list/failure_widget/failed_jobs_list_spec.js deleted file mode 100644 index 967812cc627..00000000000 --- a/spec/frontend/pipelines/components/pipelines_list/failure_widget/failed_jobs_list_spec.js +++ /dev/null @@ -1,279 +0,0 @@ -import Vue from 'vue'; -import VueApollo from 'vue-apollo'; - -import { GlLoadingIcon, GlToast } from '@gitlab/ui'; -import createMockApollo from 'helpers/mock_apollo_helper'; -import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; -import waitForPromises from 'helpers/wait_for_promises'; -import { createAlert } from '~/alert'; -import FailedJobsList from '~/pipelines/components/pipelines_list/failure_widget/failed_jobs_list.vue'; -import FailedJobDetails from '~/pipelines/components/pipelines_list/failure_widget/failed_job_details.vue'; -import * as utils from '~/pipelines/components/pipelines_list/failure_widget/utils'; -import getPipelineFailedJobs from '~/pipelines/graphql/queries/get_pipeline_failed_jobs.query.graphql'; -import { failedJobsMock, failedJobsMock2, failedJobsMockEmpty, activeFailedJobsMock } from './mock'; - -Vue.use(VueApollo); -Vue.use(GlToast); - -jest.mock('~/alert'); - -describe('FailedJobsList component', () => { - let wrapper; - let mockFailedJobsResponse; - const showToast = jest.fn(); - - const defaultProps = { - failedJobsCount: 0, - graphqlResourceEtag: 'api/graphql', - isPipelineActive: false, - pipelineIid: 1, - projectPath: 'namespace/project/', - }; - - const defaultProvide = { - graphqlPath: 'api/graphql', - }; - - const createComponent = ({ props = {}, provide } = {}) => { - const handlers = [[getPipelineFailedJobs, mockFailedJobsResponse]]; - const mockApollo = createMockApollo(handlers); - - wrapper = shallowMountExtended(FailedJobsList, { - propsData: { - ...defaultProps, - ...props, - }, - provide: { - ...defaultProvide, - ...provide, - }, - apolloProvider: mockApollo, - mocks: { - $toast: { - show: showToast, - }, - }, - }); - }; - - const findAllHeaders = () => wrapper.findAllByTestId('header'); - const findFailedJobRows = () => wrapper.findAllComponents(FailedJobDetails); - const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); - const findNoFailedJobsText = () => wrapper.findByText('No failed jobs in this pipeline 🎉'); - - beforeEach(() => { - mockFailedJobsResponse = jest.fn(); - }); - - describe('on mount', () => { - beforeEach(() => { - mockFailedJobsResponse.mockResolvedValue(failedJobsMock); - createComponent(); - }); - - it('fires the graphql query', () => { - expect(mockFailedJobsResponse).toHaveBeenCalledTimes(1); - expect(mockFailedJobsResponse).toHaveBeenCalledWith({ - fullPath: defaultProps.projectPath, - pipelineIid: defaultProps.pipelineIid, - }); - }); - }); - - describe('when loading failed jobs', () => { - beforeEach(() => { - mockFailedJobsResponse.mockResolvedValue(failedJobsMock); - createComponent(); - }); - - it('shows a loading icon', () => { - expect(findLoadingIcon().exists()).toBe(true); - }); - }); - - describe('when failed jobs have loaded', () => { - beforeEach(async () => { - mockFailedJobsResponse.mockResolvedValue(failedJobsMock); - jest.spyOn(utils, 'sortJobsByStatus'); - - createComponent(); - - await waitForPromises(); - }); - - it('does not renders a loading icon', () => { - expect(findLoadingIcon().exists()).toBe(false); - }); - - it('renders table column', () => { - expect(findAllHeaders()).toHaveLength(3); - }); - - it('shows the list of failed jobs', () => { - expect(findFailedJobRows()).toHaveLength( - failedJobsMock.data.project.pipeline.jobs.nodes.length, - ); - }); - - it('does not renders the empty state', () => { - expect(findNoFailedJobsText().exists()).toBe(false); - }); - - it('calls sortJobsByStatus', () => { - expect(utils.sortJobsByStatus).toHaveBeenCalledWith( - failedJobsMock.data.project.pipeline.jobs.nodes, - ); - }); - }); - - describe('when there are no failed jobs', () => { - beforeEach(async () => { - mockFailedJobsResponse.mockResolvedValue(failedJobsMockEmpty); - jest.spyOn(utils, 'sortJobsByStatus'); - - createComponent(); - - await waitForPromises(); - }); - - it('renders the empty state', () => { - expect(findNoFailedJobsText().exists()).toBe(true); - }); - }); - - describe('polling', () => { - it.each` - isGraphqlActive | text - ${true} | ${'polls'} - ${false} | ${'does not poll'} - `(`$text when isGraphqlActive: $isGraphqlActive`, async ({ isGraphqlActive }) => { - const defaultCount = 2; - const newCount = 1; - - const expectedCount = isGraphqlActive ? newCount : defaultCount; - const expectedCallCount = isGraphqlActive ? 2 : 1; - const mockResponse = isGraphqlActive ? activeFailedJobsMock : failedJobsMock; - - // Second result is to simulate polling with a different response - mockFailedJobsResponse.mockResolvedValueOnce(mockResponse); - mockFailedJobsResponse.mockResolvedValueOnce(failedJobsMock2); - - createComponent(); - await waitForPromises(); - - // Initially, we get the first response which is always the default - expect(mockFailedJobsResponse).toHaveBeenCalledTimes(1); - expect(findFailedJobRows()).toHaveLength(defaultCount); - - jest.advanceTimersByTime(10000); - await waitForPromises(); - - expect(mockFailedJobsResponse).toHaveBeenCalledTimes(expectedCallCount); - expect(findFailedJobRows()).toHaveLength(expectedCount); - }); - }); - - describe('when a REST action occurs', () => { - beforeEach(() => { - // Second result is to simulate polling with a different response - mockFailedJobsResponse.mockResolvedValueOnce(failedJobsMock); - mockFailedJobsResponse.mockResolvedValueOnce(failedJobsMock2); - }); - - it.each([true, false])('triggers a refetch of the jobs count', async (isPipelineActive) => { - const defaultCount = 2; - const newCount = 1; - - createComponent({ props: { isPipelineActive } }); - await waitForPromises(); - - // Initially, we get the first response which is always the default - expect(mockFailedJobsResponse).toHaveBeenCalledTimes(1); - expect(findFailedJobRows()).toHaveLength(defaultCount); - - wrapper.setProps({ isPipelineActive: !isPipelineActive }); - await waitForPromises(); - - expect(mockFailedJobsResponse).toHaveBeenCalledTimes(2); - expect(findFailedJobRows()).toHaveLength(newCount); - }); - }); - - describe('When the job count changes from REST', () => { - beforeEach(() => { - mockFailedJobsResponse.mockResolvedValue(failedJobsMockEmpty); - - createComponent(); - }); - - describe('and the count is the same', () => { - it('does not re-fetch the query', async () => { - expect(mockFailedJobsResponse).toHaveBeenCalledTimes(1); - - await wrapper.setProps({ failedJobsCount: 0 }); - - expect(mockFailedJobsResponse).toHaveBeenCalledTimes(1); - }); - }); - - describe('and the count is different', () => { - it('re-fetches the query', async () => { - expect(mockFailedJobsResponse).toHaveBeenCalledTimes(1); - - await wrapper.setProps({ failedJobsCount: 10 }); - - expect(mockFailedJobsResponse).toHaveBeenCalledTimes(2); - }); - }); - }); - - describe('when an error occurs loading jobs', () => { - const errorMessage = "We couldn't fetch jobs for you because you are not qualified"; - - beforeEach(async () => { - mockFailedJobsResponse.mockRejectedValue({ message: errorMessage }); - - createComponent(); - - await waitForPromises(); - }); - it('does not renders a loading icon', () => { - expect(findLoadingIcon().exists()).toBe(false); - }); - - it('calls create Alert with the error message and danger variant', () => { - expect(createAlert).toHaveBeenCalledWith({ message: errorMessage, variant: 'danger' }); - }); - }); - - describe('when `refetch-jobs` job is fired from the widget', () => { - beforeEach(async () => { - mockFailedJobsResponse.mockResolvedValueOnce(failedJobsMock); - mockFailedJobsResponse.mockResolvedValueOnce(failedJobsMock2); - - createComponent(); - - await waitForPromises(); - }); - - it('refetches all failed jobs', async () => { - expect(findFailedJobRows()).not.toHaveLength( - failedJobsMock2.data.project.pipeline.jobs.nodes.length, - ); - - await findFailedJobRows().at(0).vm.$emit('job-retried', 'job-name'); - await waitForPromises(); - - expect(findFailedJobRows()).toHaveLength( - failedJobsMock2.data.project.pipeline.jobs.nodes.length, - ); - }); - - it('shows a toast message', async () => { - await findFailedJobRows().at(0).vm.$emit('job-retried', 'job-name'); - await waitForPromises(); - - expect(showToast).toHaveBeenCalledWith('job-name job is being retried'); - }); - }); -}); diff --git a/spec/frontend/pipelines/components/pipelines_list/failure_widget/mock.js b/spec/frontend/pipelines/components/pipelines_list/failure_widget/mock.js deleted file mode 100644 index 318d787a984..00000000000 --- a/spec/frontend/pipelines/components/pipelines_list/failure_widget/mock.js +++ /dev/null @@ -1,78 +0,0 @@ -export const job = { - id: 'gid://gitlab/Ci::Build/5241', - allowFailure: false, - detailedStatus: { - id: 'status', - detailsPath: '/jobs/5241', - action: { - id: 'action', - path: '/retry', - icon: 'retry', - }, - group: 'running', - icon: 'status_running_icon', - }, - name: 'job-name', - retried: false, - retryable: true, - kind: 'BUILD', - stage: { - id: '1', - name: 'build', - }, - trace: { - htmlSummary: '<h1>Hello</h1>', - }, - userPermissions: { - readBuild: true, - updateBuild: true, - }, -}; - -export const allowedToFailJob = { - ...job, - id: 'gid://gitlab/Ci::Build/5242', - allowFailure: true, -}; - -export const createFailedJobsMockCount = ({ count = 4, active = false } = {}) => { - return { - data: { - project: { - id: 'gid://gitlab/Project/20', - pipeline: { - id: 'gid://gitlab/Pipeline/20', - active, - jobs: { - count, - }, - }, - }, - }, - }; -}; - -const createFailedJobsMock = (nodes, active = false) => { - return { - data: { - project: { - id: 'gid://gitlab/Project/20', - pipeline: { - active, - id: 'gid://gitlab/Pipeline/20', - jobs: { - count: nodes.length, - nodes, - }, - }, - }, - }, - }; -}; - -export const failedJobsMock = createFailedJobsMock([allowedToFailJob, job]); -export const failedJobsMockEmpty = createFailedJobsMock([]); - -export const activeFailedJobsMock = createFailedJobsMock([allowedToFailJob, job], true); - -export const failedJobsMock2 = createFailedJobsMock([job]); diff --git a/spec/frontend/pipelines/components/pipelines_list/failure_widget/pipeline_failed_jobs_widget_spec.js b/spec/frontend/pipelines/components/pipelines_list/failure_widget/pipeline_failed_jobs_widget_spec.js deleted file mode 100644 index 5bbb874edb0..00000000000 --- a/spec/frontend/pipelines/components/pipelines_list/failure_widget/pipeline_failed_jobs_widget_spec.js +++ /dev/null @@ -1,139 +0,0 @@ -import { GlButton, GlCard, GlIcon, GlPopover } from '@gitlab/ui'; -import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; -import PipelineFailedJobsWidget from '~/pipelines/components/pipelines_list/failure_widget/pipeline_failed_jobs_widget.vue'; -import FailedJobsList from '~/pipelines/components/pipelines_list/failure_widget/failed_jobs_list.vue'; - -jest.mock('~/alert'); - -describe('PipelineFailedJobsWidget component', () => { - let wrapper; - - const defaultProps = { - failedJobsCount: 4, - isPipelineActive: false, - pipelineIid: 1, - pipelinePath: '/pipelines/1', - projectPath: 'namespace/project/', - }; - - const defaultProvide = { - fullPath: 'namespace/project/', - }; - - const createComponent = ({ props = {}, provide = {} } = {}) => { - wrapper = shallowMountExtended(PipelineFailedJobsWidget, { - propsData: { - ...defaultProps, - ...props, - }, - provide: { - ...defaultProvide, - ...provide, - }, - stubs: { GlCard }, - }); - }; - - const findFailedJobsCard = () => wrapper.findByTestId('failed-jobs-card'); - const findFailedJobsButton = () => wrapper.findComponent(GlButton); - const findFailedJobsList = () => wrapper.findAllComponents(FailedJobsList); - const findInfoIcon = () => wrapper.findComponent(GlIcon); - const findInfoPopover = () => wrapper.findComponent(GlPopover); - - describe('when there are no failed jobs', () => { - beforeEach(() => { - createComponent({ props: { failedJobsCount: 0 } }); - }); - - it('renders the show failed jobs button with a count of 0', () => { - expect(findFailedJobsButton().exists()).toBe(true); - expect(findFailedJobsButton().text()).toBe('Failed jobs (0)'); - }); - }); - - describe('when there are failed jobs', () => { - beforeEach(() => { - createComponent(); - }); - - it('renders the show failed jobs button with correct count', () => { - expect(findFailedJobsButton().exists()).toBe(true); - expect(findFailedJobsButton().text()).toBe(`Failed jobs (${defaultProps.failedJobsCount})`); - }); - - it('renders the info icon', () => { - expect(findInfoIcon().exists()).toBe(true); - }); - - it('renders the info popover', () => { - expect(findInfoPopover().exists()).toBe(true); - }); - - it('does not render the failed jobs widget', () => { - expect(findFailedJobsList().exists()).toBe(false); - }); - }); - - describe('when the job button is clicked', () => { - beforeEach(async () => { - createComponent(); - await findFailedJobsButton().vm.$emit('click'); - }); - - it('renders the failed jobs widget', () => { - expect(findFailedJobsList().exists()).toBe(true); - }); - - it('removes the CSS border classes', () => { - expect(findFailedJobsCard().attributes('class')).not.toContain( - 'gl-border-white gl-hover-border-gray-100', - ); - }); - }); - - describe('when the job details are not expanded', () => { - beforeEach(() => { - createComponent(); - }); - - it('has the CSS border classes', () => { - expect(findFailedJobsCard().attributes('class')).toContain( - 'gl-border-white gl-hover-border-gray-100', - ); - }); - }); - - describe('when the job count changes', () => { - beforeEach(() => { - createComponent(); - }); - - describe('from the prop', () => { - it('updates the job count', async () => { - const newJobCount = 12; - - expect(findFailedJobsButton().text()).toContain(String(defaultProps.failedJobsCount)); - - await wrapper.setProps({ failedJobsCount: newJobCount }); - - expect(findFailedJobsButton().text()).toContain(String(newJobCount)); - }); - }); - - describe('from the event', () => { - beforeEach(async () => { - await findFailedJobsButton().vm.$emit('click'); - }); - - it('updates the job count', async () => { - const newJobCount = 12; - - expect(findFailedJobsButton().text()).toContain(String(defaultProps.failedJobsCount)); - - await findFailedJobsList().at(0).vm.$emit('failed-jobs-count', newJobCount); - - expect(findFailedJobsButton().text()).toContain(String(newJobCount)); - }); - }); - }); -}); diff --git a/spec/frontend/pipelines/components/pipelines_list/failure_widget/utils_spec.js b/spec/frontend/pipelines/components/pipelines_list/failure_widget/utils_spec.js deleted file mode 100644 index 44f16478151..00000000000 --- a/spec/frontend/pipelines/components/pipelines_list/failure_widget/utils_spec.js +++ /dev/null @@ -1,58 +0,0 @@ -import { - isFailedJob, - sortJobsByStatus, -} from '~/pipelines/components/pipelines_list/failure_widget/utils'; - -describe('isFailedJob', () => { - describe('when the job argument is undefined', () => { - it('returns false', () => { - expect(isFailedJob()).toBe(false); - }); - }); - - describe('when the job is of status `failed`', () => { - it('returns false', () => { - expect(isFailedJob({ detailedStatus: { group: 'success' } })).toBe(false); - }); - }); - - describe('when the job status is `failed`', () => { - it('returns true', () => { - expect(isFailedJob({ detailedStatus: { group: 'failed' } })).toBe(true); - }); - }); -}); - -describe('sortJobsByStatus', () => { - describe('when the arg is undefined', () => { - it('returns an empty array', () => { - expect(sortJobsByStatus()).toEqual([]); - }); - }); - - describe('when receiving an empty array', () => { - it('returns an empty array', () => { - expect(sortJobsByStatus([])).toEqual([]); - }); - }); - - describe('when reciving a list of jobs', () => { - const jobArr = [ - { detailedStatus: { group: 'failed' } }, - { detailedStatus: { group: 'allowed_to_fail' } }, - { detailedStatus: { group: 'failed' } }, - { detailedStatus: { group: 'success' } }, - ]; - - const expectedResult = [ - { detailedStatus: { group: 'failed' } }, - { detailedStatus: { group: 'failed' } }, - { detailedStatus: { group: 'allowed_to_fail' } }, - { detailedStatus: { group: 'success' } }, - ]; - - it('sorts failed jobs first', () => { - expect(sortJobsByStatus(jobArr)).toEqual(expectedResult); - }); - }); -}); diff --git a/spec/frontend/pipelines/components/pipelines_list/pipieline_stop_modal_spec.js b/spec/frontend/pipelines/components/pipelines_list/pipieline_stop_modal_spec.js deleted file mode 100644 index 249126390f1..00000000000 --- a/spec/frontend/pipelines/components/pipelines_list/pipieline_stop_modal_spec.js +++ /dev/null @@ -1,27 +0,0 @@ -import { shallowMount } from '@vue/test-utils'; -import { GlSprintf } from '@gitlab/ui'; -import PipelineStopModal from '~/pipelines/components/pipelines_list/pipeline_stop_modal.vue'; -import { mockPipelineHeader } from '../../mock_data'; - -describe('PipelineStopModal', () => { - let wrapper; - - const createComponent = () => { - wrapper = shallowMount(PipelineStopModal, { - propsData: { - pipeline: mockPipelineHeader, - }, - stubs: { - GlSprintf, - }, - }); - }; - - beforeEach(() => { - createComponent(); - }); - - it('should render "stop pipeline" warning', () => { - expect(wrapper.text()).toMatch(`You’re about to stop pipeline #${mockPipelineHeader.id}.`); - }); -}); diff --git a/spec/frontend/pipelines/empty_state_spec.js b/spec/frontend/pipelines/empty_state_spec.js deleted file mode 100644 index 5465e4d77da..00000000000 --- a/spec/frontend/pipelines/empty_state_spec.js +++ /dev/null @@ -1,87 +0,0 @@ -import '~/commons'; -import { shallowMount } from '@vue/test-utils'; -import { GlEmptyState } from '@gitlab/ui'; -import { stubExperiments } from 'helpers/experimentation_helper'; -import EmptyState from '~/pipelines/components/pipelines_list/empty_state.vue'; -import GitlabExperiment from '~/experimentation/components/gitlab_experiment.vue'; -import PipelinesCiTemplates from '~/pipelines/components/pipelines_list/empty_state/pipelines_ci_templates.vue'; -import IosTemplates from '~/pipelines/components/pipelines_list/empty_state/ios_templates.vue'; - -describe('Pipelines Empty State', () => { - let wrapper; - - const findIllustration = () => wrapper.find('img'); - const findButton = () => wrapper.find('a'); - const pipelinesCiTemplates = () => wrapper.findComponent(PipelinesCiTemplates); - const iosTemplates = () => wrapper.findComponent(IosTemplates); - - const createWrapper = (props = {}) => { - wrapper = shallowMount(EmptyState, { - provide: { - pipelineEditorPath: '', - suggestedCiTemplates: [], - anyRunnersAvailable: true, - ciRunnerSettingsPath: '', - }, - propsData: { - emptyStateSvgPath: 'foo.svg', - canSetCi: true, - ...props, - }, - stubs: { - GlEmptyState, - GitlabExperiment, - }, - }); - }; - - describe('when user can configure CI', () => { - describe('when the ios_specific_templates experiment is active', () => { - beforeEach(() => { - stubExperiments({ ios_specific_templates: 'candidate' }); - createWrapper(); - }); - - it('should render the iOS templates', () => { - expect(iosTemplates().exists()).toBe(true); - }); - - it('should not render the CI/CD templates', () => { - expect(pipelinesCiTemplates().exists()).toBe(false); - }); - }); - - describe('when the ios_specific_templates experiment is inactive', () => { - beforeEach(() => { - stubExperiments({ ios_specific_templates: 'control' }); - createWrapper(); - }); - - it('should render the CI/CD templates', () => { - expect(pipelinesCiTemplates().exists()).toBe(true); - }); - - it('should not render the iOS templates', () => { - expect(iosTemplates().exists()).toBe(false); - }); - }); - }); - - describe('when user cannot configure CI', () => { - beforeEach(() => { - createWrapper({ canSetCi: false }); - }); - - it('should render empty state SVG', () => { - expect(findIllustration().attributes('src')).toBe('foo.svg'); - }); - - it('should render empty state header', () => { - expect(wrapper.text()).toBe('This project is not currently set up to run pipelines.'); - }); - - it('should not render a link', () => { - expect(findButton().exists()).toBe(false); - }); - }); -}); diff --git a/spec/frontend/pipelines/graph/action_component_spec.js b/spec/frontend/pipelines/graph/action_component_spec.js deleted file mode 100644 index 890255f225e..00000000000 --- a/spec/frontend/pipelines/graph/action_component_spec.js +++ /dev/null @@ -1,116 +0,0 @@ -import { GlButton } from '@gitlab/ui'; -import { mount } from '@vue/test-utils'; -import MockAdapter from 'axios-mock-adapter'; -import { nextTick } from 'vue'; -import waitForPromises from 'helpers/wait_for_promises'; -import axios from '~/lib/utils/axios_utils'; -import { HTTP_STATUS_OK } from '~/lib/utils/http_status'; -import ActionComponent from '~/pipelines/components/jobs_shared/action_component.vue'; - -describe('pipeline graph action component', () => { - let wrapper; - let mock; - const findButton = () => wrapper.findComponent(GlButton); - const findTooltipWrapper = () => wrapper.find('[data-testid="ci-action-icon-tooltip-wrapper"]'); - - const defaultProps = { - tooltipText: 'bar', - link: 'foo', - actionIcon: 'cancel', - }; - - const createComponent = ({ props } = {}) => { - wrapper = mount(ActionComponent, { - propsData: { ...defaultProps, ...props }, - }); - }; - - beforeEach(() => { - mock = new MockAdapter(axios); - - mock.onPost('foo.json').reply(HTTP_STATUS_OK); - }); - - afterEach(() => { - mock.restore(); - }); - - describe('render', () => { - beforeEach(() => { - createComponent(); - }); - - it('should render the provided title as a bootstrap tooltip', () => { - expect(findTooltipWrapper().attributes('title')).toBe('bar'); - }); - - it('should update bootstrap tooltip when title changes', async () => { - wrapper.setProps({ tooltipText: 'changed' }); - - await nextTick(); - expect(findTooltipWrapper().attributes('title')).toBe('changed'); - }); - - it('should render an svg', () => { - expect(wrapper.find('.ci-action-icon-wrapper').exists()).toBe(true); - expect(wrapper.find('svg').exists()).toBe(true); - }); - }); - - describe('on click', () => { - beforeEach(() => { - createComponent(); - }); - - it('emits `pipelineActionRequestComplete` after a successful request', async () => { - findButton().trigger('click'); - - await waitForPromises(); - - expect(wrapper.emitted().pipelineActionRequestComplete).toHaveLength(1); - }); - - it('renders a loading icon while waiting for request', async () => { - findButton().trigger('click'); - - await nextTick(); - expect(wrapper.find('.js-action-icon-loading').exists()).toBe(true); - }); - }); - - describe('when has a confirmation modal', () => { - beforeEach(() => { - createComponent({ props: { withConfirmationModal: true, shouldTriggerClick: false } }); - }); - - describe('and a first click is initiated', () => { - beforeEach(async () => { - findButton().trigger('click'); - - await waitForPromises(); - }); - - it('emits `showActionConfirmationModal` event', () => { - expect(wrapper.emitted().showActionConfirmationModal).toHaveLength(1); - }); - - it('does not emit `pipelineActionRequestComplete` event', () => { - expect(wrapper.emitted().pipelineActionRequestComplete).toBeUndefined(); - }); - }); - - describe('and the `shouldTriggerClick` value becomes true', () => { - beforeEach(async () => { - await wrapper.setProps({ shouldTriggerClick: true }); - }); - - it('does not emit `showActionConfirmationModal` event', () => { - expect(wrapper.emitted().showActionConfirmationModal).toBeUndefined(); - }); - - it('emits `actionButtonClicked` event', () => { - expect(wrapper.emitted().actionButtonClicked).toHaveLength(1); - }); - }); - }); -}); diff --git a/spec/frontend/pipelines/graph/graph_component_spec.js b/spec/frontend/pipelines/graph/graph_component_spec.js deleted file mode 100644 index e9bce037800..00000000000 --- a/spec/frontend/pipelines/graph/graph_component_spec.js +++ /dev/null @@ -1,182 +0,0 @@ -import { shallowMount } from '@vue/test-utils'; -import mockPipelineResponse from 'test_fixtures/pipelines/pipeline_details.json'; -import { mountExtended } from 'helpers/vue_test_utils_helper'; -import { LAYER_VIEW, STAGE_VIEW } from '~/pipelines/components/graph/constants'; -import PipelineGraph from '~/pipelines/components/graph/graph_component.vue'; -import JobItem from '~/pipelines/components/graph/job_item.vue'; -import LinkedPipelinesColumn from '~/pipelines/components/graph/linked_pipelines_column.vue'; -import StageColumnComponent from '~/pipelines/components/graph/stage_column_component.vue'; -import { calculatePipelineLayersInfo } from '~/pipelines/components/graph/utils'; -import LinksLayer from '~/pipelines/components/graph_shared/links_layer.vue'; - -import { generateResponse, pipelineWithUpstreamDownstream } from './mock_data'; - -describe('graph component', () => { - let wrapper; - - const findDownstreamColumn = () => wrapper.findByTestId('downstream-pipelines'); - const findLinkedColumns = () => wrapper.findAllComponents(LinkedPipelinesColumn); - const findLinksLayer = () => wrapper.findComponent(LinksLayer); - const findStageColumns = () => wrapper.findAllComponents(StageColumnComponent); - const findStageNameInJob = () => wrapper.findByTestId('stage-name-in-job'); - - const defaultProps = { - pipeline: generateResponse(mockPipelineResponse, 'root/fungi-xoxo'), - showLinks: false, - viewType: STAGE_VIEW, - configPaths: { - metricsPath: '', - graphqlResourceEtag: 'this/is/a/path', - }, - }; - - const defaultData = { - measurements: { - width: 800, - height: 800, - }, - }; - - const createComponent = ({ - data = {}, - mountFn = shallowMount, - props = {}, - stubOverride = {}, - } = {}) => { - wrapper = mountFn(PipelineGraph, { - propsData: { - ...defaultProps, - ...props, - }, - data() { - return { - ...defaultData, - ...data, - }; - }, - stubs: { - 'links-inner': true, - 'linked-pipeline': true, - 'job-item': true, - 'job-group-dropdown': true, - ...stubOverride, - }, - }); - }; - - describe('with data', () => { - beforeEach(() => { - createComponent({ mountFn: mountExtended }); - }); - - it('renders the main columns in the graph', () => { - expect(findStageColumns()).toHaveLength(defaultProps.pipeline.stages.length); - }); - - it('renders the links layer', () => { - expect(findLinksLayer().exists()).toBe(true); - }); - - it('does not display stage name on the job in default (stage) mode', () => { - expect(findStageNameInJob().exists()).toBe(false); - }); - - describe('when column requests a refresh', () => { - beforeEach(() => { - findStageColumns().at(0).vm.$emit('refreshPipelineGraph'); - }); - - it('refreshPipelineGraph is emitted', () => { - expect(wrapper.emitted().refreshPipelineGraph).toHaveLength(1); - }); - }); - - describe('when column request an update to the retry confirmation modal', () => { - beforeEach(() => { - findStageColumns().at(0).vm.$emit('setSkipRetryModal'); - }); - - it('setSkipRetryModal is emitted', () => { - expect(wrapper.emitted().setSkipRetryModal).toHaveLength(1); - }); - }); - - describe('when links are present', () => { - beforeEach(() => { - createComponent({ - mountFn: mountExtended, - stubOverride: { 'job-item': false }, - data: { hoveredJobName: 'test_a' }, - }); - findLinksLayer().vm.$emit('highlightedJobsChange', ['test_c', 'build_c']); - }); - - it('dims unrelated jobs', () => { - const unrelatedJob = wrapper.findComponent(JobItem); - expect(findLinksLayer().emitted().highlightedJobsChange).toHaveLength(1); - expect(unrelatedJob.classes('gl-opacity-3')).toBe(true); - }); - }); - }); - - describe('when linked pipelines are not present', () => { - beforeEach(() => { - createComponent({ mountFn: mountExtended }); - }); - - it('should not render a linked pipelines column', () => { - expect(findLinkedColumns()).toHaveLength(0); - }); - }); - - describe('when linked pipelines are present', () => { - beforeEach(() => { - createComponent({ - mountFn: mountExtended, - props: { pipeline: pipelineWithUpstreamDownstream(mockPipelineResponse) }, - }); - }); - - it('should render linked pipelines columns', () => { - expect(findLinkedColumns()).toHaveLength(2); - }); - }); - - describe('in layers mode', () => { - beforeEach(() => { - createComponent({ - mountFn: mountExtended, - stubOverride: { - 'job-item': false, - 'job-group-dropdown': false, - }, - props: { - viewType: LAYER_VIEW, - computedPipelineInfo: calculatePipelineLayersInfo(defaultProps.pipeline, 'layer', ''), - }, - }); - }); - - it('displays the stage name on the job', () => { - expect(findStageNameInJob().exists()).toBe(true); - }); - }); - - describe('downstream pipelines', () => { - beforeEach(() => { - createComponent({ - mountFn: mountExtended, - props: { - pipeline: pipelineWithUpstreamDownstream(mockPipelineResponse), - }, - }); - }); - - it('filters pipelines spawned from the same trigger job', () => { - // The mock data has one downstream with `retried: true and one - // with retried false. We filter the `retried: true` out so we - // should only pass one downstream - expect(findDownstreamColumn().props().linkedPipelines).toHaveLength(1); - }); - }); -}); diff --git a/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js b/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js deleted file mode 100644 index 7b59d82ae6f..00000000000 --- a/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js +++ /dev/null @@ -1,603 +0,0 @@ -import { GlAlert, GlButton, GlButtonGroup, GlLoadingIcon, GlToggle } from '@gitlab/ui'; -import MockAdapter from 'axios-mock-adapter'; -import Vue from 'vue'; -import VueApollo from 'vue-apollo'; -import mockPipelineResponse from 'test_fixtures/pipelines/pipeline_details.json'; -import { useLocalStorageSpy } from 'helpers/local_storage_helper'; -import createMockApollo from 'helpers/mock_apollo_helper'; -import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper'; -import { stubPerformanceWebAPI } from 'helpers/performance'; -import waitForPromises from 'helpers/wait_for_promises'; -import getPipelineDetails from 'shared_queries/pipelines/get_pipeline_details.query.graphql'; -import getUserCallouts from '~/graphql_shared/queries/get_user_callouts.query.graphql'; -import axios from '~/lib/utils/axios_utils'; -import { HTTP_STATUS_OK } from '~/lib/utils/http_status'; -import { - PIPELINES_DETAIL_LINK_DURATION, - PIPELINES_DETAIL_LINKS_TOTAL, - PIPELINES_DETAIL_LINKS_JOB_RATIO, -} from '~/performance/constants'; -import * as perfUtils from '~/performance/utils'; -import { - ACTION_FAILURE, - LAYER_VIEW, - STAGE_VIEW, - VIEW_TYPE_KEY, -} from '~/pipelines/components/graph/constants'; -import PipelineGraph from '~/pipelines/components/graph/graph_component.vue'; -import PipelineGraphWrapper from '~/pipelines/components/graph/graph_component_wrapper.vue'; -import GraphViewSelector from '~/pipelines/components/graph/graph_view_selector.vue'; -import * as Api from '~/pipelines/components/graph_shared/api'; -import LinksLayer from '~/pipelines/components/graph_shared/links_layer.vue'; -import * as parsingUtils from '~/pipelines/components/parsing_utils'; -import getPipelineHeaderData from '~/pipelines/graphql/queries/get_pipeline_header_data.query.graphql'; -import * as sentryUtils from '~/pipelines/utils'; -import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue'; -import { mockRunningPipelineHeaderData } from '../mock_data'; -import { - mapCallouts, - mockCalloutsResponse, - mockPipelineResponseWithTooManyJobs, -} from './mock_data'; - -const defaultProvide = { - graphqlResourceEtag: 'frog/amphibirama/etag/', - metricsPath: '', - pipelineProjectPath: 'frog/amphibirama', - pipelineIid: '22', -}; - -describe('Pipeline graph wrapper', () => { - Vue.use(VueApollo); - useLocalStorageSpy(); - - let wrapper; - let requestHandlers; - let pipelineDetailsHandler; - - const findAlert = () => wrapper.findByTestId('error-alert'); - const findJobCountWarning = () => wrapper.findByTestId('job-count-warning'); - const findDependenciesToggle = () => wrapper.findByTestId('show-links-toggle'); - const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); - const findLinksLayer = () => wrapper.findComponent(LinksLayer); - const findGraph = () => wrapper.findComponent(PipelineGraph); - const findStageColumnTitle = () => wrapper.findByTestId('stage-column-title'); - const findViewSelector = () => wrapper.findComponent(GraphViewSelector); - const findViewSelectorToggle = () => findViewSelector().findComponent(GlToggle); - const findViewSelectorTrip = () => findViewSelector().findComponent(GlAlert); - const getLocalStorageSync = () => wrapper.findComponent(LocalStorageSync); - - const createComponent = ({ - apolloProvider, - data = {}, - provide = {}, - mountFn = shallowMountExtended, - } = {}) => { - wrapper = mountFn(PipelineGraphWrapper, { - provide: { - ...defaultProvide, - ...provide, - }, - apolloProvider, - data() { - return { - ...data, - }; - }, - }); - }; - - const createComponentWithApollo = ({ - calloutsList = [], - data = {}, - mountFn = shallowMountExtended, - provide = {}, - } = {}) => { - const callouts = mapCallouts(calloutsList); - - requestHandlers = { - getUserCalloutsHandler: jest.fn().mockResolvedValue(mockCalloutsResponse(callouts)), - getPipelineHeaderDataHandler: jest.fn().mockResolvedValue(mockRunningPipelineHeaderData), - getPipelineDetailsHandler: pipelineDetailsHandler, - }; - - const handlers = [ - [getPipelineHeaderData, requestHandlers.getPipelineHeaderDataHandler], - [getPipelineDetails, requestHandlers.getPipelineDetailsHandler], - [getUserCallouts, requestHandlers.getUserCalloutsHandler], - ]; - - const apolloProvider = createMockApollo(handlers); - createComponent({ apolloProvider, data, provide, mountFn }); - }; - - beforeEach(() => { - pipelineDetailsHandler = jest.fn(); - pipelineDetailsHandler.mockResolvedValue(mockPipelineResponse); - }); - - describe('when data is loading', () => { - beforeEach(() => { - createComponentWithApollo(); - }); - - it('displays the loading icon', () => { - expect(findLoadingIcon().exists()).toBe(true); - }); - - it('does not display the alert', () => { - expect(findAlert().exists()).toBe(false); - }); - - it('does not display the graph', () => { - expect(findGraph().exists()).toBe(false); - }); - - it('skips querying headerPipeline', () => { - expect(wrapper.vm.$apollo.queries.headerPipeline.skip).toBe(true); - }); - }); - - describe('when data has loaded', () => { - beforeEach(async () => { - createComponentWithApollo(); - await waitForPromises(); - }); - - it('does not display the loading icon', () => { - expect(findLoadingIcon().exists()).toBe(false); - }); - - it('does not display the alert', () => { - expect(findAlert().exists()).toBe(false); - }); - - it('displays the graph', () => { - expect(findGraph().exists()).toBe(true); - }); - - it('passes the etag resource and metrics path to the graph', () => { - expect(findGraph().props('configPaths')).toMatchObject({ - graphqlResourceEtag: defaultProvide.graphqlResourceEtag, - metricsPath: defaultProvide.metricsPath, - }); - }); - }); - - describe('when a stage has 100 jobs or more', () => { - beforeEach(async () => { - pipelineDetailsHandler.mockResolvedValue(mockPipelineResponseWithTooManyJobs); - createComponentWithApollo(); - await waitForPromises(); - }); - - it('show a warning alert', () => { - expect(findJobCountWarning().exists()).toBe(true); - expect(findJobCountWarning().props().title).toBe( - 'Only the first 100 jobs per stage are displayed', - ); - }); - }); - - describe('when there is an error', () => { - beforeEach(async () => { - pipelineDetailsHandler.mockRejectedValue(new Error('GraphQL error')); - createComponentWithApollo(); - await waitForPromises(); - }); - - it('does not display the loading icon', () => { - expect(findLoadingIcon().exists()).toBe(false); - }); - - it('displays the alert', () => { - expect(findAlert().exists()).toBe(true); - }); - - it('does not display the graph', () => { - expect(findGraph().exists()).toBe(false); - }); - }); - - describe('when there is no pipeline iid available', () => { - beforeEach(async () => { - createComponentWithApollo({ - provide: { - pipelineIid: '', - }, - }); - await waitForPromises(); - }); - - it('does not display the loading icon', () => { - expect(findLoadingIcon().exists()).toBe(false); - }); - - it('displays the no iid alert', () => { - expect(findAlert().exists()).toBe(true); - expect(findAlert().text()).toBe( - 'The data in this pipeline is too old to be rendered as a graph. Please check the Jobs tab to access historical data.', - ); - }); - - it('does not display the graph', () => { - expect(findGraph().exists()).toBe(false); - }); - }); - - describe('events', () => { - beforeEach(async () => { - createComponentWithApollo(); - await waitForPromises(); - }); - describe('when receiving `setSkipRetryModal` event', () => { - it('passes down `skipRetryModal` value as true', async () => { - expect(findGraph().props('skipRetryModal')).toBe(false); - - await findGraph().vm.$emit('setSkipRetryModal'); - - expect(findGraph().props('skipRetryModal')).toBe(true); - }); - }); - }); - - describe('when there is an error with an action in the graph', () => { - beforeEach(async () => { - createComponentWithApollo(); - await waitForPromises(); - await findGraph().vm.$emit('error', { type: ACTION_FAILURE }); - }); - - it('does not display the loading icon', () => { - expect(findLoadingIcon().exists()).toBe(false); - }); - - it('displays the action error alert', () => { - expect(findAlert().exists()).toBe(true); - expect(findAlert().text()).toBe('An error occurred while performing this action.'); - }); - - it('displays the graph', () => { - expect(findGraph().exists()).toBe(true); - }); - }); - - describe('when refresh action is emitted', () => { - beforeEach(async () => { - createComponentWithApollo(); - await waitForPromises(); - findGraph().vm.$emit('refreshPipelineGraph'); - }); - - it('calls refetch', () => { - expect(requestHandlers.getPipelineHeaderDataHandler).toHaveBeenCalledWith({ - fullPath: 'frog/amphibirama', - iid: '22', - }); - expect(requestHandlers.getPipelineDetailsHandler).toHaveBeenCalledTimes(2); - expect(requestHandlers.getUserCalloutsHandler).toHaveBeenCalledWith({}); - }); - }); - - describe('when query times out', () => { - const advanceApolloTimers = async () => { - jest.runOnlyPendingTimers(); - await waitForPromises(); - }; - - beforeEach(async () => { - const errorData = { - data: { - project: { - pipelines: null, - }, - }, - errors: [{ message: 'timeout' }], - }; - - pipelineDetailsHandler - .mockResolvedValueOnce(errorData) - .mockResolvedValueOnce(mockPipelineResponse) - .mockResolvedValueOnce(errorData); - - createComponentWithApollo(); - await waitForPromises(); - }); - - it('shows correct errors and does not overwrite populated data when data is empty', async () => { - /* fails at first, shows error, no data yet */ - expect(findAlert().exists()).toBe(true); - expect(findGraph().exists()).toBe(false); - - /* succeeds, clears error, shows graph */ - await advanceApolloTimers(); - expect(findAlert().exists()).toBe(false); - expect(findGraph().exists()).toBe(true); - - /* fails again, alert returns but data persists */ - await advanceApolloTimers(); - expect(findAlert().exists()).toBe(true); - expect(findGraph().exists()).toBe(true); - }); - }); - - describe('view dropdown', () => { - describe('default', () => { - let layersFn; - beforeEach(async () => { - layersFn = jest.spyOn(parsingUtils, 'listByLayers'); - createComponentWithApollo({ - mountFn: mountExtended, - }); - - await waitForPromises(); - }); - - it('appears when pipeline uses needs', () => { - expect(findViewSelector().exists()).toBe(true); - }); - - it('switches between views', async () => { - expect(findStageColumnTitle().text()).toBe('deploy'); - - await findViewSelector().vm.$emit('updateViewType', LAYER_VIEW); - - expect(findStageColumnTitle().text()).toBe(''); - }); - - it('saves the view type to local storage', async () => { - await findViewSelector().vm.$emit('updateViewType', LAYER_VIEW); - expect(localStorage.setItem.mock.calls).toEqual([[VIEW_TYPE_KEY, LAYER_VIEW]]); - }); - - it('calls listByLayers only once no matter how many times view is switched', async () => { - expect(layersFn).not.toHaveBeenCalled(); - await findViewSelector().vm.$emit('updateViewType', LAYER_VIEW); - expect(layersFn).toHaveBeenCalledTimes(1); - await findViewSelector().vm.$emit('updateViewType', STAGE_VIEW); - await findViewSelector().vm.$emit('updateViewType', LAYER_VIEW); - await findViewSelector().vm.$emit('updateViewType', STAGE_VIEW); - expect(layersFn).toHaveBeenCalledTimes(1); - }); - }); - - describe('when layers view is selected', () => { - beforeEach(async () => { - createComponentWithApollo({ - data: { - currentViewType: LAYER_VIEW, - }, - mountFn: mountExtended, - }); - - jest.runOnlyPendingTimers(); - await waitForPromises(); - }); - - it('sets showLinks to true', async () => { - /* This spec uses .props for performance reasons. */ - expect(findLinksLayer().exists()).toBe(true); - expect(findLinksLayer().props('showLinks')).toBe(false); - expect(findViewSelector().props('type')).toBe(LAYER_VIEW); - await findDependenciesToggle().vm.$emit('change', true); - - jest.runOnlyPendingTimers(); - await waitForPromises(); - expect(wrapper.findComponent(LinksLayer).props('showLinks')).toBe(true); - }); - }); - - describe('when layers view is selected, and links are active', () => { - beforeEach(async () => { - createComponentWithApollo({ - data: { - currentViewType: LAYER_VIEW, - showLinks: true, - }, - mountFn: mountExtended, - }); - - await waitForPromises(); - }); - - it('shows the hover tip in the view selector', async () => { - await findViewSelectorToggle().vm.$emit('change', true); - expect(findViewSelectorTrip().exists()).toBe(true); - }); - }); - - describe('when hover tip would otherwise show, but it has been previously dismissed', () => { - beforeEach(async () => { - createComponentWithApollo({ - data: { - currentViewType: LAYER_VIEW, - showLinks: true, - }, - mountFn: mountExtended, - calloutsList: ['pipeline_needs_hover_tip'.toUpperCase()], - }); - - jest.runOnlyPendingTimers(); - await waitForPromises(); - }); - - it('does not show the hover tip', async () => { - await findViewSelectorToggle().vm.$emit('change', true); - expect(findViewSelectorTrip().exists()).toBe(false); - }); - }); - - describe('when feature flag is on and local storage is set', () => { - beforeEach(async () => { - localStorage.setItem(VIEW_TYPE_KEY, LAYER_VIEW); - - createComponentWithApollo({ - mountFn: mountExtended, - }); - - await waitForPromises(); - }); - - afterEach(() => { - localStorage.clear(); - }); - - it('sets the asString prop on the LocalStorageSync component', () => { - expect(getLocalStorageSync().props('asString')).toBe(true); - }); - - it('reads the view type from localStorage when available', () => { - const viewSelectorNeedsSegment = wrapper - .findComponent(GlButtonGroup) - .findAllComponents(GlButton) - .at(1); - expect(viewSelectorNeedsSegment.classes()).toContain('selected'); - }); - }); - - describe('when feature flag is on and local storage is set, but the graph does not use needs', () => { - beforeEach(async () => { - const nonNeedsResponse = { ...mockPipelineResponse }; - nonNeedsResponse.data.project.pipeline.usesNeeds = false; - - localStorage.setItem(VIEW_TYPE_KEY, LAYER_VIEW); - - pipelineDetailsHandler.mockResolvedValue(nonNeedsResponse); - createComponentWithApollo({ - mountFn: mountExtended, - }); - - await waitForPromises(); - }); - - afterEach(() => { - localStorage.clear(); - }); - - it('still passes stage type to graph', () => { - expect(findGraph().props('viewType')).toBe(STAGE_VIEW); - }); - }); - - describe('when feature flag is on but pipeline does not use needs', () => { - beforeEach(async () => { - const nonNeedsResponse = { ...mockPipelineResponse }; - nonNeedsResponse.data.project.pipeline.usesNeeds = false; - - pipelineDetailsHandler.mockResolvedValue(nonNeedsResponse); - createComponentWithApollo({ - mountFn: mountExtended, - }); - - jest.runOnlyPendingTimers(); - await waitForPromises(); - }); - - it('does not appear when pipeline does not use needs', () => { - expect(findViewSelector().exists()).toBe(false); - }); - }); - }); - - describe('performance metrics', () => { - const metricsPath = '/root/project/-/ci/prometheus_metrics/histograms.json'; - let markAndMeasure; - let reportToSentry; - let reportPerformance; - let mock; - - beforeEach(() => { - jest.spyOn(window, 'requestAnimationFrame').mockImplementation((cb) => cb()); - markAndMeasure = jest.spyOn(perfUtils, 'performanceMarkAndMeasure'); - reportToSentry = jest.spyOn(sentryUtils, 'reportToSentry'); - reportPerformance = jest.spyOn(Api, 'reportPerformance'); - }); - - describe('with no metrics path', () => { - beforeEach(async () => { - createComponentWithApollo(); - await waitForPromises(); - }); - - it('is not called', () => { - expect(markAndMeasure).not.toHaveBeenCalled(); - expect(reportToSentry).not.toHaveBeenCalled(); - expect(reportPerformance).not.toHaveBeenCalled(); - }); - }); - - describe('with metrics path', () => { - const duration = 500; - const numLinks = 3; - const totalGroups = 7; - const metricsData = { - histograms: [ - { name: PIPELINES_DETAIL_LINK_DURATION, value: duration / 1000 }, - { name: PIPELINES_DETAIL_LINKS_TOTAL, value: numLinks }, - { - name: PIPELINES_DETAIL_LINKS_JOB_RATIO, - value: numLinks / totalGroups, - }, - ], - }; - - describe('when no duration is obtained', () => { - beforeEach(async () => { - stubPerformanceWebAPI(); - - createComponentWithApollo({ - provide: { - metricsPath, - glFeatures: { - pipelineGraphLayersView: true, - }, - }, - data: { - currentViewType: LAYER_VIEW, - }, - }); - - await waitForPromises(); - }); - - it('attempts to collect metrics', () => { - expect(markAndMeasure).toHaveBeenCalled(); - expect(reportPerformance).not.toHaveBeenCalled(); - expect(reportToSentry).not.toHaveBeenCalled(); - }); - }); - - describe('with duration and no error', () => { - beforeEach(async () => { - mock = new MockAdapter(axios); - mock.onPost(metricsPath).reply(HTTP_STATUS_OK, {}); - - jest.spyOn(window.performance, 'getEntriesByName').mockImplementation(() => { - return [{ duration }]; - }); - - createComponentWithApollo({ - provide: { - metricsPath, - }, - data: { - currentViewType: LAYER_VIEW, - }, - }); - await waitForPromises(); - }); - - afterEach(() => { - mock.restore(); - }); - - it('calls reportPerformance with expected arguments', () => { - expect(markAndMeasure).toHaveBeenCalled(); - expect(reportPerformance).toHaveBeenCalled(); - expect(reportPerformance).toHaveBeenCalledWith(metricsPath, metricsData); - expect(reportToSentry).not.toHaveBeenCalled(); - }); - }); - }); - }); -}); diff --git a/spec/frontend/pipelines/graph/graph_view_selector_spec.js b/spec/frontend/pipelines/graph/graph_view_selector_spec.js deleted file mode 100644 index 65ae9d19978..00000000000 --- a/spec/frontend/pipelines/graph/graph_view_selector_spec.js +++ /dev/null @@ -1,217 +0,0 @@ -import { GlAlert, GlButton, GlButtonGroup, GlLoadingIcon } 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(GlButtonGroup); - const findStageViewButton = () => findViewTypeSelector().findAllComponents(GlButton).at(0); - const findLayerViewButton = () => findViewTypeSelector().findAllComponents(GlButton).at(1); - const findSwitcherLoader = () => wrapper.find('[data-testid="switcher-loading-state"]'); - const findToggleLoader = () => findDependenciesToggle().findComponent(GlLoadingIcon); - const findHoverTip = () => wrapper.findComponent(GlAlert); - - const defaultProps = { - showLinks: false, - tipPreviouslyDismissed: false, - type: STAGE_VIEW, - }; - - const defaultData = { - hoverTipDismissed: false, - isToggleLoading: false, - isSwitcherLoading: false, - showLinksActive: false, - }; - - const createComponent = ({ data = {}, mountFn = shallowMount, props = {} } = {}) => { - wrapper = mountFn(GraphViewSelector, { - propsData: { - ...defaultProps, - ...props, - }, - data() { - return { - ...defaultData, - ...data, - }; - }, - }); - }; - - describe('when showing stage view', () => { - beforeEach(() => { - createComponent({ mountFn: mount }); - }); - - it('shows the Stage view button as selected', () => { - expect(findStageViewButton().classes('selected')).toBe(true); - }); - - it('shows the Job dependencies view button not selected', () => { - expect(findLayerViewButton().exists()).toBe(true); - expect(findLayerViewButton().classes('selected')).toBe(false); - }); - - 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 as selected', () => { - expect(findLayerViewButton().classes('selected')).toBe(true); - }); - - it('shows the Stage button as not selected', () => { - expect(findStageViewButton().exists()).toBe(true); - expect(findStageViewButton().classes('selected')).toBe(false); - }); - - it('shows the Job dependencies (links) toggle', () => { - expect(findDependenciesToggle().exists()).toBe(true); - }); - }); - - describe('events', () => { - beforeEach(() => { - 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 findStageViewButton().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().vm.$emit('change', true); - /* - 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]]); - }); - - it('does not emit an event if the click occurs on the currently selected view button', async () => { - expect(wrapper.emitted().updateShowLinksState).toBeUndefined(); - - await findLayerViewButton().trigger('click'); - - expect(wrapper.emitted().updateShowLinksState).toBeUndefined(); - }); - }); - - describe('hover tip callout', () => { - describe('when links are live and it has not been previously dismissed', () => { - beforeEach(() => { - createComponent({ - props: { - showLinks: true, - type: LAYER_VIEW, - }, - data: { - showLinksActive: true, - }, - mountFn: mount, - }); - }); - - it('is displayed', () => { - expect(findHoverTip().exists()).toBe(true); - expect(findHoverTip().text()).toBe(wrapper.vm.$options.i18n.hoverTipText); - }); - - it('emits dismissHoverTip event when the tip is dismissed', async () => { - expect(wrapper.emitted().dismissHoverTip).toBeUndefined(); - await findHoverTip().find('button').trigger('click'); - expect(wrapper.emitted().dismissHoverTip).toHaveLength(1); - }); - - it('is displayed at first then hidden on swith to STAGE_VIEW then displayed on switch to LAYER_VIEW', async () => { - expect(findHoverTip().exists()).toBe(true); - expect(findHoverTip().text()).toBe(wrapper.vm.$options.i18n.hoverTipText); - - await findStageViewButton().trigger('click'); - expect(findHoverTip().exists()).toBe(false); - - await findLayerViewButton().trigger('click'); - expect(findHoverTip().exists()).toBe(true); - expect(findHoverTip().text()).toBe(wrapper.vm.$options.i18n.hoverTipText); - }); - }); - - describe('when links are live and it has been previously dismissed', () => { - beforeEach(() => { - createComponent({ - props: { - showLinks: true, - tipPreviouslyDismissed: true, - type: LAYER_VIEW, - }, - data: { - showLinksActive: true, - }, - }); - }); - - it('is not displayed', () => { - expect(findHoverTip().exists()).toBe(false); - }); - }); - - describe('when links are not live', () => { - beforeEach(() => { - createComponent({ - props: { - showLinks: true, - type: LAYER_VIEW, - }, - data: { - showLinksActive: false, - }, - }); - }); - - it('is not displayed', () => { - expect(findHoverTip().exists()).toBe(false); - }); - }); - }); -}); diff --git a/spec/frontend/pipelines/graph/job_group_dropdown_spec.js b/spec/frontend/pipelines/graph/job_group_dropdown_spec.js deleted file mode 100644 index 1419a7b9982..00000000000 --- a/spec/frontend/pipelines/graph/job_group_dropdown_spec.js +++ /dev/null @@ -1,84 +0,0 @@ -import { shallowMount, mount } from '@vue/test-utils'; -import JobGroupDropdown from '~/pipelines/components/graph/job_group_dropdown.vue'; - -describe('job group dropdown component', () => { - const group = { - jobs: [ - { - id: 4256, - name: '<img src=x onerror=alert(document.domain)>', - status: { - icon: 'status_success', - text: 'passed', - label: 'passed', - tooltip: 'passed', - group: 'success', - details_path: '/root/ci-mock/builds/4256', - has_details: true, - action: { - icon: 'retry', - title: 'Retry', - path: '/root/ci-mock/builds/4256/retry', - method: 'post', - }, - }, - }, - { - id: 4299, - name: 'test', - status: { - icon: 'status_success', - text: 'passed', - label: 'passed', - tooltip: 'passed', - group: 'success', - details_path: '/root/ci-mock/builds/4299', - has_details: true, - action: { - icon: 'retry', - title: 'Retry', - path: '/root/ci-mock/builds/4299/retry', - method: 'post', - }, - }, - }, - ], - name: 'rspec:linux', - size: 2, - status: { - icon: 'status_success', - text: 'passed', - label: 'passed', - tooltip: 'passed', - group: 'success', - details_path: '/root/ci-mock/builds/4256', - has_details: true, - action: { - icon: 'retry', - title: 'Retry', - path: '/root/ci-mock/builds/4256/retry', - method: 'post', - }, - }, - }; - - let wrapper; - const findButton = () => wrapper.find('button'); - - const createComponent = ({ mountFn = shallowMount }) => { - wrapper = mountFn(JobGroupDropdown, { propsData: { group } }); - }; - - beforeEach(() => { - createComponent({ mountFn: mount }); - }); - - it('renders button with group name and size', () => { - expect(findButton().text()).toContain(group.name); - expect(findButton().text()).toContain(group.size.toString()); - }); - - it('renders dropdown with jobs', () => { - expect(wrapper.findAll('.scrollable-menu>ul>li').length).toBe(group.jobs.length); - }); -}); diff --git a/spec/frontend/pipelines/graph/job_item_spec.js b/spec/frontend/pipelines/graph/job_item_spec.js deleted file mode 100644 index 8a8b0e9aa63..00000000000 --- a/spec/frontend/pipelines/graph/job_item_spec.js +++ /dev/null @@ -1,492 +0,0 @@ -import MockAdapter from 'axios-mock-adapter'; -import Vue, { nextTick } from 'vue'; -import { GlBadge, GlModal, GlToast } from '@gitlab/ui'; -import JobItem from '~/pipelines/components/graph/job_item.vue'; -import axios from '~/lib/utils/axios_utils'; -import { useLocalStorageSpy } from 'helpers/local_storage_helper'; -import ActionComponent from '~/pipelines/components/jobs_shared/action_component.vue'; - -import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper'; -import { - delayedJob, - mockJob, - mockJobWithoutDetails, - mockJobWithUnauthorizedAction, - mockFailedJob, - triggerJob, - triggerJobWithRetryAction, -} from './mock_data'; - -describe('pipeline graph job item', () => { - useLocalStorageSpy(); - Vue.use(GlToast); - - let wrapper; - let mockAxios; - - const findJobWithoutLink = () => wrapper.findByTestId('job-without-link'); - const findJobWithLink = () => wrapper.findByTestId('job-with-link'); - const findActionVueComponent = () => wrapper.findComponent(ActionComponent); - const findActionComponent = () => wrapper.findByTestId('ci-action-component'); - const findBadge = () => wrapper.findComponent(GlBadge); - const findJobLink = () => wrapper.findByTestId('job-with-link'); - const findModal = () => wrapper.findComponent(GlModal); - - const clickOnModalPrimaryBtn = () => findModal().vm.$emit('primary'); - const clickOnModalCancelBtn = () => findModal().vm.$emit('hide'); - const clickOnModalCloseBtn = () => findModal().vm.$emit('close'); - - const myCustomClass1 = 'my-class-1'; - const myCustomClass2 = 'my-class-2'; - - const defaultProps = { - job: mockJob, - }; - - const createWrapper = ({ props, data, mountFn = mountExtended, mocks = {} } = {}) => { - wrapper = mountFn(JobItem, { - data() { - return { - ...data, - }; - }, - propsData: { - ...defaultProps, - ...props, - }, - mocks: { - ...mocks, - }, - }); - }; - - const triggerActiveClass = 'gl-shadow-x0-y0-b3-s1-blue-500'; - - beforeEach(() => { - mockAxios = new MockAdapter(axios); - }); - - afterEach(() => { - mockAxios.restore(); - }); - - describe('name with link', () => { - it('should render the job name and status with a link', async () => { - createWrapper(); - - await nextTick(); - const link = findJobLink(); - - expect(link.attributes('href')).toBe(mockJob.status.detailsPath); - - expect(link.attributes('title')).toBe(`${mockJob.name} - ${mockJob.status.label}`); - - expect(wrapper.find('.ci-status-icon-success').exists()).toBe(true); - - expect(wrapper.text()).toBe(mockJob.name); - }); - }); - - describe('name without link', () => { - beforeEach(() => { - createWrapper({ - props: { - job: mockJobWithoutDetails, - cssClassJobName: 'css-class-job-name', - jobHovered: 'test', - }, - }); - }); - - it('should render status and name', () => { - expect(wrapper.find('.ci-status-icon-success').exists()).toBe(true); - expect(findJobLink().exists()).toBe(false); - - expect(wrapper.text()).toBe(mockJobWithoutDetails.name); - }); - - it('should apply hover class and provided class name', () => { - expect(findJobWithoutLink().classes()).toContain('css-class-job-name'); - }); - }); - - describe('action icon', () => { - it('should render the action icon', () => { - createWrapper(); - - const actionComponent = findActionComponent(); - - expect(actionComponent.exists()).toBe(true); - expect(actionComponent.props('actionIcon')).toBe('retry'); - expect(actionComponent.attributes('disabled')).toBeUndefined(); - }); - - it('should render disabled action icon when user cannot run the action', () => { - createWrapper({ - props: { - job: mockJobWithUnauthorizedAction, - }, - }); - - const actionComponent = findActionComponent(); - - expect(actionComponent.exists()).toBe(true); - expect(actionComponent.props('actionIcon')).toBe('stop'); - expect(actionComponent.attributes('disabled')).toBeDefined(); - }); - - it('action icon tooltip text when job has passed but can be ran again', () => { - createWrapper({ props: { job: mockJob } }); - - expect(findActionComponent().props('tooltipText')).toBe('Run again'); - }); - - it('action icon tooltip text when job has failed and can be retried', () => { - createWrapper({ props: { job: mockFailedJob } }); - - expect(findActionComponent().props('tooltipText')).toBe('Retry'); - }); - }); - - describe('job style', () => { - beforeEach(() => { - createWrapper({ - props: { - job: mockJob, - cssClassJobName: 'css-class-job-name', - }, - }); - }); - - it('should render provided class name', () => { - expect(findJobLink().classes()).toContain('css-class-job-name'); - }); - - it('does not show a badge on the job item', () => { - expect(findBadge().exists()).toBe(false); - }); - - it('does not apply the trigger job class', () => { - expect(findJobWithLink().classes()).not.toContain('gl-rounded-lg'); - }); - }); - - describe('status label', () => { - it('should not render status label when it is not provided', () => { - createWrapper({ - props: { - job: { - id: 4258, - name: 'test', - status: { - icon: 'status_success', - }, - }, - }, - }); - - expect(findJobWithoutLink().attributes('title')).toBe('test'); - }); - - it('should not render status label when it is provided', () => { - createWrapper({ - props: { - job: { - id: 4259, - name: 'test', - status: { - icon: 'status_success', - label: 'success', - tooltip: 'success', - }, - }, - }, - }); - - expect(findJobWithoutLink().attributes('title')).toBe('test - success'); - }); - }); - - describe('for delayed job', () => { - it('displays remaining time in tooltip', () => { - createWrapper({ - props: { - job: delayedJob, - }, - }); - - expect(findJobWithLink().attributes('title')).toBe( - `delayed job - delayed manual action (00:00:00)`, - ); - }); - }); - - describe('trigger job', () => { - describe('card', () => { - beforeEach(() => { - createWrapper({ - props: { - job: triggerJob, - }, - }); - }); - - it('shows a badge on the job item', () => { - expect(findBadge().exists()).toBe(true); - expect(findBadge().text()).toBe('Trigger job'); - }); - - it('applies a rounded corner style instead of the usual pill shape', () => { - expect(findJobWithoutLink().classes()).toContain('gl-rounded-lg'); - }); - }); - - describe('when retrying', () => { - const mockToastShow = jest.fn(); - - beforeEach(async () => { - createWrapper({ - mountFn: shallowMountExtended, - props: { - skipRetryModal: true, - job: triggerJobWithRetryAction, - }, - mocks: { - $toast: { - show: mockToastShow, - }, - }, - }); - - await findActionVueComponent().vm.$emit('pipelineActionRequestComplete'); - await nextTick(); - }); - - it('shows a toast message that the downstream is being created', () => { - expect(mockToastShow).toHaveBeenCalledTimes(1); - }); - }); - - describe('highlighting', () => { - it.each` - job | jobName | expanded | link - ${mockJob} | ${mockJob.name} | ${true} | ${true} - ${mockJobWithoutDetails} | ${mockJobWithoutDetails.name} | ${true} | ${false} - `( - `trigger job should stay highlighted when downstream is expanded`, - ({ job, jobName, expanded, link }) => { - createWrapper({ - props: { - job, - pipelineExpanded: { jobName, expanded }, - }, - }); - const findJobEl = link ? findJobWithLink : findJobWithoutLink; - - expect(findJobEl().classes()).toContain(triggerActiveClass); - }, - ); - - it.each` - job | jobName | expanded | link - ${mockJob} | ${mockJob.name} | ${false} | ${true} - ${mockJobWithoutDetails} | ${mockJobWithoutDetails.name} | ${false} | ${false} - `( - `trigger job should not be highlighted when downstream is not expanded`, - ({ job, jobName, expanded, link }) => { - createWrapper({ - props: { - job, - pipelineExpanded: { jobName, expanded }, - }, - }); - const findJobEl = link ? findJobWithLink : findJobWithoutLink; - - expect(findJobEl().classes()).not.toContain(triggerActiveClass); - }, - ); - }); - }); - - describe('job classes', () => { - it('job class is shown', () => { - createWrapper({ - props: { - job: mockJob, - cssClassJobName: 'my-class', - }, - }); - - const jobLinkEl = findJobLink(); - - expect(jobLinkEl.classes()).toContain('my-class'); - - expect(jobLinkEl.classes()).not.toContain(triggerActiveClass); - }); - - it('job class is shown, along with hover', () => { - createWrapper({ - props: { - job: mockJob, - cssClassJobName: 'my-class', - sourceJobHovered: mockJob.name, - }, - }); - - const jobLinkEl = findJobLink(); - - expect(jobLinkEl.classes()).toContain('my-class'); - expect(jobLinkEl.classes()).toContain(triggerActiveClass); - }); - - it('multiple job classes are shown', () => { - createWrapper({ - props: { - job: mockJob, - cssClassJobName: [myCustomClass1, myCustomClass2], - }, - }); - - const jobLinkEl = findJobLink(); - - expect(jobLinkEl.classes()).toContain(myCustomClass1); - expect(jobLinkEl.classes()).toContain(myCustomClass2); - - expect(jobLinkEl.classes()).not.toContain(triggerActiveClass); - }); - - it('multiple job classes are shown conditionally', () => { - createWrapper({ - props: { - job: mockJob, - cssClassJobName: { [myCustomClass1]: true, [myCustomClass2]: true }, - }, - }); - - const jobLinkEl = findJobLink(); - - expect(jobLinkEl.classes()).toContain(myCustomClass1); - expect(jobLinkEl.classes()).toContain(myCustomClass2); - - expect(jobLinkEl.classes()).not.toContain(triggerActiveClass); - }); - - it('multiple job classes are shown, along with a hover', () => { - createWrapper({ - props: { - job: mockJob, - cssClassJobName: [myCustomClass1, myCustomClass2], - sourceJobHovered: mockJob.name, - }, - }); - - const jobLinkEl = findJobLink(); - - expect(jobLinkEl.classes()).toContain(myCustomClass1); - expect(jobLinkEl.classes()).toContain(myCustomClass2); - expect(jobLinkEl.classes()).toContain(triggerActiveClass); - }); - }); - - describe('confirmation modal', () => { - describe('when clicking on the action component', () => { - it.each` - skipRetryModal | exists | visibilityText - ${false} | ${true} | ${'shows'} - ${true} | ${false} | ${'hides'} - `( - '$visibilityText the modal when `skipRetryModal` is $skipRetryModal', - async ({ exists, skipRetryModal }) => { - createWrapper({ - props: { - skipRetryModal, - job: triggerJobWithRetryAction, - }, - }); - await findActionComponent().trigger('click'); - - expect(findModal().exists()).toBe(exists); - }, - ); - }); - - describe('when showing the modal', () => { - it.each` - buttonName | shouldTriggerActionClick | actionBtn - ${'primary'} | ${true} | ${clickOnModalPrimaryBtn} - ${'cancel'} | ${false} | ${clickOnModalCancelBtn} - ${'close'} | ${false} | ${clickOnModalCloseBtn} - `( - 'clicking on $buttonName will pass down shouldTriggerActionClick as $shouldTriggerActionClick to the action component', - async ({ shouldTriggerActionClick, actionBtn }) => { - createWrapper({ - props: { - skipRetryModal: false, - job: triggerJobWithRetryAction, - }, - }); - await findActionComponent().trigger('click'); - - await actionBtn(); - - expect(findActionComponent().props().shouldTriggerClick).toBe(shouldTriggerActionClick); - }, - ); - }); - - describe('when not checking the "do not show this again" checkbox', () => { - it.each` - actionName | actionBtn - ${'closing'} | ${clickOnModalCloseBtn} - ${'cancelling'} | ${clickOnModalCancelBtn} - ${'confirming'} | ${clickOnModalPrimaryBtn} - `( - 'does not emit any event and will not modify localstorage on $actionName', - async ({ actionBtn }) => { - createWrapper({ - props: { - skipRetryModal: false, - job: triggerJobWithRetryAction, - }, - }); - await findActionComponent().trigger('click'); - await actionBtn(); - - expect(wrapper.emitted().setSkipRetryModal).toBeUndefined(); - expect(localStorage.setItem).not.toHaveBeenCalled(); - }, - ); - }); - - describe('when checking the "do not show this again" checkbox', () => { - it.each` - actionName | actionBtn - ${'closing'} | ${clickOnModalCloseBtn} - ${'cancelling'} | ${clickOnModalCancelBtn} - ${'confirming'} | ${clickOnModalPrimaryBtn} - `( - 'emits "setSkipRetryModal" and set local storage key on $actionName the modal', - async ({ actionBtn }) => { - // We are passing the checkbox as a slot to the GlModal. - // The way GlModal is mounted, we can neither click on the box - // or emit an event directly. We therefore set the data property - // as it would be if the box was checked. - createWrapper({ - data: { - currentSkipModalValue: true, - }, - props: { - skipRetryModal: false, - job: triggerJobWithRetryAction, - }, - }); - await findActionComponent().trigger('click'); - await actionBtn(); - - expect(wrapper.emitted().setSkipRetryModal).toHaveLength(1); - expect(localStorage.setItem).toHaveBeenCalledWith('skip_retry_modal', 'true'); - }, - ); - }); - }); -}); diff --git a/spec/frontend/pipelines/graph/job_name_component_spec.js b/spec/frontend/pipelines/graph/job_name_component_spec.js deleted file mode 100644 index fca4c43d9fa..00000000000 --- a/spec/frontend/pipelines/graph/job_name_component_spec.js +++ /dev/null @@ -1,30 +0,0 @@ -import { mount } from '@vue/test-utils'; -import jobNameComponent from '~/pipelines/components/jobs_shared/job_name_component.vue'; -import CiIcon from '~/vue_shared/components/ci_icon.vue'; - -describe('job name component', () => { - let wrapper; - - const propsData = { - name: 'foo', - status: { - icon: 'status_success', - group: 'success', - }, - }; - - beforeEach(() => { - wrapper = mount(jobNameComponent, { - propsData, - }); - }); - - it('should render the provided name', () => { - expect(wrapper.text()).toBe(propsData.name); - }); - - it('should render an icon with the provided status', () => { - expect(wrapper.findComponent(CiIcon).exists()).toBe(true); - expect(wrapper.find('.ci-status-icon-success').exists()).toBe(true); - }); -}); diff --git a/spec/frontend/pipelines/graph/linked_pipeline_spec.js b/spec/frontend/pipelines/graph/linked_pipeline_spec.js deleted file mode 100644 index 8dae2aac664..00000000000 --- a/spec/frontend/pipelines/graph/linked_pipeline_spec.js +++ /dev/null @@ -1,464 +0,0 @@ -import Vue from 'vue'; -import VueApollo from 'vue-apollo'; -import { GlButton, GlLoadingIcon, GlTooltip } from '@gitlab/ui'; -import { createWrapper } from '@vue/test-utils'; -import createMockApollo from 'helpers/mock_apollo_helper'; -import { mountExtended } from 'helpers/vue_test_utils_helper'; -import waitForPromises from 'helpers/wait_for_promises'; -import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants'; -import { ACTION_FAILURE, UPSTREAM, DOWNSTREAM } from '~/pipelines/components/graph/constants'; -import LinkedPipelineComponent from '~/pipelines/components/graph/linked_pipeline.vue'; -import CancelPipelineMutation from '~/pipelines/graphql/mutations/cancel_pipeline.mutation.graphql'; -import RetryPipelineMutation from '~/pipelines/graphql/mutations/retry_pipeline.mutation.graphql'; -import CiIcon from '~/vue_shared/components/ci_icon.vue'; -import mockPipeline from './linked_pipelines_mock_data'; - -describe('Linked pipeline', () => { - let wrapper; - let requestHandlers; - - const downstreamProps = { - pipeline: { - ...mockPipeline, - multiproject: false, - }, - columnTitle: 'Downstream', - type: DOWNSTREAM, - expanded: false, - isLoading: false, - }; - - const upstreamProps = { - ...downstreamProps, - columnTitle: 'Upstream', - type: UPSTREAM, - }; - - const findButton = () => wrapper.findComponent(GlButton); - const findCancelButton = () => wrapper.findByLabelText('Cancel downstream pipeline'); - const findCardTooltip = () => wrapper.findComponent(GlTooltip); - const findDownstreamPipelineTitle = () => wrapper.findByTestId('downstream-title'); - const findExpandButton = () => wrapper.findByTestId('expand-pipeline-button'); - const findLinkedPipeline = () => wrapper.findComponent({ ref: 'linkedPipeline' }); - const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); - const findPipelineLabel = () => wrapper.findByTestId('downstream-pipeline-label'); - const findPipelineLink = () => wrapper.findByTestId('pipelineLink'); - const findRetryButton = () => wrapper.findByLabelText('Retry downstream pipeline'); - - const defaultHandlers = { - cancelPipeline: jest.fn().mockResolvedValue({ data: { pipelineCancel: { errors: [] } } }), - retryPipeline: jest.fn().mockResolvedValue({ data: { pipelineRetry: { errors: [] } } }), - }; - - const createMockApolloProvider = (handlers) => { - Vue.use(VueApollo); - - requestHandlers = handlers; - return createMockApollo([ - [CancelPipelineMutation, requestHandlers.cancelPipeline], - [RetryPipelineMutation, requestHandlers.retryPipeline], - ]); - }; - - const createComponent = ({ propsData, handlers = defaultHandlers }) => { - const mockApollo = createMockApolloProvider(handlers); - - wrapper = mountExtended(LinkedPipelineComponent, { - propsData, - apolloProvider: mockApollo, - }); - }; - - describe('rendered output', () => { - const props = { - pipeline: mockPipeline, - columnTitle: 'Downstream', - type: DOWNSTREAM, - expanded: false, - isLoading: false, - }; - - beforeEach(() => { - createComponent({ propsData: props }); - }); - - it('should render the project name', () => { - expect(wrapper.text()).toContain(props.pipeline.project.name); - }); - - it('should render an svg within the status container', () => { - const pipelineStatusElement = wrapper.findComponent(CiIcon); - - expect(pipelineStatusElement.find('svg').exists()).toBe(true); - }); - - it('should render the pipeline status icon svg', () => { - expect(wrapper.find('.ci-status-icon-success svg').exists()).toBe(true); - }); - - it('should have a ci-status child component', () => { - expect(wrapper.findComponent(CiIcon).exists()).toBe(true); - }); - - it('should render the pipeline id', () => { - expect(wrapper.text()).toContain(`#${props.pipeline.id}`); - }); - - it('adds the card tooltip text to the DOM', () => { - expect(findCardTooltip().exists()).toBe(true); - - expect(findCardTooltip().text()).toContain(mockPipeline.project.name); - expect(findCardTooltip().text()).toContain(mockPipeline.status.label); - expect(findCardTooltip().text()).toContain(mockPipeline.sourceJob.name); - expect(findCardTooltip().text()).toContain(mockPipeline.id.toString()); - }); - - it('should display multi-project label when pipeline project id is not the same as triggered pipeline project id', () => { - expect(findPipelineLabel().text()).toBe('Multi-project'); - }); - }); - - describe('upstream pipelines', () => { - beforeEach(() => { - createComponent({ propsData: upstreamProps }); - }); - - it('should display parent label when pipeline project id is the same as triggered_by pipeline project id', () => { - expect(findPipelineLabel().exists()).toBe(true); - }); - - it('upstream pipeline should contain the correct link', () => { - expect(findPipelineLink().attributes('href')).toBe(upstreamProps.pipeline.path); - }); - - it('applies the reverse-row css class to the card', () => { - expect(findLinkedPipeline().classes()).toContain('gl-flex-direction-row-reverse'); - expect(findLinkedPipeline().classes()).not.toContain('gl-flex-direction-row'); - }); - }); - - describe('downstream pipelines', () => { - describe('styling', () => { - beforeEach(() => { - createComponent({ propsData: downstreamProps }); - }); - - it('parent/child label container should exist', () => { - expect(findPipelineLabel().exists()).toBe(true); - }); - - it('should display child label when pipeline project id is the same as triggered pipeline project id', () => { - expect(findPipelineLabel().exists()).toBe(true); - }); - - it('should have the name of the trigger job on the card when it is a child pipeline', () => { - expect(findDownstreamPipelineTitle().text()).toBe(mockPipeline.sourceJob.name); - }); - - it('downstream pipeline should contain the correct link', () => { - expect(findPipelineLink().attributes('href')).toBe(downstreamProps.pipeline.path); - }); - - it('applies the flex-row css class to the card', () => { - expect(findLinkedPipeline().classes()).toContain('gl-flex-direction-row'); - expect(findLinkedPipeline().classes()).not.toContain('gl-flex-direction-row-reverse'); - }); - }); - - describe('action button', () => { - describe('with permissions', () => { - describe('on an upstream', () => { - describe('when retryable', () => { - beforeEach(() => { - const retryablePipeline = { - ...upstreamProps, - pipeline: { ...mockPipeline, retryable: true }, - }; - - createComponent({ propsData: retryablePipeline }); - }); - - it('does not show the retry or cancel button', () => { - expect(findCancelButton().exists()).toBe(false); - expect(findRetryButton().exists()).toBe(false); - }); - }); - }); - - describe('on a downstream', () => { - const retryablePipeline = { - ...downstreamProps, - pipeline: { ...mockPipeline, retryable: true }, - }; - - describe('when retryable', () => { - beforeEach(() => { - createComponent({ propsData: retryablePipeline }); - }); - - it('shows only the retry button', () => { - expect(findCancelButton().exists()).toBe(false); - expect(findRetryButton().exists()).toBe(true); - }); - - it.each` - findElement | name - ${findRetryButton} | ${'retry button'} - ${findExpandButton} | ${'expand button'} - `('hides the card tooltip when $name is hovered', async ({ findElement }) => { - expect(findCardTooltip().exists()).toBe(true); - - await findElement().trigger('mouseover'); - - expect(findCardTooltip().exists()).toBe(false); - }); - - describe('and the retry button is clicked', () => { - describe('on success', () => { - beforeEach(async () => { - await findRetryButton().trigger('click'); - }); - - it('calls the retry mutation', () => { - expect(requestHandlers.retryPipeline).toHaveBeenCalledTimes(1); - expect(requestHandlers.retryPipeline).toHaveBeenCalledWith({ - id: 'gid://gitlab/Ci::Pipeline/195', - }); - }); - - it('emits the refreshPipelineGraph event', async () => { - await waitForPromises(); - expect(wrapper.emitted('refreshPipelineGraph')).toHaveLength(1); - }); - }); - - describe('on failure', () => { - beforeEach(async () => { - createComponent({ - propsData: retryablePipeline, - handlers: { - retryPipeline: jest.fn().mockRejectedValue({ errors: [] }), - cancelPipeline: jest.fn().mockRejectedValue({ errors: [] }), - }, - }); - - await findRetryButton().trigger('click'); - }); - - it('emits an error event', async () => { - await waitForPromises(); - expect(wrapper.emitted('error')).toEqual([[{ type: ACTION_FAILURE }]]); - }); - }); - }); - }); - - describe('when cancelable', () => { - const cancelablePipeline = { - ...downstreamProps, - pipeline: { ...mockPipeline, cancelable: true }, - }; - - beforeEach(() => { - createComponent({ propsData: cancelablePipeline }); - }); - - it('shows only the cancel button', () => { - expect(findCancelButton().exists()).toBe(true); - expect(findRetryButton().exists()).toBe(false); - }); - - it.each` - findElement | name - ${findCancelButton} | ${'cancel button'} - ${findExpandButton} | ${'expand button'} - `('hides the card tooltip when $name is hovered', async ({ findElement }) => { - expect(findCardTooltip().exists()).toBe(true); - - await findElement().trigger('mouseover'); - - expect(findCardTooltip().exists()).toBe(false); - }); - - describe('and the cancel button is clicked', () => { - describe('on success', () => { - beforeEach(async () => { - await findCancelButton().trigger('click'); - }); - - it('calls the cancel mutation', () => { - expect(requestHandlers.cancelPipeline).toHaveBeenCalledTimes(1); - expect(requestHandlers.cancelPipeline).toHaveBeenCalledWith({ - id: 'gid://gitlab/Ci::Pipeline/195', - }); - }); - it('emits the refreshPipelineGraph event', async () => { - await waitForPromises(); - expect(wrapper.emitted('refreshPipelineGraph')).toHaveLength(1); - }); - }); - - describe('on failure', () => { - beforeEach(async () => { - createComponent({ - propsData: cancelablePipeline, - handlers: { - retryPipeline: jest.fn().mockRejectedValue({ errors: [] }), - cancelPipeline: jest.fn().mockRejectedValue({ errors: [] }), - }, - }); - - await findCancelButton().trigger('click'); - }); - - it('emits an error event', async () => { - await waitForPromises(); - expect(wrapper.emitted('error')).toEqual([[{ type: ACTION_FAILURE }]]); - }); - }); - }); - }); - - describe('when both cancellable and retryable', () => { - beforeEach(() => { - const pipelineWithTwoActions = { - ...downstreamProps, - pipeline: { ...mockPipeline, cancelable: true, retryable: true }, - }; - - createComponent({ propsData: pipelineWithTwoActions }); - }); - - it('only shows the cancel button', () => { - expect(findRetryButton().exists()).toBe(false); - expect(findCancelButton().exists()).toBe(true); - }); - }); - }); - }); - - describe('without permissions', () => { - beforeEach(() => { - const pipelineWithTwoActions = { - ...downstreamProps, - pipeline: { - ...mockPipeline, - cancelable: true, - retryable: true, - userPermissions: { updatePipeline: false }, - }, - }; - - createComponent({ propsData: pipelineWithTwoActions }); - }); - - it('does not show any action button', () => { - expect(findRetryButton().exists()).toBe(false); - expect(findCancelButton().exists()).toBe(false); - }); - }); - }); - }); - - describe('expand button', () => { - it.each` - pipelineType | chevronPosition | buttonBorderClasses | expanded - ${downstreamProps} | ${'chevron-lg-right'} | ${'gl-border-l-0!'} | ${false} - ${downstreamProps} | ${'chevron-lg-left'} | ${'gl-border-l-0!'} | ${true} - ${upstreamProps} | ${'chevron-lg-left'} | ${'gl-border-r-0!'} | ${false} - ${upstreamProps} | ${'chevron-lg-right'} | ${'gl-border-r-0!'} | ${true} - `( - '$pipelineType.columnTitle pipeline button icon should be $chevronPosition with $buttonBorderClasses if expanded state is $expanded', - ({ pipelineType, chevronPosition, buttonBorderClasses, expanded }) => { - createComponent({ propsData: { ...pipelineType, expanded } }); - expect(findExpandButton().props('icon')).toBe(chevronPosition); - expect(findExpandButton().classes()).toContain(buttonBorderClasses); - }, - ); - - describe('shadow border', () => { - beforeEach(() => { - createComponent({ propsData: downstreamProps }); - }); - - it.each` - activateEventName | deactivateEventName - ${'mouseover'} | ${'mouseout'} - ${'focus'} | ${'blur'} - `( - 'applies the class on $activateEventName and removes it on $deactivateEventName', - async ({ activateEventName, deactivateEventName }) => { - const shadowClass = 'gl-shadow-none!'; - - expect(findExpandButton().classes()).toContain(shadowClass); - - await findExpandButton().vm.$emit(activateEventName); - expect(findExpandButton().classes()).not.toContain(shadowClass); - - await findExpandButton().vm.$emit(deactivateEventName); - expect(findExpandButton().classes()).toContain(shadowClass); - }, - ); - }); - }); - - describe('when isLoading is true', () => { - const props = { - pipeline: mockPipeline, - columnTitle: 'Downstream', - type: DOWNSTREAM, - expanded: false, - isLoading: true, - }; - - beforeEach(() => { - createComponent({ propsData: props }); - }); - - it('loading icon is visible', () => { - expect(findLoadingIcon().exists()).toBe(true); - }); - }); - - describe('on click/hover', () => { - const props = { - pipeline: mockPipeline, - columnTitle: 'Downstream', - type: DOWNSTREAM, - expanded: false, - isLoading: false, - }; - - beforeEach(() => { - createComponent({ propsData: props }); - }); - - it('emits `pipelineClicked` event', () => { - findButton().trigger('click'); - - expect(wrapper.emitted('pipelineClicked')).toHaveLength(1); - }); - - it(`should emit ${BV_HIDE_TOOLTIP} to close the tooltip`, async () => { - const root = createWrapper(wrapper.vm.$root); - await findButton().vm.$emit('click'); - - expect(root.emitted(BV_HIDE_TOOLTIP)).toHaveLength(1); - }); - - it('should emit downstreamHovered with job name on mouseover', () => { - findLinkedPipeline().trigger('mouseover'); - expect(wrapper.emitted('downstreamHovered')).toStrictEqual([['test_c']]); - }); - - it('should emit downstreamHovered with empty string on mouseleave', () => { - findLinkedPipeline().trigger('mouseleave'); - expect(wrapper.emitted('downstreamHovered')).toStrictEqual([['']]); - }); - - it('should emit pipelineExpanded with job name and expanded state on click', () => { - findExpandButton().trigger('click'); - expect(wrapper.emitted('pipelineExpandToggle')).toStrictEqual([['test_c', true]]); - }); - }); -}); diff --git a/spec/frontend/pipelines/graph/linked_pipelines_column_spec.js b/spec/frontend/pipelines/graph/linked_pipelines_column_spec.js deleted file mode 100644 index bcea140f2dd..00000000000 --- a/spec/frontend/pipelines/graph/linked_pipelines_column_spec.js +++ /dev/null @@ -1,214 +0,0 @@ -import { mount, shallowMount } from '@vue/test-utils'; -import Vue, { nextTick } from 'vue'; -import VueApollo from 'vue-apollo'; -import mockPipelineResponse from 'test_fixtures/pipelines/pipeline_details.json'; -import createMockApollo from 'helpers/mock_apollo_helper'; -import waitForPromises from 'helpers/wait_for_promises'; -import getPipelineDetails from 'shared_queries/pipelines/get_pipeline_details.query.graphql'; -import { - DOWNSTREAM, - UPSTREAM, - LAYER_VIEW, - STAGE_VIEW, -} from '~/pipelines/components/graph/constants'; -import PipelineGraph from '~/pipelines/components/graph/graph_component.vue'; -import LinkedPipeline from '~/pipelines/components/graph/linked_pipeline.vue'; -import LinkedPipelinesColumn from '~/pipelines/components/graph/linked_pipelines_column.vue'; -import * as parsingUtils from '~/pipelines/components/parsing_utils'; -import { LOAD_FAILURE } from '~/pipelines/constants'; - -import { pipelineWithUpstreamDownstream, wrappedPipelineReturn } from './mock_data'; - -const processedPipeline = pipelineWithUpstreamDownstream(mockPipelineResponse); - -describe('Linked Pipelines Column', () => { - const defaultProps = { - columnTitle: 'Downstream', - linkedPipelines: processedPipeline.downstream, - showLinks: false, - type: DOWNSTREAM, - viewType: STAGE_VIEW, - configPaths: { - metricsPath: '', - graphqlResourceEtag: 'this/is/a/path', - }, - }; - - let wrapper; - const findLinkedColumnTitle = () => wrapper.find('[data-testid="linked-column-title"]'); - const findLinkedPipelineElements = () => wrapper.findAllComponents(LinkedPipeline); - const findPipelineGraph = () => wrapper.findComponent(PipelineGraph); - const findExpandButton = () => wrapper.find('[data-testid="expand-pipeline-button"]'); - - Vue.use(VueApollo); - - const createComponent = ({ apolloProvider, mountFn = shallowMount, props = {} } = {}) => { - wrapper = mountFn(LinkedPipelinesColumn, { - apolloProvider, - propsData: { - ...defaultProps, - ...props, - }, - }); - }; - - const createComponentWithApollo = ({ - mountFn = shallowMount, - getPipelineDetailsHandler = jest.fn().mockResolvedValue(wrappedPipelineReturn), - props = {}, - } = {}) => { - const requestHandlers = [[getPipelineDetails, getPipelineDetailsHandler]]; - - const apolloProvider = createMockApollo(requestHandlers); - createComponent({ apolloProvider, mountFn, props }); - }; - - describe('it renders correctly', () => { - beforeEach(() => { - createComponentWithApollo(); - }); - - it('renders the pipeline title', () => { - expect(findLinkedColumnTitle().text()).toBe(defaultProps.columnTitle); - }); - - it('renders the correct number of linked pipelines', () => { - expect(findLinkedPipelineElements()).toHaveLength(defaultProps.linkedPipelines.length); - }); - }); - - describe('click action', () => { - const clickExpandButton = async () => { - await findExpandButton().trigger('click'); - await waitForPromises(); - }; - - describe('layer type rendering', () => { - let layersFn; - - beforeEach(() => { - layersFn = jest.spyOn(parsingUtils, 'listByLayers'); - createComponentWithApollo({ mountFn: mount }); - }); - - it('calls listByLayers only once no matter how many times view is switched', async () => { - expect(layersFn).not.toHaveBeenCalled(); - await clickExpandButton(); - await wrapper.setProps({ viewType: LAYER_VIEW }); - await nextTick(); - expect(layersFn).toHaveBeenCalledTimes(1); - await wrapper.setProps({ viewType: STAGE_VIEW }); - await wrapper.setProps({ viewType: LAYER_VIEW }); - await wrapper.setProps({ viewType: STAGE_VIEW }); - expect(layersFn).toHaveBeenCalledTimes(1); - }); - }); - - describe('when graph does not use needs', () => { - beforeEach(() => { - const nonNeedsResponse = { ...wrappedPipelineReturn }; - nonNeedsResponse.data.project.pipeline.usesNeeds = false; - - createComponentWithApollo({ - props: { - viewType: LAYER_VIEW, - }, - getPipelineDetailsHandler: jest.fn().mockResolvedValue(nonNeedsResponse), - mountFn: mount, - }); - }); - - it('shows the stage view, even when the main graph view type is layers', async () => { - await clickExpandButton(); - expect(findPipelineGraph().props('viewType')).toBe(STAGE_VIEW); - }); - }); - - describe('downstream', () => { - describe('when successful', () => { - beforeEach(() => { - createComponentWithApollo({ mountFn: mount }); - }); - - it('toggles the pipeline visibility', async () => { - expect(findPipelineGraph().exists()).toBe(false); - await clickExpandButton(); - expect(findPipelineGraph().exists()).toBe(true); - await clickExpandButton(); - expect(findPipelineGraph().exists()).toBe(false); - }); - }); - - describe('on error', () => { - beforeEach(() => { - createComponentWithApollo({ - mountFn: mount, - getPipelineDetailsHandler: jest.fn().mockRejectedValue(new Error('GraphQL error')), - }); - }); - - it('emits the error', async () => { - await clickExpandButton(); - expect(wrapper.emitted().error).toEqual([[{ type: LOAD_FAILURE, skipSentry: true }]]); - }); - - it('does not show the pipeline', async () => { - expect(findPipelineGraph().exists()).toBe(false); - await clickExpandButton(); - expect(findPipelineGraph().exists()).toBe(false); - }); - }); - }); - - describe('upstream', () => { - const upstreamProps = { - columnTitle: 'Upstream', - /* - Because the IDs need to match to work, rather - than make new mock data, we are representing - the upstream pipeline with the downstream data. - */ - linkedPipelines: processedPipeline.downstream, - type: UPSTREAM, - }; - - describe('when successful', () => { - beforeEach(() => { - createComponentWithApollo({ - mountFn: mount, - props: upstreamProps, - }); - }); - - it('toggles the pipeline visibility', async () => { - expect(findPipelineGraph().exists()).toBe(false); - await clickExpandButton(); - expect(findPipelineGraph().exists()).toBe(true); - await clickExpandButton(); - expect(findPipelineGraph().exists()).toBe(false); - }); - }); - - describe('on error', () => { - beforeEach(() => { - createComponentWithApollo({ - mountFn: mount, - getPipelineDetailsHandler: jest.fn().mockRejectedValue(new Error('GraphQL error')), - props: upstreamProps, - }); - }); - - it('emits the error', async () => { - await clickExpandButton(); - expect(wrapper.emitted().error).toEqual([[{ type: LOAD_FAILURE, skipSentry: true }]]); - }); - - it('does not show the pipeline', async () => { - expect(findPipelineGraph().exists()).toBe(false); - await clickExpandButton(); - expect(findPipelineGraph().exists()).toBe(false); - }); - }); - }); - }); -}); diff --git a/spec/frontend/pipelines/graph/linked_pipelines_mock_data.js b/spec/frontend/pipelines/graph/linked_pipelines_mock_data.js deleted file mode 100644 index f7f5738e46d..00000000000 --- a/spec/frontend/pipelines/graph/linked_pipelines_mock_data.js +++ /dev/null @@ -1,27 +0,0 @@ -export default { - __typename: 'Pipeline', - id: 195, - iid: '5', - retryable: false, - cancelable: false, - userPermissions: { - updatePipeline: true, - }, - path: '/root/elemenohpee/-/pipelines/195', - status: { - __typename: 'DetailedStatus', - group: 'success', - label: 'passed', - icon: 'status_success', - }, - sourceJob: { - __typename: 'CiJob', - name: 'test_c', - }, - project: { - __typename: 'Project', - name: 'elemenohpee', - fullPath: 'root/elemenohpee', - }, - multiproject: true, -}; diff --git a/spec/frontend/pipelines/graph/mock_data.js b/spec/frontend/pipelines/graph/mock_data.js deleted file mode 100644 index 8d06d6931ed..00000000000 --- a/spec/frontend/pipelines/graph/mock_data.js +++ /dev/null @@ -1,387 +0,0 @@ -import mockPipelineResponse from 'test_fixtures/pipelines/pipeline_details.json'; -import { unwrapPipelineData } from '~/pipelines/components/graph/utils'; -import { - BUILD_KIND, - BRIDGE_KIND, - RETRY_ACTION_TITLE, -} from '~/pipelines/components/graph/constants'; - -// We mock this instead of using fixtures for performance reason. -const mockPipelineResponseCopy = JSON.parse(JSON.stringify(mockPipelineResponse)); -const groups = new Array(100).fill({ - ...mockPipelineResponse.data.project.pipeline.stages.nodes[0].groups.nodes[0], -}); -mockPipelineResponseCopy.data.project.pipeline.stages.nodes[0].groups.nodes = groups; -export const mockPipelineResponseWithTooManyJobs = mockPipelineResponseCopy; - -export const downstream = { - nodes: [ - { - id: 175, - iid: '31', - path: '/root/elemenohpee/-/pipelines/175', - retryable: true, - cancelable: false, - userPermissions: { - updatePipeline: true, - }, - status: { - id: '70', - group: 'success', - label: 'passed', - icon: 'status_success', - __typename: 'DetailedStatus', - }, - sourceJob: { - name: 'test_c', - id: '71', - retried: false, - __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', - retryable: true, - cancelable: false, - userPermissions: { - updatePipeline: true, - }, - status: { - id: '72', - group: 'success', - label: 'passed', - icon: 'status_success', - __typename: 'DetailedStatus', - }, - sourceJob: { - id: '73', - name: 'test_d', - retried: true, - __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', - retryable: true, - cancelable: false, - userPermissions: { - updatePipeline: true, - }, - status: { - id: '74', - group: 'success', - label: 'passed', - icon: 'status_success', - __typename: 'DetailedStatus', - }, - sourceJob: null, - project: { - id: 'gid://gitlab/Project/23', - name: 'abcd-dag', - fullPath: 'root/abcd-dag', - __typename: 'Project', - }, - __typename: 'Pipeline', - multiproject: true, -}; - -export const wrappedPipelineReturn = { - data: { - project: { - __typename: 'Project', - id: '75', - pipeline: { - __typename: 'Pipeline', - id: 'gid://gitlab/Ci::Pipeline/175', - iid: '38', - complete: true, - usesNeeds: true, - userPermissions: { - __typename: 'PipelinePermissions', - updatePipeline: true, - }, - downstream: { - retryable: true, - cancelable: false, - userPermissions: { - updatePipeline: true, - }, - __typename: 'PipelineConnection', - nodes: [], - }, - upstream: { - id: 'gid://gitlab/Ci::Pipeline/174', - iid: '37', - path: '/root/elemenohpee/-/pipelines/174', - retryable: true, - cancelable: false, - userPermissions: { - updatePipeline: true, - }, - __typename: 'Pipeline', - status: { - __typename: 'DetailedStatus', - id: '77', - group: 'success', - label: 'passed', - icon: 'status_success', - }, - sourceJob: { - name: 'test_c', - id: '78', - retried: false, - __typename: 'CiJob', - }, - project: { - id: 'gid://gitlab/Project/25', - name: 'elemenohpee', - fullPath: 'root/elemenohpee', - __typename: 'Project', - }, - }, - stages: { - __typename: 'CiStageConnection', - nodes: [ - { - name: 'build', - __typename: 'CiStage', - id: '79', - status: { - action: null, - id: '80', - __typename: 'DetailedStatus', - }, - groups: { - __typename: 'CiGroupConnection', - nodes: [ - { - __typename: 'CiGroup', - id: '81', - status: { - __typename: 'DetailedStatus', - id: '82', - label: 'passed', - group: 'success', - icon: 'status_success', - }, - name: 'build_n', - size: 1, - jobs: { - __typename: 'CiJobConnection', - nodes: [ - { - __typename: 'CiJob', - id: '83', - kind: BUILD_KIND, - name: 'build_n', - scheduledAt: null, - needs: { - __typename: 'CiBuildNeedConnection', - nodes: [], - }, - previousStageJobsOrNeeds: { - __typename: 'CiJobConnection', - nodes: [], - }, - status: { - __typename: 'DetailedStatus', - id: '84', - icon: 'status_success', - tooltip: 'passed', - label: 'passed', - hasDetails: true, - detailsPath: '/root/elemenohpee/-/jobs/1662', - group: 'success', - action: { - __typename: 'StatusAction', - id: '85', - buttonTitle: 'Retry this job', - icon: 'retry', - path: '/root/elemenohpee/-/jobs/1662/retry', - title: 'Retry', - }, - }, - }, - ], - }, - }, - ], - }, - }, - ], - }, - }, - }, - }, -}; - -export const generateResponse = (raw, mockPath) => unwrapPipelineData(mockPath, raw.data); - -export const pipelineWithUpstreamDownstream = (base) => { - const pip = { ...base }; - pip.data.project.pipeline.downstream = downstream; - pip.data.project.pipeline.upstream = upstream; - - return generateResponse(pip, 'root/abcd-dag'); -}; - -export const mapCallouts = (callouts) => - callouts.map((callout) => { - return { featureName: callout, __typename: 'UserCallout' }; - }); - -export const mockCalloutsResponse = (mappedCallouts) => ({ - data: { - currentUser: { - id: 45, - __typename: 'User', - callouts: { - id: 5, - __typename: 'UserCalloutConnection', - nodes: mappedCallouts, - }, - }, - }, -}); - -export const delayedJob = { - __typename: 'CiJob', - kind: BUILD_KIND, - name: 'delayed job', - scheduledAt: '2015-07-03T10:01:00.000Z', - needs: [], - status: { - __typename: 'DetailedStatus', - icon: 'status_scheduled', - tooltip: 'delayed manual action (%{remainingTime})', - hasDetails: true, - detailsPath: '/root/kinder-pipe/-/jobs/5339', - group: 'scheduled', - action: { - __typename: 'StatusAction', - icon: 'time-out', - title: 'Unschedule', - path: '/frontend-fixtures/builds-project/-/jobs/142/unschedule', - buttonTitle: 'Unschedule job', - }, - }, -}; - -export const mockJob = { - id: 4256, - name: 'test', - kind: BUILD_KIND, - status: { - icon: 'status_success', - text: 'passed', - label: 'passed', - tooltip: 'passed', - group: 'success', - detailsPath: '/root/ci-mock/builds/4256', - hasDetails: true, - action: { - icon: 'retry', - title: 'Retry', - path: '/root/ci-mock/builds/4256/retry', - method: 'post', - }, - }, -}; - -export const mockJobWithoutDetails = { - id: 4257, - name: 'job_without_details', - status: { - icon: 'status_success', - text: 'passed', - label: 'passed', - group: 'success', - detailsPath: '/root/ci-mock/builds/4257', - hasDetails: false, - }, -}; - -export const mockJobWithUnauthorizedAction = { - id: 4258, - name: 'stop-environment', - status: { - icon: 'status_manual', - label: 'manual stop action (not allowed)', - tooltip: 'manual action', - group: 'manual', - detailsPath: '/root/ci-mock/builds/4258', - hasDetails: true, - action: null, - }, -}; - -export const triggerJob = { - id: 4259, - name: 'trigger', - kind: BRIDGE_KIND, - status: { - icon: 'status_success', - text: 'passed', - label: 'passed', - group: 'success', - action: null, - }, -}; - -export const triggerJobWithRetryAction = { - ...triggerJob, - status: { - ...triggerJob.status, - action: { - icon: 'retry', - title: RETRY_ACTION_TITLE, - path: '/root/ci-mock/builds/4259/retry', - method: 'post', - }, - }, -}; - -export const mockFailedJob = { - id: 3999, - name: 'failed job', - kind: BUILD_KIND, - status: { - id: 'failed-3999-3999', - icon: 'status_failed', - tooltip: 'failed - (stuck or timeout failure)', - hasDetails: true, - detailsPath: '/root/ci-project/-/jobs/3999', - group: 'failed', - label: 'failed', - action: { - id: 'Ci::BuildPresenter-failed-3999', - buttonTitle: 'Retry this job', - icon: 'retry', - path: '/root/ci-project/-/jobs/3999/retry', - title: 'Retry', - }, - }, -}; diff --git a/spec/frontend/pipelines/graph/stage_column_component_spec.js b/spec/frontend/pipelines/graph/stage_column_component_spec.js deleted file mode 100644 index d4d7f1618c5..00000000000 --- a/spec/frontend/pipelines/graph/stage_column_component_spec.js +++ /dev/null @@ -1,228 +0,0 @@ -import { mount, shallowMount } from '@vue/test-utils'; -import JobItem from '~/pipelines/components/graph/job_item.vue'; -import StageColumnComponent from '~/pipelines/components/graph/stage_column_component.vue'; -import ActionComponent from '~/pipelines/components/jobs_shared/action_component.vue'; - -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, jobs: [mockJob], id: idx, name: `fish-${idx}` }; - }); - -const defaultProps = { - name: 'Fish', - groups: mockGroups, - pipelineId: 159, - userPermissions: { - updatePipeline: true, - }, -}; - -describe('stage column component', () => { - let wrapper; - - 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.findComponent(JobItem); - const findActionComponent = () => wrapper.findComponent(ActionComponent); - - const createComponent = ({ method = shallowMount, props = {} } = {}) => { - wrapper = method(StageColumnComponent, { - propsData: { - ...defaultProps, - ...props, - }, - }); - }; - - describe('when mounted', () => { - beforeEach(() => { - createComponent({ method: mount }); - }); - - it('should render provided title', () => { - expect(findStageColumnTitle().text()).toBe(defaultProps.name); - }); - - it('should render the provided groups', () => { - expect(findAllStageColumnGroups().length).toBe(mockGroups.length); - }); - - it('should emit updateMeasurements event on mount', () => { - expect(wrapper.emitted().updateMeasurements).toHaveLength(1); - }); - }); - - 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('job', () => { - describe('text handling', () => { - beforeEach(() => { - createComponent({ - method: mount, - props: { - groups: [ - { - ...mockJob, - name: '<img src=x onerror=alert(document.domain)>', - jobs: [ - { - id: 4259, - name: '<img src=x onerror=alert(document.domain)>', - status: { - icon: 'status_success', - label: 'success', - tooltip: '<img src=x onerror=alert(document.domain)>', - }, - }, - ], - }, - ], - name: 'test <img src=x onerror=alert(document.domain)>', - }, - }); - }); - - it('escapes name', () => { - expect(findStageColumnTitle().html()).toContain( - 'test <img src=x onerror=alert(document.domain)>', - ); - }); - - it('escapes id', () => { - expect(findStageColumnGroup().attributes('id')).toBe( - 'ci-badge-<img src=x onerror=alert(document.domain)>', - ); - }); - }); - - describe('interactions', () => { - beforeEach(() => { - createComponent({ method: mount }); - }); - - it('emits jobHovered event on mouseenter and mouseleave', async () => { - await findStageColumnGroup().trigger('mouseenter'); - expect(wrapper.emitted().jobHover).toEqual([[defaultProps.groups[0].name]]); - await findStageColumnGroup().trigger('mouseleave'); - expect(wrapper.emitted().jobHover).toEqual([[defaultProps.groups[0].name], ['']]); - }); - }); - }); - - describe('with action', () => { - const defaults = { - 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)>', - }, - jobs: [mockJob], - }, - ], - title: 'test', - hasTriggeredBy: false, - action: { - icon: 'play', - title: 'Play all', - path: 'action', - }, - }; - - it('renders action button if permissions are permitted', () => { - createComponent({ - method: mount, - props: { - ...defaults, - }, - }); - - expect(findActionComponent().exists()).toBe(true); - }); - - it('does not render action button if permissions are not permitted', () => { - createComponent({ - method: mount, - props: { - ...defaults, - userPermissions: { - updatePipeline: false, - }, - }, - }); - - expect(findActionComponent().exists()).toBe(false); - }); - }); - - describe('without action', () => { - beforeEach(() => { - createComponent({ - method: mount, - props: { - 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)>', - }, - jobs: [mockJob], - }, - ], - title: 'test', - hasTriggeredBy: false, - }, - }); - }); - - it('does not render action button', () => { - expect(findActionComponent().exists()).toBe(false); - }); - }); -}); diff --git a/spec/frontend/pipelines/graph_shared/__snapshots__/links_inner_spec.js.snap b/spec/frontend/pipelines/graph_shared/__snapshots__/links_inner_spec.js.snap deleted file mode 100644 index 82206e907ff..00000000000 --- a/spec/frontend/pipelines/graph_shared/__snapshots__/links_inner_spec.js.snap +++ /dev/null @@ -1,30 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Links Inner component with a large number of needs matches snapshot and has expected path 1`] = ` -"<div class=\\"gl-display-flex gl-relative\\" totalgroups=\\"10\\"><svg id=\\"link-svg\\" viewBox=\\"0,0,1019,445\\" width=\\"1019px\\" height=\\"445px\\" class=\\"gl-absolute gl-pointer-events-none\\"> - <path d=\\"M202,118C52,118,52,138,102,138\\" stroke-width=\\"2\\" class=\\"gl-fill-transparent gl-transition-duration-slow gl-transition-timing-function-ease gl-stroke-gray-200\\"></path> - <path d=\\"M202,118C62,118,62,148,112,148\\" stroke-width=\\"2\\" class=\\"gl-fill-transparent gl-transition-duration-slow gl-transition-timing-function-ease gl-stroke-gray-200\\"></path> - <path d=\\"M222,138C72,138,72,158,122,158\\" stroke-width=\\"2\\" class=\\"gl-fill-transparent gl-transition-duration-slow gl-transition-timing-function-ease gl-stroke-gray-200\\"></path> - <path d=\\"M212,128C82,128,82,168,132,168\\" stroke-width=\\"2\\" class=\\"gl-fill-transparent gl-transition-duration-slow gl-transition-timing-function-ease gl-stroke-gray-200\\"></path> - <path d=\\"M232,148C92,148,92,178,142,178\\" stroke-width=\\"2\\" class=\\"gl-fill-transparent gl-transition-duration-slow gl-transition-timing-function-ease gl-stroke-gray-200\\"></path> - </svg> </div>" -`; - -exports[`Links Inner component with a parallel need matches snapshot and has expected path 1`] = ` -"<div class=\\"gl-display-flex gl-relative\\" totalgroups=\\"10\\"><svg id=\\"link-svg\\" viewBox=\\"0,0,1019,445\\" width=\\"1019px\\" height=\\"445px\\" class=\\"gl-absolute gl-pointer-events-none\\"> - <path d=\\"M192,108C32,108,32,118,82,118\\" stroke-width=\\"2\\" class=\\"gl-fill-transparent gl-transition-duration-slow gl-transition-timing-function-ease gl-stroke-gray-200\\"></path> - </svg> </div>" -`; - -exports[`Links Inner component with one need matches snapshot and has expected path 1`] = ` -"<div class=\\"gl-display-flex gl-relative\\" totalgroups=\\"10\\"><svg id=\\"link-svg\\" viewBox=\\"0,0,1019,445\\" width=\\"1019px\\" height=\\"445px\\" class=\\"gl-absolute gl-pointer-events-none\\"> - <path d=\\"M202,118C52,118,52,138,102,138\\" stroke-width=\\"2\\" class=\\"gl-fill-transparent gl-transition-duration-slow gl-transition-timing-function-ease gl-stroke-gray-200\\"></path> - </svg> </div>" -`; - -exports[`Links Inner component with same stage needs matches snapshot and has expected path 1`] = ` -"<div class=\\"gl-display-flex gl-relative\\" totalgroups=\\"10\\"><svg id=\\"link-svg\\" viewBox=\\"0,0,1019,445\\" width=\\"1019px\\" height=\\"445px\\" class=\\"gl-absolute gl-pointer-events-none\\"> - <path d=\\"M192,108C32,108,32,118,82,118\\" stroke-width=\\"2\\" class=\\"gl-fill-transparent gl-transition-duration-slow gl-transition-timing-function-ease gl-stroke-gray-200\\"></path> - <path d=\\"M202,118C42,118,42,128,92,128\\" stroke-width=\\"2\\" class=\\"gl-fill-transparent gl-transition-duration-slow gl-transition-timing-function-ease gl-stroke-gray-200\\"></path> - </svg> </div>" -`; diff --git a/spec/frontend/pipelines/graph_shared/links_inner_spec.js b/spec/frontend/pipelines/graph_shared/links_inner_spec.js deleted file mode 100644 index b4ffd2658fe..00000000000 --- a/spec/frontend/pipelines/graph_shared/links_inner_spec.js +++ /dev/null @@ -1,223 +0,0 @@ -import { shallowMount } from '@vue/test-utils'; -import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures'; -import LinksInner from '~/pipelines/components/graph_shared/links_inner.vue'; -import { parseData } from '~/pipelines/components/parsing_utils'; -import { createJobsHash } from '~/pipelines/utils'; -import { - jobRect, - largePipelineData, - parallelNeedData, - pipelineData, - pipelineDataWithNoNeeds, - rootRect, - sameStageNeeds, -} from '../pipeline_graph/mock_data'; - -describe('Links Inner component', () => { - const containerId = 'pipeline-graph-container'; - const defaultProps = { - containerId, - containerMeasurements: { width: 1019, height: 445 }, - pipelineId: 1, - pipelineData: [], - totalGroups: 10, - }; - - let wrapper; - - const createComponent = (props) => { - const currentPipelineData = props?.pipelineData || defaultProps.pipelineData; - wrapper = shallowMount(LinksInner, { - propsData: { - ...defaultProps, - ...props, - linksData: parseData(currentPipelineData.flatMap(({ groups }) => groups)).links, - }, - }); - }; - - const findLinkSvg = () => wrapper.find('#link-svg'); - const findAllLinksPath = () => findLinkSvg().findAll('path'); - - // We create fixture so that each job has an empty div that represent - // the JobPill in the DOM. Each `JobPill` would have different coordinates, - // so we increment their coordinates on each iteration to simulate different positions. - const setHTMLFixtureLocal = ({ stages }) => { - const jobs = createJobsHash(stages); - const arrayOfJobs = Object.keys(jobs); - - const linksHtmlElements = arrayOfJobs.map((job) => { - return `<div id=${job}-${defaultProps.pipelineId} />`; - }); - - setHTMLFixture(`<div id="${containerId}">${linksHtmlElements.join(' ')}</div>`); - - // We are mocking the clientRect data of each job and the container ID. - jest - .spyOn(document.getElementById(containerId), 'getBoundingClientRect') - .mockImplementation(() => rootRect); - - arrayOfJobs.forEach((job, index) => { - jest - .spyOn( - document.getElementById(`${job}-${defaultProps.pipelineId}`), - 'getBoundingClientRect', - ) - .mockImplementation(() => { - const newValue = 10 * index; - const { left, right, top, bottom, x, y } = jobRect; - return { - ...jobRect, - left: left + newValue, - right: right + newValue, - top: top + newValue, - bottom: bottom + newValue, - x: x + newValue, - y: y + newValue, - }; - }); - }); - }; - - afterEach(() => { - resetHTMLFixture(); - }); - - describe('basic SVG creation', () => { - beforeEach(() => { - createComponent(); - }); - - it('renders an SVG of the right size', () => { - expect(findLinkSvg().exists()).toBe(true); - expect(findLinkSvg().attributes('width')).toBe( - `${defaultProps.containerMeasurements.width}px`, - ); - expect(findLinkSvg().attributes('height')).toBe( - `${defaultProps.containerMeasurements.height}px`, - ); - }); - }); - - describe('no pipeline data', () => { - beforeEach(() => { - createComponent(); - }); - - it('renders the component', () => { - expect(findLinkSvg().exists()).toBe(true); - expect(findAllLinksPath()).toHaveLength(0); - }); - }); - - describe('pipeline data with no needs', () => { - beforeEach(() => { - createComponent({ pipelineData: pipelineDataWithNoNeeds.stages }); - }); - - it('renders no links', () => { - expect(findLinkSvg().exists()).toBe(true); - expect(findAllLinksPath()).toHaveLength(0); - }); - }); - - describe('with one need', () => { - beforeEach(() => { - setHTMLFixtureLocal(pipelineData); - createComponent({ pipelineData: pipelineData.stages }); - }); - - it('renders one link', () => { - expect(findAllLinksPath()).toHaveLength(1); - }); - - it('path does not contain NaN values', () => { - expect(wrapper.html()).not.toContain('NaN'); - }); - - it('matches snapshot and has expected path', () => { - expect(wrapper.html()).toMatchSnapshot(); - }); - }); - - describe('with a parallel need', () => { - beforeEach(() => { - setHTMLFixtureLocal(parallelNeedData); - createComponent({ pipelineData: parallelNeedData.stages }); - }); - - it('renders only one link for all the same parallel jobs', () => { - expect(findAllLinksPath()).toHaveLength(1); - }); - - it('path does not contain NaN values', () => { - expect(wrapper.html()).not.toContain('NaN'); - }); - - it('matches snapshot and has expected path', () => { - expect(wrapper.html()).toMatchSnapshot(); - }); - }); - - describe('with same stage needs', () => { - beforeEach(() => { - setHTMLFixtureLocal(sameStageNeeds); - createComponent({ pipelineData: sameStageNeeds.stages }); - }); - - it('renders the correct number of links', () => { - expect(findAllLinksPath()).toHaveLength(2); - }); - - it('path does not contain NaN values', () => { - expect(wrapper.html()).not.toContain('NaN'); - }); - - it('matches snapshot and has expected path', () => { - expect(wrapper.html()).toMatchSnapshot(); - }); - }); - - describe('with a large number of needs', () => { - beforeEach(() => { - setHTMLFixtureLocal(largePipelineData); - createComponent({ pipelineData: largePipelineData.stages }); - }); - - it('renders the correct number of links', () => { - expect(findAllLinksPath()).toHaveLength(5); - }); - - it('path does not contain NaN values', () => { - expect(wrapper.html()).not.toContain('NaN'); - }); - - it('matches snapshot and has expected path', () => { - expect(wrapper.html()).toMatchSnapshot(); - }); - }); - - describe('interactions', () => { - beforeEach(() => { - setHTMLFixtureLocal(largePipelineData); - createComponent({ pipelineData: largePipelineData.stages }); - }); - - it('highlight needs on hover', async () => { - const firstLink = findAllLinksPath().at(0); - - const defaultColorClass = 'gl-stroke-gray-200'; - const hoverColorClass = 'gl-stroke-blue-400'; - - expect(firstLink.classes(defaultColorClass)).toBe(true); - expect(firstLink.classes(hoverColorClass)).toBe(false); - - // Because there is a watcher, we need to set the props after the component - // has mounted. - await wrapper.setProps({ highlightedJob: 'test_1' }); - - expect(firstLink.classes(defaultColorClass)).toBe(false); - expect(firstLink.classes(hoverColorClass)).toBe(true); - }); - }); -}); diff --git a/spec/frontend/pipelines/graph_shared/links_layer_spec.js b/spec/frontend/pipelines/graph_shared/links_layer_spec.js deleted file mode 100644 index 88ba84c395a..00000000000 --- a/spec/frontend/pipelines/graph_shared/links_layer_spec.js +++ /dev/null @@ -1,85 +0,0 @@ -import { shallowMount } from '@vue/test-utils'; -import mockPipelineResponse from 'test_fixtures/pipelines/pipeline_details.json'; -import LinksInner from '~/pipelines/components/graph_shared/links_inner.vue'; -import LinksLayer from '~/pipelines/components/graph_shared/links_layer.vue'; - -import { generateResponse } from '../graph/mock_data'; - -describe('links layer component', () => { - let wrapper; - - const findLinksInner = () => wrapper.findComponent(LinksInner); - - const pipeline = generateResponse(mockPipelineResponse, 'root/fungi-xoxo'); - const containerId = `pipeline-links-container-${pipeline.id}`; - const slotContent = "<div>Ceci n'est pas un graphique</div>"; - - const defaultProps = { - containerId, - containerMeasurements: { width: 400, height: 400 }, - pipelineId: pipeline.id, - pipelineData: pipeline.stages, - showLinks: false, - }; - - const createComponent = ({ mountFn = shallowMount, props = {} } = {}) => { - wrapper = mountFn(LinksLayer, { - propsData: { - ...defaultProps, - ...props, - }, - slots: { - default: slotContent, - }, - stubs: { - 'links-inner': true, - }, - }); - }; - - describe('with show links off', () => { - beforeEach(() => { - createComponent(); - }); - - it('renders the default slot', () => { - expect(wrapper.html()).toContain(slotContent); - }); - - it('does not render inner links component', () => { - expect(findLinksInner().exists()).toBe(false); - }); - }); - - describe('with show links on', () => { - beforeEach(() => { - createComponent({ - props: { - showLinks: true, - }, - }); - }); - - it('renders the default slot', () => { - expect(wrapper.html()).toContain(slotContent); - }); - - it('renders the inner links component', () => { - expect(findLinksInner().exists()).toBe(true); - }); - }); - - describe('with width or height measurement at 0', () => { - beforeEach(() => { - createComponent({ props: { containerMeasurements: { width: 0, height: 100 } } }); - }); - - it('renders the default slot', () => { - expect(wrapper.html()).toContain(slotContent); - }); - - it('does not render the inner links component', () => { - expect(findLinksInner().exists()).toBe(false); - }); - }); -}); diff --git a/spec/frontend/pipelines/linked_pipelines_mock.json b/spec/frontend/pipelines/linked_pipelines_mock.json deleted file mode 100644 index a68283032d2..00000000000 --- a/spec/frontend/pipelines/linked_pipelines_mock.json +++ /dev/null @@ -1,3569 +0,0 @@ -{ - "id": 23211253, - "user": { - "id": 3585, - "name": "Achilleas Pipinellis", - "username": "axil", - "state": "active", - "avatar_url": "https://assets.gitlab-static.net/uploads/-/system/user/avatar/3585/avatar.png", - "web_url": "https://gitlab.com/axil", - "status_tooltip_html": "<span class=\"user-status-emoji has-tooltip\" title=\"I like pizza\" data-html=\"true\" data-placement=\"top\"><gl-emoji title=\"slice of pizza\" data-name=\"pizza\" data-unicode-version=\"6.0\">🍕</gl-emoji></span>", - "path": "/axil" - }, - "active": false, - "coverage": null, - "source": "push", - "created_at": "2018-06-05T11:31:30.452Z", - "updated_at": "2018-10-31T16:35:31.305Z", - "path": "/gitlab-org/gitlab-runner/pipelines/23211253", - "flags": { - "latest": false, - "stuck": false, - "auto_devops": false, - "merge_request": false, - "yaml_errors": false, - "retryable": false, - "cancelable": false, - "failure_reason": false - }, - "details": { - "status": { - "icon": "status_success", - "text": "passed", - "label": "passed", - "group": "success", - "tooltip": "passed", - "has_details": true, - "details_path": "/gitlab-org/gitlab-runner/pipelines/23211253", - "illustration": null, - "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png" - }, - "duration": 53, - "finished_at": "2018-10-31T16:35:31.299Z", - "stages": [ - { - "name": "prebuild", - "title": "prebuild: passed", - "groups": [ - { - "name": "review-docs-deploy", - "size": 1, - "status": { - "icon": "status_success", - "text": "passed", - "label": "manual play action", - "group": "success", - "tooltip": "passed", - "has_details": true, - "details_path": "/gitlab-org/gitlab-runner/-/jobs/72469032", - "illustration": { - "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg", - "size": "svg-394", - "title": "This job requires a manual action", - "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments" - }, - "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png", - "action": { - "icon": "play", - "title": "Play", - "path": "/gitlab-org/gitlab-runner/-/jobs/72469032/play", - "method": "post", - "button_title": "Run job" - } - }, - "jobs": [ - { - "id": 72469032, - "name": "review-docs-deploy", - "started": "2018-10-31T16:34:58.778Z", - "archived": false, - "build_path": "/gitlab-org/gitlab-runner/-/jobs/72469032", - "retry_path": "/gitlab-org/gitlab-runner/-/jobs/72469032/retry", - "play_path": "/gitlab-org/gitlab-runner/-/jobs/72469032/play", - "playable": true, - "scheduled": false, - "created_at": "2018-06-05T11:31:30.495Z", - "updated_at": "2018-10-31T16:35:31.251Z", - "status": { - "icon": "status_success", - "text": "passed", - "label": "manual play action", - "group": "success", - "tooltip": "passed", - "has_details": true, - "details_path": "/gitlab-org/gitlab-runner/-/jobs/72469032", - "illustration": { - "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg", - "size": "svg-394", - "title": "This job requires a manual action", - "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments" - }, - "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png", - "action": { - "icon": "play", - "title": "Play", - "path": "/gitlab-org/gitlab-runner/-/jobs/72469032/play", - "method": "post", - "button_title": "Run job" - } - } - } - ] - } - ], - "status": { - "icon": "status_success", - "text": "passed", - "label": "passed", - "group": "success", - "tooltip": "passed", - "has_details": true, - "details_path": "/gitlab-org/gitlab-runner/pipelines/23211253#prebuild", - "illustration": null, - "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png" - }, - "path": "/gitlab-org/gitlab-runner/pipelines/23211253#prebuild", - "dropdown_path": "/gitlab-org/gitlab-runner/pipelines/23211253/stage.json?stage=prebuild" - }, - { - "name": "test", - "title": "test: passed", - "groups": [ - { - "name": "docs check links", - "size": 1, - "status": { - "icon": "status_success", - "text": "passed", - "label": "passed", - "group": "success", - "tooltip": "passed", - "has_details": true, - "details_path": "/gitlab-org/gitlab-runner/-/jobs/72469033", - "illustration": { - "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg", - "size": "svg-430", - "title": "This job does not have a trace." - }, - "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png", - "action": { - "icon": "retry", - "title": "Retry", - "path": "/gitlab-org/gitlab-runner/-/jobs/72469033/retry", - "method": "post", - "button_title": "Retry this job" - } - }, - "jobs": [ - { - "id": 72469033, - "name": "docs check links", - "started": "2018-06-05T11:31:33.240Z", - "archived": false, - "build_path": "/gitlab-org/gitlab-runner/-/jobs/72469033", - "retry_path": "/gitlab-org/gitlab-runner/-/jobs/72469033/retry", - "playable": false, - "scheduled": false, - "created_at": "2018-06-05T11:31:30.627Z", - "updated_at": "2018-06-05T11:31:54.363Z", - "status": { - "icon": "status_success", - "text": "passed", - "label": "passed", - "group": "success", - "tooltip": "passed", - "has_details": true, - "details_path": "/gitlab-org/gitlab-runner/-/jobs/72469033", - "illustration": { - "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg", - "size": "svg-430", - "title": "This job does not have a trace." - }, - "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png", - "action": { - "icon": "retry", - "title": "Retry", - "path": "/gitlab-org/gitlab-runner/-/jobs/72469033/retry", - "method": "post", - "button_title": "Retry this job" - } - } - } - ] - } - ], - "status": { - "icon": "status_success", - "text": "passed", - "label": "passed", - "group": "success", - "tooltip": "passed", - "has_details": true, - "details_path": "/gitlab-org/gitlab-runner/pipelines/23211253#test", - "illustration": null, - "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png" - }, - "path": "/gitlab-org/gitlab-runner/pipelines/23211253#test", - "dropdown_path": "/gitlab-org/gitlab-runner/pipelines/23211253/stage.json?stage=test" - }, - { - "name": "cleanup", - "title": "cleanup: skipped", - "groups": [ - { - "name": "review-docs-cleanup", - "size": 1, - "status": { - "icon": "status_manual", - "text": "manual", - "label": "manual stop action", - "group": "manual", - "tooltip": "manual action", - "has_details": true, - "details_path": "/gitlab-org/gitlab-runner/-/jobs/72469034", - "illustration": { - "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg", - "size": "svg-394", - "title": "This job requires a manual action", - "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments" - }, - "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png", - "action": { - "icon": "stop", - "title": "Stop", - "path": "/gitlab-org/gitlab-runner/-/jobs/72469034/play", - "method": "post", - "button_title": "Stop this environment" - } - }, - "jobs": [ - { - "id": 72469034, - "name": "review-docs-cleanup", - "started": null, - "archived": false, - "build_path": "/gitlab-org/gitlab-runner/-/jobs/72469034", - "play_path": "/gitlab-org/gitlab-runner/-/jobs/72469034/play", - "playable": true, - "scheduled": false, - "created_at": "2018-06-05T11:31:30.760Z", - "updated_at": "2018-06-05T11:31:56.037Z", - "status": { - "icon": "status_manual", - "text": "manual", - "label": "manual stop action", - "group": "manual", - "tooltip": "manual action", - "has_details": true, - "details_path": "/gitlab-org/gitlab-runner/-/jobs/72469034", - "illustration": { - "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg", - "size": "svg-394", - "title": "This job requires a manual action", - "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments" - }, - "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png", - "action": { - "icon": "stop", - "title": "Stop", - "path": "/gitlab-org/gitlab-runner/-/jobs/72469034/play", - "method": "post", - "button_title": "Stop this environment" - } - } - } - ] - } - ], - "status": { - "icon": "status_skipped", - "text": "skipped", - "label": "skipped", - "group": "skipped", - "tooltip": "skipped", - "has_details": true, - "details_path": "/gitlab-org/gitlab-runner/pipelines/23211253#cleanup", - "illustration": null, - "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png" - }, - "path": "/gitlab-org/gitlab-runner/pipelines/23211253#cleanup", - "dropdown_path": "/gitlab-org/gitlab-runner/pipelines/23211253/stage.json?stage=cleanup" - } - ], - "artifacts": [ - - ], - "manual_actions": [ - { - "name": "review-docs-cleanup", - "path": "/gitlab-org/gitlab-runner/-/jobs/72469034/play", - "playable": true, - "scheduled": false - }, - { - "name": "review-docs-deploy", - "path": "/gitlab-org/gitlab-runner/-/jobs/72469032/play", - "playable": true, - "scheduled": false - } - ], - "scheduled_actions": [ - - ] - }, - "ref": { - "name": "docs/add-development-guide-to-readme", - "path": "/gitlab-org/gitlab-runner/commits/docs/add-development-guide-to-readme", - "tag": false, - "branch": true, - "merge_request": false - }, - "commit": { - "id": "8083eb0a920572214d0dccedd7981f05d535ad46", - "short_id": "8083eb0a", - "title": "Add link to development guide in readme", - "created_at": "2018-06-05T11:30:48.000Z", - "parent_ids": [ - "1d7cf79b5a1a2121b9474ac20d61c1b8f621289d" - ], - "message": "Add link to development guide in readme\n\nCloses https://gitlab.com/gitlab-org/gitlab-runner/issues/3122\n", - "author_name": "Achilleas Pipinellis", - "author_email": "axil@gitlab.com", - "authored_date": "2018-06-05T11:30:48.000Z", - "committer_name": "Achilleas Pipinellis", - "committer_email": "axil@gitlab.com", - "committed_date": "2018-06-05T11:30:48.000Z", - "author": { - "id": 3585, - "name": "Achilleas Pipinellis", - "username": "axil", - "state": "active", - "avatar_url": "https://assets.gitlab-static.net/uploads/-/system/user/avatar/3585/avatar.png", - "web_url": "https://gitlab.com/axil", - "status_tooltip_html": null, - "path": "/axil" - }, - "author_gravatar_url": "https://secure.gravatar.com/avatar/1d37af00eec153a8333a4ce18e9aea41?s=80&d=identicon", - "commit_url": "https://gitlab.com/gitlab-org/gitlab-runner/commit/8083eb0a920572214d0dccedd7981f05d535ad46", - "commit_path": "/gitlab-org/gitlab-runner/commit/8083eb0a920572214d0dccedd7981f05d535ad46" - }, - "project": { - "id": 1794617 - }, - "triggered_by": { - "id": 12, - "user": { - "id": 376774, - "name": "Alessio Caiazza", - "username": "nolith", - "state": "active", - "avatar_url": "https://assets.gitlab-static.net/uploads/-/system/user/avatar/376774/avatar.png", - "web_url": "https://gitlab.com/nolith", - "status_tooltip_html": null, - "path": "/nolith" - }, - "active": false, - "coverage": null, - "source": "pipeline", - "path": "/gitlab-com/gitlab-docs/pipelines/34993051", - "details": { - "status": { - "icon": "status_failed", - "text": "failed", - "label": "failed", - "group": "failed", - "tooltip": "failed", - "has_details": true, - "details_path": "/gitlab-com/gitlab-docs/pipelines/34993051", - "illustration": null, - "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png" - }, - "duration": 118, - "finished_at": "2018-10-31T16:41:40.615Z", - "stages": [ - { - "name": "build-images", - "title": "build-images: skipped", - "groups": [ - { - "name": "image:bootstrap", - "size": 1, - "status": { - "icon": "status_manual", - "text": "manual", - "label": "manual play action", - "group": "manual", - "tooltip": "manual action", - "has_details": true, - "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982853", - "illustration": { - "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg", - "size": "svg-394", - "title": "This job requires a manual action", - "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments" - }, - "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png", - "action": { - "icon": "play", - "title": "Play", - "path": "/gitlab-com/gitlab-docs/-/jobs/114982853/play", - "method": "post", - "button_title": "Run job" - } - }, - "jobs": [ - { - "id": 11421321982853, - "name": "image:bootstrap", - "started": null, - "archived": false, - "build_path": "/gitlab-com/gitlab-docs/-/jobs/114982853", - "play_path": "/gitlab-com/gitlab-docs/-/jobs/114982853/play", - "playable": true, - "scheduled": false, - "created_at": "2018-10-31T16:35:23.704Z", - "updated_at": "2018-10-31T16:35:24.118Z", - "status": { - "icon": "status_manual", - "text": "manual", - "label": "manual play action", - "group": "manual", - "tooltip": "manual action", - "has_details": true, - "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982853", - "illustration": { - "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg", - "size": "svg-394", - "title": "This job requires a manual action", - "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments" - }, - "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png", - "action": { - "icon": "play", - "title": "Play", - "path": "/gitlab-com/gitlab-docs/-/jobs/114982853/play", - "method": "post", - "button_title": "Run job" - } - } - } - ] - }, - { - "name": "image:builder-onbuild", - "size": 1, - "status": { - "icon": "status_manual", - "text": "manual", - "label": "manual play action", - "group": "manual", - "tooltip": "manual action", - "has_details": true, - "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982854", - "illustration": { - "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg", - "size": "svg-394", - "title": "This job requires a manual action", - "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments" - }, - "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png", - "action": { - "icon": "play", - "title": "Play", - "path": "/gitlab-com/gitlab-docs/-/jobs/114982854/play", - "method": "post", - "button_title": "Run job" - } - }, - "jobs": [ - { - "id": 1149822131854, - "name": "image:builder-onbuild", - "started": null, - "archived": false, - "build_path": "/gitlab-com/gitlab-docs/-/jobs/114982854", - "play_path": "/gitlab-com/gitlab-docs/-/jobs/114982854/play", - "playable": true, - "scheduled": false, - "created_at": "2018-10-31T16:35:23.728Z", - "updated_at": "2018-10-31T16:35:24.070Z", - "status": { - "icon": "status_manual", - "text": "manual", - "label": "manual play action", - "group": "manual", - "tooltip": "manual action", - "has_details": true, - "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982854", - "illustration": { - "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg", - "size": "svg-394", - "title": "This job requires a manual action", - "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments" - }, - "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png", - "action": { - "icon": "play", - "title": "Play", - "path": "/gitlab-com/gitlab-docs/-/jobs/114982854/play", - "method": "post", - "button_title": "Run job" - } - } - } - ] - }, - { - "name": "image:nginx-onbuild", - "size": 1, - "status": { - "icon": "status_manual", - "text": "manual", - "label": "manual play action", - "group": "manual", - "tooltip": "manual action", - "has_details": true, - "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982855", - "illustration": { - "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg", - "size": "svg-394", - "title": "This job requires a manual action", - "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments" - }, - "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png", - "action": { - "icon": "play", - "title": "Play", - "path": "/gitlab-com/gitlab-docs/-/jobs/114982855/play", - "method": "post", - "button_title": "Run job" - } - }, - "jobs": [ - { - "id": 11498285523424, - "name": "image:nginx-onbuild", - "started": null, - "archived": false, - "build_path": "/gitlab-com/gitlab-docs/-/jobs/114982855", - "play_path": "/gitlab-com/gitlab-docs/-/jobs/114982855/play", - "playable": true, - "scheduled": false, - "created_at": "2018-10-31T16:35:23.753Z", - "updated_at": "2018-10-31T16:35:24.033Z", - "status": { - "icon": "status_manual", - "text": "manual", - "label": "manual play action", - "group": "manual", - "tooltip": "manual action", - "has_details": true, - "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982855", - "illustration": { - "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg", - "size": "svg-394", - "title": "This job requires a manual action", - "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments" - }, - "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png", - "action": { - "icon": "play", - "title": "Play", - "path": "/gitlab-com/gitlab-docs/-/jobs/114982855/play", - "method": "post", - "button_title": "Run job" - } - } - } - ] - } - ], - "status": { - "icon": "status_skipped", - "text": "skipped", - "label": "skipped", - "group": "skipped", - "tooltip": "skipped", - "has_details": true, - "details_path": "/gitlab-com/gitlab-docs/pipelines/34993051#build-images", - "illustration": null, - "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png" - }, - "path": "/gitlab-com/gitlab-docs/pipelines/34993051#build-images", - "dropdown_path": "/gitlab-com/gitlab-docs/pipelines/34993051/stage.json?stage=build-images" - }, - { - "name": "build", - "title": "build: failed", - "groups": [ - { - "name": "compile_dev", - "size": 1, - "status": { - "icon": "status_failed", - "text": "failed", - "label": "failed", - "group": "failed", - "tooltip": "failed - (script failure)", - "has_details": true, - "details_path": "/gitlab-com/gitlab-docs/-/jobs/114984694", - "illustration": { - "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg", - "size": "svg-430", - "title": "This job does not have a trace." - }, - "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png", - "action": { - "icon": "retry", - "title": "Retry", - "path": "/h5bp/html5-boilerplate/-/jobs/528/retry", - "method": "post", - "button_title": "Retry this job" - } - }, - "jobs": [ - { - "id": 1149846949786, - "name": "compile_dev", - "started": "2018-10-31T16:39:41.598Z", - "archived": false, - "build_path": "/gitlab-com/gitlab-docs/-/jobs/114984694", - "retry_path": "/gitlab-com/gitlab-docs/-/jobs/114984694/retry", - "playable": false, - "scheduled": false, - "created_at": "2018-10-31T16:39:41.138Z", - "updated_at": "2018-10-31T16:41:40.072Z", - "status": { - "icon": "status_failed", - "text": "failed", - "label": "failed", - "group": "failed", - "tooltip": "failed - (script failure)", - "has_details": true, - "details_path": "/gitlab-com/gitlab-docs/-/jobs/114984694", - "illustration": { - "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg", - "size": "svg-430", - "title": "This job does not have a trace." - }, - "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png", - "action": { - "icon": "retry", - "title": "Retry", - "path": "/h5bp/html5-boilerplate/-/jobs/528/retry", - "method": "post", - "button_title": "Retry this job" - } - }, - "recoverable": false - } - ] - } - ], - "status": { - "icon": "status_failed", - "text": "failed", - "label": "failed", - "group": "failed", - "tooltip": "failed", - "has_details": true, - "details_path": "/gitlab-com/gitlab-docs/pipelines/34993051#build", - "illustration": null, - "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png" - }, - "path": "/gitlab-com/gitlab-docs/pipelines/34993051#build", - "dropdown_path": "/gitlab-com/gitlab-docs/pipelines/34993051/stage.json?stage=build" - }, - { - "name": "deploy", - "title": "deploy: skipped", - "groups": [ - { - "name": "review", - "size": 1, - "status": { - "icon": "status_skipped", - "text": "skipped", - "label": "skipped", - "group": "skipped", - "tooltip": "skipped", - "has_details": true, - "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982857", - "illustration": { - "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg", - "size": "svg-430", - "title": "This job has been skipped" - }, - "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png" - }, - "jobs": [ - { - "id": 11498282342357, - "name": "review", - "started": null, - "archived": false, - "build_path": "/gitlab-com/gitlab-docs/-/jobs/114982857", - "playable": false, - "scheduled": false, - "created_at": "2018-10-31T16:35:23.805Z", - "updated_at": "2018-10-31T16:41:40.569Z", - "status": { - "icon": "status_skipped", - "text": "skipped", - "label": "skipped", - "group": "skipped", - "tooltip": "skipped", - "has_details": true, - "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982857", - "illustration": { - "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg", - "size": "svg-430", - "title": "This job has been skipped" - }, - "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png" - } - } - ] - }, - { - "name": "review_stop", - "size": 1, - "status": { - "icon": "status_skipped", - "text": "skipped", - "label": "skipped", - "group": "skipped", - "tooltip": "skipped", - "has_details": true, - "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982858", - "illustration": { - "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg", - "size": "svg-430", - "title": "This job has been skipped" - }, - "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png" - }, - "jobs": [ - { - "id": 114982858, - "name": "review_stop", - "started": null, - "archived": false, - "build_path": "/gitlab-com/gitlab-docs/-/jobs/114982858", - "playable": false, - "scheduled": false, - "created_at": "2018-10-31T16:35:23.840Z", - "updated_at": "2018-10-31T16:41:40.480Z", - "status": { - "icon": "status_skipped", - "text": "skipped", - "label": "skipped", - "group": "skipped", - "tooltip": "skipped", - "has_details": true, - "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982858", - "illustration": { - "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg", - "size": "svg-430", - "title": "This job has been skipped" - }, - "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png" - } - } - ] - } - ], - "status": { - "icon": "status_skipped", - "text": "skipped", - "label": "skipped", - "group": "skipped", - "tooltip": "skipped", - "has_details": true, - "details_path": "/gitlab-com/gitlab-docs/pipelines/34993051#deploy", - "illustration": null, - "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png" - }, - "path": "/gitlab-com/gitlab-docs/pipelines/34993051#deploy", - "dropdown_path": "/gitlab-com/gitlab-docs/pipelines/34993051/stage.json?stage=deploy" - } - ], - "artifacts": [ - - ], - "manual_actions": [ - { - "name": "image:bootstrap", - "path": "/gitlab-com/gitlab-docs/-/jobs/114982853/play", - "playable": true, - "scheduled": false - }, - { - "name": "image:builder-onbuild", - "path": "/gitlab-com/gitlab-docs/-/jobs/114982854/play", - "playable": true, - "scheduled": false - }, - { - "name": "image:nginx-onbuild", - "path": "/gitlab-com/gitlab-docs/-/jobs/114982855/play", - "playable": true, - "scheduled": false - }, - { - "name": "review_stop", - "path": "/gitlab-com/gitlab-docs/-/jobs/114982858/play", - "playable": false, - "scheduled": false - } - ], - "scheduled_actions": [ - - ] - }, - "project": { - "id": 1794617, - "name": "Test", - "full_path": "/gitlab-com/gitlab-docs", - "full_name": "GitLab.com / GitLab Docs" - }, - "triggered_by": { - "id": 349932310342451, - "user": { - "id": 376774, - "name": "Alessio Caiazza", - "username": "nolith", - "state": "active", - "avatar_url": "https://assets.gitlab-static.net/uploads/-/system/user/avatar/376774/avatar.png", - "web_url": "https://gitlab.com/nolith", - "status_tooltip_html": null, - "path": "/nolith" - }, - "active": false, - "coverage": null, - "source": "pipeline", - "path": "/gitlab-com/gitlab-docs/pipelines/34993051", - "details": { - "status": { - "icon": "status_failed", - "text": "failed", - "label": "failed", - "group": "failed", - "tooltip": "failed", - "has_details": true, - "details_path": "/gitlab-com/gitlab-docs/pipelines/34993051", - "illustration": null, - "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png" - }, - "duration": 118, - "finished_at": "2018-10-31T16:41:40.615Z", - "stages": [ - { - "name": "build-images", - "title": "build-images: skipped", - "groups": [ - { - "name": "image:bootstrap", - "size": 1, - "status": { - "icon": "status_manual", - "text": "manual", - "label": "manual play action", - "group": "manual", - "tooltip": "manual action", - "has_details": true, - "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982853", - "illustration": { - "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg", - "size": "svg-394", - "title": "This job requires a manual action", - "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments" - }, - "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png", - "action": { - "icon": "play", - "title": "Play", - "path": "/gitlab-com/gitlab-docs/-/jobs/114982853/play", - "method": "post", - "button_title": "Run job" - } - }, - "jobs": [ - { - "id": 11421321982853, - "name": "image:bootstrap", - "started": null, - "archived": false, - "build_path": "/gitlab-com/gitlab-docs/-/jobs/114982853", - "play_path": "/gitlab-com/gitlab-docs/-/jobs/114982853/play", - "playable": true, - "scheduled": false, - "created_at": "2018-10-31T16:35:23.704Z", - "updated_at": "2018-10-31T16:35:24.118Z", - "status": { - "icon": "status_manual", - "text": "manual", - "label": "manual play action", - "group": "manual", - "tooltip": "manual action", - "has_details": true, - "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982853", - "illustration": { - "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg", - "size": "svg-394", - "title": "This job requires a manual action", - "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments" - }, - "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png", - "action": { - "icon": "play", - "title": "Play", - "path": "/gitlab-com/gitlab-docs/-/jobs/114982853/play", - "method": "post", - "button_title": "Run job" - } - } - } - ] - }, - { - "name": "image:builder-onbuild", - "size": 1, - "status": { - "icon": "status_manual", - "text": "manual", - "label": "manual play action", - "group": "manual", - "tooltip": "manual action", - "has_details": true, - "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982854", - "illustration": { - "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg", - "size": "svg-394", - "title": "This job requires a manual action", - "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments" - }, - "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png", - "action": { - "icon": "play", - "title": "Play", - "path": "/gitlab-com/gitlab-docs/-/jobs/114982854/play", - "method": "post", - "button_title": "Run job" - } - }, - "jobs": [ - { - "id": 1149822131854, - "name": "image:builder-onbuild", - "started": null, - "archived": false, - "build_path": "/gitlab-com/gitlab-docs/-/jobs/114982854", - "play_path": "/gitlab-com/gitlab-docs/-/jobs/114982854/play", - "playable": true, - "scheduled": false, - "created_at": "2018-10-31T16:35:23.728Z", - "updated_at": "2018-10-31T16:35:24.070Z", - "status": { - "icon": "status_manual", - "text": "manual", - "label": "manual play action", - "group": "manual", - "tooltip": "manual action", - "has_details": true, - "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982854", - "illustration": { - "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg", - "size": "svg-394", - "title": "This job requires a manual action", - "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments" - }, - "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png", - "action": { - "icon": "play", - "title": "Play", - "path": "/gitlab-com/gitlab-docs/-/jobs/114982854/play", - "method": "post", - "button_title": "Run job" - } - } - } - ] - }, - { - "name": "image:nginx-onbuild", - "size": 1, - "status": { - "icon": "status_manual", - "text": "manual", - "label": "manual play action", - "group": "manual", - "tooltip": "manual action", - "has_details": true, - "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982855", - "illustration": { - "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg", - "size": "svg-394", - "title": "This job requires a manual action", - "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments" - }, - "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png", - "action": { - "icon": "play", - "title": "Play", - "path": "/gitlab-com/gitlab-docs/-/jobs/114982855/play", - "method": "post", - "button_title": "Run job" - } - }, - "jobs": [ - { - "id": 11498285523424, - "name": "image:nginx-onbuild", - "started": null, - "archived": false, - "build_path": "/gitlab-com/gitlab-docs/-/jobs/114982855", - "play_path": "/gitlab-com/gitlab-docs/-/jobs/114982855/play", - "playable": true, - "scheduled": false, - "created_at": "2018-10-31T16:35:23.753Z", - "updated_at": "2018-10-31T16:35:24.033Z", - "status": { - "icon": "status_manual", - "text": "manual", - "label": "manual play action", - "group": "manual", - "tooltip": "manual action", - "has_details": true, - "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982855", - "illustration": { - "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg", - "size": "svg-394", - "title": "This job requires a manual action", - "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments" - }, - "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png", - "action": { - "icon": "play", - "title": "Play", - "path": "/gitlab-com/gitlab-docs/-/jobs/114982855/play", - "method": "post", - "button_title": "Run job" - } - } - } - ] - } - ], - "status": { - "icon": "status_skipped", - "text": "skipped", - "label": "skipped", - "group": "skipped", - "tooltip": "skipped", - "has_details": true, - "details_path": "/gitlab-com/gitlab-docs/pipelines/34993051#build-images", - "illustration": null, - "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png" - }, - "path": "/gitlab-com/gitlab-docs/pipelines/34993051#build-images", - "dropdown_path": "/gitlab-com/gitlab-docs/pipelines/34993051/stage.json?stage=build-images" - }, - { - "name": "build", - "title": "build: failed", - "groups": [ - { - "name": "compile_dev", - "size": 1, - "status": { - "icon": "status_failed", - "text": "failed", - "label": "failed", - "group": "failed", - "tooltip": "failed - (script failure)", - "has_details": true, - "details_path": "/gitlab-com/gitlab-docs/-/jobs/114984694", - "illustration": { - "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg", - "size": "svg-430", - "title": "This job does not have a trace." - }, - "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png", - "action": { - "icon": "retry", - "title": "Retry", - "path": "/gitlab-com/gitlab-docs/-/jobs/114984694/retry", - "method": "post", - "button_title": "Retry this job" - } - }, - "jobs": [ - { - "id": 1149846949786, - "name": "compile_dev", - "started": "2018-10-31T16:39:41.598Z", - "archived": false, - "build_path": "/gitlab-com/gitlab-docs/-/jobs/114984694", - "retry_path": "/gitlab-com/gitlab-docs/-/jobs/114984694/retry", - "playable": false, - "scheduled": false, - "created_at": "2018-10-31T16:39:41.138Z", - "updated_at": "2018-10-31T16:41:40.072Z", - "status": { - "icon": "status_failed", - "text": "failed", - "label": "failed", - "group": "failed", - "tooltip": "failed - (script failure)", - "has_details": true, - "details_path": "/gitlab-com/gitlab-docs/-/jobs/114984694", - "illustration": { - "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg", - "size": "svg-430", - "title": "This job does not have a trace." - }, - "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png", - "action": { - "icon": "retry", - "title": "Retry", - "path": "/gitlab-com/gitlab-docs/-/jobs/114984694/retry", - "method": "post", - "button_title": "Retry this job" - } - }, - "recoverable": false - } - ] - } - ], - "status": { - "icon": "status_failed", - "text": "failed", - "label": "failed", - "group": "failed", - "tooltip": "failed", - "has_details": true, - "details_path": "/gitlab-com/gitlab-docs/pipelines/34993051#build", - "illustration": null, - "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png" - }, - "path": "/gitlab-com/gitlab-docs/pipelines/34993051#build", - "dropdown_path": "/gitlab-com/gitlab-docs/pipelines/34993051/stage.json?stage=build" - }, - { - "name": "deploy", - "title": "deploy: skipped", - "groups": [ - { - "name": "review", - "size": 1, - "status": { - "icon": "status_skipped", - "text": "skipped", - "label": "skipped", - "group": "skipped", - "tooltip": "skipped", - "has_details": true, - "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982857", - "illustration": { - "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg", - "size": "svg-430", - "title": "This job has been skipped" - }, - "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png" - }, - "jobs": [ - { - "id": 11498282342357, - "name": "review", - "started": null, - "archived": false, - "build_path": "/gitlab-com/gitlab-docs/-/jobs/114982857", - "playable": false, - "scheduled": false, - "created_at": "2018-10-31T16:35:23.805Z", - "updated_at": "2018-10-31T16:41:40.569Z", - "status": { - "icon": "status_skipped", - "text": "skipped", - "label": "skipped", - "group": "skipped", - "tooltip": "skipped", - "has_details": true, - "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982857", - "illustration": { - "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg", - "size": "svg-430", - "title": "This job has been skipped" - }, - "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png" - } - } - ] - }, - { - "name": "review_stop", - "size": 1, - "status": { - "icon": "status_skipped", - "text": "skipped", - "label": "skipped", - "group": "skipped", - "tooltip": "skipped", - "has_details": true, - "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982858", - "illustration": { - "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg", - "size": "svg-430", - "title": "This job has been skipped" - }, - "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png" - }, - "jobs": [ - { - "id": 114982858, - "name": "review_stop", - "started": null, - "archived": false, - "build_path": "/gitlab-com/gitlab-docs/-/jobs/114982858", - "playable": false, - "scheduled": false, - "created_at": "2018-10-31T16:35:23.840Z", - "updated_at": "2018-10-31T16:41:40.480Z", - "status": { - "icon": "status_skipped", - "text": "skipped", - "label": "skipped", - "group": "skipped", - "tooltip": "skipped", - "has_details": true, - "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982858", - "illustration": { - "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg", - "size": "svg-430", - "title": "This job has been skipped" - }, - "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png" - } - } - ] - } - ], - "status": { - "icon": "status_skipped", - "text": "skipped", - "label": "skipped", - "group": "skipped", - "tooltip": "skipped", - "has_details": true, - "details_path": "/gitlab-com/gitlab-docs/pipelines/34993051#deploy", - "illustration": null, - "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png" - }, - "path": "/gitlab-com/gitlab-docs/pipelines/34993051#deploy", - "dropdown_path": "/gitlab-com/gitlab-docs/pipelines/34993051/stage.json?stage=deploy" - } - ], - "artifacts": [ - - ], - "manual_actions": [ - { - "name": "image:bootstrap", - "path": "/gitlab-com/gitlab-docs/-/jobs/114982853/play", - "playable": true, - "scheduled": false - }, - { - "name": "image:builder-onbuild", - "path": "/gitlab-com/gitlab-docs/-/jobs/114982854/play", - "playable": true, - "scheduled": false - }, - { - "name": "image:nginx-onbuild", - "path": "/gitlab-com/gitlab-docs/-/jobs/114982855/play", - "playable": true, - "scheduled": false - }, - { - "name": "review_stop", - "path": "/gitlab-com/gitlab-docs/-/jobs/114982858/play", - "playable": false, - "scheduled": false - } - ], - "scheduled_actions": [ - - ] - }, - "project": { - "id": 1794617, - "name": "GitLab Docs", - "full_path": "/gitlab-com/gitlab-docs", - "full_name": "GitLab.com / GitLab Docs" - } - }, - "triggered": [ - - ] - }, - "triggered": [ - { - "id": 34993051, - "user": { - "id": 376774, - "name": "Alessio Caiazza", - "username": "nolith", - "state": "active", - "avatar_url": "https://assets.gitlab-static.net/uploads/-/system/user/avatar/376774/avatar.png", - "web_url": "https://gitlab.com/nolith", - "status_tooltip_html": null, - "path": "/nolith" - }, - "active": false, - "coverage": null, - "source": "pipeline", - "path": "/gitlab-com/gitlab-docs/pipelines/34993051", - "details": { - "status": { - "icon": "status_failed", - "text": "failed", - "label": "failed", - "group": "failed", - "tooltip": "failed", - "has_details": true, - "details_path": "/gitlab-com/gitlab-docs/pipelines/34993051", - "illustration": null, - "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png" - }, - "duration": 118, - "finished_at": "2018-10-31T16:41:40.615Z", - "stages": [ - { - "name": "build-images", - "title": "build-images: skipped", - "groups": [ - { - "name": "image:bootstrap", - "size": 1, - "status": { - "icon": "status_manual", - "text": "manual", - "label": "manual play action", - "group": "manual", - "tooltip": "manual action", - "has_details": true, - "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982853", - "illustration": { - "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg", - "size": "svg-394", - "title": "This job requires a manual action", - "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments" - }, - "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png", - "action": { - "icon": "play", - "title": "Play", - "path": "/gitlab-com/gitlab-docs/-/jobs/114982853/play", - "method": "post", - "button_title": "Run job" - } - }, - "jobs": [ - { - "id": 114982853, - "name": "image:bootstrap", - "started": null, - "archived": false, - "build_path": "/gitlab-com/gitlab-docs/-/jobs/114982853", - "play_path": "/gitlab-com/gitlab-docs/-/jobs/114982853/play", - "playable": true, - "scheduled": false, - "created_at": "2018-10-31T16:35:23.704Z", - "updated_at": "2018-10-31T16:35:24.118Z", - "status": { - "icon": "status_manual", - "text": "manual", - "label": "manual play action", - "group": "manual", - "tooltip": "manual action", - "has_details": true, - "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982853", - "illustration": { - "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg", - "size": "svg-394", - "title": "This job requires a manual action", - "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments" - }, - "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png", - "action": { - "icon": "play", - "title": "Play", - "path": "/gitlab-com/gitlab-docs/-/jobs/114982853/play", - "method": "post", - "button_title": "Run job" - } - } - } - ] - }, - { - "name": "image:builder-onbuild", - "size": 1, - "status": { - "icon": "status_manual", - "text": "manual", - "label": "manual play action", - "group": "manual", - "tooltip": "manual action", - "has_details": true, - "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982854", - "illustration": { - "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg", - "size": "svg-394", - "title": "This job requires a manual action", - "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments" - }, - "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png", - "action": { - "icon": "play", - "title": "Play", - "path": "/gitlab-com/gitlab-docs/-/jobs/114982854/play", - "method": "post", - "button_title": "Run job" - } - }, - "jobs": [ - { - "id": 114982854, - "name": "image:builder-onbuild", - "started": null, - "archived": false, - "build_path": "/gitlab-com/gitlab-docs/-/jobs/114982854", - "play_path": "/gitlab-com/gitlab-docs/-/jobs/114982854/play", - "playable": true, - "scheduled": false, - "created_at": "2018-10-31T16:35:23.728Z", - "updated_at": "2018-10-31T16:35:24.070Z", - "status": { - "icon": "status_manual", - "text": "manual", - "label": "manual play action", - "group": "manual", - "tooltip": "manual action", - "has_details": true, - "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982854", - "illustration": { - "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg", - "size": "svg-394", - "title": "This job requires a manual action", - "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments" - }, - "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png", - "action": { - "icon": "play", - "title": "Play", - "path": "/gitlab-com/gitlab-docs/-/jobs/114982854/play", - "method": "post", - "button_title": "Run job" - } - } - } - ] - }, - { - "name": "image:nginx-onbuild", - "size": 1, - "status": { - "icon": "status_manual", - "text": "manual", - "label": "manual play action", - "group": "manual", - "tooltip": "manual action", - "has_details": true, - "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982855", - "illustration": { - "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg", - "size": "svg-394", - "title": "This job requires a manual action", - "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments" - }, - "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png", - "action": { - "icon": "play", - "title": "Play", - "path": "/gitlab-com/gitlab-docs/-/jobs/114982855/play", - "method": "post", - "button_title": "Run job" - } - }, - "jobs": [ - { - "id": 114982855, - "name": "image:nginx-onbuild", - "started": null, - "archived": false, - "build_path": "/gitlab-com/gitlab-docs/-/jobs/114982855", - "play_path": "/gitlab-com/gitlab-docs/-/jobs/114982855/play", - "playable": true, - "scheduled": false, - "created_at": "2018-10-31T16:35:23.753Z", - "updated_at": "2018-10-31T16:35:24.033Z", - "status": { - "icon": "status_manual", - "text": "manual", - "label": "manual play action", - "group": "manual", - "tooltip": "manual action", - "has_details": true, - "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982855", - "illustration": { - "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg", - "size": "svg-394", - "title": "This job requires a manual action", - "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments" - }, - "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png", - "action": { - "icon": "play", - "title": "Play", - "path": "/gitlab-com/gitlab-docs/-/jobs/114982855/play", - "method": "post", - "button_title": "Run job" - } - } - } - ] - } - ], - "status": { - "icon": "status_skipped", - "text": "skipped", - "label": "skipped", - "group": "skipped", - "tooltip": "skipped", - "has_details": true, - "details_path": "/gitlab-com/gitlab-docs/pipelines/34993051#build-images", - "illustration": null, - "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png" - }, - "path": "/gitlab-com/gitlab-docs/pipelines/34993051#build-images", - "dropdown_path": "/gitlab-com/gitlab-docs/pipelines/34993051/stage.json?stage=build-images" - }, - { - "name": "build", - "title": "build: failed", - "groups": [ - { - "name": "compile_dev", - "size": 1, - "status": { - "icon": "status_failed", - "text": "failed", - "label": "failed", - "group": "failed", - "tooltip": "failed - (script failure)", - "has_details": true, - "details_path": "/gitlab-com/gitlab-docs/-/jobs/114984694", - "illustration": { - "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg", - "size": "svg-430", - "title": "This job does not have a trace." - }, - "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png", - "action": { - "icon": "retry", - "title": "Retry", - "path": "/h5bp/html5-boilerplate/-/jobs/528/retry", - "method": "post", - "button_title": "Retry this job" - } - }, - "jobs": [ - { - "id": 114984694, - "name": "compile_dev", - "started": "2018-10-31T16:39:41.598Z", - "archived": false, - "build_path": "/gitlab-com/gitlab-docs/-/jobs/114984694", - "retry_path": "/gitlab-com/gitlab-docs/-/jobs/114984694/retry", - "playable": false, - "scheduled": false, - "created_at": "2018-10-31T16:39:41.138Z", - "updated_at": "2018-10-31T16:41:40.072Z", - "status": { - "icon": "status_failed", - "text": "failed", - "label": "failed", - "group": "failed", - "tooltip": "failed - (script failure)", - "has_details": true, - "details_path": "/gitlab-com/gitlab-docs/-/jobs/114984694", - "illustration": { - "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg", - "size": "svg-430", - "title": "This job does not have a trace." - }, - "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png", - "action": { - "icon": "retry", - "title": "Retry", - "path": "/h5bp/html5-boilerplate/-/jobs/528/retry", - "method": "post", - "button_title": "Retry this job" - } - }, - "recoverable": false - } - ] - } - ], - "status": { - "icon": "status_failed", - "text": "failed", - "label": "failed", - "group": "failed", - "tooltip": "failed", - "has_details": true, - "details_path": "/gitlab-com/gitlab-docs/pipelines/34993051#build", - "illustration": null, - "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png" - }, - "path": "/gitlab-com/gitlab-docs/pipelines/34993051#build", - "dropdown_path": "/gitlab-com/gitlab-docs/pipelines/34993051/stage.json?stage=build" - }, - { - "name": "deploy", - "title": "deploy: skipped", - "groups": [ - { - "name": "review", - "size": 1, - "status": { - "icon": "status_skipped", - "text": "skipped", - "label": "skipped", - "group": "skipped", - "tooltip": "skipped", - "has_details": true, - "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982857", - "illustration": { - "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg", - "size": "svg-430", - "title": "This job has been skipped" - }, - "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png" - }, - "jobs": [ - { - "id": 114982857, - "name": "review", - "started": null, - "archived": false, - "build_path": "/gitlab-com/gitlab-docs/-/jobs/114982857", - "playable": false, - "scheduled": false, - "created_at": "2018-10-31T16:35:23.805Z", - "updated_at": "2018-10-31T16:41:40.569Z", - "status": { - "icon": "status_skipped", - "text": "skipped", - "label": "skipped", - "group": "skipped", - "tooltip": "skipped", - "has_details": true, - "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982857", - "illustration": { - "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg", - "size": "svg-430", - "title": "This job has been skipped" - }, - "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png" - } - } - ] - }, - { - "name": "review_stop", - "size": 1, - "status": { - "icon": "status_skipped", - "text": "skipped", - "label": "skipped", - "group": "skipped", - "tooltip": "skipped", - "has_details": true, - "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982858", - "illustration": { - "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg", - "size": "svg-430", - "title": "This job has been skipped" - }, - "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png" - }, - "jobs": [ - { - "id": 114982858, - "name": "review_stop", - "started": null, - "archived": false, - "build_path": "/gitlab-com/gitlab-docs/-/jobs/114982858", - "playable": false, - "scheduled": false, - "created_at": "2018-10-31T16:35:23.840Z", - "updated_at": "2018-10-31T16:41:40.480Z", - "status": { - "icon": "status_skipped", - "text": "skipped", - "label": "skipped", - "group": "skipped", - "tooltip": "skipped", - "has_details": true, - "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982858", - "illustration": { - "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg", - "size": "svg-430", - "title": "This job has been skipped" - }, - "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png" - } - } - ] - } - ], - "status": { - "icon": "status_skipped", - "text": "skipped", - "label": "skipped", - "group": "skipped", - "tooltip": "skipped", - "has_details": true, - "details_path": "/gitlab-com/gitlab-docs/pipelines/34993051#deploy", - "illustration": null, - "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png" - }, - "path": "/gitlab-com/gitlab-docs/pipelines/34993051#deploy", - "dropdown_path": "/gitlab-com/gitlab-docs/pipelines/34993051/stage.json?stage=deploy" - } - ], - "artifacts": [ - - ], - "manual_actions": [ - { - "name": "image:bootstrap", - "path": "/gitlab-com/gitlab-docs/-/jobs/114982853/play", - "playable": true, - "scheduled": false - }, - { - "name": "image:builder-onbuild", - "path": "/gitlab-com/gitlab-docs/-/jobs/114982854/play", - "playable": true, - "scheduled": false - }, - { - "name": "image:nginx-onbuild", - "path": "/gitlab-com/gitlab-docs/-/jobs/114982855/play", - "playable": true, - "scheduled": false - }, - { - "name": "review_stop", - "path": "/gitlab-com/gitlab-docs/-/jobs/114982858/play", - "playable": false, - "scheduled": false - } - ], - "scheduled_actions": [ - - ] - }, - "project": { - "id": 1794617, - "name": "GitLab Docs", - "full_path": "/gitlab-com/gitlab-docs", - "full_name": "GitLab.com / GitLab Docs" - }, - "triggered": [ - { - } - ] - }, - { - "id": 34993052, - "user": { - "id": 376774, - "name": "Alessio Caiazza", - "username": "nolith", - "state": "active", - "avatar_url": "https://assets.gitlab-static.net/uploads/-/system/user/avatar/376774/avatar.png", - "web_url": "https://gitlab.com/nolith", - "status_tooltip_html": null, - "path": "/nolith" - }, - "active": false, - "coverage": null, - "source": "pipeline", - "path": "/gitlab-com/gitlab-docs/pipelines/34993051", - "details": { - "status": { - "icon": "status_failed", - "text": "failed", - "label": "failed", - "group": "failed", - "tooltip": "failed", - "has_details": true, - "details_path": "/gitlab-com/gitlab-docs/pipelines/34993051", - "illustration": null, - "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png" - }, - "duration": 118, - "finished_at": "2018-10-31T16:41:40.615Z", - "stages": [ - { - "name": "build-images", - "title": "build-images: skipped", - "groups": [ - { - "name": "image:bootstrap", - "size": 1, - "status": { - "icon": "status_manual", - "text": "manual", - "label": "manual play action", - "group": "manual", - "tooltip": "manual action", - "has_details": true, - "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982853", - "illustration": { - "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg", - "size": "svg-394", - "title": "This job requires a manual action", - "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments" - }, - "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png", - "action": { - "icon": "play", - "title": "Play", - "path": "/gitlab-com/gitlab-docs/-/jobs/114982853/play", - "method": "post", - "button_title": "Run job" - } - }, - "jobs": [ - { - "id": 114982853, - "name": "image:bootstrap", - "started": null, - "archived": false, - "build_path": "/gitlab-com/gitlab-docs/-/jobs/114982853", - "play_path": "/gitlab-com/gitlab-docs/-/jobs/114982853/play", - "playable": true, - "scheduled": false, - "created_at": "2018-10-31T16:35:23.704Z", - "updated_at": "2018-10-31T16:35:24.118Z", - "status": { - "icon": "status_manual", - "text": "manual", - "label": "manual play action", - "group": "manual", - "tooltip": "manual action", - "has_details": true, - "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982853", - "illustration": { - "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg", - "size": "svg-394", - "title": "This job requires a manual action", - "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments" - }, - "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png", - "action": { - "icon": "play", - "title": "Play", - "path": "/gitlab-com/gitlab-docs/-/jobs/114982853/play", - "method": "post", - "button_title": "Run job" - } - } - } - ] - }, - { - "name": "image:builder-onbuild", - "size": 1, - "status": { - "icon": "status_manual", - "text": "manual", - "label": "manual play action", - "group": "manual", - "tooltip": "manual action", - "has_details": true, - "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982854", - "illustration": { - "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg", - "size": "svg-394", - "title": "This job requires a manual action", - "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments" - }, - "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png", - "action": { - "icon": "play", - "title": "Play", - "path": "/gitlab-com/gitlab-docs/-/jobs/114982854/play", - "method": "post", - "button_title": "Run job" - } - }, - "jobs": [ - { - "id": 114982854, - "name": "image:builder-onbuild", - "started": null, - "archived": false, - "build_path": "/gitlab-com/gitlab-docs/-/jobs/114982854", - "play_path": "/gitlab-com/gitlab-docs/-/jobs/114982854/play", - "playable": true, - "scheduled": false, - "created_at": "2018-10-31T16:35:23.728Z", - "updated_at": "2018-10-31T16:35:24.070Z", - "status": { - "icon": "status_manual", - "text": "manual", - "label": "manual play action", - "group": "manual", - "tooltip": "manual action", - "has_details": true, - "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982854", - "illustration": { - "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg", - "size": "svg-394", - "title": "This job requires a manual action", - "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments" - }, - "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png", - "action": { - "icon": "play", - "title": "Play", - "path": "/gitlab-com/gitlab-docs/-/jobs/114982854/play", - "method": "post", - "button_title": "Run job" - } - } - } - ] - }, - { - "name": "image:nginx-onbuild", - "size": 1, - "status": { - "icon": "status_manual", - "text": "manual", - "label": "manual play action", - "group": "manual", - "tooltip": "manual action", - "has_details": true, - "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982855", - "illustration": { - "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg", - "size": "svg-394", - "title": "This job requires a manual action", - "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments" - }, - "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png", - "action": { - "icon": "play", - "title": "Play", - "path": "/gitlab-com/gitlab-docs/-/jobs/114982855/play", - "method": "post", - "button_title": "Run job" - } - }, - "jobs": [ - { - "id": 1224982855, - "name": "image:nginx-onbuild", - "started": null, - "archived": false, - "build_path": "/gitlab-com/gitlab-docs/-/jobs/114982855", - "play_path": "/gitlab-com/gitlab-docs/-/jobs/114982855/play", - "playable": true, - "scheduled": false, - "created_at": "2018-10-31T16:35:23.753Z", - "updated_at": "2018-10-31T16:35:24.033Z", - "status": { - "icon": "status_manual", - "text": "manual", - "label": "manual play action", - "group": "manual", - "tooltip": "manual action", - "has_details": true, - "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982855", - "illustration": { - "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg", - "size": "svg-394", - "title": "This job requires a manual action", - "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments" - }, - "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png", - "action": { - "icon": "play", - "title": "Play", - "path": "/gitlab-com/gitlab-docs/-/jobs/114982855/play", - "method": "post", - "button_title": "Run job" - } - } - } - ] - } - ], - "status": { - "icon": "status_skipped", - "text": "skipped", - "label": "skipped", - "group": "skipped", - "tooltip": "skipped", - "has_details": true, - "details_path": "/gitlab-com/gitlab-docs/pipelines/34993051#build-images", - "illustration": null, - "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png" - }, - "path": "/gitlab-com/gitlab-docs/pipelines/34993051#build-images", - "dropdown_path": "/gitlab-com/gitlab-docs/pipelines/34993051/stage.json?stage=build-images" - }, - { - "name": "build", - "title": "build: failed", - "groups": [ - { - "name": "compile_dev", - "size": 1, - "status": { - "icon": "status_failed", - "text": "failed", - "label": "failed", - "group": "failed", - "tooltip": "failed - (script failure)", - "has_details": true, - "details_path": "/gitlab-com/gitlab-docs/-/jobs/114984694", - "illustration": { - "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg", - "size": "svg-430", - "title": "This job does not have a trace." - }, - "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png", - "action": { - "icon": "retry", - "title": "Retry", - "path": "/gitlab-com/gitlab-docs/-/jobs/114984694/retry", - "method": "post", - "button_title": "Retry this job" - } - }, - "jobs": [ - { - "id": 1123984694, - "name": "compile_dev", - "started": "2018-10-31T16:39:41.598Z", - "archived": false, - "build_path": "/gitlab-com/gitlab-docs/-/jobs/114984694", - "retry_path": "/gitlab-com/gitlab-docs/-/jobs/114984694/retry", - "playable": false, - "scheduled": false, - "created_at": "2018-10-31T16:39:41.138Z", - "updated_at": "2018-10-31T16:41:40.072Z", - "status": { - "icon": "status_failed", - "text": "failed", - "label": "failed", - "group": "failed", - "tooltip": "failed - (script failure)", - "has_details": true, - "details_path": "/gitlab-com/gitlab-docs/-/jobs/114984694", - "illustration": { - "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg", - "size": "svg-430", - "title": "This job does not have a trace." - }, - "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png", - "action": { - "icon": "retry", - "title": "Retry", - "path": "/gitlab-com/gitlab-docs/-/jobs/114984694/retry", - "method": "post", - "button_title": "Retry this job" - } - }, - "recoverable": false - } - ] - } - ], - "status": { - "icon": "status_failed", - "text": "failed", - "label": "failed", - "group": "failed", - "tooltip": "failed", - "has_details": true, - "details_path": "/gitlab-com/gitlab-docs/pipelines/34993051#build", - "illustration": null, - "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png" - }, - "path": "/gitlab-com/gitlab-docs/pipelines/34993051#build", - "dropdown_path": "/gitlab-com/gitlab-docs/pipelines/34993051/stage.json?stage=build" - }, - { - "name": "deploy", - "title": "deploy: skipped", - "groups": [ - { - "name": "review", - "size": 1, - "status": { - "icon": "status_skipped", - "text": "skipped", - "label": "skipped", - "group": "skipped", - "tooltip": "skipped", - "has_details": true, - "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982857", - "illustration": { - "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg", - "size": "svg-430", - "title": "This job has been skipped" - }, - "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png" - }, - "jobs": [ - { - "id": 1143232982857, - "name": "review", - "started": null, - "archived": false, - "build_path": "/gitlab-com/gitlab-docs/-/jobs/114982857", - "playable": false, - "scheduled": false, - "created_at": "2018-10-31T16:35:23.805Z", - "updated_at": "2018-10-31T16:41:40.569Z", - "status": { - "icon": "status_skipped", - "text": "skipped", - "label": "skipped", - "group": "skipped", - "tooltip": "skipped", - "has_details": true, - "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982857", - "illustration": { - "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg", - "size": "svg-430", - "title": "This job has been skipped" - }, - "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png" - } - } - ] - }, - { - "name": "review_stop", - "size": 1, - "status": { - "icon": "status_skipped", - "text": "skipped", - "label": "skipped", - "group": "skipped", - "tooltip": "skipped", - "has_details": true, - "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982858", - "illustration": { - "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg", - "size": "svg-430", - "title": "This job has been skipped" - }, - "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png" - }, - "jobs": [ - { - "id": 114921313182858, - "name": "review_stop", - "started": null, - "archived": false, - "build_path": "/gitlab-com/gitlab-docs/-/jobs/114982858", - "playable": false, - "scheduled": false, - "created_at": "2018-10-31T16:35:23.840Z", - "updated_at": "2018-10-31T16:41:40.480Z", - "status": { - "icon": "status_skipped", - "text": "skipped", - "label": "skipped", - "group": "skipped", - "tooltip": "skipped", - "has_details": true, - "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982858", - "illustration": { - "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg", - "size": "svg-430", - "title": "This job has been skipped" - }, - "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png" - } - } - ] - } - ], - "status": { - "icon": "status_skipped", - "text": "skipped", - "label": "skipped", - "group": "skipped", - "tooltip": "skipped", - "has_details": true, - "details_path": "/gitlab-com/gitlab-docs/pipelines/34993051#deploy", - "illustration": null, - "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png" - }, - "path": "/gitlab-com/gitlab-docs/pipelines/34993051#deploy", - "dropdown_path": "/gitlab-com/gitlab-docs/pipelines/34993051/stage.json?stage=deploy" - } - ], - "artifacts": [ - - ], - "manual_actions": [ - { - "name": "image:bootstrap", - "path": "/gitlab-com/gitlab-docs/-/jobs/114982853/play", - "playable": true, - "scheduled": false - }, - { - "name": "image:builder-onbuild", - "path": "/gitlab-com/gitlab-docs/-/jobs/114982854/play", - "playable": true, - "scheduled": false - }, - { - "name": "image:nginx-onbuild", - "path": "/gitlab-com/gitlab-docs/-/jobs/114982855/play", - "playable": true, - "scheduled": false - }, - { - "name": "review_stop", - "path": "/gitlab-com/gitlab-docs/-/jobs/114982858/play", - "playable": false, - "scheduled": false - } - ], - "scheduled_actions": [ - - ] - }, - "project": { - "id": 1794617, - "name": "GitLab Docs", - "full_path": "/gitlab-com/gitlab-docs", - "full_name": "GitLab.com / GitLab Docs" - }, - "triggered": [ - { - "id": 26, - "user": null, - "active": false, - "coverage": null, - "source": "push", - "created_at": "2019-01-06T17:48:37.599Z", - "updated_at": "2019-01-06T17:48:38.371Z", - "path": "/h5bp/html5-boilerplate/pipelines/26", - "flags": { - "latest": true, - "stuck": false, - "auto_devops": false, - "merge_request": false, - "yaml_errors": false, - "retryable": true, - "cancelable": false, - "failure_reason": false - }, - "details": { - "status": { - "icon": "status_warning", - "text": "passed", - "label": "passed with warnings", - "group": "success-with-warnings", - "tooltip": "passed", - "has_details": true, - "details_path": "/h5bp/html5-boilerplate/pipelines/26", - "illustration": null, - "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png" - }, - "duration": null, - "finished_at": "2019-01-06T17:48:38.370Z", - "stages": [ - { - "name": "build", - "title": "build: passed", - "groups": [ - { - "name": "build:linux", - "size": 1, - "status": { - "icon": "status_success", - "text": "passed", - "label": "passed", - "group": "success", - "tooltip": "passed", - "has_details": true, - "details_path": "/h5bp/html5-boilerplate/-/jobs/526", - "illustration": { - "image": "/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg", - "size": "svg-430", - "title": "This job does not have a trace." - }, - "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png", - "action": { - "icon": "retry", - "title": "Retry", - "path": "/h5bp/html5-boilerplate/-/jobs/526/retry", - "method": "post", - "button_title": "Retry this job" - } - }, - "jobs": [ - { - "id": 526, - "name": "build:linux", - "started": "2019-01-06T08:48:20.236Z", - "archived": false, - "build_path": "/h5bp/html5-boilerplate/-/jobs/526", - "retry_path": "/h5bp/html5-boilerplate/-/jobs/526/retry", - "playable": false, - "scheduled": false, - "created_at": "2019-01-06T17:48:37.806Z", - "updated_at": "2019-01-06T17:48:37.806Z", - "status": { - "icon": "status_success", - "text": "passed", - "label": "passed", - "group": "success", - "tooltip": "passed", - "has_details": true, - "details_path": "/h5bp/html5-boilerplate/-/jobs/526", - "illustration": { - "image": "/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg", - "size": "svg-430", - "title": "This job does not have a trace." - }, - "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png", - "action": { - "icon": "retry", - "title": "Retry", - "path": "/h5bp/html5-boilerplate/-/jobs/526/retry", - "method": "post", - "button_title": "Retry this job" - } - } - } - ] - }, - { - "name": "build:osx", - "size": 1, - "status": { - "icon": "status_success", - "text": "passed", - "label": "passed", - "group": "success", - "tooltip": "passed", - "has_details": true, - "details_path": "/h5bp/html5-boilerplate/-/jobs/527", - "illustration": { - "image": "/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg", - "size": "svg-430", - "title": "This job does not have a trace." - }, - "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png", - "action": { - "icon": "retry", - "title": "Retry", - "path": "/h5bp/html5-boilerplate/-/jobs/527/retry", - "method": "post", - "button_title": "Retry this job" - } - }, - "jobs": [ - { - "id": 527, - "name": "build:osx", - "started": "2019-01-06T07:48:20.237Z", - "archived": false, - "build_path": "/h5bp/html5-boilerplate/-/jobs/527", - "retry_path": "/h5bp/html5-boilerplate/-/jobs/527/retry", - "playable": false, - "scheduled": false, - "created_at": "2019-01-06T17:48:37.846Z", - "updated_at": "2019-01-06T17:48:37.846Z", - "status": { - "icon": "status_success", - "text": "passed", - "label": "passed", - "group": "success", - "tooltip": "passed", - "has_details": true, - "details_path": "/h5bp/html5-boilerplate/-/jobs/527", - "illustration": { - "image": "/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg", - "size": "svg-430", - "title": "This job does not have a trace." - }, - "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png", - "action": { - "icon": "retry", - "title": "Retry", - "path": "/h5bp/html5-boilerplate/-/jobs/527/retry", - "method": "post", - "button_title": "Retry this job" - } - } - } - ] - } - ], - "status": { - "icon": "status_success", - "text": "passed", - "label": "passed", - "group": "success", - "tooltip": "passed", - "has_details": true, - "details_path": "/h5bp/html5-boilerplate/pipelines/26#build", - "illustration": null, - "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png" - }, - "path": "/h5bp/html5-boilerplate/pipelines/26#build", - "dropdown_path": "/h5bp/html5-boilerplate/pipelines/26/stage.json?stage=build" - }, - { - "name": "test", - "title": "test: passed with warnings", - "groups": [ - { - "name": "jenkins", - "size": 1, - "status": { - "icon": "status_success", - "text": "passed", - "label": null, - "group": "success", - "tooltip": null, - "has_details": false, - "details_path": null, - "illustration": null, - "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png" - }, - "jobs": [ - { - "id": 546, - "name": "jenkins", - "started": "2019-01-06T11:48:20.237Z", - "archived": false, - "build_path": "/h5bp/html5-boilerplate/-/jobs/546", - "playable": false, - "scheduled": false, - "created_at": "2019-01-06T17:48:38.359Z", - "updated_at": "2019-01-06T17:48:38.359Z", - "status": { - "icon": "status_success", - "text": "passed", - "label": null, - "group": "success", - "tooltip": null, - "has_details": false, - "details_path": null, - "illustration": null, - "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png" - } - } - ] - }, - { - "name": "rspec:linux", - "size": 3, - "status": { - "icon": "status_success", - "text": "passed", - "label": "passed", - "group": "success", - "tooltip": "passed", - "has_details": false, - "details_path": null, - "illustration": null, - "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png" - }, - "jobs": [ - { - "id": 528, - "name": "rspec:linux 0 3", - "started": "2019-01-06T09:48:20.237Z", - "archived": false, - "build_path": "/h5bp/html5-boilerplate/-/jobs/528", - "retry_path": "/h5bp/html5-boilerplate/-/jobs/528/retry", - "playable": false, - "scheduled": false, - "created_at": "2019-01-06T17:48:37.885Z", - "updated_at": "2019-01-06T17:48:37.885Z", - "status": { - "icon": "status_success", - "text": "passed", - "label": "passed", - "group": "success", - "tooltip": "passed", - "has_details": true, - "details_path": "/h5bp/html5-boilerplate/-/jobs/528", - "illustration": { - "image": "/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg", - "size": "svg-430", - "title": "This job does not have a trace." - }, - "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png", - "action": { - "icon": "retry", - "title": "Retry", - "path": "/h5bp/html5-boilerplate/-/jobs/528/retry", - "method": "post", - "button_title": "Retry this job" - } - } - }, - { - "id": 529, - "name": "rspec:linux 1 3", - "started": "2019-01-06T09:48:20.237Z", - "archived": false, - "build_path": "/h5bp/html5-boilerplate/-/jobs/529", - "retry_path": "/h5bp/html5-boilerplate/-/jobs/529/retry", - "playable": false, - "scheduled": false, - "created_at": "2019-01-06T17:48:37.907Z", - "updated_at": "2019-01-06T17:48:37.907Z", - "status": { - "icon": "status_success", - "text": "passed", - "label": "passed", - "group": "success", - "tooltip": "passed", - "has_details": true, - "details_path": "/h5bp/html5-boilerplate/-/jobs/529", - "illustration": { - "image": "/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg", - "size": "svg-430", - "title": "This job does not have a trace." - }, - "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png", - "action": { - "icon": "retry", - "title": "Retry", - "path": "/h5bp/html5-boilerplate/-/jobs/529/retry", - "method": "post", - "button_title": "Retry this job" - } - } - }, - { - "id": 530, - "name": "rspec:linux 2 3", - "started": "2019-01-06T09:48:20.237Z", - "archived": false, - "build_path": "/h5bp/html5-boilerplate/-/jobs/530", - "retry_path": "/h5bp/html5-boilerplate/-/jobs/530/retry", - "playable": false, - "scheduled": false, - "created_at": "2019-01-06T17:48:37.927Z", - "updated_at": "2019-01-06T17:48:37.927Z", - "status": { - "icon": "status_success", - "text": "passed", - "label": "passed", - "group": "success", - "tooltip": "passed", - "has_details": true, - "details_path": "/h5bp/html5-boilerplate/-/jobs/530", - "illustration": { - "image": "/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg", - "size": "svg-430", - "title": "This job does not have a trace." - }, - "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png", - "action": { - "icon": "retry", - "title": "Retry", - "path": "/h5bp/html5-boilerplate/-/jobs/530/retry", - "method": "post", - "button_title": "Retry this job" - } - } - } - ] - }, - { - "name": "rspec:osx", - "size": 1, - "status": { - "icon": "status_success", - "text": "passed", - "label": "passed", - "group": "success", - "tooltip": "passed", - "has_details": true, - "details_path": "/h5bp/html5-boilerplate/-/jobs/535", - "illustration": { - "image": "/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg", - "size": "svg-430", - "title": "This job does not have a trace." - }, - "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png", - "action": { - "icon": "retry", - "title": "Retry", - "path": "/h5bp/html5-boilerplate/-/jobs/535/retry", - "method": "post", - "button_title": "Retry this job" - } - }, - "jobs": [ - { - "id": 535, - "name": "rspec:osx", - "started": "2019-01-06T09:48:20.237Z", - "archived": false, - "build_path": "/h5bp/html5-boilerplate/-/jobs/535", - "retry_path": "/h5bp/html5-boilerplate/-/jobs/535/retry", - "playable": false, - "scheduled": false, - "created_at": "2019-01-06T17:48:38.018Z", - "updated_at": "2019-01-06T17:48:38.018Z", - "status": { - "icon": "status_success", - "text": "passed", - "label": "passed", - "group": "success", - "tooltip": "passed", - "has_details": true, - "details_path": "/h5bp/html5-boilerplate/-/jobs/535", - "illustration": { - "image": "/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg", - "size": "svg-430", - "title": "This job does not have a trace." - }, - "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png", - "action": { - "icon": "retry", - "title": "Retry", - "path": "/h5bp/html5-boilerplate/-/jobs/535/retry", - "method": "post", - "button_title": "Retry this job" - } - } - } - ] - }, - { - "name": "rspec:windows", - "size": 3, - "status": { - "icon": "status_success", - "text": "passed", - "label": "passed", - "group": "success", - "tooltip": "passed", - "has_details": false, - "details_path": null, - "illustration": null, - "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png" - }, - "jobs": [ - { - "id": 531, - "name": "rspec:windows 0 3", - "started": "2019-01-06T09:48:20.237Z", - "archived": false, - "build_path": "/h5bp/html5-boilerplate/-/jobs/531", - "retry_path": "/h5bp/html5-boilerplate/-/jobs/531/retry", - "playable": false, - "scheduled": false, - "created_at": "2019-01-06T17:48:37.944Z", - "updated_at": "2019-01-06T17:48:37.944Z", - "status": { - "icon": "status_success", - "text": "passed", - "label": "passed", - "group": "success", - "tooltip": "passed", - "has_details": true, - "details_path": "/h5bp/html5-boilerplate/-/jobs/531", - "illustration": { - "image": "/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg", - "size": "svg-430", - "title": "This job does not have a trace." - }, - "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png", - "action": { - "icon": "retry", - "title": "Retry", - "path": "/h5bp/html5-boilerplate/-/jobs/531/retry", - "method": "post", - "button_title": "Retry this job" - } - } - }, - { - "id": 532, - "name": "rspec:windows 1 3", - "started": "2019-01-06T09:48:20.237Z", - "archived": false, - "build_path": "/h5bp/html5-boilerplate/-/jobs/532", - "retry_path": "/h5bp/html5-boilerplate/-/jobs/532/retry", - "playable": false, - "scheduled": false, - "created_at": "2019-01-06T17:48:37.962Z", - "updated_at": "2019-01-06T17:48:37.962Z", - "status": { - "icon": "status_success", - "text": "passed", - "label": "passed", - "group": "success", - "tooltip": "passed", - "has_details": true, - "details_path": "/h5bp/html5-boilerplate/-/jobs/532", - "illustration": { - "image": "/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg", - "size": "svg-430", - "title": "This job does not have a trace." - }, - "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png", - "action": { - "icon": "retry", - "title": "Retry", - "path": "/h5bp/html5-boilerplate/-/jobs/532/retry", - "method": "post", - "button_title": "Retry this job" - } - } - }, - { - "id": 534, - "name": "rspec:windows 2 3", - "started": "2019-01-06T09:48:20.237Z", - "archived": false, - "build_path": "/h5bp/html5-boilerplate/-/jobs/534", - "retry_path": "/h5bp/html5-boilerplate/-/jobs/534/retry", - "playable": false, - "scheduled": false, - "created_at": "2019-01-06T17:48:37.999Z", - "updated_at": "2019-01-06T17:48:37.999Z", - "status": { - "icon": "status_success", - "text": "passed", - "label": "passed", - "group": "success", - "tooltip": "passed", - "has_details": true, - "details_path": "/h5bp/html5-boilerplate/-/jobs/534", - "illustration": { - "image": "/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg", - "size": "svg-430", - "title": "This job does not have a trace." - }, - "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png", - "action": { - "icon": "retry", - "title": "Retry", - "path": "/h5bp/html5-boilerplate/-/jobs/534/retry", - "method": "post", - "button_title": "Retry this job" - } - } - } - ] - }, - { - "name": "spinach:linux", - "size": 1, - "status": { - "icon": "status_success", - "text": "passed", - "label": "passed", - "group": "success", - "tooltip": "passed", - "has_details": true, - "details_path": "/h5bp/html5-boilerplate/-/jobs/536", - "illustration": { - "image": "/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg", - "size": "svg-430", - "title": "This job does not have a trace." - }, - "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png", - "action": { - "icon": "retry", - "title": "Retry", - "path": "/h5bp/html5-boilerplate/-/jobs/536/retry", - "method": "post", - "button_title": "Retry this job" - } - }, - "jobs": [ - { - "id": 536, - "name": "spinach:linux", - "started": "2019-01-06T09:48:20.237Z", - "archived": false, - "build_path": "/h5bp/html5-boilerplate/-/jobs/536", - "retry_path": "/h5bp/html5-boilerplate/-/jobs/536/retry", - "playable": false, - "scheduled": false, - "created_at": "2019-01-06T17:48:38.050Z", - "updated_at": "2019-01-06T17:48:38.050Z", - "status": { - "icon": "status_success", - "text": "passed", - "label": "passed", - "group": "success", - "tooltip": "passed", - "has_details": true, - "details_path": "/h5bp/html5-boilerplate/-/jobs/536", - "illustration": { - "image": "/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg", - "size": "svg-430", - "title": "This job does not have a trace." - }, - "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png", - "action": { - "icon": "retry", - "title": "Retry", - "path": "/h5bp/html5-boilerplate/-/jobs/536/retry", - "method": "post", - "button_title": "Retry this job" - } - } - } - ] - }, - { - "name": "spinach:osx", - "size": 1, - "status": { - "icon": "status_warning", - "text": "failed", - "label": "failed (allowed to fail)", - "group": "failed-with-warnings", - "tooltip": "failed - (unknown failure) (allowed to fail)", - "has_details": true, - "details_path": "/h5bp/html5-boilerplate/-/jobs/537", - "illustration": { - "image": "/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg", - "size": "svg-430", - "title": "This job does not have a trace." - }, - "favicon": "/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png", - "action": { - "icon": "retry", - "title": "Retry", - "path": "/h5bp/html5-boilerplate/-/jobs/537/retry", - "method": "post", - "button_title": "Retry this job" - } - }, - "jobs": [ - { - "id": 537, - "name": "spinach:osx", - "started": "2019-01-06T09:48:20.237Z", - "archived": false, - "build_path": "/h5bp/html5-boilerplate/-/jobs/537", - "retry_path": "/h5bp/html5-boilerplate/-/jobs/537/retry", - "playable": false, - "scheduled": false, - "created_at": "2019-01-06T17:48:38.069Z", - "updated_at": "2019-01-06T17:48:38.069Z", - "status": { - "icon": "status_warning", - "text": "failed", - "label": "failed (allowed to fail)", - "group": "failed-with-warnings", - "tooltip": "failed - (unknown failure) (allowed to fail)", - "has_details": true, - "details_path": "/h5bp/html5-boilerplate/-/jobs/537", - "illustration": { - "image": "/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg", - "size": "svg-430", - "title": "This job does not have a trace." - }, - "favicon": "/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png", - "action": { - "icon": "retry", - "title": "Retry", - "path": "/h5bp/html5-boilerplate/-/jobs/537/retry", - "method": "post", - "button_title": "Retry this job" - } - }, - "callout_message": "There is an unknown failure, please try again", - "recoverable": true - } - ] - } - ], - "status": { - "icon": "status_warning", - "text": "passed", - "label": "passed with warnings", - "group": "success-with-warnings", - "tooltip": "passed", - "has_details": true, - "details_path": "/h5bp/html5-boilerplate/pipelines/26#test", - "illustration": null, - "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png" - }, - "path": "/h5bp/html5-boilerplate/pipelines/26#test", - "dropdown_path": "/h5bp/html5-boilerplate/pipelines/26/stage.json?stage=test" - }, - { - "name": "security", - "title": "security: passed", - "groups": [ - { - "name": "container_scanning", - "size": 1, - "status": { - "icon": "status_success", - "text": "passed", - "label": "passed", - "group": "success", - "tooltip": "passed", - "has_details": true, - "details_path": "/h5bp/html5-boilerplate/-/jobs/541", - "illustration": { - "image": "/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg", - "size": "svg-430", - "title": "This job does not have a trace." - }, - "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png", - "action": { - "icon": "retry", - "title": "Retry", - "path": "/h5bp/html5-boilerplate/-/jobs/541/retry", - "method": "post", - "button_title": "Retry this job" - } - }, - "jobs": [ - { - "id": 541, - "name": "container_scanning", - "started": "2019-01-06T09:48:20.237Z", - "archived": false, - "build_path": "/h5bp/html5-boilerplate/-/jobs/541", - "retry_path": "/h5bp/html5-boilerplate/-/jobs/541/retry", - "playable": false, - "scheduled": false, - "created_at": "2019-01-06T17:48:38.186Z", - "updated_at": "2019-01-06T17:48:38.186Z", - "status": { - "icon": "status_success", - "text": "passed", - "label": "passed", - "group": "success", - "tooltip": "passed", - "has_details": true, - "details_path": "/h5bp/html5-boilerplate/-/jobs/541", - "illustration": { - "image": "/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg", - "size": "svg-430", - "title": "This job does not have a trace." - }, - "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png", - "action": { - "icon": "retry", - "title": "Retry", - "path": "/h5bp/html5-boilerplate/-/jobs/541/retry", - "method": "post", - "button_title": "Retry this job" - } - } - } - ] - }, - { - "name": "dast", - "size": 1, - "status": { - "icon": "status_success", - "text": "passed", - "label": "passed", - "group": "success", - "tooltip": "passed", - "has_details": true, - "details_path": "/h5bp/html5-boilerplate/-/jobs/538", - "illustration": { - "image": "/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg", - "size": "svg-430", - "title": "This job does not have a trace." - }, - "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png", - "action": { - "icon": "retry", - "title": "Retry", - "path": "/h5bp/html5-boilerplate/-/jobs/538/retry", - "method": "post", - "button_title": "Retry this job" - } - }, - "jobs": [ - { - "id": 538, - "name": "dast", - "started": "2019-01-06T09:48:20.237Z", - "archived": false, - "build_path": "/h5bp/html5-boilerplate/-/jobs/538", - "retry_path": "/h5bp/html5-boilerplate/-/jobs/538/retry", - "playable": false, - "scheduled": false, - "created_at": "2019-01-06T17:48:38.087Z", - "updated_at": "2019-01-06T17:48:38.087Z", - "status": { - "icon": "status_success", - "text": "passed", - "label": "passed", - "group": "success", - "tooltip": "passed", - "has_details": true, - "details_path": "/h5bp/html5-boilerplate/-/jobs/538", - "illustration": { - "image": "/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg", - "size": "svg-430", - "title": "This job does not have a trace." - }, - "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png", - "action": { - "icon": "retry", - "title": "Retry", - "path": "/h5bp/html5-boilerplate/-/jobs/538/retry", - "method": "post", - "button_title": "Retry this job" - } - } - } - ] - }, - { - "name": "dependency_scanning", - "size": 1, - "status": { - "icon": "status_success", - "text": "passed", - "label": "passed", - "group": "success", - "tooltip": "passed", - "has_details": true, - "details_path": "/h5bp/html5-boilerplate/-/jobs/540", - "illustration": { - "image": "/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg", - "size": "svg-430", - "title": "This job does not have a trace." - }, - "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png", - "action": { - "icon": "retry", - "title": "Retry", - "path": "/h5bp/html5-boilerplate/-/jobs/540/retry", - "method": "post", - "button_title": "Retry this job" - } - }, - "jobs": [ - { - "id": 540, - "name": "dependency_scanning", - "started": "2019-01-06T09:48:20.237Z", - "archived": false, - "build_path": "/h5bp/html5-boilerplate/-/jobs/540", - "retry_path": "/h5bp/html5-boilerplate/-/jobs/540/retry", - "playable": false, - "scheduled": false, - "created_at": "2019-01-06T17:48:38.153Z", - "updated_at": "2019-01-06T17:48:38.153Z", - "status": { - "icon": "status_success", - "text": "passed", - "label": "passed", - "group": "success", - "tooltip": "passed", - "has_details": true, - "details_path": "/h5bp/html5-boilerplate/-/jobs/540", - "illustration": { - "image": "/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg", - "size": "svg-430", - "title": "This job does not have a trace." - }, - "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png", - "action": { - "icon": "retry", - "title": "Retry", - "path": "/h5bp/html5-boilerplate/-/jobs/540/retry", - "method": "post", - "button_title": "Retry this job" - } - } - } - ] - }, - { - "name": "sast", - "size": 1, - "status": { - "icon": "status_success", - "text": "passed", - "label": "passed", - "group": "success", - "tooltip": "passed", - "has_details": true, - "details_path": "/h5bp/html5-boilerplate/-/jobs/539", - "illustration": { - "image": "/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg", - "size": "svg-430", - "title": "This job does not have a trace." - }, - "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png", - "action": { - "icon": "retry", - "title": "Retry", - "path": "/h5bp/html5-boilerplate/-/jobs/539/retry", - "method": "post", - "button_title": "Retry this job" - } - }, - "jobs": [ - { - "id": 539, - "name": "sast", - "started": "2019-01-06T09:48:20.237Z", - "archived": false, - "build_path": "/h5bp/html5-boilerplate/-/jobs/539", - "retry_path": "/h5bp/html5-boilerplate/-/jobs/539/retry", - "playable": false, - "scheduled": false, - "created_at": "2019-01-06T17:48:38.121Z", - "updated_at": "2019-01-06T17:48:38.121Z", - "status": { - "icon": "status_success", - "text": "passed", - "label": "passed", - "group": "success", - "tooltip": "passed", - "has_details": true, - "details_path": "/h5bp/html5-boilerplate/-/jobs/539", - "illustration": { - "image": "/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg", - "size": "svg-430", - "title": "This job does not have a trace." - }, - "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png", - "action": { - "icon": "retry", - "title": "Retry", - "path": "/h5bp/html5-boilerplate/-/jobs/539/retry", - "method": "post", - "button_title": "Retry this job" - } - } - } - ] - } - ], - "status": { - "icon": "status_success", - "text": "passed", - "label": "passed", - "group": "success", - "tooltip": "passed", - "has_details": true, - "details_path": "/h5bp/html5-boilerplate/pipelines/26#security", - "illustration": null, - "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png" - }, - "path": "/h5bp/html5-boilerplate/pipelines/26#security", - "dropdown_path": "/h5bp/html5-boilerplate/pipelines/26/stage.json?stage=security" - }, - { - "name": "deploy", - "title": "deploy: passed", - "groups": [ - { - "name": "production", - "size": 1, - "status": { - "icon": "status_skipped", - "text": "skipped", - "label": "skipped", - "group": "skipped", - "tooltip": "skipped", - "has_details": true, - "details_path": "/h5bp/html5-boilerplate/-/jobs/544", - "illustration": { - "image": "/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg", - "size": "svg-430", - "title": "This job has been skipped" - }, - "favicon": "/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png" - }, - "jobs": [ - { - "id": 544, - "name": "production", - "started": null, - "archived": false, - "build_path": "/h5bp/html5-boilerplate/-/jobs/544", - "playable": false, - "scheduled": false, - "created_at": "2019-01-06T17:48:38.313Z", - "updated_at": "2019-01-06T17:48:38.313Z", - "status": { - "icon": "status_skipped", - "text": "skipped", - "label": "skipped", - "group": "skipped", - "tooltip": "skipped", - "has_details": true, - "details_path": "/h5bp/html5-boilerplate/-/jobs/544", - "illustration": { - "image": "/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg", - "size": "svg-430", - "title": "This job has been skipped" - }, - "favicon": "/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png" - } - } - ] - }, - { - "name": "staging", - "size": 1, - "status": { - "icon": "status_success", - "text": "passed", - "label": "passed", - "group": "success", - "tooltip": "passed", - "has_details": true, - "details_path": "/h5bp/html5-boilerplate/-/jobs/542", - "illustration": { - "image": "/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg", - "size": "svg-430", - "title": "This job does not have a trace." - }, - "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png", - "action": { - "icon": "retry", - "title": "Retry", - "path": "/h5bp/html5-boilerplate/-/jobs/542/retry", - "method": "post", - "button_title": "Retry this job" - } - }, - "jobs": [ - { - "id": 542, - "name": "staging", - "started": "2019-01-06T11:48:20.237Z", - "archived": false, - "build_path": "/h5bp/html5-boilerplate/-/jobs/542", - "retry_path": "/h5bp/html5-boilerplate/-/jobs/542/retry", - "playable": false, - "scheduled": false, - "created_at": "2019-01-06T17:48:38.219Z", - "updated_at": "2019-01-06T17:48:38.219Z", - "status": { - "icon": "status_success", - "text": "passed", - "label": "passed", - "group": "success", - "tooltip": "passed", - "has_details": true, - "details_path": "/h5bp/html5-boilerplate/-/jobs/542", - "illustration": { - "image": "/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg", - "size": "svg-430", - "title": "This job does not have a trace." - }, - "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png", - "action": { - "icon": "retry", - "title": "Retry", - "path": "/h5bp/html5-boilerplate/-/jobs/542/retry", - "method": "post", - "button_title": "Retry this job" - } - } - } - ] - }, - { - "name": "stop staging", - "size": 1, - "status": { - "icon": "status_skipped", - "text": "skipped", - "label": "skipped", - "group": "skipped", - "tooltip": "skipped", - "has_details": true, - "details_path": "/h5bp/html5-boilerplate/-/jobs/543", - "illustration": { - "image": "/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg", - "size": "svg-430", - "title": "This job has been skipped" - }, - "favicon": "/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png" - }, - "jobs": [ - { - "id": 543, - "name": "stop staging", - "started": null, - "archived": false, - "build_path": "/h5bp/html5-boilerplate/-/jobs/543", - "playable": false, - "scheduled": false, - "created_at": "2019-01-06T17:48:38.283Z", - "updated_at": "2019-01-06T17:48:38.283Z", - "status": { - "icon": "status_skipped", - "text": "skipped", - "label": "skipped", - "group": "skipped", - "tooltip": "skipped", - "has_details": true, - "details_path": "/h5bp/html5-boilerplate/-/jobs/543", - "illustration": { - "image": "/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg", - "size": "svg-430", - "title": "This job has been skipped" - }, - "favicon": "/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png" - } - } - ] - } - ], - "status": { - "icon": "status_success", - "text": "passed", - "label": "passed", - "group": "success", - "tooltip": "passed", - "has_details": true, - "details_path": "/h5bp/html5-boilerplate/pipelines/26#deploy", - "illustration": null, - "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png" - }, - "path": "/h5bp/html5-boilerplate/pipelines/26#deploy", - "dropdown_path": "/h5bp/html5-boilerplate/pipelines/26/stage.json?stage=deploy" - }, - { - "name": "notify", - "title": "notify: passed", - "groups": [ - { - "name": "slack", - "size": 1, - "status": { - "icon": "status_success", - "text": "passed", - "label": "manual play action", - "group": "success", - "tooltip": "passed", - "has_details": true, - "details_path": "/h5bp/html5-boilerplate/-/jobs/545", - "illustration": { - "image": "/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg", - "size": "svg-394", - "title": "This job requires a manual action", - "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments" - }, - "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png", - "action": { - "icon": "play", - "title": "Play", - "path": "/h5bp/html5-boilerplate/-/jobs/545/play", - "method": "post", - "button_title": "Run job" - } - }, - "jobs": [ - { - "id": 545, - "name": "slack", - "started": null, - "archived": false, - "build_path": "/h5bp/html5-boilerplate/-/jobs/545", - "retry_path": "/h5bp/html5-boilerplate/-/jobs/545/retry", - "play_path": "/h5bp/html5-boilerplate/-/jobs/545/play", - "playable": true, - "scheduled": false, - "created_at": "2019-01-06T17:48:38.341Z", - "updated_at": "2019-01-06T17:48:38.341Z", - "status": { - "icon": "status_success", - "text": "passed", - "label": "manual play action", - "group": "success", - "tooltip": "passed", - "has_details": true, - "details_path": "/h5bp/html5-boilerplate/-/jobs/545", - "illustration": { - "image": "/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg", - "size": "svg-394", - "title": "This job requires a manual action", - "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments" - }, - "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png", - "action": { - "icon": "play", - "title": "Play", - "path": "/h5bp/html5-boilerplate/-/jobs/545/play", - "method": "post", - "button_title": "Run job" - } - } - } - ] - } - ], - "status": { - "icon": "status_success", - "text": "passed", - "label": "passed", - "group": "success", - "tooltip": "passed", - "has_details": true, - "details_path": "/h5bp/html5-boilerplate/pipelines/26#notify", - "illustration": null, - "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png" - }, - "path": "/h5bp/html5-boilerplate/pipelines/26#notify", - "dropdown_path": "/h5bp/html5-boilerplate/pipelines/26/stage.json?stage=notify" - } - ], - "artifacts": [ - { - "name": "build:linux", - "expired": null, - "expire_at": null, - "path": "/h5bp/html5-boilerplate/-/jobs/526/artifacts/download", - "browse_path": "/h5bp/html5-boilerplate/-/jobs/526/artifacts/browse" - }, - { - "name": "build:osx", - "expired": null, - "expire_at": null, - "path": "/h5bp/html5-boilerplate/-/jobs/527/artifacts/download", - "browse_path": "/h5bp/html5-boilerplate/-/jobs/527/artifacts/browse" - } - ], - "manual_actions": [ - { - "name": "stop staging", - "path": "/h5bp/html5-boilerplate/-/jobs/543/play", - "playable": false, - "scheduled": false - }, - { - "name": "production", - "path": "/h5bp/html5-boilerplate/-/jobs/544/play", - "playable": false, - "scheduled": false - }, - { - "name": "slack", - "path": "/h5bp/html5-boilerplate/-/jobs/545/play", - "playable": true, - "scheduled": false - } - ], - "scheduled_actions": [ - - ] - }, - "ref": { - "name": "master", - "path": "/h5bp/html5-boilerplate/commits/master", - "tag": false, - "branch": true, - "merge_request": false - }, - "commit": { - "id": "bad98c453eab56d20057f3929989251d45cd1a8b", - "short_id": "bad98c45", - "title": "remove instances of shrink-to-fit=no (#2103)", - "created_at": "2018-12-17T20:52:18.000Z", - "parent_ids": [ - "49130f6cfe9ff1f749015d735649a2bc6f66cf3a" - ], - "message": "remove instances of shrink-to-fit=no (#2103)\n\ncloses #2102\r\n\r\nPer my findings, the need for it as a default was rectified with the release of iOS 9.3, where the viewport no longer shrunk to accommodate overflow, as was introduced in iOS 9.", - "author_name": "Scott O'Hara", - "author_email": "scottaohara@users.noreply.github.com", - "authored_date": "2018-12-17T20:52:18.000Z", - "committer_name": "Rob Larsen", - "committer_email": "rob@drunkenfist.com", - "committed_date": "2018-12-17T20:52:18.000Z", - "author": null, - "author_gravatar_url": "https://www.gravatar.com/avatar/6d597df7cf998d16cbe00ccac063b31e?s=80&d=identicon", - "commit_url": "http://localhost:3001/h5bp/html5-boilerplate/commit/bad98c453eab56d20057f3929989251d45cd1a8b", - "commit_path": "/h5bp/html5-boilerplate/commit/bad98c453eab56d20057f3929989251d45cd1a8b" - }, - "retry_path": "/h5bp/html5-boilerplate/pipelines/26/retry", - "triggered_by": { - "id": 4, - "user": null, - "active": false, - "coverage": null, - "source": "push", - "path": "/gitlab-org/gitlab-test/pipelines/4", - "details": { - "status": { - "icon": "status_warning", - "text": "passed", - "label": "passed with warnings", - "group": "success-with-warnings", - "tooltip": "passed", - "has_details": true, - "details_path": "/gitlab-org/gitlab-test/pipelines/4", - "illustration": null, - "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png" - } - }, - "project": { - "id": 1, - "name": "Gitlab Test", - "full_path": "/gitlab-org/gitlab-test", - "full_name": "Gitlab Org / Gitlab Test" - } - }, - "triggered": [ - - ], - "project": { - "id": 1794617, - "name": "GitLab Docs", - "full_path": "/gitlab-com/gitlab-docs", - "full_name": "GitLab.com / GitLab Docs" - } - } - ] - } - ] -} diff --git a/spec/frontend/pipelines/mock_data.js b/spec/frontend/pipelines/mock_data.js deleted file mode 100644 index 673db3b5178..00000000000 --- a/spec/frontend/pipelines/mock_data.js +++ /dev/null @@ -1,1379 +0,0 @@ -import pipelineHeaderSuccess from 'test_fixtures/graphql/pipelines/pipeline_header_success.json'; -import pipelineHeaderRunning from 'test_fixtures/graphql/pipelines/pipeline_header_running.json'; -import pipelineHeaderRunningWithDuration from 'test_fixtures/graphql/pipelines/pipeline_header_running_with_duration.json'; -import pipelineHeaderFailed from 'test_fixtures/graphql/pipelines/pipeline_header_failed.json'; - -const PIPELINE_RUNNING = 'RUNNING'; -const PIPELINE_CANCELED = 'CANCELED'; -const PIPELINE_FAILED = 'FAILED'; - -const threeWeeksAgo = new Date(); -threeWeeksAgo.setDate(threeWeeksAgo.getDate() - 21); - -export { - pipelineHeaderSuccess, - pipelineHeaderRunning, - pipelineHeaderRunningWithDuration, - pipelineHeaderFailed, -}; - -export const pipelineRetryMutationResponseSuccess = { - data: { pipelineRetry: { errors: [] } }, -}; - -export const pipelineRetryMutationResponseFailed = { - data: { pipelineRetry: { errors: ['error'] } }, -}; - -export const pipelineCancelMutationResponseSuccess = { - data: { pipelineCancel: { errors: [] } }, -}; - -export const pipelineCancelMutationResponseFailed = { - data: { pipelineCancel: { errors: ['error'] } }, -}; - -export const pipelineDeleteMutationResponseSuccess = { - data: { pipelineDestroy: { errors: [] } }, -}; - -export const pipelineDeleteMutationResponseFailed = { - data: { pipelineDestroy: { errors: ['error'] } }, -}; - -export const mockPipelineHeader = { - detailedStatus: {}, - id: 123, - userPermissions: { - destroyPipeline: true, - updatePipeline: true, - }, - createdAt: threeWeeksAgo.toISOString(), - user: { - id: 'user-1', - name: 'Foo', - username: 'foobar', - email: 'foo@bar.com', - avatarUrl: 'link', - }, -}; - -export const mockFailedPipelineHeader = { - ...mockPipelineHeader, - status: PIPELINE_FAILED, - retryable: true, - cancelable: false, - detailedStatus: { - id: 'status-1', - group: 'failed', - icon: 'status_failed', - label: 'failed', - text: 'failed', - detailsPath: 'path', - }, -}; - -export const mockFailedPipelineNoPermissions = { - id: 123, - userPermissions: { - destroyPipeline: false, - updatePipeline: false, - }, - createdAt: threeWeeksAgo.toISOString(), - user: { - id: 'user-1', - name: 'Foo', - username: 'foobar', - email: 'foo@bar.com', - avatarUrl: 'link', - }, - status: PIPELINE_RUNNING, - retryable: true, - cancelable: false, - detailedStatus: { - id: 'status-1', - group: 'running', - icon: 'status_running', - label: 'running', - text: 'running', - detailsPath: 'path', - }, -}; - -export const mockRunningPipelineHeader = { - ...mockPipelineHeader, - status: PIPELINE_RUNNING, - retryable: false, - cancelable: true, - detailedStatus: { - id: 'status-1', - group: 'running', - icon: 'status_running', - label: 'running', - text: 'running', - detailsPath: 'path', - }, -}; - -export const mockRunningPipelineNoPermissions = { - id: 123, - userPermissions: { - destroyPipeline: false, - updatePipeline: false, - }, - createdAt: threeWeeksAgo.toISOString(), - user: { - id: 'user-1', - name: 'Foo', - username: 'foobar', - email: 'foo@bar.com', - avatarUrl: 'link', - }, - status: PIPELINE_RUNNING, - retryable: false, - cancelable: true, - detailedStatus: { - id: 'status-1', - group: 'running', - icon: 'status_running', - label: 'running', - text: 'running', - detailsPath: 'path', - }, -}; - -export const mockCancelledPipelineHeader = { - ...mockPipelineHeader, - status: PIPELINE_CANCELED, - retryable: true, - cancelable: false, - detailedStatus: { - id: 'status-1', - group: 'cancelled', - icon: 'status_cancelled', - label: 'cancelled', - text: 'cancelled', - detailsPath: 'path', - }, -}; - -export const mockSuccessfulPipelineHeader = { - ...mockPipelineHeader, - status: 'SUCCESS', - retryable: false, - cancelable: false, - detailedStatus: { - id: 'status-1', - group: 'success', - icon: 'status_success', - label: 'success', - text: 'success', - detailsPath: 'path', - }, -}; - -export const mockRunningPipelineHeaderData = { - data: { - project: { - id: '1', - pipeline: { - ...mockRunningPipelineHeader, - iid: '28', - user: { - id: 'user-1', - name: 'Foo', - username: 'foobar', - webPath: '/foo', - webUrl: '/foo', - email: 'foo@bar.com', - avatarUrl: 'link', - status: null, - __typename: 'UserCore', - }, - __typename: 'Pipeline', - }, - __typename: 'Project', - }, - }, -}; - -export const stageReply = { - name: 'deploy', - title: 'deploy: running', - latest_statuses: [ - { - id: 928, - name: 'stop staging', - started: false, - build_path: '/twitter/flight/-/jobs/928', - cancel_path: '/twitter/flight/-/jobs/928/cancel', - playable: false, - created_at: '2018-04-04T20:02:02.728Z', - updated_at: '2018-04-04T20:02:02.766Z', - status: { - icon: 'status_pending', - text: 'pending', - label: 'pending', - group: 'pending', - tooltip: 'pending', - has_details: true, - details_path: '/twitter/flight/-/jobs/928', - favicon: - '/assets/ci_favicons/dev/favicon_status_pending-db32e1faf94b9f89530ac519790920d1f18ea8f6af6cd2e0a26cd6840cacf101.ico', - action: { - icon: 'cancel', - title: 'Cancel', - path: '/twitter/flight/-/jobs/928/cancel', - method: 'post', - }, - }, - }, - { - id: 926, - name: 'production', - started: false, - build_path: '/twitter/flight/-/jobs/926', - retry_path: '/twitter/flight/-/jobs/926/retry', - play_path: '/twitter/flight/-/jobs/926/play', - playable: true, - created_at: '2018-04-04T20:00:57.202Z', - updated_at: '2018-04-04T20:11:13.110Z', - status: { - icon: 'status_canceled', - text: 'canceled', - label: 'manual play action', - group: 'canceled', - tooltip: 'canceled', - has_details: true, - details_path: '/twitter/flight/-/jobs/926', - favicon: - '/assets/ci_favicons/dev/favicon_status_canceled-5491840b9b6feafba0bc599cbd49ee9580321dc809683856cf1b0d51532b1af6.ico', - action: { - icon: 'play', - title: 'Play', - path: '/twitter/flight/-/jobs/926/play', - method: 'post', - }, - }, - }, - { - id: 217, - name: 'staging', - started: '2018-03-07T08:41:46.234Z', - build_path: '/twitter/flight/-/jobs/217', - retry_path: '/twitter/flight/-/jobs/217/retry', - playable: false, - created_at: '2018-03-07T14:41:58.093Z', - updated_at: '2018-03-07T14:41:58.093Z', - status: { - icon: 'status_success', - text: 'passed', - label: 'passed', - group: 'success', - tooltip: 'passed', - has_details: true, - details_path: '/twitter/flight/-/jobs/217', - favicon: - '/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico', - action: { - icon: 'retry', - title: 'Retry', - path: '/twitter/flight/-/jobs/217/retry', - method: 'post', - }, - }, - }, - ], - status: { - icon: 'status_running', - text: 'running', - label: 'running', - group: 'running', - tooltip: 'running', - has_details: true, - details_path: '/twitter/flight/pipelines/13#deploy', - favicon: - '/assets/ci_favicons/dev/favicon_status_running-c3ad2fc53ea6079c174e5b6c1351ff349e99ec3af5a5622fb77b0fe53ea279c1.ico', - }, - path: '/twitter/flight/pipelines/13#deploy', - dropdown_path: '/twitter/flight/pipelines/13/stage.json?stage=deploy', -}; - -export const users = [ - { - id: 1, - name: 'Administrator', - username: 'root', - state: 'active', - avatar_url: - 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon', - web_url: 'http://192.168.1.22:3000/root', - }, - { - id: 10, - name: 'Angel Spinka', - username: 'shalonda', - state: 'active', - avatar_url: - 'https://www.gravatar.com/avatar/709df1b65ad06764ee2b0edf1b49fc27?s=80\u0026d=identicon', - web_url: 'http://192.168.1.22:3000/shalonda', - }, - { - id: 11, - name: 'Art Davis', - username: 'deja.green', - state: 'active', - avatar_url: - 'https://www.gravatar.com/avatar/bb56834c061522760e7a6dd7d431a306?s=80\u0026d=identicon', - web_url: 'http://192.168.1.22:3000/deja.green', - }, - { - id: 32, - name: 'Arnold Mante', - username: 'reported_user_10', - state: 'active', - avatar_url: - 'https://www.gravatar.com/avatar/ab558033a82466d7905179e837d7723a?s=80\u0026d=identicon', - web_url: 'http://192.168.1.22:3000/reported_user_10', - }, - { - id: 38, - name: 'Cher Wintheiser', - username: 'reported_user_16', - state: 'active', - avatar_url: - 'https://www.gravatar.com/avatar/2640356e8b5bc4314133090994ed162b?s=80\u0026d=identicon', - web_url: 'http://192.168.1.22:3000/reported_user_16', - }, - { - id: 39, - name: 'Bethel Wolf', - username: 'reported_user_17', - state: 'active', - avatar_url: - 'https://www.gravatar.com/avatar/4b948694fadba4b01e4acfc06b065e8e?s=80\u0026d=identicon', - web_url: 'http://192.168.1.22:3000/reported_user_17', - }, -]; - -export const branches = [ - { - name: 'branch-1', - commit: { - id: '21fb056cc47dcf706670e6de635b1b326490ebdc', - short_id: '21fb056c', - created_at: '2020-05-07T10:58:28.000-04:00', - parent_ids: null, - title: 'Add new file', - message: 'Add new file', - author_name: 'Administrator', - author_email: 'admin@example.com', - authored_date: '2020-05-07T10:58:28.000-04:00', - committer_name: 'Administrator', - committer_email: 'admin@example.com', - committed_date: '2020-05-07T10:58:28.000-04:00', - web_url: - 'http://192.168.1.22:3000/root/dag-pipeline/-/commit/21fb056cc47dcf706670e6de635b1b326490ebdc', - }, - merged: false, - protected: false, - developers_can_push: false, - developers_can_merge: false, - can_push: true, - default: false, - web_url: 'http://192.168.1.22:3000/root/dag-pipeline/-/tree/branch-1', - }, - { - name: 'branch-10', - commit: { - id: '66673b07efef254dab7d537f0433a40e61cf84fe', - short_id: '66673b07', - created_at: '2020-03-16T11:04:46.000-04:00', - parent_ids: null, - title: 'Update .gitlab-ci.yml', - message: 'Update .gitlab-ci.yml', - author_name: 'Administrator', - author_email: 'admin@example.com', - authored_date: '2020-03-16T11:04:46.000-04:00', - committer_name: 'Administrator', - committer_email: 'admin@example.com', - committed_date: '2020-03-16T11:04:46.000-04:00', - web_url: - 'http://192.168.1.22:3000/root/dag-pipeline/-/commit/66673b07efef254dab7d537f0433a40e61cf84fe', - }, - merged: false, - protected: false, - developers_can_push: false, - developers_can_merge: false, - can_push: true, - default: false, - web_url: 'http://192.168.1.22:3000/root/dag-pipeline/-/tree/branch-10', - }, - { - name: 'branch-11', - commit: { - id: '66673b07efef254dab7d537f0433a40e61cf84fe', - short_id: '66673b07', - created_at: '2020-03-16T11:04:46.000-04:00', - parent_ids: null, - title: 'Update .gitlab-ci.yml', - message: 'Update .gitlab-ci.yml', - author_name: 'Administrator', - author_email: 'admin@example.com', - authored_date: '2020-03-16T11:04:46.000-04:00', - committer_name: 'Administrator', - committer_email: 'admin@example.com', - committed_date: '2020-03-16T11:04:46.000-04:00', - web_url: - 'http://192.168.1.22:3000/root/dag-pipeline/-/commit/66673b07efef254dab7d537f0433a40e61cf84fe', - }, - merged: false, - protected: false, - developers_can_push: false, - developers_can_merge: false, - can_push: true, - default: false, - web_url: 'http://192.168.1.22:3000/root/dag-pipeline/-/tree/branch-11', - }, -]; - -export const tags = [ - { - name: 'tag-3', - message: '', - target: '66673b07efef254dab7d537f0433a40e61cf84fe', - commit: { - id: '66673b07efef254dab7d537f0433a40e61cf84fe', - short_id: '66673b07', - created_at: '2020-03-16T11:04:46.000-04:00', - parent_ids: ['def28bf679235071140180495f25b657e2203587'], - title: 'Update .gitlab-ci.yml', - message: 'Update .gitlab-ci.yml', - author_name: 'Administrator', - author_email: 'admin@example.com', - authored_date: '2020-03-16T11:04:46.000-04:00', - committer_name: 'Administrator', - committer_email: 'admin@example.com', - committed_date: '2020-03-16T11:04:46.000-04:00', - web_url: - 'http://192.168.1.22:3000/root/dag-pipeline/-/commit/66673b07efef254dab7d537f0433a40e61cf84fe', - }, - release: null, - protected: false, - }, - { - name: 'tag-2', - message: '', - target: '66673b07efef254dab7d537f0433a40e61cf84fe', - commit: { - id: '66673b07efef254dab7d537f0433a40e61cf84fe', - short_id: '66673b07', - created_at: '2020-03-16T11:04:46.000-04:00', - parent_ids: ['def28bf679235071140180495f25b657e2203587'], - title: 'Update .gitlab-ci.yml', - message: 'Update .gitlab-ci.yml', - author_name: 'Administrator', - author_email: 'admin@example.com', - authored_date: '2020-03-16T11:04:46.000-04:00', - committer_name: 'Administrator', - committer_email: 'admin@example.com', - committed_date: '2020-03-16T11:04:46.000-04:00', - web_url: - 'http://192.168.1.22:3000/root/dag-pipeline/-/commit/66673b07efef254dab7d537f0433a40e61cf84fe', - }, - release: null, - protected: false, - }, - { - name: 'tag-1', - message: '', - target: '66673b07efef254dab7d537f0433a40e61cf84fe', - commit: { - id: '66673b07efef254dab7d537f0433a40e61cf84fe', - short_id: '66673b07', - created_at: '2020-03-16T11:04:46.000-04:00', - parent_ids: ['def28bf679235071140180495f25b657e2203587'], - title: 'Update .gitlab-ci.yml', - message: 'Update .gitlab-ci.yml', - author_name: 'Administrator', - author_email: 'admin@example.com', - authored_date: '2020-03-16T11:04:46.000-04:00', - committer_name: 'Administrator', - committer_email: 'admin@example.com', - committed_date: '2020-03-16T11:04:46.000-04:00', - web_url: - 'http://192.168.1.22:3000/root/dag-pipeline/-/commit/66673b07efef254dab7d537f0433a40e61cf84fe', - }, - release: null, - protected: false, - }, - { - name: 'main-tag', - message: '', - target: '66673b07efef254dab7d537f0433a40e61cf84fe', - commit: { - id: '66673b07efef254dab7d537f0433a40e61cf84fe', - short_id: '66673b07', - created_at: '2020-03-16T11:04:46.000-04:00', - parent_ids: ['def28bf679235071140180495f25b657e2203587'], - title: 'Update .gitlab-ci.yml', - message: 'Update .gitlab-ci.yml', - author_name: 'Administrator', - author_email: 'admin@example.com', - authored_date: '2020-03-16T11:04:46.000-04:00', - committer_name: 'Administrator', - committer_email: 'admin@example.com', - committed_date: '2020-03-16T11:04:46.000-04:00', - web_url: - 'http://192.168.1.22:3000/root/dag-pipeline/-/commit/66673b07efef254dab7d537f0433a40e61cf84fe', - }, - release: null, - protected: false, - }, -]; - -export const mockSearch = [ - { type: 'username', value: { data: 'root', operator: '=' } }, - { type: 'ref', value: { data: 'main', operator: '=' } }, - { type: 'status', value: { data: 'pending', operator: '=' } }, -]; - -export const mockBranchesAfterMap = ['branch-1', 'branch-10', 'branch-11']; - -export const mockTagsAfterMap = ['tag-3', 'tag-2', 'tag-1', 'main-tag']; - -export const mockPipelineJobsQueryResponse = { - data: { - project: { - id: 'gid://gitlab/Project/20', - __typename: 'Project', - pipeline: { - id: 'gid://gitlab/Ci::Pipeline/224', - __typename: 'Pipeline', - jobs: { - __typename: 'CiJobConnection', - pageInfo: { - endCursor: 'eyJpZCI6Ijg0NyJ9', - hasNextPage: true, - hasPreviousPage: false, - startCursor: 'eyJpZCI6IjYyMCJ9', - __typename: 'PageInfo', - }, - nodes: [ - { - artifacts: { - nodes: [ - { - downloadPath: '/root/ci-project/-/jobs/620/artifacts/download?file_type=trace', - fileType: 'TRACE', - __typename: 'CiJobArtifact', - }, - ], - __typename: 'CiJobArtifactConnection', - }, - allowFailure: false, - status: 'SUCCESS', - scheduledAt: null, - manualJob: false, - triggered: null, - createdByTag: false, - detailedStatus: { - id: 'success-620-620', - detailsPath: '/root/ci-project/-/jobs/620', - group: 'success', - icon: 'status_success', - label: 'passed', - text: 'passed', - tooltip: 'passed (retried)', - action: null, - __typename: 'DetailedStatus', - }, - id: 'gid://gitlab/Ci::Build/620', - refName: 'main', - refPath: '/root/ci-project/-/commits/main', - tags: [], - shortSha: '5acce24b', - commitPath: '/root/ci-project/-/commit/5acce24b3737d4f0d649ad0a26ae1903a2b35f5e', - stage: { id: 'gid://gitlab/Ci::Stage/148', name: 'test', __typename: 'CiStage' }, - name: 'coverage_job', - duration: 4, - finishedAt: '2021-12-06T14:13:49Z', - coverage: 82.71, - retryable: false, - playable: false, - cancelable: false, - active: false, - stuck: false, - userPermissions: { - readBuild: true, - readJobArtifacts: true, - updateBuild: true, - __typename: 'JobPermissions', - }, - __typename: 'CiJob', - }, - { - artifacts: { - nodes: [ - { - downloadPath: '/root/ci-project/-/jobs/619/artifacts/download?file_type=trace', - fileType: 'TRACE', - __typename: 'CiJobArtifact', - }, - ], - __typename: 'CiJobArtifactConnection', - }, - allowFailure: false, - status: 'SUCCESS', - scheduledAt: null, - manualJob: false, - triggered: null, - createdByTag: false, - detailedStatus: { - id: 'success-619-619', - detailsPath: '/root/ci-project/-/jobs/619', - group: 'success', - icon: 'status_success', - label: 'passed', - text: 'passed', - tooltip: 'passed (retried)', - action: null, - __typename: 'DetailedStatus', - }, - id: 'gid://gitlab/Ci::Build/619', - refName: 'main', - refPath: '/root/ci-project/-/commits/main', - tags: [], - shortSha: '5acce24b', - commitPath: '/root/ci-project/-/commit/5acce24b3737d4f0d649ad0a26ae1903a2b35f5e', - stage: { id: 'gid://gitlab/Ci::Stage/148', name: 'test', __typename: 'CiStage' }, - name: 'test_job_two', - duration: 4, - finishedAt: '2021-12-06T14:13:44Z', - coverage: null, - retryable: false, - playable: false, - cancelable: false, - active: false, - stuck: false, - userPermissions: { - readBuild: true, - readJobArtifacts: true, - updateBuild: true, - __typename: 'JobPermissions', - }, - __typename: 'CiJob', - }, - ], - }, - }, - }, - }, -}; - -export const mockPipeline = (projectPath) => { - return { - pipeline: { - id: 1, - user: { - id: 1, - name: 'Administrator', - username: 'root', - state: 'active', - avatar_url: '', - web_url: 'http://0.0.0.0:3000/root', - show_status: false, - path: '/root', - }, - active: false, - source: 'merge_request_event', - created_at: '2021-10-19T21:17:38.698Z', - updated_at: '2021-10-21T18:00:42.758Z', - path: 'foo', - flags: {}, - merge_request: { - iid: 1, - path: `/${projectPath}/1`, - title: 'commit', - source_branch: 'test-commit-name', - source_branch_path: `/${projectPath}`, - target_branch: 'main', - target_branch_path: `/${projectPath}/-/commit/main`, - }, - ref: { - name: 'refs/merge-requests/1/head', - path: `/${projectPath}/-/commits/refs/merge-requests/1/head`, - tag: false, - branch: false, - merge_request: true, - }, - commit: { - id: 'fd6df5b3229e213c97d308844a6f3e7fd71e8f8c', - short_id: 'fd6df5b3', - created_at: '2021-10-19T21:17:12.000+00:00', - parent_ids: ['7147906b84306e83cb3fec6582a25390b75713c6'], - title: 'Commit', - message: 'Commit', - author_name: 'Administrator', - author_email: 'admin@example.com', - authored_date: '2021-10-19T21:17:12.000+00:00', - committer_name: 'Administrator', - committer_email: 'admin@example.com', - committed_date: '2021-10-19T21:17:12.000+00:00', - trailers: {}, - web_url: '', - author: { - id: 1, - name: 'Administrator', - username: 'root', - state: 'active', - avatar_url: '', - web_url: '', - show_status: false, - path: '/root', - }, - author_gravatar_url: '', - commit_url: `/${projectPath}/fd6df5b3229e213c97d308844a6f3e7fd71e8f8c`, - commit_path: `/${projectPath}/commit/fd6df5b3229e213c97d308844a6f3e7fd71e8f8c`, - }, - project: { - full_path: `/${projectPath}`, - }, - triggered_by: null, - triggered: [], - }, - pipelineScheduleUrl: 'foo', - pipelineKey: 'id', - viewType: 'root', - }; -}; - -export const mockPipelineTag = () => { - return { - pipeline: { - id: 311, - iid: 37, - user: { - id: 1, - username: 'root', - name: 'Administrator', - state: 'active', - avatar_url: - 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', - web_url: 'http://gdk.test:3000/root', - show_status: false, - path: '/root', - }, - active: false, - source: 'push', - name: 'Build pipeline', - created_at: '2022-02-02T15:39:04.012Z', - updated_at: '2022-02-02T15:40:59.573Z', - path: '/root/mr-widgets/-/pipelines/311', - flags: { - stuck: false, - auto_devops: false, - merge_request: false, - yaml_errors: false, - retryable: true, - cancelable: false, - failure_reason: false, - detached_merge_request_pipeline: false, - merge_request_pipeline: false, - merge_train_pipeline: false, - latest: true, - }, - details: { - status: { - icon: 'status_warning', - text: 'passed', - label: 'passed with warnings', - group: 'success-with-warnings', - tooltip: 'passed', - has_details: true, - details_path: '/root/mr-widgets/-/pipelines/311', - illustration: null, - favicon: - '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png', - }, - stages: [ - { - name: 'accessibility', - title: 'accessibility: passed', - status: { - icon: 'status_success', - text: 'passed', - label: 'passed', - group: 'success', - tooltip: 'passed', - has_details: true, - details_path: '/root/mr-widgets/-/pipelines/311#accessibility', - illustration: null, - favicon: - '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png', - }, - path: '/root/mr-widgets/-/pipelines/311#accessibility', - dropdown_path: '/root/mr-widgets/-/pipelines/311/stage.json?stage=accessibility', - }, - { - name: 'validate', - title: 'validate: passed with warnings', - status: { - icon: 'status_warning', - text: 'passed', - label: 'passed with warnings', - group: 'success-with-warnings', - tooltip: 'passed', - has_details: true, - details_path: '/root/mr-widgets/-/pipelines/311#validate', - illustration: null, - favicon: - '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png', - }, - path: '/root/mr-widgets/-/pipelines/311#validate', - dropdown_path: '/root/mr-widgets/-/pipelines/311/stage.json?stage=validate', - }, - { - name: 'test', - title: 'test: passed', - status: { - icon: 'status_success', - text: 'passed', - label: 'passed', - group: 'success', - tooltip: 'passed', - has_details: true, - details_path: '/root/mr-widgets/-/pipelines/311#test', - illustration: null, - favicon: - '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png', - }, - path: '/root/mr-widgets/-/pipelines/311#test', - dropdown_path: '/root/mr-widgets/-/pipelines/311/stage.json?stage=test', - }, - { - name: 'build', - title: 'build: passed', - status: { - icon: 'status_success', - text: 'passed', - label: 'passed', - group: 'success', - tooltip: 'passed', - has_details: true, - details_path: '/root/mr-widgets/-/pipelines/311#build', - illustration: null, - favicon: - '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png', - }, - path: '/root/mr-widgets/-/pipelines/311#build', - dropdown_path: '/root/mr-widgets/-/pipelines/311/stage.json?stage=build', - }, - ], - duration: 93, - finished_at: '2022-02-02T15:40:59.384Z', - event_type_name: 'Pipeline', - manual_actions: [], - scheduled_actions: [], - }, - ref: { - name: 'test', - path: '/root/mr-widgets/-/commits/test', - tag: true, - branch: false, - merge_request: false, - }, - commit: { - id: '9b92b4f730d1611bd9a086ca221ae206d5da1e59', - short_id: '9b92b4f7', - created_at: '2022-01-13T13:59:03.000+00:00', - parent_ids: ['0ba763634114e207dc72c65c8e9459556b1204fb'], - title: 'Update hello_world.js', - message: 'Update hello_world.js', - author_name: 'Administrator', - author_email: 'admin@example.com', - authored_date: '2022-01-13T13:59:03.000+00:00', - committer_name: 'Administrator', - committer_email: 'admin@example.com', - committed_date: '2022-01-13T13:59:03.000+00:00', - trailers: {}, - web_url: - 'http://gdk.test:3000/root/mr-widgets/-/commit/9b92b4f730d1611bd9a086ca221ae206d5da1e59', - author: { - id: 1, - username: 'root', - name: 'Administrator', - state: 'active', - avatar_url: - 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', - web_url: 'http://gdk.test:3000/root', - show_status: false, - path: '/root', - }, - author_gravatar_url: - 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', - commit_url: - 'http://gdk.test:3000/root/mr-widgets/-/commit/9b92b4f730d1611bd9a086ca221ae206d5da1e59', - commit_path: '/root/mr-widgets/-/commit/9b92b4f730d1611bd9a086ca221ae206d5da1e59', - }, - retry_path: '/root/mr-widgets/-/pipelines/311/retry', - delete_path: '/root/mr-widgets/-/pipelines/311', - failed_builds: [ - { - id: 1696, - name: 'fmt', - started: '2022-02-02T15:39:45.192Z', - complete: true, - archived: false, - build_path: '/root/mr-widgets/-/jobs/1696', - retry_path: '/root/mr-widgets/-/jobs/1696/retry', - playable: false, - scheduled: false, - created_at: '2022-02-02T15:39:04.136Z', - updated_at: '2022-02-02T15:39:57.969Z', - status: { - icon: 'status_warning', - text: 'failed', - label: 'failed (allowed to fail)', - group: 'failed-with-warnings', - tooltip: 'failed - (script failure) (allowed to fail)', - has_details: true, - details_path: '/root/mr-widgets/-/jobs/1696', - illustration: { - image: - '/assets/illustrations/skipped-job_empty-29a8a37d8a61d1b6f68cf3484f9024e53cd6eb95e28eae3554f8011a1146bf27.svg', - size: 'svg-430', - title: 'This job does not have a trace.', - }, - favicon: - '/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png', - action: { - icon: 'retry', - title: 'Retry', - path: '/root/mr-widgets/-/jobs/1696/retry', - method: 'post', - button_title: 'Retry this job', - }, - }, - recoverable: false, - }, - ], - project: { - id: 23, - name: 'mr-widgets', - full_path: '/root/mr-widgets', - full_name: 'Administrator / mr-widgets', - }, - triggered_by: null, - triggered: [], - }, - pipelineScheduleUrl: 'foo', - pipelineKey: 'id', - viewType: 'root', - }; -}; - -export const mockPipelineBranch = () => { - return { - pipeline: { - id: 268, - iid: 34, - user: { - id: 1, - username: 'root', - name: 'Administrator', - state: 'active', - avatar_url: - 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', - web_url: 'http://gdk.test:3000/root', - show_status: false, - path: '/root', - }, - active: false, - source: 'push', - name: 'Build pipeline', - created_at: '2022-01-14T17:40:27.866Z', - updated_at: '2022-01-14T18:02:35.850Z', - path: '/root/mr-widgets/-/pipelines/268', - flags: { - stuck: false, - auto_devops: false, - merge_request: false, - yaml_errors: false, - retryable: true, - cancelable: false, - failure_reason: false, - detached_merge_request_pipeline: false, - merge_request_pipeline: false, - merge_train_pipeline: false, - latest: true, - }, - details: { - status: { - icon: 'status_warning', - text: 'passed', - label: 'passed with warnings', - group: 'success-with-warnings', - tooltip: 'passed', - has_details: true, - details_path: '/root/mr-widgets/-/pipelines/268', - illustration: null, - favicon: - '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png', - }, - stages: [ - { - name: 'validate', - title: 'validate: passed with warnings', - status: { - icon: 'status_warning', - text: 'passed', - label: 'passed with warnings', - group: 'success-with-warnings', - tooltip: 'passed', - has_details: true, - details_path: '/root/mr-widgets/-/pipelines/268#validate', - illustration: null, - favicon: - '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png', - }, - path: '/root/mr-widgets/-/pipelines/268#validate', - dropdown_path: '/root/mr-widgets/-/pipelines/268/stage.json?stage=validate', - }, - { - name: 'test', - title: 'test: passed', - status: { - icon: 'status_success', - text: 'passed', - label: 'passed', - group: 'success', - tooltip: 'passed', - has_details: true, - details_path: '/root/mr-widgets/-/pipelines/268#test', - illustration: null, - favicon: - '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png', - }, - path: '/root/mr-widgets/-/pipelines/268#test', - dropdown_path: '/root/mr-widgets/-/pipelines/268/stage.json?stage=test', - }, - { - name: 'build', - title: 'build: passed', - status: { - icon: 'status_success', - text: 'passed', - label: 'passed', - group: 'success', - tooltip: 'passed', - has_details: true, - details_path: '/root/mr-widgets/-/pipelines/268#build', - illustration: null, - favicon: - '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png', - }, - path: '/root/mr-widgets/-/pipelines/268#build', - dropdown_path: '/root/mr-widgets/-/pipelines/268/stage.json?stage=build', - }, - ], - duration: 75, - finished_at: '2022-01-14T18:02:35.842Z', - event_type_name: 'Pipeline', - manual_actions: [], - scheduled_actions: [], - }, - ref: { - name: 'update-ci', - path: '/root/mr-widgets/-/commits/update-ci', - tag: false, - branch: true, - merge_request: false, - }, - commit: { - id: '96aef9ecec5752c09371c1ade5fc77860aafc863', - short_id: '96aef9ec', - created_at: '2022-01-14T17:40:26.000+00:00', - parent_ids: ['06860257572d4cf84b73806250b78169050aed83'], - title: 'Update main.tf', - message: 'Update main.tf', - author_name: 'Administrator', - author_email: 'admin@example.com', - authored_date: '2022-01-14T17:40:26.000+00:00', - committer_name: 'Administrator', - committer_email: 'admin@example.com', - committed_date: '2022-01-14T17:40:26.000+00:00', - trailers: {}, - web_url: - 'http://gdk.test:3000/root/mr-widgets/-/commit/96aef9ecec5752c09371c1ade5fc77860aafc863', - author: { - id: 1, - username: 'root', - name: 'Administrator', - state: 'active', - avatar_url: - 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', - web_url: 'http://gdk.test:3000/root', - show_status: false, - path: '/root', - }, - author_gravatar_url: - 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', - commit_url: - 'http://gdk.test:3000/root/mr-widgets/-/commit/96aef9ecec5752c09371c1ade5fc77860aafc863', - commit_path: '/root/mr-widgets/-/commit/96aef9ecec5752c09371c1ade5fc77860aafc863', - }, - retry_path: '/root/mr-widgets/-/pipelines/268/retry', - delete_path: '/root/mr-widgets/-/pipelines/268', - failed_builds: [ - { - id: 1260, - name: 'fmt', - started: '2022-01-14T17:40:36.435Z', - complete: true, - archived: false, - build_path: '/root/mr-widgets/-/jobs/1260', - retry_path: '/root/mr-widgets/-/jobs/1260/retry', - playable: false, - scheduled: false, - created_at: '2022-01-14T17:40:27.879Z', - updated_at: '2022-01-14T17:40:42.129Z', - status: { - icon: 'status_warning', - text: 'failed', - label: 'failed (allowed to fail)', - group: 'failed-with-warnings', - tooltip: 'failed - (script failure) (allowed to fail)', - has_details: true, - details_path: '/root/mr-widgets/-/jobs/1260', - illustration: { - image: - '/assets/illustrations/skipped-job_empty-29a8a37d8a61d1b6f68cf3484f9024e53cd6eb95e28eae3554f8011a1146bf27.svg', - size: 'svg-430', - title: 'This job does not have a trace.', - }, - favicon: - '/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png', - action: { - icon: 'retry', - title: 'Retry', - path: '/root/mr-widgets/-/jobs/1260/retry', - method: 'post', - button_title: 'Retry this job', - }, - }, - recoverable: false, - }, - ], - project: { - id: 23, - name: 'mr-widgets', - full_path: '/root/mr-widgets', - full_name: 'Administrator / mr-widgets', - }, - triggered_by: null, - triggered: [], - }, - pipelineScheduleUrl: 'foo', - pipelineKey: 'id', - viewType: 'root', - }; -}; - -export const mockFailedJobsQueryResponse = { - data: { - project: { - __typename: 'Project', - id: 'gid://gitlab/Project/20', - pipeline: { - __typename: 'Pipeline', - id: 'gid://gitlab/Ci::Pipeline/300', - jobs: { - __typename: 'CiJobConnection', - nodes: [ - { - __typename: 'CiJob', - status: 'FAILED', - detailedStatus: { - __typename: 'DetailedStatus', - id: 'failed-1848-1848', - detailsPath: '/root/ci-project/-/jobs/1848', - group: 'failed', - icon: 'status_failed', - label: 'failed', - text: 'failed', - tooltip: 'failed - (script failure)', - action: { - __typename: 'StatusAction', - id: 'Ci::Build-failed-1848', - buttonTitle: 'Retry this job', - icon: 'retry', - method: 'post', - path: '/root/ci-project/-/jobs/1848/retry', - title: 'Retry', - }, - }, - id: 'gid://gitlab/Ci::Build/1848', - stage: { - __typename: 'CiStage', - id: 'gid://gitlab/Ci::Stage/358', - name: 'build', - }, - name: 'wait_job', - retryable: true, - userPermissions: { - __typename: 'JobPermissions', - readBuild: true, - updateBuild: true, - }, - trace: { - htmlSummary: '<span>Html Summary</span>', - }, - failureMessage: 'Failed', - }, - { - __typename: 'CiJob', - status: 'FAILED', - detailedStatus: { - __typename: 'DetailedStatus', - id: 'failed-1710-1710', - detailsPath: '/root/ci-project/-/jobs/1710', - group: 'failed', - icon: 'status_failed', - label: 'failed', - text: 'failed', - tooltip: 'failed - (script failure) (retried)', - action: null, - }, - id: 'gid://gitlab/Ci::Build/1710', - stage: { - __typename: 'CiStage', - id: 'gid://gitlab/Ci::Stage/358', - name: 'build', - }, - name: 'wait_job', - retryable: false, - userPermissions: { - __typename: 'JobPermissions', - readBuild: true, - updateBuild: true, - }, - trace: null, - failureMessage: 'Failed', - }, - ], - }, - }, - }, - }, -}; - -export const mockFailedJobsData = [ - { - __typename: 'CiJob', - status: 'FAILED', - detailedStatus: { - __typename: 'DetailedStatus', - id: 'failed-1848-1848', - detailsPath: '/root/ci-project/-/jobs/1848', - group: 'failed', - icon: 'status_failed', - label: 'failed', - text: 'failed', - tooltip: 'failed - (script failure)', - action: { - __typename: 'StatusAction', - id: 'Ci::Build-failed-1848', - buttonTitle: 'Retry this job', - icon: 'retry', - method: 'post', - path: '/root/ci-project/-/jobs/1848/retry', - title: 'Retry', - }, - }, - id: 'gid://gitlab/Ci::Build/1848', - stage: { - __typename: 'CiStage', - id: 'gid://gitlab/Ci::Stage/358', - name: 'build', - }, - name: 'wait_job', - retryable: true, - userPermissions: { - __typename: 'JobPermissions', - readBuild: true, - updateBuild: true, - }, - trace: { - htmlSummary: '<span>Html Summary</span>', - }, - failureMessage: 'Job failed', - _showDetails: true, - }, - { - __typename: 'CiJob', - status: 'FAILED', - detailedStatus: { - __typename: 'DetailedStatus', - id: 'failed-1710-1710', - detailsPath: '/root/ci-project/-/jobs/1710', - group: 'failed', - icon: 'status_failed', - label: 'failed', - text: 'failed', - tooltip: 'failed - (script failure) (retried)', - action: null, - }, - id: 'gid://gitlab/Ci::Build/1710', - stage: { - __typename: 'CiStage', - id: 'gid://gitlab/Ci::Stage/358', - name: 'build', - }, - name: 'wait_job', - retryable: false, - userPermissions: { - __typename: 'JobPermissions', - readBuild: true, - updateBuild: true, - }, - trace: null, - failureMessage: 'Job failed', - _showDetails: true, - }, -]; - -export const mockFailedJobsDataNoPermission = [ - { - ...mockFailedJobsData[0], - userPermissions: { __typename: 'JobPermissions', readBuild: false, updateBuild: false }, - }, -]; - -export const successRetryMutationResponse = { - data: { - jobRetry: { - job: { - __typename: 'CiJob', - id: '"gid://gitlab/Ci::Build/1985"', - detailedStatus: { - detailsPath: '/root/project/-/jobs/1985', - id: 'pending-1985-1985', - __typename: 'DetailedStatus', - }, - }, - errors: [], - __typename: 'JobRetryPayload', - }, - }, -}; - -export const failedRetryMutationResponse = { - data: { - jobRetry: { - job: {}, - errors: ['New Error'], - __typename: 'JobRetryPayload', - }, - }, -}; diff --git a/spec/frontend/pipelines/nav_controls_spec.js b/spec/frontend/pipelines/nav_controls_spec.js deleted file mode 100644 index 15de7dc51f1..00000000000 --- a/spec/frontend/pipelines/nav_controls_spec.js +++ /dev/null @@ -1,80 +0,0 @@ -import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; -import NavControls from '~/pipelines/components/pipelines_list/nav_controls.vue'; - -describe('Pipelines Nav Controls', () => { - let wrapper; - - const createComponent = (props) => { - wrapper = shallowMountExtended(NavControls, { - propsData: { - ...props, - }, - }); - }; - - const findRunPipelineButton = () => wrapper.findByTestId('run-pipeline-button'); - const findCiLintButton = () => wrapper.findByTestId('ci-lint-button'); - const findClearCacheButton = () => wrapper.findByTestId('clear-cache-button'); - - it('should render link to create a new pipeline', () => { - const mockData = { - newPipelinePath: 'foo', - ciLintPath: 'foo', - resetCachePath: 'foo', - }; - - createComponent(mockData); - - const runPipelineButton = findRunPipelineButton(); - expect(runPipelineButton.text()).toContain('Run pipeline'); - expect(runPipelineButton.attributes('href')).toBe(mockData.newPipelinePath); - }); - - it('should not render link to create pipeline if no path is provided', () => { - const mockData = { - helpPagePath: 'foo', - ciLintPath: 'foo', - resetCachePath: 'foo', - }; - - createComponent(mockData); - - expect(findRunPipelineButton().exists()).toBe(false); - }); - - it('should render link for CI lint', () => { - const mockData = { - newPipelinePath: 'foo', - helpPagePath: 'foo', - ciLintPath: 'foo', - resetCachePath: 'foo', - }; - - createComponent(mockData); - const ciLintButton = findCiLintButton(); - - expect(ciLintButton.text()).toContain('CI lint'); - expect(ciLintButton.attributes('href')).toBe(mockData.ciLintPath); - }); - - describe('Reset Runners Cache', () => { - beforeEach(() => { - const mockData = { - newPipelinePath: 'foo', - ciLintPath: 'foo', - resetCachePath: 'foo', - }; - createComponent(mockData); - }); - - it('should render button for resetting runner caches', () => { - expect(findClearCacheButton().text()).toContain('Clear runner caches'); - }); - - it('should emit postAction event when reset runner cache button is clicked', () => { - findClearCacheButton().vm.$emit('click'); - - expect(wrapper.emitted('resetRunnersCache')).toEqual([['foo']]); - }); - }); -}); diff --git a/spec/frontend/pipelines/notification/mock_data.js b/spec/frontend/pipelines/notification/mock_data.js deleted file mode 100644 index e36f391a854..00000000000 --- a/spec/frontend/pipelines/notification/mock_data.js +++ /dev/null @@ -1,33 +0,0 @@ -const randomWarning = { - content: 'another random warning', - id: 'gid://gitlab/Ci::PipelineMessage/272', -}; - -const rootTypeWarning = { - content: 'root `types` will be removed in 15.0.', - id: 'gid://gitlab/Ci::PipelineMessage/273', -}; - -const typeWarning = { - content: '`type` will be removed in 15.0.', - id: 'gid://gitlab/Ci::PipelineMessage/274', -}; - -function createWarningMock(warnings) { - return { - data: { - project: { - id: 'gid://gitlab/Project/28"', - pipeline: { - id: 'gid://gitlab/Ci::Pipeline/183', - warningMessages: warnings, - }, - }, - }, - }; -} - -export const mockWarningsWithoutDeprecation = createWarningMock([randomWarning]); -export const mockWarningsRootType = createWarningMock([rootTypeWarning]); -export const mockWarningsType = createWarningMock([typeWarning]); -export const mockWarningsTypesAll = createWarningMock([rootTypeWarning, typeWarning]); diff --git a/spec/frontend/pipelines/pipeline_details_header_spec.js b/spec/frontend/pipelines/pipeline_details_header_spec.js deleted file mode 100644 index 5c75020afad..00000000000 --- a/spec/frontend/pipelines/pipeline_details_header_spec.js +++ /dev/null @@ -1,452 +0,0 @@ -import { GlAlert, GlBadge, GlLoadingIcon, GlModal, GlSprintf } from '@gitlab/ui'; -import Vue, { nextTick } from 'vue'; -import VueApollo from 'vue-apollo'; -import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; -import createMockApollo from 'helpers/mock_apollo_helper'; -import waitForPromises from 'helpers/wait_for_promises'; -import { getIdFromGraphQLId } from '~/graphql_shared/utils'; -import PipelineDetailsHeader from '~/pipelines/components/pipeline_details_header.vue'; -import { BUTTON_TOOLTIP_RETRY, BUTTON_TOOLTIP_CANCEL } from '~/pipelines/constants'; -import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue'; -import cancelPipelineMutation from '~/pipelines/graphql/mutations/cancel_pipeline.mutation.graphql'; -import deletePipelineMutation from '~/pipelines/graphql/mutations/delete_pipeline.mutation.graphql'; -import retryPipelineMutation from '~/pipelines/graphql/mutations/retry_pipeline.mutation.graphql'; -import getPipelineDetailsQuery from '~/pipelines/graphql/queries/get_pipeline_header_data.query.graphql'; -import { - pipelineHeaderSuccess, - pipelineHeaderRunning, - pipelineHeaderRunningWithDuration, - pipelineHeaderFailed, - pipelineRetryMutationResponseSuccess, - pipelineCancelMutationResponseSuccess, - pipelineDeleteMutationResponseSuccess, - pipelineRetryMutationResponseFailed, - pipelineCancelMutationResponseFailed, - pipelineDeleteMutationResponseFailed, -} from './mock_data'; - -Vue.use(VueApollo); - -describe('Pipeline details header', () => { - let wrapper; - let glModalDirective; - - const successHandler = jest.fn().mockResolvedValue(pipelineHeaderSuccess); - const runningHandler = jest.fn().mockResolvedValue(pipelineHeaderRunning); - const runningHandlerWithDuration = jest.fn().mockResolvedValue(pipelineHeaderRunningWithDuration); - const failedHandler = jest.fn().mockResolvedValue(pipelineHeaderFailed); - - const retryMutationHandlerSuccess = jest - .fn() - .mockResolvedValue(pipelineRetryMutationResponseSuccess); - const cancelMutationHandlerSuccess = jest - .fn() - .mockResolvedValue(pipelineCancelMutationResponseSuccess); - const deleteMutationHandlerSuccess = jest - .fn() - .mockResolvedValue(pipelineDeleteMutationResponseSuccess); - const retryMutationHandlerFailed = jest - .fn() - .mockResolvedValue(pipelineRetryMutationResponseFailed); - const cancelMutationHandlerFailed = jest - .fn() - .mockResolvedValue(pipelineCancelMutationResponseFailed); - const deleteMutationHandlerFailed = jest - .fn() - .mockResolvedValue(pipelineDeleteMutationResponseFailed); - - const findAlert = () => wrapper.findComponent(GlAlert); - const findStatus = () => wrapper.findComponent(CiBadgeLink); - const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); - const findAllBadges = () => wrapper.findAllComponents(GlBadge); - const findDeleteModal = () => wrapper.findComponent(GlModal); - const findCreatedTimeAgo = () => wrapper.findByTestId('pipeline-created-time-ago'); - const findFinishedTimeAgo = () => wrapper.findByTestId('pipeline-finished-time-ago'); - const findPipelineName = () => wrapper.findByTestId('pipeline-name'); - const findCommitTitle = () => wrapper.findByTestId('pipeline-commit-title'); - const findTotalJobs = () => wrapper.findByTestId('total-jobs'); - const findComputeMinutes = () => wrapper.findByTestId('compute-minutes'); - const findCommitLink = () => wrapper.findByTestId('commit-link'); - const findPipelineRunningText = () => wrapper.findByTestId('pipeline-running-text').text(); - const findPipelineRefText = () => wrapper.findByTestId('pipeline-ref-text').text(); - const findRetryButton = () => wrapper.findByTestId('retry-pipeline'); - const findCancelButton = () => wrapper.findByTestId('cancel-pipeline'); - const findDeleteButton = () => wrapper.findByTestId('delete-pipeline'); - const findPipelineUserLink = () => wrapper.findByTestId('pipeline-user-link'); - const findPipelineDuration = () => wrapper.findByTestId('pipeline-duration-text'); - - const defaultHandlers = [[getPipelineDetailsQuery, successHandler]]; - - const defaultProvideOptions = { - pipelineIid: 1, - paths: { - pipelinesPath: '/namespace/my-project/-/pipelines', - fullProject: '/namespace/my-project', - triggeredByPath: '', - }, - }; - - const defaultProps = { - name: 'Ruby 3.0 master branch pipeline', - totalJobs: '50', - computeMinutes: '0.65', - yamlErrors: 'errors', - failureReason: 'pipeline failed', - badges: { - schedule: true, - child: false, - latest: true, - mergeTrainPipeline: false, - invalid: false, - failed: false, - autoDevops: false, - detached: false, - stuck: false, - }, - refText: - 'Related merge request <a class="mr-iid" href="/root/ci-project/-/merge_requests/1">!1</a> to merge <a class="ref-name" href="/root/ci-project/-/commits/test">test</a>', - }; - - const createMockApolloProvider = (handlers) => { - return createMockApollo(handlers); - }; - - const createComponent = (handlers = defaultHandlers, props = defaultProps) => { - glModalDirective = jest.fn(); - - wrapper = shallowMountExtended(PipelineDetailsHeader, { - provide: { - ...defaultProvideOptions, - }, - propsData: { - ...props, - }, - directives: { - glModal: { - bind(_, { value }) { - glModalDirective(value); - }, - }, - }, - stubs: { GlSprintf }, - apolloProvider: createMockApolloProvider(handlers), - }); - }; - - describe('loading state', () => { - it('shows a loading state while graphQL is fetching initial data', () => { - createComponent(); - - expect(findLoadingIcon().exists()).toBe(true); - }); - }); - - describe('defaults', () => { - beforeEach(async () => { - createComponent(); - - await waitForPromises(); - }); - - it('does not display loading icon', () => { - expect(findLoadingIcon().exists()).toBe(false); - }); - - it('displays pipeline status', () => { - expect(findStatus().exists()).toBe(true); - }); - - it('displays pipeline name', () => { - expect(findPipelineName().text()).toBe(defaultProps.name); - }); - - it('displays total jobs', () => { - expect(findTotalJobs().text()).toBe('50 Jobs'); - }); - - it('has link to commit', () => { - const { - data: { - project: { pipeline }, - }, - } = pipelineHeaderSuccess; - - expect(findCommitLink().attributes('href')).toBe(pipeline.commit.webPath); - }); - - it('displays correct badges', () => { - expect(findAllBadges()).toHaveLength(2); - expect(wrapper.findByText('latest').exists()).toBe(true); - expect(wrapper.findByText('Scheduled').exists()).toBe(true); - }); - - it('displays ref text', () => { - expect(findPipelineRefText()).toBe('Related merge request !1 to merge test'); - }); - - it('displays pipeline user link with required user popover attributes', () => { - const { - data: { - project: { - pipeline: { user }, - }, - }, - } = pipelineHeaderSuccess; - - const userId = getIdFromGraphQLId(user.id).toString(); - - expect(findPipelineUserLink().classes()).toContain('js-user-link'); - expect(findPipelineUserLink().attributes('data-user-id')).toBe(userId); - expect(findPipelineUserLink().attributes('data-username')).toBe(user.username); - expect(findPipelineUserLink().attributes('href')).toBe(user.webUrl); - }); - }); - - describe('without pipeline name', () => { - it('displays commit title', async () => { - createComponent(defaultHandlers, { ...defaultProps, name: '' }); - - await waitForPromises(); - - const expectedTitle = pipelineHeaderSuccess.data.project.pipeline.commit.title; - - expect(findPipelineName().exists()).toBe(false); - expect(findCommitTitle().text()).toBe(expectedTitle); - }); - }); - - describe('finished pipeline', () => { - it('displays compute minutes when not zero', async () => { - createComponent(); - - await waitForPromises(); - - expect(findComputeMinutes().text()).toBe('0.65'); - }); - - it('does not display compute minutes when zero', async () => { - createComponent(defaultHandlers, { ...defaultProps, computeMinutes: '0.0' }); - - await waitForPromises(); - - expect(findComputeMinutes().exists()).toBe(false); - }); - - it('does not display created time ago', async () => { - createComponent(); - - await waitForPromises(); - - expect(findCreatedTimeAgo().exists()).toBe(false); - }); - - it('displays finished time ago', async () => { - createComponent(); - - await waitForPromises(); - - expect(findFinishedTimeAgo().exists()).toBe(true); - }); - - it('displays pipeline duartion text', async () => { - createComponent(); - - await waitForPromises(); - - expect(findPipelineDuration().text()).toBe( - '120 minutes 10 seconds, queued for 3,600 seconds', - ); - }); - }); - - describe('running pipeline', () => { - beforeEach(async () => { - createComponent([[getPipelineDetailsQuery, runningHandler]]); - - await waitForPromises(); - }); - - it('does not display compute minutes', () => { - expect(findComputeMinutes().exists()).toBe(false); - }); - - it('does not display finished time ago', () => { - expect(findFinishedTimeAgo().exists()).toBe(false); - }); - - it('does not display pipeline duration text', () => { - expect(findPipelineDuration().exists()).toBe(false); - }); - - it('displays pipeline running text', () => { - expect(findPipelineRunningText()).toBe('In progress, queued for 3,600 seconds'); - }); - - it('displays created time ago', () => { - expect(findCreatedTimeAgo().exists()).toBe(true); - }); - }); - - describe('running pipeline with duration', () => { - beforeEach(async () => { - createComponent([[getPipelineDetailsQuery, runningHandlerWithDuration]]); - - await waitForPromises(); - }); - - it('does not display pipeline duration text', () => { - expect(findPipelineDuration().exists()).toBe(false); - }); - }); - - describe('actions', () => { - describe('retry action', () => { - beforeEach(async () => { - createComponent([ - [getPipelineDetailsQuery, failedHandler], - [retryPipelineMutation, retryMutationHandlerSuccess], - ]); - - await waitForPromises(); - }); - - it('should call retryPipeline Mutation with pipeline id', () => { - findRetryButton().vm.$emit('click'); - - expect(retryMutationHandlerSuccess).toHaveBeenCalledWith({ - id: pipelineHeaderFailed.data.project.pipeline.id, - }); - expect(findAlert().exists()).toBe(false); - }); - - it('should render retry action tooltip', () => { - expect(findRetryButton().attributes('title')).toBe(BUTTON_TOOLTIP_RETRY); - }); - }); - - describe('retry action failed', () => { - beforeEach(async () => { - createComponent([ - [getPipelineDetailsQuery, failedHandler], - [retryPipelineMutation, retryMutationHandlerFailed], - ]); - - await waitForPromises(); - }); - - it('should display error message on failure', async () => { - findRetryButton().vm.$emit('click'); - - await waitForPromises(); - - expect(findAlert().exists()).toBe(true); - }); - - it('retry button loading state should reset on error', async () => { - findRetryButton().vm.$emit('click'); - - await nextTick(); - - expect(findRetryButton().props('loading')).toBe(true); - - await waitForPromises(); - - expect(findRetryButton().props('loading')).toBe(false); - }); - }); - - describe('cancel action', () => { - it('should call cancelPipeline Mutation with pipeline id', async () => { - createComponent([ - [getPipelineDetailsQuery, runningHandler], - [cancelPipelineMutation, cancelMutationHandlerSuccess], - ]); - - await waitForPromises(); - - findCancelButton().vm.$emit('click'); - - expect(cancelMutationHandlerSuccess).toHaveBeenCalledWith({ - id: pipelineHeaderRunning.data.project.pipeline.id, - }); - expect(findAlert().exists()).toBe(false); - }); - - it('should render cancel action tooltip', async () => { - createComponent([ - [getPipelineDetailsQuery, runningHandler], - [cancelPipelineMutation, cancelMutationHandlerSuccess], - ]); - - await waitForPromises(); - - expect(findCancelButton().attributes('title')).toBe(BUTTON_TOOLTIP_CANCEL); - }); - - it('should display error message on failure', async () => { - createComponent([ - [getPipelineDetailsQuery, runningHandler], - [cancelPipelineMutation, cancelMutationHandlerFailed], - ]); - - await waitForPromises(); - - findCancelButton().vm.$emit('click'); - - await waitForPromises(); - - expect(findAlert().exists()).toBe(true); - }); - }); - - describe('delete action', () => { - it('displays delete modal when clicking on delete and does not call the delete action', async () => { - createComponent([ - [getPipelineDetailsQuery, successHandler], - [deletePipelineMutation, deleteMutationHandlerSuccess], - ]); - - await waitForPromises(); - - findDeleteButton().vm.$emit('click'); - - const modalId = 'pipeline-delete-modal'; - - expect(findDeleteModal().props('modalId')).toBe(modalId); - expect(glModalDirective).toHaveBeenCalledWith(modalId); - expect(deleteMutationHandlerSuccess).not.toHaveBeenCalled(); - expect(findAlert().exists()).toBe(false); - }); - - it('should call deletePipeline Mutation with pipeline id when modal is submitted', async () => { - createComponent([ - [getPipelineDetailsQuery, successHandler], - [deletePipelineMutation, deleteMutationHandlerSuccess], - ]); - - await waitForPromises(); - - findDeleteModal().vm.$emit('primary'); - - expect(deleteMutationHandlerSuccess).toHaveBeenCalledWith({ - id: pipelineHeaderSuccess.data.project.pipeline.id, - }); - }); - - it('should display error message on failure', async () => { - createComponent([ - [getPipelineDetailsQuery, successHandler], - [deletePipelineMutation, deleteMutationHandlerFailed], - ]); - - await waitForPromises(); - - findDeleteModal().vm.$emit('primary'); - - await waitForPromises(); - - expect(findAlert().exists()).toBe(true); - }); - }); - }); -}); diff --git a/spec/frontend/pipelines/pipeline_graph/mock_data.js b/spec/frontend/pipelines/pipeline_graph/mock_data.js deleted file mode 100644 index db77e0a0573..00000000000 --- a/spec/frontend/pipelines/pipeline_graph/mock_data.js +++ /dev/null @@ -1,283 +0,0 @@ -export const yamlString = `stages: -- empty -- build -- test -- deploy -- final - -include: -- template: 'Workflows/MergeRequest-Pipelines.gitlab-ci.yml' - -build_a: - stage: build - script: echo hello -build_b: - stage: build - script: echo hello -build_c: - stage: build - script: echo hello -build_d: - stage: Queen - script: echo hello - -test_a: - stage: test - script: ls - needs: [build_a, build_b, build_c] -test_b: - stage: test - script: ls - needs: [build_a, build_b, build_d] -test_c: - stage: test - script: ls - needs: [build_a, build_b, build_c] - -deploy_a: - stage: deploy - script: echo hello -`; - -export const pipelineDataWithNoNeeds = { - stages: [ - { - name: 'build', - groups: [ - { - name: 'build_1', - jobs: [{ script: 'echo hello', stage: 'build' }], - }, - ], - }, - { - name: 'test', - groups: [ - { - name: 'test_1', - jobs: [{ script: 'yarn test', stage: 'test' }], - }, - ], - }, - ], -}; - -export const pipelineData = { - stages: [ - { - name: 'build', - groups: [ - { - name: 'build_1', - jobs: [{ script: 'echo hello', stage: 'build' }], - }, - ], - }, - { - name: 'test', - groups: [ - { - name: 'test_1', - jobs: [{ script: 'yarn test', stage: 'test' }], - }, - { - name: 'test_2', - jobs: [{ script: 'yarn karma', stage: 'test' }], - }, - ], - }, - { - name: 'deploy', - groups: [ - { - name: 'deploy_1', - jobs: [{ script: 'yarn magick', stage: 'deploy', needs: ['test_1'] }], - }, - ], - }, - ], -}; - -export const invalidNeedsData = { - stages: [ - { - name: 'build', - groups: [ - { - name: 'build_1', - jobs: [{ script: 'echo hello', stage: 'build' }], - }, - ], - }, - { - name: 'test', - groups: [ - { - name: 'test_1', - jobs: [{ script: 'yarn test', stage: 'test' }], - }, - { - name: 'test_2', - jobs: [{ script: 'yarn karma', stage: 'test' }], - }, - ], - }, - { - name: 'deploy', - groups: [ - { - name: 'deploy_1', - jobs: [{ script: 'yarn magick', stage: 'deploy', needs: ['invalid_job'] }], - }, - ], - }, - ], -}; - -export const parallelNeedData = { - stages: [ - { - name: 'build', - groups: [ - { - name: 'build_1', - parallel: 3, - jobs: [ - { script: 'echo hello', stage: 'build', name: 'build_1 1/3' }, - { script: 'echo hello', stage: 'build', name: 'build_1 2/3' }, - { script: 'echo hello', stage: 'build', name: 'build_1 3/3' }, - ], - }, - ], - }, - { - name: 'test', - groups: [ - { - name: 'test_1', - jobs: [{ script: 'yarn test', stage: 'test', needs: ['build_1'] }], - }, - ], - }, - ], -}; - -export const sameStageNeeds = { - stages: [ - { - name: 'build', - groups: [ - { - name: 'build_1', - jobs: [{ script: 'echo hello', stage: 'build', name: 'build_1' }], - }, - ], - }, - { - name: 'build', - groups: [ - { - name: 'build_2', - jobs: [{ script: 'yarn test', stage: 'build', needs: ['build_1'] }], - }, - ], - }, - { - name: 'build', - groups: [ - { - name: 'build_3', - jobs: [{ script: 'yarn test', stage: 'build', needs: ['build_2'] }], - }, - ], - }, - ], -}; - -export const largePipelineData = { - stages: [ - { - name: 'build', - groups: [ - { - name: 'build_1', - jobs: [{ script: 'echo hello', stage: 'build' }], - }, - { - name: 'build_2', - jobs: [{ script: 'echo hello', stage: 'build' }], - }, - { - name: 'build_3', - jobs: [{ script: 'echo hello', stage: 'build' }], - }, - ], - }, - { - name: 'test', - groups: [ - { - name: 'test_1', - jobs: [{ script: 'yarn test', stage: 'test', needs: ['build_2'] }], - }, - { - name: 'test_2', - jobs: [{ script: 'yarn karma', stage: 'test', needs: ['build_2'] }], - }, - ], - }, - { - name: 'deploy', - groups: [ - { - name: 'deploy_1', - jobs: [{ script: 'yarn magick', stage: 'deploy', needs: ['test_1'] }], - }, - { - name: 'deploy_2', - jobs: [{ script: 'yarn magick', stage: 'deploy', needs: ['build_3'] }], - }, - { - name: 'deploy_3', - jobs: [{ script: 'yarn magick', stage: 'deploy', needs: ['test_2'] }], - }, - ], - }, - ], -}; - -export const singleStageData = { - stages: [ - { - name: 'build', - groups: [ - { - name: 'build_1', - jobs: [{ script: 'echo hello', stage: 'build' }], - }, - ], - }, - ], -}; - -export const rootRect = { - bottom: 463, - height: 271, - left: 236, - right: 1252, - top: 192, - width: 1016, - x: 236, - y: 192, -}; - -export const jobRect = { - bottom: 312, - height: 24, - left: 308, - right: 428, - top: 288, - width: 120, - x: 308, - y: 288, -}; diff --git a/spec/frontend/pipelines/pipeline_graph/pipeline_graph_spec.js b/spec/frontend/pipelines/pipeline_graph/pipeline_graph_spec.js deleted file mode 100644 index 123f2e011c3..00000000000 --- a/spec/frontend/pipelines/pipeline_graph/pipeline_graph_spec.js +++ /dev/null @@ -1,100 +0,0 @@ -import { GlAlert } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; -import { setHTMLFixture } from 'helpers/fixtures'; -import { CI_CONFIG_STATUS_VALID } from '~/ci/pipeline_editor/constants'; -import LinksInner from '~/pipelines/components/graph_shared/links_inner.vue'; -import LinksLayer from '~/pipelines/components/graph_shared/links_layer.vue'; -import JobPill from '~/pipelines/components/pipeline_graph/job_pill.vue'; -import PipelineGraph from '~/pipelines/components/pipeline_graph/pipeline_graph.vue'; -import StageName from '~/pipelines/components/pipeline_graph/stage_name.vue'; -import { pipelineData, singleStageData } from './mock_data'; - -describe('pipeline graph component', () => { - const defaultProps = { pipelineData }; - let wrapper; - - const containerId = 'pipeline-graph-container-0'; - setHTMLFixture(`<div id="${containerId}"></div>`); - - const createComponent = (props = defaultProps) => { - return shallowMount(PipelineGraph, { - propsData: { - ...props, - }, - stubs: { LinksLayer, LinksInner }, - data() { - return { - measurements: { - width: 1000, - height: 1000, - }, - }; - }, - }); - }; - - const findAlert = () => wrapper.findComponent(GlAlert); - const findAllJobPills = () => wrapper.findAllComponents(JobPill); - const findAllStageNames = () => wrapper.findAllComponents(StageName); - const findLinksLayer = () => wrapper.findComponent(LinksLayer); - const findPipelineGraph = () => wrapper.find('[data-testid="graph-container"]'); - - describe('with `VALID` status', () => { - beforeEach(() => { - wrapper = createComponent({ - pipelineData: { - status: CI_CONFIG_STATUS_VALID, - stages: [{ name: 'hello', groups: [] }], - }, - }); - }); - - it('renders the graph with no status error', () => { - expect(findAlert().exists()).toBe(false); - expect(findPipelineGraph().exists()).toBe(true); - expect(findLinksLayer().exists()).toBe(true); - }); - }); - - describe('with only one stage', () => { - beforeEach(() => { - wrapper = createComponent({ pipelineData: singleStageData }); - }); - - it('renders the right number of stage titles', () => { - const expectedStagesLength = singleStageData.stages.length; - - expect(findAllStageNames()).toHaveLength(expectedStagesLength); - }); - - it('renders the right number of job pills', () => { - // We count the number of jobs in the mock data - const expectedJobsLength = singleStageData.stages.reduce((acc, val) => { - return acc + val.groups.length; - }, 0); - - expect(findAllJobPills()).toHaveLength(expectedJobsLength); - }); - }); - - describe('with multiple stages and jobs', () => { - beforeEach(() => { - wrapper = createComponent(); - }); - - it('renders the right number of stage titles', () => { - const expectedStagesLength = pipelineData.stages.length; - - expect(findAllStageNames()).toHaveLength(expectedStagesLength); - }); - - it('renders the right number of job pills', () => { - // We count the number of jobs in the mock data - const expectedJobsLength = pipelineData.stages.reduce((acc, val) => { - return acc + val.groups.length; - }, 0); - - expect(findAllJobPills()).toHaveLength(expectedJobsLength); - }); - }); -}); diff --git a/spec/frontend/pipelines/pipeline_graph/utils_spec.js b/spec/frontend/pipelines/pipeline_graph/utils_spec.js deleted file mode 100644 index 96b18fcf96f..00000000000 --- a/spec/frontend/pipelines/pipeline_graph/utils_spec.js +++ /dev/null @@ -1,197 +0,0 @@ -import { createJobsHash, generateJobNeedsDict, getPipelineDefaultTab } from '~/pipelines/utils'; -import { validPipelineTabNames, pipelineTabName } from '~/pipelines/constants'; - -describe('utils functions', () => { - const jobName1 = 'build_1'; - const jobName2 = 'build_2'; - const jobName3 = 'test_1'; - const jobName4 = 'deploy_1'; - const job1 = { name: jobName1, script: 'echo hello', stage: 'build' }; - const job2 = { name: jobName2, script: 'echo build', stage: 'build' }; - const job3 = { - name: jobName3, - script: 'echo test', - stage: 'test', - needs: [jobName1, jobName2], - }; - const job4 = { - name: jobName4, - script: 'echo deploy', - stage: 'deploy', - needs: [jobName3], - }; - const userDefinedStage = 'myStage'; - - const pipelineGraphData = { - stages: [ - { - name: userDefinedStage, - groups: [], - }, - { - name: job4.stage, - groups: [ - { - name: jobName4, - jobs: [{ ...job4 }], - }, - ], - }, - { - name: job1.stage, - groups: [ - { - name: jobName1, - jobs: [{ ...job1 }], - }, - { - name: jobName2, - jobs: [{ ...job2 }], - }, - ], - }, - { - name: job3.stage, - groups: [ - { - name: jobName3, - jobs: [{ ...job3 }], - }, - ], - }, - ], - }; - - describe('createJobsHash', () => { - it('returns an empty object if there are no jobs received as argument', () => { - expect(createJobsHash([])).toEqual({}); - }); - - it('returns a hash with the jobname as key and all its data as value', () => { - const jobs = { - [jobName1]: { jobs: [job1], name: jobName1, needs: [] }, - [jobName2]: { jobs: [job2], name: jobName2, needs: [] }, - [jobName3]: { jobs: [job3], name: jobName3, needs: job3.needs }, - [jobName4]: { jobs: [job4], name: jobName4, needs: job4.needs }, - }; - - expect(createJobsHash(pipelineGraphData.stages)).toEqual(jobs); - }); - }); - - describe('generateJobNeedsDict', () => { - it('generates an empty object if it receives no jobs', () => { - expect(generateJobNeedsDict({})).toEqual({}); - }); - - it('generates a dict with empty needs if there are no dependencies', () => { - const smallGraph = { - [jobName1]: job1, - [jobName2]: job2, - }; - - expect(generateJobNeedsDict(smallGraph)).toEqual({ - [jobName1]: [], - [jobName2]: [], - }); - }); - - it('generates a dict where key is the a job and its value is an array of all its needs', () => { - const jobsWithNeeds = { - [jobName1]: job1, - [jobName2]: job2, - [jobName3]: job3, - [jobName4]: job4, - }; - - expect(generateJobNeedsDict(jobsWithNeeds)).toEqual({ - [jobName1]: [], - [jobName2]: [], - [jobName3]: [jobName1, jobName2], - [jobName4]: [jobName3, jobName1, jobName2], - }); - }); - - it('removes needs which are not in the data', () => { - const inexistantJobName = 'job5'; - const jobsWithNeeds = { - [jobName1]: job1, - [jobName2]: job2, - [jobName3]: job3, - [jobName4]: { - name: jobName4, - script: 'echo deploy', - stage: 'deploy', - needs: [inexistantJobName], - }, - }; - - expect(generateJobNeedsDict(jobsWithNeeds)).toEqual({ - [jobName1]: [], - [jobName2]: [], - [jobName3]: [jobName1, jobName2], - [jobName4]: [], - }); - }); - - it('handles parallel jobs by adding the group name as a need', () => { - const size = 3; - const jobOptimize1 = 'optimize_1'; - const jobPrepareA = 'prepare_a'; - const jobPrepareA1 = `${jobPrepareA} 1/${size}`; - const jobPrepareA2 = `${jobPrepareA} 2/${size}`; - const jobPrepareA3 = `${jobPrepareA} 3/${size}`; - - const jobsParallel = { - [jobOptimize1]: { - jobs: [job1], - name: [jobOptimize1], - needs: [jobPrepareA1, jobPrepareA2, jobPrepareA3], - }, - [jobPrepareA]: { jobs: [], name: jobPrepareA, needs: [], size }, - [jobPrepareA1]: { jobs: [], name: jobPrepareA, needs: [], size }, - [jobPrepareA2]: { jobs: [], name: jobPrepareA, needs: [], size }, - [jobPrepareA3]: { jobs: [], name: jobPrepareA, needs: [], size }, - }; - - expect(generateJobNeedsDict(jobsParallel)).toEqual({ - [jobOptimize1]: [ - jobPrepareA1, - // This is the important part, the `jobPrepareA` group name has been - // added to our list of needs. - jobPrepareA, - jobPrepareA2, - jobPrepareA3, - ], - [jobPrepareA]: [], - [jobPrepareA1]: [], - [jobPrepareA2]: [], - [jobPrepareA3]: [], - }); - }); - }); - - describe('getPipelineDefaultTab', () => { - const baseUrl = 'http://gitlab.com/user/multi-projects-small/-/pipelines/332/'; - it('returns pipeline tab name if there is only the base url', () => { - expect(getPipelineDefaultTab(baseUrl)).toBe(pipelineTabName); - }); - - it('returns null if there was no valid last url part', () => { - expect(getPipelineDefaultTab(`${baseUrl}something`)).toBe(null); - }); - - it('returns the correct tab name if present', () => { - validPipelineTabNames.forEach((tabName) => { - expect(getPipelineDefaultTab(`${baseUrl}${tabName}`)).toBe(tabName); - }); - }); - - it('returns the right value even with query params', () => { - const [tabName] = validPipelineTabNames; - expect(getPipelineDefaultTab(`${baseUrl}${tabName}?query="something"&query2="else"`)).toBe( - tabName, - ); - }); - }); -}); diff --git a/spec/frontend/pipelines/pipeline_labels_spec.js b/spec/frontend/pipelines/pipeline_labels_spec.js deleted file mode 100644 index 6a37e36352b..00000000000 --- a/spec/frontend/pipelines/pipeline_labels_spec.js +++ /dev/null @@ -1,164 +0,0 @@ -import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; -import { trimText } from 'helpers/text_helper'; -import PipelineLabelsComponent from '~/pipelines/components/pipelines_list/pipeline_labels.vue'; -import { mockPipeline } from './mock_data'; - -const projectPath = 'test/test'; - -describe('Pipeline label component', () => { - let wrapper; - - const findScheduledTag = () => wrapper.findByTestId('pipeline-url-scheduled'); - const findLatestTag = () => wrapper.findByTestId('pipeline-url-latest'); - const findYamlTag = () => wrapper.findByTestId('pipeline-url-yaml'); - const findStuckTag = () => wrapper.findByTestId('pipeline-url-stuck'); - const findAutoDevopsTag = () => wrapper.findByTestId('pipeline-url-autodevops'); - const findAutoDevopsTagLink = () => wrapper.findByTestId('pipeline-url-autodevops-link'); - const findDetachedTag = () => wrapper.findByTestId('pipeline-url-detached'); - const findFailureTag = () => wrapper.findByTestId('pipeline-url-failure'); - const findForkTag = () => wrapper.findByTestId('pipeline-url-fork'); - const findTrainTag = () => wrapper.findByTestId('pipeline-url-train'); - - const defaultProps = mockPipeline(projectPath); - - const createComponent = (props) => { - wrapper = shallowMountExtended(PipelineLabelsComponent, { - propsData: { ...defaultProps, ...props }, - provide: { - targetProjectFullPath: projectPath, - }, - }); - }; - - it('should not render tags when flags are not set', () => { - createComponent(); - - expect(findStuckTag().exists()).toBe(false); - expect(findLatestTag().exists()).toBe(false); - expect(findYamlTag().exists()).toBe(false); - expect(findAutoDevopsTag().exists()).toBe(false); - expect(findFailureTag().exists()).toBe(false); - expect(findScheduledTag().exists()).toBe(false); - expect(findForkTag().exists()).toBe(false); - expect(findTrainTag().exists()).toBe(false); - }); - - it('should render the stuck tag when flag is provided', () => { - const stuckPipeline = defaultProps.pipeline; - stuckPipeline.flags.stuck = true; - - createComponent({ - ...stuckPipeline.pipeline, - }); - - expect(findStuckTag().text()).toContain('stuck'); - }); - - it('should render latest tag when flag is provided', () => { - const latestPipeline = defaultProps.pipeline; - latestPipeline.flags.latest = true; - - createComponent({ - ...latestPipeline, - }); - - expect(findLatestTag().text()).toContain('latest'); - }); - - it('should render a yaml badge when it is invalid', () => { - const yamlPipeline = defaultProps.pipeline; - yamlPipeline.flags.yaml_errors = true; - - createComponent({ - ...yamlPipeline, - }); - - expect(findYamlTag().text()).toContain('yaml invalid'); - }); - - it('should render an autodevops badge when flag is provided', () => { - const autoDevopsPipeline = defaultProps.pipeline; - autoDevopsPipeline.flags.auto_devops = true; - - createComponent({ - ...autoDevopsPipeline, - }); - - expect(trimText(findAutoDevopsTag().text())).toBe('Auto DevOps'); - - expect(findAutoDevopsTagLink().attributes()).toMatchObject({ - href: '/help/topics/autodevops/index.md', - target: '_blank', - }); - }); - - it('should render a detached badge when flag is provided', () => { - const detachedMRPipeline = defaultProps.pipeline; - detachedMRPipeline.flags.detached_merge_request_pipeline = true; - - createComponent({ - ...detachedMRPipeline, - }); - - expect(findDetachedTag().text()).toBe('merge request'); - }); - - it('should render error badge when pipeline has a failure reason set', () => { - const failedPipeline = defaultProps.pipeline; - failedPipeline.flags.failure_reason = true; - failedPipeline.failure_reason = 'some reason'; - - createComponent({ - ...failedPipeline, - }); - - expect(findFailureTag().text()).toContain('error'); - expect(findFailureTag().attributes('title')).toContain('some reason'); - }); - - it('should render scheduled badge when pipeline was triggered by a schedule', () => { - const scheduledPipeline = defaultProps.pipeline; - scheduledPipeline.source = 'schedule'; - - createComponent({ - ...scheduledPipeline, - }); - - expect(findScheduledTag().exists()).toBe(true); - expect(findScheduledTag().text()).toContain('Scheduled'); - }); - - it('should render the fork badge when the pipeline was run in a fork', () => { - const forkedPipeline = defaultProps.pipeline; - forkedPipeline.project.full_path = '/test/forked'; - - createComponent({ - ...forkedPipeline, - }); - - expect(findForkTag().exists()).toBe(true); - expect(findForkTag().text()).toBe('fork'); - }); - - it('should render the train badge when the pipeline is a merge train pipeline', () => { - const mergeTrainPipeline = defaultProps.pipeline; - mergeTrainPipeline.flags.merge_train_pipeline = true; - - createComponent({ - ...mergeTrainPipeline, - }); - - expect(findTrainTag().text()).toBe('merge train'); - }); - - it('should not render the train badge when the pipeline is not a merge train pipeline', () => { - const mergeTrainPipeline = defaultProps.pipeline; - mergeTrainPipeline.flags.merge_train_pipeline = false; - - createComponent({ - ...mergeTrainPipeline, - }); - - expect(findTrainTag().exists()).toBe(false); - }); -}); diff --git a/spec/frontend/pipelines/pipeline_multi_actions_spec.js b/spec/frontend/pipelines/pipeline_multi_actions_spec.js deleted file mode 100644 index 0fdc45a5931..00000000000 --- a/spec/frontend/pipelines/pipeline_multi_actions_spec.js +++ /dev/null @@ -1,288 +0,0 @@ -import { nextTick } from 'vue'; -import { GlAlert, GlDropdown, GlSprintf, GlLoadingIcon, GlSearchBoxByType } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; -import MockAdapter from 'axios-mock-adapter'; -import { mockTracking, unmockTracking } from 'helpers/tracking_helper'; -import { extendedWrapper } from 'helpers/vue_test_utils_helper'; -import waitForPromises from 'helpers/wait_for_promises'; -import { stubComponent } from 'helpers/stub_component'; -import axios from '~/lib/utils/axios_utils'; -import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status'; -import PipelineMultiActions, { - i18n, -} from '~/pipelines/components/pipelines_list/pipeline_multi_actions.vue'; -import { TRACKING_CATEGORIES } from '~/pipelines/constants'; - -describe('Pipeline Multi Actions Dropdown', () => { - let wrapper; - let mockAxios; - const focusInputMock = jest.fn(); - - const artifacts = [ - { - name: 'job my-artifact', - path: '/download/path', - }, - { - name: 'job-2 my-artifact-2', - path: '/download/path-two', - }, - ]; - const newArtifacts = [ - { - name: 'job-3 my-new-artifact', - path: '/new/download/path', - }, - { - name: 'job-4 my-new-artifact-2', - path: '/new/download/path-two', - }, - { - name: 'job-5 my-new-artifact-3', - path: '/new/download/path-three', - }, - ]; - const artifactItemTestId = 'artifact-item'; - const artifactsEndpointPlaceholder = ':pipeline_artifacts_id'; - const artifactsEndpoint = `endpoint/${artifactsEndpointPlaceholder}/artifacts.json`; - const pipelineId = 108; - - const createComponent = () => { - wrapper = extendedWrapper( - shallowMount(PipelineMultiActions, { - provide: { - artifactsEndpoint, - artifactsEndpointPlaceholder, - }, - propsData: { - pipelineId, - }, - stubs: { - GlSprintf, - GlDropdown, - GlSearchBoxByType: stubComponent(GlSearchBoxByType, { - methods: { focusInput: focusInputMock }, - }), - }, - }), - ); - }; - - const findAlert = () => wrapper.findComponent(GlAlert); - const findDropdown = () => wrapper.findComponent(GlDropdown); - const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); - const findAllArtifactItems = () => wrapper.findAllByTestId(artifactItemTestId); - const findFirstArtifactItem = () => wrapper.findByTestId(artifactItemTestId); - const findAllArtifactItemsData = () => - wrapper.findAllByTestId(artifactItemTestId).wrappers.map((x) => ({ - path: x.attributes('href'), - name: x.text(), - })); - const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType); - const findEmptyMessage = () => wrapper.findByTestId('artifacts-empty-message'); - const findWarning = () => wrapper.findByTestId('artifacts-fetch-warning'); - const changePipelineId = (newId) => wrapper.setProps({ pipelineId: newId }); - - beforeEach(() => { - mockAxios = new MockAdapter(axios); - }); - - afterEach(() => { - mockAxios.restore(); - }); - - it('should render the dropdown', () => { - createComponent(); - - expect(findDropdown().exists()).toBe(true); - }); - - describe('Artifacts', () => { - const endpoint = artifactsEndpoint.replace(artifactsEndpointPlaceholder, pipelineId); - - describe('while loading artifacts', () => { - beforeEach(() => { - mockAxios.onGet(endpoint).replyOnce(HTTP_STATUS_OK, { artifacts }); - }); - - it('should render a loading spinner and no empty message', async () => { - createComponent(); - - findDropdown().vm.$emit('show'); - await nextTick(); - - expect(findLoadingIcon().exists()).toBe(true); - expect(findEmptyMessage().exists()).toBe(false); - }); - }); - - describe('artifacts loaded successfully', () => { - describe('artifacts exist', () => { - beforeEach(async () => { - mockAxios.onGet(endpoint).replyOnce(HTTP_STATUS_OK, { artifacts }); - - createComponent(); - - findDropdown().vm.$emit('show'); - await waitForPromises(); - }); - - it('should fetch artifacts and show search box on dropdown click', () => { - expect(mockAxios.history.get).toHaveLength(1); - expect(findSearchBox().exists()).toBe(true); - }); - - it('should focus the search box when opened with artifacts', () => { - findDropdown().vm.$emit('shown'); - - expect(focusInputMock).toHaveBeenCalled(); - }); - - it('should render all the provided artifacts when search query is empty', () => { - findSearchBox().vm.$emit('input', ''); - - expect(findAllArtifactItems()).toHaveLength(artifacts.length); - expect(findEmptyMessage().exists()).toBe(false); - }); - - it('should render filtered artifacts when search query is not empty', async () => { - findSearchBox().vm.$emit('input', 'job-2'); - await waitForPromises(); - - expect(findAllArtifactItems()).toHaveLength(1); - expect(findEmptyMessage().exists()).toBe(false); - }); - - it('should render the correct artifact name and path', () => { - expect(findFirstArtifactItem().attributes('href')).toBe(artifacts[0].path); - expect(findFirstArtifactItem().text()).toBe(artifacts[0].name); - }); - - describe('when opened again with new artifacts', () => { - describe('with a successful refetch', () => { - beforeEach(async () => { - mockAxios.resetHistory(); - mockAxios.onGet(endpoint).replyOnce(HTTP_STATUS_OK, { artifacts: newArtifacts }); - - findDropdown().vm.$emit('show'); - await nextTick(); - }); - - it('should hide list and render a loading spinner on dropdown click', () => { - expect(findAllArtifactItems()).toHaveLength(0); - expect(findLoadingIcon().exists()).toBe(true); - }); - - it('should not render warning or empty message while loading', () => { - expect(findEmptyMessage().exists()).toBe(false); - expect(findWarning().exists()).toBe(false); - }); - - it('should render the correct new list', async () => { - await waitForPromises(); - - expect(findAllArtifactItemsData()).toEqual(newArtifacts); - }); - }); - - describe('with a failing refetch', () => { - beforeEach(async () => { - mockAxios.onGet(endpoint).replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR); - - findDropdown().vm.$emit('show'); - await waitForPromises(); - }); - - it('should render warning', () => { - expect(findWarning().text()).toBe(i18n.artifactsFetchWarningMessage); - }); - - it('should render old list', () => { - expect(findAllArtifactItemsData()).toEqual(artifacts); - }); - }); - }); - - describe('pipeline id has changed', () => { - const newEndpoint = artifactsEndpoint.replace( - artifactsEndpointPlaceholder, - pipelineId + 1, - ); - - beforeEach(() => { - changePipelineId(pipelineId + 1); - }); - - describe('followed by a failing request', () => { - beforeEach(async () => { - mockAxios.onGet(newEndpoint).replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR); - - findDropdown().vm.$emit('show'); - await waitForPromises(); - }); - - it('should render error message and no warning', () => { - expect(findWarning().exists()).toBe(false); - expect(findAlert().text()).toBe(i18n.artifactsFetchErrorMessage); - }); - - it('should clear list', () => { - expect(findAllArtifactItems()).toHaveLength(0); - }); - }); - }); - }); - - describe('artifacts list is empty', () => { - beforeEach(() => { - mockAxios.onGet(endpoint).replyOnce(HTTP_STATUS_OK, { artifacts: [] }); - }); - - it('should render empty message and no search box when no artifacts are found', async () => { - createComponent(); - - findDropdown().vm.$emit('show'); - await waitForPromises(); - - expect(findEmptyMessage().exists()).toBe(true); - expect(findSearchBox().exists()).toBe(false); - expect(findLoadingIcon().exists()).toBe(false); - }); - }); - }); - - describe('with a failing request', () => { - beforeEach(() => { - mockAxios.onGet(endpoint).replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR); - }); - - it('should render an error message', async () => { - createComponent(); - findDropdown().vm.$emit('show'); - await waitForPromises(); - - const error = findAlert(); - expect(error.exists()).toBe(true); - expect(error.text()).toBe(i18n.artifactsFetchErrorMessage); - }); - }); - }); - - describe('tracking', () => { - afterEach(() => { - unmockTracking(); - }); - - it('tracks artifacts dropdown click', () => { - const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn); - - createComponent(); - - findDropdown().vm.$emit('show'); - - expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_artifacts_dropdown', { - label: TRACKING_CATEGORIES.table, - }); - }); - }); -}); diff --git a/spec/frontend/pipelines/pipeline_operations_spec.js b/spec/frontend/pipelines/pipeline_operations_spec.js deleted file mode 100644 index b2191453824..00000000000 --- a/spec/frontend/pipelines/pipeline_operations_spec.js +++ /dev/null @@ -1,77 +0,0 @@ -import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; -import PipelinesManualActions from '~/pipelines/components/pipelines_list/pipelines_manual_actions.vue'; -import PipelineMultiActions from '~/pipelines/components/pipelines_list/pipeline_multi_actions.vue'; -import PipelineOperations from '~/pipelines/components/pipelines_list/pipeline_operations.vue'; -import eventHub from '~/pipelines/event_hub'; - -describe('Pipeline operations', () => { - let wrapper; - - const defaultProps = { - pipeline: { - id: 329, - iid: 234, - details: { - has_manual_actions: true, - has_scheduled_actions: false, - }, - flags: { - retryable: true, - cancelable: true, - }, - cancel_path: '/root/ci-project/-/pipelines/329/cancel', - retry_path: '/root/ci-project/-/pipelines/329/retry', - }, - }; - - const createComponent = (props = defaultProps) => { - wrapper = shallowMountExtended(PipelineOperations, { - propsData: { - ...props, - }, - }); - }; - - const findManualActions = () => wrapper.findComponent(PipelinesManualActions); - const findMultiActions = () => wrapper.findComponent(PipelineMultiActions); - const findRetryBtn = () => wrapper.findByTestId('pipelines-retry-button'); - const findCancelBtn = () => wrapper.findByTestId('pipelines-cancel-button'); - - it('should display pipeline manual actions', () => { - createComponent(); - - expect(findManualActions().exists()).toBe(true); - }); - - it('should display pipeline multi actions', () => { - createComponent(); - - expect(findMultiActions().exists()).toBe(true); - }); - - describe('events', () => { - beforeEach(() => { - createComponent(); - - jest.spyOn(eventHub, '$emit').mockImplementation(() => {}); - }); - - it('should emit retryPipeline event', () => { - findRetryBtn().vm.$emit('click'); - - expect(eventHub.$emit).toHaveBeenCalledWith( - 'retryPipeline', - defaultProps.pipeline.retry_path, - ); - }); - - it('should emit openConfirmationModal event', () => { - findCancelBtn().vm.$emit('click'); - - expect(eventHub.$emit).toHaveBeenCalledWith('openConfirmationModal', { - pipeline: defaultProps.pipeline, - endpoint: defaultProps.pipeline.cancel_path, - }); - }); - }); -}); diff --git a/spec/frontend/pipelines/pipeline_tabs_spec.js b/spec/frontend/pipelines/pipeline_tabs_spec.js deleted file mode 100644 index 8d1cd98e981..00000000000 --- a/spec/frontend/pipelines/pipeline_tabs_spec.js +++ /dev/null @@ -1,63 +0,0 @@ -import { createAppOptions } from '~/pipelines/pipeline_tabs'; - -jest.mock('~/lib/utils/url_utility', () => ({ - removeParams: () => 'gitlab.com', - joinPaths: () => {}, - setUrlFragment: () => {}, -})); - -jest.mock('~/pipelines/utils', () => ({ - getPipelineDefaultTab: () => '', -})); - -describe('~/pipelines/pipeline_tabs.js', () => { - describe('createAppOptions', () => { - const SELECTOR = 'SELECTOR'; - - let el; - - const createElement = () => { - el = document.createElement('div'); - el.id = SELECTOR; - el.dataset.canGenerateCodequalityReports = 'true'; - el.dataset.codequalityReportDownloadPath = 'codequalityReportDownloadPath'; - el.dataset.downloadablePathForReportType = 'downloadablePathForReportType'; - el.dataset.exposeSecurityDashboard = 'true'; - el.dataset.exposeLicenseScanningData = 'true'; - el.dataset.failedJobsCount = 1; - el.dataset.graphqlResourceEtag = 'graphqlResourceEtag'; - el.dataset.pipelineIid = '123'; - el.dataset.pipelineProjectPath = 'pipelineProjectPath'; - - document.body.appendChild(el); - }; - - afterEach(() => { - el = null; - }); - - it("extracts the properties from the element's dataset", () => { - createElement(); - const options = createAppOptions(`#${SELECTOR}`, null); - - expect(options).toMatchObject({ - el, - provide: { - canGenerateCodequalityReports: true, - codequalityReportDownloadPath: 'codequalityReportDownloadPath', - downloadablePathForReportType: 'downloadablePathForReportType', - exposeSecurityDashboard: true, - exposeLicenseScanningData: true, - failedJobsCount: '1', - graphqlResourceEtag: 'graphqlResourceEtag', - pipelineIid: '123', - pipelineProjectPath: 'pipelineProjectPath', - }, - }); - }); - - it('returns `null` if el does not exist', () => { - expect(createAppOptions('foo', null)).toBe(null); - }); - }); -}); diff --git a/spec/frontend/pipelines/pipeline_triggerer_spec.js b/spec/frontend/pipelines/pipeline_triggerer_spec.js deleted file mode 100644 index 856c0484075..00000000000 --- a/spec/frontend/pipelines/pipeline_triggerer_spec.js +++ /dev/null @@ -1,76 +0,0 @@ -import { GlAvatar, GlAvatarLink } from '@gitlab/ui'; -import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; -import pipelineTriggerer from '~/pipelines/components/pipelines_list/pipeline_triggerer.vue'; -import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; - -describe('Pipelines Triggerer', () => { - let wrapper; - - const mockData = { - pipeline: { - user: { - name: 'foo', - avatar_url: '/avatar', - path: '/path', - }, - }, - }; - - const createComponent = (props) => { - wrapper = shallowMountExtended(pipelineTriggerer, { - propsData: { - ...props, - }, - directives: { - GlTooltip: createMockDirective('gl-tooltip'), - }, - }); - }; - - const findAvatarLink = () => wrapper.findComponent(GlAvatarLink); - const findAvatar = () => wrapper.findComponent(GlAvatar); - const findTriggerer = () => wrapper.findByText('API'); - - describe('when user was a triggerer', () => { - beforeEach(() => { - createComponent(mockData); - }); - - it('should render pipeline triggerer table cell', () => { - expect(wrapper.find('[data-testid="pipeline-triggerer"]').exists()).toBe(true); - }); - - it('should render only user avatar', () => { - expect(findAvatarLink().exists()).toBe(true); - expect(findTriggerer().exists()).toBe(false); - }); - - it('should set correct props on avatar link component', () => { - expect(findAvatarLink().attributes()).toMatchObject({ - title: mockData.pipeline.user.name, - href: mockData.pipeline.user.path, - }); - }); - - it('should add tooltip to avatar link', () => { - const tooltip = getBinding(findAvatarLink().element, 'gl-tooltip'); - - expect(tooltip).toBeDefined(); - }); - - it('should set correct props on avatar component', () => { - expect(findAvatar().attributes().src).toBe(mockData.pipeline.user.avatar_url); - }); - }); - - describe('when API was a triggerer', () => { - beforeEach(() => { - createComponent({ pipeline: {} }); - }); - - it('should render label only', () => { - expect(findAvatarLink().exists()).toBe(false); - expect(findTriggerer().exists()).toBe(true); - }); - }); -}); diff --git a/spec/frontend/pipelines/pipeline_url_spec.js b/spec/frontend/pipelines/pipeline_url_spec.js deleted file mode 100644 index 797ec676ccc..00000000000 --- a/spec/frontend/pipelines/pipeline_url_spec.js +++ /dev/null @@ -1,184 +0,0 @@ -import { merge } from 'lodash'; -import { mockTracking, unmockTracking } from 'helpers/tracking_helper'; -import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; -import PipelineUrlComponent from '~/pipelines/components/pipelines_list/pipeline_url.vue'; -import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue'; -import { TRACKING_CATEGORIES } from '~/pipelines/constants'; -import { mockPipeline, mockPipelineBranch, mockPipelineTag } from './mock_data'; - -const projectPath = 'test/test'; - -describe('Pipeline Url Component', () => { - let wrapper; - let trackingSpy; - - const findTableCell = () => wrapper.findByTestId('pipeline-url-table-cell'); - const findPipelineUrlLink = () => wrapper.findByTestId('pipeline-url-link'); - const findRefName = () => wrapper.findByTestId('merge-request-ref'); - const findCommitShortSha = () => wrapper.findByTestId('commit-short-sha'); - const findCommitIcon = () => wrapper.findByTestId('commit-icon'); - const findCommitIconType = () => wrapper.findByTestId('commit-icon-type'); - const findCommitRefName = () => wrapper.findByTestId('commit-ref-name'); - - const findCommitTitleContainer = () => wrapper.findByTestId('commit-title-container'); - const findPipelineNameContainer = () => wrapper.findByTestId('pipeline-name-container'); - const findCommitTitle = (commitWrapper) => commitWrapper.find('[data-testid="commit-title"]'); - - const defaultProps = { ...mockPipeline(projectPath), refClass: 'gl-text-black' }; - - const createComponent = (props) => { - wrapper = shallowMountExtended(PipelineUrlComponent, { - propsData: { ...defaultProps, ...props }, - provide: { - targetProjectFullPath: projectPath, - }, - }); - }; - - it('should render pipeline url table cell', () => { - createComponent(); - - expect(findTableCell().exists()).toBe(true); - }); - - it('should render a link the provided path and id', () => { - createComponent(); - - expect(findPipelineUrlLink().attributes('href')).toBe('foo'); - - expect(findPipelineUrlLink().text()).toBe('#1'); - }); - - it('should render the pipeline name instead of commit title', () => { - createComponent(merge(mockPipeline(projectPath), { pipeline: { name: 'Build pipeline' } })); - - expect(findCommitTitleContainer().exists()).toBe(false); - expect(findPipelineNameContainer().exists()).toBe(true); - expect(findRefName().exists()).toBe(true); - expect(findCommitShortSha().exists()).toBe(true); - }); - - it('should render the commit title when pipeline has no name', () => { - createComponent(); - - const commitWrapper = findCommitTitleContainer(); - - expect(findCommitTitle(commitWrapper).exists()).toBe(true); - expect(findRefName().exists()).toBe(true); - expect(findCommitShortSha().exists()).toBe(true); - expect(findPipelineNameContainer().exists()).toBe(false); - }); - - it('should pass the refClass prop to merge request link', () => { - createComponent(); - - expect(findRefName().classes()).toContain(defaultProps.refClass); - }); - - it('should pass the refClass prop to the commit ref name link', () => { - createComponent(mockPipelineBranch()); - - expect(findCommitRefName().classes()).toContain(defaultProps.refClass); - }); - - describe('commit user avatar', () => { - it('renders when commit author exists', () => { - const pipelineBranch = mockPipelineBranch(); - const { avatar_url: imgSrc, name, path } = pipelineBranch.pipeline.commit.author; - createComponent(pipelineBranch); - - const component = wrapper.findComponent(UserAvatarLink); - expect(component.exists()).toBe(true); - expect(component.props()).toMatchObject({ - imgSize: 16, - imgSrc, - imgAlt: name, - linkHref: path, - tooltipText: name, - }); - }); - - it('does not render when commit author does not exist', () => { - createComponent(); - - expect(wrapper.findComponent(UserAvatarLink).exists()).toBe(false); - }); - }); - - it('should render commit icon tooltip', () => { - createComponent(); - - expect(findCommitIcon().attributes('title')).toBe('Commit'); - }); - - it.each` - pipeline | expectedTitle - ${mockPipelineTag()} | ${'Tag'} - ${mockPipelineBranch()} | ${'Branch'} - ${mockPipeline()} | ${'Merge Request'} - `('should render tooltip $expectedTitle for commit icon type', ({ pipeline, expectedTitle }) => { - createComponent(pipeline); - - expect(findCommitIconType().attributes('title')).toBe(expectedTitle); - }); - - describe('tracking', () => { - beforeEach(() => { - trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn); - }); - - afterEach(() => { - unmockTracking(); - }); - - it('tracks pipeline id click', () => { - createComponent(); - - findPipelineUrlLink().vm.$emit('click'); - - expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_pipeline_id', { - label: TRACKING_CATEGORIES.table, - }); - }); - - it('tracks merge request ref click', () => { - createComponent(); - - findRefName().vm.$emit('click'); - - expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_mr_ref', { - label: TRACKING_CATEGORIES.table, - }); - }); - - it('tracks commit ref name click', () => { - createComponent(mockPipelineBranch()); - - findCommitRefName().vm.$emit('click'); - - expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_commit_name', { - label: TRACKING_CATEGORIES.table, - }); - }); - - it('tracks commit title click', () => { - createComponent(merge(mockPipelineBranch(), { pipeline: { name: null } })); - - findCommitTitle(findCommitTitleContainer()).vm.$emit('click'); - - expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_commit_title', { - label: TRACKING_CATEGORIES.table, - }); - }); - - it('tracks commit short sha click', () => { - createComponent(mockPipelineBranch()); - - findCommitShortSha().vm.$emit('click'); - - expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_commit_sha', { - label: TRACKING_CATEGORIES.table, - }); - }); - }); -}); diff --git a/spec/frontend/pipelines/pipelines_artifacts_spec.js b/spec/frontend/pipelines/pipelines_artifacts_spec.js deleted file mode 100644 index 1abc2887682..00000000000 --- a/spec/frontend/pipelines/pipelines_artifacts_spec.js +++ /dev/null @@ -1,64 +0,0 @@ -import { - GlDisclosureDropdown, - GlDisclosureDropdownItem, - GlDisclosureDropdownGroup, - GlSprintf, -} from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; -import PipelineArtifacts from '~/pipelines/components/pipelines_list/pipelines_artifacts.vue'; - -describe('Pipelines Artifacts dropdown', () => { - let wrapper; - - const artifacts = [ - { - name: 'job my-artifact', - path: '/download/path', - }, - { - name: 'job-2 my-artifact-2', - path: '/download/path-two', - }, - ]; - const pipelineId = 108; - - const createComponent = ({ mockArtifacts = artifacts } = {}) => { - wrapper = shallowMount(PipelineArtifacts, { - propsData: { - pipelineId, - artifacts: mockArtifacts, - }, - stubs: { - GlSprintf, - GlDisclosureDropdown, - GlDisclosureDropdownItem, - GlDisclosureDropdownGroup, - }, - }); - }; - - const findGlDropdown = () => wrapper.findComponent(GlDisclosureDropdown); - const findFirstGlDropdownItem = () => wrapper.findComponent(GlDisclosureDropdownItem); - - it('should render a dropdown with all the provided artifacts', () => { - createComponent(); - - const [{ items }] = findGlDropdown().props('items'); - expect(items).toHaveLength(artifacts.length); - }); - - it('should render a link with the provided path', () => { - createComponent(); - - expect(findFirstGlDropdownItem().props('item').href).toBe(artifacts[0].path); - expect(findFirstGlDropdownItem().text()).toBe(artifacts[0].name); - }); - - describe('with no artifacts', () => { - it('should not render the dropdown', () => { - createComponent({ mockArtifacts: [] }); - - expect(findGlDropdown().exists()).toBe(false); - }); - }); -}); diff --git a/spec/frontend/pipelines/pipelines_manual_actions_spec.js b/spec/frontend/pipelines/pipelines_manual_actions_spec.js deleted file mode 100644 index 82cab88c9eb..00000000000 --- a/spec/frontend/pipelines/pipelines_manual_actions_spec.js +++ /dev/null @@ -1,216 +0,0 @@ -import { GlDropdown, GlDropdownItem, GlLoadingIcon } from '@gitlab/ui'; -import MockAdapter from 'axios-mock-adapter'; -import Vue, { nextTick } from 'vue'; -import VueApollo from 'vue-apollo'; -import mockPipelineActionsQueryResponse from 'test_fixtures/graphql/pipelines/get_pipeline_actions.query.graphql.json'; -import { mockTracking, unmockTracking } from 'helpers/tracking_helper'; -import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; -import createMockApollo from 'helpers/mock_apollo_helper'; -import waitForPromises from 'helpers/wait_for_promises'; -import { createAlert } from '~/alert'; -import axios from '~/lib/utils/axios_utils'; -import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status'; -import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal'; -import PipelinesManualActions from '~/pipelines/components/pipelines_list/pipelines_manual_actions.vue'; -import getPipelineActionsQuery from '~/pipelines/graphql/queries/get_pipeline_actions.query.graphql'; -import { TRACKING_CATEGORIES } from '~/pipelines/constants'; -import GlCountdown from '~/vue_shared/components/gl_countdown.vue'; - -Vue.use(VueApollo); - -jest.mock('~/alert'); -jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal'); - -describe('Pipeline manual actions', () => { - let wrapper; - let mock; - - const queryHandler = jest.fn().mockResolvedValue(mockPipelineActionsQueryResponse); - const { - data: { - project: { - pipeline: { - jobs: { nodes }, - }, - }, - }, - } = mockPipelineActionsQueryResponse; - - const mockPath = nodes[2].playPath; - - const createComponent = (limit = 50) => { - wrapper = shallowMountExtended(PipelinesManualActions, { - provide: { - fullPath: 'root/ci-project', - manualActionsLimit: limit, - }, - propsData: { - iid: 100, - }, - stubs: { - GlDropdown, - }, - apolloProvider: createMockApollo([[getPipelineActionsQuery, queryHandler]]), - }); - }; - - const findDropdown = () => wrapper.findComponent(GlDropdown); - const findAllDropdownItems = () => wrapper.findAllComponents(GlDropdownItem); - const findAllCountdowns = () => wrapper.findAllComponents(GlCountdown); - const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); - const findLimitMessage = () => wrapper.findByTestId('limit-reached-msg'); - - it('skips calling query on mount', () => { - createComponent(); - - expect(queryHandler).not.toHaveBeenCalled(); - }); - - describe('loading', () => { - beforeEach(() => { - createComponent(); - - findDropdown().vm.$emit('shown'); - }); - - it('display loading state while actions are being fetched', () => { - expect(findAllDropdownItems().at(0).text()).toBe('Loading...'); - expect(findLoadingIcon().exists()).toBe(true); - expect(findAllDropdownItems()).toHaveLength(1); - }); - }); - - describe('loaded', () => { - beforeEach(async () => { - mock = new MockAdapter(axios); - - createComponent(); - - findDropdown().vm.$emit('shown'); - - await waitForPromises(); - }); - - afterEach(() => { - mock.restore(); - confirmAction.mockReset(); - }); - - it('displays dropdown with the provided actions', () => { - expect(findAllDropdownItems()).toHaveLength(3); - }); - - it("displays a disabled action when it's not playable", () => { - expect(findAllDropdownItems().at(0).attributes('disabled')).toBeDefined(); - }); - - describe('on action click', () => { - it('makes a request and toggles the loading state', async () => { - mock.onPost(mockPath).reply(HTTP_STATUS_OK); - - findAllDropdownItems().at(1).vm.$emit('click'); - - await nextTick(); - - expect(findDropdown().props('loading')).toBe(true); - - await waitForPromises(); - - expect(findDropdown().props('loading')).toBe(false); - }); - - it('makes a failed request and toggles the loading state', async () => { - mock.onPost(mockPath).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR); - - findAllDropdownItems().at(1).vm.$emit('click'); - - await nextTick(); - - expect(findDropdown().props('loading')).toBe(true); - - await waitForPromises(); - - expect(findDropdown().props('loading')).toBe(false); - expect(createAlert).toHaveBeenCalledTimes(1); - }); - }); - - describe('tracking', () => { - afterEach(() => { - unmockTracking(); - }); - - it('tracks manual actions click', () => { - const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn); - - findDropdown().vm.$emit('shown'); - - expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_manual_actions', { - label: TRACKING_CATEGORIES.table, - }); - }); - }); - - describe('scheduled jobs', () => { - beforeEach(() => { - jest - .spyOn(Date, 'now') - .mockImplementation(() => new Date('2063-04-04T00:42:00Z').getTime()); - }); - - it('makes post request after confirming', async () => { - mock.onPost(mockPath).reply(HTTP_STATUS_OK); - - confirmAction.mockResolvedValueOnce(true); - - findAllDropdownItems().at(2).vm.$emit('click'); - - expect(confirmAction).toHaveBeenCalled(); - - await waitForPromises(); - - expect(mock.history.post).toHaveLength(1); - }); - - it('does not make post request if confirmation is cancelled', async () => { - mock.onPost(mockPath).reply(HTTP_STATUS_OK); - - confirmAction.mockResolvedValueOnce(false); - - findAllDropdownItems().at(2).vm.$emit('click'); - - expect(confirmAction).toHaveBeenCalled(); - - await waitForPromises(); - - expect(mock.history.post).toHaveLength(0); - }); - - it('displays the remaining time in the dropdown', () => { - expect(findAllCountdowns().at(0).props('endDateString')).toBe(nodes[2].scheduledAt); - }); - }); - }); - - describe('limit message', () => { - it('limit message does not show', async () => { - createComponent(); - - findDropdown().vm.$emit('shown'); - - await waitForPromises(); - - expect(findLimitMessage().exists()).toBe(false); - }); - - it('limit message does show', async () => { - createComponent(3); - - findDropdown().vm.$emit('shown'); - - await waitForPromises(); - - expect(findLimitMessage().exists()).toBe(true); - }); - }); -}); diff --git a/spec/frontend/pipelines/pipelines_spec.js b/spec/frontend/pipelines/pipelines_spec.js deleted file mode 100644 index cc85d6d99e0..00000000000 --- a/spec/frontend/pipelines/pipelines_spec.js +++ /dev/null @@ -1,850 +0,0 @@ -import '~/commons'; -import { - GlButton, - GlEmptyState, - GlFilteredSearch, - GlLoadingIcon, - GlPagination, - GlCollapsibleListbox, -} from '@gitlab/ui'; -import * as Sentry from '@sentry/browser'; -import { mount } from '@vue/test-utils'; -import MockAdapter from 'axios-mock-adapter'; -import { chunk } from 'lodash'; -import { nextTick } from 'vue'; -import mockPipelinesResponse from 'test_fixtures/pipelines/pipelines.json'; -import setWindowLocation from 'helpers/set_window_location_helper'; -import { TEST_HOST } from 'helpers/test_constants'; -import { mockTracking } from 'helpers/tracking_helper'; -import { extendedWrapper } from 'helpers/vue_test_utils_helper'; -import waitForPromises from 'helpers/wait_for_promises'; -import createMockApollo from 'helpers/mock_apollo_helper'; -import Api from '~/api'; -import { createAlert, VARIANT_WARNING } from '~/alert'; -import setSortPreferenceMutation from '~/issues/list/queries/set_sort_preference.mutation.graphql'; -import axios from '~/lib/utils/axios_utils'; -import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status'; -import NavigationControls from '~/pipelines/components/pipelines_list/nav_controls.vue'; -import PipelinesComponent from '~/pipelines/components/pipelines_list/pipelines.vue'; -import PipelinesCiTemplates from '~/pipelines/components/pipelines_list/empty_state/pipelines_ci_templates.vue'; -import PipelinesTableComponent from '~/pipelines/components/pipelines_list/pipelines_table.vue'; -import { RAW_TEXT_WARNING, TRACKING_CATEGORIES } from '~/pipelines/constants'; -import Store from '~/pipelines/stores/pipelines_store'; -import NavigationTabs from '~/vue_shared/components/navigation_tabs.vue'; -import TablePagination from '~/vue_shared/components/pagination/table_pagination.vue'; -import { - setIdTypePreferenceMutationResponse, - setIdTypePreferenceMutationResponseWithErrors, -} from 'jest/issues/list/mock_data'; - -import { stageReply, users, mockSearch, branches } from './mock_data'; - -jest.mock('@sentry/browser'); -jest.mock('~/alert'); - -const mockProjectPath = 'twitter/flight'; -const mockProjectId = '21'; -const mockDefaultBranchName = 'main'; -const mockPipelinesEndpoint = `/${mockProjectPath}/pipelines.json`; -const mockPipelinesIds = mockPipelinesResponse.pipelines.map(({ id }) => id); -const mockPipelineWithStages = mockPipelinesResponse.pipelines.find( - (p) => p.details.stages && p.details.stages.length, -); - -describe('Pipelines', () => { - let wrapper; - let mockApollo; - let mock; - let trackingSpy; - - const paths = { - emptyStateSvgPath: '/assets/illustrations/empty-state/empty-pipeline-md.svg', - errorStateSvgPath: '/assets/illustrations/pipelines_failed.svg', - noPipelinesSvgPath: '/assets/illustrations/empty-state/empty-pipeline-md.svg', - ciLintPath: '/ci/lint', - resetCachePath: `${mockProjectPath}/settings/ci_cd/reset_cache`, - newPipelinePath: `${mockProjectPath}/pipelines/new`, - - ciRunnerSettingsPath: `${mockProjectPath}/-/settings/ci_cd#js-runners-settings`, - }; - - const noPermissions = { - emptyStateSvgPath: '/assets/illustrations/empty-state/empty-pipeline-md.svg', - errorStateSvgPath: '/assets/illustrations/pipelines_failed.svg', - noPipelinesSvgPath: '/assets/illustrations/empty-state/empty-pipeline-md.svg', - }; - - const defaultProps = { - hasGitlabCi: true, - canCreatePipeline: true, - ...paths, - }; - - const findFilteredSearch = () => wrapper.findComponent(GlFilteredSearch); - const findEmptyState = () => wrapper.findComponent(GlEmptyState); - const findNavigationTabs = () => wrapper.findComponent(NavigationTabs); - const findNavigationControls = () => wrapper.findComponent(NavigationControls); - const findPipelinesTable = () => wrapper.findComponent(PipelinesTableComponent); - const findTablePagination = () => wrapper.findComponent(TablePagination); - const findPipelineKeyCollapsibleBoxVue = () => wrapper.findComponent(GlCollapsibleListbox); - - const findTab = (tab) => wrapper.findByTestId(`pipelines-tab-${tab}`); - const findPipelineKeyCollapsibleBox = () => wrapper.findByTestId('pipeline-key-collapsible-box'); - const findRunPipelineButton = () => wrapper.findByTestId('run-pipeline-button'); - const findCiLintButton = () => wrapper.findByTestId('ci-lint-button'); - const findCleanCacheButton = () => wrapper.findByTestId('clear-cache-button'); - const findStagesDropdownToggle = () => - wrapper.find('[data-testid="mini-pipeline-graph-dropdown"] .dropdown-toggle'); - const findPipelineUrlLinks = () => wrapper.findAll('[data-testid="pipeline-url-link"]'); - - const createComponent = (props = defaultProps) => { - const { mutationMock, ...restProps } = props; - mockApollo = createMockApollo([[setSortPreferenceMutation, mutationMock]]); - - wrapper = extendedWrapper( - mount(PipelinesComponent, { - provide: { - pipelineEditorPath: '', - suggestedCiTemplates: [], - ciRunnerSettingsPath: paths.ciRunnerSettingsPath, - anyRunnersAvailable: true, - }, - propsData: { - store: new Store(), - projectId: mockProjectId, - defaultBranchName: mockDefaultBranchName, - endpoint: mockPipelinesEndpoint, - params: {}, - ...restProps, - }, - apolloProvider: mockApollo, - }), - ); - }; - - beforeEach(() => { - setWindowLocation(TEST_HOST); - }); - - beforeEach(() => { - mock = new MockAdapter(axios); - - jest.spyOn(window.history, 'pushState'); - jest.spyOn(Api, 'projectUsers').mockResolvedValue(users); - jest.spyOn(Api, 'branches').mockResolvedValue({ data: branches }); - }); - - afterEach(() => { - mock.reset(); - mockApollo = null; - window.history.pushState.mockReset(); - }); - - describe('when pipelines are not yet loaded', () => { - beforeEach(async () => { - createComponent(); - await nextTick(); - }); - - it('shows loading state when the app is loading', () => { - expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true); - }); - - it('does not display tabs when the first request has not yet been made', () => { - expect(findNavigationTabs().exists()).toBe(false); - }); - - it('does not display buttons', () => { - expect(findNavigationControls().exists()).toBe(false); - }); - }); - - describe('when there are pipelines in the project', () => { - beforeEach(() => { - mock - .onGet(mockPipelinesEndpoint, { params: { scope: 'all', page: '1' } }) - .reply(HTTP_STATUS_OK, mockPipelinesResponse); - }); - - describe('when user has no permissions', () => { - beforeEach(async () => { - createComponent({ hasGitlabCi: true, canCreatePipeline: false, ...noPermissions }); - await waitForPromises(); - }); - - it('renders "All" tab with count different from "0"', () => { - expect(findTab('all').text()).toMatchInterpolatedText('All 3'); - }); - - it('does not render buttons', () => { - expect(findNavigationControls().exists()).toBe(false); - - expect(findRunPipelineButton().exists()).toBe(false); - expect(findCiLintButton().exists()).toBe(false); - expect(findCleanCacheButton().exists()).toBe(false); - }); - - it('renders pipelines in a table', () => { - expect(findPipelinesTable().exists()).toBe(true); - - expect(findPipelineUrlLinks()).toHaveLength(mockPipelinesIds.length); - expect(findPipelineUrlLinks().at(0).text()).toBe(`#${mockPipelinesIds[0]}`); - expect(findPipelineUrlLinks().at(1).text()).toBe(`#${mockPipelinesIds[1]}`); - expect(findPipelineUrlLinks().at(2).text()).toBe(`#${mockPipelinesIds[2]}`); - }); - }); - - describe('when user has permissions', () => { - beforeEach(async () => { - createComponent(); - await waitForPromises(); - }); - - it('should set up navigation tabs', () => { - expect(findNavigationTabs().props('tabs')).toEqual([ - { name: 'All', scope: 'all', count: '3', isActive: true }, - { name: 'Finished', scope: 'finished', count: undefined, isActive: false }, - { name: 'Branches', scope: 'branches', isActive: false }, - { name: 'Tags', scope: 'tags', isActive: false }, - ]); - }); - - it('renders "All" tab with count different from "0"', () => { - expect(findTab('all').text()).toMatchInterpolatedText('All 3'); - }); - - it('should render other navigation tabs', () => { - expect(findTab('finished').text()).toBe('Finished'); - expect(findTab('branches').text()).toBe('Branches'); - expect(findTab('tags').text()).toBe('Tags'); - }); - - it('shows navigation controls', () => { - expect(findNavigationControls().exists()).toBe(true); - }); - - it('renders Run pipeline link', () => { - expect(findRunPipelineButton().attributes('href')).toBe(paths.newPipelinePath); - }); - - it('renders CI lint link', () => { - expect(findCiLintButton().attributes('href')).toBe(paths.ciLintPath); - }); - - it('renders Clear runner cache button', () => { - expect(findCleanCacheButton().text()).toBe('Clear runner caches'); - }); - - it('renders pipelines in a table', () => { - expect(findPipelinesTable().exists()).toBe(true); - - expect(findPipelineUrlLinks()).toHaveLength(mockPipelinesIds.length); - expect(findPipelineUrlLinks().at(0).text()).toBe(`#${mockPipelinesIds[0]}`); - expect(findPipelineUrlLinks().at(1).text()).toBe(`#${mockPipelinesIds[1]}`); - expect(findPipelineUrlLinks().at(2).text()).toBe(`#${mockPipelinesIds[2]}`); - }); - - describe('when user goes to a tab', () => { - const goToTab = (tab) => { - findNavigationTabs().vm.$emit('onChangeTab', tab); - }; - - describe('when the scope in the tab has pipelines', () => { - const mockFinishedPipeline = mockPipelinesResponse.pipelines[0]; - - beforeEach(async () => { - mock - .onGet(mockPipelinesEndpoint, { params: { scope: 'finished', page: '1' } }) - .reply(HTTP_STATUS_OK, { - pipelines: [mockFinishedPipeline], - count: mockPipelinesResponse.count, - }); - - trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn); - - goToTab('finished'); - - await waitForPromises(); - }); - - it('should filter pipelines', () => { - expect(findPipelinesTable().exists()).toBe(true); - - expect(findPipelineUrlLinks()).toHaveLength(1); - expect(findPipelineUrlLinks().at(0).text()).toBe(`#${mockFinishedPipeline.id}`); - }); - - it('should update browser bar', () => { - expect(window.history.pushState).toHaveBeenCalledTimes(1); - expect(window.history.pushState).toHaveBeenCalledWith( - expect.anything(), - expect.anything(), - `${window.location.pathname}?scope=finished&page=1`, - ); - }); - - it.each(['all', 'finished', 'branches', 'tags'])('tracks %p tab click', async (scope) => { - goToTab(scope); - - await waitForPromises(); - - expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_filter_tabs', { - label: TRACKING_CATEGORIES.tabs, - property: scope, - }); - }); - }); - - describe('when the scope in the tab is empty', () => { - beforeEach(async () => { - mock - .onGet(mockPipelinesEndpoint, { params: { scope: 'branches', page: '1' } }) - .reply(HTTP_STATUS_OK, { - pipelines: [], - count: mockPipelinesResponse.count, - }); - - goToTab('branches'); - - await waitForPromises(); - }); - - it('should filter pipelines', () => { - expect(findEmptyState().text()).toBe('There are currently no pipelines.'); - }); - - it('should update browser bar', () => { - expect(window.history.pushState).toHaveBeenCalledTimes(1); - expect(window.history.pushState).toHaveBeenCalledWith( - expect.anything(), - expect.anything(), - `${window.location.pathname}?scope=branches&page=1`, - ); - }); - }); - }); - - describe('when user triggers a filtered search', () => { - const mockFilteredPipeline = mockPipelinesResponse.pipelines[1]; - - let expectedParams; - - beforeEach(async () => { - expectedParams = { - page: '1', - scope: 'all', - username: 'root', - ref: 'main', - status: 'pending', - }; - - mock - .onGet(mockPipelinesEndpoint, { - params: expectedParams, - }) - .replyOnce(HTTP_STATUS_OK, { - pipelines: [mockFilteredPipeline], - count: mockPipelinesResponse.count, - }); - - findFilteredSearch().vm.$emit('submit', mockSearch); - - await waitForPromises(); - }); - - it('requests data with query params on filter submit', () => { - expect(mock.history.get[1].params).toEqual(expectedParams); - }); - - it('renders filtered pipelines', () => { - expect(findPipelineUrlLinks()).toHaveLength(1); - expect(findPipelineUrlLinks().at(0).text()).toBe(`#${mockFilteredPipeline.id}`); - }); - - it('should update browser bar', () => { - expect(window.history.pushState).toHaveBeenCalledTimes(1); - expect(window.history.pushState).toHaveBeenCalledWith( - expect.anything(), - expect.anything(), - `${window.location.pathname}?page=1&scope=all&username=root&ref=main&status=pending`, - ); - }); - }); - - describe('when user changes Show Pipeline ID to Show Pipeline IID', () => { - const mockFilteredPipeline = mockPipelinesResponse.pipelines[0]; - - beforeEach(() => { - gon.current_user_id = 1; - }); - - it('should change the text to Show Pipeline IID', async () => { - expect(findPipelineKeyCollapsibleBox().exists()).toBe(true); - expect(findPipelineUrlLinks().at(0).text()).toBe(`#${mockFilteredPipeline.id}`); - findPipelineKeyCollapsibleBoxVue().vm.$emit('select', 'iid'); - - await waitForPromises(); - - expect(findPipelineUrlLinks().at(0).text()).toBe(`#${mockFilteredPipeline.iid}`); - }); - - it('calls mutation to save idType preference', () => { - const mutationMock = jest.fn().mockResolvedValue(setIdTypePreferenceMutationResponse); - createComponent({ ...defaultProps, mutationMock }); - - findPipelineKeyCollapsibleBoxVue().vm.$emit('select', 'iid'); - - expect(mutationMock).toHaveBeenCalledWith({ input: { visibilityPipelineIdType: 'IID' } }); - }); - - it('captures error when mutation response has errors', async () => { - const mutationMock = jest - .fn() - .mockResolvedValue(setIdTypePreferenceMutationResponseWithErrors); - createComponent({ ...defaultProps, mutationMock }); - - findPipelineKeyCollapsibleBoxVue().vm.$emit('select', 'iid'); - await waitForPromises(); - - expect(Sentry.captureException).toHaveBeenCalledWith(new Error('oh no!')); - }); - }); - - describe('when user triggers a filtered search with raw text', () => { - beforeEach(async () => { - findFilteredSearch().vm.$emit('submit', ['rawText']); - - await waitForPromises(); - }); - - it('requests data with query params on filter submit', () => { - expect(mock.history.get[1].params).toEqual({ page: '1', scope: 'all' }); - }); - - it('displays a warning message if raw text search is used', () => { - expect(createAlert).toHaveBeenCalledTimes(1); - expect(createAlert).toHaveBeenCalledWith({ - message: RAW_TEXT_WARNING, - variant: VARIANT_WARNING, - }); - }); - - it('should update browser bar', () => { - expect(window.history.pushState).toHaveBeenCalledTimes(1); - expect(window.history.pushState).toHaveBeenCalledWith( - expect.anything(), - expect.anything(), - `${window.location.pathname}?page=1&scope=all`, - ); - }); - }); - }); - }); - - describe('when there are multiple pages of pipelines', () => { - const mockPageSize = 2; - const mockPageHeaders = ({ page = 1 } = {}) => { - return { - 'X-PER-PAGE': `${mockPageSize}`, - 'X-PREV-PAGE': `${page - 1}`, - 'X-PAGE': `${page}`, - 'X-NEXT-PAGE': `${page + 1}`, - }; - }; - const [firstPage, secondPage] = chunk(mockPipelinesResponse.pipelines, mockPageSize); - - const goToPage = (page) => { - findTablePagination().findComponent(GlPagination).vm.$emit('input', page); - }; - - beforeEach(async () => { - mock.onGet(mockPipelinesEndpoint, { params: { scope: 'all', page: '1' } }).reply( - HTTP_STATUS_OK, - { - pipelines: firstPage, - count: mockPipelinesResponse.count, - }, - mockPageHeaders({ page: 1 }), - ); - mock.onGet(mockPipelinesEndpoint, { params: { scope: 'all', page: '2' } }).reply( - HTTP_STATUS_OK, - { - pipelines: secondPage, - count: mockPipelinesResponse.count, - }, - mockPageHeaders({ page: 2 }), - ); - - createComponent(); - - await waitForPromises(); - }); - - it('shows the first page of pipelines', () => { - expect(findPipelineUrlLinks()).toHaveLength(firstPage.length); - expect(findPipelineUrlLinks().at(0).text()).toBe(`#${firstPage[0].id}`); - expect(findPipelineUrlLinks().at(1).text()).toBe(`#${firstPage[1].id}`); - }); - - it('should not update browser bar', () => { - expect(window.history.pushState).not.toHaveBeenCalled(); - }); - - describe('when user goes to next page', () => { - beforeEach(async () => { - goToPage(2); - await waitForPromises(); - }); - - it('should update page and keep scope the same scope', () => { - expect(findPipelineUrlLinks()).toHaveLength(secondPage.length); - expect(findPipelineUrlLinks().at(0).text()).toBe(`#${secondPage[0].id}`); - }); - - it('should update browser bar', () => { - expect(window.history.pushState).toHaveBeenCalledTimes(1); - expect(window.history.pushState).toHaveBeenCalledWith( - expect.anything(), - expect.anything(), - `${window.location.pathname}?page=2&scope=all`, - ); - }); - - it('should reset page to 1 when filtering pipelines', () => { - expect(window.history.pushState).toHaveBeenCalledTimes(1); - expect(window.history.pushState).toHaveBeenCalledWith( - expect.anything(), - expect.anything(), - `${window.location.pathname}?page=2&scope=all`, - ); - - findFilteredSearch().vm.$emit('submit', [ - { type: 'status', value: { data: 'success', operator: '=' } }, - ]); - - expect(window.history.pushState).toHaveBeenCalledTimes(2); - expect(window.history.pushState).toHaveBeenCalledWith( - expect.anything(), - expect.anything(), - `${window.location.pathname}?page=1&scope=all&status=success`, - ); - }); - }); - }); - - describe('when pipelines can be polled', () => { - beforeEach(() => { - const emptyResponse = { - pipelines: [], - count: { all: '0' }, - }; - - // Mock no pipelines in the first attempt - mock - .onGet(mockPipelinesEndpoint, { params: { scope: 'all', page: '1' } }) - .replyOnce(HTTP_STATUS_OK, emptyResponse, { - 'POLL-INTERVAL': 100, - }); - // Mock pipelines in the next attempt - mock - .onGet(mockPipelinesEndpoint, { params: { scope: 'all', page: '1' } }) - .reply(HTTP_STATUS_OK, mockPipelinesResponse, { - 'POLL-INTERVAL': 100, - }); - }); - - describe('data is loaded for the first time', () => { - beforeEach(async () => { - createComponent(); - await waitForPromises(); - }); - - it('shows tabs', () => { - expect(findNavigationTabs().exists()).toBe(true); - }); - - it('should update page and keep scope the same scope', () => { - expect(findPipelineUrlLinks()).toHaveLength(0); - }); - - describe('data is loaded for a second time', () => { - beforeEach(async () => { - jest.runOnlyPendingTimers(); - await waitForPromises(); - }); - - it('shows tabs', () => { - expect(findNavigationTabs().exists()).toBe(true); - }); - - it('is loading after a time', () => { - expect(findPipelineUrlLinks()).toHaveLength(mockPipelinesIds.length); - expect(findPipelineUrlLinks().at(0).text()).toBe(`#${mockPipelinesIds[0]}`); - expect(findPipelineUrlLinks().at(1).text()).toBe(`#${mockPipelinesIds[1]}`); - expect(findPipelineUrlLinks().at(2).text()).toBe(`#${mockPipelinesIds[2]}`); - }); - }); - }); - }); - - describe('when no pipelines exist', () => { - beforeEach(() => { - mock - .onGet(mockPipelinesEndpoint, { params: { scope: 'all', page: '1' } }) - .reply(HTTP_STATUS_OK, { - pipelines: [], - count: { all: '0' }, - }); - }); - - describe('when CI is enabled and user has permissions', () => { - beforeEach(async () => { - createComponent(); - await waitForPromises(); - }); - - it('renders tab with count of "0"', () => { - expect(findNavigationTabs().exists()).toBe(true); - expect(findTab('all').text()).toMatchInterpolatedText('All 0'); - }); - - it('renders Run pipeline link', () => { - expect(findRunPipelineButton().attributes('href')).toBe(paths.newPipelinePath); - }); - - it('renders CI lint link', () => { - expect(findCiLintButton().attributes('href')).toBe(paths.ciLintPath); - }); - - it('renders Clear runner cache button', () => { - expect(findCleanCacheButton().text()).toBe('Clear runner caches'); - }); - - it('renders empty state', () => { - expect(findEmptyState().text()).toBe('There are currently no pipelines.'); - }); - - it('renders filtered search', () => { - expect(findFilteredSearch().exists()).toBe(true); - }); - - it('renders the pipeline key collapsible box', () => { - expect(findPipelineKeyCollapsibleBox().exists()).toBe(true); - }); - - it('renders tab empty state finished scope', async () => { - mock - .onGet(mockPipelinesEndpoint, { params: { scope: 'finished', page: '1' } }) - .reply(HTTP_STATUS_OK, { - pipelines: [], - count: { all: '0' }, - }); - - findNavigationTabs().vm.$emit('onChangeTab', 'finished'); - - await waitForPromises(); - - expect(findEmptyState().text()).toBe('There are currently no finished pipelines.'); - }); - }); - - describe('when CI is not enabled and user has permissions', () => { - beforeEach(async () => { - createComponent({ hasGitlabCi: false, canCreatePipeline: true, ...paths }); - await waitForPromises(); - }); - - it('renders the CI/CD templates', () => { - expect(wrapper.findComponent(PipelinesCiTemplates).exists()).toBe(true); - }); - - it('does not render filtered search', () => { - expect(findFilteredSearch().exists()).toBe(false); - }); - - it('does not render the pipeline key dropdown', () => { - expect(findPipelineKeyCollapsibleBox().exists()).toBe(false); - }); - - it('does not render tabs nor buttons', () => { - expect(findNavigationTabs().exists()).toBe(false); - expect(findTab('all').exists()).toBe(false); - expect(findRunPipelineButton().exists()).toBe(false); - expect(findCiLintButton().exists()).toBe(false); - expect(findCleanCacheButton().exists()).toBe(false); - }); - }); - - describe('when CI is not enabled and user has no permissions', () => { - beforeEach(async () => { - createComponent({ hasGitlabCi: false, canCreatePipeline: false, ...noPermissions }); - await waitForPromises(); - }); - - it('renders empty state without button to set CI', () => { - expect(findEmptyState().text()).toBe( - 'This project is not currently set up to run pipelines.', - ); - - expect(findEmptyState().findComponent(GlButton).exists()).toBe(false); - }); - - it('does not render tabs or buttons', () => { - expect(findTab('all').exists()).toBe(false); - expect(findRunPipelineButton().exists()).toBe(false); - expect(findCiLintButton().exists()).toBe(false); - expect(findCleanCacheButton().exists()).toBe(false); - }); - }); - - describe('when CI is enabled and user has no permissions', () => { - beforeEach(() => { - createComponent({ hasGitlabCi: true, canCreatePipeline: false, ...noPermissions }); - - return waitForPromises(); - }); - - it('renders tab with count of "0"', () => { - expect(findTab('all').text()).toMatchInterpolatedText('All 0'); - }); - - it('does not render buttons', () => { - expect(findRunPipelineButton().exists()).toBe(false); - expect(findCiLintButton().exists()).toBe(false); - expect(findCleanCacheButton().exists()).toBe(false); - }); - - it('renders empty state', () => { - expect(findEmptyState().text()).toBe('There are currently no pipelines.'); - }); - }); - }); - - describe('when a pipeline with stages exists', () => { - describe('updates results when a staged is clicked', () => { - let stopMock; - let restartMock; - let cancelMock; - - beforeEach(() => { - mock.onGet(mockPipelinesEndpoint, { scope: 'all', page: '1' }).reply( - HTTP_STATUS_OK, - { - pipelines: [mockPipelineWithStages], - count: { all: '1' }, - }, - { - 'POLL-INTERVAL': 100, - }, - ); - - mock - .onGet(mockPipelineWithStages.details.stages[0].dropdown_path) - .reply(HTTP_STATUS_OK, stageReply); - - createComponent(); - - stopMock = jest.spyOn(window, 'clearTimeout'); - restartMock = jest.spyOn(axios, 'get'); - }); - - describe('when a request is being made', () => { - beforeEach(async () => { - mock.onGet(mockPipelinesEndpoint).reply(HTTP_STATUS_OK, mockPipelinesResponse); - - await waitForPromises(); - }); - - it('stops polling, cancels the request, & restarts polling', async () => { - // Mock init a polling cycle - wrapper.vm.poll.options.notificationCallback(true); - - await findStagesDropdownToggle().trigger('click'); - jest.runOnlyPendingTimers(); - - // cancelMock is getting overwritten in pipelines_service.js#L29 - // so we have to spy on it again here - cancelMock = jest.spyOn(axios.CancelToken, 'source'); - - await waitForPromises(); - - expect(cancelMock).toHaveBeenCalled(); - expect(stopMock).toHaveBeenCalled(); - expect(restartMock).toHaveBeenCalledWith( - `${mockPipelinesResponse.pipelines[0].path}/stage.json?stage=build`, - ); - }); - - it('stops polling & restarts polling', async () => { - await findStagesDropdownToggle().trigger('click'); - jest.runOnlyPendingTimers(); - await waitForPromises(); - - expect(cancelMock).not.toHaveBeenCalled(); - expect(stopMock).toHaveBeenCalled(); - expect(restartMock).toHaveBeenCalledWith( - `${mockPipelinesResponse.pipelines[0].path}/stage.json?stage=build`, - ); - }); - }); - }); - }); - - describe('when pipelines cannot be loaded', () => { - beforeEach(() => { - mock.onGet(mockPipelinesEndpoint).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR, {}); - }); - - describe('when user has no permissions', () => { - beforeEach(async () => { - createComponent({ hasGitlabCi: false, canCreatePipeline: true, ...noPermissions }); - - await waitForPromises(); - }); - - it('renders tabs', () => { - expect(findNavigationTabs().exists()).toBe(true); - expect(findTab('all').text()).toBe('All'); - }); - - it('does not render buttons', () => { - expect(findRunPipelineButton().exists()).toBe(false); - expect(findCiLintButton().exists()).toBe(false); - expect(findCleanCacheButton().exists()).toBe(false); - }); - - it('shows error state', () => { - expect(findEmptyState().props('title')).toBe('There was an error fetching the pipelines.'); - expect(findEmptyState().props('description')).toBe( - 'Try again in a few moments or contact your support team.', - ); - }); - }); - - describe('when user has permissions', () => { - beforeEach(async () => { - createComponent(); - - await waitForPromises(); - }); - - it('renders tabs', () => { - expect(findTab('all').text()).toBe('All'); - }); - - it('renders buttons', () => { - expect(findRunPipelineButton().attributes('href')).toBe(paths.newPipelinePath); - - expect(findCiLintButton().attributes('href')).toBe(paths.ciLintPath); - expect(findCleanCacheButton().text()).toBe('Clear runner caches'); - }); - - it('shows error state', () => { - expect(findEmptyState().props('title')).toBe('There was an error fetching the pipelines.'); - expect(findEmptyState().props('description')).toBe( - 'Try again in a few moments or contact your support team.', - ); - }); - }); - }); -}); diff --git a/spec/frontend/pipelines/pipelines_store_spec.js b/spec/frontend/pipelines/pipelines_store_spec.js deleted file mode 100644 index f374ecd0c0a..00000000000 --- a/spec/frontend/pipelines/pipelines_store_spec.js +++ /dev/null @@ -1,80 +0,0 @@ -import PipelineStore from '~/pipelines/stores/pipelines_store'; - -describe('Pipelines Store', () => { - let store; - - beforeEach(() => { - store = new PipelineStore(); - }); - - it('should be initialized with an empty state', () => { - expect(store.state.pipelines).toEqual([]); - expect(store.state.count).toEqual({}); - expect(store.state.pageInfo).toEqual({}); - }); - - describe('storePipelines', () => { - it('should use the default parameter if none is provided', () => { - store.storePipelines(); - - expect(store.state.pipelines).toEqual([]); - }); - - it('should store the provided array', () => { - const array = [ - { id: 1, status: 'running' }, - { id: 2, status: 'success' }, - ]; - store.storePipelines(array); - - expect(store.state.pipelines).toEqual(array); - }); - }); - - describe('storeCount', () => { - it('should use the default parameter if none is provided', () => { - store.storeCount(); - - expect(store.state.count).toEqual({}); - }); - - it('should store the provided count', () => { - const count = { all: 20, finished: 10 }; - store.storeCount(count); - - expect(store.state.count).toEqual(count); - }); - }); - - describe('storePagination', () => { - it('should use the default parameter if none is provided', () => { - store.storePagination(); - - expect(store.state.pageInfo).toEqual({}); - }); - - it('should store pagination information normalized and parsed', () => { - const pagination = { - 'X-nExt-pAge': '2', - 'X-page': '1', - 'X-Per-Page': '1', - 'X-Prev-Page': '2', - 'X-TOTAL': '37', - 'X-Total-Pages': '2', - }; - - const expectedResult = { - perPage: 1, - page: 1, - total: 37, - totalPages: 2, - nextPage: 2, - previousPage: 2, - }; - - store.storePagination(pagination); - - expect(store.state.pageInfo).toEqual(expectedResult); - }); - }); -}); diff --git a/spec/frontend/pipelines/pipelines_table_spec.js b/spec/frontend/pipelines/pipelines_table_spec.js deleted file mode 100644 index 950a6b21e16..00000000000 --- a/spec/frontend/pipelines/pipelines_table_spec.js +++ /dev/null @@ -1,280 +0,0 @@ -import '~/commons'; -import { GlTableLite } from '@gitlab/ui'; -import { mount } from '@vue/test-utils'; -import fixture from 'test_fixtures/pipelines/pipelines.json'; -import { mockTracking, unmockTracking } from 'helpers/tracking_helper'; -import { extendedWrapper } from 'helpers/vue_test_utils_helper'; -import LegacyPipelineMiniGraph from '~/pipelines/components/pipeline_mini_graph/legacy_pipeline_mini_graph.vue'; -import PipelineFailedJobsWidget from '~/pipelines/components/pipelines_list/failure_widget/pipeline_failed_jobs_widget.vue'; -import PipelineOperations from '~/pipelines/components/pipelines_list/pipeline_operations.vue'; -import PipelineTriggerer from '~/pipelines/components/pipelines_list/pipeline_triggerer.vue'; -import PipelineUrl from '~/pipelines/components/pipelines_list/pipeline_url.vue'; -import PipelinesTable from '~/pipelines/components/pipelines_list/pipelines_table.vue'; -import PipelinesTimeago from '~/pipelines/components/pipelines_list/time_ago.vue'; -import { - PipelineKeyOptions, - BUTTON_TOOLTIP_RETRY, - BUTTON_TOOLTIP_CANCEL, - TRACKING_CATEGORIES, -} from '~/pipelines/constants'; - -import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue'; - -jest.mock('~/pipelines/event_hub'); - -describe('Pipelines Table', () => { - let pipeline; - let wrapper; - let trackingSpy; - - const defaultProvide = { - glFeatures: {}, - withFailedJobsDetails: false, - }; - - const provideWithDetails = { - glFeatures: { - ciJobFailuresInMr: true, - }, - withFailedJobsDetails: true, - }; - - const defaultProps = { - pipelines: [], - viewType: 'root', - pipelineKeyOption: PipelineKeyOptions[0], - }; - - const createMockPipeline = () => { - // Clone fixture as it could be modified by tests - const { pipelines } = JSON.parse(JSON.stringify(fixture)); - return pipelines.find((p) => p.user !== null && p.commit !== null); - }; - - const createComponent = (props = {}, provide = {}) => { - wrapper = extendedWrapper( - mount(PipelinesTable, { - propsData: { - ...defaultProps, - ...props, - }, - provide: { - ...defaultProvide, - ...provide, - }, - stubs: ['PipelineFailedJobsWidget'], - }), - ); - }; - - const findGlTableLite = () => wrapper.findComponent(GlTableLite); - const findCiBadgeLink = () => wrapper.findComponent(CiBadgeLink); - const findPipelineInfo = () => wrapper.findComponent(PipelineUrl); - const findTriggerer = () => wrapper.findComponent(PipelineTriggerer); - const findLegacyPipelineMiniGraph = () => wrapper.findComponent(LegacyPipelineMiniGraph); - const findTimeAgo = () => wrapper.findComponent(PipelinesTimeago); - const findActions = () => wrapper.findComponent(PipelineOperations); - - const findPipelineFailureWidget = () => wrapper.findComponent(PipelineFailedJobsWidget); - const findTableRows = () => wrapper.findAllByTestId('pipeline-table-row'); - const findStatusTh = () => wrapper.findByTestId('status-th'); - const findPipelineTh = () => wrapper.findByTestId('pipeline-th'); - const findStagesTh = () => wrapper.findByTestId('stages-th'); - const findActionsTh = () => wrapper.findByTestId('actions-th'); - const findRetryBtn = () => wrapper.findByTestId('pipelines-retry-button'); - const findCancelBtn = () => wrapper.findByTestId('pipelines-cancel-button'); - - beforeEach(() => { - pipeline = createMockPipeline(); - }); - - describe('Pipelines Table', () => { - beforeEach(() => { - createComponent({ pipelines: [pipeline], viewType: 'root' }); - }); - - it('displays table', () => { - expect(findGlTableLite().exists()).toBe(true); - }); - - it('should render table head with correct columns', () => { - expect(findStatusTh().text()).toBe('Status'); - expect(findPipelineTh().text()).toBe('Pipeline'); - expect(findStagesTh().text()).toBe('Stages'); - expect(findActionsTh().text()).toBe('Actions'); - }); - - it('should display a table row', () => { - expect(findTableRows()).toHaveLength(1); - }); - - describe('status cell', () => { - it('should render a status badge', () => { - expect(findCiBadgeLink().exists()).toBe(true); - }); - }); - - describe('pipeline cell', () => { - it('should render pipeline information', () => { - expect(findPipelineInfo().exists()).toBe(true); - }); - - it('should display the pipeline id', () => { - expect(findPipelineInfo().text()).toContain(`#${pipeline.id}`); - }); - }); - - describe('stages cell', () => { - it('should render pipeline mini graph', () => { - expect(findLegacyPipelineMiniGraph().exists()).toBe(true); - }); - - it('should render the right number of stages', () => { - const stagesLength = pipeline.details.stages.length; - expect(findLegacyPipelineMiniGraph().props('stages').length).toBe(stagesLength); - }); - - it('should render the latest downstream pipelines only', () => { - // component receives two downstream pipelines. one of them is already outdated - // because we retried the trigger job, so the mini pipeline graph will only - // render the newly created downstream pipeline instead - expect(pipeline.triggered).toHaveLength(2); - expect(findLegacyPipelineMiniGraph().props('downstreamPipelines')).toHaveLength(1); - }); - - describe('when pipeline does not have stages', () => { - beforeEach(() => { - pipeline = createMockPipeline(); - pipeline.details.stages = []; - - createComponent({ pipelines: [pipeline] }); - }); - - it('stages are not rendered', () => { - expect(findLegacyPipelineMiniGraph().props('stages')).toHaveLength(0); - }); - }); - }); - - describe('duration cell', () => { - it('should render duration information', () => { - expect(findTimeAgo().exists()).toBe(true); - }); - }); - - describe('operations cell', () => { - it('should render pipeline operations', () => { - expect(findActions().exists()).toBe(true); - }); - - it('should render retry action tooltip', () => { - expect(findRetryBtn().attributes('title')).toBe(BUTTON_TOOLTIP_RETRY); - }); - - it('should render cancel action tooltip', () => { - expect(findCancelBtn().attributes('title')).toBe(BUTTON_TOOLTIP_CANCEL); - }); - }); - - describe('triggerer cell', () => { - it('should render the pipeline triggerer', () => { - expect(findTriggerer().exists()).toBe(true); - }); - }); - - describe('failed jobs details', () => { - describe('row', () => { - describe('when the FF is disabled', () => { - beforeEach(() => { - createComponent({ pipelines: [pipeline] }); - }); - - it('does not render', () => { - expect(findTableRows()).toHaveLength(1); - expect(findPipelineFailureWidget().exists()).toBe(false); - }); - }); - - describe('when the FF is enabled', () => { - describe('and `withFailedJobsDetails` value is provided', () => { - beforeEach(() => { - createComponent({ pipelines: [pipeline] }, provideWithDetails); - }); - - it('renders', () => { - expect(findTableRows()).toHaveLength(2); - expect(findPipelineFailureWidget().exists()).toBe(true); - }); - - it('passes the expected props', () => { - expect(findPipelineFailureWidget().props()).toStrictEqual({ - failedJobsCount: pipeline.failed_builds.length, - isPipelineActive: pipeline.active, - pipelineIid: pipeline.iid, - pipelinePath: pipeline.path, - // Make sure the forward slash was removed - projectPath: 'frontend-fixtures/pipelines-project', - }); - }); - }); - - describe('and `withFailedJobsDetails` value is not provided', () => { - beforeEach(() => { - createComponent( - { pipelines: [pipeline] }, - { glFeatures: { ciJobFailuresInMr: true } }, - ); - }); - - it('does not render', () => { - expect(findTableRows()).toHaveLength(1); - expect(findPipelineFailureWidget().exists()).toBe(false); - }); - }); - }); - }); - }); - - describe('tracking', () => { - beforeEach(() => { - trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn); - }); - - afterEach(() => { - unmockTracking(); - }); - - it('tracks status badge click', () => { - findCiBadgeLink().vm.$emit('ciStatusBadgeClick'); - - expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_ci_status_badge', { - label: TRACKING_CATEGORIES.table, - }); - }); - - it('tracks retry pipeline button click', () => { - findRetryBtn().vm.$emit('click'); - - expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_retry_button', { - label: TRACKING_CATEGORIES.table, - }); - }); - - it('tracks cancel pipeline button click', () => { - findCancelBtn().vm.$emit('click'); - - expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_cancel_button', { - label: TRACKING_CATEGORIES.table, - }); - }); - - it('tracks pipeline mini graph stage click', () => { - findLegacyPipelineMiniGraph().vm.$emit('miniGraphStageClick'); - - expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_minigraph', { - label: TRACKING_CATEGORIES.table, - }); - }); - }); - }); -}); diff --git a/spec/frontend/pipelines/test_reports/empty_state_spec.js b/spec/frontend/pipelines/test_reports/empty_state_spec.js deleted file mode 100644 index ee0f8a90a11..00000000000 --- a/spec/frontend/pipelines/test_reports/empty_state_spec.js +++ /dev/null @@ -1,45 +0,0 @@ -import { GlEmptyState } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; -import EmptyState, { i18n } from '~/pipelines/components/test_reports/empty_state.vue'; - -describe('Test report empty state', () => { - let wrapper; - - const findEmptyState = () => wrapper.findComponent(GlEmptyState); - - const createComponent = ({ hasTestReport = true } = {}) => { - wrapper = shallowMount(EmptyState, { - provide: { - emptyStateImagePath: '/image/path', - hasTestReport, - }, - stubs: { - GlEmptyState, - }, - }); - }; - - describe('when pipeline has a test report', () => { - it('should render empty test report message', () => { - createComponent(); - - expect(findEmptyState().props()).toMatchObject({ - primaryButtonText: i18n.noTestsButton, - description: i18n.noTestsDescription, - title: i18n.noTestsTitle, - }); - }); - }); - - describe('when pipeline does not have a test report', () => { - it('should render no test report message', () => { - createComponent({ hasTestReport: false }); - - expect(findEmptyState().props()).toMatchObject({ - primaryButtonText: i18n.noReportsButton, - description: i18n.noReportsDescription, - title: i18n.noReportsTitle, - }); - }); - }); -}); diff --git a/spec/frontend/pipelines/test_reports/mock_data.js b/spec/frontend/pipelines/test_reports/mock_data.js deleted file mode 100644 index c3ca1429842..00000000000 --- a/spec/frontend/pipelines/test_reports/mock_data.js +++ /dev/null @@ -1,31 +0,0 @@ -import { TestStatus } from '~/pipelines/constants'; - -export default [ - { - classname: 'spec.test_spec', - file: 'spec/trace_spec.rb', - execution_time: 0, - name: 'Test#skipped text', - stack_trace: null, - status: TestStatus.SKIPPED, - system_output: null, - }, - { - classname: 'spec.test_spec', - file: 'spec/trace_spec.rb', - execution_time: 0, - name: 'Test#error text', - stack_trace: null, - status: TestStatus.ERROR, - system_output: null, - }, - { - classname: 'spec.test_spec', - file: 'spec/trace_spec.rb', - execution_time: 0, - name: 'Test#unknown text', - stack_trace: null, - status: TestStatus.UNKNOWN, - system_output: null, - }, -]; diff --git a/spec/frontend/pipelines/test_reports/stores/actions_spec.js b/spec/frontend/pipelines/test_reports/stores/actions_spec.js deleted file mode 100644 index e05d2151f0a..00000000000 --- a/spec/frontend/pipelines/test_reports/stores/actions_spec.js +++ /dev/null @@ -1,149 +0,0 @@ -import MockAdapter from 'axios-mock-adapter'; -import testReports from 'test_fixtures/pipelines/test_report.json'; -import { TEST_HOST } from 'helpers/test_constants'; -import testAction from 'helpers/vuex_action_helper'; -import { createAlert } from '~/alert'; -import axios from '~/lib/utils/axios_utils'; -import { HTTP_STATUS_OK } from '~/lib/utils/http_status'; -import * as actions from '~/pipelines/stores/test_reports/actions'; -import * as types from '~/pipelines/stores/test_reports/mutation_types'; - -jest.mock('~/alert'); - -describe('Actions TestReports Store', () => { - let mock; - let state; - - const summary = { total_count: 1 }; - - const suiteEndpoint = `${TEST_HOST}/tests/suite.json`; - const summaryEndpoint = `${TEST_HOST}/test_reports/summary.json`; - const defaultState = { - suiteEndpoint, - summaryEndpoint, - testReports: {}, - selectedSuite: null, - }; - - beforeEach(() => { - mock = new MockAdapter(axios); - state = { ...defaultState }; - }); - - afterEach(() => { - mock.restore(); - }); - - describe('fetch report summary', () => { - beforeEach(() => { - mock.onGet(summaryEndpoint).replyOnce(HTTP_STATUS_OK, summary, {}); - }); - - it('sets testReports and shows tests', () => { - return testAction( - actions.fetchSummary, - null, - state, - [{ type: types.SET_SUMMARY, payload: summary }], - [{ type: 'toggleLoading' }, { type: 'toggleLoading' }], - ); - }); - - it('should create alert on API error', async () => { - await testAction( - actions.fetchSummary, - null, - { summaryEndpoint: null }, - [], - [{ type: 'toggleLoading' }, { type: 'toggleLoading' }], - ); - expect(createAlert).toHaveBeenCalled(); - }); - }); - - describe('fetch test suite', () => { - beforeEach(() => { - const buildIds = [1]; - testReports.test_suites[0].build_ids = buildIds; - mock - .onGet(suiteEndpoint, { params: { build_ids: buildIds } }) - .replyOnce(HTTP_STATUS_OK, testReports.test_suites[0], {}); - }); - - it('sets test suite and shows tests', () => { - const suite = testReports.test_suites[0]; - const index = 0; - - return testAction( - actions.fetchTestSuite, - index, - { ...state, testReports }, - [{ type: types.SET_SUITE, payload: { suite, index } }], - [{ type: 'toggleLoading' }, { type: 'toggleLoading' }], - ); - }); - - it('should call SET_SUITE_ERROR on error', () => { - const index = 0; - - return testAction( - actions.fetchTestSuite, - index, - { ...state, testReports, suiteEndpoint: null }, - [{ type: types.SET_SUITE_ERROR, payload: expect.any(Error) }], - [{ type: 'toggleLoading' }, { type: 'toggleLoading' }], - ); - }); - - describe('when we already have the suite data', () => { - it('should not fetch suite', () => { - const index = 0; - testReports.test_suites[0].hasFullSuite = true; - - return testAction(actions.fetchTestSuite, index, { ...state, testReports }, [], []); - }); - }); - }); - - describe('set selected suite index', () => { - it('sets selectedSuiteIndex', () => { - const selectedSuiteIndex = 0; - - return testAction( - actions.setSelectedSuiteIndex, - selectedSuiteIndex, - { ...state, hasFullReport: true }, - [{ type: types.SET_SELECTED_SUITE_INDEX, payload: selectedSuiteIndex }], - [], - ); - }); - }); - - describe('remove selected suite index', () => { - it('sets selectedSuiteIndex to null', () => { - return testAction( - actions.removeSelectedSuiteIndex, - {}, - state, - [{ type: types.SET_SELECTED_SUITE_INDEX, payload: null }], - [], - ); - }); - }); - - describe('toggles loading', () => { - it('sets isLoading to true', () => { - return testAction(actions.toggleLoading, {}, state, [{ type: types.TOGGLE_LOADING }], []); - }); - - it('toggles isLoading to false', () => { - return testAction( - actions.toggleLoading, - {}, - { ...state, isLoading: true }, - [{ type: types.TOGGLE_LOADING }], - [], - ); - }); - }); -}); diff --git a/spec/frontend/pipelines/test_reports/stores/getters_spec.js b/spec/frontend/pipelines/test_reports/stores/getters_spec.js deleted file mode 100644 index 70e3a01dbf1..00000000000 --- a/spec/frontend/pipelines/test_reports/stores/getters_spec.js +++ /dev/null @@ -1,171 +0,0 @@ -import testReports from 'test_fixtures/pipelines/test_report.json'; -import * as getters from '~/pipelines/stores/test_reports/getters'; -import { - iconForTestStatus, - formatFilePath, - formattedTime, -} from '~/pipelines/stores/test_reports/utils'; - -describe('Getters TestReports Store', () => { - let state; - - const defaultState = { - blobPath: '/test/blob/path', - testReports, - selectedSuiteIndex: 0, - pageInfo: { - page: 1, - perPage: 2, - }, - }; - - const emptyState = { - blobPath: '', - testReports: {}, - selectedSuite: null, - pageInfo: { - page: 1, - perPage: 2, - }, - }; - - beforeEach(() => { - state = { - testReports, - }; - }); - - const setupState = (testState = defaultState) => { - state = testState; - }; - - describe('getTestSuites', () => { - it('should return the test suites', () => { - setupState(); - - const suites = getters.getTestSuites(state); - const expected = testReports.test_suites.map((x) => ({ - ...x, - formattedTime: formattedTime(x.total_time), - })); - - expect(suites).toEqual(expected); - }); - - it('should return an empty array when testReports is empty', () => { - setupState(emptyState); - - expect(getters.getTestSuites(state)).toEqual([]); - }); - }); - - describe('getSelectedSuite', () => { - it('should return the selected suite', () => { - setupState(); - - const selectedSuite = getters.getSelectedSuite(state); - const expected = testReports.test_suites[state.selectedSuiteIndex]; - - expect(selectedSuite).toEqual(expected); - }); - }); - - describe('getSuiteTests', () => { - it('should return the current page of test cases inside the suite', () => { - setupState(); - - const cases = getters.getSuiteTests(state); - const expected = testReports.test_suites[0].test_cases - .map((x) => ({ - ...x, - filePath: `${state.blobPath}/${formatFilePath(x.file)}`, - formattedTime: formattedTime(x.execution_time), - icon: iconForTestStatus(x.status), - })) - .slice(0, state.pageInfo.perPage); - - expect(cases).toEqual(expected); - }); - - it('should return an empty array when testReports is empty', () => { - setupState(emptyState); - - expect(getters.getSuiteTests(state)).toEqual([]); - }); - - describe('when a test case classname property is null', () => { - it('should return an empty string value for the classname property', () => { - const testCases = testReports.test_suites[0].test_cases; - setupState({ - ...defaultState, - testReports: { - ...testReports, - test_suites: [ - { - test_cases: testCases.map((testCase) => ({ - ...testCase, - classname: null, - })), - }, - ], - }, - }); - - const expected = testCases - .map((x) => ({ - ...x, - classname: '', - filePath: `${state.blobPath}/${formatFilePath(x.file)}`, - formattedTime: formattedTime(x.execution_time), - icon: iconForTestStatus(x.status), - })) - .slice(0, state.pageInfo.perPage); - - expect(getters.getSuiteTests(state)).toEqual(expected); - }); - }); - - describe('when a test case name property is null', () => { - it('should return an empty string value for the name property', () => { - const testCases = testReports.test_suites[0].test_cases; - setupState({ - ...defaultState, - testReports: { - ...testReports, - test_suites: [ - { - test_cases: testCases.map((testCase) => ({ - ...testCase, - name: null, - })), - }, - ], - }, - }); - - const expected = testCases - .map((x) => ({ - ...x, - name: '', - filePath: `${state.blobPath}/${formatFilePath(x.file)}`, - formattedTime: formattedTime(x.execution_time), - icon: iconForTestStatus(x.status), - })) - .slice(0, state.pageInfo.perPage); - - expect(getters.getSuiteTests(state)).toEqual(expected); - }); - }); - }); - - describe('getSuiteTestCount', () => { - it('should return the total number of test cases', () => { - setupState(); - - const testCount = getters.getSuiteTestCount(state); - const expected = testReports.test_suites[0].test_cases.length; - - expect(testCount).toEqual(expected); - }); - }); -}); diff --git a/spec/frontend/pipelines/test_reports/stores/mutations_spec.js b/spec/frontend/pipelines/test_reports/stores/mutations_spec.js deleted file mode 100644 index 685ac6ea3e5..00000000000 --- a/spec/frontend/pipelines/test_reports/stores/mutations_spec.js +++ /dev/null @@ -1,114 +0,0 @@ -import testReports from 'test_fixtures/pipelines/test_report.json'; -import * as types from '~/pipelines/stores/test_reports/mutation_types'; -import mutations from '~/pipelines/stores/test_reports/mutations'; -import { createAlert } from '~/alert'; - -jest.mock('~/alert'); - -describe('Mutations TestReports Store', () => { - let mockState; - - const defaultState = { - endpoint: '', - testReports: {}, - selectedSuite: null, - isLoading: false, - pageInfo: { - page: 1, - perPage: 2, - }, - }; - - beforeEach(() => { - mockState = { ...defaultState }; - }); - - describe('set page', () => { - it('should set the current page to display', () => { - const pageToDisplay = 3; - mutations[types.SET_PAGE](mockState, pageToDisplay); - - expect(mockState.pageInfo.page).toEqual(pageToDisplay); - }); - }); - - describe('set suite', () => { - it('should set the suite at the given index', () => { - mockState.testReports = testReports; - const suite = { name: 'test_suite' }; - const index = 0; - const expectedState = { ...mockState }; - expectedState.testReports.test_suites[index] = { suite, hasFullSuite: true }; - mutations[types.SET_SUITE](mockState, { suite, index }); - - expect(mockState.testReports.test_suites[index]).toEqual( - expectedState.testReports.test_suites[index], - ); - }); - }); - - describe('set suite error', () => { - it('should set the error message in state if provided', () => { - const message = 'Test report artifacts not found'; - - mutations[types.SET_SUITE_ERROR](mockState, { - response: { data: { errors: message } }, - }); - - expect(mockState.errorMessage).toBe(message); - }); - - it('should show an alert otherwise', () => { - mutations[types.SET_SUITE_ERROR](mockState, {}); - - expect(createAlert).toHaveBeenCalled(); - }); - }); - - describe('set selected suite index', () => { - it('should set selectedSuiteIndex', () => { - const selectedSuiteIndex = 0; - mutations[types.SET_SELECTED_SUITE_INDEX](mockState, selectedSuiteIndex); - - expect(mockState.selectedSuiteIndex).toEqual(selectedSuiteIndex); - }); - }); - - describe('set summary', () => { - it('should set summary', () => { - const summary = { - total: { time: 0, count: 10, success: 1, failed: 2, skipped: 3, error: 4 }, - }; - const expectedSummary = { - ...summary, - total_time: 0, - total_count: 10, - success_count: 1, - failed_count: 2, - skipped_count: 3, - error_count: 4, - }; - mutations[types.SET_SUMMARY](mockState, summary); - - expect(mockState.testReports).toEqual(expectedSummary); - }); - }); - - describe('toggle loading', () => { - it('should set to true', () => { - const expectedState = { ...mockState, isLoading: true }; - mutations[types.TOGGLE_LOADING](mockState); - - expect(mockState.isLoading).toEqual(expectedState.isLoading); - }); - - it('should toggle back to false', () => { - const expectedState = { ...mockState, isLoading: false }; - mockState.isLoading = true; - - mutations[types.TOGGLE_LOADING](mockState); - - expect(mockState.isLoading).toEqual(expectedState.isLoading); - }); - }); -}); diff --git a/spec/frontend/pipelines/test_reports/stores/utils_spec.js b/spec/frontend/pipelines/test_reports/stores/utils_spec.js deleted file mode 100644 index 703fe69026c..00000000000 --- a/spec/frontend/pipelines/test_reports/stores/utils_spec.js +++ /dev/null @@ -1,40 +0,0 @@ -import { formatFilePath, formattedTime } from '~/pipelines/stores/test_reports/utils'; - -describe('Test reports utils', () => { - describe('formatFilePath', () => { - it.each` - file | expected - ${'./test.js'} | ${'test.js'} - ${'/test.js'} | ${'test.js'} - ${'.//////////////test.js'} | ${'test.js'} - ${'test.js'} | ${'test.js'} - ${'mock/path./test.js'} | ${'mock/path./test.js'} - ${'./mock/path./test.js'} | ${'mock/path./test.js'} - `('should format $file to be $expected', ({ file, expected }) => { - expect(formatFilePath(file)).toBe(expected); - }); - }); - - describe('formattedTime', () => { - describe('when time is smaller than a second', () => { - it('should return time in milliseconds fixed to 2 decimals', () => { - const result = formattedTime(0.4815162342); - expect(result).toBe('481.52ms'); - }); - }); - - describe('when time is equal to a second', () => { - it('should return time in seconds fixed to 2 decimals', () => { - const result = formattedTime(1); - expect(result).toBe('1.00s'); - }); - }); - - describe('when time is greater than a second', () => { - it('should return time in seconds fixed to 2 decimals', () => { - const result = formattedTime(4.815162342); - expect(result).toBe('4.82s'); - }); - }); - }); -}); diff --git a/spec/frontend/pipelines/test_reports/test_case_details_spec.js b/spec/frontend/pipelines/test_reports/test_case_details_spec.js deleted file mode 100644 index f8663408817..00000000000 --- a/spec/frontend/pipelines/test_reports/test_case_details_spec.js +++ /dev/null @@ -1,149 +0,0 @@ -import { GlModal, GlLink } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; -import { extendedWrapper } from 'helpers/vue_test_utils_helper'; -import TestCaseDetails from '~/pipelines/components/test_reports/test_case_details.vue'; -import CodeBlock from '~/vue_shared/components/code_block.vue'; -import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue'; - -describe('Test case details', () => { - let wrapper; - const defaultTestCase = { - classname: 'spec.test_spec', - name: 'Test#something cool', - file: '~/index.js', - filePath: '/src/javascripts/index.js', - formattedTime: '10.04ms', - recent_failures: { - count: 2, - base_branch: 'main', - }, - system_output: 'Line 42 is broken', - }; - - const findCopyFileBtn = () => wrapper.findComponent(ModalCopyButton); - const findModal = () => wrapper.findComponent(GlModal); - const findName = () => wrapper.findByTestId('test-case-name'); - const findFile = () => wrapper.findByTestId('test-case-file'); - const findFileLink = () => wrapper.findComponent(GlLink); - const findDuration = () => wrapper.findByTestId('test-case-duration'); - const findRecentFailures = () => wrapper.findByTestId('test-case-recent-failures'); - const findAttachmentUrl = () => wrapper.findByTestId('test-case-attachment-url'); - const findSystemOutput = () => wrapper.findByTestId('test-case-trace'); - - const createComponent = (testCase = {}) => { - wrapper = extendedWrapper( - shallowMount(TestCaseDetails, { - propsData: { - modalId: 'my-modal', - testCase: { - ...defaultTestCase, - ...testCase, - }, - }, - stubs: { CodeBlock, GlModal }, - }), - ); - }; - - describe('required details', () => { - beforeEach(() => { - createComponent(); - }); - - it('renders the test case classname as modal title', () => { - expect(findModal().props('title')).toBe(defaultTestCase.classname); - }); - - it('renders the test case name', () => { - expect(findName().text()).toBe(defaultTestCase.name); - }); - - it('renders the test case file', () => { - expect(findFile().text()).toBe(defaultTestCase.file); - expect(findFileLink().attributes('href')).toBe(defaultTestCase.filePath); - }); - - it('renders copy button for test case file', () => { - expect(findCopyFileBtn().attributes('text')).toBe(defaultTestCase.file); - }); - - it('renders the test case duration', () => { - expect(findDuration().text()).toBe(defaultTestCase.formattedTime); - }); - }); - - describe('when test case has execution time instead of formatted time', () => { - beforeEach(() => { - createComponent({ ...defaultTestCase, formattedTime: null, execution_time: 17 }); - }); - - it('renders the test case duration', () => { - expect(findDuration().text()).toBe('17 s'); - }); - }); - - describe('when test case has recent failures', () => { - describe('has only 1 recent failure', () => { - it('renders the recent failure', () => { - createComponent({ recent_failures: { ...defaultTestCase.recent_failures, count: 1 } }); - - expect(findRecentFailures().text()).toContain( - `Failed 1 time in ${defaultTestCase.recent_failures.base_branch} in the last 14 days`, - ); - }); - }); - - describe('has more than 1 recent failure', () => { - it('renders the recent failures', () => { - createComponent(); - - expect(findRecentFailures().text()).toContain( - `Failed ${defaultTestCase.recent_failures.count} times in ${defaultTestCase.recent_failures.base_branch} in the last 14 days`, - ); - }); - }); - }); - - describe('when test case does not have recent failures', () => { - it('does not render the recent failures', () => { - createComponent({ recent_failures: null }); - - expect(findRecentFailures().exists()).toBe(false); - }); - }); - - describe('when test case has attachment URL', () => { - it('renders the attachment URL as a link', () => { - const expectedUrl = '/my/path.jpg'; - createComponent({ attachment_url: expectedUrl }); - const attachmentUrl = findAttachmentUrl(); - - expect(attachmentUrl.exists()).toBe(true); - expect(attachmentUrl.attributes('href')).toBe(expectedUrl); - }); - }); - - describe('when test case does not have attachment URL', () => { - it('does not render the attachment URL', () => { - createComponent({ attachment_url: null }); - - expect(findAttachmentUrl().exists()).toBe(false); - }); - }); - - describe('when test case has system output', () => { - it('renders the test case system output', () => { - createComponent(); - - expect(findSystemOutput().text()).toContain(defaultTestCase.system_output); - }); - }); - - describe('when test case does not have system output', () => { - it('does not render the test case system output', () => { - createComponent({ system_output: null }); - - expect(findSystemOutput().exists()).toBe(false); - }); - }); -}); diff --git a/spec/frontend/pipelines/test_reports/test_reports_spec.js b/spec/frontend/pipelines/test_reports/test_reports_spec.js deleted file mode 100644 index de16f496eff..00000000000 --- a/spec/frontend/pipelines/test_reports/test_reports_spec.js +++ /dev/null @@ -1,125 +0,0 @@ -import { GlLoadingIcon } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; -import Vue from 'vue'; -// eslint-disable-next-line no-restricted-imports -import Vuex from 'vuex'; -import testReports from 'test_fixtures/pipelines/test_report.json'; -import { extendedWrapper } from 'helpers/vue_test_utils_helper'; -import EmptyState from '~/pipelines/components/test_reports/empty_state.vue'; -import TestReports from '~/pipelines/components/test_reports/test_reports.vue'; -import TestSummary from '~/pipelines/components/test_reports/test_summary.vue'; -import TestSummaryTable from '~/pipelines/components/test_reports/test_summary_table.vue'; -import * as getters from '~/pipelines/stores/test_reports/getters'; - -Vue.use(Vuex); - -describe('Test reports app', () => { - let wrapper; - let store; - - const loadingSpinner = () => wrapper.findComponent(GlLoadingIcon); - const testsDetail = () => wrapper.findByTestId('tests-detail'); - const emptyState = () => wrapper.findComponent(EmptyState); - const testSummary = () => wrapper.findComponent(TestSummary); - const testSummaryTable = () => wrapper.findComponent(TestSummaryTable); - - const actionSpies = { - fetchTestSuite: jest.fn(), - fetchSummary: jest.fn(), - setSelectedSuiteIndex: jest.fn(), - removeSelectedSuiteIndex: jest.fn(), - }; - - const createComponent = ({ state = {} } = {}) => { - store = new Vuex.Store({ - modules: { - testReports: { - namespaced: true, - state: { - isLoading: false, - selectedSuiteIndex: null, - testReports, - ...state, - }, - actions: actionSpies, - getters, - }, - }, - }); - - jest.spyOn(store, 'registerModule').mockReturnValue(null); - - wrapper = extendedWrapper( - shallowMount(TestReports, { - provide: { - blobPath: '/blob/path', - summaryEndpoint: '/summary.json', - suiteEndpoint: '/suite.json', - }, - store, - }), - ); - }; - - describe('when component is created', () => { - it('should call fetchSummary when pipeline has test report', () => { - createComponent(); - - expect(actionSpies.fetchSummary).toHaveBeenCalled(); - }); - }); - - describe('when loading', () => { - beforeEach(() => createComponent({ state: { isLoading: true } })); - - it('shows the loading spinner', () => { - expect(emptyState().exists()).toBe(false); - expect(testsDetail().exists()).toBe(false); - expect(loadingSpinner().exists()).toBe(true); - }); - }); - - describe('when the api returns no data', () => { - it('displays empty state component', () => { - createComponent({ state: { testReports: {} } }); - - expect(emptyState().exists()).toBe(true); - }); - }); - - describe('when the api returns data', () => { - beforeEach(() => createComponent()); - - it('sets testReports and shows tests', () => { - expect(wrapper.vm.testReports).toEqual(expect.any(Object)); - expect(wrapper.vm.showTests).toBe(true); - }); - - it('shows tests details', () => { - expect(testsDetail().exists()).toBe(true); - }); - }); - - describe('when a suite is clicked', () => { - beforeEach(() => { - createComponent({ state: { hasFullReport: true } }); - testSummaryTable().vm.$emit('row-click', 0); - }); - - it('should call setSelectedSuiteIndex and fetchTestSuite', () => { - expect(actionSpies.setSelectedSuiteIndex).toHaveBeenCalled(); - expect(actionSpies.fetchTestSuite).toHaveBeenCalled(); - }); - }); - - describe('when clicking back to summary', () => { - beforeEach(() => { - createComponent({ state: { selectedSuiteIndex: 0 } }); - testSummary().vm.$emit('on-back-click'); - }); - - it('should call removeSelectedSuiteIndex', () => { - expect(actionSpies.removeSelectedSuiteIndex).toHaveBeenCalled(); - }); - }); -}); diff --git a/spec/frontend/pipelines/test_reports/test_suite_table_spec.js b/spec/frontend/pipelines/test_reports/test_suite_table_spec.js deleted file mode 100644 index 08b430fa703..00000000000 --- a/spec/frontend/pipelines/test_reports/test_suite_table_spec.js +++ /dev/null @@ -1,169 +0,0 @@ -import { GlButton, GlFriendlyWrap, GlLink, GlPagination, GlEmptyState } from '@gitlab/ui'; -import Vue from 'vue'; -// eslint-disable-next-line no-restricted-imports -import Vuex from 'vuex'; -import testReports from 'test_fixtures/pipelines/test_report.json'; -import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; -import SuiteTable, { i18n } from '~/pipelines/components/test_reports/test_suite_table.vue'; -import { TestStatus } from '~/pipelines/constants'; -import * as getters from '~/pipelines/stores/test_reports/getters'; -import { formatFilePath } from '~/pipelines/stores/test_reports/utils'; -import { ARTIFACTS_EXPIRED_ERROR_MESSAGE } from '~/pipelines/stores/test_reports/constants'; -import skippedTestCases from './mock_data'; - -Vue.use(Vuex); - -describe('Test reports suite table', () => { - let wrapper; - let store; - - const { - test_suites: [testSuite], - } = testReports; - - testSuite.test_cases = [...testSuite.test_cases, ...skippedTestCases]; - const testCases = testSuite.test_cases; - const blobPath = '/test/blob/path'; - - const noCasesMessage = () => wrapper.findByTestId('no-test-cases'); - const artifactsExpiredMessage = () => wrapper.findByTestId('artifacts-expired'); - const artifactsExpiredEmptyState = () => wrapper.findComponent(GlEmptyState); - const allCaseRows = () => wrapper.findAllByTestId('test-case-row'); - const findCaseRowAtIndex = (index) => wrapper.findAllByTestId('test-case-row').at(index); - const findLinkForRow = (row) => row.findComponent(GlLink); - const findIconForRow = (row, status) => row.find(`.ci-status-icon-${status}`); - - const createComponent = ({ suite = testSuite, perPage = 20, errorMessage } = {}) => { - store = new Vuex.Store({ - modules: { - testReports: { - namespaced: true, - state: { - blobPath, - testReports: { - test_suites: [suite], - }, - selectedSuiteIndex: 0, - pageInfo: { - page: 1, - perPage, - }, - errorMessage, - }, - getters, - }, - }, - }); - - wrapper = shallowMountExtended(SuiteTable, { - provide: { - blobPath: '/blob/path', - summaryEndpoint: '/summary.json', - suiteEndpoint: '/suite.json', - }, - store, - stubs: { GlFriendlyWrap }, - }); - }; - - it('should render a message when there are no test cases', () => { - createComponent({ suite: [] }); - - expect(noCasesMessage().exists()).toBe(true); - expect(artifactsExpiredMessage().exists()).toBe(false); - }); - - it('should render an empty state when artifacts have expired', () => { - createComponent({ suite: [], errorMessage: ARTIFACTS_EXPIRED_ERROR_MESSAGE }); - const emptyState = artifactsExpiredEmptyState(); - - expect(noCasesMessage().exists()).toBe(false); - expect(artifactsExpiredMessage().exists()).toBe(true); - - expect(emptyState.exists()).toBe(true); - expect(emptyState.props('title')).toBe(i18n.expiredArtifactsTitle); - }); - - describe('when a test suite is supplied', () => { - beforeEach(() => createComponent()); - - it('renders the correct number of rows', () => { - expect(allCaseRows()).toHaveLength(testCases.length); - }); - - it.each([ - TestStatus.ERROR, - TestStatus.FAILED, - TestStatus.SKIPPED, - TestStatus.SUCCESS, - 'unknown', - ])('renders the correct icon for test case with %s status', (status) => { - const test = testCases.findIndex((x) => x.status === status); - const row = findCaseRowAtIndex(test); - - expect(findIconForRow(row, status).exists()).toBe(true); - }); - - it('renders the file name for the test with a copy button', () => { - const { file } = testCases[0]; - const relativeFile = formatFilePath(file); - const filePath = `${blobPath}/${relativeFile}`; - const row = findCaseRowAtIndex(0); - const fileLink = findLinkForRow(row); - const button = row.findComponent(GlButton); - - expect(fileLink.attributes('href')).toBe(filePath); - expect(row.text()).toContain(file); - expect(button.exists()).toBe(true); - expect(button.attributes('data-clipboard-text')).toBe(file); - }); - }); - - describe('when a test suite has more test cases than the pagination size', () => { - const perPage = 2; - - beforeEach(() => { - createComponent({ testSuite, perPage }); - }); - - it('renders one page of test cases', () => { - expect(allCaseRows().length).toBe(perPage); - }); - - it('renders a pagination component', () => { - expect(wrapper.findComponent(GlPagination).exists()).toBe(true); - }); - }); - - describe('when a test case classname property is null', () => { - it('still renders all test cases', () => { - createComponent({ - testSuite: { - ...testSuite, - test_cases: testSuite.test_cases.map((testCase) => ({ - ...testCase, - classname: null, - })), - }, - }); - - expect(allCaseRows()).toHaveLength(testCases.length); - }); - }); - - describe('when a test case name property is null', () => { - it('still renders all test cases', () => { - createComponent({ - testSuite: { - ...testSuite, - test_cases: testSuite.test_cases.map((testCase) => ({ - ...testCase, - name: null, - })), - }, - }); - - expect(allCaseRows()).toHaveLength(testCases.length); - }); - }); -}); diff --git a/spec/frontend/pipelines/test_reports/test_summary_spec.js b/spec/frontend/pipelines/test_reports/test_summary_spec.js deleted file mode 100644 index 7eed6671fb9..00000000000 --- a/spec/frontend/pipelines/test_reports/test_summary_spec.js +++ /dev/null @@ -1,106 +0,0 @@ -import { mount } from '@vue/test-utils'; -import testReports from 'test_fixtures/pipelines/test_report.json'; -import Summary from '~/pipelines/components/test_reports/test_summary.vue'; -import { formattedTime } from '~/pipelines/stores/test_reports/utils'; - -describe('Test reports summary', () => { - let wrapper; - - const { - test_suites: [testSuite], - } = testReports; - - const backButton = () => wrapper.find('.js-back-button'); - const totalTests = () => wrapper.find('.js-total-tests'); - const failedTests = () => wrapper.find('.js-failed-tests'); - const erroredTests = () => wrapper.find('.js-errored-tests'); - const successRate = () => wrapper.find('.js-success-rate'); - const duration = () => wrapper.find('.js-duration'); - - const defaultProps = { - report: testSuite, - showBack: false, - }; - - const createComponent = (props) => { - wrapper = mount(Summary, { - propsData: { - ...defaultProps, - ...props, - }, - }); - }; - - describe('should not render', () => { - beforeEach(() => { - createComponent(); - }); - - it('a back button by default', () => { - expect(backButton().exists()).toBe(false); - }); - }); - - describe('should render', () => { - beforeEach(() => { - createComponent(); - }); - - it('a back button and emit on-back-click event', () => { - createComponent({ - showBack: true, - }); - - expect(backButton().exists()).toBe(true); - }); - }); - - describe('when a report is supplied', () => { - beforeEach(() => { - createComponent(); - }); - - it('displays the correct total', () => { - expect(totalTests().text()).toBe('4 tests'); - }); - - it('displays the correct failure count', () => { - expect(failedTests().text()).toBe('2 failures'); - }); - - it('displays the correct error count', () => { - expect(erroredTests().text()).toBe('0 errors'); - }); - - it('calculates and displays percentages correctly', () => { - expect(successRate().text()).toBe('50% success rate'); - }); - - it('displays the correctly formatted duration', () => { - expect(duration().text()).toBe(formattedTime(testSuite.total_time)); - }); - }); - - describe('success percentage calculation', () => { - it.each` - name | successCount | totalCount | skippedCount | result - ${'displays 0 when there are no tests'} | ${0} | ${0} | ${0} | ${'0'} - ${'displays whole number when possible'} | ${10} | ${50} | ${0} | ${'20'} - ${'excludes skipped tests from total'} | ${10} | ${50} | ${5} | ${'22.22'} - ${'rounds to 0.01'} | ${1} | ${16604} | ${0} | ${'0.01'} - ${'correctly rounds to 50'} | ${8302} | ${16604} | ${0} | ${'50'} - ${'rounds down for large close numbers'} | ${16603} | ${16604} | ${0} | ${'99.99'} - ${'correctly displays 100'} | ${16604} | ${16604} | ${0} | ${'100'} - `('$name', ({ successCount, totalCount, skippedCount, result }) => { - createComponent({ - report: { - success_count: successCount, - skipped_count: skippedCount, - total_count: totalCount, - }, - }); - - expect(successRate().text()).toBe(`${result}% success rate`); - }); - }); -}); diff --git a/spec/frontend/pipelines/test_reports/test_summary_table_spec.js b/spec/frontend/pipelines/test_reports/test_summary_table_spec.js deleted file mode 100644 index a45946d5a03..00000000000 --- a/spec/frontend/pipelines/test_reports/test_summary_table_spec.js +++ /dev/null @@ -1,100 +0,0 @@ -import { mount } from '@vue/test-utils'; -import Vue from 'vue'; -// eslint-disable-next-line no-restricted-imports -import Vuex from 'vuex'; -import testReports from 'test_fixtures/pipelines/test_report.json'; -import SummaryTable from '~/pipelines/components/test_reports/test_summary_table.vue'; -import * as getters from '~/pipelines/stores/test_reports/getters'; - -Vue.use(Vuex); - -describe('Test reports summary table', () => { - let wrapper; - let store; - - const allSuitesRows = () => wrapper.findAll('.js-suite-row'); - const noSuitesToShow = () => wrapper.find('.js-no-tests-suites'); - - const defaultProps = { - testReports, - }; - - const createComponent = (reports = null) => { - store = new Vuex.Store({ - modules: { - testReports: { - namespaced: true, - state: { - testReports: reports || testReports, - }, - getters, - }, - }, - }); - - wrapper = mount(SummaryTable, { - provide: { - blobPath: '/blob/path', - summaryEndpoint: '/summary.json', - suiteEndpoint: '/suite.json', - }, - propsData: defaultProps, - store, - }); - }; - - describe('when test reports are supplied', () => { - beforeEach(() => createComponent()); - const findErrorIcon = () => wrapper.findComponent({ ref: 'suiteErrorIcon' }); - - it('renders the correct number of rows', () => { - expect(noSuitesToShow().exists()).toBe(false); - expect(allSuitesRows().length).toBe(testReports.test_suites.length); - }); - - describe('when there is a suite error', () => { - beforeEach(() => { - createComponent({ - test_suites: [ - { - ...testReports.test_suites[0], - suite_error: 'Suite Error', - }, - ], - }); - }); - - it('renders error icon', () => { - expect(findErrorIcon().exists()).toBe(true); - expect(findErrorIcon().attributes('title')).toEqual('Suite Error'); - }); - }); - - describe('when there is not a suite error', () => { - beforeEach(() => { - createComponent({ - test_suites: [ - { - ...testReports.test_suites[0], - suite_error: null, - }, - ], - }); - }); - - it('does not render error icon', () => { - expect(findErrorIcon().exists()).toBe(false); - }); - }); - }); - - describe('when there are no test suites', () => { - beforeEach(() => { - createComponent({ test_suites: [] }); - }); - - it('displays the no suites to show message', () => { - expect(noSuitesToShow().exists()).toBe(true); - }); - }); -}); diff --git a/spec/frontend/pipelines/time_ago_spec.js b/spec/frontend/pipelines/time_ago_spec.js deleted file mode 100644 index d2aa340a980..00000000000 --- a/spec/frontend/pipelines/time_ago_spec.js +++ /dev/null @@ -1,85 +0,0 @@ -import { GlIcon } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; -import { extendedWrapper } from 'helpers/vue_test_utils_helper'; -import TimeAgo from '~/pipelines/components/pipelines_list/time_ago.vue'; - -describe('Timeago component', () => { - let wrapper; - - const defaultProps = { duration: 0, finished_at: '' }; - - const createComponent = (props = defaultProps, extraProps) => { - wrapper = extendedWrapper( - shallowMount(TimeAgo, { - propsData: { - pipeline: { - details: { - ...props, - }, - }, - ...extraProps, - }, - data() { - return { - iconTimerSvg: `<svg></svg>`, - }; - }, - }), - ); - }; - - const duration = () => wrapper.find('.duration'); - const finishedAt = () => wrapper.find('.finished-at'); - const findCalendarIcon = () => wrapper.findByTestId('calendar-icon'); - - describe('with duration', () => { - beforeEach(() => { - createComponent({ duration: 10, finished_at: '' }); - }); - - it('should render duration and timer svg', () => { - const icon = duration().findComponent(GlIcon); - - expect(duration().exists()).toBe(true); - expect(icon.props('name')).toBe('timer'); - }); - }); - - describe('without duration', () => { - beforeEach(() => { - createComponent(); - }); - - it('should not render duration and timer svg', () => { - expect(duration().exists()).toBe(false); - }); - }); - - describe('with finishedTime', () => { - it('should render time', () => { - createComponent({ duration: 0, finished_at: '2017-04-26T12:40:23.277Z' }); - - const time = finishedAt().find('time'); - - expect(finishedAt().exists()).toBe(true); - expect(time.exists()).toBe(true); - }); - - it('should display calendar icon', () => { - createComponent({ duration: 0, finished_at: '2017-04-26T12:40:23.277Z' }); - - expect(findCalendarIcon().exists()).toBe(true); - }); - }); - - describe('without finishedTime', () => { - beforeEach(() => { - createComponent(); - }); - - it('should not render time and calendar icon', () => { - expect(finishedAt().exists()).toBe(false); - expect(findCalendarIcon().exists()).toBe(false); - }); - }); -}); diff --git a/spec/frontend/pipelines/tokens/pipeline_branch_name_token_spec.js b/spec/frontend/pipelines/tokens/pipeline_branch_name_token_spec.js deleted file mode 100644 index d518519a424..00000000000 --- a/spec/frontend/pipelines/tokens/pipeline_branch_name_token_spec.js +++ /dev/null @@ -1,142 +0,0 @@ -import { GlFilteredSearchToken, GlFilteredSearchSuggestion, GlLoadingIcon } from '@gitlab/ui'; -import { nextTick } from 'vue'; -import { shallowMount } from '@vue/test-utils'; -import waitForPromises from 'helpers/wait_for_promises'; -import Api from '~/api'; -import PipelineBranchNameToken from '~/pipelines/components/pipelines_list/tokens/pipeline_branch_name_token.vue'; -import { branches, mockBranchesAfterMap } from '../mock_data'; - -describe('Pipeline Branch Name Token', () => { - let wrapper; - - const findFilteredSearchToken = () => wrapper.findComponent(GlFilteredSearchToken); - const findAllFilteredSearchSuggestions = () => - wrapper.findAllComponents(GlFilteredSearchSuggestion); - const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); - const getBranchSuggestions = () => - findAllFilteredSearchSuggestions().wrappers.map((w) => w.text()); - - const stubs = { - GlFilteredSearchToken: { - template: `<div><slot name="suggestions"></slot></div>`, - }, - }; - - const defaultProps = { - config: { - type: 'ref', - icon: 'branch', - title: 'Branch name', - unique: true, - projectId: '21', - defaultBranchName: null, - disabled: false, - }, - value: { - data: '', - }, - cursorPosition: 'start', - }; - - const optionsWithDefaultBranchName = (options) => { - return { - propsData: { - ...defaultProps, - config: { - ...defaultProps.config, - defaultBranchName: 'main', - }, - }, - ...options, - }; - }; - - const createComponent = (options, data) => { - wrapper = shallowMount(PipelineBranchNameToken, { - propsData: { - ...defaultProps, - }, - data() { - return { - ...data, - }; - }, - ...options, - }); - }; - - beforeEach(() => { - jest.spyOn(Api, 'branches').mockResolvedValue({ data: branches }); - - createComponent(); - }); - - it('passes config correctly', () => { - expect(findFilteredSearchToken().props('config')).toEqual(defaultProps.config); - }); - - it('fetches and sets project branches', () => { - expect(Api.branches).toHaveBeenCalled(); - - expect(wrapper.vm.branches).toEqual(mockBranchesAfterMap); - expect(findLoadingIcon().exists()).toBe(false); - }); - - describe('displays loading icon correctly', () => { - it('shows loading icon', () => { - createComponent({ stubs }, { loading: true }); - - expect(findLoadingIcon().exists()).toBe(true); - }); - - it('does not show loading icon', () => { - createComponent({ stubs }, { loading: false }); - - expect(findLoadingIcon().exists()).toBe(false); - }); - }); - - describe('shows branches correctly', () => { - it('renders all branches', () => { - createComponent({ stubs }, { branches, loading: false }); - - expect(findAllFilteredSearchSuggestions()).toHaveLength(branches.length); - }); - - it('renders only the branch searched for', () => { - const mockBranches = ['main']; - createComponent({ stubs }, { branches: mockBranches, loading: false }); - - expect(findAllFilteredSearchSuggestions()).toHaveLength(mockBranches.length); - }); - - it('shows the default branch first if no branch was searched for', async () => { - const mockBranches = [{ name: 'branch-1' }]; - jest.spyOn(Api, 'branches').mockResolvedValue({ data: mockBranches }); - - createComponent(optionsWithDefaultBranchName({ stubs }), { loading: false }); - await nextTick(); - expect(getBranchSuggestions()).toEqual(['main', 'branch-1']); - }); - - it('does not show the default branch if a search term was provided', async () => { - const mockBranches = [{ name: 'branch-1' }]; - jest.spyOn(Api, 'branches').mockResolvedValue({ data: mockBranches }); - - createComponent(optionsWithDefaultBranchName(), { loading: false }); - - findFilteredSearchToken().vm.$emit('input', { data: 'branch-1' }); - await waitForPromises(); - expect(getBranchSuggestions()).toEqual(['branch-1']); - }); - - it('shows the default branch only once if it appears in the results', async () => { - const mockBranches = [{ name: 'main' }]; - jest.spyOn(Api, 'branches').mockResolvedValue({ data: mockBranches }); - - createComponent(optionsWithDefaultBranchName({ stubs }), { loading: false }); - await nextTick(); - expect(getBranchSuggestions()).toEqual(['main']); - }); - }); -}); diff --git a/spec/frontend/pipelines/tokens/pipeline_source_token_spec.js b/spec/frontend/pipelines/tokens/pipeline_source_token_spec.js deleted file mode 100644 index 60abb63a7e0..00000000000 --- a/spec/frontend/pipelines/tokens/pipeline_source_token_spec.js +++ /dev/null @@ -1,53 +0,0 @@ -import { GlFilteredSearchToken, GlFilteredSearchSuggestion } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; -import { PIPELINE_SOURCES } from 'ee_else_ce/pipelines/components/pipelines_list/tokens/constants'; -import { stubComponent } from 'helpers/stub_component'; -import PipelineSourceToken from '~/pipelines/components/pipelines_list/tokens/pipeline_source_token.vue'; - -describe('Pipeline Source Token', () => { - let wrapper; - - const findFilteredSearchToken = () => wrapper.findComponent(GlFilteredSearchToken); - const findAllFilteredSearchSuggestions = () => - wrapper.findAllComponents(GlFilteredSearchSuggestion); - - const defaultProps = { - config: { - type: 'source', - icon: 'trigger-source', - title: 'Source', - unique: true, - }, - value: { - data: '', - }, - cursorPosition: 'start', - }; - - const createComponent = () => { - wrapper = shallowMount(PipelineSourceToken, { - propsData: { - ...defaultProps, - }, - stubs: { - GlFilteredSearchToken: stubComponent(GlFilteredSearchToken, { - template: `<div><slot name="suggestions"></slot></div>`, - }), - }, - }); - }; - - beforeEach(() => { - createComponent(); - }); - - it('passes config correctly', () => { - expect(findFilteredSearchToken().props('config')).toEqual(defaultProps.config); - }); - - describe('shows sources correctly', () => { - it('renders all pipeline sources available', () => { - expect(findAllFilteredSearchSuggestions()).toHaveLength(PIPELINE_SOURCES.length); - }); - }); -}); diff --git a/spec/frontend/pipelines/tokens/pipeline_status_token_spec.js b/spec/frontend/pipelines/tokens/pipeline_status_token_spec.js deleted file mode 100644 index cf4ccb5ce43..00000000000 --- a/spec/frontend/pipelines/tokens/pipeline_status_token_spec.js +++ /dev/null @@ -1,58 +0,0 @@ -import { GlFilteredSearchToken, GlFilteredSearchSuggestion, GlIcon } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; -import { stubComponent } from 'helpers/stub_component'; -import PipelineStatusToken from '~/pipelines/components/pipelines_list/tokens/pipeline_status_token.vue'; -import { - TOKEN_TITLE_STATUS, - TOKEN_TYPE_STATUS, -} from '~/vue_shared/components/filtered_search_bar/constants'; - -describe('Pipeline Status Token', () => { - let wrapper; - - const findFilteredSearchToken = () => wrapper.findComponent(GlFilteredSearchToken); - const findAllFilteredSearchSuggestions = () => - wrapper.findAllComponents(GlFilteredSearchSuggestion); - const findAllGlIcons = () => wrapper.findAllComponents(GlIcon); - - const defaultProps = { - config: { - type: TOKEN_TYPE_STATUS, - icon: 'status', - title: TOKEN_TITLE_STATUS, - unique: true, - }, - value: { - data: '', - }, - cursorPosition: 'start', - }; - - const createComponent = () => { - wrapper = shallowMount(PipelineStatusToken, { - propsData: { - ...defaultProps, - }, - stubs: { - GlFilteredSearchToken: stubComponent(GlFilteredSearchToken, { - template: `<div><slot name="suggestions"></slot></div>`, - }), - }, - }); - }; - - beforeEach(() => { - createComponent(); - }); - - it('passes config correctly', () => { - expect(findFilteredSearchToken().props('config')).toEqual(defaultProps.config); - }); - - describe('shows statuses correctly', () => { - it('renders all pipeline statuses available', () => { - expect(findAllFilteredSearchSuggestions()).toHaveLength(wrapper.vm.statuses.length); - expect(findAllGlIcons()).toHaveLength(wrapper.vm.statuses.length); - }); - }); -}); diff --git a/spec/frontend/pipelines/tokens/pipeline_tag_name_token_spec.js b/spec/frontend/pipelines/tokens/pipeline_tag_name_token_spec.js deleted file mode 100644 index 88c88d8f16f..00000000000 --- a/spec/frontend/pipelines/tokens/pipeline_tag_name_token_spec.js +++ /dev/null @@ -1,95 +0,0 @@ -import { GlFilteredSearchToken, GlFilteredSearchSuggestion, GlLoadingIcon } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; -import Api from '~/api'; -import PipelineTagNameToken from '~/pipelines/components/pipelines_list/tokens/pipeline_tag_name_token.vue'; -import { tags, mockTagsAfterMap } from '../mock_data'; - -describe('Pipeline Branch Name Token', () => { - let wrapper; - - const findFilteredSearchToken = () => wrapper.findComponent(GlFilteredSearchToken); - const findAllFilteredSearchSuggestions = () => - wrapper.findAllComponents(GlFilteredSearchSuggestion); - const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); - - const stubs = { - GlFilteredSearchToken: { - template: `<div><slot name="suggestions"></slot></div>`, - }, - }; - - const defaultProps = { - config: { - type: 'tag', - icon: 'tag', - title: 'Tag name', - unique: true, - projectId: '21', - disabled: false, - }, - value: { - data: '', - }, - cursorPosition: 'start', - }; - - const createComponent = (options, data) => { - wrapper = shallowMount(PipelineTagNameToken, { - propsData: { - ...defaultProps, - }, - data() { - return { - ...data, - }; - }, - ...options, - }); - }; - - beforeEach(() => { - jest.spyOn(Api, 'tags').mockResolvedValue({ data: tags }); - - createComponent(); - }); - - it('passes config correctly', () => { - expect(findFilteredSearchToken().props('config')).toEqual(defaultProps.config); - }); - - it('fetches and sets project tags', () => { - expect(Api.tags).toHaveBeenCalled(); - - expect(wrapper.vm.tags).toEqual(mockTagsAfterMap); - expect(findLoadingIcon().exists()).toBe(false); - }); - - describe('displays loading icon correctly', () => { - it('shows loading icon', () => { - createComponent({ stubs }, { loading: true }); - - expect(findLoadingIcon().exists()).toBe(true); - }); - - it('does not show loading icon', () => { - createComponent({ stubs }, { loading: false }); - - expect(findLoadingIcon().exists()).toBe(false); - }); - }); - - describe('shows tags correctly', () => { - it('renders all tags', () => { - createComponent({ stubs }, { tags, loading: false }); - - expect(findAllFilteredSearchSuggestions()).toHaveLength(tags.length); - }); - - it('renders only the tag searched for', () => { - const mockTags = ['main-tag']; - createComponent({ stubs }, { tags: mockTags, loading: false }); - - expect(findAllFilteredSearchSuggestions()).toHaveLength(mockTags.length); - }); - }); -}); diff --git a/spec/frontend/pipelines/tokens/pipeline_trigger_author_token_spec.js b/spec/frontend/pipelines/tokens/pipeline_trigger_author_token_spec.js deleted file mode 100644 index e9ec684a350..00000000000 --- a/spec/frontend/pipelines/tokens/pipeline_trigger_author_token_spec.js +++ /dev/null @@ -1,99 +0,0 @@ -import { GlFilteredSearchToken, GlFilteredSearchSuggestion, GlLoadingIcon } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; -import { stubComponent } from 'helpers/stub_component'; -import Api from '~/api'; -import PipelineTriggerAuthorToken from '~/pipelines/components/pipelines_list/tokens/pipeline_trigger_author_token.vue'; -import { users } from '../mock_data'; - -describe('Pipeline Trigger Author Token', () => { - let wrapper; - - const findFilteredSearchToken = () => wrapper.findComponent(GlFilteredSearchToken); - const findAllFilteredSearchSuggestions = () => - wrapper.findAllComponents(GlFilteredSearchSuggestion); - const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); - - const defaultProps = { - config: { - type: 'username', - icon: 'user', - title: 'Trigger author', - dataType: 'username', - unique: true, - triggerAuthors: users, - }, - value: { - data: '', - }, - cursorPosition: 'start', - }; - - const createComponent = (data) => { - wrapper = shallowMount(PipelineTriggerAuthorToken, { - propsData: { - ...defaultProps, - }, - data() { - return { - ...data, - }; - }, - stubs: { - GlFilteredSearchToken: stubComponent(GlFilteredSearchToken, { - template: `<div><slot name="suggestions"></slot></div>`, - }), - }, - }); - }; - - beforeEach(() => { - jest.spyOn(Api, 'projectUsers').mockResolvedValue(users); - - createComponent(); - }); - - it('passes config correctly', () => { - expect(findFilteredSearchToken().props('config')).toEqual(defaultProps.config); - }); - - it('fetches and sets project users', () => { - expect(Api.projectUsers).toHaveBeenCalled(); - - expect(wrapper.vm.users).toEqual(users); - expect(findLoadingIcon().exists()).toBe(false); - }); - - describe('displays loading icon correctly', () => { - it('shows loading icon', () => { - createComponent({ loading: true }); - - expect(findLoadingIcon().exists()).toBe(true); - }); - - it('does not show loading icon', () => { - createComponent({ loading: false }); - - expect(findLoadingIcon().exists()).toBe(false); - }); - }); - - describe('shows trigger authors correctly', () => { - beforeEach(() => {}); - - it('renders all trigger authors', () => { - createComponent({ users, loading: false }); - - // should have length of all users plus the static 'Any' option - expect(findAllFilteredSearchSuggestions()).toHaveLength(users.length + 1); - }); - - it('renders only the trigger author searched for', () => { - createComponent({ - users: [{ name: 'Arnold', username: 'admin', state: 'active', avatar_url: 'avatar-link' }], - loading: false, - }); - - expect(findAllFilteredSearchSuggestions()).toHaveLength(2); - }); - }); -}); diff --git a/spec/frontend/pipelines/unwrapping_utils_spec.js b/spec/frontend/pipelines/unwrapping_utils_spec.js deleted file mode 100644 index a6ce7d4049f..00000000000 --- a/spec/frontend/pipelines/unwrapping_utils_spec.js +++ /dev/null @@ -1,127 +0,0 @@ -import { - unwrapGroups, - unwrapNodesWithName, - unwrapStagesWithNeeds, -} from '~/pipelines/components/unwrapping_utils'; - -const groupsArray = [ - { - name: 'build_a', - size: 1, - status: { - label: 'passed', - group: 'success', - icon: 'status_success', - }, - }, - { - name: 'bob_the_build', - size: 1, - status: { - label: 'passed', - group: 'success', - icon: 'status_success', - }, - }, -]; - -const basicStageInfo = { - name: 'center_stage', - status: { - action: null, - }, -}; - -const stagesAndGroups = [ - { - ...basicStageInfo, - groups: { - nodes: groupsArray, - }, - }, -]; - -const needArray = [ - { - name: 'build_b', - }, -]; - -const elephantArray = [ - { - name: 'build_b', - elephant: 'gray', - }, -]; - -const baseJobs = { - name: 'test_d', - status: { - icon: 'status_success', - tooltip: null, - hasDetails: true, - detailsPath: '/root/abcd-dag/-/pipelines/162', - group: 'success', - action: null, - }, -}; - -const jobArrayWithNeeds = [ - { - ...baseJobs, - needs: { - nodes: needArray, - }, - }, -]; - -const jobArrayWithElephant = [ - { - ...baseJobs, - needs: { - nodes: elephantArray, - }, - }, -]; - -const completeMock = [ - { - ...basicStageInfo, - groups: { - nodes: groupsArray.map((group) => ({ ...group, jobs: { nodes: jobArrayWithNeeds } })), - }, - }, -]; - -describe('Shared pipeline unwrapping utils', () => { - describe('unwrapGroups', () => { - it('takes stages without nodes and returns the unwrapped groups', () => { - expect(unwrapGroups(stagesAndGroups)[0].node.groups).toEqual(groupsArray); - }); - - it('keeps other stage properties intact', () => { - expect(unwrapGroups(stagesAndGroups)[0].node).toMatchObject(basicStageInfo); - }); - }); - - describe('unwrapNodesWithName', () => { - it('works with no field argument', () => { - expect(unwrapNodesWithName(jobArrayWithNeeds, 'needs')[0].needs).toEqual([needArray[0].name]); - }); - - it('works with custom field argument', () => { - expect(unwrapNodesWithName(jobArrayWithElephant, 'needs', 'elephant')[0].needs).toEqual([ - elephantArray[0].elephant, - ]); - }); - }); - - describe('unwrapStagesWithNeeds', () => { - it('removes nodes from groups, jobs, and needs', () => { - const firstProcessedGroup = unwrapStagesWithNeeds(completeMock)[0].groups[0]; - expect(firstProcessedGroup).toMatchObject(groupsArray[0]); - expect(firstProcessedGroup.jobs[0]).toMatchObject(baseJobs); - expect(firstProcessedGroup.jobs[0].needs[0]).toBe(needArray[0].name); - }); - }); -}); diff --git a/spec/frontend/pipelines/utils_spec.js b/spec/frontend/pipelines/utils_spec.js deleted file mode 100644 index 286d79edc6c..00000000000 --- a/spec/frontend/pipelines/utils_spec.js +++ /dev/null @@ -1,191 +0,0 @@ -import mockPipelineResponse from 'test_fixtures/pipelines/pipeline_details.json'; -import { createSankey } from '~/pipelines/components/dag/drawing_utils'; -import { - makeLinksFromNodes, - filterByAncestors, - generateColumnsFromLayersListBare, - keepLatestDownstreamPipelines, - listByLayers, - parseData, - removeOrphanNodes, - getMaxNodes, -} from '~/pipelines/components/parsing_utils'; -import { createNodeDict } from '~/pipelines/utils'; - -import { mockDownstreamPipelinesRest } from '../vue_merge_request_widget/mock_data'; -import { mockDownstreamPipelinesGraphql } from '../commit/mock_data'; -import { mockParsedGraphQLNodes, missingJob } from './components/dag/mock_data'; -import { generateResponse } from './graph/mock_data'; - -describe('DAG visualization parsing utilities', () => { - const nodeDict = createNodeDict(mockParsedGraphQLNodes); - const unfilteredLinks = makeLinksFromNodes(mockParsedGraphQLNodes, nodeDict); - const parsed = parseData(mockParsedGraphQLNodes); - - describe('makeLinksFromNodes', () => { - it('returns the expected link structure', () => { - expect(unfilteredLinks[0]).toHaveProperty('source', 'build_a'); - expect(unfilteredLinks[0]).toHaveProperty('target', 'test_a'); - expect(unfilteredLinks[0]).toHaveProperty('value', 10); - }); - - it('does not generate a link for non-existing jobs', () => { - const sources = unfilteredLinks.map(({ source }) => source); - - expect(sources.includes(missingJob)).toBe(false); - }); - }); - - describe('filterByAncestors', () => { - const allLinks = [ - { source: 'job1', target: 'job4' }, - { source: 'job1', target: 'job2' }, - { source: 'job2', target: 'job4' }, - ]; - - const dedupedLinks = [ - { source: 'job1', target: 'job2' }, - { source: 'job2', target: 'job4' }, - ]; - - const nodeLookup = { - job1: { - name: 'job1', - }, - job2: { - name: 'job2', - needs: ['job1'], - }, - job4: { - name: 'job4', - needs: ['job1', 'job2'], - category: 'build', - }, - }; - - it('dedupes links', () => { - expect(filterByAncestors(allLinks, nodeLookup)).toMatchObject(dedupedLinks); - }); - }); - - describe('parseData parent function', () => { - it('returns an object containing a list of nodes and links', () => { - // an array of nodes exist and the values are defined - expect(parsed).toHaveProperty('nodes'); - expect(Array.isArray(parsed.nodes)).toBe(true); - expect(parsed.nodes.filter(Boolean)).not.toHaveLength(0); - - // an array of links exist and the values are defined - expect(parsed).toHaveProperty('links'); - expect(Array.isArray(parsed.links)).toBe(true); - expect(parsed.links.filter(Boolean)).not.toHaveLength(0); - }); - }); - - describe('removeOrphanNodes', () => { - it('removes sankey nodes that have no needs and are not needed', () => { - const layoutSettings = { - width: 200, - height: 200, - nodeWidth: 10, - nodePadding: 20, - paddingForLabels: 100, - }; - - const sankeyLayout = createSankey(layoutSettings)(parsed); - const cleanedNodes = removeOrphanNodes(sankeyLayout.nodes); - /* - These lengths are determined by the mock data. - If the data changes, the numbers may also change. - */ - expect(parsed.nodes).toHaveLength(mockParsedGraphQLNodes.length); - expect(cleanedNodes).toHaveLength(12); - }); - }); - - describe('getMaxNodes', () => { - it('returns the number of nodes in the most populous generation', () => { - const layerNodes = [ - { layer: 0 }, - { layer: 0 }, - { layer: 1 }, - { layer: 1 }, - { layer: 0 }, - { layer: 3 }, - { layer: 2 }, - { layer: 4 }, - { layer: 1 }, - { layer: 3 }, - { layer: 4 }, - ]; - expect(getMaxNodes(layerNodes)).toBe(3); - }); - }); - - describe('generateColumnsFromLayersList', () => { - const pipeline = generateResponse(mockPipelineResponse, 'root/fungi-xoxo'); - const { pipelineLayers } = listByLayers(pipeline); - const columns = generateColumnsFromLayersListBare(pipeline, pipelineLayers); - - it('returns stage-like objects with default name, id, and status', () => { - columns.forEach((col, idx) => { - expect(col).toMatchObject({ - name: '', - status: { action: null }, - id: `layer-${idx}`, - }); - }); - }); - - it('creates groups that match the list created in listByLayers', () => { - columns.forEach((col, idx) => { - const groupNames = col.groups.map(({ name }) => name); - expect(groupNames).toEqual(pipelineLayers[idx]); - }); - }); - - it('looks up the correct group object', () => { - columns.forEach((col) => { - col.groups.forEach((group) => { - const groupStage = pipeline.stages.find((el) => el.name === group.stageName); - const groupObject = groupStage.groups.find((el) => el.name === group.name); - expect(group).toBe(groupObject); - }); - }); - }); - }); -}); - -describe('linked pipeline utilities', () => { - describe('keepLatestDownstreamPipelines', () => { - it('filters data from GraphQL', () => { - const downstream = mockDownstreamPipelinesGraphql().nodes; - const latestDownstream = keepLatestDownstreamPipelines(downstream); - - expect(downstream).toHaveLength(3); - expect(latestDownstream).toHaveLength(1); - }); - - it('filters data from REST', () => { - const downstream = mockDownstreamPipelinesRest(); - const latestDownstream = keepLatestDownstreamPipelines(downstream); - - expect(downstream).toHaveLength(2); - expect(latestDownstream).toHaveLength(1); - }); - - it('returns downstream pipelines if sourceJob.retried is null', () => { - const downstream = mockDownstreamPipelinesGraphql({ includeSourceJobRetried: false }).nodes; - const latestDownstream = keepLatestDownstreamPipelines(downstream); - - expect(latestDownstream).toHaveLength(downstream.length); - }); - - it('returns downstream pipelines if source_job.retried is null', () => { - const downstream = mockDownstreamPipelinesRest({ includeSourceJobRetried: false }); - const latestDownstream = keepLatestDownstreamPipelines(downstream); - - expect(latestDownstream).toHaveLength(downstream.length); - }); - }); -}); |