diff --git a/limiter.js b/api/limiter.js similarity index 63% rename from limiter.js rename to api/limiter.js index ec35a80e..64202901 100644 --- a/limiter.js +++ b/api/limiter.js @@ -1,26 +1,28 @@ +"use strict" + var FRAME_BUDGET = 16 // 60 frames per second = 1 call per 16 ms module.exports = function($window, render) { var rAF = $window.requestAnimationFrame || $window.setTimeout - var cAF = $window.cancelAnimationFrame || $window.clearTimeout var last = 0 - var pending + var pending = null - return function() { + return function(force) { var now = new Date() - // First render, OR if the time since the last render is greater - // than the frame budget - // just immediately render - if(!last || now - last > FRAME_BUDGET) { + // Immediately render if: + // Forced + // Haven't rendered yet + // Time since the last render is greater than the frame budget + if(force || !last || now - last > FRAME_BUDGET) { last = now; return render() } // Redraw already pending, abort - if(pending) { + if(pending !== null) { return } diff --git a/mount.js b/api/mount.js similarity index 83% rename from mount.js rename to api/mount.js index cf633938..f1526613 100644 --- a/mount.js +++ b/api/mount.js @@ -1,4 +1,6 @@ -var createRenderer = require("./render/render") +"use strict" + +var createRenderer = require("../render/render") var limiter = require("./limiter"); module.exports = function($window, redraw) { diff --git a/router.js b/api/router.js similarity index 83% rename from router.js rename to api/router.js index 5a3f5f62..4da9795d 100644 --- a/router.js +++ b/api/router.js @@ -1,5 +1,7 @@ -var createRenderer = require("./render/render") -var createRouter = require("./router/router") +"use strict" + +var createRenderer = require("../render/render") +var createRouter = require("../router/router") var limiter = require("./limiter") module.exports = function($window, redraw) { diff --git a/api/tests/async.js b/api/tests/async.js new file mode 100644 index 00000000..f3206fd7 --- /dev/null +++ b/api/tests/async.js @@ -0,0 +1,46 @@ +var _fns = [] +var _last = 0 +var _frame = 1000 / 60 + +module.exports = { + setTimeout : function($window) { + $window.setTimeout = typeof window === "object" ? window.setTimeout : global.setTimeout; + $window.clearTimeout = typeof window === "object" ? window.clearTimeout : global.setTimeout; + }, + + requestAnimationFrame : function($window) { + // Modified version of https://github.com/chrisdickinson/raf + // Copyright chrisdickinson I guess? + $window.requestAnimationFrame = typeof window === "object" ? window.requestAnimationFrame : function(fn) { + if(!_fns.length) { + var now = Date.now() + var next = Math.max(0, _frame - (now - _last)) + + _last = next + now + + setTimeout(function() { + var fns = _fns.slice() + + _fns = [] + + for(var i = 0; i < fns.length; i++) { + if(typeof fns[i] !== "function") { + continue + } + + fns[i](_last) + } + }, Math.round(next)) + } + + _fns.push(fn) + + return _fns.length - 1; + } + + $window.cancelAnimationFrame = typeof window === "object" ? window.cancelAnimationFrame : function(handle) { + _fns[handle] = null + }; + } +} + diff --git a/api/tests/index.html b/api/tests/index.html new file mode 100644 index 00000000..bc0d8b51 --- /dev/null +++ b/api/tests/index.html @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/api/tests/test-limiter.js b/api/tests/test-limiter.js new file mode 100644 index 00000000..8d4456ca --- /dev/null +++ b/api/tests/test-limiter.js @@ -0,0 +1,69 @@ +"use strict" + +var o = require("../../ospec/ospec") +var domMock = require("../../test-utils/domMock") +var async = require("./async") + +var limiter = require("../limiter") + +o.spec("fps limiter", function() { + var $window, root + + void [ + "setTimeout", + "requestAnimationFrame", + ].forEach(function(type) { + o.spec(type, function() { + o.beforeEach(function() { + $window = domMock() + + async[type]($window) + }) + + o("is a function", function() { + o(typeof limiter).equals("function") + }) + + o("it returns a function", function() { + o(typeof limiter($window, false)).equals("function") + }) + + o("it runs synchronously the first time", function() { + var spy = o.spy() + var run = limiter($window, spy) + + run() + + o(spy.callCount).equals(1) + }) + + o("it only runs once per tick", function(done) { + var spy = o.spy() + var run = limiter($window, spy) + + run() + run() + run() + + o(spy.callCount).equals(1) + + setTimeout(function() { + o(spy.callCount).equals(2) + + done() + }, 17) + }) + + o("it supports forcing a synchronous redraw", function() { + var spy = o.spy() + var run = limiter($window, spy) + + run() + run() + run(true) + + o(spy.callCount).equals(2) + }) + }) + }) +}) diff --git a/api/tests/test-mount.js b/api/tests/test-mount.js new file mode 100644 index 00000000..a9474761 --- /dev/null +++ b/api/tests/test-mount.js @@ -0,0 +1,114 @@ +"use strict" + +var o = require("../../ospec/ospec") +var domMock = require("../../test-utils/domMock") +var async = require("./async") + +var m = require("../../render/hyperscript") +var createMounter = require("../mount") + +o.spec("m.mount", function() { + var $window, root + + o.beforeEach(function() { + $window = domMock() + async.setTimeout($window) + root = $window.document.body + }) + + o("is a function", function() { + o(typeof createMounter).equals("function") + }) + + o("returns a function after invocation", function() { + o(typeof createMounter()).equals("function") + }) + + o("updates passed in redraw object", function() { + var redraw = {} + var mount = createMounter($window, redraw) + + mount(root, { + view : function() { + return m("div") + } + }) + + o(typeof redraw.run).equals("function") + }) + + o("renders into `root`", function() { + var mount = createMounter($window, {}) + + mount(root, { + view : function() { + return m("div") + } + }) + + o(root.firstChild.nodeName).equals("DIV") + }) + + o("redraws on redraw.run()", function(done) { + var onupdate = o.spy() + var oninit = o.spy() + var redraw = {} + var mount = createMounter($window, redraw) + + mount(root, { + view : function() { + return m("div", { + oninit : oninit, + onupdate : onupdate + }) + } + }) + + o(oninit.callCount).equals(1) + + redraw.run() + + // Wrapped to give time for the rate-limited redraw to fire + setTimeout(function() { + o(onupdate.callCount).equals(1) + + done() + }, 20) + }) + + o("redraws on events", function(done, timeout) { + var onupdate = o.spy() + var oninit = o.spy() + var onclick = o.spy() + var mount = createMounter($window, {}) + var e = $window.document.createEvent("MouseEvents") + + e.initEvent("click", true, true) + + mount(root, { + view : function() { + return m("div", { + oninit : oninit, + onupdate : onupdate, + onclick : onclick, + }) + } + }) + + root.firstChild.dispatchEvent(e) + + o(oninit.callCount).equals(1) + + o(onclick.callCount).equals(1) + o(onclick.this).equals(root.firstChild) + o(onclick.args[0].type).equals("click") + o(onclick.args[0].target).equals(root.firstChild) + + // Wrapped to give time for the rate-limited redraw to fire + setTimeout(function() { + o(onupdate.callCount).equals(1) + + done() + }, 20) + }) +}) diff --git a/api/tests/test-router.js b/api/tests/test-router.js new file mode 100644 index 00000000..e7a0aab5 --- /dev/null +++ b/api/tests/test-router.js @@ -0,0 +1,191 @@ +"use strict" + +var o = require("../../ospec/ospec") +var pushStateMock = require("../../test-utils/pushStateMock") +var domMock = require("../../test-utils/domMock") +var async = require("./async") + +var m = require("../../render/hyperscript") +// Convention would be `createRouter`, but that causes variable shadowing bugs +// in browsers when running tests, so `makeRouter` it is +var makeRouter = require("../router") + +o.spec("m.route", function() { + var $window, root, router + + void [ + "setTimeout", + "requestAnimationFrame" + ].forEach(function(timing) { + o.spec(timing, function() { + void [ + "#", + "?", + "#!", + "?!", + "" + ].forEach(function(prefix) { + var spec = prefix ? "prefix " + prefix : "pushstate"; + + o.spec(spec, function() { + o.beforeEach(function() { + var dom = domMock() + var location = pushStateMock() + + // Generate a DOM + Location mock + Object.keys(location).forEach(function(key) { + dom[key] = location[key] + }) + + $window = dom + async[timing]($window) + root = $window.document.body + }) + + o("is a function", function() { + o(typeof makeRouter).equals("function") + }) + + o("returns a function after invocation", function() { + o(typeof makeRouter($window)).equals("function") + }) + + o("updates passed in redraw object", function() { + var redraw = {} + var router = makeRouter($window, redraw) + + router.prefix(prefix) + + router(root, "/", { + "/" : { + view: function() { + return m("div") + } + } + }) + + o(typeof redraw.run).equals("function") + }) + + o("renders into `root`", function() { + var router = makeRouter($window, {}) + + router.prefix(prefix) + + router(root, "/", { + "/" : { + view: function() { + return m("div") + } + } + }) + + o(root.firstChild.nodeName).equals("DIV") + }) + + o("redraws on redraw.run()", function(done) { + var onupdate = o.spy() + var oninit = o.spy() + var redraw = {} + var router = makeRouter($window, redraw) + + router.prefix(prefix) + + router(root, "/", { + "/" : { + view: function() { + return m("div", { + oninit: oninit, + onupdate: onupdate + }) + } + } + }) + + o(oninit.callCount).equals(1) + + redraw.run() + + // Wrapped to give time for the rate-limited redraw to fire + setTimeout(function() { + o(onupdate.callCount).equals(1) + + done() + }, 20) + }) + + o("redraws on events", function(done, timeout) { + var onupdate = o.spy() + var oninit = o.spy() + var onclick = o.spy() + var router = makeRouter($window, {}) + var e = $window.document.createEvent("MouseEvents") + + e.initEvent("click", true, true) + + router.prefix(prefix) + + router(root, "/", { + "/" : { + view: function() { + return m("div", { + oninit: oninit, + onupdate: onupdate, + onclick: onclick, + }) + } + } + }) + + root.firstChild.dispatchEvent(e) + + o(oninit.callCount).equals(1) + + o(onclick.callCount).equals(1) + o(onclick.this).equals(root.firstChild) + o(onclick.args[0].type).equals("click") + o(onclick.args[0].target).equals(root.firstChild) + + // Wrapped to give time for the rate-limited redraw to fire + setTimeout(function() { + o(onupdate.callCount).equals(1) + + done() + }, 20) + }) + + o("changes location on route.link", function() { + var router = makeRouter($window, {}) + var e = $window.document.createEvent("MouseEvents") + + e.initEvent("click", true, true) + + router.prefix(prefix) + + router(root, "/", { + "/" : { + view: function() { + return m("a", { + href: "/test", + oncreate: router.link + }) + } + }, + "/test" : { + view : function() { + return m("div") + } + } + }) + + o($window.location.href).equals("http://localhost/" + (prefix ? prefix + "/" : "")) + + root.firstChild.dispatchEvent(e) + + o($window.location.href).equals("http://localhost/" + (prefix ? prefix + "/test" : "test")) + }) + }) + }) + }) + }) +}) diff --git a/bundler/bundler.js b/bundler/bundler.js index c551649a..46787186 100644 --- a/bundler/bundler.js +++ b/bundler/bundler.js @@ -9,7 +9,7 @@ function resolve(dir, data) { var replacements = [] data = data.replace(/((?:var|let|const|)\s*)([\w_$]+)(\s*=\s*)require\(([^\)]+)\)/g, function(match, def, variable, eq, dep) { var filename = new Function("return " + dep).call() - var pathname = path.dirname(filename) + var pathname = path.dirname(path.resolve(dir, filename)) var normalized = path.normalize(dir + "/" + filename) if (modules[normalized] === undefined) { modules[normalized] = variable diff --git a/index.js b/index.js index c2bf5b9e..deaf3d1f 100644 --- a/index.js +++ b/index.js @@ -2,8 +2,8 @@ var m = require("./render/hyperscript") var trust = require("./render/trust") -var createMounter = require("./mount") -var createRouterInstance = require("./router") +var createMounter = require("./api/mount") +var createRouterInstance = require("./api/router") var createRequester = require("./request/request") var redraw = {run: function() {}} diff --git a/test-redraw.html b/test-redraw.html deleted file mode 100644 index bffd0396..00000000 --- a/test-redraw.html +++ /dev/null @@ -1,57 +0,0 @@ - - - - - -
- - - - - diff --git a/test-utils/domMock.js b/test-utils/domMock.js index 753a803f..e8bf9095 100644 --- a/test-utils/domMock.js +++ b/test-utils/domMock.js @@ -173,6 +173,9 @@ module.exports = function() { events[e.type][i].call(this, e) } } + e.preventDefault = function() { + // TODO: should this do something? + } if (typeof this["on" + e.type] === "function" && !isModernEvent(e.type)) this["on" + e.type](e) }, } @@ -333,4 +336,4 @@ module.exports = function() { activeElement = $window.document.body return $window -} \ No newline at end of file +}