diff options
Diffstat (limited to 'spec/frontend/ci/ci_variable_list/components')
8 files changed, 1562 insertions, 0 deletions
diff --git a/spec/frontend/ci/ci_variable_list/components/ci_admin_variables_spec.js b/spec/frontend/ci/ci_variable_list/components/ci_admin_variables_spec.js new file mode 100644 index 00000000000..5e0c35c9f90 --- /dev/null +++ b/spec/frontend/ci/ci_variable_list/components/ci_admin_variables_spec.js @@ -0,0 +1,36 @@ +import { shallowMount } from '@vue/test-utils'; + +import ciAdminVariables from '~/ci/ci_variable_list/components/ci_admin_variables.vue'; +import ciVariableShared from '~/ci/ci_variable_list/components/ci_variable_shared.vue'; + +describe('Ci Project Variable wrapper', () => { + let wrapper; + + const findCiShared = () => wrapper.findComponent(ciVariableShared); + + const createComponent = () => { + wrapper = shallowMount(ciAdminVariables); + }; + + beforeEach(() => { + createComponent(); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('Passes down the correct props to ci_variable_shared', () => { + expect(findCiShared().props()).toEqual({ + areScopedVariablesAvailable: false, + componentName: 'InstanceVariables', + entity: '', + hideEnvironmentScope: true, + mutationData: wrapper.vm.$options.mutationData, + queryData: wrapper.vm.$options.queryData, + refetchAfterMutation: true, + fullPath: null, + id: null, + }); + }); +}); diff --git a/spec/frontend/ci/ci_variable_list/components/ci_environments_dropdown_spec.js b/spec/frontend/ci/ci_variable_list/components/ci_environments_dropdown_spec.js new file mode 100644 index 00000000000..2fd395a1230 --- /dev/null +++ b/spec/frontend/ci/ci_variable_list/components/ci_environments_dropdown_spec.js @@ -0,0 +1,118 @@ +import { GlListboxItem, GlCollapsibleListbox, GlDropdownItem, GlIcon } from '@gitlab/ui'; +import { mount } from '@vue/test-utils'; +import { allEnvironments } from '~/ci/ci_variable_list/constants'; +import CiEnvironmentsDropdown from '~/ci/ci_variable_list/components/ci_environments_dropdown.vue'; + +describe('Ci environments dropdown', () => { + let wrapper; + + const envs = ['dev', 'prod', 'staging']; + const defaultProps = { environments: envs, selectedEnvironmentScope: '' }; + + const findAllListboxItems = () => wrapper.findAllComponents(GlListboxItem); + const findListboxItemByIndex = (index) => wrapper.findAllComponents(GlListboxItem).at(index); + const findActiveIconByIndex = (index) => findListboxItemByIndex(index).findComponent(GlIcon); + const findListbox = () => wrapper.findComponent(GlCollapsibleListbox); + const findListboxText = () => findListbox().props('toggleText'); + const findCreateWildcardButton = () => wrapper.findComponent(GlDropdownItem); + + const createComponent = ({ props = {}, searchTerm = '' } = {}) => { + wrapper = mount(CiEnvironmentsDropdown, { + propsData: { + ...defaultProps, + ...props, + }, + }); + + findListbox().vm.$emit('search', searchTerm); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + describe('No environments found', () => { + beforeEach(() => { + createComponent({ searchTerm: 'stable' }); + }); + + it('renders create button with search term if environments do not contain search term', () => { + const button = findCreateWildcardButton(); + expect(button.exists()).toBe(true); + expect(button.text()).toBe('Create wildcard: stable'); + }); + }); + + describe('Search term is empty', () => { + beforeEach(() => { + createComponent({ props: { environments: envs } }); + }); + + it('renders all environments when search term is empty', () => { + expect(findListboxItemByIndex(0).text()).toBe(envs[0]); + expect(findListboxItemByIndex(1).text()).toBe(envs[1]); + expect(findListboxItemByIndex(2).text()).toBe(envs[2]); + }); + + it('does not display active checkmark on the inactive stage', () => { + expect(findActiveIconByIndex(0).classes('gl-visibility-hidden')).toBe(true); + }); + }); + + describe('when `*` is the value of selectedEnvironmentScope props', () => { + const wildcardScope = '*'; + + beforeEach(() => { + createComponent({ props: { selectedEnvironmentScope: wildcardScope } }); + }); + + it('shows the `All environments` text and not the wildcard', () => { + expect(findListboxText()).toContain(allEnvironments.text); + expect(findListboxText()).not.toContain(wildcardScope); + }); + }); + + describe('Environments found', () => { + const currentEnv = envs[2]; + + beforeEach(() => { + createComponent({ searchTerm: currentEnv }); + }); + + it('renders only the environment searched for', () => { + expect(findAllListboxItems()).toHaveLength(1); + expect(findListboxItemByIndex(0).text()).toBe(currentEnv); + }); + + it('does not display create button', () => { + expect(findCreateWildcardButton().exists()).toBe(false); + }); + + describe('Custom events', () => { + describe('when selecting an environment', () => { + const itemIndex = 0; + + beforeEach(() => { + createComponent(); + }); + + it('emits `select-environment` when an environment is clicked', () => { + findListbox().vm.$emit('select', envs[itemIndex]); + expect(wrapper.emitted('select-environment')).toEqual([[envs[itemIndex]]]); + }); + }); + + describe('when creating a new environment from a search term', () => { + const search = 'new-env'; + beforeEach(() => { + createComponent({ searchTerm: search }); + }); + + it('emits create-environment-scope', () => { + findCreateWildcardButton().vm.$emit('click'); + expect(wrapper.emitted('create-environment-scope')).toEqual([[search]]); + }); + }); + }); + }); +}); diff --git a/spec/frontend/ci/ci_variable_list/components/ci_group_variables_spec.js b/spec/frontend/ci/ci_variable_list/components/ci_group_variables_spec.js new file mode 100644 index 00000000000..3f1eebbc6a5 --- /dev/null +++ b/spec/frontend/ci/ci_variable_list/components/ci_group_variables_spec.js @@ -0,0 +1,73 @@ +import { shallowMount } from '@vue/test-utils'; +import { convertToGraphQLId } from '~/graphql_shared/utils'; + +import ciGroupVariables from '~/ci/ci_variable_list/components/ci_group_variables.vue'; +import ciVariableShared from '~/ci/ci_variable_list/components/ci_variable_shared.vue'; + +import { GRAPHQL_GROUP_TYPE } from '~/ci/ci_variable_list/constants'; + +const mockProvide = { + glFeatures: { + groupScopedCiVariables: false, + }, + groupPath: '/group', + groupId: 12, +}; + +describe('Ci Group Variable wrapper', () => { + let wrapper; + + const findCiShared = () => wrapper.findComponent(ciVariableShared); + + const createComponent = ({ provide = {} } = {}) => { + wrapper = shallowMount(ciGroupVariables, { + provide: { ...mockProvide, ...provide }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + describe('Props', () => { + beforeEach(() => { + createComponent(); + }); + + it('are passed down the correctly to ci_variable_shared', () => { + expect(findCiShared().props()).toEqual({ + id: convertToGraphQLId(GRAPHQL_GROUP_TYPE, mockProvide.groupId), + areScopedVariablesAvailable: false, + componentName: 'GroupVariables', + entity: 'group', + fullPath: mockProvide.groupPath, + hideEnvironmentScope: false, + mutationData: wrapper.vm.$options.mutationData, + queryData: wrapper.vm.$options.queryData, + refetchAfterMutation: false, + }); + }); + }); + + describe('feature flag', () => { + describe('When enabled', () => { + beforeEach(() => { + createComponent({ provide: { glFeatures: { groupScopedCiVariables: true } } }); + }); + + it('Passes down `true` to variable shared component', () => { + expect(findCiShared().props('areScopedVariablesAvailable')).toBe(true); + }); + }); + + describe('When disabled', () => { + beforeEach(() => { + createComponent({ provide: { glFeatures: { groupScopedCiVariables: false } } }); + }); + + it('Passes down `false` to variable shared component', () => { + expect(findCiShared().props('areScopedVariablesAvailable')).toBe(false); + }); + }); + }); +}); diff --git a/spec/frontend/ci/ci_variable_list/components/ci_project_variables_spec.js b/spec/frontend/ci/ci_variable_list/components/ci_project_variables_spec.js new file mode 100644 index 00000000000..7230017c560 --- /dev/null +++ b/spec/frontend/ci/ci_variable_list/components/ci_project_variables_spec.js @@ -0,0 +1,46 @@ +import { shallowMount } from '@vue/test-utils'; +import { convertToGraphQLId } from '~/graphql_shared/utils'; + +import ciProjectVariables from '~/ci/ci_variable_list/components/ci_project_variables.vue'; +import ciVariableShared from '~/ci/ci_variable_list/components/ci_variable_shared.vue'; + +import { GRAPHQL_PROJECT_TYPE } from '~/ci/ci_variable_list/constants'; + +const mockProvide = { + projectFullPath: '/namespace/project', + projectId: 1, +}; + +describe('Ci Project Variable wrapper', () => { + let wrapper; + + const findCiShared = () => wrapper.findComponent(ciVariableShared); + + const createComponent = () => { + wrapper = shallowMount(ciProjectVariables, { + provide: mockProvide, + }); + }; + + beforeEach(() => { + createComponent(); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('Passes down the correct props to ci_variable_shared', () => { + expect(findCiShared().props()).toEqual({ + id: convertToGraphQLId(GRAPHQL_PROJECT_TYPE, mockProvide.projectId), + areScopedVariablesAvailable: true, + componentName: 'ProjectVariables', + entity: 'project', + fullPath: mockProvide.projectFullPath, + hideEnvironmentScope: false, + mutationData: wrapper.vm.$options.mutationData, + queryData: wrapper.vm.$options.queryData, + refetchAfterMutation: false, + }); + }); +}); diff --git a/spec/frontend/ci/ci_variable_list/components/ci_variable_modal_spec.js b/spec/frontend/ci/ci_variable_list/components/ci_variable_modal_spec.js new file mode 100644 index 00000000000..7838e4884d8 --- /dev/null +++ b/spec/frontend/ci/ci_variable_list/components/ci_variable_modal_spec.js @@ -0,0 +1,520 @@ +import { GlButton, GlFormInput } from '@gitlab/ui'; +import { mockTracking } from 'helpers/tracking_helper'; +import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_helper'; +import CiEnvironmentsDropdown from '~/ci/ci_variable_list/components/ci_environments_dropdown.vue'; +import CiVariableModal from '~/ci/ci_variable_list/components/ci_variable_modal.vue'; +import { + ADD_VARIABLE_ACTION, + AWS_ACCESS_KEY_ID, + EDIT_VARIABLE_ACTION, + EVENT_LABEL, + EVENT_ACTION, + ENVIRONMENT_SCOPE_LINK_TITLE, + instanceString, + variableOptions, +} from '~/ci/ci_variable_list/constants'; +import { mockVariablesWithScopes } from '../mocks'; +import ModalStub from '../stubs'; + +describe('Ci variable modal', () => { + let wrapper; + let trackingSpy; + + const maskableRegex = '^[a-zA-Z0-9_+=/@:.~-]{8,}$'; + const mockVariables = mockVariablesWithScopes(instanceString); + + const defaultProvide = { + awsLogoSvgPath: '/logo', + awsTipCommandsLink: '/tips', + awsTipDeployLink: '/deploy', + awsTipLearnLink: '/learn-link', + containsVariableReferenceLink: '/reference', + environmentScopeLink: '/help/environments', + isProtectedByDefault: false, + maskedEnvironmentVariablesLink: '/variables-link', + maskableRegex, + protectedEnvironmentVariablesLink: '/protected-link', + }; + + const defaultProps = { + areScopedVariablesAvailable: true, + environments: [], + hideEnvironmentScope: false, + mode: ADD_VARIABLE_ACTION, + selectedVariable: {}, + variable: [], + }; + + const createComponent = ({ mountFn = shallowMountExtended, props = {}, provide = {} } = {}) => { + wrapper = mountFn(CiVariableModal, { + attachTo: document.body, + provide: { ...defaultProvide, ...provide }, + propsData: { + ...defaultProps, + ...props, + }, + stubs: { + GlModal: ModalStub, + }, + }); + }; + + const findCiEnvironmentsDropdown = () => wrapper.findComponent(CiEnvironmentsDropdown); + const findReferenceWarning = () => wrapper.findByTestId('contains-variable-reference'); + const findModal = () => wrapper.findComponent(ModalStub); + const findAWSTip = () => wrapper.findByTestId('aws-guidance-tip'); + const findAddorUpdateButton = () => wrapper.findByTestId('ciUpdateOrAddVariableBtn'); + const deleteVariableButton = () => + findModal() + .findAllComponents(GlButton) + .wrappers.find((button) => button.props('variant') === 'danger'); + const findExpandedVariableCheckbox = () => wrapper.findByTestId('ci-variable-expanded-checkbox'); + const findProtectedVariableCheckbox = () => + wrapper.findByTestId('ci-variable-protected-checkbox'); + const findMaskedVariableCheckbox = () => wrapper.findByTestId('ci-variable-masked-checkbox'); + const findValueField = () => wrapper.find('#ci-variable-value'); + const findEnvScopeLink = () => wrapper.findByTestId('environment-scope-link'); + const findEnvScopeInput = () => + wrapper.findByTestId('environment-scope').findComponent(GlFormInput); + const findRawVarTip = () => wrapper.findByTestId('raw-variable-tip'); + const findVariableTypeDropdown = () => wrapper.find('#ci-variable-type'); + const findEnvironmentScopeText = () => wrapper.findByText('Environment scope'); + + afterEach(() => { + wrapper.destroy(); + }); + + describe('Adding a variable', () => { + describe('when no key/value pair are present', () => { + beforeEach(() => { + createComponent(); + }); + + it('shows the submit button as disabled', () => { + expect(findAddorUpdateButton().attributes('disabled')).toBe('true'); + }); + }); + + describe('when a key/value pair is present', () => { + beforeEach(() => { + createComponent({ props: { selectedVariable: mockVariables[0] } }); + }); + + it('shows the submit button as enabled', () => { + expect(findAddorUpdateButton().attributes('disabled')).toBeUndefined(); + }); + }); + + describe('events', () => { + const [currentVariable] = mockVariables; + + beforeEach(() => { + createComponent({ props: { selectedVariable: currentVariable } }); + jest.spyOn(wrapper.vm, '$emit'); + }); + + it('Dispatches `add-variable` action on submit', () => { + findAddorUpdateButton().vm.$emit('click'); + expect(wrapper.emitted('add-variable')).toEqual([[currentVariable]]); + }); + + it('Dispatches the `hideModal` event when dismissing', () => { + findModal().vm.$emit('hidden'); + expect(wrapper.emitted('hideModal')).toEqual([[]]); + }); + }); + }); + + describe('when protected by default', () => { + describe('when adding a new variable', () => { + beforeEach(() => { + createComponent({ provide: { isProtectedByDefault: true } }); + findModal().vm.$emit('shown'); + }); + + it('updates the protected value to true', () => { + expect(findProtectedVariableCheckbox().attributes('data-is-protected-checked')).toBe( + 'true', + ); + }); + }); + + describe('when editing a variable', () => { + beforeEach(() => { + createComponent({ + provide: { isProtectedByDefault: false }, + props: { + selectedVariable: {}, + mode: EDIT_VARIABLE_ACTION, + }, + }); + findModal().vm.$emit('shown'); + }); + + it('keeps the value as false', async () => { + expect( + findProtectedVariableCheckbox().attributes('data-is-protected-checked'), + ).toBeUndefined(); + }); + }); + }); + + describe('Adding a new non-AWS variable', () => { + beforeEach(() => { + const [variable] = mockVariables; + createComponent({ mountFn: mountExtended, props: { selectedVariable: variable } }); + }); + + it('does not show AWS guidance tip', () => { + const tip = findAWSTip(); + expect(tip.exists()).toBe(true); + expect(tip.isVisible()).toBe(false); + }); + }); + + describe('Adding a new AWS variable', () => { + beforeEach(() => { + const [variable] = mockVariables; + const AWSKeyVariable = { + ...variable, + key: AWS_ACCESS_KEY_ID, + value: 'AKIAIOSFODNN7EXAMPLEjdhy', + }; + createComponent({ mountFn: mountExtended, props: { selectedVariable: AWSKeyVariable } }); + }); + + it('shows AWS guidance tip', () => { + const tip = findAWSTip(); + expect(tip.exists()).toBe(true); + expect(tip.isVisible()).toBe(true); + }); + }); + + describe('when expanded', () => { + describe('with a $ character', () => { + beforeEach(() => { + const [variable] = mockVariables; + const variableWithDollarSign = { + ...variable, + value: 'valueWith$', + }; + createComponent({ + mountFn: mountExtended, + props: { selectedVariable: variableWithDollarSign }, + }); + }); + + it(`renders the variable reference warning`, () => { + expect(findReferenceWarning().exists()).toBe(true); + }); + + it(`does not render raw variable tip`, () => { + expect(findRawVarTip().exists()).toBe(false); + }); + }); + + describe('without a $ character', () => { + beforeEach(() => { + const [variable] = mockVariables; + createComponent({ + mountFn: mountExtended, + props: { selectedVariable: variable }, + }); + }); + + it(`does not render the variable reference warning`, () => { + expect(findReferenceWarning().exists()).toBe(false); + }); + + it(`does not render raw variable tip`, () => { + expect(findRawVarTip().exists()).toBe(false); + }); + }); + + describe('setting raw value', () => { + const [variable] = mockVariables; + + it('defaults to expanded and raw:false when adding a variable', () => { + createComponent({ props: { selectedVariable: variable } }); + jest.spyOn(wrapper.vm, '$emit'); + + findModal().vm.$emit('shown'); + + expect(findExpandedVariableCheckbox().attributes('checked')).toBe('true'); + + findAddorUpdateButton().vm.$emit('click'); + + expect(wrapper.emitted('add-variable')).toEqual([ + [ + { + ...variable, + raw: false, + }, + ], + ]); + }); + + it('sets correct raw value when editing', async () => { + createComponent({ + props: { + selectedVariable: variable, + mode: EDIT_VARIABLE_ACTION, + }, + }); + jest.spyOn(wrapper.vm, '$emit'); + + findModal().vm.$emit('shown'); + await findExpandedVariableCheckbox().vm.$emit('change'); + await findAddorUpdateButton().vm.$emit('click'); + + expect(wrapper.emitted('update-variable')).toEqual([ + [ + { + ...variable, + raw: true, + }, + ], + ]); + }); + }); + }); + + describe('when not expanded', () => { + describe('with a $ character', () => { + beforeEach(() => { + const selectedVariable = mockVariables[1]; + createComponent({ + mountFn: mountExtended, + props: { selectedVariable }, + }); + }); + + it(`renders raw variable tip`, () => { + expect(findRawVarTip().exists()).toBe(true); + }); + }); + }); + + describe('Editing a variable', () => { + const [variable] = mockVariables; + + beforeEach(() => { + createComponent({ props: { selectedVariable: variable, mode: EDIT_VARIABLE_ACTION } }); + jest.spyOn(wrapper.vm, '$emit'); + }); + + 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(wrapper.emitted('update-variable')).toEqual([[variable]]); + }); + + it('Propagates the `hideModal` event', () => { + findModal().vm.$emit('hidden'); + expect(wrapper.emitted('hideModal')).toEqual([[]]); + }); + + it('dispatches `delete-variable` with correct variable to delete', () => { + deleteVariableButton().vm.$emit('click'); + expect(wrapper.emitted('delete-variable')).toEqual([[variable]]); + }); + }); + + describe('Environment scope', () => { + describe('when feature is available', () => { + describe('and section is not hidden', () => { + beforeEach(() => { + createComponent({ + mountFn: mountExtended, + props: { + areScopedVariablesAvailable: true, + hideEnvironmentScope: false, + }, + }); + }); + + it('renders the environment dropdown and section title', () => { + expect(findCiEnvironmentsDropdown().exists()).toBe(true); + expect(findCiEnvironmentsDropdown().isVisible()).toBe(true); + expect(findEnvironmentScopeText().exists()).toBe(true); + }); + + it('renders a link to documentation on scopes', () => { + const link = findEnvScopeLink(); + + expect(link.attributes('title')).toBe(ENVIRONMENT_SCOPE_LINK_TITLE); + expect(link.attributes('href')).toBe(defaultProvide.environmentScopeLink); + }); + }); + + describe('and section is hidden', () => { + beforeEach(() => { + createComponent({ + mountFn: mountExtended, + props: { + areScopedVariablesAvailable: true, + hideEnvironmentScope: true, + }, + }); + }); + + it('does not renders the environment dropdown and section title', () => { + expect(findCiEnvironmentsDropdown().exists()).toBe(false); + expect(findEnvironmentScopeText().exists()).toBe(false); + }); + }); + }); + + describe('when feature is not available', () => { + describe('and section is not hidden', () => { + beforeEach(() => { + createComponent({ + mountFn: mountExtended, + props: { + areScopedVariablesAvailable: false, + hideEnvironmentScope: false, + }, + }); + }); + + it('disables the dropdown', () => { + expect(findCiEnvironmentsDropdown().exists()).toBe(false); + expect(findEnvironmentScopeText().exists()).toBe(true); + expect(findEnvScopeInput().attributes('readonly')).toBe('readonly'); + }); + }); + + describe('and section is hidden', () => { + beforeEach(() => { + createComponent({ + mountFn: mountExtended, + props: { + areScopedVariablesAvailable: false, + hideEnvironmentScope: true, + }, + }); + }); + + it('hides the dropdown', () => { + expect(findEnvironmentScopeText().exists()).toBe(false); + expect(findCiEnvironmentsDropdown().exists()).toBe(false); + }); + }); + }); + }); + + describe('variable type dropdown', () => { + describe('default behaviour', () => { + beforeEach(() => { + createComponent({ mountFn: mountExtended }); + }); + + it('adds each option as a dropdown item', () => { + expect(findVariableTypeDropdown().findAll('option')).toHaveLength(variableOptions.length); + variableOptions.forEach((v) => { + expect(findVariableTypeDropdown().text()).toContain(v.text); + }); + }); + }); + }); + + describe('Validations', () => { + const maskError = 'This variable can not be masked.'; + + describe('when the mask state is invalid', () => { + beforeEach(async () => { + const [variable] = mockVariables; + const invalidMaskVariable = { + ...variable, + value: 'd:;', + masked: false, + }; + createComponent({ + mountFn: mountExtended, + props: { selectedVariable: invalidMaskVariable }, + }); + trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn); + await findMaskedVariableCheckbox().trigger('click'); + }); + + 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 | masked | eventSent | trackingErrorProperty + ${'secretValue'} | ${false} | ${0} | ${null} + ${'short'} | ${true} | ${0} | ${null} + ${'dollar$ign'} | ${false} | ${1} | ${'$'} + ${'dollar$ign'} | ${true} | ${1} | ${'$'} + ${'unsupported|char'} | ${true} | ${1} | ${'|'} + ${'unsupported|char'} | ${false} | ${0} | ${null} + `('Adding a new variable', ({ value, masked, eventSent, trackingErrorProperty }) => { + beforeEach(async () => { + const [variable] = mockVariables; + const invalidKeyVariable = { + ...variable, + value: '', + masked: false, + }; + createComponent({ + mountFn: mountExtended, + props: { selectedVariable: invalidKeyVariable }, + }); + trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn); + await findValueField().vm.$emit('input', value); + if (masked) { + await findMaskedVariableCheckbox().trigger('click'); + } + }); + + it(`${ + eventSent > 0 ? 'sends the correct' : 'does not send the' + } variable validation tracking event with ${value}`, () => { + expect(trackingSpy).toHaveBeenCalledTimes(eventSent); + + if (eventSent > 0) { + expect(trackingSpy).toHaveBeenCalledWith(undefined, EVENT_ACTION, { + label: EVENT_LABEL, + property: trackingErrorProperty, + }); + } + }); + }); + + describe('when masked variable has acceptable value', () => { + beforeEach(() => { + const [variable] = mockVariables; + const validMaskandKeyVariable = { + ...variable, + key: AWS_ACCESS_KEY_ID, + value: '12345678', + masked: true, + }; + createComponent({ + mountFn: mountExtended, + props: { selectedVariable: validMaskandKeyVariable }, + }); + }); + + it('does not disable the submit button', () => { + expect(findAddorUpdateButton().attributes('disabled')).toBeUndefined(); + }); + }); + }); +}); diff --git a/spec/frontend/ci/ci_variable_list/components/ci_variable_settings_spec.js b/spec/frontend/ci/ci_variable_list/components/ci_variable_settings_spec.js new file mode 100644 index 00000000000..32af2ec4de9 --- /dev/null +++ b/spec/frontend/ci/ci_variable_list/components/ci_variable_settings_spec.js @@ -0,0 +1,147 @@ +import { nextTick } from 'vue'; +import { shallowMount } from '@vue/test-utils'; +import CiVariableSettings from '~/ci/ci_variable_list/components/ci_variable_settings.vue'; +import ciVariableModal from '~/ci/ci_variable_list/components/ci_variable_modal.vue'; +import ciVariableTable from '~/ci/ci_variable_list/components/ci_variable_table.vue'; +import { + ADD_VARIABLE_ACTION, + EDIT_VARIABLE_ACTION, + projectString, +} from '~/ci/ci_variable_list/constants'; +import { mapEnvironmentNames } from '~/ci/ci_variable_list/utils'; + +import { mockEnvs, mockVariablesWithScopes, newVariable } from '../mocks'; + +describe('Ci variable table', () => { + let wrapper; + + const defaultProps = { + areScopedVariablesAvailable: true, + entity: 'project', + environments: mapEnvironmentNames(mockEnvs), + hideEnvironmentScope: false, + isLoading: false, + maxVariableLimit: 5, + variables: mockVariablesWithScopes(projectString), + }; + + const findCiVariableTable = () => wrapper.findComponent(ciVariableTable); + const findCiVariableModal = () => wrapper.findComponent(ciVariableModal); + + const createComponent = ({ props = {} } = {}) => { + wrapper = shallowMount(CiVariableSettings, { + propsData: { + ...defaultProps, + ...props, + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + describe('props passing', () => { + it('passes props down correctly to the ci table', () => { + createComponent(); + + expect(findCiVariableTable().props()).toEqual({ + entity: 'project', + isLoading: defaultProps.isLoading, + maxVariableLimit: defaultProps.maxVariableLimit, + variables: defaultProps.variables, + }); + }); + + it('passes props down correctly to the ci modal', async () => { + createComponent(); + + findCiVariableTable().vm.$emit('set-selected-variable'); + await nextTick(); + + expect(findCiVariableModal().props()).toEqual({ + areScopedVariablesAvailable: defaultProps.areScopedVariablesAvailable, + environments: defaultProps.environments, + hideEnvironmentScope: defaultProps.hideEnvironmentScope, + variables: defaultProps.variables, + mode: ADD_VARIABLE_ACTION, + selectedVariable: {}, + }); + }); + }); + + describe('modal mode', () => { + beforeEach(() => { + createComponent(); + }); + + it('passes down ADD mode when receiving an empty variable', async () => { + findCiVariableTable().vm.$emit('set-selected-variable'); + await nextTick(); + + expect(findCiVariableModal().props('mode')).toBe(ADD_VARIABLE_ACTION); + }); + + it('passes down EDIT mode when receiving a variable', async () => { + findCiVariableTable().vm.$emit('set-selected-variable', newVariable); + await nextTick(); + + expect(findCiVariableModal().props('mode')).toBe(EDIT_VARIABLE_ACTION); + }); + }); + + describe('variable modal', () => { + beforeEach(() => { + createComponent(); + }); + + it('is hidden by default', () => { + expect(findCiVariableModal().exists()).toBe(false); + }); + + it('shows modal when adding a new variable', async () => { + findCiVariableTable().vm.$emit('set-selected-variable'); + await nextTick(); + + expect(findCiVariableModal().exists()).toBe(true); + }); + + it('shows modal when updating a variable', async () => { + findCiVariableTable().vm.$emit('set-selected-variable', newVariable); + await nextTick(); + + expect(findCiVariableModal().exists()).toBe(true); + }); + + it('hides modal when receiving the event from the modal', async () => { + findCiVariableTable().vm.$emit('set-selected-variable'); + await nextTick(); + + findCiVariableModal().vm.$emit('hideModal'); + await nextTick(); + + expect(findCiVariableModal().exists()).toBe(false); + }); + }); + + describe('variable events', () => { + beforeEach(() => { + createComponent(); + }); + + it.each` + eventName + ${'add-variable'} + ${'update-variable'} + ${'delete-variable'} + `('bubbles up the $eventName event', async ({ eventName }) => { + findCiVariableTable().vm.$emit('set-selected-variable'); + await nextTick(); + + findCiVariableModal().vm.$emit(eventName, newVariable); + await nextTick(); + + expect(wrapper.emitted(eventName)).toEqual([[newVariable]]); + }); + }); +}); diff --git a/spec/frontend/ci/ci_variable_list/components/ci_variable_shared_spec.js b/spec/frontend/ci/ci_variable_list/components/ci_variable_shared_spec.js new file mode 100644 index 00000000000..2d39bff8ce0 --- /dev/null +++ b/spec/frontend/ci/ci_variable_list/components/ci_variable_shared_spec.js @@ -0,0 +1,450 @@ +import Vue, { nextTick } from 'vue'; +import VueApollo from 'vue-apollo'; +import { GlLoadingIcon, GlTable } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import waitForPromises from 'helpers/wait_for_promises'; +import { createAlert } from '~/flash'; +import { resolvers } from '~/ci/ci_variable_list/graphql/settings'; +import { convertToGraphQLId } from '~/graphql_shared/utils'; + +import ciVariableShared from '~/ci/ci_variable_list/components/ci_variable_shared.vue'; +import ciVariableSettings from '~/ci/ci_variable_list/components/ci_variable_settings.vue'; +import ciVariableTable from '~/ci/ci_variable_list/components/ci_variable_table.vue'; +import getProjectEnvironments from '~/ci/ci_variable_list/graphql/queries/project_environments.query.graphql'; +import getAdminVariables from '~/ci/ci_variable_list/graphql/queries/variables.query.graphql'; +import getGroupVariables from '~/ci/ci_variable_list/graphql/queries/group_variables.query.graphql'; +import getProjectVariables from '~/ci/ci_variable_list/graphql/queries/project_variables.query.graphql'; + +import { + ADD_MUTATION_ACTION, + DELETE_MUTATION_ACTION, + UPDATE_MUTATION_ACTION, + environmentFetchErrorText, + genericMutationErrorText, + variableFetchErrorText, +} from '~/ci/ci_variable_list/constants'; + +import { + createGroupProps, + createInstanceProps, + createProjectProps, + createGroupProvide, + createProjectProvide, + devName, + mockProjectEnvironments, + mockProjectVariables, + newVariable, + prodName, + mockGroupVariables, + mockAdminVariables, +} from '../mocks'; + +jest.mock('~/flash'); + +Vue.use(VueApollo); + +const mockProvide = { + endpoint: '/variables', + isGroup: false, + isProject: false, +}; + +const defaultProps = { + areScopedVariablesAvailable: true, + hideEnvironmentScope: false, + refetchAfterMutation: false, +}; + +describe('Ci Variable Shared Component', () => { + let wrapper; + + let mockApollo; + let mockEnvironments; + let mockVariables; + + const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); + const findCiTable = () => wrapper.findComponent(GlTable); + const findCiSettings = () => wrapper.findComponent(ciVariableSettings); + + // eslint-disable-next-line consistent-return + async function createComponentWithApollo({ + customHandlers = null, + isLoading = false, + props = { ...createProjectProps() }, + provide = {}, + } = {}) { + const handlers = customHandlers || [ + [getProjectEnvironments, mockEnvironments], + [getProjectVariables, mockVariables], + ]; + + mockApollo = createMockApollo(handlers, resolvers); + + wrapper = shallowMount(ciVariableShared, { + propsData: { + ...defaultProps, + ...props, + }, + provide: { + ...mockProvide, + ...provide, + }, + apolloProvider: mockApollo, + stubs: { ciVariableSettings, ciVariableTable }, + }); + + if (!isLoading) { + return waitForPromises(); + } + } + + beforeEach(() => { + mockEnvironments = jest.fn(); + mockVariables = jest.fn(); + }); + + describe('while queries are being fetch', () => { + beforeEach(() => { + createComponentWithApollo({ isLoading: true }); + }); + + it('shows a loading icon', () => { + expect(findLoadingIcon().exists()).toBe(true); + expect(findCiTable().exists()).toBe(false); + }); + }); + + describe('when queries are resolved', () => { + describe('successfully', () => { + beforeEach(async () => { + mockEnvironments.mockResolvedValue(mockProjectEnvironments); + mockVariables.mockResolvedValue(mockProjectVariables); + + await createComponentWithApollo({ provide: createProjectProvide() }); + }); + + it('passes down the expected max variable limit as props', () => { + expect(findCiSettings().props('maxVariableLimit')).toBe( + mockProjectVariables.data.project.ciVariables.limit, + ); + }); + + it('passes down the expected environments as props', () => { + expect(findCiSettings().props('environments')).toEqual([prodName, devName]); + }); + + it('passes down the expected variables as props', () => { + expect(findCiSettings().props('variables')).toEqual( + mockProjectVariables.data.project.ciVariables.nodes, + ); + }); + + it('createAlert was not called', () => { + expect(createAlert).not.toHaveBeenCalled(); + }); + }); + + describe('with an error for variables', () => { + beforeEach(async () => { + mockEnvironments.mockResolvedValue(mockProjectEnvironments); + mockVariables.mockRejectedValue(); + + await createComponentWithApollo(); + }); + + it('calls createAlert with the expected error message', () => { + expect(createAlert).toHaveBeenCalledWith({ message: variableFetchErrorText }); + }); + }); + + describe('with an error for environments', () => { + beforeEach(async () => { + mockEnvironments.mockRejectedValue(); + mockVariables.mockResolvedValue(mockProjectVariables); + + await createComponentWithApollo(); + }); + + it('calls createAlert with the expected error message', () => { + expect(createAlert).toHaveBeenCalledWith({ message: environmentFetchErrorText }); + }); + }); + }); + + describe('environment query', () => { + describe('when there is an environment key in queryData', () => { + beforeEach(async () => { + mockEnvironments.mockResolvedValue(mockProjectEnvironments); + mockVariables.mockResolvedValue(mockProjectVariables); + + await createComponentWithApollo({ props: { ...createProjectProps() } }); + }); + + it('is executed', () => { + expect(mockVariables).toHaveBeenCalled(); + }); + }); + + describe('when there isnt an environment key in queryData', () => { + beforeEach(async () => { + mockVariables.mockResolvedValue(mockGroupVariables); + + await createComponentWithApollo({ props: { ...createGroupProps() } }); + }); + + it('is skipped', () => { + expect(mockVariables).not.toHaveBeenCalled(); + }); + }); + }); + + describe('mutations', () => { + const groupProps = createGroupProps(); + + beforeEach(async () => { + mockVariables.mockResolvedValue(mockGroupVariables); + + await createComponentWithApollo({ + customHandlers: [[getGroupVariables, mockVariables]], + props: groupProps, + }); + }); + it.each` + actionName | mutation | event + ${'add'} | ${groupProps.mutationData[ADD_MUTATION_ACTION]} | ${'add-variable'} + ${'update'} | ${groupProps.mutationData[UPDATE_MUTATION_ACTION]} | ${'update-variable'} + ${'delete'} | ${groupProps.mutationData[DELETE_MUTATION_ACTION]} | ${'delete-variable'} + `( + 'calls the right mutation from propsData when user performs $actionName variable', + async ({ event, mutation }) => { + jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(); + + await findCiSettings().vm.$emit(event, newVariable); + + expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({ + mutation, + variables: { + endpoint: mockProvide.endpoint, + fullPath: groupProps.fullPath, + id: convertToGraphQLId('Group', groupProps.id), + variable: newVariable, + }, + }); + }, + ); + + it.each` + actionName | event + ${'add'} | ${'add-variable'} + ${'update'} | ${'update-variable'} + ${'delete'} | ${'delete-variable'} + `( + 'throws with the specific graphql error if present when user performs $actionName variable', + async ({ event }) => { + const graphQLErrorMessage = 'There is a problem with this graphQL action'; + jest + .spyOn(wrapper.vm.$apollo, 'mutate') + .mockResolvedValue({ data: { ciVariableMutation: { errors: [graphQLErrorMessage] } } }); + await findCiSettings().vm.$emit(event, newVariable); + await nextTick(); + + expect(wrapper.vm.$apollo.mutate).toHaveBeenCalled(); + expect(createAlert).toHaveBeenCalledWith({ message: graphQLErrorMessage }); + }, + ); + + it.each` + actionName | event + ${'add'} | ${'add-variable'} + ${'update'} | ${'update-variable'} + ${'delete'} | ${'delete-variable'} + `( + 'throws generic error on failure with no graphql errors and user performs $actionName variable', + async ({ event }) => { + jest.spyOn(wrapper.vm.$apollo, 'mutate').mockImplementationOnce(() => { + throw new Error(); + }); + await findCiSettings().vm.$emit(event, newVariable); + + expect(wrapper.vm.$apollo.mutate).toHaveBeenCalled(); + expect(createAlert).toHaveBeenCalledWith({ message: genericMutationErrorText }); + }, + ); + + describe('without fullpath and ID props', () => { + beforeEach(async () => { + mockVariables.mockResolvedValue(mockAdminVariables); + + await createComponentWithApollo({ + customHandlers: [[getAdminVariables, mockVariables]], + props: createInstanceProps(), + }); + }); + + it('does not pass fullPath and ID to the mutation', async () => { + jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(); + + await findCiSettings().vm.$emit('add-variable', newVariable); + + expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({ + mutation: wrapper.props().mutationData[ADD_MUTATION_ACTION], + variables: { + endpoint: mockProvide.endpoint, + variable: newVariable, + }, + }); + }); + }); + }); + + describe('Props', () => { + const mockGroupCiVariables = mockGroupVariables.data.group.ciVariables; + const mockProjectCiVariables = mockProjectVariables.data.project.ciVariables; + + describe('in a specific context as', () => { + it.each` + name | mockVariablesValue | mockEnvironmentsValue | withEnvironments | expectedEnvironments | propsFn | provideFn | mutation | maxVariableLimit + ${'project'} | ${mockProjectVariables} | ${mockProjectEnvironments} | ${true} | ${['prod', 'dev']} | ${createProjectProps} | ${createProjectProvide} | ${null} | ${mockProjectCiVariables.limit} + ${'group'} | ${mockGroupVariables} | ${[]} | ${false} | ${[]} | ${createGroupProps} | ${createGroupProvide} | ${getGroupVariables} | ${mockGroupCiVariables.limit} + ${'instance'} | ${mockAdminVariables} | ${[]} | ${false} | ${[]} | ${createInstanceProps} | ${() => {}} | ${getAdminVariables} | ${0} + `( + 'passes down all the required props when its a $name component', + async ({ + mutation, + maxVariableLimit, + mockVariablesValue, + mockEnvironmentsValue, + withEnvironments, + expectedEnvironments, + propsFn, + provideFn, + }) => { + const props = propsFn(); + const provide = provideFn(); + + mockVariables.mockResolvedValue(mockVariablesValue); + + if (withEnvironments) { + mockEnvironments.mockResolvedValue(mockEnvironmentsValue); + } + + let customHandlers = null; + + if (mutation) { + customHandlers = [[mutation, mockVariables]]; + } + + await createComponentWithApollo({ customHandlers, props, provide }); + + expect(findCiSettings().props()).toEqual({ + areScopedVariablesAvailable: wrapper.props().areScopedVariablesAvailable, + hideEnvironmentScope: defaultProps.hideEnvironmentScope, + isLoading: false, + maxVariableLimit, + variables: wrapper.props().queryData.ciVariables.lookup(mockVariablesValue.data)?.nodes, + entity: props.entity, + environments: expectedEnvironments, + }); + }, + ); + }); + + describe('refetchAfterMutation', () => { + it.each` + bool | text + ${true} | ${'refetches the variables'} + ${false} | ${'does not refetch the variables'} + `('when $bool it $text', async ({ bool }) => { + await createComponentWithApollo({ + props: { ...createInstanceProps(), refetchAfterMutation: bool }, + }); + + jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue({ data: {} }); + jest.spyOn(wrapper.vm.$apollo.queries.ciVariables, 'refetch').mockImplementation(jest.fn()); + + await findCiSettings().vm.$emit('add-variable', newVariable); + + await nextTick(); + + if (bool) { + expect(wrapper.vm.$apollo.queries.ciVariables.refetch).toHaveBeenCalled(); + } else { + expect(wrapper.vm.$apollo.queries.ciVariables.refetch).not.toHaveBeenCalled(); + } + }); + }); + + describe('Validators', () => { + describe('queryData', () => { + let error; + + beforeEach(async () => { + mockVariables.mockResolvedValue(mockGroupVariables); + }); + + it('will mount component with right data', async () => { + try { + await createComponentWithApollo({ + customHandlers: [[getGroupVariables, mockVariables]], + props: { ...createGroupProps() }, + }); + } catch (e) { + error = e; + } finally { + expect(wrapper.exists()).toBe(true); + expect(error).toBeUndefined(); + } + }); + + it('will not mount component with wrong data', async () => { + try { + await createComponentWithApollo({ + customHandlers: [[getGroupVariables, mockVariables]], + props: { ...createGroupProps(), queryData: { wrongKey: {} } }, + }); + } catch (e) { + error = e; + } finally { + expect(wrapper.exists()).toBe(false); + expect(error.toString()).toContain('custom validator check failed for prop'); + } + }); + }); + + describe('mutationData', () => { + let error; + + beforeEach(async () => { + mockVariables.mockResolvedValue(mockGroupVariables); + }); + + it('will mount component with right data', async () => { + try { + await createComponentWithApollo({ + props: { ...createGroupProps() }, + }); + } catch (e) { + error = e; + } finally { + expect(wrapper.exists()).toBe(true); + expect(error).toBeUndefined(); + } + }); + + it('will not mount component with wrong data', async () => { + try { + await createComponentWithApollo({ + props: { ...createGroupProps(), mutationData: { wrongKey: {} } }, + }); + } catch (e) { + error = e; + } finally { + expect(wrapper.exists()).toBe(false); + expect(error.toString()).toContain('custom validator check failed for prop'); + } + }); + }); + }); + }); +}); diff --git a/spec/frontend/ci/ci_variable_list/components/ci_variable_table_spec.js b/spec/frontend/ci/ci_variable_list/components/ci_variable_table_spec.js new file mode 100644 index 00000000000..9e2508c56ee --- /dev/null +++ b/spec/frontend/ci/ci_variable_list/components/ci_variable_table_spec.js @@ -0,0 +1,172 @@ +import { GlAlert } from '@gitlab/ui'; +import { sprintf } from '~/locale'; +import { mountExtended } from 'helpers/vue_test_utils_helper'; +import CiVariableTable from '~/ci/ci_variable_list/components/ci_variable_table.vue'; +import { EXCEEDS_VARIABLE_LIMIT_TEXT, projectString } from '~/ci/ci_variable_list/constants'; +import { mockVariables } from '../mocks'; + +describe('Ci variable table', () => { + let wrapper; + + const defaultProps = { + entity: 'project', + isLoading: false, + maxVariableLimit: mockVariables(projectString).length + 1, + variables: mockVariables(projectString), + }; + + const mockMaxVariableLimit = defaultProps.variables.length; + + const createComponent = ({ props = {} } = {}) => { + wrapper = mountExtended(CiVariableTable, { + attachTo: document.body, + propsData: { + ...defaultProps, + ...props, + }, + }); + }; + + const findRevealButton = () => wrapper.findByText('Reveal values'); + const findAddButton = () => wrapper.findByLabelText('Add'); + const findEditButton = () => wrapper.findByLabelText('Edit'); + const findEmptyVariablesPlaceholder = () => wrapper.findByText('There are no variables yet.'); + const findHiddenValues = () => wrapper.findAllByTestId('hiddenValue'); + const findLimitReachedAlerts = () => wrapper.findAllComponents(GlAlert); + const findRevealedValues = () => wrapper.findAllByTestId('revealedValue'); + const findOptionsValues = (rowIndex) => + wrapper.findAllByTestId('ci-variable-table-row-options').at(rowIndex).text(); + + const generateExceedsVariableLimitText = (entity, currentVariableCount, maxVariableLimit) => { + return sprintf(EXCEEDS_VARIABLE_LIMIT_TEXT, { entity, currentVariableCount, maxVariableLimit }); + }; + + beforeEach(() => { + createComponent(); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + describe('When table is empty', () => { + beforeEach(() => { + createComponent({ props: { 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(() => { + createComponent(); + }); + + 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(defaultProps.variables.length); + }); + + it('displays the correct variable options', async () => { + expect(findOptionsValues(0)).toBe('Protected, Expanded'); + expect(findOptionsValues(1)).toBe('Masked'); + }); + + it('enables the Add Variable button', () => { + expect(findAddButton().props('disabled')).toBe(false); + }); + }); + + describe('When variables have exceeded the max limit', () => { + beforeEach(() => { + createComponent({ props: { maxVariableLimit: mockVariables(projectString).length } }); + }); + + it('disables the Add Variable button', () => { + expect(findAddButton().props('disabled')).toBe(true); + }); + }); + + describe('max limit reached alert', () => { + describe('when there is no variable limit', () => { + beforeEach(() => { + createComponent({ + props: { maxVariableLimit: 0 }, + }); + }); + + it('hides alert', () => { + expect(findLimitReachedAlerts().length).toBe(0); + }); + }); + + describe('when variable limit exists', () => { + it('hides alert when limit has not been reached', () => { + createComponent(); + + expect(findLimitReachedAlerts().length).toBe(0); + }); + + it('shows alert when limit has been reached', () => { + const exceedsVariableLimitText = generateExceedsVariableLimitText( + defaultProps.entity, + defaultProps.variables.length, + mockMaxVariableLimit, + ); + + createComponent({ + props: { maxVariableLimit: mockMaxVariableLimit }, + }); + + expect(findLimitReachedAlerts().length).toBe(2); + + expect(findLimitReachedAlerts().at(0).props('dismissible')).toBe(false); + expect(findLimitReachedAlerts().at(0).text()).toContain(exceedsVariableLimitText); + + expect(findLimitReachedAlerts().at(1).props('dismissible')).toBe(false); + expect(findLimitReachedAlerts().at(1).text()).toContain(exceedsVariableLimitText); + }); + }); + }); + + describe('Table click actions', () => { + beforeEach(() => { + createComponent(); + }); + + it('reveals secret values when button is clicked', async () => { + expect(findHiddenValues()).toHaveLength(defaultProps.variables.length); + expect(findRevealedValues()).toHaveLength(0); + + await findRevealButton().trigger('click'); + + expect(findHiddenValues()).toHaveLength(0); + expect(findRevealedValues()).toHaveLength(defaultProps.variables.length); + }); + + it('dispatches `setSelectedVariable` with correct variable to edit', async () => { + await findEditButton().trigger('click'); + + expect(wrapper.emitted('set-selected-variable')).toEqual([[defaultProps.variables[0]]]); + }); + + it('dispatches `setSelectedVariable` with no variable when adding a new one', async () => { + await findAddButton().trigger('click'); + + expect(wrapper.emitted('set-selected-variable')).toEqual([[null]]); + }); + }); +}); |