diff options
author | John Jarvis <jarv@gitlab.com> | 2019-04-08 11:50:19 +0300 |
---|---|---|
committer | John Jarvis <jarv@gitlab.com> | 2019-04-08 11:50:19 +0300 |
commit | b8ed65619075d3b5892a3ec16e562518d5285a75 (patch) | |
tree | f6a31b4819339aeb170f9df5886afab23638f172 /spec/javascripts | |
parent | 9ed8b61b8692dbafe189fce68237efcc9345ee9f (diff) | |
parent | 435c492bac54f588b92e733b632edd4149a6db37 (diff) |
Merge branch 'master' into 11-10-stable
Diffstat (limited to 'spec/javascripts')
45 files changed, 1395 insertions, 1852 deletions
diff --git a/spec/javascripts/boards/issue_spec.js b/spec/javascripts/boards/issue_spec.js index 54fb0e8228b..e4ff3eb381f 100644 --- a/spec/javascripts/boards/issue_spec.js +++ b/spec/javascripts/boards/issue_spec.js @@ -178,6 +178,7 @@ describe('Issue model', () => { spyOn(Vue.http, 'patch').and.callFake((url, data) => { expect(data.issue.assignee_ids).toEqual([1]); done(); + return Promise.resolve(); }); issue.update('url'); @@ -187,6 +188,7 @@ describe('Issue model', () => { spyOn(Vue.http, 'patch').and.callFake((url, data) => { expect(data.issue.assignee_ids).toEqual([0]); done(); + return Promise.resolve(); }); issue.removeAllAssignees(); diff --git a/spec/javascripts/clusters/clusters_bundle_spec.js b/spec/javascripts/clusters/clusters_bundle_spec.js deleted file mode 100644 index 0d3dcc29f22..00000000000 --- a/spec/javascripts/clusters/clusters_bundle_spec.js +++ /dev/null @@ -1,346 +0,0 @@ -import Clusters from '~/clusters/clusters_bundle'; -import { - REQUEST_SUBMITTED, - REQUEST_FAILURE, - APPLICATION_STATUS, - INGRESS_DOMAIN_SUFFIX, -} from '~/clusters/constants'; -import getSetTimeoutPromise from 'spec/helpers/set_timeout_promise_helper'; - -describe('Clusters', () => { - let cluster; - preloadFixtures('clusters/show_cluster.html'); - - beforeEach(() => { - loadFixtures('clusters/show_cluster.html'); - cluster = new Clusters(); - }); - - afterEach(() => { - cluster.destroy(); - }); - - describe('toggle', () => { - it('should update the button and the input field on click', done => { - const toggleButton = document.querySelector( - '.js-cluster-enable-toggle-area .js-project-feature-toggle', - ); - const toggleInput = document.querySelector( - '.js-cluster-enable-toggle-area .js-project-feature-toggle-input', - ); - - toggleButton.click(); - - getSetTimeoutPromise() - .then(() => { - expect(toggleButton.classList).not.toContain('is-checked'); - - expect(toggleInput.getAttribute('value')).toEqual('false'); - }) - .then(done) - .catch(done.fail); - }); - }); - - describe('showToken', () => { - it('should update token field type', () => { - cluster.showTokenButton.click(); - - expect(cluster.tokenField.getAttribute('type')).toEqual('text'); - - cluster.showTokenButton.click(); - - expect(cluster.tokenField.getAttribute('type')).toEqual('password'); - }); - - it('should update show token button text', () => { - cluster.showTokenButton.click(); - - expect(cluster.showTokenButton.textContent).toEqual('Hide'); - - cluster.showTokenButton.click(); - - expect(cluster.showTokenButton.textContent).toEqual('Show'); - }); - }); - - describe('checkForNewInstalls', () => { - const INITIAL_APP_MAP = { - helm: { status: null, title: 'Helm Tiller' }, - ingress: { status: null, title: 'Ingress' }, - runner: { status: null, title: 'GitLab Runner' }, - }; - - it('does not show alert when things transition from initial null state to something', () => { - cluster.checkForNewInstalls(INITIAL_APP_MAP, { - ...INITIAL_APP_MAP, - helm: { status: APPLICATION_STATUS.INSTALLABLE, title: 'Helm Tiller' }, - }); - - const flashMessage = document.querySelector('.js-cluster-application-notice .flash-text'); - - expect(flashMessage).toBeNull(); - }); - - it('shows an alert when something gets newly installed', () => { - cluster.checkForNewInstalls( - { - ...INITIAL_APP_MAP, - helm: { status: APPLICATION_STATUS.INSTALLING, title: 'Helm Tiller' }, - }, - { - ...INITIAL_APP_MAP, - helm: { status: APPLICATION_STATUS.INSTALLED, title: 'Helm Tiller' }, - }, - ); - - const flashMessage = document.querySelector('.js-cluster-application-notice .flash-text'); - - expect(flashMessage).not.toBeNull(); - expect(flashMessage.textContent.trim()).toEqual( - 'Helm Tiller was successfully installed on your Kubernetes cluster', - ); - }); - - it('shows an alert when multiple things gets newly installed', () => { - cluster.checkForNewInstalls( - { - ...INITIAL_APP_MAP, - helm: { status: APPLICATION_STATUS.INSTALLING, title: 'Helm Tiller' }, - ingress: { status: APPLICATION_STATUS.INSTALLABLE, title: 'Ingress' }, - }, - { - ...INITIAL_APP_MAP, - helm: { status: APPLICATION_STATUS.INSTALLED, title: 'Helm Tiller' }, - ingress: { status: APPLICATION_STATUS.INSTALLED, title: 'Ingress' }, - }, - ); - - const flashMessage = document.querySelector('.js-cluster-application-notice .flash-text'); - - expect(flashMessage).not.toBeNull(); - expect(flashMessage.textContent.trim()).toEqual( - 'Helm Tiller, Ingress was successfully installed on your Kubernetes cluster', - ); - }); - }); - - describe('updateContainer', () => { - describe('when creating cluster', () => { - it('should show the creating container', () => { - cluster.updateContainer(null, 'creating'); - - expect(cluster.creatingContainer.classList.contains('hidden')).toBeFalsy(); - - expect(cluster.successContainer.classList.contains('hidden')).toBeTruthy(); - - expect(cluster.errorContainer.classList.contains('hidden')).toBeTruthy(); - }); - - it('should continue to show `creating` banner with subsequent updates of the same status', () => { - cluster.updateContainer('creating', 'creating'); - - expect(cluster.creatingContainer.classList.contains('hidden')).toBeFalsy(); - - expect(cluster.successContainer.classList.contains('hidden')).toBeTruthy(); - - expect(cluster.errorContainer.classList.contains('hidden')).toBeTruthy(); - }); - }); - - describe('when cluster is created', () => { - it('should show the success container and fresh the page', () => { - cluster.updateContainer(null, 'created'); - - expect(cluster.creatingContainer.classList.contains('hidden')).toBeTruthy(); - - expect(cluster.successContainer.classList.contains('hidden')).toBeFalsy(); - - expect(cluster.errorContainer.classList.contains('hidden')).toBeTruthy(); - }); - - it('should not show a banner when status is already `created`', () => { - cluster.updateContainer('created', 'created'); - - expect(cluster.creatingContainer.classList.contains('hidden')).toBeTruthy(); - - expect(cluster.successContainer.classList.contains('hidden')).toBeTruthy(); - - expect(cluster.errorContainer.classList.contains('hidden')).toBeTruthy(); - }); - }); - - describe('when cluster has error', () => { - it('should show the error container', () => { - cluster.updateContainer(null, 'errored', 'this is an error'); - - expect(cluster.creatingContainer.classList.contains('hidden')).toBeTruthy(); - - expect(cluster.successContainer.classList.contains('hidden')).toBeTruthy(); - - expect(cluster.errorContainer.classList.contains('hidden')).toBeFalsy(); - - expect(cluster.errorReasonContainer.textContent).toContain('this is an error'); - }); - - it('should show `error` banner when previously `creating`', () => { - cluster.updateContainer('creating', 'errored'); - - expect(cluster.creatingContainer.classList.contains('hidden')).toBeTruthy(); - - expect(cluster.successContainer.classList.contains('hidden')).toBeTruthy(); - - expect(cluster.errorContainer.classList.contains('hidden')).toBeFalsy(); - }); - }); - }); - - describe('installApplication', () => { - it('tries to install helm', () => { - spyOn(cluster.service, 'installApplication').and.returnValue(Promise.resolve()); - - expect(cluster.store.state.applications.helm.requestStatus).toEqual(null); - - cluster.installApplication({ id: 'helm' }); - - expect(cluster.store.state.applications.helm.requestStatus).toEqual(REQUEST_SUBMITTED); - expect(cluster.store.state.applications.helm.requestReason).toEqual(null); - expect(cluster.service.installApplication).toHaveBeenCalledWith('helm', undefined); - }); - - it('tries to install ingress', () => { - spyOn(cluster.service, 'installApplication').and.returnValue(Promise.resolve()); - - expect(cluster.store.state.applications.ingress.requestStatus).toEqual(null); - - cluster.installApplication({ id: 'ingress' }); - - expect(cluster.store.state.applications.ingress.requestStatus).toEqual(REQUEST_SUBMITTED); - expect(cluster.store.state.applications.ingress.requestReason).toEqual(null); - expect(cluster.service.installApplication).toHaveBeenCalledWith('ingress', undefined); - }); - - it('tries to install runner', () => { - spyOn(cluster.service, 'installApplication').and.returnValue(Promise.resolve()); - - expect(cluster.store.state.applications.runner.requestStatus).toEqual(null); - - cluster.installApplication({ id: 'runner' }); - - expect(cluster.store.state.applications.runner.requestStatus).toEqual(REQUEST_SUBMITTED); - expect(cluster.store.state.applications.runner.requestReason).toEqual(null); - expect(cluster.service.installApplication).toHaveBeenCalledWith('runner', undefined); - }); - - it('tries to install jupyter', () => { - 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_SUBMITTED); - expect(cluster.store.state.applications.jupyter.requestReason).toEqual(null); - expect(cluster.service.installApplication).toHaveBeenCalledWith('jupyter', { - hostname: cluster.store.state.applications.jupyter.hostname, - }); - }); - - 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({ id: 'helm' }); - - expect(cluster.store.state.applications.helm.requestStatus).toEqual(REQUEST_SUBMITTED); - expect(cluster.store.state.applications.helm.requestReason).toEqual(null); - expect(cluster.service.installApplication).toHaveBeenCalled(); - - getSetTimeoutPromise() - .then(() => { - expect(cluster.store.state.applications.helm.requestStatus).toEqual(REQUEST_FAILURE); - expect(cluster.store.state.applications.helm.requestReason).toBeDefined(); - }) - .then(done) - .catch(done.fail); - }); - }); - - describe('handleSuccess', () => { - beforeEach(() => { - spyOn(cluster.store, 'updateStateFromServer'); - spyOn(cluster, 'toggleIngressDomainHelpText'); - spyOn(cluster, 'checkForNewInstalls'); - spyOn(cluster, 'updateContainer'); - - cluster.handleSuccess({ data: {} }); - }); - - it('updates clusters store', () => { - expect(cluster.store.updateStateFromServer).toHaveBeenCalled(); - }); - - it('checks for new installable apps', () => { - expect(cluster.checkForNewInstalls).toHaveBeenCalled(); - }); - - it('toggles ingress domain help text', () => { - expect(cluster.toggleIngressDomainHelpText).toHaveBeenCalled(); - }); - - it('updates message containers', () => { - expect(cluster.updateContainer).toHaveBeenCalled(); - }); - }); - - describe('toggleIngressDomainHelpText', () => { - const { INSTALLED, INSTALLABLE, NOT_INSTALLABLE } = APPLICATION_STATUS; - - const ingressPreviousState = { status: INSTALLABLE }; - const ingressNewState = { status: INSTALLED, externalIp: '127.0.0.1' }; - - describe(`when ingress application new status is ${INSTALLED}`, () => { - beforeEach(() => { - ingressNewState.status = INSTALLED; - cluster.toggleIngressDomainHelpText(ingressPreviousState, ingressNewState); - }); - - it('displays custom domain help text', () => { - expect(cluster.ingressDomainHelpText.classList.contains('hide')).toEqual(false); - }); - - it('updates ingress external ip address', () => { - expect(cluster.ingressDomainSnippet.textContent).toEqual( - `${ingressNewState.externalIp}${INGRESS_DOMAIN_SUFFIX}`, - ); - }); - }); - - describe(`when ingress application new status is different from ${INSTALLED}`, () => { - it('hides custom domain help text', () => { - ingressNewState.status = NOT_INSTALLABLE; - cluster.ingressDomainHelpText.classList.remove('hide'); - - cluster.toggleIngressDomainHelpText(ingressPreviousState, ingressNewState); - - expect(cluster.ingressDomainHelpText.classList.contains('hide')).toEqual(true); - }); - }); - - describe('when ingress application new status and old status are the same', () => { - it('does not modify custom domain help text', () => { - ingressPreviousState.status = INSTALLED; - ingressNewState.status = ingressPreviousState.status; - - cluster.toggleIngressDomainHelpText(ingressPreviousState, ingressNewState); - - expect(cluster.ingressDomainHelpText.classList.contains('hide')).toEqual(true); - }); - }); - }); -}); diff --git a/spec/javascripts/clusters/components/application_row_spec.js b/spec/javascripts/clusters/components/application_row_spec.js deleted file mode 100644 index a2dd4e93daf..00000000000 --- a/spec/javascripts/clusters/components/application_row_spec.js +++ /dev/null @@ -1,406 +0,0 @@ -import Vue from 'vue'; -import eventHub from '~/clusters/event_hub'; -import { APPLICATION_STATUS, REQUEST_SUBMITTED, REQUEST_FAILURE } from '~/clusters/constants'; -import applicationRow from '~/clusters/components/application_row.vue'; -import mountComponent from 'spec/helpers/vue_mount_component_helper'; -import { DEFAULT_APPLICATION_STATE } from '../services/mock_data'; - -describe('Application Row', () => { - let vm; - let ApplicationRow; - - beforeEach(() => { - ApplicationRow = Vue.extend(applicationRow); - }); - - afterEach(() => { - vm.$destroy(); - }); - - describe('Title', () => { - it('shows title', () => { - vm = mountComponent(ApplicationRow, { - ...DEFAULT_APPLICATION_STATE, - titleLink: null, - }); - const title = vm.$el.querySelector('.js-cluster-application-title'); - - expect(title.tagName).toEqual('SPAN'); - expect(title.textContent.trim()).toEqual(DEFAULT_APPLICATION_STATE.title); - }); - - it('shows title link', () => { - expect(DEFAULT_APPLICATION_STATE.titleLink).toBeDefined(); - - vm = mountComponent(ApplicationRow, { - ...DEFAULT_APPLICATION_STATE, - }); - const title = vm.$el.querySelector('.js-cluster-application-title'); - - expect(title.tagName).toEqual('A'); - expect(title.textContent.trim()).toEqual(DEFAULT_APPLICATION_STATE.title); - }); - }); - - describe('Install button', () => { - it('has indeterminate state on page load', () => { - vm = mountComponent(ApplicationRow, { - ...DEFAULT_APPLICATION_STATE, - status: null, - }); - - expect(vm.installButtonLabel).toBeUndefined(); - }); - - it('has install button', () => { - const installationBtn = vm.$el.querySelector('.js-cluster-application-install-button'); - - expect(installationBtn).not.toBe(null); - }); - - it('has disabled "Install" when APPLICATION_STATUS.NOT_INSTALLABLE', () => { - vm = mountComponent(ApplicationRow, { - ...DEFAULT_APPLICATION_STATE, - status: APPLICATION_STATUS.NOT_INSTALLABLE, - }); - - expect(vm.installButtonLabel).toEqual('Install'); - expect(vm.installButtonLoading).toEqual(false); - expect(vm.installButtonDisabled).toEqual(true); - }); - - it('has enabled "Install" when APPLICATION_STATUS.INSTALLABLE', () => { - vm = mountComponent(ApplicationRow, { - ...DEFAULT_APPLICATION_STATE, - status: APPLICATION_STATUS.INSTALLABLE, - }); - - expect(vm.installButtonLabel).toEqual('Install'); - expect(vm.installButtonLoading).toEqual(false); - expect(vm.installButtonDisabled).toEqual(false); - }); - - it('has loading "Installing" when APPLICATION_STATUS.SCHEDULED', () => { - vm = mountComponent(ApplicationRow, { - ...DEFAULT_APPLICATION_STATE, - status: APPLICATION_STATUS.SCHEDULED, - }); - - expect(vm.installButtonLabel).toEqual('Installing'); - expect(vm.installButtonLoading).toEqual(true); - expect(vm.installButtonDisabled).toEqual(true); - }); - - it('has loading "Installing" when APPLICATION_STATUS.INSTALLING', () => { - vm = mountComponent(ApplicationRow, { - ...DEFAULT_APPLICATION_STATE, - status: APPLICATION_STATUS.INSTALLING, - }); - - expect(vm.installButtonLabel).toEqual('Installing'); - expect(vm.installButtonLoading).toEqual(true); - expect(vm.installButtonDisabled).toEqual(true); - }); - - it('has loading "Installing" when REQUEST_SUBMITTED', () => { - vm = mountComponent(ApplicationRow, { - ...DEFAULT_APPLICATION_STATE, - status: APPLICATION_STATUS.INSTALLABLE, - requestStatus: REQUEST_SUBMITTED, - }); - - expect(vm.installButtonLabel).toEqual('Installing'); - expect(vm.installButtonLoading).toEqual(true); - expect(vm.installButtonDisabled).toEqual(true); - }); - - it('has disabled "Installed" when APPLICATION_STATUS.INSTALLED', () => { - vm = mountComponent(ApplicationRow, { - ...DEFAULT_APPLICATION_STATE, - status: APPLICATION_STATUS.INSTALLED, - }); - - expect(vm.installButtonLabel).toEqual('Installed'); - expect(vm.installButtonLoading).toEqual(false); - expect(vm.installButtonDisabled).toEqual(true); - }); - - it('has disabled "Installed" when APPLICATION_STATUS.UPDATING', () => { - vm = mountComponent(ApplicationRow, { - ...DEFAULT_APPLICATION_STATE, - status: APPLICATION_STATUS.UPDATING, - }); - - expect(vm.installButtonLabel).toEqual('Installed'); - expect(vm.installButtonLoading).toEqual(false); - expect(vm.installButtonDisabled).toEqual(true); - }); - - it('has enabled "Install" when APPLICATION_STATUS.ERROR', () => { - vm = mountComponent(ApplicationRow, { - ...DEFAULT_APPLICATION_STATE, - status: APPLICATION_STATUS.ERROR, - }); - - expect(vm.installButtonLabel).toEqual('Install'); - expect(vm.installButtonLoading).toEqual(false); - expect(vm.installButtonDisabled).toEqual(false); - }); - - it('has enabled "Install" when REQUEST_FAILURE (so you can try installing again)', () => { - vm = mountComponent(ApplicationRow, { - ...DEFAULT_APPLICATION_STATE, - status: APPLICATION_STATUS.INSTALLABLE, - requestStatus: REQUEST_FAILURE, - }); - - expect(vm.installButtonLabel).toEqual('Install'); - expect(vm.installButtonLoading).toEqual(false); - expect(vm.installButtonDisabled).toEqual(false); - }); - - it('clicking install button emits event', () => { - spyOn(eventHub, '$emit'); - vm = mountComponent(ApplicationRow, { - ...DEFAULT_APPLICATION_STATE, - status: APPLICATION_STATUS.INSTALLABLE, - }); - const installButton = vm.$el.querySelector('.js-cluster-application-install-button'); - - installButton.click(); - - 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_STATUS.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', () => { - spyOn(eventHub, '$emit'); - vm = mountComponent(ApplicationRow, { - ...DEFAULT_APPLICATION_STATE, - status: APPLICATION_STATUS.INSTALLING, - }); - const installButton = vm.$el.querySelector('.js-cluster-application-install-button'); - - expect(vm.installButtonDisabled).toEqual(true); - - installButton.click(); - - expect(eventHub.$emit).not.toHaveBeenCalled(); - }); - }); - - describe('Upgrade button', () => { - it('has indeterminate state on page load', () => { - vm = mountComponent(ApplicationRow, { - ...DEFAULT_APPLICATION_STATE, - status: null, - }); - const upgradeBtn = vm.$el.querySelector('.js-cluster-application-upgrade-button'); - - expect(upgradeBtn).toBe(null); - }); - - it('has enabled "Upgrade" when "upgradeAvailable" is true', () => { - vm = mountComponent(ApplicationRow, { - ...DEFAULT_APPLICATION_STATE, - upgradeAvailable: true, - }); - const upgradeBtn = vm.$el.querySelector('.js-cluster-application-upgrade-button'); - - expect(upgradeBtn).not.toBe(null); - expect(upgradeBtn.innerHTML).toContain('Upgrade'); - }); - - it('has enabled "Retry update" when APPLICATION_STATUS.UPDATE_ERRORED', () => { - vm = mountComponent(ApplicationRow, { - ...DEFAULT_APPLICATION_STATE, - status: APPLICATION_STATUS.UPDATE_ERRORED, - }); - const upgradeBtn = vm.$el.querySelector('.js-cluster-application-upgrade-button'); - - expect(upgradeBtn).not.toBe(null); - expect(vm.upgradeFailed).toBe(true); - expect(upgradeBtn.innerHTML).toContain('Retry update'); - }); - - it('has disabled "Updating" when APPLICATION_STATUS.UPDATING', () => { - vm = mountComponent(ApplicationRow, { - ...DEFAULT_APPLICATION_STATE, - status: APPLICATION_STATUS.UPDATING, - }); - const upgradeBtn = vm.$el.querySelector('.js-cluster-application-upgrade-button'); - - expect(upgradeBtn).not.toBe(null); - expect(vm.isUpgrading).toBe(true); - expect(upgradeBtn.innerHTML).toContain('Updating'); - }); - - it('clicking upgrade button emits event', () => { - spyOn(eventHub, '$emit'); - vm = mountComponent(ApplicationRow, { - ...DEFAULT_APPLICATION_STATE, - status: APPLICATION_STATUS.UPDATE_ERRORED, - }); - const upgradeBtn = vm.$el.querySelector('.js-cluster-application-upgrade-button'); - - upgradeBtn.click(); - - expect(eventHub.$emit).toHaveBeenCalledWith('upgradeApplication', { - id: DEFAULT_APPLICATION_STATE.id, - params: {}, - }); - }); - - it('clicking disabled upgrade button emits nothing', () => { - spyOn(eventHub, '$emit'); - vm = mountComponent(ApplicationRow, { - ...DEFAULT_APPLICATION_STATE, - status: APPLICATION_STATUS.UPDATING, - }); - const upgradeBtn = vm.$el.querySelector('.js-cluster-application-upgrade-button'); - - upgradeBtn.click(); - - expect(eventHub.$emit).not.toHaveBeenCalled(); - }); - - it('displays an error message if application upgrade failed', () => { - vm = mountComponent(ApplicationRow, { - ...DEFAULT_APPLICATION_STATE, - title: 'GitLab Runner', - status: APPLICATION_STATUS.UPDATE_ERRORED, - }); - const failureMessage = vm.$el.querySelector( - '.js-cluster-application-upgrade-failure-message', - ); - - expect(failureMessage).not.toBe(null); - expect(failureMessage.innerHTML).toContain( - 'Update failed. Please check the logs and try again.', - ); - }); - }); - - describe('Version', () => { - it('displays a version number if application has been upgraded', () => { - const version = '0.1.45'; - vm = mountComponent(ApplicationRow, { - ...DEFAULT_APPLICATION_STATE, - status: APPLICATION_STATUS.UPDATED, - version, - }); - const upgradeDetails = vm.$el.querySelector('.js-cluster-application-upgrade-details'); - const versionEl = vm.$el.querySelector('.js-cluster-application-upgrade-version'); - - expect(upgradeDetails.innerHTML).toContain('Upgraded'); - expect(versionEl).not.toBe(null); - expect(versionEl.innerHTML).toContain(version); - }); - - it('contains a link to the chart repo if application has been upgraded', () => { - const version = '0.1.45'; - const chartRepo = 'https://gitlab.com/charts/gitlab-runner'; - vm = mountComponent(ApplicationRow, { - ...DEFAULT_APPLICATION_STATE, - status: APPLICATION_STATUS.UPDATED, - chartRepo, - version, - }); - const versionEl = vm.$el.querySelector('.js-cluster-application-upgrade-version'); - - expect(versionEl.href).toEqual(chartRepo); - expect(versionEl.target).toEqual('_blank'); - }); - - it('does not display a version number if application upgrade failed', () => { - const version = '0.1.45'; - vm = mountComponent(ApplicationRow, { - ...DEFAULT_APPLICATION_STATE, - status: APPLICATION_STATUS.UPDATE_ERRORED, - version, - }); - const upgradeDetails = vm.$el.querySelector('.js-cluster-application-upgrade-details'); - const versionEl = vm.$el.querySelector('.js-cluster-application-upgrade-version'); - - expect(upgradeDetails.innerHTML).toContain('failed'); - expect(versionEl).toBe(null); - }); - }); - - describe('Error block', () => { - it('does not show error block when there is no error', () => { - vm = mountComponent(ApplicationRow, { - ...DEFAULT_APPLICATION_STATE, - status: null, - requestStatus: null, - }); - const generalErrorMessage = vm.$el.querySelector( - '.js-cluster-application-general-error-message', - ); - - expect(generalErrorMessage).toBeNull(); - }); - - it('shows status reason when APPLICATION_STATUS.ERROR', () => { - const statusReason = 'We broke it 0.0'; - vm = mountComponent(ApplicationRow, { - ...DEFAULT_APPLICATION_STATE, - status: APPLICATION_STATUS.ERROR, - statusReason, - }); - const generalErrorMessage = vm.$el.querySelector( - '.js-cluster-application-general-error-message', - ); - const statusErrorMessage = vm.$el.querySelector( - '.js-cluster-application-status-error-message', - ); - - expect(generalErrorMessage.textContent.trim()).toEqual( - `Something went wrong while installing ${DEFAULT_APPLICATION_STATE.title}`, - ); - - expect(statusErrorMessage.textContent.trim()).toEqual(statusReason); - }); - - it('shows request reason when REQUEST_FAILURE', () => { - const requestReason = 'We broke thre request 0.0'; - vm = mountComponent(ApplicationRow, { - ...DEFAULT_APPLICATION_STATE, - status: APPLICATION_STATUS.INSTALLABLE, - requestStatus: REQUEST_FAILURE, - requestReason, - }); - const generalErrorMessage = vm.$el.querySelector( - '.js-cluster-application-general-error-message', - ); - const requestErrorMessage = vm.$el.querySelector( - '.js-cluster-application-request-error-message', - ); - - expect(generalErrorMessage.textContent.trim()).toEqual( - `Something went wrong while installing ${DEFAULT_APPLICATION_STATE.title}`, - ); - - expect(requestErrorMessage.textContent.trim()).toEqual(requestReason); - }); - }); -}); diff --git a/spec/javascripts/clusters/components/applications_spec.js b/spec/javascripts/clusters/components/applications_spec.js deleted file mode 100644 index 0f8153ad493..00000000000 --- a/spec/javascripts/clusters/components/applications_spec.js +++ /dev/null @@ -1,350 +0,0 @@ -import Vue from 'vue'; -import applications from '~/clusters/components/applications.vue'; -import { CLUSTER_TYPE } from '~/clusters/constants'; -import eventHub from '~/clusters/event_hub'; -import mountComponent from 'spec/helpers/vue_mount_component_helper'; -import { APPLICATIONS_MOCK_STATE } from '../services/mock_data'; - -describe('Applications', () => { - let vm; - let Applications; - - beforeEach(() => { - Applications = Vue.extend(applications); - }); - - afterEach(() => { - vm.$destroy(); - }); - - describe('Project cluster applications', () => { - beforeEach(() => { - vm = mountComponent(Applications, { - applications: APPLICATIONS_MOCK_STATE, - type: CLUSTER_TYPE.PROJECT, - }); - }); - - it('renders a row for Helm Tiller', () => { - expect(vm.$el.querySelector('.js-cluster-application-row-helm')).not.toBeNull(); - }); - - it('renders a row for Ingress', () => { - expect(vm.$el.querySelector('.js-cluster-application-row-ingress')).not.toBeNull(); - }); - - it('renders a row for Cert-Manager', () => { - expect(vm.$el.querySelector('.js-cluster-application-row-cert_manager')).not.toBeNull(); - }); - - it('renders a row for Prometheus', () => { - expect(vm.$el.querySelector('.js-cluster-application-row-prometheus')).not.toBeNull(); - }); - - it('renders a row for GitLab Runner', () => { - expect(vm.$el.querySelector('.js-cluster-application-row-runner')).not.toBeNull(); - }); - - it('renders a row for Jupyter', () => { - expect(vm.$el.querySelector('.js-cluster-application-row-jupyter')).not.toBeNull(); - }); - - it('renders a row for Knative', () => { - expect(vm.$el.querySelector('.js-cluster-application-row-knative')).not.toBeNull(); - }); - }); - - describe('Group cluster applications', () => { - beforeEach(() => { - vm = mountComponent(Applications, { - type: CLUSTER_TYPE.GROUP, - applications: APPLICATIONS_MOCK_STATE, - }); - }); - - it('renders a row for Helm Tiller', () => { - expect(vm.$el.querySelector('.js-cluster-application-row-helm')).not.toBeNull(); - }); - - it('renders a row for Ingress', () => { - expect(vm.$el.querySelector('.js-cluster-application-row-ingress')).not.toBeNull(); - }); - - it('renders a row for Cert-Manager', () => { - expect(vm.$el.querySelector('.js-cluster-application-row-cert_manager')).not.toBeNull(); - }); - - it('renders a row for Prometheus', () => { - expect(vm.$el.querySelector('.js-cluster-application-row-prometheus')).toBeNull(); - }); - - it('renders a row for GitLab Runner', () => { - expect(vm.$el.querySelector('.js-cluster-application-row-runner')).not.toBeNull(); - }); - - it('renders a row for Jupyter', () => { - expect(vm.$el.querySelector('.js-cluster-application-row-jupyter')).toBeNull(); - }); - - it('renders a row for Knative', () => { - expect(vm.$el.querySelector('.js-cluster-application-row-knative')).toBeNull(); - }); - }); - - describe('Ingress application', () => { - describe('when installed', () => { - describe('with ip address', () => { - it('renders ip address with a clipboard button', () => { - vm = mountComponent(Applications, { - applications: { - ...APPLICATIONS_MOCK_STATE, - ingress: { - title: 'Ingress', - status: 'installed', - externalIp: '0.0.0.0', - }, - }, - }); - - expect(vm.$el.querySelector('.js-endpoint').value).toEqual('0.0.0.0'); - - expect( - vm.$el.querySelector('.js-clipboard-btn').getAttribute('data-clipboard-text'), - ).toEqual('0.0.0.0'); - }); - }); - - describe('with hostname', () => { - it('renders hostname with a clipboard button', () => { - vm = mountComponent(Applications, { - applications: { - ingress: { - title: 'Ingress', - status: 'installed', - externalHostname: 'localhost.localdomain', - }, - helm: { title: 'Helm Tiller' }, - cert_manager: { title: 'Cert-Manager' }, - runner: { title: 'GitLab Runner' }, - prometheus: { title: 'Prometheus' }, - jupyter: { title: 'JupyterHub', hostname: '' }, - knative: { title: 'Knative', hostname: '' }, - }, - }); - - expect(vm.$el.querySelector('.js-endpoint').value).toEqual('localhost.localdomain'); - - expect( - vm.$el.querySelector('.js-clipboard-btn').getAttribute('data-clipboard-text'), - ).toEqual('localhost.localdomain'); - }); - }); - - describe('without ip address', () => { - it('renders an input text with a loading icon and an alert text', () => { - vm = mountComponent(Applications, { - applications: { - ...APPLICATIONS_MOCK_STATE, - ingress: { - title: 'Ingress', - status: 'installed', - }, - }, - }); - - expect(vm.$el.querySelector('.js-ingress-ip-loading-icon')).not.toBe(null); - expect(vm.$el.querySelector('.js-no-endpoint-message')).not.toBe(null); - }); - }); - }); - - describe('before installing', () => { - it('does not render the IP address', () => { - vm = mountComponent(Applications, { - applications: APPLICATIONS_MOCK_STATE, - }); - - expect(vm.$el.textContent).not.toContain('Ingress IP Address'); - expect(vm.$el.querySelector('.js-endpoint')).toBe(null); - }); - }); - - describe('Cert-Manager application', () => { - describe('when not installed', () => { - it('renders email & allows editing', () => { - vm = mountComponent(Applications, { - applications: { - ...APPLICATIONS_MOCK_STATE, - cert_manager: { - title: 'Cert-Manager', - email: 'before@example.com', - status: 'installable', - }, - }, - }); - - expect(vm.$el.querySelector('.js-email').value).toEqual('before@example.com'); - expect(vm.$el.querySelector('.js-email').getAttribute('readonly')).toBe(null); - }); - }); - - describe('when installed', () => { - it('renders email in readonly', () => { - vm = mountComponent(Applications, { - applications: { - ...APPLICATIONS_MOCK_STATE, - cert_manager: { - title: 'Cert-Manager', - email: 'after@example.com', - status: 'installed', - }, - }, - }); - - expect(vm.$el.querySelector('.js-email').value).toEqual('after@example.com'); - expect(vm.$el.querySelector('.js-email').getAttribute('readonly')).toEqual('readonly'); - }); - }); - }); - - describe('Jupyter application', () => { - describe('with ingress installed with ip & jupyter installable', () => { - it('renders hostname active input', () => { - vm = mountComponent(Applications, { - applications: { - ...APPLICATIONS_MOCK_STATE, - ingress: { - title: 'Ingress', - status: 'installed', - externalIp: '1.1.1.1', - }, - }, - }); - - 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: { - ...APPLICATIONS_MOCK_STATE, - ingress: { title: 'Ingress', status: 'installed' }, - }, - }); - - expect(vm.$el.querySelector('.js-hostname')).toBe(null); - }); - }); - - describe('with ingress & jupyter installed', () => { - it('renders readonly input', () => { - vm = mountComponent(Applications, { - applications: { - ...APPLICATIONS_MOCK_STATE, - ingress: { title: 'Ingress', status: 'installed', externalIp: '1.1.1.1' }, - 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: APPLICATIONS_MOCK_STATE, - }); - }); - - 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'); - }); - }); - }); - }); - - describe('Knative application', () => { - describe('when installed', () => { - describe('with ip address', () => { - const props = { - applications: { - ...APPLICATIONS_MOCK_STATE, - knative: { - title: 'Knative', - hostname: 'example.com', - status: 'installed', - externalIp: '1.1.1.1', - }, - }, - }; - it('renders ip address with a clipboard button', () => { - vm = mountComponent(Applications, props); - - expect(vm.$el.querySelector('.js-knative-endpoint').value).toEqual('1.1.1.1'); - - expect( - vm.$el - .querySelector('.js-knative-endpoint-clipboard-btn') - .getAttribute('data-clipboard-text'), - ).toEqual('1.1.1.1'); - }); - - it('renders domain & allows editing', () => { - expect(vm.$el.querySelector('.js-knative-domainname').value).toEqual('example.com'); - expect(vm.$el.querySelector('.js-knative-domainname').getAttribute('readonly')).toBe( - null, - ); - }); - - it('renders an update/save Knative domain button', () => { - expect(vm.$el.querySelector('.js-knative-save-domain-button')).not.toBe(null); - }); - - it('emits event when clicking Save changes button', () => { - spyOn(eventHub, '$emit'); - vm = mountComponent(Applications, props); - - const saveButton = vm.$el.querySelector('.js-knative-save-domain-button'); - - saveButton.click(); - - expect(eventHub.$emit).toHaveBeenCalledWith('saveKnativeDomain', { - id: 'knative', - params: { hostname: 'example.com' }, - }); - }); - }); - - describe('without ip address', () => { - it('renders an input text with a loading icon and an alert text', () => { - vm = mountComponent(Applications, { - applications: { - ...APPLICATIONS_MOCK_STATE, - knative: { - title: 'Knative', - hostname: 'example.com', - status: 'installed', - }, - }, - }); - - expect(vm.$el.querySelector('.js-knative-ip-loading-icon')).not.toBe(null); - expect(vm.$el.querySelector('.js-no-knative-endpoint-message')).not.toBe(null); - }); - }); - }); - }); -}); diff --git a/spec/javascripts/clusters/services/mock_data.js b/spec/javascripts/clusters/services/mock_data.js deleted file mode 100644 index b4d1bb710e0..00000000000 --- a/spec/javascripts/clusters/services/mock_data.js +++ /dev/null @@ -1,130 +0,0 @@ -import { APPLICATION_STATUS } from '~/clusters/constants'; - -const CLUSTERS_MOCK_DATA = { - GET: { - '/gitlab-org/gitlab-shell/clusters/1/status.json': { - data: { - status: 'errored', - status_reason: 'Failed to request to CloudPlatform.', - applications: [ - { - name: 'helm', - status: APPLICATION_STATUS.INSTALLABLE, - status_reason: null, - }, - { - name: 'ingress', - status: APPLICATION_STATUS.ERROR, - status_reason: 'Cannot connect', - external_ip: null, - external_hostname: null, - }, - { - name: 'runner', - status: APPLICATION_STATUS.INSTALLING, - status_reason: null, - }, - { - name: 'prometheus', - status: APPLICATION_STATUS.ERROR, - status_reason: 'Cannot connect', - }, - { - name: 'jupyter', - status: APPLICATION_STATUS.INSTALLING, - status_reason: 'Cannot connect', - }, - { - name: 'knative', - status: APPLICATION_STATUS.INSTALLING, - status_reason: 'Cannot connect', - }, - { - name: 'cert_manager', - status: APPLICATION_STATUS.ERROR, - status_reason: 'Cannot connect', - email: 'test@example.com', - }, - ], - }, - }, - '/gitlab-org/gitlab-shell/clusters/2/status.json': { - data: { - status: 'errored', - status_reason: 'Failed to request to CloudPlatform.', - applications: [ - { - name: 'helm', - status: APPLICATION_STATUS.INSTALLED, - status_reason: null, - }, - { - name: 'ingress', - status: APPLICATION_STATUS.INSTALLED, - status_reason: 'Cannot connect', - external_ip: '1.1.1.1', - external_hostname: null, - }, - { - name: 'runner', - status: APPLICATION_STATUS.INSTALLING, - status_reason: null, - }, - { - name: 'prometheus', - status: APPLICATION_STATUS.ERROR, - status_reason: 'Cannot connect', - }, - { - name: 'jupyter', - status: APPLICATION_STATUS.INSTALLABLE, - status_reason: 'Cannot connect', - }, - { - name: 'knative', - status: APPLICATION_STATUS.INSTALLABLE, - status_reason: 'Cannot connect', - }, - { - name: 'cert_manager', - status: APPLICATION_STATUS.ERROR, - status_reason: 'Cannot connect', - email: 'test@example.com', - }, - ], - }, - }, - }, - POST: { - '/gitlab-org/gitlab-shell/clusters/1/applications/helm': {}, - '/gitlab-org/gitlab-shell/clusters/1/applications/ingress': {}, - '/gitlab-org/gitlab-shell/clusters/1/applications/cert_manager': {}, - '/gitlab-org/gitlab-shell/clusters/1/applications/runner': {}, - '/gitlab-org/gitlab-shell/clusters/1/applications/prometheus': {}, - '/gitlab-org/gitlab-shell/clusters/1/applications/jupyter': {}, - '/gitlab-org/gitlab-shell/clusters/1/applications/knative': {}, - }, -}; - -const DEFAULT_APPLICATION_STATE = { - id: 'some-app', - title: 'My App', - titleLink: 'https://about.gitlab.com/', - description: 'Some description about this interesting application!', - status: null, - statusReason: null, - requestStatus: null, - requestReason: null, -}; - -const APPLICATIONS_MOCK_STATE = { - helm: { title: 'Helm Tiller', status: 'installable' }, - ingress: { title: 'Ingress', status: 'installable' }, - cert_manager: { title: 'Cert-Manager', status: 'installable' }, - runner: { title: 'GitLab Runner' }, - prometheus: { title: 'Prometheus' }, - jupyter: { title: 'JupyterHub', status: 'installable', hostname: '' }, - knative: { title: 'Knative ', status: 'installable', hostname: '' }, -}; - -export { CLUSTERS_MOCK_DATA, DEFAULT_APPLICATION_STATE, APPLICATIONS_MOCK_STATE }; diff --git a/spec/javascripts/clusters/stores/clusters_store_spec.js b/spec/javascripts/clusters/stores/clusters_store_spec.js deleted file mode 100644 index 161722ec571..00000000000 --- a/spec/javascripts/clusters/stores/clusters_store_spec.js +++ /dev/null @@ -1,142 +0,0 @@ -import ClustersStore from '~/clusters/stores/clusters_store'; -import { APPLICATION_STATUS } from '~/clusters/constants'; -import { CLUSTERS_MOCK_DATA } from '../services/mock_data'; - -describe('Clusters Store', () => { - let store; - - beforeEach(() => { - store = new ClustersStore(); - }); - - describe('updateStatus', () => { - it('should store new status', () => { - expect(store.state.status).toEqual(null); - - const newStatus = 'errored'; - store.updateStatus(newStatus); - - expect(store.state.status).toEqual(newStatus); - }); - }); - - describe('updateStatusReason', () => { - it('should store new reason', () => { - expect(store.state.statusReason).toEqual(null); - - const newReason = 'Something went wrong!'; - store.updateStatusReason(newReason); - - expect(store.state.statusReason).toEqual(newReason); - }); - }); - - describe('updateAppProperty', () => { - it('should store new request status', () => { - expect(store.state.applications.helm.requestStatus).toEqual(null); - - const newStatus = APPLICATION_STATUS.INSTALLING; - store.updateAppProperty('helm', 'requestStatus', newStatus); - - expect(store.state.applications.helm.requestStatus).toEqual(newStatus); - }); - - it('should store new request reason', () => { - expect(store.state.applications.helm.requestReason).toEqual(null); - - const newReason = 'We broke it.'; - store.updateAppProperty('helm', 'requestReason', newReason); - - expect(store.state.applications.helm.requestReason).toEqual(newReason); - }); - }); - - describe('updateStateFromServer', () => { - it('should store new polling data from server', () => { - const mockResponseData = - CLUSTERS_MOCK_DATA.GET['/gitlab-org/gitlab-shell/clusters/1/status.json'].data; - store.updateStateFromServer(mockResponseData); - - expect(store.state).toEqual({ - helpPath: null, - ingressHelpPath: null, - status: mockResponseData.status, - statusReason: mockResponseData.status_reason, - rbac: false, - applications: { - helm: { - title: 'Helm Tiller', - status: mockResponseData.applications[0].status, - statusReason: mockResponseData.applications[0].status_reason, - requestStatus: null, - requestReason: null, - }, - ingress: { - title: 'Ingress', - status: mockResponseData.applications[1].status, - statusReason: mockResponseData.applications[1].status_reason, - requestStatus: null, - requestReason: null, - externalIp: null, - externalHostname: null, - }, - runner: { - title: 'GitLab Runner', - status: mockResponseData.applications[2].status, - statusReason: mockResponseData.applications[2].status_reason, - requestStatus: null, - requestReason: null, - version: mockResponseData.applications[2].version, - upgradeAvailable: mockResponseData.applications[2].update_available, - chartRepo: 'https://gitlab.com/charts/gitlab-runner', - }, - prometheus: { - title: 'Prometheus', - status: mockResponseData.applications[3].status, - statusReason: mockResponseData.applications[3].status_reason, - requestStatus: null, - requestReason: null, - }, - jupyter: { - title: 'JupyterHub', - status: mockResponseData.applications[4].status, - statusReason: mockResponseData.applications[4].status_reason, - requestStatus: null, - requestReason: null, - hostname: '', - }, - knative: { - title: 'Knative', - status: mockResponseData.applications[5].status, - statusReason: mockResponseData.applications[5].status_reason, - requestStatus: null, - requestReason: null, - hostname: null, - isEditingHostName: false, - externalIp: null, - externalHostname: null, - }, - cert_manager: { - title: 'Cert-Manager', - status: mockResponseData.applications[6].status, - statusReason: mockResponseData.applications[6].status_reason, - requestStatus: null, - requestReason: null, - email: mockResponseData.applications[6].email, - }, - }, - }); - }); - - 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}.nip.io`, - ); - }); - }); -}); diff --git a/spec/javascripts/diffs/components/app_spec.js b/spec/javascripts/diffs/components/app_spec.js index 8d7c52a2876..3ce69bc3c20 100644 --- a/spec/javascripts/diffs/components/app_spec.js +++ b/spec/javascripts/diffs/components/app_spec.js @@ -57,6 +57,24 @@ describe('diffs/components/app', () => { wrapper.destroy(); }); + it('adds container-limiting classes when showFileTree is false with inline diffs', () => { + createComponent({}, ({ state }) => { + state.diffs.showTreeList = false; + state.diffs.isParallelView = false; + }); + + expect(wrapper.contains('.container-limited.limit-container-width')).toBe(true); + }); + + it('does not add container-limiting classes when showFileTree is false with inline diffs', () => { + createComponent({}, ({ state }) => { + state.diffs.showTreeList = true; + state.diffs.isParallelView = false; + }); + + expect(wrapper.contains('.container-limited.limit-container-width')).toBe(false); + }); + it('displays loading icon on loading', () => { createComponent({}, ({ state }) => { state.diffs.isLoading = true; diff --git a/spec/javascripts/diffs/components/compare_versions_spec.js b/spec/javascripts/diffs/components/compare_versions_spec.js index e886f962d2f..77f8352047c 100644 --- a/spec/javascripts/diffs/components/compare_versions_spec.js +++ b/spec/javascripts/diffs/components/compare_versions_spec.js @@ -66,6 +66,26 @@ describe('CompareVersions', () => { expect(inlineBtn.innerHTML).toContain('Inline'); expect(parallelBtn.innerHTML).toContain('Side-by-side'); }); + + it('adds container-limiting classes when showFileTree is false with inline diffs', () => { + vm.isLimitedContainer = true; + + vm.$nextTick(() => { + const limitedContainer = vm.$el.querySelector('.container-limited.limit-container-width'); + + expect(limitedContainer).not.toBeNull(); + }); + }); + + it('does not add container-limiting classes when showFileTree is false with inline diffs', () => { + vm.isLimitedContainer = false; + + vm.$nextTick(() => { + const limitedContainer = vm.$el.querySelector('.container-limited.limit-container-width'); + + expect(limitedContainer).toBeNull(); + }); + }); }); describe('setInlineDiffViewType', () => { diff --git a/spec/javascripts/diffs/components/diff_file_header_spec.js b/spec/javascripts/diffs/components/diff_file_header_spec.js index 6614069f44d..e1170c9762e 100644 --- a/spec/javascripts/diffs/components/diff_file_header_spec.js +++ b/spec/javascripts/diffs/components/diff_file_header_spec.js @@ -672,7 +672,7 @@ describe('diff_file_header', () => { vm = mountComponentWithStore(Component, { props, store }); - expect(vm.$el.querySelector('.js-expand-file').textContent).toContain('Show changes only'); + expect(vm.$el.querySelector('.ic-doc-changes')).not.toBeNull(); }); it('shows expand text', () => { @@ -680,7 +680,7 @@ describe('diff_file_header', () => { vm = mountComponentWithStore(Component, { props, store }); - expect(vm.$el.querySelector('.js-expand-file').textContent).toContain('Show full file'); + expect(vm.$el.querySelector('.ic-doc-expand')).not.toBeNull(); }); it('renders loading icon', () => { diff --git a/spec/javascripts/fixtures/issues.rb b/spec/javascripts/fixtures/issues.rb index 645b3aa788a..0f3f9a10f94 100644 --- a/spec/javascripts/fixtures/issues.rb +++ b/spec/javascripts/fixtures/issues.rb @@ -65,3 +65,61 @@ describe Projects::IssuesController, '(JavaScript fixtures)', type: :controller store_frontend_fixture(response, fixture_file_name) end end + +describe API::Issues, '(JavaScript fixtures)', type: :request do + include ApiHelpers + include JavaScriptFixturesHelpers + + def get_related_merge_requests(project_id, issue_iid, user = nil) + get api("/projects/#{project_id}/issues/#{issue_iid}/related_merge_requests", user) + end + + def create_referencing_mr(user, project, issue) + attributes = { + author: user, + source_project: project, + target_project: project, + source_branch: "master", + target_branch: "test", + assignee: user, + description: "See #{issue.to_reference}" + } + create(:merge_request, attributes).tap do |merge_request| + create(:note, :system, project: issue.project, noteable: issue, author: user, note: merge_request.to_reference(full: true)) + end + end + + it 'issues/related_merge_requests.json' do |example| + user = create(:user) + project = create(:project, :public, creator_id: user.id, namespace: user.namespace) + issue_title = 'foo' + issue_description = 'closed' + milestone = create(:milestone, title: '1.0.0', project: project) + issue = create :issue, + author: user, + assignees: [user], + project: project, + milestone: milestone, + created_at: generate(:past_time), + updated_at: 1.hour.ago, + title: issue_title, + description: issue_description + + project.add_reporter(user) + create_referencing_mr(user, project, issue) + + create(:merge_request, + :simple, + author: user, + source_project: project, + target_project: project, + description: "Some description") + project2 = create(:project, :public, creator_id: user.id, namespace: user.namespace) + create_referencing_mr(user, project2, issue).update!(head_pipeline: create(:ci_pipeline)) + + get_related_merge_requests(project.id, issue.iid, user) + + expect(response).to be_success + store_frontend_fixture(response, example.description) + end +end diff --git a/spec/javascripts/frequent_items/components/frequent_items_list_item_spec.js b/spec/javascripts/frequent_items/components/frequent_items_list_item_spec.js index 7deed985219..f00bc2eeb6d 100644 --- a/spec/javascripts/frequent_items/components/frequent_items_list_item_spec.js +++ b/spec/javascripts/frequent_items/components/frequent_items_list_item_spec.js @@ -1,25 +1,31 @@ import Vue from 'vue'; import frequentItemsListItemComponent from '~/frequent_items/components/frequent_items_list_item.vue'; -import mountComponent from 'spec/helpers/vue_mount_component_helper'; +import { shallowMount } from '@vue/test-utils'; +import { trimText } from 'spec/helpers/vue_component_helper'; import { mockProject } from '../mock_data'; // can also use 'mockGroup', but not useful to test here const createComponent = () => { const Component = Vue.extend(frequentItemsListItemComponent); - return mountComponent(Component, { - itemId: mockProject.id, - itemName: mockProject.name, - namespace: mockProject.namespace, - webUrl: mockProject.webUrl, - avatarUrl: mockProject.avatarUrl, + return shallowMount(Component, { + propsData: { + itemId: mockProject.id, + itemName: mockProject.name, + namespace: mockProject.namespace, + webUrl: mockProject.webUrl, + avatarUrl: mockProject.avatarUrl, + }, }); }; describe('FrequentItemsListItemComponent', () => { + let wrapper; let vm; beforeEach(() => { - vm = createComponent(); + wrapper = createComponent(); + + ({ vm } = wrapper); }); afterEach(() => { @@ -29,11 +35,11 @@ describe('FrequentItemsListItemComponent', () => { describe('computed', () => { describe('hasAvatar', () => { it('should return `true` or `false` if whether avatar is present or not', () => { - vm.avatarUrl = 'path/to/avatar.png'; + wrapper.setProps({ avatarUrl: 'path/to/avatar.png' }); expect(vm.hasAvatar).toBe(true); - vm.avatarUrl = null; + wrapper.setProps({ avatarUrl: null }); expect(vm.hasAvatar).toBe(false); }); @@ -41,41 +47,49 @@ describe('FrequentItemsListItemComponent', () => { describe('highlightedItemName', () => { it('should enclose part of project name in <b> & </b> which matches with `matcher` prop', () => { - vm.matcher = 'lab'; + wrapper.setProps({ matcher: 'lab' }); - expect(vm.highlightedItemName).toContain('<b>Lab</b>'); + expect(wrapper.find('.js-frequent-items-item-title').html()).toContain( + '<b>L</b><b>a</b><b>b</b>', + ); }); it('should return project name as it is if `matcher` is not available', () => { - vm.matcher = null; + wrapper.setProps({ matcher: null }); - expect(vm.highlightedItemName).toBe(mockProject.name); + expect(trimText(wrapper.find('.js-frequent-items-item-title').text())).toBe( + mockProject.name, + ); }); }); describe('truncatedNamespace', () => { it('should truncate project name from namespace string', () => { - vm.namespace = 'platform / nokia-3310'; + wrapper.setProps({ namespace: 'platform / nokia-3310' }); - expect(vm.truncatedNamespace).toBe('platform'); + expect(trimText(wrapper.find('.js-frequent-items-item-namespace').text())).toBe('platform'); }); it('should truncate namespace string from the middle if it includes more than two groups in path', () => { - vm.namespace = 'platform / hardware / broadcom / Wifi Group / Mobile Chipset / nokia-3310'; + wrapper.setProps({ + namespace: 'platform / hardware / broadcom / Wifi Group / Mobile Chipset / nokia-3310', + }); - expect(vm.truncatedNamespace).toBe('platform / ... / Mobile Chipset'); + expect(trimText(wrapper.find('.js-frequent-items-item-namespace').text())).toBe( + 'platform / ... / Mobile Chipset', + ); }); }); }); describe('template', () => { it('should render component element', () => { - expect(vm.$el.classList.contains('frequent-items-list-item-container')).toBeTruthy(); - expect(vm.$el.querySelectorAll('a').length).toBe(1); - expect(vm.$el.querySelectorAll('.frequent-items-item-avatar-container').length).toBe(1); - expect(vm.$el.querySelectorAll('.frequent-items-item-metadata-container').length).toBe(1); - expect(vm.$el.querySelectorAll('.frequent-items-item-title').length).toBe(1); - expect(vm.$el.querySelectorAll('.frequent-items-item-namespace').length).toBe(1); + expect(wrapper.classes()).toContain('frequent-items-list-item-container'); + expect(wrapper.findAll('a').length).toBe(1); + expect(wrapper.findAll('.frequent-items-item-avatar-container').length).toBe(1); + expect(wrapper.findAll('.frequent-items-item-metadata-container').length).toBe(1); + expect(wrapper.findAll('.frequent-items-item-title').length).toBe(1); + expect(wrapper.findAll('.frequent-items-item-namespace').length).toBe(1); }); }); }); diff --git a/spec/javascripts/frequent_items/components/frequent_items_search_input_spec.js b/spec/javascripts/frequent_items/components/frequent_items_search_input_spec.js index d564292f1ba..ddbbc5c2d29 100644 --- a/spec/javascripts/frequent_items/components/frequent_items_search_input_spec.js +++ b/spec/javascripts/frequent_items/components/frequent_items_search_input_spec.js @@ -1,19 +1,22 @@ import Vue from 'vue'; import searchComponent from '~/frequent_items/components/frequent_items_search_input.vue'; import eventHub from '~/frequent_items/event_hub'; -import mountComponent from 'spec/helpers/vue_mount_component_helper'; +import { shallowMount } from '@vue/test-utils'; const createComponent = (namespace = 'projects') => { const Component = Vue.extend(searchComponent); - return mountComponent(Component, { namespace }); + return shallowMount(Component, { propsData: { namespace } }); }; describe('FrequentItemsSearchInputComponent', () => { + let wrapper; let vm; beforeEach(() => { - vm = createComponent(); + wrapper = createComponent(); + + ({ vm } = wrapper); }); afterEach(() => { @@ -35,7 +38,7 @@ describe('FrequentItemsSearchInputComponent', () => { describe('mounted', () => { it('should listen `dropdownOpen` event', done => { spyOn(eventHub, '$on'); - const vmX = createComponent(); + const vmX = createComponent().vm; Vue.nextTick(() => { expect(eventHub.$on).toHaveBeenCalledWith( @@ -49,7 +52,7 @@ describe('FrequentItemsSearchInputComponent', () => { describe('beforeDestroy', () => { it('should unbind event listeners on eventHub', done => { - const vmX = createComponent(); + const vmX = createComponent().vm; spyOn(eventHub, '$off'); vmX.$mount(); @@ -67,12 +70,12 @@ describe('FrequentItemsSearchInputComponent', () => { describe('template', () => { it('should render component element', () => { - const inputEl = vm.$el.querySelector('input.form-control'); - - expect(vm.$el.classList.contains('search-input-container')).toBeTruthy(); - expect(inputEl).not.toBe(null); - expect(inputEl.getAttribute('placeholder')).toBe('Search your projects'); - expect(vm.$el.querySelector('.search-icon')).toBeDefined(); + expect(wrapper.classes()).toContain('search-input-container'); + expect(wrapper.contains('input.form-control')).toBe(true); + expect(wrapper.contains('.search-icon')).toBe(true); + expect(wrapper.find('input.form-control').attributes('placeholder')).toBe( + 'Search your projects', + ); }); }); }); diff --git a/spec/javascripts/ide/components/commit_sidebar/actions_spec.js b/spec/javascripts/ide/components/commit_sidebar/actions_spec.js index 3a5d6c8a90b..23e6a055518 100644 --- a/spec/javascripts/ide/components/commit_sidebar/actions_spec.js +++ b/spec/javascripts/ide/components/commit_sidebar/actions_spec.js @@ -1,5 +1,6 @@ import Vue from 'vue'; import store from '~/ide/stores'; +import consts from '~/ide/stores/modules/commit/constants'; import commitActions from '~/ide/components/commit_sidebar/actions.vue'; import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; import { resetStore } from 'spec/ide/helpers'; @@ -7,20 +8,33 @@ import { projectData } from 'spec/ide/mock_data'; describe('IDE commit sidebar actions', () => { let vm; - - beforeEach(done => { + const createComponent = ({ + hasMR = false, + commitAction = consts.COMMIT_TO_NEW_BRANCH, + mergeRequestsEnabled = true, + currentBranchId = 'master', + shouldCreateMR = false, + } = {}) => { const Component = Vue.extend(commitActions); vm = createComponentWithStore(Component, store); - vm.$store.state.currentBranchId = 'master'; + vm.$store.state.currentBranchId = currentBranchId; vm.$store.state.currentProjectId = 'abcproject'; + vm.$store.state.commit.commitAction = commitAction; Vue.set(vm.$store.state.projects, 'abcproject', { ...projectData }); + vm.$store.state.projects.abcproject.merge_requests_enabled = mergeRequestsEnabled; + vm.$store.state.commit.shouldCreateMR = shouldCreateMR; - vm.$mount(); + if (hasMR) { + vm.$store.state.currentMergeRequestId = '1'; + vm.$store.state.projects[store.state.currentProjectId].mergeRequests[ + store.state.currentMergeRequestId + ] = { foo: 'bar' }; + } - Vue.nextTick(done); - }); + return vm.$mount(); + }; afterEach(() => { vm.$destroy(); @@ -28,16 +42,20 @@ describe('IDE commit sidebar actions', () => { resetStore(vm.$store); }); - it('renders 3 groups', () => { - expect(vm.$el.querySelectorAll('input[type="radio"]').length).toBe(3); + it('renders 2 groups', () => { + createComponent(); + + expect(vm.$el.querySelectorAll('input[type="radio"]').length).toBe(2); }); it('renders current branch text', () => { + createComponent(); + expect(vm.$el.textContent).toContain('Commit to master branch'); }); it('hides merge request option when project merge requests are disabled', done => { - vm.$store.state.projects.abcproject.merge_requests_enabled = false; + createComponent({ mergeRequestsEnabled: false }); vm.$nextTick(() => { expect(vm.$el.querySelectorAll('input[type="radio"]').length).toBe(2); @@ -49,9 +67,53 @@ describe('IDE commit sidebar actions', () => { describe('commitToCurrentBranchText', () => { it('escapes current branch', () => { - vm.$store.state.currentBranchId = '<img src="x" />'; + const injectedSrc = '<img src="x" />'; + createComponent({ currentBranchId: injectedSrc }); + + expect(vm.commitToCurrentBranchText).not.toContain(injectedSrc); + }); + }); + + describe('create new MR checkbox', () => { + it('disables `createMR` button when an MR already exists and committing to current branch', () => { + createComponent({ hasMR: true, commitAction: consts.COMMIT_TO_CURRENT_BRANCH }); + + expect(vm.$el.querySelector('input[type="checkbox"]').disabled).toBe(true); + }); + + it('does not disable checkbox if MR does not exist', () => { + createComponent({ hasMR: false }); + + expect(vm.$el.querySelector('input[type="checkbox"]').disabled).toBe(false); + }); + + it('does not disable checkbox when creating a new branch', () => { + createComponent({ commitAction: consts.COMMIT_TO_NEW_BRANCH }); + + expect(vm.$el.querySelector('input[type="checkbox"]').disabled).toBe(false); + }); + + it('toggles off new MR when switching back to commit to current branch and MR exists', () => { + createComponent({ + commitAction: consts.COMMIT_TO_NEW_BRANCH, + shouldCreateMR: true, + }); + const currentBranchRadio = vm.$el.querySelector( + `input[value="${consts.COMMIT_TO_CURRENT_BRANCH}"`, + ); + currentBranchRadio.click(); + + vm.$nextTick(() => { + expect(vm.$store.state.commit.shouldCreateMR).toBe(false); + }); + }); + + it('toggles `shouldCreateMR` when clicking checkbox', () => { + createComponent(); + const el = vm.$el.querySelector('input[type="checkbox"]'); + el.dispatchEvent(new Event('change')); - expect(vm.commitToCurrentBranchText).not.toContain('<img src="x" />'); + expect(vm.$store.state.commit.shouldCreateMR).toBe(true); }); }); }); diff --git a/spec/javascripts/ide/components/file_row_extra_spec.js b/spec/javascripts/ide/components/file_row_extra_spec.js index c93a939ad71..d7fed3f0681 100644 --- a/spec/javascripts/ide/components/file_row_extra_spec.js +++ b/spec/javascripts/ide/components/file_row_extra_spec.js @@ -20,7 +20,7 @@ describe('IDE extra file row component', () => { file: { ...file('test'), }, - mouseOver: false, + dropdownOpen: false, }); spyOnProperty(vm, 'getUnstagedFilesCountForPath').and.returnValue(() => unstagedFilesCount); diff --git a/spec/javascripts/ide/components/new_dropdown/index_spec.js b/spec/javascripts/ide/components/new_dropdown/index_spec.js index 83e530f0a6a..aaebe88f314 100644 --- a/spec/javascripts/ide/components/new_dropdown/index_spec.js +++ b/spec/javascripts/ide/components/new_dropdown/index_spec.js @@ -56,11 +56,11 @@ describe('new dropdown component', () => { }); }); - describe('dropdownOpen', () => { + describe('isOpen', () => { it('scrolls dropdown into view', done => { spyOn(vm.$refs.dropdownMenu, 'scrollIntoView'); - vm.dropdownOpen = true; + vm.isOpen = true; setTimeout(() => { expect(vm.$refs.dropdownMenu.scrollIntoView).toHaveBeenCalledWith({ diff --git a/spec/javascripts/ide/stores/actions/merge_request_spec.js b/spec/javascripts/ide/stores/actions/merge_request_spec.js index a5839630657..4dd0c1150eb 100644 --- a/spec/javascripts/ide/stores/actions/merge_request_spec.js +++ b/spec/javascripts/ide/stores/actions/merge_request_spec.js @@ -2,7 +2,6 @@ import MockAdapter from 'axios-mock-adapter'; import axios from '~/lib/utils/axios_utils'; import store from '~/ide/stores'; import actions, { - getMergeRequestsForBranch, getMergeRequestData, getMergeRequestChanges, getMergeRequestVersions, @@ -12,13 +11,17 @@ import service from '~/ide/services'; import { activityBarViews } from '~/ide/constants'; import { resetStore } from '../../helpers'; +const TEST_PROJECT = 'abcproject'; +const TEST_PROJECT_ID = 17; + describe('IDE store merge request actions', () => { let mock; beforeEach(() => { mock = new MockAdapter(axios); - store.state.projects.abcproject = { + store.state.projects[TEST_PROJECT] = { + id: TEST_PROJECT_ID, mergeRequests: {}, }; }); @@ -41,10 +44,11 @@ describe('IDE store merge request actions', () => { it('calls getProjectMergeRequests service method', done => { store - .dispatch('getMergeRequestsForBranch', { projectId: 'abcproject', branchId: 'bar' }) + .dispatch('getMergeRequestsForBranch', { projectId: TEST_PROJECT, branchId: 'bar' }) .then(() => { - expect(service.getProjectMergeRequests).toHaveBeenCalledWith('abcproject', { + expect(service.getProjectMergeRequests).toHaveBeenCalledWith(TEST_PROJECT, { source_branch: 'bar', + source_project_id: TEST_PROJECT_ID, order_by: 'created_at', per_page: 1, }); @@ -56,13 +60,11 @@ describe('IDE store merge request actions', () => { it('sets the "Merge Request" Object', done => { store - .dispatch('getMergeRequestsForBranch', { projectId: 'abcproject', branchId: 'bar' }) + .dispatch('getMergeRequestsForBranch', { projectId: TEST_PROJECT, branchId: 'bar' }) .then(() => { - expect(Object.keys(store.state.projects.abcproject.mergeRequests).length).toEqual(1); - expect(Object.keys(store.state.projects.abcproject.mergeRequests)[0]).toEqual('2'); - expect(store.state.projects.abcproject.mergeRequests[2]).toEqual( - jasmine.objectContaining(mrData), - ); + expect(store.state.projects.abcproject.mergeRequests).toEqual({ + '2': jasmine.objectContaining(mrData), + }); done(); }) .catch(done.fail); @@ -70,7 +72,7 @@ describe('IDE store merge request actions', () => { it('sets "Current Merge Request" object to the most recent MR', done => { store - .dispatch('getMergeRequestsForBranch', { projectId: 'abcproject', branchId: 'bar' }) + .dispatch('getMergeRequestsForBranch', { projectId: TEST_PROJECT, branchId: 'bar' }) .then(() => { expect(store.state.currentMergeRequestId).toEqual('2'); done(); @@ -87,9 +89,9 @@ describe('IDE store merge request actions', () => { it('does not fail if there are no merge requests for current branch', done => { store - .dispatch('getMergeRequestsForBranch', { projectId: 'abcproject', branchId: 'foo' }) + .dispatch('getMergeRequestsForBranch', { projectId: TEST_PROJECT, branchId: 'foo' }) .then(() => { - expect(Object.keys(store.state.projects.abcproject.mergeRequests).length).toEqual(0); + expect(store.state.projects[TEST_PROJECT].mergeRequests).toEqual({}); expect(store.state.currentMergeRequestId).toEqual(''); done(); }) @@ -106,7 +108,8 @@ describe('IDE store merge request actions', () => { it('flashes message, if error', done => { const flashSpy = spyOnDependency(actions, 'flash'); - getMergeRequestsForBranch({ commit() {} }, { projectId: 'abcproject', branchId: 'bar' }) + store + .dispatch('getMergeRequestsForBranch', { projectId: TEST_PROJECT, branchId: 'bar' }) .then(() => { fail('Expected getMergeRequestsForBranch to throw an error'); }) @@ -132,9 +135,9 @@ describe('IDE store merge request actions', () => { it('calls getProjectMergeRequestData service method', done => { store - .dispatch('getMergeRequestData', { projectId: 'abcproject', mergeRequestId: 1 }) + .dispatch('getMergeRequestData', { projectId: TEST_PROJECT, mergeRequestId: 1 }) .then(() => { - expect(service.getProjectMergeRequestData).toHaveBeenCalledWith('abcproject', 1, { + expect(service.getProjectMergeRequestData).toHaveBeenCalledWith(TEST_PROJECT, 1, { render_html: true, }); @@ -145,10 +148,12 @@ describe('IDE store merge request actions', () => { it('sets the Merge Request Object', done => { store - .dispatch('getMergeRequestData', { projectId: 'abcproject', mergeRequestId: 1 }) + .dispatch('getMergeRequestData', { projectId: TEST_PROJECT, mergeRequestId: 1 }) .then(() => { - expect(store.state.projects.abcproject.mergeRequests['1'].title).toBe('mergerequest'); expect(store.state.currentMergeRequestId).toBe(1); + expect(store.state.projects[TEST_PROJECT].mergeRequests['1'].title).toBe( + 'mergerequest', + ); done(); }) @@ -170,7 +175,7 @@ describe('IDE store merge request actions', () => { dispatch, state: store.state, }, - { projectId: 'abcproject', mergeRequestId: 1 }, + { projectId: TEST_PROJECT, mergeRequestId: 1 }, ) .then(done.fail) .catch(() => { @@ -179,7 +184,7 @@ describe('IDE store merge request actions', () => { action: jasmine.any(Function), actionText: 'Please try again', actionPayload: { - projectId: 'abcproject', + projectId: TEST_PROJECT, mergeRequestId: 1, force: false, }, @@ -193,7 +198,7 @@ describe('IDE store merge request actions', () => { describe('getMergeRequestChanges', () => { beforeEach(() => { - store.state.projects.abcproject.mergeRequests['1'] = { changes: [] }; + store.state.projects[TEST_PROJECT].mergeRequests['1'] = { changes: [] }; }); describe('success', () => { @@ -207,9 +212,9 @@ describe('IDE store merge request actions', () => { it('calls getProjectMergeRequestChanges service method', done => { store - .dispatch('getMergeRequestChanges', { projectId: 'abcproject', mergeRequestId: 1 }) + .dispatch('getMergeRequestChanges', { projectId: TEST_PROJECT, mergeRequestId: 1 }) .then(() => { - expect(service.getProjectMergeRequestChanges).toHaveBeenCalledWith('abcproject', 1); + expect(service.getProjectMergeRequestChanges).toHaveBeenCalledWith(TEST_PROJECT, 1); done(); }) @@ -218,9 +223,9 @@ describe('IDE store merge request actions', () => { it('sets the Merge Request Changes Object', done => { store - .dispatch('getMergeRequestChanges', { projectId: 'abcproject', mergeRequestId: 1 }) + .dispatch('getMergeRequestChanges', { projectId: TEST_PROJECT, mergeRequestId: 1 }) .then(() => { - expect(store.state.projects.abcproject.mergeRequests['1'].changes.title).toBe( + expect(store.state.projects[TEST_PROJECT].mergeRequests['1'].changes.title).toBe( 'mergerequest', ); done(); @@ -243,7 +248,7 @@ describe('IDE store merge request actions', () => { dispatch, state: store.state, }, - { projectId: 'abcproject', mergeRequestId: 1 }, + { projectId: TEST_PROJECT, mergeRequestId: 1 }, ) .then(done.fail) .catch(() => { @@ -252,7 +257,7 @@ describe('IDE store merge request actions', () => { action: jasmine.any(Function), actionText: 'Please try again', actionPayload: { - projectId: 'abcproject', + projectId: TEST_PROJECT, mergeRequestId: 1, force: false, }, @@ -266,7 +271,7 @@ describe('IDE store merge request actions', () => { describe('getMergeRequestVersions', () => { beforeEach(() => { - store.state.projects.abcproject.mergeRequests['1'] = { versions: [] }; + store.state.projects[TEST_PROJECT].mergeRequests['1'] = { versions: [] }; }); describe('success', () => { @@ -279,9 +284,9 @@ describe('IDE store merge request actions', () => { it('calls getProjectMergeRequestVersions service method', done => { store - .dispatch('getMergeRequestVersions', { projectId: 'abcproject', mergeRequestId: 1 }) + .dispatch('getMergeRequestVersions', { projectId: TEST_PROJECT, mergeRequestId: 1 }) .then(() => { - expect(service.getProjectMergeRequestVersions).toHaveBeenCalledWith('abcproject', 1); + expect(service.getProjectMergeRequestVersions).toHaveBeenCalledWith(TEST_PROJECT, 1); done(); }) @@ -290,9 +295,9 @@ describe('IDE store merge request actions', () => { it('sets the Merge Request Versions Object', done => { store - .dispatch('getMergeRequestVersions', { projectId: 'abcproject', mergeRequestId: 1 }) + .dispatch('getMergeRequestVersions', { projectId: TEST_PROJECT, mergeRequestId: 1 }) .then(() => { - expect(store.state.projects.abcproject.mergeRequests['1'].versions.length).toBe(1); + expect(store.state.projects[TEST_PROJECT].mergeRequests['1'].versions.length).toBe(1); done(); }) .catch(done.fail); @@ -313,7 +318,7 @@ describe('IDE store merge request actions', () => { dispatch, state: store.state, }, - { projectId: 'abcproject', mergeRequestId: 1 }, + { projectId: TEST_PROJECT, mergeRequestId: 1 }, ) .then(done.fail) .catch(() => { @@ -322,7 +327,7 @@ describe('IDE store merge request actions', () => { action: jasmine.any(Function), actionText: 'Please try again', actionPayload: { - projectId: 'abcproject', + projectId: TEST_PROJECT, mergeRequestId: 1, force: false, }, @@ -336,7 +341,7 @@ describe('IDE store merge request actions', () => { describe('openMergeRequest', () => { const mr = { - projectId: 'abcproject', + projectId: TEST_PROJECT, targetProjectId: 'defproject', mergeRequestId: 2, }; diff --git a/spec/javascripts/ide/stores/actions/project_spec.js b/spec/javascripts/ide/stores/actions/project_spec.js index 7b0963713fb..cd519eaed7c 100644 --- a/spec/javascripts/ide/stores/actions/project_spec.js +++ b/spec/javascripts/ide/stores/actions/project_spec.js @@ -279,5 +279,22 @@ describe('IDE store project actions', () => { .then(done) .catch(done.fail); }); + + it('creates a new file supplied via URL if the file does not exist yet', done => { + openBranch(store, { ...branch, basePath: 'not-existent.md' }) + .then(() => { + expect(store.dispatch).not.toHaveBeenCalledWith( + 'handleTreeEntryAction', + jasmine.anything(), + ); + + expect(store.dispatch).toHaveBeenCalledWith('createTempEntry', { + name: 'not-existent.md', + type: 'blob', + }); + }) + .then(done) + .catch(done.fail); + }); }); }); diff --git a/spec/javascripts/ide/stores/actions/tree_spec.js b/spec/javascripts/ide/stores/actions/tree_spec.js index fbb676aab33..5ed9b9003a7 100644 --- a/spec/javascripts/ide/stores/actions/tree_spec.js +++ b/spec/javascripts/ide/stores/actions/tree_spec.js @@ -1,6 +1,6 @@ import MockAdapter from 'axios-mock-adapter'; import testAction from 'spec/helpers/vuex_action_helper'; -import { showTreeEntry, getFiles } from '~/ide/stores/actions/tree'; +import { showTreeEntry, getFiles, setDirectoryData } from '~/ide/stores/actions/tree'; import * as types from '~/ide/stores/mutation_types'; import axios from '~/lib/utils/axios_utils'; import store from '~/ide/stores'; @@ -206,4 +206,35 @@ describe('Multi-file store tree actions', () => { ); }); }); + + describe('setDirectoryData', () => { + it('sets tree correctly if there are no opened files yet', done => { + const treeFile = file({ name: 'README.md' }); + store.state.trees['abcproject/master'] = {}; + + testAction( + setDirectoryData, + { projectId: 'abcproject', branchId: 'master', treeList: [treeFile] }, + store.state, + [ + { + type: types.SET_DIRECTORY_DATA, + payload: { + treePath: 'abcproject/master', + data: [treeFile], + }, + }, + { + type: types.TOGGLE_LOADING, + payload: { + entry: {}, + forceValue: false, + }, + }, + ], + [], + done, + ); + }); + }); }); diff --git a/spec/javascripts/ide/stores/modules/commit/actions_spec.js b/spec/javascripts/ide/stores/modules/commit/actions_spec.js index 34d97347438..abc97f3072c 100644 --- a/spec/javascripts/ide/stores/modules/commit/actions_spec.js +++ b/spec/javascripts/ide/stores/modules/commit/actions_spec.js @@ -3,7 +3,7 @@ import store from '~/ide/stores'; import service from '~/ide/services'; import router from '~/ide/ide_router'; import eventHub from '~/ide/eventhub'; -import * as consts from '~/ide/stores/modules/commit/constants'; +import consts from '~/ide/stores/modules/commit/constants'; import { resetStore, file } from 'spec/ide/helpers'; describe('IDE commit module actions', () => { @@ -389,7 +389,8 @@ describe('IDE commit module actions', () => { it('redirects to new merge request page', done => { spyOn(eventHub, '$on'); - store.state.commit.commitAction = '3'; + store.state.commit.commitAction = consts.COMMIT_TO_NEW_BRANCH; + store.state.commit.shouldCreateMR = true; store .dispatch('commit/commitChanges') @@ -405,6 +406,21 @@ describe('IDE commit module actions', () => { .catch(done.fail); }); + it('does not redirect to new merge request page when shouldCreateMR is not checked', done => { + spyOn(eventHub, '$on'); + + store.state.commit.commitAction = consts.COMMIT_TO_NEW_BRANCH; + store.state.commit.shouldCreateMR = false; + + store + .dispatch('commit/commitChanges') + .then(() => { + expect(visitUrl).not.toHaveBeenCalled(); + done(); + }) + .catch(done.fail); + }); + it('resets changed files before redirecting', done => { spyOn(eventHub, '$on'); diff --git a/spec/javascripts/ide/stores/modules/commit/getters_spec.js b/spec/javascripts/ide/stores/modules/commit/getters_spec.js index 702e78ef773..e00fd7199d7 100644 --- a/spec/javascripts/ide/stores/modules/commit/getters_spec.js +++ b/spec/javascripts/ide/stores/modules/commit/getters_spec.js @@ -1,5 +1,5 @@ import commitState from '~/ide/stores/modules/commit/state'; -import * as consts from '~/ide/stores/modules/commit/constants'; +import consts from '~/ide/stores/modules/commit/constants'; import * as getters from '~/ide/stores/modules/commit/getters'; describe('IDE commit module getters', () => { @@ -46,7 +46,7 @@ describe('IDE commit module getters', () => { currentBranchId: 'master', }; const localGetters = { - placeholderBranchName: 'newBranchName', + placeholderBranchName: 'placeholder-branch-name', }; beforeEach(() => { @@ -59,25 +59,28 @@ describe('IDE commit module getters', () => { expect(getters.branchName(state, null, rootState)).toBe('master'); }); - ['COMMIT_TO_NEW_BRANCH', 'COMMIT_TO_NEW_BRANCH_MR'].forEach(type => { - describe(type, () => { - beforeEach(() => { - Object.assign(state, { - commitAction: consts[type], - }); + describe('COMMIT_TO_NEW_BRANCH', () => { + beforeEach(() => { + Object.assign(state, { + commitAction: consts.COMMIT_TO_NEW_BRANCH, }); + }); - it('uses newBranchName when not empty', () => { - expect(getters.branchName(state, localGetters, rootState)).toBe('state-newBranchName'); + it('uses newBranchName when not empty', () => { + const newBranchName = 'nonempty-branch-name'; + Object.assign(state, { + newBranchName, }); - it('uses placeholderBranchName when state newBranchName is empty', () => { - Object.assign(state, { - newBranchName: '', - }); + expect(getters.branchName(state, localGetters, rootState)).toBe(newBranchName); + }); - expect(getters.branchName(state, localGetters, rootState)).toBe('newBranchName'); + it('uses placeholderBranchName when state newBranchName is empty', () => { + Object.assign(state, { + newBranchName: '', }); + + expect(getters.branchName(state, localGetters, rootState)).toBe('placeholder-branch-name'); }); }); }); @@ -141,4 +144,33 @@ describe('IDE commit module getters', () => { }); }); }); + + describe('shouldDisableNewMrOption', () => { + it('returns false if commitAction `COMMIT_TO_NEW_BRANCH`', () => { + state.commitAction = consts.COMMIT_TO_NEW_BRANCH; + const rootState = { + currentMergeRequest: { foo: 'bar' }, + }; + + expect(getters.shouldDisableNewMrOption(state, null, null, rootState)).toBeFalsy(); + }); + + it('returns false if there is no current merge request', () => { + state.commitAction = consts.COMMIT_TO_CURRENT_BRANCH; + const rootState = { + currentMergeRequest: null, + }; + + expect(getters.shouldDisableNewMrOption(state, null, null, rootState)).toBeFalsy(); + }); + + it('returns true an MR exists and commit action is `COMMIT_TO_CURRENT_BRANCH`', () => { + state.commitAction = consts.COMMIT_TO_CURRENT_BRANCH; + const rootState = { + currentMergeRequest: { foo: 'bar' }, + }; + + expect(getters.shouldDisableNewMrOption(state, null, null, rootState)).toBeTruthy(); + }); + }); }); diff --git a/spec/javascripts/ide/stores/mutations/tree_spec.js b/spec/javascripts/ide/stores/mutations/tree_spec.js index 67e9f7509da..7f9c978aa46 100644 --- a/spec/javascripts/ide/stores/mutations/tree_spec.js +++ b/spec/javascripts/ide/stores/mutations/tree_spec.js @@ -26,17 +26,11 @@ describe('Multi-file store tree mutations', () => { }); describe('SET_DIRECTORY_DATA', () => { - const data = [ - { - name: 'tree', - }, - { - name: 'submodule', - }, - { - name: 'blob', - }, - ]; + let data; + + beforeEach(() => { + data = [file('tree'), file('foo'), file('blob')]; + }); it('adds directory data', () => { localState.trees['project/master'] = { @@ -52,7 +46,7 @@ describe('Multi-file store tree mutations', () => { expect(tree.tree.length).toBe(3); expect(tree.tree[0].name).toBe('tree'); - expect(tree.tree[1].name).toBe('submodule'); + expect(tree.tree[1].name).toBe('foo'); expect(tree.tree[2].name).toBe('blob'); }); @@ -65,6 +59,49 @@ describe('Multi-file store tree mutations', () => { expect(localState.trees['project/master'].loading).toBe(true); }); + + it('does not override tree already in state, but merges the two with correct order', () => { + const openedFile = file('new'); + + localState.trees['project/master'] = { + loading: true, + tree: [openedFile], + }; + + mutations.SET_DIRECTORY_DATA(localState, { + data, + treePath: 'project/master', + }); + + const { tree } = localState.trees['project/master']; + + expect(tree.length).toBe(4); + expect(tree[0].name).toBe('blob'); + expect(tree[1].name).toBe('foo'); + expect(tree[2].name).toBe('new'); + expect(tree[3].name).toBe('tree'); + }); + + it('returns tree unchanged if the opened file is already in the tree', () => { + const openedFile = file('foo'); + localState.trees['project/master'] = { + loading: true, + tree: [openedFile], + }; + + mutations.SET_DIRECTORY_DATA(localState, { + data, + treePath: 'project/master', + }); + + const { tree } = localState.trees['project/master']; + + expect(tree.length).toBe(3); + + expect(tree[0].name).toBe('tree'); + expect(tree[1].name).toBe('foo'); + expect(tree[2].name).toBe('blob'); + }); }); describe('REMOVE_ALL_CHANGES_FILES', () => { diff --git a/spec/javascripts/ide/stores/utils_spec.js b/spec/javascripts/ide/stores/utils_spec.js index 9f18034f8a3..c4f122efdda 100644 --- a/spec/javascripts/ide/stores/utils_spec.js +++ b/spec/javascripts/ide/stores/utils_spec.js @@ -235,4 +235,129 @@ describe('Multi-file store utils', () => { ]); }); }); + + describe('mergeTrees', () => { + let fromTree; + let toTree; + + beforeEach(() => { + fromTree = [file('foo')]; + toTree = [file('bar')]; + }); + + it('merges simple trees with sorting the result', () => { + toTree = [file('beta'), file('alpha'), file('gamma')]; + const res = utils.mergeTrees(fromTree, toTree); + + expect(res.length).toEqual(4); + expect(res[0].name).toEqual('alpha'); + expect(res[1].name).toEqual('beta'); + expect(res[2].name).toEqual('foo'); + expect(res[3].name).toEqual('gamma'); + expect(res[2]).toBe(fromTree[0]); + }); + + it('handles edge cases', () => { + expect(utils.mergeTrees({}, []).length).toEqual(0); + + let res = utils.mergeTrees({}, toTree); + + expect(res.length).toEqual(1); + expect(res[0].name).toEqual('bar'); + + res = utils.mergeTrees(fromTree, []); + + expect(res.length).toEqual(1); + expect(res[0].name).toEqual('foo'); + expect(res[0]).toBe(fromTree[0]); + }); + + it('merges simple trees without producing duplicates', () => { + toTree.push(file('foo')); + + const res = utils.mergeTrees(fromTree, toTree); + + expect(res.length).toEqual(2); + expect(res[0].name).toEqual('bar'); + expect(res[1].name).toEqual('foo'); + expect(res[1]).not.toBe(fromTree[0]); + }); + + it('merges nested tree into the main one without duplicates', () => { + fromTree[0].tree.push({ + ...file('alpha'), + path: 'foo/alpha', + tree: [ + { + ...file('beta.md'), + path: 'foo/alpha/beta.md', + }, + ], + }); + + toTree.push({ + ...file('foo'), + tree: [ + { + ...file('alpha'), + path: 'foo/alpha', + tree: [ + { + ...file('gamma.md'), + path: 'foo/alpha/gamma.md', + }, + ], + }, + ], + }); + + const res = utils.mergeTrees(fromTree, toTree); + + expect(res.length).toEqual(2); + expect(res[1].name).toEqual('foo'); + + const finalBranch = res[1].tree[0].tree; + + expect(finalBranch.length).toEqual(2); + expect(finalBranch[0].name).toEqual('beta.md'); + expect(finalBranch[1].name).toEqual('gamma.md'); + }); + + it('marks correct folders as opened as the parsing goes on', () => { + fromTree[0].tree.push({ + ...file('alpha'), + path: 'foo/alpha', + tree: [ + { + ...file('beta.md'), + path: 'foo/alpha/beta.md', + }, + ], + }); + + toTree.push({ + ...file('foo'), + tree: [ + { + ...file('alpha'), + path: 'foo/alpha', + tree: [ + { + ...file('gamma.md'), + path: 'foo/alpha/gamma.md', + }, + ], + }, + ], + }); + + const res = utils.mergeTrees(fromTree, toTree); + + expect(res[1].name).toEqual('foo'); + expect(res[1].opened).toEqual(true); + + expect(res[1].tree[0].name).toEqual('alpha'); + expect(res[1].tree[0].opened).toEqual(true); + }); + }); }); diff --git a/spec/javascripts/jobs/components/job_app_spec.js b/spec/javascripts/jobs/components/job_app_spec.js index ba5d672f189..cef40117304 100644 --- a/spec/javascripts/jobs/components/job_app_spec.js +++ b/spec/javascripts/jobs/components/job_app_spec.js @@ -17,6 +17,7 @@ describe('Job App ', () => { const props = { endpoint: `${gl.TEST_HOST}jobs/123.json`, runnerHelpUrl: 'help/runner', + deploymentHelpUrl: 'help/deployment', runnerSettingsUrl: 'settings/ci-cd/runners', terminalPath: 'jobs/123/terminal', pagePath: `${gl.TEST_HOST}jobs/123`, @@ -253,6 +254,41 @@ describe('Job App ', () => { }); }); + describe('unmet prerequisites block', () => { + it('renders unmet prerequisites block when there is an unmet prerequisites failure', done => { + mock.onGet(props.endpoint).replyOnce( + 200, + Object.assign({}, job, { + status: { + group: 'failed', + icon: 'status_failed', + label: 'failed', + text: 'failed', + details_path: 'path', + illustration: { + content: 'Retry this job in order to create the necessary resources.', + image: 'path', + size: 'svg-430', + title: 'Failed to create resources', + }, + }, + failure_reason: 'unmet_prerequisites', + has_trace: false, + runners: { + available: true, + }, + tags: [], + }), + ); + vm = mountComponentWithStore(Component, { props, store }); + + setTimeout(() => { + expect(vm.$el.querySelector('.js-job-failed')).not.toBeNull(); + done(); + }, 0); + }); + }); + describe('environments block', () => { it('renders environment block when job has environment', done => { mock.onGet(props.endpoint).replyOnce( diff --git a/spec/javascripts/jobs/components/unmet_prerequisites_block_spec.js b/spec/javascripts/jobs/components/unmet_prerequisites_block_spec.js new file mode 100644 index 00000000000..68fcb321214 --- /dev/null +++ b/spec/javascripts/jobs/components/unmet_prerequisites_block_spec.js @@ -0,0 +1,37 @@ +import Vue from 'vue'; +import component from '~/jobs/components/unmet_prerequisites_block.vue'; +import mountComponent from '../../helpers/vue_mount_component_helper'; + +describe('Unmet Prerequisites Block Job component', () => { + const Component = Vue.extend(component); + let vm; + const helpPath = '/user/project/clusters/index.html#troubleshooting-failed-deployment-jobs'; + + beforeEach(() => { + vm = mountComponent(Component, { + hasNoRunnersForProject: true, + helpPath, + }); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('renders an alert with the correct message', () => { + const container = vm.$el.querySelector('.js-failed-unmet-prerequisites'); + const alertMessage = + 'This job failed because the necessary resources were not successfully created.'; + + expect(container).not.toBeNull(); + expect(container.innerHTML).toContain(alertMessage); + }); + + it('renders link to help page', () => { + const helpLink = vm.$el.querySelector('.js-help-path'); + + expect(helpLink).not.toBeNull(); + expect(helpLink.innerHTML).toContain('More information'); + expect(helpLink.getAttribute('href')).toEqual(helpPath); + }); +}); diff --git a/spec/javascripts/lib/utils/common_utils_spec.js b/spec/javascripts/lib/utils/common_utils_spec.js index 2084c36e484..da012e1d5f7 100644 --- a/spec/javascripts/lib/utils/common_utils_spec.js +++ b/spec/javascripts/lib/utils/common_utils_spec.js @@ -2,7 +2,7 @@ import axios from '~/lib/utils/axios_utils'; import * as commonUtils from '~/lib/utils/common_utils'; import MockAdapter from 'axios-mock-adapter'; import { faviconDataUrl, overlayDataUrl, faviconWithOverlayDataUrl } from './mock_data'; -import BreakpointInstance from '~/breakpoints'; +import breakpointInstance from '~/breakpoints'; const PIXEL_TOLERANCE = 0.2; @@ -383,7 +383,7 @@ describe('common_utils', () => { describe('contentTop', () => { it('does not add height for fileTitle or compareVersionsHeader if screen is too small', () => { - spyOn(BreakpointInstance, 'getBreakpointSize').and.returnValue('sm'); + spyOn(breakpointInstance, 'isDesktop').and.returnValue(false); setFixtures(` <div class="diff-file file-title-flex-parent"> @@ -398,7 +398,7 @@ describe('common_utils', () => { }); it('adds height for fileTitle and compareVersionsHeader screen is large enough', () => { - spyOn(BreakpointInstance, 'getBreakpointSize').and.returnValue('lg'); + spyOn(breakpointInstance, 'isDesktop').and.returnValue(true); setFixtures(` <div class="diff-file file-title-flex-parent"> diff --git a/spec/javascripts/lib/utils/higlight_spec.js b/spec/javascripts/lib/utils/higlight_spec.js new file mode 100644 index 00000000000..638bbf65ae9 --- /dev/null +++ b/spec/javascripts/lib/utils/higlight_spec.js @@ -0,0 +1,43 @@ +import highlight from '~/lib/utils/highlight'; + +describe('highlight', () => { + it(`should appropriately surround substring matches`, () => { + const expected = 'g<b>i</b><b>t</b>lab'; + + expect(highlight('gitlab', 'it')).toBe(expected); + }); + + it(`should return an empty string in the case of invalid inputs`, () => { + [null, undefined].forEach(input => { + expect(highlight(input, 'match')).toBe(''); + }); + }); + + it(`should return the original value if match is null, undefined, or ''`, () => { + [null, undefined].forEach(match => { + expect(highlight('gitlab', match)).toBe('gitlab'); + }); + }); + + it(`should highlight matches in non-string inputs`, () => { + const expected = '123<b>4</b><b>5</b>6'; + + expect(highlight(123456, 45)).toBe(expected); + }); + + it(`should sanitize the input string before highlighting matches`, () => { + const expected = 'hello <b>w</b>orld'; + + expect(highlight('hello <b>world</b>', 'w')).toBe(expected); + }); + + it(`should not highlight anything if no matches are found`, () => { + expect(highlight('gitlab', 'hello')).toBe('gitlab'); + }); + + it(`should allow wrapping elements to be customized`, () => { + const expected = '1<hello>2</hello>3'; + + expect(highlight('123', '2', '<hello>', '</hello>')).toBe(expected); + }); +}); diff --git a/spec/javascripts/monitoring/charts/area_spec.js b/spec/javascripts/monitoring/charts/area_spec.js index 549a7935c0f..41a6c04efb9 100644 --- a/spec/javascripts/monitoring/charts/area_spec.js +++ b/spec/javascripts/monitoring/charts/area_spec.js @@ -1,5 +1,5 @@ import { shallowMount } from '@vue/test-utils'; -import { GlAreaChart } from '@gitlab/ui/dist/charts'; +import { GlAreaChart, GlChartSeriesLabel } from '@gitlab/ui/dist/charts'; import { shallowWrapperContainsSlotText } from 'spec/helpers/vue_test_utils_helper'; import Area from '~/monitoring/components/charts/area.vue'; import MonitoringStore from '~/monitoring/stores/monitoring_store'; @@ -65,7 +65,7 @@ describe('Area component', () => { expect(props.data).toBe(areaChart.vm.chartData); expect(props.option).toBe(areaChart.vm.chartOptions); expect(props.formatTooltipText).toBe(areaChart.vm.formatTooltipText); - expect(props.thresholds).toBe(areaChart.props('alertData')); + expect(props.thresholds).toBe(areaChart.vm.thresholds); }); it('recieves a tooltip title', () => { @@ -105,12 +105,13 @@ describe('Area component', () => { seriesName: areaChart.vm.chartData[0].name, componentSubType: type, value: [mockDate, 5.55555], + seriesIndex: 0, }, ], value: mockDate, }); - describe('series is of line type', () => { + describe('when series is of line type', () => { beforeEach(() => { areaChart.vm.formatTooltipText(generateSeriesData('line')); }); @@ -120,18 +121,20 @@ describe('Area component', () => { }); it('formats tooltip content', () => { - expect(areaChart.vm.tooltip.content).toEqual([{ name: 'Core Usage', value: '5.556' }]); + const name = 'Core Usage'; + const value = '5.556'; + const seriesLabel = areaChart.find(GlChartSeriesLabel); + + expect(seriesLabel.vm.color).toBe(''); + expect(shallowWrapperContainsSlotText(seriesLabel, 'default', name)).toBe(true); + expect(areaChart.vm.tooltip.content).toEqual([{ name, value, color: undefined }]); expect( - shallowWrapperContainsSlotText( - areaChart.find(GlAreaChart), - 'tooltipContent', - 'Core Usage 5.556', - ), + shallowWrapperContainsSlotText(areaChart.find(GlAreaChart), 'tooltipContent', value), ).toBe(true); }); }); - describe('series is of scatter type', () => { + describe('when series is of scatter type', () => { beforeEach(() => { areaChart.vm.formatTooltipText(generateSeriesData('scatter')); }); diff --git a/spec/javascripts/monitoring/dashboard_spec.js b/spec/javascripts/monitoring/dashboard_spec.js index 454777fa912..ce2c6c43c0f 100644 --- a/spec/javascripts/monitoring/dashboard_spec.js +++ b/spec/javascripts/monitoring/dashboard_spec.js @@ -1,6 +1,7 @@ import Vue from 'vue'; import MockAdapter from 'axios-mock-adapter'; import Dashboard from '~/monitoring/components/dashboard.vue'; +import { timeWindows } from '~/monitoring/constants'; import axios from '~/lib/utils/axios_utils'; import { metricsGroupsAPIResponse, mockApiEndpoint, environmentData } from './mock_data'; @@ -50,7 +51,7 @@ describe('Dashboard', () => { it('shows a getting started empty state when no metrics are present', () => { const component = new DashboardComponent({ el: document.querySelector('.prometheus-graphs'), - propsData, + propsData: { ...propsData, showTimeWindowDropdown: false }, }); expect(component.$el.querySelector('.prometheus-graphs')).toBe(null); @@ -66,7 +67,7 @@ describe('Dashboard', () => { it('shows up a loading state', done => { const component = new DashboardComponent({ el: document.querySelector('.prometheus-graphs'), - propsData: { ...propsData, hasMetrics: true }, + propsData: { ...propsData, hasMetrics: true, showTimeWindowDropdown: false }, }); Vue.nextTick(() => { @@ -78,7 +79,12 @@ describe('Dashboard', () => { it('hides the legend when showLegend is false', done => { const component = new DashboardComponent({ el: document.querySelector('.prometheus-graphs'), - propsData: { ...propsData, hasMetrics: true, showLegend: false }, + propsData: { + ...propsData, + hasMetrics: true, + showLegend: false, + showTimeWindowDropdown: false, + }, }); setTimeout(() => { @@ -92,7 +98,12 @@ describe('Dashboard', () => { it('hides the group panels when showPanels is false', done => { const component = new DashboardComponent({ el: document.querySelector('.prometheus-graphs'), - propsData: { ...propsData, hasMetrics: true, showPanels: false }, + propsData: { + ...propsData, + hasMetrics: true, + showPanels: false, + showTimeWindowDropdown: false, + }, }); setTimeout(() => { @@ -106,7 +117,12 @@ describe('Dashboard', () => { it('renders the environments dropdown with a number of environments', done => { const component = new DashboardComponent({ el: document.querySelector('.prometheus-graphs'), - propsData: { ...propsData, hasMetrics: true, showPanels: false }, + propsData: { + ...propsData, + hasMetrics: true, + showPanels: false, + showTimeWindowDropdown: false, + }, }); component.store.storeEnvironmentsData(environmentData); @@ -124,7 +140,12 @@ describe('Dashboard', () => { it('hides the environments dropdown list when there is no environments', done => { const component = new DashboardComponent({ el: document.querySelector('.prometheus-graphs'), - propsData: { ...propsData, hasMetrics: true, showPanels: false }, + propsData: { + ...propsData, + hasMetrics: true, + showPanels: false, + showTimeWindowDropdown: false, + }, }); component.store.storeEnvironmentsData([]); @@ -142,7 +163,12 @@ describe('Dashboard', () => { it('renders the environments dropdown with a single is-active element', done => { const component = new DashboardComponent({ el: document.querySelector('.prometheus-graphs'), - propsData: { ...propsData, hasMetrics: true, showPanels: false }, + propsData: { + ...propsData, + hasMetrics: true, + showPanels: false, + showTimeWindowDropdown: false, + }, }); component.store.storeEnvironmentsData(environmentData); @@ -166,6 +192,7 @@ describe('Dashboard', () => { hasMetrics: true, showPanels: false, environmentsEndpoint: '', + showTimeWindowDropdown: false, }, }); @@ -176,6 +203,51 @@ describe('Dashboard', () => { done(); }); }); + + it('does not show the time window dropdown when the feature flag is not set', done => { + const component = new DashboardComponent({ + el: document.querySelector('.prometheus-graphs'), + propsData: { + ...propsData, + hasMetrics: true, + showPanels: false, + showTimeWindowDropdown: false, + }, + }); + + setTimeout(() => { + const timeWindowDropdown = component.$el.querySelector('.js-time-window-dropdown'); + + expect(timeWindowDropdown).toBeNull(); + + done(); + }); + }); + + it('renders the time window dropdown with a set of options', done => { + const component = new DashboardComponent({ + el: document.querySelector('.prometheus-graphs'), + propsData: { + ...propsData, + hasMetrics: true, + showPanels: false, + showTimeWindowDropdown: true, + }, + }); + const numberOfTimeWindows = Object.keys(timeWindows).length; + + setTimeout(() => { + const timeWindowDropdown = component.$el.querySelector('.js-time-window-dropdown'); + const timeWindowDropdownEls = component.$el.querySelectorAll( + '.js-time-window-dropdown .dropdown-item', + ); + + expect(timeWindowDropdown).not.toBeNull(); + expect(timeWindowDropdownEls.length).toEqual(numberOfTimeWindows); + + done(); + }); + }); }); describe('when the window resizes', () => { @@ -191,7 +263,12 @@ describe('Dashboard', () => { it('sets elWidth to page width when the sidebar is resized', done => { const component = new DashboardComponent({ el: document.querySelector('.prometheus-graphs'), - propsData: { ...propsData, hasMetrics: true, showPanels: false }, + propsData: { + ...propsData, + hasMetrics: true, + showPanels: false, + showTimeWindowDropdown: false, + }, }); expect(component.elWidth).toEqual(0); diff --git a/spec/javascripts/related_merge_requests/components/related_merge_requests_spec.js b/spec/javascripts/related_merge_requests/components/related_merge_requests_spec.js new file mode 100644 index 00000000000..29760f79c3c --- /dev/null +++ b/spec/javascripts/related_merge_requests/components/related_merge_requests_spec.js @@ -0,0 +1,89 @@ +import { mount, createLocalVue } from '@vue/test-utils'; +import MockAdapter from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; +import RelatedIssuableItem from '~/vue_shared/components/issue/related_issuable_item.vue'; +import RelatedMergeRequests from '~/related_merge_requests/components/related_merge_requests.vue'; +import createStore from '~/related_merge_requests/store/index'; + +const FIXTURE_PATH = 'issues/related_merge_requests.json'; +const API_ENDPOINT = '/api/v4/projects/2/issues/33/related_merge_requests'; +const localVue = createLocalVue(); + +describe('RelatedMergeRequests', () => { + let wrapper; + let mock; + let mockData; + + beforeEach(done => { + loadFixtures(FIXTURE_PATH); + mockData = getJSONFixture(FIXTURE_PATH); + mock = new MockAdapter(axios); + mock.onGet(`${API_ENDPOINT}?per_page=100`).reply(200, mockData, { 'x-total': 2 }); + + wrapper = mount(RelatedMergeRequests, { + localVue, + sync: false, + store: createStore(), + propsData: { + endpoint: API_ENDPOINT, + projectNamespace: 'gitlab-org', + projectPath: 'gitlab-ce', + }, + }); + + setTimeout(done); + }); + + afterEach(() => { + wrapper.destroy(); + mock.restore(); + }); + + describe('methods', () => { + describe('getAssignees', () => { + const assignees = [{ name: 'foo' }, { name: 'bar' }]; + + describe('when there is assignees array', () => { + it('should return assignees array', () => { + const mr = { assignees }; + + expect(wrapper.vm.getAssignees(mr)).toEqual(assignees); + }); + }); + + it('should return an array with single assingee', () => { + const mr = { assignee: assignees[0] }; + + expect(wrapper.vm.getAssignees(mr)).toEqual([assignees[0]]); + }); + + it('should return empty array when assignee is not set', () => { + expect(wrapper.vm.getAssignees({})).toEqual([]); + expect(wrapper.vm.getAssignees({ assignee: null })).toEqual([]); + }); + }); + }); + + describe('template', () => { + it('should render related merge request items', () => { + expect(wrapper.find('.js-items-count').text()).toEqual('2'); + expect(wrapper.findAll(RelatedIssuableItem).length).toEqual(2); + + const props = wrapper + .findAll(RelatedIssuableItem) + .at(1) + .props(); + const data = mockData[1]; + + expect(props.idKey).toEqual(data.id); + expect(props.pathIdSeparator).toEqual('!'); + expect(props.pipelineStatus).toBe(data.head_pipeline.detailed_status); + expect(props.assignees).toEqual([data.assignee]); + expect(props.isMergeRequest).toBe(true); + expect(props.confidential).toEqual(false); + expect(props.title).toEqual(data.title); + expect(props.state).toEqual(data.state); + expect(props.createdAt).toEqual(data.created_at); + }); + }); +}); diff --git a/spec/javascripts/related_merge_requests/store/actions_spec.js b/spec/javascripts/related_merge_requests/store/actions_spec.js new file mode 100644 index 00000000000..65e436fbb17 --- /dev/null +++ b/spec/javascripts/related_merge_requests/store/actions_spec.js @@ -0,0 +1,110 @@ +import MockAdapter from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; +import * as types from '~/related_merge_requests/store/mutation_types'; +import actionsModule, * as actions from '~/related_merge_requests/store/actions'; +import testAction from 'spec/helpers/vuex_action_helper'; + +describe('RelatedMergeRequest store actions', () => { + let state; + let flashSpy; + let mock; + + beforeEach(() => { + state = { + apiEndpoint: '/api/related_merge_requests', + }; + flashSpy = spyOnDependency(actionsModule, 'createFlash'); + mock = new MockAdapter(axios); + }); + + afterEach(() => { + mock.restore(); + }); + + describe('setInitialState', () => { + it('commits types.SET_INITIAL_STATE with given props', done => { + const props = { a: 1, b: 2 }; + + testAction( + actions.setInitialState, + props, + {}, + [{ type: types.SET_INITIAL_STATE, payload: props }], + [], + done, + ); + }); + }); + + describe('requestData', () => { + it('commits types.REQUEST_DATA', done => { + testAction(actions.requestData, null, {}, [{ type: types.REQUEST_DATA }], [], done); + }); + }); + + describe('receiveDataSuccess', () => { + it('commits types.RECEIVE_DATA_SUCCESS with data', done => { + const data = { a: 1, b: 2 }; + + testAction( + actions.receiveDataSuccess, + data, + {}, + [{ type: types.RECEIVE_DATA_SUCCESS, payload: data }], + [], + done, + ); + }); + }); + + describe('receiveDataError', () => { + it('commits types.RECEIVE_DATA_ERROR', done => { + testAction( + actions.receiveDataError, + null, + {}, + [{ type: types.RECEIVE_DATA_ERROR }], + [], + done, + ); + }); + }); + + describe('fetchMergeRequests', () => { + describe('for a successful request', () => { + it('should dispatch success action', done => { + const data = { a: 1 }; + mock.onGet(`${state.apiEndpoint}?per_page=100`).replyOnce(200, data, { 'x-total': 2 }); + + testAction( + actions.fetchMergeRequests, + null, + state, + [], + [{ type: 'requestData' }, { type: 'receiveDataSuccess', payload: { data, total: 2 } }], + done, + ); + }); + }); + + describe('for a failing request', () => { + it('should dispatch error action', done => { + mock.onGet(`${state.apiEndpoint}?per_page=100`).replyOnce(400); + + testAction( + actions.fetchMergeRequests, + null, + state, + [], + [{ type: 'requestData' }, { type: 'receiveDataError' }], + () => { + expect(flashSpy).toHaveBeenCalledTimes(1); + expect(flashSpy).toHaveBeenCalledWith(jasmine.stringMatching('Something went wrong')); + + done(); + }, + ); + }); + }); + }); +}); diff --git a/spec/javascripts/related_merge_requests/store/mutations_spec.js b/spec/javascripts/related_merge_requests/store/mutations_spec.js new file mode 100644 index 00000000000..21b6e26376b --- /dev/null +++ b/spec/javascripts/related_merge_requests/store/mutations_spec.js @@ -0,0 +1,49 @@ +import mutations from '~/related_merge_requests/store/mutations'; +import * as types from '~/related_merge_requests/store/mutation_types'; + +describe('RelatedMergeRequests Store Mutations', () => { + describe('SET_INITIAL_STATE', () => { + it('should set initial state according to given data', () => { + const apiEndpoint = '/api'; + const state = {}; + + mutations[types.SET_INITIAL_STATE](state, { apiEndpoint }); + + expect(state.apiEndpoint).toEqual(apiEndpoint); + }); + }); + + describe('REQUEST_DATA', () => { + it('should set loading flag', () => { + const state = {}; + + mutations[types.REQUEST_DATA](state); + + expect(state.isFetchingMergeRequests).toEqual(true); + }); + }); + + describe('RECEIVE_DATA_SUCCESS', () => { + it('should set loading flag and data', () => { + const state = {}; + const mrs = [1, 2, 3]; + + mutations[types.RECEIVE_DATA_SUCCESS](state, { data: mrs, total: mrs.length }); + + expect(state.isFetchingMergeRequests).toEqual(false); + expect(state.mergeRequests).toEqual(mrs); + expect(state.totalCount).toEqual(mrs.length); + }); + }); + + describe('RECEIVE_DATA_ERROR', () => { + it('should set loading and error flags', () => { + const state = {}; + + mutations[types.RECEIVE_DATA_ERROR](state); + + expect(state.isFetchingMergeRequests).toEqual(false); + expect(state.hasErrorFetchingMergeRequests).toEqual(true); + }); + }); +}); diff --git a/spec/javascripts/serverless/components/environment_row_spec.js b/spec/javascripts/serverless/components/environment_row_spec.js deleted file mode 100644 index bdf7a714910..00000000000 --- a/spec/javascripts/serverless/components/environment_row_spec.js +++ /dev/null @@ -1,81 +0,0 @@ -import Vue from 'vue'; - -import environmentRowComponent from '~/serverless/components/environment_row.vue'; -import mountComponent from 'spec/helpers/vue_mount_component_helper'; -import ServerlessStore from '~/serverless/stores/serverless_store'; - -import { mockServerlessFunctions, mockServerlessFunctionsDiffEnv } from '../mock_data'; - -const createComponent = (env, envName) => - mountComponent(Vue.extend(environmentRowComponent), { env, envName }); - -describe('environment row component', () => { - describe('default global cluster case', () => { - let vm; - - beforeEach(() => { - const store = new ServerlessStore(false, '/cluster_path', 'help_path'); - store.updateFunctionsFromServer(mockServerlessFunctions); - vm = createComponent(store.state.functions['*'], '*'); - }); - - it('has the correct envId', () => { - expect(vm.envId).toEqual('env-global'); - vm.$destroy(); - }); - - it('is open by default', () => { - expect(vm.isOpenClass).toEqual({ 'is-open': true }); - vm.$destroy(); - }); - - it('generates correct output', () => { - expect(vm.$el.querySelectorAll('li').length).toEqual(2); - expect(vm.$el.id).toEqual('env-global'); - expect(vm.$el.classList.contains('is-open')).toBe(true); - expect(vm.$el.querySelector('div.title').innerHTML.trim()).toEqual('*'); - - vm.$destroy(); - }); - - it('opens and closes correctly', () => { - expect(vm.isOpen).toBe(true); - - vm.toggleOpen(); - Vue.nextTick(() => { - expect(vm.isOpen).toBe(false); - }); - - vm.$destroy(); - }); - }); - - describe('default named cluster case', () => { - let vm; - - beforeEach(() => { - const store = new ServerlessStore(false, '/cluster_path', 'help_path'); - store.updateFunctionsFromServer(mockServerlessFunctionsDiffEnv); - vm = createComponent(store.state.functions.test, 'test'); - }); - - it('has the correct envId', () => { - expect(vm.envId).toEqual('env-test'); - vm.$destroy(); - }); - - it('is open by default', () => { - expect(vm.isOpenClass).toEqual({ 'is-open': true }); - vm.$destroy(); - }); - - it('generates correct output', () => { - expect(vm.$el.querySelectorAll('li').length).toEqual(1); - expect(vm.$el.id).toEqual('env-test'); - expect(vm.$el.classList.contains('is-open')).toBe(true); - expect(vm.$el.querySelector('div.title').innerHTML.trim()).toEqual('test'); - - vm.$destroy(); - }); - }); -}); diff --git a/spec/javascripts/serverless/components/function_row_spec.js b/spec/javascripts/serverless/components/function_row_spec.js deleted file mode 100644 index 6933a8f6c87..00000000000 --- a/spec/javascripts/serverless/components/function_row_spec.js +++ /dev/null @@ -1,33 +0,0 @@ -import Vue from 'vue'; - -import functionRowComponent from '~/serverless/components/function_row.vue'; -import mountComponent from 'spec/helpers/vue_mount_component_helper'; - -import { mockServerlessFunction } from '../mock_data'; - -const createComponent = func => mountComponent(Vue.extend(functionRowComponent), { func }); - -describe('functionRowComponent', () => { - it('Parses the function details correctly', () => { - const vm = createComponent(mockServerlessFunction); - - expect(vm.$el.querySelector('b').innerHTML).toEqual(mockServerlessFunction.name); - expect(vm.$el.querySelector('span').innerHTML).toEqual(mockServerlessFunction.image); - expect(vm.$el.querySelector('time').getAttribute('data-original-title')).not.toBe(null); - expect(vm.$el.querySelector('div.url-text-field').innerHTML).toEqual( - mockServerlessFunction.url, - ); - - vm.$destroy(); - }); - - it('handles clicks correctly', () => { - const vm = createComponent(mockServerlessFunction); - - expect(vm.checkClass(vm.$el.querySelector('p'))).toBe(true); // check somewhere inside the row - expect(vm.checkClass(vm.$el.querySelector('svg'))).toBe(false); // check a button image - expect(vm.checkClass(vm.$el.querySelector('div.url-text-field'))).toBe(false); // check the url bar - - vm.$destroy(); - }); -}); diff --git a/spec/javascripts/serverless/components/functions_spec.js b/spec/javascripts/serverless/components/functions_spec.js deleted file mode 100644 index 85cfe71281f..00000000000 --- a/spec/javascripts/serverless/components/functions_spec.js +++ /dev/null @@ -1,68 +0,0 @@ -import Vue from 'vue'; - -import functionsComponent from '~/serverless/components/functions.vue'; -import mountComponent from 'spec/helpers/vue_mount_component_helper'; -import ServerlessStore from '~/serverless/stores/serverless_store'; - -import { mockServerlessFunctions } from '../mock_data'; - -const createComponent = ( - functions, - installed = true, - loadingData = true, - hasFunctionData = true, -) => { - const component = Vue.extend(functionsComponent); - - return mountComponent(component, { - functions, - installed, - clustersPath: '/testClusterPath', - helpPath: '/helpPath', - loadingData, - hasFunctionData, - }); -}; - -describe('functionsComponent', () => { - it('should render empty state when Knative is not installed', () => { - const vm = createComponent({}, false); - - expect(vm.$el.querySelector('div.row').classList.contains('js-empty-state')).toBe(true); - expect(vm.$el.querySelector('h4.state-title').innerHTML.trim()).toEqual( - 'Getting started with serverless', - ); - - vm.$destroy(); - }); - - it('should render a loading component', () => { - const vm = createComponent({}); - - expect(vm.$el.querySelector('.gl-responsive-table-row')).not.toBe(null); - expect(vm.$el.querySelector('div.animation-container')).not.toBe(null); - }); - - it('should render empty state when there is no function data', () => { - const vm = createComponent({}, true, false, false); - - expect( - vm.$el.querySelector('.empty-state, .js-empty-state').classList.contains('js-empty-state'), - ).toBe(true); - - expect(vm.$el.querySelector('h4.state-title').innerHTML.trim()).toEqual( - 'No functions available', - ); - - vm.$destroy(); - }); - - it('should render the functions list', () => { - const store = new ServerlessStore(false, '/cluster_path', 'help_path'); - store.updateFunctionsFromServer(mockServerlessFunctions); - const vm = createComponent(store.state.functions, true, false); - - expect(vm.$el.querySelector('div.groups-list-tree-container')).not.toBe(null); - expect(vm.$el.querySelector('#env-global').classList.contains('has-children')).toBe(true); - }); -}); diff --git a/spec/javascripts/serverless/components/url_spec.js b/spec/javascripts/serverless/components/url_spec.js deleted file mode 100644 index 21a879a49bb..00000000000 --- a/spec/javascripts/serverless/components/url_spec.js +++ /dev/null @@ -1,28 +0,0 @@ -import Vue from 'vue'; - -import urlComponent from '~/serverless/components/url.vue'; -import mountComponent from 'spec/helpers/vue_mount_component_helper'; - -const createComponent = uri => { - const component = Vue.extend(urlComponent); - - return mountComponent(component, { - uri, - }); -}; - -describe('urlComponent', () => { - it('should render correctly', () => { - const uri = 'http://testfunc.apps.example.com'; - const vm = createComponent(uri); - - expect(vm.$el.classList.contains('clipboard-group')).toBe(true); - expect(vm.$el.querySelector('.js-clipboard-btn').getAttribute('data-clipboard-text')).toEqual( - uri, - ); - - expect(vm.$el.querySelector('.url-text-field').innerHTML).toEqual(uri); - - vm.$destroy(); - }); -}); diff --git a/spec/javascripts/serverless/mock_data.js b/spec/javascripts/serverless/mock_data.js deleted file mode 100644 index ecd393b174c..00000000000 --- a/spec/javascripts/serverless/mock_data.js +++ /dev/null @@ -1,79 +0,0 @@ -export const mockServerlessFunctions = [ - { - name: 'testfunc1', - namespace: 'tm-example', - environment_scope: '*', - cluster_id: 46, - detail_url: '/testuser/testproj/serverless/functions/*/testfunc1', - podcount: null, - created_at: '2019-02-05T01:01:23Z', - url: 'http://testfunc1.tm-example.apps.example.com', - description: 'A test service', - image: 'knative-test-container-buildtemplate', - }, - { - name: 'testfunc2', - namespace: 'tm-example', - environment_scope: '*', - cluster_id: 46, - detail_url: '/testuser/testproj/serverless/functions/*/testfunc2', - podcount: null, - created_at: '2019-02-05T01:01:23Z', - url: 'http://testfunc2.tm-example.apps.example.com', - description: 'A second test service\nThis one with additional descriptions', - image: 'knative-test-echo-buildtemplate', - }, -]; - -export const mockServerlessFunctionsDiffEnv = [ - { - name: 'testfunc1', - namespace: 'tm-example', - environment_scope: '*', - cluster_id: 46, - detail_url: '/testuser/testproj/serverless/functions/*/testfunc1', - podcount: null, - created_at: '2019-02-05T01:01:23Z', - url: 'http://testfunc1.tm-example.apps.example.com', - description: 'A test service', - image: 'knative-test-container-buildtemplate', - }, - { - name: 'testfunc2', - namespace: 'tm-example', - environment_scope: 'test', - cluster_id: 46, - detail_url: '/testuser/testproj/serverless/functions/*/testfunc2', - podcount: null, - created_at: '2019-02-05T01:01:23Z', - url: 'http://testfunc2.tm-example.apps.example.com', - description: 'A second test service\nThis one with additional descriptions', - image: 'knative-test-echo-buildtemplate', - }, -]; - -export const mockServerlessFunction = { - name: 'testfunc1', - namespace: 'tm-example', - environment_scope: '*', - cluster_id: 46, - detail_url: '/testuser/testproj/serverless/functions/*/testfunc1', - podcount: '3', - created_at: '2019-02-05T01:01:23Z', - url: 'http://testfunc1.tm-example.apps.example.com', - description: 'A test service', - image: 'knative-test-container-buildtemplate', -}; - -export const mockMultilineServerlessFunction = { - name: 'testfunc1', - namespace: 'tm-example', - environment_scope: '*', - cluster_id: 46, - detail_url: '/testuser/testproj/serverless/functions/*/testfunc1', - podcount: '3', - created_at: '2019-02-05T01:01:23Z', - url: 'http://testfunc1.tm-example.apps.example.com', - description: 'testfunc1\nA test service line\\nWith additional services', - image: 'knative-test-container-buildtemplate', -}; diff --git a/spec/javascripts/serverless/stores/serverless_store_spec.js b/spec/javascripts/serverless/stores/serverless_store_spec.js deleted file mode 100644 index 72fd903d7d1..00000000000 --- a/spec/javascripts/serverless/stores/serverless_store_spec.js +++ /dev/null @@ -1,36 +0,0 @@ -import ServerlessStore from '~/serverless/stores/serverless_store'; -import { mockServerlessFunctions, mockServerlessFunctionsDiffEnv } from '../mock_data'; - -describe('Serverless Functions Store', () => { - let store; - - beforeEach(() => { - store = new ServerlessStore(false, '/cluster_path', 'help_path'); - }); - - describe('#updateFunctionsFromServer', () => { - it('should pass an empty hash object', () => { - store.updateFunctionsFromServer(); - - expect(store.state.functions).toEqual({}); - }); - - it('should group functions to one global environment', () => { - const mockServerlessData = mockServerlessFunctions; - store.updateFunctionsFromServer(mockServerlessData); - - expect(Object.keys(store.state.functions)).toEqual(jasmine.objectContaining(['*'])); - expect(store.state.functions['*'].length).toEqual(2); - }); - - it('should group functions to multiple environments', () => { - const mockServerlessData = mockServerlessFunctionsDiffEnv; - store.updateFunctionsFromServer(mockServerlessData); - - expect(Object.keys(store.state.functions)).toEqual(jasmine.objectContaining(['*'])); - expect(store.state.functions['*'].length).toEqual(1); - expect(store.state.functions.test.length).toEqual(1); - expect(store.state.functions.test[0].name).toEqual('testfunc2'); - }); - }); -}); diff --git a/spec/javascripts/test_bundle.js b/spec/javascripts/test_bundle.js index 235a17d13b0..87ef0885d8c 100644 --- a/spec/javascripts/test_bundle.js +++ b/spec/javascripts/test_bundle.js @@ -69,7 +69,7 @@ window.gl = window.gl || {}; window.gl.TEST_HOST = TEST_HOST; window.gon = window.gon || {}; window.gon.test_env = true; -window.gon.ee = process.env.EE; +window.gon.ee = process.env.IS_GITLAB_EE; gon.relative_url_root = ''; let hasUnhandledPromiseRejections = false; @@ -124,7 +124,7 @@ const axiosDefaultAdapter = getDefaultAdapter(); // render all of our tests const testContexts = [require.context('spec', true, /_spec$/)]; -if (process.env.EE) { +if (process.env.IS_GITLAB_EE) { testContexts.push(require.context('ee_spec', true, /_spec$/)); } @@ -213,7 +213,7 @@ if (process.env.BABEL_ENV === 'coverage') { describe('Uncovered files', function() { const sourceFilesContexts = [require.context('~', true, /\.(js|vue)$/)]; - if (process.env.EE) { + if (process.env.IS_GITLAB_EE) { sourceFilesContexts.push(require.context('ee', true, /\.(js|vue)$/)); } diff --git a/spec/javascripts/vue_shared/components/file_row_spec.js b/spec/javascripts/vue_shared/components/file_row_spec.js index d1fd899c1a8..7da69e3fa84 100644 --- a/spec/javascripts/vue_shared/components/file_row_spec.js +++ b/spec/javascripts/vue_shared/components/file_row_spec.js @@ -1,5 +1,6 @@ import Vue from 'vue'; import FileRow from '~/vue_shared/components/file_row.vue'; +import FileRowExtra from '~/ide/components/file_row_extra.vue'; import { file } from 'spec/ide/helpers'; import mountComponent from '../../helpers/vue_mount_component_helper'; @@ -16,6 +17,10 @@ describe('File row component', () => { vm.$destroy(); }); + const findNewDropdown = () => vm.$el.querySelector('.ide-new-btn .dropdown'); + const findNewDropdownButton = () => vm.$el.querySelector('.ide-new-btn .dropdown button'); + const findFileRow = () => vm.$el.querySelector('.file-row'); + it('renders name', () => { createComponent({ file: file('t4'), @@ -84,4 +89,59 @@ describe('File row component', () => { expect(vm.$el.querySelector('.js-file-row-header')).not.toBe(null); }); + + describe('new dropdown', () => { + beforeEach(() => { + createComponent({ + file: file('t5'), + level: 1, + extraComponent: FileRowExtra, + }); + }); + + it('renders in extra component', () => { + expect(findNewDropdown()).not.toBe(null); + }); + + it('is hidden at start', () => { + expect(findNewDropdown()).not.toHaveClass('show'); + }); + + it('is opened when button is clicked', done => { + expect(vm.dropdownOpen).toBe(false); + findNewDropdownButton().dispatchEvent(new Event('click')); + + vm.$nextTick() + .then(() => { + expect(vm.dropdownOpen).toBe(true); + expect(findNewDropdown()).toHaveClass('show'); + }) + .then(done) + .catch(done.fail); + }); + + describe('when opened', () => { + beforeEach(() => { + vm.dropdownOpen = true; + }); + + it('stays open when button triggers mouseout', () => { + findNewDropdownButton().dispatchEvent(new Event('mouseout')); + + expect(vm.dropdownOpen).toBe(true); + }); + + it('stays open when button triggers mouseleave', () => { + findNewDropdownButton().dispatchEvent(new Event('mouseleave')); + + expect(vm.dropdownOpen).toBe(true); + }); + + it('closes when row triggers mouseleave', () => { + findFileRow().dispatchEvent(new Event('mouseleave')); + + expect(vm.dropdownOpen).toBe(false); + }); + }); + }); }); diff --git a/spec/javascripts/vue_shared/components/project_selector/project_list_item_spec.js b/spec/javascripts/vue_shared/components/project_selector/project_list_item_spec.js new file mode 100644 index 00000000000..b95183747bb --- /dev/null +++ b/spec/javascripts/vue_shared/components/project_selector/project_list_item_spec.js @@ -0,0 +1,110 @@ +import ProjectListItem from '~/vue_shared/components/project_selector/project_list_item.vue'; +import { shallowMount, createLocalVue } from '@vue/test-utils'; +import { trimText } from 'spec/helpers/vue_component_helper'; + +const localVue = createLocalVue(); + +describe('ProjectListItem component', () => { + const Component = localVue.extend(ProjectListItem); + let wrapper; + let vm; + let options; + loadJSONFixtures('projects.json'); + const project = getJSONFixture('projects.json')[0]; + + beforeEach(() => { + options = { + propsData: { + project, + selected: false, + }, + sync: false, + localVue, + }; + }); + + afterEach(() => { + wrapper.vm.$destroy(); + }); + + it('does not render a check mark icon if selected === false', () => { + wrapper = shallowMount(Component, options); + + expect(wrapper.contains('.js-selected-icon.js-unselected')).toBe(true); + }); + + it('renders a check mark icon if selected === true', () => { + options.propsData.selected = true; + + wrapper = shallowMount(Component, options); + + expect(wrapper.contains('.js-selected-icon.js-selected')).toBe(true); + }); + + it(`emits a "clicked" event when clicked`, () => { + wrapper = shallowMount(Component, options); + ({ vm } = wrapper); + + spyOn(vm, '$emit'); + wrapper.vm.onClick(); + + expect(wrapper.vm.$emit).toHaveBeenCalledWith('click'); + }); + + it(`renders the project avatar`, () => { + wrapper = shallowMount(Component, options); + + expect(wrapper.contains('.js-project-avatar')).toBe(true); + }); + + it(`renders a simple namespace name with a trailing slash`, () => { + options.propsData.project.name_with_namespace = 'a / b'; + + wrapper = shallowMount(Component, options); + const renderedNamespace = trimText(wrapper.find('.js-project-namespace').text()); + + expect(renderedNamespace).toBe('a /'); + }); + + it(`renders a properly truncated namespace with a trailing slash`, () => { + options.propsData.project.name_with_namespace = 'a / b / c / d / e / f'; + + wrapper = shallowMount(Component, options); + const renderedNamespace = trimText(wrapper.find('.js-project-namespace').text()); + + expect(renderedNamespace).toBe('a / ... / e /'); + }); + + it(`renders the project name`, () => { + options.propsData.project.name = 'my-test-project'; + + wrapper = shallowMount(Component, options); + const renderedName = trimText(wrapper.find('.js-project-name').text()); + + expect(renderedName).toBe('my-test-project'); + }); + + it(`renders the project name with highlighting in the case of a search query match`, () => { + options.propsData.project.name = 'my-test-project'; + options.propsData.matcher = 'pro'; + + wrapper = shallowMount(Component, options); + const renderedName = trimText(wrapper.find('.js-project-name').html()); + const expected = 'my-test-<b>p</b><b>r</b><b>o</b>ject'; + + expect(renderedName).toContain(expected); + }); + + it('prevents search query and project name XSS', () => { + const alertSpy = spyOn(window, 'alert'); + options.propsData.project.name = "my-xss-pro<script>alert('XSS');</script>ject"; + options.propsData.matcher = "pro<script>alert('XSS');</script>"; + + wrapper = shallowMount(Component, options); + const renderedName = trimText(wrapper.find('.js-project-name').html()); + const expected = 'my-xss-project'; + + expect(renderedName).toContain(expected); + expect(alertSpy).not.toHaveBeenCalled(); + }); +}); diff --git a/spec/javascripts/vue_shared/components/project_selector/project_selector_spec.js b/spec/javascripts/vue_shared/components/project_selector/project_selector_spec.js new file mode 100644 index 00000000000..ba9ec8f2f19 --- /dev/null +++ b/spec/javascripts/vue_shared/components/project_selector/project_selector_spec.js @@ -0,0 +1,132 @@ +import Vue from 'vue'; +import _ from 'underscore'; +import ProjectSelector from '~/vue_shared/components/project_selector/project_selector.vue'; +import ProjectListItem from '~/vue_shared/components/project_selector/project_list_item.vue'; +import { shallowMount } from '@vue/test-utils'; +import { trimText } from 'spec/helpers/vue_component_helper'; + +describe('ProjectSelector component', () => { + let wrapper; + let vm; + loadJSONFixtures('projects.json'); + const allProjects = getJSONFixture('projects.json'); + const searchResults = allProjects.slice(0, 5); + let selected = []; + selected = selected.concat(allProjects.slice(0, 3)).concat(allProjects.slice(5, 8)); + + beforeEach(() => { + jasmine.clock().install(); + + wrapper = shallowMount(Vue.extend(ProjectSelector), { + propsData: { + projectSearchResults: searchResults, + selectedProjects: selected, + showNoResultsMessage: false, + showMinimumSearchQueryMessage: false, + showLoadingIndicator: false, + showSearchErrorMessage: false, + }, + attachToDocument: true, + }); + + ({ vm } = wrapper); + }); + + afterEach(() => { + jasmine.clock().uninstall(); + vm.$destroy(); + }); + + it('renders the search results', () => { + expect(wrapper.findAll('.js-project-list-item').length).toBe(5); + }); + + it(`triggers a (debounced) search when the search input value changes`, () => { + spyOn(vm, '$emit'); + const query = 'my test query!'; + const searchInput = wrapper.find('.js-project-selector-input'); + searchInput.setValue(query); + searchInput.trigger('input'); + + expect(vm.$emit).not.toHaveBeenCalledWith(); + jasmine.clock().tick(501); + + expect(vm.$emit).toHaveBeenCalledWith('searched', query); + }); + + it(`debounces the search input`, () => { + spyOn(vm, '$emit'); + const searchInput = wrapper.find('.js-project-selector-input'); + + const updateSearchQuery = (count = 0) => { + if (count === 10) { + jasmine.clock().tick(101); + + expect(vm.$emit).toHaveBeenCalledTimes(1); + expect(vm.$emit).toHaveBeenCalledWith('searched', `search query #9`); + } else { + searchInput.setValue(`search query #${count}`); + searchInput.trigger('input'); + + jasmine.clock().tick(400); + updateSearchQuery(count + 1); + } + }; + + updateSearchQuery(); + }); + + it(`includes a placeholder in the search box`, () => { + expect(wrapper.find('.js-project-selector-input').attributes('placeholder')).toBe( + 'Search your projects', + ); + }); + + it(`triggers a "projectClicked" event when a project is clicked`, () => { + spyOn(vm, '$emit'); + wrapper.find(ProjectListItem).vm.$emit('click', _.first(searchResults)); + + expect(vm.$emit).toHaveBeenCalledWith('projectClicked', _.first(searchResults)); + }); + + it(`shows a "no results" message if showNoResultsMessage === true`, () => { + wrapper.setProps({ showNoResultsMessage: true }); + + expect(wrapper.contains('.js-no-results-message')).toBe(true); + + const noResultsEl = wrapper.find('.js-no-results-message'); + + expect(trimText(noResultsEl.text())).toEqual('Sorry, no projects matched your search'); + }); + + it(`shows a "minimum seach query" message if showMinimumSearchQueryMessage === true`, () => { + wrapper.setProps({ showMinimumSearchQueryMessage: true }); + + expect(wrapper.contains('.js-minimum-search-query-message')).toBe(true); + + const minimumSearchEl = wrapper.find('.js-minimum-search-query-message'); + + expect(trimText(minimumSearchEl.text())).toEqual('Enter at least three characters to search'); + }); + + it(`shows a error message if showSearchErrorMessage === true`, () => { + wrapper.setProps({ showSearchErrorMessage: true }); + + expect(wrapper.contains('.js-search-error-message')).toBe(true); + + const errorMessageEl = wrapper.find('.js-search-error-message'); + + expect(trimText(errorMessageEl.text())).toEqual( + 'Something went wrong, unable to search projects', + ); + }); + + it(`focuses the input element when the focusSearchInput() method is called`, () => { + const input = wrapper.find('.js-project-selector-input'); + + expect(document.activeElement).not.toBe(input.element); + vm.focusSearchInput(); + + expect(document.activeElement).toBe(input.element); + }); +}); diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button_spec.js b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button_spec.js index 5cf6afebd7e..0689fc1cf1f 100644 --- a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button_spec.js +++ b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button_spec.js @@ -45,12 +45,21 @@ describe('DropdownButtonComponent', () => { }); const vmMoreLabels = createComponent(mockMoreLabels); - expect(vmMoreLabels.dropdownToggleText).toBe('Foo Label +1 more'); + expect(vmMoreLabels.dropdownToggleText).toBe( + `Foo Label +${mockMoreLabels.labels.length - 1} more`, + ); vmMoreLabels.$destroy(); }); it('returns first label name when `labels` prop has only one item present', () => { - expect(vm.dropdownToggleText).toBe('Foo Label'); + const singleLabel = Object.assign({}, componentConfig, { + labels: [mockLabels[0]], + }); + const vmSingleLabel = createComponent(singleLabel); + + expect(vmSingleLabel.dropdownToggleText).toBe(mockLabels[0].title); + + vmSingleLabel.$destroy(); }); }); }); @@ -73,7 +82,7 @@ describe('DropdownButtonComponent', () => { const dropdownToggleTextEl = vm.$el.querySelector('.dropdown-toggle-text'); expect(dropdownToggleTextEl).not.toBeNull(); - expect(dropdownToggleTextEl.innerText.trim()).toBe('Foo Label'); + expect(dropdownToggleTextEl.innerText.trim()).toBe('Foo Label +1 more'); }); it('renders dropdown button icon', () => { diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js index 804b33422bd..cb49fa31d20 100644 --- a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js +++ b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js @@ -35,9 +35,12 @@ describe('DropdownValueCollapsedComponent', () => { }); it('returns labels names separated by coma when `labels` prop has more than one item', () => { - const vmMoreLabels = createComponent(mockLabels.concat(mockLabels)); + const labels = mockLabels.concat(mockLabels); + const vmMoreLabels = createComponent(labels); - expect(vmMoreLabels.labelsList).toBe('Foo Label, Foo Label'); + const expectedText = labels.map(label => label.title).join(', '); + + expect(vmMoreLabels.labelsList).toBe(expectedText); vmMoreLabels.$destroy(); }); @@ -49,14 +52,19 @@ describe('DropdownValueCollapsedComponent', () => { const vmMoreLabels = createComponent(mockMoreLabels); - expect(vmMoreLabels.labelsList).toBe( - 'Foo Label, Foo Label, Foo Label, Foo Label, Foo Label, and 2 more', - ); + const expectedText = `${mockMoreLabels + .slice(0, 5) + .map(label => label.title) + .join(', ')}, and ${mockMoreLabels.length - 5} more`; + + expect(vmMoreLabels.labelsList).toBe(expectedText); vmMoreLabels.$destroy(); }); it('returns first label name when `labels` prop has only one item present', () => { - expect(vm.labelsList).toBe('Foo Label'); + const text = mockLabels.map(label => label.title).join(', '); + + expect(vm.labelsList).toBe(text); }); }); }); 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 3fff781594f..35a9c300953 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 @@ -1,4 +1,5 @@ import Vue from 'vue'; +import $ from 'jquery'; import dropdownValueComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_value.vue'; @@ -15,6 +16,7 @@ const createComponent = ( return mountComponent(Component, { labels, labelFilterBasePath, + enableScopedLabels: true, }); }; @@ -67,6 +69,26 @@ describe('DropdownValueComponent', () => { expect(styleObj.backgroundColor).toBe(label.color); }); }); + + describe('scopedLabelsDescription', () => { + it('returns html for tooltip', () => { + const html = vm.scopedLabelsDescription(mockLabels[1]); + const $el = $.parseHTML(html); + + expect($el[0]).toHaveClass('scoped-label-tooltip-title'); + expect($el[2].textContent).toEqual(mockLabels[1].description); + }); + }); + + describe('showScopedLabels', () => { + it('returns true if the label is scoped label', () => { + expect(vm.showScopedLabels(mockLabels[1])).toBe(true); + }); + + it('returns false when label is a regular label', () => { + expect(vm.showScopedLabels(mockLabels[0])).toBe(false); + }); + }); }); describe('template', () => { @@ -91,15 +113,25 @@ describe('DropdownValueComponent', () => { ); }); - it('renders label element with tooltip and styles based on label details', () => { + it('renders label element and styles based on label details', () => { 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'); - expect(labelEl.dataset.originalTitle).toBe(mockLabels[0].description); expect(labelEl.getAttribute('style')).toBe('background-color: rgb(186, 218, 85);'); expect(labelEl.innerText.trim()).toBe(mockLabels[0].title); }); + + describe('label is of scoped-label type', () => { + it('renders a scoped-label-wrapper span to incorporate 2 anchors', () => { + expect(vm.$el.querySelector('span.scoped-label-wrapper')).not.toBeNull(); + }); + + it('renders anchor tag containing question icon', () => { + const anchor = vm.$el.querySelector('.scoped-label-wrapper a.scoped-label'); + + expect(anchor).not.toBeNull(); + expect(anchor.querySelector('i.fa-question-circle')).not.toBeNull(); + }); + }); }); }); diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/mock_data.js b/spec/javascripts/vue_shared/components/sidebar/labels_select/mock_data.js index 3fcb91b6f5e..70025f041a7 100644 --- a/spec/javascripts/vue_shared/components/sidebar/labels_select/mock_data.js +++ b/spec/javascripts/vue_shared/components/sidebar/labels_select/mock_data.js @@ -6,6 +6,13 @@ export const mockLabels = [ color: '#BADA55', text_color: '#FFFFFF', }, + { + id: 27, + title: 'Foo::Bar', + description: 'Foobar', + color: '#0033CC', + text_color: '#FFFFFF', + }, ]; export const mockSuggestedColors = [ |