pinball_wizard 0.0.1.pre → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,44 @@
1
+ define ->
2
+
3
+ # Add .use-{feature-name} to <html> right away to prevent flickering.
4
+ # Works with ?pinball=feature_name and activated via optimizely.
5
+
6
+ # NOTE: underscores are converted to -. e.g. my_feature -> use-my-feature
7
+
8
+ # This component is designed to be minified and put in an inline <script> in
9
+ # the document <head>. Above CSS (so that it runs right away).
10
+
11
+ # Usage
12
+ # <script type="text/javascript">
13
+ # (
14
+ # function (...) { # minified script }
15
+ # )(document.documentElement, window.pinball, , window.location.search)
16
+ # </script>
17
+
18
+ (ele, pinballQueue, searchQuery) ->
19
+ classNames = []
20
+
21
+ add = (name) ->
22
+ classNames.push 'use-' + name.split('_').join('-')
23
+
24
+ for entry in pinballQueue
25
+ continue unless entry.length
26
+
27
+ switch entry[0]
28
+ when 'activate'
29
+ add entry[1]
30
+
31
+ when 'add'
32
+ for feature, state of entry[1]
33
+ add feature if state == 'active'
34
+
35
+ matches = searchQuery.match(/pinball=([a-z-_,]+)/i)
36
+ if matches && matches.length > 1
37
+ featureNames = (matches[1] + '').split(',')
38
+
39
+ for feature in featureNames
40
+ add feature
41
+
42
+ ele.className += ' ' + classNames.join(' ') if ele
43
+
44
+ return
@@ -0,0 +1,157 @@
1
+ 'use strict'
2
+
3
+ define ->
4
+
5
+ features = {}
6
+ subscribers = {}
7
+
8
+ showLog = false
9
+ logPrefix = '[PinballWizard]'
10
+
11
+ _log = (message, args...) ->
12
+ if showLog && window.console && window.console.log
13
+ console.log("#{logPrefix} #{message}", args...)
14
+ return
15
+
16
+ _notifySubscribersOnActivate = (name) ->
17
+ subscribers[name] ?= []
18
+ for subscriber in subscribers[name]
19
+ _notifySubscriberOnActivate(subscriber, name)
20
+
21
+ _notifySubscriberOnActivate = (subscriber, name) ->
22
+ _log 'Notify subscriber that %s is active', name
23
+ subscriber.onActivate()
24
+
25
+ _notifySubscribersOnDeactivate = (name) ->
26
+ subscribers[name] ?= []
27
+ for subscriber in subscribers[name]
28
+ subscriber.onDeactivate()
29
+
30
+ # Support ?pinball=name1,name2,debug
31
+ _urlValueMatches = (value) ->
32
+ for v in _urlValues()
33
+ return true if value == v
34
+ false
35
+
36
+ _urlValues = (search = window.location.search) ->
37
+ pairs = search.substr(1).split('&')
38
+ for pair in pairs
39
+ [key, value] = pair.split('=')
40
+ if key == 'pinball' and value?
41
+ return value.split(',')
42
+ []
43
+ urlValues = _urlValues() # Memoize
44
+
45
+ cssClassName = (name, prefix = 'use-') ->
46
+ prefix + name.split('_').join('-')
47
+
48
+ addCSSClassName = (name, ele = document.documentElement) ->
49
+ cN = cssClassName(name)
50
+ if ele.className.indexOf(cN) < 0
51
+ ele.className += ' ' + cN
52
+
53
+ removeCSSClassName = (name, ele = document.documentElement) ->
54
+ cN = cssClassName(name)
55
+ if ele.className.indexOf(cN) >= 0
56
+ ele.className = ele.className.replace cN, ''
57
+
58
+ add = (list) ->
59
+ for name, state of list
60
+ features[name] = state
61
+ _log "Added %s: %s.", name, state
62
+
63
+ if isActive(name)
64
+ activate(name, "automatic. added as '#{state}'")
65
+ else if _urlValueMatches(name, urlValues)
66
+ activate(name, 'URL')
67
+
68
+ get = (name) ->
69
+ features[name]
70
+
71
+ update = (name, state) ->
72
+ features[name] = state
73
+
74
+ activate = (name, sourceName = null) ->
75
+ state = get(name)
76
+ source = if sourceName? then " (source: #{sourceName})" else ''
77
+ switch state
78
+ when undefined
79
+ _log "Attempted to activate %s, but it was not found%s.", name, source
80
+ when 'inactive'
81
+ _log "Activate %s%s.", name, source
82
+ update(name, 'active')
83
+ addCSSClassName(name)
84
+ _notifySubscribersOnActivate(name)
85
+ when 'active'
86
+ _log "Attempted to activate %s, but it is already active%s.", name, source
87
+ else
88
+ _log "Attempted to activate %s, but it is %s%s.", name, state, source
89
+
90
+ deactivate = (name, source = null) ->
91
+ state = get(name)
92
+ source = if sourceName? then " (source: #{sourceName})" else ''
93
+ switch state
94
+ when undefined
95
+ _log "Attempted to deactivate %s, but it was not found%s.", name, source
96
+ when 'active'
97
+ _log "Dectivate %s%s.", name, source
98
+ update(name, 'inactive')
99
+ removeCSSClassName(name)
100
+ _notifySubscribersOnDeactivate(name)
101
+ else
102
+ _log "Attempted to deactivate %s, but it is %s%s.", name, state, source
103
+
104
+ isActive = (name) ->
105
+ get(name) == 'active'
106
+
107
+ _buildSubscriber = (onActivate, onDeactivate) ->
108
+ onActivate: if onActivate? then onActivate else ->
109
+ onDeactivate: if onDeactivate? then onDeactivate else ->
110
+
111
+ # If the feature is already active, the callback is invoked immediately.
112
+ subscribe = (name, onActivate, onDeactivate) ->
113
+ _log 'Added subscriber to %s', name
114
+ subscriber = _buildSubscriber(onActivate, onDeactivate)
115
+ subscribers[name] ?= []
116
+ subscribers[name].push(subscriber)
117
+ _notifySubscriberOnActivate(subscriber, name) if isActive(name)
118
+
119
+ push = (params) ->
120
+ method = params.shift()
121
+ @[method].apply(@, params)
122
+
123
+ state = ->
124
+ features
125
+
126
+ reset = ->
127
+ features = {}
128
+
129
+ debug = ->
130
+ showLog = true
131
+
132
+ # Exports
133
+ exports = {
134
+ add
135
+ get
136
+ activate
137
+ deactivate
138
+ isActive
139
+ subscribe
140
+ push
141
+ state
142
+ reset
143
+ debug
144
+ cssClassName
145
+ addCSSClassName
146
+ removeCSSClassName
147
+ _urlValues
148
+ }
149
+
150
+ # Initialize
151
+ if window?.pinball
152
+ debug() if _urlValueMatches('debug')
153
+ while window.pinball.length
154
+ exports.push window.pinball.shift()
155
+ window.pinball = exports
156
+
157
+ exports
@@ -0,0 +1,50 @@
1
+ define ['css_tagger'], (tagger) ->
2
+
3
+ describe 'css_tagger', ->
4
+ it 'should add classes from the pinball async function queue', ->
5
+ ele = document.createElement 'div'
6
+ pinballQueue = [
7
+ ['activate','my_feature']
8
+ ]
9
+
10
+ tagger ele, pinballQueue, ''
11
+ expect(ele.className).toEqual ' use-my-feature'
12
+
13
+ it 'should add classes from query params', ->
14
+ ele = document.createElement 'div'
15
+ tagger ele, [], '?pinball=feature_a,feature_b&other=param'
16
+ expect(ele.className).toEqual ' use-feature-a use-feature-b'
17
+
18
+ it 'should not interfere with existing class names', ->
19
+ ele = document.createElement 'div'
20
+ ele.className = 'foo-bar'
21
+ tagger ele, [], '?pinball=feature_a,feature_b&other=param'
22
+ expect(ele.className).toEqual 'foo-bar use-feature-a use-feature-b'
23
+
24
+ it 'should add classes from added pinball features', ->
25
+ ele = document.createElement 'div'
26
+ pinballQueue = [
27
+ [
28
+ 'add'
29
+ feature_a: 'active'
30
+ feature_b: 'inactive'
31
+ feature_c: 'active'
32
+ ]
33
+ ]
34
+
35
+ tagger ele, pinballQueue, ''
36
+ expect(ele.className).toEqual ' use-feature-a use-feature-c'
37
+
38
+ it 'should add classes from queue and query params', ->
39
+ ele = document.createElement 'div'
40
+ pinballQueue = [
41
+ [
42
+ 'add'
43
+ feature_a: 'active'
44
+ ],
45
+ ['activate', 'feature_b'],
46
+ ['something-odd']
47
+ ]
48
+
49
+ tagger ele, pinballQueue, '?pinball=feature_c'
50
+ expect(ele.className).toEqual ' use-feature-a use-feature-b use-feature-c'
@@ -0,0 +1,268 @@
1
+ define ['pinball_wizard'], (pinball) ->
2
+
3
+ beforeEach ->
4
+ pinball.reset()
5
+
6
+ describe 'initialize', ->
7
+ it 'is defined', ->
8
+ expect(pinball).toBeDefined()
9
+
10
+ describe '#reset', ->
11
+ it 'removes all features', ->
12
+ pinball.add
13
+ a: 'active'
14
+ pinball.reset()
15
+ expect(pinball.state()).toEqual({})
16
+
17
+ describe '#add', ->
18
+ it 'activates if active', ->
19
+ pinball.add
20
+ a: 'active'
21
+ expect(pinball.isActive('a')).toEqual(true)
22
+
23
+ it 'does not activate if inactive', ->
24
+ pinball.add
25
+ a: 'inactive'
26
+ expect(pinball.isActive('a')).toEqual(false)
27
+
28
+ it 'does not activate if disabled', ->
29
+ pinball.add
30
+ a: 'disabled: reason'
31
+ expect(pinball.isActive('a')).toEqual(false)
32
+
33
+ describe 'with a ?pinball=feature,feature url param', ->
34
+
35
+ originalPathname = null
36
+
37
+ beforeEach ->
38
+ originalPathname = window.location.pathname
39
+
40
+ afterEach ->
41
+ window.history.replaceState(null, null, originalPathname)
42
+
43
+ it 'is not active when mismatched', ->
44
+ urlParam = '?pinball=foo,bar'
45
+ window.history.replaceState(null, null, window.location.pathname + urlParam)
46
+ pinball.add
47
+ a: 'inactive'
48
+ b: 'inactive'
49
+ expect(pinball.isActive('a')).toEqual(false)
50
+ expect(pinball.isActive('b')).toEqual(false)
51
+
52
+ it 'is active when matching', ->
53
+ urlParam = '?pinball=a,b'
54
+ # Mock a different url
55
+ window.history.replaceState(null, null, window.location.pathname + urlParam)
56
+ pinball.add
57
+ a: 'inactive'
58
+ b: 'inactive'
59
+ expect(pinball.isActive('a')).toEqual(true)
60
+ expect(pinball.isActive('b')).toEqual(true)
61
+
62
+ describe '#state', ->
63
+ it 'displays a list based on state', ->
64
+ pinball.add
65
+ a: 'active'
66
+ b: 'inactive'
67
+ c: 'disabled: reason'
68
+
69
+ expect(pinball.state()).toEqual({
70
+ a: 'active'
71
+ b: 'inactive'
72
+ c: 'disabled: reason'
73
+ })
74
+
75
+ describe '#activate', ->
76
+ it 'makes an inactive feature active', ->
77
+ pinball.add
78
+ a: 'inactive'
79
+ pinball.activate 'a'
80
+
81
+ expect(pinball.get('a')).toEqual('active')
82
+
83
+ it 'does not make a disabled feature active', ->
84
+ pinball.add
85
+ a: 'disabled'
86
+ pinball.activate 'a'
87
+
88
+ expect(pinball.get('a')).toEqual('disabled')
89
+
90
+ describe '#deactivate', ->
91
+ it 'makes an active feature inactive', ->
92
+ pinball.add
93
+ a: 'active'
94
+ pinball.deactivate 'a'
95
+
96
+ expect(pinball.get('a')).toEqual('inactive')
97
+
98
+ describe '#isActive', ->
99
+ beforeEach ->
100
+ pinball.add
101
+ a: 'inactive'
102
+
103
+ it 'is true after activating', ->
104
+ pinball.activate 'a'
105
+ expect(pinball.isActive('a')).toEqual(true)
106
+
107
+ describe '#subscribe', ->
108
+ callback = null
109
+ beforeEach ->
110
+ callback = jasmine.createSpy('callback')
111
+
112
+ describe 'when the activate callback should be called', ->
113
+ it 'calls after activating', ->
114
+ pinball.add
115
+ a: 'inactive'
116
+ pinball.subscribe 'a', callback
117
+ pinball.activate 'a'
118
+ expect(callback).toHaveBeenCalled()
119
+
120
+ it 'calls it once on multiple activations', ->
121
+ pinball.add
122
+ a: 'inactive'
123
+ pinball.subscribe 'a', callback
124
+ pinball.activate 'a'
125
+ pinball.activate 'a'
126
+ pinball.activate 'a'
127
+ expect(callback.calls.count()).toEqual(1)
128
+
129
+ it 'calls it twice when toggling activations', ->
130
+ pinball.add
131
+ a: 'inactive'
132
+ pinball.subscribe 'a', callback
133
+ pinball.activate 'a'
134
+ pinball.deactivate 'a'
135
+ pinball.activate 'a'
136
+ expect(callback.calls.count()).toEqual(2)
137
+
138
+ it 'calls when subscribing then adding and then activating a feature', ->
139
+ pinball.subscribe 'a', callback
140
+ pinball.add
141
+ a: 'inactive'
142
+ pinball.activate 'a'
143
+ expect(callback).toHaveBeenCalled()
144
+
145
+ it 'calls when subscribing to an already active feature', ->
146
+ pinball.add
147
+ a: 'active'
148
+ pinball.subscribe 'a', callback
149
+ expect(callback).toHaveBeenCalled()
150
+
151
+ describe 'when the activate callback should not be called', ->
152
+ it 'does not call when the feature is missing', ->
153
+ pinball.subscribe 'a', callback
154
+ pinball.activate 'a'
155
+ expect(callback).not.toHaveBeenCalled()
156
+
157
+ it 'does not call when the feature is disabled', ->
158
+ pinball.add
159
+ a: 'disabled: reason'
160
+ pinball.subscribe 'a', callback
161
+ pinball.activate 'a'
162
+ expect(callback).not.toHaveBeenCalled()
163
+
164
+ describe 'when the deactivate callback should be called', ->
165
+ it 'calls after deactivate', ->
166
+ pinball.add
167
+ a: 'inactive'
168
+ pinball.subscribe 'a', null, callback
169
+ pinball.activate 'a'
170
+ pinball.deactivate 'a'
171
+ expect(callback).toHaveBeenCalled()
172
+
173
+ it 'calls it once on multiple deactivations', ->
174
+ pinball.add
175
+ a: 'inactive'
176
+ pinball.subscribe 'a', null, callback
177
+ pinball.activate 'a'
178
+ pinball.deactivate 'a'
179
+ pinball.deactivate 'a'
180
+ expect(callback.calls.count()).toEqual(1)
181
+
182
+ it 'calls it twice when toggling deactivations', ->
183
+ pinball.add
184
+ a: 'inactive'
185
+ pinball.subscribe 'a', null, callback
186
+ pinball.activate 'a'
187
+ pinball.deactivate 'a'
188
+ pinball.activate 'a'
189
+ pinball.deactivate 'a'
190
+ expect(callback.calls.count()).toEqual(2)
191
+
192
+ it 'calls when subscribing, adding adding an active then deactivating', ->
193
+ pinball.subscribe 'a', null, callback
194
+ pinball.add
195
+ a: 'active'
196
+ pinball.deactivate 'a'
197
+ expect(callback).toHaveBeenCalled()
198
+
199
+ it 'calls when subscribing then adding and then deactivating a feature', ->
200
+ pinball.subscribe 'a', null, callback
201
+ pinball.add
202
+ a: 'inactive'
203
+ pinball.activate 'a'
204
+ pinball.deactivate 'a'
205
+ expect(callback).toHaveBeenCalled()
206
+
207
+ describe 'when the deactivate callback should not be called', ->
208
+ it 'does not call when subscribing then adding an active feature', ->
209
+ pinball.subscribe 'a', null, callback
210
+ pinball.add
211
+ a: 'active'
212
+ expect(callback).not.toHaveBeenCalled()
213
+
214
+ it 'does not call when the feature is missing', ->
215
+ pinball.subscribe 'a', null, callback
216
+ pinball.activate 'a'
217
+ pinball.deactivate 'a'
218
+ expect(callback).not.toHaveBeenCalled()
219
+
220
+ it 'does not call when the feature is disabled', ->
221
+ pinball.add
222
+ a: 'disabled'
223
+ pinball.subscribe 'a', null, callback
224
+ pinball.activate 'a'
225
+ pinball.deactivate 'a'
226
+ expect(callback).not.toHaveBeenCalled()
227
+
228
+ describe '#push', ->
229
+ it 'calls the function with the first entry and the args for the rest', ->
230
+ spyOn(pinball, 'activate')
231
+ pinball.push ['activate','my-feature']
232
+ expect(pinball.activate).toHaveBeenCalledWith('my-feature')
233
+
234
+ describe '#cssClassName', ->
235
+ it 'builds the name with the prefix', ->
236
+ expect(pinball.cssClassName('my_feature')).toEqual 'use-my-feature'
237
+
238
+ describe '#addCSSClassName', ->
239
+ it 'appends', ->
240
+ ele = document.createElement 'div'
241
+ pinball.addCSSClassName('my_feature', ele)
242
+ expect(ele.className).toEqual ' use-my-feature'
243
+
244
+ it 'does not append twice', ->
245
+ ele = document.createElement 'div'
246
+ pinball.addCSSClassName('my_feature', ele)
247
+ pinball.addCSSClassName('my_feature', ele)
248
+ expect(ele.className).toEqual ' use-my-feature'
249
+
250
+ describe '#removeCSSClassName', ->
251
+ it 'removes it', ->
252
+ ele = document.createElement 'div'
253
+ pinball.addCSSClassName('my_feature', ele)
254
+ pinball.removeCSSClassName('my_feature', ele)
255
+ expect(ele.className).toEqual ''
256
+
257
+ describe '#_urlValues', ->
258
+ it 'pulls out the parts', ->
259
+ urlParam = '?pinball=a,b'
260
+ expect(pinball._urlValues(urlParam)).toEqual(['a','b'])
261
+
262
+ it 'is empty for blank values', ->
263
+ urlParam = '?pinball'
264
+ expect(pinball._urlValues(urlParam)).toEqual([])
265
+
266
+ it 'works with other keys/values', ->
267
+ urlParam = '?foo=bar&pinball=a,b&bar'
268
+ expect(pinball._urlValues(urlParam)).toEqual(['a','b'])