diff --git a/api/tests/index.html b/api/tests/index.html index 71e26cdf..75d15bea 100644 --- a/api/tests/index.html +++ b/api/tests/index.html @@ -6,14 +6,18 @@ + + + + diff --git a/api/tests/test-router.js b/api/tests/test-router.js index dd47528a..d958ac2e 100644 --- a/api/tests/test-router.js +++ b/api/tests/test-router.js @@ -2,8 +2,7 @@ var o = require("../../ospec/ospec") var callAsync = require("../../test-utils/callAsync") -var pushStateMock = require("../../test-utils/pushStateMock") -var domMock = require("../../test-utils/domMock") +var browserMock = require("../../test-utils/browserMock") var m = require("../../render/hyperscript") var coreRenderer = require("../../render/render") @@ -18,13 +17,7 @@ o.spec("route", function() { var $window, root, redraw, route o.beforeEach(function() { - $window = {} - - var dom = domMock() - for (var key in dom) $window[key] = dom[key] - - var loc = pushStateMock(env) - for (var key in loc) $window[key] = loc[key] + $window = browserMock(env) root = $window.document.body diff --git a/bundler/lintdocs.js b/bundler/lintdocs.js index d8213019..f356fd12 100644 --- a/bundler/lintdocs.js +++ b/bundler/lintdocs.js @@ -88,15 +88,7 @@ function traverseDirectory(pathname, callback) { } //init mocks -var domMock = require("../test-utils/domMock")() -var xhrMock = require("../test-utils/xhrMock")() -var pushStateMock = require("../test-utils/pushStateMock")() - -global.window = {} -for (var key in domMock) if (!window[key]) window[key] = domMock[key] -for (var key in xhrMock) if (!window[key]) window[key] = xhrMock[key] -for (var key in pushStateMock) if (!window[key]) window[key] = pushStateMock[key] - +global.window = require("../test-utils/browserMock")() global.document = window.document global.m = require("../index") diff --git a/docs/fragment.md b/docs/fragment.md new file mode 100644 index 00000000..653fc828 --- /dev/null +++ b/docs/fragment.md @@ -0,0 +1,43 @@ +# fragment(html) + +- [API](#api) +- [How it works](#how-it-works) + +--- + +### API + +Generates a trusted HTML [vnode](vnodes.md) + +`vnode = m.fragment(attrs, children)` + +Argument | Type | Required | Description +----------- | -------------------- | -------- | --- +`attrs` | `Object` | Yes | A map of attributes +`children` | `Array` | Yes | A list of vnodes +**returns** | `Vnode` | | A fragment [vnode](vnodes.md) + +[How to read signatures](signatures.md) + +--- + +### How it works + +`m.fragment()` creates a [fragment vnode](vnodes.md) with attributes. It is meant for advanced use cases involving keys or lifecyle methods. + +Normally you can use simple arrays instead to denote a list of child nodes or a range of nodes within a node list: + +```javascript +var groupVisible = true + +m("ul", [ + m("li", "child 1"), + m("li", "child 2"), + groupVisible ? [ + m("li", "child 3"), + m("li", "child 4"), + ] : null +]) +``` + +There are a few benefits that come from using `m.fragment` instead of handwriting a vnode object structure: m.fragment creates [monomorphic objects](vnodes.md#monomorphic-objects), which have better performance characteristics than creating objects dynamically. In addition, using `m.fragment` makes your intentions clear, and it makes it less likely that you'll mistakenly set attributes on the vnode object rather than on the attrs object. \ No newline at end of file diff --git a/render/hyperscript.js b/render/hyperscript.js index 12fcf0be..5a815ebb 100644 --- a/render/hyperscript.js +++ b/render/hyperscript.js @@ -65,4 +65,7 @@ function hyperscript(selector) { return Vnode(selector, attrs && attrs.key, attrs || {}, Vnode.normalizeChildren(children), undefined, undefined) } +hyperscript.trust = require("./trust") +hyperscript.fragment = require("./fragment") + module.exports = hyperscript diff --git a/render/tests/index.html b/render/tests/index.html index 2b4e8542..3cc2da65 100644 --- a/render/tests/index.html +++ b/render/tests/index.html @@ -11,10 +11,12 @@ + + diff --git a/test-utils/browserMock.js b/test-utils/browserMock.js new file mode 100644 index 00000000..d667772a --- /dev/null +++ b/test-utils/browserMock.js @@ -0,0 +1,18 @@ +"use strict" + +var pushStateMock = require("./pushStateMock") +var domMock = require("./domMock") +var xhrMock = require("./xhrMock") + +module.exports = function(env) { + var $window = {} + + var dom = domMock() + var xhr = xhrMock() + var ps = pushStateMock(env) + for (var key in dom) if (!$window[key]) $window[key] = dom[key] + for (var key in xhr) if (!$window[key]) $window[key] = xhr[key] + for (var key in ps) if (!$window[key]) $window[key] = ps[key] + + return $window +} \ No newline at end of file diff --git a/tests/index.html b/tests/index.html new file mode 100644 index 00000000..91db3a04 --- /dev/null +++ b/tests/index.html @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/test-api.js b/tests/test-api.js new file mode 100644 index 00000000..584144f0 --- /dev/null +++ b/tests/test-api.js @@ -0,0 +1,197 @@ +"use strict" + +var o = require("../ospec/ospec") +var browserMock = require("../test-utils/browserMock") + +o.spec("api", function() { + var m + var FRAME_BUDGET = Math.floor(1000 / 60) + o.beforeEach(function() { + var mock = browserMock() + if (typeof global !== "undefined") global.window = mock, global.document = mock.document + m = require("../mithril") + }) + + o.spec("m", function() { + o("works", function() { + var vnode = m("div") + + o(vnode.tag).equals("div") + }) + }) + o.spec("m.version", function() { + o("works", function() { + o(typeof m.version).equals("string") + o(m.version.indexOf(".") > -1).equals(true) + o(/\d/.test(m.version)).equals(true) + }) + }) + o.spec("m.trust", function() { + o("works", function() { + var vnode = m.trust("
") + + o(vnode.tag).equals("<") + o(vnode.children).equals("
") + }) + }) + o.spec("m.fragment", function() { + o("works", function() { + var vnode = m.fragment({key: 123}, [m("div")]) + + o(vnode.tag).equals("[") + o(vnode.key).equals(123) + o(vnode.children.length).equals(1) + o(vnode.children[0].tag).equals("div") + }) + }) + o.spec("m.prop", function() { + o("works", function() { + var stream = m.prop(5) + var doubled = stream.run(function(value) {return value * 2}) + + o(doubled()).equals(10) + }) + o("m.prop.combine works", function() { + var added = m.prop.combine(function(a, b) {return a() + b()}, [ + m.prop(1), + m.prop(2), + ]) + + o(added()).equals(3) + }) + o("m.prop.merge works", function() { + var added = m.prop.merge([ + m.prop(1), + m.prop(2), + ]) + .run(function(values) {return values[0] + values[1]}) + + o(added()).equals(3) + }) + o("m.prop.reject works", function() { + var stream = m.prop.reject(new Error("error")) + + o(stream.error().message).equals("error") + }) + }) + o.spec("m.withAttr", function() { + o("works", function() { + var spy = o.spy() + var handler = m.withAttr("value", spy) + + handler({currentTarget: {value: 10}}) + + o(spy.args[0]).equals(10) + }) + }) + o.spec("m.parseQueryString", function() { + o("works", function() { + var query = m.parseQueryString("?a=1&b=2") + + o(query).deepEquals({a: 1, b: 2}) + }) + }) + o.spec("m.buildQueryString", function() { + o("works", function() { + var query = m.buildQueryString({a: 1, b: 2}) + + 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) { + var root = window.document.createElement("div") + m.route(root, "/a", { + "/:id": {view: function() {return m("div")}} + }) + + setTimeout(function() { + m.route.set("/b") + o(m.route.get()).equals("/b") + + done() + }, 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 + }) + }) + o.spec("m.jsonp", function() { + o("works", function() { + o(typeof m.jsonp).equals("function") // TODO improve + }) + }) +}) \ No newline at end of file