From d1c42d1254596a98da7b3687e14502ee4c02397a Mon Sep 17 00:00:00 2001 From: Oana Umbres Date: Thu, 6 Apr 2017 10:45:35 +0300 Subject: [PATCH 1/7] Configurable application name --- demo/scripts/directives.js | 5 +- src/featureFlagOverrides.service.js | 9 ++-- src/featureFlags.service.js | 80 +++++++++++++++++++++++++++++ src/featureFlagsConfig.provider.js | 49 ++++++++++++++++++ 4 files changed, 137 insertions(+), 6 deletions(-) create mode 100644 src/featureFlags.service.js create mode 100644 src/featureFlagsConfig.provider.js diff --git a/demo/scripts/directives.js b/demo/scripts/directives.js index c7fe4cf..2c23c55 100644 --- a/demo/scripts/directives.js +++ b/demo/scripts/directives.js @@ -31,6 +31,7 @@ angular.module('my-app') replace: true }; }) - .run(function(featureFlags, $http) { - featureFlags.set($http.get('../data/flags.json')); + .run(function($http, featureFlagConfig) { + featureFlagConfig.setAppName('myapp'); + featureFlagConfig.setInitialFlags($http.get('../data/flags.json')); }); diff --git a/src/featureFlagOverrides.service.js b/src/featureFlagOverrides.service.js index 14b8f96..1601adf 100644 --- a/src/featureFlagOverrides.service.js +++ b/src/featureFlagOverrides.service.js @@ -1,7 +1,5 @@ -angular.module('feature-flags').service('featureFlagOverrides', function($rootElement) { - var appName = $rootElement.attr('ng-app'), - keyPrefix = 'featureFlags.' + appName + '.', - +angular.module('feature-flags').service('featureFlagOverrides', function($rootElement, featureFlagConfig) { + var keyPrefix = 'featureFlags.', localStorageAvailable = (function() { try { localStorage.setItem('featureFlags.availableTest', 'test'); @@ -38,6 +36,9 @@ angular.module('feature-flags').service('featureFlagOverrides', function($rootEl } }; + if (featureFlagConfig.getAppName().length > 0) { + keyPrefix += featureFlagConfig.getAppName() + '.'; + } return { isPresent: function(key) { var value = get(key); diff --git a/src/featureFlags.service.js b/src/featureFlags.service.js new file mode 100644 index 0000000..50acaf1 --- /dev/null +++ b/src/featureFlags.service.js @@ -0,0 +1,80 @@ +angular.module('feature-flags').service('featureFlags', function($q, featureFlagOverrides, featureFlagConfig) { + var serverFlagCache = {}, + flags = [], + + resolve = function(val) { + var deferred = $q.defer(); + deferred.resolve(val); + return deferred.promise; + }, + + isOverridden = function(key) { + return featureFlagOverrides.isPresent(key); + }, + + isOn = function(key) { + return isOverridden(key) ? featureFlagOverrides.get(key) === 'true' : serverFlagCache[key]; + }, + + isOnByDefault = function(key) { + return serverFlagCache[key]; + }, + + updateFlagsAndGetAll = function(newFlags) { + newFlags.forEach(function(flag) { + serverFlagCache[flag.key] = flag.active; + flag.active = isOn(flag.key); + }); + angular.copy(newFlags, flags); + + return flags; + }, + + updateFlagsWithPromise = function(promise) { + return promise.then(function(value) { + return updateFlagsAndGetAll(value.data || value); + }); + }, + + get = function() { + return flags; + }, + + set = function(newFlags) { + return angular.isArray(newFlags) ? resolve(updateFlagsAndGetAll(newFlags)) : updateFlagsWithPromise(newFlags); + }, + + enable = function(flag) { + flag.active = true; + featureFlagOverrides.set(flag.key, true); + }, + + disable = function(flag) { + flag.active = false; + featureFlagOverrides.set(flag.key, false); + }, + + reset = function(flag) { + flag.active = serverFlagCache[flag.key]; + featureFlagOverrides.remove(flag.key); + }, + + init = function() { + var iniFlags = featureFlagConfig.getInitialFlags(); + if (iniFlags) { + set(iniFlags); + } + }; + init(); + + return { + set: set, + get: get, + enable: enable, + disable: disable, + reset: reset, + isOn: isOn, + isOnByDefault: isOnByDefault, + isOverridden: isOverridden + }; +}); diff --git a/src/featureFlagsConfig.provider.js b/src/featureFlagsConfig.provider.js new file mode 100644 index 0000000..8cbaedd --- /dev/null +++ b/src/featureFlagsConfig.provider.js @@ -0,0 +1,49 @@ +function FeatureFlagConfig(initialFlags, userAppName) { + var initFlags = [], + appName = '', + + getInitialFlags = function() { + return initFlags; + }, + setInitialFlags = function(value) { + initFlags = value; + }, + getAppName = function() { + return appName; + }, + setAppName = function(value) { + appName = value; + }, + init = function() { + if (initialFlags) { + initFlags = initialFlags; + } + if (userAppName) { + appName = userAppName; + } + }; + init(); + + return { + getInitialFlags: getInitialFlags, + setInitialFlags: setInitialFlags, + getAppName: getAppName, + setAppName: setAppName + }; +} + +angular.module('feature-flags').provider('featureFlagConfig', function() { + var initialFlags = [], + appName = ''; + + this.setInitialFlags = function(flags) { + initialFlags = flags; + }; + + this.setAppName = function(name) { + appName = name; + }; + this.$get = function() { + return new FeatureFlagConfig(initialFlags, appName); + }; +}); From 54a9cd11893d36e1317e98465281390d8fe0f4aa Mon Sep 17 00:00:00 2001 From: Oana Umbres Date: Thu, 6 Apr 2017 10:45:57 +0300 Subject: [PATCH 2/7] Configurable application name --- src/featureFlags.provider.js | 91 ------------------------------------ 1 file changed, 91 deletions(-) delete mode 100644 src/featureFlags.provider.js diff --git a/src/featureFlags.provider.js b/src/featureFlags.provider.js deleted file mode 100644 index db7ac99..0000000 --- a/src/featureFlags.provider.js +++ /dev/null @@ -1,91 +0,0 @@ -function FeatureFlags($q, featureFlagOverrides, initialFlags) { - var serverFlagCache = {}, - flags = [], - - resolve = function(val) { - var deferred = $q.defer(); - deferred.resolve(val); - return deferred.promise; - }, - - isOverridden = function(key) { - return featureFlagOverrides.isPresent(key); - }, - - isOn = function(key) { - return isOverridden(key) ? featureFlagOverrides.get(key) === 'true' : serverFlagCache[key]; - }, - - isOnByDefault = function(key) { - return serverFlagCache[key]; - }, - - updateFlagsAndGetAll = function(newFlags) { - newFlags.forEach(function(flag) { - serverFlagCache[flag.key] = flag.active; - flag.active = isOn(flag.key); - }); - angular.copy(newFlags, flags); - - return flags; - }, - - updateFlagsWithPromise = function(promise) { - return promise.then(function(value) { - return updateFlagsAndGetAll(value.data || value); - }); - }, - - get = function() { - return flags; - }, - - set = function(newFlags) { - return angular.isArray(newFlags) ? resolve(updateFlagsAndGetAll(newFlags)) : updateFlagsWithPromise(newFlags); - }, - - enable = function(flag) { - flag.active = true; - featureFlagOverrides.set(flag.key, true); - }, - - disable = function(flag) { - flag.active = false; - featureFlagOverrides.set(flag.key, false); - }, - - reset = function(flag) { - flag.active = serverFlagCache[flag.key]; - featureFlagOverrides.remove(flag.key); - }, - - init = function() { - if (initialFlags) { - set(initialFlags); - } - }; - init(); - - return { - set: set, - get: get, - enable: enable, - disable: disable, - reset: reset, - isOn: isOn, - isOnByDefault: isOnByDefault, - isOverridden: isOverridden - }; -} - -angular.module('feature-flags').provider('featureFlags', function() { - var initialFlags = []; - - this.setInitialFlags = function(flags) { - initialFlags = flags; - }; - - this.$get = function($q, featureFlagOverrides) { - return new FeatureFlags($q, featureFlagOverrides, initialFlags); - }; -}); From 66e1d8f8636d8f405ee95177d369d8fec23d0481 Mon Sep 17 00:00:00 2001 From: Oana Umbres Date: Thu, 6 Apr 2017 13:08:45 +0300 Subject: [PATCH 3/7] Fixed tests --- test/featureFlagOverrides.service.spec.js | 108 ++++++++++-------- ...r.spec.js => featureFlags.service.spec.js} | 4 +- test/featureFlagsConfig.provider.spec.js | 69 +++++++++++ 3 files changed, 130 insertions(+), 51 deletions(-) rename test/{featureFlags.provider.spec.js => featureFlags.service.spec.js} (99%) create mode 100644 test/featureFlagsConfig.provider.spec.js diff --git a/test/featureFlagOverrides.service.spec.js b/test/featureFlagOverrides.service.spec.js index 6291d26..743dafb 100644 --- a/test/featureFlagOverrides.service.spec.js +++ b/test/featureFlagOverrides.service.spec.js @@ -1,31 +1,41 @@ -(function(angular) { +(function (angular) { 'use strict'; var module = angular.mock.module, inject = angular.mock.inject; - describe('Service: featureFlagOverrides', function() { - var service, appName = ''; + describe('Service: featureFlagOverrides', function () { + var service, appName = 'myapp'; - beforeEach(module('feature-flags')); + beforeEach(module('feature-flags', function ($provide) { + $provide.provider('featureFlagConfig', function () { + this.$get = function () { + return { + getAppName: function () { + return appName; + } + }; + } + }); + })); - beforeEach(inject(function(featureFlagOverrides) { + beforeEach(inject(function (featureFlagOverrides) { service = featureFlagOverrides; })); - describe('when I set an override', function() { - beforeEach(function() { + describe('when I set an override', function () { + beforeEach(function () { spyOn(localStorage, 'setItem'); service.set('FLAG_KEY', 'VALUE'); }); - it('should save the value', function() { + it('should save the value', function () { expect(localStorage.setItem).toHaveBeenCalledWith('featureFlags.' + appName + '.' + 'FLAG_KEY', 'VALUE'); }); }); - describe('when I set a hash of overrides', function() { - beforeEach(function() { + describe('when I set a hash of overrides', function () { + beforeEach(function () { spyOn(localStorage, 'setItem'); service.set({ 'FLAG_KEY_1': 'VALUE_1', @@ -34,59 +44,59 @@ }); }); - it('should save the values', function() { + it('should save the values', function () { expect(localStorage.setItem).toHaveBeenCalledWith('featureFlags.' + appName + '.' + 'FLAG_KEY_1', 'VALUE_1'); expect(localStorage.setItem).toHaveBeenCalledWith('featureFlags.' + appName + '.' + 'FLAG_KEY_2', 'VALUE_2'); expect(localStorage.setItem).toHaveBeenCalledWith('featureFlags.' + appName + '.' + 'FLAG_KEY_3', 'VALUE_3'); }); }); - describe('when I get an override', function() { - beforeEach(function() { + describe('when I get an override', function () { + beforeEach(function () { spyOn(localStorage, 'getItem'); service.get('FLAG_KEY'); }); - it('should get the value', function() { + it('should get the value', function () { expect(localStorage.getItem).toHaveBeenCalledWith('featureFlags.' + appName + '.' + 'FLAG_KEY'); }); }); - describe('when I remove an override', function() { - beforeEach(function() { + describe('when I remove an override', function () { + beforeEach(function () { spyOn(localStorage, 'removeItem'); service.remove('FLAG_KEY'); }); - it('should delete the value', function() { + it('should delete the value', function () { expect(localStorage.removeItem).toHaveBeenCalledWith('featureFlags.' + appName + '.' + 'FLAG_KEY'); }); }); - describe('when I check the state of an override', function() { - describe('if there is one', function() { - beforeEach(function() { + describe('when I check the state of an override', function () { + describe('if there is one', function () { + beforeEach(function () { spyOn(localStorage, 'getItem').andReturn('true'); }); - it('should return true if there is a value', function() { + it('should return true if there is a value', function () { expect(service.isPresent('FLAG_KEY')).toBe(true); }); }); - describe('if there is not one', function() { - beforeEach(function() { + describe('if there is not one', function () { + beforeEach(function () { spyOn(localStorage, 'getItem').andReturn(null); }); - it('should return false if there is no value', function() { + it('should return false if there is no value', function () { expect(service.isPresent('FLAG_KEY')).toBe(false); }); }); }); - describe('when I have a series of overrides and then clear them', function() { - beforeEach(function() { + describe('when I have a series of overrides and then clear them', function () { + beforeEach(function () { spyOn(localStorage, 'removeItem'); localStorage.setItem('someOtherData', true); service.set('FLAG_KEY_1', 'VALUE'); @@ -95,26 +105,26 @@ service.reset(); }); - afterEach(function() { + afterEach(function () { localStorage.clear(); }); - it('should remove all feature flags from local storage', function() { + it('should remove all feature flags from local storage', function () { expect(localStorage.removeItem).toHaveBeenCalledWith('featureFlags.' + appName + '.' + 'FLAG_KEY_1'); expect(localStorage.removeItem).toHaveBeenCalledWith('featureFlags.' + appName + '.' + 'FLAG_KEY_2'); expect(localStorage.removeItem).toHaveBeenCalledWith('featureFlags.' + appName + '.' + 'FLAG_KEY_3'); }); - it('should not remove unrelated local storage items', function() { + it('should not remove unrelated local storage items', function () { expect(localStorage.removeItem).not.toHaveBeenCalledWith('someOtherData'); }); }); }); - describe('Service: featureFlagOverrides when localStorage is not available', function() { + describe('Service: featureFlagOverrides when localStorage is not available', function () { var service; - beforeEach(function() { + beforeEach(function () { spyOn(localStorage, 'setItem').andThrow(); spyOn(localStorage, 'getItem').andThrow(); spyOn(localStorage, 'removeItem').andThrow(); @@ -122,65 +132,65 @@ beforeEach(module('feature-flags')); - beforeEach(inject(function(featureFlagOverrides) { + beforeEach(inject(function (featureFlagOverrides) { service = featureFlagOverrides; localStorage.setItem.reset(); })); - describe('when I set an override', function() { - beforeEach(function() { + describe('when I set an override', function () { + beforeEach(function () { service.set('FLAG_KEY', 'VALUE'); }); - it('do nothing', function() { + it('do nothing', function () { expect(localStorage.setItem).not.toHaveBeenCalled(); }); }); - describe('when I set a hash of overrides', function() { - beforeEach(function() { + describe('when I set a hash of overrides', function () { + beforeEach(function () { service.set({ 'FLAG_KEY_1': 'VALUE_1' }); }); - it('do nothing', function() { + it('do nothing', function () { expect(localStorage.setItem).not.toHaveBeenCalled(); }); }); - describe('when I get an override', function() { - beforeEach(function() { + describe('when I get an override', function () { + beforeEach(function () { service.get('FLAG_KEY'); }); - it('should do nothing', function() { + it('should do nothing', function () { expect(localStorage.getItem).not.toHaveBeenCalled(); }); }); - describe('when I remove an override', function() { - beforeEach(function() { + describe('when I remove an override', function () { + beforeEach(function () { service.remove('FLAG_KEY'); }); - it('should do nothing', function() { + it('should do nothing', function () { expect(localStorage.removeItem).not.toHaveBeenCalled(); }); }); - describe('when I clear all overrides', function() { - beforeEach(function() { + describe('when I clear all overrides', function () { + beforeEach(function () { service.reset(); }); - it('should do nothing', function() { + it('should do nothing', function () { expect(localStorage.removeItem).not.toHaveBeenCalled(); }); }); - describe('when I check the state of an override', function() { - it('should return false', function() { + describe('when I check the state of an override', function () { + it('should return false', function () { expect(service.isPresent('FLAG_KEY')).toBeFalsy(); }); }); diff --git a/test/featureFlags.provider.spec.js b/test/featureFlags.service.spec.js similarity index 99% rename from test/featureFlags.provider.spec.js rename to test/featureFlags.service.spec.js index 99df006..d84c83a 100644 --- a/test/featureFlags.provider.spec.js +++ b/test/featureFlags.service.spec.js @@ -354,7 +354,7 @@ }); }); - describe('Provider: featureFlags', function() { + /*describe('Provider: featureFlags', function() { var featureFlags, flags = [{ active: true, @@ -391,5 +391,5 @@ expect(featureFlags.get()).toEqual(flags); }); }); - }); + });*/ }(window.angular)); diff --git a/test/featureFlagsConfig.provider.spec.js b/test/featureFlagsConfig.provider.spec.js new file mode 100644 index 0000000..433835d --- /dev/null +++ b/test/featureFlagsConfig.provider.spec.js @@ -0,0 +1,69 @@ +(function (angular) { + 'use strict'; + + var module = angular.mock.module, + inject = angular.mock.inject; + + describe('Provider: featureFlagsConfig', function () { + var featureFlags, + flags = [{ + active: true, + key: 'FLAG_KEY' + }, { + active: false, + key: 'FLAG_KEY_2' + }], + appName='myapp'; + + describe('When no flags are set in the config phase', function () { + beforeEach(module('feature-flags', function ($provide) { + $provide.provider('featureFlagConfig', function () { + this.$get = function () { + return { + getInitialFlags: function () { + return []; + }, + getAppName : function(){ + return appName; + } + }; + } + }); + })); + + beforeEach(inject(function (_featureFlags_) { + featureFlags = _featureFlags_; + })); + + it('should return an empty array for current feature flags', function () { + expect(featureFlags.get()).toEqual([]); + }); + }); + + describe('When flags are set in the config phase', function () { + beforeEach(module('feature-flags', function ($provide) { + $provide.provider('featureFlagConfig', function () { + this.$get = function () { + return { + getInitialFlags: function () { + return flags; + }, + getAppName : function(){ + return appName; + } + }; + } + }); + })); + + beforeEach(inject(function (_featureFlags_) { + featureFlags = _featureFlags_; + })); + + it('should init the flags with the ones set in the config phase', function () { + expect(featureFlags.get()).toEqual(flags); + }); + }); + }); + +}(window.angular)); From 10e7ce1292713b33233b17b5699f71fa96025139 Mon Sep 17 00:00:00 2001 From: Oana Umbres Date: Thu, 6 Apr 2017 15:05:54 +0300 Subject: [PATCH 4/7] Updated tests --- test/featureFlagOverrides.service.spec.js | 114 +++++++++++----------- test/featureFlags.service.spec.js | 41 ++++++-- test/featureFlagsConfig.provider.spec.js | 61 ++++-------- 3 files changed, 113 insertions(+), 103 deletions(-) diff --git a/test/featureFlagOverrides.service.spec.js b/test/featureFlagOverrides.service.spec.js index 743dafb..f744c24 100644 --- a/test/featureFlagOverrides.service.spec.js +++ b/test/featureFlagOverrides.service.spec.js @@ -1,41 +1,43 @@ -(function (angular) { +(function(angular) { 'use strict'; var module = angular.mock.module, inject = angular.mock.inject; - describe('Service: featureFlagOverrides', function () { + describe('Service: featureFlagOverrides', function() { var service, appName = 'myapp'; - beforeEach(module('feature-flags', function ($provide) { - $provide.provider('featureFlagConfig', function () { - this.$get = function () { - return { - getAppName: function () { - return appName; - } - }; - } + beforeEach(module('feature-flags', function($provide) { + $provide.provider('featureFlagConfig', function() { + return { + $get: function() { + return { + getAppName: function() { + return appName; + } + }; + } + }; }); })); - beforeEach(inject(function (featureFlagOverrides) { + beforeEach(inject(function(featureFlagOverrides) { service = featureFlagOverrides; })); - describe('when I set an override', function () { - beforeEach(function () { + describe('when I set an override', function() { + beforeEach(function() { spyOn(localStorage, 'setItem'); service.set('FLAG_KEY', 'VALUE'); }); - it('should save the value', function () { + it('should save the value', function() { expect(localStorage.setItem).toHaveBeenCalledWith('featureFlags.' + appName + '.' + 'FLAG_KEY', 'VALUE'); }); }); - describe('when I set a hash of overrides', function () { - beforeEach(function () { + describe('when I set a hash of overrides', function() { + beforeEach(function() { spyOn(localStorage, 'setItem'); service.set({ 'FLAG_KEY_1': 'VALUE_1', @@ -44,59 +46,59 @@ }); }); - it('should save the values', function () { + it('should save the values', function() { expect(localStorage.setItem).toHaveBeenCalledWith('featureFlags.' + appName + '.' + 'FLAG_KEY_1', 'VALUE_1'); expect(localStorage.setItem).toHaveBeenCalledWith('featureFlags.' + appName + '.' + 'FLAG_KEY_2', 'VALUE_2'); expect(localStorage.setItem).toHaveBeenCalledWith('featureFlags.' + appName + '.' + 'FLAG_KEY_3', 'VALUE_3'); }); }); - describe('when I get an override', function () { - beforeEach(function () { + describe('when I get an override', function() { + beforeEach(function() { spyOn(localStorage, 'getItem'); service.get('FLAG_KEY'); }); - it('should get the value', function () { + it('should get the value', function() { expect(localStorage.getItem).toHaveBeenCalledWith('featureFlags.' + appName + '.' + 'FLAG_KEY'); }); }); - describe('when I remove an override', function () { - beforeEach(function () { + describe('when I remove an override', function() { + beforeEach(function() { spyOn(localStorage, 'removeItem'); service.remove('FLAG_KEY'); }); - it('should delete the value', function () { + it('should delete the value', function() { expect(localStorage.removeItem).toHaveBeenCalledWith('featureFlags.' + appName + '.' + 'FLAG_KEY'); }); }); - describe('when I check the state of an override', function () { - describe('if there is one', function () { - beforeEach(function () { + describe('when I check the state of an override', function() { + describe('if there is one', function() { + beforeEach(function() { spyOn(localStorage, 'getItem').andReturn('true'); }); - it('should return true if there is a value', function () { + it('should return true if there is a value', function() { expect(service.isPresent('FLAG_KEY')).toBe(true); }); }); - describe('if there is not one', function () { - beforeEach(function () { + describe('if there is not one', function() { + beforeEach(function() { spyOn(localStorage, 'getItem').andReturn(null); }); - it('should return false if there is no value', function () { + it('should return false if there is no value', function() { expect(service.isPresent('FLAG_KEY')).toBe(false); }); }); }); - describe('when I have a series of overrides and then clear them', function () { - beforeEach(function () { + describe('when I have a series of overrides and then clear them', function() { + beforeEach(function() { spyOn(localStorage, 'removeItem'); localStorage.setItem('someOtherData', true); service.set('FLAG_KEY_1', 'VALUE'); @@ -105,26 +107,26 @@ service.reset(); }); - afterEach(function () { + afterEach(function() { localStorage.clear(); }); - it('should remove all feature flags from local storage', function () { + it('should remove all feature flags from local storage', function() { expect(localStorage.removeItem).toHaveBeenCalledWith('featureFlags.' + appName + '.' + 'FLAG_KEY_1'); expect(localStorage.removeItem).toHaveBeenCalledWith('featureFlags.' + appName + '.' + 'FLAG_KEY_2'); expect(localStorage.removeItem).toHaveBeenCalledWith('featureFlags.' + appName + '.' + 'FLAG_KEY_3'); }); - it('should not remove unrelated local storage items', function () { + it('should not remove unrelated local storage items', function() { expect(localStorage.removeItem).not.toHaveBeenCalledWith('someOtherData'); }); }); }); - describe('Service: featureFlagOverrides when localStorage is not available', function () { + describe('Service: featureFlagOverrides when localStorage is not available', function() { var service; - beforeEach(function () { + beforeEach(function() { spyOn(localStorage, 'setItem').andThrow(); spyOn(localStorage, 'getItem').andThrow(); spyOn(localStorage, 'removeItem').andThrow(); @@ -132,65 +134,65 @@ beforeEach(module('feature-flags')); - beforeEach(inject(function (featureFlagOverrides) { + beforeEach(inject(function(featureFlagOverrides) { service = featureFlagOverrides; localStorage.setItem.reset(); })); - describe('when I set an override', function () { - beforeEach(function () { + describe('when I set an override', function() { + beforeEach(function() { service.set('FLAG_KEY', 'VALUE'); }); - it('do nothing', function () { + it('do nothing', function() { expect(localStorage.setItem).not.toHaveBeenCalled(); }); }); - describe('when I set a hash of overrides', function () { - beforeEach(function () { + describe('when I set a hash of overrides', function() { + beforeEach(function() { service.set({ 'FLAG_KEY_1': 'VALUE_1' }); }); - it('do nothing', function () { + it('do nothing', function() { expect(localStorage.setItem).not.toHaveBeenCalled(); }); }); - describe('when I get an override', function () { - beforeEach(function () { + describe('when I get an override', function() { + beforeEach(function() { service.get('FLAG_KEY'); }); - it('should do nothing', function () { + it('should do nothing', function() { expect(localStorage.getItem).not.toHaveBeenCalled(); }); }); - describe('when I remove an override', function () { - beforeEach(function () { + describe('when I remove an override', function() { + beforeEach(function() { service.remove('FLAG_KEY'); }); - it('should do nothing', function () { + it('should do nothing', function() { expect(localStorage.removeItem).not.toHaveBeenCalled(); }); }); - describe('when I clear all overrides', function () { - beforeEach(function () { + describe('when I clear all overrides', function() { + beforeEach(function() { service.reset(); }); - it('should do nothing', function () { + it('should do nothing', function() { expect(localStorage.removeItem).not.toHaveBeenCalled(); }); }); - describe('when I check the state of an override', function () { - it('should return false', function () { + describe('when I check the state of an override', function() { + it('should return false', function() { expect(service.isPresent('FLAG_KEY')).toBeFalsy(); }); }); diff --git a/test/featureFlags.service.spec.js b/test/featureFlags.service.spec.js index d84c83a..258e26e 100644 --- a/test/featureFlags.service.spec.js +++ b/test/featureFlags.service.spec.js @@ -354,7 +354,7 @@ }); }); - /*describe('Provider: featureFlags', function() { + describe('Service with provider config: featureFlags', function() { var featureFlags, flags = [{ active: true, @@ -362,11 +362,25 @@ }, { active: false, key: 'FLAG_KEY_2' - }]; + }], + appName = 'myapp'; describe('When no flags are set in the config phase', function() { - beforeEach(module('feature-flags', function(featureFlagsProvider) { - featureFlagsProvider.setInitialFlags(null); + beforeEach(module('feature-flags', function($provide) { + $provide.provider('featureFlagConfig', function() { + return { + $get: function() { + return { + getInitialFlags: function() { + return []; + }, + getAppName: function() { + return appName; + } + }; + } + }; + }); })); beforeEach(inject(function(_featureFlags_) { @@ -379,8 +393,21 @@ }); describe('When flags are set in the config phase', function() { - beforeEach(module('feature-flags', function(featureFlagsProvider) { - featureFlagsProvider.setInitialFlags(flags); + beforeEach(module('feature-flags', function($provide) { + $provide.provider('featureFlagConfig', function() { + return { + $get: function() { + return { + getInitialFlags: function() { + return flags; + }, + getAppName: function() { + return appName; + } + }; + } + }; + }); })); beforeEach(inject(function(_featureFlags_) { @@ -391,5 +418,5 @@ expect(featureFlags.get()).toEqual(flags); }); }); - });*/ + }); }(window.angular)); diff --git a/test/featureFlagsConfig.provider.spec.js b/test/featureFlagsConfig.provider.spec.js index 433835d..0c5fdf4 100644 --- a/test/featureFlagsConfig.provider.spec.js +++ b/test/featureFlagsConfig.provider.spec.js @@ -1,69 +1,50 @@ -(function (angular) { +(function(angular) { 'use strict'; var module = angular.mock.module, inject = angular.mock.inject; - describe('Provider: featureFlagsConfig', function () { + describe('Provider: featureFlags', function() { var featureFlags, + featureFlagsConfig, flags = [{ active: true, key: 'FLAG_KEY' }, { active: false, key: 'FLAG_KEY_2' - }], - appName='myapp'; + }]; - describe('When no flags are set in the config phase', function () { - beforeEach(module('feature-flags', function ($provide) { - $provide.provider('featureFlagConfig', function () { - this.$get = function () { - return { - getInitialFlags: function () { - return []; - }, - getAppName : function(){ - return appName; - } - }; - } - }); + describe('When no flags are set in the config phase', function() { + beforeEach(module('feature-flags', function(featureFlagConfigProvider) { + featureFlagConfigProvider.setInitialFlags(null); + featureFlagConfigProvider.setAppName(null); })); - beforeEach(inject(function (_featureFlags_) { - featureFlags = _featureFlags_; + beforeEach(inject(function(_featureFlagConfig_) { + featureFlagsConfig = _featureFlagConfig_; })); - it('should return an empty array for current feature flags', function () { - expect(featureFlags.get()).toEqual([]); + it('should return an empty array for current feature flags', function() { + expect(featureFlagsConfig).not.toBeUndefined(); + expect(featureFlagsConfig.getInitialFlags()).toEqual([]); + expect(featureFlagsConfig.getAppName()).toEqual(''); }); }); - describe('When flags are set in the config phase', function () { - beforeEach(module('feature-flags', function ($provide) { - $provide.provider('featureFlagConfig', function () { - this.$get = function () { - return { - getInitialFlags: function () { - return flags; - }, - getAppName : function(){ - return appName; - } - }; - } - }); + xdescribe('When flags are set in the config phase', function() { + beforeEach(module('feature-flags', function(featureFlagConfigProvider) { + featureFlagConfigProvider.setInitialFlags(flags); + featureFlagConfigProvider.setAppName('myapp'); })); - beforeEach(inject(function (_featureFlags_) { - featureFlags = _featureFlags_; + beforeEach(inject(function(_featureFlagConfig_) { + featureFlagsConfig = _featureFlagConfig_; })); - it('should init the flags with the ones set in the config phase', function () { + it('should init the flags with the ones set in the config phase', function() { expect(featureFlags.get()).toEqual(flags); }); }); }); - }(window.angular)); From f00201692cdb52701210867b695ed432a85506fb Mon Sep 17 00:00:00 2001 From: Oana Umbres Date: Thu, 6 Apr 2017 16:03:42 +0300 Subject: [PATCH 5/7] Improved coverage --- test/featureFlags.service.spec.js | 2 +- test/featureFlagsConfig.provider.spec.js | 57 ++++++++++++++++-------- 2 files changed, 39 insertions(+), 20 deletions(-) diff --git a/test/featureFlags.service.spec.js b/test/featureFlags.service.spec.js index 258e26e..e999a4e 100644 --- a/test/featureFlags.service.spec.js +++ b/test/featureFlags.service.spec.js @@ -372,7 +372,7 @@ $get: function() { return { getInitialFlags: function() { - return []; + return null; }, getAppName: function() { return appName; diff --git a/test/featureFlagsConfig.provider.spec.js b/test/featureFlagsConfig.provider.spec.js index 0c5fdf4..b221d57 100644 --- a/test/featureFlagsConfig.provider.spec.js +++ b/test/featureFlagsConfig.provider.spec.js @@ -1,12 +1,12 @@ -(function(angular) { +(function (angular) { 'use strict'; var module = angular.mock.module, inject = angular.mock.inject; - describe('Provider: featureFlags', function() { - var featureFlags, - featureFlagsConfig, + describe('Provider: featureFlags', function () { + var appName = 'myapp', + featureFlagConfig, flags = [{ active: true, key: 'FLAG_KEY' @@ -15,36 +15,55 @@ key: 'FLAG_KEY_2' }]; - describe('When no flags are set in the config phase', function() { - beforeEach(module('feature-flags', function(featureFlagConfigProvider) { + describe('When no flags are set in the config phase', function () { + beforeEach(module('feature-flags', function (featureFlagConfigProvider) { featureFlagConfigProvider.setInitialFlags(null); featureFlagConfigProvider.setAppName(null); })); - beforeEach(inject(function(_featureFlagConfig_) { - featureFlagsConfig = _featureFlagConfig_; + beforeEach(inject(function (_featureFlagConfig_) { + featureFlagConfig = _featureFlagConfig_; })); - it('should return an empty array for current feature flags', function() { - expect(featureFlagsConfig).not.toBeUndefined(); - expect(featureFlagsConfig.getInitialFlags()).toEqual([]); - expect(featureFlagsConfig.getAppName()).toEqual(''); + it('should return an empty array for current feature flags', function () { + expect(featureFlagConfig).not.toBeUndefined(); + expect(featureFlagConfig.getInitialFlags()).toEqual([]); + expect(featureFlagConfig.getAppName()).toEqual(''); }); }); - xdescribe('When flags are set in the config phase', function() { - beforeEach(module('feature-flags', function(featureFlagConfigProvider) { + describe('When flags are set in the config phase', function () { + beforeEach(module('feature-flags', function (featureFlagConfigProvider) { featureFlagConfigProvider.setInitialFlags(flags); - featureFlagConfigProvider.setAppName('myapp'); + featureFlagConfigProvider.setAppName(appName); })); - beforeEach(inject(function(_featureFlagConfig_) { - featureFlagsConfig = _featureFlagConfig_; + beforeEach(inject(function (_featureFlagConfig_) { + featureFlagConfig = _featureFlagConfig_; })); - it('should init the flags with the ones set in the config phase', function() { - expect(featureFlags.get()).toEqual(flags); + it('should init the flags with the ones set in the config phase', function () { + expect(featureFlagConfig).not.toBeUndefined(); + expect(featureFlagConfig.getInitialFlags()).toEqual(flags); + expect(featureFlagConfig.getAppName()).toEqual(appName); }); }); + + describe('When flags are set in the run phase', function () { + beforeEach(module('feature-flags', function (featureFlagConfigProvider) { + })); + beforeEach(inject(function (_featureFlagConfig_) { + featureFlagConfig = _featureFlagConfig_; + })); + + it('should init the flags with the ones set in the run phase', function () { + expect(featureFlagConfig).not.toBeUndefined(); + featureFlagConfig.setInitialFlags(flags); + featureFlagConfig.setAppName(appName); + expect(featureFlagConfig.getInitialFlags()).toEqual(flags); + expect(featureFlagConfig.getAppName()).toEqual(appName); + }); + }); + }); }(window.angular)); From 7395961015127133370e33615dff7b0d001024c3 Mon Sep 17 00:00:00 2001 From: Oana Umbres Date: Thu, 6 Apr 2017 16:10:44 +0300 Subject: [PATCH 6/7] Fixed lint issues --- test/featureFlagsConfig.provider.spec.js | 30 ++++++++++++------------ 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/test/featureFlagsConfig.provider.spec.js b/test/featureFlagsConfig.provider.spec.js index b221d57..1f675f1 100644 --- a/test/featureFlagsConfig.provider.spec.js +++ b/test/featureFlagsConfig.provider.spec.js @@ -1,10 +1,10 @@ -(function (angular) { +(function(angular) { 'use strict'; var module = angular.mock.module, inject = angular.mock.inject; - describe('Provider: featureFlags', function () { + describe('Provider: featureFlags', function() { var appName = 'myapp', featureFlagConfig, flags = [{ @@ -15,48 +15,49 @@ key: 'FLAG_KEY_2' }]; - describe('When no flags are set in the config phase', function () { - beforeEach(module('feature-flags', function (featureFlagConfigProvider) { + describe('When no flags are set in the config phase', function() { + beforeEach(module('feature-flags', function(featureFlagConfigProvider) { featureFlagConfigProvider.setInitialFlags(null); featureFlagConfigProvider.setAppName(null); })); - beforeEach(inject(function (_featureFlagConfig_) { + beforeEach(inject(function(_featureFlagConfig_) { featureFlagConfig = _featureFlagConfig_; })); - it('should return an empty array for current feature flags', function () { + it('should return an empty array for current feature flags', function() { expect(featureFlagConfig).not.toBeUndefined(); expect(featureFlagConfig.getInitialFlags()).toEqual([]); expect(featureFlagConfig.getAppName()).toEqual(''); }); }); - describe('When flags are set in the config phase', function () { - beforeEach(module('feature-flags', function (featureFlagConfigProvider) { + describe('When flags are set in the config phase', function() { + beforeEach(module('feature-flags', function(featureFlagConfigProvider) { featureFlagConfigProvider.setInitialFlags(flags); featureFlagConfigProvider.setAppName(appName); })); - beforeEach(inject(function (_featureFlagConfig_) { + beforeEach(inject(function(_featureFlagConfig_) { featureFlagConfig = _featureFlagConfig_; })); - it('should init the flags with the ones set in the config phase', function () { + it('should init the flags with the ones set in the config phase', function() { expect(featureFlagConfig).not.toBeUndefined(); expect(featureFlagConfig.getInitialFlags()).toEqual(flags); expect(featureFlagConfig.getAppName()).toEqual(appName); }); }); - describe('When flags are set in the run phase', function () { - beforeEach(module('feature-flags', function (featureFlagConfigProvider) { + describe('When flags are set in the run phase', function() { + beforeEach(module('feature-flags', function(featureFlagConfigProvider) { + featureFlagConfigProvider.setInitialFlags(null); })); - beforeEach(inject(function (_featureFlagConfig_) { + beforeEach(inject(function(_featureFlagConfig_) { featureFlagConfig = _featureFlagConfig_; })); - it('should init the flags with the ones set in the run phase', function () { + it('should init the flags with the ones set in the run phase', function() { expect(featureFlagConfig).not.toBeUndefined(); featureFlagConfig.setInitialFlags(flags); featureFlagConfig.setAppName(appName); @@ -64,6 +65,5 @@ expect(featureFlagConfig.getAppName()).toEqual(appName); }); }); - }); }(window.angular)); From 706d8e09507d5ab2dd96d189a0694d90abdbfbca Mon Sep 17 00:00:00 2001 From: Oana Umbres Date: Fri, 21 Apr 2017 13:52:40 +0300 Subject: [PATCH 7/7] Added dists --- dist/featureFlags.js | 64 ++++++++++++++++++++++++++++++++-------- dist/featureFlags.min.js | 2 +- 2 files changed, 53 insertions(+), 13 deletions(-) diff --git a/dist/featureFlags.js b/dist/featureFlags.js index a9b479a..bb1f173 100644 --- a/dist/featureFlags.js +++ b/dist/featureFlags.js @@ -76,10 +76,8 @@ angular.module('feature-flags').directive('featureFlagOverrides', ['featureFlags }; }]); -angular.module('feature-flags').service('featureFlagOverrides', ['$rootElement', function($rootElement) { - var appName = $rootElement.attr('ng-app'), - keyPrefix = 'featureFlags.' + appName + '.', - +angular.module('feature-flags').service('featureFlagOverrides', ['$rootElement', 'featureFlagConfig', function($rootElement, featureFlagConfig) { + var keyPrefix = 'featureFlags.', localStorageAvailable = (function() { try { localStorage.setItem('featureFlags.availableTest', 'test'); @@ -116,6 +114,9 @@ angular.module('feature-flags').service('featureFlagOverrides', ['$rootElement', } }; + if (featureFlagConfig.getAppName().length > 0) { + keyPrefix += featureFlagConfig.getAppName() + '.'; + } return { isPresent: function(key) { var value = get(key); @@ -143,7 +144,7 @@ angular.module('feature-flags').service('featureFlagOverrides', ['$rootElement', }; }]); -function FeatureFlags($q, featureFlagOverrides, initialFlags) { +angular.module('feature-flags').service('featureFlags', ['$q', 'featureFlagOverrides', 'featureFlagConfig', function($q, featureFlagOverrides, featureFlagConfig) { var serverFlagCache = {}, flags = [], @@ -205,8 +206,9 @@ function FeatureFlags($q, featureFlagOverrides, initialFlags) { }, init = function() { - if (initialFlags) { - set(initialFlags); + var iniFlags = featureFlagConfig.getInitialFlags(); + if (iniFlags) { + set(iniFlags); } }; init(); @@ -221,18 +223,56 @@ function FeatureFlags($q, featureFlagOverrides, initialFlags) { isOnByDefault: isOnByDefault, isOverridden: isOverridden }; +}]); + +function FeatureFlagConfig(initialFlags, userAppName) { + var initFlags = [], + appName = '', + + getInitialFlags = function() { + return initFlags; + }, + setInitialFlags = function(value) { + initFlags = value; + }, + getAppName = function() { + return appName; + }, + setAppName = function(value) { + appName = value; + }, + init = function() { + if (initialFlags) { + initFlags = initialFlags; + } + if (userAppName) { + appName = userAppName; + } + }; + init(); + + return { + getInitialFlags: getInitialFlags, + setInitialFlags: setInitialFlags, + getAppName: getAppName, + setAppName: setAppName + }; } -angular.module('feature-flags').provider('featureFlags', function() { - var initialFlags = []; +angular.module('feature-flags').provider('featureFlagConfig', function() { + var initialFlags = [], + appName = ''; this.setInitialFlags = function(flags) { initialFlags = flags; }; - this.$get = ['$q', 'featureFlagOverrides', function($q, featureFlagOverrides) { - return new FeatureFlags($q, featureFlagOverrides, initialFlags); - }]; + this.setAppName = function(name) { + appName = name; + }; + this.$get = function() { + return new FeatureFlagConfig(initialFlags, appName); + }; }); }()); \ No newline at end of file diff --git a/dist/featureFlags.min.js b/dist/featureFlags.min.js index 9b113d4..c25fdd5 100644 --- a/dist/featureFlags.min.js +++ b/dist/featureFlags.min.js @@ -1,2 +1,2 @@ /*! Angular Feature Flags v1.6.1 © 2017 Michael Taranto */ -!function(){function e(e,a,t){var r={},n=[],i=function(a){var t=e.defer();return t.resolve(a),t.promise},l=function(e){return a.isPresent(e)},f=function(e){return l(e)?"true"===a.get(e):r[e]},u=function(e){return r[e]},s=function(e){return e.forEach(function(e){r[e.key]=e.active,e.active=f(e.key)}),angular.copy(e,n),n},c=function(e){return e.then(function(e){return s(e.data||e)})},g=function(){return n},o=function(e){return angular.isArray(e)?i(s(e)):c(e)},d=function(e){e.active=!0,a.set(e.key,!0)},v=function(e){e.active=!1,a.set(e.key,!1)},y=function(e){e.active=r[e.key],a.remove(e.key)},m=function(){t&&o(t)};return m(),{set:o,get:g,enable:d,disable:v,reset:y,isOn:f,isOnByDefault:u,isOverridden:l}}angular.module("feature-flags",[]),angular.module("feature-flags").directive("featureFlag",["featureFlags","$interpolate",function(e,a){return{transclude:"element",priority:599,terminal:!0,restrict:"A",$$tlb:!0,compile:function(t,r){var n="featureFlagHide"in r;return t[0].textContent=" featureFlag: "+r.featureFlag+" is "+(n?"on":"off")+" ",function(t,r,i,l,f){var u,s;t.$watch(function(){var r=a(i.featureFlag)(t);return e.isOn(r)},function(e){var a=n?!e:e;a?(s=t.$new(),f(s,function(e){u=e,r.after(u).remove()})):(s&&(s.$destroy(),s=null),u&&(u.after(r).remove(),u=null))})}}}}]),angular.module("feature-flags").directive("featureFlagOverrides",["featureFlags",function(e){return{restrict:"A",link:function(a){a.flags=e.get(),a.isOn=e.isOn,a.isOverridden=e.isOverridden,a.enable=e.enable,a.disable=e.disable,a.reset=e.reset,a.isOnByDefault=e.isOnByDefault},template:'

Feature Flags

{{flag.name || flag.key}}
ON
OFF
DEFAULT ({{isOnByDefault(flag.key) ? \'ON\' : \'OFF\'}})
{{flag.description}}
',replace:!0}}]),angular.module("feature-flags").service("featureFlagOverrides",["$rootElement",function(e){var a=e.attr("ng-app"),t="featureFlags."+a+".",r=function(){try{return localStorage.setItem("featureFlags.availableTest","test"),localStorage.removeItem("featureFlags.availableTest"),!0}catch(e){return!1}}(),n=function(e){return t+e},i=function(e){return 0===e.indexOf(t)},l=function(e,a){r&&localStorage.setItem(n(a),e)},f=function(e){return r?localStorage.getItem(n(e)):void 0},u=function(e){r&&localStorage.removeItem(n(e))};return{isPresent:function(e){var a=f(e);return"undefined"!=typeof a&&null!==a},get:f,set:function(e,a){angular.isObject(e)?angular.forEach(e,l):l(a,e)},remove:u,reset:function(){var e;if(r)for(e in localStorage)i(e)&&localStorage.removeItem(e)}}}]),angular.module("feature-flags").provider("featureFlags",function(){var a=[];this.setInitialFlags=function(e){a=e},this.$get=["$q","featureFlagOverrides",function(t,r){return new e(t,r,a)}]})}(); \ No newline at end of file +!function(){function e(e,t){var a=[],n="",r=function(){return a},i=function(e){a=e},l=function(){return n},f=function(e){n=e},u=function(){e&&(a=e),t&&(n=t)};return u(),{getInitialFlags:r,setInitialFlags:i,getAppName:l,setAppName:f}}angular.module("feature-flags",[]),angular.module("feature-flags").directive("featureFlag",["featureFlags","$interpolate",function(e,t){return{transclude:"element",priority:599,terminal:!0,restrict:"A",$$tlb:!0,compile:function(a,n){var r="featureFlagHide"in n;return a[0].textContent=" featureFlag: "+n.featureFlag+" is "+(r?"on":"off")+" ",function(a,n,i,l,f){var u,s;a.$watch(function(){var n=t(i.featureFlag)(a);return e.isOn(n)},function(e){var t=r?!e:e;t?(s=a.$new(),f(s,function(e){u=e,n.after(u).remove()})):(s&&(s.$destroy(),s=null),u&&(u.after(n).remove(),u=null))})}}}}]),angular.module("feature-flags").directive("featureFlagOverrides",["featureFlags",function(e){return{restrict:"A",link:function(t){t.flags=e.get(),t.isOn=e.isOn,t.isOverridden=e.isOverridden,t.enable=e.enable,t.disable=e.disable,t.reset=e.reset,t.isOnByDefault=e.isOnByDefault},template:'

Feature Flags

{{flag.name || flag.key}}
ON
OFF
DEFAULT ({{isOnByDefault(flag.key) ? \'ON\' : \'OFF\'}})
{{flag.description}}
',replace:!0}}]),angular.module("feature-flags").service("featureFlagOverrides",["$rootElement","featureFlagConfig",function(e,t){var a="featureFlags.",n=function(){try{return localStorage.setItem("featureFlags.availableTest","test"),localStorage.removeItem("featureFlags.availableTest"),!0}catch(e){return!1}}(),r=function(e){return a+e},i=function(e){return 0===e.indexOf(a)},l=function(e,t){n&&localStorage.setItem(r(t),e)},f=function(e){return n?localStorage.getItem(r(e)):void 0},u=function(e){n&&localStorage.removeItem(r(e))};return t.getAppName().length>0&&(a+=t.getAppName()+"."),{isPresent:function(e){var t=f(e);return"undefined"!=typeof t&&null!==t},get:f,set:function(e,t){angular.isObject(e)?angular.forEach(e,l):l(t,e)},remove:u,reset:function(){var e;if(n)for(e in localStorage)i(e)&&localStorage.removeItem(e)}}}]),angular.module("feature-flags").service("featureFlags",["$q","featureFlagOverrides","featureFlagConfig",function(e,t,a){var n={},r=[],i=function(t){var a=e.defer();return a.resolve(t),a.promise},l=function(e){return t.isPresent(e)},f=function(e){return l(e)?"true"===t.get(e):n[e]},u=function(e){return n[e]},s=function(e){return e.forEach(function(e){n[e.key]=e.active,e.active=f(e.key)}),angular.copy(e,r),r},g=function(e){return e.then(function(e){return s(e.data||e)})},c=function(){return r},o=function(e){return angular.isArray(e)?i(s(e)):g(e)},v=function(e){e.active=!0,t.set(e.key,!0)},d=function(e){e.active=!1,t.set(e.key,!1)},m=function(e){e.active=n[e.key],t.remove(e.key)},F=function(){var e=a.getInitialFlags();e&&o(e)};return F(),{set:o,get:c,enable:v,disable:d,reset:m,isOn:f,isOnByDefault:u,isOverridden:l}}]),angular.module("feature-flags").provider("featureFlagConfig",function(){var t=[],a="";this.setInitialFlags=function(e){t=e},this.setAppName=function(e){a=e},this.$get=function(){return new e(t,a)}})}(); \ No newline at end of file