diff options
Diffstat (limited to 'spec/javascripts')
39 files changed, 1264 insertions, 523 deletions
diff --git a/spec/javascripts/.eslintrc b/spec/javascripts/.eslintrc deleted file mode 100644 index 9eb0e732572..00000000000 --- a/spec/javascripts/.eslintrc +++ /dev/null @@ -1,33 +0,0 @@ -{ - "env": { - "jasmine": true - }, - "extends": "plugin:jasmine/recommended", - "globals": { - "appendLoadFixtures": false, - "appendLoadStyleFixtures": false, - "appendSetFixtures": false, - "appendSetStyleFixtures": false, - "getJSONFixture": false, - "loadFixtures": false, - "loadJSONFixtures": false, - "loadStyleFixtures": false, - "preloadFixtures": false, - "preloadStyleFixtures": false, - "readFixtures": false, - "sandbox": false, - "setFixtures": false, - "setStyleFixtures": false, - "spyOnDependency": false, - "spyOnEvent": false, - "ClassSpecHelper": false - }, - "plugins": ["jasmine"], - "rules": { - "func-names": 0, - "jasmine/no-suite-dupes": [1, "branch"], - "jasmine/no-spec-dupes": [1, "branch"], - "no-console": 0, - "prefer-arrow-callback": 0 - } -} diff --git a/spec/javascripts/.eslintrc.yml b/spec/javascripts/.eslintrc.yml new file mode 100644 index 00000000000..8bceb2c50fc --- /dev/null +++ b/spec/javascripts/.eslintrc.yml @@ -0,0 +1,34 @@ +--- +env: + jasmine: true +extends: plugin:jasmine/recommended +globals: + appendLoadFixtures: false + appendLoadStyleFixtures: false + appendSetFixtures: false + appendSetStyleFixtures: false + getJSONFixture: false + loadFixtures: false + loadJSONFixtures: false + loadStyleFixtures: false + preloadFixtures: false + preloadStyleFixtures: false + readFixtures: false + sandbox: false + setFixtures: false + setStyleFixtures: false + spyOnDependency: false + spyOnEvent: false + ClassSpecHelper: false +plugins: + - jasmine +rules: + func-names: off + jasmine/no-suite-dupes: + - warn + - branch + jasmine/no-spec-dupes: + - warn + - branch + no-console: off + prefer-arrow-callback: off diff --git a/spec/javascripts/blob/notebook/index_spec.js b/spec/javascripts/blob/notebook/index_spec.js index a143fc827d5..80c09a544d6 100644 --- a/spec/javascripts/blob/notebook/index_spec.js +++ b/spec/javascripts/blob/notebook/index_spec.js @@ -84,9 +84,14 @@ describe('iPython notebook renderer', () => { describe('error in JSON response', () => { let mock; - beforeEach((done) => { + beforeEach(done => { mock = new MockAdapter(axios); - mock.onGet('/test').reply(() => Promise.reject({ status: 200, data: '{ "cells": [{"cell_type": "markdown"} }' })); + mock + .onGet('/test') + .reply(() => + // eslint-disable-next-line prefer-promise-reject-errors + Promise.reject({ status: 200, data: '{ "cells": [{"cell_type": "markdown"} }' }), + ); renderNotebook(); diff --git a/spec/javascripts/boards/board_blank_state_spec.js b/spec/javascripts/boards/board_blank_state_spec.js index 664ea202e93..89a4fae4b59 100644 --- a/spec/javascripts/boards/board_blank_state_spec.js +++ b/spec/javascripts/boards/board_blank_state_spec.js @@ -1,4 +1,3 @@ -/* global BoardService */ import Vue from 'vue'; import '~/boards/stores/boards_store'; import BoardBlankState from '~/boards/components/board_blank_state.vue'; diff --git a/spec/javascripts/boards/board_card_spec.js b/spec/javascripts/boards/board_card_spec.js index 13d607a06d2..9b4db774b63 100644 --- a/spec/javascripts/boards/board_card_spec.js +++ b/spec/javascripts/boards/board_card_spec.js @@ -1,7 +1,6 @@ /* global List */ /* global ListAssignee */ /* global ListLabel */ -/* global BoardService */ import Vue from 'vue'; import MockAdapter from 'axios-mock-adapter'; diff --git a/spec/javascripts/boards/board_list_spec.js b/spec/javascripts/boards/board_list_spec.js index c06b2f60813..de261d36c61 100644 --- a/spec/javascripts/boards/board_list_spec.js +++ b/spec/javascripts/boards/board_list_spec.js @@ -1,10 +1,9 @@ -/* global BoardService */ /* global List */ /* global ListIssue */ import Vue from 'vue'; import MockAdapter from 'axios-mock-adapter'; import axios from '~/lib/utils/axios_utils'; -import Sortable from 'vendor/Sortable'; +import Sortable from 'sortablejs'; import BoardList from '~/boards/components/board_list.vue'; import eventHub from '~/boards/eventhub'; import '~/boards/mixins/sortable_default_options'; diff --git a/spec/javascripts/boards/board_new_issue_spec.js b/spec/javascripts/boards/board_new_issue_spec.js index d5fbfdeaa91..ee37821ad08 100644 --- a/spec/javascripts/boards/board_new_issue_spec.js +++ b/spec/javascripts/boards/board_new_issue_spec.js @@ -1,4 +1,3 @@ -/* global BoardService */ /* global List */ import Vue from 'vue'; diff --git a/spec/javascripts/boards/boards_store_spec.js b/spec/javascripts/boards/boards_store_spec.js index 0cf9e4c9ba1..46fa10e1789 100644 --- a/spec/javascripts/boards/boards_store_spec.js +++ b/spec/javascripts/boards/boards_store_spec.js @@ -1,5 +1,4 @@ /* eslint-disable comma-dangle, one-var, no-unused-vars */ -/* global BoardService */ /* global ListIssue */ import Vue from 'vue'; diff --git a/spec/javascripts/boards/issue_spec.js b/spec/javascripts/boards/issue_spec.js index 4a11131b55c..d90f9a41231 100644 --- a/spec/javascripts/boards/issue_spec.js +++ b/spec/javascripts/boards/issue_spec.js @@ -1,5 +1,4 @@ /* eslint-disable comma-dangle */ -/* global BoardService */ /* global ListIssue */ import Vue from 'vue'; diff --git a/spec/javascripts/boards/list_spec.js b/spec/javascripts/boards/list_spec.js index d9a1d692949..d5d1139de15 100644 --- a/spec/javascripts/boards/list_spec.js +++ b/spec/javascripts/boards/list_spec.js @@ -1,5 +1,4 @@ /* eslint-disable comma-dangle */ -/* global BoardService */ /* global List */ /* global ListIssue */ diff --git a/spec/javascripts/clusters/clusters_bundle_spec.js b/spec/javascripts/clusters/clusters_bundle_spec.js index a5cd247b689..abe2954d506 100644 --- a/spec/javascripts/clusters/clusters_bundle_spec.js +++ b/spec/javascripts/clusters/clusters_bundle_spec.js @@ -207,11 +207,11 @@ describe('Clusters', () => { spyOn(cluster.service, 'installApplication').and.returnValue(Promise.resolve()); expect(cluster.store.state.applications.helm.requestStatus).toEqual(null); - cluster.installApplication('helm'); + cluster.installApplication({ id: 'helm' }); expect(cluster.store.state.applications.helm.requestStatus).toEqual(REQUEST_LOADING); expect(cluster.store.state.applications.helm.requestReason).toEqual(null); - expect(cluster.service.installApplication).toHaveBeenCalledWith('helm'); + expect(cluster.service.installApplication).toHaveBeenCalledWith('helm', undefined); getSetTimeoutPromise() .then(() => { @@ -226,11 +226,11 @@ describe('Clusters', () => { spyOn(cluster.service, 'installApplication').and.returnValue(Promise.resolve()); expect(cluster.store.state.applications.ingress.requestStatus).toEqual(null); - cluster.installApplication('ingress'); + cluster.installApplication({ id: 'ingress' }); expect(cluster.store.state.applications.ingress.requestStatus).toEqual(REQUEST_LOADING); expect(cluster.store.state.applications.ingress.requestReason).toEqual(null); - expect(cluster.service.installApplication).toHaveBeenCalledWith('ingress'); + expect(cluster.service.installApplication).toHaveBeenCalledWith('ingress', undefined); getSetTimeoutPromise() .then(() => { @@ -245,11 +245,11 @@ describe('Clusters', () => { spyOn(cluster.service, 'installApplication').and.returnValue(Promise.resolve()); expect(cluster.store.state.applications.runner.requestStatus).toEqual(null); - cluster.installApplication('runner'); + cluster.installApplication({ id: 'runner' }); expect(cluster.store.state.applications.runner.requestStatus).toEqual(REQUEST_LOADING); expect(cluster.store.state.applications.runner.requestReason).toEqual(null); - expect(cluster.service.installApplication).toHaveBeenCalledWith('runner'); + expect(cluster.service.installApplication).toHaveBeenCalledWith('runner', undefined); getSetTimeoutPromise() .then(() => { @@ -260,11 +260,29 @@ describe('Clusters', () => { .catch(done.fail); }); + it('tries to install jupyter', (done) => { + spyOn(cluster.service, 'installApplication').and.returnValue(Promise.resolve()); + expect(cluster.store.state.applications.jupyter.requestStatus).toEqual(null); + cluster.installApplication({ id: 'jupyter', params: { hostname: cluster.store.state.applications.jupyter.hostname } }); + + expect(cluster.store.state.applications.jupyter.requestStatus).toEqual(REQUEST_LOADING); + expect(cluster.store.state.applications.jupyter.requestReason).toEqual(null); + expect(cluster.service.installApplication).toHaveBeenCalledWith('jupyter', { hostname: cluster.store.state.applications.jupyter.hostname }); + + getSetTimeoutPromise() + .then(() => { + expect(cluster.store.state.applications.jupyter.requestStatus).toEqual(REQUEST_SUCCESS); + expect(cluster.store.state.applications.jupyter.requestReason).toEqual(null); + }) + .then(done) + .catch(done.fail); + }); + it('sets error request status when the request fails', (done) => { spyOn(cluster.service, 'installApplication').and.returnValue(Promise.reject(new Error('STUBBED ERROR'))); expect(cluster.store.state.applications.helm.requestStatus).toEqual(null); - cluster.installApplication('helm'); + cluster.installApplication({ id: 'helm' }); expect(cluster.store.state.applications.helm.requestStatus).toEqual(REQUEST_LOADING); expect(cluster.store.state.applications.helm.requestReason).toEqual(null); diff --git a/spec/javascripts/clusters/components/application_row_spec.js b/spec/javascripts/clusters/components/application_row_spec.js index 2c4707bb856..c83cbe90a57 100644 --- a/spec/javascripts/clusters/components/application_row_spec.js +++ b/spec/javascripts/clusters/components/application_row_spec.js @@ -174,7 +174,27 @@ describe('Application Row', () => { installButton.click(); - expect(eventHub.$emit).toHaveBeenCalledWith('installApplication', DEFAULT_APPLICATION_STATE.id); + expect(eventHub.$emit).toHaveBeenCalledWith('installApplication', { + id: DEFAULT_APPLICATION_STATE.id, + params: {}, + }); + }); + + it('clicking install button when installApplicationRequestParams are provided emits event', () => { + spyOn(eventHub, '$emit'); + vm = mountComponent(ApplicationRow, { + ...DEFAULT_APPLICATION_STATE, + status: APPLICATION_INSTALLABLE, + installApplicationRequestParams: { hostname: 'jupyter' }, + }); + const installButton = vm.$el.querySelector('.js-cluster-application-install-button'); + + installButton.click(); + + expect(eventHub.$emit).toHaveBeenCalledWith('installApplication', { + id: DEFAULT_APPLICATION_STATE.id, + params: { hostname: 'jupyter' }, + }); }); it('clicking disabled install button emits nothing', () => { diff --git a/spec/javascripts/clusters/components/applications_spec.js b/spec/javascripts/clusters/components/applications_spec.js index d546543d273..a70138c7eee 100644 --- a/spec/javascripts/clusters/components/applications_spec.js +++ b/spec/javascripts/clusters/components/applications_spec.js @@ -22,6 +22,7 @@ describe('Applications', () => { ingress: { title: 'Ingress' }, runner: { title: 'GitLab Runner' }, prometheus: { title: 'Prometheus' }, + jupyter: { title: 'JupyterHub' }, }, }); }); @@ -41,6 +42,10 @@ describe('Applications', () => { it('renders a row for GitLab Runner', () => { expect(vm.$el.querySelector('.js-cluster-application-row-runner')).toBeDefined(); }); + + it('renders a row for Jupyter', () => { + expect(vm.$el.querySelector('.js-cluster-application-row-jupyter')).not.toBe(null); + }); }); describe('Ingress application', () => { @@ -57,12 +62,11 @@ describe('Applications', () => { helm: { title: 'Helm Tiller' }, runner: { title: 'GitLab Runner' }, prometheus: { title: 'Prometheus' }, + jupyter: { title: 'JupyterHub', hostname: '' }, }, }); - expect( - vm.$el.querySelector('.js-ip-address').value, - ).toEqual('0.0.0.0'); + expect(vm.$el.querySelector('.js-ip-address').value).toEqual('0.0.0.0'); expect( vm.$el.querySelector('.js-clipboard-btn').getAttribute('data-clipboard-text'), @@ -81,12 +85,11 @@ describe('Applications', () => { helm: { title: 'Helm Tiller' }, runner: { title: 'GitLab Runner' }, prometheus: { title: 'Prometheus' }, + jupyter: { title: 'JupyterHub', hostname: '' }, }, }); - expect( - vm.$el.querySelector('.js-ip-address').value, - ).toEqual('?'); + expect(vm.$el.querySelector('.js-ip-address').value).toEqual('?'); expect(vm.$el.querySelector('.js-no-ip-message')).not.toBe(null); }); @@ -101,6 +104,7 @@ describe('Applications', () => { ingress: { title: 'Ingress' }, runner: { title: 'GitLab Runner' }, prometheus: { title: 'Prometheus' }, + jupyter: { title: 'JupyterHub', hostname: '' }, }, }); @@ -108,5 +112,83 @@ describe('Applications', () => { expect(vm.$el.querySelector('.js-ip-address')).toBe(null); }); }); + + describe('Jupyter application', () => { + describe('with ingress installed with ip & jupyter installable', () => { + it('renders hostname active input', () => { + vm = mountComponent(Applications, { + applications: { + helm: { title: 'Helm Tiller', status: 'installed' }, + ingress: { title: 'Ingress', status: 'installed', externalIp: '1.1.1.1' }, + runner: { title: 'GitLab Runner' }, + prometheus: { title: 'Prometheus' }, + jupyter: { title: 'JupyterHub', hostname: '', status: 'installable' }, + }, + }); + + expect(vm.$el.querySelector('.js-hostname').getAttribute('readonly')).toEqual(null); + }); + }); + + describe('with ingress installed without external ip', () => { + it('does not render hostname input', () => { + vm = mountComponent(Applications, { + applications: { + helm: { title: 'Helm Tiller', status: 'installed' }, + ingress: { title: 'Ingress', status: 'installed' }, + runner: { title: 'GitLab Runner' }, + prometheus: { title: 'Prometheus' }, + jupyter: { title: 'JupyterHub', hostname: '', status: 'installable' }, + }, + }); + + expect(vm.$el.querySelector('.js-hostname')).toBe(null); + }); + }); + + describe('with ingress & jupyter installed', () => { + it('renders readonly input', () => { + vm = mountComponent(Applications, { + applications: { + helm: { title: 'Helm Tiller', status: 'installed' }, + ingress: { title: 'Ingress', status: 'installed', externalIp: '1.1.1.1' }, + runner: { title: 'GitLab Runner' }, + prometheus: { title: 'Prometheus' }, + jupyter: { title: 'JupyterHub', status: 'installed', hostname: '' }, + }, + }); + + expect(vm.$el.querySelector('.js-hostname').getAttribute('readonly')).toEqual('readonly'); + }); + }); + + describe('without ingress installed', () => { + beforeEach(() => { + vm = mountComponent(Applications, { + applications: { + helm: { title: 'Helm Tiller' }, + ingress: { title: 'Ingress' }, + runner: { title: 'GitLab Runner' }, + prometheus: { title: 'Prometheus' }, + jupyter: { title: 'JupyterHub', status: 'not_installable' }, + }, + }); + }); + + it('does not render input', () => { + expect(vm.$el.querySelector('.js-hostname')).toBe(null); + }); + + it('renders disabled install button', () => { + expect( + vm.$el + .querySelector( + '.js-cluster-application-row-jupyter .js-cluster-application-install-button', + ) + .getAttribute('disabled'), + ).toEqual('disabled'); + }); + }); + }); }); }); diff --git a/spec/javascripts/clusters/services/mock_data.js b/spec/javascripts/clusters/services/mock_data.js index 6ae7a792329..b2b0ebf840b 100644 --- a/spec/javascripts/clusters/services/mock_data.js +++ b/spec/javascripts/clusters/services/mock_data.js @@ -1,4 +1,5 @@ import { + APPLICATION_INSTALLED, APPLICATION_INSTALLABLE, APPLICATION_INSTALLING, APPLICATION_ERROR, @@ -28,6 +29,39 @@ const CLUSTERS_MOCK_DATA = { name: 'prometheus', status: APPLICATION_ERROR, status_reason: 'Cannot connect', + }, { + name: 'jupyter', + status: APPLICATION_INSTALLING, + status_reason: 'Cannot connect', + }], + }, + }, + '/gitlab-org/gitlab-shell/clusters/2/status.json': { + data: { + status: 'errored', + status_reason: 'Failed to request to CloudPlatform.', + applications: [{ + name: 'helm', + status: APPLICATION_INSTALLED, + status_reason: null, + }, { + name: 'ingress', + status: APPLICATION_INSTALLED, + status_reason: 'Cannot connect', + external_ip: '1.1.1.1', + }, { + name: 'runner', + status: APPLICATION_INSTALLING, + status_reason: null, + }, + { + name: 'prometheus', + status: APPLICATION_ERROR, + status_reason: 'Cannot connect', + }, { + name: 'jupyter', + status: APPLICATION_INSTALLABLE, + status_reason: 'Cannot connect', }], }, }, @@ -37,6 +71,7 @@ const CLUSTERS_MOCK_DATA = { '/gitlab-org/gitlab-shell/clusters/1/applications/ingress': { }, '/gitlab-org/gitlab-shell/clusters/1/applications/runner': { }, '/gitlab-org/gitlab-shell/clusters/1/applications/prometheus': { }, + '/gitlab-org/gitlab-shell/clusters/1/applications/jupyter': { }, }, }; diff --git a/spec/javascripts/clusters/stores/clusters_store_spec.js b/spec/javascripts/clusters/stores/clusters_store_spec.js index 8028faf2f02..6854b016852 100644 --- a/spec/javascripts/clusters/stores/clusters_store_spec.js +++ b/spec/javascripts/clusters/stores/clusters_store_spec.js @@ -91,8 +91,26 @@ describe('Clusters Store', () => { requestStatus: null, requestReason: null, }, + jupyter: { + title: 'JupyterHub', + status: mockResponseData.applications[4].status, + statusReason: mockResponseData.applications[4].status_reason, + requestStatus: null, + requestReason: null, + hostname: '', + }, }, }); }); + + it('sets default hostname for jupyter when ingress has a ip address', () => { + const mockResponseData = CLUSTERS_MOCK_DATA.GET['/gitlab-org/gitlab-shell/clusters/2/status.json'].data; + + store.updateStateFromServer(mockResponseData); + + expect( + store.state.applications.jupyter.hostname, + ).toEqual(`jupyter.${store.state.applications.ingress.externalIp}.xip.io`); + }); }); }); diff --git a/spec/javascripts/commit/commit_pipeline_status_component_spec.js b/spec/javascripts/commit/commit_pipeline_status_component_spec.js index 421fe62a1e7..d3776d0c3cf 100644 --- a/spec/javascripts/commit/commit_pipeline_status_component_spec.js +++ b/spec/javascripts/commit/commit_pipeline_status_component_spec.js @@ -75,10 +75,7 @@ describe('Commit pipeline status component', () => { describe('When polling data was not succesful', () => { beforeEach(() => { mock = new MockAdapter(axios); - mock.onGet('/dummy/endpoint').reply(() => { - const res = Promise.reject([502, { }]); - return res; - }); + mock.onGet('/dummy/endpoint').reply(502, {}); vm = new Component({ props: { endpoint: '/dummy/endpoint', diff --git a/spec/javascripts/helpers/vue_mount_component_helper.js b/spec/javascripts/helpers/vue_mount_component_helper.js index a34a1add4e0..5ba17ecf5b5 100644 --- a/spec/javascripts/helpers/vue_mount_component_helper.js +++ b/spec/javascripts/helpers/vue_mount_component_helper.js @@ -1,30 +1,18 @@ -import Vue from 'vue'; - -const mountComponent = (Component, props = {}, el = null) => new Component({ - propsData: props, -}).$mount(el); - -export const createComponentWithStore = (Component, store, propsData = {}) => new Component({ - store, - propsData, -}); +const mountComponent = (Component, props = {}, el = null) => + new Component({ + propsData: props, + }).$mount(el); -export const createComponentWithMixin = (mixins = [], state = {}, props = {}, template = '<div></div>') => { - const Component = Vue.extend({ - template, - mixins, - data() { - return props; - }, +export const createComponentWithStore = (Component, store, propsData = {}) => + new Component({ + store, + propsData, }); - return mountComponent(Component, props); -}; - export const mountComponentWithStore = (Component, { el, props, store }) => new Component({ store, - propsData: props || { }, + propsData: props || {}, }).$mount(el); export default mountComponent; diff --git a/spec/javascripts/ide/components/external_link_spec.js b/spec/javascripts/ide/components/external_link_spec.js new file mode 100644 index 00000000000..b3d94c041fa --- /dev/null +++ b/spec/javascripts/ide/components/external_link_spec.js @@ -0,0 +1,35 @@ +import Vue from 'vue'; +import externalLink from '~/ide/components/external_link.vue'; +import createVueComponent from '../../helpers/vue_mount_component_helper'; +import { file } from '../helpers'; + +describe('ExternalLink', () => { + const activeFile = file(); + let vm; + + function createComponent() { + const ExternalLink = Vue.extend(externalLink); + + activeFile.permalink = 'test'; + + return createVueComponent(ExternalLink, { + file: activeFile, + }); + } + + afterEach(() => { + vm.$destroy(); + }); + + it('renders the external link with the correct href', done => { + activeFile.binary = true; + vm = createComponent(); + + vm.$nextTick(() => { + const openLink = vm.$el.querySelector('a'); + + expect(openLink.href).toMatch(`/${activeFile.permalink}`); + done(); + }); + }); +}); diff --git a/spec/javascripts/ide/components/ide_file_buttons_spec.js b/spec/javascripts/ide/components/ide_file_buttons_spec.js deleted file mode 100644 index 8ac8d1b2acf..00000000000 --- a/spec/javascripts/ide/components/ide_file_buttons_spec.js +++ /dev/null @@ -1,61 +0,0 @@ -import Vue from 'vue'; -import repoFileButtons from '~/ide/components/ide_file_buttons.vue'; -import createVueComponent from '../../helpers/vue_mount_component_helper'; -import { file } from '../helpers'; - -describe('RepoFileButtons', () => { - const activeFile = file(); - let vm; - - function createComponent() { - const RepoFileButtons = Vue.extend(repoFileButtons); - - activeFile.rawPath = 'test'; - activeFile.blamePath = 'test'; - activeFile.commitsPath = 'test'; - - return createVueComponent(RepoFileButtons, { - file: activeFile, - }); - } - - afterEach(() => { - vm.$destroy(); - }); - - it('renders Raw, Blame, History and Permalink', done => { - vm = createComponent(); - - vm.$nextTick(() => { - const raw = vm.$el.querySelector('.raw'); - const blame = vm.$el.querySelector('.blame'); - const history = vm.$el.querySelector('.history'); - - expect(raw.href).toMatch(`/${activeFile.rawPath}`); - expect(raw.getAttribute('data-original-title')).toEqual('Raw'); - expect(blame.href).toMatch(`/${activeFile.blamePath}`); - expect(blame.getAttribute('data-original-title')).toEqual('Blame'); - expect(history.href).toMatch(`/${activeFile.commitsPath}`); - expect(history.getAttribute('data-original-title')).toEqual('History'); - expect(vm.$el.querySelector('.permalink').getAttribute('data-original-title')).toEqual( - 'Permalink', - ); - - done(); - }); - }); - - it('renders Download', done => { - activeFile.binary = true; - vm = createComponent(); - - vm.$nextTick(() => { - const raw = vm.$el.querySelector('.raw'); - - expect(raw.href).toMatch(`/${activeFile.rawPath}`); - expect(raw.getAttribute('data-original-title')).toEqual('Download'); - - done(); - }); - }); -}); diff --git a/spec/javascripts/ide/components/jobs/item_spec.js b/spec/javascripts/ide/components/jobs/item_spec.js new file mode 100644 index 00000000000..7c1dd4e475c --- /dev/null +++ b/spec/javascripts/ide/components/jobs/item_spec.js @@ -0,0 +1,29 @@ +import Vue from 'vue'; +import JobItem from '~/ide/components/jobs/item.vue'; +import mountComponent from '../../../helpers/vue_mount_component_helper'; +import { jobs } from '../../mock_data'; + +describe('IDE jobs item', () => { + const Component = Vue.extend(JobItem); + const job = jobs[0]; + let vm; + + beforeEach(() => { + vm = mountComponent(Component, { + job, + }); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('renders job details', () => { + expect(vm.$el.textContent).toContain(job.name); + expect(vm.$el.textContent).toContain(`#${job.id}`); + }); + + it('renders CI icon', () => { + expect(vm.$el.querySelector('.ic-status_passed_borderless')).not.toBe(null); + }); +}); diff --git a/spec/javascripts/ide/components/jobs/list_spec.js b/spec/javascripts/ide/components/jobs/list_spec.js new file mode 100644 index 00000000000..b24853c56fa --- /dev/null +++ b/spec/javascripts/ide/components/jobs/list_spec.js @@ -0,0 +1,67 @@ +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/jobs/stage_spec.js b/spec/javascripts/ide/components/jobs/stage_spec.js new file mode 100644 index 00000000000..fc3831f2d05 --- /dev/null +++ b/spec/javascripts/ide/components/jobs/stage_spec.js @@ -0,0 +1,95 @@ +import Vue from 'vue'; +import Stage from '~/ide/components/jobs/stage.vue'; +import { stages, jobs } from '../../mock_data'; + +describe('IDE pipeline stage', () => { + const Component = Vue.extend(Stage); + let vm; + let stage; + + beforeEach(() => { + stage = { + ...stages[0], + id: 0, + dropdownPath: stages[0].dropdown_path, + jobs: [...jobs], + isLoading: false, + isCollapsed: false, + }; + + vm = new Component({ + propsData: { stage }, + }); + + spyOn(vm, '$emit'); + + vm.$mount(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('emits fetch event when mounted', () => { + expect(vm.$emit).toHaveBeenCalledWith('fetch', vm.stage); + }); + + it('renders stages details', () => { + expect(vm.$el.textContent).toContain(vm.stage.name); + }); + + it('renders CI icon', () => { + expect(vm.$el.querySelector('.ic-status_failed')).not.toBe(null); + }); + + describe('collapsed', () => { + it('emits event when clicking header', done => { + vm.$el.querySelector('.card-header').click(); + + vm.$nextTick(() => { + expect(vm.$emit).toHaveBeenCalledWith('toggleCollapsed', vm.stage.id); + + done(); + }); + }); + + it('toggles collapse status when collapsed', done => { + vm.stage.isCollapsed = true; + + vm.$nextTick(() => { + expect(vm.$el.querySelector('.card-body').style.display).toBe('none'); + + done(); + }); + }); + + it('sets border bottom class when collapsed', done => { + vm.stage.isCollapsed = true; + + vm.$nextTick(() => { + expect(vm.$el.querySelector('.card-header').classList).toContain('border-bottom-0'); + + done(); + }); + }); + }); + + it('renders jobs count', () => { + expect(vm.$el.querySelector('.badge').textContent).toContain('4'); + }); + + it('renders loading icon when no jobs and isLoading is true', done => { + vm.stage.isLoading = true; + vm.stage.jobs = []; + + vm.$nextTick(() => { + expect(vm.$el.querySelector('.loading-container')).not.toBe(null); + + done(); + }); + }); + + it('renders list of jobs', () => { + expect(vm.$el.querySelectorAll('.ide-job-item').length).toBe(4); + }); +}); diff --git a/spec/javascripts/ide/components/pipelines/list_spec.js b/spec/javascripts/ide/components/pipelines/list_spec.js new file mode 100644 index 00000000000..2bb5aa08c3b --- /dev/null +++ b/spec/javascripts/ide/components/pipelines/list_spec.js @@ -0,0 +1,117 @@ +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; + + 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(() => { + vm.$store.dispatch('pipelines/stopPipelinePolling'); + vm.$store.dispatch('pipelines/clearEtagPoll'); + + vm.$destroy(); + mock.restore(); + }); + + 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 = false; + + vm.$nextTick(() => { + expect(vm.$el.querySelector('.empty-state')).not.toBe(null); + + done(); + }); + }); + }); + + describe('loading state', () => { + it('renders loading state when there is no latest pipeline', done => { + vm.$store.state.pipelines.latestPipeline = null; + vm.$store.state.pipelines.isLoadingPipeline = true; + + vm.$nextTick(() => { + expect(vm.$el.querySelector('.loading-container')).not.toBe(null); + + done(); + }); + }); + }); +}); diff --git a/spec/javascripts/ide/helpers.js b/spec/javascripts/ide/helpers.js index 98db6defc7a..9312e17704e 100644 --- a/spec/javascripts/ide/helpers.js +++ b/spec/javascripts/ide/helpers.js @@ -1,11 +1,15 @@ import { decorateData } from '~/ide/stores/utils'; import state from '~/ide/stores/state'; import commitState from '~/ide/stores/modules/commit/state'; +import mergeRequestsState from '~/ide/stores/modules/merge_requests/state'; +import pipelinesState from '~/ide/stores/modules/pipelines/state'; export const resetStore = store => { const newState = { ...state(), commit: commitState(), + mergeRequests: mergeRequestsState(), + pipelines: pipelinesState(), }; store.replaceState(newState); }; diff --git a/spec/javascripts/ide/mock_data.js b/spec/javascripts/ide/mock_data.js index c68ae050641..dcf857f7e04 100644 --- a/spec/javascripts/ide/mock_data.js +++ b/spec/javascripts/ide/mock_data.js @@ -19,13 +19,48 @@ export const pipelines = [ id: 1, ref: 'master', sha: '123', - status: 'failed', + details: { + status: { + icon: 'status_failed', + group: 'failed', + text: 'Failed', + }, + }, + commit: { id: '123' }, }, { id: 2, ref: 'master', sha: '213', - status: 'success', + details: { + status: { + icon: 'status_failed', + group: 'failed', + text: 'Failed', + }, + }, + commit: { id: '213' }, + }, +]; + +export const stages = [ + { + dropdown_path: `${gl.TEST_HOST}/testing`, + name: 'build', + status: { + icon: 'status_failed', + group: 'failed', + text: 'failed', + }, + }, + { + dropdown_path: 'testing', + name: 'test', + status: { + icon: 'status_failed', + group: 'failed', + text: 'failed', + }, }, ]; @@ -33,28 +68,44 @@ export const jobs = [ { id: 1, name: 'test', - status: 'failed', + path: 'testing', + status: { + icon: 'status_passed', + text: 'passed', + }, stage: 'test', duration: 1, }, { id: 2, name: 'test 2', - status: 'failed', + path: 'testing2', + status: { + icon: 'status_passed', + text: 'passed', + }, stage: 'test', duration: 1, }, { id: 3, name: 'test 3', - status: 'failed', + path: 'testing3', + status: { + icon: 'status_passed', + text: 'passed', + }, stage: 'test', duration: 1, }, { id: 4, - name: 'test 3', - status: 'failed', + name: 'test 4', + path: 'testing4', + status: { + icon: 'status_failed', + text: 'failed', + }, stage: 'build', duration: 1, }, @@ -68,14 +119,16 @@ export const fullPipelinesResponse = { pipelines: [ { id: '51', + path: 'test', commit: { - id: 'xxxxxxxxxxxxxxxxxxxx', + id: '123', }, details: { status: { icon: 'status_failed', text: 'failed', }, + stages: [...stages], }, }, { @@ -88,8 +141,19 @@ export const fullPipelinesResponse = { icon: 'status_passed', text: 'passed', }, + stages: [...stages], }, }, ], }, }; + +export const mergeRequests = [ + { + id: 1, + iid: 1, + title: 'Test merge request', + project_id: 1, + web_url: `${gl.TEST_HOST}/namespace/project-path/merge_requests/1`, + }, +]; diff --git a/spec/javascripts/ide/stores/actions/project_spec.js b/spec/javascripts/ide/stores/actions/project_spec.js index 8e078ae7138..d71fc0e035e 100644 --- a/spec/javascripts/ide/stores/actions/project_spec.js +++ b/spec/javascripts/ide/stores/actions/project_spec.js @@ -1,31 +1,10 @@ -import Visibility from 'visibilityjs'; -import MockAdapter from 'axios-mock-adapter'; -import { refreshLastCommitData, pollSuccessCallBack } from '~/ide/stores/actions'; +import { refreshLastCommitData } from '~/ide/stores/actions'; import store from '~/ide/stores'; import service from '~/ide/services'; -import axios from '~/lib/utils/axios_utils'; -import { fullPipelinesResponse } from '../../mock_data'; import { resetStore } from '../../helpers'; import testAction from '../../../helpers/vuex_action_helper'; describe('IDE store project actions', () => { - const setProjectState = () => { - store.state.currentProjectId = 'abc/def'; - store.state.currentBranchId = 'master'; - store.state.projects['abc/def'] = { - id: 4, - path_with_namespace: 'abc/def', - branches: { - master: { - commit: { - id: 'abc123def456ghi789jkl', - title: 'example', - }, - }, - }, - }; - }; - beforeEach(() => { store.state.projects['abc/def'] = {}; }); @@ -101,92 +80,4 @@ describe('IDE store project actions', () => { ); }); }); - - describe('pipelinePoll', () => { - let mock; - - beforeEach(() => { - setProjectState(); - jasmine.clock().install(); - mock = new MockAdapter(axios); - mock - .onGet('/abc/def/commit/abc123def456ghi789jkl/pipelines') - .reply(200, { data: { foo: 'bar' } }, { 'poll-interval': '10000' }); - }); - - afterEach(() => { - jasmine.clock().uninstall(); - mock.restore(); - store.dispatch('stopPipelinePolling'); - }); - - it('calls service periodically', done => { - spyOn(axios, 'get').and.callThrough(); - spyOn(Visibility, 'hidden').and.returnValue(false); - - store - .dispatch('pipelinePoll') - .then(() => { - jasmine.clock().tick(1000); - - expect(axios.get).toHaveBeenCalled(); - expect(axios.get.calls.count()).toBe(1); - }) - .then(() => new Promise(resolve => requestAnimationFrame(resolve))) - .then(() => { - jasmine.clock().tick(10000); - expect(axios.get.calls.count()).toBe(2); - }) - .then(() => new Promise(resolve => requestAnimationFrame(resolve))) - .then(() => { - jasmine.clock().tick(10000); - expect(axios.get.calls.count()).toBe(3); - }) - .then(() => new Promise(resolve => requestAnimationFrame(resolve))) - .then(() => { - jasmine.clock().tick(10000); - expect(axios.get.calls.count()).toBe(4); - }) - - .then(done) - .catch(done.fail); - }); - }); - - describe('pollSuccessCallBack', () => { - beforeEach(() => { - setProjectState(); - }); - - it('commits correct pipeline', done => { - testAction( - pollSuccessCallBack, - fullPipelinesResponse, - store.state, - [ - { - type: 'SET_LAST_COMMIT_PIPELINE', - payload: { - projectId: 'abc/def', - branchId: 'master', - pipeline: { - id: '50', - commit: { - id: 'abc123def456ghi789jkl', - }, - details: { - status: { - icon: 'status_passed', - text: 'passed', - }, - }, - }, - }, - }, - ], // mutations - [], // action - done, - ); - }); - }); }); diff --git a/spec/javascripts/ide/stores/modules/merge_requests/actions_spec.js b/spec/javascripts/ide/stores/modules/merge_requests/actions_spec.js new file mode 100644 index 00000000000..b571cfb963a --- /dev/null +++ b/spec/javascripts/ide/stores/modules/merge_requests/actions_spec.js @@ -0,0 +1,182 @@ +import MockAdapter from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; +import state from '~/ide/stores/modules/merge_requests/state'; +import * as types from '~/ide/stores/modules/merge_requests/mutation_types'; +import actions, { + requestMergeRequests, + receiveMergeRequestsError, + receiveMergeRequestsSuccess, + fetchMergeRequests, + resetMergeRequests, +} from '~/ide/stores/modules/merge_requests/actions'; +import { mergeRequests } from '../../../mock_data'; +import testAction from '../../../../helpers/vuex_action_helper'; + +describe('IDE merge requests actions', () => { + let mockedState; + let mock; + + beforeEach(() => { + mockedState = state(); + mock = new MockAdapter(axios); + }); + + afterEach(() => { + mock.restore(); + }); + + describe('requestMergeRequests', () => { + it('should should commit request', done => { + testAction( + requestMergeRequests, + null, + mockedState, + [{ type: types.REQUEST_MERGE_REQUESTS }], + [], + done, + ); + }); + }); + + describe('receiveMergeRequestsError', () => { + let flashSpy; + + beforeEach(() => { + flashSpy = spyOnDependency(actions, 'flash'); + }); + + it('should should commit error', done => { + testAction( + receiveMergeRequestsError, + null, + mockedState, + [{ type: types.RECEIVE_MERGE_REQUESTS_ERROR }], + [], + done, + ); + }); + + it('creates flash message', () => { + receiveMergeRequestsError({ commit() {} }); + + expect(flashSpy).toHaveBeenCalled(); + }); + }); + + describe('receiveMergeRequestsSuccess', () => { + it('should commit received data', done => { + testAction( + receiveMergeRequestsSuccess, + 'data', + mockedState, + [{ type: types.RECEIVE_MERGE_REQUESTS_SUCCESS, payload: 'data' }], + [], + done, + ); + }); + }); + + describe('fetchMergeRequests', () => { + beforeEach(() => { + gon.api_version = 'v4'; + }); + + describe('success', () => { + beforeEach(() => { + mock.onGet(/\/api\/v4\/merge_requests(.*)$/).replyOnce(200, mergeRequests); + }); + + it('calls API with params from state', () => { + const apiSpy = spyOn(axios, 'get').and.callThrough(); + + fetchMergeRequests({ dispatch() {}, state: mockedState }); + + expect(apiSpy).toHaveBeenCalledWith(jasmine.anything(), { + params: { + scope: 'assigned-to-me', + state: 'opened', + search: '', + }, + }); + }); + + it('calls API with search', () => { + const apiSpy = spyOn(axios, 'get').and.callThrough(); + + fetchMergeRequests({ dispatch() {}, state: mockedState }, 'testing search'); + + expect(apiSpy).toHaveBeenCalledWith(jasmine.anything(), { + params: { + scope: 'assigned-to-me', + state: 'opened', + search: 'testing search', + }, + }); + }); + + it('dispatches request', done => { + testAction( + fetchMergeRequests, + null, + mockedState, + [], + [ + { type: 'requestMergeRequests' }, + { type: 'resetMergeRequests' }, + { type: 'receiveMergeRequestsSuccess' }, + ], + done, + ); + }); + + it('dispatches success with received data', done => { + testAction( + fetchMergeRequests, + null, + mockedState, + [], + [ + { type: 'requestMergeRequests' }, + { type: 'resetMergeRequests' }, + { type: 'receiveMergeRequestsSuccess', payload: mergeRequests }, + ], + done, + ); + }); + }); + + describe('error', () => { + beforeEach(() => { + mock.onGet(/\/api\/v4\/merge_requests(.*)$/).replyOnce(500); + }); + + it('dispatches error', done => { + testAction( + fetchMergeRequests, + null, + mockedState, + [], + [ + { type: 'requestMergeRequests' }, + { type: 'resetMergeRequests' }, + { type: 'receiveMergeRequestsError' }, + ], + done, + ); + }); + }); + }); + + describe('resetMergeRequests', () => { + it('commits reset', done => { + testAction( + resetMergeRequests, + null, + mockedState, + [{ type: types.RESET_MERGE_REQUESTS }], + [], + done, + ); + }); + }); +}); diff --git a/spec/javascripts/ide/stores/modules/merge_requests/mutations_spec.js b/spec/javascripts/ide/stores/modules/merge_requests/mutations_spec.js new file mode 100644 index 00000000000..664d3914564 --- /dev/null +++ b/spec/javascripts/ide/stores/modules/merge_requests/mutations_spec.js @@ -0,0 +1,55 @@ +import state from '~/ide/stores/modules/merge_requests/state'; +import mutations from '~/ide/stores/modules/merge_requests/mutations'; +import * as types from '~/ide/stores/modules/merge_requests/mutation_types'; +import { mergeRequests } from '../../../mock_data'; + +describe('IDE merge requests mutations', () => { + let mockedState; + + beforeEach(() => { + mockedState = state(); + }); + + describe(types.REQUEST_MERGE_REQUESTS, () => { + it('sets loading to true', () => { + mutations[types.REQUEST_MERGE_REQUESTS](mockedState); + + expect(mockedState.isLoading).toBe(true); + }); + }); + + describe(types.RECEIVE_MERGE_REQUESTS_ERROR, () => { + it('sets loading to false', () => { + mutations[types.RECEIVE_MERGE_REQUESTS_ERROR](mockedState); + + expect(mockedState.isLoading).toBe(false); + }); + }); + + describe(types.RECEIVE_MERGE_REQUESTS_SUCCESS, () => { + it('sets merge requests', () => { + gon.gitlab_url = gl.TEST_HOST; + mutations[types.RECEIVE_MERGE_REQUESTS_SUCCESS](mockedState, mergeRequests); + + expect(mockedState.mergeRequests).toEqual([ + { + id: 1, + iid: 1, + title: 'Test merge request', + projectId: 1, + projectPathWithNamespace: 'namespace/project-path', + }, + ]); + }); + }); + + describe(types.RESET_MERGE_REQUESTS, () => { + it('clears merge request array', () => { + mockedState.mergeRequests = ['test']; + + mutations[types.RESET_MERGE_REQUESTS](mockedState); + + expect(mockedState.mergeRequests).toEqual([]); + }); + }); +}); diff --git a/spec/javascripts/ide/stores/modules/pipelines/actions_spec.js b/spec/javascripts/ide/stores/modules/pipelines/actions_spec.js index 85fbcf8084b..f26eaf9c81f 100644 --- a/spec/javascripts/ide/stores/modules/pipelines/actions_spec.js +++ b/spec/javascripts/ide/stores/modules/pipelines/actions_spec.js @@ -1,3 +1,4 @@ +import Visibility from 'visibilityjs'; import MockAdapter from 'axios-mock-adapter'; import axios from '~/lib/utils/axios_utils'; import actions, { @@ -5,10 +6,13 @@ import actions, { receiveLatestPipelineError, receiveLatestPipelineSuccess, fetchLatestPipeline, + stopPipelinePolling, + clearEtagPoll, requestJobs, receiveJobsError, receiveJobsSuccess, fetchJobs, + toggleStageCollapsed, } from '~/ide/stores/modules/pipelines/actions'; import state from '~/ide/stores/modules/pipelines/state'; import * as types from '~/ide/stores/modules/pipelines/mutation_types'; @@ -51,7 +55,7 @@ describe('IDE pipelines actions', () => { null, mockedState, [{ type: types.RECEIVE_LASTEST_PIPELINE_ERROR }], - [], + [{ type: 'stopPipelinePolling' }], done, ); }); @@ -59,91 +63,128 @@ describe('IDE pipelines actions', () => { it('creates flash message', () => { const flashSpy = spyOnDependency(actions, 'flash'); - receiveLatestPipelineError({ commit() {} }); + receiveLatestPipelineError({ commit() {}, dispatch() {} }); expect(flashSpy).toHaveBeenCalled(); }); }); describe('receiveLatestPipelineSuccess', () => { - it('commits pipeline', done => { - testAction( - receiveLatestPipelineSuccess, + const rootGetters = { + lastCommit: { id: '123' }, + }; + let commit; + + beforeEach(() => { + commit = jasmine.createSpy('commit'); + }); + + it('commits pipeline', () => { + receiveLatestPipelineSuccess({ rootGetters, commit }, { pipelines }); + + expect(commit.calls.argsFor(0)).toEqual([ + types.RECEIVE_LASTEST_PIPELINE_SUCCESS, pipelines[0], - mockedState, - [{ type: types.RECEIVE_LASTEST_PIPELINE_SUCCESS, payload: pipelines[0] }], - [], - done, - ); + ]); + }); + + it('commits false when there are no pipelines', () => { + receiveLatestPipelineSuccess({ rootGetters, commit }, { pipelines: [] }); + + expect(commit.calls.argsFor(0)).toEqual([types.RECEIVE_LASTEST_PIPELINE_SUCCESS, false]); }); }); describe('fetchLatestPipeline', () => { + beforeEach(() => { + jasmine.clock().install(); + }); + + afterEach(() => { + jasmine.clock().uninstall(); + stopPipelinePolling(); + clearEtagPoll(); + }); + describe('success', () => { beforeEach(() => { - mock.onGet(/\/api\/v4\/projects\/(.*)\/pipelines(.*)/).replyOnce(200, pipelines); + mock + .onGet('/abc/def/commit/abc123def456ghi789jkl/pipelines') + .reply(200, { data: { foo: 'bar' } }, { 'poll-interval': '10000' }); }); it('dispatches request', done => { - testAction( - fetchLatestPipeline, - '123', - mockedState, - [], - [{ type: 'requestLatestPipeline' }, { type: 'receiveLatestPipelineSuccess' }], - done, - ); - }); - - it('dispatches success with latest pipeline', done => { - testAction( - fetchLatestPipeline, - '123', - mockedState, - [], - [ - { type: 'requestLatestPipeline' }, - { type: 'receiveLatestPipelineSuccess', payload: pipelines[0] }, - ], - done, - ); - }); - - it('calls axios with correct params', () => { - const apiSpy = spyOn(axios, 'get').and.callThrough(); - - fetchLatestPipeline({ dispatch() {}, rootState: state }, '123'); - - expect(apiSpy).toHaveBeenCalledWith(jasmine.anything(), { - params: { - sha: '123', - per_page: '1', - }, - }); + spyOn(axios, 'get').and.callThrough(); + spyOn(Visibility, 'hidden').and.returnValue(false); + + const dispatch = jasmine.createSpy('dispatch'); + const rootGetters = { + lastCommit: { id: 'abc123def456ghi789jkl' }, + currentProject: { path_with_namespace: 'abc/def' }, + }; + + fetchLatestPipeline({ dispatch, rootGetters }); + + expect(dispatch.calls.argsFor(0)).toEqual(['requestLatestPipeline']); + + jasmine.clock().tick(1000); + + new Promise(resolve => requestAnimationFrame(resolve)) + .then(() => { + expect(axios.get).toHaveBeenCalled(); + expect(axios.get.calls.count()).toBe(1); + + expect(dispatch.calls.argsFor(1)).toEqual([ + 'receiveLatestPipelineSuccess', + jasmine.anything(), + ]); + + jasmine.clock().tick(10000); + }) + .then(() => new Promise(resolve => requestAnimationFrame(resolve))) + .then(() => { + expect(axios.get).toHaveBeenCalled(); + expect(axios.get.calls.count()).toBe(2); + + expect(dispatch.calls.argsFor(2)).toEqual([ + 'receiveLatestPipelineSuccess', + jasmine.anything(), + ]); + }) + .then(done) + .catch(done.fail); }); }); describe('error', () => { beforeEach(() => { - mock.onGet(/\/api\/v4\/projects\/(.*)\/pipelines(.*)/).replyOnce(500); + mock.onGet('/abc/def/commit/abc123def456ghi789jkl/pipelines').reply(500); }); it('dispatches error', done => { - testAction( - fetchLatestPipeline, - '123', - mockedState, - [], - [{ type: 'requestLatestPipeline' }, { type: 'receiveLatestPipelineError' }], - done, - ); + const dispatch = jasmine.createSpy('dispatch'); + const rootGetters = { + lastCommit: { id: 'abc123def456ghi789jkl' }, + currentProject: { path_with_namespace: 'abc/def' }, + }; + + fetchLatestPipeline({ dispatch, rootGetters }); + + jasmine.clock().tick(1500); + + new Promise(resolve => requestAnimationFrame(resolve)) + .then(() => { + expect(dispatch.calls.argsFor(1)).toEqual(['receiveLatestPipelineError']); + }) + .then(done) + .catch(done.fail); }); }); }); describe('requestJobs', () => { it('commits request', done => { - testAction(requestJobs, null, mockedState, [{ type: types.REQUEST_JOBS }], [], done); + testAction(requestJobs, 1, mockedState, [{ type: types.REQUEST_JOBS, payload: 1 }], [], done); }); }); @@ -151,9 +192,9 @@ describe('IDE pipelines actions', () => { it('commits error', done => { testAction( receiveJobsError, - null, + 1, mockedState, - [{ type: types.RECEIVE_JOBS_ERROR }], + [{ type: types.RECEIVE_JOBS_ERROR, payload: 1 }], [], done, ); @@ -162,19 +203,19 @@ describe('IDE pipelines actions', () => { it('creates flash message', () => { const flashSpy = spyOnDependency(actions, 'flash'); - receiveJobsError({ commit() {} }); + receiveJobsError({ commit() {} }, 1); expect(flashSpy).toHaveBeenCalled(); }); }); describe('receiveJobsSuccess', () => { - it('commits jobs', done => { + it('commits data', done => { testAction( receiveJobsSuccess, - jobs, + { id: 1, data: jobs }, mockedState, - [{ type: types.RECEIVE_JOBS_SUCCESS, payload: jobs }], + [{ type: types.RECEIVE_JOBS_SUCCESS, payload: { id: 1, data: jobs } }], [], done, ); @@ -182,108 +223,62 @@ describe('IDE pipelines actions', () => { }); describe('fetchJobs', () => { - let page = ''; - - beforeEach(() => { - mockedState.latestPipeline = pipelines[0]; - }); + const stage = { + id: 1, + dropdownPath: `${gl.TEST_HOST}/jobs`, + }; describe('success', () => { beforeEach(() => { - mock.onGet(/\/api\/v4\/projects\/(.*)\/pipelines\/(.*)\/jobs/).replyOnce(() => [ - 200, - jobs, - { - 'x-next-page': page, - }, - ]); + mock.onGet(stage.dropdownPath).replyOnce(200, jobs); }); it('dispatches request', done => { testAction( fetchJobs, - null, - mockedState, - [], - [{ type: 'requestJobs' }, { type: 'receiveJobsSuccess' }], - done, - ); - }); - - it('dispatches success with latest pipeline', done => { - testAction( - fetchJobs, - null, - mockedState, - [], - [{ type: 'requestJobs' }, { type: 'receiveJobsSuccess', payload: jobs }], - done, - ); - }); - - it('dispatches twice for both pages', done => { - page = '2'; - - testAction( - fetchJobs, - null, + stage, mockedState, [], [ - { type: 'requestJobs' }, - { type: 'receiveJobsSuccess', payload: jobs }, - { type: 'fetchJobs', payload: '2' }, - { type: 'requestJobs' }, - { type: 'receiveJobsSuccess', payload: jobs }, + { type: 'requestJobs', payload: stage.id }, + { type: 'receiveJobsSuccess', payload: { id: stage.id, data: jobs } }, ], done, ); }); - - it('calls axios with correct URL', () => { - const apiSpy = spyOn(axios, 'get').and.callThrough(); - - fetchJobs({ dispatch() {}, state: mockedState, rootState: mockedState }); - - expect(apiSpy).toHaveBeenCalledWith('/api/v4/projects/test%2Fproject/pipelines/1/jobs', { - params: { page: '1' }, - }); - }); - - it('calls axios with page next page', () => { - const apiSpy = spyOn(axios, 'get').and.callThrough(); - - fetchJobs({ dispatch() {}, state: mockedState, rootState: mockedState }); - - expect(apiSpy).toHaveBeenCalledWith('/api/v4/projects/test%2Fproject/pipelines/1/jobs', { - params: { page: '1' }, - }); - - page = '2'; - - fetchJobs({ dispatch() {}, state: mockedState, rootState: mockedState }, page); - - expect(apiSpy).toHaveBeenCalledWith('/api/v4/projects/test%2Fproject/pipelines/1/jobs', { - params: { page: '2' }, - }); - }); }); describe('error', () => { beforeEach(() => { - mock.onGet(/\/api\/v4\/projects\/(.*)\/pipelines(.*)/).replyOnce(500); + mock.onGet(stage.dropdownPath).replyOnce(500); }); it('dispatches error', done => { testAction( fetchJobs, - null, + stage, mockedState, [], - [{ type: 'requestJobs' }, { type: 'receiveJobsError' }], + [ + { type: 'requestJobs', payload: stage.id }, + { type: 'receiveJobsError', payload: stage.id }, + ], done, ); }); }); }); + + describe('toggleStageCollapsed', () => { + it('commits collapse', done => { + testAction( + toggleStageCollapsed, + 1, + mockedState, + [{ type: types.TOGGLE_STAGE_COLLAPSE, payload: 1 }], + [], + done, + ); + }); + }); }); diff --git a/spec/javascripts/ide/stores/modules/pipelines/getters_spec.js b/spec/javascripts/ide/stores/modules/pipelines/getters_spec.js index b2a7e8a9025..4514896b5ea 100644 --- a/spec/javascripts/ide/stores/modules/pipelines/getters_spec.js +++ b/spec/javascripts/ide/stores/modules/pipelines/getters_spec.js @@ -37,35 +37,4 @@ describe('IDE pipeline getters', () => { expect(getters.hasLatestPipeline(mockedState)).toBe(true); }); }); - - describe('failedJobs', () => { - it('returns array of failed jobs', () => { - mockedState.stages = [ - { - title: 'test', - jobs: [{ id: 1, status: 'failed' }, { id: 2, status: 'success' }], - }, - { - title: 'build', - jobs: [{ id: 3, status: 'failed' }, { id: 4, status: 'failed' }], - }, - ]; - - expect(getters.failedJobs(mockedState).length).toBe(3); - expect(getters.failedJobs(mockedState)).toEqual([ - { - id: 1, - status: jasmine.anything(), - }, - { - id: 3, - status: jasmine.anything(), - }, - { - id: 4, - status: jasmine.anything(), - }, - ]); - }); - }); }); diff --git a/spec/javascripts/ide/stores/modules/pipelines/mutations_spec.js b/spec/javascripts/ide/stores/modules/pipelines/mutations_spec.js index 8262e916243..6285c01d483 100644 --- a/spec/javascripts/ide/stores/modules/pipelines/mutations_spec.js +++ b/spec/javascripts/ide/stores/modules/pipelines/mutations_spec.js @@ -1,7 +1,7 @@ import mutations from '~/ide/stores/modules/pipelines/mutations'; import state from '~/ide/stores/modules/pipelines/state'; import * as types from '~/ide/stores/modules/pipelines/mutation_types'; -import { pipelines, jobs } from '../../../mock_data'; +import { fullPipelinesResponse, stages, jobs } from '../../../mock_data'; describe('IDE pipelines mutations', () => { let mockedState; @@ -28,93 +28,147 @@ describe('IDE pipelines mutations', () => { describe(types.RECEIVE_LASTEST_PIPELINE_SUCCESS, () => { it('sets loading to false on success', () => { - mutations[types.RECEIVE_LASTEST_PIPELINE_SUCCESS](mockedState, pipelines[0]); + mutations[types.RECEIVE_LASTEST_PIPELINE_SUCCESS]( + mockedState, + fullPipelinesResponse.data.pipelines[0], + ); expect(mockedState.isLoadingPipeline).toBe(false); }); it('sets latestPipeline', () => { - mutations[types.RECEIVE_LASTEST_PIPELINE_SUCCESS](mockedState, pipelines[0]); + mutations[types.RECEIVE_LASTEST_PIPELINE_SUCCESS]( + mockedState, + fullPipelinesResponse.data.pipelines[0], + ); expect(mockedState.latestPipeline).toEqual({ - id: pipelines[0].id, - status: pipelines[0].status, + id: '51', + path: 'test', + commit: { id: '123' }, + details: { status: jasmine.any(Object) }, + yamlError: undefined, }); }); it('does not set latest pipeline if pipeline is null', () => { mutations[types.RECEIVE_LASTEST_PIPELINE_SUCCESS](mockedState, null); - expect(mockedState.latestPipeline).toEqual(null); + expect(mockedState.latestPipeline).toEqual(false); + }); + + it('sets stages', () => { + mutations[types.RECEIVE_LASTEST_PIPELINE_SUCCESS]( + mockedState, + fullPipelinesResponse.data.pipelines[0], + ); + + expect(mockedState.stages.length).toBe(2); + expect(mockedState.stages).toEqual([ + { + id: 0, + dropdownPath: stages[0].dropdown_path, + name: stages[0].name, + status: stages[0].status, + isCollapsed: false, + isLoading: false, + jobs: [], + }, + { + id: 1, + dropdownPath: stages[1].dropdown_path, + name: stages[1].name, + status: stages[1].status, + isCollapsed: false, + isLoading: false, + jobs: [], + }, + ]); }); }); describe(types.REQUEST_JOBS, () => { - it('sets jobs loading to true', () => { - mutations[types.REQUEST_JOBS](mockedState); + beforeEach(() => { + mockedState.stages = stages.map((stage, i) => ({ + ...stage, + id: i, + })); + }); + + it('sets isLoading on stage', () => { + mutations[types.REQUEST_JOBS](mockedState, mockedState.stages[0].id); - expect(mockedState.isLoadingJobs).toBe(true); + expect(mockedState.stages[0].isLoading).toBe(true); }); }); describe(types.RECEIVE_JOBS_ERROR, () => { - it('sets jobs loading to false', () => { - mutations[types.RECEIVE_JOBS_ERROR](mockedState); + beforeEach(() => { + mockedState.stages = stages.map((stage, i) => ({ + ...stage, + id: i, + })); + }); + + it('sets isLoading on stage after error', () => { + mutations[types.RECEIVE_JOBS_ERROR](mockedState, mockedState.stages[0].id); - expect(mockedState.isLoadingJobs).toBe(false); + expect(mockedState.stages[0].isLoading).toBe(false); }); }); describe(types.RECEIVE_JOBS_SUCCESS, () => { - it('sets jobs loading to false on success', () => { - mutations[types.RECEIVE_JOBS_SUCCESS](mockedState, jobs); + let data; - expect(mockedState.isLoadingJobs).toBe(false); + beforeEach(() => { + mockedState.stages = stages.map((stage, i) => ({ + ...stage, + id: i, + })); + + data = { + latest_statuses: [...jobs], + }; }); - it('sets stages', () => { - mutations[types.RECEIVE_JOBS_SUCCESS](mockedState, jobs); + it('updates loading', () => { + mutations[types.RECEIVE_JOBS_SUCCESS](mockedState, { id: mockedState.stages[0].id, data }); - expect(mockedState.stages.length).toBe(2); - expect(mockedState.stages).toEqual([ - { - title: 'test', - jobs: jasmine.anything(), - }, - { - title: 'build', - jobs: jasmine.anything(), - }, - ]); + expect(mockedState.stages[0].isLoading).toBe(false); }); - it('sets jobs in stages', () => { - mutations[types.RECEIVE_JOBS_SUCCESS](mockedState, jobs); + it('sets jobs on stage', () => { + mutations[types.RECEIVE_JOBS_SUCCESS](mockedState, { id: mockedState.stages[0].id, data }); + + expect(mockedState.stages[0].jobs.length).toBe(jobs.length); + expect(mockedState.stages[0].jobs).toEqual( + jobs.map(job => ({ + id: job.id, + name: job.name, + status: job.status, + path: job.build_path, + })), + ); + }); + }); - expect(mockedState.stages[0].jobs.length).toBe(3); - expect(mockedState.stages[1].jobs.length).toBe(1); - expect(mockedState.stages).toEqual([ - { - title: jasmine.anything(), - jobs: jobs.filter(job => job.stage === 'test').map(job => ({ - id: job.id, - name: job.name, - status: job.status, - stage: job.stage, - duration: job.duration, - })), - }, - { - title: jasmine.anything(), - jobs: jobs.filter(job => job.stage === 'build').map(job => ({ - id: job.id, - name: job.name, - status: job.status, - stage: job.stage, - duration: job.duration, - })), - }, - ]); + describe(types.TOGGLE_STAGE_COLLAPSE, () => { + beforeEach(() => { + mockedState.stages = stages.map((stage, i) => ({ + ...stage, + id: i, + isCollapsed: false, + })); + }); + + it('toggles collapsed state', () => { + mutations[types.TOGGLE_STAGE_COLLAPSE](mockedState, mockedState.stages[0].id); + + expect(mockedState.stages[0].isCollapsed).toBe(true); + + mutations[types.TOGGLE_STAGE_COLLAPSE](mockedState, mockedState.stages[0].id); + + expect(mockedState.stages[0].isCollapsed).toBe(false); }); }); }); diff --git a/spec/javascripts/ide/stores/mutations/branch_spec.js b/spec/javascripts/ide/stores/mutations/branch_spec.js index f2f1f2a9a2e..29eb859ddaf 100644 --- a/spec/javascripts/ide/stores/mutations/branch_spec.js +++ b/spec/javascripts/ide/stores/mutations/branch_spec.js @@ -37,40 +37,4 @@ describe('Multi-file store branch mutations', () => { expect(localState.projects.Example.branches.master.commit.title).toBe('Example commit'); }); }); - - describe('SET_LAST_COMMIT_PIPELINE', () => { - it('sets the pipeline for the last commit on current project', () => { - localState.projects = { - Example: { - branches: { - master: { - commit: {}, - }, - }, - }, - }; - - mutations.SET_LAST_COMMIT_PIPELINE(localState, { - projectId: 'Example', - branchId: 'master', - pipeline: { - id: '50', - details: { - status: { - icon: 'status_passed', - text: 'passed', - }, - }, - }, - }); - - expect(localState.projects.Example.branches.master.commit.pipeline.id).toBe('50'); - expect(localState.projects.Example.branches.master.commit.pipeline.details.status.text).toBe( - 'passed', - ); - expect(localState.projects.Example.branches.master.commit.pipeline.details.status.icon).toBe( - 'status_passed', - ); - }); - }); }); diff --git a/spec/javascripts/integrations/integration_settings_form_spec.js b/spec/javascripts/integrations/integration_settings_form_spec.js index 050b1f2074e..e07343810d2 100644 --- a/spec/javascripts/integrations/integration_settings_form_spec.js +++ b/spec/javascripts/integrations/integration_settings_form_spec.js @@ -143,6 +143,7 @@ describe('IntegrationSettingsForm', () => { error: true, message: errorMessage, service_response: 'some error', + test_failed: true, }); integrationSettingsForm.testSettings(formData) @@ -157,6 +158,27 @@ describe('IntegrationSettingsForm', () => { .catch(done.fail); }); + it('should not show error Flash with `Save anyway` action if ajax request responds with error in validation', (done) => { + const errorMessage = 'Validations failed.'; + mock.onPut(integrationSettingsForm.testEndPoint).reply(200, { + error: true, + message: errorMessage, + service_response: 'some error', + test_failed: false, + }); + + integrationSettingsForm.testSettings(formData) + .then(() => { + const $flashContainer = $('.flash-container'); + expect($flashContainer.find('.flash-text').text().trim()).toEqual('Validations failed. some error'); + expect($flashContainer.find('.flash-action')).toBeDefined(); + expect($flashContainer.find('.flash-action').text().trim()).toEqual(''); + + done(); + }) + .catch(done.fail); + }); + it('should submit form if ajax request responds without any error in test', (done) => { spyOn(integrationSettingsForm.$form, 'submit'); @@ -180,6 +202,7 @@ describe('IntegrationSettingsForm', () => { mock.onPut(integrationSettingsForm.testEndPoint).reply(200, { error: true, message: errorMessage, + test_failed: true, }); integrationSettingsForm.testSettings(formData) diff --git a/spec/javascripts/labels_select_spec.js b/spec/javascripts/labels_select_spec.js index a2b89c0aef5..386e00bfd0c 100644 --- a/spec/javascripts/labels_select_spec.js +++ b/spec/javascripts/labels_select_spec.js @@ -40,5 +40,9 @@ describe('LabelsSelect', () => { it('generated label item template has correct label styles', () => { expect($labelEl.find('span.label').attr('style')).toBe(`background-color: ${label.color}; color: ${label.text_color};`); }); + + it('generated label item has a badge class', () => { + expect($labelEl.find('span').hasClass('badge')).toEqual(true); + }); }); }); diff --git a/spec/javascripts/sidebar/confidential_issue_sidebar_spec.js b/spec/javascripts/sidebar/confidential_issue_sidebar_spec.js index 0c173062835..6110d5d89ac 100644 --- a/spec/javascripts/sidebar/confidential_issue_sidebar_spec.js +++ b/spec/javascripts/sidebar/confidential_issue_sidebar_spec.js @@ -8,10 +8,7 @@ describe('Confidential Issue Sidebar Block', () => { beforeEach(() => { const Component = Vue.extend(confidentialIssueSidebar); const service = { - update: () => new Promise((resolve, reject) => { - resolve(true); - reject('failed!'); - }), + update: () => Promise.resolve(true), }; vm1 = new Component({ diff --git a/spec/javascripts/u2f/mock_u2f_device.js b/spec/javascripts/u2f/mock_u2f_device.js index 5a1ace2b4d6..8fec6ae3fa4 100644 --- a/spec/javascripts/u2f/mock_u2f_device.js +++ b/spec/javascripts/u2f/mock_u2f_device.js @@ -1,5 +1,5 @@ /* eslint-disable prefer-rest-params, wrap-iife, -no-unused-expressions, no-return-assign, no-param-reassign*/ +no-unused-expressions, no-return-assign, no-param-reassign */ export default class MockU2FDevice { constructor() { diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js index 4397b00acfa..370a296bd8f 100644 --- a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js +++ b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js @@ -82,7 +82,7 @@ describe('DropdownValueComponent', () => { }); it('renders label element with tooltip and styles based on label details', () => { - const labelEl = vm.$el.querySelector('a span.label.color-label'); + const labelEl = vm.$el.querySelector('a span.badge.color-label'); expect(labelEl).not.toBeNull(); expect(labelEl.dataset.placement).toBe('bottom'); expect(labelEl.dataset.container).toBe('body'); diff --git a/spec/javascripts/vue_shared/components/tabs/tab_spec.js b/spec/javascripts/vue_shared/components/tabs/tab_spec.js new file mode 100644 index 00000000000..8437fe37738 --- /dev/null +++ b/spec/javascripts/vue_shared/components/tabs/tab_spec.js @@ -0,0 +1,32 @@ +import Vue from 'vue'; +import mountComponent from 'spec/helpers/vue_mount_component_helper'; +import Tab from '~/vue_shared/components/tabs/tab.vue'; + +describe('Tab component', () => { + const Component = Vue.extend(Tab); + let vm; + + beforeEach(() => { + vm = mountComponent(Component); + }); + + it('sets localActive to equal active', done => { + vm.active = true; + + vm.$nextTick(() => { + expect(vm.localActive).toBe(true); + + done(); + }); + }); + + it('sets active class', done => { + vm.active = true; + + vm.$nextTick(() => { + expect(vm.$el.classList).toContain('active'); + + done(); + }); + }); +}); diff --git a/spec/javascripts/vue_shared/components/tabs/tabs_spec.js b/spec/javascripts/vue_shared/components/tabs/tabs_spec.js new file mode 100644 index 00000000000..50ba18cd338 --- /dev/null +++ b/spec/javascripts/vue_shared/components/tabs/tabs_spec.js @@ -0,0 +1,68 @@ +import Vue from 'vue'; +import Tabs from '~/vue_shared/components/tabs/tabs'; +import Tab from '~/vue_shared/components/tabs/tab.vue'; + +describe('Tabs component', () => { + let vm; + + beforeEach(done => { + vm = new Vue({ + components: { + Tabs, + Tab, + }, + template: ` + <div> + <tabs> + <tab title="Testing" active> + First tab + </tab> + <tab> + <template slot="title">Test slot</template> + Second tab + </tab> + </tabs> + </div> + `, + }).$mount(); + + setTimeout(done); + }); + + describe('tab links', () => { + it('renders links for tabs', () => { + expect(vm.$el.querySelectorAll('a').length).toBe(2); + }); + + it('renders link titles from props', () => { + expect(vm.$el.querySelector('a').textContent).toContain('Testing'); + }); + + it('renders link titles from slot', () => { + expect(vm.$el.querySelectorAll('a')[1].textContent).toContain('Test slot'); + }); + + it('renders active class', () => { + expect(vm.$el.querySelector('a').classList).toContain('active'); + }); + + it('updates active class on click', done => { + vm.$el.querySelectorAll('a')[1].click(); + + setTimeout(() => { + expect(vm.$el.querySelector('a').classList).not.toContain('active'); + expect(vm.$el.querySelectorAll('a')[1].classList).toContain('active'); + + done(); + }); + }); + }); + + describe('content', () => { + it('renders content panes', () => { + expect(vm.$el.querySelectorAll('.tab-pane').length).toBe(2); + expect(vm.$el.querySelectorAll('.tab-pane')[0].textContent).toContain('First tab'); + expect(vm.$el.querySelectorAll('.tab-pane')[1].textContent).toContain('Second tab'); + }); + }); +}); |