unpoly-rails 0.57.0 → 0.60.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

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)