From 30b577138dda685f65a8529be1866afa6e321845 Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Tue, 4 Oct 2016 11:50:26 +0200 Subject: Squashed 'thirdparty/preact/' content from commit b2d90cc git-subtree-dir: thirdparty/preact git-subtree-split: b2d90cc116f1d1998f7a7c98dc6986bf4c1841f4 --- test/browser/render.js | 439 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 439 insertions(+) create mode 100644 test/browser/render.js (limited to 'test/browser/render.js') diff --git a/test/browser/render.js b/test/browser/render.js new file mode 100644 index 000000000..5d18fb282 --- /dev/null +++ b/test/browser/render.js @@ -0,0 +1,439 @@ +/* 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('')); + }); +}); -- cgit v1.2.3