UNPKG

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