spec_app/spec/javascripts/up/proxy_spec.js.coffee in unpoly-rails-0.37.0 vs spec_app/spec/javascripts/up/proxy_spec.js.coffee in unpoly-rails-0.50.0
- old
+ new
@@ -2,387 +2,844 @@
u = up.util
describe 'JavaScript functions', ->
- beforeEach ->
- jasmine.clock().install()
- jasmine.clock().mockDate()
+ describe 'up.request', ->
- describe 'up.ajax', ->
-
it 'makes a request with the given URL and params', ->
- up.ajax('/foo', data: { key: 'value' }, method: 'post')
+ up.request('/foo', data: { key: 'value' }, method: 'post')
request = @lastRequest()
- expect(request.url).toEqualUrl('/foo')
+ expect(request.url).toMatchUrl('/foo')
expect(request.data()).toEqual(key: ['value'])
expect(request.method).toEqual('POST')
it 'also allows to pass the URL as a { url } option instead', ->
- up.ajax(url: '/foo', data: { key: 'value' }, method: 'post')
+ up.request(url: '/foo', data: { key: 'value' }, method: 'post')
request = @lastRequest()
- expect(request.url).toEqualUrl('/foo')
+ expect(request.url).toMatchUrl('/foo')
expect(request.data()).toEqual(key: ['value'])
expect(request.method).toEqual('POST')
- it 'caches server responses for 5 minutes', ->
+ it 'submits the replacement targets as HTTP headers, so the server may choose to only frender the requested fragments', asyncSpec (next) ->
+ up.request(url: '/foo', target: '.target', failTarget: '.fail-target')
+
+ next =>
+ request = @lastRequest()
+ expect(request.requestHeaders['X-Up-Target']).toEqual('.target')
+ expect(request.requestHeaders['X-Up-Fail-Target']).toEqual('.fail-target')
+
+ it 'resolves to a Response object that contains information about the response and request', (done) ->
+ promise = up.request(
+ url: '/url'
+ data: { key: 'value' }
+ method: 'post'
+ target: '.target'
+ )
+
+ u.nextFrame =>
+ @respondWith(
+ status: 201,
+ responseText: 'response-text'
+ )
+
+ promise.then (response) ->
+ expect(response.request.url).toMatchUrl('/url')
+ expect(response.request.data).toEqual(key: 'value')
+ expect(response.request.method).toEqual('POST')
+ expect(response.request.target).toEqual('.target')
+ expect(response.request.hash).toBeBlank()
+
+ expect(response.url).toMatchUrl('/url') # If the server signaled a redirect with X-Up-Location, this would be reflected here
+ expect(response.method).toEqual('POST') # If the server sent a X-Up-Method header, this would be reflected here
+ expect(response.text).toEqual('response-text')
+ expect(response.status).toEqual(201)
+ expect(response.xhr).toBePresent()
+
+ done()
+
+ 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) ->
+ promise = up.request('/url#hash')
+
+ u.nextFrame =>
+ request = @lastRequest()
+ expect(request.url).toMatchUrl('/url')
+
+ @respondWith('response-text')
+
+ promise.then (response) ->
+ expect(response.request.url).toMatchUrl('/url')
+ expect(response.request.hash).toEqual('#hash')
+ expect(response.url).toMatchUrl('/url')
+ done()
+
+ describe 'when the server responds with an X-Up-Method header', ->
+
+ it 'updates the { method } property in the response object', (done) ->
+ promise = up.request(
+ url: '/url'
+ data: { key: 'value' }
+ method: 'post'
+ target: '.target'
+ )
+
+ u.nextFrame =>
+ @respondWith(
+ responseHeaders:
+ 'X-Up-Location': '/redirect'
+ 'X-Up-Method': 'GET'
+ )
+
+ promise.then (response) ->
+ expect(response.request.url).toMatchUrl('/url')
+ expect(response.request.method).toEqual('POST')
+ expect(response.url).toMatchUrl('/redirect')
+ expect(response.method).toEqual('GET')
+ done()
+
+ describe 'when the server responds with an X-Up-Location header', ->
+
+ it 'sets the { url } property on the response object', (done) ->
+ promise = up.request('/request-url#request-hash')
+
+ u.nextFrame =>
+ @respondWith
+ responseHeaders:
+ 'X-Up-Location': '/response-url'
+
+ promise.then (response) ->
+ expect(response.request.url).toMatchUrl('/request-url')
+ expect(response.request.hash).toEqual('#request-hash')
+ expect(response.url).toMatchUrl('/response-url')
+ done()
+
+ it 'considers a redirection URL an alias for the requested URL', asyncSpec (next) ->
+ up.request('/foo')
+
+ next =>
+ expect(jasmine.Ajax.requests.count()).toEqual(1)
+ @respondWith
+ responseHeaders:
+ 'X-Up-Location': '/bar'
+ 'X-Up-Method': 'GET'
+
+ next =>
+ up.request('/bar')
+
+ next =>
+ # See that the cached alias is used and no additional requests are made
+ expect(jasmine.Ajax.requests.count()).toEqual(1)
+
+ it 'does not considers a redirection URL an alias for the requested URL if the original request was never cached', asyncSpec (next) ->
+ up.request('/foo', method: 'post') # POST requests are not cached
+
+ next =>
+ expect(jasmine.Ajax.requests.count()).toEqual(1)
+ @respondWith
+ responseHeaders:
+ 'X-Up-Location': '/bar'
+ 'X-Up-Method': 'GET'
+
+ next =>
+ up.request('/bar')
+
+ next =>
+ # See that an additional request was made
+ expect(jasmine.Ajax.requests.count()).toEqual(2)
+
+ it 'does not considers a redirection URL an alias for the requested URL if the response returned a non-200 status code', asyncSpec (next) ->
+ up.request('/foo')
+
+ next =>
+ expect(jasmine.Ajax.requests.count()).toEqual(1)
+ @respondWith
+ responseHeaders:
+ 'X-Up-Location': '/bar'
+ 'X-Up-Method': 'GET'
+ status: 500
+
+ next =>
+ up.request('/bar')
+
+ next =>
+ # See that an additional request was made
+ expect(jasmine.Ajax.requests.count()).toEqual(2)
+
+ it "does not explode if the original request's { data } is a FormData object", asyncSpec (next) ->
+ up.request('/foo', method: 'post', data: new FormData()) # POST requests are not cached
+
+ next =>
+ expect(jasmine.Ajax.requests.count()).toEqual(1)
+ @respondWith
+ responseHeaders:
+ 'X-Up-Location': '/bar'
+ 'X-Up-Method': 'GET'
+
+ next =>
+ @secondAjaxPromise = up.request('/bar')
+
+ next.await =>
+ promiseState(@secondAjaxPromise).then (result) ->
+ # See that the promise was not rejected due to an internal error.
+ expect(result.state).toEqual('pending')
+
+ describe 'CSRF', ->
+
+ beforeEach ->
+ up.protocol.config.csrfHeader = 'csrf-header'
+ up.protocol.config.csrfToken = 'csrf-token'
+
+ it 'sets a CSRF token in the header', asyncSpec (next) ->
+ up.request('/path', method: 'post')
+ next =>
+ headers = @lastRequest().requestHeaders
+ expect(headers['csrf-header']).toEqual('csrf-token')
+
+ it 'does not add a CSRF token if there is none', asyncSpec (next) ->
+ up.protocol.config.csrfToken = ''
+ up.request('/path', method: 'post')
+ next =>
+ headers = @lastRequest().requestHeaders
+ expect(headers['csrf-header']).toBeMissing()
+
+ it 'does not add a CSRF token for GET requests', asyncSpec (next) ->
+ up.request('/path', method: 'get')
+ next =>
+ headers = @lastRequest().requestHeaders
+ expect(headers['csrf-header']).toBeMissing()
+
+ it 'does not add a CSRF token when loading content from another domain', asyncSpec (next) ->
+ up.request('http://other-domain.tld/path', method: 'post')
+ next =>
+ headers = @lastRequest().requestHeaders
+ expect(headers['csrf-header']).toBeMissing()
+
+ describe 'with { data } option', ->
+
+ it "uses the given params as a non-GET request's payload", asyncSpec (next) ->
+ givenParams = { 'foo-key': 'foo-value', 'bar-key': 'bar-value' }
+ up.request(url: '/path', method: 'put', data: givenParams)
+
+ next =>
+ expect(@lastRequest().data()['foo-key']).toEqual(['foo-value'])
+ expect(@lastRequest().data()['bar-key']).toEqual(['bar-value'])
+
+ it "encodes the given params into the URL of a GET request", (done) ->
+ givenParams = { 'foo-key': 'foo-value', 'bar-key': 'bar-value' }
+ promise = up.request(url: '/path', method: 'get', data: givenParams)
+
+ u.nextFrame =>
+ expect(@lastRequest().url).toMatchUrl('/path?foo-key=foo-value&bar-key=bar-value')
+ expect(@lastRequest().data()).toBeBlank()
+
+ @respondWith('response-text')
+
+ promise.then (response) ->
+ # See that the response object has been updated by moving the data options
+ # to the URL. This is important for up.dom code that works on response.request.
+ expect(response.request.url).toMatchUrl('/path?foo-key=foo-value&bar-key=bar-value')
+ expect(response.request.data).toBeBlank()
+ done()
+
+ it 'caches server responses for the configured duration', asyncSpec (next) ->
+ up.proxy.config.cacheExpiry = 200 # 1 second for test
+
responses = []
+ trackResponse = (response) -> responses.push(response.text)
- # Send the same request for the same path, 3 minutes apart
- up.ajax(url: '/foo').then (data) -> responses.push(data)
- jasmine.clock().tick(3 * 60 * 1000)
- up.ajax(url: '/foo').then (data) -> responses.push(data)
+ next =>
+ up.request(url: '/foo').then(trackResponse)
+ expect(jasmine.Ajax.requests.count()).toEqual(1)
- # See that only a single network request was triggered
- expect(jasmine.Ajax.requests.count()).toEqual(1)
- expect(responses).toEqual([])
+ next.after (10), =>
+ # Send the same request for the same path
+ up.request(url: '/foo').then(trackResponse)
- @respondWith('foo')
+ # See that only a single network request was triggered
+ expect(jasmine.Ajax.requests.count()).toEqual(1)
+ expect(responses).toEqual([])
- # See that both requests have been fulfilled by the same response
- expect(responses).toEqual(['foo', 'foo'])
+ next =>
+ # Server responds once.
+ @respondWith('foo')
- # Send another request after another 3 minutes
- # The clock is now a total of 6 minutes after the first request,
- # exceeding the cache's retention time of 5 minutes.
- jasmine.clock().tick(3 * 60 * 1000)
- up.ajax(url: '/foo').then (data) -> responses.push(data)
+ next =>
+ # See that both requests have been fulfilled
+ expect(responses).toEqual(['foo', 'foo'])
- # See that we have triggered a second request
- expect(jasmine.Ajax.requests.count()).toEqual(2)
+ next.after (200), =>
+ # Send another request after another 3 minutes
+ # The clock is now a total of 6 minutes after the first request,
+ # exceeding the cache's retention time of 5 minutes.
+ up.request(url: '/foo').then(trackResponse)
- @respondWith('bar')
+ # See that we have triggered a second request
+ expect(jasmine.Ajax.requests.count()).toEqual(2)
- expect(responses).toEqual(['foo', 'foo', 'bar'])
+ next =>
+ @respondWith('bar')
- it "does not cache responses if config.cacheExpiry is 0", ->
+ next =>
+ expect(responses).toEqual(['foo', 'foo', 'bar'])
+
+ it "does not cache responses if config.cacheExpiry is 0", asyncSpec (next) ->
up.proxy.config.cacheExpiry = 0
- up.ajax(url: '/foo')
- up.ajax(url: '/foo')
- expect(jasmine.Ajax.requests.count()).toEqual(2)
+ next => up.request(url: '/foo')
+ next => up.request(url: '/foo')
+ next => expect(jasmine.Ajax.requests.count()).toEqual(2)
- it "does not cache responses if config.cacheSize is 0", ->
+ it "does not cache responses if config.cacheSize is 0", asyncSpec (next) ->
up.proxy.config.cacheSize = 0
- up.ajax(url: '/foo')
- up.ajax(url: '/foo')
- expect(jasmine.Ajax.requests.count()).toEqual(2)
+ next => up.request(url: '/foo')
+ next => up.request(url: '/foo')
+ next => expect(jasmine.Ajax.requests.count()).toEqual(2)
it 'does not limit the number of cache entries if config.cacheSize is undefined'
it 'never discards old cache entries if config.cacheExpiry is undefined'
- it 'respects a config.cacheSize setting', ->
+ it 'respects a config.cacheSize setting', asyncSpec (next) ->
up.proxy.config.cacheSize = 2
- up.ajax(url: '/foo')
- up.ajax(url: '/bar')
- up.ajax(url: '/baz')
- up.ajax(url: '/foo')
- expect(jasmine.Ajax.requests.count()).toEqual(4)
+ next => up.request(url: '/foo')
+ next => up.request(url: '/bar')
+ next => up.request(url: '/baz')
+ next => up.request(url: '/foo')
+ next => expect(jasmine.Ajax.requests.count()).toEqual(4)
- it "doesn't reuse responses when asked for the same path, but different selectors", ->
- up.ajax(url: '/path', target: '.a')
- up.ajax(url: '/path', target: '.b')
- expect(jasmine.Ajax.requests.count()).toEqual(2)
+ it "doesn't reuse responses when asked for the same path, but different selectors", asyncSpec (next) ->
+ next => up.request(url: '/path', target: '.a')
+ next => up.request(url: '/path', target: '.b')
+ next => expect(jasmine.Ajax.requests.count()).toEqual(2)
- it "doesn't reuse responses when asked for the same path, but different params", ->
- up.ajax(url: '/path', data: { query: 'foo' })
- up.ajax(url: '/path', data: { query: 'bar' })
- expect(jasmine.Ajax.requests.count()).toEqual(2)
+ it "doesn't reuse responses when asked for the same path, but different params", asyncSpec (next) ->
+ next => up.request(url: '/path', data: { query: 'foo' })
+ next => up.request(url: '/path', data: { query: 'bar' })
+ next => expect(jasmine.Ajax.requests.count()).toEqual(2)
- it "reuses a response for an 'html' selector when asked for the same path and any other selector", ->
- up.ajax(url: '/path', target: 'html')
- up.ajax(url: '/path', target: 'body')
- up.ajax(url: '/path', target: 'p')
- up.ajax(url: '/path', target: '.klass')
- expect(jasmine.Ajax.requests.count()).toEqual(1)
+ it "reuses a response for an 'html' selector when asked for the same path and any other selector", asyncSpec (next) ->
+ next => up.request(url: '/path', target: 'html')
+ next => up.request(url: '/path', target: 'body')
+ next => up.request(url: '/path', target: 'p')
+ next => up.request(url: '/path', target: '.klass')
+ next => expect(jasmine.Ajax.requests.count()).toEqual(1)
- it "reuses a response for a 'body' selector when asked for the same path and any other selector other than 'html'", ->
- up.ajax(url: '/path', target: 'body')
- up.ajax(url: '/path', target: 'p')
- up.ajax(url: '/path', target: '.klass')
- expect(jasmine.Ajax.requests.count()).toEqual(1)
+ it "reuses a response for a 'body' selector when asked for the same path and any other selector other than 'html'", asyncSpec (next) ->
+ next => up.request(url: '/path', target: 'body')
+ next => up.request(url: '/path', target: 'p')
+ next => up.request(url: '/path', target: '.klass')
+ next => expect(jasmine.Ajax.requests.count()).toEqual(1)
- it "doesn't reuse a response for a 'body' selector when asked for the same path but an 'html' selector", ->
- up.ajax(url: '/path', target: 'body')
- up.ajax(url: '/path', target: 'html')
- expect(jasmine.Ajax.requests.count()).toEqual(2)
+ it "doesn't reuse a response for a 'body' selector when asked for the same path but an 'html' selector", asyncSpec (next) ->
+ next => up.request(url: '/path', target: 'body')
+ next => up.request(url: '/path', target: 'html')
+ next => expect(jasmine.Ajax.requests.count()).toEqual(2)
- it "doesn't reuse responses for different paths", ->
- up.ajax(url: '/foo')
- up.ajax(url: '/bar')
- expect(jasmine.Ajax.requests.count()).toEqual(2)
+ it "doesn't reuse responses for different paths", asyncSpec (next) ->
+ next => up.request(url: '/foo')
+ next => up.request(url: '/bar')
+ next => expect(jasmine.Ajax.requests.count()).toEqual(2)
u.each ['GET', 'HEAD', 'OPTIONS'], (method) ->
- it "caches #{method} requests", ->
- u.times 2, -> up.ajax(url: '/foo', method: method)
- expect(jasmine.Ajax.requests.count()).toEqual(1)
+ it "caches #{method} requests", asyncSpec (next) ->
+ next => up.request(url: '/foo', method: method)
+ next => up.request(url: '/foo', method: method)
+ next => expect(jasmine.Ajax.requests.count()).toEqual(1)
- it "does not cache #{method} requests with cache: false option", ->
- u.times 2, -> up.ajax(url: '/foo', method: method, cache: false)
- expect(jasmine.Ajax.requests.count()).toEqual(2)
+ it "does not cache #{method} requests with { cache: false }", asyncSpec (next) ->
+ next => up.request(url: '/foo', method: method, cache: false)
+ next => up.request(url: '/foo', method: method, cache: false)
+ next => expect(jasmine.Ajax.requests.count()).toEqual(2)
u.each ['POST', 'PUT', 'DELETE'], (method) ->
- it "does not cache #{method} requests", ->
- u.times 2, -> up.ajax(url: '/foo', method: method)
- expect(jasmine.Ajax.requests.count()).toEqual(2)
+ it "does not cache #{method} requests", asyncSpec (next) ->
+ next => up.request(url: '/foo', method: method)
+ next => up.request(url: '/foo', method: method)
+ next => expect(jasmine.Ajax.requests.count()).toEqual(2)
- it "caches #{method} requests with cache: true option", ->
- u.times 2, -> up.ajax(url: '/foo', method: method, cache: true)
- expect(jasmine.Ajax.requests.count()).toEqual(1)
+ it 'does not cache responses with a non-200 status code', asyncSpec (next) ->
+ next => up.request(url: '/foo')
+ next => @respondWith(status: 500, contentType: 'text/html', responseText: 'foo')
+ next => up.request(url: '/foo')
+ next => expect(jasmine.Ajax.requests.count()).toEqual(2)
- it 'does not cache responses with a non-200 status code', ->
- # Send the same request for the same path, 3 minutes apart
- up.ajax(url: '/foo')
-
- @respondWith
- status: 500
- contentType: 'text/html'
- responseText: 'foo'
-
- up.ajax(url: '/foo')
-
- expect(jasmine.Ajax.requests.count()).toEqual(2)
-
describe 'with config.wrapMethods set', ->
it 'should be set by default', ->
expect(up.proxy.config.wrapMethods).toBePresent()
-# beforeEach ->
-# @oldWrapMethod = up.proxy.config.wrapMethod
-# up.proxy.config.wrapMethod = true
-#
-# afterEach ->
-# up.proxy.config.wrapMethod = @oldWrapMetod
-
u.each ['GET', 'POST', 'HEAD', 'OPTIONS'], (method) ->
- it "does not change the method of a #{method} request", ->
- up.ajax(url: '/foo', method: method)
- request = @lastRequest()
- expect(request.method).toEqual(method)
- expect(request.data()['_method']).toBeUndefined()
+ it "does not change the method of a #{method} request", asyncSpec (next) ->
+ up.request(url: '/foo', method: method)
+ next =>
+ request = @lastRequest()
+ expect(request.method).toEqual(method)
+ expect(request.data()['_method']).toBeUndefined()
+
u.each ['PUT', 'PATCH', 'DELETE'], (method) ->
- it "turns a #{method} request into a POST request and sends the actual method as a { _method } param", ->
- up.ajax(url: '/foo', method: method)
- request = @lastRequest()
- expect(request.method).toEqual('POST')
- expect(request.data()['_method']).toEqual([method])
+ 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) ->
+ up.request(url: '/foo', method: method)
+ next =>
+ request = @lastRequest()
+ expect(request.method).toEqual('POST')
+ expect(request.data()['_method']).toEqual([method])
+
describe 'with config.maxRequests set', ->
beforeEach ->
@oldMaxRequests = up.proxy.config.maxRequests
up.proxy.config.maxRequests = 1
afterEach ->
up.proxy.config.maxRequests = @oldMaxRequests
- it 'limits the number of concurrent requests', ->
+ it 'limits the number of concurrent requests', asyncSpec (next) ->
responses = []
- up.ajax(url: '/foo').then (html) -> responses.push(html)
- up.ajax(url: '/bar').then (html) -> responses.push(html)
- expect(jasmine.Ajax.requests.count()).toEqual(1) # only one request was made
- @respondWith('first response', request: jasmine.Ajax.requests.at(0))
- expect(responses).toEqual ['first response']
- expect(jasmine.Ajax.requests.count()).toEqual(2) # a second request was made
- @respondWith('second response', request: jasmine.Ajax.requests.at(1))
- expect(responses).toEqual ['first response', 'second response']
+ trackResponse = (response) -> responses.push(response.text)
-# it 'considers preloading links for the request limit', ->
-# up.ajax(url: '/foo', preload: true)
-# up.ajax(url: '/bar')
-# expect(jasmine.Ajax.requests.count()).toEqual(1)
+ next =>
+ up.request(url: '/foo').then(trackResponse)
+ up.request(url: '/bar').then(trackResponse)
- describe 'events', ->
-
+ next =>
+ expect(jasmine.Ajax.requests.count()).toEqual(1) # only one request was made
+
+ next =>
+ @respondWith('first response', request: jasmine.Ajax.requests.at(0))
+
+ next =>
+ expect(responses).toEqual ['first response']
+ expect(jasmine.Ajax.requests.count()).toEqual(2) # a second request was made
+
+ next =>
+ @respondWith('second response', request: jasmine.Ajax.requests.at(1))
+
+ next =>
+ expect(responses).toEqual ['first response', 'second response']
+
+ it 'ignores preloading for the request limit', asyncSpec (next) ->
+ next => up.request(url: '/foo', preload: true)
+ next => up.request(url: '/bar')
+ next => expect(jasmine.Ajax.requests.count()).toEqual(2)
+ next => up.request(url: '/bar')
+ next => expect(jasmine.Ajax.requests.count()).toEqual(2)
+
+ describe 'up:proxy:load event', ->
+
+ it 'emits an up:proxy:load event before the request touches the network', asyncSpec (next) ->
+ listener = jasmine.createSpy('listener')
+ up.on 'up:proxy:load', listener
+ up.request('/bar')
+
+ next =>
+ expect(jasmine.Ajax.requests.count()).toEqual(1)
+
+ partialRequest = jasmine.objectContaining(
+ method: 'GET',
+ url: jasmine.stringMatching('/bar')
+ )
+ partialEvent = jasmine.objectContaining(request: partialRequest)
+
+ expect(listener).toHaveBeenCalledWith(partialEvent, jasmine.anything(), jasmine.anything())
+
+ it 'allows up:proxy:load listeners to prevent the request (useful to cancel all requests when stopping a test scenario)', (done) ->
+ listener = jasmine.createSpy('listener').and.callFake (event) ->
+ expect(jasmine.Ajax.requests.count()).toEqual(0)
+ event.preventDefault()
+
+ up.on 'up:proxy:load', listener
+
+ promise = up.request('/bar')
+
+ u.nextFrame ->
+ expect(listener).toHaveBeenCalled()
+ expect(jasmine.Ajax.requests.count()).toEqual(0)
+
+ promiseState(promise).then (result) ->
+ expect(result.state).toEqual('rejected')
+ expect(result.value).toBeError(/prevented/i)
+ done()
+
+ it 'does not block the queue when a request was prevented', (done) ->
+ up.proxy.config.maxRequests = 1
+
+ listener = jasmine.createSpy('listener').and.callFake (event) ->
+ # only prevent the first request
+ if event.request.url.indexOf('/path1') >= 0
+ event.preventDefault()
+
+ up.on 'up:proxy:load', listener
+
+ promise1 = up.request('/path1')
+ promise2 = up.request('/path2')
+
+ u.nextFrame =>
+ expect(listener.calls.count()).toBe(2)
+ expect(jasmine.Ajax.requests.count()).toEqual(1)
+ expect(@lastRequest().url).toMatchUrl('/path2')
+ done()
+
+ it 'allows up:proxy:load listeners to manipulate the request headers', (done) ->
+ listener = (event) ->
+ event.request.headers['X-From-Listener'] = 'foo'
+
+ up.on 'up:proxy:load', listener
+
+ up.request('/path1')
+
+ u.nextFrame =>
+ expect(@lastRequest().requestHeaders['X-From-Listener']).toEqual('foo')
+ done()
+
+ describe 'up:proxy:slow and up:proxy:recover events', ->
+
beforeEach ->
up.proxy.config.slowDelay = 0
@events = []
- u.each ['up:proxy:load', 'up:proxy:received', 'up:proxy:slow', 'up:proxy:recover'], (eventName) =>
+ u.each ['up:proxy:load', 'up:proxy:loaded', 'up:proxy:slow', 'up:proxy:recover'], (eventName) =>
up.on eventName, =>
@events.push eventName
- it 'emits an up:proxy:slow event once the proxy started loading, and up:proxy:recover if it is done loading', ->
-
- up.ajax(url: '/foo')
-
- expect(@events).toEqual([
- 'up:proxy:load',
- 'up:proxy:slow'
- ])
-
- up.ajax(url: '/bar')
-
- expect(@events).toEqual([
- 'up:proxy:load',
- 'up:proxy:slow',
- 'up:proxy:load'
- ])
-
- jasmine.Ajax.requests.at(0).respondWith
- status: 200
- contentType: 'text/html'
- responseText: 'foo'
-
- expect(@events).toEqual([
- 'up:proxy:load',
- 'up:proxy:slow',
- 'up:proxy:load',
- 'up:proxy:received'
- ])
-
- jasmine.Ajax.requests.at(1).respondWith
- status: 200
- contentType: 'text/html'
- responseText: 'bar'
-
- expect(@events).toEqual([
- 'up:proxy:load',
- 'up:proxy:slow',
- 'up:proxy:load',
- 'up:proxy:received',
- 'up:proxy:received',
- 'up:proxy:recover'
- ])
-
- it 'does not emit an up:proxy:slow event if preloading', ->
+ it 'emits an up:proxy:slow event if the server takes too long to respond'
- # A request for preloading preloading purposes
- # doesn't make us busy.
- up.ajax(url: '/foo', preload: true)
- expect(@events).toEqual([
- 'up:proxy:load'
- ])
- expect(up.proxy.isBusy()).toBe(false)
+ it 'does not emit an up:proxy:slow event if preloading', asyncSpec (next) ->
+ next =>
+ # A request for preloading preloading purposes
+ # doesn't make us busy.
+ up.request(url: '/foo', preload: true)
- # The same request with preloading does make us busy.
- up.ajax(url: '/foo')
- expect(@events).toEqual([
- 'up:proxy:load',
- 'up:proxy:slow'
- ])
- expect(up.proxy.isBusy()).toBe(true)
+ next =>
+ expect(@events).toEqual([
+ 'up:proxy:load'
+ ])
+ expect(up.proxy.isBusy()).toBe(false)
- # The response resolves both promises and makes
- # the proxy idle again.
- jasmine.Ajax.requests.at(0).respondWith
- status: 200
- contentType: 'text/html'
- responseText: 'foo'
- expect(@events).toEqual([
- 'up:proxy:load',
- 'up:proxy:slow',
- 'up:proxy:received',
- 'up:proxy:recover'
- ])
- expect(up.proxy.isBusy()).toBe(false)
+ next =>
+ # The same request with preloading does trigger up:proxy:slow.
+ up.request(url: '/foo')
- it 'can delay the up:proxy:slow event to prevent flickering of spinners', ->
- up.proxy.config.slowDelay = 100
+ next =>
+ expect(@events).toEqual([
+ 'up:proxy:load',
+ 'up:proxy:slow'
+ ])
+ expect(up.proxy.isBusy()).toBe(true)
- up.ajax(url: '/foo')
- expect(@events).toEqual([
- 'up:proxy:load'
- ])
+ next =>
+ # The response resolves both promises and makes
+ # the proxy idle again.
+ jasmine.Ajax.requests.at(0).respondWith
+ status: 200
+ contentType: 'text/html'
+ responseText: 'foo'
- jasmine.clock().tick(50)
- expect(@events).toEqual([
- 'up:proxy:load'
- ])
+ next =>
+ expect(@events).toEqual([
+ 'up:proxy:load',
+ 'up:proxy:slow',
+ 'up:proxy:loaded',
+ 'up:proxy:recover'
+ ])
+ expect(up.proxy.isBusy()).toBe(false)
- jasmine.clock().tick(50)
- expect(@events).toEqual([
- 'up:proxy:load',
- 'up:proxy:slow'
- ])
+ it 'can delay the up:proxy:slow event to prevent flickering of spinners', asyncSpec (next) ->
+ next =>
+ up.proxy.config.slowDelay = 100
+ up.request(url: '/foo')
- jasmine.Ajax.requests.at(0).respondWith
- status: 200
- contentType: 'text/html'
- responseText: 'foo'
+ next =>
+ expect(@events).toEqual([
+ 'up:proxy:load'
+ ])
- expect(@events).toEqual([
- 'up:proxy:load',
- 'up:proxy:slow',
- 'up:proxy:received',
- 'up:proxy:recover'
- ])
+ next.after 50, =>
+ expect(@events).toEqual([
+ 'up:proxy:load'
+ ])
- it 'does not emit up:proxy:recover if a delayed up:proxy:slow was never emitted due to a fast response', ->
- up.proxy.config.slowDelay = 100
+ next.after 60, =>
+ expect(@events).toEqual([
+ 'up:proxy:load',
+ 'up:proxy:slow'
+ ])
- up.ajax(url: '/foo')
- expect(@events).toEqual([
- 'up:proxy:load'
- ])
+ next =>
+ jasmine.Ajax.requests.at(0).respondWith
+ status: 200
+ contentType: 'text/html'
+ responseText: 'foo'
- jasmine.clock().tick(50)
+ next =>
+ expect(@events).toEqual([
+ 'up:proxy:load',
+ 'up:proxy:slow',
+ 'up:proxy:loaded',
+ 'up:proxy:recover'
+ ])
- jasmine.Ajax.requests.at(0).respondWith
- status: 200
- contentType: 'text/html'
- responseText: 'foo'
+ it 'does not emit up:proxy:recover if a delayed up:proxy:slow was never emitted due to a fast response', asyncSpec (next) ->
+ next =>
+ up.proxy.config.slowDelay = 100
+ up.request(url: '/foo')
- jasmine.clock().tick(100)
+ next =>
+ expect(@events).toEqual([
+ 'up:proxy:load'
+ ])
- expect(@events).toEqual([
- 'up:proxy:load',
- 'up:proxy:received'
- ])
+ next.after 50, =>
+ jasmine.Ajax.requests.at(0).respondWith
+ status: 200
+ contentType: 'text/html'
+ responseText: 'foo'
- it 'emits up:proxy:recover if a request returned but failed', ->
+ next.after 150, =>
+ expect(@events).toEqual([
+ 'up:proxy:load',
+ 'up:proxy:loaded'
+ ])
- up.ajax(url: '/foo')
+ it 'emits up:proxy:recover if a request returned but failed', asyncSpec (next) ->
+ next =>
+ up.request(url: '/foo')
- expect(@events).toEqual([
- 'up:proxy:load',
- 'up:proxy:slow'
- ])
+ next =>
+ expect(@events).toEqual([
+ 'up:proxy:load',
+ 'up:proxy:slow'
+ ])
- jasmine.Ajax.requests.at(0).respondWith
- status: 500
- contentType: 'text/html'
- responseText: 'something went wrong'
+ next =>
+ jasmine.Ajax.requests.at(0).respondWith
+ status: 500
+ contentType: 'text/html'
+ responseText: 'something went wrong'
- expect(@events).toEqual([
- 'up:proxy:load',
- 'up:proxy:slow',
- 'up:proxy:received',
- 'up:proxy:recover'
- ])
+ next =>
+ expect(@events).toEqual([
+ 'up:proxy:load',
+ 'up:proxy:slow',
+ 'up:proxy:loaded',
+ 'up:proxy:recover'
+ ])
+ describe 'up.ajax', ->
+
+ it 'fulfills to the response text in order to match the $.ajax() API as good as possible', (done) ->
+ promise = up.ajax('/url')
+
+ u.setTimer 100, =>
+ @respondWith('response-text')
+
+ promise.then (text) ->
+ expect(text).toEqual('response-text')
+
+ done()
+
describe 'up.proxy.preload', ->
describeCapability 'canPushState', ->
- it "loads and caches the given link's destination", ->
- $link = affix('a[href="/path"]')
+ beforeEach ->
+ @requestTarget = => @lastRequest().requestHeaders['X-Up-Target']
+
+ it "loads and caches the given link's destination", asyncSpec (next) ->
+ affix('.target')
+ $link = affix('a[href="/path"][up-target=".target"]')
+
up.proxy.preload($link)
- expect(u.isPromise(up.proxy.get(url: '/path'))).toBe(true)
- it "does not load a link whose method has side-effects", ->
+ next =>
+ cachedPromise = up.proxy.get(url: '/path', target: '.target')
+ expect(u.isPromise(cachedPromise)).toBe(true)
+
+ it "does not load a link whose method has side-effects", asyncSpec (next) ->
$link = affix('a[href="/path"][data-method="post"]')
up.proxy.preload($link)
- expect(up.proxy.get(url: '/path')).toBeUndefined()
+ next => expect(up.proxy.get(url: '/path')).toBeUndefined()
+
+ describe 'for an [up-target] link', ->
+
+ it 'includes the [up-target] selector as an X-Up-Target header if the targeted element is currently on the page', asyncSpec (next) ->
+ affix('.target')
+ $link = affix('a[href="/path"][up-target=".target"]')
+ up.proxy.preload($link)
+ next => expect(@requestTarget()).toEqual('.target')
+
+ 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) ->
+ $link = affix('a[href="/path"][up-target=".target"]')
+ up.proxy.preload($link)
+ # The default fallback would usually be `body`, but in Jasmine specs we change
+ # it to protect the test runner during failures.
+ next => expect(@requestTarget()).toEqual('.default-fallback')
+
+ it 'calls up.request() with a { preload: true } option so it bypasses the concurrency limit', asyncSpec (next) ->
+ requestSpy = spyOn(up, 'request')
+
+ $link = affix('a[href="/path"][up-target=".target"]')
+ up.proxy.preload($link)
+
+ next =>
+ expect(requestSpy).toHaveBeenCalledWith(jasmine.objectContaining(url: '/path', preload: true))
+
+ describe 'for an [up-modal] link', ->
+
+ beforeEach ->
+ up.motion.config.enabled = false
+
+ 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) ->
+ $link = affix('a[href="/path"][up-modal=".target"]')
+ up.proxy.preload($link)
+ next => expect(@requestTarget()).toEqual('.target')
+
+ it 'does not create a modal frame', asyncSpec (next) ->
+ $link = affix('a[href="/path"][up-modal=".target"]')
+ up.proxy.preload($link)
+ next =>
+ expect('.up-modal').not.toExist()
+
+ it 'does not emit an up:modal:open event', asyncSpec (next) ->
+ $link = affix('a[href="/path"][up-modal=".target"]')
+ openListener = jasmine.createSpy('listener')
+ up.on('up:modal:open', openListener)
+ up.proxy.preload($link)
+ next =>
+ expect(openListener).not.toHaveBeenCalled()
+
+ it 'does not close a currently open modal', asyncSpec (next) ->
+ $link = affix('a[href="/path"][up-modal=".target"]')
+ closeListener = jasmine.createSpy('listener')
+ up.on('up:modal:close', closeListener)
+
+ up.modal.extract('.content', '<div class="content">Modal content</div>')
+
+ next =>
+ expect('.up-modal .content').toBeInDOM()
+
+ next =>
+ up.proxy.preload($link)
+
+ next =>
+ expect('.up-modal .content').toBeInDOM()
+ expect(closeListener).not.toHaveBeenCalled()
+
+ next =>
+ up.modal.close()
+
+ next =>
+ expect('.up-modal .content').not.toBeInDOM()
+ expect(closeListener).toHaveBeenCalled()
+
+ it 'does not prevent the opening of other modals while the request is still pending', asyncSpec (next) ->
+ $link = affix('a[href="/path"][up-modal=".target"]')
+ up.proxy.preload($link)
+
+ next =>
+ up.modal.extract('.content', '<div class="content">Modal content</div>')
+
+ next =>
+ expect('.up-modal .content').toBeInDOM()
+
+ it 'calls up.request() with a { preload: true } option so it bypasses the concurrency limit', asyncSpec (next) ->
+ requestSpy = spyOn(up, 'request')
+
+ $link = affix('a[href="/path"][up-modal=".target"]')
+ up.proxy.preload($link)
+
+ next =>
+ expect(requestSpy).toHaveBeenCalledWith(jasmine.objectContaining(url: '/path', preload: true))
+
+ describe 'for an [up-popup] link', ->
+
+ beforeEach ->
+ up.motion.config.enabled = false
+
+ 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) ->
+ $link = affix('a[href="/path"][up-popup=".target"]')
+ up.proxy.preload($link)
+ next => expect(@requestTarget()).toEqual('.target')
+
+
+ it 'does not create a popup frame', asyncSpec (next) ->
+ $link = affix('a[href="/path"][up-popup=".target"]')
+ up.proxy.preload($link)
+ next =>
+ expect('.up-popup').not.toExist()
+
+ it 'does not emit an up:popup:open event', asyncSpec (next) ->
+ $link = affix('a[href="/path"][up-popup=".target"]')
+ openListener = jasmine.createSpy('listener')
+ up.on('up:popup:open', openListener)
+ up.proxy.preload($link)
+ next =>
+ expect(openListener).not.toHaveBeenCalled()
+
+ it 'does not close a currently open popup', asyncSpec (next) ->
+ $link = affix('a[href="/path"][up-popup=".target"]')
+ closeListener = jasmine.createSpy('listener')
+ up.on('up:popup:close', closeListener)
+
+ $existingAnchor = affix('.existing-anchor')
+ up.popup.attach($existingAnchor, target: '.content', html: '<div class="content">popup content</div>')
+
+ next =>
+ expect('.up-popup .content').toBeInDOM()
+
+ next =>
+ up.proxy.preload($link)
+
+ next =>
+ expect('.up-popup .content').toBeInDOM()
+ expect(closeListener).not.toHaveBeenCalled()
+
+ next =>
+ up.popup.close()
+
+ next =>
+ expect('.up-popup .content').not.toBeInDOM()
+ expect(closeListener).toHaveBeenCalled()
+
+ it 'does not prevent the opening of other popups while the request is still pending', asyncSpec (next) ->
+ $link = affix('a[href="/path"][up-popup=".target"]')
+ up.proxy.preload($link)
+
+ next =>
+ $anchor = affix('.existing-anchor')
+ up.popup.attach($anchor, target: '.content', html: '<div class="content">popup content</div>')
+
+ next =>
+ expect('.up-popup .content').toBeInDOM()
+
+ it 'calls up.request() with a { preload: true } option so it bypasses the concurrency limit', asyncSpec (next) ->
+ requestSpy = spyOn(up, 'request')
+
+ $link = affix('a[href="/path"][up-popup=".target"]')
+ up.proxy.preload($link)
+
+ next =>
+ expect(requestSpy).toHaveBeenCalledWith(jasmine.objectContaining(url: '/path', preload: true))
+
describeFallback 'canPushState', ->
- it "does nothing", ->
- $link = affix('a[href="/path"]')
+ it "does nothing", asyncSpec (next) ->
+ affix('.target')
+ $link = affix('a[href="/path"][up-target=".target"]')
up.proxy.preload($link)
- expect(jasmine.Ajax.requests.count()).toBe(0)
+ next =>
+ expect(jasmine.Ajax.requests.count()).toBe(0)
describe 'up.proxy.get', ->
it 'returns an existing cache entry for the given request', ->
- promise1 = up.ajax(url: '/foo', data: { key: 'value' })
+ promise1 = up.request(url: '/foo', data: { key: 'value' })
promise2 = up.proxy.get(url: '/foo', data: { key: 'value' })
expect(promise1).toBe(promise2)
it 'returns undefined if the given request is not cached', ->
promise = up.proxy.get(url: '/foo', data: { key: 'value' })
@@ -398,28 +855,45 @@
describe 'up.proxy.alias', ->
it 'uses an existing cache entry for another request (used in case of redirects)'
+ describe 'up.proxy.remove', ->
+
+ it 'removes the cache entry for the given request'
+
+ it 'does nothing if the given request is not cached'
+
+ it 'does not crash when passed a request with FormData (bugfix)', ->
+ removal = -> up.proxy.remove(url: '/path', data: new FormData())
+ expect(removal).not.toThrowError()
+
describe 'up.proxy.clear', ->
it 'removes all cache entries'
describe 'unobtrusive behavior', ->
describe '[up-preload]', ->
it 'preloads the link destination on mouseover, after a delay'
- it 'triggers a separate AJAX request with a short cache expiry when hovered multiple times', (done) ->
- up.proxy.config.cacheExpiry = 10
+ it 'triggers a separate AJAX request when hovered multiple times and the cache expires between hovers', asyncSpec (next) ->
+ up.proxy.config.cacheExpiry = 50
up.proxy.config.preloadDelay = 0
- spyOn(up, 'follow')
$element = affix('a[href="/foo"][up-preload]')
Trigger.mouseover($element)
- u.setTimer 1, =>
- expect(up.follow.calls.count()).toBe(1)
- u.setTimer 16, =>
- Trigger.mouseover($element)
- u.setTimer 1, =>
- expect(up.follow.calls.count()).toBe(2)
- done()
+
+ next.after 1, =>
+ expect(jasmine.Ajax.requests.count()).toEqual(1)
+
+ next.after 1, =>
+ Trigger.mouseover($element)
+
+ next.after 1, =>
+ expect(jasmine.Ajax.requests.count()).toEqual(1)
+
+ next.after 60, =>
+ Trigger.mouseover($element)
+
+ next.after 1, =>
+ expect(jasmine.Ajax.requests.count()).toEqual(2)