diff options
Diffstat (limited to 'spec/frontend/pipeline_editor/pipeline_editor_app_spec.js')
-rw-r--r-- | spec/frontend/pipeline_editor/pipeline_editor_app_spec.js | 444 |
1 files changed, 375 insertions, 69 deletions
diff --git a/spec/frontend/pipeline_editor/pipeline_editor_app_spec.js b/spec/frontend/pipeline_editor/pipeline_editor_app_spec.js index 46523baadf3..14d6b03645c 100644 --- a/spec/frontend/pipeline_editor/pipeline_editor_app_spec.js +++ b/spec/frontend/pipeline_editor/pipeline_editor_app_spec.js @@ -1,139 +1,445 @@ import { nextTick } from 'vue'; -import { shallowMount } from '@vue/test-utils'; -import { GlAlert, GlLoadingIcon, GlTabs, GlTab } from '@gitlab/ui'; +import { mount, shallowMount, createLocalVue } from '@vue/test-utils'; +import { + GlAlert, + GlButton, + GlFormInput, + GlFormTextarea, + GlLoadingIcon, + GlTabs, + GlTab, +} from '@gitlab/ui'; +import waitForPromises from 'helpers/wait_for_promises'; +import VueApollo from 'vue-apollo'; +import createMockApollo from 'jest/helpers/mock_apollo_helper'; -import { mockProjectPath, mockDefaultBranch, mockCiConfigPath, mockCiYml } from './mock_data'; -import TextEditor from '~/pipeline_editor/components/text_editor.vue'; -import EditorLite from '~/vue_shared/components/editor_lite.vue'; +import { objectToQuery, redirectTo, refreshCurrentPage } from '~/lib/utils/url_utility'; +import { + mockCiConfigPath, + mockCiConfigQueryResponse, + mockCiYml, + mockCommitId, + mockCommitMessage, + mockDefaultBranch, + mockProjectPath, + mockNewMergeRequestPath, +} from './mock_data'; + +import CommitForm from '~/pipeline_editor/components/commit/commit_form.vue'; +import getCiConfig from '~/pipeline_editor/graphql/queries/ci_config.graphql'; 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, +})); describe('~/pipeline_editor/pipeline_editor_app.vue', () => { let wrapper; - const createComponent = ( - { props = {}, data = {}, loading = false } = {}, + let mockApollo; + let mockBlobContentData; + let mockCiConfigData; + let mockMutate; + + const createComponent = ({ + props = {}, + blobLoading = false, + lintLoading = false, + options = {}, mountFn = shallowMount, - ) => { + provide = { + glFeatures: { + ciConfigVisualizationTab: true, + }, + }, + } = {}) => { + mockMutate = jest.fn().mockResolvedValue({ + data: { + commitCreate: { + errors: [], + commit: {}, + }, + }, + }); + wrapper = mountFn(PipelineEditorApp, { propsData: { - projectPath: mockProjectPath, - defaultBranch: mockDefaultBranch, ciConfigPath: mockCiConfigPath, + commitId: mockCommitId, + defaultBranch: mockDefaultBranch, + projectPath: mockProjectPath, + newMergeRequestPath: mockNewMergeRequestPath, ...props, }, - data() { - return data; - }, + provide, stubs: { GlTabs, + GlButton, + CommitForm, + EditorLite: { + template: '<div/>', + }, TextEditor, }, mocks: { $apollo: { queries: { content: { - loading, + loading: blobLoading, + }, + ciConfigData: { + loading: lintLoading, }, }, + mutate: mockMutate, }, }, + // attachToDocument is required for input/submit events + attachToDocument: mountFn === mount, + ...options, }); }; + const createComponentWithApollo = ({ props = {}, mountFn = shallowMount } = {}) => { + const handlers = [[getCiConfig, mockCiConfigData]]; + const resolvers = { + Query: { + blobContent() { + return { + __typename: 'BlobContent', + rawData: mockBlobContentData(), + }; + }, + }, + }; + + mockApollo = createMockApollo(handlers, resolvers); + + const options = { + localVue, + mocks: {}, + apolloProvider: mockApollo, + }; + + createComponent({ props, options }, mountFn); + }; + const findLoadingIcon = () => wrapper.find(GlLoadingIcon); const findAlert = () => wrapper.find(GlAlert); + const findBlobFailureAlert = () => wrapper.find(GlAlert); const findTabAt = i => wrapper.findAll(GlTab).at(i); - const findEditorLite = () => wrapper.find(EditorLite); + const findVisualizationTab = () => wrapper.find('[data-testid="visualization-tab"]'); + const findTextEditor = () => wrapper.find(TextEditor); + const findCommitForm = () => wrapper.find(CommitForm); + const findPipelineGraph = () => wrapper.find(PipelineGraph); + const findCommitBtnLoadingIcon = () => wrapper.find('[type="submit"]').find(GlLoadingIcon); beforeEach(() => { - createComponent(); + mockBlobContentData = jest.fn(); + mockCiConfigData = jest.fn().mockResolvedValue(mockCiConfigQueryResponse); }); afterEach(() => { + mockBlobContentData.mockReset(); + mockCiConfigData.mockReset(); + refreshCurrentPage.mockReset(); + redirectTo.mockReset(); + mockMutate.mockReset(); + wrapper.destroy(); wrapper = null; }); - it('displays content', () => { - createComponent({ data: { content: mockCiYml } }); - - expect(findLoadingIcon().exists()).toBe(false); - expect(findEditorLite().props('value')).toBe(mockCiYml); - }); - - it('displays a loading icon if the query is loading', () => { - createComponent({ loading: true }); + it('displays a loading icon if the blob query is loading', () => { + createComponent({ blobLoading: true }); expect(findLoadingIcon().exists()).toBe(true); + expect(findTextEditor().exists()).toBe(false); }); describe('tabs', () => { - it('displays tabs and their content', () => { - createComponent({ data: { content: mockCiYml } }); - - expect( - findTabAt(0) - .find(EditorLite) - .exists(), - ).toBe(true); - expect( - findTabAt(1) - .find(PipelineGraph) - .exists(), - ).toBe(true); + describe('editor tab', () => { + beforeEach(() => { + createComponent(); + }); + + it('displays the tab and its content', async () => { + expect( + findTabAt(0) + .find(TextEditor) + .exists(), + ).toBe(true); + }); + + it('displays tab lazily, until editor is ready', async () => { + expect(findTabAt(0).attributes('lazy')).toBe('true'); + + findTextEditor().vm.$emit('editor-ready'); + + await nextTick(); + + expect(findTabAt(0).attributes('lazy')).toBe(undefined); + }); }); - it('displays editor tab lazily, until editor is ready', async () => { - createComponent({ data: { content: mockCiYml } }); + 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(findTabAt(0).attributes('lazy')).toBe('true'); + expect(findLoadingIcon().exists()).toBe(true); + expect(findPipelineGraph().exists()).toBe(false); + }); + }); - findEditorLite().vm.$emit('editor-ready'); - await nextTick(); + describe('with feature flag off', () => { + beforeEach(() => { + createComponent({ provide: { glFeatures: { ciConfigVisualizationTab: false } } }); + }); - expect(findTabAt(0).attributes('lazy')).toBe(undefined); + it('does not display the tab', () => { + expect(findVisualizationTab().exists()).toBe(false); + }); + }); }); }); - describe('when in error state', () => { - class MockError extends Error { - constructor(message, data) { - super(message); - if (data) { - this.networkError = { - response: { data }, - }; + describe('when data is set', () => { + beforeEach(async () => { + createComponent({ mountFn: mount }); + + await wrapper.setData({ + content: mockCiYml, + contentModel: mockCiYml, + }); + }); + + it('displays content after the query loads', () => { + expect(findLoadingIcon().exists()).toBe(false); + expect(findTextEditor().attributes('value')).toBe(mockCiYml); + }); + + describe('commit form', () => { + const mockVariables = { + content: mockCiYml, + filePath: mockCiConfigPath, + lastCommitId: mockCommitId, + message: mockCommitMessage, + projectPath: mockProjectPath, + 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('refreshes the page', () => { + expect(refreshCurrentPage).toHaveBeenCalled(); + }); + + it('shows no saving state', () => { + expect(findCommitBtnLoadingIcon().exists()).toBe(false); + }); + }); + + 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, + }, + }); + }); + + it('refreshes the page', () => { + expect(refreshCurrentPage).toHaveBeenCalledWith(); + }); + }); + + 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 a the error message', async () => { + mockMutate.mockRejectedValueOnce(new Error('commit failed')); - it('shows a generic error', () => { - const error = new MockError('An error message'); - createComponent({ data: { error } }); + await submitCommit(); - expect(findAlert().text()).toBe('CI file could not be loaded: An error message'); + 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(findTextEditor().attributes('value')).toBe(mockCiYml); + }); + }); + }); + }); + + describe('displays fetch content errors', () => { + it('no error is shown when data is set', async () => { + mockBlobContentData.mockResolvedValue(mockCiYml); + createComponentWithApollo(); + + await waitForPromises(); + + expect(findBlobFailureAlert().exists()).toBe(false); + expect(findTextEditor().attributes('value')).toBe(mockCiYml); }); - it('shows a ref missing error state', () => { - const error = new MockError('Ref missing!', { - error: 'ref is missing, ref is empty', + it('shows a 404 error message', async () => { + mockBlobContentData.mockRejectedValueOnce({ + response: { + status: 404, + }, }); - createComponent({ data: { error } }); + createComponentWithApollo(); + + await waitForPromises(); - expect(findAlert().text()).toMatch( - 'CI file could not be loaded: ref is missing, ref is empty', + expect(findBlobFailureAlert().text()).toBe( + 'No CI file found in this repository, please add one.', ); }); - it('shows a file missing error state', async () => { - const error = new MockError('File missing!', { - message: 'file not found', + it('shows a 400 error message', async () => { + mockBlobContentData.mockRejectedValueOnce({ + response: { + status: 400, + }, }); + createComponentWithApollo(); + + await waitForPromises(); + + expect(findBlobFailureAlert().text()).toBe( + 'Repository does not have a default branch, please set one.', + ); + }); - await wrapper.setData({ error }); + it('shows a unkown error message', async () => { + mockBlobContentData.mockRejectedValueOnce(new Error('My error!')); + createComponentWithApollo(); + await waitForPromises(); - expect(findAlert().text()).toMatch('CI file could not be loaded: file not found'); + expect(findBlobFailureAlert().text()).toBe( + 'The CI configuration was not loaded, please try again.', + ); }); }); }); |