diff options
Diffstat (limited to 'spec/frontend/pipeline_editor')
12 files changed, 597 insertions, 92 deletions
diff --git a/spec/frontend/pipeline_editor/components/commit/commit_form_spec.js b/spec/frontend/pipeline_editor/components/commit/commit_form_spec.js index 8040c9d701c..23219042008 100644 --- a/spec/frontend/pipeline_editor/components/commit/commit_form_spec.js +++ b/spec/frontend/pipeline_editor/components/commit/commit_form_spec.js @@ -5,6 +5,9 @@ import CommitForm from '~/pipeline_editor/components/commit/commit_form.vue'; import { mockCommitMessage, mockDefaultBranch } from '../../mock_data'; +const scrollIntoViewMock = jest.fn(); +HTMLElement.prototype.scrollIntoView = scrollIntoViewMock; + describe('Pipeline Editor | Commit Form', () => { let wrapper; @@ -113,4 +116,20 @@ describe('Pipeline Editor | Commit Form', () => { expect(findSubmitBtn().attributes('disabled')).toBe('disabled'); }); }); + + describe('when scrollToCommitForm becomes true', () => { + beforeEach(async () => { + createComponent(); + wrapper.setProps({ scrollToCommitForm: true }); + await wrapper.vm.$nextTick(); + }); + + it('scrolls into view', () => { + expect(scrollIntoViewMock).toHaveBeenCalledWith({ behavior: 'smooth' }); + }); + + it('emits "scrolled-to-commit-form"', () => { + expect(wrapper.emitted()['scrolled-to-commit-form']).toBeTruthy(); + }); + }); }); diff --git a/spec/frontend/pipeline_editor/components/commit/commit_section_spec.js b/spec/frontend/pipeline_editor/components/commit/commit_section_spec.js index 2f934898ef1..efc345d8877 100644 --- a/spec/frontend/pipeline_editor/components/commit/commit_section_spec.js +++ b/spec/frontend/pipeline_editor/components/commit/commit_section_spec.js @@ -52,6 +52,7 @@ describe('Pipeline Editor | Commit section', () => { const defaultProps = { ciFileContent: mockCiYml, commitSha: mockCommitSha, + isNewCiConfigFile: false, }; const createComponent = ({ props = {}, options = {}, provide = {} } = {}) => { @@ -72,7 +73,6 @@ describe('Pipeline Editor | Commit section', () => { data() { return { currentBranch: mockDefaultBranch, - isNewCiConfigFile: Boolean(options?.isNewCiConfigfile), }; }, mocks: { @@ -115,7 +115,7 @@ describe('Pipeline Editor | Commit section', () => { describe('when the user commits a new file', () => { beforeEach(async () => { - createComponent({ options: { isNewCiConfigfile: true } }); + createComponent({ props: { isNewCiConfigFile: true } }); await submitCommit(); }); @@ -277,4 +277,16 @@ describe('Pipeline Editor | Commit section', () => { expect(wrapper.emitted('resetContent')).toHaveLength(1); }); }); + + it('sets listeners on commit form', () => { + const handler = jest.fn(); + createComponent({ options: { listeners: { event: handler } } }); + findCommitForm().vm.$emit('event'); + expect(handler).toHaveBeenCalled(); + }); + + it('passes down scroll-to-commit-form prop to commit form', () => { + createComponent({ props: { 'scroll-to-commit-form': true } }); + expect(findCommitForm().props('scrollToCommitForm')).toBe(true); + }); }); diff --git a/spec/frontend/pipeline_editor/components/drawer/pipeline_editor_drawer_spec.js b/spec/frontend/pipeline_editor/components/drawer/pipeline_editor_drawer_spec.js index 1b68cd3dc43..4df7768b035 100644 --- a/spec/frontend/pipeline_editor/components/drawer/pipeline_editor_drawer_spec.js +++ b/spec/frontend/pipeline_editor/components/drawer/pipeline_editor_drawer_spec.js @@ -1,6 +1,7 @@ import { GlButton } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import { nextTick } from 'vue'; +import { stubExperiments } from 'helpers/experimentation_helper'; import { useLocalStorageSpy } from 'helpers/local_storage_helper'; import FirstPipelineCard from '~/pipeline_editor/components/drawer/cards/first_pipeline_card.vue'; import GettingStartedCard from '~/pipeline_editor/components/drawer/cards/getting_started_card.vue'; @@ -33,19 +34,41 @@ describe('Pipeline editor drawer', () => { const clickToggleBtn = async () => findToggleBtn().vm.$emit('click'); + const originalObjects = []; + + beforeEach(() => { + originalObjects.push(window.gon, window.gl); + stubExperiments({ pipeline_editor_walkthrough: 'control' }); + }); + afterEach(() => { wrapper.destroy(); localStorage.clear(); + [window.gon, window.gl] = originalObjects; }); - it('it sets the drawer to be opened by default', async () => { - createComponent(); - - expect(findDrawerContent().exists()).toBe(false); - - await nextTick(); + describe('default expanded state', () => { + describe('when experiment control', () => { + it('sets the drawer to be opened by default', async () => { + createComponent(); + expect(findDrawerContent().exists()).toBe(false); + await nextTick(); + expect(findDrawerContent().exists()).toBe(true); + }); + }); - expect(findDrawerContent().exists()).toBe(true); + describe('when experiment candidate', () => { + beforeEach(() => { + stubExperiments({ pipeline_editor_walkthrough: 'candidate' }); + }); + + it('sets the drawer to be closed by default', async () => { + createComponent(); + expect(findDrawerContent().exists()).toBe(false); + await nextTick(); + expect(findDrawerContent().exists()).toBe(false); + }); + }); }); describe('when the drawer is collapsed', () => { diff --git a/spec/frontend/pipeline_editor/components/file-nav/branch_switcher_spec.js b/spec/frontend/pipeline_editor/components/file-nav/branch_switcher_spec.js index b5881790b0b..6532c4e289d 100644 --- a/spec/frontend/pipeline_editor/components/file-nav/branch_switcher_spec.js +++ b/spec/frontend/pipeline_editor/components/file-nav/branch_switcher_spec.js @@ -36,8 +36,9 @@ describe('Pipeline editor branch switcher', () => { let mockLastCommitBranchQuery; const createComponent = ( - { currentBranch, isQueryLoading, mountFn, options } = { + { currentBranch, isQueryLoading, mountFn, options, props } = { currentBranch: mockDefaultBranch, + hasUnsavedChanges: false, isQueryLoading: false, mountFn: shallowMount, options: {}, @@ -45,6 +46,7 @@ describe('Pipeline editor branch switcher', () => { ) => { wrapper = mountFn(BranchSwitcher, { propsData: { + ...props, paginationLimit: mockBranchPaginationLimit, }, provide: { @@ -70,7 +72,7 @@ describe('Pipeline editor branch switcher', () => { }); }; - const createComponentWithApollo = (mountFn = shallowMount) => { + const createComponentWithApollo = ({ mountFn = shallowMount, props = {} } = {}) => { const handlers = [[getAvailableBranchesQuery, mockAvailableBranchQuery]]; const resolvers = { Query: { @@ -86,6 +88,7 @@ describe('Pipeline editor branch switcher', () => { createComponent({ mountFn, + props, options: { localVue, apolloProvider: mockApollo, @@ -138,8 +141,8 @@ describe('Pipeline editor branch switcher', () => { createComponentWithApollo(); }); - it('does not render dropdown', () => { - expect(findDropdown().exists()).toBe(false); + it('disables the dropdown', () => { + expect(findDropdown().props('disabled')).toBe(true); }); }); @@ -149,7 +152,7 @@ describe('Pipeline editor branch switcher', () => { availableBranches: mockProjectBranches, currentBranch: mockDefaultBranch, }); - createComponentWithApollo(mount); + createComponentWithApollo({ mountFn: mount }); await waitForPromises(); }); @@ -186,7 +189,7 @@ describe('Pipeline editor branch switcher', () => { }); it('does not render dropdown', () => { - expect(findDropdown().exists()).toBe(false); + expect(findDropdown().props('disabled')).toBe(true); }); it('shows an error message', () => { @@ -201,7 +204,7 @@ describe('Pipeline editor branch switcher', () => { availableBranches: mockProjectBranches, currentBranch: mockDefaultBranch, }); - createComponentWithApollo(mount); + createComponentWithApollo({ mountFn: mount }); await waitForPromises(); }); @@ -247,6 +250,23 @@ describe('Pipeline editor branch switcher', () => { expect(wrapper.emitted('refetchContent')).toBeUndefined(); }); + + describe('with unsaved changes', () => { + beforeEach(async () => { + createComponentWithApollo({ mountFn: mount, props: { hasUnsavedChanges: true } }); + await waitForPromises(); + }); + + it('emits `select-branch` event and does not switch branch', async () => { + expect(wrapper.emitted('select-branch')).toBeUndefined(); + + const branch = findDropdownItems().at(1); + await branch.vm.$emit('click'); + + expect(wrapper.emitted('select-branch')).toEqual([[branch.text()]]); + expect(wrapper.emitted('refetchContent')).toBeUndefined(); + }); + }); }); describe('when searching', () => { @@ -255,7 +275,7 @@ describe('Pipeline editor branch switcher', () => { availableBranches: mockProjectBranches, currentBranch: mockDefaultBranch, }); - createComponentWithApollo(mount); + createComponentWithApollo({ mountFn: mount }); await waitForPromises(); }); @@ -429,7 +449,7 @@ describe('Pipeline editor branch switcher', () => { availableBranches: mockProjectBranches, currentBranch: mockDefaultBranch, }); - createComponentWithApollo(mount); + createComponentWithApollo({ mountFn: mount }); await waitForPromises(); await createNewBranch(); }); diff --git a/spec/frontend/pipeline_editor/components/header/pipeline_status_spec.js b/spec/frontend/pipeline_editor/components/header/pipeline_status_spec.js index 44656b2b67d..29ab52bde8f 100644 --- a/spec/frontend/pipeline_editor/components/header/pipeline_status_spec.js +++ b/spec/frontend/pipeline_editor/components/header/pipeline_status_spec.js @@ -16,7 +16,7 @@ describe('Pipeline Status', () => { let mockApollo; let mockPipelineQuery; - const createComponentWithApollo = (glFeatures = {}) => { + const createComponentWithApollo = () => { const handlers = [[getPipelineQuery, mockPipelineQuery]]; mockApollo = createMockApollo(handlers); @@ -27,7 +27,6 @@ describe('Pipeline Status', () => { commitSha: mockCommitSha, }, provide: { - glFeatures, projectFullPath: mockProjectFullPath, }, stubs: { GlLink, GlSprintf }, @@ -40,6 +39,8 @@ describe('Pipeline Status', () => { const findPipelineId = () => wrapper.find('[data-testid="pipeline-id"]'); const findPipelineCommit = () => wrapper.find('[data-testid="pipeline-commit"]'); const findPipelineErrorMsg = () => wrapper.find('[data-testid="pipeline-error-msg"]'); + const findPipelineNotTriggeredErrorMsg = () => + wrapper.find('[data-testid="pipeline-not-triggered-error-msg"]'); const findPipelineLoadingMsg = () => wrapper.find('[data-testid="pipeline-loading-msg"]'); const findPipelineViewBtn = () => wrapper.find('[data-testid="pipeline-view-btn"]'); const findStatusIcon = () => wrapper.find('[data-testid="pipeline-status-icon"]'); @@ -95,17 +96,18 @@ describe('Pipeline Status', () => { it('renders pipeline data', () => { const { id, + commit: { title }, detailedStatus: { detailsPath }, } = mockProjectPipeline().pipeline; expect(findStatusIcon().exists()).toBe(true); expect(findPipelineId().text()).toBe(`#${id.match(/\d+/g)[0]}`); - expect(findPipelineCommit().text()).toBe(mockCommitSha); + expect(findPipelineCommit().text()).toBe(`${mockCommitSha}: ${title}`); expect(findPipelineViewBtn().attributes('href')).toBe(detailsPath); }); - it('does not render the pipeline mini graph', () => { - expect(findPipelineEditorMiniGraph().exists()).toBe(false); + it('renders the pipeline mini graph', () => { + expect(findPipelineEditorMiniGraph().exists()).toBe(true); }); }); @@ -117,7 +119,8 @@ describe('Pipeline Status', () => { await waitForPromises(); }); - it('renders error', () => { + it('renders api error', () => { + expect(findPipelineNotTriggeredErrorMsg().exists()).toBe(false); expect(findIcon().attributes('name')).toBe('warning-solid'); expect(findPipelineErrorMsg().text()).toBe(i18n.fetchError); }); @@ -129,20 +132,22 @@ describe('Pipeline Status', () => { expect(findPipelineViewBtn().exists()).toBe(false); }); }); - }); - describe('when feature flag for pipeline mini graph is enabled', () => { - beforeEach(() => { - mockPipelineQuery.mockResolvedValue({ - data: { project: mockProjectPipeline() }, - }); + describe('when pipeline is null', () => { + beforeEach(() => { + mockPipelineQuery.mockResolvedValue({ + data: { project: { pipeline: null } }, + }); - createComponentWithApollo({ pipelineEditorMiniGraph: true }); - waitForPromises(); - }); + createComponentWithApollo(); + waitForPromises(); + }); - it('renders the pipeline mini graph', () => { - expect(findPipelineEditorMiniGraph().exists()).toBe(true); + it('renders pipeline not triggered error', () => { + expect(findPipelineErrorMsg().exists()).toBe(false); + expect(findIcon().attributes('name')).toBe('information-o'); + expect(findPipelineNotTriggeredErrorMsg().text()).toBe(i18n.pipelineNotTriggeredMsg); + }); }); }); }); diff --git a/spec/frontend/pipeline_editor/components/header/pipline_editor_mini_graph_spec.js b/spec/frontend/pipeline_editor/components/header/pipline_editor_mini_graph_spec.js index 3d7c3c839da..6b9f576917f 100644 --- a/spec/frontend/pipeline_editor/components/header/pipline_editor_mini_graph_spec.js +++ b/spec/frontend/pipeline_editor/components/header/pipline_editor_mini_graph_spec.js @@ -1,22 +1,54 @@ -import { shallowMount } from '@vue/test-utils'; +import { shallowMount, createLocalVue } from '@vue/test-utils'; +import VueApollo from 'vue-apollo'; +import createMockApollo from 'helpers/mock_apollo_helper'; import PipelineEditorMiniGraph from '~/pipeline_editor/components/header/pipeline_editor_mini_graph.vue'; import PipelineMiniGraph from '~/pipelines/components/pipelines_list/pipeline_mini_graph.vue'; -import { mockProjectPipeline } from '../../mock_data'; +import getLinkedPipelinesQuery from '~/projects/commit_box/info/graphql/queries/get_linked_pipelines.query.graphql'; +import { PIPELINE_FAILURE } from '~/pipeline_editor/constants'; +import { mockLinkedPipelines, mockProjectFullPath, mockProjectPipeline } from '../../mock_data'; + +const localVue = createLocalVue(); +localVue.use(VueApollo); describe('Pipeline Status', () => { let wrapper; + let mockApollo; + let mockLinkedPipelinesQuery; - const createComponent = ({ hasStages = true } = {}) => { + const createComponent = ({ hasStages = true, options } = {}) => { wrapper = shallowMount(PipelineEditorMiniGraph, { + provide: { + dataMethod: 'graphql', + projectFullPath: mockProjectFullPath, + }, propsData: { pipeline: mockProjectPipeline({ hasStages }).pipeline, }, + ...options, + }); + }; + + const createComponentWithApollo = (hasStages = true) => { + const handlers = [[getLinkedPipelinesQuery, mockLinkedPipelinesQuery]]; + mockApollo = createMockApollo(handlers); + + createComponent({ + hasStages, + options: { + localVue, + apolloProvider: mockApollo, + }, }); }; const findPipelineMiniGraph = () => wrapper.findComponent(PipelineMiniGraph); + beforeEach(() => { + mockLinkedPipelinesQuery = jest.fn(); + }); + afterEach(() => { + mockLinkedPipelinesQuery.mockReset(); wrapper.destroy(); }); @@ -39,4 +71,38 @@ describe('Pipeline Status', () => { expect(findPipelineMiniGraph().exists()).toBe(false); }); }); + + describe('when querying upstream and downstream pipelines', () => { + describe('when query succeeds', () => { + beforeEach(() => { + mockLinkedPipelinesQuery.mockResolvedValue(mockLinkedPipelines()); + createComponentWithApollo(); + }); + + it('should call the query with the correct variables', () => { + expect(mockLinkedPipelinesQuery).toHaveBeenCalledTimes(1); + expect(mockLinkedPipelinesQuery).toHaveBeenCalledWith({ + fullPath: mockProjectFullPath, + iid: mockProjectPipeline().pipeline.iid, + }); + }); + }); + + describe('when query fails', () => { + beforeEach(() => { + mockLinkedPipelinesQuery.mockRejectedValue(new Error()); + createComponentWithApollo(); + }); + + it('should emit an error event when query fails', async () => { + expect(wrapper.emitted('showError')).toHaveLength(1); + expect(wrapper.emitted('showError')[0]).toEqual([ + { + type: PIPELINE_FAILURE, + reasons: [wrapper.vm.$options.i18n.linkedPipelinesFetchError], + }, + ]); + }); + }); + }); }); diff --git a/spec/frontend/pipeline_editor/components/pipeline_editor_tabs_spec.js b/spec/frontend/pipeline_editor/components/pipeline_editor_tabs_spec.js index 5cf8d47bc23..f6154f50bc0 100644 --- a/spec/frontend/pipeline_editor/components/pipeline_editor_tabs_spec.js +++ b/spec/frontend/pipeline_editor/components/pipeline_editor_tabs_spec.js @@ -1,19 +1,27 @@ -import { GlAlert, GlLoadingIcon } from '@gitlab/ui'; +import { GlAlert, GlLoadingIcon, GlTabs } from '@gitlab/ui'; import { shallowMount, mount } from '@vue/test-utils'; -import { nextTick } from 'vue'; +import Vue, { nextTick } from 'vue'; +import setWindowLocation from 'helpers/set_window_location_helper'; import CiConfigMergedPreview from '~/pipeline_editor/components/editor/ci_config_merged_preview.vue'; +import WalkthroughPopover from '~/pipeline_editor/components/walkthrough_popover.vue'; import CiLint from '~/pipeline_editor/components/lint/ci_lint.vue'; import PipelineEditorTabs from '~/pipeline_editor/components/pipeline_editor_tabs.vue'; import EditorTab from '~/pipeline_editor/components/ui/editor_tab.vue'; +import { stubExperiments } from 'helpers/experimentation_helper'; import { + CREATE_TAB, EDITOR_APP_STATUS_EMPTY, - EDITOR_APP_STATUS_ERROR, EDITOR_APP_STATUS_LOADING, EDITOR_APP_STATUS_INVALID, EDITOR_APP_STATUS_VALID, + MERGED_TAB, + TAB_QUERY_PARAM, + TABS_INDEX, } from '~/pipeline_editor/constants'; import PipelineGraph from '~/pipelines/components/pipeline_graph/pipeline_graph.vue'; -import { mockLintResponse, mockCiYml } from '../mock_data'; +import { mockLintResponse, mockLintResponseWithoutMerged, mockCiYml } from '../mock_data'; + +Vue.config.ignoredElements = ['gl-emoji']; describe('Pipeline editor tabs component', () => { let wrapper; @@ -22,6 +30,7 @@ describe('Pipeline editor tabs component', () => { }; const createComponent = ({ + listeners = {}, props = {}, provide = {}, appStatus = EDITOR_APP_STATUS_VALID, @@ -31,6 +40,7 @@ describe('Pipeline editor tabs component', () => { propsData: { ciConfigData: mockLintResponse, ciFileContent: mockCiYml, + isNewCiConfigFile: true, ...props, }, data() { @@ -43,6 +53,7 @@ describe('Pipeline editor tabs component', () => { TextEditor: MockTextEditor, EditorTab, }, + listeners, }); }; @@ -53,10 +64,12 @@ describe('Pipeline editor tabs component', () => { const findAlert = () => wrapper.findComponent(GlAlert); const findCiLint = () => wrapper.findComponent(CiLint); + const findGlTabs = () => wrapper.findComponent(GlTabs); const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); const findPipelineGraph = () => wrapper.findComponent(PipelineGraph); const findTextEditor = () => wrapper.findComponent(MockTextEditor); const findMergedPreview = () => wrapper.findComponent(CiConfigMergedPreview); + const findWalkthroughPopover = () => wrapper.findComponent(WalkthroughPopover); afterEach(() => { wrapper.destroy(); @@ -137,7 +150,7 @@ describe('Pipeline editor tabs component', () => { describe('when there is a fetch error', () => { beforeEach(() => { - createComponent({ appStatus: EDITOR_APP_STATUS_ERROR }); + createComponent({ props: { ciConfigData: mockLintResponseWithoutMerged } }); }); it('show an error message', () => { @@ -181,4 +194,113 @@ describe('Pipeline editor tabs component', () => { }, ); }); + + describe('default tab based on url query param', () => { + const gitlabUrl = 'https://gitlab.test/ci/editor/'; + const matchObject = { + hostname: 'gitlab.test', + pathname: '/ci/editor/', + search: '', + }; + + it(`is ${CREATE_TAB} if the query param ${TAB_QUERY_PARAM} is not present`, () => { + setWindowLocation(gitlabUrl); + createComponent(); + + expect(window.location).toMatchObject(matchObject); + }); + + it(`is ${CREATE_TAB} tab if the query param ${TAB_QUERY_PARAM} is invalid`, () => { + const queryValue = 'FOO'; + setWindowLocation(`${gitlabUrl}?${TAB_QUERY_PARAM}=${queryValue}`); + createComponent(); + + // If the query param remains unchanged, then we have ignored it. + expect(window.location).toMatchObject({ + ...matchObject, + search: `?${TAB_QUERY_PARAM}=${queryValue}`, + }); + }); + + it('is the tab specified in query param and transform it into an index value', async () => { + setWindowLocation(`${gitlabUrl}?${TAB_QUERY_PARAM}=${MERGED_TAB}`); + createComponent(); + + // If the query param has changed to an index, it means we have synced the + // query with. + expect(window.location).toMatchObject({ + ...matchObject, + search: `?${TAB_QUERY_PARAM}=${TABS_INDEX[MERGED_TAB]}`, + }); + }); + }); + + describe('glTabs', () => { + beforeEach(() => { + createComponent(); + }); + + it('passes the `sync-active-tab-with-query-params` prop', () => { + expect(findGlTabs().props('syncActiveTabWithQueryParams')).toBe(true); + }); + }); + + describe('pipeline_editor_walkthrough experiment', () => { + describe('when in control path', () => { + beforeEach(() => { + stubExperiments({ pipeline_editor_walkthrough: 'control' }); + }); + + it('does not show walkthrough popover', async () => { + createComponent({ mountFn: mount }); + await nextTick(); + expect(findWalkthroughPopover().exists()).toBe(false); + }); + }); + + describe('when in candidate path', () => { + beforeEach(() => { + stubExperiments({ pipeline_editor_walkthrough: 'candidate' }); + }); + + describe('when isNewCiConfigFile prop is true (default)', () => { + beforeEach(async () => { + createComponent({ + mountFn: mount, + }); + await nextTick(); + }); + + it('shows walkthrough popover', async () => { + expect(findWalkthroughPopover().exists()).toBe(true); + }); + }); + + describe('when isNewCiConfigFile prop is false', () => { + it('does not show walkthrough popover', async () => { + createComponent({ props: { isNewCiConfigFile: false }, mountFn: mount }); + await nextTick(); + expect(findWalkthroughPopover().exists()).toBe(false); + }); + }); + }); + }); + + it('sets listeners on walkthrough popover', async () => { + stubExperiments({ pipeline_editor_walkthrough: 'candidate' }); + + const handler = jest.fn(); + + createComponent({ + mountFn: mount, + listeners: { + event: handler, + }, + }); + await nextTick(); + + findWalkthroughPopover().vm.$emit('event'); + + expect(handler).toHaveBeenCalled(); + }); }); diff --git a/spec/frontend/pipeline_editor/components/ui/pipeline_editor_messages_spec.js b/spec/frontend/pipeline_editor/components/ui/pipeline_editor_messages_spec.js index 9f910ed4f9c..a55176ccd79 100644 --- a/spec/frontend/pipeline_editor/components/ui/pipeline_editor_messages_spec.js +++ b/spec/frontend/pipeline_editor/components/ui/pipeline_editor_messages_spec.js @@ -11,6 +11,7 @@ import { DEFAULT_FAILURE, DEFAULT_SUCCESS, LOAD_FAILURE_UNKNOWN, + PIPELINE_FAILURE, } from '~/pipeline_editor/constants'; beforeEach(() => { @@ -65,6 +66,7 @@ describe('Pipeline Editor messages', () => { failureType | message | expectedFailureType ${COMMIT_FAILURE} | ${'failed commit'} | ${COMMIT_FAILURE} ${LOAD_FAILURE_UNKNOWN} | ${'loading failure'} | ${LOAD_FAILURE_UNKNOWN} + ${PIPELINE_FAILURE} | ${'pipeline failure'} | ${PIPELINE_FAILURE} ${'random'} | ${'error without a specified type'} | ${DEFAULT_FAILURE} `('shows a message for $message', ({ failureType, expectedFailureType }) => { createComponent({ failureType, showFailure: true }); diff --git a/spec/frontend/pipeline_editor/components/walkthrough_popover_spec.js b/spec/frontend/pipeline_editor/components/walkthrough_popover_spec.js new file mode 100644 index 00000000000..a9ce89ff521 --- /dev/null +++ b/spec/frontend/pipeline_editor/components/walkthrough_popover_spec.js @@ -0,0 +1,29 @@ +import { mount, shallowMount } from '@vue/test-utils'; +import Vue from 'vue'; +import WalkthroughPopover from '~/pipeline_editor/components/walkthrough_popover.vue'; +import { extendedWrapper } from 'helpers/vue_test_utils_helper'; + +Vue.config.ignoredElements = ['gl-emoji']; + +describe('WalkthroughPopover component', () => { + let wrapper; + + const createComponent = (mountFn = shallowMount) => { + return extendedWrapper(mountFn(WalkthroughPopover)); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + describe('CTA button clicked', () => { + beforeEach(async () => { + wrapper = createComponent(mount); + await wrapper.findByTestId('ctaBtn').trigger('click'); + }); + + it('emits "walkthrough-popover-cta-clicked" event', async () => { + expect(wrapper.emitted()['walkthrough-popover-cta-clicked']).toBeTruthy(); + }); + }); +}); diff --git a/spec/frontend/pipeline_editor/mock_data.js b/spec/frontend/pipeline_editor/mock_data.js index 0b0ff14486e..1bfc5c3b93d 100644 --- a/spec/frontend/pipeline_editor/mock_data.js +++ b/spec/frontend/pipeline_editor/mock_data.js @@ -1,4 +1,4 @@ -import { CI_CONFIG_STATUS_VALID } from '~/pipeline_editor/constants'; +import { CI_CONFIG_STATUS_INVALID, CI_CONFIG_STATUS_VALID } from '~/pipeline_editor/constants'; import { unwrapStagesWithNeeds } from '~/pipelines/components/unwrapping_utils'; export const mockProjectNamespace = 'user1'; @@ -35,6 +35,17 @@ job_build: - echo "build" needs: ["job_test_2"] `; + +export const mockCiTemplateQueryResponse = { + data: { + project: { + ciTemplate: { + content: mockCiYml, + }, + }, + }, +}; + export const mockBlobContentQueryResponse = { data: { project: { repository: { blobs: { nodes: [{ rawBlob: mockCiYml }] } } }, @@ -274,11 +285,14 @@ export const mockProjectPipeline = ({ hasStages = true } = {}) => { return { pipeline: { - commitPath: '/-/commit/aabbccdd', id: 'gid://gitlab/Ci::Pipeline/118', iid: '28', shortSha: mockCommitSha, status: 'SUCCESS', + commit: { + title: 'Update .gitlabe-ci.yml', + webPath: '/-/commit/aabbccdd', + }, detailedStatus: { detailsPath: '/root/sample-ci-project/-/pipelines/118', group: 'success', @@ -290,6 +304,62 @@ export const mockProjectPipeline = ({ hasStages = true } = {}) => { }; }; +export const mockLinkedPipelines = ({ hasDownstream = true, hasUpstream = true } = {}) => { + let upstream = null; + let downstream = { + nodes: [], + __typename: 'PipelineConnection', + }; + + if (hasDownstream) { + downstream = { + nodes: [ + { + id: 'gid://gitlab/Ci::Pipeline/612', + path: '/root/job-log-sections/-/pipelines/612', + project: { name: 'job-log-sections', __typename: 'Project' }, + detailedStatus: { + group: 'success', + icon: 'status_success', + label: 'passed', + __typename: 'DetailedStatus', + }, + __typename: 'Pipeline', + }, + ], + __typename: 'PipelineConnection', + }; + } + + if (hasUpstream) { + upstream = { + id: 'gid://gitlab/Ci::Pipeline/610', + path: '/root/trigger-downstream/-/pipelines/610', + project: { name: 'trigger-downstream', __typename: 'Project' }, + detailedStatus: { + group: 'success', + icon: 'status_success', + label: 'passed', + __typename: 'DetailedStatus', + }, + __typename: 'Pipeline', + }; + } + + return { + data: { + project: { + pipeline: { + path: '/root/ci-project/-/pipelines/790', + downstream, + upstream, + }, + __typename: 'Project', + }, + }, + }; +}; + export const mockLintResponse = { valid: true, mergedYaml: mockCiYml, @@ -326,6 +396,14 @@ export const mockLintResponse = { ], }; +export const mockLintResponseWithoutMerged = { + valid: false, + status: CI_CONFIG_STATUS_INVALID, + errors: ['error'], + warnings: [], + jobs: [], +}; + export const mockJobs = [ { name: 'job_1', diff --git a/spec/frontend/pipeline_editor/pipeline_editor_app_spec.js b/spec/frontend/pipeline_editor/pipeline_editor_app_spec.js index b6713319e69..f6afef595c6 100644 --- a/spec/frontend/pipeline_editor/pipeline_editor_app_spec.js +++ b/spec/frontend/pipeline_editor/pipeline_editor_app_spec.js @@ -1,11 +1,9 @@ -import { GlAlert, GlButton, GlLoadingIcon, GlTabs } from '@gitlab/ui'; +import { GlAlert, GlButton, GlLoadingIcon } from '@gitlab/ui'; import { shallowMount, createLocalVue } from '@vue/test-utils'; import VueApollo from 'vue-apollo'; import createMockApollo from 'helpers/mock_apollo_helper'; import setWindowLocation from 'helpers/set_window_location_helper'; import waitForPromises from 'helpers/wait_for_promises'; -import CommitForm from '~/pipeline_editor/components/commit/commit_form.vue'; -import TextEditor from '~/pipeline_editor/components/editor/text_editor.vue'; import PipelineEditorTabs from '~/pipeline_editor/components/pipeline_editor_tabs.vue'; import PipelineEditorEmptyState from '~/pipeline_editor/components/ui/pipeline_editor_empty_state.vue'; @@ -13,17 +11,21 @@ import PipelineEditorMessages from '~/pipeline_editor/components/ui/pipeline_edi import { COMMIT_SUCCESS, COMMIT_FAILURE } from '~/pipeline_editor/constants'; import getBlobContent from '~/pipeline_editor/graphql/queries/blob_content.graphql'; import getCiConfigData from '~/pipeline_editor/graphql/queries/ci_config.graphql'; -import getPipelineQuery from '~/pipeline_editor/graphql/queries/client/pipeline.graphql'; import getTemplate from '~/pipeline_editor/graphql/queries/get_starter_template.query.graphql'; import getLatestCommitShaQuery from '~/pipeline_editor/graphql/queries/latest_commit_sha.query.graphql'; + +import getPipelineQuery from '~/pipeline_editor/graphql/queries/client/pipeline.graphql'; + import PipelineEditorApp from '~/pipeline_editor/pipeline_editor_app.vue'; import PipelineEditorHome from '~/pipeline_editor/pipeline_editor_home.vue'; + import { mockCiConfigPath, mockCiConfigQueryResponse, mockBlobContentQueryResponse, mockBlobContentQueryResponseNoCiFile, mockCiYml, + mockCiTemplateQueryResponse, mockCommitSha, mockCommitShaResults, mockDefaultBranch, @@ -35,10 +37,6 @@ import { const localVue = createLocalVue(); localVue.use(VueApollo); -const MockSourceEditor = { - template: '<div/>', -}; - const mockProvide = { ciConfigPath: mockCiConfigPath, defaultBranch: mockDefaultBranch, @@ -55,19 +53,15 @@ describe('Pipeline editor app component', () => { let mockLatestCommitShaQuery; let mockPipelineQuery; - const createComponent = ({ blobLoading = false, options = {}, provide = {} } = {}) => { + const createComponent = ({ + blobLoading = false, + options = {}, + provide = {}, + stubs = {}, + } = {}) => { wrapper = shallowMount(PipelineEditorApp, { provide: { ...mockProvide, ...provide }, - stubs: { - GlTabs, - GlButton, - CommitForm, - PipelineEditorHome, - PipelineEditorTabs, - PipelineEditorMessages, - SourceEditor: MockSourceEditor, - PipelineEditorEmptyState, - }, + stubs, data() { return { commitSha: '', @@ -89,7 +83,7 @@ describe('Pipeline editor app component', () => { }); }; - const createComponentWithApollo = async ({ props = {}, provide = {} } = {}) => { + const createComponentWithApollo = async ({ provide = {}, stubs = {} } = {}) => { const handlers = [ [getBlobContent, mockBlobContentData], [getCiConfigData, mockCiConfigData], @@ -97,7 +91,6 @@ describe('Pipeline editor app component', () => { [getLatestCommitShaQuery, mockLatestCommitShaQuery], [getPipelineQuery, mockPipelineQuery], ]; - mockApollo = createMockApollo(handlers); const options = { @@ -105,13 +98,15 @@ describe('Pipeline editor app component', () => { data() { return { currentBranch: mockDefaultBranch, + lastCommitBranch: '', + appStatus: '', }; }, mocks: {}, apolloProvider: mockApollo, }; - createComponent({ props, provide, options }); + createComponent({ provide, stubs, options }); return waitForPromises(); }; @@ -119,7 +114,6 @@ describe('Pipeline editor app component', () => { const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); const findAlert = () => wrapper.findComponent(GlAlert); const findEditorHome = () => wrapper.findComponent(PipelineEditorHome); - const findTextEditor = () => wrapper.findComponent(TextEditor); const findEmptyState = () => wrapper.findComponent(PipelineEditorEmptyState); const findEmptyStateButton = () => wrapper.findComponent(PipelineEditorEmptyState).findComponent(GlButton); @@ -141,7 +135,7 @@ describe('Pipeline editor app component', () => { createComponent({ blobLoading: true }); expect(findLoadingIcon().exists()).toBe(true); - expect(findTextEditor().exists()).toBe(false); + expect(findEditorHome().exists()).toBe(false); }); }); @@ -185,7 +179,11 @@ describe('Pipeline editor app component', () => { describe('when no CI config file exists', () => { beforeEach(async () => { mockBlobContentData.mockResolvedValue(mockBlobContentQueryResponseNoCiFile); - await createComponentWithApollo(); + await createComponentWithApollo({ + stubs: { + PipelineEditorEmptyState, + }, + }); jest .spyOn(wrapper.vm.$apollo.queries.commitSha, 'startPolling') @@ -206,8 +204,12 @@ describe('Pipeline editor app component', () => { it('shows a unkown error message', async () => { const loadUnknownFailureText = 'The CI configuration was not loaded, please try again.'; - mockBlobContentData.mockRejectedValueOnce(new Error('My error!')); - await createComponentWithApollo(); + mockBlobContentData.mockRejectedValueOnce(); + await createComponentWithApollo({ + stubs: { + PipelineEditorMessages, + }, + }); expect(findEmptyState().exists()).toBe(false); @@ -222,15 +224,20 @@ describe('Pipeline editor app component', () => { mockBlobContentData.mockResolvedValue(mockBlobContentQueryResponseNoCiFile); mockLatestCommitShaQuery.mockResolvedValue(mockEmptyCommitShaResults); - await createComponentWithApollo(); + await createComponentWithApollo({ + stubs: { + PipelineEditorHome, + PipelineEditorEmptyState, + }, + }); expect(findEmptyState().exists()).toBe(true); - expect(findTextEditor().exists()).toBe(false); + expect(findEditorHome().exists()).toBe(false); await findEmptyStateButton().vm.$emit('click'); expect(findEmptyState().exists()).toBe(false); - expect(findTextEditor().exists()).toBe(true); + expect(findEditorHome().exists()).toBe(true); }); }); @@ -241,7 +248,7 @@ describe('Pipeline editor app component', () => { describe('and the commit mutation succeeds', () => { beforeEach(async () => { window.scrollTo = jest.fn(); - await createComponentWithApollo(); + await createComponentWithApollo({ stubs: { PipelineEditorMessages } }); findEditorHome().vm.$emit('commit', { type: COMMIT_SUCCESS }); }); @@ -295,7 +302,7 @@ describe('Pipeline editor app component', () => { beforeEach(async () => { window.scrollTo = jest.fn(); - await createComponentWithApollo(); + await createComponentWithApollo({ stubs: { PipelineEditorMessages } }); findEditorHome().vm.$emit('showError', { type: COMMIT_FAILURE, @@ -319,7 +326,7 @@ describe('Pipeline editor app component', () => { beforeEach(async () => { window.scrollTo = jest.fn(); - await createComponentWithApollo(); + await createComponentWithApollo({ stubs: { PipelineEditorMessages } }); findEditorHome().vm.$emit('showError', { type: COMMIT_FAILURE, @@ -342,6 +349,8 @@ describe('Pipeline editor app component', () => { describe('when refetching content', () => { beforeEach(() => { + mockBlobContentData.mockResolvedValue(mockBlobContentQueryResponse); + mockCiConfigData.mockResolvedValue(mockCiConfigQueryResponse); mockLatestCommitShaQuery.mockResolvedValue(mockCommitShaResults); }); @@ -377,7 +386,10 @@ describe('Pipeline editor app component', () => { const originalLocation = window.location.href; beforeEach(() => { + mockBlobContentData.mockResolvedValue(mockBlobContentQueryResponse); + mockCiConfigData.mockResolvedValue(mockCiConfigQueryResponse); mockLatestCommitShaQuery.mockResolvedValue(mockCommitShaResults); + mockGetTemplate.mockResolvedValue(mockCiTemplateQueryResponse); setWindowLocation('?template=Android'); }); @@ -386,7 +398,9 @@ describe('Pipeline editor app component', () => { }); it('renders the given template', async () => { - await createComponentWithApollo(); + await createComponentWithApollo({ + stubs: { PipelineEditorHome, PipelineEditorTabs }, + }); expect(mockGetTemplate).toHaveBeenCalledWith({ projectPath: mockProjectFullPath, @@ -394,7 +408,40 @@ describe('Pipeline editor app component', () => { }); expect(findEmptyState().exists()).toBe(false); - expect(findTextEditor().exists()).toBe(true); + expect(findEditorHome().exists()).toBe(true); + }); + }); + + describe('when add_new_config_file query param is present', () => { + const originalLocation = window.location.href; + + beforeEach(() => { + setWindowLocation('?add_new_config_file=true'); + + mockCiConfigData.mockResolvedValue(mockCiConfigQueryResponse); + }); + + afterEach(() => { + setWindowLocation(originalLocation); + }); + + describe('when CI config file does not exist', () => { + beforeEach(async () => { + mockBlobContentData.mockResolvedValue(mockBlobContentQueryResponseNoCiFile); + mockLatestCommitShaQuery.mockResolvedValue(mockEmptyCommitShaResults); + mockGetTemplate.mockResolvedValue(mockCiTemplateQueryResponse); + + await createComponentWithApollo(); + + jest + .spyOn(wrapper.vm.$apollo.queries.commitSha, 'startPolling') + .mockImplementation(jest.fn()); + }); + + it('skips empty state and shows editor home component', () => { + expect(findEmptyState().exists()).toBe(false); + expect(findEditorHome().exists()).toBe(true); + }); }); }); }); diff --git a/spec/frontend/pipeline_editor/pipeline_editor_home_spec.js b/spec/frontend/pipeline_editor/pipeline_editor_home_spec.js index 335049892ec..6f969546171 100644 --- a/spec/frontend/pipeline_editor/pipeline_editor_home_spec.js +++ b/spec/frontend/pipeline_editor/pipeline_editor_home_spec.js @@ -1,21 +1,25 @@ import { shallowMount } from '@vue/test-utils'; import { nextTick } from 'vue'; - +import { GlModal } from '@gitlab/ui'; import CommitSection from '~/pipeline_editor/components/commit/commit_section.vue'; import PipelineEditorDrawer from '~/pipeline_editor/components/drawer/pipeline_editor_drawer.vue'; import PipelineEditorFileNav from '~/pipeline_editor/components/file_nav/pipeline_editor_file_nav.vue'; +import BranchSwitcher from '~/pipeline_editor/components/file_nav/branch_switcher.vue'; import PipelineEditorHeader from '~/pipeline_editor/components/header/pipeline_editor_header.vue'; import PipelineEditorTabs from '~/pipeline_editor/components/pipeline_editor_tabs.vue'; -import { MERGED_TAB, VISUALIZE_TAB } from '~/pipeline_editor/constants'; +import { MERGED_TAB, VISUALIZE_TAB, CREATE_TAB, LINT_TAB } from '~/pipeline_editor/constants'; import PipelineEditorHome from '~/pipeline_editor/pipeline_editor_home.vue'; import { mockLintResponse, mockCiYml } from './mock_data'; +jest.mock('~/lib/utils/common_utils'); + describe('Pipeline editor home wrapper', () => { let wrapper; - const createComponent = ({ props = {}, glFeatures = {} } = {}) => { + const createComponent = ({ props = {}, glFeatures = {}, data = {}, stubs = {} } = {}) => { wrapper = shallowMount(PipelineEditorHome, { + data: () => data, propsData: { ciConfigData: mockLintResponse, ciFileContent: mockCiYml, @@ -24,22 +28,26 @@ describe('Pipeline editor home wrapper', () => { ...props, }, provide: { + projectFullPath: '', + totalBranches: 19, glFeatures: { ...glFeatures, }, }, + stubs, }); }; + const findBranchSwitcher = () => wrapper.findComponent(BranchSwitcher); const findCommitSection = () => wrapper.findComponent(CommitSection); const findFileNav = () => wrapper.findComponent(PipelineEditorFileNav); + const findModal = () => wrapper.findComponent(GlModal); const findPipelineEditorDrawer = () => wrapper.findComponent(PipelineEditorDrawer); const findPipelineEditorHeader = () => wrapper.findComponent(PipelineEditorHeader); const findPipelineEditorTabs = () => wrapper.findComponent(PipelineEditorTabs); afterEach(() => { wrapper.destroy(); - wrapper = null; }); describe('renders', () => { @@ -68,29 +76,103 @@ describe('Pipeline editor home wrapper', () => { }); }); + describe('modal when switching branch', () => { + describe('when `showSwitchBranchModal` value is false', () => { + beforeEach(() => { + createComponent(); + }); + + it('is not visible', () => { + expect(findModal().exists()).toBe(false); + }); + }); + describe('when `showSwitchBranchModal` value is true', () => { + beforeEach(() => { + createComponent({ + data: { showSwitchBranchModal: true }, + stubs: { PipelineEditorFileNav }, + }); + }); + + it('is visible', () => { + expect(findModal().exists()).toBe(true); + }); + + it('pass down `shouldLoadNewBranch` to the branch switcher when primary is selected', async () => { + expect(findBranchSwitcher().props('shouldLoadNewBranch')).toBe(false); + + await findModal().vm.$emit('primary'); + + expect(findBranchSwitcher().props('shouldLoadNewBranch')).toBe(true); + }); + + it('closes the modal when secondary action is selected', async () => { + expect(findModal().exists()).toBe(true); + + await findModal().vm.$emit('secondary'); + + expect(findModal().exists()).toBe(false); + }); + }); + }); + describe('commit form toggle', () => { beforeEach(() => { createComponent(); }); - it('hides the commit form when in the merged tab', async () => { - expect(findCommitSection().exists()).toBe(true); + it.each` + tab | shouldShow + ${MERGED_TAB} | ${false} + ${VISUALIZE_TAB} | ${false} + ${LINT_TAB} | ${false} + ${CREATE_TAB} | ${true} + `( + 'when the active tab is $tab the commit form is shown: $shouldShow', + async ({ tab, shouldShow }) => { + expect(findCommitSection().exists()).toBe(true); - findPipelineEditorTabs().vm.$emit('set-current-tab', MERGED_TAB); - await nextTick(); - expect(findCommitSection().exists()).toBe(false); - }); + findPipelineEditorTabs().vm.$emit('set-current-tab', tab); + + await nextTick(); - it('shows the form again when leaving the merged tab', async () => { + expect(findCommitSection().exists()).toBe(shouldShow); + }, + ); + + it('shows the commit form again when coming back to the create tab', async () => { expect(findCommitSection().exists()).toBe(true); findPipelineEditorTabs().vm.$emit('set-current-tab', MERGED_TAB); await nextTick(); expect(findCommitSection().exists()).toBe(false); - findPipelineEditorTabs().vm.$emit('set-current-tab', VISUALIZE_TAB); + findPipelineEditorTabs().vm.$emit('set-current-tab', CREATE_TAB); await nextTick(); expect(findCommitSection().exists()).toBe(true); }); }); + + describe('WalkthroughPopover events', () => { + beforeEach(() => { + createComponent(); + }); + + describe('when "walkthrough-popover-cta-clicked" is emitted from pipeline editor tabs', () => { + it('passes down `scrollToCommitForm=true` to commit section', async () => { + expect(findCommitSection().props('scrollToCommitForm')).toBe(false); + await findPipelineEditorTabs().vm.$emit('walkthrough-popover-cta-clicked'); + expect(findCommitSection().props('scrollToCommitForm')).toBe(true); + }); + }); + + describe('when "scrolled-to-commit-form" is emitted from commit section', () => { + it('passes down `scrollToCommitForm=false` to commit section', async () => { + await findPipelineEditorTabs().vm.$emit('walkthrough-popover-cta-clicked'); + expect(findCommitSection().props('scrollToCommitForm')).toBe(true); + await findCommitSection().vm.$emit('scrolled-to-commit-form'); + expect(findCommitSection().props('scrollToCommitForm')).toBe(false); + }); + }); + }); }); |