describe('Core htmx Shadow DOM Tests', function() {
// Skip these tests if browser doesn't support shadow DOM
if (typeof window.ShadowRoot === 'undefined') return
const chai = window.chai
before(function() {
this.initialWorkArea = getWorkArea().outerHTML
})
after(function() {
getWorkArea().outerHTML = this.initialWorkArea
})
beforeEach(function() {
this.server = makeServer()
clearWorkArea()
var workArea = getWorkArea()
if (!workArea.shadowRoot) workArea.attachShadow({ mode: 'open' })
workArea.shadowRoot.innerHTML = ''
})
afterEach(function() {
this.server.restore()
clearWorkArea()
var workArea = getWorkArea()
if (!workArea.shadowRoot) workArea.attachShadow({ mode: 'open' })
workArea.shadowRoot.innerHTML = ''
})
// Locally redefine the `byId` and `make` functions to use shadow DOM
function byId(id) {
return getWorkArea().shadowRoot.getElementById(id) || document.getElementById(id)
}
function make(htmlStr) {
htmlStr = htmlStr.trim()
var makeFn = function() {
var range = document.createRange()
var fragment = range.createContextualFragment(htmlStr)
var wa = getWorkArea().shadowRoot
var child = null
var children = fragment.children || fragment.childNodes // IE
var appendedChildren = []
while (children.length > 0) {
child = children[0]
wa.appendChild(child)
appendedChildren.push(child)
}
htmx.process(wa)
return child // return last added element
}
if (getWorkArea()) {
return makeFn()
} else {
ready(makeFn)
}
}
// special target selector extensions
it('properly retrieves shadow root for extended selector', function() {
var div = make('
')
htmx.defineExtension('test/shadowdom.js', {
init: function(api) {
api.getTarget(div).should.equal(getWorkArea().shadowRoot)
}
})
})
it('properly escapes shadow root for extended selector', function() {
var div = make('')
htmx.defineExtension('test/shadowdom.js', {
init: function(api) {
api.getTarget(div).should.equal(getWorkArea())
}
})
})
it('properly retrives shadow root host for extended selector', function() {
var div = make('')
htmx.defineExtension('test/shadowdom.js', {
init: function(api) {
api.getTarget(div).should.equal(getWorkArea())
}
})
})
// bootstrap test
it('issues a GET request on click and swaps content', function() {
this.server.respondWith('GET', '/test', 'Clicked!')
var btn = make('')
btn.click()
this.server.respond()
btn.innerHTML.should.equal('Clicked!')
})
it('processes inner content properly', function() {
this.server.respondWith('GET', '/test', 'Click Me')
this.server.respondWith('GET', '/test2', 'Clicked!')
var div = make('')
div.click()
this.server.respond()
div.innerHTML.should.equal('Click Me')
var a = div.querySelector('a')
a.click()
this.server.respond()
a.innerHTML.should.equal('Clicked!')
})
it('handles swap outerHTML properly', function() {
this.server.respondWith('GET', '/test', 'Click Me')
this.server.respondWith('GET', '/test2', 'Clicked!')
var div = make('')
div.click()
should.equal(byId('d1'), div)
this.server.respond()
should.equal(byId('d1'), null)
byId('a1').click()
this.server.respond()
byId('a1').innerHTML.should.equal('Clicked!')
})
it('handles beforebegin properly', function() {
var i = 0
this.server.respondWith('GET', '/test', function(xhr) {
i++
xhr.respond(200, {}, '' + i + '')
})
this.server.respondWith('GET', '/test2', '*')
// extra wrapping div here because `ShadowRoot` doesn't support `innerText` or `child.parentElement`
var div = make('
*
').children[0]
var parent = div.parentElement
div.click()
this.server.respond()
div.innerText.should.equal('*')
removeWhiteSpace(parent.innerText).should.equal('1*')
byId('a1').click()
this.server.respond()
removeWhiteSpace(parent.innerText).should.equal('**')
div.click()
this.server.respond()
div.innerText.should.equal('*')
removeWhiteSpace(parent.innerText).should.equal('*2*')
byId('a2').click()
this.server.respond()
removeWhiteSpace(parent.innerText).should.equal('***')
})
it('handles afterbegin properly', function() {
var i = 0
this.server.respondWith('GET', '/test', function(xhr) {
i++
xhr.respond(200, {}, '' + i)
})
var div = make('
*
')
div.click()
this.server.respond()
div.innerText.should.equal('1*')
div.click()
this.server.respond()
div.innerText.should.equal('21*')
div.click()
this.server.respond()
div.innerText.should.equal('321*')
})
it('handles afterbegin properly with no initial content', function() {
var i = 0
this.server.respondWith('GET', '/test', function(xhr) {
i++
xhr.respond(200, {}, '' + i)
})
var div = make('')
div.click()
this.server.respond()
div.innerText.should.equal('1')
div.click()
this.server.respond()
div.innerText.should.equal('21')
div.click()
this.server.respond()
div.innerText.should.equal('321')
})
it('handles afterend properly', function() {
var i = 0
this.server.respondWith('GET', '/test', function(xhr) {
i++
xhr.respond(200, {}, '' + i + '')
})
this.server.respondWith('GET', '/test2', '*')
// extra wrapping div here because `ShadowRoot` doesn't support `innerText` or `child.parentElement`
var div = make('
*
').children[0]
var parent = div.parentElement
div.click()
this.server.respond()
div.innerText.should.equal('*')
removeWhiteSpace(parent.innerText).should.equal('*1')
byId('a1').click()
this.server.respond()
removeWhiteSpace(parent.innerText).should.equal('**')
div.click()
this.server.respond()
div.innerText.should.equal('*')
removeWhiteSpace(parent.innerText).should.equal('*2*')
byId('a2').click()
this.server.respond()
removeWhiteSpace(parent.innerText).should.equal('***')
})
it('handles beforeend properly', function() {
var i = 0
this.server.respondWith('GET', '/test', function(xhr) {
i++
xhr.respond(200, {}, '' + i)
})
var div = make('
*
')
div.click()
this.server.respond()
div.innerText.should.equal('*1')
div.click()
this.server.respond()
div.innerText.should.equal('*12')
div.click()
this.server.respond()
div.innerText.should.equal('*123')
})
it('handles beforeend properly with no initial content', function() {
var i = 0
this.server.respondWith('GET', '/test', function(xhr) {
i++
xhr.respond(200, {}, '' + i)
})
var div = make('')
div.click()
this.server.respond()
div.innerText.should.equal('1')
div.click()
this.server.respond()
div.innerText.should.equal('12')
div.click()
this.server.respond()
div.innerText.should.equal('123')
})
it('handles hx-target properly', function() {
this.server.respondWith('GET', '/test', 'Clicked!')
var btn = make('')
var target = make('Initial')
btn.click()
target.innerHTML.should.equal('Initial')
this.server.respond()
target.innerHTML.should.equal('Clicked!')
})
it('handles 204 NO CONTENT responses properly', function() {
this.server.respondWith('GET', '/test', [204, {}, 'No Content!'])
var btn = make('')
btn.click()
btn.innerHTML.should.equal('Click Me!')
this.server.respond()
btn.innerHTML.should.equal('Click Me!')
})
it('handles 304 NOT MODIFIED responses properly', function() {
this.server.respondWith('GET', '/test-1', [200, {}, 'Content for Tab 1'])
this.server.respondWith('GET', '/test-2', [200, {}, 'Content for Tab 2'])
var target = make('')
var btn1 = make('')
var btn2 = make('')
btn1.click()
target.innerHTML.should.equal('')
this.server.respond()
target.innerHTML.should.equal('Content for Tab 1')
btn2.click()
this.server.respond()
target.innerHTML.should.equal('Content for Tab 2')
this.server.respondWith('GET', '/test-1', [304, {}, 'Content for Tab 1'])
this.server.respondWith('GET', '/test-2', [304, {}, 'Content for Tab 2'])
btn1.click()
this.server.respond()
target.innerHTML.should.equal('Content for Tab 1')
btn2.click()
this.server.respond()
target.innerHTML.should.equal('Content for Tab 2')
})
it('handles hx-trigger with non-default value', function() {
this.server.respondWith('GET', '/test', 'Clicked!')
var form = make('')
form.click()
form.innerHTML.should.equal('Click Me!')
this.server.respond()
form.innerHTML.should.equal('Clicked!')
})
it('handles hx-trigger with load event', function() {
this.server.respondWith('GET', '/test', 'Loaded!')
var div = make('
Load Me!
')
div.innerHTML.should.equal('Load Me!')
this.server.respond()
div.innerHTML.should.equal('Loaded!')
})
it('sets the content type of the request properly', function(done) {
this.server.respondWith('GET', '/test', function(xhr) {
xhr.respond(200, {}, 'done')
xhr.overriddenMimeType.should.equal('text/html')
done()
})
var div = make('
Click Me!
')
div.click()
this.server.respond()
})
it('issues two requests when clicked twice before response', function() {
var i = 1
this.server.respondWith('GET', '/test', function(xhr) {
xhr.respond(200, {}, 'click ' + i)
i++
})
var div = make('')
div.click()
div.click()
this.server.respond()
div.innerHTML.should.equal('click 1')
this.server.respond()
div.innerHTML.should.equal('click 2')
})
it('issues two requests when clicked three times before response', function() {
var i = 1
this.server.respondWith('GET', '/test', function(xhr) {
xhr.respond(200, {}, 'click ' + i)
i++
})
var div = make('')
div.click()
div.click()
div.click()
this.server.respondAll()
div.innerHTML.should.equal('click 2')
})
it('properly handles hx-select for basic situation', function() {
var i = 1
this.server.respondWith('GET', '/test', "
foo
bar
")
var div = make('')
div.click()
this.server.respond()
div.innerHTML.should.equal('
foo
')
})
it('properly handles hx-select for full html document situation', function() {
this.server.respondWith('GET', '/test', "
foo
bar
")
var div = make('')
div.click()
this.server.respond()
div.innerHTML.should.equal('
")
var div = make("")
div.click()
this.server.respond()
window.tempVal.should.equal('After settle...')
} finally {
delete window.callGlobal
delete window.tempVal
}
})
it('script node exceptions do not break rendering', function() {
this.skip('Rendering does not break, but the exception bubbles up and mocha reports it')
this.server.respondWith('GET', '/test', "clicked")
var div = make("")
div.click()
this.server.respond()
div.innerText.should.equal('clicked')
console.log(div.innerText)
console.log('here')
})
it('allows empty verb values', function() {
var path = null
var div = make("")
htmx.on(div, 'htmx:configRequest', function(evt) {
path = evt.detail.path
return false
})
div.click()
this.server.respond()
path.should.not.be.null
})
it('allows blank verb values', function() {
var path = null
var div = make('')
htmx.on(div, 'htmx:configRequest', function(evt) {
path = evt.detail.path
return false
})
div.click()
this.server.respond()
path.should.not.be.null
})
it('input values are not settle swapped (causes flicker)', function() {
this.server.respondWith('GET', '/test', "")
var input = make("")
input.click()
this.server.respond()
input = byId('i1')
input.value.should.equal('bar')
})
it('autofocus attribute works properly', function() {
this.server.respondWith('GET', '/test', "")
var input = make("")
input.focus()
input.click()
getWorkArea().shadowRoot.activeElement.should.equal(input)
this.server.respond()
var input2 = byId('i2')
getWorkArea().shadowRoot.activeElement.should.equal(input2)
})
it('autofocus attribute works properly w/ child', function() {
this.server.respondWith('GET', '/test', "")
var input = make("")
input.focus()
input.click()
getWorkArea().shadowRoot.activeElement.should.equal(input)
this.server.respond()
var input2 = byId('i2')
getWorkArea().shadowRoot.activeElement.should.equal(input2)
})
it('autofocus attribute works properly w/ true value', function() {
this.server.respondWith('GET', '/test', "")
var input = make("")
input.focus()
input.click()
getWorkArea().shadowRoot.activeElement.should.equal(input)
this.server.respond()
var input2 = byId('i2')
getWorkArea().shadowRoot.activeElement.should.equal(input2)
})
it('multipart/form-data encoding works', function() {
this.server.respondWith('POST', '/test', function(xhr) {
should.equal(xhr.requestHeaders['Content-Type'], undefined)
if (xhr.requestBody.get) { // IE 11 does not support
xhr.requestBody.get('i1').should.equal('foo')
}
xhr.respond(200, {}, 'body: ' + xhr.requestBody)
})
var form = make("')
form.focus()
form.click()
this.server.respond()
})
it('removed elements do not issue requests', function() {
var count = 0
this.server.respondWith('GET', '/test', function(xhr) {
count++
xhr.respond(200, {}, '')
})
var btn = make('')
htmx.remove(btn)
btn.click()
this.server.respond()
count.should.equal(0)
})
it('title tags update title', function() {
this.server.respondWith('GET', '/test', function(xhr) {
xhr.respond(200, {}, "htmx rocks!Clicked!")
})
var btn = make('')
btn.click()
this.server.respond()
btn.innerText.should.equal('Clicked!')
window.document.title.should.equal('htmx rocks!')
})
it('svg title tags do not update title', function() {
var originalTitle = window.document.title
this.server.respondWith('GET', '/test', function(xhr) {
xhr.respond(200, {}, "Clicked!')
})
var btn = make('')
btn.click()
this.server.respond()
btn.innerText.should.equal('Clicked!')
window.document.title.should.equal(originalTitle)
})
it('first title tag outside svg title tags updates title', function() {
var originalTitle = window.document.title
var newTitle = originalTitle + '!!!'
this.server.respondWith('GET', '/test', function(xhr) {
xhr.respond(200, {}, "" + newTitle + "Clicked!x")
})
var btn = make('')
btn.click()
this.server.respond()
btn.innerText.should.equal('Clicked!')
window.document.title.should.equal(newTitle)
})
it('title update does not URL escape', function() {
this.server.respondWith('GET', '/test', function(xhr) {
xhr.respond(200, {}, '</> htmx rocks!Clicked!')
})
var btn = make('')
btn.click()
this.server.respond()
btn.innerText.should.equal('Clicked!')
window.document.title.should.equal('> htmx rocks!')
})
it('by default 400 content is not swapped', function() {
this.server.respondWith('GET', '/test', function(xhr) {
xhr.respond(400, {}, 'Clicked!')
})
var btn = make('')
btn.click()
this.server.respond()
btn.innerText.should.equal('Click Me!')
})
it('400 content can be swapped if configured to do so', function() {
var handler = htmx.on('htmx:beforeSwap', function(event) {
if (event.detail.xhr.status === 400) {
event.detail.shouldSwap = true
}
})
this.server.respondWith('GET', '/test', function(xhr) {
xhr.respond(400, {}, 'Clicked!')
})
var btn = make('')
btn.click()
this.server.respond()
btn.innerText.should.equal('Clicked!')
htmx.off('htmx:beforeSwap', handler)
})
it('400 content can be retargeted if configured to do so', function() {
var handler = htmx.on('htmx:beforeSwap', function(event) {
if (event.detail.xhr.status === 400) {
event.detail.shouldSwap = true
event.detail.target = byId('d1')
}
})
this.server.respondWith('GET', '/test', function(xhr) {
xhr.respond(400, {}, 'Clicked!')
})
var btn = make('')
var div = make('')
btn.click()
this.server.respond()
div.innerText.should.equal('Clicked!')
htmx.off('htmx:beforeSwap', handler)
})
it('errors are triggered only on 400+', function() {
var errors = 0
var handler = htmx.on('htmx:responseError', function() {
errors++
})
this.server.respondWith('GET', '/test1', function(xhr) {
xhr.respond(204, {}, 'Clicked!')
})
this.server.respondWith('GET', '/test2', function(xhr) {
xhr.respond(400, {}, 'Clicked!')
})
var btn1 = make('')
var btn2 = make('')
btn1.click()
btn2.click()
this.server.respond()
this.server.respond()
errors.should.equal(1)
htmx.off('htmx:responseError', handler)
})
it('content can be modified if configured to do so', function() {
var handler = htmx.on('htmx:beforeSwap', function(event) {
if (event.detail.xhr.status === 400) {
event.detail.shouldSwap = true
event.detail.serverResponse = event.detail.serverResponse + '!!'
}
})
this.server.respondWith('GET', '/test', function(xhr) {
xhr.respond(400, {}, 'Clicked!')
})
var btn = make('')
btn.click()
this.server.respond()
btn.innerText.should.equal('Clicked!!!')
htmx.off('htmx:beforeSwap', handler)
})
// This test is causing a global leak because that setGlobal.js script fires twice on load
// Skipping only so I can make progress on some other things—this needs to be fixed
it.skip('scripts w/ src attribute are properly loaded', function(done) {
try {
this.server.respondWith('GET', '/test', "")
var div = make("")
div.click()
this.server.respond()
byId('setGlobalScript').addEventListener('load', function() {
window.globalWasCalled.should.equal(true)
delete window.globalWasCalled
done()
})
} finally {
delete window.globalWasCalled
}
})
it('should load tags with colon in their names', function() {
this.server.respondWith('GET', '/test', 'Foobar')
var btn = make('')
btn.click()
this.server.respond()
btn.innerHTML.should.equal('Foobar')
})
it('properly handles clicked submit button with a value inside a htmx form', function() {
var values
this.server.respondWith('Post', '/test', function(xhr) {
values = getParameters(xhr)
xhr.respond(204, {}, '')
})
make('')
byId('submit').click()
this.server.respond()
values.should.deep.equal({ t1: 'textValue', b1: 'buttonValue' })
})
it('properly handles clicked submit input with a value inside a htmx form', function() {
var values
this.server.respondWith('Post', '/test', function(xhr) {
values = getParameters(xhr)
xhr.respond(204, {}, '')
})
make('')
byId('submit').click()
this.server.respond()
values.should.deep.equal({ t1: 'textValue', b1: 'buttonValue' })
})
it('properly handles clicked submit button with a value inside a non-htmx form', function() {
var values
this.server.respondWith('Post', '/test', function(xhr) {
values = getParameters(xhr)
xhr.respond(204, {}, '')
})
make('')
byId('submit').click()
this.server.respond()
values.should.deep.equal({ t1: 'textValue', b1: 'buttonValue' })
})
it('properly handles clicked submit input with a value inside a non-htmx form', function() {
var values
this.server.respondWith('Post', '/test', function(xhr) {
values = getParameters(xhr)
xhr.respond(204, {}, '')
})
make('')
byId('submit').click()
this.server.respond()
values.should.deep.equal({ t1: 'textValue', b1: 'buttonValue' })
})
it('properly handles clicked submit button with a value outside a htmx form', function() {
var values
this.server.respondWith('Post', '/test', function(xhr) {
values = getParameters(xhr)
xhr.respond(204, {}, '')
})
make('' +
'')
byId('submit').click()
this.server.respond()
values.should.deep.equal({ t1: 'textValue', b1: 'buttonValue' })
})
it('properly handles clicked submit input with a value outside a htmx form', function() {
var values
this.server.respondWith('Post', '/test', function(xhr) {
values = getParameters(xhr)
xhr.respond(204, {}, '')
})
make('' +
'')
byId('submit').click()
this.server.respond()
values.should.deep.equal({ t1: 'textValue', b1: 'buttonValue' })
})
it('properly handles clicked submit button with a value stacking with regular input', function() {
var values
this.server.respondWith('Post', '/test', function(xhr) {
values = getParameters(xhr)
xhr.respond(204, {}, '')
})
make('')
byId('btnA').click()
this.server.respond()
values.should.deep.equal({ action: 'A' })
byId('btnB').click()
this.server.respond()
values.should.deep.equal({ action: ['A', 'B'] })
byId('btnC').click()
this.server.respond()
values.should.deep.equal({ action: ['A', 'C'] })
})
it('properly handles clicked submit input with a value stacking with regular input', function() {
var values
this.server.respondWith('Post', '/test', function(xhr) {
values = getParameters(xhr)
xhr.respond(204, {}, '')
})
make('')
byId('btnA').click()
this.server.respond()
values.should.deep.equal({ action: 'A' })
byId('btnB').click()
this.server.respond()
values.should.deep.equal({ action: ['A', 'B'] })
byId('btnC').click()
this.server.respond()
values.should.deep.equal({ action: ['A', 'C'] })
})
it('properly handles clicked submit button with a value inside a form, referencing another form', function() {
var values
this.server.respondWith('Post', '/test', function(xhr) {
values = getParameters(xhr)
xhr.respond(204, {}, '')
})
make('' +
'')
byId('submit').click()
this.server.respond()
values.should.deep.equal({ t1: 'textValue', b1: ['inputValue', 'buttonValue'] })
})
it('properly handles clicked submit input with a value inside a form, referencing another form', function() {
var values
this.server.respondWith('Post', '/test', function(xhr) {
values = getParameters(xhr)
xhr.respond(204, {}, '')
})
make('' +
'')
byId('submit').click()
this.server.respond()
values.should.deep.equal({ t1: 'textValue', b1: ['inputValue', 'buttonValue'] })
})
it('properly handles inputs external to form', function() {
var values
this.server.respondWith('Post', '/test', function(xhr) {
values = getParameters(xhr)
xhr.respond(204, {}, '')
})
make('' +
'' +
'' +
'')
byId('submit').click()
this.server.respond()
values.should.deep.equal({ t1: 'textValue', b1: ['inputValue', 'buttonValue'], s1: 'selectValue' })
})
it('handles form post with button formmethod dialog properly', function() {
var values
this.server.respondWith('POST', '/test', function(xhr) {
values = getParameters(xhr)
xhr.respond(200, {}, '')
})
make('')
byId('submit').click()
this.server.respond()
values.should.deep.equal({ foo: 'bar' })
})
it('handles form get with button formmethod dialog properly', function() {
var responded = false
this.server.respondWith('GET', '/test', function(xhr) {
responded = true
xhr.respond(200, {}, '')
})
make('')
byId('submit').click()
this.server.respond()
responded.should.equal(true)
})
it('can associate submit buttons from outside a form with the current version of the form after swap', function() {
const template = '\n' +
''
var values
this.server.respondWith('/test', function(xhr) {
values = getParameters(xhr)
xhr.respond(200, {}, template)
})
make(template)
const button = byId('outside')
button.focus()
button.click()
this.server.respond()
values.should.deep.equal({ name: '', outside: '' })
button.focus()
button.click()
this.server.respond()
values.should.deep.equal({ name: '', outside: '' })
})
it('can handle basic events w/ no other attributes', function() {
var btn = make("")
btn.click()
window.foo.should.equal(true)
delete window.foo
})
it('can handle basic events w/ no other attributes in child', function() {
var div = make("")
var btn = div.querySelector('#b1')
btn.click()
window.foo.should.equal(true)
delete window.foo
})
it('can target shadow DOM Host and place content below web component', function() {
this.server.respondWith('GET', '/test', '
Clicked!
')
var btn = make('')
btn.click()
this.server.respond()
var r1 = document.getElementById('r1')
r1.innerHTML.should.equal('Clicked!')
r1.remove()
})
it('can target global id outside shadow DOM and place content', function() {
this.server.respondWith('GET', '/test', '
Clicked!
')
var btn = make('')
btn.click()
this.server.respond()
var r2 = document.getElementById('r2')
r2.innerHTML.should.equal('Clicked!')
r2.remove()
})
it('can target shadow DOM Host with outerHTML swap and replace it', function() {
this.server.respondWith('GET', '/test', '