diff options
Diffstat (limited to 'spec/frontend/feature_flags/components/form_spec.js')
-rw-r--r-- | spec/frontend/feature_flags/components/form_spec.js | 493 |
1 files changed, 493 insertions, 0 deletions
diff --git a/spec/frontend/feature_flags/components/form_spec.js b/spec/frontend/feature_flags/components/form_spec.js new file mode 100644 index 00000000000..33c7eeb54b7 --- /dev/null +++ b/spec/frontend/feature_flags/components/form_spec.js @@ -0,0 +1,493 @@ +import { uniqueId } from 'lodash'; +import { shallowMount } from '@vue/test-utils'; +import { GlFormTextarea, GlFormCheckbox, GlButton } from '@gitlab/ui'; +import Api from '~/api'; +import Form from '~/feature_flags/components/form.vue'; +import EnvironmentsDropdown from '~/feature_flags/components/environments_dropdown.vue'; +import Strategy from '~/feature_flags/components/strategy.vue'; +import { + ROLLOUT_STRATEGY_ALL_USERS, + ROLLOUT_STRATEGY_PERCENT_ROLLOUT, + INTERNAL_ID_PREFIX, + DEFAULT_PERCENT_ROLLOUT, + LEGACY_FLAG, + NEW_VERSION_FLAG, +} from '~/feature_flags/constants'; +import RelatedIssuesRoot from '~/related_issues/components/related_issues_root.vue'; +import ToggleButton from '~/vue_shared/components/toggle_button.vue'; +import { featureFlag, userList, allUsersStrategy } from '../mock_data'; + +jest.mock('~/api.js'); + +describe('feature flag form', () => { + let wrapper; + const requiredProps = { + cancelPath: 'feature_flags', + submitText: 'Create', + }; + + const requiredInjections = { + environmentsEndpoint: '/environments.json', + projectId: '1', + glFeatures: { + featureFlagPermissions: true, + featureFlagsNewVersion: true, + }, + }; + + const factory = (props = {}, provide = {}) => { + wrapper = shallowMount(Form, { + propsData: { ...requiredProps, ...props }, + provide: { + ...requiredInjections, + ...provide, + }, + }); + }; + + beforeEach(() => { + Api.fetchFeatureFlagUserLists.mockResolvedValue({ data: [] }); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('should render provided submitText', () => { + factory(requiredProps); + + expect(wrapper.find('.js-ff-submit').text()).toEqual(requiredProps.submitText); + }); + + it('should render provided cancelPath', () => { + factory(requiredProps); + + expect(wrapper.find('.js-ff-cancel').attributes('href')).toEqual(requiredProps.cancelPath); + }); + + it('does not render the related issues widget without the featureFlagIssuesEndpoint', () => { + factory(requiredProps); + + expect(wrapper.find(RelatedIssuesRoot).exists()).toBe(false); + }); + + it('renders the related issues widget when the featureFlagIssuesEndpoint is provided', () => { + factory( + {}, + { + ...requiredInjections, + featureFlagIssuesEndpoint: '/some/endpoint', + }, + ); + + expect(wrapper.find(RelatedIssuesRoot).exists()).toBe(true); + }); + + describe('without provided data', () => { + beforeEach(() => { + factory(requiredProps); + }); + + it('should render name input text', () => { + expect(wrapper.find('#feature-flag-name').exists()).toBe(true); + }); + + it('should render description textarea', () => { + expect(wrapper.find('#feature-flag-description').exists()).toBe(true); + }); + + describe('scopes', () => { + it('should render scopes table', () => { + expect(wrapper.find('.js-scopes-table').exists()).toBe(true); + }); + + it('should render scopes table with a new row ', () => { + expect(wrapper.find('.js-add-new-scope').exists()).toBe(true); + }); + + describe('status toggle', () => { + describe('without filled text input', () => { + it('should add a new scope with the text value empty and the status', () => { + wrapper.find(ToggleButton).vm.$emit('change', true); + + expect(wrapper.vm.formScopes).toHaveLength(1); + expect(wrapper.vm.formScopes[0].active).toEqual(true); + expect(wrapper.vm.formScopes[0].environmentScope).toEqual(''); + + expect(wrapper.vm.newScope).toEqual(''); + }); + }); + + it('should be disabled if the feature flag is not active', done => { + wrapper.setProps({ active: false }); + wrapper.vm.$nextTick(() => { + expect(wrapper.find(ToggleButton).props('disabledInput')).toBe(true); + done(); + }); + }); + }); + }); + }); + + describe('with provided data', () => { + beforeEach(() => { + factory({ + ...requiredProps, + name: featureFlag.name, + description: featureFlag.description, + active: true, + version: LEGACY_FLAG, + scopes: [ + { + id: 1, + active: true, + environmentScope: 'scope', + canUpdate: true, + protected: false, + rolloutStrategy: ROLLOUT_STRATEGY_PERCENT_ROLLOUT, + rolloutPercentage: '54', + rolloutUserIds: '123', + shouldIncludeUserIds: true, + }, + { + id: 2, + active: true, + environmentScope: 'scope', + canUpdate: false, + protected: true, + rolloutStrategy: ROLLOUT_STRATEGY_PERCENT_ROLLOUT, + rolloutPercentage: '54', + rolloutUserIds: '123', + shouldIncludeUserIds: true, + }, + ], + }); + }); + + describe('scopes', () => { + it('should be possible to remove a scope', () => { + expect(wrapper.find('.js-feature-flag-delete').exists()).toEqual(true); + }); + + it('renders empty row to add a new scope', () => { + expect(wrapper.find('.js-add-new-scope').exists()).toEqual(true); + }); + + it('renders the user id checkbox', () => { + expect(wrapper.find(GlFormCheckbox).exists()).toBe(true); + }); + + it('renders the user id text area', () => { + expect(wrapper.find(GlFormTextarea).exists()).toBe(true); + + expect(wrapper.find(GlFormTextarea).vm.value).toBe('123'); + }); + + describe('update scope', () => { + describe('on click on toggle', () => { + it('should update the scope', () => { + wrapper.find(ToggleButton).vm.$emit('change', false); + + expect(wrapper.vm.formScopes[0].active).toBe(false); + }); + + it('should be disabled if the feature flag is not active', done => { + wrapper.setProps({ active: false }); + + wrapper.vm.$nextTick(() => { + expect(wrapper.find(ToggleButton).props('disabledInput')).toBe(true); + done(); + }); + }); + }); + describe('on strategy change', () => { + it('should not include user IDs if All Users is selected', () => { + const scope = wrapper.find({ ref: 'scopeRow' }); + scope.find('select').setValue(ROLLOUT_STRATEGY_ALL_USERS); + return wrapper.vm.$nextTick().then(() => { + expect(scope.find('#rollout-user-id-0').exists()).toBe(false); + }); + }); + }); + }); + + describe('deleting an existing scope', () => { + beforeEach(() => { + wrapper.find('.js-delete-scope').vm.$emit('click'); + }); + + it('should add `shouldBeDestroyed` key the clicked scope', () => { + expect(wrapper.vm.formScopes[0].shouldBeDestroyed).toBe(true); + }); + + it('should not render deleted scopes', () => { + expect(wrapper.vm.filteredScopes).toEqual([expect.objectContaining({ id: 2 })]); + }); + }); + + describe('deleting a new scope', () => { + it('should remove the scope from formScopes', () => { + factory({ + ...requiredProps, + name: 'feature_flag_1', + description: 'this is a feature flag', + scopes: [ + { + environmentScope: 'new_scope', + active: false, + id: uniqueId(INTERNAL_ID_PREFIX), + canUpdate: true, + protected: false, + strategies: [ + { + name: ROLLOUT_STRATEGY_ALL_USERS, + parameters: {}, + }, + ], + }, + ], + }); + + wrapper.find('.js-delete-scope').vm.$emit('click'); + + expect(wrapper.vm.formScopes).toEqual([]); + }); + }); + + describe('with * scope', () => { + beforeEach(() => { + factory({ + ...requiredProps, + name: 'feature_flag_1', + description: 'this is a feature flag', + scopes: [ + { + environmentScope: '*', + active: false, + canUpdate: false, + rolloutStrategy: ROLLOUT_STRATEGY_ALL_USERS, + rolloutPercentage: DEFAULT_PERCENT_ROLLOUT, + }, + ], + }); + }); + + it('renders read only name', () => { + expect(wrapper.find('.js-scope-all').exists()).toEqual(true); + }); + }); + + describe('without permission to update', () => { + it('should have the flag name input disabled', () => { + const input = wrapper.find('#feature-flag-name'); + + expect(input.element.disabled).toBe(true); + }); + + it('should have the flag discription text area disabled', () => { + const textarea = wrapper.find('#feature-flag-description'); + + expect(textarea.element.disabled).toBe(true); + }); + + it('should have the scope that cannot be updated be disabled', () => { + const row = wrapper.findAll('.gl-responsive-table-row').at(2); + + expect(row.find(EnvironmentsDropdown).vm.disabled).toBe(true); + expect(row.find(ToggleButton).vm.disabledInput).toBe(true); + expect(row.find('.js-delete-scope').exists()).toBe(false); + }); + }); + }); + + describe('on submit', () => { + const selectFirstRolloutStrategyOption = dropdownIndex => { + wrapper + .findAll('select.js-rollout-strategy') + .at(dropdownIndex) + .findAll('option') + .at(1) + .setSelected(); + }; + + beforeEach(() => { + factory({ + ...requiredProps, + name: 'feature_flag_1', + active: true, + description: 'this is a feature flag', + scopes: [ + { + id: 1, + environmentScope: 'production', + canUpdate: true, + protected: true, + active: false, + rolloutStrategy: ROLLOUT_STRATEGY_ALL_USERS, + rolloutPercentage: DEFAULT_PERCENT_ROLLOUT, + rolloutUserIds: '', + }, + ], + }); + + return wrapper.vm.$nextTick(); + }); + + it('should emit handleSubmit with the updated data', () => { + wrapper.find('#feature-flag-name').setValue('feature_flag_2'); + + return wrapper.vm + .$nextTick() + .then(() => { + wrapper + .find('.js-new-scope-name') + .find(EnvironmentsDropdown) + .vm.$emit('selectEnvironment', 'review'); + + return wrapper.vm.$nextTick(); + }) + .then(() => { + wrapper + .find('.js-add-new-scope') + .find(ToggleButton) + .vm.$emit('change', true); + }) + .then(() => { + wrapper.find(ToggleButton).vm.$emit('change', true); + return wrapper.vm.$nextTick(); + }) + + .then(() => { + selectFirstRolloutStrategyOption(0); + return wrapper.vm.$nextTick(); + }) + .then(() => { + selectFirstRolloutStrategyOption(2); + return wrapper.vm.$nextTick(); + }) + .then(() => { + wrapper.find('.js-rollout-percentage').setValue('55'); + + return wrapper.vm.$nextTick(); + }) + .then(() => { + wrapper.find({ ref: 'submitButton' }).vm.$emit('click'); + + const data = wrapper.emitted().handleSubmit[0][0]; + + expect(data.name).toEqual('feature_flag_2'); + expect(data.description).toEqual('this is a feature flag'); + expect(data.active).toBe(true); + + expect(data.scopes).toEqual([ + { + id: 1, + active: true, + environmentScope: 'production', + canUpdate: true, + protected: true, + rolloutStrategy: ROLLOUT_STRATEGY_PERCENT_ROLLOUT, + rolloutPercentage: '55', + rolloutUserIds: '', + shouldIncludeUserIds: false, + }, + { + id: expect.any(String), + active: false, + environmentScope: 'review', + canUpdate: true, + protected: false, + rolloutStrategy: ROLLOUT_STRATEGY_ALL_USERS, + rolloutPercentage: DEFAULT_PERCENT_ROLLOUT, + rolloutUserIds: '', + }, + { + id: expect.any(String), + active: true, + environmentScope: '', + canUpdate: true, + protected: false, + rolloutStrategy: ROLLOUT_STRATEGY_PERCENT_ROLLOUT, + rolloutPercentage: DEFAULT_PERCENT_ROLLOUT, + rolloutUserIds: '', + shouldIncludeUserIds: false, + }, + ]); + }); + }); + }); + }); + + describe('with strategies', () => { + beforeEach(() => { + Api.fetchFeatureFlagUserLists.mockResolvedValue({ data: [userList] }); + factory({ + ...requiredProps, + name: featureFlag.name, + description: featureFlag.description, + active: true, + version: NEW_VERSION_FLAG, + strategies: [ + { + type: ROLLOUT_STRATEGY_PERCENT_ROLLOUT, + parameters: { percentage: '30' }, + scopes: [], + }, + { + type: ROLLOUT_STRATEGY_ALL_USERS, + parameters: {}, + scopes: [{ environment_scope: 'review/*' }], + }, + ], + }); + }); + + it('should request the user lists on mount', () => { + return wrapper.vm.$nextTick(() => { + expect(Api.fetchFeatureFlagUserLists).toHaveBeenCalledWith('1'); + }); + }); + + it('should show the strategy component', () => { + const strategy = wrapper.find(Strategy); + expect(strategy.exists()).toBe(true); + expect(strategy.props('strategy')).toEqual({ + type: ROLLOUT_STRATEGY_PERCENT_ROLLOUT, + parameters: { percentage: '30' }, + scopes: [], + }); + }); + + it('should show one strategy component per strategy', () => { + expect(wrapper.findAll(Strategy)).toHaveLength(2); + }); + + it('adds an all users strategy when clicking the Add button', () => { + wrapper.find(GlButton).vm.$emit('click'); + + return wrapper.vm.$nextTick().then(() => { + const strategies = wrapper.findAll(Strategy); + + expect(strategies).toHaveLength(3); + expect(strategies.at(2).props('strategy')).toEqual(allUsersStrategy); + }); + }); + + it('should remove a strategy on delete', () => { + const strategy = { + type: ROLLOUT_STRATEGY_PERCENT_ROLLOUT, + parameters: { percentage: '30' }, + scopes: [], + }; + wrapper.find(Strategy).vm.$emit('delete'); + return wrapper.vm.$nextTick().then(() => { + expect(wrapper.findAll(Strategy)).toHaveLength(1); + expect(wrapper.find(Strategy).props('strategy')).not.toEqual(strategy); + }); + }); + + it('should provide the user lists to the strategy', () => { + expect(wrapper.find(Strategy).props('userLists')).toEqual([userList]); + }); + }); +}); |