diff options
Diffstat (limited to 'spec/frontend/pipelines')
31 files changed, 691 insertions, 184 deletions
diff --git a/spec/frontend/pipelines/blank_state_spec.js b/spec/frontend/pipelines/blank_state_spec.js index 033bd5ccb73..bb069fdc2c8 100644 --- a/spec/frontend/pipelines/blank_state_spec.js +++ b/spec/frontend/pipelines/blank_state_spec.js @@ -1,5 +1,5 @@ import Vue from 'vue'; -import component from '~/pipelines/components/blank_state.vue'; +import component from '~/pipelines/components/pipelines_list/blank_state.vue'; import mountComponent from '../helpers/vue_mount_component_helper'; describe('Pipelines Blank State', () => { 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 index 629efc6d3fa..cb5f6ff5307 100644 --- a/spec/frontend/pipelines/components/dag/__snapshots__/dag_graph_spec.js.snap +++ b/spec/frontend/pipelines/components/dag/__snapshots__/dag_graph_spec.js.snap @@ -3,7 +3,7 @@ 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-cursor-pointer\\"> + <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> @@ -20,7 +20,7 @@ exports[`The DAG graph in the basic case renders the graph svg 1`] = ` </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-cursor-pointer\\"> + <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> @@ -37,7 +37,7 @@ exports[`The DAG graph in the basic case renders the graph svg 1`] = ` </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-cursor-pointer\\"> + <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> @@ -54,7 +54,7 @@ exports[`The DAG graph in the basic case renders the graph svg 1`] = ` </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-cursor-pointer\\"> + <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> @@ -71,7 +71,7 @@ exports[`The DAG graph in the basic case renders the graph svg 1`] = ` </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-cursor-pointer\\"> + <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> @@ -88,7 +88,7 @@ exports[`The DAG graph in the basic case renders the graph svg 1`] = ` </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-cursor-pointer\\"> + <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> @@ -105,7 +105,7 @@ exports[`The DAG graph in the basic case renders the graph svg 1`] = ` </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-cursor-pointer\\"> + <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> @@ -122,7 +122,7 @@ exports[`The DAG graph in the basic case renders the graph svg 1`] = ` </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-cursor-pointer\\"> + <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> @@ -139,7 +139,7 @@ exports[`The DAG graph in the basic case renders the graph svg 1`] = ` </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-cursor-pointer\\"> + <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> @@ -156,7 +156,7 @@ exports[`The DAG graph in the basic case renders the graph svg 1`] = ` </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-cursor-pointer\\"> + <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> @@ -175,18 +175,18 @@ exports[`The DAG graph in the basic case renders the graph svg 1`] = ` </g> </g> <g> - <line class=\\"dag-node gl-cursor-pointer\\" 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-cursor-pointer\\" 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-cursor-pointer\\" 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-cursor-pointer\\" 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-cursor-pointer\\" 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-cursor-pointer\\" 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-cursor-pointer\\" 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-cursor-pointer\\" 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-cursor-pointer\\" 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-cursor-pointer\\" 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-cursor-pointer\\" 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-cursor-pointer\\" id=\\"dag-node84\\" stroke=\\"#275600\\" stroke-width=\\"16\\" stroke-linecap=\\"round\\" x1=\\"892\\" x2=\\"892\\" 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-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\\"> diff --git a/spec/frontend/pipelines/components/dag/dag_annotations_spec.js b/spec/frontend/pipelines/components/dag/dag_annotations_spec.js new file mode 100644 index 00000000000..5747c91bee8 --- /dev/null +++ b/spec/frontend/pipelines/components/dag/dag_annotations_spec.js @@ -0,0 +1,112 @@ +import { shallowMount, mount } from '@vue/test-utils'; +import { GlButton } from '@gitlab/ui'; +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.find(GlButton); + + const createComponent = (propsData = {}, method = shallowMount) => { + if (wrapper?.destroy) { + wrapper.destroy(); + } + + wrapper = method(DagAnnotations, { + propsData, + data() { + return { + showList: true, + }; + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + 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', () => { + expect(getAllTextBlocks().length).toBe(Object.keys(multiNote).length); + expect(getToggleButton().text()).toBe('Hide list'); + getToggleButton().trigger('click'); + return wrapper.vm.$nextTick().then(() => { + expect(getAllTextBlocks().length).toBe(0); + expect(getToggleButton().text()).toBe('Show list'); + }); + }); + }); + + describe('clicking show', () => { + it('shows listed items and changes text to hide', () => { + getToggleButton().trigger('click'); + getToggleButton().trigger('click'); + + return wrapper.vm.$nextTick().then(() => { + 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 index 017461dfb84..e312791b01f 100644 --- a/spec/frontend/pipelines/components/dag/dag_graph_spec.js +++ b/spec/frontend/pipelines/components/dag/dag_graph_spec.js @@ -1,4 +1,4 @@ -import { mount } from '@vue/test-utils'; +import { shallowMount } from '@vue/test-utils'; import DagGraph from '~/pipelines/components/dag/dag_graph.vue'; import { IS_HIGHLIGHTED, LINK_SELECTOR, NODE_SELECTOR } from '~/pipelines/components/dag/constants'; import { highlightIn, highlightOut } from '~/pipelines/components/dag/interactions'; @@ -19,7 +19,7 @@ describe('The DAG graph', () => { wrapper.destroy(); } - wrapper = mount(DagGraph, { + wrapper = shallowMount(DagGraph, { attachToDocument: true, propsData, data() { diff --git a/spec/frontend/pipelines/components/dag/dag_spec.js b/spec/frontend/pipelines/components/dag/dag_spec.js index 666b4cfaa2f..7dea6d819b9 100644 --- a/spec/frontend/pipelines/components/dag/dag_spec.js +++ b/spec/frontend/pipelines/components/dag/dag_spec.js @@ -2,17 +2,28 @@ import { mount, shallowMount } from '@vue/test-utils'; import MockAdapter from 'axios-mock-adapter'; import axios from '~/lib/utils/axios_utils'; import waitForPromises from 'helpers/wait_for_promises'; -import { GlAlert } from '@gitlab/ui'; +import { GlAlert, GlEmptyState } from '@gitlab/ui'; import Dag from '~/pipelines/components/dag/dag.vue'; import DagGraph from '~/pipelines/components/dag/dag_graph.vue'; +import DagAnnotations from '~/pipelines/components/dag/dag_annotations.vue'; import { + ADD_NOTE, + REMOVE_NOTE, + REPLACE_NOTES, DEFAULT, PARSE_FAILURE, LOAD_FAILURE, UNSUPPORTED_DATA, } from '~/pipelines/components/dag//constants'; -import { mockBaseData, tooSmallGraph, unparseableGraph } from './mock_data'; +import { + mockBaseData, + tooSmallGraph, + unparseableGraph, + graphWithoutDependencies, + singleNote, + multiNote, +} from './mock_data'; describe('Pipeline DAG graph wrapper', () => { let wrapper; @@ -20,7 +31,9 @@ describe('Pipeline DAG graph wrapper', () => { const getAlert = () => wrapper.find(GlAlert); const getAllAlerts = () => wrapper.findAll(GlAlert); const getGraph = () => wrapper.find(DagGraph); + const getNotes = () => wrapper.find(DagAnnotations); const getErrorText = type => wrapper.vm.$options.errorTexts[type]; + const getEmptyState = () => wrapper.find(GlEmptyState); const dataPath = '/root/test/pipelines/90/dag.json'; @@ -30,7 +43,11 @@ describe('Pipeline DAG graph wrapper', () => { } wrapper = method(Dag, { - propsData, + propsData: { + emptySvgPath: '/my-svg', + dagDocPath: '/my-doc', + ...propsData, + }, data() { return { showFailureAlert: false, @@ -59,79 +76,153 @@ describe('Pipeline DAG graph wrapper', () => { expect(getAlert().text()).toBe(getErrorText(DEFAULT)); expect(getGraph().exists()).toBe(false); }); + + it('does not render the empty state', () => { + expect(getEmptyState().exists()).toBe(false); + }); }); describe('when there is a dataUrl', () => { describe('but the data fetch fails', () => { - beforeEach(() => { + beforeEach(async () => { mock.onGet(dataPath).replyOnce(500); createComponent({ graphUrl: dataPath }); + + await wrapper.vm.$nextTick(); + + return waitForPromises(); }); it('shows the LOAD_FAILURE alert and not the graph', () => { - return wrapper.vm - .$nextTick() - .then(waitForPromises) - .then(() => { - expect(getAlert().exists()).toBe(true); - expect(getAlert().text()).toBe(getErrorText(LOAD_FAILURE)); - expect(getGraph().exists()).toBe(false); - }); + expect(getAlert().exists()).toBe(true); + expect(getAlert().text()).toBe(getErrorText(LOAD_FAILURE)); + expect(getGraph().exists()).toBe(false); + }); + + it('does not render the empty state', () => { + expect(getEmptyState().exists()).toBe(false); }); }); describe('the data fetch succeeds but the parse fails', () => { - beforeEach(() => { + beforeEach(async () => { mock.onGet(dataPath).replyOnce(200, unparseableGraph); createComponent({ graphUrl: dataPath }); + + await wrapper.vm.$nextTick(); + + return waitForPromises(); }); it('shows the PARSE_FAILURE alert and not the graph', () => { - return wrapper.vm - .$nextTick() - .then(waitForPromises) - .then(() => { - expect(getAlert().exists()).toBe(true); - expect(getAlert().text()).toBe(getErrorText(PARSE_FAILURE)); - expect(getGraph().exists()).toBe(false); - }); + 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('and the data fetch and parse succeeds', () => { - beforeEach(() => { + beforeEach(async () => { mock.onGet(dataPath).replyOnce(200, mockBaseData); createComponent({ graphUrl: dataPath }, mount); + + await wrapper.vm.$nextTick(); + + return waitForPromises(); }); - it('shows the graph and not the beta alert', () => { - return wrapper.vm - .$nextTick() - .then(waitForPromises) - .then(() => { - expect(getAllAlerts().length).toBe(1); - expect(getAlert().text()).toContain('This feature is currently in beta.'); - expect(getGraph().exists()).toBe(true); - }); + it('shows the graph and the beta alert', () => { + expect(getAllAlerts().length).toBe(1); + expect(getAlert().text()).toContain('This feature is currently in beta.'); + expect(getGraph().exists()).toBe(true); + }); + + it('does not render the empty state', () => { + expect(getEmptyState().exists()).toBe(false); }); }); describe('the data fetch and parse succeeds, but the resulting graph is too small', () => { - beforeEach(() => { + beforeEach(async () => { mock.onGet(dataPath).replyOnce(200, tooSmallGraph); createComponent({ graphUrl: dataPath }); + + await wrapper.vm.$nextTick(); + + return waitForPromises(); }); it('shows the UNSUPPORTED_DATA alert and not the graph', () => { - return wrapper.vm - .$nextTick() - .then(waitForPromises) - .then(() => { - expect(getAlert().exists()).toBe(true); - expect(getAlert().text()).toBe(getErrorText(UNSUPPORTED_DATA)); - expect(getGraph().exists()).toBe(false); - }); + 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 data fetch succeeds but the returned data is empty', () => { + beforeEach(async () => { + mock.onGet(dataPath).replyOnce(200, graphWithoutDependencies); + createComponent({ graphUrl: dataPath }, mount); + + await wrapper.vm.$nextTick(); + + return waitForPromises(); + }); + + it('does not render an error alert or the graph', () => { + expect(getAllAlerts().length).toBe(1); + expect(getAlert().text()).toContain('This feature is currently in beta.'); + expect(getGraph().exists()).toBe(false); }); + + it('shows the empty dag graph state', () => { + expect(getEmptyState().exists()).toBe(true); + }); + }); + }); + + describe('annotations', () => { + beforeEach(async () => { + mock.onGet(dataPath).replyOnce(200, mockBaseData); + createComponent({ graphUrl: dataPath }, mount); + + await wrapper.vm.$nextTick(); + + return waitForPromises(); + }); + + 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 wrapper.vm.$nextTick(); + expect(getNotes().exists()).toBe(true); + + getGraph().vm.$emit('update-annotation', { type: REMOVE_NOTE, data: currentNote }); + await wrapper.vm.$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 wrapper.vm.$nextTick(); + expect(getNotes().exists()).toBe(true); + + getGraph().vm.$emit('update-annotation', { type: REPLACE_NOTES, data: {} }); + await wrapper.vm.$nextTick(); + expect(getNotes().exists()).toBe(false); }); }); }); diff --git a/spec/frontend/pipelines/components/dag/mock_data.js b/spec/frontend/pipelines/components/dag/mock_data.js index 5de8697170a..3b39b9cd21c 100644 --- a/spec/frontend/pipelines/components/dag/mock_data.js +++ b/spec/frontend/pipelines/components/dag/mock_data.js @@ -83,6 +83,46 @@ export const tooSmallGraph = { ], }; +export const graphWithoutDependencies = { + stages: [ + { + name: 'test', + groups: [ + { + name: 'jest', + size: 2, + jobs: [{ name: 'jest 1/2' }, { name: 'jest 2/2' }], + }, + { + name: 'rspec', + size: 1, + jobs: [{ name: 'rspec' }], + }, + ], + }, + { + name: 'fixtures', + groups: [ + { + name: 'frontend fixtures', + size: 1, + jobs: [{ name: 'frontend fixtures' }], + }, + ], + }, + { + name: 'un-needed', + groups: [ + { + name: 'un-needed', + size: 1, + jobs: [{ name: 'un-needed' }], + }, + ], + }, + ], +}; + export const unparseableGraph = [ { name: 'test', @@ -388,3 +428,43 @@ export const parsedData = { }, ], }; + +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', + }, + }, +}; diff --git a/spec/frontend/pipelines/components/pipelines_filtered_search_spec.js b/spec/frontend/pipelines/components/pipelines_filtered_search_spec.js index bdc807fcbfe..add7b56845e 100644 --- a/spec/frontend/pipelines/components/pipelines_filtered_search_spec.js +++ b/spec/frontend/pipelines/components/pipelines_filtered_search_spec.js @@ -2,7 +2,7 @@ import Api from '~/api'; import { mount } from '@vue/test-utils'; import MockAdapter from 'axios-mock-adapter'; import axios from '~/lib/utils/axios_utils'; -import PipelinesFilteredSearch from '~/pipelines/components/pipelines_filtered_search.vue'; +import PipelinesFilteredSearch from '~/pipelines/components/pipelines_list/pipelines_filtered_search.vue'; import { users, mockSearch, branches, tags } from '../mock_data'; import { GlFilteredSearch } from '@gitlab/ui'; diff --git a/spec/frontend/pipelines/empty_state_spec.js b/spec/frontend/pipelines/empty_state_spec.js index f12950b8fce..79356664834 100644 --- a/spec/frontend/pipelines/empty_state_spec.js +++ b/spec/frontend/pipelines/empty_state_spec.js @@ -1,5 +1,5 @@ import Vue from 'vue'; -import emptyStateComp from '~/pipelines/components/empty_state.vue'; +import emptyStateComp from '~/pipelines/components/pipelines_list/empty_state.vue'; import mountComponent from '../helpers/vue_mount_component_helper'; describe('Pipelines Empty State', () => { diff --git a/spec/frontend/pipelines/graph/job_item_spec.js b/spec/frontend/pipelines/graph/job_item_spec.js index da777466e3e..2c5e7a1f6e9 100644 --- a/spec/frontend/pipelines/graph/job_item_spec.js +++ b/spec/frontend/pipelines/graph/job_item_spec.js @@ -5,6 +5,8 @@ import JobItem from '~/pipelines/components/graph/job_item.vue'; describe('pipeline graph job item', () => { let wrapper; + const findJobWithoutLink = () => wrapper.find('[data-testid="job-without-link"]'); + const createWrapper = propsData => { wrapper = mount(JobItem, { propsData, @@ -57,7 +59,7 @@ describe('pipeline graph job item', () => { }); describe('name without link', () => { - it('it should render status and name', () => { + beforeEach(() => { createWrapper({ job: { id: 4257, @@ -71,13 +73,22 @@ describe('pipeline graph job item', () => { has_details: false, }, }, + cssClassJobName: 'css-class-job-name', + jobHovered: 'test', }); + }); + it('it should render status and name', () => { expect(wrapper.find('.ci-status-icon-success').exists()).toBe(true); expect(wrapper.find('a').exists()).toBe(false); expect(trimText(wrapper.find('.ci-status-text').text())).toEqual(mockJob.name); }); + + it('should apply hover class and provided class name', () => { + expect(findJobWithoutLink().classes()).toContain('gl-inset-border-1-blue-500'); + expect(findJobWithoutLink().classes()).toContain('css-class-job-name'); + }); }); describe('action icon', () => { diff --git a/spec/frontend/pipelines/graph/linked_pipeline_spec.js b/spec/frontend/pipelines/graph/linked_pipeline_spec.js index cf78aa3ef71..133d5695afb 100644 --- a/spec/frontend/pipelines/graph/linked_pipeline_spec.js +++ b/spec/frontend/pipelines/graph/linked_pipeline_spec.js @@ -11,7 +11,10 @@ const invalidTriggeredPipelineId = mockPipeline.project.id + 5; describe('Linked pipeline', () => { let wrapper; + const findButton = () => wrapper.find('button'); + const findPipelineLabel = () => wrapper.find('[data-testid="downstream-pipeline-label"]'); + const findLinkedPipeline = () => wrapper.find({ ref: 'linkedPipeline' }); const createWrapper = propsData => { wrapper = mount(LinkedPipelineComponent, { @@ -69,6 +72,8 @@ describe('Linked pipeline', () => { it('should correctly compute the tooltip text', () => { expect(wrapper.vm.tooltipText).toContain(mockPipeline.project.name); expect(wrapper.vm.tooltipText).toContain(mockPipeline.details.status.label); + expect(wrapper.vm.tooltipText).toContain(mockPipeline.source_job.name); + expect(wrapper.vm.tooltipText).toContain(mockPipeline.id); }); it('should render the tooltip text as the title attribute', () => { @@ -83,9 +88,8 @@ describe('Linked pipeline', () => { expect(wrapper.find('.js-linked-pipeline-loading').exists()).toBe(false); }); - it('should not display child label when pipeline project id is not the same as triggered pipeline project id', () => { - const labelContainer = wrapper.find('.parent-child-label-container'); - expect(labelContainer.exists()).toBe(false); + 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'); }); }); @@ -103,17 +107,17 @@ describe('Linked pipeline', () => { it('parent/child label container should exist', () => { createWrapper(downstreamProps); - expect(wrapper.find('.parent-child-label-container').exists()).toBe(true); + expect(findPipelineLabel().exists()).toBe(true); }); it('should display child label when pipeline project id is the same as triggered pipeline project id', () => { createWrapper(downstreamProps); - expect(wrapper.find('.parent-child-label-container').text()).toContain('Child'); + expect(findPipelineLabel().exists()).toBe(true); }); it('should display parent label when pipeline project id is the same as triggered_by pipeline project id', () => { createWrapper(upstreamProps); - expect(wrapper.find('.parent-child-label-container').text()).toContain('Parent'); + expect(findPipelineLabel().exists()).toBe(true); }); }); @@ -133,7 +137,7 @@ describe('Linked pipeline', () => { }); }); - describe('on click', () => { + describe('on click/hover', () => { const props = { pipeline: mockPipeline, projectId: validTriggeredPipelineId, @@ -160,5 +164,15 @@ describe('Linked pipeline', () => { 'js-linked-pipeline-34993051', ]); }); + + it('should emit downstreamHovered with job name on mouseover', () => { + findLinkedPipeline().trigger('mouseover'); + expect(wrapper.emitted().downstreamHovered).toStrictEqual([['trigger_job']]); + }); + + it('should emit downstreamHovered with empty string on mouseleave', () => { + findLinkedPipeline().trigger('mouseleave'); + expect(wrapper.emitted().downstreamHovered).toStrictEqual([['']]); + }); }); }); diff --git a/spec/frontend/pipelines/graph/linked_pipelines_mock_data.js b/spec/frontend/pipelines/graph/linked_pipelines_mock_data.js index 3e9c0814403..5756a666ff3 100644 --- a/spec/frontend/pipelines/graph/linked_pipelines_mock_data.js +++ b/spec/frontend/pipelines/graph/linked_pipelines_mock_data.js @@ -14,6 +14,9 @@ export default { active: false, coverage: null, source: 'push', + source_job: { + name: 'trigger_job', + }, created_at: '2018-06-05T11:31:30.452Z', updated_at: '2018-10-31T16:35:31.305Z', path: '/gitlab-org/gitlab-runner/pipelines/23211253', @@ -381,6 +384,9 @@ export default { active: false, coverage: null, source: 'pipeline', + source_job: { + name: 'trigger_job', + }, path: '/gitlab-com/gitlab-docs/pipelines/34993051', details: { status: { @@ -889,6 +895,9 @@ export default { active: false, coverage: null, source: 'pipeline', + source_job: { + name: 'trigger_job', + }, path: '/gitlab-com/gitlab-docs/pipelines/34993051', details: { status: { @@ -1402,6 +1411,9 @@ export default { active: false, coverage: null, source: 'pipeline', + source_job: { + name: 'trigger_job', + }, path: '/gitlab-com/gitlab-docs/pipelines/34993051', details: { status: { @@ -1912,6 +1924,9 @@ export default { active: false, coverage: null, source: 'pipeline', + source_job: { + name: 'trigger_job', + }, path: '/gitlab-com/gitlab-docs/pipelines/34993051', details: { status: { @@ -2412,6 +2427,9 @@ export default { active: false, coverage: null, source: 'push', + source_job: { + name: 'trigger_job', + }, created_at: '2019-01-06T17:48:37.599Z', updated_at: '2019-01-06T17:48:38.371Z', path: '/h5bp/html5-boilerplate/pipelines/26', @@ -3743,6 +3761,9 @@ export default { active: false, coverage: null, source: 'push', + source_job: { + name: 'trigger_job', + }, path: '/gitlab-org/gitlab-test/pipelines/4', details: { status: { diff --git a/spec/frontend/pipelines/nav_controls_spec.js b/spec/frontend/pipelines/nav_controls_spec.js index 6d28da0ea2a..139d53881c8 100644 --- a/spec/frontend/pipelines/nav_controls_spec.js +++ b/spec/frontend/pipelines/nav_controls_spec.js @@ -1,5 +1,5 @@ import Vue from 'vue'; -import navControlsComp from '~/pipelines/components/nav_controls.vue'; +import navControlsComp from '~/pipelines/components/pipelines_list/nav_controls.vue'; import mountComponent from '../helpers/vue_mount_component_helper'; describe('Pipelines Nav Controls', () => { diff --git a/spec/frontend/pipelines/pipeline_triggerer_spec.js b/spec/frontend/pipelines/pipeline_triggerer_spec.js index a8eec274487..6fd9a143d82 100644 --- a/spec/frontend/pipelines/pipeline_triggerer_spec.js +++ b/spec/frontend/pipelines/pipeline_triggerer_spec.js @@ -1,6 +1,6 @@ import { shallowMount } from '@vue/test-utils'; import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue'; -import pipelineTriggerer from '~/pipelines/components/pipeline_triggerer.vue'; +import pipelineTriggerer from '~/pipelines/components/pipelines_list/pipeline_triggerer.vue'; describe('Pipelines Triggerer', () => { let wrapper; diff --git a/spec/frontend/pipelines/pipeline_url_spec.js b/spec/frontend/pipelines/pipeline_url_spec.js index 70b94f2c8e1..0bcc3f96f7c 100644 --- a/spec/frontend/pipelines/pipeline_url_spec.js +++ b/spec/frontend/pipelines/pipeline_url_spec.js @@ -1,108 +1,140 @@ import $ from 'jquery'; import { trimText } from 'helpers/text_helper'; import { shallowMount } from '@vue/test-utils'; -import PipelineUrlComponent from '~/pipelines/components/pipeline_url.vue'; +import PipelineUrlComponent from '~/pipelines/components/pipelines_list/pipeline_url.vue'; $.fn.popover = () => {}; describe('Pipeline Url Component', () => { let wrapper; + const findPipelineUrlLink = () => wrapper.find('[data-testid="pipeline-url-link"]'); + const findScheduledTag = () => wrapper.find('[data-testid="pipeline-url-scheduled"]'); + const findLatestTag = () => wrapper.find('[data-testid="pipeline-url-latest"]'); + const findYamlTag = () => wrapper.find('[data-testid="pipeline-url-yaml"]'); + const findFailureTag = () => wrapper.find('[data-testid="pipeline-url-failure"]'); + const findAutoDevopsTag = () => wrapper.find('[data-testid="pipeline-url-autodevops"]'); + const findStuckTag = () => wrapper.find('[data-testid="pipeline-url-stuck"]'); + const findDetachedTag = () => wrapper.find('[data-testid="pipeline-url-detached"]'); + + const defaultProps = { + pipeline: { + id: 1, + path: 'foo', + flags: {}, + }, + autoDevopsHelpPath: 'foo', + pipelineScheduleUrl: 'foo', + }; + const createComponent = props => { wrapper = shallowMount(PipelineUrlComponent, { - propsData: props, + propsData: { ...defaultProps, ...props }, }); }; afterEach(() => { wrapper.destroy(); + wrapper = null; }); it('should render a table cell', () => { + createComponent(); + + expect(wrapper.attributes('class')).toContain('table-section'); + }); + + 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 stuck tag when flag is provided', () => { createComponent({ pipeline: { - id: 1, - path: 'foo', - flags: {}, + flags: { + stuck: true, + }, }, - autoDevopsHelpPath: 'foo', }); - expect(wrapper.attributes('class')).toContain('table-section'); + expect(findStuckTag().text()).toContain('stuck'); }); - it('should render a link the provided path and id', () => { + it('should render latest tag when flag is provided', () => { createComponent({ pipeline: { - id: 1, - path: 'foo', - flags: {}, + flags: { + latest: true, + }, }, - autoDevopsHelpPath: 'foo', }); - expect(wrapper.find('.js-pipeline-url-link').attributes('href')).toBe('foo'); - - expect(wrapper.find('.js-pipeline-url-link span').text()).toBe('#1'); + expect(findLatestTag().text()).toContain('latest'); }); - it('should render latest, yaml invalid, merge request, and stuck flags when provided', () => { + it('should render a yaml badge when it is invalid', () => { createComponent({ pipeline: { - id: 1, - path: 'foo', flags: { - latest: true, yaml_errors: true, - stuck: true, - merge_request_pipeline: true, - detached_merge_request_pipeline: true, }, }, - autoDevopsHelpPath: 'foo', }); - expect(wrapper.find('.js-pipeline-url-latest').text()).toContain('latest'); - - expect(wrapper.find('.js-pipeline-url-yaml').text()).toContain('yaml invalid'); + expect(findYamlTag().text()).toContain('yaml invalid'); + }); - expect(wrapper.find('.js-pipeline-url-stuck').text()).toContain('stuck'); + it('should render an autodevops badge when flag is provided', () => { + createComponent({ + pipeline: { + flags: { + auto_devops: true, + }, + }, + }); - expect(wrapper.find('.js-pipeline-url-detached').text()).toContain('detached'); + expect(trimText(findAutoDevopsTag().text())).toBe('Auto DevOps'); }); - it('should render a badge for autodevops', () => { + it('should render a detached badge when flag is provided', () => { createComponent({ pipeline: { - id: 1, - path: 'foo', flags: { - latest: true, - yaml_errors: true, - stuck: true, - auto_devops: true, + detached_merge_request_pipeline: true, }, }, - autoDevopsHelpPath: 'foo', }); - expect(trimText(wrapper.find('.js-pipeline-url-autodevops').text())).toEqual('Auto DevOps'); + expect(findDetachedTag().text()).toContain('detached'); }); it('should render error badge when pipeline has a failure reason set', () => { createComponent({ pipeline: { - id: 1, - path: 'foo', flags: { failure_reason: true, }, failure_reason: 'some reason', }, - autoDevopsHelpPath: 'foo', }); - expect(wrapper.find('.js-pipeline-url-failure').text()).toContain('error'); - expect(wrapper.find('.js-pipeline-url-failure').attributes('title')).toContain('some reason'); + expect(findFailureTag().text()).toContain('error'); + expect(findFailureTag().attributes('title')).toContain('some reason'); + }); + + it('should render scheduled badge when pipeline was triggered by a schedule', () => { + createComponent({ + pipeline: { + flags: {}, + source: 'schedule', + }, + }); + + expect(findScheduledTag().exists()).toBe(true); + expect(findScheduledTag().text()).toContain('Scheduled'); }); }); diff --git a/spec/frontend/pipelines/pipelines_actions_spec.js b/spec/frontend/pipelines/pipelines_actions_spec.js index 5e8d21660de..aef54d94974 100644 --- a/spec/frontend/pipelines/pipelines_actions_spec.js +++ b/spec/frontend/pipelines/pipelines_actions_spec.js @@ -2,7 +2,7 @@ import { shallowMount } from '@vue/test-utils'; import MockAdapter from 'axios-mock-adapter'; import { TEST_HOST } from 'spec/test_constants'; import axios from '~/lib/utils/axios_utils'; -import PipelinesActions from '~/pipelines/components/pipelines_actions.vue'; +import PipelinesActions from '~/pipelines/components/pipelines_list/pipelines_actions.vue'; import { GlDeprecatedButton } from '@gitlab/ui'; import GlCountdown from '~/vue_shared/components/gl_countdown.vue'; import waitForPromises from 'helpers/wait_for_promises'; diff --git a/spec/frontend/pipelines/pipelines_artifacts_spec.js b/spec/frontend/pipelines/pipelines_artifacts_spec.js index a93cc8a62ab..512205c3fc3 100644 --- a/spec/frontend/pipelines/pipelines_artifacts_spec.js +++ b/spec/frontend/pipelines/pipelines_artifacts_spec.js @@ -1,5 +1,5 @@ import { shallowMount } from '@vue/test-utils'; -import PipelineArtifacts from '~/pipelines/components/pipelines_artifacts.vue'; +import PipelineArtifacts from '~/pipelines/components/pipelines_list/pipelines_artifacts.vue'; import { GlLink } from '@gitlab/ui'; describe('Pipelines Artifacts dropdown', () => { diff --git a/spec/frontend/pipelines/pipelines_spec.js b/spec/frontend/pipelines/pipelines_spec.js index 0eeaef01a2d..66446b9aa1d 100644 --- a/spec/frontend/pipelines/pipelines_spec.js +++ b/spec/frontend/pipelines/pipelines_spec.js @@ -3,7 +3,7 @@ import { mount } from '@vue/test-utils'; import MockAdapter from 'axios-mock-adapter'; import axios from '~/lib/utils/axios_utils'; import waitForPromises from 'helpers/wait_for_promises'; -import PipelinesComponent from '~/pipelines/components/pipelines.vue'; +import PipelinesComponent from '~/pipelines/components/pipelines_list/pipelines.vue'; import Store from '~/pipelines/stores/pipelines_store'; import { pipelineWithStages, stageReply, users, mockSearch, branches } from './mock_data'; import { RAW_TEXT_WARNING } from '~/pipelines/constants'; @@ -343,12 +343,8 @@ describe('Pipelines', () => { }); it('should render navigation tabs', () => { - expect(wrapper.find('.js-pipelines-tab-pending').text()).toContain('Pending'); - expect(wrapper.find('.js-pipelines-tab-all').text()).toContain('All'); - expect(wrapper.find('.js-pipelines-tab-running').text()).toContain('Running'); - expect(wrapper.find('.js-pipelines-tab-finished').text()).toContain('Finished'); expect(wrapper.find('.js-pipelines-tab-branches').text()).toContain('Branches'); @@ -452,8 +448,6 @@ describe('Pipelines', () => { it('returns default tabs', () => { expect(wrapper.vm.tabs).toEqual([ { name: 'All', scope: 'all', count: undefined, isActive: true }, - { name: 'Pending', scope: 'pending', count: undefined, isActive: false }, - { name: 'Running', scope: 'running', count: undefined, isActive: false }, { name: 'Finished', scope: 'finished', count: undefined, isActive: false }, { name: 'Branches', scope: 'branches', isActive: false }, { name: 'Tags', scope: 'tags', isActive: false }, @@ -462,11 +456,11 @@ describe('Pipelines', () => { }); describe('emptyTabMessage', () => { - it('returns message with scope', () => { - wrapper.vm.scope = 'pending'; + it('returns message with finished scope', () => { + wrapper.vm.scope = 'finished'; return wrapper.vm.$nextTick().then(() => { - expect(wrapper.vm.emptyTabMessage).toEqual('There are currently no pending pipelines.'); + expect(wrapper.vm.emptyTabMessage).toEqual('There are currently no finished pipelines.'); }); }); diff --git a/spec/frontend/pipelines/pipelines_table_row_spec.js b/spec/frontend/pipelines/pipelines_table_row_spec.js index 3d564c8758c..9901f476f1b 100644 --- a/spec/frontend/pipelines/pipelines_table_row_spec.js +++ b/spec/frontend/pipelines/pipelines_table_row_spec.js @@ -1,5 +1,5 @@ import { mount } from '@vue/test-utils'; -import PipelinesTableRowComponent from '~/pipelines/components/pipelines_table_row.vue'; +import PipelinesTableRowComponent from '~/pipelines/components/pipelines_list/pipelines_table_row.vue'; import eventHub from '~/pipelines/event_hub'; describe('Pipelines Table Row', () => { diff --git a/spec/frontend/pipelines/pipelines_table_spec.js b/spec/frontend/pipelines/pipelines_table_spec.js index b0ab250dd16..c7d104bbde8 100644 --- a/spec/frontend/pipelines/pipelines_table_spec.js +++ b/spec/frontend/pipelines/pipelines_table_spec.js @@ -1,5 +1,5 @@ import { mount } from '@vue/test-utils'; -import PipelinesTable from '~/pipelines/components/pipelines_table.vue'; +import PipelinesTable from '~/pipelines/components/pipelines_list/pipelines_table.vue'; describe('Pipelines Table', () => { let pipeline; diff --git a/spec/frontend/pipelines/stage_spec.js b/spec/frontend/pipelines/stage_spec.js index 6aa041bcb7f..547f8994ca5 100644 --- a/spec/frontend/pipelines/stage_spec.js +++ b/spec/frontend/pipelines/stage_spec.js @@ -1,7 +1,7 @@ import { mount } from '@vue/test-utils'; import MockAdapter from 'axios-mock-adapter'; import axios from '~/lib/utils/axios_utils'; -import StageComponent from '~/pipelines/components/stage.vue'; +import StageComponent from '~/pipelines/components/pipelines_list/stage.vue'; import eventHub from '~/pipelines/event_hub'; import { stageReply } from './mock_data'; import waitForPromises from 'helpers/wait_for_promises'; diff --git a/spec/frontend/pipelines/test_reports/stores/actions_spec.js b/spec/frontend/pipelines/test_reports/stores/actions_spec.js index 56148361e0a..d4647c55a53 100644 --- a/spec/frontend/pipelines/test_reports/stores/actions_spec.js +++ b/spec/frontend/pipelines/test_reports/stores/actions_spec.js @@ -14,31 +14,100 @@ describe('Actions TestReports Store', () => { let state; const testReports = getJSONFixture('pipelines/test_report.json'); + const summary = { total_count: 1 }; - const endpoint = `${TEST_HOST}/test_reports.json`; + const fullReportEndpoint = `${TEST_HOST}/test_reports.json`; + const summaryEndpoint = `${TEST_HOST}/test_reports/summary.json`; const defaultState = { - endpoint, + fullReportEndpoint, + summaryEndpoint, testReports: {}, - selectedSuite: {}, + selectedSuite: null, + useBuildSummaryReport: false, }; beforeEach(() => { mock = new MockAdapter(axios); - state = defaultState; + state = { ...defaultState }; }); afterEach(() => { mock.restore(); }); - describe('fetch reports', () => { + describe('fetch report summary', () => { beforeEach(() => { - mock.onGet(`${TEST_HOST}/test_reports.json`).replyOnce(200, testReports, {}); + mock.onGet(summaryEndpoint).replyOnce(200, summary, {}); + }); + + describe('when useBuildSummaryReport in state is true', () => { + it('sets testReports and shows tests', done => { + testAction( + actions.fetchSummary, + null, + { ...state, useBuildSummaryReport: true }, + [{ type: types.SET_SUMMARY, payload: summary }], + [{ type: 'toggleLoading' }, { type: 'toggleLoading' }], + done, + ); + }); + + it('should create flash on API error', done => { + testAction( + actions.fetchSummary, + null, + { + summaryEndpoint: null, + useBuildSummaryReport: true, + }, + [], + [{ type: 'toggleLoading' }, { type: 'toggleLoading' }], + () => { + expect(createFlash).toHaveBeenCalled(); + done(); + }, + ); + }); + }); + + describe('when useBuildSummaryReport in state is false', () => { + it('sets testReports and shows tests', done => { + testAction( + actions.fetchSummary, + null, + state, + [{ type: types.SET_SUMMARY, payload: summary }], + [], + done, + ); + }); + + it('should create flash on API error', done => { + testAction( + actions.fetchSummary, + null, + { + summaryEndpoint: null, + }, + [], + [], + () => { + expect(createFlash).toHaveBeenCalled(); + done(); + }, + ); + }); + }); + }); + + describe('fetch full report', () => { + beforeEach(() => { + mock.onGet(fullReportEndpoint).replyOnce(200, testReports, {}); }); it('sets testReports and shows tests', done => { testAction( - actions.fetchReports, + actions.fetchFullReport, null, state, [{ type: types.SET_REPORTS, payload: testReports }], @@ -49,10 +118,10 @@ describe('Actions TestReports Store', () => { it('should create flash on API error', done => { testAction( - actions.fetchReports, + actions.fetchFullReport, null, { - endpoint: null, + fullReportEndpoint: null, }, [], [{ type: 'toggleLoading' }, { type: 'toggleLoading' }], @@ -64,28 +133,28 @@ describe('Actions TestReports Store', () => { }); }); - describe('set selected suite', () => { - const selectedSuite = testReports.test_suites[0]; + describe('set selected suite index', () => { + it('sets selectedSuiteIndex', done => { + const selectedSuiteIndex = 0; - it('sets selectedSuite', done => { testAction( - actions.setSelectedSuite, - selectedSuite, - state, - [{ type: types.SET_SELECTED_SUITE, payload: selectedSuite }], + actions.setSelectedSuiteIndex, + selectedSuiteIndex, + { ...state, hasFullReport: true }, + [{ type: types.SET_SELECTED_SUITE_INDEX, payload: selectedSuiteIndex }], [], done, ); }); }); - describe('remove selected suite', () => { - it('sets selectedSuite to {}', done => { + describe('remove selected suite index', () => { + it('sets selectedSuiteIndex to null', done => { testAction( - actions.removeSelectedSuite, + actions.removeSelectedSuiteIndex, {}, state, - [{ type: types.SET_SELECTED_SUITE, payload: {} }], + [{ type: types.SET_SELECTED_SUITE_INDEX, payload: null }], [], done, ); diff --git a/spec/frontend/pipelines/test_reports/stores/getters_spec.js b/spec/frontend/pipelines/test_reports/stores/getters_spec.js index 011a7e68908..ca9ebb54138 100644 --- a/spec/frontend/pipelines/test_reports/stores/getters_spec.js +++ b/spec/frontend/pipelines/test_reports/stores/getters_spec.js @@ -9,12 +9,12 @@ describe('Getters TestReports Store', () => { const defaultState = { testReports, - selectedSuite: testReports.test_suites[0], + selectedSuiteIndex: 0, }; const emptyState = { testReports: {}, - selectedSuite: {}, + selectedSuite: null, }; beforeEach(() => { @@ -47,6 +47,17 @@ describe('Getters TestReports Store', () => { }); }); + 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 test cases inside the suite', () => { setupState(); diff --git a/spec/frontend/pipelines/test_reports/stores/mutations_spec.js b/spec/frontend/pipelines/test_reports/stores/mutations_spec.js index a0eb93c4e6b..f4cc5c4bc5d 100644 --- a/spec/frontend/pipelines/test_reports/stores/mutations_spec.js +++ b/spec/frontend/pipelines/test_reports/stores/mutations_spec.js @@ -10,21 +10,13 @@ describe('Mutations TestReports Store', () => { const defaultState = { endpoint: '', testReports: {}, - selectedSuite: {}, + selectedSuite: null, isLoading: false, + hasFullReport: false, }; beforeEach(() => { - mockState = defaultState; - }); - - describe('set endpoint', () => { - it('should set endpoint', () => { - const expectedState = { ...mockState, endpoint: 'foo' }; - mutations[types.SET_ENDPOINT](mockState, 'foo'); - - expect(mockState.endpoint).toEqual(expectedState.endpoint); - }); + mockState = { ...defaultState }; }); describe('set reports', () => { @@ -33,15 +25,25 @@ describe('Mutations TestReports Store', () => { mutations[types.SET_REPORTS](mockState, testReports); expect(mockState.testReports).toEqual(expectedState.testReports); + expect(mockState.hasFullReport).toBe(true); + }); + }); + + 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 selected suite', () => { - it('should set selectedSuite', () => { - const selectedSuite = testReports.test_suites[0]; - mutations[types.SET_SELECTED_SUITE](mockState, selectedSuite); + describe('set summary', () => { + it('should set summary', () => { + const summary = { total_count: 1 }; + mutations[types.SET_SUMMARY](mockState, summary); - expect(mockState.selectedSuite).toEqual(selectedSuite); + expect(mockState.testReports).toEqual(summary); }); }); diff --git a/spec/frontend/pipelines/test_reports/test_reports_spec.js b/spec/frontend/pipelines/test_reports/test_reports_spec.js index cc86ba6d46d..ef0bcffabe3 100644 --- a/spec/frontend/pipelines/test_reports/test_reports_spec.js +++ b/spec/frontend/pipelines/test_reports/test_reports_spec.js @@ -1,8 +1,13 @@ import Vuex from 'vuex'; -import { shallowMount } from '@vue/test-utils'; +import { shallowMount, createLocalVue } from '@vue/test-utils'; import { getJSONFixture } from 'helpers/fixtures'; import TestReports from '~/pipelines/components/test_reports/test_reports.vue'; -import * as actions from '~/pipelines/stores/test_reports/actions'; +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'; + +const localVue = createLocalVue(); +localVue.use(Vuex); describe('Test reports app', () => { let wrapper; @@ -13,20 +18,31 @@ describe('Test reports app', () => { const loadingSpinner = () => wrapper.find('.js-loading-spinner'); const testsDetail = () => wrapper.find('.js-tests-detail'); const noTestsToShow = () => wrapper.find('.js-no-tests-to-show'); + const testSummary = () => wrapper.find(TestSummary); + const testSummaryTable = () => wrapper.find(TestSummaryTable); + + const actionSpies = { + fetchFullReport: jest.fn(), + fetchSummary: jest.fn(), + setSelectedSuiteIndex: jest.fn(), + removeSelectedSuiteIndex: jest.fn(), + }; const createComponent = (state = {}) => { store = new Vuex.Store({ state: { isLoading: false, - selectedSuite: {}, + selectedSuiteIndex: null, testReports, ...state, }, - actions, + actions: actionSpies, + getters, }); wrapper = shallowMount(TestReports, { store, + localVue, }); }; @@ -34,6 +50,16 @@ describe('Test reports app', () => { wrapper.destroy(); }); + describe('when component is created', () => { + beforeEach(() => { + createComponent(); + }); + + it('should call fetchSummary', () => { + expect(actionSpies.fetchSummary).toHaveBeenCalled(); + }); + }); + describe('when loading', () => { beforeEach(() => createComponent({ isLoading: true })); @@ -63,4 +89,41 @@ describe('Test reports app', () => { expect(wrapper.vm.showTests).toBeTruthy(); }); }); + + describe('when a suite is clicked', () => { + describe('when the full test report has already been received', () => { + beforeEach(() => { + createComponent({ hasFullReport: true }); + testSummaryTable().vm.$emit('row-click', 0); + }); + + it('should only call setSelectedSuiteIndex', () => { + expect(actionSpies.setSelectedSuiteIndex).toHaveBeenCalled(); + expect(actionSpies.fetchFullReport).not.toHaveBeenCalled(); + }); + }); + + describe('when the full test report has not been received', () => { + beforeEach(() => { + createComponent({ hasFullReport: false }); + testSummaryTable().vm.$emit('row-click', 0); + }); + + it('should call setSelectedSuiteIndex and fetchFullReport', () => { + expect(actionSpies.setSelectedSuiteIndex).toHaveBeenCalled(); + expect(actionSpies.fetchFullReport).toHaveBeenCalled(); + }); + }); + }); + + describe('when clicking back to summary', () => { + beforeEach(() => { + createComponent({ 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 index a5b093cf769..65bffe7039a 100644 --- a/spec/frontend/pipelines/test_reports/test_suite_table_spec.js +++ b/spec/frontend/pipelines/test_reports/test_suite_table_spec.js @@ -1,11 +1,14 @@ import Vuex from 'vuex'; -import { shallowMount } from '@vue/test-utils'; +import { shallowMount, createLocalVue } from '@vue/test-utils'; import { getJSONFixture } from 'helpers/fixtures'; import SuiteTable from '~/pipelines/components/test_reports/test_suite_table.vue'; import * as getters from '~/pipelines/stores/test_reports/getters'; import { TestStatus } from '~/pipelines/constants'; import skippedTestCases from './mock_data'; +const localVue = createLocalVue(); +localVue.use(Vuex); + describe('Test reports suite table', () => { let wrapper; let store; @@ -25,13 +28,17 @@ describe('Test reports suite table', () => { const createComponent = (suite = testSuite) => { store = new Vuex.Store({ state: { - selectedSuite: suite, + testReports: { + test_suites: [suite], + }, + selectedSuiteIndex: 0, }, getters, }); wrapper = shallowMount(SuiteTable, { store, + localVue, }); }; diff --git a/spec/frontend/pipelines/test_reports/test_summary_spec.js b/spec/frontend/pipelines/test_reports/test_summary_spec.js index 8f041e46472..79be6c168cf 100644 --- a/spec/frontend/pipelines/test_reports/test_summary_spec.js +++ b/spec/frontend/pipelines/test_reports/test_summary_spec.js @@ -60,7 +60,7 @@ describe('Test reports summary', () => { }); it('displays the correct total', () => { - expect(totalTests().text()).toBe('4 jobs'); + expect(totalTests().text()).toBe('4 tests'); }); it('displays the correct failure count', () => { diff --git a/spec/frontend/pipelines/time_ago_spec.js b/spec/frontend/pipelines/time_ago_spec.js index 1bd16182d47..04934fb93b0 100644 --- a/spec/frontend/pipelines/time_ago_spec.js +++ b/spec/frontend/pipelines/time_ago_spec.js @@ -1,5 +1,5 @@ import { shallowMount } from '@vue/test-utils'; -import TimeAgo from '~/pipelines/components/time_ago.vue'; +import TimeAgo from '~/pipelines/components/pipelines_list/time_ago.vue'; describe('Timeago component', () => { let wrapper; diff --git a/spec/frontend/pipelines/tokens/pipeline_branch_name_token_spec.js b/spec/frontend/pipelines/tokens/pipeline_branch_name_token_spec.js index 1a85221581e..650dd8a1def 100644 --- a/spec/frontend/pipelines/tokens/pipeline_branch_name_token_spec.js +++ b/spec/frontend/pipelines/tokens/pipeline_branch_name_token_spec.js @@ -1,7 +1,7 @@ import Api from '~/api'; import { GlFilteredSearchToken, GlFilteredSearchSuggestion, GlLoadingIcon } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; -import PipelineBranchNameToken from '~/pipelines/components/tokens/pipeline_branch_name_token.vue'; +import PipelineBranchNameToken from '~/pipelines/components/pipelines_list/tokens/pipeline_branch_name_token.vue'; import { branches, mockBranchesAfterMap } from '../mock_data'; describe('Pipeline Branch Name Token', () => { diff --git a/spec/frontend/pipelines/tokens/pipeline_status_token_spec.js b/spec/frontend/pipelines/tokens/pipeline_status_token_spec.js index ee3694868a5..096e4cd97f6 100644 --- a/spec/frontend/pipelines/tokens/pipeline_status_token_spec.js +++ b/spec/frontend/pipelines/tokens/pipeline_status_token_spec.js @@ -1,6 +1,6 @@ import { GlFilteredSearchToken, GlFilteredSearchSuggestion, GlIcon } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; -import PipelineStatusToken from '~/pipelines/components/tokens/pipeline_status_token.vue'; +import PipelineStatusToken from '~/pipelines/components/pipelines_list/tokens/pipeline_status_token.vue'; describe('Pipeline Status Token', () => { let wrapper; diff --git a/spec/frontend/pipelines/tokens/pipeline_tag_name_token_spec.js b/spec/frontend/pipelines/tokens/pipeline_tag_name_token_spec.js index 9fecc9412b7..15b283dc2ff 100644 --- a/spec/frontend/pipelines/tokens/pipeline_tag_name_token_spec.js +++ b/spec/frontend/pipelines/tokens/pipeline_tag_name_token_spec.js @@ -1,7 +1,7 @@ import Api from '~/api'; import { GlFilteredSearchToken, GlFilteredSearchSuggestion, GlLoadingIcon } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; -import PipelineTagNameToken from '~/pipelines/components/tokens/pipeline_tag_name_token.vue'; +import PipelineTagNameToken from '~/pipelines/components/pipelines_list/tokens/pipeline_tag_name_token.vue'; import { tags, mockTagsAfterMap } from '../mock_data'; describe('Pipeline Branch Name Token', () => { diff --git a/spec/frontend/pipelines/tokens/pipeline_trigger_author_token_spec.js b/spec/frontend/pipelines/tokens/pipeline_trigger_author_token_spec.js index 98de4f40c51..0b5cf2e202b 100644 --- a/spec/frontend/pipelines/tokens/pipeline_trigger_author_token_spec.js +++ b/spec/frontend/pipelines/tokens/pipeline_trigger_author_token_spec.js @@ -1,7 +1,7 @@ import Api from '~/api'; import { GlFilteredSearchToken, GlFilteredSearchSuggestion, GlLoadingIcon } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; -import PipelineTriggerAuthorToken from '~/pipelines/components/tokens/pipeline_trigger_author_token.vue'; +import PipelineTriggerAuthorToken from '~/pipelines/components/pipelines_list/tokens/pipeline_trigger_author_token.vue'; import { users } from '../mock_data'; describe('Pipeline Trigger Author Token', () => { |