UNPKG

mithril

Version:

A framework for building brilliant applications

944 lines (826 loc) 24.6 kB
"use strict" var o = require("ospec") var components = require("../../test-utils/components") var domMock = require("../../test-utils/domMock") var vdom = require("../../render/render") var m = require("../../render/hyperscript") o.spec("component", function() { var $window, root, render o.beforeEach(function() { $window = domMock() root = $window.document.createElement("div") render = vdom($window) }) 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 m("div", {id: "a"}, "b") } }) var node = m(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 m("div", vnode.attrs, vnode.children) } }) var node = m(component, {id: "a"}, "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 m("div", vnode.attrs, vnode.children) } }) render(root, [m(component, {id: "a"}, "b")]) render(root, [m(component, {id: "c"}, "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 ? m("div") : null } }) render(root, m(component)) visible = true render(root, m(component)) o(root.firstChild.nodeName).equals("DIV") }) o("updates root from primitive", function() { var visible = false var component = createComponent({ view: function() { return visible ? m("div") : false } }) render(root, m(component)) visible = true render(root, m(component)) o(root.firstChild.nodeName).equals("DIV") }) o("updates root to null", function() { var visible = true var component = createComponent({ view: function() { return visible ? m("div") : null } }) render(root, m(component)) visible = false render(root, m(component)) o(root.childNodes.length).equals(0) }) o("updates root to primitive", function() { var visible = true var component = createComponent({ view: function() { return visible ? m("div") : false } }) render(root, m(component)) visible = false render(root, m(component)) o(root.childNodes.length).equals(0) }) o("updates root from null to null", function() { var component = createComponent({ view: function() { return null } }) render(root, m(component)) render(root, m(component)) o(root.childNodes.length).equals(0) }) o("removes", function() { var component = createComponent({ view: function() { return m("div") } }) var div = m("div", {key: 2}) render(root, [m(component, {key: 1}), div]) render(root, div) 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 m("g") } }) render(root, m("svg", m(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 m("g") } }) render(root, m("svg", m(component))) render(root, m("svg", m(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 [ m("label"), m("input"), ] } }) render(root, m(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, m(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, m(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, m(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, m(component)) o(root.firstChild.nodeType).equals(3) o(root.firstChild.nodeValue).equals("0") }) o("can return `true`", function() { var component = createComponent({ view: function() { return true } }) render(root, m(component)) o(root.childNodes.length).equals(0) }) o("can return `false`", function() { var component = createComponent({ view: function() { return false } }) render(root, m(component)) o(root.childNodes.length).equals(0) }) o("can return null", function() { var component = createComponent({ view: function() { return null } }) render(root, m(component)) o(root.childNodes.length).equals(0) }) o("can return undefined", function() { var component = createComponent({ view: function() { return undefined } }) render(root, m(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, m(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, m(component)) o(root.childNodes.length).equals(0) try { render(root, m(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 [ m("label"), m("input"), ] } }) render(root, m(component)) render(root, m(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, m(component)) render(root, m(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, m(component)) render(root, m(component)) o(root.childNodes.length).equals(0) }) o("can remove when returning fragments", function() { var component = createComponent({ view: function() { return [ m("label"), m("input"), ] } }) var div = m("div", {key: 2}) render(root, [m(component, {key: 1}), div]) render(root, [m("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 = m("div", {key: 2}) render(root, [m(component, {key: 1}), div]) render(root, [m("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 m("div", {id: "a"}, "b") } }) render(root, m(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 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 [m("div", {id: "a"}, "b")] } }) render(root, m(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 oninit before view", function() { var viewCalled = false var component = createComponent({ view: function() { viewCalled = true return m("div", {id: "a"}, "b") }, oninit: function() { o(viewCalled).equals(false) }, }) render(root, m(component)) }) o("does not calls oninit on redraw", function() { var init = o.spy() var component = createComponent({ view: function() { return m("div", {id: "a"}, "b") }, oninit: init, }) function view() { return m(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 m("div", {id: "a"}, "b") } }) render(root, m(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("does not calls oncreate on redraw", function() { var create = o.spy() var component = createComponent({ view: function() { return m("div", {id: "a"}, "b") }, oncreate: create, }) function view() { return m(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 m("div", {id: "a"}, "b") } }) render(root, m(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", 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 m("div", {id: "a"}, "b") } }) render(root, m(component)) o(called).equals(0) render(root, m(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 [m("div", {id: "a"}, "b")] } }) render(root, m(component)) o(called).equals(0) render(root, m(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 m("div", {id: "a"}, "b") } }) render(root, m(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 [m("div", {id: "a"}, "b")] } }) render(root, m(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 m("div", {id: "a"}, "b") } }) render(root, m(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 [m("div", {id: "a"}, "b")] } }) render(root, m(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 m("div") } }) var vnode = m(component, {key: 1}) var updated = m(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) { if (hook === "onbeforeupdate") { // the component's `onbeforeupdate` is called after the `attrs`' one attrs[hook] = o.spy(function() { o(attrs[hook].callCount).equals(methods[hook].callCount + 1)(hook) }) methods[hook] = o.spy(function() { o(attrs[hook].callCount).equals(methods[hook].callCount)(hook) }) } else { // the other component hooks are called before the `attrs` ones methods[hook] = o.spy(function() { o(attrs[hook].callCount).equals(methods[hook].callCount - 1)(hook) }) attrs[hook] = o.spy(function() { o(attrs[hook].callCount).equals(methods[hook].callCount)(hook) }) } }) 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, [m(component, 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, [m(component, 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, [m(component, attrs)]) render(root, [m(component, 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("no recycling occurs (was: 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 m("div") }) var component = createComponent({view: view}) render(root, [m("div", m(component, {key: 1}))]) var child = root.firstChild.firstChild render(root, []) step = 1 render(root, [m("div", m(component, {key: 1}))]) o(child).notEquals(root.firstChild.firstChild) // this used to be a recycling pool test 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, m(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, m(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, m(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, [m(component, {oninit: oninit})]) render(root, [m(component, {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, [m(component, {oninit: oninit})]) render(root, [m(component, {oninit: oninit})]) render(root, []) o(component.callCount).equals(1) o(oninit.callCount).equals(1) o(view.callCount).equals(2) }) }) }) })