Skip to content
代码片段 群组 项目
提交 92bc037e 编辑于 作者: Frédéric Caplette's avatar Frédéric Caplette 提交者: Zack Cuddy
浏览文件

Extract event_hub out of pipelines_table components

To make the pipelines_table component more reusable,
we extract the the event hub and mixin logic out of the
components and instead emit native vue events and let
the wrapper components handle it.
上级 32c9e8fc
No related branches found
No related tags found
无相关合并请求
显示
390 个添加204 个删除
......@@ -8,9 +8,7 @@ import { TRACKING_CATEGORIES } from '~/ci/constants';
import { keepLatestDownstreamPipelines } from '~/ci/pipeline_details/utils/parsing_utils';
import LegacyPipelineMiniGraph from '~/ci/pipeline_mini_graph/legacy_pipeline_mini_graph.vue';
import PipelineFailedJobsWidget from '~/ci/pipelines_page/components/failure_widget/pipeline_failed_jobs_widget.vue';
import eventHub from '~/ci/event_hub';
import PipelineOperations from '../pipelines_page/components/pipeline_operations.vue';
import PipelineStopModal from '../pipelines_page/components/pipeline_stop_modal.vue';
import PipelineTriggerer from '../pipelines_page/components/pipeline_triggerer.vue';
import PipelineUrl from '../pipelines_page/components/pipeline_url.vue';
import PipelinesStatusBadge from '../pipelines_page/components/pipelines_status_badge.vue';
......@@ -19,6 +17,23 @@ const HIDE_TD_ON_MOBILE = 'gl-display-none! gl-lg-display-table-cell!';
const DEFAULT_TH_CLASSES =
'gl-bg-transparent! gl-border-b-solid! gl-border-b-gray-100! gl-p-5! gl-border-b-1!';
/**
* Pipelines Table
*
* Presentational component of a table of pipelines. This component does not
* fetch the list of pipelines and instead expects it as a prop.
* GraphQL actions for pipelines, such as retrying, canceling, etc.
* are handled within this component.
*
* Use this `legacy_pipelines_table_wrapper` if you need a fully functional REST component.
*
* IMPORTANT: When using this component, make sure to handle the following events:
* 1- @refresh-pipeline-table
* 2- @cancel-pipeline
* 3- @retry-pipeline
*
*/
export default {
components: {
GlTableLite,
......@@ -26,7 +41,6 @@ export default {
PipelineFailedJobsWidget,
PipelineOperations,
PipelinesStatusBadge,
PipelineStopModal,
PipelineTriggerer,
PipelineUrl,
},
......@@ -63,14 +77,6 @@ export default {
required: true,
},
},
data() {
return {
pipelineId: 0,
pipeline: {},
endpoint: '',
cancelingPipeline: null,
};
},
computed: {
showFailedJobsWidget() {
return this.glFeatures.ciJobFailuresInMr;
......@@ -131,17 +137,6 @@ export default {
return this.pipelines;
},
},
watch: {
pipelines() {
this.cancelingPipeline = null;
},
},
created() {
eventHub.$on('openConfirmationModal', this.setModalData);
},
beforeDestroy() {
eventHub.$off('openConfirmationModal', this.setModalData);
},
methods: {
getDownstreamPipelines(pipeline) {
const downstream = pipeline.triggered;
......@@ -153,14 +148,16 @@ export default {
failedJobsCount(pipeline) {
return pipeline?.failed_builds?.length || 0;
},
setModalData(data) {
this.pipelineId = data.pipeline.id;
this.pipeline = data.pipeline;
this.endpoint = data.endpoint;
onRefreshPipelinesTable() {
this.$emit('refresh-pipelines-table');
},
onRetryPipeline(pipeline) {
// This emit is only used by the `legacy_pipelines_table_wrapper`.
this.$emit('retry-pipeline', pipeline);
},
onSubmit() {
eventHub.$emit('postAction', this.endpoint);
this.cancelingPipeline = this.pipelineId;
onCancelPipeline(pipeline) {
// This emit is only used by the `legacy_pipelines_table_wrapper`.
this.$emit('cancel-pipeline', pipeline);
},
trackPipelineMiniGraph() {
this.track('click_minigraph', { label: TRACKING_CATEGORIES.table });
......@@ -219,7 +216,12 @@ export default {
</template>
<template #cell(actions)="{ item }">
<pipeline-operations :pipeline="item" :canceling-pipeline="cancelingPipeline" />
<pipeline-operations
:pipeline="item"
@cancel-pipeline="onCancelPipeline"
@refresh-pipelines-table="onRefreshPipelinesTable"
@retry-pipeline="onRetryPipeline"
/>
</template>
<template #row-details="{ item }">
......@@ -234,7 +236,5 @@ export default {
/>
</template>
</gl-table-lite>
<pipeline-stop-modal :pipeline="pipeline" @submit="onSubmit" />
</div>
</template>
......@@ -52,14 +52,12 @@ export default {
});
eventHub.$on('postAction', this.postAction);
eventHub.$on('retryPipeline', this.postAction);
eventHub.$on('clickedDropdown', this.updateTable);
eventHub.$on('updateTable', this.updateTable);
eventHub.$on('runMergeRequestPipeline', this.runMergeRequestPipeline);
},
beforeDestroy() {
eventHub.$off('postAction', this.postAction);
eventHub.$off('retryPipeline', this.postAction);
eventHub.$off('clickedDropdown', this.updateTable);
eventHub.$off('updateTable', this.updateTable);
eventHub.$off('runMergeRequestPipeline', this.runMergeRequestPipeline);
......@@ -68,6 +66,15 @@ export default {
this.poll.stop();
},
methods: {
onCancelPipeline(pipeline) {
this.postAction(pipeline.cancel_path);
},
onRefreshPipelinesTable() {
this.updateTable();
},
onRetryPipeline(pipeline) {
this.postAction(pipeline.retry_path);
},
updateInternalState(parameters) {
this.poll.stop();
......
......@@ -7,28 +7,25 @@ export default {
GlButton,
},
props: {
newPipelinePath: {
ciLintPath: {
type: String,
required: false,
default: null,
},
resetCachePath: {
type: String,
isResetCacheButtonLoading: {
type: Boolean,
required: false,
default: null,
default: false,
},
ciLintPath: {
newPipelinePath: {
type: String,
required: false,
default: null,
},
isResetCacheButtonLoading: {
type: Boolean,
resetCachePath: {
type: String,
required: false,
default: false,
default: null,
},
},
methods: {
......
<script>
import { GlButton, GlTooltipDirective, GlModalDirective } from '@gitlab/ui';
import { GlButton, GlTooltipDirective } from '@gitlab/ui';
import Tracking from '~/tracking';
import { BUTTON_TOOLTIP_RETRY, BUTTON_TOOLTIP_CANCEL, TRACKING_CATEGORIES } from '~/ci/constants';
import eventHub from '../../event_hub';
import PipelineMultiActions from './pipeline_multi_actions.vue';
import PipelinesManualActions from './pipelines_manual_actions.vue';
import PipelineStopModal from './pipeline_stop_modal.vue';
export default {
BUTTON_TOOLTIP_RETRY,
BUTTON_TOOLTIP_CANCEL,
directives: {
GlTooltip: GlTooltipDirective,
GlModalDirective,
},
components: {
GlButton,
PipelineMultiActions,
PipelinesManualActions,
PipelineStopModal,
},
mixins: [Tracking.mixin()],
props: {
......@@ -24,15 +24,12 @@ export default {
type: Object,
required: true,
},
cancelingPipeline: {
type: Number,
required: false,
default: null,
},
},
data() {
return {
isCanceling: false,
isRetrying: false,
showConfirmationModal: false,
};
},
computed: {
......@@ -41,27 +38,36 @@ export default {
this.pipeline?.details?.has_manual_actions || this.pipeline?.details?.has_scheduled_actions
);
},
isCancelling() {
return this.cancelingPipeline === this.pipeline.id;
},
},
watch: {
pipeline() {
this.isRetrying = false;
if (this.isCanceling || this.isRetrying) {
this.isCanceling = false;
this.isRetrying = false;
}
},
},
methods: {
onCloseModal() {
this.showConfirmationModal = false;
},
onConfirmCancelPipeline() {
this.isCanceling = true;
this.showConfirmationModal = false;
this.$emit('cancel-pipeline', this.pipeline);
},
handleCancelClick() {
this.showConfirmationModal = true;
this.trackClick('click_cancel_button');
eventHub.$emit('openConfirmationModal', {
pipeline: this.pipeline,
endpoint: this.pipeline.cancel_path,
});
},
handleRetryClick() {
this.isRetrying = true;
this.trackClick('click_retry_button');
eventHub.$emit('retryPipeline', this.pipeline.retry_path);
this.$emit('retry-pipeline', this.pipeline);
},
trackClick(action) {
this.track(action, { label: TRACKING_CATEGORIES.table });
......@@ -72,8 +78,19 @@ export default {
<template>
<div class="gl-text-right">
<pipeline-stop-modal
:pipeline="pipeline"
:show-confirmation-modal="showConfirmationModal"
@submit="onConfirmCancelPipeline"
@close-modal="onCloseModal"
/>
<div class="btn-group">
<pipelines-manual-actions v-if="hasActions" :iid="pipeline.iid" />
<pipelines-manual-actions
v-if="hasActions"
:iid="pipeline.iid"
@refresh-pipeline-table="$emit('refresh-pipelines-table')"
/>
<gl-button
v-if="pipeline.flags.retryable"
......@@ -94,11 +111,10 @@ export default {
<gl-button
v-if="pipeline.flags.cancelable"
v-gl-tooltip.hover
v-gl-modal-directive="'confirmation-modal'"
:aria-label="$options.BUTTON_TOOLTIP_CANCEL"
:title="$options.BUTTON_TOOLTIP_CANCEL"
:loading="isCancelling"
:disabled="isCancelling"
:loading="isCanceling"
:disabled="isCanceling"
icon="cancel"
variant="danger"
category="primary"
......
......@@ -7,7 +7,7 @@ import CiIcon from '~/vue_shared/components/ci_icon.vue';
/**
* Pipeline Stop Modal.
*
* Renders the modal used to confirm stopping a pipeline.
* Renders the modal used to confirm cancelling a pipeline.
*/
export default {
components: {
......@@ -22,8 +22,15 @@ export default {
required: true,
deep: true,
},
showConfirmationModal: {
type: Boolean,
required: true,
},
},
computed: {
hasRef() {
return !isEmpty(this.pipeline.ref);
},
modalTitle() {
return sprintf(
s__('Pipeline|Stop pipeline #%{pipelineId}?'),
......@@ -34,10 +41,7 @@ export default {
);
},
modalText() {
return s__(`Pipeline|You’re about to stop pipeline #%{pipelineId}.`);
},
hasRef() {
return !isEmpty(this.pipeline.ref);
return s__(`Pipeline|You're about to stop pipeline #%{pipelineId}.`);
},
primaryProps() {
return {
......@@ -45,10 +49,13 @@ export default {
attributes: { variant: 'danger' },
};
},
cancelProps() {
return {
text: __('Cancel'),
};
showModal: {
get() {
return this.showConfirmationModal;
},
set() {
this.$emit('close-modal');
},
},
},
methods: {
......@@ -56,14 +63,16 @@ export default {
this.$emit('submit', event);
},
},
cancelProps: { text: __('Cancel') },
};
</script>
<template>
<gl-modal
v-model="showModal"
modal-id="confirmation-modal"
:title="modalTitle"
:action-primary="primaryProps"
:action-cancel="cancelProps"
:action-cancel="$options.cancelProps"
@primary="emitSubmit($event)"
>
<p>
......@@ -74,7 +83,7 @@ export default {
</gl-sprintf>
</p>
<p v-if="pipeline">
<p>
<ci-icon
v-if="pipeline.details"
:status="pipeline.details.status"
......
......@@ -6,7 +6,6 @@ import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_m
import { s__, __, sprintf } from '~/locale';
import Tracking from '~/tracking';
import GlCountdown from '~/vue_shared/components/gl_countdown.vue';
import eventHub from '../../event_hub';
import { TRACKING_CATEGORIES } from '../../constants';
import getPipelineActionsQuery from '../graphql/queries/get_pipeline_actions.query.graphql';
......@@ -94,7 +93,7 @@ export default {
.post(`${action.playPath}.json`)
.then(() => {
this.isLoading = false;
eventHub.$emit('updateTable');
this.$emit('refresh-pipeline-table');
})
.catch(() => {
this.isLoading = false;
......
......@@ -13,7 +13,7 @@ import {
RAW_TEXT_WARNING,
TRACKING_CATEGORIES,
} from '~/ci/constants';
import PipelinesTableComponent from '~/ci/common/pipelines_table.vue';
import PipelinesTable from '~/ci/common/pipelines_table.vue';
import PipelinesMixin from '~/ci/pipeline_details/mixins/pipelines_mixin';
import { validateParams } from '~/ci/pipeline_details/utils';
import NavigationTabs from '~/vue_shared/components/navigation_tabs.vue';
......@@ -37,7 +37,7 @@ export default {
NavigationTabs,
NavigationControls,
PipelinesFilteredSearch,
PipelinesTableComponent,
PipelinesTable,
TablePagination,
},
mixins: [PipelinesMixin, Tracking.mixin()],
......@@ -431,12 +431,15 @@ export default {
/>
<div v-else-if="stateToRender === $options.stateMap.tableList">
<pipelines-table-component
<pipelines-table
:pipelines="state.pipelines"
:pipeline-schedule-url="pipelineScheduleUrl"
:update-graph-dropdown="updateGraphDropdown"
:view-type="viewType"
:pipeline-key-option="selectedPipelineKeyOption"
@cancel-pipeline="onCancelPipeline"
@refresh-pipelines-table="onRefreshPipelinesTable"
@retry-pipeline="onRetryPipeline"
/>
</div>
......
......@@ -2,7 +2,7 @@
import { GlButton, GlEmptyState, GlLoadingIcon, GlModal, GlLink, GlSprintf } from '@gitlab/ui';
import { helpPagePath } from '~/helpers/help_page_helper';
import { getParameterByName } from '~/lib/utils/url_utility';
import PipelinesTableComponent from '~/ci/common/pipelines_table.vue';
import PipelinesTable from '~/ci/common/pipelines_table.vue';
import { PipelineKeyOptions } from '~/ci/constants';
import eventHub from '~/ci/event_hub';
import PipelinesMixin from '~/ci/pipeline_details/mixins/pipelines_mixin';
......@@ -21,7 +21,7 @@ export default {
GlLoadingIcon,
GlModal,
GlSprintf,
PipelinesTableComponent,
PipelinesTable,
TablePagination,
},
mixins: [PipelinesMixin, glFeatureFlagMixin()],
......@@ -279,11 +279,14 @@ export default {
{{ $options.i18n.runPipelineText }}
</gl-button>
<pipelines-table-component
<pipelines-table
:pipelines="state.pipelines"
:update-graph-dropdown="updateGraphDropdown"
:view-type="viewType"
:pipeline-key-option="$options.PipelineKeyOptions[0]"
@cancel-pipeline="onCancelPipeline"
@refresh-pipelines-table="onRefreshPipelinesTable"
@retry-pipeline="onRetryPipeline"
>
<template #table-header-actions>
<div v-if="canRenderPipelineButton" class="gl-text-right">
......@@ -296,7 +299,7 @@ export default {
</gl-button>
</div>
</template>
</pipelines-table-component>
</pipelines-table>
</div>
<gl-modal
......
......@@ -11,7 +11,7 @@ const apolloProvider = new VueApollo({
/**
* Used in:
* - Project Pipelines List (projects:pipelines:index)
* - Project Pipelines List (projects:pipelines)
* - Commit details View > Pipelines Tab > Pipelines Table (projects:commit:pipelines)
* - Merge request details View > Pipelines Tab > Pipelines Table (projects:merge_requests:show)
* - New merge request View > Pipelines Tab > Pipelines Table (projects:merge_requests:creations:new)
......
......@@ -93,7 +93,7 @@ function mountPipelines() {
const { mrWidgetData } = gl;
const table = new Vue({
components: {
CommitPipelinesTable: () => {
MergeRequestPipelinesTable: () => {
return gon.features.mrPipelinesGraphql
? import('~/ci/merge_requests/components/pipelines_table_wrapper.vue')
: import('~/commit/pipelines/legacy_pipelines_table_wrapper.vue');
......@@ -112,7 +112,7 @@ function mountPipelines() {
withFailedJobsDetails: true,
},
render(createElement) {
return createElement('commit-pipelines-table', {
return createElement('merge-request-pipelines-table', {
props: {
endpoint: pipelineTableViewEl.dataset.endpoint,
emptyStateSvgPath: pipelineTableViewEl.dataset.emptyStateSvgPath,
......@@ -347,11 +347,11 @@ export default class MergeRequestTabs {
}
// this.hideSidebar();
this.resetViewContainer();
this.commitPipelinesTable = destroyPipelines(this.commitPipelinesTable);
this.mergeRequestPipelinesTable = destroyPipelines(this.mergeRequestPipelinesTable);
} else if (action === 'new') {
this.expandView();
this.resetViewContainer();
this.commitPipelinesTable = destroyPipelines(this.commitPipelinesTable);
this.mergeRequestPipelinesTable = destroyPipelines(this.mergeRequestPipelinesTable);
} else if (this.isDiffAction(action)) {
if (!isInVueNoteablePage()) {
/*
......@@ -366,7 +366,7 @@ export default class MergeRequestTabs {
}
// this.hideSidebar();
this.expandViewContainer();
this.commitPipelinesTable = destroyPipelines(this.commitPipelinesTable);
this.mergeRequestPipelinesTable = destroyPipelines(this.mergeRequestPipelinesTable);
this.commitsTab.classList.remove('active');
} else if (action === 'pipelines') {
// this.hideSidebar();
......@@ -384,7 +384,7 @@ export default class MergeRequestTabs {
// this.showSidebar();
this.resetViewContainer();
this.commitPipelinesTable = destroyPipelines(this.commitPipelinesTable);
this.mergeRequestPipelinesTable = destroyPipelines(this.mergeRequestPipelinesTable);
}
renderGFM(document.querySelector('.detail-page-description'));
......@@ -522,7 +522,7 @@ export default class MergeRequestTabs {
}
mountPipelinesView() {
this.commitPipelinesTable = mountPipelines();
this.mergeRequestPipelinesTable = mountPipelines();
}
// load the diff tab content from the backend
......
......@@ -34952,7 +34952,7 @@ msgstr ""
msgid "Pipeline|We are currently unable to fetch pipeline data"
msgstr ""
 
msgid "Pipeline|Youre about to stop pipeline #%{pipelineId}."
msgid "Pipeline|You're about to stop pipeline #%{pipelineId}."
msgstr ""
 
msgid "Pipeline|for"
import '~/commons';
import { GlTableLite } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import fixture from 'test_fixtures/pipelines/pipelines.json';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import LegacyPipelineMiniGraph from '~/ci/pipeline_mini_graph/legacy_pipeline_mini_graph.vue';
import PipelineFailedJobsWidget from '~/ci/pipelines_page/components/failure_widget/pipeline_failed_jobs_widget.vue';
import PipelineOperations from '~/ci/pipelines_page/components/pipeline_operations.vue';
......@@ -20,14 +18,12 @@ import {
import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
jest.mock('~/ci/event_hub');
describe('Pipelines Table', () => {
let pipeline;
let wrapper;
let trackingSpy;
const defaultProvide = {
fullPath: '/my-project/',
glFeatures: {},
withFailedJobsDetails: false,
};
......@@ -39,32 +35,31 @@ describe('Pipelines Table', () => {
withFailedJobsDetails: true,
};
const { pipelines } = fixture;
const defaultProps = {
pipelines: [],
pipelines,
viewType: 'root',
pipelineKeyOption: PipelineKeyOptions[0],
};
const createMockPipeline = () => {
// Clone fixture as it could be modified by tests
const { pipelines } = JSON.parse(JSON.stringify(fixture));
return pipelines.find((p) => p.user !== null && p.commit !== null);
};
const createComponent = (props = {}, provide = {}) => {
wrapper = extendedWrapper(
mount(PipelinesTable, {
propsData: {
...defaultProps,
...props,
},
provide: {
...defaultProvide,
...provide,
},
stubs: ['PipelineFailedJobsWidget'],
}),
);
const [firstPipeline] = pipelines;
const createComponent = ({ props = {}, provide = {}, stubs = {} } = {}) => {
wrapper = mountExtended(PipelinesTable, {
propsData: {
...defaultProps,
...props,
},
provide: {
...defaultProvide,
...provide,
},
stubs: {
PipelineOperations: true,
...stubs,
},
});
};
const findGlTableLite = () => wrapper.findComponent(GlTableLite);
......@@ -84,13 +79,9 @@ describe('Pipelines Table', () => {
const findRetryBtn = () => wrapper.findByTestId('pipelines-retry-button');
const findCancelBtn = () => wrapper.findByTestId('pipelines-cancel-button');
beforeEach(() => {
pipeline = createMockPipeline();
});
describe('Pipelines Table', () => {
beforeEach(() => {
createComponent({ pipelines: [pipeline], viewType: 'root' });
createComponent({ props: { viewType: 'root' } });
});
it('displays table', () => {
......@@ -105,7 +96,7 @@ describe('Pipelines Table', () => {
});
it('should display a table row', () => {
expect(findTableRows()).toHaveLength(1);
expect(findTableRows()).toHaveLength(pipelines.length);
});
describe('status cell', () => {
......@@ -120,7 +111,7 @@ describe('Pipelines Table', () => {
});
it('should display the pipeline id', () => {
expect(findPipelineInfo().text()).toContain(`#${pipeline.id}`);
expect(findPipelineInfo().text()).toContain(`#${firstPipeline.id}`);
});
});
......@@ -130,24 +121,33 @@ describe('Pipelines Table', () => {
});
it('should render the right number of stages', () => {
const stagesLength = pipeline.details.stages.length;
expect(findLegacyPipelineMiniGraph().props('stages').length).toBe(stagesLength);
const stagesLength = firstPipeline.details.stages.length;
expect(findLegacyPipelineMiniGraph().props('stages')).toHaveLength(stagesLength);
});
it('should render the latest downstream pipelines only', () => {
// component receives two downstream pipelines. one of them is already outdated
// because we retried the trigger job, so the mini pipeline graph will only
// render the newly created downstream pipeline instead
expect(pipeline.triggered).toHaveLength(2);
expect(firstPipeline.triggered).toHaveLength(2);
expect(findLegacyPipelineMiniGraph().props('downstreamPipelines')).toHaveLength(1);
});
describe('when pipeline does not have stages', () => {
beforeEach(() => {
pipeline = createMockPipeline();
pipeline.details.stages = [];
createComponent({ pipelines: [pipeline] });
createComponent({
props: {
pipelines: [
{
...firstPipeline,
details: {
...firstPipeline.details,
stages: [],
},
},
],
},
});
});
it('stages are not rendered', () => {
......@@ -163,6 +163,10 @@ describe('Pipelines Table', () => {
});
describe('operations cell', () => {
beforeEach(() => {
createComponent({ stubs: { PipelineOperations } });
});
it('should render pipeline operations', () => {
expect(findActions().exists()).toBe(true);
});
......@@ -186,11 +190,11 @@ describe('Pipelines Table', () => {
describe('row', () => {
describe('when the FF is disabled', () => {
beforeEach(() => {
createComponent({ pipelines: [pipeline] });
createComponent();
});
it('does not render', () => {
expect(findTableRows()).toHaveLength(1);
expect(findTableRows()).toHaveLength(pipelines.length);
expect(findPipelineFailureWidget().exists()).toBe(false);
});
});
......@@ -198,20 +202,21 @@ describe('Pipelines Table', () => {
describe('when the FF is enabled', () => {
describe('and `withFailedJobsDetails` value is provided', () => {
beforeEach(() => {
createComponent({ pipelines: [pipeline] }, provideWithDetails);
createComponent({ provide: provideWithDetails });
});
it('renders', () => {
expect(findTableRows()).toHaveLength(2);
// We have 2 rows per pipeline with the widget
expect(findTableRows()).toHaveLength(pipelines.length * 2);
expect(findPipelineFailureWidget().exists()).toBe(true);
});
it('passes the expected props', () => {
expect(findPipelineFailureWidget().props()).toStrictEqual({
failedJobsCount: pipeline.failed_builds.length,
isPipelineActive: pipeline.active,
pipelineIid: pipeline.iid,
pipelinePath: pipeline.path,
failedJobsCount: firstPipeline.failed_builds.length,
isPipelineActive: firstPipeline.active,
pipelineIid: firstPipeline.iid,
pipelinePath: firstPipeline.path,
// Make sure the forward slash was removed
projectPath: 'frontend-fixtures/pipelines-project',
});
......@@ -220,14 +225,13 @@ describe('Pipelines Table', () => {
describe('and `withFailedJobsDetails` value is not provided', () => {
beforeEach(() => {
createComponent(
{ pipelines: [pipeline] },
{ glFeatures: { ciJobFailuresInMr: true } },
);
createComponent({
provide: { glFeatures: { ciJobFailuresInMr: true } },
});
});
it('does not render', () => {
expect(findTableRows()).toHaveLength(1);
expect(findTableRows()).toHaveLength(pipelines.length);
expect(findPipelineFailureWidget().exists()).toBe(false);
});
});
......@@ -235,35 +239,55 @@ describe('Pipelines Table', () => {
});
});
describe('tracking', () => {
describe('events', () => {
beforeEach(() => {
trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
createComponent();
});
afterEach(() => {
unmockTracking();
describe('when confirming to cancel a pipeline', () => {
beforeEach(async () => {
await findActions().vm.$emit('cancel-pipeline', firstPipeline);
});
it('emits the `cancel-pipeline` event', () => {
expect(wrapper.emitted('cancel-pipeline')).toEqual([[firstPipeline]]);
});
});
it('tracks status badge click', () => {
findCiBadgeLink().vm.$emit('ciStatusBadgeClick');
describe('when retrying a pipeline', () => {
beforeEach(() => {
findActions().vm.$emit('retry-pipeline', firstPipeline);
});
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_ci_status_badge', {
label: TRACKING_CATEGORIES.table,
it('emits the `retry-pipeline` event', () => {
expect(wrapper.emitted('retry-pipeline')).toEqual([[firstPipeline]]);
});
});
it('tracks retry pipeline button click', () => {
findRetryBtn().vm.$emit('click');
describe('when refreshing pipelines', () => {
beforeEach(() => {
findActions().vm.$emit('refresh-pipelines-table');
});
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_retry_button', {
label: TRACKING_CATEGORIES.table,
it('emits the `refresh-pipelines-table` event', () => {
expect(wrapper.emitted('refresh-pipelines-table')).toEqual([[]]);
});
});
});
it('tracks cancel pipeline button click', () => {
findCancelBtn().vm.$emit('click');
describe('tracking', () => {
beforeEach(() => {
trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
});
afterEach(() => {
unmockTracking();
});
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_cancel_button', {
it('tracks status badge click', () => {
findCiBadgeLink().vm.$emit('ciStatusBadgeClick');
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_ci_status_badge', {
label: TRACKING_CATEGORIES.table,
});
});
......
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import PipelinesManualActions from '~/ci/pipelines_page/components/pipelines_manual_actions.vue';
import PipelineMultiActions from '~/ci/pipelines_page/components/pipeline_multi_actions.vue';
import PipelineOperations from '~/ci/pipelines_page/components/pipeline_operations.vue';
import eventHub from '~/ci/event_hub';
import PipelineStopModal from '~/ci/pipelines_page/components/pipeline_stop_modal.vue';
import { TRACKING_CATEGORIES } from '~/ci/constants';
describe('Pipeline operations', () => {
let trackingSpy;
let wrapper;
const defaultProps = {
......@@ -36,6 +39,7 @@ describe('Pipeline operations', () => {
const findMultiActions = () => wrapper.findComponent(PipelineMultiActions);
const findRetryBtn = () => wrapper.findByTestId('pipelines-retry-button');
const findCancelBtn = () => wrapper.findByTestId('pipelines-cancel-button');
const findPipelineStopModal = () => wrapper.findComponent(PipelineStopModal);
it('should display pipeline manual actions', () => {
createComponent();
......@@ -49,28 +53,71 @@ describe('Pipeline operations', () => {
expect(findMultiActions().exists()).toBe(true);
});
it('does not show the confirmation modal', () => {
createComponent();
expect(findPipelineStopModal().props().showConfirmationModal).toBe(false);
});
describe('when cancelling a pipeline', () => {
beforeEach(async () => {
createComponent();
await findCancelBtn().vm.$emit('click');
});
it('should show a confirmation modal', () => {
expect(findPipelineStopModal().props().showConfirmationModal).toBe(true);
});
it('should emit cancel-pipeline event when confirming', async () => {
await findPipelineStopModal().vm.$emit('submit');
expect(wrapper.emitted('cancel-pipeline')).toEqual([[defaultProps.pipeline]]);
expect(findPipelineStopModal().props().showConfirmationModal).toBe(false);
});
it('should hide the modal when closing', async () => {
await findPipelineStopModal().vm.$emit('close-modal');
expect(findPipelineStopModal().props().showConfirmationModal).toBe(false);
});
});
describe('events', () => {
beforeEach(() => {
createComponent();
jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
});
it('should emit retryPipeline event', () => {
findRetryBtn().vm.$emit('click');
expect(eventHub.$emit).toHaveBeenCalledWith(
'retryPipeline',
defaultProps.pipeline.retry_path,
);
expect(wrapper.emitted('retry-pipeline')).toEqual([[defaultProps.pipeline]]);
});
});
describe('tracking', () => {
beforeEach(() => {
createComponent();
trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
});
afterEach(() => {
unmockTracking();
});
it('tracks retry pipeline button click', () => {
findRetryBtn().vm.$emit('click');
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_retry_button', {
label: TRACKING_CATEGORIES.table,
});
});
it('should emit openConfirmationModal event', () => {
it('tracks cancel pipeline button click', () => {
findCancelBtn().vm.$emit('click');
expect(eventHub.$emit).toHaveBeenCalledWith('openConfirmationModal', {
pipeline: defaultProps.pipeline,
endpoint: defaultProps.pipeline.cancel_path,
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_cancel_button', {
label: TRACKING_CATEGORIES.table,
});
});
});
......
import { shallowMount } from '@vue/test-utils';
import { GlSprintf } from '@gitlab/ui';
import { GlModal, GlSprintf } from '@gitlab/ui';
import { mockPipelineHeader } from 'jest/ci/pipeline_details/mock_data';
import PipelineStopModal from '~/ci/pipelines_page/components/pipeline_stop_modal.vue';
describe('PipelineStopModal', () => {
let wrapper;
const createComponent = () => {
const createComponent = ({ props = {} } = {}) => {
wrapper = shallowMount(PipelineStopModal, {
propsData: {
pipeline: mockPipelineHeader,
showConfirmationModal: false,
...props,
},
stubs: {
GlSprintf,
......@@ -17,11 +19,43 @@ describe('PipelineStopModal', () => {
});
};
const findModal = () => wrapper.findComponent(GlModal);
beforeEach(() => {
createComponent();
});
it('should render "stop pipeline" warning', () => {
expect(wrapper.text()).toMatch(`You’re about to stop pipeline #${mockPipelineHeader.id}.`);
describe('when `showConfirmationModal` is false', () => {
it('passes the visiblity value to the modal', () => {
expect(findModal().props().visible).toBe(false);
});
});
describe('when `showConfirmationModal` is true', () => {
beforeEach(() => {
createComponent({ props: { showConfirmationModal: true } });
});
it('passes the visiblity value to the modal', () => {
expect(findModal().props().visible).toBe(true);
});
it('renders "stop pipeline" warning', () => {
expect(wrapper.text()).toMatch(`You're about to stop pipeline #${mockPipelineHeader.id}.`);
});
});
describe('events', () => {
beforeEach(() => {
createComponent({ props: { showConfirmationModal: true } });
});
it('emits the close-modal event when the visiblity changes', async () => {
expect(wrapper.emitted('close-modal')).toBeUndefined();
await findModal().vm.$emit('change', false);
expect(wrapper.emitted('close-modal')).toEqual([[]]);
});
});
});
import { GlLoadingIcon, GlModal, GlTableLite } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue';
import fixture from 'test_fixtures/pipelines/pipelines.json';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { stubComponent } from 'helpers/stub_component';
import waitForPromises from 'helpers/wait_for_promises';
import Api from '~/api';
import LegacyPipelinesTableWraper from '~/commit/pipelines/legacy_pipelines_table_wrapper.vue';
import LegacyPipelinesTableWrapper from '~/commit/pipelines/legacy_pipelines_table_wrapper.vue';
import PipelinesTable from '~/ci/common/pipelines_table.vue';
import {
HTTP_STATUS_BAD_REQUEST,
HTTP_STATUS_INTERNAL_SERVER_ERROR,
......@@ -39,27 +39,26 @@ describe('Pipelines table in Commits and Merge requests', () => {
const findTableRows = () => wrapper.findAllByTestId('pipeline-table-row');
const findModal = () => wrapper.findComponent(GlModal);
const findMrPipelinesDocsLink = () => wrapper.findByTestId('mr-pipelines-docs-link');
const createComponent = ({ props = {} } = {}) => {
wrapper = extendedWrapper(
mount(LegacyPipelinesTableWraper, {
propsData: {
endpoint: 'endpoint.json',
emptyStateSvgPath: 'foo',
errorStateSvgPath: 'foo',
...props,
},
mocks: {
$toast,
},
stubs: {
GlModal: stubComponent(GlModal, {
template: '<div />',
methods: { show: showMock },
}),
},
}),
);
const findPipelinesTable = () => wrapper.findComponent(PipelinesTable);
const createComponent = ({ props = {}, mountFn = mountExtended } = {}) => {
wrapper = mountFn(LegacyPipelinesTableWrapper, {
propsData: {
endpoint: 'endpoint.json',
emptyStateSvgPath: 'foo',
errorStateSvgPath: 'foo',
...props,
},
mocks: {
$toast,
},
stubs: {
GlModal: stubComponent(GlModal, {
template: '<div />',
methods: { show: showMock },
}),
},
});
};
beforeEach(() => {
......@@ -116,7 +115,6 @@ describe('Pipelines table in Commits and Merge requests', () => {
it('should make an API request when using pagination', async () => {
expect(mock.history.get).toHaveLength(1);
expect(mock.history.get[0].params.page).toBe('1');
wrapper.find('.next-page-item').trigger('click');
......@@ -359,4 +357,53 @@ describe('Pipelines table in Commits and Merge requests', () => {
);
});
});
describe('events', () => {
beforeEach(async () => {
mock.onGet('endpoint.json').reply(HTTP_STATUS_OK, [pipeline]);
createComponent({ mountFn: shallowMountExtended });
await waitForPromises();
});
describe('When cancelling a pipeline', () => {
it('sends the cancel action', async () => {
expect(mock.history.post).toHaveLength(0);
findPipelinesTable().vm.$emit('cancel-pipeline', pipeline);
await waitForPromises();
expect(mock.history.post).toHaveLength(1);
expect(mock.history.post[0].url).toContain('cancel.json');
});
});
describe('When retrying a pipeline', () => {
it('sends the retry action', async () => {
expect(mock.history.post).toHaveLength(0);
findPipelinesTable().vm.$emit('retry-pipeline', pipeline);
await waitForPromises();
expect(mock.history.post).toHaveLength(1);
expect(mock.history.post[0].url).toContain('retry.json');
});
});
describe('When refreshing a pipeline', () => {
it('calls the pipelines endpoint again', async () => {
expect(mock.history.get).toHaveLength(1);
findPipelinesTable().vm.$emit('refresh-pipelines-table');
await waitForPromises();
expect(mock.history.get).toHaveLength(2);
expect(mock.history.get[1].url).toContain('endpoint.json');
});
});
});
});
0% 加载中 .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册