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/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..64cd6fcb 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,689 +14,715 @@ 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} + components.forEach(function(cmp){ + o.spec(cmp.kind, function(){ + var createComponent = cmp.create - render(root, [node]) + o.spec("basics", function() { + o("works", function() { + var component = createComponent({ + view: function() { + return {tag: "div", attrs: {id: "a"}, text: "b"} + } + }) + var node = {tag: component} - 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]) - 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"} - 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"}]) + render(root, [node]) - 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(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("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(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 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.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.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.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.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("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.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.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(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.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.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.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(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.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 "copy" is shallow', 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("return value", function() { - o("can return fragments", function() { - var component = { - view: function(vnode) { - return [ - {tag: "label"}, - {tag: "input"}, - ] - } - } - render(root, [{tag: component}]) + o.spec("Tests specific to certain component kinds", function() { - 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: { + o.spec("POJO state", function() { + o("copies state", function() { + var called = 0 + var data = {a: 1} + var component = { + data: data, + oninit: init, view: function() { - viewCalled = true - return [{tag: "div", attrs: {id: "a"}, text: "b"}] - }, - oninit: function(vnode) { - o(viewCalled).equals(false) - }, + return "" + } + } + + render(root, [{tag: component}]) + + function init(vnode) { + o(vnode.state.data).equals(data) + + //inherits state via prototype + component.x = 1 + o(vnode.state.x).equals(1) } }) }) - 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) @@ -762,7 +789,7 @@ o.spec("component", function() { o(proto.onbeforeremove.args.length).equals(1) o(proto.onremove.args.length).equals(1) }) - o("Factory functions can be used as components", function() { + o("Closure functions can be used as components", function() { var state, context function component(vnode) { o(vnode.state).equals(null) 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..cf16dd1c 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,177 @@ 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} - }, - } + components.forEach(function(cmp){ + o.spec(cmp.kind, function(){ + var createComponent = cmp.create - var count = 0 - var vnode = {tag: component, attrs: {id: "a"}} - var updated = {tag: component, attrs: {id: "b"}} + 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]) + render(root, [vnode]) + render(root, [updated]) - function onbeforeupdate(vnode, old) { - count++ - return true - } + o(root.firstChild.firstChild.nodeValue).equals("a") + }) - o(count).equals(1) + 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 @@ + + diff --git a/test-utils/tests/test-components.js b/test-utils/tests/test-components.js new file mode 100644 index 00000000..cc9a091b --- /dev/null +++ b/test-utils/tests/test-components.js @@ -0,0 +1,54 @@ +"use strict" + +var o = require("../../ospec/ospec") +var components = require("../../test-utils/components") + +o.spec("test-utils/components", function() { + var test = o.spy(function(component) { + return function() { + o('works', function() { + o(typeof component.kind).equals('string') + + var methods = {oninit: function(){}, view: function(){}} + + var cmp1, cmp2 + + if (component.kind === "POJO") { + cmp1 = component.create() + cmp2 = component.create(methods) + } else if (component.kind === "constructible") { + cmp1 = new (component.create()) + cmp2 = new (component.create(methods)) + } else if (component.kind === "closure") { + cmp1 = component.create()() + cmp2 = component.create(methods)() + } else { + throw new Error("unexpected component kind") + } + + o(cmp1 != null).equals(true) + o(typeof cmp1.view).equals("function") + + var vnode = cmp1.view() + + o(vnode != null).equals(true) + o(vnode).deepEquals({tag: "div"}) + + if (component.kind !== 'constructible') { + o(cmp2).deepEquals(methods) + } else { + // deepEquals doesn't search the prototype, do it manually + o(cmp2 != null).equals(true) + o(cmp2.view).equals(methods.view) + o(cmp2.oninit).equals(methods.oninit) + } + }) + } + }) + o.after(function(){ + o(test.callCount).equals(3) + }) + components.forEach(function(component) { + o.spec(component.kind, test(component)) + }) +}) diff --git a/tests/test-api.js b/tests/test-api.js index 989b00a4..ec57a0f3 100644 --- a/tests/test-api.js +++ b/tests/test-api.js @@ -2,6 +2,7 @@ var o = require("../ospec/ospec") var browserMock = require("../test-utils/browserMock") +var components = require("../test-utils/components") o.spec("api", function() { var m @@ -68,95 +69,6 @@ o.spec("api", function() { o(query).equals("a=1&b=2") }) }) - o.spec("m.render", function() { - o("works", function() { - var root = window.document.createElement("div") - m.render(root, m("div")) - - o(root.childNodes.length).equals(1) - o(root.firstChild.nodeName).equals("DIV") - }) - }) - o.spec("m.mount", function() { - o("works", function() { - var root = window.document.createElement("div") - m.mount(root, {view: function() {return m("div")}}) - - o(root.childNodes.length).equals(1) - o(root.firstChild.nodeName).equals("DIV") - }) - }) - o.spec("m.route", function() { - o("works", function(done) { - var root = window.document.createElement("div") - m.route(root, "/a", { - "/a": {view: function() {return m("div")}} - }) - - setTimeout(function() { - o(root.childNodes.length).equals(1) - o(root.firstChild.nodeName).equals("DIV") - - done() - }, FRAME_BUDGET) - }) - o("m.route.prefix", function(done) { - var root = window.document.createElement("div") - m.route.prefix("#") - m.route(root, "/a", { - "/a": {view: function() {return m("div")}} - }) - - setTimeout(function() { - o(root.childNodes.length).equals(1) - o(root.firstChild.nodeName).equals("DIV") - - done() - }, FRAME_BUDGET) - }) - o("m.route.get", function(done) { - var root = window.document.createElement("div") - m.route(root, "/a", { - "/a": {view: function() {return m("div")}} - }) - - setTimeout(function() { - o(m.route.get()).equals("/a") - - done() - }, FRAME_BUDGET) - }) - o("m.route.set", function(done, timeout) { - timeout(100) - var root = window.document.createElement("div") - m.route(root, "/a", { - "/:id": {view: function() {return m("div")}} - }) - - setTimeout(function() { - m.route.set("/b") - setTimeout(function() { - o(m.route.get()).equals("/b") - - done() - }, FRAME_BUDGET) - }, FRAME_BUDGET) - }) - }) - o.spec("m.redraw", function() { - o("works", function(done) { - var count = 0 - var root = window.document.createElement("div") - m.mount(root, {view: function() {count++}}) - setTimeout(function() { - m.redraw() - - o(count).equals(2) - - done() - }, FRAME_BUDGET) - }) - }) o.spec("m.request", function() { o("works", function() { o(typeof m.request).equals("function") // TODO improve @@ -167,4 +79,99 @@ o.spec("api", function() { o(typeof m.jsonp).equals("function") // TODO improve }) }) -}) + o.spec("m.render", function() { + o("works", function() { + var root = window.document.createElement("div") + m.render(root, m("div")) + + o(root.childNodes.length).equals(1) + o(root.firstChild.nodeName).equals("DIV") + }) + }) + components.forEach(function(cmp){ + o.spec(cmp.kind, function(){ + var createComponent = cmp.create + + o.spec("m.mount", function() { + o("works", function() { + var root = window.document.createElement("div") + m.mount(root, createComponent({view: function() {return m("div")}})) + + o(root.childNodes.length).equals(1) + o(root.firstChild.nodeName).equals("DIV") + }) + }) + o.spec("m.route", function() { + o("works", function(done) { + var root = window.document.createElement("div") + m.route(root, "/a", { + "/a": createComponent({view: function() {return m("div")}}) + }) + + setTimeout(function() { + o(root.childNodes.length).equals(1) + o(root.firstChild.nodeName).equals("DIV") + + done() + }, FRAME_BUDGET) + }) + o("m.route.prefix", function(done) { + var root = window.document.createElement("div") + m.route.prefix("#") + m.route(root, "/a", { + "/a": createComponent({view: function() {return m("div")}}) + }) + + setTimeout(function() { + o(root.childNodes.length).equals(1) + o(root.firstChild.nodeName).equals("DIV") + + done() + }, FRAME_BUDGET) + }) + o("m.route.get", function(done) { + var root = window.document.createElement("div") + m.route(root, "/a", { + "/a": createComponent({view: function() {return m("div")}}) + }) + + setTimeout(function() { + o(m.route.get()).equals("/a") + + done() + }, FRAME_BUDGET) + }) + o("m.route.set", function(done, timeout) { + timeout(100) + var root = window.document.createElement("div") + m.route(root, "/a", { + "/:id": createComponent({view: function() {return m("div")}}) + }) + + setTimeout(function() { + m.route.set("/b") + setTimeout(function() { + o(m.route.get()).equals("/b") + + done() + }, FRAME_BUDGET) + }, FRAME_BUDGET) + }) + }) + o.spec("m.redraw", function() { + o("works", function(done) { + var count = 0 + var root = window.document.createElement("div") + m.mount(root, createComponent({view: function() {count++}})) + setTimeout(function() { + m.redraw() + + o(count).equals(2) + + done() + }, FRAME_BUDGET) + }) + }) + }) + }) +}) \ No newline at end of file