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.
578 lines
13 KiB
JavaScript
578 lines
13 KiB
JavaScript
"use strict"
|
|
|
|
// Low-priority TODO: remove the dependency on the renderer here.
|
|
var o = require("../../ospec/ospec")
|
|
var components = require("../../test-utils/components")
|
|
var domMock = require("../../test-utils/domMock")
|
|
var throttleMocker = require("../../test-utils/throttleMock")
|
|
var mountRedraw = require("../../api/mount-redraw")
|
|
var coreRenderer = require("../../render/render")
|
|
var h = require("../../render/hyperscript")
|
|
|
|
o.spec("mount/redraw", function() {
|
|
var root, m, throttleMock, consoleMock, $document, errors
|
|
o.beforeEach(function() {
|
|
var $window = domMock()
|
|
consoleMock = {error: o.spy()}
|
|
throttleMock = throttleMocker()
|
|
root = $window.document.body
|
|
m = mountRedraw(coreRenderer($window), throttleMock.schedule, consoleMock)
|
|
$document = $window.document
|
|
errors = []
|
|
})
|
|
|
|
o.afterEach(function() {
|
|
o(consoleMock.error.calls.map(function(c) {
|
|
return c.args[0]
|
|
})).deepEquals(errors)
|
|
o(throttleMock.queueLength()).equals(0)
|
|
})
|
|
|
|
o("shouldn't error if there are no renderers", function() {
|
|
m.redraw()
|
|
throttleMock.fire()
|
|
})
|
|
|
|
o("schedules correctly", function() {
|
|
var spy = o.spy()
|
|
|
|
m.mount(root, {view: spy})
|
|
o(spy.callCount).equals(1)
|
|
m.redraw()
|
|
o(spy.callCount).equals(1)
|
|
throttleMock.fire()
|
|
o(spy.callCount).equals(2)
|
|
})
|
|
|
|
o("should run a single renderer entry", function() {
|
|
var spy = o.spy()
|
|
|
|
m.mount(root, {view: spy})
|
|
|
|
o(spy.callCount).equals(1)
|
|
|
|
m.redraw()
|
|
m.redraw()
|
|
m.redraw()
|
|
|
|
o(spy.callCount).equals(1)
|
|
throttleMock.fire()
|
|
o(spy.callCount).equals(2)
|
|
})
|
|
|
|
o("should run all renderer entries", 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})
|
|
m.mount(el2, {view: spy2})
|
|
m.mount(el3, {view: spy3})
|
|
|
|
m.redraw()
|
|
|
|
o(spy1.callCount).equals(1)
|
|
o(spy2.callCount).equals(1)
|
|
o(spy3.callCount).equals(1)
|
|
|
|
m.redraw()
|
|
|
|
o(spy1.callCount).equals(1)
|
|
o(spy2.callCount).equals(1)
|
|
o(spy3.callCount).equals(1)
|
|
|
|
throttleMock.fire()
|
|
|
|
o(spy1.callCount).equals(2)
|
|
o(spy2.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() {
|
|
var spy = o.spy()
|
|
|
|
m.mount(root, {view: spy})
|
|
o(spy.callCount).equals(1)
|
|
m.mount(root, null)
|
|
|
|
m.redraw()
|
|
|
|
o(spy.callCount).equals(1)
|
|
throttleMock.fire()
|
|
o(spy.callCount).equals(1)
|
|
})
|
|
|
|
o("should stop running after mount undefined", function() {
|
|
var spy = o.spy()
|
|
|
|
m.mount(root, {view: spy})
|
|
o(spy.callCount).equals(1)
|
|
m.mount(root, undefined)
|
|
|
|
m.redraw()
|
|
|
|
o(spy.callCount).equals(1)
|
|
throttleMock.fire()
|
|
o(spy.callCount).equals(1)
|
|
})
|
|
|
|
o("should stop running after mount no arg", function() {
|
|
var spy = o.spy()
|
|
|
|
m.mount(root, {view: spy})
|
|
o(spy.callCount).equals(1)
|
|
m.mount(root)
|
|
|
|
m.redraw()
|
|
|
|
o(spy.callCount).equals(1)
|
|
throttleMock.fire()
|
|
o(spy.callCount).equals(1)
|
|
})
|
|
|
|
o("should invoke remove callback on unmount", function() {
|
|
var spy = o.spy()
|
|
var onremove = o.spy()
|
|
|
|
m.mount(root, {view: spy, onremove: onremove})
|
|
o(spy.callCount).equals(1)
|
|
m.mount(root)
|
|
|
|
o(spy.callCount).equals(1)
|
|
o(onremove.callCount).equals(1)
|
|
})
|
|
|
|
o("should stop running after unsubscribe, even if it occurs after redraw is requested", function() {
|
|
var spy = o.spy()
|
|
|
|
m.mount(root, {view: spy})
|
|
o(spy.callCount).equals(1)
|
|
m.redraw()
|
|
m.mount(root)
|
|
|
|
o(spy.callCount).equals(1)
|
|
throttleMock.fire()
|
|
o(spy.callCount).equals(1)
|
|
})
|
|
|
|
o("does nothing on invalid unmount", function() {
|
|
var spy = o.spy()
|
|
|
|
m.mount(root, {view: spy})
|
|
o(spy.callCount).equals(1)
|
|
|
|
m.mount(null)
|
|
m.redraw()
|
|
throttleMock.fire()
|
|
o(spy.callCount).equals(2)
|
|
})
|
|
|
|
o("redraw.sync() redraws all roots synchronously", 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})
|
|
m.mount(el2, {view: spy2})
|
|
m.mount(el3, {view: spy3})
|
|
|
|
o(spy1.callCount).equals(1)
|
|
o(spy2.callCount).equals(1)
|
|
o(spy3.callCount).equals(1)
|
|
|
|
m.redraw.sync()
|
|
|
|
o(spy1.callCount).equals(2)
|
|
o(spy2.callCount).equals(2)
|
|
o(spy3.callCount).equals(2)
|
|
|
|
m.redraw.sync()
|
|
|
|
o(spy1.callCount).equals(3)
|
|
o(spy2.callCount).equals(3)
|
|
o(spy3.callCount).equals(3)
|
|
})
|
|
|
|
|
|
o("throws on invalid component", function() {
|
|
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){
|
|
o.spec(cmp.kind, function(){
|
|
var createComponent = cmp.create
|
|
|
|
o("throws on invalid `root` DOM node", function() {
|
|
o(function() {
|
|
m.mount(null, createComponent({view: function() {}}))
|
|
}).throws(TypeError)
|
|
})
|
|
|
|
o("renders into `root` synchronously", function() {
|
|
m.mount(root, createComponent({
|
|
view: function() {
|
|
return h("div")
|
|
}
|
|
}))
|
|
|
|
o(root.firstChild.nodeName).equals("DIV")
|
|
})
|
|
|
|
o("mounting null unmounts", function() {
|
|
m.mount(root, createComponent({
|
|
view: function() {
|
|
return h("div")
|
|
}
|
|
}))
|
|
|
|
m.mount(root, null)
|
|
|
|
o(root.childNodes.length).equals(0)
|
|
})
|
|
|
|
o("Mounting a second root doesn't cause the first one to redraw", function() {
|
|
var root1 = $document.createElement("div")
|
|
var root2 = $document.createElement("div")
|
|
var view = o.spy()
|
|
|
|
m.mount(root1, createComponent({view: view}))
|
|
o(view.callCount).equals(1)
|
|
|
|
m.mount(root2, createComponent({view: function() {}}))
|
|
|
|
o(view.callCount).equals(1)
|
|
|
|
throttleMock.fire()
|
|
o(view.callCount).equals(1)
|
|
})
|
|
|
|
o("redraws on events", function() {
|
|
var onupdate = o.spy()
|
|
var oninit = o.spy()
|
|
var onclick = o.spy()
|
|
var e = $document.createEvent("MouseEvents")
|
|
|
|
e.initEvent("click", true, true)
|
|
|
|
m.mount(root, createComponent({
|
|
view: function() {
|
|
return h("div", {
|
|
oninit: oninit,
|
|
onupdate: onupdate,
|
|
onclick: onclick,
|
|
})
|
|
}
|
|
}))
|
|
|
|
root.firstChild.dispatchEvent(e)
|
|
|
|
o(oninit.callCount).equals(1)
|
|
o(onupdate.callCount).equals(0)
|
|
|
|
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)
|
|
|
|
throttleMock.fire()
|
|
|
|
o(onupdate.callCount).equals(1)
|
|
})
|
|
|
|
o("redraws several mount points on events", function() {
|
|
var onupdate0 = o.spy()
|
|
var oninit0 = o.spy()
|
|
var onclick0 = o.spy()
|
|
var onupdate1 = o.spy()
|
|
var oninit1 = o.spy()
|
|
var onclick1 = o.spy()
|
|
|
|
var root1 = $document.createElement("div")
|
|
var root2 = $document.createElement("div")
|
|
var e = $document.createEvent("MouseEvents")
|
|
|
|
e.initEvent("click", true, true)
|
|
|
|
m.mount(root1, createComponent({
|
|
view: function() {
|
|
return h("div", {
|
|
oninit: oninit0,
|
|
onupdate: onupdate0,
|
|
onclick: onclick0,
|
|
})
|
|
}
|
|
}))
|
|
|
|
o(oninit0.callCount).equals(1)
|
|
o(onupdate0.callCount).equals(0)
|
|
|
|
m.mount(root2, createComponent({
|
|
view: function() {
|
|
return h("div", {
|
|
oninit: oninit1,
|
|
onupdate: onupdate1,
|
|
onclick: onclick1,
|
|
})
|
|
}
|
|
}))
|
|
|
|
o(oninit1.callCount).equals(1)
|
|
o(onupdate1.callCount).equals(0)
|
|
|
|
root1.firstChild.dispatchEvent(e)
|
|
o(onclick0.callCount).equals(1)
|
|
o(onclick0.this).equals(root1.firstChild)
|
|
|
|
throttleMock.fire()
|
|
|
|
o(onupdate0.callCount).equals(1)
|
|
o(onupdate1.callCount).equals(1)
|
|
|
|
root2.firstChild.dispatchEvent(e)
|
|
|
|
o(onclick1.callCount).equals(1)
|
|
o(onclick1.this).equals(root2.firstChild)
|
|
|
|
throttleMock.fire()
|
|
|
|
o(onupdate0.callCount).equals(2)
|
|
o(onupdate1.callCount).equals(2)
|
|
})
|
|
|
|
o("event handlers can skip redraw", function() {
|
|
var onupdate = o.spy(function(){
|
|
throw new Error("This shouldn't have been called")
|
|
})
|
|
var oninit = o.spy()
|
|
var e = $document.createEvent("MouseEvents")
|
|
|
|
e.initEvent("click", true, true)
|
|
|
|
m.mount(root, createComponent({
|
|
view: function() {
|
|
return h("div", {
|
|
oninit: oninit,
|
|
onupdate: onupdate,
|
|
onclick: function(e) {
|
|
e.redraw = false
|
|
}
|
|
})
|
|
}
|
|
}))
|
|
|
|
root.firstChild.dispatchEvent(e)
|
|
|
|
o(oninit.callCount).equals(1)
|
|
o(e.redraw).equals(false)
|
|
|
|
throttleMock.fire()
|
|
|
|
o(onupdate.callCount).equals(0)
|
|
o(e.redraw).equals(false)
|
|
})
|
|
|
|
o("redraws when the render function is run", function() {
|
|
var onupdate = o.spy()
|
|
var oninit = o.spy()
|
|
|
|
m.mount(root, createComponent({
|
|
view: function() {
|
|
return h("div", {
|
|
oninit: oninit,
|
|
onupdate: onupdate
|
|
})
|
|
}
|
|
}))
|
|
|
|
o(oninit.callCount).equals(1)
|
|
o(onupdate.callCount).equals(0)
|
|
|
|
m.redraw()
|
|
|
|
throttleMock.fire()
|
|
|
|
o(onupdate.callCount).equals(1)
|
|
})
|
|
|
|
o("emits errors correctly", function() {
|
|
errors = ["foo", "bar", "baz"]
|
|
var counter = -1
|
|
|
|
m.mount(root, createComponent({
|
|
view: function() {
|
|
var value = errors[counter++]
|
|
if (value != null) throw value
|
|
return null
|
|
}
|
|
}))
|
|
|
|
m.redraw()
|
|
throttleMock.fire()
|
|
m.redraw()
|
|
throttleMock.fire()
|
|
m.redraw()
|
|
throttleMock.fire()
|
|
})
|
|
})
|
|
})
|
|
})
|