Commit 96dc583b authored by Rémy Coutable's avatar Rémy Coutable

Merge branch 'code-quality-walkthrough' into 'master'

Experiment: Code quality walkthrough [RUN ALL RSPEC] [RUN AS-IF-FOSS]

See merge request gitlab-org/gitlab!58900
parents fc930922 d136dd27
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
import $ from 'jquery'; import $ from 'jquery';
import initPopover from '~/blob/suggest_gitlab_ci_yml'; import initPopover from '~/blob/suggest_gitlab_ci_yml';
import initCodeQualityWalkthrough from '~/code_quality_walkthrough';
import { deprecatedCreateFlash as createFlash } from '~/flash'; import { deprecatedCreateFlash as createFlash } from '~/flash';
import { disableButtonIfEmptyField, setCookie } from '~/lib/utils/common_utils'; import { disableButtonIfEmptyField, setCookie } from '~/lib/utils/common_utils';
import Tracking from '~/tracking'; import Tracking from '~/tracking';
...@@ -38,6 +39,13 @@ const initPopovers = () => { ...@@ -38,6 +39,13 @@ const initPopovers = () => {
} }
}; };
const initCodeQualityWalkthroughStep = () => {
const codeQualityWalkthroughEl = document.querySelector('.js-code-quality-walkthrough');
if (codeQualityWalkthroughEl) {
initCodeQualityWalkthrough(codeQualityWalkthroughEl);
}
};
export const initUploadForm = () => { export const initUploadForm = () => {
const uploadBlobForm = $('.js-upload-blob-form'); const uploadBlobForm = $('.js-upload-blob-form');
if (uploadBlobForm.length) { if (uploadBlobForm.length) {
...@@ -74,6 +82,7 @@ export default () => { ...@@ -74,6 +82,7 @@ export default () => {
isMarkdown, isMarkdown,
}); });
initPopovers(); initPopovers();
initCodeQualityWalkthroughStep();
}) })
.catch((e) => createFlash(e)); .catch((e) => createFlash(e));
......
<script>
import { GlPopover, GlSprintf, GlButton, GlAlert } from '@gitlab/ui';
import { STEPS, STEPSTATES } from '../constants';
import {
isWalkthroughEnabled,
getExperimentSettings,
setExperimentSettings,
track,
} from '../utils';
export default {
target: '#js-code-quality-walkthrough',
components: {
GlPopover,
GlSprintf,
GlButton,
GlAlert,
},
props: {
step: {
type: String,
required: true,
},
link: {
type: String,
required: false,
default: null,
},
},
data() {
return {
dismissedSettings: getExperimentSettings(),
currentStep: STEPSTATES[this.step],
};
},
computed: {
isPopoverVisible() {
return (
[
STEPS.commitCiFile,
STEPS.runningPipeline,
STEPS.successPipeline,
STEPS.failedPipeline,
].includes(this.step) &&
isWalkthroughEnabled() &&
!this.isDismissed
);
},
isAlertVisible() {
return this.step === STEPS.troubleshootJob && isWalkthroughEnabled() && !this.isDismissed;
},
isDismissed() {
return this.dismissedSettings[this.step];
},
title() {
return this.currentStep?.title || '';
},
body() {
return this.currentStep?.body || '';
},
buttonText() {
return this.currentStep?.buttonText || '';
},
buttonLink() {
return [STEPS.successPipeline, STEPS.failedPipeline].includes(this.step) ? this.link : '';
},
placement() {
return this.currentStep?.placement || 'bottom';
},
offset() {
return this.currentStep?.offset || 0;
},
},
created() {
this.trackDisplayed();
},
updated() {
this.trackDisplayed();
},
methods: {
onDismiss() {
this.$set(this.dismissedSettings, this.step, true);
setExperimentSettings(this.dismissedSettings);
const action = [STEPS.successPipeline, STEPS.failedPipeline].includes(this.step)
? 'view_logs'
: 'dismissed';
this.trackAction(action);
},
trackDisplayed() {
if (this.isPopoverVisible || this.isAlertVisible) {
this.trackAction('displayed');
}
},
trackAction(action) {
track(`${this.step}_${action}`);
},
},
};
</script>
<template>
<div>
<gl-popover
v-if="isPopoverVisible"
:key="step"
:target="$options.target"
:placement="placement"
:offset="offset"
show
triggers="manual"
container="viewport"
>
<template #title>
<gl-sprintf :message="title">
<template #emoji="{ content }">
<gl-emoji class="gl-mr-2" :data-name="content"
/></template>
</gl-sprintf>
</template>
<gl-sprintf :message="body">
<template #strong="{ content }">
<strong>{{ content }}</strong>
</template>
<template #lineBreak>
<div class="gl-mt-5"></div>
</template>
<template #emoji="{ content }">
<gl-emoji :data-name="content" />
</template>
</gl-sprintf>
<div class="gl-mt-2 gl-text-right">
<gl-button category="tertiary" variant="link" :href="buttonLink" @click="onDismiss">
{{ buttonText }}
</gl-button>
</div>
</gl-popover>
<gl-alert
v-if="isAlertVisible"
variant="tip"
:title="title"
:primary-button-text="buttonText"
:primary-button-link="link"
class="gl-my-5"
@primaryAction="trackAction('clicked')"
@dismiss="onDismiss"
>
{{ body }}
</gl-alert>
</div>
</template>
import { s__ } from '~/locale';
export const EXPERIMENT_NAME = 'code_quality_walkthrough';
export const STEPS = {
commitCiFile: 'commit_ci_file',
runningPipeline: 'running_pipeline',
successPipeline: 'success_pipeline',
failedPipeline: 'failed_pipeline',
troubleshootJob: 'troubleshoot_job',
};
export const STEPSTATES = {
[STEPS.commitCiFile]: {
title: s__("codeQualityWalkthrough|Let's start by creating a new CI file."),
body: s__(
'codeQualityWalkthrough|To begin with code quality, we first need to create a new CI file using our code editor. We added a code quality template in the code editor to help you get started %{emojiStart}wink%{emojiEnd} .%{lineBreak}Take some time to review the template, when you are ready, use the %{strongStart}commit changes%{strongEnd} button at the bottom of the page.',
),
buttonText: s__('codeQualityWalkthrough|Got it'),
placement: 'right',
offset: 90,
},
[STEPS.runningPipeline]: {
title: s__(
'codeQualityWalkthrough|Congrats! Your first pipeline is running %{emojiStart}zap%{emojiEnd}',
),
body: s__(
"codeQualityWalkthrough|Your pipeline can take a few minutes to run. If you enabled email notifications, you'll receive an email with your pipeline status. In the meantime, why don't you get some coffee? You earned it!",
),
buttonText: s__('codeQualityWalkthrough|Got it'),
offset: 97,
},
[STEPS.successPipeline]: {
title: s__(
"codeQualityWalkthrough|Well done! You've just automated your code quality review. %{emojiStart}raised_hands%{emojiEnd}",
),
body: s__(
'codeQualityWalkthrough|A code quality job will now run every time you or your team members commit changes to your project. You can view the results of the code quality job in the job logs.',
),
buttonText: s__('codeQualityWalkthrough|View the logs'),
offset: 98,
},
[STEPS.failedPipeline]: {
title: s__(
"codeQualityWalkthrough|Something went wrong. %{emojiStart}thinking%{emojiEnd} Let's fix it.",
),
body: s__(
"codeQualityWalkthrough|Your job failed. No worries - this happens. Let's view the logs, and see how we can fix it.",
),
buttonText: s__('codeQualityWalkthrough|View the logs'),
offset: 98,
},
[STEPS.troubleshootJob]: {
title: s__('codeQualityWalkthrough|Troubleshoot your code quality job'),
body: s__(
'codeQualityWalkthrough|Not sure how to fix your failed job? We have compiled some tips on how to troubleshoot code quality jobs in the documentation.',
),
buttonText: s__('codeQualityWalkthrough|Read the documentation'),
},
};
export const PIPELINE_STATUSES = {
running: 'running',
successWithWarnings: 'success-with-warnings',
success: 'success',
failed: 'failed',
};
import Vue from 'vue';
import Step from './components/step.vue';
export default (el) =>
new Vue({
el,
render(createElement) {
return createElement(Step, {
props: {
step: el.dataset.step,
},
});
},
});
import { TRACKING_CONTEXT_SCHEMA } from '~/experimentation/constants';
import { getExperimentData } from '~/experimentation/utils';
import { setCookie, getCookie, getParameterByName } from '~/lib/utils/common_utils';
import Tracking from '~/tracking';
import { EXPERIMENT_NAME } from './constants';
export function getExperimentSettings() {
return JSON.parse(getCookie(EXPERIMENT_NAME) || '{}');
}
export function setExperimentSettings(settings) {
setCookie(EXPERIMENT_NAME, settings);
}
export function isWalkthroughEnabled() {
return getParameterByName(EXPERIMENT_NAME);
}
export function track(action) {
const { data } = getExperimentSettings();
if (data) {
Tracking.event(EXPERIMENT_NAME, action, {
context: {
schema: TRACKING_CONTEXT_SCHEMA,
data,
},
});
}
}
export function startCodeQualityWalkthrough() {
const data = getExperimentData(EXPERIMENT_NAME);
if (data) {
setExperimentSettings({ data });
}
}
...@@ -3,6 +3,7 @@ import { GlLoadingIcon, GlIcon, GlSafeHtmlDirective as SafeHtml, GlAlert } from ...@@ -3,6 +3,7 @@ import { GlLoadingIcon, GlIcon, GlSafeHtmlDirective as SafeHtml, GlAlert } from
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils'; import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
import { throttle, isEmpty } from 'lodash'; import { throttle, isEmpty } from 'lodash';
import { mapGetters, mapState, mapActions } from 'vuex'; import { mapGetters, mapState, mapActions } from 'vuex';
import CodeQualityWalkthrough from '~/code_quality_walkthrough/components/step.vue';
import { isScrolledToBottom } from '~/lib/utils/scroll_utils'; import { isScrolledToBottom } from '~/lib/utils/scroll_utils';
import { sprintf } from '~/locale'; import { sprintf } from '~/locale';
import CiHeader from '~/vue_shared/components/header_ci_component.vue'; import CiHeader from '~/vue_shared/components/header_ci_component.vue';
...@@ -32,6 +33,7 @@ export default { ...@@ -32,6 +33,7 @@ export default {
GlLoadingIcon, GlLoadingIcon,
SharedRunner: () => import('ee_component/jobs/components/shared_runner_limit_block.vue'), SharedRunner: () => import('ee_component/jobs/components/shared_runner_limit_block.vue'),
GlAlert, GlAlert,
CodeQualityWalkthrough,
}, },
directives: { directives: {
SafeHtml, SafeHtml,
...@@ -72,6 +74,11 @@ export default { ...@@ -72,6 +74,11 @@ export default {
required: false, required: false,
default: null, default: null,
}, },
codeQualityHelpUrl: {
type: String,
required: false,
default: null,
},
}, },
computed: { computed: {
...mapState([ ...mapState([
...@@ -120,6 +127,10 @@ export default { ...@@ -120,6 +127,10 @@ export default {
shouldRenderHeaderCallout() { shouldRenderHeaderCallout() {
return this.shouldRenderCalloutMessage && !this.hasUnmetPrerequisitesFailure; return this.shouldRenderCalloutMessage && !this.hasUnmetPrerequisitesFailure;
}, },
shouldRenderCodeQualityWalkthrough() {
return this.job.status.group === 'failed-with-warnings';
},
}, },
watch: { watch: {
// Once the job log is loaded, // Once the job log is loaded,
...@@ -216,6 +227,11 @@ export default { ...@@ -216,6 +227,11 @@ export default {
> >
<div v-safe-html="job.callout_message"></div> <div v-safe-html="job.callout_message"></div>
</gl-alert> </gl-alert>
<code-quality-walkthrough
v-if="shouldRenderCodeQualityWalkthrough"
step="troubleshoot_job"
:link="codeQualityHelpUrl"
/>
</header> </header>
<!-- EO Header Section --> <!-- EO Header Section -->
......
...@@ -13,6 +13,7 @@ export default () => { ...@@ -13,6 +13,7 @@ export default () => {
const { const {
artifactHelpUrl, artifactHelpUrl,
deploymentHelpUrl, deploymentHelpUrl,
codeQualityHelpUrl,
runnerSettingsUrl, runnerSettingsUrl,
variablesSettingsUrl, variablesSettingsUrl,
subscriptionsMoreMinutesUrl, subscriptionsMoreMinutesUrl,
...@@ -38,6 +39,7 @@ export default () => { ...@@ -38,6 +39,7 @@ export default () => {
props: { props: {
artifactHelpUrl, artifactHelpUrl,
deploymentHelpUrl, deploymentHelpUrl,
codeQualityHelpUrl,
runnerSettingsUrl, runnerSettingsUrl,
variablesSettingsUrl, variablesSettingsUrl,
subscriptionsMoreMinutesUrl, subscriptionsMoreMinutesUrl,
......
<script> <script>
import { GlEmptyState } from '@gitlab/ui'; import { GlEmptyState, GlButton } from '@gitlab/ui';
import { startCodeQualityWalkthrough, track } from '~/code_quality_walkthrough/utils';
import GitlabExperiment from '~/experimentation/components/gitlab_experiment.vue'; import GitlabExperiment from '~/experimentation/components/gitlab_experiment.vue';
import { getExperimentData } from '~/experimentation/utils';
import { helpPagePath } from '~/helpers/help_page_helper'; import { helpPagePath } from '~/helpers/help_page_helper';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
import PipelinesCiTemplates from './pipelines_ci_templates.vue'; import PipelinesCiTemplates from './pipelines_ci_templates.vue';
...@@ -12,11 +14,17 @@ export default { ...@@ -12,11 +14,17 @@ export default {
test, and deploy your code. Let GitLab take care of time test, and deploy your code. Let GitLab take care of time
consuming tasks, so you can spend more time creating.`), consuming tasks, so you can spend more time creating.`),
btnText: s__('Pipelines|Get started with CI/CD'), btnText: s__('Pipelines|Get started with CI/CD'),
codeQualityTitle: s__('Pipelines|Improve code quality with GitLab CI/CD'),
codeQualityDescription: s__(`Pipelines|To keep your codebase simple,
readable, and accessible to contributors, use GitLab CI/CD
to analyze your code quality with every push to your project.`),
codeQualityBtnText: s__('Pipelines|Add a code quality job'),
noCiDescription: s__('Pipelines|This project is not currently set up to run pipelines.'), noCiDescription: s__('Pipelines|This project is not currently set up to run pipelines.'),
}, },
name: 'PipelinesEmptyState', name: 'PipelinesEmptyState',
components: { components: {
GlEmptyState, GlEmptyState,
GlButton,
GitlabExperiment, GitlabExperiment,
PipelinesCiTemplates, PipelinesCiTemplates,
}, },
...@@ -29,36 +37,82 @@ export default { ...@@ -29,36 +37,82 @@ export default {
type: Boolean, type: Boolean,
required: true, required: true,
}, },
codeQualityPagePath: {
type: String,
required: false,
default: null,
},
}, },
computed: { computed: {
ciHelpPagePath() { ciHelpPagePath() {
return helpPagePath('ci/quick_start/index.md'); return helpPagePath('ci/quick_start/index.md');
}, },
isPipelineEmptyStateTemplatesExperimentActive() {
return this.canSetCi && Boolean(getExperimentData('pipeline_empty_state_templates'));
},
},
mounted() {
startCodeQualityWalkthrough();
},
methods: {
trackClick() {
track('cta_clicked');
},
}, },
}; };
</script> </script>
<template> <template>
<div> <div>
<gitlab-experiment name="pipeline_empty_state_templates"> <gitlab-experiment
v-if="isPipelineEmptyStateTemplatesExperimentActive"
name="pipeline_empty_state_templates"
>
<template #control> <template #control>
<gl-empty-state <gl-empty-state
v-if="canSetCi"
:title="$options.i18n.title" :title="$options.i18n.title"
:svg-path="emptyStateSvgPath" :svg-path="emptyStateSvgPath"
:description="$options.i18n.description" :description="$options.i18n.description"
:primary-button-text="$options.i18n.btnText" :primary-button-text="$options.i18n.btnText"
:primary-button-link="ciHelpPagePath" :primary-button-link="ciHelpPagePath"
/> />
</template>
<template #candidate>
<pipelines-ci-templates />
</template>
</gitlab-experiment>
<gitlab-experiment v-else-if="canSetCi" name="code_quality_walkthrough">
<template #control>
<gl-empty-state <gl-empty-state
v-else :title="$options.i18n.title"
title=""
:svg-path="emptyStateSvgPath" :svg-path="emptyStateSvgPath"
:description="$options.i18n.noCiDescription" :description="$options.i18n.description"
/> >
<template #actions>
<gl-button :href="ciHelpPagePath" variant="confirm" @click="trackClick()">
{{ $options.i18n.btnText }}
</gl-button>
</template>
</gl-empty-state>
</template> </template>
<template #candidate> <template #candidate>
<pipelines-ci-templates /> <gl-empty-state
:title="$options.i18n.codeQualityTitle"
:svg-path="emptyStateSvgPath"
:description="$options.i18n.codeQualityDescription"
>
<template #actions>
<gl-button :href="codeQualityPagePath" variant="confirm" @click="trackClick()">
{{ $options.i18n.codeQualityBtnText }}
</gl-button>
</template>
</gl-empty-state>
</template> </template>
</gitlab-experiment> </gitlab-experiment>
<gl-empty-state
v-else
title=""
:svg-path="emptyStateSvgPath"
:description="$options.i18n.noCiDescription"
/>
</div> </div>
</template> </template>
...@@ -94,6 +94,11 @@ export default { ...@@ -94,6 +94,11 @@ export default {
type: Object, type: Object,
required: true, required: true,
}, },
codeQualityPagePath: {
type: String,
required: false,
default: null,
},
}, },
data() { data() {
return { return {
...@@ -331,6 +336,7 @@ export default { ...@@ -331,6 +336,7 @@ export default {
v-else-if="stateToRender === $options.stateMap.emptyState" v-else-if="stateToRender === $options.stateMap.emptyState"
:empty-state-svg-path="emptyStateSvgPath" :empty-state-svg-path="emptyStateSvgPath"
:can-set-ci="canCreatePipeline" :can-set-ci="canCreatePipeline"
:code-quality-page-path="codeQualityPagePath"
/> />
<gl-empty-state <gl-empty-state
......
<script> <script>
import CodeQualityWalkthrough from '~/code_quality_walkthrough/components/step.vue';
import { PIPELINE_STATUSES } from '~/code_quality_walkthrough/constants';
import { CHILD_VIEW } from '~/pipelines/constants'; import { CHILD_VIEW } from '~/pipelines/constants';
import CiBadge from '~/vue_shared/components/ci_badge_link.vue'; import CiBadge from '~/vue_shared/components/ci_badge_link.vue';
export default { export default {
components: { components: {
CodeQualityWalkthrough,
CiBadge, CiBadge,
}, },
props: { props: {
...@@ -23,15 +26,37 @@ export default { ...@@ -23,15 +26,37 @@ export default {
isChildView() { isChildView() {
return this.viewType === CHILD_VIEW; return this.viewType === CHILD_VIEW;
}, },
shouldRenderCodeQualityWalkthrough() {
return Object.values(PIPELINE_STATUSES).includes(this.pipelineStatus.group);
},
codeQualityStep() {
const prefix = [PIPELINE_STATUSES.successWithWarnings, PIPELINE_STATUSES.failed].includes(
this.pipelineStatus.group,
)
? 'failed'
: this.pipelineStatus.group;
return `${prefix}_pipeline`;
},
codeQualityBuildPath() {
return this.pipeline?.details?.code_quality_build_path;
},
}, },
}; };
</script> </script>
<template> <template>
<div>
<ci-badge <ci-badge
id="js-code-quality-walkthrough"
:status="pipelineStatus" :status="pipelineStatus"
:show-text="!isChildView" :show-text="!isChildView"
:icon-classes="'gl-vertical-align-middle!'" :icon-classes="'gl-vertical-align-middle!'"
data-qa-selector="pipeline_commit_status" data-qa-selector="pipeline_commit_status"
/> />
<code-quality-walkthrough
v-if="shouldRenderCodeQualityWalkthrough"
:step="codeQualityStep"
:link="codeQualityBuildPath"
/>
</div>
</template> </template>
...@@ -37,6 +37,7 @@ export const initPipelinesIndex = (selector = '#pipelines-list-vue') => { ...@@ -37,6 +37,7 @@ export const initPipelinesIndex = (selector = '#pipelines-list-vue') => {
resetCachePath, resetCachePath,
projectId, projectId,
params, params,
codeQualityPagePath,
} = el.dataset; } = el.dataset;
return new Vue({ return new Vue({
...@@ -74,6 +75,7 @@ export const initPipelinesIndex = (selector = '#pipelines-list-vue') => { ...@@ -74,6 +75,7 @@ export const initPipelinesIndex = (selector = '#pipelines-list-vue') => {
resetCachePath, resetCachePath,
projectId, projectId,
params: JSON.parse(params), params: JSON.parse(params),
codeQualityPagePath,
}, },
}); });
}, },
......
...@@ -31,6 +31,7 @@ class Projects::BlobController < Projects::ApplicationController ...@@ -31,6 +31,7 @@ class Projects::BlobController < Projects::ApplicationController
before_action :editor_variables, except: [:show, :preview, :diff] before_action :editor_variables, except: [:show, :preview, :diff]
before_action :validate_diff_params, only: :diff before_action :validate_diff_params, only: :diff
before_action :set_last_commit_sha, only: [:edit, :update] before_action :set_last_commit_sha, only: [:edit, :update]
before_action :track_experiment, only: :create
track_redis_hll_event :create, :update, name: 'g_edit_by_sfe' track_redis_hll_event :create, :update, name: 'g_edit_by_sfe'
...@@ -46,7 +47,7 @@ class Projects::BlobController < Projects::ApplicationController ...@@ -46,7 +47,7 @@ class Projects::BlobController < Projects::ApplicationController
def create def create
create_commit(Files::CreateService, success_notice: _("The file has been successfully created."), create_commit(Files::CreateService, success_notice: _("The file has been successfully created."),
success_path: -> { project_blob_path(@project, File.join(@branch_name, @file_path)) }, success_path: -> { create_success_path },
failure_view: :new, failure_view: :new,
failure_path: project_new_blob_path(@project, @ref)) failure_path: project_new_blob_path(@project, @ref))
end end
...@@ -264,4 +265,18 @@ class Projects::BlobController < Projects::ApplicationController ...@@ -264,4 +265,18 @@ class Projects::BlobController < Projects::ApplicationController
def visitor_id def visitor_id
current_user&.id current_user&.id
end end
def create_success_path
if params[:code_quality_walkthrough]
project_pipelines_path(@project, code_quality_walkthrough: true)
else
project_blob_path(@project, File.join(@branch_name, @file_path))
end
end
def track_experiment
return unless params[:code_quality_walkthrough]
experiment(:code_quality_walkthrough, namespace: @project.root_ancestor).track(:commit_created)
end
end end
...@@ -59,6 +59,17 @@ class Projects::PipelinesController < Projects::ApplicationController ...@@ -59,6 +59,17 @@ class Projects::PipelinesController < Projects::ApplicationController
e.try {} e.try {}
e.track(:view, value: project.namespace_id) e.track(:view, value: project.namespace_id)
end end
experiment(:code_quality_walkthrough, namespace: project.root_ancestor) do |e|
e.exclude! unless current_user
e.exclude! unless can?(current_user, :create_pipeline, project)
e.exclude! unless project.root_ancestor.recent?
e.exclude! if @pipelines_count.to_i > 0
e.exclude! if helpers.has_gitlab_ci?(project)
e.use {}
e.try {}
e.track(:view, property: project.root_ancestor.id.to_s)
end
end end
format.json do format.json do
Gitlab::PollingInterval.set_header(response, interval: POLLING_INTERVAL) Gitlab::PollingInterval.set_header(response, interval: POLLING_INTERVAL)
...@@ -223,7 +234,7 @@ class Projects::PipelinesController < Projects::ApplicationController ...@@ -223,7 +234,7 @@ class Projects::PipelinesController < Projects::ApplicationController
PipelineSerializer PipelineSerializer
.new(project: @project, current_user: @current_user) .new(project: @project, current_user: @current_user)
.with_pagination(request, response) .with_pagination(request, response)
.represent(@pipelines, disable_coverage: true, preload: true) .represent(@pipelines, disable_coverage: true, preload: true, code_quality_walkthrough: params[:code_quality_walkthrough].present?)
end end
def render_show def render_show
......
...@@ -15,7 +15,8 @@ module Ci ...@@ -15,7 +15,8 @@ module Ci
"build_stage" => @build.stage, "build_stage" => @build.stage,
"log_state" => '', "log_state" => '',
"build_options" => javascript_build_options, "build_options" => javascript_build_options,
"retry_outdated_job_docs_url" => help_page_path('ci/pipelines/settings', anchor: 'retry-outdated-jobs') "retry_outdated_job_docs_url" => help_page_path('ci/pipelines/settings', anchor: 'retry-outdated-jobs'),
"code_quality_help_url" => help_page_path('user/project/merge_requests/code_quality', anchor: 'troubleshooting')
} }
end end
......
...@@ -139,6 +139,17 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated ...@@ -139,6 +139,17 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
add_special_file_path(file_name: ci_config_path_or_default) add_special_file_path(file_name: ci_config_path_or_default)
end end
def add_code_quality_ci_yml_path
add_special_file_path(
file_name: ci_config_path_or_default,
commit_message: s_("CommitMessage|Add %{file_name} and create a code quality job") % { file_name: ci_config_path_or_default },
additional_params: {
template: 'Code-Quality',
code_quality_walkthrough: true
}
)
end
def license_short_name def license_short_name
license = repository.license license = repository.license
license&.nickname || license&.name || 'LICENSE' license&.nickname || license&.name || 'LICENSE'
...@@ -468,14 +479,15 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated ...@@ -468,14 +479,15 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
end end
end end
def add_special_file_path(file_name:, commit_message: nil, branch_name: nil) def add_special_file_path(file_name:, commit_message: nil, branch_name: nil, additional_params: {})
commit_message ||= s_("CommitMessage|Add %{file_name}") % { file_name: file_name } commit_message ||= s_("CommitMessage|Add %{file_name}") % { file_name: file_name }
project_new_blob_path( project_new_blob_path(
project, project,
default_branch_or_main, default_branch_or_main,
file_name: file_name, file_name: file_name,
commit_message: commit_message, commit_message: commit_message,
branch_name: branch_name branch_name: branch_name,
**additional_params
) )
end end
end end
......
...@@ -10,6 +10,11 @@ class PipelineDetailsEntity < Ci::PipelineEntity ...@@ -10,6 +10,11 @@ class PipelineDetailsEntity < Ci::PipelineEntity
expose :details do expose :details do
expose :manual_actions, using: BuildActionEntity expose :manual_actions, using: BuildActionEntity
expose :scheduled_actions, using: BuildActionEntity expose :scheduled_actions, using: BuildActionEntity
expose :code_quality_build_path, if: -> (_, options) { options[:code_quality_walkthrough] } do |pipeline|
next unless code_quality_build = pipeline.builds.finished.find_by_name('code_quality')
project_job_path(pipeline.project, code_quality_build, code_quality_walkthrough: true)
end
end end
expose :triggered_by_pipeline, as: :triggered_by, with: TriggeredPipelineEntity expose :triggered_by_pipeline, as: :triggered_by, with: TriggeredPipelineEntity
......
- breadcrumb_title _("Repository") - breadcrumb_title _("Repository")
- page_title _("New File"), @path.presence, @ref - page_title _("New File"), @path.presence, @ref
%h3.page-title.blob-new-page-title %h3.page-title.blob-new-page-title#js-code-quality-walkthrough
New file = _('New file')
.js-code-quality-walkthrough{ data: { step: 'commit_ci_file' } }
.file-editor .file-editor
= form_tag(project_create_blob_path(@project, @id), method: :post, class: 'js-edit-blob-form js-new-blob-form js-quick-submit js-requires-input', data: blob_editor_paths(@project)) do = form_tag(project_create_blob_path(@project, @id), method: :post, class: 'js-edit-blob-form js-new-blob-form js-quick-submit js-requires-input', data: blob_editor_paths(@project)) do
= render 'projects/blob/editor', ref: @ref = render 'projects/blob/editor', ref: @ref
= render 'shared/new_commit_form', placeholder: "Add new file" = render 'shared/new_commit_form', placeholder: "Add new file"
- if params[:code_quality_walkthrough]
= hidden_field_tag 'code_quality_walkthrough', 'true'
= hidden_field_tag 'content', '', id: 'file-content' = hidden_field_tag 'content', '', id: 'file-content'
= render 'projects/commit_button', ref: @ref, = render 'projects/commit_button', ref: @ref,
cancel_path: project_tree_path(@project, @id) cancel_path: project_tree_path(@project, @id)
......
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
= render_if_exists "shared/shared_runners_minutes_limit_flash_message" = render_if_exists "shared/shared_runners_minutes_limit_flash_message"
#pipelines-list-vue{ data: { endpoint: project_pipelines_path(@project, format: :json), #pipelines-list-vue{ data: { endpoint: project_pipelines_path(@project, format: :json, code_quality_walkthrough: params[:code_quality_walkthrough]),
project_id: @project.id, project_id: @project.id,
params: params.to_json, params: params.to_json,
"artifacts-endpoint" => downloadable_artifacts_project_pipeline_path(@project, artifacts_endpoint_placeholder, format: :json), "artifacts-endpoint" => downloadable_artifacts_project_pipeline_path(@project, artifacts_endpoint_placeholder, format: :json),
...@@ -20,4 +20,5 @@ ...@@ -20,4 +20,5 @@
"reset-cache-path" => can?(current_user, :admin_pipeline, @project) && reset_cache_project_settings_ci_cd_path(@project), "reset-cache-path" => can?(current_user, :admin_pipeline, @project) && reset_cache_project_settings_ci_cd_path(@project),
"has-gitlab-ci" => has_gitlab_ci?(@project).to_s, "has-gitlab-ci" => has_gitlab_ci?(@project).to_s,
"add-ci-yml-path" => can?(current_user, :create_pipeline, @project) && @project.present(current_user: current_user).add_ci_yml_path, "add-ci-yml-path" => can?(current_user, :create_pipeline, @project) && @project.present(current_user: current_user).add_ci_yml_path,
"suggested-ci-templates" => experiment_suggested_ci_templates.to_json } } "suggested-ci-templates" => experiment_suggested_ci_templates.to_json,
"code-quality-page-path" => @project.present(current_user: current_user).add_code_quality_ci_yml_path } }
---
name: code_quality_walkthrough
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/58900
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/327229
milestone: "13.12"
type: experiment
group: group::activation
default_enabled: false
...@@ -8089,6 +8089,9 @@ msgstr "" ...@@ -8089,6 +8089,9 @@ msgstr ""
msgid "CommitMessage|Add %{file_name}" msgid "CommitMessage|Add %{file_name}"
msgstr "" msgstr ""
msgid "CommitMessage|Add %{file_name} and create a code quality job"
msgstr ""
msgid "CommitWidget|authored" msgid "CommitWidget|authored"
msgstr "" msgstr ""
...@@ -23980,6 +23983,9 @@ msgstr "" ...@@ -23980,6 +23983,9 @@ msgstr ""
msgid "Pipelines|API" msgid "Pipelines|API"
msgstr "" msgstr ""
msgid "Pipelines|Add a code quality job"
msgstr ""
msgid "Pipelines|Are you sure you want to run this pipeline?" msgid "Pipelines|Are you sure you want to run this pipeline?"
msgstr "" msgstr ""
...@@ -24037,6 +24043,9 @@ msgstr "" ...@@ -24037,6 +24043,9 @@ msgstr ""
msgid "Pipelines|If you are unsure, please ask a project maintainer to review it for you." msgid "Pipelines|If you are unsure, please ask a project maintainer to review it for you."
msgstr "" msgstr ""
msgid "Pipelines|Improve code quality with GitLab CI/CD"
msgstr ""
msgid "Pipelines|It is recommended the code is reviewed thoroughly before running this pipeline with the parent project's CI resource." msgid "Pipelines|It is recommended the code is reviewed thoroughly before running this pipeline with the parent project's CI resource."
msgstr "" msgstr ""
...@@ -24115,6 +24124,9 @@ msgstr "" ...@@ -24115,6 +24124,9 @@ msgstr ""
msgid "Pipelines|This project is not currently set up to run pipelines." msgid "Pipelines|This project is not currently set up to run pipelines."
msgstr "" msgstr ""
msgid "Pipelines|To keep your codebase simple, readable, and accessible to contributors, use GitLab CI/CD to analyze your code quality with every push to your project."
msgstr ""
msgid "Pipelines|Token" msgid "Pipelines|Token"
msgstr "" msgstr ""
...@@ -38007,6 +38019,45 @@ msgstr "" ...@@ -38007,6 +38019,45 @@ msgstr ""
msgid "closed issue" msgid "closed issue"
msgstr "" msgstr ""
msgid "codeQualityWalkthrough|A code quality job will now run every time you or your team members commit changes to your project. You can view the results of the code quality job in the job logs."
msgstr ""
msgid "codeQualityWalkthrough|Congrats! Your first pipeline is running %{emojiStart}zap%{emojiEnd}"
msgstr ""
msgid "codeQualityWalkthrough|Got it"
msgstr ""
msgid "codeQualityWalkthrough|Let's start by creating a new CI file."
msgstr ""
msgid "codeQualityWalkthrough|Not sure how to fix your failed job? We have compiled some tips on how to troubleshoot code quality jobs in the documentation."
msgstr ""
msgid "codeQualityWalkthrough|Read the documentation"
msgstr ""
msgid "codeQualityWalkthrough|Something went wrong. %{emojiStart}thinking%{emojiEnd} Let's fix it."
msgstr ""
msgid "codeQualityWalkthrough|To begin with code quality, we first need to create a new CI file using our code editor. We added a code quality template in the code editor to help you get started %{emojiStart}wink%{emojiEnd} .%{lineBreak}Take some time to review the template, when you are ready, use the %{strongStart}commit changes%{strongEnd} button at the bottom of the page."
msgstr ""
msgid "codeQualityWalkthrough|Troubleshoot your code quality job"
msgstr ""
msgid "codeQualityWalkthrough|View the logs"
msgstr ""
msgid "codeQualityWalkthrough|Well done! You've just automated your code quality review. %{emojiStart}raised_hands%{emojiEnd}"
msgstr ""
msgid "codeQualityWalkthrough|Your job failed. No worries - this happens. Let's view the logs, and see how we can fix it."
msgstr ""
msgid "codeQualityWalkthrough|Your pipeline can take a few minutes to run. If you enabled email notifications, you'll receive an email with your pipeline status. In the meantime, why don't you get some coffee? You earned it!"
msgstr ""
msgid "collect usage information" msgid "collect usage information"
msgstr "" msgstr ""
......
...@@ -464,11 +464,36 @@ RSpec.describe Projects::BlobController do ...@@ -464,11 +464,36 @@ RSpec.describe Projects::BlobController do
sign_in(user) sign_in(user)
end end
it_behaves_like 'tracking unique hll events' do
subject(:request) { post :create, params: default_params } subject(:request) { post :create, params: default_params }
it_behaves_like 'tracking unique hll events' do
let(:target_id) { 'g_edit_by_sfe' } let(:target_id) { 'g_edit_by_sfe' }
let(:expected_type) { instance_of(Integer) } let(:expected_type) { instance_of(Integer) }
end end
it 'redirects to blob' do
request
expect(response).to redirect_to(project_blob_path(project, 'master/docs/EXAMPLE_FILE'))
end
context 'when code_quality_walkthrough param is present' do
let(:default_params) { super().merge(code_quality_walkthrough: true) }
it 'redirects to the pipelines page' do
request
expect(response).to redirect_to(project_pipelines_path(project, code_quality_walkthrough: true))
end
it 'creates an "commit_created" experiment tracking event' do
experiment = double(track: true)
expect(controller).to receive(:experiment).with(:code_quality_walkthrough, namespace: project.root_ancestor).and_return(experiment)
request
expect(experiment).to have_received(:track).with(:commit_created)
end
end
end end
end end
...@@ -288,6 +288,17 @@ RSpec.describe Projects::PipelinesController do ...@@ -288,6 +288,17 @@ RSpec.describe Projects::PipelinesController do
get :index, params: { namespace_id: project.namespace, project_id: project } get :index, params: { namespace_id: project.namespace, project_id: project }
end end
end end
context 'code_quality_walkthrough experiment' do
it 'tracks the view', :experiment do
expect(experiment(:code_quality_walkthrough))
.to track(:view, property: project.root_ancestor.id.to_s)
.with_context(namespace: project.root_ancestor)
.on_next_instance
get :index, params: { namespace_id: project.namespace, project_id: project }
end
end
end end
describe 'GET #show' do describe 'GET #show' do
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`When the code_quality_walkthrough URL parameter is present Code Quality Walkthrough Step component commit_ci_file step renders a popover 1`] = `
<div>
<gl-popover-stub
container="viewport"
cssclasses=""
offset="90"
placement="right"
show=""
target="#js-code-quality-walkthrough"
triggers="manual"
>
<gl-sprintf-stub
message="To begin with code quality, we first need to create a new CI file using our code editor. We added a code quality template in the code editor to help you get started %{emojiStart}wink%{emojiEnd} .%{lineBreak}Take some time to review the template, when you are ready, use the %{strongStart}commit changes%{strongEnd} button at the bottom of the page."
/>
<div
class="gl-mt-2 gl-text-right"
>
<gl-button-stub
buttontextclasses=""
category="tertiary"
href=""
icon=""
size="medium"
variant="link"
>
Got it
</gl-button-stub>
</div>
</gl-popover-stub>
<!---->
</div>
`;
exports[`When the code_quality_walkthrough URL parameter is present Code Quality Walkthrough Step component failed_pipeline step renders a popover 1`] = `
<div>
<gl-popover-stub
container="viewport"
cssclasses=""
offset="98"
placement="bottom"
show=""
target="#js-code-quality-walkthrough"
triggers="manual"
>
<gl-sprintf-stub
message="Your job failed. No worries - this happens. Let's view the logs, and see how we can fix it."
/>
<div
class="gl-mt-2 gl-text-right"
>
<gl-button-stub
buttontextclasses=""
category="tertiary"
href="/group/project/-/jobs/:id?code_quality_walkthrough=true"
icon=""
size="medium"
variant="link"
>
View the logs
</gl-button-stub>
</div>
</gl-popover-stub>
<!---->
</div>
`;
exports[`When the code_quality_walkthrough URL parameter is present Code Quality Walkthrough Step component running_pipeline step renders a popover 1`] = `
<div>
<gl-popover-stub
container="viewport"
cssclasses=""
offset="97"
placement="bottom"
show=""
target="#js-code-quality-walkthrough"
triggers="manual"
>
<gl-sprintf-stub
message="Your pipeline can take a few minutes to run. If you enabled email notifications, you'll receive an email with your pipeline status. In the meantime, why don't you get some coffee? You earned it!"
/>
<div
class="gl-mt-2 gl-text-right"
>
<gl-button-stub
buttontextclasses=""
category="tertiary"
href=""
icon=""
size="medium"
variant="link"
>
Got it
</gl-button-stub>
</div>
</gl-popover-stub>
<!---->
</div>
`;
exports[`When the code_quality_walkthrough URL parameter is present Code Quality Walkthrough Step component success_pipeline step renders a popover 1`] = `
<div>
<gl-popover-stub
container="viewport"
cssclasses=""
offset="98"
placement="bottom"
show=""
target="#js-code-quality-walkthrough"
triggers="manual"
>
<gl-sprintf-stub
message="A code quality job will now run every time you or your team members commit changes to your project. You can view the results of the code quality job in the job logs."
/>
<div
class="gl-mt-2 gl-text-right"
>
<gl-button-stub
buttontextclasses=""
category="tertiary"
href="/group/project/-/jobs/:id?code_quality_walkthrough=true"
icon=""
size="medium"
variant="link"
>
View the logs
</gl-button-stub>
</div>
</gl-popover-stub>
<!---->
</div>
`;
exports[`When the code_quality_walkthrough URL parameter is present Code Quality Walkthrough Step component troubleshoot_job step renders an alert 1`] = `
<div>
<!---->
<gl-alert-stub
class="gl-my-5"
dismissible="true"
dismisslabel="Dismiss"
primarybuttontext="Read the documentation"
secondarybuttonlink=""
secondarybuttontext=""
title="Troubleshoot your code quality job"
variant="tip"
>
Not sure how to fix your failed job? We have compiled some tips on how to troubleshoot code quality jobs in the documentation.
</gl-alert-stub>
</div>
`;
import { GlButton, GlPopover } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Cookies from 'js-cookie';
import Step from '~/code_quality_walkthrough/components/step.vue';
import { EXPERIMENT_NAME, STEPS } from '~/code_quality_walkthrough/constants';
import { TRACKING_CONTEXT_SCHEMA } from '~/experimentation/constants';
import { getParameterByName } from '~/lib/utils/common_utils';
import Tracking from '~/tracking';
jest.mock('~/lib/utils/common_utils', () => ({
...jest.requireActual('~/lib/utils/common_utils'),
getParameterByName: jest.fn(),
}));
let wrapper;
function factory({ step, link }) {
wrapper = shallowMount(Step, {
propsData: { step, link },
});
}
afterEach(() => {
wrapper.destroy();
});
const dummyLink = '/group/project/-/jobs/:id?code_quality_walkthrough=true';
const dummyContext = 'experiment_context';
const findButton = () => wrapper.findComponent(GlButton);
const findPopover = () => wrapper.findComponent(GlPopover);
describe('When the code_quality_walkthrough URL parameter is missing', () => {
beforeEach(() => {
getParameterByName.mockReturnValue(false);
});
it('does not render the component', () => {
factory({
step: STEPS.commitCiFile,
});
expect(findPopover().exists()).toBe(false);
});
});
describe('When the code_quality_walkthrough URL parameter is present', () => {
beforeEach(() => {
getParameterByName.mockReturnValue(true);
Cookies.set(EXPERIMENT_NAME, { data: dummyContext });
});
afterEach(() => {
Cookies.remove(EXPERIMENT_NAME);
});
describe('When mounting the component', () => {
beforeEach(() => {
jest.spyOn(Tracking, 'event');
factory({
step: STEPS.commitCiFile,
});
});
it('tracks an event', () => {
expect(Tracking.event).toHaveBeenCalledWith(
EXPERIMENT_NAME,
`${STEPS.commitCiFile}_displayed`,
{
context: {
schema: TRACKING_CONTEXT_SCHEMA,
data: dummyContext,
},
},
);
});
});
describe('When updating the component', () => {
beforeEach(() => {
factory({
step: STEPS.runningPipeline,
});
jest.spyOn(Tracking, 'event');
wrapper.setProps({ step: STEPS.successPipeline });
});
it('tracks an event', () => {
expect(Tracking.event).toHaveBeenCalledWith(
EXPERIMENT_NAME,
`${STEPS.successPipeline}_displayed`,
{
context: {
schema: TRACKING_CONTEXT_SCHEMA,
data: dummyContext,
},
},
);
});
});
describe('When dismissing a popover', () => {
beforeEach(() => {
factory({
step: STEPS.commitCiFile,
});
jest.spyOn(Cookies, 'set');
jest.spyOn(Tracking, 'event');
findButton().vm.$emit('click');
});
it('sets a cookie', () => {
expect(Cookies.set).toHaveBeenCalledWith(
EXPERIMENT_NAME,
{ commit_ci_file: true, data: dummyContext },
{ expires: 365 },
);
});
it('removes the popover', () => {
expect(findPopover().exists()).toBe(false);
});
it('tracks an event', () => {
expect(Tracking.event).toHaveBeenCalledWith(
EXPERIMENT_NAME,
`${STEPS.commitCiFile}_dismissed`,
{
context: {
schema: TRACKING_CONTEXT_SCHEMA,
data: dummyContext,
},
},
);
});
});
describe('Code Quality Walkthrough Step component', () => {
describe.each(Object.values(STEPS))('%s step', (step) => {
it(`renders ${step === STEPS.troubleshootJob ? 'an alert' : 'a popover'}`, () => {
const options = { step };
if ([STEPS.successPipeline, STEPS.failedPipeline].includes(step)) {
options.link = dummyLink;
}
factory(options);
expect(wrapper.element).toMatchSnapshot();
});
});
});
});
import '~/commons';
import MockAdapter from 'axios-mock-adapter'; import MockAdapter from 'axios-mock-adapter';
import Vue from 'vue'; import Vue from 'vue';
import mountComponent from 'helpers/vue_mount_component_helper'; import mountComponent from 'helpers/vue_mount_component_helper';
......
...@@ -35,6 +35,7 @@ describe('Job App', () => { ...@@ -35,6 +35,7 @@ describe('Job App', () => {
const props = { const props = {
artifactHelpUrl: 'help/artifact', artifactHelpUrl: 'help/artifact',
deploymentHelpUrl: 'help/deployment', deploymentHelpUrl: 'help/deployment',
codeQualityHelpPath: '/help/code_quality',
runnerSettingsUrl: 'settings/ci-cd/runners', runnerSettingsUrl: 'settings/ci-cd/runners',
variablesSettingsUrl: 'settings/ci-cd/variables', variablesSettingsUrl: 'settings/ci-cd/variables',
terminalPath: 'jobs/123/terminal', terminalPath: 'jobs/123/terminal',
......
import '~/commons';
import { GlButton, GlEmptyState, GlFilteredSearch, GlLoadingIcon, GlPagination } from '@gitlab/ui'; import { GlButton, GlEmptyState, GlFilteredSearch, GlLoadingIcon, GlPagination } from '@gitlab/ui';
import { mount } from '@vue/test-utils'; import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter'; import MockAdapter from 'axios-mock-adapter';
...@@ -6,6 +7,7 @@ import { nextTick } from 'vue'; ...@@ -6,6 +7,7 @@ import { nextTick } from 'vue';
import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises'; import waitForPromises from 'helpers/wait_for_promises';
import Api from '~/api'; import Api from '~/api';
import { getExperimentVariant } from '~/experimentation/utils';
import { deprecatedCreateFlash as createFlash } from '~/flash'; import { deprecatedCreateFlash as createFlash } from '~/flash';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import NavigationControls from '~/pipelines/components/pipelines_list/nav_controls.vue'; import NavigationControls from '~/pipelines/components/pipelines_list/nav_controls.vue';
...@@ -19,6 +21,10 @@ import TablePagination from '~/vue_shared/components/pagination/table_pagination ...@@ -19,6 +21,10 @@ import TablePagination from '~/vue_shared/components/pagination/table_pagination
import { stageReply, users, mockSearch, branches } from './mock_data'; import { stageReply, users, mockSearch, branches } from './mock_data';
jest.mock('~/flash'); jest.mock('~/flash');
jest.mock('~/experimentation/utils', () => ({
...jest.requireActual('~/experimentation/utils'),
getExperimentVariant: jest.fn().mockReturnValue('control'),
}));
const mockProjectPath = 'twitter/flight'; const mockProjectPath = 'twitter/flight';
const mockProjectId = '21'; const mockProjectId = '21';
...@@ -41,6 +47,7 @@ describe('Pipelines', () => { ...@@ -41,6 +47,7 @@ describe('Pipelines', () => {
ciLintPath: '/ci/lint', ciLintPath: '/ci/lint',
resetCachePath: `${mockProjectPath}/settings/ci_cd/reset_cache`, resetCachePath: `${mockProjectPath}/settings/ci_cd/reset_cache`,
newPipelinePath: `${mockProjectPath}/pipelines/new`, newPipelinePath: `${mockProjectPath}/pipelines/new`,
codeQualityPagePath: `${mockProjectPath}/-/new/master?commit_message=Add+.gitlab-ci.yml+and+create+a+code+quality+job&file_name=.gitlab-ci.yml&template=Code-Quality`,
}; };
const noPermissions = { const noPermissions = {
...@@ -551,6 +558,19 @@ describe('Pipelines', () => { ...@@ -551,6 +558,19 @@ describe('Pipelines', () => {
); );
}); });
describe('when the code_quality_walkthrough experiment is active', () => {
beforeAll(() => {
getExperimentVariant.mockReturnValue('candidate');
});
it('renders another CTA button', () => {
expect(findEmptyState().findComponent(GlButton).text()).toBe('Add a code quality job');
expect(findEmptyState().findComponent(GlButton).attributes('href')).toBe(
paths.codeQualityPagePath,
);
});
});
it('does not render filtered search', () => { it('does not render filtered search', () => {
expect(findFilteredSearch().exists()).toBe(false); expect(findFilteredSearch().exists()).toBe(false);
}); });
......
import '~/commons';
import { GlTable } from '@gitlab/ui'; import { GlTable } from '@gitlab/ui';
import { mount } from '@vue/test-utils'; import { mount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import { extendedWrapper } from 'helpers/vue_test_utils_helper';
...@@ -5,11 +6,11 @@ import PipelineMiniGraph from '~/pipelines/components/pipelines_list/pipeline_mi ...@@ -5,11 +6,11 @@ import PipelineMiniGraph from '~/pipelines/components/pipelines_list/pipeline_mi
import PipelineOperations from '~/pipelines/components/pipelines_list/pipeline_operations.vue'; import PipelineOperations from '~/pipelines/components/pipelines_list/pipeline_operations.vue';
import PipelineTriggerer from '~/pipelines/components/pipelines_list/pipeline_triggerer.vue'; import PipelineTriggerer from '~/pipelines/components/pipelines_list/pipeline_triggerer.vue';
import PipelineUrl from '~/pipelines/components/pipelines_list/pipeline_url.vue'; import PipelineUrl from '~/pipelines/components/pipelines_list/pipeline_url.vue';
import PipelinesStatusBadge from '~/pipelines/components/pipelines_list/pipelines_status_badge.vue';
import PipelinesTable from '~/pipelines/components/pipelines_list/pipelines_table.vue'; import PipelinesTable from '~/pipelines/components/pipelines_list/pipelines_table.vue';
import PipelinesTimeago from '~/pipelines/components/pipelines_list/time_ago.vue'; import PipelinesTimeago from '~/pipelines/components/pipelines_list/time_ago.vue';
import eventHub from '~/pipelines/event_hub'; import eventHub from '~/pipelines/event_hub';
import CiBadge from '~/vue_shared/components/ci_badge_link.vue';
import CommitComponent from '~/vue_shared/components/commit.vue'; import CommitComponent from '~/vue_shared/components/commit.vue';
jest.mock('~/pipelines/event_hub'); jest.mock('~/pipelines/event_hub');
...@@ -42,7 +43,7 @@ describe('Pipelines Table', () => { ...@@ -42,7 +43,7 @@ describe('Pipelines Table', () => {
}; };
const findGlTable = () => wrapper.findComponent(GlTable); const findGlTable = () => wrapper.findComponent(GlTable);
const findStatusBadge = () => wrapper.findComponent(PipelinesStatusBadge); const findStatusBadge = () => wrapper.findComponent(CiBadge);
const findPipelineInfo = () => wrapper.findComponent(PipelineUrl); const findPipelineInfo = () => wrapper.findComponent(PipelineUrl);
const findTriggerer = () => wrapper.findComponent(PipelineTriggerer); const findTriggerer = () => wrapper.findComponent(PipelineTriggerer);
const findCommit = () => wrapper.findComponent(CommitComponent); const findCommit = () => wrapper.findComponent(CommitComponent);
......
...@@ -794,6 +794,12 @@ RSpec.describe ProjectPresenter do ...@@ -794,6 +794,12 @@ RSpec.describe ProjectPresenter do
end end
end end
describe '#add_code_quality_ci_yml_path' do
subject { presenter.add_code_quality_ci_yml_path }
it { is_expected.to match(/code_quality_walkthrough=true.*template=Code-Quality/) }
end
describe 'empty_repo_upload_experiment?' do describe 'empty_repo_upload_experiment?' do
subject { presenter.empty_repo_upload_experiment? } subject { presenter.empty_repo_upload_experiment? }
......
...@@ -70,6 +70,20 @@ RSpec.describe PipelineDetailsEntity do ...@@ -70,6 +70,20 @@ RSpec.describe PipelineDetailsEntity do
expect(subject[:flags][:retryable]).to eq false expect(subject[:flags][:retryable]).to eq false
end end
end end
it 'does not contain code_quality_build_path in details' do
expect(subject[:details]).not_to include :code_quality_build_path
end
context 'when option code_quality_walkthrough is set and pipeline is a success' do
let(:entity) do
described_class.represent(pipeline, request: request, code_quality_walkthrough: true)
end
it 'contains details.code_quality_build_path' do
expect(subject[:details]).to include :code_quality_build_path
end
end
end end
context 'when pipeline is cancelable' do context 'when pipeline is cancelable' do
......
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