diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-02-04 00:09:17 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-02-04 00:09:17 +0300 |
commit | a1ed241c8212fe848501de9d561796ed6879307f (patch) | |
tree | f4cd88555f3e9be61e498761c7a289260135c806 /spec/frontend/pipeline_editor | |
parent | 174343966742d2f4b87ac84f9ce4ee576cb9d75e (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec/frontend/pipeline_editor')
-rw-r--r-- | spec/frontend/pipeline_editor/components/commit/commit_form_spec.js | 6 | ||||
-rw-r--r-- | spec/frontend/pipeline_editor/components/commit/commit_section_spec.js | 223 | ||||
-rw-r--r-- | spec/frontend/pipeline_editor/components/header/pipeline_editor_header_spec.js | 34 | ||||
-rw-r--r-- | spec/frontend/pipeline_editor/components/header/validation_segment_spec.js (renamed from spec/frontend/pipeline_editor/components/info/validation_segment_spec.js) | 9 | ||||
-rw-r--r-- | spec/frontend/pipeline_editor/components/pipeline_editor_tabs_spec.js | 129 | ||||
-rw-r--r-- | spec/frontend/pipeline_editor/components/text_editor_spec.js | 89 | ||||
-rw-r--r-- | spec/frontend/pipeline_editor/pipeline_editor_app_spec.js | 422 | ||||
-rw-r--r-- | spec/frontend/pipeline_editor/pipeline_editor_home_spec.js | 50 |
8 files changed, 592 insertions, 370 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..76c56bd2815 100644 --- a/spec/frontend/pipeline_editor/components/commit/commit_form_spec.js +++ b/spec/frontend/pipeline_editor/components/commit/commit_form_spec.js @@ -5,7 +5,7 @@ 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..a480af3ea5b --- /dev/null +++ b/spec/frontend/pipeline_editor/components/commit/commit_section_spec.js @@ -0,0 +1,223 @@ +import { mount } from '@vue/test-utils'; +import { GlFormTextarea, GlFormInput, GlLoadingIcon } from '@gitlab/ui'; +import CommitSection from '~/pipeline_editor/components/commit/commit_section.vue'; +import CommitForm from '~/pipeline_editor/components/commit/commit_form.vue'; +import { objectToQuery, redirectTo } from '~/lib/utils/url_utility'; +import commitCreate from '~/pipeline_editor/graphql/mutations/commit_ci_file.mutation.graphql'; + +import { + mockCiConfigPath, + mockCiYml, + mockCommitSha, + mockCommitNextSha, + mockCommitMessage, + mockDefaultBranch, + mockProjectFullPath, + mockNewMergeRequestPath, +} from '../../mock_data'; +import { COMMIT_SUCCESS } from '~/pipeline_editor/constants'; + +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/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..c5b88e4b904 100644 --- a/spec/frontend/pipeline_editor/components/info/validation_segment_spec.js +++ b/spec/frontend/pipeline_editor/components/header/validation_segment_spec.js @@ -3,7 +3,9 @@ import { shallowMount } from '@vue/test-utils'; import { GlIcon } from '@gitlab/ui'; 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/pipeline_editor_tabs_spec.js b/spec/frontend/pipeline_editor/components/pipeline_editor_tabs_spec.js new file mode 100644 index 00000000000..275e2987f38 --- /dev/null +++ b/spec/frontend/pipeline_editor/components/pipeline_editor_tabs_spec.js @@ -0,0 +1,129 @@ +import { nextTick } from 'vue'; +import { shallowMount, mount } from '@vue/test-utils'; +import { GlLoadingIcon } from '@gitlab/ui'; +import PipelineGraph from '~/pipelines/components/pipeline_graph/pipeline_graph.vue'; +import PipelineEditorTabs from '~/pipeline_editor/components/pipeline_editor_tabs.vue'; +import CiLint from '~/pipeline_editor/components/lint/ci_lint.vue'; + +import { mockLintResponse, mockCiYml } from '../mock_data'; + +describe('Pipeline editor tabs component', () => { + let wrapper; + const MockTextEditor = { + template: '<div />', + }; + const mockProvide = { + glFeatures: { + ciConfigVisualizationTab: 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 findVisualizationTab = () => wrapper.find('[data-testid="visualization-tab"]'); + const findCiLint = () => wrapper.findComponent(CiLint); + const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); + const findPipelineGraph = () => wrapper.findComponent(PipelineGraph); + const findTextEditor = () => wrapper.findComponent(MockTextEditor); + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + describe('tabs', () => { + 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); + }); + }); + }); + }); +}); diff --git a/spec/frontend/pipeline_editor/components/text_editor_spec.js b/spec/frontend/pipeline_editor/components/text_editor_spec.js index 5cd226a1345..86ee370bb72 100644 --- a/spec/frontend/pipeline_editor/components/text_editor_spec.js +++ b/spec/frontend/pipeline_editor/components/text_editor_spec.js @@ -1,7 +1,5 @@ import { shallowMount } from '@vue/test-utils'; -import { EDITOR_READY_EVENT } from '~/editor/constants'; -import TextEditor from '~/pipeline_editor/components/text_editor.vue'; import { mockCiConfigPath, mockCiYml, @@ -10,7 +8,10 @@ import { mockProjectNamespace, } from '../mock_data'; -describe('~/pipeline_editor/components/text_editor.vue', () => { +import { EDITOR_READY_EVENT } from '~/editor/constants'; +import TextEditor from '~/pipeline_editor/components/text_editor.vue'; + +describe('Pipeline Editor | Text editor component', () => { let wrapper; let editorReadyListener; @@ -36,14 +37,17 @@ describe('~/pipeline_editor/components/text_editor.vue', () => { provide: { projectPath: mockProjectPath, projectNamespace: mockProjectNamespace, - }, - propsData: { ciConfigPath: mockCiConfigPath, - commitSha: mockCommitSha, }, attrs: { value: mockCiYml, }, + // Simulate graphQL client query result + data() { + return { + commitSha: mockCommitSha, + }; + }, listeners: { [EDITOR_READY_EVENT]: editorReadyListener, }, @@ -54,41 +58,64 @@ describe('~/pipeline_editor/components/text_editor.vue', () => { }); }; - const findEditor = () => wrapper.find(MockEditorLite); + const findEditor = () => wrapper.findComponent(MockEditorLite); - beforeEach(() => { - editorReadyListener = jest.fn(); - mockUse = jest.fn(); - mockRegisterCiSchema = jest.fn(); + afterEach(() => { + wrapper.destroy(); + wrapper = null; - createComponent(); + mockUse.mockClear(); + mockRegisterCiSchema.mockClear(); }); - it('contains an editor', () => { - expect(findEditor().exists()).toBe(true); - }); + describe('template', () => { + beforeEach(() => { + editorReadyListener = jest.fn(); + mockUse = jest.fn(); + mockRegisterCiSchema = jest.fn(); - it('editor contains the value provided', () => { - expect(findEditor().props('value')).toBe(mockCiYml); - }); + createComponent(); + }); - it('editor is configured for the CI config path', () => { - expect(findEditor().props('fileName')).toBe(mockCiConfigPath); - }); + it('contains an editor', () => { + expect(findEditor().exists()).toBe(true); + }); - 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('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(); }); }); - it('bubbles up events', () => { - findEditor().vm.$emit(EDITOR_READY_EVENT); + 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(); - expect(editorReadyListener).toHaveBeenCalled(); + 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/pipeline_editor_app_spec.js b/spec/frontend/pipeline_editor/pipeline_editor_app_spec.js index e9423cf3fba..d9e7c708396 100644 --- a/spec/frontend/pipeline_editor/pipeline_editor_app_spec.js +++ b/spec/frontend/pipeline_editor/pipeline_editor_app_spec.js @@ -1,119 +1,71 @@ -import { nextTick } from 'vue'; -import { mount, shallowMount, createLocalVue } from '@vue/test-utils'; -import { GlAlert, GlButton, GlFormInput, GlFormTextarea, GlLoadingIcon, GlTabs } from '@gitlab/ui'; +import { shallowMount, createLocalVue } from '@vue/test-utils'; +import { GlAlert, GlButton, GlLoadingIcon, GlTabs } from '@gitlab/ui'; import VueApollo from 'vue-apollo'; import waitForPromises from 'helpers/wait_for_promises'; import createMockApollo from 'helpers/mock_apollo_helper'; +import TextEditor from '~/pipeline_editor/components/text_editor.vue'; 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 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'; import { mockCiConfigPath, mockCiConfigQueryResponse, mockCiYml, - mockCommitSha, - mockCommitNextSha, - mockCommitMessage, mockDefaultBranch, - mockProjectPath, mockProjectFullPath, - mockProjectNamespace, - mockNewMergeRequestPath, } from './mock_data'; +import { COMMIT_SUCCESS, COMMIT_FAILURE, LOAD_FAILURE_UNKNOWN } from '~/pipeline_editor/constants'; +import CommitForm from '~/pipeline_editor/components/commit/commit_form.vue'; +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'; + 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 +86,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 +102,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 +114,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 +127,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 +148,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 +161,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 +175,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 +184,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..6ef1b3cdd1c --- /dev/null +++ b/spec/frontend/pipeline_editor/pipeline_editor_home_spec.js @@ -0,0 +1,50 @@ +import { shallowMount } from '@vue/test-utils'; + +import PipelineEditorHome from '~/pipeline_editor/pipeline_editor_home.vue'; +import PipelineEditorTabs from '~/pipeline_editor/components/pipeline_editor_tabs.vue'; +import CommitSection from '~/pipeline_editor/components/commit/commit_section.vue'; +import PipelineEditorHeader from '~/pipeline_editor/components/header/pipeline_editor_header.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(PipelineEditorTabs); + const findPipelineEditorTabs = () => wrapper.findComponent(CommitSection); + const findCommitSection = () => wrapper.findComponent(PipelineEditorHeader); + + 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', () => { + expect(findCommitSection().exists()).toBe(true); + }); + }); +}); |