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 @@
import EditorLite from '~/vue_shared/components/editor_lite.vue';
import { CiSchemaExtension } from '~/editor/extensions/editor_ci_schema_ext';
import { EDITOR_READY_EVENT } from '~/editor/constants';
import getCommitSha from '../graphql/queries/client/commit_sha.graphql';
export default {
components: {
EditorLite,
},
inject: ['projectPath', 'projectNamespace'],
inject: ['ciConfigPath', 'projectPath', 'projectNamespace'],
inheritAttrs: false,
props: {
ciConfigPath: {
type: String,
required: true,
},
data() {
return {
commitSha: '',
};
},
apollo: {
commitSha: {
type: String,
required: false,
default: null,
query: getCommitSha,
},
},
methods: {
onEditorReady() {
onCiConfigUpdate(content) {
this.$emit('updateCiConfig', content);
},
registerCiSchema() {
const editorInstance = this.$refs.editor.getEditor();
editorInstance.use(new CiSchemaExtension());
......@@ -41,7 +44,8 @@ export default {
ref="editor"
:file-name="ciConfigPath"
v-bind="$attrs"
@[$options.readyEvent]="onEditorReady"
@[$options.readyEvent]="registerCiSchema"
@input="onCiConfigUpdate"
v-on="$listeners"
/>
</div>
......
export const CI_CONFIG_STATUS_VALID = 'VALID';
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!
$branch: String!
$startBranch: String
......
......@@ -15,14 +15,13 @@ export const initPipelineEditor = (selector = '#js-pipeline-editor') => {
}
const {
// props
ciConfigPath,
// Add to apollo cache as it can be updated by future queries
commitSha,
// Add to provide/inject API for static values
ciConfigPath,
defaultBranch,
newMergeRequestPath,
// `provide/inject` data
lintHelpPagePath,
newMergeRequestPath,
projectFullPath,
projectPath,
projectNamespace,
......@@ -35,25 +34,27 @@ export const initPipelineEditor = (selector = '#js-pipeline-editor') => {
defaultClient: createDefaultClient(resolvers, { typeDefs }),
});
apolloProvider.clients.defaultClient.cache.writeData({
data: {
commitSha,
},
});
return new Vue({
el,
apolloProvider,
provide: {
ciConfigPath,
defaultBranch,
lintHelpPagePath,
newMergeRequestPath,
projectFullPath,
projectPath,
projectNamespace,
ymlHelpPagePath,
},
render(h) {
return h(PipelineEditorApp, {
props: {
ciConfigPath,
commitSha,
defaultBranch,
newMergeRequestPath,
},
});
return h(PipelineEditorApp);
},
});
};
<script>
import { GlAlert, GlLoadingIcon, GlTabs, GlTab } from '@gitlab/ui';
import { GlAlert, GlLoadingIcon } from '@gitlab/ui';
import { __, s__, sprintf } from '~/locale';
import { mergeUrlParams, redirectTo } from '~/lib/utils/url_utility';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import httpStatusCodes from '~/lib/utils/http_status';
import PipelineGraph from '~/pipelines/components/pipeline_graph/pipeline_graph.vue';
import { unwrapStagesWithNeeds } from '~/pipelines/components/unwrapping_utils';
import CiLint from './components/lint/ci_lint.vue';
import CommitForm from './components/commit/commit_form.vue';
import ConfirmUnsavedChangesDialog from './components/ui/confirm_unsaved_changes_dialog.vue';
import EditorTab from './components/ui/editor_tab.vue';
import TextEditor from './components/text_editor.vue';
import ValidationSegment from './components/info/validation_segment.vue';
import commitCiFileMutation from './graphql/mutations/commit_ci_file.mutation.graphql';
import {
COMMIT_FAILURE,
COMMIT_SUCCESS,
DEFAULT_FAILURE,
LOAD_FAILURE_NO_FILE,
LOAD_FAILURE_UNKNOWN,
} from './constants';
import getBlobContent from './graphql/queries/blob_content.graphql';
import getCiConfigData from './graphql/queries/ci_config.graphql';
const MR_SOURCE_BRANCH = 'merge_request[source_branch]';
const MR_TARGET_BRANCH = 'merge_request[target_branch]';
const COMMIT_FAILURE = 'COMMIT_FAILURE';
const COMMIT_SUCCESS = 'COMMIT_SUCCESS';
const DEFAULT_FAILURE = 'DEFAULT_FAILURE';
const LOAD_FAILURE_NO_FILE = 'LOAD_FAILURE_NO_FILE';
const LOAD_FAILURE_UNKNOWN = 'LOAD_FAILURE_UNKNOWN';
import PipelineEditorHome from './pipeline_editor_home.vue';
export default {
components: {
CiLint,
CommitForm,
ConfirmUnsavedChangesDialog,
EditorTab,
GlAlert,
GlLoadingIcon,
GlTabs,
GlTab,
PipelineGraph,
TextEditor,
ValidationSegment,
PipelineEditorHome,
},
mixins: [glFeatureFlagsMixin()],
inject: ['projectFullPath'],
props: {
defaultBranch: {
type: String,
required: false,
default: null,
inject: {
ciConfigPath: {
default: '',
},
commitSha: {
type: String,
required: false,
defaultBranch: {
default: null,
},
ciConfigPath: {
type: String,
required: true,
},
newMergeRequestPath: {
type: String,
required: true,
projectFullPath: {
default: '',
},
},
data() {
return {
ciConfigData: {},
content: '',
contentModel: '',
lastCommittedContent: '',
lastCommitSha: this.commitSha,
isSaving: false,
// Success and failure state
failureType: null,
showFailureAlert: false,
failureReasons: [],
successType: null,
initialCiFileContent: '',
lastCommittedContent: '',
currentCiFileContent: '',
showFailureAlert: false,
showSuccessAlert: false,
successType: null,
};
},
apollo: {
content: {
initialCiFileContent: {
query: getBlobContent,
variables() {
return {
......@@ -91,12 +59,13 @@ export default {
};
},
update(data) {
const content = data?.blobContent?.rawData;
this.lastCommittedContent = content;
return content;
return data?.blobContent?.rawData;
},
result({ data }) {
this.contentModel = data?.blobContent?.rawData ?? '';
const fileContent = data?.blobContent?.rawData ?? '';
this.lastCommittedContent = fileContent;
this.currentCiFileContent = fileContent;
},
error(error) {
this.handleBlobContentError(error);
......@@ -105,13 +74,13 @@ export default {
ciConfigData: {
query: getCiConfigData,
// If content is not loaded, we can't lint the data
skip: ({ contentModel }) => {
return !contentModel;
skip: ({ currentCiFileContent }) => {
return !currentCiFileContent;
},
variables() {
return {
projectPath: this.projectFullPath,
content: this.contentModel,
content: this.currentCiFileContent,
};
},
update(data) {
......@@ -128,10 +97,10 @@ export default {
},
computed: {
hasUnsavedChanges() {
return this.lastCommittedContent !== this.contentModel;
return this.lastCommittedContent !== this.currentCiFileContent;
},
isBlobContentLoading() {
return this.$apollo.queries.content.loading;
return this.$apollo.queries.initialCiFileContent.loading;
},
isBlobContentError() {
return this.failureType === LOAD_FAILURE_NO_FILE;
......@@ -139,62 +108,60 @@ export default {
isCiConfigDataLoading() {
return this.$apollo.queries.ciConfigData.loading;
},
defaultCommitMessage() {
return sprintf(this.$options.i18n.defaultCommitMessage, { sourcePath: this.ciConfigPath });
},
success() {
switch (this.successType) {
case COMMIT_SUCCESS:
return {
text: this.$options.alertTexts[COMMIT_SUCCESS],
variant: 'info',
};
default:
return null;
}
},
failure() {
switch (this.failureType) {
case LOAD_FAILURE_NO_FILE:
return {
text: sprintf(this.$options.alertTexts[LOAD_FAILURE_NO_FILE], {
text: sprintf(this.$options.errorTexts[LOAD_FAILURE_NO_FILE], {
filePath: this.ciConfigPath,
}),
variant: 'danger',
};
case LOAD_FAILURE_UNKNOWN:
return {
text: this.$options.alertTexts[LOAD_FAILURE_UNKNOWN],
text: this.$options.errorTexts[LOAD_FAILURE_UNKNOWN],
variant: 'danger',
};
case COMMIT_FAILURE:
return {
text: this.$options.alertTexts[COMMIT_FAILURE],
text: this.$options.errorTexts[COMMIT_FAILURE],
variant: 'danger',
};
default:
return {
text: this.$options.alertTexts[DEFAULT_FAILURE],
text: this.$options.errorTexts[DEFAULT_FAILURE],
variant: 'danger',
};
}
},
success() {
switch (this.successType) {
case COMMIT_SUCCESS:
return {
text: this.$options.successTexts[COMMIT_SUCCESS],
variant: 'info',
};
default:
return null;
}
},
},
i18n: {
defaultCommitMessage: __('Update %{sourcePath} file'),
tabEdit: s__('Pipelines|Write pipeline configuration'),
tabGraph: s__('Pipelines|Visualize'),
tabLint: s__('Pipelines|Lint'),
},
alertTexts: {
errorTexts: {
[COMMIT_FAILURE]: s__('Pipelines|The GitLab CI configuration could not be updated.'),
[COMMIT_SUCCESS]: __('Your changes have been successfully committed.'),
[DEFAULT_FAILURE]: __('Something went wrong on our end.'),
[LOAD_FAILURE_NO_FILE]: s__(
'Pipelines|There is no %{filePath} file in this repository, please add one and visit the Pipeline Editor again.',
),
[LOAD_FAILURE_UNKNOWN]: s__('Pipelines|The CI configuration was not loaded, please try again.'),
},
successTexts: {
[COMMIT_SUCCESS]: __('Your changes have been successfully committed.'),
},
methods: {
handleBlobContentError(error = {}) {
const { networkError } = error;
......@@ -215,73 +182,32 @@ export default {
dismissFailure() {
this.showFailureAlert = false;
},
dismissSuccess() {
this.showSuccessAlert = false;
},
reportFailure(type, reasons = []) {
this.showFailureAlert = true;
this.failureType = type;
this.failureReasons = reasons;
},
dismissSuccess() {
this.showSuccessAlert = false;
},
reportSuccess(type) {
this.showSuccessAlert = true;
this.successType = type;
},
redirectToNewMergeRequest(sourceBranch) {
const url = mergeUrlParams(
{
[MR_SOURCE_BRANCH]: sourceBranch,
[MR_TARGET_BRANCH]: this.defaultBranch,
},
this.newMergeRequestPath,
);
redirectTo(url);
resetContent() {
this.currentCiFileContent = this.lastCommittedContent;
},
async onCommitSubmit(event) {
this.isSaving = true;
const { message, branch, openMergeRequest } = event;
try {
const {
data: {
commitCreate: { errors, commit },
},
} = await this.$apollo.mutate({
mutation: commitCiFileMutation,
variables: {
projectPath: this.projectFullPath,
branch,
startBranch: this.defaultBranch,
message,
filePath: this.ciConfigPath,
content: this.contentModel,
lastCommitId: this.lastCommitSha,
},
});
if (errors?.length) {
this.reportFailure(COMMIT_FAILURE, errors);
return;
}
if (openMergeRequest) {
this.redirectToNewMergeRequest(branch);
} else {
this.reportSuccess(COMMIT_SUCCESS);
// Update latest commit
this.lastCommitSha = commit.sha;
this.lastCommittedContent = this.contentModel;
}
} catch (error) {
this.reportFailure(COMMIT_FAILURE, [error?.message]);
} finally {
this.isSaving = false;
}
showErrorAlert({ type, reasons = [] }) {
this.reportFailure(type, reasons);
},
onCommitCancel() {
this.contentModel = this.content;
updateCiConfig(ciFileContent) {
this.currentCiFileContent = ciFileContent;
},
updateOnCommit({ type }) {
this.reportSuccess(type);
// Keep track of the latest commited content to know
// if the user has made changes to the file that are unsaved.
this.lastCommittedContent = this.currentCiFileContent;
},
},
};
......@@ -289,20 +215,10 @@ export default {
<template>
<div class="gl-mt-4">
<gl-alert
v-if="showSuccessAlert"
:variant="success.variant"
:dismissible="true"
@dismiss="dismissSuccess"
>
<gl-alert v-if="showSuccessAlert" :variant="success.variant" @dismiss="dismissSuccess">
{{ success.text }}
</gl-alert>
<gl-alert
v-if="showFailureAlert"
:variant="failure.variant"
:dismissible="true"
@dismiss="dismissFailure"
>
<gl-alert v-if="showFailureAlert" :variant="failure.variant" @dismiss="dismissFailure">
{{ failure.text }}
<ul v-if="failureReasons.length" class="gl-mb-0">
<li v-for="reason in failureReasons" :key="reason">{{ reason }}</li>
......@@ -310,45 +226,14 @@ export default {
</gl-alert>
<gl-loading-icon v-if="isBlobContentLoading" size="lg" class="gl-m-3" />
<div v-else-if="!isBlobContentError" class="gl-mt-4">
<div class="file-editor gl-mb-3">
<div class="info-well gl-display-none gl-sm-display-block">
<validation-segment
class="well-segment"
:loading="isCiConfigDataLoading"
:ci-config="ciConfigData"
/>
</div>
<gl-tabs>
<editor-tab :lazy="true" :title="$options.i18n.tabEdit">
<text-editor
v-model="contentModel"
:ci-config-path="ciConfigPath"
:commit-sha="lastCommitSha"
/>
</editor-tab>
<gl-tab
v-if="glFeatures.ciConfigVisualizationTab"
:lazy="true"
:title="$options.i18n.tabGraph"
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">
<gl-loading-icon v-if="isCiConfigDataLoading" size="lg" class="gl-m-3" />
<ci-lint v-else :ci-config="ciConfigData" />
</editor-tab>
</gl-tabs>
</div>
<commit-form
:default-branch="defaultBranch"
:default-message="defaultCommitMessage"
:is-saving="isSaving"
@cancel="onCommitCancel"
@submit="onCommitSubmit"
<pipeline-editor-home
:is-ci-config-data-loading="isCiConfigDataLoading"
:ci-config-data="ciConfigData"
:ci-file-content="currentCiFileContent"
@commit="updateOnCommit"
@resetContent="resetContent"
@showError="showErrorAlert"
@updateCiConfig="updateCiConfig"
/>
</div>
<confirm-unsaved-changes-dialog :has-unsaved-changes="hasUnsavedChanges" />
......
<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';
import { mockCommitMessage, mockDefaultBranch } from '../../mock_data';
describe('~/pipeline_editor/pipeline_editor_app.vue', () => {
describe('Pipeline Editor | Commit Form', () => {
let wrapper;
const createComponent = ({ props = {} } = {}, mountFn = shallowMount) => {
......@@ -21,8 +21,8 @@ describe('~/pipeline_editor/pipeline_editor_app.vue', () => {
});
};
const findCommitTextarea = () => wrapper.find(GlFormTextarea);
const findBranchInput = () => wrapper.find(GlFormInput);
const findCommitTextarea = () => wrapper.findComponent(GlFormTextarea);
const findBranchInput = () => wrapper.findComponent(GlFormInput);
const findNewMrCheckbox = () => wrapper.find('[data-testid="new-mr-checkbox"]');
const findSubmitBtn = () => wrapper.find('[type="submit"]');
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';
import { GlIcon } from '@gitlab/ui';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
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 { mockYmlHelpPagePath, mergeUnwrappedCiConfig } from '../../mock_data';
......@@ -29,6 +31,11 @@ describe('~/pipeline_editor/components/info/validation_segment.vue', () => {
const findLearnMoreLink = () => wrapper.findByTestId('learnMoreLink');
const findValidationMsg = () => wrapper.findByTestId('validationMsg');
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
it('shows the loading state', () => {
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 { EDITOR_READY_EVENT } from '~/editor/constants';
import TextEditor from '~/pipeline_editor/components/text_editor.vue';
import {
mockCiConfigPath,
mockCiYml,
......@@ -10,7 +8,10 @@ import {
mockProjectNamespace,
} 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 editorReadyListener;
......@@ -36,14 +37,17 @@ describe('~/pipeline_editor/components/text_editor.vue', () => {
provide: {
projectPath: mockProjectPath,
projectNamespace: mockProjectNamespace,
},
propsData: {
ciConfigPath: mockCiConfigPath,
commitSha: mockCommitSha,
},
attrs: {
value: mockCiYml,
},
// Simulate graphQL client query result
data() {
return {
commitSha: mockCommitSha,
};
},
listeners: {
[EDITOR_READY_EVENT]: editorReadyListener,
},
......@@ -54,41 +58,64 @@ describe('~/pipeline_editor/components/text_editor.vue', () => {
});
};
const findEditor = () => wrapper.find(MockEditorLite);
const findEditor = () => wrapper.findComponent(MockEditorLite);
beforeEach(() => {
editorReadyListener = jest.fn();
mockUse = jest.fn();
mockRegisterCiSchema = jest.fn();
afterEach(() => {
wrapper.destroy();
wrapper = null;
createComponent();
mockUse.mockClear();
mockRegisterCiSchema.mockClear();
});
it('contains an editor', () => {
expect(findEditor().exists()).toBe(true);
});
describe('template', () => {
beforeEach(() => {
editorReadyListener = jest.fn();
mockUse = jest.fn();
mockRegisterCiSchema = jest.fn();
it('editor contains the value provided', () => {
expect(findEditor().props('value')).toBe(mockCiYml);
});
createComponent();
});
it('editor is configured for the CI config path', () => {
expect(findEditor().props('fileName')).toBe(mockCiConfigPath);
});
it('contains an editor', () => {
expect(findEditor().exists()).toBe(true);
});
it('editor is configured with syntax highligting', async () => {
expect(mockUse).toHaveBeenCalledTimes(1);
expect(mockRegisterCiSchema).toHaveBeenCalledTimes(1);
expect(mockRegisterCiSchema).toHaveBeenCalledWith({
projectNamespace: mockProjectNamespace,
projectPath: mockProjectPath,
ref: mockCommitSha,
it('editor contains the value provided', () => {
expect(findEditor().props('value')).toBe(mockCiYml);
});
it('editor is configured for the CI config path', () => {
expect(findEditor().props('fileName')).toBe(mockCiConfigPath);
});
it('bubbles up events', () => {
findEditor().vm.$emit(EDITOR_READY_EVENT);
expect(editorReadyListener).toHaveBeenCalled();
});
});
it('bubbles up events', () => {
findEditor().vm.$emit(EDITOR_READY_EVENT);
describe('register CI schema', () => {
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 { nextTick } from 'vue';
import { mount, shallowMount, createLocalVue } from '@vue/test-utils';
import { GlAlert, GlButton, GlFormInput, GlFormTextarea, GlLoadingIcon, GlTabs } from '@gitlab/ui';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import { GlAlert, GlButton, GlLoadingIcon, GlTabs } from '@gitlab/ui';
import VueApollo from 'vue-apollo';
import waitForPromises from 'helpers/wait_for_promises';
import createMockApollo from 'helpers/mock_apollo_helper';
import TextEditor from '~/pipeline_editor/components/text_editor.vue';
import httpStatusCodes from '~/lib/utils/http_status';
import { objectToQuery, redirectTo, refreshCurrentPage } from '~/lib/utils/url_utility';
import CommitForm from '~/pipeline_editor/components/commit/commit_form.vue';
import getCiConfigData from '~/pipeline_editor/graphql/queries/ci_config.graphql';
import EditorTab from '~/pipeline_editor/components/ui/editor_tab.vue';
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';
import {
mockCiConfigPath,
mockCiConfigQueryResponse,
mockCiYml,
mockCommitSha,
mockCommitNextSha,
mockCommitMessage,
mockDefaultBranch,
mockProjectPath,
mockProjectFullPath,
mockProjectNamespace,
mockNewMergeRequestPath,
} from './mock_data';
import { COMMIT_SUCCESS, COMMIT_FAILURE, LOAD_FAILURE_UNKNOWN } from '~/pipeline_editor/constants';
import CommitForm from '~/pipeline_editor/components/commit/commit_form.vue';
import getCiConfigData from '~/pipeline_editor/graphql/queries/ci_config.graphql';
import PipelineEditorApp from '~/pipeline_editor/pipeline_editor_app.vue';
import PipelineEditorHome from '~/pipeline_editor/pipeline_editor_home.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,
}));
const MockEditorLite = {
template: '<div/>',
};
const mockProvide = {
ciConfigPath: mockCiConfigPath,
defaultBranch: mockDefaultBranch,
projectFullPath: mockProjectFullPath,
projectPath: mockProjectPath,
projectNamespace: mockProjectNamespace,
glFeatures: {
ciConfigVisualizationTab: true,
},
};
describe('~/pipeline_editor/pipeline_editor_app.vue', () => {
describe('Pipeline editor app component', () => {
let wrapper;
let mockApollo;
let mockBlobContentData;
let mockCiConfigData;
let mockMutate;
const createComponent = ({
props = {},
blobLoading = false,
lintLoading = false,
options = {},
mountFn = shallowMount,
provide = mockProvide,
} = {}) => {
mockMutate = jest.fn().mockResolvedValue({
data: {
commitCreate: {
errors: [],
commit: {
sha: mockCommitNextSha,
},
},
},
});
wrapper = mountFn(PipelineEditorApp, {
propsData: {
ciConfigPath: mockCiConfigPath,
commitSha: mockCommitSha,
defaultBranch: mockDefaultBranch,
newMergeRequestPath: mockNewMergeRequestPath,
...props,
},
provide,
const createComponent = ({ blobLoading = false, options = {} } = {}) => {
wrapper = shallowMount(PipelineEditorApp, {
provide: mockProvide,
stubs: {
GlTabs,
GlButton,
CommitForm,
EditorLite: MockEditorLite,
TextEditor,
},
mocks: {
$apollo: {
queries: {
content: {
initialCiFileContent: {
loading: blobLoading,
},
ciConfigData: {
loading: lintLoading,
loading: false,
},
},
mutate: mockMutate,
},
},
// attachTo is required for input/submit events
attachTo: mountFn === mount ? document.body : null,
...options,
});
};
const createComponentWithApollo = ({ props = {}, mountFn = shallowMount } = {}) => {
const createComponentWithApollo = ({ props = {} } = {}) => {
const handlers = [[getCiConfigData, mockCiConfigData]];
const resolvers = {
Query: {
......@@ -134,18 +86,13 @@ describe('~/pipeline_editor/pipeline_editor_app.vue', () => {
apolloProvider: mockApollo,
};
createComponent({ props, options }, mountFn);
createComponent({ props, options });
};
const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
const findAlert = () => wrapper.find(GlAlert);
const findTabAt = (i) => wrapper.findAll(EditorTab).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);
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findAlert = () => wrapper.findComponent(GlAlert);
const findEditorHome = () => wrapper.findComponent(PipelineEditorHome);
const findTextEditor = () => wrapper.findComponent(TextEditor);
beforeEach(() => {
mockBlobContentData = jest.fn();
......@@ -155,9 +102,6 @@ describe('~/pipeline_editor/pipeline_editor_app.vue', () => {
afterEach(() => {
mockBlobContentData.mockReset();
mockCiConfigData.mockReset();
refreshCurrentPage.mockReset();
redirectTo.mockReset();
mockMutate.mockReset();
wrapper.destroy();
wrapper = null;
......@@ -170,245 +114,6 @@ describe('~/pipeline_editor/pipeline_editor_app.vue', () => {
expect(findTextEditor().exists()).toBe(false);
});
describe('tabs', () => {
describe('editor tab', () => {
it('displays editor only after the tab is mounted', async () => {
createComponent({ mountFn: mount });
expect(findTabAt(0).find(TextEditor).exists()).toBe(false);
await nextTick();
expect(findTabAt(0).find(TextEditor).exists()).toBe(true);
});
});
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(findLoadingIcon().exists()).toBe(true);
expect(findPipelineGraph().exists()).toBe(false);
});
});
describe('with feature flag off', () => {
beforeEach(() => {
createComponent({
provide: {
...mockProvide,
glFeatures: { ciConfigVisualizationTab: false },
},
});
});
it('does not display the tab', () => {
expect(findVisualizationTab().exists()).toBe(false);
});
});
});
});
describe('when data is set', () => {
beforeEach(async () => {
createComponent({ mountFn: mount });
wrapper.setData({
content: mockCiYml,
contentModel: mockCiYml,
});
await waitForPromises();
});
it('displays content after the query loads', () => {
expect(findLoadingIcon().exists()).toBe(false);
expect(findEditorLite().attributes('value')).toBe(mockCiYml);
expect(findEditorLite().attributes('file-name')).toBe(mockCiConfigPath);
});
it('configures text editor', () => {
expect(findTextEditor().props('commitSha')).toBe(mockCommitSha);
});
describe('commit form', () => {
const mockVariables = {
content: mockCiYml,
filePath: mockCiConfigPath,
lastCommitId: mockCommitSha,
message: mockCommitMessage,
projectPath: mockProjectFullPath,
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('displays an alert to indicate success', () => {
expect(findAlert().text()).toMatchInterpolatedText(
'Your changes have been successfully committed.',
);
});
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).toHaveBeenLastCalledWith({
mutation: expect.any(Object),
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: expect.any(Object),
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 () => {
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 an error message', async () => {
mockMutate.mockRejectedValueOnce(new Error('commit failed'));
await submitCommit();
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(findEditorLite().attributes('value')).toBe(mockCiYml);
});
});
});
});
describe('when queries are called', () => {
beforeEach(() => {
mockBlobContentData.mockResolvedValue(mockCiYml);
......@@ -422,14 +127,12 @@ describe('~/pipeline_editor/pipeline_editor_app.vue', () => {
await waitForPromises();
});
it('shows editor and commit form', () => {
expect(findEditorLite().exists()).toBe(true);
expect(findTextEditor().exists()).toBe(true);
it('shows pipeline editor home component', () => {
expect(findEditorHome().exists()).toBe(true);
});
it('no error is shown when data is set', async () => {
it('no error is shown when data is set', () => {
expect(findAlert().exists()).toBe(false);
expect(findEditorLite().attributes('value')).toBe(mockCiYml);
});
it('ci config query is called with correct variables', async () => {
......@@ -445,10 +148,10 @@ describe('~/pipeline_editor/pipeline_editor_app.vue', () => {
});
describe('when no file exists', () => {
const expectedAlertMsg =
const noFileAlertMsg =
'There is no .gitlab-ci.yml file in this repository, please add one and visit the Pipeline Editor again.';
it('shows a 404 error message and does not show editor or commit form', async () => {
it('shows a 404 error message and does not show editor home component', async () => {
mockBlobContentData.mockRejectedValueOnce({
response: {
status: httpStatusCodes.NOT_FOUND,
......@@ -458,12 +161,11 @@ describe('~/pipeline_editor/pipeline_editor_app.vue', () => {
await waitForPromises();
expect(findAlert().text()).toBe(expectedAlertMsg);
expect(findEditorLite().exists()).toBe(false);
expect(findTextEditor().exists()).toBe(false);
expect(findAlert().text()).toBe(noFileAlertMsg);
expect(findEditorHome().exists()).toBe(false);
});
it('shows a 400 error message and does not show editor or commit form', async () => {
it('shows a 400 error message and does not show editor home component', async () => {
mockBlobContentData.mockRejectedValueOnce({
response: {
status: httpStatusCodes.BAD_REQUEST,
......@@ -473,9 +175,8 @@ describe('~/pipeline_editor/pipeline_editor_app.vue', () => {
await waitForPromises();
expect(findAlert().text()).toBe(expectedAlertMsg);
expect(findEditorLite().exists()).toBe(false);
expect(findTextEditor().exists()).toBe(false);
expect(findAlert().text()).toBe(noFileAlertMsg);
expect(findEditorHome().exists()).toBe(false);
});
it('shows a unkown error message', async () => {
......@@ -483,9 +184,60 @@ describe('~/pipeline_editor/pipeline_editor_app.vue', () => {
createComponentWithApollo();
await waitForPromises();
expect(findAlert().text()).toBe('The CI configuration was not loaded, please try again.');
expect(findEditorLite().exists()).toBe(true);
expect(findTextEditor().exists()).toBe(true);
expect(findAlert().text()).toBe(wrapper.vm.$options.errorTexts[LOAD_FAILURE_UNKNOWN]);
expect(findEditorHome().exists()).toBe(true);
});
});
describe('when the user commits', () => {
const updateFailureMessage = 'The GitLab CI configuration could not be updated.';
describe('and the commit mutation succeeds', () => {
beforeEach(() => {
createComponent();
findEditorHome().vm.$emit('commit', { type: COMMIT_SUCCESS });
});
it('shows a confirmation message', () => {
expect(findAlert().text()).toBe(wrapper.vm.$options.successTexts[COMMIT_SUCCESS]);
});
});
describe('and the commit mutation fails', () => {
const commitFailedReasons = ['Commit failed'];
beforeEach(() => {
createComponent();
findEditorHome().vm.$emit('showError', {
type: COMMIT_FAILURE,
reasons: commitFailedReasons,
});
});
it('shows an error message', () => {
expect(findAlert().text()).toMatchInterpolatedText(
`${updateFailureMessage} ${commitFailedReasons[0]}`,
);
});
});
describe('when an unknown error occurs', () => {
const unknownReasons = ['Commit failed'];
beforeEach(() => {
createComponent();
findEditorHome().vm.$emit('showError', {
type: COMMIT_FAILURE,
reasons: unknownReasons,
});
});
it('shows an error message', () => {
expect(findAlert().text()).toMatchInterpolatedText(
`${updateFailureMessage} ${unknownReasons[0]}`,
);
});
});
});
});
......
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