diff options
Diffstat (limited to 'spec/frontend/registry')
8 files changed, 512 insertions, 62 deletions
diff --git a/spec/frontend/registry/settings/components/__snapshots__/registry_settings_app_spec.js.snap b/spec/frontend/registry/settings/components/__snapshots__/registry_settings_app_spec.js.snap index c6dbb1da8e9..77f031db120 100644 --- a/spec/frontend/registry/settings/components/__snapshots__/registry_settings_app_spec.js.snap +++ b/spec/frontend/registry/settings/components/__snapshots__/registry_settings_app_spec.js.snap @@ -1,10 +1,10 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Registry List renders 1`] = ` +exports[`Registry Settings App renders 1`] = ` <div> <p> - Tag retention policies are designed to: + Tag expiration policy is designed to: </p> @@ -20,14 +20,6 @@ exports[`Registry List renders 1`] = ` </li> </ul> - <p> - Read more about the - <a - href="foo" - target="_blank" - > - Container Registry tag retention policies - </a> - </p> + <settingsform-stub /> </div> `; diff --git a/spec/frontend/registry/settings/components/__snapshots__/settings_form_spec.js.snap b/spec/frontend/registry/settings/components/__snapshots__/settings_form_spec.js.snap new file mode 100644 index 00000000000..0ae37f70273 --- /dev/null +++ b/spec/frontend/registry/settings/components/__snapshots__/settings_form_spec.js.snap @@ -0,0 +1,155 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Settings Form renders 1`] = ` +<div + class="card" +> + <form> + <div + class="card-header" + > + + Tag expiration policy + + </div> + + <div + class="card-body" + > + <glformgroup-stub + id="expiration-policy-toggle-group" + label="Expiration policy:" + label-align="right" + label-cols="3" + label-for="expiration-policy-toggle" + > + <div + class="d-flex align-items-start" + > + <gltoggle-stub + id="expiration-policy-toggle" + labeloff="Toggle Status: OFF" + labelon="Toggle Status: ON" + /> + + <span + class="mb-2 ml-1 lh-2" + > + Docker tag expiration policy is + <strong> + disabled + </strong> + </span> + </div> + </glformgroup-stub> + + <glformgroup-stub + id="expiration-policy-interval-group" + label="Expiration interval:" + label-align="right" + label-cols="3" + label-for="expiration-policy-interval" + > + <glformselect-stub + id="expiration-policy-interval" + > + <option + value="1" + > + Option 1 + </option> + + <option + value="2" + > + Option 2 + </option> + </glformselect-stub> + </glformgroup-stub> + + <glformgroup-stub + id="expiration-policy-schedule-group" + label="Expiration schedule:" + label-align="right" + label-cols="3" + label-for="expiration-policy-schedule" + > + <glformselect-stub + id="expiration-policy-schedule" + > + <option + value="1" + > + Option 1 + </option> + + <option + value="2" + > + Option 2 + </option> + </glformselect-stub> + </glformgroup-stub> + + <glformgroup-stub + id="expiration-policy-latest-group" + label="Expiration latest:" + label-align="right" + label-cols="3" + label-for="expiration-policy-latest" + > + <glformselect-stub + id="expiration-policy-latest" + > + <option + value="1" + > + Option 1 + </option> + + <option + value="2" + > + Option 2 + </option> + </glformselect-stub> + </glformgroup-stub> + + <glformgroup-stub + id="expiration-policy-name-matching-group" + invalid-feedback="The value of this input should be less than 255 characters" + label="Expire Docker tags with name matching:" + label-align="right" + label-cols="3" + label-for="expiration-policy-name-matching" + > + <glformtextarea-stub + id="expiration-policy-name-matching" + placeholder=".*" + trim="" + value="" + /> + </glformgroup-stub> + </div> + + <div + class="card-footer text-right" + > + <glbutton-stub + type="reset" + > + Cancel + </glbutton-stub> + + <glbutton-stub + type="submit" + variant="success" + > + + Save Expiration Policy + + </glbutton-stub> + </div> + </form> +</div> +`; diff --git a/spec/frontend/registry/settings/components/registry_settings_app_spec.js b/spec/frontend/registry/settings/components/registry_settings_app_spec.js index 666d970aa6b..e0fe6172064 100644 --- a/spec/frontend/registry/settings/components/registry_settings_app_spec.js +++ b/spec/frontend/registry/settings/components/registry_settings_app_spec.js @@ -1,29 +1,34 @@ import Vuex from 'vuex'; import { shallowMount, createLocalVue } from '@vue/test-utils'; import component from '~/registry/settings/components/registry_settings_app.vue'; -import { createStore } from '~/registry/settings/stores/'; +import { createStore } from '~/registry/settings/store/'; const localVue = createLocalVue(); localVue.use(Vuex); -describe('Registry List', () => { +describe('Registry Settings App', () => { let wrapper; let store; + let fetchSpy; - const helpPagePath = 'foo'; - const findHelpLink = () => wrapper.find({ ref: 'help-link' }).find('a'); + const findSettingsComponent = () => wrapper.find({ ref: 'settings-form' }); + const findLoadingComponent = () => wrapper.find({ ref: 'loading-icon' }); - const mountComponent = (options = {}) => - shallowMount(component, { + const mountComponent = (options = {}) => { + fetchSpy = jest.fn(); + wrapper = shallowMount(component, { sync: false, store, + methods: { + fetchSettings: fetchSpy, + }, ...options, }); + }; beforeEach(() => { store = createStore(); - store.dispatch('setInitialState', { helpPagePath }); - wrapper = mountComponent(); + mountComponent(); }); afterEach(() => { @@ -34,7 +39,18 @@ describe('Registry List', () => { expect(wrapper.element).toMatchSnapshot(); }); - it('renders an help link dependant on the helphPagePath', () => { - expect(findHelpLink().attributes('href')).toBe(helpPagePath); + it('call the store function to load the data on mount', () => { + expect(fetchSpy).toHaveBeenCalled(); + }); + + it('renders a loader if isLoading is true', () => { + store.dispatch('toggleLoading'); + return wrapper.vm.$nextTick().then(() => { + expect(findLoadingComponent().exists()).toBe(true); + expect(findSettingsComponent().exists()).toBe(false); + }); + }); + it('renders the setting form', () => { + expect(findSettingsComponent().exists()).toBe(true); }); }); diff --git a/spec/frontend/registry/settings/components/settings_form_spec.js b/spec/frontend/registry/settings/components/settings_form_spec.js new file mode 100644 index 00000000000..6d69b987c7f --- /dev/null +++ b/spec/frontend/registry/settings/components/settings_form_spec.js @@ -0,0 +1,154 @@ +import Vuex from 'vuex'; +import { shallowMount, createLocalVue } from '@vue/test-utils'; +import component from '~/registry/settings/components/settings_form.vue'; +import { createStore } from '~/registry/settings/store/'; +import { NAME_REGEX_LENGTH } from '~/registry/settings/constants'; + +const localVue = createLocalVue(); +localVue.use(Vuex); + +describe('Settings Form', () => { + let wrapper; + let store; + let saveSpy; + let resetSpy; + + const helpPagePath = 'foo'; + const findFormGroup = name => wrapper.find(`#expiration-policy-${name}-group`); + const findFormElements = (name, father = wrapper) => father.find(`#expiration-policy-${name}`); + const findCancelButton = () => wrapper.find({ ref: 'cancel-button' }); + const findSaveButton = () => wrapper.find({ ref: 'save-button' }); + const findForm = () => wrapper.find({ ref: 'form-element' }); + + const mountComponent = (options = {}) => { + saveSpy = jest.fn(); + resetSpy = jest.fn(); + wrapper = shallowMount(component, { + sync: false, + store, + methods: { + saveSettings: saveSpy, + resetSettings: resetSpy, + }, + ...options, + }); + }; + + beforeEach(() => { + store = createStore(); + store.dispatch('setInitialState', { helpPagePath }); + mountComponent(); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('renders', () => { + expect(wrapper.element).toMatchSnapshot(); + }); + + describe.each` + elementName | modelName | value + ${'toggle'} | ${'enabled'} | ${true} + ${'interval'} | ${'older_than'} | ${'foo'} + ${'schedule'} | ${'cadence'} | ${'foo'} + ${'latest'} | ${'keep_n'} | ${'foo'} + ${'name-matching'} | ${'name_regex'} | ${'foo'} + `('%s form element', ({ elementName, modelName, value }) => { + let formGroup; + beforeEach(() => { + formGroup = findFormGroup(elementName); + }); + it(`${elementName} form group exist in the dom`, () => { + expect(formGroup.exists()).toBe(true); + }); + + it(`${elementName} form group has a label-for property`, () => { + expect(formGroup.attributes('label-for')).toBe(`expiration-policy-${elementName}`); + }); + + it(`${elementName} form group has a label-cols property`, () => { + expect(formGroup.attributes('label-cols')).toBe(`${wrapper.vm.$options.labelsConfig.cols}`); + }); + + it(`${elementName} form group has a label-align property`, () => { + expect(formGroup.attributes('label-align')).toBe(`${wrapper.vm.$options.labelsConfig.align}`); + }); + + it(`${elementName} form group contains an input element`, () => { + expect(findFormElements(elementName, formGroup).exists()).toBe(true); + }); + + it(`${elementName} form element change updated ${modelName} with ${value}`, () => { + const element = findFormElements(elementName, formGroup); + element.vm.$emit('input', value); + expect(wrapper.vm[modelName]).toBe(value); + }); + }); + + describe('form actions', () => { + let form; + beforeEach(() => { + form = findForm(); + }); + it('cancel has type reset', () => { + expect(findCancelButton().attributes('type')).toBe('reset'); + }); + + it('form reset event call the appropriate function', () => { + form.trigger('reset'); + expect(resetSpy).toHaveBeenCalled(); + }); + + it('save has type submit', () => { + expect(findSaveButton().attributes('type')).toBe('submit'); + }); + + it('form submit event call the appropriate function', () => { + form.trigger('submit'); + expect(saveSpy).toHaveBeenCalled(); + }); + }); + + describe('form validation', () => { + describe(`when name regex is longer than ${NAME_REGEX_LENGTH}`, () => { + const invalidString = new Array(NAME_REGEX_LENGTH + 2).join(','); + beforeEach(() => { + store.dispatch('updateSettings', { name_regex: invalidString }); + }); + + it('save btn is disabled', () => { + expect(findSaveButton().attributes('disabled')).toBeTruthy(); + }); + + it('nameRegexState is false', () => { + expect(wrapper.vm.nameRegexState).toBe(false); + }); + }); + + it('if the user did not type validation is null', () => { + store.dispatch('updateSettings', { name_regex: null }); + expect(wrapper.vm.nameRegexState).toBe(null); + return wrapper.vm.$nextTick().then(() => { + expect(findSaveButton().attributes('disabled')).toBeFalsy(); + }); + }); + + it(`if the user typed and is less than ${NAME_REGEX_LENGTH} state is true`, () => { + store.dispatch('updateSettings', { name_regex: 'abc' }); + expect(wrapper.vm.nameRegexState).toBe(true); + }); + }); + + describe('help text', () => { + it('toggleDescriptionText text reflects enabled property', () => { + const toggleHelpText = findFormGroup('toggle').find('span'); + expect(toggleHelpText.html()).toContain('disabled'); + wrapper.vm.enabled = true; + return wrapper.vm.$nextTick().then(() => { + expect(toggleHelpText.html()).toContain('enabled'); + }); + }); + }); +}); diff --git a/spec/frontend/registry/settings/store/actions_spec.js b/spec/frontend/registry/settings/store/actions_spec.js new file mode 100644 index 00000000000..71c815cd19c --- /dev/null +++ b/spec/frontend/registry/settings/store/actions_spec.js @@ -0,0 +1,120 @@ +import Api from '~/api'; +import createFlash from '~/flash'; +import testAction from 'helpers/vuex_action_helper'; +import * as actions from '~/registry/settings/store/actions'; +import * as types from '~/registry/settings/store/mutation_types'; +import { + UPDATE_SETTINGS_ERROR_MESSAGE, + FETCH_SETTINGS_ERROR_MESSAGE, + UPDATE_SETTINGS_SUCCESS_MESSAGE, +} from '~/registry/settings/constants'; + +jest.mock('~/flash'); + +describe('Actions Registry Store', () => { + describe.each` + actionName | mutationName | payload + ${'setInitialState'} | ${types.SET_INITIAL_STATE} | ${'foo'} + ${'updateSettings'} | ${types.UPDATE_SETTINGS} | ${'foo'} + ${'receiveSettingsSuccess'} | ${types.SET_SETTINGS} | ${'foo'} + ${'toggleLoading'} | ${types.TOGGLE_LOADING} | ${undefined} + ${'resetSettings'} | ${types.RESET_SETTINGS} | ${undefined} + `('%s action invokes %s mutation with payload %s', ({ actionName, mutationName, payload }) => { + it('should set the initial state', done => { + testAction(actions[actionName], payload, {}, [{ type: mutationName, payload }], [], done); + }); + }); + + describe.each` + actionName | message + ${'receiveSettingsError'} | ${FETCH_SETTINGS_ERROR_MESSAGE} + ${'updateSettingsError'} | ${UPDATE_SETTINGS_ERROR_MESSAGE} + `('%s action', ({ actionName, message }) => { + it(`should call createFlash with ${message}`, done => { + testAction(actions[actionName], null, null, [], [], () => { + expect(createFlash).toHaveBeenCalledWith(message); + done(); + }); + }); + }); + + describe('fetchSettings', () => { + const state = { + projectId: 'bar', + }; + + const payload = { + tag_expiration_policies: 'foo', + }; + + it('should fetch the data from the API', done => { + Api.project = jest.fn().mockResolvedValue(payload); + testAction( + actions.fetchSettings, + null, + state, + [], + [ + { type: 'toggleLoading' }, + { type: 'receiveSettingsSuccess', payload: payload.tag_expiration_policies }, + { type: 'toggleLoading' }, + ], + done, + ); + }); + + it('should call receiveSettingsError on error', done => { + Api.project = jest.fn().mockRejectedValue(); + testAction( + actions.fetchSettings, + null, + state, + [], + [{ type: 'toggleLoading' }, { type: 'receiveSettingsError' }, { type: 'toggleLoading' }], + done, + ); + }); + }); + + describe('saveSettings', () => { + const state = { + projectId: 'bar', + settings: 'baz', + }; + + const payload = { + tag_expiration_policies: 'foo', + }; + + it('should fetch the data from the API', done => { + Api.updateProject = jest.fn().mockResolvedValue(payload); + testAction( + actions.saveSettings, + null, + state, + [], + [ + { type: 'toggleLoading' }, + { type: 'receiveSettingsSuccess', payload: payload.tag_expiration_policies }, + { type: 'toggleLoading' }, + ], + () => { + expect(createFlash).toHaveBeenCalledWith(UPDATE_SETTINGS_SUCCESS_MESSAGE); + done(); + }, + ); + }); + + it('should call receiveSettingsError on error', done => { + Api.updateProject = jest.fn().mockRejectedValue(); + testAction( + actions.saveSettings, + null, + state, + [], + [{ type: 'toggleLoading' }, { type: 'updateSettingsError' }, { type: 'toggleLoading' }], + done, + ); + }); + }); +}); diff --git a/spec/frontend/registry/settings/store/mutations_spec.js b/spec/frontend/registry/settings/store/mutations_spec.js new file mode 100644 index 00000000000..a8c7ed3bafa --- /dev/null +++ b/spec/frontend/registry/settings/store/mutations_spec.js @@ -0,0 +1,54 @@ +import mutations from '~/registry/settings/store/mutations'; +import * as types from '~/registry/settings/store/mutation_types'; +import createState from '~/registry/settings/store/state'; + +describe('Mutations Registry Store', () => { + let mockState; + + beforeEach(() => { + mockState = createState(); + }); + + describe('SET_INITIAL_STATE', () => { + it('should set the initial state', () => { + const payload = { helpPagePath: 'foo', projectId: 'bar' }; + const expectedState = { ...mockState, ...payload }; + mutations[types.SET_INITIAL_STATE](mockState, payload); + + expect(mockState.projectId).toEqual(expectedState.projectId); + }); + }); + + describe('UPDATE_SETTINGS', () => { + it('should update the settings', () => { + mockState.settings = { foo: 'bar' }; + const payload = { foo: 'baz' }; + const expectedState = { ...mockState, settings: payload }; + mutations[types.UPDATE_SETTINGS](mockState, payload); + expect(mockState.settings).toEqual(expectedState.settings); + }); + }); + describe('SET_SETTINGS', () => { + it('should set the settings and original', () => { + const payload = { foo: 'baz' }; + const expectedState = { ...mockState, settings: payload }; + mutations[types.SET_SETTINGS](mockState, payload); + expect(mockState.settings).toEqual(expectedState.settings); + expect(mockState.original).toEqual(expectedState.settings); + }); + }); + describe('RESET_SETTINGS', () => { + it('should copy original over settings', () => { + mockState.settings = { foo: 'bar' }; + mockState.original = { foo: 'baz' }; + mutations[types.RESET_SETTINGS](mockState); + expect(mockState.settings).toEqual(mockState.original); + }); + }); + describe('TOGGLE_LOADING', () => { + it('should toggle the loading', () => { + mutations[types.TOGGLE_LOADING](mockState); + expect(mockState.isLoading).toEqual(true); + }); + }); +}); diff --git a/spec/frontend/registry/settings/stores/actions_spec.js b/spec/frontend/registry/settings/stores/actions_spec.js deleted file mode 100644 index 484f1b2dc0a..00000000000 --- a/spec/frontend/registry/settings/stores/actions_spec.js +++ /dev/null @@ -1,20 +0,0 @@ -import testAction from 'helpers/vuex_action_helper'; -import * as actions from '~/registry/settings/stores/actions'; -import * as types from '~/registry/settings/stores/mutation_types'; - -jest.mock('~/flash.js'); - -describe('Actions Registry Store', () => { - describe('setInitialState', () => { - it('should set the initial state', done => { - testAction( - actions.setInitialState, - 'foo', - {}, - [{ type: types.SET_INITIAL_STATE, payload: 'foo' }], - [], - done, - ); - }); - }); -}); diff --git a/spec/frontend/registry/settings/stores/mutations_spec.js b/spec/frontend/registry/settings/stores/mutations_spec.js deleted file mode 100644 index 421cd3f13cb..00000000000 --- a/spec/frontend/registry/settings/stores/mutations_spec.js +++ /dev/null @@ -1,21 +0,0 @@ -import mutations from '~/registry/settings/stores/mutations'; -import * as types from '~/registry/settings/stores/mutation_types'; -import createState from '~/registry/settings/stores/state'; - -describe('Mutations Registry Store', () => { - let mockState; - - beforeEach(() => { - mockState = createState(); - }); - - describe('SET_INITIAL_STATE', () => { - it('should set the initial state', () => { - const payload = { helpPagePath: 'foo', registrySettingsEndpoint: 'bar' }; - const expectedState = { ...mockState, ...payload }; - mutations[types.SET_INITIAL_STATE](mockState, payload); - - expect(mockState.endpoint).toEqual(expectedState.endpoint); - }); - }); -}); |