Commit 89a9e6c5 authored by Frédéric Caplette's avatar Frédéric Caplette Committed by Natalia Tepluhina

Notify users about unavailable linter in the linter widget

parent 74f64ebd
......@@ -5,6 +5,7 @@ import getAppStatus from '~/pipeline_editor/graphql/queries/client/app_status.qu
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue';
import {
EDITOR_APP_STATUS_EMPTY,
EDITOR_APP_STATUS_LINT_UNAVAILABLE,
EDITOR_APP_STATUS_LOADING,
EDITOR_APP_STATUS_VALID,
} from '../../constants';
......@@ -17,6 +18,7 @@ export const i18n = {
loading: s__('Pipelines|Validating GitLab CI configuration…'),
invalid: s__('Pipelines|This GitLab CI configuration is invalid.'),
invalidWithReason: s__('Pipelines|This GitLab CI configuration is invalid: %{reason}.'),
unavailableValidation: s__('Pipelines|Configuration validation currently not available.'),
valid: s__('Pipelines|This GitLab CI configuration is valid.'),
};
......@@ -29,6 +31,9 @@ export default {
TooltipOnTruncate,
},
inject: {
lintUnavailableHelpPagePath: {
default: '',
},
ymlHelpPagePath: {
default: '',
},
......@@ -49,9 +54,15 @@ export default {
},
},
computed: {
helpPath() {
return this.isLintUnavailable ? this.lintUnavailableHelpPagePath : this.ymlHelpPagePath;
},
isEmpty() {
return this.appStatus === EDITOR_APP_STATUS_EMPTY;
},
isLintUnavailable() {
return this.appStatus === EDITOR_APP_STATUS_LINT_UNAVAILABLE;
},
isLoading() {
return this.appStatus === EDITOR_APP_STATUS_LOADING;
},
......@@ -62,6 +73,8 @@ export default {
switch (this.appStatus) {
case EDITOR_APP_STATUS_EMPTY:
return 'check';
case EDITOR_APP_STATUS_LINT_UNAVAILABLE:
return 'time-out';
case EDITOR_APP_STATUS_VALID:
return 'check';
default:
......@@ -74,6 +87,8 @@ export default {
switch (this.appStatus) {
case EDITOR_APP_STATUS_EMPTY:
return this.$options.i18n.empty;
case EDITOR_APP_STATUS_LINT_UNAVAILABLE:
return this.$options.i18n.unavailableValidation;
case EDITOR_APP_STATUS_VALID:
return this.$options.i18n.valid;
default:
......@@ -99,7 +114,7 @@ export default {
<gl-icon :name="icon" /> <span data-testid="validationMsg">{{ message }}</span>
</tooltip-on-truncate>
<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="helpPath">
{{ $options.i18n.learnMore }}
</gl-link>
</span>
......
......@@ -11,6 +11,7 @@ import {
EDITOR_APP_STATUS_INVALID,
EDITOR_APP_STATUS_LOADING,
EDITOR_APP_STATUS_VALID,
EDITOR_APP_STATUS_LINT_UNAVAILABLE,
LINT_TAB,
MERGED_TAB,
TAB_QUERY_PARAM,
......@@ -106,6 +107,9 @@ export default {
isInvalid() {
return this.appStatus === EDITOR_APP_STATUS_INVALID;
},
isLintUnavailable() {
return this.appStatus === EDITOR_APP_STATUS_LINT_UNAVAILABLE;
},
isValid() {
return this.appStatus === EDITOR_APP_STATUS_VALID;
},
......@@ -166,6 +170,7 @@ export default {
:empty-message="$options.i18n.empty.visualization"
:is-empty="isEmpty"
:is-invalid="isInvalid"
:is-unavailable="isLintUnavailable"
:keep-component-mounted="false"
:title="$options.i18n.tabGraph"
lazy
......@@ -179,6 +184,7 @@ export default {
class="gl-mb-3"
:empty-message="$options.i18n.empty.lint"
:is-empty="isEmpty"
:is-unavailable="isLintUnavailable"
:title="$options.i18n.tabLint"
data-testid="lint-tab"
@click="setCurrentTab($options.tabConstants.LINT_TAB)"
......@@ -192,6 +198,7 @@ export default {
:keep-component-mounted="false"
:is-empty="isEmpty"
:is-invalid="isInvalid"
:is-unavailable="isLintUnavailable"
:title="$options.i18n.tabMergedYaml"
lazy
data-testid="merged-tab"
......
......@@ -42,6 +42,9 @@ import { __, s__ } from '~/locale';
export default {
i18n: {
invalid: __('Your CI/CD configuration syntax is invalid. View Lint tab for more details.'),
unavailable: __(
"We're experiencing difficulties and this tab content is currently unavailable.",
),
},
components: {
GlAlert,
......@@ -66,14 +69,14 @@ export default {
isEmpty: {
type: Boolean,
required: false,
default: null,
default: false,
},
isInvalid: {
type: Boolean,
required: false,
default: null,
default: false,
},
lazy: {
isUnavailable: {
type: Boolean,
required: false,
default: false,
......@@ -83,6 +86,11 @@ export default {
required: false,
default: true,
},
lazy: {
type: Boolean,
required: false,
default: false,
},
},
data() {
return {
......@@ -109,6 +117,9 @@ export default {
<template>
<gl-tab :lazy="isLazy" v-bind="$attrs" v-on="$listeners">
<gl-alert v-if="isEmpty" variant="tip">{{ emptyMessage }}</gl-alert>
<gl-alert v-else-if="isUnavailable" variant="danger" :dismissible="false">
{{ $options.i18n.unavailable }}</gl-alert
>
<gl-alert v-else-if="isInvalid" variant="danger">{{ $options.i18n.invalid }}</gl-alert>
<template v-else>
<slot v-for="slot in slots" :name="slot"></slot>
......
......@@ -6,12 +6,14 @@ export const CI_CONFIG_STATUS_VALID = 'VALID';
// represent the global state of the pipeline editor app.
export const EDITOR_APP_STATUS_EMPTY = 'EMPTY';
export const EDITOR_APP_STATUS_INVALID = CI_CONFIG_STATUS_INVALID;
export const EDITOR_APP_STATUS_LINT_UNAVAILABLE = 'LINT_DOWN';
export const EDITOR_APP_STATUS_LOADING = 'LOADING';
export const EDITOR_APP_STATUS_VALID = CI_CONFIG_STATUS_VALID;
export const EDITOR_APP_VALID_STATUSES = [
EDITOR_APP_STATUS_EMPTY,
EDITOR_APP_STATUS_INVALID,
EDITOR_APP_STATUS_LINT_UNAVAILABLE,
EDITOR_APP_STATUS_LOADING,
EDITOR_APP_STATUS_VALID,
];
......
......@@ -37,6 +37,7 @@ export const initPipelineEditor = (selector = '#js-pipeline-editor') => {
emptyStateIllustrationPath,
helpPaths,
lintHelpPagePath,
lintUnavailableHelpPagePath,
needsHelpPagePath,
newMergeRequestPath,
pipelinePagePath,
......@@ -124,6 +125,7 @@ export const initPipelineEditor = (selector = '#js-pipeline-editor') => {
emptyStateIllustrationPath,
helpPaths,
lintHelpPagePath,
lintUnavailableHelpPagePath,
needsHelpPagePath,
newMergeRequestPath,
pipelinePagePath,
......
......@@ -12,8 +12,9 @@ import PipelineEditorMessages from './components/ui/pipeline_editor_messages.vue
import {
COMMIT_SHA_POLL_INTERVAL,
EDITOR_APP_STATUS_EMPTY,
EDITOR_APP_VALID_STATUSES,
EDITOR_APP_STATUS_LOADING,
EDITOR_APP_STATUS_LINT_UNAVAILABLE,
EDITOR_APP_VALID_STATUSES,
LOAD_FAILURE_UNKNOWN,
STARTER_TEMPLATE_NAME,
} from './constants';
......@@ -51,6 +52,7 @@ export default {
failureReasons: [],
initialCiFileContent: '',
isFetchingCommitSha: false,
isLintUnavailable: false,
isNewCiConfigFile: false,
lastCommittedContent: '',
shouldSkipStartScreen: false,
......@@ -147,10 +149,19 @@ export default {
return { ...ciConfig, stages };
},
result({ data }) {
this.setAppStatus(data?.ciConfig?.status);
if (data?.ciConfig?.status) {
this.setAppStatus(data.ciConfig.status);
if (this.isLintUnavailable) {
this.isLintUnavailable = false;
}
}
},
error(err) {
this.reportFailure(LOAD_FAILURE_UNKNOWN, [String(err)]);
error() {
// We are not using `reportFailure` here because we don't
// need to bring attention to the linter being down. We let
// the user work on their file and if they look at their
// lint status, they will notice that the service is down
this.isLintUnavailable = true;
},
watchLoading(isLoading) {
if (isLoading) {
......@@ -247,6 +258,13 @@ export default {
this.setAppStatus(EDITOR_APP_STATUS_EMPTY);
}
},
isLintUnavailable(flag) {
if (flag) {
// We cannot set this status directly in the `error`
// hook otherwise we get an infinite loop caused by apollo.
this.setAppStatus(EDITOR_APP_STATUS_LINT_UNAVAILABLE);
}
},
},
mounted() {
this.loadTemplateFromURL();
......@@ -269,14 +287,10 @@ export default {
await this.$apollo.queries.initialCiFileContent.refetch();
},
reportFailure(type, reasons = []) {
const isCurrentFailure = this.failureType === type && this.failureReasons[0] === reasons[0];
if (!isCurrentFailure) {
this.showFailure = true;
this.failureType = type;
this.failureReasons = reasons;
window.scrollTo({ top: 0, behavior: 'smooth' });
}
this.showFailure = true;
this.failureType = type;
this.failureReasons = reasons;
window.scrollTo({ top: 0, behavior: 'smooth' });
},
reportSuccess(type) {
window.scrollTo({ top: 0, behavior: 'smooth' });
......@@ -289,7 +303,10 @@ export default {
},
setAppStatus(appStatus) {
if (EDITOR_APP_VALID_STATUSES.includes(appStatus)) {
this.$apollo.mutate({ mutation: updateAppStatus, variables: { appStatus } });
this.$apollo.mutate({
mutation: updateAppStatus,
variables: { appStatus },
});
}
},
setNewEmptyCiConfigFile() {
......
......@@ -20,6 +20,7 @@ module Ci
"empty-state-illustration-path" => image_path('illustrations/empty-state/empty-dag-md.svg'),
"initial-branch-name" => initial_branch,
"lint-help-page-path" => help_page_path('ci/lint', anchor: 'validate-basic-logic-and-syntax'),
"lint-unavailable-help-page-path" => help_page_path('ci/pipeline_editor/index', anchor: 'configuration-validation-currently-not-available'),
"needs-help-page-path" => help_page_path('ci/yaml/index', anchor: 'needs'),
"new-merge-request-path" => namespace_project_new_merge_request_path,
"pipeline_etag" => latest_commit ? graphql_etag_pipeline_sha_path(commit_sha) : '',
......
......@@ -97,3 +97,20 @@ If you enter a new branch name, the **Start a new merge request with these chang
checkbox appears. Select it to start a new merge request after you commit the changes.
![The commit form with a new branch](img/pipeline_editor_commit_v13_8.png)
## Troubleshooting
### `Configuration validation currently not available` message
This message is due to a problem with the syntax validation in the pipeline editor.
If GitLab is unable to communicate with the service that validates the syntax, the
information in these sections may not display properly:
- The syntax status on the **Edit** tab (valid or invalid).
- The **Visualize** tab.
- The **Lint** tab.
- The **View merged YAML** tab.
You can still work on your CI/CD configuration and commit the changes you made without
any issues. As soon as the service becomes available again, the syntax validation
should display immediately.
......@@ -25948,6 +25948,9 @@ msgstr ""
msgid "Pipelines|Clear runner caches"
msgstr ""
msgid "Pipelines|Configuration validation currently not available."
msgstr ""
msgid "Pipelines|Copy trigger token"
msgstr ""
......@@ -39421,6 +39424,9 @@ msgstr ""
msgid "We'll use this to help surface the right features and information to you."
msgstr ""
msgid "We're experiencing difficulties and this tab content is currently unavailable."
msgstr ""
msgid "We've found no vulnerabilities"
msgstr ""
......
......@@ -11,9 +11,15 @@ import {
EDITOR_APP_STATUS_EMPTY,
EDITOR_APP_STATUS_INVALID,
EDITOR_APP_STATUS_LOADING,
EDITOR_APP_STATUS_LINT_UNAVAILABLE,
EDITOR_APP_STATUS_VALID,
} from '~/pipeline_editor/constants';
import { mockYmlHelpPagePath, mergeUnwrappedCiConfig, mockCiYml } from '../../mock_data';
import {
mergeUnwrappedCiConfig,
mockCiYml,
mockLintUnavailableHelpPagePath,
mockYmlHelpPagePath,
} from '../../mock_data';
describe('Validation segment component', () => {
let wrapper;
......@@ -23,6 +29,7 @@ describe('Validation segment component', () => {
shallowMount(ValidationSegment, {
provide: {
ymlHelpPagePath: mockYmlHelpPagePath,
lintUnavailableHelpPagePath: mockLintUnavailableHelpPagePath,
},
propsData: {
ciConfig: mergeUnwrappedCiConfig(),
......@@ -149,4 +156,28 @@ describe('Validation segment component', () => {
});
});
});
describe('when the lint service is unavailable', () => {
beforeEach(() => {
createComponent({
appStatus: EDITOR_APP_STATUS_LINT_UNAVAILABLE,
props: {
ciConfig: {},
},
});
});
it('show a message that the service is unavailable', () => {
expect(findValidationMsg().text()).toBe(i18n.unavailableValidation);
});
it('shows the time-out icon', () => {
expect(findIcon().props('name')).toBe('time-out');
});
it('shows the learn more link', () => {
expect(findLearnMoreLink().attributes('href')).toBe(mockLintUnavailableHelpPagePath);
expect(findLearnMoreLink().text()).toBe(i18n.learnMore);
});
});
});
......@@ -75,34 +75,83 @@ describe('~/pipeline_editor/components/ui/editor_tab.vue', () => {
expect(mockChildMounted).toHaveBeenCalledWith(mockContent1);
});
describe('showing the tab content depending on `isEmpty` and `isInvalid`', () => {
describe('alerts', () => {
describe('unavailable state', () => {
beforeEach(() => {
createWrapper({ props: { isUnavailable: true } });
});
it('shows the invalid alert when the status is invalid', () => {
const alert = findAlert();
expect(alert.exists()).toBe(true);
expect(alert.text()).toContain(wrapper.vm.$options.i18n.unavailable);
});
});
describe('invalid state', () => {
beforeEach(() => {
createWrapper({ props: { isInvalid: true } });
});
it('shows the invalid alert when the status is invalid', () => {
const alert = findAlert();
expect(alert.exists()).toBe(true);
expect(alert.text()).toBe(wrapper.vm.$options.i18n.invalid);
});
});
describe('empty state', () => {
const text = 'my custom alert message';
beforeEach(() => {
createWrapper({
props: { isEmpty: true, emptyMessage: text },
});
});
it('displays an empty message', () => {
createWrapper({
props: { isEmpty: true },
});
const alert = findAlert();
expect(alert.exists()).toBe(true);
expect(alert.text()).toBe(
'This tab will be usable when the CI/CD configuration file is populated with valid syntax.',
);
});
it('can have a custom empty message', () => {
const alert = findAlert();
expect(alert.exists()).toBe(true);
expect(alert.text()).toBe(text);
});
});
});
describe('showing the tab content depending on `isEmpty`, `isUnavailable` and `isInvalid`', () => {
it.each`
isEmpty | isInvalid | showSlotComponent | text
${undefined} | ${undefined} | ${true} | ${'renders'}
${false} | ${false} | ${true} | ${'renders'}
${undefined} | ${true} | ${false} | ${'hides'}
${true} | ${false} | ${false} | ${'hides'}
${false} | ${true} | ${false} | ${'hides'}
isEmpty | isUnavailable | isInvalid | showSlotComponent | text
${undefined} | ${undefined} | ${undefined} | ${true} | ${'renders'}
${false} | ${false} | ${false} | ${true} | ${'renders'}
${undefined} | ${true} | ${true} | ${false} | ${'hides'}
${true} | ${false} | ${false} | ${false} | ${'hides'}
${false} | ${true} | ${false} | ${false} | ${'hides'}
${false} | ${false} | ${true} | ${false} | ${'hides'}
`(
'$text the slot component when isEmpty:$isEmpty and isInvalid:$isInvalid',
({ isEmpty, isInvalid, showSlotComponent }) => {
'$text the slot component when isEmpty:$isEmpty, isUnavailable:$isUnavailable and isInvalid:$isInvalid',
({ isEmpty, isUnavailable, isInvalid, showSlotComponent }) => {
createWrapper({
props: { isEmpty, isInvalid },
props: { isEmpty, isUnavailable, isInvalid },
});
expect(findSlotComponent().exists()).toBe(showSlotComponent);
expect(findAlert().exists()).toBe(!showSlotComponent);
},
);
it('can have a custom empty message', () => {
const text = 'my custom alert message';
createWrapper({ props: { isEmpty: true, emptyMessage: text } });
const alert = findAlert();
expect(alert.exists()).toBe(true);
expect(alert.text()).toBe(text);
});
});
describe('user interaction', () => {
......
......@@ -10,6 +10,7 @@ export const mockNewMergeRequestPath = '/-/merge_requests/new';
export const mockCommitSha = 'aabbccdd';
export const mockCommitNextSha = 'eeffgghh';
export const mockLintHelpPagePath = '/-/lint-help';
export const mockLintUnavailableHelpPagePath = '/-/pipeline-editor/troubleshoot';
export const mockYmlHelpPagePath = '/-/yml-help';
export const mockCommitMessage = 'My commit message';
......
......@@ -5,10 +5,15 @@ import createMockApollo from 'helpers/mock_apollo_helper';
import setWindowLocation from 'helpers/set_window_location_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { resolvers } from '~/pipeline_editor/graphql/resolvers';
import PipelineEditorTabs from '~/pipeline_editor/components/pipeline_editor_tabs.vue';
import PipelineEditorEmptyState from '~/pipeline_editor/components/ui/pipeline_editor_empty_state.vue';
import PipelineEditorMessages from '~/pipeline_editor/components/ui/pipeline_editor_messages.vue';
import { COMMIT_SUCCESS, COMMIT_FAILURE, LOAD_FAILURE_UNKNOWN } from '~/pipeline_editor/constants';
import PipelineEditorHeader from '~/pipeline_editor/components/header/pipeline_editor_header.vue';
import ValidationSegment, {
i18n as validationSegmenti18n,
} from '~/pipeline_editor/components/header/validation_segment.vue';
import { COMMIT_SUCCESS, COMMIT_FAILURE } from '~/pipeline_editor/constants';
import getBlobContent from '~/pipeline_editor/graphql/queries/blob_content.query.graphql';
import getCiConfigData from '~/pipeline_editor/graphql/queries/ci_config.query.graphql';
import getTemplate from '~/pipeline_editor/graphql/queries/get_starter_template.query.graphql';
......@@ -61,11 +66,6 @@ describe('Pipeline editor app component', () => {
wrapper = shallowMount(PipelineEditorApp, {
provide: { ...mockProvide, ...provide },
stubs,
data() {
return {
commitSha: '',
};
},
mocks: {
$apollo: {
queries: {
......@@ -90,17 +90,11 @@ describe('Pipeline editor app component', () => {
[getLatestCommitShaQuery, mockLatestCommitShaQuery],
[getPipelineQuery, mockPipelineQuery],
];
mockApollo = createMockApollo(handlers);
mockApollo = createMockApollo(handlers, resolvers);
const options = {
localVue,
data() {
return {
currentBranch: mockDefaultBranch,
lastCommitBranch: '',
appStatus: '',
};
},
mocks: {},
apolloProvider: mockApollo,
};
......@@ -116,6 +110,7 @@ describe('Pipeline editor app component', () => {
const findEmptyState = () => wrapper.findComponent(PipelineEditorEmptyState);
const findEmptyStateButton = () =>
wrapper.findComponent(PipelineEditorEmptyState).findComponent(GlButton);
const findValidationSegment = () => wrapper.findComponent(ValidationSegment);
beforeEach(() => {
mockBlobContentData = jest.fn();
......@@ -240,6 +235,26 @@ describe('Pipeline editor app component', () => {
});
});
describe('when the lint query returns a 500 error', () => {
beforeEach(async () => {
mockCiConfigData.mockRejectedValueOnce(new Error(500));
await createComponentWithApollo({
stubs: { PipelineEditorHome, PipelineEditorHeader, ValidationSegment },
});
});
it('shows that the lint service is down', () => {
expect(findValidationSegment().text()).toContain(
validationSegmenti18n.unavailableValidation,
);
});
it('does not report an error or scroll to the top', () => {
expect(findAlert().exists()).toBe(false);
expect(window.scrollTo).not.toHaveBeenCalled();
});
});
describe('when the user commits', () => {
const updateFailureMessage = 'The GitLab CI configuration could not be updated.';
const updateSuccessMessage = 'Your changes have been successfully committed.';
......@@ -411,94 +426,6 @@ describe('Pipeline editor app component', () => {
});
});
describe('when multiple errors occurs in a row', () => {
const updateFailureMessage = 'The GitLab CI configuration could not be updated.';
const unknownFailureMessage = 'The CI configuration was not loaded, please try again.';
const unknownReasons = ['Commit failed'];
const alertErrorMessage = `${updateFailureMessage} ${unknownReasons[0]}`;
const emitError = (type = COMMIT_FAILURE, reasons = unknownReasons) =>
findEditorHome().vm.$emit('showError', {
type,
reasons,
});
beforeEach(async () => {
mockBlobContentData.mockResolvedValue(mockBlobContentQueryResponse);
mockCiConfigData.mockResolvedValue(mockCiConfigQueryResponse);
mockLatestCommitShaQuery.mockResolvedValue(mockCommitShaResults);
window.scrollTo = jest.fn();
await createComponentWithApollo({ stubs: { PipelineEditorMessages } });
await emitError();
});
it('shows an error message for the first error', () => {
expect(findAlert().text()).toMatchInterpolatedText(alertErrorMessage);
});
it('scrolls to the top of the page to bring attention to the error message', () => {
expect(window.scrollTo).toHaveBeenCalledWith({ top: 0, behavior: 'smooth' });
expect(window.scrollTo).toHaveBeenCalledTimes(1);
});
it('does not scroll to the top of the page if the same error occur multiple times in a row', async () => {
await emitError();
expect(window.scrollTo).toHaveBeenCalledTimes(1);
expect(findAlert().text()).toMatchInterpolatedText(alertErrorMessage);
});
it('scrolls to the top if the error is different', async () => {
await emitError(LOAD_FAILURE_UNKNOWN, []);
expect(findAlert().text()).toMatchInterpolatedText(unknownFailureMessage);
expect(window.scrollTo).toHaveBeenCalledTimes(2);
});
describe('when a user dismiss the alert', () => {
beforeEach(async () => {
await findAlert().vm.$emit('dismiss');
});
it('shows an error if the type is the same, but the reason is different', async () => {
const newReason = 'Something broke';
await emitError(COMMIT_FAILURE, [newReason]);
expect(window.scrollTo).toHaveBeenCalledTimes(2);
expect(findAlert().text()).toMatchInterpolatedText(`${updateFailureMessage} ${newReason}`);
});
it('does not show an error or scroll if a new error with the same type occurs', async () => {
await emitError();
expect(window.scrollTo).toHaveBeenCalledTimes(1);
expect(findAlert().exists()).toBe(false);
});
it('it shows an error and scroll when a new type is emitted', async () => {
await emitError(LOAD_FAILURE_UNKNOWN, []);
expect(window.scrollTo).toHaveBeenCalledTimes(2);
expect(findAlert().text()).toMatchInterpolatedText(unknownFailureMessage);
});
it('it shows an error and scroll if a previously shown type happen again', async () => {
await emitError(LOAD_FAILURE_UNKNOWN, []);
expect(window.scrollTo).toHaveBeenCalledTimes(2);
expect(findAlert().text()).toMatchInterpolatedText(unknownFailureMessage);
await emitError();
expect(window.scrollTo).toHaveBeenCalledTimes(3);
expect(findAlert().text()).toMatchInterpolatedText(alertErrorMessage);
});
});
});
describe('when add_new_config_file query param is present', () => {
const originalLocation = window.location.href;
......
......@@ -46,6 +46,7 @@ RSpec.describe Ci::PipelineEditorHelper do
"empty-state-illustration-path" => 'foo',
"initial-branch-name" => nil,
"lint-help-page-path" => help_page_path('ci/lint', anchor: 'validate-basic-logic-and-syntax'),
"lint-unavailable-help-page-path" => help_page_path('ci/pipeline_editor/index', anchor: 'configuration-validation-currently-not-available'),
"needs-help-page-path" => help_page_path('ci/yaml/index', anchor: 'needs'),
"new-merge-request-path" => '/mock/project/-/merge_requests/new',
"pipeline_etag" => graphql_etag_pipeline_sha_path(project.commit.sha),
......@@ -72,6 +73,7 @@ RSpec.describe Ci::PipelineEditorHelper do
"empty-state-illustration-path" => 'foo',
"initial-branch-name" => nil,
"lint-help-page-path" => help_page_path('ci/lint', anchor: 'validate-basic-logic-and-syntax'),
"lint-unavailable-help-page-path" => help_page_path('ci/pipeline_editor/index', anchor: 'configuration-validation-currently-not-available'),
"needs-help-page-path" => help_page_path('ci/yaml/index', anchor: 'needs'),
"new-merge-request-path" => '/mock/project/-/merge_requests/new',
"pipeline_etag" => '',
......
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