From 1f4b2cf49ab71531807080769928f3630780d0cf Mon Sep 17 00:00:00 2001 From: Isiah Meadows Date: Sun, 7 Jul 2019 18:28:43 -0400 Subject: [PATCH] Deservicify core (#2458) * De-servicify router (mostly) Still uses the redraw service, but it no longer has an intermediate service of its own. Also, did a *lot* of test deduplication in this. About 30-40% of the router service tests were already tested on the main router API instance itself. Bundle size decreased from 9560 to 9548 bytes min+gzip. * Merge `m.mount` + `m.redraw`, update router Simplifies the router and redraw mechanism, and makes it much easier to keep predictable. Bundle size down to 9433 bytes min+gzip, docs updated accordingly. * Make `mithril/render` just return the `m.render` function directly. * Deservicify `m.render`, revise `m.route` - You now have to use `mithril/render/render` directly if you want an implicit redraw function. (This will likely be going away in v3.) - Revise `m.route` to only `key` components * Add `redraw` to `m.render`, deservicify requests * Test error logging * Update docs + changelog [skip ci] --- api/mount-redraw.js | 50 +++ api/mount.js | 15 - api/redraw.js | 58 --- api/router.js | 186 ++++++-- api/tests/index.html | 8 +- api/tests/test-mount.js | 274 ----------- api/tests/test-mountRedraw.js | 424 ++++++++++++++++++ api/tests/test-redraw.js | 195 -------- api/tests/test-router.js | 202 ++++++++- api/tests/test-routerGetSet.js | 282 ++++++++++++ docs/change-log.md | 8 + docs/hyperscript.md | 2 +- docs/index.md | 2 +- docs/keys.md | 2 +- docs/mount.md | 30 ++ docs/render.md | 11 +- docs/route.md | 6 +- index.js | 19 +- mount-redraw.js | 5 + mount.js | 4 +- performance/test-perf.js | 2 +- redraw.js | 2 +- render/render.js | 26 +- render/tests/test-attributes.js | 12 +- render/tests/test-component.js | 2 +- render/tests/test-createElement.js | 2 +- render/tests/test-createFragment.js | 2 +- render/tests/test-createHTML.js | 2 +- render/tests/test-createNodes.js | 2 +- render/tests/test-createText.js | 2 +- render/tests/test-event.js | 5 +- render/tests/test-input.js | 2 +- .../tests/test-normalizeComponentChildren.js | 2 +- render/tests/test-onbeforeremove.js | 2 +- render/tests/test-onbeforeupdate.js | 2 +- render/tests/test-oncreate.js | 2 +- render/tests/test-oninit.js | 2 +- render/tests/test-onremove.js | 2 +- render/tests/test-onupdate.js | 2 +- .../test-render-hyperscript-integration.js | 2 +- render/tests/test-render.js | 2 +- render/tests/test-textContent.js | 2 +- render/tests/test-updateElement.js | 2 +- render/tests/test-updateFragment.js | 2 +- render/tests/test-updateHTML.js | 2 +- render/tests/test-updateNodes.js | 2 +- render/tests/test-updateNodesFuzzer.js | 2 +- render/tests/test-updateText.js | 2 +- request.js | 4 +- request/request.js | 6 +- request/tests/test-jsonp.js | 6 +- request/tests/test-request.js | 9 +- route.js | 4 +- router/router.js | 112 ----- router/tests/index.html | 26 -- router/tests/test-defineRoutes.js | 259 ----------- router/tests/test-getPath.js | 52 --- router/tests/test-setPath.js | 175 -------- test-utils/tests/test-throttleMock.js | 63 +-- test-utils/throttleMock.js | 13 +- 60 files changed, 1212 insertions(+), 1393 deletions(-) create mode 100644 api/mount-redraw.js delete mode 100644 api/mount.js delete mode 100644 api/redraw.js delete mode 100644 api/tests/test-mount.js create mode 100644 api/tests/test-mountRedraw.js delete mode 100644 api/tests/test-redraw.js create mode 100644 api/tests/test-routerGetSet.js create mode 100644 mount-redraw.js delete mode 100644 router/router.js delete mode 100644 router/tests/index.html delete mode 100644 router/tests/test-defineRoutes.js delete mode 100644 router/tests/test-getPath.js delete mode 100644 router/tests/test-setPath.js diff --git a/api/mount-redraw.js b/api/mount-redraw.js new file mode 100644 index 00000000..d51da61b --- /dev/null +++ b/api/mount-redraw.js @@ -0,0 +1,50 @@ +"use strict" + +var Vnode = require("../render/vnode") + +module.exports = function(render, schedule, console) { + var subscriptions = [] + var rendering = false + var pending = false + + function sync() { + if (rendering) throw new Error("Nested m.redraw.sync() call") + rendering = true + for (var i = 0; i < subscriptions.length; i += 2) { + try { render(subscriptions[i], Vnode(subscriptions[i + 1]), redraw) } + catch (e) { console.error(e) } + } + rendering = false + } + + function redraw() { + if (!pending) { + pending = true + schedule(function() { + pending = false + sync() + }) + } + } + + redraw.sync = sync + + function mount(root, component) { + if (component != null && component.view == null && typeof component !== "function") { + throw new TypeError("m.mount(element, component) expects a component, not a vnode") + } + + var index = subscriptions.indexOf(root) + if (index >= 0) { + subscriptions.splice(index, 2) + render(root, [], redraw) + } + + if (component != null) { + subscriptions.push(root, component) + render(root, Vnode(component), redraw) + } + } + + return {mount: mount, redraw: redraw} +} diff --git a/api/mount.js b/api/mount.js deleted file mode 100644 index ab8ecc13..00000000 --- a/api/mount.js +++ /dev/null @@ -1,15 +0,0 @@ -"use strict" - -var Vnode = require("../render/vnode") - -module.exports = function(redrawService) { - return function(root, component) { - if (component === null) { - redrawService.unsubscribe(root) - } else if (component.view == null && typeof component !== "function") { - throw new Error("m.mount(element, component) expects a component, not a vnode") - } else { - redrawService.subscribe(root, function() { return Vnode(component) }) - } - } -} diff --git a/api/redraw.js b/api/redraw.js deleted file mode 100644 index 235157f3..00000000 --- a/api/redraw.js +++ /dev/null @@ -1,58 +0,0 @@ -"use strict" - -var coreRenderer = require("../render/render") - -function throttle(callback) { - var pending = null - return function() { - if (pending === null) { - pending = requestAnimationFrame(function() { - pending = null - callback() - }) - } - } -} - -module.exports = function($window, throttleMock) { - var renderService = coreRenderer($window) - var subscriptions = [] - var rendering = false - - function run(sub) { - var vnode = sub.c(sub) - if (vnode !== sub) renderService.render(sub.k, vnode) - } - function subscribe(key, callback, onremove) { - var sub = {k: key, c: callback, r: onremove} - unsubscribe(key) - subscriptions.push(sub) - var vnode = sub.c(sub) - if (vnode !== sub) renderService.render(sub.k, vnode) - } - function unsubscribe(key) { - for (var i = 0; i < subscriptions.length; i++) { - var sub = subscriptions[i] - if (sub.k === key) { - subscriptions.splice(i, 1) - renderService.render(sub.k, []) - if (typeof sub.r === "function") sub.r() - break - } - } - } - function sync() { - if (rendering) throw new Error("Nested m.redraw.sync() call") - rendering = true - for (var i = 0; i < subscriptions.length; i++) { - try { run(subscriptions[i]) } - catch (e) { if (typeof console !== "undefined") console.error(e) } - } - rendering = false - } - - var redraw = (throttleMock || throttle)(sync) - redraw.sync = sync - renderService.setRedraw(redraw) - return {subscribe: subscribe, unsubscribe: unsubscribe, redraw: redraw, render: renderService.render} -} diff --git a/api/router.js b/api/router.js index a388b40c..3357e7f8 100644 --- a/api/router.js +++ b/api/router.js @@ -2,55 +2,153 @@ var Vnode = require("../render/vnode") var Promise = require("../promise/promise") -var coreRouter = require("../router/router") + +var buildPathname = require("../pathname/build") +var parsePathname = require("../pathname/parse") +var compileTemplate = require("../pathname/compileTemplate") +var assign = require("../pathname/assign") var sentinel = {} -module.exports = function($window, redrawService) { - var routeService = coreRouter($window) +module.exports = function($window, mountRedraw) { + var callAsync = typeof setImmediate === "function" ? setImmediate : setTimeout + var supportsPushState = typeof $window.history.pushState === "function" + var routePrefix = "#!" + var fireAsync + + function setPath(path, data, options) { + path = buildPathname(path, data) + if (fireAsync != null) { + fireAsync() + var state = options ? options.state : null + var title = options ? options.title : null + if (options && options.replace) $window.history.replaceState(state, title, routePrefix + path) + else $window.history.pushState(state, title, routePrefix + path) + } + else { + $window.location.href = routePrefix + path + } + } var currentResolver = sentinel, component, attrs, currentPath, lastUpdate var route = function(root, defaultRoute, routes) { if (root == null) throw new Error("Ensure the DOM element that was passed to `m.route` is not undefined") - var init = false - var bail = function(path) { - if (path !== defaultRoute) routeService.setPath(defaultRoute, null, {replace: true}) - else throw new Error("Could not resolve default route " + defaultRoute) + // 0 = start + // 1 = init + // 2 = ready + var state = 0 + + var compiled = Object.keys(routes).map(function(route) { + if (route[0] !== "/") throw new SyntaxError("Routes must start with a `/`") + if ((/:([^\/\.-]+)(\.{3})?:/).test(route)) { + throw new SyntaxError("Route parameter names must be separated with either `/`, `.`, or `-`") + } + return { + route: route, + component: routes[route], + check: compileTemplate(route), + } + }) + var onremove, asyncId + + fireAsync = null + + if (defaultRoute != null) { + var defaultData = parsePathname(defaultRoute) + + if (!compiled.some(function (i) { return i.check(defaultData) })) { + throw new ReferenceError("Default route doesn't match any known routes") + } } - function run() { - init = true - if (sentinel !== currentResolver) { - var vnode = Vnode(component, attrs.key, attrs) - if (currentResolver) vnode = currentResolver.render(vnode) + + function resolveRoute() { + // Consider the pathname holistically. The prefix might even be invalid, + // but that's not our problem. + var prefix = $window.location.hash + if (routePrefix[0] !== "#") { + prefix = $window.location.search + prefix + if (routePrefix[0] !== "?") { + prefix = $window.location.pathname + prefix + if (prefix[0] !== "/") prefix = "/" + prefix + } + } + // This seemingly useless `.concat()` speeds up the tests quite a bit, + // since the representation is consistently a relatively poorly + // optimized cons string. + var path = prefix.concat() + .replace(/(?:%[a-f89][a-f0-9])+/gim, decodeURIComponent) + .slice(routePrefix.length) + var data = parsePathname(path) + + assign(data.params, $window.history.state) + + for (var i = 0; i < compiled.length; i++) { + if (compiled[i].check(data)) { + var payload = compiled[i].component + var route = compiled[i].route + var update = lastUpdate = function(routeResolver, comp) { + if (update !== lastUpdate) return + component = comp != null && (typeof comp.view === "function" || typeof comp === "function")? comp : "div" + attrs = data.params, currentPath = path, lastUpdate = null + currentResolver = routeResolver.render ? routeResolver : null + if (state === 2) mountRedraw.redraw() + else { + state = 2 + mountRedraw.redraw.sync() + } + } + if (payload.view || typeof payload === "function") update({}, payload) + else { + if (payload.onmatch) { + Promise.resolve(payload.onmatch(data.params, path, route)).then(function(resolved) { + update(payload, resolved) + }, function () { + if (path === defaultRoute) throw new Error("Could not resolve default route " + defaultRoute) + setPath(defaultRoute, null, {replace: true}) + }) + } + else update(payload, "div") + } + return + } + } + + if (path === defaultRoute) throw new Error("Could not resolve default route " + defaultRoute) + setPath(defaultRoute, null, {replace: true}) + } + + if (supportsPushState) { + onremove = function() { + $window.removeEventListener("popstate", fireAsync, false) + } + $window.addEventListener("popstate", fireAsync = function() { + if (asyncId) return + asyncId = callAsync(function() { + asyncId = null + resolveRoute() + }) + }, false) + } else if (routePrefix[0] === "#") { + onremove = function() { + $window.removeEventListener("hashchange", resolveRoute, false) + } + $window.addEventListener("hashchange", resolveRoute, false) + } + + return mountRedraw.mount(root, { + onbeforeupdate: function() { + state = state ? 2 : 1 + return !(!state || sentinel === currentResolver) + }, + oncreate: resolveRoute, + onremove: onremove, + view: function() { + if (!state || sentinel === currentResolver) return + // Wrap in a fragment to preserve existing key semantics + var vnode = [Vnode(component, attrs.key, attrs)] + if (currentResolver) vnode = currentResolver.render(vnode[0]) return vnode - } - } - routeService.defineRoutes(routes, function(payload, params, path, route) { - var update = lastUpdate = function(routeResolver, comp) { - if (update !== lastUpdate) return - component = comp != null && (typeof comp.view === "function" || typeof comp === "function")? comp : "div" - attrs = params, currentPath = path, lastUpdate = null - currentResolver = routeResolver.render ? routeResolver : null - if (init) redrawService.redraw() - else { - init = true - redrawService.redraw.sync() - } - } - if (payload.view || typeof payload === "function") update({}, payload) - else { - if (payload.onmatch) { - Promise.resolve(payload.onmatch(params, path, route)).then(function(resolved) { - update(payload, resolved) - }, function () { bail(path) }) - } - else update(payload, "div") - } - }, bail, defaultRoute, function (unsubscribe) { - redrawService.subscribe(root, function(sub) { - sub.c = run - return sub - }, unsubscribe) + }, }) } route.set = function(path, data, options) { @@ -59,18 +157,18 @@ module.exports = function($window, redrawService) { options.replace = true } lastUpdate = null - routeService.setPath(path, data, options) + setPath(path, data, options) } route.get = function() {return currentPath} - route.prefix = function(prefix) {routeService.prefix = prefix} + route.prefix = function(prefix) {routePrefix = prefix} var link = function(options, vnode) { - vnode.dom.setAttribute("href", routeService.prefix + vnode.attrs.href) + vnode.dom.setAttribute("href", routePrefix + vnode.attrs.href) vnode.dom.onclick = function(e) { if (e.ctrlKey || e.metaKey || e.shiftKey || e.which === 2) return e.preventDefault() e.redraw = false var href = this.getAttribute("href") - if (href.indexOf(routeService.prefix) === 0) href = href.slice(routeService.prefix.length) + if (href.indexOf(routePrefix) === 0) href = href.slice(routePrefix.length) route.set(href, undefined, options) } } diff --git a/api/tests/index.html b/api/tests/index.html index e85bd4b7..e7d28bdb 100644 --- a/api/tests/index.html +++ b/api/tests/index.html @@ -24,13 +24,11 @@ - - - + - - + + diff --git a/api/tests/test-mount.js b/api/tests/test-mount.js deleted file mode 100644 index 7385e43d..00000000 --- a/api/tests/test-mount.js +++ /dev/null @@ -1,274 +0,0 @@ -"use strict" - -var o = require("../../ospec/ospec") -var components = require("../../test-utils/components") -var domMock = require("../../test-utils/domMock") -var throttleMocker = require("../../test-utils/throttleMock") - -var m = require("../../render/hyperscript") -var apiRedraw = require("../../api/redraw") -var apiMounter = require("../../api/mount") - -o.spec("mount", function() { - var $window, root, redrawService, mount, render, throttleMock - - o.beforeEach(function() { - $window = domMock() - throttleMock = throttleMocker() - - root = $window.document.body - redrawService = apiRedraw($window, throttleMock.throttle) - mount = apiMounter(redrawService) - render = redrawService.render - }) - - o.afterEach(function() { - o(throttleMock.queueLength()).equals(0) - }) - - o("throws on invalid component", function() { - var threw = false - try { - mount(root, {}) - } catch (e) { - threw = true - } - o(threw).equals(true) - }) - - components.forEach(function(cmp){ - o.spec(cmp.kind, function(){ - var createComponent = cmp.create - - o("throws on invalid `root` DOM node", function() { - var threw = false - try { - mount(null, createComponent({view: function() {}})) - } catch (e) { - threw = true - } - o(threw).equals(true) - }) - - o("renders into `root` synchronoulsy", function() { - mount(root, createComponent({ - view : function() { - return m("div") - } - })) - - o(root.firstChild.nodeName).equals("DIV") - }) - - o("mounting null unmounts", function() { - mount(root, createComponent({ - view : function() { - return m("div") - } - })) - - mount(root, null) - - o(root.childNodes.length).equals(0) - }) - - o("Mounting a second root doesn't cause the first one to redraw", function() { - var view = o.spy(function() { - return m("div") - }) - - render(root, [ - m("#child0"), - m("#child1") - ]) - - mount(root.childNodes[0], createComponent({ - view : view - })) - - o(root.firstChild.nodeName).equals("DIV") - o(view.callCount).equals(1) - - mount(root.childNodes[1], createComponent({ - view : function() { - return m("div") - } - })) - - 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 = $window.document.createEvent("MouseEvents") - - e.initEvent("click", true, true) - - mount(root, createComponent({ - view : function() { - return m("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 e = $window.document.createEvent("MouseEvents") - - e.initEvent("click", true, true) - - render(root, [ - m("#child0"), - m("#child1") - ]) - - mount(root.childNodes[0], createComponent({ - view : function() { - return m("div", { - oninit : oninit0, - onupdate : onupdate0, - onclick : onclick0, - }) - } - })) - - o(oninit0.callCount).equals(1) - o(onupdate0.callCount).equals(0) - - mount(root.childNodes[1], createComponent({ - view : function() { - return m("div", { - oninit : oninit1, - onupdate : onupdate1, - onclick : onclick1, - }) - } - })) - - o(oninit1.callCount).equals(1) - o(onupdate1.callCount).equals(0) - - root.childNodes[0].firstChild.dispatchEvent(e) - o(onclick0.callCount).equals(1) - o(onclick0.this).equals(root.childNodes[0].firstChild) - - throttleMock.fire() - - o(onupdate0.callCount).equals(1) - o(onupdate1.callCount).equals(1) - - root.childNodes[1].firstChild.dispatchEvent(e) - - o(onclick1.callCount).equals(1) - o(onclick1.this).equals(root.childNodes[1].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 = $window.document.createEvent("MouseEvents") - - e.initEvent("click", true, true) - - mount(root, createComponent({ - view: function() { - return m("div", { - oninit: oninit, - onupdate: onupdate, - onclick: function(e) { - e.redraw = false - } - }) - } - })) - - root.firstChild.dispatchEvent(e) - - o(oninit.callCount).equals(1) - - throttleMock.fire() - - o(onupdate.callCount).equals(0) - }) - - o("redraws when the render function is run", function() { - var onupdate = o.spy() - var oninit = o.spy() - - mount(root, createComponent({ - view : function() { - return m("div", { - oninit: oninit, - onupdate: onupdate - }) - } - })) - - o(oninit.callCount).equals(1) - o(onupdate.callCount).equals(0) - - redrawService.redraw() - - throttleMock.fire() - - o(onupdate.callCount).equals(1) - }) - - o("throttles", function() { - var i = 0 - mount(root, createComponent({view: function() {i++}})) - var before = i - - redrawService.redraw() - redrawService.redraw() - redrawService.redraw() - redrawService.redraw() - - var after = i - - throttleMock.fire() - - o(before).equals(1) // mounts synchronously - o(after).equals(1) // throttles rest - o(i).equals(2) - }) - }) - }) -}) diff --git a/api/tests/test-mountRedraw.js b/api/tests/test-mountRedraw.js new file mode 100644 index 00000000..0c52379c --- /dev/null +++ b/api/tests/test-mountRedraw.js @@ -0,0 +1,424 @@ +"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 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) + }) + + 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() + }) + }) + }) +}) diff --git a/api/tests/test-redraw.js b/api/tests/test-redraw.js deleted file mode 100644 index a04dbe84..00000000 --- a/api/tests/test-redraw.js +++ /dev/null @@ -1,195 +0,0 @@ -"use strict" - -var o = require("../../ospec/ospec") -var domMock = require("../../test-utils/domMock") -var throttleMocker = require("../../test-utils/throttleMock") -var apiRedraw = require("../../api/redraw") - -// Because Node doesn't have this. -if (typeof requestAnimationFrame !== "function") { - global.requestAnimationFrame = (function (delay, last) { - return function(callback) { - var elapsed = Date.now() - last - return setTimeout(function() { - callback() - last = Date.now() - }, delay - elapsed) - } - })(16, 0) -} - -o.spec("redrawService", function() { - var root, redrawService, $document - o.beforeEach(function() { - var $window = domMock() - root = $window.document.body - redrawService = apiRedraw($window) - $document = $window.document - }) - - o("shouldn't error if there are no renderers", function() { - redrawService.redraw() - }) - - o("honours throttleMock", function() { - var throttleMock = throttleMocker() - redrawService = apiRedraw(domMock(), throttleMock.throttle) - var spy = o.spy() - - redrawService.subscribe(root, spy) - - o(spy.callCount).equals(1) - - redrawService.redraw() - - o(spy.callCount).equals(1) - - throttleMock.fire() - - o(spy.callCount).equals(2) - }) - - o("should run a single renderer entry", function(done) { - var spy = o.spy() - - redrawService.subscribe(root, spy) - - o(spy.callCount).equals(1) - - redrawService.redraw() - redrawService.redraw() - redrawService.redraw() - - o(spy.callCount).equals(1) - setTimeout(function() { - o(spy.callCount).equals(2) - - done() - }, 20) - }) - - o("should run all renderer entries", function(done) { - 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() - - redrawService.subscribe(el1, spy1) - redrawService.subscribe(el2, spy2) - redrawService.subscribe(el3, spy3) - - redrawService.redraw() - - o(spy1.callCount).equals(1) - o(spy2.callCount).equals(1) - o(spy3.callCount).equals(1) - - redrawService.redraw() - - o(spy1.callCount).equals(1) - o(spy2.callCount).equals(1) - o(spy3.callCount).equals(1) - - setTimeout(function() { - o(spy1.callCount).equals(2) - o(spy2.callCount).equals(2) - o(spy3.callCount).equals(2) - - done() - }, 20) - }) - - o("should stop running after unsubscribe", function(done) { - var spy = o.spy() - - redrawService.subscribe(root, spy) - o(spy.callCount).equals(1) - redrawService.unsubscribe(root) - - redrawService.redraw() - - o(spy.callCount).equals(1) - setTimeout(function() { - o(spy.callCount).equals(1) - - done() - }, 20) - }) - - o("should invoke remove callback on unsubscribe", function() { - var spy = o.spy() - var onremove = o.spy() - - redrawService.subscribe(root, spy, onremove) - o(spy.callCount).equals(1) - redrawService.unsubscribe(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(done) { - var spy = o.spy() - - redrawService.subscribe(root, spy) - o(spy.callCount).equals(1) - - redrawService.redraw() - - redrawService.unsubscribe(root) - - o(spy.callCount).equals(1) - setTimeout(function() { - o(spy.callCount).equals(1) - - done() - }, 20) - }) - - o("does nothing on invalid unsubscribe", function(done) { - var spy = o.spy() - - redrawService.subscribe(root, spy) - o(spy.callCount).equals(1) - - redrawService.unsubscribe(null) - redrawService.redraw() - - setTimeout(function() { - o(spy.callCount).equals(2) - - done() - }, 20) - }) - - 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() - - redrawService.subscribe(el1, spy1) - redrawService.subscribe(el2, spy2) - redrawService.subscribe(el3, spy3) - - o(spy1.callCount).equals(1) - o(spy2.callCount).equals(1) - o(spy3.callCount).equals(1) - - redrawService.redraw.sync() - - o(spy1.callCount).equals(2) - o(spy2.callCount).equals(2) - o(spy3.callCount).equals(2) - - redrawService.redraw.sync() - - o(spy1.callCount).equals(3) - o(spy2.callCount).equals(3) - o(spy3.callCount).equals(3) - }) -}) diff --git a/api/tests/test-router.js b/api/tests/test-router.js index 1f7fc1d9..b0d36bab 100644 --- a/api/tests/test-router.js +++ b/api/tests/test-router.js @@ -1,13 +1,15 @@ "use strict" +// Low-priority TODO: remove the dependency on the renderer here. var o = require("../../ospec/ospec") var callAsync = require("../../test-utils/callAsync") var browserMock = require("../../test-utils/browserMock") var throttleMocker = require("../../test-utils/throttleMock") var m = require("../../render/hyperscript") +var coreRenderer = require("../../render/render") var callAsync = require("../../test-utils/callAsync") -var apiRedraw = require("../../api/redraw") +var apiMountRedraw = require("../../api/mount-redraw") var apiRouter = require("../../api/router") var Promise = require("../../promise/promise") @@ -15,7 +17,7 @@ o.spec("route", function() { void [{protocol: "http:", hostname: "localhost"}, {protocol: "file:", hostname: "/"}].forEach(function(env) { void ["#", "?", "", "#!", "?!", "/foo"].forEach(function(prefix) { o.spec("using prefix `" + prefix + "` starting on " + env.protocol + "//" + env.hostname, function() { - var $window, root, redrawService, route, throttleMock + var $window, root, mountRedraw, route, throttleMock o.beforeEach(function() { $window = browserMock(env) @@ -23,8 +25,8 @@ o.spec("route", function() { root = $window.document.body - redrawService = apiRedraw($window, throttleMock.throttle) - route = apiRouter($window, redrawService) + mountRedraw = apiMountRedraw(coreRenderer($window), throttleMock.schedule, console) + route = apiRouter($window, mountRedraw) route.prefix(prefix) }) @@ -55,6 +57,165 @@ o.spec("route", function() { o(root.firstChild.nodeName).equals("DIV") }) + o("resolves to route w/ escaped unicode", function() { + $window.location.href = prefix + "/%C3%B6?%C3%B6=%C3%B6" + route(root, "/ö", { + "/ö" : { + view: function() { + return m("div") + } + } + }) + + o(root.firstChild.nodeName).equals("DIV") + }) + + o("resolves to route w/ unicode", function() { + $window.location.href = prefix + "/ö?ö=ö" + route(root, "/ö", { + "/ö" : { + view: function() { + return JSON.stringify(route.param()) + " " + + route.get() + } + } + }) + + o(root.firstChild.nodeValue).equals('{"ö":"ö"} /ö?ö=ö') + }) + + o("handles parameterized route", function() { + $window.location.href = prefix + "/test/x" + route(root, "/test/:a", { + "/test/:a" : { + view: function(vnode) { + return JSON.stringify(route.param()) + " " + + JSON.stringify(vnode.attrs) + " " + + route.get() + } + } + }) + + o(root.firstChild.nodeValue).equals( + '{"a":"x"} {"a":"x"} /test/x' + ) + }) + + o("handles multi-parameterized route", function() { + $window.location.href = prefix + "/test/x/y" + route(root, "/test/:a/:b", { + "/test/:a/:b" : { + view: function(vnode) { + return JSON.stringify(route.param()) + " " + + JSON.stringify(vnode.attrs) + " " + + route.get() + } + } + }) + + o(root.firstChild.nodeValue).equals( + '{"a":"x","b":"y"} {"a":"x","b":"y"} /test/x/y' + ) + }) + + o("handles rest parameterized route", function() { + $window.location.href = prefix + "/test/x/y" + route(root, "/test/:a...", { + "/test/:a..." : { + view: function(vnode) { + return JSON.stringify(route.param()) + " " + + JSON.stringify(vnode.attrs) + " " + + route.get() + } + } + }) + + o(root.firstChild.nodeValue).equals( + '{"a":"x/y"} {"a":"x/y"} /test/x/y' + ) + }) + + o("handles route with search", function() { + $window.location.href = prefix + "/test?a=b&c=d" + route(root, "/test", { + "/test" : { + view: function(vnode) { + return JSON.stringify(route.param()) + " " + + JSON.stringify(vnode.attrs) + " " + + route.get() + } + } + }) + + o(root.firstChild.nodeValue).equals( + '{"a":"b","c":"d"} {"a":"b","c":"d"} /test?a=b&c=d' + ) + }) + + o("redirects to default route if no match", function(done) { + $window.location.href = prefix + "/test" + route(root, "/other", { + "/other": { + view: function(vnode) { + return JSON.stringify(route.param()) + " " + + JSON.stringify(vnode.attrs) + " " + + route.get() + } + } + }) + + callAsync(function() { + o(root.firstChild.nodeValue).equals("{} {} /other") + done() + }) + }) + + o("handles out of order routes", function() { + $window.location.href = prefix + "/z/y/x" + + route(root, "/z/y/x", { + "/z/y/x": { + view: function() { return "1" }, + }, + "/:a...": { + view: function() { return "2" }, + }, + }) + + o(root.firstChild.nodeValue).equals("1") + }) + + o("handles reverse out of order routes", function() { + $window.location.href = prefix + "/z/y/x" + + route(root, "/z/y/x", { + "/:a...": { + view: function() { return "2" }, + }, + "/z/y/x": { + view: function() { return "1" }, + }, + }) + + o(root.firstChild.nodeValue).equals("2") + }) + + o("resolves to route on fallback mode", function() { + $window.location.href = "file://" + prefix + "/test" + + route(root, "/test", { + "/test" : { + view: function(vnode) { + return JSON.stringify(route.param()) + " " + + JSON.stringify(vnode.attrs) + " " + + route.get() + } + } + }) + + o(root.firstChild.nodeValue).equals("{} {} /test") + }) + o("routed mount points only redraw asynchronously (POJO component)", function() { var view = o.spy() @@ -63,7 +224,7 @@ o.spec("route", function() { o(view.callCount).equals(1) - redrawService.redraw() + mountRedraw.redraw() o(view.callCount).equals(1) @@ -83,7 +244,7 @@ o.spec("route", function() { o(view.callCount).equals(1) - redrawService.redraw() + mountRedraw.redraw() o(view.callCount).equals(1) @@ -102,7 +263,7 @@ o.spec("route", function() { o(view.callCount).equals(1) - redrawService.redraw() + mountRedraw.redraw() o(view.callCount).equals(1) @@ -124,8 +285,7 @@ o.spec("route", function() { o(root.firstChild.nodeName).equals("DIV") - // unsubscribe as if via `m.mount(root)` - redrawService.unsubscribe(root) + mountRedraw.mount(root) o(root.childNodes.length).equals(0) }) @@ -192,7 +352,7 @@ o.spec("route", function() { o(oninit.callCount).equals(1) - redrawService.redraw() + mountRedraw.redraw() throttleMock.fire() o(onupdate.callCount).equals(1) @@ -259,8 +419,6 @@ o.spec("route", function() { root.firstChild.dispatchEvent(e) - o(e.redraw).notEquals(false) - // Wrapped to ensure no redraw fired callAsync(function() { o(onupdate.callCount).equals(0) @@ -550,7 +708,7 @@ o.spec("route", function() { }) }) - o("changing `vnode.key` in `render` resets the component", function(done){ + o("changing `key` param resets the component", function(done){ var oninit = o.spy() var Component = { oninit: oninit, @@ -560,9 +718,7 @@ o.spec("route", function() { } $window.location.href = prefix + "/abc" route(root, "/abc", { - "/:id": {render: function(vnode) { - return m(Component, {key: vnode.attrs.id}) - }} + "/:key": Component, }) callAsync(function() { o(oninit.callCount).equals(1) @@ -661,7 +817,7 @@ o.spec("route", function() { o(matchCount).equals(1) o(renderCount).equals(1) - redrawService.redraw() + mountRedraw.redraw() throttleMock.fire() o(matchCount).equals(1) @@ -697,7 +853,7 @@ o.spec("route", function() { o(matchCount).equals(1) o(renderCount).equals(1) - redrawService.redraw() + mountRedraw.redraw() throttleMock.fire() o(matchCount).equals(1) @@ -1022,7 +1178,7 @@ o.spec("route", function() { o(view.callCount).equals(1) o(onmatch.callCount).equals(1) - redrawService.redraw() + mountRedraw.redraw() throttleMock.fire() o(view.callCount).equals(2) @@ -1286,10 +1442,10 @@ o.spec("route", function() { }) var before = i - redrawService.redraw() - redrawService.redraw() - redrawService.redraw() - redrawService.redraw() + mountRedraw.redraw() + mountRedraw.redraw() + mountRedraw.redraw() + mountRedraw.redraw() var after = i throttleMock.fire() diff --git a/api/tests/test-routerGetSet.js b/api/tests/test-routerGetSet.js new file mode 100644 index 00000000..cd23ae20 --- /dev/null +++ b/api/tests/test-routerGetSet.js @@ -0,0 +1,282 @@ +"use strict" + +// Low-priority TODO: remove the dependency on the renderer here. +var o = require("../../ospec/ospec") +var callAsync = require("../../test-utils/callAsync") +var browserMock = require("../../test-utils/browserMock") +var throttleMocker = require("../../test-utils/throttleMock") + +var callAsync = require("../../test-utils/callAsync") +var apiMountRedraw = require("../../api/mount-redraw") +var coreRenderer = require("../../render/render") +var apiRouter = require("../../api/router") + +o.spec("route.get/route.set", function() { + void [{protocol: "http:", hostname: "localhost"}, {protocol: "file:", hostname: "/"}].forEach(function(env) { + void ["#", "?", "", "#!", "?!", "/foo"].forEach(function(prefix) { + o.spec("using prefix `" + prefix + "` starting on " + env.protocol + "//" + env.hostname, function() { + var $window, root, mountRedraw, route, throttleMock + + o.beforeEach(function() { + $window = browserMock(env) + throttleMock = throttleMocker() + + root = $window.document.body + + mountRedraw = apiMountRedraw(coreRenderer($window), throttleMock.schedule, console) + route = apiRouter($window, mountRedraw) + route.prefix(prefix) + }) + + o.afterEach(function() { + o(throttleMock.queueLength()).equals(0) + }) + + o("gets route", function() { + $window.location.href = prefix + "/test" + route(root, "/test", {"/test": {view: function() {}}}) + + o(route.get()).equals("/test") + }) + + o("gets route w/ params", function() { + $window.location.href = prefix + "/other/x/y/z?c=d#e=f" + + route(root, "/other/x/y/z?c=d#e=f", { + "/test": {view: function() {}}, + "/other/:a/:b...": {view: function() {}}, + }) + + o(route.get()).equals("/other/x/y/z?c=d#e=f") + }) + + o("gets route w/ escaped unicode", function() { + $window.location.href = prefix + encodeURI("/ö/é/å?ö=ö#ö=ö") + + route(root, "/ö/é/å?ö=ö#ö=ö", { + "/test": {view: function() {}}, + "/ö/:a/:b...": {view: function() {}}, + }) + + o(route.get()).equals("/ö/é/å?ö=ö#ö=ö") + }) + + o("gets route w/ unicode", function() { + $window.location.href = prefix + "/ö/é/å?ö=ö#ö=ö" + + route(root, "/ö/é/å?ö=ö#ö=ö", { + "/test": {view: function() {}}, + "/ö/:a/:b...": {view: function() {}}, + }) + + o(route.get()).equals("/ö/é/å?ö=ö#ö=ö") + }) + + o("sets path asynchronously", function(done) { + $window.location.href = prefix + "/a" + var spy1 = o.spy() + var spy2 = o.spy() + + route(root, "/a", { + "/a": {view: spy1}, + "/b": {view: spy2}, + }) + + o(spy1.callCount).equals(1) + o(spy2.callCount).equals(0) + route.set("/b") + o(spy1.callCount).equals(1) + o(spy2.callCount).equals(0) + callAsync(function() { + throttleMock.fire() + + o(spy1.callCount).equals(1) + o(spy2.callCount).equals(1) + done() + }) + }) + + o("sets fallback asynchronously", function(done) { + $window.location.href = prefix + "/b" + var spy1 = o.spy() + var spy2 = o.spy() + + route(root, "/a", { + "/a": {view: spy1}, + "/b": {view: spy2}, + }) + + o(spy1.callCount).equals(0) + o(spy2.callCount).equals(1) + route.set("/c") + o(spy1.callCount).equals(0) + o(spy2.callCount).equals(1) + callAsync(function() { + // Yep, before even the throttle mechanism takes hold. + o(route.get()).equals("/b") + callAsync(function() { + // Yep, before even the throttle mechanism takes hold. + o(route.get()).equals("/a") + throttleMock.fire() + + o(spy1.callCount).equals(1) + o(spy2.callCount).equals(1) + done() + }) + }) + }) + + o("exposes new route asynchronously", function(done) { + $window.location.href = prefix + "/test" + route(root, "/test", { + "/test": {view: function() {}}, + "/other/:a/:b...": {view: function() {}}, + }) + + route.set("/other/x/y/z?c=d#e=f") + callAsync(function() { + // Yep, before even the throttle mechanism takes hold. + o(route.get()).equals("/other/x/y/z?c=d#e=f") + throttleMock.fire() + done() + }) + }) + + o("exposes new escaped unicode route asynchronously", function(done) { + $window.location.href = prefix + "/test" + route(root, "/test", { + "/test": {view: function() {}}, + "/ö": {view: function() {}}, + }) + + route.set(encodeURI("/ö?ö=ö#ö=ö")) + callAsync(function() { + // Yep, before even the throttle mechanism takes hold. + o(route.get()).equals("/ö?ö=ö#ö=ö") + throttleMock.fire() + done() + }) + }) + + o("exposes new unescaped unicode route asynchronously", function(done) { + $window.location.href = "file://" + prefix + "/test" + route(root, "/test", { + "/test": {view: function() {}}, + "/ö": {view: function() {}}, + }) + + route.set("/ö?ö=ö#ö=ö") + callAsync(function() { + // Yep, before even the throttle mechanism takes hold. + o(route.get()).equals("/ö?ö=ö#ö=ö") + throttleMock.fire() + done() + }) + }) + + o("exposes new route asynchronously on fallback mode", function(done) { + $window.location.href = prefix + "/test" + route(root, "/test", { + "/test": {view: function() {}}, + "/other/:a/:b...": {view: function() {}}, + }) + + route.set("/other/x/y/z?c=d#e=f") + callAsync(function() { + // Yep, before even the throttle mechanism takes hold. + o(route.get()).equals("/other/x/y/z?c=d#e=f") + throttleMock.fire() + done() + }) + }) + + o("sets route via pushState/onpopstate", function(done) { + $window.location.href = prefix + "/test" + route(root, "/test", { + "/test": {view: function() {}}, + "/other/:a/:b...": {view: function() {}}, + }) + + callAsync(function() { + $window.history.pushState(null, null, prefix + "/other/x/y/z?c=d#e=f") + $window.onpopstate() + + callAsync(function() { + // Yep, before even the throttle mechanism takes hold. + o(route.get()).equals("/other/x/y/z?c=d#e=f") + throttleMock.fire() + + done() + }) + }) + }) + + o("sets parameterized route", function(done) { + $window.location.href = prefix + "/test" + route(root, "/test", { + "/test": {view: function() {}}, + "/other/:a/:b...": {view: function() {}}, + }) + + route.set("/other/:a/:b", {a: "x", b: "y/z", c: "d", e: "f"}) + callAsync(function() { + // Yep, before even the throttle mechanism takes hold. + o(route.get()).equals("/other/x/y%2Fz?c=d&e=f") + throttleMock.fire() + done() + }) + }) + + o("replace:true works", function(done) { + $window.location.href = prefix + "/test" + route(root, "/test", { + "/test": {view: function() {}}, + "/other": {view: function() {}}, + }) + + route.set("/other", null, {replace: true}) + + callAsync(function() { + throttleMock.fire() + $window.history.back() + o($window.location.href).equals(env.protocol + "//" + (env.hostname === "/" ? "" : env.hostname) + "/") + done() + }) + }) + + o("replace:false works", function(done) { + $window.location.href = prefix + "/test" + route(root, "/test", { + "/test": {view: function() {}}, + "/other": {view: function() {}}, + }) + + route.set("/other", null, {replace: false}) + + callAsync(function() { + throttleMock.fire() + $window.history.back() + var slash = prefix[0] === "/" ? "" : "/" + o($window.location.href).equals(env.protocol + "//" + (env.hostname === "/" ? "" : env.hostname) + slash + (prefix ? prefix + "/" : "") + "test") + done() + }) + }) + + o("state works", function(done) { + $window.location.href = prefix + "/test" + route(root, "/test", { + "/test": {view: function() {}}, + "/other": {view: function() {}}, + }) + + route.set("/other", null, {state: {a: 1}}) + callAsync(function() { + throttleMock.fire() + o($window.history.state).deepEquals({a: 1}) + done() + }) + }) + }) + }) + }) +}) diff --git a/docs/change-log.md b/docs/change-log.md index 39348f54..cf3e00f9 100644 --- a/docs/change-log.md +++ b/docs/change-log.md @@ -54,6 +54,13 @@ - Previously, numeric children weren't coerced. Now, they are. - Unlikely to break most components, but it *could* break some users. - This increases consistency with how booleans are handled with children, so it should be more intuitive. +- route: `key` parameter for routes now only works globally for components ([#2458](https://github.com/MithrilJS/mithril.js/pull/2458) [@isiahmeadows](https://github.com/isiahmeadows)) + - Previously, it worked for route resolvers, too. + - This lets you ensure global layouts used in `render` still render by diff. +- redraw: `mithril/redraw` now just exposes the `m.redraw` callback ([#2458](https://github.com/MithrilJS/mithril.js/pull/2458) [@isiahmeadows](https://github.com/isiahmeadows)) + - The `.schedule`, `.unschedule`, and `.render` properties of the former `redrawService` are all removed. + - If you want to know how to work around it, look at the call to `mount` in Mithril's source for `m.route`. That should help you in finding ways around the removed feature. (It doesn't take that much more code.) + #### News @@ -81,6 +88,7 @@ - route: Use `m.mount(root, null)` to unsubscribe and clean up after a `m.route(root, ...)` call. ([#2453](https://github.com/MithrilJS/mithril.js/pull/2453)) - version: `m.version` returns the previous version string for what's in `next`. ([#2453](https://github.com/MithrilJS/mithril.js/pull/2453)) - If you're using `next`, you should hopefully know what you're doing. If you need stability, don't use `next`. (This is also why I'm not labelling it as a breaking change.) +- render: new `redraw` parameter exposed any time a child event handler is used ([#2458](https://github.com/MithrilJS/mithril.js/pull/2458) [@isiahmeadows](https://github.com/isiahmeadows)) #### Bug fixes diff --git a/docs/hyperscript.md b/docs/hyperscript.md index f71ef3e7..7bf438d4 100644 --- a/docs/hyperscript.md +++ b/docs/hyperscript.md @@ -454,7 +454,7 @@ var isError = false m("div", isError ? "An error occurred" : "Saved") //
Saved
``` -You cannot use JavaScript statements such as `if` or `for` within JavaScript expressions. It's preferable to avoid using those statements altogether and instead, use the constructs above exclusively in order to keep the structure of the templates linear and declarative, and to avoid deoptimizations. +You cannot use JavaScript statements such as `if` or `for` within JavaScript expressions. It's preferable to avoid using those statements altogether and instead, use the constructs above exclusively in order to keep the structure of the templates linear and declarative. --- diff --git a/docs/index.md b/docs/index.md index 8589f517..4fbffd82 100644 --- a/docs/index.md +++ b/docs/index.md @@ -18,7 +18,7 @@ It's small (< 10kb gzip), fast and provides routing and XHR utilities out of the
Download size
- Mithril (9.6kb) + Mithril (9.4kb)
Vue + Vue-Router + Vuex + fetch (40kb)
diff --git a/docs/keys.md b/docs/keys.md index 8319350e..de9076a4 100644 --- a/docs/keys.md +++ b/docs/keys.md @@ -75,7 +75,7 @@ function correctUserList(users) { } ``` -Also, you might want to reinitialize a component. You can use the common pattern of a single-item keyed fragment where you change the key to destroy and reinitialize the element. +Also, you might want to reinitialize a component. You can use the common pattern of a single-child keyed fragment where you change the key to destroy and reinitialize the element. ```javascript function ResettableToggle() { diff --git a/docs/mount.md b/docs/mount.md index eacb03b7..5263ccdb 100644 --- a/docs/mount.md +++ b/docs/mount.md @@ -3,6 +3,7 @@ - [Description](#description) - [Signature](#signature) - [How it works](#how-it-works) +- [Headless mounts](#headless-mounts) - [Performance considerations](#performance-considerations) - [Differences from m.render](#differences-from-mrender) @@ -61,6 +62,35 @@ Running `mount(element, OtherComponent)` where `element` is a current mount poin Using `m.mount(element, null)` on an element with a previously mounted component unmounts it and cleans up Mithril internal state. This can be useful to prevent memory leaks when removing the `root` node manually from the DOM. +#### Headless mounts + +In certain more advanced situations, you may want to subscribe and listen for [redraws](autoredraw.md) without rendering anything to the screen. This can be done using a headless mount, created by simply invoking `m.mount` with an element that's not added to the live DOM tree and putting all your useful logic in the component you're mounting with. You still need a `view` in your component, just it doesn't have to return anything useful and it can just return a junk value like `null` or `undefined`. + +```javascript +var elem = document.createElement("div") + +// Subscribe +m.mount(elem, { + oncreate: function() { + // once added + }, + onupdate: function() { + // on each redraw + }, + onremove: function() { + // clean up whatever you need + }, + + // Necessary boilerplate + view: function () {}, +}) + +// Unsubscribe +m.mount(elem, null) +``` + +There's no need to worry about other mount roots. Multiple roots are supported and they won't step on each other. You can even do the above in a component when integrating with another framework, and it won't be a problem. + --- ### Performance considerations diff --git a/docs/render.md b/docs/render.md index 0e2af728..36516d83 100644 --- a/docs/render.md +++ b/docs/render.md @@ -22,12 +22,13 @@ m.render(document.body, "hello") ### Signature -`m.render(element, vnodes)` +`m.render(element, vnodes, redraw)` Argument | Type | Required | Description ----------- | -------------------- | -------- | --- `element` | `Element` | Yes | A DOM element that will be the parent node to the subtree `vnodes` | `Array|Vnode` | Yes | The [vnodes](vnodes.md) to be rendered +`redraw` | `() -> any` | No | A callback invoked each time an event handler in the subtree is invoked **returns** | | | Returns `undefined` [How to read signatures](signatures.md) @@ -36,7 +37,9 @@ Argument | Type | Required | Description ### How it works -The `m.render(element, vnodes)` method takes a virtual DOM tree (typically generated via the [`m()` hyperscript function](hyperscript.md)), generates a DOM tree and mounts it on `element`. If `element` already has a DOM tree mounted via a previous `m.render()` call, `vnodes` is diffed against the previous `vnodes` tree and the existing DOM tree is modified only where needed to reflect the changes. Unchanged DOM nodes are not touched at all. +The `m.render(element, vnodes)` method takes a virtual DOM tree (typically generated via the [`m()` hyperscript function](hyperscript.md)), generates a DOM tree and mounts it on `element`. If `element` already has a DOM tree mounted via a previous `m.render()` call, `vnodes` is diffed against the previous `vnodes` tree and the existing DOM tree is modified only where needed to reflect the changes. Unchanged DOM nodes are not touched at all. + +If you pass the optional `redraw` argument, that is invoked each time an event handler anywhere in the subtree is called. This is used by [`m.mount`](mount.md) and [`m.redraw`](redraw.md) to implement the [autoredraw](autoredraw.md) functionality, but it's also exposed for more advanced use cases like integration with some third-party frameworks. `m.render` is synchronous. @@ -66,6 +69,6 @@ Another difference is that `m.render` method expects a [vnode](vnodes.md) (or a `var render = require("mithril/render")` -The `m.render` module is similar in scope to view libraries like Knockout, React and Vue. It is approximately 500 lines of code (3kb min+gzip) and implements a virtual DOM diffing engine with a modern search space reduction algorithm and DOM recycling, which translate to top-of-class performance, both in terms of initial page load and re-rendering. It has no dependencies on other parts of Mithril and can be used as a standalone library. +The `m.render` module is similar in scope to view libraries like Knockout, React and Vue. It implements a virtual DOM diffing engine with a modern search space reduction algorithm and DOM recycling, which translate to top-of-class performance, both in terms of initial page load and re-rendering. It has no dependencies on other parts of Mithril aside from normalization exposed via `require("mithril/render/vnode")` and can be used as a standalone library. -Despite being incredibly small, the render module is fully functional and self-sufficient. It supports everything you might expect: SVG, custom elements, and all valid attributes and events - without any weird case-sensitive edge cases or exceptions. Of course, it also fully supports [components](components.md) and [lifecycle methods](lifecycle-methods.md). +Despite being relatively small, the render module is fully functional and self-sufficient. It supports everything you might expect: SVG, custom elements, and all valid attributes and events - without any weird case-sensitive edge cases or exceptions. Of course, it also fully supports [components](components.md) and [lifecycle methods](lifecycle-methods.md). diff --git a/docs/route.md b/docs/route.md index 787ab0a8..4b24d94b 100644 --- a/docs/route.md +++ b/docs/route.md @@ -206,7 +206,7 @@ Argument | Type | Description #### How it works -Routing is a system that allows creating Single-Page-Applications (SPA), i.e. applications that can go from a "page" to another without causing a full browser refresh. +Routing is a system that allows creating Single Page Applications (SPA), i.e. applications that can go from a "page" to another without causing a full browser refresh. It enables seamless navigability while preserving the ability to bookmark each page individually, and the ability to navigate the application via the browser's history mechanism. @@ -336,6 +336,8 @@ Or even use the [`history state`](#history-state) feature to achieve reloadable `m.route.set(m.route.get(), null, {state: {key: Date.now()}})` +Note that the key parameter works only for component routes. If you're using a route resolver, you'll need to use a [single-child keyed fragment](keys.md), passing `key: m.route.param("key")`, to accomplish the same. + #### Variadic routes It's also possible to have variadic routes, i.e. a route with an argument that contains URL pathnames that contain slashes: @@ -739,7 +741,7 @@ m.route(document.body, "/", { In certain situations, you may find yourself needing to interoperate with another framework like React. Here's how you do it: - Define all your routes using `m.route` as normal, but make sure you only use it *once*. Multiple route points are not supported. -- When you need to remove routing subscriptions, use `m.mount(root, null)`, using the same root you used `m.route(root, ...)` on. +- When you need to remove routing subscriptions, use `m.mount(root, null)`, using the same root you used `m.route(root, ...)` on. `m.route` uses `m.mount` internally to hook everything up, so it's not magic. Here's an example with React: diff --git a/index.js b/index.js index 1f775a5d..4fdf3d9e 100644 --- a/index.js +++ b/index.js @@ -1,22 +1,19 @@ "use strict" var hyperscript = require("./hyperscript") +var request = require("./request") +var mountRedraw = require("./mount-redraw") + var m = function m() { return hyperscript.apply(this, arguments) } m.m = hyperscript m.trust = hyperscript.trust m.fragment = hyperscript.fragment - -var requestService = require("./request") -var redrawService = require("./redraw") - -requestService.setCompletionCallback(redrawService.redraw) - -m.mount = require("./mount") +m.mount = mountRedraw.mount m.route = require("./route") -m.render = require("./render").render -m.redraw = redrawService.redraw -m.request = requestService.request -m.jsonp = requestService.jsonp +m.render = require("./render") +m.redraw = mountRedraw.redraw +m.request = request.request +m.jsonp = request.jsonp m.parseQueryString = require("./querystring/parse") m.buildQueryString = require("./querystring/build") m.parsePathname = require("./pathname/parse") diff --git a/mount-redraw.js b/mount-redraw.js new file mode 100644 index 00000000..8cd149ec --- /dev/null +++ b/mount-redraw.js @@ -0,0 +1,5 @@ +"use strict" + +var render = require("./render") + +module.exports = require("./api/mount-redraw")(render, requestAnimationFrame, console) diff --git a/mount.js b/mount.js index 73fa1ce9..6e169443 100644 --- a/mount.js +++ b/mount.js @@ -1,5 +1,3 @@ "use strict" -var redrawService = require("./redraw") - -module.exports = require("./api/mount")(redrawService) +module.exports = require("./mount-redraw").mount diff --git a/performance/test-perf.js b/performance/test-perf.js index 020960c4..ee6db754 100644 --- a/performance/test-perf.js +++ b/performance/test-perf.js @@ -44,7 +44,7 @@ if(!doc) { } var m = require("../render/hyperscript") -m.render = require("../render/render")(window).render +m.render = require("../render/render")(window) function resetScratch() { diff --git a/redraw.js b/redraw.js index 314e5002..af43394d 100644 --- a/redraw.js +++ b/redraw.js @@ -1,3 +1,3 @@ "use strict" -module.exports = require("./api/redraw")(window) +module.exports = require("./mount-redraw").redraw diff --git a/render/render.js b/render/render.js index 7a461198..64d375b3 100644 --- a/render/render.js +++ b/render/render.js @@ -4,15 +4,13 @@ var Vnode = require("../render/vnode") module.exports = function($window) { var $doc = $window.document + var currentRedraw var nameSpace = { svg: "http://www.w3.org/2000/svg", math: "http://www.w3.org/1998/Math/MathML" } - var redraw - function setRedraw(callback) {return redraw = callback} - function getNameSpace(vnode) { return vnode.attrs && vnode.attrs.xmlns || nameSpace[vnode.tag] } @@ -797,15 +795,17 @@ module.exports = function($window) { // that below. // 6. In function-based event handlers, `return false` prevents the default // action and stops event propagation. We replicate that below. - function EventDict() {} + function EventDict() { + // Save this, so the current redraw is correctly tracked. + this._ = currentRedraw + } EventDict.prototype = Object.create(null) EventDict.prototype.handleEvent = function (ev) { var handler = this["on" + ev.type] var result if (typeof handler === "function") result = handler.call(ev.currentTarget, ev) else if (typeof handler.handleEvent === "function") handler.handleEvent(ev) - if (ev.redraw === false) ev.redraw = undefined - else if (typeof redraw === "function") redraw() + if (this._ && ev.redraw !== false) (0, this._)() if (result === false) { ev.preventDefault() ev.stopPropagation() @@ -866,8 +866,8 @@ module.exports = function($window) { return true } - function render(dom, vnodes) { - if (!dom) throw new Error("Ensure the DOM element being passed to m.route/m.mount/m.render is not undefined.") + 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.") var hooks = [] var active = activeElement() var namespace = dom.namespaceURI @@ -876,12 +876,16 @@ module.exports = function($window) { 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) + var prevRedraw = currentRedraw + try { + currentRedraw = typeof redraw === "function" ? redraw : undefined + updateNodes(dom, dom.vnodes, vnodes, hooks, null, namespace === "http://www.w3.org/1999/xhtml" ? undefined : namespace) + } finally { + currentRedraw = prevRedraw + } 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]() } - - return {render: render, setRedraw: setRedraw} } diff --git a/render/tests/test-attributes.js b/render/tests/test-attributes.js index 00eb80dc..15632395 100644 --- a/render/tests/test-attributes.js +++ b/render/tests/test-attributes.js @@ -9,7 +9,7 @@ o.spec("attributes", function() { o.beforeEach(function() { $window = domMock() root = $window.document.body - render = vdom($window).render + render = vdom($window) }) o.spec("basics", function() { o("works (create/update/remove)", function() { @@ -255,7 +255,7 @@ o.spec("attributes", function() { o("isn't set when equivalent to the previous value and focused", function() { var $window = domMock({spy: o.spy}) var root = $window.document.body - var render = vdom($window).render + var render = vdom($window) var a = {tag: "input"} var b = {tag: "input", attrs: {value: "1"}} @@ -294,7 +294,7 @@ o.spec("attributes", function() { o("the input.type setter is never used", function() { var $window = domMock({spy: o.spy}) var root = $window.document.body - var render = vdom($window).render + var render = vdom($window) var a = {tag: "input", attrs: {type: "radio"}} var b = {tag: "input", attrs: {type: "text"}} @@ -334,7 +334,7 @@ o.spec("attributes", function() { o("isn't set when equivalent to the previous value and focused", function() { var $window = domMock({spy: o.spy}) var root = $window.document.body - var render = vdom($window).render + var render = vdom($window) var a = {tag: "textarea"} var b = {tag: "textarea", attrs: {value: "1"}} @@ -480,7 +480,7 @@ o.spec("attributes", function() { o("isn't set when equivalent to the previous value", function() { var $window = domMock({spy: o.spy}) var root = $window.document.body - var render = vdom($window).render + var render = vdom($window) var a = {tag: "option"} var b = {tag: "option", attrs: {value: "1"}} @@ -618,7 +618,7 @@ o.spec("attributes", function() { o("updates with the same value do not re-set the attribute if the select has focus", function() { var $window = domMock({spy: o.spy}) var root = $window.document.body - var render = vdom($window).render + var render = vdom($window) var a = makeSelect() var b = makeSelect("1") diff --git a/render/tests/test-component.js b/render/tests/test-component.js index 9b2e90e2..423ceead 100644 --- a/render/tests/test-component.js +++ b/render/tests/test-component.js @@ -11,7 +11,7 @@ o.spec("component", function() { $window = domMock() root = $window.document.createElement("div") - render = vdom($window).render + render = vdom($window) }) components.forEach(function(cmp){ diff --git a/render/tests/test-createElement.js b/render/tests/test-createElement.js index 54119c68..ba250a6e 100644 --- a/render/tests/test-createElement.js +++ b/render/tests/test-createElement.js @@ -9,7 +9,7 @@ o.spec("createElement", function() { o.beforeEach(function() { $window = domMock() root = $window.document.createElement("div") - render = vdom($window).render + render = vdom($window) }) o("creates element", function() { diff --git a/render/tests/test-createFragment.js b/render/tests/test-createFragment.js index a40967a9..5000f859 100644 --- a/render/tests/test-createFragment.js +++ b/render/tests/test-createFragment.js @@ -9,7 +9,7 @@ o.spec("createFragment", function() { o.beforeEach(function() { $window = domMock() root = $window.document.createElement("div") - render = vdom($window).render + render = vdom($window) }) o("creates fragment", function() { diff --git a/render/tests/test-createHTML.js b/render/tests/test-createHTML.js index a337213b..bf3055b2 100644 --- a/render/tests/test-createHTML.js +++ b/render/tests/test-createHTML.js @@ -9,7 +9,7 @@ o.spec("createHTML", function() { o.beforeEach(function() { $window = domMock() root = $window.document.createElement("div") - render = vdom($window).render + render = vdom($window) }) o("creates HTML", function() { diff --git a/render/tests/test-createNodes.js b/render/tests/test-createNodes.js index 946f41a2..649ebf9a 100644 --- a/render/tests/test-createNodes.js +++ b/render/tests/test-createNodes.js @@ -9,7 +9,7 @@ o.spec("createNodes", function() { o.beforeEach(function() { $window = domMock() root = $window.document.createElement("div") - render = vdom($window).render + render = vdom($window) }) o("creates nodes", function() { diff --git a/render/tests/test-createText.js b/render/tests/test-createText.js index 73a37156..7a05c44e 100644 --- a/render/tests/test-createText.js +++ b/render/tests/test-createText.js @@ -9,7 +9,7 @@ o.spec("createText", function() { o.beforeEach(function() { $window = domMock() root = $window.document.createElement("div") - render = vdom($window).render + render = vdom($window) }) o("creates string", function() { diff --git a/render/tests/test-event.js b/render/tests/test-event.js index 74dcb75b..381b4276 100644 --- a/render/tests/test-event.js +++ b/render/tests/test-event.js @@ -11,8 +11,9 @@ o.spec("event", function() { root = $window.document.body redraw = o.spy() var renderer = vdom($window) - renderer.setRedraw(redraw) - render = renderer.render + render = function(dom, vnode) { + return renderer(dom, vnode, redraw) + } }) o("handles onclick", function() { diff --git a/render/tests/test-input.js b/render/tests/test-input.js index c443db1a..e28784a2 100644 --- a/render/tests/test-input.js +++ b/render/tests/test-input.js @@ -8,7 +8,7 @@ o.spec("form inputs", function() { var $window, root, render o.beforeEach(function() { $window = domMock() - render = vdom($window).render + render = vdom($window) root = $window.document.createElement("div") $window.document.body.appendChild(root) }) diff --git a/render/tests/test-normalizeComponentChildren.js b/render/tests/test-normalizeComponentChildren.js index 86f75ffe..6ef57f6c 100644 --- a/render/tests/test-normalizeComponentChildren.js +++ b/render/tests/test-normalizeComponentChildren.js @@ -8,7 +8,7 @@ var vdom = require("../../render/render") o.spec("component children", function () { var $window = domMock() var root = $window.document.createElement("div") - var render = vdom($window).render + var render = vdom($window) o.spec("component children", function () { var component = { diff --git a/render/tests/test-onbeforeremove.js b/render/tests/test-onbeforeremove.js index 834f7510..9e9a2e59 100644 --- a/render/tests/test-onbeforeremove.js +++ b/render/tests/test-onbeforeremove.js @@ -12,7 +12,7 @@ o.spec("onbeforeremove", function() { o.beforeEach(function() { $window = domMock() root = $window.document.createElement("div") - render = vdom($window).render + render = vdom($window) }) o("does not call onbeforeremove when creating", function() { diff --git a/render/tests/test-onbeforeupdate.js b/render/tests/test-onbeforeupdate.js index 6c5adac8..e179bad2 100644 --- a/render/tests/test-onbeforeupdate.js +++ b/render/tests/test-onbeforeupdate.js @@ -10,7 +10,7 @@ o.spec("onbeforeupdate", function() { o.beforeEach(function() { $window = domMock() root = $window.document.createElement("div") - render = vdom($window).render + render = vdom($window) }) o("prevents update in element", function() { diff --git a/render/tests/test-oncreate.js b/render/tests/test-oncreate.js index eb1daa5e..df651f90 100644 --- a/render/tests/test-oncreate.js +++ b/render/tests/test-oncreate.js @@ -9,7 +9,7 @@ o.spec("oncreate", function() { o.beforeEach(function() { $window = domMock() root = $window.document.createElement("div") - render = vdom($window).render + render = vdom($window) }) o("calls oncreate when creating element", function() { diff --git a/render/tests/test-oninit.js b/render/tests/test-oninit.js index f6ffb873..a42d5100 100644 --- a/render/tests/test-oninit.js +++ b/render/tests/test-oninit.js @@ -9,7 +9,7 @@ o.spec("oninit", function() { o.beforeEach(function() { $window = domMock() root = $window.document.createElement("div") - render = vdom($window).render + render = vdom($window) }) o("calls oninit when creating element", function() { diff --git a/render/tests/test-onremove.js b/render/tests/test-onremove.js index 6cc3fd5c..bab0d308 100644 --- a/render/tests/test-onremove.js +++ b/render/tests/test-onremove.js @@ -11,7 +11,7 @@ o.spec("onremove", function() { o.beforeEach(function() { $window = domMock() root = $window.document.createElement("div") - render = vdom($window).render + render = vdom($window) }) o("does not call onremove when creating", function() { diff --git a/render/tests/test-onupdate.js b/render/tests/test-onupdate.js index 4b74d288..10794dd0 100644 --- a/render/tests/test-onupdate.js +++ b/render/tests/test-onupdate.js @@ -9,7 +9,7 @@ o.spec("onupdate", function() { o.beforeEach(function() { $window = domMock() root = $window.document.createElement("div") - render = vdom($window).render + render = vdom($window) }) o("does not call onupdate when creating element", function() { diff --git a/render/tests/test-render-hyperscript-integration.js b/render/tests/test-render-hyperscript-integration.js index 73d96a8f..1b722fb0 100644 --- a/render/tests/test-render-hyperscript-integration.js +++ b/render/tests/test-render-hyperscript-integration.js @@ -10,7 +10,7 @@ o.spec("render/hyperscript integration", function() { o.beforeEach(function() { $window = domMock() root = $window.document.createElement("div") - render = vdom($window).render + render = vdom($window) }) o.spec("setting class", function() { o("selector only", function() { diff --git a/render/tests/test-render.js b/render/tests/test-render.js index 33bd496a..9236e9ef 100644 --- a/render/tests/test-render.js +++ b/render/tests/test-render.js @@ -9,7 +9,7 @@ o.spec("render", function() { o.beforeEach(function() { $window = domMock() root = $window.document.createElement("div") - render = vdom($window).render + render = vdom($window) }) o("renders plain text", function() { diff --git a/render/tests/test-textContent.js b/render/tests/test-textContent.js index 9be5b4ba..0cdd0b4c 100644 --- a/render/tests/test-textContent.js +++ b/render/tests/test-textContent.js @@ -9,7 +9,7 @@ o.spec("textContent", function() { o.beforeEach(function() { $window = domMock() root = $window.document.createElement("div") - render = vdom($window).render + render = vdom($window) }) o("ignores null", function() { diff --git a/render/tests/test-updateElement.js b/render/tests/test-updateElement.js index c1eb573f..7a7285fa 100644 --- a/render/tests/test-updateElement.js +++ b/render/tests/test-updateElement.js @@ -9,7 +9,7 @@ o.spec("updateElement", function() { o.beforeEach(function() { $window = domMock() root = $window.document.createElement("div") - render = vdom($window).render + render = vdom($window) }) o("updates attr", function() { diff --git a/render/tests/test-updateFragment.js b/render/tests/test-updateFragment.js index 0204d935..e0cdecf8 100644 --- a/render/tests/test-updateFragment.js +++ b/render/tests/test-updateFragment.js @@ -9,7 +9,7 @@ o.spec("updateFragment", function() { o.beforeEach(function() { $window = domMock() root = $window.document.createElement("div") - render = vdom($window).render + render = vdom($window) }) o("updates fragment", function() { diff --git a/render/tests/test-updateHTML.js b/render/tests/test-updateHTML.js index c678e539..294456e9 100644 --- a/render/tests/test-updateHTML.js +++ b/render/tests/test-updateHTML.js @@ -9,7 +9,7 @@ o.spec("updateHTML", function() { o.beforeEach(function() { $window = domMock() root = $window.document.createElement("div") - render = vdom($window).render + render = vdom($window) }) o("updates html", function() { diff --git a/render/tests/test-updateNodes.js b/render/tests/test-updateNodes.js index f6bf3668..5ce9e32f 100644 --- a/render/tests/test-updateNodes.js +++ b/render/tests/test-updateNodes.js @@ -10,7 +10,7 @@ o.spec("updateNodes", function() { o.beforeEach(function() { $window = domMock() root = $window.document.createElement("div") - render = vdom($window).render + render = vdom($window) }) o("handles el noop", function() { diff --git a/render/tests/test-updateNodesFuzzer.js b/render/tests/test-updateNodesFuzzer.js index 8580c139..c5ad44d0 100644 --- a/render/tests/test-updateNodesFuzzer.js +++ b/render/tests/test-updateNodesFuzzer.js @@ -10,7 +10,7 @@ o.spec("updateNodes keyed list Fuzzer", function() { o.beforeEach(function() { $window = domMock() root = $window.document.createElement("div") - render = vdom($window).render + render = vdom($window) }) diff --git a/render/tests/test-updateText.js b/render/tests/test-updateText.js index 86618e12..f8579dea 100644 --- a/render/tests/test-updateText.js +++ b/render/tests/test-updateText.js @@ -9,7 +9,7 @@ o.spec("updateText", function() { o.beforeEach(function() { $window = domMock() root = $window.document.createElement("div") - render = vdom($window).render + render = vdom($window) }) o("updates to string", function() { diff --git a/request.js b/request.js index cb3bfbbe..06fb2bbd 100644 --- a/request.js +++ b/request.js @@ -1,4 +1,6 @@ "use strict" var PromisePolyfill = require("./promise/promise") -module.exports = require("./request/request")(window, PromisePolyfill) +var mountRedraw = require("./mount-redraw") + +module.exports = require("./request/request")(window, PromisePolyfill, mountRedraw.redraw) diff --git a/request/request.js b/request/request.js index 48f52f39..0e267769 100644 --- a/request/request.js +++ b/request/request.js @@ -2,9 +2,8 @@ var buildPathname = require("../pathname/build") -module.exports = function($window, Promise) { +module.exports = function($window, Promise, oncompletion) { var callbackCount = 0 - var oncompletion function PromiseProxy(executor) { return new Promise(executor) @@ -191,8 +190,5 @@ module.exports = function($window, Promise) { encodeURIComponent(callbackName) $window.document.documentElement.appendChild(script) }), - setCompletionCallback: function(callback) { - oncompletion = callback - }, } } diff --git a/request/tests/test-jsonp.js b/request/tests/test-jsonp.js index 58c7f08b..6dfed4e7 100644 --- a/request/tests/test-jsonp.js +++ b/request/tests/test-jsonp.js @@ -3,17 +3,15 @@ var o = require("../../ospec/ospec") var xhrMock = require("../../test-utils/xhrMock") var Request = require("../../request/request") -var Promise = require("../../promise/promise") +var PromisePolyfill = require("../../promise/promise") var parseQueryString = require("../../querystring/parse") o.spec("jsonp", function() { var mock, jsonp, complete o.beforeEach(function() { mock = xhrMock() - var requestService = Request(mock, Promise) - jsonp = requestService.jsonp complete = o.spy() - requestService.setCompletionCallback(complete) + jsonp = Request(mock, PromisePolyfill, complete).jsonp }) o("works", function(done) { diff --git a/request/tests/test-request.js b/request/tests/test-request.js index 969de68b..706c8404 100644 --- a/request/tests/test-request.js +++ b/request/tests/test-request.js @@ -10,10 +10,8 @@ o.spec("request", function() { var mock, request, complete o.beforeEach(function() { mock = xhrMock() - var requestService = Request(mock, PromisePolyfill) - request = requestService.request complete = o.spy() - requestService.setCompletionCallback(complete) + request = Request(mock, PromisePolyfill, complete).request }) o.spec("success", function() { @@ -835,10 +833,7 @@ o.spec("request", function() { // if you use the polyfill, as it's based on `setImmediate` (falling // back to `setTimeout`), and promise microtasks are run at higher // priority than either of those. - var requestService = Request(mock, Promise) - request = requestService.request - complete = o.spy() - requestService.setCompletionCallback(complete) + request = Request(mock, Promise, complete).request mock.$defineRoutes({ "GET /item": function() { return {status: 200, responseText: "[]"} diff --git a/route.js b/route.js index 4e829527..9cf121e2 100644 --- a/route.js +++ b/route.js @@ -1,5 +1,5 @@ "use strict" -var redrawService = require("./redraw") +var mountRedraw = require("./mount-redraw") -module.exports = require("./api/router")(window, redrawService) +module.exports = require("./api/router")(window, mountRedraw) diff --git a/router/router.js b/router/router.js deleted file mode 100644 index 37fc29b5..00000000 --- a/router/router.js +++ /dev/null @@ -1,112 +0,0 @@ -"use strict" - -var buildPathname = require("../pathname/build") -var parsePathname = require("../pathname/parse") -var compileTemplate = require("../pathname/compileTemplate") -var assign = require("../pathname/assign") - -module.exports = function($window) { - var callAsync = typeof setImmediate === "function" ? setImmediate : setTimeout - var supportsPushState = typeof $window.history.pushState === "function" - var fireAsync - - return { - prefix: "#!", - - getPath: function() { - // Consider the pathname holistically. The prefix might even be invalid, - // but that's not our problem. - var prefix = $window.location.hash - if (this.prefix[0] !== "#") { - prefix = $window.location.search + prefix - if (this.prefix[0] !== "?") { - prefix = $window.location.pathname + prefix - if (prefix[0] !== "/") prefix = "/" + prefix - } - } - // This seemingly useless `.concat()` speeds up the tests quite a bit, - // since the representation is consistently a relatively poorly - // optimized cons string. - return prefix.concat() - .replace(/(?:%[a-f89][a-f0-9])+/gim, decodeURIComponent) - .slice(this.prefix.length) - }, - - setPath: function(path, data, options) { - path = buildPathname(path, data) - if (fireAsync != null) { - fireAsync() - var state = options ? options.state : null - var title = options ? options.title : null - if (options && options.replace) $window.history.replaceState(state, title, this.prefix + path) - else $window.history.pushState(state, title, this.prefix + path) - } - else { - $window.location.href = this.prefix + path - } - }, - - defineRoutes: function(routes, resolve, reject, defaultRoute, subscribe) { - var self = this - var compiled = Object.keys(routes).map(function(route) { - if (route[0] !== "/") throw new SyntaxError("Routes must start with a `/`") - if ((/:([^\/\.-]+)(\.{3})?:/).test(route)) { - throw new SyntaxError("Route parameter names must be separated with either `/`, `.`, or `-`") - } - return { - route: route, - component: routes[route], - check: compileTemplate(route), - } - }) - var unsubscribe, asyncId - - fireAsync = null - - if (defaultRoute != null) { - var defaultData = parsePathname(defaultRoute) - - if (!compiled.some(function (i) { return i.check(defaultData) })) { - throw new ReferenceError("Default route doesn't match any known routes") - } - } - - function resolveRoute() { - var path = self.getPath() - var data = parsePathname(path) - - assign(data.params, $window.history.state) - - for (var i = 0; i < compiled.length; i++) { - if (compiled[i].check(data)) { - resolve(compiled[i].component, data.params, path, compiled[i].route) - return - } - } - - reject(path, data.params) - } - - if (supportsPushState) { - unsubscribe = function() { - $window.removeEventListener("popstate", fireAsync, false) - } - $window.addEventListener("popstate", fireAsync = function() { - if (asyncId) return - asyncId = callAsync(function() { - asyncId = null - resolveRoute() - }) - }, false) - } else if (this.prefix[0] === "#") { - unsubscribe = function() { - $window.removeEventListener("hashchange", resolveRoute, false) - } - $window.addEventListener("hashchange", resolveRoute, false) - } - - subscribe(unsubscribe) - resolveRoute() - }, - } -} diff --git a/router/tests/index.html b/router/tests/index.html deleted file mode 100644 index 1f186449..00000000 --- a/router/tests/index.html +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/router/tests/test-defineRoutes.js b/router/tests/test-defineRoutes.js deleted file mode 100644 index 39b85366..00000000 --- a/router/tests/test-defineRoutes.js +++ /dev/null @@ -1,259 +0,0 @@ -"use strict" - -var o = require("../../ospec/ospec") -var callAsync = require("../../test-utils/callAsync") -var pushStateMock = require("../../test-utils/pushStateMock") -var Router = require("../../router/router") - -o.spec("Router.defineRoutes", function() { - void [{protocol: "http:", hostname: "localhost"}, {protocol: "file:", hostname: "/"}].forEach(function(env) { - void ["#", "?", "", "#!", "?!", "/foo", "?#", "##"].forEach(function(prefix) { - o.spec("using prefix `" + prefix + "` starting on " + env.protocol + "//" + env.hostname, function() { - var $window, router, onRouteChange, onFail - - function defineRoutes(routes, defaultRoute) { - router.defineRoutes(routes, onRouteChange, onFail, defaultRoute, function() {}) - } - - o.beforeEach(function() { - $window = pushStateMock(env) - router = new Router($window) - router.prefix = prefix - onRouteChange = o.spy() - onFail = o.spy() - }) - - o("calls onRouteChange on init", function(done) { - $window.location.href = prefix + "/a" - var subscribe = o.spy() - - router.defineRoutes({"/a": {data: 1}}, onRouteChange, onFail, null, subscribe) - o(subscribe.callCount).equals(1) - - callAsync(function() { - o(onRouteChange.callCount).equals(1) - - done() - }) - }) - - o("resolves to route", function(done) { - $window.location.href = prefix + "/test" - defineRoutes({"/test": {data: 1}}) - - callAsync(function() { - o(onRouteChange.callCount).equals(1) - o(onRouteChange.args).deepEquals([{data: 1}, {}, "/test", "/test"]) - o(onFail.callCount).equals(0) - - done() - }) - }) - - o("resolves to route w/ escaped unicode", function(done) { - $window.location.href = prefix + "/%C3%B6?%C3%B6=%C3%B6" - defineRoutes({"/ö": {data: 2}}) - - callAsync(function() { - o(onRouteChange.callCount).equals(1) - o(onRouteChange.args).deepEquals([{data: 2}, {"ö": "ö"}, "/ö?ö=ö", "/ö"]) - o(onFail.callCount).equals(0) - - done() - }) - }) - - o("resolves to route w/ unicode", function(done) { - $window.location.href = prefix + "/ö?ö=ö" - defineRoutes({"/ö": {data: 2}}) - - callAsync(function() { - o(onRouteChange.callCount).equals(1) - o(onRouteChange.args).deepEquals([{data: 2}, {"ö": "ö"}, "/ö?ö=ö", "/ö"]) - o(onFail.callCount).equals(0) - - done() - }) - }) - - o("resolves to route on fallback mode", function(done) { - $window.location.href = "file://" + prefix + "/test" - - router = new Router($window) - router.prefix = prefix - - defineRoutes({"/test": {data: 1}}) - - callAsync(function() { - o(onRouteChange.callCount).equals(1) - o(onRouteChange.args).deepEquals([{data: 1}, {}, "/test", "/test"]) - o(onFail.callCount).equals(0) - - done() - }) - }) - - o("handles parameterized route", function(done) { - $window.location.href = prefix + "/test/x" - defineRoutes({"/test/:a": {data: 1}}) - - callAsync(function() { - o(onRouteChange.callCount).equals(1) - o(onRouteChange.args).deepEquals([{data: 1}, {a: "x"}, "/test/x", "/test/:a"]) - o(onFail.callCount).equals(0) - - done() - }) - }) - - o("handles multi-parameterized route", function(done) { - $window.location.href = prefix + "/test/x/y" - defineRoutes({"/test/:a/:b": {data: 1}}) - - callAsync(function() { - o(onRouteChange.callCount).equals(1) - o(onRouteChange.args).deepEquals([{data: 1}, {a: "x", b: "y"}, "/test/x/y", "/test/:a/:b"]) - o(onFail.callCount).equals(0) - - done() - }) - }) - - o("handles rest parameterized route", function(done) { - $window.location.href = prefix + "/test/x/y" - defineRoutes({"/test/:a...": {data: 1}}) - - callAsync(function() { - o(onRouteChange.callCount).equals(1) - o(onRouteChange.args).deepEquals([{data: 1}, {a: "x/y"}, "/test/x/y", "/test/:a..."]) - o(onFail.callCount).equals(0) - - done() - }) - }) - - o("handles route with search", function(done) { - $window.location.href = prefix + "/test?a=b&c=d" - defineRoutes({"/test": {data: 1}}) - - callAsync(function() { - o(onRouteChange.callCount).equals(1) - o(onRouteChange.args).deepEquals([{data: 1}, {a: "b", c: "d"}, "/test?a=b&c=d", "/test"]) - o(onFail.callCount).equals(0) - - done() - }) - }) - - o("calls reject", function(done) { - $window.location.href = prefix + "/test" - defineRoutes({"/other": {data: 1}}) - - callAsync(function() { - o(onFail.callCount).equals(1) - o(onFail.args).deepEquals(["/test", {}]) - - done() - }) - }) - - o("handles out of order routes", function(done) { - $window.location.href = prefix + "/z/y/x" - defineRoutes({"/z/y/x": {data: 1}, "/:a...": {data: 2}}) - - callAsync(function() { - o(onRouteChange.callCount).equals(1) - o(onRouteChange.args).deepEquals([{data: 1}, {}, "/z/y/x", "/z/y/x"]) - - done() - }) - }) - - o("handles reverse out of order routes", function(done) { - $window.location.href = prefix + "/z/y/x" - defineRoutes({"/:a...": {data: 2}, "/z/y/x": {data: 1}}) - - callAsync(function() { - o(onRouteChange.callCount).equals(1) - o(onRouteChange.args).deepEquals([{data: 2}, {a: "z/y/x"}, "/z/y/x", "/:a..."]) - - done() - }) - }) - - o("handles dynamically added out of order routes", function(done) { - var routes = {} - routes["/z/y/x"] = {data: 1} - routes["/:a..."] = {data: 2} - - $window.location.href = prefix + "/z/y/x" - defineRoutes(routes) - - callAsync(function() { - o(onRouteChange.callCount).equals(1) - o(onRouteChange.args).deepEquals([{data: 1}, {}, "/z/y/x", "/z/y/x"]) - - done() - }) - }) - - o("handles reversed dynamically added out of order routes", function(done) { - var routes = {} - routes["/:a..."] = {data: 2} - routes["/z/y/x"] = {data: 1} - - $window.location.href = prefix + "/z/y/x" - defineRoutes(routes) - - callAsync(function() { - o(onRouteChange.callCount).equals(1) - o(onRouteChange.args).deepEquals([{data: 2}, {a: "z/y/x"}, "/z/y/x", "/:a..."]) - - done() - }) - }) - - o("handles mixed out of order routes", function(done) { - var routes = {"/z/y/x": {data: 1}} - routes["/:a..."] = {data: 2} - - $window.location.href = prefix + "/z/y/x" - defineRoutes(routes) - - callAsync(function() { - o(onRouteChange.callCount).equals(1) - o(onRouteChange.args).deepEquals([{data: 1}, {}, "/z/y/x", "/z/y/x"]) - - done() - }) - }) - - o("handles reverse mixed out of order routes", function(done) { - var routes = {"/:a...": {data: 2}} - routes["/z/y/x"] = {data: 12} - - $window.location.href = prefix + "/z/y/x" - defineRoutes(routes) - - callAsync(function() { - o(onRouteChange.callCount).equals(1) - o(onRouteChange.args).deepEquals([{data: 2}, {a: "z/y/x"}, "/z/y/x", "/:a..."]) - - done() - }) - }) - - o("handles non-ascii routes", function(done) { - $window.location.href = prefix + "/ö" - defineRoutes({"/ö": "aaa"}) - - callAsync(function() { - o(onRouteChange.callCount).equals(1) - - done() - }) - }) - }) - }) - }) -}) diff --git a/router/tests/test-getPath.js b/router/tests/test-getPath.js deleted file mode 100644 index f79e133f..00000000 --- a/router/tests/test-getPath.js +++ /dev/null @@ -1,52 +0,0 @@ -"use strict" - -var o = require("../../ospec/ospec") -var pushStateMock = require("../../test-utils/pushStateMock") -var Router = require("../../router/router") - -o.spec("Router.getPath", function() { - void [{protocol: "http:", hostname: "localhost"}, {protocol: "file:", hostname: "/"}].forEach(function(env) { - void ["#", "?", "", "#!", "?!", "/foo", "?#", "##"].forEach(function(prefix) { - o.spec("using prefix `" + prefix + "` starting on " + env.protocol + "//" + env.hostname, function() { - var $window, router, onRouteChange, onFail - - function defineRoutes(routes, defaultRoute) { - router.defineRoutes(routes, onRouteChange, onFail, defaultRoute, function() {}) - } - - o.beforeEach(function() { - $window = pushStateMock(env) - router = new Router($window) - router.prefix = prefix - onRouteChange = o.spy() - onFail = o.spy() - }) - - o("gets route", function() { - $window.location.href = prefix + "/test" - defineRoutes({"/test": {data: 1}}) - - o(router.getPath()).equals("/test") - }) - o("gets route w/ params", function() { - $window.location.href = prefix + "/other/x/y/z?c=d#e=f" - defineRoutes({"/test": {data: 1}, "/other/:a/:b...": {data: 2}}) - - o(router.getPath()).equals("/other/x/y/z?c=d#e=f") - }) - o("gets route w/ escaped unicode", function() { - $window.location.href = prefix + "/%C3%B6?%C3%B6=%C3%B6#%C3%B6=%C3%B6" - defineRoutes({"/test": {data: 1}, "/ö/:a/:b...": {data: 2}}) - - o(router.getPath()).equals("/ö?ö=ö#ö=ö") - }) - o("gets route w/ unicode", function() { - $window.location.href = prefix + "/ö?ö=ö#ö=ö" - defineRoutes({"/test": {data: 1}, "/ö/:a/:b...": {data: 2}}) - - o(router.getPath()).equals("/ö?ö=ö#ö=ö") - }) - }) - }) - }) -}) diff --git a/router/tests/test-setPath.js b/router/tests/test-setPath.js deleted file mode 100644 index 8447c512..00000000 --- a/router/tests/test-setPath.js +++ /dev/null @@ -1,175 +0,0 @@ -"use strict" - -var o = require("../../ospec/ospec") -var callAsync = require("../../test-utils/callAsync") -var pushStateMock = require("../../test-utils/pushStateMock") -var Router = require("../../router/router") - -o.spec("Router.setPath", function() { - void [{protocol: "http:", hostname: "localhost"}, {protocol: "file:", hostname: "/"}].forEach(function(env) { - void ["#", "?", "", "#!", "?!", "/foo"].forEach(function(prefix) { - o.spec("using prefix `" + prefix + "` starting on " + env.protocol + "//" + env.hostname, function() { - var $window, router, onRouteChange, onFail - - function defineRoutes(routes, defaultRoute) { - router.defineRoutes(routes, onRouteChange, onFail, defaultRoute, function() {}) - } - - o.beforeEach(function() { - $window = pushStateMock(env) - router = new Router($window) - router.prefix = prefix - onRouteChange = o.spy() - onFail = o.spy() - }) - - o("setPath calls onRouteChange asynchronously", function(done) { - $window.location.href = prefix + "/a" - defineRoutes({"/a": {data: 1}, "/b": {data: 2}}) - - callAsync(function() { - 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" - defineRoutes({"/a": {data: 1}, "/b": {data: 2}}) - - callAsync(function() { - router.setPath("/c") - - o(onFail.callCount).equals(0) - callAsync(function() { - o(onFail.callCount).equals(1) - done() - }) - }) - }) - o("sets route via API", function(done) { - $window.location.href = prefix + "/test" - defineRoutes({"/test": {data: 1}, "/other/:a/:b...": {data: 2}}) - - callAsync(function() { - router.setPath("/other/x/y/z?c=d#e=f") - - o(router.getPath()).equals("/other/x/y/z?c=d#e=f") - - done() - }) - }) - o("sets route w/ escaped unicode", function(done) { - $window.location.href = prefix + "/test" - defineRoutes({"/test": {data: 1}, "/ö/:a/:b...": {data: 2}}) - - callAsync(function() { - router.setPath("/%C3%B6?%C3%B6=%C3%B6#%C3%B6=%C3%B6") - - o(router.getPath()).equals("/ö?ö=ö#ö=ö") - - done() - }) - }) - o("sets route w/ unicode", function(done) { - $window.location.href = prefix + "/test" - defineRoutes({"/test": {data: 1}, "/ö/:a/:b...": {data: 2}}) - - callAsync(function() { - router.setPath("/ö?ö=ö#ö=ö") - - o(router.getPath()).equals("/ö?ö=ö#ö=ö") - - done() - }) - }) - - o("sets route on fallback mode", function(done) { - $window.location.href = "file://" + prefix + "/test" - - router = new Router($window) - router.prefix = prefix - - defineRoutes({"/test": {data: 1}, "/other/:a/:b...": {data: 2}}) - - callAsync(function() { - router.setPath("/other/x/y/z?c=d#e=f") - - o(router.getPath()).equals("/other/x/y/z?c=d#e=f") - - done() - }) - }) - o("sets route via pushState/onpopstate", function(done) { - $window.location.href = prefix + "/test" - defineRoutes({"/test": {data: 1}, "/other/:a/:b...": {data: 2}}) - - callAsync(function() { - $window.history.pushState(null, null, prefix + "/other/x/y/z?c=d#e=f") - $window.onpopstate() - - o(router.getPath()).equals("/other/x/y/z?c=d#e=f") - - done() - }) - }) - o("sets parameterized route", function(done) { - $window.location.href = prefix + "/test" - defineRoutes({"/test": {data: 1}, "/other/:a/:b...": {data: 2}}) - - callAsync(function() { - router.setPath("/other/:a/:b", {a: "x", b: "y/z", c: "d", e: "f"}) - - o(router.getPath()).equals("/other/x/y%2Fz?c=d&e=f") - - done() - }) - }) - o("replace:true works", function(done) { - $window.location.href = prefix + "/test" - defineRoutes({"/test": {data: 1}, "/other": {data: 2}}) - - callAsync(function() { - router.setPath("/other", null, {replace: true}) - $window.history.back() - - o($window.location.href).equals(env.protocol + "//" + (env.hostname === "/" ? "" : env.hostname) + "/") - - done() - }) - }) - o("replace:false works", function(done) { - $window.location.href = prefix + "/test" - defineRoutes({"/test": {data: 1}, "/other": {data: 2}}) - - callAsync(function() { - router.setPath("/other", null, {replace: false}) - $window.history.back() - - var slash = prefix[0] === "/" ? "" : "/" - - o($window.location.href).equals(env.protocol + "//" + (env.hostname === "/" ? "" : env.hostname) + slash + (prefix ? prefix + "/" : "") + "test") - - done() - }) - }) - o("state works", function(done) { - $window.location.href = prefix + "/test" - defineRoutes({"/test": {data: 1}, "/other": {data: 2}}) - - callAsync(function() { - router.setPath("/other", null, {state: {a: 1}}) - - o($window.history.state).deepEquals({a: 1}) - - done() - }) - }) - }) - }) - }) -}) diff --git a/test-utils/tests/test-throttleMock.js b/test-utils/tests/test-throttleMock.js index 69920623..fd00d2bd 100644 --- a/test-utils/tests/test-throttleMock.js +++ b/test-utils/tests/test-throttleMock.js @@ -4,88 +4,35 @@ var o = require("../../ospec/ospec") var throttleMocker = require("../../test-utils/throttleMock") o.spec("throttleMock", function() { - o("works with one callback", function() { + o("schedules one callback", function() { var throttleMock = throttleMocker() var spy = o.spy() o(throttleMock.queueLength()).equals(0) - - var throttled = throttleMock.throttle(spy) - - o(throttleMock.queueLength()).equals(0) - o(spy.callCount).equals(0) - - throttled() - + throttleMock.schedule(spy) o(throttleMock.queueLength()).equals(1) o(spy.callCount).equals(0) - - throttled() - - o(throttleMock.queueLength()).equals(1) - o(spy.callCount).equals(0) - throttleMock.fire() - o(throttleMock.queueLength()).equals(0) o(spy.callCount).equals(1) - - throttleMock.fire() - - o(spy.callCount).equals(1) }) - o("works with two callbacks", function() { + o("schedules two callbacks", function() { var throttleMock = throttleMocker() var spy1 = o.spy() var spy2 = o.spy() o(throttleMock.queueLength()).equals(0) - - var throttled1 = throttleMock.throttle(spy1) - - o(throttleMock.queueLength()).equals(0) - o(spy1.callCount).equals(0) - o(spy2.callCount).equals(0) - - throttled1() - + throttleMock.schedule(spy1) o(throttleMock.queueLength()).equals(1) o(spy1.callCount).equals(0) o(spy2.callCount).equals(0) - - throttled1() - - o(throttleMock.queueLength()).equals(1) - o(spy1.callCount).equals(0) - o(spy2.callCount).equals(0) - - var throttled2 = throttleMock.throttle(spy2) - - o(throttleMock.queueLength()).equals(1) - o(spy1.callCount).equals(0) - o(spy2.callCount).equals(0) - - throttled2() - + throttleMock.schedule(spy2) o(throttleMock.queueLength()).equals(2) o(spy1.callCount).equals(0) o(spy2.callCount).equals(0) - - throttled2() - - o(throttleMock.queueLength()).equals(2) - o(spy1.callCount).equals(0) - o(spy2.callCount).equals(0) - throttleMock.fire() - o(throttleMock.queueLength()).equals(0) o(spy1.callCount).equals(1) o(spy2.callCount).equals(1) - - throttleMock.fire() - - o(spy1.callCount).equals(1) - o(spy2.callCount).equals(1) }) }) diff --git a/test-utils/throttleMock.js b/test-utils/throttleMock.js index 6cdb5710..21eb53be 100644 --- a/test-utils/throttleMock.js +++ b/test-utils/throttleMock.js @@ -3,17 +3,8 @@ module.exports = function() { var queue = [] return { - throttle: function(fn) { - var pending = false - return function() { - if (!pending) { - queue.push(function(){ - pending = false - fn() - }) - pending = true - } - } + schedule: function(fn) { + queue.push(fn) }, fire: function() { var tasks = queue