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
@@ -4,173 +4,389 @@ describe 'up.proxy', ->
4
4
 
5
5
  describe 'JavaScript functions', ->
6
6
 
7
- beforeEach ->
8
- jasmine.clock().install()
9
- jasmine.clock().mockDate()
10
-
11
- describe 'up.ajax', ->
7
+ describe 'up.request', ->
12
8
 
13
9
  it 'makes a request with the given URL and params', ->
14
- up.ajax('/foo', data: { key: 'value' }, method: 'post')
10
+ up.request('/foo', data: { key: 'value' }, method: 'post')
15
11
  request = @lastRequest()
16
- expect(request.url).toEqualUrl('/foo')
12
+ expect(request.url).toMatchUrl('/foo')
17
13
  expect(request.data()).toEqual(key: ['value'])
18
14
  expect(request.method).toEqual('POST')
19
15
 
20
16
  it 'also allows to pass the URL as a { url } option instead', ->
21
- up.ajax(url: '/foo', data: { key: 'value' }, method: 'post')
17
+ up.request(url: '/foo', data: { key: 'value' }, method: 'post')
22
18
  request = @lastRequest()
23
- expect(request.url).toEqualUrl('/foo')
19
+ expect(request.url).toMatchUrl('/foo')
24
20
  expect(request.data()).toEqual(key: ['value'])
25
21
  expect(request.method).toEqual('POST')
26
22
 
27
- it 'caches server responses for 5 minutes', ->
23
+ it 'submits the replacement targets as HTTP headers, so the server may choose to only frender the requested fragments', asyncSpec (next) ->
24
+ up.request(url: '/foo', target: '.target', failTarget: '.fail-target')
25
+
26
+ next =>
27
+ request = @lastRequest()
28
+ expect(request.requestHeaders['X-Up-Target']).toEqual('.target')
29
+ expect(request.requestHeaders['X-Up-Fail-Target']).toEqual('.fail-target')
30
+
31
+ it 'resolves to a Response object that contains information about the response and request', (done) ->
32
+ promise = up.request(
33
+ url: '/url'
34
+ data: { key: 'value' }
35
+ method: 'post'
36
+ target: '.target'
37
+ )
38
+
39
+ u.nextFrame =>
40
+ @respondWith(
41
+ status: 201,
42
+ responseText: 'response-text'
43
+ )
44
+
45
+ promise.then (response) ->
46
+ expect(response.request.url).toMatchUrl('/url')
47
+ expect(response.request.data).toEqual(key: 'value')
48
+ expect(response.request.method).toEqual('POST')
49
+ expect(response.request.target).toEqual('.target')
50
+ expect(response.request.hash).toBeBlank()
51
+
52
+ expect(response.url).toMatchUrl('/url') # If the server signaled a redirect with X-Up-Location, this would be reflected here
53
+ expect(response.method).toEqual('POST') # If the server sent a X-Up-Method header, this would be reflected here
54
+ expect(response.text).toEqual('response-text')
55
+ expect(response.status).toEqual(201)
56
+ expect(response.xhr).toBePresent()
57
+
58
+ done()
59
+
60
+ it "preserves the URL hash in a separate { hash } property, since although it isn't sent to server, code might need it to process the response", (done) ->
61
+ promise = up.request('/url#hash')
62
+
63
+ u.nextFrame =>
64
+ request = @lastRequest()
65
+ expect(request.url).toMatchUrl('/url')
66
+
67
+ @respondWith('response-text')
68
+
69
+ promise.then (response) ->
70
+ expect(response.request.url).toMatchUrl('/url')
71
+ expect(response.request.hash).toEqual('#hash')
72
+ expect(response.url).toMatchUrl('/url')
73
+ done()
74
+
75
+ describe 'when the server responds with an X-Up-Method header', ->
76
+
77
+ it 'updates the { method } property in the response object', (done) ->
78
+ promise = up.request(
79
+ url: '/url'
80
+ data: { key: 'value' }
81
+ method: 'post'
82
+ target: '.target'
83
+ )
84
+
85
+ u.nextFrame =>
86
+ @respondWith(
87
+ responseHeaders:
88
+ 'X-Up-Location': '/redirect'
89
+ 'X-Up-Method': 'GET'
90
+ )
91
+
92
+ promise.then (response) ->
93
+ expect(response.request.url).toMatchUrl('/url')
94
+ expect(response.request.method).toEqual('POST')
95
+ expect(response.url).toMatchUrl('/redirect')
96
+ expect(response.method).toEqual('GET')
97
+ done()
98
+
99
+ describe 'when the server responds with an X-Up-Location header', ->
100
+
101
+ it 'sets the { url } property on the response object', (done) ->
102
+ promise = up.request('/request-url#request-hash')
103
+
104
+ u.nextFrame =>
105
+ @respondWith
106
+ responseHeaders:
107
+ 'X-Up-Location': '/response-url'
108
+
109
+ promise.then (response) ->
110
+ expect(response.request.url).toMatchUrl('/request-url')
111
+ expect(response.request.hash).toEqual('#request-hash')
112
+ expect(response.url).toMatchUrl('/response-url')
113
+ done()
114
+
115
+ it 'considers a redirection URL an alias for the requested URL', asyncSpec (next) ->
116
+ up.request('/foo')
117
+
118
+ next =>
119
+ expect(jasmine.Ajax.requests.count()).toEqual(1)
120
+ @respondWith
121
+ responseHeaders:
122
+ 'X-Up-Location': '/bar'
123
+ 'X-Up-Method': 'GET'
124
+
125
+ next =>
126
+ up.request('/bar')
127
+
128
+ next =>
129
+ # See that the cached alias is used and no additional requests are made
130
+ expect(jasmine.Ajax.requests.count()).toEqual(1)
131
+
132
+ it 'does not considers a redirection URL an alias for the requested URL if the original request was never cached', asyncSpec (next) ->
133
+ up.request('/foo', method: 'post') # POST requests are not cached
134
+
135
+ next =>
136
+ expect(jasmine.Ajax.requests.count()).toEqual(1)
137
+ @respondWith
138
+ responseHeaders:
139
+ 'X-Up-Location': '/bar'
140
+ 'X-Up-Method': 'GET'
141
+
142
+ next =>
143
+ up.request('/bar')
144
+
145
+ next =>
146
+ # See that an additional request was made
147
+ expect(jasmine.Ajax.requests.count()).toEqual(2)
148
+
149
+ it 'does not considers a redirection URL an alias for the requested URL if the response returned a non-200 status code', asyncSpec (next) ->
150
+ up.request('/foo')
151
+
152
+ next =>
153
+ expect(jasmine.Ajax.requests.count()).toEqual(1)
154
+ @respondWith
155
+ responseHeaders:
156
+ 'X-Up-Location': '/bar'
157
+ 'X-Up-Method': 'GET'
158
+ status: 500
159
+
160
+ next =>
161
+ up.request('/bar')
162
+
163
+ next =>
164
+ # See that an additional request was made
165
+ expect(jasmine.Ajax.requests.count()).toEqual(2)
166
+
167
+ it "does not explode if the original request's { data } is a FormData object", asyncSpec (next) ->
168
+ up.request('/foo', method: 'post', data: new FormData()) # POST requests are not cached
169
+
170
+ next =>
171
+ expect(jasmine.Ajax.requests.count()).toEqual(1)
172
+ @respondWith
173
+ responseHeaders:
174
+ 'X-Up-Location': '/bar'
175
+ 'X-Up-Method': 'GET'
176
+
177
+ next =>
178
+ @secondAjaxPromise = up.request('/bar')
179
+
180
+ next.await =>
181
+ promiseState(@secondAjaxPromise).then (result) ->
182
+ # See that the promise was not rejected due to an internal error.
183
+ expect(result.state).toEqual('pending')
184
+
185
+ describe 'CSRF', ->
186
+
187
+ beforeEach ->
188
+ up.protocol.config.csrfHeader = 'csrf-header'
189
+ up.protocol.config.csrfToken = 'csrf-token'
190
+
191
+ it 'sets a CSRF token in the header', asyncSpec (next) ->
192
+ up.request('/path', method: 'post')
193
+ next =>
194
+ headers = @lastRequest().requestHeaders
195
+ expect(headers['csrf-header']).toEqual('csrf-token')
196
+
197
+ it 'does not add a CSRF token if there is none', asyncSpec (next) ->
198
+ up.protocol.config.csrfToken = ''
199
+ up.request('/path', method: 'post')
200
+ next =>
201
+ headers = @lastRequest().requestHeaders
202
+ expect(headers['csrf-header']).toBeMissing()
203
+
204
+ it 'does not add a CSRF token for GET requests', asyncSpec (next) ->
205
+ up.request('/path', method: 'get')
206
+ next =>
207
+ headers = @lastRequest().requestHeaders
208
+ expect(headers['csrf-header']).toBeMissing()
209
+
210
+ it 'does not add a CSRF token when loading content from another domain', asyncSpec (next) ->
211
+ up.request('http://other-domain.tld/path', method: 'post')
212
+ next =>
213
+ headers = @lastRequest().requestHeaders
214
+ expect(headers['csrf-header']).toBeMissing()
215
+
216
+ describe 'with { data } option', ->
217
+
218
+ it "uses the given params as a non-GET request's payload", asyncSpec (next) ->
219
+ givenParams = { 'foo-key': 'foo-value', 'bar-key': 'bar-value' }
220
+ up.request(url: '/path', method: 'put', data: givenParams)
221
+
222
+ next =>
223
+ expect(@lastRequest().data()['foo-key']).toEqual(['foo-value'])
224
+ expect(@lastRequest().data()['bar-key']).toEqual(['bar-value'])
225
+
226
+ it "encodes the given params into the URL of a GET request", (done) ->
227
+ givenParams = { 'foo-key': 'foo-value', 'bar-key': 'bar-value' }
228
+ promise = up.request(url: '/path', method: 'get', data: givenParams)
229
+
230
+ u.nextFrame =>
231
+ expect(@lastRequest().url).toMatchUrl('/path?foo-key=foo-value&bar-key=bar-value')
232
+ expect(@lastRequest().data()).toBeBlank()
233
+
234
+ @respondWith('response-text')
235
+
236
+ promise.then (response) ->
237
+ # See that the response object has been updated by moving the data options
238
+ # to the URL. This is important for up.dom code that works on response.request.
239
+ expect(response.request.url).toMatchUrl('/path?foo-key=foo-value&bar-key=bar-value')
240
+ expect(response.request.data).toBeBlank()
241
+ done()
242
+
243
+ it 'caches server responses for the configured duration', asyncSpec (next) ->
244
+ up.proxy.config.cacheExpiry = 200 # 1 second for test
245
+
28
246
  responses = []
247
+ trackResponse = (response) -> responses.push(response.text)
29
248
 
30
- # Send the same request for the same path, 3 minutes apart
31
- up.ajax(url: '/foo').then (data) -> responses.push(data)
32
- jasmine.clock().tick(3 * 60 * 1000)
33
- up.ajax(url: '/foo').then (data) -> responses.push(data)
249
+ next =>
250
+ up.request(url: '/foo').then(trackResponse)
251
+ expect(jasmine.Ajax.requests.count()).toEqual(1)
34
252
 
35
- # See that only a single network request was triggered
36
- expect(jasmine.Ajax.requests.count()).toEqual(1)
37
- expect(responses).toEqual([])
253
+ next.after (10), =>
254
+ # Send the same request for the same path
255
+ up.request(url: '/foo').then(trackResponse)
38
256
 
39
- @respondWith('foo')
257
+ # See that only a single network request was triggered
258
+ expect(jasmine.Ajax.requests.count()).toEqual(1)
259
+ expect(responses).toEqual([])
40
260
 
41
- # See that both requests have been fulfilled by the same response
42
- expect(responses).toEqual(['foo', 'foo'])
261
+ next =>
262
+ # Server responds once.
263
+ @respondWith('foo')
43
264
 
44
- # Send another request after another 3 minutes
45
- # The clock is now a total of 6 minutes after the first request,
46
- # exceeding the cache's retention time of 5 minutes.
47
- jasmine.clock().tick(3 * 60 * 1000)
48
- up.ajax(url: '/foo').then (data) -> responses.push(data)
265
+ next =>
266
+ # See that both requests have been fulfilled
267
+ expect(responses).toEqual(['foo', 'foo'])
49
268
 
50
- # See that we have triggered a second request
51
- expect(jasmine.Ajax.requests.count()).toEqual(2)
269
+ next.after (200), =>
270
+ # Send another request after another 3 minutes
271
+ # The clock is now a total of 6 minutes after the first request,
272
+ # exceeding the cache's retention time of 5 minutes.
273
+ up.request(url: '/foo').then(trackResponse)
52
274
 
53
- @respondWith('bar')
275
+ # See that we have triggered a second request
276
+ expect(jasmine.Ajax.requests.count()).toEqual(2)
277
+
278
+ next =>
279
+ @respondWith('bar')
54
280
 
55
- expect(responses).toEqual(['foo', 'foo', 'bar'])
281
+ next =>
282
+ expect(responses).toEqual(['foo', 'foo', 'bar'])
56
283
 
57
- it "does not cache responses if config.cacheExpiry is 0", ->
284
+ it "does not cache responses if config.cacheExpiry is 0", asyncSpec (next) ->
58
285
  up.proxy.config.cacheExpiry = 0
59
- up.ajax(url: '/foo')
60
- up.ajax(url: '/foo')
61
- expect(jasmine.Ajax.requests.count()).toEqual(2)
286
+ next => up.request(url: '/foo')
287
+ next => up.request(url: '/foo')
288
+ next => expect(jasmine.Ajax.requests.count()).toEqual(2)
62
289
 
63
- it "does not cache responses if config.cacheSize is 0", ->
290
+ it "does not cache responses if config.cacheSize is 0", asyncSpec (next) ->
64
291
  up.proxy.config.cacheSize = 0
65
- up.ajax(url: '/foo')
66
- up.ajax(url: '/foo')
67
- expect(jasmine.Ajax.requests.count()).toEqual(2)
292
+ next => up.request(url: '/foo')
293
+ next => up.request(url: '/foo')
294
+ next => expect(jasmine.Ajax.requests.count()).toEqual(2)
68
295
 
69
296
  it 'does not limit the number of cache entries if config.cacheSize is undefined'
70
297
 
71
298
  it 'never discards old cache entries if config.cacheExpiry is undefined'
72
299
 
73
- it 'respects a config.cacheSize setting', ->
300
+ it 'respects a config.cacheSize setting', asyncSpec (next) ->
74
301
  up.proxy.config.cacheSize = 2
75
- up.ajax(url: '/foo')
76
- up.ajax(url: '/bar')
77
- up.ajax(url: '/baz')
78
- up.ajax(url: '/foo')
79
- expect(jasmine.Ajax.requests.count()).toEqual(4)
80
-
81
- it "doesn't reuse responses when asked for the same path, but different selectors", ->
82
- up.ajax(url: '/path', target: '.a')
83
- up.ajax(url: '/path', target: '.b')
84
- expect(jasmine.Ajax.requests.count()).toEqual(2)
85
-
86
- it "doesn't reuse responses when asked for the same path, but different params", ->
87
- up.ajax(url: '/path', data: { query: 'foo' })
88
- up.ajax(url: '/path', data: { query: 'bar' })
89
- expect(jasmine.Ajax.requests.count()).toEqual(2)
90
-
91
- it "reuses a response for an 'html' selector when asked for the same path and any other selector", ->
92
- up.ajax(url: '/path', target: 'html')
93
- up.ajax(url: '/path', target: 'body')
94
- up.ajax(url: '/path', target: 'p')
95
- up.ajax(url: '/path', target: '.klass')
96
- expect(jasmine.Ajax.requests.count()).toEqual(1)
97
-
98
- it "reuses a response for a 'body' selector when asked for the same path and any other selector other than 'html'", ->
99
- up.ajax(url: '/path', target: 'body')
100
- up.ajax(url: '/path', target: 'p')
101
- up.ajax(url: '/path', target: '.klass')
102
- expect(jasmine.Ajax.requests.count()).toEqual(1)
103
-
104
- it "doesn't reuse a response for a 'body' selector when asked for the same path but an 'html' selector", ->
105
- up.ajax(url: '/path', target: 'body')
106
- up.ajax(url: '/path', target: 'html')
107
- expect(jasmine.Ajax.requests.count()).toEqual(2)
108
-
109
- it "doesn't reuse responses for different paths", ->
110
- up.ajax(url: '/foo')
111
- up.ajax(url: '/bar')
112
- expect(jasmine.Ajax.requests.count()).toEqual(2)
302
+ next => up.request(url: '/foo')
303
+ next => up.request(url: '/bar')
304
+ next => up.request(url: '/baz')
305
+ next => up.request(url: '/foo')
306
+ next => expect(jasmine.Ajax.requests.count()).toEqual(4)
307
+
308
+ it "doesn't reuse responses when asked for the same path, but different selectors", asyncSpec (next) ->
309
+ next => up.request(url: '/path', target: '.a')
310
+ next => up.request(url: '/path', target: '.b')
311
+ next => expect(jasmine.Ajax.requests.count()).toEqual(2)
312
+
313
+ it "doesn't reuse responses when asked for the same path, but different params", asyncSpec (next) ->
314
+ next => up.request(url: '/path', data: { query: 'foo' })
315
+ next => up.request(url: '/path', data: { query: 'bar' })
316
+ next => expect(jasmine.Ajax.requests.count()).toEqual(2)
317
+
318
+ it "reuses a response for an 'html' selector when asked for the same path and any other selector", asyncSpec (next) ->
319
+ next => up.request(url: '/path', target: 'html')
320
+ next => up.request(url: '/path', target: 'body')
321
+ next => up.request(url: '/path', target: 'p')
322
+ next => up.request(url: '/path', target: '.klass')
323
+ next => expect(jasmine.Ajax.requests.count()).toEqual(1)
324
+
325
+ it "reuses a response for a 'body' selector when asked for the same path and any other selector other than 'html'", asyncSpec (next) ->
326
+ next => up.request(url: '/path', target: 'body')
327
+ next => up.request(url: '/path', target: 'p')
328
+ next => up.request(url: '/path', target: '.klass')
329
+ next => expect(jasmine.Ajax.requests.count()).toEqual(1)
330
+
331
+ it "doesn't reuse a response for a 'body' selector when asked for the same path but an 'html' selector", asyncSpec (next) ->
332
+ next => up.request(url: '/path', target: 'body')
333
+ next => up.request(url: '/path', target: 'html')
334
+ next => expect(jasmine.Ajax.requests.count()).toEqual(2)
335
+
336
+ it "doesn't reuse responses for different paths", asyncSpec (next) ->
337
+ next => up.request(url: '/foo')
338
+ next => up.request(url: '/bar')
339
+ next => expect(jasmine.Ajax.requests.count()).toEqual(2)
113
340
 
114
341
  u.each ['GET', 'HEAD', 'OPTIONS'], (method) ->
115
342
 
116
- it "caches #{method} requests", ->
117
- u.times 2, -> up.ajax(url: '/foo', method: method)
118
- expect(jasmine.Ajax.requests.count()).toEqual(1)
343
+ it "caches #{method} requests", asyncSpec (next) ->
344
+ next => up.request(url: '/foo', method: method)
345
+ next => up.request(url: '/foo', method: method)
346
+ next => expect(jasmine.Ajax.requests.count()).toEqual(1)
119
347
 
120
- it "does not cache #{method} requests with cache: false option", ->
121
- u.times 2, -> up.ajax(url: '/foo', method: method, cache: false)
122
- expect(jasmine.Ajax.requests.count()).toEqual(2)
348
+ it "does not cache #{method} requests with { cache: false }", asyncSpec (next) ->
349
+ next => up.request(url: '/foo', method: method, cache: false)
350
+ next => up.request(url: '/foo', method: method, cache: false)
351
+ next => expect(jasmine.Ajax.requests.count()).toEqual(2)
123
352
 
124
353
  u.each ['POST', 'PUT', 'DELETE'], (method) ->
125
354
 
126
- it "does not cache #{method} requests", ->
127
- u.times 2, -> up.ajax(url: '/foo', method: method)
128
- expect(jasmine.Ajax.requests.count()).toEqual(2)
129
-
130
- it "caches #{method} requests with cache: true option", ->
131
- u.times 2, -> up.ajax(url: '/foo', method: method, cache: true)
132
- expect(jasmine.Ajax.requests.count()).toEqual(1)
133
-
134
- it 'does not cache responses with a non-200 status code', ->
135
- # Send the same request for the same path, 3 minutes apart
136
- up.ajax(url: '/foo')
137
-
138
- @respondWith
139
- status: 500
140
- contentType: 'text/html'
141
- responseText: 'foo'
355
+ it "does not cache #{method} requests", asyncSpec (next) ->
356
+ next => up.request(url: '/foo', method: method)
357
+ next => up.request(url: '/foo', method: method)
358
+ next => expect(jasmine.Ajax.requests.count()).toEqual(2)
142
359
 
143
- up.ajax(url: '/foo')
144
-
145
- expect(jasmine.Ajax.requests.count()).toEqual(2)
360
+ it 'does not cache responses with a non-200 status code', asyncSpec (next) ->
361
+ next => up.request(url: '/foo')
362
+ next => @respondWith(status: 500, contentType: 'text/html', responseText: 'foo')
363
+ next => up.request(url: '/foo')
364
+ next => expect(jasmine.Ajax.requests.count()).toEqual(2)
146
365
 
147
366
  describe 'with config.wrapMethods set', ->
148
367
 
149
368
  it 'should be set by default', ->
150
369
  expect(up.proxy.config.wrapMethods).toBePresent()
151
370
 
152
- # beforeEach ->
153
- # @oldWrapMethod = up.proxy.config.wrapMethod
154
- # up.proxy.config.wrapMethod = true
155
- #
156
- # afterEach ->
157
- # up.proxy.config.wrapMethod = @oldWrapMetod
158
-
159
371
  u.each ['GET', 'POST', 'HEAD', 'OPTIONS'], (method) ->
160
372
 
161
- it "does not change the method of a #{method} request", ->
162
- up.ajax(url: '/foo', method: method)
163
- request = @lastRequest()
164
- expect(request.method).toEqual(method)
165
- expect(request.data()['_method']).toBeUndefined()
373
+ it "does not change the method of a #{method} request", asyncSpec (next) ->
374
+ up.request(url: '/foo', method: method)
375
+
376
+ next =>
377
+ request = @lastRequest()
378
+ expect(request.method).toEqual(method)
379
+ expect(request.data()['_method']).toBeUndefined()
166
380
 
167
381
  u.each ['PUT', 'PATCH', 'DELETE'], (method) ->
168
382
 
169
- it "turns a #{method} request into a POST request and sends the actual method as a { _method } param", ->
170
- up.ajax(url: '/foo', method: method)
171
- request = @lastRequest()
172
- expect(request.method).toEqual('POST')
173
- expect(request.data()['_method']).toEqual([method])
383
+ it "turns a #{method} request into a POST request and sends the actual method as a { _method } param to prevent unexpected redirect behavior (https://makandracards.com/makandra/38347)", asyncSpec (next) ->
384
+ up.request(url: '/foo', method: method)
385
+
386
+ next =>
387
+ request = @lastRequest()
388
+ expect(request.method).toEqual('POST')
389
+ expect(request.data()['_method']).toEqual([method])
174
390
 
175
391
  describe 'with config.maxRequests set', ->
176
392
 
@@ -181,206 +397,447 @@ describe 'up.proxy', ->
181
397
  afterEach ->
182
398
  up.proxy.config.maxRequests = @oldMaxRequests
183
399
 
184
- it 'limits the number of concurrent requests', ->
400
+ it 'limits the number of concurrent requests', asyncSpec (next) ->
185
401
  responses = []
186
- up.ajax(url: '/foo').then (html) -> responses.push(html)
187
- up.ajax(url: '/bar').then (html) -> responses.push(html)
188
- expect(jasmine.Ajax.requests.count()).toEqual(1) # only one request was made
189
- @respondWith('first response', request: jasmine.Ajax.requests.at(0))
190
- expect(responses).toEqual ['first response']
191
- expect(jasmine.Ajax.requests.count()).toEqual(2) # a second request was made
192
- @respondWith('second response', request: jasmine.Ajax.requests.at(1))
193
- expect(responses).toEqual ['first response', 'second response']
194
-
195
- # it 'considers preloading links for the request limit', ->
196
- # up.ajax(url: '/foo', preload: true)
197
- # up.ajax(url: '/bar')
198
- # expect(jasmine.Ajax.requests.count()).toEqual(1)
199
-
200
- describe 'events', ->
201
-
402
+ trackResponse = (response) -> responses.push(response.text)
403
+
404
+ next =>
405
+ up.request(url: '/foo').then(trackResponse)
406
+ up.request(url: '/bar').then(trackResponse)
407
+
408
+ next =>
409
+ expect(jasmine.Ajax.requests.count()).toEqual(1) # only one request was made
410
+
411
+ next =>
412
+ @respondWith('first response', request: jasmine.Ajax.requests.at(0))
413
+
414
+ next =>
415
+ expect(responses).toEqual ['first response']
416
+ expect(jasmine.Ajax.requests.count()).toEqual(2) # a second request was made
417
+
418
+ next =>
419
+ @respondWith('second response', request: jasmine.Ajax.requests.at(1))
420
+
421
+ next =>
422
+ expect(responses).toEqual ['first response', 'second response']
423
+
424
+ it 'ignores preloading for the request limit', asyncSpec (next) ->
425
+ next => up.request(url: '/foo', preload: true)
426
+ next => up.request(url: '/bar')
427
+ next => expect(jasmine.Ajax.requests.count()).toEqual(2)
428
+ next => up.request(url: '/bar')
429
+ next => expect(jasmine.Ajax.requests.count()).toEqual(2)
430
+
431
+ describe 'up:proxy:load event', ->
432
+
433
+ it 'emits an up:proxy:load event before the request touches the network', asyncSpec (next) ->
434
+ listener = jasmine.createSpy('listener')
435
+ up.on 'up:proxy:load', listener
436
+ up.request('/bar')
437
+
438
+ next =>
439
+ expect(jasmine.Ajax.requests.count()).toEqual(1)
440
+
441
+ partialRequest = jasmine.objectContaining(
442
+ method: 'GET',
443
+ url: jasmine.stringMatching('/bar')
444
+ )
445
+ partialEvent = jasmine.objectContaining(request: partialRequest)
446
+
447
+ expect(listener).toHaveBeenCalledWith(partialEvent, jasmine.anything(), jasmine.anything())
448
+
449
+ it 'allows up:proxy:load listeners to prevent the request (useful to cancel all requests when stopping a test scenario)', (done) ->
450
+ listener = jasmine.createSpy('listener').and.callFake (event) ->
451
+ expect(jasmine.Ajax.requests.count()).toEqual(0)
452
+ event.preventDefault()
453
+
454
+ up.on 'up:proxy:load', listener
455
+
456
+ promise = up.request('/bar')
457
+
458
+ u.nextFrame ->
459
+ expect(listener).toHaveBeenCalled()
460
+ expect(jasmine.Ajax.requests.count()).toEqual(0)
461
+
462
+ promiseState(promise).then (result) ->
463
+ expect(result.state).toEqual('rejected')
464
+ expect(result.value).toBeError(/prevented/i)
465
+ done()
466
+
467
+ it 'does not block the queue when a request was prevented', (done) ->
468
+ up.proxy.config.maxRequests = 1
469
+
470
+ listener = jasmine.createSpy('listener').and.callFake (event) ->
471
+ # only prevent the first request
472
+ if event.request.url.indexOf('/path1') >= 0
473
+ event.preventDefault()
474
+
475
+ up.on 'up:proxy:load', listener
476
+
477
+ promise1 = up.request('/path1')
478
+ promise2 = up.request('/path2')
479
+
480
+ u.nextFrame =>
481
+ expect(listener.calls.count()).toBe(2)
482
+ expect(jasmine.Ajax.requests.count()).toEqual(1)
483
+ expect(@lastRequest().url).toMatchUrl('/path2')
484
+ done()
485
+
486
+ it 'allows up:proxy:load listeners to manipulate the request headers', (done) ->
487
+ listener = (event) ->
488
+ event.request.headers['X-From-Listener'] = 'foo'
489
+
490
+ up.on 'up:proxy:load', listener
491
+
492
+ up.request('/path1')
493
+
494
+ u.nextFrame =>
495
+ expect(@lastRequest().requestHeaders['X-From-Listener']).toEqual('foo')
496
+ done()
497
+
498
+ describe 'up:proxy:slow and up:proxy:recover events', ->
499
+
202
500
  beforeEach ->
203
501
  up.proxy.config.slowDelay = 0
204
502
  @events = []
205
- u.each ['up:proxy:load', 'up:proxy:received', 'up:proxy:slow', 'up:proxy:recover'], (eventName) =>
503
+ u.each ['up:proxy:load', 'up:proxy:loaded', 'up:proxy:slow', 'up:proxy:recover'], (eventName) =>
206
504
  up.on eventName, =>
207
505
  @events.push eventName
208
506
 
209
- it 'emits an up:proxy:slow event once the proxy started loading, and up:proxy:recover if it is done loading', ->
210
-
211
- up.ajax(url: '/foo')
212
-
213
- expect(@events).toEqual([
214
- 'up:proxy:load',
215
- 'up:proxy:slow'
216
- ])
217
-
218
- up.ajax(url: '/bar')
219
-
220
- expect(@events).toEqual([
221
- 'up:proxy:load',
222
- 'up:proxy:slow',
223
- 'up:proxy:load'
224
- ])
225
-
226
- jasmine.Ajax.requests.at(0).respondWith
227
- status: 200
228
- contentType: 'text/html'
229
- responseText: 'foo'
230
-
231
- expect(@events).toEqual([
232
- 'up:proxy:load',
233
- 'up:proxy:slow',
234
- 'up:proxy:load',
235
- 'up:proxy:received'
236
- ])
237
-
238
- jasmine.Ajax.requests.at(1).respondWith
239
- status: 200
240
- contentType: 'text/html'
241
- responseText: 'bar'
242
-
243
- expect(@events).toEqual([
244
- 'up:proxy:load',
245
- 'up:proxy:slow',
246
- 'up:proxy:load',
247
- 'up:proxy:received',
248
- 'up:proxy:received',
249
- 'up:proxy:recover'
250
- ])
251
-
252
- it 'does not emit an up:proxy:slow event if preloading', ->
253
-
254
- # A request for preloading preloading purposes
255
- # doesn't make us busy.
256
- up.ajax(url: '/foo', preload: true)
257
- expect(@events).toEqual([
258
- 'up:proxy:load'
259
- ])
260
- expect(up.proxy.isBusy()).toBe(false)
261
-
262
- # The same request with preloading does make us busy.
263
- up.ajax(url: '/foo')
264
- expect(@events).toEqual([
265
- 'up:proxy:load',
266
- 'up:proxy:slow'
267
- ])
268
- expect(up.proxy.isBusy()).toBe(true)
269
-
270
- # The response resolves both promises and makes
271
- # the proxy idle again.
272
- jasmine.Ajax.requests.at(0).respondWith
273
- status: 200
274
- contentType: 'text/html'
275
- responseText: 'foo'
276
- expect(@events).toEqual([
277
- 'up:proxy:load',
278
- 'up:proxy:slow',
279
- 'up:proxy:received',
280
- 'up:proxy:recover'
281
- ])
282
- expect(up.proxy.isBusy()).toBe(false)
283
-
284
- it 'can delay the up:proxy:slow event to prevent flickering of spinners', ->
285
- up.proxy.config.slowDelay = 100
286
-
287
- up.ajax(url: '/foo')
288
- expect(@events).toEqual([
289
- 'up:proxy:load'
290
- ])
291
-
292
- jasmine.clock().tick(50)
293
- expect(@events).toEqual([
294
- 'up:proxy:load'
295
- ])
296
-
297
- jasmine.clock().tick(50)
298
- expect(@events).toEqual([
299
- 'up:proxy:load',
300
- 'up:proxy:slow'
301
- ])
302
-
303
- jasmine.Ajax.requests.at(0).respondWith
304
- status: 200
305
- contentType: 'text/html'
306
- responseText: 'foo'
307
-
308
- expect(@events).toEqual([
309
- 'up:proxy:load',
310
- 'up:proxy:slow',
311
- 'up:proxy:received',
312
- 'up:proxy:recover'
313
- ])
314
-
315
- it 'does not emit up:proxy:recover if a delayed up:proxy:slow was never emitted due to a fast response', ->
316
- up.proxy.config.slowDelay = 100
317
-
318
- up.ajax(url: '/foo')
319
- expect(@events).toEqual([
320
- 'up:proxy:load'
321
- ])
322
-
323
- jasmine.clock().tick(50)
324
-
325
- jasmine.Ajax.requests.at(0).respondWith
326
- status: 200
327
- contentType: 'text/html'
328
- responseText: 'foo'
329
-
330
- jasmine.clock().tick(100)
331
-
332
- expect(@events).toEqual([
333
- 'up:proxy:load',
334
- 'up:proxy:received'
335
- ])
336
-
337
- it 'emits up:proxy:recover if a request returned but failed', ->
338
-
339
- up.ajax(url: '/foo')
340
-
341
- expect(@events).toEqual([
342
- 'up:proxy:load',
343
- 'up:proxy:slow'
344
- ])
345
-
346
- jasmine.Ajax.requests.at(0).respondWith
347
- status: 500
348
- contentType: 'text/html'
349
- responseText: 'something went wrong'
350
-
351
- expect(@events).toEqual([
352
- 'up:proxy:load',
353
- 'up:proxy:slow',
354
- 'up:proxy:received',
355
- 'up:proxy:recover'
356
- ])
507
+ it 'emits an up:proxy:slow event if the server takes too long to respond'
508
+
509
+ it 'does not emit an up:proxy:slow event if preloading', asyncSpec (next) ->
510
+ next =>
511
+ # A request for preloading preloading purposes
512
+ # doesn't make us busy.
513
+ up.request(url: '/foo', preload: true)
514
+
515
+ next =>
516
+ expect(@events).toEqual([
517
+ 'up:proxy:load'
518
+ ])
519
+ expect(up.proxy.isBusy()).toBe(false)
520
+
521
+ next =>
522
+ # The same request with preloading does trigger up:proxy:slow.
523
+ up.request(url: '/foo')
524
+
525
+ next =>
526
+ expect(@events).toEqual([
527
+ 'up:proxy:load',
528
+ 'up:proxy:slow'
529
+ ])
530
+ expect(up.proxy.isBusy()).toBe(true)
531
+
532
+ next =>
533
+ # The response resolves both promises and makes
534
+ # the proxy idle again.
535
+ jasmine.Ajax.requests.at(0).respondWith
536
+ status: 200
537
+ contentType: 'text/html'
538
+ responseText: 'foo'
539
+
540
+ next =>
541
+ expect(@events).toEqual([
542
+ 'up:proxy:load',
543
+ 'up:proxy:slow',
544
+ 'up:proxy:loaded',
545
+ 'up:proxy:recover'
546
+ ])
547
+ expect(up.proxy.isBusy()).toBe(false)
548
+
549
+ it 'can delay the up:proxy:slow event to prevent flickering of spinners', asyncSpec (next) ->
550
+ next =>
551
+ up.proxy.config.slowDelay = 100
552
+ up.request(url: '/foo')
553
+
554
+ next =>
555
+ expect(@events).toEqual([
556
+ 'up:proxy:load'
557
+ ])
558
+
559
+ next.after 50, =>
560
+ expect(@events).toEqual([
561
+ 'up:proxy:load'
562
+ ])
563
+
564
+ next.after 60, =>
565
+ expect(@events).toEqual([
566
+ 'up:proxy:load',
567
+ 'up:proxy:slow'
568
+ ])
569
+
570
+ next =>
571
+ jasmine.Ajax.requests.at(0).respondWith
572
+ status: 200
573
+ contentType: 'text/html'
574
+ responseText: 'foo'
575
+
576
+ next =>
577
+ expect(@events).toEqual([
578
+ 'up:proxy:load',
579
+ 'up:proxy:slow',
580
+ 'up:proxy:loaded',
581
+ 'up:proxy:recover'
582
+ ])
583
+
584
+ it 'does not emit up:proxy:recover if a delayed up:proxy:slow was never emitted due to a fast response', asyncSpec (next) ->
585
+ next =>
586
+ up.proxy.config.slowDelay = 100
587
+ up.request(url: '/foo')
588
+
589
+ next =>
590
+ expect(@events).toEqual([
591
+ 'up:proxy:load'
592
+ ])
593
+
594
+ next.after 50, =>
595
+ jasmine.Ajax.requests.at(0).respondWith
596
+ status: 200
597
+ contentType: 'text/html'
598
+ responseText: 'foo'
599
+
600
+ next.after 150, =>
601
+ expect(@events).toEqual([
602
+ 'up:proxy:load',
603
+ 'up:proxy:loaded'
604
+ ])
605
+
606
+ it 'emits up:proxy:recover if a request returned but failed', asyncSpec (next) ->
607
+ next =>
608
+ up.request(url: '/foo')
609
+
610
+ next =>
611
+ expect(@events).toEqual([
612
+ 'up:proxy:load',
613
+ 'up:proxy:slow'
614
+ ])
615
+
616
+ next =>
617
+ jasmine.Ajax.requests.at(0).respondWith
618
+ status: 500
619
+ contentType: 'text/html'
620
+ responseText: 'something went wrong'
621
+
622
+ next =>
623
+ expect(@events).toEqual([
624
+ 'up:proxy:load',
625
+ 'up:proxy:slow',
626
+ 'up:proxy:loaded',
627
+ 'up:proxy:recover'
628
+ ])
629
+
630
+
631
+ describe 'up.ajax', ->
632
+
633
+ it 'fulfills to the response text in order to match the $.ajax() API as good as possible', (done) ->
634
+ promise = up.ajax('/url')
635
+
636
+ u.setTimer 100, =>
637
+ @respondWith('response-text')
357
638
 
639
+ promise.then (text) ->
640
+ expect(text).toEqual('response-text')
641
+
642
+ done()
358
643
 
359
644
  describe 'up.proxy.preload', ->
360
645
 
361
646
  describeCapability 'canPushState', ->
362
647
 
363
- it "loads and caches the given link's destination", ->
364
- $link = affix('a[href="/path"]')
648
+ beforeEach ->
649
+ @requestTarget = => @lastRequest().requestHeaders['X-Up-Target']
650
+
651
+ it "loads and caches the given link's destination", asyncSpec (next) ->
652
+ affix('.target')
653
+ $link = affix('a[href="/path"][up-target=".target"]')
654
+
365
655
  up.proxy.preload($link)
366
- expect(u.isPromise(up.proxy.get(url: '/path'))).toBe(true)
367
656
 
368
- it "does not load a link whose method has side-effects", ->
657
+ next =>
658
+ cachedPromise = up.proxy.get(url: '/path', target: '.target')
659
+ expect(u.isPromise(cachedPromise)).toBe(true)
660
+
661
+ it "does not load a link whose method has side-effects", asyncSpec (next) ->
369
662
  $link = affix('a[href="/path"][data-method="post"]')
370
663
  up.proxy.preload($link)
371
- expect(up.proxy.get(url: '/path')).toBeUndefined()
664
+
665
+ next => expect(up.proxy.get(url: '/path')).toBeUndefined()
666
+
667
+ describe 'for an [up-target] link', ->
668
+
669
+ it 'includes the [up-target] selector as an X-Up-Target header if the targeted element is currently on the page', asyncSpec (next) ->
670
+ affix('.target')
671
+ $link = affix('a[href="/path"][up-target=".target"]')
672
+ up.proxy.preload($link)
673
+ next => expect(@requestTarget()).toEqual('.target')
674
+
675
+ it 'replaces the [up-target] selector as with a fallback and uses that as an X-Up-Target header if the targeted element is not currently on the page', asyncSpec (next) ->
676
+ $link = affix('a[href="/path"][up-target=".target"]')
677
+ up.proxy.preload($link)
678
+ # The default fallback would usually be `body`, but in Jasmine specs we change
679
+ # it to protect the test runner during failures.
680
+ next => expect(@requestTarget()).toEqual('.default-fallback')
681
+
682
+ it 'calls up.request() with a { preload: true } option so it bypasses the concurrency limit', asyncSpec (next) ->
683
+ requestSpy = spyOn(up, 'request')
684
+
685
+ $link = affix('a[href="/path"][up-target=".target"]')
686
+ up.proxy.preload($link)
687
+
688
+ next =>
689
+ expect(requestSpy).toHaveBeenCalledWith(jasmine.objectContaining(url: '/path', preload: true))
690
+
691
+ describe 'for an [up-modal] link', ->
692
+
693
+ beforeEach ->
694
+ up.motion.config.enabled = false
695
+
696
+ it 'includes the [up-modal] selector as an X-Up-Target header and does not replace it with a fallback, since the modal frame always exists', asyncSpec (next) ->
697
+ $link = affix('a[href="/path"][up-modal=".target"]')
698
+ up.proxy.preload($link)
699
+ next => expect(@requestTarget()).toEqual('.target')
700
+
701
+ it 'does not create a modal frame', asyncSpec (next) ->
702
+ $link = affix('a[href="/path"][up-modal=".target"]')
703
+ up.proxy.preload($link)
704
+ next =>
705
+ expect('.up-modal').not.toExist()
706
+
707
+ it 'does not emit an up:modal:open event', asyncSpec (next) ->
708
+ $link = affix('a[href="/path"][up-modal=".target"]')
709
+ openListener = jasmine.createSpy('listener')
710
+ up.on('up:modal:open', openListener)
711
+ up.proxy.preload($link)
712
+ next =>
713
+ expect(openListener).not.toHaveBeenCalled()
714
+
715
+ it 'does not close a currently open modal', asyncSpec (next) ->
716
+ $link = affix('a[href="/path"][up-modal=".target"]')
717
+ closeListener = jasmine.createSpy('listener')
718
+ up.on('up:modal:close', closeListener)
719
+
720
+ up.modal.extract('.content', '<div class="content">Modal content</div>')
721
+
722
+ next =>
723
+ expect('.up-modal .content').toBeInDOM()
724
+
725
+ next =>
726
+ up.proxy.preload($link)
727
+
728
+ next =>
729
+ expect('.up-modal .content').toBeInDOM()
730
+ expect(closeListener).not.toHaveBeenCalled()
731
+
732
+ next =>
733
+ up.modal.close()
734
+
735
+ next =>
736
+ expect('.up-modal .content').not.toBeInDOM()
737
+ expect(closeListener).toHaveBeenCalled()
738
+
739
+ it 'does not prevent the opening of other modals while the request is still pending', asyncSpec (next) ->
740
+ $link = affix('a[href="/path"][up-modal=".target"]')
741
+ up.proxy.preload($link)
742
+
743
+ next =>
744
+ up.modal.extract('.content', '<div class="content">Modal content</div>')
745
+
746
+ next =>
747
+ expect('.up-modal .content').toBeInDOM()
748
+
749
+ it 'calls up.request() with a { preload: true } option so it bypasses the concurrency limit', asyncSpec (next) ->
750
+ requestSpy = spyOn(up, 'request')
751
+
752
+ $link = affix('a[href="/path"][up-modal=".target"]')
753
+ up.proxy.preload($link)
754
+
755
+ next =>
756
+ expect(requestSpy).toHaveBeenCalledWith(jasmine.objectContaining(url: '/path', preload: true))
757
+
758
+ describe 'for an [up-popup] link', ->
759
+
760
+ beforeEach ->
761
+ up.motion.config.enabled = false
762
+
763
+ it 'includes the [up-popup] selector as an X-Up-Target header and does not replace it with a fallback, since the popup frame always exists', asyncSpec (next) ->
764
+ $link = affix('a[href="/path"][up-popup=".target"]')
765
+ up.proxy.preload($link)
766
+ next => expect(@requestTarget()).toEqual('.target')
767
+
768
+
769
+ it 'does not create a popup frame', asyncSpec (next) ->
770
+ $link = affix('a[href="/path"][up-popup=".target"]')
771
+ up.proxy.preload($link)
772
+ next =>
773
+ expect('.up-popup').not.toExist()
774
+
775
+ it 'does not emit an up:popup:open event', asyncSpec (next) ->
776
+ $link = affix('a[href="/path"][up-popup=".target"]')
777
+ openListener = jasmine.createSpy('listener')
778
+ up.on('up:popup:open', openListener)
779
+ up.proxy.preload($link)
780
+ next =>
781
+ expect(openListener).not.toHaveBeenCalled()
782
+
783
+ it 'does not close a currently open popup', asyncSpec (next) ->
784
+ $link = affix('a[href="/path"][up-popup=".target"]')
785
+ closeListener = jasmine.createSpy('listener')
786
+ up.on('up:popup:close', closeListener)
787
+
788
+ $existingAnchor = affix('.existing-anchor')
789
+ up.popup.attach($existingAnchor, target: '.content', html: '<div class="content">popup content</div>')
790
+
791
+ next =>
792
+ expect('.up-popup .content').toBeInDOM()
793
+
794
+ next =>
795
+ up.proxy.preload($link)
796
+
797
+ next =>
798
+ expect('.up-popup .content').toBeInDOM()
799
+ expect(closeListener).not.toHaveBeenCalled()
800
+
801
+ next =>
802
+ up.popup.close()
803
+
804
+ next =>
805
+ expect('.up-popup .content').not.toBeInDOM()
806
+ expect(closeListener).toHaveBeenCalled()
807
+
808
+ it 'does not prevent the opening of other popups while the request is still pending', asyncSpec (next) ->
809
+ $link = affix('a[href="/path"][up-popup=".target"]')
810
+ up.proxy.preload($link)
811
+
812
+ next =>
813
+ $anchor = affix('.existing-anchor')
814
+ up.popup.attach($anchor, target: '.content', html: '<div class="content">popup content</div>')
815
+
816
+ next =>
817
+ expect('.up-popup .content').toBeInDOM()
818
+
819
+ it 'calls up.request() with a { preload: true } option so it bypasses the concurrency limit', asyncSpec (next) ->
820
+ requestSpy = spyOn(up, 'request')
821
+
822
+ $link = affix('a[href="/path"][up-popup=".target"]')
823
+ up.proxy.preload($link)
824
+
825
+ next =>
826
+ expect(requestSpy).toHaveBeenCalledWith(jasmine.objectContaining(url: '/path', preload: true))
372
827
 
373
828
  describeFallback 'canPushState', ->
374
829
 
375
- it "does nothing", ->
376
- $link = affix('a[href="/path"]')
830
+ it "does nothing", asyncSpec (next) ->
831
+ affix('.target')
832
+ $link = affix('a[href="/path"][up-target=".target"]')
377
833
  up.proxy.preload($link)
378
- expect(jasmine.Ajax.requests.count()).toBe(0)
834
+ next =>
835
+ expect(jasmine.Ajax.requests.count()).toBe(0)
379
836
 
380
837
  describe 'up.proxy.get', ->
381
838
 
382
839
  it 'returns an existing cache entry for the given request', ->
383
- promise1 = up.ajax(url: '/foo', data: { key: 'value' })
840
+ promise1 = up.request(url: '/foo', data: { key: 'value' })
384
841
  promise2 = up.proxy.get(url: '/foo', data: { key: 'value' })
385
842
  expect(promise1).toBe(promise2)
386
843
 
@@ -400,6 +857,16 @@ describe 'up.proxy', ->
400
857
 
401
858
  it 'uses an existing cache entry for another request (used in case of redirects)'
402
859
 
860
+ describe 'up.proxy.remove', ->
861
+
862
+ it 'removes the cache entry for the given request'
863
+
864
+ it 'does nothing if the given request is not cached'
865
+
866
+ it 'does not crash when passed a request with FormData (bugfix)', ->
867
+ removal = -> up.proxy.remove(url: '/path', data: new FormData())
868
+ expect(removal).not.toThrowError()
869
+
403
870
  describe 'up.proxy.clear', ->
404
871
 
405
872
  it 'removes all cache entries'
@@ -410,16 +877,23 @@ describe 'up.proxy', ->
410
877
 
411
878
  it 'preloads the link destination on mouseover, after a delay'
412
879
 
413
- it 'triggers a separate AJAX request with a short cache expiry when hovered multiple times', (done) ->
414
- up.proxy.config.cacheExpiry = 10
880
+ it 'triggers a separate AJAX request when hovered multiple times and the cache expires between hovers', asyncSpec (next) ->
881
+ up.proxy.config.cacheExpiry = 50
415
882
  up.proxy.config.preloadDelay = 0
416
- spyOn(up, 'follow')
417
883
  $element = affix('a[href="/foo"][up-preload]')
418
884
  Trigger.mouseover($element)
419
- u.setTimer 1, =>
420
- expect(up.follow.calls.count()).toBe(1)
421
- u.setTimer 16, =>
422
- Trigger.mouseover($element)
423
- u.setTimer 1, =>
424
- expect(up.follow.calls.count()).toBe(2)
425
- done()
885
+
886
+ next.after 1, =>
887
+ expect(jasmine.Ajax.requests.count()).toEqual(1)
888
+
889
+ next.after 1, =>
890
+ Trigger.mouseover($element)
891
+
892
+ next.after 1, =>
893
+ expect(jasmine.Ajax.requests.count()).toEqual(1)
894
+
895
+ next.after 60, =>
896
+ Trigger.mouseover($element)
897
+
898
+ next.after 1, =>
899
+ expect(jasmine.Ajax.requests.count()).toEqual(2)