diff options
Diffstat (limited to 'spec/frontend/pipeline_editor')
17 files changed, 882 insertions, 446 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 aae25a3aa6d..5dae77a4626 100644 --- a/spec/frontend/pipeline_editor/components/commit/commit_form_spec.js +++ b/spec/frontend/pipeline_editor/components/commit/commit_form_spec.js @@ -1,11 +1,11 @@ -import { shallowMount, mount } from '@vue/test-utils'; import { GlFormInput, GlFormTextarea } from '@gitlab/ui'; +import { shallowMount, mount } from '@vue/test-utils'; import CommitForm from '~/pipeline_editor/components/commit/commit_form.vue'; import { mockCommitMessage, mockDefaultBranch } from '../../mock_data'; -describe('~/pipeline_editor/pipeline_editor_app.vue', () => { +describe('Pipeline Editor | Commit Form', () => { let wrapper; const createComponent = ({ props = {} } = {}, mountFn = shallowMount) => { @@ -21,8 +21,8 @@ describe('~/pipeline_editor/pipeline_editor_app.vue', () => { }); }; - const findCommitTextarea = () => wrapper.find(GlFormTextarea); - const findBranchInput = () => wrapper.find(GlFormInput); + const findCommitTextarea = () => wrapper.findComponent(GlFormTextarea); + const findBranchInput = () => wrapper.findComponent(GlFormInput); const findNewMrCheckbox = () => wrapper.find('[data-testid="new-mr-checkbox"]'); const findSubmitBtn = () => wrapper.find('[type="submit"]'); const findCancelBtn = () => wrapper.find('[type="reset"]'); diff --git a/spec/frontend/pipeline_editor/components/commit/commit_section_spec.js b/spec/frontend/pipeline_editor/components/commit/commit_section_spec.js new file mode 100644 index 00000000000..b87ff6ec0de --- /dev/null +++ b/spec/frontend/pipeline_editor/components/commit/commit_section_spec.js @@ -0,0 +1,223 @@ +import { GlFormTextarea, GlFormInput, GlLoadingIcon } from '@gitlab/ui'; +import { mount } from '@vue/test-utils'; +import { objectToQuery, redirectTo } from '~/lib/utils/url_utility'; +import CommitForm from '~/pipeline_editor/components/commit/commit_form.vue'; +import CommitSection from '~/pipeline_editor/components/commit/commit_section.vue'; +import { COMMIT_SUCCESS } from '~/pipeline_editor/constants'; +import commitCreate from '~/pipeline_editor/graphql/mutations/commit_ci_file.mutation.graphql'; + +import { + mockCiConfigPath, + mockCiYml, + mockCommitSha, + mockCommitNextSha, + mockCommitMessage, + mockDefaultBranch, + mockProjectFullPath, + mockNewMergeRequestPath, +} from '../../mock_data'; + +jest.mock('~/lib/utils/url_utility', () => ({ + redirectTo: jest.fn(), + refreshCurrentPage: jest.fn(), + objectToQuery: jest.requireActual('~/lib/utils/url_utility').objectToQuery, + mergeUrlParams: jest.requireActual('~/lib/utils/url_utility').mergeUrlParams, +})); + +const mockVariables = { + projectPath: mockProjectFullPath, + startBranch: mockDefaultBranch, + message: mockCommitMessage, + filePath: mockCiConfigPath, + content: mockCiYml, + lastCommitId: mockCommitSha, +}; + +const mockProvide = { + ciConfigPath: mockCiConfigPath, + defaultBranch: mockDefaultBranch, + projectFullPath: mockProjectFullPath, + newMergeRequestPath: mockNewMergeRequestPath, +}; + +describe('Pipeline Editor | Commit section', () => { + let wrapper; + let mockMutate; + + const defaultProps = { ciFileContent: mockCiYml }; + + const createComponent = ({ props = {}, options = {}, provide = {} } = {}) => { + mockMutate = jest.fn().mockResolvedValue({ + data: { + commitCreate: { + errors: [], + commit: { + sha: mockCommitNextSha, + }, + }, + }, + }); + + wrapper = mount(CommitSection, { + propsData: { ...defaultProps, ...props }, + provide: { ...mockProvide, ...provide }, + data() { + return { + commitSha: mockCommitSha, + }; + }, + mocks: { + $apollo: { + mutate: mockMutate, + }, + }, + attachTo: document.body, + ...options, + }); + }; + + const findCommitForm = () => wrapper.findComponent(CommitForm); + const findCommitBtnLoadingIcon = () => + wrapper.find('[type="submit"]').findComponent(GlLoadingIcon); + + const submitCommit = async ({ + message = mockCommitMessage, + branch = mockDefaultBranch, + openMergeRequest = false, + } = {}) => { + await findCommitForm().findComponent(GlFormTextarea).setValue(message); + await findCommitForm().findComponent(GlFormInput).setValue(branch); + if (openMergeRequest) { + await findCommitForm().find('[data-testid="new-mr-checkbox"]').setChecked(openMergeRequest); + } + await findCommitForm().find('[type="submit"]').trigger('click'); + // Simulate the write to local cache that occurs after a commit + await wrapper.setData({ commitSha: mockCommitNextSha }); + }; + + const cancelCommitForm = async () => { + const findCancelBtn = () => wrapper.find('[type="reset"]'); + await findCancelBtn().trigger('click'); + }; + + beforeEach(() => { + createComponent(); + }); + + afterEach(() => { + mockMutate.mockReset(); + + wrapper.destroy(); + wrapper = null; + }); + + describe('when the user commits changes to the current branch', () => { + beforeEach(async () => { + await submitCommit(); + }); + + it('calls the mutation with the default branch', () => { + expect(mockMutate).toHaveBeenCalledTimes(1); + expect(mockMutate).toHaveBeenCalledWith({ + mutation: commitCreate, + update: expect.any(Function), + variables: { + ...mockVariables, + branch: mockDefaultBranch, + }, + }); + }); + + it('emits an event to communicate the commit was successful', () => { + expect(wrapper.emitted('commit')).toHaveLength(1); + expect(wrapper.emitted('commit')[0]).toEqual([{ type: COMMIT_SUCCESS }]); + }); + + it('shows no saving state', () => { + expect(findCommitBtnLoadingIcon().exists()).toBe(false); + }); + + it('a second commit submits the latest sha, keeping the form updated', async () => { + await submitCommit(); + + expect(mockMutate).toHaveBeenCalledTimes(2); + expect(mockMutate).toHaveBeenCalledWith({ + mutation: commitCreate, + update: expect.any(Function), + variables: { + ...mockVariables, + lastCommitId: mockCommitNextSha, + branch: mockDefaultBranch, + }, + }); + }); + }); + + describe('when the user commits changes to a new branch', () => { + const newBranch = 'new-branch'; + + beforeEach(async () => { + await submitCommit({ + branch: newBranch, + }); + }); + + it('calls the mutation with the new branch', () => { + expect(mockMutate).toHaveBeenCalledWith({ + mutation: commitCreate, + update: expect.any(Function), + variables: { + ...mockVariables, + branch: newBranch, + }, + }); + }); + }); + + describe('when the user commits changes to open a new merge request', () => { + const newBranch = 'new-branch'; + + beforeEach(async () => { + await submitCommit({ + branch: newBranch, + openMergeRequest: true, + }); + }); + + it('redirects to the merge request page with source and target branches', () => { + const branchesQuery = objectToQuery({ + 'merge_request[source_branch]': newBranch, + 'merge_request[target_branch]': mockDefaultBranch, + }); + + expect(redirectTo).toHaveBeenCalledWith(`${mockNewMergeRequestPath}?${branchesQuery}`); + }); + }); + + describe('when the commit is ocurring', () => { + it('shows a saving state', async () => { + mockMutate.mockImplementationOnce(() => { + expect(findCommitBtnLoadingIcon().exists()).toBe(true); + return Promise.resolve(); + }); + + await submitCommit({ + message: mockCommitMessage, + branch: mockDefaultBranch, + openMergeRequest: false, + }); + }); + }); + + describe('when the commit form is cancelled', () => { + beforeEach(async () => { + createComponent(); + }); + + it('emits an event so that it cab be reseted', async () => { + await cancelCommitForm(); + + expect(wrapper.emitted('resetContent')).toHaveLength(1); + }); + }); +}); diff --git a/spec/frontend/pipeline_editor/components/editor/ci_config_merged_preview_spec.js b/spec/frontend/pipeline_editor/components/editor/ci_config_merged_preview_spec.js new file mode 100644 index 00000000000..866069f337b --- /dev/null +++ b/spec/frontend/pipeline_editor/components/editor/ci_config_merged_preview_spec.js @@ -0,0 +1,88 @@ +import { GlAlert, GlIcon } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; + +import { EDITOR_READY_EVENT } from '~/editor/constants'; +import CiConfigMergedPreview from '~/pipeline_editor/components/editor/ci_config_merged_preview.vue'; +import { CI_CONFIG_STATUS_INVALID } from '~/pipeline_editor/constants'; +import { INVALID_CI_CONFIG } from '~/pipelines/constants'; +import { mockLintResponse, mockCiConfigPath } from '../../mock_data'; + +describe('Text editor component', () => { + let wrapper; + + const MockEditorLite = { + template: '<div/>', + props: ['value', 'fileName', 'editorOptions'], + mounted() { + this.$emit(EDITOR_READY_EVENT); + }, + }; + + const createComponent = ({ props = {} } = {}) => { + wrapper = shallowMount(CiConfigMergedPreview, { + propsData: { + ciConfigData: mockLintResponse, + ...props, + }, + provide: { + ciConfigPath: mockCiConfigPath, + }, + stubs: { + EditorLite: MockEditorLite, + }, + }); + }; + + const findAlert = () => wrapper.findComponent(GlAlert); + const findIcon = () => wrapper.findComponent(GlIcon); + const findEditor = () => wrapper.findComponent(MockEditorLite); + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + describe('when status is invalid', () => { + beforeEach(() => { + createComponent({ props: { ciConfigData: { status: CI_CONFIG_STATUS_INVALID } } }); + }); + + it('show an error message', () => { + expect(findAlert().exists()).toBe(true); + expect(findAlert().text()).toBe(wrapper.vm.$options.errorTexts[INVALID_CI_CONFIG]); + }); + + it('hides the editor', () => { + expect(findEditor().exists()).toBe(false); + }); + }); + + describe('when status is valid', () => { + beforeEach(() => { + createComponent(); + }); + + it('shows an information message that the section is not editable', () => { + expect(findIcon().exists()).toBe(true); + expect(wrapper.text()).toContain(wrapper.vm.$options.i18n.viewOnlyMessage); + }); + + it('contains an editor', () => { + expect(findEditor().exists()).toBe(true); + }); + + it('editor contains the value provided', () => { + expect(findEditor().props('value')).toBe(mockLintResponse.mergedYaml); + }); + + it('editor is configured for the CI config path', () => { + expect(findEditor().props('fileName')).toBe(mockCiConfigPath); + }); + + it('editor is readonly', () => { + expect(findEditor().props('editorOptions')).toMatchObject({ + readOnly: true, + }); + }); + }); +}); diff --git a/spec/frontend/pipeline_editor/components/editor/text_editor_spec.js b/spec/frontend/pipeline_editor/components/editor/text_editor_spec.js new file mode 100644 index 00000000000..3bf5a291c69 --- /dev/null +++ b/spec/frontend/pipeline_editor/components/editor/text_editor_spec.js @@ -0,0 +1,120 @@ +import { shallowMount } from '@vue/test-utils'; + +import { EDITOR_READY_EVENT } from '~/editor/constants'; +import TextEditor from '~/pipeline_editor/components/editor/text_editor.vue'; +import { + mockCiConfigPath, + mockCiYml, + mockCommitSha, + mockProjectPath, + mockProjectNamespace, +} from '../../mock_data'; + +describe('Pipeline Editor | Text editor component', () => { + let wrapper; + + let editorReadyListener; + let mockUse; + let mockRegisterCiSchema; + + const MockEditorLite = { + template: '<div/>', + props: ['value', 'fileName'], + mounted() { + this.$emit(EDITOR_READY_EVENT); + }, + methods: { + getEditor: () => ({ + use: mockUse, + registerCiSchema: mockRegisterCiSchema, + }), + }, + }; + + const createComponent = (opts = {}, mountFn = shallowMount) => { + wrapper = mountFn(TextEditor, { + provide: { + projectPath: mockProjectPath, + projectNamespace: mockProjectNamespace, + ciConfigPath: mockCiConfigPath, + }, + attrs: { + value: mockCiYml, + }, + // Simulate graphQL client query result + data() { + return { + commitSha: mockCommitSha, + }; + }, + listeners: { + [EDITOR_READY_EVENT]: editorReadyListener, + }, + stubs: { + EditorLite: MockEditorLite, + }, + ...opts, + }); + }; + + const findEditor = () => wrapper.findComponent(MockEditorLite); + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + + mockUse.mockClear(); + mockRegisterCiSchema.mockClear(); + }); + + describe('template', () => { + beforeEach(() => { + editorReadyListener = jest.fn(); + mockUse = jest.fn(); + mockRegisterCiSchema = jest.fn(); + + createComponent(); + }); + + it('contains an editor', () => { + expect(findEditor().exists()).toBe(true); + }); + + it('editor contains the value provided', () => { + expect(findEditor().props('value')).toBe(mockCiYml); + }); + + it('editor is configured for the CI config path', () => { + expect(findEditor().props('fileName')).toBe(mockCiConfigPath); + }); + + it('bubbles up events', () => { + findEditor().vm.$emit(EDITOR_READY_EVENT); + + expect(editorReadyListener).toHaveBeenCalled(); + }); + }); + + describe('register CI schema', () => { + beforeEach(async () => { + createComponent(); + + // Since the editor will have already mounted, the event will have fired. + // To ensure we properly test this, we clear the mock and re-remit the event. + mockRegisterCiSchema.mockClear(); + mockUse.mockClear(); + + findEditor().vm.$emit(EDITOR_READY_EVENT); + }); + + it('configures editor with syntax highlight', async () => { + expect(mockUse).toHaveBeenCalledTimes(1); + expect(mockRegisterCiSchema).toHaveBeenCalledTimes(1); + expect(mockRegisterCiSchema).toHaveBeenCalledWith({ + projectNamespace: mockProjectNamespace, + projectPath: mockProjectPath, + ref: mockCommitSha, + }); + }); + }); +}); diff --git a/spec/frontend/pipeline_editor/components/header/pipeline_editor_header_spec.js b/spec/frontend/pipeline_editor/components/header/pipeline_editor_header_spec.js new file mode 100644 index 00000000000..df15a6c8e7f --- /dev/null +++ b/spec/frontend/pipeline_editor/components/header/pipeline_editor_header_spec.js @@ -0,0 +1,34 @@ +import { shallowMount } from '@vue/test-utils'; +import PipelineEditorHeader from '~/pipeline_editor/components/header/pipeline_editor_header.vue'; +import ValidationSegment from '~/pipeline_editor/components/header/validation_segment.vue'; + +import { mockLintResponse } from '../../mock_data'; + +describe('Pipeline editor header', () => { + let wrapper; + + const createComponent = () => { + wrapper = shallowMount(PipelineEditorHeader, { + props: { + ciConfigData: mockLintResponse, + isCiConfigDataLoading: false, + }, + }); + }; + + const findValidationSegment = () => wrapper.findComponent(ValidationSegment); + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + describe('template', () => { + beforeEach(() => { + createComponent(); + }); + it('renders the validation segment', () => { + expect(findValidationSegment().exists()).toBe(true); + }); + }); +}); diff --git a/spec/frontend/pipeline_editor/components/info/validation_segment_spec.js b/spec/frontend/pipeline_editor/components/header/validation_segment_spec.js index 8a991d82018..cf1d89e1d7c 100644 --- a/spec/frontend/pipeline_editor/components/info/validation_segment_spec.js +++ b/spec/frontend/pipeline_editor/components/header/validation_segment_spec.js @@ -1,9 +1,11 @@ -import { escape } from 'lodash'; -import { shallowMount } from '@vue/test-utils'; import { GlIcon } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import { escape } from 'lodash'; import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import { sprintf } from '~/locale'; -import ValidationSegment, { i18n } from '~/pipeline_editor/components/info/validation_segment.vue'; +import ValidationSegment, { + i18n, +} from '~/pipeline_editor/components/header/validation_segment.vue'; import { CI_CONFIG_STATUS_INVALID } from '~/pipeline_editor/constants'; import { mockYmlHelpPagePath, mergeUnwrappedCiConfig } from '../../mock_data'; @@ -29,6 +31,11 @@ describe('~/pipeline_editor/components/info/validation_segment.vue', () => { const findLearnMoreLink = () => wrapper.findByTestId('learnMoreLink'); const findValidationMsg = () => wrapper.findByTestId('validationMsg'); + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + it('shows the loading state', () => { createComponent({ loading: true }); diff --git a/spec/frontend/pipeline_editor/components/lint/ci_lint_results_spec.js b/spec/frontend/pipeline_editor/components/lint/ci_lint_results_spec.js index 5e9471376bd..6775433deb9 100644 --- a/spec/frontend/pipeline_editor/components/lint/ci_lint_results_spec.js +++ b/spec/frontend/pipeline_editor/components/lint/ci_lint_results_spec.js @@ -1,7 +1,7 @@ -import { shallowMount, mount } from '@vue/test-utils'; import { GlTable, GlLink } from '@gitlab/ui'; -import CiLintResults from '~/pipeline_editor/components/lint/ci_lint_results.vue'; +import { shallowMount, mount } from '@vue/test-utils'; import { capitalizeFirstCharacter } from '~/lib/utils/text_utility'; +import CiLintResults from '~/pipeline_editor/components/lint/ci_lint_results.vue'; import { mockJobs, mockErrors, mockWarnings } from '../../mock_data'; describe('CI Lint Results', () => { diff --git a/spec/frontend/pipeline_editor/components/lint/ci_lint_spec.js b/spec/frontend/pipeline_editor/components/lint/ci_lint_spec.js index 5ccf4bbdab4..fdddca3d62b 100644 --- a/spec/frontend/pipeline_editor/components/lint/ci_lint_spec.js +++ b/spec/frontend/pipeline_editor/components/lint/ci_lint_spec.js @@ -1,5 +1,5 @@ -import { shallowMount, mount } from '@vue/test-utils'; import { GlAlert, GlLink } from '@gitlab/ui'; +import { shallowMount, mount } from '@vue/test-utils'; import CiLint from '~/pipeline_editor/components/lint/ci_lint.vue'; import { CI_CONFIG_STATUS_INVALID } from '~/pipeline_editor/constants'; import { mergeUnwrappedCiConfig, mockLintHelpPagePath } from '../../mock_data'; diff --git a/spec/frontend/pipeline_editor/components/lint/ci_lint_warnings_spec.js b/spec/frontend/pipeline_editor/components/lint/ci_lint_warnings_spec.js index b441d26c146..4b576508ee9 100644 --- a/spec/frontend/pipeline_editor/components/lint/ci_lint_warnings_spec.js +++ b/spec/frontend/pipeline_editor/components/lint/ci_lint_warnings_spec.js @@ -1,5 +1,5 @@ -import { mount } from '@vue/test-utils'; import { GlAlert, GlSprintf } from '@gitlab/ui'; +import { mount } from '@vue/test-utils'; import { trimText } from 'helpers/text_helper'; import CiLintWarnings from '~/pipeline_editor/components/lint/ci_lint_warnings.vue'; diff --git a/spec/frontend/pipeline_editor/components/pipeline_editor_tabs_spec.js b/spec/frontend/pipeline_editor/components/pipeline_editor_tabs_spec.js new file mode 100644 index 00000000000..24af17e9ce6 --- /dev/null +++ b/spec/frontend/pipeline_editor/components/pipeline_editor_tabs_spec.js @@ -0,0 +1,183 @@ +import { GlAlert, GlLoadingIcon } from '@gitlab/ui'; +import { shallowMount, mount } from '@vue/test-utils'; +import { nextTick } from 'vue'; +import CiConfigMergedPreview from '~/pipeline_editor/components/editor/ci_config_merged_preview.vue'; +import CiLint from '~/pipeline_editor/components/lint/ci_lint.vue'; +import PipelineEditorTabs from '~/pipeline_editor/components/pipeline_editor_tabs.vue'; +import PipelineGraph from '~/pipelines/components/pipeline_graph/pipeline_graph.vue'; + +import { mockLintResponse, mockCiYml } from '../mock_data'; + +describe('Pipeline editor tabs component', () => { + let wrapper; + const MockTextEditor = { + template: '<div />', + }; + const mockProvide = { + glFeatures: { + ciConfigVisualizationTab: true, + ciConfigMergedTab: true, + }, + }; + + const createComponent = ({ props = {}, provide = {}, mountFn = shallowMount } = {}) => { + wrapper = mountFn(PipelineEditorTabs, { + propsData: { + ciConfigData: mockLintResponse, + ciFileContent: mockCiYml, + isCiConfigDataLoading: false, + ...props, + }, + provide: { ...mockProvide, ...provide }, + stubs: { + TextEditor: MockTextEditor, + }, + }); + }; + + const findEditorTab = () => wrapper.find('[data-testid="editor-tab"]'); + const findLintTab = () => wrapper.find('[data-testid="lint-tab"]'); + const findMergedTab = () => wrapper.find('[data-testid="merged-tab"]'); + const findVisualizationTab = () => wrapper.find('[data-testid="visualization-tab"]'); + + const findAlert = () => wrapper.findComponent(GlAlert); + const findCiLint = () => wrapper.findComponent(CiLint); + const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); + const findPipelineGraph = () => wrapper.findComponent(PipelineGraph); + const findTextEditor = () => wrapper.findComponent(MockTextEditor); + const findMergedPreview = () => wrapper.findComponent(CiConfigMergedPreview); + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + describe('editor tab', () => { + it('displays editor only after the tab is mounted', async () => { + createComponent({ mountFn: mount }); + + expect(findTextEditor().exists()).toBe(false); + + await nextTick(); + + expect(findTextEditor().exists()).toBe(true); + expect(findEditorTab().exists()).toBe(true); + }); + }); + + describe('visualization tab', () => { + describe('with feature flag on', () => { + describe('while loading', () => { + beforeEach(() => { + createComponent({ props: { isCiConfigDataLoading: true } }); + }); + + it('displays a loading icon if the lint query is loading', () => { + expect(findLoadingIcon().exists()).toBe(true); + expect(findPipelineGraph().exists()).toBe(false); + }); + }); + describe('after loading', () => { + beforeEach(() => { + createComponent(); + }); + + it('display the tab and visualization', () => { + expect(findVisualizationTab().exists()).toBe(true); + expect(findPipelineGraph().exists()).toBe(true); + }); + }); + }); + + describe('with feature flag off', () => { + beforeEach(() => { + createComponent({ + provide: { + glFeatures: { ciConfigVisualizationTab: false }, + }, + }); + }); + + it('does not display the tab or component', () => { + expect(findVisualizationTab().exists()).toBe(false); + expect(findPipelineGraph().exists()).toBe(false); + }); + }); + }); + + describe('lint tab', () => { + describe('while loading', () => { + beforeEach(() => { + createComponent({ props: { isCiConfigDataLoading: true } }); + }); + + it('displays a loading icon if the lint query is loading', () => { + expect(findLoadingIcon().exists()).toBe(true); + }); + + it('does not display the lint component', () => { + expect(findCiLint().exists()).toBe(false); + }); + }); + describe('after loading', () => { + beforeEach(() => { + createComponent(); + }); + + it('display the tab and the lint component', () => { + expect(findLintTab().exists()).toBe(true); + expect(findCiLint().exists()).toBe(true); + }); + }); + }); + + describe('merged tab', () => { + describe('with feature flag on', () => { + describe('while loading', () => { + beforeEach(() => { + createComponent({ props: { isCiConfigDataLoading: true } }); + }); + + it('displays a loading icon if the lint query is loading', () => { + expect(findLoadingIcon().exists()).toBe(true); + }); + }); + + describe('when `mergedYaml` is undefined', () => { + beforeEach(() => { + createComponent({ props: { ciConfigData: {} } }); + }); + + it('show an error message', () => { + expect(findAlert().exists()).toBe(true); + expect(findAlert().text()).toBe(wrapper.vm.$options.errorTexts.loadMergedYaml); + }); + + it('does not render the `meged_preview` component', () => { + expect(findMergedPreview().exists()).toBe(false); + }); + }); + + describe('after loading', () => { + beforeEach(() => { + createComponent(); + }); + + it('display the tab and the merged preview component', () => { + expect(findMergedTab().exists()).toBe(true); + expect(findMergedPreview().exists()).toBe(true); + }); + }); + }); + describe('with feature flag off', () => { + beforeEach(() => { + createComponent({ provide: { glFeatures: { ciConfigMergedTab: false } } }); + }); + + it('does not display the merged tab', () => { + expect(findMergedTab().exists()).toBe(false); + expect(findMergedPreview().exists()).toBe(false); + }); + }); + }); +}); diff --git a/spec/frontend/pipeline_editor/components/text_editor_spec.js b/spec/frontend/pipeline_editor/components/text_editor_spec.js deleted file mode 100644 index 9221d64c44b..00000000000 --- a/spec/frontend/pipeline_editor/components/text_editor_spec.js +++ /dev/null @@ -1,93 +0,0 @@ -import { shallowMount } from '@vue/test-utils'; -import { - mockCiConfigPath, - mockCiYml, - mockCommitSha, - mockProjectPath, - mockProjectNamespace, -} from '../mock_data'; - -import TextEditor from '~/pipeline_editor/components/text_editor.vue'; - -describe('~/pipeline_editor/components/text_editor.vue', () => { - let wrapper; - - let editorReadyListener; - let mockUse; - let mockRegisterCiSchema; - - const MockEditorLite = { - template: '<div/>', - props: ['value', 'fileName'], - mounted() { - this.$emit('editor-ready'); - }, - methods: { - getEditor: () => ({ - use: mockUse, - registerCiSchema: mockRegisterCiSchema, - }), - }, - }; - - const createComponent = (opts = {}, mountFn = shallowMount) => { - wrapper = mountFn(TextEditor, { - provide: { - projectPath: mockProjectPath, - projectNamespace: mockProjectNamespace, - }, - propsData: { - ciConfigPath: mockCiConfigPath, - commitSha: mockCommitSha, - }, - attrs: { - value: mockCiYml, - }, - listeners: { - 'editor-ready': editorReadyListener, - }, - stubs: { - EditorLite: MockEditorLite, - }, - ...opts, - }); - }; - - const findEditor = () => wrapper.find(MockEditorLite); - - beforeEach(() => { - editorReadyListener = jest.fn(); - mockUse = jest.fn(); - mockRegisterCiSchema = jest.fn(); - - createComponent(); - }); - - it('contains an editor', () => { - expect(findEditor().exists()).toBe(true); - }); - - it('editor contains the value provided', () => { - expect(findEditor().props('value')).toBe(mockCiYml); - }); - - it('editor is configured for the CI config path', () => { - expect(findEditor().props('fileName')).toBe(mockCiConfigPath); - }); - - it('editor is configured with syntax highligting', async () => { - expect(mockUse).toHaveBeenCalledTimes(1); - expect(mockRegisterCiSchema).toHaveBeenCalledTimes(1); - expect(mockRegisterCiSchema).toHaveBeenCalledWith({ - projectNamespace: mockProjectNamespace, - projectPath: mockProjectPath, - ref: mockCommitSha, - }); - }); - - it('bubbles up events', () => { - findEditor().vm.$emit('editor-ready'); - - expect(editorReadyListener).toHaveBeenCalled(); - }); -}); diff --git a/spec/frontend/pipeline_editor/components/ui/confirm_unsaved_changes_dialog_spec.js b/spec/frontend/pipeline_editor/components/ui/confirm_unsaved_changes_dialog_spec.js new file mode 100644 index 00000000000..44fda2812d8 --- /dev/null +++ b/spec/frontend/pipeline_editor/components/ui/confirm_unsaved_changes_dialog_spec.js @@ -0,0 +1,42 @@ +import { shallowMount } from '@vue/test-utils'; +import ConfirmDialog from '~/pipeline_editor/components/ui/confirm_unsaved_changes_dialog.vue'; + +describe('pipeline_editor/components/ui/confirm_unsaved_changes_dialog', () => { + let beforeUnloadEvent; + let setDialogContent; + let wrapper; + + const createComponent = (propsData = {}) => { + wrapper = shallowMount(ConfirmDialog, { + propsData, + }); + }; + + beforeEach(() => { + beforeUnloadEvent = new Event('beforeunload'); + jest.spyOn(beforeUnloadEvent, 'preventDefault'); + setDialogContent = jest.spyOn(beforeUnloadEvent, 'returnValue', 'set'); + }); + + afterEach(() => { + beforeUnloadEvent.preventDefault.mockRestore(); + setDialogContent.mockRestore(); + wrapper.destroy(); + }); + + it('shows confirmation dialog when there are unsaved changes', () => { + createComponent({ hasUnsavedChanges: true }); + window.dispatchEvent(beforeUnloadEvent); + + expect(beforeUnloadEvent.preventDefault).toHaveBeenCalled(); + expect(setDialogContent).toHaveBeenCalledWith(''); + }); + + it('does not show confirmation dialog when there are no unsaved changes', () => { + createComponent({ hasUnsavedChanges: false }); + window.dispatchEvent(beforeUnloadEvent); + + expect(beforeUnloadEvent.preventDefault).not.toHaveBeenCalled(); + expect(setDialogContent).not.toHaveBeenCalled(); + }); +}); diff --git a/spec/frontend/pipeline_editor/components/ui/editor_tab_spec.js b/spec/frontend/pipeline_editor/components/ui/editor_tab_spec.js index d3d9bf08209..291468c5229 100644 --- a/spec/frontend/pipeline_editor/components/ui/editor_tab_spec.js +++ b/spec/frontend/pipeline_editor/components/ui/editor_tab_spec.js @@ -1,6 +1,6 @@ -import { nextTick } from 'vue'; -import { mount } from '@vue/test-utils'; import { GlTabs } from '@gitlab/ui'; +import { mount } from '@vue/test-utils'; +import { nextTick } from 'vue'; import EditorTab from '~/pipeline_editor/components/ui/editor_tab.vue'; diff --git a/spec/frontend/pipeline_editor/graphql/resolvers_spec.js b/spec/frontend/pipeline_editor/graphql/resolvers_spec.js index 3e008527415..d39c0d80296 100644 --- a/spec/frontend/pipeline_editor/graphql/resolvers_spec.js +++ b/spec/frontend/pipeline_editor/graphql/resolvers_spec.js @@ -1,5 +1,8 @@ import MockAdapter from 'axios-mock-adapter'; import Api from '~/api'; +import axios from '~/lib/utils/axios_utils'; +import httpStatus from '~/lib/utils/http_status'; +import { resolvers } from '~/pipeline_editor/graphql/resolvers'; import { mockCiConfigPath, mockCiYml, @@ -7,9 +10,6 @@ import { mockLintResponse, mockProjectFullPath, } from '../mock_data'; -import httpStatus from '~/lib/utils/http_status'; -import axios from '~/lib/utils/axios_utils'; -import { resolvers } from '~/pipeline_editor/graphql/resolvers'; jest.mock('~/api', () => { return { diff --git a/spec/frontend/pipeline_editor/mock_data.js b/spec/frontend/pipeline_editor/mock_data.js index 3eacc467c51..8e248c11b87 100644 --- a/spec/frontend/pipeline_editor/mock_data.js +++ b/spec/frontend/pipeline_editor/mock_data.js @@ -54,6 +54,7 @@ export const mockCiConfigQueryResponse = { data: { ciConfig: { errors: [], + mergedYaml: mockCiYml, status: CI_CONFIG_STATUS_VALID, stages: { __typename: 'CiConfigStageConnection', @@ -139,6 +140,8 @@ export const mergeUnwrappedCiConfig = (mergedConfig) => { export const mockLintResponse = { valid: true, + mergedYaml: mockCiYml, + status: CI_CONFIG_STATUS_VALID, errors: [], warnings: [], jobs: [ diff --git a/spec/frontend/pipeline_editor/pipeline_editor_app_spec.js b/spec/frontend/pipeline_editor/pipeline_editor_app_spec.js index d6b90900600..46d0452f437 100644 --- a/spec/frontend/pipeline_editor/pipeline_editor_app_spec.js +++ b/spec/frontend/pipeline_editor/pipeline_editor_app_spec.js @@ -1,119 +1,70 @@ -import { nextTick } from 'vue'; -import { mount, shallowMount, createLocalVue } from '@vue/test-utils'; -import { GlAlert, GlButton, GlFormInput, GlFormTextarea, GlLoadingIcon, GlTabs } from '@gitlab/ui'; -import waitForPromises from 'helpers/wait_for_promises'; +import { GlAlert, GlButton, GlLoadingIcon, GlTabs } from '@gitlab/ui'; +import { shallowMount, createLocalVue } from '@vue/test-utils'; import VueApollo from 'vue-apollo'; import createMockApollo from 'helpers/mock_apollo_helper'; - +import waitForPromises from 'helpers/wait_for_promises'; import httpStatusCodes from '~/lib/utils/http_status'; -import { objectToQuery, redirectTo, refreshCurrentPage } from '~/lib/utils/url_utility'; +import CommitForm from '~/pipeline_editor/components/commit/commit_form.vue'; +import TextEditor from '~/pipeline_editor/components/editor/text_editor.vue'; + +import { COMMIT_SUCCESS, COMMIT_FAILURE, LOAD_FAILURE_UNKNOWN } from '~/pipeline_editor/constants'; +import getCiConfigData from '~/pipeline_editor/graphql/queries/ci_config.graphql'; +import PipelineEditorApp from '~/pipeline_editor/pipeline_editor_app.vue'; +import PipelineEditorHome from '~/pipeline_editor/pipeline_editor_home.vue'; import { mockCiConfigPath, mockCiConfigQueryResponse, mockCiYml, - mockCommitSha, - mockCommitNextSha, - mockCommitMessage, mockDefaultBranch, - mockProjectPath, mockProjectFullPath, - mockProjectNamespace, - mockNewMergeRequestPath, } from './mock_data'; -import CommitForm from '~/pipeline_editor/components/commit/commit_form.vue'; -import getCiConfigData from '~/pipeline_editor/graphql/queries/ci_config.graphql'; -import EditorTab from '~/pipeline_editor/components/ui/editor_tab.vue'; -import PipelineGraph from '~/pipelines/components/pipeline_graph/pipeline_graph.vue'; -import PipelineEditorApp from '~/pipeline_editor/pipeline_editor_app.vue'; -import TextEditor from '~/pipeline_editor/components/text_editor.vue'; - const localVue = createLocalVue(); localVue.use(VueApollo); -jest.mock('~/lib/utils/url_utility', () => ({ - redirectTo: jest.fn(), - refreshCurrentPage: jest.fn(), - objectToQuery: jest.requireActual('~/lib/utils/url_utility').objectToQuery, - mergeUrlParams: jest.requireActual('~/lib/utils/url_utility').mergeUrlParams, -})); - const MockEditorLite = { template: '<div/>', }; const mockProvide = { + ciConfigPath: mockCiConfigPath, + defaultBranch: mockDefaultBranch, projectFullPath: mockProjectFullPath, - projectPath: mockProjectPath, - projectNamespace: mockProjectNamespace, - glFeatures: { - ciConfigVisualizationTab: true, - }, }; -describe('~/pipeline_editor/pipeline_editor_app.vue', () => { +describe('Pipeline editor app component', () => { let wrapper; let mockApollo; let mockBlobContentData; let mockCiConfigData; - let mockMutate; - - const createComponent = ({ - props = {}, - blobLoading = false, - lintLoading = false, - options = {}, - mountFn = shallowMount, - provide = mockProvide, - } = {}) => { - mockMutate = jest.fn().mockResolvedValue({ - data: { - commitCreate: { - errors: [], - commit: { - sha: mockCommitNextSha, - }, - }, - }, - }); - wrapper = mountFn(PipelineEditorApp, { - propsData: { - ciConfigPath: mockCiConfigPath, - commitSha: mockCommitSha, - defaultBranch: mockDefaultBranch, - newMergeRequestPath: mockNewMergeRequestPath, - ...props, - }, - provide, + const createComponent = ({ blobLoading = false, options = {} } = {}) => { + wrapper = shallowMount(PipelineEditorApp, { + provide: mockProvide, stubs: { GlTabs, GlButton, CommitForm, EditorLite: MockEditorLite, - TextEditor, }, mocks: { $apollo: { queries: { - content: { + initialCiFileContent: { loading: blobLoading, }, ciConfigData: { - loading: lintLoading, + loading: false, }, }, - mutate: mockMutate, }, }, - // attachTo is required for input/submit events - attachTo: mountFn === mount ? document.body : null, ...options, }); }; - const createComponentWithApollo = ({ props = {}, mountFn = shallowMount } = {}) => { + const createComponentWithApollo = ({ props = {} } = {}) => { const handlers = [[getCiConfigData, mockCiConfigData]]; const resolvers = { Query: { @@ -134,18 +85,13 @@ describe('~/pipeline_editor/pipeline_editor_app.vue', () => { apolloProvider: mockApollo, }; - createComponent({ props, options }, mountFn); + createComponent({ props, options }); }; - const findLoadingIcon = () => wrapper.find(GlLoadingIcon); - const findAlert = () => wrapper.find(GlAlert); - const findTabAt = (i) => wrapper.findAll(EditorTab).at(i); - const findVisualizationTab = () => wrapper.find('[data-testid="visualization-tab"]'); - const findTextEditor = () => wrapper.find(TextEditor); - const findEditorLite = () => wrapper.find(MockEditorLite); - const findCommitForm = () => wrapper.find(CommitForm); - const findPipelineGraph = () => wrapper.find(PipelineGraph); - const findCommitBtnLoadingIcon = () => wrapper.find('[type="submit"]').find(GlLoadingIcon); + const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); + const findAlert = () => wrapper.findComponent(GlAlert); + const findEditorHome = () => wrapper.findComponent(PipelineEditorHome); + const findTextEditor = () => wrapper.findComponent(TextEditor); beforeEach(() => { mockBlobContentData = jest.fn(); @@ -155,9 +101,6 @@ describe('~/pipeline_editor/pipeline_editor_app.vue', () => { afterEach(() => { mockBlobContentData.mockReset(); mockCiConfigData.mockReset(); - refreshCurrentPage.mockReset(); - redirectTo.mockReset(); - mockMutate.mockReset(); wrapper.destroy(); wrapper = null; @@ -170,245 +113,6 @@ describe('~/pipeline_editor/pipeline_editor_app.vue', () => { expect(findTextEditor().exists()).toBe(false); }); - describe('tabs', () => { - describe('editor tab', () => { - it('displays editor only after the tab is mounted', async () => { - createComponent({ mountFn: mount }); - - expect(findTabAt(0).find(TextEditor).exists()).toBe(false); - - await nextTick(); - - expect(findTabAt(0).find(TextEditor).exists()).toBe(true); - }); - }); - - describe('visualization tab', () => { - describe('with feature flag on', () => { - beforeEach(() => { - createComponent(); - }); - - it('display the tab', () => { - expect(findVisualizationTab().exists()).toBe(true); - }); - - it('displays a loading icon if the lint query is loading', () => { - createComponent({ lintLoading: true }); - - expect(findLoadingIcon().exists()).toBe(true); - expect(findPipelineGraph().exists()).toBe(false); - }); - }); - - describe('with feature flag off', () => { - beforeEach(() => { - createComponent({ - provide: { - ...mockProvide, - glFeatures: { ciConfigVisualizationTab: false }, - }, - }); - }); - - it('does not display the tab', () => { - expect(findVisualizationTab().exists()).toBe(false); - }); - }); - }); - }); - - describe('when data is set', () => { - beforeEach(async () => { - createComponent({ mountFn: mount }); - - wrapper.setData({ - content: mockCiYml, - contentModel: mockCiYml, - }); - - await waitForPromises(); - }); - - it('displays content after the query loads', () => { - expect(findLoadingIcon().exists()).toBe(false); - - expect(findEditorLite().attributes('value')).toBe(mockCiYml); - expect(findEditorLite().attributes('file-name')).toBe(mockCiConfigPath); - }); - - it('configures text editor', () => { - expect(findTextEditor().props('commitSha')).toBe(mockCommitSha); - }); - - describe('commit form', () => { - const mockVariables = { - content: mockCiYml, - filePath: mockCiConfigPath, - lastCommitId: mockCommitSha, - message: mockCommitMessage, - projectPath: mockProjectFullPath, - startBranch: mockDefaultBranch, - }; - - const findInForm = (selector) => findCommitForm().find(selector); - - const submitCommit = async ({ - message = mockCommitMessage, - branch = mockDefaultBranch, - openMergeRequest = false, - } = {}) => { - await findInForm(GlFormTextarea).setValue(message); - await findInForm(GlFormInput).setValue(branch); - if (openMergeRequest) { - await findInForm('[data-testid="new-mr-checkbox"]').setChecked(openMergeRequest); - } - await findInForm('[type="submit"]').trigger('click'); - }; - - const cancelCommitForm = async () => { - const findCancelBtn = () => wrapper.find('[type="reset"]'); - await findCancelBtn().trigger('click'); - }; - - describe('when the user commits changes to the current branch', () => { - beforeEach(async () => { - await submitCommit(); - }); - - it('calls the mutation with the default branch', () => { - expect(mockMutate).toHaveBeenCalledWith({ - mutation: expect.any(Object), - variables: { - ...mockVariables, - branch: mockDefaultBranch, - }, - }); - }); - - it('displays an alert to indicate success', () => { - expect(findAlert().text()).toMatchInterpolatedText( - 'Your changes have been successfully committed.', - ); - }); - - it('shows no saving state', () => { - expect(findCommitBtnLoadingIcon().exists()).toBe(false); - }); - - it('a second commit submits the latest sha, keeping the form updated', async () => { - await submitCommit(); - - expect(mockMutate).toHaveBeenCalledTimes(2); - expect(mockMutate).toHaveBeenLastCalledWith({ - mutation: expect.any(Object), - variables: { - ...mockVariables, - lastCommitId: mockCommitNextSha, - branch: mockDefaultBranch, - }, - }); - }); - }); - - describe('when the user commits changes to a new branch', () => { - const newBranch = 'new-branch'; - - beforeEach(async () => { - await submitCommit({ - branch: newBranch, - }); - }); - - it('calls the mutation with the new branch', () => { - expect(mockMutate).toHaveBeenCalledWith({ - mutation: expect.any(Object), - variables: { - ...mockVariables, - branch: newBranch, - }, - }); - }); - }); - - describe('when the user commits changes to open a new merge request', () => { - const newBranch = 'new-branch'; - - beforeEach(async () => { - await submitCommit({ - branch: newBranch, - openMergeRequest: true, - }); - }); - - it('redirects to the merge request page with source and target branches', () => { - const branchesQuery = objectToQuery({ - 'merge_request[source_branch]': newBranch, - 'merge_request[target_branch]': mockDefaultBranch, - }); - - expect(redirectTo).toHaveBeenCalledWith(`${mockNewMergeRequestPath}?${branchesQuery}`); - }); - }); - - describe('when the commit is ocurring', () => { - it('shows a saving state', async () => { - await mockMutate.mockImplementationOnce(() => { - expect(findCommitBtnLoadingIcon().exists()).toBe(true); - return Promise.resolve(); - }); - - await submitCommit({ - message: mockCommitMessage, - branch: mockDefaultBranch, - openMergeRequest: false, - }); - }); - }); - - describe('when the commit fails', () => { - it('shows an error message', async () => { - mockMutate.mockRejectedValueOnce(new Error('commit failed')); - - await submitCommit(); - - await waitForPromises(); - - expect(findAlert().text()).toMatchInterpolatedText( - 'The GitLab CI configuration could not be updated. commit failed', - ); - }); - - it('shows an unkown error', async () => { - mockMutate.mockRejectedValueOnce(); - - await submitCommit(); - - await waitForPromises(); - - expect(findAlert().text()).toMatchInterpolatedText( - 'The GitLab CI configuration could not be updated.', - ); - }); - }); - - describe('when the commit form is cancelled', () => { - const otherContent = 'other content'; - - beforeEach(async () => { - findTextEditor().vm.$emit('input', otherContent); - await nextTick(); - }); - - it('content is restored after cancel is called', async () => { - await cancelCommitForm(); - - expect(findEditorLite().attributes('value')).toBe(mockCiYml); - }); - }); - }); - }); - describe('when queries are called', () => { beforeEach(() => { mockBlobContentData.mockResolvedValue(mockCiYml); @@ -422,14 +126,12 @@ describe('~/pipeline_editor/pipeline_editor_app.vue', () => { await waitForPromises(); }); - it('shows editor and commit form', () => { - expect(findEditorLite().exists()).toBe(true); - expect(findTextEditor().exists()).toBe(true); + it('shows pipeline editor home component', () => { + expect(findEditorHome().exists()).toBe(true); }); - it('no error is shown when data is set', async () => { + it('no error is shown when data is set', () => { expect(findAlert().exists()).toBe(false); - expect(findEditorLite().attributes('value')).toBe(mockCiYml); }); it('ci config query is called with correct variables', async () => { @@ -445,10 +147,10 @@ describe('~/pipeline_editor/pipeline_editor_app.vue', () => { }); describe('when no file exists', () => { - const expectedAlertMsg = + const noFileAlertMsg = 'There is no .gitlab-ci.yml file in this repository, please add one and visit the Pipeline Editor again.'; - it('shows a 404 error message and does not show editor or commit form', async () => { + it('shows a 404 error message and does not show editor home component', async () => { mockBlobContentData.mockRejectedValueOnce({ response: { status: httpStatusCodes.NOT_FOUND, @@ -458,12 +160,11 @@ describe('~/pipeline_editor/pipeline_editor_app.vue', () => { await waitForPromises(); - expect(findAlert().text()).toBe(expectedAlertMsg); - expect(findEditorLite().exists()).toBe(false); - expect(findTextEditor().exists()).toBe(false); + expect(findAlert().text()).toBe(noFileAlertMsg); + expect(findEditorHome().exists()).toBe(false); }); - it('shows a 400 error message and does not show editor or commit form', async () => { + it('shows a 400 error message and does not show editor home component', async () => { mockBlobContentData.mockRejectedValueOnce({ response: { status: httpStatusCodes.BAD_REQUEST, @@ -473,9 +174,8 @@ describe('~/pipeline_editor/pipeline_editor_app.vue', () => { await waitForPromises(); - expect(findAlert().text()).toBe(expectedAlertMsg); - expect(findEditorLite().exists()).toBe(false); - expect(findTextEditor().exists()).toBe(false); + expect(findAlert().text()).toBe(noFileAlertMsg); + expect(findEditorHome().exists()).toBe(false); }); it('shows a unkown error message', async () => { @@ -483,9 +183,60 @@ describe('~/pipeline_editor/pipeline_editor_app.vue', () => { createComponentWithApollo(); await waitForPromises(); - expect(findAlert().text()).toBe('The CI configuration was not loaded, please try again.'); - expect(findEditorLite().exists()).toBe(true); - expect(findTextEditor().exists()).toBe(true); + expect(findAlert().text()).toBe(wrapper.vm.$options.errorTexts[LOAD_FAILURE_UNKNOWN]); + expect(findEditorHome().exists()).toBe(true); + }); + }); + + describe('when the user commits', () => { + const updateFailureMessage = 'The GitLab CI configuration could not be updated.'; + + describe('and the commit mutation succeeds', () => { + beforeEach(() => { + createComponent(); + + findEditorHome().vm.$emit('commit', { type: COMMIT_SUCCESS }); + }); + + it('shows a confirmation message', () => { + expect(findAlert().text()).toBe(wrapper.vm.$options.successTexts[COMMIT_SUCCESS]); + }); + }); + describe('and the commit mutation fails', () => { + const commitFailedReasons = ['Commit failed']; + + beforeEach(() => { + createComponent(); + + findEditorHome().vm.$emit('showError', { + type: COMMIT_FAILURE, + reasons: commitFailedReasons, + }); + }); + + it('shows an error message', () => { + expect(findAlert().text()).toMatchInterpolatedText( + `${updateFailureMessage} ${commitFailedReasons[0]}`, + ); + }); + }); + describe('when an unknown error occurs', () => { + const unknownReasons = ['Commit failed']; + + beforeEach(() => { + createComponent(); + + findEditorHome().vm.$emit('showError', { + type: COMMIT_FAILURE, + reasons: unknownReasons, + }); + }); + + it('shows an error message', () => { + expect(findAlert().text()).toMatchInterpolatedText( + `${updateFailureMessage} ${unknownReasons[0]}`, + ); + }); }); }); }); diff --git a/spec/frontend/pipeline_editor/pipeline_editor_home_spec.js b/spec/frontend/pipeline_editor/pipeline_editor_home_spec.js new file mode 100644 index 00000000000..9864f3c13f9 --- /dev/null +++ b/spec/frontend/pipeline_editor/pipeline_editor_home_spec.js @@ -0,0 +1,78 @@ +import { shallowMount } from '@vue/test-utils'; +import { nextTick } from 'vue'; + +import CommitSection from '~/pipeline_editor/components/commit/commit_section.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 PipelineEditorHome from '~/pipeline_editor/pipeline_editor_home.vue'; + +import { mockLintResponse, mockCiYml } from './mock_data'; + +describe('Pipeline editor home wrapper', () => { + let wrapper; + + const createComponent = ({ props = {} } = {}) => { + wrapper = shallowMount(PipelineEditorHome, { + propsData: { + ciConfigData: mockLintResponse, + ciFileContent: mockCiYml, + isCiConfigDataLoading: false, + ...props, + }, + }); + }; + + const findPipelineEditorHeader = () => wrapper.findComponent(PipelineEditorHeader); + const findPipelineEditorTabs = () => wrapper.findComponent(PipelineEditorTabs); + const findCommitSection = () => wrapper.findComponent(CommitSection); + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + describe('renders', () => { + beforeEach(() => { + createComponent(); + }); + + it('shows the pipeline editor header', () => { + expect(findPipelineEditorHeader().exists()).toBe(true); + }); + + it('shows the pipeline editor tabs', () => { + expect(findPipelineEditorTabs().exists()).toBe(true); + }); + + it('shows the commit section by default', () => { + expect(findCommitSection().exists()).toBe(true); + }); + }); + + describe('commit form toggle', () => { + beforeEach(() => { + createComponent(); + }); + + it('hides the commit form when in the merged tab', async () => { + expect(findCommitSection().exists()).toBe(true); + + findPipelineEditorTabs().vm.$emit('set-current-tab', MERGED_TAB); + await nextTick(); + expect(findCommitSection().exists()).toBe(false); + }); + + it('shows the form again when leaving the merged 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); + await nextTick(); + expect(findCommitSection().exists()).toBe(true); + }); + }); +}); |