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)