summaryrefslogtreecommitdiff
path: root/spec/frontend/pipelines
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-06-18 11:18:50 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2020-06-18 11:18:50 +0000
commit8c7f4e9d5f36cff46365a7f8c4b9c21578c1e781 (patch)
treea77e7fe7a93de11213032ed4ab1f33a3db51b738 /spec/frontend/pipelines
parent00b35af3db1abfe813a778f643dad221aad51fca (diff)
downloadgitlab-ce-8c7f4e9d5f36cff46365a7f8c4b9c21578c1e781.tar.gz
Add latest changes from gitlab-org/gitlab@13-1-stable-ee
Diffstat (limited to 'spec/frontend/pipelines')
-rw-r--r--spec/frontend/pipelines/components/dag/__snapshots__/dag_graph_spec.js.snap230
-rw-r--r--spec/frontend/pipelines/components/dag/dag_graph_spec.js218
-rw-r--r--spec/frontend/pipelines/components/dag/dag_spec.js137
-rw-r--r--spec/frontend/pipelines/components/dag/drawing_utils_spec.js57
-rw-r--r--spec/frontend/pipelines/components/dag/mock_data.js390
-rw-r--r--spec/frontend/pipelines/components/dag/parsing_utils_spec.js133
-rw-r--r--spec/frontend/pipelines/components/pipelines_filtered_search_spec.js123
-rw-r--r--spec/frontend/pipelines/graph/graph_component_spec.js5
-rw-r--r--spec/frontend/pipelines/mock_data.js98
-rw-r--r--spec/frontend/pipelines/pipelines_spec.js9
-rw-r--r--spec/frontend/pipelines/tokens/pipeline_branch_name_token_spec.js17
-rw-r--r--spec/frontend/pipelines/tokens/pipeline_status_token_spec.js62
-rw-r--r--spec/frontend/pipelines/tokens/pipeline_tag_name_token_spec.js98
-rw-r--r--spec/frontend/pipelines/tokens/pipeline_trigger_author_token_spec.js10
14 files changed, 1559 insertions, 28 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
new file mode 100644
index 00000000000..629efc6d3fa
--- /dev/null
+++ b/spec/frontend/pipelines/components/dag/__snapshots__/dag_graph_spec.js.snap
@@ -0,0 +1,230 @@
+// 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-cursor-pointer\\">
+ <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-cursor-pointer\\">
+ <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-cursor-pointer\\">
+ <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-cursor-pointer\\">
+ <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-cursor-pointer\\">
+ <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-cursor-pointer\\">
+ <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-cursor-pointer\\">
+ <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-cursor-pointer\\">
+ <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-cursor-pointer\\">
+ <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-cursor-pointer\\">
+ <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-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>
+ </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_graph_spec.js b/spec/frontend/pipelines/components/dag/dag_graph_spec.js
new file mode 100644
index 00000000000..017461dfb84
--- /dev/null
+++ b/spec/frontend/pipelines/components/dag/dag_graph_spec.js
@@ -0,0 +1,218 @@
+import { mount } 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';
+import { createSankey } from '~/pipelines/components/dag/drawing_utils';
+import { removeOrphanNodes } from '~/pipelines/components/dag/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 = mount(DagGraph, {
+ attachToDocument: true,
+ propsData,
+ data() {
+ return {
+ color: () => {},
+ width: 0,
+ height: 0,
+ };
+ },
+ });
+ };
+
+ beforeEach(() => {
+ createComponent({ graphData: parsedData });
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ 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
new file mode 100644
index 00000000000..666b4cfaa2f
--- /dev/null
+++ b/spec/frontend/pipelines/components/dag/dag_spec.js
@@ -0,0 +1,137 @@
+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 Dag from '~/pipelines/components/dag/dag.vue';
+import DagGraph from '~/pipelines/components/dag/dag_graph.vue';
+
+import {
+ DEFAULT,
+ PARSE_FAILURE,
+ LOAD_FAILURE,
+ UNSUPPORTED_DATA,
+} from '~/pipelines/components/dag//constants';
+import { mockBaseData, tooSmallGraph, unparseableGraph } from './mock_data';
+
+describe('Pipeline DAG graph wrapper', () => {
+ let wrapper;
+ let mock;
+ const getAlert = () => wrapper.find(GlAlert);
+ const getAllAlerts = () => wrapper.findAll(GlAlert);
+ const getGraph = () => wrapper.find(DagGraph);
+ const getErrorText = type => wrapper.vm.$options.errorTexts[type];
+
+ const dataPath = '/root/test/pipelines/90/dag.json';
+
+ const createComponent = (propsData = {}, method = shallowMount) => {
+ if (wrapper?.destroy) {
+ wrapper.destroy();
+ }
+
+ wrapper = method(Dag, {
+ propsData,
+ data() {
+ return {
+ showFailureAlert: false,
+ };
+ },
+ });
+ };
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ });
+
+ afterEach(() => {
+ mock.restore();
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe('when there is no dataUrl', () => {
+ beforeEach(() => {
+ createComponent({ graphUrl: undefined });
+ });
+
+ it('shows the DEFAULT alert and not the graph', () => {
+ expect(getAlert().exists()).toBe(true);
+ expect(getAlert().text()).toBe(getErrorText(DEFAULT));
+ expect(getGraph().exists()).toBe(false);
+ });
+ });
+
+ describe('when there is a dataUrl', () => {
+ describe('but the data fetch fails', () => {
+ beforeEach(() => {
+ mock.onGet(dataPath).replyOnce(500);
+ createComponent({ graphUrl: dataPath });
+ });
+
+ 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);
+ });
+ });
+ });
+
+ describe('the data fetch succeeds but the parse fails', () => {
+ beforeEach(() => {
+ mock.onGet(dataPath).replyOnce(200, unparseableGraph);
+ createComponent({ graphUrl: dataPath });
+ });
+
+ 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);
+ });
+ });
+ });
+
+ describe('and the data fetch and parse succeeds', () => {
+ beforeEach(() => {
+ mock.onGet(dataPath).replyOnce(200, mockBaseData);
+ createComponent({ graphUrl: dataPath }, mount);
+ });
+
+ 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);
+ });
+ });
+ });
+
+ describe('the data fetch and parse succeeds, but the resulting graph is too small', () => {
+ beforeEach(() => {
+ mock.onGet(dataPath).replyOnce(200, tooSmallGraph);
+ createComponent({ graphUrl: dataPath });
+ });
+
+ 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);
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/pipelines/components/dag/drawing_utils_spec.js b/spec/frontend/pipelines/components/dag/drawing_utils_spec.js
new file mode 100644
index 00000000000..a50163411ed
--- /dev/null
+++ b/spec/frontend/pipelines/components/dag/drawing_utils_spec.js
@@ -0,0 +1,57 @@
+import { createSankey } from '~/pipelines/components/dag/drawing_utils';
+import { parseData } from '~/pipelines/components/dag/parsing_utils';
+import { mockBaseData } from './mock_data';
+
+describe('DAG visualization drawing utilities', () => {
+ const parsed = parseData(mockBaseData.stages);
+
+ 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
new file mode 100644
index 00000000000..5de8697170a
--- /dev/null
+++ b/spec/frontend/pipelines/components/dag/mock_data.js
@@ -0,0 +1,390 @@
+/*
+ It is important that the simple 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 mockBaseData = {
+ stages: [
+ {
+ 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: '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 tooSmallGraph = {
+ stages: [
+ {
+ name: 'test',
+ groups: [
+ {
+ name: 'jest',
+ size: 2,
+ jobs: [{ name: 'jest 1/2' }, { name: 'jest 2/2' }],
+ },
+ {
+ name: 'rspec',
+ size: 1,
+ jobs: [{ name: 'rspec', needs: ['frontend fixtures'] }],
+ },
+ ],
+ },
+ {
+ 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',
+ 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,
+ },
+ ],
+};
diff --git a/spec/frontend/pipelines/components/dag/parsing_utils_spec.js b/spec/frontend/pipelines/components/dag/parsing_utils_spec.js
new file mode 100644
index 00000000000..d9a1296e572
--- /dev/null
+++ b/spec/frontend/pipelines/components/dag/parsing_utils_spec.js
@@ -0,0 +1,133 @@
+import {
+ createNodesStructure,
+ makeLinksFromNodes,
+ filterByAncestors,
+ parseData,
+ removeOrphanNodes,
+ getMaxNodes,
+} from '~/pipelines/components/dag/parsing_utils';
+
+import { createSankey } from '~/pipelines/components/dag/drawing_utils';
+import { mockBaseData } from './mock_data';
+
+describe('DAG visualization parsing utilities', () => {
+ const { nodes, nodeDict } = createNodesStructure(mockBaseData.stages);
+ const unfilteredLinks = makeLinksFromNodes(nodes, nodeDict);
+ const parsed = parseData(mockBaseData.stages);
+
+ const layoutSettings = {
+ width: 200,
+ height: 200,
+ nodeWidth: 10,
+ nodePadding: 20,
+ paddingForLabels: 100,
+ };
+
+ const sankeyLayout = createSankey(layoutSettings)(parsed);
+
+ describe('createNodesStructure', () => {
+ const parallelGroupName = 'jest';
+ const parallelJobName = 'jest 1/2';
+ const singleJobName = 'frontend fixtures';
+
+ const { name, jobs, size } = mockBaseData.stages[0].groups[0];
+
+ it('returns the expected node structure', () => {
+ expect(nodes[0]).toHaveProperty('category', mockBaseData.stages[0].name);
+ expect(nodes[0]).toHaveProperty('name', name);
+ expect(nodes[0]).toHaveProperty('jobs', jobs);
+ expect(nodes[0]).toHaveProperty('size', size);
+ });
+
+ it('adds needs to top level of nodeDict entries', () => {
+ expect(nodeDict[parallelGroupName]).toHaveProperty('needs');
+ expect(nodeDict[parallelJobName]).toHaveProperty('needs');
+ expect(nodeDict[singleJobName]).toHaveProperty('needs');
+ });
+
+ it('makes entries in nodeDict for jobs and parallel jobs', () => {
+ const nodeNames = Object.keys(nodeDict);
+
+ expect(nodeNames.includes(parallelGroupName)).toBe(true);
+ expect(nodeNames.includes(parallelJobName)).toBe(true);
+ expect(nodeNames.includes(singleJobName)).toBe(true);
+ });
+ });
+
+ describe('makeLinksFromNodes', () => {
+ it('returns the expected link structure', () => {
+ expect(unfilteredLinks[0]).toHaveProperty('source', 'frontend fixtures');
+ expect(unfilteredLinks[0]).toHaveProperty('target', 'jest');
+ expect(unfilteredLinks[0]).toHaveProperty('value', 10);
+ });
+ });
+
+ 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 cleanedNodes = removeOrphanNodes(sankeyLayout.nodes);
+ expect(cleanedNodes).toHaveLength(sankeyLayout.nodes.length - 1);
+ });
+ });
+
+ 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);
+ });
+ });
+});
diff --git a/spec/frontend/pipelines/components/pipelines_filtered_search_spec.js b/spec/frontend/pipelines/components/pipelines_filtered_search_spec.js
index 12c6fab9c41..bdc807fcbfe 100644
--- a/spec/frontend/pipelines/components/pipelines_filtered_search_spec.js
+++ b/spec/frontend/pipelines/components/pipelines_filtered_search_spec.js
@@ -3,13 +3,7 @@ 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 {
- users,
- mockSearch,
- pipelineWithStages,
- branches,
- mockBranchesAfterMap,
-} from '../mock_data';
+import { users, mockSearch, branches, tags } from '../mock_data';
import { GlFilteredSearch } from '@gitlab/ui';
describe('Pipelines filtered search', () => {
@@ -21,12 +15,16 @@ describe('Pipelines filtered search', () => {
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 createComponent = () => {
+ const createComponent = (params = {}) => {
wrapper = mount(PipelinesFilteredSearch, {
propsData: {
- pipelines: [pipelineWithStages],
projectId: '21',
+ params,
},
attachToDocument: true,
});
@@ -37,6 +35,7 @@ describe('Pipelines filtered search', () => {
jest.spyOn(Api, 'projectUsers').mockResolvedValue(users);
jest.spyOn(Api, 'branches').mockResolvedValue({ data: branches });
+ jest.spyOn(Api, 'tags').mockResolvedValue({ data: tags });
createComponent();
});
@@ -55,37 +54,39 @@ describe('Pipelines filtered search', () => {
});
it('displays search tokens', () => {
- expect(getSearchToken('username')).toMatchObject({
+ expect(findUserToken()).toMatchObject({
type: 'username',
icon: 'user',
title: 'Trigger author',
unique: true,
- triggerAuthors: users,
projectId: '21',
operators: [expect.objectContaining({ value: '=' })],
});
- expect(getSearchToken('ref')).toMatchObject({
+ expect(findBranchToken()).toMatchObject({
type: 'ref',
icon: 'branch',
title: 'Branch name',
unique: true,
- branches: mockBranchesAfterMap,
projectId: '21',
operators: [expect.objectContaining({ value: '=' })],
});
- });
-
- it('fetches and sets project users', () => {
- expect(Api.projectUsers).toHaveBeenCalled();
-
- expect(wrapper.vm.projectUsers).toEqual(users);
- });
- it('fetches and sets branches', () => {
- expect(Api.branches).toHaveBeenCalled();
+ expect(findStatusToken()).toMatchObject({
+ type: 'status',
+ icon: 'status',
+ title: 'Status',
+ unique: true,
+ operators: [expect.objectContaining({ value: '=' })],
+ });
- expect(wrapper.vm.projectBranches).toEqual(mockBranchesAfterMap);
+ expect(findTagToken()).toMatchObject({
+ type: 'tag',
+ icon: 'tag',
+ title: 'Tag name',
+ unique: true,
+ operators: [expect.objectContaining({ value: '=' })],
+ });
});
it('emits filterPipelines on submit with correct filter', () => {
@@ -94,4 +95,80 @@ describe('Pipelines filtered search', () => {
expect(wrapper.emitted('filterPipelines')).toBeTruthy();
expect(wrapper.emitted('filterPipelines')[0]).toEqual([mockSearch]);
});
+
+ it('disables tag name token when branch name token is active', () => {
+ findFilteredSearch().vm.$emit('input', [
+ { type: 'ref', value: { data: 'branch-1', operator: '=' } },
+ { type: 'filtered-search-term', value: { data: '' } },
+ ]);
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(findBranchToken().disabled).toBe(false);
+ expect(findTagToken().disabled).toBe(true);
+ });
+ });
+
+ it('disables branch name token when tag name token is active', () => {
+ findFilteredSearch().vm.$emit('input', [
+ { type: 'tag', value: { data: 'tag-1', operator: '=' } },
+ { type: 'filtered-search-term', value: { data: '' } },
+ ]);
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(findBranchToken().disabled).toBe(true);
+ expect(findTagToken().disabled).toBe(false);
+ });
+ });
+
+ it('resets tokens disabled state on clear', () => {
+ findFilteredSearch().vm.$emit('clearInput');
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(findBranchToken().disabled).toBe(false);
+ expect(findTagToken().disabled).toBe(false);
+ });
+ });
+
+ it('resets tokens disabled state when clearing tokens by backspace', () => {
+ findFilteredSearch().vm.$emit('input', [{ type: 'filtered-search-term', value: { data: '' } }]);
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(findBranchToken().disabled).toBe(false);
+ expect(findTagToken().disabled).toBe(false);
+ });
+ });
+
+ describe('Url query params', () => {
+ const params = {
+ username: 'deja.green',
+ ref: 'master',
+ };
+
+ 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')).toEqual(expectedValueProp);
+ expect(findFilteredSearch().props('value')).toHaveLength(expectedValueProp.length);
+ });
+ });
});
diff --git a/spec/frontend/pipelines/graph/graph_component_spec.js b/spec/frontend/pipelines/graph/graph_component_spec.js
index a9b06eab3fa..9731ce3f8a6 100644
--- a/spec/frontend/pipelines/graph/graph_component_spec.js
+++ b/spec/frontend/pipelines/graph/graph_component_spec.js
@@ -7,6 +7,7 @@ import linkedPipelinesColumn from '~/pipelines/components/graph/linked_pipelines
import graphJSON from './mock_data';
import linkedPipelineJSON from './linked_pipelines_mock_data';
import PipelinesMediator from '~/pipelines/pipeline_details_mediator';
+import { setHTMLFixture } from 'helpers/fixtures';
describe('graph component', () => {
const store = new PipelineStore();
@@ -15,6 +16,10 @@ describe('graph component', () => {
let wrapper;
+ beforeEach(() => {
+ setHTMLFixture('<div class="layout-page"></div>');
+ });
+
afterEach(() => {
wrapper.destroy();
wrapper = null;
diff --git a/spec/frontend/pipelines/mock_data.js b/spec/frontend/pipelines/mock_data.js
index 37c1e471415..e63efc543f1 100644
--- a/spec/frontend/pipelines/mock_data.js
+++ b/spec/frontend/pipelines/mock_data.js
@@ -560,9 +560,107 @@ export const branches = [
},
];
+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: 'master-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: 'master', 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', 'master-tag'];
diff --git a/spec/frontend/pipelines/pipelines_spec.js b/spec/frontend/pipelines/pipelines_spec.js
index 2ddd2116e2c..0eeaef01a2d 100644
--- a/spec/frontend/pipelines/pipelines_spec.js
+++ b/spec/frontend/pipelines/pipelines_spec.js
@@ -56,6 +56,7 @@ describe('Pipelines', () => {
propsData: {
store: new Store(),
projectId: '21',
+ params: {},
...props,
},
methods: {
@@ -683,7 +684,13 @@ describe('Pipelines', () => {
});
it('updates request data and query params on filter submit', () => {
- const expectedQueryParams = { page: '1', scope: 'all', username: 'root', ref: 'master' };
+ const expectedQueryParams = {
+ page: '1',
+ scope: 'all',
+ username: 'root',
+ ref: 'master',
+ status: 'pending',
+ };
findFilteredSearch().vm.$emit('submit', mockSearch);
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 a6753600792..1a85221581e 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,8 @@
+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 { branches } from '../mock_data';
+import { branches, mockBranchesAfterMap } from '../mock_data';
describe('Pipeline Branch Name Token', () => {
let wrapper;
@@ -21,10 +22,9 @@ describe('Pipeline Branch Name Token', () => {
type: 'ref',
icon: 'branch',
title: 'Branch name',
- dataType: 'ref',
unique: true,
- branches,
projectId: '21',
+ disabled: false,
},
value: {
data: '',
@@ -46,6 +46,8 @@ describe('Pipeline Branch Name Token', () => {
};
beforeEach(() => {
+ jest.spyOn(Api, 'branches').mockResolvedValue({ data: branches });
+
createComponent();
});
@@ -58,6 +60,13 @@ describe('Pipeline Branch Name Token', () => {
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 });
@@ -73,7 +82,7 @@ describe('Pipeline Branch Name Token', () => {
});
describe('shows branches correctly', () => {
- it('renders all trigger authors', () => {
+ it('renders all branches', () => {
createComponent({ stubs }, { branches, loading: false });
expect(findAllFilteredSearchSuggestions()).toHaveLength(branches.length);
diff --git a/spec/frontend/pipelines/tokens/pipeline_status_token_spec.js b/spec/frontend/pipelines/tokens/pipeline_status_token_spec.js
new file mode 100644
index 00000000000..ee3694868a5
--- /dev/null
+++ b/spec/frontend/pipelines/tokens/pipeline_status_token_spec.js
@@ -0,0 +1,62 @@
+import { GlFilteredSearchToken, GlFilteredSearchSuggestion, GlIcon } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import PipelineStatusToken from '~/pipelines/components/tokens/pipeline_status_token.vue';
+
+describe('Pipeline Status Token', () => {
+ let wrapper;
+
+ const findFilteredSearchToken = () => wrapper.find(GlFilteredSearchToken);
+ const findAllFilteredSearchSuggestions = () => wrapper.findAll(GlFilteredSearchSuggestion);
+ const findAllGlIcons = () => wrapper.findAll(GlIcon);
+
+ const stubs = {
+ GlFilteredSearchToken: {
+ template: `<div><slot name="suggestions"></slot></div>`,
+ },
+ };
+
+ const defaultProps = {
+ config: {
+ type: 'status',
+ icon: 'status',
+ title: 'Status',
+ unique: true,
+ },
+ value: {
+ data: '',
+ },
+ };
+
+ const createComponent = options => {
+ wrapper = shallowMount(PipelineStatusToken, {
+ propsData: {
+ ...defaultProps,
+ },
+ ...options,
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ it('passes config correctly', () => {
+ expect(findFilteredSearchToken().props('config')).toEqual(defaultProps.config);
+ });
+
+ describe('shows statuses correctly', () => {
+ beforeEach(() => {
+ createComponent({ stubs });
+ });
+
+ 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
new file mode 100644
index 00000000000..9fecc9412b7
--- /dev/null
+++ b/spec/frontend/pipelines/tokens/pipeline_tag_name_token_spec.js
@@ -0,0 +1,98 @@
+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 { tags, mockTagsAfterMap } from '../mock_data';
+
+describe('Pipeline Branch Name Token', () => {
+ let wrapper;
+
+ const findFilteredSearchToken = () => wrapper.find(GlFilteredSearchToken);
+ const findAllFilteredSearchSuggestions = () => wrapper.findAll(GlFilteredSearchSuggestion);
+ const findLoadingIcon = () => wrapper.find(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: '',
+ },
+ };
+
+ const createComponent = (options, data) => {
+ wrapper = shallowMount(PipelineTagNameToken, {
+ propsData: {
+ ...defaultProps,
+ },
+ data() {
+ return {
+ ...data,
+ };
+ },
+ ...options,
+ });
+ };
+
+ beforeEach(() => {
+ jest.spyOn(Api, 'tags').mockResolvedValue({ data: tags });
+
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ 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 = ['master-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
index 00a9ff04e75..98de4f40c51 100644
--- a/spec/frontend/pipelines/tokens/pipeline_trigger_author_token_spec.js
+++ b/spec/frontend/pipelines/tokens/pipeline_trigger_author_token_spec.js
@@ -1,3 +1,4 @@
+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';
@@ -45,6 +46,8 @@ describe('Pipeline Trigger Author Token', () => {
};
beforeEach(() => {
+ jest.spyOn(Api, 'projectUsers').mockResolvedValue(users);
+
createComponent();
});
@@ -57,6 +60,13 @@ describe('Pipeline Trigger Author Token', () => {
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({ stubs }, { loading: true });