Commit 6dcad937 authored by Frédéric Caplette's avatar Frédéric Caplette Committed by Andrew Fontaine

Add generic message when no pipeline in MR

When there are no pipeline in a MR,
we now show a more generic message
that informs the user that we are
checking for their pipeline to
be created.
parent 02f27285
...@@ -309,7 +309,8 @@ export default { ...@@ -309,7 +309,8 @@ export default {
<div <div
v-for="(stage, index) in pipeline.details.stages" v-for="(stage, index) in pipeline.details.stages"
:key="index" :key="index"
class="stage-container dropdown js-mini-pipeline-graph" class="stage-container dropdown"
data-testid="widget-mini-pipeline-graph"
> >
<pipeline-stage <pipeline-stage
:type="$options.pipelinesTable" :type="$options.pipelinesTable"
......
<script> <script>
/* eslint-disable vue/require-default-prop */ /* eslint-disable vue/require-default-prop */
import { GlTooltipDirective, GlLink } from '@gitlab/ui'; import { GlIcon, GlLink, GlLoadingIcon, GlSprintf, GlTooltipDirective } from '@gitlab/ui';
import mrWidgetPipelineMixin from 'ee_else_ce/vue_merge_request_widget/mixins/mr_widget_pipeline'; import mrWidgetPipelineMixin from 'ee_else_ce/vue_merge_request_widget/mixins/mr_widget_pipeline';
import { sprintf, s__ } from '~/locale'; import { s__ } from '~/locale';
import PipelineStage from '~/pipelines/components/pipelines_list/stage.vue'; import PipelineStage from '~/pipelines/components/pipelines_list/stage.vue';
import CiIcon from '~/vue_shared/components/ci_icon.vue'; import CiIcon from '~/vue_shared/components/ci_icon.vue';
import Icon from '~/vue_shared/components/icon.vue';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue'; import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
export default { export default {
name: 'MRWidgetPipeline', name: 'MRWidgetPipeline',
components: { components: {
PipelineStage,
CiIcon, CiIcon,
Icon,
TooltipOnTruncate,
GlLink, GlLink,
GlLoadingIcon,
GlIcon,
GlSprintf,
PipelineStage,
TooltipOnTruncate,
LinkedPipelinesMiniList: () => LinkedPipelinesMiniList: () =>
import('ee_component/vue_shared/components/linked_pipelines_mini_list.vue'), import('ee_component/vue_shared/components/linked_pipelines_mini_list.vue'),
}, },
...@@ -54,7 +55,11 @@ export default { ...@@ -54,7 +55,11 @@ export default {
type: String, type: String,
required: false, required: false,
}, },
troubleshootingDocsPath: { mrTroubleshootingDocsPath: {
type: String,
required: true,
},
ciTroubleshootingDocsPath: {
type: String, type: String,
required: true, required: true,
}, },
...@@ -64,10 +69,7 @@ export default { ...@@ -64,10 +69,7 @@ export default {
return this.pipeline && Object.keys(this.pipeline).length > 0; return this.pipeline && Object.keys(this.pipeline).length > 0;
}, },
hasCIError() { hasCIError() {
return (this.hasCi && !this.ciStatus) || this.hasPipelineMustSucceedConflict; return this.hasPipeline && !this.ciStatus;
},
hasPipelineMustSucceedConflict() {
return !this.hasCi && this.pipelineMustSucceed;
}, },
status() { status() {
return this.pipeline.details && this.pipeline.details.status return this.pipeline.details && this.pipeline.details.status
...@@ -82,22 +84,6 @@ export default { ...@@ -82,22 +84,6 @@ export default {
hasCommitInfo() { hasCommitInfo() {
return this.pipeline.commit && Object.keys(this.pipeline.commit).length > 0; return this.pipeline.commit && Object.keys(this.pipeline.commit).length > 0;
}, },
errorText() {
if (this.hasPipelineMustSucceedConflict) {
return s__('Pipeline|No pipeline has been run for this commit.');
}
return sprintf(
s__(
'Pipeline|Could not retrieve the pipeline status. For troubleshooting steps, read the %{linkStart}documentation%{linkEnd}.',
),
{
linkStart: `<a href="${this.troubleshootingDocsPath}">`,
linkEnd: '</a>',
},
false,
);
},
isTriggeredByMergeRequest() { isTriggeredByMergeRequest() {
return Boolean(this.pipeline.merge_request); return Boolean(this.pipeline.merge_request);
}, },
...@@ -118,15 +104,51 @@ export default { ...@@ -118,15 +104,51 @@ export default {
return ''; return '';
}, },
}, },
errorText: s__(
'Pipeline|Could not retrieve the pipeline status. For troubleshooting steps, read the %{linkStart}documentation%{linkEnd}.',
),
monitoringPipelineText: s__('Pipeline|Checking pipeline status.'),
}; };
</script> </script>
<template> <template>
<div class="ci-widget media js-ci-widget"> <div class="ci-widget media">
<template v-if="!hasPipeline || hasCIError"> <template v-if="hasCIError">
<div class="add-border ci-status-icon ci-status-icon-failed ci-error js-ci-error"> <gl-icon name="status_failed" class="gl-text-red-500" :size="24" />
<icon :size="24" name="status_failed_borderless" /> <div
class="gl-flex-fill-1 gl-ml-5"
tabindex="0"
role="text"
:aria-label="$options.errorText"
data-testid="ci-error-message"
>
<gl-sprintf :message="$options.errorText">
<template #link="{content}">
<gl-link :href="mrTroubleshootingDocsPath">{{ content }}</gl-link>
</template>
</gl-sprintf>
</div>
</template>
<template v-else-if="!hasPipeline">
<gl-loading-icon size="md" />
<div class="gl-flex-fill-1 gl-display-flex gl-ml-5" data-testid="monitoring-pipeline-message">
<span tabindex="0" role="text" :aria-label="$options.monitoringPipelineText">
<gl-sprintf :message="$options.monitoringPipelineText" />
</span>
<gl-link
:href="ciTroubleshootingDocsPath"
target="_blank"
class="gl-display-flex gl-align-items-center gl-ml-2"
tabindex="0"
>
<gl-icon
name="question"
:small="12"
tabindex="0"
role="text"
:aria-label="__('Link to go to GitLab pipeline documentation')"
/>
</gl-link>
</div> </div>
<div class="media-body gl-ml-3" v-html="errorText"></div>
</template> </template>
<template v-else-if="hasPipeline"> <template v-else-if="hasPipeline">
<a :href="status.details_path" class="align-self-start gl-mr-3"> <a :href="status.details_path" class="align-self-start gl-mr-3">
...@@ -136,13 +158,15 @@ export default { ...@@ -136,13 +158,15 @@ export default {
<div class="ci-widget-content"> <div class="ci-widget-content">
<div class="media-body"> <div class="media-body">
<div <div
class="font-weight-bold js-pipeline-info-container" class="gl-font-weight-bold"
data-testid="pipeline-info-container"
data-qa-selector="merge_request_pipeline_info_content" data-qa-selector="merge_request_pipeline_info_content"
> >
{{ pipeline.details.name }} {{ pipeline.details.name }}
<gl-link <gl-link
:href="pipeline.path" :href="pipeline.path"
class="pipeline-id font-weight-normal pipeline-number" class="pipeline-id gl-font-weight-normal pipeline-number"
data-testid="pipeline-id"
data-qa-selector="pipeline_link" data-qa-selector="pipeline_link"
>#{{ pipeline.id }}</gl-link >#{{ pipeline.id }}</gl-link
> >
...@@ -151,7 +175,8 @@ export default { ...@@ -151,7 +175,8 @@ export default {
{{ s__('Pipeline|for') }} {{ s__('Pipeline|for') }}
<gl-link <gl-link
:href="pipeline.commit.commit_path" :href="pipeline.commit.commit_path"
class="commit-sha js-commit-link font-weight-normal" class="commit-sha gl-font-weight-normal"
data-testid="commit-link"
>{{ pipeline.commit.short_id }}</gl-link >{{ pipeline.commit.short_id }}</gl-link
> >
</template> </template>
...@@ -160,18 +185,18 @@ export default { ...@@ -160,18 +185,18 @@ export default {
<tooltip-on-truncate <tooltip-on-truncate
:title="sourceBranch" :title="sourceBranch"
truncate-target="child" truncate-target="child"
class="label-branch label-truncate font-weight-normal" class="label-branch label-truncate gl-font-weight-normal"
v-html="sourceBranchLink" v-html="sourceBranchLink"
/> />
</template> </template>
</div> </div>
<div v-if="pipeline.coverage" class="coverage"> <div v-if="pipeline.coverage" class="coverage" data-testid="pipeline-coverage">
{{ s__('Pipeline|Coverage') }} {{ pipeline.coverage }}% {{ s__('Pipeline|Coverage') }} {{ pipeline.coverage }}%
<span <span
v-if="pipelineCoverageDelta" v-if="pipelineCoverageDelta"
class="js-pipeline-coverage-delta"
:class="coverageDeltaClass" :class="coverageDeltaClass"
data-testid="pipeline-coverage-delta"
> >
({{ pipelineCoverageDelta }}%) ({{ pipelineCoverageDelta }}%)
</span> </span>
...@@ -189,13 +214,13 @@ export default { ...@@ -189,13 +214,13 @@ export default {
:class="{ :class="{
'has-downstream': hasDownstream(i), 'has-downstream': hasDownstream(i),
}" }"
class="stage-container dropdown js-mini-pipeline-graph mr-widget-pipeline-stages" class="stage-container dropdown mr-widget-pipeline-stages"
data-testid="widget-mini-pipeline-graph"
> >
<pipeline-stage :stage="stage" /> <pipeline-stage :stage="stage" />
</div> </div>
</template> </template>
</span> </span>
<linked-pipelines-mini-list v-if="triggered.length" :triggered="triggered" /> <linked-pipelines-mini-list v-if="triggered.length" :triggered="triggered" />
</span> </span>
</div> </div>
......
...@@ -82,7 +82,8 @@ export default { ...@@ -82,7 +82,8 @@ export default {
:pipeline-must-succeed="mr.onlyAllowMergeIfPipelineSucceeds" :pipeline-must-succeed="mr.onlyAllowMergeIfPipelineSucceeds"
:source-branch="branch" :source-branch="branch"
:source-branch-link="branchLink" :source-branch-link="branchLink"
:troubleshooting-docs-path="mr.troubleshootingDocsPath" :mr-troubleshooting-docs-path="mr.mrTroubleshootingDocsPath"
:ci-troubleshooting-docs-path="mr.ciTroubleshootingDocsPath"
/> />
<template #footer> <template #footer>
<div v-if="mr.exposedArtifactsPath" class="js-exposed-artifacts"> <div v-if="mr.exposedArtifactsPath" class="js-exposed-artifacts">
......
export const SUCCESS = 'success';
export const WARNING = 'warning'; export const WARNING = 'warning';
export const DANGER = 'danger'; export const DANGER = 'danger';
......
...@@ -163,7 +163,8 @@ export default class MergeRequestStore { ...@@ -163,7 +163,8 @@ export default class MergeRequestStore {
setPaths(data) { setPaths(data) {
// Paths are set on the first load of the page and not auto-refreshed // Paths are set on the first load of the page and not auto-refreshed
this.squashBeforeMergeHelpPath = data.squash_before_merge_help_path; this.squashBeforeMergeHelpPath = data.squash_before_merge_help_path;
this.troubleshootingDocsPath = data.troubleshooting_docs_path; this.mrTroubleshootingDocsPath = data.mr_troubleshooting_docs_path;
this.ciTroubleshootingDocsPath = data.ci_troubleshooting_docs_path;
this.pipelineMustSucceedDocsPath = data.pipeline_must_succeed_docs_path; this.pipelineMustSucceedDocsPath = data.pipeline_must_succeed_docs_path;
this.mergeRequestBasicPath = data.merge_request_basic_path; this.mergeRequestBasicPath = data.merge_request_basic_path;
this.mergeRequestWidgetPath = data.merge_request_widget_path; this.mergeRequestWidgetPath = data.merge_request_widget_path;
......
...@@ -7,7 +7,8 @@ ...@@ -7,7 +7,8 @@
window.gl.mrWidgetData = #{serialize_issuable(@merge_request, serializer: 'widget', issues_links: true)} window.gl.mrWidgetData = #{serialize_issuable(@merge_request, serializer: 'widget', issues_links: true)}
window.gl.mrWidgetData.squash_before_merge_help_path = '#{help_page_path("user/project/merge_requests/squash_and_merge")}'; window.gl.mrWidgetData.squash_before_merge_help_path = '#{help_page_path("user/project/merge_requests/squash_and_merge")}';
window.gl.mrWidgetData.troubleshooting_docs_path = '#{help_page_path('user/project/merge_requests/reviewing_and_managing_merge_requests.md', anchor: 'troubleshooting')}'; window.gl.mrWidgetData.ci_troubleshooting_docs_path = '#{help_page_path('ci/troubleshooting.md')}';
window.gl.mrWidgetData.mr_troubleshooting_docs_path = '#{help_page_path('user/project/merge_requests/reviewing_and_managing_merge_requests.md', anchor: 'troubleshooting')}';
window.gl.mrWidgetData.pipeline_must_succeed_docs_path = '#{help_page_path('user/project/merge_requests/merge_when_pipeline_succeeds.md', anchor: 'only-allow-merge-requests-to-be-merged-if-the-pipeline-succeeds')}'; window.gl.mrWidgetData.pipeline_must_succeed_docs_path = '#{help_page_path('user/project/merge_requests/merge_when_pipeline_succeeds.md', anchor: 'only-allow-merge-requests-to-be-merged-if-the-pipeline-succeeds')}';
window.gl.mrWidgetData.security_approvals_help_page_path = '#{help_page_path('user/application_security/index.md', anchor: 'security-approvals-in-merge-requests-ultimate')}'; window.gl.mrWidgetData.security_approvals_help_page_path = '#{help_page_path('user/application_security/index.md', anchor: 'security-approvals-in-merge-requests-ultimate')}';
window.gl.mrWidgetData.eligible_approvers_docs_path = '#{help_page_path('user/project/merge_requests/merge_request_approvals', anchor: 'eligible-approvers')}'; window.gl.mrWidgetData.eligible_approvers_docs_path = '#{help_page_path('user/project/merge_requests/merge_request_approvals', anchor: 'eligible-approvers')}';
......
---
title: Add generic message when no pipeline in MR
merge_request: 35980
author:
type: fixed
---
stage: Verify
group: Continuous Integration
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
type: reference
---
# Troubleshooting CI/CD
## Merge request pipeline widget
The merge request pipeline widget shows information about the pipeline status in a Merge Request. It's displayed above the [merge request ability to merge widget](#merge-request-ability-to-merge-widget).
There are several messages that can be displayed depending on the status of the pipeline.
### "Checking pipeline status"
This message is shown when the merge request has no pipeline associated with the latest commit yet and [Pipelines must succeed](../user/project/merge_requests/merge_when_pipeline_succeeds.md#only-allow-merge-requests-to-be-merged-if-the-pipeline-succeeds) is turned on. This might be because:
- GitLab hasn't finished creating the pipeline yet.
- You are using an external CI service and GitLab hasn't heard back from the service yet.
- You are not using CI/CD pipelines in your project.
After the pipeline is created, the message will update with the pipeline status.
Note: Currently if you delete the latest pipeline of a Merge Request, this message will be shown instead of a meaningful error message. This is a known issue and should be resolved soon.
## Merge request ability to merge widget
The merge request status widget shows the **Merge** button and whether or not a merge request is ready to merge. If the merge request can't be merged, the reason for this is displayed.
If the pipeline is still running, the **Merge** button is replaced with the **Merge when pipeline succeeds** button.
If [**Merge Trains**](merge_request_pipelines/pipelines_for_merged_results/merge_trains/index.md) are enabled, the button is either **Add to merge train** or **Add to merge train when pipeline succeeds**. **(PREMIUM)**
### "A CI/CD pipeline must run and be successful before merge"
This message is shown if the [Pipelines must succeed](../user/project/merge_requests/merge_when_pipeline_succeeds.md#only-allow-merge-requests-to-be-merged-if-the-pipeline-succeeds) setting is enabled in the project and a pipeline has not yet run successfully. This also applies if the pipeline has not been created yet, or if you are waiting for an external CI service. If you don't use pipelines for your project, then you should disable **Pipelines must succeed** so you can accept merge requests.
...@@ -7,6 +7,8 @@ import mockLinkedPipelines from '../vue_shared/components/linked_pipelines_mock_ ...@@ -7,6 +7,8 @@ import mockLinkedPipelines from '../vue_shared/components/linked_pipelines_mock_
describe('MRWidgetPipeline', () => { describe('MRWidgetPipeline', () => {
let wrapper; let wrapper;
const findPipelineInfoContainer = () => wrapper.find('[data-testid="pipeline-info-container"');
function createComponent(pipeline) { function createComponent(pipeline) {
wrapper = mount(pipelineComponent, { wrapper = mount(pipelineComponent, {
propsData: { propsData: {
...@@ -16,7 +18,8 @@ describe('MRWidgetPipeline', () => { ...@@ -16,7 +18,8 @@ describe('MRWidgetPipeline', () => {
ciStatus: 'success', ciStatus: 'success',
sourceBranchLink: undefined, sourceBranchLink: undefined,
sourceBranch: undefined, sourceBranch: undefined,
troubleshootingDocsPath: 'help', mrTroubleshootingDocsPath: 'help',
ciTroubleshootingDocsPath: 'help2',
}, },
}); });
} }
...@@ -73,7 +76,7 @@ describe('MRWidgetPipeline', () => { ...@@ -73,7 +76,7 @@ describe('MRWidgetPipeline', () => {
createComponent(pipeline); createComponent(pipeline);
const expected = `Merge train pipeline #${pipeline.id} ${pipeline.details.status.label} for ${pipeline.commit.short_id}`; const expected = `Merge train pipeline #${pipeline.id} ${pipeline.details.status.label} for ${pipeline.commit.short_id}`;
const actual = trimText(wrapper.find('.js-pipeline-info-container').text()); const actual = trimText(findPipelineInfoContainer().text());
expect(actual).toBe(expected); expect(actual).toBe(expected);
}); });
...@@ -87,7 +90,7 @@ describe('MRWidgetPipeline', () => { ...@@ -87,7 +90,7 @@ describe('MRWidgetPipeline', () => {
createComponent(pipeline); createComponent(pipeline);
const expected = `Merged result pipeline #${pipeline.id} ${pipeline.details.status.label} for ${pipeline.commit.short_id}`; const expected = `Merged result pipeline #${pipeline.id} ${pipeline.details.status.label} for ${pipeline.commit.short_id}`;
const actual = trimText(wrapper.find('.js-pipeline-info-container').text()); const actual = trimText(findPipelineInfoContainer().text());
expect(actual).toBe(expected); expect(actual).toBe(expected);
}); });
......
...@@ -13708,6 +13708,9 @@ msgstr "" ...@@ -13708,6 +13708,9 @@ msgstr ""
msgid "Link title is required" msgid "Link title is required"
msgstr "" msgstr ""
msgid "Link to go to GitLab pipeline documentation"
msgstr ""
msgid "Linked emails (%{email_count})" msgid "Linked emails (%{email_count})"
msgstr "" msgstr ""
...@@ -16779,6 +16782,9 @@ msgstr "" ...@@ -16779,6 +16782,9 @@ msgstr ""
msgid "Pipeline|Canceled" msgid "Pipeline|Canceled"
msgstr "" msgstr ""
msgid "Pipeline|Checking pipeline status."
msgstr ""
msgid "Pipeline|Commit" msgid "Pipeline|Commit"
msgstr "" msgstr ""
...@@ -16818,9 +16824,6 @@ msgstr "" ...@@ -16818,9 +16824,6 @@ msgstr ""
msgid "Pipeline|Merged result pipeline" msgid "Pipeline|Merged result pipeline"
msgstr "" msgstr ""
msgid "Pipeline|No pipeline has been run for this commit."
msgstr ""
msgid "Pipeline|Passed" msgid "Pipeline|Passed"
msgstr "" msgstr ""
......
...@@ -268,7 +268,7 @@ RSpec.describe 'Merge request > User sees merge widget', :js do ...@@ -268,7 +268,7 @@ RSpec.describe 'Merge request > User sees merge widget', :js do
end end
end end
context 'view merge request where project has CI set up but no CI status' do context 'view merge request where there is no pipeline yet' do
before do before do
pipeline = create(:ci_pipeline, project: project, pipeline = create(:ci_pipeline, project: project,
sha: merge_request.diff_head_sha, sha: merge_request.diff_head_sha,
...@@ -278,11 +278,11 @@ RSpec.describe 'Merge request > User sees merge widget', :js do ...@@ -278,11 +278,11 @@ RSpec.describe 'Merge request > User sees merge widget', :js do
visit project_merge_request_path(project, merge_request) visit project_merge_request_path(project, merge_request)
end end
it 'has pipeline error text' do it 'has pipeline loading state' do
# Wait for the `ci_status` and `merge_check` requests # Wait for the `ci_status` and `merge_check` requests
wait_for_requests wait_for_requests
expect(page).to have_text("Could not retrieve the pipeline status. For troubleshooting steps, read the documentation.") expect(page).to have_text("Checking pipeline status")
end end
end end
...@@ -889,9 +889,9 @@ RSpec.describe 'Merge request > User sees merge widget', :js do ...@@ -889,9 +889,9 @@ RSpec.describe 'Merge request > User sees merge widget', :js do
visit project_merge_request_path(project, merge_request) visit project_merge_request_path(project, merge_request)
end end
it 'renders a CI pipeline error' do it 'renders a CI pipeline loading state' do
within '.ci-widget' do within '.ci-widget' do
expect(page).to have_content('Could not retrieve the pipeline status.') expect(page).to have_content('Checking pipeline status')
end end
end end
end end
......
...@@ -38,14 +38,6 @@ RSpec.describe 'Merge request > User sees pipelines', :js do ...@@ -38,14 +38,6 @@ RSpec.describe 'Merge request > User sees pipelines', :js do
expect(page).to have_selector('.stage-cell') expect(page).to have_selector('.stage-cell')
end end
it 'pipeline sha does not equal last commit sha' do
pipeline.update_attribute(:sha, '19e2e9b4ef76b422ce1154af39a91323ccc57434')
visit project_merge_request_path(project, merge_request)
wait_for_requests
expect(page.find('.ci-widget')).to have_text("Could not retrieve the pipeline status. For troubleshooting steps, read the documentation.")
end
context 'with a detached merge request pipeline' do context 'with a detached merge request pipeline' do
let(:merge_request) { create(:merge_request, :with_detached_merge_request_pipeline) } let(:merge_request) { create(:merge_request, :with_detached_merge_request_pipeline) }
......
...@@ -528,7 +528,7 @@ RSpec.describe 'Pipelines', :js do ...@@ -528,7 +528,7 @@ RSpec.describe 'Pipelines', :js do
end end
it 'renders a mini pipeline graph' do it 'renders a mini pipeline graph' do
expect(page).to have_selector('.js-mini-pipeline-graph') expect(page).to have_selector('[data-testid="widget-mini-pipeline-graph"]')
expect(page).to have_selector('.js-builds-dropdown-button') expect(page).to have_selector('.js-builds-dropdown-button')
end end
......
<div class="js-builds-dropdown-tests dropdown dropdown js-mini-pipeline-graph"> <div class="js-builds-dropdown-tests dropdown dropdown" data-testid="widget-mini-pipeline-graph">
<button class="js-builds-dropdown-button" data-toggle="dropdown" data-stage-endpoint="foobar"> <button class="js-builds-dropdown-button" data-toggle="dropdown" data-stage-endpoint="foobar">
Dropdown Dropdown
</button> </button>
<ul class="dropdown-menu mini-pipeline-graph-dropdown-menu js-builds-dropdown-container"> <ul class="dropdown-menu mini-pipeline-graph-dropdown-menu js-builds-dropdown-container">
<li class="js-builds-dropdown-list scrollable-menu"> <li class="js-builds-dropdown-list scrollable-menu">
<ul></ul> <ul></ul>
</li> </li>
<li class="js-builds-dropdown-loading hidden"> <li class="js-builds-dropdown-loading hidden">
<span class="fa fa-spinner"></span> <span class="fa fa-spinner"></span>
</li> </li>
</ul> </ul>
</div> </div>
import Vue from 'vue'; import { shallowMount, mount } from '@vue/test-utils';
import mountComponent from 'helpers/vue_mount_component_helper'; import { GlLoadingIcon } from '@gitlab/ui';
import { trimText } from 'helpers/text_helper'; import { trimText } from 'helpers/text_helper';
import pipelineComponent from '~/vue_merge_request_widget/components/mr_widget_pipeline.vue'; import { SUCCESS } from '~/vue_merge_request_widget/constants';
import PipelineComponent from '~/vue_merge_request_widget/components/mr_widget_pipeline.vue';
import PipelineStage from '~/pipelines/components/pipelines_list/stage.vue';
import mockData from '../mock_data'; import mockData from '../mock_data';
describe('MRWidgetPipeline', () => { describe('MRWidgetPipeline', () => {
let vm; let wrapper;
let Component;
beforeEach(() => { const defaultProps = {
Component = Vue.extend(pipelineComponent); pipeline: mockData.pipeline,
ciStatus: SUCCESS,
hasCi: true,
mrTroubleshootingDocsPath: 'help',
ciTroubleshootingDocsPath: 'ci-help',
};
const ciErrorMessage =
'Could not retrieve the pipeline status. For troubleshooting steps, read the documentation.';
const monitoringMessage = 'Checking pipeline status.';
const findCIErrorMessage = () => wrapper.find('[data-testid="ci-error-message"]');
const findPipelineID = () => wrapper.find('[data-testid="pipeline-id"]');
const findPipelineInfoContainer = () => wrapper.find('[data-testid="pipeline-info-container"]');
const findCommitLink = () => wrapper.find('[data-testid="commit-link"]');
const findPipelineGraph = () => wrapper.find('[data-testid="widget-mini-pipeline-graph"]');
const findAllPipelineStages = () => wrapper.findAll(PipelineStage);
const findPipelineCoverage = () => wrapper.find('[data-testid="pipeline-coverage"]');
const findPipelineCoverageDelta = () => wrapper.find('[data-testid="pipeline-coverage-delta"]');
const findMonitoringPipelineMessage = () =>
wrapper.find('[data-testid="monitoring-pipeline-message"]');
const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
const createWrapper = (props, mountFn = shallowMount) => {
wrapper = mountFn(PipelineComponent, {
propsData: {
...defaultProps,
...props,
},
}); });
};
afterEach(() => { afterEach(() => {
vm.$destroy(); if (wrapper?.destroy) {
wrapper.destroy();
wrapper = null;
}
}); });
describe('computed', () => { describe('computed', () => {
describe('hasPipeline', () => { describe('hasPipeline', () => {
it('should return true when there is a pipeline', () => { beforeEach(() => {
vm = mountComponent(Component, { createWrapper();
pipeline: mockData.pipeline,
ciStatus: 'success',
hasCi: true,
troubleshootingDocsPath: 'help',
}); });
expect(vm.hasPipeline).toEqual(true); it('should return true when there is a pipeline', () => {
expect(wrapper.vm.hasPipeline).toBe(true);
}); });
it('should return false when there is no pipeline', () => { it('should return false when there is no pipeline', async () => {
vm = mountComponent(Component, { wrapper.setProps({ pipeline: {} });
pipeline: {},
troubleshootingDocsPath: 'help', await wrapper.vm.$nextTick();
});
expect(vm.hasPipeline).toEqual(false); expect(wrapper.vm.hasPipeline).toBe(false);
}); });
}); });
describe('hasCIError', () => { describe('hasCIError', () => {
it('should return false when there is no CI error', () => { beforeEach(() => {
vm = mountComponent(Component, { createWrapper();
pipeline: mockData.pipeline,
hasCi: true,
ciStatus: 'success',
troubleshootingDocsPath: 'help',
}); });
expect(vm.hasCIError).toEqual(false); it('should return false when there is no CI error', () => {
expect(wrapper.vm.hasCIError).toBe(false);
}); });
it('should return true when there is a CI error', () => { it('should return true when there is a pipeline, but no ci status', async () => {
vm = mountComponent(Component, { wrapper.setProps({ ciStatus: null });
pipeline: mockData.pipeline,
hasCi: true, await wrapper.vm.$nextTick();
ciStatus: null,
troubleshootingDocsPath: 'help',
});
expect(vm.hasCIError).toEqual(true); expect(wrapper.vm.hasCIError).toBe(true);
}); });
}); });
describe('coverageDeltaClass', () => { describe('coverageDeltaClass', () => {
it('should return no class if there is no coverage change', () => { beforeEach(() => {
vm = mountComponent(Component, { createWrapper({ pipelineCoverageDelta: '0' });
pipeline: mockData.pipeline,
pipelineCoverageDelta: '0',
troubleshootingDocsPath: 'help',
}); });
expect(vm.coverageDeltaClass).toEqual(''); it('should return no class if there is no coverage change', async () => {
expect(wrapper.vm.coverageDeltaClass).toBe('');
}); });
it('should return text-success if the coverage increased', () => { it('should return text-success if the coverage increased', async () => {
vm = mountComponent(Component, { wrapper.setProps({ pipelineCoverageDelta: '10' });
pipeline: mockData.pipeline,
pipelineCoverageDelta: '10',
troubleshootingDocsPath: 'help',
});
expect(vm.coverageDeltaClass).toEqual('text-success'); await wrapper.vm.$nextTick();
});
it('should return text-danger if the coverage decreased', () => { expect(wrapper.vm.coverageDeltaClass).toBe('text-success');
vm = mountComponent(Component, {
pipeline: mockData.pipeline,
pipelineCoverageDelta: '-12',
troubleshootingDocsPath: 'help',
}); });
expect(vm.coverageDeltaClass).toEqual('text-danger'); it('should return text-danger if the coverage decreased', async () => {
wrapper.setProps({ pipelineCoverageDelta: '-12' });
await wrapper.vm.$nextTick();
expect(wrapper.vm.coverageDeltaClass).toBe('text-danger');
}); });
}); });
}); });
describe('rendered output', () => { describe('rendered output', () => {
it('should render CI error', () => { beforeEach(() => {
vm = mountComponent(Component, { createWrapper({ ciStatus: null }, mount);
pipeline: mockData.pipeline,
hasCi: true,
troubleshootingDocsPath: 'help',
});
expect(vm.$el.querySelector('.media-body').textContent.trim()).toContain(
'Could not retrieve the pipeline status. For troubleshooting steps, read the documentation.',
);
}); });
it('should render CI error when no pipeline is provided', () => { it('should render CI error if there is a pipeline, but no status', async () => {
vm = mountComponent(Component, { expect(findCIErrorMessage().text()).toBe(ciErrorMessage);
pipeline: {},
hasCi: true,
ciStatus: 'success',
troubleshootingDocsPath: 'help',
}); });
expect(vm.$el.querySelector('.media-body').textContent.trim()).toContain( it('should render a loading state when no pipeline is found', async () => {
'Could not retrieve the pipeline status. For troubleshooting steps, read the documentation.', wrapper.setProps({
);
});
it('should render CI error when no CI is provided and pipeline must succeed is turned on', () => {
vm = mountComponent(Component, {
pipeline: {}, pipeline: {},
hasCi: false, hasCi: false,
pipelineMustSucceed: true, pipelineMustSucceed: true,
troubleshootingDocsPath: 'help',
}); });
expect(vm.$el.querySelector('.media-body').textContent.trim()).toContain( await wrapper.vm.$nextTick();
'No pipeline has been run for this commit.',
); expect(findMonitoringPipelineMessage().text()).toBe(monitoringMessage);
expect(findLoadingIcon().exists()).toBe(true);
}); });
describe('with a pipeline', () => { describe('with a pipeline', () => {
beforeEach(() => { beforeEach(() => {
vm = mountComponent(Component, { createWrapper(
pipeline: mockData.pipeline, {
hasCi: true,
ciStatus: 'success',
pipelineCoverageDelta: mockData.pipelineCoverageDelta, pipelineCoverageDelta: mockData.pipelineCoverageDelta,
troubleshootingDocsPath: 'help', },
}); mount,
);
}); });
it('should render pipeline ID', () => { it('should render pipeline ID', () => {
expect(vm.$el.querySelector('.pipeline-id').textContent.trim()).toEqual( expect(
`#${mockData.pipeline.id}`, findPipelineID()
); .text()
.trim(),
).toBe(`#${mockData.pipeline.id}`);
}); });
it('should render pipeline status and commit id', () => { it('should render pipeline status and commit id', () => {
expect(vm.$el.querySelector('.media-body').textContent.trim()).toContain( expect(findPipelineInfoContainer().text()).toMatch(mockData.pipeline.details.status.label);
mockData.pipeline.details.status.label,
);
expect(vm.$el.querySelector('.js-commit-link').textContent.trim()).toEqual( expect(
mockData.pipeline.commit.short_id, findCommitLink()
); .text()
.trim(),
).toBe(mockData.pipeline.commit.short_id);
expect(vm.$el.querySelector('.js-commit-link').getAttribute('href')).toEqual( expect(findCommitLink().attributes('href')).toBe(mockData.pipeline.commit.commit_path);
mockData.pipeline.commit.commit_path,
);
}); });
it('should render pipeline graph', () => { it('should render pipeline graph', () => {
expect(vm.$el.querySelector('.mr-widget-pipeline-graph')).toBeDefined(); expect(findPipelineGraph().exists()).toBe(true);
expect(vm.$el.querySelectorAll('.stage-container').length).toEqual( expect(findAllPipelineStages().length).toBe(mockData.pipeline.details.stages.length);
mockData.pipeline.details.stages.length,
);
}); });
it('should render coverage information', () => { it('should render coverage information', () => {
expect(vm.$el.querySelector('.media-body').textContent).toContain( expect(findPipelineCoverage().text()).toMatch(`Coverage ${mockData.pipeline.coverage}%`);
`Coverage ${mockData.pipeline.coverage}`,
);
}); });
it('should render pipeline coverage delta information', () => { it('should render pipeline coverage delta information', () => {
expect(vm.$el.querySelector('.js-pipeline-coverage-delta.text-danger')).toBeDefined(); expect(findPipelineCoverageDelta().exists()).toBe(true);
expect(vm.$el.querySelector('.js-pipeline-coverage-delta').textContent).toContain( expect(findPipelineCoverageDelta().text()).toBe(`(${mockData.pipelineCoverageDelta}%)`);
`(${mockData.pipelineCoverageDelta}%)`,
);
}); });
}); });
...@@ -192,71 +185,61 @@ describe('MRWidgetPipeline', () => { ...@@ -192,71 +185,61 @@ describe('MRWidgetPipeline', () => {
const mockCopy = JSON.parse(JSON.stringify(mockData)); const mockCopy = JSON.parse(JSON.stringify(mockData));
delete mockCopy.pipeline.commit; delete mockCopy.pipeline.commit;
vm = mountComponent(Component, { createWrapper({}, mount);
pipeline: mockCopy.pipeline,
hasCi: true,
ciStatus: 'success',
troubleshootingDocsPath: 'help',
});
}); });
it('should render pipeline ID', () => { it('should render pipeline ID', () => {
expect(vm.$el.querySelector('.pipeline-id').textContent.trim()).toEqual( expect(
`#${mockData.pipeline.id}`, findPipelineID()
); .text()
.trim(),
).toBe(`#${mockData.pipeline.id}`);
}); });
it('should render pipeline status', () => { it('should render pipeline status', () => {
expect(vm.$el.querySelector('.media-body').textContent.trim()).toContain( expect(findPipelineInfoContainer().text()).toMatch(mockData.pipeline.details.status.label);
mockData.pipeline.details.status.label,
);
expect(vm.$el.querySelector('.js-commit-link')).toBeNull();
}); });
it('should render pipeline graph', () => { it('should render pipeline graph', () => {
expect(vm.$el.querySelector('.mr-widget-pipeline-graph')).toBeDefined(); expect(findPipelineGraph().exists()).toBe(true);
expect(vm.$el.querySelectorAll('.stage-container').length).toEqual( expect(findAllPipelineStages().length).toBe(mockData.pipeline.details.stages.length);
mockData.pipeline.details.stages.length,
);
}); });
it('should render coverage information', () => { it('should render coverage information', () => {
expect(vm.$el.querySelector('.media-body').textContent).toContain( expect(findPipelineCoverage().text()).toMatch(`Coverage ${mockData.pipeline.coverage}%`);
`Coverage ${mockData.pipeline.coverage}`,
);
}); });
}); });
describe('without coverage', () => { describe('without coverage', () => {
it('should not render a coverage', () => { beforeEach(() => {
const mockCopy = JSON.parse(JSON.stringify(mockData)); const mockCopy = JSON.parse(JSON.stringify(mockData));
delete mockCopy.pipeline.coverage; delete mockCopy.pipeline.coverage;
vm = mountComponent(Component, { createWrapper(
{
pipeline: mockCopy.pipeline, pipeline: mockCopy.pipeline,
hasCi: true, },
ciStatus: 'success', mount,
troubleshootingDocsPath: 'help', );
}); });
expect(vm.$el.querySelector('.media-body').textContent).not.toContain('Coverage'); it('should not render a coverage component', () => {
expect(findPipelineCoverage().exists()).toBe(false);
}); });
}); });
describe('without a pipeline graph', () => { describe('without a pipeline graph', () => {
it('should not render a pipeline graph', () => { beforeEach(() => {
const mockCopy = JSON.parse(JSON.stringify(mockData)); const mockCopy = JSON.parse(JSON.stringify(mockData));
delete mockCopy.pipeline.details.stages; delete mockCopy.pipeline.details.stages;
vm = mountComponent(Component, { createWrapper({
pipeline: mockCopy.pipeline, pipeline: mockCopy.pipeline,
hasCi: true, });
ciStatus: 'success',
troubleshootingDocsPath: 'help',
}); });
expect(vm.$el.querySelector('.js-mini-pipeline-graph')).toEqual(null); it('should not render a pipeline graph', () => {
expect(findPipelineGraph().exists()).toBe(false);
}); });
}); });
...@@ -273,11 +256,8 @@ describe('MRWidgetPipeline', () => { ...@@ -273,11 +256,8 @@ describe('MRWidgetPipeline', () => {
}); });
const factory = () => { const factory = () => {
vm = mountComponent(Component, { createWrapper({
pipeline, pipeline,
hasCi: true,
ciStatus: 'success',
troubleshootingDocsPath: 'help',
sourceBranchLink: mockData.source_branch_link, sourceBranchLink: mockData.source_branch_link,
}); });
}; };
...@@ -289,7 +269,7 @@ describe('MRWidgetPipeline', () => { ...@@ -289,7 +269,7 @@ describe('MRWidgetPipeline', () => {
factory(); factory();
const expected = `Pipeline #${pipeline.id} ${pipeline.details.status.label} for ${pipeline.commit.short_id} on ${mockData.source_branch_link}`; const expected = `Pipeline #${pipeline.id} ${pipeline.details.status.label} for ${pipeline.commit.short_id} on ${mockData.source_branch_link}`;
const actual = trimText(vm.$el.querySelector('.js-pipeline-info-container').innerText); const actual = trimText(findPipelineInfoContainer().text());
expect(actual).toBe(expected); expect(actual).toBe(expected);
}); });
...@@ -302,7 +282,7 @@ describe('MRWidgetPipeline', () => { ...@@ -302,7 +282,7 @@ describe('MRWidgetPipeline', () => {
factory(); factory();
const expected = `Pipeline #${pipeline.id} ${pipeline.details.status.label} for ${pipeline.commit.short_id}`; const expected = `Pipeline #${pipeline.id} ${pipeline.details.status.label} for ${pipeline.commit.short_id}`;
const actual = trimText(vm.$el.querySelector('.js-pipeline-info-container').innerText); const actual = trimText(findPipelineInfoContainer().text());
expect(actual).toBe(expected); expect(actual).toBe(expected);
}); });
...@@ -316,7 +296,7 @@ describe('MRWidgetPipeline', () => { ...@@ -316,7 +296,7 @@ describe('MRWidgetPipeline', () => {
factory(); factory();
const expected = `Detached merge request pipeline #${pipeline.id} ${pipeline.details.status.label} for ${pipeline.commit.short_id}`; const expected = `Detached merge request pipeline #${pipeline.id} ${pipeline.details.status.label} for ${pipeline.commit.short_id}`;
const actual = trimText(vm.$el.querySelector('.js-pipeline-info-container').innerText); const actual = trimText(findPipelineInfoContainer().text());
expect(actual).toBe(expected); expect(actual).toBe(expected);
}); });
......
...@@ -239,7 +239,8 @@ export default { ...@@ -239,7 +239,8 @@ export default {
commit_change_content_path: '/root/acets-app/-/merge_requests/22/commit_change_content', commit_change_content_path: '/root/acets-app/-/merge_requests/22/commit_change_content',
merge_commit_path: merge_commit_path:
'http://localhost:3000/root/acets-app/commit/53027d060246c8f47e4a9310fb332aa52f221775', 'http://localhost:3000/root/acets-app/commit/53027d060246c8f47e4a9310fb332aa52f221775',
troubleshooting_docs_path: 'help', mr_troubleshooting_docs_path: 'help',
ci_troubleshooting_docs_path: 'help2',
merge_request_pipelines_docs_path: '/help/ci/merge_request_pipelines/index.md', merge_request_pipelines_docs_path: '/help/ci/merge_request_pipelines/index.md',
merge_train_when_pipeline_succeeds_docs_path: merge_train_when_pipeline_succeeds_docs_path:
'/help/ci/merge_request_pipelines/pipelines_for_merged_results/merge_trains/#startadd-to-merge-train-when-pipeline-succeeds', '/help/ci/merge_request_pipelines/pipelines_for_merged_results/merge_trains/#startadd-to-merge-train-when-pipeline-succeeds',
...@@ -312,7 +313,8 @@ export const mockStore = { ...@@ -312,7 +313,8 @@ export const mockStore = {
{ id: 0, name: 'prod', status: SUCCESS }, { id: 0, name: 'prod', status: SUCCESS },
{ id: 1, name: 'prod-docs', status: SUCCESS }, { id: 1, name: 'prod-docs', status: SUCCESS },
], ],
troubleshootingDocsPath: 'troubleshooting-docs-path', mrTroubleshootingDocsPath: 'mr-troubleshooting-docs-path',
ciTroubleshootingDocsPath: 'ci-troubleshooting-docs-path',
ciStatus: 'ci-status', ciStatus: 'ci-status',
hasCI: true, hasCI: true,
exposedArtifactsPath: 'exposed_artifacts.json', exposedArtifactsPath: 'exposed_artifacts.json',
......
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