diff --git a/packages/core/src/api/app/FeatureFlags.test.tsx b/packages/core/src/api/app/FeatureFlags.test.tsx
new file mode 100644
index 0000000000..2159d05e65
--- /dev/null
+++ b/packages/core/src/api/app/FeatureFlags.test.tsx
@@ -0,0 +1,155 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import React, { useContext } from 'react';
+import { render } from '@testing-library/react';
+import {
+ FeatureFlags,
+ FeatureFlagsContext,
+ FeatureFlagsContextProvider,
+} from './FeatureFlags';
+import { FeatureFlagState } from '../apis/definitions/featureFlags';
+
+describe('FeatureFlags', () => {
+ beforeEach(() => {
+ window.localStorage.clear();
+ });
+
+ describe('#get', () => {
+ it('defaults to .NotEnabled', () => {
+ expect(FeatureFlags.get('enable-feature-flag')).toBe(
+ FeatureFlagState.NotEnabled,
+ );
+ });
+
+ it('returns a .Enabled state', () => {
+ FeatureFlags.set('enable-feature-flag', FeatureFlagState.Enabled);
+ expect(FeatureFlags.get('enable-feature-flag')).toBe(
+ FeatureFlagState.Enabled,
+ );
+ });
+
+ it('returns a .NotEnabled state', () => {
+ FeatureFlags.set('enable-feature-flag', FeatureFlagState.NotEnabled);
+ expect(FeatureFlags.get('enable-feature-flag')).toBe(
+ FeatureFlagState.NotEnabled,
+ );
+ });
+ });
+
+ describe('#set', () => {
+ it('fails if name is less than three characters', () => {
+ expect(() => {
+ FeatureFlags.set('ab', FeatureFlagState.NotEnabled);
+ }).toThrow(/minimum length of three characters/i);
+ });
+
+ it('fails if name is greater than 150 characters', () => {
+ expect(() => {
+ FeatureFlags.set(
+ 'loremipsumdolorsitametconsecteturadipiscingelitnuncvitaeportaexaullamcorperturpismaurisutmattisnequemorbisediaculisauguevivamuspulvinarcursuseratblandithendreritquisqueuttinciduntmagnavestibulumblanditaugueat',
+ FeatureFlagState.NotEnabled,
+ );
+ }).toThrow(/not exceed 150 characters/i);
+ });
+
+ it('fails if name does not start with a lowercase letter', () => {
+ expect(() => {
+ FeatureFlags.set('123456789', FeatureFlagState.NotEnabled);
+ }).toThrow(/start with a lowercase letter/i);
+ });
+
+ it('fails if name contains characters other than lowercase letters, numbers and hyphens', () => {
+ expect(() => {
+ FeatureFlags.set('Invalid_Feature_Flag', FeatureFlagState.NotEnabled);
+ }).toThrow(/only contain lowercase letters, numbers and hyphens/i);
+ });
+
+ it('fails if state is not recognized from FeatureFlagState', () => {
+ expect(() => {
+ // @ts-ignore
+ FeatureFlags.set('valid-feature-flag', 'invalid state');
+ }).toThrow(/requires a recognized value from the FeatureFlagState/i);
+ });
+
+ it('sets a feature flag to enabled', () => {
+ expect(window.localStorage.featureFlags).toBeUndefined();
+ FeatureFlags.set('valid-feature-flag', FeatureFlagState.Enabled);
+ expect(window.localStorage.featureFlags).toBe(
+ '{"valid-feature-flag":true}',
+ );
+ });
+
+ it('sets a feature flag to disabled', () => {
+ expect(window.localStorage.featureFlags).toBeUndefined();
+ FeatureFlags.set('valid-feature-flag', FeatureFlagState.NotEnabled);
+ expect(window.localStorage.featureFlags).toBe('{}');
+ });
+
+ it('sets a feature flag to disabled then enabled', () => {
+ expect(window.localStorage.featureFlags).toBeUndefined();
+ FeatureFlags.set('valid-feature-flag', FeatureFlagState.NotEnabled);
+ FeatureFlags.set('valid-feature-flag', FeatureFlagState.Enabled);
+ expect(window.localStorage.featureFlags).toBe(
+ '{"valid-feature-flag":true}',
+ );
+ });
+
+ it('sets multiple feature flags', () => {
+ expect(window.localStorage.featureFlags).toBeUndefined();
+ FeatureFlags.set('valid-feature-flag', FeatureFlagState.Enabled);
+ FeatureFlags.set('another-valid-feature-flag', FeatureFlagState.Enabled);
+ expect(window.localStorage.featureFlags).toBe(
+ '{"valid-feature-flag":true,"another-valid-feature-flag":true}',
+ );
+ });
+ });
+});
+
+describe('FeatureFlagsContext', () => {
+ it('returns an empty list without the context', () => {
+ expect.assertions(1);
+
+ const Component = () => {
+ const { registeredFeatureFlags } = useContext(FeatureFlagsContext);
+ expect(registeredFeatureFlags).toEqual([]);
+ return null;
+ };
+
+ render();
+ });
+
+ it('returns a list of registered feature flags', () => {
+ expect.assertions(1);
+
+ const mockFeatureFlags = [
+ { name: 'feature-flag-one', pluginId: 'plugin-one' },
+ { name: 'feature-flag-two', pluginId: 'plugin-two' },
+ ];
+
+ const Component = () => {
+ const { registeredFeatureFlags } = useContext(FeatureFlagsContext);
+ expect(registeredFeatureFlags).toBe(mockFeatureFlags);
+ return null;
+ };
+
+ render(
+
+
+ ,
+ );
+ });
+});
diff --git a/packages/core/src/api/app/FeatureFlags.tsx b/packages/core/src/api/app/FeatureFlags.tsx
index 74060beb6d..2b543db216 100644
--- a/packages/core/src/api/app/FeatureFlags.tsx
+++ b/packages/core/src/api/app/FeatureFlags.tsx
@@ -56,8 +56,34 @@ class FeatureFlagsImpl implements FeatureFlagsApi {
set(name: FeatureFlagName, state: FeatureFlagState): void {
const flags = this.getUserEnabledFeatureFlags();
- if (state === FeatureFlagState.NotEnabled) flags.delete(name);
- if (state === FeatureFlagState.Enabled) flags.add(name);
+ if (name.length < 3) {
+ throw new Error(
+ 'The `name` argument must have a minimum length of three characters.',
+ );
+ }
+
+ if (name.length > 150) {
+ throw new Error('The `name` argument must not exceed 150 characters.');
+ }
+
+ if (!name.match(/^[a-z]+[a-z0-9-]+$/)) {
+ throw new Error(
+ 'The `name` argument must start with a lowercase letter and only contain lowercase letters, numbers and hyphens.' +
+ 'Examples: feature-flag-one, alpha, release-2020',
+ );
+ }
+
+ if (state === FeatureFlagState.Enabled) {
+ flags.add(name);
+ } else if (state === FeatureFlagState.NotEnabled) {
+ flags.delete(name);
+ } else {
+ throw new Error(
+ 'The `state` argument requires a recognized value from the FeatureFlagState enum. ' +
+ 'Please check the Backstage documentation to see all the available options.' +
+ 'Example values: FeatureFlagState.NotEnabled, FeatureFlagState.Enabled',
+ );
+ }
window.localStorage.setItem(
this.localStorageKey,