unpoly-rails 0.56.7 → 0.57.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of unpoly-rails might be problematic. Click here for more details.

Files changed (67) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +74 -1
  3. data/dist/unpoly.js +1569 -793
  4. data/dist/unpoly.min.js +4 -4
  5. data/lib/assets/javascripts/unpoly.coffee +2 -0
  6. data/lib/assets/javascripts/unpoly/browser.coffee.erb +25 -41
  7. data/lib/assets/javascripts/unpoly/bus.coffee.erb +20 -6
  8. data/lib/assets/javascripts/unpoly/classes/cache.coffee +23 -13
  9. data/lib/assets/javascripts/unpoly/classes/compile_pass.coffee +87 -0
  10. data/lib/assets/javascripts/unpoly/classes/focus_tracker.coffee +29 -0
  11. data/lib/assets/javascripts/unpoly/classes/follow_variant.coffee +7 -4
  12. data/lib/assets/javascripts/unpoly/classes/record.coffee +1 -1
  13. data/lib/assets/javascripts/unpoly/classes/request.coffee +38 -45
  14. data/lib/assets/javascripts/unpoly/classes/response.coffee +16 -1
  15. data/lib/assets/javascripts/unpoly/classes/store/memory.coffee +26 -0
  16. data/lib/assets/javascripts/unpoly/classes/store/session.coffee +59 -0
  17. data/lib/assets/javascripts/unpoly/cookie.coffee +56 -0
  18. data/lib/assets/javascripts/unpoly/dom.coffee.erb +67 -39
  19. data/lib/assets/javascripts/unpoly/feedback.coffee +2 -2
  20. data/lib/assets/javascripts/unpoly/form.coffee.erb +23 -12
  21. data/lib/assets/javascripts/unpoly/history.coffee +2 -2
  22. data/lib/assets/javascripts/unpoly/layout.coffee.erb +118 -99
  23. data/lib/assets/javascripts/unpoly/link.coffee.erb +12 -5
  24. data/lib/assets/javascripts/unpoly/log.coffee +6 -5
  25. data/lib/assets/javascripts/unpoly/modal.coffee.erb +9 -2
  26. data/lib/assets/javascripts/unpoly/motion.coffee.erb +2 -6
  27. data/lib/assets/javascripts/unpoly/namespace.coffee.erb +2 -2
  28. data/lib/assets/javascripts/unpoly/params.coffee.erb +522 -0
  29. data/lib/assets/javascripts/unpoly/popup.coffee.erb +3 -3
  30. data/lib/assets/javascripts/unpoly/proxy.coffee +42 -34
  31. data/lib/assets/javascripts/unpoly/{syntax.coffee → syntax.coffee.erb} +59 -117
  32. data/lib/assets/javascripts/unpoly/{util.coffee → util.coffee.erb} +206 -171
  33. data/lib/unpoly/rails/version.rb +1 -1
  34. data/package.json +1 -1
  35. data/spec_app/Gemfile.lock +1 -1
  36. data/spec_app/app/assets/javascripts/integration_test.coffee +0 -4
  37. data/spec_app/app/assets/stylesheets/integration_test.sass +7 -1
  38. data/spec_app/app/controllers/pages_controller.rb +4 -0
  39. data/spec_app/app/views/form_test/basics/new.erb +34 -5
  40. data/spec_app/app/views/form_test/submission_result.erb +2 -2
  41. data/spec_app/app/views/form_test/uploads/new.erb +15 -2
  42. data/spec_app/app/views/hash_test/unpoly.erb +30 -0
  43. data/spec_app/app/views/pages/start.erb +2 -1
  44. data/spec_app/spec/javascripts/helpers/parse_form_data.js.coffee +17 -2
  45. data/spec_app/spec/javascripts/helpers/reset_up.js.coffee +5 -0
  46. data/spec_app/spec/javascripts/helpers/to_be_error.coffee +1 -1
  47. data/spec_app/spec/javascripts/helpers/to_match_selector.coffee +5 -0
  48. data/spec_app/spec/javascripts/up/browser_spec.js.coffee +8 -8
  49. data/spec_app/spec/javascripts/up/bus_spec.js.coffee +58 -20
  50. data/spec_app/spec/javascripts/up/classes/cache_spec.js.coffee +78 -0
  51. data/spec_app/spec/javascripts/up/classes/focus_tracker_spec.coffee +31 -0
  52. data/spec_app/spec/javascripts/up/classes/request_spec.coffee +50 -0
  53. data/spec_app/spec/javascripts/up/classes/store/memory_spec.js.coffee +67 -0
  54. data/spec_app/spec/javascripts/up/classes/store/session_spec.js.coffee +113 -0
  55. data/spec_app/spec/javascripts/up/dom_spec.js.coffee +133 -45
  56. data/spec_app/spec/javascripts/up/form_spec.js.coffee +13 -13
  57. data/spec_app/spec/javascripts/up/layout_spec.js.coffee +110 -26
  58. data/spec_app/spec/javascripts/up/link_spec.js.coffee +1 -1
  59. data/spec_app/spec/javascripts/up/modal_spec.js.coffee +1 -0
  60. data/spec_app/spec/javascripts/up/motion_spec.js.coffee +52 -51
  61. data/spec_app/spec/javascripts/up/namespace_spec.js.coffee +2 -2
  62. data/spec_app/spec/javascripts/up/params_spec.coffee +768 -0
  63. data/spec_app/spec/javascripts/up/proxy_spec.js.coffee +75 -36
  64. data/spec_app/spec/javascripts/up/syntax_spec.js.coffee +48 -15
  65. data/spec_app/spec/javascripts/up/util_spec.js.coffee +148 -131
  66. metadata +17 -5
  67. data/spec_app/spec/javascripts/up/classes/.keep +0 -0
@@ -7,19 +7,28 @@ describe 'up.proxy', ->
7
7
  describe 'up.request', ->
8
8
 
9
9
  it 'makes a request with the given URL and params', ->
10
- up.request('/foo', data: { key: 'value' }, method: 'post')
10
+ up.request('/foo', params: { key: 'value' }, method: 'post')
11
11
  request = @lastRequest()
12
12
  expect(request.url).toMatchUrl('/foo')
13
13
  expect(request.data()).toEqual(key: ['value'])
14
14
  expect(request.method).toEqual('POST')
15
15
 
16
16
  it 'also allows to pass the URL as a { url } option instead', ->
17
- up.request(url: '/foo', data: { key: 'value' }, method: 'post')
17
+ up.request(url: '/foo', params: { key: 'value' }, method: 'post')
18
18
  request = @lastRequest()
19
19
  expect(request.url).toMatchUrl('/foo')
20
20
  expect(request.data()).toEqual(key: ['value'])
21
21
  expect(request.method).toEqual('POST')
22
22
 
23
+ it 'allows to pass in an up.Request instance instead of an options object', ->
24
+ requestArg = new up.Request(url: '/foo', params: { key: 'value' }, method: 'post')
25
+ up.request(requestArg)
26
+
27
+ jasmineRequest = @lastRequest()
28
+ expect(jasmineRequest.url).toMatchUrl('/foo')
29
+ expect(jasmineRequest.data()).toEqual(key: ['value'])
30
+ expect(jasmineRequest.method).toEqual('POST')
31
+
23
32
  it 'submits the replacement targets as HTTP headers, so the server may choose to only frender the requested fragments', asyncSpec (next) ->
24
33
  up.request(url: '/foo', target: '.target', failTarget: '.fail-target')
25
34
 
@@ -31,7 +40,7 @@ describe 'up.proxy', ->
31
40
  it 'resolves to a Response object that contains information about the response and request', (done) ->
32
41
  promise = up.request(
33
42
  url: '/url'
34
- data: { key: 'value' }
43
+ params: { key: 'value' }
35
44
  method: 'post'
36
45
  target: '.target'
37
46
  )
@@ -44,7 +53,7 @@ describe 'up.proxy', ->
44
53
 
45
54
  promise.then (response) ->
46
55
  expect(response.request.url).toMatchUrl('/url')
47
- expect(response.request.data).toEqual(key: 'value')
56
+ expect(response.request.params).toEqual(key: 'value')
48
57
  expect(response.request.method).toEqual('POST')
49
58
  expect(response.request.target).toEqual('.target')
50
59
  expect(response.request.hash).toBeBlank()
@@ -57,6 +66,22 @@ describe 'up.proxy', ->
57
66
 
58
67
  done()
59
68
 
69
+ it 'resolves to a Response that contains the response headers', (done) ->
70
+ promise = up.request(url: '/url')
71
+
72
+ u.nextFrame =>
73
+ @respondWith
74
+ responseHeaders: { 'foo': 'bar', 'baz': 'bam' }
75
+ responseText: 'hello'
76
+
77
+ promise.then (response) ->
78
+ expect(response.getHeader('foo')).toEqual('bar')
79
+
80
+ # Lookup is case-insensitive
81
+ expect(response.getHeader('BAZ')).toEqual('bam')
82
+
83
+ done()
84
+
60
85
  it "preserves the URL hash in a separate { hash } property, since although it isn't sent to server, code might need it to process the response", (done) ->
61
86
  promise = up.request('/url#hash')
62
87
 
@@ -77,7 +102,7 @@ describe 'up.proxy', ->
77
102
  it 'updates the { method } property in the response object', (done) ->
78
103
  promise = up.request(
79
104
  url: '/url'
80
- data: { key: 'value' }
105
+ params: { key: 'value' }
81
106
  method: 'post'
82
107
  target: '.target'
83
108
  )
@@ -164,8 +189,8 @@ describe 'up.proxy', ->
164
189
  # See that an additional request was made
165
190
  expect(jasmine.Ajax.requests.count()).toEqual(2)
166
191
 
167
- it "does not explode if the original request's { data } is a FormData object", asyncSpec (next) ->
168
- up.request('/foo', method: 'post', data: new FormData()) # POST requests are not cached
192
+ it "does not explode if the original request's { params } is a FormData object", asyncSpec (next) ->
193
+ up.request('/foo', method: 'post', params: new FormData()) # POST requests are not cached
169
194
 
170
195
  next =>
171
196
  expect(jasmine.Ajax.requests.count()).toEqual(1)
@@ -286,11 +311,11 @@ describe 'up.proxy', ->
286
311
  headers = @lastRequest().requestHeaders
287
312
  expect(headers['X-Requested-With']).toEqual('Love')
288
313
 
289
- describe 'with { data } option', ->
314
+ describe 'with { params } option', ->
290
315
 
291
316
  it "uses the given params as a non-GET request's payload", asyncSpec (next) ->
292
317
  givenParams = { 'foo-key': 'foo-value', 'bar-key': 'bar-value' }
293
- up.request(url: '/path', method: 'put', data: givenParams)
318
+ up.request(url: '/path', method: 'put', params: givenParams)
294
319
 
295
320
  next =>
296
321
  expect(@lastRequest().data()['foo-key']).toEqual(['foo-value'])
@@ -298,7 +323,7 @@ describe 'up.proxy', ->
298
323
 
299
324
  it "encodes the given params into the URL of a GET request", (done) ->
300
325
  givenParams = { 'foo-key': 'foo-value', 'bar-key': 'bar-value' }
301
- promise = up.request(url: '/path', method: 'get', data: givenParams)
326
+ promise = up.request(url: '/path', method: 'get', params: givenParams)
302
327
 
303
328
  u.nextFrame =>
304
329
  expect(@lastRequest().url).toMatchUrl('/path?foo-key=foo-value&bar-key=bar-value')
@@ -310,7 +335,7 @@ describe 'up.proxy', ->
310
335
  # See that the response object has been updated by moving the data options
311
336
  # to the URL. This is important for up.dom code that works on response.request.
312
337
  expect(response.request.url).toMatchUrl('/path?foo-key=foo-value&bar-key=bar-value')
313
- expect(response.request.data).toBeBlank()
338
+ expect(response.request.params).toBeBlank()
314
339
  done()
315
340
 
316
341
  it 'caches server responses for the configured duration', asyncSpec (next) ->
@@ -384,8 +409,8 @@ describe 'up.proxy', ->
384
409
  next => expect(jasmine.Ajax.requests.count()).toEqual(2)
385
410
 
386
411
  it "doesn't reuse responses when asked for the same path, but different params", asyncSpec (next) ->
387
- next => up.request(url: '/path', data: { query: 'foo' })
388
- next => up.request(url: '/path', data: { query: 'bar' })
412
+ next => up.request(url: '/path', params: { query: 'foo' })
413
+ next => up.request(url: '/path', params: { query: 'bar' })
389
414
  next => expect(jasmine.Ajax.requests.count()).toEqual(2)
390
415
 
391
416
  it "reuses a response for an 'html' selector when asked for the same path and any other selector", asyncSpec (next) ->
@@ -460,6 +485,7 @@ describe 'up.proxy', ->
460
485
  request = @lastRequest()
461
486
  expect(request.method).toEqual('POST')
462
487
  expect(request.data()['_method']).toEqual([method])
488
+ # expect(request.data()['foo']).toEqual('bar')
463
489
 
464
490
  describe 'with config.maxRequests set', ->
465
491
 
@@ -656,7 +682,7 @@ describe 'up.proxy', ->
656
682
 
657
683
  it 'does not emit up:proxy:recover if a delayed up:proxy:slow was never emitted due to a fast response', asyncSpec (next) ->
658
684
  next =>
659
- up.proxy.config.slowDelay = 100
685
+ up.proxy.config.slowDelay = 200
660
686
  up.request(url: '/foo')
661
687
 
662
688
  next =>
@@ -664,13 +690,13 @@ describe 'up.proxy', ->
664
690
  'up:proxy:load'
665
691
  ])
666
692
 
667
- next.after 50, =>
693
+ next.after 100, =>
668
694
  jasmine.Ajax.requests.at(0).respondWith
669
695
  status: 200
670
696
  contentType: 'text/html'
671
697
  responseText: 'foo'
672
698
 
673
- next.after 150, =>
699
+ next.after 250, =>
674
700
  expect(@events).toEqual([
675
701
  'up:proxy:load',
676
702
  'up:proxy:loaded'
@@ -756,11 +782,24 @@ describe 'up.proxy', ->
756
782
  cachedPromise = up.proxy.get(url: '/path', target: '.target')
757
783
  expect(u.isPromise(cachedPromise)).toBe(true)
758
784
 
759
- it "does not load a link whose method has side-effects", asyncSpec (next) ->
760
- $link = affix('a[href="/path"][data-method="post"]')
761
- up.proxy.preload($link)
785
+ it "does not load a link whose method has side-effects", (done) ->
786
+ affix('.target')
787
+ $link = affix('a[href="/path"][up-target=".target"][data-method="post"]')
788
+ preloadPromise = up.proxy.preload($link)
789
+
790
+ promiseState(preloadPromise).then (result) ->
791
+ expect(result.state).toEqual('rejected')
792
+ expect(up.proxy.get(url: '/path', target: '.target')).toBeUndefined()
793
+ done()
794
+
795
+ it 'accepts options', asyncSpec (next) ->
796
+ affix('.target')
797
+ $link = affix('a[href="/path"][up-target=".target"]')
798
+ up.proxy.preload($link, url: '/options-path')
762
799
 
763
- next => expect(up.proxy.get(url: '/path')).toBeUndefined()
800
+ next =>
801
+ cachedPromise = up.proxy.get(url: '/options-path', target: '.target')
802
+ expect(u.isPromise(cachedPromise)).toBe(true)
764
803
 
765
804
  describe 'for an [up-target] link', ->
766
805
 
@@ -784,7 +823,7 @@ describe 'up.proxy', ->
784
823
  up.proxy.preload($link)
785
824
 
786
825
  next =>
787
- expect(requestSpy).toHaveBeenCalledWith(jasmine.objectContaining(url: '/path', preload: true))
826
+ expect(requestSpy).toHaveBeenCalledWith(jasmine.objectContaining(preload: true))
788
827
 
789
828
  describe 'for an [up-modal] link', ->
790
829
 
@@ -851,7 +890,7 @@ describe 'up.proxy', ->
851
890
  up.proxy.preload($link)
852
891
 
853
892
  next =>
854
- expect(requestSpy).toHaveBeenCalledWith(jasmine.objectContaining(url: '/path', preload: true))
893
+ expect(requestSpy).toHaveBeenCalledWith(jasmine.objectContaining(preload: true))
855
894
 
856
895
  describe 'for an [up-popup] link', ->
857
896
 
@@ -921,7 +960,7 @@ describe 'up.proxy', ->
921
960
  up.proxy.preload($link)
922
961
 
923
962
  next =>
924
- expect(requestSpy).toHaveBeenCalledWith(jasmine.objectContaining(url: '/path', preload: true))
963
+ expect(requestSpy).toHaveBeenCalledWith(jasmine.objectContaining(preload: true))
925
964
 
926
965
  describeFallback 'canPushState', ->
927
966
 
@@ -935,16 +974,16 @@ describe 'up.proxy', ->
935
974
  describe 'up.proxy.get', ->
936
975
 
937
976
  it 'returns an existing cache entry for the given request', ->
938
- promise1 = up.request(url: '/foo', data: { key: 'value' })
939
- promise2 = up.proxy.get(url: '/foo', data: { key: 'value' })
977
+ promise1 = up.request(url: '/foo', params: { key: 'value' })
978
+ promise2 = up.proxy.get(url: '/foo', params: { key: 'value' })
940
979
  expect(promise1).toBe(promise2)
941
980
 
942
981
  it 'returns undefined if the given request is not cached', ->
943
- promise = up.proxy.get(url: '/foo', data: { key: 'value' })
982
+ promise = up.proxy.get(url: '/foo', params: { key: 'value' })
944
983
  expect(promise).toBeUndefined()
945
984
 
946
- it "returns undefined if the given request's { data } is a FormData object", ->
947
- promise = up.proxy.get(url: '/foo', data: new FormData())
985
+ it "returns undefined if the given request's { params } is a FormData object", ->
986
+ promise = up.proxy.get(url: '/foo', params: new FormData())
948
987
  expect(promise).toBeUndefined()
949
988
 
950
989
  describe 'up.proxy.set', ->
@@ -962,7 +1001,7 @@ describe 'up.proxy', ->
962
1001
  it 'does nothing if the given request is not cached'
963
1002
 
964
1003
  it 'does not crash when passed a request with FormData (bugfix)', ->
965
- removal = -> up.proxy.remove(url: '/path', data: new FormData())
1004
+ removal = -> up.proxy.remove(url: '/path', params: new FormData())
966
1005
  expect(removal).not.toThrowError()
967
1006
 
968
1007
  describe 'up.proxy.clear', ->
@@ -1041,7 +1080,7 @@ describe 'up.proxy', ->
1041
1080
 
1042
1081
  Trigger.hoverSequence($link)
1043
1082
 
1044
- next.after 2, =>
1083
+ next.after 50, =>
1045
1084
  expect(jasmine.Ajax.requests.count()).toEqual(1)
1046
1085
 
1047
1086
  @respondWith
@@ -1067,7 +1106,7 @@ describe 'up.proxy', ->
1067
1106
  expect(window).not.toHaveUnhandledRejections() if REJECTION_EVENTS_SUPPORTED
1068
1107
 
1069
1108
  it 'triggers a separate AJAX request when hovered multiple times and the cache expires between hovers', asyncSpec (next) ->
1070
- up.proxy.config.cacheExpiry = 50
1109
+ up.proxy.config.cacheExpiry = 100
1071
1110
  up.proxy.config.preloadDelay = 0
1072
1111
 
1073
1112
  $element = affix('a[href="/foo"][up-preload]')
@@ -1075,17 +1114,17 @@ describe 'up.proxy', ->
1075
1114
 
1076
1115
  Trigger.hoverSequence($element)
1077
1116
 
1078
- next.after 1, =>
1117
+ next.after 10, =>
1079
1118
  expect(jasmine.Ajax.requests.count()).toEqual(1)
1080
1119
 
1081
- next.after 1, =>
1120
+ next.after 10, =>
1082
1121
  Trigger.hoverSequence($element)
1083
1122
 
1084
- next.after 1, =>
1123
+ next.after 10, =>
1085
1124
  expect(jasmine.Ajax.requests.count()).toEqual(1)
1086
1125
 
1087
- next.after 60, =>
1126
+ next.after 150, =>
1088
1127
  Trigger.hoverSequence($element)
1089
1128
 
1090
- next.after 1, =>
1129
+ next.after 30, =>
1091
1130
  expect(jasmine.Ajax.requests.count()).toEqual(2)
@@ -1,4 +1,6 @@
1
1
  describe 'up.syntax', ->
2
+
3
+ u = up.util
2
4
 
3
5
  describe 'JavaScript functions', ->
4
6
 
@@ -96,28 +98,38 @@ describe 'up.syntax', ->
96
98
  promiseState(promise).then (result) ->
97
99
  expect(result.state).toEqual('fulfilled')
98
100
 
99
- it 'parses an up-data attribute as JSON and passes the parsed object as a second argument to the initializer', ->
101
+ describe 'passing of [up-data]', ->
102
+
103
+ it 'parses an [up-data] attribute as JSON and passes the parsed object as a second argument to the compiler', ->
104
+ observeArgs = jasmine.createSpy()
105
+ up.compiler '.child', ($element, data) ->
106
+ observeArgs($element.attr('class'), data)
107
+
108
+ data = { key1: 'value1', key2: 'value2' }
109
+
110
+ $tag = affix(".child").attr('up-data', JSON.stringify(data))
111
+ up.hello($tag)
100
112
 
101
- observeArgs = jasmine.createSpy()
102
- up.compiler '.child', ($element, data) ->
103
- observeArgs($element.attr('class'), data)
113
+ expect(observeArgs).toHaveBeenCalledWith('child', data)
104
114
 
105
- data = { key1: 'value1', key2: 'value2' }
115
+ it 'passes an empty object as a second argument to the compiler if there is no [up-data] attribute', ->
116
+ observeArgs = jasmine.createSpy()
117
+ up.compiler '.child', ($element, data) ->
118
+ observeArgs($element.attr('class'), data)
106
119
 
107
- $tag = affix(".child").attr('up-data', JSON.stringify(data))
108
- up.hello($tag)
120
+ up.hello(affix(".child"))
109
121
 
110
- expect(observeArgs).toHaveBeenCalledWith('child', data)
122
+ expect(observeArgs).toHaveBeenCalledWith('child', {})
111
123
 
112
- it 'passes an empty object as a second argument to the initializer if there is no up-data attribute', ->
124
+ it 'does not parse an [up-data] attribute if the compiler function only takes a single argument', ->
125
+ parseDataSpy = spyOn(up.syntax, 'data').and.returnValue({})
113
126
 
114
- observeArgs = jasmine.createSpy()
115
- up.compiler '.child', ($element, data) ->
116
- observeArgs($element.attr('class'), data)
127
+ $child = affix(".child")
117
128
 
118
- up.hello(affix(".child"))
129
+ up.compiler '.child', ($element) -> # no-op
130
+ up.hello($child)
119
131
 
120
- expect(observeArgs).toHaveBeenCalledWith('child', {})
132
+ expect(parseDataSpy).not.toHaveBeenCalled()
121
133
 
122
134
  it 'compiles matching elements one-by-one', ->
123
135
  compiler = jasmine.createSpy('compiler')
@@ -143,6 +155,13 @@ describe 'up.syntax', ->
143
155
  expect(compiler.calls.count()).toEqual(1)
144
156
  expect(compiler).toHaveBeenCalledWith($both)
145
157
 
158
+ it 'throws an error if the batch compiler returns a destructor', ->
159
+ destructor = ->
160
+ up.compiler '.element', { batch: true }, ($element) -> destructor
161
+ $container = affix('.element')
162
+ compile = -> up.hello($container)
163
+ expect(compile).toThrowError(/cannot return destructor/i)
164
+
146
165
  describe 'with { keep } option', ->
147
166
 
148
167
  it 'adds an up-keep attribute to the fragment during compilation', ->
@@ -238,4 +257,18 @@ describe 'up.syntax', ->
238
257
 
239
258
  it 'should have tests'
240
259
 
241
-
260
+ describe 'up.syntax.data', ->
261
+
262
+ it 'returns the [up-data] attribute of the given element, parsed as JSON', ->
263
+ $element = affix('.element').attr('up-data', '{ "foo": 1, "bar": 2 }')
264
+ data = up.syntax.data($element)
265
+ expect(data).toEqual(foo: 1, bar: 2)
266
+
267
+ it 'returns en empty object if the given element has no [up-data] attribute', ->
268
+ $element = affix('.element')
269
+ data = up.syntax.data($element)
270
+ expect(data).toEqual({})
271
+
272
+ it 'returns undefined if undefined is passed instead of an element', ->
273
+ data = up.syntax.data(undefined)
274
+ expect(data).toBeUndefined()
@@ -4,6 +4,13 @@ describe 'up.util', ->
4
4
 
5
5
  describe 'JavaScript functions', ->
6
6
 
7
+ # describe 'up.util.flatMap', ->
8
+ #
9
+ # it 'collects the Array results of the given map function, then concatenates the result arrays into one flat array', ->
10
+ # fun = (x) -> [x, x]
11
+ # result = up.util.flatMap([1, 2, 3], fun)
12
+ # expect(result).toEqual([1, 1, 2, 2, 3, 3])
13
+
7
14
  describe 'up.util.uniq', ->
8
15
 
9
16
  it 'returns the given array with duplicates elements removed', ->
@@ -35,6 +42,56 @@ describe 'up.util', ->
35
42
  result = up.util.uniqBy(input, 'length')
36
43
  expect(result).toEqual ['foo', 'apple', 'orange']
37
44
 
45
+ # describe 'up.util.parsePath', ->
46
+ #
47
+ # it 'parses a plain name', ->
48
+ # path = up.util.parsePath("foo")
49
+ # expect(path).toEqual ['foo']
50
+ #
51
+ # it 'considers underscores to be part of a name', ->
52
+ # path = up.util.parsePath("foo_bar")
53
+ # expect(path).toEqual ['foo_bar']
54
+ #
55
+ # it 'considers dashes to be part of a name', ->
56
+ # path = up.util.parsePath("foo-bar")
57
+ # expect(path).toEqual ['foo-bar']
58
+ #
59
+ # it 'parses dot-separated names into multiple path segments', ->
60
+ # path = up.util.parsePath('foo.bar.baz')
61
+ # expect(path).toEqual ['foo', 'bar', 'baz']
62
+ #
63
+ # it 'parses nested params notation with square brackets', ->
64
+ # path = up.util.parsePath('user[account][email]')
65
+ # expect(path).toEqual ['user', 'account', 'email']
66
+ #
67
+ # it 'parses double quotes in square brackets', ->
68
+ # path = up.util.parsePath('user["account"]["email"]')
69
+ # expect(path).toEqual ['user', 'account', 'email']
70
+ #
71
+ # it 'parses single quotes in square brackets', ->
72
+ # path = up.util.parsePath("user['account']['email']")
73
+ # expect(path).toEqual ['user', 'account', 'email']
74
+ #
75
+ # it 'allows square brackets inside quotes', ->
76
+ # path = up.util.parsePath("element['a[up-href]']")
77
+ # expect(path).toEqual ['element', 'a[up-href]']
78
+ #
79
+ # it 'allows single quotes inside double quotes', ->
80
+ # path = up.util.parsePath("element[\"a[up-href='foo']\"]")
81
+ # expect(path).toEqual ['element', "a[up-href='foo']"]
82
+ #
83
+ # it 'allows double quotes inside single quotes', ->
84
+ # path = up.util.parsePath("element['a[up-href=\"foo\"]']")
85
+ # expect(path).toEqual ['element', 'a[up-href="foo"]']
86
+ #
87
+ # it 'allows dots in square brackets when it is quoted', ->
88
+ # path = up.util.parsePath('elements[".foo"]')
89
+ # expect(path).toEqual ['elements', '.foo']
90
+ #
91
+ # it 'allows different notation for each segment', ->
92
+ # path = up.util.parsePath('foo.bar[baz]["bam"][\'qux\']')
93
+ # expect(path).toEqual ['foo', 'bar', 'baz', 'bam', 'qux']
94
+
38
95
  describe 'up.util.map', ->
39
96
 
40
97
  it 'creates a new array of values by calling the given function on each item of the given array', ->
@@ -211,6 +268,17 @@ describe 'up.util', ->
211
268
  fooBar: 'one'
212
269
  barBaz: 'two'
213
270
 
271
+ # describe 'up.util.lowerCaseKeys', ->
272
+ #
273
+ # it "returns a copy of the given object will all keys in lower case", ->
274
+ # input =
275
+ # 'A-B': 'C-D'
276
+ # 'E-F': 'G-H'
277
+ # result = up.util.lowerCaseKeys(input)
278
+ # expect(result).toEqual
279
+ # 'a-b': 'C-D'
280
+ # 'e-f': 'G-H'
281
+
214
282
  describe 'up.util.DivertibleChain', ->
215
283
 
216
284
  it "instantiates a task queue whose (2..n)th tasks can be changed by calling '.asap'", (done) ->
@@ -569,7 +637,11 @@ describe 'up.util', ->
569
637
  expect(up.util.selectorForElement($element)).toBe('input[name="name-value"]')
570
638
 
571
639
  it "prefers using the element's classes to using the element's ARIA label", ->
572
- $element = affix('div.class1.class2')
640
+ $element = affix('div.class1.class2[aria-label="ARIA label value"]')
641
+ expect(up.util.selectorForElement($element)).toBe(".class1.class2")
642
+
643
+ it 'does not use Unpoly classes to compose a class selector', ->
644
+ $element = affix('div.class1.up-current.class2')
573
645
  expect(up.util.selectorForElement($element)).toBe(".class1.class2")
574
646
 
575
647
  it "prefers using the element's ARIA label to using the element's tag name", ->
@@ -862,56 +934,6 @@ describe 'up.util', ->
862
934
  up.util.remove(array, obj2)
863
935
  expect(array).toEqual [obj1, obj3]
864
936
 
865
-
866
- describe 'up.util.requestDataAsQuery', ->
867
-
868
- encodedOpeningBracket = '%5B'
869
- encodedClosingBracket = '%5D'
870
- encodedSpace = '%20'
871
-
872
- it 'returns the query section for the given object', ->
873
- string = up.util.requestDataAsQuery('foo-key': 'foo value', 'bar-key': 'bar value')
874
- expect(string).toEqual("foo-key=foo#{encodedSpace}value&bar-key=bar#{encodedSpace}value")
875
-
876
- it 'returns the query section for the given nested object', ->
877
- string = up.util.requestDataAsQuery('foo-key': { 'bar-key': 'bar-value' }, 'bam-key': 'bam-value')
878
- expect(string).toEqual("foo-key#{encodedOpeningBracket}bar-key#{encodedClosingBracket}=bar-value&bam-key=bam-value")
879
-
880
- it 'returns the query section for the given array with { name } and { value } keys', ->
881
- string = up.util.requestDataAsQuery([
882
- { name: 'foo-key', value: 'foo value' },
883
- { name: 'bar-key', value: 'bar value' }
884
- ])
885
- expect(string).toEqual("foo-key=foo#{encodedSpace}value&bar-key=bar#{encodedSpace}value")
886
-
887
- it 'returns a given query string', ->
888
- string = up.util.requestDataAsQuery('foo=bar')
889
- expect(string).toEqual('foo=bar')
890
-
891
- it 'strips a leading question mark from the given query string', ->
892
- string = up.util.requestDataAsQuery('?foo=bar')
893
- expect(string).toEqual('foo=bar')
894
-
895
- it 'returns an empty string for an empty object', ->
896
- string = up.util.requestDataAsQuery({})
897
- expect(string).toEqual('')
898
-
899
- it 'returns an empty string for an empty string', ->
900
- string = up.util.requestDataAsQuery('')
901
- expect(string).toEqual('')
902
-
903
- it 'returns an empty string for undefined', ->
904
- string = up.util.requestDataAsQuery(undefined)
905
- expect(string).toEqual('')
906
-
907
- it 'URL-encodes characters in the key and value', ->
908
- string = up.util.requestDataAsQuery({ 'äpfel': 'bäume' })
909
- expect(string).toEqual('%C3%A4pfel=b%C3%A4ume')
910
-
911
- it 'URL-encodes plus characters', ->
912
- string = up.util.requestDataAsQuery({ 'my+key': 'my+value' })
913
- expect(string).toEqual('my%2Bkey=my%2Bvalue')
914
-
915
937
  describe 'up.util.unresolvablePromise', ->
916
938
 
917
939
  it 'return a pending promise', (done) ->
@@ -925,84 +947,6 @@ describe 'up.util', ->
925
947
  two = up.util.unresolvablePromise()
926
948
  expect(one).not.toBe(two)
927
949
 
928
- describe 'up.util.requestDataAsArray', ->
929
-
930
- it 'normalized null to an empty array', ->
931
- array = up.util.requestDataAsArray(null)
932
- expect(array).toEqual([])
933
-
934
- it 'normalized undefined to an empty array', ->
935
- array = up.util.requestDataAsArray(undefined)
936
- expect(array).toEqual([])
937
-
938
- it 'normalizes an object hash to an array of objects with { name } and { value } keys', ->
939
- array = up.util.requestDataAsArray(
940
- 'foo-key': 'foo-value'
941
- 'bar-key': 'bar-value'
942
- )
943
- expect(array).toEqual([
944
- { name: 'foo-key', value: 'foo-value' },
945
- { name: 'bar-key', value: 'bar-value' },
946
- ])
947
-
948
- it 'normalizes a nested object hash to a flat array using param naming conventions', ->
949
- array = up.util.requestDataAsArray(
950
- 'foo-key': 'foo-value'
951
- 'bar-key': {
952
- 'bam-key': 'bam-value'
953
- 'baz-key': {
954
- 'qux-key': 'qux-value'
955
- }
956
- }
957
- )
958
- expect(array).toEqual([
959
- { name: 'foo-key', value: 'foo-value' },
960
- { name: 'bar-key[bam-key]', value: 'bam-value' },
961
- { name: 'bar-key[baz-key][qux-key]', value: 'qux-value' },
962
- ])
963
-
964
- it 'returns a given array without modification', ->
965
- array = up.util.requestDataAsArray([
966
- { name: 'foo-key', value: 'foo-value' },
967
- { name: 'bar-key', value: 'bar-value' },
968
- ])
969
- expect(array).toEqual([
970
- { name: 'foo-key', value: 'foo-value' },
971
- { name: 'bar-key', value: 'bar-value' },
972
- ])
973
-
974
- it 'does not URL-encode special characters keys or values', ->
975
- array = up.util.requestDataAsArray(
976
- 'äpfel': { 'bäume': 'börse' }
977
- )
978
- expect(array).toEqual([
979
- { name: 'äpfel[bäume]', value: 'börse' },
980
- ])
981
-
982
- it 'does not URL-encode spaces in keys or values', ->
983
- array = up.util.requestDataAsArray(
984
- 'my key': 'my value'
985
- )
986
- expect(array).toEqual([
987
- { name: 'my key', value: 'my value' },
988
- ])
989
-
990
- it 'does not URL-encode ampersands in keys or values', ->
991
- array = up.util.requestDataAsArray(
992
- 'my&key': 'my&value'
993
- )
994
- expect(array).toEqual([
995
- { name: 'my&key', value: 'my&value' },
996
- ])
997
-
998
- it 'does not URL-encode equal signs in keys or values', ->
999
- array = up.util.requestDataAsArray(
1000
- 'my=key': 'my=value'
1001
- )
1002
- expect(array).toEqual([
1003
- { name: 'my=key', value: 'my=value' },
1004
- ])
1005
-
1006
950
  describe 'up.util.flatten', ->
1007
951
 
1008
952
  it 'flattens the given array', ->
@@ -1105,6 +1049,9 @@ describe 'up.util', ->
1105
1049
  it 'returns true for an object literal', ->
1106
1050
  expect(up.util.isOptions({ foo: 'bar'})).toBe(true)
1107
1051
 
1052
+ it 'returns true for a prototype-less object', ->
1053
+ expect(up.util.isOptions(Object.create(null))).toBe(true)
1054
+
1108
1055
  it 'returns false for undefined', ->
1109
1056
  expect(up.util.isOptions(undefined)).toBe(false)
1110
1057
 
@@ -1116,18 +1063,24 @@ describe 'up.util', ->
1116
1063
  fn.key = 'value'
1117
1064
  expect(up.util.isOptions(fn)).toBe(false)
1118
1065
 
1119
- it 'returns false for an array', ->
1066
+ it 'returns false for an Array', ->
1120
1067
  expect(up.util.isOptions(['foo'])).toBe(false)
1121
1068
 
1122
1069
  it 'returns false for a jQuery collection', ->
1123
1070
  expect(up.util.isOptions($('body'))).toBe(false)
1124
1071
 
1125
- it 'returns false for a promise', ->
1072
+ it 'returns false for a Promise', ->
1126
1073
  expect(up.util.isOptions(Promise.resolve())).toBe(false)
1127
1074
 
1128
1075
  it 'returns false for a FormData object', ->
1129
1076
  expect(up.util.isOptions(new FormData())).toBe(false)
1130
1077
 
1078
+ it 'returns false for a Date', ->
1079
+ expect(up.util.isOptions(new Date())).toBe(false)
1080
+
1081
+ it 'returns false for a RegExp', ->
1082
+ expect(up.util.isOptions(new RegExp('foo'))).toBe(false)
1083
+
1131
1084
  describe 'up.util.isObject', ->
1132
1085
 
1133
1086
  it 'returns true for an Object instance', ->
@@ -1159,6 +1112,70 @@ describe 'up.util', ->
1159
1112
  it 'returns true for a FormData object', ->
1160
1113
  expect(up.util.isObject(new FormData())).toBe(true)
1161
1114
 
1115
+ describe 'up.util.merge', ->
1116
+
1117
+ it 'merges the given objects', ->
1118
+ obj = { a: '1', b: '2' }
1119
+ other = { b: '3', c: '4' }
1120
+ obj = up.util.merge(obj, other)
1121
+ expect(obj).toEqual { a: '1', b: '3', c: '4' }
1122
+
1123
+ it 'overrides (not merges) keys with object value', ->
1124
+ obj = { a: '1', b: { c: '2', d: '3' } }
1125
+ other = { e: '4', b: { f: '5', g: '6' }}
1126
+ obj = up.util.merge(obj, other)
1127
+ expect(obj).toEqual { a: '1', e: '4', b: { f: '5', g: '6' } }
1128
+
1129
+ it 'ignores undefined arguments', ->
1130
+ obj = { a: 1, b: 2 }
1131
+
1132
+ result = up.util.merge(obj, undefined)
1133
+ expect(result).toEqual { a: 1, b: 2 }
1134
+
1135
+ reverseResult = up.util.merge(undefined, obj)
1136
+ expect(reverseResult).toEqual { a: 1, b: 2 }
1137
+
1138
+ it 'ignores null arguments', ->
1139
+ obj = { a: 1, b: 2 }
1140
+
1141
+ result = up.util.merge(obj, null)
1142
+ expect(result).toEqual { a: 1, b: 2 }
1143
+
1144
+ reverseResult = up.util.merge(null, obj)
1145
+ expect(reverseResult).toEqual { a: 1, b: 2 }
1146
+
1147
+ # describe 'up.util.deepMerge', ->
1148
+ #
1149
+ # it 'recursively merges the given objects', ->
1150
+ # obj = { a: '1', b: { c: '2', d: '3' } }
1151
+ # other = { e: '4', b: { f: '5', g: '6' }}
1152
+ # obj = up.util.deepMerge(obj, other)
1153
+ # expect(obj).toEqual { a: '1', e: '4', b: { c: '2', d: '3', f: '5', g: '6' } }
1154
+ #
1155
+ # it 'ignores undefined arguments', ->
1156
+ # obj = { a: 1, b: 2 }
1157
+ #
1158
+ # result = up.util.deepMerge(obj, undefined)
1159
+ # expect(result).toEqual { a: 1, b: 2 }
1160
+ #
1161
+ # reverseResult = up.util.deepMerge(undefined, obj)
1162
+ # expect(reverseResult).toEqual { a: 1, b: 2 }
1163
+ #
1164
+ # it 'ignores null arguments', ->
1165
+ # obj = { a: 1, b: 2 }
1166
+ #
1167
+ # result = up.util.deepMerge(obj, null)
1168
+ # expect(result).toEqual { a: 1, b: 2 }
1169
+ #
1170
+ # reverseResult = up.util.deepMerge(null, obj)
1171
+ # expect(reverseResult).toEqual { a: 1, b: 2 }
1172
+ #
1173
+ # it 'overwrites (and does not concatenate) array values', ->
1174
+ # obj = { a: ['1', '2'] }
1175
+ # other = { a: ['3', '4'] }
1176
+ # obj = up.util.deepMerge(obj, other)
1177
+ # expect(obj).toEqual { a: ['3', '4'] }
1178
+
1162
1179
  describe 'up.util.memoize', ->
1163
1180
 
1164
1181
  it 'returns a function that calls the memoized function', ->