diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2019-09-24 09:06:04 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2019-09-24 09:06:04 +0000 |
commit | bc89882970d6a14b1f72eb9c715fae90b26d066c (patch) | |
tree | f5cb59d5130d7585980eb39437071e07ebc12f87 /spec | |
parent | 4a45a787703cb78c6101750cfbdc9f656b934b42 (diff) | |
download | gitlab-ce-bc89882970d6a14b1f72eb9c715fae90b26d066c.tar.gz |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
16 files changed, 720 insertions, 273 deletions
diff --git a/spec/frontend/create_cluster/eks_cluster/components/eks_cluster_configuration_form_spec.js b/spec/frontend/create_cluster/eks_cluster/components/eks_cluster_configuration_form_spec.js new file mode 100644 index 00000000000..5b04328bb78 --- /dev/null +++ b/spec/frontend/create_cluster/eks_cluster/components/eks_cluster_configuration_form_spec.js @@ -0,0 +1,74 @@ +import { shallowMount, createLocalVue } from '@vue/test-utils'; +import Vuex from 'vuex'; +import Vue from 'vue'; +import EksClusterConfigurationForm from '~/create_cluster/eks_cluster/components/eks_cluster_configuration_form.vue'; +import RegionDropdown from '~/create_cluster/eks_cluster/components/region_dropdown.vue'; + +const localVue = createLocalVue(); +localVue.use(Vuex); + +describe('EksClusterConfigurationForm', () => { + let store; + let actions; + let state; + let vm; + + beforeEach(() => { + actions = { + fetchRegions: jest.fn(), + setRegion: jest.fn(), + }; + state = { + regions: [{ name: 'region 1' }], + isLoadingRegions: false, + loadingRegionsError: { message: '' }, + }; + store = new Vuex.Store({ + state, + actions, + }); + }); + + beforeEach(() => { + vm = shallowMount(EksClusterConfigurationForm, { + localVue, + store, + }); + }); + + afterEach(() => { + vm.destroy(); + }); + + const findRegionDropdown = () => vm.find(RegionDropdown); + + describe('when mounted', () => { + it('fetches available regions', () => { + expect(actions.fetchRegions).toHaveBeenCalled(); + }); + }); + + it('sets isLoadingRegions to RegionDropdown loading property', () => { + state.isLoadingRegions = true; + + return Vue.nextTick().then(() => { + expect(findRegionDropdown().props('loading')).toEqual(state.isLoadingRegions); + }); + }); + + it('sets regions to RegionDropdown regions property', () => { + expect(findRegionDropdown().props('regions')).toEqual(state.regions); + }); + + it('sets loadingRegionsError to RegionDropdown error property', () => { + expect(findRegionDropdown().props('error')).toEqual(state.loadingRegionsError); + }); + + it('dispatches setRegion action when region is selected', () => { + const region = { region: 'us-west-2' }; + + findRegionDropdown().vm.$emit('input', region); + + expect(actions.setRegion).toHaveBeenCalledWith(expect.anything(), { region }, undefined); + }); +}); diff --git a/spec/frontend/create_cluster/eks_cluster/components/region_dropdown_spec.js b/spec/frontend/create_cluster/eks_cluster/components/region_dropdown_spec.js new file mode 100644 index 00000000000..0ebb5026a4b --- /dev/null +++ b/spec/frontend/create_cluster/eks_cluster/components/region_dropdown_spec.js @@ -0,0 +1,55 @@ +import { shallowMount } from '@vue/test-utils'; + +import ClusterFormDropdown from '~/create_cluster/eks_cluster/components/cluster_form_dropdown.vue'; +import RegionDropdown from '~/create_cluster/eks_cluster/components/region_dropdown.vue'; + +describe('RegionDropdown', () => { + let vm; + + const getClusterFormDropdown = () => vm.find(ClusterFormDropdown); + + beforeEach(() => { + vm = shallowMount(RegionDropdown); + }); + afterEach(() => vm.destroy()); + + it('renders a cluster-form-dropdown', () => { + expect(getClusterFormDropdown().exists()).toBe(true); + }); + + it('sets regions to cluster-form-dropdown items property', () => { + const regions = [{ name: 'basic' }]; + + vm.setProps({ regions }); + + expect(getClusterFormDropdown().props('items')).toEqual(regions); + }); + + it('sets a loading text', () => { + expect(getClusterFormDropdown().props('loadingText')).toEqual('Loading Regions'); + }); + + it('sets a placeholder', () => { + expect(getClusterFormDropdown().props('placeholder')).toEqual('Select a region'); + }); + + it('sets an empty results text', () => { + expect(getClusterFormDropdown().props('emptyText')).toEqual('No region found'); + }); + + it('sets a search field placeholder', () => { + expect(getClusterFormDropdown().props('searchFieldPlaceholder')).toEqual('Search regions'); + }); + + it('sets hasErrors property', () => { + vm.setProps({ error: {} }); + + expect(getClusterFormDropdown().props('hasErrors')).toEqual(true); + }); + + it('sets an error message', () => { + expect(getClusterFormDropdown().props('errorMessage')).toEqual( + 'Could not load regions from your AWS account', + ); + }); +}); diff --git a/spec/frontend/create_cluster/eks_cluster/store/actions_spec.js b/spec/frontend/create_cluster/eks_cluster/store/actions_spec.js new file mode 100644 index 00000000000..9a3970813ed --- /dev/null +++ b/spec/frontend/create_cluster/eks_cluster/store/actions_spec.js @@ -0,0 +1,98 @@ +import testAction from 'helpers/vuex_action_helper'; + +import * as awsServicesFacade from '~/create_cluster/eks_cluster/services/aws_services_facade'; +import createState from '~/create_cluster/eks_cluster/store/state'; +import * as types from '~/create_cluster/eks_cluster/store/mutation_types'; +import * as actions from '~/create_cluster/eks_cluster/store/actions'; + +describe('EKS Cluster Store Actions', () => { + const regions = [{ name: 'region 1' }]; + + describe('fetchRegions', () => { + describe('on success', () => { + beforeEach(() => { + jest.spyOn(awsServicesFacade, 'fetchRegions').mockResolvedValueOnce(regions); + }); + + it('dispatches success with received regions', () => + testAction( + actions.fetchRegions, + null, + createState(), + [], + [ + { type: 'requestRegions' }, + { + type: 'receiveRegionsSuccess', + payload: { regions }, + }, + ], + )); + }); + + describe('on failure', () => { + const error = new Error('Could not fetch regions'); + + beforeEach(() => { + jest.spyOn(awsServicesFacade, 'fetchRegions').mockRejectedValueOnce(error); + }); + + it('dispatches success with received regions', () => + testAction( + actions.fetchRegions, + null, + createState(), + [], + [ + { type: 'requestRegions' }, + { + type: 'receiveRegionsError', + payload: { error }, + }, + ], + )); + }); + }); + + describe('requestRegions', () => { + it(`commits ${types.REQUEST_REGIONS} mutation`, () => + testAction(actions.requestRegions, null, createState(), [{ type: types.REQUEST_REGIONS }])); + }); + + describe('receiveRegionsSuccess', () => { + it(`commits ${types.RECEIVE_REGIONS_SUCCESS} mutation`, () => + testAction(actions.receiveRegionsSuccess, { regions }, createState(), [ + { + type: types.RECEIVE_REGIONS_SUCCESS, + payload: { + regions, + }, + }, + ])); + }); + + describe('receiveRegionsError', () => { + it(`commits ${types.RECEIVE_REGIONS_ERROR} mutation`, () => { + const error = new Error('Error fetching regions'); + + testAction(actions.receiveRegionsError, { error }, createState(), [ + { + type: types.RECEIVE_REGIONS_ERROR, + payload: { + error, + }, + }, + ]); + }); + }); + + describe('setRegion', () => { + it(`commits ${types.SET_REGION} mutation`, () => { + const region = { name: 'west-1' }; + + testAction(actions.setRegion, { region }, createState(), [ + { type: types.SET_REGION, payload: { region } }, + ]); + }); + }); +}); diff --git a/spec/frontend/create_cluster/eks_cluster/store/mutations_spec.js b/spec/frontend/create_cluster/eks_cluster/store/mutations_spec.js new file mode 100644 index 00000000000..f2d48635f8c --- /dev/null +++ b/spec/frontend/create_cluster/eks_cluster/store/mutations_spec.js @@ -0,0 +1,40 @@ +import { + REQUEST_REGIONS, + RECEIVE_REGIONS_ERROR, + RECEIVE_REGIONS_SUCCESS, + SET_REGION, +} from '~/create_cluster/eks_cluster/store/mutation_types'; +import createState from '~/create_cluster/eks_cluster/store/state'; +import mutations from '~/create_cluster/eks_cluster/store/mutations'; + +describe('Create EKS cluster store mutations', () => { + let state; + let emptyPayload; + let regions; + let region; + let error; + + beforeEach(() => { + emptyPayload = {}; + region = { name: 'regions-1' }; + regions = [region]; + error = new Error('could not load error'); + state = createState(); + }); + + it.each` + mutation | mutatedProperty | payload | expectedValue | expectedValueDescription + ${REQUEST_REGIONS} | ${'isLoadingRegions'} | ${emptyPayload} | ${true} | ${true} + ${REQUEST_REGIONS} | ${'loadingRegionsError'} | ${emptyPayload} | ${null} | ${null} + ${RECEIVE_REGIONS_SUCCESS} | ${'isLoadingRegions'} | ${{ regions }} | ${false} | ${false} + ${RECEIVE_REGIONS_SUCCESS} | ${'regions'} | ${{ regions }} | ${regions} | ${'regions payload'} + ${RECEIVE_REGIONS_ERROR} | ${'isLoadingRegions'} | ${{ error }} | ${false} | ${false} + ${RECEIVE_REGIONS_ERROR} | ${'error'} | ${{ error }} | ${error} | ${'received error object'} + ${SET_REGION} | ${'selectedRegion'} | ${{ region }} | ${region} | ${'selected region payload'} + `(`$mutation sets $mutatedProperty to $expectedValueDescription`, data => { + const { mutation, mutatedProperty, payload, expectedValue } = data; + + mutations[mutation](state, payload); + expect(state[mutatedProperty]).toBe(expectedValue); + }); +}); diff --git a/spec/frontend/ide/components/jobs/list_spec.js b/spec/frontend/ide/components/jobs/list_spec.js new file mode 100644 index 00000000000..ec2e5b05048 --- /dev/null +++ b/spec/frontend/ide/components/jobs/list_spec.js @@ -0,0 +1,115 @@ +import { shallowMount, mount, createLocalVue } from '@vue/test-utils'; +import { GlLoadingIcon } from '@gitlab/ui'; +import Vuex from 'vuex'; +import StageList from '~/ide/components/jobs/list.vue'; +import Stage from '~/ide/components/jobs/stage.vue'; + +const localVue = createLocalVue(); +localVue.use(Vuex); +const storeActions = { + fetchJobs: jest.fn(), + toggleStageCollapsed: jest.fn(), + setDetailJob: jest.fn(), +}; + +const store = new Vuex.Store({ + modules: { + pipelines: { + namespaced: true, + actions: storeActions, + }, + }, +}); + +describe('IDE stages list', () => { + let wrapper; + + const defaultProps = { + stages: [], + loading: false, + }; + + const stages = ['build', 'test', 'deploy'].map((name, id) => ({ + id, + name, + jobs: [], + status: { icon: 'status_success' }, + })); + + const createComponent = props => { + wrapper = shallowMount(StageList, { + propsData: { + ...defaultProps, + ...props, + }, + localVue, + store, + sync: false, + }); + }; + + afterEach(() => { + Object.values(storeActions).forEach(actionMock => actionMock.mockClear()); + }); + + afterAll(() => { + wrapper.destroy(); + wrapper = null; + }); + + it('renders loading icon when no stages & loading', () => { + createComponent({ loading: true, stages: [] }); + + expect(wrapper.find(GlLoadingIcon).exists()).toBe(true); + }); + + it('renders stages components for each stage', () => { + createComponent({ stages }); + expect(wrapper.findAll(Stage).length).toBe(stages.length); + }); + + it('triggers fetchJobs action when stage emits fetch event', () => { + createComponent({ stages }); + wrapper.find(Stage).vm.$emit('fetch'); + expect(storeActions.fetchJobs).toHaveBeenCalled(); + }); + + it('triggers toggleStageCollapsed action when stage emits toggleCollapsed event', () => { + createComponent({ stages }); + wrapper.find(Stage).vm.$emit('toggleCollapsed'); + expect(storeActions.toggleStageCollapsed).toHaveBeenCalled(); + }); + + it('triggers setDetailJob action when stage emits clickViewLog event', () => { + createComponent({ stages }); + wrapper.find(Stage).vm.$emit('clickViewLog'); + expect(storeActions.setDetailJob).toHaveBeenCalled(); + }); + + describe('integration tests', () => { + const findCardHeader = () => wrapper.find('.card-header'); + + beforeEach(() => { + wrapper = mount(StageList, { + propsData: { ...defaultProps, stages }, + store, + sync: false, + localVue, + }); + }); + + it('calls toggleStageCollapsed when clicking stage header', () => { + findCardHeader().trigger('click'); + + expect(storeActions.toggleStageCollapsed).toHaveBeenCalledWith( + expect.any(Object), + 0, + undefined, + ); + }); + + it('calls fetchJobs when stage is mounted', () => { + expect(storeActions.fetchJobs.mock.calls.map(([, stage]) => stage)).toEqual(stages); + }); + }); +}); diff --git a/spec/frontend/ide/components/pipelines/__snapshots__/list_spec.js.snap b/spec/frontend/ide/components/pipelines/__snapshots__/list_spec.js.snap new file mode 100644 index 00000000000..5fbe6af750d --- /dev/null +++ b/spec/frontend/ide/components/pipelines/__snapshots__/list_spec.js.snap @@ -0,0 +1,15 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`IDE pipelines list when loaded renders empty state when no latestPipeline 1`] = ` +<div + class="ide-pipeline" +> + <!----> + + <emptystate-stub + cansetci="true" + emptystatesvgpath="http://test.host" + helppagepath="http://test.host" + /> +</div> +`; diff --git a/spec/frontend/ide/components/pipelines/list_spec.js b/spec/frontend/ide/components/pipelines/list_spec.js new file mode 100644 index 00000000000..a974139a8f9 --- /dev/null +++ b/spec/frontend/ide/components/pipelines/list_spec.js @@ -0,0 +1,193 @@ +import { shallowMount, createLocalVue } from '@vue/test-utils'; +import Vuex from 'vuex'; +import List from '~/ide/components/pipelines/list.vue'; +import JobsList from '~/ide/components/jobs/list.vue'; +import Tab from '~/vue_shared/components/tabs/tab.vue'; +import CiIcon from '~/vue_shared/components/ci_icon.vue'; +import { GlLoadingIcon } from '@gitlab/ui'; +import { TEST_HOST } from 'helpers/test_constants'; +import { pipelines } from '../../../../javascripts/ide/mock_data'; + +const localVue = createLocalVue(); +localVue.use(Vuex); + +describe('IDE pipelines list', () => { + let wrapper; + + const defaultState = { + links: { ciHelpPagePath: TEST_HOST }, + pipelinesEmptyStateSvgPath: TEST_HOST, + pipelines: { + stages: [], + failedStages: [], + isLoadingJobs: false, + }, + }; + + const fetchLatestPipelineMock = jest.fn(); + const failedStagesGetterMock = jest.fn().mockReturnValue([]); + + const createComponent = (state = {}) => { + const { pipelines: pipelinesState, ...restOfState } = state; + const { defaultPipelines, ...defaultRestOfState } = defaultState; + + const fakeStore = new Vuex.Store({ + getters: { currentProject: () => ({ web_url: 'some/url ' }) }, + state: { + ...defaultRestOfState, + ...restOfState, + }, + modules: { + pipelines: { + namespaced: true, + state: { + ...defaultPipelines, + ...pipelinesState, + }, + actions: { + fetchLatestPipeline: fetchLatestPipelineMock, + }, + getters: { + jobsCount: () => 1, + failedJobsCount: () => 1, + failedStages: failedStagesGetterMock, + pipelineFailed: () => false, + }, + methods: { + fetchLatestPipeline: jest.fn(), + }, + }, + }, + }); + + wrapper = shallowMount(List, { + localVue, + store: fakeStore, + sync: false, + }); + }; + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + it('fetches latest pipeline', () => { + createComponent(); + + expect(fetchLatestPipelineMock).toHaveBeenCalled(); + }); + + describe('when loading', () => { + let defaultPipelinesLoadingState; + beforeAll(() => { + defaultPipelinesLoadingState = { + ...defaultState.pipelines, + isLoadingPipeline: true, + }; + }); + + it('does not render when pipeline has loaded before', () => { + createComponent({ + pipelines: { + ...defaultPipelinesLoadingState, + hasLoadedPipeline: true, + }, + }); + + expect(wrapper.find(GlLoadingIcon).exists()).toBe(false); + }); + + it('renders loading state', () => { + createComponent({ + pipelines: { + ...defaultPipelinesLoadingState, + hasLoadedPipeline: false, + }, + }); + + expect(wrapper.find(GlLoadingIcon).exists()).toBe(true); + }); + }); + + describe('when loaded', () => { + let defaultPipelinesLoadedState; + beforeAll(() => { + defaultPipelinesLoadedState = { + ...defaultState.pipelines, + isLoadingPipeline: false, + hasLoadedPipeline: true, + }; + }); + + it('renders empty state when no latestPipeline', () => { + createComponent({ pipelines: { ...defaultPipelinesLoadedState, latestPipeline: null } }); + expect(wrapper.element).toMatchSnapshot(); + }); + + describe('with latest pipeline loaded', () => { + let withLatestPipelineState; + beforeAll(() => { + withLatestPipelineState = { + ...defaultPipelinesLoadedState, + latestPipeline: pipelines[0], + }; + }); + + it('renders ci icon', () => { + createComponent({ pipelines: withLatestPipelineState }); + expect(wrapper.find(CiIcon).exists()).toBe(true); + }); + + it('renders pipeline data', () => { + createComponent({ pipelines: withLatestPipelineState }); + + expect(wrapper.text()).toContain('#1'); + }); + + it('renders list of jobs', () => { + const stages = []; + const isLoadingJobs = true; + createComponent({ pipelines: { ...withLatestPipelineState, stages, isLoadingJobs } }); + + const jobProps = wrapper + .findAll(Tab) + .at(0) + .find(JobsList) + .props(); + expect(jobProps.stages).toBe(stages); + expect(jobProps.loading).toBe(isLoadingJobs); + }); + + it('renders list of failed jobs', () => { + const failedStages = []; + failedStagesGetterMock.mockReset().mockReturnValue(failedStages); + const isLoadingJobs = true; + createComponent({ pipelines: { ...withLatestPipelineState, isLoadingJobs } }); + + const jobProps = wrapper + .findAll(Tab) + .at(1) + .find(JobsList) + .props(); + expect(jobProps.stages).toBe(failedStages); + expect(jobProps.loading).toBe(isLoadingJobs); + }); + + describe('with YAML error', () => { + it('renders YAML error', () => { + const yamlError = 'test yaml error'; + createComponent({ + pipelines: { + ...defaultPipelinesLoadedState, + latestPipeline: { ...pipelines[0], yamlError }, + }, + }); + + expect(wrapper.text()).toContain('Found errors in your .gitlab-ci.yml:'); + expect(wrapper.text()).toContain(yamlError); + }); + }); + }); + }); +}); diff --git a/spec/frontend/jobs/components/log/collapsible_section_spec.js b/spec/frontend/jobs/components/log/collapsible_section_spec.js index 6c1ebf0a7c1..01184a51193 100644 --- a/spec/frontend/jobs/components/log/collapsible_section_spec.js +++ b/spec/frontend/jobs/components/log/collapsible_section_spec.js @@ -1,6 +1,6 @@ import { mount } from '@vue/test-utils'; import CollpasibleSection from '~/jobs/components/log/collapsible_section.vue'; -import { nestedSectionOpened, nestedSectionClosed } from './mock_data'; +import { collapsibleSectionClosed, collapsibleSectionOpened } from './mock_data'; describe('Job Log Collapsible Section', () => { let wrapper; @@ -8,6 +8,7 @@ describe('Job Log Collapsible Section', () => { const traceEndpoint = 'jobs/335'; const findCollapsibleLine = () => wrapper.find('.collapsible-line'); + const findCollapsibleLineSvg = () => wrapper.find('.collapsible-line svg'); const createComponent = (props = {}) => { wrapper = mount(CollpasibleSection, { @@ -22,10 +23,10 @@ describe('Job Log Collapsible Section', () => { wrapper.destroy(); }); - describe('with closed nested section', () => { + describe('with closed section', () => { beforeEach(() => { createComponent({ - section: nestedSectionClosed, + section: collapsibleSectionClosed, traceEndpoint, }); }); @@ -33,24 +34,36 @@ describe('Job Log Collapsible Section', () => { it('renders clickable header line', () => { expect(findCollapsibleLine().attributes('role')).toBe('button'); }); + + it('renders an icon with the closed state', () => { + expect(findCollapsibleLineSvg().classes()).toContain('ic-angle-right'); + }); }); - describe('with opened nested section', () => { + describe('with opened section', () => { beforeEach(() => { createComponent({ - section: nestedSectionOpened, + section: collapsibleSectionOpened, traceEndpoint, }); }); - it('renders all sections opened', () => { - expect(wrapper.findAll('.collapsible-line').length).toBe(2); + it('renders clickable header line', () => { + expect(findCollapsibleLine().attributes('role')).toBe('button'); + }); + + it('renders an icon with the open state', () => { + expect(findCollapsibleLineSvg().classes()).toContain('ic-angle-down'); + }); + + it('renders collapsible lines content', () => { + expect(wrapper.findAll('.js-line').length).toEqual(collapsibleSectionOpened.lines.length); }); }); it('emits onClickCollapsibleLine on click', () => { createComponent({ - section: nestedSectionOpened, + section: collapsibleSectionOpened, traceEndpoint, }); diff --git a/spec/frontend/jobs/components/log/mock_data.js b/spec/frontend/jobs/components/log/mock_data.js index 0dae306dcc7..d375d82d3ca 100644 --- a/spec/frontend/jobs/components/log/mock_data.js +++ b/spec/frontend/jobs/components/log/mock_data.js @@ -14,13 +14,13 @@ export const jobLog = [ text: 'Using Docker executor with image dev.gitlab.org3', }, ], - sections: ['prepare-executor'], + section: 'prepare-executor', section_header: true, }, { offset: 1003, content: [{ text: 'Starting service postgres:9.6.14 ...', style: 'text-green' }], - sections: ['prepare-executor'], + section: 'prepare-executor', }, ]; @@ -37,23 +37,23 @@ export const utilsMockData = [ 'Using Docker executor with image dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.6.3-golang-1.11-git-2.22-chrome-73.0-node-12.x-yarn-1.16-postgresql-9.6-graphicsmagick-1.3.33', }, ], - sections: ['prepare-executor'], + section: 'prepare-executor', section_header: true, }, { offset: 1003, content: [{ text: 'Starting service postgres:9.6.14 ...' }], - sections: ['prepare-executor'], + section: 'prepare-executor', }, { offset: 1004, content: [{ text: 'Pulling docker image postgres:9.6.14 ...', style: 'term-fg-l-green' }], - sections: ['prepare-executor'], + section: 'prepare-executor', }, { offset: 1005, content: [], - sections: ['prepare-executor'], + section: 'prepare-executor', section_duration: '10:00', }, ]; @@ -100,7 +100,7 @@ export const headerTrace = [ text: 'log line', }, ], - sections: ['section'], + section: 'section', }, ]; @@ -113,7 +113,7 @@ export const headerTraceIncremental = [ text: 'updated log line', }, ], - sections: ['section'], + section: 'section', }, ]; @@ -126,7 +126,7 @@ export const collapsibleTrace = [ text: 'log line', }, ], - sections: ['section'], + section: 'section', }, { offset: 2, @@ -135,7 +135,7 @@ export const collapsibleTrace = [ text: 'log line', }, ], - sections: ['section'], + section: 'section', }, ]; @@ -147,76 +147,48 @@ export const collapsibleTraceIncremental = [ text: 'updated log line', }, ], - sections: ['section'], + section: 'section', }, ]; -export const nestedSectionClosed = { +export const collapsibleSectionClosed = { offset: 5, section_header: true, isHeader: true, isClosed: true, line: { content: [{ text: 'foo' }], - sections: ['prepare-script'], + section: 'prepare-script', lineNumber: 1, }, section_duration: '00:03', lines: [ { - section_header: true, - section_duration: '00:02', - isHeader: true, - isClosed: true, - line: { - offset: 52, - content: [{ text: 'bar' }], - sections: ['prepare-script', 'prepare-script-nested'], - lineNumber: 2, - }, - lines: [ - { - offset: 80, - content: [{ text: 'this is a collapsible nested section' }], - sections: ['prepare-script', 'prepare-script-nested'], - lineNumber: 3, - }, - ], + offset: 80, + content: [{ text: 'this is a collapsible nested section' }], + section: 'prepare-script', + lineNumber: 3, }, ], }; -export const nestedSectionOpened = { +export const collapsibleSectionOpened = { offset: 5, section_header: true, isHeader: true, isClosed: false, line: { content: [{ text: 'foo' }], - sections: ['prepare-script'], + section: 'prepare-script', lineNumber: 1, }, section_duration: '00:03', lines: [ { - section_header: true, - section_duration: '00:02', - isHeader: true, - isClosed: false, - line: { - offset: 52, - content: [{ text: 'bar' }], - sections: ['prepare-script', 'prepare-script-nested'], - lineNumber: 2, - }, - lines: [ - { - offset: 80, - content: [{ text: 'this is a collapsible nested section' }], - sections: ['prepare-script', 'prepare-script-nested'], - lineNumber: 3, - }, - ], + offset: 80, + content: [{ text: 'this is a collapsible nested section' }], + section: 'prepare-script', + lineNumber: 3, }, ], }; diff --git a/spec/frontend/jobs/store/utils_spec.js b/spec/frontend/jobs/store/utils_spec.js index 780d42fd6a1..1e885b6b788 100644 --- a/spec/frontend/jobs/store/utils_spec.js +++ b/spec/frontend/jobs/store/utils_spec.js @@ -1,4 +1,9 @@ -import { logLinesParser, updateIncrementalTrace } from '~/jobs/store/utils'; +import { + logLinesParser, + updateIncrementalTrace, + parseHeaderLine, + parseLine, +} from '~/jobs/store/utils'; import { utilsMockData, originalTrace, @@ -11,6 +16,32 @@ import { } 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: true, + isHeader: true, + line: { + ...headerLine, + lineNumber: 2, + }, + lines: [], + }); + }); + }); + + 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('logLinesParser', () => { let result; @@ -117,7 +148,7 @@ describe('Jobs Store Utils', () => { text: 'updated log line', }, ], - sections: ['section'], + section: 'section', lineNumber: 0, }, lines: [], @@ -147,7 +178,7 @@ describe('Jobs Store Utils', () => { text: 'log line', }, ], - sections: ['section'], + section: 'section', lineNumber: 0, }, lines: [ @@ -158,7 +189,7 @@ describe('Jobs Store Utils', () => { text: 'updated log line', }, ], - sections: ['section'], + section: 'section', lineNumber: 1, }, ], diff --git a/spec/javascripts/ide/components/jobs/list_spec.js b/spec/javascripts/ide/components/jobs/list_spec.js deleted file mode 100644 index b24853c56fa..00000000000 --- a/spec/javascripts/ide/components/jobs/list_spec.js +++ /dev/null @@ -1,67 +0,0 @@ -import Vue from 'vue'; -import StageList from '~/ide/components/jobs/list.vue'; -import { createStore } from '~/ide/stores'; -import { createComponentWithStore } from '../../../helpers/vue_mount_component_helper'; -import { stages, jobs } from '../../mock_data'; - -describe('IDE stages list', () => { - const Component = Vue.extend(StageList); - let vm; - - beforeEach(() => { - const store = createStore(); - - vm = createComponentWithStore(Component, store, { - stages: stages.map((mappedState, i) => ({ - ...mappedState, - id: i, - dropdownPath: mappedState.dropdown_path, - jobs: [...jobs], - isLoading: false, - isCollapsed: false, - })), - loading: false, - }); - - spyOn(vm, 'fetchJobs'); - spyOn(vm, 'toggleStageCollapsed'); - - vm.$mount(); - }); - - afterEach(() => { - vm.$destroy(); - }); - - it('renders list of stages', () => { - expect(vm.$el.querySelectorAll('.card').length).toBe(2); - }); - - it('renders loading icon when no stages & is loading', done => { - vm.stages = []; - vm.loading = true; - - vm.$nextTick(() => { - expect(vm.$el.querySelector('.loading-container')).not.toBe(null); - - done(); - }); - }); - - it('calls toggleStageCollapsed when clicking stage header', done => { - vm.$el.querySelector('.card-header').click(); - - vm.$nextTick(() => { - expect(vm.toggleStageCollapsed).toHaveBeenCalledWith(0); - - done(); - }); - }); - - it('calls fetchJobs when stage is mounted', () => { - expect(vm.fetchJobs.calls.count()).toBe(stages.length); - - expect(vm.fetchJobs.calls.argsFor(0)).toEqual([vm.stages[0]]); - expect(vm.fetchJobs.calls.argsFor(1)).toEqual([vm.stages[1]]); - }); -}); diff --git a/spec/javascripts/ide/components/pipelines/list_spec.js b/spec/javascripts/ide/components/pipelines/list_spec.js deleted file mode 100644 index 80829f2358a..00000000000 --- a/spec/javascripts/ide/components/pipelines/list_spec.js +++ /dev/null @@ -1,137 +0,0 @@ -import Vue from 'vue'; -import MockAdapter from 'axios-mock-adapter'; -import axios from '~/lib/utils/axios_utils'; -import { createStore } from '~/ide/stores'; -import List from '~/ide/components/pipelines/list.vue'; -import { createComponentWithStore } from '../../../helpers/vue_mount_component_helper'; -import { pipelines, projectData, stages, jobs } from '../../mock_data'; - -describe('IDE pipelines list', () => { - const Component = Vue.extend(List); - let vm; - let mock; - - const findLoadingState = () => vm.$el.querySelector('.loading-container'); - - beforeEach(done => { - const store = createStore(); - - mock = new MockAdapter(axios); - - store.state.currentProjectId = 'abc/def'; - store.state.currentBranchId = 'master'; - store.state.projects['abc/def'] = { - ...projectData, - path_with_namespace: 'abc/def', - branches: { - master: { commit: { id: '123' } }, - }, - }; - store.state.links = { ciHelpPagePath: gl.TEST_HOST }; - store.state.pipelinesEmptyStateSvgPath = gl.TEST_HOST; - store.state.pipelines.stages = stages.map((mappedState, i) => ({ - ...mappedState, - id: i, - dropdownPath: mappedState.dropdown_path, - jobs: [...jobs], - isLoading: false, - isCollapsed: false, - })); - - mock - .onGet('/abc/def/commit/123/pipelines') - .replyOnce(200, { pipelines: [...pipelines] }, { 'poll-interval': '-1' }); - - vm = createComponentWithStore(Component, store).$mount(); - - setTimeout(done); - }); - - afterEach(done => { - vm.$destroy(); - mock.restore(); - - vm.$store - .dispatch('pipelines/stopPipelinePolling') - .then(() => vm.$store.dispatch('pipelines/clearEtagPoll')) - .then(done) - .catch(done.fail); - }); - - it('renders pipeline data', () => { - expect(vm.$el.textContent).toContain('#1'); - }); - - it('renders CI icon', () => { - expect(vm.$el.querySelector('.ci-status-icon-failed')).not.toBe(null); - }); - - it('renders list of jobs', () => { - expect(vm.$el.querySelectorAll('.tab-pane:first-child .ide-job-item').length).toBe( - jobs.length * stages.length, - ); - }); - - it('renders list of failed jobs on failed jobs tab', done => { - vm.$el.querySelectorAll('.tab-links a')[1].click(); - - vm.$nextTick(() => { - expect(vm.$el.querySelectorAll('.tab-pane.active .ide-job-item').length).toBe(2); - - done(); - }); - }); - - describe('YAML error', () => { - it('renders YAML error', done => { - vm.$store.state.pipelines.latestPipeline.yamlError = 'test yaml error'; - - vm.$nextTick(() => { - expect(vm.$el.textContent).toContain('Found errors in your .gitlab-ci.yml:'); - expect(vm.$el.textContent).toContain('test yaml error'); - - done(); - }); - }); - }); - - describe('empty state', () => { - it('renders pipelines empty state', done => { - vm.$store.state.pipelines.latestPipeline = null; - - vm.$nextTick(() => { - expect(vm.$el.querySelector('.empty-state')).not.toBe(null); - - done(); - }); - }); - }); - - describe('loading state', () => { - beforeEach(() => { - vm.$store.state.pipelines.isLoadingPipeline = true; - }); - - it('does not render when pipeline has loaded before', done => { - vm.$store.state.pipelines.hasLoadedPipeline = true; - - vm.$nextTick() - .then(() => { - expect(findLoadingState()).toBe(null); - }) - .then(done) - .catch(done.fail); - }); - - it('renders loading state when there is no latest pipeline', done => { - vm.$store.state.pipelines.hasLoadedPipeline = false; - - vm.$nextTick() - .then(() => { - expect(findLoadingState()).not.toBe(null); - }) - .then(done) - .catch(done.fail); - }); - }); -}); diff --git a/spec/javascripts/ide/mock_data.js b/spec/javascripts/ide/mock_data.js index c02c7e5d45e..1c2e082489e 100644 --- a/spec/javascripts/ide/mock_data.js +++ b/spec/javascripts/ide/mock_data.js @@ -1,3 +1,5 @@ +import { TEST_HOST } from '../test_constants'; + export const projectData = { id: 1, name: 'abcproject', @@ -50,7 +52,7 @@ export const pipelines = [ export const stages = [ { - dropdown_path: `${gl.TEST_HOST}/testing`, + dropdown_path: `${TEST_HOST}/testing`, name: 'build', status: { icon: 'status_failed', @@ -163,7 +165,7 @@ export const mergeRequests = [ iid: 1, title: 'Test merge request', project_id: 1, - web_url: `${gl.TEST_HOST}/namespace/project-path/merge_requests/1`, + web_url: `${TEST_HOST}/namespace/project-path/merge_requests/1`, }, ]; diff --git a/spec/lib/gitlab/sidekiq_logging/exception_handler_spec.rb b/spec/lib/gitlab/sidekiq_logging/exception_handler_spec.rb new file mode 100644 index 00000000000..24b6090cb19 --- /dev/null +++ b/spec/lib/gitlab/sidekiq_logging/exception_handler_spec.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::SidekiqLogging::ExceptionHandler do + describe '#call' do + let(:job) do + { + "class" => "TestWorker", + "args" => [1234, 'hello'], + "retry" => false, + "queue" => "cronjob:test_queue", + "queue_namespace" => "cronjob", + "jid" => "da883554ee4fe414012f5f42", + "correlation_id" => 'cid' + } + end + + let(:exception_message) { 'An error was thrown' } + let(:backtrace) { caller } + let(:exception) { RuntimeError.new(exception_message) } + let(:logger) { double } + + before do + allow(Sidekiq).to receive(:logger).and_return(logger) + allow(exception).to receive(:backtrace).and_return(backtrace) + end + + subject { described_class.new.call(exception, { context: 'Test', job: job }) } + + it 'logs job data into root tree' do + expected_data = job.merge( + error_class: 'RuntimeError', + error_message: exception_message, + context: 'Test', + error_backtrace: Gitlab::Profiler.clean_backtrace(backtrace) + ) + + expect(logger).to receive(:warn).with(expected_data) + + subject + end + end +end diff --git a/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb b/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb index 1b89c094a6b..6e6a8e14fc9 100644 --- a/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb +++ b/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb @@ -47,7 +47,7 @@ describe Gitlab::SidekiqLogging::StructuredLogger do end_payload.merge( 'message' => 'TestWorker JID-da883554ee4fe414012f5f42: fail: 0.0 sec', 'job_status' => 'fail', - 'error' => ArgumentError, + 'error_class' => 'ArgumentError', 'error_message' => 'some exception' ) end @@ -86,7 +86,6 @@ describe Gitlab::SidekiqLogging::StructuredLogger do it 'logs an exception in job' do Timecop.freeze(timestamp) do expect(logger).to receive(:info).with(start_payload) - # This excludes the exception_backtrace expect(logger).to receive(:warn).with(hash_including(exception_payload)) expect(subject).to receive(:log_job_start).and_call_original expect(subject).to receive(:log_job_done).and_call_original diff --git a/spec/lib/gitlab/tracking_spec.rb b/spec/lib/gitlab/tracking_spec.rb index 3cce82e522b..6891349a1dc 100644 --- a/spec/lib/gitlab/tracking_spec.rb +++ b/spec/lib/gitlab/tracking_spec.rb @@ -49,8 +49,8 @@ describe Gitlab::Tracking do it 'can track events' do tracker = double - expect(SnowplowTracker::Emitter).to receive(:new).with( - 'gitfoo.com' + expect(SnowplowTracker::AsyncEmitter).to receive(:new).with( + 'gitfoo.com', { protocol: 'https' } ).and_return('_emitter_') expect(SnowplowTracker::Tracker).to receive(:new).with( |