Commit 8f07b5a4 authored by Miguel Rincon's avatar Miguel Rincon

Add static validation to gitlab CI yaml

This change fetches the external YML schema for the gitlab-ci file
and adds it to the Pipeline/CI Editor.

Adds option `inheritAttrs: false` to prevent the
contents of the file from being added to the text editor <div>
container.
parent aaec0061
<script>
import EditorLite from '~/vue_shared/components/editor_lite.vue';
import { CiSchemaExtension } from '~/editor/editor_ci_schema_ext';
export default {
components: {
EditorLite,
},
inheritAttrs: false,
props: {
ciConfigPath: {
type: String,
required: true,
},
commitSha: {
type: String,
required: false,
default: null,
},
projectPath: {
type: String,
required: true,
},
},
methods: {
onEditorReady() {
const editorInstance = this.$refs.editor.getEditor();
const [projectNamespace, projectPath] = this.projectPath.split('/');
editorInstance.use(new CiSchemaExtension());
editorInstance.registerCiSchema({
projectPath,
projectNamespace,
ref: this.commitSha,
});
},
},
};
</script>
<template>
<div class="gl-border-solid gl-border-gray-100 gl-border-1">
<editor-lite file-name="*.yml" v-bind="$attrs" v-on="$listeners" />
<editor-lite
ref="editor"
:file-name="ciConfigPath"
v-bind="$attrs"
@editor-ready="onEditorReady"
v-on="$listeners"
/>
</div>
</template>
......@@ -277,7 +277,13 @@ export default {
<!-- editor should be mounted when its tab is visible, so the container has a size -->
<gl-tab :title="$options.i18n.tabEdit" :lazy="!editorIsReady">
<!-- editor should be mounted only once, when the tab is displayed -->
<text-editor v-model="contentModel" @editor-ready="editorIsReady = true" />
<text-editor
v-model="contentModel"
:ci-config-path="ciConfigPath"
:commit-sha="lastCommitSha"
:project-path="projectPath"
@editor-ready="editorIsReady = true"
/>
</gl-tab>
<gl-tab
......
import { shallowMount } from '@vue/test-utils';
import EditorLite from '~/vue_shared/components/editor_lite.vue';
import { mockCiYml } from '../mock_data';
import {
mockCiConfigPath,
mockCiYml,
mockCommitSha,
mockProjectPath,
mockNamespace,
mockProjectName,
} from '../mock_data';
import TextEditor from '~/pipeline_editor/components/text_editor.vue';
describe('~/pipeline_editor/components/text_editor.vue', () => {
let wrapper;
const editorReadyListener = jest.fn();
const createComponent = (attrs = {}, mountFn = shallowMount) => {
let editorReadyListener;
let mockUse;
let mockRegisterCiSchema;
const MockEditorLite = {
template: '<div/>',
props: ['value', 'fileName'],
mounted() {
this.$emit('editor-ready');
},
methods: {
getEditor: () => ({
use: mockUse,
registerCiSchema: mockRegisterCiSchema,
}),
},
};
const createComponent = (opts = {}, mountFn = shallowMount) => {
wrapper = mountFn(TextEditor, {
propsData: {
ciConfigPath: mockCiConfigPath,
commitSha: mockCommitSha,
projectPath: mockProjectPath,
},
attrs: {
value: mockCiYml,
...attrs,
},
listeners: {
'editor-ready': editorReadyListener,
},
stubs: {
EditorLite: MockEditorLite,
},
...opts,
});
};
const findEditor = () => wrapper.find(EditorLite);
const findEditor = () => wrapper.find(MockEditorLite);
beforeEach(() => {
editorReadyListener = jest.fn();
mockUse = jest.fn();
mockRegisterCiSchema = jest.fn();
it('contains an editor', () => {
createComponent();
});
it('contains an editor', () => {
expect(findEditor().exists()).toBe(true);
});
......@@ -32,8 +69,18 @@ describe('~/pipeline_editor/components/text_editor.vue', () => {
expect(findEditor().props('value')).toBe(mockCiYml);
});
it('editor is configured for .yml', () => {
expect(findEditor().props('fileName')).toBe('*.yml');
it('editor is configured for the CI config path', () => {
expect(findEditor().props('fileName')).toBe(mockCiConfigPath);
});
it('editor is configured with syntax highligting', async () => {
expect(mockUse).toHaveBeenCalledTimes(1);
expect(mockRegisterCiSchema).toHaveBeenCalledTimes(1);
expect(mockRegisterCiSchema).toHaveBeenCalledWith({
projectNamespace: mockNamespace,
projectPath: mockProjectName,
ref: mockCommitSha,
});
});
it('bubbles up events', () => {
......
import { CI_CONFIG_STATUS_VALID } from '~/pipeline_editor/constants';
export const mockProjectPath = 'user1/project1';
export const mockNamespace = 'user1';
export const mockProjectName = 'project1';
export const mockProjectPath = `${mockNamespace}/${mockProjectName}`;
export const mockDefaultBranch = 'master';
export const mockNewMergeRequestPath = '/-/merge_requests/new';
export const mockCommitSha = 'aabbccdd';
......
......@@ -42,6 +42,10 @@ jest.mock('~/lib/utils/url_utility', () => ({
mergeUrlParams: jest.requireActual('~/lib/utils/url_utility').mergeUrlParams,
}));
const MockEditorLite = {
template: '<div/>',
};
describe('~/pipeline_editor/pipeline_editor_app.vue', () => {
let wrapper;
......@@ -87,9 +91,7 @@ describe('~/pipeline_editor/pipeline_editor_app.vue', () => {
GlTabs,
GlButton,
CommitForm,
EditorLite: {
template: '<div/>',
},
EditorLite: MockEditorLite,
TextEditor,
},
mocks: {
......@@ -140,6 +142,7 @@ describe('~/pipeline_editor/pipeline_editor_app.vue', () => {
const findTabAt = i => wrapper.findAll(GlTab).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);
......@@ -236,7 +239,14 @@ describe('~/pipeline_editor/pipeline_editor_app.vue', () => {
it('displays content after the query loads', () => {
expect(findLoadingIcon().exists()).toBe(false);
expect(findTextEditor().attributes('value')).toBe(mockCiYml);
expect(findEditorLite().attributes('value')).toBe(mockCiYml);
expect(findEditorLite().attributes('file-name')).toBe(mockCiConfigPath);
});
it('configures text editor', () => {
expect(findTextEditor().props('commitSha')).toBe(mockCommitSha);
expect(findTextEditor().props('projectPath')).toBe(mockProjectPath);
});
describe('commit form', () => {
......@@ -389,7 +399,7 @@ describe('~/pipeline_editor/pipeline_editor_app.vue', () => {
it('content is restored after cancel is called', async () => {
await cancelCommitForm();
expect(findTextEditor().attributes('value')).toBe(mockCiYml);
expect(findEditorLite().attributes('value')).toBe(mockCiYml);
});
});
});
......@@ -403,7 +413,7 @@ describe('~/pipeline_editor/pipeline_editor_app.vue', () => {
await waitForPromises();
expect(findAlert().exists()).toBe(false);
expect(findTextEditor().attributes('value')).toBe(mockCiYml);
expect(findEditorLite().attributes('value')).toBe(mockCiYml);
});
it('shows a 404 error message', async () => {
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment