/* global DISABLE_FLAKEY */ import { h, render } from '../../src/preact'; /** @jsx h */ function getAttributes(node) { let attrs = {}; for (let i=node.attributes.length; i--; ) { attrs[node.attributes[i].name] = node.attributes[i].value; } return attrs; } // hacky normalization of attribute order across browsers. function sortAttributes(html) { return html.replace(/<([a-z0-9-]+)((?:\s[a-z0-9:_.-]+=".*?")+)((?:\s*\/)?>)/gi, (s, pre, attrs, after) => { let list = attrs.match(/\s[a-z0-9:_.-]+=".*?"/gi).sort( (a, b) => a>b ? 1 : -1 ); if (~after.indexOf('/')) after = '>'; return '<' + pre + list.join('') + after; }); } describe('render()', () => { let scratch; before( () => { scratch = document.createElement('div'); (document.body || document.documentElement).appendChild(scratch); }); beforeEach( () => { scratch.innerHTML = ''; }); after( () => { scratch.parentNode.removeChild(scratch); scratch = null; }); it('should create empty nodes (<* />)', () => { render(
, scratch); expect(scratch.childNodes) .to.have.length(1) .and.to.have.deep.property('0.nodeName', 'DIV'); scratch.innerHTML = ''; render(, scratch); expect(scratch.childNodes) .to.have.length(1) .and.to.have.deep.property('0.nodeName', 'SPAN'); scratch.innerHTML = ''; render(, scratch); 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'); }); it('should nest empty nodes', () => { render((
), scratch); expect(scratch.childNodes) .to.have.length(1) .and.to.have.deep.property('0.nodeName', 'DIV'); let c = scratch.childNodes[0].childNodes; expect(c).to.have.length(3); expect(c).to.have.deep.property('0.nodeName', 'SPAN'); expect(c).to.have.deep.property('1.nodeName', 'FOO'); expect(c).to.have.deep.property('2.nodeName', 'X-BAR'); }); it('should not render falsey values', () => { render((
{null},{undefined},{false},{0},{NaN}
), scratch); expect(scratch.firstChild).to.have.property('innerHTML', ',,,0,NaN'); }); it('should clear falsey attributes', () => { let root = render((
), scratch); root = render((
), scratch, root); expect(getAttributes(scratch.firstChild), 'from previous truthy values').to.eql({ a0: '0', anan: 'NaN' }); scratch.innerHTML = ''; root = render((
), scratch); expect(getAttributes(scratch.firstChild), 'initial render').to.eql({ a0: '0', anan: 'NaN' }); }); it('should clear falsey input values', () => { let root = render((
), scratch); expect(root.children[0]).to.have.property('value', '0'); expect(root.children[1]).to.have.property('value', 'false'); expect(root.children[2]).to.have.property('value', ''); expect(root.children[3]).to.have.property('value', ''); }); it('should clear falsey DOM properties', () => { let root; function test(val) { root = render((
), scratch, root); } test('2'); test(false); expect(scratch).to.have.property('innerHTML', '
', 'for false'); test('3'); test(null); expect(scratch).to.have.property('innerHTML', '
', 'for null'); test('4'); test(undefined); expect(scratch).to.have.property('innerHTML', '
', 'for undefined'); }); it('should apply string attributes', () => { render(
, scratch); let div = scratch.childNodes[0]; expect(div).to.have.deep.property('attributes.length', 2); expect(div).to.have.deep.property('attributes[0].name', 'foo'); expect(div).to.have.deep.property('attributes[0].value', 'bar'); expect(div).to.have.deep.property('attributes[1].name', 'data-foo'); expect(div).to.have.deep.property('attributes[1].value', 'databar'); }); it('should apply class as String', () => { render(
, scratch); expect(scratch.childNodes[0]).to.have.property('className', 'foo'); }); it('should alias className to class', () => { render(
, scratch); expect(scratch.childNodes[0]).to.have.property('className', 'bar'); }); it('should apply style as String', () => { render(
, scratch); expect(scratch.childNodes[0]).to.have.deep.property('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 = () => {}; let proto = document.createElement('div').constructor.prototype; sinon.spy(proto, 'addEventListener'); render(
, scratch); expect(scratch.childNodes[0]).to.have.deep.property('attributes.length', 0); expect(proto.addEventListener).to.have.been.calledOnce .and.to.have.been.calledWithExactly('click', sinon.match.func, false); proto.addEventListener.restore(); }); it('should add and remove event handlers', () => { let click = sinon.spy(), mousedown = sinon.spy(); let proto = document.createElement('div').constructor.prototype; sinon.spy(proto, 'addEventListener'); sinon.spy(proto, 'removeEventListener'); function fireEvent(on, type) { let e = document.createEvent('Event'); e.initEvent(type, true, true); on.dispatchEvent(e); } render(
click(1) } onMouseDown={ mousedown } />, scratch); expect(proto.addEventListener).to.have.been.calledTwice .and.to.have.been.calledWith('click') .and.calledWith('mousedown'); fireEvent(scratch.childNodes[0], 'click'); expect(click).to.have.been.calledOnce .and.calledWith(1); proto.addEventListener.reset(); click.reset(); render(
click(2) } />, scratch, scratch.firstChild); expect(proto.addEventListener).not.to.have.been.called; expect(proto.removeEventListener) .to.have.been.calledOnce .and.calledWith('mousedown'); fireEvent(scratch.childNodes[0], 'click'); expect(click).to.have.been.calledOnce .and.to.have.been.calledWith(2); fireEvent(scratch.childNodes[0], 'mousedown'); expect(mousedown).not.to.have.been.called; proto.removeEventListener.reset(); click.reset(); mousedown.reset(); render(
, scratch, scratch.firstChild); expect(proto.removeEventListener) .to.have.been.calledOnce .and.calledWith('click'); fireEvent(scratch.childNodes[0], 'click'); expect(click).not.to.have.been.called; proto.addEventListener.restore(); proto.removeEventListener.restore(); }); it('should use capturing for events that do not bubble', () => { let click = sinon.spy(), focus = sinon.spy(); let root = render((
), scratch); root.firstElementChild.click(); root.firstElementChild.focus(); expect(click, 'click').to.have.been.calledOnce; 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; // 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 serialize style objects', () => { let root = render((
test
), scratch); 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).to.have.deep.property('style.cssText').that.equals('color: rgb(0, 255, 255);'); root = render((
test
), scratch, root); expect(root).to.have.deep.property('style.cssText').that.equals('display: inline;'); root = render((
test
), scratch, root); expect(root).to.have.deep.property('style.cssText').that.equals('background-color: rgb(0, 255, 255);'); }); it('should serialize class/className', () => { render(
, scratch); let { className } = scratch.childNodes[0]; expect(className).to.be.a.string; expect(className.split(' ')) .to.include.members(['yes1', 'yes2', 'yes3', 'yes4', 'yes5']) .and.not.include.members(['no1', 'no2', 'no3', 'no4', 'no5']); }); it('should support dangerouslySetInnerHTML', () => { let html = 'foo & bar'; let root = render(
, scratch); expect(scratch.firstChild).to.have.property('innerHTML', html); expect(scratch.innerHTML).to.equal('
'+html+'
'); root = render(
ab
, scratch, root); expect(scratch).to.have.property('innerHTML', `
ab
`); root = render(
, scratch, root); expect(scratch.innerHTML).to.equal('
'+html+'
'); }); it('should reconcile mutated DOM attributes', () => { let check = p => render(, scratch, scratch.lastChild), value = () => scratch.lastChild.checked, setValue = p => scratch.lastChild.checked = p; check(true); expect(value()).to.equal(true); check(false); expect(value()).to.equal(false); check(true); expect(value()).to.equal(true); setValue(true); check(false); expect(value()).to.equal(false); setValue(false); check(true); expect(value()).to.equal(true); }); it('should ignore props.children if children are manually specified', () => { expect(
c
).to.eql(
c
); }); it('should reorder child pairs', () => { let root = render((
a b
), scratch, root); let a = scratch.firstChild.firstChild; let b = scratch.firstChild.lastChild; expect(a).to.have.property('nodeName', 'A'); expect(b).to.have.property('nodeName', 'B'); root = render((
b a
), scratch, root); expect(scratch.firstChild.firstChild).to.have.property('nodeName', 'B'); expect(scratch.firstChild.lastChild).to.have.property('nodeName', 'A'); expect(scratch.firstChild.firstChild).to.equal(b); expect(scratch.firstChild.lastChild).to.equal(a); }); // Discussion: https://github.com/developit/preact/issues/287 ('HTMLDataListElement' in window ? it : xit)('should allow to pass through as an attribute', () => { render((
), scratch); let html = scratch.firstElementChild.firstElementChild.outerHTML; expect(sortAttributes(html)).to.equal(sortAttributes('')); }); });