diff options
Diffstat (limited to 'spec/frontend/pipelines/graph_shared')
3 files changed, 319 insertions, 0 deletions
diff --git a/spec/frontend/pipelines/graph_shared/__snapshots__/links_inner_spec.js.snap b/spec/frontend/pipelines/graph_shared/__snapshots__/links_inner_spec.js.snap new file mode 100644 index 00000000000..cf2b66dea5f --- /dev/null +++ b/spec/frontend/pipelines/graph_shared/__snapshots__/links_inner_spec.js.snap @@ -0,0 +1,23 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Links Inner component with a large number of needs matches snapshot and has expected path 1`] = ` +"<div class=\\"gl-display-flex gl-relative\\"><svg id=\\"link-svg\\" viewBox=\\"0,0,1019,445\\" width=\\"1019px\\" height=\\"445px\\" class=\\"gl-absolute gl-pointer-events-none\\"> + <path d=\\"M202,118L42,118C72,118,72,138,102,138\\" stroke-width=\\"2\\" class=\\"gl-fill-transparent gl-transition-duration-slow gl-transition-timing-function-ease gl-stroke-gray-200\\"></path> + <path d=\\"M202,118L52,118C82,118,82,148,112,148\\" stroke-width=\\"2\\" class=\\"gl-fill-transparent gl-transition-duration-slow gl-transition-timing-function-ease gl-stroke-gray-200\\"></path> + <path d=\\"M222,138L62,138C92,138,92,158,122,158\\" stroke-width=\\"2\\" class=\\"gl-fill-transparent gl-transition-duration-slow gl-transition-timing-function-ease gl-stroke-gray-200\\"></path> + <path d=\\"M212,128L72,128C102,128,102,168,132,168\\" stroke-width=\\"2\\" class=\\"gl-fill-transparent gl-transition-duration-slow gl-transition-timing-function-ease gl-stroke-gray-200\\"></path> + <path d=\\"M232,148L82,148C112,148,112,178,142,178\\" stroke-width=\\"2\\" class=\\"gl-fill-transparent gl-transition-duration-slow gl-transition-timing-function-ease gl-stroke-gray-200\\"></path> + </svg> </div>" +`; + +exports[`Links Inner component with a parallel need matches snapshot and has expected path 1`] = ` +"<div class=\\"gl-display-flex gl-relative\\"><svg id=\\"link-svg\\" viewBox=\\"0,0,1019,445\\" width=\\"1019px\\" height=\\"445px\\" class=\\"gl-absolute gl-pointer-events-none\\"> + <path d=\\"M192,108L22,108C52,108,52,118,82,118\\" stroke-width=\\"2\\" class=\\"gl-fill-transparent gl-transition-duration-slow gl-transition-timing-function-ease gl-stroke-gray-200\\"></path> + </svg> </div>" +`; + +exports[`Links Inner component with one need matches snapshot and has expected path 1`] = ` +"<div class=\\"gl-display-flex gl-relative\\"><svg id=\\"link-svg\\" viewBox=\\"0,0,1019,445\\" width=\\"1019px\\" height=\\"445px\\" class=\\"gl-absolute gl-pointer-events-none\\"> + <path d=\\"M202,118L42,118C72,118,72,138,102,138\\" stroke-width=\\"2\\" class=\\"gl-fill-transparent gl-transition-duration-slow gl-transition-timing-function-ease gl-stroke-gray-200\\"></path> + </svg> </div>" +`; diff --git a/spec/frontend/pipelines/graph_shared/links_inner_spec.js b/spec/frontend/pipelines/graph_shared/links_inner_spec.js new file mode 100644 index 00000000000..6cabe2bc8a7 --- /dev/null +++ b/spec/frontend/pipelines/graph_shared/links_inner_spec.js @@ -0,0 +1,197 @@ +import { shallowMount } from '@vue/test-utils'; +import { setHTMLFixture } from 'helpers/fixtures'; +import LinksInner from '~/pipelines/components/graph_shared/links_inner.vue'; +import { createJobsHash } from '~/pipelines/utils'; +import { + jobRect, + largePipelineData, + parallelNeedData, + pipelineData, + pipelineDataWithNoNeeds, + rootRect, +} from '../pipeline_graph/mock_data'; + +describe('Links Inner component', () => { + const containerId = 'pipeline-graph-container'; + const defaultProps = { + containerId, + containerMeasurements: { width: 1019, height: 445 }, + pipelineId: 1, + pipelineData: [], + }; + let wrapper; + + const createComponent = (props) => { + wrapper = shallowMount(LinksInner, { + propsData: { ...defaultProps, ...props }, + }); + }; + + const findLinkSvg = () => wrapper.find('#link-svg'); + const findAllLinksPath = () => findLinkSvg().findAll('path'); + + // We create fixture so that each job has an empty div that represent + // the JobPill in the DOM. Each `JobPill` would have different coordinates, + // so we increment their coordinates on each iteration to simulat different positions. + const setFixtures = ({ stages }) => { + const jobs = createJobsHash(stages); + const arrayOfJobs = Object.keys(jobs); + + const linksHtmlElements = arrayOfJobs.map((job) => { + return `<div id=${job}-${defaultProps.pipelineId} />`; + }); + + setHTMLFixture(`<div id="${containerId}">${linksHtmlElements.join(' ')}</div>`); + + // We are mocking the clientRect data of each job and the container ID. + jest + .spyOn(document.getElementById(containerId), 'getBoundingClientRect') + .mockImplementation(() => rootRect); + + arrayOfJobs.forEach((job, index) => { + jest + .spyOn( + document.getElementById(`${job}-${defaultProps.pipelineId}`), + 'getBoundingClientRect', + ) + .mockImplementation(() => { + const newValue = 10 * index; + const { left, right, top, bottom, x, y } = jobRect; + return { + ...jobRect, + left: left + newValue, + right: right + newValue, + top: top + newValue, + bottom: bottom + newValue, + x: x + newValue, + y: y + newValue, + }; + }); + }); + }; + + afterEach(() => { + jest.restoreAllMocks(); + wrapper.destroy(); + wrapper = null; + }); + + describe('basic SVG creation', () => { + beforeEach(() => { + createComponent(); + }); + + it('renders an SVG of the right size', () => { + expect(findLinkSvg().exists()).toBe(true); + expect(findLinkSvg().attributes('width')).toBe( + `${defaultProps.containerMeasurements.width}px`, + ); + expect(findLinkSvg().attributes('height')).toBe( + `${defaultProps.containerMeasurements.height}px`, + ); + }); + }); + + describe('no pipeline data', () => { + beforeEach(() => { + createComponent(); + }); + + it('renders the component', () => { + expect(findLinkSvg().exists()).toBe(true); + expect(findAllLinksPath()).toHaveLength(0); + }); + }); + + describe('pipeline data with no needs', () => { + beforeEach(() => { + createComponent({ pipelineData: pipelineDataWithNoNeeds.stages }); + }); + + it('renders no links', () => { + expect(findLinkSvg().exists()).toBe(true); + expect(findAllLinksPath()).toHaveLength(0); + }); + }); + + describe('with one need', () => { + beforeEach(() => { + setFixtures(pipelineData); + createComponent({ pipelineData: pipelineData.stages }); + }); + + it('renders one link', () => { + expect(findAllLinksPath()).toHaveLength(1); + }); + + it('path does not contain NaN values', () => { + expect(wrapper.html()).not.toContain('NaN'); + }); + + it('matches snapshot and has expected path', () => { + expect(wrapper.html()).toMatchSnapshot(); + }); + }); + + describe('with a parallel need', () => { + beforeEach(() => { + setFixtures(parallelNeedData); + createComponent({ pipelineData: parallelNeedData.stages }); + }); + + it('renders only one link for all the same parallel jobs', () => { + expect(findAllLinksPath()).toHaveLength(1); + }); + + it('path does not contain NaN values', () => { + expect(wrapper.html()).not.toContain('NaN'); + }); + + it('matches snapshot and has expected path', () => { + expect(wrapper.html()).toMatchSnapshot(); + }); + }); + + describe('with a large number of needs', () => { + beforeEach(() => { + setFixtures(largePipelineData); + createComponent({ pipelineData: largePipelineData.stages }); + }); + + it('renders the correct number of links', () => { + expect(findAllLinksPath()).toHaveLength(5); + }); + + it('path does not contain NaN values', () => { + expect(wrapper.html()).not.toContain('NaN'); + }); + + it('matches snapshot and has expected path', () => { + expect(wrapper.html()).toMatchSnapshot(); + }); + }); + + describe('interactions', () => { + beforeEach(() => { + setFixtures(largePipelineData); + createComponent({ pipelineData: largePipelineData.stages }); + }); + + it('highlight needs on hover', async () => { + const firstLink = findAllLinksPath().at(0); + + const defaultColorClass = 'gl-stroke-gray-200'; + const hoverColorClass = 'gl-stroke-blue-400'; + + expect(firstLink.classes(defaultColorClass)).toBe(true); + expect(firstLink.classes(hoverColorClass)).toBe(false); + + // Because there is a watcher, we need to set the props after the component + // has mounted. + await wrapper.setProps({ highlightedJob: 'test_1' }); + + expect(firstLink.classes(defaultColorClass)).toBe(false); + expect(firstLink.classes(hoverColorClass)).toBe(true); + }); + }); +}); diff --git a/spec/frontend/pipelines/graph_shared/links_layer_spec.js b/spec/frontend/pipelines/graph_shared/links_layer_spec.js new file mode 100644 index 00000000000..0ff8583fbff --- /dev/null +++ b/spec/frontend/pipelines/graph_shared/links_layer_spec.js @@ -0,0 +1,99 @@ +import { GlAlert, GlButton } from '@gitlab/ui'; +import { mount, shallowMount } from '@vue/test-utils'; +import LinksInner from '~/pipelines/components/graph_shared/links_inner.vue'; +import LinksLayer from '~/pipelines/components/graph_shared/links_layer.vue'; +import { generateResponse, mockPipelineResponse } from '../graph/mock_data'; + +describe('links layer component', () => { + let wrapper; + + const findAlert = () => wrapper.find(GlAlert); + const findShowAnyways = () => findAlert().find(GlButton); + const findLinksInner = () => wrapper.find(LinksInner); + + const pipeline = generateResponse(mockPipelineResponse, 'root/fungi-xoxo'); + const containerId = `pipeline-links-container-${pipeline.id}`; + const slotContent = "<div>Ceci n'est pas un graphique</div>"; + + const tooManyStages = Array(101) + .fill(0) + .flatMap(() => pipeline.stages); + + const defaultProps = { + containerId, + containerMeasurements: { width: 400, height: 400 }, + pipelineId: pipeline.id, + pipelineData: pipeline.stages, + }; + + const createComponent = ({ mountFn = shallowMount, props = {} } = {}) => { + wrapper = mountFn(LinksLayer, { + propsData: { + ...defaultProps, + ...props, + }, + slots: { + default: slotContent, + }, + stubs: { + 'links-inner': true, + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + describe('with data under max stages', () => { + beforeEach(() => { + createComponent(); + }); + + it('renders the default slot', () => { + expect(wrapper.html()).toContain(slotContent); + }); + + it('renders the inner links component', () => { + expect(findLinksInner().exists()).toBe(true); + }); + }); + + describe('with more than the max number of stages', () => { + describe('rendering', () => { + beforeEach(() => { + createComponent({ props: { pipelineData: tooManyStages } }); + }); + + it('renders the default slot', () => { + expect(wrapper.html()).toContain(slotContent); + }); + + it('renders the alert component', () => { + expect(findAlert().exists()).toBe(true); + }); + + it('does not render the inner links component', () => { + expect(findLinksInner().exists()).toBe(false); + }); + }); + + describe('interactions', () => { + beforeEach(() => { + createComponent({ mountFn: mount, props: { pipelineData: tooManyStages } }); + }); + + it('renders the disable button', () => { + expect(findShowAnyways().exists()).toBe(true); + expect(findShowAnyways().text()).toBe(wrapper.vm.$options.i18n.showLinksAnyways); + }); + + it('shows links when override is clicked', async () => { + expect(findLinksInner().exists()).toBe(false); + await findShowAnyways().trigger('click'); + expect(findLinksInner().exists()).toBe(true); + }); + }); + }); +}); |