`}
+
+ `
+ }
+})
+
+test('updates in place', function (t) {
+ var id = makeId()
+ var div = document.createElement('div')
+ div.innerHTML = 'Hi world!'
+ document.body.appendChild(div)
+
+ render(main('planet'), div)
+ var res = document.getElementById(id)
+ t.equal(res.innerText, 'planet', 'content mounted')
+ render(main('world'), div)
+ t.equal(res.innerText, 'world', 'content updated')
+ document.body.removeChild(div)
+ t.end()
+
+ function main (text) {
+ return html`
Hello ${html`${text}`}!
`
+ }
+})
+
+test('persists in DOM between mounts', function (t) {
+ var id = makeId()
+ var div = document.createElement('div')
+ div.innerHTML = 'Hi world!'
+ document.body.appendChild(div)
+
+ render(foo('planet'), div)
+ var res = document.getElementById(id)
+ t.equal(res.innerText, 'planet', 'did mount')
+
+ render(bar('world'), div)
+ var updated = document.getElementById(id)
+ t.ok(res.isSameNode(updated), 'same node')
+ t.equal(updated.innerText, 'world', 'content updated')
+ document.body.removeChild(div)
+ t.end()
+
+ function foo (text) {
+ return html`
Hello ${name(text)}!
`
+ }
+
+ function bar (text) {
+ return html`
Hello ${name(text)}!
`
}
- t.equal(result.tagName, 'UL')
- t.equal(result.querySelector('button').textContent, 'click me')
+ function name (text) {
+ return html`${text}`
+ }
+})
+
+test('updating with null', function (t) {
+ var id = makeId()
+ var div = document.createElement('div')
+ div.innerHTML = 'Hi world!'
+ document.body.appendChild(div)
+
+ render(main('planet'), div)
+ var res = document.getElementById(id)
+ t.equal(res.innerHTML, 'Hello planet!', 'did mount')
+ render(main(false), div)
+ t.equal(res.innerHTML, 'Hello !', 'node was removed')
+ render(main('world'), div)
+ t.equal(res.innerHTML, 'Hello world!', 'node added back')
+ document.body.removeChild(div)
+ t.end()
+
+ function main (text) {
+ return html`
Hello ${text || null}!
`
+ }
+})
+
+test('updating from null', function (t) {
+ var id = makeId()
+ var div = document.createElement('div')
+ div.innerHTML = 'Hi world!'
+ document.body.appendChild(div)
+
+ render(main(false), div)
+ var res = document.getElementById(id)
+ t.equal(res.innerHTML, 'Hello !', 'node was removed')
+ render(main('planet'), div)
+ t.equal(res.innerHTML, 'Hello planet!', 'did mount')
+ render(main('world'), div)
+ t.equal(res.innerHTML, 'Hello world!', 'node added back')
+ document.body.removeChild(div)
+ t.end()
+
+ function main (text) {
+ return html`
Hello ${text || null}!
`
+ }
+})
+
+test('updating with array', function (t) {
+ var div = document.createElement('div')
+ div.innerHTML = 'Hi world!'
+ document.body.appendChild(div)
+
+ var children = [
+ html`planet`,
+ html`world`
+ ]
+ render(main(children[0]), div)
+ var firstChild = div.firstElementChild
+ t.equal(div.innerText, 'Hello planet!', 'child mounted')
+ render(main(children), div)
+ t.equal(div.innerText, 'Hello planetworld!', 'all children mounted')
+ t.equal(div.firstElementChild, firstChild, 'child remained in place')
+ document.body.removeChild(div)
+ t.end()
- button.click()
+ function main (children) {
+ return html`
Hello ${children}!
`
+ }
})
-test('using class and className', function (t) {
- t.plan(2)
- var result = html``
- t.equal(result.className, 'test1')
- result = html``
- t.equal(result.className, 'test2 another')
+test('alternating partials', function (t) {
+ var id = makeId()
+ var world = html`world`
+ var planet = html`planet`
+ var div = document.createElement('div')
+ div.innerHTML = 'Hi world!'
+ document.body.appendChild(div)
+
+ render(main(planet), div)
+ var res = document.getElementById(id)
+ t.equal(res.innerText, 'Hello planet!', 'did mount')
+ render(main(world), div)
+ t.equal(res.innerText, 'Hello world!', 'did update')
+ document.body.removeChild(div)
t.end()
+
+ function main (child) {
+ return html`
Hello ${child}!
`
+ }
})
+
+test('reordering children', function (t) {
+ var ids = [makeId(), makeId()]
+ var children = [
+ html`world`,
+ html`planet`
+ ]
+ var div = document.createElement('div')
+ div.innerHTML = 'Hi world!'
+ document.body.appendChild(div)
+
+ render(main(children), div)
+ var world = document.getElementById(ids[0])
+ var planet = document.getElementById(ids[1])
+ t.equal(world.nextSibling, planet, 'mounted in order')
+ t.equal(div.innerText, 'Hello worldplanet!', 'children in order')
+ render(main(children.reverse()), div)
+ world = document.getElementById(ids[0])
+ planet = document.getElementById(ids[1])
+ t.equal(planet.nextElementSibling, world, 'children reordered')
+ t.equal(div.innerText, 'Hello planetworld!', 'children in (reversed) order')
+ document.body.removeChild(div)
+ t.end()
+
+ function main (children) {
+ return html`
Hello ${children}!
`
+ }
+})
+
+test('async partials', function (t) {
+ t.test('resolves generators', function (t) {
+ t.plan(5)
+ var div = document.createElement('div')
+ render(html`
Hello ${outer('world')}!
`, div)
+ t.equal(div.innerText, 'Hello world!', 'content rendered')
+ t.equal(div.dataset.test, 'test', 'attribute resolved')
+
+ function * outer (text) {
+ var res = yield * echo(text)
+ t.equal(res, text, 'nested generators are unwound')
+ return html`${res}`
+ }
+
+ function * echo (arg) {
+ var res = yield 'foo'
+ t.equal(res, 'foo', 'yielded values are returned')
+ return arg
+ }
+ })
+
+ t.test('resolves promises', function (t) {
+ t.plan(4)
+ var div = document.createElement('div')
+ render(html`
Hello ${Promise.resolve('world')}!
`, div)
+ t.equal(div.innerText, 'Hello !', 'promise partial left blank')
+ t.equal(div.dataset.test, undefined, 'attribute left blank')
+ window.requestAnimationFrame(function () {
+ t.equal(div.innerText, 'Hello world!', 'content updated once resolved')
+ t.equal(div.dataset.test, 'test', 'attribute updated once resolved')
+ })
+ })
+
+ t.test('recursively resolves nested generators and promises', function (t) {
+ t.plan(4)
+ var div = document.createElement('div')
+ render(html`
Hello ${outer('world')}!
`, div)
+ t.equal(div.innerText, 'Hello !', 'promise partial left blank')
+ window.requestAnimationFrame(function () {
+ t.equal(div.innerText, 'Hello world!', 'content updated once resolved')
+ t.equal(div.firstElementChild.dataset.test, 'test', 'attribute updated once resolved')
+ })
+
+ function * outer (text) {
+ var res = yield Promise.resolve(inner(text))
+ t.equal(res, text, 'yielded promises are resolved')
+ return html`${res}`
+
+ function * inner (text) {
+ var res = yield text
+ return res
+ }
+ }
+ })
+
+ t.test('top level async generators resolve', function (t) {
+ t.plan(3)
+ var div = document.createElement('div')
+ var res = render(main('world'), div)
+ t.ok(res instanceof Promise, 'returns promise')
+ t.equal(div.innerText, '', 'nothing renderd async')
+ res.then(function () {
+ t.equal(div.innerText, 'Hello world!', 'content updated once resolved')
+ })
+
+ function * main (text) {
+ var res = yield Promise.resolve(text)
+ return html`
Hello ${res}!
`
+ }
+ })
+
+ t.test('does not succomb to race condition', function (t) {
+ t.plan(4)
+ var div = document.createElement('div')
+ render(Promise.resolve(main(Promise.resolve('world'))), div)
+ render(main('planet'), div)
+ t.equal(div.innerText, 'Hello planet!', 'sync content rendered')
+ t.equal(div.dataset.test, 'planet', 'sync attribute rendered')
+ window.requestAnimationFrame(function () {
+ t.equal(div.innerText, 'Hello planet!', 'sync content persisted')
+ t.equal(div.dataset.test, 'planet', 'sync attribute persisted')
+ })
+
+ function main (val) {
+ return html`
Hello ${val}!
`
+ }
+ })
+
+ t.test('does not affect ordering', function (t) {
+ t.plan(2)
+ var state = 0
+ var list = render(main())
+ t.equal(list.textContent, '1246', 'sync content rendered')
+ render(main(), list)
+ list.children.four.remove()
+ window.requestAnimationFrame(function () {
+ t.equal(list.textContent, '1356', 'async content in place')
+ })
+
+ function main (val) {
+ return html`
+
+
1
+ ${state++ ? null : html`
2
`}
+ ${Promise.resolve(html`
3
`)}
+ ${[
+ html`
4
`,
+ Promise.resolve(html`
5
`),
+ html`
6
`
+ ]}
+
+ `
+ }
+ })
+})
+
+test('can access elements with ref', function (t) {
+ var ref1 = new Ref('test')
+ var ref2
+
+ t.throws(function () {
+ ref1.className // eslint-disable-line
+ }, 'throws when accessing Element prototype property before render')
+ t.doesNotThrow(function () {
+ ref1.foo // eslint-disable-line
+ }, 'does not throw when accessing arbitrary prop')
+ t.throws(function () {
+ 'use strict'
+ ref1.className = 'foo'
+ }, 'mutating ref before render in strict mode throws')
+ t.doesNotThrow(function () {
+ ref1.className = 'foo'
+ }, 'mutation fails silently in non-strict mode')
+
+ var div = document.createElement('div')
+ document.body.appendChild(div)
+ render(main(ref1), div)
+
+ t.equal(div.id, 'test', 'custom ref id assigned')
+ t.equal(ref1.element, div, 'ref.element references element')
+ t.equal(ref1.className, 'test1', 'properties are read from element')
+ ref1.className = 'test2'
+ t.equal(div.className, 'test2', 'properties are set to element')
+ var id = ref2.id
+ render(main(ref1), div)
+ t.equal(id, ref2.id, 'id persist between renders')
+ document.body.removeChild(div)
+ t.end()
+
+ function main () {
+ ref2 = new Ref()
+ return html`
+
+ Hello world!
+
+ `
+ }
+})
+
+function makeId () {
+ return 'uid-' + Math.random().toString(36).substr(-4)
+}
diff --git a/tests/browser/component.js b/tests/browser/component.js
new file mode 100644
index 0000000..f868825
--- /dev/null
+++ b/tests/browser/component.js
@@ -0,0 +1,154 @@
+var test = require('tape')
+var { html, render } = require('../../')
+var { Component, memo, onupdate, onload } = require('../../component')
+
+// TODO: test alternating return value (null/array/partial)
+
+test('component can render', function (t) {
+ var id = makeId()
+ var div = document.createElement('div')
+ div.innerHTML = 'Hi world!'
+ document.body.appendChild(div)
+
+ var Greeting = Component(function Greeting (text) {
+ return html`Hello ${text}!`
+ })
+
+ render(main('planet'), div)
+ var res = document.getElementById(id)
+ t.equal(res.innerText, 'planet', 'content mounted')
+ render(main('world'), div)
+ t.equal(res.innerText, 'world', 'content updated')
+ document.body.removeChild(div)
+ t.end()
+
+ function main (text) {
+ return html`
${Greeting(text)}
`
+ }
+})
+
+test('component can render fragment', function (t) {
+ var update
+ var Greeting = Component(function Greeting (text) {
+ update = onupdate()
+ return html`Hello${text}!`
+ })
+ var div = document.createElement('div')
+ render(html`
${Greeting('world')}
`, div)
+ t.equal(div.innerText, 'Hello world!', 'children rendered')
+ update('planet')
+ t.equal(div.innerText, 'Hello planet!', 'children updated')
+ t.end()
+})
+
+test('component can update', function (t) {
+ t.plan(6)
+
+ var id = makeId()
+ var div = document.createElement('div')
+ div.innerHTML = 'Hi world!'
+ document.body.appendChild(div)
+
+ var update
+ var BEFORE_UPDATE = 0
+ var AFTER_UPDATE = 1
+ var state = BEFORE_UPDATE
+ var Greeting = Component(function Greeting (value) {
+ update = onupdate(function afterupdate (value) {
+ t.equal(value, state, 'afterupdate called with latest argument')
+ return function beforeupdate (value) {
+ t.equal(value, state, 'beforeupdate called with latest argument')
+ }
+ })
+ return html`Hello ${value}!`
+ })
+
+ render(main(state), div)
+ var res = document.getElementById(id)
+ t.equal(res.innerText, BEFORE_UPDATE.toString(), 'content mounted')
+ t.equal(typeof update, 'function', 'onupdate returns function')
+ update(++state)
+ t.equal(res.innerText, AFTER_UPDATE.toString(), 'content updated')
+ document.body.removeChild(div)
+
+ function main (value) {
+ return html`
${Greeting(value)}
`
+ }
+})
+
+test('component can memoize arguments', function (t) {
+ t.plan(12)
+
+ var id = makeId()
+ var div = document.createElement('div')
+ div.innerHTML = 'Hi world!'
+ document.body.appendChild(div)
+
+ var update
+ var BEFORE_UPDATE = [0, 0, 0]
+ var AFTER_UPDATE = [1, 1, 1]
+ var state = BEFORE_UPDATE
+ var Greeting = Component(function Greeting (a, b = memo(state[1]), c = memo(init)) {
+ t.deepEqual([a, b, c], state, 'all arguments in place')
+ update = onupdate(function afterupdate (a, b, c, d = memo(1)) {
+ t.deepEqual([a, b, c], state, 'afterupdate called with latest argument')
+ t.equal(d, 1, 'afterupdate can add more args')
+ return function beforeupdate (a, b, c, d = memo()) {
+ t.deepEqual([a, b, c], state, 'beforeupdate called with latest argument')
+ t.equal(d, 1, 'beforeupdate received additional arg')
+ }
+ })
+ return html`Hello ${[a, b, c].join('')}!`
+ })
+
+ render(main(state[0]), div)
+ var res = document.getElementById(id)
+ t.equal(res.innerText, BEFORE_UPDATE.join(''), 'content mounted')
+ state = AFTER_UPDATE
+ update(...state)
+ t.equal(res.innerText, AFTER_UPDATE.join(''), 'content updated')
+ document.body.removeChild(div)
+
+ function init (a, b) {
+ t.equal(a, state[0], 'static arg forwarded to init')
+ t.equal(b, state[1], 'dynamic arg forwarded to init')
+ return state[2]
+ }
+
+ function main (value) {
+ return html`
${Greeting(value)}
`
+ }
+})
+
+test('component is notified of being added to the DOM', function (t) {
+ t.plan(4)
+
+ var id = makeId()
+ var div = document.createElement('div')
+ div.innerHTML = 'Hi world!'
+ document.body.appendChild(div)
+
+ var Greeting = Component(function Greeting () {
+ onload(function (element) {
+ var el = document.getElementById(id)
+ t.pass('onload handler called')
+ t.equal(element, el, 'onload handler called with root element')
+ document.body.removeChild(div)
+ return function (value) {
+ t.pass('onload callback called')
+ t.equal(element, el, 'onload callback called with root element')
+ }
+ })
+ return html`Hello planet!`
+ })
+
+ render(main(), div)
+
+ function main (value) {
+ return html`
${Greeting(value)}
`
+ }
+})
+
+function makeId () {
+ return 'uid-' + Math.random().toString(36).substr(-4)
+}
diff --git a/tests/browser/elements.js b/tests/browser/elements.js
index 408a722..c4095b2 100644
--- a/tests/browser/elements.js
+++ b/tests/browser/elements.js
@@ -1,86 +1,20 @@
var test = require('tape')
-if (typeof window !== 'undefined') {
- var document = window.document
- var html = require('../../')
-} else {
- var nano = require('./html')
- document = nano.document
- html = nano.html
-}
+var { html, render } = require('../../')
test('create inputs', function (t) {
- t.plan(7)
-
var expected = 'testing'
- var result = html``
+ var result = render(html``)
t.equal(result.tagName, 'INPUT', 'created an input')
t.equal(result.value, expected, 'set the value of an input')
-
- result = html``
- t.equal(result.getAttribute('type'), 'checkbox', 'created a checkbox')
- t.equal(result.getAttribute('checked'), 'checked', 'set the checked attribute')
- t.equal(result.getAttribute('disabled'), null, 'should not have set the disabled attribute')
- t.equal(result.indeterminate, true, 'should have set indeterminate property')
-
- result = html``
- t.equal(result.indeterminate, true, 'should have set indeterminate property')
-
t.end()
})
-test('create inputs with object spread', function (t) {
- t.plan(7)
-
- var expected = 'testing'
- var props = { type: 'text', value: expected }
- var result = html``
- t.equal(result.tagName, 'INPUT', 'created an input')
- t.equal(result.value, expected, 'set the value of an input')
-
- props = { type: 'checkbox', checked: true, disabled: false, indeterminate: true }
- result = html``
- t.equal(result.getAttribute('type'), 'checkbox', 'created a checkbox')
- t.equal(result.getAttribute('checked'), 'checked', 'set the checked attribute')
- t.equal(result.getAttribute('disabled'), null, 'should not have set the disabled attribute')
- t.equal(result.indeterminate, true, 'should have set indeterminate property')
-
- props = { indeterminate: true }
- result = html``
- t.equal(result.indeterminate, true, 'should have set indeterminate property')
-
- t.end()
-})
-
-test('can update and submit inputs', function (t) {
- t.plan(2)
- document.body.innerHTML = ''
- var expected = 'testing'
- function render (data, onsubmit) {
- var input = html``
- return html`
- ${input}
-
-
`
- }
- var result = render(expected, function onsubmit (newvalue) {
- t.equal(newvalue, 'changed')
- document.body.innerHTML = ''
- t.end()
- })
- document.body.appendChild(result)
- t.equal(document.querySelector('input').value, expected, 'set the input correctly')
- document.querySelector('input').value = 'changed'
- document.querySelector('button').click()
-})
-
test('svg', function (t) {
t.plan(4)
- var result = html``)
t.equal(result.tagName, 'svg', 'create svg tag')
t.equal(result.childNodes[0].tagName, 'rect', 'created child rect tag')
t.equal(result.childNodes[1].getAttribute('xlink:href'), '#test', 'created child use tag with xlink:href')
@@ -92,9 +26,9 @@ test('svg with namespace', function (t) {
t.plan(3)
var result
function create () {
- result = html`
+ result = render(html`
- `
+ `)
}
t.doesNotThrow(create)
t.equal(result.tagName, 'svg', 'create svg tag')
@@ -105,9 +39,9 @@ test('svg with xmlns:svg', function (t) {
t.plan(3)
var result
function create () {
- result = html`
+ result = render(html`
- `
+ `)
}
t.doesNotThrow(create)
t.equal(result.tagName, 'svg', 'create svg tag')
@@ -115,7 +49,7 @@ test('svg with xmlns:svg', function (t) {
})
test('comments', function (t) {
- var result = html``
+ var result = render(html``)
t.equal(result.outerHTML, '', 'created comment')
t.end()
})
@@ -123,7 +57,7 @@ test('comments', function (t) {
test('style', function (t) {
t.plan(2)
var name = 'test'
- var result = html`
Hey ${name.toUpperCase()}, This is a card!!!
`
+ var result = render(html`
Hey ${name.toUpperCase()}, This is a card!!!
`)
t.equal(result.style.color, 'red', 'set style color on parent')
t.equal(result.querySelector('span').style.color, 'blue', 'set style color on child')
t.end()
@@ -133,114 +67,141 @@ test('adjacent text nodes', function (t) {
t.plan(2)
var who = 'world'
var exclamation = ['!', ' :)']
- var result = html`
hello ${who}${exclamation}
`
- t.equal(result.childNodes.length, 1, 'should be merged')
+ var result = render(html`
', 'should have correct output')
t.end()
})
test('space in only-child text nodes', function (t) {
t.plan(1)
- var result = html`
+ var result = render(html`
surrounding
newlines
- `
+ `)
t.equal(result.outerHTML, 'surrounding newlines', 'should remove extra space')
t.end()
})
test('space between text and non-text nodes', function (t) {
t.plan(1)
- var result = html`
+ var result = render(html`
whitespace
is empty
- `
+ `)
t.equal(result.outerHTML, '
whitespace is empty
', 'should have correct output')
t.end()
})
test('space between text followed by non-text nodes', function (t) {
t.plan(1)
- var result = html`
+ var result = render(html`
whitespace
is strong
- `
+ `)
t.equal(result.outerHTML, '
whitespace is strong
', 'should have correct output')
t.end()
})
test('space around text surrounded by non-text nodes', function (t) {
t.plan(1)
- var result = html`
+ var result = render(html`
I agree
whitespace
is strong
- `
+ `)
t.equal(result.outerHTML, '
I agree whitespace is strong
', 'should have correct output')
t.end()
})
test('space between non-text nodes', function (t) {
t.plan(1)
- var result = html`
+ var result = render(html`
whitespace
is beautiful
- `
+ `)
t.equal(result.outerHTML, '
whitespace is beautiful
', 'should have correct output')
t.end()
})
test('space in
', function (t) {
t.plan(1)
- var result = html`
+ var result = render(html`
whitespace is empty
- `
+ `)
t.equal(result.outerHTML, '
\n whitespace is empty\n
', 'should preserve space')
t.end()
})
test('space in