virtex
Version:
Small, focused virtual dom library.
1,116 lines (939 loc) • 27.5 kB
JavaScript
/**
* Imports
*/
import test from 'tape'
import virtex from '../src'
import dom from 'virtex-dom'
import raf from 'component-raf'
import delegant from 'delegant'
import trigger from 'trigger-event'
import element from 'virtex-element'
import component from 'virtex-component'
import {createStore, applyMiddleware} from 'redux'
/**
* Setup store
*/
let context = {}
let forceUpdate = false
const store = applyMiddleware(dom, component({
getContext: () => context,
ignoreShouldUpdate: () => forceUpdate
}))(createStore)(() => {}, {})
/**
* Initialize virtex
*/
const {create, update} = virtex(store.dispatch)
// Test Components
const RenderChildren = ({children}) => children[0]
const ListItem = ({children}) => <li>{children}</li>
const Wrapper = ({children}) => <div>{children}</div>
const TwoWords = ({props}) => <span>{props.one} {props.two}</span>
// Test helpers
function div () {
const el = document.createElement('div')
document.body.appendChild(el)
return el
}
function setup (equal) {
const el = div()
delegant(el)
let tree
let node
return {
mount (vnode) {
if (tree) node = update(tree, vnode).element
else {
node = create(vnode).element
el.appendChild(node)
}
tree = vnode
},
unmount () {
node.parentNode.removeChild(node)
tree = null
},
renderer: {
remove () {
const newTree = {type: 'fake-element', children: []}
const parentNode = node.parentNode
update(tree, newTree)
parentNode.removeChild(parentNode.firstChild)
tree = newTree
// node.parentNode.removeChild(node)
tree = null
}
},
el,
$: el.querySelector.bind(el),
html: createAssertHTML(el, equal)
}
}
function teardown ({renderer, el}) {
renderer.remove()
if (el.parentNode) el.parentNode.removeChild(el)
}
function createAssertHTML (container, equal) {
const dummy = document.createElement('div')
return (html, message) => {
html = html.replace(/\n(\s+)?/g,'').replace(/\s+/g,' ')
equal(html, container.innerHTML, message || 'innerHTML is equal')
}
}
/**
* Tests
*
* Note: Essentially 100% of these tests copied from Deku
* https://github.com/dekujs/deku/blob/1.0.0/test/dom/index.js
* So all credit to them for this great test suite
*/
test('rendering DOM', t => {
const {renderer, el, mount, unmount, html} = setup(t.equal)
let rootEl
// Render
mount(<span />)
html('<span></span>', 'no attribute')
// Add
mount(<span name="Bob" />)
html('<span name="Bob"></span>', 'attribute added')
// Update
mount(<span name="Tom" />)
html('<span name="Tom"></span>', 'attribute updated')
// Update
mount(<span name={null} />)
html('<span></span>', 'attribute removed with null')
// Update
mount(<span name={undefined} />)
html('<span></span>', 'attribute removed with undefined')
// Update
mount(<span name="Bob" />)
el.children[0].setAttribute = () => fail('DOM was touched')
// Update
mount(<span name="Bob" />)
t.pass('DOM not updated without change')
// Update
mount(<span>Hello World</span>)
html(`<span>Hello World</span>`, 'text rendered')
rootEl = el.firstChild
// Update
mount(<span>Hello Pluto</span>)
html('<span>Hello Pluto</span>', 'text updated')
// Remove
mount(<span></span>)
html('<span></span>', 'text removed')
// Update
mount(<span>{undefined} World</span>)
html('<span> World</span>', 'text was replaced by undefined')
// Root element should still be the same
t.equal(el.firstChild, rootEl, 'root element not replaced')
// Replace
mount(<div>Foo!</div>)
html('<div>Foo!</div>', 'element is replaced')
t.notEqual(el.firstChild, rootEl, 'root element replaced')
// Clear
unmount()
html('', 'element is removed when unmounted')
// Render
mount(<div>Foo!</div>)
html('<div>Foo!</div>', 'element is rendered again')
rootEl = el.firstChild
// Update
mount(<div><span/></div>)
html('<div><span></span></div>', 'replaced text with an element')
// Update
mount(<div>bar</div>)
html('<div>bar</div>', 'replaced child with text')
// Update
mount(<div><span>Hello World</span></div>)
html('<div><span>Hello World</span></div>', 'replaced text with element')
// Remove
mount(<div></div>)
html('<div></div>', 'removed element')
t.equal(el.firstChild, rootEl, 'root element not replaced')
// Children added
mount(
<div>
<span>one</span>
<span>two</span>
<span>three</span>
</div>
)
html(`
<div>
<span>one</span>
<span>two</span>
<span>three</span>
</div>`
)
t.equal(el.firstChild, rootEl, 'root element not replaced')
const span = el.firstChild.firstChild
// Siblings removed
mount(
<div>
<span>one</span>
</div>
)
html('<div><span>one</span></div>', 'added element')
t.equal(el.firstChild.firstChild, span, 'child element not replaced')
t.equal(el.firstChild, rootEl, 'root element not replaced')
// Removing the renderer
teardown({renderer, el})
html('', 'element is removed')
t.end()
})
test('falsy attributes should not touch the DOM', t => {
const {renderer, el, mount} = setup(t.equal)
mount(<span name="" />)
const child = el.children[0]
child.setAttribute = () => t.fail('should not set attributes')
child.removeAttribute = () => t.fail('should not remove attributes')
mount(<span name="" />)
t.pass('DOM not touched')
teardown({renderer, el})
t.end()
})
test('innerHTML attribute', t => {
const {html, mount, el, renderer} = setup(t.equal)
mount(<div innerHTML="Hello <strong>deku</strong>" />)
html('<div>Hello <strong>deku</strong></div>', 'innerHTML is rendered')
mount(<div innerHTML="Hello <strong>Pluto</strong>" />)
html('<div>Hello <strong>Pluto</strong></div>', 'innerHTML is updated')
mount(<div />)
// Causing issues in IE10. Renders with a for some reason
// html('<div></div>', 'innerHTML is removed')
teardown({renderer, el})
t.end()
})
test('input attributes', t => {
const {html, mount, el, renderer, $} = setup(t.equal)
mount(<input />)
const checkbox = $('input')
t.comment('input.value')
mount(<input value="Bob" />)
t.equal(checkbox.value, 'Bob', 'value property set')
mount(<input value="Tom" />)
t.equal(checkbox.value, 'Tom', 'value property updated')
mount(<input />)
t.equal(checkbox.value, '', 'value property removed')
t.comment('input cursor position')
mount(<input type="text" value="Game of Thrones" />)
let input = $('input')
input.focus()
input.setSelectionRange(5,7)
mount(<input type="text" value="Way of Kings" />)
t.equal(input.selectionStart, 5, 'selection start')
t.equal(input.selectionEnd, 7, 'selection end')
t.comment('input cursor position on inputs that don\'t support text selection')
mount(<input type="email" value="a@b.com" />)
t.comment('input cursor position only the active element')
mount(<input type="text" value="Hello World" />)
input = $('input')
input.setSelectionRange(5,7)
if (input.setActive) document.body.setActive()
else input.blur()
mount(<input type="text" value="Hello World!" />)
t.notEqual(input.selectionStart, 5, 'selection start')
t.notEqual(input.selectionEnd, 7, 'selection end')
t.comment('input.checked')
mount(<input checked={true} />)
t.ok(checkbox.checked, 'checked with a true value')
t.equal(checkbox.getAttribute('checked'), null, 'has checked attribute')
mount(<input checked={false} />)
t.ok(!checkbox.checked, 'unchecked with a false value')
t.ok(!checkbox.hasAttribute('checked'), 'has no checked attribute')
mount(<input checked />)
t.ok(checkbox.checked, 'checked with a boolean attribute')
t.equal(checkbox.getAttribute('checked'), null, 'has checked attribute')
mount(<input />)
t.ok(!checkbox.checked, 'unchecked when attribute is removed')
t.ok(!checkbox.hasAttribute('checked'), 'has no checked attribute')
t.comment('input.disabled')
mount(<input disabled={true} />)
t.ok(checkbox.disabled, 'disabled with a true value')
t.equal(checkbox.hasAttribute('disabled'), true, 'has disabled attribute')
mount(<input disabled={false} />)
t.equal(checkbox.disabled, false, 'disabled is false with false value')
t.equal(checkbox.hasAttribute('disabled'), false, 'has no disabled attribute')
mount(<input disabled />)
t.ok(checkbox.disabled, 'disabled is true with a boolean attribute')
t.equal(checkbox.hasAttribute('disabled'), true, 'has disabled attribute')
mount(<input />)
t.equal(checkbox.disabled, false, 'disabled is false when attribute is removed')
t.equal(checkbox.hasAttribute('disabled'), false, 'has no disabled attribute')
teardown({renderer, el})
t.end()
})
test('option[selected]', t => {
const {mount, renderer, el} = setup(t.equal)
let options
// first should be selected
mount(
<select>
<option selected>one</option>
<option>two</option>
</select>
)
options = el.querySelectorAll('option')
t.ok(!options[1].selected, 'is not selected')
t.ok(options[0].selected, 'is selected')
// second should be selected
mount(
<select>
<option>one</option>
<option selected>two</option>
</select>
)
options = el.querySelectorAll('option')
t.ok(!options[0].selected, 'is not selected')
t.ok(options[1].selected, 'is selected')
teardown({renderer, el})
t.end()
})
test('components', t => {
const {el, renderer, mount, html} = setup(t.equal)
const Test = ({props}) => <span count={props.count}>Hello World</span>
mount(<Test count={2} />)
const root = el.firstElementChild
t.equal(root.getAttribute('count'), '2', 'rendered with props')
mount(<Test count={3} />)
t.equal(root.getAttribute('count'), '3', 'props updated')
teardown({renderer,el})
t.equal(el.innerHTML, '', 'the element is removed')
t.end()
})
test('simple components', t => {
const {el, renderer, mount, html} = setup(t.equal)
const Box = ({props}) => <div>{props.text}</div>
mount(<Box text="Hello World" />)
html('<div>Hello World</div>', 'function component rendered')
teardown({renderer, el})
t.end()
})
test('nested component lifecycle hooks fire in the correct order', t => {
const {el, renderer, mount} = setup(t.equal)
let log = []
const LifecycleLogger = {
render ({props, children}) {
log.push(props.name + ' render')
return <div>{children}</div>
},
onCreate ({props}) {
log.push(props.name + ' onCreate')
},
onRemove ({props}) {
log.push(props.name + ' onRemove')
},
shouldUpdate () {
return true
}
}
mount(
<Wrapper>
<LifecycleLogger name="GrandParent">
<LifecycleLogger name="Parent">
<LifecycleLogger name="Child" />
</LifecycleLogger>
</LifecycleLogger>
</Wrapper>
)
t.deepEqual(log, [
'GrandParent onCreate',
'GrandParent render',
'Parent onCreate',
'Parent render',
'Child onCreate',
'Child render'
], 'initial render')
log = []
mount(
<Wrapper>
<LifecycleLogger name="GrandParent">
<LifecycleLogger name="Parent">
<LifecycleLogger name="Child" />
</LifecycleLogger>
</LifecycleLogger>
</Wrapper>
)
t.deepEqual(log, [
'GrandParent render',
'Parent render',
'Child render',
], 'updated')
log = []
mount(<Wrapper></Wrapper>)
t.deepEqual(log, [
'Child onRemove',
'Parent onRemove',
'GrandParent onRemove'
], 'unmounted')
mount(
<Wrapper>
<LifecycleLogger name="GrandParent">
<LifecycleLogger name="Parent">
<LifecycleLogger name="Child" />
</LifecycleLogger>
</LifecycleLogger>
</Wrapper>
)
log = []
teardown({renderer, el})
t.deepEqual(log, [
'Child onRemove',
'Parent onRemove',
'GrandParent onRemove'
], 'unmounted')
t.end()
})
test('component lifecycle hook signatures', t => {
const {mount, renderer, el} = setup(t.equal)
const MyComponent = {
render ({props}) {
t.ok(props, 'render has props')
return <div id="foo" />
},
onCreate ({props}) {
t.ok(props, 'onCreate has props')
},
onRemove ({props}) {
t.ok(props, 'onRemove has props')
t.end()
}
}
mount(<MyComponent />)
teardown({renderer, el})
})
test('replace props instead of merging', t => {
const {mount, renderer, el} = setup(t.equal)
mount(<TwoWords one="Hello" two="World" />)
mount(<TwoWords two="Pluto" />)
t.equal(el.innerHTML, '<span> Pluto</span>')
teardown({renderer,el})
t.end()
})
test(`should update all children when a parent component changes`, t => {
const {mount, renderer, el} = setup(t.equal)
let parentCalls = 0
let childCalls = 0
const Child = {
render ({props}) {
childCalls++
return <span>{props.text}</span>
},
shouldUpdate () {
return true
}
}
const Parent = {
render ({props}) {
parentCalls++
return (
<div name={props.character}>
<Child text="foo" />
</div>
)
}
}
mount(<Parent character="Link" />)
mount(<Parent character="Zelda" />)
t.equal(childCalls, 2, 'child rendered twice')
t.equal(parentCalls, 2, 'parent rendered twice')
teardown({renderer, el})
t.end()
})
test.skip('batched rendering', t => {
let i = 0
const IncrementOnUpdate = {
render: function(){
return <div></div>
},
onUpdate: function(){
i++
}
}
const el = document.createElement('div')
const app = deku()
app.mount(<IncrementOnUpdate text="one" />)
const renderer = render(app, el)
app.mount(<IncrementOnUpdate text="two" />)
app.mount(<IncrementOnUpdate text="three" />)
raf(() => {
t.equal(i, 1, 'rendered *once* on the next frame')
renderer.remove()
t.end()
})
})
test('rendering nested components', t => {
const {mount, renderer, el, html} = setup(t.equal)
const ComponentA = ({children}) => <div name="ComponentA">{children}</div>
const ComponentB = ({children}) => <div name="ComponentB">{children}</div>
const ComponentC = ({props}) => {
return (
<div name="ComponentC">
<ComponentB>
<ComponentA>
<span>{props.text}</span>
</ComponentA>
</ComponentB>
</div>
)
}
mount(<ComponentC text='Hello World!' />)
html('<div name="ComponentC"><div name="ComponentB"><div name="ComponentA"><span>Hello World!</span></div></div></div>', 'element is rendered')
mount(<ComponentC text='Hello Pluto!' />)
t.equal(el.innerHTML, '<div name="ComponentC"><div name="ComponentB"><div name="ComponentA"><span>Hello Pluto!</span></div></div></div>', 'element is updated with props')
teardown({renderer, el})
html('', 'element is removed')
t.end()
})
test('skipping updates when the same virtual element is returned', t => {
const {mount, renderer, el} = setup(t.equal)
let i = 0
const vnode = <div onUpdate={el => i++} />
const Component = {
render (component) {
return vnode
}
}
mount(<Component />)
mount(<Component />)
t.equal(i, 1, 'component not updated')
teardown({renderer, el})
t.end()
})
test('firing mount events on sub-components created later', t => {
const {mount, renderer, el} = setup(t.equal)
const ComponentA = {
render: () => <div />,
onRemove: () => t.pass('onRemove called'),
onCreate: () => t.pass('onCreate called')
}
t.plan(2)
mount(<ComponentA />)
mount(<div />)
teardown({renderer, el})
})
test('should change root node and still update correctly', t => {
const {mount, html, renderer, el} = setup(t.equal)
const ComponentA = ({props}) => element(props.type, null, props.text)
const Test = ({props}) => <ComponentA type={props.type} text={props.text} />
mount(<Test type="span" text="test" />)
html('<span>test</span>')
mount(<Test type="div" text="test" />)
html('<div>test</div>')
mount(<Test type="div" text="foo" />)
html('<div>foo</div>')
teardown({renderer, el})
t.end()
})
test('replacing components with other components', t => {
const {mount, renderer, el, html} = setup(t.equal)
const ComponentA = () => <div>A</div>
const ComponentB = () => <div>B</div>
const ComponentC = ({props}) => {
if (props.type === 'A') {
return <ComponentA />
} else {
return <ComponentB />
}
}
mount(<ComponentC type="A" />)
html('<div>A</div>')
mount(<ComponentC type="B" />)
html('<div>B</div>')
teardown({renderer, el})
t.end()
})
test('adding, removing and updating events', t => {
const {mount, renderer, el, $} = setup(t.equal)
let count = 0
const onclicka = () => count += 1
const onclickb = () => count -= 1
mount(<Page clicker={onclicka} />)
trigger($('span'), 'click')
t.equal(count, 1, 'event added')
mount(<Page clicker={onclickb} />)
trigger($('span'), 'click')
t.equal(count, 0, 'event updated')
mount(<Page />)
trigger($('span'), 'click')
t.equal(count, 0, 'event removed')
teardown({renderer, el})
t.end()
function Page ({props}) {
return <span onClick={props.clicker} />
}
})
test('should bubble events', t => {
const {mount, renderer, el, $} = setup(t.equal)
const state = {}
const Test = {
render: function ({props}) {
const {state} = props
return (
<div onClick={onParentClick}>
<div class={state.active ? 'active' : ''} onClick={onClickTest}>
<a>link</a>
</div>
</div>
)
},
shouldUpdate () {
return true
}
}
mount(<Test state={state} />)
trigger($('a'), 'click')
t.equal(state.active, true, 'state was changed')
mount(<Test state={state} />)
t.ok($('.active'), 'event fired on parent element')
teardown({renderer, el})
t.end()
function onClickTest (event) {
state.active = true
t.equal(el.firstChild.firstChild.firstChild, event.target, 'event.target is set')
event.stopImmediatePropagation()
}
function onParentClick () {
t.fail('event bubbling was not stopped')
}
})
test('unmounting components when removing an element', t => {
const {mount, renderer, el} = setup(t.equal)
const Test = {
render: () => <div />,
onRemove: () => t.pass('component was unmounted')
}
t.plan(1)
mount(<div><div><Test /></div></div>)
mount(<div></div>)
teardown({renderer, el})
t.end()
})
test('update sub-components with the same element', t => {
const {mount, renderer, el} = setup(t.equal)
const Page1 = {
render ({props}) {
return (
<Wrapper>
<Wrapper>
<Wrapper>
{
props.show ?
<div>
<label/>
<input/>
</div>
:
<span>
Hello
</span>
}
</Wrapper>
</Wrapper>
</Wrapper>
)
}
}
const Page2 = ({props}) => {
return (
<div>
<span>{props.title}</span>
</div>
)
}
const App = ({props}) => props.page === 1 ? <Page1 show={props.show} /> : <Page2 title={props.title} />
mount(<App page={1} show={true} />)
mount(<App page={1} show={false} />)
mount(<App page={2} title="Hello World" />)
mount(<App page={2} title="foo" />)
t.equal(el.innerHTML, '<div><span>foo</span></div>')
teardown({renderer, el})
t.end()
})
test('replace elements with component nodes', t => {
const {mount, renderer, el} = setup(t.equal)
mount(<span/>)
t.equal(el.innerHTML, '<span></span>', 'rendered element')
mount(<Wrapper>component</Wrapper>)
t.equal(el.innerHTML, '<div>component</div>', 'replaced with component')
teardown({renderer, el})
t.end()
})
test('svg elements', t => {
const {mount, renderer, el} = setup(t.equal)
mount(
<svg width="92px" height="92px" viewBox="0 0 92 92">
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<circle id="circle" fill="#D8D8D8" cx="46" cy="46" r="46"></circle>
</g>
</svg>
)
t.equal(el.firstChild.tagName, 'svg', 'rendered svg element')
teardown({renderer, el})
t.end()
})
test('moving components with keys', t => {
const {mount, renderer, el} = setup(t.equal)
let one, two, three
t.plan(10)
mount(
<ul>
<ListItem key="foo">One</ListItem>
<ListItem key="bar">Two</ListItem>
</ul>
)
;[one, two] = [].slice.call(el.querySelectorAll('li'))
// Moving
mount(
<ul>
<ListItem key="bar">Two</ListItem>
<ListItem key="foo">One</ListItem>
</ul>
)
let updated = el.querySelectorAll('li')
t.ok(updated[1] === one, 'foo moved down')
t.ok(updated[0] === two, 'bar moved up')
// Removing
mount(
<ul>
<ListItem key="bar">Two</ListItem>
</ul>
)
updated = el.querySelectorAll('li')
t.ok(updated[0] === two && updated.length === 1, 'foo was removed')
// Updating
mount(
<ul>
<ListItem key="foo">One</ListItem>
<ListItem key="bar">Two</ListItem>
<ListItem key="baz">Three</ListItem>
</ul>
)
;[one,two,three] = [].slice.call(el.querySelectorAll('li'))
mount(
<ul>
<ListItem key="foo">One</ListItem>
<ListItem key="baz">Four</ListItem>
</ul>
)
updated = el.querySelectorAll('li')
t.ok(updated[0] === one, 'foo is the same')
t.ok(updated[1] === three, 'baz is the same')
t.ok(updated[1].innerHTML === 'Four', 'baz was updated')
let [foo, baz] = [].slice.call(updated)
// Adding
mount(
<ul>
<ListItem key="foo">One</ListItem>
<ListItem key="bar">Five</ListItem>
<ListItem key="baz">Four</ListItem>
</ul>
)
updated = el.querySelectorAll('li')
t.ok(updated[0] === foo, 'foo is the same')
t.ok(updated[2] === baz, 'baz is the same')
t.ok(updated[1].innerHTML === 'Five', 'bar was added')
// Moving event handlers
const clicked = () => t.pass('event handler moved')
mount(
<ul>
<ListItem key="foo">One</ListItem>
<ListItem key="bar">
<span onClick={clicked}>Click Me!</span>
</ListItem>
</ul>
)
mount(
<ul>
<ListItem key="bar">
<span onClick={clicked}>Click Me!</span>
</ListItem>
<ListItem key="foo">One</ListItem>
</ul>
)
trigger(el.querySelector('span'), 'click')
// Removing handlers. If the handler isn't removed from
// the path correctly, it will still fire the handler from
// the previous assertion.
mount(
<ul>
<ListItem key="foo">
<span>One</span>
</ListItem>
</ul>
)
trigger(el.querySelector('span'), 'click')
teardown({renderer, el})
t.end()
})
test('updating event handlers when children are removed', t => {
const {mount, renderer, el} = setup(t.equal)
const items = ['foo','bar','baz']
const ListItem = {
shouldUpdate () { return true },
render ({props}) {
return (
<li>
<a onClick={e => { items.splice(props.index, 1); console.log('remove') }} />
</li>
)
}
}
const List = {
shouldUpdate () { return true },
render ({props}) {
return (
<ul>
{props.items.map((_,i) => <ListItem index={i} />)}
</ul>
)
}
}
mount(<List items={items} />)
trigger(el.querySelector('a'), 'click')
mount(<List items={items} />)
trigger(el.querySelector('a'), 'click')
mount(<List items={items} />)
trigger(el.querySelector('a'), 'click')
mount(<List items={items} />)
t.equal(el.innerHTML, '<ul></ul>', 'all items were removed')
teardown({renderer, el})
t.end()
})
test('components should receive path based keys if they are not specified', t => {
t.end()
})
test('array jsx', t => {
const {mount, renderer, el} = setup(t.equal)
const arr = [1, 2]
mount(
<div>
Hello World
{arr.map(i => <span>{i}</span>)}
<hr/>
</div>
)
const n = el.firstChild
t.equal(n.childNodes[0].nodeName, '#text')
t.equal(n.childNodes[1].nodeName, 'SPAN')
t.equal(n.childNodes[2].nodeName, 'SPAN')
t.equal(n.childNodes[3].nodeName, 'HR')
teardown({renderer, el})
t.end()
})
test('getProps', t => {
const {mount, renderer, el} = setup(t.equal)
const vals = {}
function save (name) { return ({props}) => { vals[name] = props.value }}
const Test = {
getProps () {
return {
value: 1
}
},
onCreate: save('create'),
render ({props}) {
vals.render = props.value
return <span></span>
},
onUpdate: save('update'),
onRemove: save('remove'),
shouldUpdate () { return true }
}
mount(<Test value={3} />)
mount(<Test value={4} />)
mount(<span></span>)
t.equal(vals.create, 1, 'onCreate hook')
t.equal(vals.render, 1, 'render')
t.equal(vals.update, 1, 'onUpdate hook')
t.equal(vals.remove, 1, 'onRemove hook')
teardown({renderer, el})
t.end()
})
test('should have context', t => {
const {mount, renderer, el} = setup(t.equal)
let a, b, c
const CtxTest = {
getProps (props, context) {
return {
...props,
...context
}
},
render ({props}) {
a = props.a
b = props.b
c = props.c
return <span/>
}
}
context = {b: 2}
mount(<CtxTest a={1} />)
t.equal(a, 1)
t.equal(b, 2)
t.equal(c, undefined)
context = {b: 2, c: 3}
forceUpdate = true
mount(<CtxTest a={1} />)
forceUpdate = false
t.equal(a, 1)
t.equal(b, 2)
t.equal(c, 3)
teardown({renderer, el})
t.end()
})
test('event handlers should be removed properly', t => {
const {renderer, el, mount} = setup(t.equal)
mount(<span onClick={() => t.pass()} />)
t.plan(1)
const child = el.children[0]
trigger(child, 'click')
mount(<span />)
trigger(child, 'click')
teardown({renderer, el})
t.end()
})
test('diff', t => {
t.test('reverse', diffXf(r => r.reverse()))
t.test('prepend (1)', diffXf(r => [11].concat(r)))
t.test('remove (1)', diffXf(r => r.slice(1)))
t.test('reverse, remove(1)', diffXf(r => r.reverse().slice(1)))
t.test('remove (1), reverse', diffXf(r => r.slice(1).reverse()))
t.test('reverse, append (1)', diffXf(r => r.reverse().concat(11)))
t.test('reverse, prepend (1)', diffXf(r => [11].concat(r.reverse())))
t.test('sides reversed, middle same', diffXf(r => r.slice().reverse().slice(0, 3).concat(r.slice(3, 7)).concat(r.slice().reverse().slice(7))))
t.test('replace all', diffXf(r => range(11, 25)))
t.test('insert (3), randomize', diffXf(r => randomize(r.concat(range(13, 17)))))
t.test('moveFromStartToEnd (1)', diffXf(r => r.slice(1).concat(r[0])))
})
function randomize (r) {
return r.reduce(acc => {
const i = Math.floor(Math.random() * 100000) % r.length
acc.push(r[i])
r.splice(i, 1)
return acc
}, [])
}
function range (begin, end) {
const r = []
for (let i = begin; i < end; i++) {
r.push(i)
}
return r
}
function diffXf (xf) {
return t => {
const r = range(0, 10)
diffTest(t, r, xf(r.slice()))
t.end()
}
}
function diffTest (t, a, b) {
const {mount, renderer, el} = setup(t.equal)
mount(<div>{a.map(i => <span key={i}>{i}</span>)}</div>)
mount(<div>{b.map(i => <span key={i}>{i}</span>)}</div>)
const node = el.firstChild
for (let i = 0; i < node.childNodes.length; i++) {
t.equal(node.childNodes[i].textContent, b[i].toString())
}
t.equal(node.childNodes.length, b.length)
}