diff options
Diffstat (limited to 'spec/frontend/deploy_freeze')
6 files changed, 497 insertions, 0 deletions
diff --git a/spec/frontend/deploy_freeze/components/deploy_freeze_modal_spec.js b/spec/frontend/deploy_freeze/components/deploy_freeze_modal_spec.js new file mode 100644 index 00000000000..9ecf6bf375b --- /dev/null +++ b/spec/frontend/deploy_freeze/components/deploy_freeze_modal_spec.js @@ -0,0 +1,92 @@ +import Vuex from 'vuex'; +import { createLocalVue, shallowMount } from '@vue/test-utils'; +import { GlButton, GlModal } from '@gitlab/ui'; +import DeployFreezeModal from '~/deploy_freeze/components/deploy_freeze_modal.vue'; +import TimezoneDropdown from '~/vue_shared/components/timezone_dropdown.vue'; +import createStore from '~/deploy_freeze/store'; + +const localVue = createLocalVue(); +localVue.use(Vuex); + +describe('Deploy freeze modal', () => { + let wrapper; + let store; + const freezePeriodsFixture = getJSONFixture('/api/freeze-periods/freeze_periods.json'); + const timezoneDataFixture = getJSONFixture('/api/freeze-periods/timezone_data.json'); + + beforeEach(() => { + store = createStore({ + projectId: '8', + timezoneData: timezoneDataFixture, + }); + wrapper = shallowMount(DeployFreezeModal, { + attachToDocument: true, + stubs: { + GlModal, + }, + localVue, + store, + }); + }); + + const findModal = () => wrapper.find(GlModal); + const addDeployFreezeButton = () => + findModal() + .findAll(GlButton) + .at(1); + + const setInput = (freezeStartCron, freezeEndCron, selectedTimezone) => { + store.state.freezeStartCron = freezeStartCron; + store.state.freezeEndCron = freezeEndCron; + store.state.selectedTimezone = selectedTimezone; + + wrapper.find('#deploy-freeze-start').trigger('input'); + wrapper.find('#deploy-freeze-end').trigger('input'); + wrapper.find(TimezoneDropdown).trigger('input'); + }; + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + describe('Basic interactions', () => { + it('button is disabled when freeze period is invalid', () => { + expect(addDeployFreezeButton().attributes('disabled')).toBeTruthy(); + }); + }); + + describe('Adding a new deploy freeze', () => { + beforeEach(() => { + const { freeze_start, freeze_end, cron_timezone } = freezePeriodsFixture[0]; + setInput(freeze_start, freeze_end, cron_timezone); + }); + + it('button is enabled when valid freeze period settings are present', () => { + expect(addDeployFreezeButton().attributes('disabled')).toBeUndefined(); + }); + }); + + describe('Validations', () => { + describe('when the cron state is invalid', () => { + beforeEach(() => { + setInput('invalid cron', 'invalid cron', 'invalid timezone'); + }); + + it('disables the add deploy freeze button', () => { + expect(addDeployFreezeButton().attributes('disabled')).toBeTruthy(); + }); + }); + + describe('when the cron state is valid', () => { + beforeEach(() => { + const { freeze_start, freeze_end, cron_timezone } = freezePeriodsFixture[0]; + setInput(freeze_start, freeze_end, cron_timezone); + }); + + it('does not disable the submit button', () => { + expect(addDeployFreezeButton().attributes('disabled')).toBeFalsy(); + }); + }); + }); +}); diff --git a/spec/frontend/deploy_freeze/components/deploy_freeze_settings_spec.js b/spec/frontend/deploy_freeze/components/deploy_freeze_settings_spec.js new file mode 100644 index 00000000000..d40df7de7d1 --- /dev/null +++ b/spec/frontend/deploy_freeze/components/deploy_freeze_settings_spec.js @@ -0,0 +1,42 @@ +import Vuex from 'vuex'; +import { createLocalVue, shallowMount } from '@vue/test-utils'; +import DeployFreezeSettings from '~/deploy_freeze/components/deploy_freeze_settings.vue'; +import DeployFreezeTable from '~/deploy_freeze/components/deploy_freeze_table.vue'; +import DeployFreezeModal from '~/deploy_freeze/components/deploy_freeze_modal.vue'; +import createStore from '~/deploy_freeze/store'; + +const localVue = createLocalVue(); +localVue.use(Vuex); + +describe('Deploy freeze settings', () => { + let wrapper; + let store; + const timezoneDataFixture = getJSONFixture('/api/freeze-periods/timezone_data.json'); + + beforeEach(() => { + store = createStore({ + projectId: '8', + timezoneData: timezoneDataFixture, + }); + jest.spyOn(store, 'dispatch').mockImplementation(); + wrapper = shallowMount(DeployFreezeSettings, { + localVue, + store, + }); + }); + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + describe('Deploy freeze table contains components', () => { + it('contains deploy freeze table', () => { + expect(wrapper.find(DeployFreezeTable).exists()).toBe(true); + }); + + it('contains deploy freeze modal', () => { + expect(wrapper.find(DeployFreezeModal).exists()).toBe(true); + }); + }); +}); diff --git a/spec/frontend/deploy_freeze/components/deploy_freeze_table_spec.js b/spec/frontend/deploy_freeze/components/deploy_freeze_table_spec.js new file mode 100644 index 00000000000..383ffa90b22 --- /dev/null +++ b/spec/frontend/deploy_freeze/components/deploy_freeze_table_spec.js @@ -0,0 +1,70 @@ +import Vuex from 'vuex'; +import { createLocalVue, mount } from '@vue/test-utils'; +import DeployFreezeTable from '~/deploy_freeze/components/deploy_freeze_table.vue'; +import createStore from '~/deploy_freeze/store'; + +const localVue = createLocalVue(); +localVue.use(Vuex); + +describe('Deploy freeze table', () => { + let wrapper; + let store; + const timezoneDataFixture = getJSONFixture('/api/freeze-periods/timezone_data.json'); + + const createComponent = () => { + store = createStore({ + projectId: '8', + timezoneData: timezoneDataFixture, + }); + jest.spyOn(store, 'dispatch').mockImplementation(); + wrapper = mount(DeployFreezeTable, { + attachToDocument: true, + localVue, + store, + }); + }; + + const findEmptyFreezePeriods = () => wrapper.find('[data-testid="empty-freeze-periods"]'); + const findAddDeployFreezeButton = () => wrapper.find('[data-testid="add-deploy-freeze"]'); + const findDeployFreezeTable = () => wrapper.find('[data-testid="deploy-freeze-table"]'); + + beforeEach(() => { + createComponent(); + }); + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + it('dispatches fetchFreezePeriods when mounted', () => { + expect(store.dispatch).toHaveBeenCalledWith('fetchFreezePeriods'); + }); + + describe('Renders correct data', () => { + it('displays empty', () => { + expect(findEmptyFreezePeriods().exists()).toBe(true); + expect(findEmptyFreezePeriods().text()).toBe( + 'No deploy freezes exist for this project. To add one, click Add deploy freeze', + ); + }); + + it('displays data', () => { + const freezePeriodsFixture = getJSONFixture('/api/freeze-periods/freeze_periods.json'); + store.state.freezePeriods = freezePeriodsFixture; + + return wrapper.vm.$nextTick(() => { + const tableRows = findDeployFreezeTable().findAll('tbody tr'); + expect(tableRows.length).toBe(freezePeriodsFixture.length); + expect(findEmptyFreezePeriods().exists()).toBe(false); + }); + }); + }); + + describe('Table click actions', () => { + it('displays add deploy freeze button', () => { + expect(findAddDeployFreezeButton().exists()).toBe(true); + expect(findAddDeployFreezeButton().text()).toBe('Add deploy freeze'); + }); + }); +}); diff --git a/spec/frontend/deploy_freeze/components/timezone_dropdown_spec.js b/spec/frontend/deploy_freeze/components/timezone_dropdown_spec.js new file mode 100644 index 00000000000..644cd0b5f27 --- /dev/null +++ b/spec/frontend/deploy_freeze/components/timezone_dropdown_spec.js @@ -0,0 +1,98 @@ +import Vuex from 'vuex'; +import { shallowMount, createLocalVue } from '@vue/test-utils'; +import { GlDeprecatedDropdownItem, GlNewDropdown } from '@gitlab/ui'; +import TimezoneDropdown from '~/vue_shared/components/timezone_dropdown.vue'; +import createStore from '~/deploy_freeze/store'; + +const localVue = createLocalVue(); +localVue.use(Vuex); + +describe('Deploy freeze timezone dropdown', () => { + let wrapper; + let store; + const timezoneDataFixture = getJSONFixture('/api/freeze-periods/timezone_data.json'); + + const createComponent = (searchTerm, selectedTimezone) => { + store = createStore({ + projectId: '8', + timezoneData: timezoneDataFixture, + }); + wrapper = shallowMount(TimezoneDropdown, { + store, + localVue, + propsData: { + value: selectedTimezone, + timezoneData: timezoneDataFixture, + }, + }); + + wrapper.setData({ searchTerm }); + }; + + const findAllDropdownItems = () => wrapper.findAll(GlDeprecatedDropdownItem); + const findDropdownItemByIndex = index => wrapper.findAll(GlDeprecatedDropdownItem).at(index); + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + describe('No time zones found', () => { + beforeEach(() => { + createComponent('UTC timezone'); + }); + + it('renders empty results message', () => { + expect(findDropdownItemByIndex(0).text()).toBe('No matching results'); + }); + }); + + describe('Search term is empty', () => { + beforeEach(() => { + createComponent(''); + }); + + it('renders all timezones when search term is empty', () => { + expect(findAllDropdownItems()).toHaveLength(timezoneDataFixture.length); + }); + }); + + describe('Time zones found', () => { + beforeEach(() => { + createComponent('Alaska'); + }); + + it('renders only the time zone searched for', () => { + expect(findAllDropdownItems()).toHaveLength(1); + expect(findDropdownItemByIndex(0).text()).toBe('[UTC -8] Alaska'); + }); + + it('should not display empty results message', () => { + expect(wrapper.find('[data-testid="noMatchingResults"]').exists()).toBe(false); + }); + + describe('Custom events', () => { + it('should emit input if a time zone is clicked', () => { + findDropdownItemByIndex(0).vm.$emit('click'); + expect(wrapper.emitted('input')).toEqual([ + [ + { + formattedTimezone: '[UTC -8] Alaska', + identifier: 'America/Juneau', + }, + ], + ]); + }); + }); + }); + + describe('Selected time zone', () => { + beforeEach(() => { + createComponent('', 'Alaska'); + }); + + it('renders selected time zone as dropdown label', () => { + expect(wrapper.find(GlNewDropdown).vm.text).toBe('Alaska'); + }); + }); +}); diff --git a/spec/frontend/deploy_freeze/store/actions_spec.js b/spec/frontend/deploy_freeze/store/actions_spec.js new file mode 100644 index 00000000000..97f94cdbf5e --- /dev/null +++ b/spec/frontend/deploy_freeze/store/actions_spec.js @@ -0,0 +1,123 @@ +import MockAdapter from 'axios-mock-adapter'; +import testAction from 'helpers/vuex_action_helper'; +import Api from '~/api'; +import axios from '~/lib/utils/axios_utils'; +import { deprecatedCreateFlash as createFlash } from '~/flash'; +import getInitialState from '~/deploy_freeze/store/state'; +import * as actions from '~/deploy_freeze/store/actions'; +import * as types from '~/deploy_freeze/store/mutation_types'; + +jest.mock('~/api.js'); +jest.mock('~/flash.js'); + +describe('deploy freeze store actions', () => { + let mock; + let state; + const freezePeriodsFixture = getJSONFixture('/api/freeze-periods/freeze_periods.json'); + const timezoneDataFixture = getJSONFixture('/api/freeze-periods/timezone_data.json'); + + beforeEach(() => { + mock = new MockAdapter(axios); + state = getInitialState({ + projectId: '8', + timezoneData: timezoneDataFixture, + }); + Api.freezePeriods.mockResolvedValue({ data: freezePeriodsFixture }); + Api.createFreezePeriod.mockResolvedValue(); + }); + + afterEach(() => { + mock.restore(); + }); + + describe('setSelectedTimezone', () => { + it('commits SET_SELECTED_TIMEZONE mutation', () => { + testAction(actions.setSelectedTimezone, {}, {}, [ + { + payload: {}, + type: types.SET_SELECTED_TIMEZONE, + }, + ]); + }); + }); + + describe('setFreezeStartCron', () => { + it('commits SET_FREEZE_START_CRON mutation', () => { + testAction(actions.setFreezeStartCron, {}, {}, [ + { + type: types.SET_FREEZE_START_CRON, + }, + ]); + }); + }); + + describe('setFreezeEndCron', () => { + it('commits SET_FREEZE_END_CRON mutation', () => { + testAction(actions.setFreezeEndCron, {}, {}, [ + { + type: types.SET_FREEZE_END_CRON, + }, + ]); + }); + }); + + describe('addFreezePeriod', () => { + it('dispatch correct actions on adding a freeze period', () => { + testAction( + actions.addFreezePeriod, + {}, + state, + [{ type: 'RESET_MODAL' }], + [ + { type: 'requestAddFreezePeriod' }, + { type: 'receiveAddFreezePeriodSuccess' }, + { type: 'fetchFreezePeriods' }, + ], + ); + }); + + it('should show flash error and set error in state on add failure', () => { + Api.createFreezePeriod.mockRejectedValue(); + + testAction( + actions.addFreezePeriod, + {}, + state, + [], + [{ type: 'requestAddFreezePeriod' }, { type: 'receiveAddFreezePeriodError' }], + () => expect(createFlash).toHaveBeenCalled(), + ); + }); + }); + + describe('fetchFreezePeriods', () => { + it('dispatch correct actions on fetchFreezePeriods', () => { + testAction( + actions.fetchFreezePeriods, + {}, + state, + [ + { type: types.REQUEST_FREEZE_PERIODS }, + { type: types.RECEIVE_FREEZE_PERIODS_SUCCESS, payload: freezePeriodsFixture }, + ], + [], + ); + }); + + it('should show flash error and set error in state on fetch variables failure', () => { + Api.freezePeriods.mockRejectedValue(); + + testAction( + actions.fetchFreezePeriods, + {}, + state, + [{ type: types.REQUEST_FREEZE_PERIODS }], + [], + () => + expect(createFlash).toHaveBeenCalledWith( + 'There was an error fetching the deploy freezes.', + ), + ); + }); + }); +}); diff --git a/spec/frontend/deploy_freeze/store/mutations_spec.js b/spec/frontend/deploy_freeze/store/mutations_spec.js new file mode 100644 index 00000000000..0453e037e15 --- /dev/null +++ b/spec/frontend/deploy_freeze/store/mutations_spec.js @@ -0,0 +1,72 @@ +import state from '~/deploy_freeze/store/state'; +import mutations from '~/deploy_freeze/store/mutations'; +import * as types from '~/deploy_freeze/store/mutation_types'; +import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; + +describe('Deploy freeze mutations', () => { + let stateCopy; + const timezoneDataFixture = getJSONFixture('/api/freeze-periods/timezone_data.json'); + + beforeEach(() => { + stateCopy = state({ + projectId: '8', + timezoneData: timezoneDataFixture, + }); + }); + + describe('RESET_MODAL', () => { + it('should reset modal state', () => { + mutations[types.RESET_MODAL](stateCopy); + + expect(stateCopy.freezeStartCron).toBe(''); + expect(stateCopy.freezeEndCron).toBe(''); + expect(stateCopy.selectedTimezone).toBe(''); + expect(stateCopy.selectedTimezoneIdentifier).toBe(''); + }); + }); + + describe('RECEIVE_FREEZE_PERIODS_SUCCESS', () => { + it('should set freeze periods and format timezones from identifiers to names', () => { + const timezoneNames = ['Berlin', 'UTC', 'Eastern Time (US & Canada)']; + const freezePeriodsFixture = getJSONFixture('/api/freeze-periods/freeze_periods.json'); + + mutations[types.RECEIVE_FREEZE_PERIODS_SUCCESS](stateCopy, freezePeriodsFixture); + + const expectedFreezePeriods = freezePeriodsFixture.map((freezePeriod, index) => ({ + ...convertObjectPropsToCamelCase(freezePeriod), + cronTimezone: timezoneNames[index], + })); + + expect(stateCopy.freezePeriods).toMatchObject(expectedFreezePeriods); + }); + }); + + describe('SET_SELECTED_TIMEZONE', () => { + it('should set the cron timezone', () => { + const timezone = { + formattedTimezone: '[UTC -7] Pacific Time (US & Canada)', + identifier: 'America/Los_Angeles', + }; + mutations[types.SET_SELECTED_TIMEZONE](stateCopy, timezone); + + expect(stateCopy.selectedTimezone).toEqual(timezone.formattedTimezone); + expect(stateCopy.selectedTimezoneIdentifier).toEqual(timezone.identifier); + }); + }); + + describe('SET_FREEZE_START_CRON', () => { + it('should set freezeStartCron', () => { + mutations[types.SET_FREEZE_START_CRON](stateCopy, '5 0 * 8 *'); + + expect(stateCopy.freezeStartCron).toBe('5 0 * 8 *'); + }); + }); + + describe('SET_FREEZE_ENDT_CRON', () => { + it('should set freezeEndCron', () => { + mutations[types.SET_FREEZE_END_CRON](stateCopy, '5 0 * 8 *'); + + expect(stateCopy.freezeEndCron).toBe('5 0 * 8 *'); + }); + }); +}); |