├── .gitignore ├── .travis.yml ├── void-elements.js ├── package.json ├── LICENSE ├── create-attribute.js ├── README.md ├── index.js ├── property-config.js └── test └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store* 2 | *.log 3 | *.gz 4 | 5 | node_modules 6 | coverage 7 | cache -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | node_js: 2 | - "4" 3 | - "5" 4 | language: node_js 5 | script: "npm run-script test-travis" 6 | after_script: "npm install coveralls@2 && cat ./coverage/lcov.info | coveralls" -------------------------------------------------------------------------------- /void-elements.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Void elements. 4 | * 5 | * https://github.com/facebook/react/blob/v0.12.0/src/browser/ui/ReactDOMComponent.js#L99 6 | */ 7 | 8 | module.exports = { 9 | 'area': true, 10 | 'base': true, 11 | 'br': true, 12 | 'col': true, 13 | 'embed': true, 14 | 'hr': true, 15 | 'img': true, 16 | 'input': true, 17 | 'keygen': true, 18 | 'link': true, 19 | 'meta': true, 20 | 'param': true, 21 | 'source': true, 22 | 'track': true, 23 | 'wbr': true 24 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vdom-to-html", 3 | "description": "Turn virtual-dom nodes into HTML", 4 | "version": "2.3.1", 5 | "author": "Nathan Tran ", 6 | "license": "MIT", 7 | "repository": "nthtran/vdom-to-html", 8 | "keywords": [], 9 | "dependencies": { 10 | "escape-html": "^1.0.1", 11 | "param-case": "^1.0.1", 12 | "xtend": "^4.0.0" 13 | }, 14 | "devDependencies": { 15 | "istanbul": "0", 16 | "mocha": "2", 17 | "vdom-thunk": "^3.0.0", 18 | "virtual-dom": "^2.0.0" 19 | }, 20 | "peerDependencies": { 21 | "virtual-dom": "^2.0.0" 22 | }, 23 | "scripts": { 24 | "test": "mocha --reporter spec", 25 | "test-cov": "istanbul cover node_modules/mocha/bin/_mocha -- --reporter dot", 26 | "test-travis": "istanbul cover node_modules/mocha/bin/_mocha --report lcovonly -- --reporter dot" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Nathan Tran ntran013@gmail.com 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /create-attribute.js: -------------------------------------------------------------------------------- 1 | var escape = require('escape-html'); 2 | var propConfig = require('./property-config'); 3 | var types = propConfig.attributeTypes; 4 | var properties = propConfig.properties; 5 | var attributeNames = propConfig.attributeNames; 6 | 7 | var prefixAttribute = memoizeString(function (name) { 8 | return escape(name) + '="'; 9 | }); 10 | 11 | module.exports = createAttribute; 12 | 13 | /** 14 | * Create attribute string. 15 | * 16 | * @param {String} name The name of the property or attribute 17 | * @param {*} value The value 18 | * @param {Boolean} [isAttribute] Denotes whether `name` is an attribute. 19 | * @return {?String} Attribute string || null if not a valid property or custom attribute. 20 | */ 21 | 22 | function createAttribute(name, value, isAttribute) { 23 | if (properties.hasOwnProperty(name)) { 24 | if (shouldSkip(name, value)) return ''; 25 | name = (attributeNames[name] || name).toLowerCase(); 26 | var attrType = properties[name]; 27 | // for BOOLEAN `value` only has to be truthy 28 | // for OVERLOADED_BOOLEAN `value` has to be === true 29 | if ((attrType === types.BOOLEAN) || 30 | (attrType === types.OVERLOADED_BOOLEAN && value === true)) { 31 | return escape(name); 32 | } 33 | return prefixAttribute(name) + escape(value) + '"'; 34 | } else if (isAttribute) { 35 | if (value == null) return ''; 36 | return prefixAttribute(name) + escape(value) + '"'; 37 | } 38 | // return null if `name` is neither a valid property nor an attribute 39 | return null; 40 | } 41 | 42 | /** 43 | * Should skip false boolean attributes. 44 | */ 45 | 46 | function shouldSkip(name, value) { 47 | var attrType = properties[name]; 48 | return value == null || 49 | (attrType === types.BOOLEAN && !value) || 50 | (attrType === types.OVERLOADED_BOOLEAN && value === false); 51 | } 52 | 53 | /** 54 | * Memoizes the return value of a function that accepts one string argument. 55 | * 56 | * @param {function} callback 57 | * @return {function} 58 | */ 59 | 60 | function memoizeString(callback) { 61 | var cache = {}; 62 | return function(string) { 63 | if (cache.hasOwnProperty(string)) { 64 | return cache[string]; 65 | } else { 66 | return cache[string] = callback.call(this, string); 67 | } 68 | }; 69 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # vdom-to-html 3 | 4 | [![NPM version][npm-image]][npm-url] 5 | [![Github release][github-image]][github-url] 6 | [![Build status][travis-image]][travis-url] 7 | [![Test coverage][coveralls-image]][coveralls-url] 8 | [![Dependency Status][david-image]][david-url] 9 | [![License][license-image]][license-url] 10 | 11 | Turn [virtual-dom](https://github.com/Matt-Esch/virtual-dom/) nodes into HTML 12 | 13 | ## Installation 14 | 15 | ```sh 16 | npm install --save vdom-to-html 17 | ``` 18 | 19 | ## Usage 20 | 21 | ```js 22 | var VNode = require('virtual-dom/vnode/vnode'); 23 | var toHTML = require('vdom-to-html'); 24 | 25 | toHTML(new VNode('input', { className: 'name', type: 'text' })); 26 | // => '' 27 | ``` 28 | 29 | ### Special case for Widgets 30 | 31 | >Widgets are used to take control of the patching process, allowing the user to create stateful components, control sub-tree rendering, and hook into element removal. 32 | [Documentation is available here](https://github.com/Matt-Esch/virtual-dom/blob/master/docs/widget.md). 33 | 34 | Widgets are given an opportunity to provide a vdom representation through an optional `render` method. If the `render` method is not found an empty string will be used instead. 35 | 36 | ```js 37 | var Widget = function(text) { 38 | this.text = text; 39 | } 40 | Widget.prototype.type = 'Widget'; 41 | // provide a vdom representation of the widget 42 | Widget.prototype.render = function() { 43 | return new VNode('span', null, [new VText(this.text)]); 44 | }; 45 | // other widget prototype methods would be implemented 46 | 47 | toHTML(new Widget('hello')); 48 | // => 'hello' 49 | ``` 50 | 51 | [npm-image]: https://img.shields.io/npm/v/vdom-to-html.svg?style=flat-square 52 | [npm-url]: https://npmjs.org/package/vdom-to-html 53 | [github-image]: http://img.shields.io/github/release/nthtran/vdom-to-html.svg?style=flat-square 54 | [github-url]: https://github.com/nthtran/vdom-to-html/releases 55 | [travis-image]: https://img.shields.io/travis/nthtran/vdom-to-html.svg?style=flat-square 56 | [travis-url]: https://travis-ci.org/nthtran/vdom-to-html 57 | [coveralls-image]: https://img.shields.io/coveralls/nthtran/vdom-to-html.svg?style=flat-square 58 | [coveralls-url]: https://coveralls.io/r/nthtran/vdom-to-html?branch=master 59 | [david-image]: http://img.shields.io/david/nthtran/vdom-to-html.svg?style=flat-square 60 | [david-url]: https://david-dm.org/nthtran/vdom-to-html 61 | [license-image]: http://img.shields.io/npm/l/vdom-to-html.svg?style=flat-square 62 | [license-url]: LICENSE 63 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var escape = require('escape-html'); 2 | var extend = require('xtend'); 3 | var isVNode = require('virtual-dom/vnode/is-vnode'); 4 | var isVText = require('virtual-dom/vnode/is-vtext'); 5 | var isThunk = require('virtual-dom/vnode/is-thunk'); 6 | var isWidget = require('virtual-dom/vnode/is-widget'); 7 | var softHook = require('virtual-dom/virtual-hyperscript/hooks/soft-set-hook'); 8 | var attrHook = require('virtual-dom/virtual-hyperscript/hooks/attribute-hook'); 9 | var paramCase = require('param-case'); 10 | var createAttribute = require('./create-attribute'); 11 | var voidElements = require('./void-elements'); 12 | 13 | module.exports = toHTML; 14 | 15 | function toHTML(node, parent) { 16 | if (!node) return ''; 17 | 18 | if (isThunk(node)) { 19 | node = node.render(); 20 | } 21 | 22 | if (isWidget(node) && node.render) { 23 | node = node.render(); 24 | } 25 | 26 | if (isVNode(node)) { 27 | return openTag(node) + tagContent(node) + closeTag(node); 28 | } else if (isVText(node)) { 29 | if (parent && (parent.tagName.toLowerCase() === 'script' 30 | || parent.tagName.toLowerCase() === 'style')) 31 | return String(node.text); 32 | return escape(String(node.text)); 33 | } 34 | 35 | return ''; 36 | } 37 | 38 | function openTag(node) { 39 | var props = node.properties; 40 | var ret = '<' + node.tagName.toLowerCase(); 41 | 42 | for (var name in props) { 43 | var value = props[name]; 44 | if (value == null) continue; 45 | 46 | if (name == 'attributes') { 47 | value = extend({}, value); 48 | for (var attrProp in value) { 49 | ret += ' ' + createAttribute(attrProp, value[attrProp], true); 50 | } 51 | continue; 52 | } 53 | 54 | if (name == 'dataset') { 55 | value = extend({}, value); 56 | for (var dataProp in value) { 57 | ret += ' ' + createAttribute('data-' + paramCase(dataProp), value[dataProp], true); 58 | } 59 | continue; 60 | } 61 | 62 | if (name == 'style') { 63 | var css = ''; 64 | value = extend({}, value); 65 | for (var styleProp in value) { 66 | css += paramCase(styleProp) + ': ' + value[styleProp] + '; '; 67 | } 68 | value = css.trim(); 69 | } 70 | 71 | if (value instanceof softHook || value instanceof attrHook) { 72 | ret += ' ' + createAttribute(name, value.value, true); 73 | continue; 74 | } 75 | 76 | var attr = createAttribute(name, value); 77 | if (attr) ret += ' ' + attr; 78 | } 79 | 80 | return ret + '>'; 81 | } 82 | 83 | function tagContent(node) { 84 | var innerHTML = node.properties.innerHTML; 85 | if (innerHTML != null) return innerHTML; 86 | else { 87 | var ret = ''; 88 | if (node.children && node.children.length) { 89 | for (var i = 0, l = node.children.length; i'; 101 | } 102 | -------------------------------------------------------------------------------- /property-config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Attribute types. 3 | */ 4 | 5 | var types = { 6 | BOOLEAN: 1, 7 | OVERLOADED_BOOLEAN: 2 8 | }; 9 | 10 | /** 11 | * Properties. 12 | * 13 | * Taken from https://github.com/facebook/react/blob/847357e42e5267b04dd6e297219eaa125ab2f9f4/src/browser/ui/dom/HTMLDOMPropertyConfig.js 14 | * 15 | */ 16 | 17 | var properties = { 18 | /** 19 | * Standard Properties 20 | */ 21 | accept: true, 22 | acceptCharset: true, 23 | accessKey: true, 24 | action: true, 25 | allowFullScreen: types.BOOLEAN, 26 | allowTransparency: true, 27 | alt: true, 28 | async: types.BOOLEAN, 29 | autocomplete: true, 30 | autofocus: types.BOOLEAN, 31 | autoplay: types.BOOLEAN, 32 | cellPadding: true, 33 | cellSpacing: true, 34 | charset: true, 35 | checked: types.BOOLEAN, 36 | classID: true, 37 | className: true, 38 | cols: true, 39 | colSpan: true, 40 | content: true, 41 | contentEditable: true, 42 | contextMenu: true, 43 | controls: types.BOOLEAN, 44 | coords: true, 45 | crossOrigin: true, 46 | data: true, // For `` acts as `src`. 47 | dateTime: true, 48 | defer: types.BOOLEAN, 49 | dir: true, 50 | disabled: types.BOOLEAN, 51 | download: types.OVERLOADED_BOOLEAN, 52 | draggable: true, 53 | enctype: true, 54 | form: true, 55 | formAction: true, 56 | formEncType: true, 57 | formMethod: true, 58 | formNoValidate: types.BOOLEAN, 59 | formTarget: true, 60 | frameBorder: true, 61 | headers: true, 62 | height: true, 63 | hidden: types.BOOLEAN, 64 | href: true, 65 | hreflang: true, 66 | htmlFor: true, 67 | httpEquiv: true, 68 | icon: true, 69 | id: true, 70 | label: true, 71 | lang: true, 72 | list: true, 73 | loop: types.BOOLEAN, 74 | manifest: true, 75 | marginHeight: true, 76 | marginWidth: true, 77 | max: true, 78 | maxLength: true, 79 | media: true, 80 | mediaGroup: true, 81 | method: true, 82 | min: true, 83 | multiple: types.BOOLEAN, 84 | muted: types.BOOLEAN, 85 | name: true, 86 | noValidate: types.BOOLEAN, 87 | open: true, 88 | pattern: true, 89 | placeholder: true, 90 | poster: true, 91 | preload: true, 92 | radiogroup: true, 93 | readOnly: types.BOOLEAN, 94 | rel: true, 95 | required: types.BOOLEAN, 96 | role: true, 97 | rows: true, 98 | rowSpan: true, 99 | sandbox: true, 100 | scope: true, 101 | scrolling: true, 102 | seamless: types.BOOLEAN, 103 | selected: types.BOOLEAN, 104 | shape: true, 105 | size: true, 106 | sizes: true, 107 | span: true, 108 | spellcheck: true, 109 | src: true, 110 | srcdoc: true, 111 | srcset: true, 112 | start: true, 113 | step: true, 114 | style: true, 115 | tabIndex: true, 116 | target: true, 117 | title: true, 118 | type: true, 119 | useMap: true, 120 | value: true, 121 | width: true, 122 | wmode: true, 123 | 124 | /** 125 | * Non-standard Properties 126 | */ 127 | // autoCapitalize and autoCorrect are supported in Mobile Safari for 128 | // keyboard hints. 129 | autocapitalize: true, 130 | autocorrect: true, 131 | // itemProp, itemScope, itemType are for Microdata support. See 132 | // http://schema.org/docs/gs.html 133 | itemProp: true, 134 | itemScope: types.BOOLEAN, 135 | itemType: true, 136 | // property is supported for OpenGraph in meta tags. 137 | property: true 138 | }; 139 | 140 | /** 141 | * Properties to attributes mapping. 142 | * 143 | * The ones not here are simply converted to lower case. 144 | */ 145 | 146 | var attributeNames = { 147 | acceptCharset: 'accept-charset', 148 | className: 'class', 149 | htmlFor: 'for', 150 | httpEquiv: 'http-equiv' 151 | }; 152 | 153 | /** 154 | * Exports. 155 | */ 156 | 157 | module.exports = { 158 | attributeTypes: types, 159 | properties: properties, 160 | attributeNames: attributeNames 161 | }; -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | 2 | var VNode = require('virtual-dom/vnode/vnode'); 3 | var VText = require('virtual-dom/vnode/vtext'); 4 | var h = require('virtual-dom/h'); 5 | var svg = require('virtual-dom/virtual-hyperscript/svg'); 6 | var partial = require('vdom-thunk'); 7 | var assert = require('assert'); 8 | var toHTML = require('..'); 9 | 10 | describe('toHTML()', function () { 11 | it('should not render invalid virtual nodes', function () { 12 | assert.equal(toHTML(null), ''); 13 | assert.equal(toHTML('hi'), ''); 14 | }); 15 | 16 | it('should render simple HTML', function () { 17 | var node = new VNode('span'); 18 | assert.equal(toHTML(node), ''); 19 | }); 20 | 21 | it('should render inner text', function () { 22 | var node = new VNode('span', null, [new VText('hello')]); 23 | assert.equal(toHTML(node), 'hello'); 24 | }); 25 | 26 | it('should convert properties to attributes', function () { 27 | var node = new VNode('form', { 28 | className: 'login', 29 | acceptCharset: 'ISO-8859-1', 30 | accessKey: 'h' // prop to lower case 31 | }); 32 | assert.equal(toHTML(node), ''); 33 | }); 34 | 35 | it('should not render end tags for void elements', function () { 36 | var node = new VNode('input'); 37 | assert.equal(toHTML(node), ''); 38 | node = new VNode('br'); 39 | assert.equal(toHTML(node), '
'); 40 | }); 41 | 42 | it('should not render non-standard properties', function () { 43 | var node = new VNode('web-component', { 44 | 'ev-click': function () {}, 45 | 'random-prop': 'random!', 46 | toString: function () {} 47 | }); 48 | assert.equal(toHTML(node), ''); 49 | }); 50 | 51 | it('should not render null properties', function () { 52 | var node = new VNode('web-component', { 53 | 'className': null, 54 | 'id': null 55 | }); 56 | assert.equal(toHTML(node), ''); 57 | }); 58 | 59 | it('should render CSS for style property', function () { 60 | var node = new VNode('div', { 61 | style: { 62 | background: 'black', 63 | color: 'red' 64 | } 65 | }); 66 | assert.equal(toHTML(node), '
'); 67 | }); 68 | 69 | it('should convert style property to param-case', function () { 70 | var node = new VNode('div', { 71 | style: { 72 | background: 'black', 73 | color: 'red', 74 | zIndex: '1' 75 | } 76 | }); 77 | assert.equal(toHTML(node), '
'); 78 | }); 79 | 80 | it('should render style element correctly', function(){ 81 | var node = new VNode('style',{},[ 82 | new VText(".logo {background-image: url('/mylogo.png');}") 83 | ]); 84 | assert.equal(toHTML(node),""); 85 | }); 86 | 87 | it('should render data- attributes for dataset properties', function () { 88 | var node = new VNode('div', { 89 | dataset: { 90 | foo: 'bar', 91 | num: 42 92 | } 93 | }); 94 | assert.equal(toHTML(node), '
'); 95 | }); 96 | 97 | it('should convert data- attributes to param-case', function () { 98 | var node = new VNode('div', { 99 | dataset: { 100 | fooBar: 'baz' 101 | } 102 | }); 103 | assert.equal(toHTML(node), '
'); 104 | }); 105 | 106 | it('should render boolean properties', function () { 107 | var node = new VNode('input', { 108 | autofocus: true, 109 | disabled: false 110 | }); 111 | assert.equal(toHTML(node), ''); 112 | }); 113 | 114 | it('should render overloaded boolean properties', function () { 115 | var node = new VNode('a', { 116 | href: '/images/xxx.jpg', 117 | download: true 118 | }); 119 | assert.equal(toHTML(node), ''); 120 | node = new VNode('a', { 121 | href: '/images/xxx.jpg', 122 | download: 'sfw' 123 | }); 124 | assert.equal(toHTML(node), ''); 125 | }); 126 | 127 | it('should render any attributes', function () { 128 | var node = new VNode('circle', { 129 | attributes: { 130 | cx: "60", 131 | cy: "60", 132 | r: "50" 133 | } 134 | }); 135 | assert.equal(toHTML(node), ''); 136 | }); 137 | 138 | it('should not render null attributes', function () { 139 | var node = new VNode('circle', { 140 | attributes: { 141 | cx: "60", 142 | cy: "60", 143 | r: null 144 | } 145 | }); 146 | assert.equal(toHTML(node), ''); 147 | }); 148 | 149 | it('should render nested children', function () { 150 | var node = new VNode('div', null, [ 151 | new VNode('div', { id: 'a-div' }, [ 152 | new VNode('div', null, [new VText('HI!')]) 153 | ]), 154 | new VNode('div', { className: 'just-another-div' }) 155 | ]); 156 | assert.equal(toHTML(node), '
HI!
'); 157 | }); 158 | 159 | it('should encode attribute names/values and text contents', function () { 160 | var node = new VNode('div', { 161 | attributes: { 162 | 'data-"hi"': '"hello"' 163 | } 164 | }, [new VText('sup')]); 165 | assert.equal(toHTML(node), '
<span>sup</span>
'); 166 | }); 167 | 168 | it('should not encode script tag contents', function () { 169 | var node = new VNode('div', null, [ 170 | new VNode('script', null, [new VText('console.log("zzz");')]) 171 | ]); 172 | assert.equal(toHTML(node), '
'); 173 | }); 174 | 175 | it('should render `innerHTML`', function () { 176 | var node = new VNode('div', { 177 | innerHTML: 'sup' 178 | }); 179 | assert.equal(toHTML(node), '
sup
'); 180 | }); 181 | 182 | it('should render thunks', function () { 183 | var fn = function fn(text) { 184 | return new VNode('span', null, [new VText(text)]); 185 | }; 186 | var node = partial(fn, 'hello'); 187 | assert.equal(toHTML(node), 'hello'); 188 | }); 189 | 190 | it('should render widgets', function () { 191 | var Widget = function(text) { 192 | this.text = text; 193 | } 194 | Widget.prototype.type = 'Widget'; 195 | Widget.prototype.render = function() { 196 | return new VNode('span', null, [new VText(this.text)]); 197 | }; 198 | var node = new Widget('hello'); 199 | assert.equal(toHTML(node), 'hello'); 200 | }); 201 | 202 | it('should render tag in lowercase', function () { 203 | var node = new VNode('SPAN', null, [new VText('hello')]); 204 | assert.equal(toHTML(node), 'hello'); 205 | }); 206 | 207 | it('should render hyperscript', function () { 208 | var node = h('span', null, 'test'); 209 | assert.equal(toHTML(node), 'test'); 210 | }); 211 | 212 | it('should not encode script tag contents, when using hyperscript', function () { 213 | var node = h('div', null, [ 214 | h('script', null, 'console.log("zzz");') 215 | ]); 216 | assert.equal(toHTML(node), '
'); 217 | }); 218 | 219 | it('should not render end tags for void elements, when using hyperscript', function () { 220 | var node = h('input'); 221 | assert.equal(toHTML(node), ''); 222 | node = h('br'); 223 | assert.equal(toHTML(node), '
'); 224 | }); 225 | 226 | it('should preserve UTF-8 entities and escape special html characters', function () { 227 | var node = h('span', null, '测试&\"\'<>'); 228 | assert.equal(toHTML(node), '测试&"'<>'); 229 | }); 230 | 231 | it('should render svg with attributes in default namespace', function () { 232 | var node = svg('svg', { 233 | 'viewBox': '0 0 10 10' 234 | }); 235 | assert.equal(toHTML(node), ''); 236 | }); 237 | 238 | it('should render svg with attributes in non-default namespace', function () { 239 | var node = svg('use', { 240 | 'xlink:href': '/abc.jpg' 241 | }); 242 | assert.equal(toHTML(node), ''); 243 | }); 244 | 245 | it('should render input value', function () { 246 | var node = h('input', { type: 'submit', value: 'add' }); 247 | assert.equal(toHTML(node), ''); 248 | }); 249 | }); 250 | --------------------------------------------------------------------------------