Make errors and their messages more accurate and helpful (#2536)
Also, I normalized them to all be sentences for consistency, and I moved the reentrancy check from `m.mount` to `m.render` to be a little more helpful. The router change during mounting is inconsequential and only to avoid the new modified error, and the change to the update loop is to send the original error if an error occurred while initializing the default route. (This is all around more useful anyways.) And while I was at it, I fixed an obscure bug with sync redraws.
This commit is contained in:
parent
475747800a
commit
b98ab29efd
14 changed files with 310 additions and 54 deletions
|
|
@ -4,17 +4,15 @@ var Vnode = require("../render/vnode")
|
||||||
|
|
||||||
module.exports = function(render, schedule, console) {
|
module.exports = function(render, schedule, console) {
|
||||||
var subscriptions = []
|
var subscriptions = []
|
||||||
var rendering = false
|
|
||||||
var pending = false
|
var pending = false
|
||||||
|
var offset = -1
|
||||||
|
|
||||||
function sync() {
|
function sync() {
|
||||||
if (rendering) throw new Error("Nested m.redraw.sync() call")
|
for (offset = 0; offset < subscriptions.length; offset += 2) {
|
||||||
rendering = true
|
try { render(subscriptions[offset], Vnode(subscriptions[offset + 1]), redraw) }
|
||||||
for (var i = 0; i < subscriptions.length; i += 2) {
|
|
||||||
try { render(subscriptions[i], Vnode(subscriptions[i + 1]), redraw) }
|
|
||||||
catch (e) { console.error(e) }
|
catch (e) { console.error(e) }
|
||||||
}
|
}
|
||||||
rendering = false
|
offset = -1
|
||||||
}
|
}
|
||||||
|
|
||||||
function redraw() {
|
function redraw() {
|
||||||
|
|
@ -31,13 +29,14 @@ module.exports = function(render, schedule, console) {
|
||||||
|
|
||||||
function mount(root, component) {
|
function mount(root, component) {
|
||||||
if (component != null && component.view == null && typeof component !== "function") {
|
if (component != null && component.view == null && typeof component !== "function") {
|
||||||
throw new TypeError("m.mount(element, component) expects a component, not a vnode")
|
throw new TypeError("m.mount expects a component, not a vnode.")
|
||||||
}
|
}
|
||||||
|
|
||||||
var index = subscriptions.indexOf(root)
|
var index = subscriptions.indexOf(root)
|
||||||
if (index >= 0) {
|
if (index >= 0) {
|
||||||
subscriptions.splice(index, 2)
|
subscriptions.splice(index, 2)
|
||||||
render(root, [], redraw)
|
if (index <= offset) offset -= 2
|
||||||
|
render(root, [])
|
||||||
}
|
}
|
||||||
|
|
||||||
if (component != null) {
|
if (component != null) {
|
||||||
|
|
|
||||||
|
|
@ -33,16 +33,16 @@ module.exports = function($window, mountRedraw) {
|
||||||
var SKIP = route.SKIP = {}
|
var SKIP = route.SKIP = {}
|
||||||
|
|
||||||
function route(root, defaultRoute, routes) {
|
function route(root, defaultRoute, routes) {
|
||||||
if (root == null) throw new Error("Ensure the DOM element that was passed to `m.route` is not undefined")
|
if (!root) throw new TypeError("DOM element being rendered to does not exist.")
|
||||||
// 0 = start
|
// 0 = start
|
||||||
// 1 = init
|
// 1 = init
|
||||||
// 2 = ready
|
// 2 = ready
|
||||||
var state = 0
|
var state = 0
|
||||||
|
|
||||||
var compiled = Object.keys(routes).map(function(route) {
|
var compiled = Object.keys(routes).map(function(route) {
|
||||||
if (route[0] !== "/") throw new SyntaxError("Routes must start with a `/`")
|
if (route[0] !== "/") throw new SyntaxError("Routes must start with a '/'.")
|
||||||
if ((/:([^\/\.-]+)(\.{3})?:/).test(route)) {
|
if ((/:([^\/\.-]+)(\.{3})?:/).test(route)) {
|
||||||
throw new SyntaxError("Route parameter names must be separated with either `/`, `.`, or `-`")
|
throw new SyntaxError("Route parameter names must be separated with either '/', '.', or '-'.")
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
route: route,
|
route: route,
|
||||||
|
|
@ -61,7 +61,7 @@ module.exports = function($window, mountRedraw) {
|
||||||
var defaultData = parsePathname(defaultRoute)
|
var defaultData = parsePathname(defaultRoute)
|
||||||
|
|
||||||
if (!compiled.some(function (i) { return i.check(defaultData) })) {
|
if (!compiled.some(function (i) { return i.check(defaultData) })) {
|
||||||
throw new ReferenceError("Default route doesn't match any known routes")
|
throw new ReferenceError("Default route doesn't match any known routes.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -87,8 +87,8 @@ module.exports = function($window, mountRedraw) {
|
||||||
|
|
||||||
assign(data.params, $window.history.state)
|
assign(data.params, $window.history.state)
|
||||||
|
|
||||||
function fail() {
|
function reject(e) {
|
||||||
if (path === defaultRoute) throw new Error("Could not resolve default route " + defaultRoute)
|
console.error(e)
|
||||||
setPath(defaultRoute, null, {replace: true})
|
setPath(defaultRoute, null, {replace: true})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -123,13 +123,17 @@ module.exports = function($window, mountRedraw) {
|
||||||
else if (payload.onmatch) {
|
else if (payload.onmatch) {
|
||||||
p.then(function () {
|
p.then(function () {
|
||||||
return payload.onmatch(data.params, path, matchedRoute)
|
return payload.onmatch(data.params, path, matchedRoute)
|
||||||
}).then(update, fail)
|
}).then(update, path === defaultRoute ? null : reject)
|
||||||
}
|
}
|
||||||
else update("div")
|
else update("div")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fail()
|
|
||||||
|
if (path === defaultRoute) {
|
||||||
|
throw new Error("Could not resolve default route " + defaultRoute + ".")
|
||||||
|
}
|
||||||
|
setPath(defaultRoute, null, {replace: true})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -157,12 +161,11 @@ module.exports = function($window, mountRedraw) {
|
||||||
$window.addEventListener("hashchange", resolveRoute, false)
|
$window.addEventListener("hashchange", resolveRoute, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
return mountRedraw.mount(root, {
|
mountRedraw.mount(root, {
|
||||||
onbeforeupdate: function() {
|
onbeforeupdate: function() {
|
||||||
state = state ? 2 : 1
|
state = state ? 2 : 1
|
||||||
return !(!state || sentinel === currentResolver)
|
return !(!state || sentinel === currentResolver)
|
||||||
},
|
},
|
||||||
oncreate: resolveRoute,
|
|
||||||
onremove: onremove,
|
onremove: onremove,
|
||||||
view: function() {
|
view: function() {
|
||||||
if (!state || sentinel === currentResolver) return
|
if (!state || sentinel === currentResolver) return
|
||||||
|
|
@ -172,6 +175,7 @@ module.exports = function($window, mountRedraw) {
|
||||||
return vnode
|
return vnode
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
resolveRoute()
|
||||||
}
|
}
|
||||||
route.set = function(path, data, options) {
|
route.set = function(path, data, options) {
|
||||||
if (lastUpdate != null) {
|
if (lastUpdate != null) {
|
||||||
|
|
|
||||||
|
|
@ -91,6 +91,30 @@ o.spec("mount/redraw", function() {
|
||||||
o(spy3.callCount).equals(2)
|
o(spy3.callCount).equals(2)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
o("should not redraw when mounting another root", function() {
|
||||||
|
var el1 = $document.createElement("div")
|
||||||
|
var el2 = $document.createElement("div")
|
||||||
|
var el3 = $document.createElement("div")
|
||||||
|
var spy1 = o.spy()
|
||||||
|
var spy2 = o.spy()
|
||||||
|
var spy3 = o.spy()
|
||||||
|
|
||||||
|
m.mount(el1, {view: spy1})
|
||||||
|
o(spy1.callCount).equals(1)
|
||||||
|
o(spy2.callCount).equals(0)
|
||||||
|
o(spy3.callCount).equals(0)
|
||||||
|
|
||||||
|
m.mount(el2, {view: spy2})
|
||||||
|
o(spy1.callCount).equals(1)
|
||||||
|
o(spy2.callCount).equals(1)
|
||||||
|
o(spy3.callCount).equals(0)
|
||||||
|
|
||||||
|
m.mount(el3, {view: spy3})
|
||||||
|
o(spy1.callCount).equals(1)
|
||||||
|
o(spy2.callCount).equals(1)
|
||||||
|
o(spy3.callCount).equals(1)
|
||||||
|
})
|
||||||
|
|
||||||
o("should stop running after mount null", function() {
|
o("should stop running after mount null", function() {
|
||||||
var spy = o.spy()
|
var spy = o.spy()
|
||||||
|
|
||||||
|
|
@ -204,6 +228,136 @@ o.spec("mount/redraw", function() {
|
||||||
o(function() { m.mount(root, {}) }).throws(TypeError)
|
o(function() { m.mount(root, {}) }).throws(TypeError)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
o("skips roots that were synchronously unsubscribed before they were visited", function() {
|
||||||
|
var calls = []
|
||||||
|
var root1 = $document.createElement("div")
|
||||||
|
var root2 = $document.createElement("div")
|
||||||
|
var root3 = $document.createElement("div")
|
||||||
|
|
||||||
|
m.mount(root1, {
|
||||||
|
onbeforeupdate: function() {
|
||||||
|
m.mount(root2, null)
|
||||||
|
},
|
||||||
|
view: function() { calls.push("root1") },
|
||||||
|
})
|
||||||
|
m.mount(root2, {view: function() { calls.push("root2") }})
|
||||||
|
m.mount(root3, {view: function() { calls.push("root3") }})
|
||||||
|
o(calls).deepEquals([
|
||||||
|
"root1", "root2", "root3",
|
||||||
|
])
|
||||||
|
|
||||||
|
m.redraw.sync()
|
||||||
|
o(calls).deepEquals([
|
||||||
|
"root1", "root2", "root3",
|
||||||
|
"root1", "root3",
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
o("keeps its place when synchronously unsubscribing previously visited roots", function() {
|
||||||
|
var calls = []
|
||||||
|
var root1 = $document.createElement("div")
|
||||||
|
var root2 = $document.createElement("div")
|
||||||
|
var root3 = $document.createElement("div")
|
||||||
|
|
||||||
|
m.mount(root1, {view: function() { calls.push("root1") }})
|
||||||
|
m.mount(root2, {
|
||||||
|
onbeforeupdate: function() {
|
||||||
|
m.mount(root1, null)
|
||||||
|
},
|
||||||
|
view: function() { calls.push("root2") },
|
||||||
|
})
|
||||||
|
m.mount(root3, {view: function() { calls.push("root3") }})
|
||||||
|
o(calls).deepEquals([
|
||||||
|
"root1", "root2", "root3",
|
||||||
|
])
|
||||||
|
|
||||||
|
m.redraw.sync()
|
||||||
|
o(calls).deepEquals([
|
||||||
|
"root1", "root2", "root3",
|
||||||
|
"root1", "root2", "root3",
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
o("keeps its place when synchronously unsubscribing previously visited roots in the face of errors", function() {
|
||||||
|
errors = ["fail"]
|
||||||
|
var calls = []
|
||||||
|
var root1 = $document.createElement("div")
|
||||||
|
var root2 = $document.createElement("div")
|
||||||
|
var root3 = $document.createElement("div")
|
||||||
|
|
||||||
|
m.mount(root1, {view: function() { calls.push("root1") }})
|
||||||
|
m.mount(root2, {
|
||||||
|
onbeforeupdate: function() {
|
||||||
|
m.mount(root1, null)
|
||||||
|
throw "fail"
|
||||||
|
},
|
||||||
|
view: function() { calls.push("root2") },
|
||||||
|
})
|
||||||
|
m.mount(root3, {view: function() { calls.push("root3") }})
|
||||||
|
o(calls).deepEquals([
|
||||||
|
"root1", "root2", "root3",
|
||||||
|
])
|
||||||
|
|
||||||
|
m.redraw.sync()
|
||||||
|
o(calls).deepEquals([
|
||||||
|
"root1", "root2", "root3",
|
||||||
|
"root1", "root3",
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
o("keeps its place when synchronously unsubscribing the current root", function() {
|
||||||
|
var calls = []
|
||||||
|
var root1 = $document.createElement("div")
|
||||||
|
var root2 = $document.createElement("div")
|
||||||
|
var root3 = $document.createElement("div")
|
||||||
|
|
||||||
|
m.mount(root1, {view: function() { calls.push("root1") }})
|
||||||
|
m.mount(root2, {
|
||||||
|
onbeforeupdate: function() {
|
||||||
|
try { m.mount(root2, null) } catch (e) { calls.push([e.constructor, e.message]) }
|
||||||
|
},
|
||||||
|
view: function() { calls.push("root2") },
|
||||||
|
})
|
||||||
|
m.mount(root3, {view: function() { calls.push("root3") }})
|
||||||
|
o(calls).deepEquals([
|
||||||
|
"root1", "root2", "root3",
|
||||||
|
])
|
||||||
|
|
||||||
|
m.redraw.sync()
|
||||||
|
o(calls).deepEquals([
|
||||||
|
"root1", "root2", "root3",
|
||||||
|
"root1", [TypeError, "Node is currently being rendered to and thus is locked."], "root2", "root3",
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
o("keeps its place when synchronously unsubscribing the current root in the face of an error", function() {
|
||||||
|
errors = [
|
||||||
|
[TypeError, "Node is currently being rendered to and thus is locked."],
|
||||||
|
]
|
||||||
|
var calls = []
|
||||||
|
var root1 = $document.createElement("div")
|
||||||
|
var root2 = $document.createElement("div")
|
||||||
|
var root3 = $document.createElement("div")
|
||||||
|
|
||||||
|
m.mount(root1, {view: function() { calls.push("root1") }})
|
||||||
|
m.mount(root2, {
|
||||||
|
onbeforeupdate: function() {
|
||||||
|
try { m.mount(root2, null) } catch (e) { throw [e.constructor, e.message] }
|
||||||
|
},
|
||||||
|
view: function() { calls.push("root2") },
|
||||||
|
})
|
||||||
|
m.mount(root3, {view: function() { calls.push("root3") }})
|
||||||
|
o(calls).deepEquals([
|
||||||
|
"root1", "root2", "root3",
|
||||||
|
])
|
||||||
|
|
||||||
|
m.redraw.sync()
|
||||||
|
o(calls).deepEquals([
|
||||||
|
"root1", "root2", "root3",
|
||||||
|
"root1", "root3",
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
components.forEach(function(cmp){
|
components.forEach(function(cmp){
|
||||||
o.spec(cmp.kind, function(){
|
o.spec(cmp.kind, function(){
|
||||||
var createComponent = cmp.create
|
var createComponent = cmp.create
|
||||||
|
|
|
||||||
|
|
@ -69,6 +69,9 @@ o.spec("route", function() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// In case it doesn't get reset
|
||||||
|
var realError = console.error
|
||||||
|
|
||||||
o.beforeEach(function() {
|
o.beforeEach(function() {
|
||||||
currentTest = nextID++
|
currentTest = nextID++
|
||||||
$window = browserMock(env)
|
$window = browserMock(env)
|
||||||
|
|
@ -79,11 +82,16 @@ o.spec("route", function() {
|
||||||
mountRedraw = apiMountRedraw(coreRenderer($window), throttleMock.schedule, console)
|
mountRedraw = apiMountRedraw(coreRenderer($window), throttleMock.schedule, console)
|
||||||
route = apiRouter($window, mountRedraw)
|
route = apiRouter($window, mountRedraw)
|
||||||
route.prefix = prefix
|
route.prefix = prefix
|
||||||
|
console.error = function() {
|
||||||
|
realError.call(this, new Error("Unexpected `console.error` call"))
|
||||||
|
realError.apply(this, arguments)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
o.afterEach(function() {
|
o.afterEach(function() {
|
||||||
o(throttleMock.queueLength()).equals(0)
|
o(throttleMock.queueLength()).equals(0)
|
||||||
currentTest = -1 // doesn't match any test
|
currentTest = -1 // doesn't match any test
|
||||||
|
console.error = realError
|
||||||
})
|
})
|
||||||
|
|
||||||
o("throws on invalid `root` DOM node", function() {
|
o("throws on invalid `root` DOM node", function() {
|
||||||
|
|
@ -1082,11 +1090,13 @@ o.spec("route", function() {
|
||||||
var matchCount = 0
|
var matchCount = 0
|
||||||
var renderCount = 0
|
var renderCount = 0
|
||||||
var spy = o.spy()
|
var spy = o.spy()
|
||||||
|
var error = new Error("error")
|
||||||
|
var errorSpy = console.error = o.spy()
|
||||||
|
|
||||||
var resolver = {
|
var resolver = {
|
||||||
onmatch: lock(function() {
|
onmatch: lock(function() {
|
||||||
matchCount++
|
matchCount++
|
||||||
return Promise.reject(new Error("error"))
|
return Promise.reject(error)
|
||||||
}),
|
}),
|
||||||
render: lock(function(vnode) {
|
render: lock(function(vnode) {
|
||||||
renderCount++
|
renderCount++
|
||||||
|
|
@ -1104,6 +1114,8 @@ o.spec("route", function() {
|
||||||
o(matchCount).equals(1)
|
o(matchCount).equals(1)
|
||||||
o(renderCount).equals(0)
|
o(renderCount).equals(0)
|
||||||
o(spy.callCount).equals(1)
|
o(spy.callCount).equals(1)
|
||||||
|
o(errorSpy.callCount).equals(1)
|
||||||
|
o(errorSpy.args[0]).equals(error)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,14 @@
|
||||||
|
|
||||||
### Upcoming...
|
### Upcoming...
|
||||||
|
|
||||||
|
*Note for later: release as semver-minor.*
|
||||||
|
|
||||||
|
- Improved error messages in multiple places. ([#2536](https://github.com/MithrilJS/mithril.js/pull/2536) [@isiahmeadows](https://github.com/isiahmeadows))
|
||||||
|
- The redraw reentrancy check was moved from `m.mount` to `m.render` and its error message was updated accordingly. ([#2536](https://github.com/MithrilJS/mithril.js/pull/2536) [@isiahmeadows](https://github.com/isiahmeadows))
|
||||||
|
- This is unlikely to break people because if you were to do it with `m.render` directly before now, you'd corrupt Mithril's internal representation and internal errors could occur as a result. Now, it just warns you.
|
||||||
|
- For a better debugging experience with `m.route` route resolvers, errors on `onmatch` in the default route are left unhandled and errors in `onmatch` in other routes are logged to the console before redirecting. ([#2536](https://github.com/MithrilJS/mithril.js/pull/2536) [@isiahmeadows](https://github.com/isiahmeadows))
|
||||||
|
- Bug fix with `m.redraw` where if you removed a root that was previously visited in the current redraw pass, it would lose its place and skip the next root.
|
||||||
|
|
||||||
-->
|
-->
|
||||||
|
|
||||||
### v2.0.4
|
### v2.0.4
|
||||||
|
|
|
||||||
|
|
@ -14,12 +14,12 @@ else window.o = m()
|
||||||
}
|
}
|
||||||
function o(subject, predicate) {
|
function o(subject, predicate) {
|
||||||
if (predicate === undefined) {
|
if (predicate === undefined) {
|
||||||
if (!isRunning()) throw new Error("Assertions should not occur outside test definitions")
|
if (!isRunning()) throw new Error("Assertions should not occur outside test definitions.")
|
||||||
return new Assert(subject)
|
return new Assert(subject)
|
||||||
} else {
|
} else {
|
||||||
if (isRunning()) throw new Error("Test definitions and hooks shouldn't be nested. To group tests use `o.spec()`")
|
if (isRunning()) throw new Error("Test definitions and hooks shouldn't be nested. To group tests, use 'o.spec()'.")
|
||||||
subject = String(subject)
|
subject = String(subject)
|
||||||
if (subject.charCodeAt(0) === 1) throw new Error("test names starting with '\\x01' are reserved for internal use")
|
if (subject.charCodeAt(0) === 1) throw new Error("test names starting with '\\x01' are reserved for internal use.")
|
||||||
ctx[unique(subject)] = new Task(predicate, ensureStackTrace(new Error))
|
ctx[unique(subject)] = new Task(predicate, ensureStackTrace(new Error))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -28,9 +28,9 @@ else window.o = m()
|
||||||
o.beforeEach = hook("\x01beforeEach")
|
o.beforeEach = hook("\x01beforeEach")
|
||||||
o.afterEach = hook("\x01afterEach")
|
o.afterEach = hook("\x01afterEach")
|
||||||
o.specTimeout = function (t) {
|
o.specTimeout = function (t) {
|
||||||
if (isRunning()) throw new Error("o.specTimeout() can only be called before o.run()")
|
if (isRunning()) throw new Error("o.specTimeout() can only be called before o.run().")
|
||||||
if (hasOwn.call(ctx, "\x01specTimeout")) throw new Error("A default timeout has already been defined in this context")
|
if (hasOwn.call(ctx, "\x01specTimeout")) throw new Error("A default timeout has already been defined in this context.")
|
||||||
if (typeof t !== "number") throw new Error("o.specTimeout() expects a number as argument")
|
if (typeof t !== "number") throw new Error("o.specTimeout() expects a number as argument.")
|
||||||
ctx["\x01specTimeout"] = t
|
ctx["\x01specTimeout"] = t
|
||||||
}
|
}
|
||||||
o.new = init
|
o.new = init
|
||||||
|
|
@ -139,7 +139,7 @@ else window.o = m()
|
||||||
// public API, may only be called once from use code (or after returned Promise resolution)
|
// public API, may only be called once from use code (or after returned Promise resolution)
|
||||||
function done(err) {
|
function done(err) {
|
||||||
if (!isDone) isDone = true
|
if (!isDone) isDone = true
|
||||||
else throw new Error("`" + arg + "()` should only be called once")
|
else throw new Error("'" + arg + "()' should only be called once.")
|
||||||
if (timeout === undefined) console.warn("# elapsed: " + Math.round(new Date - s) + "ms, expected under " + delay + "ms\n" + o.cleanStackTrace(task.err))
|
if (timeout === undefined) console.warn("# elapsed: " + Math.round(new Date - s) + "ms, expected under " + delay + "ms\n" + o.cleanStackTrace(task.err))
|
||||||
finalizeAsync(err)
|
finalizeAsync(err)
|
||||||
}
|
}
|
||||||
|
|
@ -161,7 +161,7 @@ else window.o = m()
|
||||||
}, Math.min(delay, 2147483647))
|
}, Math.min(delay, 2147483647))
|
||||||
}
|
}
|
||||||
function setDelay (t) {
|
function setDelay (t) {
|
||||||
if (typeof t !== "number") throw new Error("timeout() and o.timeout() expect a number as argument")
|
if (typeof t !== "number") throw new Error("timeout() and o.timeout() expect a number as argument.")
|
||||||
delay = t
|
delay = t
|
||||||
}
|
}
|
||||||
if (fn.length > 0) {
|
if (fn.length > 0) {
|
||||||
|
|
@ -169,7 +169,7 @@ else window.o = m()
|
||||||
arg = (body.match(/^(.+?)(?:\s|\/\*[\s\S]*?\*\/|\/\/.*?\n)*=>/) || body.match(/\((?:\s|\/\*[\s\S]*?\*\/|\/\/.*?\n)*(.+?)(?:\s|\/\*[\s\S]*?\*\/|\/\/.*?\n)*[,\)]/) || []).pop()
|
arg = (body.match(/^(.+?)(?:\s|\/\*[\s\S]*?\*\/|\/\/.*?\n)*=>/) || body.match(/\((?:\s|\/\*[\s\S]*?\*\/|\/\/.*?\n)*(.+?)(?:\s|\/\*[\s\S]*?\*\/|\/\/.*?\n)*[,\)]/) || []).pop()
|
||||||
if (body.indexOf(arg) === body.lastIndexOf(arg)) {
|
if (body.indexOf(arg) === body.lastIndexOf(arg)) {
|
||||||
var e = new Error
|
var e = new Error
|
||||||
e.stack = "`" + arg + "()` should be called at least once\n" + o.cleanStackTrace(task.err)
|
e.stack = "'" + arg + "()' should be called at least once\n" + o.cleanStackTrace(task.err)
|
||||||
throw e
|
throw e
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
|
@ -204,14 +204,14 @@ else window.o = m()
|
||||||
}
|
}
|
||||||
function unique(subject) {
|
function unique(subject) {
|
||||||
if (hasOwn.call(ctx, subject)) {
|
if (hasOwn.call(ctx, subject)) {
|
||||||
console.warn("A test or a spec named `" + subject + "` was already defined")
|
console.warn("A test or a spec named '" + subject + "' was already defined.")
|
||||||
while (hasOwn.call(ctx, subject)) subject += "*"
|
while (hasOwn.call(ctx, subject)) subject += "*"
|
||||||
}
|
}
|
||||||
return subject
|
return subject
|
||||||
}
|
}
|
||||||
function hook(name) {
|
function hook(name) {
|
||||||
return function(predicate) {
|
return function(predicate) {
|
||||||
if (ctx[name]) throw new Error("This hook should be defined outside of a loop or inside a nested test group:\n" + predicate)
|
if (ctx[name]) throw new Error(name.slice(1) + " should be defined outside of a loop or inside a nested test group.")
|
||||||
ctx[name] = new Task(predicate, ensureStackTrace(new Error))
|
ctx[name] = new Task(predicate, ensureStackTrace(new Error))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -317,7 +317,7 @@ else window.o = m()
|
||||||
try {return JSON.stringify(value)} catch (e) {return String(value)}
|
try {return JSON.stringify(value)} catch (e) {return String(value)}
|
||||||
}
|
}
|
||||||
function noTimeoutRightNow() {
|
function noTimeoutRightNow() {
|
||||||
throw new Error("o.timeout must be called snchronously from within a test definition or a hook")
|
throw new Error("o.timeout must be called snchronously from within a test definition or a hook.")
|
||||||
}
|
}
|
||||||
var colorCodes = {
|
var colorCodes = {
|
||||||
red: "31m",
|
red: "31m",
|
||||||
|
|
|
||||||
|
|
@ -532,7 +532,7 @@ o.spec("ospec", function() {
|
||||||
err = e
|
err = e
|
||||||
}
|
}
|
||||||
o(err instanceof Error).equals(true)
|
o(err instanceof Error).equals(true)
|
||||||
o(err.message).equals("`oodone()` should only be called once")
|
o(err.message).equals("'oodone()' should only be called once.")
|
||||||
})
|
})
|
||||||
oo.run(function(results) {
|
oo.run(function(results) {
|
||||||
o(results.length).equals(1)
|
o(results.length).equals(1)
|
||||||
|
|
@ -551,7 +551,7 @@ o.spec("ospec", function() {
|
||||||
err = e
|
err = e
|
||||||
}
|
}
|
||||||
o(err instanceof Error).equals(true)
|
o(err instanceof Error).equals(true)
|
||||||
o(err.message).equals("`oodone()` should only be called once")
|
o(err.message).equals("'oodone()' should only be called once.")
|
||||||
})
|
})
|
||||||
oo.run(function(results) {
|
oo.run(function(results) {
|
||||||
o(results.length).equals(1)
|
o(results.length).equals(1)
|
||||||
|
|
@ -570,7 +570,7 @@ o.spec("ospec", function() {
|
||||||
err = e
|
err = e
|
||||||
}
|
}
|
||||||
o(err instanceof Error).equals(true)
|
o(err instanceof Error).equals(true)
|
||||||
o(err.message).equals("`oodone()` should only be called once")
|
o(err.message).equals("'oodone()' should only be called once.")
|
||||||
})
|
})
|
||||||
oo.run(function(results) {
|
oo.run(function(results) {
|
||||||
o(results.length).equals(1)
|
o(results.length).equals(1)
|
||||||
|
|
@ -590,7 +590,7 @@ o.spec("ospec", function() {
|
||||||
err = e
|
err = e
|
||||||
}
|
}
|
||||||
o(err instanceof Error).equals(true)
|
o(err instanceof Error).equals(true)
|
||||||
o(err.message).equals("`oodone()` should only be called once")
|
o(err.message).equals("'oodone()' should only be called once.")
|
||||||
})
|
})
|
||||||
oo.run(function(results) {
|
oo.run(function(results) {
|
||||||
o(results.length).equals(1)
|
o(results.length).equals(1)
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ var assign = require("./assign")
|
||||||
// Returns `path` from `template` + `params`
|
// Returns `path` from `template` + `params`
|
||||||
module.exports = function(template, params) {
|
module.exports = function(template, params) {
|
||||||
if ((/:([^\/\.-]+)(\.{3})?:/).test(template)) {
|
if ((/:([^\/\.-]+)(\.{3})?:/).test(template)) {
|
||||||
throw new SyntaxError("Template parameter names *must* be separated")
|
throw new SyntaxError("Template parameter names must be separated by either a '/', '-', or '.'.")
|
||||||
}
|
}
|
||||||
if (params == null) return template
|
if (params == null) return template
|
||||||
var queryIndex = template.indexOf("?")
|
var queryIndex = template.indexOf("?")
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
"use strict"
|
"use strict"
|
||||||
/** @constructor */
|
/** @constructor */
|
||||||
var PromisePolyfill = function(executor) {
|
var PromisePolyfill = function(executor) {
|
||||||
if (!(this instanceof PromisePolyfill)) throw new Error("Promise must be called with `new`")
|
if (!(this instanceof PromisePolyfill)) throw new Error("Promise must be called with 'new'.")
|
||||||
if (typeof executor !== "function") throw new TypeError("executor must be a function")
|
if (typeof executor !== "function") throw new TypeError("executor must be a function.")
|
||||||
|
|
||||||
var self = this, resolvers = [], rejectors = [], resolveCurrent = handler(resolvers, true), rejectCurrent = handler(rejectors, false)
|
var self = this, resolvers = [], rejectors = [], resolveCurrent = handler(resolvers, true), rejectCurrent = handler(rejectors, false)
|
||||||
var instance = self._instance = {resolvers: resolvers, rejectors: rejectors}
|
var instance = self._instance = {resolvers: resolvers, rejectors: rejectors}
|
||||||
|
|
@ -12,7 +12,7 @@ var PromisePolyfill = function(executor) {
|
||||||
var then
|
var then
|
||||||
try {
|
try {
|
||||||
if (shouldAbsorb && value != null && (typeof value === "object" || typeof value === "function") && typeof (then = value.then) === "function") {
|
if (shouldAbsorb && value != null && (typeof value === "object" || typeof value === "function") && typeof (then = value.then) === "function") {
|
||||||
if (value === self) throw new TypeError("Promise can't be resolved w/ itself")
|
if (value === self) throw new TypeError("Promise can't be resolved with itself.")
|
||||||
executeOnce(then.bind(value))
|
executeOnce(then.bind(value))
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ module.exports = function($window) {
|
||||||
|
|
||||||
//sanity check to discourage people from doing `vnode.state = ...`
|
//sanity check to discourage people from doing `vnode.state = ...`
|
||||||
function checkState(vnode, original) {
|
function checkState(vnode, original) {
|
||||||
if (vnode.state !== original) throw new Error("`vnode.state` must not be modified")
|
if (vnode.state !== original) throw new Error("'vnode.state' must not be modified.")
|
||||||
}
|
}
|
||||||
|
|
||||||
//Note: the hook is passed as the `this` argument to allow proxying the
|
//Note: the hook is passed as the `this` argument to allow proxying the
|
||||||
|
|
@ -618,7 +618,7 @@ module.exports = function($window) {
|
||||||
var content = children[0].children
|
var content = children[0].children
|
||||||
if (vnode.dom.innerHTML !== content) vnode.dom.innerHTML = content
|
if (vnode.dom.innerHTML !== content) vnode.dom.innerHTML = content
|
||||||
}
|
}
|
||||||
else if (vnode.text != null || children != null && children.length !== 0) throw new Error("Child node of a contenteditable must be trusted")
|
else if (vnode.text != null || children != null && children.length !== 0) throw new Error("Child node of a contenteditable must be trusted.")
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -948,26 +948,33 @@ module.exports = function($window) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var currentDOM
|
||||||
|
|
||||||
return function(dom, vnodes, redraw) {
|
return function(dom, vnodes, redraw) {
|
||||||
if (!dom) throw new TypeError("Ensure the DOM element being passed to m.route/m.mount/m.render is not undefined.")
|
if (!dom) throw new TypeError("DOM element being rendered to does not exist.")
|
||||||
|
if (currentDOM != null && dom.contains(currentDOM)) {
|
||||||
|
throw new TypeError("Node is currently being rendered to and thus is locked.")
|
||||||
|
}
|
||||||
|
var prevRedraw = currentRedraw
|
||||||
|
var prevDOM = currentDOM
|
||||||
var hooks = []
|
var hooks = []
|
||||||
var active = activeElement()
|
var active = activeElement()
|
||||||
var namespace = dom.namespaceURI
|
var namespace = dom.namespaceURI
|
||||||
|
|
||||||
// First time rendering into a node clears it out
|
currentDOM = dom
|
||||||
if (dom.vnodes == null) dom.textContent = ""
|
currentRedraw = typeof redraw === "function" ? redraw : undefined
|
||||||
|
|
||||||
vnodes = Vnode.normalizeChildren(Array.isArray(vnodes) ? vnodes : [vnodes])
|
|
||||||
var prevRedraw = currentRedraw
|
|
||||||
try {
|
try {
|
||||||
currentRedraw = typeof redraw === "function" ? redraw : undefined
|
// First time rendering into a node clears it out
|
||||||
|
if (dom.vnodes == null) dom.textContent = ""
|
||||||
|
vnodes = Vnode.normalizeChildren(Array.isArray(vnodes) ? vnodes : [vnodes])
|
||||||
updateNodes(dom, dom.vnodes, vnodes, hooks, null, namespace === "http://www.w3.org/1999/xhtml" ? undefined : namespace)
|
updateNodes(dom, dom.vnodes, vnodes, hooks, null, namespace === "http://www.w3.org/1999/xhtml" ? undefined : namespace)
|
||||||
|
dom.vnodes = vnodes
|
||||||
|
// `document.activeElement` can return null: https://html.spec.whatwg.org/multipage/interaction.html#dom-document-activeelement
|
||||||
|
if (active != null && activeElement() !== active && typeof active.focus === "function") active.focus()
|
||||||
|
for (var i = 0; i < hooks.length; i++) hooks[i]()
|
||||||
} finally {
|
} finally {
|
||||||
currentRedraw = prevRedraw
|
currentRedraw = prevRedraw
|
||||||
|
currentDOM = prevDOM
|
||||||
}
|
}
|
||||||
dom.vnodes = vnodes
|
|
||||||
// `document.activeElement` can return null: https://html.spec.whatwg.org/multipage/interaction.html#dom-document-activeelement
|
|
||||||
if (active != null && activeElement() !== active && typeof active.focus === "function") active.focus()
|
|
||||||
for (var i = 0; i < hooks.length; i++) hooks[i]()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -329,4 +329,65 @@ o.spec("render", function() {
|
||||||
render(root.childNodes[0], [{tag: "g"}])
|
render(root.childNodes[0], [{tag: "g"}])
|
||||||
o(root.childNodes[0].childNodes[0].namespaceURI).equals("http://www.w3.org/2000/svg")
|
o(root.childNodes[0].childNodes[0].namespaceURI).equals("http://www.w3.org/2000/svg")
|
||||||
})
|
})
|
||||||
|
o("does not allow reentrant invocations", function() {
|
||||||
|
var thrown = []
|
||||||
|
function A() {
|
||||||
|
var updated = false
|
||||||
|
try {render(root, {tag: A})} catch (e) {thrown.push("construct")}
|
||||||
|
return {
|
||||||
|
oninit: function() {
|
||||||
|
try {render(root, {tag: A})} catch (e) {thrown.push("oninit")}
|
||||||
|
},
|
||||||
|
oncreate: function() {
|
||||||
|
try {render(root, {tag: A})} catch (e) {thrown.push("oncreate")}
|
||||||
|
},
|
||||||
|
onbeforeupdate: function() {
|
||||||
|
try {render(root, {tag: A})} catch (e) {thrown.push("onbeforeupdate")}
|
||||||
|
},
|
||||||
|
onupdate: function() {
|
||||||
|
if (updated) return
|
||||||
|
updated = true
|
||||||
|
try {render(root, {tag: A})} catch (e) {thrown.push("onupdate")}
|
||||||
|
},
|
||||||
|
onbeforeremove: function() {
|
||||||
|
try {render(root, {tag: A})} catch (e) {thrown.push("onbeforeremove")}
|
||||||
|
},
|
||||||
|
onremove: function() {
|
||||||
|
try {render(root, {tag: A})} catch (e) {thrown.push("onremove")}
|
||||||
|
},
|
||||||
|
view: function() {
|
||||||
|
try {render(root, {tag: A})} catch (e) {thrown.push("view")}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
render(root, {tag: A})
|
||||||
|
o(thrown).deepEquals([
|
||||||
|
"construct",
|
||||||
|
"oninit",
|
||||||
|
"view",
|
||||||
|
"oncreate",
|
||||||
|
])
|
||||||
|
render(root, {tag: A})
|
||||||
|
o(thrown).deepEquals([
|
||||||
|
"construct",
|
||||||
|
"oninit",
|
||||||
|
"view",
|
||||||
|
"oncreate",
|
||||||
|
"onbeforeupdate",
|
||||||
|
"view",
|
||||||
|
"onupdate",
|
||||||
|
])
|
||||||
|
render(root, [])
|
||||||
|
o(thrown).deepEquals([
|
||||||
|
"construct",
|
||||||
|
"oninit",
|
||||||
|
"view",
|
||||||
|
"oncreate",
|
||||||
|
"onbeforeupdate",
|
||||||
|
"view",
|
||||||
|
"onupdate",
|
||||||
|
"onbeforeremove",
|
||||||
|
"onremove",
|
||||||
|
])
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,11 @@ Vnode.normalizeChildren = function(input) {
|
||||||
// it, noticeably so.
|
// it, noticeably so.
|
||||||
for (var i = 1; i < input.length; i++) {
|
for (var i = 1; i < input.length; i++) {
|
||||||
if ((input[i] != null && input[i].key != null) !== isKeyed) {
|
if ((input[i] != null && input[i].key != null) !== isKeyed) {
|
||||||
throw new TypeError("Vnodes must either always have keys or never have keys!")
|
throw new TypeError(
|
||||||
|
isKeyed && (input[i] != null || typeof input[i] === "boolean")
|
||||||
|
? "In fragments, vnodes must either all have keys or none have keys. You may wish to consider using an explicit keyed empty fragment, m.fragment({key: ...}), instead of a hole."
|
||||||
|
: "In fragments, vnodes must either all have keys or none have keys."
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (var i = 0; i < input.length; i++) {
|
for (var i = 0; i < input.length; i++) {
|
||||||
|
|
|
||||||
|
|
@ -96,7 +96,7 @@ function Stream(value) {
|
||||||
function combine(fn, streams) {
|
function combine(fn, streams) {
|
||||||
var ready = streams.every(function(s) {
|
var ready = streams.every(function(s) {
|
||||||
if (s.constructor !== Stream)
|
if (s.constructor !== Stream)
|
||||||
throw new Error("Ensure that each item passed to stream.combine/stream.merge/lift is a stream")
|
throw new Error("Ensure that each item passed to stream.combine/stream.merge/lift is a stream.")
|
||||||
return s._state === "active"
|
return s._state === "active"
|
||||||
})
|
})
|
||||||
var stream = ready
|
var stream = ready
|
||||||
|
|
|
||||||
|
|
@ -304,6 +304,13 @@ module.exports = function(options) {
|
||||||
parentNode: null,
|
parentNode: null,
|
||||||
childNodes: [],
|
childNodes: [],
|
||||||
attributes: {},
|
attributes: {},
|
||||||
|
contains: function(child) {
|
||||||
|
while (child != null) {
|
||||||
|
if (child === this) return true
|
||||||
|
child = child.parentNode
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
},
|
||||||
get firstChild() {
|
get firstChild() {
|
||||||
return this.childNodes[0] || null
|
return this.childNodes[0] || null
|
||||||
},
|
},
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue