Commit cf26610f authored by Phil Hughes's avatar Phil Hughes

Merge branch '50904-stages-sidebar' into 'master'

Moves stages dropdown into the new vue app

See merge request gitlab-org/gitlab-ce!21971
parents c4d9f402 f72a1bf0
......@@ -24,7 +24,6 @@ export default class Job extends LogOutputBehaviours {
this.$document = $(document);
this.$window = $(window);
this.logBytes = 0;
this.updateDropdown = this.updateDropdown.bind(this);
this.$buildTrace = $('#build-trace');
this.$buildRefreshAnimation = $('.js-build-refresh');
......@@ -35,18 +34,12 @@ export default class Job extends LogOutputBehaviours {
clearTimeout(this.timeout);
this.initSidebar();
this.populateJobs(this.buildStage);
this.updateStageDropdownText(this.buildStage);
this.sidebarOnResize();
this.$document
.off('click', '.js-sidebar-build-toggle')
.on('click', '.js-sidebar-build-toggle', this.sidebarOnClick.bind(this));
this.$document
.off('click', '.stage-item')
.on('click', '.stage-item', this.updateDropdown);
this.scrollThrottled = _.throttle(this.toggleScroll.bind(this), 100);
this.$window
......@@ -194,20 +187,4 @@ export default class Job extends LogOutputBehaviours {
if (this.shouldHideSidebarForViewport()) this.toggleSidebar();
}
// eslint-disable-next-line class-methods-use-this
populateJobs(stage) {
$('.build-job').hide();
$(`.build-job[data-stage="${stage}"]`).show();
}
// eslint-disable-next-line class-methods-use-this
updateStageDropdownText(stage) {
$('.stage-selection').text(stage);
}
updateDropdown(e) {
e.preventDefault();
const stage = e.currentTarget.text;
this.updateStageDropdownText(stage);
this.populateJobs(stage);
}
}
<script>
import _ from 'underscore';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
......@@ -16,26 +17,39 @@
type: Array,
required: true,
},
jobId: {
type: Number,
required: true,
},
},
methods: {
isJobActive(currentJobId) {
return this.jobId === currentJobId;
},
tooltipText(job) {
return `${_.escape(job.name)} - ${job.status.tooltip}`;
},
},
};
</script>
<template>
<div class="builds-container">
<div class="js-jobs-container builds-container">
<div
v-for="job in jobs"
:key="job.id"
class="build-job"
:class="{ retried: job.retried, active: isJobActive(job.id) }"
>
<a
v-for="job in jobs"
:key="job.id"
v-tooltip
:href="job.path"
:title="job.tooltip"
:class="{ active: job.active, retried: job.retried }"
:href="job.status.details_path"
:title="tooltipText(job)"
data-container="body"
>
<icon
v-if="job.active"
v-if="isJobActive(job.id)"
name="arrow-right"
class="js-arrow-right"
class="js-arrow-right icon-arrow-right"
/>
<ci-icon :status="job.status" />
......
<script>
import _ from 'underscore';
import { mapActions, mapState } from 'vuex';
import timeagoMixin from '~/vue_shared/mixins/timeago';
import { timeIntervalInWords } from '~/lib/utils/datetime_utility';
import Icon from '~/vue_shared/components/icon.vue';
......@@ -7,26 +8,22 @@
import ArtifactsBlock from './artifacts_block.vue';
import TriggerBlock from './trigger_block.vue';
import CommitBlock from './commit_block.vue';
import StagesDropdown from './stages_dropdown.vue';
import JobsContainer from './jobs_container.vue';
export default {
name: 'SidebarDetailsBlock',
name: 'JobSidebar',
components: {
ArtifactsBlock,
CommitBlock,
DetailRow,
Icon,
TriggerBlock,
StagesDropdown,
JobsContainer,
},
mixins: [timeagoMixin],
props: {
job: {
type: Object,
required: true,
},
isLoading: {
type: Boolean,
required: true,
},
runnerHelpUrl: {
type: String,
required: false,
......@@ -39,9 +36,7 @@
},
},
computed: {
shouldRenderContent() {
return !this.isLoading && Object.keys(this.job).length > 0;
},
...mapState(['job', 'isLoading', 'stages', 'jobs']),
coverage() {
return `${this.job.coverage}%`;
},
......@@ -97,20 +92,31 @@
},
hasStages() {
return (
this.job &&
(this.job &&
this.job.pipeline &&
this.job.pipeline.stages &&
this.job.pipeline.stages.length > 0
) || false;
this.job.pipeline.stages.length > 0) ||
false
);
},
commit() {
return this.job.pipeline.commit || {};
},
},
methods: {
...mapActions(['fetchJobsForStage']),
},
};
</script>
<template>
<div>
<aside
class="right-sidebar right-sidebar-expanded build-sidebar"
data-offset-top="101"
data-spy="affix"
>
<div class="sidebar-container">
<div class="blocks-container">
<template v-if="!isLoading">
<div class="block">
<strong class="inline prepend-top-8">
{{ job.name }}
......@@ -137,7 +143,8 @@
<button
:aria-label="__('Toggle Sidebar')"
type="button"
class="btn btn-blank gutter-toggle float-right d-block d-md-none js-sidebar-build-toggle"
class="btn btn-blank gutter-toggle
float-right d-block d-md-none js-sidebar-build-toggle"
>
<i
aria-hidden="true"
......@@ -146,7 +153,6 @@
></i>
</button>
</div>
<template v-if="shouldRenderContent">
<div
v-if="job.retry_path || job.new_issue_path"
class="block retry-link"
......@@ -168,7 +174,7 @@
{{ __('Retry') }}
</a>
</div>
<div :class="{block : renderBlock }">
<div :class="{ block : renderBlock }">
<p
v-if="job.merge_request"
class="build-detail-row js-job-mr"
......@@ -266,11 +272,26 @@
:commit="commit"
:merge-request="job.merge_request"
/>
<stages-dropdown
:stages="stages"
:pipeline="job.pipeline"
@requestSidebarStageDropdown="fetchJobsForStage"
/>
</template>
<gl-loading-icon
v-if="isLoading"
v-else
:size="2"
class="prepend-top-10"
/>
</div>
<jobs-container
v-if="!isLoading && jobs.length"
:jobs="jobs"
:job-id="job.id"
/>
</div>
</aside>
</template>
<script>
import _ from 'underscore';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
import Icon from '~/vue_shared/components/icon.vue';
import { sprintf, __ } from '~/locale';
import { __ } from '~/locale';
export default {
components: {
......@@ -10,30 +10,14 @@
Icon,
},
props: {
pipelineId: {
type: Number,
required: true,
},
pipelinePath: {
type: String,
required: true,
},
pipelineRef: {
type: String,
required: true,
},
pipelineRefPath: {
type: String,
pipeline: {
type: Object,
required: true,
},
stages: {
type: Array,
required: true,
},
pipelineStatus: {
type: Object,
required: true,
},
},
data() {
return {
......@@ -41,51 +25,68 @@
};
},
computed: {
pipelineLink() {
return sprintf(__('Pipeline %{pipelineLinkStart} #%{pipelineId} %{pipelineLinkEnd} from %{pipelineLinkRefStart} %{pipelineRef} %{pipelineLinkRefEnd}'), {
pipelineLinkStart: `<a href=${this.pipelinePath} class="js-pipeline-path link-commit">`,
pipelineId: this.pipelineId,
pipelineLinkEnd: '</a>',
pipelineLinkRefStart: `<a href=${this.pipelineRefPath} class="link-commit ref-name">`,
pipelineRef: this.pipelineRef,
pipelineLinkRefEnd: '</a>',
}, false);
hasRef() {
return !_.isEmpty(this.pipeline.ref);
},
},
watch: {
// When the component is initially mounted it may start with an empty stages array.
// Once the prop is updated, we set the first stage as the selected one
stages(newVal) {
if (newVal.length) {
this.selectedStage = newVal[0].name;
}
},
},
methods: {
onStageClick(stage) {
// todo: consider moving into store
this.selectedStage = stage.name;
// update dropdown with jobs
// jobs container is a new component.
this.$emit('requestSidebarStageDropdown', stage);
this.selectedStage = stage.name;
},
},
};
</script>
<template>
<div class="block-last">
<ci-icon :status="pipelineStatus" />
<div class="block-last dropdown">
<ci-icon
:status="pipeline.details.status"
class="vertical-align-middle"
/>
<p v-html="pipelineLink"></p>
{{ __('Pipeline') }}
<a
:href="pipeline.path"
class="js-pipeline-path link-commit"
>
#{{ pipeline.id }}
</a>
<template v-if="hasRef">
{{ __('from') }}
<a
:href="pipeline.ref.path"
class="link-commit ref-name"
>
{{ pipeline.ref.name }}
</a>
</template>
<div class="dropdown">
<button
type="button"
data-toggle="dropdown"
class="js-selected-stage dropdown-menu-toggle prepend-top-8"
>
{{ selectedStage }}
<icon name="chevron-down" />
<i class="fa fa-chevron-down" ></i>
</button>
<ul class="dropdown-menu">
<li
v-for="(stage, index) in stages"
:key="index"
v-for="stage in stages"
:key="stage.name"
>
<button
type="button"
class="stage-item"
class="js-stage-item stage-item"
@click="onStageClick(stage)"
>
{{ stage.name }}
......@@ -93,5 +94,4 @@
</li>
</ul>
</div>
</div>
</template>
import { mapState } from 'vuex';
import _ from 'underscore';
import { mapState, mapActions } from 'vuex';
import Vue from 'vue';
import Job from '../job';
import JobHeader from './components/header.vue';
import DetailsBlock from './components/sidebar_details_block.vue';
import Sidebar from './components/sidebar.vue';
import createStore from './store';
export default () => {
......@@ -13,6 +14,7 @@ export default () => {
const store = createStore();
store.dispatch('setJobEndpoint', dataset.endpoint);
store.dispatch('fetchJob');
// Header
......@@ -43,17 +45,25 @@ export default () => {
new Vue({
el: detailsBlockElement,
components: {
DetailsBlock,
Sidebar,
},
store,
computed: {
...mapState(['job', 'isLoading']),
...mapState(['job']),
},
watch: {
job(newVal, oldVal) {
if (_.isEmpty(oldVal) && !_.isEmpty(newVal.pipeline)) {
this.fetchStages();
}
},
},
methods: {
...mapActions(['fetchStages']),
},
store,
render(createElement) {
return createElement('details-block', {
return createElement('sidebar', {
props: {
isLoading: this.isLoading,
job: this.job,
runnerHelpUrl: dataset.runnerHelpUrl,
terminalPath: detailsBlockDataset.terminalPath,
},
......
......@@ -62,7 +62,9 @@ export const fetchJob = ({ state, dispatch }) => {
});
};
export const receiveJobSuccess = ({ commit }, data) => commit(types.RECEIVE_JOB_SUCCESS, data);
export const receiveJobSuccess = ({ commit }, data) => {
commit(types.RECEIVE_JOB_SUCCESS, data);
};
export const receiveJobError = ({ commit }) => {
commit(types.RECEIVE_JOB_ERROR);
flash(__('An error occurred while fetching the job.'));
......@@ -137,8 +139,11 @@ export const fetchStages = ({ state, dispatch }) => {
dispatch('requestStages');
axios
.get(state.stagesEndpoint)
.then(({ data }) => dispatch('receiveStagesSuccess', data))
.get(state.job.pipeline.path)
.then(({ data }) => {
dispatch('receiveStagesSuccess', data.details.stages);
dispatch('fetchJobsForStage', data.details.stages[0]);
})
.catch(() => dispatch('receiveStagesError'));
};
export const receiveStagesSuccess = ({ commit }, data) =>
......@@ -152,16 +157,23 @@ export const receiveStagesError = ({ commit }) => {
* Jobs list on sidebar - depend on stages dropdown
*/
export const requestJobsForStage = ({ commit }) => commit(types.REQUEST_JOBS_FOR_STAGE);
export const setSelectedStage = ({ commit }, stage) => commit(types.SET_SELECTED_STAGE, stage);
// On stage click, set selected stage + fetch job
export const fetchJobsForStage = ({ state, dispatch }, stage) => {
dispatch('setSelectedStage', stage);
export const fetchJobsForStage = ({ dispatch }, stage) => {
dispatch('requestJobsForStage');
axios
.get(state.stageJobsEndpoint)
.then(({ data }) => dispatch('receiveJobsForStageSuccess', data))
.get(stage.dropdown_path, {
params: {
retried: 1,
},
})
.then(({ data }) => {
const retriedJobs = data.retried.map(job => Object.assign({}, job, { retried: true }));
const jobs = data.latest_statuses.concat(retriedJobs);
dispatch('receiveJobsForStageSuccess', jobs);
})
.catch(() => dispatch('receiveJobsForStageError'));
};
export const receiveJobsForStageSuccess = ({ commit }, data) =>
......
......@@ -328,23 +328,6 @@
}
}
.build-dropdown {
margin: $gl-padding 0;
padding: 0;
.dropdown-menu-toggle {
margin-top: #{$gl-padding / 2};
}
svg {
position: relative;
top: 3px;
margin-right: 3px;
width: 14px;
height: 14px;
}
}
.builds-container {
background-color: $white-light;
border-top: 1px solid $border-color;
......@@ -381,15 +364,11 @@
position: absolute;
left: 15px;
top: 20px;
display: none;
display: block;
}
&.active {
font-weight: $gl-font-weight-bold;
.icon-arrow-right {
display: block;
}
}
&.retried {
......
%aside.right-sidebar.right-sidebar-expanded.build-sidebar.js-build-sidebar.js-right-sidebar{ data: { "offset-top" => "101", "spy" => "affix" } }
.sidebar-container
.blocks-container
#js-details-block-vue{ data: { terminal_path: can?(current_user, :create_build_terminal, @build) && @build.has_terminal? ? terminal_project_job_path(@project, @build) : nil } }
- if @build.pipeline.stages_count > 1
.block-last.dropdown.build-dropdown
%div
%span{ class: "ci-status-icon-#{@build.pipeline.status}" }
= ci_icon_for_status(@build.pipeline.status)
Pipeline
= link_to "##{@build.pipeline.id}", project_pipeline_path(@project, @build.pipeline), class: 'link-commit'
from
= link_to "#{@build.pipeline.ref}", project_ref_path(@project, @build.pipeline.ref), class: 'link-commit ref-name'
%button.dropdown-menu-toggle{ type: 'button', 'data-toggle' => 'dropdown' }
%span.stage-selection More
= icon('chevron-down')
%ul.dropdown-menu
- @build.pipeline.legacy_stages.each do |stage|
%li
%a.stage-item= stage.name
.builds-container
- HasStatus::ORDERED_STATUSES.each do |build_status|
- builds.select{|build| build.status == build_status}.each do |build|
.build-job{ class: sidebar_build_class(build, @build), data: { stage: build.stage } }
- tooltip = sanitize(build.tooltip_message.dup)
= link_to(project_job_path(@project, build), data: { toggle: 'tooltip', title: tooltip, container: 'body' }) do
= sprite_icon('arrow-right', size:16, css_class: 'icon-arrow-right')
%span{ class: "ci-status-icon-#{build.status}" }
= ci_icon_for_status(build.status)
%span
- if build.name
= build.name
- else
= build.id
- if build.retried?
= sprite_icon('retry', size:16, css_class: 'icon-retry')
......@@ -93,7 +93,7 @@
- else
= render "empty_states"
= render "sidebar", builds: @builds
#js-details-block-vue{ data: { terminal_path: can?(current_user, :create_build_terminal, @build) && @build.has_terminal? ? terminal_project_job_path(@project, @build) : nil } }
.js-build-options{ data: javascript_build_options }
......
......@@ -4310,9 +4310,6 @@ msgstr ""
msgid "Pipeline"
msgstr ""
msgid "Pipeline %{pipelineLinkStart} #%{pipelineId} %{pipelineLinkEnd} from %{pipelineLinkRefStart} %{pipelineRef} %{pipelineLinkRefEnd}"
msgstr ""
msgid "Pipeline Health"
msgstr ""
......@@ -7039,6 +7036,9 @@ msgstr ""
msgid "for this project"
msgstr ""
msgid "from"
msgstr ""
msgid "here"
msgstr ""
......
......@@ -60,7 +60,7 @@ describe 'Environment' do
context 'with manual action' do
let(:action) do
create(:ci_build, :manual, pipeline: pipeline,
name: 'deploy to production')
name: 'deploy to production', environment: environment.name)
end
context 'when user has ability to trigger deployment' do
......@@ -73,12 +73,16 @@ describe 'Environment' do
expect(page).to have_link(action.name.humanize)
end
it 'does allow to play manual action' do
it 'does allow to play manual action', :js do
expect(action).to be_manual
find('button.dropdown').click
expect { click_link(action.name.humanize) }
.not_to change { Ci::Pipeline.count }
wait_for_all_requests
expect(page).to have_content(action.name)
expect(action.reload).to be_pending
end
......@@ -165,10 +169,10 @@ describe 'Environment' do
name: action.ref, project: project)
end
it 'allows to stop environment' do
it 'allows to stop environment', :js do
click_button('Stop')
click_button('Stop environment') # confirm modal
wait_for_all_requests
expect(page).to have_content('close_app')
end
end
......
......@@ -38,9 +38,10 @@ describe 'User browses a job', :js do
let!(:build) { create(:ci_build, :failed, :trace_artifact, pipeline: pipeline) }
it 'displays the failure reason' do
wait_for_all_requests
within('.builds-container') do
build_link = first('.build-job > a')
expect(build_link['data-title']).to eq('test - failed - (unknown failure)')
expect(build_link['data-original-title']).to eq('test - failed - (unknown failure)')
end
end
end
......@@ -49,9 +50,10 @@ describe 'User browses a job', :js do
let!(:build) { create(:ci_build, :failed, :retried, :trace_artifact, pipeline: pipeline) }
it 'displays the failure reason and retried label' do
wait_for_all_requests
within('.builds-container') do
build_link = first('.build-job > a')
expect(build_link['data-title']).to eq('test - failed - (unknown failure) (retried)')
expect(build_link['data-original-title']).to eq('test - failed - (unknown failure) (retried)')
end
end
end
......
......@@ -134,23 +134,25 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do
expect(page).to have_content pipeline.commit.title
end
it 'shows active job' do
it 'shows active job', :js do
visit project_job_path(project, job)
wait_for_requests
expect(page).to have_selector('.build-job.active')
end
end
context 'sidebar' do
context 'sidebar', :js do
let(:job) { create(:ci_build, :success, :trace_live, pipeline: pipeline, name: '<img src=x onerror=alert(document.domain)>') }
before do
visit project_job_path(project, job)
wait_for_requests
end
it 'renders escaped tooltip name' do
page.within('aside.right-sidebar') do
expect(find('.active.build-job a')['data-title']).to eq('<img src="x"> - passed')
expect(find('.active.build-job a')['data-original-title']).to eq('&lt;img src=x onerror=alert(document.domain)&gt; - passed')
end
end
end
......
......@@ -57,25 +57,6 @@ describe('Job', () => {
expect(job.buildStage).toBe('test');
expect(job.state).toBe('');
});
it('only shows the jobs matching the current stage', () => {
expect($('.build-job[data-stage="build"]').is(':visible')).toBe(false);
expect($('.build-job[data-stage="test"]').is(':visible')).toBe(true);
expect($('.build-job[data-stage="deploy"]').is(':visible')).toBe(false);
});
it('selects the current stage in the build dropdown menu', () => {
expect($('.stage-selection').text()).toBe('test');
});
it('updates the jobs when the build dropdown changes', () => {
$('.stage-item:contains("build")').click();
expect($('.stage-selection').text()).toBe('build');
expect($('.build-job[data-stage="build"]').is(':visible')).toBe(true);
expect($('.build-job[data-stage="test"]').is(':visible')).toBe(false);
expect($('.build-job[data-stage="deploy"]').is(':visible')).toBe(false);
});
});
describe('running build', () => {
......
......@@ -2,7 +2,7 @@ import Vue from 'vue';
import component from '~/jobs/components/jobs_container.vue';
import mountComponent from '../../helpers/vue_mount_component_helper';
describe('Artifacts block', () => {
describe('Jobs List block', () => {
const Component = Vue.extend(component);
let vm;
......@@ -16,8 +16,7 @@ describe('Artifacts block', () => {
text: 'passed',
tooltip: 'passed',
},
path: 'job/233432756',
id: '233432756',
id: 233432756,
tooltip: 'build - passed',
retried: true,
};
......@@ -33,8 +32,7 @@ describe('Artifacts block', () => {
text: 'passed',
tooltip: 'passed',
},
path: 'job/2322756',
id: '2322756',
id: 2322756,
tooltip: 'build - passed',
active: true,
};
......@@ -50,8 +48,7 @@ describe('Artifacts block', () => {
text: 'passed',
tooltip: 'passed',
},
path: 'job/232153',
id: '232153',
id: 232153,
tooltip: 'build - passed',
};
......@@ -62,14 +59,16 @@ describe('Artifacts block', () => {
it('renders list of jobs', () => {
vm = mountComponent(Component, {
jobs: [job, retried, active],
jobId: 12313,
});
expect(vm.$el.querySelectorAll('a').length).toEqual(3);
});
it('renders arrow right when job is active', () => {
it('renders arrow right when job id matches `jobId`', () => {
vm = mountComponent(Component, {
jobs: [active],
jobId: active.id,
});
expect(vm.$el.querySelector('a .js-arrow-right')).not.toBeNull();
......@@ -78,6 +77,7 @@ describe('Artifacts block', () => {
it('does not render arrow right when job is not active', () => {
vm = mountComponent(Component, {
jobs: [job],
jobId: active.id,
});
expect(vm.$el.querySelector('a .js-arrow-right')).toBeNull();
......@@ -86,6 +86,7 @@ describe('Artifacts block', () => {
it('renders job name when present', () => {
vm = mountComponent(Component, {
jobs: [job],
jobId: active.id,
});
expect(vm.$el.querySelector('a').textContent.trim()).toContain(job.name);
......@@ -95,6 +96,7 @@ describe('Artifacts block', () => {
it('renders job id when job name is not available', () => {
vm = mountComponent(Component, {
jobs: [retried],
jobId: active.id,
});
expect(vm.$el.querySelector('a').textContent.trim()).toContain(retried.id);
......@@ -103,14 +105,16 @@ describe('Artifacts block', () => {
it('links to the job page', () => {
vm = mountComponent(Component, {
jobs: [job],
jobId: active.id,
});
expect(vm.$el.querySelector('a').getAttribute('href')).toEqual(job.path);
expect(vm.$el.querySelector('a').getAttribute('href')).toEqual(job.status.details_path);
});
it('renders retry icon when job was retried', () => {
vm = mountComponent(Component, {
jobs: [retried],
jobId: active.id,
});
expect(vm.$el.querySelector('.js-retry-icon')).not.toBeNull();
......@@ -119,6 +123,7 @@ describe('Artifacts block', () => {
it('does not render retry icon when job was not retried', () => {
vm = mountComponent(Component, {
jobs: [job],
jobId: active.id,
});
expect(vm.$el.querySelector('.js-retry-icon')).toBeNull();
......
import Vue from 'vue';
import sidebarDetailsBlock from '~/jobs/components/sidebar_details_block.vue';
import job from '../mock_data';
import mountComponent from '../../helpers/vue_mount_component_helper';
import sidebarDetailsBlock from '~/jobs/components/sidebar.vue';
import createStore from '~/jobs/store';
import job, { stages, jobsInStage } from '../mock_data';
import { mountComponentWithStore } from '../../helpers/vue_mount_component_helper';
import { trimText } from '../../helpers/vue_component_helper';
describe('Sidebar details block', () => {
let SidebarComponent;
const SidebarComponent = Vue.extend(sidebarDetailsBlock);
let vm;
function trimWhitespace(element) {
return element.textContent.replace(/\s+/g, ' ').trim();
}
let store;
beforeEach(() => {
SidebarComponent = Vue.extend(sidebarDetailsBlock);
store = createStore();
});
afterEach(() => {
......@@ -21,19 +20,21 @@ describe('Sidebar details block', () => {
describe('when it is loading', () => {
it('should render a loading spinner', () => {
vm = mountComponent(SidebarComponent, {
job: {},
isLoading: true,
});
store.dispatch('requestJob');
vm = mountComponentWithStore(SidebarComponent, { store });
expect(vm.$el.querySelector('.fa-spinner')).toBeDefined();
});
});
describe('when there is no retry path retry', () => {
it('should not render a retry button', () => {
vm = mountComponent(SidebarComponent, {
job: {},
isLoading: false,
const copy = Object.assign({}, job);
delete copy.retry_path;
store.dispatch('receiveJobSuccess', copy);
vm = mountComponentWithStore(SidebarComponent, {
store,
});
expect(vm.$el.querySelector('.js-retry-job')).toBeNull();
......@@ -42,10 +43,8 @@ describe('Sidebar details block', () => {
describe('without terminal path', () => {
it('does not render terminal link', () => {
vm = mountComponent(SidebarComponent, {
job,
isLoading: false,
});
store.dispatch('receiveJobSuccess', job);
vm = mountComponentWithStore(SidebarComponent, { store });
expect(vm.$el.querySelector('.js-terminal-link')).toBeNull();
});
......@@ -53,10 +52,12 @@ describe('Sidebar details block', () => {
describe('with terminal path', () => {
it('renders terminal link', () => {
vm = mountComponent(SidebarComponent, {
job,
isLoading: false,
store.dispatch('receiveJobSuccess', job);
vm = mountComponentWithStore(SidebarComponent, {
store,
props: {
terminalPath: 'job/43123/terminal',
},
});
expect(vm.$el.querySelector('.js-terminal-link')).not.toBeNull();
......@@ -64,10 +65,8 @@ describe('Sidebar details block', () => {
});
beforeEach(() => {
vm = mountComponent(SidebarComponent, {
job,
isLoading: false,
});
store.dispatch('receiveJobSuccess', job);
vm = mountComponentWithStore(SidebarComponent, { store });
});
describe('actions', () => {
......@@ -89,7 +88,7 @@ describe('Sidebar details block', () => {
describe('information', () => {
it('should render merge request link', () => {
expect(trimWhitespace(vm.$el.querySelector('.js-job-mr'))).toEqual('Merge Request: !2');
expect(trimText(vm.$el.querySelector('.js-job-mr').textContent)).toEqual('Merge Request: !2');
expect(vm.$el.querySelector('.js-job-mr a').getAttribute('href')).toEqual(
job.merge_request.path,
......@@ -97,43 +96,101 @@ describe('Sidebar details block', () => {
});
it('should render job duration', () => {
expect(trimWhitespace(vm.$el.querySelector('.js-job-duration'))).toEqual(
expect(trimText(vm.$el.querySelector('.js-job-duration').textContent)).toEqual(
'Duration: 6 seconds',
);
});
it('should render erased date', () => {
expect(trimWhitespace(vm.$el.querySelector('.js-job-erased'))).toEqual('Erased: 3 weeks ago');
expect(trimText(vm.$el.querySelector('.js-job-erased').textContent)).toEqual(
'Erased: 3 weeks ago',
);
});
it('should render finished date', () => {
expect(trimWhitespace(vm.$el.querySelector('.js-job-finished'))).toEqual(
expect(trimText(vm.$el.querySelector('.js-job-finished').textContent)).toEqual(
'Finished: 3 weeks ago',
);
});
it('should render queued date', () => {
expect(trimWhitespace(vm.$el.querySelector('.js-job-queued'))).toEqual('Queued: 9 seconds');
expect(trimText(vm.$el.querySelector('.js-job-queued').textContent)).toEqual(
'Queued: 9 seconds',
);
});
it('should render runner ID', () => {
expect(trimWhitespace(vm.$el.querySelector('.js-job-runner'))).toEqual(
expect(trimText(vm.$el.querySelector('.js-job-runner').textContent)).toEqual(
'Runner: local ci runner (#1)',
);
});
it('should render timeout information', () => {
expect(trimWhitespace(vm.$el.querySelector('.js-job-timeout'))).toEqual(
expect(trimText(vm.$el.querySelector('.js-job-timeout').textContent)).toEqual(
'Timeout: 1m 40s (from runner)',
);
});
it('should render coverage', () => {
expect(trimWhitespace(vm.$el.querySelector('.js-job-coverage'))).toEqual('Coverage: 20%');
expect(trimText(vm.$el.querySelector('.js-job-coverage').textContent)).toEqual(
'Coverage: 20%',
);
});
it('should render tags', () => {
expect(trimWhitespace(vm.$el.querySelector('.js-job-tags'))).toEqual('Tags: tag');
expect(trimText(vm.$el.querySelector('.js-job-tags').textContent)).toEqual('Tags: tag');
});
});
describe('stages dropdown', () => {
beforeEach(() => {
store.dispatch('receiveJobSuccess', job);
});
describe('while fetching stages', () => {
it('renders dropdown with More label', () => {
vm = mountComponentWithStore(SidebarComponent, { store });
expect(vm.$el.querySelector('.js-selected-stage').textContent.trim()).toEqual('More');
});
});
describe('with stages', () => {
beforeEach(() => {
store.dispatch('receiveStagesSuccess', stages);
vm = mountComponentWithStore(SidebarComponent, { store });
});
it('renders first stage as selected', () => {
expect(vm.$el.querySelector('.js-selected-stage').textContent.trim()).toEqual(
stages[0].name,
);
});
});
describe('without jobs for stages', () => {
beforeEach(() => {
store.dispatch('receiveJobSuccess', job);
store.dispatch('receiveStagesSuccess', stages);
vm = mountComponentWithStore(SidebarComponent, { store });
});
it('does not render job container', () => {
expect(vm.$el.querySelector('.js-jobs-container')).toBeNull();
});
});
describe('with jobs for stages', () => {
beforeEach(() => {
store.dispatch('receiveJobSuccess', job);
store.dispatch('receiveStagesSuccess', stages);
store.dispatch('receiveJobsForStageSuccess', jobsInStage.latest_statuses);
vm = mountComponentWithStore(SidebarComponent, { store });
});
it('renders list of jobs', () => {
expect(vm.$el.querySelector('.js-jobs-container')).not.toBeNull();
});
});
});
});
......@@ -8,19 +8,10 @@ describe('Artifacts block', () => {
beforeEach(() => {
vm = mountComponent(Component, {
pipelineId: 28029444,
pipelinePath: 'pipeline/28029444',
pipelineRef: '50101-truncated-job-information',
pipelineRefPath: 'commits/50101-truncated-job-information',
stages: [
{
name: 'build',
},
{
name: 'test',
},
],
pipelineStatus: {
pipeline: {
id: 28029444,
details: {
status: {
details_path: '/gitlab-org/gitlab-ce/pipelines/28029444',
group: 'success',
has_details: true,
......@@ -29,6 +20,21 @@ describe('Artifacts block', () => {
text: 'passed',
tooltip: 'passed',
},
},
path: 'pipeline/28029444',
},
ref: {
path: 'commits/50101-truncated-job-information',
name: '50101-truncated-job-information',
},
stages: [
{
name: 'build',
},
{
name: 'test',
},
],
});
});
......
This diff is collapsed.
......@@ -27,7 +27,6 @@ import {
receiveStagesSuccess,
receiveStagesError,
requestJobsForStage,
setSelectedStage,
fetchJobsForStage,
receiveJobsForStageSuccess,
receiveJobsForStageError,
......@@ -236,7 +235,8 @@ describe('Job State actions', () => {
},
{
payload: {
html: 'I, [2018-08-17T22:57:45.707325 #1841] INFO -- :', complete: true,
html: 'I, [2018-08-17T22:57:45.707325 #1841] INFO -- :',
complete: true,
},
type: 'receiveTraceSuccess',
},
......@@ -421,7 +421,9 @@ describe('Job State actions', () => {
let mock;
beforeEach(() => {
mockedState.stagesEndpoint = `${TEST_HOST}/endpoint.json`;
mockedState.job.pipeline = {
path: `${TEST_HOST}/endpoint.json/stages`,
};
mock = new MockAdapter(axios);
});
......@@ -430,8 +432,10 @@ describe('Job State actions', () => {
});
describe('success', () => {
it('dispatches requestStages and receiveStagesSuccess ', done => {
mock.onGet(`${TEST_HOST}/endpoint.json`).replyOnce(200, [{ id: 121212, name: 'build' }]);
it('dispatches requestStages and receiveStagesSuccess, fetchJobsForStage ', done => {
mock
.onGet(`${TEST_HOST}/endpoint.json/stages`)
.replyOnce(200, { details: { stages: [{ id: 121212, name: 'build' }] } });
testAction(
fetchStages,
......@@ -446,6 +450,10 @@ describe('Job State actions', () => {
payload: [{ id: 121212, name: 'build' }],
type: 'receiveStagesSuccess',
},
{
payload: { id: 121212, name: 'build' },
type: 'fetchJobsForStage',
},
],
done,
);
......@@ -516,24 +524,10 @@ describe('Job State actions', () => {
});
});
describe('setSelectedStage', () => {
it('should commit SET_SELECTED_STAGE mutation ', done => {
testAction(
setSelectedStage,
{ name: 'build' },
mockedState,
[{ type: types.SET_SELECTED_STAGE, payload: { name: 'build' } }],
[],
done,
);
});
});
describe('fetchJobsForStage', () => {
let mock;
beforeEach(() => {
mockedState.stageJobsEndpoint = `${TEST_HOST}/endpoint.json`;
mock = new MockAdapter(axios);
});
......@@ -542,19 +536,17 @@ describe('Job State actions', () => {
});
describe('success', () => {
it('dispatches setSelectedStage, requestJobsForStage and receiveJobsForStageSuccess ', done => {
mock.onGet(`${TEST_HOST}/endpoint.json`).replyOnce(200, [{ id: 121212, name: 'build' }]);
it('dispatches requestJobsForStage and receiveJobsForStageSuccess ', done => {
mock
.onGet(`${TEST_HOST}/jobs.json`)
.replyOnce(200, { latest_statuses: [{ id: 121212, name: 'build' }], retried: [] });
testAction(
fetchJobsForStage,
null,
{ dropdown_path: `${TEST_HOST}/jobs.json` },
mockedState,
[],
[
{
type: 'setSelectedStage',
payload: null,
},
{
type: 'requestJobsForStage',
},
......@@ -570,20 +562,16 @@ describe('Job State actions', () => {
describe('error', () => {
beforeEach(() => {
mock.onGet(`${TEST_HOST}/endpoint.json`).reply(500);
mock.onGet(`${TEST_HOST}/jobs.json`).reply(500);
});
it('dispatches setSelectedStage, requestJobsForStage and receiveJobsForStageError', done => {
it('dispatches requestJobsForStage and receiveJobsForStageError', done => {
testAction(
fetchJobsForStage,
null,
{ dropdown_path: `${TEST_HOST}/jobs.json` },
mockedState,
[],
[
{
payload: null,
type: 'setSelectedStage',
},
{
type: 'requestJobsForStage',
},
......
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