diff --git a/test/browser/components.js b/test/browser/components.js index 5026e19104..0fe3fcd838 100644 --- a/test/browser/components.js +++ b/test/browser/components.js @@ -151,12 +151,6 @@ describe('Components', () => { expect(scratch.innerHTML).to.equal(''); }); - // Test for #651 - it('should set enumerable boolean attribute', () => { - render(, scratch); - expect(scratch.firstChild.spellcheck).to.equal(false); - }); - // Test for Issue #73 it('should remove orphaned elements replaced by Components', () => { class Comp extends Component { diff --git a/test/browser/render.js b/test/browser/render.js index 38d643877b..b012861e0e 100644 --- a/test/browser/render.js +++ b/test/browser/render.js @@ -1,6 +1,6 @@ /* global DISABLE_FLAKEY */ -import { h, render, Component } from '../../src/preact'; +import { h, render, Component, rerender } from '../../src/preact'; /** @jsx h */ function getAttributes(node) { @@ -37,7 +37,7 @@ describe('render()', () => { scratch = null; }); - it('should render a empty text node', () => { + it('should render a empty text node given null', () => { render(null, scratch); let c = scratch.childNodes; expect(c).to.have.length(1); @@ -45,6 +45,14 @@ describe('render()', () => { expect(c[0].nodeName).to.equal('#text'); }); + it('should render an empty text node given an empty string', () => { + render('', scratch); + let c = scratch.childNodes; + expect(c).to.have.length(1); + expect(c[0].data).to.equal(''); + expect(c[0].nodeName).to.equal('#text'); + }); + it('should create empty nodes (<* />)', () => { render(
, scratch); expect(scratch.childNodes).to.have.length(1); @@ -56,13 +64,39 @@ describe('render()', () => { expect(scratch.childNodes).to.have.length(1); expect(scratch.childNodes[0].nodeName).to.equal('SPAN'); - scratch.innerHTML = ''; + }); + it('should support custom tag names', () => { render(, scratch); + expect(scratch.childNodes).to.have.length(1); + expect(scratch.firstChild).to.have.property('nodeName', 'FOO'); + + scratch.innerHTML = ''; + render(, scratch); + expect(scratch.childNodes).to.have.length(1); + expect(scratch.firstChild).to.have.property('nodeName', 'X-BAR'); + }); + + it('should append new elements when called without a merge argument', () => { + render(
, scratch); + expect(scratch.childNodes).to.have.length(1); + expect(scratch.firstChild).to.have.property('nodeName', 'DIV'); + + render(, scratch); expect(scratch.childNodes).to.have.length(2); - expect(scratch.childNodes[0]).to.have.property('nodeName', 'FOO'); - expect(scratch.childNodes[1]).to.have.property('nodeName', 'X-BAR'); + expect(scratch.childNodes[0]).to.have.property('nodeName', 'DIV'); + expect(scratch.childNodes[1]).to.have.property('nodeName', 'SPAN'); + }); + + it('should merge new elements when called with a merge argument', () => { + let root = render(
, scratch); + expect(scratch.childNodes).to.have.length(1); + expect(scratch.firstChild).to.have.property('nodeName', 'DIV'); + + render(, scratch, root); + expect(scratch.childNodes).to.have.length(1); + expect(scratch.firstChild).to.have.property('nodeName', 'SPAN'); }); it('should nest empty nodes', () => { @@ -148,8 +182,9 @@ describe('render()', () => { anan: 'NaN' }); - scratch.innerHTML = ''; + }); + it('should not render falsy attributes on initial render', () => { render((
), scratch); @@ -200,6 +235,12 @@ describe('render()', () => { expect(scratch).to.have.property('innerHTML', '
', 'for undefined'); }); + // Test for #651 + it('should set enumerable boolean attribute', () => { + render(, scratch); + expect(scratch.firstChild.spellcheck).to.equal(false); + }); + it('should apply string attributes', () => { render(
, scratch); @@ -243,38 +284,87 @@ describe('render()', () => { expect(scratch.childNodes[0]).to.have.property('className', 'bar'); }); - it('should apply style as String', () => { - render(
, scratch); - expect(scratch.childNodes[0].style.cssText) - .that.matches(/top\s*:\s*5px\s*/) - .and.matches(/position\s*:\s*relative\s*/); - }); + describe('style attribute', () => { + it('should apply style as String', () => { + render(
, scratch); + expect(scratch.childNodes[0].style.cssText) + .that.matches(/top\s*:\s*5px\s*/) + .and.matches(/position\s*:\s*relative\s*/); + }); - it('should only register on* functions as handlers', () => { - let click = () => {}, - onclick = () => {}; + it('should properly switch from string styles to object styles and back', () => { + let root = render(( +
test
+ ), scratch); - let proto = document.createElement('div').constructor.prototype; + expect(root.style.cssText).to.equal('display: inline;'); - sinon.spy(proto, 'addEventListener'); + root = render(( +
+ ), scratch, root); - render(
, scratch); + expect(root.style.cssText).to.equal('color: red;'); - expect(scratch.childNodes[0].attributes.length).to.equal(0); + root = render(( +
+ ), scratch, root); - expect(proto.addEventListener).to.have.been.calledOnce - .and.to.have.been.calledWithExactly('click', sinon.match.func, false); + expect(root.style.cssText).to.equal('color: blue;'); - proto.addEventListener.restore(); - }); + root = render(( +
+ ), scratch, root); + + expect(root.style.cssText).to.equal('color: yellow;'); + + root = render(( +
+ ), scratch, root); + + expect(root.style.cssText).to.equal('display: block;'); + }); - it('should add and remove event handlers', () => { - let click = sinon.spy(), - mousedown = sinon.spy(); + it('should serialize style objects', () => { + let root = render(( +
+ test +
+ ), scratch); - let proto = document.createElement('div').constructor.prototype; - sinon.spy(proto, 'addEventListener'); - sinon.spy(proto, 'removeEventListener'); + let { style } = scratch.childNodes[0]; + expect(style).to.have.property('color').that.equals('rgb(255, 255, 255)'); + expect(style).to.have.property('background').that.contains('rgb(255, 100, 0)'); + expect(style).to.have.property('backgroundPosition').that.equals('10px 10px'); + expect(style).to.have.property('backgroundSize', 'cover'); + expect(style).to.have.property('padding', '5px'); + expect(style).to.have.property('top', '100px'); + expect(style).to.have.property('left', '100%'); + + root = render(( +
test
+ ), scratch, root); + + expect(root.style.cssText).to.equal('color: rgb(0, 255, 255);'); + + root = render(( +
test
+ ), scratch, root); + + expect(root.style.cssText).to.equal('background-color: rgb(0, 255, 255);'); + }); + }); + + describe('event handling', () => { + let proto; function fireEvent(on, type) { let e = document.createEvent('Event'); @@ -282,170 +372,191 @@ describe('render()', () => { on.dispatchEvent(e); } - render(
click(1) } onMouseDown={ mousedown } />, scratch); + beforeEach(() => { + proto = document.createElement('div').constructor.prototype; - expect(proto.addEventListener).to.have.been.calledTwice - .and.to.have.been.calledWith('click') - .and.calledWith('mousedown'); + sinon.spy(proto, 'addEventListener'); + sinon.spy(proto, 'removeEventListener'); + }); - fireEvent(scratch.childNodes[0], 'click'); - expect(click).to.have.been.calledOnce - .and.calledWith(1); + afterEach(() => { + proto.addEventListener.restore(); + proto.removeEventListener.restore(); + }); - proto.addEventListener.resetHistory(); - click.resetHistory(); + it('should only register on* functions as handlers', () => { + let click = () => {}, + onclick = () => {}; - render(
click(2) } />, scratch, scratch.firstChild); + render(
, scratch); - expect(proto.addEventListener).not.to.have.been.called; + expect(scratch.childNodes[0].attributes.length).to.equal(0); - expect(proto.removeEventListener) - .to.have.been.calledOnce - .and.calledWith('mousedown'); + expect(proto.addEventListener).to.have.been.calledOnce + .and.to.have.been.calledWithExactly('click', sinon.match.func, false); + }); - fireEvent(scratch.childNodes[0], 'click'); - expect(click).to.have.been.calledOnce - .and.to.have.been.calledWith(2); + it('should support native event names', () => { + let click = sinon.spy(), + mousedown = sinon.spy(); - fireEvent(scratch.childNodes[0], 'mousedown'); - expect(mousedown).not.to.have.been.called; + render(
click(1)} onmousedown={mousedown} />, scratch); - proto.removeEventListener.resetHistory(); - click.resetHistory(); - mousedown.resetHistory(); + expect(proto.addEventListener).to.have.been.calledTwice + .and.to.have.been.calledWith('click') + .and.calledWith('mousedown'); - render(
, scratch, scratch.firstChild); + fireEvent(scratch.childNodes[0], 'click'); + expect(click).to.have.been.calledOnce + .and.calledWith(1); + }); - expect(proto.removeEventListener) - .to.have.been.calledOnce - .and.calledWith('click'); + it('should support camel-case event names', () => { + let click = sinon.spy(), + mousedown = sinon.spy(); - fireEvent(scratch.childNodes[0], 'click'); - expect(click).not.to.have.been.called; + render(
click(1)} onMouseDown={mousedown} />, scratch); - proto.addEventListener.restore(); - proto.removeEventListener.restore(); - }); + expect(proto.addEventListener).to.have.been.calledTwice + .and.to.have.been.calledWith('click') + .and.calledWith('mousedown'); - it('should use capturing for event props ending with *Capture', () => { - let click = sinon.spy(), - focus = sinon.spy(); + fireEvent(scratch.childNodes[0], 'click'); + expect(click).to.have.been.calledOnce + .and.calledWith(1); + }); - let root = render(( -
-
- ), scratch); + it('should update event handlers', () => { + let click1 = sinon.spy(); + let click2 = sinon.spy(); - root.firstElementChild.click(); - root.firstElementChild.focus(); + render(
, scratch); - expect(click, 'click').to.have.been.calledOnce; + fireEvent(scratch.childNodes[0], 'click'); + expect(click1).to.have.been.calledOnce; + expect(click2).to.not.have.been.called; - if (DISABLE_FLAKEY!==true) { - // Focus delegation requires a 50b hack I'm not sure we want to incur - expect(focus, 'focus').to.have.been.calledOnce; + click1.resetHistory(); + click2.resetHistory(); - // IE doesn't set it - expect(click).to.have.been.calledWithMatch({ eventPhase: 0 }); // capturing - expect(focus).to.have.been.calledWithMatch({ eventPhase: 0 }); // capturing - } - }); + render(
, scratch, scratch.firstChild); - it('should serialize style objects', () => { - let root = render(( -
- test -
- ), scratch); + fireEvent(scratch.childNodes[0], 'click'); + expect(click1).to.not.have.been.called; + expect(click2).to.have.been.called; + }); - let { style } = scratch.childNodes[0]; - expect(style).to.have.property('color').that.equals('rgb(255, 255, 255)'); - expect(style).to.have.property('background').that.contains('rgb(255, 100, 0)'); - expect(style).to.have.property('backgroundPosition').that.equals('10px 10px'); - expect(style).to.have.property('backgroundSize', 'cover'); - expect(style).to.have.property('padding', '5px'); - expect(style).to.have.property('top', '100px'); - expect(style).to.have.property('left', '100%'); + it('should remove event handlers', () => { + let click = sinon.spy(), + mousedown = sinon.spy(); - root = render(( -
test
- ), scratch, root); + render(
click(1)} onMouseDown={mousedown} />, scratch); + render(
click(2)} />, scratch, scratch.firstChild); - expect(root.style.cssText).to.equal('color: rgb(0, 255, 255);'); + expect(proto.removeEventListener) + .to.have.been.calledOnce + .and.calledWith('mousedown'); - root = render(( -
test
- ), scratch, root); + fireEvent(scratch.childNodes[0], 'mousedown'); + expect(mousedown).not.to.have.been.called; - expect(root.style.cssText).to.equal('display: inline;'); + proto.removeEventListener.resetHistory(); + click.resetHistory(); + mousedown.resetHistory(); - root = render(( -
test
- ), scratch, root); + render(
, scratch, scratch.firstChild); - expect(root.style.cssText).to.equal('background-color: rgb(0, 255, 255);'); - }); + expect(proto.removeEventListener) + .to.have.been.calledOnce + .and.calledWith('click'); + + fireEvent(scratch.childNodes[0], 'click'); + expect(click).not.to.have.been.called; + }); - it('should support dangerouslySetInnerHTML', () => { - let html = 'foo & bar'; - let root = render(
, scratch); + it('should use capturing for event props ending with *Capture', () => { + let click = sinon.spy(), + focus = sinon.spy(); - expect(scratch.firstChild, 'set').to.have.property('innerHTML', html); - expect(scratch.innerHTML).to.equal('
'+html+'
'); + let root = render(( +
+
+ ), scratch); - root = render(
ab
, scratch, root); + root.firstElementChild.click(); + root.firstElementChild.focus(); - expect(scratch, 'unset').to.have.property('innerHTML', `
ab
`); + expect(click, 'click').to.have.been.calledOnce; - render(
, scratch, root); + if (DISABLE_FLAKEY!==true) { + // Focus delegation requires a 50b hack I'm not sure we want to incur + expect(focus, 'focus').to.have.been.calledOnce; - expect(scratch.innerHTML, 're-set').to.equal('
'+html+'
'); + // IE doesn't set it + expect(click).to.have.been.calledWithMatch({ eventPhase: 0 }); // capturing + expect(focus).to.have.been.calledWithMatch({ eventPhase: 0 }); // capturing + } + }); }); - it('should apply proper mutation for VNodes with dangerouslySetInnerHTML attr', () => { - class Thing extends Component { - constructor(props, context) { - super(props, context); - this.state.html = this.props.html; - } - render(props, { html }) { - return html ?
:
; + describe('dangerouslySetInnerHTML', () => { + it('should support dangerouslySetInnerHTML', () => { + let html = 'foo & bar'; + // eslint-disable-next-line react/no-danger + let root = render(
, scratch); + + expect(scratch.firstChild, 'set').to.have.property('innerHTML', html); + expect(scratch.innerHTML).to.equal('
'+html+'
'); + + root = render(
ab
, scratch, root); + + expect(scratch, 'unset').to.have.property('innerHTML', `
ab
`); + + // eslint-disable-next-line react/no-danger + render(
, scratch, root); + + expect(scratch.innerHTML, 're-set').to.equal('
'+html+'
'); + }); + + it('should apply proper mutation for VNodes with dangerouslySetInnerHTML attr', () => { + class Thing extends Component { + constructor(props, context) { + super(props, context); + this.state.html = this.props.html; + } + render(props, { html }) { + // eslint-disable-next-line react/no-danger + return html ?
:
; + } } - } - let thing; + let thing; - render( thing=c } html="test" />, scratch); + render( thing=c} html="test" />, scratch); - expect(scratch.innerHTML).to.equal('
test
'); + expect(scratch.innerHTML).to.equal('
test
'); - thing.setState({ html: false }); - thing.forceUpdate(); + thing.setState({ html: false }); + thing.forceUpdate(); - expect(scratch.innerHTML).to.equal('
'); + expect(scratch.innerHTML).to.equal('
'); - thing.setState({ html: 'test' }); - thing.forceUpdate(); + thing.setState({ html: 'test' }); + thing.forceUpdate(); - expect(scratch.innerHTML).to.equal('
test
'); - }); + expect(scratch.innerHTML).to.equal('
test
'); + }); - it('should hydrate with dangerouslySetInnerHTML', () => { - let html = 'foo & bar'; - scratch.innerHTML = `
${html}
`; - render(
, scratch, scratch.lastChild); + it('should hydrate with dangerouslySetInnerHTML', () => { + let html = 'foo & bar'; + scratch.innerHTML = `
${html}
`; + // eslint-disable-next-line react/no-danger + render(
, scratch, scratch.lastChild); - expect(scratch.firstChild).to.have.property('innerHTML', html); - expect(scratch.innerHTML).to.equal(`
${html}
`); + expect(scratch.firstChild).to.have.property('innerHTML', html); + expect(scratch.innerHTML).to.equal(`
${html}
`); + }); }); it('should reconcile mutated DOM attributes', () => { @@ -509,7 +620,7 @@ describe('render()', () => { }; const DOMElement = html`
`; - const preactElement =
; + const preactElement =
; render(preactElement, scratch, DOMElement); expect(scratch).to.have.property('innerHTML', '
'); @@ -529,7 +640,7 @@ describe('render()', () => { } let comp; - let root = render( comp = c } />, scratch, root); + let root = render( comp = c} />, scratch, root); let c = document.createElement('c'); c.textContent = 'baz'; @@ -554,18 +665,18 @@ describe('render()', () => { // Re-rendering from the root is non-destructive if the root was a previous render: comp.alt = false; - root = render( comp = c } />, scratch, root); + root = render( comp = c} />, scratch, root); expect(scratch.firstChild.children, 'root re-render').to.have.length(4); expect(scratch.innerHTML, 'root re-render').to.equal(`
foobarbazbat
`); comp.alt = true; - root = render( comp = c } />, scratch, root); + root = render( comp = c} />, scratch, root); expect(scratch.firstChild.children, 'root re-render 2').to.have.length(4); expect(scratch.innerHTML, 'root re-render 2').to.equal(`
altfoobazbat
`); - root = render(
comp = c } />
, scratch, root); + root = render(
comp = c} />
, scratch, root); expect(scratch.firstChild.children, 'root re-render changed').to.have.length(3); expect(scratch.innerHTML, 'root re-render changed').to.equal(`
foobar
bazbat
`); @@ -588,44 +699,69 @@ describe('render()', () => { expect(sortAttributes(html)).to.equal(sortAttributes('')); }); - it('should not execute append operation when child is at last', (done) => { + it('should not execute append operation when child is at last', () => { + // See developit/preact#717 for discussion about the issue this addresses + + let todoText = 'new todo that I should complete'; let input; + let setText; + let addTodo; + + const ENTER = 13; + class TodoList extends Component { constructor(props) { super(props); this.state = { todos: [], text: '' }; - this.setText = this.setText.bind(this); - this.addTodo = this.addTodo.bind(this); + setText = this.setText = this.setText.bind(this); + addTodo = this.addTodo = this.addTodo.bind(this); } setText(e) { this.setState({ text: e.target.value }); } - addTodo() { - let { todos, text } = this.state; - todos = todos.concat({ text }); - this.setState({ todos, text: '' }); + addTodo(e) { + if (e.keyCode === ENTER) { + let { todos, text } = this.state; + todos = todos.concat({ text }); + this.setState({ todos, text: '' }); + } } render() { - const {todos, text} = this.state; + const { todos, text } = this.state; return ( -
- { todos.map( todo => (
{todo.text}
)) } +
+ { todos.map( todo => ([ + {todo.text}, + [ Delete ], +
+ ])) } input = i} />
); } } - const root = render(, scratch); + + render(, scratch); + + // Simulate user typing input.focus(); - input.value = 1; - root._component.setText({ + input.value = todoText; + setText({ target: input }); - root._component.addTodo(); + + // Simulate user pressing enter + addTodo({ + keyCode: ENTER + }); + + // Before Preact rerenders, focus should be on the input + expect(document.activeElement).to.equal(input); + + rerender(); + + // After Preact rerenders, focus should remain on the input expect(document.activeElement).to.equal(input); - setTimeout(() =>{ - expect(/1/.test(scratch.innerHTML)).to.equal(true); - done(); - }, 10); + expect(scratch.innerHTML).to.contain(`${todoText}`); }); });