diff --git a/api/tests/index.html b/api/tests/index.html
index fd2557d5..37d313f4 100644
--- a/api/tests/index.html
+++ b/api/tests/index.html
@@ -13,6 +13,7 @@
+
diff --git a/api/tests/test-mount.js b/api/tests/test-mount.js
index 038f470a..4ba466c7 100644
--- a/api/tests/test-mount.js
+++ b/api/tests/test-mount.js
@@ -1,6 +1,7 @@
"use strict"
var o = require("../../ospec/ospec")
+var components = require("../../test-utils/components")
var domMock = require("../../test-utils/domMock")
var m = require("../../render/hyperscript")
@@ -22,16 +23,6 @@ o.spec("mount", function() {
render = coreRenderer($window).render
})
- o("throws on invalid `root` DOM node", function() {
- var threw = false
- try {
- mount(null, {view: function() {}})
- } catch (e) {
- threw = true
- }
- o(threw).equals(true)
- })
-
o("throws on invalid component", function() {
var threw = false
try {
@@ -42,227 +33,223 @@ o.spec("mount", function() {
o(threw).equals(true)
})
- o("renders into `root` (POJO component)", function() {
- mount(root, {
- view : function() {
- return m("div")
- }
- })
+ components.forEach(function(cmp){
+ o.spec(cmp.kind, function(){
+ var createComponent = cmp.create
- o(root.firstChild.nodeName).equals("DIV")
- })
-
- o("renders into `root` (class component)", function() {
- function Cmp(){}
- Cmp.prototype.view = function(){return m("div")}
- mount(root, Cmp)
-
- o(root.firstChild.nodeName).equals("DIV")
- })
-
- o("renders into `root` factory (factory component)", function() {
- mount(root, function(){
- return {
- view : function() {
- return m("div")
+ o("throws on invalid `root` DOM node", function() {
+ var threw = false
+ try {
+ mount(null, createComponent({view: function() {}}))
+ } catch (e) {
+ threw = true
}
- }
- })
+ o(threw).equals(true)
+ })
- o(root.firstChild.nodeName).equals("DIV")
- })
-
- o("mounting null unmounts", function() {
- mount(root, {
- view : function() {
- return m("div")
- }
- })
-
- mount(root, null)
-
- o(root.childNodes.length).equals(0)
- })
-
- o("redraws on events", function(done) {
- var onupdate = o.spy()
- var oninit = o.spy()
- var onclick = o.spy()
- var e = $window.document.createEvent("MouseEvents")
-
- e.initEvent("click", true, true)
-
- mount(root, {
- view : function() {
- return m("div", {
- oninit : oninit,
- onupdate : onupdate,
- onclick : onclick,
- })
- }
- })
-
- root.firstChild.dispatchEvent(e)
-
- o(oninit.callCount).equals(1)
- o(onupdate.callCount).equals(0)
-
- o(onclick.callCount).equals(1)
- o(onclick.this).equals(root.firstChild)
- o(onclick.args[0].type).equals("click")
- o(onclick.args[0].target).equals(root.firstChild)
-
- // Wrapped to give time for the rate-limited redraw to fire
- setTimeout(function() {
- o(onupdate.callCount).equals(1)
-
- done()
- }, FRAME_BUDGET)
- })
-
- o("redraws several mount points on events", function(done, timeout) {
- timeout(60)
-
- var onupdate0 = o.spy()
- var oninit0 = o.spy()
- var onclick0 = o.spy()
- var onupdate1 = o.spy()
- var oninit1 = o.spy()
- var onclick1 = o.spy()
-
- var e = $window.document.createEvent("MouseEvents")
-
- e.initEvent("click", true, true)
-
- render(root, [
- m("#child0"),
- m("#child1")
- ])
-
- mount(root.childNodes[0], {
- view : function() {
- return m("div", {
- oninit : oninit0,
- onupdate : onupdate0,
- onclick : onclick0,
- })
- }
- })
-
- o(oninit0.callCount).equals(1)
- o(onupdate0.callCount).equals(0)
-
- mount(root.childNodes[1], {
- view : function() {
- return m("div", {
- oninit : oninit1,
- onupdate : onupdate1,
- onclick : onclick1,
- })
- }
- })
-
- o(oninit1.callCount).equals(1)
- o(onupdate1.callCount).equals(0)
-
- root.childNodes[0].firstChild.dispatchEvent(e)
- o(onclick0.callCount).equals(1)
- o(onclick0.this).equals(root.childNodes[0].firstChild)
-
- setTimeout(function() {
- o(onupdate0.callCount).equals(1)
- o(onupdate1.callCount).equals(1)
-
- root.childNodes[1].firstChild.dispatchEvent(e)
- o(onclick1.callCount).equals(1)
- o(onclick1.this).equals(root.childNodes[1].firstChild)
-
- setTimeout(function() {
- o(onupdate0.callCount).equals(2)
- o(onupdate1.callCount).equals(2)
-
- done()
- }, FRAME_BUDGET)
- }, FRAME_BUDGET)
-
- })
-
- o("event handlers can skip redraw", function(done) {
- var onupdate = o.spy()
- var oninit = o.spy()
- var e = $window.document.createEvent("MouseEvents")
-
- e.initEvent("click", true, true)
-
- mount(root, {
- view: function() {
- return m("div", {
- oninit: oninit,
- onupdate: onupdate,
- onclick: function(e) {
- e.redraw = false
+ o("renders into `root`", function() {
+ mount(root, createComponent({
+ view : function() {
+ return m("div")
}
- })
- }
+ }))
+
+ o(root.firstChild.nodeName).equals("DIV")
+ })
+
+ o("mounting null unmounts", function() {
+ mount(root, createComponent({
+ view : function() {
+ return m("div")
+ }
+ }))
+
+ mount(root, null)
+
+ o(root.childNodes.length).equals(0)
+ })
+
+ o("redraws on events", function(done) {
+ var onupdate = o.spy()
+ var oninit = o.spy()
+ var onclick = o.spy()
+ var e = $window.document.createEvent("MouseEvents")
+
+ e.initEvent("click", true, true)
+
+ mount(root, createComponent({
+ view : function() {
+ return m("div", {
+ oninit : oninit,
+ onupdate : onupdate,
+ onclick : onclick,
+ })
+ }
+ }))
+
+ root.firstChild.dispatchEvent(e)
+
+ o(oninit.callCount).equals(1)
+ o(onupdate.callCount).equals(0)
+
+ o(onclick.callCount).equals(1)
+ o(onclick.this).equals(root.firstChild)
+ o(onclick.args[0].type).equals("click")
+ o(onclick.args[0].target).equals(root.firstChild)
+
+ // Wrapped to give time for the rate-limited redraw to fire
+ setTimeout(function() {
+ o(onupdate.callCount).equals(1)
+
+ done()
+ }, FRAME_BUDGET)
+ })
+
+ o("redraws several mount points on events", function(done, timeout) {
+ timeout(60)
+
+ var onupdate0 = o.spy()
+ var oninit0 = o.spy()
+ var onclick0 = o.spy()
+ var onupdate1 = o.spy()
+ var oninit1 = o.spy()
+ var onclick1 = o.spy()
+
+ var e = $window.document.createEvent("MouseEvents")
+
+ e.initEvent("click", true, true)
+
+ render(root, [
+ m("#child0"),
+ m("#child1")
+ ])
+
+ mount(root.childNodes[0], createComponent({
+ view : function() {
+ return m("div", {
+ oninit : oninit0,
+ onupdate : onupdate0,
+ onclick : onclick0,
+ })
+ }
+ }))
+
+ o(oninit0.callCount).equals(1)
+ o(onupdate0.callCount).equals(0)
+
+ mount(root.childNodes[1], createComponent({
+ view : function() {
+ return m("div", {
+ oninit : oninit1,
+ onupdate : onupdate1,
+ onclick : onclick1,
+ })
+ }
+ }))
+
+ o(oninit1.callCount).equals(1)
+ o(onupdate1.callCount).equals(0)
+
+ root.childNodes[0].firstChild.dispatchEvent(e)
+ o(onclick0.callCount).equals(1)
+ o(onclick0.this).equals(root.childNodes[0].firstChild)
+
+ setTimeout(function() {
+ o(onupdate0.callCount).equals(1)
+ o(onupdate1.callCount).equals(1)
+
+ root.childNodes[1].firstChild.dispatchEvent(e)
+ o(onclick1.callCount).equals(1)
+ o(onclick1.this).equals(root.childNodes[1].firstChild)
+
+ setTimeout(function() {
+ o(onupdate0.callCount).equals(2)
+ o(onupdate1.callCount).equals(2)
+
+ done()
+ }, FRAME_BUDGET)
+ }, FRAME_BUDGET)
+
+ })
+
+ o("event handlers can skip redraw", function(done) {
+ var onupdate = o.spy()
+ var oninit = o.spy()
+ var e = $window.document.createEvent("MouseEvents")
+
+ e.initEvent("click", true, true)
+
+ mount(root, createComponent({
+ view: function() {
+ return m("div", {
+ oninit: oninit,
+ onupdate: onupdate,
+ onclick: function(e) {
+ e.redraw = false
+ }
+ })
+ }
+ }))
+
+ root.firstChild.dispatchEvent(e)
+
+ o(oninit.callCount).equals(1)
+
+ // Wrapped to ensure no redraw fired
+ setTimeout(function() {
+ o(onupdate.callCount).equals(0)
+
+ done()
+ }, FRAME_BUDGET)
+ })
+
+ o("redraws when the render function is run", function(done) {
+ var onupdate = o.spy()
+ var oninit = o.spy()
+
+ mount(root, createComponent({
+ view : function() {
+ return m("div", {
+ oninit: oninit,
+ onupdate: onupdate
+ })
+ }
+ }))
+
+ o(oninit.callCount).equals(1)
+ o(onupdate.callCount).equals(0)
+
+ redrawService.redraw()
+
+ // Wrapped to give time for the rate-limited redraw to fire
+ setTimeout(function() {
+ o(onupdate.callCount).equals(1)
+
+ done()
+ }, FRAME_BUDGET)
+ })
+
+ o("throttles", function(done, timeout) {
+ timeout(200)
+
+ var i = 0
+ mount(root, createComponent({view: function() {i++}}))
+ var before = i
+
+ redrawService.redraw()
+ redrawService.redraw()
+ redrawService.redraw()
+ redrawService.redraw()
+
+ var after = i
+
+ setTimeout(function(){
+ o(before).equals(1) // mounts synchronously
+ o(after).equals(1) // throttles rest
+ o(i).equals(2)
+ done()
+ },40)
+ })
})
-
- root.firstChild.dispatchEvent(e)
-
- o(oninit.callCount).equals(1)
-
- // Wrapped to ensure no redraw fired
- setTimeout(function() {
- o(onupdate.callCount).equals(0)
-
- done()
- }, FRAME_BUDGET)
})
-
- o("redraws when the render function is run", function(done) {
- var onupdate = o.spy()
- var oninit = o.spy()
-
- mount(root, {
- view : function() {
- return m("div", {
- oninit: oninit,
- onupdate: onupdate
- })
- }
- })
-
- o(oninit.callCount).equals(1)
- o(onupdate.callCount).equals(0)
-
- redrawService.redraw()
-
- // Wrapped to give time for the rate-limited redraw to fire
- setTimeout(function() {
- o(onupdate.callCount).equals(1)
-
- done()
- }, FRAME_BUDGET)
- })
-
- o("throttles", function(done, timeout) {
- timeout(200)
-
- var i = 0
- mount(root, {view: function() {i++}})
- var before = i
-
- redrawService.redraw()
- redrawService.redraw()
- redrawService.redraw()
- redrawService.redraw()
-
- var after = i
-
- setTimeout(function(){
- o(before).equals(1) // mounts synchronously
- o(after).equals(1) // throttles rest
- o(i).equals(2)
- done()
- },40)
- })
-})
+})
\ No newline at end of file
diff --git a/api/tests/test-router.js b/api/tests/test-router.js
index c624789b..f43605e1 100644
--- a/api/tests/test-router.js
+++ b/api/tests/test-router.js
@@ -82,7 +82,7 @@ o.spec("route", function() {
})
- o("routed mount points can redraw synchronously (factory component)", function() {
+ o("routed mount points can redraw synchronously (closure component)", function() {
var view = o.spy()
function Cmp() {return {view: view}}
diff --git a/render/render.js b/render/render.js
index 483c2637..f5d35085 100644
--- a/render/render.js
+++ b/render/render.js
@@ -20,8 +20,8 @@ module.exports = function($window) {
}
function createNode(parent, vnode, hooks, ns, nextSibling) {
var tag = vnode.tag
- if (vnode.attrs != null) initLifecycle(vnode.attrs, vnode, hooks)
if (typeof tag === "string") {
+ if (vnode.attrs != null) initLifecycle(vnode.attrs, vnode, hooks)
switch (tag) {
case "#": return createText(parent, vnode, nextSibling)
case "<": return createHTML(parent, vnode, nextSibling)
@@ -100,7 +100,7 @@ module.exports = function($window) {
}
return element
}
- function createComponent(parent, vnode, hooks, ns, nextSibling) {
+ function initComponent(vnode, hooks) {
var sentinel
if (typeof vnode.tag === "function") {
vnode.state = null
@@ -116,10 +116,13 @@ module.exports = function($window) {
sentinel.$$reentrantLock$$ = true
}
+ if (vnode.attrs != null) initLifecycle(vnode.attrs, vnode, hooks)
initLifecycle(vnode.state, vnode, hooks)
vnode.instance = Vnode.normalize(vnode.state.view(vnode))
sentinel.$$reentrantLock$$ = null
-
+ }
+ function createComponent(parent, vnode, hooks, ns, nextSibling) {
+ initComponent(vnode, hooks)
if (vnode.instance != null) {
if (vnode.instance === vnode) throw Error("A view cannot return the vnode it received as arguments")
var element = createNode(parent, vnode.instance, hooks, ns, nextSibling)
@@ -232,11 +235,12 @@ module.exports = function($window) {
if (oldTag === tag) {
vnode.state = old.state
vnode.events = old.events
- if (shouldUpdate(vnode, old)) return
- if (vnode.attrs != null) {
- updateLifecycle(vnode.attrs, vnode, hooks, recycling)
- }
+ if (!recycling && shouldNotUpdate(vnode, old)) return
if (typeof oldTag === "string") {
+ if (vnode.attrs != null) {
+ if (recycling) initLifecycle(vnode.attrs, vnode, hooks)
+ else updateLifecycle(vnode.attrs, vnode, hooks)
+ }
switch (oldTag) {
case "#": updateText(old, vnode); break
case "<": updateHTML(parent, old, vnode, nextSibling); break
@@ -306,8 +310,13 @@ module.exports = function($window) {
}
}
function updateComponent(parent, old, vnode, hooks, nextSibling, recycling, ns) {
- vnode.instance = Vnode.normalize(vnode.state.view(vnode))
- updateLifecycle(vnode.state, vnode, hooks, recycling)
+ if (recycling) {
+ initComponent(vnode, hooks)
+ } else {
+ vnode.instance = Vnode.normalize(vnode.state.view(vnode))
+ if (vnode.attrs != null) updateLifecycle(vnode.attrs, vnode, hooks)
+ updateLifecycle(vnode.state, vnode, hooks)
+ }
if (vnode.instance != null) {
if (old.instance == null) createNode(parent, vnode.instance, hooks, ns, nextSibling)
else updateNode(parent, old.instance, vnode.instance, hooks, nextSibling, recycling, ns)
@@ -566,11 +575,10 @@ module.exports = function($window) {
if (typeof source.oninit === "function") source.oninit.call(vnode.state, vnode)
if (typeof source.oncreate === "function") hooks.push(source.oncreate.bind(vnode.state, vnode))
}
- function updateLifecycle(source, vnode, hooks, recycling) {
- if (recycling) initLifecycle(source, vnode, hooks)
- else if (typeof source.onupdate === "function") hooks.push(source.onupdate.bind(vnode.state, vnode))
+ function updateLifecycle(source, vnode, hooks) {
+ if (typeof source.onupdate === "function") hooks.push(source.onupdate.bind(vnode.state, vnode))
}
- function shouldUpdate(vnode, old) {
+ function shouldNotUpdate(vnode, old) {
var forceVnodeUpdate, forceComponentUpdate
if (vnode.attrs != null && typeof vnode.attrs.onbeforeupdate === "function") forceVnodeUpdate = vnode.attrs.onbeforeupdate.call(vnode.state, vnode, old)
if (typeof vnode.tag !== "string" && typeof vnode.state.onbeforeupdate === "function") forceComponentUpdate = vnode.state.onbeforeupdate(vnode, old)
diff --git a/render/tests/index.html b/render/tests/index.html
index 480b8b7c..b978ae6f 100644
--- a/render/tests/index.html
+++ b/render/tests/index.html
@@ -8,6 +8,7 @@
+
diff --git a/render/tests/test-component.js b/render/tests/test-component.js
index ed70fe7d..0ff4e07e 100644
--- a/render/tests/test-component.js
+++ b/render/tests/test-component.js
@@ -1,6 +1,7 @@
"use strict"
var o = require("../../ospec/ospec")
+var components = require("../../test-utils/components")
var domMock = require("../../test-utils/domMock")
var vdom = require("../../render/render")
@@ -13,823 +14,901 @@ o.spec("component", function() {
render = vdom($window).render
})
- o.spec("basics", function() {
- o("works", function() {
- var component = {
- view: function() {
- return {tag: "div", attrs: {id: "a"}, text: "b"}
- }
- }
- var node = {tag: component}
-
- render(root, [node])
-
- o(root.firstChild.nodeName).equals("DIV")
- o(root.firstChild.attributes["id"].nodeValue).equals("a")
- o(root.firstChild.firstChild.nodeValue).equals("b")
- })
- o("receives arguments", function() {
- var component = {
- view: function(vnode) {
- return {tag: "div", attrs: vnode.attrs, text: vnode.text}
- }
- }
- var node = {tag: component, attrs: {id: "a"}, text: "b"}
-
- render(root, [node])
-
- o(root.firstChild.nodeName).equals("DIV")
- o(root.firstChild.attributes["id"].nodeValue).equals("a")
- o(root.firstChild.firstChild.nodeValue).equals("b")
- })
- o("updates", function() {
- var component = {
- view: function(vnode) {
- return {tag: "div", attrs: vnode.attrs, text: vnode.text}
- }
- }
- render(root, [{tag: component, attrs: {id: "a"}, text: "b"}])
- render(root, [{tag: component, attrs: {id: "c"}, text: "d"}])
-
- o(root.firstChild.nodeName).equals("DIV")
- o(root.firstChild.attributes["id"].nodeValue).equals("c")
- o(root.firstChild.firstChild.nodeValue).equals("d")
- })
- o("updates root from null", function() {
- var visible = false
- var component = {
- view: function(vnode) {
- return visible ? {tag: "div"} : null
- }
- }
- render(root, [{tag: component}])
- visible = true
- render(root, [{tag: component}])
-
- o(root.firstChild.nodeName).equals("DIV")
- })
- o("updates root from primitive", function() {
- var visible = false
- var component = {
- view: function(vnode) {
- return visible ? {tag: "div"} : false
- }
- }
- render(root, [{tag: component}])
- visible = true
- render(root, [{tag: component}])
-
- o(root.firstChild.nodeName).equals("DIV")
- })
- o("updates root to null", function() {
- var visible = true
- var component = {
- view: function(vnode) {
- return visible ? {tag: "div"} : null
- }
- }
- render(root, [{tag: component}])
- visible = false
- render(root, [{tag: component}])
-
- o(root.childNodes.length).equals(0)
- })
- o("updates root to primitive", function() {
- var visible = true
- var component = {
- view: function(vnode) {
- return visible ? {tag: "div"} : false
- }
- }
- render(root, [{tag: component}])
- visible = false
- render(root, [{tag: component}])
-
- o(root.firstChild.nodeValue).equals("")
- })
- o("updates root from null to null", function() {
- var component = {
- view: function(vnode) {
- return null
- }
- }
- render(root, [{tag: component}])
- render(root, [{tag: component}])
-
- o(root.childNodes.length).equals(0)
- })
- o("removes", function() {
- var component = {
- view: function(vnode) {
- return {tag: "div"}
- }
- }
- var div = {tag: "div", key: 2}
- render(root, [{tag: component, key: 1}, div])
- render(root, [{tag: "div", key: 2}])
-
- o(root.childNodes.length).equals(1)
- o(root.firstChild).equals(div.dom)
- })
- o("svg works when creating across component boundary", function() {
- var component = {
- view: function(vnode) {
- return {tag: "g"}
- }
- }
- render(root, [{tag: "svg", children: [{tag: component}]}])
-
- o(root.firstChild.firstChild.namespaceURI).equals("http://www.w3.org/2000/svg")
- })
- o("svg works when updating across component boundary", function() {
- var component = {
- view: function(vnode) {
- return {tag: "g"}
- }
- }
- render(root, [{tag: "svg", children: [{tag: component}]}])
- render(root, [{tag: "svg", children: [{tag: component}]}])
-
- o(root.firstChild.firstChild.namespaceURI).equals("http://www.w3.org/2000/svg")
- })
- })
- o.spec("return value", function() {
- o("can return fragments", function() {
- var component = {
- view: function(vnode) {
- return [
- {tag: "label"},
- {tag: "input"},
- ]
- }
- }
- render(root, [{tag: component}])
-
- o(root.childNodes.length).equals(2)
- o(root.childNodes[0].nodeName).equals("LABEL")
- o(root.childNodes[1].nodeName).equals("INPUT")
- })
- o("can return string", function() {
- var component = {
- view: function(vnode) {
- return "a"
- }
- }
- render(root, [{tag: component}])
-
- o(root.firstChild.nodeType).equals(3)
- o(root.firstChild.nodeValue).equals("a")
- })
- o("can return falsy string", function() {
- var component = {
- view: function(vnode) {
- return ""
- }
- }
- render(root, [{tag: component}])
-
- o(root.firstChild.nodeType).equals(3)
- o(root.firstChild.nodeValue).equals("")
- })
- o("can return number", function() {
- var component = {
- view: function(vnode) {
- return 1
- }
- }
- render(root, [{tag: component}])
-
- o(root.firstChild.nodeType).equals(3)
- o(root.firstChild.nodeValue).equals("1")
- })
- o("can return falsy number", function() {
- var component = {
- view: function(vnode) {
- return 0
- }
- }
- render(root, [{tag: component}])
-
- o(root.firstChild.nodeType).equals(3)
- o(root.firstChild.nodeValue).equals("0")
- })
- o("can return boolean", function() {
- var component = {
- view: function(vnode) {
- return true
- }
- }
- render(root, [{tag: component}])
-
- o(root.firstChild.nodeType).equals(3)
- o(root.firstChild.nodeValue).equals("true")
- })
- o("can return falsy boolean", function() {
- var component = {
- view: function(vnode) {
- return false
- }
- }
- render(root, [{tag: component}])
-
- o(root.firstChild.nodeType).equals(3)
- o(root.firstChild.nodeValue).equals("")
- })
- o("can return null", function() {
- var component = {
- view: function(vnode) {
- return null
- }
- }
- render(root, [{tag: component}])
-
- o(root.childNodes.length).equals(0)
- })
- o("can return undefined", function() {
- var component = {
- view: function(vnode) {
- return undefined
- }
- }
- render(root, [{tag: component}])
-
- o(root.childNodes.length).equals(0)
- })
- o("throws a custom error if it returns itself", function() {
- // A view that returns its vnode would otherwise trigger an infinite loop
- var component = {
- view: function(vnode) {
- return vnode
- }
- }
- try {
- render(root, [{tag: component}])
- }
- catch (e) {
- o(e instanceof Error).equals(true)
- // Call stack exception is a RangeError
- o(e instanceof RangeError).equals(false)
- }
- })
- o("can update when returning fragments", function() {
- var component = {
- view: function(vnode) {
- return [
- {tag: "label"},
- {tag: "input"},
- ]
- }
- }
- render(root, [{tag: component}])
- render(root, [{tag: component}])
-
- o(root.childNodes.length).equals(2)
- o(root.childNodes[0].nodeName).equals("LABEL")
- o(root.childNodes[1].nodeName).equals("INPUT")
- })
- o("can update when returning primitive", function() {
- var component = {
- view: function(vnode) {
- return "a"
- }
- }
- render(root, [{tag: component}])
- render(root, [{tag: component}])
-
- o(root.firstChild.nodeType).equals(3)
- o(root.firstChild.nodeValue).equals("a")
- })
- o("can update when returning null", function() {
- var component = {
- view: function(vnode) {
- return null
- }
- }
- render(root, [{tag: component}])
- render(root, [{tag: component}])
-
- o(root.childNodes.length).equals(0)
- })
- o("can remove when returning fragments", function() {
- var component = {
- view: function(vnode) {
- return [
- {tag: "label"},
- {tag: "input"},
- ]
- }
- }
- var div = {tag: "div", key: 2}
- render(root, [{tag: component, key: 1}, div])
-
- render(root, [{tag: "div", key: 2}])
-
- o(root.childNodes.length).equals(1)
- o(root.firstChild).equals(div.dom)
- })
- o("can remove when returning primitive", function() {
- var component = {
- view: function(vnode) {
- return "a"
- }
- }
- var div = {tag: "div", key: 2}
- render(root, [{tag: component, key: 1}, div])
-
- render(root, [{tag: "div", key: 2}])
-
- o(root.childNodes.length).equals(1)
- o(root.firstChild).equals(div.dom)
- })
- })
- o.spec("lifecycle", function() {
- o("calls oninit", function() {
- var called = 0
- var component = {
- oninit: function(vnode) {
- called++
-
- o(vnode.tag).equals(component)
- o(vnode.dom).equals(undefined)
- o(root.childNodes.length).equals(0)
- },
- view: function() {
- return {tag: "div", attrs: {id: "a"}, text: "b"}
- }
- }
- var node = {tag: component}
-
- render(root, [node])
-
- o(called).equals(1)
- o(root.firstChild.nodeName).equals("DIV")
- o(root.firstChild.attributes["id"].nodeValue).equals("a")
- o(root.firstChild.firstChild.nodeValue).equals("b")
- })
- o("calls oninit when returning fragment", function() {
- var called = 0
- var component = {
- oninit: function(vnode) {
- called++
-
- o(vnode.tag).equals(component)
- o(vnode.dom).equals(undefined)
- o(root.childNodes.length).equals(0)
- },
- view: function() {
- return [{tag: "div", attrs: {id: "a"}, text: "b"}]
- }
- }
- var node = {tag: component}
-
- render(root, [node])
-
- o(called).equals(1)
- o(root.firstChild.nodeName).equals("DIV")
- o(root.firstChild.attributes["id"].nodeValue).equals("a")
- o(root.firstChild.firstChild.nodeValue).equals("b")
- })
- o("calls oninit before view", function() {
- var viewCalled = false
-
- render(root, {
- tag: {
- view: function() {
- viewCalled = true
- return [{tag: "div", attrs: {id: "a"}, text: "b"}]
- },
- oninit: function(vnode) {
- o(viewCalled).equals(false)
- },
- }
- })
- })
- o("does not calls oninit on redraw", function() {
- var init = o.spy()
- var component = {
- view: function() {
- return {tag: "div", attrs: {id: "a"}, text: "b"}
- },
- oninit: init,
- }
-
- function view() {
- return {tag: component}
- }
-
- render(root, view())
- render(root, view())
-
- o(init.callCount).equals(1)
- })
- o("calls oncreate", function() {
- var called = 0
- var component = {
- oncreate: function(vnode) {
- called++
-
- o(vnode.dom).notEquals(undefined)
- o(vnode.dom).equals(root.firstChild)
- o(root.childNodes.length).equals(1)
- },
- view: function() {
- return {tag: "div", attrs: {id: "a"}, text: "b"}
- }
- }
- var node = {tag: component}
-
- render(root, [node])
-
- o(called).equals(1)
- o(root.firstChild.nodeName).equals("DIV")
- o(root.firstChild.attributes["id"].nodeValue).equals("a")
- o(root.firstChild.firstChild.nodeValue).equals("b")
- })
- o("does not calls oncreate on redraw", function() {
- var create = o.spy()
- var component = {
- view: function() {
- return {tag: "div", attrs: {id: "a"}, text: "b"}
- },
- oncreate: create,
- }
-
- function view() {
- return {tag: component}
- }
-
- render(root, view())
- render(root, view())
-
- o(create.callCount).equals(1)
- })
- o("calls oncreate when returning fragment", function() {
- var called = 0
- var component = {
- oncreate: function(vnode) {
- called++
-
- o(vnode.dom).notEquals(undefined)
- o(vnode.dom).equals(root.firstChild)
- o(root.childNodes.length).equals(1)
- },
- view: function() {
- return [{tag: "div", attrs: {id: "a"}, text: "b"}]
- }
- }
- var node = {tag: component}
-
- render(root, [node])
-
- o(called).equals(1)
- o(root.firstChild.nodeName).equals("DIV")
- o(root.firstChild.attributes["id"].nodeValue).equals("a")
- o(root.firstChild.firstChild.nodeValue).equals("b")
- })
- o("calls onupdate", function() {
- var called = 0
- var component = {
- onupdate: function(vnode) {
- called++
-
- o(vnode.dom).notEquals(undefined)
- o(vnode.dom).equals(root.firstChild)
- o(root.childNodes.length).equals(1)
- },
- view: function() {
- return {tag: "div", attrs: {id: "a"}, text: "b"}
- }
- }
-
- render(root, [{tag: component}])
-
- o(called).equals(0)
-
- render(root, [{tag: component}])
-
- o(called).equals(1)
- o(root.firstChild.nodeName).equals("DIV")
- o(root.firstChild.attributes["id"].nodeValue).equals("a")
- o(root.firstChild.firstChild.nodeValue).equals("b")
- })
- o("calls onupdate when returning fragment", function() {
- var called = 0
- var component = {
- onupdate: function(vnode) {
- called++
-
- o(vnode.dom).notEquals(undefined)
- o(vnode.dom).equals(root.firstChild)
- o(root.childNodes.length).equals(1)
- },
- view: function() {
- return [{tag: "div", attrs: {id: "a"}, text: "b"}]
- }
- }
-
- render(root, [{tag: component}])
-
- o(called).equals(0)
-
- render(root, [{tag: component}])
-
- o(called).equals(1)
- o(root.firstChild.nodeName).equals("DIV")
- o(root.firstChild.attributes["id"].nodeValue).equals("a")
- o(root.firstChild.firstChild.nodeValue).equals("b")
- })
- o("calls onremove", function() {
- var called = 0
- var component = {
- onremove: function(vnode) {
- called++
-
- o(vnode.dom).notEquals(undefined)
- o(vnode.dom).equals(root.firstChild)
- o(root.childNodes.length).equals(1)
- },
- view: function() {
- return {tag: "div", attrs: {id: "a"}, text: "b"}
- }
- }
-
- render(root, [{tag: component}])
-
- o(called).equals(0)
-
- render(root, [])
-
- o(called).equals(1)
- o(root.childNodes.length).equals(0)
- })
- o("calls onremove when returning fragment", function() {
- var called = 0
- var component = {
- onremove: function(vnode) {
- called++
-
- o(vnode.dom).notEquals(undefined)
- o(vnode.dom).equals(root.firstChild)
- o(root.childNodes.length).equals(1)
- },
- view: function() {
- return [{tag: "div", attrs: {id: "a"}, text: "b"}]
- }
- }
-
- render(root, [{tag: component}])
-
- o(called).equals(0)
-
- render(root, [])
-
- o(called).equals(1)
- o(root.childNodes.length).equals(0)
- })
- o("calls onbeforeremove", function() {
- var called = 0
- var component = {
- onbeforeremove: function(vnode) {
- called++
-
- o(vnode.dom).notEquals(undefined)
- o(vnode.dom).equals(root.firstChild)
- o(root.childNodes.length).equals(1)
- },
- view: function() {
- return {tag: "div", attrs: {id: "a"}, text: "b"}
- }
- }
-
- render(root, [{tag: component}])
-
- o(called).equals(0)
-
- render(root, [])
-
- o(called).equals(1)
- o(root.childNodes.length).equals(0)
- })
- o("calls onbeforeremove when returning fragment", function() {
- var called = 0
- var component = {
- onbeforeremove: function(vnode) {
- called++
-
- o(vnode.dom).notEquals(undefined)
- o(vnode.dom).equals(root.firstChild)
- o(root.childNodes.length).equals(1)
- },
- view: function() {
- return [{tag: "div", attrs: {id: "a"}, text: "b"}]
- }
- }
-
- render(root, [{tag: component}])
-
- o(called).equals(0)
-
- render(root, [])
-
- o(called).equals(1)
- o(root.childNodes.length).equals(0)
- })
- o("does not recycle when there's an onupdate", function() {
- var component = {
- onupdate: function() {},
- view: function() {
- return {tag: "div"}
- }
- }
- var update = o.spy()
- var vnode = {tag: component, key: 1}
- var updated = {tag: component, key: 1}
-
- render(root, [vnode])
- render(root, [])
- render(root, [updated])
-
- o(vnode.dom).notEquals(updated.dom)
- })
- })
- o.spec("state", function() {
- o("copies state", function() {
- var called = 0
- var data = {a: 1}
- var component = {
- data: data,
- oninit: init,
- view: function() {
- return ""
- }
- }
-
- render(root, [{tag: component}])
-
- function init(vnode) {
- o(vnode.state.data).deepEquals(data)
- o(vnode.state.data).equals(data)
-
- //inherits state via prototype
- component.x = 1
- o(vnode.state.x).equals(1)
- }
- })
- o("state copy is shallow", function() {
- var called = 0
- var body = {a: 1}
- var data = [body]
- var component = {
- data: data,
- oninit: init,
- view: function() {
- return ""
- }
- }
-
- render(root, [{tag: component}])
-
- function init(vnode) {
- o(vnode.state.data).equals(data)
- o(vnode.state.data[0]).equals(body)
- }
- })
- })
- o.spec("Alternative ways to specify componenents", function() {
- o("Classes can be used as components", function() {
- function MyComponent(vnode){
- o(vnode.state).equals(null)
- }
- var proto = MyComponent.prototype
-
- var context
-
- proto.oninit = o.spy(function(vnode) {
- o(this).equals(vnode.state)
- context = this
- })
- proto.oncreate = o.spy()
- proto.onbeforeupdate = o.spy()
- proto.onupdate = o.spy()
- proto.onbeforeremove = o.spy()
- proto.onremove = o.spy()
- proto.view = o.spy(function() {
- return ""
- })
-
- render(root, [{tag: MyComponent}])
-
- o(context instanceof MyComponent).equals(true)
-
- o(proto.view.callCount).equals(1)
- o(proto.oncreate.callCount).equals(1)
- o(proto.onbeforeupdate.callCount).equals(0)
- o(proto.onupdate.callCount).equals(0)
- o(proto.onbeforeremove.callCount).equals(0)
- o(proto.onremove.callCount).equals(0)
-
- render(root, [{tag: MyComponent}])
-
- o(proto.view.callCount).equals(2)
- o(proto.oncreate.callCount).equals(1)
- o(proto.onbeforeupdate.callCount).equals(1)
- o(proto.onupdate.callCount).equals(1)
- o(proto.onbeforeremove.callCount).equals(0)
- o(proto.onremove.callCount).equals(0)
-
- render(root, [])
-
- o(proto.view.callCount).equals(2)
- o(proto.oncreate.callCount).equals(1)
- o(proto.onbeforeupdate.callCount).equals(1)
- o(proto.onupdate.callCount).equals(1)
- o(proto.onbeforeremove.callCount).equals(1)
- o(proto.onremove.callCount).equals(1)
-
- o(proto.oninit.this).equals(context)
- o(proto.view.this).equals(context)
- o(proto.oncreate.this).equals(context)
- o(proto.onbeforeupdate.this).equals(context)
- o(proto.onupdate.this).equals(context)
- o(proto.onbeforeremove.this).equals(context)
- o(proto.onremove.this).equals(context)
-
- o(proto.oninit.args.length).equals(1)
- o(proto.view.args.length).equals(1)
- o(proto.oncreate.args.length).equals(1)
- o(proto.onbeforeupdate.args.length).equals(2)
- o(proto.onupdate.args.length).equals(1)
- o(proto.onbeforeremove.args.length).equals(1)
- o(proto.onremove.args.length).equals(1)
- })
- o("Factory functions can be used as components", function() {
- var state, context
- function component(vnode) {
- o(vnode.state).equals(null)
-
- return state = {
- oninit: o.spy(function(vnode) {
- o(this).equals(vnode.state)
- context = this
- }),
- oncreate: o.spy(),
- onbeforeupdate: o.spy(),
- onupdate: o.spy(),
- onbeforeremove: o.spy(),
- onremove: o.spy(),
- view: o.spy(function() {
- return ""
+ components.forEach(function(cmp){
+ o.spec(cmp.kind, function(){
+ var createComponent = cmp.create
+
+ o.spec("basics", function() {
+ o("works", function() {
+ var component = createComponent({
+ view: function() {
+ return {tag: "div", attrs: {id: "a"}, text: "b"}
+ }
})
+ var node = {tag: component}
+
+ render(root, [node])
+
+ o(root.firstChild.nodeName).equals("DIV")
+ o(root.firstChild.attributes["id"].nodeValue).equals("a")
+ o(root.firstChild.firstChild.nodeValue).equals("b")
+ })
+ o("receives arguments", function() {
+ var component = createComponent({
+ view: function(vnode) {
+ return {tag: "div", attrs: vnode.attrs, text: vnode.text}
+ }
+ })
+ var node = {tag: component, attrs: {id: "a"}, text: "b"}
+
+ render(root, [node])
+
+ o(root.firstChild.nodeName).equals("DIV")
+ o(root.firstChild.attributes["id"].nodeValue).equals("a")
+ o(root.firstChild.firstChild.nodeValue).equals("b")
+ })
+ o("updates", function() {
+ var component = createComponent({
+ view: function(vnode) {
+ return {tag: "div", attrs: vnode.attrs, text: vnode.text}
+ }
+ })
+ render(root, [{tag: component, attrs: {id: "a"}, text: "b"}])
+ render(root, [{tag: component, attrs: {id: "c"}, text: "d"}])
+
+ o(root.firstChild.nodeName).equals("DIV")
+ o(root.firstChild.attributes["id"].nodeValue).equals("c")
+ o(root.firstChild.firstChild.nodeValue).equals("d")
+ })
+ o("updates root from null", function() {
+ var visible = false
+ var component = createComponent({
+ view: function(vnode) {
+ return visible ? {tag: "div"} : null
+ }
+ })
+ render(root, [{tag: component}])
+ visible = true
+ render(root, [{tag: component}])
+
+ o(root.firstChild.nodeName).equals("DIV")
+ })
+ o("updates root from primitive", function() {
+ var visible = false
+ var component = createComponent({
+ view: function(vnode) {
+ return visible ? {tag: "div"} : false
+ }
+ })
+ render(root, [{tag: component}])
+ visible = true
+ render(root, [{tag: component}])
+
+ o(root.firstChild.nodeName).equals("DIV")
+ })
+ o("updates root to null", function() {
+ var visible = true
+ var component = createComponent({
+ view: function(vnode) {
+ return visible ? {tag: "div"} : null
+ }
+ })
+ render(root, [{tag: component}])
+ visible = false
+ render(root, [{tag: component}])
+
+ o(root.childNodes.length).equals(0)
+ })
+ o("updates root to primitive", function() {
+ var visible = true
+ var component = createComponent({
+ view: function(vnode) {
+ return visible ? {tag: "div"} : false
+ }
+ })
+ render(root, [{tag: component}])
+ visible = false
+ render(root, [{tag: component}])
+
+ o(root.firstChild.nodeValue).equals("")
+ })
+ o("updates root from null to null", function() {
+ var component = createComponent({
+ view: function(vnode) {
+ return null
+ }
+ })
+ render(root, [{tag: component}])
+ render(root, [{tag: component}])
+
+ o(root.childNodes.length).equals(0)
+ })
+ o("removes", function() {
+ var component = createComponent({
+ view: function(vnode) {
+ return {tag: "div"}
+ }
+ })
+ var div = {tag: "div", key: 2}
+ render(root, [{tag: component, key: 1}, div])
+ render(root, [{tag: "div", key: 2}])
+
+ o(root.childNodes.length).equals(1)
+ o(root.firstChild).equals(div.dom)
+ })
+ o("svg works when creating across component boundary", function() {
+ var component = createComponent({
+ view: function(vnode) {
+ return {tag: "g"}
+ }
+ })
+ render(root, [{tag: "svg", children: [{tag: component}]}])
+
+ o(root.firstChild.firstChild.namespaceURI).equals("http://www.w3.org/2000/svg")
+ })
+ o("svg works when updating across component boundary", function() {
+ var component = createComponent({
+ view: function(vnode) {
+ return {tag: "g"}
+ }
+ })
+ render(root, [{tag: "svg", children: [{tag: component}]}])
+ render(root, [{tag: "svg", children: [{tag: component}]}])
+
+ o(root.firstChild.firstChild.namespaceURI).equals("http://www.w3.org/2000/svg")
+ })
+ })
+ o.spec("return value", function() {
+ o("can return fragments", function() {
+ var component = createComponent({
+ view: function(vnode) {
+ return [
+ {tag: "label"},
+ {tag: "input"},
+ ]
+ }
+ })
+ render(root, [{tag: component}])
+
+ o(root.childNodes.length).equals(2)
+ o(root.childNodes[0].nodeName).equals("LABEL")
+ o(root.childNodes[1].nodeName).equals("INPUT")
+ })
+ o("can return string", function() {
+ var component = createComponent({
+ view: function(vnode) {
+ return "a"
+ }
+ })
+ render(root, [{tag: component}])
+
+ o(root.firstChild.nodeType).equals(3)
+ o(root.firstChild.nodeValue).equals("a")
+ })
+ o("can return falsy string", function() {
+ var component = createComponent({
+ view: function(vnode) {
+ return ""
+ }
+ })
+ render(root, [{tag: component}])
+
+ o(root.firstChild.nodeType).equals(3)
+ o(root.firstChild.nodeValue).equals("")
+ })
+ o("can return number", function() {
+ var component = createComponent({
+ view: function(vnode) {
+ return 1
+ }
+ })
+ render(root, [{tag: component}])
+
+ o(root.firstChild.nodeType).equals(3)
+ o(root.firstChild.nodeValue).equals("1")
+ })
+ o("can return falsy number", function() {
+ var component = createComponent({
+ view: function(vnode) {
+ return 0
+ }
+ })
+ render(root, [{tag: component}])
+
+ o(root.firstChild.nodeType).equals(3)
+ o(root.firstChild.nodeValue).equals("0")
+ })
+ o("can return boolean", function() {
+ var component = createComponent({
+ view: function(vnode) {
+ return true
+ }
+ })
+ render(root, [{tag: component}])
+
+ o(root.firstChild.nodeType).equals(3)
+ o(root.firstChild.nodeValue).equals("true")
+ })
+ o("can return falsy boolean", function() {
+ var component = createComponent({
+ view: function(vnode) {
+ return false
+ }
+ })
+ render(root, [{tag: component}])
+
+ o(root.firstChild.nodeType).equals(3)
+ o(root.firstChild.nodeValue).equals("")
+ })
+ o("can return null", function() {
+ var component = createComponent({
+ view: function(vnode) {
+ return null
+ }
+ })
+ render(root, [{tag: component}])
+
+ o(root.childNodes.length).equals(0)
+ })
+ o("can return undefined", function() {
+ var component = createComponent({
+ view: function(vnode) {
+ return undefined
+ }
+ })
+ render(root, [{tag: component}])
+
+ o(root.childNodes.length).equals(0)
+ })
+ o("throws a custom error if it returns itself", function() {
+ // A view that returns its vnode would otherwise trigger an infinite loop
+ var component = createComponent({
+ view: function(vnode) {
+ return vnode
+ }
+ })
+ try {
+ render(root, [{tag: component}])
+ }
+ catch (e) {
+ o(e instanceof Error).equals(true)
+ // Call stack exception is a RangeError
+ o(e instanceof RangeError).equals(false)
+ }
+ })
+ o("can update when returning fragments", function() {
+ var component = createComponent({
+ view: function(vnode) {
+ return [
+ {tag: "label"},
+ {tag: "input"},
+ ]
+ }
+ })
+ render(root, [{tag: component}])
+ render(root, [{tag: component}])
+
+ o(root.childNodes.length).equals(2)
+ o(root.childNodes[0].nodeName).equals("LABEL")
+ o(root.childNodes[1].nodeName).equals("INPUT")
+ })
+ o("can update when returning primitive", function() {
+ var component = createComponent({
+ view: function(vnode) {
+ return "a"
+ }
+ })
+ render(root, [{tag: component}])
+ render(root, [{tag: component}])
+
+ o(root.firstChild.nodeType).equals(3)
+ o(root.firstChild.nodeValue).equals("a")
+ })
+ o("can update when returning null", function() {
+ var component = createComponent({
+ view: function(vnode) {
+ return null
+ }
+ })
+ render(root, [{tag: component}])
+ render(root, [{tag: component}])
+
+ o(root.childNodes.length).equals(0)
+ })
+ o("can remove when returning fragments", function() {
+ var component = createComponent({
+ view: function(vnode) {
+ return [
+ {tag: "label"},
+ {tag: "input"},
+ ]
+ }
+ })
+ var div = {tag: "div", key: 2}
+ render(root, [{tag: component, key: 1}, div])
+
+ render(root, [{tag: "div", key: 2}])
+
+ o(root.childNodes.length).equals(1)
+ o(root.firstChild).equals(div.dom)
+ })
+ o("can remove when returning primitive", function() {
+ var component = createComponent({
+ view: function(vnode) {
+ return "a"
+ }
+ })
+ var div = {tag: "div", key: 2}
+ render(root, [{tag: component, key: 1}, div])
+
+ render(root, [{tag: "div", key: 2}])
+
+ o(root.childNodes.length).equals(1)
+ o(root.firstChild).equals(div.dom)
+ })
+ })
+ o.spec("lifecycle", function() {
+ o("calls oninit", function() {
+ var called = 0
+ var component = createComponent({
+ oninit: function(vnode) {
+ called++
+
+ o(vnode.tag).equals(component)
+ o(vnode.dom).equals(undefined)
+ o(root.childNodes.length).equals(0)
+ },
+ view: function() {
+ return {tag: "div", attrs: {id: "a"}, text: "b"}
+ }
+ })
+ var node = {tag: component}
+
+ render(root, [node])
+
+ o(called).equals(1)
+ o(root.firstChild.nodeName).equals("DIV")
+ o(root.firstChild.attributes["id"].nodeValue).equals("a")
+ o(root.firstChild.firstChild.nodeValue).equals("b")
+ })
+ o("calls oninit when returning fragment", function() {
+ var called = 0
+ var component = createComponent({
+ oninit: function(vnode) {
+ called++
+
+ o(vnode.tag).equals(component)
+ o(vnode.dom).equals(undefined)
+ o(root.childNodes.length).equals(0)
+ },
+ view: function() {
+ return [{tag: "div", attrs: {id: "a"}, text: "b"}]
+ }
+ })
+ var node = {tag: component}
+
+ render(root, [node])
+
+ o(called).equals(1)
+ o(root.firstChild.nodeName).equals("DIV")
+ o(root.firstChild.attributes["id"].nodeValue).equals("a")
+ o(root.firstChild.firstChild.nodeValue).equals("b")
+ })
+ o("calls oninit before view", function() {
+ var viewCalled = false
+
+ render(root, createComponent({
+ tag: {
+ view: function() {
+ viewCalled = true
+ return [{tag: "div", attrs: {id: "a"}, text: "b"}]
+ },
+ oninit: function(vnode) {
+ o(viewCalled).equals(false)
+ },
+ }
+ }))
+ })
+ o("does not calls oninit on redraw", function() {
+ var init = o.spy()
+ var component = createComponent({
+ view: function() {
+ return {tag: "div", attrs: {id: "a"}, text: "b"}
+ },
+ oninit: init,
+ })
+
+ function view() {
+ return {tag: component}
+ }
+
+ render(root, view())
+ render(root, view())
+
+ o(init.callCount).equals(1)
+ })
+ o("calls oncreate", function() {
+ var called = 0
+ var component = createComponent({
+ oncreate: function(vnode) {
+ called++
+
+ o(vnode.dom).notEquals(undefined)
+ o(vnode.dom).equals(root.firstChild)
+ o(root.childNodes.length).equals(1)
+ },
+ view: function() {
+ return {tag: "div", attrs: {id: "a"}, text: "b"}
+ }
+ })
+ var node = {tag: component}
+
+ render(root, [node])
+
+ o(called).equals(1)
+ o(root.firstChild.nodeName).equals("DIV")
+ o(root.firstChild.attributes["id"].nodeValue).equals("a")
+ o(root.firstChild.firstChild.nodeValue).equals("b")
+ })
+ o("does not calls oncreate on redraw", function() {
+ var create = o.spy()
+ var component = createComponent({
+ view: function() {
+ return {tag: "div", attrs: {id: "a"}, text: "b"}
+ },
+ oncreate: create,
+ })
+
+ function view() {
+ return {tag: component}
+ }
+
+ render(root, view())
+ render(root, view())
+
+ o(create.callCount).equals(1)
+ })
+ o("calls oncreate when returning fragment", function() {
+ var called = 0
+ var component = createComponent({
+ oncreate: function(vnode) {
+ called++
+
+ o(vnode.dom).notEquals(undefined)
+ o(vnode.dom).equals(root.firstChild)
+ o(root.childNodes.length).equals(1)
+ },
+ view: function() {
+ return [{tag: "div", attrs: {id: "a"}, text: "b"}]
+ }
+ })
+ var node = {tag: component}
+
+ render(root, [node])
+
+ o(called).equals(1)
+ o(root.firstChild.nodeName).equals("DIV")
+ o(root.firstChild.attributes["id"].nodeValue).equals("a")
+ o(root.firstChild.firstChild.nodeValue).equals("b")
+ })
+ o("calls onupdate", function() {
+ var called = 0
+ var component = createComponent({
+ onupdate: function(vnode) {
+ called++
+
+ o(vnode.dom).notEquals(undefined)
+ o(vnode.dom).equals(root.firstChild)
+ o(root.childNodes.length).equals(1)
+ },
+ view: function() {
+ return {tag: "div", attrs: {id: "a"}, text: "b"}
+ }
+ })
+
+ render(root, [{tag: component}])
+
+ o(called).equals(0)
+
+ render(root, [{tag: component}])
+
+ o(called).equals(1)
+ o(root.firstChild.nodeName).equals("DIV")
+ o(root.firstChild.attributes["id"].nodeValue).equals("a")
+ o(root.firstChild.firstChild.nodeValue).equals("b")
+ })
+ o("calls onupdate when returning fragment", function() {
+ var called = 0
+ var component = createComponent({
+ onupdate: function(vnode) {
+ called++
+
+ o(vnode.dom).notEquals(undefined)
+ o(vnode.dom).equals(root.firstChild)
+ o(root.childNodes.length).equals(1)
+ },
+ view: function() {
+ return [{tag: "div", attrs: {id: "a"}, text: "b"}]
+ }
+ })
+
+ render(root, [{tag: component}])
+
+ o(called).equals(0)
+
+ render(root, [{tag: component}])
+
+ o(called).equals(1)
+ o(root.firstChild.nodeName).equals("DIV")
+ o(root.firstChild.attributes["id"].nodeValue).equals("a")
+ o(root.firstChild.firstChild.nodeValue).equals("b")
+ })
+ o("calls onremove", function() {
+ var called = 0
+ var component = createComponent({
+ onremove: function(vnode) {
+ called++
+
+ o(vnode.dom).notEquals(undefined)
+ o(vnode.dom).equals(root.firstChild)
+ o(root.childNodes.length).equals(1)
+ },
+ view: function() {
+ return {tag: "div", attrs: {id: "a"}, text: "b"}
+ }
+ })
+
+ render(root, [{tag: component}])
+
+ o(called).equals(0)
+
+ render(root, [])
+
+ o(called).equals(1)
+ o(root.childNodes.length).equals(0)
+ })
+ o("calls onremove when returning fragment", function() {
+ var called = 0
+ var component = createComponent({
+ onremove: function(vnode) {
+ called++
+
+ o(vnode.dom).notEquals(undefined)
+ o(vnode.dom).equals(root.firstChild)
+ o(root.childNodes.length).equals(1)
+ },
+ view: function() {
+ return [{tag: "div", attrs: {id: "a"}, text: "b"}]
+ }
+ })
+
+ render(root, [{tag: component}])
+
+ o(called).equals(0)
+
+ render(root, [])
+
+ o(called).equals(1)
+ o(root.childNodes.length).equals(0)
+ })
+ o("calls onbeforeremove", function() {
+ var called = 0
+ var component = createComponent({
+ onbeforeremove: function(vnode) {
+ called++
+
+ o(vnode.dom).notEquals(undefined)
+ o(vnode.dom).equals(root.firstChild)
+ o(root.childNodes.length).equals(1)
+ },
+ view: function() {
+ return {tag: "div", attrs: {id: "a"}, text: "b"}
+ }
+ })
+
+ render(root, [{tag: component}])
+
+ o(called).equals(0)
+
+ render(root, [])
+
+ o(called).equals(1)
+ o(root.childNodes.length).equals(0)
+ })
+ o("calls onbeforeremove when returning fragment", function() {
+ var called = 0
+ var component = createComponent({
+ onbeforeremove: function(vnode) {
+ called++
+
+ o(vnode.dom).notEquals(undefined)
+ o(vnode.dom).equals(root.firstChild)
+ o(root.childNodes.length).equals(1)
+ },
+ view: function() {
+ return [{tag: "div", attrs: {id: "a"}, text: "b"}]
+ }
+ })
+
+ render(root, [{tag: component}])
+
+ o(called).equals(0)
+
+ render(root, [])
+
+ o(called).equals(1)
+ o(root.childNodes.length).equals(0)
+ })
+ o("does not recycle when there's an onupdate", function() {
+ var component = createComponent({
+ onupdate: function() {},
+ view: function() {
+ return {tag: "div"}
+ }
+ })
+ var update = o.spy()
+ var vnode = {tag: component, key: 1}
+ var updated = {tag: component, key: 1}
+
+ render(root, [vnode])
+ render(root, [])
+ render(root, [updated])
+
+ o(vnode.dom).notEquals(updated.dom)
+ })
+ o("lifecycle timing megatest (for a single component)", function() {
+ var methods = {
+ view: o.spy(function() {
+ return ""
+ })
+ }
+ var attrs = {}
+ var hooks = [
+ "oninit", "oncreate", "onbeforeupdate",
+ "onupdate", "onbeforeremove", "onremove"
+ ]
+ hooks.forEach(function(hook) {
+ // the `attrs` hooks are called before the component ones
+ attrs[hook] = o.spy(function() {
+ o(attrs[hook].callCount).equals(methods[hook].callCount + 1)
+ })
+ methods[hook] = o.spy(function() {
+ o(attrs[hook].callCount).equals(methods[hook].callCount)
+ })
+ })
+
+ var component = createComponent(methods)
+
+ o(methods.view.callCount).equals(0)
+ o(methods.oninit.callCount).equals(0)
+ o(methods.oncreate.callCount).equals(0)
+ o(methods.onbeforeupdate.callCount).equals(0)
+ o(methods.onupdate.callCount).equals(0)
+ o(methods.onbeforeremove.callCount).equals(0)
+ o(methods.onremove.callCount).equals(0)
+
+ hooks.forEach(function(hook) {
+ o(attrs[hook].callCount).equals(methods[hook].callCount)(hook)
+ })
+
+ render(root, [{tag: component, attrs: attrs}])
+
+ o(methods.view.callCount).equals(1)
+ o(methods.oninit.callCount).equals(1)
+ o(methods.oncreate.callCount).equals(1)
+ o(methods.onbeforeupdate.callCount).equals(0)
+ o(methods.onupdate.callCount).equals(0)
+ o(methods.onbeforeremove.callCount).equals(0)
+ o(methods.onremove.callCount).equals(0)
+
+ hooks.forEach(function(hook) {
+ o(attrs[hook].callCount).equals(methods[hook].callCount)(hook)
+ })
+
+ render(root, [{tag: component, attrs: attrs}])
+
+ o(methods.view.callCount).equals(2)
+ o(methods.oninit.callCount).equals(1)
+ o(methods.oncreate.callCount).equals(1)
+ o(methods.onbeforeupdate.callCount).equals(1)
+ o(methods.onupdate.callCount).equals(1)
+ o(methods.onbeforeremove.callCount).equals(0)
+ o(methods.onremove.callCount).equals(0)
+
+ hooks.forEach(function(hook) {
+ o(attrs[hook].callCount).equals(methods[hook].callCount)(hook)
+ })
+
+ render(root, [])
+
+ o(methods.view.callCount).equals(2)
+ o(methods.oninit.callCount).equals(1)
+ o(methods.oncreate.callCount).equals(1)
+ o(methods.onbeforeupdate.callCount).equals(1)
+ o(methods.onupdate.callCount).equals(1)
+ o(methods.onbeforeremove.callCount).equals(1)
+ o(methods.onremove.callCount).equals(1)
+
+ hooks.forEach(function(hook) {
+ o(attrs[hook].callCount).equals(methods[hook].callCount)(hook)
+ })
+ })
+ o("hook state and arguments validation", function(){
+ var methods = {
+ view: o.spy(function(vnode) {
+ o(this).equals(vnode.state)
+ return ""
+ })
+ }
+ var attrs = {}
+ var hooks = [
+ "oninit", "oncreate", "onbeforeupdate",
+ "onupdate", "onbeforeremove", "onremove"
+ ]
+ hooks.forEach(function(hook) {
+ attrs[hook] = o.spy(function(vnode){
+ o(this).equals(vnode.state)(hook)
+ })
+ methods[hook] = o.spy(function(vnode){
+ o(this).equals(vnode.state)
+ })
+ })
+
+ var component = createComponent(methods)
+
+ render(root, [{tag: component, attrs: attrs}])
+ render(root, [{tag: component, attrs: attrs}])
+ render(root, [])
+
+ hooks.forEach(function(hook) {
+ o(attrs[hook].this).equals(methods.view.this)(hook)
+ o(methods[hook].this).equals(methods.view.this)(hook)
+ })
+
+ o(methods.view.args.length).equals(1)
+ o(methods.oninit.args.length).equals(1)
+ o(methods.oncreate.args.length).equals(1)
+ o(methods.onbeforeupdate.args.length).equals(2)
+ o(methods.onupdate.args.length).equals(1)
+ o(methods.onbeforeremove.args.length).equals(1)
+ o(methods.onremove.args.length).equals(1)
+
+ hooks.forEach(function(hook) {
+ o(methods[hook].args.length).equals(attrs[hook].args.length)(hook)
+ })
+ })
+ o("recycled components get a fresh state", function() {
+ var step = 0
+ var firstState
+ var view = o.spy(function(vnode) {
+ if (step === 0) {
+ firstState = vnode.state
+ } else {
+ o(vnode.state).notEquals(firstState)
+ }
+ return {tag: 'div'}
+ })
+ var component = createComponent({view: view})
+
+ render(root, [{tag: 'div', children: [{tag: component, key: 1}]}])
+ var child = root.firstChild.firstChild
+ render(root, [])
+ step = 1
+ render(root, [{tag: 'div', children: [{tag: component, key: 1}]}])
+
+ o(child).equals(root.firstChild.firstChild)
+ o(view.callCount).equals(2)
+ })
+ })
+ o.spec("state", function() {
+ o("initializes state", function() {
+ var called = 0
+ var data = {a: 1}
+ var component = createComponent(createComponent({
+ data: data,
+ oninit: init,
+ view: function() {
+ return ""
+ }
+ }))
+
+ render(root, [{tag: component}])
+
+ function init(vnode) {
+ o(vnode.state.data).equals(data)
+ }
+ })
+ o('state proxies to the component object/prototype', function() {
+ var called = 0
+ var body = {a: 1}
+ var data = [body]
+ var component = createComponent(createComponent({
+ data: data,
+ oninit: init,
+ view: function() {
+ return ""
+ }
+ }))
+
+ render(root, [{tag: component}])
+
+ function init(vnode) {
+ o(vnode.state.data).equals(data)
+ o(vnode.state.data[0]).equals(body)
+ }
+ })
+ })
+ })
+ })
+ o.spec("Tests specific to certain component kinds", function() {
+ o.spec("state", function() {
+ o("POJO", function() {
+ var called = 0
+ var data = {}
+ var component = {
+ data: data,
+ oninit: init,
+ view: function() {
+ return ""
+ }
}
- }
- render(root, [{tag: component}])
+ render(root, [{tag: component}])
- o(state).equals(context)
+ function init(vnode) {
+ o(vnode.state.data).equals(data)
- o(state.oninit.callCount).equals(1)
- o(state.view.callCount).equals(1)
- o(state.oncreate.callCount).equals(1)
- o(state.onbeforeupdate.callCount).equals(0)
- o(state.onupdate.callCount).equals(0)
- o(state.onbeforeremove.callCount).equals(0)
- o(state.onremove.callCount).equals(0)
+ //inherits state via prototype
+ component.x = 1
+ o(vnode.state.x).equals(1)
+ }
+ })
+ o("Constructible", function() {
+ var oninit = o.spy()
+ var component = o.spy(function(vnode){
+ o(vnode.state).equals(null)
+ o(oninit.callCount).equals(0)
+ })
+ var view = o.spy(function(){
+ o(this instanceof component).equals(true)
+ return ""
+ })
+ component.prototype.view = view
+ component.prototype.oninit = oninit
- render(root, [{tag: component}])
+ var context
- o(state.oninit.callCount).equals(1)
- o(state.view.callCount).equals(2)
- o(state.oncreate.callCount).equals(1)
- o(state.onbeforeupdate.callCount).equals(1)
- o(state.onupdate.callCount).equals(1)
- o(state.onbeforeremove.callCount).equals(0)
- o(state.onremove.callCount).equals(0)
+ render(root, [{tag: component, attrs: {oninit: oninit}}])
+ render(root, [{tag: component, attrs: {oninit: oninit}}])
+ render(root, [])
- render(root, [])
+ o(component.callCount).equals(1)
+ o(oninit.callCount).equals(2)
+ o(view.callCount).equals(2)
+ })
+ o("Closure", function() {
+ var state
+ var oninit = o.spy()
+ var view = o.spy(function() {
+ o(this).equals(state)
+ return ""
+ })
+ var component = o.spy(function(vnode) {
+ o(vnode.state).equals(null)
+ o(oninit.callCount).equals(0)
+ return state = {
+ view: view
+ }
+ })
- o(state.oninit.callCount).equals(1)
- o(state.view.callCount).equals(2)
- o(state.oncreate.callCount).equals(1)
- o(state.onbeforeupdate.callCount).equals(1)
- o(state.onupdate.callCount).equals(1)
- o(state.onbeforeremove.callCount).equals(1)
- o(state.onremove.callCount).equals(1)
+ render(root, [{tag: component, attrs: {oninit: oninit}}])
+ render(root, [{tag: component, attrs: {oninit: oninit}}])
+ render(root, [])
- o(state.oninit.this).equals(state)
- o(state.view.this).equals(state)
- o(state.oncreate.this).equals(state)
- o(state.onbeforeupdate.this).equals(state)
- o(state.onupdate.this).equals(state)
- o(state.onbeforeremove.this).equals(state)
- o(state.onremove.this).equals(state)
-
- o(state.oninit.args.length).equals(1)
- o(state.view.args.length).equals(1)
- o(state.oncreate.args.length).equals(1)
- o(state.onbeforeupdate.args.length).equals(2)
- o(state.onupdate.args.length).equals(1)
- o(state.onbeforeremove.args.length).equals(1)
- o(state.onremove.args.length).equals(1)
+ o(component.callCount).equals(1)
+ o(oninit.callCount).equals(1)
+ o(view.callCount).equals(2)
+ })
})
})
})
diff --git a/render/tests/test-onbeforeremove.js b/render/tests/test-onbeforeremove.js
index c9af4894..4395f90a 100644
--- a/render/tests/test-onbeforeremove.js
+++ b/render/tests/test-onbeforeremove.js
@@ -2,6 +2,7 @@
var o = require("../../ospec/ospec")
var callAsync = require("../../test-utils/callAsync")
+var components = require("../../test-utils/components")
var domMock = require("../../test-utils/domMock")
var vdom = require("../../render/render")
var Promise = require("../../promise/promise")
@@ -169,39 +170,44 @@ o.spec("onbeforeremove", function() {
done()
})
})
- o("finalizes the remove phase asynchronously when promise is returned synchronously from both attrs- and tag.onbeforeremove", function(done) {
- var onremove = o.spy()
- var onbeforeremove = function(){return Promise.resolve()}
- var component = {
- onbeforeremove: onbeforeremove,
- onremove: onremove,
- view: function() {},
- }
- render(root, [{tag: component, attrs: {onbeforeremove: onbeforeremove, onremove: onremove}}])
- render(root, [])
- callAsync(function() {
- o(onremove.callCount).equals(2) // once for `tag`, once for `attrs`
- done()
- })
- })
- o("awaits promise resolution before removing the node", function(done) {
- var view = o.spy()
- var onremove = o.spy()
- var onbeforeremove = function(){return new Promise(function(resolve){callAsync(resolve)})}
- var component = {
- onbeforeremove: onbeforeremove,
- onremove: onremove,
- view: view,
- }
- render(root, [{tag: component}])
- render(root, [])
+ components.forEach(function(cmp){
+ o.spec(cmp.kind, function(){
+ var createComponent = cmp.create
+ o("finalizes the remove phase asynchronously when promise is returned synchronously from both attrs- and tag.onbeforeremove", function(done) {
+ var onremove = o.spy()
+ var onbeforeremove = function(){return Promise.resolve()}
+ var component = createComponent({
+ onbeforeremove: onbeforeremove,
+ onremove: onremove,
+ view: function() {},
+ })
+ render(root, [{tag: component, attrs: {onbeforeremove: onbeforeremove, onremove: onremove}}])
+ render(root, [])
+ callAsync(function() {
+ o(onremove.callCount).equals(2) // once for `tag`, once for `attrs`
+ done()
+ })
+ })
+ o("awaits promise resolution before removing the node", function(done) {
+ var view = o.spy()
+ var onremove = o.spy()
+ var onbeforeremove = function(){return new Promise(function(resolve){callAsync(resolve)})}
+ var component = createComponent({
+ onbeforeremove: onbeforeremove,
+ onremove: onremove,
+ view: view,
+ })
+ render(root, [{tag: component}])
+ render(root, [])
- callAsync(function(){
- o(onremove.callCount).equals(0)
+ callAsync(function(){
+ o(onremove.callCount).equals(0)
- callAsync(function() {
- o(onremove.callCount).equals(1)
- done()
+ callAsync(function() {
+ o(onremove.callCount).equals(1)
+ done()
+ })
+ })
})
})
})
diff --git a/render/tests/test-onbeforeupdate.js b/render/tests/test-onbeforeupdate.js
index 8b326b8c..93643409 100644
--- a/render/tests/test-onbeforeupdate.js
+++ b/render/tests/test-onbeforeupdate.js
@@ -1,6 +1,7 @@
"use strict"
var o = require("../../ospec/ospec")
+var components = require("../../test-utils/components")
var domMock = require("../../test-utils/domMock")
var vdom = require("../../render/render")
@@ -56,86 +57,6 @@ o.spec("onbeforeupdate", function() {
o(root.firstChild.nodeValue).equals("a")
})
- o("prevents update in component", function() {
- var component = {
- onbeforeupdate: function() {return false},
- view: function(vnode) {
- return {tag: "div", children: vnode.children}
- },
- }
- var vnode = {tag: component, children: [{tag: "#", children: "a"}]}
- var updated = {tag: component, children: [{tag: "#", children: "b"}]}
-
- render(root, [vnode])
- render(root, [updated])
-
- o(root.firstChild.firstChild.nodeValue).equals("a")
- })
-
- o("prevents update if returning false in component and false in vnode", function() {
- var component = {
- onbeforeupdate: function() {return false},
- view: function(vnode) {
- return {tag: "div", attrs: {id: vnode.attrs.id}}
- },
- }
- var vnode = {tag: component, attrs: {id: "a", onbeforeupdate: function() {return false}}}
- var updated = {tag: component, attrs: {id: "b", onbeforeupdate: function() {return false}}}
-
- render(root, [vnode])
- render(root, [updated])
-
- o(root.firstChild.attributes["id"].nodeValue).equals("a")
- })
-
- o("does not prevent update if returning true in component and true in vnode", function() {
- var component = {
- onbeforeupdate: function() {return true},
- view: function(vnode) {
- return {tag: "div", attrs: {id: vnode.attrs.id}}
- },
- }
- var vnode = {tag: component, attrs: {id: "a", onbeforeupdate: function() {return true}}}
- var updated = {tag: component, attrs: {id: "b", onbeforeupdate: function() {return true}}}
-
- render(root, [vnode])
- render(root, [updated])
-
- o(root.firstChild.attributes["id"].nodeValue).equals("b")
- })
-
- o("does not prevent update if returning false in component but true in vnode", function() {
- var component = {
- onbeforeupdate: function() {return false},
- view: function(vnode) {
- return {tag: "div", attrs: {id: vnode.attrs.id}}
- },
- }
- var vnode = {tag: component, attrs: {id: "a", onbeforeupdate: function() {return true}}}
- var updated = {tag: component, attrs: {id: "b", onbeforeupdate: function() {return true}}}
-
- render(root, [vnode])
- render(root, [updated])
-
- o(root.firstChild.attributes["id"].nodeValue).equals("b")
- })
-
- o("does not prevent update if returning true in component but false in vnode", function() {
- var component = {
- onbeforeupdate: function() {return true},
- view: function(vnode) {
- return {tag: "div", attrs: {id: vnode.attrs.id}}
- },
- }
- var vnode = {tag: component, attrs: {id: "a", onbeforeupdate: function() {return false}}}
- var updated = {tag: component, attrs: {id: "b", onbeforeupdate: function() {return false}}}
-
- render(root, [vnode])
- render(root, [updated])
-
- o(root.firstChild.attributes["id"].nodeValue).equals("b")
- })
-
o("does not prevent update if returning true", function() {
var onbeforeupdate = function() {return true}
var vnode = {tag: "div", attrs: {id: "a", onbeforeupdate: onbeforeupdate}}
@@ -147,22 +68,6 @@ o.spec("onbeforeupdate", function() {
o(root.firstChild.attributes["id"].nodeValue).equals("b")
})
- o("does not prevent update if returning true from component", function() {
- var component = {
- onbeforeupdate: function() {return true},
- view: function(vnode) {
- return {tag: "div", attrs: vnode.attrs}
- },
- }
- var vnode = {tag: component, attrs: {id: "a"}}
- var updated = {tag: component, attrs: {id: "b"}}
-
- render(root, [vnode])
- render(root, [updated])
-
- o(root.firstChild.attributes["id"].nodeValue).equals("b")
- })
-
o("accepts arguments for comparison", function() {
var count = 0
var vnode = {tag: "div", attrs: {id: "a", onbeforeupdate: onbeforeupdate}}
@@ -184,33 +89,6 @@ o.spec("onbeforeupdate", function() {
o(root.firstChild.attributes["id"].nodeValue).equals("b")
})
- o("accepts arguments for comparison in component", function() {
- var component = {
- onbeforeupdate: onbeforeupdate,
- view: function(vnode) {
- return {tag: "div", attrs: vnode.attrs}
- },
- }
- var count = 0
- var vnode = {tag: component, attrs: {id: "a"}}
- var updated = {tag: component, attrs: {id: "b"}}
-
- render(root, [vnode])
- render(root, [updated])
-
- function onbeforeupdate(vnode, old) {
- count++
-
- o(old.attrs.id).equals("a")
- o(vnode.attrs.id).equals("b")
-
- return old.attrs.id !== vnode.attrs.id
- }
-
- o(count).equals(1)
- o(root.firstChild.attributes["id"].nodeValue).equals("b")
- })
-
o("is not called on creation", function() {
var count = 0
var vnode = {tag: "div", attrs: {id: "a", onbeforeupdate: onbeforeupdate}}
@@ -226,28 +104,6 @@ o.spec("onbeforeupdate", function() {
o(count).equals(0)
})
- o("is not called on component creation", function() {
- var component = {
- onbeforeupdate: onbeforeupdate,
- view: function(vnode) {
- return {tag: "div", attrs: vnode.attrs}
- },
- }
-
- var count = 0
- var vnode = {tag: "div", attrs: {id: "a"}}
- var updated = {tag: "div", attrs: {id: "b"}}
-
- render(root, [vnode])
-
- function onbeforeupdate(vnode, old) {
- count++
- return true
- }
-
- o(count).equals(0)
- })
-
o("is called only once on update", function() {
var count = 0
var vnode = {tag: "div", attrs: {id: "a", onbeforeupdate: onbeforeupdate}}
@@ -264,26 +120,192 @@ o.spec("onbeforeupdate", function() {
o(count).equals(1)
})
- o("is called only once on component update", function() {
- var component = {
- onbeforeupdate: onbeforeupdate,
- view: function(vnode) {
- return {tag: "div", attrs: vnode.attrs}
- },
- }
+ o("doesn't fire on recycled nodes", function() {
+ var onbeforeupdate = o.spy()
+ var vnodes = [{tag: "div", key: 1}]
+ var temp = []
+ var updated = [{tag: "div", key: 1, attrs: {onbeforeupdate: onbeforeupdate}}]
- var count = 0
- var vnode = {tag: component, attrs: {id: "a"}}
- var updated = {tag: component, attrs: {id: "b"}}
+ render(root, vnodes)
+ render(root, temp)
+ render(root, updated)
- render(root, [vnode])
- render(root, [updated])
-
- function onbeforeupdate(vnode, old) {
- count++
- return true
- }
-
- o(count).equals(1)
+ o(vnodes[0].dom).equals(updated[0].dom)
+ o(updated[0].dom.nodeName).equals("DIV")
+ o(onbeforeupdate.callCount).equals(0)
})
-})
+
+ components.forEach(function(cmp){
+ o.spec(cmp.kind, function(){
+ var createComponent = cmp.create
+
+ o("prevents update in component", function() {
+ var component = createComponent({
+ onbeforeupdate: function() {return false},
+ view: function(vnode) {
+ return {tag: "div", children: vnode.children}
+ },
+ })
+ var vnode = {tag: component, children: [{tag: "#", children: "a"}]}
+ var updated = {tag: component, children: [{tag: "#", children: "b"}]}
+
+ render(root, [vnode])
+ render(root, [updated])
+
+ o(root.firstChild.firstChild.nodeValue).equals("a")
+ })
+
+ o("prevents update if returning false in component and false in vnode", function() {
+ var component = createComponent({
+ onbeforeupdate: function() {return false},
+ view: function(vnode) {
+ return {tag: "div", attrs: {id: vnode.attrs.id}}
+ },
+ })
+ var vnode = {tag: component, attrs: {id: "a", onbeforeupdate: function() {return false}}}
+ var updated = {tag: component, attrs: {id: "b", onbeforeupdate: function() {return false}}}
+
+ render(root, [vnode])
+ render(root, [updated])
+
+ o(root.firstChild.attributes["id"].nodeValue).equals("a")
+ })
+
+ o("does not prevent update if returning true in component and true in vnode", function() {
+ var component = createComponent({
+ onbeforeupdate: function() {return true},
+ view: function(vnode) {
+ return {tag: "div", attrs: {id: vnode.attrs.id}}
+ },
+ })
+ var vnode = {tag: component, attrs: {id: "a", onbeforeupdate: function() {return true}}}
+ var updated = {tag: component, attrs: {id: "b", onbeforeupdate: function() {return true}}}
+
+ render(root, [vnode])
+ render(root, [updated])
+
+ o(root.firstChild.attributes["id"].nodeValue).equals("b")
+ })
+
+ o("does not prevent update if returning false in component but true in vnode", function() {
+ var component = createComponent({
+ onbeforeupdate: function() {return false},
+ view: function(vnode) {
+ return {tag: "div", attrs: {id: vnode.attrs.id}}
+ },
+ })
+ var vnode = {tag: component, attrs: {id: "a", onbeforeupdate: function() {return true}}}
+ var updated = {tag: component, attrs: {id: "b", onbeforeupdate: function() {return true}}}
+
+ render(root, [vnode])
+ render(root, [updated])
+
+ o(root.firstChild.attributes["id"].nodeValue).equals("b")
+ })
+
+ o("does not prevent update if returning true in component but false in vnode", function() {
+ var component = createComponent({
+ onbeforeupdate: function() {return true},
+ view: function(vnode) {
+ return {tag: "div", attrs: {id: vnode.attrs.id}}
+ },
+ })
+ var vnode = {tag: component, attrs: {id: "a", onbeforeupdate: function() {return false}}}
+ var updated = {tag: component, attrs: {id: "b", onbeforeupdate: function() {return false}}}
+
+ render(root, [vnode])
+ render(root, [updated])
+
+ o(root.firstChild.attributes["id"].nodeValue).equals("b")
+ })
+
+ o("does not prevent update if returning true from component", function() {
+ var component = createComponent({
+ onbeforeupdate: function() {return true},
+ view: function(vnode) {
+ return {tag: "div", attrs: vnode.attrs}
+ },
+ })
+ var vnode = {tag: component, attrs: {id: "a"}}
+ var updated = {tag: component, attrs: {id: "b"}}
+
+ render(root, [vnode])
+ render(root, [updated])
+
+ o(root.firstChild.attributes["id"].nodeValue).equals("b")
+ })
+
+ o("accepts arguments for comparison in component", function() {
+ var component = createComponent({
+ onbeforeupdate: onbeforeupdate,
+ view: function(vnode) {
+ return {tag: "div", attrs: vnode.attrs}
+ },
+ })
+ var count = 0
+ var vnode = {tag: component, attrs: {id: "a"}}
+ var updated = {tag: component, attrs: {id: "b"}}
+
+ render(root, [vnode])
+ render(root, [updated])
+
+ function onbeforeupdate(vnode, old) {
+ count++
+
+ o(old.attrs.id).equals("a")
+ o(vnode.attrs.id).equals("b")
+
+ return old.attrs.id !== vnode.attrs.id
+ }
+
+ o(count).equals(1)
+ o(root.firstChild.attributes["id"].nodeValue).equals("b")
+ })
+
+ o("is not called on component creation", function() {
+ var component = createComponent({
+ onbeforeupdate: onbeforeupdate,
+ view: function(vnode) {
+ return {tag: "div", attrs: vnode.attrs}
+ },
+ })
+
+ var count = 0
+ var vnode = {tag: "div", attrs: {id: "a"}}
+ var updated = {tag: "div", attrs: {id: "b"}}
+
+ render(root, [vnode])
+
+ function onbeforeupdate(vnode, old) {
+ count++
+ return true
+ }
+
+ o(count).equals(0)
+ })
+
+ o("is called only once on component update", function() {
+ var component = createComponent({
+ onbeforeupdate: onbeforeupdate,
+ view: function(vnode) {
+ return {tag: "div", attrs: vnode.attrs}
+ },
+ })
+
+ var count = 0
+ var vnode = {tag: component, attrs: {id: "a"}}
+ var updated = {tag: component, attrs: {id: "b"}}
+
+ render(root, [vnode])
+ render(root, [updated])
+
+ function onbeforeupdate(vnode, old) {
+ count++
+ return true
+ }
+
+ o(count).equals(1)
+ })
+ })
+ })
+})
\ No newline at end of file
diff --git a/render/tests/test-onremove.js b/render/tests/test-onremove.js
index d3f423fc..a7f88a6b 100644
--- a/render/tests/test-onremove.js
+++ b/render/tests/test-onremove.js
@@ -1,6 +1,7 @@
"use strict"
var o = require("../../ospec/ospec")
+var components = require("../../test-utils/components")
var domMock = require("../../test-utils/domMock")
var vdom = require("../../render/render")
var m = require("../../render/hyperscript")
@@ -80,39 +81,6 @@ o.spec("onremove", function() {
o(remove.this).equals(vnode.state)
o(remove.args[0]).equals(vnode)
})
- o("calls onremove on nested component", function() {
- var spy = o.spy()
- var comp = {
- view: function() {return m(outer)}
- }
- var outer = {
- view: function() {return m(inner)}
- }
- var inner = {
- onremove: spy,
- view: function() {return m("div")}
- }
- render(root, {tag: comp})
- render(root, null)
-
- o(spy.callCount).equals(1)
- })
- o("calls onremove on nested component child", function() {
- var spy = o.spy()
- var comp = {
- view: function() {return m(outer)}
- }
- var outer = {
- view: function() {return m(inner, m("a", {onremove: spy}))}
- }
- var inner = {
- view: function(vnode) {return m("div", vnode.children)}
- }
- render(root, {tag: comp})
- render(root, null)
-
- o(spy.callCount).equals(1)
- })
o("does not set onremove as an event handler", function() {
var remove = o.spy()
var vnode = {tag: "div", attrs: {onremove: remove}, children: []}
@@ -145,4 +113,43 @@ o.spec("onremove", function() {
o(vnode.dom).notEquals(updated.dom)
})
-})
+ components.forEach(function(cmp){
+ o.spec(cmp.kind, function(){
+ var createComponent = cmp.create
+
+ o("calls onremove on nested component", function() {
+ var spy = o.spy()
+ var comp = createComponent({
+ view: function() {return m(outer)}
+ })
+ var outer = createComponent({
+ view: function() {return m(inner)}
+ })
+ var inner = createComponent({
+ onremove: spy,
+ view: function() {return m("div")}
+ })
+ render(root, {tag: comp})
+ render(root, null)
+
+ o(spy.callCount).equals(1)
+ })
+ o("calls onremove on nested component child", function() {
+ var spy = o.spy()
+ var comp = createComponent({
+ view: function() {return m(outer)}
+ })
+ var outer = createComponent({
+ view: function() {return m(inner, m("a", {onremove: spy}))}
+ })
+ var inner = createComponent({
+ view: function(vnode) {return m("div", vnode.children)}
+ })
+ render(root, {tag: comp})
+ render(root, null)
+
+ o(spy.callCount).equals(1)
+ })
+ })
+ })
+})
\ No newline at end of file
diff --git a/render/tests/test-render.js b/render/tests/test-render.js
index 2f3ebb9a..82e5ddba 100644
--- a/render/tests/test-render.js
+++ b/render/tests/test-render.js
@@ -1,6 +1,7 @@
"use strict"
var o = require("../../ospec/ospec")
+var components = require("../../test-utils/components")
var domMock = require("../../test-utils/domMock")
var vdom = require("../../render/render")
@@ -118,7 +119,7 @@ o.spec("render", function() {
o(oninit.callCount).equals(0)
o(onbeforeupdate.callCount).equals(0)
})
- o("does not try to re-initialize a factory component whose view has thrown", function() {
+ o("does not try to re-initialize a closure component whose view has thrown", function() {
var oninit = o.spy()
var onbeforeupdate = o.spy()
function A() {
@@ -141,7 +142,7 @@ o.spec("render", function() {
o(oninit.callCount).equals(1)
o(onbeforeupdate.callCount).equals(0)
})
- o("does not try to re-initialize a factory component whose oninit has thrown", function() {
+ o("does not try to re-initialize a closure component whose oninit has thrown", function() {
var oninit = o.spy(function(vnode) {throw new Error("error")})
var onbeforeupdate = o.spy()
function A() {
@@ -164,7 +165,7 @@ o.spec("render", function() {
o(oninit.callCount).equals(1)
o(onbeforeupdate.callCount).equals(0)
})
- o("does not try to re-initialize a factory component whose factory has thrown", function() {
+ o("does not try to re-initialize a closure component whose closure has thrown", function() {
function A() {
throw new Error("error")
}
diff --git a/render/tests/test-updateNodes.js b/render/tests/test-updateNodes.js
index b0e8c337..c58a29b4 100644
--- a/render/tests/test-updateNodes.js
+++ b/render/tests/test-updateNodes.js
@@ -1,6 +1,7 @@
"use strict"
var o = require("../../ospec/ospec")
+var components = require("../../test-utils/components")
var domMock = require("../../test-utils/domMock")
var vdom = require("../../render/render")
@@ -838,38 +839,6 @@ o.spec("updateNodes", function() {
o(root.childNodes[0].nodeName).equals("A")
o(root.childNodes[1].nodeName).equals("B")
})
- o("fragment child toggles from null when followed by null component then tag", function() {
- var component = {view: function() {return null}}
- var vnodes = [{tag: "[", children: [{tag: "a"}, {tag: component}, {tag: "b"}]}]
- var temp = [{tag: "[", children: [null, {tag: component}, {tag: "b"}]}]
- var updated = [{tag: "[", children: [{tag: "a"}, {tag: component}, {tag: "b"}]}]
-
- render(root, vnodes)
- render(root, temp)
- render(root, updated)
-
- o(root.childNodes.length).equals(2)
- o(root.childNodes[0].nodeName).equals("A")
- o(root.childNodes[1].nodeName).equals("B")
- })
- o("fragment child toggles from null in component when followed by null component then tag", function() {
- var flag = true
- var a = {view: function() {return flag ? {tag: "a"} : null}}
- var b = {view: function() {return null}}
- var vnodes = [{tag: "[", children: [{tag: a}, {tag: b}, {tag: "s"}]}]
- var temp = [{tag: "[", children: [{tag: a}, {tag: b}, {tag: "s"}]}]
- var updated = [{tag: "[", children: [{tag: a}, {tag: b}, {tag: "s"}]}]
-
- render(root, vnodes)
- flag = false
- render(root, temp)
- flag = true
- render(root, updated)
-
- o(root.childNodes.length).equals(2)
- o(root.childNodes[0].nodeName).equals("A")
- o(root.childNodes[1].nodeName).equals("S")
- })
o("cached, non-keyed nodes skip diff", function () {
var onupdate = o.spy();
var cached = {tag:"a", attrs:{onupdate: onupdate}}
@@ -926,7 +895,7 @@ o.spec("updateNodes", function() {
o(update.callCount).equals(2)
o(remove.callCount).equals(0)
})
- o("component is recreated if key changes to undefined", function () {
+ o("node is recreated if key changes to undefined", function () {
var vnode = {tag: "b", key: 1}
var updated = {tag: "b"}
@@ -936,4 +905,42 @@ o.spec("updateNodes", function() {
o(vnode.dom).notEquals(updated.dom)
})
-})
+ components.forEach(function(cmp){
+ o.spec(cmp.kind, function(){
+ var createComponent = cmp.create
+
+ o("fragment child toggles from null when followed by null component then tag", function() {
+ var component = createComponent({view: function() {return null}})
+ var vnodes = [{tag: "[", children: [{tag: "a"}, {tag: component}, {tag: "b"}]}]
+ var temp = [{tag: "[", children: [null, {tag: component}, {tag: "b"}]}]
+ var updated = [{tag: "[", children: [{tag: "a"}, {tag: component}, {tag: "b"}]}]
+
+ render(root, vnodes)
+ render(root, temp)
+ render(root, updated)
+
+ o(root.childNodes.length).equals(2)
+ o(root.childNodes[0].nodeName).equals("A")
+ o(root.childNodes[1].nodeName).equals("B")
+ })
+ o("fragment child toggles from null in component when followed by null component then tag", function() {
+ var flag = true
+ var a = createComponent({view: function() {return flag ? {tag: "a"} : null}})
+ var b = createComponent({view: function() {return null}})
+ var vnodes = [{tag: "[", children: [{tag: a}, {tag: b}, {tag: "s"}]}]
+ var temp = [{tag: "[", children: [{tag: a}, {tag: b}, {tag: "s"}]}]
+ var updated = [{tag: "[", children: [{tag: a}, {tag: b}, {tag: "s"}]}]
+
+ render(root, vnodes)
+ flag = false
+ render(root, temp)
+ flag = true
+ render(root, updated)
+
+ o(root.childNodes.length).equals(2)
+ o(root.childNodes[0].nodeName).equals("A")
+ o(root.childNodes[1].nodeName).equals("S")
+ })
+ })
+ })
+})
\ No newline at end of file
diff --git a/test-utils/components.js b/test-utils/components.js
new file mode 100644
index 00000000..c25ad5da
--- /dev/null
+++ b/test-utils/components.js
@@ -0,0 +1,27 @@
+module.exports = [
+ {
+ kind: 'POJO',
+ create: function(methods) {
+ var res = {view: function() {return {tag:'div'}}}
+ Object.keys(methods || {}).forEach(function(m){res[m] = methods[m]})
+ return res
+ }
+ }, {
+ kind: 'constructible',
+ create: function(methods) {
+ function res(){}
+ res.prototype.view = function() {return {tag:'div'}}
+ Object.keys(methods || {}).forEach(function(m){res.prototype[m] = methods[m]})
+ return res
+ }
+ }, {
+ kind: 'closure',
+ create: function(methods) {
+ return function() {
+ var res = {view: function() {return {tag:'div'}}}
+ Object.keys(methods || {}).forEach(function(m){res[m] = methods[m]})
+ return res
+ }
+ }
+ }
+]
diff --git a/test-utils/tests/index.html b/test-utils/tests/index.html
index e24fa2f8..51b04d73 100644
--- a/test-utils/tests/index.html
+++ b/test-utils/tests/index.html
@@ -14,12 +14,14 @@
+
+