diff options
Diffstat (limited to 'spec/frontend/pipeline_editor')
8 files changed, 903 insertions, 78 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 new file mode 100644 index 00000000000..ae2a9e5065d --- /dev/null +++ b/spec/frontend/pipeline_editor/components/commit/commit_form_spec.js @@ -0,0 +1,116 @@ +import { shallowMount, mount } from '@vue/test-utils'; +import { GlFormInput, GlFormTextarea } from '@gitlab/ui'; + +import CommitForm from '~/pipeline_editor/components/commit/commit_form.vue'; + +import { mockCommitMessage, mockDefaultBranch } from '../../mock_data'; + +describe('~/pipeline_editor/pipeline_editor_app.vue', () => { + let wrapper; + + const createComponent = ({ props = {} } = {}, mountFn = shallowMount) => { + wrapper = mountFn(CommitForm, { + propsData: { + defaultMessage: mockCommitMessage, + defaultBranch: mockDefaultBranch, + ...props, + }, + + // attachToDocument is required for input/submit events + attachToDocument: mountFn === mount, + }); + }; + + const findCommitTextarea = () => wrapper.find(GlFormTextarea); + const findBranchInput = () => wrapper.find(GlFormInput); + const findNewMrCheckbox = () => wrapper.find('[data-testid="new-mr-checkbox"]'); + const findSubmitBtn = () => wrapper.find('[type="submit"]'); + const findCancelBtn = () => wrapper.find('[type="reset"]'); + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + describe('when the form is displayed', () => { + beforeEach(async () => { + createComponent(); + }); + + it('shows a default commit message', () => { + expect(findCommitTextarea().attributes('value')).toBe(mockCommitMessage); + }); + + it('shows a default branch', () => { + expect(findBranchInput().attributes('value')).toBe(mockDefaultBranch); + }); + + it('shows buttons', () => { + expect(findSubmitBtn().exists()).toBe(true); + expect(findCancelBtn().exists()).toBe(true); + }); + + it('does not show a new MR checkbox by default', () => { + expect(findNewMrCheckbox().exists()).toBe(false); + }); + }); + + describe('when buttons are clicked', () => { + beforeEach(async () => { + createComponent({}, mount); + }); + + it('emits an event when the form submits', () => { + findSubmitBtn().trigger('click'); + + expect(wrapper.emitted('submit')[0]).toEqual([ + { + message: mockCommitMessage, + branch: mockDefaultBranch, + openMergeRequest: false, + }, + ]); + }); + + it('emits an event when the form resets', () => { + findCancelBtn().trigger('click'); + + expect(wrapper.emitted('cancel')).toHaveLength(1); + }); + }); + + describe('when user inputs values', () => { + const anotherMessage = 'Another commit message'; + const anotherBranch = 'my-branch'; + + beforeEach(() => { + createComponent({}, mount); + + findCommitTextarea().setValue(anotherMessage); + findBranchInput().setValue(anotherBranch); + }); + + it('shows a new MR checkbox', () => { + expect(findNewMrCheckbox().exists()).toBe(true); + }); + + it('emits an event with values', async () => { + await findNewMrCheckbox().setChecked(); + await findSubmitBtn().trigger('click'); + + expect(wrapper.emitted('submit')[0]).toEqual([ + { + message: anotherMessage, + branch: anotherBranch, + openMergeRequest: true, + }, + ]); + }); + + it('when the commit message is empty, submit button is disabled', async () => { + await findCommitTextarea().setValue(''); + + expect(findSubmitBtn().attributes('disabled')).toBe('disabled'); + }); + }); +}); 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 new file mode 100644 index 00000000000..e9c6ed60860 --- /dev/null +++ b/spec/frontend/pipeline_editor/components/lint/ci_lint_results_spec.js @@ -0,0 +1,129 @@ +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 { capitalizeFirstCharacter } from '~/lib/utils/text_utility'; +import { mockJobs, mockErrors, mockWarnings } from '../../mock_data'; + +describe('CI Lint Results', () => { + let wrapper; + const defaultProps = { + valid: true, + jobs: mockJobs, + errors: [], + warnings: [], + dryRun: false, + lintHelpPagePath: '/help', + }; + + const createComponent = (props = {}, mountFn = shallowMount) => { + wrapper = mountFn(CiLintResults, { + propsData: { + ...defaultProps, + ...props, + }, + }); + }; + + const findTable = () => wrapper.find(GlTable); + const findByTestId = selector => () => wrapper.find(`[data-testid="ci-lint-${selector}"]`); + const findAllByTestId = selector => () => wrapper.findAll(`[data-testid="ci-lint-${selector}"]`); + const findLinkToDoc = () => wrapper.find(GlLink); + const findErrors = findByTestId('errors'); + const findWarnings = findByTestId('warnings'); + const findStatus = findByTestId('status'); + const findOnlyExcept = findByTestId('only-except'); + const findLintParameters = findAllByTestId('parameter'); + const findBeforeScripts = findAllByTestId('before-script'); + const findScripts = findAllByTestId('script'); + const findAfterScripts = findAllByTestId('after-script'); + const filterEmptyScripts = property => mockJobs.filter(job => job[property].length !== 0); + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + describe('Invalid results', () => { + beforeEach(() => { + createComponent({ valid: false, errors: mockErrors, warnings: mockWarnings }, mount); + }); + + it('does not display the table', () => { + expect(findTable().exists()).toBe(false); + }); + + it('displays the invalid status', () => { + expect(findStatus().text()).toContain(`Status: ${wrapper.vm.$options.incorrect.text}`); + expect(findStatus().props('variant')).toBe(wrapper.vm.$options.incorrect.variant); + }); + + it('contains the link to documentation', () => { + expect(findLinkToDoc().text()).toBe('More information'); + expect(findLinkToDoc().attributes('href')).toBe(defaultProps.lintHelpPagePath); + }); + + it('displays the error message', () => { + const [expectedError] = mockErrors; + + expect(findErrors().text()).toBe(expectedError); + }); + + it('displays the warning message', () => { + const [expectedWarning] = mockWarnings; + + expect(findWarnings().exists()).toBe(true); + expect(findWarnings().text()).toContain(expectedWarning); + }); + }); + + describe('Valid results with dry run', () => { + beforeEach(() => { + createComponent({ dryRun: true }, mount); + }); + + it('displays table', () => { + expect(findTable().exists()).toBe(true); + }); + + it('displays the valid status', () => { + expect(findStatus().text()).toContain(wrapper.vm.$options.correct.text); + expect(findStatus().props('variant')).toBe(wrapper.vm.$options.correct.variant); + }); + + it('does not display only/expect values with dry run', () => { + expect(findOnlyExcept().exists()).toBe(false); + }); + + it('contains the link to documentation', () => { + expect(findLinkToDoc().text()).toBe('More information'); + expect(findLinkToDoc().attributes('href')).toBe(defaultProps.lintHelpPagePath); + }); + }); + + describe('Lint results', () => { + beforeEach(() => { + createComponent({}, mount); + }); + + it('formats parameter value', () => { + findLintParameters().wrappers.forEach((job, index) => { + const { stage } = mockJobs[index]; + const { name } = mockJobs[index]; + + expect(job.text()).toBe(`${capitalizeFirstCharacter(stage)} Job - ${name}`); + }); + }); + + it('only shows before scripts when data is present', () => { + expect(findBeforeScripts()).toHaveLength(filterEmptyScripts('beforeScript').length); + }); + + it('only shows script when data is present', () => { + expect(findScripts()).toHaveLength(filterEmptyScripts('script').length); + }); + + it('only shows after script when data is present', () => { + expect(findAfterScripts()).toHaveLength(filterEmptyScripts('afterScript').length); + }); + }); +}); 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 new file mode 100644 index 00000000000..b441d26c146 --- /dev/null +++ b/spec/frontend/pipeline_editor/components/lint/ci_lint_warnings_spec.js @@ -0,0 +1,54 @@ +import { mount } from '@vue/test-utils'; +import { GlAlert, GlSprintf } from '@gitlab/ui'; +import { trimText } from 'helpers/text_helper'; +import CiLintWarnings from '~/pipeline_editor/components/lint/ci_lint_warnings.vue'; + +const warnings = ['warning 1', 'warning 2', 'warning 3']; + +describe('CI lint warnings', () => { + let wrapper; + + const createComponent = (limit = 25) => { + wrapper = mount(CiLintWarnings, { + propsData: { + warnings, + maxWarnings: limit, + }, + }); + }; + + const findWarningAlert = () => wrapper.find(GlAlert); + const findWarnings = () => wrapper.findAll('[data-testid="ci-lint-warning"]'); + const findWarningMessage = () => trimText(wrapper.find(GlSprintf).text()); + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + it('displays the warning alert', () => { + createComponent(); + + expect(findWarningAlert().exists()).toBe(true); + }); + + it('displays all the warnings', () => { + createComponent(); + + expect(findWarnings()).toHaveLength(warnings.length); + }); + + it('shows the correct message when the limit is not passed', () => { + createComponent(); + + expect(findWarningMessage()).toBe(`${warnings.length} warnings found:`); + }); + + it('shows the correct message when the limit is passed', () => { + const limit = 2; + + createComponent(limit); + + expect(findWarningMessage()).toBe(`${warnings.length} warnings found: showing first ${limit}`); + }); +}); diff --git a/spec/frontend/pipeline_editor/components/text_editor_spec.js b/spec/frontend/pipeline_editor/components/text_editor_spec.js index 39d205839f4..18f71ebc95c 100644 --- a/spec/frontend/pipeline_editor/components/text_editor_spec.js +++ b/spec/frontend/pipeline_editor/components/text_editor_spec.js @@ -6,12 +6,16 @@ import TextEditor from '~/pipeline_editor/components/text_editor.vue'; describe('~/pipeline_editor/components/text_editor.vue', () => { let wrapper; + const editorReadyListener = jest.fn(); - const createComponent = (props = {}, mountFn = shallowMount) => { + const createComponent = (attrs = {}, mountFn = shallowMount) => { wrapper = mountFn(TextEditor, { - propsData: { + attrs: { value: mockCiYml, - ...props, + ...attrs, + }, + listeners: { + 'editor-ready': editorReadyListener, }, }); }; @@ -28,14 +32,13 @@ describe('~/pipeline_editor/components/text_editor.vue', () => { expect(findEditor().props('value')).toBe(mockCiYml); }); - it('editor is readony and configured for .yml', () => { - expect(findEditor().props('editorOptions')).toEqual({ readOnly: true }); + it('editor is configured for .yml', () => { expect(findEditor().props('fileName')).toBe('*.yml'); }); - it('bubbles up editor-ready event', () => { + it('bubbles up events', () => { findEditor().vm.$emit('editor-ready'); - expect(wrapper.emitted('editor-ready')).toHaveLength(1); + expect(editorReadyListener).toHaveBeenCalled(); }); }); diff --git a/spec/frontend/pipeline_editor/graphql/__snapshots__/resolvers_spec.js.snap b/spec/frontend/pipeline_editor/graphql/__snapshots__/resolvers_spec.js.snap new file mode 100644 index 00000000000..d7d4d0af90c --- /dev/null +++ b/spec/frontend/pipeline_editor/graphql/__snapshots__/resolvers_spec.js.snap @@ -0,0 +1,73 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`~/pipeline_editor/graphql/resolvers Mutation lintCI lint data is as expected 1`] = ` +Object { + "__typename": "CiLintContent", + "errors": Array [], + "jobs": Array [ + Object { + "__typename": "CiLintJob", + "afterScript": Array [ + "echo 'after script 1", + ], + "allowFailure": false, + "beforeScript": Array [ + "echo 'before script 1'", + ], + "environment": "prd", + "except": Object { + "refs": Array [ + "master@gitlab-org/gitlab", + "/^release/.*$/@gitlab-org/gitlab", + ], + }, + "name": "job_1", + "only": null, + "script": Array [ + "echo 'script 1'", + ], + "stage": "test", + "tagList": Array [ + "tag 1", + ], + "when": "on_success", + }, + Object { + "__typename": "CiLintJob", + "afterScript": Array [ + "echo 'after script 2", + ], + "allowFailure": true, + "beforeScript": Array [ + "echo 'before script 2'", + ], + "environment": "stg", + "except": Object { + "refs": Array [ + "master@gitlab-org/gitlab", + "/^release/.*$/@gitlab-org/gitlab", + ], + }, + "name": "job_2", + "only": Object { + "__typename": "CiLintJobOnlyPolicy", + "refs": Array [ + "web", + "chat", + "pushes", + ], + }, + "script": Array [ + "echo 'script 2'", + ], + "stage": "test", + "tagList": Array [ + "tag 2", + ], + "when": "on_success", + }, + ], + "valid": true, + "warnings": Array [], +} +`; diff --git a/spec/frontend/pipeline_editor/graphql/resolvers_spec.js b/spec/frontend/pipeline_editor/graphql/resolvers_spec.js index 90acdf3ec0b..b531f8af797 100644 --- a/spec/frontend/pipeline_editor/graphql/resolvers_spec.js +++ b/spec/frontend/pipeline_editor/graphql/resolvers_spec.js @@ -1,6 +1,14 @@ +import MockAdapter from 'axios-mock-adapter'; import Api from '~/api'; -import { mockProjectPath, mockDefaultBranch, mockCiConfigPath, mockCiYml } from '../mock_data'; - +import { + mockCiConfigPath, + mockCiYml, + mockDefaultBranch, + mockLintResponse, + mockProjectPath, +} 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', () => { @@ -39,4 +47,43 @@ describe('~/pipeline_editor/graphql/resolvers', () => { }); }); }); + + describe('Mutation', () => { + describe('lintCI', () => { + let mock; + let result; + + const endpoint = '/ci/lint'; + + beforeEach(async () => { + mock = new MockAdapter(axios); + mock.onPost(endpoint).reply(httpStatus.OK, mockLintResponse); + + result = await resolvers.Mutation.lintCI(null, { + endpoint, + content: 'content', + dry_run: true, + }); + }); + + afterEach(() => { + mock.restore(); + }); + + /* eslint-disable no-underscore-dangle */ + it('lint data has correct type names', async () => { + expect(result.__typename).toBe('CiLintContent'); + + expect(result.jobs[0].__typename).toBe('CiLintJob'); + expect(result.jobs[1].__typename).toBe('CiLintJob'); + + expect(result.jobs[1].only.__typename).toBe('CiLintJobOnlyPolicy'); + }); + /* eslint-enable no-underscore-dangle */ + + it('lint data is as expected', () => { + expect(result).toMatchSnapshot(); + }); + }); + }); }); diff --git a/spec/frontend/pipeline_editor/mock_data.js b/spec/frontend/pipeline_editor/mock_data.js index 96fa6e5e004..d882490c272 100644 --- a/spec/frontend/pipeline_editor/mock_data.js +++ b/spec/frontend/pipeline_editor/mock_data.js @@ -1,5 +1,8 @@ export const mockProjectPath = 'user1/project1'; export const mockDefaultBranch = 'master'; +export const mockNewMergeRequestPath = '/-/merge_requests/new'; +export const mockCommitId = 'aabbccdd'; +export const mockCommitMessage = 'My commit message'; export const mockCiConfigPath = '.gitlab-ci.yml'; export const mockCiYml = ` @@ -8,3 +11,97 @@ job1: script: - echo 'test' `; + +export const mockCiConfigQueryResponse = { + data: { + ciConfig: { + errors: [], + stages: [], + status: '', + }, + }, +}; + +export const mockLintResponse = { + valid: true, + errors: [], + warnings: [], + jobs: [ + { + name: 'job_1', + stage: 'test', + before_script: ["echo 'before script 1'"], + script: ["echo 'script 1'"], + after_script: ["echo 'after script 1"], + tag_list: ['tag 1'], + environment: 'prd', + when: 'on_success', + allow_failure: false, + only: null, + except: { refs: ['master@gitlab-org/gitlab', '/^release/.*$/@gitlab-org/gitlab'] }, + }, + { + name: 'job_2', + stage: 'test', + before_script: ["echo 'before script 2'"], + script: ["echo 'script 2'"], + after_script: ["echo 'after script 2"], + tag_list: ['tag 2'], + environment: 'stg', + when: 'on_success', + allow_failure: true, + only: { refs: ['web', 'chat', 'pushes'] }, + except: { refs: ['master@gitlab-org/gitlab', '/^release/.*$/@gitlab-org/gitlab'] }, + }, + ], +}; + +export const mockJobs = [ + { + name: 'job_1', + stage: 'build', + beforeScript: [], + script: ["echo 'Building'"], + afterScript: [], + tagList: [], + environment: null, + when: 'on_success', + allowFailure: true, + only: { refs: ['web', 'chat', 'pushes'] }, + except: null, + }, + { + name: 'multi_project_job', + stage: 'test', + beforeScript: [], + script: [], + afterScript: [], + tagList: [], + environment: null, + when: 'on_success', + allowFailure: false, + only: { refs: ['branches', 'tags'] }, + except: null, + }, + { + name: 'job_2', + stage: 'test', + beforeScript: ["echo 'before script'"], + script: ["echo 'script'"], + afterScript: ["echo 'after script"], + tagList: [], + environment: null, + when: 'on_success', + allowFailure: false, + only: { refs: ['branches@gitlab-org/gitlab'] }, + except: { refs: ['master@gitlab-org/gitlab', '/^release/.*$/@gitlab-org/gitlab'] }, + }, +]; + +export const mockErrors = [ + '"job_1 job: chosen stage does not exist; available stages are .pre, build, test, deploy, .post"', +]; + +export const mockWarnings = [ + '"jobs:multi_project_job may allow multiple pipelines to run for a single action due to `rules:when` clause with no `workflow:rules` - read more: https://docs.gitlab.com/ee/ci/troubleshooting.html#pipeline-warnings"', +]; 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.', + ); }); }); }); |