From 9f46488805e86b1bc341ea1620b866016c2ce5ed Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Wed, 20 May 2020 14:34:42 +0000 Subject: Add latest changes from gitlab-org/gitlab@13-0-stable-ee --- .../jobs/components/artifacts_block_spec.js | 119 ++++++++++ spec/frontend/jobs/components/commit_block_spec.js | 89 +++++++ spec/frontend/jobs/components/empty_state_spec.js | 141 +++++++++++ .../jobs/components/environments_block_spec.js | 261 +++++++++++++++++++++ .../jobs/components/job_container_item_spec.js | 101 ++++++++ spec/frontend/jobs/components/job_log_spec.js | 65 +++++ .../jobs/components/jobs_container_spec.js | 131 +++++++++++ .../jobs/components/log/line_header_spec.js | 2 +- .../jobs/components/manual_variables_form_spec.js | 103 ++++++++ spec/frontend/jobs/components/sidebar_spec.js | 166 +++++++++++++ .../jobs/components/stages_dropdown_spec.js | 163 +++++++++++++ .../frontend/jobs/components/trigger_block_spec.js | 100 ++++++++ .../components/unmet_prerequisites_block_spec.js | 37 +++ 13 files changed, 1477 insertions(+), 1 deletion(-) create mode 100644 spec/frontend/jobs/components/artifacts_block_spec.js create mode 100644 spec/frontend/jobs/components/commit_block_spec.js create mode 100644 spec/frontend/jobs/components/empty_state_spec.js create mode 100644 spec/frontend/jobs/components/environments_block_spec.js create mode 100644 spec/frontend/jobs/components/job_container_item_spec.js create mode 100644 spec/frontend/jobs/components/job_log_spec.js create mode 100644 spec/frontend/jobs/components/jobs_container_spec.js create mode 100644 spec/frontend/jobs/components/manual_variables_form_spec.js create mode 100644 spec/frontend/jobs/components/sidebar_spec.js create mode 100644 spec/frontend/jobs/components/stages_dropdown_spec.js create mode 100644 spec/frontend/jobs/components/trigger_block_spec.js create mode 100644 spec/frontend/jobs/components/unmet_prerequisites_block_spec.js (limited to 'spec/frontend/jobs/components') diff --git a/spec/frontend/jobs/components/artifacts_block_spec.js b/spec/frontend/jobs/components/artifacts_block_spec.js new file mode 100644 index 00000000000..9cb56737f3e --- /dev/null +++ b/spec/frontend/jobs/components/artifacts_block_spec.js @@ -0,0 +1,119 @@ +import Vue from 'vue'; +import { getTimeago } from '~/lib/utils/datetime_utility'; +import component from '~/jobs/components/artifacts_block.vue'; +import mountComponent from '../../helpers/vue_mount_component_helper'; +import { trimText } from '../../helpers/text_helper'; + +describe('Artifacts block', () => { + const Component = Vue.extend(component); + let vm; + + const expireAt = '2018-08-14T09:38:49.157Z'; + const timeago = getTimeago(); + const formattedDate = timeago.format(expireAt); + + const expiredArtifact = { + expire_at: expireAt, + expired: true, + }; + + const nonExpiredArtifact = { + download_path: '/gitlab-org/gitlab-foss/-/jobs/98314558/artifacts/download', + browse_path: '/gitlab-org/gitlab-foss/-/jobs/98314558/artifacts/browse', + keep_path: '/gitlab-org/gitlab-foss/-/jobs/98314558/artifacts/keep', + expire_at: expireAt, + expired: false, + }; + + afterEach(() => { + vm.$destroy(); + }); + + describe('with expired artifacts', () => { + it('renders expired artifact date and info', () => { + vm = mountComponent(Component, { + artifact: expiredArtifact, + }); + + expect(vm.$el.querySelector('.js-artifacts-removed')).not.toBeNull(); + expect(vm.$el.querySelector('.js-artifacts-will-be-removed')).toBeNull(); + expect(trimText(vm.$el.querySelector('.js-artifacts-removed').textContent)).toEqual( + `The artifacts were removed ${formattedDate}`, + ); + }); + }); + + describe('with artifacts that will expire', () => { + it('renders will expire artifact date and info', () => { + vm = mountComponent(Component, { + artifact: nonExpiredArtifact, + }); + + expect(vm.$el.querySelector('.js-artifacts-removed')).toBeNull(); + expect(vm.$el.querySelector('.js-artifacts-will-be-removed')).not.toBeNull(); + expect(trimText(vm.$el.querySelector('.js-artifacts-will-be-removed').textContent)).toEqual( + `The artifacts will be removed ${formattedDate}`, + ); + }); + }); + + describe('with keep path', () => { + it('renders the keep button', () => { + vm = mountComponent(Component, { + artifact: nonExpiredArtifact, + }); + + expect(vm.$el.querySelector('.js-keep-artifacts')).not.toBeNull(); + }); + }); + + describe('without keep path', () => { + it('does not render the keep button', () => { + vm = mountComponent(Component, { + artifact: expiredArtifact, + }); + + expect(vm.$el.querySelector('.js-keep-artifacts')).toBeNull(); + }); + }); + + describe('with download path', () => { + it('renders the download button', () => { + vm = mountComponent(Component, { + artifact: nonExpiredArtifact, + }); + + expect(vm.$el.querySelector('.js-download-artifacts')).not.toBeNull(); + }); + }); + + describe('without download path', () => { + it('does not render the keep button', () => { + vm = mountComponent(Component, { + artifact: expiredArtifact, + }); + + expect(vm.$el.querySelector('.js-download-artifacts')).toBeNull(); + }); + }); + + describe('with browse path', () => { + it('does not render the browse button', () => { + vm = mountComponent(Component, { + artifact: nonExpiredArtifact, + }); + + expect(vm.$el.querySelector('.js-browse-artifacts')).not.toBeNull(); + }); + }); + + describe('without browse path', () => { + it('does not render the browse button', () => { + vm = mountComponent(Component, { + artifact: expiredArtifact, + }); + + expect(vm.$el.querySelector('.js-browse-artifacts')).toBeNull(); + }); + }); +}); diff --git a/spec/frontend/jobs/components/commit_block_spec.js b/spec/frontend/jobs/components/commit_block_spec.js new file mode 100644 index 00000000000..4e2d0053831 --- /dev/null +++ b/spec/frontend/jobs/components/commit_block_spec.js @@ -0,0 +1,89 @@ +import Vue from 'vue'; +import component from '~/jobs/components/commit_block.vue'; +import mountComponent from '../../helpers/vue_mount_component_helper'; + +describe('Commit block', () => { + const Component = Vue.extend(component); + let vm; + + const props = { + commit: { + short_id: '1f0fb84f', + id: '1f0fb84fb6770d74d97eee58118fd3909cd4f48c', + commit_path: 'commit/1f0fb84fb6770d74d97eee58118fd3909cd4f48c', + title: 'Update README.md', + }, + mergeRequest: { + iid: '!21244', + path: 'merge_requests/21244', + }, + isLastBlock: true, + }; + + afterEach(() => { + vm.$destroy(); + }); + + describe('pipeline short sha', () => { + beforeEach(() => { + vm = mountComponent(Component, { + ...props, + }); + }); + + it('renders pipeline short sha link', () => { + expect(vm.$el.querySelector('.js-commit-sha').getAttribute('href')).toEqual( + props.commit.commit_path, + ); + + expect(vm.$el.querySelector('.js-commit-sha').textContent.trim()).toEqual( + props.commit.short_id, + ); + }); + + it('renders clipboard button', () => { + expect(vm.$el.querySelector('button').getAttribute('data-clipboard-text')).toEqual( + props.commit.id, + ); + }); + }); + + describe('with merge request', () => { + it('renders merge request link and reference', () => { + vm = mountComponent(Component, { + ...props, + }); + + expect(vm.$el.querySelector('.js-link-commit').getAttribute('href')).toEqual( + props.mergeRequest.path, + ); + + expect(vm.$el.querySelector('.js-link-commit').textContent.trim()).toEqual( + `!${props.mergeRequest.iid}`, + ); + }); + }); + + describe('without merge request', () => { + it('does not render merge request', () => { + const copyProps = { ...props }; + delete copyProps.mergeRequest; + + vm = mountComponent(Component, { + ...copyProps, + }); + + expect(vm.$el.querySelector('.js-link-commit')).toBeNull(); + }); + }); + + describe('git commit title', () => { + it('renders git commit title', () => { + vm = mountComponent(Component, { + ...props, + }); + + expect(vm.$el.textContent).toContain(props.commit.title); + }); + }); +}); diff --git a/spec/frontend/jobs/components/empty_state_spec.js b/spec/frontend/jobs/components/empty_state_spec.js new file mode 100644 index 00000000000..c6eac4e27b3 --- /dev/null +++ b/spec/frontend/jobs/components/empty_state_spec.js @@ -0,0 +1,141 @@ +import Vue from 'vue'; +import component from '~/jobs/components/empty_state.vue'; +import mountComponent from '../../helpers/vue_mount_component_helper'; + +describe('Empty State', () => { + const Component = Vue.extend(component); + let vm; + + const props = { + illustrationPath: 'illustrations/pending_job_empty.svg', + illustrationSizeClass: 'svg-430', + title: 'This job has not started yet', + playable: false, + variablesSettingsUrl: '', + }; + + const content = 'This job is in pending state and is waiting to be picked by a runner'; + + afterEach(() => { + vm.$destroy(); + }); + + describe('renders image and title', () => { + beforeEach(() => { + vm = mountComponent(Component, { + ...props, + content, + }); + }); + + it('renders img with provided path and size', () => { + expect(vm.$el.querySelector('img').getAttribute('src')).toEqual(props.illustrationPath); + expect(vm.$el.querySelector('.svg-content').classList).toContain(props.illustrationSizeClass); + }); + + it('renders provided title', () => { + expect(vm.$el.querySelector('.js-job-empty-state-title').textContent.trim()).toEqual( + props.title, + ); + }); + }); + + describe('with content', () => { + it('renders content', () => { + vm = mountComponent(Component, { + ...props, + content, + }); + + expect(vm.$el.querySelector('.js-job-empty-state-content').textContent.trim()).toEqual( + content, + ); + }); + }); + + describe('without content', () => { + it('does not render content', () => { + vm = mountComponent(Component, { + ...props, + }); + + expect(vm.$el.querySelector('.js-job-empty-state-content')).toBeNull(); + }); + }); + + describe('with action', () => { + it('renders action', () => { + vm = mountComponent(Component, { + ...props, + content, + action: { + path: 'runner', + button_title: 'Check runner', + method: 'post', + }, + }); + + expect(vm.$el.querySelector('.js-job-empty-state-action').getAttribute('href')).toEqual( + 'runner', + ); + }); + }); + + describe('without action', () => { + it('does not render action', () => { + vm = mountComponent(Component, { + ...props, + content, + action: null, + }); + + expect(vm.$el.querySelector('.js-job-empty-state-action')).toBeNull(); + }); + }); + + describe('without playbale action', () => { + it('does not render manual variables form', () => { + vm = mountComponent(Component, { + ...props, + content, + }); + + expect(vm.$el.querySelector('.js-manual-vars-form')).toBeNull(); + }); + }); + + describe('with playbale action and not scheduled job', () => { + beforeEach(() => { + vm = mountComponent(Component, { + ...props, + content, + playable: true, + scheduled: false, + action: { + path: 'runner', + button_title: 'Check runner', + method: 'post', + }, + }); + }); + + it('renders manual variables form', () => { + expect(vm.$el.querySelector('.js-manual-vars-form')).not.toBeNull(); + }); + + it('does not render the empty state action', () => { + expect(vm.$el.querySelector('.js-job-empty-state-action')).toBeNull(); + }); + }); + + describe('with playbale action and scheduled job', () => { + it('does not render manual variables form', () => { + vm = mountComponent(Component, { + ...props, + content, + }); + + expect(vm.$el.querySelector('.js-manual-vars-form')).toBeNull(); + }); + }); +}); diff --git a/spec/frontend/jobs/components/environments_block_spec.js b/spec/frontend/jobs/components/environments_block_spec.js new file mode 100644 index 00000000000..4f2359e83b6 --- /dev/null +++ b/spec/frontend/jobs/components/environments_block_spec.js @@ -0,0 +1,261 @@ +import Vue from 'vue'; +import component from '~/jobs/components/environments_block.vue'; +import mountComponent from '../../helpers/vue_mount_component_helper'; + +const TEST_CLUSTER_NAME = 'test_cluster'; +const TEST_CLUSTER_PATH = 'path/to/test_cluster'; +const TEST_KUBERNETES_NAMESPACE = 'this-is-a-kubernetes-namespace'; + +describe('Environments block', () => { + const Component = Vue.extend(component); + let vm; + const status = { + group: 'success', + icon: 'status_success', + label: 'passed', + text: 'passed', + tooltip: 'passed', + }; + + const environment = { + environment_path: '/environment', + name: 'environment', + }; + + const lastDeployment = { iid: 'deployment', deployable: { build_path: 'bar' } }; + + const createEnvironmentWithLastDeployment = () => ({ + ...environment, + last_deployment: { ...lastDeployment }, + }); + + const createDeploymentWithCluster = () => ({ name: TEST_CLUSTER_NAME, path: TEST_CLUSTER_PATH }); + + const createDeploymentWithClusterAndKubernetesNamespace = () => ({ + name: TEST_CLUSTER_NAME, + path: TEST_CLUSTER_PATH, + kubernetes_namespace: TEST_KUBERNETES_NAMESPACE, + }); + + const createComponent = (deploymentStatus = {}, deploymentCluster = {}) => { + vm = mountComponent(Component, { + deploymentStatus, + deploymentCluster, + iconStatus: status, + }); + }; + + const findText = () => vm.$el.textContent.trim(); + const findJobDeploymentLink = () => vm.$el.querySelector('.js-job-deployment-link'); + const findEnvironmentLink = () => vm.$el.querySelector('.js-environment-link'); + const findClusterLink = () => vm.$el.querySelector('.js-job-cluster-link'); + + afterEach(() => { + vm.$destroy(); + }); + + describe('with last deployment', () => { + it('renders info for most recent deployment', () => { + createComponent({ + status: 'last', + environment, + }); + + expect(findText()).toEqual('This job is deployed to environment.'); + }); + + describe('when there is a cluster', () => { + it('renders info with cluster', () => { + createComponent( + { + status: 'last', + environment: createEnvironmentWithLastDeployment(), + }, + createDeploymentWithCluster(), + ); + + expect(findText()).toEqual( + `This job is deployed to environment using cluster ${TEST_CLUSTER_NAME}.`, + ); + }); + + describe('when there is a kubernetes namespace', () => { + it('renders info with cluster', () => { + createComponent( + { + status: 'last', + environment: createEnvironmentWithLastDeployment(), + }, + createDeploymentWithClusterAndKubernetesNamespace(), + ); + + expect(findText()).toEqual( + `This job is deployed to environment using cluster ${TEST_CLUSTER_NAME} and namespace ${TEST_KUBERNETES_NAMESPACE}.`, + ); + }); + }); + }); + }); + + describe('with out of date deployment', () => { + describe('with last deployment', () => { + it('renders info for out date and most recent', () => { + createComponent({ + status: 'out_of_date', + environment: createEnvironmentWithLastDeployment(), + }); + + expect(findText()).toEqual( + 'This job is an out-of-date deployment to environment. View the most recent deployment.', + ); + + expect(findJobDeploymentLink().getAttribute('href')).toEqual('bar'); + }); + + describe('when there is a cluster', () => { + it('renders info with cluster', () => { + createComponent( + { + status: 'out_of_date', + environment: createEnvironmentWithLastDeployment(), + }, + createDeploymentWithCluster(), + ); + + expect(findText()).toEqual( + `This job is an out-of-date deployment to environment using cluster ${TEST_CLUSTER_NAME}. View the most recent deployment.`, + ); + }); + + describe('when there is a kubernetes namespace', () => { + it('renders info with cluster', () => { + createComponent( + { + status: 'out_of_date', + environment: createEnvironmentWithLastDeployment(), + }, + createDeploymentWithClusterAndKubernetesNamespace(), + ); + + expect(findText()).toEqual( + `This job is an out-of-date deployment to environment using cluster ${TEST_CLUSTER_NAME} and namespace ${TEST_KUBERNETES_NAMESPACE}. View the most recent deployment.`, + ); + }); + }); + }); + }); + + describe('without last deployment', () => { + it('renders info about out of date deployment', () => { + createComponent({ + status: 'out_of_date', + environment, + }); + + expect(findText()).toEqual('This job is an out-of-date deployment to environment.'); + }); + }); + }); + + describe('with failed deployment', () => { + it('renders info about failed deployment', () => { + createComponent({ + status: 'failed', + environment, + }); + + expect(findText()).toEqual('The deployment of this job to environment did not succeed.'); + }); + }); + + describe('creating deployment', () => { + describe('with last deployment', () => { + it('renders info about creating deployment and overriding latest deployment', () => { + createComponent({ + status: 'creating', + environment: createEnvironmentWithLastDeployment(), + }); + + expect(findText()).toEqual( + 'This job is creating a deployment to environment. This will overwrite the latest deployment.', + ); + + expect(findJobDeploymentLink().getAttribute('href')).toEqual('bar'); + expect(findEnvironmentLink().getAttribute('href')).toEqual(environment.environment_path); + expect(findClusterLink()).toBeNull(); + }); + }); + + describe('without last deployment', () => { + it('renders info about deployment being created', () => { + createComponent({ + status: 'creating', + environment, + }); + + expect(findText()).toEqual('This job is creating a deployment to environment.'); + }); + + describe('when there is a cluster', () => { + it('inclues information about the cluster', () => { + createComponent( + { + status: 'creating', + environment, + }, + createDeploymentWithCluster(), + ); + + expect(findText()).toEqual( + `This job is creating a deployment to environment using cluster ${TEST_CLUSTER_NAME}.`, + ); + }); + }); + }); + + describe('without environment', () => { + it('does not render environment link', () => { + createComponent({ + status: 'creating', + environment: null, + }); + + expect(findEnvironmentLink()).toBeNull(); + }); + }); + }); + + describe('with a cluster', () => { + it('renders the cluster link', () => { + createComponent( + { + status: 'last', + environment: createEnvironmentWithLastDeployment(), + }, + createDeploymentWithCluster(), + ); + + expect(findText()).toEqual( + `This job is deployed to environment using cluster ${TEST_CLUSTER_NAME}.`, + ); + + expect(findClusterLink().getAttribute('href')).toEqual(TEST_CLUSTER_PATH); + }); + + describe('when the cluster is missing the path', () => { + it('renders the name without a link', () => { + createComponent( + { + status: 'last', + environment: createEnvironmentWithLastDeployment(), + }, + { name: 'the-cluster' }, + ); + + expect(findText()).toContain('using cluster the-cluster.'); + + expect(findClusterLink()).toBeNull(); + }); + }); + }); +}); diff --git a/spec/frontend/jobs/components/job_container_item_spec.js b/spec/frontend/jobs/components/job_container_item_spec.js new file mode 100644 index 00000000000..9019504d22d --- /dev/null +++ b/spec/frontend/jobs/components/job_container_item_spec.js @@ -0,0 +1,101 @@ +import Vue from 'vue'; +import mountComponent from 'helpers/vue_mount_component_helper'; +import JobContainerItem from '~/jobs/components/job_container_item.vue'; +import job from '../mock_data'; + +describe('JobContainerItem', () => { + const delayedJobFixture = getJSONFixture('jobs/delayed.json'); + const Component = Vue.extend(JobContainerItem); + let vm; + + afterEach(() => { + vm.$destroy(); + }); + + const sharedTests = () => { + it('displays a status icon', () => { + expect(vm.$el).toHaveSpriteIcon(job.status.icon); + }); + + it('displays the job name', () => { + expect(vm.$el.innerText).toContain(job.name); + }); + + it('displays a link to the job', () => { + const link = vm.$el.querySelector('.js-job-link'); + + expect(link.href).toBe(job.status.details_path); + }); + }; + + describe('when a job is not active and not retied', () => { + beforeEach(() => { + vm = mountComponent(Component, { + job, + isActive: false, + }); + }); + + sharedTests(); + }); + + describe('when a job is active', () => { + beforeEach(() => { + vm = mountComponent(Component, { + job, + isActive: true, + }); + }); + + sharedTests(); + + it('displays an arrow', () => { + expect(vm.$el).toHaveSpriteIcon('arrow-right'); + }); + }); + + describe('when a job is retried', () => { + beforeEach(() => { + vm = mountComponent(Component, { + job: { + ...job, + retried: true, + }, + isActive: false, + }); + }); + + sharedTests(); + + it('displays an icon', () => { + expect(vm.$el).toHaveSpriteIcon('retry'); + }); + }); + + describe('for delayed job', () => { + beforeEach(() => { + const remainingMilliseconds = 1337000; + jest + .spyOn(Date, 'now') + .mockImplementation( + () => new Date(delayedJobFixture.scheduled_at).getTime() - remainingMilliseconds, + ); + }); + + it('displays remaining time in tooltip', done => { + vm = mountComponent(Component, { + job: delayedJobFixture, + isActive: false, + }); + + Vue.nextTick() + .then(() => { + expect(vm.$el.querySelector('.js-job-link').getAttribute('data-original-title')).toEqual( + 'delayed job - delayed manual action (00:22:17)', + ); + }) + .then(done) + .catch(done.fail); + }); + }); +}); diff --git a/spec/frontend/jobs/components/job_log_spec.js b/spec/frontend/jobs/components/job_log_spec.js new file mode 100644 index 00000000000..2bb1e0af3a2 --- /dev/null +++ b/spec/frontend/jobs/components/job_log_spec.js @@ -0,0 +1,65 @@ +import Vue from 'vue'; +import { mountComponentWithStore } from 'helpers/vue_mount_component_helper'; +import component from '~/jobs/components/job_log.vue'; +import createStore from '~/jobs/store'; +import { resetStore } from '../store/helpers'; + +describe('Job Log', () => { + const Component = Vue.extend(component); + let store; + let vm; + + const trace = + 'Running with gitlab-runner 12.1.0 (de7731dd)
on docker-auto-scale-com d5ae8d25
Using Docker executor with image ruby:2.6 ...
'; + + beforeEach(() => { + store = createStore(); + }); + + afterEach(() => { + resetStore(store); + vm.$destroy(); + }); + + it('renders provided trace', () => { + vm = mountComponentWithStore(Component, { + props: { + trace, + isComplete: true, + }, + store, + }); + + expect(vm.$el.querySelector('code').textContent).toContain( + 'Running with gitlab-runner 12.1.0 (de7731dd)', + ); + }); + + describe('while receiving trace', () => { + it('renders animation', () => { + vm = mountComponentWithStore(Component, { + props: { + trace, + isComplete: false, + }, + store, + }); + + expect(vm.$el.querySelector('.js-log-animation')).not.toBeNull(); + }); + }); + + describe('when build trace has finishes', () => { + it('does not render animation', () => { + vm = mountComponentWithStore(Component, { + props: { + trace, + isComplete: true, + }, + store, + }); + + expect(vm.$el.querySelector('.js-log-animation')).toBeNull(); + }); + }); +}); diff --git a/spec/frontend/jobs/components/jobs_container_spec.js b/spec/frontend/jobs/components/jobs_container_spec.js new file mode 100644 index 00000000000..119b18b7557 --- /dev/null +++ b/spec/frontend/jobs/components/jobs_container_spec.js @@ -0,0 +1,131 @@ +import Vue from 'vue'; +import component from '~/jobs/components/jobs_container.vue'; +import mountComponent from '../../helpers/vue_mount_component_helper'; + +describe('Jobs List block', () => { + const Component = Vue.extend(component); + let vm; + + const retried = { + status: { + details_path: '/gitlab-org/gitlab-foss/pipelines/28029444', + group: 'success', + has_details: true, + icon: 'status_success', + label: 'passed', + text: 'passed', + tooltip: 'passed', + }, + id: 233432756, + tooltip: 'build - passed', + retried: true, + }; + + const active = { + name: 'test', + status: { + details_path: '/gitlab-org/gitlab-foss/pipelines/28029444', + group: 'success', + has_details: true, + icon: 'status_success', + label: 'passed', + text: 'passed', + tooltip: 'passed', + }, + id: 2322756, + tooltip: 'build - passed', + active: true, + }; + + const job = { + name: 'build', + status: { + details_path: '/gitlab-org/gitlab-foss/pipelines/28029444', + group: 'success', + has_details: true, + icon: 'status_success', + label: 'passed', + text: 'passed', + tooltip: 'passed', + }, + id: 232153, + tooltip: 'build - passed', + }; + + afterEach(() => { + vm.$destroy(); + }); + + it('renders list of jobs', () => { + vm = mountComponent(Component, { + jobs: [job, retried, active], + jobId: 12313, + }); + + expect(vm.$el.querySelectorAll('a').length).toEqual(3); + }); + + it('renders arrow right when job id matches `jobId`', () => { + vm = mountComponent(Component, { + jobs: [active], + jobId: active.id, + }); + + expect(vm.$el.querySelector('a .js-arrow-right')).not.toBeNull(); + }); + + it('does not render arrow right when job is not active', () => { + vm = mountComponent(Component, { + jobs: [job], + jobId: active.id, + }); + + expect(vm.$el.querySelector('a .js-arrow-right')).toBeNull(); + }); + + it('renders job name when present', () => { + vm = mountComponent(Component, { + jobs: [job], + jobId: active.id, + }); + + expect(vm.$el.querySelector('a').textContent.trim()).toContain(job.name); + expect(vm.$el.querySelector('a').textContent.trim()).not.toContain(job.id); + }); + + it('renders job id when job name is not available', () => { + vm = mountComponent(Component, { + jobs: [retried], + jobId: active.id, + }); + + expect(vm.$el.querySelector('a').textContent.trim()).toContain(retried.id); + }); + + it('links to the job page', () => { + vm = mountComponent(Component, { + jobs: [job], + jobId: active.id, + }); + + expect(vm.$el.querySelector('a').getAttribute('href')).toEqual(job.status.details_path); + }); + + it('renders retry icon when job was retried', () => { + vm = mountComponent(Component, { + jobs: [retried], + jobId: active.id, + }); + + expect(vm.$el.querySelector('.js-retry-icon')).not.toBeNull(); + }); + + it('does not render retry icon when job was not retried', () => { + vm = mountComponent(Component, { + jobs: [job], + jobId: active.id, + }); + + expect(vm.$el.querySelector('.js-retry-icon')).toBeNull(); + }); +}); diff --git a/spec/frontend/jobs/components/log/line_header_spec.js b/spec/frontend/jobs/components/log/line_header_spec.js index f2e202674ee..5ce69221dab 100644 --- a/spec/frontend/jobs/components/log/line_header_spec.js +++ b/spec/frontend/jobs/components/log/line_header_spec.js @@ -86,7 +86,7 @@ describe('Job Log Header Line', () => { describe('with duration', () => { beforeEach(() => { - createComponent(Object.assign({}, data, { duration: '00:10' })); + createComponent({ ...data, duration: '00:10' }); }); it('renders the duration badge', () => { diff --git a/spec/frontend/jobs/components/manual_variables_form_spec.js b/spec/frontend/jobs/components/manual_variables_form_spec.js new file mode 100644 index 00000000000..82fd73ef033 --- /dev/null +++ b/spec/frontend/jobs/components/manual_variables_form_spec.js @@ -0,0 +1,103 @@ +import { shallowMount, createLocalVue } from '@vue/test-utils'; +import { GlDeprecatedButton } from '@gitlab/ui'; +import Form from '~/jobs/components/manual_variables_form.vue'; + +const localVue = createLocalVue(); + +describe('Manual Variables Form', () => { + let wrapper; + + const requiredProps = { + action: { + path: '/play', + method: 'post', + button_title: 'Trigger this manual action', + }, + variablesSettingsUrl: '/settings', + }; + + const factory = (props = {}) => { + wrapper = shallowMount(localVue.extend(Form), { + propsData: props, + localVue, + }); + }; + + beforeEach(() => { + factory(requiredProps); + }); + + afterEach(done => { + // The component has a `nextTick` callback after some events so we need + // to wait for those to finish before destroying. + setImmediate(() => { + wrapper.destroy(); + wrapper = null; + + done(); + }); + }); + + it('renders empty form with correct placeholders', () => { + expect(wrapper.find({ ref: 'inputKey' }).attributes('placeholder')).toBe('Input variable key'); + expect(wrapper.find({ ref: 'inputSecretValue' }).attributes('placeholder')).toBe( + 'Input variable value', + ); + }); + + it('renders help text with provided link', () => { + expect(wrapper.find('p').text()).toBe( + 'Specify variable values to be used in this run. The values specified in CI/CD settings will be used as default', + ); + + expect(wrapper.find('a').attributes('href')).toBe(requiredProps.variablesSettingsUrl); + }); + + describe('when adding a new variable', () => { + it('creates a new variable when user types a new key and resets the form', done => { + wrapper.vm + .$nextTick() + .then(() => wrapper.find({ ref: 'inputKey' }).setValue('new key')) + .then(() => { + expect(wrapper.vm.variables.length).toBe(1); + expect(wrapper.vm.variables[0].key).toBe('new key'); + expect(wrapper.find({ ref: 'inputKey' }).attributes('value')).toBe(undefined); + }) + .then(done) + .catch(done.fail); + }); + + it('creates a new variable when user types a new value and resets the form', done => { + wrapper.vm + .$nextTick() + .then(() => wrapper.find({ ref: 'inputSecretValue' }).setValue('new value')) + .then(() => { + expect(wrapper.vm.variables.length).toBe(1); + expect(wrapper.vm.variables[0].secret_value).toBe('new value'); + expect(wrapper.find({ ref: 'inputSecretValue' }).attributes('value')).toBe(undefined); + }) + .then(done) + .catch(done.fail); + }); + }); + + describe('when deleting a variable', () => { + beforeEach(done => { + wrapper.vm.variables = [ + { + key: 'new key', + secret_value: 'value', + id: '1', + }, + ]; + + wrapper.vm.$nextTick(done); + }); + + it('removes the variable row', () => { + wrapper.find(GlDeprecatedButton).vm.$emit('click'); + + expect(wrapper.vm.variables.length).toBe(0); + }); + }); +}); diff --git a/spec/frontend/jobs/components/sidebar_spec.js b/spec/frontend/jobs/components/sidebar_spec.js new file mode 100644 index 00000000000..0c8e2dc3aef --- /dev/null +++ b/spec/frontend/jobs/components/sidebar_spec.js @@ -0,0 +1,166 @@ +import Vue from 'vue'; +import sidebarDetailsBlock from '~/jobs/components/sidebar.vue'; +import createStore from '~/jobs/store'; +import job, { jobsInStage } from '../mock_data'; +import { mountComponentWithStore } from '../../helpers/vue_mount_component_helper'; +import { trimText } from '../../helpers/text_helper'; + +describe('Sidebar details block', () => { + const SidebarComponent = Vue.extend(sidebarDetailsBlock); + let vm; + let store; + + beforeEach(() => { + store = createStore(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('when there is no retry path retry', () => { + it('should not render a retry button', () => { + const copy = { ...job }; + delete copy.retry_path; + + store.dispatch('receiveJobSuccess', copy); + vm = mountComponentWithStore(SidebarComponent, { + store, + }); + + expect(vm.$el.querySelector('.js-retry-button')).toBeNull(); + }); + }); + + describe('without terminal path', () => { + it('does not render terminal link', () => { + store.dispatch('receiveJobSuccess', job); + vm = mountComponentWithStore(SidebarComponent, { store }); + + expect(vm.$el.querySelector('.js-terminal-link')).toBeNull(); + }); + }); + + describe('with terminal path', () => { + it('renders terminal link', () => { + store.dispatch('receiveJobSuccess', { ...job, terminal_path: 'job/43123/terminal' }); + vm = mountComponentWithStore(SidebarComponent, { + store, + }); + + expect(vm.$el.querySelector('.js-terminal-link')).not.toBeNull(); + }); + }); + + beforeEach(() => { + store.dispatch('receiveJobSuccess', job); + vm = mountComponentWithStore(SidebarComponent, { store }); + }); + + describe('actions', () => { + it('should render link to new issue', () => { + expect(vm.$el.querySelector('.js-new-issue').getAttribute('href')).toEqual( + job.new_issue_path, + ); + + expect(vm.$el.querySelector('.js-new-issue').textContent.trim()).toEqual('New issue'); + }); + + it('should render link to retry job', () => { + expect(vm.$el.querySelector('.js-retry-button').getAttribute('href')).toEqual(job.retry_path); + }); + + it('should render link to cancel job', () => { + expect(vm.$el.querySelector('.js-cancel-job').getAttribute('href')).toEqual(job.cancel_path); + }); + }); + + describe('information', () => { + it('should render job duration', () => { + expect(trimText(vm.$el.querySelector('.js-job-duration').textContent)).toEqual( + 'Duration: 6 seconds', + ); + }); + + it('should render erased date', () => { + expect(trimText(vm.$el.querySelector('.js-job-erased').textContent)).toEqual( + 'Erased: 3 weeks ago', + ); + }); + + it('should render finished date', () => { + expect(trimText(vm.$el.querySelector('.js-job-finished').textContent)).toEqual( + 'Finished: 3 weeks ago', + ); + }); + + it('should render queued date', () => { + expect(trimText(vm.$el.querySelector('.js-job-queued').textContent)).toEqual( + 'Queued: 9 seconds', + ); + }); + + it('should render runner ID', () => { + expect(trimText(vm.$el.querySelector('.js-job-runner').textContent)).toEqual( + 'Runner: local ci runner (#1)', + ); + }); + + it('should render timeout information', () => { + expect(trimText(vm.$el.querySelector('.js-job-timeout').textContent)).toEqual( + 'Timeout: 1m 40s (from runner)', + ); + }); + + it('should render coverage', () => { + expect(trimText(vm.$el.querySelector('.js-job-coverage').textContent)).toEqual( + 'Coverage: 20%', + ); + }); + + it('should render tags', () => { + expect(trimText(vm.$el.querySelector('.js-job-tags').textContent)).toEqual('Tags: tag'); + }); + }); + + describe('stages dropdown', () => { + beforeEach(() => { + store.dispatch('receiveJobSuccess', job); + }); + + describe('with stages', () => { + beforeEach(() => { + vm = mountComponentWithStore(SidebarComponent, { store }); + }); + + it('renders value provided as selectedStage as selected', () => { + expect(vm.$el.querySelector('.js-selected-stage').textContent.trim()).toEqual( + vm.selectedStage, + ); + }); + }); + + describe('without jobs for stages', () => { + beforeEach(() => { + store.dispatch('receiveJobSuccess', job); + vm = mountComponentWithStore(SidebarComponent, { store }); + }); + + it('does not render job container', () => { + expect(vm.$el.querySelector('.js-jobs-container')).toBeNull(); + }); + }); + + describe('with jobs for stages', () => { + beforeEach(() => { + store.dispatch('receiveJobSuccess', job); + store.dispatch('receiveJobsForStageSuccess', jobsInStage.latest_statuses); + vm = mountComponentWithStore(SidebarComponent, { store }); + }); + + it('renders list of jobs', () => { + expect(vm.$el.querySelector('.js-jobs-container')).not.toBeNull(); + }); + }); + }); +}); diff --git a/spec/frontend/jobs/components/stages_dropdown_spec.js b/spec/frontend/jobs/components/stages_dropdown_spec.js new file mode 100644 index 00000000000..e8fa6094c25 --- /dev/null +++ b/spec/frontend/jobs/components/stages_dropdown_spec.js @@ -0,0 +1,163 @@ +import Vue from 'vue'; +import { trimText } from 'helpers/text_helper'; +import component from '~/jobs/components/stages_dropdown.vue'; +import mountComponent from '../../helpers/vue_mount_component_helper'; + +describe('Stages Dropdown', () => { + const Component = Vue.extend(component); + let vm; + + const mockPipelineData = { + id: 28029444, + details: { + status: { + details_path: '/gitlab-org/gitlab-foss/pipelines/28029444', + group: 'success', + has_details: true, + icon: 'status_success', + label: 'passed', + text: 'passed', + tooltip: 'passed', + }, + }, + path: 'pipeline/28029444', + flags: { + merge_request_pipeline: true, + detached_merge_request_pipeline: false, + }, + merge_request: { + iid: 1234, + path: '/root/detached-merge-request-pipelines/-/merge_requests/1', + title: 'Update README.md', + source_branch: 'feature-1234', + source_branch_path: '/root/detached-merge-request-pipelines/branches/feature-1234', + target_branch: 'master', + target_branch_path: '/root/detached-merge-request-pipelines/branches/master', + }, + ref: { + name: 'test-branch', + }, + }; + + describe('without a merge request pipeline', () => { + let pipeline; + + beforeEach(() => { + pipeline = JSON.parse(JSON.stringify(mockPipelineData)); + delete pipeline.merge_request; + delete pipeline.flags.merge_request_pipeline; + delete pipeline.flags.detached_merge_request_pipeline; + + vm = mountComponent(Component, { + pipeline, + stages: [{ name: 'build' }, { name: 'test' }], + selectedStage: 'deploy', + }); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('renders pipeline status', () => { + expect(vm.$el.querySelector('.js-ci-status-icon-success')).not.toBeNull(); + }); + + it('renders pipeline link', () => { + expect(vm.$el.querySelector('.js-pipeline-path').getAttribute('href')).toEqual( + 'pipeline/28029444', + ); + }); + + it('renders dropdown with stages', () => { + expect(vm.$el.querySelector('.dropdown .js-stage-item').textContent).toContain('build'); + }); + + it('rendes selected stage', () => { + expect(vm.$el.querySelector('.dropdown .js-selected-stage').textContent).toContain('deploy'); + }); + + it(`renders the pipeline info text like "Pipeline #123 for source_branch"`, () => { + const expected = `Pipeline #${pipeline.id} for ${pipeline.ref.name}`; + const actual = trimText(vm.$el.querySelector('.js-pipeline-info').innerText); + + expect(actual).toBe(expected); + }); + }); + + describe('with an "attached" merge request pipeline', () => { + let pipeline; + + beforeEach(() => { + pipeline = JSON.parse(JSON.stringify(mockPipelineData)); + pipeline.flags.merge_request_pipeline = true; + pipeline.flags.detached_merge_request_pipeline = false; + + vm = mountComponent(Component, { + pipeline, + stages: [], + selectedStage: 'deploy', + }); + }); + + it(`renders the pipeline info text like "Pipeline #123 for !456 with source_branch into target_branch"`, () => { + const expected = `Pipeline #${pipeline.id} for !${pipeline.merge_request.iid} with ${pipeline.merge_request.source_branch} into ${pipeline.merge_request.target_branch}`; + const actual = trimText(vm.$el.querySelector('.js-pipeline-info').innerText); + + expect(actual).toBe(expected); + }); + + it(`renders the correct merge request link`, () => { + const actual = vm.$el.querySelector('.js-mr-link').href; + + expect(actual).toContain(pipeline.merge_request.path); + }); + + it(`renders the correct source branch link`, () => { + const actual = vm.$el.querySelector('.js-source-branch-link').href; + + expect(actual).toContain(pipeline.merge_request.source_branch_path); + }); + + it(`renders the correct target branch link`, () => { + const actual = vm.$el.querySelector('.js-target-branch-link').href; + + expect(actual).toContain(pipeline.merge_request.target_branch_path); + }); + }); + + describe('with a detached merge request pipeline', () => { + let pipeline; + + beforeEach(() => { + pipeline = JSON.parse(JSON.stringify(mockPipelineData)); + pipeline.flags.merge_request_pipeline = false; + pipeline.flags.detached_merge_request_pipeline = true; + + vm = mountComponent(Component, { + pipeline, + stages: [], + selectedStage: 'deploy', + }); + }); + + it(`renders the pipeline info like "Pipeline #123 for !456 with source_branch"`, () => { + const expected = `Pipeline #${pipeline.id} for !${pipeline.merge_request.iid} with ${pipeline.merge_request.source_branch}`; + const actual = trimText(vm.$el.querySelector('.js-pipeline-info').innerText); + + expect(actual).toBe(expected); + }); + + it(`renders the correct merge request link`, () => { + const actual = vm.$el.querySelector('.js-mr-link').href; + + expect(actual).toContain(pipeline.merge_request.path); + }); + + it(`renders the correct source branch link`, () => { + const actual = vm.$el.querySelector('.js-source-branch-link').href; + + expect(actual).toContain(pipeline.merge_request.source_branch_path); + }); + }); +}); diff --git a/spec/frontend/jobs/components/trigger_block_spec.js b/spec/frontend/jobs/components/trigger_block_spec.js new file mode 100644 index 00000000000..448197b82c0 --- /dev/null +++ b/spec/frontend/jobs/components/trigger_block_spec.js @@ -0,0 +1,100 @@ +import Vue from 'vue'; +import component from '~/jobs/components/trigger_block.vue'; +import mountComponent from '../../helpers/vue_mount_component_helper'; + +describe('Trigger block', () => { + const Component = Vue.extend(component); + let vm; + + afterEach(() => { + vm.$destroy(); + }); + + describe('with short token', () => { + it('renders short token', () => { + vm = mountComponent(Component, { + trigger: { + short_token: '0a666b2', + }, + }); + + expect(vm.$el.querySelector('.js-short-token').textContent).toContain('0a666b2'); + }); + }); + + describe('without short token', () => { + it('does not render short token', () => { + vm = mountComponent(Component, { trigger: {} }); + + expect(vm.$el.querySelector('.js-short-token')).toBeNull(); + }); + }); + + describe('with variables', () => { + describe('hide/reveal variables', () => { + it('should toggle variables on click', done => { + vm = mountComponent(Component, { + trigger: { + short_token: 'bd7e', + variables: [ + { key: 'UPLOAD_TO_GCS', value: 'false', public: false }, + { key: 'UPLOAD_TO_S3', value: 'true', public: false }, + ], + }, + }); + + vm.$el.querySelector('.js-reveal-variables').click(); + + vm.$nextTick() + .then(() => { + expect(vm.$el.querySelector('.js-build-variables')).not.toBeNull(); + expect(vm.$el.querySelector('.js-reveal-variables').textContent.trim()).toEqual( + 'Hide values', + ); + + expect(vm.$el.querySelector('.js-build-variables').textContent).toContain( + 'UPLOAD_TO_GCS', + ); + + expect(vm.$el.querySelector('.js-build-variables').textContent).toContain('false'); + expect(vm.$el.querySelector('.js-build-variables').textContent).toContain( + 'UPLOAD_TO_S3', + ); + + expect(vm.$el.querySelector('.js-build-variables').textContent).toContain('true'); + + vm.$el.querySelector('.js-reveal-variables').click(); + }) + .then(vm.$nextTick) + .then(() => { + expect(vm.$el.querySelector('.js-reveal-variables').textContent.trim()).toEqual( + 'Reveal values', + ); + + expect(vm.$el.querySelector('.js-build-variables').textContent).toContain( + 'UPLOAD_TO_GCS', + ); + + expect(vm.$el.querySelector('.js-build-value').textContent).toContain('••••••'); + + expect(vm.$el.querySelector('.js-build-variables').textContent).toContain( + 'UPLOAD_TO_S3', + ); + + expect(vm.$el.querySelector('.js-build-value').textContent).toContain('••••••'); + }) + .then(done) + .catch(done.fail); + }); + }); + }); + + describe('without variables', () => { + it('does not render variables', () => { + vm = mountComponent(Component, { trigger: {} }); + + expect(vm.$el.querySelector('.js-reveal-variables')).toBeNull(); + expect(vm.$el.querySelector('.js-build-variables')).toBeNull(); + }); + }); +}); diff --git a/spec/frontend/jobs/components/unmet_prerequisites_block_spec.js b/spec/frontend/jobs/components/unmet_prerequisites_block_spec.js new file mode 100644 index 00000000000..68fcb321214 --- /dev/null +++ b/spec/frontend/jobs/components/unmet_prerequisites_block_spec.js @@ -0,0 +1,37 @@ +import Vue from 'vue'; +import component from '~/jobs/components/unmet_prerequisites_block.vue'; +import mountComponent from '../../helpers/vue_mount_component_helper'; + +describe('Unmet Prerequisites Block Job component', () => { + const Component = Vue.extend(component); + let vm; + const helpPath = '/user/project/clusters/index.html#troubleshooting-failed-deployment-jobs'; + + beforeEach(() => { + vm = mountComponent(Component, { + hasNoRunnersForProject: true, + helpPath, + }); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('renders an alert with the correct message', () => { + const container = vm.$el.querySelector('.js-failed-unmet-prerequisites'); + const alertMessage = + 'This job failed because the necessary resources were not successfully created.'; + + expect(container).not.toBeNull(); + expect(container.innerHTML).toContain(alertMessage); + }); + + it('renders link to help page', () => { + const helpLink = vm.$el.querySelector('.js-help-path'); + + expect(helpLink).not.toBeNull(); + expect(helpLink.innerHTML).toContain('More information'); + expect(helpLink.getAttribute('href')).toEqual(helpPath); + }); +}); -- cgit v1.2.3