611 строки
18 KiB
JavaScript
611 строки
18 KiB
JavaScript
/* eslint-env mocha */
|
|
|
|
let count
|
|
const responses = {
|
|
'/hello': function () {
|
|
return new Response('<div id="replaced">hello</div>', {
|
|
status: 200,
|
|
headers: {
|
|
'Content-Type': 'text/html; charset=utf-8'
|
|
}
|
|
})
|
|
},
|
|
'/slow-hello': function () {
|
|
return new Promise(resolve => {
|
|
setTimeout(resolve, 100)
|
|
}).then(responses['/hello'])
|
|
},
|
|
'/one-two': function () {
|
|
return new Response('<p id="one">one</p><p id="two">two</p>', {
|
|
status: 200,
|
|
headers: {
|
|
'Content-Type': 'text/html'
|
|
}
|
|
})
|
|
},
|
|
'/blank-type': function () {
|
|
return new Response('<div id="replaced">hello</div>', {
|
|
status: 200,
|
|
headers: {
|
|
'Content-Type': null
|
|
}
|
|
})
|
|
},
|
|
'/boom': function () {
|
|
return new Response('boom', {
|
|
status: 500
|
|
})
|
|
},
|
|
'/count': function () {
|
|
count++
|
|
return new Response(`${count}`, {
|
|
status: 200,
|
|
headers: {
|
|
'Content-Type': 'text/html'
|
|
}
|
|
})
|
|
},
|
|
'/fragment': function (request) {
|
|
if (request.headers.get('Accept') === 'text/fragment+html') {
|
|
return new Response('<div id="fragment">fragment</div>', {
|
|
status: 200,
|
|
headers: {
|
|
'Content-Type': 'text/fragment+html'
|
|
}
|
|
})
|
|
} else {
|
|
return new Response('406', {
|
|
status: 406
|
|
})
|
|
}
|
|
},
|
|
'/test.js': function () {
|
|
return new Response('alert("what")', {
|
|
status: 200,
|
|
headers: {
|
|
'Content-Type': 'text/javascript'
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
function when(el, eventType) {
|
|
return new Promise(function (resolve) {
|
|
el.addEventListener(eventType, resolve)
|
|
})
|
|
}
|
|
|
|
setup(function () {
|
|
count = 0
|
|
window.IncludeFragmentElement.prototype.fetch = function (request) {
|
|
const pathname = new URL(request.url, window.location.origin).pathname
|
|
return Promise.resolve(responses[pathname](request))
|
|
}
|
|
})
|
|
|
|
suite('include-fragment-element', function () {
|
|
teardown(() => {
|
|
document.body.innerHTML = ''
|
|
})
|
|
test('create from document.createElement', function () {
|
|
const el = document.createElement('include-fragment')
|
|
assert.equal('INCLUDE-FRAGMENT', el.nodeName)
|
|
})
|
|
|
|
test('create from constructor', function () {
|
|
const el = new window.IncludeFragmentElement()
|
|
assert.equal('INCLUDE-FRAGMENT', el.nodeName)
|
|
})
|
|
|
|
test('src property', function () {
|
|
const el = document.createElement('include-fragment')
|
|
assert.equal(null, el.getAttribute('src'))
|
|
assert.equal('', el.src)
|
|
|
|
el.src = '/hello'
|
|
assert.equal('/hello', el.getAttribute('src'))
|
|
const link = document.createElement('a')
|
|
link.href = '/hello'
|
|
assert.equal(link.href, el.src)
|
|
})
|
|
|
|
test('initial data is in error state', function () {
|
|
const el = document.createElement('include-fragment')
|
|
|
|
return el.data['catch'](function (error) {
|
|
assert.ok(error)
|
|
})
|
|
})
|
|
|
|
test('data with src property', function () {
|
|
const el = document.createElement('include-fragment')
|
|
el.src = '/hello'
|
|
|
|
return el.data.then(
|
|
function (html) {
|
|
assert.equal('<div id="replaced">hello</div>', html)
|
|
},
|
|
function () {
|
|
assert.ok(false)
|
|
}
|
|
)
|
|
})
|
|
|
|
test('data with src attribute', function () {
|
|
const el = document.createElement('include-fragment')
|
|
el.setAttribute('src', '/hello')
|
|
|
|
return el.data.then(
|
|
function (html) {
|
|
assert.equal('<div id="replaced">hello</div>', html)
|
|
},
|
|
function () {
|
|
assert.ok(false)
|
|
}
|
|
)
|
|
})
|
|
|
|
test('setting data with src property multiple times', function () {
|
|
const el = document.createElement('include-fragment')
|
|
el.src = '/count'
|
|
|
|
return el.data
|
|
.then(function (text) {
|
|
assert.equal('1', text)
|
|
el.src = '/count'
|
|
})
|
|
.then(function () {
|
|
return el.data
|
|
})
|
|
.then(function (text) {
|
|
assert.equal('1', text)
|
|
})
|
|
['catch'](function () {
|
|
assert.ok(false)
|
|
})
|
|
})
|
|
|
|
test('setting data with src attribute multiple times', function () {
|
|
const el = document.createElement('include-fragment')
|
|
el.setAttribute('src', '/count')
|
|
|
|
return el.data
|
|
.then(function (text) {
|
|
assert.equal('1', text)
|
|
el.setAttribute('src', '/count')
|
|
})
|
|
.then(function () {
|
|
return el.data
|
|
})
|
|
.then(function (text) {
|
|
assert.equal('1', text)
|
|
})
|
|
['catch'](function () {
|
|
assert.ok(false)
|
|
})
|
|
})
|
|
|
|
test('throws on incorrect Content-Type', function () {
|
|
const el = document.createElement('include-fragment')
|
|
el.setAttribute('src', '/test.js')
|
|
|
|
return el.data.then(
|
|
() => {
|
|
assert.ok(false)
|
|
},
|
|
error => {
|
|
assert.match(error, /expected text\/html but was text\/javascript/)
|
|
}
|
|
)
|
|
})
|
|
|
|
test('throws on non-matching Content-Type', function () {
|
|
const el = document.createElement('include-fragment')
|
|
el.setAttribute('accept', 'text/fragment+html')
|
|
el.setAttribute('src', '/hello')
|
|
|
|
return el.data.then(
|
|
() => {
|
|
assert.ok(false)
|
|
},
|
|
error => {
|
|
assert.match(error, /expected text\/fragment\+html but was text\/html; charset=utf-8/)
|
|
}
|
|
)
|
|
})
|
|
|
|
test('throws on 406', function () {
|
|
const el = document.createElement('include-fragment')
|
|
el.setAttribute('src', '/fragment')
|
|
|
|
return el.data.then(
|
|
() => {
|
|
assert.ok(false)
|
|
},
|
|
error => {
|
|
assert.match(error, /the server responded with a status of 406/)
|
|
}
|
|
)
|
|
})
|
|
|
|
test('data is not writable', function () {
|
|
const el = document.createElement('include-fragment')
|
|
assert.ok(el.data !== 42)
|
|
try {
|
|
el.data = 42
|
|
} finally {
|
|
assert.ok(el.data !== 42)
|
|
}
|
|
})
|
|
|
|
test('data is not configurable', function () {
|
|
const el = document.createElement('include-fragment')
|
|
assert.ok(el.data !== undefined)
|
|
try {
|
|
delete el.data
|
|
} finally {
|
|
assert.ok(el.data !== undefined)
|
|
}
|
|
})
|
|
|
|
test('replaces element on 200 status', function () {
|
|
const div = document.createElement('div')
|
|
div.innerHTML = '<include-fragment src="/hello">loading</include-fragment>'
|
|
document.body.appendChild(div)
|
|
|
|
return when(div.firstChild, 'load').then(() => {
|
|
assert.equal(document.querySelector('include-fragment'), null)
|
|
assert.equal(document.querySelector('#replaced').textContent, 'hello')
|
|
})
|
|
})
|
|
|
|
test('does not replace element if it has no parent', function () {
|
|
const div = document.createElement('div')
|
|
div.innerHTML = '<include-fragment>loading</include-fragment>'
|
|
document.body.appendChild(div)
|
|
|
|
const fragment = div.firstChild
|
|
fragment.remove()
|
|
fragment.src = '/hello'
|
|
|
|
let didRun = false
|
|
|
|
window.addEventListener('unhandledrejection', function () {
|
|
assert.ok(false)
|
|
})
|
|
|
|
fragment.addEventListener('loadstart', () => {
|
|
didRun = true
|
|
})
|
|
|
|
setTimeout(() => {
|
|
assert.ok(!didRun)
|
|
div.appendChild(fragment)
|
|
}, 10)
|
|
|
|
return when(fragment, 'load').then(() => {
|
|
assert.equal(document.querySelector('#replaced').textContent, 'hello')
|
|
})
|
|
})
|
|
|
|
test('replaces with several new elements on 200 status', function () {
|
|
const div = document.createElement('div')
|
|
div.innerHTML = '<include-fragment src="/one-two">loading</include-fragment>'
|
|
document.body.appendChild(div)
|
|
|
|
return when(div.firstChild, 'load').then(() => {
|
|
assert.equal(document.querySelector('include-fragment'), null)
|
|
assert.equal(document.querySelector('#one').textContent, 'one')
|
|
assert.equal(document.querySelector('#two').textContent, 'two')
|
|
})
|
|
})
|
|
|
|
test('replaces with response with accept header for any', function () {
|
|
const div = document.createElement('div')
|
|
div.innerHTML = '<include-fragment src="/test.js" accept="*/*">loading</include-fragment>'
|
|
document.body.appendChild(div)
|
|
|
|
return when(div.firstChild, 'load').then(() => {
|
|
assert.equal(document.querySelector('include-fragment'), null)
|
|
assert.match(document.body.textContent, /alert\("what"\)/)
|
|
})
|
|
})
|
|
|
|
test('replaces with response with the right accept header', function () {
|
|
const div = document.createElement('div')
|
|
div.innerHTML = '<include-fragment src="/fragment" accept="text/fragment+html">loading</include-fragment>'
|
|
document.body.appendChild(div)
|
|
|
|
return when(div.firstChild, 'load').then(() => {
|
|
assert.equal(document.querySelector('include-fragment'), null)
|
|
assert.equal(document.querySelector('#fragment').textContent, 'fragment')
|
|
})
|
|
})
|
|
|
|
test('error event is not cancelable or bubbles', function () {
|
|
const div = document.createElement('div')
|
|
div.innerHTML = '<include-fragment src="/boom">loading</include-fragment>'
|
|
document.body.appendChild(div)
|
|
|
|
return when(div.firstChild, 'error').then(event => {
|
|
assert.equal(event.bubbles, false)
|
|
assert.equal(event.cancelable, false)
|
|
})
|
|
})
|
|
|
|
test('adds is-error class on 500 status', function () {
|
|
const div = document.createElement('div')
|
|
div.innerHTML = '<include-fragment src="/boom">loading</include-fragment>'
|
|
document.body.appendChild(div)
|
|
|
|
return when(div.firstChild, 'error').then(() =>
|
|
assert.ok(document.querySelector('include-fragment').classList.contains('is-error'))
|
|
)
|
|
})
|
|
|
|
test('adds is-error class on mising Content-Type', function () {
|
|
const div = document.createElement('div')
|
|
div.innerHTML = '<include-fragment src="/blank-type">loading</include-fragment>'
|
|
document.body.appendChild(div)
|
|
|
|
return when(div.firstChild, 'error').then(() =>
|
|
assert.ok(document.querySelector('include-fragment').classList.contains('is-error'))
|
|
)
|
|
})
|
|
|
|
test('adds is-error class on incorrect Content-Type', function () {
|
|
const div = document.createElement('div')
|
|
div.innerHTML = '<include-fragment src="/fragment">loading</include-fragment>'
|
|
document.body.appendChild(div)
|
|
|
|
return when(div.firstChild, 'error').then(() =>
|
|
assert.ok(document.querySelector('include-fragment').classList.contains('is-error'))
|
|
)
|
|
})
|
|
|
|
test('replaces element when src attribute is changed', function () {
|
|
const elem = document.createElement('include-fragment')
|
|
document.body.appendChild(elem)
|
|
|
|
setTimeout(function () {
|
|
elem.src = '/hello'
|
|
}, 10)
|
|
|
|
return when(elem, 'load').then(() => {
|
|
assert.equal(document.querySelector('include-fragment'), null)
|
|
assert.equal(document.querySelector('#replaced').textContent, 'hello')
|
|
})
|
|
})
|
|
|
|
test('fires replaced event', function () {
|
|
const elem = document.createElement('include-fragment')
|
|
document.body.appendChild(elem)
|
|
|
|
setTimeout(function () {
|
|
elem.src = '/hello'
|
|
}, 10)
|
|
|
|
return when(elem, 'include-fragment-replaced').then(() => {
|
|
assert.equal(document.querySelector('include-fragment'), null)
|
|
assert.equal(document.querySelector('#replaced').textContent, 'hello')
|
|
})
|
|
})
|
|
|
|
test('fires events for include-fragment node replacement operations for fragment manipulation', function () {
|
|
const elem = document.createElement('include-fragment')
|
|
document.body.appendChild(elem)
|
|
|
|
setTimeout(function () {
|
|
elem.src = '/hello'
|
|
}, 10)
|
|
|
|
elem.addEventListener('include-fragment-replace', event => {
|
|
event.detail.fragment.querySelector('*').textContent = 'hey'
|
|
})
|
|
|
|
return when(elem, 'include-fragment-replaced').then(() => {
|
|
assert.equal(document.querySelector('include-fragment'), null)
|
|
assert.equal(document.querySelector('#replaced').textContent, 'hey')
|
|
})
|
|
})
|
|
|
|
test('does not replace node if event was canceled ', function () {
|
|
const elem = document.createElement('include-fragment')
|
|
document.body.appendChild(elem)
|
|
|
|
setTimeout(function () {
|
|
elem.src = '/hello'
|
|
}, 10)
|
|
|
|
elem.addEventListener('include-fragment-replace', event => {
|
|
event.preventDefault()
|
|
})
|
|
|
|
return when(elem, 'load').then(() => {
|
|
assert(document.querySelector('include-fragment'), 'Node should not be replaced')
|
|
})
|
|
})
|
|
|
|
suite('event order', () => {
|
|
const originalSetTimeout = window.setTimeout
|
|
setup(() => {
|
|
// Emulate some kind of timer clamping
|
|
let i = 60
|
|
window.setTimeout = (fn, ms, ...rest) => originalSetTimeout.call(window, fn, ms + (i -= 20), ...rest)
|
|
})
|
|
teardown(() => {
|
|
window.setTimeout = originalSetTimeout
|
|
})
|
|
|
|
test('loading events fire in guaranteed order', function () {
|
|
const elem = document.createElement('include-fragment')
|
|
const order = []
|
|
const connected = []
|
|
const events = [
|
|
when(elem, 'loadend').then(() => {
|
|
order.push('loadend')
|
|
connected.push(elem.isConnected)
|
|
}),
|
|
when(elem, 'load').then(() => {
|
|
order.push('load')
|
|
connected.push(elem.isConnected)
|
|
}),
|
|
when(elem, 'loadstart').then(() => {
|
|
order.push('loadstart')
|
|
connected.push(elem.isConnected)
|
|
})
|
|
]
|
|
elem.src = '/hello'
|
|
document.body.appendChild(elem)
|
|
|
|
return Promise.all(events).then(() => {
|
|
assert.deepStrictEqual(order, ['loadstart', 'load', 'loadend'])
|
|
assert.deepStrictEqual(connected, [true, false, false])
|
|
})
|
|
})
|
|
})
|
|
|
|
test('sets loading to "eager" by default', function () {
|
|
const div = document.createElement('div')
|
|
div.innerHTML = '<include-fragment loading="lazy" src="/hello">loading</include-fragment>'
|
|
document.body.appendChild(div)
|
|
|
|
assert(div.firstChild.loading, 'eager')
|
|
})
|
|
|
|
test('loading will return "eager" even if set to junk value', function () {
|
|
const div = document.createElement('div')
|
|
div.innerHTML = '<include-fragment loading="junk" src="/hello">loading</include-fragment>'
|
|
document.body.appendChild(div)
|
|
|
|
assert(div.firstChild.loading, 'eager')
|
|
})
|
|
|
|
test('loading=lazy loads if already visible on page', function () {
|
|
const div = document.createElement('div')
|
|
div.innerHTML = '<include-fragment loading="lazy" src="/hello">loading</include-fragment>'
|
|
document.body.appendChild(div)
|
|
return when(div.firstChild, 'include-fragment-replaced').then(() => {
|
|
assert.equal(document.querySelector('include-fragment'), null)
|
|
assert.equal(document.querySelector('#replaced').textContent, 'hello')
|
|
})
|
|
})
|
|
|
|
test('loading=lazy does not load if not visible on page', function () {
|
|
const div = document.createElement('div')
|
|
div.innerHTML = '<include-fragment loading="lazy" src="/hello">loading</include-fragment>'
|
|
div.hidden = true
|
|
document.body.appendChild(div)
|
|
return Promise.race([
|
|
when(div.firstChild, 'load').then(() => {
|
|
throw new Error('<include-fragment loading=lazy> loaded too early')
|
|
}),
|
|
new Promise(resolve => setTimeout(resolve, 100))
|
|
])
|
|
})
|
|
|
|
test('loading=lazy does not load when src is changed', function () {
|
|
const div = document.createElement('div')
|
|
div.innerHTML = '<include-fragment loading="lazy" src="">loading</include-fragment>'
|
|
div.hidden = true
|
|
document.body.appendChild(div)
|
|
div.firstChild.src = '/hello'
|
|
return Promise.race([
|
|
when(div.firstChild, 'load').then(() => {
|
|
throw new Error('<include-fragment loading=lazy> loaded too early')
|
|
}),
|
|
new Promise(resolve => setTimeout(resolve, 100))
|
|
])
|
|
})
|
|
|
|
test('loading=lazy loads as soon as element visible on page', function () {
|
|
const div = document.createElement('div')
|
|
div.innerHTML = '<include-fragment loading="lazy" src="/hello">loading</include-fragment>'
|
|
div.hidden = true
|
|
let failed = false
|
|
document.body.appendChild(div)
|
|
const fail = () => (failed = true)
|
|
div.firstChild.addEventListener('load', fail)
|
|
|
|
setTimeout(function () {
|
|
div.hidden = false
|
|
div.firstChild.removeEventListener('load', fail)
|
|
}, 100)
|
|
|
|
return when(div.firstChild, 'load').then(() => {
|
|
assert.ok(!failed, 'Load occured too early')
|
|
})
|
|
})
|
|
|
|
test('loading=lazy does not observably change during load cycle', function () {
|
|
const div = document.createElement('div')
|
|
div.innerHTML = '<include-fragment loading="lazy" src="/hello">loading</include-fragment>'
|
|
const elem = div.firstChild
|
|
document.body.appendChild(div)
|
|
|
|
return when(elem, 'loadstart').then(() => {
|
|
assert.equal(elem.loading, 'lazy', 'loading mode changed observably')
|
|
})
|
|
})
|
|
|
|
test('loading=lazy can be switched to eager to load', function () {
|
|
const div = document.createElement('div')
|
|
div.innerHTML = '<include-fragment loading="lazy" src="/hello">loading</include-fragment>'
|
|
div.hidden = true
|
|
let failed = false
|
|
document.body.appendChild(div)
|
|
const fail = () => (failed = true)
|
|
div.firstChild.addEventListener('load', fail)
|
|
|
|
setTimeout(function () {
|
|
div.firstChild.loading = 'eager'
|
|
div.firstChild.removeEventListener('load', fail)
|
|
}, 100)
|
|
|
|
return when(div.firstChild, 'load').then(() => {
|
|
assert.ok(!failed, 'Load occured too early')
|
|
})
|
|
})
|
|
|
|
test('loading=lazy wont load twice even if load is manually called', function () {
|
|
const div = document.createElement('div')
|
|
div.innerHTML = '<include-fragment loading="lazy" src="/slow-hello">loading</include-fragment>'
|
|
div.hidden = true
|
|
document.body.appendChild(div)
|
|
let loadCount = 0
|
|
div.firstChild.addEventListener('loadstart', () => (loadCount += 1))
|
|
const load = div.firstChild.load()
|
|
setTimeout(() => {
|
|
div.hidden = false
|
|
}, 0)
|
|
|
|
return load
|
|
.then(() => when(div.firstChild, 'include-fragment-replaced'))
|
|
.then(() => {
|
|
assert.equal(loadCount, 1, 'Load occured too many times')
|
|
assert.equal(document.querySelector('include-fragment'), null)
|
|
assert.equal(document.querySelector('#replaced').textContent, 'hello')
|
|
})
|
|
})
|
|
|
|
test('include-fragment-replaced is only called once', function () {
|
|
const div = document.createElement('div')
|
|
div.hidden = true
|
|
document.body.append(div)
|
|
|
|
div.innerHTML = `<include-fragment src="/hello">loading</include-fragment>`
|
|
div.firstChild.addEventListener('include-fragment-replaced', () => (loadCount += 1))
|
|
|
|
let loadCount = 0
|
|
setTimeout(() => {
|
|
div.hidden = false
|
|
}, 0)
|
|
|
|
return when(div.firstChild, 'include-fragment-replaced').then(() => {
|
|
assert.equal(loadCount, 1, 'Load occured too many times')
|
|
assert.equal(document.querySelector('include-fragment'), null)
|
|
assert.equal(document.querySelector('#replaced').textContent, 'hello')
|
|
})
|
|
})
|
|
})
|