diff options
author | Simon Knox <simon@gitlab.com> | 2019-07-03 17:13:08 +0300 |
---|---|---|
committer | Kushal Pandya <kushalspandya@gmail.com> | 2019-07-03 17:13:08 +0300 |
commit | ec54a56238e72431b3ee3d9b4500a841bc523372 (patch) | |
tree | 93b83500842b8979cf8bd108345277867c2f5f71 /spec/frontend | |
parent | 7a8746896011762d002a0c17d5910899c45ad420 (diff) |
Move error_tracking_frontend specs to Jest
Update TEST_HOST import paths. Tests all pass
Diffstat (limited to 'spec/frontend')
8 files changed, 753 insertions, 0 deletions
diff --git a/spec/frontend/error_tracking_settings/components/app_spec.js b/spec/frontend/error_tracking_settings/components/app_spec.js new file mode 100644 index 00000000000..022f12ef191 --- /dev/null +++ b/spec/frontend/error_tracking_settings/components/app_spec.js @@ -0,0 +1,63 @@ +import Vuex from 'vuex'; +import { createLocalVue, shallowMount } from '@vue/test-utils'; +import ErrorTrackingSettings from '~/error_tracking_settings/components/app.vue'; +import ErrorTrackingForm from '~/error_tracking_settings/components/error_tracking_form.vue'; +import ProjectDropdown from '~/error_tracking_settings/components/project_dropdown.vue'; +import createStore from '~/error_tracking_settings/store'; +import { TEST_HOST } from 'helpers/test_constants'; + +const localVue = createLocalVue(); +localVue.use(Vuex); + +describe('error tracking settings app', () => { + let store; + let wrapper; + + function mountComponent() { + wrapper = shallowMount(ErrorTrackingSettings, { + localVue, + store, // Override the imported store + propsData: { + initialEnabled: 'true', + initialApiHost: TEST_HOST, + initialToken: 'someToken', + initialProject: null, + listProjectsEndpoint: TEST_HOST, + operationsSettingsEndpoint: TEST_HOST, + }, + }); + } + + beforeEach(() => { + store = createStore(); + + mountComponent(); + }); + + afterEach(() => { + if (wrapper) { + wrapper.destroy(); + } + }); + + describe('section', () => { + it('renders the form and dropdown', () => { + expect(wrapper.find(ErrorTrackingForm).exists()).toBeTruthy(); + expect(wrapper.find(ProjectDropdown).exists()).toBeTruthy(); + }); + + it('renders the Save Changes button', () => { + expect(wrapper.find('.js-error-tracking-button').exists()).toBeTruthy(); + }); + + it('enables the button by default', () => { + expect(wrapper.find('.js-error-tracking-button').attributes('disabled')).toBeFalsy(); + }); + + it('disables the button when saving', () => { + store.state.settingsLoading = true; + + expect(wrapper.find('.js-error-tracking-button').attributes('disabled')).toBeTruthy(); + }); + }); +}); diff --git a/spec/frontend/error_tracking_settings/components/error_tracking_form_spec.js b/spec/frontend/error_tracking_settings/components/error_tracking_form_spec.js new file mode 100644 index 00000000000..23e57c4bbf1 --- /dev/null +++ b/spec/frontend/error_tracking_settings/components/error_tracking_form_spec.js @@ -0,0 +1,91 @@ +import Vuex from 'vuex'; +import { createLocalVue, shallowMount } from '@vue/test-utils'; +import { GlButton, GlFormInput } from '@gitlab/ui'; +import ErrorTrackingForm from '~/error_tracking_settings/components/error_tracking_form.vue'; +import { defaultProps } from '../mock'; + +const localVue = createLocalVue(); +localVue.use(Vuex); + +describe('error tracking settings form', () => { + let wrapper; + + function mountComponent() { + wrapper = shallowMount(ErrorTrackingForm, { + localVue, + propsData: defaultProps, + }); + } + + beforeEach(() => { + mountComponent(); + }); + + afterEach(() => { + if (wrapper) { + wrapper.destroy(); + } + }); + + describe('an empty form', () => { + it('is rendered', () => { + expect(wrapper.findAll(GlFormInput).length).toBe(2); + expect(wrapper.find(GlFormInput).attributes('id')).toBe('error-tracking-api-host'); + expect( + wrapper + .findAll(GlFormInput) + .at(1) + .attributes('id'), + ).toBe('error-tracking-token'); + + expect(wrapper.findAll(GlButton).exists()).toBe(true); + }); + + it('is rendered with labels and placeholders', () => { + const pageText = wrapper.text(); + + expect(pageText).toContain('Find your hostname in your Sentry account settings page'); + expect(pageText).toContain( + "After adding your Auth Token, use the 'Connect' button to load projects", + ); + + expect(pageText).not.toContain('Connection has failed. Re-check Auth Token and try again'); + expect( + wrapper + .findAll(GlFormInput) + .at(0) + .attributes('placeholder'), + ).toContain('https://mysentryserver.com'); + }); + }); + + describe('after a successful connection', () => { + beforeEach(() => { + wrapper.setProps({ connectSuccessful: true }); + }); + + it('shows the success checkmark', () => { + expect(wrapper.find('.js-error-tracking-connect-success').isVisible()).toBe(true); + }); + + it('does not show an error', () => { + expect(wrapper.text()).not.toContain( + 'Connection has failed. Re-check Auth Token and try again', + ); + }); + }); + + describe('after an unsuccessful connection', () => { + beforeEach(() => { + wrapper.setProps({ connectError: true }); + }); + + it('does not show the check mark', () => { + expect(wrapper.find('.js-error-tracking-connect-success').isVisible()).toBe(false); + }); + + it('shows an error', () => { + expect(wrapper.text()).toContain('Connection has failed. Re-check Auth Token and try again'); + }); + }); +}); diff --git a/spec/frontend/error_tracking_settings/components/project_dropdown_spec.js b/spec/frontend/error_tracking_settings/components/project_dropdown_spec.js new file mode 100644 index 00000000000..8e5dbe28452 --- /dev/null +++ b/spec/frontend/error_tracking_settings/components/project_dropdown_spec.js @@ -0,0 +1,109 @@ +import _ from 'underscore'; +import Vuex from 'vuex'; +import { createLocalVue, shallowMount } from '@vue/test-utils'; +import { GlDropdown, GlDropdownItem } from '@gitlab/ui'; +import ProjectDropdown from '~/error_tracking_settings/components/project_dropdown.vue'; +import { defaultProps, projectList, staleProject } from '../mock'; + +const localVue = createLocalVue(); +localVue.use(Vuex); + +describe('error tracking settings project dropdown', () => { + let wrapper; + + function mountComponent() { + wrapper = shallowMount(ProjectDropdown, { + localVue, + propsData: { + ..._.pick( + defaultProps, + 'dropdownLabel', + 'invalidProjectLabel', + 'projects', + 'projectSelectionLabel', + 'selectedProject', + 'token', + ), + hasProjects: false, + isProjectInvalid: false, + }, + }); + } + + beforeEach(() => { + mountComponent(); + }); + + afterEach(() => { + if (wrapper) { + wrapper.destroy(); + } + }); + + describe('empty project list', () => { + it('renders the dropdown', () => { + expect(wrapper.find('#project-dropdown').exists()).toBeTruthy(); + expect(wrapper.find(GlDropdown).exists()).toBeTruthy(); + }); + + it('shows helper text', () => { + expect(wrapper.find('.js-project-dropdown-label').exists()).toBeTruthy(); + expect(wrapper.find('.js-project-dropdown-label').text()).toContain( + 'To enable project selection', + ); + }); + + it('does not show an error', () => { + expect(wrapper.find('.js-project-dropdown-error').exists()).toBeFalsy(); + }); + + it('does not contain any dropdown items', () => { + expect(wrapper.find(GlDropdownItem).exists()).toBeFalsy(); + expect(wrapper.find(GlDropdown).props('text')).toBe('No projects available'); + }); + }); + + describe('populated project list', () => { + beforeEach(() => { + wrapper.setProps({ projects: _.clone(projectList), hasProjects: true }); + }); + + it('renders the dropdown', () => { + expect(wrapper.find('#project-dropdown').exists()).toBeTruthy(); + expect(wrapper.find(GlDropdown).exists()).toBeTruthy(); + }); + + it('contains a number of dropdown items', () => { + expect(wrapper.find(GlDropdownItem).exists()).toBeTruthy(); + expect(wrapper.findAll(GlDropdownItem).length).toBe(2); + }); + }); + + describe('selected project', () => { + const selectedProject = _.clone(projectList[0]); + + beforeEach(() => { + wrapper.setProps({ projects: _.clone(projectList), selectedProject, hasProjects: true }); + }); + + it('does not show helper text', () => { + expect(wrapper.find('.js-project-dropdown-label').exists()).toBeFalsy(); + expect(wrapper.find('.js-project-dropdown-error').exists()).toBeFalsy(); + }); + }); + + describe('invalid project selected', () => { + beforeEach(() => { + wrapper.setProps({ + projects: _.clone(projectList), + selectedProject: staleProject, + isProjectInvalid: true, + }); + }); + + it('displays a error', () => { + expect(wrapper.find('.js-project-dropdown-label').exists()).toBeFalsy(); + expect(wrapper.find('.js-project-dropdown-error').exists()).toBeTruthy(); + }); + }); +}); diff --git a/spec/frontend/error_tracking_settings/mock.js b/spec/frontend/error_tracking_settings/mock.js new file mode 100644 index 00000000000..42233f82d54 --- /dev/null +++ b/spec/frontend/error_tracking_settings/mock.js @@ -0,0 +1,92 @@ +import createStore from '~/error_tracking_settings/store'; +import { TEST_HOST } from '../helpers/test_constants'; + +const defaultStore = createStore(); + +export const projectList = [ + { + name: 'name', + slug: 'slug', + organizationName: 'organizationName', + organizationSlug: 'organizationSlug', + }, + { + name: 'name2', + slug: 'slug2', + organizationName: 'organizationName2', + organizationSlug: 'organizationSlug2', + }, +]; + +export const staleProject = { + name: 'staleName', + slug: 'staleSlug', + organizationName: 'staleOrganizationName', + organizationSlug: 'staleOrganizationSlug', +}; + +export const normalizedProject = { + name: 'name', + slug: 'slug', + organizationName: 'organization_name', + organizationSlug: 'organization_slug', +}; + +export const sampleBackendProject = { + name: normalizedProject.name, + slug: normalizedProject.slug, + organization_name: normalizedProject.organizationName, + organization_slug: normalizedProject.organizationSlug, +}; + +export const sampleFrontendSettings = { + apiHost: 'apiHost', + enabled: false, + token: 'token', + selectedProject: { + slug: normalizedProject.slug, + name: normalizedProject.name, + organizationName: normalizedProject.organizationName, + organizationSlug: normalizedProject.organizationSlug, + }, +}; + +export const transformedSettings = { + api_host: 'apiHost', + enabled: false, + token: 'token', + project: { + slug: normalizedProject.slug, + name: normalizedProject.name, + organization_name: normalizedProject.organizationName, + organization_slug: normalizedProject.organizationSlug, + }, +}; + +export const defaultProps = { + ...defaultStore.state, + ...defaultStore.getters, +}; + +export const initialEmptyState = { + apiHost: '', + enabled: false, + project: null, + token: '', + listProjectsEndpoint: TEST_HOST, + operationsSettingsEndpoint: TEST_HOST, +}; + +export const initialPopulatedState = { + apiHost: 'apiHost', + enabled: true, + project: JSON.stringify(projectList[0]), + token: 'token', + listProjectsEndpoint: TEST_HOST, + operationsSettingsEndpoint: TEST_HOST, +}; + +export const projectWithHtmlTemplate = { + ...projectList[0], + name: '<strong>bold</strong>', +}; diff --git a/spec/frontend/error_tracking_settings/store/actions_spec.js b/spec/frontend/error_tracking_settings/store/actions_spec.js new file mode 100644 index 00000000000..1eab0f7470b --- /dev/null +++ b/spec/frontend/error_tracking_settings/store/actions_spec.js @@ -0,0 +1,194 @@ +import MockAdapter from 'axios-mock-adapter'; +import testAction from 'helpers/vuex_action_helper'; +import { TEST_HOST } from 'helpers/test_constants'; +import axios from '~/lib/utils/axios_utils'; +import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; +import { refreshCurrentPage } from '~/lib/utils/url_utility'; +import * as actions from '~/error_tracking_settings/store/actions'; +import * as types from '~/error_tracking_settings/store/mutation_types'; +import defaultState from '~/error_tracking_settings/store/state'; +import { projectList } from '../mock'; + +jest.mock('~/lib/utils/url_utility'); + +describe('error tracking settings actions', () => { + let state; + + describe('project list actions', () => { + let mock; + + beforeEach(() => { + mock = new MockAdapter(axios); + state = { ...defaultState(), listProjectsEndpoint: TEST_HOST }; + }); + + afterEach(() => { + mock.restore(); + refreshCurrentPage.mockClear(); + }); + + it('should request and transform the project list', done => { + mock.onPost(TEST_HOST).reply(() => [200, { projects: projectList }]); + testAction( + actions.fetchProjects, + null, + state, + [], + [ + { type: 'requestProjects' }, + { + type: 'receiveProjectsSuccess', + payload: projectList.map(convertObjectPropsToCamelCase), + }, + ], + () => { + expect(mock.history.post.length).toBe(1); + done(); + }, + ); + }); + + it('should handle a server error', done => { + mock.onPost(`${TEST_HOST}.json`).reply(() => [400]); + testAction( + actions.fetchProjects, + null, + state, + [], + [ + { type: 'requestProjects' }, + { + type: 'receiveProjectsError', + }, + ], + () => { + expect(mock.history.post.length).toBe(1); + done(); + }, + ); + }); + + it('should request projects correctly', done => { + testAction(actions.requestProjects, null, state, [{ type: types.RESET_CONNECT }], [], done); + }); + + it('should receive projects correctly', done => { + const testPayload = []; + testAction( + actions.receiveProjectsSuccess, + testPayload, + state, + [ + { type: types.UPDATE_CONNECT_SUCCESS }, + { type: types.RECEIVE_PROJECTS, payload: testPayload }, + ], + [], + done, + ); + }); + + it('should handle errors when receiving projects', done => { + const testPayload = []; + testAction( + actions.receiveProjectsError, + testPayload, + state, + [{ type: types.UPDATE_CONNECT_ERROR }, { type: types.CLEAR_PROJECTS }], + [], + done, + ); + }); + }); + + describe('save changes actions', () => { + let mock; + + beforeEach(() => { + mock = new MockAdapter(axios); + state = { + operationsSettingsEndpoint: TEST_HOST, + }; + }); + + afterEach(() => { + mock.restore(); + }); + + it('should save the page', done => { + mock.onPatch(TEST_HOST).reply(200); + testAction(actions.updateSettings, null, state, [], [{ type: 'requestSettings' }], () => { + expect(mock.history.patch.length).toBe(1); + expect(refreshCurrentPage).toHaveBeenCalled(); + done(); + }); + }); + + it('should handle a server error', done => { + mock.onPatch(TEST_HOST).reply(400); + testAction( + actions.updateSettings, + null, + state, + [], + [ + { type: 'requestSettings' }, + { + type: 'receiveSettingsError', + payload: new Error('Request failed with status code 400'), + }, + ], + () => { + expect(mock.history.patch.length).toBe(1); + done(); + }, + ); + }); + + it('should request to save the page', done => { + testAction( + actions.requestSettings, + null, + state, + [{ type: types.UPDATE_SETTINGS_LOADING, payload: true }], + [], + done, + ); + }); + + it('should handle errors when requesting to save the page', done => { + testAction( + actions.receiveSettingsError, + {}, + state, + [{ type: types.UPDATE_SETTINGS_LOADING, payload: false }], + [], + done, + ); + }); + }); + + describe('generic actions to update the store', () => { + const testData = 'test'; + it('should reset the `connect success` flag when updating the api host', done => { + testAction( + actions.updateApiHost, + testData, + state, + [{ type: types.UPDATE_API_HOST, payload: testData }, { type: types.RESET_CONNECT }], + [], + done, + ); + }); + + it('should reset the `connect success` flag when updating the token', done => { + testAction( + actions.updateToken, + testData, + state, + [{ type: types.UPDATE_TOKEN, payload: testData }, { type: types.RESET_CONNECT }], + [], + done, + ); + }); + }); +}); diff --git a/spec/frontend/error_tracking_settings/store/getters_spec.js b/spec/frontend/error_tracking_settings/store/getters_spec.js new file mode 100644 index 00000000000..2c5ff084b8a --- /dev/null +++ b/spec/frontend/error_tracking_settings/store/getters_spec.js @@ -0,0 +1,93 @@ +import * as getters from '~/error_tracking_settings/store/getters'; +import defaultState from '~/error_tracking_settings/store/state'; +import { projectList, projectWithHtmlTemplate, staleProject } from '../mock'; + +describe('Error Tracking Settings - Getters', () => { + let state; + + beforeEach(() => { + state = defaultState(); + }); + + describe('hasProjects', () => { + it('should reflect when no projects exist', () => { + expect(getters.hasProjects(state)).toEqual(false); + }); + + it('should reflect when projects exist', () => { + state.projects = projectList; + + expect(getters.hasProjects(state)).toEqual(true); + }); + }); + + describe('isProjectInvalid', () => { + const mockGetters = { hasProjects: true }; + it('should show when a project is valid', () => { + state.projects = projectList; + [state.selectedProject] = projectList; + + expect(getters.isProjectInvalid(state, mockGetters)).toEqual(false); + }); + + it('should show when a project is invalid', () => { + state.projects = projectList; + state.selectedProject = staleProject; + + expect(getters.isProjectInvalid(state, mockGetters)).toEqual(true); + }); + }); + + describe('dropdownLabel', () => { + const mockGetters = { hasProjects: false }; + it('should display correctly when there are no projects available', () => { + expect(getters.dropdownLabel(state, mockGetters)).toEqual('No projects available'); + }); + + it('should display correctly when a project is selected', () => { + [state.selectedProject] = projectList; + + expect(getters.dropdownLabel(state, mockGetters)).toEqual('organizationName | name'); + }); + + it('should display correctly when no project is selected', () => { + state.projects = projectList; + + expect(getters.dropdownLabel(state, { hasProjects: true })).toEqual('Select project'); + }); + }); + + describe('invalidProjectLabel', () => { + it('should display an error containing the project name', () => { + [state.selectedProject] = projectList; + + expect(getters.invalidProjectLabel(state)).toEqual( + 'Project "name" is no longer available. Select another project to continue.', + ); + }); + + it('should properly escape the label text', () => { + state.selectedProject = projectWithHtmlTemplate; + + expect(getters.invalidProjectLabel(state)).toEqual( + 'Project "<strong>bold</strong>" is no longer available. Select another project to continue.', + ); + }); + }); + + describe('projectSelectionLabel', () => { + it('should show the correct message when the token is empty', () => { + expect(getters.projectSelectionLabel(state)).toEqual( + 'To enable project selection, enter a valid Auth Token', + ); + }); + + it('should show the correct message when token exists', () => { + state.token = 'test-token'; + + expect(getters.projectSelectionLabel(state)).toEqual( + "Click 'Connect' to re-establish the connection to Sentry and activate the dropdown.", + ); + }); + }); +}); diff --git a/spec/frontend/error_tracking_settings/store/mutation_spec.js b/spec/frontend/error_tracking_settings/store/mutation_spec.js new file mode 100644 index 00000000000..fa188462c3f --- /dev/null +++ b/spec/frontend/error_tracking_settings/store/mutation_spec.js @@ -0,0 +1,82 @@ +import { TEST_HOST } from 'helpers/test_constants'; +import mutations from '~/error_tracking_settings/store/mutations'; +import defaultState from '~/error_tracking_settings/store/state'; +import * as types from '~/error_tracking_settings/store/mutation_types'; +import { + initialEmptyState, + initialPopulatedState, + projectList, + sampleBackendProject, + normalizedProject, +} from '../mock'; + +describe('error tracking settings mutations', () => { + describe('mutations', () => { + let state; + + beforeEach(() => { + state = defaultState(); + }); + + it('should create an empty initial state correctly', () => { + mutations[types.SET_INITIAL_STATE](state, { + ...initialEmptyState, + }); + + expect(state.apiHost).toEqual(''); + expect(state.enabled).toEqual(false); + expect(state.selectedProject).toEqual(null); + expect(state.token).toEqual(''); + expect(state.listProjectsEndpoint).toEqual(TEST_HOST); + expect(state.operationsSettingsEndpoint).toEqual(TEST_HOST); + }); + + it('should populate the initial state correctly', () => { + mutations[types.SET_INITIAL_STATE](state, { + ...initialPopulatedState, + }); + + expect(state.apiHost).toEqual('apiHost'); + expect(state.enabled).toEqual(true); + expect(state.selectedProject).toEqual(projectList[0]); + expect(state.token).toEqual('token'); + expect(state.listProjectsEndpoint).toEqual(TEST_HOST); + expect(state.operationsSettingsEndpoint).toEqual(TEST_HOST); + }); + + it('should receive projects successfully', () => { + mutations[types.RECEIVE_PROJECTS](state, [sampleBackendProject]); + + expect(state.projects).toEqual([normalizedProject]); + }); + + it('should strip out unnecessary project properties', () => { + mutations[types.RECEIVE_PROJECTS](state, [ + { ...sampleBackendProject, extra_property: 'extra_property' }, + ]); + + expect(state.projects).toEqual([normalizedProject]); + }); + + it('should update state when connect is successful', () => { + mutations[types.UPDATE_CONNECT_SUCCESS](state); + + expect(state.connectSuccessful).toBe(true); + expect(state.connectError).toBe(false); + }); + + it('should update state when connect fails', () => { + mutations[types.UPDATE_CONNECT_ERROR](state); + + expect(state.connectSuccessful).toBe(false); + expect(state.connectError).toBe(true); + }); + + it('should update state when connect is reset', () => { + mutations[types.RESET_CONNECT](state); + + expect(state.connectSuccessful).toBe(false); + expect(state.connectError).toBe(false); + }); + }); +}); diff --git a/spec/frontend/error_tracking_settings/utils_spec.js b/spec/frontend/error_tracking_settings/utils_spec.js new file mode 100644 index 00000000000..4b144f7daf1 --- /dev/null +++ b/spec/frontend/error_tracking_settings/utils_spec.js @@ -0,0 +1,29 @@ +import { transformFrontendSettings } from '~/error_tracking_settings/utils'; +import { sampleFrontendSettings, transformedSettings } from './mock'; + +describe('error tracking settings utils', () => { + describe('data transform functions', () => { + it('should transform settings successfully for the backend', () => { + expect(transformFrontendSettings(sampleFrontendSettings)).toEqual(transformedSettings); + }); + + it('should transform empty values in the settings object to null', () => { + const emptyFrontendSettingsObject = { + apiHost: '', + enabled: false, + token: '', + selectedProject: null, + }; + const transformedEmptySettingsObject = { + api_host: null, + enabled: false, + token: null, + project: null, + }; + + expect(transformFrontendSettings(emptyFrontendSettingsObject)).toEqual( + transformedEmptySettingsObject, + ); + }); + }); +}); |