提交 e3e30055 编写于 作者: G GitLab Bot

Add latest changes from gitlab-org/gitlab@master

上级 a821bd6a
......@@ -483,6 +483,16 @@ export const historyPushState = newUrl => {
window.history.pushState({}, document.title, newUrl);
};
/**
* Based on the current location and the string parameters provided
* overwrites the current entry in the history without reloading the page.
*
* @param {String} param
*/
export const historyReplaceState = newUrl => {
window.history.replaceState({}, document.title, newUrl);
};
/**
* Returns true for a String value of "true" and false otherwise.
* This is the opposite of Boolean(...).toString().
......
import Vue from 'vue';
import { GlToast } from '@gitlab/ui';
import { doesHashExistInUrl } from '~/lib/utils/url_utility';
import {
parseBoolean,
historyReplaceState,
buildUrlWithCurrentLocation,
} from '~/lib/utils/common_utils';
import { __ } from '~/locale';
import PipelinesStore from '../../../../pipelines/stores/pipelines_store';
import pipelinesComponent from '../../../../pipelines/components/pipelines.vue';
import Translate from '../../../../vue_shared/translate';
import { parseBoolean } from '../../../../lib/utils/common_utils';
Vue.use(Translate);
Vue.use(GlToast);
document.addEventListener(
'DOMContentLoaded',
......@@ -21,6 +29,11 @@ document.addEventListener(
},
created() {
this.dataset = document.querySelector(this.$options.el).dataset;
if (doesHashExistInUrl('delete_success')) {
this.$toast.show(__('The pipeline has been deleted'));
historyReplaceState(buildUrlWithCurrentLocation());
}
},
render(createElement) {
return createElement('pipelines-component', {
......
<script>
import { GlLoadingIcon } from '@gitlab/ui';
import { GlLoadingIcon, GlModal } from '@gitlab/ui';
import ciHeader from '../../vue_shared/components/header_ci_component.vue';
import eventHub from '../event_hub';
import { __ } from '~/locale';
const DELETE_MODAL_ID = 'pipeline-delete-modal';
export default {
name: 'PipelineHeaderSection',
components: {
ciHeader,
GlLoadingIcon,
GlModal,
},
props: {
pipeline: {
......@@ -33,6 +36,11 @@ export default {
shouldRenderContent() {
return !this.isLoading && Object.keys(this.pipeline).length;
},
deleteModalConfirmationText() {
return __(
'Are you sure you want to delete this pipeline? Doing so will expire all pipeline caches and delete all related objects, such as builds, logs, artifacts, and triggers. This action cannot be undone.',
);
},
},
watch: {
......@@ -42,6 +50,13 @@ export default {
},
methods: {
onActionClicked(action) {
if (action.modal) {
this.$root.$emit('bv::show::modal', action.modal);
} else {
this.postAction(action);
}
},
postAction(action) {
const index = this.actions.indexOf(action);
......@@ -49,6 +64,13 @@ export default {
eventHub.$emit('headerPostAction', action);
},
deletePipeline() {
const index = this.actions.findIndex(action => action.modal === DELETE_MODAL_ID);
this.$set(this.actions[index], 'isLoading', true);
eventHub.$emit('headerDeleteAction', this.actions[index]);
},
getActions() {
const actions = [];
......@@ -58,7 +80,6 @@ export default {
label: __('Retry'),
path: this.pipeline.retry_path,
cssClass: 'js-retry-button btn btn-inverted-secondary',
type: 'button',
isLoading: false,
});
}
......@@ -68,7 +89,16 @@ export default {
label: __('Cancel running'),
path: this.pipeline.cancel_path,
cssClass: 'js-btn-cancel-pipeline btn btn-danger',
type: 'button',
isLoading: false,
});
}
if (this.pipeline.delete_path) {
actions.push({
label: __('Delete'),
path: this.pipeline.delete_path,
modal: DELETE_MODAL_ID,
cssClass: 'js-btn-delete-pipeline btn btn-danger btn-inverted',
isLoading: false,
});
}
......@@ -76,6 +106,7 @@ export default {
return actions;
},
},
DELETE_MODAL_ID,
};
</script>
<template>
......@@ -88,8 +119,21 @@ export default {
:user="pipeline.user"
:actions="actions"
item-name="Pipeline"
@actionClicked="postAction"
@actionClicked="onActionClicked"
/>
<gl-loading-icon v-if="isLoading" :size="2" class="prepend-top-default append-bottom-default" />
<gl-modal
:modal-id="$options.DELETE_MODAL_ID"
:title="__('Delete pipeline')"
:ok-title="__('Delete pipeline')"
ok-variant="danger"
@ok="deletePipeline()"
>
<p>
{{ deleteModalConfirmationText }}
</p>
</gl-modal>
</div>
</template>
......@@ -2,6 +2,7 @@ import Vue from 'vue';
import Flash from '~/flash';
import Translate from '~/vue_shared/translate';
import { __ } from '~/locale';
import { setUrlFragment, redirectTo } from '~/lib/utils/url_utility';
import pipelineGraph from './components/graph/graph_component.vue';
import GraphBundleMixin from './mixins/graph_pipeline_bundle_mixin';
import PipelinesMediator from './pipeline_details_mediator';
......@@ -62,9 +63,11 @@ export default () => {
},
created() {
eventHub.$on('headerPostAction', this.postAction);
eventHub.$on('headerDeleteAction', this.deleteAction);
},
beforeDestroy() {
eventHub.$off('headerPostAction', this.postAction);
eventHub.$off('headerDeleteAction', this.deleteAction);
},
methods: {
postAction(action) {
......@@ -73,6 +76,13 @@ export default () => {
.then(() => this.mediator.refreshPipeline())
.catch(() => Flash(__('An error occurred while making the request.')));
},
deleteAction(action) {
this.mediator.stopPipelinePoll();
this.mediator.service
.deleteAction(action.path)
.then(({ request }) => redirectTo(setUrlFragment(request.responseURL, 'delete_success')))
.catch(() => Flash(__('An error occurred while deleting the pipeline.')));
},
},
render(createElement) {
return createElement('pipeline-header', {
......
......@@ -35,7 +35,7 @@ export default class pipelinesMediator {
if (!Visibility.hidden()) {
this.poll.restart();
} else {
this.poll.stop();
this.stopPipelinePoll();
}
});
}
......@@ -51,7 +51,7 @@ export default class pipelinesMediator {
}
refreshPipeline() {
this.poll.stop();
this.stopPipelinePoll();
return this.service
.getPipeline()
......@@ -64,6 +64,10 @@ export default class pipelinesMediator {
);
}
stopPipelinePoll() {
this.poll.stop();
}
/**
* Backend expects paramets in the following format: `expanded[]=id&expanded[]=id`
*/
......
......@@ -9,6 +9,11 @@ export default class PipelineService {
return axios.get(this.pipeline, { params });
}
// eslint-disable-next-line class-methods-use-this
deleteAction(endpoint) {
return axios.delete(`${endpoint}.json`);
}
// eslint-disable-next-line class-methods-use-this
postAction(endpoint) {
return axios.post(`${endpoint}.json`);
......
......@@ -117,28 +117,7 @@ export default {
<section v-if="actions.length" class="header-action-buttons">
<template v-for="(action, i) in actions">
<gl-link
v-if="action.type === 'link'"
:key="i"
:href="action.path"
:class="action.cssClass"
>
{{ action.label }}
</gl-link>
<gl-link
v-else-if="action.type === 'ujs-link'"
:key="i"
:href="action.path"
:class="action.cssClass"
data-method="post"
rel="nofollow"
>
{{ action.label }}
</gl-link>
<loading-button
v-else-if="action.type === 'button'"
:key="i"
:loading="action.isLoading"
:disabled="action.isLoading"
......
......@@ -80,6 +80,12 @@ class Projects::PipelinesController < Projects::ApplicationController
end
end
def destroy
::Ci::DestroyPipelineService.new(project, current_user).execute(pipeline)
redirect_to project_pipelines_path(project), status: :see_other
end
def builds
render_show
end
......
......@@ -17,7 +17,7 @@ class PipelinesFinder
return Ci::Pipeline.none
end
items = pipelines.no_child
items = pipelines
items = by_scope(items)
items = by_status(items)
items = by_ref(items)
......
......@@ -54,10 +54,6 @@ module Ci
def to_partial_path
'projects/generic_commit_statuses/generic_commit_status'
end
def yaml_for_downstream
nil
end
end
end
......
......@@ -61,9 +61,7 @@ module Ci
has_one :chat_data, class_name: 'Ci::PipelineChatData'
has_many :triggered_pipelines, through: :sourced_pipelines, source: :pipeline
has_many :child_pipelines, -> { merge(Ci::Sources::Pipeline.same_project) }, through: :sourced_pipelines, source: :pipeline
has_one :triggered_by_pipeline, through: :source_pipeline, source: :source_pipeline
has_one :parent_pipeline, -> { merge(Ci::Sources::Pipeline.same_project) }, through: :source_pipeline, source: :source_pipeline
has_one :source_job, through: :source_pipeline, source: :source_job
has_one :pipeline_config, class_name: 'Ci::PipelineConfig', inverse_of: :pipeline
......@@ -215,7 +213,6 @@ module Ci
end
scope :internal, -> { where(source: internal_sources) }
scope :no_child, -> { where.not(source: :parent_pipeline) }
scope :ci_sources, -> { where(config_source: ::Ci::PipelineEnums.ci_config_sources_values) }
scope :for_user, -> (user) { where(user: user) }
scope :for_sha, -> (sha) { where(sha: sha) }
......@@ -511,6 +508,10 @@ module Ci
builds.skipped.after_stage(stage_idx).find_each(&:process)
end
def child?
false
end
def latest?
return false unless git_ref && commit.present?
......@@ -693,24 +694,6 @@ module Ci
all_merge_requests.order(id: :desc)
end
# If pipeline is a child of another pipeline, include the parent
# and the siblings, otherwise return only itself.
def same_family_pipeline_ids
if (parent = parent_pipeline)
[parent.id] + parent.child_pipelines.pluck(:id)
else
[self.id]
end
end
def child?
parent_pipeline.present?
end
def parent?
child_pipelines.exists?
end
def detailed_status(current_user)
Gitlab::Ci::Status::Pipeline::Factory
.new(self, current_user)
......
......@@ -23,11 +23,10 @@ module Ci
schedule: 4,
api: 5,
external: 6,
cross_project_pipeline: 7,
pipeline: 7,
chat: 8,
merge_request_event: 10,
external_pull_request_event: 11,
parent_pipeline: 12
external_pull_request_event: 11
}
end
......@@ -39,8 +38,7 @@ module Ci
repository_source: 1,
auto_devops_source: 2,
remote_source: 4,
external_project_source: 5,
bridge_source: 6
external_project_source: 5
}
end
......
......@@ -18,8 +18,6 @@ module Ci
validates :source_project, presence: true
validates :source_job, presence: true
validates :source_pipeline, presence: true
scope :same_project, -> { where(arel_table[:source_project_id].eq(arel_table[:project_id])) }
end
end
end
......
# frozen_string_literal: true
class PipelineDetailsEntity < PipelineEntity
expose :project, using: ProjectEntity
expose :flags do
expose :latest?, as: :latest
end
......
......@@ -77,6 +77,10 @@ class PipelineEntity < Grape::Entity
cancel_project_pipeline_path(pipeline.project, pipeline)
end
expose :delete_path, if: -> (*) { can_delete? } do |pipeline|
project_pipeline_path(pipeline.project, pipeline)
end
expose :failed_builds, if: -> (*) { can_retry? }, using: JobEntity do |pipeline|
pipeline.failed_builds
end
......@@ -95,6 +99,10 @@ class PipelineEntity < Grape::Entity
pipeline.cancelable?
end
def can_delete?
can?(request.current_user, :destroy_pipeline, pipeline)
end
def has_presentable_merge_request?
pipeline.triggered_by_merge_request? &&
can?(request.current_user, :read_merge_request, pipeline.merge_request)
......
......@@ -41,7 +41,6 @@ class PipelineSerializer < BaseSerializer
def preloaded_relations
[
:latest_statuses_ordered_by_stage,
:project,
:stages,
{
failed_builds: %i(project metadata)
......
......@@ -23,7 +23,7 @@ module Ci
Gitlab::Ci::Pipeline::Chain::Limit::JobActivity].freeze
# rubocop: disable Metrics/ParameterLists
def execute(source, ignore_skip_ci: false, save_on_errors: true, trigger_request: nil, schedule: nil, merge_request: nil, external_pull_request: nil, bridge: nil, **options, &block)
def execute(source, ignore_skip_ci: false, save_on_errors: true, trigger_request: nil, schedule: nil, merge_request: nil, external_pull_request: nil, **options, &block)
@pipeline = Ci::Pipeline.new
command = Gitlab::Ci::Pipeline::Chain::Command.new(
......@@ -46,7 +46,6 @@ module Ci
current_user: current_user,
push_options: params[:push_options] || {},
chat_data: params[:chat_data],
bridge: bridge,
**extra_options(options))
sequence = Gitlab::Ci::Pipeline::Chain::Sequence
......@@ -105,14 +104,14 @@ module Ci
if Feature.enabled?(:ci_support_interruptible_pipelines, project, default_enabled: true)
project.ci_pipelines
.where(ref: pipeline.ref)
.where.not(id: pipeline.same_family_pipeline_ids)
.where.not(id: pipeline.id)
.where.not(sha: project.commit(pipeline.ref).try(:id))
.alive_or_scheduled
.with_only_interruptible_builds
else
project.ci_pipelines
.where(ref: pipeline.ref)
.where.not(id: pipeline.same_family_pipeline_ids)
.where.not(id: pipeline.id)
.where.not(sha: project.commit(pipeline.ref).try(:id))
.created_or_pending
end
......
......@@ -44,7 +44,7 @@ module Ci
return error("400 Job has to be running", 400) unless job.running?
pipeline = Ci::CreatePipelineService.new(project, job.user, ref: params[:ref])
.execute(:cross_project_pipeline, ignore_skip_ci: true) do |pipeline|
.execute(:pipeline, ignore_skip_ci: true) do |pipeline|
source = job.sourced_pipelines.build(
source_pipeline: job.pipeline,
source_project: job.project,
......
---
title: Fix Delete Selected button being active after uploading designs after a deletion
merge_request: 22516
author:
type: fixed
---
title: Allow an upstream pipeline to create a downstream pipeline in the same project
merge_request: 20930
author:
type: added
---
title: Add pipeline deletion button to pipeline details page
merge_request: 21365
author: Fabio Huser
type: added
......@@ -343,7 +343,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
draw :merge_requests
end
resources :pipelines, only: [:index, :new, :create, :show] do
resources :pipelines, only: [:index, :new, :create, :show, :destroy] do
collection do
resource :pipelines_settings, path: 'settings', only: [:show, :update]
get :charts
......
......@@ -305,12 +305,14 @@ For example, the query string
### Accessing pipelines
You can find the current and historical pipeline runs under your project's
**CI/CD > Pipelines** page. Clicking on a pipeline will show the jobs that were run for
that pipeline.
**CI/CD > Pipelines** page. You can also access pipelines for a merge request by navigating
to its **Pipelines** tab.
![Pipelines index page](img/pipelines_index.png)
You can also access pipelines for a merge request by navigating to its **Pipelines** tab.
Clicking on a pipeline will bring you to the **Pipeline Details** page and show
the jobs that were run for that pipeline. From here you can cancel a running pipeline,
retry jobs on a failed pipeline, or [delete a pipeline](#deleting-a-single-pipeline).
### Accessing individual jobs
......@@ -410,6 +412,20 @@ This functionality is only available:
- For users with at least Developer access.
- If the the stage contains [manual actions](#manual-actions-from-pipeline-graphs).
### Deleting a single pipeline
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/24851) in GitLab 12.7.
Users with [owner permissions](../user/permissions.md) in a project can delete a pipeline
by clicking on the pipeline in the **CI/CD > Pipelines** to get to the **Pipeline Details**
page, then using the **Delete** button.
![Pipeline Delete Button](img/pipeline-delete.png)
CAUTION: **Warning:**
Deleting a pipeline will expire all pipeline caches, and delete all related objects,
such as builds, logs, artifacts, and triggers. **This action cannot be undone.**
## Most Recent Pipeline
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/issues/50499) in GitLab 12.3.
......
......@@ -98,6 +98,10 @@ This was originally implemented in: <https://gitlab.com/gitlab-org/gitlab-foss/m
- Memory is through the roof! (TL;DR: Load images but block images requests!): <https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/12003>
#### Capybara expectation times out
- [Test imports a project (via Sidekiq) that is growing over time, leading to timeouts when the import takes longer than 60 seconds](https://gitlab.com/gitlab-org/gitlab/merge_requests/22599)
## Resources
- [Flaky Tests: Are You Sure You Want to Rerun Them?](http://semaphoreci.com/blog/2017/04/20/flaky-tests.html)
......
......@@ -10,7 +10,7 @@ module Gitlab
:trigger_request, :schedule, :merge_request, :external_pull_request,
:ignore_skip_ci, :save_incompleted,
:seeds_block, :variables_attributes, :push_options,
:chat_data, :allow_mirror_update, :bridge,
:chat_data, :allow_mirror_update,
# These attributes are set by Chains during processing:
:config_content, :config_processor, :stage_seeds
) do
......
......@@ -9,7 +9,7 @@ module Gitlab
include Chain::Helpers
SOURCES = [
Gitlab::Ci::Pipeline::Chain::Config::Content::Bridge,
Gitlab::Ci::Pipeline::Chain::Config::Content::Runtime,
Gitlab::Ci::Pipeline::Chain::Config::Content::Repository,
Gitlab::Ci::Pipeline::Chain::Config::Content::ExternalProject,
Gitlab::Ci::Pipeline::Chain::Config::Content::Remote,
......@@ -17,7 +17,7 @@ module Gitlab
].freeze
LEGACY_SOURCES = [
Gitlab::Ci::Pipeline::Chain::Config::Content::Bridge,
Gitlab::Ci::Pipeline::Chain::Config::Content::Runtime,
Gitlab::Ci::Pipeline::Chain::Config::Content::LegacyRepository,
Gitlab::Ci::Pipeline::Chain::Config::Content::LegacyAutoDevops
].freeze
......
......@@ -6,15 +6,20 @@ module Gitlab
module Chain
module Config
class Content
class Bridge < Source
class Runtime < Source
def content
return unless @command.bridge
@command.bridge.yaml_for_downstream
@command.config_content
end
def source
:bridge_source
# The only case when this source is used is when the config content
# is passed in as parameter to Ci::CreatePipelineService.
# This would only occur with parent/child pipelines which is being
# implemented.
# TODO: change source to return :runtime_source
# https://gitlab.com/gitlab-org/gitlab/merge_requests/21041
nil
end
end
end
......
......@@ -1631,6 +1631,9 @@ msgstr ""
msgid "An error occurred while deleting the comment"
msgstr ""
msgid "An error occurred while deleting the pipeline."
msgstr ""
msgid "An error occurred while detecting host keys"
msgstr ""
......@@ -2092,6 +2095,9 @@ msgstr ""
msgid "Are you sure you want to delete this pipeline schedule?"
msgstr ""
msgid "Are you sure you want to delete this pipeline? Doing so will expire all pipeline caches and delete all related objects, such as builds, logs, artifacts, and triggers. This action cannot be undone."
msgstr ""
msgid "Are you sure you want to erase this build?"
msgstr ""
......@@ -3266,6 +3272,9 @@ msgstr ""
msgid "Checkout"
msgstr ""
msgid "Checkout|%{selectedPlanText} plan"
msgstr ""
msgid "Checkout|1. Your profile"
msgstr ""
......@@ -3278,9 +3287,36 @@ msgstr ""
msgid "Checkout|Checkout"
msgstr ""
msgid "Checkout|Continue to billing"
msgstr ""
msgid "Checkout|Edit"
msgstr ""
msgid "Checkout|GitLab plan"
msgstr ""
msgid "Checkout|Group"
msgstr ""
msgid "Checkout|Name of company or organization using GitLab"
msgstr ""
msgid "Checkout|Need more users? Purchase GitLab for your %{company}."
msgstr ""
msgid "Checkout|Number of users"
msgstr ""
msgid "Checkout|Subscription details"
msgstr ""
msgid "Checkout|Users"
msgstr ""
msgid "Checkout|company or team"
msgstr ""
msgid "Cherry-pick this commit"
msgstr ""
......@@ -5735,6 +5771,9 @@ msgstr ""
msgid "Delete list"
msgstr ""
msgid "Delete pipeline"
msgstr ""
msgid "Delete snippet"
msgstr ""
......@@ -18098,6 +18137,9 @@ msgstr ""
msgid "The phase of the development lifecycle."
msgstr ""
msgid "The pipeline has been deleted"
msgstr ""
msgid "The pipelines schedule runs pipelines in the future, repeatedly, for specific branches or tags. Those scheduled pipelines will inherit limited project access based on their associated user."
msgstr ""
......
......@@ -740,4 +740,51 @@ describe Projects::PipelinesController do
expect(response).to have_gitlab_http_status(404)
end
end
describe 'DELETE #destroy' do
let!(:project) { create(:project, :private, :repository) }
let!(:pipeline) { create(:ci_pipeline, :failed, project: project) }
let!(:build) { create(:ci_build, :failed, pipeline: pipeline) }
context 'when user has ability to delete pipeline' do
before do
sign_in(project.owner)
end
it 'deletes pipeline and redirects' do
delete_pipeline
expect(response).to have_gitlab_http_status(303)
expect(Ci::Build.exists?(build.id)).to be_falsy
expect(Ci::Pipeline.exists?(pipeline.id)).to be_falsy
end
context 'and builds are disabled' do
let(:feature) { ProjectFeature::DISABLED }
it 'fails to delete pipeline' do
delete_pipeline
expect(response).to have_gitlab_http_status(404)
end
end
end
context 'when user has no privileges' do
it 'fails to delete pipeline' do
delete_pipeline
expect(response).to have_gitlab_http_status(403)
end
end
def delete_pipeline
delete :destroy, params: {
namespace_id: project.namespace,
project_id: project,
id: pipeline.id
}
end
end
end
......@@ -24,17 +24,17 @@ describe 'Import multiple repositories by uploading a manifest file', :js do
expect(page).to have_content('https://android-review.googlesource.com/platform/build/blueprint')
end
it 'imports successfully imports a project', :sidekiq_might_not_need_inline do
it 'imports successfully imports a project', :sidekiq_inline do
visit new_import_manifest_path
attach_file('manifest', Rails.root.join('spec/fixtures/aosp_manifest.xml'))
click_on 'List available repositories'
page.within(first_row) do
page.within(second_row) do
click_on 'Import'
expect(page).to have_content 'Done'
expect(page).to have_content("#{group.full_path}/build/make")
expect(page).to have_content("#{group.full_path}/build/blueprint")
end
end
......@@ -47,7 +47,7 @@ describe 'Import multiple repositories by uploading a manifest file', :js do
expect(page).to have_content 'The uploaded file is not a valid XML file.'
end
def first_row
page.all('table.import-jobs tbody tr')[0]
def second_row
page.all('table.import-jobs tbody tr')[1]
end
end
......@@ -59,7 +59,8 @@ describe 'Pipeline', :js do
describe 'GET /:project/pipelines/:id' do
include_context 'pipeline builds'
let(:project) { create(:project, :repository) }
let(:group) { create(:group) }
let(:project) { create(:project, :repository, group: group) }
let(:pipeline) { create(:ci_pipeline, project: project, ref: 'master', sha: project.commit.id, user: user) }
subject(:visit_pipeline) { visit project_pipeline_path(project, pipeline) }
......@@ -329,6 +330,32 @@ describe 'Pipeline', :js do
end
end
context 'deleting pipeline' do
context 'when user can not delete' do
before do
visit_pipeline
end
it { expect(page).not_to have_button('Delete') }
end
context 'when deleting' do
before do
group.add_owner(user)
visit_pipeline
click_button 'Delete'
click_button 'Delete pipeline'
end
it 'redirects to pipeline overview page', :sidekiq_might_not_need_inline do
expect(page).to have_content('The pipeline has been deleted')
expect(current_path).to eq(project_pipelines_path(project))
end
end
end
context 'when pipeline ref does not exist in repository anymore' do
let(:pipeline) do
create(:ci_empty_pipeline, project: project,
......
......@@ -64,19 +64,6 @@ describe PipelinesFinder do
end
end
context 'when project has child pipelines' do
let!(:parent_pipeline) { create(:ci_pipeline, project: project) }
let!(:child_pipeline) { create(:ci_pipeline, project: project, source: :parent_pipeline) }
let!(:pipeline_source) do
create(:ci_sources_pipeline, pipeline: child_pipeline, source_pipeline: parent_pipeline)
end
it 'filters out child pipelines and show only the parents' do
is_expected.to eq([parent_pipeline])
end
end
HasStatus::AVAILABLE_STATUSES.each do |target|
context "when status is #{target}" do
let(:params) { { status: target } }
......
import { shallowMount, createLocalVue } from '@vue/test-utils';
import { shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import ProjectFormGroup from '~/confidential_merge_request/components/project_form_group.vue';
const localVue = createLocalVue();
const mockData = [
{
id: 1,
......@@ -30,7 +29,6 @@ function factory(projects = mockData) {
mock.onGet(/api\/(.*)\/projects\/gitlab-org%2Fgitlab-ce\/forks/).reply(200, projects);
vm = shallowMount(ProjectFormGroup, {
localVue,
propsData: {
namespacePath: 'gitlab-org',
projectPath: 'gitlab-org/gitlab-ce',
......@@ -49,7 +47,7 @@ describe('Confidential merge request project form group component', () => {
it('renders fork dropdown', () => {
factory();
return localVue.nextTick(() => {
return vm.vm.$nextTick(() => {
expect(vm.element).toMatchSnapshot();
});
});
......@@ -57,7 +55,7 @@ describe('Confidential merge request project form group component', () => {
it('sets selected project as first fork', () => {
factory();
return localVue.nextTick(() => {
return vm.vm.$nextTick(() => {
expect(vm.vm.selectedProject).toEqual({
id: 1,
name: 'root / gitlab-ce',
......@@ -70,7 +68,7 @@ describe('Confidential merge request project form group component', () => {
it('renders empty state when response is empty', () => {
factory([]);
return localVue.nextTick(() => {
return vm.vm.$nextTick(() => {
expect(vm.element).toMatchSnapshot();
});
});
......
import Vue from 'vue';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import { shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import { createStore } from '~/contributors/stores';
import axios from '~/lib/utils/axios_utils';
import ContributorsCharts from '~/contributors/components/contributors.vue';
const localVue = createLocalVue();
let wrapper;
let mock;
let store;
......@@ -52,7 +51,7 @@ describe('Contributors charts', () => {
it('should display loader whiled loading data', () => {
wrapper.vm.$store.state.loading = true;
return localVue.nextTick(() => {
return wrapper.vm.$nextTick(() => {
expect(wrapper.find('.contributors-loader').exists()).toBe(true);
});
});
......@@ -60,7 +59,7 @@ describe('Contributors charts', () => {
it('should render charts when loading completed and there is chart data', () => {
wrapper.vm.$store.state.loading = false;
wrapper.vm.$store.state.chartData = chartData;
return localVue.nextTick(() => {
return wrapper.vm.$nextTick(() => {
expect(wrapper.find('.contributors-loader').exists()).toBe(false);
expect(wrapper.find('.contributors-charts').exists()).toBe(true);
expect(wrapper.element).toMatchSnapshot();
......
import { shallowMount, createLocalVue } from '@vue/test-utils';
import { shallowMount } from '@vue/test-utils';
import DiffGutterAvatars from '~/diffs/components/diff_gutter_avatars.vue';
import discussionsMockData from '../mock_data/diff_discussions';
const localVue = createLocalVue();
const getDiscussionsMockData = () => [Object.assign({}, discussionsMockData)];
describe('DiffGutterAvatars', () => {
......@@ -14,7 +13,6 @@ describe('DiffGutterAvatars', () => {
const createComponent = (props = {}) => {
wrapper = shallowMount(DiffGutterAvatars, {
localVue,
propsData: {
...props,
},
......
import { shallowMount, createLocalVue } from '@vue/test-utils';
import { shallowMount } from '@vue/test-utils';
import EditButton from '~/diffs/components/edit_button.vue';
const localVue = createLocalVue();
const editPath = 'test-path';
describe('EditButton', () => {
......@@ -9,7 +8,6 @@ describe('EditButton', () => {
const createComponent = (props = {}) => {
wrapper = shallowMount(EditButton, {
localVue,
propsData: { ...props },
sync: false,
attachToDocument: true,
......
import { shallowMount, createLocalVue } from '@vue/test-utils';
import { shallowMount } from '@vue/test-utils';
import HiddenFilesWarning from '~/diffs/components/hidden_files_warning.vue';
const localVue = createLocalVue();
const propsData = {
total: '10',
visible: 5,
......@@ -14,7 +13,6 @@ describe('HiddenFilesWarning', () => {
const createComponent = () => {
wrapper = shallowMount(HiddenFilesWarning, {
localVue,
sync: false,
propsData,
});
......
......@@ -13,7 +13,7 @@ describe('Diff no changes empty state', () => {
const store = createStore();
extendStore(store);
vm = shallowMount(localVue.extend(NoChanges), {
vm = shallowMount(NoChanges, {
localVue,
store,
propsData: {
......
......@@ -25,7 +25,7 @@ describe('ide/components/ide_status_list', () => {
},
});
wrapper = shallowMount(localVue.extend(IdeStatusList), {
wrapper = shallowMount(IdeStatusList, {
localVue,
sync: false,
store,
......
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import { shallowMount } from '@vue/test-utils';
import { GlEmptyState, GlPagination, GlSkeletonLoading } from '@gitlab/ui';
import waitForPromises from 'helpers/wait_for_promises';
import { TEST_HOST } from 'helpers/test_constants';
......@@ -18,8 +18,6 @@ const TEST_ENDPOINT = '/issues';
const TEST_CREATE_ISSUES_PATH = '/createIssue';
const TEST_EMPTY_SVG_PATH = '/emptySvg';
const localVue = createLocalVue();
const MOCK_ISSUES = Array(PAGE_SIZE_MANUAL)
.fill(0)
.map((_, i) => ({
......@@ -40,14 +38,13 @@ describe('Issuables list component', () => {
};
const factory = (props = { sortKey: 'priority' }) => {
wrapper = shallowMount(localVue.extend(IssuablesListApp), {
wrapper = shallowMount(IssuablesListApp, {
propsData: {
endpoint: TEST_ENDPOINT,
createIssuePath: TEST_CREATE_ISSUES_PATH,
emptySvgPath: TEST_EMPTY_SVG_PATH,
...props,
},
localVue,
sync: false,
attachToDocument: true,
});
......
import { shallowMount, createLocalVue } from '@vue/test-utils';
import { shallowMount } from '@vue/test-utils';
import { GlLink } from '@gitlab/ui';
import PinnedLinks from '~/issue_show/components/pinned_links.vue';
const localVue = createLocalVue();
const plainZoomUrl = 'https://zoom.us/j/123456789';
describe('PinnedLinks', () => {
......@@ -19,8 +17,7 @@ describe('PinnedLinks', () => {
};
const createComponent = props => {
wrapper = shallowMount(localVue.extend(PinnedLinks), {
localVue,
wrapper = shallowMount(PinnedLinks, {
sync: false,
propsData: {
zoomMeetingUrl: null,
......
import { shallowMount, createLocalVue } from '@vue/test-utils';
import { shallowMount } from '@vue/test-utils';
import { GlColumnChart } from '@gitlab/ui/dist/charts';
import ColumnChart from '~/monitoring/components/charts/column.vue';
const localVue = createLocalVue();
jest.mock('~/lib/utils/icon_utils', () => ({
getSvgIconPathContent: jest.fn().mockResolvedValue('mockSvgPathContent'),
}));
......@@ -12,7 +10,7 @@ describe('Column component', () => {
let columnChart;
beforeEach(() => {
columnChart = shallowMount(localVue.extend(ColumnChart), {
columnChart = shallowMount(ColumnChart, {
propsData: {
graphData: {
metrics: [
......@@ -35,7 +33,6 @@ describe('Column component', () => {
containerWidth: 100,
},
sync: false,
localVue,
});
});
......
import { shallowMount, createLocalVue } from '@vue/test-utils';
import { shallowMount } from '@vue/test-utils';
import EmptyChart from '~/monitoring/components/charts/empty_chart.vue';
const localVue = createLocalVue();
describe('Empty Chart component', () => {
let emptyChart;
const graphTitle = 'Memory Usage';
beforeEach(() => {
emptyChart = shallowMount(localVue.extend(EmptyChart), {
emptyChart = shallowMount(EmptyChart, {
propsData: {
graphTitle,
},
sync: false,
localVue,
});
});
......
import { shallowMount, createLocalVue } from '@vue/test-utils';
import { shallowMount } from '@vue/test-utils';
import SingleStatChart from '~/monitoring/components/charts/single_stat.vue';
import { graphDataPrometheusQuery } from '../../mock_data';
const localVue = createLocalVue();
describe('Single Stat Chart component', () => {
let singleStatChart;
beforeEach(() => {
singleStatChart = shallowMount(localVue.extend(SingleStatChart), {
singleStatChart = shallowMount(SingleStatChart, {
propsData: {
graphData: graphDataPrometheusQuery,
},
sync: false,
localVue,
});
});
......
import { mount, shallowMount, createLocalVue } from '@vue/test-utils';
import { mount, shallowMount } from '@vue/test-utils';
import { GlButton, GlLink, GlFormGroup, GlFormInput } from '@gitlab/ui';
import { TEST_HOST } from 'helpers/test_constants';
import ExternalDashboard from '~/operation_settings/components/external_dashboard.vue';
......@@ -15,12 +15,10 @@ describe('operation settings external dashboard component', () => {
const operationsSettingsEndpoint = `${TEST_HOST}/mock/ops/settings/endpoint`;
const externalDashboardUrl = `http://mock-external-domain.com/external/dashboard/url`;
const externalDashboardHelpPagePath = `${TEST_HOST}/help/page/path`;
const localVue = createLocalVue();
const mountComponent = (shallow = true) => {
const config = [
ExternalDashboard,
{
localVue,
store: store({
operationsSettingsEndpoint,
externalDashboardUrl,
......
import Vue from 'vue';
import { mount } from '@vue/test-utils';
import { TEST_HOST } from 'helpers/test_constants';
import registry from '~/registry/list/components/app.vue';
......@@ -35,12 +34,8 @@ describe('Registry List', () => {
};
beforeEach(() => {
// This is needed due to console.error called by vue to emit a warning that stop the tests.
// See https://github.com/vuejs/vue-test-utils/issues/532.
Vue.config.silent = true;
wrapper = mount(registry, {
attachToDocument: true,
sync: false,
propsData,
computed: {
repos() {
......@@ -52,7 +47,6 @@ describe('Registry List', () => {
});
afterEach(() => {
Vue.config.silent = false;
wrapper.destroy();
});
......@@ -138,7 +132,7 @@ describe('Registry List', () => {
wrapper = mount(registry, {
propsData: {
...propsData,
endpoint: null,
endpoint: '',
isGroupPage,
},
methods,
......@@ -146,7 +140,7 @@ describe('Registry List', () => {
});
it('call the right vuex setters', () => {
expect(methods.setMainEndpoint).toHaveBeenLastCalledWith(null);
expect(methods.setMainEndpoint).toHaveBeenLastCalledWith('');
expect(methods.setIsDeleteDisabled).toHaveBeenLastCalledWith(true);
});
......
import Vue from 'vue';
import Vuex from 'vuex';
import { mount, createLocalVue } from '@vue/test-utils';
import createFlash from '~/flash';
......@@ -28,14 +27,10 @@ describe('collapsible registry container', () => {
store,
localVue,
attachToDocument: true,
sync: false,
});
beforeEach(() => {
createFlash.mockClear();
// This is needed due to console.error called by vue to emit a warning that stop the tests
// see https://github.com/vuejs/vue-test-utils/issues/532
Vue.config.silent = true;
store = new Vuex.Store({
state: {
isDeleteDisabled: false,
......@@ -51,7 +46,6 @@ describe('collapsible registry container', () => {
});
afterEach(() => {
Vue.config.silent = false;
wrapper.destroy();
});
......@@ -72,25 +66,23 @@ describe('collapsible registry container', () => {
expectIsClosed();
});
it('should be open when user clicks on closed repo', done => {
it('should be open when user clicks on closed repo', () => {
const toggleRepos = findToggleRepos();
toggleRepos.at(0).trigger('click');
Vue.nextTick(() => {
return wrapper.vm.$nextTick().then(() => {
const container = findContainerImageTags();
expect(container.exists()).toBe(true);
expect(wrapper.vm.fetchList).toHaveBeenCalled();
done();
});
});
it('should be closed when the user clicks on an opened repo', done => {
it('should be closed when the user clicks on an opened repo', () => {
const toggleRepos = findToggleRepos();
toggleRepos.at(0).trigger('click');
Vue.nextTick(() => {
return wrapper.vm.$nextTick().then(() => {
toggleRepos.at(0).trigger('click');
Vue.nextTick(() => {
wrapper.vm.$nextTick(() => {
expectIsClosed();
done();
});
});
});
......
import Vue from 'vue';
import Vuex from 'vuex';
import { mount, createLocalVue } from '@vue/test-utils';
import createFlash from '~/flash';
......@@ -29,13 +28,9 @@ describe('table registry', () => {
const bulkDeletePath = 'path';
const mountWithStore = config =>
mount(tableRegistry, { ...config, store, localVue, attachToDocument: true, sync: false });
mount(tableRegistry, { ...config, store, localVue, attachToDocument: true });
beforeEach(() => {
// This is needed due to console.error called by vue to emit a warning that stop the tests
// see https://github.com/vuejs/vue-test-utils/issues/532
Vue.config.silent = true;
store = new Vuex.Store({
state: {
isDeleteDisabled: false,
......@@ -52,7 +47,6 @@ describe('table registry', () => {
});
afterEach(() => {
Vue.config.silent = false;
wrapper.destroy();
});
......@@ -82,53 +76,53 @@ describe('table registry', () => {
});
describe('multi select', () => {
it('selecting a row should enable delete button', done => {
it('selecting a row should enable delete button', () => {
const deleteBtn = findDeleteButton();
const checkboxes = findSelectCheckboxes();
expect(deleteBtn.attributes('disabled')).toBe('disabled');
checkboxes.at(0).trigger('click');
Vue.nextTick(() => {
return wrapper.vm.$nextTick().then(() => {
expect(deleteBtn.attributes('disabled')).toEqual(undefined);
done();
});
});
it('selecting all checkbox should select all rows and enable delete button', done => {
it('selecting all checkbox should select all rows and enable delete button', () => {
const selectAll = findSelectAllCheckbox();
selectAll.trigger('click');
Vue.nextTick(() => {
return wrapper.vm.$nextTick().then(() => {
const checkboxes = findSelectCheckboxes();
const checked = checkboxes.filter(w => w.element.checked);
expect(checked.length).toBe(checkboxes.length);
done();
});
});
it('deselecting select all checkbox should deselect all rows and disable delete button', done => {
it('deselecting select all checkbox should deselect all rows and disable delete button', () => {
const checkboxes = findSelectCheckboxes();
const selectAll = findSelectAllCheckbox();
selectAll.trigger('click');
selectAll.trigger('click');
Vue.nextTick(() => {
return wrapper.vm.$nextTick().then(() => {
const checked = checkboxes.filter(w => !w.element.checked);
expect(checked.length).toBe(checkboxes.length);
done();
});
});
it('should delete multiple items when multiple items are selected', done => {
it('should delete multiple items when multiple items are selected', () => {
const multiDeleteItems = jest.fn().mockResolvedValue();
wrapper.setMethods({ multiDeleteItems });
Vue.nextTick(() => {
const selectAll = findSelectAllCheckbox();
selectAll.trigger('click');
Vue.nextTick(() => {
return wrapper.vm
.$nextTick()
.then(() => {
const selectAll = findSelectAllCheckbox();
selectAll.trigger('click');
return wrapper.vm.$nextTick();
})
.then(() => {
const deleteBtn = findDeleteButton();
expect(wrapper.vm.selectedItems).toEqual([0, 1]);
expect(deleteBtn.attributes('disabled')).toEqual(undefined);
......@@ -140,9 +134,7 @@ describe('table registry', () => {
path: bulkDeletePath,
items: [firstImage.tag, secondImage.tag],
});
done();
});
});
});
it('should show an error message if bulkDeletePath is not set', () => {
......
import { mount, createLocalVue } from '@vue/test-utils';
import { mount } from '@vue/test-utils';
import { GlLink } from '@gitlab/ui';
import { truncateSha } from '~/lib/utils/text_utility';
import Icon from '~/vue_shared/components/icon.vue';
......@@ -10,10 +10,7 @@ describe('Evidence Block', () => {
let wrapper;
const factory = (options = {}) => {
const localVue = createLocalVue();
wrapper = mount(localVue.extend(EvidenceBlock), {
localVue,
wrapper = mount(EvidenceBlock, {
...options,
});
};
......
import { createLocalVue, shallowMount } from '@vue/test-utils';
import { shallowMount } from '@vue/test-utils';
import environmentRowComponent from '~/serverless/components/environment_row.vue';
import { mockServerlessFunctions, mockServerlessFunctionsDiffEnv } from '../mock_data';
import { translate } from '~/serverless/utils';
const createComponent = (localVue, env, envName) =>
shallowMount(environmentRowComponent, { localVue, propsData: { env, envName }, sync: false }).vm;
const createComponent = (env, envName) =>
shallowMount(environmentRowComponent, { propsData: { env, envName }, sync: false }).vm;
describe('environment row component', () => {
describe('default global cluster case', () => {
let localVue;
let vm;
beforeEach(() => {
localVue = createLocalVue();
vm = createComponent(localVue, translate(mockServerlessFunctions.functions)['*'], '*');
vm = createComponent(translate(mockServerlessFunctions.functions)['*'], '*');
});
afterEach(() => vm.$destroy());
......@@ -44,15 +42,9 @@ describe('environment row component', () => {
describe('default named cluster case', () => {
let vm;
let localVue;
beforeEach(() => {
localVue = createLocalVue();
vm = createComponent(
localVue,
translate(mockServerlessFunctionsDiffEnv.functions).test,
'test',
);
vm = createComponent(translate(mockServerlessFunctionsDiffEnv.functions).test, 'test');
});
afterEach(() => vm.$destroy());
......
......@@ -34,6 +34,7 @@ describe('Pipeline details header', () => {
avatar_url: 'link',
},
retry_path: 'path',
delete_path: 'path',
},
isLoading: false,
};
......@@ -55,12 +56,22 @@ describe('Pipeline details header', () => {
});
describe('action buttons', () => {
it('should call postAction when button action is clicked', () => {
it('should call postAction when retry button action is clicked', done => {
eventHub.$on('headerPostAction', action => {
expect(action.path).toEqual('path');
done();
});
vm.$el.querySelector('button').click();
vm.$el.querySelector('.js-retry-button').click();
});
it('should fire modal event when delete button action is clicked', done => {
vm.$root.$on('bv::modal::show', action => {
expect(action.componentId).toEqual('pipeline-delete-modal');
done();
});
vm.$el.querySelector('.js-btn-delete-pipeline').click();
});
});
});
......@@ -31,17 +31,9 @@ describe('Header CI Component', () => {
{
label: 'Retry',
path: 'path',
type: 'button',
cssClass: 'btn',
isLoading: false,
},
{
label: 'Go',
path: 'path',
type: 'link',
cssClass: 'link',
isLoading: false,
},
],
hasSidebarButton: true,
};
......@@ -77,11 +69,10 @@ describe('Header CI Component', () => {
});
it('should render provided actions', () => {
expect(vm.$el.querySelector('.btn').tagName).toEqual('BUTTON');
expect(vm.$el.querySelector('.btn').textContent.trim()).toEqual(props.actions[0].label);
expect(vm.$el.querySelector('.link').tagName).toEqual('A');
expect(vm.$el.querySelector('.link').textContent.trim()).toEqual(props.actions[1].label);
expect(vm.$el.querySelector('.link').getAttribute('href')).toEqual(props.actions[0].path);
const btn = vm.$el.querySelector('.btn');
expect(btn.tagName).toEqual('BUTTON');
expect(btn.textContent.trim()).toEqual(props.actions[0].label);
});
it('should show loading icon', done => {
......
......@@ -15,42 +15,6 @@ describe Gitlab::Ci::Pipeline::Chain::Config::Content do
stub_feature_flags(ci_root_config_content: false)
end
context 'when bridge job is passed in as parameter' do
let(:ci_config_path) { nil }
let(:bridge) { create(:ci_bridge) }
before do
command.bridge = bridge
end
context 'when bridge job has downstream yaml' do
before do
allow(bridge).to receive(:yaml_for_downstream).and_return('the-yaml')
end
it 'returns the content already available in command' do
subject.perform!
expect(pipeline.config_source).to eq 'bridge_source'
expect(command.config_content).to eq 'the-yaml'
end
end
context 'when bridge job does not have downstream yaml' do
before do
allow(bridge).to receive(:yaml_for_downstream).and_return(nil)
end
it 'returns the next available source' do
subject.perform!
expect(pipeline.config_source).to eq 'auto_devops_source'
template = Gitlab::Template::GitlabCiYmlTemplate.find('Beta/Auto-DevOps')
expect(command.config_content).to eq(template.content)
end
end
end
context 'when config is defined in a custom path in the repository' do
let(:ci_config_path) { 'path/to/config.yml' }
......@@ -171,23 +135,6 @@ describe Gitlab::Ci::Pipeline::Chain::Config::Content do
end
end
context 'when bridge job is passed in as parameter' do
let(:ci_config_path) { nil }
let(:bridge) { create(:ci_bridge) }
before do
command.bridge = bridge
allow(bridge).to receive(:yaml_for_downstream).and_return('the-yaml')
end
it 'returns the content already available in command' do
subject.perform!
expect(pipeline.config_source).to eq 'bridge_source'
expect(command.config_content).to eq 'the-yaml'
end
end
context 'when config is defined in a custom path in the repository' do
let(:ci_config_path) { 'path/to/config.yml' }
let(:config_content_result) do
......
......@@ -201,8 +201,6 @@ ci_pipelines:
- sourced_pipelines
- triggered_by_pipeline
- triggered_pipelines
- child_pipelines
- parent_pipeline
- downstream_bridges
- job_artifacts
- vulnerabilities_occurrence_pipelines
......
......@@ -2716,114 +2716,4 @@ describe Ci::Pipeline, :mailer do
end
end
end
describe '#parent_pipeline' do
let(:project) { create(:project) }
let(:pipeline) { create(:ci_pipeline, project: project) }
context 'when pipeline is triggered by a pipeline from the same project' do
let(:upstream_pipeline) { create(:ci_pipeline, project: pipeline.project) }
before do
create(:ci_sources_pipeline,
source_pipeline: upstream_pipeline,
source_project: project,
pipeline: pipeline,
project: project)
end
it 'returns the parent pipeline' do
expect(pipeline.parent_pipeline).to eq(upstream_pipeline)
end
it 'is child' do
expect(pipeline).to be_child
end
end
context 'when pipeline is triggered by a pipeline from another project' do
let(:upstream_pipeline) { create(:ci_pipeline) }
before do
create(:ci_sources_pipeline,
source_pipeline: upstream_pipeline,
source_project: upstream_pipeline.project,
pipeline: pipeline,
project: project)
end
it 'returns nil' do
expect(pipeline.parent_pipeline).to be_nil
end
it 'is not child' do
expect(pipeline).not_to be_child
end
end
context 'when pipeline is not triggered by a pipeline' do
it 'returns nil' do
expect(pipeline.parent_pipeline).to be_nil
end
it 'is not child' do
expect(pipeline).not_to be_child
end
end
end
describe '#child_pipelines' do
let(:project) { create(:project) }
let(:pipeline) { create(:ci_pipeline, project: project) }
context 'when pipeline triggered other pipelines on same project' do
let(:downstream_pipeline) { create(:ci_pipeline, project: pipeline.project) }
before do
create(:ci_sources_pipeline,
source_pipeline: pipeline,
source_project: pipeline.project,
pipeline: downstream_pipeline,
project: pipeline.project)
end
it 'returns the child pipelines' do
expect(pipeline.child_pipelines).to eq [downstream_pipeline]
end
it 'is parent' do
expect(pipeline).to be_parent
end
end
context 'when pipeline triggered other pipelines on another project' do
let(:downstream_pipeline) { create(:ci_pipeline) }
before do
create(:ci_sources_pipeline,
source_pipeline: pipeline,
source_project: pipeline.project,
pipeline: downstream_pipeline,
project: downstream_pipeline.project)
end
it 'returns empty array' do
expect(pipeline.child_pipelines).to be_empty
end
it 'is not parent' do
expect(pipeline).not_to be_parent
end
end
context 'when pipeline did not trigger any pipelines' do
it 'returns empty array' do
expect(pipeline.child_pipelines).to be_empty
end
it 'is not parent' do
expect(pipeline).not_to be_parent
end
end
end
end
......@@ -123,6 +123,26 @@ describe PipelineEntity do
end
end
context 'delete path' do
context 'user has ability to delete pipeline' do
let(:project) { create(:project, namespace: user.namespace) }
let(:pipeline) { create(:ci_pipeline, project: project) }
it 'contains delete path' do
expect(subject[:delete_path]).to be_present
end
end
context 'user does not have ability to delete pipeline' do
let(:project) { create(:project) }
let(:pipeline) { create(:ci_pipeline, project: project) }
it 'does not contain delete path' do
expect(subject).not_to have_key(:delete_path)
end
end
end
context 'when pipeline ref is empty' do
let(:pipeline) { create(:ci_empty_pipeline) }
......
# frozen_string_literal: true
require 'spec_helper'
describe Ci::CreatePipelineService do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:user) { create(:admin) }
let(:ref) { 'refs/heads/master' }
let(:service) { described_class.new(project, user, { ref: ref }) }
context 'custom config content' do
let(:bridge) do
double(:bridge, yaml_for_downstream: <<~YML
rspec:
script: rspec
custom:
script: custom
YML
)
end
subject { service.execute(:push, bridge: bridge) }
it 'creates a pipeline using the content passed in as param' do
expect(subject).to be_persisted
expect(subject.builds.map(&:name)).to eq %w[rspec custom]
expect(subject.config_source).to eq 'bridge_source'
end
end
end
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册