diff options
Diffstat (limited to 'spec/frontend/ci/job_details/store')
-rw-r--r-- | spec/frontend/ci/job_details/store/actions_spec.js | 502 | ||||
-rw-r--r-- | spec/frontend/ci/job_details/store/getters_spec.js | 245 | ||||
-rw-r--r-- | spec/frontend/ci/job_details/store/helpers.js | 5 | ||||
-rw-r--r-- | spec/frontend/ci/job_details/store/mutations_spec.js | 269 | ||||
-rw-r--r-- | spec/frontend/ci/job_details/store/utils_spec.js | 510 |
5 files changed, 1531 insertions, 0 deletions
diff --git a/spec/frontend/ci/job_details/store/actions_spec.js b/spec/frontend/ci/job_details/store/actions_spec.js new file mode 100644 index 00000000000..bb5c1fe32bd --- /dev/null +++ b/spec/frontend/ci/job_details/store/actions_spec.js @@ -0,0 +1,502 @@ +import MockAdapter from 'axios-mock-adapter'; +import { TEST_HOST } from 'helpers/test_constants'; +import testAction from 'helpers/vuex_action_helper'; +import { + setJobEndpoint, + setJobLogOptions, + clearEtagPoll, + stopPolling, + requestJob, + fetchJob, + receiveJobSuccess, + receiveJobError, + scrollTop, + scrollBottom, + requestJobLog, + fetchJobLog, + startPollingJobLog, + stopPollingJobLog, + receiveJobLogSuccess, + receiveJobLogError, + toggleCollapsibleLine, + requestJobsForStage, + fetchJobsForStage, + receiveJobsForStageSuccess, + receiveJobsForStageError, + hideSidebar, + showSidebar, + toggleSidebar, +} from '~/ci/job_details/store/actions'; +import * as types from '~/ci/job_details/store/mutation_types'; +import state from '~/ci/job_details/store/state'; +import axios from '~/lib/utils/axios_utils'; +import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status'; + +describe('Job State actions', () => { + let mockedState; + + beforeEach(() => { + mockedState = state(); + }); + + describe('setJobEndpoint', () => { + it('should commit SET_JOB_ENDPOINT mutation', () => { + return testAction( + setJobEndpoint, + 'job/872324.json', + mockedState, + [{ type: types.SET_JOB_ENDPOINT, payload: 'job/872324.json' }], + [], + ); + }); + }); + + describe('setJobLogOptions', () => { + it('should commit SET_JOB_LOG_OPTIONS mutation', () => { + return testAction( + setJobLogOptions, + { pagePath: 'job/872324/trace.json' }, + mockedState, + [{ type: types.SET_JOB_LOG_OPTIONS, payload: { pagePath: 'job/872324/trace.json' } }], + [], + ); + }); + }); + + describe('hideSidebar', () => { + it('should commit HIDE_SIDEBAR mutation', () => { + return testAction(hideSidebar, null, mockedState, [{ type: types.HIDE_SIDEBAR }], []); + }); + }); + + describe('showSidebar', () => { + it('should commit SHOW_SIDEBAR mutation', () => { + return testAction(showSidebar, null, mockedState, [{ type: types.SHOW_SIDEBAR }], []); + }); + }); + + describe('toggleSidebar', () => { + describe('when isSidebarOpen is true', () => { + it('should dispatch hideSidebar', () => { + return testAction(toggleSidebar, null, mockedState, [], [{ type: 'hideSidebar' }]); + }); + }); + + describe('when isSidebarOpen is false', () => { + it('should dispatch showSidebar', () => { + mockedState.isSidebarOpen = false; + + return testAction(toggleSidebar, null, mockedState, [], [{ type: 'showSidebar' }]); + }); + }); + }); + + describe('requestJob', () => { + it('should commit REQUEST_JOB mutation', () => { + return testAction(requestJob, null, mockedState, [{ type: types.REQUEST_JOB }], []); + }); + }); + + 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', () => { + mock + .onGet(`${TEST_HOST}/endpoint.json`) + .replyOnce(HTTP_STATUS_OK, { id: 121212, name: 'karma' }); + + return testAction( + fetchJob, + null, + mockedState, + [], + [ + { + type: 'requestJob', + }, + { + payload: { id: 121212, name: 'karma' }, + type: 'receiveJobSuccess', + }, + ], + ); + }); + }); + + describe('error', () => { + beforeEach(() => { + mock.onGet(`${TEST_HOST}/endpoint.json`).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR); + }); + + it('dispatches requestJob and receiveJobError', () => { + return testAction( + fetchJob, + null, + mockedState, + [], + [ + { + type: 'requestJob', + }, + { + type: 'receiveJobError', + }, + ], + ); + }); + }); + }); + + describe('receiveJobSuccess', () => { + it('should commit RECEIVE_JOB_SUCCESS mutation', () => { + return testAction( + receiveJobSuccess, + { id: 121232132 }, + mockedState, + [{ type: types.RECEIVE_JOB_SUCCESS, payload: { id: 121232132 } }], + [], + ); + }); + }); + + describe('receiveJobError', () => { + it('should commit RECEIVE_JOB_ERROR mutation', () => { + return testAction( + receiveJobError, + null, + mockedState, + [{ type: types.RECEIVE_JOB_ERROR }], + [], + ); + }); + }); + + describe('scrollTop', () => { + it('should dispatch toggleScrollButtons action', () => { + return testAction(scrollTop, null, mockedState, [], [{ type: 'toggleScrollButtons' }]); + }); + }); + + describe('scrollBottom', () => { + it('should dispatch toggleScrollButtons action', () => { + return testAction(scrollBottom, null, mockedState, [], [{ type: 'toggleScrollButtons' }]); + }); + }); + + describe('requestJobLog', () => { + it('should commit REQUEST_JOB_LOG mutation', () => { + return testAction(requestJobLog, null, mockedState, [{ type: types.REQUEST_JOB_LOG }], []); + }); + }); + + describe('fetchJobLog', () => { + let mock; + + beforeEach(() => { + mockedState.jobLogEndpoint = `${TEST_HOST}/endpoint`; + mock = new MockAdapter(axios); + }); + + afterEach(() => { + mock.restore(); + stopPolling(); + clearEtagPoll(); + }); + + describe('success', () => { + it('dispatches requestJobLog, receiveJobLogSuccess and stopPollingJobLog when job is complete', () => { + mock.onGet(`${TEST_HOST}/endpoint/trace.json`).replyOnce(HTTP_STATUS_OK, { + html: 'I, [2018-08-17T22:57:45.707325 #1841] INFO -- :', + complete: true, + }); + + return testAction( + fetchJobLog, + null, + mockedState, + [], + [ + { + type: 'toggleScrollisInBottom', + payload: true, + }, + { + payload: { + html: 'I, [2018-08-17T22:57:45.707325 #1841] INFO -- :', + complete: true, + }, + type: 'receiveJobLogSuccess', + }, + { + type: 'stopPollingJobLog', + }, + ], + ); + }); + + describe('when job is incomplete', () => { + let jobLogPayload; + + beforeEach(() => { + jobLogPayload = { + html: 'I, [2018-08-17T22:57:45.707325 #1841] INFO -- :', + complete: false, + }; + + mock.onGet(`${TEST_HOST}/endpoint/trace.json`).replyOnce(HTTP_STATUS_OK, jobLogPayload); + }); + + it('dispatches startPollingJobLog', () => { + return testAction( + fetchJobLog, + null, + mockedState, + [], + [ + { type: 'toggleScrollisInBottom', payload: true }, + { type: 'receiveJobLogSuccess', payload: jobLogPayload }, + { type: 'startPollingJobLog' }, + ], + ); + }); + + it('does not dispatch startPollingJobLog when timeout is non-empty', () => { + mockedState.jobLogTimeout = 1; + + return testAction( + fetchJobLog, + null, + mockedState, + [], + [ + { type: 'toggleScrollisInBottom', payload: true }, + { type: 'receiveJobLogSuccess', payload: jobLogPayload }, + ], + ); + }); + }); + }); + + describe('error', () => { + beforeEach(() => { + mock.onGet(`${TEST_HOST}/endpoint/trace.json`).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR); + }); + + it('dispatches requestJobLog and receiveJobLogError', () => { + return testAction( + fetchJobLog, + null, + mockedState, + [], + [ + { + type: 'receiveJobLogError', + }, + ], + ); + }); + }); + }); + + describe('startPollingJobLog', () => { + let dispatch; + let commit; + + beforeEach(() => { + dispatch = jest.fn(); + commit = jest.fn(); + + startPollingJobLog({ dispatch, commit }); + }); + + afterEach(() => { + jest.clearAllTimers(); + }); + + it('should save the timeout id but not call fetchJobLog', () => { + expect(commit).toHaveBeenCalledWith(types.SET_JOB_LOG_TIMEOUT, expect.any(Number)); + expect(commit.mock.calls[0][1]).toBeGreaterThan(0); + + expect(dispatch).not.toHaveBeenCalledWith('fetchJobLog'); + }); + + describe('after timeout has passed', () => { + beforeEach(() => { + jest.advanceTimersByTime(4000); + }); + + it('should clear the timeout id and fetchJobLog', () => { + expect(commit).toHaveBeenCalledWith(types.SET_JOB_LOG_TIMEOUT, 0); + expect(dispatch).toHaveBeenCalledWith('fetchJobLog'); + }); + }); + }); + + describe('stopPollingJobLog', () => { + 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_JOB_LOG mutation', async () => { + const jobLogTimeout = 7; + + await testAction( + stopPollingJobLog, + null, + { ...mockedState, jobLogTimeout }, + [{ type: types.SET_JOB_LOG_TIMEOUT, payload: 0 }, { type: types.STOP_POLLING_JOB_LOG }], + [], + ); + expect(window.clearTimeout).toHaveBeenCalledWith(jobLogTimeout); + }); + }); + + describe('receiveJobLogSuccess', () => { + it('should commit RECEIVE_JOB_LOG_SUCCESS mutation', () => { + return testAction( + receiveJobLogSuccess, + 'hello world', + mockedState, + [{ type: types.RECEIVE_JOB_LOG_SUCCESS, payload: 'hello world' }], + [], + ); + }); + }); + + describe('receiveJobLogError', () => { + it('should commit stop polling job log', () => { + return testAction(receiveJobLogError, null, mockedState, [], [{ type: 'stopPollingJobLog' }]); + }); + }); + + describe('toggleCollapsibleLine', () => { + it('should commit TOGGLE_COLLAPSIBLE_LINE mutation', () => { + return testAction( + toggleCollapsibleLine, + { isClosed: true }, + mockedState, + [{ type: types.TOGGLE_COLLAPSIBLE_LINE, payload: { isClosed: true } }], + [], + ); + }); + }); + + describe('requestJobsForStage', () => { + it('should commit REQUEST_JOBS_FOR_STAGE mutation', () => { + return testAction( + requestJobsForStage, + { name: 'deploy' }, + mockedState, + [{ type: types.REQUEST_JOBS_FOR_STAGE, payload: { name: 'deploy' } }], + [], + ); + }); + }); + + describe('fetchJobsForStage', () => { + let mock; + + beforeEach(() => { + mock = new MockAdapter(axios); + }); + + afterEach(() => { + mock.restore(); + }); + + describe('success', () => { + it('dispatches requestJobsForStage and receiveJobsForStageSuccess', () => { + mock.onGet(`${TEST_HOST}/jobs.json`).replyOnce(HTTP_STATUS_OK, { + latest_statuses: [{ id: 121212, name: 'build' }], + retried: [], + }); + + return 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', + }, + ], + ); + }); + }); + + describe('error', () => { + beforeEach(() => { + mock.onGet(`${TEST_HOST}/jobs.json`).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR); + }); + + it('dispatches requestJobsForStage and receiveJobsForStageError', () => { + return testAction( + fetchJobsForStage, + { dropdown_path: `${TEST_HOST}/jobs.json` }, + mockedState, + [], + [ + { + type: 'requestJobsForStage', + payload: { dropdown_path: `${TEST_HOST}/jobs.json` }, + }, + { + type: 'receiveJobsForStageError', + }, + ], + ); + }); + }); + }); + + describe('receiveJobsForStageSuccess', () => { + it('should commit RECEIVE_JOBS_FOR_STAGE_SUCCESS mutation', () => { + return testAction( + receiveJobsForStageSuccess, + [{ id: 121212, name: 'karma' }], + mockedState, + [{ type: types.RECEIVE_JOBS_FOR_STAGE_SUCCESS, payload: [{ id: 121212, name: 'karma' }] }], + [], + ); + }); + }); + + describe('receiveJobsForStageError', () => { + it('should commit RECEIVE_JOBS_FOR_STAGE_ERROR mutation', () => { + return testAction( + receiveJobsForStageError, + null, + mockedState, + [{ type: types.RECEIVE_JOBS_FOR_STAGE_ERROR }], + [], + ); + }); + }); +}); diff --git a/spec/frontend/ci/job_details/store/getters_spec.js b/spec/frontend/ci/job_details/store/getters_spec.js new file mode 100644 index 00000000000..dfa5f9d4781 --- /dev/null +++ b/spec/frontend/ci/job_details/store/getters_spec.js @@ -0,0 +1,245 @@ +import * as getters from '~/ci/job_details/store/getters'; +import state from '~/ci/job_details/store/state'; + +describe('Job Store Getters', () => { + let localState; + + beforeEach(() => { + localState = state(); + }); + + describe('headerTime', () => { + describe('when the job has started key', () => { + it('returns started_at value', () => { + const started = '2018-08-31T16:20:49.023Z'; + const startedAt = '2018-08-31T16:20:49.023Z'; + localState.job.started_at = startedAt; + localState.job.started = started; + + expect(getters.headerTime(localState)).toEqual(startedAt); + }); + }); + + describe('when the job does not have started key', () => { + it('returns created_at value', () => { + const created = '2018-08-31T16:20:49.023Z'; + localState.job.created_at = created; + + expect(getters.headerTime(localState)).toEqual(created); + }); + }); + }); + + describe('shouldRenderCalloutMessage', () => { + describe('with status and callout message', () => { + it('returns true', () => { + localState.job.callout_message = 'Callout message'; + localState.job.status = { icon: 'passed' }; + + expect(getters.shouldRenderCalloutMessage(localState)).toEqual(true); + }); + }); + + describe('without status & with callout message', () => { + it('returns false', () => { + localState.job.callout_message = 'Callout message'; + + expect(getters.shouldRenderCalloutMessage(localState)).toEqual(false); + }); + }); + + describe('with status & without callout message', () => { + it('returns false', () => { + localState.job.status = { icon: 'passed' }; + + expect(getters.shouldRenderCalloutMessage(localState)).toEqual(false); + }); + }); + }); + + describe('shouldRenderTriggeredLabel', () => { + describe('when started equals null', () => { + it('returns false', () => { + localState.job.started_at = null; + + expect(getters.shouldRenderTriggeredLabel(localState)).toEqual(false); + }); + }); + + describe('when started equals string', () => { + it('returns true', () => { + localState.job.started_at = '2018-08-31T16:20:49.023Z'; + + expect(getters.shouldRenderTriggeredLabel(localState)).toEqual(true); + }); + }); + }); + + describe('hasEnvironment', () => { + describe('without `deployment_status`', () => { + it('returns false', () => { + expect(getters.hasEnvironment(localState)).toEqual(false); + }); + }); + + describe('with an empty object for `deployment_status`', () => { + it('returns false', () => { + localState.job.deployment_status = {}; + + expect(getters.hasEnvironment(localState)).toEqual(false); + }); + }); + + describe('when `deployment_status` is defined and not empty', () => { + it('returns true', () => { + localState.job.deployment_status = { + status: 'creating', + environment: { + last_deployment: {}, + }, + }; + + expect(getters.hasEnvironment(localState)).toEqual(true); + }); + }); + }); + + describe('hasJobLog', () => { + describe('when has_trace is true', () => { + it('returns true', () => { + localState.job.has_trace = true; + localState.job.status = {}; + + expect(getters.hasJobLog(localState)).toEqual(true); + }); + }); + + describe('when job is running', () => { + it('returns true', () => { + localState.job.has_trace = false; + localState.job.status = { group: 'running' }; + + expect(getters.hasJobLog(localState)).toEqual(true); + }); + }); + + describe('when has_trace is false and job is not running', () => { + it('returns false', () => { + localState.job.has_trace = false; + localState.job.status = { group: 'pending' }; + + expect(getters.hasJobLog(localState)).toEqual(false); + }); + }); + }); + + describe('emptyStateIllustration', () => { + describe('with defined illustration', () => { + it('returns the state illustration object', () => { + localState.job.status = { + illustration: { + path: 'foo', + }, + }; + + expect(getters.emptyStateIllustration(localState)).toEqual({ path: 'foo' }); + }); + }); + + describe('when illustration is not defined', () => { + it('returns an empty object', () => { + expect(getters.emptyStateIllustration(localState)).toEqual({}); + }); + }); + }); + + describe('shouldRenderSharedRunnerLimitWarning', () => { + describe('without runners information', () => { + it('returns false', () => { + expect(getters.shouldRenderSharedRunnerLimitWarning(localState)).toEqual(false); + }); + }); + + describe('with runners information', () => { + describe('when used quota is less than limit', () => { + it('returns false', () => { + localState.job.runners = { + quota: { + used: 33, + limit: 2000, + }, + available: true, + online: true, + }; + + expect(getters.shouldRenderSharedRunnerLimitWarning(localState)).toEqual(false); + }); + }); + + describe('when used quota is equal to limit', () => { + it('returns true', () => { + localState.job.runners = { + quota: { + used: 2000, + limit: 2000, + }, + available: true, + online: true, + }; + + expect(getters.shouldRenderSharedRunnerLimitWarning(localState)).toEqual(true); + }); + }); + + describe('when used quota is bigger than limit', () => { + it('returns true', () => { + localState.job.runners = { + quota: { + used: 2002, + limit: 2000, + }, + available: true, + online: true, + }; + + expect(getters.shouldRenderSharedRunnerLimitWarning(localState)).toEqual(true); + }); + }); + }); + }); + + describe('hasOfflineRunnersForProject', () => { + describe('with available and offline runners', () => { + it('returns true', () => { + localState.job.runners = { + available: true, + online: false, + }; + + expect(getters.hasOfflineRunnersForProject(localState)).toEqual(true); + }); + }); + + describe('with non available runners', () => { + it('returns false', () => { + localState.job.runners = { + available: false, + online: false, + }; + + expect(getters.hasOfflineRunnersForProject(localState)).toEqual(false); + }); + }); + + describe('with online runners', () => { + it('returns false', () => { + localState.job.runners = { + available: false, + online: true, + }; + + expect(getters.hasOfflineRunnersForProject(localState)).toEqual(false); + }); + }); + }); +}); diff --git a/spec/frontend/ci/job_details/store/helpers.js b/spec/frontend/ci/job_details/store/helpers.js new file mode 100644 index 00000000000..6b186e094e7 --- /dev/null +++ b/spec/frontend/ci/job_details/store/helpers.js @@ -0,0 +1,5 @@ +import state from '~/ci/job_details/store/state'; + +export const resetStore = (store) => { + store.replaceState(state()); +}; diff --git a/spec/frontend/ci/job_details/store/mutations_spec.js b/spec/frontend/ci/job_details/store/mutations_spec.js new file mode 100644 index 00000000000..0835c534fb9 --- /dev/null +++ b/spec/frontend/ci/job_details/store/mutations_spec.js @@ -0,0 +1,269 @@ +import * as types from '~/ci/job_details/store/mutation_types'; +import mutations from '~/ci/job_details/store/mutations'; +import state from '~/ci/job_details/store/state'; + +describe('Jobs Store Mutations', () => { + let stateCopy; + + const html = + 'I, [2018-08-17T22:57:45.707325 #1841] INFO -- : Writing /builds/ab89e95b0fa0b9272ea0c797b76908f24d36992630e9325273a4ce3.png<br>I'; + + beforeEach(() => { + stateCopy = state(); + }); + + describe('SET_JOB_ENDPOINT', () => { + it('should set jobEndpoint', () => { + mutations[types.SET_JOB_ENDPOINT](stateCopy, 'job/21312321.json'); + + expect(stateCopy.jobEndpoint).toEqual('job/21312321.json'); + }); + }); + + describe('HIDE_SIDEBAR', () => { + it('should set isSidebarOpen to false', () => { + mutations[types.HIDE_SIDEBAR](stateCopy); + + expect(stateCopy.isSidebarOpen).toEqual(false); + }); + }); + + describe('SHOW_SIDEBAR', () => { + it('should set isSidebarOpen to true', () => { + mutations[types.SHOW_SIDEBAR](stateCopy); + + expect(stateCopy.isSidebarOpen).toEqual(true); + }); + }); + + describe('RECEIVE_JOB_LOG_SUCCESS', () => { + describe('when job log has state', () => { + it('sets jobLogState', () => { + const stateLog = + 'eyJvZmZzZXQiOjczNDQ1MSwibl9vcGVuX3RhZ3MiOjAsImZnX2NvbG9yIjpudWxsLCJiZ19jb2xvciI6bnVsbCwic3R5bGVfbWFzayI6MH0='; + mutations[types.RECEIVE_JOB_LOG_SUCCESS](stateCopy, { + state: stateLog, + }); + + expect(stateCopy.jobLogState).toEqual(stateLog); + }); + }); + + describe('when jobLogSize is smaller than the total size', () => { + it('sets isJobLogSizeVisible to true', () => { + mutations[types.RECEIVE_JOB_LOG_SUCCESS](stateCopy, { total: 51184600, size: 1231 }); + + expect(stateCopy.isJobLogSizeVisible).toEqual(true); + }); + }); + + describe('when jobLogSize is bigger than the total size', () => { + it('sets isJobLogSizeVisible to false', () => { + const copy = { ...stateCopy, jobLogSize: 5118460, size: 2321312 }; + + mutations[types.RECEIVE_JOB_LOG_SUCCESS](copy, { total: 511846 }); + + expect(copy.isJobLogSizeVisible).toEqual(false); + }); + }); + + it('sets job log size and isJobLogComplete', () => { + mutations[types.RECEIVE_JOB_LOG_SUCCESS](stateCopy, { + append: true, + html, + size: 511846, + complete: true, + lines: [], + }); + + expect(stateCopy.jobLogSize).toEqual(511846); + expect(stateCopy.isJobLogComplete).toEqual(true); + }); + + describe('with new job log', () => { + describe('log.lines', () => { + describe('when append is true', () => { + it('sets the parsed log', () => { + mutations[types.RECEIVE_JOB_LOG_SUCCESS](stateCopy, { + append: true, + size: 511846, + complete: true, + lines: [ + { + offset: 1, + content: [{ text: 'Running with gitlab-runner 11.12.1 (5a147c92)' }], + }, + ], + }); + + expect(stateCopy.jobLog).toEqual([ + { + offset: 1, + content: [{ text: 'Running with gitlab-runner 11.12.1 (5a147c92)' }], + lineNumber: 0, + }, + ]); + }); + }); + + describe('when it is defined', () => { + it('sets the parsed log', () => { + mutations[types.RECEIVE_JOB_LOG_SUCCESS](stateCopy, { + append: false, + size: 511846, + complete: true, + lines: [ + { offset: 0, content: [{ text: 'Running with gitlab-runner 11.11.1 (5a147c92)' }] }, + ], + }); + + expect(stateCopy.jobLog).toEqual([ + { + offset: 0, + content: [{ text: 'Running with gitlab-runner 11.11.1 (5a147c92)' }], + lineNumber: 0, + }, + ]); + }); + }); + + describe('when it is null', () => { + it('sets the default value', () => { + mutations[types.RECEIVE_JOB_LOG_SUCCESS](stateCopy, { + append: true, + html, + size: 511846, + complete: false, + lines: null, + }); + + expect(stateCopy.jobLog).toEqual([]); + }); + }); + }); + }); + }); + + describe('SET_JOB_LOG_TIMEOUT', () => { + it('sets the jobLogTimeout id', () => { + const id = 7; + + expect(stateCopy.jobLogTimeout).not.toEqual(id); + + mutations[types.SET_JOB_LOG_TIMEOUT](stateCopy, id); + + expect(stateCopy.jobLogTimeout).toEqual(id); + }); + }); + + describe('STOP_POLLING_JOB_LOG', () => { + it('sets isJobLogComplete to true', () => { + mutations[types.STOP_POLLING_JOB_LOG](stateCopy); + + expect(stateCopy.isJobLogComplete).toEqual(true); + }); + }); + + describe('TOGGLE_COLLAPSIBLE_LINE', () => { + it('toggles the `isClosed` property of the provided object', () => { + const section = { isClosed: true }; + mutations[types.TOGGLE_COLLAPSIBLE_LINE](stateCopy, section); + expect(section.isClosed).toEqual(false); + }); + }); + + describe('REQUEST_JOB', () => { + it('sets isLoading to true', () => { + mutations[types.REQUEST_JOB](stateCopy); + + expect(stateCopy.isLoading).toEqual(true); + }); + }); + + describe('RECEIVE_JOB_SUCCESS', () => { + it('sets is loading to false', () => { + mutations[types.RECEIVE_JOB_SUCCESS](stateCopy, { id: 1312321 }); + + expect(stateCopy.isLoading).toEqual(false); + }); + + it('sets hasError to false', () => { + mutations[types.RECEIVE_JOB_SUCCESS](stateCopy, { id: 1312321 }); + + expect(stateCopy.hasError).toEqual(false); + }); + + it('sets job data', () => { + mutations[types.RECEIVE_JOB_SUCCESS](stateCopy, { id: 1312321 }); + + expect(stateCopy.job).toEqual({ id: 1312321 }); + }); + + it('sets selectedStage when the selectedStage is empty', () => { + expect(stateCopy.selectedStage).toEqual(''); + mutations[types.RECEIVE_JOB_SUCCESS](stateCopy, { id: 1312321, stage: 'deploy' }); + + expect(stateCopy.selectedStage).toEqual('deploy'); + }); + + it('does not set selectedStage when the selectedStage is not More', () => { + stateCopy.selectedStage = 'notify'; + + expect(stateCopy.selectedStage).toEqual('notify'); + mutations[types.RECEIVE_JOB_SUCCESS](stateCopy, { id: 1312321, stage: 'deploy' }); + + expect(stateCopy.selectedStage).toEqual('notify'); + }); + }); + + describe('RECEIVE_JOB_ERROR', () => { + it('resets job data', () => { + mutations[types.RECEIVE_JOB_ERROR](stateCopy); + + expect(stateCopy.isLoading).toEqual(false); + expect(stateCopy.job).toEqual({}); + }); + }); + + describe('REQUEST_JOBS_FOR_STAGE', () => { + it('sets isLoadingJobs to true', () => { + mutations[types.REQUEST_JOBS_FOR_STAGE](stateCopy, { name: 'deploy' }); + + expect(stateCopy.isLoadingJobs).toEqual(true); + }); + + it('sets selectedStage', () => { + mutations[types.REQUEST_JOBS_FOR_STAGE](stateCopy, { name: 'deploy' }); + + expect(stateCopy.selectedStage).toEqual('deploy'); + }); + }); + + describe('RECEIVE_JOBS_FOR_STAGE_SUCCESS', () => { + beforeEach(() => { + mutations[types.RECEIVE_JOBS_FOR_STAGE_SUCCESS](stateCopy, [{ name: 'karma' }]); + }); + + it('sets isLoadingJobs to false', () => { + expect(stateCopy.isLoadingJobs).toEqual(false); + }); + + it('sets jobs', () => { + expect(stateCopy.jobs).toEqual([{ name: 'karma' }]); + }); + }); + + describe('RECEIVE_JOBS_FOR_STAGE_ERROR', () => { + beforeEach(() => { + mutations[types.RECEIVE_JOBS_FOR_STAGE_ERROR](stateCopy); + }); + + it('sets isLoadingJobs to false', () => { + expect(stateCopy.isLoadingJobs).toEqual(false); + }); + + it('resets jobs', () => { + expect(stateCopy.jobs).toEqual([]); + }); + }); +}); diff --git a/spec/frontend/ci/job_details/store/utils_spec.js b/spec/frontend/ci/job_details/store/utils_spec.js new file mode 100644 index 00000000000..4ffba35761e --- /dev/null +++ b/spec/frontend/ci/job_details/store/utils_spec.js @@ -0,0 +1,510 @@ +import { + logLinesParser, + updateIncrementalJobLog, + parseHeaderLine, + parseLine, + addDurationToHeader, + isCollapsibleSection, + findOffsetAndRemove, + getIncrementalLineNumber, +} from '~/ci/job_details/store/utils'; +import { + utilsMockData, + originalTrace, + regularIncremental, + regularIncrementalRepeated, + headerTrace, + headerTraceIncremental, + collapsibleTrace, + collapsibleTraceIncremental, +} from '../components/log/mock_data'; + +describe('Jobs Store Utils', () => { + describe('parseHeaderLine', () => { + it('returns a new object with the header keys and the provided line parsed', () => { + const headerLine = { content: [{ text: 'foo' }] }; + const parsedHeaderLine = parseHeaderLine(headerLine, 2); + + expect(parsedHeaderLine).toEqual({ + isClosed: false, + isHeader: true, + line: { + ...headerLine, + lineNumber: 2, + }, + lines: [], + }); + }); + + it('pre-closes a section when specified in options', () => { + const headerLine = { content: [{ text: 'foo' }], section_options: { collapsed: 'true' } }; + + const parsedHeaderLine = parseHeaderLine(headerLine, 2); + + expect(parsedHeaderLine.isClosed).toBe(true); + }); + + it('expands all pre-closed sections if hash is present', () => { + const headerLine = { content: [{ text: 'foo' }], section_options: { collapsed: 'true' } }; + + const parsedHeaderLine = parseHeaderLine(headerLine, 2, '#L33'); + + expect(parsedHeaderLine.isClosed).toBe(false); + }); + }); + + describe('parseLine', () => { + it('returns a new object with the lineNumber key added to the provided line object', () => { + const line = { content: [{ text: 'foo' }] }; + const parsed = parseLine(line, 1); + expect(parsed.content).toEqual(line.content); + expect(parsed.lineNumber).toEqual(1); + }); + }); + + describe('addDurationToHeader', () => { + const duration = { + offset: 106, + content: [], + section: 'prepare-script', + section_duration: '00:03', + }; + + it('adds the section duration to the correct header', () => { + const parsed = [ + { + isClosed: false, + isHeader: true, + line: { + section: 'prepare-script', + content: [{ text: 'foo' }], + }, + lines: [], + }, + { + isClosed: false, + isHeader: true, + line: { + section: 'foo-bar', + content: [{ text: 'foo' }], + }, + lines: [], + }, + ]; + + addDurationToHeader(parsed, duration); + + expect(parsed[0].line.section_duration).toEqual(duration.section_duration); + expect(parsed[1].line.section_duration).toEqual(undefined); + }); + + it('does not add the section duration when the headers do not match', () => { + const parsed = [ + { + isClosed: false, + isHeader: true, + line: { + section: 'bar-foo', + content: [{ text: 'foo' }], + }, + lines: [], + }, + { + isClosed: false, + isHeader: true, + line: { + section: 'foo-bar', + content: [{ text: 'foo' }], + }, + lines: [], + }, + ]; + addDurationToHeader(parsed, duration); + + expect(parsed[0].line.section_duration).toEqual(undefined); + expect(parsed[1].line.section_duration).toEqual(undefined); + }); + + it('does not add when content has no headers', () => { + const parsed = [ + { + section: 'bar-foo', + content: [{ text: 'foo' }], + lineNumber: 1, + }, + { + section: 'foo-bar', + content: [{ text: 'foo' }], + lineNumber: 2, + }, + ]; + + addDurationToHeader(parsed, duration); + + expect(parsed[0].line).toEqual(undefined); + expect(parsed[1].line).toEqual(undefined); + }); + }); + + describe('isCollapsibleSection', () => { + const header = { + isHeader: true, + line: { + section: 'foo', + }, + }; + const line = { + lineNumber: 1, + section: 'foo', + content: [], + }; + + it('returns true when line belongs to the last section', () => { + expect(isCollapsibleSection([header], header, { section: 'foo', content: [] })).toEqual(true); + }); + + it('returns false when last line was not an header', () => { + expect(isCollapsibleSection([line], line, { section: 'bar' })).toEqual(false); + }); + + it('returns false when accumulator is empty', () => { + expect(isCollapsibleSection([], { isHeader: true }, { section: 'bar' })).toEqual(false); + }); + + it('returns false when section_duration is defined', () => { + expect(isCollapsibleSection([header], header, { section_duration: '10:00' })).toEqual(false); + }); + + it('returns false when `section` is not a match', () => { + expect(isCollapsibleSection([header], header, { section: 'bar' })).toEqual(false); + }); + + it('returns false when no parameters are provided', () => { + expect(isCollapsibleSection()).toEqual(false); + }); + }); + describe('logLinesParser', () => { + let result; + + beforeEach(() => { + result = logLinesParser(utilsMockData); + }); + + describe('regular line', () => { + it('adds a lineNumber property with correct index', () => { + expect(result[0].lineNumber).toEqual(0); + expect(result[1].line.lineNumber).toEqual(1); + }); + }); + + describe('collapsible section', () => { + it('adds a `isClosed` property', () => { + expect(result[1].isClosed).toEqual(false); + }); + + it('adds a `isHeader` property', () => { + expect(result[1].isHeader).toEqual(true); + }); + + it('creates a lines array property with the content of the collapsible section', () => { + expect(result[1].lines.length).toEqual(2); + expect(result[1].lines[0].content).toEqual(utilsMockData[2].content); + expect(result[1].lines[1].content).toEqual(utilsMockData[3].content); + }); + }); + + describe('section duration', () => { + it('adds the section information to the header section', () => { + expect(result[1].line.section_duration).toEqual(utilsMockData[4].section_duration); + }); + + it('does not add section duration as a line', () => { + expect(result[1].lines.includes(utilsMockData[4])).toEqual(false); + }); + }); + }); + + describe('findOffsetAndRemove', () => { + describe('when last item is header', () => { + const existingLog = [ + { + isHeader: true, + isClosed: false, + line: { content: [{ text: 'bar' }], offset: 10, lineNumber: 1 }, + }, + ]; + + describe('and matches the offset', () => { + it('returns an array with the item removed', () => { + const newData = [{ offset: 10, content: [{ text: 'foobar' }] }]; + const result = findOffsetAndRemove(newData, existingLog); + + expect(result).toEqual([]); + }); + }); + + describe('and does not match the offset', () => { + it('returns the provided existing log', () => { + const newData = [{ offset: 110, content: [{ text: 'foobar' }] }]; + const result = findOffsetAndRemove(newData, existingLog); + + expect(result).toEqual(existingLog); + }); + }); + }); + + describe('when last item is a regular line', () => { + const existingLog = [{ content: [{ text: 'bar' }], offset: 10, lineNumber: 1 }]; + + describe('and matches the offset', () => { + it('returns an array with the item removed', () => { + const newData = [{ offset: 10, content: [{ text: 'foobar' }] }]; + const result = findOffsetAndRemove(newData, existingLog); + + expect(result).toEqual([]); + }); + }); + + describe('and does not match the fofset', () => { + it('returns the provided old log', () => { + const newData = [{ offset: 101, content: [{ text: 'foobar' }] }]; + const result = findOffsetAndRemove(newData, existingLog); + + expect(result).toEqual(existingLog); + }); + }); + }); + + describe('when last item is nested', () => { + const existingLog = [ + { + isHeader: true, + isClosed: false, + lines: [{ offset: 101, content: [{ text: 'foobar' }], lineNumber: 2 }], + line: { + offset: 10, + lineNumber: 1, + section_duration: '10:00', + }, + }, + ]; + + describe('and matches the offset', () => { + it('returns an array with the last nested line item removed', () => { + const newData = [{ offset: 101, content: [{ text: 'foobar' }] }]; + + const result = findOffsetAndRemove(newData, existingLog); + expect(result[0].lines).toEqual([]); + }); + }); + + describe('and does not match the offset', () => { + it('returns the provided old log', () => { + const newData = [{ offset: 120, content: [{ text: 'foobar' }] }]; + + const result = findOffsetAndRemove(newData, existingLog); + expect(result).toEqual(existingLog); + }); + }); + }); + + describe('when no data is provided', () => { + it('returns an empty array', () => { + const result = findOffsetAndRemove(); + expect(result).toEqual([]); + }); + }); + }); + + describe('getIncrementalLineNumber', () => { + describe('when last line is 0', () => { + it('returns 1', () => { + const log = [ + { + content: [], + lineNumber: 0, + }, + ]; + + expect(getIncrementalLineNumber(log)).toEqual(1); + }); + }); + + describe('with unnested line', () => { + it('returns the lineNumber of the last item in the array', () => { + const log = [ + { + content: [], + lineNumber: 10, + }, + { + content: [], + lineNumber: 101, + }, + ]; + + expect(getIncrementalLineNumber(log)).toEqual(102); + }); + }); + + describe('when last line is the header section', () => { + it('returns the lineNumber of the last item in the array', () => { + const log = [ + { + content: [], + lineNumber: 10, + }, + { + isHeader: true, + line: { + lineNumber: 101, + content: [], + }, + lines: [], + }, + ]; + + expect(getIncrementalLineNumber(log)).toEqual(102); + }); + }); + + describe('when last line is a nested line', () => { + it('returns the lineNumber of the last item in the nested array', () => { + const log = [ + { + content: [], + lineNumber: 10, + }, + { + isHeader: true, + line: { + lineNumber: 101, + content: [], + }, + lines: [ + { + lineNumber: 102, + content: [], + }, + { lineNumber: 103, content: [] }, + ], + }, + ]; + + expect(getIncrementalLineNumber(log)).toEqual(104); + }); + }); + }); + + describe('updateIncrementalJobLog', () => { + describe('without repeated section', () => { + it('concats and parses both arrays', () => { + const oldLog = logLinesParser(originalTrace); + const result = updateIncrementalJobLog(regularIncremental, oldLog); + + expect(result).toEqual([ + { + offset: 1, + content: [ + { + text: 'Downloading', + }, + ], + lineNumber: 0, + }, + { + offset: 2, + content: [ + { + text: 'log line', + }, + ], + lineNumber: 1, + }, + ]); + }); + }); + + describe('with regular line repeated offset', () => { + it('updates the last line and formats with the incremental part', () => { + const oldLog = logLinesParser(originalTrace); + const result = updateIncrementalJobLog(regularIncrementalRepeated, oldLog); + + expect(result).toEqual([ + { + offset: 1, + content: [ + { + text: 'log line', + }, + ], + lineNumber: 0, + }, + ]); + }); + }); + + describe('with header line repeated', () => { + it('updates the header line and formats with the incremental part', () => { + const oldLog = logLinesParser(headerTrace); + const result = updateIncrementalJobLog(headerTraceIncremental, oldLog); + + expect(result).toEqual([ + { + isClosed: false, + isHeader: true, + line: { + offset: 1, + section_header: true, + content: [ + { + text: 'updated log line', + }, + ], + section: 'section', + lineNumber: 0, + }, + lines: [], + }, + ]); + }); + }); + + describe('with collapsible line repeated', () => { + it('updates the collapsible line and formats with the incremental part', () => { + const oldLog = logLinesParser(collapsibleTrace); + const result = updateIncrementalJobLog(collapsibleTraceIncremental, oldLog); + + expect(result).toEqual([ + { + isClosed: false, + isHeader: true, + line: { + offset: 1, + section_header: true, + content: [ + { + text: 'log line', + }, + ], + section: 'section', + lineNumber: 0, + }, + lines: [ + { + offset: 2, + content: [ + { + text: 'updated log line', + }, + ], + section: 'section', + lineNumber: 1, + }, + ], + }, + ]); + }); + }); + }); +}); |