diff options
Diffstat (limited to 'spec/frontend/pipelines')
18 files changed, 872 insertions, 721 deletions
diff --git a/spec/frontend/pipelines/components/pipelines_list/pipeline_mini_graph_spec.js b/spec/frontend/pipelines/components/pipelines_list/pipeline_mini_graph_spec.js new file mode 100644 index 00000000000..154828aff4b --- /dev/null +++ b/spec/frontend/pipelines/components/pipelines_list/pipeline_mini_graph_spec.js @@ -0,0 +1,83 @@ +import { shallowMount } from '@vue/test-utils'; +import PipelineMiniGraph from '~/pipelines/components/pipelines_list/pipeline_mini_graph.vue'; +import PipelineStage from '~/pipelines/components/pipelines_list/pipeline_stage.vue'; + +const { pipelines } = getJSONFixture('pipelines/pipelines.json'); +const mockStages = pipelines[0].details.stages; + +describe('Pipeline Mini Graph', () => { + let wrapper; + + const findPipelineStages = () => wrapper.findAll(PipelineStage); + const findPipelineStagesAt = (i) => findPipelineStages().at(i); + + const createComponent = (props = {}) => { + wrapper = shallowMount(PipelineMiniGraph, { + propsData: { + stages: mockStages, + ...props, + }, + }); + }; + + it('renders stages', () => { + createComponent(); + + expect(findPipelineStages()).toHaveLength(mockStages.length); + }); + + it('renders stages with a custom class', () => { + createComponent({ stagesClass: 'my-class' }); + + expect(wrapper.findAll('.my-class')).toHaveLength(mockStages.length); + }); + + it('does not fail when stages are empty', () => { + createComponent({ stages: [] }); + + expect(wrapper.exists()).toBe(true); + expect(findPipelineStages()).toHaveLength(0); + }); + + it('triggers events in "action request complete" in stages', () => { + createComponent(); + + findPipelineStagesAt(0).vm.$emit('pipelineActionRequestComplete'); + findPipelineStagesAt(1).vm.$emit('pipelineActionRequestComplete'); + + expect(wrapper.emitted('pipelineActionRequestComplete')).toHaveLength(2); + }); + + it('update dropdown is false by default', () => { + createComponent(); + + expect(findPipelineStagesAt(0).props('updateDropdown')).toBe(false); + expect(findPipelineStagesAt(1).props('updateDropdown')).toBe(false); + }); + + it('update dropdown is set to true', () => { + createComponent({ updateDropdown: true }); + + expect(findPipelineStagesAt(0).props('updateDropdown')).toBe(true); + expect(findPipelineStagesAt(1).props('updateDropdown')).toBe(true); + }); + + it('is merge train is false by default', () => { + createComponent(); + + expect(findPipelineStagesAt(0).props('isMergeTrain')).toBe(false); + expect(findPipelineStagesAt(1).props('isMergeTrain')).toBe(false); + }); + + it('is merge train is set to true', () => { + createComponent({ isMergeTrain: true }); + + expect(findPipelineStagesAt(0).props('isMergeTrain')).toBe(true); + expect(findPipelineStagesAt(1).props('isMergeTrain')).toBe(true); + }); + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); +}); diff --git a/spec/frontend/pipelines/components/pipelines_list/pipeline_stage_spec.js b/spec/frontend/pipelines/components/pipelines_list/pipeline_stage_spec.js new file mode 100644 index 00000000000..60026f69b84 --- /dev/null +++ b/spec/frontend/pipelines/components/pipelines_list/pipeline_stage_spec.js @@ -0,0 +1,210 @@ +import { GlDropdown } from '@gitlab/ui'; +import { mount } from '@vue/test-utils'; +import MockAdapter from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; +import PipelineStage from '~/pipelines/components/pipelines_list/pipeline_stage.vue'; +import eventHub from '~/pipelines/event_hub'; +import { stageReply } from '../../mock_data'; + +const dropdownPath = 'path.json'; + +describe('Pipelines stage component', () => { + let wrapper; + let mock; + + const createComponent = (props = {}) => { + wrapper = mount(PipelineStage, { + attachTo: document.body, + propsData: { + stage: { + status: { + group: 'success', + icon: 'status_success', + title: 'success', + }, + dropdown_path: dropdownPath, + }, + updateDropdown: false, + ...props, + }, + }); + }; + + beforeEach(() => { + mock = new MockAdapter(axios); + jest.spyOn(eventHub, '$emit'); + }); + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + + eventHub.$emit.mockRestore(); + mock.restore(); + }); + + const findDropdown = () => wrapper.findComponent(GlDropdown); + const findDropdownToggle = () => wrapper.find('button.dropdown-toggle'); + const findDropdownMenu = () => + wrapper.find('[data-testid="mini-pipeline-graph-dropdown-menu-list"]'); + const findCiActionBtn = () => wrapper.find('.js-ci-action'); + const findMergeTrainWarning = () => wrapper.find('[data-testid="warning-message-merge-trains"]'); + + const openStageDropdown = () => { + findDropdownToggle().trigger('click'); + return new Promise((resolve) => { + wrapper.vm.$root.$on('bv::dropdown::show', resolve); + }); + }; + + describe('default appearance', () => { + beforeEach(() => { + createComponent(); + }); + + it('should render a dropdown with the status icon', () => { + expect(findDropdown().exists()).toBe(true); + expect(findDropdownToggle().exists()).toBe(true); + expect(wrapper.find('[data-testid="status_success_borderless-icon"]').exists()).toBe(true); + }); + }); + + describe('when update dropdown is changed', () => { + beforeEach(() => { + createComponent(); + }); + }); + + describe('when user opens dropdown and stage request is successful', () => { + beforeEach(async () => { + mock.onGet(dropdownPath).reply(200, stageReply); + createComponent(); + + await openStageDropdown(); + await axios.waitForAll(); + }); + + it('should render the received data and emit `clickedDropdown` event', async () => { + expect(findDropdownMenu().text()).toContain(stageReply.latest_statuses[0].name); + expect(eventHub.$emit).toHaveBeenCalledWith('clickedDropdown'); + }); + + it('should refresh when updateDropdown is set to true', async () => { + expect(mock.history.get).toHaveLength(1); + + wrapper.setProps({ updateDropdown: true }); + await axios.waitForAll(); + + expect(mock.history.get).toHaveLength(2); + }); + }); + + describe('when user opens dropdown and stage request fails', () => { + beforeEach(async () => { + mock.onGet(dropdownPath).reply(500); + createComponent(); + + await openStageDropdown(); + await axios.waitForAll(); + }); + + it('should close the dropdown', () => { + expect(findDropdown().classes('show')).toBe(false); + }); + }); + + describe('update endpoint correctly', () => { + beforeEach(async () => { + const copyStage = { ...stageReply }; + copyStage.latest_statuses[0].name = 'this is the updated content'; + mock.onGet('bar.json').reply(200, copyStage); + createComponent({ + stage: { + status: { + group: 'running', + icon: 'status_running', + title: 'running', + }, + dropdown_path: 'bar.json', + }, + }); + await axios.waitForAll(); + }); + + it('should update the stage to request the new endpoint provided', async () => { + await openStageDropdown(); + await axios.waitForAll(); + + expect(findDropdownMenu().text()).toContain('this is the updated content'); + }); + }); + + describe('pipelineActionRequestComplete', () => { + beforeEach(() => { + mock.onGet(dropdownPath).reply(200, stageReply); + mock.onPost(`${stageReply.latest_statuses[0].status.action.path}.json`).reply(200); + + createComponent(); + }); + + const clickCiAction = async () => { + await openStageDropdown(); + await axios.waitForAll(); + + findCiActionBtn().trigger('click'); + await axios.waitForAll(); + }; + + it('closes dropdown when job item action is clicked', async () => { + const hidden = jest.fn(); + + wrapper.vm.$root.$on('bv::dropdown::hide', hidden); + + expect(hidden).toHaveBeenCalledTimes(0); + + await clickCiAction(); + + expect(hidden).toHaveBeenCalledTimes(1); + }); + + it('emits `pipelineActionRequestComplete` when job item action is clicked', async () => { + await clickCiAction(); + + expect(wrapper.emitted('pipelineActionRequestComplete')).toHaveLength(1); + }); + }); + + describe('With merge trains enabled', () => { + beforeEach(async () => { + mock.onGet(dropdownPath).reply(200, stageReply); + createComponent({ + isMergeTrain: true, + }); + + await openStageDropdown(); + await axios.waitForAll(); + }); + + it('shows a warning on the dropdown', () => { + const warning = findMergeTrainWarning(); + + expect(warning.text()).toBe('Merge train pipeline jobs can not be retried'); + }); + }); + + describe('With merge trains disabled', () => { + beforeEach(async () => { + mock.onGet(dropdownPath).reply(200, stageReply); + createComponent(); + + await openStageDropdown(); + await axios.waitForAll(); + }); + + it('does not show a warning on the dropdown', () => { + const warning = findMergeTrainWarning(); + + expect(warning.exists()).toBe(false); + }); + }); +}); diff --git a/spec/frontend/pipelines/empty_state_spec.js b/spec/frontend/pipelines/empty_state_spec.js index 3ebedc9ac87..912bc7a104a 100644 --- a/spec/frontend/pipelines/empty_state_spec.js +++ b/spec/frontend/pipelines/empty_state_spec.js @@ -1,24 +1,25 @@ -import { shallowMount } from '@vue/test-utils'; +import { mount } from '@vue/test-utils'; import EmptyState from '~/pipelines/components/pipelines_list/empty_state.vue'; describe('Pipelines Empty State', () => { let wrapper; - const findGetStartedButton = () => wrapper.find('[data-testid="get-started-pipelines"]'); - const findInfoText = () => wrapper.find('[data-testid="info-text"]').text(); - const createWrapper = () => { - wrapper = shallowMount(EmptyState, { + const findIllustration = () => wrapper.find('img'); + const findButton = () => wrapper.find('a'); + + const createWrapper = (props = {}) => { + wrapper = mount(EmptyState, { propsData: { - helpPagePath: 'foo', - emptyStateSvgPath: 'foo', + emptyStateSvgPath: 'foo.svg', canSetCi: true, + ...props, }, }); }; - describe('renders', () => { + describe('when user can configure CI', () => { beforeEach(() => { - createWrapper(); + createWrapper({}, mount); }); afterEach(() => { @@ -27,26 +28,49 @@ describe('Pipelines Empty State', () => { }); it('should render empty state SVG', () => { - expect(wrapper.find('img').attributes('src')).toBe('foo'); + expect(findIllustration().attributes('src')).toBe('foo.svg'); }); it('should render empty state header', () => { - expect(wrapper.find('[data-testid="header-text"]').text()).toBe('Build with confidence'); - }); - - it('should render a link with provided help path', () => { - expect(findGetStartedButton().attributes('href')).toBe('foo'); + expect(wrapper.text()).toContain('Build with confidence'); }); it('should render empty state information', () => { - expect(findInfoText()).toContain( + expect(wrapper.text()).toContain( 'GitLab CI/CD can automatically build, test, and deploy your code. Let GitLab take care of time', 'consuming tasks, so you can spend more time creating', ); }); + it('should render button with help path', () => { + expect(findButton().attributes('href')).toBe('/help/ci/quick_start/index.md'); + }); + it('should render button text', () => { - expect(findGetStartedButton().text()).toBe('Get started with CI/CD'); + expect(findButton().text()).toBe('Get started with CI/CD'); + }); + }); + + describe('when user cannot configure CI', () => { + beforeEach(() => { + createWrapper({ canSetCi: false }, mount); + }); + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + it('should render empty state SVG', () => { + expect(findIllustration().attributes('src')).toBe('foo.svg'); + }); + + it('should render empty state header', () => { + expect(wrapper.text()).toBe('This project is not currently set up to run pipelines.'); + }); + + it('should not render a link', () => { + expect(findButton().exists()).toBe(false); }); }); }); diff --git a/spec/frontend/pipelines/graph/graph_component_spec.js b/spec/frontend/pipelines/graph/graph_component_spec.js index 3e8d4ba314c..6c3f848333c 100644 --- a/spec/frontend/pipelines/graph/graph_component_spec.js +++ b/spec/frontend/pipelines/graph/graph_component_spec.js @@ -20,6 +20,10 @@ describe('graph component', () => { const defaultProps = { pipeline: generateResponse(mockPipelineResponse, 'root/fungi-xoxo'), + configPaths: { + metricsPath: '', + graphqlResourceEtag: 'this/is/a/path', + }, }; const defaultData = { diff --git a/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js b/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js index 202365ecd35..44d8e467f51 100644 --- a/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js +++ b/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js @@ -9,6 +9,8 @@ import PipelineGraphWrapper from '~/pipelines/components/graph/graph_component_w import { mockPipelineResponse } from './mock_data'; const defaultProvide = { + graphqlResourceEtag: 'frog/amphibirama/etag/', + metricsPath: '', pipelineProjectPath: 'frog/amphibirama', pipelineIid: '22', }; @@ -87,6 +89,13 @@ describe('Pipeline graph wrapper', () => { it('displays the graph', () => { expect(getGraph().exists()).toBe(true); }); + + it('passes the etag resource and metrics path to the graph', () => { + expect(getGraph().props('configPaths')).toMatchObject({ + graphqlResourceEtag: defaultProvide.graphqlResourceEtag, + metricsPath: defaultProvide.metricsPath, + }); + }); }); describe('when there is an error', () => { @@ -121,4 +130,48 @@ describe('Pipeline graph wrapper', () => { expect(wrapper.vm.$apollo.queries.pipeline.refetch).toHaveBeenCalled(); }); }); + + describe('when query times out', () => { + const advanceApolloTimers = async () => { + jest.runOnlyPendingTimers(); + await wrapper.vm.$nextTick(); + await wrapper.vm.$nextTick(); + }; + + beforeEach(async () => { + const errorData = { + data: { + project: { + pipelines: null, + }, + }, + errors: [{ message: 'timeout' }], + }; + + const failSucceedFail = jest + .fn() + .mockResolvedValueOnce(errorData) + .mockResolvedValueOnce(mockPipelineResponse) + .mockResolvedValueOnce(errorData); + + createComponentWithApollo(failSucceedFail); + await wrapper.vm.$nextTick(); + }); + + it('shows correct errors and does not overwrite populated data when data is empty', async () => { + /* fails at first, shows error, no data yet */ + expect(getAlert().exists()).toBe(true); + expect(getGraph().exists()).toBe(false); + + /* succeeds, clears error, shows graph */ + await advanceApolloTimers(); + expect(getAlert().exists()).toBe(false); + expect(getGraph().exists()).toBe(true); + + /* fails again, alert returns but data persists */ + await advanceApolloTimers(); + expect(getAlert().exists()).toBe(true); + expect(getGraph().exists()).toBe(true); + }); + }); }); diff --git a/spec/frontend/pipelines/graph/linked_pipelines_column_spec.js b/spec/frontend/pipelines/graph/linked_pipelines_column_spec.js index 8f01accccc1..4c72dad735e 100644 --- a/spec/frontend/pipelines/graph/linked_pipelines_column_spec.js +++ b/spec/frontend/pipelines/graph/linked_pipelines_column_spec.js @@ -20,6 +20,10 @@ describe('Linked Pipelines Column', () => { columnTitle: 'Downstream', linkedPipelines: processedPipeline.downstream, type: DOWNSTREAM, + configPaths: { + metricsPath: '', + graphqlResourceEtag: 'this/is/a/path', + }, }; let wrapper; @@ -112,7 +116,7 @@ describe('Linked Pipelines Column', () => { it('emits the error', async () => { await clickExpandButton(); - expect(wrapper.emitted().error).toEqual([[LOAD_FAILURE]]); + expect(wrapper.emitted().error).toEqual([[{ type: LOAD_FAILURE, skipSentry: true }]]); }); it('does not show the pipeline', async () => { @@ -163,7 +167,7 @@ describe('Linked Pipelines Column', () => { it('emits the error', async () => { await clickExpandButton(); - expect(wrapper.emitted().error).toEqual([[LOAD_FAILURE]]); + expect(wrapper.emitted().error).toEqual([[{ type: LOAD_FAILURE, skipSentry: true }]]); }); it('does not show the pipeline', async () => { diff --git a/spec/frontend/pipelines/graph_shared/links_inner_spec.js b/spec/frontend/pipelines/graph_shared/links_inner_spec.js index 6cabe2bc8a7..6fef1c9b62e 100644 --- a/spec/frontend/pipelines/graph_shared/links_inner_spec.js +++ b/spec/frontend/pipelines/graph_shared/links_inner_spec.js @@ -1,5 +1,15 @@ import { shallowMount } from '@vue/test-utils'; +import MockAdapter from 'axios-mock-adapter'; import { setHTMLFixture } from 'helpers/fixtures'; +import axios from '~/lib/utils/axios_utils'; +import { + PIPELINES_DETAIL_LINK_DURATION, + PIPELINES_DETAIL_LINKS_TOTAL, + PIPELINES_DETAIL_LINKS_JOB_RATIO, +} from '~/performance/constants'; +import * as perfUtils from '~/performance/utils'; +import * as sentryUtils from '~/pipelines/components/graph/utils'; +import * as Api from '~/pipelines/components/graph_shared/api'; import LinksInner from '~/pipelines/components/graph_shared/links_inner.vue'; import { createJobsHash } from '~/pipelines/utils'; import { @@ -18,7 +28,9 @@ describe('Links Inner component', () => { containerMeasurements: { width: 1019, height: 445 }, pipelineId: 1, pipelineData: [], + totalGroups: 10, }; + let wrapper; const createComponent = (props) => { @@ -194,4 +206,141 @@ describe('Links Inner component', () => { expect(firstLink.classes(hoverColorClass)).toBe(true); }); }); + + describe('performance metrics', () => { + let markAndMeasure; + let reportToSentry; + let reportPerformance; + let mock; + + beforeEach(() => { + mock = new MockAdapter(axios); + jest.spyOn(window, 'requestAnimationFrame').mockImplementation((cb) => cb()); + markAndMeasure = jest.spyOn(perfUtils, 'performanceMarkAndMeasure'); + reportToSentry = jest.spyOn(sentryUtils, 'reportToSentry'); + reportPerformance = jest.spyOn(Api, 'reportPerformance'); + }); + + afterEach(() => { + mock.restore(); + }); + + describe('with no metrics config object', () => { + beforeEach(() => { + setFixtures(pipelineData); + createComponent({ + pipelineData: pipelineData.stages, + }); + }); + + it('is not called', () => { + expect(markAndMeasure).not.toHaveBeenCalled(); + expect(reportToSentry).not.toHaveBeenCalled(); + expect(reportPerformance).not.toHaveBeenCalled(); + }); + }); + + describe('with metrics config set to false', () => { + beforeEach(() => { + setFixtures(pipelineData); + createComponent({ + pipelineData: pipelineData.stages, + metricsConfig: { + collectMetrics: false, + metricsPath: '/path/to/metrics', + }, + }); + }); + + it('is not called', () => { + expect(markAndMeasure).not.toHaveBeenCalled(); + expect(reportToSentry).not.toHaveBeenCalled(); + expect(reportPerformance).not.toHaveBeenCalled(); + }); + }); + + describe('with no metrics path', () => { + beforeEach(() => { + setFixtures(pipelineData); + createComponent({ + pipelineData: pipelineData.stages, + metricsConfig: { + collectMetrics: true, + metricsPath: '', + }, + }); + }); + + it('is not called', () => { + expect(markAndMeasure).not.toHaveBeenCalled(); + expect(reportToSentry).not.toHaveBeenCalled(); + expect(reportPerformance).not.toHaveBeenCalled(); + }); + }); + + describe('with metrics path and collect set to true', () => { + const metricsPath = '/root/project/-/ci/prometheus_metrics/histograms.json'; + const duration = 0.0478; + const numLinks = 1; + const metricsData = { + histograms: [ + { name: PIPELINES_DETAIL_LINK_DURATION, value: duration / 1000 }, + { name: PIPELINES_DETAIL_LINKS_TOTAL, value: numLinks }, + { + name: PIPELINES_DETAIL_LINKS_JOB_RATIO, + value: numLinks / defaultProps.totalGroups, + }, + ], + }; + + describe('when no duration is obtained', () => { + beforeEach(() => { + jest.spyOn(window.performance, 'getEntriesByName').mockImplementation(() => { + return []; + }); + + setFixtures(pipelineData); + + createComponent({ + pipelineData: pipelineData.stages, + metricsConfig: { + collectMetrics: true, + path: metricsPath, + }, + }); + }); + + it('attempts to collect metrics', () => { + expect(markAndMeasure).toHaveBeenCalled(); + expect(reportPerformance).not.toHaveBeenCalled(); + expect(reportToSentry).not.toHaveBeenCalled(); + }); + }); + + describe('with duration and no error', () => { + beforeEach(() => { + jest.spyOn(window.performance, 'getEntriesByName').mockImplementation(() => { + return [{ duration }]; + }); + + setFixtures(pipelineData); + + createComponent({ + pipelineData: pipelineData.stages, + metricsConfig: { + collectMetrics: true, + path: metricsPath, + }, + }); + }); + + it('it calls reportPerformance with expected arguments', () => { + expect(markAndMeasure).toHaveBeenCalled(); + expect(reportPerformance).toHaveBeenCalled(); + expect(reportPerformance).toHaveBeenCalledWith(metricsPath, metricsData); + expect(reportToSentry).not.toHaveBeenCalled(); + }); + }); + }); + }); }); diff --git a/spec/frontend/pipelines/graph_shared/links_layer_spec.js b/spec/frontend/pipelines/graph_shared/links_layer_spec.js index 0ff8583fbff..43d8fe28893 100644 --- a/spec/frontend/pipelines/graph_shared/links_layer_spec.js +++ b/spec/frontend/pipelines/graph_shared/links_layer_spec.js @@ -79,6 +79,24 @@ describe('links layer component', () => { }); }); + describe('with width or height measurement at 0', () => { + beforeEach(() => { + createComponent({ props: { containerMeasurements: { width: 0, height: 100 } } }); + }); + + it('renders the default slot', () => { + expect(wrapper.html()).toContain(slotContent); + }); + + it('does not render the alert component', () => { + expect(findAlert().exists()).toBe(false); + }); + + it('does not render the inner links component', () => { + expect(findLinksInner().exists()).toBe(false); + }); + }); + describe('interactions', () => { beforeEach(() => { createComponent({ mountFn: mount, props: { pipelineData: tooManyStages } }); diff --git a/spec/frontend/pipelines/mock_data.js b/spec/frontend/pipelines/mock_data.js index 2afdbb05107..337838c41b3 100644 --- a/spec/frontend/pipelines/mock_data.js +++ b/spec/frontend/pipelines/mock_data.js @@ -2,328 +2,6 @@ const PIPELINE_RUNNING = 'RUNNING'; const PIPELINE_CANCELED = 'CANCELED'; const PIPELINE_FAILED = 'FAILED'; -export const pipelineWithStages = { - id: 20333396, - user: { - id: 128633, - name: 'Rémy Coutable', - username: 'rymai', - state: 'active', - avatar_url: - 'https://secure.gravatar.com/avatar/263da227929cc0035cb0eba512bcf81a?s=80\u0026d=identicon', - web_url: 'https://gitlab.com/rymai', - path: '/rymai', - }, - active: true, - coverage: '58.24', - source: 'push', - created_at: '2018-04-11T14:04:53.881Z', - updated_at: '2018-04-11T14:05:00.792Z', - path: '/gitlab-org/gitlab/pipelines/20333396', - flags: { - latest: true, - stuck: false, - auto_devops: false, - yaml_errors: false, - retryable: false, - cancelable: true, - failure_reason: false, - }, - details: { - status: { - icon: 'status_running', - text: 'running', - label: 'running', - group: 'running', - has_details: true, - details_path: '/gitlab-org/gitlab/pipelines/20333396', - favicon: - 'https://assets.gitlab-static.net/assets/ci_favicons/favicon_status_running-2eb56be2871937954b2ba6d6f4ee9fdf7e5e1c146ac45f7be98119ccaca1aca9.ico', - }, - duration: null, - finished_at: null, - stages: [ - { - name: 'build', - title: 'build: skipped', - status: { - icon: 'status_skipped', - text: 'skipped', - label: 'skipped', - group: 'skipped', - has_details: true, - details_path: '/gitlab-org/gitlab/pipelines/20333396#build', - favicon: - 'https://assets.gitlab-static.net/assets/ci_favicons/favicon_status_skipped-a2eee568a5bffdb494050c7b62dde241de9189280836288ac8923d369f16222d.ico', - }, - path: '/gitlab-org/gitlab/pipelines/20333396#build', - dropdown_path: '/gitlab-org/gitlab/pipelines/20333396/stage.json?stage=build', - }, - { - name: 'prepare', - title: 'prepare: passed', - status: { - icon: 'status_success', - text: 'passed', - label: 'passed', - group: 'success', - has_details: true, - details_path: '/gitlab-org/gitlab/pipelines/20333396#prepare', - favicon: - 'https://assets.gitlab-static.net/assets/ci_favicons/favicon_status_success-26f59841becbef8c6fe414e9e74471d8bfd6a91b5855c19fe7f5923a40a7da47.ico', - }, - path: '/gitlab-org/gitlab/pipelines/20333396#prepare', - dropdown_path: '/gitlab-org/gitlab/pipelines/20333396/stage.json?stage=prepare', - }, - { - name: 'test', - title: 'test: running', - status: { - icon: 'status_running', - text: 'running', - label: 'running', - group: 'running', - has_details: true, - details_path: '/gitlab-org/gitlab/pipelines/20333396#test', - favicon: - 'https://assets.gitlab-static.net/assets/ci_favicons/favicon_status_running-2eb56be2871937954b2ba6d6f4ee9fdf7e5e1c146ac45f7be98119ccaca1aca9.ico', - }, - path: '/gitlab-org/gitlab/pipelines/20333396#test', - dropdown_path: '/gitlab-org/gitlab/pipelines/20333396/stage.json?stage=test', - }, - { - name: 'post-test', - title: 'post-test: created', - status: { - icon: 'status_created', - text: 'created', - label: 'created', - group: 'created', - has_details: true, - details_path: '/gitlab-org/gitlab/pipelines/20333396#post-test', - favicon: - 'https://assets.gitlab-static.net/assets/ci_favicons/favicon_status_created-e997aa0b7db73165df8a9d6803932b18d7b7cc37d604d2d96e378fea2dba9c5f.ico', - }, - path: '/gitlab-org/gitlab/pipelines/20333396#post-test', - dropdown_path: '/gitlab-org/gitlab/pipelines/20333396/stage.json?stage=post-test', - }, - { - name: 'pages', - title: 'pages: created', - status: { - icon: 'status_created', - text: 'created', - label: 'created', - group: 'created', - has_details: true, - details_path: '/gitlab-org/gitlab/pipelines/20333396#pages', - favicon: - 'https://assets.gitlab-static.net/assets/ci_favicons/favicon_status_created-e997aa0b7db73165df8a9d6803932b18d7b7cc37d604d2d96e378fea2dba9c5f.ico', - }, - path: '/gitlab-org/gitlab/pipelines/20333396#pages', - dropdown_path: '/gitlab-org/gitlab/pipelines/20333396/stage.json?stage=pages', - }, - { - name: 'post-cleanup', - title: 'post-cleanup: created', - status: { - icon: 'status_created', - text: 'created', - label: 'created', - group: 'created', - has_details: true, - details_path: '/gitlab-org/gitlab/pipelines/20333396#post-cleanup', - favicon: - 'https://assets.gitlab-static.net/assets/ci_favicons/favicon_status_created-e997aa0b7db73165df8a9d6803932b18d7b7cc37d604d2d96e378fea2dba9c5f.ico', - }, - path: '/gitlab-org/gitlab/pipelines/20333396#post-cleanup', - dropdown_path: '/gitlab-org/gitlab/pipelines/20333396/stage.json?stage=post-cleanup', - }, - ], - artifacts: [ - { - name: 'gitlab:assets:compile', - expired: false, - expire_at: '2018-05-12T14:22:54.730Z', - path: '/gitlab-org/gitlab/-/jobs/62411438/artifacts/download', - keep_path: '/gitlab-org/gitlab/-/jobs/62411438/artifacts/keep', - browse_path: '/gitlab-org/gitlab/-/jobs/62411438/artifacts/browse', - }, - { - name: 'rspec-mysql 12 28', - expired: false, - expire_at: '2018-05-12T14:22:45.136Z', - path: '/gitlab-org/gitlab/-/jobs/62411397/artifacts/download', - keep_path: '/gitlab-org/gitlab/-/jobs/62411397/artifacts/keep', - browse_path: '/gitlab-org/gitlab/-/jobs/62411397/artifacts/browse', - }, - { - name: 'rspec-mysql 6 28', - expired: false, - expire_at: '2018-05-12T14:22:41.523Z', - path: '/gitlab-org/gitlab/-/jobs/62411391/artifacts/download', - keep_path: '/gitlab-org/gitlab/-/jobs/62411391/artifacts/keep', - browse_path: '/gitlab-org/gitlab/-/jobs/62411391/artifacts/browse', - }, - { - name: 'rspec-pg geo 0 1', - expired: false, - expire_at: '2018-05-12T14:22:13.287Z', - path: '/gitlab-org/gitlab/-/jobs/62411353/artifacts/download', - keep_path: '/gitlab-org/gitlab/-/jobs/62411353/artifacts/keep', - browse_path: '/gitlab-org/gitlab/-/jobs/62411353/artifacts/browse', - }, - { - name: 'rspec-mysql 0 28', - expired: false, - expire_at: '2018-05-12T14:22:06.834Z', - path: '/gitlab-org/gitlab/-/jobs/62411385/artifacts/download', - keep_path: '/gitlab-org/gitlab/-/jobs/62411385/artifacts/keep', - browse_path: '/gitlab-org/gitlab/-/jobs/62411385/artifacts/browse', - }, - { - name: 'spinach-mysql 0 2', - expired: false, - expire_at: '2018-05-12T14:21:51.409Z', - path: '/gitlab-org/gitlab/-/jobs/62411423/artifacts/download', - keep_path: '/gitlab-org/gitlab/-/jobs/62411423/artifacts/keep', - browse_path: '/gitlab-org/gitlab/-/jobs/62411423/artifacts/browse', - }, - { - name: 'karma', - expired: false, - expire_at: '2018-05-12T14:21:20.934Z', - path: '/gitlab-org/gitlab/-/jobs/62411440/artifacts/download', - keep_path: '/gitlab-org/gitlab/-/jobs/62411440/artifacts/keep', - browse_path: '/gitlab-org/gitlab/-/jobs/62411440/artifacts/browse', - }, - { - name: 'spinach-pg 0 2', - expired: false, - expire_at: '2018-05-12T14:20:01.028Z', - path: '/gitlab-org/gitlab/-/jobs/62411419/artifacts/download', - keep_path: '/gitlab-org/gitlab/-/jobs/62411419/artifacts/keep', - browse_path: '/gitlab-org/gitlab/-/jobs/62411419/artifacts/browse', - }, - { - name: 'spinach-pg 1 2', - expired: false, - expire_at: '2018-05-12T14:19:04.336Z', - path: '/gitlab-org/gitlab/-/jobs/62411421/artifacts/download', - keep_path: '/gitlab-org/gitlab/-/jobs/62411421/artifacts/keep', - browse_path: '/gitlab-org/gitlab/-/jobs/62411421/artifacts/browse', - }, - { - name: 'sast', - expired: null, - expire_at: null, - path: '/gitlab-org/gitlab/-/jobs/62411442/artifacts/download', - browse_path: '/gitlab-org/gitlab/-/jobs/62411442/artifacts/browse', - }, - { - name: 'code_quality', - expired: false, - expire_at: '2018-04-18T14:16:24.484Z', - path: '/gitlab-org/gitlab/-/jobs/62411441/artifacts/download', - keep_path: '/gitlab-org/gitlab/-/jobs/62411441/artifacts/keep', - browse_path: '/gitlab-org/gitlab/-/jobs/62411441/artifacts/browse', - }, - { - name: 'cache gems', - expired: null, - expire_at: null, - path: '/gitlab-org/gitlab/-/jobs/62411447/artifacts/download', - browse_path: '/gitlab-org/gitlab/-/jobs/62411447/artifacts/browse', - }, - { - name: 'dependency_scanning', - expired: null, - expire_at: null, - path: '/gitlab-org/gitlab/-/jobs/62411443/artifacts/download', - browse_path: '/gitlab-org/gitlab/-/jobs/62411443/artifacts/browse', - }, - { - name: 'compile-assets', - expired: false, - expire_at: '2018-04-18T14:12:07.638Z', - path: '/gitlab-org/gitlab/-/jobs/62411334/artifacts/download', - keep_path: '/gitlab-org/gitlab/-/jobs/62411334/artifacts/keep', - browse_path: '/gitlab-org/gitlab/-/jobs/62411334/artifacts/browse', - }, - { - name: 'setup-test-env', - expired: false, - expire_at: '2018-04-18T14:10:27.024Z', - path: '/gitlab-org/gitlab/-/jobs/62411336/artifacts/download', - keep_path: '/gitlab-org/gitlab/-/jobs/62411336/artifacts/keep', - browse_path: '/gitlab-org/gitlab/-/jobs/62411336/artifacts/browse', - }, - { - name: 'retrieve-tests-metadata', - expired: false, - expire_at: '2018-05-12T14:06:35.926Z', - path: '/gitlab-org/gitlab/-/jobs/62411333/artifacts/download', - keep_path: '/gitlab-org/gitlab/-/jobs/62411333/artifacts/keep', - browse_path: '/gitlab-org/gitlab/-/jobs/62411333/artifacts/browse', - }, - ], - manual_actions: [ - { - name: 'package-and-qa', - path: '/gitlab-org/gitlab/-/jobs/62411330/play', - playable: true, - }, - { - name: 'review-docs-deploy', - path: '/gitlab-org/gitlab/-/jobs/62411332/play', - playable: true, - }, - ], - }, - ref: { - name: 'master', - path: '/gitlab-org/gitlab/commits/master', - tag: false, - branch: true, - }, - commit: { - id: 'e6a2885c503825792cb8a84a8731295e361bd059', - short_id: 'e6a2885c', - title: "Merge branch 'ce-to-ee-2018-04-11' into 'master'", - created_at: '2018-04-11T14:04:39.000Z', - parent_ids: [ - '5d9b5118f6055f72cff1a82b88133609912f2c1d', - '6fdc6ee76a8062fe41b1a33f7c503334a6ebdc02', - ], - message: - "Merge branch 'ce-to-ee-2018-04-11' into 'master'\n\nCE upstream - 2018-04-11 12:26 UTC\n\nSee merge request gitlab-org/gitlab-ee!5326", - author_name: 'Rémy Coutable', - author_email: 'remy@rymai.me', - authored_date: '2018-04-11T14:04:39.000Z', - committer_name: 'Rémy Coutable', - committer_email: 'remy@rymai.me', - committed_date: '2018-04-11T14:04:39.000Z', - author: { - id: 128633, - name: 'Rémy Coutable', - username: 'rymai', - state: 'active', - avatar_url: - 'https://secure.gravatar.com/avatar/263da227929cc0035cb0eba512bcf81a?s=80\u0026d=identicon', - web_url: 'https://gitlab.com/rymai', - path: '/rymai', - }, - author_gravatar_url: - 'https://secure.gravatar.com/avatar/263da227929cc0035cb0eba512bcf81a?s=80\u0026d=identicon', - commit_url: - 'https://gitlab.com/gitlab-org/gitlab/commit/e6a2885c503825792cb8a84a8731295e361bd059', - commit_path: '/gitlab-org/gitlab/commit/e6a2885c503825792cb8a84a8731295e361bd059', - }, - cancel_path: '/gitlab-org/gitlab/pipelines/20333396/cancel', - triggered_by: null, - triggered: [], -}; - const threeWeeksAgo = new Date(); threeWeeksAgo.setDate(threeWeeksAgo.getDate() - 21); diff --git a/spec/frontend/pipelines/pipeline_triggerer_spec.js b/spec/frontend/pipelines/pipeline_triggerer_spec.js index 467a97d95c7..ffb2721f159 100644 --- a/spec/frontend/pipelines/pipeline_triggerer_spec.js +++ b/spec/frontend/pipelines/pipeline_triggerer_spec.js @@ -35,8 +35,8 @@ describe('Pipelines Triggerer', () => { wrapper.destroy(); }); - it('should render a table cell', () => { - expect(wrapper.find('.table-section').exists()).toBe(true); + it('should render pipeline triggerer table cell', () => { + expect(wrapper.find('[data-testid="pipeline-triggerer"]').exists()).toBe(true); }); it('should pass triggerer information when triggerer is provided', () => { diff --git a/spec/frontend/pipelines/pipeline_url_spec.js b/spec/frontend/pipelines/pipeline_url_spec.js index 44c9def99cc..367c7f2b2f6 100644 --- a/spec/frontend/pipelines/pipeline_url_spec.js +++ b/spec/frontend/pipelines/pipeline_url_spec.js @@ -1,19 +1,20 @@ import { shallowMount } from '@vue/test-utils'; -import $ from 'jquery'; import { trimText } from 'helpers/text_helper'; import PipelineUrlComponent from '~/pipelines/components/pipelines_list/pipeline_url.vue'; -$.fn.popover = () => {}; +const projectPath = 'test/test'; describe('Pipeline Url Component', () => { let wrapper; + const findTableCell = () => wrapper.find('[data-testid="pipeline-url-table-cell"]'); 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 findAutoDevopsTagLink = () => wrapper.find('[data-testid="pipeline-url-autodevops-link"]'); const findStuckTag = () => wrapper.find('[data-testid="pipeline-url-stuck"]'); const findDetachedTag = () => wrapper.find('[data-testid="pipeline-url-detached"]'); const findForkTag = () => wrapper.find('[data-testid="pipeline-url-fork"]'); @@ -23,9 +24,9 @@ describe('Pipeline Url Component', () => { pipeline: { id: 1, path: 'foo', + project: { full_path: `/${projectPath}` }, flags: {}, }, - autoDevopsHelpPath: 'foo', pipelineScheduleUrl: 'foo', }; @@ -33,7 +34,7 @@ describe('Pipeline Url Component', () => { wrapper = shallowMount(PipelineUrlComponent, { propsData: { ...defaultProps, ...props }, provide: { - targetProjectFullPath: 'test/test', + targetProjectFullPath: projectPath, }, }); }; @@ -43,10 +44,10 @@ describe('Pipeline Url Component', () => { wrapper = null; }); - it('should render a table cell', () => { + it('should render pipeline url table cell', () => { createComponent(); - expect(wrapper.attributes('class')).toContain('table-section'); + expect(findTableCell().exists()).toBe(true); }); it('should render a link the provided path and id', () => { @@ -57,6 +58,19 @@ describe('Pipeline Url Component', () => { expect(findPipelineUrlLink().text()).toBe('#1'); }); + it('should not render tags when flags are not set', () => { + createComponent(); + + expect(findStuckTag().exists()).toBe(false); + expect(findLatestTag().exists()).toBe(false); + expect(findYamlTag().exists()).toBe(false); + expect(findAutoDevopsTag().exists()).toBe(false); + expect(findFailureTag().exists()).toBe(false); + expect(findScheduledTag().exists()).toBe(false); + expect(findForkTag().exists()).toBe(false); + expect(findTrainTag().exists()).toBe(false); + }); + it('should render the stuck tag when flag is provided', () => { createComponent({ pipeline: { @@ -96,6 +110,7 @@ describe('Pipeline Url Component', () => { it('should render an autodevops badge when flag is provided', () => { createComponent({ pipeline: { + ...defaultProps.pipeline, flags: { auto_devops: true, }, @@ -103,6 +118,11 @@ describe('Pipeline Url Component', () => { }); expect(trimText(findAutoDevopsTag().text())).toBe('Auto DevOps'); + + expect(findAutoDevopsTagLink().attributes()).toMatchObject({ + href: '/help/topics/autodevops/index.md', + target: '_blank', + }); }); it('should render a detached badge when flag is provided', () => { @@ -147,7 +167,7 @@ describe('Pipeline Url Component', () => { createComponent({ pipeline: { flags: {}, - project: { fullPath: 'test/forked' }, + project: { fullPath: '/test/forked' }, }, }); diff --git a/spec/frontend/pipelines/pipelines_actions_spec.js b/spec/frontend/pipelines/pipelines_actions_spec.js index 1e6c9e50a7e..c4bfec8ae14 100644 --- a/spec/frontend/pipelines/pipelines_actions_spec.js +++ b/spec/frontend/pipelines/pipelines_actions_spec.js @@ -1,11 +1,11 @@ import { GlDropdown, GlDropdownItem } from '@gitlab/ui'; -import { shallowMount, mount } from '@vue/test-utils'; +import { shallowMount } from '@vue/test-utils'; import MockAdapter from 'axios-mock-adapter'; import waitForPromises from 'helpers/wait_for_promises'; import { TEST_HOST } from 'spec/test_constants'; import createFlash from '~/flash'; import axios from '~/lib/utils/axios_utils'; -import PipelinesActions from '~/pipelines/components/pipelines_list/pipelines_actions.vue'; +import PipelinesManualActions from '~/pipelines/components/pipelines_list/pipelines_manual_actions.vue'; import GlCountdown from '~/vue_shared/components/gl_countdown.vue'; jest.mock('~/flash'); @@ -15,7 +15,7 @@ describe('Pipelines Actions dropdown', () => { let mock; const createComponent = (props, mountFn = shallowMount) => { - wrapper = mountFn(PipelinesActions, { + wrapper = mountFn(PipelinesManualActions, { propsData: { ...props, }, @@ -63,10 +63,6 @@ describe('Pipelines Actions dropdown', () => { }); describe('on click', () => { - beforeEach(() => { - createComponent({ actions: mockActions }, mount); - }); - it('makes a request and toggles the loading state', async () => { mock.onPost(mockActions.path).reply(200); diff --git a/spec/frontend/pipelines/pipelines_artifacts_spec.js b/spec/frontend/pipelines/pipelines_artifacts_spec.js index f077833ae16..d4a2db08d97 100644 --- a/spec/frontend/pipelines/pipelines_artifacts_spec.js +++ b/spec/frontend/pipelines/pipelines_artifacts_spec.js @@ -1,24 +1,27 @@ -import { GlDropdown, GlDropdownItem } from '@gitlab/ui'; -import { mount } from '@vue/test-utils'; +import { GlDropdown, GlDropdownItem, GlSprintf } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; import PipelineArtifacts from '~/pipelines/components/pipelines_list/pipelines_artifacts.vue'; describe('Pipelines Artifacts dropdown', () => { let wrapper; const createComponent = () => { - wrapper = mount(PipelineArtifacts, { + wrapper = shallowMount(PipelineArtifacts, { propsData: { artifacts: [ { - name: 'artifact', + name: 'job my-artifact', path: '/download/path', }, { - name: 'artifact two', + name: 'job-2 my-artifact-2', path: '/download/path-two', }, ], }, + stubs: { + GlSprintf, + }, }); }; @@ -39,8 +42,8 @@ describe('Pipelines Artifacts dropdown', () => { }); it('should render a link with the provided path', () => { - expect(findFirstGlDropdownItem().find('a').attributes('href')).toEqual('/download/path'); + expect(findFirstGlDropdownItem().attributes('href')).toBe('/download/path'); - expect(findFirstGlDropdownItem().text()).toContain('artifact'); + expect(findFirstGlDropdownItem().text()).toBe('Download job my-artifact artifact'); }); }); diff --git a/spec/frontend/pipelines/pipelines_spec.js b/spec/frontend/pipelines/pipelines_spec.js index 811303a5624..b04880b43ae 100644 --- a/spec/frontend/pipelines/pipelines_spec.js +++ b/spec/frontend/pipelines/pipelines_spec.js @@ -1,4 +1,4 @@ -import { GlFilteredSearch, GlButton, GlLoadingIcon, GlPagination } from '@gitlab/ui'; +import { GlButton, GlFilteredSearch, GlLoadingIcon, GlPagination } from '@gitlab/ui'; import { mount } from '@vue/test-utils'; import MockAdapter from 'axios-mock-adapter'; import { chunk } from 'lodash'; @@ -18,7 +18,7 @@ import Store from '~/pipelines/stores/pipelines_store'; import NavigationTabs from '~/vue_shared/components/navigation_tabs.vue'; import TablePagination from '~/vue_shared/components/pagination/table_pagination.vue'; -import { pipelineWithStages, stageReply, users, mockSearch, branches } from './mock_data'; +import { stageReply, users, mockSearch, branches } from './mock_data'; jest.mock('~/flash'); @@ -27,6 +27,9 @@ const mockProjectId = '21'; const mockPipelinesEndpoint = `/${mockProjectPath}/pipelines.json`; const mockPipelinesResponse = getJSONFixture('pipelines/pipelines.json'); const mockPipelinesIds = mockPipelinesResponse.pipelines.map(({ id }) => id); +const mockPipelineWithStages = mockPipelinesResponse.pipelines.find( + (p) => p.details.stages && p.details.stages.length, +); describe('Pipelines', () => { let wrapper; @@ -34,8 +37,6 @@ describe('Pipelines', () => { let origWindowLocation; const paths = { - autoDevopsHelpPath: '/help/topics/autodevops/index.md', - helpPagePath: '/help/ci/quick_start/README', emptyStateSvgPath: '/assets/illustrations/pipelines_empty.svg', errorStateSvgPath: '/assets/illustrations/pipelines_failed.svg', noPipelinesSvgPath: '/assets/illustrations/pipelines_pending.svg', @@ -45,8 +46,6 @@ describe('Pipelines', () => { }; const noPermissions = { - autoDevopsHelpPath: '/help/topics/autodevops/index.md', - helpPagePath: '/help/ci/quick_start/README', emptyStateSvgPath: '/assets/illustrations/pipelines_empty.svg', errorStateSvgPath: '/assets/illustrations/pipelines_failed.svg', noPipelinesSvgPath: '/assets/illustrations/pipelines_pending.svg', @@ -70,7 +69,8 @@ describe('Pipelines', () => { const findRunPipelineButton = () => wrapper.findByTestId('run-pipeline-button'); const findCiLintButton = () => wrapper.findByTestId('ci-lint-button'); const findCleanCacheButton = () => wrapper.findByTestId('clear-cache-button'); - const findStagesDropdown = () => wrapper.findByTestId('mini-pipeline-graph-dropdown-toggle'); + const findStagesDropdownToggle = () => + wrapper.find('[data-testid="mini-pipeline-graph-dropdown"] .dropdown-toggle'); const findPipelineUrlLinks = () => wrapper.findAll('[data-testid="pipeline-url-link"]'); const createComponent = (props = defaultProps) => { @@ -539,14 +539,15 @@ describe('Pipelines', () => { }); it('renders empty state', () => { - expect(findEmptyState().find('[data-testid="header-text"]').text()).toBe( - 'Build with confidence', - ); - expect(findEmptyState().find('[data-testid="info-text"]').text()).toContain( + expect(findEmptyState().text()).toContain('Build with confidence'); + expect(findEmptyState().text()).toContain( 'GitLab CI/CD can automatically build, test, and deploy your code.', ); + expect(findEmptyState().find(GlButton).text()).toBe('Get started with CI/CD'); - expect(findEmptyState().find(GlButton).attributes('href')).toBe(paths.helpPagePath); + expect(findEmptyState().find(GlButton).attributes('href')).toBe( + '/help/ci/quick_start/index.md', + ); }); it('does not render tabs nor buttons', () => { @@ -613,14 +614,15 @@ describe('Pipelines', () => { mock.onGet(mockPipelinesEndpoint, { scope: 'all', page: '1' }).reply( 200, { - pipelines: [pipelineWithStages], + pipelines: [mockPipelineWithStages], count: { all: '1' }, }, { 'POLL-INTERVAL': 100, }, ); - mock.onGet(pipelineWithStages.details.stages[0].dropdown_path).reply(200, stageReply); + + mock.onGet(mockPipelineWithStages.details.stages[0].dropdown_path).reply(200, stageReply); createComponent(); @@ -640,7 +642,7 @@ describe('Pipelines', () => { // Mock init a polling cycle wrapper.vm.poll.options.notificationCallback(true); - findStagesDropdown().trigger('click'); + findStagesDropdownToggle().trigger('click'); await waitForPromises(); @@ -650,7 +652,9 @@ describe('Pipelines', () => { }); it('stops polling & restarts polling', async () => { - findStagesDropdown().trigger('click'); + findStagesDropdownToggle().trigger('click'); + + await waitForPromises(); expect(cancelMock).not.toHaveBeenCalled(); expect(stopMock).toHaveBeenCalled(); diff --git a/spec/frontend/pipelines/pipelines_table_row_spec.js b/spec/frontend/pipelines/pipelines_table_row_spec.js index 660651547fc..68d46575081 100644 --- a/spec/frontend/pipelines/pipelines_table_row_spec.js +++ b/spec/frontend/pipelines/pipelines_table_row_spec.js @@ -1,4 +1,5 @@ import { mount } from '@vue/test-utils'; +import waitForPromises from 'helpers/wait_for_promises'; import PipelinesTableRowComponent from '~/pipelines/components/pipelines_list/pipelines_table_row.vue'; import eventHub from '~/pipelines/event_hub'; @@ -9,7 +10,6 @@ describe('Pipelines Table Row', () => { mount(PipelinesTableRowComponent, { propsData: { pipeline, - autoDevopsHelpPath: 'foo', viewType: 'root', }, }); @@ -19,8 +19,6 @@ describe('Pipelines Table Row', () => { let pipelineWithoutAuthor; let pipelineWithoutCommit; - preloadFixtures(jsonFixtureName); - beforeEach(() => { const { pipelines } = getJSONFixture(jsonFixtureName); @@ -149,16 +147,22 @@ describe('Pipelines Table Row', () => { }); describe('stages column', () => { - beforeEach(() => { + const findAllMiniPipelineStages = () => + wrapper.findAll('.table-section:nth-child(5) [data-testid="mini-pipeline-graph-dropdown"]'); + + it('should render an icon for each stage', () => { wrapper = createWrapper(pipeline); + + expect(findAllMiniPipelineStages()).toHaveLength(pipeline.details.stages.length); }); - it('should render an icon for each stage', () => { - expect( - wrapper.findAll( - '.table-section:nth-child(4) [data-testid="mini-pipeline-graph-dropdown-toggle"]', - ).length, - ).toEqual(pipeline.details.stages.length); + it('should not render stages when stages are empty', () => { + const withoutStages = { ...pipeline }; + withoutStages.details = { ...withoutStages.details, stages: null }; + + wrapper = createWrapper(withoutStages); + + expect(findAllMiniPipelineStages()).toHaveLength(0); }); }); @@ -183,9 +187,16 @@ describe('Pipelines Table Row', () => { expect(wrapper.find('.js-pipelines-retry-button').attributes('title')).toMatch('Retry'); expect(wrapper.find('.js-pipelines-cancel-button').exists()).toBe(true); expect(wrapper.find('.js-pipelines-cancel-button').attributes('title')).toMatch('Cancel'); - const dropdownMenu = wrapper.find('.dropdown-menu'); + }); + + it('should render the manual actions', async () => { + const manualActions = wrapper.find('[data-testid="pipelines-manual-actions-dropdown"]'); + + // Click on the dropdown and wait for `lazy` dropdown items + manualActions.find('.dropdown-toggle').trigger('click'); + await waitForPromises(); - expect(dropdownMenu.text()).toContain(scheduledJobAction.name); + expect(manualActions.text()).toContain(scheduledJobAction.name); }); it('emits `retryPipeline` event when retry button is clicked and toggles loading', () => { diff --git a/spec/frontend/pipelines/pipelines_table_spec.js b/spec/frontend/pipelines/pipelines_table_spec.js index fd73d507919..952bea81052 100644 --- a/spec/frontend/pipelines/pipelines_table_spec.js +++ b/spec/frontend/pipelines/pipelines_table_spec.js @@ -1,5 +1,18 @@ +import { GlTable } from '@gitlab/ui'; import { mount } from '@vue/test-utils'; +import { extendedWrapper } from 'helpers/vue_test_utils_helper'; +import PipelineMiniGraph from '~/pipelines/components/pipelines_list/pipeline_mini_graph.vue'; +import PipelineOperations from '~/pipelines/components/pipelines_list/pipeline_operations.vue'; +import PipelineTriggerer from '~/pipelines/components/pipelines_list/pipeline_triggerer.vue'; +import PipelineUrl from '~/pipelines/components/pipelines_list/pipeline_url.vue'; +import PipelinesStatusBadge from '~/pipelines/components/pipelines_list/pipelines_status_badge.vue'; import PipelinesTable from '~/pipelines/components/pipelines_list/pipelines_table.vue'; +import PipelinesTimeago from '~/pipelines/components/pipelines_list/time_ago.vue'; + +import eventHub from '~/pipelines/event_hub'; +import CommitComponent from '~/vue_shared/components/commit.vue'; + +jest.mock('~/pipelines/event_hub'); describe('Pipelines Table', () => { let pipeline; @@ -9,24 +22,52 @@ describe('Pipelines Table', () => { const defaultProps = { pipelines: [], - autoDevopsHelpPath: 'foo', viewType: 'root', }; - const createComponent = (props = defaultProps) => { - wrapper = mount(PipelinesTable, { - propsData: props, - }); + const createMockPipeline = () => { + const { pipelines } = getJSONFixture(jsonFixtureName); + return pipelines.find((p) => p.user !== null && p.commit !== null); }; - const findRows = () => wrapper.findAll('.commit.gl-responsive-table-row'); - preloadFixtures(jsonFixtureName); + const createComponent = (props = {}, flagState = false) => { + wrapper = extendedWrapper( + mount(PipelinesTable, { + propsData: { + ...defaultProps, + ...props, + }, + provide: { + glFeatures: { + newPipelinesTable: flagState, + }, + }, + }), + ); + }; - beforeEach(() => { - const { pipelines } = getJSONFixture(jsonFixtureName); - pipeline = pipelines.find((p) => p.user !== null && p.commit !== null); + const findRows = () => wrapper.findAll('.commit.gl-responsive-table-row'); + const findGlTable = () => wrapper.findComponent(GlTable); + const findStatusBadge = () => wrapper.findComponent(PipelinesStatusBadge); + const findPipelineInfo = () => wrapper.findComponent(PipelineUrl); + const findTriggerer = () => wrapper.findComponent(PipelineTriggerer); + const findCommit = () => wrapper.findComponent(CommitComponent); + const findPipelineMiniGraph = () => wrapper.findComponent(PipelineMiniGraph); + const findTimeAgo = () => wrapper.findComponent(PipelinesTimeago); + const findActions = () => wrapper.findComponent(PipelineOperations); + + const findLegacyTable = () => wrapper.findByTestId('legacy-ci-table'); + const findTableRows = () => wrapper.findAll('[data-testid="pipeline-table-row"]'); + const findStatusTh = () => wrapper.findByTestId('status-th'); + const findPipelineTh = () => wrapper.findByTestId('pipeline-th'); + const findTriggererTh = () => wrapper.findByTestId('triggerer-th'); + const findCommitTh = () => wrapper.findByTestId('commit-th'); + const findStagesTh = () => wrapper.findByTestId('stages-th'); + const findTimeAgoTh = () => wrapper.findByTestId('timeago-th'); + const findActionsTh = () => wrapper.findByTestId('actions-th'); - createComponent(); + beforeEach(() => { + pipeline = createMockPipeline(); }); afterEach(() => { @@ -34,33 +75,161 @@ describe('Pipelines Table', () => { wrapper = null; }); - describe('table', () => { - it('should render a table', () => { - expect(wrapper.classes()).toContain('ci-table'); + describe('table with feature flag off', () => { + describe('renders the table correctly', () => { + beforeEach(() => { + createComponent(); + }); + + it('should render a table', () => { + expect(wrapper.classes()).toContain('ci-table'); + }); + + it('should render table head with correct columns', () => { + expect(wrapper.find('.table-section.js-pipeline-status').text()).toEqual('Status'); + + expect(wrapper.find('.table-section.js-pipeline-info').text()).toEqual('Pipeline'); + + expect(wrapper.find('.table-section.js-pipeline-commit').text()).toEqual('Commit'); + + expect(wrapper.find('.table-section.js-pipeline-stages').text()).toEqual('Stages'); + }); }); - it('should render table head with correct columns', () => { - expect(wrapper.find('.table-section.js-pipeline-status').text()).toEqual('Status'); + describe('without data', () => { + it('should render an empty table', () => { + createComponent(); - expect(wrapper.find('.table-section.js-pipeline-info').text()).toEqual('Pipeline'); + expect(findRows()).toHaveLength(0); + }); + }); - expect(wrapper.find('.table-section.js-pipeline-commit').text()).toEqual('Commit'); + describe('with data', () => { + it('should render rows', () => { + createComponent({ pipelines: [pipeline], viewType: 'root' }); - expect(wrapper.find('.table-section.js-pipeline-stages').text()).toEqual('Stages'); + expect(findRows()).toHaveLength(1); + }); }); }); - describe('without data', () => { - it('should render an empty table', () => { - expect(findRows()).toHaveLength(0); + describe('table with feature flag on', () => { + beforeEach(() => { + createComponent({ pipelines: [pipeline], viewType: 'root' }, true); + }); + + it('displays new table', () => { + expect(findGlTable().exists()).toBe(true); + expect(findLegacyTable().exists()).toBe(false); + }); + + it('should render table head with correct columns', () => { + expect(findStatusTh().text()).toBe('Status'); + expect(findPipelineTh().text()).toBe('Pipeline'); + expect(findTriggererTh().text()).toBe('Triggerer'); + expect(findCommitTh().text()).toBe('Commit'); + expect(findStagesTh().text()).toBe('Stages'); + expect(findTimeAgoTh().text()).toBe('Duration'); + expect(findActionsTh().text()).toBe('Actions'); + }); + + it('should display a table row', () => { + expect(findTableRows()).toHaveLength(1); }); - }); - describe('with data', () => { - it('should render rows', () => { - createComponent({ pipelines: [pipeline], autoDevopsHelpPath: 'foo', viewType: 'root' }); + describe('status cell', () => { + it('should render a status badge', () => { + expect(findStatusBadge().exists()).toBe(true); + }); + + it('should render status badge with correct path', () => { + expect(findStatusBadge().attributes('href')).toBe(pipeline.path); + }); + }); + + describe('pipeline cell', () => { + it('should render pipeline information', () => { + expect(findPipelineInfo().exists()).toBe(true); + }); + + it('should display the pipeline id', () => { + expect(findPipelineInfo().text()).toContain(`#${pipeline.id}`); + }); + }); + + describe('triggerer cell', () => { + it('should render the pipeline triggerer', () => { + expect(findTriggerer().exists()).toBe(true); + }); + }); + + describe('commit cell', () => { + it('should render commit information', () => { + expect(findCommit().exists()).toBe(true); + }); + + it('should display and link to commit', () => { + expect(findCommit().text()).toContain(pipeline.commit.short_id); + expect(findCommit().props('commitUrl')).toBe(pipeline.commit.commit_path); + }); + + it('should display the commit author', () => { + expect(findCommit().props('author')).toEqual(pipeline.commit.author); + }); + }); + + describe('stages cell', () => { + it('should render a pipeline mini graph', () => { + expect(findPipelineMiniGraph().exists()).toBe(true); + }); + + it('should render the right number of stages', () => { + const stagesLength = pipeline.details.stages.length; + expect( + findPipelineMiniGraph().findAll('[data-testid="mini-pipeline-graph-dropdown"]'), + ).toHaveLength(stagesLength); + }); + + describe('when pipeline does not have stages', () => { + beforeEach(() => { + pipeline = createMockPipeline(); + pipeline.details.stages = null; + + createComponent({ pipelines: [pipeline] }, true); + }); + + it('stages are not rendered', () => { + expect(findPipelineMiniGraph().exists()).toBe(false); + }); + }); + + it('should not update dropdown', () => { + expect(findPipelineMiniGraph().props('updateDropdown')).toBe(false); + }); + + it('when update graph dropdown is set, should update graph dropdown', () => { + createComponent({ pipelines: [pipeline], updateGraphDropdown: true }, true); + + expect(findPipelineMiniGraph().props('updateDropdown')).toBe(true); + }); + + it('when action request is complete, should refresh table', () => { + findPipelineMiniGraph().vm.$emit('pipelineActionRequestComplete'); + + expect(eventHub.$emit).toHaveBeenCalledWith('refreshPipelinesTable'); + }); + }); + + describe('duration cell', () => { + it('should render duration information', () => { + expect(findTimeAgo().exists()).toBe(true); + }); + }); - expect(findRows()).toHaveLength(1); + describe('operations cell', () => { + it('should render pipeline operations', () => { + expect(findActions().exists()).toBe(true); + }); }); }); }); diff --git a/spec/frontend/pipelines/stage_spec.js b/spec/frontend/pipelines/stage_spec.js deleted file mode 100644 index 87b43558252..00000000000 --- a/spec/frontend/pipelines/stage_spec.js +++ /dev/null @@ -1,297 +0,0 @@ -import 'bootstrap/js/dist/dropdown'; -import { GlDropdown } from '@gitlab/ui'; -import { mount } from '@vue/test-utils'; -import MockAdapter from 'axios-mock-adapter'; -import $ from 'jquery'; -import axios from '~/lib/utils/axios_utils'; -import StageComponent from '~/pipelines/components/pipelines_list/stage.vue'; -import eventHub from '~/pipelines/event_hub'; -import { stageReply } from './mock_data'; - -describe('Pipelines stage component', () => { - let wrapper; - let mock; - let glFeatures; - - const defaultProps = { - stage: { - status: { - group: 'success', - icon: 'status_success', - title: 'success', - }, - dropdown_path: 'path.json', - }, - updateDropdown: false, - }; - - const createComponent = (props = {}) => { - wrapper = mount(StageComponent, { - attachTo: document.body, - propsData: { - ...defaultProps, - ...props, - }, - provide: { - glFeatures, - }, - }); - }; - - beforeEach(() => { - mock = new MockAdapter(axios); - jest.spyOn(eventHub, '$emit'); - glFeatures = {}; - }); - - afterEach(() => { - wrapper.destroy(); - wrapper = null; - - eventHub.$emit.mockRestore(); - mock.restore(); - }); - - describe('when ci_mini_pipeline_gl_dropdown feature flag is disabled', () => { - const isDropdownOpen = () => wrapper.classes('show'); - - describe('default', () => { - beforeEach(() => { - createComponent(); - }); - - it('should render a dropdown with the status icon', () => { - expect(wrapper.attributes('class')).toEqual('dropdown'); - expect(wrapper.find('svg').exists()).toBe(true); - expect(wrapper.find('button').attributes('data-toggle')).toEqual('dropdown'); - }); - }); - - describe('with successful request', () => { - beforeEach(() => { - mock.onGet('path.json').reply(200, stageReply); - createComponent(); - }); - - it('should render the received data and emit `clickedDropdown` event', async () => { - wrapper.find('button').trigger('click'); - - await axios.waitForAll(); - expect(wrapper.find('.js-builds-dropdown-container ul').text()).toContain( - stageReply.latest_statuses[0].name, - ); - - expect(eventHub.$emit).toHaveBeenCalledWith('clickedDropdown'); - }); - }); - - it('when request fails should close the dropdown', async () => { - mock.onGet('path.json').reply(500); - createComponent(); - wrapper.find({ ref: 'dropdown' }).trigger('click'); - - expect(isDropdownOpen()).toBe(true); - - wrapper.find('button').trigger('click'); - await axios.waitForAll(); - - expect(isDropdownOpen()).toBe(false); - }); - - describe('update endpoint correctly', () => { - beforeEach(() => { - const copyStage = { ...stageReply }; - copyStage.latest_statuses[0].name = 'this is the updated content'; - mock.onGet('bar.json').reply(200, copyStage); - createComponent({ - stage: { - status: { - group: 'running', - icon: 'status_running', - title: 'running', - }, - dropdown_path: 'bar.json', - }, - }); - return axios.waitForAll(); - }); - - it('should update the stage to request the new endpoint provided', async () => { - wrapper.find('button').trigger('click'); - await axios.waitForAll(); - - expect(wrapper.find('.js-builds-dropdown-container ul').text()).toContain( - 'this is the updated content', - ); - }); - }); - - describe('pipelineActionRequestComplete', () => { - beforeEach(() => { - mock.onGet('path.json').reply(200, stageReply); - mock.onPost(`${stageReply.latest_statuses[0].status.action.path}.json`).reply(200); - }); - - const clickCiAction = async () => { - wrapper.find('button').trigger('click'); - await axios.waitForAll(); - - wrapper.find('.js-ci-action').trigger('click'); - await axios.waitForAll(); - }; - - describe('within pipeline table', () => { - it('emits `refreshPipelinesTable` event when `pipelineActionRequestComplete` is triggered', async () => { - createComponent({ type: 'PIPELINES_TABLE' }); - - await clickCiAction(); - - expect(eventHub.$emit).toHaveBeenCalledWith('refreshPipelinesTable'); - }); - }); - - describe('in MR widget', () => { - beforeEach(() => { - jest.spyOn($.fn, 'dropdown'); - }); - - it('closes the dropdown when `pipelineActionRequestComplete` is triggered', async () => { - createComponent(); - - await clickCiAction(); - - expect($.fn.dropdown).toHaveBeenCalledWith('toggle'); - }); - }); - }); - }); - - describe('when ci_mini_pipeline_gl_dropdown feature flag is enabled', () => { - const findDropdown = () => wrapper.find(GlDropdown); - const findDropdownToggle = () => wrapper.find('button.gl-dropdown-toggle'); - const findDropdownMenu = () => - wrapper.find('[data-testid="mini-pipeline-graph-dropdown-menu-list"]'); - const findCiActionBtn = () => wrapper.find('.js-ci-action'); - - const openGlDropdown = () => { - findDropdownToggle().trigger('click'); - return new Promise((resolve) => { - wrapper.vm.$root.$on('bv::dropdown::show', resolve); - }); - }; - - beforeEach(() => { - glFeatures = { ciMiniPipelineGlDropdown: true }; - }); - - describe('default', () => { - beforeEach(() => { - createComponent(); - }); - - it('should render a dropdown with the status icon', () => { - expect(findDropdown().exists()).toBe(true); - expect(findDropdownToggle().classes('gl-dropdown-toggle')).toEqual(true); - expect(wrapper.find('[data-testid="status_success_borderless-icon"]').exists()).toBe(true); - }); - }); - - describe('with successful request', () => { - beforeEach(() => { - mock.onGet('path.json').reply(200, stageReply); - createComponent(); - }); - - it('should render the received data and emit `clickedDropdown` event', async () => { - await openGlDropdown(); - await axios.waitForAll(); - - expect(findDropdownMenu().text()).toContain(stageReply.latest_statuses[0].name); - expect(eventHub.$emit).toHaveBeenCalledWith('clickedDropdown'); - }); - }); - - it('when request fails should close the dropdown', async () => { - mock.onGet('path.json').reply(500); - - createComponent(); - - await openGlDropdown(); - await axios.waitForAll(); - - expect(findDropdown().classes('show')).toBe(false); - }); - - describe('update endpoint correctly', () => { - beforeEach(async () => { - const copyStage = { ...stageReply }; - copyStage.latest_statuses[0].name = 'this is the updated content'; - mock.onGet('bar.json').reply(200, copyStage); - createComponent({ - stage: { - status: { - group: 'running', - icon: 'status_running', - title: 'running', - }, - dropdown_path: 'bar.json', - }, - }); - await axios.waitForAll(); - }); - - it('should update the stage to request the new endpoint provided', async () => { - await openGlDropdown(); - await axios.waitForAll(); - - expect(findDropdownMenu().text()).toContain('this is the updated content'); - }); - }); - - describe('pipelineActionRequestComplete', () => { - beforeEach(() => { - mock.onGet('path.json').reply(200, stageReply); - mock.onPost(`${stageReply.latest_statuses[0].status.action.path}.json`).reply(200); - }); - - const clickCiAction = async () => { - await openGlDropdown(); - await axios.waitForAll(); - - findCiActionBtn().trigger('click'); - await axios.waitForAll(); - }; - - describe('within pipeline table', () => { - beforeEach(() => { - createComponent({ type: 'PIPELINES_TABLE' }); - }); - - it('emits `refreshPipelinesTable` event when `pipelineActionRequestComplete` is triggered', async () => { - await clickCiAction(); - - expect(eventHub.$emit).toHaveBeenCalledWith('refreshPipelinesTable'); - }); - }); - - describe('in MR widget', () => { - beforeEach(() => { - jest.spyOn($.fn, 'dropdown'); - createComponent(); - }); - - it('closes the dropdown when `pipelineActionRequestComplete` is triggered', async () => { - const hidden = jest.fn(); - - wrapper.vm.$root.$on('bv::dropdown::hide', hidden); - - expect(hidden).toHaveBeenCalledTimes(0); - - await clickCiAction(); - - expect(hidden).toHaveBeenCalledTimes(1); - }); - }); - }); - }); -}); diff --git a/spec/frontend/pipelines/time_ago_spec.js b/spec/frontend/pipelines/time_ago_spec.js index 55a19ef5165..93aeb049434 100644 --- a/spec/frontend/pipelines/time_ago_spec.js +++ b/spec/frontend/pipelines/time_ago_spec.js @@ -8,7 +8,11 @@ describe('Timeago component', () => { const createComponent = (props = {}) => { wrapper = shallowMount(TimeAgo, { propsData: { - ...props, + pipeline: { + details: { + ...props, + }, + }, }, data() { return { @@ -25,10 +29,11 @@ describe('Timeago component', () => { const duration = () => wrapper.find('.duration'); const finishedAt = () => wrapper.find('.finished-at'); + const findInProgress = () => wrapper.find('[data-testid="pipeline-in-progress"]'); describe('with duration', () => { beforeEach(() => { - createComponent({ duration: 10, finishedTime: '' }); + createComponent({ duration: 10, finished_at: '' }); }); it('should render duration and timer svg', () => { @@ -41,7 +46,7 @@ describe('Timeago component', () => { describe('without duration', () => { beforeEach(() => { - createComponent({ duration: 0, finishedTime: '' }); + createComponent({ duration: 0, finished_at: '' }); }); it('should not render duration and timer svg', () => { @@ -51,7 +56,7 @@ describe('Timeago component', () => { describe('with finishedTime', () => { beforeEach(() => { - createComponent({ duration: 0, finishedTime: '2017-04-26T12:40:23.277Z' }); + createComponent({ duration: 0, finished_at: '2017-04-26T12:40:23.277Z' }); }); it('should render time and calendar icon', () => { @@ -66,11 +71,28 @@ describe('Timeago component', () => { describe('without finishedTime', () => { beforeEach(() => { - createComponent({ duration: 0, finishedTime: '' }); + createComponent({ duration: 0, finished_at: '' }); }); it('should not render time and calendar icon', () => { expect(finishedAt().exists()).toBe(false); }); }); + + describe('in progress', () => { + it.each` + durationTime | finishedAtTime | shouldShow + ${10} | ${'2017-04-26T12:40:23.277Z'} | ${false} + ${10} | ${''} | ${false} + ${0} | ${'2017-04-26T12:40:23.277Z'} | ${false} + ${0} | ${''} | ${true} + `( + 'progress state shown: $shouldShow when pipeline duration is $durationTime and finished_at is $finishedAtTime', + ({ durationTime, finishedAtTime, shouldShow }) => { + createComponent({ duration: durationTime, finished_at: finishedAtTime }); + + expect(findInProgress().exists()).toBe(shouldShow); + }, + ); + }); }); |