Commit 251ffe30 authored by Amy Troschinetz's avatar Amy Troschinetz

Support flexible rollout strategy in the UX

**doc/operations/feature_flags.md:**

Adds documentation for this new feature.

**app/assets/javascripts/feature_flags/constants.js:**

Adds flexible rollout strategy.

**app/assets/javascripts/feature_flags/utils.js:**

Adds flexible rollout strategy.

**app/assets/javascripts/feature_flags/components/strategy.vue:**

- Removes some cruft.
- Adds a Tip to suggest using flexible rollout instead of users rollout.
- Moves form description elements to the correct location.

**app/assets/javascripts/feature_flags/components/
strategy_parameters.vue:**

Adds flexible rollout strategy.

**app/assets/javascripts/feature_flags/components/strategies/
flexible_rollout.vue:**

Implementation of the new flexible rollout strategy.

**app/assets/javascripts/feature_flags/components/strategies/
gitlab_user_list.vue:**

Don't show description when there's an error.

**app/assets/javascripts/feature_flags/components/strategies/
percent_rollout.vue:**

Default to 100%.

**spec/frontend/feature_flags/mock_data.js:**

Adds flexible rollout strategy.

**spec/frontend/feature_flags/components/strategy_spec.js:**

Adds flexible rollout strategy.

**spec/frontend/feature_flags/components/strategies/
flexible_rollout_spec.js:**

Spec tests for the new flexible rollout strategy.

**locale/gitlab.pot:**

I18n updates.

**changelogs/unreleased/feature-flags-flexible-rollout-ux.yml:**

Changelog.
parent f9c4b9d8
<script>
import { GlFormInput, GlFormSelect } from '@gitlab/ui';
import { __ } from '~/locale';
import { PERCENT_ROLLOUT_GROUP_ID } from '../../constants';
import ParameterFormGroup from './parameter_form_group.vue';
export default {
components: {
GlFormInput,
GlFormSelect,
ParameterFormGroup,
},
props: {
strategy: {
required: true,
type: Object,
},
},
i18n: {
percentageDescription: __('Enter a whole number between 0 and 100'),
percentageInvalid: __('Percent rollout must be a whole number between 0 and 100'),
percentageLabel: __('Percentage'),
stickinessDescription: __('Consistency guarantee method'),
stickinessLabel: __('Based on'),
},
stickinessOptions: [
{
value: 'DEFAULT',
text: __('Available ID'),
},
{
value: 'USERID',
text: __('User ID'),
},
{
value: 'SESSIONID',
text: __('Session ID'),
},
{
value: 'RANDOM',
text: __('Random'),
},
],
computed: {
isValid() {
const percentageNum = Number(this.percentage);
return Number.isInteger(percentageNum) && percentageNum >= 0 && percentageNum <= 100;
},
percentage() {
return this.strategy?.parameters?.rollout ?? '100';
},
stickiness() {
return this.strategy?.parameters?.stickiness ?? this.$options.stickinessOptions[0].value;
},
},
methods: {
onPercentageChange(value) {
this.$emit('change', {
parameters: {
groupId: PERCENT_ROLLOUT_GROUP_ID,
rollout: value,
stickiness: this.stickiness,
},
});
},
onStickinessChange(value) {
this.$emit('change', {
parameters: {
groupId: PERCENT_ROLLOUT_GROUP_ID,
rollout: this.percentage,
stickiness: value,
},
});
},
},
};
</script>
<template>
<div class="gl-display-flex">
<div class="gl-mr-7" data-testid="strategy-flexible-rollout-percentage">
<parameter-form-group
:label="$options.i18n.percentageLabel"
:description="isValid ? $options.i18n.percentageDescription : ''"
:invalid-feedback="$options.i18n.percentageInvalid"
:state="isValid"
>
<template #default="{ inputId }">
<div class="gl-display-flex gl-align-items-center">
<gl-form-input
:id="inputId"
:value="percentage"
:state="isValid"
class="rollout-percentage gl-text-right gl-w-9"
type="number"
min="0"
max="100"
@input="onPercentageChange"
/>
<span class="ml-1">%</span>
</div>
</template>
</parameter-form-group>
</div>
<div class="gl-mr-7" data-testid="strategy-flexible-rollout-stickiness">
<parameter-form-group
:label="$options.i18n.stickinessLabel"
:description="$options.i18n.stickinessDescription"
>
<template #default="{ inputId }">
<gl-form-select
:id="inputId"
:value="stickiness"
:options="$options.stickinessOptions"
@change="onStickinessChange"
/>
</template>
</parameter-form-group>
</div>
</div>
</template>
......@@ -49,7 +49,7 @@ export default {
:state="hasUserLists"
:invalid-feedback="$options.translations.rolloutUserListNoListError"
:label="$options.translations.rolloutUserListLabel"
:description="$options.translations.rolloutUserListDescription"
:description="hasUserLists ? $options.translations.rolloutUserListDescription : ''"
>
<template #default="{ inputId }">
<gl-form-select
......
......@@ -15,7 +15,7 @@ export default {
type: Object,
},
},
translations: {
i18n: {
rolloutPercentageDescription: __('Enter a whole number between 0 and 100'),
rolloutPercentageInvalid: s__(
'FeatureFlags|Percent rollout must be a whole number between 0 and 100',
......@@ -24,10 +24,11 @@ export default {
},
computed: {
isValid() {
return Number(this.percentage) >= 0 && Number(this.percentage) <= 100;
const percentageNum = Number(this.percentage);
return Number.isInteger(percentageNum) && percentageNum >= 0 && percentageNum <= 100;
},
percentage() {
return this.strategy?.parameters?.percentage ?? '';
return this.strategy?.parameters?.percentage ?? '100';
},
},
methods: {
......@@ -44,9 +45,9 @@ export default {
</script>
<template>
<parameter-form-group
:label="$options.translations.rolloutPercentageLabel"
:description="$options.translations.rolloutPercentageDescription"
:invalid-feedback="$options.translations.rolloutPercentageInvalid"
:label="$options.i18n.rolloutPercentageLabel"
:description="isValid ? $options.i18n.rolloutPercentageDescription : ''"
:invalid-feedback="$options.i18n.rolloutPercentageInvalid"
:state="isValid"
>
<template #default="{ inputId }">
......
<script>
import Vue from 'vue';
import { isNumber } from 'lodash';
import { GlButton, GlFormSelect, GlFormGroup, GlIcon, GlLink, GlToken } from '@gitlab/ui';
import { GlAlert, GlButton, GlFormSelect, GlFormGroup, GlIcon, GlLink, GlToken } from '@gitlab/ui';
import { s__, __ } from '~/locale';
import { EMPTY_PARAMETERS, STRATEGY_SELECTIONS } from '../constants';
import {
EMPTY_PARAMETERS,
STRATEGY_SELECTIONS,
ROLLOUT_STRATEGY_PERCENT_ROLLOUT,
} from '../constants';
import NewEnvironmentsDropdown from './new_environments_dropdown.vue';
import StrategyParameters from './strategy_parameters.vue';
export default {
components: {
GlAlert,
GlButton,
GlFormGroup,
GlFormSelect,
......@@ -51,13 +56,13 @@ export default {
i18n: {
allEnvironments: __('All environments'),
environmentsLabel: __('Environments'),
rolloutUserListLabel: s__('FeatureFlag|List'),
rolloutUserListDescription: s__('FeatureFlag|Select a user list'),
rolloutUserListNoListError: s__('FeatureFlag|There are no configured user lists'),
strategyTypeDescription: __('Select strategy activation method.'),
strategyTypeDescription: __('Select strategy activation method'),
strategyTypeLabel: s__('FeatureFlag|Type'),
environmentsSelectDescription: s__(
'FeatureFlag|Select the environment scope for this feature flag.',
'FeatureFlag|Select the environment scope for this feature flag',
),
considerFlexibleRollout: s__(
'FeatureFlags|Consider using the more flexible "Percent rollout" strategy instead.',
),
},
......@@ -85,6 +90,9 @@ export default {
filteredEnvironments() {
return this.environments.filter(e => !e.shouldBeDestroyed);
},
isPercentUserRollout() {
return this.formStrategy.name === ROLLOUT_STRATEGY_PERCENT_ROLLOUT;
},
},
methods: {
addEnvironment(environment) {
......@@ -121,73 +129,84 @@ export default {
};
</script>
<template>
<div class="gl-border-t-solid gl-border-t-1 gl-border-t-gray-100 gl-py-6">
<div class="gl-display-flex gl-flex-direction-column gl-md-flex-direction-row flex-md-wrap">
<div class="mr-5">
<gl-form-group :label="$options.i18n.strategyTypeLabel" :label-for="strategyTypeId">
<p class="gl-display-inline-block ">{{ $options.i18n.strategyTypeDescription }}</p>
<gl-link :href="strategyTypeDocsPagePath" target="_blank">
<gl-icon name="question" />
</gl-link>
<gl-form-select
:id="strategyTypeId"
:value="formStrategy.name"
:options="$options.strategies"
@change="onStrategyTypeChange"
<div>
<gl-alert v-if="isPercentUserRollout" variant="tip" :dismissible="false">
{{ $options.i18n.considerFlexibleRollout }}
</gl-alert>
<div class="gl-border-t-solid gl-border-t-1 gl-border-t-gray-100 gl-py-6">
<div class="gl-display-flex gl-flex-direction-column gl-md-flex-direction-row flex-md-wrap">
<div class="mr-5">
<gl-form-group :label="$options.i18n.strategyTypeLabel" :label-for="strategyTypeId">
<template #description>
{{ $options.i18n.strategyTypeDescription }}
<gl-link :href="strategyTypeDocsPagePath" target="_blank">
<gl-icon name="question" />
</gl-link>
</template>
<gl-form-select
:id="strategyTypeId"
:value="formStrategy.name"
:options="$options.strategies"
@change="onStrategyTypeChange"
/>
</gl-form-group>
</div>
<div data-testid="strategy">
<strategy-parameters
:strategy="strategy"
:user-lists="userLists"
@change="onStrategyChange"
/>
</gl-form-group>
</div>
</div>
<div data-testid="strategy">
<strategy-parameters
:strategy="strategy"
:user-lists="userLists"
@change="onStrategyChange"
/>
<div
class="align-self-end align-self-md-stretch order-first offset-md-0 order-md-0 gl-ml-auto"
>
<gl-button
data-testid="delete-strategy-button"
variant="danger"
icon="remove"
@click="$emit('delete')"
/>
</div>
</div>
<div
class="align-self-end align-self-md-stretch order-first offset-md-0 order-md-0 gl-ml-auto"
>
<gl-button
data-testid="delete-strategy-button"
variant="danger"
icon="remove"
@click="$emit('delete')"
/>
</div>
</div>
<label class="gl-display-block" :for="environmentsDropdownId">{{
$options.i18n.environmentsLabel
}}</label>
<p class="gl-display-inline-block">{{ $options.i18n.environmentsSelectDescription }}</p>
<gl-link :href="environmentsScopeDocsPath" target="_blank">
<gl-icon name="question" />
</gl-link>
<div class="gl-display-flex gl-flex-direction-column">
<div
class="gl-display-flex gl-flex-direction-column gl-md-flex-direction-row align-items-start gl-md-align-items-center"
>
<new-environments-dropdown
:id="environmentsDropdownId"
:endpoint="endpoint"
class="gl-mr-3"
@add="addEnvironment"
/>
<span v-if="appliesToAllEnvironments" class="text-secondary gl-mt-3 mt-md-0 ml-md-3">
{{ $options.i18n.allEnvironments }}
</span>
<div v-else class="gl-display-flex gl-align-items-center">
<gl-token
v-for="environment in filteredEnvironments"
:key="environment.id"
class="gl-mt-3 gl-mr-3 mt-md-0 mr-md-0 ml-md-2 rounded-pill"
@close="removeScope(environment)"
>
{{ environment.environmentScope }}
</gl-token>
<label class="gl-display-block" :for="environmentsDropdownId">{{
$options.i18n.environmentsLabel
}}</label>
<div class="gl-display-flex gl-flex-direction-column">
<div
class="gl-display-flex gl-flex-direction-column gl-md-flex-direction-row align-items-start gl-md-align-items-center"
>
<new-environments-dropdown
:id="environmentsDropdownId"
:endpoint="endpoint"
class="gl-mr-3"
@add="addEnvironment"
/>
<span v-if="appliesToAllEnvironments" class="text-secondary gl-mt-3 mt-md-0 ml-md-3">
{{ $options.i18n.allEnvironments }}
</span>
<div v-else class="gl-display-flex gl-align-items-center">
<gl-token
v-for="environment in filteredEnvironments"
:key="environment.id"
class="gl-mt-3 gl-mr-3 mt-md-0 mr-md-0 ml-md-2 rounded-pill"
@close="removeScope(environment)"
>
{{ environment.environmentScope }}
</gl-token>
</div>
</div>
</div>
<span class="gl-display-inline-block gl-py-3">
{{ $options.i18n.environmentsSelectDescription }}
</span>
<gl-link :href="environmentsScopeDocsPath" target="_blank">
<gl-icon name="question" />
</gl-link>
</div>
</div>
</template>
<script>
import {
ROLLOUT_STRATEGY_ALL_USERS,
ROLLOUT_STRATEGY_FLEXIBLE_ROLLOUT,
ROLLOUT_STRATEGY_PERCENT_ROLLOUT,
ROLLOUT_STRATEGY_USER_ID,
ROLLOUT_STRATEGY_GITLAB_USER_LIST,
} from '../constants';
import Default from './strategies/default.vue';
import FlexibleRollout from './strategies/flexible_rollout.vue';
import PercentRollout from './strategies/percent_rollout.vue';
import UsersWithId from './strategies/users_with_id.vue';
import GitlabUserList from './strategies/gitlab_user_list.vue';
const STRATEGIES = Object.freeze({
[ROLLOUT_STRATEGY_ALL_USERS]: Default,
[ROLLOUT_STRATEGY_FLEXIBLE_ROLLOUT]: FlexibleRollout,
[ROLLOUT_STRATEGY_PERCENT_ROLLOUT]: PercentRollout,
[ROLLOUT_STRATEGY_USER_ID]: UsersWithId,
[ROLLOUT_STRATEGY_GITLAB_USER_LIST]: GitlabUserList,
......
......@@ -3,6 +3,7 @@ import { s__ } from '~/locale';
export const ROLLOUT_STRATEGY_ALL_USERS = 'default';
export const ROLLOUT_STRATEGY_PERCENT_ROLLOUT = 'gradualRolloutUserId';
export const ROLLOUT_STRATEGY_FLEXIBLE_ROLLOUT = 'flexibleRollout';
export const ROLLOUT_STRATEGY_USER_ID = 'userWithId';
export const ROLLOUT_STRATEGY_GITLAB_USER_LIST = 'gitlabUserList';
......@@ -34,6 +35,10 @@ export const STRATEGY_SELECTIONS = [
value: ROLLOUT_STRATEGY_ALL_USERS,
text: s__('FeatureFlags|All users'),
},
{
value: ROLLOUT_STRATEGY_FLEXIBLE_ROLLOUT,
text: s__('FeatureFlags|Percent rollout'),
},
{
value: ROLLOUT_STRATEGY_PERCENT_ROLLOUT,
text: s__('FeatureFlags|Percent of users'),
......
......@@ -2,6 +2,7 @@ import { s__, n__, sprintf } from '~/locale';
import {
ALL_ENVIRONMENTS_NAME,
ROLLOUT_STRATEGY_ALL_USERS,
ROLLOUT_STRATEGY_FLEXIBLE_ROLLOUT,
ROLLOUT_STRATEGY_PERCENT_ROLLOUT,
ROLLOUT_STRATEGY_USER_ID,
ROLLOUT_STRATEGY_GITLAB_USER_LIST,
......@@ -12,6 +13,23 @@ const badgeTextByType = {
name: s__('FeatureFlags|All Users'),
parameters: null,
},
[ROLLOUT_STRATEGY_FLEXIBLE_ROLLOUT]: {
name: s__('FeatureFlags|Percent rollout'),
parameters: ({ parameters: { rollout, stickiness } }) => {
switch (stickiness) {
case 'USERID':
return sprintf(s__('FeatureFlags|%{percent} by user ID'), { percent: `${rollout}%` });
case 'SESSIONID':
return sprintf(s__('FeatureFlags|%{percent} by session ID'), { percent: `${rollout}%` });
case 'RANDOM':
return sprintf(s__('FeatureFlags|%{percent} randomly'), { percent: `${rollout}%` });
default:
return sprintf(s__('FeatureFlags|%{percent} by available ID'), {
percent: `${rollout}%`,
});
}
},
},
[ROLLOUT_STRATEGY_PERCENT_ROLLOUT]: {
name: s__('FeatureFlags|Percent of users'),
parameters: ({ parameters: { percentage } }) => `${percentage}%`,
......
---
title: Adds flexible rollout strategy UX and documentation
merge_request: 43611
author:
type: added
......@@ -87,12 +87,49 @@ and clicking **{pencil}** (edit).
Enables the feature for all users. It uses the [`default`](https://unleash.github.io/docs/activation_strategy#default)
Unleash activation strategy.
### Percent Rollout
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/43340) in GitLab 13.5.
Enables the feature for a percentage of page views, with configurable consistency
of behavior. This consistency is also known as stickiness. It uses the
[`flexibleRollout`](https://unleash.github.io/docs/activation_strategy#flexiblerollout)
Unleash activation strategy.
You can configure the consistency to be based on:
- **User IDs**: Each user ID has a consistent behavior, ignoring session IDs.
- **Session IDs**: Each session ID has a consistent behavior, ignoring user IDs.
- **Random**: Consistent behavior is not guaranteed. The feature is enabled for the
selected percentage of page views randomly. User IDs and session IDs are ignored.
- **Available ID**: Consistent behavior is attempted based on the status of the user:
- If the user is logged in, make behavior consistent based on user ID.
- If the user is anonymous, make the behavior consistent based on the session ID.
- If there is no user ID or session ID, then the feature is enabled for the selected
percentage of page view randomly.
For example, set a value of 15% based on **Available ID** to enable the feature for 15% of page views. For
authenticated users this is based on their user ID. For anonymous users with a session ID it would be based on their
session ID instead as they do not have a user ID. Then if no session ID is provided, it falls back to random.
The rollout percentage can be from 0% to 100%.
Selecting a consistency based on User IDs functions the same as the [percent of Users](#percent-of-users) rollout.
CAUTION: **Caution:**
Selecting **Random** provides inconsistent application behavior for individual users.
### Percent of Users
Enables the feature for a percentage of authenticated users. It uses the
[`gradualRolloutUserId`](https://unleash.github.io/docs/activation_strategy#gradualrolloutuserid)
Unleash activation strategy.
NOTE: **Note:**
[Percent rollout](#percent-rollout) with a consistency based on **User IDs** has the same
behavior. It is recommended to use percent rollout instead of percent of users as
it is more flexible.
For example, set a value of 15% to enable the feature for 15% of authenticated users.
The rollout percentage can be from 0% to 100%.
......
......@@ -3897,6 +3897,9 @@ msgstr ""
msgid "Available"
msgstr ""
msgid "Available ID"
msgstr ""
msgid "Available Runners: %{runners}"
msgstr ""
......@@ -4038,6 +4041,9 @@ msgstr ""
msgid "BambooService|You must set up automatic revision labeling and a repository trigger in Bamboo."
msgstr ""
msgid "Based on"
msgstr ""
msgid "Be careful. Changing the project's namespace can have unintended side effects."
msgstr ""
......@@ -6878,6 +6884,9 @@ msgstr ""
msgid "Connection timeout"
msgstr ""
msgid "Consistency guarantee method"
msgstr ""
msgid "Contact sales to upgrade"
msgstr ""
......@@ -11023,6 +11032,18 @@ msgid_plural "FeatureFlags|%d users"
msgstr[0] ""
msgstr[1] ""
msgid "FeatureFlags|%{percent} by available ID"
msgstr ""
msgid "FeatureFlags|%{percent} by session ID"
msgstr ""
msgid "FeatureFlags|%{percent} by user ID"
msgstr ""
msgid "FeatureFlags|%{percent} randomly"
msgstr ""
msgid "FeatureFlags|* (All Environments)"
msgstr ""
......@@ -11053,6 +11074,9 @@ msgstr ""
msgid "FeatureFlags|Configure feature flags"
msgstr ""
msgid "FeatureFlags|Consider using the more flexible \"Percent rollout\" strategy instead."
msgstr ""
msgid "FeatureFlags|Create feature flag"
msgstr ""
......@@ -11170,6 +11194,9 @@ msgstr ""
msgid "FeatureFlags|Percent of users"
msgstr ""
msgid "FeatureFlags|Percent rollout"
msgstr ""
msgid "FeatureFlags|Percent rollout (logged in users)"
msgstr ""
......@@ -11233,7 +11260,7 @@ msgstr ""
msgid "FeatureFlag|Select a user list"
msgstr ""
msgid "FeatureFlag|Select the environment scope for this feature flag."
msgid "FeatureFlag|Select the environment scope for this feature flag"
msgstr ""
msgid "FeatureFlag|There are no configured user lists"
......@@ -18752,6 +18779,9 @@ msgstr ""
msgid "People without permission will never get a notification."
msgstr ""
msgid "Percent rollout must be a whole number between 0 and 100"
msgstr ""
msgid "Percentage"
msgstr ""
......@@ -21233,6 +21263,9 @@ msgstr ""
msgid "Rake Tasks Help"
msgstr ""
msgid "Random"
msgstr ""
msgid "Raw blob request rate limit per minute"
msgstr ""
......@@ -23299,7 +23332,7 @@ msgstr ""
msgid "Select status"
msgstr ""
msgid "Select strategy activation method."
msgid "Select strategy activation method"
msgstr ""
msgid "Select subscription"
......@@ -23521,6 +23554,9 @@ msgstr ""
msgid "Service URL"
msgstr ""
msgid "Session ID"
msgstr ""
msgid "Session duration (minutes)"
msgstr ""
......@@ -28165,6 +28201,9 @@ msgstr ""
msgid "User %{username} was successfully removed."
msgstr ""
msgid "User ID"
msgstr ""
msgid "User OAuth applications"
msgstr ""
......
import { mount } from '@vue/test-utils';
import { GlFormInput, GlFormSelect } from '@gitlab/ui';
import FlexibleRollout from '~/feature_flags/components/strategies/flexible_rollout.vue';
import ParameterFormGroup from '~/feature_flags/components/strategies/parameter_form_group.vue';
import { PERCENT_ROLLOUT_GROUP_ID } from '~/feature_flags/constants';
import { flexibleRolloutStrategy } from '../../mock_data';
const DEFAULT_PROPS = {
strategy: flexibleRolloutStrategy,
};
describe('feature_flags/components/strategies/flexible_rollout.vue', () => {
let wrapper;
let percentageFormGroup;
let percentageInput;
let stickinessFormGroup;
let stickinessSelect;
const factory = (props = {}) =>
mount(FlexibleRollout, { propsData: { ...DEFAULT_PROPS, ...props } });
afterEach(() => {
if (wrapper?.destroy) {
wrapper.destroy();
}
wrapper = null;
});
describe('with valid percentage', () => {
beforeEach(() => {
wrapper = factory();
percentageFormGroup = wrapper
.find('[data-testid="strategy-flexible-rollout-percentage"]')
.find(ParameterFormGroup);
percentageInput = percentageFormGroup.find(GlFormInput);
stickinessFormGroup = wrapper
.find('[data-testid="strategy-flexible-rollout-stickiness"]')
.find(ParameterFormGroup);
stickinessSelect = stickinessFormGroup.find(GlFormSelect);
});
it('displays the current percentage value', () => {
expect(percentageInput.element.value).toBe(flexibleRolloutStrategy.parameters.rollout);
});
it('displays the current stickiness value', () => {
expect(stickinessSelect.element.value).toBe(flexibleRolloutStrategy.parameters.stickiness);
});
it('emits a change when the percentage value changes', async () => {
percentageInput.setValue('75');
await wrapper.vm.$nextTick();
expect(wrapper.emitted('change')).toEqual([
[
{
parameters: {
rollout: '75',
groupId: PERCENT_ROLLOUT_GROUP_ID,
stickiness: flexibleRolloutStrategy.parameters.stickiness,
},
},
],
]);
});
it('emits a change when the stickiness value changes', async () => {
stickinessSelect.setValue('USERID');
await wrapper.vm.$nextTick();
expect(wrapper.emitted('change')).toEqual([
[
{
parameters: {
rollout: flexibleRolloutStrategy.parameters.rollout,
groupId: PERCENT_ROLLOUT_GROUP_ID,
stickiness: 'USERID',
},
},
],
]);
});
it('does not show errors', () => {
expect(percentageFormGroup.attributes('state')).toBe('true');
});
});
describe('with percentage that is out of range', () => {
beforeEach(() => {
wrapper = factory({ strategy: { parameters: { rollout: '101' } } });
});
it('shows errors', () => {
const formGroup = wrapper
.find('[data-testid="strategy-flexible-rollout-percentage"]')
.find(ParameterFormGroup);
expect(formGroup.attributes('state')).toBeUndefined();
});
});
describe('with percentage that is not a whole number', () => {
beforeEach(() => {
wrapper = factory({ strategy: { parameters: { rollout: '3.14' } } });
});
it('shows errors', () => {
const formGroup = wrapper
.find('[data-testid="strategy-flexible-rollout-percentage"]')
.find(ParameterFormGroup);
expect(formGroup.attributes('state')).toBeUndefined();
});
});
});
......@@ -62,4 +62,17 @@ describe('~/feature_flags/components/strategies/percent_rollout.vue', () => {
expect(formGroup.attributes('state')).toBeUndefined();
});
});
describe('with percentage that is not a whole number', () => {
beforeEach(() => {
wrapper = factory({ strategy: { parameters: { percentage: '3.14' } } });
input = wrapper.find(GlFormInput);
formGroup = wrapper.find(ParameterFormGroup);
});
it('shows errors', () => {
expect(formGroup.attributes('state')).toBeUndefined();
});
});
});
import { mount } from '@vue/test-utils';
import { last } from 'lodash';
import { GlFormSelect, GlLink, GlToken, GlButton } from '@gitlab/ui';
import { GlAlert, GlFormSelect, GlLink, GlToken, GlButton } from '@gitlab/ui';
import {
PERCENT_ROLLOUT_GROUP_ID,
ROLLOUT_STRATEGY_ALL_USERS,
ROLLOUT_STRATEGY_PERCENT_ROLLOUT,
ROLLOUT_STRATEGY_FLEXIBLE_ROLLOUT,
ROLLOUT_STRATEGY_USER_ID,
ROLLOUT_STRATEGY_GITLAB_USER_LIST,
} from '~/feature_flags/constants';
......@@ -66,6 +67,7 @@ describe('Feature flags strategy', () => {
name
${ROLLOUT_STRATEGY_ALL_USERS}
${ROLLOUT_STRATEGY_PERCENT_ROLLOUT}
${ROLLOUT_STRATEGY_FLEXIBLE_ROLLOUT}
${ROLLOUT_STRATEGY_USER_ID}
${ROLLOUT_STRATEGY_GITLAB_USER_LIST}
`('with strategy $name', ({ name }) => {
......@@ -91,6 +93,26 @@ describe('Feature flags strategy', () => {
});
});
describe('with the gradualRolloutByUserId strategy', () => {
let strategy;
beforeEach(() => {
strategy = {
name: ROLLOUT_STRATEGY_PERCENT_ROLLOUT,
parameters: { percentage: '50', groupId: 'default' },
scopes: [{ environmentScope: 'production' }],
};
const propsData = { strategy, index: 0, endpoint: '' };
factory({ propsData, provide });
});
it('shows an alert asking users to consider using flexibleRollout instead', () => {
expect(wrapper.find(GlAlert).text()).toContain(
'Consider using the more flexible "Percent rollout" strategy instead.',
);
});
});
describe('with a strategy', () => {
describe('with a single environment scope defined', () => {
let strategy;
......
import {
ROLLOUT_STRATEGY_ALL_USERS,
ROLLOUT_STRATEGY_PERCENT_ROLLOUT,
ROLLOUT_STRATEGY_FLEXIBLE_ROLLOUT,
ROLLOUT_STRATEGY_GITLAB_USER_LIST,
ROLLOUT_STRATEGY_USER_ID,
} from '~/feature_flags/constants';
......@@ -78,6 +79,24 @@ export const featureFlag = {
},
],
},
{
id: 5,
active: true,
environment_scope: 'development',
can_update: true,
protected: false,
created_at: '2019-01-14T06:41:40.987Z',
updated_at: '2019-01-14T06:41:40.987Z',
strategies: [
{
name: ROLLOUT_STRATEGY_FLEXIBLE_ROLLOUT,
parameters: {
rollout: '42',
stickiness: 'DEFAULT',
},
},
],
},
],
};
......@@ -117,6 +136,12 @@ export const percentRolloutStrategy = {
scopes: [],
};
export const flexibleRolloutStrategy = {
name: ROLLOUT_STRATEGY_FLEXIBLE_ROLLOUT,
parameters: { rollout: '50', groupId: 'default', stickiness: 'DEFAULT' },
scopes: [],
};
export const usersWithIdStrategy = {
name: ROLLOUT_STRATEGY_USER_ID,
parameters: { userIds: '1,2,3' },
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment