"use strict" var o = require("../../ospec/ospec") var components = require("../../test-utils/components") var domMock = require("../../test-utils/domMock") var vdom = require("../../render/render") o.spec("component", function() { var $window, root, render o.beforeEach(function() { $window = domMock() root = $window.document.createElement("div") render = vdom($window).render }) 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"].value).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"].value).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"].value).equals("c") o(root.firstChild.firstChild.nodeValue).equals("d") }) o("updates root from null", function() { var visible = false var component = createComponent({ view: function() { 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() { 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() { 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() { 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() { return null } }) render(root, [{tag: component}]) render(root, [{tag: component}]) o(root.childNodes.length).equals(0) }) o("removes", function() { var component = createComponent({ view: function() { 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() { 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() { 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() { 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() { 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() { 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() { 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() { 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() { 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() { 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() { return null } }) render(root, [{tag: component}]) o(root.childNodes.length).equals(0) }) o("can return undefined", function() { var component = createComponent({ view: function() { return undefined } }) render(root, [{tag: component}]) o(root.childNodes.length).equals(0) }) o("throws a custom error if it returns itself when created", function() { // A view that returns its vnode would otherwise trigger an infinite loop var threw = false var component = createComponent({ view: function(vnode) { return vnode } }) try { render(root, [{tag: component}]) } catch (e) { threw = true o(e instanceof Error).equals(true) // Call stack exception is a RangeError o(e instanceof RangeError).equals(false) } o(threw).equals(true) }) o("throws a custom error if it returns itself when updated", function() { // A view that returns its vnode would otherwise trigger an infinite loop var threw = false var init = true var oninit = o.spy() var component = createComponent({ oninit: oninit, view: function(vnode) { if (init) return init = false else return vnode } }) render(root, [{tag: component}]) o(root.firstChild.nodeType).equals(3) o(root.firstChild.nodeValue).equals("") try { render(root, [{tag: component}]) } catch (e) { threw = true o(e instanceof Error).equals(true) // Call stack exception is a RangeError o(e instanceof RangeError).equals(false) } o(threw).equals(true) o(oninit.callCount).equals(1) }) o("can update when returning fragments", function() { var component = createComponent({ view: function() { 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() { 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() { 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() { 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() { 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"].value).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"].value).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() { 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"].value).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"].value).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"].value).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"].value).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 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("lifecycle timing megatest (for a single component with the state overwritten)", function() { var methods = { view: o.spy(function(vnode) { o(vnode.state).equals(1) 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(vnode) { o(vnode.state).equals(1) o(attrs[hook].callCount).equals(methods[hook].callCount + 1) }) methods[hook] = o.spy(function(vnode) { o(vnode.state).equals(1) o(attrs[hook].callCount).equals(methods[hook].callCount) }) }) var attrsOninit = attrs.oninit var methodsOninit = methods.oninit attrs.oninit = o.spy(function(vnode){ vnode.state = 1 return attrsOninit.call(this, vnode) }) methods.oninit = o.spy(function(vnode){ vnode.state = 1 return methodsOninit.call(this, vnode) }) 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 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 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 data = {} var component = { data: data, oninit: init, view: function() { 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("Constructible", function() { var oninit = o.spy() var component = o.spy(function(vnode){ o(vnode.state).equals(undefined) 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, attrs: {oninit: oninit}}]) render(root, [{tag: component, attrs: {oninit: oninit}}]) 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(undefined) o(oninit.callCount).equals(0) return state = { view: view } }) render(root, [{tag: component, attrs: {oninit: oninit}}]) render(root, [{tag: component, attrs: {oninit: oninit}}]) render(root, []) o(component.callCount).equals(1) o(oninit.callCount).equals(1) o(view.callCount).equals(2) }) }) }) })