Commit 80a8670e authored by Frédéric Caplette's avatar Frédéric Caplette Committed by Andrew Fontaine

Break down pipeline editor into smaller components

This refactor will allow the new pipeline editor section
to grow organically from an architectural POV. Each section
is now its own component and at the very top, we have the app
that fetches the graphQL queries and pasases it down to all
other part of the applications that need it.
parent 5d588d80
<script>
import CommitForm from './commit_form.vue';
import commitCIFile from '../../graphql/mutations/commit_ci_file.mutation.graphql';
import getCommitSha from '../../graphql/queries/client/commit_sha.graphql';
import { COMMIT_FAILURE, COMMIT_SUCCESS } from '../../constants';
import { mergeUrlParams, redirectTo } from '~/lib/utils/url_utility';
import { __, s__, sprintf } from '~/locale';
const MR_SOURCE_BRANCH = 'merge_request[source_branch]';
const MR_TARGET_BRANCH = 'merge_request[target_branch]';
export default {
alertTexts: {
[COMMIT_FAILURE]: s__('Pipelines|The GitLab CI configuration could not be updated.'),
[COMMIT_SUCCESS]: __('Your changes have been successfully committed.'),
},
i18n: {
defaultCommitMessage: __('Update %{sourcePath} file'),
},
components: {
CommitForm,
},
inject: ['projectFullPath', 'ciConfigPath', 'defaultBranch', 'newMergeRequestPath'],
props: {
ciFileContent: {
type: String,
required: true,
},
},
data() {
return {
commit: {},
isSaving: false,
};
},
apollo: {
commitSha: {
query: getCommitSha,
},
},
computed: {
defaultCommitMessage() {
return sprintf(this.$options.i18n.defaultCommitMessage, { sourcePath: this.ciConfigPath });
},
},
methods: {
redirectToNewMergeRequest(sourceBranch) {
const url = mergeUrlParams(
{
[MR_SOURCE_BRANCH]: sourceBranch,
[MR_TARGET_BRANCH]: this.defaultBranch,
},
this.newMergeRequestPath,
);
redirectTo(url);
},
async onCommitSubmit({ message, branch, openMergeRequest }) {
this.isSaving = true;
try {
const {
data: {
commitCreate: { errors },
},
} = await this.$apollo.mutate({
mutation: commitCIFile,
variables: {
projectPath: this.projectFullPath,
branch,
startBranch: this.defaultBranch,
message,
filePath: this.ciConfigPath,
content: this.ciFileContent,
lastCommitId: this.commitSha,
},
update(store, { data }) {
const commitSha = data?.commitCreate?.commit?.sha;
if (commitSha) {
store.writeQuery({ query: getCommitSha, data: { commitSha } });
}
},
});
if (errors?.length) {
this.$emit('showError', { type: COMMIT_FAILURE, reasons: errors });
} else if (openMergeRequest) {
this.redirectToNewMergeRequest(branch);
} else {
this.$emit('commit', { type: COMMIT_SUCCESS });
}
} catch (error) {
this.$emit('showError', { type: COMMIT_FAILURE, reasons: [error?.message] });
} finally {
this.isSaving = false;
}
},
onCommitCancel() {
this.$emit('resetContent');
},
},
};
</script>
<template>
<commit-form
:default-branch="defaultBranch"
:default-message="defaultCommitMessage"
:is-saving="isSaving"
@cancel="onCommitCancel"
@submit="onCommitSubmit"
/>
</template>
<script>
import ValidationSegment from './validation_segment.vue';
export default {
validationSegmentClasses:
'gl-p-5 gl-bg-gray-10 gl-border-solid gl-border-1 gl-border-gray-100 gl-rounded-base',
components: {
ValidationSegment,
},
props: {
ciConfigData: {
type: Object,
required: true,
},
isCiConfigDataLoading: {
type: Boolean,
required: true,
},
},
};
</script>
<template>
<div class="gl-mb-5">
<validation-segment
:class="$options.validationSegmentClasses"
:loading="isCiConfigDataLoading"
:ci-config="ciConfigData"
/>
</div>
</template>
<script>
import { GlLoadingIcon, GlTabs, GlTab } from '@gitlab/ui';
import { s__ } from '~/locale';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import CiLint from './lint/ci_lint.vue';
import EditorTab from './ui/editor_tab.vue';
import TextEditor from './text_editor.vue';
import PipelineGraph from '~/pipelines/components/pipeline_graph/pipeline_graph.vue';
export default {
i18n: {
tabEdit: s__('Pipelines|Write pipeline configuration'),
tabGraph: s__('Pipelines|Visualize'),
tabLint: s__('Pipelines|Lint'),
},
components: {
CiLint,
EditorTab,
GlLoadingIcon,
GlTab,
GlTabs,
PipelineGraph,
TextEditor,
},
mixins: [glFeatureFlagsMixin()],
props: {
ciConfigData: {
type: Object,
required: true,
},
ciFileContent: {
type: String,
required: true,
},
isCiConfigDataLoading: {
type: Boolean,
required: false,
default: false,
},
},
};
</script>
<template>
<gl-tabs class="file-editor gl-mb-3">
<editor-tab :title="$options.i18n.tabEdit" lazy data-testid="editor-tab">
<text-editor :value="ciFileContent" v-on="$listeners" />
</editor-tab>
<gl-tab
v-if="glFeatures.ciConfigVisualizationTab"
:title="$options.i18n.tabGraph"
lazy
data-testid="visualization-tab"
>
<gl-loading-icon v-if="isCiConfigDataLoading" size="lg" class="gl-m-3" />
<pipeline-graph v-else :pipeline-data="ciConfigData" />
</gl-tab>
<editor-tab :title="$options.i18n.tabLint" data-testid="lint-tab">
<gl-loading-icon v-if="isCiConfigDataLoading" size="lg" class="gl-m-3" />
<ci-lint v-else :ci-config="ciConfigData" />
</editor-tab>
</gl-tabs>
</template>
...@@ -2,26 +2,29 @@ ...@@ -2,26 +2,29 @@
import EditorLite from '~/vue_shared/components/editor_lite.vue'; import EditorLite from '~/vue_shared/components/editor_lite.vue';
import { CiSchemaExtension } from '~/editor/extensions/editor_ci_schema_ext'; import { CiSchemaExtension } from '~/editor/extensions/editor_ci_schema_ext';
import { EDITOR_READY_EVENT } from '~/editor/constants'; import { EDITOR_READY_EVENT } from '~/editor/constants';
import getCommitSha from '../graphql/queries/client/commit_sha.graphql';
export default { export default {
components: { components: {
EditorLite, EditorLite,
}, },
inject: ['projectPath', 'projectNamespace'], inject: ['ciConfigPath', 'projectPath', 'projectNamespace'],
inheritAttrs: false, inheritAttrs: false,
props: { data() {
ciConfigPath: { return {
type: String, commitSha: '',
required: true, };
}, },
apollo: {
commitSha: { commitSha: {
type: String, query: getCommitSha,
required: false,
default: null,
}, },
}, },
methods: { methods: {
onEditorReady() { onCiConfigUpdate(content) {
this.$emit('updateCiConfig', content);
},
registerCiSchema() {
const editorInstance = this.$refs.editor.getEditor(); const editorInstance = this.$refs.editor.getEditor();
editorInstance.use(new CiSchemaExtension()); editorInstance.use(new CiSchemaExtension());
...@@ -41,7 +44,8 @@ export default { ...@@ -41,7 +44,8 @@ export default {
ref="editor" ref="editor"
:file-name="ciConfigPath" :file-name="ciConfigPath"
v-bind="$attrs" v-bind="$attrs"
@[$options.readyEvent]="onEditorReady" @[$options.readyEvent]="registerCiSchema"
@input="onCiConfigUpdate"
v-on="$listeners" v-on="$listeners"
/> />
</div> </div>
......
export const CI_CONFIG_STATUS_VALID = 'VALID'; export const CI_CONFIG_STATUS_VALID = 'VALID';
export const CI_CONFIG_STATUS_INVALID = 'INVALID'; export const CI_CONFIG_STATUS_INVALID = 'INVALID';
export const COMMIT_FAILURE = 'COMMIT_FAILURE';
export const COMMIT_SUCCESS = 'COMMIT_SUCCESS';
export const DEFAULT_FAILURE = 'DEFAULT_FAILURE';
export const LOAD_FAILURE_NO_FILE = 'LOAD_FAILURE_NO_FILE';
export const LOAD_FAILURE_UNKNOWN = 'LOAD_FAILURE_UNKNOWN';
mutation commitCIFileMutation( mutation commitCIFile(
$projectPath: ID! $projectPath: ID!
$branch: String! $branch: String!
$startBranch: String $startBranch: String
......
...@@ -15,14 +15,13 @@ export const initPipelineEditor = (selector = '#js-pipeline-editor') => { ...@@ -15,14 +15,13 @@ export const initPipelineEditor = (selector = '#js-pipeline-editor') => {
} }
const { const {
// props // Add to apollo cache as it can be updated by future queries
ciConfigPath,
commitSha, commitSha,
// Add to provide/inject API for static values
ciConfigPath,
defaultBranch, defaultBranch,
newMergeRequestPath,
// `provide/inject` data
lintHelpPagePath, lintHelpPagePath,
newMergeRequestPath,
projectFullPath, projectFullPath,
projectPath, projectPath,
projectNamespace, projectNamespace,
...@@ -35,25 +34,27 @@ export const initPipelineEditor = (selector = '#js-pipeline-editor') => { ...@@ -35,25 +34,27 @@ export const initPipelineEditor = (selector = '#js-pipeline-editor') => {
defaultClient: createDefaultClient(resolvers, { typeDefs }), defaultClient: createDefaultClient(resolvers, { typeDefs }),
}); });
apolloProvider.clients.defaultClient.cache.writeData({
data: {
commitSha,
},
});
return new Vue({ return new Vue({
el, el,
apolloProvider, apolloProvider,
provide: { provide: {
ciConfigPath,
defaultBranch,
lintHelpPagePath, lintHelpPagePath,
newMergeRequestPath,
projectFullPath, projectFullPath,
projectPath, projectPath,
projectNamespace, projectNamespace,
ymlHelpPagePath, ymlHelpPagePath,
}, },
render(h) { render(h) {
return h(PipelineEditorApp, { return h(PipelineEditorApp);
props: {
ciConfigPath,
commitSha,
defaultBranch,
newMergeRequestPath,
},
});
}, },
}); });
}; };
<script>
import CommitSection from './components/commit/commit_section.vue';
import PipelineEditorTabs from './components/pipeline_editor_tabs.vue';
import PipelineEditorHeader from './components/header/pipeline_editor_header.vue';
export default {
components: {
CommitSection,
PipelineEditorHeader,
PipelineEditorTabs,
},
props: {
ciConfigData: {
type: Object,
required: true,
},
ciFileContent: {
type: String,
required: true,
},
isCiConfigDataLoading: {
type: Boolean,
required: true,
},
},
};
</script>
<template>
<div>
<pipeline-editor-header
:ci-config-data="ciConfigData"
:is-ci-config-data-loading="isCiConfigDataLoading"
/>
<pipeline-editor-tabs
:ci-config-data="ciConfigData"
:ci-file-content="ciFileContent"
:is-ci-config-data-loading="isCiConfigDataLoading"
v-on="$listeners"
/>
<commit-section :ci-file-content="ciFileContent" v-on="$listeners" />
</div>
</template>
...@@ -5,7 +5,7 @@ import CommitForm from '~/pipeline_editor/components/commit/commit_form.vue'; ...@@ -5,7 +5,7 @@ import CommitForm from '~/pipeline_editor/components/commit/commit_form.vue';
import { mockCommitMessage, mockDefaultBranch } from '../../mock_data'; import { mockCommitMessage, mockDefaultBranch } from '../../mock_data';
describe('~/pipeline_editor/pipeline_editor_app.vue', () => { describe('Pipeline Editor | Commit Form', () => {
let wrapper; let wrapper;
const createComponent = ({ props = {} } = {}, mountFn = shallowMount) => { const createComponent = ({ props = {} } = {}, mountFn = shallowMount) => {
...@@ -21,8 +21,8 @@ describe('~/pipeline_editor/pipeline_editor_app.vue', () => { ...@@ -21,8 +21,8 @@ describe('~/pipeline_editor/pipeline_editor_app.vue', () => {
}); });
}; };
const findCommitTextarea = () => wrapper.find(GlFormTextarea); const findCommitTextarea = () => wrapper.findComponent(GlFormTextarea);
const findBranchInput = () => wrapper.find(GlFormInput); const findBranchInput = () => wrapper.findComponent(GlFormInput);
const findNewMrCheckbox = () => wrapper.find('[data-testid="new-mr-checkbox"]'); const findNewMrCheckbox = () => wrapper.find('[data-testid="new-mr-checkbox"]');
const findSubmitBtn = () => wrapper.find('[type="submit"]'); const findSubmitBtn = () => wrapper.find('[type="submit"]');
const findCancelBtn = () => wrapper.find('[type="reset"]'); const findCancelBtn = () => wrapper.find('[type="reset"]');
......
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);
});
});
});
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);
});
});
});
...@@ -3,7 +3,9 @@ import { shallowMount } from '@vue/test-utils'; ...@@ -3,7 +3,9 @@ import { shallowMount } from '@vue/test-utils';
import { GlIcon } from '@gitlab/ui'; import { GlIcon } from '@gitlab/ui';
import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import { sprintf } from '~/locale'; 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 { CI_CONFIG_STATUS_INVALID } from '~/pipeline_editor/constants';
import { mockYmlHelpPagePath, mergeUnwrappedCiConfig } from '../../mock_data'; import { mockYmlHelpPagePath, mergeUnwrappedCiConfig } from '../../mock_data';
...@@ -29,6 +31,11 @@ describe('~/pipeline_editor/components/info/validation_segment.vue', () => { ...@@ -29,6 +31,11 @@ describe('~/pipeline_editor/components/info/validation_segment.vue', () => {
const findLearnMoreLink = () => wrapper.findByTestId('learnMoreLink'); const findLearnMoreLink = () => wrapper.findByTestId('learnMoreLink');
const findValidationMsg = () => wrapper.findByTestId('validationMsg'); const findValidationMsg = () => wrapper.findByTestId('validationMsg');
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
it('shows the loading state', () => { it('shows the loading state', () => {
createComponent({ loading: true }); createComponent({ loading: true });
......
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);
});
});
});
});
});
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import { EDITOR_READY_EVENT } from '~/editor/constants';
import TextEditor from '~/pipeline_editor/components/text_editor.vue';
import { import {
mockCiConfigPath, mockCiConfigPath,
mockCiYml, mockCiYml,
...@@ -10,7 +8,10 @@ import { ...@@ -10,7 +8,10 @@ import {
mockProjectNamespace, mockProjectNamespace,
} from '../mock_data'; } 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 wrapper;
let editorReadyListener; let editorReadyListener;
...@@ -36,14 +37,17 @@ describe('~/pipeline_editor/components/text_editor.vue', () => { ...@@ -36,14 +37,17 @@ describe('~/pipeline_editor/components/text_editor.vue', () => {
provide: { provide: {
projectPath: mockProjectPath, projectPath: mockProjectPath,
projectNamespace: mockProjectNamespace, projectNamespace: mockProjectNamespace,
},
propsData: {
ciConfigPath: mockCiConfigPath, ciConfigPath: mockCiConfigPath,
commitSha: mockCommitSha,
}, },
attrs: { attrs: {
value: mockCiYml, value: mockCiYml,
}, },
// Simulate graphQL client query result
data() {
return {
commitSha: mockCommitSha,
};
},
listeners: { listeners: {
[EDITOR_READY_EVENT]: editorReadyListener, [EDITOR_READY_EVENT]: editorReadyListener,
}, },
...@@ -54,41 +58,64 @@ describe('~/pipeline_editor/components/text_editor.vue', () => { ...@@ -54,41 +58,64 @@ describe('~/pipeline_editor/components/text_editor.vue', () => {
}); });
}; };
const findEditor = () => wrapper.find(MockEditorLite); const findEditor = () => wrapper.findComponent(MockEditorLite);
beforeEach(() => { afterEach(() => {
editorReadyListener = jest.fn(); wrapper.destroy();
mockUse = jest.fn(); wrapper = null;
mockRegisterCiSchema = jest.fn();
createComponent(); mockUse.mockClear();
mockRegisterCiSchema.mockClear();
}); });
it('contains an editor', () => { describe('template', () => {
expect(findEditor().exists()).toBe(true); beforeEach(() => {
}); editorReadyListener = jest.fn();
mockUse = jest.fn();
mockRegisterCiSchema = jest.fn();
it('editor contains the value provided', () => { createComponent();
expect(findEditor().props('value')).toBe(mockCiYml); });
});
it('editor is configured for the CI config path', () => { it('contains an editor', () => {
expect(findEditor().props('fileName')).toBe(mockCiConfigPath); expect(findEditor().exists()).toBe(true);
}); });
it('editor is configured with syntax highligting', async () => { it('editor contains the value provided', () => {
expect(mockUse).toHaveBeenCalledTimes(1); expect(findEditor().props('value')).toBe(mockCiYml);
expect(mockRegisterCiSchema).toHaveBeenCalledTimes(1); });
expect(mockRegisterCiSchema).toHaveBeenCalledWith({
projectNamespace: mockProjectNamespace, it('editor is configured for the CI config path', () => {
projectPath: mockProjectPath, expect(findEditor().props('fileName')).toBe(mockCiConfigPath);
ref: mockCommitSha, });
it('bubbles up events', () => {
findEditor().vm.$emit(EDITOR_READY_EVENT);
expect(editorReadyListener).toHaveBeenCalled();
}); });
}); });
it('bubbles up events', () => { describe('register CI schema', () => {
findEditor().vm.$emit(EDITOR_READY_EVENT); 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,
});
});
}); });
}); });
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);
});
});
});
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