pinball_wizard 0.0.1.pre → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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'])