Commit 5bb7067a authored by Phil Hughes's avatar Phil Hughes

Merge branch '50904-job-log' into 'master'

Resolve "Integrate new vue+vuex code base with new API and remove old haml code"

Closes #50904

See merge request gitlab-org/gitlab-ce!22116
parents 712f41e1 5ed91cf8
import $ from 'jquery';
import _ from 'underscore';
import { polyfillSticky } from './lib/utils/sticky';
import axios from './lib/utils/axios_utils';
import { visitUrl } from './lib/utils/url_utility';
import bp from './breakpoints';
import { numberToHumanSize } from './lib/utils/number_utils';
import { setCiStatusFavicon } from './lib/utils/common_utils';
import { isScrolledToBottom, scrollDown, scrollUp } from './lib/utils/scroll_utils';
import LogOutputBehaviours from './lib/utils/logoutput_behaviours';
export default class Job extends LogOutputBehaviours {
constructor(options) {
super();
this.timeout = null;
this.state = null;
this.fetchingStatusFavicon = false;
this.options = options || $('.js-build-options').data();
this.pagePath = this.options.pagePath;
this.buildStatus = this.options.buildStatus;
this.state = this.options.logState;
this.buildStage = this.options.buildStage;
this.$document = $(document);
this.$window = $(window);
this.logBytes = 0;
this.$buildTrace = $('#build-trace');
this.$buildRefreshAnimation = $('.js-build-refresh');
this.$truncatedInfo = $('.js-truncated-info');
this.$buildTraceOutput = $('.js-build-output');
this.$topBar = $('.js-top-bar');
clearTimeout(this.timeout);
this.initSidebar();
this.sidebarOnResize();
this.$document
.off('click', '.js-sidebar-build-toggle')
.on('click', '.js-sidebar-build-toggle', this.sidebarOnClick.bind(this));
this.scrollThrottled = _.throttle(this.toggleScroll.bind(this), 100);
this.$window.off('scroll').on('scroll', () => {
if (!isScrolledToBottom()) {
this.toggleScrollAnimation(false);
} else if (isScrolledToBottom() && !this.isLogComplete) {
this.toggleScrollAnimation(true);
}
this.scrollThrottled();
});
this.$window
.off('resize.build')
.on('resize.build', _.throttle(this.sidebarOnResize.bind(this), 100));
this.initAffixTopArea();
this.getBuildTrace();
}
initAffixTopArea() {
polyfillSticky(this.$topBar);
}
scrollToBottom() {
scrollDown();
this.hasBeenScrolled = true;
this.toggleScroll();
}
scrollToTop() {
scrollUp();
this.hasBeenScrolled = true;
this.toggleScroll();
}
toggleScrollAnimation(toggle) {
this.$scrollBottomBtn.toggleClass('animate', toggle);
}
initSidebar() {
this.$sidebar = $('.js-build-sidebar');
}
getBuildTrace() {
return axios
.get(`${this.pagePath}/trace.json`, {
params: { state: this.state },
})
.then(res => {
const log = res.data;
if (!this.fetchingStatusFavicon) {
this.fetchingStatusFavicon = true;
setCiStatusFavicon(`${this.pagePath}/status.json`)
.then(() => {
this.fetchingStatusFavicon = false;
})
.catch(() => {
this.fetchingStatusFavicon = false;
});
}
if (log.state) {
this.state = log.state;
}
this.isScrollInBottom = isScrolledToBottom();
if (log.append) {
this.$buildTraceOutput.append(log.html);
this.logBytes += log.size;
} else {
this.$buildTraceOutput.html(log.html);
this.logBytes = log.size;
}
// if the incremental sum of logBytes we received is less than the total
// we need to show a message warning the user about that.
if (this.logBytes < log.total) {
// size is in bytes, we need to calculate KiB
const size = numberToHumanSize(this.logBytes);
$('.js-truncated-info-size').html(`${size}`);
this.$truncatedInfo.removeClass('hidden');
} else {
this.$truncatedInfo.addClass('hidden');
}
this.isLogComplete = log.complete;
if (log.complete === false) {
this.timeout = setTimeout(() => {
this.getBuildTrace();
}, 4000);
} else {
this.$buildRefreshAnimation.remove();
this.toggleScrollAnimation(false);
}
if (log.status !== this.buildStatus) {
visitUrl(this.pagePath);
}
})
.catch(() => {
this.$buildRefreshAnimation.remove();
})
.then(() => {
if (this.isScrollInBottom) {
scrollDown();
}
})
.then(() => this.toggleScroll());
}
// eslint-disable-next-line class-methods-use-this
shouldHideSidebarForViewport() {
const bootstrapBreakpoint = bp.getBreakpointSize();
return bootstrapBreakpoint === 'xs';
}
toggleSidebar(shouldHide) {
const shouldShow = typeof shouldHide === 'boolean' ? !shouldHide : undefined;
const $toggleButton = $('.js-sidebar-build-toggle-header');
this.$sidebar
.toggleClass('right-sidebar-expanded', shouldShow)
.toggleClass('right-sidebar-collapsed', shouldHide);
this.$topBar
.toggleClass('sidebar-expanded', shouldShow)
.toggleClass('sidebar-collapsed', shouldHide);
if (this.$sidebar.hasClass('right-sidebar-expanded')) {
$toggleButton.addClass('hidden');
} else {
$toggleButton.removeClass('hidden');
}
}
sidebarOnResize() {
this.toggleSidebar(this.shouldHideSidebarForViewport());
}
sidebarOnClick() {
if (this.shouldHideSidebarForViewport()) this.toggleSidebar();
}
}
<script>
import { mapGetters, mapState } from 'vuex';
import _ from 'underscore';
import { mapGetters, mapState, mapActions } from 'vuex';
import { isScrolledToBottom } from '~/lib/utils/scroll_utils';
import bp from '~/breakpoints';
import CiHeader from '~/vue_shared/components/header_ci_component.vue';
import Callout from '~/vue_shared/components/callout.vue';
import createStore from '../store';
import EmptyState from './empty_state.vue';
import EnvironmentsBlock from './environments_block.vue';
import ErasedBlock from './erased_block.vue';
import Log from './job_log.vue';
import LogTopBar from './job_log_controllers.vue';
import StuckBlock from './stuck_block.vue';
import Sidebar from './sidebar.vue';
export default {
name: 'JobPageApp',
store: createStore(),
components: {
CiHeader,
Callout,
EmptyState,
EnvironmentsBlock,
ErasedBlock,
Log,
LogTopBar,
StuckBlock,
Sidebar,
},
props: {
runnerSettingsUrl: {
......@@ -23,9 +34,43 @@
required: false,
default: null,
},
runnerHelpUrl: {
type: String,
required: false,
default: null,
},
endpoint: {
type: String,
required: true,
},
terminalPath: {
type: String,
required: false,
default: null,
},
pagePath: {
type: String,
required: true,
},
logState: {
type: String,
required: true,
},
},
computed: {
...mapState(['isLoading', 'job']),
...mapState([
'isLoading',
'job',
'isSidebarOpen',
'trace',
'isTraceComplete',
'traceSize',
'isTraceSizeVisible',
'isScrollBottomDisabled',
'isScrollTopDisabled',
'isScrolledToBottomBeforeReceivingTrace',
'hasError',
]),
...mapGetters([
'headerActions',
'headerTime',
......@@ -35,7 +80,83 @@
'isJobStuck',
'hasTrace',
'emptyStateIllustration',
'isScrollingDown',
'emptyStateAction',
]),
shouldRenderContent() {
return !this.isLoading && !this.hasError;
}
},
watch: {
// Once the job log is loaded,
// fetch the stages for the dropdown on the sidebar
job(newVal, oldVal) {
if (_.isEmpty(oldVal) && !_.isEmpty(newVal.pipeline)) {
this.fetchStages();
}
},
},
created() {
this.throttled = _.throttle(this.toggleScrollButtons, 100);
this.setJobEndpoint(this.endpoint);
this.setTraceOptions({
logState: this.logState,
pagePath: this.pagePath,
});
this.fetchJob();
this.fetchTrace();
window.addEventListener('resize', this.onResize);
window.addEventListener('scroll', this.updateScroll);
},
mounted() {
this.updateSidebar();
},
destroyed() {
window.removeEventListener('resize', this.onResize);
window.removeEventListener('scroll', this.updateScroll);
},
methods: {
...mapActions([
'setJobEndpoint',
'setTraceOptions',
'fetchJob',
'fetchStages',
'hideSidebar',
'showSidebar',
'toggleSidebar',
'fetchTrace',
'scrollBottom',
'scrollTop',
'toggleScrollButtons',
'toggleScrollAnimation',
]),
onResize() {
this.updateSidebar();
this.updateScroll();
},
updateSidebar() {
if (bp.getBreakpointSize() === 'xs') {
this.hideSidebar();
} else if (!this.isSidebarOpen) {
this.showSidebar();
}
},
updateScroll() {
if (!isScrolledToBottom()) {
this.toggleScrollAnimation(false);
} else if (this.isScrollingDown) {
this.toggleScrollAnimation(true);
}
this.throttled();
},
},
};
</script>
......@@ -44,10 +165,11 @@
<gl-loading-icon
v-if="isLoading"
:size="2"
class="prepend-top-20"
class="js-job-loading prepend-top-20"
/>
<template v-else>
<template v-else-if="shouldRenderContent">
<div class="js-job-content build-page">
<!-- Header Section -->
<header>
<div class="js-build-header build-header top-area">
......@@ -60,6 +182,7 @@
:has-sidebar-button="true"
:should-render-triggered-label="shouldRenderTriggeredLabel"
:item-name="__('Job')"
@clickedSidebarButton="toggleSidebar"
/>
</div>
......@@ -94,6 +217,29 @@
/>
<!--job log -->
<div
v-if="hasTrace"
class="build-trace-container prepend-top-default">
<log-top-bar
:class="{
'sidebar-expanded': isSidebarOpen,
'sidebar-collapsed': !isSidebarOpen
}"
:erase-path="job.erase_path"
:size="traceSize"
:raw-path="job.raw_path"
:is-scroll-bottom-disabled="isScrollBottomDisabled"
:is-scroll-top-disabled="isScrollTopDisabled"
:is-trace-size-visible="isTraceSizeVisible"
:is-scrolling-down="isScrollingDown"
@scrollJobLogTop="scrollTop"
@scrollJobLogBottom="scrollBottom"
/>
<log
:trace="trace"
:is-complete="isTraceComplete"
/>
</div>
<!-- EO job log -->
<!--empty state -->
......@@ -104,11 +250,22 @@
:illustration-size-class="emptyStateIllustration.size"
:title="emptyStateIllustration.title"
:content="emptyStateIllustration.content"
:action="job.status.action"
:action="emptyStateAction"
/>
<!-- EO empty state -->
<!-- EO Body Section -->
</div>
</template>
<sidebar
v-if="shouldRenderContent"
class="js-job-sidebar"
:class="{
'right-sidebar-expanded': isSidebarOpen,
'right-sidebar-collapsed': !isSidebarOpen
}"
:runner-help-url="runnerHelpUrl"
/>
</div>
</template>
<script>
export default {
import { mapState, mapActions } from 'vuex';
export default {
name: 'JobLog',
props: {
trace: {
......@@ -11,10 +13,36 @@ export default {
required: true,
},
},
};
computed: {
...mapState(['isScrolledToBottomBeforeReceivingTrace']),
},
updated() {
this.$nextTick(() => this.handleScrollDown());
},
mounted() {
this.$nextTick(() => this.handleScrollDown());
},
methods: {
...mapActions(['scrollBottom']),
/**
* The job log is sent in HTML, which means we need to use `v-html` to render it
* Using the updated hook with $nextTick is not enough to wait for the DOM to be updated
* in this case because it runs before `v-html` has finished running, since there's no
* Vue binding.
* In order to scroll the page down after `v-html` has finished, we need to use setTimeout
*/
handleScrollDown() {
if (this.isScrolledToBottomBeforeReceivingTrace) {
setTimeout(() => {
this.scrollBottom();
}, 0);
}
},
},
};
</script>
<template>
<pre class="build-trace">
<pre class="js-build-trace build-trace">
<code
class="bash"
v-html="trace"
......@@ -22,7 +50,7 @@ export default {
</code>
<div
v-if="isComplete"
v-if="!isComplete"
class="js-log-animation build-loader-animation"
>
<div class="dot"></div>
......
......@@ -4,6 +4,7 @@ import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
import { numberToHumanSize } from '~/lib/utils/number_utils';
import { sprintf } from '~/locale';
import scrollDown from '../svg/scroll_down.svg';
export default {
components: {
......@@ -12,6 +13,7 @@ export default {
directives: {
tooltip,
},
scrollDown,
props: {
erasePath: {
type: String,
......@@ -65,7 +67,7 @@ export default {
};
</script>
<template>
<div class="top-bar affix js-top-bar">
<div class="top-bar affix">
<!-- truncate information -->
<div class="js-truncated-info truncated-info d-none d-sm-block float-left">
<template v-if="isTraceSizeVisible">
......@@ -100,7 +102,7 @@ export default {
v-tooltip
:title="s__('Job|Erase job log')"
:href="erasePath"
data-confirm="__('Are you sure you want to erase this build?')"
:data-confirm="__('Are you sure you want to erase this build?')"
class="js-erase-link controllers-buttons"
data-container="body"
data-method="post"
......@@ -138,8 +140,8 @@ export default {
class="js-scroll-bottom btn-scroll btn-transparent btn-blank"
:class="{ animate: isScrollingDown }"
@click="handleScrollToBottom"
v-html="$options.scrollDown"
>
<icon name="scroll_down"/>
</button>
</div>
<!-- eo scroll buttons -->
......
......@@ -29,14 +29,9 @@ export default {
required: false,
default: '',
},
terminalPath: {
type: String,
required: false,
default: null,
},
},
computed: {
...mapState(['job', 'isLoading', 'stages', 'jobs', 'selectedStage']),
...mapState(['job', 'stages', 'jobs', 'selectedStage']),
coverage() {
return `${this.job.coverage}%`;
},
......@@ -100,23 +95,22 @@ export default {
);
},
commit() {
return this.job.pipeline.commit || {};
return this.job.pipeline && this.job.pipeline.commit ? this.job.pipeline.commit : {};
},
},
methods: {
...mapActions(['fetchJobsForStage']),
...mapActions(['fetchJobsForStage', 'toggleSidebar']),
},
};
</script>
<template>
<aside
class="js-build-sidebar right-sidebar right-sidebar-expanded build-sidebar"
class="right-sidebar 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 }}
......@@ -131,8 +125,8 @@ export default {
{{ __('Retry') }}
</a>
<a
v-if="terminalPath"
:href="terminalPath"
v-if="job.terminal_path"
:href="job.terminal_path"
class="js-terminal-link pull-right btn btn-primary
btn-inverted visible-md-block visible-lg-block"
target="_blank"
......@@ -145,6 +139,7 @@ export default {
type="button"
class="btn btn-blank gutter-toggle
float-right d-block d-md-none js-sidebar-build-toggle"
@click="toggleSidebar"
>
<i
aria-hidden="true"
......@@ -259,6 +254,7 @@ export default {
</a>
</div>
</div>
<artifacts-block
v-if="hasArtifact"
:artifact="job.artifact"
......@@ -279,17 +275,10 @@ export default {
:selected-stage="selectedStage"
@requestSidebarStageDropdown="fetchJobsForStage"
/>
</template>
<gl-loading-icon
v-else
:size="2"
class="prepend-top-10"
/>
</div>
<jobs-container
v-if="!isLoading && jobs.length"
v-if="jobs.length"
:jobs="jobs"
:job-id="job.id"
/>
......
import Vue from 'vue';
import JobApp from './components/job_app.vue';
export default () => {
const element = document.getElementById('js-job-vue-app');
return new Vue({
el: element,
components: {
JobApp,
},
render(createElement) {
return createElement('job-app', {
props: {
runnerHelpUrl: element.dataset.runnerHelpUrl,
runnerSettingsUrl: element.dataset.runnerSettingsUrl,
endpoint: element.dataset.endpoint,
pagePath: element.dataset.buildOptionsPagePath,
logState: element.dataset.buildOptionsLogState,
buildStatus: element.dataset.buildOptionsBuildStatus,
},
});
},
});
};
import _ from 'underscore';
import { mapState, mapActions } from 'vuex';
import Vue from 'vue';
import Job from '../job';
import JobApp from './components/job_app.vue';
import Sidebar from './components/sidebar.vue';
import createStore from './store';
export default () => {
const { dataset } = document.getElementById('js-job-details-vue');
const store = createStore();
store.dispatch('setJobEndpoint', dataset.endpoint);
store.dispatch('fetchJob');
// Header
// eslint-disable-next-line no-new
new Vue({
el: '#js-build-header-vue',
components: {
JobApp,
},
store,
computed: {
...mapState(['job', 'isLoading']),
},
render(createElement) {
return createElement('job-app', {
props: {
isLoading: this.isLoading,
job: this.job,
runnerSettingsUrl: dataset.runnerSettingsUrl,
},
});
},
});
// Sidebar information block
const detailsBlockElement = document.getElementById('js-details-block-vue');
const detailsBlockDataset = detailsBlockElement.dataset;
// eslint-disable-next-line
new Vue({
el: detailsBlockElement,
components: {
Sidebar,
},
computed: {
...mapState(['job']),
},
watch: {
job(newVal, oldVal) {
if (_.isEmpty(oldVal) && !_.isEmpty(newVal.pipeline)) {
this.fetchStages();
}
},
},
methods: {
...mapActions(['fetchStages']),
},
store,
render(createElement) {
return createElement('sidebar', {
props: {
runnerHelpUrl: dataset.runnerHelpUrl,
terminalPath: detailsBlockDataset.terminalPath,
},
});
},
});
// eslint-disable-next-line no-new
new Job();
};
import Visibility from 'visibilityjs';
import * as types from './mutation_types';
import axios from '../../lib/utils/axios_utils';
import Poll from '../../lib/utils/poll';
import { setCiStatusFavicon } from '../../lib/utils/common_utils';
import flash from '../../flash';
import { __ } from '../../locale';
import axios from '~/lib/utils/axios_utils';
import Poll from '~/lib/utils/poll';
import { setFaviconOverlay, resetFavicon } from '~/lib/utils/common_utils';
import flash from '~/flash';
import { __ } from '~/locale';
import {
canScroll,
isScrolledToBottom,
isScrolledToTop,
isScrolledToMiddle,
scrollDown,
scrollUp,
} from '~/lib/utils/scroll_utils';
export const setJobEndpoint = ({ commit }, endpoint) => commit(types.SET_JOB_ENDPOINT, endpoint);
export const setTraceEndpoint = ({ commit }, endpoint) =>
commit(types.SET_TRACE_ENDPOINT, endpoint);
export const setStagesEndpoint = ({ commit }, endpoint) =>
commit(types.SET_STAGES_ENDPOINT, endpoint);
export const setJobsEndpoint = ({ commit }, endpoint) => commit(types.SET_JOBS_ENDPOINT, endpoint);
export const setTraceOptions = ({ commit }, options) => commit(types.SET_TRACE_OPTIONS, options);
export const hideSidebar = ({ commit }) => commit(types.HIDE_SIDEBAR);
export const showSidebar = ({ commit }) => commit(types.SHOW_SIDEBAR);
export const toggleSidebar = ({ dispatch, state }) => {
if (state.isSidebarOpen) {
dispatch('hideSidebar');
} else {
dispatch('showSidebar');
}
};
let eTagPoll;
......@@ -62,41 +77,84 @@ export const fetchJob = ({ state, dispatch }) => {
});
};
export const receiveJobSuccess = ({ commit }, data) => {
export const receiveJobSuccess = ({ commit }, data = {}) => {
commit(types.RECEIVE_JOB_SUCCESS, data);
if (data.favicon) {
setFaviconOverlay(data.favicon);
} else {
resetFavicon();
}
};
export const receiveJobError = ({ commit }) => {
commit(types.RECEIVE_JOB_ERROR);
flash(__('An error occurred while fetching the job.'));
resetFavicon();
};
/**
* Job's Trace
*/
export const scrollTop = ({ commit }) => {
commit(types.SCROLL_TO_TOP);
window.scrollTo({ top: 0 });
export const scrollTop = ({ dispatch }) => {
scrollUp();
dispatch('toggleScrollButtons');
};
export const scrollBottom = ({ commit }) => {
commit(types.SCROLL_TO_BOTTOM);
window.scrollTo({ top: document.height });
export const scrollBottom = ({ dispatch }) => {
scrollDown();
dispatch('toggleScrollButtons');
};
/**
* Responsible for toggling the disabled state of the scroll buttons
*/
export const toggleScrollButtons = ({ dispatch }) => {
if (canScroll()) {
if (isScrolledToMiddle()) {
dispatch('enableScrollTop');
dispatch('enableScrollBottom');
} else if (isScrolledToTop()) {
dispatch('disableScrollTop');
dispatch('enableScrollBottom');
} else if (isScrolledToBottom()) {
dispatch('disableScrollBottom');
dispatch('enableScrollTop');
}
} else {
dispatch('disableScrollBottom');
dispatch('disableScrollTop');
}
};
export const disableScrollBottom = ({ commit }) => commit(types.DISABLE_SCROLL_BOTTOM);
export const disableScrollTop = ({ commit }) => commit(types.DISABLE_SCROLL_TOP);
export const enableScrollBottom = ({ commit }) => commit(types.ENABLE_SCROLL_BOTTOM);
export const enableScrollTop = ({ commit }) => commit(types.ENABLE_SCROLL_TOP);
/**
* While the automatic scroll down is active,
* we show the scroll down button with an animation
*/
export const toggleScrollAnimation = ({ commit }, toggle) =>
commit(types.TOGGLE_SCROLL_ANIMATION, toggle);
/**
* Responsible to handle automatic scroll
*/
export const toggleScrollisInBottom = ({ commit }, toggle) => {
commit(types.TOGGLE_IS_SCROLL_IN_BOTTOM_BEFORE_UPDATING_TRACE, toggle);
};
export const requestTrace = ({ commit }) => commit(types.REQUEST_TRACE);
let traceTimeout;
export const fetchTrace = ({ dispatch, state }) => {
dispatch('requestTrace');
export const fetchTrace = ({ dispatch, state }) =>
axios
.get(`${state.traceEndpoint}/trace.json`, {
params: { state: state.traceState },
})
.then(({ data }) => {
if (!state.fetchingStatusFavicon) {
dispatch('fetchFavicon');
}
dispatch('toggleScrollisInBottom', isScrolledToBottom());
dispatch('receiveTraceSuccess', data);
if (!data.complete) {
......@@ -108,7 +166,7 @@ export const fetchTrace = ({ dispatch, state }) => {
}
})
.catch(() => dispatch('receiveTraceError'));
};
export const stopPollingTrace = ({ commit }) => {
commit(types.STOP_POLLING_TRACE);
clearTimeout(traceTimeout);
......@@ -120,17 +178,6 @@ export const receiveTraceError = ({ commit }) => {
flash(__('An error occurred while fetching the job log.'));
};
export const fetchFavicon = ({ state, dispatch }) => {
dispatch('requestStatusFavicon');
setCiStatusFavicon(`${state.pagePath}/status.json`)
.then(() => dispatch('receiveStatusFaviconSuccess'))
.catch(() => dispatch('requestStatusFaviconError'));
};
export const requestStatusFavicon = ({ commit }) => commit(types.REQUEST_STATUS_FAVICON);
export const receiveStatusFaviconSuccess = ({ commit }) =>
commit(types.RECEIVE_STATUS_FAVICON_SUCCESS);
export const requestStatusFaviconError = ({ commit }) => commit(types.RECEIVE_STATUS_FAVICON_ERROR);
/**
* Stages dropdown on sidebar
*/
......
import _ from 'underscore';
import { __ } from '~/locale';
import { isScrolledToBottom } from '~/lib/utils/scroll_utils';
export const headerActions = state => {
if (state.job.new_issue_path) {
......@@ -34,11 +35,12 @@ export const hasEnvironment = state => !_.isEmpty(state.job.deployment_status);
* Used to check if it should render the job log or the empty state
* @returns {Boolean}
*/
export const hasTrace = state => state.job.has_trace || state.job.status.group === 'running';
export const hasTrace = state => state.job.has_trace || (!_.isEmpty(state.job.status) && state.job.status.group === 'running');
export const emptyStateIllustration = state =>
(state.job && state.job.status && state.job.status.illustration) || {};
export const emptyStateAction = state => (state.job && state.job.status && state.job.status.action) || {};
/**
* When the job is pending and there are no available runners
* we need to render the stuck block;
......@@ -46,8 +48,10 @@ export const emptyStateIllustration = state =>
* @returns {Boolean}
*/
export const isJobStuck = state =>
state.job.status.group === 'pending' &&
(!_.isEmpty(state.job.status) && state.job.status.group === 'pending') &&
(!_.isEmpty(state.job.runners) && state.job.runners.available === false);
export const isScrollingDown = state => isScrolledToBottom() && !state.isTraceComplete;
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
export const SET_JOB_ENDPOINT = 'SET_JOB_ENDPOINT';
export const SET_TRACE_ENDPOINT = 'SET_TRACE_ENDPOINT';
export const SET_STAGES_ENDPOINT = 'SET_STAGES_ENDPOINT';
export const SET_JOBS_ENDPOINT = 'SET_JOBS_ENDPOINT';
export const SET_TRACE_OPTIONS = 'SET_TRACE_OPTIONS';
export const HIDE_SIDEBAR = 'HIDE_SIDEBAR';
export const SHOW_SIDEBAR = 'SHOW_SIDEBAR';
export const SCROLL_TO_TOP = 'SCROLL_TO_TOP';
export const SCROLL_TO_BOTTOM = 'SCROLL_TO_BOTTOM';
export const DISABLE_SCROLL_BOTTOM = 'DISABLE_SCROLL_BOTTOM';
export const DISABLE_SCROLL_TOP = 'DISABLE_SCROLL_TOP';
export const ENABLE_SCROLL_BOTTOM = 'ENABLE_SCROLL_BOTTOM';
export const ENABLE_SCROLL_TOP = 'ENABLE_SCROLL_TOP';
// TODO
export const TOGGLE_SCROLL_ANIMATION = 'TOGGLE_SCROLL_ANIMATION';
export const TOGGLE_IS_SCROLL_IN_BOTTOM_BEFORE_UPDATING_TRACE = 'TOGGLE_IS_SCROLL_IN_BOTTOM';
export const REQUEST_JOB = 'REQUEST_JOB';
export const RECEIVE_JOB_SUCCESS = 'RECEIVE_JOB_SUCCESS';
......@@ -15,10 +24,6 @@ export const STOP_POLLING_TRACE = 'STOP_POLLING_TRACE';
export const RECEIVE_TRACE_SUCCESS = 'RECEIVE_TRACE_SUCCESS';
export const RECEIVE_TRACE_ERROR = 'RECEIVE_TRACE_ERROR';
export const REQUEST_STATUS_FAVICON = 'REQUEST_STATUS_FAVICON';
export const RECEIVE_STATUS_FAVICON_SUCCESS = 'RECEIVE_STATUS_FAVICON_SUCCESS';
export const RECEIVE_STATUS_FAVICON_ERROR = 'RECEIVE_STATUS_FAVICON_ERROR';
export const REQUEST_STAGES = 'REQUEST_STAGES';
export const RECEIVE_STAGES_SUCCESS = 'RECEIVE_STAGES_SUCCESS';
export const RECEIVE_STAGES_ERROR = 'RECEIVE_STAGES_ERROR';
......
......@@ -4,14 +4,17 @@ export default {
[types.SET_JOB_ENDPOINT](state, endpoint) {
state.jobEndpoint = endpoint;
},
[types.REQUEST_STATUS_FAVICON](state) {
state.fetchingStatusFavicon = true;
[types.SET_TRACE_OPTIONS](state, options = {}) {
state.traceEndpoint = options.pagePath;
state.traceState = options.logState;
},
[types.RECEIVE_STATUS_FAVICON_SUCCESS](state) {
state.fetchingStatusFavicon = false;
[types.HIDE_SIDEBAR](state) {
state.isSidebarOpen = false;
},
[types.RECEIVE_STATUS_FAVICON_ERROR](state) {
state.fetchingStatusFavicon = false;
[types.SHOW_SIDEBAR](state) {
state.isSidebarOpen = true;
},
[types.RECEIVE_TRACE_SUCCESS](state, log) {
......@@ -23,8 +26,12 @@ export default {
state.trace += log.html;
state.traceSize += log.size;
} else {
state.trace = log.html;
state.traceSize = log.size;
// When the job still does not have a trace
// the trace response will not have a defined
// html or size. We keep the old value otherwise these
// will be set to `undefined`
state.trace = log.html || state.trace;
state.traceSize = log.size || state.traceSize;
}
if (state.traceSize < log.total) {
......@@ -33,25 +40,29 @@ export default {
state.isTraceSizeVisible = false;
}
state.isTraceComplete = log.complete;
state.hasTraceError = false;
state.isTraceComplete = log.complete || state.isTraceComplete;
},
/**
* Will remove loading animation
*/
[types.STOP_POLLING_TRACE](state) {
state.isTraceComplete = true;
},
// todo_fl: check this.
/**
* Will remove loading animation
*/
[types.RECEIVE_TRACE_ERROR](state) {
state.isLoadingTrace = false;
state.isTraceComplete = true;
state.hasTraceError = true;
},
[types.REQUEST_JOB](state) {
state.isLoading = true;
},
[types.RECEIVE_JOB_SUCCESS](state, job) {
state.isLoading = false;
state.hasError = false;
state.isLoading = false;
state.job = job;
/**
......@@ -66,17 +77,28 @@ export default {
},
[types.RECEIVE_JOB_ERROR](state) {
state.isLoading = false;
state.hasError = true;
state.job = {};
state.hasError = true;
},
[types.SCROLL_TO_TOP](state) {
state.isTraceScrolledToBottom = false;
state.hasBeenScrolled = true;
[types.ENABLE_SCROLL_TOP](state) {
state.isScrollTopDisabled = false;
},
[types.DISABLE_SCROLL_TOP](state) {
state.isScrollTopDisabled = true;
},
[types.SCROLL_TO_BOTTOM](state) {
state.isTraceScrolledToBottom = true;
state.hasBeenScrolled = true;
[types.ENABLE_SCROLL_BOTTOM](state) {
state.isScrollBottomDisabled = false;
},
[types.DISABLE_SCROLL_BOTTOM](state) {
state.isScrollBottomDisabled = true;
},
[types.TOGGLE_SCROLL_ANIMATION](state, toggle) {
state.isScrollingDown = toggle;
},
[types.TOGGLE_IS_SCROLL_IN_BOTTOM_BEFORE_UPDATING_TRACE](state, toggle) {
state.isScrolledToBottomBeforeReceivingTrace = toggle;
},
[types.REQUEST_STAGES](state) {
......
......@@ -4,36 +4,29 @@ export default () => ({
jobEndpoint: null,
traceEndpoint: null,
// dropdown options
stagesEndpoint: null,
// list of jobs on sidebard
stageJobsEndpoint: null,
// sidebar
isSidebarOpen: true,
// job log
isLoading: false,
hasError: false,
job: {},
// trace
isLoadingTrace: false,
hasTraceError: false,
// scroll buttons state
isScrollBottomDisabled: true,
isScrollTopDisabled: true,
trace: '',
isTraceScrolledToBottom: false,
hasBeenScrolled: false,
// Used to check if we should keep the automatic scroll
isScrolledToBottomBeforeReceivingTrace: true,
trace: '',
isTraceComplete: false,
traceSize: 0, // todo_fl: needs to be converted into human readable format in components
traceSize: 0,
isTraceSizeVisible: false,
fetchingStatusFavicon: false,
// used as a query parameter
// used as a query parameter to fetch the trace
traceState: null,
// used to check if we need to redirect the user - todo_fl: check if actually needed
traceStatus: null,
// sidebar dropdown
// sidebar dropdown & list of jobs
isLoadingStages: false,
isLoadingJobs: false,
selectedStage: __('More'),
......
<svg width="12" height="16" viewBox="0 0 12 16" xmlns="http://www.w3.org/2000/svg">
<path class="first-triangle" d="M1.048 14.155a.508.508 0 0 0-.32.105c-.091.07-.136.154-.136.25v.71c0 .095.045.178.135.249.09.07.197.105.321.105h10.043c.124 0 .23-.035.321-.105.09-.07.136-.154.136-.25v-.71c0-.095-.045-.178-.136-.249a.508.508 0 0 0-.32-.105"/>
<path class="second-triangle" d="M.687 8.027c-.09-.087-.122-.16-.093-.22.028-.06.104-.09.228-.09h10.5c.123 0 .2.03.228.09.029.06-.002.133-.093.22L6.393 12.91a.458.458 0 0 1-.136.089h-.37a.626.626 0 0 1-.136-.09"/>
<path class="third-triangle" d="M.687 1.027C.597.94.565.867.594.807c.028-.06.104-.09.228-.09h10.5c.123 0 .2.03.228.09.029.06-.002.133-.093.22L6.393 5.91A.458.458 0 0 1 6.257 6h-.37a.626.626 0 0 1-.136-.09"/>
</svg>
import initJobDetails from '~/jobs/job_details_bundle';
import initJobDetails from '~/jobs';
document.addEventListener('DOMContentLoaded', initJobDetails);
......@@ -69,6 +69,9 @@ export default {
onClickAction(action) {
this.$emit('actionClicked', action);
},
onClickSidebarButton() {
this.$emit('clickedSidebarButton');
},
},
};
</script>
......@@ -161,13 +164,14 @@ export default {
</i>
</button>
</template>
</section>
<button
v-if="hasSidebarButton"
id="toggleSidebar"
type="button"
class="btn btn-default d-block d-sm-none d-md-none
class="btn btn-default d-block d-sm-none
sidebar-toggle-btn js-sidebar-build-toggle js-sidebar-build-toggle-header"
aria-label="Toggle Sidebar"
@click="onClickSidebarButton"
>
<i
class="fa fa-angle-double-left"
......@@ -176,6 +180,5 @@ sidebar-toggle-btn js-sidebar-build-toggle js-sidebar-build-toggle-header"
>
</i>
</button>
</section>
</header>
</template>
......@@ -117,7 +117,6 @@
.controllers {
display: flex;
font-size: 15px;
justify-content: center;
align-items: center;
......@@ -179,6 +178,7 @@
.build-loader-animation {
@include build-loader-animation;
float: left;
}
}
......
- breadcrumb_title _('Artifacts')
- page_title @path.presence, 'Artifacts', "#{@build.name} (##{@build.id})", 'Jobs'
= render "projects/jobs/header", show_controls: false
= render "projects/jobs/header"
- add_to_breadcrumbs(s_('CICD|Jobs'), project_jobs_path(@project))
- add_to_breadcrumbs("##{@build.id}", project_jobs_path(@project))
......
- page_title @path, 'Artifacts', "#{@build.name} (##{@build.id})", 'Jobs'
= render "projects/jobs/header", show_controls: false
= render "projects/jobs/header"
.tree-holder
.nav-block
......
- show_controls = local_assigns.fetch(:show_controls, true)
- pipeline = @build.pipeline
.content-block.build-header.top-area.page-content-header
......@@ -20,12 +19,3 @@
= render "projects/jobs/user" if @build.user
= time_ago_with_tooltip(@build.created_at)
- if show_controls
.nav-controls
- if can?(current_user, :create_issue, @project) && @build.failed?
= link_to "New issue", new_project_issue_path(@project, issue: build_failed_issue_options), class: 'btn btn-success btn-inverted'
- if can?(current_user, :update_build, @build) && @build.retryable?
= link_to "Retry job", retry_project_job_path(@project, @build), class: 'btn btn-inverted-secondary', method: :post
%button.btn.btn-default.float-right.d-block.d-sm-none.d-md-none.build-gutter-toggle.js-sidebar-build-toggle{ role: "button", type: "button" }
= icon('angle-double-left')
- @no_container = true
- add_to_breadcrumbs "Jobs", project_jobs_path(@project)
- add_to_breadcrumbs _("Jobs"), project_jobs_path(@project)
- breadcrumb_title "##{@build.id}"
- page_title "#{@build.name} (##{@build.id})", "Jobs"
- page_title "#{@build.name} (##{@build.id})", _("Jobs")
- content_for :page_specific_javascripts do
= stylesheet_link_tag 'page_bundles/xterm'
%div{ class: container_class }
.build-page.js-build-page
#js-build-header-vue
- if @build.running? || @build.has_trace?
.build-trace-container.prepend-top-default
.top-bar.js-top-bar
.js-truncated-info.truncated-info.d-none.d-sm-block.float-left.hidden<
Showing last
%span.js-truncated-info-size.truncated-info-size><
of log -
%a.js-raw-link.raw-link{ href: raw_project_job_path(@project, @build) }>< Complete Raw
.controllers.float-right
- if @build.has_trace?
= link_to raw_project_job_path(@project, @build),
title: 'Show complete raw',
data: { placement: 'top', container: 'body' },
class: 'js-raw-link-controller has-tooltip controllers-buttons' do
= icon('file-text-o')
- if @build.erasable? && can?(current_user, :erase_build, @build)
= link_to erase_project_job_path(@project, @build),
method: :post,
data: { confirm: 'Are you sure you want to erase this build?', placement: 'top', container: 'body' },
title: 'Erase job log',
class: 'has-tooltip js-erase-link controllers-buttons' do
= icon('trash')
.has-tooltip.controllers-buttons{ title: 'Scroll to top', data: { placement: 'top', container: 'body'} }
%button.js-scroll-up.btn-scroll.btn-transparent.btn-blank{ type: 'button', disabled: true }
= custom_icon('scroll_up')
.has-tooltip.controllers-buttons{ title: 'Scroll to bottom', data: { placement: 'top', container: 'body'} }
%button.js-scroll-down.btn-scroll.btn-transparent.btn-blank{ type: 'button', disabled: true }
= custom_icon('scroll_down')
= render 'shared/builds/build_output'
#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 }
#js-job-details-vue{ data: { endpoint: project_job_path(@project, @build, format: :json),
#js-job-vue-app{ data: { endpoint: project_job_path(@project, @build, format: :json),
runner_help_url: help_page_path('ci/runners/README.html', anchor: 'setting-maximum-job-timeout-for-a-runner'),
runner_settings_url: project_runners_path(@build.project, anchor: 'js-runners-settings') } }
runner_settings_url: project_runners_path(@build.project, anchor: 'js-runners-settings'),
build_options: javascript_build_options } }
---
title: Transform job page into a single Vue+Vuex application
merge_request:
author:
type: other
......@@ -627,6 +627,9 @@ msgstr ""
msgid "Are you sure you want to delete this pipeline schedule?"
msgstr ""
msgid "Are you sure you want to erase this build?"
msgstr ""
msgid "Are you sure you want to lose unsaved changes?"
msgstr ""
......
......@@ -67,7 +67,7 @@ describe 'Project Jobs Permissions' do
it_behaves_like 'recent job page details responds with status', 200 do
it 'renders job details', :js do
expect(page).to have_content "Job ##{job.id}"
expect(page).to have_css '#build-trace'
expect(page).to have_css '.js-build-trace'
end
end
......
......@@ -20,7 +20,7 @@ describe 'User browses a job', :js do
wait_for_requests
expect(page).to have_content("Job ##{build.id}")
expect(page).to have_css('#build-trace')
expect(page).to have_css('.js-build-trace')
# scroll to the top of the page first
execute_script "window.scrollTo(0,0)"
......
......@@ -294,7 +294,7 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do
end
end
describe 'Raw trace' do
describe 'Raw trace', :js do
before do
job.run!
......@@ -302,7 +302,8 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do
end
it do
expect(page).to have_css('.js-raw-link')
wait_for_all_requests
expect(page).to have_css('.js-raw-link-controller')
end
end
......@@ -636,7 +637,7 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do
end
end
context 'Canceled job' do
context 'Canceled job', :js do
context 'with log' do
let(:job) { create(:ci_build, :canceled, :trace_artifact, pipeline: pipeline) }
......@@ -645,7 +646,8 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do
end
it 'renders job log' do
expect(page).to have_selector('.js-build-output')
wait_for_all_requests
expect(page).to have_selector('.js-build-trace')
end
end
......@@ -658,7 +660,7 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do
it 'renders empty state' do
expect(page).to have_content(job.detailed_status(user).illustration[:title])
expect(page).not_to have_selector('.js-build-output')
expect(page).not_to have_selector('.js-build-trace')
expect(page).to have_content('This job has been canceled')
end
end
......@@ -673,7 +675,7 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do
it 'renders empty state' do
expect(page).to have_content(job.detailed_status(user).illustration[:title])
expect(page).not_to have_selector('.js-build-output')
expect(page).not_to have_selector('.js-build-trace')
expect(page).to have_content('This job has been skipped')
end
end
......@@ -722,8 +724,8 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do
visit project_job_path(project, job)
wait_for_requests
expect(page).to have_css('.js-build-sidebar.right-sidebar-collapsed', visible: false)
expect(page).not_to have_css('.js-build-sidebar.right-sidebar-expanded', visible: false)
expect(page).to have_css('.js-job-sidebar.right-sidebar-collapsed', visible: false)
expect(page).not_to have_css('.js-job-sidebar.right-sidebar-expanded', visible: false)
end
end
......@@ -734,8 +736,8 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do
visit project_job_path(project, job)
wait_for_requests
expect(page).to have_css('.js-build-sidebar.right-sidebar-expanded')
expect(page).not_to have_css('.js-build-sidebar.right-sidebar-collpased')
expect(page).to have_css('.js-job-sidebar.right-sidebar-expanded')
expect(page).not_to have_css('.js-job-sidebar.right-sidebar-collpased')
end
end
end
......
import $ from 'jquery';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import { numberToHumanSize } from '~/lib/utils/number_utils';
import '~/lib/utils/datetime_utility';
import Job from '~/job';
import '~/breakpoints';
import waitForPromises from 'spec/helpers/wait_for_promises';
describe('Job', () => {
const JOB_URL = `${gl.TEST_HOST}/frontend-fixtures/builds-project/-/jobs/1`;
let mock;
let response;
let job;
preloadFixtures('builds/build-with-artifacts.html.raw');
beforeEach(() => {
loadFixtures('builds/build-with-artifacts.html.raw');
spyOnDependency(Job, 'visitUrl');
response = {};
mock = new MockAdapter(axios);
mock.onGet(new RegExp(`${JOB_URL}/trace.json?(.*)`)).reply(() => [200, response]);
});
afterEach(() => {
mock.restore();
clearTimeout(job.timeout);
});
describe('class constructor', () => {
beforeEach(() => {
jasmine.clock().install();
});
afterEach(() => {
jasmine.clock().uninstall();
});
describe('setup', () => {
beforeEach(function (done) {
job = new Job();
waitForPromises()
.then(done)
.catch(done.fail);
});
it('copies build options', function () {
expect(job.pagePath).toBe(JOB_URL);
expect(job.buildStatus).toBe('success');
expect(job.buildStage).toBe('test');
expect(job.state).toBe('');
});
});
describe('running build', () => {
it('updates the build trace on an interval', function (done) {
response = {
html: '<span>Update<span>',
status: 'running',
state: 'newstate',
append: true,
complete: false,
};
job = new Job();
waitForPromises()
.then(() => {
expect($('#build-trace .js-build-output').text()).toMatch(/Update/);
expect(job.state).toBe('newstate');
response = {
html: '<span>More</span>',
status: 'running',
state: 'finalstate',
append: true,
complete: true,
};
})
.then(() => jasmine.clock().tick(4001))
.then(waitForPromises)
.then(() => {
expect($('#build-trace .js-build-output').text()).toMatch(/UpdateMore/);
expect(job.state).toBe('finalstate');
})
.then(done)
.catch(done.fail);
});
it('replaces the entire build trace', (done) => {
response = {
html: '<span>Update<span>',
status: 'running',
append: false,
complete: false,
};
job = new Job();
waitForPromises()
.then(() => {
expect($('#build-trace .js-build-output').text()).toMatch(/Update/);
response = {
html: '<span>Different</span>',
status: 'running',
append: false,
};
})
.then(() => jasmine.clock().tick(4001))
.then(waitForPromises)
.then(() => {
expect($('#build-trace .js-build-output').text()).not.toMatch(/Update/);
expect($('#build-trace .js-build-output').text()).toMatch(/Different/);
})
.then(done)
.catch(done.fail);
});
});
describe('truncated information', () => {
describe('when size is less than total', () => {
it('shows information about truncated log', (done) => {
response = {
html: '<span>Update</span>',
status: 'success',
append: false,
size: 50,
total: 100,
};
job = new Job();
waitForPromises()
.then(() => {
expect(document.querySelector('.js-truncated-info').classList).not.toContain('hidden');
})
.then(done)
.catch(done.fail);
});
it('shows the size in KiB', (done) => {
const size = 50;
response = {
html: '<span>Update</span>',
status: 'success',
append: false,
size,
total: 100,
};
job = new Job();
waitForPromises()
.then(() => {
expect(
document.querySelector('.js-truncated-info-size').textContent.trim(),
).toEqual(`${numberToHumanSize(size)}`);
})
.then(done)
.catch(done.fail);
});
it('shows incremented size', (done) => {
response = {
html: '<span>Update</span>',
status: 'success',
append: false,
size: 50,
total: 100,
complete: false,
};
job = new Job();
waitForPromises()
.then(() => {
expect(
document.querySelector('.js-truncated-info-size').textContent.trim(),
).toEqual(`${numberToHumanSize(50)}`);
response = {
html: '<span>Update</span>',
status: 'success',
append: true,
size: 10,
total: 100,
complete: true,
};
})
.then(() => jasmine.clock().tick(4001))
.then(waitForPromises)
.then(() => {
expect(
document.querySelector('.js-truncated-info-size').textContent.trim(),
).toEqual(`${numberToHumanSize(60)}`);
})
.then(done)
.catch(done.fail);
});
it('renders the raw link', () => {
response = {
html: '<span>Update</span>',
status: 'success',
append: false,
size: 50,
total: 100,
};
job = new Job();
expect(
document.querySelector('.js-raw-link').textContent.trim(),
).toContain('Complete Raw');
});
});
describe('when size is equal than total', () => {
it('does not show the trunctated information', (done) => {
response = {
html: '<span>Update</span>',
status: 'success',
append: false,
size: 100,
total: 100,
};
job = new Job();
waitForPromises()
.then(() => {
expect(document.querySelector('.js-truncated-info').classList).toContain('hidden');
})
.then(done)
.catch(done.fail);
});
});
});
describe('output trace', () => {
beforeEach((done) => {
response = {
html: '<span>Update</span>',
status: 'success',
append: false,
size: 50,
total: 100,
};
job = new Job();
waitForPromises()
.then(done)
.catch(done.fail);
});
it('should render trace controls', () => {
const controllers = document.querySelector('.controllers');
expect(controllers.querySelector('.js-raw-link-controller')).not.toBeNull();
expect(controllers.querySelector('.js-scroll-up')).not.toBeNull();
expect(controllers.querySelector('.js-scroll-down')).not.toBeNull();
});
it('should render received output', () => {
expect(
document.querySelector('.js-build-output').innerHTML,
).toEqual('<span>Update</span>');
});
});
});
describe('getBuildTrace', () => {
it('should request build trace with state parameter', (done) => {
spyOn(axios, 'get').and.callThrough();
job = new Job();
setTimeout(() => {
expect(axios.get).toHaveBeenCalledWith(
`${JOB_URL}/trace.json`, { params: { state: '' } },
);
done();
}, 0);
});
});
});
// import $ from 'jquery';
// import MockAdapter from 'axios-mock-adapter';
// import axios from '~/lib/utils/axios_utils';
// import { numberToHumanSize } from '~/lib/utils/number_utils';
// import '~/lib/utils/datetime_utility';
// import Job from '~/job';
// import '~/breakpoints';
// import waitForPromises from 'spec/helpers/wait_for_promises';
// describe('Job', () => {
// const JOB_URL = `${gl.TEST_HOST}/frontend-fixtures/builds-project/-/jobs/1`;
// let mock;
// let response;
// let job;
// preloadFixtures('builds/build-with-artifacts.html.raw');
// beforeEach(() => {
// loadFixtures('builds/build-with-artifacts.html.raw');
// spyOnDependency(Job, 'visitUrl');
// response = {};
// mock = new MockAdapter(axios);
// mock.onGet(new RegExp(`${JOB_URL}/trace.json?(.*)`)).reply(() => [200, response]);
// });
// afterEach(() => {
// mock.restore();
// clearTimeout(job.timeout);
// });
// describe('class constructor', () => {
// beforeEach(() => {
// jasmine.clock().install();
// });
// afterEach(() => {
// jasmine.clock().uninstall();
// });
// describe('running build', () => {
// it('updates the build trace on an interval', function (done) {
// response = {
// html: '<span>Update<span>',
// status: 'running',
// state: 'newstate',
// append: true,
// complete: false,
// };
// job = new Job();
// waitForPromises()
// .then(() => {
// expect($('#build-trace .js-build-output').text()).toMatch(/Update/);
// expect(job.state).toBe('newstate');
// response = {
// html: '<span>More</span>',
// status: 'running',
// state: 'finalstate',
// append: true,
// complete: true,
// };
// })
// .then(() => jasmine.clock().tick(4001))
// .then(waitForPromises)
// .then(() => {
// expect($('#build-trace .js-build-output').text()).toMatch(/UpdateMore/);
// expect(job.state).toBe('finalstate');
// })
// .then(done)
// .catch(done.fail);
// });
// it('replaces the entire build trace', (done) => {
// response = {
// html: '<span>Update<span>',
// status: 'running',
// append: false,
// complete: false,
// };
// job = new Job();
// waitForPromises()
// .then(() => {
// expect($('#build-trace .js-build-output').text()).toMatch(/Update/);
// response = {
// html: '<span>Different</span>',
// status: 'running',
// append: false,
// };
// })
// .then(() => jasmine.clock().tick(4001))
// .then(waitForPromises)
// .then(() => {
// expect($('#build-trace .js-build-output').text()).not.toMatch(/Update/);
// expect($('#build-trace .js-build-output').text()).toMatch(/Different/);
// })
// .then(done)
// .catch(done.fail);
// });
// });
// describe('truncated information', () => {
// describe('when size is less than total', () => {
// it('shows information about truncated log', (done) => {
// response = {
// html: '<span>Update</span>',
// status: 'success',
// append: false,
// size: 50,
// total: 100,
// };
// job = new Job();
// waitForPromises()
// .then(() => {
// expect(document.querySelector('.js-truncated-info').classList).not.toContain('hidden');
// })
// .then(done)
// .catch(done.fail);
// });
// it('shows the size in KiB', (done) => {
// const size = 50;
// response = {
// html: '<span>Update</span>',
// status: 'success',
// append: false,
// size,
// total: 100,
// };
// job = new Job();
// waitForPromises()
// .then(() => {
// expect(
// document.querySelector('.js-truncated-info-size').textContent.trim(),
// ).toEqual(`${numberToHumanSize(size)}`);
// })
// .then(done)
// .catch(done.fail);
// });
// it('shows incremented size', (done) => {
// response = {
// html: '<span>Update</span>',
// status: 'success',
// append: false,
// size: 50,
// total: 100,
// complete: false,
// };
// job = new Job();
// waitForPromises()
// .then(() => {
// expect(
// document.querySelector('.js-truncated-info-size').textContent.trim(),
// ).toEqual(`${numberToHumanSize(50)}`);
// response = {
// html: '<span>Update</span>',
// status: 'success',
// append: true,
// size: 10,
// total: 100,
// complete: true,
// };
// })
// .then(() => jasmine.clock().tick(4001))
// .then(waitForPromises)
// .then(() => {
// expect(
// document.querySelector('.js-truncated-info-size').textContent.trim(),
// ).toEqual(`${numberToHumanSize(60)}`);
// })
// .then(done)
// .catch(done.fail);
// });
// it('renders the raw link', () => {
// response = {
// html: '<span>Update</span>',
// status: 'success',
// append: false,
// size: 50,
// total: 100,
// };
// job = new Job();
// expect(
// document.querySelector('.js-raw-link').textContent.trim(),
// ).toContain('Complete Raw');
// });
// });
// describe('when size is equal than total', () => {
// it('does not show the trunctated information', (done) => {
// response = {
// html: '<span>Update</span>',
// status: 'success',
// append: false,
// size: 100,
// total: 100,
// };
// job = new Job();
// waitForPromises()
// .then(() => {
// expect(document.querySelector('.js-truncated-info').classList).toContain('hidden');
// })
// .then(done)
// .catch(done.fail);
// });
// });
// });
// describe('output trace', () => {
// beforeEach((done) => {
// response = {
// html: '<span>Update</span>',
// status: 'success',
// append: false,
// size: 50,
// total: 100,
// };
// job = new Job();
// waitForPromises()
// .then(done)
// .catch(done.fail);
// });
// it('should render trace controls', () => {
// const controllers = document.querySelector('.controllers');
// expect(controllers.querySelector('.js-raw-link-controller')).not.toBeNull();
// expect(controllers.querySelector('.js-scroll-up')).not.toBeNull();
// expect(controllers.querySelector('.js-scroll-down')).not.toBeNull();
// });
// it('should render received output', () => {
// expect(
// document.querySelector('.js-build-output').innerHTML,
// ).toEqual('<span>Update</span>');
// });
// });
// });
// });
import Vue from 'vue';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import jobApp from '~/jobs/components/job_app.vue';
import createStore from '~/jobs/store';
import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import { resetStore } from '../store/helpers';
import job from '../mock_data';
describe('Job App ', () => {
const Component = Vue.extend(jobApp);
let store;
let vm;
const threeWeeksAgo = new Date();
threeWeeksAgo.setDate(threeWeeksAgo.getDate() - 21);
const twoDaysAgo = new Date();
twoDaysAgo.setDate(twoDaysAgo.getDate() - 2);
const job = {
status: {
group: 'failed',
icon: 'status_failed',
label: 'failed',
text: 'failed',
details_path: 'path',
},
id: 123,
created_at: threeWeeksAgo.toISOString(),
user: {
web_url: 'path',
name: 'Foo',
username: 'foobar',
email: 'foo@bar.com',
avatar_url: 'link',
},
started: twoDaysAgo.toISOString(),
new_issue_path: 'path',
runners: {
available: false,
},
tags: ['docker'],
has_trace: true,
};
let mock;
const props = {
endpoint: `${gl.TEST_HOST}jobs/123.json`,
runnerHelpUrl: 'help/runner',
runnerSettingsUrl: 'settings/ci-cd/runners',
terminalPath: 'jobs/123/terminal',
pagePath: `${gl.TEST_HOST}jobs/123`,
logState:
'eyJvZmZzZXQiOjE3NDUxLCJuX29wZW5fdGFncyI6MCwiZmdfY29sb3IiOm51bGwsImJnX2NvbG9yIjpudWxsLCJzdHlsZV9tYXNrIjowfQ%3D%3D',
};
beforeEach(() => {
mock = new MockAdapter(axios);
store = createStore();
});
afterEach(() => {
resetStore(store);
vm.$destroy();
mock.restore();
});
describe('Header section', () => {
describe('job callout message', () => {
it('should not render the reason when reason is absent', () => {
store.dispatch('receiveJobSuccess', job);
describe('while loading', () => {
beforeEach(() => {
mock.onGet(props.endpoint).reply(200, job, {});
mock.onGet(`${props.pagePath}/trace.json`).reply(200, {});
vm = mountComponentWithStore(Component, { props, store });
});
vm = mountComponentWithStore(Component, {
props,
store,
it('renders loading icon', done => {
expect(vm.$el.querySelector('.js-job-loading')).not.toBeNull();
expect(vm.$el.querySelector('.js-job-sidebar')).toBeNull();
expect(vm.$el.querySelector('.js-job-content')).toBeNull();
setTimeout(() => {
done();
}, 0);
});
});
describe('with successfull request', () => {
beforeEach(() => {
mock.onGet(`${props.pagePath}/trace.json`).replyOnce(200, {});
});
describe('Header section', () => {
describe('job callout message', () => {
it('should not render the reason when reason is absent', done => {
mock.onGet(props.endpoint).replyOnce(200, job);
vm = mountComponentWithStore(Component, { props, store });
setTimeout(() => {
expect(vm.shouldRenderCalloutMessage).toBe(false);
done();
}, 0);
});
it('should render the reason when reason is present', () => {
store.dispatch(
'receiveJobSuccess',
it('should render the reason when reason is present', done => {
mock.onGet(props.endpoint).replyOnce(
200,
Object.assign({}, job, {
callout_message: 'There is an unknown failure, please try again',
}),
);
vm = mountComponentWithStore(Component, {
props,
store,
});
vm = mountComponentWithStore(Component, { props, store });
setTimeout(() => {
expect(vm.shouldRenderCalloutMessage).toBe(true);
done();
}, 0);
});
});
describe('triggered job', () => {
beforeEach(() => {
store.dispatch('receiveJobSuccess', job);
vm = mountComponentWithStore(Component, {
props,
store,
});
mock.onGet(props.endpoint).replyOnce(200, Object.assign({}, job, { started: '2017-05-24T10:59:52.000+01:00' }));
vm = mountComponentWithStore(Component, { props, store });
});
it('should render provided job information', () => {
it('should render provided job information', done => {
setTimeout(() => {
expect(
vm.$el
.querySelector('.header-main-content')
.textContent.replace(/\s+/g, ' ')
.trim(),
).toEqual('failed Job #123 triggered 2 days ago by Foo');
).toEqual('passed Job #4757 triggered 1 year ago by Root');
done();
}, 0);
});
it('should render new issue link', () => {
it('should render new issue link', done => {
setTimeout(() => {
expect(vm.$el.querySelector('.js-new-issue').getAttribute('href')).toEqual(
job.new_issue_path,
);
done();
}, 0);
});
});
describe('created job', () => {
it('should render created key', () => {
store.dispatch('receiveJobSuccess', Object.assign({}, job, { started: false }));
vm = mountComponentWithStore(Component, {
props,
store,
});
it('should render created key', done => {
mock.onGet(props.endpoint).replyOnce(200, job);
vm = mountComponentWithStore(Component, { props, store });
setTimeout(() => {
expect(
vm.$el
.querySelector('.header-main-content')
.textContent.replace(/\s+/g, ' ')
.trim(),
).toEqual('failed Job #123 created 3 weeks ago by Foo');
).toEqual('passed Job #4757 created 3 weeks ago by Root');
done();
}, 0);
});
});
});
describe('stuck block', () => {
it('renders stuck block when there are no runners', () => {
store.dispatch(
'receiveJobSuccess',
it('renders stuck block when there are no runners', done => {
mock.onGet(props.endpoint).replyOnce(
200,
Object.assign({}, job, {
status: {
group: 'pending',
......@@ -139,20 +144,23 @@ describe('Job App ', () => {
text: 'pending',
details_path: 'path',
},
runners: {
available: false,
},
}),
);
vm = mountComponentWithStore(Component, { props, store });
vm = mountComponentWithStore(Component, {
props,
store,
});
setTimeout(() => {
expect(vm.$el.querySelector('.js-job-stuck')).not.toBeNull();
done();
}, 0);
});
it('renders tags in stuck block when there are no runners', () => {
store.dispatch(
'receiveJobSuccess',
it('renders tags in stuck block when there are no runners', done => {
mock.onGet(props.endpoint).replyOnce(
200,
Object.assign({}, job, {
status: {
group: 'pending',
......@@ -161,6 +169,9 @@ describe('Job App ', () => {
text: 'pending',
details_path: 'path',
},
runners: {
available: false,
},
}),
);
......@@ -169,25 +180,37 @@ describe('Job App ', () => {
store,
});
setTimeout(() => {
expect(vm.$el.querySelector('.js-job-stuck').textContent).toContain(job.tags[0]);
done();
}, 0);
});
it(' does not renders stuck block when there are no runners', () => {
store.dispatch('receiveJobSuccess', Object.assign({}, job, { runners: { available: true } }));
it('does not renders stuck block when there are no runners', done => {
mock.onGet(props.endpoint).replyOnce(
200,
Object.assign({}, job, {
runners: { available: true },
}),
);
vm = mountComponentWithStore(Component, {
props,
store,
});
setTimeout(() => {
expect(vm.$el.querySelector('.js-job-stuck')).toBeNull();
done();
}, 0);
});
});
describe('environments block', () => {
it('renders environment block when job has environment', () => {
store.dispatch(
'receiveJobSuccess',
it('renders environment block when job has environment', done => {
mock.onGet(props.endpoint).replyOnce(
200,
Object.assign({}, job, {
deployment_status: {
environment: {
......@@ -203,25 +226,32 @@ describe('Job App ', () => {
store,
});
setTimeout(() => {
expect(vm.$el.querySelector('.js-job-environment')).not.toBeNull();
done();
}, 0);
});
it('does not render environment block when job has environment', () => {
store.dispatch('receiveJobSuccess', job);
it('does not render environment block when job has environment', done => {
mock.onGet(props.endpoint).replyOnce(200, job);
vm = mountComponentWithStore(Component, {
props,
store,
});
setTimeout(() => {
expect(vm.$el.querySelector('.js-job-environment')).toBeNull();
done();
}, 0);
});
});
describe('erased block', () => {
it('renders erased block when `erased` is true', () => {
store.dispatch(
'receiveJobSuccess',
it('renders erased block when `erased` is true', done => {
mock.onGet(props.endpoint).replyOnce(
200,
Object.assign({}, job, {
erased_by: {
username: 'root',
......@@ -236,25 +266,33 @@ describe('Job App ', () => {
store,
});
setTimeout(() => {
expect(vm.$el.querySelector('.js-job-erased-block')).not.toBeNull();
done();
}, 0);
});
it('does not render erased block when `erased` is false', () => {
store.dispatch('receiveJobSuccess', Object.assign({}, job, { erased_at: null }));
it('does not render erased block when `erased` is false', done => {
mock.onGet(props.endpoint).replyOnce(200, Object.assign({}, job, { erased_at: null }));
vm = mountComponentWithStore(Component, {
props,
store,
});
setTimeout(() => {
expect(vm.$el.querySelector('.js-job-erased-block')).toBeNull();
done();
}, 0);
});
});
describe('empty states block', () => {
it('renders empty state when job does not have trace and is not running', () => {
store.dispatch(
'receiveJobSuccess',
it('renders empty state when job does not have trace and is not running', done => {
mock.onGet(props.endpoint).replyOnce(
200,
Object.assign({}, job, {
has_trace: false,
status: {
......@@ -283,12 +321,16 @@ describe('Job App ', () => {
store,
});
setTimeout(() => {
expect(vm.$el.querySelector('.js-job-empty-state')).not.toBeNull();
done();
}, 0);
});
it('does not render empty state when job does not have trace but it is running', () => {
store.dispatch(
'receiveJobSuccess',
it('does not render empty state when job does not have trace but it is running', done => {
mock.onGet(props.endpoint).replyOnce(
200,
Object.assign({}, job, {
has_trace: false,
status: {
......@@ -306,18 +348,175 @@ describe('Job App ', () => {
store,
});
setTimeout(() => {
expect(vm.$el.querySelector('.js-job-empty-state')).toBeNull();
done();
}, 0);
});
it('does not render empty state when job has trace but it is not running', () => {
store.dispatch('receiveJobSuccess', Object.assign({}, job, { has_trace: true }));
it('does not render empty state when job has trace but it is not running', done => {
mock.onGet(props.endpoint).replyOnce(200, Object.assign({}, job, { has_trace: true }));
vm = mountComponentWithStore(Component, {
props,
store,
});
setTimeout(() => {
expect(vm.$el.querySelector('.js-job-empty-state')).toBeNull();
done();
}, 0);
});
});
});
describe('trace output', () => {
beforeEach(() => {
mock.onGet(props.endpoint).reply(200, job, {});
});
describe('with append flag', () => {
it('appends the log content to the existing one', done => {
mock.onGet(`${props.pagePath}/trace.json`).reply(200, {
html: '<span>More<span>',
status: 'running',
state: 'newstate',
append: true,
complete: true,
});
vm = mountComponentWithStore(Component, {
props,
store,
});
vm.$store.state.trace = 'Update';
setTimeout(() => {
expect(vm.$el.querySelector('.js-build-trace').textContent.trim()).toContain('Update');
done();
}, 0);
});
});
describe('without append flag', () => {
it('replaces the trace', done => {
mock.onGet(`${props.pagePath}/trace.json`).reply(200, {
html: '<span>Different<span>',
status: 'running',
append: false,
complete: true,
});
vm = mountComponentWithStore(Component, {
props,
store,
});
vm.$store.state.trace = 'Update';
setTimeout(() => {
expect(vm.$el.querySelector('.js-build-trace').textContent.trim()).not.toContain('Update');
expect(vm.$el.querySelector('.js-build-trace').textContent.trim()).toContain(
'Different',
);
done();
}, 0);
});
});
describe('truncated information', () => {
describe('when size is less than total', () => {
it('shows information about truncated log', done => {
mock.onGet(`${props.pagePath}/trace.json`).reply(200, {
html: '<span>Update</span>',
status: 'success',
append: false,
size: 50,
total: 100,
complete: true,
});
vm = mountComponentWithStore(Component, {
props,
store,
});
setTimeout(() => {
expect(vm.$el.querySelector('.js-truncated-info').textContent.trim()).toContain(
'50 bytes',
);
done();
}, 0);
});
});
describe('when size is equal than total', () => {
it('does not show the truncated information', done => {
mock.onGet(`${props.pagePath}/trace.json`).reply(200, {
html: '<span>Update</span>',
status: 'success',
append: false,
size: 100,
total: 100,
complete: true,
});
vm = mountComponentWithStore(Component, {
props,
store,
});
setTimeout(() => {
expect(vm.$el.querySelector('.js-truncated-info').textContent.trim()).not.toContain(
'50 bytes',
);
done();
}, 0);
});
});
});
describe('trace controls', () => {
beforeEach(() => {
mock.onGet(`${props.pagePath}/trace.json`).reply(200, {
html: '<span>Update</span>',
status: 'success',
append: false,
size: 50,
total: 100,
complete: true,
});
vm = mountComponentWithStore(Component, {
props,
store,
});
});
it('should render scroll buttons', done => {
setTimeout(() => {
expect(vm.$el.querySelector('.js-scroll-top')).not.toBeNull();
expect(vm.$el.querySelector('.js-scroll-bottom')).not.toBeNull();
done();
}, 0);
});
it('should render link to raw ouput', done => {
setTimeout(() => {
expect(vm.$el.querySelector('.js-raw-link-controller')).not.toBeNull();
done();
}, 0);
});
it('should render link to erase job', done => {
setTimeout(() => {
expect(vm.$el.querySelector('.js-erase-link')).not.toBeNull();
done();
}, 0);
});
});
});
});
import Vue from 'vue';
import component from '~/jobs/components/job_log.vue';
import mountComponent from '../../helpers/vue_mount_component_helper';
import createStore from '~/jobs/store';
import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import { resetStore } from '../store/helpers';
describe('Job Log', () => {
const Component = Vue.extend(component);
let store;
let vm;
const trace = 'Running with gitlab-runner 11.1.0 (081978aa)<br> on docker-auto-scale-com d5ae8d25<br>Using Docker executor with image dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.4.4-golang-1.9-git-2.18-chrome-67.0-node-8.x-yarn-1.2-postgresql-9.6-graphicsmagick-1.3.29 ...<br>';
beforeEach(() => {
store = createStore();
});
afterEach(() => {
resetStore(store);
vm.$destroy();
});
it('renders provided trace', () => {
vm = mountComponent(Component, {
vm = mountComponentWithStore(Component, {
props: {
trace,
isComplete: true,
},
store,
});
expect(vm.$el.querySelector('code').textContent).toContain('Running with gitlab-runner 11.1.0 (081978aa)');
......@@ -23,9 +34,12 @@ describe('Job Log', () => {
describe('while receiving trace', () => {
it('renders animation', () => {
vm = mountComponent(Component, {
vm = mountComponentWithStore(Component, {
props: {
trace,
isComplete: true,
isComplete: false,
},
store,
});
expect(vm.$el.querySelector('.js-log-animation')).not.toBeNull();
......@@ -34,9 +48,12 @@ describe('Job Log', () => {
describe('when build trace has finishes', () => {
it('does not render animation', () => {
vm = mountComponent(Component, {
vm = mountComponentWithStore(Component, {
props: {
trace,
isComplete: false,
isComplete: true,
},
store,
});
expect(vm.$el.querySelector('.js-log-animation')).toBeNull();
......
......@@ -18,15 +18,6 @@ describe('Sidebar details block', () => {
vm.$destroy();
});
describe('when it is loading', () => {
it('should render a loading spinner', () => {
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', () => {
const copy = Object.assign({}, job);
......@@ -52,12 +43,12 @@ describe('Sidebar details block', () => {
describe('with terminal path', () => {
it('renders terminal link', () => {
store.dispatch('receiveJobSuccess', job);
store.dispatch(
'receiveJobSuccess',
Object.assign({}, job, { terminal_path: 'job/43123/terminal' }),
);
vm = mountComponentWithStore(SidebarComponent, {
store,
props: {
terminalPath: 'job/43123/terminal',
},
});
expect(vm.$el.querySelector('.js-terminal-link')).not.toBeNull();
......
......@@ -31,6 +31,7 @@ export default {
},
coverage: 20,
erased_at: threeWeeksAgo.toISOString(),
erased: false,
duration: 6.785563,
tags: ['tag'],
user: {
......@@ -131,6 +132,7 @@ export default {
path: '/root/ci-mock/merge_requests/2',
},
raw_path: '/root/ci-mock/builds/4757/raw',
has_trace: true,
};
export const stages = [
......
......@@ -2,9 +2,7 @@ import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import {
setJobEndpoint,
setTraceEndpoint,
setStagesEndpoint,
setJobsEndpoint,
setTraceOptions,
clearEtagPoll,
stopPolling,
requestJob,
......@@ -18,10 +16,6 @@ import {
stopPollingTrace,
receiveTraceSuccess,
receiveTraceError,
fetchFavicon,
requestStatusFavicon,
receiveStatusFaviconSuccess,
requestStatusFaviconError,
requestStages,
fetchStages,
receiveStagesSuccess,
......@@ -30,6 +24,9 @@ import {
fetchJobsForStage,
receiveJobsForStageSuccess,
receiveJobsForStageError,
hideSidebar,
showSidebar,
toggleSidebar,
} from '~/jobs/store/actions';
import state from '~/jobs/store/state';
import * as types from '~/jobs/store/mutation_types';
......@@ -56,45 +53,75 @@ describe('Job State actions', () => {
});
});
describe('setTraceEndpoint', () => {
it('should commit SET_TRACE_ENDPOINT mutation', done => {
describe('setTraceOptions', () => {
it('should commit SET_TRACE_OPTIONS mutation', done => {
testAction(
setTraceEndpoint,
'job/872324/trace.json',
setTraceOptions,
{ pagePath: 'job/872324/trace.json' },
mockedState,
[{ type: types.SET_TRACE_ENDPOINT, payload: 'job/872324/trace.json' }],
[{ type: types.SET_TRACE_OPTIONS, payload: { pagePath: 'job/872324/trace.json' } }],
[],
done,
);
});
});
describe('setStagesEndpoint', () => {
it('should commit SET_STAGES_ENDPOINT mutation', done => {
describe('hideSidebar', () => {
it('should commit HIDE_SIDEBAR mutation', done => {
testAction(
setStagesEndpoint,
'job/872324/stages.json',
hideSidebar,
null,
mockedState,
[{ type: types.SET_STAGES_ENDPOINT, payload: 'job/872324/stages.json' }],
[{ type: types.HIDE_SIDEBAR }],
[],
done,
);
});
});
describe('setJobsEndpoint', () => {
it('should commit SET_JOBS_ENDPOINT mutation', done => {
describe('showSidebar', () => {
it('should commit HIDE_SIDEBAR mutation', done => {
testAction(
setJobsEndpoint,
'job/872324/stages/build.json',
showSidebar,
null,
mockedState,
[{ type: types.SET_JOBS_ENDPOINT, payload: 'job/872324/stages/build.json' }],
[{ type: types.SHOW_SIDEBAR }],
[],
done,
);
});
});
describe('toggleSidebar', () => {
describe('when isSidebarOpen is true', () => {
it('should dispatch hideSidebar', done => {
testAction(
toggleSidebar,
null,
mockedState,
[],
[{ type: 'hideSidebar' }],
done,
);
});
});
describe('when isSidebarOpen is false', () => {
it('should dispatch showSidebar', done => {
mockedState.isSidebarOpen = false;
testAction(
toggleSidebar,
null,
mockedState,
[],
[{ type: 'showSidebar' }],
done,
);
});
});
});
describe('requestJob', () => {
it('should commit REQUEST_JOB mutation', done => {
testAction(requestJob, null, mockedState, [{ type: types.REQUEST_JOB }], [], done);
......@@ -183,14 +210,14 @@ describe('Job State actions', () => {
});
describe('scrollTop', () => {
it('should commit SCROLL_TO_TOP mutation', done => {
testAction(scrollTop, null, mockedState, [{ type: types.SCROLL_TO_TOP }], [], done);
it('should dispatch toggleScrollButtons action', done => {
testAction(scrollTop, null, mockedState, [], [{ type: 'toggleScrollButtons' }], done);
});
});
describe('scrollBottom', () => {
it('should commit SCROLL_TO_BOTTOM mutation', done => {
testAction(scrollBottom, null, mockedState, [{ type: types.SCROLL_TO_BOTTOM }], [], done);
it('should dispatch toggleScrollButtons action', done => {
testAction(scrollBottom, null, mockedState, [], [{ type: 'toggleScrollButtons' }], done);
});
});
......@@ -215,7 +242,7 @@ describe('Job State actions', () => {
});
describe('success', () => {
it('dispatches requestTrace, fetchFavicon, receiveTraceSuccess and stopPollingTrace when job is complete', done => {
it('dispatches requestTrace, receiveTraceSuccess and stopPollingTrace when job is complete', done => {
mock.onGet(`${TEST_HOST}/endpoint/trace.json`).replyOnce(200, {
html: 'I, [2018-08-17T22:57:45.707325 #1841] INFO -- :',
complete: true,
......@@ -228,10 +255,8 @@ describe('Job State actions', () => {
[],
[
{
type: 'requestTrace',
},
{
type: 'fetchFavicon',
type: 'toggleScrollisInBottom',
payload: true,
},
{
payload: {
......@@ -261,9 +286,6 @@ describe('Job State actions', () => {
mockedState,
[],
[
{
type: 'requestTrace',
},
{
type: 'receiveTraceError',
},
......@@ -313,104 +335,6 @@ describe('Job State actions', () => {
});
});
describe('fetchFavicon', () => {
let mock;
beforeEach(() => {
mockedState.pagePath = `${TEST_HOST}/endpoint`;
mock = new MockAdapter(axios);
});
afterEach(() => {
mock.restore();
});
describe('success', () => {
it('dispatches requestStatusFavicon and receiveStatusFaviconSuccess ', done => {
mock.onGet(`${TEST_HOST}/endpoint/status.json`).replyOnce(200);
testAction(
fetchFavicon,
null,
mockedState,
[],
[
{
type: 'requestStatusFavicon',
},
{
type: 'receiveStatusFaviconSuccess',
},
],
done,
);
});
});
describe('error', () => {
beforeEach(() => {
mock.onGet(`${TEST_HOST}/endpoint/status.json`).replyOnce(500);
});
it('dispatches requestStatusFavicon and requestStatusFaviconError ', done => {
testAction(
fetchFavicon,
null,
mockedState,
[],
[
{
type: 'requestStatusFavicon',
},
{
type: 'requestStatusFaviconError',
},
],
done,
);
});
});
});
describe('requestStatusFavicon', () => {
it('should commit REQUEST_STATUS_FAVICON mutation ', done => {
testAction(
requestStatusFavicon,
null,
mockedState,
[{ type: types.REQUEST_STATUS_FAVICON }],
[],
done,
);
});
});
describe('receiveStatusFaviconSuccess', () => {
it('should commit RECEIVE_STATUS_FAVICON_SUCCESS mutation ', done => {
testAction(
receiveStatusFaviconSuccess,
null,
mockedState,
[{ type: types.RECEIVE_STATUS_FAVICON_SUCCESS }],
[],
done,
);
});
});
describe('requestStatusFaviconError', () => {
it('should commit RECEIVE_STATUS_FAVICON_ERROR mutation ', done => {
testAction(
requestStatusFaviconError,
null,
mockedState,
[{ type: types.RECEIVE_STATUS_FAVICON_ERROR }],
[],
done,
);
});
});
describe('requestStages', () => {
it('should commit REQUEST_STAGES mutation ', done => {
testAction(requestStages, null, mockedState, [{ type: types.REQUEST_STAGES }], [], done);
......
import state from '~/jobs/store/state';
// eslint-disable-next-line import/prefer-default-export
export const resetStore = store => {
store.replaceState(state());
};
......@@ -20,27 +20,19 @@ describe('Jobs Store Mutations', () => {
});
});
describe('REQUEST_STATUS_FAVICON', () => {
it('should set fetchingStatusFavicon to true', () => {
mutations[types.REQUEST_STATUS_FAVICON](stateCopy);
describe('HIDE_SIDEBAR', () => {
it('should set isSidebarOpen to false', () => {
mutations[types.HIDE_SIDEBAR](stateCopy);
expect(stateCopy.fetchingStatusFavicon).toEqual(true);
expect(stateCopy.isSidebarOpen).toEqual(false);
});
});
describe('RECEIVE_STATUS_FAVICON_SUCCESS', () => {
it('should set fetchingStatusFavicon to false', () => {
mutations[types.RECEIVE_STATUS_FAVICON_SUCCESS](stateCopy);
describe('SHOW_SIDEBAR', () => {
it('should set isSidebarOpen to true', () => {
mutations[types.SHOW_SIDEBAR](stateCopy);
expect(stateCopy.fetchingStatusFavicon).toEqual(false);
});
});
describe('RECEIVE_STATUS_FAVICON_ERROR', () => {
it('should set fetchingStatusFavicon to false', () => {
mutations[types.RECEIVE_STATUS_FAVICON_ERROR](stateCopy);
expect(stateCopy.fetchingStatusFavicon).toEqual(false);
expect(stateCopy.isSidebarOpen).toEqual(true);
});
});
......@@ -101,9 +93,7 @@ describe('Jobs Store Mutations', () => {
it('resets trace state and sets error to true', () => {
mutations[types.RECEIVE_TRACE_ERROR](stateCopy);
expect(stateCopy.isLoadingTrace).toEqual(false);
expect(stateCopy.isTraceComplete).toEqual(true);
expect(stateCopy.hasTraceError).toEqual(true);
});
});
......@@ -156,39 +146,10 @@ describe('Jobs Store Mutations', () => {
mutations[types.RECEIVE_JOB_ERROR](stateCopy);
expect(stateCopy.isLoading).toEqual(false);
expect(stateCopy.hasError).toEqual(true);
expect(stateCopy.job).toEqual({});
});
});
describe('SCROLL_TO_TOP', () => {
beforeEach(() => {
mutations[types.SCROLL_TO_TOP](stateCopy);
});
it('sets isTraceScrolledToBottom to false', () => {
expect(stateCopy.isTraceScrolledToBottom).toEqual(false);
});
it('sets hasBeenScrolled to true', () => {
expect(stateCopy.hasBeenScrolled).toEqual(true);
});
});
describe('SCROLL_TO_BOTTOM', () => {
beforeEach(() => {
mutations[types.SCROLL_TO_BOTTOM](stateCopy);
});
it('sets isTraceScrolledToBottom to true', () => {
expect(stateCopy.isTraceScrolledToBottom).toEqual(true);
});
it('sets hasBeenScrolled to true', () => {
expect(stateCopy.hasBeenScrolled).toEqual(true);
});
});
describe('REQUEST_STAGES', () => {
it('sets isLoadingStages to true', () => {
mutations[types.REQUEST_STAGES](stateCopy);
......
......@@ -94,7 +94,7 @@ describe('Header CI Component', () => {
});
it('should render sidebar toggle button', () => {
expect(vm.$el.querySelector('.js-sidebar-build-toggle')).toBeDefined();
expect(vm.$el.querySelector('.js-sidebar-build-toggle')).not.toBeNull();
});
});
......
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