From 9128e7849dbc064913b52ad427dcfb15386ad23e Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Wed, 3 Oct 2018 15:29:07 +0000 Subject: Uses Vue app to render part of job show page --- .../jobs/components/environments_block.vue | 68 +++++--- app/assets/javascripts/jobs/components/header.vue | 95 ----------- app/assets/javascripts/jobs/components/job_app.vue | 99 +++++++++++ app/assets/javascripts/jobs/job_details_bundle.js | 7 +- app/assets/javascripts/jobs/store/getters.js | 42 +++++ app/assets/javascripts/jobs/store/index.js | 2 + app/views/projects/jobs/show.html.haml | 48 ------ changelogs/unreleased/50904-vuex-show-block.yml | 5 + locale/gitlab.pot | 5 +- .../projects/jobs/user_browses_job_spec.rb | 8 +- spec/features/projects/jobs_spec.rb | 158 ++++++++++++++++-- .../jobs/components/environments_block_spec.js | 59 ++++--- spec/javascripts/jobs/components/header_spec.js | 98 ----------- spec/javascripts/jobs/components/job_app_spec.js | 185 +++++++++++++++++++++ spec/javascripts/jobs/store/getters_spec.js | 121 ++++++++++++++ spec/views/projects/jobs/show.html.haml_spec.rb | 155 ----------------- 16 files changed, 687 insertions(+), 468 deletions(-) delete mode 100644 app/assets/javascripts/jobs/components/header.vue create mode 100644 app/assets/javascripts/jobs/components/job_app.vue create mode 100644 app/assets/javascripts/jobs/store/getters.js create mode 100644 changelogs/unreleased/50904-vuex-show-block.yml delete mode 100644 spec/javascripts/jobs/components/header_spec.js create mode 100644 spec/javascripts/jobs/components/job_app_spec.js create mode 100644 spec/javascripts/jobs/store/getters_spec.js diff --git a/app/assets/javascripts/jobs/components/environments_block.vue b/app/assets/javascripts/jobs/components/environments_block.vue index ca6386595c7..e6e1d418194 100644 --- a/app/assets/javascripts/jobs/components/environments_block.vue +++ b/app/assets/javascripts/jobs/components/environments_block.vue @@ -12,12 +12,16 @@ type: Object, required: true, }, + iconStatus: { + type: Object, + required: true, + }, }, computed: { environment() { let environmentText; switch (this.deploymentStatus.status) { - case 'latest': + case 'last': environmentText = sprintf( __('This job is the most recent deployment to %{link}.'), { link: this.environmentLink }, @@ -32,7 +36,7 @@ ), { environmentLink: this.environmentLink, - deploymentLink: this.deploymentLink, + deploymentLink: this.deploymentLink(`#${this.lastDeployment.iid}`), }, false, ); @@ -56,11 +60,11 @@ if (this.hasLastDeployment) { environmentText = sprintf( __( - 'This job is creating a deployment to %{environmentLink} and will overwrite the last %{deploymentLink}.', + 'This job is creating a deployment to %{environmentLink} and will overwrite the %{deploymentLink}.', ), { environmentLink: this.environmentLink, - deploymentLink: this.deploymentLink, + deploymentLink: this.deploymentLink(__('latest deployment')), }, false, ); @@ -78,41 +82,57 @@ return environmentText; }, environmentLink() { - return sprintf( - '%{startLink}%{name}%{endLink}', - { - startLink: ``, - name: _.escape(this.deploymentStatus.environment.name), - endLink: '', - }, - false, - ); + if (this.hasEnvironment) { + return sprintf( + '%{startLink}%{name}%{endLink}', + { + startLink: ``, + name: _.escape(this.deploymentStatus.environment.name), + endLink: '', + }, + false, + ); + } + return ''; }, - deploymentLink() { + hasLastDeployment() { + return this.hasEnvironment && this.deploymentStatus.environment.last_deployment; + }, + lastDeployment() { + return this.hasLastDeployment ? this.deploymentStatus.environment.last_deployment : {}; + }, + hasEnvironment() { + return !_.isEmpty(this.deploymentStatus.environment); + }, + lastDeploymentPath() { + return !_.isEmpty(this.lastDeployment.deployable) ? this.lastDeployment.deployable.build_path : ''; + }, + }, + methods: { + deploymentLink(name) { return sprintf( '%{startLink}%{name}%{endLink}', { - startLink: ``, - name: _.escape(this.lastDeployment.name), + startLink: ``, + name, endLink: '', }, false, ); }, - hasLastDeployment() { - return this.deploymentStatus.environment.last_deployment; - }, - lastDeployment() { - return this.deploymentStatus.environment.last_deployment; - }, }, }; diff --git a/app/assets/javascripts/jobs/components/header.vue b/app/assets/javascripts/jobs/components/header.vue deleted file mode 100644 index 63324e68d68..00000000000 --- a/app/assets/javascripts/jobs/components/header.vue +++ /dev/null @@ -1,95 +0,0 @@ - - diff --git a/app/assets/javascripts/jobs/components/job_app.vue b/app/assets/javascripts/jobs/components/job_app.vue new file mode 100644 index 00000000000..bac8bd71d64 --- /dev/null +++ b/app/assets/javascripts/jobs/components/job_app.vue @@ -0,0 +1,99 @@ + + diff --git a/app/assets/javascripts/jobs/job_details_bundle.js b/app/assets/javascripts/jobs/job_details_bundle.js index ae40f4cdf3b..3eb75e72506 100644 --- a/app/assets/javascripts/jobs/job_details_bundle.js +++ b/app/assets/javascripts/jobs/job_details_bundle.js @@ -2,7 +2,7 @@ import _ from 'underscore'; import { mapState, mapActions } from 'vuex'; import Vue from 'vue'; import Job from '../job'; -import JobHeader from './components/header.vue'; +import JobApp from './components/job_app.vue'; import Sidebar from './components/sidebar.vue'; import createStore from './store'; @@ -22,17 +22,18 @@ export default () => { new Vue({ el: '#js-build-header-vue', components: { - JobHeader, + JobApp, }, store, computed: { ...mapState(['job', 'isLoading']), }, render(createElement) { - return createElement('job-header', { + return createElement('job-app', { props: { isLoading: this.isLoading, job: this.job, + runnerHelpUrl: dataset.runnerHelpUrl, }, }); }, diff --git a/app/assets/javascripts/jobs/store/getters.js b/app/assets/javascripts/jobs/store/getters.js new file mode 100644 index 00000000000..62d154ff584 --- /dev/null +++ b/app/assets/javascripts/jobs/store/getters.js @@ -0,0 +1,42 @@ +import _ from 'underscore'; +import { __ } from '~/locale'; + +export const headerActions = state => { + if (state.job.new_issue_path) { + return [ + { + label: __('New issue'), + path: state.job.new_issue_path, + cssClass: + 'js-new-issue btn btn-success btn-inverted d-none d-md-block d-lg-block d-xl-block', + type: 'link', + }, + ]; + } + return []; +}; + +export const headerTime = state => (state.job.started ? state.job.started : state.job.created_at); + +export const shouldRenderCalloutMessage = state => + !_.isEmpty(state.job.status) && !_.isEmpty(state.job.callout_message); + +/** + * When job has not started the key will be `false` + * When job started the key will be a string with a date. + */ +export const jobHasStarted = state => !(state.job.started === false); + +export const hasEnvironment = state => !_.isEmpty(state.job.deployment_status); + +/** + * When the job is pending and there are no available runners + * we need to render the stuck block; + * + * @returns {Boolean} + */ +export const isJobStuck = state => + state.job.status.group === 'pending' && state.job.runners && state.job.runners.available === false; + +// prevent babel-plugin-rewire from generating an invalid default during karma tests +export default () => {}; diff --git a/app/assets/javascripts/jobs/store/index.js b/app/assets/javascripts/jobs/store/index.js index d8f6f56ce61..96e38f9a2fa 100644 --- a/app/assets/javascripts/jobs/store/index.js +++ b/app/assets/javascripts/jobs/store/index.js @@ -2,6 +2,7 @@ import Vue from 'vue'; import Vuex from 'vuex'; import state from './state'; import * as actions from './actions'; +import * as getters from './getters'; import mutations from './mutations'; Vue.use(Vuex); @@ -9,5 +10,6 @@ Vue.use(Vuex); export default () => new Vuex.Store({ actions, mutations, + getters, state: state(), }); diff --git a/app/views/projects/jobs/show.html.haml b/app/views/projects/jobs/show.html.haml index db62de80bf3..ab7963737ca 100644 --- a/app/views/projects/jobs/show.html.haml +++ b/app/views/projects/jobs/show.html.haml @@ -9,54 +9,6 @@ %div{ class: container_class } .build-page.js-build-page #js-build-header-vue - - if @build.stuck? - - unless @build.any_runners_online? - .bs-callout.bs-callout-warning.js-build-stuck - %p - - if @project.any_runners? - This job is stuck, because the project doesn't have any runners online assigned to it. - - elsif @build.tags.any? - This job is stuck, because you don't have any active runners online with any of these tags assigned to them: - - @build.tags.each do |tag| - %span.badge.badge-primary - = tag - - else - This job is stuck, because you don't have any active runners that can run this job. - - %br - Go to - = link_to project_runners_path(@build.project, anchor: 'js-runners-settings') do - Runners page - - - if @build.starts_environment? - .prepend-top-default.js-environment-container - .environment-information - - if @build.outdated_deployment? - = ci_icon_for_status('success_with_warnings') - - else - = ci_icon_for_status(@build.status) - - - environment = environment_for_build(@build.project, @build) - - if @build.success? && @build.last_deployment.present? - - if @build.last_deployment.last? - This job is the most recent deployment to #{environment_link_for_build(@build.project, @build)}. - - else - This job is an out-of-date deployment to #{environment_link_for_build(@build.project, @build)}. - View the most recent deployment #{deployment_link(environment.last_deployment)}. - - elsif @build.complete? && !@build.success? - The deployment of this job to #{environment_link_for_build(@build.project, @build)} did not succeed. - - else - This job is creating a deployment to #{environment_link_for_build(@build.project, @build)} - - if environment.try(:last_deployment) - and will overwrite the #{deployment_link(environment.last_deployment, text: 'latest deployment')} - - - if @build.erased? - .prepend-top-default.js-build-erased - .erased.alert.alert-warning - - if @build.erased_by_user? - Job has been erased by #{link_to(@build.erased_by_name, user_path(@build.erased_by))} #{time_ago_with_tooltip(@build.erased_at)} - - else - Job has been erased #{time_ago_with_tooltip(@build.erased_at)} - if @build.running? || @build.has_trace? .build-trace-container.prepend-top-default diff --git a/changelogs/unreleased/50904-vuex-show-block.yml b/changelogs/unreleased/50904-vuex-show-block.yml new file mode 100644 index 00000000000..5607ba3216f --- /dev/null +++ b/changelogs/unreleased/50904-vuex-show-block.yml @@ -0,0 +1,5 @@ +--- +title: Renders Job show page in new Vue app +merge_request: +author: +type: other diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 09b83bf15c1..5591b0b8bb2 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -6106,7 +6106,7 @@ msgstr "" msgid "This job is an out-of-date deployment to %{environmentLink}. View the most recent deployment %{deploymentLink}." msgstr "" -msgid "This job is creating a deployment to %{environmentLink} and will overwrite the last %{deploymentLink}." +msgid "This job is creating a deployment to %{environmentLink} and will overwrite the %{deploymentLink}." msgstr "" msgid "This job is creating a deployment to %{environmentLink}." @@ -7054,6 +7054,9 @@ msgstr "" msgid "issue boards" msgstr "" +msgid "latest deployment" +msgstr "" + msgid "latest version" msgstr "" diff --git a/spec/features/projects/jobs/user_browses_job_spec.rb b/spec/features/projects/jobs/user_browses_job_spec.rb index 7be6d23af65..fc7b78ac21f 100644 --- a/spec/features/projects/jobs/user_browses_job_spec.rb +++ b/spec/features/projects/jobs/user_browses_job_spec.rb @@ -16,7 +16,9 @@ describe 'User browses a job', :js do visit(project_job_path(project, build)) end - it 'erases the job log' do + it 'erases the job log', :js do + wait_for_requests + expect(page).to have_content("Job ##{build.id}") expect(page).to have_css('#build-trace') @@ -29,9 +31,7 @@ describe 'User browses a job', :js do expect(build.artifacts_file.exists?).to be_falsy expect(build.artifacts_metadata.exists?).to be_falsy - page.within('.erased') do - expect(page).to have_content('Job has been erased') - end + expect(page).to have_content('Job has been erased') end context 'with a failed job' do diff --git a/spec/features/projects/jobs_spec.rb b/spec/features/projects/jobs_spec.rb index a95140f0fa4..6213cabfaad 100644 --- a/spec/features/projects/jobs_spec.rb +++ b/spec/features/projects/jobs_spec.rb @@ -369,39 +369,167 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do end end - context 'when job starts environment' do - let(:environment) { create(:environment, project: project) } - let(:pipeline) { create(:ci_pipeline, project: project) } + context 'when job starts environment', :js do + let(:environment) { create(:environment, name: 'production', project: project) } - context 'job is successfull and has deployment' do - let(:deployment) { create(:deployment) } - let(:job) { create(:ci_build, :success, :trace_artifact, environment: environment.name, deployments: [deployment], pipeline: pipeline) } + context 'job is successful and has deployment' do + let(:build) { create(:ci_build, :success, :trace_live, environment: environment.name, pipeline: pipeline) } + let!(:deployment) { create(:deployment, environment: environment, project: environment.project, deployable: build) } - it 'shows a link for the job' do - visit project_job_path(project, job) + before do + visit project_job_path(project, build) + wait_for_requests + # scroll to the top of the page first + execute_script "window.scrollTo(0,0)" + end + it 'shows a link for the job' do expect(page).to have_link environment.name end + + it 'shows deployment message' do + expect(page).to have_content 'This job is the most recent deployment' + expect(find('.js-environment-link')['href']).to match("environments/#{environment.id}") + end end context 'job is complete and not successful' do - let(:job) { create(:ci_build, :failed, :trace_artifact, environment: environment.name, pipeline: pipeline) } + let(:build) { create(:ci_build, :failed, :trace_artifact, environment: environment.name, pipeline: pipeline) } it 'shows a link for the job' do - visit project_job_path(project, job) + visit project_job_path(project, build) + wait_for_requests + # scroll to the top of the page first + execute_script "window.scrollTo(0,0)" expect(page).to have_link environment.name + expect(find('.js-environment-link')['href']).to match("environments/#{environment.id}") end end - context 'job creates a new deployment' do - let!(:deployment) { create(:deployment, environment: environment, sha: project.commit.id) } - let(:job) { create(:ci_build, :success, :trace_artifact, environment: environment.name, pipeline: pipeline) } + context 'deployment still not finished' do + let(:build) { create(:ci_build, :success, environment: environment.name, pipeline: pipeline) } it 'shows a link to latest deployment' do - visit project_job_path(project, job) + visit project_job_path(project, build) + wait_for_all_requests + # scroll to the top of the page first + execute_script "window.scrollTo(0,0)" + + expect(page).to have_link environment.name + expect(page).to have_content 'This job is creating a deployment' + expect(find('.js-environment-link')['href']).to match("environments/#{environment.id}") + end + end + end + + describe 'environment info in job view', :js do + before do + visit project_job_path(project, job) + wait_for_requests + # scroll to the top of the page first + execute_script "window.scrollTo(0,0)" + end + + context 'job with outdated deployment' do + let(:job) { create(:ci_build, :success, :trace_artifact, environment: 'staging', pipeline: pipeline) } + let(:second_build) { create(:ci_build, :success, :trace_artifact, environment: 'staging', pipeline: pipeline) } + let(:environment) { create(:environment, name: 'staging', project: project) } + let!(:first_deployment) { create(:deployment, environment: environment, deployable: job) } + let!(:second_deployment) { create(:deployment, environment: environment, deployable: second_build) } + + it 'shows deployment message' do + expected_text = 'This job is an out-of-date deployment ' \ + "to staging. View the most recent deployment ##{second_deployment.iid}." + + expect(page).to have_css('.environment-information', text: expected_text) + end + + it 'renders a link to the most recent deployment' do + expect(find('.js-environment-link')['href']).to match("environments/#{environment.id}") + expect(find('.js-job-deployment-link')['href']).to include(second_deployment.deployable.project.path, second_deployment.deployable_id.to_s) + end + end + + context 'job failed to deploy' do + let(:job) { create(:ci_build, :failed, :trace_artifact, environment: 'staging', pipeline: pipeline) } + let!(:environment) { create(:environment, name: 'staging', project: project) } + + it 'shows deployment message' do + expected_text = 'The deployment of this job to staging did not succeed.' + + expect(page).to have_css( + '.environment-information', text: expected_text) + end + end + + context 'job will deploy' do + let(:job) { create(:ci_build, :running, :trace_live, environment: 'staging', pipeline: pipeline) } + + context 'when environment exists' do + let!(:environment) { create(:environment, name: 'staging', project: project) } + + it 'shows deployment message' do + expected_text = 'This job is creating a deployment to staging' + + expect(page).to have_css( + '.environment-information', text: expected_text) + expect(find('.js-environment-link')['href']).to match("environments/#{environment.id}") + end + + context 'when it has deployment' do + let!(:deployment) { create(:deployment, environment: environment) } + + it 'shows that deployment will be overwritten' do + expected_text = 'This job is creating a deployment to staging' + + expect(page).to have_css( + '.environment-information', text: expected_text) + expect(page).to have_css( + '.environment-information', text: 'latest deployment') + expect(find('.js-environment-link')['href']).to match("environments/#{environment.id}") + end + end + end + + context 'when environment does not exist' do + let!(:environment) { create(:environment, name: 'staging', project: project) } + + it 'shows deployment message' do + expected_text = 'This job is creating a deployment to staging' + + expect(page).to have_css( + '.environment-information', text: expected_text) + expect(page).not_to have_css( + '.environment-information', text: 'latest deployment') + expect(find('.js-environment-link')['href']).to match("environments/#{environment.id}") + end + end + end + + context 'job that failed to deploy and environment has not been created' do + let(:job) { create(:ci_build, :failed, :trace_artifact, environment: 'staging', pipeline: pipeline) } + let!(:environment) { create(:environment, name: 'staging', project: project) } + + it 'shows deployment message' do + expected_text = 'The deployment of this job to staging did not succeed' + + expect(page).to have_css( + '.environment-information', text: expected_text) + end + end + + context 'job that will deploy and environment has not been created' do + let(:job) { create(:ci_build, :running, :trace_live, environment: 'staging', pipeline: pipeline) } + let!(:environment) { create(:environment, name: 'staging', project: project) } + + it 'shows deployment message' do + expected_text = 'This job is creating a deployment to staging' - expect(page).to have_link('latest deployment') + expect(page).to have_css( + '.environment-information', text: expected_text) + expect(page).not_to have_css( + '.environment-information', text: 'latest deployment') end end end diff --git a/spec/javascripts/jobs/components/environments_block_spec.js b/spec/javascripts/jobs/components/environments_block_spec.js index 015c26be9fc..7d836129b13 100644 --- a/spec/javascripts/jobs/components/environments_block_spec.js +++ b/spec/javascripts/jobs/components/environments_block_spec.js @@ -5,19 +5,16 @@ import mountComponent from '../../helpers/vue_mount_component_helper'; describe('Environments block', () => { const Component = Vue.extend(component); let vm; - const icon = { + const status = { group: 'success', icon: 'status_success', label: 'passed', text: 'passed', tooltip: 'passed', }; - const deployment = { - path: 'deployment', - name: 'deployment name', - }; + const environment = { - path: '/environment', + environment_path: '/environment', name: 'environment', }; @@ -25,15 +22,14 @@ describe('Environments block', () => { vm.$destroy(); }); - describe('with latest deployment', () => { + describe('with last deployment', () => { it('renders info for most recent deployment', () => { vm = mountComponent(Component, { deploymentStatus: { - status: 'latest', - icon, - deployment, + status: 'last', environment, }, + iconStatus: status, }); expect(vm.$el.textContent.trim()).toEqual( @@ -48,17 +44,17 @@ describe('Environments block', () => { vm = mountComponent(Component, { deploymentStatus: { status: 'out_of_date', - icon, - deployment, environment: Object.assign({}, environment, { - last_deployment: { name: 'deployment', path: 'last_deployment' }, + last_deployment: { iid: 'deployment', deployable: { build_path: 'bar' } }, }), }, + iconStatus: status, }); expect(vm.$el.textContent.trim()).toEqual( - 'This job is an out-of-date deployment to environment. View the most recent deployment deployment.', + 'This job is an out-of-date deployment to environment. View the most recent deployment #deployment.', ); + expect(vm.$el.querySelector('.js-job-deployment-link').getAttribute('href')).toEqual('bar'); }); }); @@ -67,10 +63,9 @@ describe('Environments block', () => { vm = mountComponent(Component, { deploymentStatus: { status: 'out_of_date', - icon, - deployment: null, environment, }, + iconStatus: status, }); expect(vm.$el.textContent.trim()).toEqual( @@ -85,10 +80,9 @@ describe('Environments block', () => { vm = mountComponent(Component, { deploymentStatus: { status: 'failed', - icon, - deployment: null, environment, }, + iconStatus: status, }); expect(vm.$el.textContent.trim()).toEqual( @@ -99,21 +93,24 @@ describe('Environments block', () => { describe('creating deployment', () => { describe('with last deployment', () => { - it('renders info about creating deployment and overriding lastest deployment', () => { + it('renders info about creating deployment and overriding latest deployment', () => { vm = mountComponent(Component, { deploymentStatus: { status: 'creating', - icon, - deployment, environment: Object.assign({}, environment, { - last_deployment: { name: 'deployment', path: 'last_deployment' }, + last_deployment: { + iid: 'deployment', + deployable: { build_path: 'foo' }, + }, }), }, + iconStatus: status, }); expect(vm.$el.textContent.trim()).toEqual( - 'This job is creating a deployment to environment and will overwrite the last deployment.', + 'This job is creating a deployment to environment and will overwrite the latest deployment.', ); + expect(vm.$el.querySelector('.js-job-deployment-link').getAttribute('href')).toEqual('foo'); }); }); @@ -122,10 +119,9 @@ describe('Environments block', () => { vm = mountComponent(Component, { deploymentStatus: { status: 'creating', - icon, - deployment: null, environment, }, + iconStatus: status, }); expect(vm.$el.textContent.trim()).toEqual( @@ -133,5 +129,18 @@ describe('Environments block', () => { ); }); }); + + describe('without environment', () => { + it('does not render environment link', () => { + vm = mountComponent(Component, { + deploymentStatus: { + status: 'creating', + environment: null, + }, + iconStatus: status, + }); + expect(vm.$el.querySelector('.js-environment-link')).toBeNull(); + }); + }); }); }); diff --git a/spec/javascripts/jobs/components/header_spec.js b/spec/javascripts/jobs/components/header_spec.js deleted file mode 100644 index e21e2c6d6e3..00000000000 --- a/spec/javascripts/jobs/components/header_spec.js +++ /dev/null @@ -1,98 +0,0 @@ -import Vue from 'vue'; -import headerComponent from '~/jobs/components/header.vue'; -import mountComponent from 'spec/helpers/vue_mount_component_helper'; - -describe('Job details header', () => { - let HeaderComponent; - let vm; - let props; - - beforeEach(() => { - HeaderComponent = Vue.extend(headerComponent); - - const threeWeeksAgo = new Date(); - threeWeeksAgo.setDate(threeWeeksAgo.getDate() - 21); - - const twoDaysAgo = new Date(); - twoDaysAgo.setDate(twoDaysAgo.getDate() - 2); - - props = { - 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', - }, - isLoading: false, - }; - }); - - afterEach(() => { - vm.$destroy(); - }); - - describe('job reason', () => { - it('should not render the reason when reason is absent', () => { - vm = mountComponent(HeaderComponent, props); - - expect(vm.shouldRenderReason).toBe(false); - }); - - it('should render the reason when reason is present', () => { - props.job.callout_message = 'There is an unknown failure, please try again'; - - vm = mountComponent(HeaderComponent, props); - - expect(vm.shouldRenderReason).toBe(true); - }); - }); - - describe('triggered job', () => { - beforeEach(() => { - vm = mountComponent(HeaderComponent, props); - }); - - 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'); - }); - - it('should render new issue link', () => { - expect(vm.$el.querySelector('.js-new-issue').getAttribute('href')).toEqual( - props.job.new_issue_path, - ); - }); - }); - - describe('created job', () => { - it('should render created key', () => { - props.job.started = false; - vm = mountComponent(HeaderComponent, props); - - expect( - vm.$el - .querySelector('.header-main-content') - .textContent.replace(/\s+/g, ' ') - .trim(), - ).toEqual('failed Job #123 created 3 weeks ago by Foo'); - }); - }); -}); diff --git a/spec/javascripts/jobs/components/job_app_spec.js b/spec/javascripts/jobs/components/job_app_spec.js new file mode 100644 index 00000000000..c31fa6f9887 --- /dev/null +++ b/spec/javascripts/jobs/components/job_app_spec.js @@ -0,0 +1,185 @@ +import Vue from 'vue'; +import jobApp from '~/jobs/components/job_app.vue'; +import createStore from '~/jobs/store'; +import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; + +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'], + }; + + const props = { + runnerHelpUrl: 'help/runners', + }; + + beforeEach(() => { + store = createStore(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('Header section', () => { + describe('job callout message', () => { + it('should not render the reason when reason is absent', () => { + store.dispatch('receiveJobSuccess', job); + + vm = mountComponentWithStore(Component, { + props, + store, + }); + + expect(vm.shouldRenderCalloutMessage).toBe(false); + }); + + it('should render the reason when reason is present', () => { + store.dispatch( + 'receiveJobSuccess', + Object.assign({}, job, { + callout_message: 'There is an unknown failure, please try again', + }), + ); + + vm = mountComponentWithStore(Component, { + props, + store, + }); + + expect(vm.shouldRenderCalloutMessage).toBe(true); + }); + }); + + describe('triggered job', () => { + beforeEach(() => { + store.dispatch('receiveJobSuccess', job); + + 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'); + }); + + it('should render new issue link', () => { + expect(vm.$el.querySelector('.js-new-issue').getAttribute('href')).toEqual( + job.new_issue_path, + ); + }); + }); + + describe('created job', () => { + it('should render created key', () => { + store.dispatch('receiveJobSuccess', Object.assign({}, job, { started: false })); + + 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'); + }); + }); + }); + + 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, + }); + + expect(vm.$el.querySelector('.js-job-stuck')).not.toBeNull(); + }); + + 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, + }); + + expect(vm.$el.querySelector('.js-job-stuck').textContent).toContain(job.tags[0]); + }); + + 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, + }); + + expect(vm.$el.querySelector('.js-job-stuck')).toBeNull(); + }); + }); +}); diff --git a/spec/javascripts/jobs/store/getters_spec.js b/spec/javascripts/jobs/store/getters_spec.js new file mode 100644 index 00000000000..63ef4135d83 --- /dev/null +++ b/spec/javascripts/jobs/store/getters_spec.js @@ -0,0 +1,121 @@ +import * as getters from '~/jobs/store/getters'; +import state from '~/jobs/store/state'; + +describe('Job Store Getters', () => { + let localState; + + beforeEach(() => { + localState = state(); + }); + + describe('headerActions', () => { + describe('with new issue path', () => { + it('returns an array with action to create a new issue', () => { + localState.job.new_issue_path = 'issues/new'; + + expect(getters.headerActions(localState)).toEqual([ + { + label: 'New issue', + path: localState.job.new_issue_path, + cssClass: + 'js-new-issue btn btn-success btn-inverted d-none d-md-block d-lg-block d-xl-block', + type: 'link', + }, + ]); + }); + }); + + describe('without new issue path', () => { + it('returns an empty array', () => { + expect(getters.headerActions(localState)).toEqual([]); + }); + }); + }); + + describe('headerTime', () => { + describe('when the job has started key', () => { + it('returns started key', () => { + const started = '2018-08-31T16:20:49.023Z'; + localState.job.started = started; + + expect(getters.headerTime(localState)).toEqual(started); + }); + }); + + describe('when the job does not have started key', () => { + it('returns created_at key', () => { + const created = '2018-08-31T16:20:49.023Z'; + localState.job.created_at = created; + expect(getters.headerTime(localState)).toEqual(created); + }); + }); + }); + + describe('shouldRenderCalloutMessage', () => { + describe('with status and callout message', () => { + it('returns true', () => { + localState.job.callout_message = 'Callout message'; + localState.job.status = { icon: 'passed' }; + + expect(getters.shouldRenderCalloutMessage(localState)).toEqual(true); + }); + }); + + describe('without status & with callout message', () => { + it('returns false', () => { + localState.job.callout_message = 'Callout message'; + expect(getters.shouldRenderCalloutMessage(localState)).toEqual(false); + }); + }); + + describe('with status & without callout message', () => { + it('returns false', () => { + localState.job.status = { icon: 'passed' }; + + expect(getters.shouldRenderCalloutMessage(localState)).toEqual(false); + }); + }); + }); + + describe('jobHasStarted', () => { + describe('when started equals false', () => { + it('returns false', () => { + localState.job.started = false; + expect(getters.jobHasStarted(localState)).toEqual(false); + }); + }); + + describe('when started equals string', () => { + it('returns true', () => { + localState.job.started = '2018-08-31T16:20:49.023Z'; + expect(getters.jobHasStarted(localState)).toEqual(true); + }); + }); + }); + + describe('hasEnvironment', () => { + describe('without `deployment_status`', () => { + it('returns false', () => { + expect(getters.hasEnvironment(localState)).toEqual(false); + }); + }); + describe('with an empty object for `deployment_status`', () => { + it('returns false', () => { + localState.job.deployment_status = {}; + expect(getters.hasEnvironment(localState)).toEqual(false); + }); + }); + describe('when `deployment_status` is defined and not empty', () => { + it('returns true', () => { + localState.job.deployment_status = { + status: 'creating', + environment: { + last_deployment: {}, + }, + }; + + expect(getters.hasEnvironment(localState)).toEqual(true); + }); + }); + }); +}); diff --git a/spec/views/projects/jobs/show.html.haml_spec.rb b/spec/views/projects/jobs/show.html.haml_spec.rb index 496646dc623..e06a9ecb98b 100644 --- a/spec/views/projects/jobs/show.html.haml_spec.rb +++ b/spec/views/projects/jobs/show.html.haml_spec.rb @@ -18,161 +18,6 @@ describe 'projects/jobs/show' do allow(view).to receive(:can?).and_return(true) end - describe 'environment info in job view' do - context 'job with latest deployment' do - let(:build) do - create(:ci_build, :success, :trace_artifact, environment: 'staging') - end - - before do - create(:environment, name: 'staging') - create(:deployment, deployable: build) - end - - it 'shows deployment message' do - expected_text = 'This job is the most recent deployment' - render - - expect(rendered).to have_css( - '.environment-information', text: expected_text) - end - end - - context 'job with outdated deployment' do - let(:build) do - create(:ci_build, :success, :trace_artifact, environment: 'staging', pipeline: pipeline) - end - - let(:second_build) do - create(:ci_build, :success, :trace_artifact, environment: 'staging', pipeline: pipeline) - end - - let(:environment) do - create(:environment, name: 'staging', project: project) - end - - let!(:first_deployment) do - create(:deployment, environment: environment, deployable: build) - end - - let!(:second_deployment) do - create(:deployment, environment: environment, deployable: second_build) - end - - it 'shows deployment message' do - expected_text = 'This job is an out-of-date deployment ' \ - "to staging.\nView the most recent deployment ##{second_deployment.iid}." - render - - expect(rendered).to have_css('.environment-information', text: expected_text) - end - end - - context 'job failed to deploy' do - let(:build) do - create(:ci_build, :failed, :trace_artifact, environment: 'staging', pipeline: pipeline) - end - - let!(:environment) do - create(:environment, name: 'staging', project: project) - end - - it 'shows deployment message' do - expected_text = 'The deployment of this job to staging did not succeed.' - render - - expect(rendered).to have_css( - '.environment-information', text: expected_text) - end - end - - context 'job will deploy' do - let(:build) do - create(:ci_build, :running, :trace_live, environment: 'staging', pipeline: pipeline) - end - - context 'when environment exists' do - let!(:environment) do - create(:environment, name: 'staging', project: project) - end - - it 'shows deployment message' do - expected_text = 'This job is creating a deployment to staging' - render - - expect(rendered).to have_css( - '.environment-information', text: expected_text) - end - - context 'when it has deployment' do - let!(:deployment) do - create(:deployment, environment: environment) - end - - it 'shows that deployment will be overwritten' do - expected_text = 'This job is creating a deployment to staging' - render - - expect(rendered).to have_css( - '.environment-information', text: expected_text) - expect(rendered).to have_css( - '.environment-information', text: 'latest deployment') - end - end - end - - context 'when environment does not exist' do - it 'shows deployment message' do - expected_text = 'This job is creating a deployment to staging' - render - - expect(rendered).to have_css( - '.environment-information', text: expected_text) - expect(rendered).not_to have_css( - '.environment-information', text: 'latest deployment') - end - end - end - - context 'job that failed to deploy and environment has not been created' do - let(:build) do - create(:ci_build, :failed, :trace_artifact, environment: 'staging', pipeline: pipeline) - end - - let!(:environment) do - create(:environment, name: 'staging', project: project) - end - - it 'shows deployment message' do - expected_text = 'The deployment of this job to staging did not succeed' - render - - expect(rendered).to have_css( - '.environment-information', text: expected_text) - end - end - - context 'job that will deploy and environment has not been created' do - let(:build) do - create(:ci_build, :running, :trace_live, environment: 'staging', pipeline: pipeline) - end - - let!(:environment) do - create(:environment, name: 'staging', project: project) - end - - it 'shows deployment message' do - expected_text = 'This job is creating a deployment to staging' - render - - expect(rendered).to have_css( - '.environment-information', text: expected_text) - expect(rendered).not_to have_css( - '.environment-information', text: 'latest deployment') - end - end - end - context 'when job is running' do let(:build) { create(:ci_build, :trace_live, :running, pipeline: pipeline) } -- cgit v1.2.1