diff options
author | Filipa Lacerda <filipa@gitlab.com> | 2018-10-17 10:34:19 +0000 |
---|---|---|
committer | Phil Hughes <me@iamphill.com> | 2018-10-17 10:34:19 +0000 |
commit | 5ed91cf81bcc459ad65357c128b955e10ddce284 (patch) | |
tree | 77c4b367c9c2d1a34a6eb1dafeb1040cb97904a3 | |
parent | 712f41e15cb61b8804f41afddfbe5f57106248a1 (diff) | |
download | gitlab-ce-5ed91cf81bcc459ad65357c128b955e10ddce284.tar.gz |
Resolve "Integrate new vue+vuex code base with new API and remove old haml code"
34 files changed, 1448 insertions, 1400 deletions
diff --git a/app/assets/javascripts/job.js b/app/assets/javascripts/job.js deleted file mode 100644 index 80ba43dca6f..00000000000 --- a/app/assets/javascripts/job.js +++ /dev/null @@ -1,188 +0,0 @@ -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(); - } -} diff --git a/app/assets/javascripts/jobs/components/job_app.vue b/app/assets/javascripts/jobs/components/job_app.vue index 4e8d3ad24cc..fa35b87ef2b 100644 --- a/app/assets/javascripts/jobs/components/job_app.vue +++ b/app/assets/javascripts/jobs/components/job_app.vue @@ -1,21 +1,32 @@ <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,71 +165,107 @@ <gl-loading-icon v-if="isLoading" :size="2" - class="prepend-top-20" + class="js-job-loading prepend-top-20" /> - <template v-else> - <!-- Header Section --> - <header> - <div class="js-build-header build-header top-area"> - <ci-header - :status="job.status" - :item-id="job.id" - :time="headerTime" - :user="job.user" - :actions="headerActions" - :has-sidebar-button="true" - :should-render-triggered-label="shouldRenderTriggeredLabel" - :item-name="__('Job')" + <template v-else-if="shouldRenderContent"> + <div class="js-job-content build-page"> + <!-- Header Section --> + <header> + <div class="js-build-header build-header top-area"> + <ci-header + :status="job.status" + :item-id="job.id" + :time="headerTime" + :user="job.user" + :actions="headerActions" + :has-sidebar-button="true" + :should-render-triggered-label="shouldRenderTriggeredLabel" + :item-name="__('Job')" + @clickedSidebarButton="toggleSidebar" + /> + </div> + + <callout + v-if="shouldRenderCalloutMessage" + :message="job.callout_message" + /> + </header> + <!-- EO Header Section --> + + <!-- Body Section --> + <stuck-block + v-if="isJobStuck" + class="js-job-stuck" + :has-no-runners-for-project="job.runners.available" + :tags="job.tags" + :runners-path="runnerSettingsUrl" + /> + + <environments-block + v-if="hasEnvironment" + class="js-job-environment" + :deployment-status="job.deployment_status" + :icon-status="job.status" + /> + + <erased-block + v-if="job.erased_at" + class="js-job-erased-block" + :user="job.erased_by" + :erased-at="job.erased_at" + /> + + <!--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 --> - <callout - v-if="shouldRenderCalloutMessage" - :message="job.callout_message" + <!--empty state --> + <empty-state + v-if="!hasTrace" + class="js-job-empty-state" + :illustration-path="emptyStateIllustration.image" + :illustration-size-class="emptyStateIllustration.size" + :title="emptyStateIllustration.title" + :content="emptyStateIllustration.content" + :action="emptyStateAction" /> - </header> - <!-- EO Header Section --> - - <!-- Body Section --> - <stuck-block - v-if="isJobStuck" - class="js-job-stuck" - :has-no-runners-for-project="job.runners.available" - :tags="job.tags" - :runners-path="runnerSettingsUrl" - /> - - <environments-block - v-if="hasEnvironment" - class="js-job-environment" - :deployment-status="job.deployment_status" - :icon-status="job.status" - /> - - <erased-block - v-if="job.erased_at" - class="js-job-erased-block" - :user="job.erased_by" - :erased-at="job.erased_at" - /> - - <!--job log --> - <!-- EO job log --> - - <!--empty state --> - <empty-state - v-if="!hasTrace" - class="js-job-empty-state" - :illustration-path="emptyStateIllustration.image" - :illustration-size-class="emptyStateIllustration.size" - :title="emptyStateIllustration.title" - :content="emptyStateIllustration.content" - :action="job.status.action" - /> <!-- 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> diff --git a/app/assets/javascripts/jobs/components/job_log.vue b/app/assets/javascripts/jobs/components/job_log.vue index 9d78d89239a..accda5d1bd8 100644 --- a/app/assets/javascripts/jobs/components/job_log.vue +++ b/app/assets/javascripts/jobs/components/job_log.vue @@ -1,20 +1,48 @@ <script> -export default { - name: 'JobLog', - props: { - trace: { - type: String, - required: true, + import { mapState, mapActions } from 'vuex'; + + export default { + name: 'JobLog', + props: { + trace: { + type: String, + required: true, + }, + isComplete: { + type: Boolean, + required: true, + }, + }, + computed: { + ...mapState(['isScrolledToBottomBeforeReceivingTrace']), + }, + updated() { + this.$nextTick(() => this.handleScrollDown()); + }, + mounted() { + this.$nextTick(() => this.handleScrollDown()); }, - isComplete: { - type: Boolean, - required: true, + 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> diff --git a/app/assets/javascripts/jobs/components/job_log_controllers.vue b/app/assets/javascripts/jobs/components/job_log_controllers.vue index cc885ea8e1b..94ab1b16c84 100644 --- a/app/assets/javascripts/jobs/components/job_log_controllers.vue +++ b/app/assets/javascripts/jobs/components/job_log_controllers.vue @@ -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 --> diff --git a/app/assets/javascripts/jobs/components/sidebar.vue b/app/assets/javascripts/jobs/components/sidebar.vue index 8f3c6aced23..906769ee6a2 100644 --- a/app/assets/javascripts/jobs/components/sidebar.vue +++ b/app/assets/javascripts/jobs/components/sidebar.vue @@ -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}%`; }, @@ -64,10 +59,10 @@ export default { return ''; } - let t = this.job.metadata.timeout_human_readable; - if (this.job.metadata.timeout_source !== '') { - t += ` (from ${this.job.metadata.timeout_source})`; - } + let t = this.job.metadata.timeout_human_readable; + if (this.job.metadata.timeout_source !== '') { + t += ` (from ${this.job.metadata.timeout_source})`; + } return t; }, @@ -100,196 +95,190 @@ 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 }} - </strong> - <a - v-if="job.retry_path" - :class="retryButtonClass" - :href="job.retry_path" - data-method="post" - rel="nofollow" - > - {{ __('Retry') }} - </a> - <a - v-if="terminalPath" - :href="terminalPath" - class="js-terminal-link pull-right btn btn-primary - btn-inverted visible-md-block visible-lg-block" - target="_blank" - > - {{ __('Debug') }} - <icon name="external-link" /> - </a> - <button - :aria-label="__('Toggle Sidebar')" - type="button" - class="btn btn-blank gutter-toggle - float-right d-block d-md-none js-sidebar-build-toggle" - > - <i - aria-hidden="true" - data-hidden="true" - class="fa fa-angle-double-right" - ></i> - </button> - </div> - <div - v-if="job.retry_path || job.new_issue_path" - class="block retry-link" + <div class="block"> + <strong class="inline prepend-top-8"> + {{ job.name }} + </strong> + <a + v-if="job.retry_path" + :class="retryButtonClass" + :href="job.retry_path" + data-method="post" + rel="nofollow" > - <a - v-if="job.new_issue_path" - :href="job.new_issue_path" - class="js-new-issue btn btn-success btn-inverted" - > - {{ __('New issue') }} + {{ __('Retry') }} + </a> + <a + 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" + > + {{ __('Debug') }} + <icon name="external-link" /> + </a> + <button + :aria-label="__('Toggle Sidebar')" + 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" + data-hidden="true" + class="fa fa-angle-double-right" + ></i> + </button> + </div> + <div + v-if="job.retry_path || job.new_issue_path" + class="block retry-link" + > + <a + v-if="job.new_issue_path" + :href="job.new_issue_path" + class="js-new-issue btn btn-success btn-inverted" + > + {{ __('New issue') }} + </a> + <a + v-if="job.retry_path" + :href="job.retry_path" + class="js-retry-job btn btn-inverted-secondary" + data-method="post" + rel="nofollow" + > + {{ __('Retry') }} + </a> + </div> + <div :class="{ block : renderBlock }"> + <p + v-if="job.merge_request" + class="build-detail-row js-job-mr" + > + <span class="build-light-text"> + {{ __('Merge Request:') }} + </span> + <a :href="job.merge_request.path"> + !{{ job.merge_request.iid }} </a> + </p> + + <detail-row + v-if="job.duration" + :value="duration" + class="js-job-duration" + title="Duration" + /> + <detail-row + v-if="job.finished_at" + :value="timeFormated(job.finished_at)" + class="js-job-finished" + title="Finished" + /> + <detail-row + v-if="job.erased_at" + :value="timeFormated(job.erased_at)" + class="js-job-erased" + title="Erased" + /> + <detail-row + v-if="job.queued" + :value="queued" + class="js-job-queued" + title="Queued" + /> + <detail-row + v-if="hasTimeout" + :help-url="runnerHelpUrl" + :value="timeout" + class="js-job-timeout" + title="Timeout" + /> + <detail-row + v-if="job.runner" + :value="runnerId" + class="js-job-runner" + title="Runner" + /> + <detail-row + v-if="job.coverage" + :value="coverage" + class="js-job-coverage" + title="Coverage" + /> + <p + v-if="job.tags.length" + class="build-detail-row js-job-tags" + > + <span class="build-light-text"> + {{ __('Tags:') }} + </span> + <span + v-for="(tag, i) in job.tags" + :key="i" + class="label label-primary"> + {{ tag }} + </span> + </p> + + <div + v-if="job.cancel_path" + class="btn-group prepend-top-5" + role="group"> <a - v-if="job.retry_path" - :href="job.retry_path" - class="js-retry-job btn btn-inverted-secondary" + :href="job.cancel_path" + class="js-cancel-job btn btn-sm btn-default" data-method="post" rel="nofollow" > - {{ __('Retry') }} + {{ __('Cancel') }} </a> </div> - <div :class="{ block : renderBlock }"> - <p - v-if="job.merge_request" - class="build-detail-row js-job-mr" - > - <span class="build-light-text"> - {{ __('Merge Request:') }} - </span> - <a :href="job.merge_request.path"> - !{{ job.merge_request.iid }} - </a> - </p> - - <detail-row - v-if="job.duration" - :value="duration" - class="js-job-duration" - title="Duration" - /> - <detail-row - v-if="job.finished_at" - :value="timeFormated(job.finished_at)" - class="js-job-finished" - title="Finished" - /> - <detail-row - v-if="job.erased_at" - :value="timeFormated(job.erased_at)" - class="js-job-erased" - title="Erased" - /> - <detail-row - v-if="job.queued" - :value="queued" - class="js-job-queued" - title="Queued" - /> - <detail-row - v-if="hasTimeout" - :help-url="runnerHelpUrl" - :value="timeout" - class="js-job-timeout" - title="Timeout" - /> - <detail-row - v-if="job.runner" - :value="runnerId" - class="js-job-runner" - title="Runner" - /> - <detail-row - v-if="job.coverage" - :value="coverage" - class="js-job-coverage" - title="Coverage" - /> - <p - v-if="job.tags.length" - class="build-detail-row js-job-tags" - > - <span class="build-light-text"> - {{ __('Tags:') }} - </span> - <span - v-for="(tag, i) in job.tags" - :key="i" - class="label label-primary"> - {{ tag }} - </span> - </p> - - <div - v-if="job.cancel_path" - class="btn-group prepend-top-5" - role="group"> - <a - :href="job.cancel_path" - class="js-cancel-job btn btn-sm btn-default" - data-method="post" - rel="nofollow" - > - {{ __('Cancel') }} - </a> - </div> - </div> - <artifacts-block - v-if="hasArtifact" - :artifact="job.artifact" - /> - <trigger-block - v-if="hasTriggers" - :trigger="job.trigger" - /> - <commit-block - :is-last-block="hasStages" - :commit="commit" - :merge-request="job.merge_request" - /> + </div> - <stages-dropdown - :stages="stages" - :pipeline="job.pipeline" - :selected-stage="selectedStage" - @requestSidebarStageDropdown="fetchJobsForStage" - /> + <artifacts-block + v-if="hasArtifact" + :artifact="job.artifact" + /> + <trigger-block + v-if="hasTriggers" + :trigger="job.trigger" + /> + <commit-block + :is-last-block="hasStages" + :commit="commit" + :merge-request="job.merge_request" + /> - </template> - <gl-loading-icon - v-else - :size="2" - class="prepend-top-10" + <stages-dropdown + :stages="stages" + :pipeline="job.pipeline" + :selected-stage="selectedStage" + @requestSidebarStageDropdown="fetchJobsForStage" /> </div> <jobs-container - v-if="!isLoading && jobs.length" + v-if="jobs.length" :jobs="jobs" :job-id="job.id" /> diff --git a/app/assets/javascripts/jobs/index.js b/app/assets/javascripts/jobs/index.js new file mode 100644 index 00000000000..ccd096a1da5 --- /dev/null +++ b/app/assets/javascripts/jobs/index.js @@ -0,0 +1,26 @@ +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, + }, + }); + }, + }); +}; + diff --git a/app/assets/javascripts/jobs/job_details_bundle.js b/app/assets/javascripts/jobs/job_details_bundle.js deleted file mode 100644 index 15cd79b1c50..00000000000 --- a/app/assets/javascripts/jobs/job_details_bundle.js +++ /dev/null @@ -1,76 +0,0 @@ -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(); -}; diff --git a/app/assets/javascripts/jobs/store/actions.js b/app/assets/javascripts/jobs/store/actions.js index d0040161dc3..54ed217572a 100644 --- a/app/assets/javascripts/jobs/store/actions.js +++ b/app/assets/javascripts/jobs/store/actions.js @@ -1,17 +1,32 @@ 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 */ diff --git a/app/assets/javascripts/jobs/store/getters.js b/app/assets/javascripts/jobs/store/getters.js index 9f4f372e3d2..4ce395a9106 100644 --- a/app/assets/javascripts/jobs/store/getters.js +++ b/app/assets/javascripts/jobs/store/getters.js @@ -1,5 +1,6 @@ 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 () => {}; diff --git a/app/assets/javascripts/jobs/store/mutation_types.js b/app/assets/javascripts/jobs/store/mutation_types.js index e66e1d4f116..fd098f13e90 100644 --- a/app/assets/javascripts/jobs/store/mutation_types.js +++ b/app/assets/javascripts/jobs/store/mutation_types.js @@ -1,10 +1,19 @@ 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'; diff --git a/app/assets/javascripts/jobs/store/mutations.js b/app/assets/javascripts/jobs/store/mutations.js index f00e06e1a6c..4195d787f12 100644 --- a/app/assets/javascripts/jobs/store/mutations.js +++ b/app/assets/javascripts/jobs/store/mutations.js @@ -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.ENABLE_SCROLL_BOTTOM](state) { + state.isScrollBottomDisabled = false; }, - [types.SCROLL_TO_BOTTOM](state) { - state.isTraceScrolledToBottom = true; - state.hasBeenScrolled = true; + [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) { diff --git a/app/assets/javascripts/jobs/store/state.js b/app/assets/javascripts/jobs/store/state.js index afbc959bb71..0eb269ca38f 100644 --- a/app/assets/javascripts/jobs/store/state.js +++ b/app/assets/javascripts/jobs/store/state.js @@ -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'), diff --git a/app/assets/javascripts/jobs/svg/scroll_down.svg b/app/assets/javascripts/jobs/svg/scroll_down.svg new file mode 100644 index 00000000000..1d22870ec09 --- /dev/null +++ b/app/assets/javascripts/jobs/svg/scroll_down.svg @@ -0,0 +1,5 @@ +<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> diff --git a/app/assets/javascripts/pages/projects/jobs/show/index.js b/app/assets/javascripts/pages/projects/jobs/show/index.js index 3626f3ffec6..d57dbeb1242 100644 --- a/app/assets/javascripts/pages/projects/jobs/show/index.js +++ b/app/assets/javascripts/pages/projects/jobs/show/index.js @@ -1,3 +1,3 @@ -import initJobDetails from '~/jobs/job_details_bundle'; +import initJobDetails from '~/jobs'; document.addEventListener('DOMContentLoaded', initJobDetails); diff --git a/app/assets/javascripts/vue_shared/components/header_ci_component.vue b/app/assets/javascripts/vue_shared/components/header_ci_component.vue index b371b6adf7e..aee88cae48d 100644 --- a/app/assets/javascripts/vue_shared/components/header_ci_component.vue +++ b/app/assets/javascripts/vue_shared/components/header_ci_component.vue @@ -69,6 +69,9 @@ export default { onClickAction(action) { this.$emit('actionClicked', action); }, + onClickSidebarButton() { + this.$emit('clickedSidebarButton'); + }, }, }; </script> @@ -161,21 +164,21 @@ export default { </i> </button> </template> - <button - v-if="hasSidebarButton" - id="toggleSidebar" - type="button" - class="btn btn-default d-block d-sm-none d-md-none + </section> + <button + v-if="hasSidebarButton" + id="toggleSidebar" + type="button" + 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" + aria-hidden="true" + aria-labelledby="toggleSidebar" > - <i - class="fa fa-angle-double-left" - aria-hidden="true" - aria-labelledby="toggleSidebar" - > - </i> - </button> - </section> + </i> + </button> </header> </template> diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss index ed877f625b5..227f49ec595 100644 --- a/app/assets/stylesheets/pages/builds.scss +++ b/app/assets/stylesheets/pages/builds.scss @@ -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; } } diff --git a/app/views/projects/artifacts/browse.html.haml b/app/views/projects/artifacts/browse.html.haml index 87b165e581a..09295940529 100644 --- a/app/views/projects/artifacts/browse.html.haml +++ b/app/views/projects/artifacts/browse.html.haml @@ -1,7 +1,7 @@ - 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)) diff --git a/app/views/projects/artifacts/file.html.haml b/app/views/projects/artifacts/file.html.haml index f7174d6b2c6..808b4acc8f3 100644 --- a/app/views/projects/artifacts/file.html.haml +++ b/app/views/projects/artifacts/file.html.haml @@ -1,6 +1,6 @@ - page_title @path, 'Artifacts', "#{@build.name} (##{@build.id})", 'Jobs' -= render "projects/jobs/header", show_controls: false += render "projects/jobs/header" .tree-holder .nav-block diff --git a/app/views/projects/jobs/_header.html.haml b/app/views/projects/jobs/_header.html.haml index e7245622b80..018ff093475 100644 --- a/app/views/projects/jobs/_header.html.haml +++ b/app/views/projects/jobs/_header.html.haml @@ -1,4 +1,3 @@ -- 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') diff --git a/app/views/projects/jobs/show.html.haml b/app/views/projects/jobs/show.html.haml index 02a088d338b..475bae887ec 100644 --- a/app/views/projects/jobs/show.html.haml +++ b/app/views/projects/jobs/show.html.haml @@ -1,52 +1,13 @@ - @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), - 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') } } + #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'), + build_options: javascript_build_options } } diff --git a/changelogs/unreleased/50904-job-log.yml b/changelogs/unreleased/50904-job-log.yml new file mode 100644 index 00000000000..31eb2d56b77 --- /dev/null +++ b/changelogs/unreleased/50904-job-log.yml @@ -0,0 +1,5 @@ +--- +title: Transform job page into a single Vue+Vuex application +merge_request: +author: +type: other diff --git a/locale/gitlab.pot b/locale/gitlab.pot index b8c4eddbec1..e7aa7ee1347 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -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 "" diff --git a/spec/features/projects/jobs/permissions_spec.rb b/spec/features/projects/jobs/permissions_spec.rb index e639f0cf82e..6ce37297a7e 100644 --- a/spec/features/projects/jobs/permissions_spec.rb +++ b/spec/features/projects/jobs/permissions_spec.rb @@ -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 diff --git a/spec/features/projects/jobs/user_browses_job_spec.rb b/spec/features/projects/jobs/user_browses_job_spec.rb index fc7b78ac21f..908c616f2fc 100644 --- a/spec/features/projects/jobs/user_browses_job_spec.rb +++ b/spec/features/projects/jobs/user_browses_job_spec.rb @@ -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)" diff --git a/spec/features/projects/jobs_spec.rb b/spec/features/projects/jobs_spec.rb index a3a301504ff..7f5c3ff6ed8 100644 --- a/spec/features/projects/jobs_spec.rb +++ b/spec/features/projects/jobs_spec.rb @@ -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 diff --git a/spec/javascripts/job_spec.js b/spec/javascripts/job_spec.js index d6b5dec9e47..bc973407b25 100644 --- a/spec/javascripts/job_spec.js +++ b/spec/javascripts/job_spec.js @@ -1,295 +1,265 @@ -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>'); +// }); +// }); +// }); + +// }); diff --git a/spec/javascripts/jobs/components/job_app_spec.js b/spec/javascripts/jobs/components/job_app_spec.js index 6e0bcf801cd..e6d403dc826 100644 --- a/spec/javascripts/jobs/components/job_app_spec.js +++ b/spec/javascripts/jobs/components/job_app_spec.js @@ -1,75 +1,196 @@ 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 }); + }); + + 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', 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 }); + setTimeout(() => { + expect(vm.shouldRenderCalloutMessage).toBe(true); + done(); + }, 0); + }); + }); + + describe('triggered job', () => { + beforeEach(() => { + 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', done => { + setTimeout(() => { + expect( + vm.$el + .querySelector('.header-main-content') + .textContent.replace(/\s+/g, ' ') + .trim(), + ).toEqual('passed Job #4757 triggered 1 year ago by Root'); + done(); + }, 0); + }); + + 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', 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('passed Job #4757 created 3 weeks ago by Root'); + done(); + }, 0); + }); + }); + }); + + describe('stuck block', () => { + it('renders stuck block when there are no runners', done => { + mock.onGet(props.endpoint).replyOnce( + 200, + Object.assign({}, job, { + status: { + group: 'pending', + icon: 'status_pending', + label: 'pending', + text: 'pending', + details_path: 'path', + }, + runners: { + available: false, + }, + }), + ); + 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', done => { + mock.onGet(props.endpoint).replyOnce( + 200, + Object.assign({}, job, { + status: { + group: 'pending', + icon: 'status_pending', + label: 'pending', + text: 'pending', + details_path: 'path', + }, + runners: { + available: false, + }, + }), + ); vm = mountComponentWithStore(Component, { props, store, }); - expect(vm.shouldRenderCalloutMessage).toBe(false); + setTimeout(() => { + expect(vm.$el.querySelector('.js-job-stuck').textContent).toContain(job.tags[0]); + done(); + }, 0); }); - it('should render the reason when reason is present', () => { - store.dispatch( - 'receiveJobSuccess', + it('does not renders stuck block when there are no runners', done => { + mock.onGet(props.endpoint).replyOnce( + 200, Object.assign({}, job, { - callout_message: 'There is an unknown failure, please try again', + runners: { available: true }, }), ); @@ -78,246 +199,324 @@ describe('Job App ', () => { store, }); - expect(vm.shouldRenderCalloutMessage).toBe(true); + setTimeout(() => { + expect(vm.$el.querySelector('.js-job-stuck')).toBeNull(); + + done(); + }, 0); }); }); - describe('triggered job', () => { - beforeEach(() => { - store.dispatch('receiveJobSuccess', job); + describe('environments block', () => { + it('renders environment block when job has environment', done => { + mock.onGet(props.endpoint).replyOnce( + 200, + Object.assign({}, job, { + deployment_status: { + environment: { + environment_path: '/path', + name: 'foo', + }, + }, + }), + ); vm = mountComponentWithStore(Component, { props, store, }); - }); - it('should render provided job information', () => { - expect( - vm.$el - .querySelector('.header-main-content') - .textContent.replace(/\s+/g, ' ') - .trim(), - ).toEqual('failed Job #123 triggered 2 days ago by Foo'); - }); + setTimeout(() => { + expect(vm.$el.querySelector('.js-job-environment')).not.toBeNull(); - it('should render new issue link', () => { - 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 })); + it('does not render environment block when job has environment', done => { + mock.onGet(props.endpoint).replyOnce(200, job); vm = mountComponentWithStore(Component, { props, store, }); - expect( - vm.$el - .querySelector('.header-main-content') - .textContent.replace(/\s+/g, ' ') - .trim(), - ).toEqual('failed Job #123 created 3 weeks ago by Foo'); + setTimeout(() => { + expect(vm.$el.querySelector('.js-job-environment')).toBeNull(); + done(); + }, 0); }); }); - }); - describe('stuck block', () => { - it('renders stuck block when there are no runners', () => { - store.dispatch( - 'receiveJobSuccess', - Object.assign({}, job, { - status: { - group: 'pending', - icon: 'status_pending', - label: 'pending', - text: 'pending', - details_path: 'path', - }, - }), - ); - - vm = mountComponentWithStore(Component, { - props, - store, - }); + describe('erased block', () => { + it('renders erased block when `erased` is true', done => { + mock.onGet(props.endpoint).replyOnce( + 200, + Object.assign({}, job, { + erased_by: { + username: 'root', + web_url: 'gitlab.com/root', + }, + erased_at: '2016-11-07T11:11:16.525Z', + }), + ); - expect(vm.$el.querySelector('.js-job-stuck')).not.toBeNull(); - }); + vm = mountComponentWithStore(Component, { + props, + store, + }); - it('renders tags in stuck block when there are no runners', () => { - store.dispatch( - 'receiveJobSuccess', - Object.assign({}, job, { - status: { - group: 'pending', - icon: 'status_pending', - label: 'pending', - text: 'pending', - details_path: 'path', - }, - }), - ); - - vm = mountComponentWithStore(Component, { - props, - store, + setTimeout(() => { + expect(vm.$el.querySelector('.js-job-erased-block')).not.toBeNull(); + + done(); + }, 0); }); - expect(vm.$el.querySelector('.js-job-stuck').textContent).toContain(job.tags[0]); - }); + it('does not render erased block when `erased` is false', done => { + mock.onGet(props.endpoint).replyOnce(200, Object.assign({}, job, { erased_at: null })); - it(' does not renders stuck block when there are no runners', () => { - store.dispatch('receiveJobSuccess', Object.assign({}, job, { runners: { available: true } })); + vm = mountComponentWithStore(Component, { + props, + store, + }); - vm = mountComponentWithStore(Component, { - props, - store, - }); + setTimeout(() => { + expect(vm.$el.querySelector('.js-job-erased-block')).toBeNull(); - expect(vm.$el.querySelector('.js-job-stuck')).toBeNull(); + done(); + }, 0); + }); }); - }); - describe('environments block', () => { - it('renders environment block when job has environment', () => { - store.dispatch( - 'receiveJobSuccess', - Object.assign({}, job, { - deployment_status: { - environment: { - environment_path: '/path', - name: 'foo', + describe('empty states block', () => { + 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: { + group: 'pending', + icon: 'status_pending', + label: 'pending', + text: 'pending', + details_path: 'path', + illustration: { + image: 'path', + size: '340', + title: 'Empty State', + content: 'This is an empty state', + }, + action: { + button_title: 'Retry job', + method: 'post', + path: '/path', + }, }, - }, - }), - ); + }), + ); - vm = mountComponentWithStore(Component, { - props, - store, + vm = mountComponentWithStore(Component, { + props, + store, + }); + + setTimeout(() => { + expect(vm.$el.querySelector('.js-job-empty-state')).not.toBeNull(); + + done(); + }, 0); }); - expect(vm.$el.querySelector('.js-job-environment')).not.toBeNull(); - }); + 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: { + group: 'running', + icon: 'status_running', + label: 'running', + text: 'running', + details_path: 'path', + }, + }), + ); + + vm = mountComponentWithStore(Component, { + props, + store, + }); - it('does not render environment block when job has environment', () => { - store.dispatch('receiveJobSuccess', job); + setTimeout(() => { + expect(vm.$el.querySelector('.js-job-empty-state')).toBeNull(); - vm = mountComponentWithStore(Component, { - props, - store, + done(); + }, 0); }); - expect(vm.$el.querySelector('.js-job-environment')).toBeNull(); - }); - }); + 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, + }); - describe('erased block', () => { - it('renders erased block when `erased` is true', () => { - store.dispatch( - 'receiveJobSuccess', - Object.assign({}, job, { - erased_by: { - username: 'root', - web_url: 'gitlab.com/root', - }, - erased_at: '2016-11-07T11:11:16.525Z', - }), - ); - - vm = mountComponentWithStore(Component, { - props, - store, + setTimeout(() => { + expect(vm.$el.querySelector('.js-job-empty-state')).toBeNull(); + + done(); + }, 0); }); + }); + }); - expect(vm.$el.querySelector('.js-job-erased-block')).not.toBeNull(); + describe('trace output', () => { + beforeEach(() => { + mock.onGet(props.endpoint).reply(200, job, {}); }); - it('does not render erased block when `erased` is false', () => { - store.dispatch('receiveJobSuccess', Object.assign({}, job, { erased_at: null })); + 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 = mountComponentWithStore(Component, { + props, + store, + }); - expect(vm.$el.querySelector('.js-job-erased-block')).toBeNull(); - }); - }); + vm.$store.state.trace = 'Update'; - describe('empty states block', () => { - it('renders empty state when job does not have trace and is not running', () => { - store.dispatch( - 'receiveJobSuccess', - Object.assign({}, job, { - has_trace: false, - status: { - group: 'pending', - icon: 'status_pending', - label: 'pending', - text: 'pending', - details_path: 'path', - illustration: { - image: 'path', - size: '340', - title: 'Empty State', - content: 'This is an empty state', - }, - action: { - button_title: 'Retry job', - method: 'post', - path: '/path', - }, - }, - }), - ); + setTimeout(() => { + expect(vm.$el.querySelector('.js-build-trace').textContent.trim()).toContain('Update'); - vm = mountComponentWithStore(Component, { - props, - store, + 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, + }); - expect(vm.$el.querySelector('.js-job-empty-state')).not.toBeNull(); + 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); + }); }); - it('does not render empty state when job does not have trace but it is running', () => { - store.dispatch( - 'receiveJobSuccess', - Object.assign({}, job, { - has_trace: false, - status: { - group: 'running', - icon: 'status_running', - label: 'running', - text: 'running', - details_path: 'path', - }, - }), - ); - - vm = mountComponentWithStore(Component, { - props, - store, + 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); + }); }); - expect(vm.$el.querySelector('.js-job-empty-state')).toBeNull(); + 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); + }); + }); }); - it('does not render empty state when job has trace but it is not running', () => { - store.dispatch('receiveJobSuccess', Object.assign({}, job, { has_trace: true })); + 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); + }); - vm = mountComponentWithStore(Component, { - props, - store, + it('should render link to raw ouput', done => { + setTimeout(() => { + expect(vm.$el.querySelector('.js-raw-link-controller')).not.toBeNull(); + done(); + }, 0); }); - expect(vm.$el.querySelector('.js-job-empty-state')).toBeNull(); + it('should render link to erase job', done => { + setTimeout(() => { + expect(vm.$el.querySelector('.js-erase-link')).not.toBeNull(); + done(); + }, 0); + }); }); }); }); diff --git a/spec/javascripts/jobs/components/job_log_spec.js b/spec/javascripts/jobs/components/job_log_spec.js index 1011512360d..413a96e502a 100644 --- a/spec/javascripts/jobs/components/job_log_spec.js +++ b/spec/javascripts/jobs/components/job_log_spec.js @@ -1,21 +1,32 @@ 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, { - trace, - isComplete: true, + 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, { - trace, - isComplete: true, + vm = mountComponentWithStore(Component, { + props: { + trace, + 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, { - trace, - isComplete: false, + vm = mountComponentWithStore(Component, { + props: { + trace, + isComplete: true, + }, + store, }); expect(vm.$el.querySelector('.js-log-animation')).toBeNull(); diff --git a/spec/javascripts/jobs/components/sidebar_spec.js b/spec/javascripts/jobs/components/sidebar_spec.js index ca4f62bd6ef..460a2e1b5da 100644 --- a/spec/javascripts/jobs/components/sidebar_spec.js +++ b/spec/javascripts/jobs/components/sidebar_spec.js @@ -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(); diff --git a/spec/javascripts/jobs/mock_data.js b/spec/javascripts/jobs/mock_data.js index 4269b42e8b6..ca6fbabeeb6 100644 --- a/spec/javascripts/jobs/mock_data.js +++ b/spec/javascripts/jobs/mock_data.js @@ -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 = [ diff --git a/spec/javascripts/jobs/store/actions_spec.js b/spec/javascripts/jobs/store/actions_spec.js index bc410ae614c..76af08b367d 100644 --- a/spec/javascripts/jobs/store/actions_spec.js +++ b/spec/javascripts/jobs/store/actions_spec.js @@ -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: { @@ -262,9 +287,6 @@ describe('Job State actions', () => { [], [ { - 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); diff --git a/spec/javascripts/jobs/store/helpers.js b/spec/javascripts/jobs/store/helpers.js new file mode 100644 index 00000000000..81a769b4a6e --- /dev/null +++ b/spec/javascripts/jobs/store/helpers.js @@ -0,0 +1,6 @@ +import state from '~/jobs/store/state'; + +// eslint-disable-next-line import/prefer-default-export +export const resetStore = store => { + store.replaceState(state()); +}; diff --git a/spec/javascripts/jobs/store/mutations_spec.js b/spec/javascripts/jobs/store/mutations_spec.js index c058a3b3d51..9bbf4ba8951 100644 --- a/spec/javascripts/jobs/store/mutations_spec.js +++ b/spec/javascripts/jobs/store/mutations_spec.js @@ -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); diff --git a/spec/javascripts/vue_shared/components/header_ci_component_spec.js b/spec/javascripts/vue_shared/components/header_ci_component_spec.js index f17818c17c7..43750c74b99 100644 --- a/spec/javascripts/vue_shared/components/header_ci_component_spec.js +++ b/spec/javascripts/vue_shared/components/header_ci_component_spec.js @@ -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(); }); }); |