1 |
|
2 |
|
3 |
|
4 |
|
5 | import test from 'tape'
|
6 | import virtex from '../src'
|
7 | import dom from 'virtex-dom'
|
8 | import raf from 'component-raf'
|
9 | import delegant from 'delegant'
|
10 | import falsy from 'redux-falsy'
|
11 | import trigger from 'trigger-event'
|
12 | import element from 'virtex-element'
|
13 | import component from 'virtex-component'
|
14 | import {createStore, applyMiddleware} from 'redux'
|
15 |
|
16 |
|
17 |
|
18 |
|
19 |
|
20 | let context = {}
|
21 | let forceUpdate = false
|
22 | const store = applyMiddleware(falsy, dom, component({
|
23 | getContext: () => context,
|
24 | ignoreShouldUpdate: () => forceUpdate
|
25 | }))(createStore)(() => {}, {})
|
26 |
|
27 |
|
28 |
|
29 |
|
30 |
|
31 | const {create, update} = virtex(store.dispatch)
|
32 |
|
33 |
|
34 |
|
35 | const RenderChildren = ({children}) => children[0]
|
36 | const ListItem = ({children}) => <li>{children}</li>
|
37 | const Wrapper = ({children}) => <div>{children}</div>
|
38 | const TwoWords = ({props}) => <span>{props.one} {props.two}</span>
|
39 |
|
40 |
|
41 |
|
42 | function div () {
|
43 | const el = document.createElement('div')
|
44 | document.body.appendChild(el)
|
45 | return el
|
46 | }
|
47 |
|
48 | function 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 |
|
76 | tree = null
|
77 | }
|
78 | },
|
79 | el,
|
80 | $: el.querySelector.bind(el),
|
81 | html: createAssertHTML(el, equal)
|
82 | }
|
83 | }
|
84 |
|
85 | function teardown ({renderer, el}) {
|
86 | renderer.remove()
|
87 | if (el.parentNode) el.parentNode.removeChild(el)
|
88 | }
|
89 |
|
90 | function 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 |
|
100 |
|
101 |
|
102 |
|
103 |
|
104 |
|
105 |
|
106 | test('rendering DOM', t => {
|
107 | const {renderer, el, mount, unmount, html} = setup(t.equal)
|
108 | let rootEl
|
109 |
|
110 |
|
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 |
|
226 | test('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 |
|
240 | test('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 for some reason
|
248 | // html('<div></div>', 'innerHTML is removed')
|
249 | teardown({renderer, el})
|
250 | t.end()
|
251 | })
|
252 |
|
253 | test('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 |
|
322 | test('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 |
|
354 | test('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 |
|
370 | test('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 |
|
380 | test('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 |
|
467 | test('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 |
|
488 | test('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 |
|
497 | test(`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 |
|
531 | test.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 |
|
555 | test('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 |
|
582 | test('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 |
|
600 | test('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 |
|
614 | test('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 |
|
630 | test('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 |
|
651 | test('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 |
|
677 | test('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 |
|
720 | test('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 |
|
735 | test('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 |
|
781 | test('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 |
|
794 | test('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 |
|
810 | test('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 |
|
918 | test('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 |
|
963 | test('components should receive path based keys if they are not specified', t => {
|
964 | t.end()
|
965 | })
|
966 |
|
967 | test('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 |
|
989 | test('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 |
|
1025 | test('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 |
|
1042 | test('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 |
|
1056 | function 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 |
|
1065 | function 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 |
|
1075 | function diffXf (xf) {
|
1076 | return t => {
|
1077 | const r = range(0, 10)
|
1078 | diffTest(t, r, xf(r.slice()))
|
1079 | t.end()
|
1080 | }
|
1081 | }
|
1082 |
|
1083 | function 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 | }
|