UNPKG

mithril

Version:

A framework for building brilliant applications

712 lines (537 loc) 17.4 kB
"use strict" var o = require("ospec") var domMock = require("../../test-utils/domMock") var vdom = require("../../render/render") var m = require("../../render/hyperscript") var trust = require("../../render/trust") o.spec("attributes", function() { var $window, root, render o.beforeEach(function() { $window = domMock() root = $window.document.body render = vdom($window) }) o.spec("basics", function() { o("works (create/update/remove)", function() { var a = m("div") var b = m("div", {id: "test"}) var c = m("div") render(root, a); o(a.dom.hasAttribute("id")).equals(false) render(root, b); o(b.dom.getAttribute("id")).equals("test") render(root, c); o(c.dom.hasAttribute("id")).equals(false) }) o("undefined attr is equivalent to a lack of attr", function() { var a = m("div", {id: undefined}) var b = m("div", {id: "test"}) var c = m("div", {id: undefined}) render(root, a); o(a.dom.hasAttribute("id")).equals(false) render(root, b); o(b.dom.hasAttribute("id")).equals(true) o(b.dom.getAttribute("id")).equals("test") // #1804 render(root, c); o(c.dom.hasAttribute("id")).equals(false) }) }) o.spec("customElements", function(){ o("when vnode is customElement without property, custom setAttribute called", function(){ var f = $window.document.createElement var spies = [] $window.document.createElement = function(tag, is){ var el = f(tag, is) var spy = o.spy(el.setAttribute) el.setAttribute = spy spies.push(spy) spy.elem = el return el } render(root, [ m("input", {value: "hello"}), m("input", {value: "hello"}), m("input", {value: "hello"}), m("custom-element", {custom: "x"}), m("input", {is: "something-special", custom: "x"}), m("custom-element", {is: "something-special", custom: "x"}) ]) o(spies[1].callCount).equals(0) o(spies[0].callCount).equals(0) o(spies[2].callCount).equals(0) o(spies[3].calls).deepEquals([{this: spies[3].elem, args: ["custom", "x"]}]) o(spies[4].calls).deepEquals([{this: spies[4].elem, args: ["custom", "x"]}]) o(spies[5].calls).deepEquals([{this: spies[5].elem, args: ["custom", "x"]}]) }) o("when vnode is customElement with property, custom setAttribute not called", function(){ var f = $window.document.createElement var spies = [] var getters = [] var setters = [] $window.document.createElement = function(tag, is){ var el = f(tag, is) var spy = o.spy(el.setAttribute) el.setAttribute = spy spies.push(spy) spy.elem = el if (tag === "custom-element" || is && is.is === "something-special") { var custom = "foo" var getter, setter Object.defineProperty(el, "custom", { configurable: true, enumerable: true, get: getter = o.spy(function () { return custom }), set: setter = o.spy(function (value) { custom = value }) }) getters.push(getter) setters.push(setter) } return el } render(root, [ m("input", {value: "hello"}), m("input", {value: "hello"}), m("input", {value: "hello"}), m("custom-element", {custom: "x"}), m("input", {is: "something-special", custom: "x"}), m("custom-element", {is: "something-special", custom: "x"}) ]) o(spies[0].callCount).equals(0) o(spies[1].callCount).equals(0) o(spies[2].callCount).equals(0) o(spies[3].callCount).equals(0) o(spies[4].callCount).equals(0) o(spies[5].callCount).equals(0) o(getters[0].callCount).equals(0) o(getters[1].callCount).equals(0) o(getters[2].callCount).equals(0) o(setters[0].calls).deepEquals([{this: spies[3].elem, args: ["x"]}]) o(setters[1].calls).deepEquals([{this: spies[4].elem, args: ["x"]}]) o(setters[2].calls).deepEquals([{this: spies[5].elem, args: ["x"]}]) }) }) o.spec("input readonly", function() { o("when input readonly is true, attribute is present", function() { var a = m("input", {readonly: true}) render(root, a) o(a.dom.attributes["readonly"].value).equals("") }) o("when input readonly is false, attribute is not present", function() { var a = m("input", {readonly: false}) render(root, a) o(a.dom.attributes["readonly"]).equals(undefined) }) }) o.spec("input checked", function() { o("when input checked is true, attribute is not present", function() { var a = m("input", {checked: true}) render(root, a) o(a.dom.checked).equals(true) o(a.dom.attributes["checked"]).equals(undefined) }) o("when input checked is false, attribute is not present", function() { var a = m("input", {checked: false}) render(root, a) o(a.dom.checked).equals(false) o(a.dom.attributes["checked"]).equals(undefined) }) o("after input checked is changed by 3rd party, it can still be changed by render", function() { var a = m("input", {checked: false}) var b = m("input", {checked: true}) render(root, a) a.dom.checked = true //setting the javascript property makes the value no longer track the state of the attribute a.dom.checked = false render(root, b) o(a.dom.checked).equals(true) o(a.dom.attributes["checked"]).equals(undefined) }) }) o.spec("input.value", function() { o("can be set as text", function() { var a = m("input", {value: "test"}) render(root, a); o(a.dom.value).equals("test") }) o("a lack of attribute removes `value`", function() { var a = m("input") var b = m("input", {value: "test"}) var c = m("input") render(root, a) o(a.dom.value).equals("") render(root, b) o(a.dom.value).equals("test") // https://github.com/MithrilJS/mithril.js/issues/1804#issuecomment-304521235 render(root, c) o(a.dom.value).equals("") }) o("can be set as number", function() { var a = m("input", {value: 1}) render(root, a); o(a.dom.value).equals("1") }) o("null becomes the empty string", function() { var a = m("input", {value: null}) var b = m("input", {value: "test"}) var c = m("input", {value: null}) render(root, a); o(a.dom.value).equals("") o(a.dom.getAttribute("value")).equals(null) render(root, b); o(b.dom.value).equals("test") o(b.dom.getAttribute("value")).equals(null) render(root, c); o(c.dom.value).equals("") o(c.dom.getAttribute("value")).equals(null) }) o("'' and 0 are different values", function() { var a = m("input", {value: 0}) var b = m("input", {value: ""}) var c = m("input", {value: 0}) render(root, a); o(a.dom.value).equals("0") render(root, b); o(b.dom.value).equals("") // #1595 redux render(root, c); o(c.dom.value).equals("0") }) o("isn't set when equivalent to the previous value and focused", function() { var $window = domMock({spy: o.spy}) var root = $window.document.body var render = vdom($window) var a =m("input") var b = m("input", {value: "1"}) var c = m("input", {value: "1"}) var d = m("input", {value: 1}) var e = m("input", {value: 2}) render(root, a) var spies = $window.__getSpies(a.dom) a.dom.focus() o(spies.valueSetter.callCount).equals(0) render(root, b) o(b.dom.value).equals("1") o(spies.valueSetter.callCount).equals(1) render(root, c) o(c.dom.value).equals("1") o(spies.valueSetter.callCount).equals(1) render(root, d) o(d.dom.value).equals("1") o(spies.valueSetter.callCount).equals(1) render(root, e) o(d.dom.value).equals("2") o(spies.valueSetter.callCount).equals(2) }) }) o.spec("input.type", function() { o("the input.type setter is never used", function() { var $window = domMock({spy: o.spy}) var root = $window.document.body var render = vdom($window) var a = m("input", {type: "radio"}) var b = m("input", {type: "text"}) var c = m("input") render(root, a) var spies = $window.__getSpies(a.dom) o(spies.typeSetter.callCount).equals(0) o(a.dom.getAttribute("type")).equals("radio") render(root, b) o(spies.typeSetter.callCount).equals(0) o(b.dom.getAttribute("type")).equals("text") render(root, c) o(spies.typeSetter.callCount).equals(0) o(c.dom.hasAttribute("type")).equals(false) }) }) o.spec("textarea.value", function() { o("can be removed by not passing a value", function() { var a = m("textarea", {value:"x"}) var b = m("textarea") render(root, a) o(a.dom.value).equals("x") // https://github.com/MithrilJS/mithril.js/issues/1804#issuecomment-304521235 render(root, b) o(b.dom.value).equals("") }) o("isn't set when equivalent to the previous value and focused", function() { var $window = domMock({spy: o.spy}) var root = $window.document.body var render = vdom($window) var a = m("textarea") var b = m("textarea", {value: "1"}) var c = m("textarea", {value: "1"}) var d = m("textarea", {value: 1}) var e = m("textarea", {value: 2}) render(root, a) var spies = $window.__getSpies(a.dom) a.dom.focus() o(spies.valueSetter.callCount).equals(0) render(root, b) o(b.dom.value).equals("1") o(spies.valueSetter.callCount).equals(1) render(root, c) o(c.dom.value).equals("1") o(spies.valueSetter.callCount).equals(1) render(root, d) o(d.dom.value).equals("1") o(spies.valueSetter.callCount).equals(1) render(root, e) o(d.dom.value).equals("2") o(spies.valueSetter.callCount).equals(2) }) }) o.spec("link href", function() { o("when link href is true, attribute is present", function() { var a = m("a", {href: true}) render(root, a) o(a.dom.attributes["href"]).notEquals(undefined) }) o("when link href is false, attribute is not present", function() { var a = m("a", {href: false}) render(root, a) o(a.dom.attributes["href"]).equals(undefined) }) }) o.spec("canvas width and height", function() { o("uses attribute API", function() { var canvas = m("canvas", {width: "100%"}) render(root, canvas) o(canvas.dom.attributes["width"].value).equals("100%") o(canvas.dom.width).equals(100) }) }) o.spec("svg", function() { o("when className is specified then it should be added as a class", function() { var a = m("svg", {className: "test"}) render(root, a); o(a.dom.attributes["class"].value).equals("test") }) /* eslint-disable no-script-url */ o("handles xlink:href", function() { var vnode = m("svg", {ns: "http://www.w3.org/2000/svg"}, m("a", {ns: "http://www.w3.org/2000/svg", "xlink:href": "javascript:;"}) ) render(root, vnode) o(vnode.dom.nodeName).equals("svg") o(vnode.dom.firstChild.attributes["href"].value).equals("javascript:;") o(vnode.dom.firstChild.attributes["href"].namespaceURI).equals("http://www.w3.org/1999/xlink") vnode = m("svg", {ns: "http://www.w3.org/2000/svg"}, m("a", {ns: "http://www.w3.org/2000/svg"}) ) render(root, vnode) o(vnode.dom.nodeName).equals("svg") o("href" in vnode.dom.firstChild.attributes).equals(false) }) /* eslint-enable no-script-url */ }) o.spec("option.value", function() { o("can be set as text", function() { var a = m("option", {value: "test"}) render(root, a); o(a.dom.value).equals("test") }) o("can be set as number", function() { var a = m("option", {value: 1}) render(root, a); o(a.dom.value).equals("1") }) o("null removes the attribute", function() { var a = m("option", {value: null}) var b = m("option", {value: "test"}) var c = m("option", {value: null}) render(root, a); o(a.dom.value).equals("") o(a.dom.hasAttribute("value")).equals(false) render(root, b); o(b.dom.value).equals("test") o(b.dom.getAttribute("value")).equals("test") render(root, c); o(c.dom.value).equals("") o(c.dom.hasAttribute("value")).equals(false) }) o("'' and 0 are different values", function() { var a = m("option", {value: 0}, "") var b = m("option", {value: ""}, "") var c = m("option", {value: 0}, "") render(root, a); o(a.dom.value).equals("0") render(root, b); o(a.dom.value).equals("") // #1595 redux render(root, c); o(c.dom.value).equals("0") }) o("isn't set when equivalent to the previous value", function() { var $window = domMock({spy: o.spy}) var root = $window.document.body var render = vdom($window) var a = m("option") var b = m("option", {value: "1"}) var c = m("option", {value: "1"}) var d = m("option", {value: 1}) var e = m("option", {value: 2}) render(root, a) var spies = $window.__getSpies(a.dom) o(spies.valueSetter.callCount).equals(0) render(root, b) o(b.dom.value).equals("1") o(spies.valueSetter.callCount).equals(1) render(root, c) o(c.dom.value).equals("1") o(spies.valueSetter.callCount).equals(1) render(root, d) o(d.dom.value).equals("1") o(spies.valueSetter.callCount).equals(1) render(root, e) o(d.dom.value).equals("2") o(spies.valueSetter.callCount).equals(2) }) }) o.spec("select.value", function() { function makeSelect(value) { var attrs = (arguments.length === 0) ? {} : {value: value} return m("select", attrs, m("option", {value: "1"}), m("option", {value: "2"}), m("option", {value: "a"}), m("option", {value: "0"}), m("option", {value: ""}) ) } /* FIXME This incomplete test is meant for testing #1916. However it cannot be completed until #1978 is addressed which is a lack a working select.selected / option.selected attribute. Ask @dead-claudia. o("render select options", function() { var select = m("select", {selectedIndex: 0}, m("option", {value: "1", selected: ""}) ) render(root, select) }) */ o("can be set as text", function() { var a = makeSelect() var b = makeSelect("2") var c = makeSelect("a") render(root, a) o(a.dom.value).equals("1") o(a.dom.selectedIndex).equals(0) render(root, b) o(b.dom.value).equals("2") o(b.dom.selectedIndex).equals(1) render(root, c) o(c.dom.value).equals("a") o(c.dom.selectedIndex).equals(2) }) o("setting null unsets the value", function() { var a = makeSelect(null) render(root, a) o(a.dom.value).equals("") o(a.dom.selectedIndex).equals(-1) }) o("values are type converted", function() { var a = makeSelect(1) var b = makeSelect(2) render(root, a) o(a.dom.value).equals("1") o(a.dom.selectedIndex).equals(0) render(root, b) o(b.dom.value).equals("2") o(b.dom.selectedIndex).equals(1) }) o("'' and 0 are different values when focused", function() { var a = makeSelect("") var b = makeSelect(0) render(root, a) a.dom.focus() o(a.dom.value).equals("") // #1595 redux render(root, b) o(b.dom.value).equals("0") }) o("'' and null are different values when focused", function() { var a = makeSelect("") var b = makeSelect(null) var c = makeSelect("") render(root, a) a.dom.focus() o(a.dom.value).equals("") o(a.dom.selectedIndex).equals(4) render(root, b) o(b.dom.value).equals("") o(b.dom.selectedIndex).equals(-1) render(root, c) o(c.dom.value).equals("") o(c.dom.selectedIndex).equals(4) }) o("updates with the same value do not re-set the attribute if the select has focus", function() { var $window = domMock({spy: o.spy}) var root = $window.document.body var render = vdom($window) var a = makeSelect() var b = makeSelect("1") var c = makeSelect(1) var d = makeSelect("2") render(root, a) var spies = $window.__getSpies(a.dom) a.dom.focus() o(spies.valueSetter.callCount).equals(0) o(a.dom.value).equals("1") render(root, b) o(spies.valueSetter.callCount).equals(0) o(b.dom.value).equals("1") render(root, c) o(spies.valueSetter.callCount).equals(0) o(c.dom.value).equals("1") render(root, d) o(spies.valueSetter.callCount).equals(1) o(d.dom.value).equals("2") }) }) o.spec("contenteditable throws on untrusted children", function() { o("including elements", function() { var div = m("div", {contenteditable: true}, m("script", {src: "http://evil.com"})) var succeeded = false try { render(root, div) succeeded = true } catch(e){/* ignore */} o(succeeded).equals(false) }) o("tolerating empty children", function() { var div = m("div", {contenteditable: true}) var succeeded = false try { render(root, div) succeeded = true } catch(e){/* ignore */} o(succeeded).equals(true) }) o("tolerating trusted content", function() { var div = m("div", {contenteditable: true}, trust("<a></a>")) var succeeded = false try { render(root, div) succeeded = true } catch(e){/* ignore */} o(succeeded).equals(true) }) }) o.spec("mutate attr object", function() { o("warn when reusing attrs object", function() { const _consoleWarn = console.warn console.warn = o.spy() const attrs = {className: "on"} render(root, {tag: "input", attrs}) attrs.className = "off" render(root, {tag: "input", attrs}) o(console.warn.callCount).equals(1) o(console.warn.args[0]).equals("Don't reuse attrs object, use new object for every redraw, this will throw in next major") console.warn = _consoleWarn }) }) })