make setPath always async

This commit is contained in:
Leo Horie 2016-07-01 13:03:07 -04:00
parent 520d15a060
commit e8e0bae726
10 changed files with 154 additions and 61 deletions

View file

@ -38,7 +38,7 @@ Mithril's `config` method is now replaced by several lifecycle methods to improv
## Robustness
There are over 2600 assertions in the test suite, and tests cover even difficult-to-test things like `location.href`, `element.innerHTML` and `XMLHttpRequest` usage.
There are over 2700 assertions in the test suite, and tests cover even difficult-to-test things like `location.href`, `element.innerHTML` and `XMLHttpRequest` usage.
## Modularity

View file

@ -11,6 +11,7 @@
<script src="../../test-utils/domMock.js"></script>
<script src="../../test-utils/pushStateMock.js"></script>
<script src="../../util/stream.js"></script>
<script src="../../render/node.js"></script>
<script src="../../render/trust.js"></script>
<script src="../../render/hyperscript.js"></script>

View file

@ -1,6 +1,7 @@
"use strict"
var o = require("../../ospec/ospec")
var callAsync = require("../../test-utils/callAsync")
var pushStateMock = require("../../test-utils/pushStateMock")
var domMock = require("../../test-utils/domMock")
@ -28,7 +29,7 @@ o.spec("route", function() {
route = apiRouter($window, coreRenderer($window), redraw)
})
o("renders into `root`", function() {
o("renders into `root`", function(done) {
route(root, "/", {
"/" : {
view: function() {
@ -37,7 +38,11 @@ o.spec("route", function() {
}
})
o(root.firstChild.nodeName).equals("DIV")
callAsync(function() {
o(root.firstChild.nodeName).equals("DIV")
done()
})
})
o("redraws when render function is executed", function(done) {
@ -55,19 +60,21 @@ o.spec("route", function() {
}
})
o(oninit.callCount).equals(1)
callAsync(function() {
o(oninit.callCount).equals(1)
redraw.publish()
redraw.publish()
// Wrapped to give time for the rate-limited redraw to fire
setTimeout(function() {
o(onupdate.callCount).equals(1)
// Wrapped to give time for the rate-limited redraw to fire
setTimeout(function() {
o(onupdate.callCount).equals(1)
done()
}, FRAME_BUDGET)
done()
}, FRAME_BUDGET)
})
})
o("redraws on events", function(done, timeout) {
o("redraws on events", function(done) {
var onupdate = o.spy()
var oninit = o.spy()
var onclick = o.spy()
@ -87,21 +94,23 @@ o.spec("route", function() {
}
})
root.firstChild.dispatchEvent(e)
callAsync(function() {
root.firstChild.dispatchEvent(e)
o(oninit.callCount).equals(1)
o(oninit.callCount).equals(1)
o(onclick.callCount).equals(1)
o(onclick.this).equals(root.firstChild)
o(onclick.args[0].type).equals("click")
o(onclick.args[0].target).equals(root.firstChild)
o(onclick.callCount).equals(1)
o(onclick.this).equals(root.firstChild)
o(onclick.args[0].type).equals("click")
o(onclick.args[0].target).equals(root.firstChild)
// Wrapped to give time for the rate-limited redraw to fire
setTimeout(function() {
o(onupdate.callCount).equals(1)
// Wrapped to give time for the rate-limited redraw to fire
setTimeout(function() {
o(onupdate.callCount).equals(1)
done()
}, FRAME_BUDGET)
done()
}, FRAME_BUDGET)
})
})
o("event handlers can skip redraw", function(done) {
@ -126,19 +135,21 @@ o.spec("route", function() {
}
})
root.firstChild.dispatchEvent(e)
callAsync(function() {
root.firstChild.dispatchEvent(e)
o(oninit.callCount).equals(1)
o(oninit.callCount).equals(1)
// Wrapped to ensure no redraw fired
setTimeout(function() {
o(onupdate.callCount).equals(0)
// Wrapped to ensure no redraw fired
setTimeout(function() {
o(onupdate.callCount).equals(0)
done()
}, FRAME_BUDGET)
done()
}, FRAME_BUDGET)
})
})
o("changes location on route.link", function() {
o("changes location on route.link", function(done) {
var e = $window.document.createEvent("MouseEvents")
e.initEvent("click", true, true)
@ -161,10 +172,14 @@ o.spec("route", function() {
}
})
o($window.location.href).equals("http://localhost/?/")
callAsync(function() {
o($window.location.href).equals("http://localhost/?/")
root.firstChild.dispatchEvent(e)
root.firstChild.dispatchEvent(e)
o($window.location.href).equals("http://localhost/?/test")
o($window.location.href).equals("http://localhost/?/test")
done()
})
})
})

View file

@ -16,8 +16,8 @@ function initStream(stream, args) {
stream.constructor = createStream
stream._state = {id: guid++, value: undefined, error: undefined, state: 0, derive: undefined, recover: undefined, deps: {}, parents: [], errorStream: undefined, endStream: undefined}
stream.map = map, stream.ap = ap, stream.of = createStream
stream.valueOf = valueOf
stream.catch = doCatch
stream.valueOf = valueOf, stream.toJSON = toJSON
stream.run = run, stream.catch = doCatch
Object.defineProperties(stream, {
error: {get: function() {
@ -46,15 +46,18 @@ function initStream(stream, args) {
})
}
function updateStream(stream, value, error) {
if (!absorbStream(stream, value, false) && !absorbStream(stream, error, true)) {
updateState(stream, value, error)
for (var id in stream._state.deps) updateDependency(stream._state.deps[id], false)
finalize(stream)
}
updateState(stream, value, error)
for (var id in stream._state.deps) updateDependency(stream._state.deps[id], false)
finalize(stream)
}
function updateState(stream, value, error) {
error = unwrapError(value, error)
if (error !== undefined && typeof stream._state.recover === "function") {
try {updateValues(stream, stream._state.recover(), undefined)}
try {
var recovered = stream._state.recover()
if (recovered === HALT) return
updateValues(stream, recovered, undefined)
}
catch (e) {updateValues(stream, undefined, e)}
}
else updateValues(stream, value, error)
@ -73,7 +76,8 @@ function updateDependency(stream, mustSync) {
else {
try {
var value = state.derive()
if (!absorbStream(stream, value)) updateState(stream, value, undefined)
if (value === HALT) return
updateState(stream, value, undefined)
}
catch (e) {
updateState(stream, undefined, e)
@ -81,29 +85,28 @@ function updateDependency(stream, mustSync) {
}
}
}
function absorbStream(stream, value, isError) {
function unwrapError(value, error) {
if (value != null && value.constructor === createStream) {
if (value._state.state === 2) {
stream.end(true)
stream(value())
}
else if (value._state.error) stream.error(value.error())
else if (value._state.state === 0) return true
else if (!isError) stream(value())
else stream.error(value())
return true
if (value._state.error !== undefined) error = value._state.error
else error = unwrapError(value._state.value, value._state.error)
}
return false
return error
}
function finalize(stream) {
stream._state.changed = false
for (var id in stream._state.deps) stream._state.deps[id]._state.changed = false
}
function run(fn) {
var self = createStream(), stream = this
return initDependency(self, [stream], function() {
return absorb(self, fn(stream()))
}, undefined)
}
function doCatch(fn) {
var stream = this
var self = createStream(), stream = this
var derive = function() {return stream._state.value}
var recover = function() {return fn(stream._state.error)}
return initDependency(createStream(), [stream], derive, recover)
var recover = function() {return absorb(self, fn(stream._state.error))}
return initDependency(self, [stream], derive, recover)
}
function combine(fn, streams) {
return initDependency(createStream(), streams, function() {
@ -112,6 +115,16 @@ function combine(fn, streams) {
return fn.apply(this, streams.concat([streams.filter(changed)]))
}, undefined)
}
function absorb(stream, value) {
if (value != null && value.constructor === createStream) {
value.error.map(stream.error)
value.map(stream)
if (value._state.state === 0) return HALT
if (value._state.error) throw value._state.error
value = value._state.value
}
return value
}
function initDependency(dep, streams, derive, recover) {
var state = dep._state
state.derive = derive
@ -145,6 +158,7 @@ function unregisterStream(stream) {
function map(fn) {return combine(function(stream) {return fn(stream())}, [this])}
function ap(stream) {return combine(function(s1, s2) {return s1()(s2())}, [this, stream])}
function valueOf() {return this._state.value}
function toJSON() {return JSON.stringify(this._state.value)}
function active(stream) {return stream._state.state === 1}
function changed(stream) {return stream._state.changed}
function notEnded(stream) {return stream._state.state !== 2}
@ -154,7 +168,7 @@ function reject(e) {
stream.error(e)
return stream
}
var Stream = {stream: createStream, combine: combine, reject: reject}
var Stream = {stream: createStream, combine: combine, reject: reject, HALT: HALT}
function Node(tag, key, attrs, children, text, dom) {
return {tag: tag, key: key, attrs: attrs, children: children, text: text, dom: dom, domSize: undefined, state: {}, events: undefined, instance: undefined}
}
@ -782,7 +796,11 @@ var requestService = function($window) {
stream(response)
}
else stream.error(new Error(xhr.responseText))
else {
var error = new Error(xhr.responseText)
for (var key in response) error[key] = response[key]
stream.error(error)
}
}
catch (e) {
stream.error(e)
@ -892,6 +910,7 @@ var parseQueryString = function(string) {
}
var coreRouter = function($window) {
var supportsPushState = typeof $window.history.pushState === "function" && $window.location.protocol !== "file:"
var callAsync = typeof setImmediate === "function" ? setImmediate : setTimeout
var prefix = "#!"
function setPrefix(value) {prefix = value}
function normalize(fragment) {
@ -939,7 +958,7 @@ var coreRouter = function($window) {
if (supportsPushState) {
if (options && options.replace) $window.history.replaceState(null, null, prefix + path)
else $window.history.pushState(null, null, prefix + path)
$window.onpopstate()
callAsync($window.onpopstate)
}
else $window.location.href = prefix + path
}
@ -991,7 +1010,7 @@ var throttle = function(callback) {
}
else if (pending === null) {
pending = timeout(function() {
pending = 0
pending = null
callback()
last = new Date().getTime()
}, time - (now - last))

View file

@ -5,6 +5,7 @@ var parseQueryString = require("../querystring/parse")
module.exports = function($window) {
var supportsPushState = typeof $window.history.pushState === "function" && $window.location.protocol !== "file:"
var callAsync = typeof setImmediate === "function" ? setImmediate : setTimeout
var prefix = "#!"
function setPrefix(value) {prefix = value}
@ -60,7 +61,7 @@ module.exports = function($window) {
if (supportsPushState) {
if (options && options.replace) $window.history.replaceState(null, null, prefix + path)
else $window.history.pushState(null, null, prefix + path)
$window.onpopstate()
callAsync($window.onpopstate)
}
else $window.location.href = prefix + path
}

View file

@ -6,6 +6,7 @@
<body>
<script src="../../module/module.js"></script>
<script src="../../ospec/ospec.js"></script>
<script src="../../test-utils/callAsync.js"></script>
<script src="../../test-utils/parseURL.js"></script>
<script src="../../test-utils/pushStateMock.js"></script>

View file

@ -17,6 +17,13 @@ o.spec("Router.defineRoutes", function() {
onFail = o.spy()
})
o("calls onRouteChange on init", function() {
$window.location.href = prefix + "/a"
router.defineRoutes({"/a": {data: 1}}, onRouteChange, onFail)
o(onRouteChange.callCount).equals(1)
})
o("resolves to route", function() {
$window.location.href = prefix + "/test"
router.defineRoutes({"/test": {data: 1}}, onRouteChange, onFail)

View file

@ -1,6 +1,7 @@
"use strict"
var o = require("../../ospec/ospec")
var callAsync = require("../../test-utils/callAsync")
var pushStateMock = require("../../test-utils/pushStateMock")
var Router = require("../../router/router")
@ -17,6 +18,28 @@ o.spec("Router.setPath", function() {
onFail = o.spy()
})
o("setPath calls onRouteChange asynchronously", function(done) {
$window.location.href = prefix + "/a"
router.defineRoutes({"/a": {data: 1}, "/b": {data: 2}}, onRouteChange, onFail)
router.setPath("/b")
o(onRouteChange.callCount).equals(1)
callAsync(function() {
o(onRouteChange.callCount).equals(2)
done()
})
})
o("setPath calls onFail asynchronously", function(done) {
$window.location.href = prefix + "/a"
router.defineRoutes({"/a": {data: 1}, "/b": {data: 2}}, onRouteChange, onFail)
router.setPath("/c")
o(onFail.callCount).equals(0)
callAsync(function() {
o(onFail.callCount).equals(1)
done()
})
})
o("sets route via API", function() {
$window.location.href = prefix + "/test"
router.defineRoutes({"/test": {data: 1}, "/other/:a/:b...": {data: 2}}, onRouteChange, onFail)

View file

@ -13,6 +13,7 @@
<script src="../../test-utils/pushStateMock.js"></script>
<script src="../../test-utils/xhrMock.js"></script>
<script src="../../test-utils/domMock.js"></script>
<script src="test-callAsync.js"></script>
<script src="test-parseURL.js"></script>
<script src="test-pushStateMock.js"></script>
<script src="test-xhrMock.js"></script>

View file

@ -0,0 +1,25 @@
"use strict"
var o = require("../../ospec/ospec")
var callAsync = require("../../test-utils/callAsync")
o.spec("callAsync", function() {
o("works", function(done) {
var count = 0
callAsync(function() {
o(count).equals(1)
done()
})
count++
})
o("gets called before setTimeout", function(done) {
var timeout
callAsync(function() {
clearTimeout(timeout)
done()
})
timeout = setTimeout(function() {
throw new Error("callAsync was called too slow")
}, 0)
})
})