You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1243 lines
50 KiB
TypeScript
1243 lines
50 KiB
TypeScript
import { assert } from 'chai'
|
|
import shuffle from 'lodash.shuffle'
|
|
|
|
import { init, classModule, propsModule, styleModule, eventListenersModule, h, toVNode, vnode, VNode, htmlDomApi, CreateHook, InsertHook, PrePatchHook, RemoveHook, InitHook, DestroyHook, UpdateHook } from '../../src/index';
|
|
|
|
const hasSvgClassList = 'classList' in SVGElement.prototype
|
|
|
|
const patch = init([
|
|
classModule,
|
|
propsModule,
|
|
eventListenersModule
|
|
])
|
|
|
|
function prop<T> (name: string) {
|
|
return function (obj: {[index: string]: T}) {
|
|
return obj[name]
|
|
}
|
|
}
|
|
|
|
function map (fn: any, list: any[]) {
|
|
const ret = []
|
|
for (let i = 0; i < list.length; ++i) {
|
|
ret[i] = fn(list[i])
|
|
}
|
|
return ret
|
|
}
|
|
|
|
const inner = prop('innerHTML')
|
|
|
|
class A extends HTMLParagraphElement {}
|
|
class B extends HTMLParagraphElement {}
|
|
|
|
describe('snabbdom', function () {
|
|
before(function () {
|
|
customElements.define('p-a', A, { extends: 'p' })
|
|
customElements.define('p-b', B, { extends: 'p' })
|
|
})
|
|
|
|
let elm: any, vnode0: any
|
|
beforeEach(function () {
|
|
elm = document.createElement('div')
|
|
vnode0 = elm
|
|
})
|
|
|
|
describe('hyperscript', function () {
|
|
it('can create vnode with proper tag', function () {
|
|
assert.strictEqual(h('div').sel, 'div')
|
|
assert.strictEqual(h('a').sel, 'a')
|
|
})
|
|
it('can create vnode with children', function () {
|
|
const vnode = h('div', [h('span#hello'), h('b.world')])
|
|
assert.strictEqual(vnode.sel, 'div')
|
|
const children = vnode.children as [VNode, VNode]
|
|
assert.strictEqual(children[0].sel, 'span#hello')
|
|
assert.strictEqual(children[1].sel, 'b.world')
|
|
})
|
|
it('can create vnode with one child vnode', function () {
|
|
const vnode = h('div', h('span#hello'))
|
|
assert.strictEqual(vnode.sel, 'div')
|
|
const children = vnode.children as [VNode]
|
|
assert.strictEqual(children[0].sel, 'span#hello')
|
|
})
|
|
it('can create vnode with props and one child vnode', function () {
|
|
const vnode = h('div', {}, h('span#hello'))
|
|
assert.strictEqual(vnode.sel, 'div')
|
|
const children = vnode.children as [VNode]
|
|
assert.strictEqual(children[0].sel, 'span#hello')
|
|
})
|
|
it('can create vnode with text content', function () {
|
|
const vnode = h('a', ['I am a string'])
|
|
const children = vnode.children as [VNode]
|
|
assert.strictEqual(children[0].text, 'I am a string')
|
|
})
|
|
it('can create vnode with text content in string', function () {
|
|
const vnode = h('a', 'I am a string')
|
|
assert.strictEqual(vnode.text, 'I am a string')
|
|
})
|
|
it('can create vnode with props and text content in string', function () {
|
|
const vnode = h('a', {}, 'I am a string')
|
|
assert.strictEqual(vnode.text, 'I am a string')
|
|
})
|
|
it('can create vnode with null props', function () {
|
|
let vnode = h('a', null)
|
|
assert.deepEqual(vnode.data, {})
|
|
vnode = h('a', null, ['I am a string'])
|
|
const children = vnode.children as [VNode]
|
|
assert.strictEqual(children[0].text, 'I am a string')
|
|
})
|
|
it('can create vnode for comment', function () {
|
|
const vnode = h('!', 'test')
|
|
assert.strictEqual(vnode.sel, '!')
|
|
assert.strictEqual(vnode.text, 'test')
|
|
})
|
|
})
|
|
describe('created element', function () {
|
|
it('has tag', function () {
|
|
elm = patch(vnode0, h('div')).elm
|
|
assert.strictEqual(elm.tagName, 'DIV')
|
|
})
|
|
it('has different tag and id', function () {
|
|
const elm = document.createElement('div')
|
|
vnode0.appendChild(elm)
|
|
const vnode1 = h('span#id')
|
|
const patched = patch(elm, vnode1).elm as HTMLSpanElement
|
|
assert.strictEqual(patched.tagName, 'SPAN')
|
|
assert.strictEqual(patched.id, 'id')
|
|
})
|
|
it('has id', function () {
|
|
elm = patch(vnode0, h('div', [h('div#unique')])).elm
|
|
assert.strictEqual(elm.firstChild.id, 'unique')
|
|
})
|
|
it('has correct namespace', function () {
|
|
const SVGNamespace = 'http://www.w3.org/2000/svg'
|
|
const XHTMLNamespace = 'http://www.w3.org/1999/xhtml'
|
|
|
|
elm = patch(vnode0, h('div', [h('div', { ns: SVGNamespace })])).elm
|
|
assert.strictEqual(elm.firstChild.namespaceURI, SVGNamespace)
|
|
|
|
// verify that svg tag automatically gets svg namespace
|
|
elm = patch(vnode0, h('svg', [
|
|
h('foreignObject', [
|
|
h('div', ['I am HTML embedded in SVG'])
|
|
])
|
|
])).elm
|
|
assert.strictEqual(elm.namespaceURI, SVGNamespace)
|
|
assert.strictEqual(elm.firstChild.namespaceURI, SVGNamespace)
|
|
assert.strictEqual(elm.firstChild.firstChild.namespaceURI, XHTMLNamespace)
|
|
|
|
// verify that svg tag with extra selectors gets svg namespace
|
|
elm = patch(vnode0, h('svg#some-id')).elm
|
|
assert.strictEqual(elm.namespaceURI, SVGNamespace)
|
|
|
|
// verify that non-svg tag beginning with 'svg' does NOT get namespace
|
|
elm = patch(vnode0, h('svg-custom-el')).elm
|
|
assert.notStrictEqual(elm.namespaceURI, SVGNamespace)
|
|
})
|
|
it('receives classes in selector', function () {
|
|
elm = patch(vnode0, h('div', [h('i.am.a.class')])).elm
|
|
assert(elm.firstChild.classList.contains('am'))
|
|
assert(elm.firstChild.classList.contains('a'))
|
|
assert(elm.firstChild.classList.contains('class'))
|
|
})
|
|
it('receives classes in class property', function () {
|
|
elm = patch(vnode0, h('i', { class: { am: true, a: true, class: true, not: false } })).elm
|
|
assert(elm.classList.contains('am'))
|
|
assert(elm.classList.contains('a'))
|
|
assert(elm.classList.contains('class'))
|
|
assert(!elm.classList.contains('not'))
|
|
})
|
|
it('receives classes in selector when namespaced', function () {
|
|
if (!hasSvgClassList) {
|
|
this.skip()
|
|
} else {
|
|
elm = patch(vnode0,
|
|
h('svg', [
|
|
h('g.am.a.class.too')
|
|
])
|
|
).elm
|
|
assert(elm.firstChild.classList.contains('am'))
|
|
assert(elm.firstChild.classList.contains('a'))
|
|
assert(elm.firstChild.classList.contains('class'))
|
|
}
|
|
})
|
|
it('receives classes in class property when namespaced', function () {
|
|
if (!hasSvgClassList) {
|
|
this.skip()
|
|
} else {
|
|
elm = patch(vnode0,
|
|
h('svg', [
|
|
h('g', { class: { am: true, a: true, class: true, not: false, too: true } })
|
|
])
|
|
).elm
|
|
assert(elm.firstChild.classList.contains('am'))
|
|
assert(elm.firstChild.classList.contains('a'))
|
|
assert(elm.firstChild.classList.contains('class'))
|
|
assert(!elm.firstChild.classList.contains('not'))
|
|
}
|
|
})
|
|
it('handles classes from both selector and property', function () {
|
|
elm = patch(vnode0, h('div', [h('i.has', { class: { classes: true } })])).elm
|
|
assert(elm.firstChild.classList.contains('has'))
|
|
assert(elm.firstChild.classList.contains('classes'))
|
|
})
|
|
it('can create custom elements', function () {
|
|
const vnode1 = h('p', { is: 'p-a' })
|
|
elm = patch(vnode0, vnode1).elm
|
|
assert(elm instanceof A)
|
|
})
|
|
it('can create elements with text content', function () {
|
|
elm = patch(vnode0, h('div', ['I am a string'])).elm
|
|
assert.strictEqual(elm.innerHTML, 'I am a string')
|
|
})
|
|
it('can create elements with span and text content', function () {
|
|
elm = patch(vnode0, h('a', [h('span'), 'I am a string'])).elm
|
|
assert.strictEqual(elm.childNodes[0].tagName, 'SPAN')
|
|
assert.strictEqual(elm.childNodes[1].textContent, 'I am a string')
|
|
})
|
|
it('can create elements with props', function () {
|
|
elm = patch(vnode0, h('a', { props: { src: 'http://localhost/' } })).elm
|
|
assert.strictEqual(elm.src, 'http://localhost/')
|
|
})
|
|
it('can create an element created inside an iframe', function (done) {
|
|
// Only run if srcdoc is supported.
|
|
const frame = document.createElement('iframe')
|
|
if (typeof frame.srcdoc !== 'undefined') {
|
|
frame.srcdoc = '<div>Thing 1</div>'
|
|
frame.onload = function () {
|
|
const div0 = frame.contentDocument!.body.querySelector('div') as HTMLDivElement
|
|
patch(div0, h('div', 'Thing 2'))
|
|
const div1 = frame.contentDocument!.body.querySelector('div') as HTMLDivElement
|
|
assert.strictEqual(div1.textContent, 'Thing 2')
|
|
frame.remove()
|
|
done()
|
|
}
|
|
document.body.appendChild(frame)
|
|
} else {
|
|
done()
|
|
}
|
|
})
|
|
it('is a patch of the root element', function () {
|
|
const elmWithIdAndClass = document.createElement('div')
|
|
elmWithIdAndClass.id = 'id'
|
|
elmWithIdAndClass.className = 'class'
|
|
const vnode1 = h('div#id.class', [h('span', 'Hi')])
|
|
elm = patch(elmWithIdAndClass, vnode1).elm
|
|
assert.strictEqual(elm, elmWithIdAndClass)
|
|
assert.strictEqual(elm.tagName, 'DIV')
|
|
assert.strictEqual(elm.id, 'id')
|
|
assert.strictEqual(elm.className, 'class')
|
|
})
|
|
it('can create comments', function () {
|
|
elm = patch(vnode0, h('!', 'test')).elm
|
|
assert.strictEqual(elm.nodeType, document.COMMENT_NODE)
|
|
assert.strictEqual(elm.textContent, 'test')
|
|
})
|
|
})
|
|
describe('patching an element', function () {
|
|
it('changes the elements classes', function () {
|
|
const vnode1 = h('i', { class: { i: true, am: true, horse: true } })
|
|
const vnode2 = h('i', { class: { i: true, am: true, horse: false } })
|
|
patch(vnode0, vnode1)
|
|
elm = patch(vnode1, vnode2).elm
|
|
assert(elm.classList.contains('i'))
|
|
assert(elm.classList.contains('am'))
|
|
assert(!elm.classList.contains('horse'))
|
|
})
|
|
it('changes classes in selector', function () {
|
|
const vnode1 = h('i', { class: { i: true, am: true, horse: true } })
|
|
const vnode2 = h('i', { class: { i: true, am: true, horse: false } })
|
|
patch(vnode0, vnode1)
|
|
elm = patch(vnode1, vnode2).elm
|
|
assert(elm.classList.contains('i'))
|
|
assert(elm.classList.contains('am'))
|
|
assert(!elm.classList.contains('horse'))
|
|
})
|
|
it('preserves memoized classes', function () {
|
|
const cachedClass = { i: true, am: true, horse: false }
|
|
const vnode1 = h('i', { class: cachedClass })
|
|
const vnode2 = h('i', { class: cachedClass })
|
|
elm = patch(vnode0, vnode1).elm
|
|
assert(elm.classList.contains('i'))
|
|
assert(elm.classList.contains('am'))
|
|
assert(!elm.classList.contains('horse'))
|
|
elm = patch(vnode1, vnode2).elm
|
|
assert(elm.classList.contains('i'))
|
|
assert(elm.classList.contains('am'))
|
|
assert(!elm.classList.contains('horse'))
|
|
})
|
|
it('removes missing classes', function () {
|
|
const vnode1 = h('i', { class: { i: true, am: true, horse: true } })
|
|
const vnode2 = h('i', { class: { i: true, am: true } })
|
|
patch(vnode0, vnode1)
|
|
elm = patch(vnode1, vnode2).elm
|
|
assert(elm.classList.contains('i'))
|
|
assert(elm.classList.contains('am'))
|
|
assert(!elm.classList.contains('horse'))
|
|
})
|
|
it('changes an elements props', function () {
|
|
const vnode1 = h('a', { props: { src: 'http://other/' } })
|
|
const vnode2 = h('a', { props: { src: 'http://localhost/' } })
|
|
patch(vnode0, vnode1)
|
|
elm = patch(vnode1, vnode2).elm
|
|
assert.strictEqual(elm.src, 'http://localhost/')
|
|
})
|
|
it('can set prop value to `0`', function () {
|
|
const patch = init([propsModule, styleModule])
|
|
const view = (scrollTop: number) => h('div',
|
|
{
|
|
style: { height: '100px', overflowY: 'scroll' },
|
|
props: { scrollTop },
|
|
},
|
|
[h('div', { style: { height: '200px' } })]
|
|
)
|
|
const vnode1 = view(0)
|
|
const mountPoint = document.body.appendChild(document.createElement('div'))
|
|
const { elm } = patch(mountPoint, vnode1)
|
|
if (!(elm instanceof HTMLDivElement)) throw new Error()
|
|
assert.strictEqual(elm.scrollTop, 0)
|
|
const vnode2 = view(20)
|
|
patch(vnode1, vnode2)
|
|
assert.isAtLeast(elm.scrollTop, 18)
|
|
assert.isAtMost(elm.scrollTop, 20)
|
|
const vnode3 = view(0)
|
|
patch(vnode2, vnode3)
|
|
assert.strictEqual(elm.scrollTop, 0)
|
|
document.body.removeChild(mountPoint)
|
|
})
|
|
it('can set prop value to empty string', function () {
|
|
const vnode1 = h('p', { props: { textContent: 'foo' } })
|
|
const { elm } = patch(vnode0, vnode1)
|
|
if (!(elm instanceof HTMLParagraphElement)) throw new Error()
|
|
assert.strictEqual(elm.textContent, 'foo')
|
|
const vnode2 = h('p', { props: { textContent: '' } })
|
|
patch(vnode1, vnode2)
|
|
assert.strictEqual(elm.textContent, '')
|
|
})
|
|
it('preserves memoized props', function () {
|
|
const cachedProps = { src: 'http://other/' }
|
|
const vnode1 = h('a', { props: cachedProps })
|
|
const vnode2 = h('a', { props: cachedProps })
|
|
elm = patch(vnode0, vnode1).elm
|
|
assert.strictEqual(elm.src, 'http://other/')
|
|
elm = patch(vnode1, vnode2).elm
|
|
assert.strictEqual(elm.src, 'http://other/')
|
|
})
|
|
it('removes custom props', function () {
|
|
const vnode1 = h('a', { props: { src: 'http://other/' } })
|
|
const vnode2 = h('a')
|
|
patch(vnode0, vnode1)
|
|
patch(vnode1, vnode2)
|
|
assert.strictEqual(elm.src, undefined)
|
|
})
|
|
it('cannot remove native props', function () {
|
|
const vnode1 = h('a', { props: { href: 'http://example.com/' } })
|
|
const vnode2 = h('a')
|
|
const { elm: elm1 } = patch(vnode0, vnode1)
|
|
if (!(elm1 instanceof HTMLAnchorElement)) throw new Error()
|
|
assert.strictEqual(elm1.href, 'http://example.com/')
|
|
const { elm: elm2 } = patch(vnode1, vnode2)
|
|
if (!(elm2 instanceof HTMLAnchorElement)) throw new Error()
|
|
assert.strictEqual(elm2.href, 'http://example.com/')
|
|
})
|
|
it('does not delete custom props', function () {
|
|
const vnode1 = h('p', { props: { a: 'foo' } })
|
|
const vnode2 = h('p')
|
|
const { elm } = patch(vnode0, vnode1)
|
|
if (!(elm instanceof HTMLParagraphElement)) throw new Error()
|
|
assert.strictEqual((elm as any).a, 'foo')
|
|
patch(vnode1, vnode2)
|
|
assert.strictEqual((elm as any).a, 'foo')
|
|
})
|
|
it('handles changing is attribute', function () {
|
|
const vnode1 = h('p', { is: 'p-a' })
|
|
const vnode2 = h('p', { is: 'p-b' })
|
|
|
|
elm = patch(vnode0, vnode1).elm
|
|
assert(elm instanceof A)
|
|
elm = patch(vnode1, vnode2).elm
|
|
assert(elm instanceof B)
|
|
})
|
|
describe('using toVNode()', function () {
|
|
it('can remove previous children of the root element', function () {
|
|
const h2 = document.createElement('h2')
|
|
h2.textContent = 'Hello'
|
|
const prevElm = document.createElement('div')
|
|
prevElm.id = 'id'
|
|
prevElm.className = 'class'
|
|
prevElm.appendChild(h2)
|
|
const nextVNode = h('div#id.class', [h('span', 'Hi')])
|
|
elm = patch(toVNode(prevElm), nextVNode).elm
|
|
assert.strictEqual(elm, prevElm)
|
|
assert.strictEqual(elm.tagName, 'DIV')
|
|
assert.strictEqual(elm.id, 'id')
|
|
assert.strictEqual(elm.className, 'class')
|
|
assert.strictEqual(elm.childNodes.length, 1)
|
|
assert.strictEqual(elm.childNodes[0].tagName, 'SPAN')
|
|
assert.strictEqual(elm.childNodes[0].textContent, 'Hi')
|
|
})
|
|
it('can support patching in a DocumentFragment', function () {
|
|
const prevElm = document.createDocumentFragment()
|
|
const nextVNode = vnode('', {}, [
|
|
h('div#id.class', [h('span', 'Hi')])
|
|
], undefined, prevElm as any)
|
|
elm = patch(toVNode(prevElm), nextVNode).elm
|
|
assert.strictEqual(elm, prevElm)
|
|
assert.strictEqual(elm.nodeType, 11)
|
|
assert.strictEqual(elm.childNodes.length, 1)
|
|
assert.strictEqual(elm.childNodes[0].tagName, 'DIV')
|
|
assert.strictEqual(elm.childNodes[0].id, 'id')
|
|
assert.strictEqual(elm.childNodes[0].className, 'class')
|
|
assert.strictEqual(elm.childNodes[0].childNodes.length, 1)
|
|
assert.strictEqual(elm.childNodes[0].childNodes[0].tagName, 'SPAN')
|
|
assert.strictEqual(elm.childNodes[0].childNodes[0].textContent, 'Hi')
|
|
})
|
|
it('can remove some children of the root element', function () {
|
|
const h2 = document.createElement('h2')
|
|
h2.textContent = 'Hello'
|
|
const prevElm = document.createElement('div')
|
|
prevElm.id = 'id'
|
|
prevElm.className = 'class'
|
|
const text = document.createTextNode('Foobar')
|
|
const reference = {};
|
|
(text as any).testProperty = reference // ensures we dont recreate the Text Node
|
|
prevElm.appendChild(text)
|
|
prevElm.appendChild(h2)
|
|
const nextVNode = h('div#id.class', ['Foobar'])
|
|
elm = patch(toVNode(prevElm), nextVNode).elm
|
|
assert.strictEqual(elm, prevElm)
|
|
assert.strictEqual(elm.tagName, 'DIV')
|
|
assert.strictEqual(elm.id, 'id')
|
|
assert.strictEqual(elm.className, 'class')
|
|
assert.strictEqual(elm.childNodes.length, 1)
|
|
assert.strictEqual(elm.childNodes[0].nodeType, 3)
|
|
assert.strictEqual(elm.childNodes[0].wholeText, 'Foobar')
|
|
assert.strictEqual(elm.childNodes[0].testProperty, reference)
|
|
})
|
|
it('can remove text elements', function () {
|
|
const h2 = document.createElement('h2')
|
|
h2.textContent = 'Hello'
|
|
const prevElm = document.createElement('div')
|
|
prevElm.id = 'id'
|
|
prevElm.className = 'class'
|
|
const text = document.createTextNode('Foobar')
|
|
prevElm.appendChild(text)
|
|
prevElm.appendChild(h2)
|
|
const nextVNode = h('div#id.class', [h('h2', 'Hello')])
|
|
elm = patch(toVNode(prevElm), nextVNode).elm
|
|
assert.strictEqual(elm, prevElm)
|
|
assert.strictEqual(elm.tagName, 'DIV')
|
|
assert.strictEqual(elm.id, 'id')
|
|
assert.strictEqual(elm.className, 'class')
|
|
assert.strictEqual(elm.childNodes.length, 1)
|
|
assert.strictEqual(elm.childNodes[0].nodeType, 1)
|
|
assert.strictEqual(elm.childNodes[0].textContent, 'Hello')
|
|
})
|
|
it('can work with domApi', function () {
|
|
const domApi = {
|
|
...htmlDomApi,
|
|
tagName: function (elm: Element) { return 'x-' + elm.tagName.toUpperCase() }
|
|
}
|
|
const h2 = document.createElement('h2')
|
|
h2.id = 'hx'
|
|
h2.setAttribute('data-env', 'xyz')
|
|
const text = document.createTextNode('Foobar')
|
|
const elm = document.createElement('div')
|
|
elm.id = 'id'
|
|
elm.className = 'class other'
|
|
elm.setAttribute('data', 'value')
|
|
elm.appendChild(h2)
|
|
elm.appendChild(text)
|
|
const vnode = toVNode(elm, domApi)
|
|
assert.strictEqual(vnode.sel, 'x-div#id.class.other')
|
|
assert.deepEqual(vnode.data, { attrs: { data: 'value' } })
|
|
const children = vnode.children as [VNode, VNode]
|
|
assert.strictEqual(children[0].sel, 'x-h2#hx')
|
|
assert.deepEqual(children[0].data, { attrs: { 'data-env': 'xyz' } })
|
|
assert.strictEqual(children[1].text, 'Foobar')
|
|
})
|
|
})
|
|
describe('updating children with keys', function () {
|
|
function spanNum (n?: null | string | number) {
|
|
if (n == null) {
|
|
return n
|
|
} else if (typeof n === 'string') {
|
|
return h('span', {}, n)
|
|
} else {
|
|
return h('span', { key: n }, n.toString())
|
|
}
|
|
}
|
|
describe('addition of elements', function () {
|
|
it('appends elements', function () {
|
|
const vnode1 = h('span', [1].map(spanNum))
|
|
const vnode2 = h('span', [1, 2, 3].map(spanNum))
|
|
elm = patch(vnode0, vnode1).elm
|
|
assert.strictEqual(elm.children.length, 1)
|
|
elm = patch(vnode1, vnode2).elm
|
|
assert.strictEqual(elm.children.length, 3)
|
|
assert.strictEqual(elm.children[1].innerHTML, '2')
|
|
assert.strictEqual(elm.children[2].innerHTML, '3')
|
|
})
|
|
it('prepends elements', function () {
|
|
const vnode1 = h('span', [4, 5].map(spanNum))
|
|
const vnode2 = h('span', [1, 2, 3, 4, 5].map(spanNum))
|
|
elm = patch(vnode0, vnode1).elm
|
|
assert.strictEqual(elm.children.length, 2)
|
|
elm = patch(vnode1, vnode2).elm
|
|
assert.deepEqual(map(inner, elm.children), ['1', '2', '3', '4', '5'])
|
|
})
|
|
it('add elements in the middle', function () {
|
|
const vnode1 = h('span', [1, 2, 4, 5].map(spanNum))
|
|
const vnode2 = h('span', [1, 2, 3, 4, 5].map(spanNum))
|
|
elm = patch(vnode0, vnode1).elm
|
|
assert.strictEqual(elm.children.length, 4)
|
|
assert.strictEqual(elm.children.length, 4)
|
|
elm = patch(vnode1, vnode2).elm
|
|
assert.deepEqual(map(inner, elm.children), ['1', '2', '3', '4', '5'])
|
|
})
|
|
it('add elements at begin and end', function () {
|
|
const vnode1 = h('span', [2, 3, 4].map(spanNum))
|
|
const vnode2 = h('span', [1, 2, 3, 4, 5].map(spanNum))
|
|
elm = patch(vnode0, vnode1).elm
|
|
assert.strictEqual(elm.children.length, 3)
|
|
elm = patch(vnode1, vnode2).elm
|
|
assert.deepEqual(map(inner, elm.children), ['1', '2', '3', '4', '5'])
|
|
})
|
|
it('adds children to parent with no children', function () {
|
|
const vnode1 = h('span', { key: 'span' })
|
|
const vnode2 = h('span', { key: 'span' }, [1, 2, 3].map(spanNum))
|
|
elm = patch(vnode0, vnode1).elm
|
|
assert.strictEqual(elm.children.length, 0)
|
|
elm = patch(vnode1, vnode2).elm
|
|
assert.deepEqual(map(inner, elm.children), ['1', '2', '3'])
|
|
})
|
|
it('removes all children from parent', function () {
|
|
const vnode1 = h('span', { key: 'span' }, [1, 2, 3].map(spanNum))
|
|
const vnode2 = h('span', { key: 'span' })
|
|
elm = patch(vnode0, vnode1).elm
|
|
assert.deepEqual(map(inner, elm.children), ['1', '2', '3'])
|
|
elm = patch(vnode1, vnode2).elm
|
|
assert.strictEqual(elm.children.length, 0)
|
|
})
|
|
it('update one child with same key but different sel', function () {
|
|
const vnode1 = h('span', { key: 'span' }, [1, 2, 3].map(spanNum))
|
|
const vnode2 = h('span', { key: 'span' }, [spanNum(1), h('i', { key: 2 }, '2'), spanNum(3)])
|
|
elm = patch(vnode0, vnode1).elm
|
|
assert.deepEqual(map(inner, elm.children), ['1', '2', '3'])
|
|
elm = patch(vnode1, vnode2).elm
|
|
assert.deepEqual(map(inner, elm.children), ['1', '2', '3'])
|
|
assert.strictEqual(elm.children.length, 3)
|
|
assert.strictEqual(elm.children[1].tagName, 'I')
|
|
})
|
|
})
|
|
describe('removal of elements', function () {
|
|
it('removes elements from the beginning', function () {
|
|
const vnode1 = h('span', [1, 2, 3, 4, 5].map(spanNum))
|
|
const vnode2 = h('span', [3, 4, 5].map(spanNum))
|
|
elm = patch(vnode0, vnode1).elm
|
|
assert.strictEqual(elm.children.length, 5)
|
|
elm = patch(vnode1, vnode2).elm
|
|
assert.deepEqual(map(inner, elm.children), ['3', '4', '5'])
|
|
})
|
|
it('removes elements from the end', function () {
|
|
const vnode1 = h('span', [1, 2, 3, 4, 5].map(spanNum))
|
|
const vnode2 = h('span', [1, 2, 3].map(spanNum))
|
|
elm = patch(vnode0, vnode1).elm
|
|
assert.strictEqual(elm.children.length, 5)
|
|
elm = patch(vnode1, vnode2).elm
|
|
assert.strictEqual(elm.children.length, 3)
|
|
assert.strictEqual(elm.children[0].innerHTML, '1')
|
|
assert.strictEqual(elm.children[1].innerHTML, '2')
|
|
assert.strictEqual(elm.children[2].innerHTML, '3')
|
|
})
|
|
it('removes elements from the middle', function () {
|
|
const vnode1 = h('span', [1, 2, 3, 4, 5].map(spanNum))
|
|
const vnode2 = h('span', [1, 2, 4, 5].map(spanNum))
|
|
elm = patch(vnode0, vnode1).elm
|
|
assert.strictEqual(elm.children.length, 5)
|
|
elm = patch(vnode1, vnode2).elm
|
|
assert.strictEqual(elm.children.length, 4)
|
|
assert.deepEqual(elm.children[0].innerHTML, '1')
|
|
assert.strictEqual(elm.children[0].innerHTML, '1')
|
|
assert.strictEqual(elm.children[1].innerHTML, '2')
|
|
assert.strictEqual(elm.children[2].innerHTML, '4')
|
|
assert.strictEqual(elm.children[3].innerHTML, '5')
|
|
})
|
|
})
|
|
describe('element reordering', function () {
|
|
it('moves element forward', function () {
|
|
const vnode1 = h('span', [1, 2, 3, 4].map(spanNum))
|
|
const vnode2 = h('span', [2, 3, 1, 4].map(spanNum))
|
|
elm = patch(vnode0, vnode1).elm
|
|
assert.strictEqual(elm.children.length, 4)
|
|
elm = patch(vnode1, vnode2).elm
|
|
assert.strictEqual(elm.children.length, 4)
|
|
assert.strictEqual(elm.children[0].innerHTML, '2')
|
|
assert.strictEqual(elm.children[1].innerHTML, '3')
|
|
assert.strictEqual(elm.children[2].innerHTML, '1')
|
|
assert.strictEqual(elm.children[3].innerHTML, '4')
|
|
})
|
|
it('moves element to end', function () {
|
|
const vnode1 = h('span', [1, 2, 3].map(spanNum))
|
|
const vnode2 = h('span', [2, 3, 1].map(spanNum))
|
|
elm = patch(vnode0, vnode1).elm
|
|
assert.strictEqual(elm.children.length, 3)
|
|
elm = patch(vnode1, vnode2).elm
|
|
assert.strictEqual(elm.children.length, 3)
|
|
assert.strictEqual(elm.children[0].innerHTML, '2')
|
|
assert.strictEqual(elm.children[1].innerHTML, '3')
|
|
assert.strictEqual(elm.children[2].innerHTML, '1')
|
|
})
|
|
it('moves element backwards', function () {
|
|
const vnode1 = h('span', [1, 2, 3, 4].map(spanNum))
|
|
const vnode2 = h('span', [1, 4, 2, 3].map(spanNum))
|
|
elm = patch(vnode0, vnode1).elm
|
|
assert.strictEqual(elm.children.length, 4)
|
|
elm = patch(vnode1, vnode2).elm
|
|
assert.strictEqual(elm.children.length, 4)
|
|
assert.strictEqual(elm.children[0].innerHTML, '1')
|
|
assert.strictEqual(elm.children[1].innerHTML, '4')
|
|
assert.strictEqual(elm.children[2].innerHTML, '2')
|
|
assert.strictEqual(elm.children[3].innerHTML, '3')
|
|
})
|
|
it('swaps first and last', function () {
|
|
const vnode1 = h('span', [1, 2, 3, 4].map(spanNum))
|
|
const vnode2 = h('span', [4, 2, 3, 1].map(spanNum))
|
|
elm = patch(vnode0, vnode1).elm
|
|
assert.strictEqual(elm.children.length, 4)
|
|
elm = patch(vnode1, vnode2).elm
|
|
assert.strictEqual(elm.children.length, 4)
|
|
assert.strictEqual(elm.children[0].innerHTML, '4')
|
|
assert.strictEqual(elm.children[1].innerHTML, '2')
|
|
assert.strictEqual(elm.children[2].innerHTML, '3')
|
|
assert.strictEqual(elm.children[3].innerHTML, '1')
|
|
})
|
|
})
|
|
describe('combinations of additions, removals and reorderings', function () {
|
|
it('move to left and replace', function () {
|
|
const vnode1 = h('span', [1, 2, 3, 4, 5].map(spanNum))
|
|
const vnode2 = h('span', [4, 1, 2, 3, 6].map(spanNum))
|
|
elm = patch(vnode0, vnode1).elm
|
|
assert.strictEqual(elm.children.length, 5)
|
|
elm = patch(vnode1, vnode2).elm
|
|
assert.strictEqual(elm.children.length, 5)
|
|
assert.strictEqual(elm.children[0].innerHTML, '4')
|
|
assert.strictEqual(elm.children[1].innerHTML, '1')
|
|
assert.strictEqual(elm.children[2].innerHTML, '2')
|
|
assert.strictEqual(elm.children[3].innerHTML, '3')
|
|
assert.strictEqual(elm.children[4].innerHTML, '6')
|
|
})
|
|
it('moves to left and leaves hole', function () {
|
|
const vnode1 = h('span', [1, 4, 5].map(spanNum))
|
|
const vnode2 = h('span', [4, 6].map(spanNum))
|
|
elm = patch(vnode0, vnode1).elm
|
|
assert.strictEqual(elm.children.length, 3)
|
|
elm = patch(vnode1, vnode2).elm
|
|
assert.deepEqual(map(inner, elm.children), ['4', '6'])
|
|
})
|
|
it('handles moved and set to undefined element ending at the end', function () {
|
|
const vnode1 = h('span', [2, 4, 5].map(spanNum))
|
|
const vnode2 = h('span', [4, 5, 3].map(spanNum))
|
|
elm = patch(vnode0, vnode1).elm
|
|
assert.strictEqual(elm.children.length, 3)
|
|
elm = patch(vnode1, vnode2).elm
|
|
assert.strictEqual(elm.children.length, 3)
|
|
assert.strictEqual(elm.children[0].innerHTML, '4')
|
|
assert.strictEqual(elm.children[1].innerHTML, '5')
|
|
assert.strictEqual(elm.children[2].innerHTML, '3')
|
|
})
|
|
it('moves a key in non-keyed nodes with a size up', function () {
|
|
const vnode1 = h('span', [1, 'a', 'b', 'c'].map(spanNum))
|
|
const vnode2 = h('span', ['d', 'a', 'b', 'c', 1, 'e'].map(spanNum))
|
|
elm = patch(vnode0, vnode1).elm
|
|
assert.strictEqual(elm.childNodes.length, 4)
|
|
assert.strictEqual(elm.textContent, '1abc')
|
|
elm = patch(vnode1, vnode2).elm
|
|
assert.strictEqual(elm.childNodes.length, 6)
|
|
assert.strictEqual(elm.textContent, 'dabc1e')
|
|
})
|
|
})
|
|
it('reverses elements', function () {
|
|
const vnode1 = h('span', [1, 2, 3, 4, 5, 6, 7, 8].map(spanNum))
|
|
const vnode2 = h('span', [8, 7, 6, 5, 4, 3, 2, 1].map(spanNum))
|
|
elm = patch(vnode0, vnode1).elm
|
|
assert.strictEqual(elm.children.length, 8)
|
|
elm = patch(vnode1, vnode2).elm
|
|
assert.deepEqual(map(inner, elm.children), ['8', '7', '6', '5', '4', '3', '2', '1'])
|
|
})
|
|
it('something', function () {
|
|
const vnode1 = h('span', [0, 1, 2, 3, 4, 5].map(spanNum))
|
|
const vnode2 = h('span', [4, 3, 2, 1, 5, 0].map(spanNum))
|
|
elm = patch(vnode0, vnode1).elm
|
|
assert.strictEqual(elm.children.length, 6)
|
|
elm = patch(vnode1, vnode2).elm
|
|
assert.deepEqual(map(inner, elm.children), ['4', '3', '2', '1', '5', '0'])
|
|
})
|
|
it('handles random shuffles', function () {
|
|
let n
|
|
let i
|
|
const arr = []
|
|
const opacities: string[] = []
|
|
const elms = 14
|
|
const samples = 5
|
|
function spanNumWithOpacity (n: number, o: string) {
|
|
return h('span', { key: n, style: { opacity: o } }, n.toString())
|
|
}
|
|
for (n = 0; n < elms; ++n) {
|
|
arr[n] = n
|
|
}
|
|
for (n = 0; n < samples; ++n) {
|
|
const vnode1 = h('span', arr.map(function (n) {
|
|
return spanNumWithOpacity(n, '1')
|
|
}))
|
|
const shufArr = shuffle(arr.slice(0))
|
|
let elm: HTMLDivElement | HTMLSpanElement = document.createElement('div')
|
|
elm = patch(elm, vnode1).elm as HTMLSpanElement
|
|
for (i = 0; i < elms; ++i) {
|
|
assert.strictEqual(elm.children[i].innerHTML, i.toString())
|
|
opacities[i] = Math.random().toFixed(5).toString()
|
|
}
|
|
const vnode2 = h('span', arr.map(function (n) {
|
|
return spanNumWithOpacity(shufArr[n], opacities[n])
|
|
}))
|
|
elm = patch(vnode1, vnode2).elm as HTMLSpanElement
|
|
for (i = 0; i < elms; ++i) {
|
|
assert.strictEqual(elm.children[i].innerHTML, shufArr[i].toString())
|
|
const opacity = (elm.children[i] as HTMLSpanElement).style.opacity as string
|
|
assert.strictEqual(opacities[i].indexOf(opacity), 0)
|
|
}
|
|
}
|
|
})
|
|
it('supports null/undefined children', function () {
|
|
const vnode1 = h('i', [0, 1, 2, 3, 4, 5].map(spanNum))
|
|
const vnode2 = h('i', [null, 2, undefined, null, 1, 0, null, 5, 4, null, 3, undefined].map(spanNum))
|
|
elm = patch(vnode0, vnode1).elm
|
|
assert.strictEqual(elm.children.length, 6)
|
|
elm = patch(vnode1, vnode2).elm
|
|
assert.deepEqual(map(inner, elm.children), ['2', '1', '0', '5', '4', '3'])
|
|
})
|
|
it('supports all null/undefined children', function () {
|
|
const vnode1 = h('i', [0, 1, 2, 3, 4, 5].map(spanNum))
|
|
const vnode2 = h('i', [null, null, undefined, null, null, undefined])
|
|
const vnode3 = h('i', [5, 4, 3, 2, 1, 0].map(spanNum))
|
|
patch(vnode0, vnode1)
|
|
elm = patch(vnode1, vnode2).elm
|
|
assert.strictEqual(elm.children.length, 0)
|
|
elm = patch(vnode2, vnode3).elm
|
|
assert.deepEqual(map(inner, elm.children), ['5', '4', '3', '2', '1', '0'])
|
|
})
|
|
it('handles random shuffles with null/undefined children', function () {
|
|
let i
|
|
let j
|
|
let r
|
|
let len
|
|
let arr
|
|
const maxArrLen = 15
|
|
const samples = 5
|
|
let vnode1 = vnode0
|
|
let vnode2
|
|
for (i = 0; i < samples; ++i, vnode1 = vnode2) {
|
|
len = Math.floor(Math.random() * maxArrLen)
|
|
arr = []
|
|
for (j = 0; j < len; ++j) {
|
|
if ((r = Math.random()) < 0.5) arr[j] = String(j)
|
|
else if (r < 0.75) arr[j] = null
|
|
else arr[j] = undefined
|
|
}
|
|
shuffle(arr)
|
|
vnode2 = h('div', arr.map(spanNum))
|
|
elm = patch(vnode1, vnode2).elm
|
|
assert.deepEqual(map(inner, elm.children), arr.filter(function (x) {
|
|
return x != null
|
|
}))
|
|
}
|
|
})
|
|
})
|
|
describe('updating children without keys', function () {
|
|
it('appends elements', function () {
|
|
const vnode1 = h('div', [h('span', 'Hello')])
|
|
const vnode2 = h('div', [h('span', 'Hello'), h('span', 'World')])
|
|
elm = patch(vnode0, vnode1).elm
|
|
assert.deepEqual(map(inner, elm.children), ['Hello'])
|
|
elm = patch(vnode1, vnode2).elm
|
|
assert.deepEqual(map(inner, elm.children), ['Hello', 'World'])
|
|
})
|
|
it('handles unmoved text nodes', function () {
|
|
const vnode1 = h('div', ['Text', h('span', 'Span')])
|
|
const vnode2 = h('div', ['Text', h('span', 'Span')])
|
|
elm = patch(vnode0, vnode1).elm
|
|
assert.strictEqual(elm.childNodes[0].textContent, 'Text')
|
|
elm = patch(vnode1, vnode2).elm
|
|
assert.strictEqual(elm.childNodes[0].textContent, 'Text')
|
|
})
|
|
it('handles changing text children', function () {
|
|
const vnode1 = h('div', ['Text', h('span', 'Span')])
|
|
const vnode2 = h('div', ['Text2', h('span', 'Span')])
|
|
elm = patch(vnode0, vnode1).elm
|
|
assert.strictEqual(elm.childNodes[0].textContent, 'Text')
|
|
elm = patch(vnode1, vnode2).elm
|
|
assert.strictEqual(elm.childNodes[0].textContent, 'Text2')
|
|
})
|
|
it('handles unmoved comment nodes', function () {
|
|
const vnode1 = h('div', [h('!', 'Text'), h('span', 'Span')])
|
|
const vnode2 = h('div', [h('!', 'Text'), h('span', 'Span')])
|
|
elm = patch(vnode0, vnode1).elm
|
|
assert.strictEqual(elm.childNodes[0].textContent, 'Text')
|
|
elm = patch(vnode1, vnode2).elm
|
|
assert.strictEqual(elm.childNodes[0].textContent, 'Text')
|
|
})
|
|
it('handles changing comment text', function () {
|
|
const vnode1 = h('div', [h('!', 'Text'), h('span', 'Span')])
|
|
const vnode2 = h('div', [h('!', 'Text2'), h('span', 'Span')])
|
|
elm = patch(vnode0, vnode1).elm
|
|
assert.strictEqual(elm.childNodes[0].textContent, 'Text')
|
|
elm = patch(vnode1, vnode2).elm
|
|
assert.strictEqual(elm.childNodes[0].textContent, 'Text2')
|
|
})
|
|
it('handles changing empty comment', function () {
|
|
const vnode1 = h('div', [h('!'), h('span', 'Span')])
|
|
const vnode2 = h('div', [h('!', 'Test'), h('span', 'Span')])
|
|
elm = patch(vnode0, vnode1).elm
|
|
assert.strictEqual(elm.childNodes[0].textContent, '')
|
|
elm = patch(vnode1, vnode2).elm
|
|
assert.strictEqual(elm.childNodes[0].textContent, 'Test')
|
|
})
|
|
it('prepends element', function () {
|
|
const vnode1 = h('div', [h('span', 'World')])
|
|
const vnode2 = h('div', [h('span', 'Hello'), h('span', 'World')])
|
|
elm = patch(vnode0, vnode1).elm
|
|
assert.deepEqual(map(inner, elm.children), ['World'])
|
|
elm = patch(vnode1, vnode2).elm
|
|
assert.deepEqual(map(inner, elm.children), ['Hello', 'World'])
|
|
})
|
|
it('prepends element of different tag type', function () {
|
|
const vnode1 = h('div', [h('span', 'World')])
|
|
const vnode2 = h('div', [h('div', 'Hello'), h('span', 'World')])
|
|
elm = patch(vnode0, vnode1).elm
|
|
assert.deepEqual(map(inner, elm.children), ['World'])
|
|
elm = patch(vnode1, vnode2).elm
|
|
assert.deepEqual(map(prop('tagName'), elm.children), ['DIV', 'SPAN'])
|
|
assert.deepEqual(map(inner, elm.children), ['Hello', 'World'])
|
|
})
|
|
it('removes elements', function () {
|
|
const vnode1 = h('div', [h('span', 'One'), h('span', 'Two'), h('span', 'Three')])
|
|
const vnode2 = h('div', [h('span', 'One'), h('span', 'Three')])
|
|
elm = patch(vnode0, vnode1).elm
|
|
assert.deepEqual(map(inner, elm.children), ['One', 'Two', 'Three'])
|
|
elm = patch(vnode1, vnode2).elm
|
|
assert.deepEqual(map(inner, elm.children), ['One', 'Three'])
|
|
})
|
|
it('removes a single text node', function () {
|
|
const vnode1 = h('div', 'One')
|
|
const vnode2 = h('div')
|
|
patch(vnode0, vnode1)
|
|
assert.strictEqual(elm.textContent, 'One')
|
|
patch(vnode1, vnode2)
|
|
assert.strictEqual(elm.textContent, '')
|
|
})
|
|
it('removes a single text node when children are updated', function () {
|
|
const vnode1 = h('div', 'One')
|
|
const vnode2 = h('div', [h('div', 'Two'), h('span', 'Three')])
|
|
patch(vnode0, vnode1)
|
|
assert.strictEqual(elm.textContent, 'One')
|
|
patch(vnode1, vnode2)
|
|
assert.deepEqual(map(prop('textContent'), elm.childNodes), ['Two', 'Three'])
|
|
})
|
|
it('removes a text node among other elements', function () {
|
|
const vnode1 = h('div', ['One', h('span', 'Two')])
|
|
const vnode2 = h('div', [h('div', 'Three')])
|
|
patch(vnode0, vnode1)
|
|
assert.deepEqual(map(prop('textContent'), elm.childNodes), ['One', 'Two'])
|
|
patch(vnode1, vnode2)
|
|
assert.strictEqual(elm.childNodes.length, 1)
|
|
assert.strictEqual(elm.childNodes[0].tagName, 'DIV')
|
|
assert.strictEqual(elm.childNodes[0].textContent, 'Three')
|
|
})
|
|
it('reorders elements', function () {
|
|
const vnode1 = h('div', [h('span', 'One'), h('div', 'Two'), h('b', 'Three')])
|
|
const vnode2 = h('div', [h('b', 'Three'), h('span', 'One'), h('div', 'Two')])
|
|
elm = patch(vnode0, vnode1).elm
|
|
assert.deepEqual(map(inner, elm.children), ['One', 'Two', 'Three'])
|
|
elm = patch(vnode1, vnode2).elm
|
|
assert.deepEqual(map(prop('tagName'), elm.children), ['B', 'SPAN', 'DIV'])
|
|
assert.deepEqual(map(inner, elm.children), ['Three', 'One', 'Two'])
|
|
})
|
|
it('supports null/undefined children', function () {
|
|
const vnode1 = h('i', [null, h('i', '1'), h('i', '2'), null])
|
|
const vnode2 = h('i', [h('i', '2'), undefined, undefined, h('i', '1'), undefined])
|
|
const vnode3 = h('i', [null, h('i', '1'), undefined, null, h('i', '2'), undefined, null])
|
|
elm = patch(vnode0, vnode1).elm
|
|
assert.deepEqual(map(inner, elm.children), ['1', '2'])
|
|
elm = patch(vnode1, vnode2).elm
|
|
assert.deepEqual(map(inner, elm.children), ['2', '1'])
|
|
elm = patch(vnode2, vnode3).elm
|
|
assert.deepEqual(map(inner, elm.children), ['1', '2'])
|
|
})
|
|
it('supports all null/undefined children', function () {
|
|
const vnode1 = h('i', [h('i', '1'), h('i', '2')])
|
|
const vnode2 = h('i', [null, undefined])
|
|
const vnode3 = h('i', [h('i', '2'), h('i', '1')])
|
|
patch(vnode0, vnode1)
|
|
elm = patch(vnode1, vnode2).elm
|
|
assert.strictEqual(elm.children.length, 0)
|
|
elm = patch(vnode2, vnode3).elm
|
|
assert.deepEqual(map(inner, elm.children), ['2', '1'])
|
|
})
|
|
})
|
|
})
|
|
describe('hooks', function () {
|
|
describe('element hooks', function () {
|
|
it('calls `create` listener before inserted into parent but after children', function () {
|
|
const result = []
|
|
const cb: CreateHook = (empty, vnode) => {
|
|
assert(vnode.elm instanceof Element)
|
|
assert.strictEqual((vnode.elm as HTMLDivElement).children.length, 2)
|
|
assert.strictEqual(vnode.elm!.parentNode, null)
|
|
result.push(vnode)
|
|
}
|
|
const vnode1 = h('div', [
|
|
h('span', 'First sibling'),
|
|
h('div', { hook: { create: cb } }, [
|
|
h('span', 'Child 1'),
|
|
h('span', 'Child 2'),
|
|
]),
|
|
h('span', 'Can\'t touch me'),
|
|
])
|
|
patch(vnode0, vnode1)
|
|
assert.strictEqual(1, result.length)
|
|
})
|
|
it('calls `insert` listener after both parents, siblings and children have been inserted', function () {
|
|
const result = []
|
|
const cb: InsertHook = (vnode) => {
|
|
assert(vnode.elm instanceof Element)
|
|
assert.strictEqual((vnode.elm as HTMLDivElement).children.length, 2)
|
|
assert.strictEqual(vnode.elm!.parentNode!.children.length, 3)
|
|
result.push(vnode)
|
|
}
|
|
const vnode1 = h('div', [
|
|
h('span', 'First sibling'),
|
|
h('div', { hook: { insert: cb } }, [
|
|
h('span', 'Child 1'),
|
|
h('span', 'Child 2'),
|
|
]),
|
|
h('span', 'Can touch me'),
|
|
])
|
|
patch(vnode0, vnode1)
|
|
assert.strictEqual(1, result.length)
|
|
})
|
|
it('calls `prepatch` listener', function () {
|
|
const result = []
|
|
const cb: PrePatchHook = (oldVnode, vnode) => {
|
|
assert.strictEqual(oldVnode, vnode1.children![1])
|
|
assert.strictEqual(vnode, vnode2.children![1])
|
|
result.push(vnode)
|
|
}
|
|
const vnode1 = h('div', [
|
|
h('span', 'First sibling'),
|
|
h('div', { hook: { prepatch: cb } }, [
|
|
h('span', 'Child 1'),
|
|
h('span', 'Child 2'),
|
|
]),
|
|
])
|
|
const vnode2 = h('div', [
|
|
h('span', 'First sibling'),
|
|
h('div', { hook: { prepatch: cb } }, [
|
|
h('span', 'Child 1'),
|
|
h('span', 'Child 2'),
|
|
]),
|
|
])
|
|
patch(vnode0, vnode1)
|
|
patch(vnode1, vnode2)
|
|
assert.strictEqual(result.length, 1)
|
|
})
|
|
it('calls `postpatch` after `prepatch` listener', function () {
|
|
let pre = 0
|
|
let post = 0
|
|
function preCb () {
|
|
pre++
|
|
}
|
|
function postCb () {
|
|
assert.strictEqual(pre, post + 1)
|
|
post++
|
|
}
|
|
const vnode1 = h('div', [
|
|
h('span', 'First sibling'),
|
|
h('div', { hook: { prepatch: preCb, postpatch: postCb } }, [
|
|
h('span', 'Child 1'),
|
|
h('span', 'Child 2'),
|
|
]),
|
|
])
|
|
const vnode2 = h('div', [
|
|
h('span', 'First sibling'),
|
|
h('div', { hook: { prepatch: preCb, postpatch: postCb } }, [
|
|
h('span', 'Child 1'),
|
|
h('span', 'Child 2'),
|
|
]),
|
|
])
|
|
patch(vnode0, vnode1)
|
|
patch(vnode1, vnode2)
|
|
assert.strictEqual(pre, 1)
|
|
assert.strictEqual(post, 1)
|
|
})
|
|
it('calls `update` listener', function () {
|
|
const result1: VNode[] = []
|
|
const result2: VNode[] = []
|
|
function cb (result: VNode[], oldVnode: VNode, vnode: VNode) {
|
|
if (result.length > 0) {
|
|
console.log(result[result.length - 1])
|
|
console.log(oldVnode)
|
|
assert.strictEqual(result[result.length - 1], oldVnode)
|
|
}
|
|
result.push(vnode)
|
|
}
|
|
const vnode1 = h('div', [
|
|
h('span', 'First sibling'),
|
|
h('div', { hook: { update: cb.bind(null, result1) } }, [
|
|
h('span', 'Child 1'),
|
|
h('span', { hook: { update: cb.bind(null, result2) } }, 'Child 2'),
|
|
]),
|
|
])
|
|
const vnode2 = h('div', [
|
|
h('span', 'First sibling'),
|
|
h('div', { hook: { update: cb.bind(null, result1) } }, [
|
|
h('span', 'Child 1'),
|
|
h('span', { hook: { update: cb.bind(null, result2) } }, 'Child 2'),
|
|
]),
|
|
])
|
|
patch(vnode0, vnode1)
|
|
patch(vnode1, vnode2)
|
|
assert.strictEqual(result1.length, 1)
|
|
assert.strictEqual(result2.length, 1)
|
|
})
|
|
it('calls `remove` listener', function () {
|
|
const result = []
|
|
const cb: RemoveHook = (vnode, rm) => {
|
|
const parent = vnode.elm!.parentNode as HTMLDivElement
|
|
assert(vnode.elm instanceof Element)
|
|
assert.strictEqual((vnode.elm as HTMLDivElement).children.length, 2)
|
|
assert.strictEqual(parent.children.length, 2)
|
|
result.push(vnode)
|
|
rm()
|
|
assert.strictEqual(parent.children.length, 1)
|
|
}
|
|
const vnode1 = h('div', [
|
|
h('span', 'First sibling'),
|
|
h('div', { hook: { remove: cb } }, [
|
|
h('span', 'Child 1'),
|
|
h('span', 'Child 2'),
|
|
]),
|
|
])
|
|
const vnode2 = h('div', [
|
|
h('span', 'First sibling'),
|
|
])
|
|
patch(vnode0, vnode1)
|
|
patch(vnode1, vnode2)
|
|
assert.strictEqual(1, result.length)
|
|
})
|
|
it('calls `destroy` listener when patching text node over node with children', function () {
|
|
let calls = 0
|
|
function cb () {
|
|
calls++
|
|
}
|
|
const vnode1 = h('div', [
|
|
h('div', { hook: { destroy: cb } }, [
|
|
h('span', 'Child 1'),
|
|
]),
|
|
])
|
|
const vnode2 = h('div', 'Text node')
|
|
patch(vnode0, vnode1)
|
|
patch(vnode1, vnode2)
|
|
assert.strictEqual(calls, 1)
|
|
})
|
|
it('calls `init` and `prepatch` listeners on root', function () {
|
|
let count = 0
|
|
const init: InitHook = (vnode) => {
|
|
assert.strictEqual(vnode, vnode2)
|
|
count += 1
|
|
}
|
|
const prepatch: PrePatchHook = (oldVnode, vnode) => {
|
|
assert.strictEqual(vnode, vnode1)
|
|
count += 1
|
|
}
|
|
const vnode1 = h('div', { hook: { init: init, prepatch: prepatch } })
|
|
patch(vnode0, vnode1)
|
|
assert.strictEqual(1, count)
|
|
const vnode2 = h('span', { hook: { init: init, prepatch: prepatch } })
|
|
patch(vnode1, vnode2)
|
|
assert.strictEqual(2, count)
|
|
})
|
|
it('removes element when all remove listeners are done', function () {
|
|
let rm1, rm2, rm3
|
|
const patch = init([
|
|
{ remove: function (_, rm) { rm1 = rm } },
|
|
{ remove: function (_, rm) { rm2 = rm } },
|
|
])
|
|
const vnode1 = h('div', [h('a', {
|
|
hook: {
|
|
remove: function (_, rm) {
|
|
rm3 = rm
|
|
}
|
|
}
|
|
})])
|
|
const vnode2 = h('div', [])
|
|
elm = patch(vnode0, vnode1).elm
|
|
assert.strictEqual(elm.children.length, 1)
|
|
elm = patch(vnode1, vnode2).elm
|
|
assert.strictEqual(elm.children.length, 1);
|
|
(rm1 as any)()
|
|
assert.strictEqual(elm.children.length, 1);
|
|
(rm3 as any)()
|
|
assert.strictEqual(elm.children.length, 1);
|
|
(rm2 as any)()
|
|
assert.strictEqual(elm.children.length, 0)
|
|
})
|
|
it('invokes remove hook on replaced root', function () {
|
|
const result = []
|
|
const parent = document.createElement('div')
|
|
const vnode0 = document.createElement('div')
|
|
parent.appendChild(vnode0)
|
|
const cb: RemoveHook = (vnode, rm) => {
|
|
result.push(vnode)
|
|
rm()
|
|
}
|
|
const vnode1 = h('div', { hook: { remove: cb } }, [
|
|
h('b', 'Child 1'),
|
|
h('i', 'Child 2'),
|
|
])
|
|
const vnode2 = h('span', [
|
|
h('b', 'Child 1'),
|
|
h('i', 'Child 2'),
|
|
])
|
|
patch(vnode0, vnode1)
|
|
patch(vnode1, vnode2)
|
|
assert.strictEqual(1, result.length)
|
|
})
|
|
})
|
|
describe('module hooks', function () {
|
|
it('invokes `pre` and `post` hook', function () {
|
|
const result: string[] = []
|
|
const patch = init([
|
|
{ pre: function () { result.push('pre') } },
|
|
{ post: function () { result.push('post') } },
|
|
])
|
|
const vnode1 = h('div')
|
|
patch(vnode0, vnode1)
|
|
assert.deepEqual(result, ['pre', 'post'])
|
|
})
|
|
it('invokes global `destroy` hook for all removed children', function () {
|
|
const result = []
|
|
const cb: DestroyHook = (vnode) => {
|
|
result.push(vnode)
|
|
}
|
|
const vnode1 = h('div', [
|
|
h('span', 'First sibling'),
|
|
h('div', [
|
|
h('span', { hook: { destroy: cb } }, 'Child 1'),
|
|
h('span', 'Child 2'),
|
|
]),
|
|
])
|
|
const vnode2 = h('div')
|
|
patch(vnode0, vnode1)
|
|
patch(vnode1, vnode2)
|
|
assert.strictEqual(result.length, 1)
|
|
})
|
|
it('handles text vnodes with `undefined` `data` property', function () {
|
|
const vnode1 = h('div', [
|
|
' '
|
|
])
|
|
const vnode2 = h('div', [])
|
|
patch(vnode0, vnode1)
|
|
patch(vnode1, vnode2)
|
|
})
|
|
it('invokes `destroy` module hook for all removed children', function () {
|
|
let created = 0
|
|
let destroyed = 0
|
|
const patch = init([
|
|
{ create: function () { created++ } },
|
|
{ destroy: function () { destroyed++ } },
|
|
])
|
|
const vnode1 = h('div', [
|
|
h('span', 'First sibling'),
|
|
h('div', [
|
|
h('span', 'Child 1'),
|
|
h('span', 'Child 2'),
|
|
]),
|
|
])
|
|
const vnode2 = h('div')
|
|
patch(vnode0, vnode1)
|
|
patch(vnode1, vnode2)
|
|
assert.strictEqual(created, 4)
|
|
assert.strictEqual(destroyed, 4)
|
|
})
|
|
it('does not invoke `create` and `remove` module hook for text nodes', function () {
|
|
let created = 0
|
|
let removed = 0
|
|
const patch = init([
|
|
{ create: function () { created++ } },
|
|
{ remove: function () { removed++ } },
|
|
])
|
|
const vnode1 = h('div', [
|
|
h('span', 'First child'),
|
|
'',
|
|
h('span', 'Third child'),
|
|
])
|
|
const vnode2 = h('div')
|
|
patch(vnode0, vnode1)
|
|
patch(vnode1, vnode2)
|
|
assert.strictEqual(created, 2)
|
|
assert.strictEqual(removed, 2)
|
|
})
|
|
it('does not invoke `destroy` module hook for text nodes', function () {
|
|
let created = 0
|
|
let destroyed = 0
|
|
const patch = init([
|
|
{ create: function () { created++ } },
|
|
{ destroy: function () { destroyed++ } },
|
|
])
|
|
const vnode1 = h('div', [
|
|
h('span', 'First sibling'),
|
|
h('div', [
|
|
h('span', 'Child 1'),
|
|
h('span', ['Text 1', 'Text 2']),
|
|
]),
|
|
])
|
|
const vnode2 = h('div')
|
|
patch(vnode0, vnode1)
|
|
patch(vnode1, vnode2)
|
|
assert.strictEqual(created, 4)
|
|
assert.strictEqual(destroyed, 4)
|
|
})
|
|
})
|
|
})
|
|
describe('short circuiting', function () {
|
|
it('does not update strictly equal vnodes', function () {
|
|
const result = []
|
|
const cb: UpdateHook = (vnode) => {
|
|
result.push(vnode)
|
|
}
|
|
const vnode1 = h('div', [
|
|
h('span', { hook: { update: cb } }, 'Hello'),
|
|
h('span', 'there'),
|
|
])
|
|
patch(vnode0, vnode1)
|
|
patch(vnode1, vnode1)
|
|
assert.strictEqual(result.length, 0)
|
|
})
|
|
it('does not update strictly equal children', function () {
|
|
const result = []
|
|
function cb (vnode: VNode) {
|
|
result.push(vnode)
|
|
}
|
|
const vnode1 = h('div', [
|
|
h('span', { hook: { patch: cb } as any }, 'Hello'),
|
|
h('span', 'there'),
|
|
])
|
|
const vnode2 = h('div')
|
|
vnode2.children = vnode1.children
|
|
patch(vnode0, vnode1)
|
|
patch(vnode1, vnode2)
|
|
assert.strictEqual(result.length, 0)
|
|
})
|
|
})
|
|
})
|