UNPKG

27.2 kBJavaScriptView Raw
1/**
2 * Imports
3 */
4
5import test from 'tape'
6import virtex from '../src'
7import dom from 'virtex-dom'
8import raf from 'component-raf'
9import delegant from 'delegant'
10import trigger from 'trigger-event'
11import element from 'virtex-element'
12import component from 'virtex-component'
13import {createStore, applyMiddleware} from 'redux'
14
15/**
16 * Setup store
17 */
18
19let context = {}
20let forceUpdate = false
21const store = applyMiddleware(dom, component({
22 getContext: () => context,
23 ignoreShouldUpdate: () => forceUpdate
24}))(createStore)(() => {}, {})
25
26/**
27 * Initialize virtex
28 */
29
30const {create, update} = virtex(store.dispatch)
31
32// Test Components
33
34const RenderChildren = ({children}) => children[0]
35const ListItem = ({children}) => <li>{children}</li>
36const Wrapper = ({children}) => <div>{children}</div>
37const TwoWords = ({props}) => <span>{props.one} {props.two}</span>
38
39// Test helpers
40
41function div () {
42 const el = document.createElement('div')
43 document.body.appendChild(el)
44 return el
45}
46
47function setup (equal) {
48 const el = div()
49
50 delegant(el)
51 let tree
52 let node
53 return {
54 mount (vnode) {
55 if (tree) node = update(tree, vnode).element
56 else {
57 node = create(vnode).element
58 el.appendChild(node)
59 }
60
61 tree = vnode
62 },
63 unmount () {
64 node.parentNode.removeChild(node)
65 tree = null
66 },
67 renderer: {
68 remove () {
69 const newTree = {type: 'fake-element', children: []}
70 const parentNode = node.parentNode
71 update(tree, newTree)
72 parentNode.removeChild(parentNode.firstChild)
73 tree = newTree
74 // node.parentNode.removeChild(node)
75 tree = null
76 }
77 },
78 el,
79 $: el.querySelector.bind(el),
80 html: createAssertHTML(el, equal)
81 }
82}
83
84function teardown ({renderer, el}) {
85 renderer.remove()
86 if (el.parentNode) el.parentNode.removeChild(el)
87}
88
89function createAssertHTML (container, equal) {
90 const dummy = document.createElement('div')
91 return (html, message) => {
92 html = html.replace(/\n(\s+)?/g,'').replace(/\s+/g,' ')
93 equal(html, container.innerHTML, message || 'innerHTML is equal')
94 }
95}
96
97/**
98 * Tests
99 *
100 * Note: Essentially 100% of these tests copied from Deku
101 * https://github.com/dekujs/deku/blob/1.0.0/test/dom/index.js
102 * So all credit to them for this great test suite
103 */
104
105test('rendering DOM', t => {
106 const {renderer, el, mount, unmount, html} = setup(t.equal)
107 let rootEl
108
109 // Render
110 mount(<span />)
111 html('<span></span>', 'no attribute')
112
113 // Add
114 mount(<span name="Bob" />)
115 html('<span name="Bob"></span>', 'attribute added')
116
117 // Update
118 mount(<span name="Tom" />)
119 html('<span name="Tom"></span>', 'attribute updated')
120
121 // Update
122 mount(<span name={null} />)
123 html('<span></span>', 'attribute removed with null')
124
125 // Update
126 mount(<span name={undefined} />)
127 html('<span></span>', 'attribute removed with undefined')
128
129 // Update
130 mount(<span name="Bob" />)
131 el.children[0].setAttribute = () => fail('DOM was touched')
132
133 // Update
134 mount(<span name="Bob" />)
135 t.pass('DOM not updated without change')
136
137 // Update
138 mount(<span>Hello World</span>)
139 html(`<span>Hello World</span>`, 'text rendered')
140
141 rootEl = el.firstChild
142
143 // Update
144 mount(<span>Hello Pluto</span>)
145 html('<span>Hello Pluto</span>', 'text updated')
146
147 // Remove
148 mount(<span></span>)
149 html('<span></span>', 'text removed')
150
151 // Update
152 mount(<span>{undefined} World</span>)
153 html('<span> World</span>', 'text was replaced by undefined')
154
155 // Root element should still be the same
156 t.equal(el.firstChild, rootEl, 'root element not replaced')
157
158 // Replace
159 mount(<div>Foo!</div>)
160 html('<div>Foo!</div>', 'element is replaced')
161 t.notEqual(el.firstChild, rootEl, 'root element replaced')
162
163 // Clear
164 unmount()
165 html('', 'element is removed when unmounted')
166
167 // Render
168 mount(<div>Foo!</div>)
169 html('<div>Foo!</div>', 'element is rendered again')
170
171 rootEl = el.firstChild
172
173 // Update
174 mount(<div><span/></div>)
175 html('<div><span></span></div>', 'replaced text with an element')
176
177 // Update
178 mount(<div>bar</div>)
179 html('<div>bar</div>', 'replaced child with text')
180
181 // Update
182 mount(<div><span>Hello World</span></div>)
183 html('<div><span>Hello World</span></div>', 'replaced text with element')
184
185 // Remove
186 mount(<div></div>)
187 html('<div></div>', 'removed element')
188 t.equal(el.firstChild, rootEl, 'root element not replaced')
189
190 // Children added
191 mount(
192 <div>
193 <span>one</span>
194 <span>two</span>
195 <span>three</span>
196 </div>
197 )
198 html(`
199 <div>
200 <span>one</span>
201 <span>two</span>
202 <span>three</span>
203 </div>`
204 )
205
206 t.equal(el.firstChild, rootEl, 'root element not replaced')
207 const span = el.firstChild.firstChild
208
209 // Siblings removed
210 mount(
211 <div>
212 <span>one</span>
213 </div>
214 )
215 html('<div><span>one</span></div>', 'added element')
216 t.equal(el.firstChild.firstChild, span, 'child element not replaced')
217 t.equal(el.firstChild, rootEl, 'root element not replaced')
218
219 // Removing the renderer
220 teardown({renderer, el})
221 html('', 'element is removed')
222 t.end()
223})
224
225test('falsy attributes should not touch the DOM', t => {
226 const {renderer, el, mount} = setup(t.equal)
227 mount(<span name="" />)
228
229 const child = el.children[0]
230 child.setAttribute = () => t.fail('should not set attributes')
231 child.removeAttribute = () => t.fail('should not remove attributes')
232
233 mount(<span name="" />)
234 t.pass('DOM not touched')
235 teardown({renderer, el})
236 t.end()
237})
238
239test('innerHTML attribute', t => {
240 const {html, mount, el, renderer} = setup(t.equal)
241 mount(<div innerHTML="Hello <strong>deku</strong>" />)
242 html('<div>Hello <strong>deku</strong></div>', 'innerHTML is rendered')
243 mount(<div innerHTML="Hello <strong>Pluto</strong>" />)
244 html('<div>Hello <strong>Pluto</strong></div>', 'innerHTML is updated')
245 mount(<div />)
246 // Causing issues in IE10. Renders with a &nbsp; for some reason
247 // html('<div></div>', 'innerHTML is removed')
248 teardown({renderer, el})
249 t.end()
250})
251
252test('input attributes', t => {
253 const {html, mount, el, renderer, $} = setup(t.equal)
254 mount(<input />)
255 const checkbox = $('input')
256
257 t.comment('input.value')
258 mount(<input value="Bob" />)
259 t.equal(checkbox.value, 'Bob', 'value property set')
260 mount(<input value="Tom" />)
261 t.equal(checkbox.value, 'Tom', 'value property updated')
262
263 mount(<input />)
264 t.equal(checkbox.value, '', 'value property removed')
265
266 t.comment('input cursor position')
267 mount(<input type="text" value="Game of Thrones" />)
268 let input = $('input')
269 input.focus()
270 input.setSelectionRange(5,7)
271 mount(<input type="text" value="Way of Kings" />)
272 t.equal(input.selectionStart, 5, 'selection start')
273 t.equal(input.selectionEnd, 7, 'selection end')
274
275 t.comment('input cursor position on inputs that don\'t support text selection')
276 mount(<input type="email" value="a@b.com" />)
277
278 t.comment('input cursor position only the active element')
279 mount(<input type="text" value="Hello World" />)
280 input = $('input')
281 input.setSelectionRange(5,7)
282 if (input.setActive) document.body.setActive()
283 else input.blur()
284 mount(<input type="text" value="Hello World!" />)
285 t.notEqual(input.selectionStart, 5, 'selection start')
286 t.notEqual(input.selectionEnd, 7, 'selection end')
287
288 t.comment('input.checked')
289 mount(<input checked={true} />)
290 t.ok(checkbox.checked, 'checked with a true value')
291 t.equal(checkbox.getAttribute('checked'), null, 'has checked attribute')
292 mount(<input checked={false} />)
293
294 t.ok(!checkbox.checked, 'unchecked with a false value')
295 t.ok(!checkbox.hasAttribute('checked'), 'has no checked attribute')
296 mount(<input checked />)
297 t.ok(checkbox.checked, 'checked with a boolean attribute')
298 t.equal(checkbox.getAttribute('checked'), null, 'has checked attribute')
299 mount(<input />)
300 t.ok(!checkbox.checked, 'unchecked when attribute is removed')
301 t.ok(!checkbox.hasAttribute('checked'), 'has no checked attribute')
302
303 t.comment('input.disabled')
304 mount(<input disabled={true} />)
305 t.ok(checkbox.disabled, 'disabled with a true value')
306 t.equal(checkbox.hasAttribute('disabled'), true, 'has disabled attribute')
307 mount(<input disabled={false} />)
308 t.equal(checkbox.disabled, false, 'disabled is false with false value')
309 t.equal(checkbox.hasAttribute('disabled'), false, 'has no disabled attribute')
310 mount(<input disabled />)
311 t.ok(checkbox.disabled, 'disabled is true with a boolean attribute')
312 t.equal(checkbox.hasAttribute('disabled'), true, 'has disabled attribute')
313 mount(<input />)
314 t.equal(checkbox.disabled, false, 'disabled is false when attribute is removed')
315 t.equal(checkbox.hasAttribute('disabled'), false, 'has no disabled attribute')
316
317 teardown({renderer, el})
318 t.end()
319})
320
321test('option[selected]', t => {
322 const {mount, renderer, el} = setup(t.equal)
323 let options
324
325 // first should be selected
326 mount(
327 <select>
328 <option selected>one</option>
329 <option>two</option>
330 </select>
331 )
332
333 options = el.querySelectorAll('option')
334 t.ok(!options[1].selected, 'is not selected')
335 t.ok(options[0].selected, 'is selected')
336
337 // second should be selected
338 mount(
339 <select>
340 <option>one</option>
341 <option selected>two</option>
342 </select>
343 )
344
345 options = el.querySelectorAll('option')
346 t.ok(!options[0].selected, 'is not selected')
347 t.ok(options[1].selected, 'is selected')
348
349 teardown({renderer, el})
350 t.end()
351})
352
353test('components', t => {
354 const {el, renderer, mount, html} = setup(t.equal)
355 const Test = ({props}) => <span count={props.count}>Hello World</span>
356
357 mount(<Test count={2} />)
358 const root = el.firstElementChild
359 t.equal(root.getAttribute('count'), '2', 'rendered with props')
360
361 mount(<Test count={3} />)
362 t.equal(root.getAttribute('count'), '3', 'props updated')
363
364 teardown({renderer,el})
365 t.equal(el.innerHTML, '', 'the element is removed')
366 t.end()
367})
368
369test('simple components', t => {
370 const {el, renderer, mount, html} = setup(t.equal)
371 const Box = ({props}) => <div>{props.text}</div>
372
373 mount(<Box text="Hello World" />)
374 html('<div>Hello World</div>', 'function component rendered')
375 teardown({renderer, el})
376 t.end()
377})
378
379test('nested component lifecycle hooks fire in the correct order', t => {
380 const {el, renderer, mount} = setup(t.equal)
381 let log = []
382
383 const LifecycleLogger = {
384 render ({props, children}) {
385 log.push(props.name + ' render')
386 return <div>{children}</div>
387 },
388 onCreate ({props}) {
389 log.push(props.name + ' onCreate')
390 },
391 onRemove ({props}) {
392 log.push(props.name + ' onRemove')
393 },
394 shouldUpdate () {
395 return true
396 }
397 }
398
399 mount(
400 <Wrapper>
401 <LifecycleLogger name="GrandParent">
402 <LifecycleLogger name="Parent">
403 <LifecycleLogger name="Child" />
404 </LifecycleLogger>
405 </LifecycleLogger>
406 </Wrapper>
407 )
408
409 t.deepEqual(log, [
410 'GrandParent onCreate',
411 'GrandParent render',
412 'Parent onCreate',
413 'Parent render',
414 'Child onCreate',
415 'Child render'
416 ], 'initial render')
417 log = []
418
419 mount(
420 <Wrapper>
421 <LifecycleLogger name="GrandParent">
422 <LifecycleLogger name="Parent">
423 <LifecycleLogger name="Child" />
424 </LifecycleLogger>
425 </LifecycleLogger>
426 </Wrapper>
427 )
428
429 t.deepEqual(log, [
430 'GrandParent render',
431 'Parent render',
432 'Child render',
433 ], 'updated')
434 log = []
435
436 mount(<Wrapper></Wrapper>)
437
438 t.deepEqual(log, [
439 'GrandParent onRemove',
440 'Parent onRemove',
441 'Child onRemove'
442 ], 'unmounted')
443
444 mount(
445 <Wrapper>
446 <LifecycleLogger name="GrandParent">
447 <LifecycleLogger name="Parent">
448 <LifecycleLogger name="Child" />
449 </LifecycleLogger>
450 </LifecycleLogger>
451 </Wrapper>
452 )
453 log = []
454
455 teardown({renderer, el})
456
457 t.deepEqual(log, [
458 'GrandParent onRemove',
459 'Parent onRemove',
460 'Child onRemove'
461 ], 'unmounted')
462
463 t.end()
464})
465
466test('component lifecycle hook signatures', t => {
467 const {mount, renderer, el} = setup(t.equal)
468
469 const MyComponent = {
470 render ({props}) {
471 t.ok(props, 'render has props')
472 return <div id="foo" />
473 },
474 onCreate ({props}) {
475 t.ok(props, 'onCreate has props')
476 },
477 onRemove ({props}) {
478 t.ok(props, 'onRemove has props')
479 t.end()
480 }
481 }
482
483 mount(<MyComponent />)
484 teardown({renderer, el})
485})
486
487test('replace props instead of merging', t => {
488 const {mount, renderer, el} = setup(t.equal)
489 mount(<TwoWords one="Hello" two="World" />)
490 mount(<TwoWords two="Pluto" />)
491 t.equal(el.innerHTML, '<span> Pluto</span>')
492 teardown({renderer,el})
493 t.end()
494})
495
496test(`should update all children when a parent component changes`, t => {
497 const {mount, renderer, el} = setup(t.equal)
498 let parentCalls = 0
499 let childCalls = 0
500
501 const Child = {
502 render ({props}) {
503 childCalls++
504 return <span>{props.text}</span>
505 },
506 shouldUpdate () {
507 return true
508 }
509 }
510
511 const Parent = {
512 render ({props}) {
513 parentCalls++
514 return (
515 <div name={props.character}>
516 <Child text="foo" />
517 </div>
518 )
519 }
520 }
521
522 mount(<Parent character="Link" />)
523 mount(<Parent character="Zelda" />)
524 t.equal(childCalls, 2, 'child rendered twice')
525 t.equal(parentCalls, 2, 'parent rendered twice')
526 teardown({renderer, el})
527 t.end()
528})
529
530test.skip('batched rendering', t => {
531 let i = 0
532 const IncrementOnUpdate = {
533 render: function(){
534 return <div></div>
535 },
536 onUpdate: function(){
537 i++
538 }
539 }
540
541 const el = document.createElement('div')
542 const app = deku()
543 app.mount(<IncrementOnUpdate text="one" />)
544 const renderer = render(app, el)
545 app.mount(<IncrementOnUpdate text="two" />)
546 app.mount(<IncrementOnUpdate text="three" />)
547 raf(() => {
548 t.equal(i, 1, 'rendered *once* on the next frame')
549 renderer.remove()
550 t.end()
551 })
552})
553
554test('rendering nested components', t => {
555 const {mount, renderer, el, html} = setup(t.equal)
556
557 const ComponentA = ({children}) => <div name="ComponentA">{children}</div>
558 const ComponentB = ({children}) => <div name="ComponentB">{children}</div>
559
560 const ComponentC = ({props}) => {
561 return (
562 <div name="ComponentC">
563 <ComponentB>
564 <ComponentA>
565 <span>{props.text}</span>
566 </ComponentA>
567 </ComponentB>
568 </div>
569 )
570 }
571
572 mount(<ComponentC text='Hello World!' />)
573 html('<div name="ComponentC"><div name="ComponentB"><div name="ComponentA"><span>Hello World!</span></div></div></div>', 'element is rendered')
574 mount(<ComponentC text='Hello Pluto!' />)
575 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')
576 teardown({renderer, el})
577 html('', 'element is removed')
578 t.end()
579})
580
581test('skipping updates when the same virtual element is returned', t => {
582 const {mount, renderer, el} = setup(t.equal)
583 let i = 0
584 const vnode = <div onUpdate={el => i++} />
585
586 const Component = {
587 render (component) {
588 return vnode
589 }
590 }
591
592 mount(<Component />)
593 mount(<Component />)
594 t.equal(i, 1, 'component not updated')
595 teardown({renderer, el})
596 t.end()
597})
598
599test('firing mount events on sub-components created later', t => {
600 const {mount, renderer, el} = setup(t.equal)
601 const ComponentA = {
602 render: () => <div />,
603 onRemove: () => t.pass('onRemove called'),
604 onCreate: () => t.pass('onCreate called')
605 }
606
607 t.plan(2)
608 mount(<ComponentA />)
609 mount(<div />)
610 teardown({renderer, el})
611})
612
613test('should change root node and still update correctly', t => {
614 const {mount, html, renderer, el} = setup(t.equal)
615
616 const ComponentA = ({props}) => element(props.type, null, props.text)
617 const Test = ({props}) => <ComponentA type={props.type} text={props.text} />
618
619 mount(<Test type="span" text="test" />)
620 html('<span>test</span>')
621 mount(<Test type="div" text="test" />)
622 html('<div>test</div>')
623 mount(<Test type="div" text="foo" />)
624 html('<div>foo</div>')
625 teardown({renderer, el})
626 t.end()
627})
628
629test('replacing components with other components', t => {
630 const {mount, renderer, el, html} = setup(t.equal)
631 const ComponentA = () => <div>A</div>
632 const ComponentB = () => <div>B</div>
633
634 const ComponentC = ({props}) => {
635 if (props.type === 'A') {
636 return <ComponentA />
637 } else {
638 return <ComponentB />
639 }
640 }
641
642 mount(<ComponentC type="A" />)
643 html('<div>A</div>')
644 mount(<ComponentC type="B" />)
645 html('<div>B</div>')
646 teardown({renderer, el})
647 t.end()
648})
649
650test('adding, removing and updating events', t => {
651 const {mount, renderer, el, $} = setup(t.equal)
652 let count = 0
653 const onclicka = () => count += 1
654 const onclickb = () => count -= 1
655
656 mount(<Page clicker={onclicka} />)
657 trigger($('span'), 'click')
658 t.equal(count, 1, 'event added')
659 mount(<Page clicker={onclickb} />)
660 trigger($('span'), 'click')
661 t.equal(count, 0, 'event updated')
662 mount(<Page />)
663 trigger($('span'), 'click')
664 t.equal(count, 0, 'event removed')
665 teardown({renderer, el})
666 t.end()
667
668 function Page ({props}) {
669 return <span onClick={props.clicker} />
670 }
671})
672
673test('should bubble events', t => {
674 const {mount, renderer, el, $} = setup(t.equal)
675 const state = {}
676
677 const Test = {
678 render: function ({props}) {
679 const {state} = props
680
681 return (
682 <div onClick={onParentClick}>
683 <div class={state.active ? 'active' : ''} onClick={onClickTest}>
684 <a>link</a>
685 </div>
686 </div>
687 )
688 },
689 shouldUpdate () {
690 return true
691 }
692 }
693
694 mount(<Test state={state} />)
695 trigger($('a'), 'click')
696 t.equal(state.active, true, 'state was changed')
697 mount(<Test state={state} />)
698 t.ok($('.active'), 'event fired on parent element')
699 teardown({renderer, el})
700 t.end()
701
702 function onClickTest (event) {
703 state.active = true
704 t.equal(el.firstChild.firstChild.firstChild, event.target, 'event.target is set')
705 event.stopImmediatePropagation()
706 }
707
708 function onParentClick () {
709 t.fail('event bubbling was not stopped')
710 }
711})
712
713test('unmounting components when removing an element', t => {
714 const {mount, renderer, el} = setup(t.equal)
715
716 const Test = {
717 render: () => <div />,
718 onRemove: () => t.pass('component was unmounted')
719 }
720
721 t.plan(1)
722 mount(<div><div><Test /></div></div>)
723 mount(<div></div>)
724 teardown({renderer, el})
725 t.end()
726})
727
728test('update sub-components with the same element', t => {
729 const {mount, renderer, el} = setup(t.equal)
730
731 const Page1 = {
732 render ({props}) {
733 return (
734 <Wrapper>
735 <Wrapper>
736 <Wrapper>
737 {
738 props.show ?
739 <div>
740 <label/>
741 <input/>
742 </div>
743 :
744 <span>
745 Hello
746 </span>
747 }
748 </Wrapper>
749 </Wrapper>
750 </Wrapper>
751 )
752 }
753 }
754
755 const Page2 = ({props}) => {
756 return (
757 <div>
758 <span>{props.title}</span>
759 </div>
760 )
761 }
762
763 const App = ({props}) => props.page === 1 ? <Page1 show={props.show} /> : <Page2 title={props.title} />
764
765 mount(<App page={1} show={true} />)
766 mount(<App page={1} show={false} />)
767 mount(<App page={2} title="Hello World" />)
768 mount(<App page={2} title="foo" />)
769 t.equal(el.innerHTML, '<div><span>foo</span></div>')
770 teardown({renderer, el})
771 t.end()
772})
773
774test('replace elements with component nodes', t => {
775 const {mount, renderer, el} = setup(t.equal)
776
777 mount(<span/>)
778 t.equal(el.innerHTML, '<span></span>', 'rendered element')
779
780 mount(<Wrapper>component</Wrapper>)
781 t.equal(el.innerHTML, '<div>component</div>', 'replaced with component')
782
783 teardown({renderer, el})
784 t.end()
785})
786
787test('svg elements', t => {
788 const {mount, renderer, el} = setup(t.equal)
789
790 mount(
791 <svg width="92px" height="92px" viewBox="0 0 92 92">
792 <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
793 <circle id="circle" fill="#D8D8D8" cx="46" cy="46" r="46"></circle>
794 </g>
795 </svg>
796 )
797 t.equal(el.firstChild.tagName, 'svg', 'rendered svg element')
798
799 teardown({renderer, el})
800 t.end()
801})
802
803test('moving components with keys', t => {
804 const {mount, renderer, el} = setup(t.equal)
805 let one, two, three
806
807 t.plan(10)
808
809 mount(
810 <ul>
811 <ListItem key="foo">One</ListItem>
812 <ListItem key="bar">Two</ListItem>
813 </ul>
814 )
815 ;[one, two] = [].slice.call(el.querySelectorAll('li'))
816
817 // Moving
818 mount(
819 <ul>
820 <ListItem key="bar">Two</ListItem>
821 <ListItem key="foo">One</ListItem>
822 </ul>
823 )
824
825 let updated = el.querySelectorAll('li')
826 t.ok(updated[1] === one, 'foo moved down')
827 t.ok(updated[0] === two, 'bar moved up')
828
829 // Removing
830 mount(
831 <ul>
832 <ListItem key="bar">Two</ListItem>
833 </ul>
834 )
835 updated = el.querySelectorAll('li')
836 t.ok(updated[0] === two && updated.length === 1, 'foo was removed')
837
838 // Updating
839 mount(
840 <ul>
841 <ListItem key="foo">One</ListItem>
842 <ListItem key="bar">Two</ListItem>
843 <ListItem key="baz">Three</ListItem>
844 </ul>
845 )
846
847 ;[one,two,three] = [].slice.call(el.querySelectorAll('li'))
848 mount(
849 <ul>
850 <ListItem key="foo">One</ListItem>
851 <ListItem key="baz">Four</ListItem>
852 </ul>
853 )
854 updated = el.querySelectorAll('li')
855 t.ok(updated[0] === one, 'foo is the same')
856 t.ok(updated[1] === three, 'baz is the same')
857 t.ok(updated[1].innerHTML === 'Four', 'baz was updated')
858 let [foo, baz] = [].slice.call(updated)
859
860 // Adding
861 mount(
862 <ul>
863 <ListItem key="foo">One</ListItem>
864 <ListItem key="bar">Five</ListItem>
865 <ListItem key="baz">Four</ListItem>
866 </ul>
867 )
868 updated = el.querySelectorAll('li')
869 t.ok(updated[0] === foo, 'foo is the same')
870 t.ok(updated[2] === baz, 'baz is the same')
871 t.ok(updated[1].innerHTML === 'Five', 'bar was added')
872
873 // Moving event handlers
874 const clicked = () => t.pass('event handler moved')
875 mount(
876 <ul>
877 <ListItem key="foo">One</ListItem>
878 <ListItem key="bar">
879 <span onClick={clicked}>Click Me!</span>
880 </ListItem>
881 </ul>
882 )
883 mount(
884 <ul>
885 <ListItem key="bar">
886 <span onClick={clicked}>Click Me!</span>
887 </ListItem>
888 <ListItem key="foo">One</ListItem>
889 </ul>
890 )
891 trigger(el.querySelector('span'), 'click')
892
893 // Removing handlers. If the handler isn't removed from
894 // the path correctly, it will still fire the handler from
895 // the previous assertion.
896 mount(
897 <ul>
898 <ListItem key="foo">
899 <span>One</span>
900 </ListItem>
901 </ul>
902 )
903 trigger(el.querySelector('span'), 'click')
904
905 teardown({renderer, el})
906 t.end()
907})
908
909test('updating event handlers when children are removed', t => {
910 const {mount, renderer, el} = setup(t.equal)
911 const items = ['foo','bar','baz']
912
913 const ListItem = {
914 shouldUpdate () { return true },
915 render ({props}) {
916 return (
917 <li>
918 <a onClick={e => { items.splice(props.index, 1); console.log('remove') }} />
919 </li>
920 )
921 }
922 }
923
924 const List = {
925 shouldUpdate () { return true },
926 render ({props}) {
927 return (
928 <ul>
929 {props.items.map((_,i) => <ListItem index={i} />)}
930 </ul>
931 )
932 }
933 }
934
935 mount(<List items={items} />)
936 trigger(el.querySelector('a'), 'click')
937 mount(<List items={items} />)
938 trigger(el.querySelector('a'), 'click')
939 mount(<List items={items} />)
940 trigger(el.querySelector('a'), 'click')
941 mount(<List items={items} />)
942 t.equal(el.innerHTML, '<ul></ul>', 'all items were removed')
943
944 teardown({renderer, el})
945 t.end()
946})
947
948test('components should receive path based keys if they are not specified', t => {
949 t.end()
950})
951
952test('array jsx', t => {
953 const {mount, renderer, el} = setup(t.equal)
954 const arr = [1, 2]
955
956 mount(
957 <div>
958 Hello World
959 {arr.map(i => <span>{i}</span>)}
960 <hr/>
961 </div>
962 )
963
964 const n = el.firstChild
965 t.equal(n.childNodes[0].nodeName, '#text')
966 t.equal(n.childNodes[1].nodeName, 'SPAN')
967 t.equal(n.childNodes[2].nodeName, 'SPAN')
968 t.equal(n.childNodes[3].nodeName, 'HR')
969
970 teardown({renderer, el})
971 t.end()
972})
973
974test('getProps', t => {
975 const {mount, renderer, el} = setup(t.equal)
976 const vals = {}
977 function save (name) { return ({props}) => { vals[name] = props.value }}
978 const Test = {
979 getProps () {
980 return {
981 value: 1
982 }
983 },
984 onCreate: save('create'),
985 render ({props}) {
986 vals.render = props.value
987 return <span></span>
988 },
989 onUpdate: save('update'),
990 onRemove: save('remove'),
991 shouldUpdate () { return true }
992 }
993
994 mount(<Test value={3} />)
995 mount(<Test value={4} />)
996 mount(<span></span>)
997
998 t.equal(vals.create, 1, 'onCreate hook')
999 t.equal(vals.render, 1, 'render')
1000 t.equal(vals.update, 1, 'onUpdate hook')
1001 t.equal(vals.remove, 1, 'onRemove hook')
1002
1003 teardown({renderer, el})
1004 t.end()
1005})
1006
1007test('should have context', t => {
1008 const {mount, renderer, el} = setup(t.equal)
1009 let a, b, c
1010
1011 const CtxTest = {
1012 getProps (props, context) {
1013 return {
1014 ...props,
1015 ...context
1016 }
1017 },
1018
1019 render ({props}) {
1020 a = props.a
1021 b = props.b
1022 c = props.c
1023 return <span/>
1024 }
1025 }
1026
1027 context = {b: 2}
1028 mount(<CtxTest a={1} />)
1029
1030 t.equal(a, 1)
1031 t.equal(b, 2)
1032 t.equal(c, undefined)
1033
1034 context = {b: 2, c: 3}
1035 forceUpdate = true
1036 mount(<CtxTest a={1} />)
1037 forceUpdate = false
1038
1039 t.equal(a, 1)
1040 t.equal(b, 2)
1041 t.equal(c, 3)
1042
1043 teardown({renderer, el})
1044 t.end()
1045})
1046
1047test('diff', t => {
1048 t.test('reverse', diffXf(r => r.reverse()))
1049 t.test('prepend (1)', diffXf(r => [11].concat(r)))
1050 t.test('remove (1)', diffXf(r => r.slice(1)))
1051 t.test('reverse, remove(1)', diffXf(r => r.reverse().slice(1)))
1052 t.test('remove (1), reverse', diffXf(r => r.slice(1).reverse()))
1053 t.test('reverse, append (1)', diffXf(r => r.reverse().concat(11)))
1054 t.test('reverse, prepend (1)', diffXf(r => [11].concat(r.reverse())))
1055 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))))
1056 t.test('replace all', diffXf(r => range(11, 25)))
1057 t.test('insert (3), randomize', diffXf(r => randomize(r.concat(range(13, 17)))))
1058 t.test('moveFromStartToEnd (1)', diffXf(r => r.slice(1).concat(r[0])))
1059})
1060
1061function randomize (r) {
1062 return r.reduce(acc => {
1063 const i = Math.floor(Math.random() * 100000) % r.length
1064 acc.push(r[i])
1065 r.splice(i, 1)
1066 return acc
1067 }, [])
1068}
1069
1070function range (begin, end) {
1071 const r = []
1072
1073 for (let i = begin; i < end; i++) {
1074 r.push(i)
1075 }
1076
1077 return r
1078}
1079
1080function diffXf (xf) {
1081 return t => {
1082 const r = range(0, 10)
1083 diffTest(t, r, xf(r.slice()))
1084 t.end()
1085 }
1086}
1087
1088function diffTest (t, a, b) {
1089 const {mount, renderer, el} = setup(t.equal)
1090
1091 mount(<div>{a.map(i => <span key={i}>{i}</span>)}</div>)
1092 mount(<div>{b.map(i => <span key={i}>{i}</span>)}</div>)
1093
1094 const node = el.firstChild
1095 for (let i = 0; i < node.childNodes.length; i++) {
1096 t.equal(node.childNodes[i].textContent, b[i].toString())
1097 }
1098
1099 t.equal(node.childNodes.length, b.length)
1100}