"""
@respond = (options = {}) -> @respondWith(@responseText, options)
it 'replaces the given selector with the same selector from a freshly fetched page', asyncSpec (next) ->
up.replace('.middle', '/path')
next =>
@respond()
next.after 10, =>
expect($('.before')).toHaveText('old-before')
expect($('.middle')).toHaveText('new-middle')
expect($('.after')).toHaveText('old-after')
it 'returns a promise that will be fulfilled once the server response was received and the fragments were swapped', asyncSpec (next) ->
resolution = jasmine.createSpy()
promise = up.replace('.middle', '/path')
promise.then(resolution)
expect(resolution).not.toHaveBeenCalled()
expect($('.middle')).toHaveText('old-middle')
next =>
@respond()
next =>
expect(resolution).toHaveBeenCalled()
expect($('.middle')).toHaveText('new-middle')
describe 'cleaning up', ->
it 'calls destructors on the replaced element', asyncSpec (next) ->
destructor = jasmine.createSpy('destructor')
up.compiler '.container', -> destructor
$container = affix('.container')
up.hello($container)
up.replace('.container', '/path')
next =>
@respondWith '
new text
'
next =>
expect('.container').toHaveText('new text')
expect(destructor).toHaveBeenCalled()
it 'calls destructors when the replaced element is a singleton element like (bugfix)', asyncSpec (next) ->
# shouldSwapElementsDirectly() is true for body, but can't have the example replace the Jasmine test runner UI
up.dom.knife.mock('shouldSwapElementsDirectly').and.callFake ($element) -> $element.is('.container')
destructor = jasmine.createSpy('destructor')
up.compiler '.container', -> destructor
$container = affix('.container')
up.hello($container)
up.replace('.container', '/path')
next =>
@respondWith '
new text
'
next =>
expect('.container').toHaveText('new text')
expect(destructor).toHaveBeenCalled()
describe 'transitions', ->
it 'returns a promise that will be fulfilled once the server response was received and the swap transition has completed', asyncSpec (next) ->
resolution = jasmine.createSpy()
promise = up.replace('.middle', '/path', transition: 'cross-fade', duration: 50)
promise.then(resolution)
expect(resolution).not.toHaveBeenCalled()
expect($('.middle')).toHaveText('old-middle')
next =>
@respond()
expect(resolution).not.toHaveBeenCalled()
next.after 20, =>
expect(resolution).not.toHaveBeenCalled()
next.after 80, =>
expect(resolution).toHaveBeenCalled()
it 'ignores a { transition } option when replacing the body element', asyncSpec (next) ->
up.dom.knife.mock('swapElementsDirectly') # can't have the example replace the Jasmine test runner UI
up.dom.knife.mock('destroy') # if we don't swap the body, up.dom will destroy it
replaceCallback = jasmine.createSpy()
promise = up.replace('body', '/path', transition: 'cross-fade', duration: 50)
promise.then(replaceCallback)
expect(replaceCallback).not.toHaveBeenCalled()
next =>
@responseText = 'new text'
@respond()
next =>
expect(replaceCallback).toHaveBeenCalled()
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.replace('.middle', '/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", asyncSpec (next) ->
givenParams = { 'foo-key': 'foo-value', 'bar-key': 'bar-value' }
up.replace('.middle', '/path', method: 'get', data: givenParams)
next => expect(@lastRequest().url).toMatchUrl('/path?foo-key=foo-value&bar-key=bar-value')
it 'uses a HTTP method given as { method } option', asyncSpec (next) ->
up.replace('.middle', '/path', method: 'put')
next => expect(@lastRequest()).toHaveRequestMethod('PUT')
describe 'when the server responds with an error', ->
it 'replaces the first fallback instead of the given selector', asyncSpec (next) ->
up.dom.config.fallbacks = ['.fallback']
affix('.fallback')
# can't have the example replace the Jasmine test runner UI
extractSpy = up.dom.knife.mock('extract').and.returnValue(Promise.resolve())
next => up.replace('.middle', '/path')
next => @respond(status: 500)
next => expect(extractSpy).toHaveBeenCalledWith('.fallback', jasmine.any(String), jasmine.any(Object))
it 'uses a target selector given as { failTarget } option', asyncSpec (next) ->
next =>
up.replace('.middle', '/path', failTarget: '.after')
next =>
@respond(status: 500)
next =>
expect($('.middle')).toHaveText('old-middle')
expect($('.after')).toHaveText('new-after')
it 'rejects the returned promise', (done) ->
affix('.after')
promise = up.replace('.middle', '/path', failTarget: '.after')
u.nextFrame =>
promiseState(promise).then (result) =>
expect(result.state).toEqual('pending')
@respond(status: 500)
u.nextFrame =>
promiseState(promise).then (result) =>
expect(result.state).toEqual('rejected')
done()
describe 'when the request times out', ->
it "doesn't crash and rejects the returned promise", asyncSpec (next) ->
jasmine.clock().install() # required by responseTimeout()
affix('.target')
promise = up.replace('.middle', '/path', timeout: 50)
next =>
# See that the correct timeout value has been set on the XHR instance
expect(@lastRequest().timeout).toEqual(50)
next.await =>
# See that the promise is still pending before the timeout
promiseState(promise).then (result) -> expect(result.state).toEqual('pending')
next =>
@lastRequest().responseTimeout()
next.await =>
promiseState(promise).then (result) -> expect(result.state).toEqual('rejected')
describe 'when there is a network issue', ->
it "doesn't crash and rejects the returned promise", (done) ->
affix('.target')
promise = up.replace('.middle', '/path')
u.nextFrame =>
promiseState(promise).then (result) =>
expect(result.state).toEqual('pending')
@lastRequest().responseError()
u.nextFrame =>
promiseState(promise).then (result) =>
expect(result.state).toEqual('rejected')
done()
describe 'history', ->
beforeEach ->
up.history.config.enabled = true
it 'should set the browser location to the given URL', (done) ->
promise = up.replace('.middle', '/path')
@respond()
promise.then ->
expect(location.href).toMatchUrl('/path')
done()
it 'does not add a history entry after non-GET requests', asyncSpec (next) ->
up.replace('.middle', '/path', method: 'post')
next => @respond()
next => expect(location.href).toMatchUrl(@hrefBeforeExample)
it 'adds a history entry after non-GET requests if the response includes a { X-Up-Method: "get" } header (will happen after a redirect)', asyncSpec (next) ->
up.replace('.middle', '/requested-path', method: 'post')
next => @respond(responseHeaders:
'X-Up-Method': 'GET'
'X-Up-Location': '/signaled-path'
)
next => expect(location.href).toMatchUrl('/signaled-path')
it 'does not a history entry after a failed GET-request', asyncSpec (next) ->
up.replace('.middle', '/path', method: 'post', failTarget: '.middle')
next => @respond(status: 500)
next => expect(location.href).toMatchUrl(@hrefBeforeExample)
it 'does not add a history entry with { history: false } option', asyncSpec (next) ->
up.replace('.middle', '/path', history: false)
next => @respond()
next => expect(location.href).toMatchUrl(@hrefBeforeExample)
it "detects a redirect's new URL when the server sets an X-Up-Location header", asyncSpec (next) ->
up.replace('.middle', '/path')
next => @respond(responseHeaders: { 'X-Up-Location': '/other-path' })
next => expect(location.href).toMatchUrl('/other-path')
it 'adds params from a { data } option to the URL of a GET request', asyncSpec (next) ->
up.replace('.middle', '/path', data: { 'foo-key': 'foo value', 'bar-key': 'bar value' })
next => @respond()
next => expect(location.href).toMatchUrl('/path?foo-key=foo%20value&bar-key=bar%20value')
describe 'if a URL is given as { history } option', ->
it 'uses that URL as the new location after a GET request', asyncSpec (next) ->
up.replace('.middle', '/path', history: '/given-path')
next => @respond(failTarget: '.middle')
next => expect(location.href).toMatchUrl('/given-path')
it 'adds a history entry after a non-GET request', asyncSpec (next) ->
up.replace('.middle', '/path', method: 'post', history: '/given-path')
next => @respond(failTarget: '.middle')
next => expect(location.href).toMatchUrl('/given-path')
it 'does not add a history entry after a failed non-GET request', asyncSpec (next) ->
up.replace('.middle', '/path', method: 'post', history: '/given-path', failTarget: '.middle')
next => @respond(failTarget: '.middle', status: 500)
next => expect(location.href).toMatchUrl(@hrefBeforeExample)
describe 'source', ->
it 'remembers the source the fragment was retrieved from', (done) ->
promise = up.replace('.middle', '/path')
@respond()
promise.then ->
expect($('.middle').attr('up-source')).toMatch(/\/path$/)
done()
it 'reuses the previous source for a non-GET request (since that is reloadable)', asyncSpec (next) ->
@oldMiddle.attr('up-source', '/previous-source')
up.replace('.middle', '/path', method: 'post')
next =>
@respond()
next =>
expect($('.middle')).toHaveText('new-middle')
expect(up.dom.source('.middle')).toMatchUrl('/previous-source')
describe 'if a URL is given as { source } option', ->
it 'uses that URL as the source for a GET request', asyncSpec (next) ->
up.replace('.middle', '/path', source: '/given-path')
next => @respond()
next => expect(up.dom.source('.middle')).toMatchUrl('/given-path')
it 'uses that URL as the source after a non-GET request', asyncSpec (next) ->
up.replace('.middle', '/path', method: 'post', source: '/given-path')
next => @respond()
next => expect(up.dom.source('.middle')).toMatchUrl('/given-path')
it 'ignores the option and reuses the previous source after a failed non-GET request', asyncSpec (next) ->
@oldMiddle.attr('up-source', '/previous-source')
up.replace('.middle', '/path', method: 'post', source: '/given-path', failTarget: '.middle')
next => @respond(status: 500)
next => expect(up.dom.source('.middle')).toMatchUrl('/previous-source')
describe 'document title', ->
beforeEach ->
up.history.config.enabled = true
it "sets the document title to the response ", asyncSpec (next) ->
affix('.container').text('old container text')
up.replace('.container', '/path')
next =>
@respondWith """
Title from HTML
new container text
"""
next =>
expect($('.container')).toHaveText('new container text')
expect(document.title).toBe('Title from HTML')
it "sets the document title to an 'X-Up-Title' header in the response", asyncSpec (next) ->
affix('.container').text('old container text')
up.replace('.container', '/path')
next =>
@respondWith
responseHeaders:
'X-Up-Title': 'Title from header'
responseText: """
new container text
"""
next =>
expect($('.container')).toHaveText('new container text')
expect(document.title).toBe('Title from header')
it "prefers the X-Up-Title header to the response ", asyncSpec (next) ->
affix('.container').text('old container text')
up.replace('.container', '/path')
next =>
@respondWith
responseHeaders:
'X-Up-Title': 'Title from header'
responseText: """
Title from HTML
new container text
"""
next =>
expect($('.container')).toHaveText('new container text')
expect(document.title).toBe('Title from header')
it "sets the document title to the response with { history: false, title: true } options (bugfix)", asyncSpec (next) ->
affix('.container').text('old container text')
up.replace('.container', '/path', history: false, title: true)
next =>
@respondWith """
Title from HTML
new container text
"""
next =>
expect($('.container')).toHaveText('new container text')
expect(document.title).toBe('Title from HTML')
it 'does not update the document title if the response has a tag inside an inline SVG image (bugfix)', asyncSpec (next) ->
affix('.container').text('old container text')
document.title = 'old document title'
up.replace('.container', '/path', history: false, title: true)
next =>
@respondWith """
new container text
"""
next =>
expect($('.container')).toHaveText('new container text')
expect(document.title).toBe('old document title')
it "does not extract the title from the response or HTTP header if history isn't updated", asyncSpec (next) ->
affix('.container').text('old container text')
document.title = 'old document title'
up.replace('.container', '/path', history: false)
next =>
@respondWith
responseHeaders:
'X-Up-Title': 'Title from header'
responseText: """
Title from HTML
new container text
"""
next =>
expect(document.title).toBe('old document title')
it 'allows to pass an explicit title as { title } option', asyncSpec (next) ->
affix('.container').text('old container text')
up.replace('.container', '/path', title: 'Title from options')
next =>
@respondWith """
Title from HTML
new container text
"""
next =>
expect($('.container')).toHaveText('new container text')
expect(document.title).toBe('Title from options')
describe 'selector processing', ->
it 'replaces multiple selectors separated with a comma', (done) ->
promise = up.replace('.middle, .after', '/path')
@respond()
promise.then ->
expect($('.before')).toHaveText('old-before')
expect($('.middle')).toHaveText('new-middle')
expect($('.after')).toHaveText('new-after')
done()
it 'replaces the body if asked to replace the "html" selector'
it 'prepends instead of replacing when the target has a :before pseudo-selector', (done) ->
promise = up.replace('.middle:before', '/path')
@respond()
promise.then ->
expect($('.before')).toHaveText('old-before')
expect($('.middle')).toHaveText('new-middleold-middle')
expect($('.after')).toHaveText('old-after')
done()
it 'appends instead of replacing when the target has a :after pseudo-selector', (done) ->
promise = up.replace('.middle:after', '/path')
@respond()
promise.then ->
expect($('.before')).toHaveText('old-before')
expect($('.middle')).toHaveText('old-middlenew-middle')
expect($('.after')).toHaveText('old-after')
done()
it "lets the developer choose between replacing/prepending/appending for each selector", (done) ->
promise = up.replace('.before:before, .middle, .after:after', '/path')
@respond()
promise.then ->
expect($('.before')).toHaveText('new-beforeold-before')
expect($('.middle')).toHaveText('new-middle')
expect($('.after')).toHaveText('old-afternew-after')
done()
it 'understands non-standard CSS selector extensions such as :has(...)', (done) ->
$first = affix('.boxx#first')
$firstChild = $('old first').appendTo($first)
$second = affix('.boxx#second')
$secondChild = $('old second').appendTo($second)
promise = up.replace('.boxx:has(.first-child)', '/path')
@respondWith """
new first
"""
promise.then ->
expect($('#first span')).toHaveText('new first')
expect($('#second span')).toHaveText('old second')
done()
describe 'when selectors are missing on the page before the request was made', ->
beforeEach ->
up.dom.config.fallbacks = []
it 'tries selectors from options.fallback before making a request', asyncSpec (next) ->
affix('.box').text('old box')
up.replace('.unknown', '/path', fallback: '.box')
next => @respondWith '
new box
'
next => expect('.box').toHaveText('new box')
it 'rejects the promise if all alternatives are exhausted', (done) ->
promise = up.replace('.unknown', '/path', fallback: '.more-unknown')
promise.catch (e) ->
expect(e).toBeError(/Could not find target in current page/i)
done()
it 'considers a union selector to be missing if one of its selector-atoms are missing', asyncSpec (next) ->
affix('.target').text('old target')
affix('.fallback').text('old fallback')
up.replace('.target, .unknown', '/path', fallback: '.fallback')
next =>
@respondWith """
new target
new fallback
"""
next =>
expect('.target').toHaveText('old target')
expect('.fallback').toHaveText('new fallback')
it 'tries a selector from up.dom.config.fallbacks if options.fallback is missing', asyncSpec (next) ->
up.dom.config.fallbacks = ['.existing']
affix('.existing').text('old existing')
up.replace('.unknown', '/path')
next => @respondWith '
new existing
'
next => expect('.existing').toHaveText('new existing')
it 'does not try a selector from up.dom.config.fallbacks and rejects the promise if options.fallback is false', (done) ->
up.dom.config.fallbacks = ['.existing']
affix('.existing').text('old existing')
up.replace('.unknown', '/path', fallback: false).catch (e) ->
expect(e).toBeError(/Could not find target in current page/i)
done()
describe 'when selectors are missing on the page after the request was made', ->
beforeEach ->
up.dom.config.fallbacks = []
it 'tries selectors from options.fallback before swapping elements', asyncSpec (next) ->
$target = affix('.target').text('old target')
$fallback = affix('.fallback').text('old fallback')
up.replace('.target', '/path', fallback: '.fallback')
$target.remove()
next =>
@respondWith """
new target
new fallback
"""
next =>
expect('.fallback').toHaveText('new fallback')
it 'rejects the promise if all alternatives are exhausted', (done) ->
$target = affix('.target').text('old target')
$fallback = affix('.fallback').text('old fallback')
promise = up.replace('.target', '/path', fallback: '.fallback')
$target.remove()
$fallback.remove()
u.nextFrame =>
@respondWith """
new target
new fallback
"""
u.nextFrame =>
promiseState(promise).then (result) ->
expect(result.state).toEqual('rejected')
expect(result.value).toBeError(/Could not find target in current page/i)
done()
it 'considers a union selector to be missing if one of its selector-atoms are missing', asyncSpec (next) ->
$target = affix('.target').text('old target')
$target2 = affix('.target2').text('old target2')
$fallback = affix('.fallback').text('old fallback')
up.replace('.target, .target2', '/path', fallback: '.fallback')
$target2.remove()
next =>
@respondWith """
new target
new target2
new fallback
"""
next =>
expect('.target').toHaveText('old target')
expect('.fallback').toHaveText('new fallback')
it 'tries a selector from up.dom.config.fallbacks if options.fallback is missing', asyncSpec (next) ->
up.dom.config.fallbacks = ['.fallback']
$target = affix('.target').text('old target')
$fallback = affix('.fallback').text('old fallback')
up.replace('.target', '/path')
$target.remove()
next =>
@respondWith """
new target
new fallback
"""
next =>
expect('.fallback').toHaveText('new fallback')
it 'does not try a selector from up.dom.config.fallbacks and rejects the promise if options.fallback is false', (done) ->
up.dom.config.fallbacks = ['.fallback']
$target = affix('.target').text('old target')
$fallback = affix('.fallback').text('old fallback')
promise = up.replace('.target', '/path', fallback: false)
$target.remove()
u.nextFrame =>
@respondWith """
new target
new fallback
"""
promise.catch (e) ->
expect(e).toBeError(/Could not find target in current page/i)
done()
describe 'when selectors are missing in the response', ->
beforeEach ->
up.dom.config.fallbacks = []
it 'tries selectors from options.fallback before swapping elements', asyncSpec (next) ->
$target = affix('.target').text('old target')
$fallback = affix('.fallback').text('old fallback')
up.replace('.target', '/path', fallback: '.fallback')
next =>
@respondWith """
new fallback
"""
next =>
expect('.target').toHaveText('old target')
expect('.fallback').toHaveText('new fallback')
describe 'if all alternatives are exhausted', ->
it 'rejects the promise', (done) ->
$target = affix('.target').text('old target')
$fallback = affix('.fallback').text('old fallback')
promise = up.replace('.target', '/path', fallback: '.fallback')
u.nextFrame =>
@respondWith '
new unexpected
'
promise.catch (e) ->
expect(e).toBeError(/Could not find target in response/i)
done()
it 'shows a link to open the unexpected response', (done) ->
$target = affix('.target').text('old target')
$fallback = affix('.fallback').text('old fallback')
promise = up.replace('.target', '/path', fallback: '.fallback')
navigate = spyOn(up.browser, 'navigate')
u.nextFrame =>
@respondWith '
new unexpected
'
promise.catch (e) ->
$toast = $('.up-toast')
expect($toast).toExist()
$inspectLink = $toast.find(".up-toast-action:contains('Open response')")
expect($inspectLink).toExist()
expect(navigate).not.toHaveBeenCalled()
Trigger.clickSequence($inspectLink)
u.nextFrame =>
expect(navigate).toHaveBeenCalledWith('/path', {})
done()
it 'considers a union selector to be missing if one of its selector-atoms are missing', asyncSpec (next) ->
$target = affix('.target').text('old target')
$target2 = affix('.target2').text('old target2')
$fallback = affix('.fallback').text('old fallback')
up.replace('.target, .target2', '/path', fallback: '.fallback')
next =>
@respondWith """
new target
new fallback
"""
next =>
expect('.target').toHaveText('old target')
expect('.target2').toHaveText('old target2')
expect('.fallback').toHaveText('new fallback')
it 'tries a selector from up.dom.config.fallbacks if options.fallback is missing', asyncSpec (next) ->
up.dom.config.fallbacks = ['.fallback']
$target = affix('.target').text('old target')
$fallback = affix('.fallback').text('old fallback')
up.replace('.target', '/path')
next =>
@respondWith '
new fallback
'
next =>
expect('.target').toHaveText('old target')
expect('.fallback').toHaveText('new fallback')
it 'does not try a selector from up.dom.config.fallbacks and rejects the promise if options.fallback is false', (done) ->
up.dom.config.fallbacks = ['.fallback']
$target = affix('.target').text('old target')
$fallback = affix('.fallback').text('old fallback')
promise = up.replace('.target', '/path', fallback: false)
u.nextFrame =>
@respondWith '
new fallback
'
promise.catch (e) ->
expect(e).toBeError(/Could not find target in response/i)
done()
describe 'execution of script tags', ->
beforeEach ->
window.scriptTagExecuted = jasmine.createSpy('scriptTagExecuted')
describe 'inline scripts', ->
it 'does not execute inline script tags', (done) ->
@responseText = """
new-middle
"""
promise = up.replace('.middle', '/path')
@respond()
promise.then ->
expect(window.scriptTagExecuted).not.toHaveBeenCalled()
done()
it 'does not crash when the new fragment contains inline script tag that is followed by another sibling (bugfix)', (done) ->
@responseText = """
new-middle-before
new-middle-after
"""
promise = up.replace('.middle', '/path')
@respond()
u.nextFrame ->
promiseState(promise).then (result) ->
expect(result.state).toEqual('fulfilled')
expect(window.scriptTagExecuted).not.toHaveBeenCalled()
done()
describe 'linked scripts', ->
beforeEach ->
# Add a cache-buster to each path so the browser cache is guaranteed to be irrelevant
@linkedScriptPath = "/assets/fixtures/linked_script.js?cache-buster=#{Math.random().toString()}"
it 'does not execute linked scripts to prevent re-inclusion of javascript inserted before the closing body tag', (done) ->
@responseText = """
new-middle
"""
promise = up.replace('.middle', '/path')
@respond()
promise.then =>
# Must respond to this request, since jQuery makes them async: false
if u.contains(@lastRequest().url, 'linked_script')
@respondWith('window.scriptTagExecuted()')
# Now wait for jQuery to parse out