u = up.util
e = up.element
$ = jQuery
describe 'up.fragment', ->
describe 'JavaScript functions', ->
describe 'up.fragment.first', ->
it 'returns the first element with the given selector', ->
match = fixture('.match')
noMatch = fixture('.no-match')
result = up.fragment.first('.match')
expect(result).toBe(match)
it 'returns undefined if there are no matches', ->
result = up.fragment.first('.match')
expect(result).toBeUndefined()
it 'does not return an element that is currently destroying', ->
match = fixture('.match.up-destroying')
result = up.fragment.first('.match')
expect(result).toBeUndefined()
describe 'when given a root element for the search', ->
it 'only matches descendants of that root', ->
parent1 = fixture('.parent1')
parent1Match = e.affix(parent1, '.match')
parent2 = fixture('.parent1')
parent2Match = e.affix(parent2, '.match')
expect(up.fragment.first(parent1, '.match')).toBe(parent1Match)
expect(up.fragment.first(parent2, '.match')).toBe(parent2Match)
describe 'with { origin } option', ->
it 'resolves an & in the selector string with an selector for the origin'
it 'prefers to find an element in the same layer as the origin'
it "returns the element in the top-most layer if there are no matches in the origin's layer"
describe 'with { layer } option', ->
it 'only matches elements in that layer'
describe 'up.replace', ->
describeCapability 'canPushState', ->
beforeEach ->
@$oldBefore = $fixture('.before').text('old-before')
@$oldMiddle = $fixture('.middle').text('old-middle')
@$oldAfter = $fixture('.after').text('old-after')
@responseText =
"""
new-before
new-middle
new-after
"""
@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')
it 'allows to pass an element instead of a selector', asyncSpec (next) ->
up.replace(@$oldMiddle, '/path')
next =>
@respond()
next =>
expect($('.before')).toHaveText('old-before')
expect($('.middle')).toHaveText('new-middle')
expect($('.after')).toHaveText('old-after')
describe 'with { transition } option', ->
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: 200)
promise.then(resolution)
expect(resolution).not.toHaveBeenCalled()
expect($('.middle')).toHaveText('old-middle')
next =>
@respond()
expect(resolution).not.toHaveBeenCalled()
next.after 100, =>
expect(resolution).not.toHaveBeenCalled()
next.after 200, =>
expect(resolution).toHaveBeenCalled()
describe 'with { params } 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', params: 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', params: 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.fragment.config.fallbacks = ['.fallback']
$fixture('.fallback')
# can't have the example replace the Jasmine test runner UI
extractSpy = up.fragment.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) ->
$fixture('.after')
promise = up.replace('.middle', '/path', failTarget: '.after')
u.task =>
promiseState(promise).then (result) =>
expect(result.state).toEqual('pending')
@respond(status: 500)
u.task =>
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()
$fixture('.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) ->
$fixture('.target')
promise = up.replace('.middle', '/path')
u.task =>
promiseState(promise).then (result) =>
expect(result.state).toEqual('pending')
@lastRequest().responseError()
u.task =>
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 { params } option to the URL of a GET request', asyncSpec (next) ->
up.replace('.middle', '/path', params: { '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.fragment.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.fragment.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.fragment.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.fragment.source('.middle')).toMatchUrl('/previous-source')
describe 'document title', ->
beforeEach ->
up.history.config.enabled = true
it "sets the document title to the response ", asyncSpec (next) ->
$fixture('.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) ->
$fixture('.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) ->
$fixture('.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) ->
$fixture('.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) ->
$fixture('.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) ->
$fixture('.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) ->
$fixture('.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()
describe 'nested selector merging', ->
it 'replaces a single fragment if a selector contains a subsequent selector in the current page', asyncSpec (next) ->
$outer = $fixture('.outer').text('old outer text')
$inner = $outer.affix('.inner').text('old inner text')
replacePromise = up.replace('.outer, .inner', '/path')
next =>
expect(@lastRequest().requestHeaders['X-Up-Target']).toEqual('.outer')
@respondWith """
new outer text
new inner text
"""
next =>
expect($('.outer')).toBeAttached()
expect($('.outer').text()).toContain('new outer text')
expect($('.inner')).toBeAttached()
expect($('.inner').text()).toContain('new inner text')
next.await =>
promise = promiseState(replacePromise)
promise.then (result) => expect(result.state).toEqual('fulfilled')
it 'does not merge selectors if a selector contains a subsequent selector, but prepends instead of replacing', asyncSpec (next) ->
$outer = $fixture('.outer').text('old outer text')
$inner = $outer.affix('.inner').text('old inner text')
replacePromise = up.replace('.outer:before, .inner', '/path')
next =>
expect(@lastRequest().requestHeaders['X-Up-Target']).toEqual('.outer:before, .inner')
@respondWith """
new outer text
new inner text
"""
next =>
expect($('.outer')).toBeAttached()
expect($('.outer').text()).toContain('new outer text')
expect($('.outer').text()).toContain('old outer text')
expect($('.inner')).toBeAttached()
expect($('.inner').text()).toContain('new inner text')
next.await =>
promise = promiseState(replacePromise)
promise.then (result) => expect(result.state).toEqual('fulfilled')
it 'does not merge selectors if a selector contains a subsequent selector, but appends instead of replacing', asyncSpec (next) ->
$outer = $fixture('.outer').text('old outer text')
$inner = $outer.affix('.inner').text('old inner text')
replacePromise = up.replace('.outer:after, .inner', '/path')
next =>
expect(@lastRequest().requestHeaders['X-Up-Target']).toEqual('.outer:after, .inner')
@respondWith """
new outer text
new inner text
"""
next =>
expect($('.outer')).toBeAttached()
expect($('.outer').text()).toContain('old outer text')
expect($('.outer').text()).toContain('new outer text')
expect($('.inner')).toBeAttached()
expect($('.inner').text()).toContain('new inner text')
next.await =>
promise = promiseState(replacePromise)
promise.then (result) => expect(result.state).toEqual('fulfilled')
it 'does not lose selector pseudo-classes when merging selectors (bugfix)', asyncSpec (next) ->
$outer = $fixture('.outer').text('old outer text')
$inner = $outer.affix('.inner').text('old inner text')
replacePromise = up.replace('.outer:after, .inner', '/path')
next =>
expect(@lastRequest().requestHeaders['X-Up-Target']).toEqual('.outer:after, .inner')
it 'replaces a single fragment if a selector contains a previous selector in the current page', asyncSpec (next) ->
$outer = $fixture('.outer').text('old outer text')
$inner = $outer.affix('.inner').text('old inner text')
replacePromise = up.replace('.outer, .inner', '/path')
next =>
expect(@lastRequest().requestHeaders['X-Up-Target']).toEqual('.outer')
@respondWith """
new outer text
new inner text
"""
next =>
expect($('.outer')).toBeAttached()
expect($('.outer').text()).toContain('new outer text')
expect($('.inner')).toBeAttached()
expect($('.inner').text()).toContain('new inner text')
next.await =>
promise = promiseState(replacePromise)
promise.then (result) => expect(result.state).toEqual('fulfilled')
it 'does not lose a { reveal: true } option if the first selector was merged into a subsequent selector', asyncSpec (next) ->
revealStub = up.viewport.knife.mock('reveal')
$outer = $fixture('.outer').text('old outer text')
$inner = $outer.affix('.inner').text('old inner text')
up.replace('.inner, .outer', '/path', reveal: true)
next =>
expect(@lastRequest().requestHeaders['X-Up-Target']).toEqual('.outer')
@respondWith """
new outer text
new inner text
"""
next =>
expect($('.outer')).toBeAttached()
expect($('.outer').text()).toContain('new outer text')
expect($('.inner')).toBeAttached()
expect($('.inner').text()).toContain('new inner text')
expect(revealStub).toHaveBeenCalled()
it 'does not lose a { reveal: string } option if the first selector was merged into a subsequent selector', asyncSpec (next) ->
revealStub = up.viewport.knife.mock('reveal')
$outer = $fixture('.outer').text('old outer text')
$inner = $outer.affix('.inner').text('old inner text')
up.replace('.inner, .outer', '/path', reveal: '.revealee')
next =>
expect(@lastRequest().requestHeaders['X-Up-Target']).toEqual('.outer')
@respondWith """
new outer text
new inner text
revealee text
"""
next =>
expect($('.outer')).toBeAttached()
expect($('.outer').text()).toContain('new outer text')
expect($('.inner')).toBeAttached()
expect($('.inner').text()).toContain('new inner text')
expect(revealStub).toHaveBeenCalled()
revealArg = revealStub.calls.mostRecent().args[0]
expect(revealArg).toMatchSelector('.revealee')
it 'replaces a single fragment if the nesting differs in current page and response', asyncSpec (next) ->
$outer = $fixture('.outer').text('old outer text')
$inner = $outer.affix('.inner').text('old inner text')
replacePromise = up.replace('.outer, .inner', '/path')
next =>
@respondWith """
new inner text
new outer text
"""
next =>
expect($('.outer')).toBeAttached()
expect($('.outer').text()).toContain('new outer text')
expect($('.inner')).not.toBeAttached()
next.await =>
promise = promiseState(replacePromise)
promise.then (result) => expect(result.state).toEqual('fulfilled')
it 'does not crash if two selectors that are siblings in the current page are nested in the response', asyncSpec (next) ->
$outer = $fixture('.one').text('old one text')
$inner = $fixture('.two').text('old two text')
replacePromise = up.replace('.one, .two', '/path')
next =>
@respondWith """
new one text
new two text
"""
next =>
expect($('.one')).toBeAttached()
expect($('.one').text()).toContain('new one text')
expect($('.two')).toBeAttached()
expect($('.two').text()).toContain('new two text')
next.await =>
promise = promiseState(replacePromise)
promise.then (result) => expect(result.state).toEqual('fulfilled')
it 'does not crash if selectors that siblings in the current page are inversely nested in the response', asyncSpec (next) ->
$outer = $fixture('.one').text('old one text')
$inner = $fixture('.two').text('old two text')
replacePromise = up.replace('.one, .two', '/path')
next =>
@respondWith """
new two text
new one text
"""
next =>
expect($('.one')).toBeAttached()
expect($('.one').text()).toContain('new one text')
expect($('.two')).toBeAttached()
expect($('.two').text()).toContain('new two text')
next.await =>
promise = promiseState(replacePromise)
promise.then (result) => expect(result.state).toEqual('fulfilled')
it 'updates the first selector if the same element is targeted twice in a single replacement', asyncSpec (next) ->
$one = $fixture('.one.alias').text('old one text')
replacePromise = up.replace('.one, .alias', '/path')
next =>
expect(@lastRequest().requestHeaders['X-Up-Target']).toEqual('.one')
@respondWith """
new one text
"""
next =>
expect($('.one')).toBeAttached()
expect($('.one').text()).toContain('new one text')
next.await =>
promise = promiseState(replacePromise)
promise.then (result) => expect(result.state).toEqual('fulfilled')
it 'updates the first selector if the same element is prepended or appended twice in a single replacement', asyncSpec (next) ->
$one = $fixture('.one').text('old one text')
replacePromise = up.replace('.one:before, .one:after', '/path')
next =>
expect(@lastRequest().requestHeaders['X-Up-Target']).toEqual('.one:before')
@respondWith """
new one text
"""
next =>
expect($('.one')).toBeAttached()
expect($('.one').text()).toMatchText('new one text old one text')
next.await =>
promise = promiseState(replacePromise)
promise.then (result) => expect(result.state).toEqual('fulfilled')
it "updates the first selector if the same element is prepended, replaced and appended in a single replacement", asyncSpec (next) ->
$elem = $fixture('.elem.alias1.alias2').text("old text")
replacePromise = up.replace('.elem:before, .alias1, .alias2:after', '/path')
next =>
expect(@lastRequest().requestHeaders['X-Up-Target']).toEqual('.elem:before')
@respondWith """
new text
"""
next =>
expect('.elem').toBeAttached()
expect($('.elem').text()).toMatchText('new text old text')
next.await =>
promise = promiseState(replacePromise)
promise.then (result) => expect(result.state).toEqual('fulfilled')
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 = $fixture('.boxx#first')
$firstChild = $('old first').appendTo($first)
$second = $fixture('.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.fragment.config.fallbacks = []
it 'tries selectors from options.fallback before making a request', asyncSpec (next) ->
$fixture('.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) ->
$fixture('.target').text('old target')
$fixture('.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.fragment.config.fallbacks if options.fallback is missing', asyncSpec (next) ->
up.fragment.config.fallbacks = ['.existing']
$fixture('.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.fragment.config.fallbacks and rejects the promise if options.fallback is false', (done) ->
up.fragment.config.fallbacks = ['.existing']
$fixture('.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.fragment.config.fallbacks = []
it 'tries selectors from options.fallback before swapping elements', asyncSpec (next) ->
$target = $fixture('.target').text('old target')
$fallback = $fixture('.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 = $fixture('.target').text('old target')
$fallback = $fixture('.fallback').text('old fallback')
promise = up.replace('.target', '/path', fallback: '.fallback')
$target.remove()
$fallback.remove()
u.task =>
@respondWith """
new target
new fallback
"""
u.task =>
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 = $fixture('.target').text('old target')
$target2 = $fixture('.target2').text('old target2')
$fallback = $fixture('.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.fragment.config.fallbacks if options.fallback is missing', asyncSpec (next) ->
up.fragment.config.fallbacks = ['.fallback']
$target = $fixture('.target').text('old target')
$fallback = $fixture('.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.fragment.config.fallbacks and rejects the promise if options.fallback is false', (done) ->
up.fragment.config.fallbacks = ['.fallback']
$target = $fixture('.target').text('old target')
$fallback = $fixture('.fallback').text('old fallback')
promise = up.replace('.target', '/path', fallback: false)
$target.remove()
u.task =>
@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.fragment.config.fallbacks = []
it 'tries selectors from options.fallback before swapping elements', asyncSpec (next) ->
$target = $fixture('.target').text('old target')
$fallback = $fixture('.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 = $fixture('.target').text('old target')
$fallback = $fixture('.fallback').text('old fallback')
promise = up.replace('.target', '/path', fallback: '.fallback')
u.task =>
@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 = $fixture('.target').text('old target')
$fallback = $fixture('.fallback').text('old fallback')
promise = up.replace('.target', '/path', fallback: '.fallback')
navigate = spyOn(up.browser, 'navigate')
u.task =>
@respondWith '
new unexpected
'
promise.catch (e) ->
$toast = $('.up-toast')
expect($toast).toBeAttached()
$inspectLink = $toast.find(".up-toast-action:contains('Open response')")
expect($inspectLink).toBeAttached()
expect(navigate).not.toHaveBeenCalled()
Trigger.clickSequence($inspectLink)
u.task =>
expect(navigate).toHaveBeenCalledWith('/path', {})
done()
it 'considers a union selector to be missing if one of its selector-atoms are missing', asyncSpec (next) ->
$target = $fixture('.target').text('old target')
$target2 = $fixture('.target2').text('old target2')
$fallback = $fixture('.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.fragment.config.fallbacks if options.fallback is missing', asyncSpec (next) ->
up.fragment.config.fallbacks = ['.fallback']
$target = $fixture('.target').text('old target')
$fallback = $fixture('.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.fragment.config.fallbacks and rejects the promise if options.fallback is false', (done) ->
up.fragment.config.fallbacks = ['.fallback']
$target = $fixture('.target').text('old target')
$fallback = $fixture('.fallback').text('old fallback')
promise = up.replace('.target', '/path', fallback: false)
u.task =>
@respondWith '
new fallback
'
promise.catch (e) ->
expect(e).toBeError(/Could not find target in response/i)
done()
describe 'execution of scripts', ->
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.task ->
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