unpoly-rails 0.57.0 → 0.60.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 (186) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +393 -1
  3. data/Gemfile.lock +5 -2
  4. data/README.md +1 -1
  5. data/README_RAILS.md +1 -1
  6. data/Rakefile +10 -1
  7. data/design/es6.js +32 -0
  8. data/design/ie11.txt +9 -0
  9. data/design/measure_jquery/element_list.js +41 -0
  10. data/design/measure_jquery/up.on_vs_addEventListener.js +56 -0
  11. data/design/todo_jquery.txt +13 -0
  12. data/dist/unpoly-bootstrap3.js +8 -8
  13. data/dist/unpoly-bootstrap3.min.js +1 -1
  14. data/dist/unpoly.css +22 -20
  15. data/dist/unpoly.js +6990 -5336
  16. data/dist/unpoly.min.css +1 -1
  17. data/dist/unpoly.min.js +4 -4
  18. data/lib/assets/javascripts/unpoly-bootstrap3/viewport-ext.coffee +5 -0
  19. data/lib/assets/javascripts/unpoly.coffee +8 -6
  20. data/lib/assets/javascripts/unpoly/browser.coffee.erb +23 -118
  21. data/lib/assets/javascripts/unpoly/classes/body_shifter.coffee +36 -0
  22. data/lib/assets/javascripts/unpoly/classes/cache.coffee +4 -4
  23. data/lib/assets/javascripts/unpoly/classes/compile_pass.coffee +45 -39
  24. data/lib/assets/javascripts/unpoly/classes/config.coffee +9 -0
  25. data/lib/assets/javascripts/unpoly/classes/css_transition.coffee +18 -27
  26. data/lib/assets/javascripts/unpoly/classes/divertible_chain.coffee +39 -0
  27. data/lib/assets/javascripts/unpoly/classes/event_listener.coffee +116 -0
  28. data/lib/assets/javascripts/unpoly/classes/extract_cascade.coffee +8 -8
  29. data/lib/assets/javascripts/unpoly/classes/extract_plan.coffee +19 -19
  30. data/lib/assets/javascripts/unpoly/classes/field_observer.coffee +54 -31
  31. data/lib/assets/javascripts/unpoly/classes/{focus_tracker.coffee → focus_follower.coffee} +2 -2
  32. data/lib/assets/javascripts/unpoly/classes/follow_variant.coffee +25 -25
  33. data/lib/assets/javascripts/unpoly/classes/html_parser.coffee +4 -11
  34. data/lib/assets/javascripts/unpoly/classes/motion_controller.coffee +157 -0
  35. data/lib/assets/javascripts/unpoly/classes/params.coffee.erb +525 -0
  36. data/lib/assets/javascripts/unpoly/classes/record.coffee +8 -2
  37. data/lib/assets/javascripts/unpoly/classes/rect.js +21 -0
  38. data/lib/assets/javascripts/unpoly/classes/request.coffee +41 -35
  39. data/lib/assets/javascripts/unpoly/classes/response.coffee +7 -3
  40. data/lib/assets/javascripts/unpoly/classes/reveal_motion.coffee +102 -0
  41. data/lib/assets/javascripts/unpoly/classes/scroll_motion.coffee +67 -0
  42. data/lib/assets/javascripts/unpoly/classes/selector.coffee +60 -0
  43. data/lib/assets/javascripts/unpoly/classes/tether.coffee +105 -0
  44. data/lib/assets/javascripts/unpoly/classes/url_set.coffee +12 -7
  45. data/lib/assets/javascripts/unpoly/element.coffee.erb +1126 -0
  46. data/lib/assets/javascripts/unpoly/event.coffee.erb +437 -0
  47. data/lib/assets/javascripts/unpoly/feedback.coffee +73 -94
  48. data/lib/assets/javascripts/unpoly/form.coffee.erb +188 -181
  49. data/lib/assets/javascripts/unpoly/{dom.coffee.erb → fragment.coffee.erb} +250 -283
  50. data/lib/assets/javascripts/unpoly/framework.coffee +67 -0
  51. data/lib/assets/javascripts/unpoly/history.coffee +29 -28
  52. data/lib/assets/javascripts/unpoly/legacy.coffee +60 -0
  53. data/lib/assets/javascripts/unpoly/link.coffee.erb +127 -119
  54. data/lib/assets/javascripts/unpoly/log.coffee +99 -19
  55. data/lib/assets/javascripts/unpoly/modal.coffee.erb +95 -118
  56. data/lib/assets/javascripts/unpoly/motion.coffee.erb +158 -138
  57. data/lib/assets/javascripts/unpoly/namespace.coffee.erb +0 -5
  58. data/lib/assets/javascripts/unpoly/popup.coffee.erb +119 -102
  59. data/lib/assets/javascripts/unpoly/protocol.coffee +11 -15
  60. data/lib/assets/javascripts/unpoly/proxy.coffee +62 -65
  61. data/lib/assets/javascripts/unpoly/radio.coffee +3 -5
  62. data/lib/assets/javascripts/unpoly/rails.coffee +8 -9
  63. data/lib/assets/javascripts/unpoly/syntax.coffee.erb +173 -125
  64. data/lib/assets/javascripts/unpoly/toast.coffee +25 -24
  65. data/lib/assets/javascripts/unpoly/tooltip.coffee +89 -79
  66. data/lib/assets/javascripts/unpoly/util.coffee.erb +579 -1074
  67. data/lib/assets/javascripts/unpoly/{layout.coffee.erb → viewport.coffee.erb} +334 -264
  68. data/lib/assets/stylesheets/unpoly/dom.sass +1 -1
  69. data/lib/assets/stylesheets/unpoly/layout.sass +2 -0
  70. data/lib/assets/stylesheets/unpoly/popup.sass +0 -1
  71. data/lib/assets/stylesheets/unpoly/tooltip.sass +17 -12
  72. data/lib/unpoly/rails/version.rb +1 -1
  73. data/package.json +1 -2
  74. data/spec_app/Gemfile +2 -1
  75. data/spec_app/Gemfile.lock +38 -27
  76. data/spec_app/app/assets/javascripts/integration_test.coffee +1 -0
  77. data/spec_app/app/assets/javascripts/jasmine_specs.coffee +1 -2
  78. data/spec_app/app/assets/stylesheets/integration_test.sass +14 -1
  79. data/spec_app/app/controllers/scroll_test_controller.rb +5 -0
  80. data/spec_app/app/views/css_test/modal.erb +6 -6
  81. data/spec_app/app/views/css_test/popup.erb +44 -18
  82. data/spec_app/app/views/css_test/tooltip.erb +23 -4
  83. data/spec_app/app/views/error_test/trigger.erb +1 -1
  84. data/spec_app/app/views/form_test/basics/new.erb +1 -3
  85. data/spec_app/app/views/pages/start.erb +9 -2
  86. data/spec_app/app/views/reveal_test/long1.erb +1 -1
  87. data/spec_app/app/views/reveal_test/long2.erb +1 -1
  88. data/spec_app/app/views/reveal_test/within_document_viewport.erb +24 -0
  89. data/spec_app/app/views/reveal_test/within_overflowing_div_viewport.erb +28 -0
  90. data/spec_app/app/views/scroll_test/long1.erb +30 -0
  91. data/spec_app/config/routes.rb +1 -0
  92. data/spec_app/spec/javascripts/helpers/agent_detector.coffee +3 -0
  93. data/spec_app/spec/javascripts/helpers/async_sequence.js.coffee +1 -0
  94. data/spec_app/spec/javascripts/helpers/browser_switches.js.coffee +17 -5
  95. data/spec_app/spec/javascripts/helpers/enable_logging.js.coffee +1 -1
  96. data/spec_app/spec/javascripts/helpers/fixture.js.coffee +25 -0
  97. data/spec_app/spec/javascripts/helpers/jquery_no_conflict.js +1 -0
  98. data/spec_app/spec/javascripts/helpers/last_request.js.coffee +1 -0
  99. data/spec_app/spec/javascripts/helpers/mock_ajax.js.coffee +1 -1
  100. data/spec_app/spec/javascripts/helpers/parse_form_data.js.coffee +2 -2
  101. data/spec_app/spec/javascripts/helpers/protect_jasmine_runner.coffee +4 -1
  102. data/spec_app/spec/javascripts/helpers/remove_body_margin.js.coffee +3 -0
  103. data/spec_app/spec/javascripts/helpers/reset_history.js.coffee +2 -1
  104. data/spec_app/spec/javascripts/helpers/reset_knife.js.coffee +2 -2
  105. data/spec_app/spec/javascripts/helpers/reset_up.js.coffee +18 -11
  106. data/spec_app/spec/javascripts/helpers/restore_body_scroll.js.coffee +3 -0
  107. data/spec_app/spec/javascripts/helpers/show_lib_versions.coffee +3 -0
  108. data/spec_app/spec/javascripts/helpers/spec_util.coffee +47 -0
  109. data/spec_app/spec/javascripts/helpers/to_be_around.js.coffee +3 -0
  110. data/spec_app/spec/javascripts/helpers/to_be_array.coffee +5 -0
  111. data/spec_app/spec/javascripts/helpers/to_be_attached.coffee +6 -2
  112. data/spec_app/spec/javascripts/helpers/to_be_blank.js.coffee +3 -0
  113. data/spec_app/spec/javascripts/helpers/to_be_detached.coffee +6 -2
  114. data/spec_app/spec/javascripts/helpers/to_be_element.js.coffee +8 -0
  115. data/spec_app/spec/javascripts/helpers/to_be_error.coffee +3 -0
  116. data/spec_app/spec/javascripts/helpers/to_be_given.js.coffee +3 -0
  117. data/spec_app/spec/javascripts/helpers/to_be_hidden.js.coffee +8 -0
  118. data/spec_app/spec/javascripts/helpers/to_be_missing.js.coffee +3 -0
  119. data/spec_app/spec/javascripts/helpers/to_be_present.js.coffee +3 -0
  120. data/spec_app/spec/javascripts/helpers/to_be_scrolled_to.coffee +3 -0
  121. data/spec_app/spec/javascripts/helpers/to_be_visible.js.coffee +9 -0
  122. data/spec_app/spec/javascripts/helpers/to_contain.js.coffee +3 -0
  123. data/spec_app/spec/javascripts/helpers/to_end_with.js.coffee +3 -0
  124. data/spec_app/spec/javascripts/helpers/to_equal_jquery.js.coffee +1 -2
  125. data/spec_app/spec/javascripts/helpers/to_equal_node_list.coffee +7 -0
  126. data/spec_app/spec/javascripts/helpers/to_equal_via_is_equal.js.coffee +7 -0
  127. data/spec_app/spec/javascripts/helpers/to_have_class.js.coffee +10 -0
  128. data/spec_app/spec/javascripts/helpers/to_have_descendant.js.coffee +10 -0
  129. data/spec_app/spec/javascripts/helpers/to_have_length.js.coffee +8 -0
  130. data/spec_app/spec/javascripts/helpers/to_have_opacity.coffee +7 -3
  131. data/spec_app/spec/javascripts/helpers/to_have_own_property.js.coffee +3 -0
  132. data/spec_app/spec/javascripts/helpers/to_have_request_method.js.coffee +1 -0
  133. data/spec_app/spec/javascripts/helpers/to_have_text.js.coffee +9 -0
  134. data/spec_app/spec/javascripts/helpers/to_have_unhandled_rejections.coffee +0 -21
  135. data/spec_app/spec/javascripts/helpers/to_match_list.coffee +14 -0
  136. data/spec_app/spec/javascripts/helpers/to_match_selector.coffee +3 -0
  137. data/spec_app/spec/javascripts/helpers/to_match_text.js.coffee +4 -1
  138. data/spec_app/spec/javascripts/helpers/to_match_url.coffee +1 -0
  139. data/spec_app/spec/javascripts/helpers/trigger.js.coffee +91 -7
  140. data/spec_app/spec/javascripts/helpers/wait_until_dom_ready.js.coffee +3 -0
  141. data/spec_app/spec/javascripts/up/browser_spec.js.coffee +23 -90
  142. data/spec_app/spec/javascripts/up/classes/cache_spec.js.coffee +3 -0
  143. data/spec_app/spec/javascripts/up/classes/config_spec.coffee +24 -0
  144. data/spec_app/spec/javascripts/up/classes/divertible_chain_spec.coffee +45 -0
  145. data/spec_app/spec/javascripts/up/classes/focus_tracker_spec.coffee +5 -2
  146. data/spec_app/spec/javascripts/up/classes/params_spec.coffee +557 -0
  147. data/spec_app/spec/javascripts/up/classes/request_spec.coffee +7 -4
  148. data/spec_app/spec/javascripts/up/classes/scroll_motion_spec.js.coffee +51 -0
  149. data/spec_app/spec/javascripts/up/classes/store/memory_spec.js.coffee +3 -0
  150. data/spec_app/spec/javascripts/up/classes/store/session_spec.js.coffee +3 -2
  151. data/spec_app/spec/javascripts/up/element_spec.coffee +897 -0
  152. data/spec_app/spec/javascripts/up/event_spec.js.coffee +496 -0
  153. data/spec_app/spec/javascripts/up/feedback_spec.js.coffee +69 -48
  154. data/spec_app/spec/javascripts/up/form_spec.js.coffee +252 -194
  155. data/spec_app/spec/javascripts/up/{dom_spec.js.coffee → fragment_spec.js.coffee} +381 -388
  156. data/spec_app/spec/javascripts/up/history_spec.js.coffee +21 -19
  157. data/spec_app/spec/javascripts/up/jquery_spec.js.coffee +4 -0
  158. data/spec_app/spec/javascripts/up/legacy_spec.js.coffee +27 -0
  159. data/spec_app/spec/javascripts/up/link_spec.js.coffee +163 -160
  160. data/spec_app/spec/javascripts/up/log_spec.js.coffee +85 -12
  161. data/spec_app/spec/javascripts/up/modal_spec.js.coffee +141 -123
  162. data/spec_app/spec/javascripts/up/motion_spec.js.coffee +117 -113
  163. data/spec_app/spec/javascripts/up/popup_spec.js.coffee +60 -77
  164. data/spec_app/spec/javascripts/up/protocol_spec.js.coffee +1 -0
  165. data/spec_app/spec/javascripts/up/proxy_spec.js.coffee +85 -78
  166. data/spec_app/spec/javascripts/up/radio_spec.js.coffee +29 -22
  167. data/spec_app/spec/javascripts/up/rails_spec.js.coffee +14 -13
  168. data/spec_app/spec/javascripts/up/spec_spec.js.coffee +9 -0
  169. data/spec_app/spec/javascripts/up/syntax_spec.js.coffee +96 -66
  170. data/spec_app/spec/javascripts/up/toast_spec.js.coffee +37 -0
  171. data/spec_app/spec/javascripts/up/tooltip_spec.js.coffee +31 -47
  172. data/spec_app/spec/javascripts/up/util_spec.js.coffee +725 -562
  173. data/spec_app/spec/javascripts/up/{layout_spec.js.coffee → viewport_spec.js.coffee} +175 -149
  174. metadata +57 -19
  175. data/lib/assets/javascripts/unpoly-bootstrap3/layout-ext.coffee +0 -5
  176. data/lib/assets/javascripts/unpoly/bus.coffee.erb +0 -518
  177. data/lib/assets/javascripts/unpoly/classes/extract_step.coffee +0 -4
  178. data/lib/assets/javascripts/unpoly/classes/motion_tracker.coffee +0 -125
  179. data/lib/assets/javascripts/unpoly/params.coffee.erb +0 -522
  180. data/spec_app/spec/javascripts/helpers/append_fixture.js.coffee +0 -8
  181. data/spec_app/spec/javascripts/up/bus_spec.js.coffee +0 -210
  182. data/spec_app/spec/javascripts/up/namespace_spec.js.coffee +0 -9
  183. data/spec_app/spec/javascripts/up/params_spec.coffee +0 -768
  184. data/spec_app/vendor/asset-libs/jasmine-fixture-1.3.4/jasmine-fixture.js +0 -433
  185. data/spec_app/vendor/asset-libs/jasmine-jquery-2.1.1/.bower.json +0 -26
  186. data/spec_app/vendor/asset-libs/jasmine-jquery-2.1.1/jasmine-jquery.js +0 -838
@@ -0,0 +1,37 @@
1
+ u = up.util
2
+ $ = jQuery
3
+
4
+ describe 'up.toast', ->
5
+
6
+ describe 'JavaScript functions', ->
7
+
8
+ describe 'up.toast.open()', ->
9
+
10
+ it 'opens a toast box with the given message', ->
11
+ up.toast.open('This is a message')
12
+ expect('.up-toast').toBeAttached()
13
+ expect($('.up-toast-message').text()).toContain('This is a message')
14
+
15
+ it 'opens a toast box with a close link', ->
16
+ up.toast.open('This is a message')
17
+ expect('.up-toast').toBeAttached()
18
+ $closeButton = $('.up-toast-action:contains("Close")')
19
+ expect($closeButton).toBeAttached()
20
+
21
+ Trigger.clickSequence($closeButton)
22
+
23
+ expect('.up-toast').not.toBeAttached()
24
+
25
+ it 'opens a toast box with the given custom action', ->
26
+ action =
27
+ label: 'Custom action'
28
+ callback: jasmine.createSpy('action callback')
29
+ up.toast.open('This is a message', { action })
30
+ $actionButton = $('.up-toast-action:contains("Custom action")')
31
+ expect($actionButton).toBeAttached()
32
+ expect(action.callback).not.toHaveBeenCalled()
33
+
34
+ Trigger.clickSequence($actionButton)
35
+
36
+ expect(action.callback).toHaveBeenCalled()
37
+
@@ -1,36 +1,38 @@
1
- describe 'up.tooltip', ->
1
+ u = up.util
2
+ e = up.element
3
+ $ = jQuery
2
4
 
3
- u = up.util
5
+ describe 'up.tooltip', ->
4
6
 
5
7
  describe 'JavaScript functions', ->
6
8
 
7
9
  describe 'up.tooltip.attach', ->
8
10
 
9
11
  it 'opens a tooltip with the given text', (done) ->
10
- $link = affix('span')
12
+ $link = $fixture('span')
11
13
  up.tooltip.attach($link, html: 'tooltip text').then ->
12
14
  $tooltip = $('.up-tooltip')
13
15
  expect($tooltip).toHaveText('tooltip text')
14
16
  done()
15
17
 
16
18
  it 'allows HTML for the tooltip text when contents are given as { html } option', (done) ->
17
- $link = affix('span')
19
+ $link = $fixture('span')
18
20
  up.tooltip.attach($link, html: '<b>text</b>').then ->
19
21
  $tooltip = $('.up-tooltip')
20
- expect($tooltip.html()).toEqual('<b>text</b>')
22
+ expect($tooltip.html()).toContain('<b>text</b>')
21
23
  done()
22
24
 
23
25
  it 'escapes HTML for the tooltip text when contents given as { text } option', (done) ->
24
- $link = affix('span')
26
+ $link = $fixture('span')
25
27
  up.tooltip.attach($link, text: '<b>text</b>').then ->
26
28
  $tooltip = $('.up-tooltip')
27
- expect($tooltip.html()).toEqual('&lt;b&gt;text&lt;/b&gt;')
29
+ expect($tooltip.html()).toContain('&lt;b&gt;text&lt;/b&gt;')
28
30
  done()
29
31
 
30
32
  describe 'positioning', ->
31
33
 
32
34
  beforeEach ->
33
- @$link = affix('span').text('button label')
35
+ @$link = $fixture('span').text('button label')
34
36
  @$link.css(
35
37
  position: 'absolute'
36
38
  left: '200px'
@@ -40,10 +42,12 @@ describe 'up.tooltip', ->
40
42
  )
41
43
 
42
44
  beforeEach ->
43
- @restoreBodyHeight = u.writeTemporaryStyle('body', minHeight: '3000px')
45
+ @restoreBodyHeight = e.setTemporaryStyle(document.body, minHeight: '3000px')
44
46
 
45
47
  afterEach ->
46
48
  @restoreBodyHeight()
49
+
50
+ distanceFromOrigin = 10
47
51
 
48
52
  describe 'with { position: "top" }', ->
49
53
 
@@ -52,8 +56,8 @@ describe 'up.tooltip', ->
52
56
  up.tooltip.attach(@$link, html: 'tooltip text', position: 'top').then =>
53
57
  $tooltip = $('.up-tooltip')
54
58
  tooltipBox = $tooltip.get(0).getBoundingClientRect()
55
- expect(tooltipBox.top).toBeAround(@linkBox.top - tooltipBox.height, 15)
56
- expect(tooltipBox.left).toBeAround(@linkBox.left + 0.5 * (@linkBox.width - tooltipBox.width), 15)
59
+ expect(tooltipBox.top).toBeAround(@linkBox.top - tooltipBox.height - distanceFromOrigin, 1)
60
+ expect(tooltipBox.left).toBeAround(@linkBox.left + 0.5 * (@linkBox.width - tooltipBox.width), 1)
57
61
  done()
58
62
 
59
63
  describe 'with { position: "right" }', ->
@@ -63,8 +67,8 @@ describe 'up.tooltip', ->
63
67
  up.tooltip.attach(@$link, html: 'tooltip text', position: 'right').then =>
64
68
  $tooltip = $('.up-tooltip')
65
69
  tooltipBox = $tooltip.get(0).getBoundingClientRect()
66
- expect(tooltipBox.top).toBeAround(@linkBox.top + 0.5 * (@linkBox.height - tooltipBox.height), 15)
67
- expect(tooltipBox.left).toBeAround(@linkBox.left + @linkBox.width, 15)
70
+ expect(tooltipBox.top).toBeAround(@linkBox.top + 0.5 * (@linkBox.height - tooltipBox.height), 1)
71
+ expect(tooltipBox.left).toBeAround(@linkBox.left + @linkBox.width + distanceFromOrigin, 1)
68
72
  done()
69
73
 
70
74
  describe 'with { position: "bottom" }', ->
@@ -74,8 +78,8 @@ describe 'up.tooltip', ->
74
78
  up.tooltip.attach(@$link, html: 'tooltip text', position: 'bottom').then =>
75
79
  $tooltip = $('.up-tooltip')
76
80
  tooltipBox = $tooltip.get(0).getBoundingClientRect()
77
- expect(tooltipBox.top).toBeAround(@linkBox.top + @linkBox.height, 15)
78
- expect(tooltipBox.left).toBeAround(@linkBox.left + 0.5 * (@linkBox.width - tooltipBox.width), 15)
81
+ expect(tooltipBox.top).toBeAround(@linkBox.top + @linkBox.height + distanceFromOrigin, 1)
82
+ expect(tooltipBox.left).toBeAround(@linkBox.left + 0.5 * (@linkBox.width - tooltipBox.width), 1)
79
83
  done()
80
84
 
81
85
  describe 'with { position: "left" }', ->
@@ -85,33 +89,13 @@ describe 'up.tooltip', ->
85
89
  up.tooltip.attach(@$link, html: 'tooltip text', position: 'left').then =>
86
90
  $tooltip = $('.up-tooltip')
87
91
  tooltipBox = $tooltip.get(0).getBoundingClientRect()
88
- expect(tooltipBox.top).toBeAround(@linkBox.top + 0.5 * (@linkBox.height - tooltipBox.height), 15)
89
- expect(tooltipBox.left).toBeAround(@linkBox.left - tooltipBox.width, 15)
92
+ expect(tooltipBox.top).toBeAround(@linkBox.top + 0.5 * (@linkBox.height - tooltipBox.height), 1)
93
+ expect(tooltipBox.left).toBeAround(@linkBox.left - tooltipBox.width - distanceFromOrigin, 1)
90
94
  done()
91
95
 
92
- it 'gives the tooltip { position: "fixed" } if the given link is fixed', (done) ->
93
- # Let's test the harder case where the document is scrolled
94
- up.layout.scroll(document, 50)
95
- @$link.css(position: 'fixed')
96
- @linkBox = @$link.get(0).getBoundingClientRect()
97
-
98
- up.tooltip.attach(@$link, html: 'tooltip text', position: 'top').then =>
99
- $tooltip = $('.up-tooltip')
100
- tooltipBox = $tooltip.get(0).getBoundingClientRect()
101
- expect($tooltip.css('position')).toEqual('fixed')
102
- expect(tooltipBox.top).toBeAround(@linkBox.top - tooltipBox.height, 15)
103
- expect(tooltipBox.left).toBeAround(@linkBox.left + 0.5 * (@linkBox.width - tooltipBox.width), 15)
104
- done()
105
-
106
- it 'closes an existing tooltip'
107
-
108
- describe 'with position option', ->
109
-
110
- it 'anchors the tooltip at a different edge of the element'
111
-
112
96
  describe 'up.tooltip.close', ->
113
97
 
114
- it 'should have tests'
98
+ it 'closes an existing tooltip'
115
99
 
116
100
  describe 'unobtrusive behavior', ->
117
101
 
@@ -129,7 +113,7 @@ describe 'up.tooltip', ->
129
113
  up.motion.config.enabled = false
130
114
 
131
115
  it 'closes the tooltip', asyncSpec (next) ->
132
- $link = affix('.link')
116
+ $link = $fixture('.link')
133
117
  up.tooltip.attach($link, text: 'Tooltip text')
134
118
 
135
119
  next =>
@@ -140,9 +124,9 @@ describe 'up.tooltip', ->
140
124
  expect(up.tooltip.isOpen()).toBe(false)
141
125
 
142
126
  it 'closes the tooltip when a an [up-instant] link removes its parent (and thus a click event never bubbles up to the document)', asyncSpec (next) ->
143
- $parent = affix('.parent')
127
+ $parent = $fixture('.parent')
144
128
  $parentReplacingLink = $parent.affix('a[href="/foo"][up-target=".parent"][up-instant]')
145
- $tooltipOpener = affix('.link')
129
+ $tooltipOpener = $fixture('.link')
146
130
  up.tooltip.attach($tooltipOpener, text: 'Tooltip text')
147
131
 
148
132
  next =>
@@ -153,9 +137,9 @@ describe 'up.tooltip', ->
153
137
  expect(up.tooltip.isOpen()).toBe(false)
154
138
 
155
139
  it 'closes a tooltip when the user clicks on an [up-target] link outside the tooltip', asyncSpec (next) ->
156
- $target = affix('.target')
157
- $outsideLink = affix('a[href="/foo"][up-target=".target"]')
158
- $tooltipOpener = affix('.link')
140
+ $target = $fixture('.target')
141
+ $outsideLink = $fixture('a[href="/foo"][up-target=".target"]')
142
+ $tooltipOpener = $fixture('.link')
159
143
  up.tooltip.attach($tooltipOpener, text: 'Tooltip text')
160
144
 
161
145
  next =>
@@ -166,9 +150,9 @@ describe 'up.tooltip', ->
166
150
  expect(up.tooltip.isOpen()).toBe(false)
167
151
 
168
152
  it 'closes a tooltip when the user clicks on an [up-instant] link outside the tooltip', asyncSpec (next) ->
169
- $target = affix('.target')
170
- $outsideLink = affix('a[href="/foo"][up-target=".target"][up-instant]')
171
- $tooltipOpener = affix('.link')
153
+ $target = $fixture('.target')
154
+ $outsideLink = $fixture('a[href="/foo"][up-target=".target"][up-instant]')
155
+ $tooltipOpener = $fixture('.link')
172
156
  up.tooltip.attach($tooltipOpener, text: 'Tooltip text')
173
157
 
174
158
  next =>
@@ -1,15 +1,321 @@
1
- describe 'up.util', ->
1
+ u = up.util
2
+ e = up.element
3
+ $ = jQuery
2
4
 
3
- u = up.util
5
+ describe 'up.util', ->
4
6
 
5
7
  describe 'JavaScript functions', ->
6
8
 
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])
9
+ describe 'up.util.isEqual', ->
10
+
11
+ describe 'for an Element', ->
12
+
13
+ it 'returns true for the same Element reference', ->
14
+ div = document.createElement('div')
15
+ expect(up.util.isEqual(div, div)).toBe(true)
16
+
17
+ it 'returns false for a different Element reference', ->
18
+ div1 = document.createElement('div')
19
+ div2 = document.createElement('div')
20
+ expect(up.util.isEqual(div1, div2)).toBe(false)
21
+
22
+ it 'returns false for a value this is no Element', ->
23
+ div = document.createElement('div')
24
+ expect(up.util.isEqual(div, 'other')).toBe(false)
25
+
26
+ describe 'for an Array', ->
27
+
28
+ it 'returns true for a different Array reference with the same elements', ->
29
+ array1 = ['foo', 'bar']
30
+ array2 = ['foo', 'bar']
31
+ expect(up.util.isEqual(array1, array2)).toBe(true)
32
+
33
+ it 'returns false for an Array with different elements', ->
34
+ array1 = ['foo', 'bar']
35
+ array2 = ['foo', 'qux']
36
+ expect(up.util.isEqual(array1, array2)).toBe(false)
37
+
38
+ it 'returns false for an Array that is a suffix', ->
39
+ array1 = ['foo', 'bar']
40
+ array2 = [ 'bar']
41
+ expect(up.util.isEqual(array1, array2)).toBe(false)
42
+
43
+ it 'returns false for an Array that is a prefix', ->
44
+ array1 = ['foo', 'bar']
45
+ array2 = ['foo' ]
46
+ expect(up.util.isEqual(array1, array2)).toBe(false)
47
+
48
+ it 'returns true for a NodeList with the same elements', ->
49
+ parent = e.affix(document.body, '.parent')
50
+ child1 = e.affix(parent, '.child.one')
51
+ child2 = e.affix(parent, '.child.two')
52
+
53
+ array = [child1, child2]
54
+ nodeList = parent.querySelectorAll('.child')
55
+
56
+ expect(up.util.isEqual(array, nodeList)).toBe(true)
57
+
58
+ it 'returns true for a HTMLCollection with the same elements', ->
59
+ parent = e.affix(document.body, '.parent')
60
+ child1 = e.affix(parent, '.child.one')
61
+ child2 = e.affix(parent, '.child.two')
62
+
63
+ array = [child1, child2]
64
+ htmlCollection = parent.children
65
+
66
+ expect(up.util.isEqual(array, htmlCollection)).toBe(true)
67
+
68
+ it 'returns true for an arguments object with the same elements', ->
69
+ toArguments = -> return arguments
70
+ array = ['foo', 'bar']
71
+ args = toArguments('foo', 'bar')
72
+
73
+ expect(up.util.isEqual(array, args)).toBe(true)
74
+
75
+ it 'returns false for a value that is no Array', ->
76
+ array = ['foo', 'bar']
77
+ expect(up.util.isEqual(array, 'foobar')).toBe(false)
78
+
79
+ describe 'for a string', ->
80
+
81
+ it 'returns true for a different string reference with the same characters', ->
82
+ string1 = 'bar'
83
+ string2 = 'bar'
84
+ expect(up.util.isEqual(string1, string2)).toBe(true)
85
+
86
+ it 'returns false for a string with different characters', ->
87
+ string1 = 'foo'
88
+ string2 = 'bar'
89
+ expect(up.util.isEqual(string1, string2)).toBe(false)
90
+
91
+ it 'returns true for a String() object with the same characters', ->
92
+ stringLiteral = 'bar'
93
+ stringObject = new String('bar')
94
+ expect(up.util.isEqual(stringLiteral, stringObject)).toBe(true)
95
+
96
+ it 'returns false for a String() object with different characters', ->
97
+ stringLiteral = 'foo'
98
+ stringObject = new String('bar')
99
+ expect(up.util.isEqual(stringLiteral, stringObject)).toBe(false)
100
+
101
+ it 'returns false for a value that is no string', ->
102
+ expect(up.util.isEqual('foo', ['foo'])).toBe(false)
103
+
104
+ describe 'for a number', ->
105
+
106
+ it 'returns true for a different number reference with the same integer value', ->
107
+ number1 = 123
108
+ number2 = 123
109
+ expect(up.util.isEqual(number1, number2)).toBe(true)
110
+
111
+ it 'returns true for a different number reference with the same floating point value', ->
112
+ number1 = 123.4
113
+ number2 = 123.4
114
+ expect(up.util.isEqual(number1, number2)).toBe(true)
115
+
116
+ it 'returns false for a number with a different value', ->
117
+ number1 = 123
118
+ number2 = 124
119
+ expect(up.util.isEqual(number1, number2)).toBe(false)
120
+
121
+ it 'returns true for a Number() object with the same value', ->
122
+ numberLiteral = 123
123
+ numberObject = new Number(123)
124
+ expect(up.util.isEqual(numberLiteral, numberObject)).toBe(true)
125
+
126
+ it 'returns false for a Number() object with a different value', ->
127
+ numberLiteral = 123
128
+ numberObject = new Object(124)
129
+ expect(up.util.isEqual(numberLiteral, numberObject)).toBe(false)
130
+
131
+ it 'returns false for a value that is no number', ->
132
+ expect(up.util.isEqual(123, '123')).toBe(false)
133
+
134
+ describe 'for undefined', ->
135
+
136
+ it 'returns true for undefined', ->
137
+ expect(up.util.isEqual(undefined, undefined)).toBe(true)
138
+
139
+ it 'returns false for null', ->
140
+ expect(up.util.isEqual(undefined, null)).toBe(false)
141
+
142
+ it 'returns false for NaN', ->
143
+ expect(up.util.isEqual(undefined, NaN)).toBe(false)
144
+
145
+ it 'returns false for an empty Object', ->
146
+ expect(up.util.isEqual(undefined, {})).toBe(false)
147
+
148
+ it 'returns false for an empty string', ->
149
+ expect(up.util.isEqual(undefined, '')).toBe(false)
150
+
151
+ describe 'for null', ->
152
+
153
+ it 'returns true for null', ->
154
+ expect(up.util.isEqual(null, null)).toBe(true)
155
+
156
+ it 'returns false for undefined', ->
157
+ expect(up.util.isEqual(null, undefined)).toBe(false)
158
+
159
+ it 'returns false for NaN', ->
160
+ expect(up.util.isEqual(null, NaN)).toBe(false)
161
+
162
+ it 'returns false for an empty Object', ->
163
+ expect(up.util.isEqual(null, {})).toBe(false)
164
+
165
+ it 'returns false for an empty string', ->
166
+ expect(up.util.isEqual(null, '')).toBe(false)
167
+
168
+ describe 'for NaN', ->
169
+
170
+ it "returns false for NaN because it represents multiple values", ->
171
+ expect(up.util.isEqual(NaN, NaN)).toBe(false)
172
+
173
+ it 'returns false for null', ->
174
+ expect(up.util.isEqual(NaN, null)).toBe(false)
175
+
176
+ it 'returns false for undefined', ->
177
+ expect(up.util.isEqual(NaN, undefined)).toBe(false)
178
+
179
+ it 'returns false for an empty Object', ->
180
+ expect(up.util.isEqual(NaN, {})).toBe(false)
181
+
182
+ it 'returns false for an empty string', ->
183
+ expect(up.util.isEqual(NaN, '')).toBe(false)
184
+
185
+ describe 'for a Date', ->
186
+
187
+ it 'returns true for another Date object that points to the same millisecond', ->
188
+ d1 = new Date('1995-12-17T03:24:00')
189
+ d2 = new Date('1995-12-17T03:24:00')
190
+ expect(up.util.isEqual(d1, d2)).toBe(true)
191
+
192
+ it 'returns false for another Date object that points to another millisecond', ->
193
+ d1 = new Date('1995-12-17T03:24:00')
194
+ d2 = new Date('1995-12-17T03:24:01')
195
+ expect(up.util.isEqual(d1, d2)).toBe(false)
196
+
197
+ it 'returns true for another Date object that points to the same millisecond in another time zone', ->
198
+ d1 = new Date('2019-01-20T17:35:00+01:00')
199
+ d2 = new Date('2019-01-20T16:35:00+00:00')
200
+ expect(up.util.isEqual(d1, d2)).toBe(true)
201
+
202
+ it 'returns false for a value that is not a Date', ->
203
+ d1 = new Date('1995-12-17T03:24:00')
204
+ d2 = '1995-12-17T03:24:00'
205
+ expect(up.util.isEqual(d1, d2)).toBe(false)
206
+
207
+ describe 'for a plain Object', ->
208
+
209
+ it 'returns true for the same reference', ->
210
+ obj = {}
211
+ reference = obj
212
+ expect(up.util.isEqual(obj, reference)).toBe(true)
213
+
214
+ it 'returns true for another plain object with the same keys and values', ->
215
+ obj1 = { foo: 'bar', baz: 'bam' }
216
+ obj2 = { foo: 'bar', baz: 'bam' }
217
+ expect(up.util.isEqual(obj1, obj2)).toBe(true)
218
+
219
+ it 'returns false for another plain object with the same keys, but different values', ->
220
+ obj1 = { foo: 'bar', baz: 'bam' }
221
+ obj2 = { foo: 'bar', baz: 'qux' }
222
+ expect(up.util.isEqual(obj1, obj2)).toBe(false)
223
+
224
+ it 'returns false for another plain object that is missing a key', ->
225
+ obj1 = { foo: 'bar', baz: 'bam' }
226
+ obj2 = { foo: 'bar' }
227
+ expect(up.util.isEqual(obj1, obj2)).toBe(false)
228
+
229
+ it 'returns false for another plain object that has an additional key', ->
230
+ obj1 = { foo: 'bar' }
231
+ obj2 = { foo: 'bar', baz: 'bam' }
232
+ expect(up.util.isEqual(obj1, obj2)).toBe(false)
233
+
234
+ it 'returns false for a non-plain Object, even if it has the same keys and values', ->
235
+ class Account
236
+ constructor: (@email) ->
237
+
238
+ accountInstance = new Account('foo@example.com')
239
+ accountPlain = {}
240
+ for key, value of accountInstance
241
+ accountPlain[key] = value
242
+ expect(up.util.isEqual(accountPlain, accountInstance)).toBe(false)
243
+
244
+ it 'returns false for a value that is no object', ->
245
+ obj = { foo: 'bar' }
246
+ expect(up.util.isEqual(obj, 'foobar')).toBe(false)
247
+
248
+ describe 'for a non-Plain object', ->
249
+
250
+ it 'returns true for the same reference', ->
251
+ obj = new FormData()
252
+ reference = obj
253
+ expect(up.util.isEqual(obj, reference)).toBe(true)
254
+
255
+ it 'returns false for different references', ->
256
+ obj1 = new FormData()
257
+ obj2 = new FormData()
258
+ expect(up.util.isEqual(obj1, obj2)).toBe(false)
259
+
260
+ it 'returns false for a different object with the same keys and values', ->
261
+ class Account
262
+ constructor: (@email) ->
263
+
264
+ account1 = new Account('foo@example.com')
265
+ account2 = new Account('bar@example.com')
266
+
267
+ expect(up.util.isEqual(account1, account2)).toBe(false)
268
+
269
+ it 'allows the object to hook into the comparison protocol by implementing a method called `up.util.isEqual.key`', ->
270
+ class Account
271
+ constructor: (@email) ->
272
+ "#{up.util.isEqual.key}": (other) ->
273
+ @email == other.email
274
+
275
+ account1 = new Account('foo@example.com')
276
+ account2 = new Account('bar@example.com')
277
+ account3 = new Account('foo@example.com')
278
+
279
+ expect(up.util.isEqual(account1, account2)).toBe(false)
280
+ expect(up.util.isEqual(account1, account3)).toBe(true)
281
+
282
+ it 'returns false for a value that is no object', ->
283
+ class Account
284
+ constructor: (@email) ->
285
+
286
+ account = new Account('foo@example.com')
287
+
288
+ expect(up.util.isEqual(account, 'foo@example.com')).toBe(false)
289
+
290
+ describe 'up.util.flatMap', ->
291
+
292
+ it 'collects the Array results of the given map function, then concatenates the result arrays into one flat array', ->
293
+ fun = (x) -> [x, x]
294
+ result = up.util.flatMap([1, 2, 3], fun)
295
+ expect(result).toEqual([1, 1, 2, 2, 3, 3])
296
+
297
+ it 'builds an array from mixed function return values of scalar values and lists', ->
298
+ fun = (x) ->
299
+ if x == 1
300
+ 1
301
+ else
302
+ [x, x]
303
+
304
+ result = up.util.flatMap([0, 1, 2], fun)
305
+ expect(result).toEqual [0, 0, 1, 2, 2]
306
+
307
+
308
+ it 'flattens return values that are NodeLists', ->
309
+ fun = (selector) -> document.querySelectorAll(selector)
310
+
311
+ foo1 = $fixture('.foo-element')[0]
312
+ foo2 = $fixture('.foo-element')[0]
313
+ bar = $fixture('.bar-element')[0]
314
+
315
+ result = up.util.flatMap(['.foo-element', '.bar-element'], fun)
316
+
317
+ expect(result).toEqual [foo1, foo2, bar]
318
+
13
319
 
14
320
  describe 'up.util.uniq', ->
15
321
 
@@ -92,6 +398,58 @@ describe 'up.util', ->
92
398
  # path = up.util.parsePath('foo.bar[baz]["bam"][\'qux\']')
93
399
  # expect(path).toEqual ['foo', 'bar', 'baz', 'bam', 'qux']
94
400
 
401
+ describe 'up.util.parseUrl', ->
402
+
403
+ it 'parses a full URL', ->
404
+ url = up.util.parseUrl('https://subdomain.domain.tld:123/path?search#hash')
405
+ expect(url.protocol).toEqual('https:')
406
+ expect(url.hostname).toEqual('subdomain.domain.tld')
407
+ expect(url.port).toEqual('123')
408
+ expect(url.pathname).toEqual('/path')
409
+ expect(url.search).toEqual('?search')
410
+ expect(url.hash).toEqual('#hash')
411
+
412
+ it 'parses an absolute path', ->
413
+ url = up.util.parseUrl('/qux/foo?search#bar')
414
+ expect(url.protocol).toEqual(location.protocol)
415
+ expect(url.hostname).toEqual(location.hostname)
416
+ expect(url.port).toEqual(location.port)
417
+ expect(url.pathname).toEqual('/qux/foo')
418
+ expect(url.search).toEqual('?search')
419
+ expect(url.hash).toEqual('#bar')
420
+
421
+ it 'parses a relative path', ->
422
+ up.history.config.enabled = true
423
+ up.history.replace('/qux/')
424
+ url = up.util.parseUrl('foo?search#bar')
425
+ expect(url.protocol).toEqual(location.protocol)
426
+ expect(url.hostname).toEqual(location.hostname)
427
+ expect(url.port).toEqual(location.port)
428
+ expect(url.pathname).toEqual('/qux/foo')
429
+ expect(url.search).toEqual('?search')
430
+ expect(url.hash).toEqual('#bar')
431
+
432
+ it 'allows to pass a link element', ->
433
+ link = document.createElement('a')
434
+ link.href = '/qux/foo?search#bar'
435
+ url = up.util.parseUrl(link)
436
+ expect(url.protocol).toEqual(location.protocol)
437
+ expect(url.hostname).toEqual(location.hostname)
438
+ expect(url.port).toEqual(location.port)
439
+ expect(url.pathname).toEqual('/qux/foo')
440
+ expect(url.search).toEqual('?search')
441
+ expect(url.hash).toEqual('#bar')
442
+
443
+ it 'allows to pass a link element as a jQuery collection', ->
444
+ $link = $('<a></a>').attr(href: '/qux/foo?search#bar')
445
+ url = up.util.parseUrl($link)
446
+ expect(url.protocol).toEqual(location.protocol)
447
+ expect(url.hostname).toEqual(location.hostname)
448
+ expect(url.port).toEqual(location.port)
449
+ expect(url.pathname).toEqual('/qux/foo')
450
+ expect(url.search).toEqual('?search')
451
+ expect(url.hash).toEqual('#bar')
452
+
95
453
  describe 'up.util.map', ->
96
454
 
97
455
  it 'creates a new array of values by calling the given function on each item of the given array', ->
@@ -109,9 +467,19 @@ describe 'up.util', ->
109
467
  mapped = up.util.map(array, (element, i) -> i)
110
468
  expect(mapped).toEqual [0, 1, 2]
111
469
 
470
+ describe 'up.util.mapObject', ->
471
+
472
+ it 'creates an object from the given array and pairer', ->
473
+ array = ['foo', 'bar', 'baz']
474
+ object = up.util.mapObject(array, (str) -> ["#{str}Key", "#{str}Value"])
475
+ expect(object).toEqual
476
+ fooKey: 'fooValue'
477
+ barKey: 'barValue'
478
+ bazKey: 'bazValue'
479
+
112
480
  describe 'up.util.each', ->
113
481
 
114
- it 'calls the given function once for each itm of the given array', ->
482
+ it 'calls the given function once for each item of the given array', ->
115
483
  args = []
116
484
  array = ["apple", "orange", "cucumber"]
117
485
  up.util.each array, (item) -> args.push(item)
@@ -123,23 +491,43 @@ describe 'up.util', ->
123
491
  up.util.each array, (item, index) -> args.push(index)
124
492
  expect(args).toEqual [0, 1, 2]
125
493
 
126
- describe 'up.util.select', ->
494
+ it 'iterates over an array-like value', ->
495
+ one = fixture('.qwertz')
496
+ two = fixture('.qwertz')
497
+ nodeList = document.querySelectorAll('.qwertz')
498
+
499
+ callback = jasmine.createSpy()
500
+
501
+ up.util.each nodeList, callback
502
+ expect(callback.calls.allArgs()).toEqual [[one, 0], [two, 1]]
503
+
504
+ describe 'up.util.filter', ->
127
505
 
128
506
  it 'returns an array of those elements in the given array for which the given function returns true', ->
129
507
  array = ["foo", "orange", "cucumber"]
130
- results = up.util.select array, (item) -> item.length > 3
508
+ results = up.util.filter array, (item) -> item.length > 3
131
509
  expect(results).toEqual ['orange', 'cucumber']
132
510
 
133
511
  it 'passes the iteration index as second argument to the given function', ->
134
512
  array = ["apple", "orange", "cucumber", "banana"]
135
- results = up.util.select array, (item, index) -> index % 2 == 0
513
+ results = up.util.filter array, (item, index) -> index % 2 == 0
136
514
  expect(results).toEqual ['apple', 'cucumber']
137
515
 
138
516
  it 'accepts a property name instead of a function, which checks that property from each item', ->
139
517
  array = [ { name: 'a', prop: false }, { name: 'b', prop: true } ]
140
- results = up.util.select array, 'prop'
518
+ results = up.util.filter array, 'prop'
141
519
  expect(results).toEqual [{ name: 'b', prop: true }]
142
520
 
521
+ it 'iterates over an array-like value', ->
522
+ one = fixture('.qwertz')
523
+ two = fixture('.qwertz')
524
+ nodeList = document.querySelectorAll('.qwertz')
525
+
526
+ callback = jasmine.createSpy()
527
+
528
+ up.util.filter nodeList, callback
529
+ expect(callback.calls.allArgs()).toEqual [[one, 0], [two, 1]]
530
+
143
531
  describe 'up.util.reject', ->
144
532
 
145
533
  it 'returns an array of those elements in the given array for which the given function returns false', ->
@@ -157,6 +545,16 @@ describe 'up.util', ->
157
545
  results = up.util.reject array, 'prop'
158
546
  expect(results).toEqual [{ name: 'a', prop: false }]
159
547
 
548
+ it 'iterates over an array-like value', ->
549
+ one = fixture('.qwertz')
550
+ two = fixture('.qwertz')
551
+ nodeList = document.querySelectorAll('.qwertz')
552
+
553
+ callback = jasmine.createSpy()
554
+
555
+ up.util.reject nodeList, callback
556
+ expect(callback.calls.allArgs()).toEqual [[one, 0], [two, 1]]
557
+
160
558
  describe 'up.util.previewable', ->
161
559
 
162
560
  it 'wraps a function into a proxy function with an additional .promise attribute', ->
@@ -171,10 +569,10 @@ describe 'up.util', ->
171
569
  proxy = up.util.previewable(fun)
172
570
  callback = jasmine.createSpy('promise callback')
173
571
  proxy.promise.then(callback)
174
- u.nextFrame ->
572
+ u.task ->
175
573
  expect(callback).not.toHaveBeenCalled()
176
574
  proxy()
177
- u.nextFrame ->
575
+ u.task ->
178
576
  expect(callback).toHaveBeenCalledWith('return value')
179
577
  done()
180
578
 
@@ -185,141 +583,13 @@ describe 'up.util', ->
185
583
  callback = jasmine.createSpy('promise callback')
186
584
  proxy.promise.then(callback)
187
585
  proxy()
188
- u.nextFrame ->
586
+ u.task ->
189
587
  expect(callback).not.toHaveBeenCalled()
190
588
  funDeferred.resolve('return value')
191
- u.nextFrame ->
589
+ u.task ->
192
590
  expect(callback).toHaveBeenCalledWith('return value')
193
591
  done()
194
592
 
195
- describe 'up.util.kebabCase', ->
196
-
197
- it 'converts a string of multiple words from camel-case to kebap-case', ->
198
- result = up.util.kebabCase('fooBarBaz')
199
- expect(result).toEqual('foo-bar-baz')
200
-
201
- it 'does not change a single word', ->
202
- result = up.util.kebabCase('foo')
203
- expect(result).toEqual('foo')
204
-
205
- it 'downcases the first word when it starts with a capital letter', ->
206
- result = up.util.kebabCase('FooBar')
207
- expect(result).toEqual('foo-bar')
208
-
209
- it 'does not change a string that is already in kebab-case', ->
210
- result = up.util.kebabCase('foo-bar-baz')
211
- expect(result).toEqual('foo-bar-baz')
212
-
213
- describe 'up.util.camelCase', ->
214
-
215
- it 'converts a string of multiple words from kebap-case to camel-case', ->
216
- result = up.util.camelCase('foo-bar-baz')
217
- expect(result).toEqual('fooBarBaz')
218
-
219
- it 'does not change a single word', ->
220
- result = up.util.camelCase('foo')
221
- expect(result).toEqual('foo')
222
-
223
- it 'downcases the first word when it starts with a capital letter', ->
224
- result = up.util.camelCase('Foo-Bar')
225
- expect(result).toEqual('fooBar')
226
-
227
- it 'does not change a string that is already in camel-case', ->
228
- result = up.util.camelCase('fooBarBaz')
229
- expect(result).toEqual('fooBarBaz')
230
-
231
- describe 'up.util.kebabCaseKeys', ->
232
-
233
- it "converts the given object's keys from camel-case to kebab-case", ->
234
- input =
235
- fooBar: 'one'
236
- barBaz: 'two'
237
- result = up.util.kebabCaseKeys(input)
238
- expect(result).toEqual
239
- 'foo-bar': 'one'
240
- 'bar-baz': 'two'
241
-
242
- it "does not change an object whose keys are already kebab-case", ->
243
- input =
244
- 'foo-bar': 'one'
245
- 'bar-baz': 'two'
246
- result = up.util.kebabCaseKeys(input)
247
- expect(result).toEqual
248
- 'foo-bar': 'one'
249
- 'bar-baz': 'two'
250
-
251
- describe 'up.util.camelCaseKeys', ->
252
-
253
- it "converts the given object's keys from kebab-case to camel-case", ->
254
- input =
255
- 'foo-bar': 'one'
256
- 'bar-baz': 'two'
257
- result = up.util.camelCaseKeys(input)
258
- expect(result).toEqual
259
- fooBar: 'one'
260
- barBaz: 'two'
261
-
262
- it "does not change an object whose keys are already camel-case", ->
263
- input =
264
- fooBar: 'one'
265
- barBaz: 'two'
266
- result = up.util.camelCaseKeys(input)
267
- expect(result).toEqual
268
- fooBar: 'one'
269
- barBaz: 'two'
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
-
282
- describe 'up.util.DivertibleChain', ->
283
-
284
- it "instantiates a task queue whose (2..n)th tasks can be changed by calling '.asap'", (done) ->
285
- chain = new up.util.DivertibleChain()
286
-
287
- timer1Spy = jasmine.createSpy('timer1 has been called')
288
- timer1 = ->
289
- timer1Spy()
290
- u.promiseTimer(50)
291
-
292
- timer2Spy = jasmine.createSpy('timer2 has been called')
293
- timer2 = ->
294
- timer2Spy()
295
- u.promiseTimer(50)
296
-
297
- timer3Spy = jasmine.createSpy('timer3 has been called')
298
- timer3 = ->
299
- timer3Spy()
300
- u.promiseTimer(50)
301
-
302
- timer4Spy = jasmine.createSpy('timer4 has been called')
303
- timer4 = ->
304
- timer4Spy()
305
- u.promiseTimer(50)
306
-
307
- chain.asap(timer1)
308
- u.nextFrame ->
309
- expect(timer1Spy).toHaveBeenCalled()
310
- chain.asap(timer2)
311
- u.nextFrame ->
312
- # timer2 is still waiting for timer1 to finish
313
- expect(timer2Spy).not.toHaveBeenCalled()
314
- # Override the (2..n)th tasks. This unschedules timer2.
315
- chain.asap(timer3, timer4)
316
- u.setTimer 80, ->
317
- expect(timer2Spy).not.toHaveBeenCalled()
318
- expect(timer3Spy).toHaveBeenCalled()
319
- u.setTimer 70, ->
320
- expect(timer4Spy).toHaveBeenCalled()
321
- done()
322
-
323
593
  describe 'up.util.sequence', ->
324
594
 
325
595
  it 'combines the given functions into a single function', ->
@@ -327,128 +597,52 @@ describe 'up.util', ->
327
597
  one = -> values.push('one')
328
598
  two = -> values.push('two')
329
599
  three = -> values.push('three')
330
- sequence = up.util.sequence(one, two, three)
600
+ sequence = up.util.sequence([one, two, three])
331
601
  expect(values).toEqual([])
332
602
  sequence()
333
603
  expect(values).toEqual(['one', 'two', 'three'])
334
604
 
335
- describe 'up.util.createElementFromHtml', ->
336
-
337
- it 'parses a string that contains a serialized HTML document', ->
338
- string = """
339
- <html lang="foo">
340
- <head>
341
- <title>document title</title>
342
- </head>
343
- <body data-env='production'>
344
- <div>line 1</div>
345
- <div>line 2</div>
346
- </body>
347
- </html>
348
- """
349
-
350
- element = up.util.createElementFromHtml(string)
351
-
352
- expect(element.querySelector('head title').textContent).toEqual('document title')
353
- expect(element.querySelector('body').getAttribute('data-env')).toEqual('production')
354
- expect(element.querySelectorAll('body div').length).toBe(2)
355
- expect(element.querySelectorAll('body div')[0].textContent).toEqual('line 1')
356
- expect(element.querySelectorAll('body div')[1].textContent).toEqual('line 2')
357
-
358
- it 'parses a string that contains carriage returns (bugfix)', ->
359
- string = """
360
- <html>\r
361
- <body>\r
362
- <div>line</div>\r
363
- </body>\r
364
- </html>\r
365
- """
366
-
367
- $element = up.util.createElementFromHtml(string)
368
- expect($element.querySelector('body')).toBeGiven()
369
- expect($element.querySelector('body div').textContent).toEqual('line')
370
-
371
- it 'does not run forever if a page has a <head> without a <title> (bugfix)', ->
372
- html = """
373
- <!doctype html>
374
- <html>
375
- <head>
376
- <meta charset="utf-8" />
377
- <meta name="format-detection" content="telephone=no">
378
- <link href='/images/favicon.png' rel='shortcut icon' type='image/png'>
379
- <meta name='viewport' content='width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1'>
380
-
381
- <base href="/examples/update-fragment/" />
382
- <link href='http://fonts.googleapis.com/css?family=Orbitron:400|Ubuntu+Mono:400,700|Source+Sans+Pro:300,400,700,400italic,700italic' rel='stylesheet' type='text/css'>
383
- <link href="//netdna.bootstrapcdn.com/font-awesome/4.1.0/css/font-awesome.min.css" rel="stylesheet">
384
- <link href="/stylesheets/example/all.css" rel="stylesheet" />
385
- <script src="/javascripts/example.js"></script>
386
- </head>
387
- <body>
388
- <div class="page">
389
- <div class="story">
390
-
391
- <h1>Full story</h1>
392
- <p>Lorem ipsum dolor sit amet.</p>
393
-
394
- <a href="preview.html" up-target=".story">
395
- Read summary
396
- </a>
397
- </div>
398
-
399
- </div>
400
- </body>
401
- </html>
402
- """
403
- element = up.util.createElementFromHtml(html)
404
- expect(element.querySelector("title")).toBeMissing()
405
- expect(element.querySelector("h1").textContent).toEqual('Full story')
406
-
407
- it 'can parse HTML without a <head>', ->
408
- html = """
409
- <html>
410
- <body>
411
- <h1>Full story</h1>
412
- </body>
413
- </html>
414
- """
415
- element = up.util.createElementFromHtml(html)
416
- expect(element.querySelector("title")).toBeMissing()
417
- expect(element.querySelector("h1").textContent).toEqual('Full story')
418
-
419
- it 'can parse a HTML fragment without a <body>', ->
420
- html = """
421
- <h1>Full story</h1>
422
- """
423
- element = up.util.createElementFromHtml(html)
424
- expect(element.querySelector("title")).toBeMissing()
425
- expect(element.querySelector("h1").textContent).toEqual('Full story')
426
-
427
- describe 'up.util.isFixed', ->
428
-
429
- it 'returns true if the given element or one of its ancestors has a "fixed" CSS position', ->
430
- $grandGrandParent = affix('.grand-parent')
431
- $grandParent = $grandGrandParent.affix('.grand-parent')
432
- $parent = $grandParent.affix('.parent')
433
- $child = $parent.affix('.child')
434
- $grandParent.css(position: 'fixed')
435
- expect(up.util.isFixed($child)).toBe(true)
436
- expect(up.util.isFixed($parent)).toBe(true)
437
- expect(up.util.isFixed($grandParent)).toBe(true)
438
- expect(up.util.isFixed($grandGrandParent)).toBe(false)
439
-
440
- it 'returns false if the given element and its ancestors all have a non-"fixed" CSS position', ->
441
- $element = affix('.element')
442
- expect(up.util.isFixed($element)).toBe(false)
443
-
444
- describe 'up.util.setTimer', ->
605
+ describe 'up.util.muteRejection', ->
606
+
607
+ it 'returns a promise that fulfills when the given promise fulfills', (done) ->
608
+ fulfilledPromise = Promise.resolve()
609
+ mutedPromise = up.util.muteRejection(fulfilledPromise)
610
+
611
+ u.task ->
612
+ promiseState(mutedPromise).then (result) ->
613
+ expect(result.state).toEqual('fulfilled')
614
+ done()
615
+
616
+ it 'returns a promise that fulfills when the given promise rejects', (done) ->
617
+ rejectedPromise = Promise.reject()
618
+ mutedPromise = up.util.muteRejection(rejectedPromise)
619
+
620
+ u.task ->
621
+ promiseState(mutedPromise).then (result) ->
622
+ expect(result.state).toEqual('fulfilled')
623
+ done()
624
+
625
+ describe 'up.util.simpleEase', ->
626
+
627
+ it 'returns 0 for 0', ->
628
+ expect(up.util.simpleEase(0)).toBe(0)
629
+
630
+ it 'returns 1 for 1', ->
631
+ expect(up.util.simpleEase(1)).toBe(1)
632
+
633
+ it 'returns steadily increasing values between 0 and 1', ->
634
+ expect(up.util.simpleEase(0.25)).toBeAround(0.25, 0.2)
635
+ expect(up.util.simpleEase(0.50)).toBeAround(0.50, 0.2)
636
+ expect(up.util.simpleEase(0.75)).toBeAround(0.75, 0.2)
637
+
638
+ describe 'up.util.timer', ->
445
639
 
446
640
  it 'calls the given function after waiting the given milliseconds', (done) ->
447
641
  callback = jasmine.createSpy()
448
642
  expectNotCalled = -> expect(callback).not.toHaveBeenCalled()
449
643
  expectCalled = -> expect(callback).toHaveBeenCalled()
450
644
 
451
- up.util.setTimer(100, callback)
645
+ up.util.timer(100, callback)
452
646
 
453
647
  expectNotCalled()
454
648
  setTimeout(expectNotCalled, 50)
@@ -459,7 +653,7 @@ describe 'up.util', ->
459
653
 
460
654
  it 'calls the given function in the next execution frame', ->
461
655
  callback = jasmine.createSpy()
462
- up.util.setTimer(0, callback)
656
+ up.util.timer(0, callback)
463
657
  expect(callback).not.toHaveBeenCalled()
464
658
 
465
659
  setTimeout((-> expect(callback).toHaveBeenCalled()), 0)
@@ -470,12 +664,6 @@ describe 'up.util', ->
470
664
  # fun = ($element, data) ->
471
665
  # expect(up.util.argNames(fun)).toEqual(['$element', 'data'])
472
666
 
473
- describe 'up.util.trim', ->
474
-
475
- it 'removes leading and trailing whitespace from the given string', ->
476
- string = "\t\n\r abc \r\n\t"
477
- expect(up.util.trim(string)).toEqual('abc')
478
-
479
667
  describe 'up.util.only', ->
480
668
 
481
669
  it 'returns a copy of the given object with only the given whitelisted properties', ->
@@ -502,91 +690,6 @@ describe 'up.util', ->
502
690
  expect(whitelisted).toHaveOwnProperty('foo')
503
691
  expect(whitelisted).not.toHaveOwnProperty('bar')
504
692
 
505
- describe 'up.util.readInlineStyle', ->
506
-
507
- describe 'with a string as second argument', ->
508
-
509
- it 'returns a CSS value string from an inline [style] attribute', ->
510
- $div = affix('div').attr('style', 'background-color: #ff0000')
511
- style = up.util.readInlineStyle($div, 'backgroundColor')
512
- # Browsers convert colors to rgb() values, even IE11
513
- expect(style).toEqual('rgb(255, 0, 0)')
514
-
515
- it 'returns a blank value if the element does not have the given property in the [style] attribute', ->
516
- $div = affix('div').attr('style', 'background-color: red')
517
- style = up.util.readInlineStyle($div, 'color')
518
- expect(style).toBeBlank()
519
-
520
- it 'returns a blank value the given property is a computed property, but not in the [style] attribute', ->
521
- $div = affix('div[class="red-background"]')
522
- inlineStyle = up.util.readInlineStyle($div, 'backgroundColor')
523
- computedStyle = up.util.readComputedStyle($div, 'backgroundColor')
524
- expect(computedStyle).toEqual('rgb(255, 0, 0)')
525
- expect(inlineStyle).toBeBlank()
526
-
527
- describe 'with an array as second argument', ->
528
-
529
- it 'returns an object with the given inline [style] properties', ->
530
- $div = affix('div').attr('style', 'background-color: #ff0000; color: #0000ff')
531
- style = up.util.readInlineStyle($div, ['backgroundColor', 'color'])
532
- expect(style).toEqual
533
- backgroundColor: 'rgb(255, 0, 0)'
534
- color: 'rgb(0, 0, 255)'
535
-
536
- it 'returns blank keys if the element does not have the given property in the [style] attribute', ->
537
- $div = affix('div').attr('style', 'background-color: #ff0000')
538
- style = up.util.readInlineStyle($div, ['backgroundColor', 'color'])
539
- expect(style).toHaveOwnProperty('color')
540
- expect(style.color).toBeBlank()
541
-
542
- it 'returns a blank value the given property is a computed property, but not in the [style] attribute', ->
543
- $div = affix('div[class="red-background"]')
544
- inlineStyleHash = up.util.readInlineStyle($div, ['backgroundColor'])
545
- computedBackground = up.util.readComputedStyle($div, 'backgroundColor')
546
- expect(computedBackground).toEqual('rgb(255, 0, 0)')
547
- expect(inlineStyleHash).toHaveOwnProperty('backgroundColor')
548
- expect(inlineStyleHash.backgroundColor).toBeBlank()
549
-
550
- describe 'up.util.writeInlineStyle', ->
551
-
552
- it "sets the given style properties as the given element's [style] attribute", ->
553
- $div = affix('div')
554
- up.util.writeInlineStyle($div, { color: 'red', backgroundColor: 'blue' })
555
- style = $div.attr('style')
556
- expect(style).toContain('color: red')
557
- expect(style).toContain('background-color: blue')
558
-
559
- it "merges the given style properties into the given element's existing [style] value", ->
560
- $div = affix('div[style="color: red"]')
561
- up.util.writeInlineStyle($div, { backgroundColor: 'blue' })
562
- style = $div.attr('style')
563
- expect(style).toContain('color: red')
564
- expect(style).toContain('background-color: blue')
565
-
566
- it "converts the values of known length properties to px values automatically", ->
567
- $div = affix('div')
568
- up.util.writeInlineStyle($div, { paddingTop: 100 })
569
- style = $div.attr('style')
570
- expect(style).toContain('padding-top: 100px')
571
-
572
- describe 'up.util.writeTemporaryStyle', ->
573
-
574
- it "sets the given inline styles and returns a function that will restore the previous inline styles", ->
575
- $div = affix('div[style="color: red"]')
576
- restore = up.util.writeTemporaryStyle($div, { color: 'blue' })
577
- expect($div.attr('style')).toContain('color: blue')
578
- expect($div.attr('style')).not.toContain('color: red')
579
- restore()
580
- expect($div.attr('style')).not.toContain('color: blue')
581
- expect($div.attr('style')).toContain('color: red')
582
-
583
- it "does not restore inherited styles", ->
584
- $div = affix('div[class="red-background"]')
585
- restore = up.util.writeTemporaryStyle($div, { backgroundColor: 'blue' })
586
- expect($div.attr('style')).toContain('background-color: blue')
587
- restore()
588
- expect($div.attr('style')).not.toContain('background-color')
589
-
590
693
  describe 'up.util.except', ->
591
694
 
592
695
  it 'returns a copy of the given object but omits the given blacklisted properties', ->
@@ -606,133 +709,19 @@ describe 'up.util', ->
606
709
  baz: 'baz-value'
607
710
  bam: 'bam-value'
608
711
 
609
- describe 'up.util.selectorForElement', ->
610
-
611
- it "prefers using the element's 'up-id' attribute to using the element's ID", ->
612
- $element = affix('div[up-id=up-id-value]#id-value')
613
- expect(up.util.selectorForElement($element)).toBe('[up-id="up-id-value"]')
614
-
615
- it "prefers using the element's ID to using the element's name", ->
616
- $element = affix('div#id-value[name=name-value]')
617
- expect(up.util.selectorForElement($element)).toBe("#id-value")
618
-
619
- it "selects the ID with an attribute selector if the ID contains a slash", ->
620
- $element = affix('div').attr(id: 'foo/bar')
621
- expect(up.util.selectorForElement($element)).toBe('[id="foo/bar"]')
622
-
623
- it "selects the ID with an attribute selector if the ID contains a space", ->
624
- $element = affix('div').attr(id: 'foo bar')
625
- expect(up.util.selectorForElement($element)).toBe('[id="foo bar"]')
626
-
627
- it "selects the ID with an attribute selector if the ID contains a dot", ->
628
- $element = affix('div').attr(id: 'foo.bar')
629
- expect(up.util.selectorForElement($element)).toBe('[id="foo.bar"]')
630
-
631
- it "selects the ID with an attribute selector if the ID contains a quote", ->
632
- $element = affix('div').attr(id: 'foo"bar')
633
- expect(up.util.selectorForElement($element)).toBe('[id="foo\\"bar"]')
634
-
635
- it "prefers using the element's tagName + [name] to using the element's classes", ->
636
- $element = affix('input[name=name-value].class1.class2')
637
- expect(up.util.selectorForElement($element)).toBe('input[name="name-value"]')
638
-
639
- it "prefers using the element's classes to using the element's ARIA label", ->
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')
645
- expect(up.util.selectorForElement($element)).toBe(".class1.class2")
646
-
647
- it "prefers using the element's ARIA label to using the element's tag name", ->
648
- $element = affix('div[aria-label="ARIA label value"]')
649
- expect(up.util.selectorForElement($element)).toBe('[aria-label="ARIA label value"]')
650
-
651
- it "uses the element's tag name if no better description is available", ->
652
- $element = affix('div')
653
- expect(up.util.selectorForElement($element)).toBe("div")
654
-
655
- it 'escapes quotes in attribute selector values', ->
656
- $element = affix('div')
657
- $element.attr('aria-label', 'foo"bar')
658
- expect(up.util.selectorForElement($element)).toBe('[aria-label="foo\\"bar"]')
659
-
660
-
661
- describe 'up.util.addTemporaryClass', ->
662
-
663
- it 'adds the given class to the given element', ->
664
- $element = affix('.foo.bar')
665
- element = $element.get(0)
666
-
667
- expect(element.className).toEqual('foo bar')
668
-
669
- up.util.addTemporaryClass(element, 'baz')
670
-
671
- expect(element.className).toEqual('foo bar baz')
672
-
673
- it 'returns a function that restores the original class', ->
674
- $element = affix('.foo.bar')
675
- element = $element.get(0)
676
-
677
- restoreClass = up.util.addTemporaryClass(element, 'baz')
678
- expect(element.className).toEqual('foo bar baz')
679
-
680
- restoreClass()
681
- expect(element.className).toEqual('foo bar')
682
-
683
-
684
- describe 'up.util.castedAttr', ->
685
-
686
- it 'returns true if the attribute value is the string "true"', ->
687
- $element = affix('div').attr('foo', 'true')
688
- expect(up.util.castedAttr($element, 'foo')).toBe(true)
689
-
690
- it 'returns true if the attribute value is the name of the attribute', ->
691
- $element = affix('div').attr('foo', 'foo')
692
- expect(up.util.castedAttr($element, 'foo')).toBe(true)
693
-
694
- it 'returns false if the attribute value is the string "false"', ->
695
- $element = affix('div').attr('foo', 'false')
696
- expect(up.util.castedAttr($element, 'foo')).toBe(false)
697
-
698
- it 'returns undefined if the element has no such attribute', ->
699
- $element = affix('div')
700
- expect(up.util.castedAttr($element, 'foo')).toBe(undefined)
701
-
702
- it 'returns the attribute value unchanged if the value is some string', ->
703
- $element = affix('div').attr('foo', 'some text')
704
- expect(up.util.castedAttr($element, 'foo')).toBe('some text')
705
-
706
- describe 'up.util.any', ->
707
-
708
- it 'returns true if an element in the array returns true for the given function', ->
709
- result = up.util.any [null, undefined, 'foo', ''], up.util.isPresent
710
- expect(result).toBe(true)
711
-
712
- it 'returns false if no element in the array returns true for the given function', ->
713
- result = up.util.any [null, undefined, ''], up.util.isPresent
714
- expect(result).toBe(false)
715
-
716
- it 'short-circuits once an element returns true', ->
717
- count = 0
718
- up.util.any [null, undefined, 'foo', ''], (element) ->
719
- count += 1
720
- up.util.isPresent(element)
721
- expect(count).toBe(3)
722
-
723
- describe 'up.util.all', ->
712
+ describe 'up.util.every', ->
724
713
 
725
714
  it 'returns true if all element in the array returns true for the given function', ->
726
- result = up.util.all ['foo', 'bar', 'baz'], up.util.isPresent
715
+ result = up.util.every ['foo', 'bar', 'baz'], up.util.isPresent
727
716
  expect(result).toBe(true)
728
717
 
729
718
  it 'returns false if an element in the array returns false for the given function', ->
730
- result = up.util.all ['foo', 'bar', null, 'baz'], up.util.isPresent
719
+ result = up.util.every ['foo', 'bar', null, 'baz'], up.util.isPresent
731
720
  expect(result).toBe(false)
732
721
 
733
722
  it 'short-circuits once an element returns false', ->
734
723
  count = 0
735
- up.util.all ['foo', 'bar', '', 'baz'], (element) ->
724
+ up.util.every ['foo', 'bar', '', 'baz'], (element) ->
736
725
  count += 1
737
726
  up.util.isPresent(element)
738
727
  expect(count).toBe(3)
@@ -740,7 +729,7 @@ describe 'up.util', ->
740
729
  it 'passes the iteration index as second argument to the given function', ->
741
730
  array = ["apple", "orange", "cucumber"]
742
731
  args = []
743
- up.util.all array, (item, index) ->
732
+ up.util.every array, (item, index) ->
744
733
  args.push(index)
745
734
  true
746
735
  expect(args).toEqual [0, 1, 2]
@@ -748,8 +737,8 @@ describe 'up.util', ->
748
737
  it 'accepts a property name instead of a function, which collects that property from each item', ->
749
738
  allTrue = [ { prop: true }, { prop: true } ]
750
739
  someFalse = [ { prop: true }, { prop: false } ]
751
- expect(up.util.all(allTrue, 'prop')).toBe(true)
752
- expect(up.util.all(someFalse, 'prop')).toBe(false)
740
+ expect(up.util.every(allTrue, 'prop')).toBe(true)
741
+ expect(up.util.every(someFalse, 'prop')).toBe(false)
753
742
 
754
743
  # describe 'up.util.none', ->
755
744
  #
@@ -782,20 +771,20 @@ describe 'up.util', ->
782
771
  # expect(up.util.none(allFalse, 'prop')).toBe(true)
783
772
  # expect(up.util.none(someTrue, 'prop')).toBe(false)
784
773
 
785
- describe 'up.util.any', ->
774
+ describe 'up.util.some', ->
786
775
 
787
776
  it 'returns true if at least one element in the array returns true for the given function', ->
788
- result = up.util.any ['', 'bar', null], up.util.isPresent
777
+ result = up.util.some ['', 'bar', null], up.util.isPresent
789
778
  expect(result).toBe(true)
790
779
 
791
780
  it 'returns false if no element in the array returns true for the given function', ->
792
- result = up.util.any ['', null, undefined], up.util.isPresent
781
+ result = up.util.some ['', null, undefined], up.util.isPresent
793
782
  expect(result).toBe(false)
794
783
 
795
784
  it 'passes the iteration index as second argument to the given function', ->
796
785
  array = ["apple", "orange", "cucumber"]
797
786
  args = []
798
- up.util.any array, (item, index) ->
787
+ up.util.some array, (item, index) ->
799
788
  args.push(index)
800
789
  false
801
790
  expect(args).toEqual [0, 1, 2]
@@ -803,8 +792,57 @@ describe 'up.util', ->
803
792
  it 'accepts a property name instead of a function, which collects that property from each item', ->
804
793
  someTrue = [ { prop: true }, { prop: false } ]
805
794
  allFalse = [ { prop: false }, { prop: false } ]
806
- expect(up.util.any(someTrue, 'prop')).toBe(true)
807
- expect(up.util.any(allFalse, 'prop')).toBe(false)
795
+ expect(up.util.some(someTrue, 'prop')).toBe(true)
796
+ expect(up.util.some(allFalse, 'prop')).toBe(false)
797
+
798
+ it 'short-circuits once an element returns true', ->
799
+ count = 0
800
+ up.util.some [null, undefined, 'foo', ''], (element) ->
801
+ count += 1
802
+ up.util.isPresent(element)
803
+ expect(count).toBe(3)
804
+
805
+ it 'iterates over an array-like value', ->
806
+ one = fixture('.qwertz')
807
+ two = fixture('.qwertz')
808
+ nodeList = document.querySelectorAll('.qwertz')
809
+
810
+ callback = jasmine.createSpy()
811
+
812
+ up.util.some nodeList, callback
813
+ expect(callback.calls.allArgs()).toEqual [[one, 0], [two, 1]]
814
+
815
+ describe 'up.util.findResult', ->
816
+
817
+ it 'consecutively applies the function to each array element and returns the first truthy return value', ->
818
+ map = {
819
+ a: '',
820
+ b: null,
821
+ c: undefined,
822
+ d: 'DEH',
823
+ e: 'EH'
824
+ }
825
+ fn = (el) -> map[el]
826
+
827
+ result = up.util.findResult ['a', 'b', 'c', 'd', 'e'], fn
828
+ expect(result).toEqual('DEH')
829
+
830
+ it 'returns undefined if the function does not return a truthy value for any element in the array', ->
831
+ map = {}
832
+ fn = (el) -> map[el]
833
+
834
+ result = up.util.findResult ['a', 'b', 'c'], fn
835
+ expect(result).toBeUndefined()
836
+
837
+ it 'iterates over an array-like value', ->
838
+ one = fixture('.qwertz')
839
+ two = fixture('.qwertz')
840
+ nodeList = document.querySelectorAll('.qwertz')
841
+
842
+ callback = jasmine.createSpy()
843
+
844
+ up.util.findResult nodeList, callback
845
+ expect(callback.calls.allArgs()).toEqual [[one, 0], [two, 1]]
808
846
 
809
847
  describe 'up.util.isBlank', ->
810
848
 
@@ -847,10 +885,26 @@ describe 'up.util', ->
847
885
  it 'returns true for an object with at least one key', ->
848
886
  expect(up.util.isBlank({key: 'value'})).toBe(false)
849
887
 
888
+ it 'returns true for an object with an [up.util.isBlank.key] method that returns true', ->
889
+ value = {}
890
+ value[up.util.isBlank.key] = -> true
891
+ expect(up.util.isBlank(value)).toBe(true)
892
+
893
+ it 'returns false for an object with an [up.util.isBlank.key] method that returns false', ->
894
+ value = {}
895
+ value[up.util.isBlank.key] = -> false
896
+ expect(up.util.isBlank(value)).toBe(false)
897
+
898
+ it 'returns false for a DOM element', ->
899
+ value = document.body
900
+ expect(up.util.isBlank(value)).toBe(false)
901
+
850
902
  describe 'up.util.normalizeUrl', ->
851
903
 
852
904
  it 'normalizes a relative path', ->
853
- expect(up.util.normalizeUrl('foo')).toBe("http://#{location.hostname}:#{location.port}/foo")
905
+ up.history.config.enabled = true
906
+ up.history.replace('/qux/')
907
+ expect(up.util.normalizeUrl('foo')).toBe("http://#{location.hostname}:#{location.port}/qux/foo")
854
908
 
855
909
  it 'normalizes an absolute path', ->
856
910
  expect(up.util.normalizeUrl('/foo')).toBe("http://#{location.hostname}:#{location.port}/foo")
@@ -879,45 +933,17 @@ describe 'up.util', ->
879
933
  it 'puts a #hash behind the query string', ->
880
934
  expect(up.util.normalizeUrl('http://example.com/foo/bar?key=value#fragment', hash: true)).toBe('http://example.com/foo/bar?key=value#fragment')
881
935
 
882
- describe 'up.util.detect', ->
936
+ describe 'up.util.find', ->
883
937
 
884
938
  it 'finds the first element in the given array that matches the given tester', ->
885
939
  array = ['foo', 'bar', 'baz']
886
940
  tester = (element) -> element[0] == 'b'
887
- expect(up.util.detect(array, tester)).toEqual('bar')
941
+ expect(up.util.find(array, tester)).toEqual('bar')
888
942
 
889
943
  it "returns undefined if the given array doesn't contain a matching element", ->
890
944
  array = ['foo', 'bar', 'baz']
891
945
  tester = (element) -> element[0] == 'z'
892
- expect(up.util.detect(array, tester)).toBeUndefined()
893
-
894
- describe 'up.util.config', ->
895
-
896
- it 'creates an object with the given attributes', ->
897
- object = up.util.config(a: 1, b: 2)
898
- expect(object.a).toBe(1)
899
- expect(object.b).toBe(2)
900
-
901
- it 'does not allow to set a key that was not included in the factory settings', ->
902
- object = up.util.config(a: 1)
903
- object.b = 2
904
- expect(object.b).toBeUndefined()
905
-
906
- describe '#reset', ->
907
-
908
- it 'resets the object to its original state', ->
909
- object = up.util.config(a: 1)
910
- expect(object.b).toBeUndefined()
911
- object.a = 2
912
- expect(object.a).toBe(2)
913
- object.reset()
914
- expect(object.a).toBe(1)
915
-
916
- it 'does not remove the #reset or #update method from the object', ->
917
- object = up.util.config(a: 1)
918
- object.b = 2
919
- object.reset()
920
- expect(object.reset).toBeDefined()
946
+ expect(up.util.find(array, tester)).toBeUndefined()
921
947
 
922
948
  describe 'up.util.remove', ->
923
949
 
@@ -966,60 +992,41 @@ describe 'up.util', ->
966
992
  expect(object.b).toBe('b value')
967
993
  expect(object.c).toBe('a value')
968
994
 
969
- describe 'up.util.selectInSubtree', ->
970
-
971
- it 'finds the selector in ancestors and descendants of the given element', ->
972
- $grandMother = affix('.grand-mother.match')
973
- $mother = $grandMother.affix('.mother')
974
- $element = $mother.affix('.element')
975
- $child = $element.affix('.child.match')
976
- $grandChild = $child.affix('.grand-child.match')
977
-
978
- $matches = up.util.selectInSubtree($element, '.match')
979
- $expected = $child.add($grandChild)
980
- expect($matches).toEqual $expected
981
-
982
- it 'finds the element itself if it matches the selector', ->
983
- $element = affix('.element.match')
984
- $matches = up.util.selectInSubtree($element, '.match')
985
- expect($matches).toEqual $element
986
-
987
- describe 'when given a jQuery collection with multiple elements', ->
988
-
989
- it 'searches in a all subtrees of the given elements', ->
990
- $a_grandMother = affix('.grand-mother.match')
991
- $a_mother = $a_grandMother.affix('.mother')
992
- $a_element = $a_mother.affix('.element')
993
- $a_child = $a_element.affix('.child.match')
994
- $a_grandChild = $a_child.affix('.grand-child.match')
995
-
996
- $b_grandMother = affix('.grand-mother.match')
997
- $b_mother = $b_grandMother.affix('.mother')
998
- $b_element = $b_mother.affix('.element')
999
- $b_child = $b_element.affix('.child.match')
1000
- $b_grandChild = $b_child.affix('.grand-child.match')
1001
-
1002
- $matches = up.util.selectInSubtree($a_element.add($b_element), '.match')
1003
- expect($matches).toEqual $a_child.add($a_grandChild).add($b_child).add($b_grandChild)
1004
-
1005
-
1006
- describe 'up.util.selectInDynasty', ->
1007
-
1008
- it 'finds the selector in both ancestors and descendants of the given element', ->
1009
- $grandMother = affix('.grand-mother.match')
1010
- $mother = $grandMother.affix('.mother')
1011
- $element = $mother.affix('.element')
1012
- $child = $element.affix('.child.match')
1013
- $grandChild = $child.affix('.grand-child.match')
1014
-
1015
- $matches = up.util.selectInDynasty($element, '.match')
1016
- $expected = $grandMother.add($child).add($grandChild)
1017
- expect($matches).toEqual $expected
1018
-
1019
- it 'finds the element itself if it matches the selector', ->
1020
- $element = affix('.element.match')
1021
- $matches = up.util.selectInDynasty($element, '.match')
1022
- expect($matches).toEqual $element
995
+ # describe 'up.util.offsetParent', ->
996
+ #
997
+ # it 'returns the first ascendant that has a "position" style', ->
998
+ # $a = $fixture('.a')
999
+ # $b = $a.affix('.b').css(position: 'relative')
1000
+ # $c = $b.affix('.c')
1001
+ # $d = $c.affix('.d')
1002
+ #
1003
+ # expect(up.util.offsetParent($d[0])).toBe($b[0])
1004
+ #
1005
+ # it 'does not return the given element, even when it has position', ->
1006
+ # $a = $fixture('.a').css(position: 'absolute')
1007
+ # $b = $a.affix('.b').css(position: 'relative')
1008
+ #
1009
+ # expect(up.util.offsetParent($b[0])).toBe($a[0])
1010
+ #
1011
+ # it 'returns the <body> element if there is no closer offset parent', ->
1012
+ # $a = $fixture('.a')
1013
+ # $b = $a.affix('.b')
1014
+ #
1015
+ # expect(up.util.offsetParent($b[0])).toBe(document.body)
1016
+ #
1017
+ # it 'returns the offset parent for a detached element', ->
1018
+ # $a = $fixture('.a').detach()
1019
+ # $b = $a.affix('.b').css(position: 'relative')
1020
+ # $c = $b.affix('.c')
1021
+ # $d = $c.affix('.d')
1022
+ #
1023
+ # expect(up.util.offsetParent($d[0])).toBe($b[0])
1024
+ #
1025
+ # it 'returns a missing value (and not <body>) if the given detached element has no ancestor with position', ->
1026
+ # $a = $fixture('.a').detach()
1027
+ # $b = $a.affix('.b')
1028
+ #
1029
+ # expect(up.util.offsetParent($b[0])).toBeMissing()
1023
1030
 
1024
1031
  describe 'up.util.isCrossDomain', ->
1025
1032
 
@@ -1240,4 +1247,160 @@ describe 'up.util', ->
1240
1247
 
1241
1248
  expect(target).toEqual { a: 1, b: 2, c: 3, d: 4, e: 5 }
1242
1249
 
1250
+ describe 'up.util.copy', ->
1251
+
1252
+ it 'returns a shallow copy of the given array', ->
1253
+ original = ['a', { b: 'c' }, 'd']
1254
+
1255
+ copy = up.util.copy(original)
1256
+ expect(copy).toEqual(original)
1257
+
1258
+ # Test that changes to copy don't change original
1259
+ copy.pop()
1260
+ expect(copy.length).toBe(2)
1261
+ expect(original.length).toBe(3)
1262
+
1263
+ # Test that the copy is shallow
1264
+ copy[1].x = 'y'
1265
+ expect(original[1].x).toEqual('y')
1266
+
1267
+ it 'returns a shallow copy of the given plain object', ->
1268
+ original = {a: 'b', c: [1, 2], d: 'e'}
1269
+
1270
+ copy = up.util.copy(original)
1271
+ expect(copy).toEqual(original)
1272
+
1273
+ # Test that changes to copy don't change original
1274
+ copy.f = 'g'
1275
+ expect(original.f).toBeMissing()
1276
+
1277
+ # Test that the copy is shallow
1278
+ copy.c.push(3)
1279
+ expect(original.c).toEqual [1, 2, 3]
1280
+
1281
+ it 'allows custom classes to hook into the copy protocol by implementing a method named `up.util.copy.key`', ->
1282
+ class TestClass
1283
+ "#{up.util.copy.key}": ->
1284
+ return "custom copy"
1285
+
1286
+ instance = new TestClass()
1287
+ expect(up.util.copy(instance)).toEqual("custom copy")
1288
+
1289
+ it 'copies the given jQuery collection into an array', ->
1290
+ $one = $fixture('.one')
1291
+ $two = $fixture('.two')
1292
+ $collection = $one.add($two)
1293
+
1294
+ copy = up.util.copy($collection)
1295
+
1296
+ copy[0] = document.body
1297
+ expect($collection[0]).toBe($one[0])
1298
+
1299
+ it 'copies the given arguments object into an array', ->
1300
+ args = undefined
1301
+ (-> args = arguments)(1)
1302
+
1303
+ copy = up.util.copy(args)
1304
+ expect(copy).toBeArray()
1305
+
1306
+ copy[0] = 2
1307
+ expect(args[0]).toBe(1)
1308
+
1309
+ it 'returns the given string (which is immutable)', ->
1310
+ str = "foo"
1311
+ copy = up.util.copy(str)
1312
+ expect(copy).toBe(str)
1313
+
1314
+ it 'returns the given number (which is immutable)', ->
1315
+ number = 123
1316
+ copy = up.util.copy(number)
1317
+ expect(copy).toBe(number)
1318
+
1319
+ it 'copies the given Date object', ->
1320
+ date = new Date('1995-12-17T03:24:00')
1321
+ expect(date.getFullYear()).toBe(1995)
1322
+
1323
+ copy = up.util.copy(date)
1324
+
1325
+ expect(copy.getFullYear()).toBe(1995)
1326
+ expect(copy.getHours()).toBe(3)
1327
+ expect(copy.getMinutes()).toBe(24)
1328
+
1329
+ date.setFullYear(2018)
1330
+
1331
+ expect(copy.getFullYear()).toBe(1995)
1332
+
1333
+
1334
+ describe 'up.util.deepCopy', ->
1335
+
1336
+ it 'returns a deep copy of the given array', ->
1337
+ original = ['a', { b: 'c' }, 'd']
1338
+
1339
+ copy = up.util.deepCopy(original)
1340
+ expect(copy).toEqual(original)
1341
+
1342
+ # Test that changes to copy don't change original
1343
+ copy.pop()
1344
+ expect(copy.length).toBe(2)
1345
+ expect(original.length).toBe(3)
1346
+
1347
+ # Test that the copy is deep
1348
+ copy[1].x = 'y'
1349
+ expect(original[1].x).toBeUndefined()
1350
+
1351
+ it 'returns a deep copy of the given object', ->
1352
+ original = {a: 'b', c: [1, 2], d: 'e'}
1353
+
1354
+ copy = up.util.deepCopy(original)
1355
+ expect(copy).toEqual(original)
1356
+
1357
+ # Test that changes to copy don't change original
1358
+ copy.f = 'g'
1359
+ expect(original.f).toBeMissing()
1360
+
1361
+ # Test that the copy is deep
1362
+ copy.c.push(3)
1363
+ expect(original.c).toEqual [1, 2]
1364
+
1365
+ describe 'up.util.isList', ->
1366
+
1367
+ it 'returns true for an array', ->
1368
+ value = [1, 2, 3]
1369
+ expect(up.util.isList(value)).toBe(true)
1370
+
1371
+ it 'returns true for an HTMLCollection', ->
1372
+ value = document.getElementsByTagName('div')
1373
+ expect(up.util.isList(value)).toBe(true)
1374
+
1375
+ it 'returns true for a NodeList', ->
1376
+ value = document.querySelectorAll('div')
1377
+ expect(up.util.isList(value)).toBe(true)
1378
+
1379
+ it 'returns true for an arguments object', ->
1380
+ value = undefined
1381
+ (-> value = arguments)()
1382
+ expect(up.util.isList(value)).toBe(true)
1383
+
1384
+ it 'returns false for an object', ->
1385
+ value = { foo: 'bar' }
1386
+ expect(up.util.isList(value)).toBe(false)
1387
+
1388
+ it 'returns false for a string', ->
1389
+ value = 'foo'
1390
+ expect(up.util.isList(value)).toBe(false)
1391
+
1392
+ it 'returns false for a number', ->
1393
+ value = 123
1394
+ expect(up.util.isList(value)).toBe(false)
1395
+
1396
+ it 'returns false for undefined', ->
1397
+ value = undefined
1398
+ expect(up.util.isList(value)).toBe(false)
1399
+
1400
+ it 'returns false for null', ->
1401
+ value = null
1402
+ expect(up.util.isList(value)).toBe(false)
1243
1403
 
1404
+ it 'returns false for NaN', ->
1405
+ value = NaN
1406
+ expect(up.util.isList(value)).toBe(false)