Commit 247edd66 authored by Sarah Groff Hennigh-Palermo's avatar Sarah Groff Hennigh-Palermo

Merge branch 'pipeline-editor-global-state' into 'master'

Adds a global status to the pipeline editor section

See merge request gitlab-org/gitlab!55787
parents a043e506 dd287fad
......@@ -32,7 +32,7 @@ export default {
return {
content: '',
loading: false,
valid: false,
isValid: false,
errors: null,
warnings: null,
jobs: [],
......@@ -61,7 +61,7 @@ export default {
});
this.showingResults = true;
this.valid = valid;
this.isValid = valid;
this.errors = errors;
this.warnings = warnings;
this.jobs = jobs;
......@@ -120,7 +120,7 @@ export default {
<ci-lint-results
v-if="showingResults"
class="col-sm-12 gl-mt-5"
:valid="valid"
:is-valid="isValid"
:jobs="jobs"
:errors="errors"
:warnings="warnings"
......
......@@ -2,7 +2,6 @@
import { GlAlert, GlIcon } from '@gitlab/ui';
import { uniqueId } from 'lodash';
import { __, s__ } from '~/locale';
import { CI_CONFIG_STATUS_INVALID } from '~/pipeline_editor/constants';
import { DEFAULT, INVALID_CI_CONFIG } from '~/pipelines/constants';
import EditorLite from '~/vue_shared/components/editor_lite.vue';
......@@ -21,6 +20,10 @@ export default {
},
inject: ['ciConfigPath'],
props: {
isValid: {
type: Boolean,
required: true,
},
ciConfigData: {
type: Object,
required: true,
......@@ -46,9 +49,6 @@ export default {
hasError() {
return this.failureType;
},
isInvalidConfiguration() {
return this.ciConfigData.status === CI_CONFIG_STATUS_INVALID;
},
mergedYaml() {
return this.ciConfigData.mergedYaml;
},
......@@ -57,7 +57,7 @@ export default {
ciConfigData: {
immediate: true,
handler() {
if (this.isInvalidConfiguration) {
if (!this.isValid) {
this.reportFailure(INVALID_CI_CONFIG);
} else if (this.hasError) {
this.resetFailure();
......
......@@ -31,18 +31,10 @@ export default {
},
mixins: [glFeatureFlagsMixin()],
props: {
ciFileContent: {
type: String,
required: true,
},
ciConfigData: {
type: Object,
required: true,
},
isCiConfigDataLoading: {
type: Boolean,
required: true,
},
},
computed: {
showPipelineStatus() {
......@@ -61,11 +53,6 @@ export default {
<template>
<div class="gl-mb-5">
<pipeline-status v-if="showPipelineStatus" :class="$options.pipelineStatusClasses" />
<validation-segment
:class="validationStyling"
:loading="isCiConfigDataLoading"
:ci-file-content="ciFileContent"
:ci-config="ciConfigData"
/>
<validation-segment :class="validationStyling" :ci-config="ciConfigData" />
</div>
</template>
<script>
import { GlIcon, GlLink, GlLoadingIcon } from '@gitlab/ui';
import { __, s__, sprintf } from '~/locale';
import getAppStatus from '~/pipeline_editor/graphql/queries/client/app_status.graphql';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
import { CI_CONFIG_STATUS_VALID } from '../../constants';
import {
EDITOR_APP_STATUS_EMPTY,
EDITOR_APP_STATUS_LOADING,
EDITOR_APP_STATUS_VALID,
} from '../../constants';
export const i18n = {
empty: __(
......@@ -29,47 +34,51 @@ export default {
},
},
props: {
ciFileContent: {
type: String,
required: true,
},
ciConfig: {
type: Object,
required: false,
default: () => ({}),
},
loading: {
type: Boolean,
required: false,
default: false,
},
apollo: {
appStatus: {
query: getAppStatus,
},
},
computed: {
isEmpty() {
return !this.ciFileContent;
return this.appStatus === EDITOR_APP_STATUS_EMPTY;
},
isLoading() {
return this.appStatus === EDITOR_APP_STATUS_LOADING;
},
isValid() {
return this.ciConfig?.status === CI_CONFIG_STATUS_VALID;
return this.appStatus === EDITOR_APP_STATUS_VALID;
},
icon() {
if (this.isValid || this.isEmpty) {
return 'check';
switch (this.appStatus) {
case EDITOR_APP_STATUS_EMPTY:
return 'check';
case EDITOR_APP_STATUS_VALID:
return 'check';
default:
return 'warning-solid';
}
return 'warning-solid';
},
message() {
if (this.isEmpty) {
return this.$options.i18n.empty;
} else if (this.isValid) {
return this.$options.i18n.valid;
}
// Only display first error as a reason
const [reason] = this.ciConfig?.errors || [];
if (reason) {
return sprintf(this.$options.i18n.invalidWithReason, { reason }, false);
switch (this.appStatus) {
case EDITOR_APP_STATUS_EMPTY:
return this.$options.i18n.empty;
case EDITOR_APP_STATUS_VALID:
return this.$options.i18n.valid;
default:
// Only display first error as a reason
return this.ciConfig?.errors.length > 0
? sprintf(this.$options.i18n.invalidWithReason, { reason }, false)
: this.$options.i18n.invalid;
}
return this.$options.i18n.invalid;
},
},
};
......@@ -77,7 +86,7 @@ export default {
<template>
<div>
<template v-if="loading">
<template v-if="isLoading">
<gl-loading-icon inline />
{{ $options.i18n.loading }}
</template>
......
<script>
import { flatten } from 'lodash';
import { CI_CONFIG_STATUS_VALID } from '../../constants';
import CiLintResults from './ci_lint_results.vue';
export default {
......@@ -13,15 +12,16 @@ export default {
},
},
props: {
isValid: {
type: Boolean,
required: true,
},
ciConfig: {
type: Object,
required: true,
},
},
computed: {
isValid() {
return this.ciConfig?.status === CI_CONFIG_STATUS_VALID;
},
stages() {
return this.ciConfig?.stages || [];
},
......@@ -45,9 +45,9 @@ export default {
<template>
<ci-lint-results
:valid="isValid"
:jobs="jobs"
:errors="ciConfig.errors"
:is-valid="isValid"
:jobs="jobs"
:lint-help-page-path="lintHelpPagePath"
/>
</template>
......@@ -42,34 +42,34 @@ export default {
CiLintResultsParam,
},
props: {
valid: {
type: Boolean,
required: true,
},
jobs: {
type: Array,
required: false,
default: () => [],
},
errors: {
type: Array,
required: false,
default: () => [],
},
warnings: {
type: Array,
required: false,
default: () => [],
},
dryRun: {
type: Boolean,
required: false,
default: false,
},
isValid: {
type: Boolean,
required: true,
},
jobs: {
type: Array,
required: false,
default: () => [],
},
lintHelpPagePath: {
type: String,
required: true,
},
warnings: {
type: Array,
required: false,
default: () => [],
},
},
data() {
return {
......@@ -78,7 +78,7 @@ export default {
},
computed: {
status() {
return this.valid ? this.$options.correct : this.$options.incorrect;
return this.isValid ? this.$options.correct : this.$options.incorrect;
},
shouldShowTable() {
return this.errors.length === 0;
......
......@@ -4,12 +4,15 @@ import { s__ } from '~/locale';
import PipelineGraph from '~/pipelines/components/pipeline_graph/pipeline_graph.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import {
CI_CONFIG_STATUS_INVALID,
CREATE_TAB,
EDITOR_APP_STATUS_ERROR,
EDITOR_APP_STATUS_LOADING,
EDITOR_APP_STATUS_VALID,
LINT_TAB,
MERGED_TAB,
VISUALIZE_TAB,
} from '../constants';
import getAppStatus from '../graphql/queries/client/app_status.graphql';
import CiConfigMergedPreview from './editor/ci_config_merged_preview.vue';
import TextEditor from './editor/text_editor.vue';
import CiLint from './lint/ci_lint.vue';
......@@ -52,17 +55,22 @@ export default {
type: String,
required: true,
},
isCiConfigDataLoading: {
type: Boolean,
required: false,
default: false,
},
apollo: {
appStatus: {
query: getAppStatus,
},
},
computed: {
hasMergedYamlLoadError() {
return (
!this.ciConfigData?.mergedYaml && this.ciConfigData.status !== CI_CONFIG_STATUS_INVALID
);
hasAppError() {
// Not an invalid config and with `mergedYaml` data missing
return this.appStatus === EDITOR_APP_STATUS_ERROR;
},
isValid() {
return this.appStatus === EDITOR_APP_STATUS_VALID;
},
isLoading() {
return this.appStatus === EDITOR_APP_STATUS_LOADING;
},
},
methods: {
......@@ -91,7 +99,7 @@ export default {
data-testid="visualization-tab"
@click="setCurrentTab($options.tabConstants.VISUALIZE_TAB)"
>
<gl-loading-icon v-if="isCiConfigDataLoading" size="lg" class="gl-m-3" />
<gl-loading-icon v-if="isLoading" size="lg" class="gl-m-3" />
<pipeline-graph v-else :pipeline-data="ciConfigData" />
</gl-tab>
<editor-tab
......@@ -100,8 +108,8 @@ export default {
data-testid="lint-tab"
@click="setCurrentTab($options.tabConstants.LINT_TAB)"
>
<gl-loading-icon v-if="isCiConfigDataLoading" size="lg" class="gl-m-3" />
<ci-lint v-else :ci-config="ciConfigData" />
<gl-loading-icon v-if="isLoading" size="lg" class="gl-m-3" />
<ci-lint v-else :is-valid="isValid" :ci-config="ciConfigData" />
</editor-tab>
<gl-tab
v-if="glFeatures.ciConfigMergedTab"
......@@ -111,11 +119,16 @@ export default {
data-testid="merged-tab"
@click="setCurrentTab($options.tabConstants.MERGED_TAB)"
>
<gl-loading-icon v-if="isCiConfigDataLoading" size="lg" class="gl-m-3" />
<gl-alert v-else-if="hasMergedYamlLoadError" variant="danger" :dismissible="false">
<gl-loading-icon v-if="isLoading" size="lg" class="gl-m-3" />
<gl-alert v-else-if="hasAppError" variant="danger" :dismissible="false">
{{ $options.errorTexts.loadMergedYaml }}
</gl-alert>
<ci-config-merged-preview v-else :ci-config-data="ciConfigData" v-on="$listeners" />
<ci-config-merged-preview
v-else
:is-valid="isValid"
:ci-config-data="ciConfigData"
v-on="$listeners"
/>
</gl-tab>
</gl-tabs>
</template>
export const CI_CONFIG_STATUS_VALID = 'VALID';
// Values for CI_CONFIG_STATUS_* comes from lint graphQL
export const CI_CONFIG_STATUS_INVALID = 'INVALID';
export const CI_CONFIG_STATUS_VALID = 'VALID';
// Values for EDITOR_APP_STATUS_* are frontend specifics and
// represent the global state of the pipeline editor app.
export const EDITOR_APP_STATUS_EMPTY = 'EMPTY';
export const EDITOR_APP_STATUS_ERROR = 'ERROR';
export const EDITOR_APP_STATUS_INVALID = CI_CONFIG_STATUS_INVALID;
export const EDITOR_APP_STATUS_LOADING = 'LOADING';
export const EDITOR_APP_STATUS_VALID = CI_CONFIG_STATUS_VALID;
export const COMMIT_FAILURE = 'COMMIT_FAILURE';
export const COMMIT_SUCCESS = 'COMMIT_SUCCESS';
......
......@@ -3,6 +3,7 @@ import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
import { resetServiceWorkersPublicPath } from '../lib/utils/webpack';
import { EDITOR_APP_STATUS_LOADING } from './constants';
import { resolvers } from './graphql/resolvers';
import typeDefs from './graphql/typedefs.graphql';
import PipelineEditorApp from './pipeline_editor_app.vue';
......@@ -45,6 +46,7 @@ export const initPipelineEditor = (selector = '#js-pipeline-editor') => {
data: {
currentBranch: initialBranchName || defaultBranch,
commitSha,
status: EDITOR_APP_STATUS_LOADING,
},
});
......
......@@ -6,9 +6,18 @@ import { __, s__ } from '~/locale';
import { unwrapStagesWithNeeds } from '~/pipelines/components/unwrapping_utils';
import ConfirmUnsavedChangesDialog from './components/ui/confirm_unsaved_changes_dialog.vue';
import PipelineEditorEmptyState from './components/ui/pipeline_editor_empty_state.vue';
import { COMMIT_FAILURE, COMMIT_SUCCESS, DEFAULT_FAILURE, LOAD_FAILURE_UNKNOWN } from './constants';
import {
COMMIT_FAILURE,
COMMIT_SUCCESS,
DEFAULT_FAILURE,
EDITOR_APP_STATUS_EMPTY,
EDITOR_APP_STATUS_ERROR,
EDITOR_APP_STATUS_LOADING,
LOAD_FAILURE_UNKNOWN,
} from './constants';
import getBlobContent from './graphql/queries/blob_content.graphql';
import getCiConfigData from './graphql/queries/ci_config.graphql';
import getAppStatus from './graphql/queries/client/app_status.graphql';
import getCurrentBranch from './graphql/queries/client/current_branch.graphql';
import getIsNewCiConfigFile from './graphql/queries/client/is_new_ci_config_file.graphql';
import PipelineEditorHome from './pipeline_editor_home.vue';
......@@ -32,7 +41,6 @@ export default {
data() {
return {
ciConfigData: {},
// Success and failure state
failureType: null,
failureReasons: [],
showStartScreen: false,
......@@ -77,8 +85,7 @@ export default {
},
ciConfigData: {
query: getCiConfigData,
// If content is not loaded, we can't lint the data
skip: ({ currentCiFileContent }) => {
skip({ currentCiFileContent }) {
return !currentCiFileContent;
},
variables() {
......@@ -94,9 +101,20 @@ export default {
return { ...ciConfig, stages };
},
result({ data }) {
this.setAppStatus(data?.ciConfig?.status || EDITOR_APP_STATUS_ERROR);
},
error() {
this.reportFailure(LOAD_FAILURE_UNKNOWN);
},
watchLoading(isLoading) {
if (isLoading) {
this.setAppStatus(EDITOR_APP_STATUS_LOADING);
}
},
},
appStatus: {
query: getAppStatus,
},
currentBranch: {
query: getCurrentBranch,
......@@ -115,6 +133,9 @@ export default {
isCiConfigDataLoading() {
return this.$apollo.queries.ciConfigData.loading;
},
isEmpty() {
return this.currentCiFileContent === '';
},
failure() {
switch (this.failureType) {
case LOAD_FAILURE_UNKNOWN:
......@@ -159,6 +180,13 @@ export default {
successTexts: {
[COMMIT_SUCCESS]: __('Your changes have been successfully committed.'),
},
watch: {
isEmpty(flag) {
if (flag) {
this.setAppStatus(EDITOR_APP_STATUS_EMPTY);
}
},
},
methods: {
handleBlobContentError(error = {}) {
const { networkError } = error;
......@@ -170,6 +198,7 @@ export default {
response?.status === httpStatusCodes.NOT_FOUND ||
response?.status === httpStatusCodes.BAD_REQUEST
) {
this.setAppStatus(EDITOR_APP_STATUS_EMPTY);
this.showStartScreen = true;
} else {
this.reportFailure(LOAD_FAILURE_UNKNOWN);
......@@ -183,6 +212,8 @@ export default {
this.showSuccessAlert = false;
},
reportFailure(type, reasons = []) {
this.setAppStatus(EDITOR_APP_STATUS_ERROR);
window.scrollTo({ top: 0, behavior: 'smooth' });
this.showFailureAlert = true;
this.failureType = type;
......@@ -196,6 +227,9 @@ export default {
resetContent() {
this.currentCiFileContent = this.lastCommittedContent;
},
setAppStatus(appStatus) {
this.$apollo.getClient().writeQuery({ query: getAppStatus, data: { appStatus } });
},
setNewEmptyCiConfigFile() {
this.$apollo
.getClient()
......@@ -242,7 +276,6 @@ export default {
</ul>
</gl-alert>
<pipeline-editor-home
:is-ci-config-data-loading="isCiConfigDataLoading"
:ci-config-data="ciConfigData"
:ci-file-content="currentCiFileContent"
@commit="updateOnCommit"
......
......@@ -19,10 +19,6 @@ export default {
type: String,
required: true,
},
isCiConfigDataLoading: {
type: Boolean,
required: true,
},
},
data() {
return {
......@@ -44,15 +40,10 @@ export default {
<template>
<div>
<pipeline-editor-header
:ci-file-content="ciFileContent"
:ci-config-data="ciConfigData"
:is-ci-config-data-loading="isCiConfigDataLoading"
/>
<pipeline-editor-header :ci-config-data="ciConfigData" />
<pipeline-editor-tabs
:ci-config-data="ciConfigData"
:ci-file-content="ciFileContent"
:is-ci-config-data-loading="isCiConfigDataLoading"
v-on="$listeners"
@set-current-tab="setCurrentTab"
/>
......
......@@ -3,7 +3,6 @@ import { shallowMount } from '@vue/test-utils';
import { EDITOR_READY_EVENT } from '~/editor/constants';
import CiConfigMergedPreview from '~/pipeline_editor/components/editor/ci_config_merged_preview.vue';
import { CI_CONFIG_STATUS_INVALID } from '~/pipeline_editor/constants';
import { INVALID_CI_CONFIG } from '~/pipelines/constants';
import { mockLintResponse, mockCiConfigPath } from '../../mock_data';
......@@ -39,12 +38,11 @@ describe('Text editor component', () => {
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
describe('when status is invalid', () => {
beforeEach(() => {
createComponent({ props: { ciConfigData: { status: CI_CONFIG_STATUS_INVALID } } });
createComponent({ props: { isValid: false } });
});
it('show an error message', () => {
......@@ -59,7 +57,7 @@ describe('Text editor component', () => {
describe('when status is valid', () => {
beforeEach(() => {
createComponent();
createComponent({ props: { isValid: true } });
});
it('shows an information message that the section is not editable', () => {
......
......@@ -6,13 +6,19 @@ import { sprintf } from '~/locale';
import ValidationSegment, {
i18n,
} from '~/pipeline_editor/components/header/validation_segment.vue';
import { CI_CONFIG_STATUS_INVALID } from '~/pipeline_editor/constants';
import {
CI_CONFIG_STATUS_INVALID,
EDITOR_APP_STATUS_EMPTY,
EDITOR_APP_STATUS_INVALID,
EDITOR_APP_STATUS_LOADING,
EDITOR_APP_STATUS_VALID,
} from '~/pipeline_editor/constants';
import { mockYmlHelpPagePath, mergeUnwrappedCiConfig, mockCiYml } from '../../mock_data';
describe('Validation segment component', () => {
let wrapper;
const createComponent = (props = {}) => {
const createComponent = ({ props = {}, appStatus }) => {
wrapper = extendedWrapper(
shallowMount(ValidationSegment, {
provide: {
......@@ -21,9 +27,14 @@ describe('Validation segment component', () => {
propsData: {
ciConfig: mergeUnwrappedCiConfig(),
ciFileContent: mockCiYml,
loading: false,
...props,
},
// Simulate graphQL client query result
data() {
return {
appStatus,
};
},
}),
);
};
......@@ -34,18 +45,17 @@ describe('Validation segment component', () => {
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
it('shows the loading state', () => {
createComponent({ loading: true });
createComponent({ appStatus: EDITOR_APP_STATUS_LOADING });
expect(wrapper.text()).toBe(i18n.loading);
});
describe('when config is empty', () => {
beforeEach(() => {
createComponent({ ciFileContent: '' });
createComponent({ appStatus: EDITOR_APP_STATUS_EMPTY });
});
it('has check icon', () => {
......@@ -59,7 +69,7 @@ describe('Validation segment component', () => {
describe('when config is valid', () => {
beforeEach(() => {
createComponent({});
createComponent({ appStatus: EDITOR_APP_STATUS_VALID });
});
it('has check icon', () => {
......@@ -79,12 +89,9 @@ describe('Validation segment component', () => {
describe('when config is invalid', () => {
beforeEach(() => {
createComponent({
ciConfig: mergeUnwrappedCiConfig({
status: CI_CONFIG_STATUS_INVALID,
}),
appStatus: EDITOR_APP_STATUS_INVALID,
});
});
it('has warning icon', () => {
expect(findIcon().props('name')).toBe('warning-solid');
});
......@@ -93,43 +100,53 @@ describe('Validation segment component', () => {
expect(findValidationMsg().text()).toBe(i18n.invalid);
});
it('shows an invalid state with an error', () => {
it('shows the learn more link', () => {
expect(findLearnMoreLink().attributes('href')).toBe(mockYmlHelpPagePath);
expect(findLearnMoreLink().text()).toBe('Learn more');
});
describe('with multiple errors', () => {
const firstError = 'First Error';
const secondError = 'Second Error';
createComponent({
ciConfig: mergeUnwrappedCiConfig({
status: CI_CONFIG_STATUS_INVALID,
errors: [firstError, secondError],
}),
beforeEach(() => {
createComponent({
props: {
ciConfig: mergeUnwrappedCiConfig({
status: CI_CONFIG_STATUS_INVALID,
errors: [firstError, secondError],
}),
},
});
});
it('shows an invalid state with an error', () => {
// Test the error is shown _and_ the string matches
expect(findValidationMsg().text()).toContain(firstError);
expect(findValidationMsg().text()).toBe(
sprintf(i18n.invalidWithReason, { reason: firstError }),
);
});
// Test the error is shown _and_ the string matches
expect(findValidationMsg().text()).toContain(firstError);
expect(findValidationMsg().text()).toBe(
sprintf(i18n.invalidWithReason, { reason: firstError }),
);
});
it('shows an invalid state with an error while preventing XSS', () => {
describe('with XSS inside the error', () => {
const evilError = '<script>evil();</script>';
createComponent({
ciConfig: mergeUnwrappedCiConfig({
status: CI_CONFIG_STATUS_INVALID,
errors: [evilError],
}),
beforeEach(() => {
createComponent({
props: {
ciConfig: mergeUnwrappedCiConfig({
status: CI_CONFIG_STATUS_INVALID,
errors: [evilError],
}),
},
});
});
it('shows an invalid state with an error while preventing XSS', () => {
const { innerHTML } = findValidationMsg().element;
const { innerHTML } = findValidationMsg().element;
expect(innerHTML).not.toContain(evilError);
expect(innerHTML).toContain(escape(evilError));
});
it('shows the learn more link', () => {
expect(findLearnMoreLink().attributes('href')).toBe(mockYmlHelpPagePath);
expect(findLearnMoreLink().text()).toBe('Learn more');
expect(innerHTML).not.toContain(evilError);
expect(innerHTML).toContain(escape(evilError));
});
});
});
});
......@@ -7,7 +7,7 @@ import { mockJobs, mockErrors, mockWarnings } from '../../mock_data';
describe('CI Lint Results', () => {
let wrapper;
const defaultProps = {
valid: true,
isValid: true,
jobs: mockJobs,
errors: [],
warnings: [],
......@@ -42,7 +42,6 @@ describe('CI Lint Results', () => {
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
describe('Empty results', () => {
......@@ -72,7 +71,7 @@ describe('CI Lint Results', () => {
describe('Invalid results', () => {
beforeEach(() => {
createComponent({ valid: false, errors: mockErrors, warnings: mockWarnings }, mount);
createComponent({ isValid: false, errors: mockErrors, warnings: mockWarnings }, mount);
});
it('does not display the table', () => {
......
import { GlAlert, GlLink } from '@gitlab/ui';
import { shallowMount, mount } from '@vue/test-utils';
import { mount, shallowMount } from '@vue/test-utils';
import CiLint from '~/pipeline_editor/components/lint/ci_lint.vue';
import { CI_CONFIG_STATUS_INVALID } from '~/pipeline_editor/constants';
import { mergeUnwrappedCiConfig, mockLintHelpPagePath } from '../../mock_data';
describe('~/pipeline_editor/components/lint/ci_lint.vue', () => {
let wrapper;
const createComponent = (props = {}, mountFn = shallowMount) => {
const createComponent = ({ props, mountFn = shallowMount } = {}) => {
wrapper = mountFn(CiLint, {
provide: {
lintHelpPagePath: mockLintHelpPagePath,
......@@ -27,12 +26,11 @@ describe('~/pipeline_editor/components/lint/ci_lint.vue', () => {
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
describe('Valid Results', () => {
beforeEach(() => {
createComponent({}, mount);
createComponent({ props: { isValid: true }, mountFn: mount });
});
it('displays valid results', () => {
......@@ -66,14 +64,7 @@ describe('~/pipeline_editor/components/lint/ci_lint.vue', () => {
});
it('displays invalid results', () => {
createComponent(
{
ciConfig: mergeUnwrappedCiConfig({
status: CI_CONFIG_STATUS_INVALID,
}),
},
mount,
);
createComponent({ props: { isValid: false }, mountFn: mount });
expect(findAlert().text()).toMatch('Status: Syntax is incorrect.');
});
......
......@@ -4,8 +4,12 @@ import { nextTick } from 'vue';
import CiConfigMergedPreview from '~/pipeline_editor/components/editor/ci_config_merged_preview.vue';
import CiLint from '~/pipeline_editor/components/lint/ci_lint.vue';
import PipelineEditorTabs from '~/pipeline_editor/components/pipeline_editor_tabs.vue';
import {
EDITOR_APP_STATUS_ERROR,
EDITOR_APP_STATUS_LOADING,
EDITOR_APP_STATUS_VALID,
} from '~/pipeline_editor/constants';
import PipelineGraph from '~/pipelines/components/pipeline_graph/pipeline_graph.vue';
import { mockLintResponse, mockCiYml } from '../mock_data';
describe('Pipeline editor tabs component', () => {
......@@ -20,14 +24,23 @@ describe('Pipeline editor tabs component', () => {
},
};
const createComponent = ({ props = {}, provide = {}, mountFn = shallowMount } = {}) => {
const createComponent = ({
props = {},
provide = {},
appStatus = EDITOR_APP_STATUS_VALID,
mountFn = shallowMount,
} = {}) => {
wrapper = mountFn(PipelineEditorTabs, {
propsData: {
ciConfigData: mockLintResponse,
ciFileContent: mockCiYml,
isCiConfigDataLoading: false,
...props,
},
data() {
return {
appStatus,
};
},
provide: { ...mockProvide, ...provide },
stubs: {
TextEditor: MockTextEditor,
......@@ -49,7 +62,6 @@ describe('Pipeline editor tabs component', () => {
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
describe('editor tab', () => {
......@@ -69,7 +81,7 @@ describe('Pipeline editor tabs component', () => {
describe('with feature flag on', () => {
describe('while loading', () => {
beforeEach(() => {
createComponent({ props: { isCiConfigDataLoading: true } });
createComponent({ appStatus: EDITOR_APP_STATUS_LOADING });
});
it('displays a loading icon if the lint query is loading', () => {
......@@ -108,7 +120,7 @@ describe('Pipeline editor tabs component', () => {
describe('lint tab', () => {
describe('while loading', () => {
beforeEach(() => {
createComponent({ props: { isCiConfigDataLoading: true } });
createComponent({ appStatus: EDITOR_APP_STATUS_LOADING });
});
it('displays a loading icon if the lint query is loading', () => {
......@@ -135,7 +147,7 @@ describe('Pipeline editor tabs component', () => {
describe('with feature flag on', () => {
describe('while loading', () => {
beforeEach(() => {
createComponent({ props: { isCiConfigDataLoading: true } });
createComponent({ appStatus: EDITOR_APP_STATUS_LOADING });
});
it('displays a loading icon if the lint query is loading', () => {
......@@ -143,9 +155,9 @@ describe('Pipeline editor tabs component', () => {
});
});
describe('when `mergedYaml` is undefined', () => {
describe('when there is a fetch error', () => {
beforeEach(() => {
createComponent({ props: { ciConfigData: {} } });
createComponent({ appStatus: EDITOR_APP_STATUS_ERROR });
});
it('show an error message', () => {
......
......@@ -72,7 +72,7 @@ describe('Pipeline editor app component', () => {
});
};
const createComponentWithApollo = ({ props = {}, provide = {} } = {}) => {
const createComponentWithApollo = async ({ props = {}, provide = {} } = {}) => {
const handlers = [[getCiConfigData, mockCiConfigData]];
const resolvers = {
Query: {
......@@ -94,6 +94,8 @@ describe('Pipeline editor app component', () => {
};
createComponent({ props, provide, options });
return waitForPromises();
};
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
......@@ -116,11 +118,13 @@ describe('Pipeline editor app component', () => {
wrapper.destroy();
});
it('displays a loading icon if the blob query is loading', () => {
createComponent({ blobLoading: true });
describe('loading state', () => {
it('displays a loading icon if the blob query is loading', () => {
createComponent({ blobLoading: true });
expect(findLoadingIcon().exists()).toBe(true);
expect(findTextEditor().exists()).toBe(false);
expect(findLoadingIcon().exists()).toBe(true);
expect(findTextEditor().exists()).toBe(false);
});
});
describe('when queries are called', () => {
......@@ -131,9 +135,7 @@ describe('Pipeline editor app component', () => {
describe('when file exists', () => {
beforeEach(async () => {
createComponentWithApollo();
await waitForPromises();
await createComponentWithApollo();
});
it('shows pipeline editor home component', () => {
......@@ -145,10 +147,6 @@ describe('Pipeline editor app component', () => {
});
it('ci config query is called with correct variables', async () => {
createComponentWithApollo();
await waitForPromises();
expect(mockCiConfigData).toHaveBeenCalledWith({
content: mockCiYml,
projectPath: mockProjectFullPath,
......@@ -164,9 +162,7 @@ describe('Pipeline editor app component', () => {
status: httpStatusCodes.BAD_REQUEST,
},
});
createComponentWithApollo();
await waitForPromises();
await createComponentWithApollo();
expect(findEmptyState().exists()).toBe(true);
expect(findAlert().exists()).toBe(false);
......@@ -181,9 +177,7 @@ describe('Pipeline editor app component', () => {
status: httpStatusCodes.NOT_FOUND,
},
});
createComponentWithApollo();
await waitForPromises();
await createComponentWithApollo();
expect(findEmptyState().exists()).toBe(true);
expect(findAlert().exists()).toBe(false);
......@@ -194,8 +188,7 @@ describe('Pipeline editor app component', () => {
describe('because of a fetching error', () => {
it('shows a unkown error message', async () => {
mockBlobContentData.mockRejectedValueOnce(new Error('My error!'));
createComponentWithApollo();
await waitForPromises();
await createComponentWithApollo();
expect(findEmptyState().exists()).toBe(false);
expect(findAlert().text()).toBe(wrapper.vm.$options.errorTexts[LOAD_FAILURE_UNKNOWN]);
......@@ -212,7 +205,7 @@ describe('Pipeline editor app component', () => {
},
});
createComponentWithApollo({
await createComponentWithApollo({
provide: {
glFeatures: {
pipelineEditorEmptyStateAction: true,
......@@ -220,8 +213,6 @@ describe('Pipeline editor app component', () => {
},
});
await waitForPromises();
expect(findEmptyState().exists()).toBe(true);
expect(findTextEditor().exists()).toBe(false);
......@@ -254,9 +245,9 @@ describe('Pipeline editor app component', () => {
describe('and the commit mutation fails', () => {
const commitFailedReasons = ['Commit failed'];
beforeEach(() => {
beforeEach(async () => {
window.scrollTo = jest.fn();
createComponent();
await createComponentWithApollo();
findEditorHome().vm.$emit('showError', {
type: COMMIT_FAILURE,
......@@ -278,9 +269,9 @@ describe('Pipeline editor app component', () => {
describe('when an unknown error occurs', () => {
const unknownReasons = ['Commit failed'];
beforeEach(() => {
beforeEach(async () => {
window.scrollTo = jest.fn();
createComponent();
await createComponentWithApollo();
findEditorHome().vm.$emit('showError', {
type: COMMIT_FAILURE,
......
......@@ -59,7 +59,10 @@ describe('pipeline graph component', () => {
describe('with `VALID` status', () => {
beforeEach(() => {
wrapper = createComponent({
pipelineData: { status: CI_CONFIG_STATUS_VALID, stages: [{ name: 'hello', groups: [] }] },
pipelineData: {
status: CI_CONFIG_STATUS_VALID,
stages: [{ name: 'hello', groups: [] }],
},
});
});
......
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