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