diff options
Diffstat (limited to 'spec/frontend/jobs')
17 files changed, 2075 insertions, 2 deletions
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 = + '<span>Running with gitlab-runner 12.1.0 (de7731dd)<br/></span><span> on docker-auto-scale-com d5ae8d25<br/></span><div class="append-right-8" data-timestamp="1565502765" data-section="prepare-executor" role="button"></div><span class="section section-header js-s-prepare-executor">Using Docker executor with image ruby:2.6 ...<br/></span>'; + + 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); + }); +}); diff --git a/spec/frontend/jobs/mixins/delayed_job_mixin_spec.js b/spec/frontend/jobs/mixins/delayed_job_mixin_spec.js new file mode 100644 index 00000000000..2f7a6030650 --- /dev/null +++ b/spec/frontend/jobs/mixins/delayed_job_mixin_spec.js @@ -0,0 +1,79 @@ +import Vue from 'vue'; +import mountComponent from 'helpers/vue_mount_component_helper'; +import delayedJobMixin from '~/jobs/mixins/delayed_job_mixin'; + +describe('DelayedJobMixin', () => { + const delayedJobFixture = getJSONFixture('jobs/delayed.json'); + const dummyComponent = Vue.extend({ + mixins: [delayedJobMixin], + props: { + job: { + type: Object, + required: true, + }, + }, + render(createElement) { + return createElement('div', this.remainingTime); + }, + }); + + let vm; + + afterEach(() => { + vm.$destroy(); + jest.clearAllTimers(); + }); + + describe('if job is empty object', () => { + beforeEach(() => { + vm = mountComponent(dummyComponent, { + job: {}, + }); + }); + + it('sets remaining time to 00:00:00', () => { + expect(vm.$el.innerText).toBe('00:00:00'); + }); + + describe('after mounting', () => { + beforeEach(() => vm.$nextTick()); + + it('does not update remaining time', () => { + expect(vm.$el.innerText).toBe('00:00:00'); + }); + }); + }); + + describe('if job is delayed job', () => { + let remainingTimeInMilliseconds = 42000; + + beforeEach(() => { + jest + .spyOn(Date, 'now') + .mockImplementation( + () => new Date(delayedJobFixture.scheduled_at).getTime() - remainingTimeInMilliseconds, + ); + + vm = mountComponent(dummyComponent, { + job: delayedJobFixture, + }); + }); + + describe('after mounting', () => { + beforeEach(() => vm.$nextTick()); + + it('sets remaining time', () => { + expect(vm.$el.innerText).toBe('00:00:42'); + }); + + it('updates remaining time', () => { + remainingTimeInMilliseconds = 41000; + jest.advanceTimersByTime(1000); + + return vm.$nextTick().then(() => { + expect(vm.$el.innerText).toBe('00:00:41'); + }); + }); + }); + }); +}); diff --git a/spec/frontend/jobs/store/actions_spec.js b/spec/frontend/jobs/store/actions_spec.js new file mode 100644 index 00000000000..91bd5521f70 --- /dev/null +++ b/spec/frontend/jobs/store/actions_spec.js @@ -0,0 +1,512 @@ +import MockAdapter from 'axios-mock-adapter'; +import testAction from 'helpers/vuex_action_helper'; +import { TEST_HOST } from '../../helpers/test_constants'; +import axios from '~/lib/utils/axios_utils'; +import { + setJobEndpoint, + setTraceOptions, + clearEtagPoll, + stopPolling, + requestJob, + fetchJob, + receiveJobSuccess, + receiveJobError, + scrollTop, + scrollBottom, + requestTrace, + fetchTrace, + startPollingTrace, + stopPollingTrace, + receiveTraceSuccess, + receiveTraceError, + toggleCollapsibleLine, + requestJobsForStage, + fetchJobsForStage, + receiveJobsForStageSuccess, + receiveJobsForStageError, + hideSidebar, + showSidebar, + toggleSidebar, +} from '~/jobs/store/actions'; +import state from '~/jobs/store/state'; +import * as types from '~/jobs/store/mutation_types'; + +describe('Job State actions', () => { + let mockedState; + + beforeEach(() => { + mockedState = state(); + }); + + describe('setJobEndpoint', () => { + it('should commit SET_JOB_ENDPOINT mutation', done => { + testAction( + setJobEndpoint, + 'job/872324.json', + mockedState, + [{ type: types.SET_JOB_ENDPOINT, payload: 'job/872324.json' }], + [], + done, + ); + }); + }); + + describe('setTraceOptions', () => { + it('should commit SET_TRACE_OPTIONS mutation', done => { + testAction( + setTraceOptions, + { pagePath: 'job/872324/trace.json' }, + mockedState, + [{ type: types.SET_TRACE_OPTIONS, payload: { pagePath: 'job/872324/trace.json' } }], + [], + done, + ); + }); + }); + + describe('hideSidebar', () => { + it('should commit HIDE_SIDEBAR mutation', done => { + testAction(hideSidebar, null, mockedState, [{ type: types.HIDE_SIDEBAR }], [], done); + }); + }); + + describe('showSidebar', () => { + it('should commit HIDE_SIDEBAR mutation', done => { + testAction(showSidebar, null, mockedState, [{ 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); + }); + }); + + describe('fetchJob', () => { + let mock; + + beforeEach(() => { + mockedState.jobEndpoint = `${TEST_HOST}/endpoint.json`; + mock = new MockAdapter(axios); + }); + + afterEach(() => { + mock.restore(); + stopPolling(); + clearEtagPoll(); + }); + + describe('success', () => { + it('dispatches requestJob and receiveJobSuccess ', done => { + mock.onGet(`${TEST_HOST}/endpoint.json`).replyOnce(200, { id: 121212, name: 'karma' }); + + testAction( + fetchJob, + null, + mockedState, + [], + [ + { + type: 'requestJob', + }, + { + payload: { id: 121212, name: 'karma' }, + type: 'receiveJobSuccess', + }, + ], + done, + ); + }); + }); + + describe('error', () => { + beforeEach(() => { + mock.onGet(`${TEST_HOST}/endpoint.json`).reply(500); + }); + + it('dispatches requestJob and receiveJobError ', done => { + testAction( + fetchJob, + null, + mockedState, + [], + [ + { + type: 'requestJob', + }, + { + type: 'receiveJobError', + }, + ], + done, + ); + }); + }); + }); + + describe('receiveJobSuccess', () => { + it('should commit RECEIVE_JOB_SUCCESS mutation', done => { + testAction( + receiveJobSuccess, + { id: 121232132 }, + mockedState, + [{ type: types.RECEIVE_JOB_SUCCESS, payload: { id: 121232132 } }], + [], + done, + ); + }); + }); + + describe('receiveJobError', () => { + it('should commit RECEIVE_JOB_ERROR mutation', done => { + testAction(receiveJobError, null, mockedState, [{ type: types.RECEIVE_JOB_ERROR }], [], done); + }); + }); + + describe('scrollTop', () => { + it('should dispatch toggleScrollButtons action', done => { + testAction(scrollTop, null, mockedState, [], [{ type: 'toggleScrollButtons' }], done); + }); + }); + + describe('scrollBottom', () => { + it('should dispatch toggleScrollButtons action', done => { + testAction(scrollBottom, null, mockedState, [], [{ type: 'toggleScrollButtons' }], done); + }); + }); + + describe('requestTrace', () => { + it('should commit REQUEST_TRACE mutation', done => { + testAction(requestTrace, null, mockedState, [{ type: types.REQUEST_TRACE }], [], done); + }); + }); + + describe('fetchTrace', () => { + let mock; + + beforeEach(() => { + mockedState.traceEndpoint = `${TEST_HOST}/endpoint`; + mock = new MockAdapter(axios); + }); + + afterEach(() => { + mock.restore(); + stopPolling(); + clearEtagPoll(); + }); + + describe('success', () => { + 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, + }); + + testAction( + fetchTrace, + null, + mockedState, + [], + [ + { + type: 'toggleScrollisInBottom', + payload: true, + }, + { + payload: { + html: 'I, [2018-08-17T22:57:45.707325 #1841] INFO -- :', + complete: true, + }, + type: 'receiveTraceSuccess', + }, + { + type: 'stopPollingTrace', + }, + ], + done, + ); + }); + + describe('when job is incomplete', () => { + let tracePayload; + + beforeEach(() => { + tracePayload = { + html: 'I, [2018-08-17T22:57:45.707325 #1841] INFO -- :', + complete: false, + }; + + mock.onGet(`${TEST_HOST}/endpoint/trace.json`).replyOnce(200, tracePayload); + }); + + it('dispatches startPollingTrace', done => { + testAction( + fetchTrace, + null, + mockedState, + [], + [ + { type: 'toggleScrollisInBottom', payload: true }, + { type: 'receiveTraceSuccess', payload: tracePayload }, + { type: 'startPollingTrace' }, + ], + done, + ); + }); + + it('does not dispatch startPollingTrace when timeout is non-empty', done => { + mockedState.traceTimeout = 1; + + testAction( + fetchTrace, + null, + mockedState, + [], + [ + { type: 'toggleScrollisInBottom', payload: true }, + { type: 'receiveTraceSuccess', payload: tracePayload }, + ], + done, + ); + }); + }); + }); + + describe('error', () => { + beforeEach(() => { + mock.onGet(`${TEST_HOST}/endpoint/trace.json`).reply(500); + }); + + it('dispatches requestTrace and receiveTraceError ', done => { + testAction( + fetchTrace, + null, + mockedState, + [], + [ + { + type: 'receiveTraceError', + }, + ], + done, + ); + }); + }); + }); + + describe('startPollingTrace', () => { + let dispatch; + let commit; + + beforeEach(() => { + dispatch = jest.fn(); + commit = jest.fn(); + + startPollingTrace({ dispatch, commit }); + }); + + afterEach(() => { + jest.clearAllTimers(); + }); + + it('should save the timeout id but not call fetchTrace', () => { + expect(commit).toHaveBeenCalledWith(types.SET_TRACE_TIMEOUT, expect.any(Number)); + expect(commit.mock.calls[0][1]).toBeGreaterThan(0); + + expect(dispatch).not.toHaveBeenCalledWith('fetchTrace'); + }); + + describe('after timeout has passed', () => { + beforeEach(() => { + jest.advanceTimersByTime(4000); + }); + + it('should clear the timeout id and fetchTrace', () => { + expect(commit).toHaveBeenCalledWith(types.SET_TRACE_TIMEOUT, 0); + expect(dispatch).toHaveBeenCalledWith('fetchTrace'); + }); + }); + }); + + describe('stopPollingTrace', () => { + let origTimeout; + + beforeEach(() => { + // Can't use spyOn(window, 'clearTimeout') because this caused unrelated specs to timeout + // https://gitlab.com/gitlab-org/gitlab/-/merge_requests/23838#note_280277727 + origTimeout = window.clearTimeout; + window.clearTimeout = jest.fn(); + }); + + afterEach(() => { + window.clearTimeout = origTimeout; + }); + + it('should commit STOP_POLLING_TRACE mutation ', done => { + const traceTimeout = 7; + + testAction( + stopPollingTrace, + null, + { ...mockedState, traceTimeout }, + [{ type: types.SET_TRACE_TIMEOUT, payload: 0 }, { type: types.STOP_POLLING_TRACE }], + [], + ) + .then(() => { + expect(window.clearTimeout).toHaveBeenCalledWith(traceTimeout); + }) + .then(done) + .catch(done.fail); + }); + }); + + describe('receiveTraceSuccess', () => { + it('should commit RECEIVE_TRACE_SUCCESS mutation ', done => { + testAction( + receiveTraceSuccess, + 'hello world', + mockedState, + [{ type: types.RECEIVE_TRACE_SUCCESS, payload: 'hello world' }], + [], + done, + ); + }); + }); + + describe('receiveTraceError', () => { + it('should commit stop polling trace', done => { + testAction(receiveTraceError, null, mockedState, [], [{ type: 'stopPollingTrace' }], done); + }); + }); + + describe('toggleCollapsibleLine', () => { + it('should commit TOGGLE_COLLAPSIBLE_LINE mutation ', done => { + testAction( + toggleCollapsibleLine, + { isClosed: true }, + mockedState, + [{ type: types.TOGGLE_COLLAPSIBLE_LINE, payload: { isClosed: true } }], + [], + done, + ); + }); + }); + + describe('requestJobsForStage', () => { + it('should commit REQUEST_JOBS_FOR_STAGE mutation ', done => { + testAction( + requestJobsForStage, + { name: 'deploy' }, + mockedState, + [{ type: types.REQUEST_JOBS_FOR_STAGE, payload: { name: 'deploy' } }], + [], + done, + ); + }); + }); + + describe('fetchJobsForStage', () => { + let mock; + + beforeEach(() => { + mock = new MockAdapter(axios); + }); + + afterEach(() => { + mock.restore(); + }); + + describe('success', () => { + it('dispatches requestJobsForStage and receiveJobsForStageSuccess ', done => { + mock + .onGet(`${TEST_HOST}/jobs.json`) + .replyOnce(200, { latest_statuses: [{ id: 121212, name: 'build' }], retried: [] }); + + testAction( + fetchJobsForStage, + { dropdown_path: `${TEST_HOST}/jobs.json` }, + mockedState, + [], + [ + { + type: 'requestJobsForStage', + payload: { dropdown_path: `${TEST_HOST}/jobs.json` }, + }, + { + payload: [{ id: 121212, name: 'build' }], + type: 'receiveJobsForStageSuccess', + }, + ], + done, + ); + }); + }); + + describe('error', () => { + beforeEach(() => { + mock.onGet(`${TEST_HOST}/jobs.json`).reply(500); + }); + + it('dispatches requestJobsForStage and receiveJobsForStageError', done => { + testAction( + fetchJobsForStage, + { dropdown_path: `${TEST_HOST}/jobs.json` }, + mockedState, + [], + [ + { + type: 'requestJobsForStage', + payload: { dropdown_path: `${TEST_HOST}/jobs.json` }, + }, + { + type: 'receiveJobsForStageError', + }, + ], + done, + ); + }); + }); + }); + + describe('receiveJobsForStageSuccess', () => { + it('should commit RECEIVE_JOBS_FOR_STAGE_SUCCESS mutation ', done => { + testAction( + receiveJobsForStageSuccess, + [{ id: 121212, name: 'karma' }], + mockedState, + [{ type: types.RECEIVE_JOBS_FOR_STAGE_SUCCESS, payload: [{ id: 121212, name: 'karma' }] }], + [], + done, + ); + }); + }); + + describe('receiveJobsForStageError', () => { + it('should commit RECEIVE_JOBS_FOR_STAGE_ERROR mutation ', done => { + testAction( + receiveJobsForStageError, + null, + mockedState, + [{ type: types.RECEIVE_JOBS_FOR_STAGE_ERROR }], + [], + done, + ); + }); + }); +}); diff --git a/spec/frontend/jobs/store/helpers.js b/spec/frontend/jobs/store/helpers.js new file mode 100644 index 00000000000..81a769b4a6e --- /dev/null +++ b/spec/frontend/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/frontend/jobs/store/mutations_spec.js b/spec/frontend/jobs/store/mutations_spec.js index d77690ffac0..3557d3b94b6 100644 --- a/spec/frontend/jobs/store/mutations_spec.js +++ b/spec/frontend/jobs/store/mutations_spec.js @@ -59,7 +59,7 @@ describe('Jobs Store Mutations', () => { describe('when traceSize is bigger than the total size', () => { it('sets isTraceSizeVisible to false', () => { - const copy = Object.assign({}, stateCopy, { traceSize: 5118460, size: 2321312 }); + const copy = { ...stateCopy, traceSize: 5118460, size: 2321312 }; mutations[types.RECEIVE_TRACE_SUCCESS](copy, { total: 511846 }); |