unpoly-rails 0.37.0 → 0.50.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 (88) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +127 -25
  3. data/LICENSE +1 -1
  4. data/README_RAILS.md +4 -2
  5. data/Rakefile +6 -1
  6. data/dist/unpoly.js +3192 -2198
  7. data/dist/unpoly.min.js +4 -3
  8. data/lib/assets/javascripts/unpoly/browser.coffee +51 -63
  9. data/lib/assets/javascripts/unpoly/bus.coffee +58 -33
  10. data/lib/assets/javascripts/unpoly/classes/cache.coffee +117 -0
  11. data/lib/assets/javascripts/unpoly/{dom → classes}/extract_cascade.coffee +3 -3
  12. data/lib/assets/javascripts/unpoly/{dom → classes}/extract_plan.coffee +1 -1
  13. data/lib/assets/javascripts/unpoly/classes/field_observer.coffee +57 -0
  14. data/lib/assets/javascripts/unpoly/classes/follow_variant.coffee +52 -0
  15. data/lib/assets/javascripts/unpoly/classes/motion_tracker.coffee +95 -0
  16. data/lib/assets/javascripts/unpoly/classes/record.coffee +16 -0
  17. data/lib/assets/javascripts/unpoly/classes/request.coffee +228 -0
  18. data/lib/assets/javascripts/unpoly/classes/response.coffee +138 -0
  19. data/lib/assets/javascripts/unpoly/dom.coffee +151 -142
  20. data/lib/assets/javascripts/unpoly/feedback.coffee +67 -38
  21. data/lib/assets/javascripts/unpoly/form.coffee +156 -139
  22. data/lib/assets/javascripts/unpoly/history.coffee +22 -19
  23. data/lib/assets/javascripts/unpoly/layout.coffee +108 -90
  24. data/lib/assets/javascripts/unpoly/link.coffee +159 -158
  25. data/lib/assets/javascripts/unpoly/log.coffee +5 -5
  26. data/lib/assets/javascripts/unpoly/modal.coffee +93 -81
  27. data/lib/assets/javascripts/unpoly/motion.coffee +291 -250
  28. data/lib/assets/javascripts/unpoly/popup.coffee +67 -53
  29. data/lib/assets/javascripts/unpoly/protocol.coffee +67 -16
  30. data/lib/assets/javascripts/unpoly/proxy.coffee +282 -211
  31. data/lib/assets/javascripts/unpoly/rails.coffee +3 -14
  32. data/lib/assets/javascripts/unpoly/syntax.coffee +54 -49
  33. data/lib/assets/javascripts/unpoly/tooltip.coffee +18 -25
  34. data/lib/assets/javascripts/unpoly/util.coffee +236 -477
  35. data/lib/assets/javascripts/unpoly.coffee +1 -1
  36. data/lib/unpoly/rails/inspector.rb +67 -22
  37. data/lib/unpoly/rails/version.rb +1 -1
  38. data/package.json +1 -1
  39. data/spec_app/Gemfile.lock +13 -13
  40. data/spec_app/app/assets/javascripts/integration_test.coffee +1 -0
  41. data/spec_app/app/assets/javascripts/jasmine_specs.coffee +1 -1
  42. data/spec_app/app/assets/stylesheets/jasmine_specs.sass +10 -0
  43. data/spec_app/app/controllers/binding_test_controller.rb +19 -2
  44. data/spec_app/app/controllers/method_test_controller.rb +16 -0
  45. data/spec_app/app/views/layouts/jasmine_rails/spec_runner.html.erb +20 -0
  46. data/spec_app/app/views/method_test/form_target.erb +17 -0
  47. data/spec_app/app/views/method_test/page1.erb +11 -0
  48. data/spec_app/app/views/method_test/page2.erb +6 -0
  49. data/spec_app/app/views/pages/start.erb +33 -19
  50. data/spec_app/config/initializers/assets.rb +5 -0
  51. data/spec_app/config/routes.rb +3 -0
  52. data/spec_app/spec/controllers/binding_test_controller_spec.rb +82 -27
  53. data/spec_app/spec/javascripts/helpers/agent_detector.coffee +17 -0
  54. data/spec_app/spec/javascripts/helpers/async_sequence.js.coffee +102 -0
  55. data/spec_app/spec/javascripts/helpers/last_request.js.coffee +1 -1
  56. data/spec_app/spec/javascripts/helpers/mock_ajax.js.coffee +5 -2
  57. data/spec_app/spec/javascripts/helpers/promise_state.js +18 -0
  58. data/spec_app/spec/javascripts/helpers/protect_jasmine_runner.coffee +9 -0
  59. data/spec_app/spec/javascripts/helpers/reset_history.js.coffee +22 -0
  60. data/spec_app/spec/javascripts/helpers/reset_up.js.coffee +11 -3
  61. data/spec_app/spec/javascripts/helpers/show_lib_versions.coffee +10 -0
  62. data/spec_app/spec/javascripts/helpers/to_be_error.coffee +5 -0
  63. data/spec_app/spec/javascripts/helpers/to_match_url.coffee +13 -0
  64. data/spec_app/spec/javascripts/helpers/trigger.js.coffee +13 -6
  65. data/spec_app/spec/javascripts/up/browser_spec.js.coffee +92 -33
  66. data/spec_app/spec/javascripts/up/bus_spec.js.coffee +64 -15
  67. data/spec_app/spec/javascripts/up/classes/.keep +0 -0
  68. data/spec_app/spec/javascripts/up/classes/cache_spec.js.coffee +1 -0
  69. data/spec_app/spec/javascripts/up/dom_spec.js.coffee +759 -551
  70. data/spec_app/spec/javascripts/up/feedback_spec.js.coffee +155 -82
  71. data/spec_app/spec/javascripts/up/form_spec.js.coffee +490 -349
  72. data/spec_app/spec/javascripts/up/history_spec.js.coffee +226 -179
  73. data/spec_app/spec/javascripts/up/layout_spec.js.coffee +253 -185
  74. data/spec_app/spec/javascripts/up/link_spec.js.coffee +416 -270
  75. data/spec_app/spec/javascripts/up/modal_spec.js.coffee +459 -330
  76. data/spec_app/spec/javascripts/up/motion_spec.js.coffee +198 -153
  77. data/spec_app/spec/javascripts/up/namespace_spec.js.coffee +9 -0
  78. data/spec_app/spec/javascripts/up/popup_spec.js.coffee +240 -175
  79. data/spec_app/spec/javascripts/up/protocol_spec.js.coffee +38 -0
  80. data/spec_app/spec/javascripts/up/proxy_spec.js.coffee +777 -303
  81. data/spec_app/spec/javascripts/up/rails_spec.js.coffee +24 -8
  82. data/spec_app/spec/javascripts/up/syntax_spec.js.coffee +40 -23
  83. data/spec_app/spec/javascripts/up/tooltip_spec.js.coffee +80 -66
  84. data/spec_app/spec/javascripts/up/util_spec.js.coffee +227 -201
  85. data/spec_app/vendor/asset-libs/es6-promise-4.1.6/es6-promise.auto.js +1159 -0
  86. metadata +30 -7
  87. data/spec_app/spec/javascripts/helpers/reset_path.js.coffee +0 -7
  88. data/spec_app/spec/javascripts/helpers/to_equal_url.coffee +0 -11
@@ -23,43 +23,48 @@ describe 'up.dom', ->
23
23
 
24
24
  @respond = (options = {}) -> @respondWith(@responseText, options)
25
25
 
26
- it 'replaces the given selector with the same selector from a freshly fetched page', (done) ->
27
- promise = up.replace('.middle', '/path')
28
- @respond()
29
- promise.then ->
26
+ it 'replaces the given selector with the same selector from a freshly fetched page', asyncSpec (next) ->
27
+ up.replace('.middle', '/path')
28
+
29
+ next =>
30
+ @respond()
31
+
32
+ next.after 10, =>
30
33
  expect($('.before')).toHaveText('old-before')
31
34
  expect($('.middle')).toHaveText('new-middle')
32
35
  expect($('.after')).toHaveText('old-after')
33
- done()
34
-
35
- it 'sends an X-Up-Target HTTP header along with the request', ->
36
- up.replace('.middle', '/path')
37
- request = @lastRequest()
38
- expect(request.requestHeaders['X-Up-Target']).toEqual('.middle')
39
36
 
40
- it 'returns a promise that will be resolved once the server response was received and the fragments were swapped', ->
37
+ it 'returns a promise that will be fulfilled once the server response was received and the fragments were swapped', asyncSpec (next) ->
41
38
  resolution = jasmine.createSpy()
42
39
  promise = up.replace('.middle', '/path')
43
40
  promise.then(resolution)
44
41
  expect(resolution).not.toHaveBeenCalled()
45
42
  expect($('.middle')).toHaveText('old-middle')
46
- @respond()
47
- expect(resolution).toHaveBeenCalled()
48
- expect($('.middle')).toHaveText('new-middle')
43
+
44
+ next =>
45
+ @respond()
46
+
47
+ next =>
48
+ expect(resolution).toHaveBeenCalled()
49
+ expect($('.middle')).toHaveText('new-middle')
49
50
 
50
51
  describe 'cleaning up', ->
51
52
 
52
- it 'calls destructors on the replaced element', ->
53
+ it 'calls destructors on the replaced element', asyncSpec (next) ->
53
54
  destructor = jasmine.createSpy('destructor')
54
55
  up.compiler '.container', -> destructor
55
56
  $container = affix('.container')
56
57
  up.hello($container)
57
58
  up.replace('.container', '/path')
58
- @respondWith '<div class="container">new text</div>'
59
- expect('.container').toHaveText('new text')
60
- expect(destructor).toHaveBeenCalled()
61
59
 
62
- it 'calls destructors when the replaced element is a singleton element like <body> (bugfix)', ->
60
+ next =>
61
+ @respondWith '<div class="container">new text</div>'
62
+
63
+ next =>
64
+ expect('.container').toHaveText('new text')
65
+ expect(destructor).toHaveBeenCalled()
66
+
67
+ it 'calls destructors when the replaced element is a singleton element like <body> (bugfix)', asyncSpec (next) ->
63
68
  # isSingletonElement() is true for body, but can't have the example replace the Jasmine test runner UI
64
69
  up.dom.knife.mock('isSingletonElement').and.callFake ($element) -> $element.is('.container')
65
70
  destructor = jasmine.createSpy('destructor')
@@ -67,184 +72,203 @@ describe 'up.dom', ->
67
72
  $container = affix('.container')
68
73
  up.hello($container)
69
74
  up.replace('.container', '/path')
70
- @respondWith '<div class="container">new text</div>'
71
- expect('.container').toHaveText('new text')
72
- expect(destructor).toHaveBeenCalled()
75
+
76
+ next =>
77
+ @respondWith '<div class="container">new text</div>'
78
+
79
+ next =>
80
+ expect('.container').toHaveText('new text')
81
+ expect(destructor).toHaveBeenCalled()
73
82
 
74
83
  describe 'transitions', ->
75
84
 
76
- it 'returns a promise that will be resolved once the server response was received and the swap transition has completed', (done) ->
85
+ it 'returns a promise that will be fulfilled once the server response was received and the swap transition has completed', asyncSpec (next) ->
77
86
  resolution = jasmine.createSpy()
78
87
  promise = up.replace('.middle', '/path', transition: 'cross-fade', duration: 50)
79
88
  promise.then(resolution)
80
89
  expect(resolution).not.toHaveBeenCalled()
81
90
  expect($('.middle')).toHaveText('old-middle')
82
- @respond()
83
- expect(resolution).not.toHaveBeenCalled()
84
- u.setTimer 20, ->
91
+
92
+ next =>
93
+ @respond()
94
+ expect(resolution).not.toHaveBeenCalled()
95
+
96
+ next.after 20, =>
85
97
  expect(resolution).not.toHaveBeenCalled()
86
- u.setTimer 80, ->
87
- expect(resolution).toHaveBeenCalled()
88
- done()
89
98
 
90
- it 'ignores a { transition } option when replacing the body element', (done) ->
99
+ next.after 80, =>
100
+ expect(resolution).toHaveBeenCalled()
101
+
102
+ it 'ignores a { transition } option when replacing the body element', asyncSpec (next) ->
91
103
  up.dom.knife.mock('swapSingletonElement') # can't have the example replace the Jasmine test runner UI
92
104
  up.dom.knife.mock('destroy') # if we don't swap the body, up.dom will destroy it
93
105
  replaceCallback = jasmine.createSpy()
94
106
  promise = up.replace('body', '/path', transition: 'cross-fade', duration: 50)
95
107
  promise.then(replaceCallback)
96
108
  expect(replaceCallback).not.toHaveBeenCalled()
97
- @responseText = '<body>new text</body>'
98
- @respond()
99
- u.nextFrame ->
100
- expect(replaceCallback).toHaveBeenCalled()
101
- done()
102
109
 
103
- describe 'when the server signals a redirect with X-Up-Location header (bugfix, logic should be moved to up.proxy)', ->
104
-
105
- it 'considers a redirection URL an alias for the requested URL', ->
106
- up.replace('.middle', '/foo')
107
- expect(jasmine.Ajax.requests.count()).toEqual(1)
108
- @respond(responseHeaders: { 'X-Up-Location': '/bar', 'X-Up-Method': 'GET' })
109
- up.replace('.middle', '/bar')
110
- expect(jasmine.Ajax.requests.count()).toEqual(1)
111
-
112
- it 'does not considers a redirection URL an alias for the requested URL if the original request was never cached', ->
113
- up.replace('.middle', '/foo', method: 'post') # POST requests are not cached
114
- expect(jasmine.Ajax.requests.count()).toEqual(1)
115
- @respond(responseHeaders: { 'X-Up-Location': '/bar', 'X-Up-Method': 'GET' })
116
- up.replace('.middle', '/bar')
117
- expect(jasmine.Ajax.requests.count()).toEqual(2)
118
-
119
- it 'does not considers a redirection URL an alias for the requested URL if the response returned a non-200 status code', ->
120
- up.replace('.middle', '/foo', failTarget: '.middle')
121
- expect(jasmine.Ajax.requests.count()).toEqual(1)
122
- @respond(responseHeaders: { 'X-Up-Location': '/bar', 'X-Up-Method': 'GET' }, status: '500')
123
- up.replace('.middle', '/bar')
124
- expect(jasmine.Ajax.requests.count()).toEqual(2)
125
-
126
- it "does not explode if the original request's { data } is a FormData object", ->
127
- up.replace('.middle', '/foo', method: 'post', data: new FormData()) # POST requests are not cached
128
- expect(jasmine.Ajax.requests.count()).toEqual(1)
129
- @respond(responseHeaders: { 'X-Up-Location': '/bar', 'X-Up-Method': 'GET' })
130
- secondReplace = -> up.replace('.middle', '/bar')
131
- expect(secondReplace).not.toThrowError()
110
+ next =>
111
+ @responseText = '<body>new text</body>'
112
+ @respond()
113
+
114
+ next =>
115
+ expect(replaceCallback).toHaveBeenCalled()
132
116
 
133
117
  describe 'with { data } option', ->
134
118
 
135
- it "uses the given params as a non-GET request's payload", ->
119
+ it "uses the given params as a non-GET request's payload", asyncSpec (next) ->
136
120
  givenParams = { 'foo-key': 'foo-value', 'bar-key': 'bar-value' }
137
121
  up.replace('.middle', '/path', method: 'put', data: givenParams)
138
- expect(@lastRequest().data()['foo-key']).toEqual(['foo-value'])
139
- expect(@lastRequest().data()['bar-key']).toEqual(['bar-value'])
140
122
 
141
- it "encodes the given params into the URL of a GET request", ->
123
+ next =>
124
+ expect(@lastRequest().data()['foo-key']).toEqual(['foo-value'])
125
+ expect(@lastRequest().data()['bar-key']).toEqual(['bar-value'])
126
+
127
+ it "encodes the given params into the URL of a GET request", asyncSpec (next) ->
142
128
  givenParams = { 'foo-key': 'foo-value', 'bar-key': 'bar-value' }
143
129
  up.replace('.middle', '/path', method: 'get', data: givenParams)
144
- expect(@lastRequest().url).toEqualUrl('/path?foo-key=foo-value&bar-key=bar-value')
130
+ next => expect(@lastRequest().url).toMatchUrl('/path?foo-key=foo-value&bar-key=bar-value')
145
131
 
146
- it 'uses a HTTP method given as { method } option', ->
132
+ it 'uses a HTTP method given as { method } option', asyncSpec (next) ->
147
133
  up.replace('.middle', '/path', method: 'put')
148
- expect(@lastRequest()).toHaveRequestMethod('PUT')
134
+ next => expect(@lastRequest()).toHaveRequestMethod('PUT')
149
135
 
150
136
  describe 'when the server responds with a non-200 status code', ->
151
137
 
152
- it 'replaces the <body> instead of the given selector', ->
138
+ it 'replaces the first fallback instead of the given selector', asyncSpec (next) ->
139
+ up.dom.config.fallbacks = ['.fallback']
140
+ affix('.fallback')
141
+
153
142
  # can't have the example replace the Jasmine test runner UI
154
- extractSpy = up.dom.knife.mock('extract').and.returnValue(u.resolvedPromise())
155
- up.replace('.middle', '/path')
156
- @respond(status: 500)
157
- expect(extractSpy).toHaveBeenCalledWith('body', jasmine.any(String), jasmine.any(Object))
143
+ extractSpy = up.dom.knife.mock('extract').and.returnValue(Promise.resolve())
158
144
 
159
- it 'uses a target selector given as { failTarget } option', ->
160
- up.replace('.middle', '/path', failTarget: '.after')
161
- @respond(status: 500)
162
- expect($('.middle')).toHaveText('old-middle')
163
- expect($('.after')).toHaveText('new-after')
145
+ next => up.replace('.middle', '/path')
146
+ next => @respond(status: 500)
147
+ next => expect(extractSpy).toHaveBeenCalledWith('.fallback', jasmine.any(String), jasmine.any(Object))
148
+
149
+ it 'uses a target selector given as { failTarget } option', asyncSpec (next) ->
150
+ next =>
151
+ up.replace('.middle', '/path', failTarget: '.after')
164
152
 
165
- it 'rejects the returned promise', ->
153
+ next =>
154
+ @respond(status: 500)
155
+
156
+ next =>
157
+ expect($('.middle')).toHaveText('old-middle')
158
+ expect($('.after')).toHaveText('new-after')
159
+
160
+ it 'rejects the returned promise', (done) ->
166
161
  affix('.after')
167
162
  promise = up.replace('.middle', '/path', failTarget: '.after')
168
- expect(promise.state()).toEqual('pending')
169
- @respond(status: 500)
170
- expect(promise.state()).toEqual('rejected')
163
+
164
+ u.nextFrame =>
165
+ promiseState(promise).then (result) =>
166
+ expect(result.state).toEqual('pending')
167
+
168
+ @respond(status: 500)
169
+
170
+ u.nextFrame =>
171
+ promiseState(promise).then (result) =>
172
+ expect(result.state).toEqual('rejected')
173
+ done()
171
174
 
172
175
  describe 'when the request times out', ->
173
176
 
174
- it "doesn't crash and rejects the returned promise", (done) ->
177
+ it "doesn't crash and rejects the returned promise", asyncSpec (next) ->
175
178
  jasmine.clock().install() # required by responseTimeout()
176
179
  affix('.target')
177
- promise = up.replace('.middle', '/path', timeout: 10 * 1000)
178
- expect(promise.state()).toEqual('pending')
179
- jasmine.clock().tick(11 * 1000)
180
- expect(promise.state()).toEqual('rejected')
181
- done()
180
+ promise = up.replace('.middle', '/path', timeout: 50)
181
+
182
+ next =>
183
+ # See that the correct timeout value has been set on the XHR instance
184
+ expect(@lastRequest().timeout).toEqual(50)
185
+
186
+ next.await =>
187
+ # See that the promise is still pending before the timeout
188
+ promiseState(promise).then (result) -> expect(result.state).toEqual('pending')
189
+
190
+ next =>
191
+ @lastRequest().responseTimeout()
192
+
193
+ next.await =>
194
+ promiseState(promise).then (result) -> expect(result.state).toEqual('rejected')
182
195
 
183
196
  describe 'when there is a network issue', ->
184
197
 
185
198
  it "doesn't crash and rejects the returned promise", (done) ->
186
199
  affix('.target')
187
200
  promise = up.replace('.middle', '/path')
188
- @lastRequest().responseError()
189
- u.nextFrame ->
190
- expect(promise.state()).toEqual('rejected')
191
- done()
201
+
202
+ u.nextFrame =>
203
+ promiseState(promise).then (result) =>
204
+ expect(result.state).toEqual('pending')
205
+ @lastRequest().responseError()
206
+ u.nextFrame =>
207
+ promiseState(promise).then (result) =>
208
+ expect(result.state).toEqual('rejected')
209
+ done()
192
210
 
193
211
  describe 'history', ->
194
212
 
213
+ beforeEach ->
214
+ up.history.config.enabled = true
215
+
195
216
  it 'should set the browser location to the given URL', (done) ->
196
217
  promise = up.replace('.middle', '/path')
197
218
  @respond()
198
219
  promise.then ->
199
- expect(location.href).toEqualUrl('/path')
220
+ expect(location.href).toMatchUrl('/path')
200
221
  done()
201
222
 
202
- it 'does not add a history entry after non-GET requests', ->
203
- promise = up.replace('.middle', '/path', method: 'post')
204
- @respond()
205
- expect(location.href).toEqualUrl(@hrefBeforeExample)
206
-
207
- it 'adds a history entry after non-GET requests if the response includes a { X-Up-Method: "get" } header (will happen after a redirect)', ->
208
- promise = up.replace('.middle', '/path', method: 'post')
209
- @respond(responseHeaders: { 'X-Up-Method': 'GET' })
210
- expect(location.href).toEqualUrl('/path')
211
-
212
- it 'does not a history entry after a failed GET-request', ->
213
- promise = up.replace('.middle', '/path', method: 'post', failTarget: '.middle')
214
- @respond(status: 500)
215
- expect(location.href).toEqualUrl(@hrefBeforeExample)
216
-
217
- it 'does not add a history entry with { history: false } option', ->
218
- promise = up.replace('.middle', '/path', history: false)
219
- @respond()
220
- expect(location.href).toEqualUrl(@hrefBeforeExample)
221
-
222
- it "detects a redirect's new URL when the server sets an X-Up-Location header", ->
223
- promise = up.replace('.middle', '/path')
224
- @respond(responseHeaders: { 'X-Up-Location': '/other-path' })
225
- expect(location.href).toEqualUrl('/other-path')
223
+ it 'does not add a history entry after non-GET requests', asyncSpec (next) ->
224
+ up.replace('.middle', '/path', method: 'post')
225
+ next => @respond()
226
+ next => expect(location.href).toMatchUrl(@hrefBeforeExample)
227
+
228
+ it 'adds a history entry after non-GET requests if the response includes a { X-Up-Method: "get" } header (will happen after a redirect)', asyncSpec (next) ->
229
+ up.replace('.middle', '/requested-path', method: 'post')
230
+ next => @respond(responseHeaders:
231
+ 'X-Up-Method': 'GET'
232
+ 'X-Up-Location': '/signaled-path'
233
+ )
234
+ next => expect(location.href).toMatchUrl('/signaled-path')
235
+
236
+ it 'does not a history entry after a failed GET-request', asyncSpec (next) ->
237
+ up.replace('.middle', '/path', method: 'post', failTarget: '.middle')
238
+ next => @respond(status: 500)
239
+ next => expect(location.href).toMatchUrl(@hrefBeforeExample)
240
+
241
+ it 'does not add a history entry with { history: false } option', asyncSpec (next) ->
242
+ up.replace('.middle', '/path', history: false)
243
+ next => @respond()
244
+ next => expect(location.href).toMatchUrl(@hrefBeforeExample)
245
+
246
+ it "detects a redirect's new URL when the server sets an X-Up-Location header", asyncSpec (next) ->
247
+ up.replace('.middle', '/path')
248
+ next => @respond(responseHeaders: { 'X-Up-Location': '/other-path' })
249
+ next => expect(location.href).toMatchUrl('/other-path')
226
250
 
227
- it 'adds params from a { data } option to the URL of a GET request', ->
228
- promise = up.replace('.middle', '/path', data: { 'foo-key': 'foo value', 'bar-key': 'bar value' })
229
- @respond()
230
- expect(location.href).toEqualUrl('/path?foo-key=foo%20value&bar-key=bar%20value')
251
+ it 'adds params from a { data } option to the URL of a GET request', asyncSpec (next) ->
252
+ up.replace('.middle', '/path', data: { 'foo-key': 'foo value', 'bar-key': 'bar value' })
253
+ next => @respond()
254
+ next => expect(location.href).toMatchUrl('/path?foo-key=foo%20value&bar-key=bar%20value')
231
255
 
232
256
  describe 'if a URL is given as { history } option', ->
233
257
 
234
- it 'uses that URL as the new location after a GET request', ->
235
- promise = up.replace('.middle', '/path', history: '/given-path')
236
- @respond(failTarget: '.middle')
237
- expect(location.href).toEqualUrl('/given-path')
258
+ it 'uses that URL as the new location after a GET request', asyncSpec (next) ->
259
+ up.replace('.middle', '/path', history: '/given-path')
260
+ next => @respond(failTarget: '.middle')
261
+ next => expect(location.href).toMatchUrl('/given-path')
238
262
 
239
- it 'adds a history entry after a non-GET request', ->
240
- promise = up.replace('.middle', '/path', method: 'post', history: '/given-path')
241
- @respond(failTarget: '.middle')
242
- expect(location.href).toEqualUrl('/given-path')
263
+ it 'adds a history entry after a non-GET request', asyncSpec (next) ->
264
+ up.replace('.middle', '/path', method: 'post', history: '/given-path')
265
+ next => @respond(failTarget: '.middle')
266
+ next => expect(location.href).toMatchUrl('/given-path')
243
267
 
244
- it 'does not add a history entry after a failed non-GET request', ->
245
- promise = up.replace('.middle', '/path', method: 'post', history: '/given-path', failTarget: '.middle')
246
- @respond(failTarget: '.middle', status: 500)
247
- expect(location.href).toEqualUrl(@hrefBeforeExample)
268
+ it 'does not add a history entry after a failed non-GET request', asyncSpec (next) ->
269
+ up.replace('.middle', '/path', method: 'post', history: '/given-path', failTarget: '.middle')
270
+ next => @respond(failTarget: '.middle', status: 500)
271
+ next => expect(location.href).toMatchUrl(@hrefBeforeExample)
248
272
 
249
273
  describe 'source', ->
250
274
 
@@ -255,72 +279,109 @@ describe 'up.dom', ->
255
279
  expect($('.middle').attr('up-source')).toMatch(/\/path$/)
256
280
  done()
257
281
 
258
- it 'reuses the previous source for a non-GET request (since that is reloadable)', ->
282
+ it 'reuses the previous source for a non-GET request (since that is reloadable)', asyncSpec (next) ->
259
283
  @oldMiddle.attr('up-source', '/previous-source')
260
284
  up.replace('.middle', '/path', method: 'post')
261
- @respond()
262
- expect($('.middle')).toHaveText('new-middle')
263
- expect(up.dom.source('.middle')).toEqualUrl('/previous-source')
285
+ next =>
286
+ @respond()
287
+ next =>
288
+ expect($('.middle')).toHaveText('new-middle')
289
+ expect(up.dom.source('.middle')).toMatchUrl('/previous-source')
264
290
 
265
291
  describe 'if a URL is given as { source } option', ->
266
292
 
267
- it 'uses that URL as the source for a GET request', ->
268
- promise = up.replace('.middle', '/path', source: '/given-path')
269
- @respond()
270
- expect(up.dom.source('.middle')).toEqualUrl('/given-path')
293
+ it 'uses that URL as the source for a GET request', asyncSpec (next) ->
294
+ up.replace('.middle', '/path', source: '/given-path')
295
+ next => @respond()
296
+ next => expect(up.dom.source('.middle')).toMatchUrl('/given-path')
271
297
 
272
- it 'uses that URL as the source after a non-GET request', ->
273
- promise = up.replace('.middle', '/path', method: 'post', source: '/given-path')
274
- @respond()
275
- expect(up.dom.source('.middle')).toEqualUrl('/given-path')
298
+ it 'uses that URL as the source after a non-GET request', asyncSpec (next) ->
299
+ up.replace('.middle', '/path', method: 'post', source: '/given-path')
300
+ next => @respond()
301
+ next => expect(up.dom.source('.middle')).toMatchUrl('/given-path')
276
302
 
277
- it 'ignores the option and reuses the previous source after a failed non-GET request', ->
303
+ it 'ignores the option and reuses the previous source after a failed non-GET request', asyncSpec (next) ->
278
304
  @oldMiddle.attr('up-source', '/previous-source')
279
- promise = up.replace('.middle', '/path', method: 'post', source: '/given-path', failTarget: '.middle')
280
- @respond(status: 500)
281
- expect(up.dom.source('.middle')).toEqualUrl('/previous-source')
305
+ up.replace('.middle', '/path', method: 'post', source: '/given-path', failTarget: '.middle')
306
+ next => @respond(status: 500)
307
+ next => expect(up.dom.source('.middle')).toMatchUrl('/previous-source')
282
308
 
283
309
  describe 'document title', ->
284
310
 
285
- it "sets the document title to the response <title>", ->
311
+ beforeEach ->
312
+ up.history.config.enabled = true
313
+
314
+ it "sets the document title to the response <title>", asyncSpec (next) ->
286
315
  affix('.container').text('old container text')
287
316
  up.replace('.container', '/path')
288
- @respondWith """
289
- <html>
290
- <head>
291
- <title>Title from HTML</title>
292
- </head>
293
- <body>
317
+
318
+ next =>
319
+ @respondWith """
320
+ <html>
321
+ <head>
322
+ <title>Title from HTML</title>
323
+ </head>
324
+ <body>
325
+ <div class='container'>
326
+ new container text
327
+ </div>
328
+ </body>
329
+ </html>
330
+ """
331
+
332
+ next =>
333
+ expect($('.container')).toHaveText('new container text')
334
+ expect(document.title).toBe('Title from HTML')
335
+
336
+ it "sets the document title to an 'X-Up-Title' header in the response", asyncSpec (next) ->
337
+ affix('.container').text('old container text')
338
+ up.replace('.container', '/path')
339
+
340
+ next =>
341
+ @respondWith
342
+ responseHeaders:
343
+ 'X-Up-Title': 'Title from header'
344
+ responseText: """
294
345
  <div class='container'>
295
346
  new container text
296
347
  </div>
297
- </body>
298
- </html>
299
- """
300
- expect($('.container')).toHaveText('new container text')
301
- expect(document.title).toBe('Title from HTML')
348
+ """
349
+
350
+ next =>
351
+ expect($('.container')).toHaveText('new container text')
352
+ expect(document.title).toBe('Title from header')
302
353
 
303
- it "sets the document title to an 'X-Up-Title' header in the response", ->
354
+ it "prefers the X-Up-Title header to the response <title>", asyncSpec (next) ->
304
355
  affix('.container').text('old container text')
305
356
  up.replace('.container', '/path')
306
- @respondWith
307
- responseHeaders:
308
- 'X-Up-Title': 'Title from header'
309
- responseText: """
310
- <div class='container'>
311
- new container text
312
- </div>
357
+
358
+ next =>
359
+ @respondWith
360
+ responseHeaders:
361
+ 'X-Up-Title': 'Title from header'
362
+ responseText: """
363
+ <html>
364
+ <head>
365
+ <title>Title from HTML</title>
366
+ </head>
367
+ <body>
368
+ <div class='container'>
369
+ new container text
370
+ </div>
371
+ </body>
372
+ </html>
313
373
  """
314
- expect($('.container')).toHaveText('new container text')
315
- expect(document.title).toBe('Title from header')
316
374
 
317
- it "prefers the X-Up-Title header to the response <title>", ->
375
+ next =>
376
+ expect($('.container')).toHaveText('new container text')
377
+ expect(document.title).toBe('Title from header')
378
+
379
+ it "sets the document title to the response <title> with { history: false, title: true } options (bugfix)", asyncSpec (next) ->
318
380
  affix('.container').text('old container text')
319
- up.replace('.container', '/path')
320
- @respondWith
321
- responseHeaders:
322
- 'X-Up-Title': 'Title from header'
323
- responseText: """
381
+ up.replace('.container', '/path', history: false, title: true)
382
+
383
+ next =>
384
+ @respondWith """
324
385
  <html>
325
386
  <head>
326
387
  <title>Title from HTML</title>
@@ -332,85 +393,80 @@ describe 'up.dom', ->
332
393
  </body>
333
394
  </html>
334
395
  """
335
- expect($('.container')).toHaveText('new container text')
336
- expect(document.title).toBe('Title from header')
337
396
 
338
- it "sets the document title to the response <title> with { history: false, title: true } options (bugfix)", ->
339
- affix('.container').text('old container text')
340
- up.replace('.container', '/path', history: false, title: true)
341
- @respondWith """
342
- <html>
343
- <head>
344
- <title>Title from HTML</title>
345
- </head>
346
- <body>
347
- <div class='container'>
348
- new container text
349
- </div>
350
- </body>
351
- </html>
352
- """
353
- expect($('.container')).toHaveText('new container text')
354
- expect(document.title).toBe('Title from HTML')
397
+ next =>
398
+ expect($('.container')).toHaveText('new container text')
399
+ expect(document.title).toBe('Title from HTML')
355
400
 
356
- it 'does not update the document title if the response has a <title> tag inside an inline SVG image (bugfix)', ->
401
+ it 'does not update the document title if the response has a <title> tag inside an inline SVG image (bugfix)', asyncSpec (next) ->
357
402
  affix('.container').text('old container text')
358
403
  document.title = 'old document title'
359
404
  up.replace('.container', '/path', history: false, title: true)
360
405
 
361
- @respondWith """
362
- <svg width="500" height="300" xmlns="http://www.w3.org/2000/svg">
363
- <g>
364
- <title>SVG Title Demo example</title>
365
- <rect x="10" y="10" width="200" height="50" style="fill:none; stroke:blue; stroke-width:1px"/>
366
- </g>
367
- </svg>
406
+ next =>
407
+ @respondWith """
408
+ <svg width="500" height="300" xmlns="http://www.w3.org/2000/svg">
409
+ <g>
410
+ <title>SVG Title Demo example</title>
411
+ <rect x="10" y="10" width="200" height="50" style="fill:none; stroke:blue; stroke-width:1px"/>
412
+ </g>
413
+ </svg>
368
414
 
369
- <div class='container'>
370
- new container text
371
- </div>
372
- """
373
- expect($('.container')).toHaveText('new container text')
374
- expect(document.title).toBe('old document title')
415
+ <div class='container'>
416
+ new container text
417
+ </div>
418
+ """
419
+
420
+ next =>
421
+ expect($('.container')).toHaveText('new container text')
422
+ expect(document.title).toBe('old document title')
375
423
 
376
- it "does not extract the title from the response or HTTP header if history isn't updated", ->
424
+ it "does not extract the title from the response or HTTP header if history isn't updated", asyncSpec (next) ->
377
425
  affix('.container').text('old container text')
378
426
  document.title = 'old document title'
379
427
  up.replace('.container', '/path', history: false)
380
- @respondWith
381
- responseHeaders:
382
- 'X-Up-Title': 'Title from header'
383
- responseText: """
384
- <html>
385
- <head>
386
- <title>Title from HTML</title>
387
- </head>
388
- <body>
389
- <div class='container'>
390
- new container text
391
- </div>
392
- </body>
393
- </html>
394
- """
395
- expect(document.title).toBe('old document title')
396
428
 
397
- it 'allows to pass an explicit title as { title } option', ->
429
+ next =>
430
+ @respondWith
431
+ responseHeaders:
432
+ 'X-Up-Title': 'Title from header'
433
+ responseText: """
434
+ <html>
435
+ <head>
436
+ <title>Title from HTML</title>
437
+ </head>
438
+ <body>
439
+ <div class='container'>
440
+ new container text
441
+ </div>
442
+ </body>
443
+ </html>
444
+ """
445
+
446
+ next =>
447
+ expect(document.title).toBe('old document title')
448
+
449
+ it 'allows to pass an explicit title as { title } option', asyncSpec (next) ->
398
450
  affix('.container').text('old container text')
399
451
  up.replace('.container', '/path', title: 'Title from options')
400
- @respondWith """
401
- <html>
402
- <head>
403
- <title>Title from HTML</title>
404
- </head>
405
- <body>
406
- <div class='container'>
407
- new container text
408
- </div>
409
- </body>
410
- </html>
411
- """
412
- expect($('.container')).toHaveText('new container text')
413
- expect(document.title).toBe('Title from options')
452
+
453
+ next =>
454
+ @respondWith """
455
+ <html>
456
+ <head>
457
+ <title>Title from HTML</title>
458
+ </head>
459
+ <body>
460
+ <div class='container'>
461
+ new container text
462
+ </div>
463
+ </body>
464
+ </html>
465
+ """
466
+
467
+ next =>
468
+ expect($('.container')).toHaveText('new container text')
469
+ expect(document.title).toBe('Title from options')
414
470
 
415
471
  describe 'selector processing', ->
416
472
 
@@ -475,167 +531,234 @@ describe 'up.dom', ->
475
531
  beforeEach ->
476
532
  up.dom.config.fallbacks = []
477
533
 
478
- it 'tries selectors from options.fallback before making a request', ->
534
+ it 'tries selectors from options.fallback before making a request', asyncSpec (next) ->
479
535
  affix('.box').text('old box')
480
536
  up.replace('.unknown', '/path', fallback: '.box')
481
- @respondWith '<div class="box">new box</div>'
482
- expect('.box').toHaveText('new box')
483
537
 
484
- it 'throws an error if all alternatives are exhausted', ->
485
- replacement = -> up.replace('.unknown', '/path', fallback: '.more-unknown')
486
- expect(replacement).toThrowError(/Could not find target in current page/i)
538
+ next => @respondWith '<div class="box">new box</div>'
539
+ next => expect('.box').toHaveText('new box')
540
+
541
+ it 'rejects the promise if all alternatives are exhausted', (done) ->
542
+ promise = up.replace('.unknown', '/path', fallback: '.more-unknown')
543
+ promise.catch (e) ->
544
+ expect(e).toBeError(/Could not find target in current page/i)
545
+ done()
487
546
 
488
- it 'considers a union selector to be missing if one of its selector-atoms are missing', ->
547
+ it 'considers a union selector to be missing if one of its selector-atoms are missing', asyncSpec (next) ->
489
548
  affix('.target').text('old target')
490
549
  affix('.fallback').text('old fallback')
491
550
  up.replace('.target, .unknown', '/path', fallback: '.fallback')
492
- @respondWith """
493
- <div class="target">new target</div>
494
- <div class="fallback">new fallback</div>
495
- """
496
- expect('.target').toHaveText('old target')
497
- expect('.fallback').toHaveText('new fallback')
498
551
 
499
- it 'tries a selector from up.dom.config.fallbacks if options.fallback is missing', ->
552
+ next =>
553
+ @respondWith """
554
+ <div class="target">new target</div>
555
+ <div class="fallback">new fallback</div>
556
+ """
557
+
558
+ next =>
559
+ expect('.target').toHaveText('old target')
560
+ expect('.fallback').toHaveText('new fallback')
561
+
562
+ it 'tries a selector from up.dom.config.fallbacks if options.fallback is missing', asyncSpec (next) ->
500
563
  up.dom.config.fallbacks = ['.existing']
501
564
  affix('.existing').text('old existing')
502
565
  up.replace('.unknown', '/path')
503
- @respondWith '<div class="existing">new existing</div>'
504
- expect('.existing').toHaveText('new existing')
566
+ next => @respondWith '<div class="existing">new existing</div>'
567
+ next => expect('.existing').toHaveText('new existing')
505
568
 
506
- it 'does not try a selector from up.dom.config.fallbacks if options.fallback is false', ->
569
+ it 'does not try a selector from up.dom.config.fallbacks and rejects the promise if options.fallback is false', (done) ->
507
570
  up.dom.config.fallbacks = ['.existing']
508
571
  affix('.existing').text('old existing')
509
- replacement = -> up.replace('.unknown', '/path', fallback: false)
510
- expect(replacement).toThrowError(/Could not find target in current page/i)
572
+ up.replace('.unknown', '/path', fallback: false).catch (e) ->
573
+ expect(e).toBeError(/Could not find target in current page/i)
574
+ done()
511
575
 
512
576
  describe 'when selectors are missing on the page after the request was made', ->
513
577
 
514
578
  beforeEach ->
515
579
  up.dom.config.fallbacks = []
516
580
 
517
- it 'tries selectors from options.fallback before swapping elements', ->
581
+ it 'tries selectors from options.fallback before swapping elements', asyncSpec (next) ->
518
582
  $target = affix('.target').text('old target')
519
583
  $fallback = affix('.fallback').text('old fallback')
520
584
  up.replace('.target', '/path', fallback: '.fallback')
521
585
  $target.remove()
522
- @respondWith """
523
- <div class="target">new target</div>
524
- <div class="fallback">new fallback</div>
525
- """
526
- expect('.fallback').toHaveText('new fallback')
527
586
 
528
- it 'throws an error if all alternatives are exhausted', ->
587
+ next =>
588
+ @respondWith """
589
+ <div class="target">new target</div>
590
+ <div class="fallback">new fallback</div>
591
+ """
592
+
593
+ next =>
594
+ expect('.fallback').toHaveText('new fallback')
595
+
596
+ it 'rejects the promise if all alternatives are exhausted', (done) ->
529
597
  $target = affix('.target').text('old target')
530
598
  $fallback = affix('.fallback').text('old fallback')
531
- up.replace('.target', '/path', fallback: '.fallback')
599
+ promise = up.replace('.target', '/path', fallback: '.fallback')
532
600
  $target.remove()
533
601
  $fallback.remove()
534
- respond = =>
602
+
603
+ u.nextFrame =>
535
604
  @respondWith """
536
605
  <div class="target">new target</div>
537
606
  <div class="fallback">new fallback</div>
538
607
  """
539
- expect(respond).toThrowError(/Could not find target in current page/i)
540
608
 
541
- it 'considers a union selector to be missing if one of its selector-atoms are missing', ->
609
+ u.nextFrame =>
610
+ promiseState(promise).then (result) ->
611
+ expect(result.state).toEqual('rejected')
612
+ expect(result.value).toBeError(/Could not find target in current page/i)
613
+ done()
614
+
615
+ it 'considers a union selector to be missing if one of its selector-atoms are missing', asyncSpec (next) ->
542
616
  $target = affix('.target').text('old target')
543
617
  $target2 = affix('.target2').text('old target2')
544
618
  $fallback = affix('.fallback').text('old fallback')
545
619
  up.replace('.target, .target2', '/path', fallback: '.fallback')
546
620
  $target2.remove()
547
- @respondWith """
548
- <div class="target">new target</div>
549
- <div class="target2">new target2</div>
550
- <div class="fallback">new fallback</div>
551
- """
552
- expect('.target').toHaveText('old target')
553
- expect('.fallback').toHaveText('new fallback')
554
621
 
555
- it 'tries a selector from up.dom.config.fallbacks if options.fallback is missing', ->
622
+ next =>
623
+ @respondWith """
624
+ <div class="target">new target</div>
625
+ <div class="target2">new target2</div>
626
+ <div class="fallback">new fallback</div>
627
+ """
628
+ next =>
629
+ expect('.target').toHaveText('old target')
630
+ expect('.fallback').toHaveText('new fallback')
631
+
632
+ it 'tries a selector from up.dom.config.fallbacks if options.fallback is missing', asyncSpec (next) ->
556
633
  up.dom.config.fallbacks = ['.fallback']
557
634
  $target = affix('.target').text('old target')
558
635
  $fallback = affix('.fallback').text('old fallback')
559
636
  up.replace('.target', '/path')
560
637
  $target.remove()
561
- @respondWith """
562
- <div class="target">new target</div>
563
- <div class="fallback">new fallback</div>
564
- """
565
- expect('.fallback').toHaveText('new fallback')
566
638
 
567
- it 'does not try a selector from up.dom.config.fallbacks if options.fallback is false', ->
639
+ next =>
640
+ @respondWith """
641
+ <div class="target">new target</div>
642
+ <div class="fallback">new fallback</div>
643
+ """
644
+
645
+ next =>
646
+ expect('.fallback').toHaveText('new fallback')
647
+
648
+ it 'does not try a selector from up.dom.config.fallbacks and rejects the promise if options.fallback is false', (done) ->
568
649
  up.dom.config.fallbacks = ['.fallback']
569
650
  $target = affix('.target').text('old target')
570
651
  $fallback = affix('.fallback').text('old fallback')
571
- up.replace('.target', '/path', fallback: false)
652
+ promise = up.replace('.target', '/path', fallback: false)
572
653
  $target.remove()
573
- respond = =>
654
+
655
+ u.nextFrame =>
574
656
  @respondWith """
575
657
  <div class="target">new target</div>
576
658
  <div class="fallback">new fallback</div>
577
659
  """
578
- expect(respond).toThrowError(/Could not find target in current page/i)
660
+
661
+ promise.catch (e) ->
662
+ expect(e).toBeError(/Could not find target in current page/i)
663
+ done()
579
664
 
580
665
  describe 'when selectors are missing in the response', ->
581
666
 
582
667
  beforeEach ->
583
668
  up.dom.config.fallbacks = []
584
669
 
585
- it 'tries selectors from options.fallback before swapping elements', ->
670
+ it 'tries selectors from options.fallback before swapping elements', asyncSpec (next) ->
586
671
  $target = affix('.target').text('old target')
587
672
  $fallback = affix('.fallback').text('old fallback')
588
673
  up.replace('.target', '/path', fallback: '.fallback')
589
- @respondWith """
590
- <div class="fallback">new fallback</div>
591
- """
592
- expect('.target').toHaveText('old target')
593
- expect('.fallback').toHaveText('new fallback')
594
674
 
595
- it 'throws an error if all alternatives are exhausted', ->
596
- $target = affix('.target').text('old target')
597
- $fallback = affix('.fallback').text('old fallback')
598
- up.replace('.target', '/path', fallback: '.fallback')
599
- respond = =>
675
+ next =>
600
676
  @respondWith """
601
- <div class="unexpected">new unexpected</div>
677
+ <div class="fallback">new fallback</div>
602
678
  """
603
- expect(respond).toThrowError(/Could not find target in response/i)
604
679
 
605
- it 'considers a union selector to be missing if one of its selector-atoms are missing', ->
680
+ next =>
681
+ expect('.target').toHaveText('old target')
682
+ expect('.fallback').toHaveText('new fallback')
683
+
684
+ describe 'if all alternatives are exhausted', ->
685
+
686
+ it 'rejects the promise', (done) ->
687
+ $target = affix('.target').text('old target')
688
+ $fallback = affix('.fallback').text('old fallback')
689
+ promise = up.replace('.target', '/path', fallback: '.fallback')
690
+
691
+ u.nextFrame =>
692
+ @respondWith '<div class="unexpected">new unexpected</div>'
693
+
694
+ promise.catch (e) ->
695
+ expect(e).toBeError(/Could not find target in response/i)
696
+ done()
697
+
698
+ it 'shows a link to open the unexpected response', (done) ->
699
+ $target = affix('.target').text('old target')
700
+ $fallback = affix('.fallback').text('old fallback')
701
+ promise = up.replace('.target', '/path', fallback: '.fallback')
702
+ navigate = spyOn(up.browser, 'navigate')
703
+
704
+ u.nextFrame =>
705
+ @respondWith '<div class="unexpected">new unexpected</div>'
706
+
707
+ promise.catch (e) ->
708
+ $toast = $('.up-toast')
709
+ expect($toast).toExist()
710
+ $inspectLink = $toast.find(".up-toast-action:contains('Open response')")
711
+ expect($inspectLink).toExist()
712
+ expect(navigate).not.toHaveBeenCalled()
713
+
714
+ Trigger.clickSequence($inspectLink)
715
+
716
+ u.nextFrame =>
717
+ expect(navigate).toHaveBeenCalledWith('/path', {})
718
+ done()
719
+
720
+ it 'considers a union selector to be missing if one of its selector-atoms are missing', asyncSpec (next) ->
606
721
  $target = affix('.target').text('old target')
607
722
  $target2 = affix('.target2').text('old target2')
608
723
  $fallback = affix('.fallback').text('old fallback')
609
724
  up.replace('.target, .target2', '/path', fallback: '.fallback')
610
- @respondWith """
611
- <div class="target">new target</div>
612
- <div class="fallback">new fallback</div>
613
- """
614
- expect('.target').toHaveText('old target')
615
- expect('.target2').toHaveText('old target2')
616
- expect('.fallback').toHaveText('new fallback')
617
725
 
618
- it 'tries a selector from up.dom.config.fallbacks if options.fallback is missing', ->
726
+ next =>
727
+ @respondWith """
728
+ <div class="target">new target</div>
729
+ <div class="fallback">new fallback</div>
730
+ """
731
+
732
+ next =>
733
+ expect('.target').toHaveText('old target')
734
+ expect('.target2').toHaveText('old target2')
735
+ expect('.fallback').toHaveText('new fallback')
736
+
737
+ it 'tries a selector from up.dom.config.fallbacks if options.fallback is missing', asyncSpec (next) ->
619
738
  up.dom.config.fallbacks = ['.fallback']
620
739
  $target = affix('.target').text('old target')
621
740
  $fallback = affix('.fallback').text('old fallback')
622
741
  up.replace('.target', '/path')
623
- @respondWith """
624
- <div class="fallback">new fallback</div>
625
- """
626
- expect('.target').toHaveText('old target')
627
- expect('.fallback').toHaveText('new fallback')
628
742
 
629
- it 'does not try a selector from up.dom.config.fallbacks if options.fallback is false', ->
743
+ next =>
744
+ @respondWith '<div class="fallback">new fallback</div>'
745
+
746
+ next =>
747
+ expect('.target').toHaveText('old target')
748
+ expect('.fallback').toHaveText('new fallback')
749
+
750
+ it 'does not try a selector from up.dom.config.fallbacks and rejects the promise if options.fallback is false', (done) ->
630
751
  up.dom.config.fallbacks = ['.fallback']
631
752
  $target = affix('.target').text('old target')
632
753
  $fallback = affix('.fallback').text('old fallback')
633
- up.replace('.target', '/path', fallback: false)
634
- respond = =>
635
- @respondWith """
636
- <div class="fallback">new fallback</div>
637
- """
638
- expect(respond).toThrowError(/Could not find target in response/i)
754
+ promise = up.replace('.target', '/path', fallback: false)
755
+
756
+ u.nextFrame =>
757
+ @respondWith '<div class="fallback">new fallback</div>'
758
+
759
+ promise.catch (e) ->
760
+ expect(e).toBeError(/Could not find target in response/i)
761
+ done()
639
762
 
640
763
  describe 'execution of script tags', ->
641
764
 
@@ -751,7 +874,10 @@ describe 'up.dom', ->
751
874
 
752
875
  describe 'with { restoreScroll: true } option', ->
753
876
 
754
- it 'restores the scroll positions of all viewports around the target', ->
877
+ beforeEach ->
878
+ up.history.config.enabled = true
879
+
880
+ it 'restores the scroll positions of all viewports around the target', asyncSpec (next) ->
755
881
 
756
882
  $viewport = affix('div[up-viewport] .element').css
757
883
  'height': '100px'
@@ -765,19 +891,15 @@ describe 'up.dom', ->
765
891
  responseText: '<div class="element" style="height: 300px"></div>'
766
892
 
767
893
  up.replace('.element', '/foo')
768
- respond()
769
-
770
- $viewport.scrollTop(65)
771
894
 
772
- up.replace('.element', '/bar')
773
- respond()
774
-
775
- $viewport.scrollTop(0)
776
-
777
- up.replace('.element', '/foo', restoreScroll: true)
895
+ next => respond()
896
+ next => $viewport.scrollTop(65)
897
+ next => up.replace('.element', '/bar')
898
+ next => respond()
899
+ next => $viewport.scrollTop(0)
900
+ next.await => up.replace('.element', '/foo', restoreScroll: true)
778
901
  # No need to respond because /foo has been cached before
779
-
780
- expect($viewport.scrollTop()).toEqual(65)
902
+ next => expect($viewport.scrollTop()).toEqual(65)
781
903
 
782
904
 
783
905
  describe 'with { reveal: true } option', ->
@@ -791,30 +913,34 @@ describe 'up.dom', ->
791
913
  @revealedHTML.push $element.get(0).outerHTML
792
914
  @revealedText.push $element.text().trim()
793
915
  @revealOptions = options
794
- u.resolvedDeferred()
916
+ Promise.resolve()
795
917
 
796
- it 'reveals a new element before it is being replaced', (done) ->
797
- promise = up.replace('.middle', '/path', reveal: true)
798
- @respond()
799
- promise.then =>
918
+ it 'reveals a new element before it is being replaced', asyncSpec (next) ->
919
+ up.replace('.middle', '/path', reveal: true)
920
+
921
+ next =>
922
+ @respond()
923
+
924
+ next =>
800
925
  expect(@revealMock).not.toHaveBeenCalledWith(@oldMiddle)
801
926
  expect(@revealedText).toEqual ['new-middle']
802
- done()
803
927
 
804
928
  describe 'when more than one fragment is replaced', ->
805
929
 
806
- it 'only reveals the first fragment', (done) ->
807
- promise = up.replace('.middle, .after', '/path', reveal: true)
808
- @respond()
809
- promise.then =>
930
+ it 'only reveals the first fragment', asyncSpec (next) ->
931
+ up.replace('.middle, .after', '/path', reveal: true)
932
+
933
+ next =>
934
+ @respond()
935
+
936
+ next =>
810
937
  expect(@revealMock).not.toHaveBeenCalledWith(@oldMiddle)
811
938
  expect(@revealedText).toEqual ['new-middle']
812
- done()
813
939
 
814
940
  describe 'when there is an anchor #hash in the URL', ->
815
941
 
816
- it 'scrolls to the top of a child with the ID of that #hash', (done) ->
817
- promise = up.replace('.middle', '/path#three', reveal: true)
942
+ it 'scrolls to the top of a child with the ID of that #hash', asyncSpec (next) ->
943
+ up.replace('.middle', '/path#three', reveal: true)
818
944
  @responseText =
819
945
  """
820
946
  <div class="middle">
@@ -823,24 +949,28 @@ describe 'up.dom', ->
823
949
  <div id="three">three</div>
824
950
  </div>
825
951
  """
826
- @respond()
827
- promise.then =>
952
+
953
+ next =>
954
+ @respond()
955
+
956
+ next =>
828
957
  expect(@revealedHTML).toEqual ['<div id="three">three</div>']
829
958
  expect(@revealOptions).toEqual { top: true }
830
- done()
831
959
 
832
- it "reveals the entire element if it has no child with the ID of that #hash", (done) ->
833
- promise = up.replace('.middle', '/path#four', reveal: true)
834
- @responseText =
835
- """
836
- <div class="middle">
837
- new-middle
838
- </div>
839
- """
840
- @respond()
841
- promise.then =>
960
+ it "reveals the entire element if it has no child with the ID of that #hash", asyncSpec (next) ->
961
+ up.replace('.middle', '/path#four', reveal: true)
962
+
963
+ next =>
964
+ @responseText =
965
+ """
966
+ <div class="middle">
967
+ new-middle
968
+ </div>
969
+ """
970
+ @respond()
971
+
972
+ next =>
842
973
  expect(@revealedText).toEqual ['new-middle']
843
- done()
844
974
 
845
975
  it 'reveals a new element that is being appended', (done) ->
846
976
  promise = up.replace('.middle:after', '/path', reveal: true)
@@ -871,15 +1001,17 @@ describe 'up.dom', ->
871
1001
  it 'uses a { failTransition } option if the request failed'
872
1002
 
873
1003
  describeFallback 'canPushState', ->
874
-
875
- it 'makes a full page load', ->
876
- spyOn(up.browser, 'loadPage')
1004
+
1005
+ it 'makes a full page load', asyncSpec (next) ->
1006
+ spyOn(up.browser, 'navigate')
877
1007
  up.replace('.selector', '/path')
878
- expect(up.browser.loadPage).toHaveBeenCalledWith('/path', jasmine.anything())
879
-
1008
+
1009
+ next =>
1010
+ expect(up.browser.navigate).toHaveBeenCalledWith('/path', jasmine.anything())
1011
+
880
1012
  describe 'up.extract', ->
881
-
882
- it 'Updates a selector on the current page with the same selector from the given HTML string', ->
1013
+
1014
+ it 'Updates a selector on the current page with the same selector from the given HTML string', asyncSpec (next) ->
883
1015
 
884
1016
  affix('.before').text('old-before')
885
1017
  affix('.middle').text('old-middle')
@@ -894,116 +1026,141 @@ describe 'up.dom', ->
894
1026
 
895
1027
  up.extract('.middle', html)
896
1028
 
897
- expect($('.before')).toHaveText('old-before')
898
- expect($('.middle')).toHaveText('new-middle')
899
- expect($('.after')).toHaveText('old-after')
1029
+ next ->
1030
+ expect($('.before')).toHaveText('old-before')
1031
+ expect($('.middle')).toHaveText('new-middle')
1032
+ expect($('.after')).toHaveText('old-after')
900
1033
 
901
- it "throws an error if the selector can't be found on the current page", ->
1034
+ it "throws an error if the selector can't be found on the current page", (done) ->
902
1035
  html = '<div class="foo-bar">text</div>'
903
- extract = -> up.extract('.foo-bar', html)
904
- expect(extract).toThrowError(/Could not find selector in current page, modal or popup/i)
1036
+ promise = up.extract('.foo-bar', html)
1037
+ promiseState(promise).then (result) =>
1038
+ expect(result.state).toEqual('rejected')
1039
+ expect(result.value).toMatch(/Could not find selector in current page, modal or popup/i)
1040
+ done()
905
1041
 
906
- it "throws an error if the selector can't be found in the given HTML string", ->
1042
+ it "throws an error if the selector can't be found in the given HTML string", (done) ->
907
1043
  affix('.foo-bar')
908
- extract = -> up.extract('.foo-bar', '')
909
- expect(extract).toThrowError(/Could not find selector in response/i)
1044
+ promise = up.extract('.foo-bar', '')
1045
+ promiseState(promise).then (result) =>
1046
+ expect(result.state).toEqual('rejected')
1047
+ expect(result.value).toMatch(/Could not find selector in response/i)
1048
+ done()
910
1049
 
911
- it "ignores an element that matches the selector but also matches .up-destroying", ->
1050
+ it "ignores an element that matches the selector but also matches .up-destroying", (done) ->
912
1051
  html = '<div class="foo-bar">text</div>'
913
1052
  affix('.foo-bar.up-destroying')
914
- extract = -> up.extract('.foo-bar', html)
915
- expect(extract).toThrowError(/Could not find selector/i)
1053
+ promise = up.extract('.foo-bar', html)
1054
+ promiseState(promise).then (result) =>
1055
+ expect(result.state).toEqual('rejected')
1056
+ expect(result.value).toMatch(/Could not find selector/i)
1057
+ done()
916
1058
 
917
- it "ignores an element that matches the selector but also matches .up-ghost", ->
1059
+ it "ignores an element that matches the selector but also matches .up-ghost", (done) ->
918
1060
  html = '<div class="foo-bar">text</div>'
919
1061
  affix('.foo-bar.up-ghost')
920
- extract = -> up.extract('.foo-bar', html)
921
- expect(extract).toThrowError(/Could not find selector/i)
1062
+ promise = up.extract('.foo-bar', html)
1063
+ promiseState(promise).then (result) =>
1064
+ expect(result.state).toEqual('rejected')
1065
+ expect(result.value).toMatch(/Could not find selector/i)
1066
+ done()
922
1067
 
923
- it "ignores an element that matches the selector but also has a parent matching .up-destroying", ->
1068
+ it "ignores an element that matches the selector but also has a parent matching .up-destroying", (done) ->
924
1069
  html = '<div class="foo-bar">text</div>'
925
1070
  $parent = affix('.up-destroying')
926
1071
  $child = affix('.foo-bar').appendTo($parent)
927
- extract = -> up.extract('.foo-bar', html)
928
- expect(extract).toThrowError(/Could not find selector/i)
1072
+ promise = up.extract('.foo-bar', html)
1073
+ promiseState(promise).then (result) =>
1074
+ expect(result.state).toEqual('rejected')
1075
+ expect(result.value).toMatch(/Could not find selector/i)
1076
+ done()
929
1077
 
930
- it "ignores an element that matches the selector but also has a parent matching .up-ghost", ->
1078
+ it "ignores an element that matches the selector but also has a parent matching .up-ghost", (done) ->
931
1079
  html = '<div class="foo-bar">text</div>'
932
1080
  $parent = affix('.up-ghost')
933
1081
  $child = affix('.foo-bar').appendTo($parent)
934
- extract = -> up.extract('.foo-bar', html)
935
- expect(extract).toThrowError(/Could not find selector/i)
1082
+ promise = up.extract('.foo-bar', html)
1083
+ promiseState(promise).then (result) =>
1084
+ expect(result.state).toEqual('rejected')
1085
+ expect(result.value).toMatch(/Could not find selector/i)
1086
+ done()
936
1087
 
937
- it 'only replaces the first element matching the selector', ->
1088
+ it 'only replaces the first element matching the selector', asyncSpec (next) ->
938
1089
  html = '<div class="foo-bar">text</div>'
939
1090
  affix('.foo-bar')
940
1091
  affix('.foo-bar')
941
1092
  up.extract('.foo-bar', html)
942
- elements = $('.foo-bar')
943
- expect($(elements.get(0)).text()).toEqual('text')
944
- expect($(elements.get(1)).text()).toEqual('')
1093
+
1094
+ next =>
1095
+ elements = $('.foo-bar')
1096
+ expect($(elements.get(0)).text()).toEqual('text')
1097
+ expect($(elements.get(1)).text()).toEqual('')
945
1098
 
946
1099
  describe 'with { transition } option', ->
947
1100
 
948
- it 'morphs between the old and new element', (done) ->
1101
+ it 'morphs between the old and new element', asyncSpec (next) ->
949
1102
  affix('.element').text('version 1')
950
1103
  up.extract('.element', '<div class="element">version 2</div>', transition: 'cross-fade', duration: 200)
951
1104
 
952
- $ghost1 = $('.element.up-ghost:contains("version 1")')
953
- expect($ghost1).toHaveLength(1)
954
- expect(u.opacity($ghost1)).toBeAround(1.0, 0.1)
1105
+ next =>
1106
+ @$ghost1 = $('.element.up-ghost:contains("version 1")')
1107
+ expect(@$ghost1).toHaveLength(1)
1108
+ expect(u.opacity(@$ghost1)).toBeAround(1.0, 0.1)
955
1109
 
956
- $ghost2 = $('.element.up-ghost:contains("version 2")')
957
- expect($ghost2).toHaveLength(1)
958
- expect(u.opacity($ghost2)).toBeAround(0.0, 0.1)
1110
+ @$ghost2 = $('.element.up-ghost:contains("version 2")')
1111
+ expect(@$ghost2).toHaveLength(1)
1112
+ expect(u.opacity(@$ghost2)).toBeAround(0.0, 0.1)
959
1113
 
960
- u.setTimer 190, ->
961
- expect(u.opacity($ghost1)).toBeAround(0.0, 0.3)
962
- expect(u.opacity($ghost2)).toBeAround(1.0, 0.3)
963
- done()
1114
+ next.after 190, =>
1115
+ expect(u.opacity(@$ghost1)).toBeAround(0.0, 0.3)
1116
+ expect(u.opacity(@$ghost2)).toBeAround(1.0, 0.3)
964
1117
 
965
- it 'marks the old fragment and its ghost as .up-destroying during the transition', ->
1118
+ it 'marks the old fragment and its ghost as .up-destroying during the transition', asyncSpec (next) ->
966
1119
  affix('.element').text('version 1')
967
1120
  up.extract('.element', '<div class="element">version 2</div>', transition: 'cross-fade', duration: 200)
968
1121
 
969
- $version1 = $('.element:not(.up-ghost):contains("version 1")')
970
- $version1Ghost = $('.element.up-ghost:contains("version 1")')
971
- expect($version1).toHaveLength(1)
972
- expect($version1Ghost).toHaveLength(1)
973
- expect($version1).toHaveClass('up-destroying')
974
- expect($version1Ghost).toHaveClass('up-destroying')
975
-
976
- $version2 = $('.element:not(.up-ghost):contains("version 2")')
977
- $version2Ghost = $('.element.up-ghost:contains("version 2")')
978
- expect($version2).toHaveLength(1)
979
- expect($version2Ghost).toHaveLength(1)
980
- expect($version2).not.toHaveClass('up-destroying')
981
- expect($version2Ghost).not.toHaveClass('up-destroying')
982
-
983
- it 'cancels an existing transition by instantly jumping to the last frame', ->
1122
+ next =>
1123
+ $version1 = $('.element:not(.up-ghost):contains("version 1")')
1124
+ $version1Ghost = $('.element.up-ghost:contains("version 1")')
1125
+ expect($version1).toHaveLength(1)
1126
+ expect($version1Ghost).toHaveLength(1)
1127
+ expect($version1).toHaveClass('up-destroying')
1128
+ expect($version1Ghost).toHaveClass('up-destroying')
1129
+
1130
+ $version2 = $('.element:not(.up-ghost):contains("version 2")')
1131
+ $version2Ghost = $('.element.up-ghost:contains("version 2")')
1132
+ expect($version2).toHaveLength(1)
1133
+ expect($version2Ghost).toHaveLength(1)
1134
+ expect($version2).not.toHaveClass('up-destroying')
1135
+ expect($version2Ghost).not.toHaveClass('up-destroying')
1136
+
1137
+ it 'cancels an existing transition by instantly jumping to the last frame', asyncSpec (next) ->
984
1138
  affix('.element').text('version 1')
985
1139
  up.extract('.element', '<div class="element">version 2</div>', transition: 'cross-fade', duration: 200)
986
1140
 
987
- $ghost1 = $('.element.up-ghost:contains("version 1")')
988
- expect($ghost1).toHaveLength(1)
989
- expect($ghost1.css('opacity')).toBeAround(1.0, 0.1)
1141
+ next =>
1142
+ $ghost1 = $('.element.up-ghost:contains("version 1")')
1143
+ expect($ghost1).toHaveLength(1)
1144
+ expect($ghost1.css('opacity')).toBeAround(1.0, 0.1)
990
1145
 
991
- $ghost2 = $('.element.up-ghost:contains("version 2")')
992
- expect($ghost2).toHaveLength(1)
993
- expect($ghost2.css('opacity')).toBeAround(0.0, 0.1)
1146
+ $ghost2 = $('.element.up-ghost:contains("version 2")')
1147
+ expect($ghost2).toHaveLength(1)
1148
+ expect($ghost2.css('opacity')).toBeAround(0.0, 0.1)
994
1149
 
995
- up.extract('.element', '<div class="element">version 3</div>', transition: 'cross-fade', duration: 200)
1150
+ next =>
1151
+ up.extract('.element', '<div class="element">version 3</div>', transition: 'cross-fade', duration: 200)
996
1152
 
997
- $ghost1 = $('.element.up-ghost:contains("version 1")')
998
- expect($ghost1).toHaveLength(0)
1153
+ next =>
1154
+ $ghost1 = $('.element.up-ghost:contains("version 1")')
1155
+ expect($ghost1).toHaveLength(0)
999
1156
 
1000
- $ghost2 = $('.element.up-ghost:contains("version 2")')
1001
- expect($ghost2).toHaveLength(1)
1002
- expect($ghost2.css('opacity')).toBeAround(1.0, 0.1)
1157
+ $ghost2 = $('.element.up-ghost:contains("version 2")')
1158
+ expect($ghost2).toHaveLength(1)
1159
+ expect($ghost2.css('opacity')).toBeAround(1.0, 0.1)
1003
1160
 
1004
- $ghost3 = $('.element.up-ghost:contains("version 3")')
1005
- expect($ghost3).toHaveLength(1)
1006
- expect($ghost3.css('opacity')).toBeAround(0.0, 0.1)
1161
+ $ghost3 = $('.element.up-ghost:contains("version 3")')
1162
+ expect($ghost3).toHaveLength(1)
1163
+ expect($ghost3.css('opacity')).toBeAround(0.0, 0.1)
1007
1164
 
1008
1165
  it 'delays the resolution of the returned promise until the transition is over', (done) ->
1009
1166
  affix('.element').text('version 1')
@@ -1020,11 +1177,12 @@ describe 'up.dom', ->
1020
1177
  beforeEach ->
1021
1178
  up.motion.config.enabled = false
1022
1179
 
1023
- it 'immediately swaps the old and new elements', ->
1180
+ it 'immediately swaps the old and new elements without creating unnecessary ghosts', asyncSpec (next) ->
1024
1181
  affix('.element').text('version 1')
1025
1182
  up.extract('.element', '<div class="element">version 2</div>', transition: 'cross-fade', duration: 200)
1026
- expect($('.element')).toHaveText('version 2')
1027
- expect($('.up-ghost')).toHaveLength(0)
1183
+ next =>
1184
+ expect($('.element')).toHaveText('version 2')
1185
+ expect($('.up-ghost')).toHaveLength(0)
1028
1186
 
1029
1187
  describe 'handling of [up-keep] elements', ->
1030
1188
 
@@ -1039,11 +1197,12 @@ describe 'up.dom', ->
1039
1197
  # Need to refactor this spec file so examples don't all share one example
1040
1198
  $('.before, .middle, .after').remove()
1041
1199
 
1042
- it 'keeps an [up-keep] element, but does replace other elements around it', ->
1200
+ it 'keeps an [up-keep] element, but does replace other elements around it', asyncSpec (next) ->
1043
1201
  $container = affix('.container')
1044
1202
  $container.affix('.before').text('old-before')
1045
1203
  $container.affix('.middle[up-keep]').text('old-middle')
1046
1204
  $container.affix('.after').text('old-after')
1205
+
1047
1206
  up.extract '.container', """
1048
1207
  <div class='container'>
1049
1208
  <div class='before'>new-before</div>
@@ -1051,17 +1210,20 @@ describe 'up.dom', ->
1051
1210
  <div class='after'>new-after</div>
1052
1211
  </div>
1053
1212
  """
1054
- expect($('.before')).toHaveText('new-before')
1055
- expect($('.middle')).toHaveText('old-middle')
1056
- expect($('.after')).toHaveText('new-after')
1057
1213
 
1058
- it 'keeps an [up-keep] element, but does replace text nodes around it', ->
1214
+ next =>
1215
+ expect($('.before')).toHaveText('new-before')
1216
+ expect($('.middle')).toHaveText('old-middle')
1217
+ expect($('.after')).toHaveText('new-after')
1218
+
1219
+ it 'keeps an [up-keep] element, but does replace text nodes around it', asyncSpec (next) ->
1059
1220
  $container = affix('.container')
1060
1221
  $container.html """
1061
1222
  old-before
1062
1223
  <div class='element' up-keep>old-inside</div>
1063
1224
  old-after
1064
1225
  """
1226
+
1065
1227
  up.extract '.container', """
1066
1228
  <div class='container'>
1067
1229
  new-before
@@ -1069,16 +1231,20 @@ describe 'up.dom', ->
1069
1231
  new-after
1070
1232
  </div>
1071
1233
  """
1072
- expect(squish($('.container').text())).toEqual('new-before old-inside new-after')
1234
+
1235
+ next =>
1236
+ expect(squish($('.container').text())).toEqual('new-before old-inside new-after')
1073
1237
 
1074
1238
  describe 'if an [up-keep] element is itself a direct replacement target', ->
1075
1239
 
1076
- it "keeps that element", ->
1240
+ it "keeps that element", asyncSpec (next) ->
1077
1241
  affix('.keeper[up-keep]').text('old-inside')
1078
1242
  up.extract '.keeper', "<div class='keeper' up-keep>new-inside</div>"
1079
- expect($('.keeper')).toHaveText('old-inside')
1080
1243
 
1081
- it "only emits an event up:fragment:kept, but not an event up:fragment:inserted", ->
1244
+ next =>
1245
+ expect($('.keeper')).toHaveText('old-inside')
1246
+
1247
+ it "only emits an event up:fragment:kept, but not an event up:fragment:inserted", asyncSpec (next) ->
1082
1248
  insertedListener = jasmine.createSpy('subscriber to up:fragment:inserted')
1083
1249
  up.on('up:fragment:inserted', insertedListener)
1084
1250
  keptListener = jasmine.createSpy('subscriber to up:fragment:kept')
@@ -1086,10 +1252,12 @@ describe 'up.dom', ->
1086
1252
  up.on 'up:fragment:inserted', insertedListener
1087
1253
  $keeper = affix('.keeper[up-keep]').text('old-inside')
1088
1254
  up.extract '.keeper', "<div class='keeper' up-keep>new-inside</div>"
1089
- expect(insertedListener).not.toHaveBeenCalled()
1090
- expect(keptListener).toHaveBeenCalledWith(jasmine.anything(), $('.keeper'), jasmine.anything())
1091
1255
 
1092
- it "removes an [up-keep] element if no matching element is found in the response", ->
1256
+ next =>
1257
+ expect(insertedListener).not.toHaveBeenCalled()
1258
+ expect(keptListener).toHaveBeenCalledWith(jasmine.anything(), $('.keeper'), jasmine.anything())
1259
+
1260
+ it "removes an [up-keep] element if no matching element is found in the response", asyncSpec (next) ->
1093
1261
  barCompiler = jasmine.createSpy()
1094
1262
  barDestructor = jasmine.createSpy()
1095
1263
  up.compiler '.bar', ($bar) ->
@@ -1113,13 +1281,14 @@ describe 'up.dom', ->
1113
1281
  </div>
1114
1282
  """
1115
1283
 
1116
- expect($('.container .foo')).toExist()
1117
- expect($('.container .bar')).not.toExist()
1284
+ next =>
1285
+ expect($('.container .foo')).toExist()
1286
+ expect($('.container .bar')).not.toExist()
1118
1287
 
1119
- expect(barCompiler.calls.allArgs()).toEqual [['old-bar']]
1120
- expect(barDestructor.calls.allArgs()).toEqual [['old-bar']]
1288
+ expect(barCompiler.calls.allArgs()).toEqual [['old-bar']]
1289
+ expect(barDestructor.calls.allArgs()).toEqual [['old-bar']]
1121
1290
 
1122
- it "updates an element if a matching element is found in the response, but that other element is no longer [up-keep]", ->
1291
+ it "updates an element if a matching element is found in the response, but that other element is no longer [up-keep]", asyncSpec (next) ->
1123
1292
  barCompiler = jasmine.createSpy()
1124
1293
  barDestructor = jasmine.createSpy()
1125
1294
  up.compiler '.bar', ($bar) ->
@@ -1145,13 +1314,14 @@ describe 'up.dom', ->
1145
1314
  </div>
1146
1315
  """
1147
1316
 
1148
- expect($('.container .foo')).toHaveText('new-foo')
1149
- expect($('.container .bar')).toHaveText('new-bar')
1317
+ next =>
1318
+ expect($('.container .foo')).toHaveText('new-foo')
1319
+ expect($('.container .bar')).toHaveText('new-bar')
1150
1320
 
1151
- expect(barCompiler.calls.allArgs()).toEqual [['old-bar'], ['new-bar']]
1152
- expect(barDestructor.calls.allArgs()).toEqual [['old-bar']]
1321
+ expect(barCompiler.calls.allArgs()).toEqual [['old-bar'], ['new-bar']]
1322
+ expect(barDestructor.calls.allArgs()).toEqual [['old-bar']]
1153
1323
 
1154
- it 'moves a kept element to the ancestry position of the matching element in the response', ->
1324
+ it 'moves a kept element to the ancestry position of the matching element in the response', asyncSpec (next) ->
1155
1325
  $container = affix('.container')
1156
1326
  $container.html """
1157
1327
  <div class="parent1">
@@ -1169,10 +1339,12 @@ describe 'up.dom', ->
1169
1339
  </div>
1170
1340
  </div>
1171
1341
  """
1172
- expect($('.keeper')).toHaveText('old-inside')
1173
- expect($('.keeper').parent()).toEqual($('.parent2'))
1174
1342
 
1175
- it 'lets developers choose a selector to match against as the value of the up-keep attribute', ->
1343
+ next =>
1344
+ expect($('.keeper')).toHaveText('old-inside')
1345
+ expect($('.keeper').parent()).toEqual($('.parent2'))
1346
+
1347
+ it 'lets developers choose a selector to match against as the value of the up-keep attribute', asyncSpec (next) ->
1176
1348
  $container = affix('.container')
1177
1349
  $container.html """
1178
1350
  <div class="keeper" up-keep=".stayer"></div>
@@ -1182,9 +1354,11 @@ describe 'up.dom', ->
1182
1354
  <div up-keep class="stayer"></div>
1183
1355
  </div>
1184
1356
  """
1185
- expect('.keeper').toExist()
1186
1357
 
1187
- it 'does not compile a kept element a second time', ->
1358
+ next =>
1359
+ expect('.keeper').toExist()
1360
+
1361
+ it 'does not compile a kept element a second time', asyncSpec (next) ->
1188
1362
  compiler = jasmine.createSpy('compiler')
1189
1363
  up.compiler('.keeper', compiler)
1190
1364
  $container = affix('.container')
@@ -1200,10 +1374,12 @@ describe 'up.dom', ->
1200
1374
  <div class="keeper" up-keep>new-text</div>
1201
1375
  </div>
1202
1376
  """
1203
- expect(compiler.calls.count()).toEqual(1)
1204
- expect('.keeper').toExist()
1205
1377
 
1206
- it 'does not lose jQuery event handlers on a kept element (bugfix)', ->
1378
+ next =>
1379
+ expect(compiler.calls.count()).toEqual(1)
1380
+ expect('.keeper').toExist()
1381
+
1382
+ it 'does not lose jQuery event handlers on a kept element (bugfix)', asyncSpec (next) ->
1207
1383
  handler = jasmine.createSpy('event handler')
1208
1384
  up.compiler '.keeper', ($keeper) ->
1209
1385
  $keeper.on 'click', handler
@@ -1220,13 +1396,16 @@ describe 'up.dom', ->
1220
1396
  </div>
1221
1397
  """
1222
1398
 
1223
- $keeper = $('.keeper')
1224
- expect($keeper).toHaveText('old-text')
1225
- Trigger.click($keeper)
1226
- expect(handler).toHaveBeenCalled()
1399
+ next =>
1400
+ expect('.keeper').toHaveText('old-text')
1227
1401
 
1228
- it 'does not call destructors on a kept alement', ->
1229
- handler = jasmine.createSpy('event handler')
1402
+ next =>
1403
+ Trigger.click('.keeper')
1404
+
1405
+ next =>
1406
+ expect(handler).toHaveBeenCalled()
1407
+
1408
+ it 'does not call destructors on a kept alement', asyncSpec (next) ->
1230
1409
  destructor = jasmine.createSpy('destructor')
1231
1410
  up.compiler '.keeper', ($keeper) ->
1232
1411
  return destructor
@@ -1243,11 +1422,12 @@ describe 'up.dom', ->
1243
1422
  </div>
1244
1423
  """
1245
1424
 
1246
- $keeper = $('.keeper')
1247
- expect($keeper).toHaveText('old-text')
1248
- expect(destructor).not.toHaveBeenCalled()
1425
+ next =>
1426
+ $keeper = $('.keeper')
1427
+ expect($keeper).toHaveText('old-text')
1428
+ expect(destructor).not.toHaveBeenCalled()
1249
1429
 
1250
- it 'calls destructors when a kept element is eventually removed from the DOM', ->
1430
+ it 'calls destructors when a kept element is eventually removed from the DOM', asyncSpec (next) ->
1251
1431
  handler = jasmine.createSpy('event handler')
1252
1432
  destructor = jasmine.createSpy('destructor')
1253
1433
  up.compiler '.keeper', ($keeper) ->
@@ -1265,26 +1445,28 @@ describe 'up.dom', ->
1265
1445
  </div>
1266
1446
  """
1267
1447
 
1268
- $keeper = $('.keeper')
1269
- expect($keeper).toHaveText('new-text')
1270
- expect(destructor).toHaveBeenCalled()
1448
+ next =>
1449
+ $keeper = $('.keeper')
1450
+ expect($keeper).toHaveText('new-text')
1451
+ expect(destructor).toHaveBeenCalled()
1271
1452
 
1272
- it 'lets listeners cancel the keeping by preventing default on an up:fragment:keep event', ->
1453
+ it 'lets listeners cancel the keeping by preventing default on an up:fragment:keep event', asyncSpec (next) ->
1273
1454
  $keeper = affix('.keeper[up-keep]').text('old-inside')
1274
1455
  $keeper.on 'up:fragment:keep', (event) -> event.preventDefault()
1275
1456
  up.extract '.keeper', "<div class='keeper' up-keep>new-inside</div>"
1276
- expect($('.keeper')).toHaveText('new-inside')
1457
+ next => expect($('.keeper')).toHaveText('new-inside')
1277
1458
 
1278
- it 'lets listeners prevent up:fragment:keep event if the element was kept before (bugfix)', ->
1459
+ it 'lets listeners prevent up:fragment:keep event if the element was kept before (bugfix)', asyncSpec (next) ->
1279
1460
  $keeper = affix('.keeper[up-keep]').text('version 1')
1280
1461
  $keeper.on 'up:fragment:keep', (event) ->
1281
1462
  event.preventDefault() if event.$newElement.text() == 'version 3'
1282
- up.extract '.keeper', "<div class='keeper' up-keep>version 2</div>"
1283
- expect($('.keeper')).toHaveText('version 1')
1284
- up.extract '.keeper', "<div class='keeper' up-keep>version 3</div>"
1285
- expect($('.keeper')).toHaveText('version 3')
1286
1463
 
1287
- it 'emits an up:fragment:kept event on a kept element and up:fragment:inserted on an updated parent', ->
1464
+ next => up.extract '.keeper', "<div class='keeper' up-keep>version 2</div>"
1465
+ next => expect($('.keeper')).toHaveText('version 1')
1466
+ next => up.extract '.keeper', "<div class='keeper' up-keep>version 3</div>"
1467
+ next => expect($('.keeper')).toHaveText('version 3')
1468
+
1469
+ it 'emits an up:fragment:kept event on a kept element and up:fragment:inserted on an updated parent', asyncSpec (next) ->
1288
1470
  insertedListener = jasmine.createSpy()
1289
1471
  up.on('up:fragment:inserted', insertedListener)
1290
1472
  keptListener = jasmine.createSpy()
@@ -1294,28 +1476,34 @@ describe 'up.dom', ->
1294
1476
  $container.html """
1295
1477
  <div class="keeper" up-keep></div>
1296
1478
  """
1479
+
1297
1480
  up.extract '.container', """
1298
1481
  <div class='container'>
1299
1482
  <div class="keeper" up-keep></div>
1300
1483
  </div>
1301
1484
  """
1302
- expect(insertedListener).toHaveBeenCalledWith(jasmine.anything(), $('.container'), jasmine.anything())
1303
- expect(keptListener).toHaveBeenCalledWith(jasmine.anything(), $('.container .keeper'), jasmine.anything())
1304
1485
 
1305
- it 'emits an up:fragment:kept event on a kept element with a newData property corresponding to the up-data attribute value of the discarded element', ->
1486
+ next =>
1487
+ expect(insertedListener).toHaveBeenCalledWith(jasmine.anything(), $('.container'), jasmine.anything())
1488
+ expect(keptListener).toHaveBeenCalledWith(jasmine.anything(), $('.container .keeper'), jasmine.anything())
1489
+
1490
+ it 'emits an up:fragment:kept event on a kept element with a newData property corresponding to the up-data attribute value of the discarded element', (next) ->
1306
1491
  keptListener = jasmine.createSpy()
1307
1492
  up.on 'up:fragment:kept', (event) -> keptListener(event.$element, event.newData)
1308
1493
  $container = affix('.container')
1309
1494
  $keeper = $container.affix('.keeper[up-keep]').text('old-inside')
1495
+
1310
1496
  up.extract '.container', """
1311
1497
  <div class='container'>
1312
1498
  <div class='keeper' up-keep up-data='{ "foo": "bar" }'>new-inside</div>
1313
1499
  </div>
1314
1500
  """
1315
- expect($('.keeper')).toHaveText('old-inside')
1316
- expect(keptListener).toHaveBeenCalledWith($keeper, { 'foo': 'bar' })
1317
1501
 
1318
- it 'emits an up:fragment:kept with { newData: {} } if the discarded element had no up-data value', ->
1502
+ next =>
1503
+ expect($('.keeper')).toHaveText('old-inside')
1504
+ expect(keptListener).toHaveBeenCalledWith($keeper, { 'foo': 'bar' })
1505
+
1506
+ it 'emits an up:fragment:kept with { newData: {} } if the discarded element had no up-data value', asyncSpec (next) ->
1319
1507
  keptListener = jasmine.createSpy()
1320
1508
  up.on('up:fragment:kept', keptListener)
1321
1509
  $container = affix('.container')
@@ -1325,27 +1513,35 @@ describe 'up.dom', ->
1325
1513
  <div class='keeper' up-keep>new-inside</div>
1326
1514
  </div>
1327
1515
  """
1328
- expect($('.keeper')).toHaveText('old-inside')
1329
- expect(keptListener).toEqual(jasmine.anything(), $('.keeper'), {})
1330
1516
 
1331
- it 'reuses the same element and emits up:fragment:kept during multiple extractions', ->
1517
+ next =>
1518
+ expect($('.keeper')).toHaveText('old-inside')
1519
+ expect(keptListener).toEqual(jasmine.anything(), $('.keeper'), {})
1520
+
1521
+ it 'reuses the same element and emits up:fragment:kept during multiple extractions', asyncSpec (next) ->
1332
1522
  keptListener = jasmine.createSpy()
1333
1523
  up.on 'up:fragment:kept', (event) -> keptListener(event.$element, event.newData)
1334
1524
  $container = affix('.container')
1335
1525
  $keeper = $container.affix('.keeper[up-keep]').text('old-inside')
1336
- up.extract '.keeper', """
1337
- <div class='container'>
1338
- <div class='keeper' up-keep up-data='{ \"key\": \"value1\" }'>new-inside</div>
1339
- </div>
1340
- """
1341
- up.extract '.keeper', """
1342
- <div class='container'>
1343
- <div class='keeper' up-keep up-data='{ \"key\": \"value2\" }'>new-inside</div>
1344
- """
1345
- $keeper = $('.keeper')
1346
- expect($keeper).toHaveText('old-inside')
1347
- expect(keptListener).toHaveBeenCalledWith($keeper, { key: 'value1' })
1348
- expect(keptListener).toHaveBeenCalledWith($keeper, { key: 'value2' })
1526
+
1527
+ next =>
1528
+ up.extract '.keeper', """
1529
+ <div class='container'>
1530
+ <div class='keeper' up-keep up-data='{ \"key\": \"value1\" }'>new-inside</div>
1531
+ </div>
1532
+ """
1533
+
1534
+ next =>
1535
+ up.extract '.keeper', """
1536
+ <div class='container'>
1537
+ <div class='keeper' up-keep up-data='{ \"key\": \"value2\" }'>new-inside</div>
1538
+ """
1539
+
1540
+ next =>
1541
+ $keeper = $('.keeper')
1542
+ expect($keeper).toHaveText('old-inside')
1543
+ expect(keptListener).toHaveBeenCalledWith($keeper, { key: 'value1' })
1544
+ expect(keptListener).toHaveBeenCalledWith($keeper, { key: 'value2' })
1349
1545
 
1350
1546
  it "doesn't let the discarded element appear in a transition", (done) ->
1351
1547
  oldTextDuringTransition = undefined
@@ -1353,7 +1549,7 @@ describe 'up.dom', ->
1353
1549
  transition = ($old, $new) ->
1354
1550
  oldTextDuringTransition = squish($old.text())
1355
1551
  newTextDuringTransition = squish($new.text())
1356
- u.resolvedDeferred()
1552
+ Promise.resolve()
1357
1553
  $container = affix('.container')
1358
1554
  $container.html """
1359
1555
  <div class='foo'>old-foo</div>
@@ -1372,59 +1568,71 @@ describe 'up.dom', ->
1372
1568
  done()
1373
1569
 
1374
1570
  describe 'up.destroy', ->
1375
-
1376
- it 'removes the element with the given selector', ->
1571
+
1572
+ it 'removes the element with the given selector', (done) ->
1377
1573
  affix('.element')
1378
- up.destroy('.element')
1379
- expect($('.element')).not.toExist()
1380
-
1381
- it 'calls destructors for custom elements', ->
1574
+ up.destroy('.element').then ->
1575
+ expect($('.element')).not.toExist()
1576
+ done()
1577
+
1578
+ it 'calls destructors for custom elements', (done) ->
1382
1579
  up.compiler('.element', ($element) -> destructor)
1383
1580
  destructor = jasmine.createSpy('destructor')
1384
1581
  up.hello(affix('.element'))
1385
- up.destroy('.element')
1386
- expect(destructor).toHaveBeenCalled()
1387
-
1388
- it 'allows to pass a new history entry as { history } option', ->
1582
+ up.destroy('.element').then ->
1583
+ expect(destructor).toHaveBeenCalled()
1584
+ done()
1585
+
1586
+ it 'allows to pass a new history entry as { history } option', (done) ->
1587
+ up.history.config.enabled = true
1389
1588
  affix('.element')
1390
- up.destroy('.element', history: '/new-path')
1391
- expect(location.href).toEqualUrl('/new-path')
1589
+ up.destroy('.element', history: '/new-path').then ->
1590
+ u.setTimer 100, ->
1591
+ expect(location.href).toMatchUrl('/new-path')
1592
+ done()
1392
1593
 
1393
- it 'allows to pass a new document title as { title } option', ->
1594
+ it 'allows to pass a new document title as { title } option', (done) ->
1595
+ up.history.config.enabled = true
1394
1596
  affix('.element')
1395
- up.destroy('.element', history: '/new-path', title: 'Title from options')
1396
- expect(document.title).toEqual('Title from options')
1597
+ up.destroy('.element', history: '/new-path', title: 'Title from options').then ->
1598
+ expect(document.title).toEqual('Title from options')
1599
+ done()
1397
1600
 
1398
1601
 
1399
1602
  describe 'up.reload', ->
1400
1603
 
1401
1604
  describeCapability 'canPushState', ->
1402
-
1403
- it 'reloads the given selector from the closest known source URL', (done) ->
1605
+
1606
+ it 'reloads the given selector from the closest known source URL', asyncSpec (next) ->
1404
1607
  affix('.container[up-source="/source"] .element').find('.element').text('old text')
1405
-
1406
- up.reload('.element').then ->
1608
+
1609
+ next =>
1610
+ up.reload('.element')
1611
+
1612
+ next =>
1613
+ expect(@lastRequest().url).toMatch(/\/source$/)
1614
+ @respondWith """
1615
+ <div class="container">
1616
+ <div class="element">new text</div>
1617
+ </div>
1618
+ """
1619
+
1620
+ next =>
1407
1621
  expect($('.element')).toHaveText('new text')
1408
- done()
1409
-
1410
- expect(@lastRequest().url).toMatch(/\/source$/)
1411
-
1412
- @respondWith """
1413
- <div class="container">
1414
- <div class="element">new text</div>
1415
- </div>
1416
- """
1622
+
1417
1623
 
1418
1624
  describeFallback 'canPushState', ->
1419
-
1420
- it 'makes a page load from the closest known source URL', ->
1625
+
1626
+ it 'makes a page load from the closest known source URL', asyncSpec (next) ->
1421
1627
  affix('.container[up-source="/source"] .element').find('.element').text('old text')
1422
- spyOn(up.browser, 'loadPage')
1628
+ spyOn(up.browser, 'navigate')
1423
1629
  up.reload('.element')
1424
- expect(up.browser.loadPage).toHaveBeenCalledWith('/source', jasmine.anything())
1425
-
1426
-
1630
+
1631
+ next =>
1632
+ expect(up.browser.navigate).toHaveBeenCalledWith('/source', jasmine.anything())
1633
+
1634
+
1427
1635
  describe 'up.reset', ->
1428
-
1636
+
1429
1637
  it 'should have tests'
1430
1638