describe 'up.dom', -> u = up.util describe 'JavaScript functions', -> describe 'up.replace', -> describeCapability 'canPushState', -> beforeEach -> @oldBefore = affix('.before').text('old-before') @oldMiddle = affix('.middle').text('old-middle') @oldAfter = affix('.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') 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 """ <html> <head> <title>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: """ <html> <head> <title>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 """ <html> <head> <title>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 """ <svg width="500" height="300" xmlns="http://www.w3.org/2000/svg"> <g> <title>SVG Title Demo example
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