Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-12-17 14:59:07 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2020-12-17 14:59:07 +0300
commit8b573c94895dc0ac0e1d9d59cf3e8745e8b539ca (patch)
tree544930fb309b30317ae9797a9683768705d664c4 /spec/frontend/pipeline_editor
parent4b1de649d0168371549608993deac953eb692019 (diff)
Add latest changes from gitlab-org/gitlab@13-7-stable-eev13.7.0-rc42
Diffstat (limited to 'spec/frontend/pipeline_editor')
-rw-r--r--spec/frontend/pipeline_editor/components/commit/commit_form_spec.js116
-rw-r--r--spec/frontend/pipeline_editor/components/lint/ci_lint_results_spec.js129
-rw-r--r--spec/frontend/pipeline_editor/components/lint/ci_lint_warnings_spec.js54
-rw-r--r--spec/frontend/pipeline_editor/components/text_editor_spec.js17
-rw-r--r--spec/frontend/pipeline_editor/graphql/__snapshots__/resolvers_spec.js.snap73
-rw-r--r--spec/frontend/pipeline_editor/graphql/resolvers_spec.js51
-rw-r--r--spec/frontend/pipeline_editor/mock_data.js97
-rw-r--r--spec/frontend/pipeline_editor/pipeline_editor_app_spec.js444
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.',
+ );
});
});
});