494 lines
15 KiB
JavaScript
494 lines
15 KiB
JavaScript
|
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]);
|
||
|
});
|
||
|
});
|
||
|
});
|