Commit 9903c502 authored by Andrew Fontaine's avatar Andrew Fontaine

Merge branch '292930-breakdown-pipeline_editor_app-into-smaller-components' into 'master'

Merge feature branch for Pipeline Editor app refactor

See merge request gitlab-org/gitlab!52200
parents bc27d459 80a8670e
<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