diff options
Diffstat (limited to 'spec/frontend')
16 files changed, 735 insertions, 1109 deletions
diff --git a/spec/frontend/branches/components/__snapshots__/delete_merged_branches_spec.js.snap b/spec/frontend/branches/components/__snapshots__/delete_merged_branches_spec.js.snap new file mode 100644 index 00000000000..6aab3b51806 --- /dev/null +++ b/spec/frontend/branches/components/__snapshots__/delete_merged_branches_spec.js.snap @@ -0,0 +1,139 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Delete merged branches component Delete merged branches confirmation modal matches snapshot 1`] = ` +<div> + <b-button-stub + class="gl-mr-3 gl-button btn-danger-secondary" + data-qa-selector="delete_merged_branches_button" + size="md" + tag="button" + type="button" + variant="danger" + > + <!----> + + <!----> + + <span + class="gl-button-text" + > + Delete merged branches + + </span> + </b-button-stub> + + <div> + <form + action="/namespace/project/-/merged_branches" + method="post" + > + <p> + You are about to + <strong> + delete all branches + </strong> + that were merged into + <code> + master + </code> + . + </p> + + <p> + + This may include merged branches that are not visible on the current screen. + + </p> + + <p> + + A branch won't be deleted if it is protected or associated with an open merge request. + + </p> + + <p> + This bulk action is + <strong> + permanent and cannot be undone or recovered + </strong> + . + </p> + + <p> + Plese type the following to confirm: + <code> + delete + </code> + . + <b-form-input-stub + aria-labelledby="input-label" + autocomplete="off" + class="gl-form-input gl-mt-2 gl-form-input-sm" + data-qa-selector="delete_merged_branches_input" + debounce="0" + formatter="[Function]" + type="text" + value="" + /> + </p> + + <input + name="_method" + type="hidden" + value="delete" + /> + + <input + name="authenticity_token" + type="hidden" + value="mock-csrf-token" + /> + </form> + <div + class="gl-display-flex gl-flex-direction-row gl-justify-content-end gl-flex-wrap gl-m-0 gl-mr-3" + > + <b-button-stub + class="gl-button" + data-testid="delete-merged-branches-cancel-button" + size="md" + tag="button" + type="button" + variant="default" + > + <!----> + + <!----> + + <span + class="gl-button-text" + > + + Cancel + + </span> + </b-button-stub> + + <b-button-stub + class="gl-button" + data-qa-selector="delete_merged_branches_confirmation_button" + data-testid="delete-merged-branches-confirmation-button" + disabled="true" + size="md" + tag="button" + type="button" + variant="danger" + > + <!----> + + <!----> + + <span + class="gl-button-text" + > + Delete merged branches + </span> + </b-button-stub> + </div> + </div> +</div> +`; diff --git a/spec/frontend/branches/components/delete_merged_branches_spec.js b/spec/frontend/branches/components/delete_merged_branches_spec.js new file mode 100644 index 00000000000..4f1e772f4a4 --- /dev/null +++ b/spec/frontend/branches/components/delete_merged_branches_spec.js @@ -0,0 +1,143 @@ +import { GlButton, GlModal, GlFormInput, GlSprintf } from '@gitlab/ui'; +import { mount } from '@vue/test-utils'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import { stubComponent } from 'helpers/stub_component'; +import waitForPromises from 'helpers/wait_for_promises'; +import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; +import DeleteMergedBranches, { i18n } from '~/branches/components/delete_merged_branches.vue'; +import { formPath, propsDataMock } from '../mock_data'; + +jest.mock('~/lib/utils/csrf', () => ({ token: 'mock-csrf-token' })); + +let wrapper; + +const stubsData = { + GlModal: stubComponent(GlModal, { + template: + '<div><slot name="modal-title"></slot><slot></slot><slot name="modal-footer"></slot></div>', + }), + GlButton, + GlFormInput, + GlSprintf, +}; + +const createComponent = (mountFn = shallowMountExtended, stubs = {}) => { + wrapper = mountFn(DeleteMergedBranches, { + propsData: { + ...propsDataMock, + }, + directives: { + GlTooltip: createMockDirective(), + }, + stubs, + }); +}; + +const findDeleteButton = () => wrapper.findComponent(GlButton); +const findModal = () => wrapper.findComponent(GlModal); +const findConfirmationButton = () => + wrapper.findByTestId('delete-merged-branches-confirmation-button'); +const findCancelButton = () => wrapper.findByTestId('delete-merged-branches-cancel-button'); +const findFormInput = () => wrapper.findComponent(GlFormInput); +const findForm = () => wrapper.find('form'); +const submitFormSpy = () => jest.spyOn(wrapper.vm.$refs.form, 'submit'); + +describe('Delete merged branches component', () => { + beforeEach(() => { + createComponent(); + }); + + describe('Delete merged branches button', () => { + it('has correct attributes, text and tooltip', () => { + expect(findDeleteButton().attributes()).toMatchObject({ + category: 'secondary', + variant: 'danger', + }); + + expect(findDeleteButton().text()).toBe(i18n.deleteButtonText); + }); + + it('displays a tooltip', () => { + const tooltip = getBinding(findDeleteButton().element, 'gl-tooltip'); + + expect(tooltip).toBeDefined(); + expect(tooltip.value).toBe(wrapper.vm.buttonTooltipText); + }); + + it('opens modal when clicked', () => { + createComponent(mount); + jest.spyOn(wrapper.vm.$refs.modal, 'show'); + findDeleteButton().trigger('click'); + + expect(wrapper.vm.$refs.modal.show).toHaveBeenCalled(); + }); + }); + + describe('Delete merged branches confirmation modal', () => { + beforeEach(() => { + createComponent(shallowMountExtended, stubsData); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('renders correct modal title and text', () => { + const modalText = findModal().text(); + expect(findModal().props('title')).toBe(i18n.modalTitle); + expect(modalText).toContain(i18n.notVisibleBranchesWarning); + expect(modalText).toContain(i18n.protectedBranchWarning); + }); + + it('renders confirm and cancel buttons with correct text', () => { + expect(findConfirmationButton().text()).toContain(i18n.deleteButtonText); + expect(findCancelButton().text()).toContain(i18n.cancelButtonText); + }); + + it('renders form with correct attributes and hiden inputs', () => { + const form = findForm(); + expect(form.attributes()).toEqual({ + action: formPath, + method: 'post', + }); + expect(form.find('input[name="_method"]').attributes('value')).toBe('delete'); + expect(form.find('input[name="authenticity_token"]').attributes('value')).toBe( + 'mock-csrf-token', + ); + }); + + it('matches snapshot', () => { + expect(wrapper.element).toMatchSnapshot(); + }); + + it('has a disabled confirm button by default', () => { + expect(findConfirmationButton().props('disabled')).toBe(true); + }); + + it('keeps disabled state when wrong input is provided', async () => { + findFormInput().vm.$emit('input', 'hello'); + await waitForPromises(); + expect(findConfirmationButton().props('disabled')).toBe(true); + findConfirmationButton().trigger('click'); + + expect(submitFormSpy()).not.toHaveBeenCalled(); + findFormInput().trigger('keyup.enter'); + + expect(submitFormSpy()).not.toHaveBeenCalled(); + }); + + it('submits form when correct amount is provided and the confirm button is clicked', async () => { + findFormInput().vm.$emit('input', 'delete'); + await waitForPromises(); + expect(findDeleteButton().props('disabled')).not.toBe(true); + findConfirmationButton().trigger('click'); + expect(submitFormSpy()).toHaveBeenCalled(); + }); + + it('calls hide on the modal when cancel button is clicked', () => { + const closeModalSpy = jest.spyOn(wrapper.vm.$refs.modal, 'hide'); + findCancelButton().trigger('click'); + expect(closeModalSpy).toHaveBeenCalled(); + }); + }); +}); diff --git a/spec/frontend/branches/mock_data.js b/spec/frontend/branches/mock_data.js new file mode 100644 index 00000000000..9e8839d8ce9 --- /dev/null +++ b/spec/frontend/branches/mock_data.js @@ -0,0 +1,7 @@ +export const formPath = '/namespace/project/-/merged_branches'; +const defaultBranch = 'master'; + +export const propsDataMock = { + formPath, + defaultBranch, +}; diff --git a/spec/frontend/ci_variable_list/components/legacy_ci_environments_dropdown_spec.js b/spec/frontend/ci_variable_list/components/legacy_ci_environments_dropdown_spec.js deleted file mode 100644 index b3e23ba4201..00000000000 --- a/spec/frontend/ci_variable_list/components/legacy_ci_environments_dropdown_spec.js +++ /dev/null @@ -1,119 +0,0 @@ -import { GlDropdown, GlDropdownItem, GlIcon } from '@gitlab/ui'; -import { mount } from '@vue/test-utils'; -import Vue, { nextTick } from 'vue'; -import Vuex from 'vuex'; -import LegacyCiEnvironmentsDropdown from '~/ci_variable_list/components/legacy_ci_environments_dropdown.vue'; - -Vue.use(Vuex); - -describe('Ci environments dropdown', () => { - let wrapper; - let store; - - const enterSearchTerm = (value) => - wrapper.find('[data-testid="ci-environment-search"]').setValue(value); - - const createComponent = (term) => { - store = new Vuex.Store({ - getters: { - joinedEnvironments: () => ['dev', 'prod', 'staging'], - }, - }); - - wrapper = mount(LegacyCiEnvironmentsDropdown, { - store, - propsData: { - value: term, - }, - }); - enterSearchTerm(term); - }; - - const findAllDropdownItems = () => wrapper.findAllComponents(GlDropdownItem); - const findDropdownItemByIndex = (index) => wrapper.findAllComponents(GlDropdownItem).at(index); - const findActiveIconByIndex = (index) => findDropdownItemByIndex(index).findComponent(GlIcon); - - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - - describe('No environments found', () => { - beforeEach(() => { - createComponent('stable'); - }); - - it('renders create button with search term if environments do not contain search term', () => { - expect(findAllDropdownItems()).toHaveLength(2); - expect(findDropdownItemByIndex(1).text()).toBe('Create wildcard: stable'); - }); - - it('renders empty results message', () => { - expect(findDropdownItemByIndex(0).text()).toBe('No matching results'); - }); - }); - - describe('Search term is empty', () => { - beforeEach(() => { - createComponent(''); - }); - - it('renders all environments when search term is empty', () => { - expect(findAllDropdownItems()).toHaveLength(3); - expect(findDropdownItemByIndex(0).text()).toBe('dev'); - expect(findDropdownItemByIndex(1).text()).toBe('prod'); - expect(findDropdownItemByIndex(2).text()).toBe('staging'); - }); - - it('should not display active checkmark on the inactive stage', () => { - expect(findActiveIconByIndex(0).classes('gl-visibility-hidden')).toBe(true); - }); - }); - - describe('Environments found', () => { - beforeEach(async () => { - createComponent('prod'); - await nextTick(); - }); - - it('renders only the environment searched for', () => { - expect(findAllDropdownItems()).toHaveLength(1); - expect(findDropdownItemByIndex(0).text()).toBe('prod'); - }); - - it('should not display create button', () => { - const environments = findAllDropdownItems().filter((env) => env.text().startsWith('Create')); - expect(environments).toHaveLength(0); - expect(findAllDropdownItems()).toHaveLength(1); - }); - - it('should not display empty results message', () => { - expect(wrapper.findComponent({ ref: 'noMatchingResults' }).exists()).toBe(false); - }); - - it('should display active checkmark if active', () => { - expect(findActiveIconByIndex(0).classes('gl-visibility-hidden')).toBe(false); - }); - - it('should clear the search term when showing the dropdown', () => { - wrapper.findComponent(GlDropdown).trigger('click'); - - expect(wrapper.find('[data-testid="ci-environment-search"]').text()).toBe(''); - }); - - describe('Custom events', () => { - it('should emit selectEnvironment if an environment is clicked', () => { - findDropdownItemByIndex(0).vm.$emit('click'); - expect(wrapper.emitted('selectEnvironment')).toEqual([['prod']]); - }); - - it('should emit createClicked if an environment is clicked', async () => { - createComponent('newscope'); - - await nextTick(); - findDropdownItemByIndex(1).vm.$emit('click'); - expect(wrapper.emitted('createClicked')).toEqual([['newscope']]); - }); - }); - }); -}); diff --git a/spec/frontend/ci_variable_list/components/legacy_ci_variable_modal_spec.js b/spec/frontend/ci_variable_list/components/legacy_ci_variable_modal_spec.js deleted file mode 100644 index b607232907b..00000000000 --- a/spec/frontend/ci_variable_list/components/legacy_ci_variable_modal_spec.js +++ /dev/null @@ -1,323 +0,0 @@ -import { GlButton, GlFormInput } from '@gitlab/ui'; -import { shallowMount, mount } from '@vue/test-utils'; -import Vue from 'vue'; -import Vuex from 'vuex'; -import { mockTracking } from 'helpers/tracking_helper'; -import CiEnvironmentsDropdown from '~/ci_variable_list/components/ci_environments_dropdown.vue'; -import LegacyCiVariableModal from '~/ci_variable_list/components/legacy_ci_variable_modal.vue'; -import { - AWS_ACCESS_KEY_ID, - EVENT_LABEL, - EVENT_ACTION, - ENVIRONMENT_SCOPE_LINK_TITLE, -} from '~/ci_variable_list/constants'; -import createStore from '~/ci_variable_list/store'; -import mockData from '../services/mock_data'; -import ModalStub from '../stubs'; - -Vue.use(Vuex); - -describe('Ci variable modal', () => { - let wrapper; - let store; - let trackingSpy; - - const maskableRegex = '^[a-zA-Z0-9_+=/@:.~-]{8,}$'; - - const createComponent = (method, options = {}) => { - store = createStore({ - maskableRegex, - isGroup: options.isGroup, - environmentScopeLink: '/help/environments', - }); - wrapper = method(LegacyCiVariableModal, { - attachTo: document.body, - stubs: { - GlModal: ModalStub, - }, - store, - ...options, - }); - }; - - const findCiEnvironmentsDropdown = () => wrapper.findComponent(CiEnvironmentsDropdown); - const findModal = () => wrapper.findComponent(ModalStub); - const findAddorUpdateButton = () => findModal().find('[data-testid="ciUpdateOrAddVariableBtn"]'); - const deleteVariableButton = () => - findModal() - .findAllComponents(GlButton) - .wrappers.find((button) => button.props('variant') === 'danger'); - - afterEach(() => { - wrapper.destroy(); - }); - - describe('Basic interactions', () => { - beforeEach(() => { - createComponent(shallowMount); - }); - - it('button is disabled when no key/value pair are present', () => { - expect(findAddorUpdateButton().attributes('disabled')).toBe('true'); - }); - }); - - describe('Adding a new variable', () => { - beforeEach(() => { - const [variable] = mockData.mockVariables; - createComponent(shallowMount); - jest.spyOn(store, 'dispatch').mockImplementation(); - store.state.variable = variable; - }); - - it('button is enabled when key/value pair are present', () => { - expect(findAddorUpdateButton().attributes('disabled')).toBeUndefined(); - }); - - it('Add variable button dispatches addVariable action', () => { - findAddorUpdateButton().vm.$emit('click'); - expect(store.dispatch).toHaveBeenCalledWith('addVariable'); - }); - - it('Clears the modal state once modal is hidden', () => { - findModal().vm.$emit('hidden'); - expect(store.dispatch).toHaveBeenCalledWith('clearModal'); - }); - - it('should dispatch setVariableProtected when admin settings are configured to protect variables', () => { - store.state.isProtectedByDefault = true; - findModal().vm.$emit('shown'); - - expect(store.dispatch).toHaveBeenCalledWith('setVariableProtected'); - }); - }); - - describe('Adding a new non-AWS variable', () => { - beforeEach(() => { - const [variable] = mockData.mockVariables; - const invalidKeyVariable = { - ...variable, - key: 'key', - value: 'value', - secret_value: 'secret_value', - }; - createComponent(mount); - store.state.variable = invalidKeyVariable; - }); - - it('does not show AWS guidance tip', () => { - const tip = wrapper.find(`div[data-testid='aws-guidance-tip']`); - expect(tip.exists()).toBe(true); - expect(tip.isVisible()).toBe(false); - }); - }); - - describe('Adding a new AWS variable', () => { - beforeEach(() => { - const [variable] = mockData.mockVariables; - const invalidKeyVariable = { - ...variable, - key: AWS_ACCESS_KEY_ID, - value: 'AKIAIOSFODNN7EXAMPLEjdhy', - secret_value: 'AKIAIOSFODNN7EXAMPLEjdhy', - }; - createComponent(mount); - store.state.variable = invalidKeyVariable; - }); - - it('shows AWS guidance tip', () => { - const tip = wrapper.find(`[data-testid='aws-guidance-tip']`); - expect(tip.exists()).toBe(true); - expect(tip.isVisible()).toBe(true); - }); - }); - - describe.each` - value | secret | rendered - ${'value'} | ${'secret_value'} | ${false} - ${'dollar$ign'} | ${'dollar$ign'} | ${true} - `('Adding a new variable', ({ value, secret, rendered }) => { - beforeEach(() => { - const [variable] = mockData.mockVariables; - const invalidKeyVariable = { - ...variable, - key: 'key', - value, - secret_value: secret, - }; - createComponent(mount); - store.state.variable = invalidKeyVariable; - trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn); - }); - - it(`${rendered ? 'renders' : 'does not render'} the variable reference warning`, () => { - const warning = wrapper.find(`[data-testid='contains-variable-reference']`); - expect(warning.exists()).toBe(rendered); - }); - }); - - describe('Editing a variable', () => { - beforeEach(() => { - const [variable] = mockData.mockVariables; - createComponent(shallowMount); - jest.spyOn(store, 'dispatch').mockImplementation(); - store.state.variableBeingEdited = variable; - }); - - it('button text is Update variable when updating', () => { - expect(findAddorUpdateButton().text()).toBe('Update variable'); - }); - - it('Update variable button dispatches updateVariable with correct variable', () => { - findAddorUpdateButton().vm.$emit('click'); - expect(store.dispatch).toHaveBeenCalledWith('updateVariable'); - }); - - it('Resets the editing state once modal is hidden', () => { - findModal().vm.$emit('hidden'); - expect(store.dispatch).toHaveBeenCalledWith('resetEditing'); - }); - - it('dispatches deleteVariable with correct variable to delete', () => { - deleteVariableButton().vm.$emit('click'); - expect(store.dispatch).toHaveBeenCalledWith('deleteVariable'); - }); - }); - - describe('Environment scope', () => { - describe('group level variables', () => { - it('renders the environment dropdown', () => { - createComponent(shallowMount, { - isGroup: true, - provide: { - glFeatures: { - groupScopedCiVariables: true, - }, - }, - }); - - expect(findCiEnvironmentsDropdown().exists()).toBe(true); - expect(findCiEnvironmentsDropdown().isVisible()).toBe(true); - }); - - describe('licensed feature is not available', () => { - it('disables the dropdown', () => { - createComponent(mount, { - isGroup: true, - provide: { - glFeatures: { - groupScopedCiVariables: false, - }, - }, - }); - - const environmentScopeInput = wrapper - .find('[data-testid="environment-scope"]') - .findComponent(GlFormInput); - expect(findCiEnvironmentsDropdown().exists()).toBe(false); - expect(environmentScopeInput.attributes('readonly')).toBe('readonly'); - }); - }); - }); - - it('renders a link to documentation on scopes', () => { - createComponent(mount); - - const link = wrapper.find('[data-testid="environment-scope-link"]'); - - expect(link.attributes('title')).toBe(ENVIRONMENT_SCOPE_LINK_TITLE); - expect(link.attributes('href')).toBe('/help/environments'); - }); - }); - - describe('Validations', () => { - const maskError = 'This variable can not be masked.'; - - describe('when the mask state is invalid', () => { - beforeEach(() => { - const [variable] = mockData.mockVariables; - const invalidMaskVariable = { - ...variable, - key: 'qs', - value: 'd:;', - secret_value: 'd:;', - masked: true, - }; - createComponent(mount); - store.state.variable = invalidMaskVariable; - trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn); - }); - - it('disables the submit button', () => { - expect(findAddorUpdateButton().attributes('disabled')).toBe('disabled'); - }); - - it('shows the correct error text', () => { - expect(findModal().text()).toContain(maskError); - }); - - it('sends the correct tracking event', () => { - expect(trackingSpy).toHaveBeenCalledWith(undefined, EVENT_ACTION, { - label: EVENT_LABEL, - property: ';', - }); - }); - }); - - describe.each` - value | secret | masked | eventSent | trackingErrorProperty - ${'value'} | ${'secretValue'} | ${false} | ${0} | ${null} - ${'shortMasked'} | ${'short'} | ${true} | ${0} | ${null} - ${'withDollar$Sign'} | ${'dollar$ign'} | ${false} | ${1} | ${'$'} - ${'withDollar$Sign'} | ${'dollar$ign'} | ${true} | ${1} | ${'$'} - ${'unsupported'} | ${'unsupported|char'} | ${true} | ${1} | ${'|'} - ${'unsupportedMasked'} | ${'unsupported|char'} | ${false} | ${0} | ${null} - `('Adding a new variable', ({ value, secret, masked, eventSent, trackingErrorProperty }) => { - beforeEach(() => { - const [variable] = mockData.mockVariables; - const invalidKeyVariable = { - ...variable, - key: 'key', - value, - secret_value: secret, - masked, - }; - createComponent(mount); - store.state.variable = invalidKeyVariable; - trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn); - }); - - it(`${ - eventSent > 0 ? 'sends the correct' : 'does not send the' - } variable validation tracking event`, () => { - expect(trackingSpy).toHaveBeenCalledTimes(eventSent); - - if (eventSent > 0) { - expect(trackingSpy).toHaveBeenCalledWith(undefined, EVENT_ACTION, { - label: EVENT_LABEL, - property: trackingErrorProperty, - }); - } - }); - }); - - describe('when both states are valid', () => { - beforeEach(() => { - const [variable] = mockData.mockVariables; - const validMaskandKeyVariable = { - ...variable, - key: AWS_ACCESS_KEY_ID, - value: '12345678', - secret_value: '87654321', - masked: true, - }; - createComponent(mount); - store.state.variable = validMaskandKeyVariable; - }); - - it('does not disable the submit button', () => { - expect(findAddorUpdateButton().attributes('disabled')).toBeUndefined(); - }); - }); - }); -}); diff --git a/spec/frontend/ci_variable_list/components/legacy_ci_variable_settings_spec.js b/spec/frontend/ci_variable_list/components/legacy_ci_variable_settings_spec.js deleted file mode 100644 index 7def4dd4f29..00000000000 --- a/spec/frontend/ci_variable_list/components/legacy_ci_variable_settings_spec.js +++ /dev/null @@ -1,38 +0,0 @@ -import { shallowMount } from '@vue/test-utils'; -import Vue from 'vue'; -import Vuex from 'vuex'; -import LegacyCiVariableSettings from '~/ci_variable_list/components/legacy_ci_variable_settings.vue'; -import createStore from '~/ci_variable_list/store'; - -Vue.use(Vuex); - -describe('Ci variable table', () => { - let wrapper; - let store; - let isProject; - - const createComponent = (projectState) => { - store = createStore(); - store.state.isProject = projectState; - jest.spyOn(store, 'dispatch').mockImplementation(); - wrapper = shallowMount(LegacyCiVariableSettings, { - store, - }); - }; - - afterEach(() => { - wrapper.destroy(); - }); - - it('dispatches fetchEnvironments when mounted', () => { - isProject = true; - createComponent(isProject); - expect(store.dispatch).toHaveBeenCalledWith('fetchEnvironments'); - }); - - it('does not dispatch fetchenvironments when in group context', () => { - isProject = false; - createComponent(isProject); - expect(store.dispatch).not.toHaveBeenCalled(); - }); -}); diff --git a/spec/frontend/ci_variable_list/components/legacy_ci_variable_table_spec.js b/spec/frontend/ci_variable_list/components/legacy_ci_variable_table_spec.js deleted file mode 100644 index 310afc8003a..00000000000 --- a/spec/frontend/ci_variable_list/components/legacy_ci_variable_table_spec.js +++ /dev/null @@ -1,86 +0,0 @@ -import Vue from 'vue'; -import Vuex from 'vuex'; -import { mountExtended } from 'helpers/vue_test_utils_helper'; -import LegacyCiVariableTable from '~/ci_variable_list/components/legacy_ci_variable_table.vue'; -import createStore from '~/ci_variable_list/store'; -import mockData from '../services/mock_data'; - -Vue.use(Vuex); - -describe('Ci variable table', () => { - let wrapper; - let store; - - const createComponent = () => { - store = createStore(); - jest.spyOn(store, 'dispatch').mockImplementation(); - wrapper = mountExtended(LegacyCiVariableTable, { - attachTo: document.body, - store, - }); - }; - - const findRevealButton = () => wrapper.findByText('Reveal values'); - const findEditButton = () => wrapper.findByLabelText('Edit'); - const findEmptyVariablesPlaceholder = () => wrapper.findByText('There are no variables yet.'); - - beforeEach(() => { - createComponent(); - }); - - afterEach(() => { - wrapper.destroy(); - }); - - it('dispatches fetchVariables when mounted', () => { - expect(store.dispatch).toHaveBeenCalledWith('fetchVariables'); - }); - - describe('When table is empty', () => { - beforeEach(() => { - store.state.variables = []; - }); - - it('displays empty message', () => { - expect(findEmptyVariablesPlaceholder().exists()).toBe(true); - }); - - it('hides the reveal button', () => { - expect(findRevealButton().exists()).toBe(false); - }); - }); - - describe('When table has variables', () => { - beforeEach(() => { - store.state.variables = mockData.mockVariables; - }); - - it('does not display the empty message', () => { - expect(findEmptyVariablesPlaceholder().exists()).toBe(false); - }); - - it('displays the reveal button', () => { - expect(findRevealButton().exists()).toBe(true); - }); - - it('displays the correct amount of variables', async () => { - expect(wrapper.findAll('.js-ci-variable-row')).toHaveLength(1); - }); - }); - - describe('Table click actions', () => { - beforeEach(() => { - store.state.variables = mockData.mockVariables; - }); - - it('reveals secret values when button is clicked', () => { - findRevealButton().trigger('click'); - expect(store.dispatch).toHaveBeenCalledWith('toggleValues', false); - }); - - it('dispatches editVariable with correct variable to edit', () => { - findEditButton().trigger('click'); - expect(store.dispatch).toHaveBeenCalledWith('editVariable', mockData.mockVariables[0]); - }); - }); -}); diff --git a/spec/frontend/ci_variable_list/store/actions_spec.js b/spec/frontend/ci_variable_list/store/actions_spec.js deleted file mode 100644 index e8c81a53a55..00000000000 --- a/spec/frontend/ci_variable_list/store/actions_spec.js +++ /dev/null @@ -1,319 +0,0 @@ -import MockAdapter from 'axios-mock-adapter'; -import testAction from 'helpers/vuex_action_helper'; -import Api from '~/api'; -import * as actions from '~/ci_variable_list/store/actions'; -import * as types from '~/ci_variable_list/store/mutation_types'; -import getInitialState from '~/ci_variable_list/store/state'; -import { prepareDataForDisplay, prepareEnvironments } from '~/ci_variable_list/store/utils'; -import { createAlert } from '~/flash'; -import axios from '~/lib/utils/axios_utils'; -import mockData from '../services/mock_data'; - -jest.mock('~/api.js'); -jest.mock('~/flash.js'); - -describe('CI variable list store actions', () => { - let mock; - let state; - const mockVariable = { - environment_scope: '*', - id: 63, - key: 'test_var', - masked: false, - protected: false, - value: 'test_val', - variable_type: 'env_var', - _destory: true, - }; - const payloadError = new Error('Request failed with status code 500'); - - beforeEach(() => { - mock = new MockAdapter(axios); - state = getInitialState(); - state.endpoint = '/variables'; - }); - - afterEach(() => { - mock.restore(); - }); - - describe('toggleValues', () => { - const valuesHidden = false; - it('commits TOGGLE_VALUES mutation', () => { - testAction(actions.toggleValues, valuesHidden, {}, [ - { - type: types.TOGGLE_VALUES, - payload: valuesHidden, - }, - ]); - }); - }); - - describe('clearModal', () => { - it('commits CLEAR_MODAL mutation', () => { - testAction(actions.clearModal, {}, {}, [ - { - type: types.CLEAR_MODAL, - }, - ]); - }); - }); - - describe('resetEditing', () => { - it('commits RESET_EDITING mutation', () => { - testAction( - actions.resetEditing, - {}, - {}, - [ - { - type: types.RESET_EDITING, - }, - ], - [{ type: 'fetchVariables' }], - ); - }); - }); - - describe('setVariableProtected', () => { - it('commits SET_VARIABLE_PROTECTED mutation', () => { - testAction(actions.setVariableProtected, {}, {}, [ - { - type: types.SET_VARIABLE_PROTECTED, - }, - ]); - }); - }); - - describe('deleteVariable', () => { - it('dispatch correct actions on successful deleted variable', () => { - mock.onPatch(state.endpoint).reply(200); - - return testAction( - actions.deleteVariable, - {}, - state, - [], - [ - { type: 'requestDeleteVariable' }, - { type: 'receiveDeleteVariableSuccess' }, - { type: 'fetchVariables' }, - ], - ); - }); - - it('should show flash error and set error in state on delete failure', async () => { - mock.onPatch(state.endpoint).reply(500, ''); - - await testAction( - actions.deleteVariable, - {}, - state, - [], - [ - { type: 'requestDeleteVariable' }, - { - type: 'receiveDeleteVariableError', - payload: payloadError, - }, - ], - ); - expect(createAlert).toHaveBeenCalled(); - }); - }); - - describe('updateVariable', () => { - it('dispatch correct actions on successful updated variable', () => { - mock.onPatch(state.endpoint).reply(200); - - return testAction( - actions.updateVariable, - {}, - state, - [], - [ - { type: 'requestUpdateVariable' }, - { type: 'receiveUpdateVariableSuccess' }, - { type: 'fetchVariables' }, - ], - ); - }); - - it('should show flash error and set error in state on update failure', async () => { - mock.onPatch(state.endpoint).reply(500, ''); - - await testAction( - actions.updateVariable, - mockVariable, - state, - [], - [ - { type: 'requestUpdateVariable' }, - { - type: 'receiveUpdateVariableError', - payload: payloadError, - }, - ], - ); - expect(createAlert).toHaveBeenCalled(); - }); - }); - - describe('addVariable', () => { - it('dispatch correct actions on successful added variable', () => { - mock.onPatch(state.endpoint).reply(200); - - return testAction( - actions.addVariable, - {}, - state, - [], - [ - { type: 'requestAddVariable' }, - { type: 'receiveAddVariableSuccess' }, - { type: 'fetchVariables' }, - ], - ); - }); - - it('should show flash error and set error in state on add failure', async () => { - mock.onPatch(state.endpoint).reply(500, ''); - - await testAction( - actions.addVariable, - {}, - state, - [], - [ - { type: 'requestAddVariable' }, - { - type: 'receiveAddVariableError', - payload: payloadError, - }, - ], - ); - expect(createAlert).toHaveBeenCalled(); - }); - }); - - describe('fetchVariables', () => { - it('dispatch correct actions on fetchVariables', () => { - mock.onGet(state.endpoint).reply(200, { variables: mockData.mockVariables }); - - return testAction( - actions.fetchVariables, - {}, - state, - [], - [ - { type: 'requestVariables' }, - { - type: 'receiveVariablesSuccess', - payload: prepareDataForDisplay(mockData.mockVariables), - }, - ], - ); - }); - - it('should show flash error and set error in state on fetch variables failure', async () => { - mock.onGet(state.endpoint).reply(500); - - await testAction(actions.fetchVariables, {}, state, [], [{ type: 'requestVariables' }]); - expect(createAlert).toHaveBeenCalledWith({ - message: 'There was an error fetching the variables.', - }); - }); - }); - - describe('fetchEnvironments', () => { - it('dispatch correct actions on fetchEnvironments', () => { - Api.environments = jest.fn().mockResolvedValue({ data: mockData.mockEnvironments }); - - return testAction( - actions.fetchEnvironments, - {}, - state, - [], - [ - { type: 'requestEnvironments' }, - { - type: 'receiveEnvironmentsSuccess', - payload: prepareEnvironments(mockData.mockEnvironments), - }, - ], - ); - }); - - it('should show flash error and set error in state on fetch environments failure', async () => { - Api.environments = jest.fn().mockRejectedValue(); - - await testAction(actions.fetchEnvironments, {}, state, [], [{ type: 'requestEnvironments' }]); - - expect(createAlert).toHaveBeenCalledWith({ - message: 'There was an error fetching the environments information.', - }); - }); - }); - - describe('Update variable values', () => { - it('updateVariableKey', () => { - testAction( - actions.updateVariableKey, - { key: mockVariable.key }, - {}, - [ - { - type: types.UPDATE_VARIABLE_KEY, - payload: mockVariable.key, - }, - ], - [], - ); - }); - - it('updateVariableValue', () => { - testAction( - actions.updateVariableValue, - { secret_value: mockVariable.value }, - {}, - [ - { - type: types.UPDATE_VARIABLE_VALUE, - payload: mockVariable.value, - }, - ], - [], - ); - }); - - it('updateVariableType', () => { - testAction( - actions.updateVariableType, - { variable_type: mockVariable.variable_type }, - {}, - [{ type: types.UPDATE_VARIABLE_TYPE, payload: mockVariable.variable_type }], - [], - ); - }); - - it('updateVariableProtected', () => { - testAction( - actions.updateVariableProtected, - { protected_variable: mockVariable.protected }, - {}, - [{ type: types.UPDATE_VARIABLE_PROTECTED, payload: mockVariable.protected }], - [], - ); - }); - - it('updateVariableMasked', () => { - testAction( - actions.updateVariableMasked, - { masked: mockVariable.masked }, - {}, - [{ type: types.UPDATE_VARIABLE_MASKED, payload: mockVariable.masked }], - [], - ); - }); - }); -}); diff --git a/spec/frontend/ci_variable_list/store/getters_spec.js b/spec/frontend/ci_variable_list/store/getters_spec.js deleted file mode 100644 index 92f22b18763..00000000000 --- a/spec/frontend/ci_variable_list/store/getters_spec.js +++ /dev/null @@ -1,21 +0,0 @@ -import * as getters from '~/ci_variable_list/store/getters'; -import mockData from '../services/mock_data'; - -describe('Ci variable getters', () => { - describe('joinedEnvironments', () => { - it('should join fetched environments with variable environment scopes', () => { - const state = { - environments: ['All (default)', 'staging', 'deployment', 'prod'], - variables: mockData.mockVariableScopes, - }; - - expect(getters.joinedEnvironments(state)).toEqual([ - 'All (default)', - 'deployment', - 'prod', - 'production', - 'staging', - ]); - }); - }); -}); diff --git a/spec/frontend/ci_variable_list/store/mutations_spec.js b/spec/frontend/ci_variable_list/store/mutations_spec.js deleted file mode 100644 index c7d07ead09b..00000000000 --- a/spec/frontend/ci_variable_list/store/mutations_spec.js +++ /dev/null @@ -1,136 +0,0 @@ -import * as types from '~/ci_variable_list/store/mutation_types'; -import mutations from '~/ci_variable_list/store/mutations'; -import state from '~/ci_variable_list/store/state'; - -describe('CI variable list mutations', () => { - let stateCopy; - - beforeEach(() => { - stateCopy = state(); - }); - - describe('TOGGLE_VALUES', () => { - it('should toggle state', () => { - const valuesHidden = false; - - mutations[types.TOGGLE_VALUES](stateCopy, valuesHidden); - - expect(stateCopy.valuesHidden).toEqual(valuesHidden); - }); - }); - - describe('VARIABLE_BEING_EDITED', () => { - it('should set the variable that is being edited', () => { - mutations[types.VARIABLE_BEING_EDITED](stateCopy); - - expect(stateCopy.variableBeingEdited).toBe(true); - }); - }); - - describe('RESET_EDITING', () => { - it('should reset variableBeingEdited to false', () => { - mutations[types.RESET_EDITING](stateCopy); - - expect(stateCopy.variableBeingEdited).toBe(false); - }); - }); - - describe('CLEAR_MODAL', () => { - it('should clear modal state', () => { - const modalState = { - variable_type: 'Variable', - key: '', - secret_value: '', - protected_variable: false, - masked: false, - environment_scope: 'All (default)', - }; - - mutations[types.CLEAR_MODAL](stateCopy); - - expect(stateCopy.variable).toEqual(modalState); - }); - }); - - describe('RECEIVE_ENVIRONMENTS_SUCCESS', () => { - it('should set environments', () => { - const environments = ['env1', 'env2']; - - mutations[types.RECEIVE_ENVIRONMENTS_SUCCESS](stateCopy, environments); - - expect(stateCopy.environments).toEqual(['All (default)', 'env1', 'env2']); - }); - }); - - describe('SET_ENVIRONMENT_SCOPE', () => { - const environment = 'production'; - - it('should set environment scope on variable', () => { - mutations[types.SET_ENVIRONMENT_SCOPE](stateCopy, environment); - - expect(stateCopy.variable.environment_scope).toBe('production'); - }); - }); - - describe('ADD_WILD_CARD_SCOPE', () => { - it('should add wild card scope to environments array and sort', () => { - stateCopy.environments = ['dev', 'staging']; - mutations[types.ADD_WILD_CARD_SCOPE](stateCopy, 'production'); - - expect(stateCopy.environments).toEqual(['dev', 'production', 'staging']); - }); - }); - - describe('SET_VARIABLE_PROTECTED', () => { - it('should set protected value to true', () => { - mutations[types.SET_VARIABLE_PROTECTED](stateCopy); - - expect(stateCopy.variable.protected_variable).toBe(true); - }); - }); - - describe('UPDATE_VARIABLE_KEY', () => { - it('should update variable key value', () => { - const key = 'new_var'; - mutations[types.UPDATE_VARIABLE_KEY](stateCopy, key); - - expect(stateCopy.variable.key).toBe(key); - }); - }); - - describe('UPDATE_VARIABLE_VALUE', () => { - it('should update variable value', () => { - const value = 'variable_value'; - mutations[types.UPDATE_VARIABLE_VALUE](stateCopy, value); - - expect(stateCopy.variable.secret_value).toBe(value); - }); - }); - - describe('UPDATE_VARIABLE_TYPE', () => { - it('should update variable type value', () => { - const type = 'File'; - mutations[types.UPDATE_VARIABLE_TYPE](stateCopy, type); - - expect(stateCopy.variable.variable_type).toBe(type); - }); - }); - - describe('UPDATE_VARIABLE_PROTECTED', () => { - it('should update variable protected value', () => { - const protectedValue = true; - mutations[types.UPDATE_VARIABLE_PROTECTED](stateCopy, protectedValue); - - expect(stateCopy.variable.protected_variable).toBe(protectedValue); - }); - }); - - describe('UPDATE_VARIABLE_MASKED', () => { - it('should update variable masked value', () => { - const masked = true; - mutations[types.UPDATE_VARIABLE_MASKED](stateCopy, masked); - - expect(stateCopy.variable.masked).toBe(masked); - }); - }); -}); diff --git a/spec/frontend/ci_variable_list/store/utils_spec.js b/spec/frontend/ci_variable_list/store/utils_spec.js deleted file mode 100644 index 5b10370324a..00000000000 --- a/spec/frontend/ci_variable_list/store/utils_spec.js +++ /dev/null @@ -1,49 +0,0 @@ -import { - prepareDataForDisplay, - prepareEnvironments, - prepareDataForApi, -} from '~/ci_variable_list/store/utils'; -import mockData from '../services/mock_data'; - -describe('CI variables store utils', () => { - it('prepares ci variables for display', () => { - expect(prepareDataForDisplay(mockData.mockVariablesApi)).toStrictEqual( - mockData.mockVariablesDisplay, - ); - }); - - it('prepares single ci variable for api', () => { - expect(prepareDataForApi(mockData.mockVariablesDisplay[0])).toStrictEqual({ - environment_scope: '*', - id: 113, - key: 'test_var', - masked: 'false', - protected: 'false', - secret_value: 'test_val', - value: 'test_val', - variable_type: 'env_var', - }); - - expect(prepareDataForApi(mockData.mockVariablesDisplay[1])).toStrictEqual({ - environment_scope: '*', - id: 114, - key: 'test_var_2', - masked: 'false', - protected: 'false', - secret_value: 'test_val_2', - value: 'test_val_2', - variable_type: 'file', - }); - }); - - it('prepares single ci variable for delete', () => { - expect(prepareDataForApi(mockData.mockVariablesDisplay[0], true)).toHaveProperty( - '_destroy', - true, - ); - }); - - it('prepares environments for display', () => { - expect(prepareEnvironments(mockData.mockEnvironments)).toStrictEqual(['staging', 'production']); - }); -}); diff --git a/spec/frontend/packages_and_registries/settings/group/components/forwarding_settings_spec.js b/spec/frontend/packages_and_registries/settings/group/components/forwarding_settings_spec.js new file mode 100644 index 00000000000..8f229182fe5 --- /dev/null +++ b/spec/frontend/packages_and_registries/settings/group/components/forwarding_settings_spec.js @@ -0,0 +1,78 @@ +import { GlFormGroup, GlSprintf } from '@gitlab/ui'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import component from '~/packages_and_registries/settings/group/components/forwarding_settings.vue'; + +describe('Forwarding Settings', () => { + let wrapper; + + const defaultProps = { + disabled: false, + forwarding: false, + label: 'label', + lockForwarding: false, + modelNames: { + forwarding: 'forwardField', + lockForwarding: 'lockForwardingField', + isLocked: 'lockedField', + }, + }; + + const mountComponent = (propsData = defaultProps) => { + wrapper = shallowMountExtended(component, { + propsData, + stubs: { + GlSprintf, + }, + }); + }; + + const findFormGroup = () => wrapper.findComponent(GlFormGroup); + const findForwardingCheckbox = () => wrapper.findByTestId('forwarding-checkbox'); + const findLockForwardingCheckbox = () => wrapper.findByTestId('lock-forwarding-checkbox'); + + it('has a form group', () => { + mountComponent(); + + expect(findFormGroup().exists()).toBe(true); + expect(findFormGroup().attributes()).toMatchObject({ + label: defaultProps.label, + }); + }); + + describe.each` + name | finder | label | extraProps | field + ${'forwarding'} | ${findForwardingCheckbox} | ${'Forward label package requests'} | ${{ forwarding: true }} | ${defaultProps.modelNames.forwarding} + ${'lock forwarding'} | ${findLockForwardingCheckbox} | ${'Enforce label setting for all subgroups'} | ${{ lockForwarding: true }} | ${defaultProps.modelNames.lockForwarding} + `('$name checkbox', ({ name, finder, label, extraProps, field }) => { + it('is rendered', () => { + mountComponent(); + expect(finder().exists()).toBe(true); + expect(finder().text()).toMatchInterpolatedText(label); + expect(finder().attributes('disabled')).toBeUndefined(); + expect(finder().attributes('checked')).toBeUndefined(); + }); + + it(`is checked when ${name} set`, () => { + mountComponent({ ...defaultProps, ...extraProps }); + + expect(finder().attributes('checked')).toBe('true'); + }); + + it(`emits an update event with field ${field} set`, () => { + mountComponent(); + + finder().vm.$emit('change', true); + + expect(wrapper.emitted('update')).toStrictEqual([[field, true]]); + }); + }); + + describe('disabled', () => { + it('disables both checkboxes', () => { + mountComponent({ ...defaultProps, disabled: true }); + + expect(findForwardingCheckbox().attributes('disabled')).toEqual('true'); + expect(findLockForwardingCheckbox().attributes('disabled')).toEqual('true'); + }); + }); +}); diff --git a/spec/frontend/packages_and_registries/settings/group/components/group_settings_app_spec.js b/spec/frontend/packages_and_registries/settings/group/components/group_settings_app_spec.js index 31fc3ad419c..7edc321867c 100644 --- a/spec/frontend/packages_and_registries/settings/group/components/group_settings_app_spec.js +++ b/spec/frontend/packages_and_registries/settings/group/components/group_settings_app_spec.js @@ -7,6 +7,7 @@ import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; import PackagesSettings from '~/packages_and_registries/settings/group/components/packages_settings.vue'; import DependencyProxySettings from '~/packages_and_registries/settings/group/components/dependency_proxy_settings.vue'; +import PackagesForwardingSettings from '~/packages_and_registries/settings/group/components/packages_forwarding_settings.vue'; import component from '~/packages_and_registries/settings/group/components/group_settings_app.vue'; @@ -60,6 +61,7 @@ describe('Group Settings App', () => { const findAlert = () => wrapper.findComponent(GlAlert); const findPackageSettings = () => wrapper.findComponent(PackagesSettings); + const findPackageForwardingSettings = () => wrapper.findComponent(PackagesForwardingSettings); const findDependencyProxySettings = () => wrapper.findComponent(DependencyProxySettings); const waitForApolloQueryAndRender = async () => { @@ -67,16 +69,18 @@ describe('Group Settings App', () => { await nextTick(); }; - const packageSettingsProps = { packageSettings: packageSettings() }; + const packageSettingsProps = { packageSettings }; + const packageForwardingSettingsProps = { forwardSettings: { ...packageSettings } }; const dependencyProxyProps = { dependencyProxySettings: dependencyProxySettings(), dependencyProxyImageTtlPolicy: dependencyProxyImageTtlPolicy(), }; describe.each` - finder | entitySpecificProps | successMessage | errorMessage - ${findPackageSettings} | ${packageSettingsProps} | ${'Settings saved successfully'} | ${'An error occurred while saving the settings'} - ${findDependencyProxySettings} | ${dependencyProxyProps} | ${'Setting saved successfully'} | ${'An error occurred while saving the setting'} + finder | entitySpecificProps | successMessage | errorMessage + ${findPackageSettings} | ${packageSettingsProps} | ${'Settings saved successfully'} | ${'An error occurred while saving the settings'} + ${findPackageForwardingSettings} | ${packageForwardingSettingsProps} | ${'Settings saved successfully'} | ${'An error occurred while saving the settings'} + ${findDependencyProxySettings} | ${dependencyProxyProps} | ${'Setting saved successfully'} | ${'An error occurred while saving the setting'} `('settings blocks', ({ finder, entitySpecificProps, successMessage, errorMessage }) => { beforeEach(() => { mountComponent(); @@ -88,10 +92,7 @@ describe('Group Settings App', () => { }); it('binds the correctProps', () => { - expect(finder().props()).toMatchObject({ - isLoading: false, - ...entitySpecificProps, - }); + expect(finder().props()).toMatchObject(entitySpecificProps); }); describe('success event', () => { diff --git a/spec/frontend/packages_and_registries/settings/group/components/package_settings_spec.js b/spec/frontend/packages_and_registries/settings/group/components/package_settings_spec.js index 13eba39ec8c..807f332f4d3 100644 --- a/spec/frontend/packages_and_registries/settings/group/components/package_settings_spec.js +++ b/spec/frontend/packages_and_registries/settings/group/components/package_settings_spec.js @@ -48,7 +48,7 @@ describe('Packages Settings', () => { apolloProvider, provide: defaultProvide, propsData: { - packageSettings: packageSettings(), + packageSettings, }, stubs: { SettingsBlock, @@ -83,7 +83,7 @@ describe('Packages Settings', () => { }; const emitMavenSettingsUpdate = (override) => { - findGenericDuplicatedSettingsExceptionsInput().vm.$emit('update', { + findMavenDuplicatedSettingsExceptionsInput().vm.$emit('update', { mavenDuplicateExceptionRegex: ')', ...override, }); @@ -117,7 +117,7 @@ describe('Packages Settings', () => { it('renders toggle', () => { mountComponent({ mountFn: mountExtended }); - const { mavenDuplicatesAllowed } = packageSettings(); + const { mavenDuplicatesAllowed } = packageSettings; expect(findMavenDuplicatedSettingsToggle().exists()).toBe(true); @@ -132,7 +132,7 @@ describe('Packages Settings', () => { it('renders ExceptionsInput and assigns duplication allowness and exception props', () => { mountComponent({ mountFn: mountExtended }); - const { mavenDuplicatesAllowed, mavenDuplicateExceptionRegex } = packageSettings(); + const { mavenDuplicatesAllowed, mavenDuplicateExceptionRegex } = packageSettings; expect(findMavenDuplicatedSettingsExceptionsInput().exists()).toBe(true); @@ -170,7 +170,7 @@ describe('Packages Settings', () => { it('renders toggle', () => { mountComponent({ mountFn: mountExtended }); - const { genericDuplicatesAllowed } = packageSettings(); + const { genericDuplicatesAllowed } = packageSettings; expect(findGenericDuplicatedSettingsToggle().exists()).toBe(true); expect(findGenericDuplicatedSettingsToggle().props()).toMatchObject({ @@ -184,7 +184,7 @@ describe('Packages Settings', () => { it('renders ExceptionsInput and assigns duplication allowness and exception props', async () => { mountComponent({ mountFn: mountExtended }); - const { genericDuplicatesAllowed, genericDuplicateExceptionRegex } = packageSettings(); + const { genericDuplicatesAllowed, genericDuplicateExceptionRegex } = packageSettings; expect(findGenericDuplicatedSettingsExceptionsInput().props()).toMatchObject({ duplicatesAllowed: genericDuplicatesAllowed, @@ -239,7 +239,7 @@ describe('Packages Settings', () => { emitMavenSettingsUpdate({ mavenDuplicateExceptionRegex }); expect(updateGroupPackagesSettingsOptimisticResponse).toHaveBeenCalledWith({ - ...packageSettings(), + ...packageSettings, mavenDuplicateExceptionRegex, }); }); diff --git a/spec/frontend/packages_and_registries/settings/group/components/packages_forwarding_settings_spec.js b/spec/frontend/packages_and_registries/settings/group/components/packages_forwarding_settings_spec.js new file mode 100644 index 00000000000..a0b257a9496 --- /dev/null +++ b/spec/frontend/packages_and_registries/settings/group/components/packages_forwarding_settings_spec.js @@ -0,0 +1,280 @@ +import Vue from 'vue'; +import { GlButton } from '@gitlab/ui'; +import VueApollo from 'vue-apollo'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import waitForPromises from 'helpers/wait_for_promises'; +import component from '~/packages_and_registries/settings/group/components/packages_forwarding_settings.vue'; +import { + PACKAGE_FORWARDING_SETTINGS_DESCRIPTION, + PACKAGE_FORWARDING_SETTINGS_HEADER, +} from '~/packages_and_registries/settings/group/constants'; + +import updateNamespacePackageSettings from '~/packages_and_registries/settings/group/graphql/mutations/update_group_packages_settings.mutation.graphql'; +import getGroupPackagesSettingsQuery from '~/packages_and_registries/settings/group/graphql/queries/get_group_packages_settings.query.graphql'; +import SettingsBlock from '~/vue_shared/components/settings/settings_block.vue'; +import { updateGroupPackagesSettingsOptimisticResponse } from '~/packages_and_registries/settings/group/graphql/utils/optimistic_responses'; +import { + packageSettings, + packageForwardingSettings, + groupPackageSettingsMock, + groupPackageForwardSettingsMutationMock, + mutationErrorMock, + npmProps, + pypiProps, + mavenProps, +} from '../mock_data'; + +jest.mock('~/flash'); +jest.mock('~/packages_and_registries/settings/group/graphql/utils/optimistic_responses'); + +describe('Packages Forwarding Settings', () => { + let wrapper; + let apolloProvider; + const mutationResolverFn = jest.fn().mockResolvedValue(groupPackageForwardSettingsMutationMock()); + + const defaultProvide = { + groupPath: 'foo_group_path', + }; + + const mountComponent = ({ + forwardSettings = { ...packageSettings }, + features = {}, + mutationResolver = mutationResolverFn, + } = {}) => { + Vue.use(VueApollo); + + const requestHandlers = [[updateNamespacePackageSettings, mutationResolver]]; + + apolloProvider = createMockApollo(requestHandlers); + + wrapper = shallowMountExtended(component, { + apolloProvider, + provide: { + ...defaultProvide, + glFeatures: { + ...features, + }, + }, + propsData: { + forwardSettings, + }, + stubs: { + SettingsBlock, + }, + }); + }; + + const findSettingsBlock = () => wrapper.findComponent(SettingsBlock); + const findForm = () => wrapper.find('form'); + const findSubmitButton = () => findForm().findComponent(GlButton); + const findDescription = () => wrapper.findByTestId('description'); + const findMavenForwardingSettings = () => wrapper.findByTestId('maven'); + const findNpmForwardingSettings = () => wrapper.findByTestId('npm'); + const findPyPiForwardingSettings = () => wrapper.findByTestId('pypi'); + + const fillApolloCache = () => { + apolloProvider.defaultClient.cache.writeQuery({ + query: getGroupPackagesSettingsQuery, + variables: { + fullPath: defaultProvide.groupPath, + }, + ...groupPackageSettingsMock, + }); + }; + + const updateNpmSettings = () => { + findNpmForwardingSettings().vm.$emit('update', 'npmPackageRequestsForwarding', false); + }; + + const submitForm = () => { + findForm().trigger('submit'); + return waitForPromises(); + }; + + afterEach(() => { + apolloProvider = null; + }); + + it('renders a settings block', () => { + mountComponent(); + + expect(findSettingsBlock().exists()).toBe(true); + }); + + it('has the correct header text', () => { + mountComponent(); + + expect(wrapper.text()).toContain(PACKAGE_FORWARDING_SETTINGS_HEADER); + }); + + it('has the correct description text', () => { + mountComponent(); + + expect(findDescription().text()).toMatchInterpolatedText( + PACKAGE_FORWARDING_SETTINGS_DESCRIPTION, + ); + }); + + it('watches changes to props', async () => { + mountComponent(); + + expect(findNpmForwardingSettings().props()).toMatchObject(npmProps); + + await wrapper.setProps({ + forwardSettings: { + ...packageSettings, + npmPackageRequestsForwardingLocked: true, + }, + }); + + expect(findNpmForwardingSettings().props()).toMatchObject({ ...npmProps, disabled: true }); + }); + + it('submit button is disabled', () => { + mountComponent(); + + expect(findSubmitButton().props('disabled')).toBe(true); + }); + + describe.each` + type | finder | props | field + ${'npm'} | ${findNpmForwardingSettings} | ${npmProps} | ${'npmPackageRequestsForwarding'} + ${'pypi'} | ${findPyPiForwardingSettings} | ${pypiProps} | ${'pypiPackageRequestsForwarding'} + ${'maven'} | ${findMavenForwardingSettings} | ${mavenProps} | ${'mavenPackageRequestsForwarding'} + `('$type settings', ({ finder, props, field }) => { + beforeEach(() => { + mountComponent({ features: { mavenCentralRequestForwarding: true } }); + }); + + it('assigns forwarding settings props', () => { + expect(finder().props()).toMatchObject(props); + }); + + it('on update event enables submit button', async () => { + finder().vm.$emit('update', field, false); + + await waitForPromises(); + + expect(findSubmitButton().props('disabled')).toBe(false); + }); + }); + + describe('maven settings', () => { + describe('with feature turned off', () => { + it('does not exist', () => { + mountComponent(); + + expect(findMavenForwardingSettings().exists()).toBe(false); + }); + }); + }); + + describe('settings update', () => { + describe('success state', () => { + it('calls the mutation with the right variables', async () => { + const { + mavenPackageRequestsForwardingLocked, + npmPackageRequestsForwardingLocked, + pypiPackageRequestsForwardingLocked, + ...packageSettingsInput + } = packageForwardingSettings; + + mountComponent(); + + fillApolloCache(); + updateNpmSettings(); + + await submitForm(); + + expect(mutationResolverFn).toHaveBeenCalledWith({ + input: { + namespacePath: defaultProvide.groupPath, + ...packageSettingsInput, + npmPackageRequestsForwarding: false, + }, + }); + }); + + it('when field are locked calls the mutation with the right variables', async () => { + mountComponent({ + forwardSettings: { + ...packageSettings, + mavenPackageRequestsForwardingLocked: true, + pypiPackageRequestsForwardingLocked: true, + }, + }); + + fillApolloCache(); + updateNpmSettings(); + + await submitForm(); + + expect(mutationResolverFn).toHaveBeenCalledWith({ + input: { + namespacePath: defaultProvide.groupPath, + lockNpmPackageRequestsForwarding: false, + npmPackageRequestsForwarding: false, + }, + }); + }); + + it('emits a success event', async () => { + mountComponent(); + fillApolloCache(); + updateNpmSettings(); + + await submitForm(); + + expect(wrapper.emitted('success')).toHaveLength(1); + }); + + it('has an optimistic response', async () => { + const npmPackageRequestsForwarding = false; + mountComponent(); + + fillApolloCache(); + + expect(findNpmForwardingSettings().props('forwarding')).toBe(true); + + updateNpmSettings(); + await submitForm(); + + expect(updateGroupPackagesSettingsOptimisticResponse).toHaveBeenCalledWith({ + ...packageSettings, + npmPackageRequestsForwarding, + }); + expect(findNpmForwardingSettings().props('forwarding')).toBe(npmPackageRequestsForwarding); + }); + }); + + describe('errors', () => { + it('mutation payload with root level errors', async () => { + const mutationResolver = jest.fn().mockResolvedValue(mutationErrorMock); + mountComponent({ mutationResolver }); + + fillApolloCache(); + + updateNpmSettings(); + await submitForm(); + + expect(wrapper.emitted('error')).toHaveLength(1); + }); + + it.each` + type | mutationResolver + ${'local'} | ${jest.fn().mockResolvedValue(groupPackageForwardSettingsMutationMock({ errors: ['foo'] }))} + ${'network'} | ${jest.fn().mockRejectedValue()} + `('mutation payload with $type error', async ({ mutationResolver }) => { + mountComponent({ mutationResolver }); + + fillApolloCache(); + + updateNpmSettings(); + await submitForm(); + + expect(wrapper.emitted('error')).toHaveLength(1); + }); + }); + }); +}); diff --git a/spec/frontend/packages_and_registries/settings/group/mock_data.js b/spec/frontend/packages_and_registries/settings/group/mock_data.js index d53446de910..1ca9dc6daeb 100644 --- a/spec/frontend/packages_and_registries/settings/group/mock_data.js +++ b/spec/frontend/packages_and_registries/settings/group/mock_data.js @@ -1,9 +1,26 @@ -export const packageSettings = () => ({ +const packageDuplicateSettings = { mavenDuplicatesAllowed: true, mavenDuplicateExceptionRegex: '', genericDuplicatesAllowed: true, genericDuplicateExceptionRegex: '', -}); +}; + +export const packageForwardingSettings = { + mavenPackageRequestsForwarding: true, + lockMavenPackageRequestsForwarding: false, + npmPackageRequestsForwarding: true, + lockNpmPackageRequestsForwarding: false, + pypiPackageRequestsForwarding: true, + lockPypiPackageRequestsForwarding: false, + mavenPackageRequestsForwardingLocked: false, + npmPackageRequestsForwardingLocked: false, + pypiPackageRequestsForwardingLocked: false, +}; + +export const packageSettings = { + ...packageDuplicateSettings, + ...packageForwardingSettings, +}; export const dependencyProxySettings = (extend) => ({ enabled: true, @@ -21,13 +38,52 @@ export const groupPackageSettingsMock = { group: { id: '1', fullPath: 'foo_group_path', - packageSettings: packageSettings(), + packageSettings: { + ...packageSettings, + __typename: 'PackageSettings', + }, dependencyProxySetting: dependencyProxySettings(), dependencyProxyImageTtlPolicy: dependencyProxyImageTtlPolicy(), }, }, }; +export const npmProps = { + forwarding: packageForwardingSettings.npmPackageRequestsForwarding, + lockForwarding: packageForwardingSettings.lockNpmPackageRequestsForwarding, + label: 'npm', + disabled: false, + modelNames: { + forwarding: 'npmPackageRequestsForwarding', + lockForwarding: 'lockNpmPackageRequestsForwarding', + isLocked: 'npmPackageRequestsForwardingLocked', + }, +}; + +export const pypiProps = { + forwarding: packageForwardingSettings.pypiPackageRequestsForwarding, + lockForwarding: packageForwardingSettings.lockPypiPackageRequestsForwarding, + label: 'PyPI', + disabled: false, + modelNames: { + forwarding: 'pypiPackageRequestsForwarding', + lockForwarding: 'lockPypiPackageRequestsForwarding', + isLocked: 'pypiPackageRequestsForwardingLocked', + }, +}; + +export const mavenProps = { + forwarding: packageForwardingSettings.mavenPackageRequestsForwarding, + lockForwarding: packageForwardingSettings.lockMavenPackageRequestsForwarding, + label: 'Maven', + disabled: false, + modelNames: { + forwarding: 'mavenPackageRequestsForwarding', + lockForwarding: 'lockMavenPackageRequestsForwarding', + isLocked: 'mavenPackageRequestsForwardingLocked', + }, +}; + export const groupPackageSettingsMutationMock = (override) => ({ data: { updateNamespacePackageSettings: { @@ -43,6 +99,19 @@ export const groupPackageSettingsMutationMock = (override) => ({ }, }); +export const groupPackageForwardSettingsMutationMock = (override) => ({ + data: { + updateNamespacePackageSettings: { + packageSettings: { + npmPackageRequestsForwarding: true, + lockNpmPackageRequestsForwarding: false, + }, + errors: [], + ...override, + }, + }, +}); + export const dependencyProxySettingMutationMock = (override) => ({ data: { updateDependencyProxySettings: { |