Commit 27a3c00c authored by Sarah Groff Hennigh-Palermo's avatar Sarah Groff Hennigh-Palermo

Merge branch 'pipeline-editor-empty-state-2' into 'master'

Adds ability to work on new Ci config file to pipeline editor [RUN ALL RSPEC] [RUN AS-IF-FOSS]

See merge request gitlab-org/gitlab!55414
parents b54ae4eb 3448bb5d
...@@ -31,6 +31,10 @@ export default { ...@@ -31,6 +31,10 @@ export default {
}, },
mixins: [glFeatureFlagsMixin()], mixins: [glFeatureFlagsMixin()],
props: { props: {
ciFileContent: {
type: String,
required: true,
},
ciConfigData: { ciConfigData: {
type: Object, type: Object,
required: true, required: true,
...@@ -60,6 +64,7 @@ export default { ...@@ -60,6 +64,7 @@ export default {
<validation-segment <validation-segment
:class="validationStyling" :class="validationStyling"
:loading="isCiConfigDataLoading" :loading="isCiConfigDataLoading"
:ci-file-content="ciFileContent"
:ci-config="ciConfigData" :ci-config="ciConfigData"
/> />
</div> </div>
......
...@@ -5,6 +5,9 @@ import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue'; ...@@ -5,6 +5,9 @@ import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
import { CI_CONFIG_STATUS_VALID } from '../../constants'; import { CI_CONFIG_STATUS_VALID } from '../../constants';
export const i18n = { export const i18n = {
empty: __(
"We'll continuously validate your pipeline configuration. The validation results will appear here.",
),
learnMore: __('Learn more'), learnMore: __('Learn more'),
loading: s__('Pipelines|Validating GitLab CI configuration…'), loading: s__('Pipelines|Validating GitLab CI configuration…'),
invalid: s__('Pipelines|This GitLab CI configuration is invalid.'), invalid: s__('Pipelines|This GitLab CI configuration is invalid.'),
...@@ -26,6 +29,10 @@ export default { ...@@ -26,6 +29,10 @@ export default {
}, },
}, },
props: { props: {
ciFileContent: {
type: String,
required: true,
},
ciConfig: { ciConfig: {
type: Object, type: Object,
required: false, required: false,
...@@ -38,17 +45,22 @@ export default { ...@@ -38,17 +45,22 @@ export default {
}, },
}, },
computed: { computed: {
isEmpty() {
return !this.ciFileContent;
},
isValid() { isValid() {
return this.ciConfig?.status === CI_CONFIG_STATUS_VALID; return this.ciConfig?.status === CI_CONFIG_STATUS_VALID;
}, },
icon() { icon() {
if (this.isValid) { if (this.isValid || this.isEmpty) {
return 'check'; return 'check';
} }
return 'warning-solid'; return 'warning-solid';
}, },
message() { message() {
if (this.isValid) { if (this.isEmpty) {
return this.$options.i18n.empty;
} else if (this.isValid) {
return this.$options.i18n.valid; return this.$options.i18n.valid;
} }
...@@ -74,7 +86,7 @@ export default { ...@@ -74,7 +86,7 @@ export default {
<tooltip-on-truncate :title="message" class="gl-text-truncate"> <tooltip-on-truncate :title="message" class="gl-text-truncate">
<gl-icon :name="icon" /> <span data-testid="validationMsg">{{ message }}</span> <gl-icon :name="icon" /> <span data-testid="validationMsg">{{ message }}</span>
</tooltip-on-truncate> </tooltip-on-truncate>
<span class="gl-flex-shrink-0 gl-pl-2"> <span v-if="!isEmpty" class="gl-flex-shrink-0 gl-pl-2">
<gl-link data-testid="learnMoreLink" :href="ymlHelpPagePath"> <gl-link data-testid="learnMoreLink" :href="ymlHelpPagePath">
{{ $options.i18n.learnMore }} {{ $options.i18n.learnMore }}
</gl-link> </gl-link>
......
<script> <script>
import { GlSprintf } from '@gitlab/ui'; import { GlButton, GlSprintf } from '@gitlab/ui';
import { __ } from '~/locale'; import { __ } from '~/locale';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default { export default {
components: { components: {
GlButton,
GlSprintf, GlSprintf,
}, },
i18n: { i18n: {
...@@ -11,24 +13,44 @@ export default { ...@@ -11,24 +13,44 @@ export default {
body: __( body: __(
'Create a new %{codeStart}.gitlab-ci.yml%{codeEnd} file at the root of the repository to get started.', 'Create a new %{codeStart}.gitlab-ci.yml%{codeEnd} file at the root of the repository to get started.',
), ),
btnText: __('Create new CI/CD pipeline'),
}, },
mixins: [glFeatureFlagsMixin()],
inject: { inject: {
emptyStateIllustrationPath: { emptyStateIllustrationPath: {
default: '', default: '',
}, },
}, },
computed: {
showCTAButton() {
return this.glFeatures.pipelineEditorEmptyStateAction;
},
},
methods: {
createEmptyConfigFile() {
this.$emit('createEmptyConfigFile');
},
},
}; };
</script> </script>
<template> <template>
<div class="gl-display-flex gl-flex-direction-column gl-align-items-center gl-mt-11"> <div class="gl-display-flex gl-flex-direction-column gl-align-items-center gl-mt-11">
<img :src="emptyStateIllustrationPath" /> <img :src="emptyStateIllustrationPath" />
<h1 class="gl-font-size-h1">{{ $options.i18n.title }}</h1> <h1 class="gl-font-size-h1">{{ $options.i18n.title }}</h1>
<p> <p class="gl-mt-3">
<gl-sprintf :message="$options.i18n.body"> <gl-sprintf :message="$options.i18n.body">
<template #code="{ content }"> <template #code="{ content }">
<code>{{ content }}</code> <code>{{ content }}</code>
</template> </template>
</gl-sprintf> </gl-sprintf>
</p> </p>
<gl-button
v-if="showCTAButton"
variant="confirm"
class="gl-mt-3"
@click="createEmptyConfigFile"
>
{{ $options.i18n.btnText }}
</gl-button>
</div> </div>
</template> </template>
...@@ -36,7 +36,8 @@ export default { ...@@ -36,7 +36,8 @@ export default {
// Success and failure state // Success and failure state
failureType: null, failureType: null,
failureReasons: [], failureReasons: [],
hasNoCiConfigFile: false, showStartScreen: false,
isNewConfigFile: false,
initialCiFileContent: '', initialCiFileContent: '',
lastCommittedContent: '', lastCommittedContent: '',
currentCiFileContent: '', currentCiFileContent: '',
...@@ -48,6 +49,11 @@ export default { ...@@ -48,6 +49,11 @@ export default {
apollo: { apollo: {
initialCiFileContent: { initialCiFileContent: {
query: getBlobContent, query: getBlobContent,
// If we are working off a new file, we don't want to fetch
// the base data as there is nothing to fetch.
skip({ isNewConfigFile }) {
return isNewConfigFile;
},
variables() { variables() {
return { return {
projectPath: this.projectFullPath, projectPath: this.projectFullPath,
...@@ -157,7 +163,7 @@ export default { ...@@ -157,7 +163,7 @@ export default {
response?.status === httpStatusCodes.NOT_FOUND || response?.status === httpStatusCodes.NOT_FOUND ||
response?.status === httpStatusCodes.BAD_REQUEST response?.status === httpStatusCodes.BAD_REQUEST
) { ) {
this.hasNoCiConfigFile = true; this.showStartScreen = true;
} else { } else {
this.reportFailure(LOAD_FAILURE_UNKNOWN); this.reportFailure(LOAD_FAILURE_UNKNOWN);
} }
...@@ -183,6 +189,10 @@ export default { ...@@ -183,6 +189,10 @@ export default {
resetContent() { resetContent() {
this.currentCiFileContent = this.lastCommittedContent; this.currentCiFileContent = this.lastCommittedContent;
}, },
setNewEmptyCiConfigFile() {
this.showStartScreen = false;
this.isNewConfigFile = true;
},
showErrorAlert({ type, reasons = [] }) { showErrorAlert({ type, reasons = [] }) {
this.reportFailure(type, reasons); this.reportFailure(type, reasons);
}, },
...@@ -202,7 +212,10 @@ export default { ...@@ -202,7 +212,10 @@ export default {
<template> <template>
<div class="gl-mt-4 gl-relative"> <div class="gl-mt-4 gl-relative">
<gl-loading-icon v-if="isBlobContentLoading" size="lg" class="gl-m-3" /> <gl-loading-icon v-if="isBlobContentLoading" size="lg" class="gl-m-3" />
<pipeline-editor-empty-state v-else-if="hasNoCiConfigFile" /> <pipeline-editor-empty-state
v-else-if="showStartScreen"
@createEmptyConfigFile="setNewEmptyCiConfigFile"
/>
<div v-else> <div v-else>
<gl-alert v-if="showSuccessAlert" :variant="success.variant" @dismiss="dismissSuccess"> <gl-alert v-if="showSuccessAlert" :variant="success.variant" @dismiss="dismissSuccess">
{{ success.text }} {{ success.text }}
......
...@@ -45,6 +45,7 @@ export default { ...@@ -45,6 +45,7 @@ export default {
<template> <template>
<div> <div>
<pipeline-editor-header <pipeline-editor-header
:ci-file-content="ciFileContent"
:ci-config-data="ciConfigData" :ci-config-data="ciConfigData"
:is-ci-config-data-loading="isCiConfigDataLoading" :is-ci-config-data-loading="isCiConfigDataLoading"
/> />
......
...@@ -6,6 +6,7 @@ class Projects::Ci::PipelineEditorController < Projects::ApplicationController ...@@ -6,6 +6,7 @@ class Projects::Ci::PipelineEditorController < Projects::ApplicationController
push_frontend_feature_flag(:ci_config_visualization_tab, @project, default_enabled: :yaml) push_frontend_feature_flag(:ci_config_visualization_tab, @project, default_enabled: :yaml)
push_frontend_feature_flag(:ci_config_merged_tab, @project, default_enabled: :yaml) push_frontend_feature_flag(:ci_config_merged_tab, @project, default_enabled: :yaml)
push_frontend_feature_flag(:pipeline_status_for_pipeline_editor, @project, default_enabled: :yaml) push_frontend_feature_flag(:pipeline_status_for_pipeline_editor, @project, default_enabled: :yaml)
push_frontend_feature_flag(:pipeline_editor_empty_state_action, @project, default_enabled: :yaml)
end end
feature_category :pipeline_authoring feature_category :pipeline_authoring
......
---
name: pipeline_editor_empty_state_action
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/55414
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/323229
milestone: '13.10'
type: development
group: group::pipeline authoring
default_enabled: false
...@@ -8685,6 +8685,9 @@ msgstr "" ...@@ -8685,6 +8685,9 @@ msgstr ""
msgid "Create new %{name} by email" msgid "Create new %{name} by email"
msgstr "" msgstr ""
msgid "Create new CI/CD pipeline"
msgstr ""
msgid "Create new Value Stream" msgid "Create new Value Stream"
msgstr "" msgstr ""
...@@ -33455,6 +33458,9 @@ msgstr "" ...@@ -33455,6 +33458,9 @@ msgstr ""
msgid "We would like to inform you that your subscription GitLab Enterprise Edition %{plan_name} is nearing its user limit. You have %{active_user_count} active users, which is almost at the user limit of %{maximum_user_count}." msgid "We would like to inform you that your subscription GitLab Enterprise Edition %{plan_name} is nearing its user limit. You have %{active_user_count} active users, which is almost at the user limit of %{maximum_user_count}."
msgstr "" msgstr ""
msgid "We'll continuously validate your pipeline configuration. The validation results will appear here."
msgstr ""
msgid "We've found no vulnerabilities" msgid "We've found no vulnerabilities"
msgstr "" msgstr ""
......
...@@ -3,7 +3,7 @@ import PipelineEditorHeader from '~/pipeline_editor/components/header/pipeline_e ...@@ -3,7 +3,7 @@ import PipelineEditorHeader from '~/pipeline_editor/components/header/pipeline_e
import PipelineStatus from '~/pipeline_editor/components/header/pipeline_status.vue'; import PipelineStatus from '~/pipeline_editor/components/header/pipeline_status.vue';
import ValidationSegment from '~/pipeline_editor/components/header/validation_segment.vue'; import ValidationSegment from '~/pipeline_editor/components/header/validation_segment.vue';
import { mockLintResponse } from '../../mock_data'; import { mockCiYml, mockLintResponse } from '../../mock_data';
describe('Pipeline editor header', () => { describe('Pipeline editor header', () => {
let wrapper; let wrapper;
...@@ -19,8 +19,9 @@ describe('Pipeline editor header', () => { ...@@ -19,8 +19,9 @@ describe('Pipeline editor header', () => {
...mockProvide, ...mockProvide,
...provide, ...provide,
}, },
props: { propsData: {
ciConfigData: mockLintResponse, ciConfigData: mockLintResponse,
ciFileContent: mockCiYml,
isCiConfigDataLoading: false, isCiConfigDataLoading: false,
}, },
}); });
......
...@@ -7,9 +7,9 @@ import ValidationSegment, { ...@@ -7,9 +7,9 @@ import ValidationSegment, {
i18n, i18n,
} from '~/pipeline_editor/components/header/validation_segment.vue'; } from '~/pipeline_editor/components/header/validation_segment.vue';
import { CI_CONFIG_STATUS_INVALID } from '~/pipeline_editor/constants'; import { CI_CONFIG_STATUS_INVALID } from '~/pipeline_editor/constants';
import { mockYmlHelpPagePath, mergeUnwrappedCiConfig } from '../../mock_data'; import { mockYmlHelpPagePath, mergeUnwrappedCiConfig, mockCiYml } from '../../mock_data';
describe('~/pipeline_editor/components/info/validation_segment.vue', () => { describe('Validation segment component', () => {
let wrapper; let wrapper;
const createComponent = (props = {}) => { const createComponent = (props = {}) => {
...@@ -20,6 +20,7 @@ describe('~/pipeline_editor/components/info/validation_segment.vue', () => { ...@@ -20,6 +20,7 @@ describe('~/pipeline_editor/components/info/validation_segment.vue', () => {
}, },
propsData: { propsData: {
ciConfig: mergeUnwrappedCiConfig(), ciConfig: mergeUnwrappedCiConfig(),
ciFileContent: mockCiYml,
loading: false, loading: false,
...props, ...props,
}, },
...@@ -42,6 +43,20 @@ describe('~/pipeline_editor/components/info/validation_segment.vue', () => { ...@@ -42,6 +43,20 @@ describe('~/pipeline_editor/components/info/validation_segment.vue', () => {
expect(wrapper.text()).toBe(i18n.loading); expect(wrapper.text()).toBe(i18n.loading);
}); });
describe('when config is empty', () => {
beforeEach(() => {
createComponent({ ciFileContent: '' });
});
it('has check icon', () => {
expect(findIcon().props('name')).toBe('check');
});
it('shows a message for empty state', () => {
expect(findValidationMsg().text()).toBe(i18n.empty);
});
});
describe('when config is valid', () => { describe('when config is valid', () => {
beforeEach(() => { beforeEach(() => {
createComponent({}); createComponent({});
...@@ -61,7 +76,7 @@ describe('~/pipeline_editor/components/info/validation_segment.vue', () => { ...@@ -61,7 +76,7 @@ describe('~/pipeline_editor/components/info/validation_segment.vue', () => {
}); });
}); });
describe('when config is not valid', () => { describe('when config is invalid', () => {
beforeEach(() => { beforeEach(() => {
createComponent({ createComponent({
ciConfig: mergeUnwrappedCiConfig({ ciConfig: mergeUnwrappedCiConfig({
......
import { GlSprintf } from '@gitlab/ui'; import { GlButton, GlSprintf } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import PipelineEditorEmptyState from '~/pipeline_editor/components/ui/pipeline_editor_empty_state.vue'; import PipelineEditorEmptyState from '~/pipeline_editor/components/ui/pipeline_editor_empty_state.vue';
describe('Pipeline editor empty state', () => { describe('Pipeline editor empty state', () => {
let wrapper; let wrapper;
const defaultProvide = { const defaultProvide = {
glFeatures: {
pipelineEditorEmptyStateAction: false,
},
emptyStateIllustrationPath: 'my/svg/path', emptyStateIllustrationPath: 'my/svg/path',
}; };
const createComponent = () => { const createComponent = ({ provide } = {}) => {
wrapper = shallowMount(PipelineEditorEmptyState, { wrapper = shallowMount(PipelineEditorEmptyState, {
provide: defaultProvide, provide: { ...defaultProvide, ...provide },
}); });
}; };
const findSvgImage = () => wrapper.find('img'); const findSvgImage = () => wrapper.find('img');
const findTitle = () => wrapper.find('h1'); const findTitle = () => wrapper.find('h1');
const findConfirmButton = () => wrapper.findComponent(GlButton);
const findDescription = () => wrapper.findComponent(GlSprintf); const findDescription = () => wrapper.findComponent(GlSprintf);
beforeEach(() => {
createComponent();
});
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
}); });
it('renders an svg image', () => { describe('template', () => {
expect(findSvgImage().exists()).toBe(true); beforeEach(() => {
}); createComponent();
});
it('renders a title', () => { it('renders an svg image', () => {
expect(findTitle().exists()).toBe(true); expect(findSvgImage().exists()).toBe(true);
expect(findTitle().text()).toBe(wrapper.vm.$options.i18n.title); });
it('renders a title', () => {
expect(findTitle().exists()).toBe(true);
expect(findTitle().text()).toBe(wrapper.vm.$options.i18n.title);
});
it('renders a description', () => {
expect(findDescription().exists()).toBe(true);
expect(findDescription().html()).toContain(wrapper.vm.$options.i18n.body);
});
describe('with feature flag off', () => {
it('does not renders a CTA button', () => {
expect(findConfirmButton().exists()).toBe(false);
});
});
}); });
it('renders a description', () => { describe('with feature flag on', () => {
expect(findDescription().exists()).toBe(true); beforeEach(() => {
expect(findDescription().html()).toContain(wrapper.vm.$options.i18n.body); createComponent({
provide: {
glFeatures: {
pipelineEditorEmptyStateAction: true,
},
},
});
});
it('renders a CTA button', () => {
expect(findConfirmButton().exists()).toBe(true);
expect(findConfirmButton().text()).toBe(wrapper.vm.$options.i18n.btnText);
});
it('emits an event when clicking on the CTA', async () => {
const expectedEvent = 'createEmptyConfigFile';
expect(wrapper.emitted(expectedEvent)).toBeUndefined();
await findConfirmButton().vm.$emit('click');
expect(wrapper.emitted(expectedEvent)).toHaveLength(1);
});
}); });
}); });
...@@ -7,6 +7,7 @@ import httpStatusCodes from '~/lib/utils/http_status'; ...@@ -7,6 +7,7 @@ import httpStatusCodes from '~/lib/utils/http_status';
import CommitForm from '~/pipeline_editor/components/commit/commit_form.vue'; import CommitForm from '~/pipeline_editor/components/commit/commit_form.vue';
import TextEditor from '~/pipeline_editor/components/editor/text_editor.vue'; import TextEditor from '~/pipeline_editor/components/editor/text_editor.vue';
import PipelineEditorTabs from '~/pipeline_editor/components/pipeline_editor_tabs.vue';
import PipelineEditorEmptyState from '~/pipeline_editor/components/ui/pipeline_editor_empty_state.vue'; import PipelineEditorEmptyState from '~/pipeline_editor/components/ui/pipeline_editor_empty_state.vue';
import { COMMIT_SUCCESS, COMMIT_FAILURE, LOAD_FAILURE_UNKNOWN } from '~/pipeline_editor/constants'; import { COMMIT_SUCCESS, COMMIT_FAILURE, LOAD_FAILURE_UNKNOWN } from '~/pipeline_editor/constants';
import getCiConfigData from '~/pipeline_editor/graphql/queries/ci_config.graphql'; import getCiConfigData from '~/pipeline_editor/graphql/queries/ci_config.graphql';
...@@ -30,6 +31,9 @@ const MockEditorLite = { ...@@ -30,6 +31,9 @@ const MockEditorLite = {
const mockProvide = { const mockProvide = {
ciConfigPath: mockCiConfigPath, ciConfigPath: mockCiConfigPath,
defaultBranch: mockDefaultBranch, defaultBranch: mockDefaultBranch,
glFeatures: {
pipelineEditorEmptyStateAction: false,
},
projectFullPath: mockProjectFullPath, projectFullPath: mockProjectFullPath,
}; };
...@@ -40,14 +44,17 @@ describe('Pipeline editor app component', () => { ...@@ -40,14 +44,17 @@ describe('Pipeline editor app component', () => {
let mockBlobContentData; let mockBlobContentData;
let mockCiConfigData; let mockCiConfigData;
const createComponent = ({ blobLoading = false, options = {} } = {}) => { const createComponent = ({ blobLoading = false, options = {}, provide = {} } = {}) => {
wrapper = shallowMount(PipelineEditorApp, { wrapper = shallowMount(PipelineEditorApp, {
provide: mockProvide, provide: { ...mockProvide, ...provide },
stubs: { stubs: {
GlTabs, GlTabs,
GlButton, GlButton,
CommitForm, CommitForm,
PipelineEditorHome,
PipelineEditorTabs,
EditorLite: MockEditorLite, EditorLite: MockEditorLite,
PipelineEditorEmptyState,
}, },
mocks: { mocks: {
$apollo: { $apollo: {
...@@ -65,7 +72,7 @@ describe('Pipeline editor app component', () => { ...@@ -65,7 +72,7 @@ describe('Pipeline editor app component', () => {
}); });
}; };
const createComponentWithApollo = ({ props = {} } = {}) => { const createComponentWithApollo = ({ props = {}, provide = {} } = {}) => {
const handlers = [[getCiConfigData, mockCiConfigData]]; const handlers = [[getCiConfigData, mockCiConfigData]];
const resolvers = { const resolvers = {
Query: { Query: {
...@@ -86,7 +93,7 @@ describe('Pipeline editor app component', () => { ...@@ -86,7 +93,7 @@ describe('Pipeline editor app component', () => {
apolloProvider: mockApollo, apolloProvider: mockApollo,
}; };
createComponent({ props, options }); createComponent({ props, provide, options });
}; };
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
...@@ -94,6 +101,8 @@ describe('Pipeline editor app component', () => { ...@@ -94,6 +101,8 @@ describe('Pipeline editor app component', () => {
const findEditorHome = () => wrapper.findComponent(PipelineEditorHome); const findEditorHome = () => wrapper.findComponent(PipelineEditorHome);
const findTextEditor = () => wrapper.findComponent(TextEditor); const findTextEditor = () => wrapper.findComponent(TextEditor);
const findEmptyState = () => wrapper.findComponent(PipelineEditorEmptyState); const findEmptyState = () => wrapper.findComponent(PipelineEditorEmptyState);
const findEmptyStateButton = () =>
wrapper.findComponent(PipelineEditorEmptyState).findComponent(GlButton);
beforeEach(() => { beforeEach(() => {
mockBlobContentData = jest.fn(); mockBlobContentData = jest.fn();
...@@ -105,7 +114,6 @@ describe('Pipeline editor app component', () => { ...@@ -105,7 +114,6 @@ describe('Pipeline editor app component', () => {
mockCiConfigData.mockReset(); mockCiConfigData.mockReset();
wrapper.destroy(); wrapper.destroy();
wrapper = null;
}); });
it('displays a loading icon if the blob query is loading', () => { it('displays a loading icon if the blob query is loading', () => {
...@@ -196,6 +204,34 @@ describe('Pipeline editor app component', () => { ...@@ -196,6 +204,34 @@ describe('Pipeline editor app component', () => {
}); });
}); });
describe('when landing on the empty state with feature flag on', () => {
it('user can click on CTA button and see an empty editor', async () => {
mockBlobContentData.mockRejectedValueOnce({
response: {
status: httpStatusCodes.NOT_FOUND,
},
});
createComponentWithApollo({
provide: {
glFeatures: {
pipelineEditorEmptyStateAction: true,
},
},
});
await waitForPromises();
expect(findEmptyState().exists()).toBe(true);
expect(findTextEditor().exists()).toBe(false);
await findEmptyStateButton().vm.$emit('click');
expect(findEmptyState().exists()).toBe(false);
expect(findTextEditor().exists()).toBe(true);
});
});
describe('when the user commits', () => { describe('when the user commits', () => {
const updateFailureMessage = 'The GitLab CI configuration could not be updated.'; const updateFailureMessage = 'The GitLab CI configuration could not be updated.';
......
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