Commit dd287fad authored by Frédéric Caplette's avatar Frédéric Caplette Committed by Sarah Groff Hennigh-Palermo

Adds a global state to the pipeline editor section

Instead of relying on passing props data down to know
all about what is happening on a global level, we
write to apollo cache the global state of the app
that can then be consumed byy its children at any level
without needing extra layer of complexity.
parent fb9c1c99
......@@ -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