From c636c2f2d12fc7b6b4e5d6f650394f4b835492d1 Mon Sep 17 00:00:00 2001 From: Claudia Meadows Date: Wed, 25 Sep 2024 21:11:28 -0700 Subject: [PATCH 01/95] Enable ES2015 lint and minifier code gen support --- .eslintrc.js | 3 +++ scripts/bundler.js | 2 +- scripts/minify-stream.js | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index e0734315d..a16e2d8e3 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -7,6 +7,9 @@ module.exports = { "es6": true, "node": true }, + "parserOptions": { + "ecmaVersion": 2015 + }, "extends": "eslint:recommended", "rules": { "accessor-pairs": "error", diff --git a/scripts/bundler.js b/scripts/bundler.js index 2c3f63333..2c29a1914 100644 --- a/scripts/bundler.js +++ b/scripts/bundler.js @@ -42,7 +42,7 @@ async function build() { return } console.log("minifying...") - const minified = Terser.minify(original) + const minified = Terser.minify(original, {ecma: 2015}) if (minified.error) throw new Error(minified.error) await writeFile(params.output, minified.code, "utf-8") const originalSize = Buffer.byteLength(original, "utf-8") diff --git a/scripts/minify-stream.js b/scripts/minify-stream.js index 1ca13281c..de62a3219 100644 --- a/scripts/minify-stream.js +++ b/scripts/minify-stream.js @@ -35,7 +35,7 @@ async function minify() { const input = path.resolve(__dirname, "../stream/stream.js") const output = path.resolve(__dirname, "../stream/stream.min.js") const original = await fs.readFile(input, "utf-8") - const minified = Terser.minify(original) + const minified = Terser.minify(original, {ecma: 2015}) if (minified.error) throw new Error(minified.error) await fs.writeFile(output, minified.code, "utf-8") const originalSize = Buffer.byteLength(original, "utf-8") From 3e97cc9a6b20693b58311a9e575ae2a1191a3e46 Mon Sep 17 00:00:00 2001 From: Claudia Meadows Date: Wed, 25 Sep 2024 21:19:36 -0700 Subject: [PATCH 02/95] Optimize generated IIFE slightly --- scripts/_bundler-impl.js | 4 +-- scripts/tests/test-bundler.js | 62 +++++++++++++++++------------------ 2 files changed, 33 insertions(+), 33 deletions(-) diff --git a/scripts/_bundler-impl.js b/scripts/_bundler-impl.js index 49c458d55..279a59663 100644 --- a/scripts/_bundler-impl.js +++ b/scripts/_bundler-impl.js @@ -170,12 +170,12 @@ module.exports = async (input) => { + (rest ? `\n${def}${variable}${eq}_${uuid}` : "") // if `rest` is truthy, it means the expression is fluent or higher-order (e.g. require(path).foo or require(path)(foo) } - const code = ";(function() {\n" + + const code = ";(()=>{\n" + (await process(path.resolve(input), await readFile(input, "utf-8"))) .replace(/^\s*((?:var|let|const|)[\t ]*)([\w_$\.]+)(\s*=\s*)(\2)(?=[\s]+(\w)|;|$)/gm, "") // remove assignments to self .replace(/;+(\r|\n|$)/g, ";$1") // remove redundant semicolons .replace(/(\r|\n)+/g, "\n").replace(/(\r|\n)$/, "") + // remove multiline breaks - "\n}());" + "\n})();" //try {new Function(code); console.log(`build completed at ${new Date()}`)} catch (e) {} error = null diff --git a/scripts/tests/test-bundler.js b/scripts/tests/test-bundler.js index b9208aa06..efcb9f5af 100644 --- a/scripts/tests/test-bundler.js +++ b/scripts/tests/test-bundler.js @@ -39,7 +39,7 @@ o.spec("bundler", async () => { "b.js": "module.exports = 1", }) - o(await bundle(p("a.js"))).equals(";(function() {\nvar b = 1\n}());") + o(await bundle(p("a.js"))).equals(";(()=>{\nvar b = 1\n})();") }) o("relative imports works with semicolons", async () => { await setup({ @@ -47,7 +47,7 @@ o.spec("bundler", async () => { "b.js": "module.exports = 1;", }) - o(await bundle(p("a.js"))).equals(";(function() {\nvar b = 1;\n}());") + o(await bundle(p("a.js"))).equals(";(()=>{\nvar b = 1;\n})();") }) o("relative imports works with let", async () => { await setup({ @@ -55,7 +55,7 @@ o.spec("bundler", async () => { "b.js": "module.exports = 1", }) - o(await bundle(p("a.js"))).equals(";(function() {\nlet b = 1\n}());") + o(await bundle(p("a.js"))).equals(";(()=>{\nlet b = 1\n})();") }) o("relative imports works with const", async () => { await setup({ @@ -63,7 +63,7 @@ o.spec("bundler", async () => { "b.js": "module.exports = 1", }) - o(await bundle(p("a.js"))).equals(";(function() {\nconst b = 1\n}());") + o(await bundle(p("a.js"))).equals(";(()=>{\nconst b = 1\n})();") }) o("relative imports works with assignment", async () => { await setup({ @@ -71,7 +71,7 @@ o.spec("bundler", async () => { "b.js": "module.exports = 1", }) - o(await bundle(p("a.js"))).equals(";(function() {\nvar a = {}\na.b = 1\n}());") + o(await bundle(p("a.js"))).equals(";(()=>{\nvar a = {}\na.b = 1\n})();") }) o("relative imports works with reassignment", async () => { await setup({ @@ -79,7 +79,7 @@ o.spec("bundler", async () => { "b.js": "module.exports = 1", }) - o(await bundle(p("a.js"))).equals(";(function() {\nvar b = {}\nb = 1\n}());") + o(await bundle(p("a.js"))).equals(";(()=>{\nvar b = {}\nb = 1\n})();") }) o("relative imports removes extra use strict", async () => { await setup({ @@ -87,7 +87,7 @@ o.spec("bundler", async () => { "b.js": '"use strict"\nmodule.exports = 1', }) - o(await bundle(p("a.js"))).equals(';(function() {\n"use strict"\nvar b = 1\n}());') + o(await bundle(p("a.js"))).equals(';(()=>{\n"use strict"\nvar b = 1\n})();') }) o("relative imports removes extra use strict using single quotes", async () => { await setup({ @@ -95,7 +95,7 @@ o.spec("bundler", async () => { "b.js": "'use strict'\nmodule.exports = 1", }) - o(await bundle(p("a.js"))).equals(";(function() {\n'use strict'\nvar b = 1\n}());") + o(await bundle(p("a.js"))).equals(";(()=>{\n'use strict'\nvar b = 1\n})();") }) o("relative imports removes extra use strict using mixed quotes", async () => { await setup({ @@ -103,7 +103,7 @@ o.spec("bundler", async () => { "b.js": "'use strict'\nmodule.exports = 1", }) - o(await bundle(p("a.js"))).equals(';(function() {\n"use strict"\nvar b = 1\n}());') + o(await bundle(p("a.js"))).equals(';(()=>{\n"use strict"\nvar b = 1\n})();') }) o("works w/ window", async () => { await setup({ @@ -111,7 +111,7 @@ o.spec("bundler", async () => { "b.js": "module.exports = function() {return a}", }) - o(await bundle(p("a.js"))).equals(";(function() {\nwindow.a = 1\nvar b = function() {return a}\n}());") + o(await bundle(p("a.js"))).equals(";(()=>{\nwindow.a = 1\nvar b = function() {return a}\n})();") }) o("works without assignment", async () => { await setup({ @@ -119,7 +119,7 @@ o.spec("bundler", async () => { "b.js": "1 + 1", }) - o(await bundle(p("a.js"))).equals(";(function() {\n1 + 1\n}());") + o(await bundle(p("a.js"))).equals(";(()=>{\n1 + 1\n})();") }) o("works if used fluently", async () => { await setup({ @@ -127,7 +127,7 @@ o.spec("bundler", async () => { "b.js": "module.exports = []", }) - o(await bundle(p("a.js"))).equals(";(function() {\nvar _0 = []\nvar b = _0.toString()\n}());") + o(await bundle(p("a.js"))).equals(";(()=>{\nvar _0 = []\nvar b = _0.toString()\n})();") }) o("works if used fluently w/ multiline", async () => { await setup({ @@ -135,7 +135,7 @@ o.spec("bundler", async () => { "b.js": "module.exports = []", }) - o(await bundle(p("a.js"))).equals(";(function() {\nvar _0 = []\nvar b = _0\n\t.toString()\n}());") + o(await bundle(p("a.js"))).equals(";(()=>{\nvar _0 = []\nvar b = _0\n\t.toString()\n})();") }) o("works if used w/ curry", async () => { await setup({ @@ -143,7 +143,7 @@ o.spec("bundler", async () => { "b.js": "module.exports = function() {}", }) - o(await bundle(p("a.js"))).equals(";(function() {\nvar _0 = function() {}\nvar b = _0()\n}());") + o(await bundle(p("a.js"))).equals(";(()=>{\nvar _0 = function() {}\nvar b = _0()\n})();") }) o("works if used w/ curry w/ multiline", async () => { await setup({ @@ -151,7 +151,7 @@ o.spec("bundler", async () => { "b.js": "module.exports = function() {}", }) - o(await bundle(p("a.js"))).equals(";(function() {\nvar _0 = function() {}\nvar b = _0\n()\n}());") + o(await bundle(p("a.js"))).equals(";(()=>{\nvar _0 = function() {}\nvar b = _0\n()\n})();") }) o("works if used fluently in one place and not in another", async () => { await setup({ @@ -160,7 +160,7 @@ o.spec("bundler", async () => { "c.js": 'var b = require("./b")\nmodule.exports = function() {return b}', }) - o(await bundle(p("a.js"))).equals(";(function() {\nvar _0 = []\nvar b = _0.toString()\nvar b0 = _0\nvar c = function() {return b0}\n}());") + o(await bundle(p("a.js"))).equals(";(()=>{\nvar _0 = []\nvar b = _0.toString()\nvar b0 = _0\nvar c = function() {return b0}\n})();") }) o("works if used in sequence", async () => { await setup({ @@ -169,7 +169,7 @@ o.spec("bundler", async () => { "c.js": "var x\nmodule.exports = 2", }) - o(await bundle(p("a.js"))).equals(";(function() {\nvar b = 1\nvar x\nvar c = 2\n}());") + o(await bundle(p("a.js"))).equals(";(()=>{\nvar b = 1\nvar x\nvar c = 2\n})();") }) o("works if assigned to property", async () => { await setup({ @@ -178,7 +178,7 @@ o.spec("bundler", async () => { "c.js": "var cc = 2\nmodule.exports = cc", }) - o(await bundle(p("a.js"))).equals(";(function() {\nvar x = {}\nvar bb = 1\nx.b = bb\nvar cc = 2\nx.c = cc\n}());") + o(await bundle(p("a.js"))).equals(";(()=>{\nvar x = {}\nvar bb = 1\nx.b = bb\nvar cc = 2\nx.c = cc\n})();") }) o("works if assigned to property using bracket notation", async () => { await setup({ @@ -187,7 +187,7 @@ o.spec("bundler", async () => { "c.js": "var cc = 2\nmodule.exports = cc", }) - o(await bundle(p("a.js"))).equals(';(function() {\nvar x = {}\nvar bb = 1\nx["b"] = bb\nvar cc = 2\nx["c"] = cc\n}());') + o(await bundle(p("a.js"))).equals(';(()=>{\nvar x = {}\nvar bb = 1\nx["b"] = bb\nvar cc = 2\nx["c"] = cc\n})();') }) o("works if collision", async () => { await setup({ @@ -195,7 +195,7 @@ o.spec("bundler", async () => { "b.js": "var b = 1\nmodule.exports = 2", }) - o(await bundle(p("a.js"))).equals(";(function() {\nvar b0 = 1\nvar b = 2\n}());") + o(await bundle(p("a.js"))).equals(";(()=>{\nvar b0 = 1\nvar b = 2\n})();") }) o("works if multiple aliases", async () => { await setup({ @@ -204,7 +204,7 @@ o.spec("bundler", async () => { "c.js": "var b = {}\nmodule.exports = b", }) - o(await bundle(p("a.js"))).equals(";(function() {\nvar b = {}\nb.x = 1\n}());") + o(await bundle(p("a.js"))).equals(";(()=>{\nvar b = {}\nb.x = 1\n})();") }) o("works if multiple collision", async () => { await setup({ @@ -214,7 +214,7 @@ o.spec("bundler", async () => { "d.js": "var a = 3\nmodule.exports = a", }) - o(await bundle(p("a.js"))).equals(";(function() {\nvar a = 1\nvar b = a\nvar a0 = 2\nvar c = a0\nvar a1 = 3\nvar d = a1\n}());") + o(await bundle(p("a.js"))).equals(";(()=>{\nvar a = 1\nvar b = a\nvar a0 = 2\nvar c = a0\nvar a1 = 3\nvar d = a1\n})();") }) o("works if included multiple times", async () => { await setup({ @@ -223,7 +223,7 @@ o.spec("bundler", async () => { "c.js": 'var a = require("./a").toString()\nvar b = require("./b")', }) - o(await bundle(p("c.js"))).equals(";(function() {\nvar _0 = 123\nvar a = _0.toString()\nvar a0 = _0.toString()\nvar b = a0\n}());") + o(await bundle(p("c.js"))).equals(";(()=>{\nvar _0 = 123\nvar a = _0.toString()\nvar a0 = _0.toString()\nvar b = a0\n})();") }) o("works if included multiple times reverse", async () => { await setup({ @@ -232,7 +232,7 @@ o.spec("bundler", async () => { "c.js": 'var b = require("./b")\nvar a = require("./a").toString()', }) - o(await bundle(p("c.js"))).equals(";(function() {\nvar _0 = 123\nvar a0 = _0.toString()\nvar b = a0\nvar a = _0.toString()\n}());") + o(await bundle(p("c.js"))).equals(";(()=>{\nvar _0 = 123\nvar a0 = _0.toString()\nvar b = a0\nvar a = _0.toString()\n})();") }) o("reuses binding if possible", async () => { await setup({ @@ -242,7 +242,7 @@ o.spec("bundler", async () => { "d.js": "module.exports = 1", }) - o(await bundle(p("a.js"))).equals(";(function() {\nvar d = 1\nvar b = function() {return d + 1}\nvar c = function() {return d + 2}\n}());") + o(await bundle(p("a.js"))).equals(";(()=>{\nvar d = 1\nvar b = function() {return d + 1}\nvar c = function() {return d + 2}\n})();") }) o("disambiguates conflicts if imported collides with itself", async () => { await setup({ @@ -250,7 +250,7 @@ o.spec("bundler", async () => { "b.js": "var b = 1\nmodule.exports = function() {return b}", }) - o(await bundle(p("a.js"))).equals(";(function() {\nvar b0 = 1\nvar b = function() {return b0}\n}());") + o(await bundle(p("a.js"))).equals(";(()=>{\nvar b0 = 1\nvar b = function() {return b0}\n})();") }) o("disambiguates conflicts if imported collides with something else", async () => { await setup({ @@ -258,7 +258,7 @@ o.spec("bundler", async () => { "b.js": "var a = 2\nmodule.exports = function() {return a}", }) - o(await bundle(p("a.js"))).equals(";(function() {\nvar a = 1\nvar a0 = 2\nvar b = function() {return a0}\n}());") + o(await bundle(p("a.js"))).equals(";(()=>{\nvar a = 1\nvar a0 = 2\nvar b = function() {return a0}\n})();") }) o("disambiguates conflicts if imported collides with function declaration", async () => { await setup({ @@ -266,7 +266,7 @@ o.spec("bundler", async () => { "b.js": "var a = 2\nmodule.exports = function() {return a}", }) - o(await bundle(p("a.js"))).equals(";(function() {\nfunction a() {}\nvar a0 = 2\nvar b = function() {return a0}\n}());") + o(await bundle(p("a.js"))).equals(";(()=>{\nfunction a() {}\nvar a0 = 2\nvar b = function() {return a0}\n})();") }) o("disambiguates conflicts if imported collides with another module's private", async () => { await setup({ @@ -275,7 +275,7 @@ o.spec("bundler", async () => { "c.js": "var a = 2\nmodule.exports = function() {return a}", }) - o(await bundle(p("a.js"))).equals(";(function() {\nvar a = 1\nvar b = function() {return a}\nvar a0 = 2\nvar c = function() {return a0}\n}());") + o(await bundle(p("a.js"))).equals(";(()=>{\nvar a = 1\nvar b = function() {return a}\nvar a0 = 2\nvar c = function() {return a0}\n})();") }) o("does not mess up strings", async () => { await setup({ @@ -283,7 +283,7 @@ o.spec("bundler", async () => { "b.js": 'var b = "b b b \\" b"\nmodule.exports = function() {return b}', }) - o(await bundle(p("a.js"))).equals(';(function() {\nvar b0 = "b b b \\\" b"\nvar b = function() {return b0}\n}());') + o(await bundle(p("a.js"))).equals(';(()=>{\nvar b0 = "b b b \\\" b"\nvar b = function() {return b0}\n})();') }) o("does not mess up properties", async () => { await setup({ @@ -291,6 +291,6 @@ o.spec("bundler", async () => { "b.js": "var b = {b: 1}\nmodule.exports = function() {return b.b}", }) - o(await bundle(p("a.js"))).equals(";(function() {\nvar b0 = {b: 1}\nvar b = function() {return b0.b}\n}());") + o(await bundle(p("a.js"))).equals(";(()=>{\nvar b0 = {b: 1}\nvar b = function() {return b0.b}\n})();") }) }) From f07feb91ac56fba743d6f48df695011842cb0e74 Mon Sep 17 00:00:00 2001 From: Claudia Meadows Date: Wed, 25 Sep 2024 21:59:39 -0700 Subject: [PATCH 03/95] Add keyed perf tests, assert no duplicates in keyed fragments --- performance/test-perf.js | 31 +++++++++++++++++++++++++++++++ render/vnode.js | 14 +++++++++----- 2 files changed, 40 insertions(+), 5 deletions(-) diff --git a/performance/test-perf.js b/performance/test-perf.js index 6d0891fd9..6f6ff1137 100644 --- a/performance/test-perf.js +++ b/performance/test-perf.js @@ -482,6 +482,37 @@ suite.add("repeated add/removal", { }, }) +suite.add("reorder keyed list", { + setup: function () { + const keys = [] + for (let i = 0; i < 1000; i++) keys.push(`key-${i}`) + + function shuffle() { + // Performs a simple Fisher-Yates shuffle. + let current = keys.length + while (current) { + // eslint-disable-next-line no-bitwise + const index = (Math.random() * current--) | 0 + const temp = keys[index] + keys[index] = keys[current] + keys[current] = temp + } + } + + this.app = function () { + shuffle() + var vnodes = [] + for (const key of keys) { + vnodes.push(m("div.item", {key})) + } + return vnodes + } + }, + fn: function () { + m.render(rootElem, this.app()) + }, +}) + if (isDOM) { window.onload = function () { cycleRoot() diff --git a/render/vnode.js b/render/vnode.js index ec19b174f..f5a9d1919 100644 --- a/render/vnode.js +++ b/render/vnode.js @@ -10,9 +10,9 @@ Vnode.normalize = function(node) { return Vnode("#", undefined, undefined, String(node), undefined, undefined) } Vnode.normalizeChildren = function(input) { - var children = [] if (input.length) { var isKeyed = input[0] != null && input[0].key != null + var keys = new Set() // Note: this is a *very* perf-sensitive check. // Fun fact: merging the loop like this is somehow faster than splitting // it, noticeably so. @@ -24,12 +24,16 @@ Vnode.normalizeChildren = function(input) { : "In fragments, vnodes must either all have keys or none have keys." ) } + if (isKeyed) { + if (keys.has(input[i].key)) { + throw new TypeError(`Duplicate key detected: ${input[i].key}`) + } + keys.add(input[i].key) + } } - for (var i = 0; i < input.length; i++) { - children[i] = Vnode.normalize(input[i]) - } + input = input.map(Vnode.normalize) } - return children + return input } module.exports = Vnode From f291d285fd75d693550d5723bc5cc8c593f0c341 Mon Sep 17 00:00:00 2001 From: Claudia Meadows Date: Thu, 26 Sep 2024 00:24:58 -0700 Subject: [PATCH 04/95] Add duplicate key check, deduplicate code, use ES6 Perf is somewhat improved in spots, but generally the same otherwise. --- README.md | 2 +- mithril.min.js | 2 +- render/fragment.js | 11 +-- render/hyperscript.js | 112 ++++++++++++++++--------------- render/hyperscriptVnode.js | 53 --------------- render/render.js | 62 ++++++++--------- render/tests/test-hyperscript.js | 76 ++++++++++----------- render/trust.js | 2 +- render/vnode.js | 8 +-- 9 files changed, 138 insertions(+), 190 deletions(-) delete mode 100644 render/hyperscriptVnode.js diff --git a/README.md b/README.md index f4bc9bee8..0c07750d1 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ ## What is Mithril.js? -A modern client-side JavaScript framework for building Single Page Applications. It's small (9.05 KB gzipped), fast and provides routing and XHR utilities out of the box. +A modern client-side JavaScript framework for building Single Page Applications. It's small (8.99 KB gzipped), fast and provides routing and XHR utilities out of the box. Mithril.js is used by companies like Vimeo and Nike, and open source platforms like Lichess 👍. diff --git a/mithril.min.js b/mithril.min.js index bf762f12e..5f064a231 100644 --- a/mithril.min.js +++ b/mithril.min.js @@ -1 +1 @@ -!function(){"use strict";function e(e,t,n,r,o,l){return{tag:e,key:t,attrs:n,children:r,text:o,dom:l,domSize:void 0,state:void 0,events:void 0,instance:void 0}}e.normalize=function(t){return Array.isArray(t)?e("[",void 0,void 0,e.normalizeChildren(t),void 0,void 0):null==t||"boolean"==typeof t?null:"object"==typeof t?t:e("#",void 0,void 0,String(t),void 0,void 0)},e.normalizeChildren=function(t){var n=[];if(t.length){for(var r=null!=t[0]&&null!=t[0].key,o=1;o0&&(a.className=i.join(" ")),function(e){for(var t in e)if(n.call(e,t))return!1;return!0}(a)&&(a=null),o[e]={tag:l,attrs:a}}function i(e,t){var r=t.attrs,o=n.call(r,"class"),l=o?r.class:r.className;return t.tag=e.tag,null!=e.attrs?(r=Object.assign({},e.attrs,r),null==l&&null==e.attrs.className||(r.className=null!=l?null!=e.attrs.className?String(e.attrs.className)+" "+String(l):l:null!=e.attrs.className?e.attrs.className:null)):null!=l&&(r.className=l),o&&(r.class=null),"input"===e.tag&&n.call(r,"type")&&(r=Object.assign({type:r.type},r)),t.attrs=r,t}function a(n){if(null==n||"string"!=typeof n&&"function"!=typeof n&&"function"!=typeof n.view)throw Error("The selector must be either a string or a component.");var r=t.apply(1,arguments);return"string"==typeof n&&(r.children=e.normalizeChildren(r.children),"["!==n)?i(o[n]||l(n),r):(r.tag=n,r)}a.trust=function(t){return null==t&&(t=""),e("<",void 0,void 0,t,void 0,void 0)},a.fragment=function(){var n=t.apply(0,arguments);return n.tag="[",n.children=e.normalizeChildren(n.children),n};var u=new WeakMap;var s={delayedRemoval:u,domFor:function*(e,t={}){var n=e.dom,r=e.domSize,o=t.generation;if(null!=n)do{var l=n.nextSibling;u.get(n)===o&&(yield n,r--),n=l}while(r)}},f=s.delayedRemoval,c=s.domFor,d=function(){var t,n,r={svg:"http://www.w3.org/2000/svg",math:"http://www.w3.org/1998/Math/MathML"};function o(e){return e.ownerDocument}function l(e){return e.attrs&&e.attrs.xmlns||r[e.tag]}function i(e,t){if(e.state!==t)throw new Error("'vnode.state' must not be modified.")}function a(e){var t=e.state;try{return this.apply(t,arguments)}finally{i(e,t)}}function u(e){try{return o(e).activeElement}catch(e){return null}}function s(e,t,n,r,o,l,i){for(var a=n;a'+t.children+"",i=i.firstChild):i.innerHTML=t.children,t.dom=i.firstChild,t.domSize=i.childNodes.length;for(var a,u=o(e).createDocumentFragment();a=i.firstChild;)u.appendChild(a);x(e,u,r)}function v(e,t,n,r,o,l){if(t!==n&&(null!=t||null!=n))if(null==t||0===t.length)s(e,n,0,n.length,r,o,l);else if(null==n||0===n.length)S(e,t,0,t.length);else{var i=null!=t[0]&&null!=t[0].key,a=null!=n[0]&&null!=n[0].key,u=0,f=0;if(!i)for(;f=f&&z>=u&&(m=t[k],v=n[z],m.key===v.key);)m!==v&&h(e,m,v,r,o,l),null!=v.dom&&(o=v.dom),k--,z--;for(;k>=f&&z>=u&&(c=t[f],p=n[u],c.key===p.key);)f++,u++,c!==p&&h(e,c,p,r,w(t,f,o),l);for(;k>=f&&z>=u&&u!==z&&c.key===v.key&&m.key===p.key;)b(e,m,x=w(t,f,o)),m!==p&&h(e,m,p,r,x,l),++u<=--z&&b(e,c,o),c!==v&&h(e,c,v,r,o,l),null!=v.dom&&(o=v.dom),f++,m=t[--k],v=n[z],c=t[f],p=n[u];for(;k>=f&&z>=u&&m.key===v.key;)m!==v&&h(e,m,v,r,o,l),null!=v.dom&&(o=v.dom),z--,m=t[--k],v=n[z];if(u>z)S(e,t,f,k+1);else if(f>k)s(e,n,u,z+1,r,o,l);else{var j,O,A=o,C=z-u+1,T=new Array(C),N=0,$=0,L=2147483647,R=0;for($=0;$=u;$--){null==j&&(j=y(t,f,k+1));var I=j[(v=n[$]).key];null!=I&&(L=I>>1)+(r>>>1)+(n&r&1);e[t[a]]0&&(g[o]=t[n-1]),t[n]=o)}}n=t.length,r=t[n-1];for(;n-- >0;)t[n]=r,r=g[r];return g.length=0,t}(T)).length-1,$=z;$>=u;$--)p=n[$],-1===T[$-u]?d(e,p,r,l,o):O[N]===$-u?N--:b(e,p,o),null!=p.dom&&(o=n[$].dom);else for($=z;$>=u;$--)p=n[$],-1===T[$-u]&&d(e,p,r,l,o),null!=p.dom&&(o=n[$].dom)}}else{var P=t.lengthP&&S(e,t,u,t.length),n.length>P&&s(e,n,u,n.length,r,o,l)}}}function h(t,n,r,o,i,u){var s=n.tag;if(s===r.tag){if(r.state=n.state,r.events=n.events,function(e,t){do{var n;if(null!=e.attrs&&"function"==typeof e.attrs.onbeforeupdate)if(void 0!==(n=a.call(e.attrs.onbeforeupdate,e,t))&&!n)break;if("string"!=typeof e.tag&&"function"==typeof e.state.onbeforeupdate)if(void 0!==(n=a.call(e.state.onbeforeupdate,e,t))&&!n)break;return!1}while(0);return e.dom=t.dom,e.domSize=t.domSize,e.instance=t.instance,e.attrs=t.attrs,e.children=t.children,e.text=t.text,!0}(r,n))return;if("string"==typeof s)switch(null!=r.attrs&&_(r.attrs,r,o),s){case"#":!function(e,t){e.children.toString()!==t.children.toString()&&(e.dom.nodeValue=t.children);t.dom=e.dom}(n,r);break;case"<":!function(e,t,n,r,o){t.children!==n.children?(z(e,t,void 0),m(e,n,r,o)):(n.dom=t.dom,n.domSize=t.domSize)}(t,n,r,u,i);break;case"[":!function(e,t,n,r,o,l){v(e,t.children,n.children,r,o,l);var i=0,a=n.children;if(n.dom=null,null!=a){for(var u=0;u-1||null!=e.attrs&&e.attrs.is||"href"!==t&&"list"!==t&&"form"!==t&&"width"!==t&&"height"!==t)&&t in e.dom}var N,$=/[A-Z]/g;function L(e){return"-"+e.toLowerCase()}function R(e){return"-"===e[0]&&"-"===e[1]?e:"cssFloat"===e?"float":e.replace($,L)}function I(e,t,n){if(t===n);else if(null==n)e.style="";else if("object"!=typeof n)e.style=n;else if(null==t||"object"!=typeof t)for(var r in e.style.cssText="",n){null!=(o=n[r])&&e.style.setProperty(R(r),String(o))}else{for(var r in n){var o;null!=(o=n[r])&&(o=String(o))!==String(t[r])&&e.style.setProperty(R(r),o)}for(var r in t)null!=t[r]&&null==n[r]&&e.style.removeProperty(R(r))}}function P(){this._=t}function D(e,n,r){if(null!=e.events){if(e.events._=t,e.events[n]===r)return;null==r||"function"!=typeof r&&"object"!=typeof r?(null!=e.events[n]&&e.dom.removeEventListener(n.slice(2),e.events,!1),e.events[n]=void 0):(null==e.events[n]&&e.dom.addEventListener(n.slice(2),e.events,!1),e.events[n]=r)}else null==r||"function"!=typeof r&&"object"!=typeof r||(e.events=new P,e.dom.addEventListener(n.slice(2),e.events,!1),e.events[n]=r)}function F(e,t,n){"function"==typeof e.oninit&&a.call(e.oninit,t),"function"==typeof e.oncreate&&n.push(a.bind(e.oncreate,t))}function _(e,t,n){"function"==typeof e.onupdate&&n.push(a.bind(e.onupdate,t))}return P.prototype=Object.create(null),P.prototype.handleEvent=function(e){var t,n=this["on"+e.type];"function"==typeof n?t=n.call(e.currentTarget,e):"function"==typeof n.handleEvent&&n.handleEvent(e),this._&&!1!==e.redraw&&(0,this._)(),!1===t&&(e.preventDefault(),e.stopPropagation())},function(r,o,l){if(!r)throw new TypeError("DOM element being rendered to does not exist.");if(null!=N&&r.contains(N))throw new TypeError("Node is currently being rendered to and thus is locked.");var i=t,a=N,s=[],f=u(r),c=r.namespaceURI;N=r,t="function"==typeof l?l:void 0,n={};try{null==r.vnodes&&(r.textContent=""),o=e.normalizeChildren(Array.isArray(o)?o:[o]),v(r,r.vnodes,o,s,null,"http://www.w3.org/1999/xhtml"===c?void 0:c),r.vnodes=o,null!=f&&u(r)!==f&&"function"==typeof f.focus&&f.focus();for(var d=0;d=0&&(o.splice(l,2),l<=i&&(i-=2),t(n,[])),null!=r&&(o.push(n,r),t(n,e(r),u))},redraw:u}}(d,"undefined"!=typeof requestAnimationFrame?requestAnimationFrame:null,"undefined"!=typeof console?console:null),m=function(e){if("[object Object]"!==Object.prototype.toString.call(e))return"";var t=[];for(var n in e)r(n,e[n]);return t.join("&");function r(e,n){if(Array.isArray(n))for(var o=0;o=0&&(p+=e.slice(n,o)),s>=0&&(p+=(n<0?"?":"&")+u.slice(s,c));var v=m(a);return v&&(p+=(n<0&&s<0?"?":"&")+v),r>=0&&(p+=e.slice(r)),f>=0&&(p+=(r<0?"":"&")+u.slice(f)),p},h=function(e,t){function r(e){return new Promise(e)}function o(e,t){for(var r in e.headers)if(n.call(e.headers,r)&&r.toLowerCase()===t)return!0;return!1}return r.prototype=Promise.prototype,r.__proto__=Promise,{request:function(l,i){"string"!=typeof l?(i=l,l=l.url):null==i&&(i={});var a=function(t,r){return new Promise((function(l,i){t=v(t,r.params);var a,u=null!=r.method?r.method.toUpperCase():"GET",s=r.body,f=(null==r.serialize||r.serialize===JSON.serialize)&&!(s instanceof e.FormData||s instanceof e.URLSearchParams),c=r.responseType||("function"==typeof r.extract?"":"json"),d=new e.XMLHttpRequest,p=!1,m=!1,h=d,y=d.abort;for(var g in d.abort=function(){p=!0,y.call(this)},d.open(u,t,!1!==r.async,"string"==typeof r.user?r.user:void 0,"string"==typeof r.password?r.password:void 0),f&&null!=s&&!o(r,"content-type")&&d.setRequestHeader("Content-Type","application/json; charset=utf-8"),"function"==typeof r.deserialize||o(r,"accept")||d.setRequestHeader("Accept","application/json, text/*"),r.withCredentials&&(d.withCredentials=r.withCredentials),r.timeout&&(d.timeout=r.timeout),d.responseType=c,r.headers)n.call(r.headers,g)&&d.setRequestHeader(g,r.headers[g]);d.onreadystatechange=function(e){if(!p&&4===e.target.readyState)try{var n,o=e.target.status>=200&&e.target.status<300||304===e.target.status||/^file:\/\//i.test(t),a=e.target.response;if("json"===c){if(!e.target.responseType&&"function"!=typeof r.extract)try{a=JSON.parse(e.target.responseText)}catch(e){a=null}}else c&&"text"!==c||null==a&&(a=e.target.responseText);if("function"==typeof r.extract?(a=r.extract(e.target,r),o=!0):"function"==typeof r.deserialize&&(a=r.deserialize(a)),o){if("function"==typeof r.type)if(Array.isArray(a))for(var u=0;u-1&&u.pop();for(var f=0;f{"use strict";function e(e,t,n,r){return{tag:e,key:t,attrs:n,children:r,dom:void 0,domSize:void 0,state:void 0,events:void 0,instance:void 0}}e.normalize=function(t){return Array.isArray(t)?e("[",void 0,void 0,e.normalizeChildren(t)):null==t||"boolean"==typeof t?null:"object"==typeof t?t:e("#",void 0,void 0,String(t))},e.normalizeChildren=function(t){if(t.length){for(var n=null!=t[0]&&null!=t[0].key,r=new Set,o=1;o0&&(a.class=i.join(" "));var d={tag:l,attrs:u?a:null};return o.set(e,d),d}(l)),null!=c.attrs&&(u=c.attrs.class,i=Object.assign({},c.attrs,i)),null==f&&null==u||(i!==d&&(i=Object.assign({},i)),i.class=null!=f?null!=u?`${u} ${f}`:f:u,s&&(i.className=null)),e(c.tag,i.key,i,a)}function i(t,n,...r){if(null==t||"string"!=typeof t&&"function"!=typeof t&&"function"!=typeof t.view)throw new Error("The selector must be either a string or a component.");return null==n||"object"==typeof n&&null==n.tag&&!Array.isArray(n)?1===r.length&&Array.isArray(r[0])&&(r=r[0]):(r=0===r.length&&Array.isArray(n)?n:[n,...r],n=void 0),null==n&&(n={}),"string"==typeof t&&(r=e.normalizeChildren(r),"["!==t)?l(t,n,r):e(t,n.key,n,r)}i.trust=function(t){return null==t&&(t=""),e("<",void 0,void 0,t)},i.fragment=function(...e){return i("[",...e)};var a=new WeakMap;var u={delayedRemoval:a,domFor:function*(e,t={}){var n=e.dom,r=e.domSize,o=t.generation;if(null!=n)do{var l=n.nextSibling;a.get(n)===o&&(yield n,r--),n=l}while(r)}},s=u.delayedRemoval,f=u.domFor,c=function(){var t,n,r={svg:"http://www.w3.org/2000/svg",math:"http://www.w3.org/1998/Math/MathML"};function o(e){return e.ownerDocument}function l(e){return e.attrs&&e.attrs.xmlns||r[e.tag]}function i(e,t){if(e.state!==t)throw new Error("'vnode.state' must not be modified.")}function a(e){var t=e.state;try{return this.apply(t,arguments)}finally{i(e,t)}}function u(e){try{return o(e).activeElement}catch(e){return null}}function c(e,t,n,r,o,l,i){for(var a=n;a'+t.children+"",i=i.firstChild):i.innerHTML=t.children,t.dom=i.firstChild,t.domSize=i.childNodes.length;for(var a,u=o(e).createDocumentFragment();a=i.firstChild;)u.appendChild(a);k(e,u,r)}function v(e,t,n,r,o,l){if(t!==n&&(null!=t||null!=n))if(null==t||0===t.length)c(e,n,0,n.length,r,o,l);else if(null==n||0===n.length)S(e,t,0,t.length);else{var i=null!=t[0]&&null!=t[0].key,a=null!=n[0]&&null!=n[0].key,u=0,s=0;if(!i)for(;s=s&&z>=u&&(m=t[x],v=n[z],m.key===v.key);)m!==v&&h(e,m,v,r,o,l),null!=v.dom&&(o=v.dom),x--,z--;for(;x>=s&&z>=u&&(f=t[s],p=n[u],f.key===p.key);)s++,u++,f!==p&&h(e,f,p,r,w(t,s,o),l);for(;x>=s&&z>=u&&u!==z&&f.key===v.key&&m.key===p.key;)b(e,m,k=w(t,s,o)),m!==p&&h(e,m,p,r,k,l),++u<=--z&&b(e,f,o),f!==v&&h(e,f,v,r,o,l),null!=v.dom&&(o=v.dom),s++,m=t[--x],v=n[z],f=t[s],p=n[u];for(;x>=s&&z>=u&&m.key===v.key;)m!==v&&h(e,m,v,r,o,l),null!=v.dom&&(o=v.dom),z--,m=t[--x],v=n[z];if(u>z)S(e,t,s,x+1);else if(s>x)c(e,n,u,z+1,r,o,l);else{var j,O,A=o,C=z-u+1,T=new Array(C),$=0,L=0,R=2147483647,I=0;for(L=0;L=u;L--){null==j&&(j=y(t,s,x+1));var N=j[(v=n[L]).key];null!=N&&(R=N>>1)+(r>>>1)+(n&r&1);e[t[a]]0&&(g[o]=t[n-1]),t[n]=o)}}n=t.length,r=t[n-1];for(;n-- >0;)t[n]=r,r=g[r];return g.length=0,t}(T)).length-1,L=z;L>=u;L--)p=n[L],-1===T[L-u]?d(e,p,r,l,o):O[$]===L-u?$--:b(e,p,o),null!=p.dom&&(o=n[L].dom);else for(L=z;L>=u;L--)p=n[L],-1===T[L-u]&&d(e,p,r,l,o),null!=p.dom&&(o=n[L].dom)}}else{var P=t.lengthP&&S(e,t,u,t.length),n.length>P&&c(e,n,u,n.length,r,o,l)}}}function h(t,n,r,o,i,u){var s=n.tag;if(s===r.tag){if(r.state=n.state,r.events=n.events,function(e,t){do{var n;if(null!=e.attrs&&"function"==typeof e.attrs.onbeforeupdate)if(void 0!==(n=a.call(e.attrs.onbeforeupdate,e,t))&&!n)break;if("string"!=typeof e.tag&&"function"==typeof e.state.onbeforeupdate)if(void 0!==(n=a.call(e.state.onbeforeupdate,e,t))&&!n)break;return!1}while(0);return e.dom=t.dom,e.domSize=t.domSize,e.instance=t.instance,e.attrs=t.attrs,e.children=t.children,!0}(r,n))return;if("string"==typeof s)switch(null!=r.attrs&&q(r.attrs,r,o),s){case"#":!function(e,t){e.children.toString()!==t.children.toString()&&(e.dom.nodeValue=t.children);t.dom=e.dom}(n,r);break;case"<":!function(e,t,n,r,o){t.children!==n.children?(z(e,t,void 0),m(e,n,r,o)):(n.dom=t.dom,n.domSize=t.domSize)}(t,n,r,u,i);break;case"[":!function(e,t,n,r,o,l){v(e,t.children,n.children,r,o,l);var i=0,a=n.children;if(n.dom=null,null!=a){for(var u=0;u-1||null!=e.attrs&&e.attrs.is||!$.has(t))&&t in e.dom}var R,I=/[A-Z]/g;function N(e){return"-"+e.toLowerCase()}function P(e){return"-"===e[0]&&"-"===e[1]?e:"cssFloat"===e?"float":e.replace(I,N)}function D(e,t,n){if(t===n);else if(null==n)e.style="";else if("object"!=typeof n)e.style=n;else if(null==t||"object"!=typeof t)for(var r in e.style.cssText="",n){null!=(o=n[r])&&e.style.setProperty(P(r),String(o))}else{for(var r in n){var o;null!=(o=n[r])&&(o=String(o))!==String(t[r])&&e.style.setProperty(P(r),o)}for(var r in t)null!=t[r]&&null==n[r]&&e.style.removeProperty(P(r))}}function F(){this._=t}function _(e,n,r){if(null!=e.events){if(e.events._=t,e.events[n]===r)return;null==r||"function"!=typeof r&&"object"!=typeof r?(null!=e.events[n]&&e.dom.removeEventListener(n.slice(2),e.events,!1),e.events[n]=void 0):(null==e.events[n]&&e.dom.addEventListener(n.slice(2),e.events,!1),e.events[n]=r)}else null==r||"function"!=typeof r&&"object"!=typeof r||(e.events=new F,e.dom.addEventListener(n.slice(2),e.events,!1),e.events[n]=r)}function M(e,t,n){"function"==typeof e.oninit&&a.call(e.oninit,t),"function"==typeof e.oncreate&&n.push(a.bind(e.oncreate,t))}function q(e,t,n){"function"==typeof e.onupdate&&n.push(a.bind(e.onupdate,t))}return F.prototype=Object.create(null),F.prototype.handleEvent=function(e){var t,n=this["on"+e.type];"function"==typeof n?t=n.call(e.currentTarget,e):"function"==typeof n.handleEvent&&n.handleEvent(e),this._&&!1!==e.redraw&&(0,this._)(),!1===t&&(e.preventDefault(),e.stopPropagation())},function(r,o,l){if(!r)throw new TypeError("DOM element being rendered to does not exist.");if(null!=R&&r.contains(R))throw new TypeError("Node is currently being rendered to and thus is locked.");var i=t,a=R,s=[],f=u(r),c=r.namespaceURI;R=r,t="function"==typeof l?l:void 0,n={};try{null==r.vnodes&&(r.textContent=""),o=e.normalizeChildren(Array.isArray(o)?o:[o]),v(r,r.vnodes,o,s,null,"http://www.w3.org/1999/xhtml"===c?void 0:c),r.vnodes=o,null!=f&&u(r)!==f&&"function"==typeof f.focus&&f.focus();for(var d=0;d=0&&(o.splice(l,2),l<=i&&(i-=2),t(n,[])),null!=r&&(o.push(n,r),t(n,e(r),u))},redraw:u}}(c,"undefined"!=typeof requestAnimationFrame?requestAnimationFrame:null,"undefined"!=typeof console?console:null),p=function(e){if("[object Object]"!==Object.prototype.toString.call(e))return"";var t=[];for(var n in e)r(n,e[n]);return t.join("&");function r(e,n){if(Array.isArray(n))for(var o=0;o=0&&(m+=e.slice(n,o)),s>=0&&(m+=(n<0?"?":"&")+u.slice(s,c));var v=p(a);return v&&(m+=(n<0&&s<0?"?":"&")+v),r>=0&&(m+=e.slice(r)),f>=0&&(m+=(r<0?"":"&")+u.slice(f)),m},v=function(e,n){function r(e){return new Promise(e)}function o(e,n){for(var r in e.headers)if(t.call(e.headers,r)&&r.toLowerCase()===n)return!0;return!1}return r.prototype=Promise.prototype,r.__proto__=Promise,{request:function(l,i){"string"!=typeof l?(i=l,l=l.url):null==i&&(i={});var a=function(n,r){return new Promise((function(l,i){n=m(n,r.params);var a,u=null!=r.method?r.method.toUpperCase():"GET",s=r.body,f=(null==r.serialize||r.serialize===JSON.serialize)&&!(s instanceof e.FormData||s instanceof e.URLSearchParams),c=r.responseType||("function"==typeof r.extract?"":"json"),d=new e.XMLHttpRequest,p=!1,v=!1,h=d,y=d.abort;for(var g in d.abort=function(){p=!0,y.call(this)},d.open(u,n,!1!==r.async,"string"==typeof r.user?r.user:void 0,"string"==typeof r.password?r.password:void 0),f&&null!=s&&!o(r,"content-type")&&d.setRequestHeader("Content-Type","application/json; charset=utf-8"),"function"==typeof r.deserialize||o(r,"accept")||d.setRequestHeader("Accept","application/json, text/*"),r.withCredentials&&(d.withCredentials=r.withCredentials),r.timeout&&(d.timeout=r.timeout),d.responseType=c,r.headers)t.call(r.headers,g)&&d.setRequestHeader(g,r.headers[g]);d.onreadystatechange=function(e){if(!p&&4===e.target.readyState)try{var t,o=e.target.status>=200&&e.target.status<300||304===e.target.status||/^file:\/\//i.test(n),a=e.target.response;if("json"===c){if(!e.target.responseType&&"function"!=typeof r.extract)try{a=JSON.parse(e.target.responseText)}catch(e){a=null}}else c&&"text"!==c||null==a&&(a=e.target.responseText);if("function"==typeof r.extract?(a=r.extract(e.target,r),o=!0):"function"==typeof r.deserialize&&(a=r.deserialize(a)),o){if("function"==typeof r.type)if(Array.isArray(a))for(var u=0;u-1&&u.pop();for(var f=0;f 0) attrs.className = classes.join(" ") - if (isEmpty(attrs)) attrs = null - return selectorCache[selector] = {tag: tag, attrs: attrs} + + if (classes.length > 0) { + attrs.class = classes.join(" ") + } + + var state = {tag, attrs: hasAttrs ? attrs : null} + selectorCache.set(selector, state) + return state } -function execSelector(state, vnode) { - var attrs = vnode.attrs - var hasClass = hasOwn.call(attrs, "class") - var className = hasClass ? attrs.class : attrs.className +function execSelector(selector, attrs, children) { + var hasClassName = hasOwn.call(attrs, "className") + var dynamicClass = hasClassName ? attrs.className : attrs.class + var state = selectorCache.get(selector) + var original = attrs + var selectorClass - vnode.tag = state.tag + if (state == null) { + state = compileSelector(selector) + } if (state.attrs != null) { + selectorClass = state.attrs.class attrs = Object.assign({}, state.attrs, attrs) - - if (className != null || state.attrs.className != null) attrs.className = - className != null - ? state.attrs.className != null - ? String(state.attrs.className) + " " + String(className) - : className - : state.attrs.className != null - ? state.attrs.className - : null - } else { - if (className != null) attrs.className = className } - if (hasClass) attrs.class = null - - // workaround for #2622 (reorder keys in attrs to set "type" first) - // The DOM does things to inputs based on the "type", so it needs set first. - // See: https://github.com/MithrilJS/mithril.js/issues/2622 - if (state.tag === "input" && hasOwn.call(attrs, "type")) { - attrs = Object.assign({type: attrs.type}, attrs) + if (dynamicClass != null || selectorClass != null) { + if (attrs !== original) attrs = Object.assign({}, attrs) + attrs.class = dynamicClass != null + ? selectorClass != null ? `${selectorClass} ${dynamicClass}` : dynamicClass + : selectorClass + if (hasClassName) attrs.className = null } - vnode.attrs = attrs - - return vnode + return Vnode(state.tag, attrs.key, attrs, children) } -function hyperscript(selector) { +// Caution is advised when editing this - it's very perf-critical. It's specially designed to avoid +// allocations in the fast path, especially with fragments. +function hyperscript(selector, attrs, ...children) { if (selector == null || typeof selector !== "string" && typeof selector !== "function" && typeof selector.view !== "function") { - throw Error("The selector must be either a string or a component."); + throw new Error("The selector must be either a string or a component."); + } + + if (attrs == null || typeof attrs === "object" && attrs.tag == null && !Array.isArray(attrs)) { + if (children.length === 1 && Array.isArray(children[0])) children = children[0] + } else { + children = children.length === 0 && Array.isArray(attrs) ? attrs : [attrs, ...children] + attrs = undefined } - var vnode = hyperscriptVnode.apply(1, arguments) + if (attrs == null) attrs = {} if (typeof selector === "string") { - vnode.children = Vnode.normalizeChildren(vnode.children) - if (selector !== "[") return execSelector(selectorCache[selector] || compileSelector(selector), vnode) + children = Vnode.normalizeChildren(children) + if (selector !== "[") return execSelector(selector, attrs, children) } - vnode.tag = selector - return vnode + return Vnode(selector, attrs.key, attrs, children) } module.exports = hyperscript diff --git a/render/hyperscriptVnode.js b/render/hyperscriptVnode.js deleted file mode 100644 index 9f31235c4..000000000 --- a/render/hyperscriptVnode.js +++ /dev/null @@ -1,53 +0,0 @@ -"use strict" - -var Vnode = require("../render/vnode") - -// Call via `hyperscriptVnode.apply(startOffset, arguments)` -// -// The reason I do it this way, forwarding the arguments and passing the start -// offset in `this`, is so I don't have to create a temporary array in a -// performance-critical path. -// -// In native ES6, I'd instead add a final `...args` parameter to the -// `hyperscript` and `fragment` factories and define this as -// `hyperscriptVnode(...args)`, since modern engines do optimize that away. But -// ES5 (what Mithril.js requires thanks to IE support) doesn't give me that luxury, -// and engines aren't nearly intelligent enough to do either of these: -// -// 1. Elide the allocation for `[].slice.call(arguments, 1)` when it's passed to -// another function only to be indexed. -// 2. Elide an `arguments` allocation when it's passed to any function other -// than `Function.prototype.apply` or `Reflect.apply`. -// -// In ES6, it'd probably look closer to this (I'd need to profile it, though): -// module.exports = function(attrs, ...children) { -// if (attrs == null || typeof attrs === "object" && attrs.tag == null && !Array.isArray(attrs)) { -// if (children.length === 1 && Array.isArray(children[0])) children = children[0] -// } else { -// children = children.length === 0 && Array.isArray(attrs) ? attrs : [attrs, ...children] -// attrs = undefined -// } -// -// if (attrs == null) attrs = {} -// return Vnode("", attrs.key, attrs, children) -// } -module.exports = function() { - var attrs = arguments[this], start = this + 1, children - - if (attrs == null) { - attrs = {} - } else if (typeof attrs !== "object" || attrs.tag != null || Array.isArray(attrs)) { - attrs = {} - start = this - } - - if (arguments.length === start + 1) { - children = arguments[start] - if (!Array.isArray(children)) children = [children] - } else { - children = [] - while (start < arguments.length) children.push(arguments[start++]) - } - - return Vnode("", attrs.key, attrs, children) -} diff --git a/render/render.js b/render/render.js index a31d19346..3b399615d 100644 --- a/render/render.js +++ b/render/render.js @@ -675,8 +675,8 @@ module.exports = function() { setAttr(vnode, key, null, attrs[key], ns) } } - function setAttr(vnode, key, old, value, ns) { - if (key === "key" || key === "is" || value == null || isLifecycleMethod(key) || (old === value && !isFormAttribute(vnode, key)) && typeof value !== "object") return + function setAttr(vnode, key, old, value, ns, isFileInput) { + if (value == null || isSpecialAttribute.has(key) || (old === value && !isFormAttribute(vnode, key)) && typeof value !== "object" || key === "type" && vnode.tag === "input") return if (key[0] === "o" && key[1] === "n") return updateEvent(vnode, key, value) if (key.slice(0, 6) === "xlink:") vnode.dom.setAttributeNS("http://www.w3.org/1999/xlink", key.slice(6), value) else if (key === "style") updateStyle(vnode.dom, old, value) @@ -684,37 +684,37 @@ module.exports = function() { if (key === "value") { // Only do the coercion if we're actually going to check the value. /* eslint-disable no-implicit-coercion */ - var isFileInput = vnode.tag === "input" && vnode.attrs.type === "file" - //setting input[value] to same value by typing on focused element moves cursor to end in Chrome - //setting input[type=file][value] to same value causes an error to be generated if it's non-empty - if ((vnode.tag === "input" || vnode.tag === "textarea") && vnode.dom.value === "" + value && (isFileInput || vnode.dom === activeElement(vnode.dom))) return - //setting select[value] to same value while having select open blinks select dropdown in Chrome - if (vnode.tag === "select" && old !== null && vnode.dom.value === "" + value) return - //setting option[value] to same value while having select open blinks select dropdown in Chrome - if (vnode.tag === "option" && old !== null && vnode.dom.value === "" + value) return - //setting input[type=file][value] to different value is an error if it's non-empty - // Not ideal, but it at least works around the most common source of uncaught exceptions for now. - if (isFileInput && "" + value !== "") { console.error("`value` is read-only on file inputs!"); return } + switch (vnode.tag) { + //setting input[value] to same value by typing on focused element moves cursor to end in Chrome + //setting input[type=file][value] to same value causes an error to be generated if it's non-empty + case "input": + case "textarea": + if (vnode.dom.value === "" + value && (isFileInput || vnode.dom === activeElement(vnode.dom))) return + //setting input[type=file][value] to different value is an error if it's non-empty + // Not ideal, but it at least works around the most common source of uncaught exceptions for now. + if (isFileInput && "" + value !== "") { console.error("`value` is read-only on file inputs!"); return } + break + //setting select[value] or option[value] to same value while having select open blinks select dropdown in Chrome + case "select": + case "option": + if (old !== null && vnode.dom.value === "" + value) return + } /* eslint-enable no-implicit-coercion */ } - // If you assign an input type that is not supported by IE 11 with an assignment expression, an error will occur. - if (vnode.tag === "input" && key === "type") vnode.dom.setAttribute(key, value) - else vnode.dom[key] = value + vnode.dom[key] = value + } else if (value === false) { + vnode.dom.removeAttribute(key) } else { - if (typeof value === "boolean") { - if (value) vnode.dom.setAttribute(key, "") - else vnode.dom.removeAttribute(key) - } - else vnode.dom.setAttribute(key === "className" ? "class" : key, value) + vnode.dom.setAttribute(key, value === true ? "" : value) } } function removeAttr(vnode, key, old, ns) { - if (key === "key" || key === "is" || old == null || isLifecycleMethod(key)) return + if (old == null || isSpecialAttribute.has(key)) return if (key[0] === "o" && key[1] === "n") updateEvent(vnode, key, undefined) else if (key === "style") updateStyle(vnode.dom, old, null) else if ( hasPropertyKey(vnode, key, ns) - && key !== "className" + && key !== "class" && key !== "title" // creates "null" as title && !(key === "value" && ( vnode.tag === "option" @@ -726,7 +726,7 @@ module.exports = function() { } else { var nsLastIndex = key.indexOf(":") if (nsLastIndex !== -1) key = key.slice(nsLastIndex + 1) - if (old !== false) vnode.dom.removeAttribute(key === "className" ? "class" : key) + if (old !== false) vnode.dom.removeAttribute(key) } } function setLateSelectAttrs(vnode, attrs) { @@ -760,19 +760,20 @@ module.exports = function() { } } } + var isAlwaysFormAttribute = new Set(["value", "checked", "selected", "selectedIndex"]) + var isSpecialAttribute = new Set(["key", "is", "oninit", "oncreate", "onupdate", "onremove", "onbeforeupdate", "onbeforeremove"]) + // Try to avoid a few browser bugs on normal elements. + // var propertyMayBeBugged = new Set(["href", "list", "form", "width", "height", "type"]) + var propertyMayBeBugged = new Set(["href", "list", "form", "width", "height"]) function isFormAttribute(vnode, attr) { - return attr === "value" || attr === "checked" || attr === "selectedIndex" || attr === "selected" && vnode.dom === activeElement(vnode.dom) || vnode.tag === "option" && vnode.dom.parentNode === activeElement(vnode.dom) - } - function isLifecycleMethod(attr) { - return attr === "oninit" || attr === "oncreate" || attr === "onupdate" || attr === "onremove" || attr === "onbeforeremove" || attr === "onbeforeupdate" + return isAlwaysFormAttribute.has(attr) || attr === "selected" && vnode.dom === activeElement(vnode.dom) || vnode.tag === "option" && vnode.dom.parentNode === activeElement(vnode.dom) } function hasPropertyKey(vnode, key, ns) { // Filter out namespaced keys return ns === undefined && ( // If it's a custom element, just keep it. vnode.tag.indexOf("-") > -1 || vnode.attrs != null && vnode.attrs.is || - // If it's a normal element, let's try to avoid a few browser bugs. - key !== "href" && key !== "list" && key !== "form" && key !== "width" && key !== "height"// && key !== "type" + !propertyMayBeBugged.has(key) // Defer the property check until *after* we check everything. ) && key in vnode.dom } @@ -899,7 +900,6 @@ module.exports = function() { // unlike special "attributes" internally. vnode.attrs = old.attrs vnode.children = old.children - vnode.text = old.text return true } diff --git a/render/tests/test-hyperscript.js b/render/tests/test-hyperscript.js index ed918fe1e..c4675f249 100644 --- a/render/tests/test-hyperscript.js +++ b/render/tests/test-hyperscript.js @@ -25,43 +25,37 @@ o.spec("hyperscript", function() { o(m("a", { class: undefined }).attrs).deepEquals({ - class: null + class: undefined }) o(m("a", { class: false }).attrs).deepEquals({ - class: null, - className: false + class: false }) o(m("a", { class: true }).attrs).deepEquals({ - class: null, - className: true + class: true }) o(m("a.x", { class: null }).attrs).deepEquals({ - class: null, - className: "x" + class: "x" }) o(m("a.x", { class: undefined }).attrs).deepEquals({ - class: null, - className: "x" + class: "x" }) o(m("a.x", { class: false }).attrs).deepEquals({ - class: null, - className: "x false" + class: "x false" }) o(m("a.x", { class: true }).attrs).deepEquals({ - class: null, - className: "x true" + class: "x true" }) o(m("a", { className: null @@ -76,45 +70,51 @@ o.spec("hyperscript", function() { o(m("a", { className: false }).attrs).deepEquals({ - className: false + className: null, + class: false }) o(m("a", { className: true }).attrs).deepEquals({ - className: true + className: null, + class: true }) o(m("a.x", { className: null }).attrs).deepEquals({ - className: "x" + className: null, + class: "x" }) o(m("a.x", { className: undefined }).attrs).deepEquals({ - className: "x" + className: null, + class: "x" }) o(m("a.x", { className: false }).attrs).deepEquals({ - className: "x false" + className: null, + class: "x false" }) o(m("a.x", { className: true }).attrs).deepEquals({ - className: "x true" + className: null, + class: "x true" }) }) o("handles class in selector", function() { var vnode = m(".a") o(vnode.tag).equals("div") - o(vnode.attrs.className).equals("a") + o(vnode.attrs.class).equals("a") }) o("handles many classes in selector", function() { var vnode = m(".a.b.c") o(vnode.tag).equals("div") - o(vnode.attrs.className).equals("a b c") + o(vnode.attrs.class).equals("a b c") }) o("handles id in selector", function() { var vnode = m("#a") @@ -153,35 +153,35 @@ o.spec("hyperscript", function() { o(vnode.tag).equals("div") o(vnode.attrs.x).equals(true) o(vnode.attrs.a).equals("[b]") - o(vnode.attrs.className).equals("c") + o(vnode.attrs.class).equals("c") }) o("handles attr w/ unmatched square bracket", function() { var vnode = m("[a=']'].c") o(vnode.tag).equals("div") o(vnode.attrs.a).equals("]") - o(vnode.attrs.className).equals("c") + o(vnode.attrs.class).equals("c") }) o("handles attr w/ quoted square bracket and quote", function() { var vnode = m("[a='[b\"\\']'].c") // `[a='[b"\']']` o(vnode.tag).equals("div") o(vnode.attrs.a).equals("[b\"']") // `[b"']` - o(vnode.attrs.className).equals("c") + o(vnode.attrs.class).equals("c") }) o("handles attr w/ quoted square containing escaped square bracket", function() { var vnode = m("[a='[\\]]'].c") // `[a='[\]]']` o(vnode.tag).equals("div") o(vnode.attrs.a).equals("[\\]]") // `[\]]` - o(vnode.attrs.className).equals("c") + o(vnode.attrs.class).equals("c") }) o("handles attr w/ backslashes", function() { var vnode = m("[a='\\\\'].c") // `[a='\\']` o(vnode.tag).equals("div") o(vnode.attrs.a).equals("\\") - o(vnode.attrs.className).equals("c") + o(vnode.attrs.class).equals("c") }) o("handles attr w/ quotes and spaces in selector", function() { var vnode = m("[a = 'b']") @@ -200,14 +200,14 @@ o.spec("hyperscript", function() { var vnode = m("a.b[c = 'd']") o(vnode.tag).equals("a") - o(vnode.attrs.className).equals("b") + o(vnode.attrs.class).equals("b") o(vnode.attrs.c).equals("d") }) o("handles tag, mixed classes, attrs in selector", function() { var vnode = m("a.b[c = 'd'].e[f = 'g']") o(vnode.tag).equals("a") - o(vnode.attrs.className).equals("b e") + o(vnode.attrs.class).equals("b e") o(vnode.attrs.c).equals("d") o(vnode.attrs.f).equals("g") }) @@ -284,22 +284,22 @@ o.spec("hyperscript", function() { o("handles className attrs property", function() { var vnode = m("div", {className: "a"}) - o(vnode.attrs.className).equals("a") + o(vnode.attrs.class).equals("a") }) o("handles 'class' as a verbose attribute declaration", function() { var vnode = m("[class=a]") - o(vnode.attrs.className).equals("a") + o(vnode.attrs.class).equals("a") }) o("handles merging classes w/ class property", function() { var vnode = m(".a", {class: "b"}) - o(vnode.attrs.className).equals("a b") + o(vnode.attrs.class).equals("a b") }) o("handles merging classes w/ className property", function() { var vnode = m(".a", {className: "b"}) - o(vnode.attrs.className).equals("a b") + o(vnode.attrs.class).equals("a b") }) }) o.spec("custom element attrs", function() { @@ -356,7 +356,7 @@ o.spec("hyperscript", function() { o("handles className attrs property", function() { var vnode = m("custom-element", {className: "a"}) - o(vnode.attrs.className).equals("a") + o(vnode.attrs.class).equals("a") }) o("casts className using toString like browsers", function() { const className = { @@ -365,7 +365,7 @@ o.spec("hyperscript", function() { } var vnode = m("custom-element" + className, {className: className}) - o(vnode.attrs.className).equals("valueOf toString") + o(vnode.attrs.class).equals("valueOf toString") }) }) o.spec("children", function() { @@ -574,10 +574,10 @@ o.spec("hyperscript", function() { var nodeA = m(".a", attrs) var nodeB = m(".b", attrs) - o(nodeA.attrs.className).equals("a") + o(nodeA.attrs.class).equals("a") o(nodeA.attrs.a).equals("b") - o(nodeB.attrs.className).equals("b") + o(nodeB.attrs.class).equals("b") o(nodeB.attrs.a).equals("b") }) o("handles shared empty attrs (#2821)", function() { @@ -586,8 +586,8 @@ o.spec("hyperscript", function() { var nodeA = m(".a", attrs) var nodeB = m(".b", attrs) - o(nodeA.attrs.className).equals("a") - o(nodeB.attrs.className).equals("b") + o(nodeA.attrs.class).equals("a") + o(nodeB.attrs.class).equals("b") }) o("doesnt modify passed attributes object", function() { var attrs = {a: "b"} diff --git a/render/trust.js b/render/trust.js index 5995e287e..6cc3fcdc3 100644 --- a/render/trust.js +++ b/render/trust.js @@ -4,5 +4,5 @@ var Vnode = require("../render/vnode") module.exports = function(html) { if (html == null) html = "" - return Vnode("<", undefined, undefined, html, undefined, undefined) + return Vnode("<", undefined, undefined, html) } diff --git a/render/vnode.js b/render/vnode.js index f5a9d1919..e3c68092e 100644 --- a/render/vnode.js +++ b/render/vnode.js @@ -1,13 +1,13 @@ "use strict" -function Vnode(tag, key, attrs, children, text, dom) { - return {tag: tag, key: key, attrs: attrs, children: children, text: text, dom: dom, domSize: undefined, state: undefined, events: undefined, instance: undefined} +function Vnode(tag, key, attrs, children) { + return {tag, key, attrs, children, dom: undefined, domSize: undefined, state: undefined, events: undefined, instance: undefined} } Vnode.normalize = function(node) { - if (Array.isArray(node)) return Vnode("[", undefined, undefined, Vnode.normalizeChildren(node), undefined, undefined) + if (Array.isArray(node)) return Vnode("[", undefined, undefined, Vnode.normalizeChildren(node)) if (node == null || typeof node === "boolean") return null if (typeof node === "object") return node - return Vnode("#", undefined, undefined, String(node), undefined, undefined) + return Vnode("#", undefined, undefined, String(node)) } Vnode.normalizeChildren = function(input) { if (input.length) { From 01063679a62681af56b347b724f867be5fe95799 Mon Sep 17 00:00:00 2001 From: Claudia Meadows Date: Thu, 26 Sep 2024 00:44:17 -0700 Subject: [PATCH 05/95] Normalize xlink, simplify attrs update a bit --- render/render.js | 14 +++++++++----- render/tests/test-attributes.js | 8 ++------ test-utils/domMock.js | 14 ++++++++------ test-utils/tests/test-domMock.js | 9 +-------- 4 files changed, 20 insertions(+), 25 deletions(-) diff --git a/render/render.js b/render/render.js index 3b399615d..005351969 100644 --- a/render/render.js +++ b/render/render.js @@ -6,6 +6,7 @@ var delayedRemoval = df.delayedRemoval var domFor = df.domFor module.exports = function() { + var xlinkNs = "http://www.w3.org/1999/xlink" var nameSpace = { svg: "http://www.w3.org/2000/svg", math: "http://www.w3.org/1998/Math/MathML" @@ -671,14 +672,18 @@ module.exports = function() { //attrs function setAttrs(vnode, attrs, ns) { + // The DOM does things to inputs based on the value, so it needs set first. + // See: https://github.com/MithrilJS/mithril.js/issues/2622 + if (vnode.tag === "input" && attrs.type != null) vnode.dom.type = attrs.type + var isFileInput = attrs != null && vnode.tag === "input" && attrs.type === "file" for (var key in attrs) { setAttr(vnode, key, null, attrs[key], ns) } } function setAttr(vnode, key, old, value, ns, isFileInput) { if (value == null || isSpecialAttribute.has(key) || (old === value && !isFormAttribute(vnode, key)) && typeof value !== "object" || key === "type" && vnode.tag === "input") return - if (key[0] === "o" && key[1] === "n") return updateEvent(vnode, key, value) - if (key.slice(0, 6) === "xlink:") vnode.dom.setAttributeNS("http://www.w3.org/1999/xlink", key.slice(6), value) + if (key.startsWith("on")) updateEvent(vnode, key, value) + else if (key.startsWith("xlink:")) vnode.dom.setAttributeNS(xlinkNs, key.slice(6), value) else if (key === "style") updateStyle(vnode.dom, old, value) else if (hasPropertyKey(vnode, key, ns)) { if (key === "value") { @@ -710,7 +715,8 @@ module.exports = function() { } function removeAttr(vnode, key, old, ns) { if (old == null || isSpecialAttribute.has(key)) return - if (key[0] === "o" && key[1] === "n") updateEvent(vnode, key, undefined) + if (key.startsWith("on")) updateEvent(vnode, key, undefined) + else if (key.startsWith("xlink:")) vnode.dom.removeAttributeNS(xlinkNs, key.slice(6)) else if (key === "style") updateStyle(vnode.dom, old, null) else if ( hasPropertyKey(vnode, key, ns) @@ -724,8 +730,6 @@ module.exports = function() { ) { vnode.dom[key] = null } else { - var nsLastIndex = key.indexOf(":") - if (nsLastIndex !== -1) key = key.slice(nsLastIndex + 1) if (old !== false) vnode.dom.removeAttribute(key) } } diff --git a/render/tests/test-attributes.js b/render/tests/test-attributes.js index 1daed5e5f..4a4628781 100644 --- a/render/tests/test-attributes.js +++ b/render/tests/test-attributes.js @@ -293,8 +293,8 @@ o.spec("attributes", function() { }) }) o.spec("input.type", function() { - o("the input.type setter is never used", function() { - var $window = domMock({spy: o.spy}) + o("works", function() { + var $window = domMock() var root = $window.document.body var render = vdom($window) @@ -303,19 +303,15 @@ o.spec("attributes", function() { var c = m("input") render(root, a) - var spies = $window.__getSpies(a.dom) - o(spies.typeSetter.callCount).equals(0) o(a.dom.getAttribute("type")).equals("radio") render(root, b) - o(spies.typeSetter.callCount).equals(0) o(b.dom.getAttribute("type")).equals("text") render(root, c) - o(spies.typeSetter.callCount).equals(0) o(c.dom.hasAttribute("type")).equals(false) }) }) diff --git a/test-utils/domMock.js b/test-utils/domMock.js index 5536b49fd..bb1b5e242 100644 --- a/test-utils/domMock.js +++ b/test-utils/domMock.js @@ -169,6 +169,10 @@ module.exports = function(options) { function removeAttribute(name) { delete this.attributes[name] } + function removeAttributeNS(_ns, name) { + // Namespace is ignored for now + delete this.attributes[name] + } function hasAttribute(name) { return name in this.attributes } @@ -304,6 +308,7 @@ module.exports = function(options) { setAttribute: setAttribute, setAttributeNS: setAttributeNS, removeAttribute: removeAttribute, + removeAttributeNS: removeAttributeNS, parentNode: null, childNodes: [], attributes: {}, @@ -530,10 +535,6 @@ module.exports = function(options) { enumerable: true, }) - // we currently emulate the non-ie behavior, but emulating ie may be more useful (throw when an invalid type is set) - var typeSetter = spy(function(v) { - this.setAttribute("type", v) - }) Object.defineProperty(element, "type", { get: function() { if (!this.hasAttribute("type")) return "text" @@ -543,12 +544,13 @@ module.exports = function(options) { ? type : "text" }, - set: typeSetter, + set: function(v) { + this.setAttribute("type", v) + }, enumerable: true, }) registerSpies(element, { valueSetter: valueSetter, - typeSetter: typeSetter }) } diff --git a/test-utils/tests/test-domMock.js b/test-utils/tests/test-domMock.js index 36d3364cb..fbd965367 100644 --- a/test-utils/tests/test-domMock.js +++ b/test-utils/tests/test-domMock.js @@ -1817,7 +1817,7 @@ o.spec("domMock", function() { o(typeof $window.__getSpies).equals("function") o("__getSpies" in domMock()).equals(false) }) - o("input elements have spies on value and type setters", function() { + o("input elements have spies on value setters", function() { var input = $window.document.createElement("input") var spies = $window.__getSpies(input) @@ -1825,20 +1825,13 @@ o.spec("domMock", function() { o(typeof spies).equals("object") o(spies).notEquals(null) o(typeof spies.valueSetter).equals("function") - o(typeof spies.typeSetter).equals("function") o(spies.valueSetter.callCount).equals(0) - o(spies.typeSetter.callCount).equals(0) input.value = "aaa" - input.type = "radio" o(spies.valueSetter.callCount).equals(1) o(spies.valueSetter.this).equals(input) o(spies.valueSetter.args[0]).equals("aaa") - - o(spies.typeSetter.callCount).equals(1) - o(spies.typeSetter.this).equals(input) - o(spies.typeSetter.args[0]).equals("radio") }) o("select elements have spies on value setters", function() { var select = $window.document.createElement("select") From c3e90f53d2ceed80768e71e45e94116c5e239784 Mon Sep 17 00:00:00 2001 From: Claudia Meadows Date: Thu, 26 Sep 2024 00:59:45 -0700 Subject: [PATCH 06/95] Drop `m.trust` support Cuts only about 2% of the bundle, but removes a pretty bad pain point in the code. Plus, inserting arbitrary HTML outside a container is usually a recipe for disaster in terms of styling. --- README.md | 2 +- hyperscript.js | 7 +- index.js | 1 - mithril.min.js | 2 +- render/fragment.js | 7 -- render/hyperscript.js | 4 + render/render.js | 43 +------- render/tests/test-attributes.js | 14 --- render/tests/test-createFragment.js | 2 +- render/tests/test-createHTML.js | 96 ------------------ render/tests/test-createNodes.js | 21 ++-- render/tests/test-domFor.js | 4 +- render/tests/test-fragment.js | 3 +- render/tests/test-onbeforeremove.js | 5 +- render/tests/test-onbeforeupdate.js | 5 +- render/tests/test-oncreate.js | 3 +- render/tests/test-oninit.js | 3 +- render/tests/test-onremove.js | 5 +- render/tests/test-onupdate.js | 5 +- render/tests/test-trust.js | 31 ------ render/tests/test-updateFragment.js | 21 ++-- render/tests/test-updateHTML.js | 116 --------------------- render/tests/test-updateNodes.js | 151 ++++++++-------------------- render/trust.js | 8 -- tests/test-api.js | 8 -- 25 files changed, 80 insertions(+), 487 deletions(-) delete mode 100644 render/fragment.js delete mode 100644 render/tests/test-createHTML.js delete mode 100644 render/tests/test-trust.js delete mode 100644 render/tests/test-updateHTML.js delete mode 100644 render/trust.js diff --git a/README.md b/README.md index 0c07750d1..5db1ace57 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ ## What is Mithril.js? -A modern client-side JavaScript framework for building Single Page Applications. It's small (8.99 KB gzipped), fast and provides routing and XHR utilities out of the box. +A modern client-side JavaScript framework for building Single Page Applications. It's small (8.71 KB gzipped), fast and provides routing and XHR utilities out of the box. Mithril.js is used by companies like Vimeo and Nike, and open source platforms like Lichess 👍. diff --git a/hyperscript.js b/hyperscript.js index 16bf033a8..69f4e0882 100644 --- a/hyperscript.js +++ b/hyperscript.js @@ -1,8 +1,3 @@ "use strict" -var hyperscript = require("./render/hyperscript") - -hyperscript.trust = require("./render/trust") -hyperscript.fragment = require("./render/fragment") - -module.exports = hyperscript +module.exports = require("./render/hyperscript") diff --git a/index.js b/index.js index b6ca3406a..632668646 100644 --- a/index.js +++ b/index.js @@ -7,7 +7,6 @@ var domFor = require("./render/domFor") var m = function m() { return hyperscript.apply(this, arguments) } m.m = hyperscript -m.trust = hyperscript.trust m.fragment = hyperscript.fragment m.Fragment = "[" m.mount = mountRedraw.mount diff --git a/mithril.min.js b/mithril.min.js index 5f064a231..847b7f0d5 100644 --- a/mithril.min.js +++ b/mithril.min.js @@ -1 +1 @@ -(()=>{"use strict";function e(e,t,n,r){return{tag:e,key:t,attrs:n,children:r,dom:void 0,domSize:void 0,state:void 0,events:void 0,instance:void 0}}e.normalize=function(t){return Array.isArray(t)?e("[",void 0,void 0,e.normalizeChildren(t)):null==t||"boolean"==typeof t?null:"object"==typeof t?t:e("#",void 0,void 0,String(t))},e.normalizeChildren=function(t){if(t.length){for(var n=null!=t[0]&&null!=t[0].key,r=new Set,o=1;o0&&(a.class=i.join(" "));var d={tag:l,attrs:u?a:null};return o.set(e,d),d}(l)),null!=c.attrs&&(u=c.attrs.class,i=Object.assign({},c.attrs,i)),null==f&&null==u||(i!==d&&(i=Object.assign({},i)),i.class=null!=f?null!=u?`${u} ${f}`:f:u,s&&(i.className=null)),e(c.tag,i.key,i,a)}function i(t,n,...r){if(null==t||"string"!=typeof t&&"function"!=typeof t&&"function"!=typeof t.view)throw new Error("The selector must be either a string or a component.");return null==n||"object"==typeof n&&null==n.tag&&!Array.isArray(n)?1===r.length&&Array.isArray(r[0])&&(r=r[0]):(r=0===r.length&&Array.isArray(n)?n:[n,...r],n=void 0),null==n&&(n={}),"string"==typeof t&&(r=e.normalizeChildren(r),"["!==t)?l(t,n,r):e(t,n.key,n,r)}i.trust=function(t){return null==t&&(t=""),e("<",void 0,void 0,t)},i.fragment=function(...e){return i("[",...e)};var a=new WeakMap;var u={delayedRemoval:a,domFor:function*(e,t={}){var n=e.dom,r=e.domSize,o=t.generation;if(null!=n)do{var l=n.nextSibling;a.get(n)===o&&(yield n,r--),n=l}while(r)}},s=u.delayedRemoval,f=u.domFor,c=function(){var t,n,r={svg:"http://www.w3.org/2000/svg",math:"http://www.w3.org/1998/Math/MathML"};function o(e){return e.ownerDocument}function l(e){return e.attrs&&e.attrs.xmlns||r[e.tag]}function i(e,t){if(e.state!==t)throw new Error("'vnode.state' must not be modified.")}function a(e){var t=e.state;try{return this.apply(t,arguments)}finally{i(e,t)}}function u(e){try{return o(e).activeElement}catch(e){return null}}function c(e,t,n,r,o,l,i){for(var a=n;a'+t.children+"",i=i.firstChild):i.innerHTML=t.children,t.dom=i.firstChild,t.domSize=i.childNodes.length;for(var a,u=o(e).createDocumentFragment();a=i.firstChild;)u.appendChild(a);k(e,u,r)}function v(e,t,n,r,o,l){if(t!==n&&(null!=t||null!=n))if(null==t||0===t.length)c(e,n,0,n.length,r,o,l);else if(null==n||0===n.length)S(e,t,0,t.length);else{var i=null!=t[0]&&null!=t[0].key,a=null!=n[0]&&null!=n[0].key,u=0,s=0;if(!i)for(;s=s&&z>=u&&(m=t[x],v=n[z],m.key===v.key);)m!==v&&h(e,m,v,r,o,l),null!=v.dom&&(o=v.dom),x--,z--;for(;x>=s&&z>=u&&(f=t[s],p=n[u],f.key===p.key);)s++,u++,f!==p&&h(e,f,p,r,w(t,s,o),l);for(;x>=s&&z>=u&&u!==z&&f.key===v.key&&m.key===p.key;)b(e,m,k=w(t,s,o)),m!==p&&h(e,m,p,r,k,l),++u<=--z&&b(e,f,o),f!==v&&h(e,f,v,r,o,l),null!=v.dom&&(o=v.dom),s++,m=t[--x],v=n[z],f=t[s],p=n[u];for(;x>=s&&z>=u&&m.key===v.key;)m!==v&&h(e,m,v,r,o,l),null!=v.dom&&(o=v.dom),z--,m=t[--x],v=n[z];if(u>z)S(e,t,s,x+1);else if(s>x)c(e,n,u,z+1,r,o,l);else{var j,O,A=o,C=z-u+1,T=new Array(C),$=0,L=0,R=2147483647,I=0;for(L=0;L=u;L--){null==j&&(j=y(t,s,x+1));var N=j[(v=n[L]).key];null!=N&&(R=N>>1)+(r>>>1)+(n&r&1);e[t[a]]0&&(g[o]=t[n-1]),t[n]=o)}}n=t.length,r=t[n-1];for(;n-- >0;)t[n]=r,r=g[r];return g.length=0,t}(T)).length-1,L=z;L>=u;L--)p=n[L],-1===T[L-u]?d(e,p,r,l,o):O[$]===L-u?$--:b(e,p,o),null!=p.dom&&(o=n[L].dom);else for(L=z;L>=u;L--)p=n[L],-1===T[L-u]&&d(e,p,r,l,o),null!=p.dom&&(o=n[L].dom)}}else{var P=t.lengthP&&S(e,t,u,t.length),n.length>P&&c(e,n,u,n.length,r,o,l)}}}function h(t,n,r,o,i,u){var s=n.tag;if(s===r.tag){if(r.state=n.state,r.events=n.events,function(e,t){do{var n;if(null!=e.attrs&&"function"==typeof e.attrs.onbeforeupdate)if(void 0!==(n=a.call(e.attrs.onbeforeupdate,e,t))&&!n)break;if("string"!=typeof e.tag&&"function"==typeof e.state.onbeforeupdate)if(void 0!==(n=a.call(e.state.onbeforeupdate,e,t))&&!n)break;return!1}while(0);return e.dom=t.dom,e.domSize=t.domSize,e.instance=t.instance,e.attrs=t.attrs,e.children=t.children,!0}(r,n))return;if("string"==typeof s)switch(null!=r.attrs&&q(r.attrs,r,o),s){case"#":!function(e,t){e.children.toString()!==t.children.toString()&&(e.dom.nodeValue=t.children);t.dom=e.dom}(n,r);break;case"<":!function(e,t,n,r,o){t.children!==n.children?(z(e,t,void 0),m(e,n,r,o)):(n.dom=t.dom,n.domSize=t.domSize)}(t,n,r,u,i);break;case"[":!function(e,t,n,r,o,l){v(e,t.children,n.children,r,o,l);var i=0,a=n.children;if(n.dom=null,null!=a){for(var u=0;u-1||null!=e.attrs&&e.attrs.is||!$.has(t))&&t in e.dom}var R,I=/[A-Z]/g;function N(e){return"-"+e.toLowerCase()}function P(e){return"-"===e[0]&&"-"===e[1]?e:"cssFloat"===e?"float":e.replace(I,N)}function D(e,t,n){if(t===n);else if(null==n)e.style="";else if("object"!=typeof n)e.style=n;else if(null==t||"object"!=typeof t)for(var r in e.style.cssText="",n){null!=(o=n[r])&&e.style.setProperty(P(r),String(o))}else{for(var r in n){var o;null!=(o=n[r])&&(o=String(o))!==String(t[r])&&e.style.setProperty(P(r),o)}for(var r in t)null!=t[r]&&null==n[r]&&e.style.removeProperty(P(r))}}function F(){this._=t}function _(e,n,r){if(null!=e.events){if(e.events._=t,e.events[n]===r)return;null==r||"function"!=typeof r&&"object"!=typeof r?(null!=e.events[n]&&e.dom.removeEventListener(n.slice(2),e.events,!1),e.events[n]=void 0):(null==e.events[n]&&e.dom.addEventListener(n.slice(2),e.events,!1),e.events[n]=r)}else null==r||"function"!=typeof r&&"object"!=typeof r||(e.events=new F,e.dom.addEventListener(n.slice(2),e.events,!1),e.events[n]=r)}function M(e,t,n){"function"==typeof e.oninit&&a.call(e.oninit,t),"function"==typeof e.oncreate&&n.push(a.bind(e.oncreate,t))}function q(e,t,n){"function"==typeof e.onupdate&&n.push(a.bind(e.onupdate,t))}return F.prototype=Object.create(null),F.prototype.handleEvent=function(e){var t,n=this["on"+e.type];"function"==typeof n?t=n.call(e.currentTarget,e):"function"==typeof n.handleEvent&&n.handleEvent(e),this._&&!1!==e.redraw&&(0,this._)(),!1===t&&(e.preventDefault(),e.stopPropagation())},function(r,o,l){if(!r)throw new TypeError("DOM element being rendered to does not exist.");if(null!=R&&r.contains(R))throw new TypeError("Node is currently being rendered to and thus is locked.");var i=t,a=R,s=[],f=u(r),c=r.namespaceURI;R=r,t="function"==typeof l?l:void 0,n={};try{null==r.vnodes&&(r.textContent=""),o=e.normalizeChildren(Array.isArray(o)?o:[o]),v(r,r.vnodes,o,s,null,"http://www.w3.org/1999/xhtml"===c?void 0:c),r.vnodes=o,null!=f&&u(r)!==f&&"function"==typeof f.focus&&f.focus();for(var d=0;d=0&&(o.splice(l,2),l<=i&&(i-=2),t(n,[])),null!=r&&(o.push(n,r),t(n,e(r),u))},redraw:u}}(c,"undefined"!=typeof requestAnimationFrame?requestAnimationFrame:null,"undefined"!=typeof console?console:null),p=function(e){if("[object Object]"!==Object.prototype.toString.call(e))return"";var t=[];for(var n in e)r(n,e[n]);return t.join("&");function r(e,n){if(Array.isArray(n))for(var o=0;o=0&&(m+=e.slice(n,o)),s>=0&&(m+=(n<0?"?":"&")+u.slice(s,c));var v=p(a);return v&&(m+=(n<0&&s<0?"?":"&")+v),r>=0&&(m+=e.slice(r)),f>=0&&(m+=(r<0?"":"&")+u.slice(f)),m},v=function(e,n){function r(e){return new Promise(e)}function o(e,n){for(var r in e.headers)if(t.call(e.headers,r)&&r.toLowerCase()===n)return!0;return!1}return r.prototype=Promise.prototype,r.__proto__=Promise,{request:function(l,i){"string"!=typeof l?(i=l,l=l.url):null==i&&(i={});var a=function(n,r){return new Promise((function(l,i){n=m(n,r.params);var a,u=null!=r.method?r.method.toUpperCase():"GET",s=r.body,f=(null==r.serialize||r.serialize===JSON.serialize)&&!(s instanceof e.FormData||s instanceof e.URLSearchParams),c=r.responseType||("function"==typeof r.extract?"":"json"),d=new e.XMLHttpRequest,p=!1,v=!1,h=d,y=d.abort;for(var g in d.abort=function(){p=!0,y.call(this)},d.open(u,n,!1!==r.async,"string"==typeof r.user?r.user:void 0,"string"==typeof r.password?r.password:void 0),f&&null!=s&&!o(r,"content-type")&&d.setRequestHeader("Content-Type","application/json; charset=utf-8"),"function"==typeof r.deserialize||o(r,"accept")||d.setRequestHeader("Accept","application/json, text/*"),r.withCredentials&&(d.withCredentials=r.withCredentials),r.timeout&&(d.timeout=r.timeout),d.responseType=c,r.headers)t.call(r.headers,g)&&d.setRequestHeader(g,r.headers[g]);d.onreadystatechange=function(e){if(!p&&4===e.target.readyState)try{var t,o=e.target.status>=200&&e.target.status<300||304===e.target.status||/^file:\/\//i.test(n),a=e.target.response;if("json"===c){if(!e.target.responseType&&"function"!=typeof r.extract)try{a=JSON.parse(e.target.responseText)}catch(e){a=null}}else c&&"text"!==c||null==a&&(a=e.target.responseText);if("function"==typeof r.extract?(a=r.extract(e.target,r),o=!0):"function"==typeof r.deserialize&&(a=r.deserialize(a)),o){if("function"==typeof r.type)if(Array.isArray(a))for(var u=0;u-1&&u.pop();for(var f=0;f{"use strict";function e(e,t,n,r){return{tag:e,key:t,attrs:n,children:r,dom:void 0,domSize:void 0,state:void 0,events:void 0,instance:void 0}}e.normalize=function(t){return Array.isArray(t)?e("[",void 0,void 0,e.normalizeChildren(t)):null==t||"boolean"==typeof t?null:"object"==typeof t?t:e("#",void 0,void 0,String(t))},e.normalizeChildren=function(t){if(t.length){for(var n=null!=t[0]&&null!=t[0].key,r=new Set,o=1;o0&&(a.class=i.join(" "));var d={tag:l,attrs:u?a:null};return o.set(e,d),d}(l)),null!=c.attrs&&(u=c.attrs.class,i=Object.assign({},c.attrs,i)),null==f&&null==u||(i!==d&&(i=Object.assign({},i)),i.class=null!=f?null!=u?`${u} ${f}`:f:u,s&&(i.className=null)),e(c.tag,i.key,i,a)}function i(t,n,...r){if(null==t||"string"!=typeof t&&"function"!=typeof t&&"function"!=typeof t.view)throw new Error("The selector must be either a string or a component.");return null==n||"object"==typeof n&&null==n.tag&&!Array.isArray(n)?1===r.length&&Array.isArray(r[0])&&(r=r[0]):(r=0===r.length&&Array.isArray(n)?n:[n,...r],n=void 0),null==n&&(n={}),"string"==typeof t&&(r=e.normalizeChildren(r),"["!==t)?l(t,n,r):e(t,n.key,n,r)}i.fragment=function(...e){return i("[",...e)};var a=new WeakMap;var u={delayedRemoval:a,domFor:function*(e,t={}){var n=e.dom,r=e.domSize,o=t.generation;if(null!=n)do{var l=n.nextSibling;a.get(n)===o&&(yield n,r--),n=l}while(r)}},s=u.delayedRemoval,f=u.domFor,c=function(){var t,n,r="http://www.w3.org/1999/xlink",o={svg:"http://www.w3.org/2000/svg",math:"http://www.w3.org/1998/Math/MathML"};function l(e){return e.ownerDocument}function i(e){return e.attrs&&e.attrs.xmlns||o[e.tag]}function a(e,t){if(e.state!==t)throw new Error("'vnode.state' must not be modified.")}function u(e){var t=e.state;try{return this.apply(t,arguments)}finally{a(e,t)}}function c(e){try{return l(e).activeElement}catch(e){return null}}function d(e,t,n,r,o,l,i){for(var a=n;a=s&&j>=u&&(m=t[E],b=n[j],m.key===b.key);)m!==b&&v(e,m,b,r,o,l),null!=b.dom&&(o=b.dom),E--,j--;for(;E>=s&&j>=u&&(f=t[s],c=n[u],f.key===c.key);)s++,u++,f!==c&&v(e,f,c,r,g(t,s,o),l);for(;E>=s&&j>=u&&u!==j&&f.key===b.key&&m.key===c.key;)w(e,m,k=g(t,s,o)),m!==c&&v(e,m,c,r,k,l),++u<=--j&&w(e,f,o),f!==b&&v(e,f,b,r,o,l),null!=b.dom&&(o=b.dom),s++,m=t[--E],b=n[j],f=t[s],c=n[u];for(;E>=s&&j>=u&&m.key===b.key;)m!==b&&v(e,m,b,r,o,l),null!=b.dom&&(o=b.dom),j--,m=t[--E],b=n[j];if(u>j)x(e,t,s,E+1);else if(s>E)d(e,n,u,j+1,r,o,l);else{var z,A,O=o,$=j-u+1,C=new Array($),T=0,R=0,I=2147483647,L=0;for(R=0;R<$;R++)C[R]=-1;for(R=j;R>=u;R--){null==z&&(z=h(t,s,E+1));var N=z[(b=n[R]).key];null!=N&&(I=N>>1)+(r>>>1)+(n&r&1);e[t[a]]0&&(y[o]=t[n-1]),t[n]=o)}}n=t.length,r=t[n-1];for(;n-- >0;)t[n]=r,r=y[r];return y.length=0,t}(C)).length-1,R=j;R>=u;R--)c=n[R],-1===C[R-u]?p(e,c,r,l,o):A[T]===R-u?T--:w(e,c,o),null!=c.dom&&(o=n[R].dom);else for(R=j;R>=u;R--)c=n[R],-1===C[R-u]&&p(e,c,r,l,o),null!=c.dom&&(o=n[R].dom)}}else{var P=t.lengthP&&x(e,t,u,t.length),n.length>P&&d(e,n,u,n.length,r,o,l)}}}function v(t,n,r,o,l,a){var s=n.tag;if(s===r.tag){if(r.state=n.state,r.events=n.events,function(e,t){do{var n;if(null!=e.attrs&&"function"==typeof e.attrs.onbeforeupdate)if(void 0!==(n=u.call(e.attrs.onbeforeupdate,e,t))&&!n)break;if("string"!=typeof e.tag&&"function"==typeof e.state.onbeforeupdate)if(void 0!==(n=u.call(e.state.onbeforeupdate,e,t))&&!n)break;return!1}while(0);return e.dom=t.dom,e.domSize=t.domSize,e.instance=t.instance,e.attrs=t.attrs,e.children=t.children,!0}(r,n))return;if("string"==typeof s)switch(null!=r.attrs&&q(r.attrs,r,o),s){case"#":!function(e,t){e.children.toString()!==t.children.toString()&&(e.dom.nodeValue=t.children);t.dom=e.dom}(n,r);break;case"[":!function(e,t,n,r,o,l){m(e,t.children,n.children,r,o,l);var i=0,a=n.children;if(n.dom=null,null!=a){for(var u=0;u-1||null!=e.attrs&&e.attrs.is||!C.has(t))&&t in e.dom}var R,I=/[A-Z]/g;function L(e){return"-"+e.toLowerCase()}function N(e){return"-"===e[0]&&"-"===e[1]?e:"cssFloat"===e?"float":e.replace(I,L)}function P(e,t,n){if(t===n);else if(null==n)e.style="";else if("object"!=typeof n)e.style=n;else if(null==t||"object"!=typeof t)for(var r in e.style.cssText="",n){null!=(o=n[r])&&e.style.setProperty(N(r),String(o))}else{for(var r in n){var o;null!=(o=n[r])&&(o=String(o))!==String(t[r])&&e.style.setProperty(N(r),o)}for(var r in t)null!=t[r]&&null==n[r]&&e.style.removeProperty(N(r))}}function D(){this._=t}function _(e,n,r){if(null!=e.events){if(e.events._=t,e.events[n]===r)return;null==r||"function"!=typeof r&&"object"!=typeof r?(null!=e.events[n]&&e.dom.removeEventListener(n.slice(2),e.events,!1),e.events[n]=void 0):(null==e.events[n]&&e.dom.addEventListener(n.slice(2),e.events,!1),e.events[n]=r)}else null==r||"function"!=typeof r&&"object"!=typeof r||(e.events=new D,e.dom.addEventListener(n.slice(2),e.events,!1),e.events[n]=r)}function F(e,t,n){"function"==typeof e.oninit&&u.call(e.oninit,t),"function"==typeof e.oncreate&&n.push(u.bind(e.oncreate,t))}function q(e,t,n){"function"==typeof e.onupdate&&n.push(u.bind(e.onupdate,t))}return D.prototype=Object.create(null),D.prototype.handleEvent=function(e){var t,n=this["on"+e.type];"function"==typeof n?t=n.call(e.currentTarget,e):"function"==typeof n.handleEvent&&n.handleEvent(e),this._&&!1!==e.redraw&&(0,this._)(),!1===t&&(e.preventDefault(),e.stopPropagation())},function(r,o,l){if(!r)throw new TypeError("DOM element being rendered to does not exist.");if(null!=R&&r.contains(R))throw new TypeError("Node is currently being rendered to and thus is locked.");var i=t,a=R,u=[],s=c(r),f=r.namespaceURI;R=r,t="function"==typeof l?l:void 0,n={};try{null==r.vnodes&&(r.textContent=""),o=e.normalizeChildren(Array.isArray(o)?o:[o]),m(r,r.vnodes,o,u,null,"http://www.w3.org/1999/xhtml"===f?void 0:f),r.vnodes=o,null!=s&&c(r)!==s&&"function"==typeof s.focus&&s.focus();for(var d=0;d=0&&(o.splice(l,2),l<=i&&(i-=2),t(n,[])),null!=r&&(o.push(n,r),t(n,e(r),u))},redraw:u}}(c,"undefined"!=typeof requestAnimationFrame?requestAnimationFrame:null,"undefined"!=typeof console?console:null),p=function(e){if("[object Object]"!==Object.prototype.toString.call(e))return"";var t=[];for(var n in e)r(n,e[n]);return t.join("&");function r(e,n){if(Array.isArray(n))for(var o=0;o=0&&(m+=e.slice(n,o)),s>=0&&(m+=(n<0?"?":"&")+u.slice(s,c));var v=p(a);return v&&(m+=(n<0&&s<0?"?":"&")+v),r>=0&&(m+=e.slice(r)),f>=0&&(m+=(r<0?"":"&")+u.slice(f)),m},v=function(e,n){function r(e){return new Promise(e)}function o(e,n){for(var r in e.headers)if(t.call(e.headers,r)&&r.toLowerCase()===n)return!0;return!1}return r.prototype=Promise.prototype,r.__proto__=Promise,{request:function(l,i){"string"!=typeof l?(i=l,l=l.url):null==i&&(i={});var a=function(n,r){return new Promise((function(l,i){n=m(n,r.params);var a,u=null!=r.method?r.method.toUpperCase():"GET",s=r.body,f=(null==r.serialize||r.serialize===JSON.serialize)&&!(s instanceof e.FormData||s instanceof e.URLSearchParams),c=r.responseType||("function"==typeof r.extract?"":"json"),d=new e.XMLHttpRequest,p=!1,v=!1,h=d,y=d.abort;for(var g in d.abort=function(){p=!0,y.call(this)},d.open(u,n,!1!==r.async,"string"==typeof r.user?r.user:void 0,"string"==typeof r.password?r.password:void 0),f&&null!=s&&!o(r,"content-type")&&d.setRequestHeader("Content-Type","application/json; charset=utf-8"),"function"==typeof r.deserialize||o(r,"accept")||d.setRequestHeader("Accept","application/json, text/*"),r.withCredentials&&(d.withCredentials=r.withCredentials),r.timeout&&(d.timeout=r.timeout),d.responseType=c,r.headers)t.call(r.headers,g)&&d.setRequestHeader(g,r.headers[g]);d.onreadystatechange=function(e){if(!p&&4===e.target.readyState)try{var t,o=e.target.status>=200&&e.target.status<300||304===e.target.status||/^file:\/\//i.test(n),a=e.target.response;if("json"===c){if(!e.target.responseType&&"function"!=typeof r.extract)try{a=JSON.parse(e.target.responseText)}catch(e){a=null}}else c&&"text"!==c||null==a&&(a=e.target.responseText);if("function"==typeof r.extract?(a=r.extract(e.target,r),o=!0):"function"==typeof r.deserialize&&(a=r.deserialize(a)),o){if("function"==typeof r.type)if(Array.isArray(a))for(var u=0;u-1&&u.pop();for(var f=0;f "ij", no in sight. - var temp = getDocument(parent).createElement(possibleParents[match[1]] || "div") - if (ns === "http://www.w3.org/2000/svg") { - temp.innerHTML = "" + vnode.children + "" - temp = temp.firstChild - } else { - temp.innerHTML = vnode.children - } - vnode.dom = temp.firstChild - vnode.domSize = temp.childNodes.length - // Capture nodes to remove, so we don't confuse them. - var fragment = getDocument(parent).createDocumentFragment() - var child - while (child = temp.firstChild) { - fragment.appendChild(child) - } - insertDOM(parent, fragment, nextSibling) - } function createFragment(parent, vnode, hooks, ns, nextSibling) { var fragment = getDocument(parent).createDocumentFragment() if (vnode.children != null) { @@ -407,7 +381,6 @@ module.exports = function() { } switch (oldTag) { case "#": updateText(old, vnode); break - case "<": updateHTML(parent, old, vnode, ns, nextSibling); break case "[": updateFragment(parent, old, vnode, hooks, nextSibling, ns); break default: updateElement(old, vnode, hooks, ns) } @@ -425,16 +398,6 @@ module.exports = function() { } vnode.dom = old.dom } - function updateHTML(parent, old, vnode, ns, nextSibling) { - if (old.children !== vnode.children) { - removeDOM(parent, old, undefined) - createHTML(parent, vnode, ns, nextSibling) - } - else { - vnode.dom = old.dom - vnode.domSize = old.domSize - } - } function updateFragment(parent, old, vnode, hooks, nextSibling, ns) { updateNodes(parent, old.children, vnode.children, hooks, nextSibling, ns) var domSize = 0, children = vnode.children @@ -571,11 +534,7 @@ module.exports = function() { vnode.attrs.contentEditable == null // property )) return false var children = vnode.children - if (children != null && children.length === 1 && children[0].tag === "<") { - var content = children[0].children - if (vnode.dom.innerHTML !== content) vnode.dom.innerHTML = content - } - else if (children != null && children.length !== 0) throw new Error("Child node of a contenteditable must be trusted.") + if (children != null && children.length !== 0) throw new Error("Child node of a contenteditable must be trusted.") return true } diff --git a/render/tests/test-attributes.js b/render/tests/test-attributes.js index 4a4628781..d28335443 100644 --- a/render/tests/test-attributes.js +++ b/render/tests/test-attributes.js @@ -4,7 +4,6 @@ var o = require("ospec") var domMock = require("../../test-utils/domMock") var vdom = require("../../render/render") var m = require("../../render/hyperscript") -var trust = require("../../render/trust") o.spec("attributes", function() { var $window, root, render @@ -671,19 +670,6 @@ o.spec("attributes", function() { } catch(e){/* ignore */} - o(succeeded).equals(true) - }) - o("tolerating trusted content", function() { - var div = m("div", {contenteditable: true}, trust("")) - var succeeded = false - - try { - render(root, div) - - succeeded = true - } - catch(e){/* ignore */} - o(succeeded).equals(true) }) }) diff --git a/render/tests/test-createFragment.js b/render/tests/test-createFragment.js index 01f7de150..e48f55db6 100644 --- a/render/tests/test-createFragment.js +++ b/render/tests/test-createFragment.js @@ -4,7 +4,7 @@ var o = require("ospec") var domMock = require("../../test-utils/domMock") var vdom = require("../../render/render") var m = require("../../render/hyperscript") -var fragment = require("../../render/fragment") +var fragment = require("../../render/hyperscript").fragment o.spec("createFragment", function() { var $window, root, render diff --git a/render/tests/test-createHTML.js b/render/tests/test-createHTML.js deleted file mode 100644 index ddfd499d7..000000000 --- a/render/tests/test-createHTML.js +++ /dev/null @@ -1,96 +0,0 @@ -"use strict" - -var o = require("ospec") -var domMock = require("../../test-utils/domMock") -var vdom = require("../../render/render") -var m = require("../../render/hyperscript") -var trust = require("../../render/trust") - -o.spec("createHTML", function() { - var $window, root, render - o.beforeEach(function() { - $window = domMock() - root = $window.document.createElement("div") - render = vdom($window) - }) - - o("creates HTML", function() { - var vnode = trust("") - render(root, vnode) - - o(vnode.dom.nodeName).equals("A") - }) - o("creates text HTML", function() { - var vnode = trust("a") - render(root, vnode) - - o(vnode.dom.nodeValue).equals("a") - }) - o("handles empty HTML", function() { - var vnode = trust("") - render(root, vnode) - - o(vnode.dom).equals(null) - o(vnode.domSize).equals(0) - }) - o("handles multiple children in HTML", function() { - var vnode = trust("") - render(root, vnode) - - o(vnode.domSize).equals(2) - o(vnode.dom.nodeName).equals("A") - o(vnode.dom.nextSibling.nodeName).equals("B") - }) - o("handles valid html tags", function() { - //FIXME body,head,html,frame,frameset are not supported - //FIXME keygen is broken in Firefox - var tags = ["a", "abbr", "acronym", "address", "applet", "area", "article", "aside", "audio", "b", "base", "basefont", "bdi", "bdo", "big", "blockquote", /*"body",*/ "br", "button", "canvas", "caption", "center", "cite", "code", "col", "colgroup", "datalist", "dd", "del", "details", "dfn", "dialog", "dir", "div", "dl", "dt", "em", "embed", "fieldset", "figcaption", "figure", "font", "footer", "form", /*"frame", "frameset",*/ "h1", "h2", "h3", "h4", "h5", "h6", /*"head",*/ "header", "hr", /*"html",*/ "i", "iframe", "img", "input", "ins", "kbd", /*"keygen", */"label", "legend", "li", "link", "main", "map", "mark", "menu", "menuitem", "meta", "meter", "nav", "noframes", "noscript", "object", "ol", "optgroup", "option", "output", "p", "param", "pre", "progress", "q", "rp", "rt", "ruby", "s", "samp", "script", "section", "select", "small", "source", "span", "strike", "strong", "style", "sub", "summary", "sup", "table", "tbody", "td", "textarea", "tfoot", "th", "thead", "time", "title", "tr", "track", "tt", "u", "ul", "var", "video", "wbr"] - - tags.forEach(function(tag) { - var vnode = trust("<" + tag + " />") - render(root, vnode) - - o(vnode.dom.nodeName).equals(tag.toUpperCase()) - }) - }) - o("creates SVG", function() { - var vnode = trust("") - render(root, m("svg", vnode)) - - o(vnode.dom.nodeName).equals("g") - o(vnode.dom.namespaceURI).equals("http://www.w3.org/2000/svg") - }) - o("creates text SVG", function() { - var vnode = trust("a") - render(root, m("svg", vnode)) - - o(vnode.dom.nodeValue).equals("a") - }) - o("handles empty SVG", function() { - var vnode = trust("") - render(root, m("svg", vnode)) - - o(vnode.dom).equals(null) - o(vnode.domSize).equals(0) - }) - o("handles multiple children in SVG", function() { - var vnode = trust("") - render(root, m("svg", vnode)) - - o(vnode.domSize).equals(2) - o(vnode.dom.nodeName).equals("g") - o(vnode.dom.namespaceURI).equals("http://www.w3.org/2000/svg") - o(vnode.dom.nextSibling.nodeName).equals("text") - o(vnode.dom.nextSibling.namespaceURI).equals("http://www.w3.org/2000/svg") - }) - o("creates the dom correctly with a contenteditable parent", function() { - var div = m("div", {contenteditable: true}, trust("")) - - render(root, div) - var tags = [] - for (var i = 0; i < div.dom.childNodes.length; i++) { - tags.push(div.dom.childNodes[i].nodeName) - } - o(tags).deepEquals(["A"]) - }) -}) diff --git a/render/tests/test-createNodes.js b/render/tests/test-createNodes.js index f1bfc4ab2..416bf5c23 100644 --- a/render/tests/test-createNodes.js +++ b/render/tests/test-createNodes.js @@ -4,8 +4,7 @@ var o = require("ospec") var domMock = require("../../test-utils/domMock") var vdom = require("../../render/render") var m = require("../../render/hyperscript") -var fragment = require("../../render/fragment") -var trust = require("../../render/trust") +var fragment = require("../../render/hyperscript").fragment o.spec("createNodes", function() { var $window, root, render @@ -19,47 +18,41 @@ o.spec("createNodes", function() { var vnodes = [ m("a"), "b", - trust("c"), - fragment("d"), + fragment("c"), ] render(root, vnodes) - o(root.childNodes.length).equals(4) + o(root.childNodes.length).equals(3) o(root.childNodes[0].nodeName).equals("A") o(root.childNodes[1].nodeValue).equals("b") o(root.childNodes[2].nodeValue).equals("c") - o(root.childNodes[3].nodeValue).equals("d") }) o("ignores null", function() { var vnodes = [ m("a"), "b", null, - trust("c"), - fragment("d"), + fragment("c"), ] render(root, vnodes) - o(root.childNodes.length).equals(4) + o(root.childNodes.length).equals(3) o(root.childNodes[0].nodeName).equals("A") o(root.childNodes[1].nodeValue).equals("b") o(root.childNodes[2].nodeValue).equals("c") - o(root.childNodes[3].nodeValue).equals("d") }) o("ignores undefined", function() { var vnodes = [ m("a"), "b", undefined, - trust("c"), - fragment("d"), + fragment("c"), ] render(root, vnodes) - o(root.childNodes.length).equals(4) + o(root.childNodes.length).equals(3) o(root.childNodes[0].nodeName).equals("A") o(root.childNodes[1].nodeValue).equals("b") o(root.childNodes[2].nodeValue).equals("c") - o(root.childNodes[3].nodeValue).equals("d") }) }) diff --git a/render/tests/test-domFor.js b/render/tests/test-domFor.js index b0c3444fa..7321a93e9 100644 --- a/render/tests/test-domFor.js +++ b/render/tests/test-domFor.js @@ -5,7 +5,7 @@ const components = require("../../test-utils/components") const domMock = require("../../test-utils/domMock") const vdom = require("../render") const m = require("../hyperscript") -const fragment = require("../fragment") +const fragment = require("../../render/hyperscript").fragment const domFor = require("../../render/domFor").domFor o.spec("domFor(vnode)", function() { @@ -175,4 +175,4 @@ o.spec("domFor(vnode)", function() { }) }) }) -}) \ No newline at end of file +}) diff --git a/render/tests/test-fragment.js b/render/tests/test-fragment.js index cea868c0d..8d065f8a7 100644 --- a/render/tests/test-fragment.js +++ b/render/tests/test-fragment.js @@ -1,7 +1,6 @@ "use strict" var o = require("ospec") -var fragment = require("../../render/fragment") var m = require("../../render/hyperscript") function fragmentStr() { @@ -193,5 +192,5 @@ function runTest(name, fragment) { }) } -runTest("fragment", fragment); +runTest("fragment", m.fragment); runTest("fragment-string-selector", fragmentStr); diff --git a/render/tests/test-onbeforeremove.js b/render/tests/test-onbeforeremove.js index b5621e421..33ab69398 100644 --- a/render/tests/test-onbeforeremove.js +++ b/render/tests/test-onbeforeremove.js @@ -6,7 +6,6 @@ var components = require("../../test-utils/components") var domMock = require("../../test-utils/domMock") var vdom = require("../../render/render") var m = require("../../render/hyperscript") -var fragment = require("../../render/fragment") o.spec("onbeforeremove", function() { var $window, root, render @@ -57,7 +56,7 @@ o.spec("onbeforeremove", function() { } }) o("calls onbeforeremove when removing fragment", function(done) { - var vnode = fragment({onbeforeremove: remove}, m("div")) + var vnode = m.fragment({onbeforeremove: remove}, m("div")) render(root, vnode) render(root, []) @@ -76,7 +75,7 @@ o.spec("onbeforeremove", function() { }) o("calls onremove after onbeforeremove resolves", function(done) { var spy = o.spy() - var vnode = fragment({onbeforeremove: onbeforeremove, onremove: spy}, "a") + var vnode = m.fragment({onbeforeremove: onbeforeremove, onremove: spy}, "a") render(root, vnode) render(root, []) diff --git a/render/tests/test-onbeforeupdate.js b/render/tests/test-onbeforeupdate.js index 7bdcfe9ed..62f70ad2b 100644 --- a/render/tests/test-onbeforeupdate.js +++ b/render/tests/test-onbeforeupdate.js @@ -5,7 +5,6 @@ var components = require("../../test-utils/components") var domMock = require("../../test-utils/domMock") var vdom = require("../../render/render") var m = require("../../render/hyperscript") -var fragment = require("../../render/fragment") o.spec("onbeforeupdate", function() { var $window, root, render @@ -28,8 +27,8 @@ o.spec("onbeforeupdate", function() { o("prevents update in fragment", function() { var onbeforeupdate = function() {return false} - var vnode = fragment({onbeforeupdate: onbeforeupdate}, "a") - var updated = fragment({onbeforeupdate: onbeforeupdate}, "b") + var vnode = m.fragment({onbeforeupdate: onbeforeupdate}, "a") + var updated = m.fragment({onbeforeupdate: onbeforeupdate}, "b") render(root, vnode) render(root, updated) diff --git a/render/tests/test-oncreate.js b/render/tests/test-oncreate.js index 21f25bea8..93ba8c389 100644 --- a/render/tests/test-oncreate.js +++ b/render/tests/test-oncreate.js @@ -4,7 +4,6 @@ var o = require("ospec") var domMock = require("../../test-utils/domMock") var vdom = require("../../render/render") var m = require("../../render/hyperscript") -var fragment = require("../../render/fragment") o.spec("oncreate", function() { var $window, root, render @@ -26,7 +25,7 @@ o.spec("oncreate", function() { }) o("calls oncreate when creating fragment", function() { var callback = o.spy() - var vnode = fragment({oncreate: callback}) + var vnode = m.fragment({oncreate: callback}) render(root, vnode) diff --git a/render/tests/test-oninit.js b/render/tests/test-oninit.js index b287fa1ea..4965b7b9f 100644 --- a/render/tests/test-oninit.js +++ b/render/tests/test-oninit.js @@ -4,7 +4,6 @@ var o = require("ospec") var domMock = require("../../test-utils/domMock") var vdom = require("../../render/render") var m = require("../../render/hyperscript") -var fragment = require("../../render/fragment") o.spec("oninit", function() { var $window, root, render @@ -26,7 +25,7 @@ o.spec("oninit", function() { }) o("calls oninit when creating fragment", function() { var callback = o.spy() - var vnode = fragment({oninit: callback}) + var vnode = m.fragment({oninit: callback}) render(root, vnode) diff --git a/render/tests/test-onremove.js b/render/tests/test-onremove.js index bf4800ffd..37263c5e5 100644 --- a/render/tests/test-onremove.js +++ b/render/tests/test-onremove.js @@ -5,7 +5,6 @@ var components = require("../../test-utils/components") var domMock = require("../../test-utils/domMock") var vdom = require("../../render/render") var m = require("../../render/hyperscript") -var fragment = require("../../render/fragment") o.spec("onremove", function() { var $window, root, render @@ -51,7 +50,7 @@ o.spec("onremove", function() { }) o("calls onremove when removing fragment", function() { var remove = o.spy() - var vnode = fragment({onremove: remove}) + var vnode = m.fragment({onremove: remove}) render(root, vnode) render(root, []) @@ -248,7 +247,7 @@ ${actual}` render(root, m("div", - showParent && fragment( + showParent && m.fragment( {onremove: removeParent}, m("a", {onremove: removeSyncChild}, "sync child"), showChild && m(C, { diff --git a/render/tests/test-onupdate.js b/render/tests/test-onupdate.js index 436b2d42f..ef7c323e9 100644 --- a/render/tests/test-onupdate.js +++ b/render/tests/test-onupdate.js @@ -4,7 +4,6 @@ var o = require("ospec") var domMock = require("../../test-utils/domMock") var vdom = require("../../render/render") var m = require("../../render/hyperscript") -var fragment = require("../../render/fragment") o.spec("onupdate", function() { var $window, root, render @@ -114,8 +113,8 @@ o.spec("onupdate", function() { o("calls onupdate when updating fragment", function() { var create = o.spy() var update = o.spy() - var vnode = fragment({onupdate: create}) - var updated = fragment({onupdate: update}) + var vnode = m.fragment({onupdate: create}) + var updated = m.fragment({onupdate: update}) render(root, vnode) render(root, updated) diff --git a/render/tests/test-trust.js b/render/tests/test-trust.js deleted file mode 100644 index 304708cc6..000000000 --- a/render/tests/test-trust.js +++ /dev/null @@ -1,31 +0,0 @@ -"use strict" - -var o = require("ospec") -var trust = require("../../render/trust") - -o.spec("trust", function() { - o("works with html", function() { - var vnode = trust("") - - o(vnode.tag).equals("<") - o(vnode.children).equals("") - }) - o("works with text", function() { - var vnode = trust("abc") - - o(vnode.tag).equals("<") - o(vnode.children).equals("abc") - }) - o("casts null to empty string", function() { - var vnode = trust(null) - - o(vnode.tag).equals("<") - o(vnode.children).equals("") - }) - o("casts undefined to empty string", function() { - var vnode = trust(undefined) - - o(vnode.tag).equals("<") - o(vnode.children).equals("") - }) -}) diff --git a/render/tests/test-updateFragment.js b/render/tests/test-updateFragment.js index e1274fd14..5ee5e594c 100644 --- a/render/tests/test-updateFragment.js +++ b/render/tests/test-updateFragment.js @@ -4,7 +4,6 @@ var o = require("ospec") var domMock = require("../../test-utils/domMock") var vdom = require("../../render/render") var m = require("../../render/hyperscript") -var fragment = require("../../render/fragment") o.spec("updateFragment", function() { var $window, root, render @@ -15,8 +14,8 @@ o.spec("updateFragment", function() { }) o("updates fragment", function() { - var vnode = fragment(m("a")) - var updated = fragment(m("b")) + var vnode = m.fragment(m("a")) + var updated = m.fragment(m("b")) render(root, vnode) render(root, updated) @@ -25,8 +24,8 @@ o.spec("updateFragment", function() { o(updated.dom.nodeName).equals("B") }) o("adds els", function() { - var vnode = fragment() - var updated = fragment(m("a"), m("b")) + var vnode = m.fragment() + var updated = m.fragment(m("a"), m("b")) render(root, vnode) render(root, updated) @@ -38,8 +37,8 @@ o.spec("updateFragment", function() { o(root.childNodes[1].nodeName).equals("B") }) o("removes els", function() { - var vnode = fragment(m("a"), m("b")) - var updated = fragment() + var vnode = m.fragment(m("a"), m("b")) + var updated = m.fragment() render(root, vnode) render(root, updated) @@ -49,8 +48,8 @@ o.spec("updateFragment", function() { o(root.childNodes.length).equals(0) }) o("updates from childless fragment", function() { - var vnode = fragment() - var updated = fragment(m("a")) + var vnode = m.fragment() + var updated = m.fragment(m("a")) render(root, vnode) render(root, updated) @@ -59,8 +58,8 @@ o.spec("updateFragment", function() { o(updated.dom.nodeName).equals("A") }) o("updates to childless fragment", function() { - var vnode = fragment(m("a")) - var updated = fragment() + var vnode = m.fragment(m("a")) + var updated = m.fragment() render(root, vnode) render(root, updated) diff --git a/render/tests/test-updateHTML.js b/render/tests/test-updateHTML.js deleted file mode 100644 index c0f9a6c39..000000000 --- a/render/tests/test-updateHTML.js +++ /dev/null @@ -1,116 +0,0 @@ -"use strict" - -var o = require("ospec") -var domMock = require("../../test-utils/domMock") -var vdom = require("../../render/render") -var m = require("../../render/hyperscript") -var trust = require("../../render/trust") - -o.spec("updateHTML", function() { - var $window, root, render - o.beforeEach(function() { - $window = domMock() - root = $window.document.createElement("div") - render = vdom($window) - }) - - o("updates html", function() { - var vnode = trust("a") - var updated = trust("b") - - render(root, vnode) - render(root, updated) - - o(updated.dom).equals(root.firstChild) - o(updated.domSize).equals(1) - o(updated.dom.nodeValue).equals("b") - }) - o("adds html", function() { - var vnode = trust("") - var updated = trust("") - - render(root, vnode) - render(root, updated) - - o(updated.domSize).equals(2) - o(updated.dom).equals(root.firstChild) - o(root.childNodes.length).equals(2) - o(root.childNodes[0].nodeName).equals("A") - o(root.childNodes[1].nodeName).equals("B") - }) - o("removes html", function() { - var vnode = trust("") - var updated = trust("") - - render(root, vnode) - render(root, updated) - - o(updated.dom).equals(null) - o(updated.domSize).equals(0) - o(root.childNodes.length).equals(0) - }) - function childKeysOf(elem, key) { - var keys = key.split(".") - var result = [] - for (var i = 0; i < elem.childNodes.length; i++) { - var child = elem.childNodes[i] - for (var j = 0; j < keys.length; j++) child = child[keys[j]] - result.push(child) - } - return result - } - o("updates the dom correctly with a contenteditable parent", function() { - var div = m("div", {contenteditable: true}, trust("")) - - render(root, div) - o(childKeysOf(div.dom, "nodeName")).deepEquals(["A"]) - }) - o("updates dom with multiple text children", function() { - var vnode = ["a", trust(""), trust("")] - var replacement = ["a", trust(""), trust("")] - - render(root, vnode) - render(root, replacement) - - o(childKeysOf(root, "nodeName")).deepEquals(["#text", "C", "D"]) - }) - o("updates dom with multiple text children in other parents", function() { - var vnode = [ - m("div", "a", trust("")), - m("div", "b", trust("")), - ] - var replacement = [ - m("div", "c", trust("")), - m("div", "d", trust("")), - ] - - render(root, vnode) - render(root, replacement) - - o(childKeysOf(root, "nodeName")).deepEquals(["DIV", "DIV"]) - o(childKeysOf(root.childNodes[0], "nodeName")).deepEquals(["#text", "C"]) - o(root.childNodes[0].firstChild.nodeValue).equals("c") - o(childKeysOf(root.childNodes[1], "nodeName")).deepEquals(["#text", "D"]) - o(root.childNodes[1].firstChild.nodeValue).equals("d") - }) - o("correctly diffs if followed by another trusted vnode", function() { - render(root, [ - trust("A"), - trust("A"), - ]) - o(childKeysOf(root, "nodeName")).deepEquals(["SPAN", "SPAN"]) - o(childKeysOf(root, "firstChild.nodeValue")).deepEquals(["A", "A"]) - render(root, [ - trust("B"), - trust("A"), - ]) - o(childKeysOf(root, "nodeName")).deepEquals(["SPAN", "SPAN"]) - o(childKeysOf(root, "firstChild.nodeValue")).deepEquals(["B", "A"]) - render(root, [ - trust("B"), - trust("B"), - ]) - o(childKeysOf(root, "nodeName")).deepEquals(["SPAN", "SPAN"]) - o(childKeysOf(root, "firstChild.nodeValue")).deepEquals(["B", "B"]) - }) -}) diff --git a/render/tests/test-updateNodes.js b/render/tests/test-updateNodes.js index 3f1258148..fdf45c065 100644 --- a/render/tests/test-updateNodes.js +++ b/render/tests/test-updateNodes.js @@ -5,8 +5,6 @@ var components = require("../../test-utils/components") var domMock = require("../../test-utils/domMock") var vdom = require("../../render/render") var m = require("../../render/hyperscript") -var fragment = require("../../render/fragment") -var trust = require("../../render/trust") function vnodify(str) { return str.split(",").map(function(k) {return m(k, {key: k})}) @@ -76,20 +74,9 @@ o.spec("updateNodes", function() { o(root.childNodes.length).equals(1) o(root.childNodes[0].nodeValue).equals("0") }) - o("handles html noop", function() { - var vnodes = trust("a") - var updated = trust("a") - - render(root, vnodes) - render(root, updated) - - o(root.childNodes.length).equals(1) - o(root.childNodes[0].nodeValue).equals("a") - o(updated.dom).equals(root.childNodes[0]) - }) o("handles fragment noop", function() { - var vnodes = fragment(m("a")) - var updated = fragment(m("a")) + var vnodes = m.fragment(m("a")) + var updated = m.fragment(m("a")) render(root, vnodes) render(root, updated) @@ -99,8 +86,8 @@ o.spec("updateNodes", function() { o(updated.dom).equals(root.childNodes[0]) }) o("handles fragment noop w/ text child", function() { - var vnodes = fragment("a") - var updated = fragment("a") + var vnodes = m.fragment("a") + var updated = m.fragment("a") render(root, vnodes) render(root, updated) @@ -284,8 +271,8 @@ o.spec("updateNodes", function() { o(updated[2].dom).equals(root.childNodes[2]) }) o("adds to empty fragment followed by el", function() { - var vnodes = [fragment({key: 1}), m("b", {key: 2})] - var updated = [fragment({key: 1}, m("a")), m("b", {key: 2})] + var vnodes = [m.fragment({key: 1}), m("b", {key: 2})] + var updated = [m.fragment({key: 1}, m("a")), m("b", {key: 2})] render(root, vnodes) render(root, updated) @@ -297,8 +284,8 @@ o.spec("updateNodes", function() { o(updated[1].dom).equals(root.childNodes[1]) }) o("reverses followed by el", function() { - var vnodes = [fragment({key: 1}, m("a", {key: 2}), m("b", {key: 3})), m("i", {key: 4})] - var updated = [fragment({key: 1}, m("b", {key: 3}), m("a", {key: 2})), m("i", {key: 4})] + var vnodes = [m.fragment({key: 1}, m("a", {key: 2}), m("b", {key: 3})), m("i", {key: 4})] + var updated = [m.fragment({key: 1}, m("b", {key: 3}), m("a", {key: 2})), m("i", {key: 4})] render(root, vnodes) render(root, updated) @@ -311,65 +298,9 @@ o.spec("updateNodes", function() { o(updated[1].dom.nodeName).equals("I") o(updated[1].dom).equals(root.childNodes[2]) }) - o("updates empty fragment to html without key", function() { - var vnodes = fragment() - var updated = trust("") - - render(root, vnodes) - render(root, updated) - - o(root.childNodes.length).equals(2) - o(updated.dom.nodeName).equals("A") - o(updated.dom).equals(root.childNodes[0]) - o(updated.domSize).equals(2) - o(updated.dom.nextSibling.nodeName).equals("B") - o(updated.dom.nextSibling).equals(root.childNodes[1]) - }) - o("updates empty html to fragment without key", function() { - var vnodes = trust() - var updated = fragment(m("a"), m("b")) - - render(root, vnodes) - render(root, updated) - - o(root.childNodes.length).equals(2) - o(updated.dom.nodeName).equals("A") - o(updated.dom).equals(root.childNodes[0]) - o(updated.domSize).equals(2) - o(updated.dom.nextSibling.nodeName).equals("B") - o(updated.dom.nextSibling).equals(root.childNodes[1]) - }) - o("updates fragment to html without key", function() { - var vnodes = fragment(m("a"), m("b")) - var updated = trust("") - - render(root, vnodes) - render(root, updated) - - o(root.childNodes.length).equals(2) - o(updated.dom.nodeName).equals("I") - o(updated.dom).equals(root.childNodes[0]) - o(updated.domSize).equals(2) - o(updated.dom.nextSibling.nodeName).equals("S") - o(updated.dom.nextSibling).equals(root.childNodes[1]) - }) - o("updates html to fragment without key", function() { - var vnodes = trust("") - var updated = fragment(m("i"), m("s")) - - render(root, vnodes) - render(root, updated) - - o(root.childNodes.length).equals(2) - o(updated.dom.nodeName).equals("I") - o(updated.dom).equals(root.childNodes[0]) - o(updated.domSize).equals(2) - o(updated.dom.nextSibling.nodeName).equals("S") - o(updated.dom.nextSibling).equals(root.childNodes[1]) - }) o("populates fragment followed by el keyed", function() { - var vnodes = [fragment({key: 1}), m("i", {key: 2})] - var updated = [fragment({key: 1}, m("a"), m("b")), m("i", {key: 2})] + var vnodes = [m.fragment({key: 1}), m("i", {key: 2})] + var updated = [m.fragment({key: 1}, m("a"), m("b")), m("i", {key: 2})] render(root, vnodes) render(root, updated) @@ -384,20 +315,20 @@ o.spec("updateNodes", function() { o(updated[1].dom).equals(root.childNodes[2]) }) o("throws if fragment followed by null then el on first render keyed", function() { - var vnodes = [fragment({key: 1}), null, m("i", {key: 2})] + var vnodes = [m.fragment({key: 1}), null, m("i", {key: 2})] o(function () { render(root, vnodes) }).throws(TypeError) }) o("throws if fragment followed by null then el on next render keyed", function() { - var vnodes = [fragment({key: 1}), m("i", {key: 2})] - var updated = [fragment({key: 1}, m("a"), m("b")), null, m("i", {key: 2})] + var vnodes = [m.fragment({key: 1}), m("i", {key: 2})] + var updated = [m.fragment({key: 1}, m("a"), m("b")), null, m("i", {key: 2})] render(root, vnodes) o(function () { render(root, updated) }).throws(TypeError) }) o("populates childless fragment replaced followed by el keyed", function() { - var vnodes = [fragment({key: 1}), m("i", {key: 2})] - var updated = [fragment({key: 1}, m("a"), m("b")), m("i", {key: 2})] + var vnodes = [m.fragment({key: 1}), m("i", {key: 2})] + var updated = [m.fragment({key: 1}, m("a"), m("b")), m("i", {key: 2})] render(root, vnodes) render(root, updated) @@ -412,8 +343,8 @@ o.spec("updateNodes", function() { o(updated[1].dom).equals(root.childNodes[2]) }) o("throws if childless fragment replaced followed by null then el keyed", function() { - var vnodes = [fragment({key: 1}), m("i", {key: 2})] - var updated = [fragment({key: 1}, m("a"), m("b")), null, m("i", {key: 2})] + var vnodes = [m.fragment({key: 1}), m("i", {key: 2})] + var updated = [m.fragment({key: 1}, m("a"), m("b")), null, m("i", {key: 2})] render(root, vnodes) o(function () { render(root, updated) }).throws(TypeError) @@ -697,7 +628,7 @@ o.spec("updateNodes", function() { m("#", "a") ) var updated = m("div", - fragment(m("#", "b")), + m.fragment(m("#", "b")), undefined, undefined ) @@ -796,9 +727,9 @@ o.spec("updateNodes", function() { o(root.childNodes[1].nodeName).equals("B") }) o("mixed unkeyed vnode types are not broken by recycle", function() { - var vnodes = [fragment(m("a")), m("b")] + var vnodes = [m.fragment(m("a")), m("b")] var temp = [m("b")] - var updated = [fragment(m("a")), m("b")] + var updated = [m.fragment(m("a")), m("b")] render(root, vnodes) render(root, temp) @@ -946,38 +877,38 @@ o.spec("updateNodes", function() { }) o("don't add back elements from fragments that are restored from the pool #1991", function() { render(root, [ - fragment(), - fragment() + m.fragment(), + m.fragment() ]) render(root, [ - fragment(), - fragment( + m.fragment(), + m.fragment( m("div") ) ]) render(root, [ - fragment(null) + m.fragment(null) ]) render(root, [ - fragment(), - fragment() + m.fragment(), + m.fragment() ]) o(root.childNodes.length).equals(0) }) o("don't add back elements from fragments that are being removed #1991", function() { render(root, [ - fragment(), + m.fragment(), m("p"), ]) render(root, [ - fragment( + m.fragment( m("div", 5) ) ]) render(root, [ - fragment(), - fragment() + m.fragment(), + m.fragment() ]) o(root.childNodes.length).equals(0) @@ -1021,9 +952,9 @@ o.spec("updateNodes", function() { } }) o("don't fetch the nextSibling from the pool", function() { - render(root, [fragment(m("div", {key: 1}), m("div", {key: 2})), m("p")]) - render(root, [fragment(), m("p")]) - render(root, [fragment(m("div", {key: 2}), m("div", {key: 1})), m("p")]) + render(root, [m.fragment(m("div", {key: 1}), m("div", {key: 2})), m("p")]) + render(root, [m.fragment(), m("p")]) + render(root, [m.fragment(m("div", {key: 2}), m("div", {key: 1})), m("p")]) o([].map.call(root.childNodes, function(el) {return el.nodeName})).deepEquals(["DIV", "DIV", "P"]) }) @@ -1172,9 +1103,9 @@ o.spec("updateNodes", function() { o("fragment child toggles from null when followed by null component then tag", function() { var component = createComponent({view: function() {return null}}) - var vnodes = [fragment(m("a"), m(component), m("b"))] - var temp = [fragment(null, m(component), m("b"))] - var updated = [fragment(m("a"), m(component), m("b"))] + var vnodes = [m.fragment(m("a"), m(component), m("b"))] + var temp = [m.fragment(null, m(component), m("b"))] + var updated = [m.fragment(m("a"), m(component), m("b"))] render(root, vnodes) render(root, temp) @@ -1188,9 +1119,9 @@ o.spec("updateNodes", function() { var flag = true var a = createComponent({view: function() {return flag ? m("a") : null}}) var b = createComponent({view: function() {return null}}) - var vnodes = [fragment(m(a), m(b), m("s"))] - var temp = [fragment(m(a), m(b), m("s"))] - var updated = [fragment(m(a), m(b), m("s"))] + var vnodes = [m.fragment(m(a), m(b), m("s"))] + var temp = [m.fragment(m(a), m(b), m("s"))] + var updated = [m.fragment(m(a), m(b), m("s"))] render(root, vnodes) flag = false @@ -1204,7 +1135,7 @@ o.spec("updateNodes", function() { }) o("removing a component that returns a fragment doesn't throw (regression test for incidental bug introduced while debugging some Flems)", function() { var component = createComponent({ - view: function() {return fragment(m("a"), m("b"))} + view: function() {return m.fragment(m("a"), m("b"))} }) try { render(root, [m(component)]) diff --git a/render/trust.js b/render/trust.js deleted file mode 100644 index 6cc3fcdc3..000000000 --- a/render/trust.js +++ /dev/null @@ -1,8 +0,0 @@ -"use strict" - -var Vnode = require("../render/vnode") - -module.exports = function(html) { - if (html == null) html = "" - return Vnode("<", undefined, undefined, html) -} diff --git a/tests/test-api.js b/tests/test-api.js index dc61228ee..d619b9c20 100644 --- a/tests/test-api.js +++ b/tests/test-api.js @@ -25,14 +25,6 @@ o.spec("api", function() { o(vnode.tag).equals("div") }) }) - o.spec("m.trust", function() { - o("works", function() { - var vnode = m.trust("
") - - o(vnode.tag).equals("<") - o(vnode.children).equals("
") - }) - }) o.spec("m.fragment", function() { o("works", function() { var vnode = m.fragment({key: 123}, [m("div")]) From ca65f7a4dc59c6f5e77e7aa9c45a19f44c2c2b2c Mon Sep 17 00:00:00 2001 From: Claudia Meadows Date: Fri, 27 Sep 2024 11:45:43 -0700 Subject: [PATCH 07/95] Drop object component support --- api/mount-redraw.js | 2 +- api/router.js | 12 +- api/tests/test-mountRedraw.js | 74 ++--- api/tests/test-router.js | 277 +++++++++--------- api/tests/test-routerGetSet.js | 58 ++-- performance/test-perf.js | 72 ++--- render/hyperscript.js | 2 +- render/render.js | 20 +- render/tests/manual/iframe.html | 4 +- render/tests/test-component.js | 28 +- render/tests/test-hyperscript.js | 11 - .../tests/test-normalizeComponentChildren.js | 4 +- render/tests/test-onbeforeupdate.js | 8 +- render/tests/test-render.js | 25 +- test-utils/components.js | 24 +- test-utils/tests/test-components.js | 7 +- 16 files changed, 278 insertions(+), 350 deletions(-) diff --git a/api/mount-redraw.js b/api/mount-redraw.js index 4f7d0f606..67ec4076b 100644 --- a/api/mount-redraw.js +++ b/api/mount-redraw.js @@ -28,7 +28,7 @@ module.exports = function(render, schedule, console) { redraw.sync = sync function mount(root, component) { - if (component != null && component.view == null && typeof component !== "function") { + if (component != null && typeof component !== "function") { throw new TypeError("m.mount expects a component, not a vnode.") } diff --git a/api/router.js b/api/router.js index fdf4ed8a3..724792a51 100644 --- a/api/router.js +++ b/api/router.js @@ -37,7 +37,7 @@ module.exports = function($window, mountRedraw) { var currentResolver = sentinel, component, attrs, currentPath, lastUpdate - var RouterRoot = { + var RouterRoot = () => ({ onbeforeupdate: function() { state = state ? 2 : 1 return !(!state || sentinel === currentResolver) @@ -53,7 +53,7 @@ module.exports = function($window, mountRedraw) { if (currentResolver) vnode = currentResolver.render(vnode[0]) return vnode }, - } + }) var SKIP = route.SKIP = {} @@ -97,7 +97,7 @@ module.exports = function($window, mountRedraw) { var update = lastUpdate = function(comp) { if (update !== lastUpdate) return if (comp === SKIP) return loop(i + 1) - component = comp != null && (typeof comp.view === "function" || typeof comp === "function")? comp : "div" + component = typeof comp === "function" ? comp : "div" attrs = data.params, currentPath = path, lastUpdate = null currentResolver = payload.render ? payload : null if (state === 2) mountRedraw.redraw() @@ -108,7 +108,7 @@ module.exports = function($window, mountRedraw) { } // There's no understating how much I *wish* I could // use `async`/`await` here... - if (payload.view || typeof payload === "function") { + if (typeof payload === "function") { payload = {} update(localComp) } @@ -200,7 +200,7 @@ module.exports = function($window, mountRedraw) { } route.get = function() {return currentPath} route.prefix = "#!" - route.Link = { + route.Link = () => ({ view: function(vnode) { // Omit the used parameters from the rendered element - they are // internal. Also, censor the various lifecycle methods. @@ -268,7 +268,7 @@ module.exports = function($window, mountRedraw) { } return child }, - } + }) route.param = function(key) { return attrs && key != null ? attrs[key] : attrs } diff --git a/api/tests/test-mountRedraw.js b/api/tests/test-mountRedraw.js index e069e9b2c..4ebc0655e 100644 --- a/api/tests/test-mountRedraw.js +++ b/api/tests/test-mountRedraw.js @@ -36,7 +36,7 @@ o.spec("mount/redraw", function() { o("schedules correctly", function() { var spy = o.spy() - m.mount(root, {view: spy}) + m.mount(root, () => ({view: spy})) o(spy.callCount).equals(1) m.redraw() o(spy.callCount).equals(1) @@ -47,7 +47,7 @@ o.spec("mount/redraw", function() { o("should run a single renderer entry", function() { var spy = o.spy() - m.mount(root, {view: spy}) + m.mount(root, () => ({view: spy})) o(spy.callCount).equals(1) @@ -68,9 +68,9 @@ o.spec("mount/redraw", function() { var spy2 = o.spy() var spy3 = o.spy() - m.mount(el1, {view: spy1}) - m.mount(el2, {view: spy2}) - m.mount(el3, {view: spy3}) + m.mount(el1, () => ({view: spy1})) + m.mount(el2, () => ({view: spy2})) + m.mount(el3, () => ({view: spy3})) m.redraw() @@ -99,17 +99,17 @@ o.spec("mount/redraw", function() { var spy2 = o.spy() var spy3 = o.spy() - m.mount(el1, {view: spy1}) + m.mount(el1, () => ({view: spy1})) o(spy1.callCount).equals(1) o(spy2.callCount).equals(0) o(spy3.callCount).equals(0) - m.mount(el2, {view: spy2}) + m.mount(el2, () => ({view: spy2})) o(spy1.callCount).equals(1) o(spy2.callCount).equals(1) o(spy3.callCount).equals(0) - m.mount(el3, {view: spy3}) + m.mount(el3, () => ({view: spy3})) o(spy1.callCount).equals(1) o(spy2.callCount).equals(1) o(spy3.callCount).equals(1) @@ -118,7 +118,7 @@ o.spec("mount/redraw", function() { o("should stop running after mount null", function() { var spy = o.spy() - m.mount(root, {view: spy}) + m.mount(root, () => ({view: spy})) o(spy.callCount).equals(1) m.mount(root, null) @@ -132,7 +132,7 @@ o.spec("mount/redraw", function() { o("should stop running after mount undefined", function() { var spy = o.spy() - m.mount(root, {view: spy}) + m.mount(root, () => ({view: spy})) o(spy.callCount).equals(1) m.mount(root, undefined) @@ -146,7 +146,7 @@ o.spec("mount/redraw", function() { o("should stop running after mount no arg", function() { var spy = o.spy() - m.mount(root, {view: spy}) + m.mount(root, () => ({view: spy})) o(spy.callCount).equals(1) m.mount(root) @@ -161,7 +161,7 @@ o.spec("mount/redraw", function() { var spy = o.spy() var onremove = o.spy() - m.mount(root, {view: spy, onremove: onremove}) + m.mount(root, () => ({view: spy, onremove: onremove})) o(spy.callCount).equals(1) m.mount(root) @@ -172,7 +172,7 @@ o.spec("mount/redraw", function() { o("should stop running after unsubscribe, even if it occurs after redraw is requested", function() { var spy = o.spy() - m.mount(root, {view: spy}) + m.mount(root, () => ({view: spy})) o(spy.callCount).equals(1) m.redraw() m.mount(root) @@ -185,7 +185,7 @@ o.spec("mount/redraw", function() { o("does nothing on invalid unmount", function() { var spy = o.spy() - m.mount(root, {view: spy}) + m.mount(root, () => ({view: spy})) o(spy.callCount).equals(1) m.mount(null) @@ -202,9 +202,9 @@ o.spec("mount/redraw", function() { var spy2 = o.spy() var spy3 = o.spy() - m.mount(el1, {view: spy1}) - m.mount(el2, {view: spy2}) - m.mount(el3, {view: spy3}) + 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) @@ -234,14 +234,14 @@ o.spec("mount/redraw", function() { var root2 = $document.createElement("div") var root3 = $document.createElement("div") - m.mount(root1, { + m.mount(root1, () => ({ onbeforeupdate: function() { m.mount(root2, null) }, view: function() { calls.push("root1") }, - }) - m.mount(root2, {view: function() { calls.push("root2") }}) - m.mount(root3, {view: function() { calls.push("root3") }}) + })) + m.mount(root2, () => ({view: function() { calls.push("root2") }})) + m.mount(root3, () => ({view: function() { calls.push("root3") }})) o(calls).deepEquals([ "root1", "root2", "root3", ]) @@ -259,14 +259,14 @@ o.spec("mount/redraw", function() { var root2 = $document.createElement("div") var root3 = $document.createElement("div") - m.mount(root1, {view: function() { calls.push("root1") }}) - m.mount(root2, { + m.mount(root1, () => ({view: function() { calls.push("root1") }})) + m.mount(root2, () => ({ onbeforeupdate: function() { m.mount(root1, null) }, view: function() { calls.push("root2") }, - }) - m.mount(root3, {view: function() { calls.push("root3") }}) + })) + m.mount(root3, () => ({view: function() { calls.push("root3") }})) o(calls).deepEquals([ "root1", "root2", "root3", ]) @@ -285,15 +285,15 @@ o.spec("mount/redraw", function() { var root2 = $document.createElement("div") var root3 = $document.createElement("div") - m.mount(root1, {view: function() { calls.push("root1") }}) - m.mount(root2, { + m.mount(root1, () => ({view: function() { calls.push("root1") }})) + m.mount(root2, () => ({ onbeforeupdate: function() { m.mount(root1, null) throw "fail" }, view: function() { calls.push("root2") }, - }) - m.mount(root3, {view: function() { calls.push("root3") }}) + })) + m.mount(root3, () => ({view: function() { calls.push("root3") }})) o(calls).deepEquals([ "root1", "root2", "root3", ]) @@ -311,14 +311,14 @@ o.spec("mount/redraw", function() { var root2 = $document.createElement("div") var root3 = $document.createElement("div") - m.mount(root1, {view: function() { calls.push("root1") }}) - m.mount(root2, { + m.mount(root1, () => ({view: function() { calls.push("root1") }})) + m.mount(root2, () => ({ onbeforeupdate: function() { try { m.mount(root2, null) } catch (e) { calls.push([e.constructor, e.message]) } }, view: function() { calls.push("root2") }, - }) - m.mount(root3, {view: function() { calls.push("root3") }}) + })) + m.mount(root3, () => ({view: function() { calls.push("root3") }})) o(calls).deepEquals([ "root1", "root2", "root3", ]) @@ -339,14 +339,14 @@ o.spec("mount/redraw", function() { var root2 = $document.createElement("div") var root3 = $document.createElement("div") - m.mount(root1, {view: function() { calls.push("root1") }}) - m.mount(root2, { + m.mount(root1, () => ({view: function() { calls.push("root1") }})) + m.mount(root2, () => ({ onbeforeupdate: function() { try { m.mount(root2, null) } catch (e) { throw [e.constructor, e.message] } }, view: function() { calls.push("root2") }, - }) - m.mount(root3, {view: function() { calls.push("root3") }}) + })) + m.mount(root3, () => ({view: function() { calls.push("root3") }})) o(calls).deepEquals([ "root1", "root2", "root3", ]) diff --git a/api/tests/test-router.js b/api/tests/test-router.js index 3efe9fa6b..a19f89e53 100644 --- a/api/tests/test-router.js +++ b/api/tests/test-router.js @@ -97,7 +97,7 @@ o.spec("route", function() { o("throws on invalid `root` DOM node", function() { var threw = false try { - route(null, "/", {"/":{view: lock(function() {})}}) + route(null, "/", {"/": () => ({view: lock(function() {})})}) } catch (e) { threw = true } @@ -107,11 +107,11 @@ o.spec("route", function() { o("renders into `root`", function() { $window.location.href = prefix + "/" route(root, "/", { - "/" : { + "/": () => ({ view: lock(function() { return m("div") }) - } + }) }) o(root.firstChild.nodeName).equals("DIV") @@ -120,11 +120,11 @@ o.spec("route", function() { o("resolves to route with escaped unicode", function() { $window.location.href = prefix + "/%C3%B6?%C3%B6=%C3%B6" route(root, "/ö", { - "/ö" : { + "/ö": () => ({ view: lock(function() { return m("div") }) - } + }) }) o(root.firstChild.nodeName).equals("DIV") @@ -133,12 +133,12 @@ o.spec("route", function() { o("resolves to route with unicode", function() { $window.location.href = prefix + "/ö?ö=ö" route(root, "/ö", { - "/ö" : { + "/ö": () => ({ view: lock(function() { return JSON.stringify(route.param()) + " " + route.get() }) - } + }) }) o(root.firstChild.nodeValue).equals('{"ö":"ö"} /ö?ö=ö') @@ -147,11 +147,11 @@ o.spec("route", function() { o("resolves to route with matching invalid escape", function() { $window.location.href = prefix + "/%C3%B6abc%def" route(root, "/öabc%def", { - "/öabc%def" : { + "/öabc%def": () => ({ view: lock(function() { return route.get() }) - } + }) }) o(root.firstChild.nodeValue).equals("/öabc%def") @@ -160,13 +160,13 @@ o.spec("route", function() { o("handles parameterized route", function() { $window.location.href = prefix + "/test/x" route(root, "/test/:a", { - "/test/:a" : { + "/test/:a": () => ({ view: lock(function(vnode) { return JSON.stringify(route.param()) + " " + JSON.stringify(vnode.attrs) + " " + route.get() }) - } + }) }) o(root.firstChild.nodeValue).equals( @@ -177,13 +177,13 @@ o.spec("route", function() { o("handles multi-parameterized route", function() { $window.location.href = prefix + "/test/x/y" route(root, "/test/:a/:b", { - "/test/:a/:b" : { + "/test/:a/:b": () => ({ view: lock(function(vnode) { return JSON.stringify(route.param()) + " " + JSON.stringify(vnode.attrs) + " " + route.get() }) - } + }) }) o(root.firstChild.nodeValue).equals( @@ -194,13 +194,13 @@ o.spec("route", function() { o("handles rest parameterized route", function() { $window.location.href = prefix + "/test/x/y" route(root, "/test/:a...", { - "/test/:a..." : { + "/test/:a...": () => ({ view: lock(function(vnode) { return JSON.stringify(route.param()) + " " + JSON.stringify(vnode.attrs) + " " + route.get() }) - } + }) }) o(root.firstChild.nodeValue).equals( @@ -211,13 +211,13 @@ o.spec("route", function() { o("keeps trailing / in rest parameterized route", function() { $window.location.href = prefix + "/test/d/" route(root, "/test/:a...", { - "/test/:a..." : { + "/test/:a...": () => ({ view: lock(function(vnode) { return JSON.stringify(route.param()) + " " + JSON.stringify(vnode.attrs) + " " + route.get() }) - } + }) }) o(root.firstChild.nodeValue).equals( @@ -228,13 +228,13 @@ o.spec("route", function() { o("handles route with search", function() { $window.location.href = prefix + "/test?a=b&c=d" route(root, "/test", { - "/test" : { + "/test": () => ({ view: lock(function(vnode) { return JSON.stringify(route.param()) + " " + JSON.stringify(vnode.attrs) + " " + route.get() }) - } + }) }) o(root.firstChild.nodeValue).equals( @@ -245,13 +245,13 @@ o.spec("route", function() { o("redirects to default route if no match", function() { $window.location.href = prefix + "/test" route(root, "/other", { - "/other": { + "/other": () => ({ view: lock(function(vnode) { return JSON.stringify(route.param()) + " " + JSON.stringify(vnode.attrs) + " " + route.get() }) - } + }) }) return waitCycles(1).then(function() { @@ -263,12 +263,12 @@ o.spec("route", function() { $window.location.href = prefix + "/z/y/x" route(root, "/z/y/x", { - "/z/y/x": { + "/z/y/x": () => ({ view: lock(function() { return "1" }), - }, - "/:a...": { + }), + "/:a...": () => ({ view: lock(function() { return "2" }), - }, + }), }) o(root.firstChild.nodeValue).equals("1") @@ -278,12 +278,12 @@ o.spec("route", function() { $window.location.href = prefix + "/z/y/x" route(root, "/z/y/x", { - "/:a...": { + "/:a...": () => ({ view: lock(function() { return "2" }), - }, - "/z/y/x": { + }), + "/z/y/x": () => ({ view: lock(function() { return "1" }), - }, + }), }) o(root.firstChild.nodeValue).equals("2") @@ -293,13 +293,13 @@ o.spec("route", function() { $window.location.href = "file://" + prefix + "/test" route(root, "/test", { - "/test" : { + "/test": () => ({ view: lock(function(vnode) { return JSON.stringify(route.param()) + " " + JSON.stringify(vnode.attrs) + " " + route.get() }) - } + }) }) o(root.firstChild.nodeValue).equals("{} {} /test") @@ -309,7 +309,7 @@ o.spec("route", function() { var view = o.spy() $window.location.href = prefix + "/" - route(root, "/", {"/":{view:view}}) + route(root, "/", {"/": () => ({view})}) o(view.callCount).equals(1) @@ -365,11 +365,11 @@ o.spec("route", function() { $window.location.href = prefix + "/" route(root, "/", { - "/" : { + "/": () => ({ view: lock(function() { return m("div") }) - } + }) }) o(root.firstChild.nodeName).equals("DIV") @@ -384,11 +384,11 @@ o.spec("route", function() { $window.location.href = "http://new.com" route(root, "/a", { - "/a" : { + "/a": () => ({ view: lock(function() { return m("div") }) - } + }) }) return waitCycles(1).then(function() { @@ -406,14 +406,14 @@ o.spec("route", function() { o("default route does not inherit params", function() { $window.location.href = "/invalid?foo=bar" route(root, "/a", { - "/a" : { + "/a": () => ({ oninit: lock(function(vnode) { o(vnode.attrs.foo).equals(undefined) }), view: lock(function() { return m("div") }) - } + }) }) return waitCycles(1) @@ -425,14 +425,14 @@ o.spec("route", function() { $window.location.href = prefix + "/" route(root, "/", { - "/" : { + "/": () => ({ view: lock(function() { return m("div", { oninit: oninit, onupdate: onupdate }) }) - } + }) }) o(oninit.callCount).equals(1) @@ -453,7 +453,7 @@ o.spec("route", function() { $window.location.href = prefix + "/" route(root, "/", { - "/" : { + "/": () => ({ view: lock(function() { return m("div", { oninit: oninit, @@ -461,7 +461,7 @@ o.spec("route", function() { onclick: onclick, }) }) - } + }) }) root.firstChild.dispatchEvent(e) @@ -487,7 +487,7 @@ o.spec("route", function() { $window.location.href = prefix + "/" route(root, "/", { - "/" : { + "/": () => ({ view: lock(function() { return m("div", { oninit: oninit, @@ -497,7 +497,7 @@ o.spec("route", function() { }), }) }) - } + }) }) o(oninit.callCount).equals(1) @@ -519,16 +519,16 @@ o.spec("route", function() { $window.location.href = prefix + "/" route(root, "/", { - "/" : { + "/": () => ({ view: lock(function() { return m(route.Link, {href: "/test"}) }) - }, - "/test" : { + }), + "/test": () => ({ view : lock(function() { return m("div") }) - } + }) }) var slash = prefix[0] === "/" ? "" : "/" @@ -549,19 +549,19 @@ o.spec("route", function() { $window.location.href = prefix + "/" route(root, "/", { - "/" : { + "/": () => ({ view: lock(function() { return m(route.Link, { href: "/test", options: opts, }) }) - }, - "/test" : { + }), + "/test": () => ({ view : lock(function() { return m("div") }) - } + }) }) route.set = o.spy(route.set) @@ -579,19 +579,19 @@ o.spec("route", function() { $window.location.href = prefix + "/" route(root, "/", { - "/" : { + "/": () => ({ view: lock(function() { return m(route.Link, { href: "/test", params: {key: "value"}, }) }) - }, - "/test" : { + }), + "/test": () => ({ view : lock(function() { return m("div") }) - } + }) }) route.set = o.spy(route.set) @@ -807,16 +807,16 @@ o.spec("route", function() { $window.location.href = prefix + "/" route(root, "/", { - "/" : { + "/": () => ({ view: lock(function() { return m(route.Link, {href: "/test"}) }) - }, - "/test" : { + }), + "/test": () => ({ view : lock(function() { return m("div") }) - } + }) }) var slash = prefix[0] === "/" ? "" : "/" @@ -836,7 +836,7 @@ o.spec("route", function() { $window.location.href = prefix + "/" route(root, "/", { - "/" : { + "/": () => ({ view: lock(function() { return m(route.Link, { href: "/test", @@ -845,12 +845,12 @@ o.spec("route", function() { } }) }) - }, - "/test" : { + }), + "/test": () => ({ view : lock(function() { return m("div") }) - } + }) }) var slash = prefix[0] === "/" ? "" : "/" @@ -870,7 +870,7 @@ o.spec("route", function() { $window.location.href = prefix + "/" route(root, "/", { - "/" : { + "/": () => ({ view: lock(function() { return m(route.Link, { href: "/test", @@ -881,12 +881,12 @@ o.spec("route", function() { } }) }) - }, - "/test" : { + }), + "/test": () => ({ view : lock(function() { return m("div") }) - } + }) }) var slash = prefix[0] === "/" ? "" : "/" @@ -906,7 +906,7 @@ o.spec("route", function() { $window.location.href = prefix + "/" route(root, "/", { - "/" : { + "/": () => ({ view: lock(function() { return m(route.Link, { href: "/test", @@ -915,12 +915,12 @@ o.spec("route", function() { } }) }) - }, - "/test" : { + }), + "/test": () => ({ view : lock(function() { return m("div") }) - } + }) }) var slash = prefix[0] === "/" ? "" : "/" @@ -935,11 +935,11 @@ o.spec("route", function() { o("accepts RouteResolver with onmatch that returns Component", function() { var matchCount = 0 var renderCount = 0 - var Component = { + var Component = () => ({ view: lock(function() { return m("span") }) - } + }) var resolver = { onmatch: lock(function(args, requestedPath, route) { @@ -978,11 +978,11 @@ o.spec("route", function() { var match2Count = 0 var render1 = o.spy() var render2Count = 0 - var Component = { + var Component = () => ({ view: lock(function() { return m("span") }) - } + }) var resolver1 = { onmatch: lock(function(args, requestedPath, key) { @@ -1036,11 +1036,11 @@ o.spec("route", function() { o("accepts RouteResolver with onmatch that returns Promise", function() { var matchCount = 0 var renderCount = 0 - var Component = { + var Component = () => ({ view: lock(function() { return m("span") }) - } + }) var resolver = { onmatch: lock(function(args, requestedPath, route) { @@ -1166,7 +1166,7 @@ o.spec("route", function() { $window.location.href = prefix + "/test/1" route(root, "/default", { - "/default" : {view: spy}, + "/default": () => ({view: spy}), "/test/:id" : resolver }) @@ -1181,15 +1181,15 @@ o.spec("route", function() { o("accepts RouteResolver without `render` method as payload", function() { var matchCount = 0 - var Component = { + var Component = () => ({ view: lock(function() { return m("div") }) - } + }) $window.location.href = prefix + "/abc" route(root, "/abc", { - "/:id" : { + "/:id": { onmatch: lock(function(args, requestedPath, route) { matchCount++ @@ -1210,12 +1210,12 @@ o.spec("route", function() { o("changing `key` param resets the component", function(){ var oninit = o.spy() - var Component = { + var Component = () => ({ oninit: oninit, view: lock(function() { return m("div") }) - } + }) $window.location.href = prefix + "/abc" route(root, "/abc", { "/:key": Component, @@ -1232,15 +1232,15 @@ o.spec("route", function() { o("accepts RouteResolver without `onmatch` method as payload", function() { var renderCount = 0 - var Component = { + var Component = () => ({ view: lock(function() { return m("div") }) - } + }) $window.location.href = prefix + "/abc" route(root, "/abc", { - "/:id" : { + "/:id": { render: lock(function(vnode) { renderCount++ @@ -1258,12 +1258,12 @@ o.spec("route", function() { o("RouteResolver `render` does not have component semantics", function() { $window.location.href = prefix + "/a" route(root, "/a", { - "/a" : { + "/a": { render: lock(function() { return m("div", m("p")) }), }, - "/b" : { + "/b": { render: lock(function() { return m("div", m("a")) }), @@ -1288,15 +1288,15 @@ o.spec("route", function() { o("calls onmatch and view correct number of times", function() { var matchCount = 0 var renderCount = 0 - var Component = { + var Component = () => ({ view: lock(function() { return m("div") }) - } + }) $window.location.href = prefix + "/" route(root, "/", { - "/" : { + "/": { onmatch: lock(function() { matchCount++ return Component @@ -1323,15 +1323,15 @@ o.spec("route", function() { o("calls onmatch and view correct number of times when not onmatch returns undefined", function() { var matchCount = 0 var renderCount = 0 - var Component = { + var Component = () => ({ view: lock(function() { return m("div") }) - } + }) $window.location.href = prefix + "/" route(root, "/", { - "/" : { + "/": { onmatch: lock(function() { matchCount++ }), @@ -1360,17 +1360,17 @@ o.spec("route", function() { $window.location.href = prefix + "/a" route(root, "/a", { - "/a" : { + "/a": { onmatch: lock(function() { route.set("/b") }), render: lock(render) }, - "/b" : { + "/b": () => ({ view: lock(function() { redirected = true }) - } + }) }) return waitCycles(2).then(function() { @@ -1386,16 +1386,16 @@ o.spec("route", function() { $window.location.href = prefix + "/a" route(root, "/a", { - "/a" : { + "/a": { onmatch: lock(function() { route.set("/b", {}, {state: {a: 5}}) }), render: lock(render) }, - "/b" : { + "/b": { onmatch: lock(function() { redirected = true - return {view: lock(view)} + return () => ({view: lock(view)}) }) } }) @@ -1416,13 +1416,13 @@ o.spec("route", function() { $window.location.href = prefix + "/a" route(root, "/a", { - "/a" : { + "/a": { onmatch: lock(function() { route.set("/b") }), render: lock(render) }, - "/b" : { + "/b": { render: lock(function(){ redirected = true }) @@ -1442,17 +1442,17 @@ o.spec("route", function() { $window.location.href = prefix + "/a" route(root, "/a", { - "/a" : { + "/a": { onmatch: lock(function() { route.set("/b") }), render: lock(render) }, - "/b" : { + "/b": { onmatch: lock(function() { redirected = true return waitCycles(1).then(function(){ - return {view: view} + return () => ({view: view}) }) }) } @@ -1472,17 +1472,17 @@ o.spec("route", function() { $window.location.href = prefix + "/a" route(root, "/a", { - "/a" : { + "/a": { onmatch: lock(function() { waitCycles(1).then(function() {route.set("/b")}) return new Promise(function() {}) }), render: lock(render) }, - "/b" : { + "/b": { onmatch: lock(function() { redirected = true - return {view: lock(view)} + return () => ({view: lock(view)}) }) } }) @@ -1497,19 +1497,20 @@ o.spec("route", function() { o("onmatch can redirect with window.history.back()", function() { var render = o.spy() - var component = {view: o.spy()} + var instance = {view: o.spy()} + var Component = () => instance $window.location.href = prefix + "/a" route(root, "/a", { - "/a" : { + "/a": { onmatch: lock(function() { - return component + return Component }), render: lock(function(vnode) { return vnode }) }, - "/b" : { + "/b": { onmatch: lock(function() { $window.history.back() return new Promise(function() {}) @@ -1523,13 +1524,13 @@ o.spec("route", function() { route.set("/b") o(render.callCount).equals(0) - o(component.view.callCount).equals(1) + o(instance.view.callCount).equals(1) return waitCycles(4).then(function() { throttleMock.fire() o(render.callCount).equals(0) - o(component.view.callCount).equals(2) + o(instance.view.callCount).equals(2) }) }) }) @@ -1540,16 +1541,16 @@ o.spec("route", function() { $window.location.href = prefix + "/a" route(root, "/b", { - "/a" : { + "/a": { onmatch: lock(function() { route.set("/c") }), render: lock(render) }, - "/b" : { + "/b": { onmatch: lock(function(){ redirected = true - return {view: lock(function() {})} + return () => ({view: lock(function() {})}) }) } }) @@ -1566,13 +1567,13 @@ o.spec("route", function() { $window.location.href = prefix + "/a" route(root, "/b", { - "/a" : { + "/a": { onmatch: lock(function() { route.set("/c") }), render: lock(render) }, - "/b" : { + "/b": { render: lock(function(){ redirected = true }) @@ -1591,17 +1592,17 @@ o.spec("route", function() { $window.location.href = prefix + "/a" route(root, "/b", { - "/a" : { + "/a": { onmatch: lock(function() { route.set("/c") }), render: lock(render) }, - "/b" : { + "/b": () => ({ view: lock(function(){ redirected = true }) - } + }) }) return waitCycles(3).then(function() { @@ -1618,9 +1619,9 @@ o.spec("route", function() { $window.location.href = prefix + "/a" route(root, "/", { - "/a": {view: lock(view)}, + "/a": () => ({view: lock(view)}), "/b": {onmatch: lock(onmatch)}, - "/": {view: lock(function() {})} + "/": () => ({view: lock(function() {})}) }) o(view.callCount).equals(1) @@ -1725,7 +1726,7 @@ o.spec("route", function() { $window.location.href = prefix + "/" route(root, "/", { - "/": {view: lock(function() {})}, + "/": () => ({view: lock(function() {})}), "/2": { onmatch: lock(function() { return new Promise(function() {}) @@ -1783,7 +1784,7 @@ o.spec("route", function() { "/a": { onmatch: lock(function() { return waitCycles(2).then(function() { - return {view: lock(function() {rendered = true})} + return () => ({view: lock(function() {rendered = true})}) }) }), render: lock(function() { @@ -1791,11 +1792,11 @@ o.spec("route", function() { resolved = "a" }) }, - "/b": { + "/b": () => ({ view: lock(function() { resolved = "b" }) - } + }) }) route.set("/b") @@ -1816,13 +1817,13 @@ o.spec("route", function() { $window.location.href = prefix + "/a" route(root, "/a", { - "/a": { + "/a": () => ({ onbeforeremove: lock(spy), view: lock(function() {}) - }, - "/b": { + }), + "/b": () => ({ view: lock(function() {}) - } + }) }) route.set("/b") @@ -1848,11 +1849,11 @@ o.spec("route", function() { resolved = "a" }) }, - "/b": { + "/b": () => ({ view: lock(function() { resolved = "b" }) - }, + }), }) // tick for popstate for /a @@ -1869,7 +1870,7 @@ o.spec("route", function() { var i = 0 $window.location.href = prefix + "/" route(root, "/", { - "/": {view: lock(function() {i++})} + "/": () => ({view: lock(function() {i++})}) }) var before = i @@ -1890,13 +1891,13 @@ o.spec("route", function() { $window.location.href = prefix + "/" route(root, "/1", { - "/:id" : { + "/:id": () => ({ view : lock(function() { o(route.param("id")).equals("1") return m("div") }) - } + }) }) o(route.param("id")).equals(undefined); diff --git a/api/tests/test-routerGetSet.js b/api/tests/test-routerGetSet.js index 51752c80b..0f4ca8f5c 100644 --- a/api/tests/test-routerGetSet.js +++ b/api/tests/test-routerGetSet.js @@ -33,7 +33,7 @@ o.spec("route.get/route.set", function() { o("gets route", function() { $window.location.href = prefix + "/test" - route(root, "/test", {"/test": {view: function() {}}}) + route(root, "/test", {"/test": () => ({view: function() {}})}) o(route.get()).equals("/test") }) @@ -42,8 +42,8 @@ o.spec("route.get/route.set", 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() {}}, + "/test": () => ({view: function() {}}), + "/other/:a/:b...": () => ({view: function() {}}), }) o(route.get()).equals("/other/x/y/z?c=d#e=f") @@ -53,8 +53,8 @@ o.spec("route.get/route.set", function() { $window.location.href = prefix + encodeURI("/ö/é/å?ö=ö#ö=ö") route(root, "/ö/é/å?ö=ö#ö=ö", { - "/test": {view: function() {}}, - "/ö/:a/:b...": {view: function() {}}, + "/test": () => ({view: function() {}}), + "/ö/:a/:b...": () => ({view: function() {}}), }) o(route.get()).equals("/ö/é/å?ö=ö#ö=ö") @@ -64,8 +64,8 @@ o.spec("route.get/route.set", function() { $window.location.href = prefix + "/ö/é/å?ö=ö#ö=ö" route(root, "/ö/é/å?ö=ö#ö=ö", { - "/test": {view: function() {}}, - "/ö/:a/:b...": {view: function() {}}, + "/test": () => ({view: function() {}}), + "/ö/:a/:b...": () => ({view: function() {}}), }) o(route.get()).equals("/ö/é/å?ö=ö#ö=ö") @@ -77,8 +77,8 @@ o.spec("route.get/route.set", function() { var spy2 = o.spy() route(root, "/a", { - "/a": {view: spy1}, - "/b": {view: spy2}, + "/a": () => ({view: spy1}), + "/b": () => ({view: spy2}), }) o(spy1.callCount).equals(1) @@ -101,8 +101,8 @@ o.spec("route.get/route.set", function() { var spy2 = o.spy() route(root, "/a", { - "/a": {view: spy1}, - "/b": {view: spy2}, + "/a": () => ({view: spy1}), + "/b": () => ({view: spy2}), }) o(spy1.callCount).equals(0) @@ -128,8 +128,8 @@ o.spec("route.get/route.set", function() { o("exposes new route asynchronously", function(done) { $window.location.href = prefix + "/test" route(root, "/test", { - "/test": {view: function() {}}, - "/other/:a/:b...": {view: function() {}}, + "/test": () => ({view: function() {}}), + "/other/:a/:b...": () => ({view: function() {}}), }) route.set("/other/x/y/z?c=d#e=f") @@ -144,8 +144,8 @@ o.spec("route.get/route.set", function() { o("exposes new escaped unicode route asynchronously", function(done) { $window.location.href = prefix + "/test" route(root, "/test", { - "/test": {view: function() {}}, - "/ö": {view: function() {}}, + "/test": () => ({view: function() {}}), + "/ö": () => ({view: function() {}}), }) route.set(encodeURI("/ö?ö=ö#ö=ö")) @@ -160,8 +160,8 @@ o.spec("route.get/route.set", function() { o("exposes new unescaped unicode route asynchronously", function(done) { $window.location.href = "file://" + prefix + "/test" route(root, "/test", { - "/test": {view: function() {}}, - "/ö": {view: function() {}}, + "/test": () => ({view: function() {}}), + "/ö": () => ({view: function() {}}), }) route.set("/ö?ö=ö#ö=ö") @@ -176,8 +176,8 @@ o.spec("route.get/route.set", function() { 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() {}}, + "/test": () => ({view: function() {}}), + "/other/:a/:b...": () => ({view: function() {}}), }) route.set("/other/x/y/z?c=d#e=f") @@ -192,8 +192,8 @@ o.spec("route.get/route.set", function() { o("sets route via pushState/onpopstate", function(done) { $window.location.href = prefix + "/test" route(root, "/test", { - "/test": {view: function() {}}, - "/other/:a/:b...": {view: function() {}}, + "/test": () => ({view: function() {}}), + "/other/:a/:b...": () => ({view: function() {}}), }) setTimeout(function() { @@ -213,8 +213,8 @@ o.spec("route.get/route.set", function() { o("sets parameterized route", function(done) { $window.location.href = prefix + "/test" route(root, "/test", { - "/test": {view: function() {}}, - "/other/:a/:b...": {view: function() {}}, + "/test": () => ({view: function() {}}), + "/other/:a/:b...": () => ({view: function() {}}), }) route.set("/other/:a/:b", {a: "x", b: "y/z", c: "d", e: "f"}) @@ -229,8 +229,8 @@ o.spec("route.get/route.set", function() { o("replace:true works", function(done) { $window.location.href = prefix + "/test" route(root, "/test", { - "/test": {view: function() {}}, - "/other": {view: function() {}}, + "/test": () => ({view: function() {}}), + "/other": () => ({view: function() {}}), }) route.set("/other", null, {replace: true}) @@ -246,8 +246,8 @@ o.spec("route.get/route.set", function() { o("replace:false works", function(done) { $window.location.href = prefix + "/test" route(root, "/test", { - "/test": {view: function() {}}, - "/other": {view: function() {}}, + "/test": () => ({view: function() {}}), + "/other": () => ({view: function() {}}), }) route.set("/other", null, {replace: false}) @@ -264,8 +264,8 @@ o.spec("route.get/route.set", function() { o("state works", function(done) { $window.location.href = prefix + "/test" route(root, "/test", { - "/test": {view: function() {}}, - "/other": {view: function() {}}, + "/test": () => ({view: function() {}}), + "/other": () => ({view: function() {}}), }) route.set("/other", null, {state: {a: 1}}) diff --git a/performance/test-perf.js b/performance/test-perf.js index 6f6ff1137..63acc5483 100644 --- a/performance/test-perf.js +++ b/performance/test-perf.js @@ -251,8 +251,8 @@ suite.add("add large nested tree", { fields.push((i * 999).toString(36)) } - var NestedHeader = { - view: function () { + var NestedHeader = () => ({ + view() { return m("header", m("h1.asdf", "a ", "b", " c ", 0, " d"), m("nav", @@ -261,10 +261,10 @@ suite.add("add large nested tree", { ) ) } - } + }) - var NestedForm = { - view: function () { + var NestedForm = () => ({ + view() { return m("form", {onSubmit: function () {}}, m("input[type=checkbox][checked]"), m("input[type=checkbox]", {checked: false}), @@ -288,10 +288,10 @@ suite.add("add large nested tree", { m(NestedButtonBar, null) ) } - } + }) - var NestedButtonBar = { - view: function () { + var NestedButtonBar = () => ({ + view() { return m(".button-bar", m(NestedButton, {style: "width:10px; height:10px; border:1px solid #FFF;"}, @@ -311,29 +311,29 @@ suite.add("add large nested tree", { ) ) } - } + }) - var NestedButton = { - view: function (vnode) { + var NestedButton = () => ({ + view(vnode) { return m("button", m.censor(vnode.attrs), vnode.children) } - } + }) - var NestedMain = { - view: function () { + var NestedMain = () => ({ + view() { return m(NestedForm) } - } + }) - this.NestedRoot = { - view: function () { + this.NestedRoot = () => ({ + view() { return m("div.foo.bar[data-foo=bar]", {p: 2}, m(NestedHeader), m(NestedMain) ) } - } + }) }, fn: function () { m.render(rootElem, m(this.NestedRoot)) @@ -401,8 +401,8 @@ suite.add("mutate styles/properties", { suite.add("repeated add/removal", { setup: function () { - var RepeatedHeader = { - view: function () { + var RepeatedHeader = () => ({ + view() { return m("header", m("h1.asdf", "a ", "b", " c ", 0, " d"), m("nav", @@ -411,10 +411,10 @@ suite.add("repeated add/removal", { ) ) } - } + }) - var RepeatedForm = { - view: function () { + var RepeatedForm = () => ({ + view() { return m("form", {onSubmit: function () {}}, m("input", {type: "checkbox", checked: true}), m("input", {type: "checkbox", checked: false}), @@ -429,10 +429,10 @@ suite.add("repeated add/removal", { m(RepeatedButtonBar, null) ) } - } + }) - var RepeatedButtonBar = { - view: function () { + var RepeatedButtonBar = () => ({ + view() { return m(".button-bar", m(RepeatedButton, {style: "width:10px; height:10px; border:1px solid #FFF;"}, @@ -452,29 +452,29 @@ suite.add("repeated add/removal", { ) ) } - } + }) - var RepeatedButton = { - view: function (vnode) { + var RepeatedButton = () => ({ + view(vnode) { return m("button", vnode.attrs, vnode.children) } - } + }) - var RepeatedMain = { - view: function () { + var RepeatedMain = () => ({ + view() { return m(RepeatedForm) } - } + }) - this.RepeatedRoot = { - view: function () { + this.RepeatedRoot = () => ({ + view() { return m("div.foo.bar[data-foo=bar]", {p: 2}, m(RepeatedHeader, null), m(RepeatedMain, null) ) } - } + }) }, fn: function () { m.render(rootElem, [m(this.RepeatedRoot)]) diff --git a/render/hyperscript.js b/render/hyperscript.js index bfeca3666..d08192eec 100644 --- a/render/hyperscript.js +++ b/render/hyperscript.js @@ -68,7 +68,7 @@ function execSelector(selector, attrs, children) { // Caution is advised when editing this - it's very perf-critical. It's specially designed to avoid // allocations in the fast path, especially with fragments. function hyperscript(selector, attrs, ...children) { - if (selector == null || typeof selector !== "string" && typeof selector !== "function" && typeof selector.view !== "function") { + if (typeof selector !== "string" && typeof selector !== "function") { throw new Error("The selector must be either a string or a component."); } diff --git a/render/render.js b/render/render.js index 6e918838d..37508ba64 100644 --- a/render/render.js +++ b/render/render.js @@ -112,25 +112,17 @@ module.exports = function() { } } } + var reentrantLock = new WeakSet() function initComponent(vnode, hooks) { - var sentinel - if (typeof vnode.tag.view === "function") { - vnode.state = Object.create(vnode.tag) - sentinel = vnode.state.view - if (sentinel.$$reentrantLock$$ != null) return - sentinel.$$reentrantLock$$ = true - } else { - vnode.state = void 0 - sentinel = vnode.tag - if (sentinel.$$reentrantLock$$ != null) return - sentinel.$$reentrantLock$$ = true - vnode.state = (vnode.tag.prototype != null && typeof vnode.tag.prototype.view === "function") ? new vnode.tag(vnode) : vnode.tag(vnode) - } + vnode.state = void 0 + if (reentrantLock.has(vnode.tag)) return + reentrantLock.add(vnode.tag) + vnode.state = (vnode.tag.prototype != null && typeof vnode.tag.prototype.view === "function") ? new vnode.tag(vnode) : vnode.tag(vnode) initLifecycle(vnode.state, vnode, hooks) if (vnode.attrs != null) initLifecycle(vnode.attrs, vnode, hooks) vnode.instance = Vnode.normalize(callHook.call(vnode.state.view, vnode)) if (vnode.instance === vnode) throw Error("A view cannot return the vnode it received as argument") - sentinel.$$reentrantLock$$ = null + reentrantLock.delete(vnode.tag) } function createComponent(parent, vnode, hooks, ns, nextSibling) { initComponent(vnode, hooks) diff --git a/render/tests/manual/iframe.html b/render/tests/manual/iframe.html index 01cc505d6..3e9e7bcdb 100644 --- a/render/tests/manual/iframe.html +++ b/render/tests/manual/iframe.html @@ -9,14 +9,14 @@ diff --git a/render/tests/test-component.js b/render/tests/test-component.js index 74b1c87a1..91a8094ab 100644 --- a/render/tests/test-component.js +++ b/render/tests/test-component.js @@ -837,13 +837,13 @@ o.spec("component", function() { o.spec("state", function() { o("initializes state", function() { var data = {a: 1} - var component = createComponent(createComponent({ + var component = createComponent({ data: data, oninit: init, view: function() { return "" } - })) + }) render(root, m(component)) @@ -854,13 +854,13 @@ o.spec("component", function() { o("state proxies to the component object/prototype", function() { var body = {a: 1} var data = [body] - var component = createComponent(createComponent({ + var component = createComponent({ data: data, oninit: init, view: function() { return "" } - })) + }) render(root, m(component)) @@ -874,26 +874,6 @@ o.spec("component", function() { }) o.spec("Tests specific to certain component kinds", function() { o.spec("state", function() { - o("POJO", function() { - var data = {} - var component = { - data: data, - oninit: init, - view: function() { - return "" - } - } - - render(root, m(component)) - - function init(vnode) { - o(vnode.state.data).equals(data) - - //inherits state via prototype - component.x = 1 - o(vnode.state.x).equals(1) - } - }) o("Constructible", function() { var oninit = o.spy() var component = o.spy(function(vnode){ diff --git a/render/tests/test-hyperscript.js b/render/tests/test-hyperscript.js index c4675f249..464a20cb0 100644 --- a/render/tests/test-hyperscript.js +++ b/render/tests/test-hyperscript.js @@ -628,17 +628,6 @@ o.spec("hyperscript", function() { }) }) o.spec("components", function() { - o("works with POJOs", function() { - var component = { - view: function() {} - } - var vnode = m(component, {id: "a"}, "b") - - o(vnode.tag).equals(component) - o(vnode.attrs.id).equals("a") - o(vnode.children.length).equals(1) - o(vnode.children[0]).equals("b") - }) o("works with constructibles", function() { var component = o.spy() component.prototype.view = function() {} diff --git a/render/tests/test-normalizeComponentChildren.js b/render/tests/test-normalizeComponentChildren.js index 0ceabdf4f..31cc88041 100644 --- a/render/tests/test-normalizeComponentChildren.js +++ b/render/tests/test-normalizeComponentChildren.js @@ -11,11 +11,11 @@ o.spec("component children", function () { var render = vdom($window) o.spec("component children", function () { - var component = { + var component = () => ({ view: function (vnode) { return vnode.children } - } + }) var vnode = m(component, "a") diff --git a/render/tests/test-onbeforeupdate.js b/render/tests/test-onbeforeupdate.js index 62f70ad2b..901792e76 100644 --- a/render/tests/test-onbeforeupdate.js +++ b/render/tests/test-onbeforeupdate.js @@ -241,7 +241,7 @@ o.spec("onbeforeupdate", function() { }) o("is not called on component creation", function() { - createComponent({ + var component = createComponent({ onbeforeupdate: onbeforeupdate, view: function(vnode) { return m("div", vnode.attrs) @@ -249,7 +249,7 @@ o.spec("onbeforeupdate", function() { }) var count = 0 - var vnode = m("div", {id: "a"}) + var vnode = m(component, {id: "a"}) render(root, vnode) @@ -337,13 +337,13 @@ o.spec("onbeforeupdate", function() { o(root.firstChild.firstChild.firstChild.nodeValue).equals("foo") }) o("updating component children doesn't error", function() { - var Child = { + var Child = () => ({ view(v) { return m("div", v.attrs.foo ? m("div") : null ) } - } + }) render(root, m("div", {onbeforeupdate: function() { return true }}, diff --git a/render/tests/test-render.js b/render/tests/test-render.js index 005828d50..ad3e859d1 100644 --- a/render/tests/test-render.js +++ b/render/tests/test-render.js @@ -63,30 +63,7 @@ o.spec("render", function() { o(threw).equals(true) }) - o("does not enter infinite loop when oninit triggers render and view throws with an object literal component", function(done) { - var A = { - oninit: init, - view: function() {throw new Error("error")} - } - function run() { - render(root, m(A)) - } - function init() { - setTimeout(function() { - var threwInner = false - try {run()} catch (e) {threwInner = true} - - o(threwInner).equals(false) - done() - }, 0) - } - - var threwOuter = false - try {run()} catch (e) {threwOuter = true} - - o(threwOuter).equals(true) - }) - o("does not try to re-initialize a constructibe component whose view has thrown", function() { + o("does not try to re-initialize a constructible component whose view has thrown", function() { var oninit = o.spy() var onbeforeupdate = o.spy() function A(){} diff --git a/test-utils/components.js b/test-utils/components.js index 5ba37044a..27578b8f2 100644 --- a/test-utils/components.js +++ b/test-utils/components.js @@ -4,28 +4,20 @@ var m = require("../render/hyperscript") module.exports = [ { - kind: "POJO", - create: function(methods) { - var res = {view: function() {return m("div")}} - Object.keys(methods || {}).forEach(function(m){res[m] = methods[m]}) - return res - } - }, { kind: "constructible", create: function(methods) { - function res(){} - res.prototype.view = function() {return m("div")} - Object.keys(methods || {}).forEach(function(m){res.prototype[m] = methods[m]}) - return res + class C { + view() { + return m("div") + } + } + Object.assign(C.prototype, methods || {}) + return C } }, { kind: "closure", create: function(methods) { - return function() { - var res = {view: function() {return m("div")}} - Object.keys(methods || {}).forEach(function(m){res[m] = methods[m]}) - return res - } + return () => Object.assign({view: () => m("div")}, methods || {}) } } ] diff --git a/test-utils/tests/test-components.js b/test-utils/tests/test-components.js index 941fe7cf2..e0cbd3714 100644 --- a/test-utils/tests/test-components.js +++ b/test-utils/tests/test-components.js @@ -14,10 +14,7 @@ o.spec("test-utils/components", function() { var cmp1, cmp2 - if (component.kind === "POJO") { - cmp1 = component.create() - cmp2 = component.create(methods) - } else if (component.kind === "constructible") { + if (component.kind === "constructible") { cmp1 = new (component.create()) cmp2 = new (component.create(methods)) } else if (component.kind === "closure") { @@ -47,7 +44,7 @@ o.spec("test-utils/components", function() { } }) o.after(function(){ - o(test.callCount).equals(3) + o(test.callCount).equals(2) }) components.forEach(function(component) { o.spec(component.kind, test(component)) From e9fad45f4a53e66d56f7095e20627c93bcde0f44 Mon Sep 17 00:00:00 2001 From: Claudia Meadows Date: Tue, 1 Oct 2024 21:46:40 -0700 Subject: [PATCH 08/95] `key` attr -> `m.key(...)`, drop `domSize`, remove redundancy --- api/router.js | 3 +- api/tests/test-router.js | 4 +- api/tests/test-routerGetSet.js | 79 +++-- index.js | 5 +- render/domFor.js | 27 -- render/hyperscript.js | 15 +- render/render.js | 310 ++++++++++-------- render/tests/test-component.js | 102 +++--- render/tests/test-createFragment.js | 19 +- render/tests/test-domFor.js | 178 ----------- render/tests/test-fragment.js | 184 +++++------ render/tests/test-hyperscript.js | 6 +- render/tests/test-input.js | 10 +- render/tests/test-normalizeChildren.js | 21 +- render/tests/test-onbeforeremove.js | 168 +++++----- render/tests/test-onbeforeupdate.js | 15 - render/tests/test-oncreate.js | 34 +- render/tests/test-oninit.js | 40 +-- render/tests/test-onremove.js | 421 ++++++++++++++----------- render/tests/test-onupdate.js | 15 +- render/tests/test-render.js | 40 +-- render/tests/test-updateElement.js | 16 +- render/tests/test-updateFragment.js | 3 +- render/tests/test-updateNodes.js | 268 ++++++++-------- render/tests/test-updateNodesFuzzer.js | 4 +- render/vnode.js | 19 +- tests/test-api.js | 63 ++-- util/censor.js | 7 +- util/tests/test-censor.js | 8 +- 29 files changed, 977 insertions(+), 1107 deletions(-) delete mode 100644 render/domFor.js delete mode 100644 render/tests/test-domFor.js diff --git a/api/router.js b/api/router.js index 724792a51..ad17883c7 100644 --- a/api/router.js +++ b/api/router.js @@ -1,6 +1,5 @@ "use strict" -var Vnode = require("../render/vnode") var m = require("../render/hyperscript") var buildPathname = require("../pathname/build") @@ -49,7 +48,7 @@ module.exports = function($window, mountRedraw) { view: function() { if (!state || sentinel === currentResolver) return // Wrap in a fragment to preserve existing key semantics - var vnode = [Vnode(component, attrs.key, attrs)] + var vnode = [m(component, attrs)] if (currentResolver) vnode = currentResolver.render(vnode[0]) return vnode }, diff --git a/api/tests/test-router.js b/api/tests/test-router.js index a19f89e53..de4a64ed2 100644 --- a/api/tests/test-router.js +++ b/api/tests/test-router.js @@ -1208,7 +1208,7 @@ o.spec("route", function() { }) }) - o("changing `key` param resets the component", function(){ + o("changing `key` param does not reset the component", function(){ var oninit = o.spy() var Component = () => ({ oninit: oninit, @@ -1225,7 +1225,7 @@ o.spec("route", function() { route.set("/def") return waitCycles(1).then(function() { throttleMock.fire() - o(oninit.callCount).equals(2) + o(oninit.callCount).equals(1) }) }) }) diff --git a/api/tests/test-routerGetSet.js b/api/tests/test-routerGetSet.js index 0f4ca8f5c..059d20054 100644 --- a/api/tests/test-routerGetSet.js +++ b/api/tests/test-routerGetSet.js @@ -10,6 +10,10 @@ var coreRenderer = require("../../render/render") var apiRouter = require("../../api/router") o.spec("route.get/route.set", function() { + function waitTask() { + return new Promise((resolve) => setTimeout(resolve, 0)) + } + 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() { @@ -71,7 +75,7 @@ o.spec("route.get/route.set", function() { o(route.get()).equals("/ö/é/å?ö=ö#ö=ö") }) - o("sets path asynchronously", function(done) { + o("sets path asynchronously", function() { $window.location.href = prefix + "/a" var spy1 = o.spy() var spy2 = o.spy() @@ -86,16 +90,15 @@ o.spec("route.get/route.set", function() { route.set("/b") o(spy1.callCount).equals(1) o(spy2.callCount).equals(0) - setTimeout(function() { + return waitTask().then(() => { throttleMock.fire() o(spy1.callCount).equals(1) o(spy2.callCount).equals(1) - done() }) }) - o("sets fallback asynchronously", function(done) { + o("sets fallback asynchronously", function() { $window.location.href = prefix + "/b" var spy1 = o.spy() var spy2 = o.spy() @@ -110,22 +113,21 @@ o.spec("route.get/route.set", function() { route.set("/c") o(spy1.callCount).equals(0) o(spy2.callCount).equals(1) - setTimeout(function() { + return waitTask() // Yep, before even the throttle mechanism takes hold. - o(route.get()).equals("/b") - setTimeout(function() { - // Yep, before even the throttle mechanism takes hold. + .then(() => { o(route.get()).equals("/b") }) + // Yep, before even the throttle mechanism takes hold. + .then(() => waitTask()) + .then(() => { 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) { + o("exposes new route asynchronously", function() { $window.location.href = prefix + "/test" route(root, "/test", { "/test": () => ({view: function() {}}), @@ -133,15 +135,14 @@ o.spec("route.get/route.set", function() { }) route.set("/other/x/y/z?c=d#e=f") - setTimeout(function() { + return waitTask().then(() => { // 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) { + o("exposes new escaped unicode route asynchronously", function() { $window.location.href = prefix + "/test" route(root, "/test", { "/test": () => ({view: function() {}}), @@ -149,15 +150,14 @@ o.spec("route.get/route.set", function() { }) route.set(encodeURI("/ö?ö=ö#ö=ö")) - setTimeout(function() { + return waitTask().then(() => { // Yep, before even the throttle mechanism takes hold. o(route.get()).equals("/ö?ö=ö#ö=ö") throttleMock.fire() - done() }) }) - o("exposes new unescaped unicode route asynchronously", function(done) { + o("exposes new unescaped unicode route asynchronously", function() { $window.location.href = "file://" + prefix + "/test" route(root, "/test", { "/test": () => ({view: function() {}}), @@ -165,15 +165,14 @@ o.spec("route.get/route.set", function() { }) route.set("/ö?ö=ö#ö=ö") - setTimeout(function() { + return waitTask().then(() => { // 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) { + o("exposes new route asynchronously on fallback mode", function() { $window.location.href = prefix + "/test" route(root, "/test", { "/test": () => ({view: function() {}}), @@ -181,36 +180,34 @@ o.spec("route.get/route.set", function() { }) route.set("/other/x/y/z?c=d#e=f") - setTimeout(function() { + return waitTask().then(() => { // 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) { + o("sets route via pushState/onpopstate", function() { $window.location.href = prefix + "/test" route(root, "/test", { "/test": () => ({view: function() {}}), "/other/:a/:b...": () => ({view: function() {}}), }) - setTimeout(function() { - $window.history.pushState(null, null, prefix + "/other/x/y/z?c=d#e=f") - $window.onpopstate() - - setTimeout(function() { + return waitTask() + .then(() => { + $window.history.pushState(null, null, prefix + "/other/x/y/z?c=d#e=f") + $window.onpopstate() + }) + .then(() => waitTask()) + .then(() => { // 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) { + o("sets parameterized route", function() { $window.location.href = prefix + "/test" route(root, "/test", { "/test": () => ({view: function() {}}), @@ -218,15 +215,14 @@ o.spec("route.get/route.set", function() { }) route.set("/other/:a/:b", {a: "x", b: "y/z", c: "d", e: "f"}) - setTimeout(function() { + return waitTask().then(() => { // 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) { + o("replace:true works", function() { $window.location.href = prefix + "/test" route(root, "/test", { "/test": () => ({view: function() {}}), @@ -235,15 +231,14 @@ o.spec("route.get/route.set", function() { route.set("/other", null, {replace: true}) - setTimeout(function() { + return waitTask().then(() => { throttleMock.fire() $window.history.back() o($window.location.href).equals(env.protocol + "//" + (env.hostname === "/" ? "" : env.hostname) + "/") - done() }) }) - o("replace:false works", function(done) { + o("replace:false works", function() { $window.location.href = prefix + "/test" route(root, "/test", { "/test": () => ({view: function() {}}), @@ -252,16 +247,15 @@ o.spec("route.get/route.set", function() { route.set("/other", null, {replace: false}) - setTimeout(function() { + return waitTask().then(() => { 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) { + o("state works", function() { $window.location.href = prefix + "/test" route(root, "/test", { "/test": () => ({view: function() {}}), @@ -269,10 +263,9 @@ o.spec("route.get/route.set", function() { }) route.set("/other", null, {state: {a: 1}}) - setTimeout(function() { + return waitTask().then(() => { throttleMock.fire() o($window.history.state).deepEquals({a: 1}) - done() }) }) }) diff --git a/index.js b/index.js index 632668646..d7c62f1b5 100644 --- a/index.js +++ b/index.js @@ -3,11 +3,11 @@ var hyperscript = require("./hyperscript") var request = require("./request") var mountRedraw = require("./mount-redraw") -var domFor = require("./render/domFor") -var m = function m() { return hyperscript.apply(this, arguments) } +var m = (...args) => hyperscript(...args) m.m = hyperscript m.fragment = hyperscript.fragment +m.key = hyperscript.key m.Fragment = "[" m.mount = mountRedraw.mount m.route = require("./route") @@ -20,6 +20,5 @@ m.parsePathname = require("./pathname/parse") m.buildPathname = require("./pathname/build") m.vnode = require("./render/vnode") m.censor = require("./util/censor") -m.domFor = domFor.domFor module.exports = m diff --git a/render/domFor.js b/render/domFor.js deleted file mode 100644 index 16b17a972..000000000 --- a/render/domFor.js +++ /dev/null @@ -1,27 +0,0 @@ -"use strict" - -var delayedRemoval = new WeakMap - -function *domFor(vnode, object = {}) { - // To avoid unintended mangling of the internal bundler, - // parameter destructuring is not used here. - var dom = vnode.dom - var domSize = vnode.domSize - var generation = object.generation - if (dom != null) do { - var nextSibling = dom.nextSibling - - if (delayedRemoval.get(dom) === generation) { - yield dom - domSize-- - } - - dom = nextSibling - } - while (domSize) -} - -module.exports = { - delayedRemoval: delayedRemoval, - domFor: domFor, -} diff --git a/render/hyperscript.js b/render/hyperscript.js index d08192eec..6660b76ec 100644 --- a/render/hyperscript.js +++ b/render/hyperscript.js @@ -62,7 +62,7 @@ function execSelector(selector, attrs, children) { if (hasClassName) attrs.className = null } - return Vnode(state.tag, attrs.key, attrs, children) + return Vnode(state.tag, null, attrs, children) } // Caution is advised when editing this - it's very perf-critical. It's specially designed to avoid @@ -73,9 +73,9 @@ function hyperscript(selector, attrs, ...children) { } if (attrs == null || typeof attrs === "object" && attrs.tag == null && !Array.isArray(attrs)) { - if (children.length === 1 && Array.isArray(children[0])) children = children[0] + if (children.length === 1 && Array.isArray(children[0])) children = children[0].slice() } else { - children = children.length === 0 && Array.isArray(attrs) ? attrs : [attrs, ...children] + children = children.length === 0 && Array.isArray(attrs) ? attrs.slice() : [attrs, ...children] attrs = undefined } @@ -86,11 +86,18 @@ function hyperscript(selector, attrs, ...children) { if (selector !== "[") return execSelector(selector, attrs, children) } - return Vnode(selector, attrs.key, attrs, children) + return Vnode(selector, null, attrs, children) } hyperscript.fragment = function(...args) { return hyperscript("[", ...args) } +hyperscript.key = function(key, ...children) { + if (children.length === 1 && Array.isArray(children[0])) { + children = children[0].slice() + } + return Vnode("=", key, null, Vnode.normalizeChildren(children)) +} + module.exports = hyperscript diff --git a/render/render.js b/render/render.js index 37508ba64..48fc64347 100644 --- a/render/render.js +++ b/render/render.js @@ -1,9 +1,6 @@ "use strict" var Vnode = require("../render/vnode") -var df = require("../render/domFor") -var delayedRemoval = df.delayedRemoval -var domFor = df.domFor module.exports = function() { var xlinkNs = "http://www.w3.org/1999/xlink" @@ -12,8 +9,13 @@ module.exports = function() { math: "http://www.w3.org/1998/Math/MathML" } + // The vnode path is needed for proper removal unblocking. It's not retained past a given + // render and is overwritten on every vnode visit, so callers wanting to retain it should + // always clone the part they're interested in. + var vnodePath + var blockedRemovalRefCount = /*@__PURE__*/new WeakMap() + var removalRequested = /*@__PURE__*/new WeakSet() var currentRedraw - var currentRender function getDocument(dom) { return dom.ownerDocument; @@ -66,6 +68,7 @@ module.exports = function() { if (vnode.attrs != null) initLifecycle(vnode.attrs, vnode, hooks) switch (tag) { case "#": createText(parent, vnode, nextSibling); break + case "=": case "[": createFragment(parent, vnode, hooks, ns, nextSibling); break default: createElement(parent, vnode, hooks, ns, nextSibling) } @@ -83,7 +86,6 @@ module.exports = function() { createNodes(fragment, children, 0, children.length, hooks, null, ns) } vnode.dom = fragment.firstChild - vnode.domSize = fragment.childNodes.length insertDOM(parent, fragment, nextSibling) } function createElement(parent, vnode, hooks, ns, nextSibling) { @@ -129,10 +131,6 @@ module.exports = function() { if (vnode.instance != null) { createNode(parent, vnode.instance, hooks, ns, nextSibling) vnode.dom = vnode.instance.dom - vnode.domSize = vnode.dom != null ? vnode.instance.domSize : 0 - } - else { - vnode.domSize = 0 } } @@ -235,18 +233,18 @@ module.exports = function() { // the old DOM nodes before updateNode runs because it enables us to use the cached `nextSibling` // variable rather than fetching it using `getNextSibling()`. - function updateNodes(parent, old, vnodes, hooks, nextSibling, ns) { + function updateNodes(parent, old, vnodes, hooks, nextSibling, ns, pathDepth) { if (old === vnodes || old == null && vnodes == null) return else if (old == null || old.length === 0) createNodes(parent, vnodes, 0, vnodes.length, hooks, nextSibling, ns) - else if (vnodes == null || vnodes.length === 0) removeNodes(parent, old, 0, old.length) + else if (vnodes == null || vnodes.length === 0) removeNodes(parent, old, 0, old.length, pathDepth, false) else { - var isOldKeyed = old[0] != null && old[0].key != null - var isKeyed = vnodes[0] != null && vnodes[0].key != null + var isOldKeyed = old[0] != null && old[0].tag === "=" + var isKeyed = vnodes[0] != null && vnodes[0].tag === "=" var start = 0, oldStart = 0 if (!isOldKeyed) while (oldStart < old.length && old[oldStart] == null) oldStart++ if (!isKeyed) while (start < vnodes.length && vnodes[start] == null) start++ if (isOldKeyed !== isKeyed) { - removeNodes(parent, old, oldStart, old.length) + removeNodes(parent, old, oldStart, old.length, pathDepth, false) createNodes(parent, vnodes, start, vnodes.length, hooks, nextSibling, ns) } else if (!isKeyed) { // Don't index past the end of either list (causes deopts). @@ -260,10 +258,10 @@ module.exports = function() { v = vnodes[start] if (o === v || o == null && v == null) continue else if (o == null) createNode(parent, v, hooks, ns, getNextSibling(old, start + 1, nextSibling)) - else if (v == null) removeNode(parent, o) - else updateNode(parent, o, v, hooks, getNextSibling(old, start + 1, nextSibling), ns) + else if (v == null) removeNode(parent, o, pathDepth, false) + else updateNode(parent, o, v, hooks, getNextSibling(old, start + 1, nextSibling), ns, pathDepth) } - if (old.length > commonLength) removeNodes(parent, old, start, old.length) + if (old.length > commonLength) removeNodes(parent, old, start, old.length, pathDepth, false) if (vnodes.length > commonLength) createNodes(parent, vnodes, start, vnodes.length, hooks, nextSibling, ns) } else { // keyed diff @@ -274,7 +272,7 @@ module.exports = function() { oe = old[oldEnd] ve = vnodes[end] if (oe.key !== ve.key) break - if (oe !== ve) updateNode(parent, oe, ve, hooks, nextSibling, ns) + if (oe !== ve) updateNode(parent, oe, ve, hooks, nextSibling, ns, pathDepth) if (ve.dom != null) nextSibling = ve.dom oldEnd--, end-- } @@ -284,7 +282,7 @@ module.exports = function() { v = vnodes[start] if (o.key !== v.key) break oldStart++, start++ - if (o !== v) updateNode(parent, o, v, hooks, getNextSibling(old, oldStart, nextSibling), ns) + if (o !== v) updateNode(parent, o, v, hooks, getNextSibling(old, oldStart, nextSibling), ns, pathDepth) } // swaps and list reversals while (oldEnd >= oldStart && end >= start) { @@ -292,9 +290,9 @@ module.exports = function() { if (o.key !== ve.key || oe.key !== v.key) break topSibling = getNextSibling(old, oldStart, nextSibling) moveDOM(parent, oe, topSibling) - if (oe !== v) updateNode(parent, oe, v, hooks, topSibling, ns) + if (oe !== v) updateNode(parent, oe, v, hooks, topSibling, ns, pathDepth) if (++start <= --end) moveDOM(parent, o, nextSibling) - if (o !== ve) updateNode(parent, o, ve, hooks, nextSibling, ns) + if (o !== ve) updateNode(parent, o, ve, hooks, nextSibling, ns, pathDepth) if (ve.dom != null) nextSibling = ve.dom oldStart++; oldEnd-- oe = old[oldEnd] @@ -305,13 +303,13 @@ module.exports = function() { // bottom up once again while (oldEnd >= oldStart && end >= start) { if (oe.key !== ve.key) break - if (oe !== ve) updateNode(parent, oe, ve, hooks, nextSibling, ns) + if (oe !== ve) updateNode(parent, oe, ve, hooks, nextSibling, ns, pathDepth) if (ve.dom != null) nextSibling = ve.dom oldEnd--, end-- oe = old[oldEnd] ve = vnodes[end] } - if (start > end) removeNodes(parent, old, oldStart, oldEnd + 1) + if (start > end) removeNodes(parent, old, oldStart, oldEnd + 1, pathDepth, false) else if (oldStart > oldEnd) createNodes(parent, vnodes, start, end + 1, hooks, nextSibling, ns) else { // inspired by ivi https://github.com/ivijs/ivi/ by Boris Kaul @@ -326,13 +324,13 @@ module.exports = function() { oldIndices[i-start] = oldIndex oe = old[oldIndex] old[oldIndex] = null - if (oe !== ve) updateNode(parent, oe, ve, hooks, nextSibling, ns) + if (oe !== ve) updateNode(parent, oe, ve, hooks, nextSibling, ns, pathDepth) if (ve.dom != null) nextSibling = ve.dom matched++ } } nextSibling = originalNextSibling - if (matched !== oldEnd - oldStart + 1) removeNodes(parent, old, oldStart, oldEnd + 1) + if (matched !== oldEnd - oldStart + 1) removeNodes(parent, old, oldStart, oldEnd + 1, pathDepth, false) if (matched === 0) createNodes(parent, vnodes, start, end + 1, hooks, nextSibling, ns) else { if (pos === -1) { @@ -361,26 +359,29 @@ module.exports = function() { } } } - function updateNode(parent, old, vnode, hooks, nextSibling, ns) { + function updateNode(parent, old, vnode, hooks, nextSibling, ns, pathDepth) { var oldTag = old.tag, tag = vnode.tag if (oldTag === tag) { vnode.state = old.state vnode.events = old.events if (shouldNotUpdate(vnode, old)) return + vnodePath[pathDepth++] = parent + vnodePath[pathDepth++] = vnode if (typeof oldTag === "string") { if (vnode.attrs != null) { updateLifecycle(vnode.attrs, vnode, hooks) } switch (oldTag) { case "#": updateText(old, vnode); break - case "[": updateFragment(parent, old, vnode, hooks, nextSibling, ns); break - default: updateElement(old, vnode, hooks, ns) + case "=": + case "[": updateFragment(parent, old, vnode, hooks, nextSibling, ns, pathDepth); break + default: updateElement(old, vnode, hooks, ns, pathDepth) } } - else updateComponent(parent, old, vnode, hooks, nextSibling, ns) + else updateComponent(parent, old, vnode, hooks, nextSibling, ns, pathDepth) } else { - removeNode(parent, old) + removeNode(parent, old, pathDepth, false) createNode(parent, vnode, hooks, ns, nextSibling) } } @@ -390,49 +391,42 @@ module.exports = function() { } vnode.dom = old.dom } - function updateFragment(parent, old, vnode, hooks, nextSibling, ns) { - updateNodes(parent, old.children, vnode.children, hooks, nextSibling, ns) - var domSize = 0, children = vnode.children + function updateFragment(parent, old, vnode, hooks, nextSibling, ns, pathDepth) { + updateNodes(parent, old.children, vnode.children, hooks, nextSibling, ns, pathDepth) vnode.dom = null - if (children != null) { - for (var i = 0; i < children.length; i++) { - var child = children[i] + if (vnode.children != null) { + for (var child of vnode.children) { if (child != null && child.dom != null) { if (vnode.dom == null) vnode.dom = child.dom - domSize += child.domSize || 1 } } - if (domSize !== 1) vnode.domSize = domSize } } - function updateElement(old, vnode, hooks, ns) { + function updateElement(old, vnode, hooks, ns, pathDepth) { var element = vnode.dom = old.dom ns = getNameSpace(vnode) || ns updateAttrs(vnode, old.attrs, vnode.attrs, ns) if (!maybeSetContentEditable(vnode)) { - updateNodes(element, old.children, vnode.children, hooks, null, ns) + updateNodes(element, old.children, vnode.children, hooks, null, ns, pathDepth) } } - function updateComponent(parent, old, vnode, hooks, nextSibling, ns) { + function updateComponent(parent, old, vnode, hooks, nextSibling, ns, pathDepth) { vnode.instance = Vnode.normalize(callHook.call(vnode.state.view, vnode)) if (vnode.instance === vnode) throw Error("A view cannot return the vnode it received as argument") updateLifecycle(vnode.state, vnode, hooks) if (vnode.attrs != null) updateLifecycle(vnode.attrs, vnode, hooks) if (vnode.instance != null) { if (old.instance == null) createNode(parent, vnode.instance, hooks, ns, nextSibling) - else updateNode(parent, old.instance, vnode.instance, hooks, nextSibling, ns) + else updateNode(parent, old.instance, vnode.instance, hooks, nextSibling, ns, pathDepth) vnode.dom = vnode.instance.dom - vnode.domSize = vnode.instance.domSize } else if (old.instance != null) { - removeNode(parent, old.instance) + removeNode(parent, old.instance, pathDepth, false) vnode.dom = undefined - vnode.domSize = 0 } else { vnode.dom = old.dom - vnode.domSize = old.domSize } } function getKeyMap(vnodes, start, end) { @@ -440,8 +434,7 @@ module.exports = function() { for (; start < end; start++) { var vnode = vnodes[start] if (vnode != null) { - var key = vnode.key - if (key != null) map[key] = start + map[vnode.key] = start } } return map @@ -502,16 +495,18 @@ module.exports = function() { // This handles fragments with zombie children (removed from vdom, but persisted in DOM through onbeforeremove) function moveDOM(parent, vnode, nextSibling) { - if (vnode.dom != null) { - var target - if (vnode.domSize == null) { - // don't allocate for the common case - target = vnode.dom - } else { - target = getDocument(parent).createDocumentFragment() - for (var dom of domFor(vnode)) target.appendChild(dom) + if (typeof vnode.tag === "function") { + return moveDOM(parent, vnode.instance, nextSibling) + } else if (vnode.tag === "[" || vnode.tag === "=") { + if (Array.isArray(vnode.children)) { + for (var child of vnode.children) { + nextSibling = moveDOM(parent, child, nextSibling) + } } - insertDOM(parent, target, nextSibling) + return nextSibling + } else { + insertDOM(parent, vnode.dom, nextSibling) + return vnode.dom } } @@ -531,94 +526,142 @@ module.exports = function() { } //remove - function removeNodes(parent, vnodes, start, end) { - for (var i = start; i < end; i++) { - var vnode = vnodes[i] - if (vnode != null) removeNode(parent, vnode) + function invokeBeforeRemove(vnode, host) { + try { + if (typeof host.onbeforeremove === "function") { + var result = callHook.call(host.onbeforeremove, vnode) + if (result != null && typeof result.then === "function") return Promise.resolve(result) + } + } catch (e) { + // Errors during removal aren't fatal. Just log them. + console.error(e) } } - function removeNode(parent, vnode) { - var mask = 0 - var original = vnode.state - var stateResult, attrsResult - if (typeof vnode.tag !== "string" && typeof vnode.state.onbeforeremove === "function") { - var result = callHook.call(vnode.state.onbeforeremove, vnode) - if (result != null && typeof result.then === "function") { - mask = 1 - stateResult = result + function tryProcessRemoval(parent, vnode) { + // eslint-disable-next-line no-bitwise + var refCount = blockedRemovalRefCount.get(vnode) | 0 + if (refCount > 1) { + blockedRemovalRefCount.set(vnode, refCount - 1) + return false + } + + if (typeof vnode.tag !== "function" && vnode.tag !== "[" && vnode.tag !== "=") { + parent.removeChild(vnode.dom) + } + + try { + if (typeof vnode.tag !== "string" && typeof vnode.state.onremove === "function") { + callHook.call(vnode.state.onremove, vnode) + } + } catch (e) { + console.error(e) + } + + try { + if (vnode.attrs && typeof vnode.attrs.onremove === "function") { + callHook.call(vnode.attrs.onremove, vnode) } + } catch (e) { + console.error(e) } - if (vnode.attrs && typeof vnode.attrs.onbeforeremove === "function") { - var result = callHook.call(vnode.attrs.onbeforeremove, vnode) - if (result != null && typeof result.then === "function") { + + return true + } + function removeNodeAsyncRecurse(parent, vnode) { + while (vnode != null) { + // Delay the actual subtree removal if there's still pending `onbeforeremove` hooks on + // this node or a child node. + if (!tryProcessRemoval(parent, vnode)) return false + if (typeof vnode.tag !== "function") { + if (vnode.tag === "#") break + if (vnode.tag !== "[" && vnode.tag !== "=") parent = vnode.dom + // Using bitwise ops and `Array.prototype.reduce` to reduce code size. It's not + // called nearly enough to merit further optimization. // eslint-disable-next-line no-bitwise - mask |= 2 - attrsResult = result + return vnode.children.reduce((fail, child) => fail & removeNodeAsyncRecurse(parent, child), 1) } + vnode = vnode.instance } - checkState(vnode, original) - var generation - // If we can, try to fast-path it and avoid all the overhead of awaiting - if (!mask) { - onremove(vnode) - removeDOM(parent, vnode, generation) - } else { - generation = currentRender - for (var dom of domFor(vnode)) delayedRemoval.set(dom, generation) - if (stateResult != null) { - stateResult.finally(function () { - // eslint-disable-next-line no-bitwise - if (mask & 1) { - // eslint-disable-next-line no-bitwise - mask &= 2 - if (!mask) { - checkState(vnode, original) - onremove(vnode) - removeDOM(parent, vnode, generation) + + return true + } + function removeNodes(parent, vnodes, start, end, pathDepth, isDelayed) { + // Using bitwise ops to reduce code size. + var fail = 0 + // eslint-disable-next-line no-bitwise + for (var i = start; i < end; i++) fail |= !removeNode(parent, vnodes[i], pathDepth, isDelayed) + return !fail + } + function removeNode(parent, vnode, pathDepth, isDelayed) { + if (vnode != null) { + delayed: { + var attrsResult, stateResult + + // Block removes, but do call nested `onbeforeremove`. + if (typeof vnode.tag !== "string") attrsResult = invokeBeforeRemove(vnode, vnode.state) + if (vnode.attrs != null) stateResult = invokeBeforeRemove(vnode, vnode.attrs) + + vnodePath[pathDepth++] = parent + vnodePath[pathDepth++] = vnode + + if (attrsResult || stateResult) { + var path = vnodePath.slice(0, pathDepth) + var settle = () => { + + // Remove the innermost node recursively and try to remove the parents + // non-recursively. + // If it's still delayed, skip. If this node is delayed, all its ancestors are + // also necessarily delayed, and so they should be skipped. + var i = path.length - 2 + if (removeNodeAsyncRecurse(path[i], path[i + 1])) { + while ((i -= 2) >= 0 && removalRequested.has(path[i + 1])) { + tryProcessRemoval(path[i], path[i + 1]) + } } } - }) - } - if (attrsResult != null) { - attrsResult.finally(function () { - // eslint-disable-next-line no-bitwise - if (mask & 2) { + var increment = 0 + + if (attrsResult) { + attrsResult.catch(console.error) + attrsResult.then(settle, settle) + increment++ + } + + if (stateResult) { + stateResult.catch(console.error) + stateResult.then(settle, settle) + increment++ + } + + isDelayed = true + + for (var i = 1; i < pathDepth; i += 2) { // eslint-disable-next-line no-bitwise - mask &= 1 - if (!mask) { - checkState(vnode, original) - onremove(vnode) - removeDOM(parent, vnode, generation) - } + blockedRemovalRefCount.set(vnodePath[i], (blockedRemovalRefCount.get(vnodePath[i]) | 0) + increment) } - }) - } - } - } - function removeDOM(parent, vnode, generation) { - if (vnode.dom == null) return - if (vnode.domSize == null) { - // don't allocate for the common case - if (delayedRemoval.get(vnode.dom) === generation) parent.removeChild(vnode.dom) - } else { - for (var dom of domFor(vnode, {generation})) parent.removeChild(dom) - } - } + } - function onremove(vnode) { - if (typeof vnode.tag !== "string" && typeof vnode.state.onremove === "function") callHook.call(vnode.state.onremove, vnode) - if (vnode.attrs && typeof vnode.attrs.onremove === "function") callHook.call(vnode.attrs.onremove, vnode) - if (typeof vnode.tag !== "string") { - if (vnode.instance != null) onremove(vnode.instance) - } else { - var children = vnode.children - if (Array.isArray(children)) { - for (var i = 0; i < children.length; i++) { - var child = children[i] - if (child != null) onremove(child) + if (typeof vnode.tag === "function") { + if (vnode.instance != null && !removeNode(parent, vnode.instance, pathDepth, isDelayed)) break delayed + } else if (vnode.tag !== "#") { + if (!removeNodes( + vnode.tag !== "[" && vnode.tag !== "=" ? vnode.dom : parent, + vnode.children, 0, vnode.children.length, pathDepth, isDelayed + )) break delayed } + + // Don't call removal hooks if removal is delayed. + // Delay the actual subtree removal if there's still pending `onbeforeremove` hooks on + // this node or a child node. + if (isDelayed || tryProcessRemoval(parent, vnode)) break delayed + + return false } + + removalRequested.add(vnode) } + + return true } //attrs @@ -844,7 +887,6 @@ module.exports = function() { return false } while (false); // eslint-disable-line no-constant-condition vnode.dom = old.dom - vnode.domSize = old.domSize vnode.instance = old.instance // One would think having the actual latest attributes would be ideal, // but it doesn't let us properly diff based on our current internal @@ -870,15 +912,16 @@ module.exports = function() { var hooks = [] var active = activeElement(dom) var namespace = dom.namespaceURI + var prevPath = vnodePath currentDOM = dom currentRedraw = typeof redraw === "function" ? redraw : undefined - currentRender = {} + vnodePath = [] try { // First time rendering into a node clears it out if (dom.vnodes == null) dom.textContent = "" vnodes = Vnode.normalizeChildren(Array.isArray(vnodes) ? vnodes : [vnodes]) - updateNodes(dom, dom.vnodes, vnodes, hooks, null, namespace === "http://www.w3.org/1999/xhtml" ? undefined : namespace) + updateNodes(dom, dom.vnodes, vnodes, hooks, null, namespace === "http://www.w3.org/1999/xhtml" ? undefined : namespace, 0) dom.vnodes = vnodes // `document.activeElement` can return null: https://html.spec.whatwg.org/multipage/interaction.html#dom-document-activeelement if (active != null && activeElement(dom) !== active && typeof active.focus === "function") active.focus() @@ -886,6 +929,7 @@ module.exports = function() { } finally { currentRedraw = prevRedraw currentDOM = prevDOM + vnodePath = prevPath } } } diff --git a/render/tests/test-component.js b/render/tests/test-component.js index 91a8094ab..5a9c19e28 100644 --- a/render/tests/test-component.js +++ b/render/tests/test-component.js @@ -11,7 +11,6 @@ o.spec("component", function() { o.beforeEach(function() { $window = domMock() root = $window.document.createElement("div") - render = vdom($window) }) @@ -130,9 +129,9 @@ o.spec("component", function() { return m("div") } }) - var div = m("div", {key: 2}) - render(root, [m(component, {key: 1}), div]) - render(root, div) + var div = m("div") + render(root, [m.key(1, m(component)), m.key(2, div)]) + render(root, [m.key(2, div)]) o(root.childNodes.length).equals(1) o(root.firstChild).equals(div.dom) @@ -354,10 +353,10 @@ o.spec("component", function() { ] } }) - var div = m("div", {key: 2}) - render(root, [m(component, {key: 1}), div]) + var div = m("div") + render(root, [m.key(1, m(component)), m.key(2, div)]) - render(root, [m("div", {key: 2})]) + render(root, [m.key(2, m("div"))]) o(root.childNodes.length).equals(1) o(root.firstChild).equals(div.dom) @@ -368,10 +367,10 @@ o.spec("component", function() { return "a" } }) - var div = m("div", {key: 2}) - render(root, [m(component, {key: 1}), div]) + var div = m("div") + render(root, [m.key(1, m(component)), m.key(2, div)]) - render(root, [m("div", {key: 2})]) + render(root, [m.key(2, m("div"))]) o(root.childNodes.length).equals(1) o(root.firstChild).equals(div.dom) @@ -569,15 +568,12 @@ o.spec("component", function() { o(root.firstChild.firstChild.nodeValue).equals("b") }) o("calls onremove", function() { - var called = 0 + var rootCountInCall + var onremove = o.spy(() => { + rootCountInCall = root.childNodes.length + }) var component = createComponent({ - onremove: function(vnode) { - called++ - - o(vnode.dom).notEquals(undefined) - o(vnode.dom).equals(root.firstChild) - o(root.childNodes.length).equals(1) - }, + onremove, view: function() { return m("div", {id: "a"}, "b") } @@ -585,23 +581,24 @@ o.spec("component", function() { render(root, m(component)) - o(called).equals(0) + o(onremove.callCount).equals(0) + o(root.childNodes.length).equals(1) + var firstChild = root.firstChild render(root, []) - o(called).equals(1) + o(onremove.callCount).equals(1) + o(onremove.args[0].dom).equals(firstChild) + o(rootCountInCall).equals(0) o(root.childNodes.length).equals(0) }) o("calls onremove when returning fragment", function() { - var called = 0 + var rootCountInCall + var onremove = o.spy(() => { + rootCountInCall = root.childNodes.length + }) var component = createComponent({ - onremove: function(vnode) { - called++ - - o(vnode.dom).notEquals(undefined) - o(vnode.dom).equals(root.firstChild) - o(root.childNodes.length).equals(1) - }, + onremove, view: function() { return [m("div", {id: "a"}, "b")] } @@ -609,23 +606,24 @@ o.spec("component", function() { render(root, m(component)) - o(called).equals(0) + o(onremove.callCount).equals(0) + o(root.childNodes.length).equals(1) + var firstChild = root.firstChild render(root, []) - o(called).equals(1) + o(onremove.callCount).equals(1) + o(onremove.args[0].dom).equals(firstChild) + o(rootCountInCall).equals(0) o(root.childNodes.length).equals(0) }) o("calls onbeforeremove", function() { - var called = 0 + var rootCountInCall + var onbeforeremove = o.spy(() => { + rootCountInCall = root.childNodes.length + }) var component = createComponent({ - onbeforeremove: function(vnode) { - called++ - - o(vnode.dom).notEquals(undefined) - o(vnode.dom).equals(root.firstChild) - o(root.childNodes.length).equals(1) - }, + onbeforeremove, view: function() { return m("div", {id: "a"}, "b") } @@ -633,11 +631,15 @@ o.spec("component", function() { render(root, m(component)) - o(called).equals(0) + o(onbeforeremove.callCount).equals(0) + o(root.childNodes.length).equals(1) + var firstChild = root.firstChild render(root, []) - o(called).equals(1) + o(onbeforeremove.callCount).equals(1) + o(onbeforeremove.args[0].dom).equals(firstChild) + o(rootCountInCall).equals(1) o(root.childNodes.length).equals(0) }) o("calls onbeforeremove when returning fragment", function() { @@ -665,20 +667,20 @@ o.spec("component", function() { o(root.childNodes.length).equals(0) }) o("does not recycle when there's an onupdate", function() { + var view = o.spy(() => m("div")) var component = createComponent({ onupdate: function() {}, - view: function() { - return m("div") - } + view, }) - var vnode = m(component, {key: 1}) - var updated = m(component, {key: 1}) + var vnode = m(component) + var updated = m(component) - render(root, vnode) + render(root, [m.key(1, vnode)]) render(root, []) - render(root, updated) + render(root, [m.key(1, updated)]) - o(vnode.dom).notEquals(updated.dom) + o(view.calls[0].this).notEquals(view.calls[1].this) + o(view.calls[0].args[0].dom).notEquals(view.calls[1].args[0].dom) }) o("lifecycle timing megatest (for a single component)", function() { var methods = { @@ -824,11 +826,11 @@ o.spec("component", function() { }) var component = createComponent({view: view}) - render(root, [m("div", m(component, {key: 1}))]) + render(root, [m("div", m.key(1, m(component)))]) var child = root.firstChild.firstChild render(root, []) step = 1 - render(root, [m("div", m(component, {key: 1}))]) + render(root, [m("div", m.key(1, m(component)))]) o(child).notEquals(root.firstChild.firstChild) // this used to be a recycling pool test o(view.callCount).equals(2) diff --git a/render/tests/test-createFragment.js b/render/tests/test-createFragment.js index e48f55db6..69ef80aaa 100644 --- a/render/tests/test-createFragment.js +++ b/render/tests/test-createFragment.js @@ -18,35 +18,38 @@ o.spec("createFragment", function() { var vnode = fragment(m("a")) render(root, vnode) - o(vnode.dom.nodeName).equals("A") + o(root.childNodes.length).equals(1) + o(root.childNodes[0].nodeName).equals("A") }) o("handles empty fragment", function() { var vnode = fragment() render(root, vnode) o(vnode.dom).equals(null) - o(vnode.domSize).equals(0) + o(root.childNodes.length).equals(0) }) o("handles childless fragment", function() { var vnode = fragment() render(root, vnode) o(vnode.dom).equals(null) - o(vnode.domSize).equals(0) + o(root.childNodes.length).equals(0) }) o("handles multiple children", function() { var vnode = fragment(m("a"), m("b")) render(root, vnode) - o(vnode.domSize).equals(2) - o(vnode.dom.nodeName).equals("A") - o(vnode.dom.nextSibling.nodeName).equals("B") + o(root.childNodes.length).equals(2) + o(root.childNodes[0].nodeName).equals("A") + o(root.childNodes[1].nodeName).equals("B") + o(vnode.dom).equals(root.childNodes[0]) }) o("handles td", function() { var vnode = fragment(m("td")) render(root, vnode) - o(vnode.dom).notEquals(null) - o(vnode.dom.nodeName).equals("TD") + o(root.childNodes.length).equals(1) + o(root.childNodes[0].nodeName).equals("TD") + o(vnode.dom).equals(root.childNodes[0]) }) }) diff --git a/render/tests/test-domFor.js b/render/tests/test-domFor.js deleted file mode 100644 index 7321a93e9..000000000 --- a/render/tests/test-domFor.js +++ /dev/null @@ -1,178 +0,0 @@ -"use strict" - -const o = require("ospec") -const components = require("../../test-utils/components") -const domMock = require("../../test-utils/domMock") -const vdom = require("../render") -const m = require("../hyperscript") -const fragment = require("../../render/hyperscript").fragment -const domFor = require("../../render/domFor").domFor - -o.spec("domFor(vnode)", function() { - let $window, root, render - o.beforeEach(function() { - $window = domMock() - root = $window.document.createElement("div") - render = vdom($window) - }) - o("works for simple vnodes", function() { - render(root, m("div", {oncreate(vnode){ - let n = 0 - for (const dom of domFor(vnode)) { - o(dom).equals(root.firstChild) - o(++n).equals(1) - } - }})) - }) - o("works for fragments", function () { - render(root, fragment({ - oncreate(vnode){ - let n = 0 - for (const dom of domFor(vnode)) { - o(dom).equals(root.childNodes[n]) - n++ - } - o(n).equals(2) - } - }, [ - m("a"), - m("b") - ])) - }) - o("works in fragments with children that have delayed removal", function() { - function oncreate(vnode){ - o(root.childNodes.length).equals(3) - o(root.childNodes[0].nodeName).equals("A") - o(root.childNodes[1].nodeName).equals("B") - o(root.childNodes[2].nodeName).equals("C") - - const iter = domFor(vnode) - o(iter.next()).deepEquals({done:false, value: root.childNodes[0]}) - o(iter.next()).deepEquals({done:false, value: root.childNodes[1]}) - o(iter.next()).deepEquals({done:false, value: root.childNodes[2]}) - o(iter.next().done).deepEquals(true) - o(root.childNodes.length).equals(3) - } - function onupdate(vnode) { - // the b node is still present in the DOM - o(root.childNodes.length).equals(3) - o(root.childNodes[0].nodeName).equals("A") - o(root.childNodes[1].nodeName).equals("B") - o(root.childNodes[2].nodeName).equals("C") - - const iter = domFor(vnode) - o(iter.next()).deepEquals({done:false, value: root.childNodes[0]}) - o(iter.next()).deepEquals({done:false, value: root.childNodes[2]}) - o(iter.next().done).deepEquals(true) - o(root.childNodes.length).equals(3) - } - - render(root, fragment( - {oncreate, onupdate}, - [ - m("a"), - m("b", {onbeforeremove(){return {then(){}, finally(){}}}}), - m("c") - ] - )) - render(root, fragment( - {oncreate, onupdate}, - [ - m("a"), - null, - m("c"), - ] - )) - - }) - components.forEach(function(cmp){ - const {kind, create: createComponent} = cmp - o.spec(kind, function(){ - o("works for components that return one element", function() { - const C = createComponent({ - view(){return m("div")}, - oncreate(vnode){ - let n = 0 - for (const dom of domFor(vnode)) { - o(dom).equals(root.firstChild) - o(++n).equals(1) - } - } - }) - render(root, m(C)) - }) - o("works for components that return fragments", function () { - const oncreate = o.spy(function oncreate(vnode){ - o(root.childNodes.length).equals(3) - o(root.childNodes[0].nodeName).equals("A") - o(root.childNodes[1].nodeName).equals("B") - o(root.childNodes[2].nodeName).equals("C") - - const iter = domFor(vnode) - o(iter.next()).deepEquals({done:false, value: root.childNodes[0]}) - o(iter.next()).deepEquals({done:false, value: root.childNodes[1]}) - o(iter.next()).deepEquals({done:false, value: root.childNodes[2]}) - o(iter.next().done).deepEquals(true) - o(root.childNodes.length).equals(3) - }) - const C = createComponent({ - view({children}){return children}, - oncreate - }) - render(root, m(C, [ - m("a"), - m("b"), - m("c") - ])) - o(oncreate.callCount).equals(1) - }) - o("works for components that return fragments with delayed removal", function () { - const onbeforeremove = o.spy(function onbeforeremove(){return {then(){}, finally(){}}}) - const oncreate = o.spy(function oncreate(vnode){ - o(root.childNodes.length).equals(3) - o(root.childNodes[0].nodeName).equals("A") - o(root.childNodes[1].nodeName).equals("B") - o(root.childNodes[2].nodeName).equals("C") - - const iter = domFor(vnode) - o(iter.next()).deepEquals({done:false, value: root.childNodes[0]}) - o(iter.next()).deepEquals({done:false, value: root.childNodes[1]}) - o(iter.next()).deepEquals({done:false, value: root.childNodes[2]}) - o(iter.next().done).deepEquals(true) - o(root.childNodes.length).equals(3) - }) - const onupdate = o.spy(function onupdate(vnode) { - o(root.childNodes.length).equals(3) - o(root.childNodes[0].nodeName).equals("A") - o(root.childNodes[1].nodeName).equals("B") - o(root.childNodes[2].nodeName).equals("C") - - const iter = domFor(vnode) - - o(iter.next()).deepEquals({done:false, value: root.childNodes[0]}) - o(iter.next()).deepEquals({done:false, value: root.childNodes[2]}) - o(iter.next().done).deepEquals(true) - o(root.childNodes.length).equals(3) - }) - const C = createComponent({ - view({children}){return children}, - oncreate, - onupdate - }) - render(root, m(C, [ - m("a"), - m("b", {onbeforeremove}), - m("c") - ])) - render(root, m(C, [ - m("a"), - null, - m("c") - ])) - o(oncreate.callCount).equals(1) - o(onupdate.callCount).equals(1) - o(onbeforeremove.callCount).equals(1) - }) - }) - }) -}) diff --git a/render/tests/test-fragment.js b/render/tests/test-fragment.js index 8d065f8a7..a2006020c 100644 --- a/render/tests/test-fragment.js +++ b/render/tests/test-fragment.js @@ -3,43 +3,19 @@ var o = require("ospec") var m = require("../../render/hyperscript") -function fragmentStr() { - var args = [].slice.call(arguments); - args.unshift("["); - return m.apply(null, args) -} - function runTest(name, fragment) { o.spec(name, function() { o("works", function() { - var attrs = {foo: 5} var child = m("p") - var frag = fragment(attrs, child) + var frag = fragment(child) o(frag.tag).equals("[") o(Array.isArray(frag.children)).equals(true) o(frag.children.length).equals(1) o(frag.children[0]).equals(child) - - o(frag.attrs).equals(attrs) - - o(frag.key).equals(undefined) }) - o("supports keys", function() { - var attrs = {key: 7} - var frag = fragment(attrs, []) - o(frag.tag).equals("[") - - o(Array.isArray(frag.children)).equals(true) - o(frag.children.length).equals(0) - - o(frag.attrs).equals(attrs) - o(frag.attrs.key).equals(7) - - o(frag.key).equals(7) - }) - o.spec("children with no attrs", function() { + o.spec("children", function() { o("handles string single child", function() { var vnode = fragment(["a"]) @@ -117,80 +93,110 @@ function runTest(name, fragment) { o(vnode.children[0].children).equals("0") }) }) - o.spec("children with attrs", function() { - o("handles string single child", function() { - var vnode = fragment({}, ["a"]) + }) +} - o(vnode.children[0].tag).equals("#") - o(vnode.children[0].children).equals("a") - }) - o("handles falsy string single child", function() { - var vnode = fragment({}, [""]) +runTest("fragment", m.fragment); +runTest("fragment-string-selector", (...children) => m("[", ...children)); - o(vnode.children[0].tag).equals("#") - o(vnode.children[0].children).equals("") - }) - o("handles number single child", function() { - var vnode = fragment({}, [1]) +o.spec("key", function() { + o("works", function() { + var child = m("p") + var frag = m.key(undefined, child) - o(vnode.children[0].tag).equals("#") - o(vnode.children[0].children).equals("1") - }) - o("handles falsy number single child", function() { - var vnode = fragment({}, [0]) + o(frag.tag).equals("=") - o(vnode.children[0].tag).equals("#") - o(vnode.children[0].children).equals("0") - }) - o("handles boolean single child", function() { - var vnode = fragment({}, [true]) + o(Array.isArray(frag.children)).equals(true) + o(frag.children.length).equals(1) + o(frag.children[0]).equals(child) - o(vnode.children).deepEquals([null]) - }) - o("handles falsy boolean single child", function() { - var vnode = fragment({}, [false]) + o(frag.key).equals(undefined) + }) + o("supports non-null keys", function() { + var frag = m.key(7, []) + o(frag.tag).equals("=") - o(vnode.children).deepEquals([null]) - }) - o("handles null single child", function() { - var vnode = fragment({}, [null]) + o(Array.isArray(frag.children)).equals(true) + o(frag.children.length).equals(0) - o(vnode.children).deepEquals([null]) - }) - o("handles undefined single child", function() { - var vnode = fragment({}, [undefined]) + o(frag.key).equals(7) + }) + o.spec("children", function() { + o("handles string single child", function() { + var vnode = m.key("foo", ["a"]) - o(vnode.children).deepEquals([null]) - }) - o("handles multiple string children", function() { - var vnode = fragment({}, ["", "a"]) + o(vnode.children[0].tag).equals("#") + o(vnode.children[0].children).equals("a") + }) + o("handles falsy string single child", function() { + var vnode = m.key("foo", [""]) - o(vnode.children[0].tag).equals("#") - o(vnode.children[0].children).equals("") - o(vnode.children[1].tag).equals("#") - o(vnode.children[1].children).equals("a") - }) - o("handles multiple number children", function() { - var vnode = fragment({}, [0, 1]) + o(vnode.children[0].tag).equals("#") + o(vnode.children[0].children).equals("") + }) + o("handles number single child", function() { + var vnode = m.key("foo", [1]) - o(vnode.children[0].tag).equals("#") - o(vnode.children[0].children).equals("0") - o(vnode.children[1].tag).equals("#") - o(vnode.children[1].children).equals("1") - }) - o("handles multiple boolean children", function() { - var vnode = fragment({}, [false, true]) + o(vnode.children[0].tag).equals("#") + o(vnode.children[0].children).equals("1") + }) + o("handles falsy number single child", function() { + var vnode = m.key("foo", [0]) - o(vnode.children).deepEquals([null, null]) - }) - o("handles multiple null/undefined child", function() { - var vnode = fragment({}, [null, undefined]) + o(vnode.children[0].tag).equals("#") + o(vnode.children[0].children).equals("0") + }) + o("handles boolean single child", function() { + var vnode = m.key("foo", [true]) - o(vnode.children).deepEquals([null, null]) - }) + o(vnode.children).deepEquals([null]) }) - }) -} + o("handles falsy boolean single child", function() { + var vnode = m.key("foo", [false]) -runTest("fragment", m.fragment); -runTest("fragment-string-selector", fragmentStr); + o(vnode.children).deepEquals([null]) + }) + o("handles null single child", function() { + var vnode = m.key("foo", [null]) + + o(vnode.children[0]).equals(null) + }) + o("handles undefined single child", function() { + var vnode = m.key("foo", [undefined]) + + o(vnode.children).deepEquals([null]) + }) + o("handles multiple string children", function() { + var vnode = m.key("foo", ["", "a"]) + + o(vnode.children[0].tag).equals("#") + o(vnode.children[0].children).equals("") + o(vnode.children[1].tag).equals("#") + o(vnode.children[1].children).equals("a") + }) + o("handles multiple number children", function() { + var vnode = m.key("foo", [0, 1]) + + o(vnode.children[0].tag).equals("#") + o(vnode.children[0].children).equals("0") + o(vnode.children[1].tag).equals("#") + o(vnode.children[1].children).equals("1") + }) + o("handles multiple boolean children", function() { + var vnode = m.key("foo", [false, true]) + + o(vnode.children).deepEquals([null, null]) + }) + o("handles multiple null/undefined child", function() { + var vnode = m.key("foo", [null, undefined]) + + o(vnode.children).deepEquals([null, null]) + }) + o("handles falsy number single child without attrs", function() { + var vnode = m.key("foo", 0) + + o(vnode.children[0].tag).equals("#") + o(vnode.children[0].children).equals("0") + }) + }) +}) diff --git a/render/tests/test-hyperscript.js b/render/tests/test-hyperscript.js index 464a20cb0..5de0fd0b0 100644 --- a/render/tests/test-hyperscript.js +++ b/render/tests/test-hyperscript.js @@ -268,11 +268,10 @@ o.spec("hyperscript", function() { o(vnode.attrs.a).equals(false) }) o("handles only key in attrs", function() { - var vnode = m("div", {key:"a"}) + var vnode = m("div", {key: "a"}) o(vnode.tag).equals("div") - o(vnode.attrs).deepEquals({key:"a"}) - o(vnode.key).equals("a") + o(vnode.attrs).deepEquals({key: "a"}) }) o("handles many attrs", function() { var vnode = m("div", {a: "b", c: "d"}) @@ -344,7 +343,6 @@ o.spec("hyperscript", function() { o(vnode.tag).equals("custom-element") o(vnode.attrs).deepEquals({key:"a"}) - o(vnode.key).equals("a") }) o("handles many attrs", function() { var vnode = m("custom-element", {a: "b", c: "d"}) diff --git a/render/tests/test-input.js b/render/tests/test-input.js index 214b32783..89b4bfa46 100644 --- a/render/tests/test-input.js +++ b/render/tests/test-input.js @@ -20,13 +20,13 @@ o.spec("form inputs", function() { o.spec("input", function() { o("maintains focus after move", function() { - var input = m("input", {key: 1}) - var a = m("a", {key: 2}) - var b = m("b", {key: 3}) + var input = m("input") + var a = m("a") + var b = m("b") - render(root, [input, a, b]) + render(root, [m.key(1, input), m.key(2, a), m.key(3, b)]) input.dom.focus() - render(root, [a, input, b]) + render(root, [m.key(2, a), m.key(1, input), m.key(3, b)]) o($window.document.activeElement).equals(input.dom) }) diff --git a/render/tests/test-normalizeChildren.js b/render/tests/test-normalizeChildren.js index a7cd5ba5b..8afe9c7ce 100644 --- a/render/tests/test-normalizeChildren.js +++ b/render/tests/test-normalizeChildren.js @@ -2,6 +2,7 @@ var o = require("ospec") var Vnode = require("../../render/vnode") +var m = require("../../render/hyperscript") o.spec("normalizeChildren", function() { o("normalizes arrays into fragments", function() { @@ -23,33 +24,33 @@ o.spec("normalizeChildren", function() { }) o("allows all keys", function() { var children = Vnode.normalizeChildren([ - {key: 1}, - {key: 2}, + m.key(1), + m.key(2), ]) - o(children).deepEquals([{key: 1}, {key: 2}]) + o(children).deepEquals([m.key(1), m.key(2)]) }) o("allows no keys", function() { var children = Vnode.normalizeChildren([ - {data: 1}, - {data: 2}, + m("foo1"), + m("foo2"), ]) - o(children).deepEquals([{data: 1}, {data: 2}]) + o(children).deepEquals([m("foo1"), m("foo2")]) }) o("disallows mixed keys, starting with key", function() { o(function() { Vnode.normalizeChildren([ - {key: 1}, - {data: 2}, + m.key(1), + m("foo2"), ]) }).throws(TypeError) }) o("disallows mixed keys, starting with no key", function() { o(function() { Vnode.normalizeChildren([ - {data: 1}, - {key: 2}, + m("foo1"), + m.key(2), ]) }).throws(TypeError) }) diff --git a/render/tests/test-onbeforeremove.js b/render/tests/test-onbeforeremove.js index 33ab69398..521cd6213 100644 --- a/render/tests/test-onbeforeremove.js +++ b/render/tests/test-onbeforeremove.js @@ -1,7 +1,6 @@ "use strict" var o = require("ospec") -var callAsync = require("../../test-utils/callAsync") var components = require("../../test-utils/components") var domMock = require("../../test-utils/domMock") var vdom = require("../../render/render") @@ -35,64 +34,95 @@ o.spec("onbeforeremove", function() { o(create.callCount).equals(0) o(update.callCount).equals(0) }) - o("calls onbeforeremove when removing element", function(done) { - var vnode = m("div", {onbeforeremove: remove}) + o("calls onbeforeremove when removing element", function() { + var onbeforeremove = o.spy() + var vnode = m("div", {onbeforeremove}) - render(root, vnode) + render(root, [vnode]) + var firstChild = root.firstChild + o(firstChild).notEquals(null) render(root, []) - function remove(node) { - o(node).equals(vnode) - o(this).equals(vnode.state) - o(this != null && typeof this === "object").equals(true) - o(root.childNodes.length).equals(1) - o(root.firstChild).equals(vnode.dom) - - callAsync(function() { - o(root.childNodes.length).equals(0) - - done() - }) - } + o(onbeforeremove.callCount).equals(1) + o(onbeforeremove.this).equals(vnode.state) + o(onbeforeremove.this).satisfies((v) => ({ + pass: v !== null && typeof v === "object", + message: "`onbeforeremove` should be called with an object", + })) + o(onbeforeremove.args[0]).equals(vnode) + o(root.childNodes.length).equals(0) + o(vnode.dom).equals(firstChild) }) - o("calls onbeforeremove when removing fragment", function(done) { - var vnode = m.fragment({onbeforeremove: remove}, m("div")) + o("calls onbeforeremove when removing fragment", function() { + var onbeforeremove = o.spy() + var vnode = m.fragment({onbeforeremove}, m("div")) - render(root, vnode) + render(root, [vnode]) + var firstChild = root.firstChild + o(firstChild).notEquals(null) render(root, []) - function remove(node) { - o(node).equals(vnode) - o(root.childNodes.length).equals(1) - o(root.firstChild).equals(vnode.dom) + o(onbeforeremove.callCount).equals(1) + o(onbeforeremove.this).equals(vnode.state) + o(onbeforeremove.this).satisfies((v) => ({ + pass: v !== null && typeof v === "object", + message: "`onbeforeremove` should be called with an object", + })) + o(onbeforeremove.args[0]).equals(vnode) + o(root.childNodes.length).equals(0) + o(vnode.dom).equals(firstChild) + }) + o("calls onremove after onbeforeremove returns", function() { + var callOrder = [] + var onbeforeremove = o.spy(() => { callOrder.push("onbeforeremove") }) + var spy = o.spy(() => { callOrder.push("onremove") }) + var vnode = m.fragment({onbeforeremove: onbeforeremove, onremove: spy}, "a") - callAsync(function() { - o(root.childNodes.length).equals(0) + render(root, [vnode]) + var firstChild = root.firstChild + o(firstChild).notEquals(null) + render(root, []) - done() - }) - } + o(onbeforeremove.callCount).equals(1) + o(onbeforeremove.this).equals(vnode.state) + o(onbeforeremove.this).satisfies((v) => ({ + pass: v !== null && typeof v === "object", + message: "`onbeforeremove` should be called with an object", + })) + o(onbeforeremove.args[0]).equals(vnode) + o(root.childNodes.length).equals(0) + o(vnode.dom).equals(firstChild) + o(spy.callCount).equals(1) + + o(callOrder).deepEquals(["onbeforeremove", "onremove"]) }) - o("calls onremove after onbeforeremove resolves", function(done) { + o("calls onremove after onbeforeremove resolves", function() { + var removed = Promise.resolve() + var onbeforeremove = o.spy(() => removed) var spy = o.spy() var vnode = m.fragment({onbeforeremove: onbeforeremove, onremove: spy}, "a") - render(root, vnode) + render(root, [vnode]) + var firstChild = root.firstChild + o(firstChild).notEquals(null) render(root, []) - function onbeforeremove(node) { - o(node).equals(vnode) - o(root.childNodes.length).equals(1) - o(root.firstChild).equals(vnode.dom) - o(spy.callCount).equals(0) - - callAsync(function() { - o(root.childNodes.length).equals(0) - o(spy.callCount).equals(1) - - done() - }) - } + o(onbeforeremove.callCount).equals(1) + o(onbeforeremove.this).equals(vnode.state) + o(onbeforeremove.this).satisfies((v) => ({ + pass: v !== null && typeof v === "object", + message: "`onbeforeremove` should be called with an object", + })) + o(onbeforeremove.args[0]).equals(vnode) + o(root.childNodes.length).equals(1) + o(vnode.dom).equals(firstChild) + o(root.firstChild).equals(firstChild) + o(spy.callCount).equals(0) + + return removed.then(() => { + o(onbeforeremove.callCount).equals(1) + o(spy.callCount).equals(1) + }) }) o("does not set onbeforeremove as an event handler", function() { var remove = o.spy() @@ -103,60 +133,40 @@ o.spec("onbeforeremove", function() { o(vnode.dom.onbeforeremove).equals(undefined) o(vnode.dom.attributes["onbeforeremove"]).equals(undefined) }) - o("does not leave elements out of order during removal", function(done) { - var remove = function() {return Promise.resolve()} - var vnodes = [m("div", {key: 1, onbeforeremove: remove}, "1"), m("div", {key: 2, onbeforeremove: remove}, "2")] - var updated = m("div", {key: 2, onbeforeremove: remove}, "2") + o("does not leave elements out of order during removal", function() { + var removed = Promise.resolve() + var vnodes = [ + m.key(1, m("div", {onbeforeremove: () => removed})), + m.key(2, m("span")), + ] + var updated = [m.key(2, m("span"))] render(root, vnodes) render(root, updated) o(root.childNodes.length).equals(2) - o(root.firstChild.firstChild.nodeValue).equals("1") + o(root.firstChild.nodeName).equals("DIV") - callAsync(function() { + return removed.then(() => { o(root.childNodes.length).equals(1) - o(root.firstChild.firstChild.nodeValue).equals("2") - - done() + o(root.firstChild.nodeName).equals("SPAN") }) }) components.forEach(function(cmp){ o.spec(cmp.kind, function(){ var createComponent = cmp.create - o("finalizes the remove phase asynchronously when promise is returned synchronously from both attrs- and tag.onbeforeremove", function(done) { + o("finalizes the remove phase asynchronously when promise is returned synchronously from both attrs- and tag.onbeforeremove", function() { + var removed = Promise.resolve() var onremove = o.spy() - var onbeforeremove = function(){return Promise.resolve()} var component = createComponent({ - onbeforeremove: onbeforeremove, + onbeforeremove: () => removed, onremove: onremove, view: function() {}, }) - render(root, m(component, {onbeforeremove: onbeforeremove, onremove: onremove})) + render(root, [m(component, {onbeforeremove: () => removed, onremove: onremove})]) render(root, []) - callAsync(function() { + return removed.then(() => { o(onremove.callCount).equals(2) // once for `tag`, once for `attrs` - done() - }) - }) - o("awaits promise resolution before removing the node", function(done) { - var view = o.spy() - var onremove = o.spy() - var onbeforeremove = function(){return new Promise(function(resolve){callAsync(resolve)})} - var component = createComponent({ - onbeforeremove: onbeforeremove, - onremove: onremove, - view: view, - }) - render(root, m(component)) - render(root, []) - - o(onremove.callCount).equals(0) - callAsync(function(){ - callAsync(function() { - o(onremove.callCount).equals(1) - done() - }) }) }) }) diff --git a/render/tests/test-onbeforeupdate.js b/render/tests/test-onbeforeupdate.js index 901792e76..4215207a3 100644 --- a/render/tests/test-onbeforeupdate.js +++ b/render/tests/test-onbeforeupdate.js @@ -98,21 +98,6 @@ o.spec("onbeforeupdate", function() { o(count).equals(1) }) - o("doesn't fire on recycled nodes", function() { - var onbeforeupdate = o.spy() - var vnodes = [m("div", {key: 1})] - var temp = [] - var updated = [m("div", {key: 1, onbeforeupdate: onbeforeupdate})] - - render(root, vnodes) - render(root, temp) - render(root, updated) - - o(vnodes[0].dom).notEquals(updated[0].dom) // this used to be a recycling pool test - o(updated[0].dom.nodeName).equals("DIV") - o(onbeforeupdate.callCount).equals(0) - }) - components.forEach(function(cmp){ o.spec(cmp.kind, function(){ var createComponent = cmp.create diff --git a/render/tests/test-oncreate.js b/render/tests/test-oncreate.js index 93ba8c389..948447615 100644 --- a/render/tests/test-oncreate.js +++ b/render/tests/test-oncreate.js @@ -33,14 +33,14 @@ o.spec("oncreate", function() { o(callback.this).equals(vnode.state) o(callback.args[0]).equals(vnode) }) - o("calls oncreate when replacing keyed", function() { + o("calls oncreate when replacing same-keyed", function() { var createDiv = o.spy() var createA = o.spy() - var vnode = m("div", {key: 1, oncreate: createDiv}) - var updated = m("a", {key: 1, oncreate: createA}) + var vnode = m("div", {oncreate: createDiv}) + var updated = m("a", {oncreate: createA}) - render(root, vnode) - render(root, updated) + render(root, m.key(1, vnode)) + render(root, m.key(1, updated)) o(createDiv.callCount).equals(1) o(createDiv.this).equals(vnode.state) @@ -94,13 +94,13 @@ o.spec("oncreate", function() { o("does not call oncreate when updating keyed", function() { var create = o.spy() var update = o.spy() - var vnode = m("div", {key: 1, oncreate: create}) - var otherVnode = m("a", {key: 2}) - var updated = m("div", {key: 1, oncreate: update}) - var otherUpdated = m("a", {key: 2}) + var vnode = m("div", {oncreate: create}) + var otherVnode = m("a") + var updated = m("div", {oncreate: update}) + var otherUpdated = m("a") - render(root, [vnode, otherVnode]) - render(root, [otherUpdated, updated]) + render(root, [m.key(1, vnode), m.key(2, otherVnode)]) + render(root, [m.key(2, otherUpdated), m.key(1, updated)]) o(create.callCount).equals(1) o(create.this).equals(vnode.state) @@ -121,12 +121,12 @@ o.spec("oncreate", function() { o("does not recycle when there's an oncreate", function() { var create = o.spy() var update = o.spy() - var vnode = m("div", {key: 1, oncreate: create}) - var updated = m("div", {key: 1, oncreate: update}) + var vnode = m("div", {oncreate: create}) + var updated = m("div", {oncreate: update}) - render(root, vnode) + render(root, m.key(1, vnode)) render(root, []) - render(root, updated) + render(root, m.key(1, updated)) o(vnode.dom).notEquals(updated.dom) o(create.callCount).equals(1) @@ -197,9 +197,9 @@ o.spec("oncreate", function() { }) o("calls oncreate on recycle", function() { var create = o.spy() - var vnodes = m("div", {key: 1, oncreate: create}) + var vnodes = m.key(1, m("div", {oncreate: create})) var temp = [] - var updated = m("div", {key: 1, oncreate: create}) + var updated = m.key(1, m("div", {oncreate: create})) render(root, vnodes) render(root, temp) diff --git a/render/tests/test-oninit.js b/render/tests/test-oninit.js index 4965b7b9f..790d7f59a 100644 --- a/render/tests/test-oninit.js +++ b/render/tests/test-oninit.js @@ -36,11 +36,11 @@ o.spec("oninit", function() { o("calls oninit when replacing keyed", function() { var createDiv = o.spy() var createA = o.spy() - var vnode = m("div", {key: 1, oninit: createDiv}) - var updated = m("a", {key: 1, oninit: createA}) + var vnode = m("div", {oninit: createDiv}) + var updated = m("a", {oninit: createA}) - render(root, vnode) - render(root, updated) + render(root, m.key(1, vnode)) + render(root, m.key(1, updated)) o(createDiv.callCount).equals(1) o(createDiv.this).equals(vnode.state) @@ -94,13 +94,13 @@ o.spec("oninit", function() { o("does not call oninit when updating keyed", function() { var create = o.spy() var update = o.spy() - var vnode = m("div", {key: 1, oninit: create}) - var otherVnode = m("a", {key: 2}) - var updated = m("div", {key: 1, oninit: update}) - var otherUpdated = m("a", {key: 2}) + var vnode = m("div", {oninit: create}) + var otherVnode = m("a") + var updated = m("div", {oninit: update}) + var otherUpdated = m("a") - render(root, [vnode, otherVnode]) - render(root, [otherUpdated, updated]) + render(root, [m.key(1, vnode), m.key(2, otherVnode)]) + render(root, [m.key(2, otherUpdated), m.key(1, updated)]) o(create.callCount).equals(1) o(create.this).equals(vnode.state) @@ -121,12 +121,12 @@ o.spec("oninit", function() { o("calls oninit when recycling", function() { var create = o.spy() var update = o.spy() - var vnode = m("div", {key: 1, oninit: create}) - var updated = m("div", {key: 1, oninit: update}) + var vnode = m("div", {oninit: create}) + var updated = m("div", {oninit: update}) - render(root, vnode) + render(root, m.key(1, vnode)) render(root, []) - render(root, updated) + render(root, m.key(1, updated)) o(create.callCount).equals(1) o(create.this).equals(vnode.state) @@ -187,16 +187,16 @@ o.spec("oninit", function() { var oninit3 = o.spy() render(root, [ - m("p", {key: 1, oninit: oninit1}), - m("p", {key: 2, oninit: oninit2}), - m("p", {key: 3, oninit: oninit3}), + m.key(1, m("p", {oninit: oninit1})), + m.key(2, m("p", {oninit: oninit2})), + m.key(3, m("p", {oninit: oninit3})), ]) render(root, [ - m("p", {key: 1, oninit: oninit1}), - m("p", {key: 3, oninit: oninit3}), + m.key(1, m("p", {oninit: oninit1})), + m.key(3, m("p", {oninit: oninit3})), ]) render(root, [ - m("p", {key: 3, oninit: oninit3}), + m.key(3, m("p", {oninit: oninit3})), ]) o(oninit1.callCount).equals(1) diff --git a/render/tests/test-onremove.js b/render/tests/test-onremove.js index 37263c5e5..bb0d753a7 100644 --- a/render/tests/test-onremove.js +++ b/render/tests/test-onremove.js @@ -71,25 +71,25 @@ o.spec("onremove", function() { }) o("calls onremove on keyed nodes", function() { var remove = o.spy() - var vnodes = [m("div", {key: 1})] - var temp = [m("div", {key: 2, onremove: remove})] - var updated = [m("div", {key: 1})] + var vnode = m("div") + var temp = m("div", {onremove: remove}) + var updated = m("div") - render(root, vnodes) - render(root, temp) - render(root, updated) + render(root, m.key(1, vnode)) + render(root, m.key(2, temp)) + render(root, m.key(1, updated)) - o(vnodes[0].dom).notEquals(updated[0].dom) // this used to be a recycling pool test + o(vnode.dom).notEquals(updated.dom) // this used to be a recycling pool test o(remove.callCount).equals(1) }) o("does not recycle when there's an onremove", function() { var remove = o.spy() - var vnode = m("div", {key: 1, onremove: remove}) - var updated = m("div", {key: 1, onremove: remove}) + var vnode = m("div", {onremove: remove}) + var updated = m("div", {onremove: remove}) - render(root, vnode) + render(root, m.key(1, vnode)) render(root, []) - render(root, updated) + render(root, m.key(1, updated)) o(vnode.dom).notEquals(updated.dom) }) @@ -181,197 +181,234 @@ o.spec("onremove", function() { }) o("doesn't fire when removing the children of a node that's brought back from the pool (#1991 part 2)", function() { var onremove = o.spy() - var vnode = m("div", {key: 1}, m("div", {onremove: onremove})) - var temp = m("div", {key: 2}) - var updated = m("div", {key: 1}, m("p")) + var vnode = m("div", m("div", {onremove: onremove})) + var temp = m("div") + var updated = m("div", m("p")) - render(root, vnode) - render(root, temp) - render(root, updated) + render(root, m.key(1, vnode)) + render(root, m.key(2, temp)) + render(root, m.key(1, updated)) o(vnode.dom).notEquals(updated.dom) // this used to be a recycling pool test o(onremove.callCount).equals(1) }) // Warning: this test is complicated because it's replicating a race condition. o("removes correct nodes in fragment when child delays removal, parent removes, then child resolves", function () { - // Custom assertion - we need to test the entire tree for consistency. - - const template = (tpl) => (root) => { - var expected = [] - - for (var i = 0; i < tpl.length; i++) { - var name = tpl[i][0] - var text = tpl[i][1] - expected.push({ - name: name, - firstType: name === "#text" ? null : "#text", - text: text, - }) - } - - var actual = [] - var list = root.firstChild.childNodes - for (var i = 0; i < list.length; i++) { - var current = list[i] - var textNode = current.childNodes.length === 1 - ? current.firstChild - : current - actual.push({ - name: current.nodeName, - firstType: textNode === current ? null : textNode.nodeName, - text: textNode.nodeValue, - }) - } - actual = JSON.stringify(actual, null, " ") - expected = JSON.stringify(expected, null, " ") - return { - pass: actual === expected, - message: -`${expected} - expected, got -${actual}` - } - } - var finallyCB1 - var finallyCB2 + var resumeAttr1, resumeMethod1, resumeAttr2, resumeMethod2 + var attrRemoved1 = new Promise((resolve) => resumeAttr1 = resolve) + var methodRemoved1 = new Promise((resolve) => resumeMethod1 = resolve) + var attrRemoved2 = new Promise((resolve) => resumeAttr2 = resolve) + var methodRemoved2 = new Promise((resolve) => resumeMethod2 = resolve) + var calls = [] + + var methodCalled = false var C = createComponent({ - view({children}){return children}, - onbeforeremove(){ - return {then(){}, finally: function (fcb) { finallyCB1 = fcb }} - } + view: (v) => v.children, + onremove() { calls.push("component method onremove") }, + onbeforeremove() { + calls.push("component method onbeforeremove") + if (methodCalled) return methodRemoved2 + methodCalled = true + return methodRemoved1 + }, }) - function update(id, showParent, showChild) { - const removeParent = o.spy() - const removeSyncChild = o.spy() - const removeAsyncChild = o.spy() - - render(root, - m("div", - showParent && m.fragment( - {onremove: removeParent}, - m("a", {onremove: removeSyncChild}, "sync child"), - showChild && m(C, { - onbeforeremove: function () { - return {then(){}, finally: function (fcb) { finallyCB2 = fcb }} - }, - onremove: removeAsyncChild - }, m("div", id)) - ) - ) - ) - return {removeAsyncChild,removeParent, removeSyncChild} - } - - const hooks1 = update("1", true, true) - o(root).satisfies(template([ - ["A", "sync child"], - ["DIV", "1"], - ])) - o(finallyCB1).equals(undefined) - o(finallyCB2).equals(undefined) - - const hooks2 = update("2", true, false) - - o(root).satisfies(template([ - ["A", "sync child"], - ["DIV", "1"], - ])) - - o(typeof finallyCB1).equals("function") - o(typeof finallyCB2).equals("function") - - var original1 = finallyCB1 - var original2 = finallyCB2 - - const hooks3 = update("3", true, true) - - o(root).satisfies(template([ - ["A", "sync child"], - ["DIV", "1"], - ["DIV", "3"], - ])) - - o(hooks3.removeParent.callCount).equals(0) - o(hooks3.removeSyncChild.callCount).equals(0) - o(hooks3.removeAsyncChild.callCount).equals(0) - o(finallyCB1).equals(original1) - o(finallyCB2).equals(original2) - - const hooks4 = update("4", false, true) - - o(root).satisfies(template([ - ["DIV", "1"], - ])) - - o(hooks3.removeParent.callCount).equals(1) - o(hooks3.removeSyncChild.callCount).equals(1) - o(hooks3.removeAsyncChild.callCount).equals(1) - o(hooks3.removeParent.args[0].tag).equals("[") - o(finallyCB1).equals(original1) - o(finallyCB2).equals(original2) - - const hooks5 = update("5", true, true) - - - o(root).satisfies(template([ - ["DIV", "1"], - ["A", "sync child"], - ["DIV", "5"], - ])) - o(finallyCB1).equals(original1) - o(finallyCB2).equals(original2) - - o(hooks1.removeAsyncChild.callCount).equals(0) - - finallyCB1() - - o(hooks1.removeAsyncChild.callCount).equals(0) - - finallyCB2() - - o(hooks1.removeAsyncChild.callCount).equals(1) - - o(root).satisfies(template([ - ["A", "sync child"], - ["DIV", "5"], - ])) - o(finallyCB1).equals(original1) - o(finallyCB2).equals(original2) - - const hooks6 = update("6", true, true) - - o(root).satisfies(template([ - ["A", "sync child"], - ["DIV", "6"], - ])) - o(finallyCB1).equals(original1) - o(finallyCB2).equals(original2) - - // final tally - o(hooks1.removeParent.callCount).equals(0) - o(hooks1.removeSyncChild.callCount).equals(0) - o(hooks1.removeAsyncChild.callCount).equals(1) - - o(hooks2.removeParent.callCount).equals(0) - o(hooks2.removeSyncChild.callCount).equals(0) - o(hooks2.removeAsyncChild.callCount).equals(0) - - o(hooks3.removeParent.callCount).equals(1) - o(hooks3.removeSyncChild.callCount).equals(1) - o(hooks3.removeAsyncChild.callCount).equals(1) - - o(hooks4.removeParent.callCount).equals(0) - o(hooks4.removeSyncChild.callCount).equals(0) - o(hooks4.removeAsyncChild.callCount).equals(0) - - o(hooks5.removeParent.callCount).equals(0) - o(hooks5.removeSyncChild.callCount).equals(0) - o(hooks5.removeAsyncChild.callCount).equals(0) - - o(hooks6.removeParent.callCount).equals(0) - o(hooks6.removeSyncChild.callCount).equals(0) - o(hooks6.removeAsyncChild.callCount).equals(0) + render(root, m("div", m.fragment({onremove() { calls.push("parent onremove") }}, + m("a", {onremove() { calls.push("child sync onremove") }}), + m(C, { + onbeforeremove() { calls.push("component attr onbeforeremove"); return attrRemoved1 }, + onremove() { calls.push("component attr onremove") }, + }, m("span")) + ))) + + o(calls).deepEquals([]) + o(root.childNodes.length).equals(1) + o(root.childNodes[0].nodeName).equals("DIV") + o(root.childNodes[0].childNodes.length).equals(2) + o(root.childNodes[0].childNodes[0].nodeName).equals("A") + o(root.childNodes[0].childNodes[1].nodeName).equals("SPAN") + + render(root, m("div", m.fragment({onremove() { calls.push("parent onremove") }}, + m("a", {onremove() { calls.push("child sync onremove") }}) + ))) + + o(calls).deepEquals([ + "component method onbeforeremove", + "component attr onbeforeremove", + ]) + o(root.childNodes.length).equals(1) + o(root.childNodes[0].nodeName).equals("DIV") + o(root.childNodes[0].childNodes.length).equals(2) + o(root.childNodes[0].childNodes[0].nodeName).equals("A") + o(root.childNodes[0].childNodes[1].nodeName).equals("SPAN") + var firstRemoved = root.childNodes[0].childNodes[1] + + render(root, m("div", m.fragment({onremove() { calls.push("parent onremove") }}, + m("a", {onremove() { calls.push("child sync onremove") }}), + m(C, { + onbeforeremove() { calls.push("component attr onbeforeremove"); return attrRemoved2 }, + onremove() { calls.push("component attr onremove") }, + }, m("span")) + ))) + + o(calls).deepEquals([ + "component method onbeforeremove", + "component attr onbeforeremove", + ]) + o(root.childNodes.length).equals(1) + o(root.childNodes[0].nodeName).equals("DIV") + o(root.childNodes[0].childNodes.length).equals(3) + o(root.childNodes[0].childNodes[0].nodeName).equals("A") + o(root.childNodes[0].childNodes[1]).equals(firstRemoved) + o(root.childNodes[0].childNodes[2].nodeName).equals("SPAN") + var secondRemoved = root.childNodes[0].childNodes[2] + + render(root, m("div")) + + o(calls).deepEquals([ + "component method onbeforeremove", + "component attr onbeforeremove", + "child sync onremove", + "component method onbeforeremove", + "component attr onbeforeremove", + ]) + o(root.childNodes.length).equals(1) + o(root.childNodes[0].nodeName).equals("DIV") + o(root.childNodes[0].childNodes.length).equals(2) + o(root.childNodes[0].childNodes[0]).equals(firstRemoved) + o(root.childNodes[0].childNodes[1]).equals(secondRemoved) + + render(root, m("div", m.fragment({onremove() { calls.push("unexpected parent onremove") }}, + m("a", {onremove() { calls.push("unexpected child sync onremove") }}), + m(C, { + onbeforeremove() { calls.push("unexpected component attr onbeforeremove") }, + onremove() { calls.push("unexpected component attr onremove") }, + }, m("span")) + ))) + + // No change + o(calls).deepEquals([ + "component method onbeforeremove", + "component attr onbeforeremove", + "child sync onremove", + "component method onbeforeremove", + "component attr onbeforeremove", + ]) + o(root.childNodes.length).equals(1) + o(root.childNodes[0].nodeName).equals("DIV") + o(root.childNodes[0].childNodes.length).equals(4) + o(root.childNodes[0].childNodes[0]).equals(firstRemoved) + o(root.childNodes[0].childNodes[1]).equals(secondRemoved) + o(root.childNodes[0].childNodes[2].nodeName).equals("A") + o(root.childNodes[0].childNodes[3].nodeName).equals("SPAN") + + render(root, m("div", m.fragment({onremove() { calls.push("unexpected parent onremove") }}, + m("a", {onremove() { calls.push("unexpected child sync onremove") }}), + m(C, { + onbeforeremove() { calls.push("unexpected component attr onbeforeremove") }, + onremove() { calls.push("unexpected component attr onremove") }, + }, m("span")) + ))) + + // No change + o(calls).deepEquals([ + "component method onbeforeremove", + "component attr onbeforeremove", + "child sync onremove", + "component method onbeforeremove", + "component attr onbeforeremove", + ]) + o(root.childNodes.length).equals(1) + o(root.childNodes[0].nodeName).equals("DIV") + o(root.childNodes[0].childNodes.length).equals(4) + o(root.childNodes[0].childNodes[0]).equals(firstRemoved) + o(root.childNodes[0].childNodes[1]).equals(secondRemoved) + o(root.childNodes[0].childNodes[2].nodeName).equals("A") + o(root.childNodes[0].childNodes[3].nodeName).equals("SPAN") + + resumeAttr1() + + return attrRemoved1 + .then(() => { + // No change + o(calls).deepEquals([ + "component method onbeforeremove", + "component attr onbeforeremove", + "child sync onremove", + "component method onbeforeremove", + "component attr onbeforeremove", + ]) + o(root.childNodes.length).equals(1) + o(root.childNodes[0].nodeName).equals("DIV") + o(root.childNodes[0].childNodes.length).equals(4) + o(root.childNodes[0].childNodes[0]).equals(firstRemoved) + o(root.childNodes[0].childNodes[1]).equals(secondRemoved) + o(root.childNodes[0].childNodes[2].nodeName).equals("A") + o(root.childNodes[0].childNodes[3].nodeName).equals("SPAN") + + resumeAttr2() + return attrRemoved2 + }) + .then(() => { + // No change + o(calls).deepEquals([ + "component method onbeforeremove", + "component attr onbeforeremove", + "child sync onremove", + "component method onbeforeremove", + "component attr onbeforeremove", + ]) + o(root.childNodes.length).equals(1) + o(root.childNodes[0].nodeName).equals("DIV") + o(root.childNodes[0].childNodes.length).equals(4) + o(root.childNodes[0].childNodes[0]).equals(firstRemoved) + o(root.childNodes[0].childNodes[1]).equals(secondRemoved) + o(root.childNodes[0].childNodes[2].nodeName).equals("A") + o(root.childNodes[0].childNodes[3].nodeName).equals("SPAN") + + resumeMethod1() + return methodRemoved1 + }) + .then(() => { + // No change + o(calls).deepEquals([ + "component method onbeforeremove", + "component attr onbeforeremove", + "child sync onremove", + "component method onbeforeremove", + "component attr onbeforeremove", + "component method onremove", + "component attr onremove", + ]) + o(root.childNodes.length).equals(1) + o(root.childNodes[0].nodeName).equals("DIV") + o(root.childNodes[0].childNodes.length).equals(3) + o(root.childNodes[0].childNodes[0]).equals(secondRemoved) + o(root.childNodes[0].childNodes[1].nodeName).equals("A") + o(root.childNodes[0].childNodes[2].nodeName).equals("SPAN") + + resumeMethod2() + return methodRemoved2 + }) + .then(() => { + // Now, everything should be cleaned up + o(calls).deepEquals([ + "component method onbeforeremove", + "component attr onbeforeremove", + "child sync onremove", + "component method onbeforeremove", + "component attr onbeforeremove", + "component method onremove", + "component attr onremove", + "component method onremove", + "component attr onremove", + ]) + o(root.childNodes.length).equals(1) + o(root.childNodes[0].nodeName).equals("DIV") + o(root.childNodes[0].childNodes.length).equals(2) + o(root.childNodes[0].childNodes[0].nodeName).equals("A") + o(root.childNodes[0].childNodes[1].nodeName).equals("SPAN") + }) }) }) }) diff --git a/render/tests/test-onupdate.js b/render/tests/test-onupdate.js index ef7c323e9..8f02fcccb 100644 --- a/render/tests/test-onupdate.js +++ b/render/tests/test-onupdate.js @@ -39,25 +39,14 @@ o.spec("onupdate", function() { o("does not call onupdate when replacing keyed element", function() { var create = o.spy() var update = o.spy() - var vnode = m("div", {key: 1, onupdate: create}) - var updated = m("a", {key: 1, onupdate: update}) + var vnode = m.key(1, m("div", {onupdate: create})) + var updated = m.key(1, m("a", {onupdate: update})) render(root, vnode) render(root, updated) o(create.callCount).equals(0) o(update.callCount).equals(0) }) - o("does not recycle when there's an onupdate", function() { - var update = o.spy() - var vnode = m("div", {key: 1, onupdate: update}) - var updated = m("div", {key: 1, onupdate: update}) - - render(root, vnode) - render(root, []) - render(root, updated) - - o(vnode.dom).notEquals(updated.dom) - }) o("does not call old onupdate when removing the onupdate property in new vnode", function() { var create = o.spy() var vnode = m("a", {onupdate: create}) diff --git a/render/tests/test-render.js b/render/tests/test-render.js index ad3e859d1..195638a5c 100644 --- a/render/tests/test-render.js +++ b/render/tests/test-render.js @@ -193,16 +193,16 @@ o.spec("render", function() { var updateB = o.spy() var removeB = o.spy() var a = function() { - return m("div", {key: 1}, - m("div", {key: 11, oncreate: createA, onupdate: updateA, onremove: removeA}), - m("div", {key: 12}) - ) + return m.key(1, m("div", + m.key(11, m("div", {oncreate: createA, onupdate: updateA, onremove: removeA})), + m.key(12, m("div")) + )) } var b = function() { - return m("div", {key: 2}, - m("div", {key: 21, oncreate: createB, onupdate: updateB, onremove: removeB}), - m("div", {key: 22}) - ) + return m.key(2, m("div", + m.key(21, m("div", {oncreate: createB, onupdate: updateB, onremove: removeB})), + m.key(22, m("div")) + )) } render(root, a()) render(root, b()) @@ -223,14 +223,14 @@ o.spec("render", function() { var updateB = o.spy() var removeB = o.spy() var a = function() { - return m("div", {key: 1}, + return m.key(1, m("div", m("div", {oncreate: createA, onupdate: updateA, onremove: removeA}) - ) + )) } var b = function() { - return m("div", {key: 2}, + return m.key(2, m("div", m("div", {oncreate: createB, onupdate: updateB, onremove: removeB}) - ) + )) } render(root, a()) render(root, b()) @@ -252,14 +252,14 @@ o.spec("render", function() { var removeB = o.spy() var a = function() { - return m("div", {key: 1}, + return m.key(1, m("div", m("div", {oncreate: createA, onupdate: updateA, onremove: removeA}) - ) + )) } var b = function() { - return m("div", {key: 2}, + return m.key(2, m("div", m("div", {oncreate: createB, onupdate: updateB, onremove: removeB}) - ) + )) } render(root, a()) render(root, a()) @@ -282,8 +282,8 @@ o.spec("render", function() { o("svg namespace is preserved in keyed diff (#1820)", function(){ // note that this only exerciese one branch of the keyed diff algo var svg = m("svg", - m("g", {key: 0}), - m("g", {key: 1}) + m.key(0, m("g")), + m.key(1, m("g")) ) render(root, svg) @@ -292,8 +292,8 @@ o.spec("render", function() { o(svg.dom.childNodes[1].namespaceURI).equals("http://www.w3.org/2000/svg") svg = m("svg", - m("g", {key: 1, x: 1}), - m("g", {key: 2, x: 2}) + m.key(1, m("g", {x: 1})), + m.key(2, m("g", {x: 2})) ) render(root, svg) diff --git a/render/tests/test-updateElement.js b/render/tests/test-updateElement.js index eb41e06f4..a94667514 100644 --- a/render/tests/test-updateElement.js +++ b/render/tests/test-updateElement.js @@ -286,8 +286,8 @@ o.spec("updateElement", function() { o(updated.dom.firstChild.namespaceURI).equals("http://www.w3.org/2000/svg") }) o("doesn't restore since we're not recycling", function() { - var vnode = m("div", {key: 1}) - var updated = m("div", {key: 2}) + var vnode = m.key(1, m("div")) + var updated = m.key(2, m("div")) render(root, vnode) var a = vnode.dom @@ -301,12 +301,12 @@ o.spec("updateElement", function() { o(a).notEquals(c) // this used to be a recycling pool test }) o("doesn't restore since we're not recycling (via map)", function() { - var a = m("div", {key: 1}) - var b = m("div", {key: 2}) - var c = m("div", {key: 3}) - var d = m("div", {key: 4}) - var e = m("div", {key: 5}) - var f = m("div", {key: 6}) + var a = m.key(1, m("div")) + var b = m.key(2, m("div")) + var c = m.key(3, m("div")) + var d = m.key(4, m("div")) + var e = m.key(5, m("div")) + var f = m.key(6, m("div")) render(root, [a, b, c]) var x = root.childNodes[1] diff --git a/render/tests/test-updateFragment.js b/render/tests/test-updateFragment.js index 5ee5e594c..d7f6b0c25 100644 --- a/render/tests/test-updateFragment.js +++ b/render/tests/test-updateFragment.js @@ -31,7 +31,6 @@ o.spec("updateFragment", function() { render(root, updated) o(updated.dom).equals(root.firstChild) - o(updated.domSize).equals(2) o(root.childNodes.length).equals(2) o(root.childNodes[0].nodeName).equals("A") o(root.childNodes[1].nodeName).equals("B") @@ -44,7 +43,7 @@ o.spec("updateFragment", function() { render(root, updated) o(updated.dom).equals(null) - o(updated.domSize).equals(0) + o(updated.children).deepEquals([]) o(root.childNodes.length).equals(0) }) o("updates from childless fragment", function() { diff --git a/render/tests/test-updateNodes.js b/render/tests/test-updateNodes.js index fdf45c065..15c88eee8 100644 --- a/render/tests/test-updateNodes.js +++ b/render/tests/test-updateNodes.js @@ -7,7 +7,7 @@ var vdom = require("../../render/render") var m = require("../../render/hyperscript") function vnodify(str) { - return str.split(",").map(function(k) {return m(k, {key: k})}) + return str.split(",").map((k) => m.key(k, m(k))) } o.spec("updateNodes", function() { @@ -19,8 +19,8 @@ o.spec("updateNodes", function() { }) o("handles el noop", function() { - var vnodes = [m("a", {key: 1}), m("b", {key: 2})] - var updated = [m("a", {key: 1}), m("b", {key: 2})] + var vnodes = [m.key(1, m("a")), m.key(2, m("b"))] + var updated = [m.key(1, m("a")), m.key(2, m("b"))] render(root, vnodes) render(root, updated) @@ -106,8 +106,8 @@ o.spec("updateNodes", function() { o(root.childNodes.length).equals(1) }) o("reverses els w/ even count", function() { - var vnodes = [m("a", {key: 1}), m("b", {key: 2}), m("i", {key: 3}), m("s", {key: 4})] - var updated = [m("s", {key: 4}), m("i", {key: 3}), m("b", {key: 2}), m("a", {key: 1})] + var vnodes = [m.key(1, m("a")), m.key(2, m("b")), m.key(3, m("i")), m.key(4, m("s"))] + var updated = [m.key(4, m("s")), m.key(3, m("i")), m.key(2, m("b")), m.key(1, m("a"))] render(root, vnodes) render(root, updated) @@ -123,23 +123,19 @@ o.spec("updateNodes", function() { o(updated[3].dom).equals(root.childNodes[3]) }) o("reverses els w/ odd count", function() { - var vnodes = [m("a", {key: 1}), m("b", {key: 2}), m("i", {key: 3})] - var updated = [m("i", {key: 3}), m("b", {key: 2}), m("a", {key: 1})] - var expectedTags = updated.map(function(vn) {return vn.tag}) + var vnodes = [m.key(1, m("a")), m.key(2, m("b")), m.key(3, m("i"))] + var updated = [m.key(3, m("i")), m.key(2, m("b")), m.key(1, m("a"))] render(root, vnodes) render(root, updated) - var tagNames = [].map.call(root.childNodes, function(n) {return n.nodeName.toLowerCase()}) - o(root.childNodes.length).equals(3) o(updated[0].dom.nodeName).equals("I") o(updated[1].dom.nodeName).equals("B") o(updated[2].dom.nodeName).equals("A") - o(tagNames).deepEquals(expectedTags) }) o("creates el at start", function() { - var vnodes = [m("a", {key: 1})] - var updated = [m("b", {key: 2}), m("a", {key: 1})] + var vnodes = [m.key(1, m("a"))] + var updated = [m.key(2, m("b")), m.key(1, m("a"))] render(root, vnodes) render(root, updated) @@ -151,8 +147,8 @@ o.spec("updateNodes", function() { o(updated[1].dom).equals(root.childNodes[1]) }) o("creates el at end", function() { - var vnodes = [m("a", {key: 1})] - var updated = [m("a", {key: 1}), m("b", {key: 2})] + var vnodes = [m.key(1, m("a"))] + var updated = [m.key(1, m("a")), m.key(2, m("b"))] render(root, vnodes) render(root, updated) @@ -164,8 +160,8 @@ o.spec("updateNodes", function() { o(updated[1].dom).equals(root.childNodes[1]) }) o("creates el in middle", function() { - var vnodes = [m("a", {key: 1}), m("b", {key: 2})] - var updated = [m("a", {key: 1}), m("i", {key: 3}), m("b", {key: 2})] + var vnodes = [m.key(1, m("a")), m.key(2, m("b"))] + var updated = [m.key(1, m("a")), m.key(3, m("i")), m.key(2, m("b"))] render(root, vnodes) render(root, updated) @@ -178,8 +174,8 @@ o.spec("updateNodes", function() { o(updated[2].dom).equals(root.childNodes[2]) }) o("creates el while reversing", function() { - var vnodes = [m("a", {key: 1}), m("b", {key: 2})] - var updated = [m("b", {key: 2}), m("i", {key: 3}), m("a", {key: 1})] + var vnodes = [m.key(1, m("a")), m.key(2, m("b"))] + var updated = [m.key(2, m("b")), m.key(3, m("i")), m.key(1, m("a"))] render(root, vnodes) render(root, updated) @@ -193,8 +189,8 @@ o.spec("updateNodes", function() { o(updated[2].dom).equals(root.childNodes[2]) }) o("deletes el at start", function() { - var vnodes = [m("b", {key: 2}), m("a", {key: 1})] - var updated = [m("a", {key: 1})] + var vnodes = [m.key(2, m("b")), m.key(1, m("a"))] + var updated = [m.key(1, m("a"))] render(root, vnodes) render(root, updated) @@ -204,8 +200,8 @@ o.spec("updateNodes", function() { o(updated[0].dom).equals(root.childNodes[0]) }) o("deletes el at end", function() { - var vnodes = [m("a", {key: 1}), m("b", {key: 2})] - var updated = [m("a", {key: 1})] + var vnodes = [m.key(1, m("a")), m.key(2, m("b"))] + var updated = [m.key(1, m("a"))] render(root, vnodes) render(root, updated) @@ -215,8 +211,8 @@ o.spec("updateNodes", function() { o(updated[0].dom).equals(root.childNodes[0]) }) o("deletes el at middle", function() { - var vnodes = [m("a", {key: 1}), m("i", {key: 3}), m("b", {key: 2})] - var updated = [m("a", {key: 1}), m("b", {key: 2})] + var vnodes = [m.key(1, m("a")), m.key(3, m("i")), m.key(2, m("b"))] + var updated = [m.key(1, m("a")), m.key(2, m("b"))] render(root, vnodes) render(root, updated) @@ -228,8 +224,8 @@ o.spec("updateNodes", function() { o(updated[1].dom).equals(root.childNodes[1]) }) o("deletes el while reversing", function() { - var vnodes = [m("a", {key: 1}), m("i", {key: 3}), m("b", {key: 2})] - var updated = [m("b", {key: 2}), m("a", {key: 1})] + var vnodes = [m.key(1, m("a")), m.key(3, m("i")), m.key(2, m("b"))] + var updated = [m.key(2, m("b")), m.key(1, m("a"))] render(root, vnodes) render(root, updated) @@ -241,8 +237,8 @@ o.spec("updateNodes", function() { o(updated[1].dom).equals(root.childNodes[1]) }) o("creates, deletes, reverses els at same time", function() { - var vnodes = [m("a", {key: 1}), m("i", {key: 3}), m("b", {key: 2})] - var updated = [m("b", {key: 2}), m("a", {key: 1}), m("s", {key: 4})] + var vnodes = [m.key(1, m("a")), m.key(3, m("i")), m.key(2, m("b"))] + var updated = [m.key(2, m("b")), m.key(1, m("a")), m.key(4, m("s"))] render(root, vnodes) render(root, updated) @@ -256,8 +252,8 @@ o.spec("updateNodes", function() { o(updated[2].dom).equals(root.childNodes[2]) }) o("creates, deletes, reverses els at same time with '__proto__' key", function() { - var vnodes = [m("a", {key: "__proto__"}), m("i", {key: 3}), m("b", {key: 2})] - var updated = [m("b", {key: 2}), m("a", {key: "__proto__"}), m("s", {key: 4})] + var vnodes = [m.key("__proto__", m("a")), m.key(3, m("i")), m.key(2, m("b"))] + var updated = [m.key(2, m("b")), m.key("__proto__", m("a")), m.key(4, m("s"))] render(root, vnodes) render(root, updated) @@ -271,8 +267,8 @@ o.spec("updateNodes", function() { o(updated[2].dom).equals(root.childNodes[2]) }) o("adds to empty fragment followed by el", function() { - var vnodes = [m.fragment({key: 1}), m("b", {key: 2})] - var updated = [m.fragment({key: 1}, m("a")), m("b", {key: 2})] + var vnodes = [m.key(1), m.key(2, m("b"))] + var updated = [m.key(1, m("a")), m.key(2, m("b"))] render(root, vnodes) render(root, updated) @@ -284,8 +280,8 @@ o.spec("updateNodes", function() { o(updated[1].dom).equals(root.childNodes[1]) }) o("reverses followed by el", function() { - var vnodes = [m.fragment({key: 1}, m("a", {key: 2}), m("b", {key: 3})), m("i", {key: 4})] - var updated = [m.fragment({key: 1}, m("b", {key: 3}), m("a", {key: 2})), m("i", {key: 4})] + var vnodes = [m.key(1, m.key(2, m("a")), m.key(3, m("b"))), m.key(4, m("i"))] + var updated = [m.key(1, m.key(3, m("b")), m.key(2, m("a"))), m.key(4, m("i"))] render(root, vnodes) render(root, updated) @@ -299,59 +295,57 @@ o.spec("updateNodes", function() { o(updated[1].dom).equals(root.childNodes[2]) }) o("populates fragment followed by el keyed", function() { - var vnodes = [m.fragment({key: 1}), m("i", {key: 2})] - var updated = [m.fragment({key: 1}, m("a"), m("b")), m("i", {key: 2})] + var vnodes = [m.key(1), m.key(2, m("i"))] + var updated = [m.key(1, m("a"), m("b")), m.key(2, m("i"))] render(root, vnodes) render(root, updated) o(root.childNodes.length).equals(3) - o(updated[0].dom.nodeName).equals("A") - o(updated[0].dom).equals(root.childNodes[0]) - o(updated[0].domSize).equals(2) - o(updated[0].dom.nextSibling.nodeName).equals("B") - o(updated[0].dom.nextSibling).equals(root.childNodes[1]) - o(updated[1].dom.nodeName).equals("I") - o(updated[1].dom).equals(root.childNodes[2]) + o(updated[0].children[0].dom.nodeName).equals("A") + o(updated[0].children[0].dom).equals(root.childNodes[0]) + o(updated[0].children[0].dom.nextSibling.nodeName).equals("B") + o(updated[0].children[0].dom.nextSibling).equals(root.childNodes[1]) + o(updated[1].children[0].dom.nodeName).equals("I") + o(updated[1].children[0].dom).equals(root.childNodes[2]) }) o("throws if fragment followed by null then el on first render keyed", function() { - var vnodes = [m.fragment({key: 1}), null, m("i", {key: 2})] + var vnodes = [m.key(1), null, m.key(2, m("i"))] o(function () { render(root, vnodes) }).throws(TypeError) }) o("throws if fragment followed by null then el on next render keyed", function() { - var vnodes = [m.fragment({key: 1}), m("i", {key: 2})] - var updated = [m.fragment({key: 1}, m("a"), m("b")), null, m("i", {key: 2})] + var vnodes = [m.key(1), m.key(2, m("i"))] + var updated = [m.key(1, m("a"), m("b")), null, m.key(2, m("i"))] render(root, vnodes) o(function () { render(root, updated) }).throws(TypeError) }) o("populates childless fragment replaced followed by el keyed", function() { - var vnodes = [m.fragment({key: 1}), m("i", {key: 2})] - var updated = [m.fragment({key: 1}, m("a"), m("b")), m("i", {key: 2})] + var vnodes = [m.key(1), m.key(2, m("i"))] + var updated = [m.key(1, m("a"), m("b")), m.key(2, m("i"))] render(root, vnodes) render(root, updated) o(root.childNodes.length).equals(3) - o(updated[0].dom.nodeName).equals("A") - o(updated[0].dom).equals(root.childNodes[0]) - o(updated[0].domSize).equals(2) - o(updated[0].dom.nextSibling.nodeName).equals("B") - o(updated[0].dom.nextSibling).equals(root.childNodes[1]) - o(updated[1].dom.nodeName).equals("I") - o(updated[1].dom).equals(root.childNodes[2]) + o(updated[0].children[0].dom.nodeName).equals("A") + o(updated[0].children[0].dom).equals(root.childNodes[0]) + o(updated[0].children[0].dom.nextSibling.nodeName).equals("B") + o(updated[0].children[0].dom.nextSibling).equals(root.childNodes[1]) + o(updated[1].children[0].dom.nodeName).equals("I") + o(updated[1].children[0].dom).equals(root.childNodes[2]) }) o("throws if childless fragment replaced followed by null then el keyed", function() { - var vnodes = [m.fragment({key: 1}), m("i", {key: 2})] - var updated = [m.fragment({key: 1}, m("a"), m("b")), null, m("i", {key: 2})] + var vnodes = [m.key(1), m.key(2, m("i"))] + var updated = [m.key(1, m("a"), m("b")), null, m.key(2, m("i"))] render(root, vnodes) o(function () { render(root, updated) }).throws(TypeError) }) o("moves from end to start", function() { - var vnodes = [m("a", {key: 1}), m("b", {key: 2}), m("i", {key: 3}), m("s", {key: 4})] - var updated = [m("s", {key: 4}), m("a", {key: 1}), m("b", {key: 2}), m("i", {key: 3})] + var vnodes = [m.key(1, m("a")), m.key(2, m("b")), m.key(3, m("i")), m.key(4, m("s"))] + var updated = [m.key(4, m("s")), m.key(1, m("a")), m.key(2, m("b")), m.key(3, m("i"))] render(root, vnodes) render(root, updated) @@ -367,8 +361,8 @@ o.spec("updateNodes", function() { o(updated[3].dom).equals(root.childNodes[3]) }) o("moves from start to end", function() { - var vnodes = [m("a", {key: 1}), m("b", {key: 2}), m("i", {key: 3}), m("s", {key: 4})] - var updated = [m("b", {key: 2}), m("i", {key: 3}), m("s", {key: 4}), m("a", {key: 1})] + var vnodes = [m.key(1, m("a")), m.key(2, m("b")), m.key(3, m("i")), m.key(4, m("s"))] + var updated = [m.key(2, m("b")), m.key(3, m("i")), m.key(4, m("s")), m.key(1, m("a"))] render(root, vnodes) render(root, updated) @@ -384,9 +378,9 @@ o.spec("updateNodes", function() { o(updated[3].dom).equals(root.childNodes[3]) }) o("removes then recreate", function() { - var vnodes = [m("a", {key: 1}), m("b", {key: 2}), m("i", {key: 3}), m("s", {key: 4})] + var vnodes = [m.key(1, m("a")), m.key(2, m("b")), m.key(3, m("i")), m.key(4, m("s"))] var temp = [] - var updated = [m("a", {key: 1}), m("b", {key: 2}), m("i", {key: 3}), m("s", {key: 4})] + var updated = [m.key(1, m("a")), m.key(2, m("b")), m.key(3, m("i")), m.key(4, m("s"))] render(root, vnodes) render(root, temp) @@ -403,9 +397,9 @@ o.spec("updateNodes", function() { o(updated[3].dom).equals(root.childNodes[3]) }) o("removes then recreate reversed", function() { - var vnodes = [m("a", {key: 1}), m("b", {key: 2}), m("i", {key: 3}), m("s", {key: 4})] + var vnodes = [m.key(1, m("a")), m.key(2, m("b")), m.key(3, m("i")), m.key(4, m("s"))] var temp = [] - var updated = [m("s", {key: 4}), m("i", {key: 3}), m("b", {key: 2}), m("a", {key: 1})] + var updated = [m.key(4, m("s")), m.key(3, m("i")), m.key(2, m("b")), m.key(1, m("a"))] render(root, vnodes) render(root, temp) @@ -422,9 +416,9 @@ o.spec("updateNodes", function() { o(updated[3].dom).equals(root.childNodes[3]) }) o("removes then recreate smaller", function() { - var vnodes = [m("a", {key: 1}), m("b", {key: 2})] + var vnodes = [m.key(1, m("a")), m.key(2, m("b"))] var temp = [] - var updated = [m("a", {key: 1})] + var updated = [m.key(1, m("a"))] render(root, vnodes) render(root, temp) @@ -435,9 +429,9 @@ o.spec("updateNodes", function() { o(updated[0].dom).equals(root.childNodes[0]) }) o("removes then recreate bigger", function() { - var vnodes = [m("a", {key: 1}), m("b", {key: 2})] + var vnodes = [m.key(1, m("a")), m.key(2, m("b"))] var temp = [] - var updated = [m("a", {key: 1}), m("b", {key: 2}), m("i", {key: 3})] + var updated = [m.key(1, m("a")), m.key(2, m("b")), m.key(3, m("i"))] render(root, vnodes) render(root, temp) @@ -452,9 +446,9 @@ o.spec("updateNodes", function() { o(updated[2].dom).equals(root.childNodes[2]) }) o("removes then create different", function() { - var vnodes = [m("a", {key: 1}), m("b", {key: 2})] + var vnodes = [m.key(1, m("a")), m.key(2, m("b"))] var temp = [] - var updated = [m("i", {key: 3}), m("s", {key: 4})] + var updated = [m.key(3, m("i")), m.key(4, m("s"))] render(root, vnodes) render(root, temp) @@ -467,9 +461,9 @@ o.spec("updateNodes", function() { o(updated[1].dom).equals(root.childNodes[1]) }) o("removes then create different smaller", function() { - var vnodes = [m("a", {key: 1}), m("b", {key: 2})] + var vnodes = [m.key(1, m("a")), m.key(2, m("b"))] var temp = [] - var updated = [m("i", {key: 3})] + var updated = [m.key(3, m("i"))] render(root, vnodes) render(root, temp) @@ -480,10 +474,10 @@ o.spec("updateNodes", function() { o(updated[0].dom).equals(root.childNodes[0]) }) o("cached keyed nodes move when the list is reversed", function(){ - var a = m("a", {key: "a"}) - var b = m("b", {key: "b"}) - var c = m("c", {key: "c"}) - var d = m("d", {key: "d"}) + var a = m.key("a", m("a")) + var b = m.key("b", m("b")) + var c = m.key("c", m("c")) + var d = m.key("d", m("d")) render(root, [a, b, c, d]) render(root, [d, c, b, a]) @@ -496,10 +490,10 @@ o.spec("updateNodes", function() { }) o("cached keyed nodes move when diffed via the map", function() { var onupdate = o.spy() - var a = m("a", {key: "a", onupdate: onupdate}) - var b = m("b", {key: "b", onupdate: onupdate}) - var c = m("c", {key: "c", onupdate: onupdate}) - var d = m("d", {key: "d", onupdate: onupdate}) + var a = m.key("a", m("a", {onupdate})) + var b = m.key("b", m("b", {onupdate})) + var c = m.key("c", m("c", {onupdate})) + var d = m.key("d", m("d", {onupdate})) render(root, [a, b, c, d]) render(root, [b, d, a, c]) @@ -512,9 +506,9 @@ o.spec("updateNodes", function() { o(onupdate.callCount).equals(0) }) o("removes then create different bigger", function() { - var vnodes = [m("a", {key: 1}), m("b", {key: 2})] + var vnodes = [m.key(1, m("a")), m.key(2, m("b"))] var temp = [] - var updated = [m("i", {key: 3}), m("s", {key: 4}), m("div", {key: 5})] + var updated = [m.key(3, m("i")), m.key(4, m("s")), m.key(5, m("div"))] render(root, vnodes) render(root, temp) @@ -529,9 +523,9 @@ o.spec("updateNodes", function() { o(updated[2].dom).equals(root.childNodes[2]) }) o("removes then create mixed", function() { - var vnodes = [m("a", {key: 1}), m("b", {key: 2})] + var vnodes = [m.key(1, m("a")), m.key(2, m("b"))] var temp = [] - var updated = [m("a", {key: 1}), m("s", {key: 4})] + var updated = [m.key(1, m("a")), m.key(4, m("s"))] render(root, vnodes) render(root, temp) @@ -544,9 +538,9 @@ o.spec("updateNodes", function() { o(updated[1].dom).equals(root.childNodes[1]) }) o("removes then create mixed reversed", function() { - var vnodes = [m("a", {key: 1}), m("b", {key: 2})] + var vnodes = [m.key(1, m("a")), m.key(2, m("b"))] var temp = [] - var updated = [m("s", {key: 4}), m("a", {key: 1})] + var updated = [m.key(4, m("s")), m.key(1, m("a"))] render(root, vnodes) render(root, temp) @@ -559,9 +553,9 @@ o.spec("updateNodes", function() { o(updated[1].dom).equals(root.childNodes[1]) }) o("removes then create mixed smaller", function() { - var vnodes = [m("a", {key: 1}), m("b", {key: 2}), m("i", {key: 3})] + var vnodes = [m.key(1, m("a")), m.key(2, m("b")), m.key(3, m("i"))] var temp = [] - var updated = [m("a", {key: 1}), m("s", {key: 4})] + var updated = [m.key(1, m("a")), m.key(4, m("s"))] render(root, vnodes) render(root, temp) @@ -574,9 +568,9 @@ o.spec("updateNodes", function() { o(updated[1].dom).equals(root.childNodes[1]) }) o("removes then create mixed smaller reversed", function() { - var vnodes = [m("a", {key: 1}), m("b", {key: 2}), m("i", {key: 3})] + var vnodes = [m.key(1, m("a")), m.key(2, m("b")), m.key(3, m("i"))] var temp = [] - var updated = [m("s", {key: 4}), m("a", {key: 1})] + var updated = [m.key(4, m("s")), m.key(1, m("a"))] render(root, vnodes) render(root, temp) @@ -589,9 +583,9 @@ o.spec("updateNodes", function() { o(updated[1].dom).equals(root.childNodes[1]) }) o("removes then create mixed bigger", function() { - var vnodes = [m("a", {key: 1}), m("b", {key: 2})] + var vnodes = [m.key(1, m("a")), m.key(2, m("b"))] var temp = [] - var updated = [m("a", {key: 1}), m("i", {key: 3}), m("s", {key: 4})] + var updated = [m.key(1, m("a")), m.key(3, m("i")), m.key(4, m("s"))] render(root, vnodes) render(root, temp) @@ -606,9 +600,9 @@ o.spec("updateNodes", function() { o(updated[2].dom).equals(root.childNodes[2]) }) o("removes then create mixed bigger reversed", function() { - var vnodes = [m("a", {key: 1}), m("b", {key: 2})] + var vnodes = [m.key(1, m("a")), m.key(2, m("b"))] var temp = [] - var updated = [m("s", {key: 4}), m("i", {key: 3}), m("a", {key: 1})] + var updated = [m.key(4, m("s")), m.key(3, m("i")), m.key(1, m("a"))] render(root, vnodes) render(root, temp) @@ -639,10 +633,10 @@ o.spec("updateNodes", function() { o(root.firstChild.childNodes.length).equals(1) }) o("removes then recreates then reverses children", function() { - var vnodes = [m("a", {key: 1}, m("i", {key: 3}), m("s", {key: 4})), m("b", {key: 2})] + var vnodes = [m.key(1, m("a", m.key(3, m("i")), m.key(4, m("s")))), m.key(2, m("b"))] var temp1 = [] - var temp2 = [m("a", {key: 1}, m("i", {key: 3}), m("s", {key: 4})), m("b", {key: 2})] - var updated = [m("a", {key: 1}, m("s", {key: 4}), m("i", {key: 3})), m("b", {key: 2})] + var temp2 = [m.key(1, m("a", m.key(3, m("i")), m.key(4, m("s")))), m.key(2, m("b"))] + var updated = [m.key(1, m("a", m.key(4, m("s")), m.key(3, m("i")))), m.key(2, m("b"))] render(root, vnodes) render(root, temp1) @@ -659,9 +653,9 @@ o.spec("updateNodes", function() { o(updated[0].dom.childNodes[1].nodeName).equals("I") }) o("removes then recreates nested", function() { - var vnodes = [m("a", {key: 1}, m("a", {key: 3}, m("a", {key: 5})), m("a", {key: 4}, m("a", {key: 5}))), m("a", {key: 2})] + var vnodes = [m.key(1, m("a", m.key(3, m("a", m.key(5, m("a")))), m.key(4, m("a", m.key(5, m("a")))))), m.key(2, m("a"))] var temp = [] - var updated = [m("a", {key: 1}, m("a", {key: 3}, m("a", {key: 5})), m("a", {key: 4}, m("a", {key: 5}))), m("a", {key: 2})] + var updated = [m.key(1, m("a", m.key(3, m("a", m.key(5, m("a")))), m.key(4, m("a", m.key(5, m("a")))))), m.key(2, m("a"))] render(root, vnodes) render(root, temp) @@ -674,9 +668,9 @@ o.spec("updateNodes", function() { o(root.childNodes[1].childNodes.length).equals(0) }) o("doesn't recycle", function() { - var vnodes = [m("div", {key: 1})] + var vnodes = [m.key(1, m("div"))] var temp = [] - var updated = [m("div", {key: 1})] + var updated = [m.key(1, m("div"))] render(root, vnodes) render(root, temp) @@ -699,9 +693,9 @@ o.spec("updateNodes", function() { o(updated[0].dom.nodeName).equals("DIV") }) o("doesn't recycle deep", function() { - var vnodes = [m("div", m("a", {key: 1}))] + var vnodes = [m("div", m.key(1, m("a")))] var temp = [m("div")] - var updated = [m("div", m("a", {key: 1}))] + var updated = [m("div", m.key(1, m("a")))] render(root, vnodes) @@ -740,17 +734,25 @@ o.spec("updateNodes", function() { o(root.childNodes[1].nodeName).equals("B") }) o("onremove doesn't fire from nodes in the pool (#1990)", function () { - var onremove = o.spy() + var onremove1 = o.spy() + var onremove2 = o.spy() + render(root, [ - m("div", m("div", {onremove: onremove})), - m("div", m("div", {onremove: onremove})) + m("div", m("div", {onremove: onremove1})), + m("div", m("div", {onremove: onremove2})) ]) + o(onremove1.callCount).equals(0) + o(onremove2.callCount).equals(0) + render(root, [ - m("div", m("div", {onremove: onremove})) + m("div", m("div", {onremove: onremove1})) ]) - render(root,[]) + o(onremove1.callCount).equals(0) + o(onremove2.callCount).equals(1) - o(onremove.callCount).equals(2) + render(root,[]) + o(onremove1.callCount).equals(1) + o(onremove2.callCount).equals(1) }) o("cached, non-keyed nodes skip diff", function () { var onupdate = o.spy(); @@ -763,7 +765,7 @@ o.spec("updateNodes", function() { }) o("cached, keyed nodes skip diff", function () { var onupdate = o.spy() - var cached = m("a", {key: "a", onupdate: onupdate}) + var cached = m.key("a", m("a", {onupdate})) render(root, cached) render(root, cached) @@ -773,9 +775,9 @@ o.spec("updateNodes", function() { o("keyed cached elements are re-initialized when brought back from the pool (#2003)", function () { var onupdate = o.spy() var oncreate = o.spy() - var cached = m("B", {key: 1}, + var cached = m.key(1, m("B", m("A", {oncreate: oncreate, onupdate: onupdate}, "A") - ) + )) render(root, m("div", cached)) render(root, []) render(root, m("div", cached)) @@ -801,9 +803,9 @@ o.spec("updateNodes", function() { o("keyed cached elements are re-initialized when brought back from nested pools (#2003)", function () { var onupdate = o.spy() var oncreate = o.spy() - var cached = m("B", {key: 1}, + var cached = m.key(1, m("B", m("A", {oncreate: oncreate, onupdate: onupdate}, "A") - ) + )) render(root, m("div", cached)) render(root, m("div")) render(root, []) @@ -867,7 +869,7 @@ o.spec("updateNodes", function() { o(remove.callCount).equals(0) }) o("node is recreated if key changes to undefined", function () { - var vnode = m("b", {key: 1}) + var vnode = m.key(1, m("b")) var updated = m("b") render(root, vnode) @@ -927,8 +929,8 @@ o.spec("updateNodes", function() { }) o("supports changing the element of a keyed element in a list when traversed bottom-up", function() { try { - render(root, [m("a", {key: 2})]) - render(root, [m("b", {key: 1}), m("b", {key: 2})]) + render(root, [m.key(2, m("a"))]) + render(root, [m.key(1, m("b")), m.key(2, m("b"))]) o(root.childNodes.length).equals(2) o(root.childNodes[0].nodeName).equals("B") @@ -939,8 +941,8 @@ o.spec("updateNodes", function() { }) o("supports changing the element of a keyed element in a list when looking up nodes using the map", function() { try { - render(root, [m("x", {key: 1}), m("y", {key: 2}), m("z", {key: 3})]) - render(root, [m("b", {key: 2}), m("c", {key: 1}), m("d", {key: 4}), m("e", {key: 3})]) + render(root, [m.key(1, m("x")), m.key(2, m("y")), m.key(3, m("z"))]) + render(root, [m.key(2, m("b")), m.key(1, m("c")), m.key(4, m("d")), m.key(3, m("e"))]) o(root.childNodes.length).equals(4) o(root.childNodes[0].nodeName).equals("B") @@ -952,16 +954,16 @@ o.spec("updateNodes", function() { } }) o("don't fetch the nextSibling from the pool", function() { - render(root, [m.fragment(m("div", {key: 1}), m("div", {key: 2})), m("p")]) + render(root, [m.fragment(m.key(1, m("div")), m.key(2, m("div"))), m("p")]) render(root, [m.fragment(), m("p")]) - render(root, [m.fragment(m("div", {key: 2}), m("div", {key: 1})), m("p")]) + render(root, [m.fragment(m.key(2, m("div")), m.key(1, m("div"))), m("p")]) o([].map.call(root.childNodes, function(el) {return el.nodeName})).deepEquals(["DIV", "DIV", "P"]) }) o("minimizes DOM operations when scrambling a keyed lists", function() { var vnodes = vnodify("a,b,c,d") var updated = vnodify("b,a,d,c") - var expectedTagNames = updated.map(function(vn) {return vn.tag}) + var expectedTagNames = updated.map(function(vn) {return vn.children[0].tag}) render(root, vnodes) @@ -978,7 +980,7 @@ o.spec("updateNodes", function() { o("minimizes DOM operations when reversing a keyed lists with an odd number of items", function() { var vnodes = vnodify("a,b,c,d") var updated = vnodify("d,c,b,a") - var expectedTagNames = updated.map(function(vn) {return vn.tag}) + var expectedTagNames = updated.map(function(vn) {return vn.children[0].tag}) render(root, vnodes) @@ -995,9 +997,9 @@ o.spec("updateNodes", function() { o("minimizes DOM operations when reversing a keyed lists with an even number of items", function() { var vnodes = vnodify("a,b,c") var updated = vnodify("c,b,a") - var vnodes = [m("a", {key: "a"}), m("b", {key: "b"}), m("c", {key: "c"})] - var updated = [m("c", {key: "c"}), m("b", {key: "b"}), m("a", {key: "a"})] - var expectedTagNames = updated.map(function(vn) {return vn.tag}) + var vnodes = [m.key("a", m("a")), m.key("b", m("b")), m.key("c", m("c"))] + var updated = [m.key("c", m("c")), m.key("b", m("b")), m.key("a", m("a"))] + var expectedTagNames = updated.map(function(vn) {return vn.children[0].tag}) render(root, vnodes) @@ -1014,7 +1016,7 @@ o.spec("updateNodes", function() { o("minimizes DOM operations when scrambling a keyed lists with prefixes and suffixes", function() { var vnodes = vnodify("i,a,b,c,d,j") var updated = vnodify("i,b,a,d,c,j") - var expectedTagNames = updated.map(function(vn) {return vn.tag}) + var expectedTagNames = updated.map(function(vn) {return vn.children[0].tag}) render(root, vnodes) @@ -1031,7 +1033,7 @@ o.spec("updateNodes", function() { o("minimizes DOM operations when reversing a keyed lists with an odd number of items with prefixes and suffixes", function() { var vnodes = vnodify("i,a,b,c,d,j") var updated = vnodify("i,d,c,b,a,j") - var expectedTagNames = updated.map(function(vn) {return vn.tag}) + var expectedTagNames = updated.map(function(vn) {return vn.children[0].tag}) render(root, vnodes) @@ -1048,7 +1050,7 @@ o.spec("updateNodes", function() { o("minimizes DOM operations when reversing a keyed lists with an even number of items with prefixes and suffixes", function() { var vnodes = vnodify("i,a,b,c,j") var updated = vnodify("i,c,b,a,j") - var expectedTagNames = updated.map(function(vn) {return vn.tag}) + var expectedTagNames = updated.map(function(vn) {return vn.children[0].tag}) render(root, vnodes) @@ -1065,7 +1067,7 @@ o.spec("updateNodes", function() { o("scrambling sample 1", function() { var vnodes = vnodify("k0,k1,k2,k3,k4,k5,k6,k7,k8,k9") var updated = vnodify("k4,k1,k2,k9,k0,k3,k6,k5,k8,k7") - var expectedTagNames = updated.map(function(vn) {return vn.tag}) + var expectedTagNames = updated.map(function(vn) {return vn.children[0].tag}) render(root, vnodes) @@ -1082,7 +1084,7 @@ o.spec("updateNodes", function() { o("scrambling sample 2", function() { var vnodes = vnodify("k0,k1,k2,k3,k4,k5,k6,k7,k8,k9") var updated = vnodify("b,d,k1,k0,k2,k3,k4,a,c,k5,k6,k7,k8,k9") - var expectedTagNames = updated.map(function(vn) {return vn.tag}) + var expectedTagNames = updated.map(function(vn) {return vn.children[0].tag}) render(root, vnodes) diff --git a/render/tests/test-updateNodesFuzzer.js b/render/tests/test-updateNodesFuzzer.js index 0155258a7..dea06aeef 100644 --- a/render/tests/test-updateNodesFuzzer.js +++ b/render/tests/test-updateNodesFuzzer.js @@ -27,9 +27,9 @@ o.spec("updateNodes keyed list Fuzzer", function() { while (tests--) { var test = fuzzTest(c.delMax, c.movMax, c.insMax) o(i++ + ": " + test.list.join() + " -> " + test.updated.join(), function() { - render(root, test.list.map(function(x){return m(x, {key: x})})) + render(root, test.list.map((x) => m.key(x, m(x)))) addSpies(root) - render(root, test.updated.map(function(x){return m(x, {key: x})})) + render(root, test.updated.map((x) => m.key(x, m(x)))) if (root.appendChild.callCount + root.insertBefore.callCount !== test.expected.creations + test.expected.moves) console.log(test, {aC: root.appendChild.callCount, iB: root.insertBefore.callCount}, [].map.call(root.childNodes, function(n){return n.nodeName.toLowerCase()})) diff --git a/render/vnode.js b/render/vnode.js index e3c68092e..42efd216c 100644 --- a/render/vnode.js +++ b/render/vnode.js @@ -1,26 +1,28 @@ "use strict" function Vnode(tag, key, attrs, children) { - return {tag, key, attrs, children, dom: undefined, domSize: undefined, state: undefined, events: undefined, instance: undefined} + return {tag, key, attrs, children, dom: undefined, state: undefined, events: undefined, instance: undefined} } Vnode.normalize = function(node) { - if (Array.isArray(node)) return Vnode("[", undefined, undefined, Vnode.normalizeChildren(node)) if (node == null || typeof node === "boolean") return null - if (typeof node === "object") return node - return Vnode("#", undefined, undefined, String(node)) + if (typeof node !== "object") return Vnode("#", undefined, undefined, String(node)) + if (Array.isArray(node)) return Vnode("[", undefined, undefined, Vnode.normalizeChildren(node.slice())) + return node } Vnode.normalizeChildren = function(input) { if (input.length) { - var isKeyed = input[0] != null && input[0].key != null + input[0] = Vnode.normalize(input[0]) + var isKeyed = input[0] != null && input[0].tag === "=" var keys = new Set() // Note: this is a *very* perf-sensitive check. // Fun fact: merging the loop like this is somehow faster than splitting // it, noticeably so. for (var i = 1; i < input.length; i++) { - if ((input[i] != null && input[i].key != null) !== isKeyed) { + input[i] = Vnode.normalize(input[i]) + if ((input[i] != null && input[i].tag === "=") !== isKeyed) { throw new TypeError( - isKeyed && (input[i] != null || typeof input[i] === "boolean") - ? "In fragments, vnodes must either all have keys or none have keys. You may wish to consider using an explicit keyed empty fragment, m.fragment({key: ...}), instead of a hole." + isKeyed + ? "In fragments, vnodes must either all have keys or none have keys. You may wish to consider using an explicit empty key vnode, `m.key()`, instead of a hole." : "In fragments, vnodes must either all have keys or none have keys." ) } @@ -31,7 +33,6 @@ Vnode.normalizeChildren = function(input) { keys.add(input[i].key) } } - input = input.map(Vnode.normalize) } return input } diff --git a/tests/test-api.js b/tests/test-api.js index d619b9c20..f982ae182 100644 --- a/tests/test-api.js +++ b/tests/test-api.js @@ -12,6 +12,11 @@ o.spec("api", function() { global.window = mock global.requestAnimationFrame = mock.requestAnimationFrame } + + function sleep(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)) + } + var m = require("..") // eslint-disable-line global-require o.afterEach(function() { @@ -27,9 +32,18 @@ o.spec("api", function() { }) o.spec("m.fragment", function() { o("works", function() { - var vnode = m.fragment({key: 123}, [m("div")]) + var vnode = m.fragment([m("div")]) o(vnode.tag).equals("[") + o(vnode.children.length).equals(1) + o(vnode.children[0].tag).equals("div") + }) + }) + o.spec("m.key", function() { + o("works", function() { + var vnode = m.key(123, [m("div")]) + + o(vnode.tag).equals("=") o(vnode.key).equals(123) o(vnode.children.length).equals(1) o(vnode.children[0].tag).equals("div") @@ -77,76 +91,63 @@ o.spec("api", function() { }) }) o.spec("m.route", function() { - o("works", function(done) { + o("works", function() { root = window.document.createElement("div") m.route(root, "/a", { "/a": createComponent({view: function() {return m("div")}}) }) - setTimeout(function() { + return sleep(FRAME_BUDGET + 10).then(() => { o(root.childNodes.length).equals(1) o(root.firstChild.nodeName).equals("DIV") - - done() - }, FRAME_BUDGET) + }) }) - o("m.route.prefix", function(done) { + o("m.route.prefix", function() { root = window.document.createElement("div") m.route.prefix = "#" m.route(root, "/a", { "/a": createComponent({view: function() {return m("div")}}) }) - setTimeout(function() { + return sleep(FRAME_BUDGET + 10).then(() => { o(root.childNodes.length).equals(1) o(root.firstChild.nodeName).equals("DIV") - - done() - }, FRAME_BUDGET) + }) }) - o("m.route.get", function(done) { + o("m.route.get", function() { root = window.document.createElement("div") m.route(root, "/a", { "/a": createComponent({view: function() {return m("div")}}) }) - setTimeout(function() { + return sleep(FRAME_BUDGET + 10).then(() => { o(m.route.get()).equals("/a") - - done() - }, FRAME_BUDGET) + }) }) - o("m.route.set", function(done) { + o("m.route.set", function() { o.timeout(100) root = window.document.createElement("div") m.route(root, "/a", { "/:id": createComponent({view: function() {return m("div")}}) }) - setTimeout(function() { - m.route.set("/b") - setTimeout(function() { - o(m.route.get()).equals("/b") - - done() - }, FRAME_BUDGET) - }, FRAME_BUDGET) + return sleep(FRAME_BUDGET + 10) + .then(() => { m.route.set("/b") }) + .then(() => sleep(FRAME_BUDGET + 10)) + .then(() => { o(m.route.get()).equals("/b") }) }) }) o.spec("m.redraw", function() { - o("works", function(done) { + o("works", function() { var count = 0 root = window.document.createElement("div") m.mount(root, createComponent({view: function() {count++}})) o(count).equals(1) m.redraw() o(count).equals(1) - setTimeout(function() { - + return sleep(FRAME_BUDGET + 10).then(() => { o(count).equals(2) - - done() - }, FRAME_BUDGET) + }) }) o("sync", function() { root = window.document.createElement("div") diff --git a/util/censor.js b/util/censor.js index f21ce0d0f..8a548ab2a 100644 --- a/util/censor.js +++ b/util/censor.js @@ -24,21 +24,20 @@ // ``` var hasOwn = require("./hasOwn") -// Words in RegExp literals are sometimes mangled incorrectly by the internal bundler, so use RegExp(). -var magic = new RegExp("^(?:key|oninit|oncreate|onbeforeupdate|onupdate|onbeforeremove|onremove)$") +var magic = new Set(["oninit", "oncreate", "onbeforeupdate", "onupdate", "onbeforeremove", "onremove"]) module.exports = function(attrs, extras) { var result = {} if (extras != null) { for (var key in attrs) { - if (hasOwn.call(attrs, key) && !magic.test(key) && extras.indexOf(key) < 0) { + if (hasOwn.call(attrs, key) && !magic.has(key) && extras.indexOf(key) < 0) { result[key] = attrs[key] } } } else { for (var key in attrs) { - if (hasOwn.call(attrs, key) && !magic.test(key)) { + if (hasOwn.call(attrs, key) && !magic.has(key)) { result[key] = attrs[key] } } diff --git a/util/tests/test-censor.js b/util/tests/test-censor.js index 18500e202..a5e054916 100644 --- a/util/tests/test-censor.js +++ b/util/tests/test-censor.js @@ -32,7 +32,7 @@ o.spec("censor", function() { } var censored = censor(original) o(censored).notEquals(original) - o(censored).deepEquals({one: "two"}) + o(censored).deepEquals({one: "two", key: "test"}) }) o("does not modify original object", function() { var original = { @@ -87,7 +87,7 @@ o.spec("censor", function() { } var censored = censor(original, null) o(censored).notEquals(original) - o(censored).deepEquals({one: "two"}) + o(censored).deepEquals({one: "two", key: "test"}) }) o("does not modify original object", function() { var original = { @@ -142,7 +142,7 @@ o.spec("censor", function() { } var censored = censor(original, ["extra"]) o(censored).notEquals(original) - o(censored).deepEquals({one: "two"}) + o(censored).deepEquals({one: "two", key: "test"}) }) o("does not modify original object", function() { var original = { @@ -207,7 +207,7 @@ o.spec("censor", function() { } var censored = censor(original, ["extra"]) o(censored).notEquals(original) - o(censored).deepEquals({one: "two"}) + o(censored).deepEquals({one: "two", key: "test"}) }) o("does not modify original object", function() { var original = { From 2707de322057f472cf6b189783f14f5a3bc54229 Mon Sep 17 00:00:00 2001 From: Claudia Meadows Date: Tue, 1 Oct 2024 22:02:45 -0700 Subject: [PATCH 09/95] Use the `.state` property for keys --- render/hyperscript.js | 4 ++-- render/render.js | 14 +++++++------- render/tests/test-fragment.js | 4 ++-- render/vnode.js | 10 +++++----- tests/test-api.js | 2 +- 5 files changed, 17 insertions(+), 17 deletions(-) diff --git a/render/hyperscript.js b/render/hyperscript.js index 6660b76ec..9457086c3 100644 --- a/render/hyperscript.js +++ b/render/hyperscript.js @@ -86,7 +86,7 @@ function hyperscript(selector, attrs, ...children) { if (selector !== "[") return execSelector(selector, attrs, children) } - return Vnode(selector, null, attrs, children) + return Vnode(selector, undefined, attrs, children) } hyperscript.fragment = function(...args) { @@ -97,7 +97,7 @@ hyperscript.key = function(key, ...children) { if (children.length === 1 && Array.isArray(children[0])) { children = children[0].slice() } - return Vnode("=", key, null, Vnode.normalizeChildren(children)) + return Vnode("=", key, undefined, Vnode.normalizeChildren(children)) } module.exports = hyperscript diff --git a/render/render.js b/render/render.js index 48fc64347..d49a548e6 100644 --- a/render/render.js +++ b/render/render.js @@ -64,7 +64,7 @@ module.exports = function() { function createNode(parent, vnode, hooks, ns, nextSibling) { var tag = vnode.tag if (typeof tag === "string") { - vnode.state = {} + if (vnode.tag !== "=") vnode.state = {} if (vnode.attrs != null) initLifecycle(vnode.attrs, vnode, hooks) switch (tag) { case "#": createText(parent, vnode, nextSibling); break @@ -271,7 +271,7 @@ module.exports = function() { while (oldEnd >= oldStart && end >= start) { oe = old[oldEnd] ve = vnodes[end] - if (oe.key !== ve.key) break + if (oe.state !== ve.state) break if (oe !== ve) updateNode(parent, oe, ve, hooks, nextSibling, ns, pathDepth) if (ve.dom != null) nextSibling = ve.dom oldEnd--, end-- @@ -280,14 +280,14 @@ module.exports = function() { while (oldEnd >= oldStart && end >= start) { o = old[oldStart] v = vnodes[start] - if (o.key !== v.key) break + if (o.state !== v.state) break oldStart++, start++ if (o !== v) updateNode(parent, o, v, hooks, getNextSibling(old, oldStart, nextSibling), ns, pathDepth) } // swaps and list reversals while (oldEnd >= oldStart && end >= start) { if (start === end) break - if (o.key !== ve.key || oe.key !== v.key) break + if (o.state !== ve.state || oe.state !== v.state) break topSibling = getNextSibling(old, oldStart, nextSibling) moveDOM(parent, oe, topSibling) if (oe !== v) updateNode(parent, oe, v, hooks, topSibling, ns, pathDepth) @@ -302,7 +302,7 @@ module.exports = function() { } // bottom up once again while (oldEnd >= oldStart && end >= start) { - if (oe.key !== ve.key) break + if (oe.state !== ve.state) break if (oe !== ve) updateNode(parent, oe, ve, hooks, nextSibling, ns, pathDepth) if (ve.dom != null) nextSibling = ve.dom oldEnd--, end-- @@ -318,7 +318,7 @@ module.exports = function() { for (i = end; i >= start; i--) { if (map == null) map = getKeyMap(old, oldStart, oldEnd + 1) ve = vnodes[i] - var oldIndex = map[ve.key] + var oldIndex = map[ve.state] if (oldIndex != null) { pos = (oldIndex < pos) ? oldIndex : -1 // becomes -1 if nodes were re-ordered oldIndices[i-start] = oldIndex @@ -434,7 +434,7 @@ module.exports = function() { for (; start < end; start++) { var vnode = vnodes[start] if (vnode != null) { - map[vnode.key] = start + map[vnode.state] = start } } return map diff --git a/render/tests/test-fragment.js b/render/tests/test-fragment.js index a2006020c..bfab1ae78 100644 --- a/render/tests/test-fragment.js +++ b/render/tests/test-fragment.js @@ -110,7 +110,7 @@ o.spec("key", function() { o(frag.children.length).equals(1) o(frag.children[0]).equals(child) - o(frag.key).equals(undefined) + o(frag.state).equals(undefined) }) o("supports non-null keys", function() { var frag = m.key(7, []) @@ -119,7 +119,7 @@ o.spec("key", function() { o(Array.isArray(frag.children)).equals(true) o(frag.children.length).equals(0) - o(frag.key).equals(7) + o(frag.state).equals(7) }) o.spec("children", function() { o("handles string single child", function() { diff --git a/render/vnode.js b/render/vnode.js index 42efd216c..47cbd23f0 100644 --- a/render/vnode.js +++ b/render/vnode.js @@ -1,7 +1,7 @@ "use strict" -function Vnode(tag, key, attrs, children) { - return {tag, key, attrs, children, dom: undefined, state: undefined, events: undefined, instance: undefined} +function Vnode(tag, state, attrs, children) { + return {tag, state, attrs, children, dom: undefined, events: undefined, instance: undefined} } Vnode.normalize = function(node) { if (node == null || typeof node === "boolean") return null @@ -27,10 +27,10 @@ Vnode.normalizeChildren = function(input) { ) } if (isKeyed) { - if (keys.has(input[i].key)) { - throw new TypeError(`Duplicate key detected: ${input[i].key}`) + if (keys.has(input[i].state)) { + throw new TypeError(`Duplicate key detected: ${input[i].state}`) } - keys.add(input[i].key) + keys.add(input[i].state) } } } diff --git a/tests/test-api.js b/tests/test-api.js index f982ae182..13d2b9abf 100644 --- a/tests/test-api.js +++ b/tests/test-api.js @@ -44,7 +44,7 @@ o.spec("api", function() { var vnode = m.key(123, [m("div")]) o(vnode.tag).equals("=") - o(vnode.key).equals(123) + o(vnode.state).equals(123) o(vnode.children.length).equals(1) o(vnode.children[0].tag).equals("div") }) From ae28e2aeb94fc61d266fd9616802cbedbae05e88 Mon Sep 17 00:00:00 2001 From: Claudia Meadows Date: Tue, 1 Oct 2024 22:05:10 -0700 Subject: [PATCH 10/95] Move the DOM event listener to `vnode.instance` --- render/render.js | 22 +++++++++++----------- render/tests/test-onremove.js | 2 +- render/vnode.js | 2 +- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/render/render.js b/render/render.js index d49a548e6..58fd5e59b 100644 --- a/render/render.js +++ b/render/render.js @@ -363,7 +363,7 @@ module.exports = function() { var oldTag = old.tag, tag = vnode.tag if (oldTag === tag) { vnode.state = old.state - vnode.events = old.events + vnode.instance = old.instance if (shouldNotUpdate(vnode, old)) return vnodePath[pathDepth++] = parent vnodePath[pathDepth++] = vnode @@ -849,20 +849,20 @@ module.exports = function() { //event function updateEvent(vnode, key, value) { - if (vnode.events != null) { - vnode.events._ = currentRedraw - if (vnode.events[key] === value) return + if (vnode.instance != null) { + vnode.instance._ = currentRedraw + if (vnode.instance[key] === value) return if (value != null && (typeof value === "function" || typeof value === "object")) { - if (vnode.events[key] == null) vnode.dom.addEventListener(key.slice(2), vnode.events, false) - vnode.events[key] = value + if (vnode.instance[key] == null) vnode.dom.addEventListener(key.slice(2), vnode.instance, false) + vnode.instance[key] = value } else { - if (vnode.events[key] != null) vnode.dom.removeEventListener(key.slice(2), vnode.events, false) - vnode.events[key] = undefined + if (vnode.instance[key] != null) vnode.dom.removeEventListener(key.slice(2), vnode.instance, false) + vnode.instance[key] = undefined } } else if (value != null && (typeof value === "function" || typeof value === "object")) { - vnode.events = new EventDict() - vnode.dom.addEventListener(key.slice(2), vnode.events, false) - vnode.events[key] = value + vnode.instance = new EventDict() + vnode.dom.addEventListener(key.slice(2), vnode.instance, false) + vnode.instance[key] = value } } diff --git a/render/tests/test-onremove.js b/render/tests/test-onremove.js index bb0d753a7..cb7133b93 100644 --- a/render/tests/test-onremove.js +++ b/render/tests/test-onremove.js @@ -67,7 +67,7 @@ o.spec("onremove", function() { o(vnode.dom.onremove).equals(undefined) o(vnode.dom.attributes["onremove"]).equals(undefined) - o(vnode.events).equals(undefined) + o(vnode.instance).equals(undefined) }) o("calls onremove on keyed nodes", function() { var remove = o.spy() diff --git a/render/vnode.js b/render/vnode.js index 47cbd23f0..ed9fce9a2 100644 --- a/render/vnode.js +++ b/render/vnode.js @@ -1,7 +1,7 @@ "use strict" function Vnode(tag, state, attrs, children) { - return {tag, state, attrs, children, dom: undefined, events: undefined, instance: undefined} + return {tag, state, attrs, children, dom: undefined, instance: undefined} } Vnode.normalize = function(node) { if (node == null || typeof node === "boolean") return null From 7aca843768e28b50cf8cb98a2c6147185711e651 Mon Sep 17 00:00:00 2001 From: Claudia Meadows Date: Tue, 1 Oct 2024 22:07:47 -0700 Subject: [PATCH 11/95] Move state object creation to hyperscript It's simpler that way and I don't have to condition it. --- render/hyperscript.js | 4 ++-- render/render.js | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/render/hyperscript.js b/render/hyperscript.js index 9457086c3..623bd56b4 100644 --- a/render/hyperscript.js +++ b/render/hyperscript.js @@ -62,7 +62,7 @@ function execSelector(selector, attrs, children) { if (hasClassName) attrs.className = null } - return Vnode(state.tag, null, attrs, children) + return Vnode(state.tag, {}, attrs, children) } // Caution is advised when editing this - it's very perf-critical. It's specially designed to avoid @@ -86,7 +86,7 @@ function hyperscript(selector, attrs, ...children) { if (selector !== "[") return execSelector(selector, attrs, children) } - return Vnode(selector, undefined, attrs, children) + return Vnode(selector, {}, attrs, children) } hyperscript.fragment = function(...args) { diff --git a/render/render.js b/render/render.js index 58fd5e59b..1a08e409e 100644 --- a/render/render.js +++ b/render/render.js @@ -64,7 +64,6 @@ module.exports = function() { function createNode(parent, vnode, hooks, ns, nextSibling) { var tag = vnode.tag if (typeof tag === "string") { - if (vnode.tag !== "=") vnode.state = {} if (vnode.attrs != null) initLifecycle(vnode.attrs, vnode, hooks) switch (tag) { case "#": createText(parent, vnode, nextSibling); break From b72490241bdc869018b5e470a3ccd5bd412bf26a Mon Sep 17 00:00:00 2001 From: Claudia Meadows Date: Tue, 1 Oct 2024 22:19:02 -0700 Subject: [PATCH 12/95] Move `m.render` out of initialization closure It's redundant and has been for ages. A v3 increment gives me the ability to minimize surprise. --- api/mount-redraw.js | 3 +- api/tests/test-mountRedraw.js | 3 +- api/tests/test-router.js | 12 +- api/tests/test-routerGetSet.js | 3 +- mount-redraw.js | 4 +- render.js | 2 +- render/render.js | 1654 +++++++++-------- render/tests/test-attributes.js | 10 +- render/tests/test-component.js | 5 +- render/tests/test-createElement.js | 5 +- render/tests/test-createFragment.js | 5 +- render/tests/test-createNodes.js | 5 +- render/tests/test-createText.js | 5 +- render/tests/test-event.js | 5 +- render/tests/test-input.js | 6 +- .../tests/test-normalizeComponentChildren.js | 3 +- render/tests/test-onbeforeremove.js | 5 +- render/tests/test-onbeforeupdate.js | 5 +- render/tests/test-oncreate.js | 5 +- render/tests/test-oninit.js | 5 +- render/tests/test-onremove.js | 5 +- render/tests/test-onupdate.js | 5 +- .../test-render-hyperscript-integration.js | 5 +- render/tests/test-render.js | 9 +- render/tests/test-textContent.js | 5 +- render/tests/test-updateElement.js | 5 +- render/tests/test-updateFragment.js | 5 +- render/tests/test-updateNodes.js | 5 +- render/tests/test-updateNodesFuzzer.js | 5 +- render/tests/test-updateText.js | 5 +- 30 files changed, 882 insertions(+), 922 deletions(-) diff --git a/api/mount-redraw.js b/api/mount-redraw.js index 67ec4076b..44862c00e 100644 --- a/api/mount-redraw.js +++ b/api/mount-redraw.js @@ -1,8 +1,9 @@ "use strict" var Vnode = require("../render/vnode") +var render = require("../render/render") -module.exports = function(render, schedule, console) { +module.exports = function(schedule, console) { var subscriptions = [] var pending = false var offset = -1 diff --git a/api/tests/test-mountRedraw.js b/api/tests/test-mountRedraw.js index 4ebc0655e..90b40f195 100644 --- a/api/tests/test-mountRedraw.js +++ b/api/tests/test-mountRedraw.js @@ -6,7 +6,6 @@ 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() { @@ -16,7 +15,7 @@ o.spec("mount/redraw", function() { consoleMock = {error: o.spy()} throttleMock = throttleMocker() root = $window.document.body - m = mountRedraw(coreRenderer($window), throttleMock.schedule, consoleMock) + m = mountRedraw(throttleMock.schedule, consoleMock) $document = $window.document errors = [] }) diff --git a/api/tests/test-router.js b/api/tests/test-router.js index de4a64ed2..c6b07cd42 100644 --- a/api/tests/test-router.js +++ b/api/tests/test-router.js @@ -6,7 +6,7 @@ var browserMock = require("../../test-utils/browserMock") var throttleMocker = require("../../test-utils/throttleMock") var m = require("../../render/hyperscript") -var coreRenderer = require("../../render/render") +var render = require("../../render/render") var apiMountRedraw = require("../../api/mount-redraw") var apiRouter = require("../../api/router") @@ -79,7 +79,7 @@ o.spec("route", function() { root = $window.document.body - mountRedraw = apiMountRedraw(coreRenderer($window), throttleMock.schedule, console) + mountRedraw = apiMountRedraw(throttleMock.schedule, console) route = apiRouter($window, mountRedraw) route.prefix = prefix console.error = function() { @@ -603,7 +603,6 @@ o.spec("route", function() { o("route.Link can render without routes or dom access", function() { $window = browserMock(env) - var render = coreRenderer($window) route = apiRouter(null, null) route.prefix = prefix root = $window.document.body @@ -623,7 +622,6 @@ o.spec("route", function() { o("route.Link keeps magic attributes from being double-called", function() { $window = browserMock(env) - var render = coreRenderer($window) route = apiRouter(null, null) route.prefix = prefix root = $window.document.body @@ -681,7 +679,6 @@ o.spec("route", function() { o("route.Link can render other tag without routes or dom access", function() { $window = browserMock(env) - var render = coreRenderer($window) route = apiRouter(null, null) route.prefix = prefix root = $window.document.body @@ -701,7 +698,6 @@ o.spec("route", function() { o("route.Link can render other selector without routes or dom access", function() { $window = browserMock(env) - var render = coreRenderer($window) route = apiRouter(null, null) route.prefix = prefix root = $window.document.body @@ -721,7 +717,6 @@ o.spec("route", function() { o("route.Link can render not disabled", function() { $window = browserMock(env) - var render = coreRenderer($window) route = apiRouter(null, null) route.prefix = prefix root = $window.document.body @@ -741,7 +736,6 @@ o.spec("route", function() { o("route.Link can render falsy disabled", function() { $window = browserMock(env) - var render = coreRenderer($window) route = apiRouter(null, null) route.prefix = prefix root = $window.document.body @@ -761,7 +755,6 @@ o.spec("route", function() { o("route.Link can render disabled", function() { $window = browserMock(env) - var render = coreRenderer($window) route = apiRouter(null, null) route.prefix = prefix root = $window.document.body @@ -781,7 +774,6 @@ o.spec("route", function() { o("route.Link can render truthy disabled", function() { $window = browserMock(env) - var render = coreRenderer($window) route = apiRouter(null, null) route.prefix = prefix root = $window.document.body diff --git a/api/tests/test-routerGetSet.js b/api/tests/test-routerGetSet.js index 059d20054..719f040b7 100644 --- a/api/tests/test-routerGetSet.js +++ b/api/tests/test-routerGetSet.js @@ -6,7 +6,6 @@ var browserMock = require("../../test-utils/browserMock") var throttleMocker = require("../../test-utils/throttleMock") var apiMountRedraw = require("../../api/mount-redraw") -var coreRenderer = require("../../render/render") var apiRouter = require("../../api/router") o.spec("route.get/route.set", function() { @@ -26,7 +25,7 @@ o.spec("route.get/route.set", function() { root = $window.document.body - mountRedraw = apiMountRedraw(coreRenderer($window), throttleMock.schedule, console) + mountRedraw = apiMountRedraw(throttleMock.schedule, console) route = apiRouter($window, mountRedraw) route.prefix = prefix }) diff --git a/mount-redraw.js b/mount-redraw.js index bb83aede4..d3b68f4ab 100644 --- a/mount-redraw.js +++ b/mount-redraw.js @@ -1,5 +1,3 @@ "use strict" -var render = require("./render") - -module.exports = require("./api/mount-redraw")(render, typeof requestAnimationFrame !== "undefined" ? requestAnimationFrame : null, typeof console !== "undefined" ? console : null) +module.exports = require("./api/mount-redraw")(typeof requestAnimationFrame !== "undefined" ? requestAnimationFrame : null, typeof console !== "undefined" ? console : null) diff --git a/render.js b/render.js index 042d78125..1d4046fea 100644 --- a/render.js +++ b/render.js @@ -1,3 +1,3 @@ "use strict" -module.exports = require("./render/render")(typeof window !== "undefined" ? window : null) +module.exports = require("./render/render") diff --git a/render/render.js b/render/render.js index 1a08e409e..ee42593f7 100644 --- a/render/render.js +++ b/render/render.js @@ -2,933 +2,937 @@ var Vnode = require("../render/vnode") -module.exports = function() { - var xlinkNs = "http://www.w3.org/1999/xlink" - var nameSpace = { - svg: "http://www.w3.org/2000/svg", - math: "http://www.w3.org/1998/Math/MathML" - } +var xlinkNs = "http://www.w3.org/1999/xlink" +var nameSpace = { + svg: "http://www.w3.org/2000/svg", + math: "http://www.w3.org/1998/Math/MathML" +} - // The vnode path is needed for proper removal unblocking. It's not retained past a given - // render and is overwritten on every vnode visit, so callers wanting to retain it should - // always clone the part they're interested in. - var vnodePath - var blockedRemovalRefCount = /*@__PURE__*/new WeakMap() - var removalRequested = /*@__PURE__*/new WeakSet() - var currentRedraw +// The vnode path is needed for proper removal unblocking. It's not retained past a given +// render and is overwritten on every vnode visit, so callers wanting to retain it should +// always clone the part they're interested in. +var vnodePath +var blockedRemovalRefCount = /*@__PURE__*/new WeakMap() +var removalRequested = /*@__PURE__*/new WeakSet() +var currentRedraw - function getDocument(dom) { - return dom.ownerDocument; - } +function getDocument(dom) { + return dom.ownerDocument; +} - function getNameSpace(vnode) { - return vnode.attrs && vnode.attrs.xmlns || nameSpace[vnode.tag] - } +function getNameSpace(vnode) { + return vnode.attrs && vnode.attrs.xmlns || nameSpace[vnode.tag] +} - //sanity check to discourage people from doing `vnode.state = ...` - function checkState(vnode, original) { - if (vnode.state !== original) throw new Error("'vnode.state' must not be modified.") - } +//sanity check to discourage people from doing `vnode.state = ...` +function checkState(vnode, original) { + if (vnode.state !== original) throw new Error("'vnode.state' must not be modified.") +} - //Note: the hook is passed as the `this` argument to allow proxying the - //arguments without requiring a full array allocation to do so. It also - //takes advantage of the fact the current `vnode` is the first argument in - //all lifecycle methods. - function callHook(vnode) { - var original = vnode.state - try { - return this.apply(original, arguments) - } finally { - checkState(vnode, original) - } +//Note: the hook is passed as the `this` argument to allow proxying the +//arguments without requiring a full array allocation to do so. It also +//takes advantage of the fact the current `vnode` is the first argument in +//all lifecycle methods. +function callHook(vnode) { + var original = vnode.state + try { + return this.apply(original, arguments) + } finally { + checkState(vnode, original) } +} - // IE11 (at least) throws an UnspecifiedError when accessing document.activeElement when - // inside an iframe. Catch and swallow this error, and heavy-handidly return null. - function activeElement(dom) { - try { - return getDocument(dom).activeElement - } catch (e) { - return null - } +// IE11 (at least) throws an UnspecifiedError when accessing document.activeElement when +// inside an iframe. Catch and swallow this error, and heavy-handidly return null. +function activeElement(dom) { + try { + return getDocument(dom).activeElement + } catch (e) { + return null } - //create - function createNodes(parent, vnodes, start, end, hooks, nextSibling, ns) { - for (var i = start; i < end; i++) { - var vnode = vnodes[i] - if (vnode != null) { - createNode(parent, vnode, hooks, ns, nextSibling) - } +} +//create +function createNodes(parent, vnodes, start, end, hooks, nextSibling, ns) { + for (var i = start; i < end; i++) { + var vnode = vnodes[i] + if (vnode != null) { + createNode(parent, vnode, hooks, ns, nextSibling) } } - function createNode(parent, vnode, hooks, ns, nextSibling) { - var tag = vnode.tag - if (typeof tag === "string") { - if (vnode.attrs != null) initLifecycle(vnode.attrs, vnode, hooks) - switch (tag) { - case "#": createText(parent, vnode, nextSibling); break - case "=": - case "[": createFragment(parent, vnode, hooks, ns, nextSibling); break - default: createElement(parent, vnode, hooks, ns, nextSibling) - } +} +function createNode(parent, vnode, hooks, ns, nextSibling) { + var tag = vnode.tag + if (typeof tag === "string") { + if (vnode.attrs != null) initLifecycle(vnode.attrs, vnode, hooks) + switch (tag) { + case "#": createText(parent, vnode, nextSibling); break + case "=": + case "[": createFragment(parent, vnode, hooks, ns, nextSibling); break + default: createElement(parent, vnode, hooks, ns, nextSibling) } - else createComponent(parent, vnode, hooks, ns, nextSibling) } - function createText(parent, vnode, nextSibling) { - vnode.dom = getDocument(parent).createTextNode(vnode.children) - insertDOM(parent, vnode.dom, nextSibling) - } - function createFragment(parent, vnode, hooks, ns, nextSibling) { - var fragment = getDocument(parent).createDocumentFragment() - if (vnode.children != null) { - var children = vnode.children - createNodes(fragment, children, 0, children.length, hooks, null, ns) - } - vnode.dom = fragment.firstChild - insertDOM(parent, fragment, nextSibling) + else createComponent(parent, vnode, hooks, ns, nextSibling) +} +function createText(parent, vnode, nextSibling) { + vnode.dom = getDocument(parent).createTextNode(vnode.children) + insertDOM(parent, vnode.dom, nextSibling) +} +function createFragment(parent, vnode, hooks, ns, nextSibling) { + var fragment = getDocument(parent).createDocumentFragment() + if (vnode.children != null) { + var children = vnode.children + createNodes(fragment, children, 0, children.length, hooks, null, ns) } - function createElement(parent, vnode, hooks, ns, nextSibling) { - var tag = vnode.tag - var attrs = vnode.attrs - var is = attrs && attrs.is + vnode.dom = fragment.firstChild + insertDOM(parent, fragment, nextSibling) +} +function createElement(parent, vnode, hooks, ns, nextSibling) { + var tag = vnode.tag + var attrs = vnode.attrs + var is = attrs && attrs.is - ns = getNameSpace(vnode) || ns + ns = getNameSpace(vnode) || ns - var element = ns ? - is ? getDocument(parent).createElementNS(ns, tag, {is: is}) : getDocument(parent).createElementNS(ns, tag) : - is ? getDocument(parent).createElement(tag, {is: is}) : getDocument(parent).createElement(tag) - vnode.dom = element + var element = ns ? + is ? getDocument(parent).createElementNS(ns, tag, {is: is}) : getDocument(parent).createElementNS(ns, tag) : + is ? getDocument(parent).createElement(tag, {is: is}) : getDocument(parent).createElement(tag) + vnode.dom = element - if (attrs != null) { - setAttrs(vnode, attrs, ns) - } + if (attrs != null) { + setAttrs(vnode, attrs, ns) + } - insertDOM(parent, element, nextSibling) + insertDOM(parent, element, nextSibling) - if (!maybeSetContentEditable(vnode)) { - if (vnode.children != null) { - var children = vnode.children - createNodes(element, children, 0, children.length, hooks, null, ns) - if (vnode.tag === "select" && attrs != null) setLateSelectAttrs(vnode, attrs) - } + if (!maybeSetContentEditable(vnode)) { + if (vnode.children != null) { + var children = vnode.children + createNodes(element, children, 0, children.length, hooks, null, ns) + if (vnode.tag === "select" && attrs != null) setLateSelectAttrs(vnode, attrs) } } - var reentrantLock = new WeakSet() - function initComponent(vnode, hooks) { - vnode.state = void 0 - if (reentrantLock.has(vnode.tag)) return - reentrantLock.add(vnode.tag) - vnode.state = (vnode.tag.prototype != null && typeof vnode.tag.prototype.view === "function") ? new vnode.tag(vnode) : vnode.tag(vnode) - initLifecycle(vnode.state, vnode, hooks) - if (vnode.attrs != null) initLifecycle(vnode.attrs, vnode, hooks) - vnode.instance = Vnode.normalize(callHook.call(vnode.state.view, vnode)) - if (vnode.instance === vnode) throw Error("A view cannot return the vnode it received as argument") - reentrantLock.delete(vnode.tag) - } - function createComponent(parent, vnode, hooks, ns, nextSibling) { - initComponent(vnode, hooks) - if (vnode.instance != null) { - createNode(parent, vnode.instance, hooks, ns, nextSibling) - vnode.dom = vnode.instance.dom - } +} +var reentrantLock = new WeakSet() +function initComponent(vnode, hooks) { + vnode.state = void 0 + if (reentrantLock.has(vnode.tag)) return + reentrantLock.add(vnode.tag) + vnode.state = (vnode.tag.prototype != null && typeof vnode.tag.prototype.view === "function") ? new vnode.tag(vnode) : vnode.tag(vnode) + initLifecycle(vnode.state, vnode, hooks) + if (vnode.attrs != null) initLifecycle(vnode.attrs, vnode, hooks) + vnode.instance = Vnode.normalize(callHook.call(vnode.state.view, vnode)) + if (vnode.instance === vnode) throw Error("A view cannot return the vnode it received as argument") + reentrantLock.delete(vnode.tag) +} +function createComponent(parent, vnode, hooks, ns, nextSibling) { + initComponent(vnode, hooks) + if (vnode.instance != null) { + createNode(parent, vnode.instance, hooks, ns, nextSibling) + vnode.dom = vnode.instance.dom } +} - //update - /** - * @param {Element|Fragment} parent - the parent element - * @param {Vnode[] | null} old - the list of vnodes of the last `render()` call for - * this part of the tree - * @param {Vnode[] | null} vnodes - as above, but for the current `render()` call. - * @param {Function[]} hooks - an accumulator of post-render hooks (oncreate/onupdate) - * @param {Element | null} nextSibling - the next DOM node if we're dealing with a - * fragment that is not the last item in its - * parent - * @param {'svg' | 'math' | String | null} ns) - the current XML namespace, if any - * @returns void - */ - // This function diffs and patches lists of vnodes, both keyed and unkeyed. - // - // We will: - // - // 1. describe its general structure - // 2. focus on the diff algorithm optimizations - // 3. discuss DOM node operations. - - // ## Overview: - // - // The updateNodes() function: - // - deals with trivial cases - // - determines whether the lists are keyed or unkeyed based on the first non-null node - // of each list. - // - diffs them and patches the DOM if needed (that's the brunt of the code) - // - manages the leftovers: after diffing, are there: - // - old nodes left to remove? - // - new nodes to insert? - // deal with them! - // - // The lists are only iterated over once, with an exception for the nodes in `old` that - // are visited in the fourth part of the diff and in the `removeNodes` loop. - - // ## Diffing - // - // Reading https://github.com/localvoid/ivi/blob/ddc09d06abaef45248e6133f7040d00d3c6be853/packages/ivi/src/vdom/implementation.ts#L617-L837 - // may be good for context on longest increasing subsequence-based logic for moving nodes. - // - // In order to diff keyed lists, one has to - // - // 1) match nodes in both lists, per key, and update them accordingly - // 2) create the nodes present in the new list, but absent in the old one - // 3) remove the nodes present in the old list, but absent in the new one - // 4) figure out what nodes in 1) to move in order to minimize the DOM operations. - // - // To achieve 1) one can create a dictionary of keys => index (for the old list), then iterate - // over the new list and for each new vnode, find the corresponding vnode in the old list using - // the map. - // 2) is achieved in the same step: if a new node has no corresponding entry in the map, it is new - // and must be created. - // For the removals, we actually remove the nodes that have been updated from the old list. - // The nodes that remain in that list after 1) and 2) have been performed can be safely removed. - // The fourth step is a bit more complex and relies on the longest increasing subsequence (LIS) - // algorithm. - // - // the longest increasing subsequence is the list of nodes that can remain in place. Imagine going - // from `1,2,3,4,5` to `4,5,1,2,3` where the numbers are not necessarily the keys, but the indices - // corresponding to the keyed nodes in the old list (keyed nodes `e,d,c,b,a` => `b,a,e,d,c` would - // match the above lists, for example). - // - // In there are two increasing subsequences: `4,5` and `1,2,3`, the latter being the longest. We - // can update those nodes without moving them, and only call `insertNode` on `4` and `5`. - // - // @localvoid adapted the algo to also support node deletions and insertions (the `lis` is actually - // the longest increasing subsequence *of old nodes still present in the new list*). - // - // It is a general algorithm that is fireproof in all circumstances, but it requires the allocation - // and the construction of a `key => oldIndex` map, and three arrays (one with `newIndex => oldIndex`, - // the `LIS` and a temporary one to create the LIS). - // - // So we cheat where we can: if the tails of the lists are identical, they are guaranteed to be part of - // the LIS and can be updated without moving them. - // - // If two nodes are swapped, they are guaranteed not to be part of the LIS, and must be moved (with - // the exception of the last node if the list is fully reversed). - // - // ## Finding the next sibling. - // - // `updateNode()` and `createNode()` expect a nextSibling parameter to perform DOM operations. - // When the list is being traversed top-down, at any index, the DOM nodes up to the previous - // vnode reflect the content of the new list, whereas the rest of the DOM nodes reflect the old - // list. The next sibling must be looked for in the old list using `getNextSibling(... oldStart + 1 ...)`. - // - // In the other scenarios (swaps, upwards traversal, map-based diff), - // the new vnodes list is traversed upwards. The DOM nodes at the bottom of the list reflect the - // bottom part of the new vnodes list, and we can use the `v.dom` value of the previous node - // as the next sibling (cached in the `nextSibling` variable). - - - // ## DOM node moves - // - // In most scenarios `updateNode()` and `createNode()` perform the DOM operations. However, - // this is not the case if the node moved (second and fourth part of the diff algo). We move - // the old DOM nodes before updateNode runs because it enables us to use the cached `nextSibling` - // variable rather than fetching it using `getNextSibling()`. - - function updateNodes(parent, old, vnodes, hooks, nextSibling, ns, pathDepth) { - if (old === vnodes || old == null && vnodes == null) return - else if (old == null || old.length === 0) createNodes(parent, vnodes, 0, vnodes.length, hooks, nextSibling, ns) - else if (vnodes == null || vnodes.length === 0) removeNodes(parent, old, 0, old.length, pathDepth, false) - else { - var isOldKeyed = old[0] != null && old[0].tag === "=" - var isKeyed = vnodes[0] != null && vnodes[0].tag === "=" - var start = 0, oldStart = 0 - if (!isOldKeyed) while (oldStart < old.length && old[oldStart] == null) oldStart++ - if (!isKeyed) while (start < vnodes.length && vnodes[start] == null) start++ - if (isOldKeyed !== isKeyed) { - removeNodes(parent, old, oldStart, old.length, pathDepth, false) - createNodes(parent, vnodes, start, vnodes.length, hooks, nextSibling, ns) - } else if (!isKeyed) { - // Don't index past the end of either list (causes deopts). - var commonLength = old.length < vnodes.length ? old.length : vnodes.length - // Rewind if necessary to the first non-null index on either side. - // We could alternatively either explicitly create or remove nodes when `start !== oldStart` - // but that would be optimizing for sparse lists which are more rare than dense ones. - start = start < oldStart ? start : oldStart - for (; start < commonLength; start++) { - o = old[start] - v = vnodes[start] - if (o === v || o == null && v == null) continue - else if (o == null) createNode(parent, v, hooks, ns, getNextSibling(old, start + 1, nextSibling)) - else if (v == null) removeNode(parent, o, pathDepth, false) - else updateNode(parent, o, v, hooks, getNextSibling(old, start + 1, nextSibling), ns, pathDepth) - } - if (old.length > commonLength) removeNodes(parent, old, start, old.length, pathDepth, false) - if (vnodes.length > commonLength) createNodes(parent, vnodes, start, vnodes.length, hooks, nextSibling, ns) - } else { - // keyed diff - var oldEnd = old.length - 1, end = vnodes.length - 1, map, o, v, oe, ve, topSibling - - // bottom-up - while (oldEnd >= oldStart && end >= start) { - oe = old[oldEnd] - ve = vnodes[end] - if (oe.state !== ve.state) break - if (oe !== ve) updateNode(parent, oe, ve, hooks, nextSibling, ns, pathDepth) - if (ve.dom != null) nextSibling = ve.dom - oldEnd--, end-- - } - // top-down - while (oldEnd >= oldStart && end >= start) { - o = old[oldStart] - v = vnodes[start] - if (o.state !== v.state) break - oldStart++, start++ - if (o !== v) updateNode(parent, o, v, hooks, getNextSibling(old, oldStart, nextSibling), ns, pathDepth) - } - // swaps and list reversals - while (oldEnd >= oldStart && end >= start) { - if (start === end) break - if (o.state !== ve.state || oe.state !== v.state) break - topSibling = getNextSibling(old, oldStart, nextSibling) - moveDOM(parent, oe, topSibling) - if (oe !== v) updateNode(parent, oe, v, hooks, topSibling, ns, pathDepth) - if (++start <= --end) moveDOM(parent, o, nextSibling) - if (o !== ve) updateNode(parent, o, ve, hooks, nextSibling, ns, pathDepth) - if (ve.dom != null) nextSibling = ve.dom - oldStart++; oldEnd-- - oe = old[oldEnd] - ve = vnodes[end] - o = old[oldStart] - v = vnodes[start] - } - // bottom up once again - while (oldEnd >= oldStart && end >= start) { - if (oe.state !== ve.state) break - if (oe !== ve) updateNode(parent, oe, ve, hooks, nextSibling, ns, pathDepth) - if (ve.dom != null) nextSibling = ve.dom - oldEnd--, end-- - oe = old[oldEnd] - ve = vnodes[end] +//update +/** + * @param {Element|Fragment} parent - the parent element + * @param {Vnode[] | null} old - the list of vnodes of the last `render()` call for + * this part of the tree + * @param {Vnode[] | null} vnodes - as above, but for the current `render()` call. + * @param {Function[]} hooks - an accumulator of post-render hooks (oncreate/onupdate) + * @param {Element | null} nextSibling - the next DOM node if we're dealing with a + * fragment that is not the last item in its + * parent + * @param {'svg' | 'math' | String | null} ns) - the current XML namespace, if any + * @returns void + */ +// This function diffs and patches lists of vnodes, both keyed and unkeyed. +// +// We will: +// +// 1. describe its general structure +// 2. focus on the diff algorithm optimizations +// 3. discuss DOM node operations. + +// ## Overview: +// +// The updateNodes() function: +// - deals with trivial cases +// - determines whether the lists are keyed or unkeyed based on the first non-null node +// of each list. +// - diffs them and patches the DOM if needed (that's the brunt of the code) +// - manages the leftovers: after diffing, are there: +// - old nodes left to remove? +// - new nodes to insert? +// deal with them! +// +// The lists are only iterated over once, with an exception for the nodes in `old` that +// are visited in the fourth part of the diff and in the `removeNodes` loop. + +// ## Diffing +// +// Reading https://github.com/localvoid/ivi/blob/ddc09d06abaef45248e6133f7040d00d3c6be853/packages/ivi/src/vdom/implementation.ts#L617-L837 +// may be good for context on longest increasing subsequence-based logic for moving nodes. +// +// In order to diff keyed lists, one has to +// +// 1) match nodes in both lists, per key, and update them accordingly +// 2) create the nodes present in the new list, but absent in the old one +// 3) remove the nodes present in the old list, but absent in the new one +// 4) figure out what nodes in 1) to move in order to minimize the DOM operations. +// +// To achieve 1) one can create a dictionary of keys => index (for the old list), then iterate +// over the new list and for each new vnode, find the corresponding vnode in the old list using +// the map. +// 2) is achieved in the same step: if a new node has no corresponding entry in the map, it is new +// and must be created. +// For the removals, we actually remove the nodes that have been updated from the old list. +// The nodes that remain in that list after 1) and 2) have been performed can be safely removed. +// The fourth step is a bit more complex and relies on the longest increasing subsequence (LIS) +// algorithm. +// +// the longest increasing subsequence is the list of nodes that can remain in place. Imagine going +// from `1,2,3,4,5` to `4,5,1,2,3` where the numbers are not necessarily the keys, but the indices +// corresponding to the keyed nodes in the old list (keyed nodes `e,d,c,b,a` => `b,a,e,d,c` would +// match the above lists, for example). +// +// In there are two increasing subsequences: `4,5` and `1,2,3`, the latter being the longest. We +// can update those nodes without moving them, and only call `insertNode` on `4` and `5`. +// +// @localvoid adapted the algo to also support node deletions and insertions (the `lis` is actually +// the longest increasing subsequence *of old nodes still present in the new list*). +// +// It is a general algorithm that is fireproof in all circumstances, but it requires the allocation +// and the construction of a `key => oldIndex` map, and three arrays (one with `newIndex => oldIndex`, +// the `LIS` and a temporary one to create the LIS). +// +// So we cheat where we can: if the tails of the lists are identical, they are guaranteed to be part of +// the LIS and can be updated without moving them. +// +// If two nodes are swapped, they are guaranteed not to be part of the LIS, and must be moved (with +// the exception of the last node if the list is fully reversed). +// +// ## Finding the next sibling. +// +// `updateNode()` and `createNode()` expect a nextSibling parameter to perform DOM operations. +// When the list is being traversed top-down, at any index, the DOM nodes up to the previous +// vnode reflect the content of the new list, whereas the rest of the DOM nodes reflect the old +// list. The next sibling must be looked for in the old list using `getNextSibling(... oldStart + 1 ...)`. +// +// In the other scenarios (swaps, upwards traversal, map-based diff), +// the new vnodes list is traversed upwards. The DOM nodes at the bottom of the list reflect the +// bottom part of the new vnodes list, and we can use the `v.dom` value of the previous node +// as the next sibling (cached in the `nextSibling` variable). + + +// ## DOM node moves +// +// In most scenarios `updateNode()` and `createNode()` perform the DOM operations. However, +// this is not the case if the node moved (second and fourth part of the diff algo). We move +// the old DOM nodes before updateNode runs because it enables us to use the cached `nextSibling` +// variable rather than fetching it using `getNextSibling()`. + +function updateNodes(parent, old, vnodes, hooks, nextSibling, ns, pathDepth) { + if (old === vnodes || old == null && vnodes == null) return + else if (old == null || old.length === 0) createNodes(parent, vnodes, 0, vnodes.length, hooks, nextSibling, ns) + else if (vnodes == null || vnodes.length === 0) removeNodes(parent, old, 0, old.length, pathDepth, false) + else { + var isOldKeyed = old[0] != null && old[0].tag === "=" + var isKeyed = vnodes[0] != null && vnodes[0].tag === "=" + var start = 0, oldStart = 0 + if (!isOldKeyed) while (oldStart < old.length && old[oldStart] == null) oldStart++ + if (!isKeyed) while (start < vnodes.length && vnodes[start] == null) start++ + if (isOldKeyed !== isKeyed) { + removeNodes(parent, old, oldStart, old.length, pathDepth, false) + createNodes(parent, vnodes, start, vnodes.length, hooks, nextSibling, ns) + } else if (!isKeyed) { + // Don't index past the end of either list (causes deopts). + var commonLength = old.length < vnodes.length ? old.length : vnodes.length + // Rewind if necessary to the first non-null index on either side. + // We could alternatively either explicitly create or remove nodes when `start !== oldStart` + // but that would be optimizing for sparse lists which are more rare than dense ones. + start = start < oldStart ? start : oldStart + for (; start < commonLength; start++) { + o = old[start] + v = vnodes[start] + if (o === v || o == null && v == null) continue + else if (o == null) createNode(parent, v, hooks, ns, getNextSibling(old, start + 1, nextSibling)) + else if (v == null) removeNode(parent, o, pathDepth, false) + else updateNode(parent, o, v, hooks, getNextSibling(old, start + 1, nextSibling), ns, pathDepth) + } + if (old.length > commonLength) removeNodes(parent, old, start, old.length, pathDepth, false) + if (vnodes.length > commonLength) createNodes(parent, vnodes, start, vnodes.length, hooks, nextSibling, ns) + } else { + // keyed diff + var oldEnd = old.length - 1, end = vnodes.length - 1, map, o, v, oe, ve, topSibling + + // bottom-up + while (oldEnd >= oldStart && end >= start) { + oe = old[oldEnd] + ve = vnodes[end] + if (oe.state !== ve.state) break + if (oe !== ve) updateNode(parent, oe, ve, hooks, nextSibling, ns, pathDepth) + if (ve.dom != null) nextSibling = ve.dom + oldEnd--, end-- + } + // top-down + while (oldEnd >= oldStart && end >= start) { + o = old[oldStart] + v = vnodes[start] + if (o.state !== v.state) break + oldStart++, start++ + if (o !== v) updateNode(parent, o, v, hooks, getNextSibling(old, oldStart, nextSibling), ns, pathDepth) + } + // swaps and list reversals + while (oldEnd >= oldStart && end >= start) { + if (start === end) break + if (o.state !== ve.state || oe.state !== v.state) break + topSibling = getNextSibling(old, oldStart, nextSibling) + moveDOM(parent, oe, topSibling) + if (oe !== v) updateNode(parent, oe, v, hooks, topSibling, ns, pathDepth) + if (++start <= --end) moveDOM(parent, o, nextSibling) + if (o !== ve) updateNode(parent, o, ve, hooks, nextSibling, ns, pathDepth) + if (ve.dom != null) nextSibling = ve.dom + oldStart++; oldEnd-- + oe = old[oldEnd] + ve = vnodes[end] + o = old[oldStart] + v = vnodes[start] + } + // bottom up once again + while (oldEnd >= oldStart && end >= start) { + if (oe.state !== ve.state) break + if (oe !== ve) updateNode(parent, oe, ve, hooks, nextSibling, ns, pathDepth) + if (ve.dom != null) nextSibling = ve.dom + oldEnd--, end-- + oe = old[oldEnd] + ve = vnodes[end] + } + if (start > end) removeNodes(parent, old, oldStart, oldEnd + 1, pathDepth, false) + else if (oldStart > oldEnd) createNodes(parent, vnodes, start, end + 1, hooks, nextSibling, ns) + else { + // inspired by ivi https://github.com/ivijs/ivi/ by Boris Kaul + var originalNextSibling = nextSibling, vnodesLength = end - start + 1, oldIndices = new Array(vnodesLength), li=0, i=0, pos = 2147483647, matched = 0, map, lisIndices + for (i = 0; i < vnodesLength; i++) oldIndices[i] = -1 + for (i = end; i >= start; i--) { + if (map == null) map = getKeyMap(old, oldStart, oldEnd + 1) + ve = vnodes[i] + var oldIndex = map[ve.state] + if (oldIndex != null) { + pos = (oldIndex < pos) ? oldIndex : -1 // becomes -1 if nodes were re-ordered + oldIndices[i-start] = oldIndex + oe = old[oldIndex] + old[oldIndex] = null + if (oe !== ve) updateNode(parent, oe, ve, hooks, nextSibling, ns, pathDepth) + if (ve.dom != null) nextSibling = ve.dom + matched++ + } } - if (start > end) removeNodes(parent, old, oldStart, oldEnd + 1, pathDepth, false) - else if (oldStart > oldEnd) createNodes(parent, vnodes, start, end + 1, hooks, nextSibling, ns) + nextSibling = originalNextSibling + if (matched !== oldEnd - oldStart + 1) removeNodes(parent, old, oldStart, oldEnd + 1, pathDepth, false) + if (matched === 0) createNodes(parent, vnodes, start, end + 1, hooks, nextSibling, ns) else { - // inspired by ivi https://github.com/ivijs/ivi/ by Boris Kaul - var originalNextSibling = nextSibling, vnodesLength = end - start + 1, oldIndices = new Array(vnodesLength), li=0, i=0, pos = 2147483647, matched = 0, map, lisIndices - for (i = 0; i < vnodesLength; i++) oldIndices[i] = -1 - for (i = end; i >= start; i--) { - if (map == null) map = getKeyMap(old, oldStart, oldEnd + 1) - ve = vnodes[i] - var oldIndex = map[ve.state] - if (oldIndex != null) { - pos = (oldIndex < pos) ? oldIndex : -1 // becomes -1 if nodes were re-ordered - oldIndices[i-start] = oldIndex - oe = old[oldIndex] - old[oldIndex] = null - if (oe !== ve) updateNode(parent, oe, ve, hooks, nextSibling, ns, pathDepth) - if (ve.dom != null) nextSibling = ve.dom - matched++ - } - } - nextSibling = originalNextSibling - if (matched !== oldEnd - oldStart + 1) removeNodes(parent, old, oldStart, oldEnd + 1, pathDepth, false) - if (matched === 0) createNodes(parent, vnodes, start, end + 1, hooks, nextSibling, ns) - else { - if (pos === -1) { - // the indices of the indices of the items that are part of the - // longest increasing subsequence in the oldIndices list - lisIndices = makeLisIndices(oldIndices) - li = lisIndices.length - 1 - for (i = end; i >= start; i--) { - v = vnodes[i] - if (oldIndices[i-start] === -1) createNode(parent, v, hooks, ns, nextSibling) - else { - if (lisIndices[li] === i - start) li-- - else moveDOM(parent, v, nextSibling) - } - if (v.dom != null) nextSibling = vnodes[i].dom - } - } else { - for (i = end; i >= start; i--) { - v = vnodes[i] - if (oldIndices[i-start] === -1) createNode(parent, v, hooks, ns, nextSibling) - if (v.dom != null) nextSibling = vnodes[i].dom + if (pos === -1) { + // the indices of the indices of the items that are part of the + // longest increasing subsequence in the oldIndices list + lisIndices = makeLisIndices(oldIndices) + li = lisIndices.length - 1 + for (i = end; i >= start; i--) { + v = vnodes[i] + if (oldIndices[i-start] === -1) createNode(parent, v, hooks, ns, nextSibling) + else { + if (lisIndices[li] === i - start) li-- + else moveDOM(parent, v, nextSibling) } + if (v.dom != null) nextSibling = vnodes[i].dom + } + } else { + for (i = end; i >= start; i--) { + v = vnodes[i] + if (oldIndices[i-start] === -1) createNode(parent, v, hooks, ns, nextSibling) + if (v.dom != null) nextSibling = vnodes[i].dom } } } } } } - function updateNode(parent, old, vnode, hooks, nextSibling, ns, pathDepth) { - var oldTag = old.tag, tag = vnode.tag - if (oldTag === tag) { - vnode.state = old.state - vnode.instance = old.instance - if (shouldNotUpdate(vnode, old)) return - vnodePath[pathDepth++] = parent - vnodePath[pathDepth++] = vnode - if (typeof oldTag === "string") { - if (vnode.attrs != null) { - updateLifecycle(vnode.attrs, vnode, hooks) - } - switch (oldTag) { - case "#": updateText(old, vnode); break - case "=": - case "[": updateFragment(parent, old, vnode, hooks, nextSibling, ns, pathDepth); break - default: updateElement(old, vnode, hooks, ns, pathDepth) - } +} +function updateNode(parent, old, vnode, hooks, nextSibling, ns, pathDepth) { + var oldTag = old.tag, tag = vnode.tag + if (oldTag === tag) { + vnode.state = old.state + vnode.instance = old.instance + if (shouldNotUpdate(vnode, old)) return + vnodePath[pathDepth++] = parent + vnodePath[pathDepth++] = vnode + if (typeof oldTag === "string") { + if (vnode.attrs != null) { + updateLifecycle(vnode.attrs, vnode, hooks) + } + switch (oldTag) { + case "#": updateText(old, vnode); break + case "=": + case "[": updateFragment(parent, old, vnode, hooks, nextSibling, ns, pathDepth); break + default: updateElement(old, vnode, hooks, ns, pathDepth) } - else updateComponent(parent, old, vnode, hooks, nextSibling, ns, pathDepth) - } - else { - removeNode(parent, old, pathDepth, false) - createNode(parent, vnode, hooks, ns, nextSibling) } + else updateComponent(parent, old, vnode, hooks, nextSibling, ns, pathDepth) } - function updateText(old, vnode) { - if (old.children.toString() !== vnode.children.toString()) { - old.dom.nodeValue = vnode.children - } - vnode.dom = old.dom + else { + removeNode(parent, old, pathDepth, false) + createNode(parent, vnode, hooks, ns, nextSibling) } - function updateFragment(parent, old, vnode, hooks, nextSibling, ns, pathDepth) { - updateNodes(parent, old.children, vnode.children, hooks, nextSibling, ns, pathDepth) - vnode.dom = null - if (vnode.children != null) { - for (var child of vnode.children) { - if (child != null && child.dom != null) { - if (vnode.dom == null) vnode.dom = child.dom - } +} +function updateText(old, vnode) { + if (old.children.toString() !== vnode.children.toString()) { + old.dom.nodeValue = vnode.children + } + vnode.dom = old.dom +} +function updateFragment(parent, old, vnode, hooks, nextSibling, ns, pathDepth) { + updateNodes(parent, old.children, vnode.children, hooks, nextSibling, ns, pathDepth) + vnode.dom = null + if (vnode.children != null) { + for (var child of vnode.children) { + if (child != null && child.dom != null) { + if (vnode.dom == null) vnode.dom = child.dom } } } - function updateElement(old, vnode, hooks, ns, pathDepth) { - var element = vnode.dom = old.dom - ns = getNameSpace(vnode) || ns +} +function updateElement(old, vnode, hooks, ns, pathDepth) { + var element = vnode.dom = old.dom + ns = getNameSpace(vnode) || ns - updateAttrs(vnode, old.attrs, vnode.attrs, ns) - if (!maybeSetContentEditable(vnode)) { - updateNodes(element, old.children, vnode.children, hooks, null, ns, pathDepth) - } + updateAttrs(vnode, old.attrs, vnode.attrs, ns) + if (!maybeSetContentEditable(vnode)) { + updateNodes(element, old.children, vnode.children, hooks, null, ns, pathDepth) } - function updateComponent(parent, old, vnode, hooks, nextSibling, ns, pathDepth) { - vnode.instance = Vnode.normalize(callHook.call(vnode.state.view, vnode)) - if (vnode.instance === vnode) throw Error("A view cannot return the vnode it received as argument") - updateLifecycle(vnode.state, vnode, hooks) - if (vnode.attrs != null) updateLifecycle(vnode.attrs, vnode, hooks) - if (vnode.instance != null) { - if (old.instance == null) createNode(parent, vnode.instance, hooks, ns, nextSibling) - else updateNode(parent, old.instance, vnode.instance, hooks, nextSibling, ns, pathDepth) - vnode.dom = vnode.instance.dom - } - else if (old.instance != null) { - removeNode(parent, old.instance, pathDepth, false) - vnode.dom = undefined - } - else { - vnode.dom = old.dom - } +} +function updateComponent(parent, old, vnode, hooks, nextSibling, ns, pathDepth) { + vnode.instance = Vnode.normalize(callHook.call(vnode.state.view, vnode)) + if (vnode.instance === vnode) throw Error("A view cannot return the vnode it received as argument") + updateLifecycle(vnode.state, vnode, hooks) + if (vnode.attrs != null) updateLifecycle(vnode.attrs, vnode, hooks) + if (vnode.instance != null) { + if (old.instance == null) createNode(parent, vnode.instance, hooks, ns, nextSibling) + else updateNode(parent, old.instance, vnode.instance, hooks, nextSibling, ns, pathDepth) + vnode.dom = vnode.instance.dom + } + else if (old.instance != null) { + removeNode(parent, old.instance, pathDepth, false) + vnode.dom = undefined + } + else { + vnode.dom = old.dom } - function getKeyMap(vnodes, start, end) { - var map = Object.create(null) - for (; start < end; start++) { - var vnode = vnodes[start] - if (vnode != null) { - map[vnode.state] = start - } +} +function getKeyMap(vnodes, start, end) { + var map = Object.create(null) + for (; start < end; start++) { + var vnode = vnodes[start] + if (vnode != null) { + map[vnode.state] = start } - return map - } - // Lifted from ivi https://github.com/ivijs/ivi/ - // takes a list of unique numbers (-1 is special and can - // occur multiple times) and returns an array with the indices - // of the items that are part of the longest increasing - // subsequence - var lisTemp = [] - function makeLisIndices(a) { - var result = [0] - var u = 0, v = 0, i = 0 - var il = lisTemp.length = a.length - for (var i = 0; i < il; i++) lisTemp[i] = a[i] - for (var i = 0; i < il; ++i) { - if (a[i] === -1) continue - var j = result[result.length - 1] - if (a[j] < a[i]) { - lisTemp[i] = j - result.push(i) - continue - } - u = 0 - v = result.length - 1 - while (u < v) { - // Fast integer average without overflow. - // eslint-disable-next-line no-bitwise - var c = (u >>> 1) + (v >>> 1) + (u & v & 1) - if (a[result[c]] < a[i]) { - u = c + 1 - } - else { - v = c - } + } + return map +} +// Lifted from ivi https://github.com/ivijs/ivi/ +// takes a list of unique numbers (-1 is special and can +// occur multiple times) and returns an array with the indices +// of the items that are part of the longest increasing +// subsequence +var lisTemp = [] +function makeLisIndices(a) { + var result = [0] + var u = 0, v = 0, i = 0 + var il = lisTemp.length = a.length + for (var i = 0; i < il; i++) lisTemp[i] = a[i] + for (var i = 0; i < il; ++i) { + if (a[i] === -1) continue + var j = result[result.length - 1] + if (a[j] < a[i]) { + lisTemp[i] = j + result.push(i) + continue + } + u = 0 + v = result.length - 1 + while (u < v) { + // Fast integer average without overflow. + // eslint-disable-next-line no-bitwise + var c = (u >>> 1) + (v >>> 1) + (u & v & 1) + if (a[result[c]] < a[i]) { + u = c + 1 } - if (a[i] < a[result[u]]) { - if (u > 0) lisTemp[i] = result[u - 1] - result[u] = i + else { + v = c } } - u = result.length - v = result[u - 1] - while (u-- > 0) { - result[u] = v - v = lisTemp[v] + if (a[i] < a[result[u]]) { + if (u > 0) lisTemp[i] = result[u - 1] + result[u] = i } - lisTemp.length = 0 - return result } + u = result.length + v = result[u - 1] + while (u-- > 0) { + result[u] = v + v = lisTemp[v] + } + lisTemp.length = 0 + return result +} - function getNextSibling(vnodes, i, nextSibling) { - for (; i < vnodes.length; i++) { - if (vnodes[i] != null && vnodes[i].dom != null) return vnodes[i].dom - } - return nextSibling +function getNextSibling(vnodes, i, nextSibling) { + for (; i < vnodes.length; i++) { + if (vnodes[i] != null && vnodes[i].dom != null) return vnodes[i].dom } + return nextSibling +} - // This handles fragments with zombie children (removed from vdom, but persisted in DOM through onbeforeremove) - function moveDOM(parent, vnode, nextSibling) { - if (typeof vnode.tag === "function") { - return moveDOM(parent, vnode.instance, nextSibling) - } else if (vnode.tag === "[" || vnode.tag === "=") { - if (Array.isArray(vnode.children)) { - for (var child of vnode.children) { - nextSibling = moveDOM(parent, child, nextSibling) - } +// This handles fragments with zombie children (removed from vdom, but persisted in DOM through onbeforeremove) +function moveDOM(parent, vnode, nextSibling) { + if (typeof vnode.tag === "function") { + return moveDOM(parent, vnode.instance, nextSibling) + } else if (vnode.tag === "[" || vnode.tag === "=") { + if (Array.isArray(vnode.children)) { + for (var child of vnode.children) { + nextSibling = moveDOM(parent, child, nextSibling) } - return nextSibling - } else { - insertDOM(parent, vnode.dom, nextSibling) - return vnode.dom } + return nextSibling + } else { + insertDOM(parent, vnode.dom, nextSibling) + return vnode.dom } +} - function insertDOM(parent, dom, nextSibling) { - if (nextSibling != null) parent.insertBefore(dom, nextSibling) - else parent.appendChild(dom) - } +function insertDOM(parent, dom, nextSibling) { + if (nextSibling != null) parent.insertBefore(dom, nextSibling) + else parent.appendChild(dom) +} - function maybeSetContentEditable(vnode) { - if (vnode.attrs == null || ( - vnode.attrs.contenteditable == null && // attribute - vnode.attrs.contentEditable == null // property - )) return false - var children = vnode.children - if (children != null && children.length !== 0) throw new Error("Child node of a contenteditable must be trusted.") - return true - } +function maybeSetContentEditable(vnode) { + if (vnode.attrs == null || ( + vnode.attrs.contenteditable == null && // attribute + vnode.attrs.contentEditable == null // property + )) return false + var children = vnode.children + if (children != null && children.length !== 0) throw new Error("Child node of a contenteditable must be trusted.") + return true +} - //remove - function invokeBeforeRemove(vnode, host) { - try { - if (typeof host.onbeforeremove === "function") { - var result = callHook.call(host.onbeforeremove, vnode) - if (result != null && typeof result.then === "function") return Promise.resolve(result) - } - } catch (e) { - // Errors during removal aren't fatal. Just log them. - console.error(e) +//remove +function invokeBeforeRemove(vnode, host) { + try { + if (typeof host.onbeforeremove === "function") { + var result = callHook.call(host.onbeforeremove, vnode) + if (result != null && typeof result.then === "function") return Promise.resolve(result) } + } catch (e) { + // Errors during removal aren't fatal. Just log them. + console.error(e) + } +} +function tryProcessRemoval(parent, vnode) { + // eslint-disable-next-line no-bitwise + var refCount = blockedRemovalRefCount.get(vnode) | 0 + if (refCount > 1) { + blockedRemovalRefCount.set(vnode, refCount - 1) + return false } - function tryProcessRemoval(parent, vnode) { - // eslint-disable-next-line no-bitwise - var refCount = blockedRemovalRefCount.get(vnode) | 0 - if (refCount > 1) { - blockedRemovalRefCount.set(vnode, refCount - 1) - return false - } - - if (typeof vnode.tag !== "function" && vnode.tag !== "[" && vnode.tag !== "=") { - parent.removeChild(vnode.dom) - } - try { - if (typeof vnode.tag !== "string" && typeof vnode.state.onremove === "function") { - callHook.call(vnode.state.onremove, vnode) - } - } catch (e) { - console.error(e) - } + if (typeof vnode.tag !== "function" && vnode.tag !== "[" && vnode.tag !== "=") { + parent.removeChild(vnode.dom) + } - try { - if (vnode.attrs && typeof vnode.attrs.onremove === "function") { - callHook.call(vnode.attrs.onremove, vnode) - } - } catch (e) { - console.error(e) + try { + if (typeof vnode.tag !== "string" && typeof vnode.state.onremove === "function") { + callHook.call(vnode.state.onremove, vnode) } - - return true + } catch (e) { + console.error(e) } - function removeNodeAsyncRecurse(parent, vnode) { - while (vnode != null) { - // Delay the actual subtree removal if there's still pending `onbeforeremove` hooks on - // this node or a child node. - if (!tryProcessRemoval(parent, vnode)) return false - if (typeof vnode.tag !== "function") { - if (vnode.tag === "#") break - if (vnode.tag !== "[" && vnode.tag !== "=") parent = vnode.dom - // Using bitwise ops and `Array.prototype.reduce` to reduce code size. It's not - // called nearly enough to merit further optimization. - // eslint-disable-next-line no-bitwise - return vnode.children.reduce((fail, child) => fail & removeNodeAsyncRecurse(parent, child), 1) - } - vnode = vnode.instance - } - return true - } - function removeNodes(parent, vnodes, start, end, pathDepth, isDelayed) { - // Using bitwise ops to reduce code size. - var fail = 0 - // eslint-disable-next-line no-bitwise - for (var i = start; i < end; i++) fail |= !removeNode(parent, vnodes[i], pathDepth, isDelayed) - return !fail + try { + if (vnode.attrs && typeof vnode.attrs.onremove === "function") { + callHook.call(vnode.attrs.onremove, vnode) + } + } catch (e) { + console.error(e) } - function removeNode(parent, vnode, pathDepth, isDelayed) { - if (vnode != null) { - delayed: { - var attrsResult, stateResult - - // Block removes, but do call nested `onbeforeremove`. - if (typeof vnode.tag !== "string") attrsResult = invokeBeforeRemove(vnode, vnode.state) - if (vnode.attrs != null) stateResult = invokeBeforeRemove(vnode, vnode.attrs) - - vnodePath[pathDepth++] = parent - vnodePath[pathDepth++] = vnode - - if (attrsResult || stateResult) { - var path = vnodePath.slice(0, pathDepth) - var settle = () => { - - // Remove the innermost node recursively and try to remove the parents - // non-recursively. - // If it's still delayed, skip. If this node is delayed, all its ancestors are - // also necessarily delayed, and so they should be skipped. - var i = path.length - 2 - if (removeNodeAsyncRecurse(path[i], path[i + 1])) { - while ((i -= 2) >= 0 && removalRequested.has(path[i + 1])) { - tryProcessRemoval(path[i], path[i + 1]) - } - } - } - var increment = 0 - if (attrsResult) { - attrsResult.catch(console.error) - attrsResult.then(settle, settle) - increment++ - } + return true +} +function removeNodeAsyncRecurse(parent, vnode) { + while (vnode != null) { + // Delay the actual subtree removal if there's still pending `onbeforeremove` hooks on + // this node or a child node. + if (!tryProcessRemoval(parent, vnode)) return false + if (typeof vnode.tag !== "function") { + if (vnode.tag === "#") break + if (vnode.tag !== "[" && vnode.tag !== "=") parent = vnode.dom + // Using bitwise ops and `Array.prototype.reduce` to reduce code size. It's not + // called nearly enough to merit further optimization. + // eslint-disable-next-line no-bitwise + return vnode.children.reduce((fail, child) => fail & removeNodeAsyncRecurse(parent, child), 1) + } + vnode = vnode.instance + } + + return true +} +function removeNodes(parent, vnodes, start, end, pathDepth, isDelayed) { + // Using bitwise ops to reduce code size. + var fail = 0 + // eslint-disable-next-line no-bitwise + for (var i = start; i < end; i++) fail |= !removeNode(parent, vnodes[i], pathDepth, isDelayed) + return !fail +} +function removeNode(parent, vnode, pathDepth, isDelayed) { + if (vnode != null) { + delayed: { + var attrsResult, stateResult - if (stateResult) { - stateResult.catch(console.error) - stateResult.then(settle, settle) - increment++ - } + // Block removes, but do call nested `onbeforeremove`. + if (typeof vnode.tag !== "string") attrsResult = invokeBeforeRemove(vnode, vnode.state) + if (vnode.attrs != null) stateResult = invokeBeforeRemove(vnode, vnode.attrs) - isDelayed = true + vnodePath[pathDepth++] = parent + vnodePath[pathDepth++] = vnode - for (var i = 1; i < pathDepth; i += 2) { - // eslint-disable-next-line no-bitwise - blockedRemovalRefCount.set(vnodePath[i], (blockedRemovalRefCount.get(vnodePath[i]) | 0) + increment) + if (attrsResult || stateResult) { + var path = vnodePath.slice(0, pathDepth) + var settle = () => { + + // Remove the innermost node recursively and try to remove the parents + // non-recursively. + // If it's still delayed, skip. If this node is delayed, all its ancestors are + // also necessarily delayed, and so they should be skipped. + var i = path.length - 2 + if (removeNodeAsyncRecurse(path[i], path[i + 1])) { + while ((i -= 2) >= 0 && removalRequested.has(path[i + 1])) { + tryProcessRemoval(path[i], path[i + 1]) + } } } + var increment = 0 - if (typeof vnode.tag === "function") { - if (vnode.instance != null && !removeNode(parent, vnode.instance, pathDepth, isDelayed)) break delayed - } else if (vnode.tag !== "#") { - if (!removeNodes( - vnode.tag !== "[" && vnode.tag !== "=" ? vnode.dom : parent, - vnode.children, 0, vnode.children.length, pathDepth, isDelayed - )) break delayed + if (attrsResult) { + attrsResult.catch(console.error) + attrsResult.then(settle, settle) + increment++ } - // Don't call removal hooks if removal is delayed. - // Delay the actual subtree removal if there's still pending `onbeforeremove` hooks on - // this node or a child node. - if (isDelayed || tryProcessRemoval(parent, vnode)) break delayed + if (stateResult) { + stateResult.catch(console.error) + stateResult.then(settle, settle) + increment++ + } + + isDelayed = true + + for (var i = 1; i < pathDepth; i += 2) { + // eslint-disable-next-line no-bitwise + blockedRemovalRefCount.set(vnodePath[i], (blockedRemovalRefCount.get(vnodePath[i]) | 0) + increment) + } + } - return false + if (typeof vnode.tag === "function") { + if (vnode.instance != null && !removeNode(parent, vnode.instance, pathDepth, isDelayed)) break delayed + } else if (vnode.tag !== "#") { + if (!removeNodes( + vnode.tag !== "[" && vnode.tag !== "=" ? vnode.dom : parent, + vnode.children, 0, vnode.children.length, pathDepth, isDelayed + )) break delayed } - removalRequested.add(vnode) + // Don't call removal hooks if removal is delayed. + // Delay the actual subtree removal if there's still pending `onbeforeremove` hooks on + // this node or a child node. + if (isDelayed || tryProcessRemoval(parent, vnode)) break delayed + + return false } - return true + removalRequested.add(vnode) } - //attrs - function setAttrs(vnode, attrs, ns) { - // The DOM does things to inputs based on the value, so it needs set first. - // See: https://github.com/MithrilJS/mithril.js/issues/2622 - if (vnode.tag === "input" && attrs.type != null) vnode.dom.type = attrs.type - var isFileInput = attrs != null && vnode.tag === "input" && attrs.type === "file" - for (var key in attrs) { - setAttr(vnode, key, null, attrs[key], ns) - } + return true +} + +//attrs +function setAttrs(vnode, attrs, ns) { + // The DOM does things to inputs based on the value, so it needs set first. + // See: https://github.com/MithrilJS/mithril.js/issues/2622 + if (vnode.tag === "input" && attrs.type != null) vnode.dom.type = attrs.type + var isFileInput = attrs != null && vnode.tag === "input" && attrs.type === "file" + for (var key in attrs) { + setAttr(vnode, key, null, attrs[key], ns, isFileInput) } - function setAttr(vnode, key, old, value, ns, isFileInput) { - if (value == null || isSpecialAttribute.has(key) || (old === value && !isFormAttribute(vnode, key)) && typeof value !== "object" || key === "type" && vnode.tag === "input") return - if (key.startsWith("on")) updateEvent(vnode, key, value) - else if (key.startsWith("xlink:")) vnode.dom.setAttributeNS(xlinkNs, key.slice(6), value) - else if (key === "style") updateStyle(vnode.dom, old, value) - else if (hasPropertyKey(vnode, key, ns)) { - if (key === "value") { - // Only do the coercion if we're actually going to check the value. - /* eslint-disable no-implicit-coercion */ - switch (vnode.tag) { - //setting input[value] to same value by typing on focused element moves cursor to end in Chrome - //setting input[type=file][value] to same value causes an error to be generated if it's non-empty - case "input": - case "textarea": - if (vnode.dom.value === "" + value && (isFileInput || vnode.dom === activeElement(vnode.dom))) return - //setting input[type=file][value] to different value is an error if it's non-empty - // Not ideal, but it at least works around the most common source of uncaught exceptions for now. - if (isFileInput && "" + value !== "") { console.error("`value` is read-only on file inputs!"); return } - break - //setting select[value] or option[value] to same value while having select open blinks select dropdown in Chrome - case "select": - case "option": - if (old !== null && vnode.dom.value === "" + value) return - } - /* eslint-enable no-implicit-coercion */ +} +function setAttr(vnode, key, old, value, ns, isFileInput) { + if (value == null || isSpecialAttribute.has(key) || (old === value && !isFormAttribute(vnode, key)) && typeof value !== "object" || key === "type" && vnode.tag === "input") return + if (key.startsWith("on")) updateEvent(vnode, key, value) + else if (key.startsWith("xlink:")) vnode.dom.setAttributeNS(xlinkNs, key.slice(6), value) + else if (key === "style") updateStyle(vnode.dom, old, value) + else if (hasPropertyKey(vnode, key, ns)) { + if (key === "value") { + // Only do the coercion if we're actually going to check the value. + /* eslint-disable no-implicit-coercion */ + switch (vnode.tag) { + //setting input[value] to same value by typing on focused element moves cursor to end in Chrome + //setting input[type=file][value] to same value causes an error to be generated if it's non-empty + case "input": + case "textarea": + if (vnode.dom.value === "" + value && (isFileInput || vnode.dom === activeElement(vnode.dom))) return + //setting input[type=file][value] to different value is an error if it's non-empty + // Not ideal, but it at least works around the most common source of uncaught exceptions for now. + if (isFileInput && "" + value !== "") { console.error("`value` is read-only on file inputs!"); return } + break + //setting select[value] or option[value] to same value while having select open blinks select dropdown in Chrome + case "select": + case "option": + if (old !== null && vnode.dom.value === "" + value) return } - vnode.dom[key] = value - } else if (value === false) { - vnode.dom.removeAttribute(key) - } else { - vnode.dom.setAttribute(key, value === true ? "" : value) + /* eslint-enable no-implicit-coercion */ } + vnode.dom[key] = value + } else if (value === false) { + vnode.dom.removeAttribute(key) + } else { + vnode.dom.setAttribute(key, value === true ? "" : value) } - function removeAttr(vnode, key, old, ns) { - if (old == null || isSpecialAttribute.has(key)) return - if (key.startsWith("on")) updateEvent(vnode, key, undefined) - else if (key.startsWith("xlink:")) vnode.dom.removeAttributeNS(xlinkNs, key.slice(6)) - else if (key === "style") updateStyle(vnode.dom, old, null) - else if ( - hasPropertyKey(vnode, key, ns) - && key !== "class" - && key !== "title" // creates "null" as title - && !(key === "value" && ( - vnode.tag === "option" - || vnode.tag === "select" && vnode.dom.selectedIndex === -1 && vnode.dom === activeElement(vnode.dom) - )) - && !(vnode.tag === "input" && key === "type") - ) { - vnode.dom[key] = null - } else { - if (old !== false) vnode.dom.removeAttribute(key) - } +} +function removeAttr(vnode, key, old, ns) { + if (old == null || isSpecialAttribute.has(key)) return + if (key.startsWith("on")) updateEvent(vnode, key, undefined) + else if (key.startsWith("xlink:")) vnode.dom.removeAttributeNS(xlinkNs, key.slice(6)) + else if (key === "style") updateStyle(vnode.dom, old, null) + else if ( + hasPropertyKey(vnode, key, ns) + && key !== "class" + && key !== "title" // creates "null" as title + && !(key === "value" && ( + vnode.tag === "option" + || vnode.tag === "select" && vnode.dom.selectedIndex === -1 && vnode.dom === activeElement(vnode.dom) + )) + && !(vnode.tag === "input" && key === "type") + ) { + vnode.dom[key] = null + } else { + if (old !== false) vnode.dom.removeAttribute(key) } - function setLateSelectAttrs(vnode, attrs) { - if ("value" in attrs) { - if(attrs.value === null) { - if (vnode.dom.selectedIndex !== -1) vnode.dom.value = null - } else { - var normalized = "" + attrs.value // eslint-disable-line no-implicit-coercion - if (vnode.dom.value !== normalized || vnode.dom.selectedIndex === -1) { - vnode.dom.value = normalized - } +} +function setLateSelectAttrs(vnode, attrs) { + if ("value" in attrs) { + if(attrs.value === null) { + if (vnode.dom.selectedIndex !== -1) vnode.dom.value = null + } else { + var normalized = "" + attrs.value // eslint-disable-line no-implicit-coercion + if (vnode.dom.value !== normalized || vnode.dom.selectedIndex === -1) { + vnode.dom.value = normalized } } - if ("selectedIndex" in attrs) setAttr(vnode, "selectedIndex", null, attrs.selectedIndex, undefined) } - function updateAttrs(vnode, old, attrs, ns) { - if (old && old === attrs) { - console.warn("Don't reuse attrs object, use new object for every redraw, this will throw in next major") - } - if (attrs != null) { - for (var key in attrs) { - setAttr(vnode, key, old && old[key], attrs[key], ns) - } + if ("selectedIndex" in attrs) setAttr(vnode, "selectedIndex", null, attrs.selectedIndex, undefined) +} +function updateAttrs(vnode, old, attrs, ns) { + if (old && old === attrs) { + console.warn("Don't reuse attrs object, use new object for every redraw, this will throw in next major") + } + if (attrs != null) { + // If you assign an input type that is not supported by IE 11 with an assignment expression, an error will occur. + // + // Also, the DOM does things to inputs based on the value, so it needs set first. + // See: https://github.com/MithrilJS/mithril.js/issues/2622 + if (vnode.tag === "input" && attrs.type != null) vnode.dom.setAttribute("type", attrs.type) + var isFileInput = vnode.tag === "input" && attrs.type === "file" + for (var key in attrs) { + setAttr(vnode, key, old && old[key], attrs[key], ns, isFileInput) } - var val - if (old != null) { - for (var key in old) { - if (((val = old[key]) != null) && (attrs == null || attrs[key] == null)) { - removeAttr(vnode, key, val, ns) - } + } + var val + if (old != null) { + for (var key in old) { + if (((val = old[key]) != null) && (attrs == null || attrs[key] == null)) { + removeAttr(vnode, key, val, ns) } } } - var isAlwaysFormAttribute = new Set(["value", "checked", "selected", "selectedIndex"]) - var isSpecialAttribute = new Set(["key", "is", "oninit", "oncreate", "onupdate", "onremove", "onbeforeupdate", "onbeforeremove"]) - // Try to avoid a few browser bugs on normal elements. - // var propertyMayBeBugged = new Set(["href", "list", "form", "width", "height", "type"]) - var propertyMayBeBugged = new Set(["href", "list", "form", "width", "height"]) - function isFormAttribute(vnode, attr) { - return isAlwaysFormAttribute.has(attr) || attr === "selected" && vnode.dom === activeElement(vnode.dom) || vnode.tag === "option" && vnode.dom.parentNode === activeElement(vnode.dom) - } - function hasPropertyKey(vnode, key, ns) { - // Filter out namespaced keys - return ns === undefined && ( - // If it's a custom element, just keep it. - vnode.tag.indexOf("-") > -1 || vnode.attrs != null && vnode.attrs.is || - !propertyMayBeBugged.has(key) - // Defer the property check until *after* we check everything. - ) && key in vnode.dom - } - - //style - var uppercaseRegex = /[A-Z]/g - function toLowerCase(capital) { return "-" + capital.toLowerCase() } - function normalizeKey(key) { - return key[0] === "-" && key[1] === "-" ? key : - key === "cssFloat" ? "float" : - key.replace(uppercaseRegex, toLowerCase) - } - function updateStyle(element, old, style) { - if (old === style) { - // Styles are equivalent, do nothing. - } else if (style == null) { - // New style is missing, just clear it. - element.style = "" - } else if (typeof style !== "object") { - // New style is a string, let engine deal with patching. - element.style = style - } else if (old == null || typeof old !== "object") { - // `old` is missing or a string, `style` is an object. - element.style.cssText = "" - // Add new style properties - for (var key in style) { - var value = style[key] - if (value != null) element.style.setProperty(normalizeKey(key), String(value)) - } - } else { - // Both old & new are (different) objects. - // Update style properties that have changed - for (var key in style) { - var value = style[key] - if (value != null && (value = String(value)) !== String(old[key])) { - element.style.setProperty(normalizeKey(key), value) - } +} +var isAlwaysFormAttribute = new Set(["value", "checked", "selected", "selectedIndex"]) +var isSpecialAttribute = new Set(["key", "is", "oninit", "oncreate", "onupdate", "onremove", "onbeforeupdate", "onbeforeremove"]) +// Try to avoid a few browser bugs on normal elements. +// var propertyMayBeBugged = new Set(["href", "list", "form", "width", "height", "type"]) +var propertyMayBeBugged = new Set(["href", "list", "form", "width", "height"]) +function isFormAttribute(vnode, attr) { + return isAlwaysFormAttribute.has(attr) || attr === "selected" && vnode.dom === activeElement(vnode.dom) || vnode.tag === "option" && vnode.dom.parentNode === activeElement(vnode.dom) +} +function hasPropertyKey(vnode, key, ns) { + // Filter out namespaced keys + return ns === undefined && ( + // If it's a custom element, just keep it. + vnode.tag.indexOf("-") > -1 || vnode.attrs != null && vnode.attrs.is || + !propertyMayBeBugged.has(key) + // Defer the property check until *after* we check everything. + ) && key in vnode.dom +} + +//style +var uppercaseRegex = /[A-Z]/g +function toLowerCase(capital) { return "-" + capital.toLowerCase() } +function normalizeKey(key) { + return key[0] === "-" && key[1] === "-" ? key : + key === "cssFloat" ? "float" : + key.replace(uppercaseRegex, toLowerCase) +} +function updateStyle(element, old, style) { + if (old === style) { + // Styles are equivalent, do nothing. + } else if (style == null) { + // New style is missing, just clear it. + element.style = "" + } else if (typeof style !== "object") { + // New style is a string, let engine deal with patching. + element.style = style + } else if (old == null || typeof old !== "object") { + // `old` is missing or a string, `style` is an object. + element.style.cssText = "" + // Add new style properties + for (var key in style) { + var value = style[key] + if (value != null) element.style.setProperty(normalizeKey(key), String(value)) + } + } else { + // Both old & new are (different) objects. + // Update style properties that have changed + for (var key in style) { + var value = style[key] + if (value != null && (value = String(value)) !== String(old[key])) { + element.style.setProperty(normalizeKey(key), value) } - // Remove style properties that no longer exist - for (var key in old) { - if (old[key] != null && style[key] == null) { - element.style.removeProperty(normalizeKey(key)) - } + } + // Remove style properties that no longer exist + for (var key in old) { + if (old[key] != null && style[key] == null) { + element.style.removeProperty(normalizeKey(key)) } } } +} - // Here's an explanation of how this works: - // 1. The event names are always (by design) prefixed by `on`. - // 2. The EventListener interface accepts either a function or an object - // with a `handleEvent` method. - // 3. The object does not inherit from `Object.prototype`, to avoid - // any potential interference with that (e.g. setters). - // 4. The event name is remapped to the handler before calling it. - // 5. In function-based event handlers, `ev.target === this`. We replicate - // that below. - // 6. In function-based event handlers, `return false` prevents the default - // action and stops event propagation. We replicate that below. - 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 (this._ && ev.redraw !== false) (0, this._)() - if (result === false) { - ev.preventDefault() - ev.stopPropagation() - } +// Here's an explanation of how this works: +// 1. The event names are always (by design) prefixed by `on`. +// 2. The EventListener interface accepts either a function or an object +// with a `handleEvent` method. +// 3. The object does not inherit from `Object.prototype`, to avoid +// any potential interference with that (e.g. setters). +// 4. The event name is remapped to the handler before calling it. +// 5. In function-based event handlers, `ev.target === this`. We replicate +// that below. +// 6. In function-based event handlers, `return false` prevents the default +// action and stops event propagation. We replicate that below. +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 (this._ && ev.redraw !== false) (0, this._)() + if (result === false) { + ev.preventDefault() + ev.stopPropagation() } +} - //event - function updateEvent(vnode, key, value) { - if (vnode.instance != null) { - vnode.instance._ = currentRedraw - if (vnode.instance[key] === value) return - if (value != null && (typeof value === "function" || typeof value === "object")) { - if (vnode.instance[key] == null) vnode.dom.addEventListener(key.slice(2), vnode.instance, false) - vnode.instance[key] = value - } else { - if (vnode.instance[key] != null) vnode.dom.removeEventListener(key.slice(2), vnode.instance, false) - vnode.instance[key] = undefined - } - } else if (value != null && (typeof value === "function" || typeof value === "object")) { - vnode.instance = new EventDict() - vnode.dom.addEventListener(key.slice(2), vnode.instance, false) +//event +function updateEvent(vnode, key, value) { + if (vnode.instance != null) { + vnode.instance._ = currentRedraw + if (vnode.instance[key] === value) return + if (value != null && (typeof value === "function" || typeof value === "object")) { + if (vnode.instance[key] == null) vnode.dom.addEventListener(key.slice(2), vnode.instance, false) vnode.instance[key] = value + } else { + if (vnode.instance[key] != null) vnode.dom.removeEventListener(key.slice(2), vnode.instance, false) + vnode.instance[key] = undefined } + } else if (value != null && (typeof value === "function" || typeof value === "object")) { + vnode.instance = new EventDict() + vnode.dom.addEventListener(key.slice(2), vnode.instance, false) + vnode.instance[key] = value } +} - //lifecycle - function initLifecycle(source, vnode, hooks) { - if (typeof source.oninit === "function") callHook.call(source.oninit, vnode) - if (typeof source.oncreate === "function") hooks.push(callHook.bind(source.oncreate, vnode)) - } - function updateLifecycle(source, vnode, hooks) { - if (typeof source.onupdate === "function") hooks.push(callHook.bind(source.onupdate, vnode)) - } - function shouldNotUpdate(vnode, old) { - do { - if (vnode.attrs != null && typeof vnode.attrs.onbeforeupdate === "function") { - var force = callHook.call(vnode.attrs.onbeforeupdate, vnode, old) - if (force !== undefined && !force) break - } - if (typeof vnode.tag !== "string" && typeof vnode.state.onbeforeupdate === "function") { - var force = callHook.call(vnode.state.onbeforeupdate, vnode, old) - if (force !== undefined && !force) break - } - return false - } while (false); // eslint-disable-line no-constant-condition - vnode.dom = old.dom - vnode.instance = old.instance - // One would think having the actual latest attributes would be ideal, - // but it doesn't let us properly diff based on our current internal - // representation. We have to save not only the old DOM info, but also - // the attributes used to create it, as we diff *that*, not against the - // DOM directly (with a few exceptions in `setAttr`). And, of course, we - // need to save the children and text as they are conceptually not - // unlike special "attributes" internally. - vnode.attrs = old.attrs - vnode.children = old.children - return true - } - - var currentDOM - - return function(dom, vnodes, redraw) { - if (!dom) throw new TypeError("DOM element being rendered to does not exist.") - if (currentDOM != null && dom.contains(currentDOM)) { - throw new TypeError("Node is currently being rendered to and thus is locked.") - } - var prevRedraw = currentRedraw - var prevDOM = currentDOM - var hooks = [] - var active = activeElement(dom) - var namespace = dom.namespaceURI - var prevPath = vnodePath - - currentDOM = dom - currentRedraw = typeof redraw === "function" ? redraw : undefined - vnodePath = [] - try { - // First time rendering into a node clears it out - if (dom.vnodes == null) dom.textContent = "" - vnodes = Vnode.normalizeChildren(Array.isArray(vnodes) ? vnodes : [vnodes]) - updateNodes(dom, dom.vnodes, vnodes, hooks, null, namespace === "http://www.w3.org/1999/xhtml" ? undefined : namespace, 0) - dom.vnodes = vnodes - // `document.activeElement` can return null: https://html.spec.whatwg.org/multipage/interaction.html#dom-document-activeelement - if (active != null && activeElement(dom) !== active && typeof active.focus === "function") active.focus() - for (var i = 0; i < hooks.length; i++) hooks[i]() - } finally { - currentRedraw = prevRedraw - currentDOM = prevDOM - vnodePath = prevPath - } +//lifecycle +function initLifecycle(source, vnode, hooks) { + if (typeof source.oninit === "function") callHook.call(source.oninit, vnode) + if (typeof source.oncreate === "function") hooks.push(callHook.bind(source.oncreate, vnode)) +} +function updateLifecycle(source, vnode, hooks) { + if (typeof source.onupdate === "function") hooks.push(callHook.bind(source.onupdate, vnode)) +} +function shouldNotUpdate(vnode, old) { + do { + if (vnode.attrs != null && typeof vnode.attrs.onbeforeupdate === "function") { + var force = callHook.call(vnode.attrs.onbeforeupdate, vnode, old) + if (force !== undefined && !force) break + } + if (typeof vnode.tag !== "string" && typeof vnode.state.onbeforeupdate === "function") { + var force = callHook.call(vnode.state.onbeforeupdate, vnode, old) + if (force !== undefined && !force) break + } + return false + } while (false); // eslint-disable-line no-constant-condition + vnode.dom = old.dom + vnode.instance = old.instance + // One would think having the actual latest attributes would be ideal, + // but it doesn't let us properly diff based on our current internal + // representation. We have to save not only the old DOM info, but also + // the attributes used to create it, as we diff *that*, not against the + // DOM directly (with a few exceptions in `setAttr`). And, of course, we + // need to save the children and text as they are conceptually not + // unlike special "attributes" internally. + vnode.attrs = old.attrs + vnode.children = old.children + return true +} + +var currentDOM + +module.exports = function(dom, vnodes, redraw) { + if (!dom) throw new TypeError("DOM element being rendered to does not exist.") + if (currentDOM != null && dom.contains(currentDOM)) { + throw new TypeError("Node is currently being rendered to and thus is locked.") + } + var prevRedraw = currentRedraw + var prevDOM = currentDOM + var hooks = [] + var active = activeElement(dom) + var namespace = dom.namespaceURI + var prevPath = vnodePath + + currentDOM = dom + currentRedraw = typeof redraw === "function" ? redraw : undefined + vnodePath = [] + try { + // First time rendering into a node clears it out + if (dom.vnodes == null) dom.textContent = "" + vnodes = Vnode.normalizeChildren(Array.isArray(vnodes) ? vnodes : [vnodes]) + updateNodes(dom, dom.vnodes, vnodes, hooks, null, namespace === "http://www.w3.org/1999/xhtml" ? undefined : namespace, 0) + dom.vnodes = vnodes + // `document.activeElement` can return null: https://html.spec.whatwg.org/multipage/interaction.html#dom-document-activeelement + if (active != null && activeElement(dom) !== active && typeof active.focus === "function") active.focus() + for (var i = 0; i < hooks.length; i++) hooks[i]() + } finally { + currentRedraw = prevRedraw + currentDOM = prevDOM + vnodePath = prevPath } } diff --git a/render/tests/test-attributes.js b/render/tests/test-attributes.js index d28335443..c68b768ba 100644 --- a/render/tests/test-attributes.js +++ b/render/tests/test-attributes.js @@ -2,15 +2,14 @@ var o = require("ospec") var domMock = require("../../test-utils/domMock") -var vdom = require("../../render/render") +var render = require("../../render/render") var m = require("../../render/hyperscript") o.spec("attributes", function() { - var $window, root, render + var $window, root o.beforeEach(function() { $window = domMock() root = $window.document.body - render = vdom($window) }) o.spec("basics", function() { o("works (create/update/remove)", function() { @@ -256,7 +255,6 @@ 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) var a =m("input") var b = m("input", {value: "1"}) @@ -295,7 +293,6 @@ o.spec("attributes", function() { o("works", function() { var $window = domMock() var root = $window.document.body - var render = vdom($window) var a = m("input", {type: "radio"}) var b = m("input", {type: "text"}) @@ -331,7 +328,6 @@ 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) var a = m("textarea") var b = m("textarea", {value: "1"}) @@ -477,7 +473,6 @@ 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) var a = m("option") var b = m("option", {value: "1"}) @@ -615,7 +610,6 @@ 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) var a = makeSelect() var b = makeSelect("1") diff --git a/render/tests/test-component.js b/render/tests/test-component.js index 5a9c19e28..6a47c4018 100644 --- a/render/tests/test-component.js +++ b/render/tests/test-component.js @@ -3,15 +3,14 @@ var o = require("ospec") var components = require("../../test-utils/components") var domMock = require("../../test-utils/domMock") -var vdom = require("../../render/render") +var render = require("../../render/render") var m = require("../../render/hyperscript") o.spec("component", function() { - var $window, root, render + var $window, root o.beforeEach(function() { $window = domMock() root = $window.document.createElement("div") - render = vdom($window) }) components.forEach(function(cmp){ diff --git a/render/tests/test-createElement.js b/render/tests/test-createElement.js index 2eecc90fb..305119b43 100644 --- a/render/tests/test-createElement.js +++ b/render/tests/test-createElement.js @@ -2,15 +2,14 @@ var o = require("ospec") var domMock = require("../../test-utils/domMock") -var vdom = require("../../render/render") +var render = require("../../render/render") var m = require("../../render/hyperscript") o.spec("createElement", function() { - var $window, root, render + var $window, root o.beforeEach(function() { $window = domMock() root = $window.document.createElement("div") - render = vdom($window) }) o("creates element", function() { diff --git a/render/tests/test-createFragment.js b/render/tests/test-createFragment.js index 69ef80aaa..663326c34 100644 --- a/render/tests/test-createFragment.js +++ b/render/tests/test-createFragment.js @@ -2,16 +2,15 @@ var o = require("ospec") var domMock = require("../../test-utils/domMock") -var vdom = require("../../render/render") +var render = require("../../render/render") var m = require("../../render/hyperscript") var fragment = require("../../render/hyperscript").fragment o.spec("createFragment", function() { - var $window, root, render + var $window, root o.beforeEach(function() { $window = domMock() root = $window.document.createElement("div") - render = vdom($window) }) o("creates fragment", function() { diff --git a/render/tests/test-createNodes.js b/render/tests/test-createNodes.js index 416bf5c23..b4afdda05 100644 --- a/render/tests/test-createNodes.js +++ b/render/tests/test-createNodes.js @@ -2,16 +2,15 @@ var o = require("ospec") var domMock = require("../../test-utils/domMock") -var vdom = require("../../render/render") +var render = require("../../render/render") var m = require("../../render/hyperscript") var fragment = require("../../render/hyperscript").fragment o.spec("createNodes", function() { - var $window, root, render + var $window, root o.beforeEach(function() { $window = domMock() root = $window.document.createElement("div") - render = vdom($window) }) o("creates nodes", function() { diff --git a/render/tests/test-createText.js b/render/tests/test-createText.js index e7fd74ec9..b1feb5871 100644 --- a/render/tests/test-createText.js +++ b/render/tests/test-createText.js @@ -2,14 +2,13 @@ var o = require("ospec") var domMock = require("../../test-utils/domMock") -var vdom = require("../../render/render") +var render = require("../../render/render") o.spec("createText", function() { - var $window, root, render + var $window, root o.beforeEach(function() { $window = domMock() root = $window.document.createElement("div") - render = vdom($window) }) o("creates string", function() { diff --git a/render/tests/test-event.js b/render/tests/test-event.js index 5acc29f41..b783eb850 100644 --- a/render/tests/test-event.js +++ b/render/tests/test-event.js @@ -2,16 +2,15 @@ var o = require("ospec") var domMock = require("../../test-utils/domMock") -var vdom = require("../../render/render") +var reallyRender = require("../../render/render") var m = require("../../render/hyperscript") o.spec("event", function() { - var $window, root, redraw, render, reallyRender + var $window, root, redraw, render o.beforeEach(function() { $window = domMock() root = $window.document.body redraw = o.spy() - reallyRender = vdom($window) render = function(dom, vnode) { return reallyRender(dom, vnode, redraw) } diff --git a/render/tests/test-input.js b/render/tests/test-input.js index 89b4bfa46..d91f78e7c 100644 --- a/render/tests/test-input.js +++ b/render/tests/test-input.js @@ -2,14 +2,13 @@ var o = require("ospec") var domMock = require("../../test-utils/domMock") -var vdom = require("../../render/render") +var render = require("../../render/render") var m = require("../../render/hyperscript") o.spec("form inputs", function() { - var $window, root, render + var $window, root o.beforeEach(function() { $window = domMock() - render = vdom($window) root = $window.document.createElement("div") $window.document.body.appendChild(root) }) @@ -132,7 +131,6 @@ o.spec("form inputs", function() { o("retains file input value attribute if DOM value is the same as vdom value and is non-empty", function() { var $window = domMock(o) - var render = vdom($window) var root = $window.document.createElement("div") $window.document.body.appendChild(root) var input = m("input", {type: "file", value: "", onclick: function() {}}) diff --git a/render/tests/test-normalizeComponentChildren.js b/render/tests/test-normalizeComponentChildren.js index 31cc88041..e268bd99b 100644 --- a/render/tests/test-normalizeComponentChildren.js +++ b/render/tests/test-normalizeComponentChildren.js @@ -3,12 +3,11 @@ var o = require("ospec") var m = require("../../render/hyperscript") var domMock = require("../../test-utils/domMock") -var vdom = require("../../render/render") +var render = require("../../render/render") o.spec("component children", function () { var $window = domMock() var root = $window.document.createElement("div") - 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 521cd6213..e99aa3d83 100644 --- a/render/tests/test-onbeforeremove.js +++ b/render/tests/test-onbeforeremove.js @@ -3,15 +3,14 @@ var o = require("ospec") var components = require("../../test-utils/components") var domMock = require("../../test-utils/domMock") -var vdom = require("../../render/render") +var render = require("../../render/render") var m = require("../../render/hyperscript") o.spec("onbeforeremove", function() { - var $window, root, render + var $window, root o.beforeEach(function() { $window = domMock() root = $window.document.createElement("div") - 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 4215207a3..b5857e7b9 100644 --- a/render/tests/test-onbeforeupdate.js +++ b/render/tests/test-onbeforeupdate.js @@ -3,15 +3,14 @@ var o = require("ospec") var components = require("../../test-utils/components") var domMock = require("../../test-utils/domMock") -var vdom = require("../../render/render") +var render = require("../../render/render") var m = require("../../render/hyperscript") o.spec("onbeforeupdate", function() { - var $window, root, render + var $window, root o.beforeEach(function() { $window = domMock() root = $window.document.createElement("div") - render = vdom($window) }) o("prevents update in element", function() { diff --git a/render/tests/test-oncreate.js b/render/tests/test-oncreate.js index 948447615..578d29375 100644 --- a/render/tests/test-oncreate.js +++ b/render/tests/test-oncreate.js @@ -2,15 +2,14 @@ var o = require("ospec") var domMock = require("../../test-utils/domMock") -var vdom = require("../../render/render") +var render = require("../../render/render") var m = require("../../render/hyperscript") o.spec("oncreate", function() { - var $window, root, render + var $window, root o.beforeEach(function() { $window = domMock() root = $window.document.createElement("div") - 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 790d7f59a..6b33ff85b 100644 --- a/render/tests/test-oninit.js +++ b/render/tests/test-oninit.js @@ -2,15 +2,14 @@ var o = require("ospec") var domMock = require("../../test-utils/domMock") -var vdom = require("../../render/render") +var render = require("../../render/render") var m = require("../../render/hyperscript") o.spec("oninit", function() { - var $window, root, render + var $window, root o.beforeEach(function() { $window = domMock() root = $window.document.createElement("div") - 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 cb7133b93..b0f7954c8 100644 --- a/render/tests/test-onremove.js +++ b/render/tests/test-onremove.js @@ -3,15 +3,14 @@ var o = require("ospec") var components = require("../../test-utils/components") var domMock = require("../../test-utils/domMock") -var vdom = require("../../render/render") +var render = require("../../render/render") var m = require("../../render/hyperscript") o.spec("onremove", function() { - var $window, root, render + var $window, root o.beforeEach(function() { $window = domMock() root = $window.document.createElement("div") - 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 8f02fcccb..a2cd72a5c 100644 --- a/render/tests/test-onupdate.js +++ b/render/tests/test-onupdate.js @@ -2,15 +2,14 @@ var o = require("ospec") var domMock = require("../../test-utils/domMock") -var vdom = require("../../render/render") +var render = require("../../render/render") var m = require("../../render/hyperscript") o.spec("onupdate", function() { - var $window, root, render + var $window, root o.beforeEach(function() { $window = domMock() root = $window.document.createElement("div") - 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 7a94eb879..c4c2e6895 100644 --- a/render/tests/test-render-hyperscript-integration.js +++ b/render/tests/test-render-hyperscript-integration.js @@ -3,14 +3,13 @@ var o = require("ospec") var m = require("../../render/hyperscript") var domMock = require("../../test-utils/domMock") -var vdom = require("../../render/render") +var render = require("../../render/render") o.spec("render/hyperscript integration", function() { - var $window, root, render + var $window, root o.beforeEach(function() { $window = domMock() root = $window.document.createElement("div") - 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 195638a5c..d198ce3f4 100644 --- a/render/tests/test-render.js +++ b/render/tests/test-render.js @@ -2,19 +2,14 @@ var o = require("ospec") var domMock = require("../../test-utils/domMock") -var vdom = require("../../render/render") +var render = require("../../render/render") var m = require("../../render/hyperscript") o.spec("render", function() { - var $window, root, render + var $window, root o.beforeEach(function() { $window = domMock() root = $window.document.createElement("div") - render = vdom($window) - }) - - o("initializes without DOM", function() { - vdom() }) o("renders plain text", function() { diff --git a/render/tests/test-textContent.js b/render/tests/test-textContent.js index 31d911753..190b36a13 100644 --- a/render/tests/test-textContent.js +++ b/render/tests/test-textContent.js @@ -2,15 +2,14 @@ var o = require("ospec") var domMock = require("../../test-utils/domMock") -var vdom = require("../../render/render") +var render = require("../../render/render") var m = require("../../render/hyperscript") o.spec("textContent", function() { - var $window, root, render + var $window, root o.beforeEach(function() { $window = domMock() root = $window.document.createElement("div") - render = vdom($window) }) o("ignores null", function() { diff --git a/render/tests/test-updateElement.js b/render/tests/test-updateElement.js index a94667514..1aa232350 100644 --- a/render/tests/test-updateElement.js +++ b/render/tests/test-updateElement.js @@ -2,15 +2,14 @@ var o = require("ospec") var domMock = require("../../test-utils/domMock") -var vdom = require("../../render/render") +var render = require("../../render/render") var m = require("../../render/hyperscript") o.spec("updateElement", function() { - var $window, root, render + var $window, root o.beforeEach(function() { $window = domMock() root = $window.document.createElement("div") - render = vdom($window) }) o("updates attr", function() { diff --git a/render/tests/test-updateFragment.js b/render/tests/test-updateFragment.js index d7f6b0c25..52538f743 100644 --- a/render/tests/test-updateFragment.js +++ b/render/tests/test-updateFragment.js @@ -2,15 +2,14 @@ var o = require("ospec") var domMock = require("../../test-utils/domMock") -var vdom = require("../../render/render") +var render = require("../../render/render") var m = require("../../render/hyperscript") o.spec("updateFragment", function() { - var $window, root, render + var $window, root o.beforeEach(function() { $window = domMock() root = $window.document.createElement("div") - render = vdom($window) }) o("updates fragment", function() { diff --git a/render/tests/test-updateNodes.js b/render/tests/test-updateNodes.js index 15c88eee8..a8811bc10 100644 --- a/render/tests/test-updateNodes.js +++ b/render/tests/test-updateNodes.js @@ -3,7 +3,7 @@ var o = require("ospec") var components = require("../../test-utils/components") var domMock = require("../../test-utils/domMock") -var vdom = require("../../render/render") +var render = require("../../render/render") var m = require("../../render/hyperscript") function vnodify(str) { @@ -11,11 +11,10 @@ function vnodify(str) { } o.spec("updateNodes", function() { - var $window, root, render + var $window, root o.beforeEach(function() { $window = domMock() root = $window.document.createElement("div") - render = vdom($window) }) o("handles el noop", function() { diff --git a/render/tests/test-updateNodesFuzzer.js b/render/tests/test-updateNodesFuzzer.js index dea06aeef..60eaccded 100644 --- a/render/tests/test-updateNodesFuzzer.js +++ b/render/tests/test-updateNodesFuzzer.js @@ -2,16 +2,15 @@ var o = require("ospec") var domMock = require("../../test-utils/domMock") -var vdom = require("../../render/render") +var render = require("../../render/render") var m = require("../../render/hyperscript") // pilfered and adapted from https://github.com/domvm/domvm/blob/7aaec609e4c625b9acf9a22d035d6252a5ca654f/test/src/flat-list-keyed-fuzz.js o.spec("updateNodes keyed list Fuzzer", function() { - var i = 0, $window, root, render + var i = 0, $window, root o.beforeEach(function() { $window = domMock() root = $window.document.createElement("div") - render = vdom($window) }) diff --git a/render/tests/test-updateText.js b/render/tests/test-updateText.js index de6b55efc..151514425 100644 --- a/render/tests/test-updateText.js +++ b/render/tests/test-updateText.js @@ -2,14 +2,13 @@ var o = require("ospec") var domMock = require("../../test-utils/domMock") -var vdom = require("../../render/render") +var render = require("../../render/render") o.spec("updateText", function() { - var $window, root, render + var $window, root o.beforeEach(function() { $window = domMock() root = $window.document.createElement("div") - render = vdom($window) }) o("updates to string", function() { From 3ade35c76b9f0dc650eb01effe477a3748a80b1b Mon Sep 17 00:00:00 2001 From: Claudia Meadows Date: Wed, 2 Oct 2024 20:13:13 -0700 Subject: [PATCH 13/95] Drop `m.request` --- index.js | 2 - request.js | 5 - request/request.js | 199 -------- request/tests/test-request.js | 909 ---------------------------------- 4 files changed, 1115 deletions(-) delete mode 100644 request.js delete mode 100644 request/request.js delete mode 100644 request/tests/test-request.js diff --git a/index.js b/index.js index d7c62f1b5..bd7697007 100644 --- a/index.js +++ b/index.js @@ -1,7 +1,6 @@ "use strict" var hyperscript = require("./hyperscript") -var request = require("./request") var mountRedraw = require("./mount-redraw") var m = (...args) => hyperscript(...args) @@ -13,7 +12,6 @@ m.mount = mountRedraw.mount m.route = require("./route") m.render = require("./render") m.redraw = mountRedraw.redraw -m.request = request.request m.parseQueryString = require("./querystring/parse") m.buildQueryString = require("./querystring/build") m.parsePathname = require("./pathname/parse") diff --git a/request.js b/request.js deleted file mode 100644 index 740119bf5..000000000 --- a/request.js +++ /dev/null @@ -1,5 +0,0 @@ -"use strict" - -var mountRedraw = require("./mount-redraw") - -module.exports = require("./request/request")(typeof window !== "undefined" ? window : null, mountRedraw.redraw) diff --git a/request/request.js b/request/request.js deleted file mode 100644 index 7252ccade..000000000 --- a/request/request.js +++ /dev/null @@ -1,199 +0,0 @@ -"use strict" - -var buildPathname = require("../pathname/build") -var hasOwn = require("../util/hasOwn") - -module.exports = function($window, oncompletion) { - function PromiseProxy(executor) { - return new Promise(executor) - } - - function makeRequest(url, args) { - return new Promise(function(resolve, reject) { - url = buildPathname(url, args.params) - var method = args.method != null ? args.method.toUpperCase() : "GET" - var body = args.body - var assumeJSON = (args.serialize == null || args.serialize === JSON.serialize) && !(body instanceof $window.FormData || body instanceof $window.URLSearchParams) - var responseType = args.responseType || (typeof args.extract === "function" ? "" : "json") - - var xhr = new $window.XMLHttpRequest(), aborted = false, isTimeout = false - var original = xhr, replacedAbort - var abort = xhr.abort - - xhr.abort = function() { - aborted = true - abort.call(this) - } - - xhr.open(method, url, args.async !== false, typeof args.user === "string" ? args.user : undefined, typeof args.password === "string" ? args.password : undefined) - - if (assumeJSON && body != null && !hasHeader(args, "content-type")) { - xhr.setRequestHeader("Content-Type", "application/json; charset=utf-8") - } - if (typeof args.deserialize !== "function" && !hasHeader(args, "accept")) { - xhr.setRequestHeader("Accept", "application/json, text/*") - } - if (args.withCredentials) xhr.withCredentials = args.withCredentials - if (args.timeout) xhr.timeout = args.timeout - xhr.responseType = responseType - - for (var key in args.headers) { - if (hasOwn.call(args.headers, key)) { - xhr.setRequestHeader(key, args.headers[key]) - } - } - - xhr.onreadystatechange = function(ev) { - // Don't throw errors on xhr.abort(). - if (aborted) return - - if (ev.target.readyState === 4) { - try { - var success = (ev.target.status >= 200 && ev.target.status < 300) || ev.target.status === 304 || (/^file:\/\//i).test(url) - // When the response type isn't "" or "text", - // `xhr.responseText` is the wrong thing to use. - // Browsers do the right thing and throw here, and we - // should honor that and do the right thing by - // preferring `xhr.response` where possible/practical. - var response = ev.target.response, message - - if (responseType === "json") { - // For IE and Edge, which don't implement - // `responseType: "json"`. - if (!ev.target.responseType && typeof args.extract !== "function") { - // Handle no-content which will not parse. - try { response = JSON.parse(ev.target.responseText) } - catch (e) { response = null } - } - } else if (!responseType || responseType === "text") { - // Only use this default if it's text. If a parsed - // document is needed on old IE and friends (all - // unsupported), the user should use a custom - // `config` instead. They're already using this at - // their own risk. - if (response == null) response = ev.target.responseText - } - - if (typeof args.extract === "function") { - response = args.extract(ev.target, args) - success = true - } else if (typeof args.deserialize === "function") { - response = args.deserialize(response) - } - - if (success) { - if (typeof args.type === "function") { - if (Array.isArray(response)) { - for (var i = 0; i < response.length; i++) { - response[i] = new args.type(response[i]) - } - } - else response = new args.type(response) - } - resolve(response) - } - else { - var completeErrorResponse = function() { - try { message = ev.target.responseText } - catch (e) { message = response } - var error = new Error(message) - error.code = ev.target.status - error.response = response - reject(error) - } - - if (xhr.status === 0) { - // Use setTimeout to push this code block onto the event queue - // This allows `xhr.ontimeout` to run in the case that there is a timeout - // Without this setTimeout, `xhr.ontimeout` doesn't have a chance to reject - // as `xhr.onreadystatechange` will run before it - setTimeout(function() { - if (isTimeout) return - completeErrorResponse() - }) - } else completeErrorResponse() - } - } - catch (e) { - reject(e) - } - } - } - - xhr.ontimeout = function (ev) { - isTimeout = true - var error = new Error("Request timed out") - error.code = ev.target.status - reject(error) - } - - if (typeof args.config === "function") { - xhr = args.config(xhr, args, url) || xhr - - // Propagate the `abort` to any replacement XHR as well. - if (xhr !== original) { - replacedAbort = xhr.abort - xhr.abort = function() { - aborted = true - replacedAbort.call(this) - } - } - } - - if (body == null) xhr.send() - else if (typeof args.serialize === "function") xhr.send(args.serialize(body)) - else if (body instanceof $window.FormData || body instanceof $window.URLSearchParams) xhr.send(body) - else xhr.send(JSON.stringify(body)) - }) - } - - // In case the global Promise is some userland library's where they rely on - // `foo instanceof this.constructor`, `this.constructor.resolve(value)`, or - // similar. Let's *not* break them. - PromiseProxy.prototype = Promise.prototype - PromiseProxy.__proto__ = Promise // eslint-disable-line no-proto - - function hasHeader(args, name) { - for (var key in args.headers) { - if (hasOwn.call(args.headers, key) && key.toLowerCase() === name) return true - } - return false - } - - return { - request: function(url, args) { - if (typeof url !== "string") { args = url; url = url.url } - else if (args == null) args = {} - var promise = makeRequest(url, args) - if (args.background === true) return promise - var count = 0 - function complete() { - if (--count === 0 && typeof oncompletion === "function") oncompletion() - } - - return wrap(promise) - - function wrap(promise) { - var then = promise.then - // Set the constructor, so engines know to not await or resolve - // this as a native promise. At the time of writing, this is - // only necessary for V8, but their behavior is the correct - // behavior per spec. See this spec issue for more details: - // https://github.com/tc39/ecma262/issues/1577. Also, see the - // corresponding comment in `request/tests/test-request.js` for - // a bit more background on the issue at hand. - promise.constructor = PromiseProxy - promise.then = function() { - count++ - var next = then.apply(promise, arguments) - next.then(complete, function(e) { - complete() - if (count === 0) throw e - }) - return wrap(next) - } - return promise - } - } - } -} diff --git a/request/tests/test-request.js b/request/tests/test-request.js deleted file mode 100644 index a5e5d9801..000000000 --- a/request/tests/test-request.js +++ /dev/null @@ -1,909 +0,0 @@ -"use strict" - -var o = require("ospec") -var callAsync = require("../../test-utils/callAsync") -var xhrMock = require("../../test-utils/xhrMock") -var Request = require("../../request/request") - -o.spec("request", function() { - var mock, request, complete - o.beforeEach(function() { - mock = xhrMock() - complete = o.spy() - request = Request(mock, complete).request - }) - - o.spec("success", function() { - o("works via GET", function(done) { - mock.$defineRoutes({ - "GET /item": function() { - return {status: 200, responseText: JSON.stringify({a: 1})} - } - }) - request({method: "GET", url: "/item"}).then(function(data) { - o(data).deepEquals({a: 1}) - }).then(function() { - done() - }) - }) - o("implicit GET method", function(done){ - mock.$defineRoutes({ - "GET /item": function() { - return {status: 200, responseText: JSON.stringify({a: 1})} - } - }) - request({url: "/item"}).then(function(data) { - o(data).deepEquals({a: 1}) - }).then(function() { - done() - }) - }) - o("first argument can be a string aliasing url property", function(done){ - mock.$defineRoutes({ - "GET /item": function() { - return {status: 200, responseText: JSON.stringify({a: 1})} - } - }) - request("/item").then(function(data) { - o(data).deepEquals({a: 1}) - }).then(function() { - done() - }) - }) - o("works via POST", function(done) { - mock.$defineRoutes({ - "POST /item": function() { - return {status: 200, responseText: JSON.stringify({a: 1})} - } - }) - request({method: "POST", url: "/item"}).then(function(data) { - o(data).deepEquals({a: 1}) - }).then(done) - }) - o("first argument can act as URI with second argument providing options", function(done) { - mock.$defineRoutes({ - "POST /item": function() { - return {status: 200, responseText: JSON.stringify({a: 1})} - } - }) - request("/item", {method: "POST"}).then(function(data) { - o(data).deepEquals({a: 1}) - }).then(done) - }) - o("first argument keeps protocol", function(done) { - mock.$defineRoutes({ - "POST /item": function(request) { - o(request.rawUrl).equals("https://example.com/item") - return {status: 200, responseText: JSON.stringify({a: 1})} - } - }) - request("https://example.com/item", {method: "POST"}).then(function(data) { - o(data).deepEquals({a: 1}) - }).then(done) - }) - o("works w/ parameterized data via GET", function(done) { - mock.$defineRoutes({ - "GET /item": function(request) { - return {status: 200, responseText: JSON.stringify({a: request.query})} - } - }) - request({method: "GET", url: "/item", params: {x: "y"}}).then(function(data) { - o(data).deepEquals({a: "?x=y"}) - }).then(done) - }) - o("works w/ parameterized data via POST", function(done) { - mock.$defineRoutes({ - "POST /item": function(request) { - return {status: 200, responseText: JSON.stringify({a: JSON.parse(request.body)})} - } - }) - request({method: "POST", url: "/item", body: {x: "y"}}).then(function(data) { - o(data).deepEquals({a: {x: "y"}}) - }).then(done) - }) - o("works w/ parameterized data containing colon via GET", function(done) { - mock.$defineRoutes({ - "GET /item": function(request) { - return {status: 200, responseText: JSON.stringify({a: request.query})} - } - }) - request({method: "GET", url: "/item", params: {x: ":y"}}).then(function(data) { - o(data).deepEquals({a: "?x=%3Ay"}) - }).then(done) - }) - o("works w/ parameterized data containing colon via POST", function(done) { - mock.$defineRoutes({ - "POST /item": function(request) { - return {status: 200, responseText: JSON.stringify({a: JSON.parse(request.body)})} - } - }) - request({method: "POST", url: "/item", body: {x: ":y"}}).then(function(data) { - o(data).deepEquals({a: {x: ":y"}}) - }).then(done) - }) - o("works w/ parameterized url via GET", function(done) { - mock.$defineRoutes({ - "GET /item/y": function(request) { - return {status: 200, responseText: JSON.stringify({a: request.url, b: request.query, c: request.body})} - } - }) - request({method: "GET", url: "/item/:x", params: {x: "y"}}).then(function(data) { - o(data).deepEquals({a: "/item/y", b: {}, c: null}) - }).then(done) - }) - o("works w/ parameterized url via POST", function(done) { - mock.$defineRoutes({ - "POST /item/y": function(request) { - return {status: 200, responseText: JSON.stringify({a: request.url, b: request.query, c: request.body})} - } - }) - request({method: "POST", url: "/item/:x", params: {x: "y"}}).then(function(data) { - o(data).deepEquals({a: "/item/y", b: {}, c: null}) - }).then(done) - }) - o("works w/ parameterized url + body via GET", function(done) { - mock.$defineRoutes({ - "GET /item/y": function(request) { - return {status: 200, responseText: JSON.stringify({a: request.url, b: request.query, c: JSON.parse(request.body)})} - } - }) - request({method: "GET", url: "/item/:x", params: {x: "y"}, body: {a: "b"}}).then(function(data) { - o(data).deepEquals({a: "/item/y", b: {}, c: {a: "b"}}) - }).then(done) - }) - o("works w/ parameterized url + body via POST", function(done) { - mock.$defineRoutes({ - "POST /item/y": function(request) { - return {status: 200, responseText: JSON.stringify({a: request.url, b: request.query, c: JSON.parse(request.body)})} - } - }) - request({method: "POST", url: "/item/:x", params: {x: "y"}, body: {a: "b"}}).then(function(data) { - o(data).deepEquals({a: "/item/y", b: {}, c: {a: "b"}}) - }).then(done) - }) - o("works w/ parameterized url + query via GET", function(done) { - mock.$defineRoutes({ - "GET /item/y": function(request) { - return {status: 200, responseText: JSON.stringify({a: request.url, b: request.query, c: request.body})} - } - }) - request({method: "GET", url: "/item/:x", params: {x: "y", q: "term"}}).then(function(data) { - o(data).deepEquals({a: "/item/y", b: "?q=term", c: null}) - }).then(done) - }) - o("works w/ parameterized url + query via POST", function(done) { - mock.$defineRoutes({ - "POST /item/y": function(request) { - return {status: 200, responseText: JSON.stringify({a: request.url, b: request.query, c: request.body})} - } - }) - request({method: "POST", url: "/item/:x", params: {x: "y", q: "term"}}).then(function(data) { - o(data).deepEquals({a: "/item/y", b: "?q=term", c: null}) - }).then(done) - }) - o("works w/ parameterized url + query + body via GET", function(done) { - mock.$defineRoutes({ - "GET /item/y": function(request) { - return {status: 200, responseText: JSON.stringify({a: request.url, b: request.query, c: JSON.parse(request.body)})} - } - }) - request({method: "GET", url: "/item/:x", params: {x: "y", q: "term"}, body: {a: "b"}}).then(function(data) { - o(data).deepEquals({a: "/item/y", b: "?q=term", c: {a: "b"}}) - }).then(done) - }) - o("works w/ parameterized url + query + body via POST", function(done) { - mock.$defineRoutes({ - "POST /item/y": function(request) { - return {status: 200, responseText: JSON.stringify({a: request.url, b: request.query, c: JSON.parse(request.body)})} - } - }) - request({method: "POST", url: "/item/:x", params: {x: "y", q: "term"}, body: {a: "b"}}).then(function(data) { - o(data).deepEquals({a: "/item/y", b: "?q=term", c: {a: "b"}}) - }).then(done) - }) - o("works w/ array", function(done) { - mock.$defineRoutes({ - "POST /items": function(request) { - return {status: 200, responseText: JSON.stringify({a: request.url, b: JSON.parse(request.body)})} - } - }) - request({method: "POST", url: "/items", body: [{x: "y"}]}).then(function(data) { - o(data).deepEquals({a: "/items", b: [{x: "y"}]}) - }).then(done) - }) - o("works w/ URLSearchParams body", function(done) { - mock.$defineRoutes({ - "POST /item": function(request) { - return {status: 200, responseText: JSON.stringify({a: request.url, b: request.body.toString()})} - } - }) - request({method: "POST", url: "/item", body: new URLSearchParams({x: "y", z: "w"})}).then(function(data) { - o(data).deepEquals({a: "/item", b: "x=y&z=w"}) - }).then(done) - }); - o("ignores unresolved parameter via GET", function(done) { - mock.$defineRoutes({ - "GET /item/:x": function(request) { - return {status: 200, responseText: JSON.stringify({a: request.url})} - } - }) - request({method: "GET", url: "/item/:x"}).then(function(data) { - o(data).deepEquals({a: "/item/:x"}) - }).then(done) - }) - o("ignores unresolved parameter via POST", function(done) { - mock.$defineRoutes({ - "GET /item/:x": function(request) { - return {status: 200, responseText: JSON.stringify({a: request.url})} - } - }) - request({method: "GET", url: "/item/:x"}).then(function(data) { - o(data).deepEquals({a: "/item/:x"}) - }).then(done) - }) - o("type parameter works for Array responses", function(done) { - var Entity = function(args) { - return {_id: args.id} - } - - mock.$defineRoutes({ - "GET /item": function() { - return {status: 200, responseText: JSON.stringify([{id: 1}, {id: 2}, {id: 3}])} - } - }) - request({method: "GET", url: "/item", type: Entity}).then(function(data) { - o(data).deepEquals([{_id: 1}, {_id: 2}, {_id: 3}]) - }).then(done) - }) - o("type parameter works for Object responses", function(done) { - var Entity = function(args) { - return {_id: args.id} - } - - mock.$defineRoutes({ - "GET /item": function() { - return {status: 200, responseText: JSON.stringify({id: 1})} - } - }) - request({method: "GET", url: "/item", type: Entity}).then(function(data) { - o(data).deepEquals({_id: 1}) - }).then(done) - }) - o("serialize parameter works in GET", function(done) { - var serialize = function(data) { - return "id=" + data.id - } - - mock.$defineRoutes({ - "GET /item": function(request) { - return {status: 200, responseText: JSON.stringify({body: request.query})} - } - }) - request({method: "GET", url: "/item", serialize: serialize, params: {id: 1}}).then(function(data) { - o(data.body).equals("?id=1") - }).then(done) - }) - o("serialize parameter works in POST", function(done) { - var serialize = function(data) { - return "id=" + data.id - } - - mock.$defineRoutes({ - "POST /item": function(request) { - return {status: 200, responseText: JSON.stringify({body: request.body})} - } - }) - request({method: "POST", url: "/item", serialize: serialize, body: {id: 1}}).then(function(data) { - o(data.body).equals("id=1") - }).then(done) - }) - o("deserialize parameter works in GET", function(done) { - var deserialize = function(data) { - return data - } - - mock.$defineRoutes({ - "GET /item": function() { - return {status: 200, responseText: JSON.stringify({test: 123})} - } - }) - request({method: "GET", url: "/item", deserialize: deserialize}).then(function(data) { - o(data).deepEquals({test: 123}) - }).then(done) - }) - o("deserialize parameter works in POST", function(done) { - var deserialize = function(data) { - return data - } - - mock.$defineRoutes({ - "POST /item": function() { - return {status: 200, responseText: JSON.stringify({test: 123})} - } - }) - request({method: "POST", url: "/item", deserialize: deserialize}).then(function(data) { - o(data).deepEquals({test: 123}) - }).then(done) - }) - o("extract parameter works in GET", function(done) { - var extract = function() { - return {test: 123} - } - - mock.$defineRoutes({ - "GET /item": function() { - return {status: 200, responseText: ""} - } - }) - request({method: "GET", url: "/item", extract: extract}).then(function(data) { - o(data).deepEquals({test: 123}) - }).then(done) - }) - o("extract parameter works in POST", function(done) { - var extract = function() { - return {test: 123} - } - - mock.$defineRoutes({ - "POST /item": function() { - return {status: 200, responseText: ""} - } - }) - request({method: "POST", url: "/item", extract: extract}).then(function(data) { - o(data).deepEquals({test: 123}) - }).then(done) - }) - o("ignores deserialize if extract is defined", function(done) { - var extract = function(data) { - return data.status - } - var deserialize = o.spy() - - mock.$defineRoutes({ - "GET /item": function() { - return {status: 200, responseText: ""} - } - }) - request({method: "GET", url: "/item", extract: extract, deserialize: deserialize}).then(function(data) { - o(data).equals(200) - }).then(function() { - o(deserialize.callCount).equals(0) - }).then(done) - }) - o("config parameter works", function(done) { - mock.$defineRoutes({ - "POST /item": function() { - return {status: 200, responseText: ""} - } - }) - request({method: "POST", url: "/item", config: config}).then(done) - - function config(xhr) { - o(typeof xhr.setRequestHeader).equals("function") - o(typeof xhr.open).equals("function") - o(typeof xhr.send).equals("function") - } - }) - o("requests don't block each other", function(done) { - mock.$defineRoutes({ - "GET /item": function() { - return {status: 200, responseText: "[]"} - } - }) - request("/item").then(function() { - return request("/item") - }) - request("/item").then(function() { - return request("/item") - }) - setTimeout(function() { - o(complete.callCount).equals(4) - done() - }, 20) - }) - o("requests trigger finally once with a chained then", function(done) { - mock.$defineRoutes({ - "GET /item": function() { - return {status: 200, responseText: "[]"} - } - }) - var promise = request("/item") - promise.then(function() {}).then(function() {}) - promise.then(function() {}).then(function() {}) - setTimeout(function() { - o(complete.callCount).equals(1) - done() - }, 20) - }) - o("requests does not trigger finally when background: true", function(done) { - mock.$defineRoutes({ - "GET /item": function() { - return {status: 200, responseText: "[]"} - } - }) - request("/item", {background: true}).then(function() {}) - - setTimeout(function() { - o(complete.callCount).equals(0) - done() - }, 20) - }) - o("headers are set when header arg passed", function(done) { - mock.$defineRoutes({ - "POST /item": function() { - return {status: 200, responseText: ""} - } - }) - request({method: "POST", url: "/item", config: config, headers: {"Custom-Header": "Value"}}).then(done) - - function config(xhr) { - o(xhr.getRequestHeader("Custom-Header")).equals("Value") - } - }) - o("headers are with higher precedence than default headers", function(done) { - mock.$defineRoutes({ - "POST /item": function() { - return {status: 200, responseText: ""} - } - }) - request({method: "POST", url: "/item", config: config, headers: {"Content-Type": "Value"}}).then(done) - - function config(xhr) { - o(xhr.getRequestHeader("Content-Type")).equals("Value") - } - }) - o("doesn't fail on abort", function(done) { - mock.$defineRoutes({ - "GET /item": function() { - return {status: 200, responseText: JSON.stringify({a: 1})} - } - }) - - var failed = false - var resolved = false - function handleAbort(xhr) { - var onreadystatechange = xhr.onreadystatechange - xhr.onreadystatechange = function() { - onreadystatechange.call(xhr, {target: xhr}) - setTimeout(function() { // allow promises to (not) resolve first - o(failed).equals(false) - o(resolved).equals(false) - done() - }, 0) - } - xhr.abort() - } - request({method: "GET", url: "/item", config: handleAbort}).catch(function() { - failed = true - }) - .then(function() { - resolved = true - }) - }) - o("doesn't fail on replaced abort", function(done) { - mock.$defineRoutes({ - "GET /item": function() { - return {status: 200, responseText: JSON.stringify({a: 1})} - } - }) - - var failed = false - var resolved = false - var abortSpy = o.spy() - var replacement - function handleAbort(xhr) { - var onreadystatechange = xhr.onreadystatechange - xhr.onreadystatechange = function() { - onreadystatechange.call(xhr, {target: xhr}) - setTimeout(function() { // allow promises to (not) resolve first - o(failed).equals(false) - o(resolved).equals(false) - done() - }, 0) - } - return replacement = { - send: xhr.send.bind(xhr), - abort: abortSpy, - } - } - request({method: "GET", url: "/item", config: handleAbort}).then(function() { - resolved = true - }, function() { - failed = true - }) - replacement.abort() - o(abortSpy.callCount).equals(1) - }) - o("doesn't fail on file:// status 0", function(done) { - mock.$defineRoutes({ - "GET /item": function() { - return {status: 0, responseText: JSON.stringify({a: 1})} - } - }) - var failed = false - request({method: "GET", url: "file:///item"}).catch(function() { - failed = true - }).then(function(data) { - o(failed).equals(false) - o(data).deepEquals({a: 1}) - }).then(function() { - done() - }) - }) - o("set timeout to xhr instance", function() { - mock.$defineRoutes({ - "GET /item": function() { - return {status: 200, responseText: ""} - } - }) - return request({ - method: "GET", url: "/item", - timeout: 42, - config: function(xhr) { - o(xhr.timeout).equals(42) - } - }) - }) - o("set responseType to request instance", function() { - mock.$defineRoutes({ - "GET /item": function() { - return {status: 200, responseText: ""} - } - }) - return request({ - method: "GET", url: "/item", - responseType: "blob", - config: function(xhr) { - o(xhr.responseType).equals("blob") - } - }) - }) - o("params unmodified after interpolate", function() { - mock.$defineRoutes({ - "PUT /items/1": function() { - return {status: 200, responseText: "[]"} - } - }) - var params = {x: 1, y: 2} - var p = request({method: "PUT", url: "/items/:x", params: params}) - - o(params).deepEquals({x: 1, y: 2}) - - return p - }) - o("can return replacement from config", function() { - mock.$defineRoutes({ - "GET /a": function() { - return {status: 200, responseText: "[]"} - } - }) - var result - return request({ - url: "/a", - config: function(xhr) { - return result = { - send: o.spy(xhr.send.bind(xhr)), - } - }, - }) - .then(function () { - o(result.send.callCount).equals(1) - }) - }) - o("can abort from replacement", function() { - mock.$defineRoutes({ - "GET /a": function() { - return {status: 200, responseText: "[]"} - } - }) - var result - - request({ - url: "/a", - config: function(xhr) { - return result = { - send: o.spy(xhr.send.bind(xhr)), - abort: o.spy(), - } - }, - }) - - result.abort() - }) - }) - o.spec("failure", function() { - o("rejects on server error", function(done) { - mock.$defineRoutes({ - "GET /item": function() { - return {status: 500, responseText: JSON.stringify({error: "error"})} - } - }) - request({method: "GET", url: "/item"}).catch(function(e) { - o(e instanceof Error).equals(true) - o(e.message).equals("[object Object]") - o(e.response).deepEquals({error: "error"}) - o(e.code).equals(500) - }).then(done) - }) - o("adds response to Error", function(done) { - mock.$defineRoutes({ - "GET /item": function() { - return {status: 500, responseText: JSON.stringify({message: "error", stack: "error on line 1"})} - } - }) - request({method: "GET", url: "/item"}).catch(function(e) { - o(e instanceof Error).equals(true) - o(e.response.message).equals("error") - o(e.response.stack).equals("error on line 1") - }).then(done) - }) - o("rejects on non-JSON server error", function(done) { - mock.$defineRoutes({ - "GET /item": function() { - return {status: 500, responseText: "error"} - } - }) - request({method: "GET", url: "/item"}).catch(function(e) { - o(e.message).equals("null") - o(e.response).equals(null) - }).then(done) - }) - o("triggers all branched catches upon rejection", function(done) { - mock.$defineRoutes({ - "GET /item": function() { - return {status: 500, responseText: "error"} - } - }) - var promise = request({method: "GET", url: "/item"}) - var then = o.spy() - var catch1 = o.spy() - var catch2 = o.spy() - var catch3 = o.spy() - - promise.catch(catch1) - promise.then(then, catch2) - promise.then(then).catch(catch3) - - callAsync(function() { - callAsync(function() { - callAsync(function() { - o(catch1.callCount).equals(1) - o(then.callCount).equals(0) - o(catch2.callCount).equals(1) - o(catch3.callCount).equals(1) - done() - }) - }) - }) - }) - o("rejects on cors-like error", function(done) { - mock.$defineRoutes({ - "GET /item": function() { - return {status: 0} - } - }) - request({method: "GET", url: "/item"}).catch(function(e) { - o(e instanceof Error).equals(true) - }).then(done) - }) - o("rejects on request timeout", function(done) { - var timeout = 50 - var timeToGetItem = timeout + 1 - - mock.$defineRoutes({ - "GET /item": function() { - return new Promise(function(resolve) { - setTimeout(function() { - resolve({status: 200}) - }, timeToGetItem) - }) - } - }) - - request({ - method: "GET", url: "/item", - timeout: timeout - }).catch(function(e) { - o(e instanceof Error).equals(true) - o(e.message).equals("Request timed out") - o(e.code).equals(0) - }).then(function() { - done() - }) - }) - o("does not reject when time to request resource does not exceed timeout", function(done) { - var timeout = 50 - var timeToGetItem = timeout - 1 - var isRequestRejected = false - - mock.$defineRoutes({ - "GET /item": function() { - return new Promise(function(resolve) { - setTimeout(function() { - resolve({status: 200}) - }, timeToGetItem) - }) - } - }) - - request({ - method: "GET", url: "/item", - timeout: timeout - }).catch(function(e) { - isRequestRejected = true - o(e.message).notEquals("Request timed out") - }).then(function() { - o(isRequestRejected).equals(false) - done() - }) - }) - o("does not reject on status error code when extract provided", function(done) { - mock.$defineRoutes({ - "GET /item": function() { - return {status: 500, responseText: JSON.stringify({message: "error"})} - } - }) - request({ - method: "GET", url: "/item", - extract: function(xhr) {return JSON.parse(xhr.responseText)} - }).then(function(data) { - o(data.message).equals("error") - done() - }) - }) - o("rejects on error in extract", function(done) { - mock.$defineRoutes({ - "GET /item": function() { - return {status: 200, responseText: JSON.stringify({a: 1})} - } - }) - request({ - method: "GET", url: "/item", - extract: function() {throw new Error("error")} - }).catch(function(e) { - o(e instanceof Error).equals(true) - o(e.message).equals("error") - }).then(function() { - done() - }) - }) - }) - o.spec("json header", function() { - function checkUnset(method) { - o("doesn't set header on " + method + " without body", function(done) { - var routes = {} - routes[method + " /item"] = function() { - return {status: 200, responseText: JSON.stringify({a: 1})} - } - mock.$defineRoutes(routes) - request({ - method: method, url: "/item", - config: function(xhr) { - var header = xhr.getRequestHeader("Content-Type") - o(header).equals(undefined) - header = xhr.getRequestHeader("Accept") - o(header).equals("application/json, text/*") - } - }).then(function(result) { - o(result).deepEquals({a: 1}) - done() - }).catch(function(e) { - done(e) - }) - }) - } - - function checkSet(method, body) { - o("sets header on " + method + " with body", function(done) { - var routes = {} - routes[method + " /item"] = function(response) { - return { - status: 200, - responseText: JSON.stringify({body: JSON.parse(response.body)}), - } - } - mock.$defineRoutes(routes) - request({ - method: method, url: "/item", body: body, - config: function(xhr) { - var header = xhr.getRequestHeader("Content-Type") - o(header).equals("application/json; charset=utf-8") - header = xhr.getRequestHeader("Accept") - o(header).equals("application/json, text/*") - } - }).then(function(result) { - o(result).deepEquals({body: body}) - done() - }).catch(function(e) { - done(e) - }) - }) - } - - checkUnset("GET") - checkUnset("HEAD") - checkUnset("OPTIONS") - checkUnset("POST") - checkUnset("PUT") - checkUnset("DELETE") - checkUnset("PATCH") - - checkSet("GET", {foo: "bar"}) - checkSet("HEAD", {foo: "bar"}) - checkSet("OPTIONS", {foo: "bar"}) - checkSet("POST", {foo: "bar"}) - checkSet("PUT", {foo: "bar"}) - checkSet("DELETE", {foo: "bar"}) - checkSet("PATCH", {foo: "bar"}) - }) - - // See: https://github.com/MithrilJS/mithril.js/issues/2426 - // - // TL;DR: lots of subtlety. Make sure you read the ES spec closely before - // updating this code or the corresponding finalizer code in - // `request/request` responsible for scheduling autoredraws, or you might - // inadvertently break things. - // - // The precise behavior here is that it schedules a redraw immediately after - // the second tick *after* the promise resolves, but `await` in engines that - // have implemented the change in https://github.com/tc39/ecma262/pull/1250 - // will only take one tick to get the value. Engines that haven't - // implemented that spec change would wait until the tick after the redraw - // was scheduled before it can see the new value. But this only applies when - // the engine needs to coerce the value, and this is where things get a bit - // hairy. As per spec, V8 checks the `.constructor` property of promises and - // if that `=== Promise`, it does *not* coerce it using `.then`, but instead - // just resolves it directly. This, of course, can screw with our autoredraw - // behavior, and we have to work around that. At the time of writing, no - // other browser checks for this additional constraint, and just blindly - // invokes `.then` instead, and so we end up working as anticipated. But for - // obvious reasons, it's a bad idea to rely on a spec violation for things - // to work unless the spec itself is clearly broken (in this case, it's - // not). And so we need to test for this very unusual edge case. - // - // The direct `eval` is just so I can convert early errors to runtime - // errors without having to explicitly wire up all the bindings set up in - // `o.beforeEach`. I evaluate it immediately inside a `try`/`catch` instead - // of inside the test code so any relevant syntax error can be detected - // ahead of time and the test skipped entirely. It might trigger mental - // alarms because `eval` is normally asking for problems, but this is a - // rare case where it's genuinely safe and rational. - try { - // eslint-disable-next-line no-eval - var runAsyncTest = eval( - "async () => {\n" + - " var p = request('/item')\n" + - " o(complete.callCount).equals(0)\n" + - // Note: this step does *not* invoke `.then` on the promise returned - // from `p.then(resolve, reject)`. - " await p\n" + - // The spec prior to https://github.com/tc39/ecma262/pull/1250 used - // to take 3 ticks instead of 1, so `complete` would have been - // called already and we would've been done. After it, it now takes - // 1 tick and so `complete` wouldn't have yet been called - it takes - // 2 ticks to get called. And so we have to wait for one more ticks - // for `complete` to get called. - " await null\n" + - " o(complete.callCount).equals(1)\n" + - "}" - ) - - o("invokes the redraw in native async/await", function () { - // Use the native promise for correct semantics. This test will fail - // 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. - request = Request(mock, complete).request - mock.$defineRoutes({ - "GET /item": function() { - return {status: 200, responseText: "[]"} - } - }) - return runAsyncTest() - }) - } catch (e) { - // ignore - this is just for browsers that natively support - // `async`/`await`, like most modern browsers. - // it's just a syntax error anyways. - } -}) From a4521055ff79f141723c88d69e45f3d3b6d73246 Mon Sep 17 00:00:00 2001 From: Claudia Meadows Date: Wed, 2 Oct 2024 20:14:45 -0700 Subject: [PATCH 14/95] Migrate `m.buildPathname` to `m.p` and remove the path params argument from the router --- api/router.js | 19 ++++---- api/tests/test-router.js | 34 +------------- api/tests/test-routerGetSet.js | 9 ++-- index.js | 2 +- .../{test-buildPathname.js => test-build.js} | 46 +++++++++---------- 5 files changed, 39 insertions(+), 71 deletions(-) rename pathname/tests/{test-buildPathname.js => test-build.js} (63%) diff --git a/api/router.js b/api/router.js index ad17883c7..ab913cefd 100644 --- a/api/router.js +++ b/api/router.js @@ -2,7 +2,6 @@ var m = require("../render/hyperscript") -var buildPathname = require("../pathname/build") var parsePathname = require("../pathname/parse") var compileTemplate = require("../pathname/compileTemplate") var censor = require("../util/censor") @@ -80,7 +79,7 @@ module.exports = function($window, mountRedraw) { function reject(e) { console.error(e) - setPath(fallbackRoute, null, {replace: true}) + setPath(fallbackRoute, {replace: true}) } loop(0) @@ -124,7 +123,7 @@ module.exports = function($window, mountRedraw) { if (path === fallbackRoute) { throw new Error("Could not resolve default route " + fallbackRoute + ".") } - setPath(fallbackRoute, null, {replace: true}) + setPath(fallbackRoute, {replace: true}) } } @@ -142,8 +141,7 @@ module.exports = function($window, mountRedraw) { } } - function setPath(path, data, options) { - path = buildPathname(path, data) + function setPath(path, options) { if (ready) { fireAsync() var state = options ? options.state : null @@ -189,13 +187,13 @@ module.exports = function($window, mountRedraw) { mountRedraw.mount(root, RouterRoot) resolveRoute() } - route.set = function(path, data, options) { + route.set = function(path, options) { if (lastUpdate != null) { options = options || {} options.replace = true } lastUpdate = null - setPath(path, data, options) + setPath(path, options) } route.get = function() {return currentPath} route.prefix = "#!" @@ -208,7 +206,7 @@ module.exports = function($window, mountRedraw) { // let them be specified in the selector as well. var child = m( vnode.attrs.selector || "a", - censor(vnode.attrs, ["options", "params", "selector", "onclick"]), + censor(vnode.attrs, ["options", "selector", "onclick"]), vnode.children ) var options, onclick, href @@ -227,8 +225,7 @@ module.exports = function($window, mountRedraw) { } else { options = vnode.attrs.options onclick = vnode.attrs.onclick - // Easier to build it now to keep it isomorphic. - href = buildPathname(child.attrs.href, vnode.attrs.params) + href = child.attrs.href child.attrs.href = route.prefix + href child.attrs.onclick = function(e) { var result @@ -261,7 +258,7 @@ module.exports = function($window, mountRedraw) { ) { e.preventDefault() e.redraw = false - route.set(href, null, options) + route.set(href, options) } } } diff --git a/api/tests/test-router.js b/api/tests/test-router.js index c6b07cd42..199d2a4a6 100644 --- a/api/tests/test-router.js +++ b/api/tests/test-router.js @@ -568,37 +568,7 @@ o.spec("route", function() { root.firstChild.dispatchEvent(e) o(route.set.callCount).equals(1) - o(route.set.args[2]).equals(opts) - }) - - o("passes params on route.Link", function() { - var e = $window.document.createEvent("MouseEvents") - - e.initEvent("click", true, true) - e.button = 0 - $window.location.href = prefix + "/" - - route(root, "/", { - "/": () => ({ - view: lock(function() { - return m(route.Link, { - href: "/test", - params: {key: "value"}, - }) - }) - }), - "/test": () => ({ - view : lock(function() { - return m("div") - }) - }) - }) - route.set = o.spy(route.set) - - root.firstChild.dispatchEvent(e) - - o(route.set.callCount).equals(1) - o(route.set.args[0]).equals("/test?key=value") + o(route.set.args[1]).equals(opts) }) o("route.Link can render without routes or dom access", function() { @@ -1380,7 +1350,7 @@ o.spec("route", function() { route(root, "/a", { "/a": { onmatch: lock(function() { - route.set("/b", {}, {state: {a: 5}}) + route.set("/b", {state: {a: 5}}) }), render: lock(render) }, diff --git a/api/tests/test-routerGetSet.js b/api/tests/test-routerGetSet.js index 719f040b7..a624aaa9e 100644 --- a/api/tests/test-routerGetSet.js +++ b/api/tests/test-routerGetSet.js @@ -7,6 +7,7 @@ var throttleMocker = require("../../test-utils/throttleMock") var apiMountRedraw = require("../../api/mount-redraw") var apiRouter = require("../../api/router") +var p = require("../../pathname/build") o.spec("route.get/route.set", function() { function waitTask() { @@ -213,7 +214,7 @@ o.spec("route.get/route.set", function() { "/other/:a/:b...": () => ({view: function() {}}), }) - route.set("/other/:a/:b", {a: "x", b: "y/z", c: "d", e: "f"}) + route.set(p("/other/:a/:b", {a: "x", b: "y/z", c: "d", e: "f"})) return waitTask().then(() => { // Yep, before even the throttle mechanism takes hold. o(route.get()).equals("/other/x/y%2Fz?c=d&e=f") @@ -228,7 +229,7 @@ o.spec("route.get/route.set", function() { "/other": () => ({view: function() {}}), }) - route.set("/other", null, {replace: true}) + route.set("/other", {replace: true}) return waitTask().then(() => { throttleMock.fire() @@ -244,7 +245,7 @@ o.spec("route.get/route.set", function() { "/other": () => ({view: function() {}}), }) - route.set("/other", null, {replace: false}) + route.set("/other", {replace: false}) return waitTask().then(() => { throttleMock.fire() @@ -261,7 +262,7 @@ o.spec("route.get/route.set", function() { "/other": () => ({view: function() {}}), }) - route.set("/other", null, {state: {a: 1}}) + route.set("/other", {state: {a: 1}}) return waitTask().then(() => { throttleMock.fire() o($window.history.state).deepEquals({a: 1}) diff --git a/index.js b/index.js index bd7697007..8b4391286 100644 --- a/index.js +++ b/index.js @@ -15,7 +15,7 @@ m.redraw = mountRedraw.redraw m.parseQueryString = require("./querystring/parse") m.buildQueryString = require("./querystring/build") m.parsePathname = require("./pathname/parse") -m.buildPathname = require("./pathname/build") +m.p = require("./pathname/build") m.vnode = require("./render/vnode") m.censor = require("./util/censor") diff --git a/pathname/tests/test-buildPathname.js b/pathname/tests/test-build.js similarity index 63% rename from pathname/tests/test-buildPathname.js rename to pathname/tests/test-build.js index 0b763d154..fb0c18d7c 100644 --- a/pathname/tests/test-buildPathname.js +++ b/pathname/tests/test-build.js @@ -1,92 +1,92 @@ "use strict" var o = require("ospec") -var buildPathname = require("../../pathname/build") +var p = require("../build") -o.spec("buildPathname", function() { +o.spec("p", function() { function test(prefix) { o("returns path if no params", function () { - var string = buildPathname(prefix + "/route/foo", undefined) + var string = p(prefix + "/route/foo", undefined) o(string).equals(prefix + "/route/foo") }) o("skips interpolation if no params", function () { - var string = buildPathname(prefix + "/route/:id", undefined) + var string = p(prefix + "/route/:id", undefined) o(string).equals(prefix + "/route/:id") }) o("appends query strings", function () { - var string = buildPathname(prefix + "/route/foo", {a: "b", c: 1}) + var string = p(prefix + "/route/foo", {a: "b", c: 1}) o(string).equals(prefix + "/route/foo?a=b&c=1") }) o("inserts template parameters at end", function () { - var string = buildPathname(prefix + "/route/:id", {id: "1"}) + var string = p(prefix + "/route/:id", {id: "1"}) o(string).equals(prefix + "/route/1") }) o("inserts template parameters at beginning", function () { - var string = buildPathname(prefix + "/:id/foo", {id: "1"}) + var string = p(prefix + "/:id/foo", {id: "1"}) o(string).equals(prefix + "/1/foo") }) o("inserts template parameters at middle", function () { - var string = buildPathname(prefix + "/route/:id/foo", {id: "1"}) + var string = p(prefix + "/route/:id/foo", {id: "1"}) o(string).equals(prefix + "/route/1/foo") }) o("inserts variadic paths", function () { - var string = buildPathname(prefix + "/route/:foo...", {foo: "id/1"}) + var string = p(prefix + "/route/:foo...", {foo: "id/1"}) o(string).equals(prefix + "/route/id/1") }) o("inserts variadic paths with initial slashes", function () { - var string = buildPathname(prefix + "/route/:foo...", {foo: "/id/1"}) + var string = p(prefix + "/route/:foo...", {foo: "/id/1"}) o(string).equals(prefix + "/route//id/1") }) o("skips template parameters at end if param missing", function () { - var string = buildPathname(prefix + "/route/:id", {param: 1}) + var string = p(prefix + "/route/:id", {param: 1}) o(string).equals(prefix + "/route/:id?param=1") }) o("skips template parameters at beginning if param missing", function () { - var string = buildPathname(prefix + "/:id/foo", {param: 1}) + var string = p(prefix + "/:id/foo", {param: 1}) o(string).equals(prefix + "/:id/foo?param=1") }) o("skips template parameters at middle if param missing", function () { - var string = buildPathname(prefix + "/route/:id/foo", {param: 1}) + var string = p(prefix + "/route/:id/foo", {param: 1}) o(string).equals(prefix + "/route/:id/foo?param=1") }) o("skips variadic template parameters if param missing", function () { - var string = buildPathname(prefix + "/route/:foo...", {param: "/id/1"}) + var string = p(prefix + "/route/:foo...", {param: "/id/1"}) o(string).equals(prefix + "/route/:foo...?param=%2Fid%2F1") }) o("handles escaped values", function() { - var data = buildPathname(prefix + "/route/:foo", {"foo": ";:@&=+$,/?%#"}) + var data = p(prefix + "/route/:foo", {"foo": ";:@&=+$,/?%#"}) o(data).equals(prefix + "/route/%3B%3A%40%26%3D%2B%24%2C%2F%3F%25%23") }) o("handles unicode", function() { - var data = buildPathname(prefix + "/route/:ö", {"ö": "ö"}) + var data = p(prefix + "/route/:ö", {"ö": "ö"}) o(data).equals(prefix + "/route/%C3%B6") }) o("handles zero", function() { - var string = buildPathname(prefix + "/route/:a", {a: 0}) + var string = p(prefix + "/route/:a", {a: 0}) o(string).equals(prefix + "/route/0") }) o("handles false", function() { - var string = buildPathname(prefix + "/route/:a", {a: false}) + var string = p(prefix + "/route/:a", {a: false}) o(string).equals(prefix + "/route/false") }) o("handles dashes", function() { - var string = buildPathname(prefix + "/:lang-:region/route", { + var string = p(prefix + "/:lang-:region/route", { lang: "en", region: "US" }) @@ -94,7 +94,7 @@ o.spec("buildPathname", function() { o(string).equals(prefix + "/en-US/route") }) o("handles dots", function() { - var string = buildPathname(prefix + "/:file.:ext/view", { + var string = p(prefix + "/:file.:ext/view", { file: "image", ext: "png" }) @@ -102,17 +102,17 @@ o.spec("buildPathname", function() { o(string).equals(prefix + "/image.png/view") }) o("merges query strings", function() { - var string = buildPathname(prefix + "/item?a=1&b=2", {c: 3}) + var string = p(prefix + "/item?a=1&b=2", {c: 3}) o(string).equals(prefix + "/item?a=1&b=2&c=3") }) o("merges query strings with other parameters", function() { - var string = buildPathname(prefix + "/item/:id?a=1&b=2", {id: "foo", c: 3}) + var string = p(prefix + "/item/:id?a=1&b=2", {id: "foo", c: 3}) o(string).equals(prefix + "/item/foo?a=1&b=2&c=3") }) o("consumes template parameters without modifying query string", function() { - var string = buildPathname(prefix + "/item/:id?a=1&b=2", {id: "foo"}) + var string = p(prefix + "/item/:id?a=1&b=2", {id: "foo"}) o(string).equals(prefix + "/item/foo?a=1&b=2") }) From 8c619c30d1efeffb698c29884eece47e2103a779 Mon Sep 17 00:00:00 2001 From: Claudia Meadows Date: Wed, 2 Oct 2024 21:40:19 -0700 Subject: [PATCH 15/95] Add a `m.withProgress` to replace `onprogress` with `fetch`. --- index.js | 1 + util/tests/test-withProgress.js | 68 +++++++++++++++++++++++++++++++++ util/with-progress.js | 25 ++++++++++++ 3 files changed, 94 insertions(+) create mode 100644 util/tests/test-withProgress.js create mode 100644 util/with-progress.js diff --git a/index.js b/index.js index 8b4391286..5ee40f734 100644 --- a/index.js +++ b/index.js @@ -16,6 +16,7 @@ m.parseQueryString = require("./querystring/parse") m.buildQueryString = require("./querystring/build") m.parsePathname = require("./pathname/parse") m.p = require("./pathname/build") +m.withProgress = require("./util/with-progress") m.vnode = require("./render/vnode") m.censor = require("./util/censor") diff --git a/util/tests/test-withProgress.js b/util/tests/test-withProgress.js new file mode 100644 index 000000000..0aa298869 --- /dev/null +++ b/util/tests/test-withProgress.js @@ -0,0 +1,68 @@ +"use strict" + +var o = require("ospec") +var withProgress = require("../with-progress") + +if (typeof ReadableStream === "function") { + o.spec("withProgress", () => { + function sequence(chunks) { + let i = 0 + return new ReadableStream({ + type: "bytes", + pull(ctrl) { + if (i === chunks.length) { + ctrl.close() + } else { + ctrl.enqueue(Uint8Array.from(chunks[i++])) + } + }, + }) + } + + function drain(stream) { + return new Response(stream).arrayBuffer().then((buf) => [...new Uint8Array(buf)]) + } + + o("handles null body", () => { + var reports = [] + var watched = withProgress(null, (current) => reports.push(current)) + + return drain(watched).then((result) => { + o(result).deepEquals([]) + o(reports).deepEquals([]) + }) + }) + + o("handles empty body", () => { + var reports = [] + var watched = withProgress(sequence([]), (current) => reports.push(current)) + + return drain(watched).then((result) => { + o(result).deepEquals([]) + o(reports).deepEquals([]) + }) + }) + + o("adds single non-empty chunk", () => { + var reports = [] + var watched = withProgress(sequence([[10]]), (current) => reports.push(current)) + + return drain(watched).then((result) => { + o(result).deepEquals([10]) + o(reports).deepEquals([1]) + }) + }) + + o("adds multiple non-empty chunks", () => { + var reports = [] + var watched = withProgress(sequence([[10], [20]]), (current) => reports.push(current)) + + return drain(watched).then((result) => { + o(result).deepEquals([10, 20]) + o(reports).deepEquals([1, 2]) + }) + }) + }) +} else { + console.log("Skipping `withProgress` as `ReadableStream` is missing.") +} diff --git a/util/with-progress.js b/util/with-progress.js new file mode 100644 index 000000000..724b8be32 --- /dev/null +++ b/util/with-progress.js @@ -0,0 +1,25 @@ +"use strict" + +/** + * @param {ReadableStream | null} source + * @param {(current: number) => void} notify + */ +module.exports = (source, notify) => { + var reader = source && source.getReader() + var current = 0 + + return new ReadableStream({ + type: "bytes", + start: (ctrl) => reader || ctrl.close(), + cancel: (reason) => reader.cancel(reason), + pull: (ctrl) => reader.read().then((result) => { + if (result.done) { + ctrl.close() + } else { + current += result.value.length + ctrl.enqueue(result.value) + notify(current) + } + }), + }) +} From 1b0818ef866a95b36b7b3d8e6e1710fd68e485c8 Mon Sep 17 00:00:00 2001 From: Claudia Meadows Date: Wed, 2 Oct 2024 21:55:42 -0700 Subject: [PATCH 16/95] Have `m.mount` mount a view function rather than a full component --- api/mount-redraw.js | 13 +- api/router.js | 2 +- api/tests/test-mountRedraw.js | 490 +++++++++++++++----------------- render/tests/manual/iframe.html | 2 +- tests/test-api.js | 20 +- 5 files changed, 244 insertions(+), 283 deletions(-) diff --git a/api/mount-redraw.js b/api/mount-redraw.js index 44862c00e..e62994d6b 100644 --- a/api/mount-redraw.js +++ b/api/mount-redraw.js @@ -1,6 +1,5 @@ "use strict" -var Vnode = require("../render/vnode") var render = require("../render/render") module.exports = function(schedule, console) { @@ -10,7 +9,7 @@ module.exports = function(schedule, console) { function sync() { for (offset = 0; offset < subscriptions.length; offset += 2) { - try { render(subscriptions[offset], Vnode(subscriptions[offset + 1]), redraw) } + try { render(subscriptions[offset], (0, subscriptions[offset + 1])(), redraw) } catch (e) { console.error(e) } } offset = -1 @@ -28,8 +27,8 @@ module.exports = function(schedule, console) { redraw.sync = sync - function mount(root, component) { - if (component != null && typeof component !== "function") { + function mount(root, view) { + if (view != null && typeof view !== "function") { throw new TypeError("m.mount expects a component, not a vnode.") } @@ -40,9 +39,9 @@ module.exports = function(schedule, console) { render(root, []) } - if (component != null) { - subscriptions.push(root, component) - render(root, Vnode(component), redraw) + if (view != null) { + subscriptions.push(root, view) + render(root, view(), redraw) } } diff --git a/api/router.js b/api/router.js index ab913cefd..2197d11a0 100644 --- a/api/router.js +++ b/api/router.js @@ -184,7 +184,7 @@ module.exports = function($window, mountRedraw) { } ready = true - mountRedraw.mount(root, RouterRoot) + mountRedraw.mount(root, () => m(RouterRoot)) resolveRoute() } route.set = function(path, options) { diff --git a/api/tests/test-mountRedraw.js b/api/tests/test-mountRedraw.js index 90b40f195..3cdd658e4 100644 --- a/api/tests/test-mountRedraw.js +++ b/api/tests/test-mountRedraw.js @@ -2,7 +2,6 @@ // Low-priority TODO: remove the dependency on the renderer here. var o = require("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") @@ -27,6 +26,10 @@ o.spec("mount/redraw", function() { o(throttleMock.queueLength()).equals(0) }) + var Inline = () => ({ + view: ({attrs}) => attrs.view(), + }) + o("shouldn't error if there are no renderers", function() { m.redraw() throttleMock.fire() @@ -35,7 +38,7 @@ o.spec("mount/redraw", function() { o("schedules correctly", function() { var spy = o.spy() - m.mount(root, () => ({view: spy})) + m.mount(root, spy) o(spy.callCount).equals(1) m.redraw() o(spy.callCount).equals(1) @@ -46,7 +49,7 @@ o.spec("mount/redraw", function() { o("should run a single renderer entry", function() { var spy = o.spy() - m.mount(root, () => ({view: spy})) + m.mount(root, spy) o(spy.callCount).equals(1) @@ -67,9 +70,9 @@ o.spec("mount/redraw", function() { var spy2 = o.spy() var spy3 = o.spy() - m.mount(el1, () => ({view: spy1})) - m.mount(el2, () => ({view: spy2})) - m.mount(el3, () => ({view: spy3})) + m.mount(el1, spy1) + m.mount(el2, spy2) + m.mount(el3, spy3) m.redraw() @@ -98,17 +101,17 @@ o.spec("mount/redraw", function() { var spy2 = o.spy() var spy3 = o.spy() - m.mount(el1, () => ({view: spy1})) + m.mount(el1, spy1) o(spy1.callCount).equals(1) o(spy2.callCount).equals(0) o(spy3.callCount).equals(0) - m.mount(el2, () => ({view: spy2})) + m.mount(el2, spy2) o(spy1.callCount).equals(1) o(spy2.callCount).equals(1) o(spy3.callCount).equals(0) - m.mount(el3, () => ({view: spy3})) + m.mount(el3, spy3) o(spy1.callCount).equals(1) o(spy2.callCount).equals(1) o(spy3.callCount).equals(1) @@ -117,7 +120,7 @@ o.spec("mount/redraw", function() { o("should stop running after mount null", function() { var spy = o.spy() - m.mount(root, () => ({view: spy})) + m.mount(root, spy) o(spy.callCount).equals(1) m.mount(root, null) @@ -131,7 +134,7 @@ o.spec("mount/redraw", function() { o("should stop running after mount undefined", function() { var spy = o.spy() - m.mount(root, () => ({view: spy})) + m.mount(root, spy) o(spy.callCount).equals(1) m.mount(root, undefined) @@ -145,7 +148,7 @@ o.spec("mount/redraw", function() { o("should stop running after mount no arg", function() { var spy = o.spy() - m.mount(root, () => ({view: spy})) + m.mount(root, spy) o(spy.callCount).equals(1) m.mount(root) @@ -157,10 +160,10 @@ o.spec("mount/redraw", function() { }) o("should invoke remove callback on unmount", function() { - var spy = o.spy() + var spy = o.spy(() => h.fragment({onremove})) var onremove = o.spy() - m.mount(root, () => ({view: spy, onremove: onremove})) + m.mount(root, spy) o(spy.callCount).equals(1) m.mount(root) @@ -171,7 +174,7 @@ o.spec("mount/redraw", function() { o("should stop running after unsubscribe, even if it occurs after redraw is requested", function() { var spy = o.spy() - m.mount(root, () => ({view: spy})) + m.mount(root, spy) o(spy.callCount).equals(1) m.redraw() m.mount(root) @@ -184,7 +187,7 @@ o.spec("mount/redraw", function() { o("does nothing on invalid unmount", function() { var spy = o.spy() - m.mount(root, () => ({view: spy})) + m.mount(root, spy) o(spy.callCount).equals(1) m.mount(null) @@ -201,9 +204,9 @@ o.spec("mount/redraw", function() { var spy2 = o.spy() var spy3 = o.spy() - m.mount(el1, () => ({view: spy1})) - m.mount(el2, () => ({view: spy2})) - m.mount(el3, () => ({view: spy3})) + m.mount(el1, spy1) + m.mount(el2, spy2) + m.mount(el3, spy3) o(spy1.callCount).equals(1) o(spy2.callCount).equals(1) @@ -223,7 +226,7 @@ o.spec("mount/redraw", function() { }) - o("throws on invalid component", function() { + o("throws on invalid view", function() { o(function() { m.mount(root, {}) }).throws(TypeError) }) @@ -233,14 +236,12 @@ o.spec("mount/redraw", function() { var root2 = $document.createElement("div") var root3 = $document.createElement("div") - m.mount(root1, () => ({ - onbeforeupdate: function() { - m.mount(root2, null) - }, - view: function() { calls.push("root1") }, + m.mount(root1, () => h(Inline, { + onbeforeupdate() { m.mount(root2, null) }, + view() { calls.push("root1") }, })) - m.mount(root2, () => ({view: function() { calls.push("root2") }})) - m.mount(root3, () => ({view: function() { calls.push("root3") }})) + m.mount(root2, () => { calls.push("root2") }) + m.mount(root3, () => { calls.push("root3") }) o(calls).deepEquals([ "root1", "root2", "root3", ]) @@ -258,14 +259,12 @@ o.spec("mount/redraw", function() { var root2 = $document.createElement("div") var root3 = $document.createElement("div") - m.mount(root1, () => ({view: function() { calls.push("root1") }})) - m.mount(root2, () => ({ - onbeforeupdate: function() { - m.mount(root1, null) - }, - view: function() { calls.push("root2") }, + m.mount(root1, () => { calls.push("root1") }) + m.mount(root2, () => h(Inline, { + onbeforeupdate() { m.mount(root1, null) }, + view() { calls.push("root2") }, })) - m.mount(root3, () => ({view: function() { calls.push("root3") }})) + m.mount(root3, () => { calls.push("root3") }) o(calls).deepEquals([ "root1", "root2", "root3", ]) @@ -284,15 +283,12 @@ o.spec("mount/redraw", function() { var root2 = $document.createElement("div") var root3 = $document.createElement("div") - m.mount(root1, () => ({view: function() { calls.push("root1") }})) - m.mount(root2, () => ({ - onbeforeupdate: function() { - m.mount(root1, null) - throw "fail" - }, - view: function() { calls.push("root2") }, + m.mount(root1, () => { calls.push("root1") }) + m.mount(root2, () => h(Inline, { + onbeforeupdate() { m.mount(root1, null); throw "fail" }, + view() { calls.push("root2") }, })) - m.mount(root3, () => ({view: function() { calls.push("root3") }})) + m.mount(root3, () => { calls.push("root3") }) o(calls).deepEquals([ "root1", "root2", "root3", ]) @@ -310,14 +306,14 @@ o.spec("mount/redraw", function() { var root2 = $document.createElement("div") var root3 = $document.createElement("div") - m.mount(root1, () => ({view: function() { calls.push("root1") }})) - m.mount(root2, () => ({ - onbeforeupdate: function() { + m.mount(root1, () => { calls.push("root1") }) + m.mount(root2, () => h(Inline, { + onbeforeupdate() { try { m.mount(root2, null) } catch (e) { calls.push([e.constructor, e.message]) } }, - view: function() { calls.push("root2") }, + view() { calls.push("root2") }, })) - m.mount(root3, () => ({view: function() { calls.push("root3") }})) + m.mount(root3, () => { calls.push("root3") }) o(calls).deepEquals([ "root1", "root2", "root3", ]) @@ -338,14 +334,14 @@ o.spec("mount/redraw", function() { var root2 = $document.createElement("div") var root3 = $document.createElement("div") - m.mount(root1, () => ({view: function() { calls.push("root1") }})) - m.mount(root2, () => ({ - onbeforeupdate: function() { + m.mount(root1, () => { calls.push("root1") }) + m.mount(root2, () => h(Inline, { + onbeforeupdate() { try { m.mount(root2, null) } catch (e) { throw [e.constructor, e.message] } }, - view: function() { calls.push("root2") }, + view() { calls.push("root2") }, })) - m.mount(root3, () => ({view: function() { calls.push("root3") }})) + m.mount(root3, () => { calls.push("root3") }) o(calls).deepEquals([ "root1", "root2", "root3", ]) @@ -357,221 +353,185 @@ o.spec("mount/redraw", function() { ]) }) - 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() + o("throws on invalid `root` DOM node", function() { + o(function() { + m.mount(null, () => {}) + }).throws(TypeError) + }) + + o("renders into `root` synchronously", function() { + m.mount(root, () => h("div")) + + o(root.firstChild.nodeName).equals("DIV") + }) + + o("mounting null unmounts", function() { + m.mount(root, () => 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, view) + o(view.callCount).equals(1) + + m.mount(root2, () => {}) + + 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, () => 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, () => h("div", { + oninit: oninit0, + onupdate: onupdate0, + onclick: onclick0, + })) + + o(oninit0.callCount).equals(1) + o(onupdate0.callCount).equals(0) + + m.mount(root2, () => 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, () => 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(root1, createComponent({view: view})) - o(view.callCount).equals(1) - - m.mount(root2, createComponent({view: function() {}})) + m.mount(root, () => h("div", { + oninit: oninit, + onupdate: onupdate + })) + + o(oninit.callCount).equals(1) + o(onupdate.callCount).equals(0) + + m.redraw() + + throttleMock.fire() - 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() - }) + o(onupdate.callCount).equals(1) + }) + + o("emits errors correctly", function() { + errors = ["foo", "bar", "baz"] + var counter = -1 + + m.mount(root, () => { + 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/render/tests/manual/iframe.html b/render/tests/manual/iframe.html index 3e9e7bcdb..dd070cb01 100644 --- a/render/tests/manual/iframe.html +++ b/render/tests/manual/iframe.html @@ -18,7 +18,7 @@ } }) - m.mount(document.getElementById("root"), Button) + m.mount(document.getElementById("root"), () => m(Button)) diff --git a/tests/test-api.js b/tests/test-api.js index 13d2b9abf..3861c2697 100644 --- a/tests/test-api.js +++ b/tests/test-api.js @@ -77,19 +77,21 @@ o.spec("api", function() { o(root.firstChild.nodeName).equals("DIV") }) }) + + o.spec("m.mount", function() { + o("works", function() { + root = window.document.createElement("div") + m.mount(root, () => m("div")) + + o(root.childNodes.length).equals(1) + o(root.firstChild.nodeName).equals("DIV") + }) + }) + components.forEach(function(cmp){ o.spec(cmp.kind, function(){ var createComponent = cmp.create - o.spec("m.mount", function() { - o("works", function() { - root = window.document.createElement("div") - m.mount(root, createComponent({view: function() {return m("div")}})) - - o(root.childNodes.length).equals(1) - o(root.firstChild.nodeName).equals("DIV") - }) - }) o.spec("m.route", function() { o("works", function() { root = window.document.createElement("div") From 6caacf24d7aea2cdc50cb7000e69a8aca7ac7a52 Mon Sep 17 00:00:00 2001 From: Claudia Meadows Date: Wed, 2 Oct 2024 22:00:03 -0700 Subject: [PATCH 17/95] Drop a couple forgotten tests --- tests/test-api.js | 48 ++++++++++++++++++++++------------------------- 1 file changed, 22 insertions(+), 26 deletions(-) diff --git a/tests/test-api.js b/tests/test-api.js index 3861c2697..bb59365a3 100644 --- a/tests/test-api.js +++ b/tests/test-api.js @@ -63,11 +63,6 @@ o.spec("api", function() { o(query).equals("a=1&b=2") }) }) - o.spec("m.request", function() { - o("works", function() { - o(typeof m.request).equals("function") // TODO improve - }) - }) o.spec("m.render", function() { o("works", function() { root = window.document.createElement("div") @@ -88,6 +83,28 @@ o.spec("api", function() { }) }) + o.spec("m.redraw", function() { + o("works", function() { + var count = 0 + root = window.document.createElement("div") + m.mount(root, () => {count++}) + o(count).equals(1) + m.redraw() + o(count).equals(1) + return sleep(FRAME_BUDGET + 10).then(() => { + o(count).equals(2) + }) + }) + o("sync", function() { + root = window.document.createElement("div") + var view = o.spy() + m.mount(root, view) + o(view.callCount).equals(1) + m.redraw.sync() + o(view.callCount).equals(2) + }) + }) + components.forEach(function(cmp){ o.spec(cmp.kind, function(){ var createComponent = cmp.create @@ -139,27 +156,6 @@ o.spec("api", function() { .then(() => { o(m.route.get()).equals("/b") }) }) }) - o.spec("m.redraw", function() { - o("works", function() { - var count = 0 - root = window.document.createElement("div") - m.mount(root, createComponent({view: function() {count++}})) - o(count).equals(1) - m.redraw() - o(count).equals(1) - return sleep(FRAME_BUDGET + 10).then(() => { - o(count).equals(2) - }) - }) - o("sync", function() { - root = window.document.createElement("div") - var view = o.spy() - m.mount(root, createComponent({view: view})) - o(view.callCount).equals(1) - m.redraw.sync() - o(view.callCount).equals(2) - }) - }) }) }) }) From 57edf210b643e0954595f19d059021f7c103f3aa Mon Sep 17 00:00:00 2001 From: Claudia Meadows Date: Wed, 2 Oct 2024 23:29:27 -0700 Subject: [PATCH 18/95] Add lazy loader --- index.js | 1 + util/lazy.js | 35 +++ util/tests/test-lazy.js | 610 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 646 insertions(+) create mode 100644 util/lazy.js create mode 100644 util/tests/test-lazy.js diff --git a/index.js b/index.js index 5ee40f734..c5365a0e1 100644 --- a/index.js +++ b/index.js @@ -19,5 +19,6 @@ m.p = require("./pathname/build") m.withProgress = require("./util/with-progress") m.vnode = require("./render/vnode") m.censor = require("./util/censor") +m.lazy = require("./util/lazy")(mountRedraw.redraw) module.exports = m diff --git a/util/lazy.js b/util/lazy.js new file mode 100644 index 000000000..33c7b3e54 --- /dev/null +++ b/util/lazy.js @@ -0,0 +1,35 @@ +"use strict" + +var m = require("../render/hyperscript") +var censor = require("./censor") + +module.exports = (redraw) => (opts) => { + var fetched = false + var Comp = () => ({view: () => opts.pending && opts.pending()}) + var e = new ReferenceError("Component not found") + var ShowError = () => ({view: () => opts.error && opts.error(e)}) + + return () => { + if (!fetched) { + fetched = true + new Promise((resolve) => resolve(opts.fetch())).then( + (result) => { + Comp = typeof result === "function" + ? result + : result && typeof result.default === "function" + ? result.default + : ShowError + redraw() + }, + (error) => { + Comp = ShowError + e = error + if (!opts.error) console.error(error) + redraw() + } + ) + } + + return {view: ({attrs}) => m(Comp, censor(attrs))} + } +} diff --git a/util/tests/test-lazy.js b/util/tests/test-lazy.js new file mode 100644 index 000000000..359e12541 --- /dev/null +++ b/util/tests/test-lazy.js @@ -0,0 +1,610 @@ +"use strict" + +var o = require("ospec") +var components = require("../../test-utils/components") +var domMock = require("../../test-utils/domMock") +var hyperscript = require("../../render/hyperscript") +var lazy = require("../lazy") +var render = require("../../render/render") + +o.spec("lazy", () => { + var consoleError = console.error + var $window, root + o.beforeEach(() => { + $window = domMock() + root = $window.document.createElement("div") + }) + o.afterEach(() => { + console.error = consoleError + }) + + components.forEach((cmp) => { + o.spec(cmp.kind, () => { + + void [{name: "direct", wrap: (v) => v}, {name: "in module with default", wrap: (v) => ({default:v})}].forEach(({name, wrap}) => { + var createComponent = (methods) => wrap(cmp.create(methods)) + o.spec(name, () => { + o("works with only fetch and success", () => { + var calls = [] + var scheduled = 1 + var component = createComponent({ + view(vnode) { + calls.push(`view ${vnode.attrs.name}`) + return hyperscript("div", {id: "a"}, "b") + } + }) + var send, notifyRedrawn + var fetchRedrawn = new Promise((resolve) => notifyRedrawn = resolve) + var C = lazy(() => { + notifyRedrawn() + calls.push(`scheduled ${scheduled++}`) + })({ + fetch() { + calls.push("fetch") + return new Promise((resolve) => send = resolve) + }, + }) + + o(calls).deepEquals([]) + + render(root, [ + hyperscript(C, {name: "one"}), + hyperscript(C, {name: "two"}), + ]) + + o(calls).deepEquals([ + "fetch", + ]) + + send(component) + + return fetchRedrawn.then(() => { + o(calls).deepEquals([ + "fetch", + "scheduled 1", + ]) + + render(root, [ + hyperscript(C, {name: "one"}), + hyperscript(C, {name: "two"}), + ]) + + o(calls).deepEquals([ + "fetch", + "scheduled 1", + "view one", + "view two", + ]) + + render(root, [ + hyperscript(C, {name: "one"}), + hyperscript(C, {name: "two"}), + ]) + + o(calls).deepEquals([ + "fetch", + "scheduled 1", + "view one", + "view two", + "view one", + "view two", + ]) + }) + }) + + o("works with only fetch and failure", () => { + var error = new Error("test") + var calls = [] + console.error = (e) => { + calls.push("error", e.message) + } + var scheduled = 1 + var send, notifyRedrawn + var fetchRedrawn = new Promise((resolve) => notifyRedrawn = resolve) + var C = lazy(() => { + notifyRedrawn() + calls.push(`scheduled ${scheduled++}`) + })({ + fetch() { + calls.push("fetch") + return new Promise((_, reject) => send = reject) + }, + }) + + o(calls).deepEquals([]) + + render(root, [ + hyperscript(C, {name: "one"}), + hyperscript(C, {name: "two"}), + ]) + + o(calls).deepEquals([ + "fetch", + ]) + + send(error) + + return fetchRedrawn.then(() => { + o(calls).deepEquals([ + "fetch", + "error", "test", + "scheduled 1", + ]) + + render(root, [ + hyperscript(C, {name: "one"}), + hyperscript(C, {name: "two"}), + ]) + + o(calls).deepEquals([ + "fetch", + "error", "test", + "scheduled 1", + ]) + + render(root, [ + hyperscript(C, {name: "one"}), + hyperscript(C, {name: "two"}), + ]) + + o(calls).deepEquals([ + "fetch", + "error", "test", + "scheduled 1", + ]) + }) + }) + + o("works with fetch + pending and success", () => { + var calls = [] + var scheduled = 1 + var component = createComponent({ + view(vnode) { + calls.push(`view ${vnode.attrs.name}`) + return hyperscript("div", {id: "a"}, "b") + } + }) + var send, notifyRedrawn + var fetchRedrawn = new Promise((resolve) => notifyRedrawn = resolve) + var C = lazy(() => { + notifyRedrawn() + calls.push(`scheduled ${scheduled++}`) + })({ + fetch() { + calls.push("fetch") + return new Promise((resolve) => send = resolve) + }, + pending() { + calls.push("pending") + }, + }) + + o(calls).deepEquals([]) + + render(root, [ + hyperscript(C, {name: "one"}), + hyperscript(C, {name: "two"}), + ]) + + o(calls).deepEquals([ + "fetch", + "pending", + "pending", + ]) + + send(component) + + return fetchRedrawn.then(() => { + o(calls).deepEquals([ + "fetch", + "pending", + "pending", + "scheduled 1", + ]) + + render(root, [ + hyperscript(C, {name: "one"}), + hyperscript(C, {name: "two"}), + ]) + + o(calls).deepEquals([ + "fetch", + "pending", + "pending", + "scheduled 1", + "view one", + "view two", + ]) + + render(root, [ + hyperscript(C, {name: "one"}), + hyperscript(C, {name: "two"}), + ]) + + o(calls).deepEquals([ + "fetch", + "pending", + "pending", + "scheduled 1", + "view one", + "view two", + "view one", + "view two", + ]) + }) + }) + + o("works with fetch + pending and failure", () => { + var error = new Error("test") + var calls = [] + console.error = (e) => { + calls.push("error", e.message) + } + var scheduled = 1 + var send, notifyRedrawn + var fetchRedrawn = new Promise((resolve) => notifyRedrawn = resolve) + var C = lazy(() => { + notifyRedrawn() + calls.push(`scheduled ${scheduled++}`) + })({ + fetch() { + calls.push("fetch") + return new Promise((_, reject) => send = reject) + }, + pending() { + calls.push("pending") + }, + }) + + o(calls).deepEquals([]) + + render(root, [ + hyperscript(C, {name: "one"}), + hyperscript(C, {name: "two"}), + ]) + + o(calls).deepEquals([ + "fetch", + "pending", + "pending", + ]) + + send(error) + + return fetchRedrawn.then(() => { + o(calls).deepEquals([ + "fetch", + "pending", + "pending", + "error", "test", + "scheduled 1", + ]) + + render(root, [ + hyperscript(C, {name: "one"}), + hyperscript(C, {name: "two"}), + ]) + + o(calls).deepEquals([ + "fetch", + "pending", + "pending", + "error", "test", + "scheduled 1", + ]) + + render(root, [ + hyperscript(C, {name: "one"}), + hyperscript(C, {name: "two"}), + ]) + + o(calls).deepEquals([ + "fetch", + "pending", + "pending", + "error", "test", + "scheduled 1", + ]) + }) + }) + + o("works with fetch + error and success", () => { + var calls = [] + var scheduled = 1 + var component = createComponent({ + view(vnode) { + calls.push(`view ${vnode.attrs.name}`) + return hyperscript("div", {id: "a"}, "b") + } + }) + var send, notifyRedrawn + var fetchRedrawn = new Promise((resolve) => notifyRedrawn = resolve) + var C = lazy(() => { + notifyRedrawn() + calls.push(`scheduled ${scheduled++}`) + })({ + fetch() { + calls.push("fetch") + return new Promise((resolve) => send = resolve) + }, + error() { + calls.push("error") + }, + }) + + o(calls).deepEquals([]) + + render(root, [ + hyperscript(C, {name: "one"}), + hyperscript(C, {name: "two"}), + ]) + + o(calls).deepEquals([ + "fetch", + ]) + + send(component) + + return fetchRedrawn.then(() => { + o(calls).deepEquals([ + "fetch", + "scheduled 1", + ]) + + render(root, [ + hyperscript(C, {name: "one"}), + hyperscript(C, {name: "two"}), + ]) + + o(calls).deepEquals([ + "fetch", + "scheduled 1", + "view one", + "view two", + ]) + + render(root, [ + hyperscript(C, {name: "one"}), + hyperscript(C, {name: "two"}), + ]) + + o(calls).deepEquals([ + "fetch", + "scheduled 1", + "view one", + "view two", + "view one", + "view two", + ]) + }) + }) + + o("works with fetch + error and failure", () => { + var error = new Error("test") + var calls = [] + var scheduled = 1 + var send, notifyRedrawn + var fetchRedrawn = new Promise((resolve) => notifyRedrawn = resolve) + var C = lazy(() => { + notifyRedrawn() + calls.push(`scheduled ${scheduled++}`) + })({ + fetch() { + calls.push("fetch") + return new Promise((_, reject) => send = reject) + }, + error(e) { + calls.push("error", e.message) + }, + }) + + o(calls).deepEquals([]) + + render(root, [ + hyperscript(C, {name: "one"}), + hyperscript(C, {name: "two"}), + ]) + + o(calls).deepEquals([ + "fetch", + ]) + + send(error) + + return fetchRedrawn.then(() => { + o(calls).deepEquals([ + "fetch", + "scheduled 1", + ]) + + render(root, [ + hyperscript(C, {name: "one"}), + hyperscript(C, {name: "two"}), + ]) + + o(calls).deepEquals([ + "fetch", + "scheduled 1", + "error", "test", + "error", "test", + ]) + + render(root, [ + hyperscript(C, {name: "one"}), + hyperscript(C, {name: "two"}), + ]) + + o(calls).deepEquals([ + "fetch", + "scheduled 1", + "error", "test", + "error", "test", + "error", "test", + "error", "test", + ]) + }) + }) + + o("works with all hooks and success", () => { + var calls = [] + var scheduled = 1 + var component = createComponent({ + view(vnode) { + calls.push(`view ${vnode.attrs.name}`) + return hyperscript("div", {id: "a"}, "b") + } + }) + var send, notifyRedrawn + var fetchRedrawn = new Promise((resolve) => notifyRedrawn = resolve) + var C = lazy(() => { + notifyRedrawn() + calls.push(`scheduled ${scheduled++}`) + })({ + fetch() { + calls.push("fetch") + return new Promise((resolve) => send = resolve) + }, + pending() { + calls.push("pending") + }, + error() { + calls.push("error") + }, + }) + + o(calls).deepEquals([]) + + render(root, [ + hyperscript(C, {name: "one"}), + hyperscript(C, {name: "two"}), + ]) + + o(calls).deepEquals([ + "fetch", + "pending", + "pending", + ]) + + send(component) + + return fetchRedrawn.then(() => { + o(calls).deepEquals([ + "fetch", + "pending", + "pending", + "scheduled 1", + ]) + + render(root, [ + hyperscript(C, {name: "one"}), + hyperscript(C, {name: "two"}), + ]) + + o(calls).deepEquals([ + "fetch", + "pending", + "pending", + "scheduled 1", + "view one", + "view two", + ]) + + render(root, [ + hyperscript(C, {name: "one"}), + hyperscript(C, {name: "two"}), + ]) + + o(calls).deepEquals([ + "fetch", + "pending", + "pending", + "scheduled 1", + "view one", + "view two", + "view one", + "view two", + ]) + }) + }) + + o("works with all hooks and failure", () => { + var error = new Error("test") + var calls = [] + var scheduled = 1 + var send, notifyRedrawn + var fetchRedrawn = new Promise((resolve) => notifyRedrawn = resolve) + var C = lazy(() => { + notifyRedrawn() + calls.push(`scheduled ${scheduled++}`) + })({ + fetch() { + calls.push("fetch") + return new Promise((_, reject) => send = reject) + }, + pending() { + calls.push("pending") + }, + error(e) { + calls.push("error", e.message) + }, + }) + + o(calls).deepEquals([]) + + render(root, [ + hyperscript(C, {name: "one"}), + hyperscript(C, {name: "two"}), + ]) + + o(calls).deepEquals([ + "fetch", + "pending", + "pending", + ]) + + send(error) + + return fetchRedrawn.then(() => { + o(calls).deepEquals([ + "fetch", + "pending", + "pending", + "scheduled 1", + ]) + + render(root, [ + hyperscript(C, {name: "one"}), + hyperscript(C, {name: "two"}), + ]) + + o(calls).deepEquals([ + "fetch", + "pending", + "pending", + "scheduled 1", + "error", "test", + "error", "test", + ]) + + render(root, [ + hyperscript(C, {name: "one"}), + hyperscript(C, {name: "two"}), + ]) + + o(calls).deepEquals([ + "fetch", + "pending", + "pending", + "scheduled 1", + "error", "test", + "error", "test", + "error", "test", + "error", "test", + ]) + }) + }) + }) + }) + }) + }) +}) From 6b3c7b38bcb51a8ff5a2009b0ef91ca63fb098a7 Mon Sep 17 00:00:00 2001 From: Claudia Meadows Date: Thu, 3 Oct 2024 02:23:41 -0700 Subject: [PATCH 19/95] Toss the "route" part of the router People can just use equality and regexps as needed. --- api/router.js | 339 +---- api/tests/test-router.js | 1935 ++++-------------------- api/tests/test-routerGetSet.js | 274 ---- index.js | 1 - pathname/compileTemplate.js | 43 - pathname/parse.js | 23 - pathname/tests/test-compileTemplate.js | 221 --- pathname/tests/test-parsePathname.js | 126 -- route.js | 2 +- tests/test-api.js | 73 +- 10 files changed, 395 insertions(+), 2642 deletions(-) delete mode 100644 api/tests/test-routerGetSet.js delete mode 100644 pathname/compileTemplate.js delete mode 100644 pathname/parse.js delete mode 100644 pathname/tests/test-compileTemplate.js delete mode 100644 pathname/tests/test-parsePathname.js diff --git a/api/router.js b/api/router.js index 2197d11a0..52236563a 100644 --- a/api/router.js +++ b/api/router.js @@ -1,273 +1,96 @@ "use strict" -var m = require("../render/hyperscript") - -var parsePathname = require("../pathname/parse") -var compileTemplate = require("../pathname/compileTemplate") -var censor = require("../util/censor") - -var sentinel = {} - -function decodeURIComponentSave(component) { - try { - return decodeURIComponent(component) - } catch(e) { - return component +module.exports = function($window, redraw) { + var mustReplace = false + var routePrefix, currentUrl, currentPath, currentHref + + function updateRoute() { + var href = $window.location.href + + if (currentHref === href) return + currentHref = href + if (currentUrl) redraw() + + var url = new URL(href) + var urlPath = url.pathname + url.search + url.hash + var index = urlPath.indexOf(routePrefix) + var prefix = routePrefix + if (index < 0) index = urlPath.indexOf(prefix = encodeURI(prefix)) + if (index >= 0) urlPath = urlPath.slice(index + prefix.length) + if (urlPath[0] !== "/") urlPath = `/${urlPath}` + + currentUrl = new URL(urlPath, href) + currentPath = decodeURI(currentUrl.pathname) + mustReplace = false } -} - -module.exports = function($window, mountRedraw) { - var callAsync = $window == null - // In case Mithril.js' loaded globally without the DOM, let's not break - ? null - : typeof $window.setImmediate === "function" ? $window.setImmediate : $window.setTimeout - var p = Promise.resolve() - - var scheduled = false - - // state === 0: init - // state === 1: scheduled - // state === 2: done - var ready = false - var state = 0 - - var compiled, fallbackRoute - - var currentResolver = sentinel, component, attrs, currentPath, lastUpdate - - var RouterRoot = () => ({ - onbeforeupdate: function() { - state = state ? 2 : 1 - return !(!state || sentinel === currentResolver) - }, - onremove: function() { - $window.removeEventListener("popstate", fireAsync, false) - $window.removeEventListener("hashchange", resolveRoute, false) - }, - view: function() { - if (!state || sentinel === currentResolver) return - // Wrap in a fragment to preserve existing key semantics - var vnode = [m(component, attrs)] - if (currentResolver) vnode = currentResolver.render(vnode[0]) - return vnode - }, - }) - var SKIP = route.SKIP = {} - - function resolveRoute() { - scheduled = false - // Consider the pathname holistically. The prefix might even be invalid, - // but that's not our problem. - var prefix = $window.location.hash - if (route.prefix[0] !== "#") { - prefix = $window.location.search + prefix - if (route.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. - var path = prefix.concat() - .replace(/(?:%[a-f89][a-f0-9])+/gim, decodeURIComponentSave) - .slice(route.prefix.length) - var data = parsePathname(path) - - Object.assign(data.params, $window.history.state) - - function reject(e) { - console.error(e) - setPath(fallbackRoute, {replace: true}) - } - - loop(0) - function loop(i) { - // state === 0: init - // state === 1: scheduled - // state === 2: done - for (; i < compiled.length; i++) { - if (compiled[i].check(data)) { - var payload = compiled[i].component - var matchedRoute = compiled[i].route - var localComp = payload - var update = lastUpdate = function(comp) { - if (update !== lastUpdate) return - if (comp === SKIP) return loop(i + 1) - component = typeof comp === "function" ? comp : "div" - attrs = data.params, currentPath = path, lastUpdate = null - currentResolver = payload.render ? payload : null - if (state === 2) mountRedraw.redraw() - else { - state = 2 - mountRedraw.redraw.sync() - } - } - // There's no understating how much I *wish* I could - // use `async`/`await` here... - if (typeof payload === "function") { - payload = {} - update(localComp) - } - else if (payload.onmatch) { - p.then(function () { - return payload.onmatch(data.params, path, matchedRoute) - }).then(update, path === fallbackRoute ? null : reject) - } - else update("div") - return - } - } - - if (path === fallbackRoute) { - throw new Error("Could not resolve default route " + fallbackRoute + ".") - } - setPath(fallbackRoute, {replace: true}) + function set(path, {replace, state} = {}) { + if (!currentUrl) { + throw new ReferenceError("Route state must be fully initialized first") } + if (mustReplace) replace = true + mustReplace = true + queueMicrotask(updateRoute) + redraw() + $window.history[replace ? "replaceState" : "pushState"](state, "", routePrefix + path) } - // Set it unconditionally so `m.route.set` and `m.route.Link` both work, - // even if neither `pushState` nor `hashchange` are supported. It's - // cleared if `hashchange` is used, since that makes it automatically - // async. - function fireAsync() { - if (!scheduled) { - scheduled = true - // TODO: just do `mountRedraw.redraw()` here and elide the timer - // dependency. Note that this will muck with tests a *lot*, so it's - // not as easy of a change as it sounds. - callAsync(resolveRoute) - } - } - - function setPath(path, options) { - if (ready) { - fireAsync() - var state = options ? options.state : null - var title = options ? options.title : null - if (options && options.replace) $window.history.replaceState(state, title, route.prefix + path) - else $window.history.pushState(state, title, route.prefix + path) - } - else { - $window.location.href = route.prefix + path - } - } - - function route(root, defaultRoute, routes) { - if (!root) throw new TypeError("DOM element being rendered to does not exist.") - - 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), - } - }) - fallbackRoute = defaultRoute - 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.") + return { + init(prefix = "#!") { + routePrefix = prefix + if ($window) { + $window.addEventListener("popstate", updateRoute, false) + $window.addEventListener("hashchange", updateRoute, false) + updateRoute() } - } - - if (typeof $window.history.pushState === "function") { - $window.addEventListener("popstate", fireAsync, false) - } else if (route.prefix[0] === "#") { - $window.addEventListener("hashchange", resolveRoute, false) - } - - ready = true - mountRedraw.mount(root, () => m(RouterRoot)) - resolveRoute() - } - route.set = function(path, options) { - if (lastUpdate != null) { - options = options || {} - options.replace = true - } - lastUpdate = null - setPath(path, options) - } - route.get = function() {return currentPath} - route.prefix = "#!" - route.Link = () => ({ - view: function(vnode) { - // Omit the used parameters from the rendered element - they are - // internal. Also, censor the various lifecycle methods. - // - // We don't strip the other parameters because for convenience we - // let them be specified in the selector as well. - var child = m( - vnode.attrs.selector || "a", - censor(vnode.attrs, ["options", "selector", "onclick"]), - vnode.children - ) - var options, onclick, href - - // Let's provide a *right* way to disable a route link, rather than - // letting people screw up accessibility on accident. - // - // The attribute is coerced so users don't get surprised over - // `disabled: 0` resulting in a button that's somehow routable - // despite being visibly disabled. - if (child.attrs.disabled = Boolean(child.attrs.disabled)) { - child.attrs.href = null - child.attrs["aria-disabled"] = "true" + }, + set, + get: () => currentPath + currentUrl.search + currentUrl.hash, + get path() { return currentPath }, + get params() { return currentUrl.searchParams }, + // Let's provide a *right* way to manage a route link, rather than letting people screw up + // accessibility on accident. + link: (opts) => ( + opts.disabled // If you *really* do want add `onclick` on a disabled link, use // an `oncreate` hook to add it. - } else { - options = vnode.attrs.options - onclick = vnode.attrs.onclick - href = child.attrs.href - child.attrs.href = route.prefix + href - child.attrs.onclick = function(e) { - var result - if (typeof onclick === "function") { - result = onclick.call(e.currentTarget, e) - } else if (onclick == null || typeof onclick !== "object") { + ? {disabled: true, "aria-disabled": "true"} + : { + href: routePrefix + opts.href, + onclick(e) { + var result + if (typeof opts.onclick === "function") { + result = opts.onclick.call(e.currentTarget, e) + } else if (opts.onclick == null || typeof opts.onclick !== "object") { // do nothing - } else if (typeof onclick.handleEvent === "function") { - onclick.handleEvent(e) - } + } else if (typeof opts.onclick.handleEvent === "function") { + opts.onclick.handleEvent(e) + } - // Adapted from React Router's implementation: - // https://github.com/ReactTraining/react-router/blob/520a0acd48ae1b066eb0b07d6d4d1790a1d02482/packages/react-router-dom/modules/Link.js - // - // Try to be flexible and intuitive in how we handle links. - // Fun fact: links aren't as obvious to get right as you - // would expect. There's a lot more valid ways to click a - // link than this, and one might want to not simply click a - // link, but right click or command-click it to copy the - // link target, etc. Nope, this isn't just for blind people. - if ( + // Adapted from React Router's implementation: + // https://github.com/ReactTraining/react-router/blob/520a0acd48ae1b066eb0b07d6d4d1790a1d02482/packages/react-router-dom/modules/Link.js + // + // Try to be flexible and intuitive in how we handle links. + // Fun fact: links aren't as obvious to get right as you + // would expect. There's a lot more valid ways to click a + // link than this, and one might want to not simply click a + // link, but right click or command-click it to copy the + // link target, etc. Nope, this isn't just for blind people. + if ( // Skip if `onclick` prevented default - result !== false && !e.defaultPrevented && - // Ignore everything but left clicks - (e.button === 0 || e.which === 0 || e.which === 1) && - // Let the browser handle `target=_blank`, etc. - (!e.currentTarget.target || e.currentTarget.target === "_self") && - // No modifier keys - !e.ctrlKey && !e.metaKey && !e.shiftKey && !e.altKey - ) { - e.preventDefault() - e.redraw = false - route.set(href, options) - } - } - } - return child - }, - }) - route.param = function(key) { - return attrs && key != null ? attrs[key] : attrs + result !== false && !e.defaultPrevented && + // Ignore everything but left clicks + (e.button === 0 || e.which === 0 || e.which === 1) && + // Let the browser handle `target=_blank`, etc. + (!e.currentTarget.target || e.currentTarget.target === "_self") && + // No modifier keys + !e.ctrlKey && !e.metaKey && !e.shiftKey && !e.altKey + ) { + e.preventDefault() + e.redraw = false + set(opts.href, opts) + } + }, + }), } - - return route } diff --git a/api/tests/test-router.js b/api/tests/test-router.js index 199d2a4a6..e3a58da2a 100644 --- a/api/tests/test-router.js +++ b/api/tests/test-router.js @@ -6,11 +6,10 @@ var browserMock = require("../../test-utils/browserMock") var throttleMocker = require("../../test-utils/throttleMock") var m = require("../../render/hyperscript") -var render = require("../../render/render") var apiMountRedraw = require("../../api/mount-redraw") var apiRouter = require("../../api/router") -o.spec("route", function() { +o.spec("route", () => { // Note: the `n` parameter used in calls to this are generally found by // either trial-and-error or by studying the source. If tests are failing, // find the failing assertions, set `n` to about 10 on the preceding call to @@ -36,42 +35,18 @@ o.spec("route", function() { }) } - void [{protocol: "http:", hostname: "localhost"}, {protocol: "file:", hostname: "/"}, {protocol: "http:", hostname: "ööö"}].forEach(function(env) { - void ["#", "?", "", "#!", "?!", "/foo", "/föö"].forEach(function(prefix) { - o.spec("using prefix `" + prefix + "` starting on " + env.protocol + "//" + env.hostname, function() { + void [{protocol: "http:", hostname: "localhost"}, {protocol: "file:", hostname: "/"}, {protocol: "http:", hostname: "ööö"}].forEach((env) => { + void ["#", "?", "", "#!", "?!", "/foo", "/föö"].forEach((prefix) => { + o.spec(`using prefix \`${prefix}\` starting on ${env.protocol}//${env.hostname}`, () => { + var fullHost = `${env.protocol}//${env.hostname === "/" ? "" : env.hostname}` + var fullPrefix = `${fullHost}${prefix[0] === "/" ? "" : "/"}${prefix ? `${prefix}/` : ""}` + var $window, root, mountRedraw, route, throttleMock - var nextID = 0 - var currentTest = 0 - - // Once done, a root should no longer be alive. This verifies - // that, and it's a *very* subtle test bug that can lead to - // some rather unusual consequences. If this fails, use - // `waitCycles(n)` to avoid this. - function lock(func) { - var id = currentTest - var start = Date.now() - try { - throw new Error() - } catch (trace) { - return function() { - // This *will* cause a test failure. - if (id != null && id !== currentTest) { - id = undefined - trace.message = "called " + - (Date.now() - start) + "ms after test end" - console.error(trace.stack) - o("in test").equals("not in test") - } - return func.apply(this, arguments) - } - } - } // In case it doesn't get reset var realError = console.error - o.beforeEach(function() { - currentTest = nextID++ + o.beforeEach(() => { $window = browserMock(env) $window.setTimeout = setTimeout // $window.setImmediate = setImmediate @@ -80,1760 +55,453 @@ o.spec("route", function() { root = $window.document.body mountRedraw = apiMountRedraw(throttleMock.schedule, console) - route = apiRouter($window, mountRedraw) - route.prefix = prefix - console.error = function() { + route = apiRouter($window, mountRedraw.redraw) + console.error = function () { realError.call(this, new Error("Unexpected `console.error` call")) realError.apply(this, arguments) } }) - o.afterEach(function() { - o(throttleMock.queueLength()).equals(0) - currentTest = -1 // doesn't match any test + o.afterEach(() => { console.error = realError }) - o("throws on invalid `root` DOM node", function() { - var threw = false - try { - route(null, "/", {"/": () => ({view: lock(function() {})})}) - } catch (e) { - threw = true - } - o(threw).equals(true) - }) - - o("renders into `root`", function() { - $window.location.href = prefix + "/" - route(root, "/", { - "/": () => ({ - view: lock(function() { - return m("div") - }) - }) - }) - - o(root.firstChild.nodeName).equals("DIV") - }) - - o("resolves to route with escaped unicode", function() { - $window.location.href = prefix + "/%C3%B6?%C3%B6=%C3%B6" - route(root, "/ö", { - "/ö": () => ({ - view: lock(function() { - return m("div") - }) - }) - }) - - o(root.firstChild.nodeName).equals("DIV") - }) - - o("resolves to route with unicode", function() { - $window.location.href = prefix + "/ö?ö=ö" - route(root, "/ö", { - "/ö": () => ({ - view: lock(function() { - return JSON.stringify(route.param()) + " " + - route.get() - }) - }) - }) - - o(root.firstChild.nodeValue).equals('{"ö":"ö"} /ö?ö=ö') - }) - - o("resolves to route with matching invalid escape", function() { - $window.location.href = prefix + "/%C3%B6abc%def" - route(root, "/öabc%def", { - "/öabc%def": () => ({ - view: lock(function() { - return route.get() - }) - }) - }) - - o(root.firstChild.nodeValue).equals("/öabc%def") - }) - - o("handles parameterized route", function() { - $window.location.href = prefix + "/test/x" - route(root, "/test/:a", { - "/test/:a": () => ({ - view: lock(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: lock(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: lock(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("keeps trailing / in rest parameterized route", function() { - $window.location.href = prefix + "/test/d/" - route(root, "/test/:a...", { - "/test/:a...": () => ({ - view: lock(function(vnode) { - return JSON.stringify(route.param()) + " " + - JSON.stringify(vnode.attrs) + " " + - route.get() - }) - }) - }) + o("returns the right route on init", () => { + $window.location.href = `${prefix}/` - o(root.firstChild.nodeValue).equals( - '{"a":"d/"} {"a":"d/"} /test/d/' - ) + route.init(prefix) + o(route.path).equals("/") + o([...route.params]).deepEquals([]) + o(throttleMock.queueLength()).equals(0) }) - o("handles route with search", function() { - $window.location.href = prefix + "/test?a=b&c=d" - route(root, "/test", { - "/test": () => ({ - view: lock(function(vnode) { - return JSON.stringify(route.param()) + " " + - JSON.stringify(vnode.attrs) + " " + - route.get() - }) - }) - }) + o("returns alternate right route on init", () => { + $window.location.href = `${prefix}/test` - o(root.firstChild.nodeValue).equals( - '{"a":"b","c":"d"} {"a":"b","c":"d"} /test?a=b&c=d' - ) + route.init(prefix) + o(route.path).equals("/test") + o([...route.params]).deepEquals([]) + o(throttleMock.queueLength()).equals(0) }) - o("redirects to default route if no match", function() { - $window.location.href = prefix + "/test" - route(root, "/other", { - "/other": () => ({ - view: lock(function(vnode) { - return JSON.stringify(route.param()) + " " + - JSON.stringify(vnode.attrs) + " " + - route.get() - }) - }) - }) + o("returns right route on init with escaped unicode", () => { + $window.location.href = `${prefix}/%C3%B6?%C3%B6=%C3%B6` - return waitCycles(1).then(function() { - o(root.firstChild.nodeValue).equals("{} {} /other") - }) + route.init(prefix) + o(route.path).equals("/ö") + o([...route.params]).deepEquals([["ö", "ö"]]) + o(throttleMock.queueLength()).equals(0) }) - o("handles out of order routes", function() { - $window.location.href = prefix + "/z/y/x" - - route(root, "/z/y/x", { - "/z/y/x": () => ({ - view: lock(function() { return "1" }), - }), - "/:a...": () => ({ - view: lock(function() { return "2" }), - }), - }) + o("returns right route on init with unescaped unicode", () => { + $window.location.href = `${prefix}/ö?ö=ö` - o(root.firstChild.nodeValue).equals("1") + route.init(prefix) + o(route.path).equals("/ö") + o([...route.params]).deepEquals([["ö", "ö"]]) + o(throttleMock.queueLength()).equals(0) }) - o("handles reverse out of order routes", function() { - $window.location.href = prefix + "/z/y/x" + o("sets path asynchronously", () => { + $window.location.href = `${prefix}/a` + var spy1 = o.spy() + var spy2 = o.spy() - route(root, "/z/y/x", { - "/:a...": () => ({ - view: lock(function() { return "2" }), - }), - "/z/y/x": () => ({ - view: lock(function() { return "1" }), - }), + route.init(prefix) + mountRedraw.mount(root, () => { + if (route.path === "/a") { + spy1() + } else if (route.path === "/b") { + spy2() + } else { + throw new Error(`Unknown path ${route.path}`) + } }) - o(root.firstChild.nodeValue).equals("2") - }) - - o("resolves to route on fallback mode", function() { - $window.location.href = "file://" + prefix + "/test" + o(spy1.callCount).equals(1) + o(spy2.callCount).equals(0) + route.set("/b") + o(spy1.callCount).equals(1) + o(spy2.callCount).equals(0) + return waitCycles(1).then(() => { + throttleMock.fire() - route(root, "/test", { - "/test": () => ({ - view: lock(function(vnode) { - return JSON.stringify(route.param()) + " " + - JSON.stringify(vnode.attrs) + " " + - route.get() - }) - }) + o(spy1.callCount).equals(1) + o(spy2.callCount).equals(1) + o(throttleMock.queueLength()).equals(0) }) - - o(root.firstChild.nodeValue).equals("{} {} /test") }) - o("routed mount points only redraw asynchronously (POJO component)", function() { - var view = o.spy() + o("sets route via pushState/onpopstate", () => { + $window.location.href = `${prefix}/test` + route.init(prefix) - $window.location.href = prefix + "/" - route(root, "/", {"/": () => ({view})}) - - o(view.callCount).equals(1) - - mountRedraw.redraw() - - o(view.callCount).equals(1) - - throttleMock.fire() - - o(view.callCount).equals(2) - }) - - o("routed mount points only redraw asynchronously (constructible component)", function() { - var view = o.spy() - - var Cmp = lock(function(){}) - Cmp.prototype.view = lock(view) - - $window.location.href = prefix + "/" - route(root, "/", {"/":Cmp}) - - o(view.callCount).equals(1) - - mountRedraw.redraw() - - o(view.callCount).equals(1) - - throttleMock.fire() - - o(view.callCount).equals(2) - }) - - o("routed mount points only redraw asynchronously (closure component)", function() { - var view = o.spy() - - function Cmp() {return {view: lock(view)}} - - $window.location.href = prefix + "/" - route(root, "/", {"/":lock(Cmp)}) - - o(view.callCount).equals(1) - - mountRedraw.redraw() - - o(view.callCount).equals(1) - - throttleMock.fire() - - o(view.callCount).equals(2) - }) - - o("subscribes correctly and removes when unmounted", function() { - $window.location.href = prefix + "/" - - route(root, "/", { - "/": () => ({ - view: lock(function() { - return m("div") - }) + return waitCycles(1) + .then(() => { + $window.history.pushState(null, null, `${prefix}/other/x/y/z?c=d#e=f`) + $window.onpopstate() + }) + .then(() => waitCycles(1)) + .then(() => { + // Yep, before even the throttle mechanism takes hold. + o(route.get()).equals("/other/x/y/z?c=d#e=f") + throttleMock.fire() + o(throttleMock.queueLength()).equals(0) }) - }) - - o(root.firstChild.nodeName).equals("DIV") - - mountRedraw.mount(root) - - o(root.childNodes.length).equals(0) }) - o("default route doesn't break back button", function() { - $window.location.href = "http://old.com" - $window.location.href = "http://new.com" - - route(root, "/a", { - "/a": () => ({ - view: lock(function() { - return m("div") - }) - }) - }) + o("`replace: true` works", () => { + $window.location.href = `${prefix}/test` + route.init(prefix) - return waitCycles(1).then(function() { - o(root.firstChild.nodeName).equals("DIV") - - o(route.get()).equals("/a") + route.set("/other", {replace: true}) + return waitCycles(1).then(() => { + throttleMock.fire() $window.history.back() - - o($window.location.pathname).equals("/") - o($window.location.hostname).equals("old.com") - }) - }) - - o("default route does not inherit params", function() { - $window.location.href = "/invalid?foo=bar" - route(root, "/a", { - "/a": () => ({ - oninit: lock(function(vnode) { - o(vnode.attrs.foo).equals(undefined) - }), - view: lock(function() { - return m("div") - }) - }) + o($window.location.href).equals(`${fullHost}/`) + throttleMock.fire() + o($window.location.href).equals(`${fullHost}/`) + o(throttleMock.queueLength()).equals(0) }) - - return waitCycles(1) }) - o("redraws when render function is executed", function() { - var onupdate = o.spy() - var oninit = o.spy() - - $window.location.href = prefix + "/" - route(root, "/", { - "/": () => ({ - view: lock(function() { - return m("div", { - oninit: oninit, - onupdate: onupdate - }) - }) - }) - }) - - o(oninit.callCount).equals(1) - - mountRedraw.redraw() - throttleMock.fire() - - o(onupdate.callCount).equals(1) - }) + o("`replace: true` works in links", () => { + $window.location.href = `${prefix}/test` + route.init(prefix) - 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) + e.button = 0 - $window.location.href = prefix + "/" - route(root, "/", { - "/": () => ({ - view: lock(function() { - return m("div", { - oninit: oninit, - onupdate: onupdate, - onclick: onclick, - }) - }) - }) + mountRedraw.mount(root, () => { + if (route.path === "/test") { + return m("a", route.link({href: "/other", replace: true})) + } else if (route.path === "/other") { + return m("div") + } else if (route.path === "/") { + return m("span") + } else { + throw new Error(`Unknown route: ${route.path}`) + } }) 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) - - - throttleMock.fire() - o(onupdate.callCount).equals(1) - }) - - o("event handlers can skip redraw", function() { - var onupdate = o.spy() - var oninit = o.spy() - var e = $window.document.createEvent("MouseEvents") - - e.initEvent("click", true, true) - - $window.location.href = prefix + "/" - route(root, "/", { - "/": () => ({ - view: lock(function() { - return m("div", { - oninit: oninit, - onupdate: onupdate, - onclick: lock(function(e) { - e.redraw = false - }), - }) - }) - }) + return waitCycles(1).then(() => { + throttleMock.fire() + $window.history.back() + o($window.location.href).equals(`${fullHost}/`) + throttleMock.fire() + o($window.location.href).equals(`${fullHost}/`) + o(throttleMock.queueLength()).equals(0) }) + }) - o(oninit.callCount).equals(1) + o("`replace: false` works", () => { + $window.location.href = `${prefix}/test` + route.init(prefix) - root.firstChild.dispatchEvent(e) - throttleMock.fire() + route.set("/other", {replace: false}) - // Wrapped to ensure no redraw fired - return waitCycles(1).then(function() { - o(onupdate.callCount).equals(0) + return waitCycles(1).then(() => { + throttleMock.fire() + $window.history.back() + o($window.location.href).equals(`${fullPrefix}test`) + throttleMock.fire() + o($window.location.href).equals(`${fullPrefix}test`) + o(throttleMock.queueLength()).equals(0) }) }) - o("changes location on route.Link", function() { + o("`replace: false` works in links", () => { + $window.location.href = `${prefix}/test` + route.init(prefix) + var e = $window.document.createEvent("MouseEvents") e.initEvent("click", true, true) e.button = 0 - $window.location.href = prefix + "/" - route(root, "/", { - "/": () => ({ - view: lock(function() { - return m(route.Link, {href: "/test"}) - }) - }), - "/test": () => ({ - view : lock(function() { - return m("div") - }) - }) + mountRedraw.mount(root, () => { + if (route.path === "/test") { + return m("a", route.link({href: "/other", replace: false})) + } else if (route.path === "/other") { + return m("div") + } else { + throw new Error(`Unknown route: ${route.path}`) + } }) - var slash = prefix[0] === "/" ? "" : "/" - - o($window.location.href).equals(env.protocol + "//" + (env.hostname === "/" ? "" : env.hostname) + slash + (prefix ? prefix + "/" : "")) - root.firstChild.dispatchEvent(e) - throttleMock.fire() - o($window.location.href).equals(env.protocol + "//" + (env.hostname === "/" ? "" : env.hostname) + slash + (prefix ? prefix + "/" : "") + "test") - }) - - o("passes options on route.Link", function() { - var opts = {} - var e = $window.document.createEvent("MouseEvents") - e.initEvent("click", true, true) - e.button = 0 - $window.location.href = prefix + "/" - - route(root, "/", { - "/": () => ({ - view: lock(function() { - return m(route.Link, { - href: "/test", - options: opts, - }) - }) - }), - "/test": () => ({ - view : lock(function() { - return m("div") - }) - }) + return waitCycles(1).then(() => { + throttleMock.fire() + $window.history.back() + o($window.location.href).equals(`${fullPrefix}test`) + throttleMock.fire() + o($window.location.href).equals(`${fullPrefix}test`) + o(throttleMock.queueLength()).equals(0) }) - route.set = o.spy(route.set) - - root.firstChild.dispatchEvent(e) - - o(route.set.callCount).equals(1) - o(route.set.args[1]).equals(opts) }) - o("route.Link can render without routes or dom access", function() { - $window = browserMock(env) - route = apiRouter(null, null) - route.prefix = prefix - root = $window.document.body + o("state works", () => { + $window.location.href = `${prefix}/test` + route.init(prefix) - render(root, m(route.Link, {href: "/test", foo: "bar"}, "text")) - - o(root.childNodes.length).equals(1) - o(root.firstChild.nodeName).equals("A") - o(root.firstChild.href).equals(prefix + "/test") - o(root.firstChild.hasAttribute("aria-disabled")).equals(false) - o(root.firstChild.hasAttribute("disabled")).equals(false) - o(root.firstChild.attributes["foo"].value).equals("bar") - o(root.firstChild.childNodes.length).equals(1) - o(root.firstChild.firstChild.nodeName).equals("#text") - o(root.firstChild.firstChild.nodeValue).equals("text") - }) - - o("route.Link keeps magic attributes from being double-called", function() { - $window = browserMock(env) - route = apiRouter(null, null) - route.prefix = prefix - root = $window.document.body - - var oninit = o.spy() - var oncreate = o.spy() - var onbeforeupdate = o.spy() - var onupdate = o.spy() - var onbeforeremove = o.spy() - var onremove = o.spy() - - render(root, m(route.Link, { - href: "/test", - oninit: oninit, - oncreate: oncreate, - onbeforeupdate: onbeforeupdate, - onupdate: onupdate, - onbeforeremove: onbeforeremove, - onremove: onremove, - }, "text")) - - o(oninit.callCount).equals(1) - o(oncreate.callCount).equals(1) - o(onbeforeupdate.callCount).equals(0) - o(onupdate.callCount).equals(0) - o(onbeforeremove.callCount).equals(0) - o(onremove.callCount).equals(0) - - render(root, m(route.Link, { - href: "/test", - oninit: oninit, - oncreate: oncreate, - onbeforeupdate: onbeforeupdate, - onupdate: onupdate, - onbeforeremove: onbeforeremove, - onremove: onremove, - }, "text")) - - o(oninit.callCount).equals(1) - o(oncreate.callCount).equals(1) - o(onbeforeupdate.callCount).equals(1) - o(onupdate.callCount).equals(1) - o(onbeforeremove.callCount).equals(0) - o(onremove.callCount).equals(0) - - render(root, []) - - o(oninit.callCount).equals(1) - o(oncreate.callCount).equals(1) - o(onbeforeupdate.callCount).equals(1) - o(onupdate.callCount).equals(1) - o(onbeforeremove.callCount).equals(1) - o(onremove.callCount).equals(1) - }) - - o("route.Link can render other tag without routes or dom access", function() { - $window = browserMock(env) - route = apiRouter(null, null) - route.prefix = prefix - root = $window.document.body - - render(root, m(route.Link, {selector: "button", href: "/test", foo: "bar"}, "text")) - - o(root.childNodes.length).equals(1) - o(root.firstChild.nodeName).equals("BUTTON") - o(root.firstChild.attributes["href"].value).equals(prefix + "/test") - o(root.firstChild.hasAttribute("aria-disabled")).equals(false) - o(root.firstChild.hasAttribute("disabled")).equals(false) - o(root.firstChild.attributes["foo"].value).equals("bar") - o(root.firstChild.childNodes.length).equals(1) - o(root.firstChild.firstChild.nodeName).equals("#text") - o(root.firstChild.firstChild.nodeValue).equals("text") - }) - - o("route.Link can render other selector without routes or dom access", function() { - $window = browserMock(env) - route = apiRouter(null, null) - route.prefix = prefix - root = $window.document.body - - render(root, m(route.Link, {selector: "button[href=/test]", foo: "bar"}, "text")) - - o(root.childNodes.length).equals(1) - o(root.firstChild.nodeName).equals("BUTTON") - o(root.firstChild.attributes["href"].value).equals(prefix + "/test") - o(root.firstChild.hasAttribute("aria-disabled")).equals(false) - o(root.firstChild.hasAttribute("disabled")).equals(false) - o(root.firstChild.attributes["foo"].value).equals("bar") - o(root.firstChild.childNodes.length).equals(1) - o(root.firstChild.firstChild.nodeName).equals("#text") - o(root.firstChild.firstChild.nodeValue).equals("text") - }) - - o("route.Link can render not disabled", function() { - $window = browserMock(env) - route = apiRouter(null, null) - route.prefix = prefix - root = $window.document.body - - render(root, m(route.Link, {href: "/test", disabled: false, foo: "bar"}, "text")) - - o(root.childNodes.length).equals(1) - o(root.firstChild.nodeName).equals("A") - o(root.firstChild.href).equals(prefix + "/test") - o(root.firstChild.hasAttribute("aria-disabled")).equals(false) - o(root.firstChild.hasAttribute("disabled")).equals(false) - o(root.firstChild.attributes["foo"].value).equals("bar") - o(root.firstChild.childNodes.length).equals(1) - o(root.firstChild.firstChild.nodeName).equals("#text") - o(root.firstChild.firstChild.nodeValue).equals("text") - }) - - o("route.Link can render falsy disabled", function() { - $window = browserMock(env) - route = apiRouter(null, null) - route.prefix = prefix - root = $window.document.body - - render(root, m(route.Link, {href: "/test", disabled: 0, foo: "bar"}, "text")) - - o(root.childNodes.length).equals(1) - o(root.firstChild.nodeName).equals("A") - o(root.firstChild.href).equals(prefix + "/test") - o(root.firstChild.hasAttribute("aria-disabled")).equals(false) - o(root.firstChild.hasAttribute("disabled")).equals(false) - o(root.firstChild.attributes["foo"].value).equals("bar") - o(root.firstChild.childNodes.length).equals(1) - o(root.firstChild.firstChild.nodeName).equals("#text") - o(root.firstChild.firstChild.nodeValue).equals("text") + route.set("/other", {state: {a: 1}}) + return waitCycles(1).then(() => { + throttleMock.fire() + o($window.history.state).deepEquals({a: 1}) + o(throttleMock.queueLength()).equals(0) + }) }) - o("route.Link can render disabled", function() { - $window = browserMock(env) - route = apiRouter(null, null) - route.prefix = prefix - root = $window.document.body + o("adds trailing slash where needed", () => { + $window.location.href = `${prefix}/test` - render(root, m(route.Link, {href: "/test", disabled: true, foo: "bar"}, "text")) - - o(root.childNodes.length).equals(1) - o(root.firstChild.nodeName).equals("A") - o(root.firstChild.href).equals("") - o(root.firstChild.attributes["aria-disabled"].value).equals("true") - o(root.firstChild.attributes["foo"].value).equals("bar") - o(root.firstChild.attributes["disabled"].value).equals("") - o(root.firstChild.childNodes.length).equals(1) - o(root.firstChild.firstChild.nodeName).equals("#text") - o(root.firstChild.firstChild.nodeValue).equals("text") + route.init(`${prefix}/`) + o(route.path).equals("/test") + o([...route.params]).deepEquals([]) + o(throttleMock.queueLength()).equals(0) }) - o("route.Link can render truthy disabled", function() { - $window = browserMock(env) - route = apiRouter(null, null) - route.prefix = prefix - root = $window.document.body + o("handles route with search", () => { + $window.location.href = `${prefix}/test?a=b&c=d` - render(root, m(route.Link, {href: "/test", disabled: 1, foo: "bar"}, "text")) - - o(root.childNodes.length).equals(1) - o(root.firstChild.nodeName).equals("A") - o(root.firstChild.href).equals("") - o(root.firstChild.attributes["aria-disabled"].value).equals("true") - o(root.firstChild.attributes["foo"].value).equals("bar") - o(root.firstChild.attributes["disabled"].value).equals("") - o(root.firstChild.childNodes.length).equals(1) - o(root.firstChild.firstChild.nodeName).equals("#text") - o(root.firstChild.firstChild.nodeValue).equals("text") + route.init(prefix) + o(route.path).equals("/test") + o([...route.params]).deepEquals([["a", "b"], ["c", "d"]]) + o(throttleMock.queueLength()).equals(0) }) - o("route.Link doesn't redraw on wrong button", function() { - var e = $window.document.createEvent("MouseEvents") - - e.initEvent("click", true, true) - e.button = 10 - - $window.location.href = prefix + "/" - route(root, "/", { - "/": () => ({ - view: lock(function() { - return m(route.Link, {href: "/test"}) - }) - }), - "/test": () => ({ - view : lock(function() { - return m("div") - }) - }) - }) + o("reacts to back button", () => { + $window.location.href = "http://old.com" + $window.location.href = "http://new.com" - var slash = prefix[0] === "/" ? "" : "/" + route.init(prefix) - o($window.location.href).equals(env.protocol + "//" + (env.hostname === "/" ? "" : env.hostname) + slash + (prefix ? prefix + "/" : "")) + $window.history.back() - root.firstChild.dispatchEvent(e) - throttleMock.fire() - o($window.location.href).equals(env.protocol + "//" + (env.hostname === "/" ? "" : env.hostname) + slash + (prefix ? prefix + "/" : "")) + o($window.location.pathname).equals("/") + o($window.location.hostname).equals("old.com") + o(throttleMock.queueLength()).equals(0) }) - o("route.Link doesn't redraw on preventDefault", function() { + o("changes location on route.Link", () => { var e = $window.document.createEvent("MouseEvents") e.initEvent("click", true, true) e.button = 0 - $window.location.href = prefix + "/" - route(root, "/", { - "/": () => ({ - view: lock(function() { - return m(route.Link, { - href: "/test", - onclick: function(e) { - e.preventDefault() - } - }) - }) - }), - "/test": () => ({ - view : lock(function() { - return m("div") - }) - }) + $window.location.href = `${prefix}/` + route.init(prefix) + mountRedraw.mount(root, () => { + if (route.path === "/") { + return m("a", route.link({href: "/test"})) + } else if (route.path === "/test") { + return m("div") + } else { + throw new Error(`Unknown route: ${route.path}`) + } }) - var slash = prefix[0] === "/" ? "" : "/" - - o($window.location.href).equals(env.protocol + "//" + (env.hostname === "/" ? "" : env.hostname) + slash + (prefix ? prefix + "/" : "")) + o($window.location.href).equals(fullPrefix) root.firstChild.dispatchEvent(e) - throttleMock.fire() - o($window.location.href).equals(env.protocol + "//" + (env.hostname === "/" ? "" : env.hostname) + slash + (prefix ? prefix + "/" : "")) - }) - o("route.Link doesn't redraw on preventDefault in handleEvent", function() { - var e = $window.document.createEvent("MouseEvents") - - e.initEvent("click", true, true) - e.button = 0 - - $window.location.href = prefix + "/" - route(root, "/", { - "/": () => ({ - view: lock(function() { - return m(route.Link, { - href: "/test", - onclick: { - handleEvent: function(e) { - e.preventDefault() - } - } - }) - }) - }), - "/test": () => ({ - view : lock(function() { - return m("div") - }) - }) + return waitCycles(1).then(() => { + throttleMock.fire() + o($window.location.href).equals(`${fullPrefix}test`) + o(throttleMock.queueLength()).equals(0) }) - - var slash = prefix[0] === "/" ? "" : "/" - - o($window.location.href).equals(env.protocol + "//" + (env.hostname === "/" ? "" : env.hostname) + slash + (prefix ? prefix + "/" : "")) - - root.firstChild.dispatchEvent(e) - throttleMock.fire() - o($window.location.href).equals(env.protocol + "//" + (env.hostname === "/" ? "" : env.hostname) + slash + (prefix ? prefix + "/" : "")) }) - o("route.Link doesn't redraw on return false", function() { + o("passes state on route.Link", () => { var e = $window.document.createEvent("MouseEvents") e.initEvent("click", true, true) e.button = 0 - - $window.location.href = prefix + "/" - route(root, "/", { - "/": () => ({ - view: lock(function() { - return m(route.Link, { - href: "/test", - onclick: function() { - return false - } - }) - }) - }), - "/test": () => ({ - view : lock(function() { - return m("div") - }) - }) + $window.location.href = `${prefix}/` + route.init(prefix) + mountRedraw.mount(root, () => { + if (route.path === "/") { + return m("a", route.link({href: "/test", state: {a: 1}})) + } else if (route.path === "/test") { + return m("div") + } else { + throw new Error(`Unknown route: ${route.path}`) + } }) - var slash = prefix[0] === "/" ? "" : "/" - - o($window.location.href).equals(env.protocol + "//" + (env.hostname === "/" ? "" : env.hostname) + slash + (prefix ? prefix + "/" : "")) - root.firstChild.dispatchEvent(e) - throttleMock.fire() - o($window.location.href).equals(env.protocol + "//" + (env.hostname === "/" ? "" : env.hostname) + slash + (prefix ? prefix + "/" : "")) - }) - - o("accepts RouteResolver with onmatch that returns Component", function() { - var matchCount = 0 - var renderCount = 0 - var Component = () => ({ - view: lock(function() { - return m("span") - }) - }) - - var resolver = { - onmatch: lock(function(args, requestedPath, route) { - matchCount++ - - o(args.id).equals("abc") - o(requestedPath).equals("/abc") - o(route).equals("/:id") - o(this).equals(resolver) - return Component - }), - render: lock(function(vnode) { - renderCount++ - o(vnode.attrs.id).equals("abc") - o(this).equals(resolver) - - return vnode - }), - } - - $window.location.href = prefix + "/abc" - route(root, "/abc", { - "/:id" : resolver - }) - - return waitCycles(1).then(function() { - o(matchCount).equals(1) - o(renderCount).equals(1) - o(root.firstChild.nodeName).equals("SPAN") - }) - }) - - o("accepts RouteResolver with onmatch that returns route.SKIP", function() { - var match1Count = 0 - var match2Count = 0 - var render1 = o.spy() - var render2Count = 0 - var Component = () => ({ - view: lock(function() { - return m("span") - }) - }) - - var resolver1 = { - onmatch: lock(function(args, requestedPath, key) { - match1Count++ - - o(args.id1).equals("abc") - o(requestedPath).equals("/abc") - o(key).equals("/:id1") - o(this).equals(resolver1) - return route.SKIP - }), - render: lock(render1), - } - - var resolver2 = { - onmatch: function(args, requestedPath, key) { - match2Count++ - - o(args.id2).equals("abc") - o(requestedPath).equals("/abc") - o(key).equals("/:id2") - o(this).equals(resolver2) - return Component - }, - render: function(vnode) { - render2Count++ - - o(vnode.attrs.id2).equals("abc") - o(this).equals(resolver2) - o(render1.callCount).equals(0) - - return vnode - }, - } - - $window.location.href = prefix + "/abc" - route(root, "/abc", { - "/:id1" : resolver1, - "/:id2" : resolver2 - }) - - return waitCycles(4).then(function() { - o(match1Count).equals(1) - o(match2Count).equals(1) - o(render2Count).equals(1) - o(render1.callCount).equals(0) - o(root.firstChild.nodeName).equals("SPAN") - }) - }) - - o("accepts RouteResolver with onmatch that returns Promise", function() { - var matchCount = 0 - var renderCount = 0 - var Component = () => ({ - view: lock(function() { - return m("span") - }) - }) - - var resolver = { - onmatch: lock(function(args, requestedPath, route) { - matchCount++ - - o(args.id).equals("abc") - o(requestedPath).equals("/abc") - o(route).equals("/:id") - o(this).equals(resolver) - return Promise.resolve(Component) - }), - render: lock(function(vnode) { - renderCount++ - - o(vnode.attrs.id).equals("abc") - o(this).equals(resolver) - - return vnode - }), - } - - $window.location.href = prefix + "/abc" - route(root, "/abc", { - "/:id" : resolver - }) - - return waitCycles(10).then(function() { - o(matchCount).equals(1) - o(renderCount).equals(1) - o(root.firstChild.nodeName).equals("SPAN") - }) - }) - - o("accepts RouteResolver with onmatch that returns Promise", function() { - var matchCount = 0 - var renderCount = 0 - - var resolver = { - onmatch: lock(function(args, requestedPath, route) { - matchCount++ - - o(args.id).equals("abc") - o(requestedPath).equals("/abc") - o(route).equals("/:id") - o(this).equals(resolver) - return Promise.resolve() - }), - render: lock(function(vnode) { - renderCount++ - - o(vnode.attrs.id).equals("abc") - o(this).equals(resolver) - - return vnode - }), - } - - $window.location.href = prefix + "/abc" - route(root, "/abc", { - "/:id" : resolver - }) - - return waitCycles(2).then(function() { - o(matchCount).equals(1) - o(renderCount).equals(1) - o(root.firstChild.nodeName).equals("DIV") - }) - }) - - o("accepts RouteResolver with onmatch that returns Promise", function() { - var matchCount = 0 - var renderCount = 0 - - var resolver = { - onmatch: lock(function(args, requestedPath, route) { - matchCount++ - - o(args.id).equals("abc") - o(requestedPath).equals("/abc") - o(route).equals("/:id") - o(this).equals(resolver) - return Promise.resolve([]) - }), - render: lock(function(vnode) { - renderCount++ - - o(vnode.attrs.id).equals("abc") - o(this).equals(resolver) - - return vnode - }), - } - - $window.location.href = prefix + "/abc" - route(root, "/abc", { - "/:id" : resolver - }) - - return waitCycles(2).then(function() { - o(matchCount).equals(1) - o(renderCount).equals(1) - o(root.firstChild.nodeName).equals("DIV") + return waitCycles(1).then(() => { + throttleMock.fire() + o($window.history.state).deepEquals({a: 1}) + o(throttleMock.queueLength()).equals(0) }) }) - o("accepts RouteResolver with onmatch that returns rejected Promise", function() { - var matchCount = 0 - var renderCount = 0 - var spy = o.spy() - var error = new Error("error") - var errorSpy = console.error = o.spy() - - var resolver = { - onmatch: lock(function() { - matchCount++ - return Promise.reject(error) - }), - render: lock(function(vnode) { - renderCount++ - return vnode - }), - } + o("route.Link can render without routes or dom access", () => { + $window = browserMock(env) + var route = apiRouter(null, null) + route.init(prefix) - $window.location.href = prefix + "/test/1" - route(root, "/default", { - "/default": () => ({view: spy}), - "/test/:id" : resolver - }) + var enabled = route.link({href: "/test"}) + o(Object.keys(enabled)).deepEquals(["href", "onclick"]) + o(enabled.href).equals(`${prefix}/test`) + o(typeof enabled.onclick).equals("function") - return waitCycles(3).then(function() { - o(matchCount).equals(1) - o(renderCount).equals(0) - o(spy.callCount).equals(1) - o(errorSpy.callCount).equals(1) - o(errorSpy.args[0]).equals(error) - }) + var disabled = route.link({disabled: true, href: "/test"}) + o(disabled).deepEquals({disabled: true, "aria-disabled": "true"}) + o(throttleMock.queueLength()).equals(0) }) - o("accepts RouteResolver without `render` method as payload", function() { - var matchCount = 0 - var Component = () => ({ - view: lock(function() { - return m("div") - }) - }) - - $window.location.href = prefix + "/abc" - route(root, "/abc", { - "/:id": { - onmatch: lock(function(args, requestedPath, route) { - matchCount++ - - o(args.id).equals("abc") - o(requestedPath).equals("/abc") - o(route).equals("/:id") - - return Component - }), - }, - }) - - return waitCycles(2).then(function() { - o(matchCount).equals(1) - o(root.firstChild.nodeName).equals("DIV") - }) - }) + o("route.Link doesn't redraw on wrong button", () => { + var e = $window.document.createEvent("MouseEvents") - o("changing `key` param does not reset the component", function(){ - var oninit = o.spy() - var Component = () => ({ - oninit: oninit, - view: lock(function() { - return m("div") - }) - }) - $window.location.href = prefix + "/abc" - route(root, "/abc", { - "/:key": Component, - }) - return waitCycles(1).then(function() { - o(oninit.callCount).equals(1) - route.set("/def") - return waitCycles(1).then(function() { - throttleMock.fire() - o(oninit.callCount).equals(1) - }) - }) - }) + e.initEvent("click", true, true) + e.button = 10 - o("accepts RouteResolver without `onmatch` method as payload", function() { - var renderCount = 0 - var Component = () => ({ - view: lock(function() { + $window.location.href = `${prefix}/` + route.init(prefix) + mountRedraw.mount(root, () => { + if (route.path === "/") { + return m("a", route.link({href: "/test"})) + } else if (route.path === "/test") { return m("div") - }) - }) - - $window.location.href = prefix + "/abc" - route(root, "/abc", { - "/:id": { - render: lock(function(vnode) { - renderCount++ - - o(vnode.attrs.id).equals("abc") - - return m(Component) - }), - }, - }) - - o(root.firstChild.nodeName).equals("DIV") - o(renderCount).equals(1) - }) - - o("RouteResolver `render` does not have component semantics", function() { - $window.location.href = prefix + "/a" - route(root, "/a", { - "/a": { - render: lock(function() { - return m("div", m("p")) - }), - }, - "/b": { - render: lock(function() { - return m("div", m("a")) - }), - }, + } else { + throw new Error(`Unknown route: ${route.path}`) + } }) - var dom = root.firstChild - var child = dom.firstChild - - o(root.firstChild.nodeName).equals("DIV") + o($window.location.href).equals(fullPrefix) - route.set("/b") + root.firstChild.dispatchEvent(e) - return waitCycles(1).then(function() { + return waitCycles(1).then(() => { throttleMock.fire() - - o(root.firstChild).equals(dom) - o(root.firstChild.firstChild).notEquals(child) + o($window.location.href).equals(fullPrefix) + o(throttleMock.queueLength()).equals(0) }) }) - o("calls onmatch and view correct number of times", function() { - var matchCount = 0 - var renderCount = 0 - var Component = () => ({ - view: lock(function() { - return m("div") - }) - }) - - $window.location.href = prefix + "/" - route(root, "/", { - "/": { - onmatch: lock(function() { - matchCount++ - return Component - }), - render: lock(function(vnode) { - renderCount++ - return vnode - }), - }, - }) - - return waitCycles(1).then(function() { - o(matchCount).equals(1) - o(renderCount).equals(1) - - mountRedraw.redraw() - throttleMock.fire() + o("route.Link doesn't redraw on preventDefault", () => { + var e = $window.document.createEvent("MouseEvents") - o(matchCount).equals(1) - o(renderCount).equals(2) - }) - }) + e.initEvent("click", true, true) + e.button = 0 - o("calls onmatch and view correct number of times when not onmatch returns undefined", function() { - var matchCount = 0 - var renderCount = 0 - var Component = () => ({ - view: lock(function() { + $window.location.href = `${prefix}/` + route.init(prefix) + mountRedraw.mount(root, () => { + if (route.path === "/") { + return m("a", route.link({href: "/test", onclick(e) { e.preventDefault() }})) + } else if (route.path === "/test") { return m("div") - }) - }) - - $window.location.href = prefix + "/" - route(root, "/", { - "/": { - onmatch: lock(function() { - matchCount++ - }), - render: lock(function() { - renderCount++ - return m(Component) - }), - }, - }) - - return waitCycles(2).then(function() { - o(matchCount).equals(1) - o(renderCount).equals(1) - - mountRedraw.redraw() - throttleMock.fire() - - o(matchCount).equals(1) - o(renderCount).equals(2) - }) - }) - - o("onmatch can redirect to another route", function() { - var redirected = false - var render = o.spy() - - $window.location.href = prefix + "/a" - route(root, "/a", { - "/a": { - onmatch: lock(function() { - route.set("/b") - }), - render: lock(render) - }, - "/b": () => ({ - view: lock(function() { - redirected = true - }) - }) - }) - - return waitCycles(2).then(function() { - o(render.callCount).equals(0) - o(redirected).equals(true) - }) - }) - - o("onmatch can redirect to another route that has RouteResolver with only onmatch", function() { - var redirected = false - var render = o.spy() - var view = o.spy(function() {return m("div")}) - - $window.location.href = prefix + "/a" - route(root, "/a", { - "/a": { - onmatch: lock(function() { - route.set("/b", {state: {a: 5}}) - }), - render: lock(render) - }, - "/b": { - onmatch: lock(function() { - redirected = true - return () => ({view: lock(view)}) - }) + } else { + throw new Error(`Unknown route: ${route.path}`) } }) - return waitCycles(3).then(function() { - o(render.callCount).equals(0) - o(redirected).equals(true) - o(view.callCount).equals(1) - o(root.childNodes.length).equals(1) - o(root.firstChild.nodeName).equals("DIV") - o($window.history.state).deepEquals({a: 5}) - }) - }) + o($window.location.href).equals(fullPrefix) - o("onmatch can redirect to another route that has RouteResolver with only render", function() { - var redirected = false - var render = o.spy() - - $window.location.href = prefix + "/a" - route(root, "/a", { - "/a": { - onmatch: lock(function() { - route.set("/b") - }), - render: lock(render) - }, - "/b": { - render: lock(function(){ - redirected = true - }) - } - }) + root.firstChild.dispatchEvent(e) - return waitCycles(2).then(function() { - o(render.callCount).equals(0) - o(redirected).equals(true) + return waitCycles(1).then(() => { + throttleMock.fire() + o($window.location.href).equals(fullPrefix) + o(throttleMock.queueLength()).equals(0) }) }) - o("onmatch can redirect to another route that has RouteResolver whose onmatch resolves asynchronously", function() { - var redirected = false - var render = o.spy() - var view = o.spy() - - $window.location.href = prefix + "/a" - route(root, "/a", { - "/a": { - onmatch: lock(function() { - route.set("/b") - }), - render: lock(render) - }, - "/b": { - onmatch: lock(function() { - redirected = true - return waitCycles(1).then(function(){ - return () => ({view: view}) - }) - }) - } - }) + o("route.Link doesn't redraw on preventDefault in handleEvent", () => { + var e = $window.document.createEvent("MouseEvents") - return waitCycles(6).then(function() { - o(render.callCount).equals(0) - o(redirected).equals(true) - o(view.callCount).equals(1) - }) - }) + e.initEvent("click", true, true) + e.button = 0 - o("onmatch can redirect to another route asynchronously", function() { - var redirected = false - var render = o.spy() - var view = o.spy() - - $window.location.href = prefix + "/a" - route(root, "/a", { - "/a": { - onmatch: lock(function() { - waitCycles(1).then(function() {route.set("/b")}) - return new Promise(function() {}) - }), - render: lock(render) - }, - "/b": { - onmatch: lock(function() { - redirected = true - return () => ({view: lock(view)}) - }) + $window.location.href = `${prefix}/` + route.init(prefix) + mountRedraw.mount(root, () => { + if (route.path === "/") { + return m("a", route.link({href: "/test", onclick: {handleEvent(e) { e.preventDefault() }}})) + } else if (route.path === "/test") { + return m("div") + } else { + throw new Error(`Unknown route: ${route.path}`) } }) - return waitCycles(5).then(function() { - o(render.callCount).equals(0) - o(redirected).equals(true) - o(view.callCount).equals(1) - }) - }) + o($window.location.href).equals(fullPrefix) - o("onmatch can redirect with window.history.back()", function() { - - var render = o.spy() - var instance = {view: o.spy()} - var Component = () => instance - - $window.location.href = prefix + "/a" - route(root, "/a", { - "/a": { - onmatch: lock(function() { - return Component - }), - render: lock(function(vnode) { - return vnode - }) - }, - "/b": { - onmatch: lock(function() { - $window.history.back() - return new Promise(function() {}) - }), - render: lock(render) - } - }) + root.firstChild.dispatchEvent(e) - return waitCycles(2).then(function() { + return waitCycles(1).then(() => { throttleMock.fire() - - route.set("/b") - o(render.callCount).equals(0) - o(instance.view.callCount).equals(1) - - return waitCycles(4).then(function() { - throttleMock.fire() - - o(render.callCount).equals(0) - o(instance.view.callCount).equals(2) - }) + o($window.location.href).equals(fullPrefix) + o(throttleMock.queueLength()).equals(0) }) }) - o("onmatch can redirect to a non-existent route that defaults to a RouteResolver with onmatch", function() { - var redirected = false - var render = o.spy() - - $window.location.href = prefix + "/a" - route(root, "/b", { - "/a": { - onmatch: lock(function() { - route.set("/c") - }), - render: lock(render) - }, - "/b": { - onmatch: lock(function(){ - redirected = true - return () => ({view: lock(function() {})}) - }) - } - }) + o("route.Link doesn't redraw on return false", () => { + var e = $window.document.createEvent("MouseEvents") - return waitCycles(3).then(function() { - o(render.callCount).equals(0) - o(redirected).equals(true) - }) - }) + e.initEvent("click", true, true) + e.button = 0 - o("onmatch can redirect to a non-existent route that defaults to a RouteResolver with render", function() { - var redirected = false - var render = o.spy() - - $window.location.href = prefix + "/a" - route(root, "/b", { - "/a": { - onmatch: lock(function() { - route.set("/c") - }), - render: lock(render) - }, - "/b": { - render: lock(function(){ - redirected = true - }) + $window.location.href = `${prefix}/` + route.init(prefix) + mountRedraw.mount(root, () => { + if (route.path === "/") { + return m("a", route.link({href: "/test", onclick: () => false})) + } else if (route.path === "/test") { + return m("div") + } else { + throw new Error(`Unknown route: ${route.path}`) } }) - return waitCycles(3).then(function() { - o(render.callCount).equals(0) - o(redirected).equals(true) - }) - }) - - o("onmatch can redirect to a non-existent route that defaults to a component", function() { - var redirected = false - var render = o.spy() - - $window.location.href = prefix + "/a" - route(root, "/b", { - "/a": { - onmatch: lock(function() { - route.set("/c") - }), - render: lock(render) - }, - "/b": () => ({ - view: lock(function(){ - redirected = true - }) - }) - }) - - return waitCycles(3).then(function() { - o(render.callCount).equals(0) - o(redirected).equals(true) - }) - }) - - o("the previous view redraws while onmatch resolution is pending (#1268)", function() { - var view = o.spy() - var onmatch = o.spy(function() { - return new Promise(function() {}) - }) - - $window.location.href = prefix + "/a" - route(root, "/", { - "/a": () => ({view: lock(view)}), - "/b": {onmatch: lock(onmatch)}, - "/": () => ({view: lock(function() {})}) - }) - - o(view.callCount).equals(1) - o(onmatch.callCount).equals(0) + o($window.location.href).equals(fullPrefix) - route.set("/b") - - return waitCycles(1).then(function() { - o(view.callCount).equals(1) - o(onmatch.callCount).equals(1) + root.firstChild.dispatchEvent(e) - mountRedraw.redraw() + return waitCycles(1).then(() => { throttleMock.fire() - - o(view.callCount).equals(2) - o(onmatch.callCount).equals(1) + o($window.location.href).equals(fullPrefix) + o(throttleMock.queueLength()).equals(0) }) }) - o("when two async routes are racing, the last one set cancels the finalization of the first", function(done) { - var renderA = o.spy() - var renderB = o.spy() - var onmatchA = o.spy(function(){ - return waitCycles(3) - }) + o("m.route.set(m.route.get()) re-runs the resolution logic (#1180)", () => { + var render = o.spy(() => m("div")) - $window.location.href = prefix + "/a" - route(root, "/a", { - "/a": { - onmatch: lock(onmatchA), - render: lock(renderA) - }, - "/b": { - onmatch: lock(function(){ - var p = new Promise(function(fulfill) { - o(onmatchA.callCount).equals(1) - o(renderA.callCount).equals(0) - o(renderB.callCount).equals(0) - - waitCycles(3).then(function(){ - o(onmatchA.callCount).equals(1) - o(renderA.callCount).equals(0) - o(renderB.callCount).equals(0) - - fulfill() - return p - }).then(function(){ - return waitCycles(1) - }).then(function(){ - o(onmatchA.callCount).equals(1) - o(renderA.callCount).equals(0) - o(renderB.callCount).equals(1) - }).then(done, done) - }) - return p - }), - render: lock(renderB) - } - }) + $window.location.href = `${prefix}/` + route.init(prefix) + mountRedraw.mount(root, render) - waitCycles(1).then(lock(function() { - o(onmatchA.callCount).equals(1) - o(renderA.callCount).equals(0) - o(renderB.callCount).equals(0) - route.set("/b") - o(onmatchA.callCount).equals(1) - o(renderA.callCount).equals(0) - o(renderB.callCount).equals(0) - })) - }) - - o("m.route.set(m.route.get()) re-runs the resolution logic (#1180)", function(){ - var onmatch = o.spy() - var render = o.spy(function() {return m("div")}) - - $window.location.href = prefix + "/" - route(root, "/", { - "/": { - onmatch: lock(onmatch), - render: lock(render) - } - }) - - return waitCycles(1).then(function() { + return waitCycles(1).then(() => { throttleMock.fire() - - o(onmatch.callCount).equals(1) o(render.callCount).equals(1) route.set(route.get()) - return waitCycles(2).then(function() { + return waitCycles(2).then(() => { throttleMock.fire() - - o(onmatch.callCount).equals(2) o(render.callCount).equals(2) + o(throttleMock.queueLength()).equals(0) }) }) }) - o("m.route.get() returns the last fully resolved route (#1276)", function(){ - $window.location.href = prefix + "/" - - route(root, "/", { - "/": () => ({view: lock(function() {})}), - "/2": { - onmatch: lock(function() { - return new Promise(function() {}) - }) - } - }) - - - o(route.get()).equals("/") - - route.set("/2") - - return waitCycles(1).then(function() { - o(route.get()).equals("/") - }) - }) - - o("routing with RouteResolver works more than once", function() { - $window.location.href = prefix + "/a" - route(root, "/a", { - "/a": { - render: lock(function() { - return m("a", "a") - }) - }, - "/b": { - render: lock(function() { - return m("b", "b") - }) - } - }) - - route.set("/b") - - return waitCycles(1).then(function() { - throttleMock.fire() - - o(root.firstChild.nodeName).equals("B") - - route.set("/a") - - return waitCycles(1).then(function() { - throttleMock.fire() - - o(root.firstChild.nodeName).equals("A") - }) - }) - }) - - o("calling route.set invalidates pending onmatch resolution", function() { - var rendered = false - var resolved - $window.location.href = prefix + "/a" - route(root, "/a", { - "/a": { - onmatch: lock(function() { - return waitCycles(2).then(function() { - return () => ({view: lock(function() {rendered = true})}) - }) - }), - render: lock(function() { - rendered = true - resolved = "a" - }) - }, - "/b": () => ({ - view: lock(function() { - resolved = "b" - }) - }) - }) - - route.set("/b") - - return waitCycles(1).then(function() { - o(rendered).equals(false) - o(resolved).equals("b") - - return waitCycles(1).then(function() { - o(rendered).equals(false) - o(resolved).equals("b") - }) - }) - }) - - o("route changes activate onbeforeremove", function() { - var spy = o.spy() - - $window.location.href = prefix + "/a" - route(root, "/a", { - "/a": () => ({ - onbeforeremove: lock(spy), - view: lock(function() {}) - }), - "/b": () => ({ - view: lock(function() {}) - }) - }) - - route.set("/b") - - // setting the route is asynchronous - return waitCycles(1).then(function() { - throttleMock.fire() - o(spy.callCount).equals(1) - }) - }) - - o("asynchronous route.set in onmatch works", function() { - var rendered = false, resolved - route(root, "/a", { - "/a": { - onmatch: lock(function() { - return Promise.resolve().then(lock(function() { - route.set("/b") - })) - }), - render: lock(function() { - rendered = true - resolved = "a" - }) - }, - "/b": () => ({ - view: lock(function() { - resolved = "b" - }) - }), - }) - - // tick for popstate for /a - // tick for onmatch - // tick for promise in onmatch - // tick for onpopstate for /b - return waitCycles(4).then(function() { - o(rendered).equals(false) - o(resolved).equals("b") - }) - }) - - o("throttles", function() { + o("throttles", () => { var i = 0 - $window.location.href = prefix + "/" - route(root, "/", { - "/": () => ({view: lock(function() {i++})}) - }) + + $window.location.href = `${prefix}/` + route.init(prefix) + mountRedraw.mount(root, () => { i++ }) var before = i mountRedraw.redraw() @@ -1847,28 +515,7 @@ o.spec("route", function() { o(before).equals(1) // routes synchronously o(after).equals(1) // redraws asynchronously o(i).equals(2) - }) - - o("m.route.param is available outside of route handlers", function() { - $window.location.href = prefix + "/" - - route(root, "/1", { - "/:id": () => ({ - view : lock(function() { - o(route.param("id")).equals("1") - - return m("div") - }) - }) - }) - - o(route.param("id")).equals(undefined); - o(route.param()).deepEquals(undefined); - - return waitCycles(1).then(function() { - o(route.param("id")).equals("1") - o(route.param()).deepEquals({id:"1"}) - }) + o(throttleMock.queueLength()).equals(0) }) }) }) diff --git a/api/tests/test-routerGetSet.js b/api/tests/test-routerGetSet.js deleted file mode 100644 index a624aaa9e..000000000 --- a/api/tests/test-routerGetSet.js +++ /dev/null @@ -1,274 +0,0 @@ -"use strict" - -// Low-priority TODO: remove the dependency on the renderer here. -var o = require("ospec") -var browserMock = require("../../test-utils/browserMock") -var throttleMocker = require("../../test-utils/throttleMock") - -var apiMountRedraw = require("../../api/mount-redraw") -var apiRouter = require("../../api/router") -var p = require("../../pathname/build") - -o.spec("route.get/route.set", function() { - function waitTask() { - return new Promise((resolve) => setTimeout(resolve, 0)) - } - - 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() - $window.setTimeout = setTimeout - - root = $window.document.body - - mountRedraw = apiMountRedraw(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() { - $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) - return waitTask().then(() => { - throttleMock.fire() - - o(spy1.callCount).equals(1) - o(spy2.callCount).equals(1) - }) - }) - - o("sets fallback asynchronously", function() { - $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) - return waitTask() - // Yep, before even the throttle mechanism takes hold. - .then(() => { o(route.get()).equals("/b") }) - // Yep, before even the throttle mechanism takes hold. - .then(() => waitTask()) - .then(() => { - o(route.get()).equals("/a") - throttleMock.fire() - - o(spy1.callCount).equals(1) - o(spy2.callCount).equals(1) - }) - }) - - o("exposes new route asynchronously", function() { - $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") - return waitTask().then(() => { - // Yep, before even the throttle mechanism takes hold. - o(route.get()).equals("/other/x/y/z?c=d#e=f") - throttleMock.fire() - }) - }) - - o("exposes new escaped unicode route asynchronously", function() { - $window.location.href = prefix + "/test" - route(root, "/test", { - "/test": () => ({view: function() {}}), - "/ö": () => ({view: function() {}}), - }) - - route.set(encodeURI("/ö?ö=ö#ö=ö")) - return waitTask().then(() => { - // Yep, before even the throttle mechanism takes hold. - o(route.get()).equals("/ö?ö=ö#ö=ö") - throttleMock.fire() - }) - }) - - o("exposes new unescaped unicode route asynchronously", function() { - $window.location.href = "file://" + prefix + "/test" - route(root, "/test", { - "/test": () => ({view: function() {}}), - "/ö": () => ({view: function() {}}), - }) - - route.set("/ö?ö=ö#ö=ö") - return waitTask().then(() => { - // Yep, before even the throttle mechanism takes hold. - o(route.get()).equals("/ö?ö=ö#ö=ö") - throttleMock.fire() - }) - }) - - o("exposes new route asynchronously on fallback mode", function() { - $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") - return waitTask().then(() => { - // Yep, before even the throttle mechanism takes hold. - o(route.get()).equals("/other/x/y/z?c=d#e=f") - throttleMock.fire() - }) - }) - - o("sets route via pushState/onpopstate", function() { - $window.location.href = prefix + "/test" - route(root, "/test", { - "/test": () => ({view: function() {}}), - "/other/:a/:b...": () => ({view: function() {}}), - }) - - return waitTask() - .then(() => { - $window.history.pushState(null, null, prefix + "/other/x/y/z?c=d#e=f") - $window.onpopstate() - }) - .then(() => waitTask()) - .then(() => { - // Yep, before even the throttle mechanism takes hold. - o(route.get()).equals("/other/x/y/z?c=d#e=f") - throttleMock.fire() - }) - }) - - o("sets parameterized route", function() { - $window.location.href = prefix + "/test" - route(root, "/test", { - "/test": () => ({view: function() {}}), - "/other/:a/:b...": () => ({view: function() {}}), - }) - - route.set(p("/other/:a/:b", {a: "x", b: "y/z", c: "d", e: "f"})) - return waitTask().then(() => { - // Yep, before even the throttle mechanism takes hold. - o(route.get()).equals("/other/x/y%2Fz?c=d&e=f") - throttleMock.fire() - }) - }) - - o("replace:true works", function() { - $window.location.href = prefix + "/test" - route(root, "/test", { - "/test": () => ({view: function() {}}), - "/other": () => ({view: function() {}}), - }) - - route.set("/other", {replace: true}) - - return waitTask().then(() => { - throttleMock.fire() - $window.history.back() - o($window.location.href).equals(env.protocol + "//" + (env.hostname === "/" ? "" : env.hostname) + "/") - }) - }) - - o("replace:false works", function() { - $window.location.href = prefix + "/test" - route(root, "/test", { - "/test": () => ({view: function() {}}), - "/other": () => ({view: function() {}}), - }) - - route.set("/other", {replace: false}) - - return waitTask().then(() => { - throttleMock.fire() - $window.history.back() - var slash = prefix[0] === "/" ? "" : "/" - o($window.location.href).equals(env.protocol + "//" + (env.hostname === "/" ? "" : env.hostname) + slash + (prefix ? prefix + "/" : "") + "test") - }) - }) - - o("state works", function() { - $window.location.href = prefix + "/test" - route(root, "/test", { - "/test": () => ({view: function() {}}), - "/other": () => ({view: function() {}}), - }) - - route.set("/other", {state: {a: 1}}) - return waitTask().then(() => { - throttleMock.fire() - o($window.history.state).deepEquals({a: 1}) - }) - }) - }) - }) - }) -}) diff --git a/index.js b/index.js index c5365a0e1..0e01ff0cd 100644 --- a/index.js +++ b/index.js @@ -14,7 +14,6 @@ m.render = require("./render") m.redraw = mountRedraw.redraw m.parseQueryString = require("./querystring/parse") m.buildQueryString = require("./querystring/build") -m.parsePathname = require("./pathname/parse") m.p = require("./pathname/build") m.withProgress = require("./util/with-progress") m.vnode = require("./render/vnode") diff --git a/pathname/compileTemplate.js b/pathname/compileTemplate.js deleted file mode 100644 index 390c1e568..000000000 --- a/pathname/compileTemplate.js +++ /dev/null @@ -1,43 +0,0 @@ -"use strict" - -var parsePathname = require("./parse") - -// Compiles a template into a function that takes a resolved path (without query -// strings) and returns an object containing the template parameters with their -// parsed values. This expects the input of the compiled template to be the -// output of `parsePathname`. Note that it does *not* remove query parameters -// specified in the template. -module.exports = function(template) { - var templateData = parsePathname(template) - var templateKeys = Object.keys(templateData.params) - var keys = [] - var regexp = new RegExp("^" + templateData.path.replace( - // I escape literal text so people can use things like `:file.:ext` or - // `:lang-:locale` in routes. This is all merged into one pass so I - // don't also accidentally escape `-` and make it harder to detect it to - // ban it from template parameters. - /:([^\/.-]+)(\.{3}|\.(?!\.)|-)?|[\\^$*+.()|\[\]{}]/g, - function(m, key, extra) { - if (key == null) return "\\" + m - keys.push({k: key, r: extra === "..."}) - if (extra === "...") return "(.*)" - if (extra === ".") return "([^/]+)\\." - return "([^/]+)" + (extra || "") - } - ) + "$") - return function(data) { - // First, check the params. Usually, there isn't any, and it's just - // checking a static set. - for (var i = 0; i < templateKeys.length; i++) { - if (templateData.params[templateKeys[i]] !== data.params[templateKeys[i]]) return false - } - // If no interpolations exist, let's skip all the ceremony - if (!keys.length) return regexp.test(data.path) - var values = regexp.exec(data.path) - if (values == null) return false - for (var i = 0; i < keys.length; i++) { - data.params[keys[i].k] = keys[i].r ? values[i + 1] : decodeURIComponent(values[i + 1]) - } - return true - } -} diff --git a/pathname/parse.js b/pathname/parse.js deleted file mode 100644 index a7e97fae0..000000000 --- a/pathname/parse.js +++ /dev/null @@ -1,23 +0,0 @@ -"use strict" - -var parseQueryString = require("../querystring/parse") - -// Returns `{path, params}` from `url` -module.exports = function(url) { - var queryIndex = url.indexOf("?") - var hashIndex = url.indexOf("#") - var queryEnd = hashIndex < 0 ? url.length : hashIndex - var pathEnd = queryIndex < 0 ? queryEnd : queryIndex - var path = url.slice(0, pathEnd).replace(/\/{2,}/g, "/") - - if (!path) path = "/" - else { - if (path[0] !== "/") path = "/" + path - } - return { - path: path, - params: queryIndex < 0 - ? {} - : parseQueryString(url.slice(queryIndex + 1, queryEnd)), - } -} diff --git a/pathname/tests/test-compileTemplate.js b/pathname/tests/test-compileTemplate.js deleted file mode 100644 index e9725b06c..000000000 --- a/pathname/tests/test-compileTemplate.js +++ /dev/null @@ -1,221 +0,0 @@ -"use strict" - -var o = require("ospec") -var parsePathname = require("../../pathname/parse") -var compileTemplate = require("../../pathname/compileTemplate") - -o.spec("compileTemplate", function() { - o("checks empty string", function() { - var data = parsePathname("/") - o(compileTemplate("/")(data)).equals(true) - o(data.params).deepEquals({}) - }) - o("checks identical match", function() { - var data = parsePathname("/foo") - o(compileTemplate("/foo")(data)).equals(true) - o(data.params).deepEquals({}) - }) - o("checks identical mismatch", function() { - var data = parsePathname("/bar") - o(compileTemplate("/foo")(data)).equals(false) - o(data.params).deepEquals({}) - }) - o("checks single parameter", function() { - var data = parsePathname("/1") - o(compileTemplate("/:id")(data)).equals(true) - o(data.params).deepEquals({id: "1"}) - }) - o("checks single variadic parameter", function() { - var data = parsePathname("/some/path") - o(compileTemplate("/:id...")(data)).equals(true) - o(data.params).deepEquals({id: "some/path"}) - }) - o("checks single parameter with extra match", function() { - var data = parsePathname("/1/foo") - o(compileTemplate("/:id/foo")(data)).equals(true) - o(data.params).deepEquals({id: "1"}) - }) - o("checks single parameter with extra mismatch", function() { - var data = parsePathname("/1/bar") - o(compileTemplate("/:id/foo")(data)).equals(false) - o(data.params).deepEquals({}) - }) - o("checks single variadic parameter with extra match", function() { - var data = parsePathname("/some/path/foo") - o(compileTemplate("/:id.../foo")(data)).equals(true) - o(data.params).deepEquals({id: "some/path"}) - }) - o("checks single variadic parameter with extra mismatch", function() { - var data = parsePathname("/some/path/bar") - o(compileTemplate("/:id.../foo")(data)).equals(false) - o(data.params).deepEquals({}) - }) - o("checks multiple parameters", function() { - var data = parsePathname("/1/2") - o(compileTemplate("/:id/:name")(data)).equals(true) - o(data.params).deepEquals({id: "1", name: "2"}) - }) - o("checks incomplete multiple parameters", function() { - var data = parsePathname("/1") - o(compileTemplate("/:id/:name")(data)).equals(false) - o(data.params).deepEquals({}) - }) - o("checks multiple parameters with extra match", function() { - var data = parsePathname("/1/2/foo") - o(compileTemplate("/:id/:name/foo")(data)).equals(true) - o(data.params).deepEquals({id: "1", name: "2"}) - }) - o("checks multiple parameters with extra mismatch", function() { - var data = parsePathname("/1/2/bar") - o(compileTemplate("/:id/:name/foo")(data)).equals(false) - o(data.params).deepEquals({}) - }) - o("checks multiple parameters, last variadic, with extra match", function() { - var data = parsePathname("/1/some/path/foo") - o(compileTemplate("/:id/:name.../foo")(data)).equals(true) - o(data.params).deepEquals({id: "1", name: "some/path"}) - }) - o("checks multiple parameters, last variadic, with extra mismatch", function() { - var data = parsePathname("/1/some/path/bar") - o(compileTemplate("/:id/:name.../foo")(data)).equals(false) - o(data.params).deepEquals({}) - }) - o("checks multiple separated parameters", function() { - var data = parsePathname("/1/sep/2") - o(compileTemplate("/:id/sep/:name")(data)).equals(true) - o(data.params).deepEquals({id: "1", name: "2"}) - }) - o("checks incomplete multiple separated parameters", function() { - var data = parsePathname("/1") - o(compileTemplate("/:id/sep/:name")(data)).equals(false) - o(data.params).deepEquals({}) - data = parsePathname("/1/sep") - o(compileTemplate("/:id/sep/:name")(data)).equals(false) - o(data.params).deepEquals({}) - }) - o("checks multiple separated parameters missing sep", function() { - var data = parsePathname("/1/2") - o(compileTemplate("/:id/sep/:name")(data)).equals(false) - o(data.params).deepEquals({}) - }) - o("checks multiple separated parameters with extra match", function() { - var data = parsePathname("/1/sep/2/foo") - o(compileTemplate("/:id/sep/:name/foo")(data)).equals(true) - o(data.params).deepEquals({id: "1", name: "2"}) - }) - o("checks multiple separated parameters with extra mismatch", function() { - var data = parsePathname("/1/sep/2/bar") - o(compileTemplate("/:id/sep/:name/foo")(data)).equals(false) - o(data.params).deepEquals({}) - }) - o("checks multiple separated parameters, last variadic, with extra match", function() { - var data = parsePathname("/1/sep/some/path/foo") - o(compileTemplate("/:id/sep/:name.../foo")(data)).equals(true) - o(data.params).deepEquals({id: "1", name: "some/path"}) - }) - o("checks multiple separated parameters, last variadic, with extra mismatch", function() { - var data = parsePathname("/1/sep/some/path/bar") - o(compileTemplate("/:id/sep/:name.../foo")(data)).equals(false) - o(data.params).deepEquals({}) - }) - o("checks multiple parameters + prefix", function() { - var data = parsePathname("/route/1/2") - o(compileTemplate("/route/:id/:name")(data)).equals(true) - o(data.params).deepEquals({id: "1", name: "2"}) - }) - o("checks incomplete multiple parameters + prefix", function() { - var data = parsePathname("/route/1") - o(compileTemplate("/route/:id/:name")(data)).equals(false) - o(data.params).deepEquals({}) - }) - o("checks multiple parameters + prefix with extra match", function() { - var data = parsePathname("/route/1/2/foo") - o(compileTemplate("/route/:id/:name/foo")(data)).equals(true) - o(data.params).deepEquals({id: "1", name: "2"}) - }) - o("checks multiple parameters + prefix with extra mismatch", function() { - var data = parsePathname("/route/1/2/bar") - o(compileTemplate("/route/:id/:name/foo")(data)).equals(false) - o(data.params).deepEquals({}) - }) - o("checks multiple parameters + prefix, last variadic, with extra match", function() { - var data = parsePathname("/route/1/some/path/foo") - o(compileTemplate("/route/:id/:name.../foo")(data)).equals(true) - o(data.params).deepEquals({id: "1", name: "some/path"}) - }) - o("checks multiple parameters + prefix, last variadic, with extra mismatch", function() { - var data = parsePathname("/route/1/some/path/bar") - o(compileTemplate("/route/:id/:name.../foo")(data)).equals(false) - o(data.params).deepEquals({}) - }) - o("checks multiple separated parameters + prefix", function() { - var data = parsePathname("/route/1/sep/2") - o(compileTemplate("/route/:id/sep/:name")(data)).equals(true) - o(data.params).deepEquals({id: "1", name: "2"}) - }) - o("checks incomplete multiple separated parameters + prefix", function() { - var data = parsePathname("/route/1") - o(compileTemplate("/route/:id/sep/:name")(data)).equals(false) - o(data.params).deepEquals({}) - var data = parsePathname("/route/1/sep") - o(compileTemplate("/route/:id/sep/:name")(data)).equals(false) - o(data.params).deepEquals({}) - }) - o("checks multiple separated parameters + prefix missing sep", function() { - var data = parsePathname("/route/1/2") - o(compileTemplate("/route/:id/sep/:name")(data)).equals(false) - o(data.params).deepEquals({}) - }) - o("checks multiple separated parameters + prefix with extra match", function() { - var data = parsePathname("/route/1/sep/2/foo") - o(compileTemplate("/route/:id/sep/:name/foo")(data)).equals(true) - o(data.params).deepEquals({id: "1", name: "2"}) - }) - o("checks multiple separated parameters + prefix with extra mismatch", function() { - var data = parsePathname("/route/1/sep/2/bar") - o(compileTemplate("/route/:id/sep/:name/foo")(data)).equals(false) - o(data.params).deepEquals({}) - }) - o("checks multiple separated parameters + prefix, last variadic, with extra match", function() { - var data = parsePathname("/route/1/sep/some/path/foo") - o(compileTemplate("/route/:id/sep/:name.../foo")(data)).equals(true) - o(data.params).deepEquals({id: "1", name: "some/path"}) - }) - o("checks multiple separated parameters + prefix, last variadic, with extra mismatch", function() { - var data = parsePathname("/route/1/sep/some/path/bar") - o(compileTemplate("/route/:id/sep/:name.../foo")(data)).equals(false) - o(data.params).deepEquals({}) - }) - o("checks query params match", function() { - var data = parsePathname("/route/1?foo=bar") - o(compileTemplate("/route/:id?foo=bar")(data)).equals(true) - o(data.params).deepEquals({id: "1", foo: "bar"}) - }) - o("checks query params mismatch", function() { - var data = parsePathname("/route/1?foo=bar") - o(compileTemplate("/route/:id?foo=1")(data)).equals(false) - o(data.params).deepEquals({foo: "bar"}) - o(compileTemplate("/route/:id?bar=foo")(data)).equals(false) - o(data.params).deepEquals({foo: "bar"}) - }) - o("checks dot before dot", function() { - var data = parsePathname("/file.test.png/edit") - o(compileTemplate("/:file.:ext/edit")(data)).equals(true) - o(data.params).deepEquals({file: "file.test", ext: "png"}) - }) - o("checks dash before dot", function() { - var data = parsePathname("/file-test.png/edit") - o(compileTemplate("/:file.:ext/edit")(data)).equals(true) - o(data.params).deepEquals({file: "file-test", ext: "png"}) - }) - o("checks dot before dash", function() { - var data = parsePathname("/file.test-png/edit") - o(compileTemplate("/:file-:ext/edit")(data)).equals(true) - o(data.params).deepEquals({file: "file.test", ext: "png"}) - }) - o("checks dash before dash", function() { - var data = parsePathname("/file-test-png/edit") - o(compileTemplate("/:file-:ext/edit")(data)).equals(true) - o(data.params).deepEquals({file: "file-test", ext: "png"}) - }) -}) diff --git a/pathname/tests/test-parsePathname.js b/pathname/tests/test-parsePathname.js deleted file mode 100644 index 03d7c1b67..000000000 --- a/pathname/tests/test-parsePathname.js +++ /dev/null @@ -1,126 +0,0 @@ -"use strict" - -var o = require("ospec") -var parsePathname = require("../../pathname/parse") - -o.spec("parsePathname", function() { - o("parses empty string", function() { - var data = parsePathname("") - o(data).deepEquals({ - path: "/", - params: {} - }) - }) - o("parses query at start", function() { - var data = parsePathname("?a=b&c=d") - o(data).deepEquals({ - path: "/", - params: {a: "b", c: "d"} - }) - }) - o("ignores hash at start", function() { - var data = parsePathname("#a=b&c=d") - o(data).deepEquals({ - path: "/", - params: {} - }) - }) - o("parses query, ignores hash at start", function() { - var data = parsePathname("?a=1&b=2#c=3&d=4") - o(data).deepEquals({ - path: "/", - params: {a: "1", b: "2"} - }) - }) - o("parses root", function() { - var data = parsePathname("/") - o(data).deepEquals({ - path: "/", - params: {} - }) - }) - o("parses root + query at start", function() { - var data = parsePathname("/?a=b&c=d") - o(data).deepEquals({ - path: "/", - params: {a: "b", c: "d"} - }) - }) - o("parses root, ignores hash at start", function() { - var data = parsePathname("/#a=b&c=d") - o(data).deepEquals({ - path: "/", - params: {} - }) - }) - o("parses root + query, ignores hash at start", function() { - var data = parsePathname("/?a=1&b=2#c=3&d=4") - o(data).deepEquals({ - path: "/", - params: {a: "1", b: "2"} - }) - }) - o("parses route", function() { - var data = parsePathname("/route/foo") - o(data).deepEquals({ - path: "/route/foo", - params: {} - }) - }) - o("parses route + empty query", function() { - var data = parsePathname("/route/foo?") - o(data).deepEquals({ - path: "/route/foo", - params: {} - }) - }) - o("parses route + empty hash", function() { - var data = parsePathname("/route/foo?") - o(data).deepEquals({ - path: "/route/foo", - params: {} - }) - }) - o("parses route + empty query + empty hash", function() { - var data = parsePathname("/route/foo?#") - o(data).deepEquals({ - path: "/route/foo", - params: {} - }) - }) - o("parses route + query", function() { - var data = parsePathname("/route/foo?a=1&b=2") - o(data).deepEquals({ - path: "/route/foo", - params: {a: "1", b: "2"} - }) - }) - o("parses route + hash", function() { - var data = parsePathname("/route/foo?c=3&d=4") - o(data).deepEquals({ - path: "/route/foo", - params: {c: "3", d: "4"} - }) - }) - o("parses route + query, ignores hash", function() { - var data = parsePathname("/route/foo?a=1&b=2#c=3&d=4") - o(data).deepEquals({ - path: "/route/foo", - params: {a: "1", b: "2"} - }) - }) - o("parses route + query, ignores hash with lots of junk slashes", function() { - var data = parsePathname("//route/////foo//?a=1&b=2#c=3&d=4") - o(data).deepEquals({ - path: "/route/foo/", - params: {a: "1", b: "2"} - }) - }) - o("doesn't comprehend protocols", function() { - var data = parsePathname("https://example.com/foo/bar") - o(data).deepEquals({ - path: "/https:/example.com/foo/bar", - params: {} - }) - }) -}) diff --git a/route.js b/route.js index 6d9acd68b..98bd467f3 100644 --- a/route.js +++ b/route.js @@ -2,4 +2,4 @@ var mountRedraw = require("./mount-redraw") -module.exports = require("./api/router")(typeof window !== "undefined" ? window : null, mountRedraw) +module.exports = require("./api/router")(typeof window !== "undefined" ? window : null, mountRedraw.redraw) diff --git a/tests/test-api.js b/tests/test-api.js index bb59365a3..53a63113e 100644 --- a/tests/test-api.js +++ b/tests/test-api.js @@ -2,7 +2,6 @@ var o = require("ospec") var browserMock = require("../test-utils/browserMock") -var components = require("../test-utils/components") o.spec("api", function() { var FRAME_BUDGET = Math.floor(1000 / 60) @@ -105,57 +104,29 @@ o.spec("api", function() { }) }) - components.forEach(function(cmp){ - o.spec(cmp.kind, function(){ - var createComponent = cmp.create - - o.spec("m.route", function() { - o("works", function() { - root = window.document.createElement("div") - m.route(root, "/a", { - "/a": createComponent({view: function() {return m("div")}}) - }) - - return sleep(FRAME_BUDGET + 10).then(() => { - o(root.childNodes.length).equals(1) - o(root.firstChild.nodeName).equals("DIV") - }) - }) - o("m.route.prefix", function() { - root = window.document.createElement("div") - m.route.prefix = "#" - m.route(root, "/a", { - "/a": createComponent({view: function() {return m("div")}}) - }) - - return sleep(FRAME_BUDGET + 10).then(() => { - o(root.childNodes.length).equals(1) - o(root.firstChild.nodeName).equals("DIV") - }) - }) - o("m.route.get", function() { - root = window.document.createElement("div") - m.route(root, "/a", { - "/a": createComponent({view: function() {return m("div")}}) - }) - - return sleep(FRAME_BUDGET + 10).then(() => { - o(m.route.get()).equals("/a") - }) - }) - o("m.route.set", function() { - o.timeout(100) - root = window.document.createElement("div") - m.route(root, "/a", { - "/:id": createComponent({view: function() {return m("div")}}) - }) - - return sleep(FRAME_BUDGET + 10) - .then(() => { m.route.set("/b") }) - .then(() => sleep(FRAME_BUDGET + 10)) - .then(() => { o(m.route.get()).equals("/b") }) - }) + o.spec("m.route", function() { + o("works", function() { + root = window.document.createElement("div") + m.route.init("#") + m.mount(root, () => { + if (m.route.path === "/a") { + return m("div") + } else if (m.route.path === "/b") { + return m("span") + } else { + m.route.set("/a") + } }) + + return sleep(FRAME_BUDGET + 10) + .then(() => { + o(root.childNodes.length).equals(1) + o(root.firstChild.nodeName).equals("DIV") + o(m.route.get()).equals("/a") + }) + .then(() => { m.route.set("/b") }) + .then(() => sleep(FRAME_BUDGET + 10)) + .then(() => { o(m.route.get()).equals("/b") }) }) }) }) From ccea9a18b33fee4db04cec7fdfd5786fd92cf4b3 Mon Sep 17 00:00:00 2001 From: Claudia Meadows Date: Thu, 3 Oct 2024 02:52:16 -0700 Subject: [PATCH 20/95] Merge `Vnode` with the hyperscript module Also, rename it `m` to make dev stack traces a little more readable, and remove a useless layer of indirection. --- index.js | 8 +--- render/hyperscript.js | 56 ++++++++++++++++++++++---- render/render.js | 8 ++-- render/tests/test-normalize.js | 18 ++++----- render/tests/test-normalizeChildren.js | 15 ++++--- render/vnode.js | 40 ------------------ 6 files changed, 69 insertions(+), 76 deletions(-) delete mode 100644 render/vnode.js diff --git a/index.js b/index.js index 0e01ff0cd..1e0c6cd48 100644 --- a/index.js +++ b/index.js @@ -1,13 +1,8 @@ "use strict" -var hyperscript = require("./hyperscript") +var m = require("./hyperscript") var mountRedraw = require("./mount-redraw") -var m = (...args) => hyperscript(...args) -m.m = hyperscript -m.fragment = hyperscript.fragment -m.key = hyperscript.key -m.Fragment = "[" m.mount = mountRedraw.mount m.route = require("./route") m.render = require("./render") @@ -16,7 +11,6 @@ m.parseQueryString = require("./querystring/parse") m.buildQueryString = require("./querystring/build") m.p = require("./pathname/build") m.withProgress = require("./util/with-progress") -m.vnode = require("./render/vnode") m.censor = require("./util/censor") m.lazy = require("./util/lazy")(mountRedraw.redraw) diff --git a/render/hyperscript.js b/render/hyperscript.js index 623bd56b4..48b8baf54 100644 --- a/render/hyperscript.js +++ b/render/hyperscript.js @@ -1,8 +1,11 @@ "use strict" -var Vnode = require("./vnode") var hasOwn = require("../util/hasOwn") +function Vnode(tag, state, attrs, children) { + return {tag, state, attrs, children, dom: undefined, instance: undefined} +} + var selectorParser = /(?:(^|#|\.)([^#\.\[\]]+))|(\[(.+?)(?:\s*=\s*("|'|)((?:\\["'\]]|.)*?)\5)?\])/g var selectorUnescape = /\\(["'\\])/g var selectorCache = /*@__PURE__*/ new Map() @@ -67,7 +70,7 @@ function execSelector(selector, attrs, children) { // Caution is advised when editing this - it's very perf-critical. It's specially designed to avoid // allocations in the fast path, especially with fragments. -function hyperscript(selector, attrs, ...children) { +function m(selector, attrs, ...children) { if (typeof selector !== "string" && typeof selector !== "function") { throw new Error("The selector must be either a string or a component."); } @@ -82,22 +85,59 @@ function hyperscript(selector, attrs, ...children) { if (attrs == null) attrs = {} if (typeof selector === "string") { - children = Vnode.normalizeChildren(children) + children = m.normalizeChildren(children) if (selector !== "[") return execSelector(selector, attrs, children) } return Vnode(selector, {}, attrs, children) } -hyperscript.fragment = function(...args) { - return hyperscript("[", ...args) +m.fragment = function(...args) { + return m("[", ...args) } -hyperscript.key = function(key, ...children) { +m.key = function(key, ...children) { if (children.length === 1 && Array.isArray(children[0])) { children = children[0].slice() } - return Vnode("=", key, undefined, Vnode.normalizeChildren(children)) + return Vnode("=", key, undefined, m.normalizeChildren(children)) +} + +m.normalize = function(node) { + if (node == null || typeof node === "boolean") return null + if (typeof node !== "object") return Vnode("#", undefined, undefined, String(node)) + if (Array.isArray(node)) return Vnode("[", undefined, undefined, m.normalizeChildren(node.slice())) + return node } -module.exports = hyperscript +m.normalizeChildren = function(input) { + if (input.length) { + input[0] = m.normalize(input[0]) + var isKeyed = input[0] != null && input[0].tag === "=" + var keys = new Set() + // Note: this is a *very* perf-sensitive check. + // Fun fact: merging the loop like this is somehow faster than splitting + // it, noticeably so. + for (var i = 1; i < input.length; i++) { + input[i] = m.normalize(input[i]) + if ((input[i] != null && input[i].tag === "=") !== isKeyed) { + throw new TypeError( + isKeyed + ? "In fragments, vnodes must either all have keys or none have keys. You may wish to consider using an explicit empty key vnode, `m.key()`, instead of a hole." + : "In fragments, vnodes must either all have keys or none have keys." + ) + } + if (isKeyed) { + if (keys.has(input[i].state)) { + throw new TypeError(`Duplicate key detected: ${input[i].state}`) + } + keys.add(input[i].state) + } + } + } + return input +} + +m.Fragment = "[" + +module.exports = m diff --git a/render/render.js b/render/render.js index ee42593f7..d2b0f493f 100644 --- a/render/render.js +++ b/render/render.js @@ -1,6 +1,6 @@ "use strict" -var Vnode = require("../render/vnode") +var hyperscript = require("./hyperscript") var xlinkNs = "http://www.w3.org/1999/xlink" var nameSpace = { @@ -120,7 +120,7 @@ function initComponent(vnode, hooks) { vnode.state = (vnode.tag.prototype != null && typeof vnode.tag.prototype.view === "function") ? new vnode.tag(vnode) : vnode.tag(vnode) initLifecycle(vnode.state, vnode, hooks) if (vnode.attrs != null) initLifecycle(vnode.attrs, vnode, hooks) - vnode.instance = Vnode.normalize(callHook.call(vnode.state.view, vnode)) + vnode.instance = hyperscript.normalize(callHook.call(vnode.state.view, vnode)) if (vnode.instance === vnode) throw Error("A view cannot return the vnode it received as argument") reentrantLock.delete(vnode.tag) } @@ -410,7 +410,7 @@ function updateElement(old, vnode, hooks, ns, pathDepth) { } } function updateComponent(parent, old, vnode, hooks, nextSibling, ns, pathDepth) { - vnode.instance = Vnode.normalize(callHook.call(vnode.state.view, vnode)) + vnode.instance = hyperscript.normalize(callHook.call(vnode.state.view, vnode)) if (vnode.instance === vnode) throw Error("A view cannot return the vnode it received as argument") updateLifecycle(vnode.state, vnode, hooks) if (vnode.attrs != null) updateLifecycle(vnode.attrs, vnode, hooks) @@ -924,7 +924,7 @@ module.exports = function(dom, vnodes, redraw) { try { // First time rendering into a node clears it out if (dom.vnodes == null) dom.textContent = "" - vnodes = Vnode.normalizeChildren(Array.isArray(vnodes) ? vnodes : [vnodes]) + vnodes = hyperscript.normalizeChildren(Array.isArray(vnodes) ? vnodes.slice() : [vnodes]) updateNodes(dom, dom.vnodes, vnodes, hooks, null, namespace === "http://www.w3.org/1999/xhtml" ? undefined : namespace, 0) dom.vnodes = vnodes // `document.activeElement` can return null: https://html.spec.whatwg.org/multipage/interaction.html#dom-document-activeelement diff --git a/render/tests/test-normalize.js b/render/tests/test-normalize.js index 3a656dd86..c1a324caf 100644 --- a/render/tests/test-normalize.js +++ b/render/tests/test-normalize.js @@ -1,17 +1,17 @@ "use strict" var o = require("ospec") -var Vnode = require("../../render/vnode") +var m = require("../../render/hyperscript") o.spec("normalize", function() { o("normalizes array into fragment", function() { - var node = Vnode.normalize([]) + var node = m.normalize([]) o(node.tag).equals("[") o(node.children.length).equals(0) }) o("normalizes nested array into fragment", function() { - var node = Vnode.normalize([[]]) + var node = m.normalize([[]]) o(node.tag).equals("[") o(node.children.length).equals(1) @@ -19,36 +19,36 @@ o.spec("normalize", function() { o(node.children[0].children.length).equals(0) }) o("normalizes string into text node", function() { - var node = Vnode.normalize("a") + var node = m.normalize("a") o(node.tag).equals("#") o(node.children).equals("a") }) o("normalizes falsy string into text node", function() { - var node = Vnode.normalize("") + var node = m.normalize("") o(node.tag).equals("#") o(node.children).equals("") }) o("normalizes number into text node", function() { - var node = Vnode.normalize(1) + var node = m.normalize(1) o(node.tag).equals("#") o(node.children).equals("1") }) o("normalizes falsy number into text node", function() { - var node = Vnode.normalize(0) + var node = m.normalize(0) o(node.tag).equals("#") o(node.children).equals("0") }) o("normalizes `true` to `null`", function() { - var node = Vnode.normalize(true) + var node = m.normalize(true) o(node).equals(null) }) o("normalizes `false` to `null`", function() { - var node = Vnode.normalize(false) + var node = m.normalize(false) o(node).equals(null) }) diff --git a/render/tests/test-normalizeChildren.js b/render/tests/test-normalizeChildren.js index 8afe9c7ce..2daf4ba18 100644 --- a/render/tests/test-normalizeChildren.js +++ b/render/tests/test-normalizeChildren.js @@ -1,29 +1,28 @@ "use strict" var o = require("ospec") -var Vnode = require("../../render/vnode") var m = require("../../render/hyperscript") o.spec("normalizeChildren", function() { o("normalizes arrays into fragments", function() { - var children = Vnode.normalizeChildren([[]]) + var children = m.normalizeChildren([[]]) o(children[0].tag).equals("[") o(children[0].children.length).equals(0) }) o("normalizes strings into text nodes", function() { - var children = Vnode.normalizeChildren(["a"]) + var children = m.normalizeChildren(["a"]) o(children[0].tag).equals("#") o(children[0].children).equals("a") }) o("normalizes `false` values into `null`s", function() { - var children = Vnode.normalizeChildren([false]) + var children = m.normalizeChildren([false]) o(children[0]).equals(null) }) o("allows all keys", function() { - var children = Vnode.normalizeChildren([ + var children = m.normalizeChildren([ m.key(1), m.key(2), ]) @@ -31,7 +30,7 @@ o.spec("normalizeChildren", function() { o(children).deepEquals([m.key(1), m.key(2)]) }) o("allows no keys", function() { - var children = Vnode.normalizeChildren([ + var children = m.normalizeChildren([ m("foo1"), m("foo2"), ]) @@ -40,7 +39,7 @@ o.spec("normalizeChildren", function() { }) o("disallows mixed keys, starting with key", function() { o(function() { - Vnode.normalizeChildren([ + m.normalizeChildren([ m.key(1), m("foo2"), ]) @@ -48,7 +47,7 @@ o.spec("normalizeChildren", function() { }) o("disallows mixed keys, starting with no key", function() { o(function() { - Vnode.normalizeChildren([ + m.normalizeChildren([ m("foo1"), m.key(2), ]) diff --git a/render/vnode.js b/render/vnode.js deleted file mode 100644 index ed9fce9a2..000000000 --- a/render/vnode.js +++ /dev/null @@ -1,40 +0,0 @@ -"use strict" - -function Vnode(tag, state, attrs, children) { - return {tag, state, attrs, children, dom: undefined, instance: undefined} -} -Vnode.normalize = function(node) { - if (node == null || typeof node === "boolean") return null - if (typeof node !== "object") return Vnode("#", undefined, undefined, String(node)) - if (Array.isArray(node)) return Vnode("[", undefined, undefined, Vnode.normalizeChildren(node.slice())) - return node -} -Vnode.normalizeChildren = function(input) { - if (input.length) { - input[0] = Vnode.normalize(input[0]) - var isKeyed = input[0] != null && input[0].tag === "=" - var keys = new Set() - // Note: this is a *very* perf-sensitive check. - // Fun fact: merging the loop like this is somehow faster than splitting - // it, noticeably so. - for (var i = 1; i < input.length; i++) { - input[i] = Vnode.normalize(input[i]) - if ((input[i] != null && input[i].tag === "=") !== isKeyed) { - throw new TypeError( - isKeyed - ? "In fragments, vnodes must either all have keys or none have keys. You may wish to consider using an explicit empty key vnode, `m.key()`, instead of a hole." - : "In fragments, vnodes must either all have keys or none have keys." - ) - } - if (isKeyed) { - if (keys.has(input[i].state)) { - throw new TypeError(`Duplicate key detected: ${input[i].state}`) - } - keys.add(input[i].state) - } - } - } - return input -} - -module.exports = Vnode From dae22084fffa954c89d5f6434d084988424c4fbb Mon Sep 17 00:00:00 2001 From: Claudia Meadows Date: Thu, 3 Oct 2024 02:54:20 -0700 Subject: [PATCH 21/95] Rename and move lone remaining `pathname` submodule to make more sense --- index.js | 2 +- pathname/build.js => util/p.js | 3 ++- pathname/tests/test-build.js => util/tests/test-p.js | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) rename pathname/build.js => util/p.js (96%) rename pathname/tests/test-build.js => util/tests/test-p.js (99%) diff --git a/index.js b/index.js index 1e0c6cd48..3ac2cb27a 100644 --- a/index.js +++ b/index.js @@ -9,7 +9,7 @@ m.render = require("./render") m.redraw = mountRedraw.redraw m.parseQueryString = require("./querystring/parse") m.buildQueryString = require("./querystring/build") -m.p = require("./pathname/build") +m.p = require("./util/p") m.withProgress = require("./util/with-progress") m.censor = require("./util/censor") m.lazy = require("./util/lazy")(mountRedraw.redraw) diff --git a/pathname/build.js b/util/p.js similarity index 96% rename from pathname/build.js rename to util/p.js index 3cd033c3e..d29c7a1fe 100644 --- a/pathname/build.js +++ b/util/p.js @@ -1,6 +1,7 @@ "use strict" var buildQueryString = require("../querystring/build") +var assign = require("./assign") // Returns `path` from `template` + `params` module.exports = function(template, params) { @@ -15,7 +16,7 @@ module.exports = function(template, params) { var path = template.slice(0, pathEnd) var query = {} - Object.assign(query, params) + assign(query, params) var resolved = path.replace(/:([^\/\.-]+)(\.{3})?/g, function(m, key, variadic) { delete query[key] diff --git a/pathname/tests/test-build.js b/util/tests/test-p.js similarity index 99% rename from pathname/tests/test-build.js rename to util/tests/test-p.js index fb0c18d7c..693740204 100644 --- a/pathname/tests/test-build.js +++ b/util/tests/test-p.js @@ -1,7 +1,7 @@ "use strict" var o = require("ospec") -var p = require("../build") +var p = require("../p") o.spec("p", function() { function test(prefix) { From 3668dc70fe77247fad0167da749484b7ba09f355 Mon Sep 17 00:00:00 2001 From: Claudia Meadows Date: Thu, 3 Oct 2024 02:55:47 -0700 Subject: [PATCH 22/95] Drop remaining reference to `assign` --- util/p.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/util/p.js b/util/p.js index d29c7a1fe..23b81e977 100644 --- a/util/p.js +++ b/util/p.js @@ -1,7 +1,6 @@ "use strict" var buildQueryString = require("../querystring/build") -var assign = require("./assign") // Returns `path` from `template` + `params` module.exports = function(template, params) { @@ -14,9 +13,7 @@ module.exports = function(template, params) { var queryEnd = hashIndex < 0 ? template.length : hashIndex var pathEnd = queryIndex < 0 ? queryEnd : queryIndex var path = template.slice(0, pathEnd) - var query = {} - - assign(query, params) + var query = Object.assign({}, params) var resolved = path.replace(/:([^\/\.-]+)(\.{3})?/g, function(m, key, variadic) { delete query[key] From 03fd6404390cfe0bfa2fa88362336a021c7cf7b6 Mon Sep 17 00:00:00 2001 From: Claudia Meadows Date: Thu, 3 Oct 2024 03:21:48 -0700 Subject: [PATCH 23/95] Merge query string building into `m.p`, drop unused query string parser --- index.js | 2 - querystring/build.js | 26 ----- querystring/parse.js | 51 --------- querystring/tests/test-buildQueryString.js | 87 -------------- querystring/tests/test-parseQueryString.js | 119 ------------------- test-utils/xhrMock.js | 4 +- tests/test-api.js | 13 +-- util/p.js | 24 +++- util/tests/test-p.js | 127 +++++++++++++++++---- 9 files changed, 129 insertions(+), 324 deletions(-) delete mode 100644 querystring/build.js delete mode 100644 querystring/parse.js delete mode 100644 querystring/tests/test-buildQueryString.js delete mode 100644 querystring/tests/test-parseQueryString.js diff --git a/index.js b/index.js index 3ac2cb27a..a37980270 100644 --- a/index.js +++ b/index.js @@ -7,8 +7,6 @@ m.mount = mountRedraw.mount m.route = require("./route") m.render = require("./render") m.redraw = mountRedraw.redraw -m.parseQueryString = require("./querystring/parse") -m.buildQueryString = require("./querystring/build") m.p = require("./util/p") m.withProgress = require("./util/with-progress") m.censor = require("./util/censor") diff --git a/querystring/build.js b/querystring/build.js deleted file mode 100644 index 041249565..000000000 --- a/querystring/build.js +++ /dev/null @@ -1,26 +0,0 @@ -"use strict" - -module.exports = function(object) { - if (Object.prototype.toString.call(object) !== "[object Object]") return "" - - var args = [] - for (var key in object) { - destructure(key, object[key]) - } - - return args.join("&") - - function destructure(key, value) { - if (Array.isArray(value)) { - for (var i = 0; i < value.length; i++) { - destructure(key + "[" + i + "]", value[i]) - } - } - else if (Object.prototype.toString.call(value) === "[object Object]") { - for (var i in value) { - destructure(key + "[" + i + "]", value[i]) - } - } - else args.push(encodeURIComponent(key) + (value != null && value !== "" ? "=" + encodeURIComponent(value) : "")) - } -} diff --git a/querystring/parse.js b/querystring/parse.js deleted file mode 100644 index 1f2300a28..000000000 --- a/querystring/parse.js +++ /dev/null @@ -1,51 +0,0 @@ -"use strict" - -function decodeURIComponentSave(str) { - try { - return decodeURIComponent(str) - } catch(err) { - return str - } -} - -module.exports = function(string) { - if (string === "" || string == null) return {} - if (string.charAt(0) === "?") string = string.slice(1) - - var entries = string.split("&"), counters = {}, data = {} - for (var i = 0; i < entries.length; i++) { - var entry = entries[i].split("=") - var key = decodeURIComponentSave(entry[0]) - var value = entry.length === 2 ? decodeURIComponentSave(entry[1]) : "" - - if (value === "true") value = true - else if (value === "false") value = false - - var levels = key.split(/\]\[?|\[/) - var cursor = data - if (key.indexOf("[") > -1) levels.pop() - for (var j = 0; j < levels.length; j++) { - var level = levels[j], nextLevel = levels[j + 1] - var isNumber = nextLevel == "" || !isNaN(parseInt(nextLevel, 10)) - if (level === "") { - var key = levels.slice(0, j).join() - if (counters[key] == null) { - counters[key] = Array.isArray(cursor) ? cursor.length : 0 - } - level = counters[key]++ - } - // Disallow direct prototype pollution - else if (level === "__proto__") break - if (j === levels.length - 1) cursor[level] = value - else { - // Read own properties exclusively to disallow indirect - // prototype pollution - var desc = Object.getOwnPropertyDescriptor(cursor, level) - if (desc != null) desc = desc.value - if (desc == null) cursor[level] = desc = isNumber ? [] : {} - cursor = desc - } - } - } - return data -} diff --git a/querystring/tests/test-buildQueryString.js b/querystring/tests/test-buildQueryString.js deleted file mode 100644 index 9b5149af5..000000000 --- a/querystring/tests/test-buildQueryString.js +++ /dev/null @@ -1,87 +0,0 @@ -"use strict" - -var o = require("ospec") -var buildQueryString = require("../../querystring/build") - -o.spec("buildQueryString", function() { - o("handles flat object", function() { - var string = buildQueryString({a: "b", c: 1}) - - o(string).equals("a=b&c=1") - }) - o("handles escaped values", function() { - var data = buildQueryString({";:@&=+$,/?%#": ";:@&=+$,/?%#"}) - - o(data).equals("%3B%3A%40%26%3D%2B%24%2C%2F%3F%25%23=%3B%3A%40%26%3D%2B%24%2C%2F%3F%25%23") - }) - o("handles unicode", function() { - var data = buildQueryString({"ö": "ö"}) - - o(data).equals("%C3%B6=%C3%B6") - }) - o("handles nested object", function() { - var string = buildQueryString({a: {b: 1, c: 2}}) - - o(string).equals("a%5Bb%5D=1&a%5Bc%5D=2") - }) - o("handles deep nested object", function() { - var string = buildQueryString({a: {b: {c: 1, d: 2}}}) - - o(string).equals("a%5Bb%5D%5Bc%5D=1&a%5Bb%5D%5Bd%5D=2") - }) - o("handles nested array", function() { - var string = buildQueryString({a: ["x", "y"]}) - - o(string).equals("a%5B0%5D=x&a%5B1%5D=y") - }) - o("handles array w/ dupe values", function() { - var string = buildQueryString({a: ["x", "x"]}) - - o(string).equals("a%5B0%5D=x&a%5B1%5D=x") - }) - o("handles deep nested array", function() { - var string = buildQueryString({a: [["x", "y"]]}) - - o(string).equals("a%5B0%5D%5B0%5D=x&a%5B0%5D%5B1%5D=y") - }) - o("handles deep nested array in object", function() { - var string = buildQueryString({a: {b: ["x", "y"]}}) - - o(string).equals("a%5Bb%5D%5B0%5D=x&a%5Bb%5D%5B1%5D=y") - }) - o("handles deep nested object in array", function() { - var string = buildQueryString({a: [{b: 1, c: 2}]}) - - o(string).equals("a%5B0%5D%5Bb%5D=1&a%5B0%5D%5Bc%5D=2") - }) - o("handles date", function() { - var string = buildQueryString({a: new Date(0)}) - - o(string).equals("a=" + encodeURIComponent(new Date(0).toString())) - }) - o("turns null into value-less string (like jQuery)", function() { - var string = buildQueryString({a: null}) - - o(string).equals("a") - }) - o("turns undefined into value-less string (like jQuery)", function() { - var string = buildQueryString({a: undefined}) - - o(string).equals("a") - }) - o("turns empty string into value-less string (like jQuery)", function() { - var string = buildQueryString({a: ""}) - - o(string).equals("a") - }) - o("handles zero", function() { - var string = buildQueryString({a: 0}) - - o(string).equals("a=0") - }) - o("handles false", function() { - var string = buildQueryString({a: false}) - - o(string).equals("a=false") - }) -}) diff --git a/querystring/tests/test-parseQueryString.js b/querystring/tests/test-parseQueryString.js deleted file mode 100644 index 8d497cdb6..000000000 --- a/querystring/tests/test-parseQueryString.js +++ /dev/null @@ -1,119 +0,0 @@ -"use strict" - -var o = require("ospec") -var parseQueryString = require("../../querystring/parse") - -o.spec("parseQueryString", function() { - o("works", function() { - var data = parseQueryString("?aaa=bbb") - o(data).deepEquals({aaa: "bbb"}) - }) - o("parses empty string", function() { - var data = parseQueryString("") - o(data).deepEquals({}) - }) - o("parses flat object", function() { - var data = parseQueryString("?a=b&c=d") - o(data).deepEquals({a: "b", c: "d"}) - }) - o("handles escaped values", function() { - var data = parseQueryString("?%3B%3A%40%26%3D%2B%24%2C%2F%3F%25%23=%3B%3A%40%26%3D%2B%24%2C%2F%3F%25%23") - o(data).deepEquals({";:@&=+$,/?%#": ";:@&=+$,/?%#"}) - }) - o("handles wrongly escaped values", function() { - var data = parseQueryString("?test=%c5%a1%e8ZM%80%82H") - o(data).deepEquals({test: "%c5%a1%e8ZM%80%82H"}) - }) - o("handles escaped slashes followed by a number", function () { - var data = parseQueryString("?hello=%2Fen%2F1") - o(data.hello).equals("/en/1") - }) - o("handles escaped square brackets", function() { - var data = parseQueryString("?a%5B%5D=b") - o(data).deepEquals({"a": ["b"]}) - }) - o("handles escaped unicode", function() { - var data = parseQueryString("?%C3%B6=%C3%B6") - o(data).deepEquals({"ö": "ö"}) - }) - o("handles unicode", function() { - var data = parseQueryString("?ö=ö") - o(data).deepEquals({"ö": "ö"}) - }) - o("parses without question mark", function() { - var data = parseQueryString("a=b&c=d") - o(data).deepEquals({a: "b", c: "d"}) - }) - o("parses nested object", function() { - var data = parseQueryString("a[b]=x&a[c]=y") - o(data).deepEquals({a: {b: "x", c: "y"}}) - }) - o("parses deep nested object", function() { - var data = parseQueryString("a[b][c]=x&a[b][d]=y") - o(data).deepEquals({a: {b: {c: "x", d: "y"}}}) - }) - o("parses nested array", function() { - var data = parseQueryString("a[0]=x&a[1]=y") - o(data).deepEquals({a: ["x", "y"]}) - }) - o("parses deep nested array", function() { - var data = parseQueryString("a[0][0]=x&a[0][1]=y") - o(data).deepEquals({a: [["x", "y"]]}) - }) - o("parses deep nested object in array", function() { - var data = parseQueryString("a[0][c]=x&a[0][d]=y") - o(data).deepEquals({a: [{c: "x", d: "y"}]}) - }) - o("parses deep nested array in object", function() { - var data = parseQueryString("a[b][0]=x&a[b][1]=y") - o(data).deepEquals({a: {b: ["x", "y"]}}) - }) - o("parses array without index", function() { - var data = parseQueryString("a[]=x&a[]=y&b[]=w&b[]=z") - o(data).deepEquals({a: ["x", "y"], b: ["w", "z"]}) - }) - o("casts booleans", function() { - var data = parseQueryString("a=true&b=false") - o(data).deepEquals({a: true, b: false}) - }) - o("does not cast numbers", function() { - var data = parseQueryString("a=1&b=-2.3&c=0x10&d=1e2&e=Infinity") - o(data).deepEquals({a: "1", b: "-2.3", c: "0x10", d: "1e2", e: "Infinity"}) - }) - o("does not cast NaN", function() { - var data = parseQueryString("a=NaN") - o(data.a).equals("NaN") - }) - o("does not casts Date", function() { - var data = parseQueryString("a=1970-01-01") - o(typeof data.a).equals("string") - o(data.a).equals("1970-01-01") - }) - o("does not cast empty string to number", function() { - var data = parseQueryString("a=") - o(data).deepEquals({a: ""}) - }) - o("does not cast void to number", function() { - var data = parseQueryString("a") - o(data).deepEquals({a: ""}) - }) - o("prefers later values", function() { - var data = parseQueryString("a=1&b=2&a=3") - o(data).deepEquals({a: "3", b: "2"}) - }) - o("doesn't pollute prototype directly, censors `__proto__`", function() { - var prev = Object.prototype.toString - var data = parseQueryString("a=b&__proto__%5BtoString%5D=123") - o(Object.prototype.toString).equals(prev) - o(data).deepEquals({a: "b"}) - }) - o("doesn't pollute prototype indirectly, retains `constructor`", function() { - var prev = Object.prototype.toString - var data = parseQueryString("a=b&constructor%5Bprototype%5D%5BtoString%5D=123") - o(Object.prototype.toString).equals(prev) - // The deep matcher is borked here. - o(Object.keys(data)).deepEquals(["a", "constructor"]) - o(data.a).equals("b") - o(data.constructor).deepEquals({prototype: {toString: "123"}}) - }) -}) diff --git a/test-utils/xhrMock.js b/test-utils/xhrMock.js index bd42c5b70..0f0177438 100644 --- a/test-utils/xhrMock.js +++ b/test-utils/xhrMock.js @@ -2,7 +2,6 @@ var callAsync = require("../test-utils/callAsync") var parseURL = require("../test-utils/parseURL") -var parseQueryString = require("../querystring/parse") module.exports = function() { var routes = {} @@ -57,7 +56,7 @@ module.exports = function() { }}) this.send = function(body) { var self = this - + var completeResponse = function (data) { self._responseCompleted = true if(!aborted) { @@ -118,7 +117,6 @@ module.exports = function() { var urlData = parseURL(element.src, {protocol: "http:", hostname: "localhost", port: "", pathname: "/"}) var handler = routes["GET " + urlData.pathname] || serverErrorHandler.bind(null, element.src) var data = handler({url: urlData.pathname, query: urlData.search, body: null}) - parseQueryString(urlData.search) callAsync(function() { if (data.status === 200) { new Function("$window", "with ($window) return " + data.responseText).call($window, $window) diff --git a/tests/test-api.js b/tests/test-api.js index 53a63113e..8df62890e 100644 --- a/tests/test-api.js +++ b/tests/test-api.js @@ -48,18 +48,11 @@ o.spec("api", function() { o(vnode.children[0].tag).equals("div") }) }) - o.spec("m.parseQueryString", function() { + o.spec("m.p", function() { o("works", function() { - var query = m.parseQueryString("?a=1&b=2") + var query = m.p("/foo/:c", {a: 1, b: 2, c: 3}) - o(query).deepEquals({a: "1", b: "2"}) - }) - }) - o.spec("m.buildQueryString", function() { - o("works", function() { - var query = m.buildQueryString({a: 1, b: 2}) - - o(query).equals("a=1&b=2") + o(query).equals("/foo/3?a=1&b=2") }) }) o.spec("m.render", function() { diff --git a/util/p.js b/util/p.js index 23b81e977..a3ded987e 100644 --- a/util/p.js +++ b/util/p.js @@ -1,10 +1,24 @@ "use strict" -var buildQueryString = require("../querystring/build") +var toString = {}.toString + +var serializeQueryValue = (key, value) => { + if (value == null || value === false) { + return "" + } else if (Array.isArray(value)) { + return value.map((i) => serializeQueryValue(`${key}[]`, i)).join("&") + } else if (toString.call(value) !== "[object Object]") { + return `${encodeURIComponent(key)}${value === true ? "" : `=${encodeURIComponent(value)}`}` + } else { + return Object.entries(value).map(([k, v]) => serializeQueryValue(`${key}[${k}]`, v)).join("&") + } +} + +var invalidTemplateChars = /:([^\/\.-]+)(\.{3})?:/ // Returns `path` from `template` + `params` -module.exports = function(template, params) { - if ((/:([^\/\.-]+)(\.{3})?:/).test(template)) { +module.exports = (template, params) => { + if (invalidTemplateChars.test(template)) { throw new SyntaxError("Template parameter names must be separated by either a '/', '-', or '.'.") } if (params == null) return template @@ -15,7 +29,7 @@ module.exports = function(template, params) { var path = template.slice(0, pathEnd) var query = Object.assign({}, params) - var resolved = path.replace(/:([^\/\.-]+)(\.{3})?/g, function(m, key, variadic) { + var resolved = path.replace(/:([^\/\.-]+)(\.{3})?/g, (m, key, variadic) => { delete query[key] // If no such parameter exists, don't interpolate it. if (params[key] == null) return m @@ -32,7 +46,7 @@ module.exports = function(template, params) { if (queryIndex >= 0) result += template.slice(queryIndex, queryEnd) if (newQueryIndex >= 0) result += (queryIndex < 0 ? "?" : "&") + resolved.slice(newQueryIndex, newQueryEnd) - var querystring = buildQueryString(query) + var querystring = Object.entries(query).map(([k, v]) => serializeQueryValue(k, v)).join("&") if (querystring) result += (queryIndex < 0 && newQueryIndex < 0 ? "?" : "&") + querystring if (hashIndex >= 0) result += template.slice(hashIndex) if (newHashIndex >= 0) result += (hashIndex < 0 ? "" : "&") + resolved.slice(newHashIndex) diff --git a/util/tests/test-p.js b/util/tests/test-p.js index 693740204..f873358d4 100644 --- a/util/tests/test-p.js +++ b/util/tests/test-p.js @@ -8,82 +8,82 @@ o.spec("p", function() { o("returns path if no params", function () { var string = p(prefix + "/route/foo", undefined) - o(string).equals(prefix + "/route/foo") + o(string).equals(`${prefix}/route/foo`) }) o("skips interpolation if no params", function () { var string = p(prefix + "/route/:id", undefined) - o(string).equals(prefix + "/route/:id") + o(string).equals(`${prefix}/route/:id`) }) o("appends query strings", function () { var string = p(prefix + "/route/foo", {a: "b", c: 1}) - o(string).equals(prefix + "/route/foo?a=b&c=1") + o(string).equals(`${prefix}/route/foo?a=b&c=1`) }) o("inserts template parameters at end", function () { var string = p(prefix + "/route/:id", {id: "1"}) - o(string).equals(prefix + "/route/1") + o(string).equals(`${prefix}/route/1`) }) o("inserts template parameters at beginning", function () { var string = p(prefix + "/:id/foo", {id: "1"}) - o(string).equals(prefix + "/1/foo") + o(string).equals(`${prefix}/1/foo`) }) o("inserts template parameters at middle", function () { var string = p(prefix + "/route/:id/foo", {id: "1"}) - o(string).equals(prefix + "/route/1/foo") + o(string).equals(`${prefix}/route/1/foo`) }) o("inserts variadic paths", function () { var string = p(prefix + "/route/:foo...", {foo: "id/1"}) - o(string).equals(prefix + "/route/id/1") + o(string).equals(`${prefix}/route/id/1`) }) o("inserts variadic paths with initial slashes", function () { var string = p(prefix + "/route/:foo...", {foo: "/id/1"}) - o(string).equals(prefix + "/route//id/1") + o(string).equals(`${prefix}/route//id/1`) }) o("skips template parameters at end if param missing", function () { var string = p(prefix + "/route/:id", {param: 1}) - o(string).equals(prefix + "/route/:id?param=1") + o(string).equals(`${prefix}/route/:id?param=1`) }) o("skips template parameters at beginning if param missing", function () { var string = p(prefix + "/:id/foo", {param: 1}) - o(string).equals(prefix + "/:id/foo?param=1") + o(string).equals(`${prefix}/:id/foo?param=1`) }) o("skips template parameters at middle if param missing", function () { var string = p(prefix + "/route/:id/foo", {param: 1}) - o(string).equals(prefix + "/route/:id/foo?param=1") + o(string).equals(`${prefix}/route/:id/foo?param=1`) }) o("skips variadic template parameters if param missing", function () { var string = p(prefix + "/route/:foo...", {param: "/id/1"}) - o(string).equals(prefix + "/route/:foo...?param=%2Fid%2F1") + o(string).equals(`${prefix}/route/:foo...?param=%2Fid%2F1`) }) o("handles escaped values", function() { var data = p(prefix + "/route/:foo", {"foo": ";:@&=+$,/?%#"}) - o(data).equals(prefix + "/route/%3B%3A%40%26%3D%2B%24%2C%2F%3F%25%23") + o(data).equals(`${prefix}/route/%3B%3A%40%26%3D%2B%24%2C%2F%3F%25%23`) }) o("handles unicode", function() { var data = p(prefix + "/route/:ö", {"ö": "ö"}) - o(data).equals(prefix + "/route/%C3%B6") + o(data).equals(`${prefix}/route/%C3%B6`) }) o("handles zero", function() { var string = p(prefix + "/route/:a", {a: 0}) - o(string).equals(prefix + "/route/0") + o(string).equals(`${prefix}/route/0`) }) o("handles false", function() { var string = p(prefix + "/route/:a", {a: false}) - o(string).equals(prefix + "/route/false") + o(string).equals(`${prefix}/route/false`) }) o("handles dashes", function() { var string = p(prefix + "/:lang-:region/route", { @@ -91,7 +91,7 @@ o.spec("p", function() { region: "US" }) - o(string).equals(prefix + "/en-US/route") + o(string).equals(`${prefix}/en-US/route`) }) o("handles dots", function() { var string = p(prefix + "/:file.:ext/view", { @@ -99,22 +99,107 @@ o.spec("p", function() { ext: "png" }) - o(string).equals(prefix + "/image.png/view") + o(string).equals(`${prefix}/image.png/view`) }) o("merges query strings", function() { var string = p(prefix + "/item?a=1&b=2", {c: 3}) - o(string).equals(prefix + "/item?a=1&b=2&c=3") + o(string).equals(`${prefix}/item?a=1&b=2&c=3`) }) o("merges query strings with other parameters", function() { var string = p(prefix + "/item/:id?a=1&b=2", {id: "foo", c: 3}) - o(string).equals(prefix + "/item/foo?a=1&b=2&c=3") + o(string).equals(`${prefix}/item/foo?a=1&b=2&c=3`) }) o("consumes template parameters without modifying query string", function() { var string = p(prefix + "/item/:id?a=1&b=2", {id: "foo"}) - o(string).equals(prefix + "/item/foo?a=1&b=2") + o(string).equals(`${prefix}/item/foo?a=1&b=2`) + }) + o("handles flat object in query string", () => { + var string = p(prefix, {a: "b", c: 1}) + + o(string).equals(`${prefix}?a=b&c=1`) + }) + o("handles escaped values in query string", () => { + var data = p(prefix, {";:@&=+$,/?%#": ";:@&=+$,/?%#"}) + + o(data).equals(`${prefix}?%3B%3A%40%26%3D%2B%24%2C%2F%3F%25%23=%3B%3A%40%26%3D%2B%24%2C%2F%3F%25%23`) + }) + o("handles unicode in query string", () => { + var data = p(prefix, {"ö": "ö"}) + + o(data).equals(`${prefix}?%C3%B6=%C3%B6`) + }) + o("handles nested object in query string", () => { + var string = p(prefix, {a: {b: 1, c: 2}}) + + o(string).equals(`${prefix}?a%5Bb%5D=1&a%5Bc%5D=2`) + }) + o("handles deep nested object in query string", () => { + var string = p(prefix, {a: {b: {c: 1, d: 2}}}) + + o(string).equals(`${prefix}?a%5Bb%5D%5Bc%5D=1&a%5Bb%5D%5Bd%5D=2`) + }) + o("handles nested array in query string", () => { + var string = p(prefix, {a: ["x", "y"]}) + + o(string).equals(`${prefix}?a%5B%5D=x&a%5B%5D=y`) + }) + o("handles array w/ dupe values in query string", () => { + var string = p(prefix, {a: ["x", "x"]}) + + o(string).equals(`${prefix}?a%5B%5D=x&a%5B%5D=x`) + }) + o("handles deep nested array in query string", () => { + var string = p(prefix, {a: [["x", "y"]]}) + + o(string).equals(`${prefix}?a%5B%5D%5B%5D=x&a%5B%5D%5B%5D=y`) + }) + o("handles deep nested array in object in query string", () => { + var string = p(prefix, {a: {b: ["x", "y"]}}) + + o(string).equals(`${prefix}?a%5Bb%5D%5B%5D=x&a%5Bb%5D%5B%5D=y`) + }) + o("handles deep nested object in array in query string", () => { + var string = p(prefix, {a: [{b: 1, c: 2}]}) + + o(string).equals(`${prefix}?a%5B%5D%5Bb%5D=1&a%5B%5D%5Bc%5D=2`) + }) + o("handles date in query string", () => { + var string = p(prefix, {a: new Date(0)}) + + o(string).equals(`${prefix}?a=${encodeURIComponent(new Date(0).toString())}`) + }) + o("handles zero in query string", () => { + var string = p(prefix, {a: 0}) + + o(string).equals(`${prefix}?a=0`) + }) + o("retains empty string literally", () => { + var string = p(prefix, {a: ""}) + + o(string).equals(`${prefix}?a=`) + }) + o("drops `null` from query string", () => { + var string = p(prefix, {a: null}) + + o(string).equals(prefix) + }) + o("drops `undefined` from query string", () => { + var string = p(prefix, {a: undefined}) + + o(string).equals(prefix) + }) + o("turns `true` into value-less string in query string", () => { + var string = p(prefix, {a: true}) + + o(string).equals(`${prefix}?a`) + }) + o("drops `false` from query string", () => { + var string = p(prefix, {a: false}) + + o(string).equals(prefix) }) } o.spec("absolute", function() { test("") }) From eb0031cef5e3eaadb67a02de369520adf8258d55 Mon Sep 17 00:00:00 2001 From: Claudia Meadows Date: Thu, 3 Oct 2024 03:25:01 -0700 Subject: [PATCH 24/95] Normalize the callback style in `test-p.js`. --- util/tests/test-p.js | 52 ++++++++++++++++++++++---------------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/util/tests/test-p.js b/util/tests/test-p.js index f873358d4..b5c063b97 100644 --- a/util/tests/test-p.js +++ b/util/tests/test-p.js @@ -3,89 +3,89 @@ var o = require("ospec") var p = require("../p") -o.spec("p", function() { +o.spec("p", () => { function test(prefix) { - o("returns path if no params", function () { + o("returns path if no params", () => { var string = p(prefix + "/route/foo", undefined) o(string).equals(`${prefix}/route/foo`) }) - o("skips interpolation if no params", function () { + o("skips interpolation if no params", () => { var string = p(prefix + "/route/:id", undefined) o(string).equals(`${prefix}/route/:id`) }) - o("appends query strings", function () { + o("appends query strings", () => { var string = p(prefix + "/route/foo", {a: "b", c: 1}) o(string).equals(`${prefix}/route/foo?a=b&c=1`) }) - o("inserts template parameters at end", function () { + o("inserts template parameters at end", () => { var string = p(prefix + "/route/:id", {id: "1"}) o(string).equals(`${prefix}/route/1`) }) - o("inserts template parameters at beginning", function () { + o("inserts template parameters at beginning", () => { var string = p(prefix + "/:id/foo", {id: "1"}) o(string).equals(`${prefix}/1/foo`) }) - o("inserts template parameters at middle", function () { + o("inserts template parameters at middle", () => { var string = p(prefix + "/route/:id/foo", {id: "1"}) o(string).equals(`${prefix}/route/1/foo`) }) - o("inserts variadic paths", function () { + o("inserts variadic paths", () => { var string = p(prefix + "/route/:foo...", {foo: "id/1"}) o(string).equals(`${prefix}/route/id/1`) }) - o("inserts variadic paths with initial slashes", function () { + o("inserts variadic paths with initial slashes", () => { var string = p(prefix + "/route/:foo...", {foo: "/id/1"}) o(string).equals(`${prefix}/route//id/1`) }) - o("skips template parameters at end if param missing", function () { + o("skips template parameters at end if param missing", () => { var string = p(prefix + "/route/:id", {param: 1}) o(string).equals(`${prefix}/route/:id?param=1`) }) - o("skips template parameters at beginning if param missing", function () { + o("skips template parameters at beginning if param missing", () => { var string = p(prefix + "/:id/foo", {param: 1}) o(string).equals(`${prefix}/:id/foo?param=1`) }) - o("skips template parameters at middle if param missing", function () { + o("skips template parameters at middle if param missing", () => { var string = p(prefix + "/route/:id/foo", {param: 1}) o(string).equals(`${prefix}/route/:id/foo?param=1`) }) - o("skips variadic template parameters if param missing", function () { + o("skips variadic template parameters if param missing", () => { var string = p(prefix + "/route/:foo...", {param: "/id/1"}) o(string).equals(`${prefix}/route/:foo...?param=%2Fid%2F1`) }) - o("handles escaped values", function() { + o("handles escaped values", () => { var data = p(prefix + "/route/:foo", {"foo": ";:@&=+$,/?%#"}) o(data).equals(`${prefix}/route/%3B%3A%40%26%3D%2B%24%2C%2F%3F%25%23`) }) - o("handles unicode", function() { + o("handles unicode", () => { var data = p(prefix + "/route/:ö", {"ö": "ö"}) o(data).equals(`${prefix}/route/%C3%B6`) }) - o("handles zero", function() { + o("handles zero", () => { var string = p(prefix + "/route/:a", {a: 0}) o(string).equals(`${prefix}/route/0`) }) - o("handles false", function() { + o("handles false", () => { var string = p(prefix + "/route/:a", {a: false}) o(string).equals(`${prefix}/route/false`) }) - o("handles dashes", function() { + o("handles dashes", () => { var string = p(prefix + "/:lang-:region/route", { lang: "en", region: "US" @@ -93,7 +93,7 @@ o.spec("p", function() { o(string).equals(`${prefix}/en-US/route`) }) - o("handles dots", function() { + o("handles dots", () => { var string = p(prefix + "/:file.:ext/view", { file: "image", ext: "png" @@ -101,17 +101,17 @@ o.spec("p", function() { o(string).equals(`${prefix}/image.png/view`) }) - o("merges query strings", function() { + o("merges query strings", () => { var string = p(prefix + "/item?a=1&b=2", {c: 3}) o(string).equals(`${prefix}/item?a=1&b=2&c=3`) }) - o("merges query strings with other parameters", function() { + o("merges query strings with other parameters", () => { var string = p(prefix + "/item/:id?a=1&b=2", {id: "foo", c: 3}) o(string).equals(`${prefix}/item/foo?a=1&b=2&c=3`) }) - o("consumes template parameters without modifying query string", function() { + o("consumes template parameters without modifying query string", () => { var string = p(prefix + "/item/:id?a=1&b=2", {id: "foo"}) o(string).equals(`${prefix}/item/foo?a=1&b=2`) @@ -202,8 +202,8 @@ o.spec("p", function() { o(string).equals(prefix) }) } - o.spec("absolute", function() { test("") }) - o.spec("relative", function() { test("..") }) - o.spec("absolute + domain", function() { test("https://example.com") }) - o.spec("absolute + `file:`", function() { test("file://") }) + o.spec("absolute", () => { test("") }) + o.spec("relative", () => { test("..") }) + o.spec("absolute + domain", () => { test("https://example.com") }) + o.spec("absolute + `file:`", () => { test("file://") }) }) From b04519d1ffffa232117e4fdcffaf9bbc46aa51e9 Mon Sep 17 00:00:00 2001 From: Claudia Meadows Date: Thu, 3 Oct 2024 03:47:41 -0700 Subject: [PATCH 25/95] Make the event dictionary a proper class This makes that class fully monomorphic and also a bit safer. --- render/render.js | 43 +++++++++++++++++++++++-------------------- 1 file changed, 23 insertions(+), 20 deletions(-) diff --git a/render/render.js b/render/render.js index d2b0f493f..a577460a9 100644 --- a/render/render.js +++ b/render/render.js @@ -834,20 +834,22 @@ function updateStyle(element, old, style) { // that below. // 6. In function-based event handlers, `return false` prevents the default // action and stops event propagation. We replicate that below. -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 (this._ && ev.redraw !== false) (0, this._)() - if (result === false) { - ev.preventDefault() - ev.stopPropagation() +class EventDict extends Map { + constructor() { + super() + // Save this, so the current redraw is correctly tracked. + this._ = currentRedraw + } + handleEvent(ev) { + var handler = this.get(`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 (this._ && ev.redraw !== false) (0, this._)() + if (result === false) { + ev.preventDefault() + ev.stopPropagation() + } } } @@ -855,18 +857,19 @@ EventDict.prototype.handleEvent = function (ev) { function updateEvent(vnode, key, value) { if (vnode.instance != null) { vnode.instance._ = currentRedraw - if (vnode.instance[key] === value) return + var prev = vnode.instance.get(key) + if (prev === value) return if (value != null && (typeof value === "function" || typeof value === "object")) { - if (vnode.instance[key] == null) vnode.dom.addEventListener(key.slice(2), vnode.instance, false) - vnode.instance[key] = value + if (prev == null) vnode.dom.addEventListener(key.slice(2), vnode.instance, false) + vnode.instance.set(key, value) } else { - if (vnode.instance[key] != null) vnode.dom.removeEventListener(key.slice(2), vnode.instance, false) - vnode.instance[key] = undefined + if (prev != null) vnode.dom.removeEventListener(key.slice(2), vnode.instance, false) + vnode.instance.delete(key) } } else if (value != null && (typeof value === "function" || typeof value === "object")) { vnode.instance = new EventDict() vnode.dom.addEventListener(key.slice(2), vnode.instance, false) - vnode.instance[key] = value + vnode.instance.set(key, value) } } From 8fbc46a7fdcf60a1b94413f5182748e313ef3253 Mon Sep 17 00:00:00 2001 From: Claudia Meadows Date: Thu, 3 Oct 2024 14:18:20 -0700 Subject: [PATCH 26/95] Finish ES6ifying hyperscript factories --- render/hyperscript.js | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/render/hyperscript.js b/render/hyperscript.js index 48b8baf54..8ea64935c 100644 --- a/render/hyperscript.js +++ b/render/hyperscript.js @@ -68,6 +68,9 @@ function execSelector(selector, attrs, children) { return Vnode(state.tag, {}, attrs, children) } +var resolveChildren = (...children) => + (children.length === 1 && Array.isArray(children[0]) ? children[0].slice() : children) + // Caution is advised when editing this - it's very perf-critical. It's specially designed to avoid // allocations in the fast path, especially with fragments. function m(selector, attrs, ...children) { @@ -76,9 +79,9 @@ function m(selector, attrs, ...children) { } if (attrs == null || typeof attrs === "object" && attrs.tag == null && !Array.isArray(attrs)) { - if (children.length === 1 && Array.isArray(children[0])) children = children[0].slice() + children = resolveChildren(...children) } else { - children = children.length === 0 && Array.isArray(attrs) ? attrs.slice() : [attrs, ...children] + children = resolveChildren(attrs, ...children) attrs = undefined } @@ -92,25 +95,22 @@ function m(selector, attrs, ...children) { return Vnode(selector, {}, attrs, children) } -m.fragment = function(...args) { - return m("[", ...args) -} +m.fragment = (...args) => m("[", ...args) -m.key = function(key, ...children) { - if (children.length === 1 && Array.isArray(children[0])) { - children = children[0].slice() - } - return Vnode("=", key, undefined, m.normalizeChildren(children)) -} +// When removal is blocked, all ancestors are also blocked. This doesn't block other children, so +// this method also needs to accept an optional list of children to also keep alive while blocked. +// +// Note that the children are still notified of removal *immediately*. +m.key = (key, ...children) => Vnode("=", key, undefined, m.normalizeChildren(resolveChildren(...children))) -m.normalize = function(node) { +m.normalize = (node) => { if (node == null || typeof node === "boolean") return null if (typeof node !== "object") return Vnode("#", undefined, undefined, String(node)) if (Array.isArray(node)) return Vnode("[", undefined, undefined, m.normalizeChildren(node.slice())) return node } -m.normalizeChildren = function(input) { +m.normalizeChildren = (input) => { if (input.length) { input[0] = m.normalize(input[0]) var isKeyed = input[0] != null && input[0].tag === "=" From 1418bd3b482b01b6c8073faabaa9b8eca69addee Mon Sep 17 00:00:00 2001 From: Claudia Meadows Date: Thu, 3 Oct 2024 14:19:34 -0700 Subject: [PATCH 27/95] Drop stale recycling tests That's not been there for years --- render/tests/test-component.js | 16 -------- render/tests/test-oncreate.js | 30 --------------- render/tests/test-onremove.js | 11 ------ render/tests/test-updateNodes.js | 66 -------------------------------- 4 files changed, 123 deletions(-) diff --git a/render/tests/test-component.js b/render/tests/test-component.js index 6a47c4018..00d513678 100644 --- a/render/tests/test-component.js +++ b/render/tests/test-component.js @@ -665,22 +665,6 @@ o.spec("component", function() { o(called).equals(1) o(root.childNodes.length).equals(0) }) - o("does not recycle when there's an onupdate", function() { - var view = o.spy(() => m("div")) - var component = createComponent({ - onupdate: function() {}, - view, - }) - var vnode = m(component) - var updated = m(component) - - render(root, [m.key(1, vnode)]) - render(root, []) - render(root, [m.key(1, updated)]) - - o(view.calls[0].this).notEquals(view.calls[1].this) - o(view.calls[0].args[0].dom).notEquals(view.calls[1].args[0].dom) - }) o("lifecycle timing megatest (for a single component)", function() { var methods = { view: o.spy(function() { diff --git a/render/tests/test-oncreate.js b/render/tests/test-oncreate.js index 578d29375..705976755 100644 --- a/render/tests/test-oncreate.js +++ b/render/tests/test-oncreate.js @@ -117,24 +117,6 @@ o.spec("oncreate", function() { o(create.this).equals(vnode.state) o(create.args[0]).equals(vnode) }) - o("does not recycle when there's an oncreate", function() { - var create = o.spy() - var update = o.spy() - var vnode = m("div", {oncreate: create}) - var updated = m("div", {oncreate: update}) - - render(root, m.key(1, vnode)) - render(root, []) - render(root, m.key(1, updated)) - - o(vnode.dom).notEquals(updated.dom) - o(create.callCount).equals(1) - o(create.this).equals(vnode.state) - o(create.args[0]).equals(vnode) - o(update.callCount).equals(1) - o(update.this).equals(updated.state) - o(update.args[0]).equals(updated) - }) o("calls oncreate at the same step as onupdate", function() { var create = o.spy() var update = o.spy() @@ -194,16 +176,4 @@ o.spec("oncreate", function() { o(vnode.dom.oncreate).equals(undefined) o(vnode.dom.attributes["oncreate"]).equals(undefined) }) - o("calls oncreate on recycle", function() { - var create = o.spy() - var vnodes = m.key(1, m("div", {oncreate: create})) - var temp = [] - var updated = m.key(1, m("div", {oncreate: create})) - - render(root, vnodes) - render(root, temp) - render(root, updated) - - o(create.callCount).equals(2) - }) }) diff --git a/render/tests/test-onremove.js b/render/tests/test-onremove.js index b0f7954c8..ca6e7bd25 100644 --- a/render/tests/test-onremove.js +++ b/render/tests/test-onremove.js @@ -81,17 +81,6 @@ o.spec("onremove", function() { o(vnode.dom).notEquals(updated.dom) // this used to be a recycling pool test o(remove.callCount).equals(1) }) - o("does not recycle when there's an onremove", function() { - var remove = o.spy() - var vnode = m("div", {onremove: remove}) - var updated = m("div", {onremove: remove}) - - render(root, m.key(1, vnode)) - render(root, []) - render(root, m.key(1, updated)) - - o(vnode.dom).notEquals(updated.dom) - }) components.forEach(function(cmp){ o.spec(cmp.kind, function(){ var createComponent = cmp.create diff --git a/render/tests/test-updateNodes.js b/render/tests/test-updateNodes.js index a8811bc10..20b9bfea6 100644 --- a/render/tests/test-updateNodes.js +++ b/render/tests/test-updateNodes.js @@ -666,72 +666,6 @@ o.spec("updateNodes", function() { o(root.childNodes[0].childNodes[1].childNodes.length).equals(1) o(root.childNodes[1].childNodes.length).equals(0) }) - o("doesn't recycle", function() { - var vnodes = [m.key(1, m("div"))] - var temp = [] - var updated = [m.key(1, m("div"))] - - render(root, vnodes) - render(root, temp) - render(root, updated) - - o(vnodes[0].dom).notEquals(updated[0].dom) // this used to be a recycling pool test - o(updated[0].dom.nodeName).equals("DIV") - }) - o("doesn't recycle when not keyed", function() { - var vnodes = [m("div")] - var temp = [] - var updated = [m("div")] - - render(root, vnodes) - render(root, temp) - render(root, updated) - - o(root.childNodes.length).equals(1) - o(vnodes[0].dom).notEquals(updated[0].dom) // this used to be a recycling pool test - o(updated[0].dom.nodeName).equals("DIV") - }) - o("doesn't recycle deep", function() { - var vnodes = [m("div", m.key(1, m("a")))] - var temp = [m("div")] - var updated = [m("div", m.key(1, m("a")))] - - render(root, vnodes) - - var oldChild = vnodes[0].dom.firstChild - - render(root, temp) - render(root, updated) - - o(oldChild).notEquals(updated[0].dom.firstChild) // this used to be a recycling pool test - o(updated[0].dom.firstChild.nodeName).equals("A") - }) - o("mixed unkeyed tags are not broken by recycle", function() { - var vnodes = [m("a"), m("b")] - var temp = [m("b")] - var updated = [m("a"), m("b")] - - render(root, vnodes) - render(root, temp) - render(root, updated) - - o(root.childNodes.length).equals(2) - o(root.childNodes[0].nodeName).equals("A") - o(root.childNodes[1].nodeName).equals("B") - }) - o("mixed unkeyed vnode types are not broken by recycle", function() { - var vnodes = [m.fragment(m("a")), m("b")] - var temp = [m("b")] - var updated = [m.fragment(m("a")), m("b")] - - render(root, vnodes) - render(root, temp) - render(root, updated) - - o(root.childNodes.length).equals(2) - o(root.childNodes[0].nodeName).equals("A") - o(root.childNodes[1].nodeName).equals("B") - }) o("onremove doesn't fire from nodes in the pool (#1990)", function () { var onremove1 = o.spy() var onremove2 = o.spy() From 13c2e6376eeb7f0d7d7f520a2a977af80ba346ad Mon Sep 17 00:00:00 2001 From: Claudia Meadows Date: Thu, 3 Oct 2024 15:59:15 -0700 Subject: [PATCH 28/95] Add `m.tracked` utility for tracking delayed removals The long comment at the top of it explains the motivation. --- index.js | 1 + util/tests/test-tracked.js | 357 +++++++++++++++++++++++++++++++++++++ util/tracked.js | 141 +++++++++++++++ 3 files changed, 499 insertions(+) create mode 100644 util/tests/test-tracked.js create mode 100644 util/tracked.js diff --git a/index.js b/index.js index a37980270..e7fdf1ba9 100644 --- a/index.js +++ b/index.js @@ -11,5 +11,6 @@ m.p = require("./util/p") m.withProgress = require("./util/with-progress") m.censor = require("./util/censor") m.lazy = require("./util/lazy")(mountRedraw.redraw) +m.tracked = require("./util/tracked") module.exports = m diff --git a/util/tests/test-tracked.js b/util/tests/test-tracked.js new file mode 100644 index 000000000..0c252fe0e --- /dev/null +++ b/util/tests/test-tracked.js @@ -0,0 +1,357 @@ +"use strict" + +var o = require("ospec") +var makeTracked = require("../tracked") + +o.spec("tracked", () => { + /** @param {import("../tracked").Tracked} t */ + var live = (t) => t.live().map((h) => [h.key, h.value, h.signal.aborted]) + + o("initializes values correctly", () => { + var calls = 0 + var t = makeTracked([[1, "one"], [2, "two"]], () => calls++) + + o(live(t)).deepEquals([[1, "one", false], [2, "two", false]]) + o(t.list()).deepEquals([[1, "one"], [2, "two"]]) + + o(t.has(1)).equals(true) + o(t.get(1)).equals("one") + o(t.has(2)).equals(true) + o(t.get(2)).equals("two") + + o(calls).equals(0) + }) + + o("tracks values correctly", () => { + var calls = 0 + var t = makeTracked(undefined, () => calls++) + + t.set(1, "one") + o(calls).equals(1) + o(live(t)).deepEquals([[1, "one", false]]) + o(t.list()).deepEquals([[1, "one"]]) + o(t.has(1)).equals(true) + o(t.get(1)).equals("one") + var live1 = t.live()[0] + + t.set(2, "two") + o(calls).equals(2) + o(live(t)).deepEquals([[1, "one", false], [2, "two", false]]) + o(t.live()[0]).equals(live1) + o(t.list()).deepEquals([[1, "one"], [2, "two"]]) + o(t.has(1)).equals(true) + o(t.get(1)).equals("one") + o(t.has(2)).equals(true) + o(t.get(2)).equals("two") + var live2 = t.live()[1] + + t.delete(1) + o(calls).equals(3) + o(live(t)).deepEquals([[1, "one", true], [2, "two", false]]) + o(t.live()[0]).equals(live1) + o(t.live()[1]).equals(live2) + o(t.list()).deepEquals([[2, "two"]]) + o(t.has(1)).equals(false) + o(t.get(1)).equals(undefined) + o(t.has(2)).equals(true) + o(t.get(2)).equals("two") + + live1.release() + o(calls).equals(4) + o(live(t)).deepEquals([[2, "two", false]]) + o(t.live()[0]).equals(live2) + o(t.list()).deepEquals([[2, "two"]]) + o(t.has(1)).equals(false) + o(t.get(1)).equals(undefined) + o(t.has(2)).equals(true) + o(t.get(2)).equals("two") + + t.replace(2, "dos") + o(calls).equals(5) + o(live(t)).deepEquals([[2, "two", true], [2, "dos", false]]) + o(t.live()[0]).equals(live2) + o(t.list()).deepEquals([[2, "dos"]]) + o(t.has(1)).equals(false) + o(t.get(1)).equals(undefined) + o(t.has(2)).equals(true) + o(t.get(2)).equals("dos") + var live3 = t.live()[1] + + live2.release() + o(calls).equals(6) + o(live(t)).deepEquals([[2, "dos", false]]) + o(t.live()[0]).equals(live3) + o(t.list()).deepEquals([[2, "dos"]]) + o(t.has(1)).equals(false) + o(t.get(1)).equals(undefined) + o(t.has(2)).equals(true) + o(t.get(2)).equals("dos") + }) + + o("invokes `onUpdate()` after the update is fully completed, including any and all signal aborts", () => { + var live1, live2, live3 + var live1Aborted = false + var live2Aborted = false + var call = 0 + var t = makeTracked(undefined, () => { + switch (++call) { + case 1: + o(live(t)).deepEquals([[1, "one", false]]) + o(t.list()).deepEquals([[1, "one"]]) + o(t.has(1)).equals(true) + o(t.get(1)).equals("one") + live1 = t.live()[0] + break + + case 2: + o(live(t)).deepEquals([[1, "one", false], [2, "two", false]]) + o(t.live()[0]).equals(live1) + o(t.list()).deepEquals([[1, "one"], [2, "two"]]) + o(t.has(1)).equals(true) + o(t.get(1)).equals("one") + o(t.has(2)).equals(true) + o(t.get(2)).equals("two") + live2 = t.live()[1] + break + + case 3: + o(live(t)).deepEquals([[1, "one", true], [2, "two", false]]) + o(t.live()[0]).equals(live1) + o(t.live()[1]).equals(live2) + o(t.list()).deepEquals([[2, "two"]]) + o(t.has(1)).equals(false) + o(t.get(1)).equals(undefined) + o(t.has(2)).equals(true) + o(t.get(2)).equals("two") + break + + case 4: + o(live(t)).deepEquals([[2, "two", false]]) + o(t.live()[0]).equals(live2) + o(t.list()).deepEquals([[2, "two"]]) + o(t.has(1)).equals(false) + o(t.get(1)).equals(undefined) + o(t.has(2)).equals(true) + o(t.get(2)).equals("two") + break + + case 5: + o(live(t)).deepEquals([[2, "two", true], [2, "dos", false]]) + o(t.live()[0]).equals(live2) + o(t.list()).deepEquals([[2, "dos"]]) + o(t.has(1)).equals(false) + o(t.get(1)).equals(undefined) + o(t.has(2)).equals(true) + o(t.get(2)).equals("dos") + live3 = t.live()[1] + break + + case 6: + o(live(t)).deepEquals([[2, "dos", false]]) + o(t.live()[0]).equals(live3) + o(t.list()).deepEquals([[2, "dos"]]) + o(t.has(1)).equals(false) + o(t.get(1)).equals(undefined) + o(t.has(2)).equals(true) + o(t.get(2)).equals("dos") + break + + default: + throw new Error("Too many calls") + } + }) + + t.set(1, "one") + o(call).equals(1) + o(live1Aborted).equals(false) + o(live2Aborted).equals(false) + var deleteOneStarted = false + live1.signal.onabort = () => { + live1Aborted = true + o(call).equals(2) + o(deleteOneStarted).equals(true) + } + + t.set(2, "two") + o(call).equals(2) + o(live1Aborted).equals(false) + o(live2Aborted).equals(false) + var deleteTwoStarted = false + live2.signal.onabort = () => { + live2Aborted = true + o(call).equals(4) + o(deleteTwoStarted).equals(true) + } + + deleteOneStarted = true + t.delete(1) + o(call).equals(3) + o(live1Aborted).equals(true) + o(live2Aborted).equals(false) + + live1.release() + o(call).equals(4) + o(live1Aborted).equals(true) + o(live2Aborted).equals(false) + + deleteTwoStarted = true + t.replace(2, "dos") + o(call).equals(5) + o(live1Aborted).equals(true) + o(live2Aborted).equals(true) + + live2.release() + o(call).equals(6) + o(live1Aborted).equals(true) + o(live2Aborted).equals(true) + }) + + o("tracks parallel removes correctly", () => { + var calls = 0 + var t = makeTracked(undefined, () => calls++) + + t.set(1, "one") + var live1 = t.live()[0] + + t.set(2, "two") + var live2 = t.live()[1] + + t.delete(1) + o(calls).equals(3) + o(live(t)).deepEquals([[1, "one", true], [2, "two", false]]) + o(t.live()[0]).equals(live1) + o(t.live()[1]).equals(live2) + o(t.list()).deepEquals([[2, "two"]]) + o(t.has(1)).equals(false) + o(t.get(1)).equals(undefined) + o(t.has(2)).equals(true) + o(t.get(2)).equals("two") + + t.replace(2, "dos") + o(calls).equals(4) + o(live(t)).deepEquals([[1, "one", true], [2, "two", true], [2, "dos", false]]) + o(t.live()[0]).equals(live1) + o(t.live()[1]).equals(live2) + o(t.list()).deepEquals([[2, "dos"]]) + o(t.has(1)).equals(false) + o(t.get(1)).equals(undefined) + o(t.has(2)).equals(true) + o(t.get(2)).equals("dos") + var live3 = t.live()[2] + + live1.release() + o(calls).equals(5) + o(live(t)).deepEquals([[2, "two", true], [2, "dos", false]]) + o(t.live()[0]).equals(live2) + o(t.list()).deepEquals([[2, "dos"]]) + o(t.has(1)).equals(false) + o(t.get(1)).equals(undefined) + o(t.has(2)).equals(true) + o(t.get(2)).equals("dos") + + live2.release() + o(calls).equals(6) + o(live(t)).deepEquals([[2, "dos", false]]) + o(t.live()[0]).equals(live3) + o(t.list()).deepEquals([[2, "dos"]]) + o(t.has(1)).equals(false) + o(t.get(1)).equals(undefined) + o(t.has(2)).equals(true) + o(t.get(2)).equals("dos") + }) + + o("tolerates release before abort", () => { + var calls = 0 + var t = makeTracked(undefined, () => calls++) + + t.set(1, "one") + o(calls).equals(1) + o(live(t)).deepEquals([[1, "one", false]]) + o(t.list()).deepEquals([[1, "one"]]) + o(t.has(1)).equals(true) + o(t.get(1)).equals("one") + var live1 = t.live()[0] + + live1.release() + o(calls).equals(1) + o(live(t)).deepEquals([[1, "one", false]]) + o(t.list()).deepEquals([[1, "one"]]) + o(t.has(1)).equals(true) + o(t.get(1)).equals("one") + + t.delete(1) + o(calls).equals(2) + o(live(t)).deepEquals([]) + o(t.list()).deepEquals([]) + o(t.has(1)).equals(false) + o(t.get(1)).equals(undefined) + }) + + o("tolerates double release before abort", () => { + var calls = 0 + var t = makeTracked(undefined, () => calls++) + + t.set(1, "one") + var live1 = t.live()[0] + + live1.release() + live1.release() + o(calls).equals(1) + o(live(t)).deepEquals([[1, "one", false]]) + o(t.list()).deepEquals([[1, "one"]]) + o(t.has(1)).equals(true) + o(t.get(1)).equals("one") + + t.delete(1) + o(calls).equals(2) + o(live(t)).deepEquals([]) + o(t.list()).deepEquals([]) + o(t.has(1)).equals(false) + o(t.get(1)).equals(undefined) + }) + + o("tolerates double release spanning delete", () => { + var calls = 0 + var t = makeTracked(undefined, () => calls++) + + t.set(1, "one") + var live1 = t.live()[0] + live1.release() + t.delete(1) + live1.release() + + o(calls).equals(2) + o(live(t)).deepEquals([]) + o(t.list()).deepEquals([]) + o(t.has(1)).equals(false) + o(t.get(1)).equals(undefined) + }) + + o("tracks double release after delete", () => { + var calls = 0 + var t = makeTracked(undefined, () => calls++) + + t.set(1, "one") + var live1 = t.live()[0] + t.delete(1) + o(calls).equals(2) + o(live(t)).deepEquals([[1, "one", true]]) + o(t.list()).deepEquals([]) + o(t.has(1)).equals(false) + o(t.get(1)).equals(undefined) + + live1.release() + o(calls).equals(3) + o(live(t)).deepEquals([]) + o(t.list()).deepEquals([]) + o(t.has(1)).equals(false) + o(t.get(1)).equals(undefined) + + live1.release() + o(calls).equals(3) + o(live(t)).deepEquals([]) + o(t.list()).deepEquals([]) + o(t.has(1)).equals(false) + o(t.get(1)).equals(undefined) + }) +}) diff --git a/util/tracked.js b/util/tracked.js new file mode 100644 index 000000000..6409abc73 --- /dev/null +++ b/util/tracked.js @@ -0,0 +1,141 @@ +"use strict" + +var mountRedraw = require("../mount-redraw") + +/* +Here's the intent. +- Usage in model: + - List + - Get + - Track + - Delete + - Replace (equivalent to delete + track) +- Usage in view: + - Iterate live handles + - Release aborted live handles that no longer needed + +Models can do basic CRUD operations on the collection. +- They can list what's currently there. +- They can get a current value. +- They can set the current value. +- They can delete the current value. +- They can replace the current value, deleting a value that's already there. + +In the view, they use handles to abstract over the concept of a key. Duplicates are theoretically +possible, so they should use the handle itself as the key for `m.key(...)`. It might look something +like this: + +```js +return t.live().map((handle) => ( + m.key(handle, m(Entry, { + name: handle.key, + value: handle.value, + removed: handle.signal.aborted, + onremovaltransitionended: () => handle.release(), + })) +)) +``` + +There used to be an in-renderer way to manage this transparently, but there's a couple big reasons +why that was removed in favor of this: + +1. It's very complicated to get right. Like, the majority of the removal code was related to it. In + fact, this module is considerably smaller than the code that'd have to go into the renderer to + support it, as this isn't nearly as perf-sensitive as that. +2. When you need to remove something asynchronously, there's multiple ways you may want to manage + transitions. You might want to stagger them. You might want to do them all at once. You might + want to clear some state and not other state. You might want to preserve some elements of a + sibling's state. Embedding it in the renderer would force an opinion on you, and in order to + work around it, you'd have to do something like this anyways. +*/ + +/** + * @template K, V + * @typedef TrackedHandle + * + * @property {K} key + * @property {V} value + * @property {AbortSignal} signal + * @property {() => void} release + */ + +/** + * @template K, V + * @typedef Tracked + * + * @property {() => Array>} live + * @property {() => Array<[K, V]>} list + * @property {(key: K) => boolean} has + * @property {(key: K) => undefined | V} get + * @property {(key: K, value: V) => void} track + * @property {(key: K, value: V) => void} replace + * @property {(key: K) => boolean} delete + */ + +/** + * @template K, V + * @param {Iterable<[K, V]>} [initial] + * @param {() => void} [onUpdate] + * @returns {Tracked} + */ +module.exports = (initial, onUpdate = mountRedraw.redraw) => { + /** @type {Map & {_: AbortController}>} */ var state = new Map() + /** @type {Set>} */ var live = new Set() + + var abort = (prev) => { + try { + if (prev) { + if (prev._) prev._.abort() + else live.delete(prev) + } + } catch (e) { + console.error(e) + } + } + + // Bit 1 forcibly releases the old handle, and bit 2 causes an update notification to be sent + // (something that's unwanted during initialization). + var setHandle = (k, v, bits) => { + var prev = state.get(k) + var ctrl = new AbortController() + /** @type {TrackedHandle} */ + var handle = { + _: ctrl, + key: k, + value: v, + signal: ctrl.signal, + release() { + if (state.get(handle.key) === handle) { + handle._ = null + } else if (live.delete(handle)) { + onUpdate() + } + }, + } + state.set(k, handle) + live.add(handle) + // eslint-disable-next-line no-bitwise + if (bits & 1) live.delete(prev) + abort(prev) + // eslint-disable-next-line no-bitwise + if (bits & 2) onUpdate() + } + + for (var [k, v] of initial || []) setHandle(k, v, 1) + + return { + live: () => [...live], + list: () => Array.from(state.values(), (h) => [h.key, h.value]), + has: (k) => state.has(k), + get: (k) => (k = state.get(k)) && k.value, + set: (k, v) => setHandle(k, v, 3), + replace: (k, v) => setHandle(k, v, 2), + delete(k) { + var prev = state.get(k) + var result = state.delete(k) + abort(prev) + onUpdate() + return result + }, + } +} From cd20a00f1167cbd42f0dfbfce40c24d14364f977 Mon Sep 17 00:00:00 2001 From: Claudia Meadows Date: Thu, 3 Oct 2024 16:01:11 -0700 Subject: [PATCH 29/95] Make `m.lazy` not a higher-order factory - it doesn't need to be one --- index.js | 2 +- util/lazy.js | 3 +- util/tests/test-lazy.js | 66 ++++++++++++++++++++--------------------- 3 files changed, 36 insertions(+), 35 deletions(-) diff --git a/index.js b/index.js index e7fdf1ba9..ed39d8961 100644 --- a/index.js +++ b/index.js @@ -10,7 +10,7 @@ m.redraw = mountRedraw.redraw m.p = require("./util/p") m.withProgress = require("./util/with-progress") m.censor = require("./util/censor") -m.lazy = require("./util/lazy")(mountRedraw.redraw) +m.lazy = require("./util/lazy") m.tracked = require("./util/tracked") module.exports = m diff --git a/util/lazy.js b/util/lazy.js index 33c7b3e54..484b348f7 100644 --- a/util/lazy.js +++ b/util/lazy.js @@ -1,9 +1,10 @@ "use strict" +var mountRedraw = require("../mount-redraw") var m = require("../render/hyperscript") var censor = require("./censor") -module.exports = (redraw) => (opts) => { +module.exports = (opts, redraw = mountRedraw.redraw) => { var fetched = false var Comp = () => ({view: () => opts.pending && opts.pending()}) var e = new ReferenceError("Component not found") diff --git a/util/tests/test-lazy.js b/util/tests/test-lazy.js index 359e12541..e031b9791 100644 --- a/util/tests/test-lazy.js +++ b/util/tests/test-lazy.js @@ -4,7 +4,7 @@ var o = require("ospec") var components = require("../../test-utils/components") var domMock = require("../../test-utils/domMock") var hyperscript = require("../../render/hyperscript") -var lazy = require("../lazy") +var makeLazy = require("../lazy") var render = require("../../render/render") o.spec("lazy", () => { @@ -35,14 +35,14 @@ o.spec("lazy", () => { }) var send, notifyRedrawn var fetchRedrawn = new Promise((resolve) => notifyRedrawn = resolve) - var C = lazy(() => { - notifyRedrawn() - calls.push(`scheduled ${scheduled++}`) - })({ + var C = makeLazy({ fetch() { calls.push("fetch") return new Promise((resolve) => send = resolve) }, + }, () => { + notifyRedrawn() + calls.push(`scheduled ${scheduled++}`) }) o(calls).deepEquals([]) @@ -101,14 +101,14 @@ o.spec("lazy", () => { var scheduled = 1 var send, notifyRedrawn var fetchRedrawn = new Promise((resolve) => notifyRedrawn = resolve) - var C = lazy(() => { - notifyRedrawn() - calls.push(`scheduled ${scheduled++}`) - })({ + var C = makeLazy({ fetch() { calls.push("fetch") return new Promise((_, reject) => send = reject) }, + }, () => { + notifyRedrawn() + calls.push(`scheduled ${scheduled++}`) }) o(calls).deepEquals([]) @@ -166,10 +166,7 @@ o.spec("lazy", () => { }) var send, notifyRedrawn var fetchRedrawn = new Promise((resolve) => notifyRedrawn = resolve) - var C = lazy(() => { - notifyRedrawn() - calls.push(`scheduled ${scheduled++}`) - })({ + var C = makeLazy({ fetch() { calls.push("fetch") return new Promise((resolve) => send = resolve) @@ -177,6 +174,9 @@ o.spec("lazy", () => { pending() { calls.push("pending") }, + }, () => { + notifyRedrawn() + calls.push(`scheduled ${scheduled++}`) }) o(calls).deepEquals([]) @@ -243,10 +243,7 @@ o.spec("lazy", () => { var scheduled = 1 var send, notifyRedrawn var fetchRedrawn = new Promise((resolve) => notifyRedrawn = resolve) - var C = lazy(() => { - notifyRedrawn() - calls.push(`scheduled ${scheduled++}`) - })({ + var C = makeLazy({ fetch() { calls.push("fetch") return new Promise((_, reject) => send = reject) @@ -254,6 +251,9 @@ o.spec("lazy", () => { pending() { calls.push("pending") }, + }, () => { + notifyRedrawn() + calls.push(`scheduled ${scheduled++}`) }) o(calls).deepEquals([]) @@ -319,10 +319,7 @@ o.spec("lazy", () => { }) var send, notifyRedrawn var fetchRedrawn = new Promise((resolve) => notifyRedrawn = resolve) - var C = lazy(() => { - notifyRedrawn() - calls.push(`scheduled ${scheduled++}`) - })({ + var C = makeLazy({ fetch() { calls.push("fetch") return new Promise((resolve) => send = resolve) @@ -330,6 +327,9 @@ o.spec("lazy", () => { error() { calls.push("error") }, + }, () => { + notifyRedrawn() + calls.push(`scheduled ${scheduled++}`) }) o(calls).deepEquals([]) @@ -385,10 +385,7 @@ o.spec("lazy", () => { var scheduled = 1 var send, notifyRedrawn var fetchRedrawn = new Promise((resolve) => notifyRedrawn = resolve) - var C = lazy(() => { - notifyRedrawn() - calls.push(`scheduled ${scheduled++}`) - })({ + var C = makeLazy({ fetch() { calls.push("fetch") return new Promise((_, reject) => send = reject) @@ -396,6 +393,9 @@ o.spec("lazy", () => { error(e) { calls.push("error", e.message) }, + }, () => { + notifyRedrawn() + calls.push(`scheduled ${scheduled++}`) }) o(calls).deepEquals([]) @@ -456,10 +456,7 @@ o.spec("lazy", () => { }) var send, notifyRedrawn var fetchRedrawn = new Promise((resolve) => notifyRedrawn = resolve) - var C = lazy(() => { - notifyRedrawn() - calls.push(`scheduled ${scheduled++}`) - })({ + var C = makeLazy({ fetch() { calls.push("fetch") return new Promise((resolve) => send = resolve) @@ -470,6 +467,9 @@ o.spec("lazy", () => { error() { calls.push("error") }, + }, () => { + notifyRedrawn() + calls.push(`scheduled ${scheduled++}`) }) o(calls).deepEquals([]) @@ -533,10 +533,7 @@ o.spec("lazy", () => { var scheduled = 1 var send, notifyRedrawn var fetchRedrawn = new Promise((resolve) => notifyRedrawn = resolve) - var C = lazy(() => { - notifyRedrawn() - calls.push(`scheduled ${scheduled++}`) - })({ + var C = makeLazy({ fetch() { calls.push("fetch") return new Promise((_, reject) => send = reject) @@ -547,6 +544,9 @@ o.spec("lazy", () => { error(e) { calls.push("error", e.message) }, + }, () => { + notifyRedrawn() + calls.push(`scheduled ${scheduled++}`) }) o(calls).deepEquals([]) From 454f8e7d087fc0b86eefd9af8aca3c480438ce36 Mon Sep 17 00:00:00 2001 From: Claudia Meadows Date: Thu, 3 Oct 2024 16:11:27 -0700 Subject: [PATCH 30/95] Drop `onbeforeremove` It's now been made redundant with `m.tracked`. --- render/render.js | 217 +++++++-------------------- render/tests/test-component.js | 58 +------- render/tests/test-onbeforeremove.js | 173 ---------------------- render/tests/test-onremove.js | 218 ---------------------------- render/tests/test-render.js | 4 - util/censor.js | 4 +- util/tests/test-censor.js | 12 -- 7 files changed, 56 insertions(+), 630 deletions(-) delete mode 100644 render/tests/test-onbeforeremove.js diff --git a/render/render.js b/render/render.js index a577460a9..e4e633ca7 100644 --- a/render/render.js +++ b/render/render.js @@ -8,12 +8,6 @@ var nameSpace = { math: "http://www.w3.org/1998/Math/MathML" } -// The vnode path is needed for proper removal unblocking. It's not retained past a given -// render and is overwritten on every vnode visit, so callers wanting to retain it should -// always clone the part they're interested in. -var vnodePath -var blockedRemovalRefCount = /*@__PURE__*/new WeakMap() -var removalRequested = /*@__PURE__*/new WeakSet() var currentRedraw function getDocument(dom) { @@ -231,10 +225,10 @@ function createComponent(parent, vnode, hooks, ns, nextSibling) { // the old DOM nodes before updateNode runs because it enables us to use the cached `nextSibling` // variable rather than fetching it using `getNextSibling()`. -function updateNodes(parent, old, vnodes, hooks, nextSibling, ns, pathDepth) { +function updateNodes(parent, old, vnodes, hooks, nextSibling, ns) { if (old === vnodes || old == null && vnodes == null) return else if (old == null || old.length === 0) createNodes(parent, vnodes, 0, vnodes.length, hooks, nextSibling, ns) - else if (vnodes == null || vnodes.length === 0) removeNodes(parent, old, 0, old.length, pathDepth, false) + else if (vnodes == null || vnodes.length === 0) removeNodes(parent, old, 0, old.length) else { var isOldKeyed = old[0] != null && old[0].tag === "=" var isKeyed = vnodes[0] != null && vnodes[0].tag === "=" @@ -242,7 +236,7 @@ function updateNodes(parent, old, vnodes, hooks, nextSibling, ns, pathDepth) { if (!isOldKeyed) while (oldStart < old.length && old[oldStart] == null) oldStart++ if (!isKeyed) while (start < vnodes.length && vnodes[start] == null) start++ if (isOldKeyed !== isKeyed) { - removeNodes(parent, old, oldStart, old.length, pathDepth, false) + removeNodes(parent, old, oldStart, old.length) createNodes(parent, vnodes, start, vnodes.length, hooks, nextSibling, ns) } else if (!isKeyed) { // Don't index past the end of either list (causes deopts). @@ -256,10 +250,10 @@ function updateNodes(parent, old, vnodes, hooks, nextSibling, ns, pathDepth) { v = vnodes[start] if (o === v || o == null && v == null) continue else if (o == null) createNode(parent, v, hooks, ns, getNextSibling(old, start + 1, nextSibling)) - else if (v == null) removeNode(parent, o, pathDepth, false) - else updateNode(parent, o, v, hooks, getNextSibling(old, start + 1, nextSibling), ns, pathDepth) + else if (v == null) removeNode(parent, o) + else updateNode(parent, o, v, hooks, getNextSibling(old, start + 1, nextSibling), ns) } - if (old.length > commonLength) removeNodes(parent, old, start, old.length, pathDepth, false) + if (old.length > commonLength) removeNodes(parent, old, start, old.length) if (vnodes.length > commonLength) createNodes(parent, vnodes, start, vnodes.length, hooks, nextSibling, ns) } else { // keyed diff @@ -270,7 +264,7 @@ function updateNodes(parent, old, vnodes, hooks, nextSibling, ns, pathDepth) { oe = old[oldEnd] ve = vnodes[end] if (oe.state !== ve.state) break - if (oe !== ve) updateNode(parent, oe, ve, hooks, nextSibling, ns, pathDepth) + if (oe !== ve) updateNode(parent, oe, ve, hooks, nextSibling, ns) if (ve.dom != null) nextSibling = ve.dom oldEnd--, end-- } @@ -280,7 +274,7 @@ function updateNodes(parent, old, vnodes, hooks, nextSibling, ns, pathDepth) { v = vnodes[start] if (o.state !== v.state) break oldStart++, start++ - if (o !== v) updateNode(parent, o, v, hooks, getNextSibling(old, oldStart, nextSibling), ns, pathDepth) + if (o !== v) updateNode(parent, o, v, hooks, getNextSibling(old, oldStart, nextSibling), ns) } // swaps and list reversals while (oldEnd >= oldStart && end >= start) { @@ -288,9 +282,9 @@ function updateNodes(parent, old, vnodes, hooks, nextSibling, ns, pathDepth) { if (o.state !== ve.state || oe.state !== v.state) break topSibling = getNextSibling(old, oldStart, nextSibling) moveDOM(parent, oe, topSibling) - if (oe !== v) updateNode(parent, oe, v, hooks, topSibling, ns, pathDepth) + if (oe !== v) updateNode(parent, oe, v, hooks, topSibling, ns) if (++start <= --end) moveDOM(parent, o, nextSibling) - if (o !== ve) updateNode(parent, o, ve, hooks, nextSibling, ns, pathDepth) + if (o !== ve) updateNode(parent, o, ve, hooks, nextSibling, ns) if (ve.dom != null) nextSibling = ve.dom oldStart++; oldEnd-- oe = old[oldEnd] @@ -301,13 +295,13 @@ function updateNodes(parent, old, vnodes, hooks, nextSibling, ns, pathDepth) { // bottom up once again while (oldEnd >= oldStart && end >= start) { if (oe.state !== ve.state) break - if (oe !== ve) updateNode(parent, oe, ve, hooks, nextSibling, ns, pathDepth) + if (oe !== ve) updateNode(parent, oe, ve, hooks, nextSibling, ns) if (ve.dom != null) nextSibling = ve.dom oldEnd--, end-- oe = old[oldEnd] ve = vnodes[end] } - if (start > end) removeNodes(parent, old, oldStart, oldEnd + 1, pathDepth, false) + if (start > end) removeNodes(parent, old, oldStart, oldEnd + 1) else if (oldStart > oldEnd) createNodes(parent, vnodes, start, end + 1, hooks, nextSibling, ns) else { // inspired by ivi https://github.com/ivijs/ivi/ by Boris Kaul @@ -322,13 +316,13 @@ function updateNodes(parent, old, vnodes, hooks, nextSibling, ns, pathDepth) { oldIndices[i-start] = oldIndex oe = old[oldIndex] old[oldIndex] = null - if (oe !== ve) updateNode(parent, oe, ve, hooks, nextSibling, ns, pathDepth) + if (oe !== ve) updateNode(parent, oe, ve, hooks, nextSibling, ns) if (ve.dom != null) nextSibling = ve.dom matched++ } } nextSibling = originalNextSibling - if (matched !== oldEnd - oldStart + 1) removeNodes(parent, old, oldStart, oldEnd + 1, pathDepth, false) + if (matched !== oldEnd - oldStart + 1) removeNodes(parent, old, oldStart, oldEnd + 1) if (matched === 0) createNodes(parent, vnodes, start, end + 1, hooks, nextSibling, ns) else { if (pos === -1) { @@ -357,14 +351,12 @@ function updateNodes(parent, old, vnodes, hooks, nextSibling, ns, pathDepth) { } } } -function updateNode(parent, old, vnode, hooks, nextSibling, ns, pathDepth) { +function updateNode(parent, old, vnode, hooks, nextSibling, ns) { var oldTag = old.tag, tag = vnode.tag if (oldTag === tag) { vnode.state = old.state vnode.instance = old.instance if (shouldNotUpdate(vnode, old)) return - vnodePath[pathDepth++] = parent - vnodePath[pathDepth++] = vnode if (typeof oldTag === "string") { if (vnode.attrs != null) { updateLifecycle(vnode.attrs, vnode, hooks) @@ -372,14 +364,14 @@ function updateNode(parent, old, vnode, hooks, nextSibling, ns, pathDepth) { switch (oldTag) { case "#": updateText(old, vnode); break case "=": - case "[": updateFragment(parent, old, vnode, hooks, nextSibling, ns, pathDepth); break - default: updateElement(old, vnode, hooks, ns, pathDepth) + case "[": updateFragment(parent, old, vnode, hooks, nextSibling, ns); break + default: updateElement(old, vnode, hooks, ns) } } - else updateComponent(parent, old, vnode, hooks, nextSibling, ns, pathDepth) + else updateComponent(parent, old, vnode, hooks, nextSibling, ns) } else { - removeNode(parent, old, pathDepth, false) + removeNode(parent, old) createNode(parent, vnode, hooks, ns, nextSibling) } } @@ -389,8 +381,8 @@ function updateText(old, vnode) { } vnode.dom = old.dom } -function updateFragment(parent, old, vnode, hooks, nextSibling, ns, pathDepth) { - updateNodes(parent, old.children, vnode.children, hooks, nextSibling, ns, pathDepth) +function updateFragment(parent, old, vnode, hooks, nextSibling, ns) { + updateNodes(parent, old.children, vnode.children, hooks, nextSibling, ns) vnode.dom = null if (vnode.children != null) { for (var child of vnode.children) { @@ -400,27 +392,27 @@ function updateFragment(parent, old, vnode, hooks, nextSibling, ns, pathDepth) { } } } -function updateElement(old, vnode, hooks, ns, pathDepth) { +function updateElement(old, vnode, hooks, ns) { var element = vnode.dom = old.dom ns = getNameSpace(vnode) || ns updateAttrs(vnode, old.attrs, vnode.attrs, ns) if (!maybeSetContentEditable(vnode)) { - updateNodes(element, old.children, vnode.children, hooks, null, ns, pathDepth) + updateNodes(element, old.children, vnode.children, hooks, null, ns) } } -function updateComponent(parent, old, vnode, hooks, nextSibling, ns, pathDepth) { +function updateComponent(parent, old, vnode, hooks, nextSibling, ns) { vnode.instance = hyperscript.normalize(callHook.call(vnode.state.view, vnode)) if (vnode.instance === vnode) throw Error("A view cannot return the vnode it received as argument") updateLifecycle(vnode.state, vnode, hooks) if (vnode.attrs != null) updateLifecycle(vnode.attrs, vnode, hooks) if (vnode.instance != null) { if (old.instance == null) createNode(parent, vnode.instance, hooks, ns, nextSibling) - else updateNode(parent, old.instance, vnode.instance, hooks, nextSibling, ns, pathDepth) + else updateNode(parent, old.instance, vnode.instance, hooks, nextSibling, ns) vnode.dom = vnode.instance.dom } else if (old.instance != null) { - removeNode(parent, old.instance, pathDepth, false) + removeNode(parent, old.instance, false) vnode.dom = undefined } else { @@ -491,7 +483,7 @@ function getNextSibling(vnodes, i, nextSibling) { return nextSibling } -// This handles fragments with zombie children (removed from vdom, but persisted in DOM through onbeforeremove) +// This moves only the nodes tracked by Mithril function moveDOM(parent, vnode, nextSibling) { if (typeof vnode.tag === "function") { return moveDOM(parent, vnode.instance, nextSibling) @@ -524,142 +516,40 @@ function maybeSetContentEditable(vnode) { } //remove -function invokeBeforeRemove(vnode, host) { - try { - if (typeof host.onbeforeremove === "function") { - var result = callHook.call(host.onbeforeremove, vnode) - if (result != null && typeof result.then === "function") return Promise.resolve(result) - } - } catch (e) { - // Errors during removal aren't fatal. Just log them. - console.error(e) - } +function removeNodes(parent, vnodes, start, end) { + for (var i = start; i < end; i++) removeNode(parent, vnodes[i]) } -function tryProcessRemoval(parent, vnode) { - // eslint-disable-next-line no-bitwise - var refCount = blockedRemovalRefCount.get(vnode) | 0 - if (refCount > 1) { - blockedRemovalRefCount.set(vnode, refCount - 1) - return false - } - - if (typeof vnode.tag !== "function" && vnode.tag !== "[" && vnode.tag !== "=") { - parent.removeChild(vnode.dom) - } - - try { - if (typeof vnode.tag !== "string" && typeof vnode.state.onremove === "function") { - callHook.call(vnode.state.onremove, vnode) - } - } catch (e) { - console.error(e) - } - - try { - if (vnode.attrs && typeof vnode.attrs.onremove === "function") { - callHook.call(vnode.attrs.onremove, vnode) +function removeNode(parent, vnode) { + if (vnode != null) { + if (typeof vnode.tag === "function") { + if (vnode.instance != null) removeNode(parent, vnode.instance) + } else if (vnode.tag !== "#") { + removeNodes( + vnode.tag !== "[" && vnode.tag !== "=" ? vnode.dom : parent, + vnode.children, 0, vnode.children.length + ) } - } catch (e) { - console.error(e) - } - return true -} -function removeNodeAsyncRecurse(parent, vnode) { - while (vnode != null) { - // Delay the actual subtree removal if there's still pending `onbeforeremove` hooks on - // this node or a child node. - if (!tryProcessRemoval(parent, vnode)) return false - if (typeof vnode.tag !== "function") { - if (vnode.tag === "#") break - if (vnode.tag !== "[" && vnode.tag !== "=") parent = vnode.dom - // Using bitwise ops and `Array.prototype.reduce` to reduce code size. It's not - // called nearly enough to merit further optimization. - // eslint-disable-next-line no-bitwise - return vnode.children.reduce((fail, child) => fail & removeNodeAsyncRecurse(parent, child), 1) + if (typeof vnode.tag !== "function" && vnode.tag !== "[" && vnode.tag !== "=") { + parent.removeChild(vnode.dom) } - vnode = vnode.instance - } - - return true -} -function removeNodes(parent, vnodes, start, end, pathDepth, isDelayed) { - // Using bitwise ops to reduce code size. - var fail = 0 - // eslint-disable-next-line no-bitwise - for (var i = start; i < end; i++) fail |= !removeNode(parent, vnodes[i], pathDepth, isDelayed) - return !fail -} -function removeNode(parent, vnode, pathDepth, isDelayed) { - if (vnode != null) { - delayed: { - var attrsResult, stateResult - - // Block removes, but do call nested `onbeforeremove`. - if (typeof vnode.tag !== "string") attrsResult = invokeBeforeRemove(vnode, vnode.state) - if (vnode.attrs != null) stateResult = invokeBeforeRemove(vnode, vnode.attrs) - - vnodePath[pathDepth++] = parent - vnodePath[pathDepth++] = vnode - - if (attrsResult || stateResult) { - var path = vnodePath.slice(0, pathDepth) - var settle = () => { - - // Remove the innermost node recursively and try to remove the parents - // non-recursively. - // If it's still delayed, skip. If this node is delayed, all its ancestors are - // also necessarily delayed, and so they should be skipped. - var i = path.length - 2 - if (removeNodeAsyncRecurse(path[i], path[i + 1])) { - while ((i -= 2) >= 0 && removalRequested.has(path[i + 1])) { - tryProcessRemoval(path[i], path[i + 1]) - } - } - } - var increment = 0 - - if (attrsResult) { - attrsResult.catch(console.error) - attrsResult.then(settle, settle) - increment++ - } - if (stateResult) { - stateResult.catch(console.error) - stateResult.then(settle, settle) - increment++ - } - - isDelayed = true - - for (var i = 1; i < pathDepth; i += 2) { - // eslint-disable-next-line no-bitwise - blockedRemovalRefCount.set(vnodePath[i], (blockedRemovalRefCount.get(vnodePath[i]) | 0) + increment) - } + try { + if (typeof vnode.tag !== "string" && typeof vnode.state.onremove === "function") { + callHook.call(vnode.state.onremove, vnode) } + } catch (e) { + console.error(e) + } - if (typeof vnode.tag === "function") { - if (vnode.instance != null && !removeNode(parent, vnode.instance, pathDepth, isDelayed)) break delayed - } else if (vnode.tag !== "#") { - if (!removeNodes( - vnode.tag !== "[" && vnode.tag !== "=" ? vnode.dom : parent, - vnode.children, 0, vnode.children.length, pathDepth, isDelayed - )) break delayed + try { + if (vnode.attrs && typeof vnode.attrs.onremove === "function") { + callHook.call(vnode.attrs.onremove, vnode) } - - // Don't call removal hooks if removal is delayed. - // Delay the actual subtree removal if there's still pending `onbeforeremove` hooks on - // this node or a child node. - if (isDelayed || tryProcessRemoval(parent, vnode)) break delayed - - return false + } catch (e) { + console.error(e) } - - removalRequested.add(vnode) } - - return true } //attrs @@ -763,7 +653,7 @@ function updateAttrs(vnode, old, attrs, ns) { } } var isAlwaysFormAttribute = new Set(["value", "checked", "selected", "selectedIndex"]) -var isSpecialAttribute = new Set(["key", "is", "oninit", "oncreate", "onupdate", "onremove", "onbeforeupdate", "onbeforeremove"]) +var isSpecialAttribute = new Set(["key", "is", "oninit", "oncreate", "onupdate", "onremove", "onbeforeupdate"]) // Try to avoid a few browser bugs on normal elements. // var propertyMayBeBugged = new Set(["href", "list", "form", "width", "height", "type"]) var propertyMayBeBugged = new Set(["href", "list", "form", "width", "height"]) @@ -919,11 +809,9 @@ module.exports = function(dom, vnodes, redraw) { var hooks = [] var active = activeElement(dom) var namespace = dom.namespaceURI - var prevPath = vnodePath currentDOM = dom currentRedraw = typeof redraw === "function" ? redraw : undefined - vnodePath = [] try { // First time rendering into a node clears it out if (dom.vnodes == null) dom.textContent = "" @@ -936,6 +824,5 @@ module.exports = function(dom, vnodes, redraw) { } finally { currentRedraw = prevRedraw currentDOM = prevDOM - vnodePath = prevPath } } diff --git a/render/tests/test-component.js b/render/tests/test-component.js index 00d513678..594eab07c 100644 --- a/render/tests/test-component.js +++ b/render/tests/test-component.js @@ -616,55 +616,6 @@ o.spec("component", function() { o(rootCountInCall).equals(0) o(root.childNodes.length).equals(0) }) - o("calls onbeforeremove", function() { - var rootCountInCall - var onbeforeremove = o.spy(() => { - rootCountInCall = root.childNodes.length - }) - var component = createComponent({ - onbeforeremove, - view: function() { - return m("div", {id: "a"}, "b") - } - }) - - render(root, m(component)) - - o(onbeforeremove.callCount).equals(0) - o(root.childNodes.length).equals(1) - var firstChild = root.firstChild - - render(root, []) - - o(onbeforeremove.callCount).equals(1) - o(onbeforeremove.args[0].dom).equals(firstChild) - o(rootCountInCall).equals(1) - o(root.childNodes.length).equals(0) - }) - o("calls onbeforeremove when returning fragment", function() { - var called = 0 - var component = createComponent({ - onbeforeremove: function(vnode) { - called++ - - o(vnode.dom).notEquals(undefined) - o(vnode.dom).equals(root.firstChild) - o(root.childNodes.length).equals(1) - }, - view: function() { - return [m("div", {id: "a"}, "b")] - } - }) - - render(root, m(component)) - - o(called).equals(0) - - render(root, []) - - o(called).equals(1) - o(root.childNodes.length).equals(0) - }) o("lifecycle timing megatest (for a single component)", function() { var methods = { view: o.spy(function() { @@ -674,7 +625,7 @@ o.spec("component", function() { var attrs = {} var hooks = [ "oninit", "oncreate", "onbeforeupdate", - "onupdate", "onbeforeremove", "onremove" + "onupdate", "onremove" ] hooks.forEach(function(hook) { if (hook === "onbeforeupdate") { @@ -703,7 +654,6 @@ o.spec("component", function() { o(methods.oncreate.callCount).equals(0) o(methods.onbeforeupdate.callCount).equals(0) o(methods.onupdate.callCount).equals(0) - o(methods.onbeforeremove.callCount).equals(0) o(methods.onremove.callCount).equals(0) hooks.forEach(function(hook) { @@ -717,7 +667,6 @@ o.spec("component", function() { o(methods.oncreate.callCount).equals(1) o(methods.onbeforeupdate.callCount).equals(0) o(methods.onupdate.callCount).equals(0) - o(methods.onbeforeremove.callCount).equals(0) o(methods.onremove.callCount).equals(0) hooks.forEach(function(hook) { @@ -731,7 +680,6 @@ o.spec("component", function() { o(methods.oncreate.callCount).equals(1) o(methods.onbeforeupdate.callCount).equals(1) o(methods.onupdate.callCount).equals(1) - o(methods.onbeforeremove.callCount).equals(0) o(methods.onremove.callCount).equals(0) hooks.forEach(function(hook) { @@ -745,7 +693,6 @@ o.spec("component", function() { o(methods.oncreate.callCount).equals(1) o(methods.onbeforeupdate.callCount).equals(1) o(methods.onupdate.callCount).equals(1) - o(methods.onbeforeremove.callCount).equals(1) o(methods.onremove.callCount).equals(1) hooks.forEach(function(hook) { @@ -762,7 +709,7 @@ o.spec("component", function() { var attrs = {} var hooks = [ "oninit", "oncreate", "onbeforeupdate", - "onupdate", "onbeforeremove", "onremove" + "onupdate", "onremove" ] hooks.forEach(function(hook) { attrs[hook] = o.spy(function(vnode){ @@ -789,7 +736,6 @@ o.spec("component", function() { o(methods.oncreate.args.length).equals(1) o(methods.onbeforeupdate.args.length).equals(2) o(methods.onupdate.args.length).equals(1) - o(methods.onbeforeremove.args.length).equals(1) o(methods.onremove.args.length).equals(1) hooks.forEach(function(hook) { diff --git a/render/tests/test-onbeforeremove.js b/render/tests/test-onbeforeremove.js deleted file mode 100644 index e99aa3d83..000000000 --- a/render/tests/test-onbeforeremove.js +++ /dev/null @@ -1,173 +0,0 @@ -"use strict" - -var o = require("ospec") -var components = require("../../test-utils/components") -var domMock = require("../../test-utils/domMock") -var render = require("../../render/render") -var m = require("../../render/hyperscript") - -o.spec("onbeforeremove", function() { - var $window, root - o.beforeEach(function() { - $window = domMock() - root = $window.document.createElement("div") - }) - - o("does not call onbeforeremove when creating", function() { - var create = o.spy() - var vnode = m("div", {onbeforeremove: create}) - - render(root, vnode) - - o(create.callCount).equals(0) - }) - o("does not call onbeforeremove when updating", function() { - var create = o.spy() - var update = o.spy() - var vnode = m("div", {onbeforeremove: create}) - var updated = m("div", {onbeforeremove: update}) - - render(root, vnode) - render(root, updated) - - o(create.callCount).equals(0) - o(update.callCount).equals(0) - }) - o("calls onbeforeremove when removing element", function() { - var onbeforeremove = o.spy() - var vnode = m("div", {onbeforeremove}) - - render(root, [vnode]) - var firstChild = root.firstChild - o(firstChild).notEquals(null) - render(root, []) - - o(onbeforeremove.callCount).equals(1) - o(onbeforeremove.this).equals(vnode.state) - o(onbeforeremove.this).satisfies((v) => ({ - pass: v !== null && typeof v === "object", - message: "`onbeforeremove` should be called with an object", - })) - o(onbeforeremove.args[0]).equals(vnode) - o(root.childNodes.length).equals(0) - o(vnode.dom).equals(firstChild) - }) - o("calls onbeforeremove when removing fragment", function() { - var onbeforeremove = o.spy() - var vnode = m.fragment({onbeforeremove}, m("div")) - - render(root, [vnode]) - var firstChild = root.firstChild - o(firstChild).notEquals(null) - render(root, []) - - o(onbeforeremove.callCount).equals(1) - o(onbeforeremove.this).equals(vnode.state) - o(onbeforeremove.this).satisfies((v) => ({ - pass: v !== null && typeof v === "object", - message: "`onbeforeremove` should be called with an object", - })) - o(onbeforeremove.args[0]).equals(vnode) - o(root.childNodes.length).equals(0) - o(vnode.dom).equals(firstChild) - }) - o("calls onremove after onbeforeremove returns", function() { - var callOrder = [] - var onbeforeremove = o.spy(() => { callOrder.push("onbeforeremove") }) - var spy = o.spy(() => { callOrder.push("onremove") }) - var vnode = m.fragment({onbeforeremove: onbeforeremove, onremove: spy}, "a") - - render(root, [vnode]) - var firstChild = root.firstChild - o(firstChild).notEquals(null) - render(root, []) - - o(onbeforeremove.callCount).equals(1) - o(onbeforeremove.this).equals(vnode.state) - o(onbeforeremove.this).satisfies((v) => ({ - pass: v !== null && typeof v === "object", - message: "`onbeforeremove` should be called with an object", - })) - o(onbeforeremove.args[0]).equals(vnode) - o(root.childNodes.length).equals(0) - o(vnode.dom).equals(firstChild) - o(spy.callCount).equals(1) - - o(callOrder).deepEquals(["onbeforeremove", "onremove"]) - }) - o("calls onremove after onbeforeremove resolves", function() { - var removed = Promise.resolve() - var onbeforeremove = o.spy(() => removed) - var spy = o.spy() - var vnode = m.fragment({onbeforeremove: onbeforeremove, onremove: spy}, "a") - - render(root, [vnode]) - var firstChild = root.firstChild - o(firstChild).notEquals(null) - render(root, []) - - o(onbeforeremove.callCount).equals(1) - o(onbeforeremove.this).equals(vnode.state) - o(onbeforeremove.this).satisfies((v) => ({ - pass: v !== null && typeof v === "object", - message: "`onbeforeremove` should be called with an object", - })) - o(onbeforeremove.args[0]).equals(vnode) - o(root.childNodes.length).equals(1) - o(vnode.dom).equals(firstChild) - o(root.firstChild).equals(firstChild) - o(spy.callCount).equals(0) - - return removed.then(() => { - o(onbeforeremove.callCount).equals(1) - o(spy.callCount).equals(1) - }) - }) - o("does not set onbeforeremove as an event handler", function() { - var remove = o.spy() - var vnode = m("div", {onbeforeremove: remove}) - - render(root, vnode) - - o(vnode.dom.onbeforeremove).equals(undefined) - o(vnode.dom.attributes["onbeforeremove"]).equals(undefined) - }) - o("does not leave elements out of order during removal", function() { - var removed = Promise.resolve() - var vnodes = [ - m.key(1, m("div", {onbeforeremove: () => removed})), - m.key(2, m("span")), - ] - var updated = [m.key(2, m("span"))] - - render(root, vnodes) - render(root, updated) - - o(root.childNodes.length).equals(2) - o(root.firstChild.nodeName).equals("DIV") - - return removed.then(() => { - o(root.childNodes.length).equals(1) - o(root.firstChild.nodeName).equals("SPAN") - }) - }) - components.forEach(function(cmp){ - o.spec(cmp.kind, function(){ - var createComponent = cmp.create - o("finalizes the remove phase asynchronously when promise is returned synchronously from both attrs- and tag.onbeforeremove", function() { - var removed = Promise.resolve() - var onremove = o.spy() - var component = createComponent({ - onbeforeremove: () => removed, - onremove: onremove, - view: function() {}, - }) - render(root, [m(component, {onbeforeremove: () => removed, onremove: onremove})]) - render(root, []) - return removed.then(() => { - o(onremove.callCount).equals(2) // once for `tag`, once for `attrs` - }) - }) - }) - }) -}) diff --git a/render/tests/test-onremove.js b/render/tests/test-onremove.js index ca6e7bd25..7d51f0465 100644 --- a/render/tests/test-onremove.js +++ b/render/tests/test-onremove.js @@ -180,224 +180,6 @@ o.spec("onremove", function() { o(vnode.dom).notEquals(updated.dom) // this used to be a recycling pool test o(onremove.callCount).equals(1) }) - // Warning: this test is complicated because it's replicating a race condition. - o("removes correct nodes in fragment when child delays removal, parent removes, then child resolves", function () { - var resumeAttr1, resumeMethod1, resumeAttr2, resumeMethod2 - var attrRemoved1 = new Promise((resolve) => resumeAttr1 = resolve) - var methodRemoved1 = new Promise((resolve) => resumeMethod1 = resolve) - var attrRemoved2 = new Promise((resolve) => resumeAttr2 = resolve) - var methodRemoved2 = new Promise((resolve) => resumeMethod2 = resolve) - var calls = [] - - var methodCalled = false - var C = createComponent({ - view: (v) => v.children, - onremove() { calls.push("component method onremove") }, - onbeforeremove() { - calls.push("component method onbeforeremove") - if (methodCalled) return methodRemoved2 - methodCalled = true - return methodRemoved1 - }, - }) - - render(root, m("div", m.fragment({onremove() { calls.push("parent onremove") }}, - m("a", {onremove() { calls.push("child sync onremove") }}), - m(C, { - onbeforeremove() { calls.push("component attr onbeforeremove"); return attrRemoved1 }, - onremove() { calls.push("component attr onremove") }, - }, m("span")) - ))) - - o(calls).deepEquals([]) - o(root.childNodes.length).equals(1) - o(root.childNodes[0].nodeName).equals("DIV") - o(root.childNodes[0].childNodes.length).equals(2) - o(root.childNodes[0].childNodes[0].nodeName).equals("A") - o(root.childNodes[0].childNodes[1].nodeName).equals("SPAN") - - render(root, m("div", m.fragment({onremove() { calls.push("parent onremove") }}, - m("a", {onremove() { calls.push("child sync onremove") }}) - ))) - - o(calls).deepEquals([ - "component method onbeforeremove", - "component attr onbeforeremove", - ]) - o(root.childNodes.length).equals(1) - o(root.childNodes[0].nodeName).equals("DIV") - o(root.childNodes[0].childNodes.length).equals(2) - o(root.childNodes[0].childNodes[0].nodeName).equals("A") - o(root.childNodes[0].childNodes[1].nodeName).equals("SPAN") - var firstRemoved = root.childNodes[0].childNodes[1] - - render(root, m("div", m.fragment({onremove() { calls.push("parent onremove") }}, - m("a", {onremove() { calls.push("child sync onremove") }}), - m(C, { - onbeforeremove() { calls.push("component attr onbeforeremove"); return attrRemoved2 }, - onremove() { calls.push("component attr onremove") }, - }, m("span")) - ))) - - o(calls).deepEquals([ - "component method onbeforeremove", - "component attr onbeforeremove", - ]) - o(root.childNodes.length).equals(1) - o(root.childNodes[0].nodeName).equals("DIV") - o(root.childNodes[0].childNodes.length).equals(3) - o(root.childNodes[0].childNodes[0].nodeName).equals("A") - o(root.childNodes[0].childNodes[1]).equals(firstRemoved) - o(root.childNodes[0].childNodes[2].nodeName).equals("SPAN") - var secondRemoved = root.childNodes[0].childNodes[2] - - render(root, m("div")) - - o(calls).deepEquals([ - "component method onbeforeremove", - "component attr onbeforeremove", - "child sync onremove", - "component method onbeforeremove", - "component attr onbeforeremove", - ]) - o(root.childNodes.length).equals(1) - o(root.childNodes[0].nodeName).equals("DIV") - o(root.childNodes[0].childNodes.length).equals(2) - o(root.childNodes[0].childNodes[0]).equals(firstRemoved) - o(root.childNodes[0].childNodes[1]).equals(secondRemoved) - - render(root, m("div", m.fragment({onremove() { calls.push("unexpected parent onremove") }}, - m("a", {onremove() { calls.push("unexpected child sync onremove") }}), - m(C, { - onbeforeremove() { calls.push("unexpected component attr onbeforeremove") }, - onremove() { calls.push("unexpected component attr onremove") }, - }, m("span")) - ))) - - // No change - o(calls).deepEquals([ - "component method onbeforeremove", - "component attr onbeforeremove", - "child sync onremove", - "component method onbeforeremove", - "component attr onbeforeremove", - ]) - o(root.childNodes.length).equals(1) - o(root.childNodes[0].nodeName).equals("DIV") - o(root.childNodes[0].childNodes.length).equals(4) - o(root.childNodes[0].childNodes[0]).equals(firstRemoved) - o(root.childNodes[0].childNodes[1]).equals(secondRemoved) - o(root.childNodes[0].childNodes[2].nodeName).equals("A") - o(root.childNodes[0].childNodes[3].nodeName).equals("SPAN") - - render(root, m("div", m.fragment({onremove() { calls.push("unexpected parent onremove") }}, - m("a", {onremove() { calls.push("unexpected child sync onremove") }}), - m(C, { - onbeforeremove() { calls.push("unexpected component attr onbeforeremove") }, - onremove() { calls.push("unexpected component attr onremove") }, - }, m("span")) - ))) - - // No change - o(calls).deepEquals([ - "component method onbeforeremove", - "component attr onbeforeremove", - "child sync onremove", - "component method onbeforeremove", - "component attr onbeforeremove", - ]) - o(root.childNodes.length).equals(1) - o(root.childNodes[0].nodeName).equals("DIV") - o(root.childNodes[0].childNodes.length).equals(4) - o(root.childNodes[0].childNodes[0]).equals(firstRemoved) - o(root.childNodes[0].childNodes[1]).equals(secondRemoved) - o(root.childNodes[0].childNodes[2].nodeName).equals("A") - o(root.childNodes[0].childNodes[3].nodeName).equals("SPAN") - - resumeAttr1() - - return attrRemoved1 - .then(() => { - // No change - o(calls).deepEquals([ - "component method onbeforeremove", - "component attr onbeforeremove", - "child sync onremove", - "component method onbeforeremove", - "component attr onbeforeremove", - ]) - o(root.childNodes.length).equals(1) - o(root.childNodes[0].nodeName).equals("DIV") - o(root.childNodes[0].childNodes.length).equals(4) - o(root.childNodes[0].childNodes[0]).equals(firstRemoved) - o(root.childNodes[0].childNodes[1]).equals(secondRemoved) - o(root.childNodes[0].childNodes[2].nodeName).equals("A") - o(root.childNodes[0].childNodes[3].nodeName).equals("SPAN") - - resumeAttr2() - return attrRemoved2 - }) - .then(() => { - // No change - o(calls).deepEquals([ - "component method onbeforeremove", - "component attr onbeforeremove", - "child sync onremove", - "component method onbeforeremove", - "component attr onbeforeremove", - ]) - o(root.childNodes.length).equals(1) - o(root.childNodes[0].nodeName).equals("DIV") - o(root.childNodes[0].childNodes.length).equals(4) - o(root.childNodes[0].childNodes[0]).equals(firstRemoved) - o(root.childNodes[0].childNodes[1]).equals(secondRemoved) - o(root.childNodes[0].childNodes[2].nodeName).equals("A") - o(root.childNodes[0].childNodes[3].nodeName).equals("SPAN") - - resumeMethod1() - return methodRemoved1 - }) - .then(() => { - // No change - o(calls).deepEquals([ - "component method onbeforeremove", - "component attr onbeforeremove", - "child sync onremove", - "component method onbeforeremove", - "component attr onbeforeremove", - "component method onremove", - "component attr onremove", - ]) - o(root.childNodes.length).equals(1) - o(root.childNodes[0].nodeName).equals("DIV") - o(root.childNodes[0].childNodes.length).equals(3) - o(root.childNodes[0].childNodes[0]).equals(secondRemoved) - o(root.childNodes[0].childNodes[1].nodeName).equals("A") - o(root.childNodes[0].childNodes[2].nodeName).equals("SPAN") - - resumeMethod2() - return methodRemoved2 - }) - .then(() => { - // Now, everything should be cleaned up - o(calls).deepEquals([ - "component method onbeforeremove", - "component attr onbeforeremove", - "child sync onremove", - "component method onbeforeremove", - "component attr onbeforeremove", - "component method onremove", - "component attr onremove", - "component method onremove", - "component attr onremove", - ]) - o(root.childNodes.length).equals(1) - o(root.childNodes[0].nodeName).equals("DIV") - o(root.childNodes[0].childNodes.length).equals(2) - o(root.childNodes[0].childNodes[0].nodeName).equals("A") - o(root.childNodes[0].childNodes[1].nodeName).equals("SPAN") - }) - }) }) }) }) diff --git a/render/tests/test-render.js b/render/tests/test-render.js index d198ce3f4..12c20853e 100644 --- a/render/tests/test-render.js +++ b/render/tests/test-render.js @@ -322,9 +322,6 @@ o.spec("render", function() { updated = true try {render(root, m(A))} catch (e) {thrown.push("onupdate")} }, - onbeforeremove: function() { - try {render(root, m(A))} catch (e) {thrown.push("onbeforeremove")} - }, onremove: function() { try {render(root, m(A))} catch (e) {thrown.push("onremove")} }, @@ -359,7 +356,6 @@ o.spec("render", function() { "onbeforeupdate", "view", "onupdate", - "onbeforeremove", "onremove", ]) }) diff --git a/util/censor.js b/util/censor.js index 8a548ab2a..aafcd2281 100644 --- a/util/censor.js +++ b/util/censor.js @@ -13,7 +13,7 @@ // const hasOwn = require("./hasOwn") // const magic = [ // "key", "oninit", "oncreate", "onbeforeupdate", "onupdate", -// "onbeforeremove", "onremove", +// "onremove", // ] // module.exports = (attrs, extras) => { // const result = Object.assign(Object.create(null), attrs) @@ -24,7 +24,7 @@ // ``` var hasOwn = require("./hasOwn") -var magic = new Set(["oninit", "oncreate", "onbeforeupdate", "onupdate", "onbeforeremove", "onremove"]) +var magic = new Set(["oninit", "oncreate", "onbeforeupdate", "onupdate", "onremove"]) module.exports = function(attrs, extras) { var result = {} diff --git a/util/tests/test-censor.js b/util/tests/test-censor.js index a5e054916..3ed8017aa 100644 --- a/util/tests/test-censor.js +++ b/util/tests/test-censor.js @@ -27,7 +27,6 @@ o.spec("censor", function() { oncreate: "test", onbeforeupdate: "test", onupdate: "test", - onbeforeremove: "test", onremove: "test", } var censored = censor(original) @@ -42,7 +41,6 @@ o.spec("censor", function() { oncreate: "test", onbeforeupdate: "test", onupdate: "test", - onbeforeremove: "test", onremove: "test", } censor(original) @@ -53,7 +51,6 @@ o.spec("censor", function() { oncreate: "test", onbeforeupdate: "test", onupdate: "test", - onbeforeremove: "test", onremove: "test", }) }) @@ -82,7 +79,6 @@ o.spec("censor", function() { oncreate: "test", onbeforeupdate: "test", onupdate: "test", - onbeforeremove: "test", onremove: "test", } var censored = censor(original, null) @@ -97,7 +93,6 @@ o.spec("censor", function() { oncreate: "test", onbeforeupdate: "test", onupdate: "test", - onbeforeremove: "test", onremove: "test", } censor(original, null) @@ -108,7 +103,6 @@ o.spec("censor", function() { oncreate: "test", onbeforeupdate: "test", onupdate: "test", - onbeforeremove: "test", onremove: "test", }) }) @@ -137,7 +131,6 @@ o.spec("censor", function() { oncreate: "test", onbeforeupdate: "test", onupdate: "test", - onbeforeremove: "test", onremove: "test", } var censored = censor(original, ["extra"]) @@ -152,7 +145,6 @@ o.spec("censor", function() { oncreate: "test", onbeforeupdate: "test", onupdate: "test", - onbeforeremove: "test", onremove: "test", } censor(original, ["extra"]) @@ -163,7 +155,6 @@ o.spec("censor", function() { oncreate: "test", onbeforeupdate: "test", onupdate: "test", - onbeforeremove: "test", onremove: "test", }) }) @@ -202,7 +193,6 @@ o.spec("censor", function() { oncreate: "test", onbeforeupdate: "test", onupdate: "test", - onbeforeremove: "test", onremove: "test", } var censored = censor(original, ["extra"]) @@ -218,7 +208,6 @@ o.spec("censor", function() { oncreate: "test", onbeforeupdate: "test", onupdate: "test", - onbeforeremove: "test", onremove: "test", } censor(original, ["extra"]) @@ -230,7 +219,6 @@ o.spec("censor", function() { oncreate: "test", onbeforeupdate: "test", onupdate: "test", - onbeforeremove: "test", onremove: "test", }) }) From 60de2e109703341f46f2c401e6eab4c370f0f4f2 Mon Sep 17 00:00:00 2001 From: Claudia Meadows Date: Thu, 3 Oct 2024 17:04:50 -0700 Subject: [PATCH 31/95] `onbeforeremove` -> `m.retain()` --- api/tests/test-mountRedraw.js | 32 ++- render/hyperscript.js | 2 + render/render.js | 44 ++-- render/tests/test-component.js | 35 +-- render/tests/test-onbeforeupdate.js | 368 ---------------------------- render/tests/test-render.js | 50 ++-- render/tests/test-retain.js | 107 ++++++++ util/censor.js | 4 +- util/tests/test-censor.js | 12 - 9 files changed, 175 insertions(+), 479 deletions(-) delete mode 100644 render/tests/test-onbeforeupdate.js create mode 100644 render/tests/test-retain.js diff --git a/api/tests/test-mountRedraw.js b/api/tests/test-mountRedraw.js index 3cdd658e4..1401c1463 100644 --- a/api/tests/test-mountRedraw.js +++ b/api/tests/test-mountRedraw.js @@ -27,7 +27,7 @@ o.spec("mount/redraw", function() { }) var Inline = () => ({ - view: ({attrs}) => attrs.view(), + view: (vnode, old) => vnode.attrs.view(vnode, old), }) o("shouldn't error if there are no renderers", function() { @@ -237,8 +237,10 @@ o.spec("mount/redraw", function() { var root3 = $document.createElement("div") m.mount(root1, () => h(Inline, { - onbeforeupdate() { m.mount(root2, null) }, - view() { calls.push("root1") }, + view(_, old) { + if (old) m.mount(root2, null) + calls.push("root1") + }, })) m.mount(root2, () => { calls.push("root2") }) m.mount(root3, () => { calls.push("root3") }) @@ -261,8 +263,10 @@ o.spec("mount/redraw", function() { m.mount(root1, () => { calls.push("root1") }) m.mount(root2, () => h(Inline, { - onbeforeupdate() { m.mount(root1, null) }, - view() { calls.push("root2") }, + view(_, old) { + if (old) m.mount(root1, null) + calls.push("root2") + }, })) m.mount(root3, () => { calls.push("root3") }) o(calls).deepEquals([ @@ -285,8 +289,10 @@ o.spec("mount/redraw", function() { m.mount(root1, () => { calls.push("root1") }) m.mount(root2, () => h(Inline, { - onbeforeupdate() { m.mount(root1, null); throw "fail" }, - view() { calls.push("root2") }, + view(_, old) { + if (old) { m.mount(root1, null); throw "fail" } + calls.push("root2") + }, })) m.mount(root3, () => { calls.push("root3") }) o(calls).deepEquals([ @@ -308,10 +314,10 @@ o.spec("mount/redraw", function() { m.mount(root1, () => { calls.push("root1") }) m.mount(root2, () => h(Inline, { - onbeforeupdate() { - try { m.mount(root2, null) } catch (e) { calls.push([e.constructor, e.message]) } + view(_, old) { + if (old) try { m.mount(root2, null) } catch (e) { calls.push([e.constructor, e.message]) } + calls.push("root2") }, - view() { calls.push("root2") }, })) m.mount(root3, () => { calls.push("root3") }) o(calls).deepEquals([ @@ -336,10 +342,10 @@ o.spec("mount/redraw", function() { m.mount(root1, () => { calls.push("root1") }) m.mount(root2, () => h(Inline, { - onbeforeupdate() { - try { m.mount(root2, null) } catch (e) { throw [e.constructor, e.message] } + view(_, old) { + if (old) try { m.mount(root2, null) } catch (e) { throw [e.constructor, e.message] } + calls.push("root2") }, - view() { calls.push("root2") }, })) m.mount(root3, () => { calls.push("root3") }) o(calls).deepEquals([ diff --git a/render/hyperscript.js b/render/hyperscript.js index 8ea64935c..c4b8b4619 100644 --- a/render/hyperscript.js +++ b/render/hyperscript.js @@ -95,6 +95,8 @@ function m(selector, attrs, ...children) { return Vnode(selector, {}, attrs, children) } +m.retain = () => Vnode("!", undefined, undefined, undefined) + m.fragment = (...args) => m("[", ...args) // When removal is blocked, all ancestors are also blocked. This doesn't block other children, so diff --git a/render/render.js b/render/render.js index e4e633ca7..bc54f9011 100644 --- a/render/render.js +++ b/render/render.js @@ -59,6 +59,7 @@ function createNode(parent, vnode, hooks, ns, nextSibling) { if (typeof tag === "string") { if (vnode.attrs != null) initLifecycle(vnode.attrs, vnode, hooks) switch (tag) { + case "!": throw new Error("No node present to retain with `m.retain()`") case "#": createText(parent, vnode, nextSibling); break case "=": case "[": createFragment(parent, vnode, hooks, ns, nextSibling); break @@ -353,10 +354,20 @@ function updateNodes(parent, old, vnodes, hooks, nextSibling, ns) { } function updateNode(parent, old, vnode, hooks, nextSibling, ns) { var oldTag = old.tag, tag = vnode.tag - if (oldTag === tag) { + if (tag === "!") { + // If it's a retain node, transmute it into the node it's retaining. Makes it much easier + // to implement and work with. + // + // Note: this key list *must* be complete. + vnode.tag = oldTag + vnode.state = old.state + vnode.attrs = old.attrs + vnode.children = old.children + vnode.dom = old.dom + vnode.instance = old.instance + } else if (oldTag === tag) { vnode.state = old.state vnode.instance = old.instance - if (shouldNotUpdate(vnode, old)) return if (typeof oldTag === "string") { if (vnode.attrs != null) { updateLifecycle(vnode.attrs, vnode, hooks) @@ -402,7 +413,7 @@ function updateElement(old, vnode, hooks, ns) { } } function updateComponent(parent, old, vnode, hooks, nextSibling, ns) { - vnode.instance = hyperscript.normalize(callHook.call(vnode.state.view, vnode)) + vnode.instance = hyperscript.normalize(callHook.call(vnode.state.view, vnode, old)) if (vnode.instance === vnode) throw Error("A view cannot return the vnode it received as argument") updateLifecycle(vnode.state, vnode, hooks) if (vnode.attrs != null) updateLifecycle(vnode.attrs, vnode, hooks) @@ -653,7 +664,7 @@ function updateAttrs(vnode, old, attrs, ns) { } } var isAlwaysFormAttribute = new Set(["value", "checked", "selected", "selectedIndex"]) -var isSpecialAttribute = new Set(["key", "is", "oninit", "oncreate", "onupdate", "onremove", "onbeforeupdate"]) +var isSpecialAttribute = new Set(["key", "is", "oninit", "oncreate", "onupdate", "onremove"]) // Try to avoid a few browser bugs on normal elements. // var propertyMayBeBugged = new Set(["href", "list", "form", "width", "height", "type"]) var propertyMayBeBugged = new Set(["href", "list", "form", "width", "height"]) @@ -771,31 +782,6 @@ function initLifecycle(source, vnode, hooks) { function updateLifecycle(source, vnode, hooks) { if (typeof source.onupdate === "function") hooks.push(callHook.bind(source.onupdate, vnode)) } -function shouldNotUpdate(vnode, old) { - do { - if (vnode.attrs != null && typeof vnode.attrs.onbeforeupdate === "function") { - var force = callHook.call(vnode.attrs.onbeforeupdate, vnode, old) - if (force !== undefined && !force) break - } - if (typeof vnode.tag !== "string" && typeof vnode.state.onbeforeupdate === "function") { - var force = callHook.call(vnode.state.onbeforeupdate, vnode, old) - if (force !== undefined && !force) break - } - return false - } while (false); // eslint-disable-line no-constant-condition - vnode.dom = old.dom - vnode.instance = old.instance - // One would think having the actual latest attributes would be ideal, - // but it doesn't let us properly diff based on our current internal - // representation. We have to save not only the old DOM info, but also - // the attributes used to create it, as we diff *that*, not against the - // DOM directly (with a few exceptions in `setAttr`). And, of course, we - // need to save the children and text as they are conceptually not - // unlike special "attributes" internally. - vnode.attrs = old.attrs - vnode.children = old.children - return true -} var currentDOM diff --git a/render/tests/test-component.js b/render/tests/test-component.js index 594eab07c..fdca070b5 100644 --- a/render/tests/test-component.js +++ b/render/tests/test-component.js @@ -624,27 +624,17 @@ o.spec("component", function() { } var attrs = {} var hooks = [ - "oninit", "oncreate", "onbeforeupdate", + "oninit", "oncreate", "onupdate", "onremove" ] hooks.forEach(function(hook) { - if (hook === "onbeforeupdate") { - // the component's `onbeforeupdate` is called after the `attrs`' one - attrs[hook] = o.spy(function() { - o(attrs[hook].callCount).equals(methods[hook].callCount + 1)(hook) - }) - methods[hook] = o.spy(function() { - o(attrs[hook].callCount).equals(methods[hook].callCount)(hook) - }) - } else { - // the other component hooks are called before the `attrs` ones - methods[hook] = o.spy(function() { - o(attrs[hook].callCount).equals(methods[hook].callCount - 1)(hook) - }) - attrs[hook] = o.spy(function() { - o(attrs[hook].callCount).equals(methods[hook].callCount)(hook) - }) - } + // the other component hooks are called before the `attrs` ones + methods[hook] = o.spy(function() { + o(attrs[hook].callCount).equals(methods[hook].callCount - 1)(hook) + }) + attrs[hook] = o.spy(function() { + o(attrs[hook].callCount).equals(methods[hook].callCount)(hook) + }) }) var component = createComponent(methods) @@ -652,7 +642,6 @@ o.spec("component", function() { o(methods.view.callCount).equals(0) o(methods.oninit.callCount).equals(0) o(methods.oncreate.callCount).equals(0) - o(methods.onbeforeupdate.callCount).equals(0) o(methods.onupdate.callCount).equals(0) o(methods.onremove.callCount).equals(0) @@ -665,7 +654,6 @@ o.spec("component", function() { o(methods.view.callCount).equals(1) o(methods.oninit.callCount).equals(1) o(methods.oncreate.callCount).equals(1) - o(methods.onbeforeupdate.callCount).equals(0) o(methods.onupdate.callCount).equals(0) o(methods.onremove.callCount).equals(0) @@ -678,7 +666,6 @@ o.spec("component", function() { o(methods.view.callCount).equals(2) o(methods.oninit.callCount).equals(1) o(methods.oncreate.callCount).equals(1) - o(methods.onbeforeupdate.callCount).equals(1) o(methods.onupdate.callCount).equals(1) o(methods.onremove.callCount).equals(0) @@ -691,7 +678,6 @@ o.spec("component", function() { o(methods.view.callCount).equals(2) o(methods.oninit.callCount).equals(1) o(methods.oncreate.callCount).equals(1) - o(methods.onbeforeupdate.callCount).equals(1) o(methods.onupdate.callCount).equals(1) o(methods.onremove.callCount).equals(1) @@ -708,7 +694,7 @@ o.spec("component", function() { } var attrs = {} var hooks = [ - "oninit", "oncreate", "onbeforeupdate", + "oninit", "oncreate", "onupdate", "onremove" ] hooks.forEach(function(hook) { @@ -731,10 +717,9 @@ o.spec("component", function() { o(methods[hook].this).equals(methods.view.this)(hook) }) - o(methods.view.args.length).equals(1) + o(methods.view.args.length).equals(2) o(methods.oninit.args.length).equals(1) o(methods.oncreate.args.length).equals(1) - o(methods.onbeforeupdate.args.length).equals(2) o(methods.onupdate.args.length).equals(1) o(methods.onremove.args.length).equals(1) diff --git a/render/tests/test-onbeforeupdate.js b/render/tests/test-onbeforeupdate.js deleted file mode 100644 index b5857e7b9..000000000 --- a/render/tests/test-onbeforeupdate.js +++ /dev/null @@ -1,368 +0,0 @@ -"use strict" - -var o = require("ospec") -var components = require("../../test-utils/components") -var domMock = require("../../test-utils/domMock") -var render = require("../../render/render") -var m = require("../../render/hyperscript") - -o.spec("onbeforeupdate", function() { - var $window, root - o.beforeEach(function() { - $window = domMock() - root = $window.document.createElement("div") - }) - - o("prevents update in element", function() { - var onbeforeupdate = function() {return false} - var vnode = m("div", {id: "a", onbeforeupdate: onbeforeupdate}) - var updated = m("div", {id: "b", onbeforeupdate: onbeforeupdate}) - - render(root, vnode) - render(root, updated) - - o(root.firstChild.attributes["id"].value).equals("a") - }) - - o("prevents update in fragment", function() { - var onbeforeupdate = function() {return false} - var vnode = m.fragment({onbeforeupdate: onbeforeupdate}, "a") - var updated = m.fragment({onbeforeupdate: onbeforeupdate}, "b") - - render(root, vnode) - render(root, updated) - - o(root.firstChild.nodeValue).equals("a") - }) - - o("does not prevent update if returning true", function() { - var onbeforeupdate = function() {return true} - var vnode = m("div", {id: "a", onbeforeupdate: onbeforeupdate}) - var updated = m("div", {id: "b", onbeforeupdate: onbeforeupdate}) - - render(root, vnode) - render(root, updated) - - o(root.firstChild.attributes["id"].value).equals("b") - }) - - o("accepts arguments for comparison", function() { - var count = 0 - var vnode = m("div", {id: "a", onbeforeupdate: onbeforeupdate}) - var updated = m("div", {id: "b", onbeforeupdate: onbeforeupdate}) - - render(root, vnode) - render(root, updated) - - function onbeforeupdate(vnode, old) { - count++ - - o(old.attrs.id).equals("a") - o(vnode.attrs.id).equals("b") - - return old.attrs.id !== vnode.attrs.id - } - - o(count).equals(1) - o(root.firstChild.attributes["id"].value).equals("b") - }) - - o("is not called on creation", function() { - var count = 0 - var vnode = m("div", {id: "a", onbeforeupdate: onbeforeupdate}) - - render(root, vnode) - - function onbeforeupdate() { - count++ - return true - } - - o(count).equals(0) - }) - - o("is called only once on update", function() { - var count = 0 - var vnode = m("div", {id: "a", onbeforeupdate: onbeforeupdate}) - var updated = m("div", {id: "b", onbeforeupdate: onbeforeupdate}) - - render(root, vnode) - render(root, updated) - - function onbeforeupdate() { - count++ - return true - } - - o(count).equals(1) - }) - - components.forEach(function(cmp){ - o.spec(cmp.kind, function(){ - var createComponent = cmp.create - - o("prevents update in component", function() { - var component = createComponent({ - onbeforeupdate: function() {return false}, - view: function(vnode) { - return m("div", vnode.children) - }, - }) - var vnode = m(component, "a") - var updated = m(component, "b") - - render(root, vnode) - render(root, updated) - - o(root.firstChild.firstChild.nodeValue).equals("a") - }) - - o("prevents update if returning false in component and false in vnode", function() { - var component = createComponent({ - onbeforeupdate: function() {return false}, - view: function(vnode) { - return m("div", {id: vnode.attrs.id}) - }, - }) - var vnode = m(component, {id: "a", onbeforeupdate: function() {return false}}) - var updated = m(component, {id: "b", onbeforeupdate: function() {return false}}) - - render(root, vnode) - render(root, updated) - - o(root.firstChild.attributes["id"].value).equals("a") - }) - - o("does not prevent update if returning true in component and true in vnode", function() { - var component = createComponent({ - onbeforeupdate: function() {return true}, - view: function(vnode) { - return m("div", {id: vnode.attrs.id}) - }, - }) - var vnode = m(component, {id: "a", onbeforeupdate: function() {return true}}) - var updated = m(component, {id: "b", onbeforeupdate: function() {return true}}) - - render(root, vnode) - render(root, updated) - - o(root.firstChild.attributes["id"].value).equals("b") - }) - - o("prevents update if returning false in component but true in vnode", function() { - var component = createComponent({ - onbeforeupdate: function() {return false}, - view: function(vnode) { - return m("div", {id: vnode.attrs.id}) - }, - }) - var vnode = m(component, {id: "a", onbeforeupdate: function() {return true}}) - var updated = m(component, {id: "b", onbeforeupdate: function() {return true}}) - - render(root, vnode) - render(root, updated) - - o(root.firstChild.attributes["id"].value).equals("a") - }) - - o("prevents update if returning true in component but false in vnode", function() { - var component = createComponent({ - onbeforeupdate: function() {return true}, - view: function(vnode) { - return m("div", {id: vnode.attrs.id}) - }, - }) - var vnode = m(component, {id: "a", onbeforeupdate: function() {return false}}) - var updated = m(component, {id: "b", onbeforeupdate: function() {return false}}) - - render(root, vnode) - render(root, updated) - - o(root.firstChild.attributes["id"].value).equals("a") - }) - - o("does not prevent update if returning true from component", function() { - var component = createComponent({ - onbeforeupdate: function() {return true}, - view: function(vnode) { - return m("div", vnode.attrs) - }, - }) - var vnode = m(component, {id: "a"}) - var updated = m(component, {id: "b"}) - - render(root, vnode) - render(root, updated) - - o(root.firstChild.attributes["id"].value).equals("b") - }) - - o("accepts arguments for comparison in component", function() { - var component = createComponent({ - onbeforeupdate: onbeforeupdate, - view: function(vnode) { - return m("div", vnode.attrs) - }, - }) - var count = 0 - var vnode = m(component, {id: "a"}) - var updated = m(component, {id: "b"}) - - render(root, vnode) - render(root, updated) - - function onbeforeupdate(vnode, old) { - count++ - - o(old.attrs.id).equals("a") - o(vnode.attrs.id).equals("b") - - return old.attrs.id !== vnode.attrs.id - } - - o(count).equals(1) - o(root.firstChild.attributes["id"].value).equals("b") - }) - - o("is not called on component creation", function() { - var component = createComponent({ - onbeforeupdate: onbeforeupdate, - view: function(vnode) { - return m("div", vnode.attrs) - }, - }) - - var count = 0 - var vnode = m(component, {id: "a"}) - - render(root, vnode) - - function onbeforeupdate() { - count++ - return true - } - - o(count).equals(0) - }) - - o("is called only once on component update", function() { - var component = createComponent({ - onbeforeupdate: onbeforeupdate, - view: function(vnode) { - return m("div", vnode.attrs) - }, - }) - - var count = 0 - var vnode = m(component, {id: "a"}) - var updated = m(component, {id: "b"}) - - render(root, vnode) - render(root, updated) - - function onbeforeupdate() { - count++ - return true - } - - o(count).equals(1) - }) - }) - }) - - // https://github.com/MithrilJS/mithril.js/issues/2067 - o.spec("after prevented update", function() { - o("old attributes are retained", function() { - render(root, [ - m("div", {"id": "foo", onbeforeupdate: function() { return true }}) - ]) - render(root, [ - m("div", {"id": "bar", onbeforeupdate: function() { return false }}) - ]) - render(root, [ - m("div", {"id": "bar", onbeforeupdate: function() { return true }}) - ]) - o(root.firstChild.attributes["id"].value).equals("bar") - }) - o("old children is retained", function() { - render(root, - m("div", {onbeforeupdate: function() { return true }}, - m("div") - ) - ) - render(root, - m("div", {onbeforeupdate: function() { return false }}, - m("div", m("div")) - ) - ) - render(root, - m("div", {onbeforeupdate: function() { return true }}, - m("div", m("div")) - ) - ) - o(root.firstChild.firstChild.childNodes.length).equals(1) - }) - o("old text is retained", function() { - render(root, - m("div", {onbeforeupdate: function() { return true }}, - m("div") - ) - ) - render(root, - m("div", {onbeforeupdate: function() { return false }}, - m("div", "foo") - ) - ) - render(root, - m("div", {onbeforeupdate: function() { return true }}, - m("div", "foo") - ) - ) - o(root.firstChild.firstChild.firstChild.nodeValue).equals("foo") - }) - o("updating component children doesn't error", function() { - var Child = () => ({ - view(v) { - return m("div", - v.attrs.foo ? m("div") : null - ) - } - }) - - render(root, - m("div", {onbeforeupdate: function() { return true }}, - m(Child, {foo: false}) - ) - ) - render(root, - m("div", {onbeforeupdate: function() { return false }}, - m(Child, {foo: false}) - ) - ) - render(root, - m("div", {onbeforeupdate: function() { return true }}, - m(Child, {foo: true}) - ) - ) - o(root.firstChild.firstChild.childNodes.length).equals(1) - }) - o("adding dom children doesn't error", function() { - render(root, - m("div", {onbeforeupdate: function() { return true }}, - m("div") - ) - ) - render(root, - m("div", {onbeforeupdate: function() { return false }}, - m("div") - ) - ) - render(root, - m("div", {onbeforeupdate: function() { return true }}, - m("div", m("div")) - ) - ) - o(root.firstChild.firstChild.childNodes.length).equals(1) - }) - }) -}) diff --git a/render/tests/test-render.js b/render/tests/test-render.js index 12c20853e..3d2c138ff 100644 --- a/render/tests/test-render.js +++ b/render/tests/test-render.js @@ -60,75 +60,71 @@ o.spec("render", function() { o("does not try to re-initialize a constructible component whose view has thrown", function() { var oninit = o.spy() - var onbeforeupdate = o.spy() + var view = o.spy(() => { throw new Error("error") }) function A(){} - A.prototype.view = function() {throw new Error("error")} + A.prototype.view = view A.prototype.oninit = oninit - A.prototype.onbeforeupdate = onbeforeupdate var throwCount = 0 try {render(root, m(A))} catch (e) {throwCount++} o(throwCount).equals(1) o(oninit.callCount).equals(1) - o(onbeforeupdate.callCount).equals(0) + o(view.callCount).equals(1) try {render(root, m(A))} catch (e) {throwCount++} o(throwCount).equals(1) o(oninit.callCount).equals(1) - o(onbeforeupdate.callCount).equals(0) + o(view.callCount).equals(1) }) o("does not try to re-initialize a constructible component whose oninit has thrown", function() { var oninit = o.spy(function(){throw new Error("error")}) - var onbeforeupdate = o.spy() + var view = o.spy() function A(){} - A.prototype.view = function(){} + A.prototype.view = view A.prototype.oninit = oninit - A.prototype.onbeforeupdate = onbeforeupdate var throwCount = 0 try {render(root, m(A))} catch (e) {throwCount++} o(throwCount).equals(1) o(oninit.callCount).equals(1) - o(onbeforeupdate.callCount).equals(0) + o(view.callCount).equals(0) try {render(root, m(A))} catch (e) {throwCount++} o(throwCount).equals(1) o(oninit.callCount).equals(1) - o(onbeforeupdate.callCount).equals(0) + o(view.callCount).equals(0) }) o("does not try to re-initialize a constructible component whose constructor has thrown", function() { var oninit = o.spy() - var onbeforeupdate = o.spy() + var view = o.spy() function A(){throw new Error("error")} - A.prototype.view = function() {} + A.prototype.view = view A.prototype.oninit = oninit - A.prototype.onbeforeupdate = onbeforeupdate var throwCount = 0 try {render(root, m(A))} catch (e) {throwCount++} o(throwCount).equals(1) o(oninit.callCount).equals(0) - o(onbeforeupdate.callCount).equals(0) + o(view.callCount).equals(0) try {render(root, m(A))} catch (e) {throwCount++} o(throwCount).equals(1) o(oninit.callCount).equals(0) - o(onbeforeupdate.callCount).equals(0) + o(view.callCount).equals(0) }) o("does not try to re-initialize a closure component whose view has thrown", function() { var oninit = o.spy() - var onbeforeupdate = o.spy() + var view = o.spy(() => { throw new Error("error") }) function A() { return { - view: function() {throw new Error("error")}, + view: view, oninit: oninit, - onbeforeupdate: onbeforeupdate } } var throwCount = 0 @@ -136,22 +132,21 @@ o.spec("render", function() { o(throwCount).equals(1) o(oninit.callCount).equals(1) - o(onbeforeupdate.callCount).equals(0) + o(view.callCount).equals(1) try {render(root, m(A))} catch (e) {throwCount++} o(throwCount).equals(1) o(oninit.callCount).equals(1) - o(onbeforeupdate.callCount).equals(0) + o(view.callCount).equals(1) }) o("does not try to re-initialize a closure component whose oninit has thrown", function() { var oninit = o.spy(function() {throw new Error("error")}) - var onbeforeupdate = o.spy() + var view = o.spy() function A() { return { - view: function() {}, + view: view, oninit: oninit, - onbeforeupdate: onbeforeupdate } } var throwCount = 0 @@ -159,13 +154,13 @@ o.spec("render", function() { o(throwCount).equals(1) o(oninit.callCount).equals(1) - o(onbeforeupdate.callCount).equals(0) + o(view.callCount).equals(0) try {render(root, m(A))} catch (e) {throwCount++} o(throwCount).equals(1) o(oninit.callCount).equals(1) - o(onbeforeupdate.callCount).equals(0) + o(view.callCount).equals(0) }) o("does not try to re-initialize a closure component whose closure has thrown", function() { function A() { @@ -314,9 +309,6 @@ o.spec("render", function() { oncreate: function() { try {render(root, m(A))} catch (e) {thrown.push("oncreate")} }, - onbeforeupdate: function() { - try {render(root, m(A))} catch (e) {thrown.push("onbeforeupdate")} - }, onupdate: function() { if (updated) return updated = true @@ -343,7 +335,6 @@ o.spec("render", function() { "oninit", "view", "oncreate", - "onbeforeupdate", "view", "onupdate", ]) @@ -353,7 +344,6 @@ o.spec("render", function() { "oninit", "view", "oncreate", - "onbeforeupdate", "view", "onupdate", "onremove", diff --git a/render/tests/test-retain.js b/render/tests/test-retain.js new file mode 100644 index 000000000..c79623dbd --- /dev/null +++ b/render/tests/test-retain.js @@ -0,0 +1,107 @@ +"use strict" + +var o = require("ospec") +var components = require("../../test-utils/components") +var domMock = require("../../test-utils/domMock") +var render = require("../render") +var m = require("../hyperscript") + +o.spec("retain", function() { + var $window, root + o.beforeEach(function() { + $window = domMock() + root = $window.document.createElement("div") + }) + + o("prevents update in element", function() { + var vnode = m("div", {id: "a"}, "b") + var updated = m.retain() + + render(root, vnode) + render(root, updated) + + o(root.firstChild.attributes["id"].value).equals("a") + o(root.firstChild.childNodes.length).equals(1) + o(root.firstChild.childNodes[0].nodeValue).equals("b") + o(updated).deepEquals(vnode) + }) + + o("prevents update in fragment", function() { + var vnode = m.fragment("a") + var updated = m.retain() + + render(root, vnode) + render(root, updated) + + o(root.firstChild.nodeValue).equals("a") + o(updated).deepEquals(vnode) + }) + + o("throws on creation", function() { + o(() => render(root, m.retain())).throws(Error) + }) + + components.forEach(function(cmp){ + o.spec(cmp.kind, function(){ + var createComponent = cmp.create + + o("prevents update in component", function() { + var component = createComponent({ + view(vnode, old) { + if (old) return m.retain() + return m("div", vnode.children) + }, + }) + var vnode = m(component, "a") + var updated = m(component, "b") + + render(root, vnode) + render(root, updated) + + o(root.firstChild.firstChild.nodeValue).equals("a") + o(updated.instance).deepEquals(vnode.instance) + }) + + o("prevents update in component and for component", function() { + var component = createComponent({ + view(vnode, old) { + if (old) return m.retain() + return m("div", {id: vnode.attrs.id}) + }, + }) + var vnode = m(component, {id: "a"}) + var updated = m.retain() + + render(root, vnode) + render(root, updated) + + o(root.firstChild.attributes["id"].value).equals("a") + o(updated).deepEquals(vnode) + }) + + o("prevents update for component but not in component", function() { + var component = createComponent({ + view(vnode) { + return m("div", {id: vnode.attrs.id}) + }, + }) + var vnode = m(component, {id: "a"}) + var updated = m.retain() + + render(root, vnode) + render(root, updated) + + o(root.firstChild.attributes["id"].value).equals("a") + o(updated).deepEquals(vnode) + }) + + o("throws if used on component creation", function() { + var component = createComponent({ + view: () => m.retain(), + }) + + o(() => render(root, m(component))).throws(Error) + }) + }) + }) +}) diff --git a/util/censor.js b/util/censor.js index aafcd2281..cc7ac9ad2 100644 --- a/util/censor.js +++ b/util/censor.js @@ -12,7 +12,7 @@ // ```js // const hasOwn = require("./hasOwn") // const magic = [ -// "key", "oninit", "oncreate", "onbeforeupdate", "onupdate", +// "key", "oninit", "oncreate", "onupdate", // "onremove", // ] // module.exports = (attrs, extras) => { @@ -24,7 +24,7 @@ // ``` var hasOwn = require("./hasOwn") -var magic = new Set(["oninit", "oncreate", "onbeforeupdate", "onupdate", "onremove"]) +var magic = new Set(["oninit", "oncreate", "onupdate", "onremove"]) module.exports = function(attrs, extras) { var result = {} diff --git a/util/tests/test-censor.js b/util/tests/test-censor.js index 3ed8017aa..b01533367 100644 --- a/util/tests/test-censor.js +++ b/util/tests/test-censor.js @@ -25,7 +25,6 @@ o.spec("censor", function() { key: "test", oninit: "test", oncreate: "test", - onbeforeupdate: "test", onupdate: "test", onremove: "test", } @@ -39,7 +38,6 @@ o.spec("censor", function() { key: "test", oninit: "test", oncreate: "test", - onbeforeupdate: "test", onupdate: "test", onremove: "test", } @@ -49,7 +47,6 @@ o.spec("censor", function() { key: "test", oninit: "test", oncreate: "test", - onbeforeupdate: "test", onupdate: "test", onremove: "test", }) @@ -77,7 +74,6 @@ o.spec("censor", function() { key: "test", oninit: "test", oncreate: "test", - onbeforeupdate: "test", onupdate: "test", onremove: "test", } @@ -91,7 +87,6 @@ o.spec("censor", function() { key: "test", oninit: "test", oncreate: "test", - onbeforeupdate: "test", onupdate: "test", onremove: "test", } @@ -101,7 +96,6 @@ o.spec("censor", function() { key: "test", oninit: "test", oncreate: "test", - onbeforeupdate: "test", onupdate: "test", onremove: "test", }) @@ -129,7 +123,6 @@ o.spec("censor", function() { key: "test", oninit: "test", oncreate: "test", - onbeforeupdate: "test", onupdate: "test", onremove: "test", } @@ -143,7 +136,6 @@ o.spec("censor", function() { key: "test", oninit: "test", oncreate: "test", - onbeforeupdate: "test", onupdate: "test", onremove: "test", } @@ -153,7 +145,6 @@ o.spec("censor", function() { key: "test", oninit: "test", oncreate: "test", - onbeforeupdate: "test", onupdate: "test", onremove: "test", }) @@ -191,7 +182,6 @@ o.spec("censor", function() { key: "test", oninit: "test", oncreate: "test", - onbeforeupdate: "test", onupdate: "test", onremove: "test", } @@ -206,7 +196,6 @@ o.spec("censor", function() { key: "test", oninit: "test", oncreate: "test", - onbeforeupdate: "test", onupdate: "test", onremove: "test", } @@ -217,7 +206,6 @@ o.spec("censor", function() { key: "test", oninit: "test", oncreate: "test", - onbeforeupdate: "test", onupdate: "test", onremove: "test", }) From 1efdb78ffcd4136ecaaf82062a024e7595d8a1ef Mon Sep 17 00:00:00 2001 From: Claudia Meadows Date: Thu, 3 Oct 2024 17:14:50 -0700 Subject: [PATCH 32/95] Re-duplicate children Dropping a premature abstraction, and I have perf concerns. This new version should hopefully avoid needing arguments allocation --- render/hyperscript.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/render/hyperscript.js b/render/hyperscript.js index c4b8b4619..15ba686ec 100644 --- a/render/hyperscript.js +++ b/render/hyperscript.js @@ -68,9 +68,6 @@ function execSelector(selector, attrs, children) { return Vnode(state.tag, {}, attrs, children) } -var resolveChildren = (...children) => - (children.length === 1 && Array.isArray(children[0]) ? children[0].slice() : children) - // Caution is advised when editing this - it's very perf-critical. It's specially designed to avoid // allocations in the fast path, especially with fragments. function m(selector, attrs, ...children) { @@ -79,9 +76,9 @@ function m(selector, attrs, ...children) { } if (attrs == null || typeof attrs === "object" && attrs.tag == null && !Array.isArray(attrs)) { - children = resolveChildren(...children) + children = children.length === 1 && Array.isArray(children[0]) ? children[0].slice() : [...children] } else { - children = resolveChildren(attrs, ...children) + children = children.length === 0 && Array.isArray(attrs) ? attrs.slice() : [attrs, ...children] attrs = undefined } @@ -103,7 +100,10 @@ m.fragment = (...args) => m("[", ...args) // this method also needs to accept an optional list of children to also keep alive while blocked. // // Note that the children are still notified of removal *immediately*. -m.key = (key, ...children) => Vnode("=", key, undefined, m.normalizeChildren(resolveChildren(...children))) +m.key = (key, ...children) => + Vnode("=", key, undefined, m.normalizeChildren( + children.length === 1 && Array.isArray(children[0]) ? children[0].slice() : [...children] + )) m.normalize = (node) => { if (node == null || typeof node === "boolean") return null From 537f828e8d8419f3a7bacfa93f22d6754dc48790 Mon Sep 17 00:00:00 2001 From: Claudia Meadows Date: Thu, 3 Oct 2024 17:52:54 -0700 Subject: [PATCH 33/95] Drop `handleEvent` method support, wait for promises in event listeners --- api/router.js | 17 ++- api/tests/test-mountRedraw.js | 6 +- api/tests/test-router.js | 33 +----- render/hyperscript.js | 8 ++ render/render.js | 32 +++--- render/tests/test-event.js | 177 ++++++++++--------------------- render/tests/test-hyperscript.js | 16 +++ render/tests/test-input.js | 14 ++- test-utils/domMock.js | 4 + 9 files changed, 119 insertions(+), 188 deletions(-) diff --git a/api/router.js b/api/router.js index 52236563a..9eb8da48f 100644 --- a/api/router.js +++ b/api/router.js @@ -1,5 +1,7 @@ "use strict" +var m = require("../render/hyperscript") + module.exports = function($window, redraw) { var mustReplace = false var routePrefix, currentUrl, currentPath, currentHref @@ -58,13 +60,8 @@ module.exports = function($window, redraw) { : { href: routePrefix + opts.href, onclick(e) { - var result if (typeof opts.onclick === "function") { - result = opts.onclick.call(e.currentTarget, e) - } else if (opts.onclick == null || typeof opts.onclick !== "object") { - // do nothing - } else if (typeof opts.onclick.handleEvent === "function") { - opts.onclick.handleEvent(e) + opts.onclick.apply(this, arguments) } // Adapted from React Router's implementation: @@ -77,8 +74,8 @@ module.exports = function($window, redraw) { // link, but right click or command-click it to copy the // link target, etc. Nope, this isn't just for blind people. if ( - // Skip if `onclick` prevented default - result !== false && !e.defaultPrevented && + // Skip if `onclick` prevented default + !e.defaultPrevented && // Ignore everything but left clicks (e.button === 0 || e.which === 0 || e.which === 1) && // Let the browser handle `target=_blank`, etc. @@ -86,9 +83,9 @@ module.exports = function($window, redraw) { // No modifier keys !e.ctrlKey && !e.metaKey && !e.shiftKey && !e.altKey ) { - e.preventDefault() - e.redraw = false set(opts.href, opts) + // Capture the event, and don't double-call `redraw`. + return m.capture(e) } }, }), diff --git a/api/tests/test-mountRedraw.js b/api/tests/test-mountRedraw.js index 1401c1463..76123c0b9 100644 --- a/api/tests/test-mountRedraw.js +++ b/api/tests/test-mountRedraw.js @@ -488,20 +488,16 @@ o.spec("mount/redraw", function() { m.mount(root, () => h("div", { oninit: oninit, onupdate: onupdate, - onclick: function(e) { - e.redraw = false - } + onclick: () => 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() { diff --git a/api/tests/test-router.js b/api/tests/test-router.js index e3a58da2a..0522074c5 100644 --- a/api/tests/test-router.js +++ b/api/tests/test-router.js @@ -417,36 +417,7 @@ o.spec("route", () => { }) }) - o("route.Link doesn't redraw on preventDefault in handleEvent", () => { - var e = $window.document.createEvent("MouseEvents") - - e.initEvent("click", true, true) - e.button = 0 - - $window.location.href = `${prefix}/` - route.init(prefix) - mountRedraw.mount(root, () => { - if (route.path === "/") { - return m("a", route.link({href: "/test", onclick: {handleEvent(e) { e.preventDefault() }}})) - } else if (route.path === "/test") { - return m("div") - } else { - throw new Error(`Unknown route: ${route.path}`) - } - }) - - o($window.location.href).equals(fullPrefix) - - root.firstChild.dispatchEvent(e) - - return waitCycles(1).then(() => { - throttleMock.fire() - o($window.location.href).equals(fullPrefix) - o(throttleMock.queueLength()).equals(0) - }) - }) - - o("route.Link doesn't redraw on return false", () => { + o("route.Link ignores `return false`", () => { var e = $window.document.createEvent("MouseEvents") e.initEvent("click", true, true) @@ -470,7 +441,7 @@ o.spec("route", () => { return waitCycles(1).then(() => { throttleMock.fire() - o($window.location.href).equals(fullPrefix) + o($window.location.href).equals(`${fullPrefix}test`) o(throttleMock.queueLength()).equals(0) }) }) diff --git a/render/hyperscript.js b/render/hyperscript.js index 15ba686ec..387e6bcab 100644 --- a/render/hyperscript.js +++ b/render/hyperscript.js @@ -92,6 +92,14 @@ function m(selector, attrs, ...children) { return Vnode(selector, {}, attrs, children) } +// Simple and sweet. Also useful for idioms like `onfoo: m.capture` to drop events without +// redrawing. +m.capture = (ev) => { + ev.preventDefault() + ev.stopPropagation() + return false +} + m.retain = () => Vnode("!", undefined, undefined, undefined) m.fragment = (...args) => m("[", ...args) diff --git a/render/render.js b/render/render.js index bc54f9011..d5333a2dd 100644 --- a/render/render.js +++ b/render/render.js @@ -726,15 +726,13 @@ function updateStyle(element, old, style) { // Here's an explanation of how this works: // 1. The event names are always (by design) prefixed by `on`. -// 2. The EventListener interface accepts either a function or an object -// with a `handleEvent` method. -// 3. The object does not inherit from `Object.prototype`, to avoid -// any potential interference with that (e.g. setters). +// 2. The EventListener interface accepts either a function or an object with a `handleEvent` method. +// 3. The object inherits from `Map`, to avoid hitting global setters. // 4. The event name is remapped to the handler before calling it. -// 5. In function-based event handlers, `ev.target === this`. We replicate -// that below. -// 6. In function-based event handlers, `return false` prevents the default -// action and stops event propagation. We replicate that below. +// 5. In function-based event handlers, `ev.currentTarget === this`. We replicate that below. +// 6. In function-based event handlers, `return false` prevents the default action and stops event +// propagation. Instead of that, we hijack it to control implicit redrawing, and let users +// return a promise that resolves to it. class EventDict extends Map { constructor() { super() @@ -743,13 +741,17 @@ class EventDict extends Map { } handleEvent(ev) { var handler = this.get(`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 (this._ && ev.redraw !== false) (0, this._)() - if (result === false) { - ev.preventDefault() - ev.stopPropagation() + if (typeof handler === "function") { + var result = handler.call(ev.currentTarget, ev) + if (result !== false) { + if (result && typeof result.then === "function") { + Promise.resolve(result).then((value) => { + if (value !== false) (0, this._)() + }) + } else { + (0, this._)() + } + } } } } diff --git a/render/tests/test-event.js b/render/tests/test-event.js index b783eb850..9f795f252 100644 --- a/render/tests/test-event.js +++ b/render/tests/test-event.js @@ -55,8 +55,9 @@ o.spec("event", function() { o(e.defaultPrevented).equals(false) }) - o("handles onclick returning false", function() { - var spyDiv = eventSpy(function() { return false }) + o("handles onclick asynchronously returning", function() { + var promise + var spyDiv = eventSpy(() => promise = Promise.resolve()) var spyParent = eventSpy() var div = m("div", {onclick: spyDiv}) var parent = m("div", {onclick: spyParent}, div) @@ -71,20 +72,50 @@ o.spec("event", function() { o(spyDiv.calls[0].type).equals("click") o(spyDiv.calls[0].target).equals(div.dom) o(spyDiv.calls[0].currentTarget).equals(div.dom) - o(spyParent.calls.length).equals(0) + o(spyParent.calls.length).equals(1) + o(spyParent.calls[0].this).equals(parent.dom) + o(spyParent.calls[0].type).equals("click") + o(spyParent.calls[0].target).equals(div.dom) + o(spyParent.calls[0].currentTarget).equals(parent.dom) o(redraw.callCount).equals(1) o(redraw.this).equals(undefined) o(redraw.args.length).equals(0) + o(e.defaultPrevented).equals(false) + + return promise.then(() => { + o(redraw.callCount).equals(2) + o(redraw.this).equals(undefined) + o(redraw.args.length).equals(0) + }) + }) + + o("handles onclick returning false", function() { + var spyDiv = eventSpy((e) => { m.capture(e); return false }) + var spyParent = eventSpy() + var div = m("div", {onclick: spyDiv}) + var parent = m("div", {onclick: spyParent}, div) + var e = $window.document.createEvent("MouseEvents") + e.initEvent("click", true, true) + + render(root, parent) + div.dom.dispatchEvent(e) + + o(spyDiv.calls.length).equals(1) + o(spyDiv.calls[0].this).equals(div.dom) + o(spyDiv.calls[0].type).equals("click") + o(spyDiv.calls[0].target).equals(div.dom) + o(spyDiv.calls[0].currentTarget).equals(div.dom) + o(spyParent.calls.length).equals(0) + o(redraw.callCount).equals(0) o(e.defaultPrevented).equals(true) }) - o("handles click EventListener object", function() { - var spyDiv = eventSpy() + o("handles onclick asynchronously returning false", function() { + var promise + var spyDiv = eventSpy((e) => { m.capture(e); return promise = Promise.resolve(false) }) var spyParent = eventSpy() - var listenerDiv = {handleEvent: spyDiv} - var listenerParent = {handleEvent: spyParent} - var div = m("div", {onclick: listenerDiv}) - var parent = m("div", {onclick: listenerParent}, div) + var div = m("div", {onclick: spyDiv}) + var parent = m("div", {onclick: spyParent}, div) var e = $window.document.createEvent("MouseEvents") e.initEvent("click", true, true) @@ -92,28 +123,24 @@ o.spec("event", function() { div.dom.dispatchEvent(e) o(spyDiv.calls.length).equals(1) - o(spyDiv.calls[0].this).equals(listenerDiv) + o(spyDiv.calls[0].this).equals(div.dom) o(spyDiv.calls[0].type).equals("click") o(spyDiv.calls[0].target).equals(div.dom) o(spyDiv.calls[0].currentTarget).equals(div.dom) - o(spyParent.calls.length).equals(1) - o(spyParent.calls[0].this).equals(listenerParent) - o(spyParent.calls[0].type).equals("click") - o(spyParent.calls[0].target).equals(div.dom) - o(spyParent.calls[0].currentTarget).equals(parent.dom) - o(redraw.callCount).equals(2) - o(redraw.this).equals(undefined) - o(redraw.args.length).equals(0) - o(e.defaultPrevented).equals(false) + o(spyParent.calls.length).equals(0) + o(redraw.callCount).equals(0) + o(e.defaultPrevented).equals(true) + + return promise.then(() => { + o(redraw.callCount).equals(0) + }) }) - o("handles click EventListener object returning false", function() { - var spyDiv = eventSpy(function() { return false }) + o("handles onclick returning false in child then bubbling to parent and not returning false", function() { + var spyDiv = eventSpy(() => false) var spyParent = eventSpy() - var listenerDiv = {handleEvent: spyDiv} - var listenerParent = {handleEvent: spyParent} - var div = m("div", {onclick: listenerDiv}) - var parent = m("div", {onclick: listenerParent}, div) + var div = m("div", {onclick: spyDiv}) + var parent = m("div", {onclick: spyParent}, div) var e = $window.document.createEvent("MouseEvents") e.initEvent("click", true, true) @@ -121,18 +148,12 @@ o.spec("event", function() { div.dom.dispatchEvent(e) o(spyDiv.calls.length).equals(1) - o(spyDiv.calls[0].this).equals(listenerDiv) + o(spyDiv.calls[0].this).equals(div.dom) o(spyDiv.calls[0].type).equals("click") o(spyDiv.calls[0].target).equals(div.dom) o(spyDiv.calls[0].currentTarget).equals(div.dom) o(spyParent.calls.length).equals(1) - o(spyParent.calls[0].this).equals(listenerParent) - o(spyParent.calls[0].type).equals("click") - o(spyParent.calls[0].target).equals(div.dom) - o(spyParent.calls[0].currentTarget).equals(parent.dom) - o(redraw.callCount).equals(2) - o(redraw.this).equals(undefined) - o(redraw.args.length).equals(0) + o(redraw.callCount).equals(1) o(e.defaultPrevented).equals(false) }) @@ -226,54 +247,6 @@ o.spec("event", function() { o(spy.callCount).equals(0) }) - o("removes EventListener object", function() { - var spy = o.spy() - var listener = {handleEvent: spy} - var vnode = m("a", {onclick: listener}) - var updated = m("a") - - render(root, vnode) - render(root, updated) - - var e = $window.document.createEvent("MouseEvents") - e.initEvent("click", true, true) - vnode.dom.dispatchEvent(e) - - o(spy.callCount).equals(0) - }) - - o("removes EventListener object when null", function() { - var spy = o.spy() - var listener = {handleEvent: spy} - var vnode = m("a", {onclick: listener}) - var updated = m("a", {onclick: null}) - - render(root, vnode) - render(root, updated) - - var e = $window.document.createEvent("MouseEvents") - e.initEvent("click", true, true) - vnode.dom.dispatchEvent(e) - - o(spy.callCount).equals(0) - }) - - o("removes EventListener object when undefined", function() { - var spy = o.spy() - var listener = {handleEvent: spy} - var vnode = m("a", {onclick: listener}) - var updated = m("a", {onclick: undefined}) - - render(root, vnode) - render(root, updated) - - var e = $window.document.createEvent("MouseEvents") - e.initEvent("click", true, true) - vnode.dom.dispatchEvent(e) - - o(spy.callCount).equals(0) - }) - o("fires onclick only once after redraw", function() { var spy = o.spy() var div = m("div", {id: "a", onclick: spy}) @@ -296,29 +269,6 @@ o.spec("event", function() { o(div.dom.attributes["id"].value).equals("b") }) - o("fires click EventListener object only once after redraw", function() { - var spy = o.spy() - var listener = {handleEvent: spy} - var div = m("div", {id: "a", onclick: listener}) - var updated = m("div", {id: "b", onclick: listener}) - var e = $window.document.createEvent("MouseEvents") - e.initEvent("click", true, true) - - render(root, div) - render(root, updated) - div.dom.dispatchEvent(e) - - o(spy.callCount).equals(1) - o(spy.this).equals(listener) - o(spy.args[0].type).equals("click") - o(spy.args[0].target).equals(div.dom) - o(redraw.callCount).equals(1) - o(redraw.this).equals(undefined) - o(redraw.args.length).equals(0) - o(div.dom).equals(updated.dom) - o(div.dom.attributes["id"].value).equals("b") - }) - o("handles ontransitionend", function() { var spy = o.spy() var div = m("div", {ontransitionend: spy}) @@ -337,25 +287,6 @@ o.spec("event", function() { o(redraw.args.length).equals(0) }) - o("handles transitionend EventListener object", function() { - var spy = o.spy() - var listener = {handleEvent: spy} - var div = m("div", {ontransitionend: listener}) - var e = $window.document.createEvent("HTMLEvents") - e.initEvent("transitionend", true, true) - - render(root, div) - div.dom.dispatchEvent(e) - - o(spy.callCount).equals(1) - o(spy.this).equals(listener) - o(spy.args[0].type).equals("transitionend") - o(spy.args[0].target).equals(div.dom) - o(redraw.callCount).equals(1) - o(redraw.this).equals(undefined) - o(redraw.args.length).equals(0) - }) - o("handles changed spy", function() { var div1 = m("div", {ontransitionend: function() {}}) diff --git a/render/tests/test-hyperscript.js b/render/tests/test-hyperscript.js index 5de0fd0b0..572392bd9 100644 --- a/render/tests/test-hyperscript.js +++ b/render/tests/test-hyperscript.js @@ -2,6 +2,7 @@ var o = require("ospec") var m = require("../../render/hyperscript") +var domMock = require("../../test-utils/domMock") o.spec("hyperscript", function() { o.spec("selector", function() { @@ -652,4 +653,19 @@ o.spec("hyperscript", function() { o(vnode.children[0]).equals("b") }) }) + + o.spec("capture", () => { + o("works", () => { + var $window = domMock() + var e = $window.document.createEvent("MouseEvents") + e.initEvent("click", true, true) + + // Only doing this for the sake of initializing the required fields in the mock. + $window.document.body.dispatchEvent(e) + + o(m.capture(e)).equals(false) + o(e.defaultPrevented).equals(true) + o(e.cancelBubble).equals(true) + }) + }) }) diff --git a/render/tests/test-input.js b/render/tests/test-input.js index d91f78e7c..15987e72d 100644 --- a/render/tests/test-input.js +++ b/render/tests/test-input.js @@ -43,8 +43,9 @@ o.spec("form inputs", function() { o("syncs input value if DOM value differs from vdom value", function() { var input = m("input", {value: "aaa", oninput: function() {}}) var updated = m("input", {value: "aaa", oninput: function() {}}) + var redraw = o.spy() - render(root, input) + render(root, input, redraw) //simulate user typing var e = $window.document.createEvent("KeyboardEvent") @@ -52,11 +53,13 @@ o.spec("form inputs", function() { input.dom.focus() input.dom.value += "a" input.dom.dispatchEvent(e) + o(redraw.callCount).equals(1) //re-render may use same vdom value as previous render call - render(root, updated) + render(root, updated, redraw) o(updated.dom.value).equals("aaa") + o(redraw.callCount).equals(1) }) o("clear element value if vdom value is set to undefined (aka removed)", function() { @@ -72,19 +75,22 @@ o.spec("form inputs", function() { o("syncs input checked attribute if DOM value differs from vdom value", function() { var input = m("input", {type: "checkbox", checked: true, onclick: function() {}}) var updated = m("input", {type: "checkbox", checked: true, onclick: function() {}}) + var redraw = o.spy() - render(root, input) + render(root, input, redraw) //simulate user clicking checkbox var e = $window.document.createEvent("MouseEvents") e.initEvent("click", true, true) input.dom.focus() input.dom.dispatchEvent(e) + o(redraw.callCount).equals(1) //re-render may use same vdom value as previous render call - render(root, updated) + render(root, updated, redraw) o(updated.dom.checked).equals(true) + o(redraw.callCount).equals(1) }) o("syncs file input value attribute if DOM value differs from vdom value and is empty", function() { diff --git a/test-utils/domMock.js b/test-utils/domMock.js index bb1b5e242..4bd2cb466 100644 --- a/test-utils/domMock.js +++ b/test-utils/domMock.js @@ -426,6 +426,10 @@ module.exports = function(options) { e.stopPropagation = function() { stopped = true } + Object.defineProperty(e, "cancelBubble", { + configurable: true, + get: function () { return stopped } + }) e.eventPhase = 1 try { for (var i = parents.length - 1; 0 <= i; i--) { From 9070429537c68a95505ab0843a2dfa5f3cb21dc7 Mon Sep 17 00:00:00 2001 From: Claudia Meadows Date: Thu, 3 Oct 2024 18:00:07 -0700 Subject: [PATCH 34/95] Revise a comment --- api/router.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/router.js b/api/router.js index 9eb8da48f..d54c444d3 100644 --- a/api/router.js +++ b/api/router.js @@ -54,8 +54,8 @@ module.exports = function($window, redraw) { // accessibility on accident. link: (opts) => ( opts.disabled - // If you *really* do want add `onclick` on a disabled link, use - // an `oncreate` hook to add it. + // If you *really* do want add `onclick` on a disabled link, spread this and add it + // explicitly in your code. ? {disabled: true, "aria-disabled": "true"} : { href: routePrefix + opts.href, From 5c2bf3170cdd8de1d5e6652de2d76d0697161439 Mon Sep 17 00:00:00 2001 From: Claudia Meadows Date: Thu, 3 Oct 2024 20:00:26 -0700 Subject: [PATCH 35/95] Drop `oninit`, replace all remaining lifecycle methods with `m.layout` If you remember v0.2, you'll recognize the API here. Thing is, this tries to be a little more rigorous with it by making it a proper vnode instead of just a magic attribute. Also, as these are the last remaining magic attributes (and `m.route.link` just returns the needed attributes itself), `m.censor` is gone. --- api/tests/test-mountRedraw.js | 72 ++--- index.js | 1 - performance/test-perf.js | 2 +- render/hyperscript.js | 4 +- render/render.js | 156 +++++------ render/tests/test-component.js | 398 ++++++++++++---------------- render/tests/test-input.js | 6 +- render/tests/test-oncreate.js | 170 ++++++------ render/tests/test-oninit.js | 205 -------------- render/tests/test-onremove.js | 113 ++------ render/tests/test-onupdate.js | 125 ++++----- render/tests/test-render.js | 254 ++++++++---------- render/tests/test-updateNodes.js | 124 ++++----- test-utils/components.js | 23 +- test-utils/tests/test-components.js | 3 +- util/censor.js | 47 ---- util/lazy.js | 3 +- util/tests/test-censor.js | 214 --------------- 18 files changed, 582 insertions(+), 1338 deletions(-) delete mode 100644 render/tests/test-oninit.js delete mode 100644 util/censor.js delete mode 100644 util/tests/test-censor.js diff --git a/api/tests/test-mountRedraw.js b/api/tests/test-mountRedraw.js index 76123c0b9..854110811 100644 --- a/api/tests/test-mountRedraw.js +++ b/api/tests/test-mountRedraw.js @@ -160,15 +160,15 @@ o.spec("mount/redraw", function() { }) o("should invoke remove callback on unmount", function() { - var spy = o.spy(() => h.fragment({onremove})) - var onremove = o.spy() + var onabort = o.spy() + var spy = o.spy(() => h.fragment(h.layout((_, signal) => { signal.onabort = onabort }))) m.mount(root, spy) o(spy.callCount).equals(1) m.mount(root) o(spy.callCount).equals(1) - o(onremove.callCount).equals(1) + o(onabort.callCount).equals(1) }) o("should stop running after unsubscribe, even if it occurs after redraw is requested", function() { @@ -396,23 +396,19 @@ o.spec("mount/redraw", function() { }) o("redraws on events", function() { - var onupdate = o.spy() - var oninit = o.spy() + var layout = o.spy() var onclick = o.spy() var e = $document.createEvent("MouseEvents") e.initEvent("click", true, true) m.mount(root, () => h("div", { - oninit: oninit, - onupdate: onupdate, onclick: onclick, - })) + }, h.layout(layout))) root.firstChild.dispatchEvent(e) - o(oninit.callCount).equals(1) - o(onupdate.callCount).equals(0) + o(layout.calls.map((c) => c.args[2])).deepEquals([true]) o(onclick.callCount).equals(1) o(onclick.this).equals(root.firstChild) @@ -421,15 +417,13 @@ o.spec("mount/redraw", function() { throttleMock.fire() - o(onupdate.callCount).equals(1) + o(layout.calls.map((c) => c.args[2])).deepEquals([true, false]) }) o("redraws several mount points on events", function() { - var onupdate0 = o.spy() - var oninit0 = o.spy() + var layout0 = o.spy() var onclick0 = o.spy() - var onupdate1 = o.spy() - var oninit1 = o.spy() + var layout1 = o.spy() var onclick1 = o.spy() var root1 = $document.createElement("div") @@ -439,22 +433,16 @@ o.spec("mount/redraw", function() { e.initEvent("click", true, true) m.mount(root1, () => h("div", { - oninit: oninit0, - onupdate: onupdate0, onclick: onclick0, - })) + }, h.layout(layout0))) - o(oninit0.callCount).equals(1) - o(onupdate0.callCount).equals(0) + o(layout0.calls.map((c) => c.args[2])).deepEquals([true]) m.mount(root2, () => h("div", { - oninit: oninit1, - onupdate: onupdate1, onclick: onclick1, - })) + }, h.layout(layout1))) - o(oninit1.callCount).equals(1) - o(onupdate1.callCount).equals(0) + o(layout1.calls.map((c) => c.args[2])).deepEquals([true]) root1.firstChild.dispatchEvent(e) o(onclick0.callCount).equals(1) @@ -462,8 +450,8 @@ o.spec("mount/redraw", function() { throttleMock.fire() - o(onupdate0.callCount).equals(1) - o(onupdate1.callCount).equals(1) + o(layout0.calls.map((c) => c.args[2])).deepEquals([true, false]) + o(layout1.calls.map((c) => c.args[2])).deepEquals([true, false]) root2.firstChild.dispatchEvent(e) @@ -472,51 +460,41 @@ o.spec("mount/redraw", function() { throttleMock.fire() - o(onupdate0.callCount).equals(2) - o(onupdate1.callCount).equals(2) + o(layout0.calls.map((c) => c.args[2])).deepEquals([true, false, false]) + o(layout1.calls.map((c) => c.args[2])).deepEquals([true, false, false]) }) 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 layout = o.spy() var e = $document.createEvent("MouseEvents") e.initEvent("click", true, true) m.mount(root, () => h("div", { - oninit: oninit, - onupdate: onupdate, onclick: () => false, - })) + }, h.layout(layout))) root.firstChild.dispatchEvent(e) - o(oninit.callCount).equals(1) + o(layout.calls.map((c) => c.args[2])).deepEquals([true]) throttleMock.fire() - o(onupdate.callCount).equals(0) + o(layout.calls.map((c) => c.args[2])).deepEquals([true]) }) o("redraws when the render function is run", function() { - var onupdate = o.spy() - var oninit = o.spy() + var layout = o.spy() - m.mount(root, () => h("div", { - oninit: oninit, - onupdate: onupdate - })) + m.mount(root, () => h("div", h.layout(layout))) - o(oninit.callCount).equals(1) - o(onupdate.callCount).equals(0) + o(layout.calls.map((c) => c.args[2])).deepEquals([true]) m.redraw() throttleMock.fire() - o(onupdate.callCount).equals(1) + o(layout.calls.map((c) => c.args[2])).deepEquals([true, false]) }) o("emits errors correctly", function() { diff --git a/index.js b/index.js index ed39d8961..0ec45f31c 100644 --- a/index.js +++ b/index.js @@ -9,7 +9,6 @@ m.render = require("./render") m.redraw = mountRedraw.redraw m.p = require("./util/p") m.withProgress = require("./util/with-progress") -m.censor = require("./util/censor") m.lazy = require("./util/lazy") m.tracked = require("./util/tracked") diff --git a/performance/test-perf.js b/performance/test-perf.js index 63acc5483..d63b04666 100644 --- a/performance/test-perf.js +++ b/performance/test-perf.js @@ -315,7 +315,7 @@ suite.add("add large nested tree", { var NestedButton = () => ({ view(vnode) { - return m("button", m.censor(vnode.attrs), vnode.children) + return m("button", vnode.attrs, vnode.children) } }) diff --git a/render/hyperscript.js b/render/hyperscript.js index 387e6bcab..52e3662d5 100644 --- a/render/hyperscript.js +++ b/render/hyperscript.js @@ -89,7 +89,7 @@ function m(selector, attrs, ...children) { if (selector !== "[") return execSelector(selector, attrs, children) } - return Vnode(selector, {}, attrs, children) + return Vnode(selector, undefined, attrs, children) } // Simple and sweet. Also useful for idioms like `onfoo: m.capture` to drop events without @@ -102,6 +102,8 @@ m.capture = (ev) => { m.retain = () => Vnode("!", undefined, undefined, undefined) +m.layout = (f) => Vnode(">", f, undefined, undefined) + m.fragment = (...args) => m("[", ...args) // When removal is blocked, all ancestors are also blocked. This doesn't block other children, so diff --git a/render/render.js b/render/render.js index d5333a2dd..18f2039b5 100644 --- a/render/render.js +++ b/render/render.js @@ -19,20 +19,22 @@ function getNameSpace(vnode) { } //sanity check to discourage people from doing `vnode.state = ...` -function checkState(vnode, original) { - if (vnode.state !== original) throw new Error("'vnode.state' must not be modified.") -} //Note: the hook is passed as the `this` argument to allow proxying the //arguments without requiring a full array allocation to do so. It also //takes advantage of the fact the current `vnode` is the first argument in //all lifecycle methods. -function callHook(vnode) { - var original = vnode.state +function callView(vnode, old) { try { - return this.apply(original, arguments) + var original = vnode.state + var result = hyperscript.normalize(original.view(vnode, old)) + if (result === vnode) throw Error("A view cannot return the vnode it received as argument") + return result } finally { - checkState(vnode, original) + if (vnode.state !== original) { + // eslint-disable-next-line no-unsafe-finally + throw new Error("'vnode.state' must not be modified.") + } } } @@ -46,37 +48,40 @@ function activeElement(dom) { } } //create -function createNodes(parent, vnodes, start, end, hooks, nextSibling, ns) { +function createNodes(rawParent, parent, vnodes, start, end, hooks, nextSibling, ns) { for (var i = start; i < end; i++) { var vnode = vnodes[i] if (vnode != null) { - createNode(parent, vnode, hooks, ns, nextSibling) + createNode(rawParent, parent, vnode, hooks, ns, nextSibling) } } } -function createNode(parent, vnode, hooks, ns, nextSibling) { +function createNode(rawParent, parent, vnode, hooks, ns, nextSibling) { var tag = vnode.tag if (typeof tag === "string") { - if (vnode.attrs != null) initLifecycle(vnode.attrs, vnode, hooks) switch (tag) { case "!": throw new Error("No node present to retain with `m.retain()`") + case ">": createLayout(rawParent, vnode, hooks); break case "#": createText(parent, vnode, nextSibling); break case "=": - case "[": createFragment(parent, vnode, hooks, ns, nextSibling); break + case "[": createFragment(rawParent, parent, vnode, hooks, ns, nextSibling); break default: createElement(parent, vnode, hooks, ns, nextSibling) } } - else createComponent(parent, vnode, hooks, ns, nextSibling) + else createComponent(rawParent, parent, vnode, hooks, ns, nextSibling) +} +function createLayout(rawParent, vnode, hooks) { + hooks.push(vnode.state.bind(null, rawParent, (vnode.dom = new AbortController()).signal, true)) } function createText(parent, vnode, nextSibling) { vnode.dom = getDocument(parent).createTextNode(vnode.children) insertDOM(parent, vnode.dom, nextSibling) } -function createFragment(parent, vnode, hooks, ns, nextSibling) { +function createFragment(rawParent, parent, vnode, hooks, ns, nextSibling) { var fragment = getDocument(parent).createDocumentFragment() if (vnode.children != null) { var children = vnode.children - createNodes(fragment, children, 0, children.length, hooks, null, ns) + createNodes(rawParent, fragment, children, 0, children.length, hooks, null, ns) } vnode.dom = fragment.firstChild insertDOM(parent, fragment, nextSibling) @@ -102,27 +107,17 @@ function createElement(parent, vnode, hooks, ns, nextSibling) { if (!maybeSetContentEditable(vnode)) { if (vnode.children != null) { var children = vnode.children - createNodes(element, children, 0, children.length, hooks, null, ns) + createNodes(element, element, children, 0, children.length, hooks, null, ns) if (vnode.tag === "select" && attrs != null) setLateSelectAttrs(vnode, attrs) } } } -var reentrantLock = new WeakSet() -function initComponent(vnode, hooks) { +function createComponent(rawParent, parent, vnode, hooks, ns, nextSibling) { vnode.state = void 0 - if (reentrantLock.has(vnode.tag)) return - reentrantLock.add(vnode.tag) vnode.state = (vnode.tag.prototype != null && typeof vnode.tag.prototype.view === "function") ? new vnode.tag(vnode) : vnode.tag(vnode) - initLifecycle(vnode.state, vnode, hooks) - if (vnode.attrs != null) initLifecycle(vnode.attrs, vnode, hooks) - vnode.instance = hyperscript.normalize(callHook.call(vnode.state.view, vnode)) - if (vnode.instance === vnode) throw Error("A view cannot return the vnode it received as argument") - reentrantLock.delete(vnode.tag) -} -function createComponent(parent, vnode, hooks, ns, nextSibling) { - initComponent(vnode, hooks) + vnode.instance = callView(vnode) if (vnode.instance != null) { - createNode(parent, vnode.instance, hooks, ns, nextSibling) + createNode(rawParent, parent, vnode.instance, hooks, ns, nextSibling) vnode.dom = vnode.instance.dom } } @@ -133,7 +128,7 @@ function createComponent(parent, vnode, hooks, ns, nextSibling) { * @param {Vnode[] | null} old - the list of vnodes of the last `render()` call for * this part of the tree * @param {Vnode[] | null} vnodes - as above, but for the current `render()` call. - * @param {Function[]} hooks - an accumulator of post-render hooks (oncreate/onupdate) + * @param {Function[]} hooks - an accumulator of post-render layout hooks * @param {Element | null} nextSibling - the next DOM node if we're dealing with a * fragment that is not the last item in its * parent @@ -228,7 +223,7 @@ function createComponent(parent, vnode, hooks, ns, nextSibling) { function updateNodes(parent, old, vnodes, hooks, nextSibling, ns) { if (old === vnodes || old == null && vnodes == null) return - else if (old == null || old.length === 0) createNodes(parent, vnodes, 0, vnodes.length, hooks, nextSibling, ns) + else if (old == null || old.length === 0) createNodes(parent, parent, vnodes, 0, vnodes.length, hooks, nextSibling, ns) else if (vnodes == null || vnodes.length === 0) removeNodes(parent, old, 0, old.length) else { var isOldKeyed = old[0] != null && old[0].tag === "=" @@ -238,7 +233,7 @@ function updateNodes(parent, old, vnodes, hooks, nextSibling, ns) { if (!isKeyed) while (start < vnodes.length && vnodes[start] == null) start++ if (isOldKeyed !== isKeyed) { removeNodes(parent, old, oldStart, old.length) - createNodes(parent, vnodes, start, vnodes.length, hooks, nextSibling, ns) + createNodes(parent, parent, vnodes, start, vnodes.length, hooks, nextSibling, ns) } else if (!isKeyed) { // Don't index past the end of either list (causes deopts). var commonLength = old.length < vnodes.length ? old.length : vnodes.length @@ -250,12 +245,12 @@ function updateNodes(parent, old, vnodes, hooks, nextSibling, ns) { o = old[start] v = vnodes[start] if (o === v || o == null && v == null) continue - else if (o == null) createNode(parent, v, hooks, ns, getNextSibling(old, start + 1, nextSibling)) + else if (o == null) createNode(parent, parent, v, hooks, ns, getNextSibling(old, start + 1, nextSibling)) else if (v == null) removeNode(parent, o) else updateNode(parent, o, v, hooks, getNextSibling(old, start + 1, nextSibling), ns) } if (old.length > commonLength) removeNodes(parent, old, start, old.length) - if (vnodes.length > commonLength) createNodes(parent, vnodes, start, vnodes.length, hooks, nextSibling, ns) + if (vnodes.length > commonLength) createNodes(parent, parent, vnodes, start, vnodes.length, hooks, nextSibling, ns) } else { // keyed diff var oldEnd = old.length - 1, end = vnodes.length - 1, map, o, v, oe, ve, topSibling @@ -303,7 +298,7 @@ function updateNodes(parent, old, vnodes, hooks, nextSibling, ns) { ve = vnodes[end] } if (start > end) removeNodes(parent, old, oldStart, oldEnd + 1) - else if (oldStart > oldEnd) createNodes(parent, vnodes, start, end + 1, hooks, nextSibling, ns) + else if (oldStart > oldEnd) createNodes(parent, parent, vnodes, start, end + 1, hooks, nextSibling, ns) else { // inspired by ivi https://github.com/ivijs/ivi/ by Boris Kaul var originalNextSibling = nextSibling, vnodesLength = end - start + 1, oldIndices = new Array(vnodesLength), li=0, i=0, pos = 2147483647, matched = 0, map, lisIndices @@ -324,7 +319,7 @@ function updateNodes(parent, old, vnodes, hooks, nextSibling, ns) { } nextSibling = originalNextSibling if (matched !== oldEnd - oldStart + 1) removeNodes(parent, old, oldStart, oldEnd + 1) - if (matched === 0) createNodes(parent, vnodes, start, end + 1, hooks, nextSibling, ns) + if (matched === 0) createNodes(parent, parent, vnodes, start, end + 1, hooks, nextSibling, ns) else { if (pos === -1) { // the indices of the indices of the items that are part of the @@ -333,7 +328,7 @@ function updateNodes(parent, old, vnodes, hooks, nextSibling, ns) { li = lisIndices.length - 1 for (i = end; i >= start; i--) { v = vnodes[i] - if (oldIndices[i-start] === -1) createNode(parent, v, hooks, ns, nextSibling) + if (oldIndices[i-start] === -1) createNode(parent, parent, v, hooks, ns, nextSibling) else { if (lisIndices[li] === i - start) li-- else moveDOM(parent, v, nextSibling) @@ -343,7 +338,7 @@ function updateNodes(parent, old, vnodes, hooks, nextSibling, ns) { } else { for (i = end; i >= start; i--) { v = vnodes[i] - if (oldIndices[i-start] === -1) createNode(parent, v, hooks, ns, nextSibling) + if (oldIndices[i-start] === -1) createNode(parent, parent, v, hooks, ns, nextSibling) if (v.dom != null) nextSibling = vnodes[i].dom } } @@ -366,13 +361,9 @@ function updateNode(parent, old, vnode, hooks, nextSibling, ns) { vnode.dom = old.dom vnode.instance = old.instance } else if (oldTag === tag) { - vnode.state = old.state - vnode.instance = old.instance if (typeof oldTag === "string") { - if (vnode.attrs != null) { - updateLifecycle(vnode.attrs, vnode, hooks) - } switch (oldTag) { + case ">": updateLayout(parent, old, vnode, hooks); break case "#": updateText(old, vnode); break case "=": case "[": updateFragment(parent, old, vnode, hooks, nextSibling, ns); break @@ -383,9 +374,12 @@ function updateNode(parent, old, vnode, hooks, nextSibling, ns) { } else { removeNode(parent, old) - createNode(parent, vnode, hooks, ns, nextSibling) + createNode(parent, parent, vnode, hooks, ns, nextSibling) } } +function updateLayout(parent, old, vnode, hooks) { + hooks.push(vnode.state.bind(null, parent, (vnode.dom = old.dom).signal, false)) +} function updateText(old, vnode) { if (old.children.toString() !== vnode.children.toString()) { old.dom.nodeValue = vnode.children @@ -393,6 +387,8 @@ function updateText(old, vnode) { vnode.dom = old.dom } function updateFragment(parent, old, vnode, hooks, nextSibling, ns) { + vnode.state = old.state + vnode.instance = old.instance updateNodes(parent, old.children, vnode.children, hooks, nextSibling, ns) vnode.dom = null if (vnode.children != null) { @@ -404,6 +400,8 @@ function updateFragment(parent, old, vnode, hooks, nextSibling, ns) { } } function updateElement(old, vnode, hooks, ns) { + vnode.state = old.state + vnode.instance = old.instance var element = vnode.dom = old.dom ns = getNameSpace(vnode) || ns @@ -413,12 +411,11 @@ function updateElement(old, vnode, hooks, ns) { } } function updateComponent(parent, old, vnode, hooks, nextSibling, ns) { - vnode.instance = hyperscript.normalize(callHook.call(vnode.state.view, vnode, old)) - if (vnode.instance === vnode) throw Error("A view cannot return the vnode it received as argument") - updateLifecycle(vnode.state, vnode, hooks) - if (vnode.attrs != null) updateLifecycle(vnode.attrs, vnode, hooks) + vnode.state = old.state + vnode.instance = old.instance + vnode.instance = callView(vnode, old) if (vnode.instance != null) { - if (old.instance == null) createNode(parent, vnode.instance, hooks, ns, nextSibling) + if (old.instance == null) createNode(parent, parent, vnode.instance, hooks, ns, nextSibling) else updateNode(parent, old.instance, vnode.instance, hooks, nextSibling, ns) vnode.dom = vnode.instance.dom } @@ -534,31 +531,23 @@ function removeNode(parent, vnode) { if (vnode != null) { if (typeof vnode.tag === "function") { if (vnode.instance != null) removeNode(parent, vnode.instance) - } else if (vnode.tag !== "#") { - removeNodes( - vnode.tag !== "[" && vnode.tag !== "=" ? vnode.dom : parent, - vnode.children, 0, vnode.children.length - ) - } - - if (typeof vnode.tag !== "function" && vnode.tag !== "[" && vnode.tag !== "=") { - parent.removeChild(vnode.dom) - } - - try { - if (typeof vnode.tag !== "string" && typeof vnode.state.onremove === "function") { - callHook.call(vnode.state.onremove, vnode) + } else if (vnode.tag === ">") { + try { + vnode.dom.abort() + } catch (e) { + console.error(e) } - } catch (e) { - console.error(e) - } + } else { + var isNode = vnode.tag !== "[" && vnode.tag !== "=" - try { - if (vnode.attrs && typeof vnode.attrs.onremove === "function") { - callHook.call(vnode.attrs.onremove, vnode) + if (vnode.tag !== "#") { + removeNodes( + isNode ? vnode.dom : parent, + vnode.children, 0, vnode.children.length + ) } - } catch (e) { - console.error(e) + + if (isNode) parent.removeChild(vnode.dom) } } } @@ -574,7 +563,7 @@ function setAttrs(vnode, attrs, ns) { } } function setAttr(vnode, key, old, value, ns, isFileInput) { - if (value == null || isSpecialAttribute.has(key) || (old === value && !isFormAttribute(vnode, key)) && typeof value !== "object" || key === "type" && vnode.tag === "input") return + if (value == null || key === "is" || (old === value && !isFormAttribute(vnode, key)) && typeof value !== "object" || key === "type" && vnode.tag === "input") return if (key.startsWith("on")) updateEvent(vnode, key, value) else if (key.startsWith("xlink:")) vnode.dom.setAttributeNS(xlinkNs, key.slice(6), value) else if (key === "style") updateStyle(vnode.dom, old, value) @@ -607,7 +596,7 @@ function setAttr(vnode, key, old, value, ns, isFileInput) { } } function removeAttr(vnode, key, old, ns) { - if (old == null || isSpecialAttribute.has(key)) return + if (old == null || key === "is") return if (key.startsWith("on")) updateEvent(vnode, key, undefined) else if (key.startsWith("xlink:")) vnode.dom.removeAttributeNS(xlinkNs, key.slice(6)) else if (key === "style") updateStyle(vnode.dom, old, null) @@ -663,20 +652,20 @@ function updateAttrs(vnode, old, attrs, ns) { } } } -var isAlwaysFormAttribute = new Set(["value", "checked", "selected", "selectedIndex"]) -var isSpecialAttribute = new Set(["key", "is", "oninit", "oncreate", "onupdate", "onremove"]) // Try to avoid a few browser bugs on normal elements. -// var propertyMayBeBugged = new Set(["href", "list", "form", "width", "height", "type"]) -var propertyMayBeBugged = new Set(["href", "list", "form", "width", "height"]) +// var propertyMayBeBugged = /^(?:href|list|form|width|height|type)$/ +var propertyMayBeBugged = /^(?:href|list|form|width|height)$/ function isFormAttribute(vnode, attr) { - return isAlwaysFormAttribute.has(attr) || attr === "selected" && vnode.dom === activeElement(vnode.dom) || vnode.tag === "option" && vnode.dom.parentNode === activeElement(vnode.dom) + return attr === "value" || attr === "checked" || attr === "selectedIndex" || + attr === "selected" && vnode.dom === activeElement(vnode.dom) || + vnode.tag === "option" && vnode.dom.parentNode === activeElement(vnode.dom) } function hasPropertyKey(vnode, key, ns) { // Filter out namespaced keys return ns === undefined && ( // If it's a custom element, just keep it. vnode.tag.indexOf("-") > -1 || vnode.attrs != null && vnode.attrs.is || - !propertyMayBeBugged.has(key) + !propertyMayBeBugged.test(key) // Defer the property check until *after* we check everything. ) && key in vnode.dom } @@ -776,15 +765,6 @@ function updateEvent(vnode, key, value) { } } -//lifecycle -function initLifecycle(source, vnode, hooks) { - if (typeof source.oninit === "function") callHook.call(source.oninit, vnode) - if (typeof source.oncreate === "function") hooks.push(callHook.bind(source.oncreate, vnode)) -} -function updateLifecycle(source, vnode, hooks) { - if (typeof source.onupdate === "function") hooks.push(callHook.bind(source.onupdate, vnode)) -} - var currentDOM module.exports = function(dom, vnodes, redraw) { diff --git a/render/tests/test-component.js b/render/tests/test-component.js index fdca070b5..bd897f1ef 100644 --- a/render/tests/test-component.js +++ b/render/tests/test-component.js @@ -280,9 +280,9 @@ o.spec("component", function() { // A view that returns its vnode would otherwise trigger an infinite loop var threw = false var init = true - var oninit = o.spy() + var constructor = o.spy() var component = createComponent({ - oninit: oninit, + constructor: constructor, view: function(vnode) { if (init) return init = false else return vnode @@ -302,7 +302,7 @@ o.spec("component", function() { o(e instanceof RangeError).equals(false) } o(threw).equals(true) - o(oninit.callCount).equals(1) + o(constructor.callCount).equals(1) }) o("can update when returning fragments", function() { var component = createComponent({ @@ -376,10 +376,10 @@ o.spec("component", function() { }) }) o.spec("lifecycle", function() { - o("calls oninit", function() { + o("calls constructor", function() { var called = 0 var component = createComponent({ - oninit: function(vnode) { + constructor: function(vnode) { called++ o(vnode.tag).equals(component) @@ -398,10 +398,10 @@ o.spec("component", function() { o(root.firstChild.attributes["id"].value).equals("a") o(root.firstChild.firstChild.nodeValue).equals("b") }) - o("calls oninit when returning fragment", function() { + o("calls constructor when returning fragment", function() { var called = 0 var component = createComponent({ - oninit: function(vnode) { + constructor: function(vnode) { called++ o(vnode.tag).equals(component) @@ -420,27 +420,27 @@ o.spec("component", function() { o(root.firstChild.attributes["id"].value).equals("a") o(root.firstChild.firstChild.nodeValue).equals("b") }) - o("calls oninit before view", function() { + o("calls constructor before view", function() { var viewCalled = false var component = createComponent({ view: function() { viewCalled = true return m("div", {id: "a"}, "b") }, - oninit: function() { + constructor: function() { o(viewCalled).equals(false) }, }) render(root, m(component)) }) - o("does not calls oninit on redraw", function() { + o("does not calls constructor on redraw", function() { var init = o.spy() var component = createComponent({ view: function() { return m("div", {id: "a"}, "b") }, - oninit: init, + constructor: init, }) function view() { @@ -452,280 +452,217 @@ o.spec("component", function() { o(init.callCount).equals(1) }) - o("calls oncreate", function() { - var called = 0 + o("calls inner `m.layout` as initial on first render", function() { + var onabort = o.spy() + var layoutSpy = o.spy((_, signal) => { signal.onabort = onabort }) var component = createComponent({ - oncreate: function(vnode) { - called++ - - o(vnode.dom).notEquals(undefined) - o(vnode.dom).equals(root.firstChild) - o(root.childNodes.length).equals(1) - }, - view: function() { - return m("div", {id: "a"}, "b") - } + view: () => [ + m.layout(layoutSpy), + m("div", {id: "a"}, "b"), + ] }) render(root, m(component)) - o(called).equals(1) + o(layoutSpy.callCount).equals(1) + o(layoutSpy.args[0]).equals(root) + o(layoutSpy.args[1].aborted).equals(false) + o(onabort.callCount).equals(0) + o(layoutSpy.args[2]).equals(true) o(root.firstChild.nodeName).equals("DIV") o(root.firstChild.attributes["id"].value).equals("a") o(root.firstChild.firstChild.nodeValue).equals("b") }) - o("does not calls oncreate on redraw", function() { - var create = o.spy() + o("calls inner `m.layout` as non-initial on subsequent render", function() { + var onabort = o.spy() + var layoutSpy = o.spy((_, signal) => { signal.onabort = onabort }) var component = createComponent({ - view: function() { - return m("div", {id: "a"}, "b") - }, - oncreate: create, + view: () => [ + m.layout(layoutSpy), + m("div", {id: "a"}, "b"), + ] }) - function view() { - return m(component) - } - - render(root, view()) - render(root, view()) + render(root, m(component)) + render(root, m(component)) - o(create.callCount).equals(1) + o(layoutSpy.callCount).equals(2) + o(layoutSpy.args[0]).equals(root) + o(layoutSpy.args[1].aborted).equals(false) + o(onabort.callCount).equals(0) + o(layoutSpy.args[2]).equals(false) + o(root.firstChild.nodeName).equals("DIV") + o(root.firstChild.attributes["id"].value).equals("a") + o(root.firstChild.firstChild.nodeValue).equals("b") }) - o("calls oncreate when returning fragment", function() { - var called = 0 + o("aborts inner `m.layout` signal after first render", function() { + var onabort = o.spy() + var layoutSpy = o.spy((_, signal) => { signal.onabort = onabort }) var component = createComponent({ - oncreate: function(vnode) { - called++ - - o(vnode.dom).notEquals(undefined) - o(vnode.dom).equals(root.firstChild) - o(root.childNodes.length).equals(1) - }, - view: function() { - return m("div", {id: "a"}, "b") - } + view: () => [ + m.layout(layoutSpy), + m("div", {id: "a"}, "b"), + ] }) render(root, m(component)) + render(root, null) - o(called).equals(1) - o(root.firstChild.nodeName).equals("DIV") - o(root.firstChild.attributes["id"].value).equals("a") - o(root.firstChild.firstChild.nodeValue).equals("b") + o(layoutSpy.callCount).equals(1) + o(layoutSpy.args[1].aborted).equals(true) + o(onabort.callCount).equals(1) + o(root.childNodes.length).equals(0) }) - o("calls onupdate", function() { - var called = 0 + o("aborts inner `m.layout` signal after subsequent render", function() { + var onabort = o.spy() + var layoutSpy = o.spy((_, signal) => { signal.onabort = onabort }) var component = createComponent({ - onupdate: function(vnode) { - called++ - - o(vnode.dom).notEquals(undefined) - o(vnode.dom).equals(root.firstChild) - o(root.childNodes.length).equals(1) - }, - view: function() { - return m("div", {id: "a"}, "b") - } + view: () => [ + m.layout(layoutSpy), + m("div", {id: "a"}, "b"), + ] }) render(root, m(component)) + render(root, m(component)) + render(root, null) - o(called).equals(0) + o(layoutSpy.callCount).equals(2) + o(layoutSpy.args[1].aborted).equals(true) + o(onabort.callCount).equals(1) + o(root.childNodes.length).equals(0) + }) + o("calls in-element inner `m.layout` as initial on first render", function() { + var onabort = o.spy() + var layoutSpy = o.spy((_, signal) => { signal.onabort = onabort }) + var component = createComponent({ + view: () => m("div", {id: "a"}, m.layout(layoutSpy), "b"), + }) render(root, m(component)) - o(called).equals(1) + o(layoutSpy.callCount).equals(1) + o(layoutSpy.args[0]).equals(root.firstChild) + o(layoutSpy.args[1].aborted).equals(false) + o(onabort.callCount).equals(0) + o(layoutSpy.args[2]).equals(true) o(root.firstChild.nodeName).equals("DIV") o(root.firstChild.attributes["id"].value).equals("a") o(root.firstChild.firstChild.nodeValue).equals("b") }) - o("calls onupdate when returning fragment", function() { - var called = 0 + o("calls in-element inner `m.layout` as non-initial on subsequent render", function() { + var onabort = o.spy() + var layoutSpy = o.spy((_, signal) => { signal.onabort = onabort }) var component = createComponent({ - onupdate: function(vnode) { - called++ - - o(vnode.dom).notEquals(undefined) - o(vnode.dom).equals(root.firstChild) - o(root.childNodes.length).equals(1) - }, - view: function() { - return [m("div", {id: "a"}, "b")] - } + view: () => m("div", {id: "a"}, m.layout(layoutSpy), "b"), }) render(root, m(component)) - - o(called).equals(0) - render(root, m(component)) - o(called).equals(1) + o(layoutSpy.callCount).equals(2) + o(layoutSpy.args[0]).equals(root.firstChild) + o(layoutSpy.args[1].aborted).equals(false) + o(onabort.callCount).equals(0) + o(layoutSpy.args[2]).equals(false) o(root.firstChild.nodeName).equals("DIV") o(root.firstChild.attributes["id"].value).equals("a") o(root.firstChild.firstChild.nodeValue).equals("b") }) - o("calls onremove", function() { - var rootCountInCall - var onremove = o.spy(() => { - rootCountInCall = root.childNodes.length - }) + o("aborts in-element inner `m.layout` signal after first render", function() { + var onabort = o.spy() + var layoutSpy = o.spy((_, signal) => { signal.onabort = onabort }) var component = createComponent({ - onremove, - view: function() { - return m("div", {id: "a"}, "b") - } + view: () => m("div", {id: "a"}, m.layout(layoutSpy), "b"), }) render(root, m(component)) + render(root, null) - o(onremove.callCount).equals(0) - o(root.childNodes.length).equals(1) - var firstChild = root.firstChild - - render(root, []) - - o(onremove.callCount).equals(1) - o(onremove.args[0].dom).equals(firstChild) - o(rootCountInCall).equals(0) + o(layoutSpy.callCount).equals(1) + o(layoutSpy.args[1].aborted).equals(true) + o(onabort.callCount).equals(1) o(root.childNodes.length).equals(0) }) - o("calls onremove when returning fragment", function() { - var rootCountInCall - var onremove = o.spy(() => { - rootCountInCall = root.childNodes.length - }) + o("aborts in-element inner `m.layout` signal after subsequent render", function() { + var onabort = o.spy() + var layoutSpy = o.spy((_, signal) => { signal.onabort = onabort }) var component = createComponent({ - onremove, - view: function() { - return [m("div", {id: "a"}, "b")] - } + view: () => m("div", {id: "a"}, m.layout(layoutSpy), "b"), }) render(root, m(component)) + render(root, m(component)) + render(root, null) - o(onremove.callCount).equals(0) - o(root.childNodes.length).equals(1) - var firstChild = root.firstChild - - render(root, []) - - o(onremove.callCount).equals(1) - o(onremove.args[0].dom).equals(firstChild) - o(rootCountInCall).equals(0) + o(layoutSpy.callCount).equals(2) + o(layoutSpy.args[1].aborted).equals(true) + o(onabort.callCount).equals(1) o(root.childNodes.length).equals(0) }) - o("lifecycle timing megatest (for a single component)", function() { - var methods = { - view: o.spy(function() { - return "" - }) - } - var attrs = {} - var hooks = [ - "oninit", "oncreate", - "onupdate", "onremove" - ] - hooks.forEach(function(hook) { - // the other component hooks are called before the `attrs` ones - methods[hook] = o.spy(function() { - o(attrs[hook].callCount).equals(methods[hook].callCount - 1)(hook) - }) - attrs[hook] = o.spy(function() { - o(attrs[hook].callCount).equals(methods[hook].callCount)(hook) - }) - }) - - var component = createComponent(methods) - - o(methods.view.callCount).equals(0) - o(methods.oninit.callCount).equals(0) - o(methods.oncreate.callCount).equals(0) - o(methods.onupdate.callCount).equals(0) - o(methods.onremove.callCount).equals(0) - - hooks.forEach(function(hook) { - o(attrs[hook].callCount).equals(methods[hook].callCount)(hook) + o("calls direct inner `m.layout` as initial on first render", function() { + var onabort = o.spy() + var layoutSpy = o.spy((_, signal) => { signal.onabort = onabort }) + var component = createComponent({ + view: () => m.layout(layoutSpy), }) - render(root, [m(component, attrs)]) - - o(methods.view.callCount).equals(1) - o(methods.oninit.callCount).equals(1) - o(methods.oncreate.callCount).equals(1) - o(methods.onupdate.callCount).equals(0) - o(methods.onremove.callCount).equals(0) + render(root, m(component)) - hooks.forEach(function(hook) { - o(attrs[hook].callCount).equals(methods[hook].callCount)(hook) + o(layoutSpy.callCount).equals(1) + o(layoutSpy.args[0]).equals(root) + o(layoutSpy.args[1].aborted).equals(false) + o(onabort.callCount).equals(0) + o(layoutSpy.args[2]).equals(true) + o(root.childNodes.length).equals(0) + }) + o("calls direct inner `m.layout` as non-initial on subsequent render", function() { + var onabort = o.spy() + var layoutSpy = o.spy((_, signal) => { signal.onabort = onabort }) + var component = createComponent({ + view: () => m.layout(layoutSpy), }) - render(root, [m(component, attrs)]) - - o(methods.view.callCount).equals(2) - o(methods.oninit.callCount).equals(1) - o(methods.oncreate.callCount).equals(1) - o(methods.onupdate.callCount).equals(1) - o(methods.onremove.callCount).equals(0) + render(root, m(component)) + render(root, m(component)) - hooks.forEach(function(hook) { - o(attrs[hook].callCount).equals(methods[hook].callCount)(hook) + o(layoutSpy.callCount).equals(2) + o(layoutSpy.args[0]).equals(root) + o(layoutSpy.args[1].aborted).equals(false) + o(layoutSpy.args[2]).equals(false) + o(onabort.callCount).equals(0) + o(root.childNodes.length).equals(0) + }) + o("aborts direct inner `m.layout` signal after first render", function() { + var onabort = o.spy() + var layoutSpy = o.spy((_, signal) => { signal.onabort = onabort }) + var component = createComponent({ + view: () => m.layout(layoutSpy), }) - render(root, []) - - o(methods.view.callCount).equals(2) - o(methods.oninit.callCount).equals(1) - o(methods.oncreate.callCount).equals(1) - o(methods.onupdate.callCount).equals(1) - o(methods.onremove.callCount).equals(1) + render(root, m(component)) + render(root, null) - hooks.forEach(function(hook) { - o(attrs[hook].callCount).equals(methods[hook].callCount)(hook) - }) + o(layoutSpy.callCount).equals(1) + o(layoutSpy.args[1].aborted).equals(true) + o(onabort.callCount).equals(1) + o(root.childNodes.length).equals(0) }) - o("hook state and arguments validation", function(){ - var methods = { - view: o.spy(function(vnode) { - o(this).equals(vnode.state) - return "" - }) - } - var attrs = {} - var hooks = [ - "oninit", "oncreate", - "onupdate", "onremove" - ] - hooks.forEach(function(hook) { - attrs[hook] = o.spy(function(vnode){ - o(this).equals(vnode.state)(hook) - }) - methods[hook] = o.spy(function(vnode){ - o(this).equals(vnode.state) - }) - }) - - var component = createComponent(methods) - - render(root, [m(component, attrs)]) - render(root, [m(component, attrs)]) - render(root, []) - - hooks.forEach(function(hook) { - o(attrs[hook].this).equals(methods.view.this)(hook) - o(methods[hook].this).equals(methods.view.this)(hook) + o("aborts direct inner `m.layout` signal after subsequent render", function() { + var onabort = o.spy() + var layoutSpy = o.spy((_, signal) => { signal.onabort = onabort }) + var component = createComponent({ + view: () => m.layout(layoutSpy), }) - o(methods.view.args.length).equals(2) - o(methods.oninit.args.length).equals(1) - o(methods.oncreate.args.length).equals(1) - o(methods.onupdate.args.length).equals(1) - o(methods.onremove.args.length).equals(1) + render(root, m(component)) + render(root, m(component)) + render(root, null) - hooks.forEach(function(hook) { - o(methods[hook].args.length).equals(attrs[hook].args.length)(hook) - }) + o(layoutSpy.callCount).equals(2) + o(layoutSpy.args[1].aborted).equals(true) + o(onabort.callCount).equals(1) + o(root.childNodes.length).equals(0) }) o("no recycling occurs (was: recycled components get a fresh state)", function() { var step = 0 @@ -755,7 +692,7 @@ o.spec("component", function() { var data = {a: 1} var component = createComponent({ data: data, - oninit: init, + constructor: init, view: function() { return "" } @@ -763,8 +700,8 @@ o.spec("component", function() { render(root, m(component)) - function init(vnode) { - o(vnode.state.data).equals(data) + function init() { + o(this.data).equals(data) } }) o("state proxies to the component object/prototype", function() { @@ -772,7 +709,7 @@ o.spec("component", function() { var data = [body] var component = createComponent({ data: data, - oninit: init, + constructor: init, view: function() { return "" } @@ -780,9 +717,9 @@ o.spec("component", function() { render(root, m(component)) - function init(vnode) { - o(vnode.state.data).equals(data) - o(vnode.state.data[0]).equals(body) + function init() { + o(this.data).equals(data) + o(this.data[0]).equals(body) } }) }) @@ -791,47 +728,40 @@ o.spec("component", function() { o.spec("Tests specific to certain component kinds", function() { o.spec("state", function() { o("Constructible", function() { - var oninit = o.spy() var component = o.spy(function(vnode){ o(vnode.state).equals(undefined) - o(oninit.callCount).equals(0) }) var view = o.spy(function(){ o(this instanceof component).equals(true) return "" }) component.prototype.view = view - component.prototype.oninit = oninit - render(root, [m(component, {oninit: oninit})]) - render(root, [m(component, {oninit: oninit})]) + render(root, [m(component)]) + render(root, [m(component)]) render(root, []) o(component.callCount).equals(1) - o(oninit.callCount).equals(2) o(view.callCount).equals(2) }) o("Closure", function() { var state - var oninit = o.spy() var view = o.spy(function() { o(this).equals(state) return "" }) var component = o.spy(function(vnode) { o(vnode.state).equals(undefined) - o(oninit.callCount).equals(0) return state = { view: view } }) - render(root, [m(component, {oninit: oninit})]) - render(root, [m(component, {oninit: oninit})]) + render(root, [m(component)]) + render(root, [m(component)]) render(root, []) o(component.callCount).equals(1) - o(oninit.callCount).equals(1) o(view.callCount).equals(2) }) }) diff --git a/render/tests/test-input.js b/render/tests/test-input.js index 15987e72d..058d87529 100644 --- a/render/tests/test-input.js +++ b/render/tests/test-input.js @@ -31,9 +31,9 @@ o.spec("form inputs", function() { }) o("maintains focus when changed manually in hook", function() { - var input = m("input", {oncreate: function() { - input.dom.focus(); - }}); + var input = m("input", m.layout((dom) => { + dom.focus() + })); render(root, input) diff --git a/render/tests/test-oncreate.js b/render/tests/test-oncreate.js index 705976755..5d73491e6 100644 --- a/render/tests/test-oncreate.js +++ b/render/tests/test-oncreate.js @@ -5,175 +5,155 @@ var domMock = require("../../test-utils/domMock") var render = require("../../render/render") var m = require("../../render/hyperscript") -o.spec("oncreate", function() { +o.spec("layout create", function() { var $window, root o.beforeEach(function() { $window = domMock() root = $window.document.createElement("div") }) - o("calls oncreate when creating element", function() { + o("works when rendered directly", function() { var callback = o.spy() - var vnode = m("div", {oncreate: callback}) + var vnode = m.layout(callback) render(root, vnode) o(callback.callCount).equals(1) - o(callback.this).equals(vnode.state) - o(callback.args[0]).equals(vnode) + o(callback.args[0]).equals(root) + o(callback.args[1].aborted).equals(false) + o(callback.args[2]).equals(true) }) - o("calls oncreate when creating fragment", function() { + o("works when creating element", function() { var callback = o.spy() - var vnode = m.fragment({oncreate: callback}) + var vnode = m("div", m.layout(callback)) render(root, vnode) o(callback.callCount).equals(1) - o(callback.this).equals(vnode.state) - o(callback.args[0]).equals(vnode) + o(callback.args[1].aborted).equals(false) + o(callback.args[2]).equals(true) }) - o("calls oncreate when replacing same-keyed", function() { + o("works when creating fragment", function() { + var callback = o.spy() + var vnode = m.fragment(m.layout(callback)) + + render(root, vnode) + + o(callback.callCount).equals(1) + o(callback.args[1].aborted).equals(false) + o(callback.args[2]).equals(true) + }) + o("works when replacing same-keyed", function() { var createDiv = o.spy() var createA = o.spy() - var vnode = m("div", {oncreate: createDiv}) - var updated = m("a", {oncreate: createA}) + var vnode = m("div", m.layout(createDiv)) + var updated = m("a", m.layout(createA)) render(root, m.key(1, vnode)) render(root, m.key(1, updated)) o(createDiv.callCount).equals(1) - o(createDiv.this).equals(vnode.state) - o(createDiv.args[0]).equals(vnode) + o(createDiv.args[1].aborted).equals(true) + o(createDiv.args[2]).equals(true) o(createA.callCount).equals(1) - o(createA.this).equals(updated.state) - o(createA.args[0]).equals(updated) + o(createA.args[1].aborted).equals(false) + o(createA.args[2]).equals(true) }) - o("does not call oncreate when noop", function() { + o("works when creating other children", function() { var create = o.spy() - var update = o.spy() - var vnode = m("div", {oncreate: create}) - var updated = m("div", {oncreate: update}) + var vnode = m("div", m.layout(create), m("a")) render(root, vnode) - render(root, updated) o(create.callCount).equals(1) - o(create.this).equals(vnode.state) - o(create.args[0]).equals(vnode) - o(update.callCount).equals(0) + o(create.args[0]).equals(root.firstChild) + o(create.args[1].aborted).equals(false) + o(create.args[2]).equals(true) }) - o("does not call oncreate when updating attr", function() { + o("works inside keyed", function() { var create = o.spy() - var update = o.spy() - var vnode = m("div", {oncreate: create}) - var updated = m("div", {oncreate: update, id: "a"}) + var vnode = m("div", m.layout(create)) + var otherVnode = m("a") - render(root, vnode) - render(root, updated) + render(root, [m.key(1, vnode), m.key(2, otherVnode)]) o(create.callCount).equals(1) - o(create.this).equals(vnode.state) - o(create.args[0]).equals(vnode) - o(update.callCount).equals(0) + o(create.args[0]).equals(root.firstChild) + o(create.args[1].aborted).equals(false) + o(create.args[2]).equals(true) }) - o("does not call oncreate when updating children", function() { + o("does not invoke callback when removing, but aborts the provided signal", function() { var create = o.spy() - var update = o.spy() - var vnode = m("div", {oncreate: create}, m("a")) - var updated = m("div", {oncreate: update}, m("b")) + var vnode = m("div", m.layout(create)) render(root, vnode) - render(root, updated) - - o(create.callCount).equals(1) - o(create.this).equals(vnode.state) - o(create.args[0]).equals(vnode) - o(update.callCount).equals(0) - }) - o("does not call oncreate when updating keyed", function() { - var create = o.spy() - var update = o.spy() - var vnode = m("div", {oncreate: create}) - var otherVnode = m("a") - var updated = m("div", {oncreate: update}) - var otherUpdated = m("a") - - render(root, [m.key(1, vnode), m.key(2, otherVnode)]) - render(root, [m.key(2, otherUpdated), m.key(1, updated)]) o(create.callCount).equals(1) - o(create.this).equals(vnode.state) - o(create.args[0]).equals(vnode) - o(update.callCount).equals(0) - }) - o("does not call oncreate when removing", function() { - var create = o.spy() - var vnode = m("div", {oncreate: create}) + o(create.args[1].aborted).equals(false) - render(root, vnode) render(root, []) o(create.callCount).equals(1) - o(create.this).equals(vnode.state) - o(create.args[0]).equals(vnode) + o(create.args[1].aborted).equals(true) }) - o("calls oncreate at the same step as onupdate", function() { + o("works at the same step as layout update", function() { var create = o.spy() var update = o.spy() var callback = o.spy() - var vnode = m("div", {onupdate: create}) - var updated = m("div", {onupdate: update}, m("a", {oncreate: callback})) + var vnode = m("div", m.layout(create)) + var updated = m("div", m.layout(update), m("a", m.layout(callback))) render(root, vnode) render(root, updated) - o(create.callCount).equals(0) + o(create.callCount).equals(1) + o(create.args[0]).equals(root.firstChild) + o(create.args[1].aborted).equals(false) + o(create.args[2]).equals(true) + o(update.callCount).equals(1) - o(update.this).equals(vnode.state) - o(update.args[0]).equals(updated) + o(update.args[0]).equals(root.firstChild) + o(update.args[1].aborted).equals(false) + o(update.args[2]).equals(false) + o(callback.callCount).equals(1) - o(callback.this).equals(updated.children[0].state) - o(callback.args[0]).equals(updated.children[0]) + o(callback.args[0]).equals(root.firstChild.firstChild) + o(callback.args[1].aborted).equals(false) + o(callback.args[2]).equals(true) }) - o("calls oncreate on unkeyed that falls into reverse list diff code path", function() { + o("works on unkeyed that falls into reverse list diff code path", function() { var create = o.spy() - render(root, m("p", m("div"))) - render(root, m("div", {oncreate: create}, m("div"))) + render(root, [m.key(1, m("p")), m.key(2, m("div"))]) + render(root, [m.key(2, m("div", m.layout(create))), m.key(1, m("p"))]) o(create.callCount).equals(1) + o(create.args[0]).equals(root.firstChild) + o(create.args[1].aborted).equals(false) + o(create.args[2]).equals(true) }) - o("calls oncreate on unkeyed that falls into forward list diff code path", function() { + o("works on unkeyed that falls into forward list diff code path", function() { var create = o.spy() render(root, [m("div"), m("p")]) - render(root, [m("div"), m("div", {oncreate: create})]) + render(root, [m("div"), m("div", m.layout(create))]) o(create.callCount).equals(1) + o(create.args[0]).equals(root.childNodes[1]) + o(create.args[1].aborted).equals(false) + o(create.args[2]).equals(true) }) - o("calls oncreate after full DOM creation", function() { + o("works after full DOM creation", function() { var created = false - var vnode = m("div", - m("a", {oncreate: create}, - m("b") - ) - ) + var vnode = m("div", m("a", m.layout(create), m("b"))) render(root, vnode) - function create(vnode) { + function create(dom, _, isInit) { + if (!isInit) return created = true - o(vnode.dom.parentNode).notEquals(null) - o(vnode.dom.childNodes.length).equals(1) + o(dom.parentNode).equals(root.firstChild) + o(dom.childNodes.length).equals(1) } o(created).equals(true) }) - o("does not set oncreate as an event handler", function() { - var create = o.spy() - var vnode = m("div", {oncreate: create}) - - render(root, vnode) - - o(vnode.dom.oncreate).equals(undefined) - o(vnode.dom.attributes["oncreate"]).equals(undefined) - }) }) diff --git a/render/tests/test-oninit.js b/render/tests/test-oninit.js deleted file mode 100644 index 6b33ff85b..000000000 --- a/render/tests/test-oninit.js +++ /dev/null @@ -1,205 +0,0 @@ -"use strict" - -var o = require("ospec") -var domMock = require("../../test-utils/domMock") -var render = require("../../render/render") -var m = require("../../render/hyperscript") - -o.spec("oninit", function() { - var $window, root - o.beforeEach(function() { - $window = domMock() - root = $window.document.createElement("div") - }) - - o("calls oninit when creating element", function() { - var callback = o.spy() - var vnode = m("div", {oninit: callback}) - - render(root, vnode) - - o(callback.callCount).equals(1) - o(callback.this).equals(vnode.state) - o(callback.args[0]).equals(vnode) - }) - o("calls oninit when creating fragment", function() { - var callback = o.spy() - var vnode = m.fragment({oninit: callback}) - - render(root, vnode) - - o(callback.callCount).equals(1) - o(callback.this).equals(vnode.state) - o(callback.args[0]).equals(vnode) - }) - o("calls oninit when replacing keyed", function() { - var createDiv = o.spy() - var createA = o.spy() - var vnode = m("div", {oninit: createDiv}) - var updated = m("a", {oninit: createA}) - - render(root, m.key(1, vnode)) - render(root, m.key(1, updated)) - - o(createDiv.callCount).equals(1) - o(createDiv.this).equals(vnode.state) - o(createDiv.args[0]).equals(vnode) - o(createA.callCount).equals(1) - o(createA.this).equals(updated.state) - o(createA.args[0]).equals(updated) - }) - o("does not call oninit when noop", function() { - var create = o.spy() - var update = o.spy() - var vnode = m("div", {oninit: create}) - var updated = m("div", {oninit: update}) - - render(root, vnode) - render(root, updated) - - o(create.callCount).equals(1) - o(create.this).equals(vnode.state) - o(create.args[0]).equals(vnode) - o(update.callCount).equals(0) - }) - o("does not call oninit when updating attr", function() { - var create = o.spy() - var update = o.spy() - var vnode = m("div", {oninit: create}) - var updated = m("div", {oninit: update, id: "a"}) - - render(root, vnode) - render(root, updated) - - o(create.callCount).equals(1) - o(create.this).equals(vnode.state) - o(create.args[0]).equals(vnode) - o(update.callCount).equals(0) - }) - o("does not call oninit when updating children", function() { - var create = o.spy() - var update = o.spy() - var vnode = m("div", {oninit: create}, m("a")) - var updated = m("div", {oninit: update}, m("b")) - - render(root, vnode) - render(root, updated) - - o(create.callCount).equals(1) - o(create.this).equals(vnode.state) - o(create.args[0]).equals(vnode) - o(update.callCount).equals(0) - }) - o("does not call oninit when updating keyed", function() { - var create = o.spy() - var update = o.spy() - var vnode = m("div", {oninit: create}) - var otherVnode = m("a") - var updated = m("div", {oninit: update}) - var otherUpdated = m("a") - - render(root, [m.key(1, vnode), m.key(2, otherVnode)]) - render(root, [m.key(2, otherUpdated), m.key(1, updated)]) - - o(create.callCount).equals(1) - o(create.this).equals(vnode.state) - o(create.args[0]).equals(vnode) - o(update.callCount).equals(0) - }) - o("does not call oninit when removing", function() { - var create = o.spy() - var vnode = m("div", {oninit: create}) - - render(root, vnode) - render(root, []) - - o(create.callCount).equals(1) - o(create.this).equals(vnode.state) - o(create.args[0]).equals(vnode) - }) - o("calls oninit when recycling", function() { - var create = o.spy() - var update = o.spy() - var vnode = m("div", {oninit: create}) - var updated = m("div", {oninit: update}) - - render(root, m.key(1, vnode)) - render(root, []) - render(root, m.key(1, updated)) - - o(create.callCount).equals(1) - o(create.this).equals(vnode.state) - o(create.args[0]).equals(vnode) - o(update.callCount).equals(1) - o(update.this).equals(updated.state) - o(update.args[0]).equals(updated) - }) - o("calls oninit at the same step as onupdate", function() { - var create = o.spy() - var update = o.spy() - var callback = o.spy() - var vnode = m("div", {onupdate: create}) - var updated = m("div", {onupdate: update}, m("a", {oninit: callback})) - - render(root, vnode) - render(root, updated) - - o(create.callCount).equals(0) - o(update.callCount).equals(1) - o(update.this).equals(vnode.state) - o(update.args[0]).equals(updated) - o(callback.callCount).equals(1) - o(callback.this).equals(updated.children[0].state) - o(callback.args[0]).equals(updated.children[0]) - }) - o("calls oninit before full DOM creation", function() { - var called = false - var vnode = m("div", - m("a", {oninit: create}, - m("b") - ) - ) - - render(root, vnode) - - function create(vnode) { - called = true - - o(vnode.dom).equals(undefined) - o(root.childNodes.length).equals(1) - } - o(called).equals(true) - }) - o("does not set oninit as an event handler", function() { - var create = o.spy() - var vnode = m("div", {oninit: create}) - - render(root, vnode) - - o(vnode.dom.oninit).equals(undefined) - o(vnode.dom.attributes["oninit"]).equals(undefined) - }) - - o("No spurious oninit calls in mapped keyed diff when the pool is involved (#1992)", function () { - var oninit1 = o.spy() - var oninit2 = o.spy() - var oninit3 = o.spy() - - render(root, [ - m.key(1, m("p", {oninit: oninit1})), - m.key(2, m("p", {oninit: oninit2})), - m.key(3, m("p", {oninit: oninit3})), - ]) - render(root, [ - m.key(1, m("p", {oninit: oninit1})), - m.key(3, m("p", {oninit: oninit3})), - ]) - render(root, [ - m.key(3, m("p", {oninit: oninit3})), - ]) - - o(oninit1.callCount).equals(1) - o(oninit2.callCount).equals(1) - o(oninit3.callCount).equals(1) - }) -}) diff --git a/render/tests/test-onremove.js b/render/tests/test-onremove.js index 7d51f0465..d8eeecefb 100644 --- a/render/tests/test-onremove.js +++ b/render/tests/test-onremove.js @@ -6,29 +6,31 @@ var domMock = require("../../test-utils/domMock") var render = require("../../render/render") var m = require("../../render/hyperscript") -o.spec("onremove", function() { +o.spec("layout remove", function() { var $window, root o.beforeEach(function() { $window = domMock() root = $window.document.createElement("div") }) - o("does not call onremove when creating", function() { + var layoutRemove = (onabort) => m.layout((_, signal) => { signal.onabort = onabort }) + + o("does not abort layout signal when creating", function() { var create = o.spy() var update = o.spy() - var vnode = m("div", {onremove: create}) - var updated = m("div", {onremove: update}) + var vnode = m("div", layoutRemove(create)) + var updated = m("div", layoutRemove(update)) render(root, vnode) render(root, updated) o(create.callCount).equals(0) }) - o("does not call onremove when updating", function() { + o("does not abort layout signal when updating", function() { var create = o.spy() var update = o.spy() - var vnode = m("div", {onremove: create}) - var updated = m("div", {onremove: update}) + var vnode = m("div", layoutRemove(create)) + var updated = m("div", layoutRemove(update)) render(root, vnode) render(root, updated) @@ -36,42 +38,28 @@ o.spec("onremove", function() { o(create.callCount).equals(0) o(update.callCount).equals(0) }) - o("calls onremove when removing element", function() { + o("aborts layout signal when removing element", function() { var remove = o.spy() - var vnode = m("div", {onremove: remove}) + var vnode = m("div", layoutRemove(remove)) render(root, vnode) render(root, []) o(remove.callCount).equals(1) - o(remove.this).equals(vnode.state) - o(remove.args[0]).equals(vnode) }) - o("calls onremove when removing fragment", function() { + o("aborts layout signal when removing fragment", function() { var remove = o.spy() - var vnode = m.fragment({onremove: remove}) + var vnode = m.fragment(layoutRemove(remove)) render(root, vnode) render(root, []) o(remove.callCount).equals(1) - o(remove.this).equals(vnode.state) - o(remove.args[0]).equals(vnode) }) - o("does not set onremove as an event handler", function() { - var remove = o.spy() - var vnode = m("div", {onremove: remove}) - - render(root, vnode) - - o(vnode.dom.onremove).equals(undefined) - o(vnode.dom.attributes["onremove"]).equals(undefined) - o(vnode.instance).equals(undefined) - }) - o("calls onremove on keyed nodes", function() { + o("aborts layout signal on keyed nodes", function() { var remove = o.spy() var vnode = m("div") - var temp = m("div", {onremove: remove}) + var temp = m("div", layoutRemove(remove)) var updated = m("div") render(root, m.key(1, vnode)) @@ -85,7 +73,7 @@ o.spec("onremove", function() { o.spec(cmp.kind, function(){ var createComponent = cmp.create - o("calls onremove on nested component", function() { + o("aborts layout signal on nested component", function() { var spy = o.spy() var comp = createComponent({ view: function() {return m(outer)} @@ -94,21 +82,20 @@ o.spec("onremove", function() { view: function() {return m(inner)} }) var inner = createComponent({ - onremove: spy, - view: function() {return m("div")} + view: () => m.layout(spy), }) render(root, m(comp)) render(root, null) o(spy.callCount).equals(1) }) - o("calls onremove on nested component child", function() { + o("aborts layout signal on nested component child", function() { var spy = o.spy() var comp = createComponent({ view: function() {return m(outer)} }) var outer = createComponent({ - view: function() {return m(inner, m("a", {onremove: spy}))} + view: function() {return m(inner, m("a", layoutRemove(spy)))} }) var inner = createComponent({ view: function(vnode) {return m("div", vnode.children)} @@ -118,68 +105,6 @@ o.spec("onremove", function() { o(spy.callCount).equals(1) }) - o("doesn't call onremove on children when the corresponding view returns null (after removing the parent)", function() { - var threw = false - var spy = o.spy() - var parent = createComponent({ - view: function() {} - }) - var child = createComponent({ - view: function() {}, - onremove: spy - }) - render(root, m(parent, m(child))) - try { - render(root, null) - } catch (e) { - threw = e - } - - o(spy.callCount).equals(0) - o(threw).equals(false) - }) - o("doesn't call onremove on children when the corresponding view returns null (after removing the children)", function() { - var threw = false - var spy = o.spy() - var parent = createComponent({ - view: function() {} - }) - var child = createComponent({ - view: function() {}, - onremove: spy - }) - render(root, m(parent, m(child))) - try { - render(root, m(parent)) - } catch (e) { - threw = true - } - - o(spy.callCount).equals(0) - o(threw).equals(false) - }) - o("onremove doesn't fire on nodes that go from pool to pool (#1990)", function() { - var onremove = o.spy(); - - render(root, [m("div", m("div")), m("div", m("div", {onremove: onremove}))]); - render(root, [m("div", m("div"))]); - render(root, []); - - o(onremove.callCount).equals(1) - }) - o("doesn't fire when removing the children of a node that's brought back from the pool (#1991 part 2)", function() { - var onremove = o.spy() - var vnode = m("div", m("div", {onremove: onremove})) - var temp = m("div") - var updated = m("div", m("p")) - - render(root, m.key(1, vnode)) - render(root, m.key(2, temp)) - render(root, m.key(1, updated)) - - o(vnode.dom).notEquals(updated.dom) // this used to be a recycling pool test - o(onremove.callCount).equals(1) - }) }) }) }) diff --git a/render/tests/test-onupdate.js b/render/tests/test-onupdate.js index a2cd72a5c..61cf1549c 100644 --- a/render/tests/test-onupdate.js +++ b/render/tests/test-onupdate.js @@ -5,122 +5,101 @@ var domMock = require("../../test-utils/domMock") var render = require("../../render/render") var m = require("../../render/hyperscript") -o.spec("onupdate", function() { +o.spec("layout update", function() { var $window, root o.beforeEach(function() { $window = domMock() root = $window.document.createElement("div") }) - o("does not call onupdate when creating element", function() { - var create = o.spy() - var update = o.spy() - var vnode = m("div", {onupdate: create}) - var updated = m("div", {onupdate: update}) - - render(root, vnode) - render(root, updated) - - o(create.callCount).equals(0) - o(update.callCount).equals(1) - o(update.this).equals(vnode.state) - o(update.args[0]).equals(updated) - }) - o("does not call onupdate when removing element", function() { - var create = o.spy() - var vnode = m("div", {onupdate: create}) + o("is not invoked when removing element", function() { + var layout = o.spy() + var vnode = m("div", m.layout(layout)) render(root, vnode) render(root, []) - o(create.callCount).equals(0) + o(layout.calls.map((c) => c.args[2])).deepEquals([true]) }) - o("does not call onupdate when replacing keyed element", function() { - var create = o.spy() + o("is not updated when replacing keyed element", function() { + var layout = o.spy() var update = o.spy() - var vnode = m.key(1, m("div", {onupdate: create})) - var updated = m.key(1, m("a", {onupdate: update})) + var vnode = m.key(1, m("div", m.layout(layout))) + var updated = m.key(1, m("a", m.layout(update))) render(root, vnode) render(root, updated) - o(create.callCount).equals(0) - o(update.callCount).equals(0) + o(layout.calls.map((c) => c.args[2])).deepEquals([true]) + o(update.calls.map((c) => c.args[2])).deepEquals([true]) }) - o("does not call old onupdate when removing the onupdate property in new vnode", function() { - var create = o.spy() - var vnode = m("a", {onupdate: create}) + o("does not call old callback when removing layout vnode from new vnode", function() { + var layout = o.spy() + var vnode = m("a", m.layout(layout)) var updated = m("a") + render(root, vnode) render(root, vnode) render(root, updated) - o(create.callCount).equals(0) + o(layout.calls.map((c) => c.args[2])).deepEquals([true]) }) - o("calls onupdate when noop", function() { - var create = o.spy() + o("invoked on noop", function() { + var layout = o.spy() var update = o.spy() - var vnode = m("div", {onupdate: create}) - var updated = m("div", {onupdate: update}) + var vnode = m("div", m.layout(layout)) + var updated = m("div", m.layout(update)) render(root, vnode) render(root, updated) - o(create.callCount).equals(0) - o(update.callCount).equals(1) - o(update.this).equals(vnode.state) - o(update.args[0]).equals(updated) + o(layout.calls.map((c) => c.args[2])).deepEquals([true]) + o(update.calls.map((c) => c.args[2])).deepEquals([false]) }) - o("calls onupdate when updating attr", function() { - var create = o.spy() + o("invoked on updating attr", function() { + var layout = o.spy() var update = o.spy() - var vnode = m("div", {onupdate: create}) - var updated = m("div", {onupdate: update, id: "a"}) + var vnode = m("div", m.layout(layout)) + var updated = m("div", {id: "a"}, m.layout(update)) render(root, vnode) render(root, updated) - o(create.callCount).equals(0) - o(update.callCount).equals(1) - o(update.this).equals(vnode.state) - o(update.args[0]).equals(updated) + o(layout.calls.map((c) => c.args[2])).deepEquals([true]) + o(update.calls.map((c) => c.args[2])).deepEquals([false]) }) - o("calls onupdate when updating children", function() { - var create = o.spy() + o("invoked on updating children", function() { + var layout = o.spy() var update = o.spy() - var vnode = m("div", {onupdate: create}, m("a")) - var updated = m("div", {onupdate: update}, m("b")) + var vnode = m("div", m.layout(layout), m("a")) + var updated = m("div", m.layout(update), m("b")) render(root, vnode) render(root, updated) - o(create.callCount).equals(0) - o(update.callCount).equals(1) - o(update.this).equals(vnode.state) - o(update.args[0]).equals(updated) + o(layout.calls.map((c) => c.args[2])).deepEquals([true]) + o(update.calls.map((c) => c.args[2])).deepEquals([false]) }) - o("calls onupdate when updating fragment", function() { - var create = o.spy() + o("invoked on updating fragment", function() { + var layout = o.spy() var update = o.spy() - var vnode = m.fragment({onupdate: create}) - var updated = m.fragment({onupdate: update}) + var vnode = m.fragment(m.layout(layout)) + var updated = m.fragment(m.layout(update)) render(root, vnode) render(root, updated) - o(create.callCount).equals(0) - o(update.callCount).equals(1) - o(update.this).equals(vnode.state) - o(update.args[0]).equals(updated) + o(layout.calls.map((c) => c.args[2])).deepEquals([true]) + o(update.calls.map((c) => c.args[2])).deepEquals([false]) }) - o("calls onupdate after full DOM update", function() { + o("invoked on full DOM update", function() { var called = false var vnode = m("div", {id: "1"}, - m("a", {id: "2"}, + m("a", {id: "2"}, m.layout(() => {}), m("b", {id: "3"}) ) ) var updated = m("div", {id: "11"}, - m("a", {id: "22", onupdate: update}, + m("a", {id: "22"}, m.layout(update), m("b", {id: "33"}) ) ) @@ -128,22 +107,14 @@ o.spec("onupdate", function() { render(root, vnode) render(root, updated) - function update(vnode) { + function update(dom, _, isInit) { + if (isInit) return called = true - o(vnode.dom.parentNode.attributes["id"].value).equals("11") - o(vnode.dom.attributes["id"].value).equals("22") - o(vnode.dom.childNodes[0].attributes["id"].value).equals("33") + o(dom.parentNode.attributes["id"].value).equals("11") + o(dom.attributes["id"].value).equals("22") + o(dom.childNodes[0].attributes["id"].value).equals("33") } o(called).equals(true) }) - o("does not set onupdate as an event handler", function() { - var update = o.spy() - var vnode = m("div", {onupdate: update}) - - render(root, vnode) - - o(vnode.dom.onupdate).equals(undefined) - o(vnode.dom.attributes["onupdate"]).equals(undefined) - }) }) diff --git a/render/tests/test-render.js b/render/tests/test-render.js index 3d2c138ff..4104ccd35 100644 --- a/render/tests/test-render.js +++ b/render/tests/test-render.js @@ -58,216 +58,202 @@ o.spec("render", function() { o(threw).equals(true) }) - o("does not try to re-initialize a constructible component whose view has thrown", function() { - var oninit = o.spy() + o("tries to re-initialize a constructible component whose view has thrown", function() { + var A = o.spy() var view = o.spy(() => { throw new Error("error") }) - function A(){} A.prototype.view = view - A.prototype.oninit = oninit var throwCount = 0 try {render(root, m(A))} catch (e) {throwCount++} o(throwCount).equals(1) - o(oninit.callCount).equals(1) + o(A.callCount).equals(1) o(view.callCount).equals(1) try {render(root, m(A))} catch (e) {throwCount++} - o(throwCount).equals(1) - o(oninit.callCount).equals(1) - o(view.callCount).equals(1) + o(throwCount).equals(2) + o(A.callCount).equals(2) + o(view.callCount).equals(2) }) - o("does not try to re-initialize a constructible component whose oninit has thrown", function() { - var oninit = o.spy(function(){throw new Error("error")}) + o("tries to re-initialize a constructible component whose constructor has thrown", function() { + var A = o.spy(() => { throw new Error("error") }) var view = o.spy() - function A(){} A.prototype.view = view - A.prototype.oninit = oninit var throwCount = 0 try {render(root, m(A))} catch (e) {throwCount++} o(throwCount).equals(1) - o(oninit.callCount).equals(1) + o(A.callCount).equals(1) o(view.callCount).equals(0) try {render(root, m(A))} catch (e) {throwCount++} - o(throwCount).equals(1) - o(oninit.callCount).equals(1) + o(throwCount).equals(2) + o(A.callCount).equals(2) o(view.callCount).equals(0) }) - o("does not try to re-initialize a constructible component whose constructor has thrown", function() { - var oninit = o.spy() - var view = o.spy() - function A(){throw new Error("error")} - A.prototype.view = view - A.prototype.oninit = oninit - var throwCount = 0 - - try {render(root, m(A))} catch (e) {throwCount++} - - o(throwCount).equals(1) - o(oninit.callCount).equals(0) - o(view.callCount).equals(0) - - try {render(root, m(A))} catch (e) {throwCount++} - - o(throwCount).equals(1) - o(oninit.callCount).equals(0) - o(view.callCount).equals(0) - }) - o("does not try to re-initialize a closure component whose view has thrown", function() { - var oninit = o.spy() + o("tries to re-initialize a closure component whose view has thrown", function() { + var A = o.spy(() => ({view})) var view = o.spy(() => { throw new Error("error") }) - function A() { - return { - view: view, - oninit: oninit, - } - } var throwCount = 0 try {render(root, m(A))} catch (e) {throwCount++} o(throwCount).equals(1) - o(oninit.callCount).equals(1) - o(view.callCount).equals(1) - - try {render(root, m(A))} catch (e) {throwCount++} - - o(throwCount).equals(1) - o(oninit.callCount).equals(1) + o(A.callCount).equals(1) o(view.callCount).equals(1) - }) - o("does not try to re-initialize a closure component whose oninit has thrown", function() { - var oninit = o.spy(function() {throw new Error("error")}) - var view = o.spy() - function A() { - return { - view: view, - oninit: oninit, - } - } - var throwCount = 0 - try {render(root, m(A))} catch (e) {throwCount++} - - o(throwCount).equals(1) - o(oninit.callCount).equals(1) - o(view.callCount).equals(0) try {render(root, m(A))} catch (e) {throwCount++} - o(throwCount).equals(1) - o(oninit.callCount).equals(1) - o(view.callCount).equals(0) + o(throwCount).equals(2) + o(A.callCount).equals(2) + o(view.callCount).equals(2) }) - o("does not try to re-initialize a closure component whose closure has thrown", function() { - function A() { - throw new Error("error") - } + o("tries to re-initialize a closure component whose closure has thrown", function() { + var A = o.spy(() => { throw new Error("error") }) var throwCount = 0 try {render(root, m(A))} catch (e) {throwCount++} o(throwCount).equals(1) + o(A.callCount).equals(1) try {render(root, m(A))} catch (e) {throwCount++} - o(throwCount).equals(1) + o(throwCount).equals(2) + o(A.callCount).equals(2) }) o("lifecycle methods work in keyed children of recycled keyed", function() { - var createA = o.spy() - var updateA = o.spy() - var removeA = o.spy() - var createB = o.spy() - var updateB = o.spy() - var removeB = o.spy() + var onabortA = o.spy() + var onabortB = o.spy() + var layoutA = o.spy((_, signal) => { signal.onabort = onabortA }) + var layoutB = o.spy((_, signal) => { signal.onabort = onabortB }) var a = function() { return m.key(1, m("div", - m.key(11, m("div", {oncreate: createA, onupdate: updateA, onremove: removeA})), + m.key(11, m("div", m.layout(layoutA))), m.key(12, m("div")) )) } var b = function() { return m.key(2, m("div", - m.key(21, m("div", {oncreate: createB, onupdate: updateB, onremove: removeB})), + m.key(21, m("div", m.layout(layoutB))), m.key(22, m("div")) )) } render(root, a()) + var first = root.firstChild.firstChild render(root, b()) + var second = root.firstChild.firstChild render(root, a()) - - o(createA.callCount).equals(2) - o(updateA.callCount).equals(0) - o(removeA.callCount).equals(1) - o(createB.callCount).equals(1) - o(updateB.callCount).equals(0) - o(removeB.callCount).equals(1) + var third = root.firstChild.firstChild + + o(layoutA.callCount).equals(2) + o(layoutA.calls[0].args[0]).equals(first) + o(layoutA.calls[0].args[1].aborted).equals(true) + o(layoutA.calls[0].args[2]).equals(true) + o(layoutA.calls[1].args[0]).equals(third) + o(layoutA.calls[1].args[1].aborted).equals(false) + o(layoutA.calls[1].args[2]).equals(true) + o(onabortA.callCount).equals(1) + + o(layoutB.callCount).equals(1) + o(layoutB.calls[0].args[0]).equals(second) + o(layoutB.calls[0].args[1]).notEquals(layoutA.calls[0].args[1]) + o(layoutB.calls[0].args[1].aborted).equals(true) + o(layoutB.calls[0].args[2]).equals(true) + o(onabortB.callCount).equals(1) }) o("lifecycle methods work in unkeyed children of recycled keyed", function() { - var createA = o.spy() - var updateA = o.spy() - var removeA = o.spy() - var createB = o.spy() - var updateB = o.spy() - var removeB = o.spy() + var onabortA = o.spy() + var onabortB = o.spy() + var layoutA = o.spy((_, signal) => { signal.onabort = onabortA }) + var layoutB = o.spy((_, signal) => { signal.onabort = onabortB }) var a = function() { return m.key(1, m("div", - m("div", {oncreate: createA, onupdate: updateA, onremove: removeA}) + m("div", m.layout(layoutA)) )) } var b = function() { return m.key(2, m("div", - m("div", {oncreate: createB, onupdate: updateB, onremove: removeB}) + m("div", m.layout(layoutB)) )) } render(root, a()) + var first = root.firstChild.firstChild render(root, b()) + var second = root.firstChild.firstChild render(root, a()) - - o(createA.callCount).equals(2) - o(updateA.callCount).equals(0) - o(removeA.callCount).equals(1) - o(createB.callCount).equals(1) - o(updateB.callCount).equals(0) - o(removeB.callCount).equals(1) + var third = root.firstChild.firstChild + + o(layoutA.callCount).equals(2) + o(layoutA.calls[0].args[0]).equals(first) + o(layoutA.calls[0].args[1].aborted).equals(true) + o(layoutA.calls[0].args[2]).equals(true) + o(layoutA.calls[1].args[0]).equals(third) + o(layoutA.calls[1].args[1].aborted).equals(false) + o(layoutA.calls[1].args[2]).equals(true) + o(onabortA.callCount).equals(1) + + o(layoutB.callCount).equals(1) + o(layoutB.calls[0].args[0]).equals(second) + o(layoutB.calls[0].args[1]).notEquals(layoutA.calls[0].args[1]) + o(layoutB.calls[0].args[1].aborted).equals(true) + o(layoutB.calls[0].args[2]).equals(true) + o(onabortB.callCount).equals(1) }) o("update lifecycle methods work on children of recycled keyed", function() { - var createA = o.spy() - var updateA = o.spy() - var removeA = o.spy() - var createB = o.spy() - var updateB = o.spy() - var removeB = o.spy() + var onabortA = o.spy() + var onabortB = o.spy() + var layoutA = o.spy((_, signal) => { signal.onabort = onabortA }) + var layoutB = o.spy((_, signal) => { signal.onabort = onabortB }) var a = function() { return m.key(1, m("div", - m("div", {oncreate: createA, onupdate: updateA, onremove: removeA}) + m("div", m.layout(layoutA)) )) } var b = function() { return m.key(2, m("div", - m("div", {oncreate: createB, onupdate: updateB, onremove: removeB}) + m("div", m.layout(layoutB)) )) } render(root, a()) render(root, a()) - o(createA.callCount).equals(1) - o(updateA.callCount).equals(1) - o(removeA.callCount).equals(0) + var first = root.firstChild.firstChild + o(layoutA.callCount).equals(2) + o(layoutA.calls[0].args[0]).equals(first) + o(layoutA.calls[1].args[0]).equals(first) + o(layoutA.calls[0].args[1]).equals(layoutA.calls[1].args[1]) + o(layoutA.calls[0].args[1].aborted).equals(false) + o(layoutA.calls[0].args[2]).equals(true) + o(layoutA.calls[1].args[2]).equals(false) + o(onabortA.callCount).equals(0) render(root, b()) - o(createA.callCount).equals(1) - o(updateA.callCount).equals(1) - o(removeA.callCount).equals(1) + var second = root.firstChild.firstChild + o(layoutA.callCount).equals(2) + o(layoutA.calls[0].args[1].aborted).equals(true) + o(onabortA.callCount).equals(1) + + o(layoutB.callCount).equals(1) + o(layoutB.calls[0].args[0]).equals(second) + o(layoutB.calls[0].args[1].aborted).equals(false) + o(layoutB.calls[0].args[2]).equals(true) + o(onabortB.callCount).equals(0) render(root, a()) render(root, a()) - - o(createA.callCount).equals(2) - o(updateA.callCount).equals(2) - o(removeA.callCount).equals(1) + var third = root.firstChild.firstChild + o(layoutB.callCount).equals(1) + o(layoutB.calls[0].args[1].aborted).equals(true) + o(onabortB.callCount).equals(1) + + o(layoutA.callCount).equals(4) + o(layoutA.calls[2].args[0]).equals(third) + o(layoutA.calls[2].args[1]).notEquals(layoutA.calls[1].args[1]) + o(layoutA.calls[2].args[1].aborted).equals(false) + o(layoutA.calls[2].args[2]).equals(true) + o(onabortA.callCount).equals(1) }) o("svg namespace is preserved in keyed diff (#1820)", function(){ // note that this only exerciese one branch of the keyed diff algo @@ -300,23 +286,8 @@ o.spec("render", function() { o("does not allow reentrant invocations", function() { var thrown = [] function A() { - var updated = false try {render(root, m(A))} catch (e) {thrown.push("construct")} return { - oninit: function() { - try {render(root, m(A))} catch (e) {thrown.push("oninit")} - }, - oncreate: function() { - try {render(root, m(A))} catch (e) {thrown.push("oncreate")} - }, - onupdate: function() { - if (updated) return - updated = true - try {render(root, m(A))} catch (e) {thrown.push("onupdate")} - }, - onremove: function() { - try {render(root, m(A))} catch (e) {thrown.push("onremove")} - }, view: function() { try {render(root, m(A))} catch (e) {thrown.push("view")} }, @@ -325,28 +296,19 @@ o.spec("render", function() { render(root, m(A)) o(thrown).deepEquals([ "construct", - "oninit", "view", - "oncreate", ]) render(root, m(A)) o(thrown).deepEquals([ "construct", - "oninit", "view", - "oncreate", "view", - "onupdate", ]) render(root, []) o(thrown).deepEquals([ "construct", - "oninit", "view", - "oncreate", "view", - "onupdate", - "onremove", ]) }) }) diff --git a/render/tests/test-updateNodes.js b/render/tests/test-updateNodes.js index 20b9bfea6..59adc1900 100644 --- a/render/tests/test-updateNodes.js +++ b/render/tests/test-updateNodes.js @@ -488,11 +488,11 @@ o.spec("updateNodes", function() { o(root.childNodes[3].nodeName).equals("A") }) o("cached keyed nodes move when diffed via the map", function() { - var onupdate = o.spy() - var a = m.key("a", m("a", {onupdate})) - var b = m.key("b", m("b", {onupdate})) - var c = m.key("c", m("c", {onupdate})) - var d = m.key("d", m("d", {onupdate})) + var layout = o.spy() + var a = m.key("a", m("a", m.layout(layout))) + var b = m.key("b", m("b", m.layout(layout))) + var c = m.key("c", m("c", m.layout(layout))) + var d = m.key("d", m("d", m.layout(layout))) render(root, [a, b, c, d]) render(root, [b, d, a, c]) @@ -502,7 +502,8 @@ o.spec("updateNodes", function() { o(root.childNodes[1].nodeName).equals("D") o(root.childNodes[2].nodeName).equals("A") o(root.childNodes[3].nodeName).equals("C") - o(onupdate.callCount).equals(0) + + o(layout.calls.map((c) => c.args[2])).deepEquals([true, true, true, true]) }) o("removes then create different bigger", function() { var vnodes = [m.key(1, m("a")), m.key(2, m("b"))] @@ -666,110 +667,80 @@ o.spec("updateNodes", function() { o(root.childNodes[0].childNodes[1].childNodes.length).equals(1) o(root.childNodes[1].childNodes.length).equals(0) }) - o("onremove doesn't fire from nodes in the pool (#1990)", function () { - var onremove1 = o.spy() - var onremove2 = o.spy() - - render(root, [ - m("div", m("div", {onremove: onremove1})), - m("div", m("div", {onremove: onremove2})) - ]) - o(onremove1.callCount).equals(0) - o(onremove2.callCount).equals(0) - - render(root, [ - m("div", m("div", {onremove: onremove1})) - ]) - o(onremove1.callCount).equals(0) - o(onremove2.callCount).equals(1) - - render(root,[]) - o(onremove1.callCount).equals(1) - o(onremove2.callCount).equals(1) - }) o("cached, non-keyed nodes skip diff", function () { - var onupdate = o.spy(); - var cached = m("a", {onupdate: onupdate}) + var layout = o.spy(); + var cached = m("a", m.layout(layout)) render(root, cached) render(root, cached) - o(onupdate.callCount).equals(0) + o(layout.calls.map((c) => c.args[2])).deepEquals([true]) }) o("cached, keyed nodes skip diff", function () { - var onupdate = o.spy() - var cached = m.key("a", m("a", {onupdate})) + var layout = o.spy() + var cached = m.key("a", m("a", m.layout(layout))) render(root, cached) render(root, cached) - o(onupdate.callCount).equals(0) + o(layout.calls.map((c) => c.args[2])).deepEquals([true]) }) o("keyed cached elements are re-initialized when brought back from the pool (#2003)", function () { - var onupdate = o.spy() - var oncreate = o.spy() + var layout = o.spy() var cached = m.key(1, m("B", - m("A", {oncreate: oncreate, onupdate: onupdate}, "A") + m("A", m.layout(layout), "A") )) render(root, m("div", cached)) render(root, []) render(root, m("div", cached)) - o(oncreate.callCount).equals(2) - o(onupdate.callCount).equals(0) + o(layout.calls.map((c) => c.args[2])).deepEquals([true, true]) }) o("unkeyed cached elements are re-initialized when brought back from the pool (#2003)", function () { - var onupdate = o.spy() - var oncreate = o.spy() + var layout = o.spy() var cached = m("B", - m("A", {oncreate: oncreate, onupdate: onupdate}, "A") + m("A", m.layout(layout), "A") ) render(root, m("div", cached)) render(root, []) render(root, m("div", cached)) - o(oncreate.callCount).equals(2) - o(onupdate.callCount).equals(0) + o(layout.calls.map((c) => c.args[2])).deepEquals([true, true]) }) o("keyed cached elements are re-initialized when brought back from nested pools (#2003)", function () { - var onupdate = o.spy() - var oncreate = o.spy() + var layout = o.spy() var cached = m.key(1, m("B", - m("A", {oncreate: oncreate, onupdate: onupdate}, "A") + m("A", m.layout(layout), "A") )) render(root, m("div", cached)) render(root, m("div")) render(root, []) render(root, m("div", cached)) - o(oncreate.callCount).equals(2) - o(onupdate.callCount).equals(0) + o(layout.calls.map((c) => c.args[2])).deepEquals([true, true]) }) o("unkeyed cached elements are re-initialized when brought back from nested pools (#2003)", function () { - var onupdate = o.spy() - var oncreate = o.spy() + var layout = o.spy() var cached = m("B", - m("A", {oncreate: oncreate, onupdate: onupdate}, "A") + m("A", m.layout(layout), "A") ) render(root, m("div", cached)) render(root, m("div")) render(root, []) render(root, m("div", cached)) - o(oncreate.callCount).equals(2) - o(onupdate.callCount).equals(0) + o(layout.calls.map((c) => c.args[2])).deepEquals([true, true]) }) o("null stays in place", function() { - var create = o.spy() - var update = o.spy() - var remove = o.spy() - var vnodes = [m("div"), m("a", {oncreate: create, onupdate: update, onremove: remove})] - var temp = [null, m("a", {oncreate: create, onupdate: update, onremove: remove})] - var updated = [m("div"), m("a", {oncreate: create, onupdate: update, onremove: remove})] + var onabort = o.spy() + var layout = o.spy((_, signal) => { signal.onabort = onabort }) + var vnodes = [m("div"), m("a", m.layout(layout))] + var temp = [null, m("a", m.layout(layout))] + var updated = [m("div"), m("a", m.layout(layout))] render(root, vnodes) var before = vnodes[1].dom @@ -778,17 +749,15 @@ o.spec("updateNodes", function() { var after = updated[1].dom o(before).equals(after) - o(create.callCount).equals(1) - o(update.callCount).equals(2) - o(remove.callCount).equals(0) + o(layout.calls.map((c) => c.args[2])).deepEquals([true, false, false]) + o(onabort.callCount).equals(0) }) o("null stays in place if not first", function() { - var create = o.spy() - var update = o.spy() - var remove = o.spy() - var vnodes = [m("b"), m("div"), m("a", {oncreate: create, onupdate: update, onremove: remove})] - var temp = [m("b"), null, m("a", {oncreate: create, onupdate: update, onremove: remove})] - var updated = [m("b"), m("div"), m("a", {oncreate: create, onupdate: update, onremove: remove})] + var onabort = o.spy() + var layout = o.spy((_, signal) => { signal.onabort = onabort }) + var vnodes = [m("b"), m("div"), m("a", m.layout(layout))] + var temp = [m("b"), null, m("a", m.layout(layout))] + var updated = [m("b"), m("div"), m("a", m.layout(layout))] render(root, vnodes) var before = vnodes[2].dom @@ -797,9 +766,8 @@ o.spec("updateNodes", function() { var after = updated[2].dom o(before).equals(after) - o(create.callCount).equals(1) - o(update.callCount).equals(2) - o(remove.callCount).equals(0) + o(layout.calls.map((c) => c.args[2])).deepEquals([true, false, false]) + o(onabort.callCount).equals(0) }) o("node is recreated if key changes to undefined", function () { var vnode = m.key(1, m("b")) @@ -849,16 +817,14 @@ o.spec("updateNodes", function() { o(root.childNodes.length).equals(0) }) o("handles null values in unkeyed lists of different length (#2003)", function() { - var oncreate = o.spy() - var onremove = o.spy() - var onupdate = o.spy() + var onabort = o.spy() + var layout = o.spy((_, signal) => { signal.onabort = onabort }) - render(root, [m("div", {oncreate: oncreate, onremove: onremove, onupdate: onupdate}), null]) - render(root, [null, m("div", {oncreate: oncreate, onremove: onremove, onupdate: onupdate}), null]) + render(root, [m("div", m.layout(layout)), null]) + render(root, [null, m("div", m.layout(layout)), null]) - o(oncreate.callCount).equals(2) - o(onremove.callCount).equals(1) - o(onupdate.callCount).equals(0) + o(layout.calls.map((c) => c.args[2])).deepEquals([true, true]) + o(onabort.callCount).equals(1) }) o("supports changing the element of a keyed element in a list when traversed bottom-up", function() { try { diff --git a/test-utils/components.js b/test-utils/components.js index 27578b8f2..ed95808bc 100644 --- a/test-utils/components.js +++ b/test-utils/components.js @@ -6,18 +6,37 @@ module.exports = [ { kind: "constructible", create: function(methods) { + if (!methods) methods = {} + var constructor = methods.constructor + if (constructor) delete methods.constructor class C { + constructor(vnode) { + if (typeof constructor === "function") { + constructor.call(this, vnode) + } + } view() { return m("div") } } - Object.assign(C.prototype, methods || {}) + Object.assign(C.prototype, methods) return C } }, { kind: "closure", create: function(methods) { - return () => Object.assign({view: () => m("div")}, methods || {}) + if (!methods) methods = {} + var constructor = methods.constructor + if (constructor) delete methods.constructor + return (vnode) => { + var result = Object.assign({view: () => m("div")}, methods) + + if (typeof constructor === "function") { + constructor.call(result, vnode) + } + + return result + } } } ] diff --git a/test-utils/tests/test-components.js b/test-utils/tests/test-components.js index e0cbd3714..3f6829099 100644 --- a/test-utils/tests/test-components.js +++ b/test-utils/tests/test-components.js @@ -10,7 +10,7 @@ o.spec("test-utils/components", function() { o("works", function() { o(typeof component.kind).equals("string") - var methods = {oninit: function(){}, view: function(){}} + var methods = {view: function(){}} var cmp1, cmp2 @@ -38,7 +38,6 @@ o.spec("test-utils/components", function() { // deepEquals doesn't search the prototype, do it manually o(cmp2 != null).equals(true) o(cmp2.view).equals(methods.view) - o(cmp2.oninit).equals(methods.oninit) } }) } diff --git a/util/censor.js b/util/censor.js deleted file mode 100644 index cc7ac9ad2..000000000 --- a/util/censor.js +++ /dev/null @@ -1,47 +0,0 @@ -"use strict" - -// Note: this is mildly perf-sensitive. -// -// It does *not* use `delete` - dynamic `delete`s usually cause objects to bail -// out into dictionary mode and just generally cause a bunch of optimization -// issues within engines. -// -// Ideally, I would've preferred to do this, if it weren't for the optimization -// issues: -// -// ```js -// const hasOwn = require("./hasOwn") -// const magic = [ -// "key", "oninit", "oncreate", "onupdate", -// "onremove", -// ] -// module.exports = (attrs, extras) => { -// const result = Object.assign(Object.create(null), attrs) -// for (const key of magic) delete result[key] -// if (extras != null) for (const key of extras) delete result[key] -// return result -// } -// ``` - -var hasOwn = require("./hasOwn") -var magic = new Set(["oninit", "oncreate", "onupdate", "onremove"]) - -module.exports = function(attrs, extras) { - var result = {} - - if (extras != null) { - for (var key in attrs) { - if (hasOwn.call(attrs, key) && !magic.has(key) && extras.indexOf(key) < 0) { - result[key] = attrs[key] - } - } - } else { - for (var key in attrs) { - if (hasOwn.call(attrs, key) && !magic.has(key)) { - result[key] = attrs[key] - } - } - } - - return result -} diff --git a/util/lazy.js b/util/lazy.js index 484b348f7..64cb0615f 100644 --- a/util/lazy.js +++ b/util/lazy.js @@ -2,7 +2,6 @@ var mountRedraw = require("../mount-redraw") var m = require("../render/hyperscript") -var censor = require("./censor") module.exports = (opts, redraw = mountRedraw.redraw) => { var fetched = false @@ -31,6 +30,6 @@ module.exports = (opts, redraw = mountRedraw.redraw) => { ) } - return {view: ({attrs}) => m(Comp, censor(attrs))} + return {view: ({attrs}) => m(Comp, attrs)} } } diff --git a/util/tests/test-censor.js b/util/tests/test-censor.js deleted file mode 100644 index b01533367..000000000 --- a/util/tests/test-censor.js +++ /dev/null @@ -1,214 +0,0 @@ -"use strict" - -var o = require("ospec") -var censor = require("../../util/censor") - -o.spec("censor", function() { - o.spec("magic missing, no extras", function() { - o("returns new object", function() { - var original = {one: "two"} - var censored = censor(original) - o(censored).notEquals(original) - o(censored).deepEquals({one: "two"}) - }) - o("does not modify original object", function() { - var original = {one: "two"} - censor(original) - o(original).deepEquals({one: "two"}) - }) - }) - - o.spec("magic present, no extras", function() { - o("returns new object", function() { - var original = { - one: "two", - key: "test", - oninit: "test", - oncreate: "test", - onupdate: "test", - onremove: "test", - } - var censored = censor(original) - o(censored).notEquals(original) - o(censored).deepEquals({one: "two", key: "test"}) - }) - o("does not modify original object", function() { - var original = { - one: "two", - key: "test", - oninit: "test", - oncreate: "test", - onupdate: "test", - onremove: "test", - } - censor(original) - o(original).deepEquals({ - one: "two", - key: "test", - oninit: "test", - oncreate: "test", - onupdate: "test", - onremove: "test", - }) - }) - }) - - o.spec("magic missing, null extras", function() { - o("returns new object", function() { - var original = {one: "two"} - var censored = censor(original, null) - o(censored).notEquals(original) - o(censored).deepEquals({one: "two"}) - }) - o("does not modify original object", function() { - var original = {one: "two"} - censor(original, null) - o(original).deepEquals({one: "two"}) - }) - }) - - o.spec("magic present, null extras", function() { - o("returns new object", function() { - var original = { - one: "two", - key: "test", - oninit: "test", - oncreate: "test", - onupdate: "test", - onremove: "test", - } - var censored = censor(original, null) - o(censored).notEquals(original) - o(censored).deepEquals({one: "two", key: "test"}) - }) - o("does not modify original object", function() { - var original = { - one: "two", - key: "test", - oninit: "test", - oncreate: "test", - onupdate: "test", - onremove: "test", - } - censor(original, null) - o(original).deepEquals({ - one: "two", - key: "test", - oninit: "test", - oncreate: "test", - onupdate: "test", - onremove: "test", - }) - }) - }) - - o.spec("magic missing, extras missing", function() { - o("returns new object", function() { - var original = {one: "two"} - var censored = censor(original, ["extra"]) - o(censored).notEquals(original) - o(censored).deepEquals({one: "two"}) - }) - o("does not modify original object", function() { - var original = {one: "two"} - censor(original, ["extra"]) - o(original).deepEquals({one: "two"}) - }) - }) - - o.spec("magic present, extras missing", function() { - o("returns new object", function() { - var original = { - one: "two", - key: "test", - oninit: "test", - oncreate: "test", - onupdate: "test", - onremove: "test", - } - var censored = censor(original, ["extra"]) - o(censored).notEquals(original) - o(censored).deepEquals({one: "two", key: "test"}) - }) - o("does not modify original object", function() { - var original = { - one: "two", - key: "test", - oninit: "test", - oncreate: "test", - onupdate: "test", - onremove: "test", - } - censor(original, ["extra"]) - o(original).deepEquals({ - one: "two", - key: "test", - oninit: "test", - oncreate: "test", - onupdate: "test", - onremove: "test", - }) - }) - }) - - o.spec("magic missing, extras present", function() { - o("returns new object", function() { - var original = { - one: "two", - extra: "test", - } - var censored = censor(original, ["extra"]) - o(censored).notEquals(original) - o(censored).deepEquals({one: "two"}) - }) - o("does not modify original object", function() { - var original = { - one: "two", - extra: "test", - } - censor(original, ["extra"]) - o(original).deepEquals({ - one: "two", - extra: "test", - }) - }) - }) - - o.spec("magic present, extras present", function() { - o("returns new object", function() { - var original = { - one: "two", - extra: "test", - key: "test", - oninit: "test", - oncreate: "test", - onupdate: "test", - onremove: "test", - } - var censored = censor(original, ["extra"]) - o(censored).notEquals(original) - o(censored).deepEquals({one: "two", key: "test"}) - }) - o("does not modify original object", function() { - var original = { - one: "two", - extra: "test", - key: "test", - oninit: "test", - oncreate: "test", - onupdate: "test", - onremove: "test", - } - censor(original, ["extra"]) - o(original).deepEquals({ - one: "two", - extra: "test", - key: "test", - oninit: "test", - oncreate: "test", - onupdate: "test", - onremove: "test", - }) - }) - }) -}) From 2942ea5b7c17122ca2f10e764969931c5280d6c1 Mon Sep 17 00:00:00 2001 From: Claudia Meadows Date: Thu, 3 Oct 2024 20:08:12 -0700 Subject: [PATCH 36/95] Drop old vnode state stuff except for components --- render/hyperscript.js | 2 +- render/render.js | 23 ++++++++++------------- 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/render/hyperscript.js b/render/hyperscript.js index 52e3662d5..dbd8db4a0 100644 --- a/render/hyperscript.js +++ b/render/hyperscript.js @@ -65,7 +65,7 @@ function execSelector(selector, attrs, children) { if (hasClassName) attrs.className = null } - return Vnode(state.tag, {}, attrs, children) + return Vnode(state.tag, undefined, attrs, children) } // Caution is advised when editing this - it's very perf-critical. It's specially designed to avoid diff --git a/render/render.js b/render/render.js index 18f2039b5..97ced9743 100644 --- a/render/render.js +++ b/render/render.js @@ -387,8 +387,6 @@ function updateText(old, vnode) { vnode.dom = old.dom } function updateFragment(parent, old, vnode, hooks, nextSibling, ns) { - vnode.state = old.state - vnode.instance = old.instance updateNodes(parent, old.children, vnode.children, hooks, nextSibling, ns) vnode.dom = null if (vnode.children != null) { @@ -401,7 +399,6 @@ function updateFragment(parent, old, vnode, hooks, nextSibling, ns) { } function updateElement(old, vnode, hooks, ns) { vnode.state = old.state - vnode.instance = old.instance var element = vnode.dom = old.dom ns = getNameSpace(vnode) || ns @@ -747,21 +744,21 @@ class EventDict extends Map { //event function updateEvent(vnode, key, value) { - if (vnode.instance != null) { - vnode.instance._ = currentRedraw - var prev = vnode.instance.get(key) + if (vnode.state != null) { + vnode.state._ = currentRedraw + var prev = vnode.state.get(key) if (prev === value) return if (value != null && (typeof value === "function" || typeof value === "object")) { - if (prev == null) vnode.dom.addEventListener(key.slice(2), vnode.instance, false) - vnode.instance.set(key, value) + if (prev == null) vnode.dom.addEventListener(key.slice(2), vnode.state, false) + vnode.state.set(key, value) } else { - if (prev != null) vnode.dom.removeEventListener(key.slice(2), vnode.instance, false) - vnode.instance.delete(key) + if (prev != null) vnode.dom.removeEventListener(key.slice(2), vnode.state, false) + vnode.state.delete(key) } } else if (value != null && (typeof value === "function" || typeof value === "object")) { - vnode.instance = new EventDict() - vnode.dom.addEventListener(key.slice(2), vnode.instance, false) - vnode.instance.set(key, value) + vnode.state = new EventDict() + vnode.dom.addEventListener(key.slice(2), vnode.state, false) + vnode.state.set(key, value) } } From 168b403b91bb631098d7dd04aa74a2234a72d390 Mon Sep 17 00:00:00 2001 From: Claudia Meadows Date: Thu, 3 Oct 2024 20:58:04 -0700 Subject: [PATCH 37/95] Add `m.init` and `m.use` convenience utilities, simplify fragments, fix a renderer bug --- index.js | 2 ++ render/hyperscript.js | 13 ++++----- render/render.js | 2 +- util/init.js | 11 +++++++ util/tests/test-init.js | 33 +++++++++++++++++++++ util/tests/test-use.js | 63 +++++++++++++++++++++++++++++++++++++++++ util/use.js | 21 ++++++++++++++ 7 files changed, 136 insertions(+), 9 deletions(-) create mode 100644 util/init.js create mode 100644 util/tests/test-init.js create mode 100644 util/tests/test-use.js create mode 100644 util/use.js diff --git a/index.js b/index.js index 0ec45f31c..2f4e10b04 100644 --- a/index.js +++ b/index.js @@ -10,6 +10,8 @@ m.redraw = mountRedraw.redraw m.p = require("./util/p") m.withProgress = require("./util/with-progress") m.lazy = require("./util/lazy") +m.init = require("./util/init") +m.use = require("./util/use") m.tracked = require("./util/tracked") module.exports = m diff --git a/render/hyperscript.js b/render/hyperscript.js index dbd8db4a0..8f6fd6fed 100644 --- a/render/hyperscript.js +++ b/render/hyperscript.js @@ -104,17 +104,14 @@ m.retain = () => Vnode("!", undefined, undefined, undefined) m.layout = (f) => Vnode(">", f, undefined, undefined) -m.fragment = (...args) => m("[", ...args) - -// When removal is blocked, all ancestors are also blocked. This doesn't block other children, so -// this method also needs to accept an optional list of children to also keep alive while blocked. -// -// Note that the children are still notified of removal *immediately*. -m.key = (key, ...children) => - Vnode("=", key, undefined, m.normalizeChildren( +var simpleVnode = (tag, state, ...children) => + Vnode(tag, state, undefined, m.normalizeChildren( children.length === 1 && Array.isArray(children[0]) ? children[0].slice() : [...children] )) +m.fragment = (...children) => simpleVnode("[", undefined, ...children) +m.key = (key, ...children) => simpleVnode("=", key, ...children) + m.normalize = (node) => { if (node == null || typeof node === "boolean") return null if (typeof node !== "object") return Vnode("#", undefined, undefined, String(node)) diff --git a/render/render.js b/render/render.js index 97ced9743..f051ae03b 100644 --- a/render/render.js +++ b/render/render.js @@ -360,7 +360,7 @@ function updateNode(parent, old, vnode, hooks, nextSibling, ns) { vnode.children = old.children vnode.dom = old.dom vnode.instance = old.instance - } else if (oldTag === tag) { + } else if (oldTag === tag && (tag !== "=" || vnode.state === old.state)) { if (typeof oldTag === "string") { switch (oldTag) { case ">": updateLayout(parent, old, vnode, hooks); break diff --git a/util/init.js b/util/init.js new file mode 100644 index 000000000..8c2a9a624 --- /dev/null +++ b/util/init.js @@ -0,0 +1,11 @@ +"use strict" + +var m = require("../render/hyperscript") + +var Init = () => ({ + view: ({attrs: {f}}, o) => ( + o ? m.retain() : m.layout((_, signal) => queueMicrotask(() => f(signal))) + ) +}) + +module.exports = (f) => m(Init, {f}) diff --git a/util/tests/test-init.js b/util/tests/test-init.js new file mode 100644 index 000000000..2864f0abb --- /dev/null +++ b/util/tests/test-init.js @@ -0,0 +1,33 @@ +"use strict" + +var o = require("ospec") +var init = require("../init") +var domMock = require("../../test-utils/domMock") +var render = require("../../render/render") + +o.spec("m.init", () => { + o("works", () => { + var onabort = o.spy() + var initializer = o.spy((signal) => { signal.onabort = onabort }) + var $window = domMock() + + render($window.document.body, init(initializer)) + o(initializer.callCount).equals(0) + o(onabort.callCount).equals(0) + + return Promise.resolve() + .then(() => { + o(initializer.callCount).equals(1) + o(onabort.callCount).equals(0) + render($window.document.body, init(initializer)) + }) + .then(() => { + o(initializer.callCount).equals(1) + o(onabort.callCount).equals(0) + render($window.document.body, null) + + o(initializer.callCount).equals(1) + o(onabort.callCount).equals(1) + }) + }) +}) diff --git a/util/tests/test-use.js b/util/tests/test-use.js new file mode 100644 index 000000000..088c21d1a --- /dev/null +++ b/util/tests/test-use.js @@ -0,0 +1,63 @@ +"use strict" + +var o = require("ospec") +var use = require("../use") +var domMock = require("../../test-utils/domMock") +var render = require("../../render/render") +var m = require("../../hyperscript") + +o.spec("m.use", () => { + o("works with empty arrays", () => { + var onabort = o.spy() + var initializer = o.spy((_, signal) => { signal.onabort = onabort }) + var $window = domMock() + + render($window.document.body, use([], m.layout(initializer))) + o(initializer.callCount).equals(1) + o(onabort.callCount).equals(0) + + render($window.document.body, use([], m.layout(initializer))) + o(initializer.callCount).equals(2) + o(onabort.callCount).equals(0) + + render($window.document.body, null) + o(initializer.callCount).equals(2) + o(onabort.callCount).equals(1) + }) + + o("works with equal non-empty arrays", () => { + var onabort = o.spy() + var initializer = o.spy((_, signal) => { signal.onabort = onabort }) + var $window = domMock() + + render($window.document.body, use([1], m.layout(initializer))) + o(initializer.callCount).equals(1) + o(onabort.callCount).equals(0) + + render($window.document.body, use([1], m.layout(initializer))) + o(initializer.callCount).equals(2) + o(onabort.callCount).equals(0) + + render($window.document.body, null) + o(initializer.callCount).equals(2) + o(onabort.callCount).equals(1) + }) + + o("works with non-equal same-length non-empty arrays", () => { + var onabort = o.spy() + var initializer = o.spy((_, signal) => { signal.onabort = onabort }) + var $window = domMock() + + render($window.document.body, use([1], m.layout(initializer))) + o(initializer.callCount).equals(1) + o(onabort.callCount).equals(0) + + render($window.document.body, use([2], m.layout(initializer))) + o(initializer.callCount).equals(2) + o(onabort.callCount).equals(1) + + render($window.document.body, null) + o(initializer.callCount).equals(2) + o(onabort.callCount).equals(2) + }) +}) diff --git a/util/use.js b/util/use.js new file mode 100644 index 000000000..77b683fb9 --- /dev/null +++ b/util/use.js @@ -0,0 +1,21 @@ +"use strict" + +var m = require("../render/hyperscript") + +var Use = () => { + var key = 0 + return { + view: (v, o) => { + if (o && !( + v.attrs.d.length === o.attrs.d.length && + v.attrs.d.every((b, i) => Object.is(b, o.attrs.d[i])) + )) { + key++ + } + + return m.key(key, v.children) + } + } +} + +module.exports = (deps, ...children) => m(Use, {d: [...deps]}, ...children) From b168f742052027a6e1b7fd01e2378c622989ea3f Mon Sep 17 00:00:00 2001 From: Claudia Meadows Date: Thu, 3 Oct 2024 21:38:15 -0700 Subject: [PATCH 38/95] Migrate components to simple functions and closures Part of this meant moving children to an attribute property. I've also added a translation layer of sorts so one can pass them through to DOM vnodes and them "just work", even though the internal structure separates attributes and children. All in all, this should hopefully result in a lot less memory usage and some faster app load times. --- api/tests/test-mountRedraw.js | 44 +- performance/test-perf.js | 248 ++-- render/hyperscript.js | 13 +- render/render.js | 42 +- render/tests/manual/iframe.html | 10 +- render/tests/test-component.js | 1246 +++++++---------- render/tests/test-hyperscript.js | 16 +- .../tests/test-normalizeComponentChildren.js | 8 +- render/tests/test-onremove.js | 57 +- render/tests/test-render.js | 47 +- render/tests/test-retain.js | 103 +- render/tests/test-updateNodes.js | 93 +- test-utils/components.js | 42 - test-utils/tests/test-components.js | 51 - util/init.js | 6 +- util/lazy.js | 6 +- util/tests/test-lazy.js | 1157 ++++++++------- util/use.js | 18 +- 18 files changed, 1332 insertions(+), 1875 deletions(-) delete mode 100644 test-utils/components.js delete mode 100644 test-utils/tests/test-components.js diff --git a/api/tests/test-mountRedraw.js b/api/tests/test-mountRedraw.js index 854110811..ee9ed990d 100644 --- a/api/tests/test-mountRedraw.js +++ b/api/tests/test-mountRedraw.js @@ -26,10 +26,6 @@ o.spec("mount/redraw", function() { o(throttleMock.queueLength()).equals(0) }) - var Inline = () => ({ - view: (vnode, old) => vnode.attrs.view(vnode, old), - }) - o("shouldn't error if there are no renderers", function() { m.redraw() throttleMock.fire() @@ -236,11 +232,9 @@ o.spec("mount/redraw", function() { var root2 = $document.createElement("div") var root3 = $document.createElement("div") - m.mount(root1, () => h(Inline, { - view(_, old) { - if (old) m.mount(root2, null) - calls.push("root1") - }, + m.mount(root1, () => h.layout((_, __, isInit) => { + if (!isInit) m.mount(root2, null) + calls.push("root1") })) m.mount(root2, () => { calls.push("root2") }) m.mount(root3, () => { calls.push("root3") }) @@ -262,11 +256,9 @@ o.spec("mount/redraw", function() { var root3 = $document.createElement("div") m.mount(root1, () => { calls.push("root1") }) - m.mount(root2, () => h(Inline, { - view(_, old) { - if (old) m.mount(root1, null) - calls.push("root2") - }, + m.mount(root2, () => h.layout((_, __, isInit) => { + if (!isInit) m.mount(root1, null) + calls.push("root2") })) m.mount(root3, () => { calls.push("root3") }) o(calls).deepEquals([ @@ -288,11 +280,9 @@ o.spec("mount/redraw", function() { var root3 = $document.createElement("div") m.mount(root1, () => { calls.push("root1") }) - m.mount(root2, () => h(Inline, { - view(_, old) { - if (old) { m.mount(root1, null); throw "fail" } - calls.push("root2") - }, + m.mount(root2, () => h.layout((_, __, isInit) => { + if (!isInit) { m.mount(root1, null); throw "fail" } + calls.push("root2") })) m.mount(root3, () => { calls.push("root3") }) o(calls).deepEquals([ @@ -313,11 +303,9 @@ o.spec("mount/redraw", function() { var root3 = $document.createElement("div") m.mount(root1, () => { calls.push("root1") }) - m.mount(root2, () => h(Inline, { - view(_, old) { - if (old) try { m.mount(root2, null) } catch (e) { calls.push([e.constructor, e.message]) } - calls.push("root2") - }, + m.mount(root2, () => h.layout((_, __, isInit) => { + if (!isInit) try { m.mount(root2, null) } catch (e) { calls.push([e.constructor, e.message]) } + calls.push("root2") })) m.mount(root3, () => { calls.push("root3") }) o(calls).deepEquals([ @@ -341,11 +329,9 @@ o.spec("mount/redraw", function() { var root3 = $document.createElement("div") m.mount(root1, () => { calls.push("root1") }) - m.mount(root2, () => h(Inline, { - view(_, old) { - if (old) try { m.mount(root2, null) } catch (e) { throw [e.constructor, e.message] } - calls.push("root2") - }, + m.mount(root2, () => h.layout((_, __, isInit) => { + if (!isInit) try { m.mount(root2, null) } catch (e) { throw [e.constructor, e.message] } + calls.push("root2") })) m.mount(root3, () => { calls.push("root3") }) o(calls).deepEquals([ diff --git a/performance/test-perf.js b/performance/test-perf.js index d63b04666..d2a833726 100644 --- a/performance/test-perf.js +++ b/performance/test-perf.js @@ -251,89 +251,65 @@ suite.add("add large nested tree", { fields.push((i * 999).toString(36)) } - var NestedHeader = () => ({ - view() { - return m("header", - m("h1.asdf", "a ", "b", " c ", 0, " d"), - m("nav", - m("a", {href: "/foo"}, "Foo"), - m("a", {href: "/bar"}, "Bar") - ) - ) - } - }) + var NestedHeader = () => m("header", + m("h1.asdf", "a ", "b", " c ", 0, " d"), + m("nav", + m("a", {href: "/foo"}, "Foo"), + m("a", {href: "/bar"}, "Bar") + ) + ) - var NestedForm = () => ({ - view() { - return m("form", {onSubmit: function () {}}, - m("input[type=checkbox][checked]"), - m("input[type=checkbox]", {checked: false}), - m("fieldset", - m("label", - m("input[type=radio][checked]") - ), - m("label", - m("input[type=radio]") - ) - ), - m("fieldset", - fields.map(function (field) { - return m("label", - field, - ":", - m("input", {placeholder: field}) - ) - }) - ), - m(NestedButtonBar, null) + var NestedForm = () => m("form", {onSubmit: function () {}}, + m("input[type=checkbox][checked]"), + m("input[type=checkbox]", {checked: false}), + m("fieldset", + m("label", + m("input[type=radio][checked]") + ), + m("label", + m("input[type=radio]") ) - } - }) - - var NestedButtonBar = () => ({ - view() { - return m(".button-bar", - m(NestedButton, - {style: "width:10px; height:10px; border:1px solid #FFF;"}, - "Normal CSS" - ), - m(NestedButton, - {style: "top:0 ; right: 20"}, - "Poor CSS" - ), - m(NestedButton, - {style: "invalid-prop:1;padding:1px;font:12px/1.1 arial,sans-serif;", icon: true}, - "Poorer CSS" - ), - m(NestedButton, - {style: {margin: 0, padding: "10px", overflow: "visible"}}, - "Object CSS" + ), + m("fieldset", + fields.map(function (field) { + return m("label", + field, + ":", + m("input", {placeholder: field}) ) - ) - } - }) + }) + ), + m(NestedButtonBar, null) + ) - var NestedButton = () => ({ - view(vnode) { - return m("button", vnode.attrs, vnode.children) - } - }) + var NestedButtonBar = () => m(".button-bar", + m(NestedButton, + {style: "width:10px; height:10px; border:1px solid #FFF;"}, + "Normal CSS" + ), + m(NestedButton, + {style: "top:0 ; right: 20"}, + "Poor CSS" + ), + m(NestedButton, + {style: "invalid-prop:1;padding:1px;font:12px/1.1 arial,sans-serif;", icon: true}, + "Poorer CSS" + ), + m(NestedButton, + {style: {margin: 0, padding: "10px", overflow: "visible"}}, + "Object CSS" + ) + ) - var NestedMain = () => ({ - view() { - return m(NestedForm) - } - }) - - this.NestedRoot = () => ({ - view() { - return m("div.foo.bar[data-foo=bar]", - {p: 2}, - m(NestedHeader), - m(NestedMain) - ) - } - }) + var NestedButton = (attrs) => m("button", attrs) + + var NestedMain = () => m(NestedForm) + + this.NestedRoot = () => m("div.foo.bar[data-foo=bar]", + {p: 2}, + m(NestedHeader), + m(NestedMain) + ) }, fn: function () { m.render(rootElem, m(this.NestedRoot)) @@ -401,80 +377,56 @@ suite.add("mutate styles/properties", { suite.add("repeated add/removal", { setup: function () { - var RepeatedHeader = () => ({ - view() { - return m("header", - m("h1.asdf", "a ", "b", " c ", 0, " d"), - m("nav", - m("a", {href: "/foo"}, "Foo"), - m("a", {href: "/bar"}, "Bar") - ) - ) - } - }) + var RepeatedHeader = () => m("header", + m("h1.asdf", "a ", "b", " c ", 0, " d"), + m("nav", + m("a", {href: "/foo"}, "Foo"), + m("a", {href: "/bar"}, "Bar") + ) + ) - var RepeatedForm = () => ({ - view() { - return m("form", {onSubmit: function () {}}, - m("input", {type: "checkbox", checked: true}), - m("input", {type: "checkbox", checked: false}), - m("fieldset", - m("label", - m("input", {type: "radio", checked: true}) - ), - m("label", - m("input", {type: "radio"}) - ) - ), - m(RepeatedButtonBar, null) - ) - } - }) - - var RepeatedButtonBar = () => ({ - view() { - return m(".button-bar", - m(RepeatedButton, - {style: "width:10px; height:10px; border:1px solid #FFF;"}, - "Normal CSS" - ), - m(RepeatedButton, - {style: "top:0 ; right: 20"}, - "Poor CSS" - ), - m(RepeatedButton, - {style: "invalid-prop:1;padding:1px;font:12px/1.1 arial,sans-serif;", icon: true}, - "Poorer CSS" - ), - m(RepeatedButton, - {style: {margin: 0, padding: "10px", overflow: "visible"}}, - "Object CSS" - ) + var RepeatedForm = () => m("form", {onSubmit: function () {}}, + m("input", {type: "checkbox", checked: true}), + m("input", {type: "checkbox", checked: false}), + m("fieldset", + m("label", + m("input", {type: "radio", checked: true}) + ), + m("label", + m("input", {type: "radio"}) ) - } - }) + ), + m(RepeatedButtonBar, null) + ) - var RepeatedButton = () => ({ - view(vnode) { - return m("button", vnode.attrs, vnode.children) - } - }) + var RepeatedButtonBar = () => m(".button-bar", + m(RepeatedButton, + {style: "width:10px; height:10px; border:1px solid #FFF;"}, + "Normal CSS" + ), + m(RepeatedButton, + {style: "top:0 ; right: 20"}, + "Poor CSS" + ), + m(RepeatedButton, + {style: "invalid-prop:1;padding:1px;font:12px/1.1 arial,sans-serif;", icon: true}, + "Poorer CSS" + ), + m(RepeatedButton, + {style: {margin: 0, padding: "10px", overflow: "visible"}}, + "Object CSS" + ) + ) - var RepeatedMain = () => ({ - view() { - return m(RepeatedForm) - } - }) - - this.RepeatedRoot = () => ({ - view() { - return m("div.foo.bar[data-foo=bar]", - {p: 2}, - m(RepeatedHeader, null), - m(RepeatedMain, null) - ) - } - }) + var RepeatedButton = (attrs) => m("button", attrs) + + var RepeatedMain = () => m(RepeatedForm) + + this.RepeatedRoot = () => m("div.foo.bar[data-foo=bar]", + {p: 2}, + m(RepeatedHeader, null), + m(RepeatedMain, null) + ) }, fn: function () { m.render(rootElem, [m(this.RepeatedRoot)]) diff --git a/render/hyperscript.js b/render/hyperscript.js index 8f6fd6fed..105c33f46 100644 --- a/render/hyperscript.js +++ b/render/hyperscript.js @@ -76,7 +76,9 @@ function m(selector, attrs, ...children) { } if (attrs == null || typeof attrs === "object" && attrs.tag == null && !Array.isArray(attrs)) { - children = children.length === 1 && Array.isArray(children[0]) ? children[0].slice() : [...children] + children = children.length === 0 && attrs && hasOwn.call(attrs, "children") && Array.isArray(attrs.children) + ? attrs.children.slice() + : children.length === 1 && Array.isArray(children[0]) ? children[0].slice() : [...children] } else { children = children.length === 0 && Array.isArray(attrs) ? attrs.slice() : [attrs, ...children] attrs = undefined @@ -84,12 +86,13 @@ function m(selector, attrs, ...children) { if (attrs == null) attrs = {} - if (typeof selector === "string") { - children = m.normalizeChildren(children) - if (selector !== "[") return execSelector(selector, attrs, children) + if (typeof selector !== "string") { + return Vnode(selector, undefined, Object.assign({children}, attrs), undefined) } - return Vnode(selector, undefined, attrs, children) + children = m.normalizeChildren(children) + if (selector === "[") return Vnode(selector, undefined, attrs, children) + return execSelector(selector, attrs, children) } // Simple and sweet. Also useful for idioms like `onfoo: m.capture` to drop events without diff --git a/render/render.js b/render/render.js index f051ae03b..a6e17a33b 100644 --- a/render/render.js +++ b/render/render.js @@ -18,26 +18,6 @@ function getNameSpace(vnode) { return vnode.attrs && vnode.attrs.xmlns || nameSpace[vnode.tag] } -//sanity check to discourage people from doing `vnode.state = ...` - -//Note: the hook is passed as the `this` argument to allow proxying the -//arguments without requiring a full array allocation to do so. It also -//takes advantage of the fact the current `vnode` is the first argument in -//all lifecycle methods. -function callView(vnode, old) { - try { - var original = vnode.state - var result = hyperscript.normalize(original.view(vnode, old)) - if (result === vnode) throw Error("A view cannot return the vnode it received as argument") - return result - } finally { - if (vnode.state !== original) { - // eslint-disable-next-line no-unsafe-finally - throw new Error("'vnode.state' must not be modified.") - } - } -} - // IE11 (at least) throws an UnspecifiedError when accessing document.activeElement when // inside an iframe. Catch and swallow this error, and heavy-handidly return null. function activeElement(dom) { @@ -113,12 +93,12 @@ function createElement(parent, vnode, hooks, ns, nextSibling) { } } function createComponent(rawParent, parent, vnode, hooks, ns, nextSibling) { - vnode.state = void 0 - vnode.state = (vnode.tag.prototype != null && typeof vnode.tag.prototype.view === "function") ? new vnode.tag(vnode) : vnode.tag(vnode) - vnode.instance = callView(vnode) + var tree = (vnode.state = vnode.tag)(vnode.attrs) + if (typeof tree === "function") tree = (vnode.state = tree)(vnode.attrs) + if (tree === vnode) throw Error("A view cannot return the vnode it received as argument") + vnode.instance = hyperscript.normalize(tree) if (vnode.instance != null) { createNode(rawParent, parent, vnode.instance, hooks, ns, nextSibling) - vnode.dom = vnode.instance.dom } } @@ -408,20 +388,14 @@ function updateElement(old, vnode, hooks, ns) { } } function updateComponent(parent, old, vnode, hooks, nextSibling, ns) { - vnode.state = old.state - vnode.instance = old.instance - vnode.instance = callView(vnode, old) + vnode.instance = hyperscript.normalize((vnode.state = old.state)(vnode.attrs, old.attrs)) if (vnode.instance != null) { + if (vnode.instance === vnode) throw Error("A view cannot return the vnode it received as argument") if (old.instance == null) createNode(parent, parent, vnode.instance, hooks, ns, nextSibling) else updateNode(parent, old.instance, vnode.instance, hooks, nextSibling, ns) - vnode.dom = vnode.instance.dom } else if (old.instance != null) { removeNode(parent, old.instance, false) - vnode.dom = undefined - } - else { - vnode.dom = old.dom } } function getKeyMap(vnodes, start, end) { @@ -560,7 +534,7 @@ function setAttrs(vnode, attrs, ns) { } } function setAttr(vnode, key, old, value, ns, isFileInput) { - if (value == null || key === "is" || (old === value && !isFormAttribute(vnode, key)) && typeof value !== "object" || key === "type" && vnode.tag === "input") return + if (value == null || key === "is" || key === "children" || (old === value && !isFormAttribute(vnode, key)) && typeof value !== "object" || key === "type" && vnode.tag === "input") return if (key.startsWith("on")) updateEvent(vnode, key, value) else if (key.startsWith("xlink:")) vnode.dom.setAttributeNS(xlinkNs, key.slice(6), value) else if (key === "style") updateStyle(vnode.dom, old, value) @@ -593,7 +567,7 @@ function setAttr(vnode, key, old, value, ns, isFileInput) { } } function removeAttr(vnode, key, old, ns) { - if (old == null || key === "is") return + if (old == null || key === "is" || key === "children") return if (key.startsWith("on")) updateEvent(vnode, key, undefined) else if (key.startsWith("xlink:")) vnode.dom.removeAttributeNS(xlinkNs, key.slice(6)) else if (key === "style") updateStyle(vnode.dom, old, null) diff --git a/render/tests/manual/iframe.html b/render/tests/manual/iframe.html index dd070cb01..4669615a5 100644 --- a/render/tests/manual/iframe.html +++ b/render/tests/manual/iframe.html @@ -9,14 +9,8 @@ diff --git a/render/tests/test-component.js b/render/tests/test-component.js index bd897f1ef..c312d6c43 100644 --- a/render/tests/test-component.js +++ b/render/tests/test-component.js @@ -1,7 +1,6 @@ "use strict" var o = require("ospec") -var components = require("../../test-utils/components") var domMock = require("../../test-utils/domMock") var render = require("../../render/render") var m = require("../../render/hyperscript") @@ -13,757 +12,504 @@ o.spec("component", function() { root = $window.document.createElement("div") }) - components.forEach(function(cmp){ - o.spec(cmp.kind, function(){ - var createComponent = cmp.create - - o.spec("basics", function() { - o("works", function() { - var component = createComponent({ - view: function() { - return m("div", {id: "a"}, "b") - } - }) - var node = m(component) - - render(root, node) - - o(root.firstChild.nodeName).equals("DIV") - o(root.firstChild.attributes["id"].value).equals("a") - o(root.firstChild.firstChild.nodeValue).equals("b") - }) - o("receives arguments", function() { - var component = createComponent({ - view: function(vnode) { - return m("div", vnode.attrs, vnode.children) - } - }) - var node = m(component, {id: "a"}, "b") - - render(root, node) - - o(root.firstChild.nodeName).equals("DIV") - o(root.firstChild.attributes["id"].value).equals("a") - o(root.firstChild.firstChild.nodeValue).equals("b") - }) - o("updates", function() { - var component = createComponent({ - view: function(vnode) { - return m("div", vnode.attrs, vnode.children) - } - }) - render(root, [m(component, {id: "a"}, "b")]) - render(root, [m(component, {id: "c"}, "d")]) - - o(root.firstChild.nodeName).equals("DIV") - o(root.firstChild.attributes["id"].value).equals("c") - o(root.firstChild.firstChild.nodeValue).equals("d") - }) - o("updates root from null", function() { - var visible = false - var component = createComponent({ - view: function() { - return visible ? m("div") : null - } - }) - render(root, m(component)) - visible = true - render(root, m(component)) - - o(root.firstChild.nodeName).equals("DIV") - }) - o("updates root from primitive", function() { - var visible = false - var component = createComponent({ - view: function() { - return visible ? m("div") : false - } - }) - render(root, m(component)) - visible = true - render(root, m(component)) - - o(root.firstChild.nodeName).equals("DIV") - }) - o("updates root to null", function() { - var visible = true - var component = createComponent({ - view: function() { - return visible ? m("div") : null - } - }) - render(root, m(component)) - visible = false - render(root, m(component)) - - o(root.childNodes.length).equals(0) - }) - o("updates root to primitive", function() { - var visible = true - var component = createComponent({ - view: function() { - return visible ? m("div") : false - } - }) - render(root, m(component)) - visible = false - render(root, m(component)) - - o(root.childNodes.length).equals(0) - }) - o("updates root from null to null", function() { - var component = createComponent({ - view: function() { - return null - } - }) - render(root, m(component)) - render(root, m(component)) - - o(root.childNodes.length).equals(0) - }) - o("removes", function() { - var component = createComponent({ - view: function() { - return m("div") - } - }) - var div = m("div") - render(root, [m.key(1, m(component)), m.key(2, div)]) - render(root, [m.key(2, div)]) - - o(root.childNodes.length).equals(1) - o(root.firstChild).equals(div.dom) - }) - o("svg works when creating across component boundary", function() { - var component = createComponent({ - view: function() { - return m("g") - } - }) - render(root, m("svg", m(component))) - - o(root.firstChild.firstChild.namespaceURI).equals("http://www.w3.org/2000/svg") - }) - o("svg works when updating across component boundary", function() { - var component = createComponent({ - view: function() { - return m("g") - } - }) - render(root, m("svg", m(component))) - render(root, m("svg", m(component))) - - o(root.firstChild.firstChild.namespaceURI).equals("http://www.w3.org/2000/svg") - }) - }) - o.spec("return value", function() { - o("can return fragments", function() { - var component = createComponent({ - view: function() { - return [ - m("label"), - m("input"), - ] - } - }) - render(root, m(component)) - - o(root.childNodes.length).equals(2) - o(root.childNodes[0].nodeName).equals("LABEL") - o(root.childNodes[1].nodeName).equals("INPUT") - }) - o("can return string", function() { - var component = createComponent({ - view: function() { - return "a" - } - }) - render(root, m(component)) - - o(root.firstChild.nodeType).equals(3) - o(root.firstChild.nodeValue).equals("a") - }) - o("can return falsy string", function() { - var component = createComponent({ - view: function() { - return "" - } - }) - render(root, m(component)) - - o(root.firstChild.nodeType).equals(3) - o(root.firstChild.nodeValue).equals("") - }) - o("can return number", function() { - var component = createComponent({ - view: function() { - return 1 - } - }) - render(root, m(component)) - - o(root.firstChild.nodeType).equals(3) - o(root.firstChild.nodeValue).equals("1") - }) - o("can return falsy number", function() { - var component = createComponent({ - view: function() { - return 0 - } - }) - render(root, m(component)) - - o(root.firstChild.nodeType).equals(3) - o(root.firstChild.nodeValue).equals("0") - }) - o("can return `true`", function() { - var component = createComponent({ - view: function() { - return true - } - }) - render(root, m(component)) - - o(root.childNodes.length).equals(0) - }) - o("can return `false`", function() { - var component = createComponent({ - view: function() { - return false - } - }) - render(root, m(component)) - - o(root.childNodes.length).equals(0) - }) - o("can return null", function() { - var component = createComponent({ - view: function() { - return null - } - }) - render(root, m(component)) - - o(root.childNodes.length).equals(0) - }) - o("can return undefined", function() { - var component = createComponent({ - view: function() { - return undefined - } - }) - render(root, m(component)) - - o(root.childNodes.length).equals(0) - }) - o("throws a custom error if it returns itself when created", function() { - // A view that returns its vnode would otherwise trigger an infinite loop - var threw = false - var component = createComponent({ - view: function(vnode) { - return vnode - } - }) - try { - render(root, m(component)) - } - catch (e) { - threw = true - o(e instanceof Error).equals(true) - // Call stack exception is a RangeError - o(e instanceof RangeError).equals(false) - } - o(threw).equals(true) - }) - o("throws a custom error if it returns itself when updated", function() { - // A view that returns its vnode would otherwise trigger an infinite loop - var threw = false - var init = true - var constructor = o.spy() - var component = createComponent({ - constructor: constructor, - view: function(vnode) { - if (init) return init = false - else return vnode - } - }) - render(root, m(component)) - - o(root.childNodes.length).equals(0) - - try { - render(root, m(component)) - } - catch (e) { - threw = true - o(e instanceof Error).equals(true) - // Call stack exception is a RangeError - o(e instanceof RangeError).equals(false) - } - o(threw).equals(true) - o(constructor.callCount).equals(1) - }) - o("can update when returning fragments", function() { - var component = createComponent({ - view: function() { - return [ - m("label"), - m("input"), - ] - } - }) - render(root, m(component)) - render(root, m(component)) - - o(root.childNodes.length).equals(2) - o(root.childNodes[0].nodeName).equals("LABEL") - o(root.childNodes[1].nodeName).equals("INPUT") - }) - o("can update when returning primitive", function() { - var component = createComponent({ - view: function() { - return "a" - } - }) - render(root, m(component)) - render(root, m(component)) - - o(root.firstChild.nodeType).equals(3) - o(root.firstChild.nodeValue).equals("a") - }) - o("can update when returning null", function() { - var component = createComponent({ - view: function() { - return null - } - }) - render(root, m(component)) - render(root, m(component)) - - o(root.childNodes.length).equals(0) - }) - o("can remove when returning fragments", function() { - var component = createComponent({ - view: function() { - return [ - m("label"), - m("input"), - ] - } - }) - var div = m("div") - render(root, [m.key(1, m(component)), m.key(2, div)]) - - render(root, [m.key(2, m("div"))]) - - o(root.childNodes.length).equals(1) - o(root.firstChild).equals(div.dom) - }) - o("can remove when returning primitive", function() { - var component = createComponent({ - view: function() { - return "a" - } - }) - var div = m("div") - render(root, [m.key(1, m(component)), m.key(2, div)]) - - render(root, [m.key(2, m("div"))]) - - o(root.childNodes.length).equals(1) - o(root.firstChild).equals(div.dom) - }) - }) - o.spec("lifecycle", function() { - o("calls constructor", function() { - var called = 0 - var component = createComponent({ - constructor: function(vnode) { - called++ - - o(vnode.tag).equals(component) - o(vnode.dom).equals(undefined) - o(root.childNodes.length).equals(0) - }, - view: function() { - return m("div", {id: "a"}, "b") - } - }) - - render(root, m(component)) - - o(called).equals(1) - o(root.firstChild.nodeName).equals("DIV") - o(root.firstChild.attributes["id"].value).equals("a") - o(root.firstChild.firstChild.nodeValue).equals("b") - }) - o("calls constructor when returning fragment", function() { - var called = 0 - var component = createComponent({ - constructor: function(vnode) { - called++ - - o(vnode.tag).equals(component) - o(vnode.dom).equals(undefined) - o(root.childNodes.length).equals(0) - }, - view: function() { - return [m("div", {id: "a"}, "b")] - } - }) - - render(root, m(component)) - - o(called).equals(1) - o(root.firstChild.nodeName).equals("DIV") - o(root.firstChild.attributes["id"].value).equals("a") - o(root.firstChild.firstChild.nodeValue).equals("b") - }) - o("calls constructor before view", function() { - var viewCalled = false - var component = createComponent({ - view: function() { - viewCalled = true - return m("div", {id: "a"}, "b") - }, - constructor: function() { - o(viewCalled).equals(false) - }, - }) - - render(root, m(component)) - }) - o("does not calls constructor on redraw", function() { - var init = o.spy() - var component = createComponent({ - view: function() { - return m("div", {id: "a"}, "b") - }, - constructor: init, - }) - - function view() { - return m(component) - } - - render(root, view()) - render(root, view()) - - o(init.callCount).equals(1) - }) - o("calls inner `m.layout` as initial on first render", function() { - var onabort = o.spy() - var layoutSpy = o.spy((_, signal) => { signal.onabort = onabort }) - var component = createComponent({ - view: () => [ - m.layout(layoutSpy), - m("div", {id: "a"}, "b"), - ] - }) - - render(root, m(component)) - - o(layoutSpy.callCount).equals(1) - o(layoutSpy.args[0]).equals(root) - o(layoutSpy.args[1].aborted).equals(false) - o(onabort.callCount).equals(0) - o(layoutSpy.args[2]).equals(true) - o(root.firstChild.nodeName).equals("DIV") - o(root.firstChild.attributes["id"].value).equals("a") - o(root.firstChild.firstChild.nodeValue).equals("b") - }) - o("calls inner `m.layout` as non-initial on subsequent render", function() { - var onabort = o.spy() - var layoutSpy = o.spy((_, signal) => { signal.onabort = onabort }) - var component = createComponent({ - view: () => [ - m.layout(layoutSpy), - m("div", {id: "a"}, "b"), - ] - }) - - render(root, m(component)) - render(root, m(component)) - - o(layoutSpy.callCount).equals(2) - o(layoutSpy.args[0]).equals(root) - o(layoutSpy.args[1].aborted).equals(false) - o(onabort.callCount).equals(0) - o(layoutSpy.args[2]).equals(false) - o(root.firstChild.nodeName).equals("DIV") - o(root.firstChild.attributes["id"].value).equals("a") - o(root.firstChild.firstChild.nodeValue).equals("b") - }) - o("aborts inner `m.layout` signal after first render", function() { - var onabort = o.spy() - var layoutSpy = o.spy((_, signal) => { signal.onabort = onabort }) - var component = createComponent({ - view: () => [ - m.layout(layoutSpy), - m("div", {id: "a"}, "b"), - ] - }) - - render(root, m(component)) - render(root, null) - - o(layoutSpy.callCount).equals(1) - o(layoutSpy.args[1].aborted).equals(true) - o(onabort.callCount).equals(1) - o(root.childNodes.length).equals(0) - }) - o("aborts inner `m.layout` signal after subsequent render", function() { - var onabort = o.spy() - var layoutSpy = o.spy((_, signal) => { signal.onabort = onabort }) - var component = createComponent({ - view: () => [ - m.layout(layoutSpy), - m("div", {id: "a"}, "b"), - ] - }) - - render(root, m(component)) - render(root, m(component)) - render(root, null) - - o(layoutSpy.callCount).equals(2) - o(layoutSpy.args[1].aborted).equals(true) - o(onabort.callCount).equals(1) - o(root.childNodes.length).equals(0) - }) - o("calls in-element inner `m.layout` as initial on first render", function() { - var onabort = o.spy() - var layoutSpy = o.spy((_, signal) => { signal.onabort = onabort }) - var component = createComponent({ - view: () => m("div", {id: "a"}, m.layout(layoutSpy), "b"), - }) - - render(root, m(component)) - - o(layoutSpy.callCount).equals(1) - o(layoutSpy.args[0]).equals(root.firstChild) - o(layoutSpy.args[1].aborted).equals(false) - o(onabort.callCount).equals(0) - o(layoutSpy.args[2]).equals(true) - o(root.firstChild.nodeName).equals("DIV") - o(root.firstChild.attributes["id"].value).equals("a") - o(root.firstChild.firstChild.nodeValue).equals("b") - }) - o("calls in-element inner `m.layout` as non-initial on subsequent render", function() { - var onabort = o.spy() - var layoutSpy = o.spy((_, signal) => { signal.onabort = onabort }) - var component = createComponent({ - view: () => m("div", {id: "a"}, m.layout(layoutSpy), "b"), - }) - - render(root, m(component)) - render(root, m(component)) - - o(layoutSpy.callCount).equals(2) - o(layoutSpy.args[0]).equals(root.firstChild) - o(layoutSpy.args[1].aborted).equals(false) - o(onabort.callCount).equals(0) - o(layoutSpy.args[2]).equals(false) - o(root.firstChild.nodeName).equals("DIV") - o(root.firstChild.attributes["id"].value).equals("a") - o(root.firstChild.firstChild.nodeValue).equals("b") - }) - o("aborts in-element inner `m.layout` signal after first render", function() { - var onabort = o.spy() - var layoutSpy = o.spy((_, signal) => { signal.onabort = onabort }) - var component = createComponent({ - view: () => m("div", {id: "a"}, m.layout(layoutSpy), "b"), - }) - - render(root, m(component)) - render(root, null) - - o(layoutSpy.callCount).equals(1) - o(layoutSpy.args[1].aborted).equals(true) - o(onabort.callCount).equals(1) - o(root.childNodes.length).equals(0) - }) - o("aborts in-element inner `m.layout` signal after subsequent render", function() { - var onabort = o.spy() - var layoutSpy = o.spy((_, signal) => { signal.onabort = onabort }) - var component = createComponent({ - view: () => m("div", {id: "a"}, m.layout(layoutSpy), "b"), - }) - - render(root, m(component)) - render(root, m(component)) - render(root, null) - - o(layoutSpy.callCount).equals(2) - o(layoutSpy.args[1].aborted).equals(true) - o(onabort.callCount).equals(1) - o(root.childNodes.length).equals(0) - }) - o("calls direct inner `m.layout` as initial on first render", function() { - var onabort = o.spy() - var layoutSpy = o.spy((_, signal) => { signal.onabort = onabort }) - var component = createComponent({ - view: () => m.layout(layoutSpy), - }) - - render(root, m(component)) - - o(layoutSpy.callCount).equals(1) - o(layoutSpy.args[0]).equals(root) - o(layoutSpy.args[1].aborted).equals(false) - o(onabort.callCount).equals(0) - o(layoutSpy.args[2]).equals(true) - o(root.childNodes.length).equals(0) - }) - o("calls direct inner `m.layout` as non-initial on subsequent render", function() { - var onabort = o.spy() - var layoutSpy = o.spy((_, signal) => { signal.onabort = onabort }) - var component = createComponent({ - view: () => m.layout(layoutSpy), - }) - - render(root, m(component)) - render(root, m(component)) - - o(layoutSpy.callCount).equals(2) - o(layoutSpy.args[0]).equals(root) - o(layoutSpy.args[1].aborted).equals(false) - o(layoutSpy.args[2]).equals(false) - o(onabort.callCount).equals(0) - o(root.childNodes.length).equals(0) - }) - o("aborts direct inner `m.layout` signal after first render", function() { - var onabort = o.spy() - var layoutSpy = o.spy((_, signal) => { signal.onabort = onabort }) - var component = createComponent({ - view: () => m.layout(layoutSpy), - }) - - render(root, m(component)) - render(root, null) - - o(layoutSpy.callCount).equals(1) - o(layoutSpy.args[1].aborted).equals(true) - o(onabort.callCount).equals(1) - o(root.childNodes.length).equals(0) - }) - o("aborts direct inner `m.layout` signal after subsequent render", function() { - var onabort = o.spy() - var layoutSpy = o.spy((_, signal) => { signal.onabort = onabort }) - var component = createComponent({ - view: () => m.layout(layoutSpy), - }) - - render(root, m(component)) - render(root, m(component)) - render(root, null) - - o(layoutSpy.callCount).equals(2) - o(layoutSpy.args[1].aborted).equals(true) - o(onabort.callCount).equals(1) - o(root.childNodes.length).equals(0) - }) - o("no recycling occurs (was: recycled components get a fresh state)", function() { - var step = 0 - var firstState - var view = o.spy(function(vnode) { - if (step === 0) { - firstState = vnode.state - } else { - o(vnode.state).notEquals(firstState) - } - return m("div") - }) - var component = createComponent({view: view}) - - render(root, [m("div", m.key(1, m(component)))]) - var child = root.firstChild.firstChild - render(root, []) - step = 1 - render(root, [m("div", m.key(1, m(component)))]) - - o(child).notEquals(root.firstChild.firstChild) // this used to be a recycling pool test - o(view.callCount).equals(2) - }) - }) - o.spec("state", function() { - o("initializes state", function() { - var data = {a: 1} - var component = createComponent({ - data: data, - constructor: init, - view: function() { - return "" - } - }) - - render(root, m(component)) - - function init() { - o(this.data).equals(data) - } - }) - o("state proxies to the component object/prototype", function() { - var body = {a: 1} - var data = [body] - var component = createComponent({ - data: data, - constructor: init, - view: function() { - return "" - } - }) - - render(root, m(component)) - - function init() { - o(this.data).equals(data) - o(this.data[0]).equals(body) - } - }) - }) + o.spec("basics", function() { + o("works", function() { + var component = () => m("div", {id: "a"}, "b") + var node = m(component) + + render(root, node) + + o(root.firstChild.nodeName).equals("DIV") + o(root.firstChild.attributes["id"].value).equals("a") + o(root.firstChild.firstChild.nodeValue).equals("b") + }) + o("receives arguments", function() { + var component = (attrs) => m("div", attrs) + var node = m(component, {id: "a"}, "b") + + render(root, node) + + o(root.firstChild.nodeName).equals("DIV") + o(root.firstChild.attributes["id"].value).equals("a") + o(root.firstChild.firstChild.nodeValue).equals("b") + }) + o("updates", function() { + var component = (attrs) => m("div", attrs) + render(root, [m(component, {id: "a"}, "b")]) + render(root, [m(component, {id: "c"}, "d")]) + + o(root.firstChild.nodeName).equals("DIV") + o(root.firstChild.attributes["id"].value).equals("c") + o(root.firstChild.firstChild.nodeValue).equals("d") + }) + o("updates root from null", function() { + var visible = false + var component = () => (visible ? m("div") : null) + render(root, m(component)) + visible = true + render(root, m(component)) + + o(root.firstChild.nodeName).equals("DIV") + }) + o("updates root from primitive", function() { + var visible = false + var component = () => (visible ? m("div") : false) + render(root, m(component)) + visible = true + render(root, m(component)) + + o(root.firstChild.nodeName).equals("DIV") + }) + o("updates root to null", function() { + var visible = true + var component = () => (visible ? m("div") : null) + render(root, m(component)) + visible = false + render(root, m(component)) + + o(root.childNodes.length).equals(0) + }) + o("updates root to primitive", function() { + var visible = true + var component = () => (visible ? m("div") : false) + render(root, m(component)) + visible = false + render(root, m(component)) + + o(root.childNodes.length).equals(0) + }) + o("updates root from null to null", function() { + var component = () => null + render(root, m(component)) + render(root, m(component)) + + o(root.childNodes.length).equals(0) + }) + o("removes", function() { + var component = () => m("div") + var div = m("div") + render(root, [m.key(1, m(component)), m.key(2, div)]) + render(root, [m.key(2, div)]) + + o(root.childNodes.length).equals(1) + o(root.firstChild).equals(div.dom) + }) + o("svg works when creating across component boundary", function() { + var component = () => m("g") + render(root, m("svg", m(component))) + + o(root.firstChild.firstChild.namespaceURI).equals("http://www.w3.org/2000/svg") + }) + o("svg works when updating across component boundary", function() { + var component = () => m("g") + render(root, m("svg", m(component))) + render(root, m("svg", m(component))) + + o(root.firstChild.firstChild.namespaceURI).equals("http://www.w3.org/2000/svg") }) }) - o.spec("Tests specific to certain component kinds", function() { - o.spec("state", function() { - o("Constructible", function() { - var component = o.spy(function(vnode){ - o(vnode.state).equals(undefined) - }) - var view = o.spy(function(){ - o(this instanceof component).equals(true) - return "" - }) - component.prototype.view = view - - render(root, [m(component)]) - render(root, [m(component)]) - render(root, []) - - o(component.callCount).equals(1) - o(view.callCount).equals(2) - }) - o("Closure", function() { - var state - var view = o.spy(function() { - o(this).equals(state) - return "" - }) - var component = o.spy(function(vnode) { - o(vnode.state).equals(undefined) - return state = { - view: view - } - }) - - render(root, [m(component)]) - render(root, [m(component)]) - render(root, []) - - o(component.callCount).equals(1) - o(view.callCount).equals(2) - }) + o.spec("return value", function() { + o("can return fragments", function() { + var component = () => [ + m("label"), + m("input"), + ] + render(root, m(component)) + + o(root.childNodes.length).equals(2) + o(root.childNodes[0].nodeName).equals("LABEL") + o(root.childNodes[1].nodeName).equals("INPUT") + }) + o("can return string", function() { + var component = () => "a" + render(root, m(component)) + + o(root.firstChild.nodeType).equals(3) + o(root.firstChild.nodeValue).equals("a") + }) + o("can return falsy string", function() { + var component = () => "" + render(root, m(component)) + + o(root.firstChild.nodeType).equals(3) + o(root.firstChild.nodeValue).equals("") + }) + o("can return number", function() { + var component = () => 1 + render(root, m(component)) + + o(root.firstChild.nodeType).equals(3) + o(root.firstChild.nodeValue).equals("1") + }) + o("can return falsy number", function() { + var component = () => 0 + render(root, m(component)) + + o(root.firstChild.nodeType).equals(3) + o(root.firstChild.nodeValue).equals("0") + }) + o("can return `true`", function() { + var component = () => true + render(root, m(component)) + + o(root.childNodes.length).equals(0) + }) + o("can return `false`", function() { + var component = () => false + render(root, m(component)) + + o(root.childNodes.length).equals(0) + }) + o("can return null", function() { + var component = () => null + render(root, m(component)) + + o(root.childNodes.length).equals(0) + }) + o("can return undefined", function() { + var component = () => undefined + render(root, m(component)) + + o(root.childNodes.length).equals(0) + }) + o("throws a custom error if it returns itself when created", function() { + // A view that returns its vnode would otherwise trigger an infinite loop + var threw = false + var component = () => vnode + var vnode = m(component) + try { + render(root, vnode) + } + catch (e) { + threw = true + o(e instanceof Error).equals(true) + // Call stack exception is a RangeError + o(e instanceof RangeError).equals(false) + } + o(threw).equals(true) + }) + o("throws a custom error if it returns itself when updated", function() { + // A view that returns its vnode would otherwise trigger an infinite loop + var threw = false + var component = () => vnode + render(root, m(component)) + + o(root.childNodes.length).equals(0) + + var vnode = m(component) + try { + render(root, m(component)) + } + catch (e) { + threw = true + o(e instanceof Error).equals(true) + // Call stack exception is a RangeError + o(e instanceof RangeError).equals(false) + } + o(threw).equals(true) + }) + o("can update when returning fragments", function() { + var component = () => [ + m("label"), + m("input"), + ] + render(root, m(component)) + render(root, m(component)) + + o(root.childNodes.length).equals(2) + o(root.childNodes[0].nodeName).equals("LABEL") + o(root.childNodes[1].nodeName).equals("INPUT") + }) + o("can update when returning primitive", function() { + var component = () => "a" + render(root, m(component)) + render(root, m(component)) + + o(root.firstChild.nodeType).equals(3) + o(root.firstChild.nodeValue).equals("a") + }) + o("can update when returning null", function() { + var component = () => null + render(root, m(component)) + render(root, m(component)) + + o(root.childNodes.length).equals(0) + }) + o("can remove when returning fragments", function() { + var component = () => [ + m("label"), + m("input"), + ] + var div = m("div") + render(root, [m.key(1, m(component)), m.key(2, div)]) + + render(root, [m.key(2, m("div"))]) + + o(root.childNodes.length).equals(1) + o(root.firstChild).equals(div.dom) + }) + o("can remove when returning primitive", function() { + var component = () => "a" + var div = m("div") + render(root, [m.key(1, m(component)), m.key(2, div)]) + + render(root, [m.key(2, m("div"))]) + + o(root.childNodes.length).equals(1) + o(root.firstChild).equals(div.dom) + }) + }) + o.spec("lifecycle", function() { + o("constructs", function() { + var called = 0 + var component = () => { + called++ + + o(root.childNodes.length).equals(0) + + return () => m("div", {id: "a"}, "b") + } + + render(root, m(component)) + + o(called).equals(1) + o(root.firstChild.nodeName).equals("DIV") + o(root.firstChild.attributes["id"].value).equals("a") + o(root.firstChild.firstChild.nodeValue).equals("b") + }) + o("constructs when returning fragment", function() { + var called = 0 + var component = () => { + called++ + + o(root.childNodes.length).equals(0) + + return () => [m("div", {id: "a"}, "b")] + } + + render(root, m(component)) + + o(called).equals(1) + o(root.firstChild.nodeName).equals("DIV") + o(root.firstChild.attributes["id"].value).equals("a") + o(root.firstChild.firstChild.nodeValue).equals("b") + }) + o("can call view function returned on initialization", function() { + var viewCalled = false + var component = () => { + o(viewCalled).equals(false) + return () => { + viewCalled = true + return m("div", {id: "a"}, "b") + } + } + + render(root, m(component)) + }) + o("does not initialize on redraw", function() { + var component = o.spy(() => () => m("div", {id: "a"}, "b")) + + function view() { + return m(component) + } + + render(root, view()) + render(root, view()) + + o(component.callCount).equals(1) + }) + o("calls inner `m.layout` as initial on first render", function() { + var onabort = o.spy() + var layoutSpy = o.spy((_, signal) => { signal.onabort = onabort }) + var component = () => [ + m.layout(layoutSpy), + m("div", {id: "a"}, "b"), + ] + + render(root, m(component)) + + o(layoutSpy.callCount).equals(1) + o(layoutSpy.args[0]).equals(root) + o(layoutSpy.args[1].aborted).equals(false) + o(onabort.callCount).equals(0) + o(layoutSpy.args[2]).equals(true) + o(root.firstChild.nodeName).equals("DIV") + o(root.firstChild.attributes["id"].value).equals("a") + o(root.firstChild.firstChild.nodeValue).equals("b") + }) + o("calls inner `m.layout` as non-initial on subsequent render", function() { + var onabort = o.spy() + var layoutSpy = o.spy((_, signal) => { signal.onabort = onabort }) + var component = () => [ + m.layout(layoutSpy), + m("div", {id: "a"}, "b"), + ] + + render(root, m(component)) + render(root, m(component)) + + o(layoutSpy.callCount).equals(2) + o(layoutSpy.args[0]).equals(root) + o(layoutSpy.args[1].aborted).equals(false) + o(onabort.callCount).equals(0) + o(layoutSpy.args[2]).equals(false) + o(root.firstChild.nodeName).equals("DIV") + o(root.firstChild.attributes["id"].value).equals("a") + o(root.firstChild.firstChild.nodeValue).equals("b") + }) + o("aborts inner `m.layout` signal after first render", function() { + var onabort = o.spy() + var layoutSpy = o.spy((_, signal) => { signal.onabort = onabort }) + var component = () => [ + m.layout(layoutSpy), + m("div", {id: "a"}, "b"), + ] + + render(root, m(component)) + render(root, null) + + o(layoutSpy.callCount).equals(1) + o(layoutSpy.args[1].aborted).equals(true) + o(onabort.callCount).equals(1) + o(root.childNodes.length).equals(0) + }) + o("aborts inner `m.layout` signal after subsequent render", function() { + var onabort = o.spy() + var layoutSpy = o.spy((_, signal) => { signal.onabort = onabort }) + var component = () => [ + m.layout(layoutSpy), + m("div", {id: "a"}, "b"), + ] + + render(root, m(component)) + render(root, m(component)) + render(root, null) + + o(layoutSpy.callCount).equals(2) + o(layoutSpy.args[1].aborted).equals(true) + o(onabort.callCount).equals(1) + o(root.childNodes.length).equals(0) + }) + o("calls in-element inner `m.layout` as initial on first render", function() { + var onabort = o.spy() + var layoutSpy = o.spy((_, signal) => { signal.onabort = onabort }) + var component = () => m("div", {id: "a"}, m.layout(layoutSpy), "b") + render(root, m(component)) + + o(layoutSpy.callCount).equals(1) + o(layoutSpy.args[0]).equals(root.firstChild) + o(layoutSpy.args[1].aborted).equals(false) + o(onabort.callCount).equals(0) + o(layoutSpy.args[2]).equals(true) + o(root.firstChild.nodeName).equals("DIV") + o(root.firstChild.attributes["id"].value).equals("a") + o(root.firstChild.firstChild.nodeValue).equals("b") + }) + o("calls in-element inner `m.layout` as non-initial on subsequent render", function() { + var onabort = o.spy() + var layoutSpy = o.spy((_, signal) => { signal.onabort = onabort }) + var component = () => m("div", {id: "a"}, m.layout(layoutSpy), "b") + render(root, m(component)) + render(root, m(component)) + + o(layoutSpy.callCount).equals(2) + o(layoutSpy.args[0]).equals(root.firstChild) + o(layoutSpy.args[1].aborted).equals(false) + o(onabort.callCount).equals(0) + o(layoutSpy.args[2]).equals(false) + o(root.firstChild.nodeName).equals("DIV") + o(root.firstChild.attributes["id"].value).equals("a") + o(root.firstChild.firstChild.nodeValue).equals("b") + }) + o("aborts in-element inner `m.layout` signal after first render", function() { + var onabort = o.spy() + var layoutSpy = o.spy((_, signal) => { signal.onabort = onabort }) + var component = () => m("div", {id: "a"}, m.layout(layoutSpy), "b") + render(root, m(component)) + render(root, null) + + o(layoutSpy.callCount).equals(1) + o(layoutSpy.args[1].aborted).equals(true) + o(onabort.callCount).equals(1) + o(root.childNodes.length).equals(0) + }) + o("aborts in-element inner `m.layout` signal after subsequent render", function() { + var onabort = o.spy() + var layoutSpy = o.spy((_, signal) => { signal.onabort = onabort }) + var component = () => m("div", {id: "a"}, m.layout(layoutSpy), "b") + render(root, m(component)) + render(root, m(component)) + render(root, null) + + o(layoutSpy.callCount).equals(2) + o(layoutSpy.args[1].aborted).equals(true) + o(onabort.callCount).equals(1) + o(root.childNodes.length).equals(0) + }) + o("calls direct inner `m.layout` as initial on first render", function() { + var onabort = o.spy() + var layoutSpy = o.spy((_, signal) => { signal.onabort = onabort }) + var component = () => m.layout(layoutSpy) + render(root, m(component)) + + o(layoutSpy.callCount).equals(1) + o(layoutSpy.args[0]).equals(root) + o(layoutSpy.args[1].aborted).equals(false) + o(onabort.callCount).equals(0) + o(layoutSpy.args[2]).equals(true) + o(root.childNodes.length).equals(0) + }) + o("calls direct inner `m.layout` as non-initial on subsequent render", function() { + var onabort = o.spy() + var layoutSpy = o.spy((_, signal) => { signal.onabort = onabort }) + var component = () => m.layout(layoutSpy) + render(root, m(component)) + render(root, m(component)) + + o(layoutSpy.callCount).equals(2) + o(layoutSpy.args[0]).equals(root) + o(layoutSpy.args[1].aborted).equals(false) + o(layoutSpy.args[2]).equals(false) + o(onabort.callCount).equals(0) + o(root.childNodes.length).equals(0) + }) + o("aborts direct inner `m.layout` signal after first render", function() { + var onabort = o.spy() + var layoutSpy = o.spy((_, signal) => { signal.onabort = onabort }) + var component = () => m.layout(layoutSpy) + render(root, m(component)) + render(root, null) + + o(layoutSpy.callCount).equals(1) + o(layoutSpy.args[1].aborted).equals(true) + o(onabort.callCount).equals(1) + o(root.childNodes.length).equals(0) + }) + o("aborts direct inner `m.layout` signal after subsequent render", function() { + var onabort = o.spy() + var layoutSpy = o.spy((_, signal) => { signal.onabort = onabort }) + var component = () => m.layout(layoutSpy) + render(root, m(component)) + render(root, m(component)) + render(root, null) + + o(layoutSpy.callCount).equals(2) + o(layoutSpy.args[1].aborted).equals(true) + o(onabort.callCount).equals(1) + o(root.childNodes.length).equals(0) + }) + o("no recycling occurs (was: recycled components get a fresh state)", function() { + var layout = o.spy() + var component = o.spy(() => m("div", m.layout(layout))) + + render(root, [m("div", m.key(1, m(component)))]) + var child = root.firstChild.firstChild + render(root, []) + render(root, [m("div", m.key(1, m(component)))]) + + o(child).notEquals(root.firstChild.firstChild) // this used to be a recycling pool test + o(component.callCount).equals(2) + o(layout.calls.map((c) => c.args[2])).deepEquals([true, true]) }) }) }) diff --git a/render/tests/test-hyperscript.js b/render/tests/test-hyperscript.js index 572392bd9..97c37df05 100644 --- a/render/tests/test-hyperscript.js +++ b/render/tests/test-hyperscript.js @@ -439,6 +439,14 @@ o.spec("hyperscript", function() { o(vnode.children[0].children).equals("0") }) + o("handles children in attributes", function() { + var vnode = m("div", {children: ["", "a"]}) + + o(vnode.children[0].tag).equals("#") + o(vnode.children[0].children).equals("") + o(vnode.children[1].tag).equals("#") + o(vnode.children[1].children).equals("a") + }) }) o.spec("permutations", function() { o("handles null attr and children", function() { @@ -637,8 +645,8 @@ o.spec("hyperscript", function() { o(vnode.tag).equals(component) o(vnode.attrs.id).equals("a") - o(vnode.children.length).equals(1) - o(vnode.children[0]).equals("b") + o(vnode.attrs.children.length).equals(1) + o(vnode.attrs.children[0]).equals("b") }) o("works with closures", function () { var component = o.spy() @@ -649,8 +657,8 @@ o.spec("hyperscript", function() { o(vnode.tag).equals(component) o(vnode.attrs.id).equals("a") - o(vnode.children.length).equals(1) - o(vnode.children[0]).equals("b") + o(vnode.attrs.children.length).equals(1) + o(vnode.attrs.children[0]).equals("b") }) }) diff --git a/render/tests/test-normalizeComponentChildren.js b/render/tests/test-normalizeComponentChildren.js index e268bd99b..5a6757633 100644 --- a/render/tests/test-normalizeComponentChildren.js +++ b/render/tests/test-normalizeComponentChildren.js @@ -10,18 +10,14 @@ o.spec("component children", function () { var root = $window.document.createElement("div") o.spec("component children", function () { - var component = () => ({ - view: function (vnode) { - return vnode.children - } - }) + var component = (attrs) => attrs.children var vnode = m(component, "a") render(root, vnode) o("are not normalized on ingestion", function () { - o(vnode.children[0]).equals("a") + o(vnode.attrs.children[0]).equals("a") }) o("are normalized upon view interpolation", function () { diff --git a/render/tests/test-onremove.js b/render/tests/test-onremove.js index d8eeecefb..65a9f800d 100644 --- a/render/tests/test-onremove.js +++ b/render/tests/test-onremove.js @@ -1,7 +1,6 @@ "use strict" var o = require("ospec") -var components = require("../../test-utils/components") var domMock = require("../../test-utils/domMock") var render = require("../../render/render") var m = require("../../render/hyperscript") @@ -69,42 +68,24 @@ o.spec("layout remove", function() { o(vnode.dom).notEquals(updated.dom) // this used to be a recycling pool test o(remove.callCount).equals(1) }) - components.forEach(function(cmp){ - o.spec(cmp.kind, function(){ - var createComponent = cmp.create - - o("aborts layout signal on nested component", function() { - var spy = o.spy() - var comp = createComponent({ - view: function() {return m(outer)} - }) - var outer = createComponent({ - view: function() {return m(inner)} - }) - var inner = createComponent({ - view: () => m.layout(spy), - }) - render(root, m(comp)) - render(root, null) - - o(spy.callCount).equals(1) - }) - o("aborts layout signal on nested component child", function() { - var spy = o.spy() - var comp = createComponent({ - view: function() {return m(outer)} - }) - var outer = createComponent({ - view: function() {return m(inner, m("a", layoutRemove(spy)))} - }) - var inner = createComponent({ - view: function(vnode) {return m("div", vnode.children)} - }) - render(root, m(comp)) - render(root, null) - - o(spy.callCount).equals(1) - }) - }) + o("aborts layout signal on nested component", function() { + var spy = o.spy() + var comp = () => m(outer) + var outer = () => m(inner) + var inner = () => m.layout(spy) + render(root, m(comp)) + render(root, null) + + o(spy.callCount).equals(1) + }) + o("aborts layout signal on nested component child", function() { + var spy = o.spy() + var comp = () => m(outer) + var outer = () => m(inner, m("a", layoutRemove(spy))) + var inner = (attrs) => m("div", attrs.children) + render(root, m(comp)) + render(root, null) + + o(spy.callCount).equals(1) }) }) diff --git a/render/tests/test-render.js b/render/tests/test-render.js index 4104ccd35..c671bd0fa 100644 --- a/render/tests/test-render.js +++ b/render/tests/test-render.js @@ -58,44 +58,22 @@ o.spec("render", function() { o(threw).equals(true) }) - o("tries to re-initialize a constructible component whose view has thrown", function() { - var A = o.spy() - var view = o.spy(() => { throw new Error("error") }) - A.prototype.view = view - var throwCount = 0 - - try {render(root, m(A))} catch (e) {throwCount++} - - o(throwCount).equals(1) - o(A.callCount).equals(1) - o(view.callCount).equals(1) - - try {render(root, m(A))} catch (e) {throwCount++} - - o(throwCount).equals(2) - o(A.callCount).equals(2) - o(view.callCount).equals(2) - }) - o("tries to re-initialize a constructible component whose constructor has thrown", function() { + o("tries to re-initialize a component that threw on create", function() { var A = o.spy(() => { throw new Error("error") }) - var view = o.spy() - A.prototype.view = view var throwCount = 0 try {render(root, m(A))} catch (e) {throwCount++} o(throwCount).equals(1) o(A.callCount).equals(1) - o(view.callCount).equals(0) try {render(root, m(A))} catch (e) {throwCount++} o(throwCount).equals(2) o(A.callCount).equals(2) - o(view.callCount).equals(0) }) - o("tries to re-initialize a closure component whose view has thrown", function() { - var A = o.spy(() => ({view})) + o("tries to re-initialize a stateful component whose view threw on create", function() { + var A = o.spy(() => view) var view = o.spy(() => { throw new Error("error") }) var throwCount = 0 try {render(root, m(A))} catch (e) {throwCount++} @@ -110,19 +88,6 @@ o.spec("render", function() { o(A.callCount).equals(2) o(view.callCount).equals(2) }) - o("tries to re-initialize a closure component whose closure has thrown", function() { - var A = o.spy(() => { throw new Error("error") }) - var throwCount = 0 - try {render(root, m(A))} catch (e) {throwCount++} - - o(throwCount).equals(1) - o(A.callCount).equals(1) - - try {render(root, m(A))} catch (e) {throwCount++} - - o(throwCount).equals(2) - o(A.callCount).equals(2) - }) o("lifecycle methods work in keyed children of recycled keyed", function() { var onabortA = o.spy() var onabortB = o.spy() @@ -287,10 +252,8 @@ o.spec("render", function() { var thrown = [] function A() { try {render(root, m(A))} catch (e) {thrown.push("construct")} - return { - view: function() { - try {render(root, m(A))} catch (e) {thrown.push("view")} - }, + return () => { + try {render(root, m(A))} catch (e) {thrown.push("view")} } } render(root, m(A)) diff --git a/render/tests/test-retain.js b/render/tests/test-retain.js index c79623dbd..747c6f5f7 100644 --- a/render/tests/test-retain.js +++ b/render/tests/test-retain.js @@ -1,7 +1,6 @@ "use strict" var o = require("ospec") -var components = require("../../test-utils/components") var domMock = require("../../test-utils/domMock") var render = require("../render") var m = require("../hyperscript") @@ -41,67 +40,45 @@ o.spec("retain", function() { o(() => render(root, m.retain())).throws(Error) }) - components.forEach(function(cmp){ - o.spec(cmp.kind, function(){ - var createComponent = cmp.create - - o("prevents update in component", function() { - var component = createComponent({ - view(vnode, old) { - if (old) return m.retain() - return m("div", vnode.children) - }, - }) - var vnode = m(component, "a") - var updated = m(component, "b") - - render(root, vnode) - render(root, updated) - - o(root.firstChild.firstChild.nodeValue).equals("a") - o(updated.instance).deepEquals(vnode.instance) - }) - - o("prevents update in component and for component", function() { - var component = createComponent({ - view(vnode, old) { - if (old) return m.retain() - return m("div", {id: vnode.attrs.id}) - }, - }) - var vnode = m(component, {id: "a"}) - var updated = m.retain() - - render(root, vnode) - render(root, updated) - - o(root.firstChild.attributes["id"].value).equals("a") - o(updated).deepEquals(vnode) - }) - - o("prevents update for component but not in component", function() { - var component = createComponent({ - view(vnode) { - return m("div", {id: vnode.attrs.id}) - }, - }) - var vnode = m(component, {id: "a"}) - var updated = m.retain() - - render(root, vnode) - render(root, updated) - - o(root.firstChild.attributes["id"].value).equals("a") - o(updated).deepEquals(vnode) - }) - - o("throws if used on component creation", function() { - var component = createComponent({ - view: () => m.retain(), - }) - - o(() => render(root, m(component))).throws(Error) - }) - }) + o("prevents update in component", function() { + var component = (vnode, old) => (old ? m.retain() : m("div", vnode.children)) + var vnode = m(component, "a") + var updated = m(component, "b") + + render(root, vnode) + render(root, updated) + + o(root.firstChild.firstChild.nodeValue).equals("a") + o(updated.instance).deepEquals(vnode.instance) + }) + + o("prevents update in component and for component", function() { + var component = ({id}, old) => (old ? m.retain() : m("div", {id})) + var vnode = m(component, {id: "a"}) + var updated = m.retain() + + render(root, vnode) + render(root, updated) + + o(root.firstChild.attributes["id"].value).equals("a") + o(updated).deepEquals(vnode) + }) + + o("prevents update for component but not in component", function() { + var component = ({id}) => m("div", {id}) + var vnode = m(component, {id: "a"}) + var updated = m.retain() + + render(root, vnode) + render(root, updated) + + o(root.firstChild.attributes["id"].value).equals("a") + o(updated).deepEquals(vnode) + }) + + o("throws if used on component creation", function() { + var component = () => m.retain() + + o(() => render(root, m(component))).throws(Error) }) }) diff --git a/render/tests/test-updateNodes.js b/render/tests/test-updateNodes.js index 59adc1900..1d32b8220 100644 --- a/render/tests/test-updateNodes.js +++ b/render/tests/test-updateNodes.js @@ -1,7 +1,6 @@ "use strict" var o = require("ospec") -var components = require("../../test-utils/components") var domMock = require("../../test-utils/domMock") var render = require("../../render/render") var m = require("../../render/hyperscript") @@ -998,55 +997,47 @@ o.spec("updateNodes", function() { o(tagNames).deepEquals(expectedTagNames) }) - components.forEach(function(cmp){ - o.spec(cmp.kind, function(){ - var createComponent = cmp.create - - o("fragment child toggles from null when followed by null component then tag", function() { - var component = createComponent({view: function() {return null}}) - var vnodes = [m.fragment(m("a"), m(component), m("b"))] - var temp = [m.fragment(null, m(component), m("b"))] - var updated = [m.fragment(m("a"), m(component), m("b"))] - - render(root, vnodes) - render(root, temp) - render(root, updated) - - o(root.childNodes.length).equals(2) - o(root.childNodes[0].nodeName).equals("A") - o(root.childNodes[1].nodeName).equals("B") - }) - o("fragment child toggles from null in component when followed by null component then tag", function() { - var flag = true - var a = createComponent({view: function() {return flag ? m("a") : null}}) - var b = createComponent({view: function() {return null}}) - var vnodes = [m.fragment(m(a), m(b), m("s"))] - var temp = [m.fragment(m(a), m(b), m("s"))] - var updated = [m.fragment(m(a), m(b), m("s"))] - - render(root, vnodes) - flag = false - render(root, temp) - flag = true - render(root, updated) - - o(root.childNodes.length).equals(2) - o(root.childNodes[0].nodeName).equals("A") - o(root.childNodes[1].nodeName).equals("S") - }) - o("removing a component that returns a fragment doesn't throw (regression test for incidental bug introduced while debugging some Flems)", function() { - var component = createComponent({ - view: function() {return m.fragment(m("a"), m("b"))} - }) - try { - render(root, [m(component)]) - render(root, []) - - o(root.childNodes.length).equals(0) - } catch (e) { - o(e).equals(null) - } - }) - }) + o("fragment child toggles from null when followed by null component then tag", function() { + var component = () => null + var vnodes = [m.fragment(m("a"), m(component), m("b"))] + var temp = [m.fragment(null, m(component), m("b"))] + var updated = [m.fragment(m("a"), m(component), m("b"))] + + render(root, vnodes) + render(root, temp) + render(root, updated) + + o(root.childNodes.length).equals(2) + o(root.childNodes[0].nodeName).equals("A") + o(root.childNodes[1].nodeName).equals("B") + }) + o("fragment child toggles from null in component when followed by null component then tag", function() { + var flag = true + var a = () => (flag ? m("a") : null) + var b = () => null + var vnodes = [m.fragment(m(a), m(b), m("s"))] + var temp = [m.fragment(m(a), m(b), m("s"))] + var updated = [m.fragment(m(a), m(b), m("s"))] + + render(root, vnodes) + flag = false + render(root, temp) + flag = true + render(root, updated) + + o(root.childNodes.length).equals(2) + o(root.childNodes[0].nodeName).equals("A") + o(root.childNodes[1].nodeName).equals("S") + }) + o("removing a component that returns a fragment doesn't throw (regression test for incidental bug introduced while debugging some Flems)", function() { + var component = () => m.fragment(m("a"), m("b")) + try { + render(root, [m(component)]) + render(root, []) + + o(root.childNodes.length).equals(0) + } catch (e) { + o(e).equals(null) + } }) }) diff --git a/test-utils/components.js b/test-utils/components.js deleted file mode 100644 index ed95808bc..000000000 --- a/test-utils/components.js +++ /dev/null @@ -1,42 +0,0 @@ -"use strict" - -var m = require("../render/hyperscript") - -module.exports = [ - { - kind: "constructible", - create: function(methods) { - if (!methods) methods = {} - var constructor = methods.constructor - if (constructor) delete methods.constructor - class C { - constructor(vnode) { - if (typeof constructor === "function") { - constructor.call(this, vnode) - } - } - view() { - return m("div") - } - } - Object.assign(C.prototype, methods) - return C - } - }, { - kind: "closure", - create: function(methods) { - if (!methods) methods = {} - var constructor = methods.constructor - if (constructor) delete methods.constructor - return (vnode) => { - var result = Object.assign({view: () => m("div")}, methods) - - if (typeof constructor === "function") { - constructor.call(result, vnode) - } - - return result - } - } - } -] diff --git a/test-utils/tests/test-components.js b/test-utils/tests/test-components.js deleted file mode 100644 index 3f6829099..000000000 --- a/test-utils/tests/test-components.js +++ /dev/null @@ -1,51 +0,0 @@ -"use strict" - -var o = require("ospec") -var components = require("../../test-utils/components") -var m = require("../../render/hyperscript") - -o.spec("test-utils/components", function() { - var test = o.spy(function(component) { - return function() { - o("works", function() { - o(typeof component.kind).equals("string") - - var methods = {view: function(){}} - - var cmp1, cmp2 - - if (component.kind === "constructible") { - cmp1 = new (component.create()) - cmp2 = new (component.create(methods)) - } else if (component.kind === "closure") { - cmp1 = component.create()() - cmp2 = component.create(methods)() - } else { - throw new Error("unexpected component kind") - } - - o(cmp1 != null).equals(true) - o(typeof cmp1.view).equals("function") - - var vnode = cmp1.view() - - o(vnode != null).equals(true) - o(vnode).deepEquals(m("div")) - - if (component.kind !== "constructible") { - o(cmp2).deepEquals(methods) - } else { - // deepEquals doesn't search the prototype, do it manually - o(cmp2 != null).equals(true) - o(cmp2.view).equals(methods.view) - } - }) - } - }) - o.after(function(){ - o(test.callCount).equals(2) - }) - components.forEach(function(component) { - o.spec(component.kind, test(component)) - }) -}) diff --git a/util/init.js b/util/init.js index 8c2a9a624..caaf98a58 100644 --- a/util/init.js +++ b/util/init.js @@ -2,10 +2,6 @@ var m = require("../render/hyperscript") -var Init = () => ({ - view: ({attrs: {f}}, o) => ( - o ? m.retain() : m.layout((_, signal) => queueMicrotask(() => f(signal))) - ) -}) +var Init = ({f}, o) => (o ? m.retain() : m.layout((_, signal) => queueMicrotask(() => f(signal)))) module.exports = (f) => m(Init, {f}) diff --git a/util/lazy.js b/util/lazy.js index 64cb0615f..9a6ba2092 100644 --- a/util/lazy.js +++ b/util/lazy.js @@ -5,9 +5,9 @@ var m = require("../render/hyperscript") module.exports = (opts, redraw = mountRedraw.redraw) => { var fetched = false - var Comp = () => ({view: () => opts.pending && opts.pending()}) + var Comp = () => opts.pending && opts.pending() var e = new ReferenceError("Component not found") - var ShowError = () => ({view: () => opts.error && opts.error(e)}) + var ShowError = () => opts.error && opts.error(e) return () => { if (!fetched) { @@ -30,6 +30,6 @@ module.exports = (opts, redraw = mountRedraw.redraw) => { ) } - return {view: ({attrs}) => m(Comp, attrs)} + return (attrs) => m(Comp, attrs) } } diff --git a/util/tests/test-lazy.js b/util/tests/test-lazy.js index e031b9791..51908473b 100644 --- a/util/tests/test-lazy.js +++ b/util/tests/test-lazy.js @@ -1,7 +1,6 @@ "use strict" var o = require("ospec") -var components = require("../../test-utils/components") var domMock = require("../../test-utils/domMock") var hyperscript = require("../../render/hyperscript") var makeLazy = require("../lazy") @@ -18,591 +17,577 @@ o.spec("lazy", () => { console.error = consoleError }) - components.forEach((cmp) => { - o.spec(cmp.kind, () => { - - void [{name: "direct", wrap: (v) => v}, {name: "in module with default", wrap: (v) => ({default:v})}].forEach(({name, wrap}) => { - var createComponent = (methods) => wrap(cmp.create(methods)) - o.spec(name, () => { - o("works with only fetch and success", () => { - var calls = [] - var scheduled = 1 - var component = createComponent({ - view(vnode) { - calls.push(`view ${vnode.attrs.name}`) - return hyperscript("div", {id: "a"}, "b") - } - }) - var send, notifyRedrawn - var fetchRedrawn = new Promise((resolve) => notifyRedrawn = resolve) - var C = makeLazy({ - fetch() { - calls.push("fetch") - return new Promise((resolve) => send = resolve) - }, - }, () => { - notifyRedrawn() - calls.push(`scheduled ${scheduled++}`) - }) - - o(calls).deepEquals([]) - - render(root, [ - hyperscript(C, {name: "one"}), - hyperscript(C, {name: "two"}), - ]) - - o(calls).deepEquals([ - "fetch", - ]) - - send(component) - - return fetchRedrawn.then(() => { - o(calls).deepEquals([ - "fetch", - "scheduled 1", - ]) - - render(root, [ - hyperscript(C, {name: "one"}), - hyperscript(C, {name: "two"}), - ]) - - o(calls).deepEquals([ - "fetch", - "scheduled 1", - "view one", - "view two", - ]) - - render(root, [ - hyperscript(C, {name: "one"}), - hyperscript(C, {name: "two"}), - ]) - - o(calls).deepEquals([ - "fetch", - "scheduled 1", - "view one", - "view two", - "view one", - "view two", - ]) - }) - }) - - o("works with only fetch and failure", () => { - var error = new Error("test") - var calls = [] - console.error = (e) => { - calls.push("error", e.message) - } - var scheduled = 1 - var send, notifyRedrawn - var fetchRedrawn = new Promise((resolve) => notifyRedrawn = resolve) - var C = makeLazy({ - fetch() { - calls.push("fetch") - return new Promise((_, reject) => send = reject) - }, - }, () => { - notifyRedrawn() - calls.push(`scheduled ${scheduled++}`) - }) - - o(calls).deepEquals([]) - - render(root, [ - hyperscript(C, {name: "one"}), - hyperscript(C, {name: "two"}), - ]) - - o(calls).deepEquals([ - "fetch", - ]) - - send(error) - - return fetchRedrawn.then(() => { - o(calls).deepEquals([ - "fetch", - "error", "test", - "scheduled 1", - ]) - - render(root, [ - hyperscript(C, {name: "one"}), - hyperscript(C, {name: "two"}), - ]) - - o(calls).deepEquals([ - "fetch", - "error", "test", - "scheduled 1", - ]) - - render(root, [ - hyperscript(C, {name: "one"}), - hyperscript(C, {name: "two"}), - ]) - - o(calls).deepEquals([ - "fetch", - "error", "test", - "scheduled 1", - ]) - }) - }) - - o("works with fetch + pending and success", () => { - var calls = [] - var scheduled = 1 - var component = createComponent({ - view(vnode) { - calls.push(`view ${vnode.attrs.name}`) - return hyperscript("div", {id: "a"}, "b") - } - }) - var send, notifyRedrawn - var fetchRedrawn = new Promise((resolve) => notifyRedrawn = resolve) - var C = makeLazy({ - fetch() { - calls.push("fetch") - return new Promise((resolve) => send = resolve) - }, - pending() { - calls.push("pending") - }, - }, () => { - notifyRedrawn() - calls.push(`scheduled ${scheduled++}`) - }) - - o(calls).deepEquals([]) - - render(root, [ - hyperscript(C, {name: "one"}), - hyperscript(C, {name: "two"}), - ]) - - o(calls).deepEquals([ - "fetch", - "pending", - "pending", - ]) - - send(component) - - return fetchRedrawn.then(() => { - o(calls).deepEquals([ - "fetch", - "pending", - "pending", - "scheduled 1", - ]) - - render(root, [ - hyperscript(C, {name: "one"}), - hyperscript(C, {name: "two"}), - ]) - - o(calls).deepEquals([ - "fetch", - "pending", - "pending", - "scheduled 1", - "view one", - "view two", - ]) - - render(root, [ - hyperscript(C, {name: "one"}), - hyperscript(C, {name: "two"}), - ]) - - o(calls).deepEquals([ - "fetch", - "pending", - "pending", - "scheduled 1", - "view one", - "view two", - "view one", - "view two", - ]) - }) - }) - - o("works with fetch + pending and failure", () => { - var error = new Error("test") - var calls = [] - console.error = (e) => { - calls.push("error", e.message) - } - var scheduled = 1 - var send, notifyRedrawn - var fetchRedrawn = new Promise((resolve) => notifyRedrawn = resolve) - var C = makeLazy({ - fetch() { - calls.push("fetch") - return new Promise((_, reject) => send = reject) - }, - pending() { - calls.push("pending") - }, - }, () => { - notifyRedrawn() - calls.push(`scheduled ${scheduled++}`) - }) - - o(calls).deepEquals([]) - - render(root, [ - hyperscript(C, {name: "one"}), - hyperscript(C, {name: "two"}), - ]) - - o(calls).deepEquals([ - "fetch", - "pending", - "pending", - ]) - - send(error) - - return fetchRedrawn.then(() => { - o(calls).deepEquals([ - "fetch", - "pending", - "pending", - "error", "test", - "scheduled 1", - ]) - - render(root, [ - hyperscript(C, {name: "one"}), - hyperscript(C, {name: "two"}), - ]) - - o(calls).deepEquals([ - "fetch", - "pending", - "pending", - "error", "test", - "scheduled 1", - ]) - - render(root, [ - hyperscript(C, {name: "one"}), - hyperscript(C, {name: "two"}), - ]) - - o(calls).deepEquals([ - "fetch", - "pending", - "pending", - "error", "test", - "scheduled 1", - ]) - }) - }) - - o("works with fetch + error and success", () => { - var calls = [] - var scheduled = 1 - var component = createComponent({ - view(vnode) { - calls.push(`view ${vnode.attrs.name}`) - return hyperscript("div", {id: "a"}, "b") - } - }) - var send, notifyRedrawn - var fetchRedrawn = new Promise((resolve) => notifyRedrawn = resolve) - var C = makeLazy({ - fetch() { - calls.push("fetch") - return new Promise((resolve) => send = resolve) - }, - error() { - calls.push("error") - }, - }, () => { - notifyRedrawn() - calls.push(`scheduled ${scheduled++}`) - }) - - o(calls).deepEquals([]) - - render(root, [ - hyperscript(C, {name: "one"}), - hyperscript(C, {name: "two"}), - ]) - - o(calls).deepEquals([ - "fetch", - ]) - - send(component) - - return fetchRedrawn.then(() => { - o(calls).deepEquals([ - "fetch", - "scheduled 1", - ]) - - render(root, [ - hyperscript(C, {name: "one"}), - hyperscript(C, {name: "two"}), - ]) - - o(calls).deepEquals([ - "fetch", - "scheduled 1", - "view one", - "view two", - ]) - - render(root, [ - hyperscript(C, {name: "one"}), - hyperscript(C, {name: "two"}), - ]) - - o(calls).deepEquals([ - "fetch", - "scheduled 1", - "view one", - "view two", - "view one", - "view two", - ]) - }) - }) - - o("works with fetch + error and failure", () => { - var error = new Error("test") - var calls = [] - var scheduled = 1 - var send, notifyRedrawn - var fetchRedrawn = new Promise((resolve) => notifyRedrawn = resolve) - var C = makeLazy({ - fetch() { - calls.push("fetch") - return new Promise((_, reject) => send = reject) - }, - error(e) { - calls.push("error", e.message) - }, - }, () => { - notifyRedrawn() - calls.push(`scheduled ${scheduled++}`) - }) - - o(calls).deepEquals([]) - - render(root, [ - hyperscript(C, {name: "one"}), - hyperscript(C, {name: "two"}), - ]) - - o(calls).deepEquals([ - "fetch", - ]) - - send(error) - - return fetchRedrawn.then(() => { - o(calls).deepEquals([ - "fetch", - "scheduled 1", - ]) - - render(root, [ - hyperscript(C, {name: "one"}), - hyperscript(C, {name: "two"}), - ]) - - o(calls).deepEquals([ - "fetch", - "scheduled 1", - "error", "test", - "error", "test", - ]) - - render(root, [ - hyperscript(C, {name: "one"}), - hyperscript(C, {name: "two"}), - ]) - - o(calls).deepEquals([ - "fetch", - "scheduled 1", - "error", "test", - "error", "test", - "error", "test", - "error", "test", - ]) - }) - }) - - o("works with all hooks and success", () => { - var calls = [] - var scheduled = 1 - var component = createComponent({ - view(vnode) { - calls.push(`view ${vnode.attrs.name}`) - return hyperscript("div", {id: "a"}, "b") - } - }) - var send, notifyRedrawn - var fetchRedrawn = new Promise((resolve) => notifyRedrawn = resolve) - var C = makeLazy({ - fetch() { - calls.push("fetch") - return new Promise((resolve) => send = resolve) - }, - pending() { - calls.push("pending") - }, - error() { - calls.push("error") - }, - }, () => { - notifyRedrawn() - calls.push(`scheduled ${scheduled++}`) - }) - - o(calls).deepEquals([]) - - render(root, [ - hyperscript(C, {name: "one"}), - hyperscript(C, {name: "two"}), - ]) - - o(calls).deepEquals([ - "fetch", - "pending", - "pending", - ]) - - send(component) - - return fetchRedrawn.then(() => { - o(calls).deepEquals([ - "fetch", - "pending", - "pending", - "scheduled 1", - ]) - - render(root, [ - hyperscript(C, {name: "one"}), - hyperscript(C, {name: "two"}), - ]) - - o(calls).deepEquals([ - "fetch", - "pending", - "pending", - "scheduled 1", - "view one", - "view two", - ]) - - render(root, [ - hyperscript(C, {name: "one"}), - hyperscript(C, {name: "two"}), - ]) - - o(calls).deepEquals([ - "fetch", - "pending", - "pending", - "scheduled 1", - "view one", - "view two", - "view one", - "view two", - ]) - }) - }) - - o("works with all hooks and failure", () => { - var error = new Error("test") - var calls = [] - var scheduled = 1 - var send, notifyRedrawn - var fetchRedrawn = new Promise((resolve) => notifyRedrawn = resolve) - var C = makeLazy({ - fetch() { - calls.push("fetch") - return new Promise((_, reject) => send = reject) - }, - pending() { - calls.push("pending") - }, - error(e) { - calls.push("error", e.message) - }, - }, () => { - notifyRedrawn() - calls.push(`scheduled ${scheduled++}`) - }) - - o(calls).deepEquals([]) - - render(root, [ - hyperscript(C, {name: "one"}), - hyperscript(C, {name: "two"}), - ]) - - o(calls).deepEquals([ - "fetch", - "pending", - "pending", - ]) - - send(error) - - return fetchRedrawn.then(() => { - o(calls).deepEquals([ - "fetch", - "pending", - "pending", - "scheduled 1", - ]) - - render(root, [ - hyperscript(C, {name: "one"}), - hyperscript(C, {name: "two"}), - ]) - - o(calls).deepEquals([ - "fetch", - "pending", - "pending", - "scheduled 1", - "error", "test", - "error", "test", - ]) - - render(root, [ - hyperscript(C, {name: "one"}), - hyperscript(C, {name: "two"}), - ]) - - o(calls).deepEquals([ - "fetch", - "pending", - "pending", - "scheduled 1", - "error", "test", - "error", "test", - "error", "test", - "error", "test", - ]) - }) - }) + void [{name: "direct", wrap: (v) => v}, {name: "in module with default", wrap: (v) => ({default:v})}].forEach(({name, wrap}) => { + o.spec(name, () => { + o("works with only fetch and success", () => { + var calls = [] + var scheduled = 1 + var component = wrap(({name}) => { + calls.push(`view ${name}`) + return hyperscript("div", {id: "a"}, "b") + }) + var send, notifyRedrawn + var fetchRedrawn = new Promise((resolve) => notifyRedrawn = resolve) + var C = makeLazy({ + fetch() { + calls.push("fetch") + return new Promise((resolve) => send = resolve) + }, + }, () => { + notifyRedrawn() + calls.push(`scheduled ${scheduled++}`) + }) + + o(calls).deepEquals([]) + + render(root, [ + hyperscript(C, {name: "one"}), + hyperscript(C, {name: "two"}), + ]) + + o(calls).deepEquals([ + "fetch", + ]) + + send(component) + + return fetchRedrawn.then(() => { + o(calls).deepEquals([ + "fetch", + "scheduled 1", + ]) + + render(root, [ + hyperscript(C, {name: "one"}), + hyperscript(C, {name: "two"}), + ]) + + o(calls).deepEquals([ + "fetch", + "scheduled 1", + "view one", + "view two", + ]) + + render(root, [ + hyperscript(C, {name: "one"}), + hyperscript(C, {name: "two"}), + ]) + + o(calls).deepEquals([ + "fetch", + "scheduled 1", + "view one", + "view two", + "view one", + "view two", + ]) + }) + }) + + o("works with only fetch and failure", () => { + var error = new Error("test") + var calls = [] + console.error = (e) => { + calls.push("error", e.message) + } + var scheduled = 1 + var send, notifyRedrawn + var fetchRedrawn = new Promise((resolve) => notifyRedrawn = resolve) + var C = makeLazy({ + fetch() { + calls.push("fetch") + return new Promise((_, reject) => send = reject) + }, + }, () => { + notifyRedrawn() + calls.push(`scheduled ${scheduled++}`) + }) + + o(calls).deepEquals([]) + + render(root, [ + hyperscript(C, {name: "one"}), + hyperscript(C, {name: "two"}), + ]) + + o(calls).deepEquals([ + "fetch", + ]) + + send(error) + + return fetchRedrawn.then(() => { + o(calls).deepEquals([ + "fetch", + "error", "test", + "scheduled 1", + ]) + + render(root, [ + hyperscript(C, {name: "one"}), + hyperscript(C, {name: "two"}), + ]) + + o(calls).deepEquals([ + "fetch", + "error", "test", + "scheduled 1", + ]) + + render(root, [ + hyperscript(C, {name: "one"}), + hyperscript(C, {name: "two"}), + ]) + + o(calls).deepEquals([ + "fetch", + "error", "test", + "scheduled 1", + ]) + }) + }) + + o("works with fetch + pending and success", () => { + var calls = [] + var scheduled = 1 + var component = wrap(({name}) => { + calls.push(`view ${name}`) + return hyperscript("div", {id: "a"}, "b") + }) + var send, notifyRedrawn + var fetchRedrawn = new Promise((resolve) => notifyRedrawn = resolve) + var C = makeLazy({ + fetch() { + calls.push("fetch") + return new Promise((resolve) => send = resolve) + }, + pending() { + calls.push("pending") + }, + }, () => { + notifyRedrawn() + calls.push(`scheduled ${scheduled++}`) + }) + + o(calls).deepEquals([]) + + render(root, [ + hyperscript(C, {name: "one"}), + hyperscript(C, {name: "two"}), + ]) + + o(calls).deepEquals([ + "fetch", + "pending", + "pending", + ]) + + send(component) + + return fetchRedrawn.then(() => { + o(calls).deepEquals([ + "fetch", + "pending", + "pending", + "scheduled 1", + ]) + + render(root, [ + hyperscript(C, {name: "one"}), + hyperscript(C, {name: "two"}), + ]) + + o(calls).deepEquals([ + "fetch", + "pending", + "pending", + "scheduled 1", + "view one", + "view two", + ]) + + render(root, [ + hyperscript(C, {name: "one"}), + hyperscript(C, {name: "two"}), + ]) + + o(calls).deepEquals([ + "fetch", + "pending", + "pending", + "scheduled 1", + "view one", + "view two", + "view one", + "view two", + ]) + }) + }) + + o("works with fetch + pending and failure", () => { + var error = new Error("test") + var calls = [] + console.error = (e) => { + calls.push("error", e.message) + } + var scheduled = 1 + var send, notifyRedrawn + var fetchRedrawn = new Promise((resolve) => notifyRedrawn = resolve) + var C = makeLazy({ + fetch() { + calls.push("fetch") + return new Promise((_, reject) => send = reject) + }, + pending() { + calls.push("pending") + }, + }, () => { + notifyRedrawn() + calls.push(`scheduled ${scheduled++}`) + }) + + o(calls).deepEquals([]) + + render(root, [ + hyperscript(C, {name: "one"}), + hyperscript(C, {name: "two"}), + ]) + + o(calls).deepEquals([ + "fetch", + "pending", + "pending", + ]) + + send(error) + + return fetchRedrawn.then(() => { + o(calls).deepEquals([ + "fetch", + "pending", + "pending", + "error", "test", + "scheduled 1", + ]) + + render(root, [ + hyperscript(C, {name: "one"}), + hyperscript(C, {name: "two"}), + ]) + + o(calls).deepEquals([ + "fetch", + "pending", + "pending", + "error", "test", + "scheduled 1", + ]) + + render(root, [ + hyperscript(C, {name: "one"}), + hyperscript(C, {name: "two"}), + ]) + + o(calls).deepEquals([ + "fetch", + "pending", + "pending", + "error", "test", + "scheduled 1", + ]) + }) + }) + + o("works with fetch + error and success", () => { + var calls = [] + var scheduled = 1 + var component = wrap(({name}) => { + calls.push(`view ${name}`) + return hyperscript("div", {id: "a"}, "b") + }) + var send, notifyRedrawn + var fetchRedrawn = new Promise((resolve) => notifyRedrawn = resolve) + var C = makeLazy({ + fetch() { + calls.push("fetch") + return new Promise((resolve) => send = resolve) + }, + error() { + calls.push("error") + }, + }, () => { + notifyRedrawn() + calls.push(`scheduled ${scheduled++}`) + }) + + o(calls).deepEquals([]) + + render(root, [ + hyperscript(C, {name: "one"}), + hyperscript(C, {name: "two"}), + ]) + + o(calls).deepEquals([ + "fetch", + ]) + + send(component) + + return fetchRedrawn.then(() => { + o(calls).deepEquals([ + "fetch", + "scheduled 1", + ]) + + render(root, [ + hyperscript(C, {name: "one"}), + hyperscript(C, {name: "two"}), + ]) + + o(calls).deepEquals([ + "fetch", + "scheduled 1", + "view one", + "view two", + ]) + + render(root, [ + hyperscript(C, {name: "one"}), + hyperscript(C, {name: "two"}), + ]) + + o(calls).deepEquals([ + "fetch", + "scheduled 1", + "view one", + "view two", + "view one", + "view two", + ]) + }) + }) + + o("works with fetch + error and failure", () => { + var error = new Error("test") + var calls = [] + var scheduled = 1 + var send, notifyRedrawn + var fetchRedrawn = new Promise((resolve) => notifyRedrawn = resolve) + var C = makeLazy({ + fetch() { + calls.push("fetch") + return new Promise((_, reject) => send = reject) + }, + error(e) { + calls.push("error", e.message) + }, + }, () => { + notifyRedrawn() + calls.push(`scheduled ${scheduled++}`) + }) + + o(calls).deepEquals([]) + + render(root, [ + hyperscript(C, {name: "one"}), + hyperscript(C, {name: "two"}), + ]) + + o(calls).deepEquals([ + "fetch", + ]) + + send(error) + + return fetchRedrawn.then(() => { + o(calls).deepEquals([ + "fetch", + "scheduled 1", + ]) + + render(root, [ + hyperscript(C, {name: "one"}), + hyperscript(C, {name: "two"}), + ]) + + o(calls).deepEquals([ + "fetch", + "scheduled 1", + "error", "test", + "error", "test", + ]) + + render(root, [ + hyperscript(C, {name: "one"}), + hyperscript(C, {name: "two"}), + ]) + + o(calls).deepEquals([ + "fetch", + "scheduled 1", + "error", "test", + "error", "test", + "error", "test", + "error", "test", + ]) + }) + }) + + o("works with all hooks and success", () => { + var calls = [] + var scheduled = 1 + var component = wrap(({name}) => { + calls.push(`view ${name}`) + return hyperscript("div", {id: "a"}, "b") + }) + var send, notifyRedrawn + var fetchRedrawn = new Promise((resolve) => notifyRedrawn = resolve) + var C = makeLazy({ + fetch() { + calls.push("fetch") + return new Promise((resolve) => send = resolve) + }, + pending() { + calls.push("pending") + }, + error() { + calls.push("error") + }, + }, () => { + notifyRedrawn() + calls.push(`scheduled ${scheduled++}`) + }) + + o(calls).deepEquals([]) + + render(root, [ + hyperscript(C, {name: "one"}), + hyperscript(C, {name: "two"}), + ]) + + o(calls).deepEquals([ + "fetch", + "pending", + "pending", + ]) + + send(component) + + return fetchRedrawn.then(() => { + o(calls).deepEquals([ + "fetch", + "pending", + "pending", + "scheduled 1", + ]) + + render(root, [ + hyperscript(C, {name: "one"}), + hyperscript(C, {name: "two"}), + ]) + + o(calls).deepEquals([ + "fetch", + "pending", + "pending", + "scheduled 1", + "view one", + "view two", + ]) + + render(root, [ + hyperscript(C, {name: "one"}), + hyperscript(C, {name: "two"}), + ]) + + o(calls).deepEquals([ + "fetch", + "pending", + "pending", + "scheduled 1", + "view one", + "view two", + "view one", + "view two", + ]) + }) + }) + + o("works with all hooks and failure", () => { + var error = new Error("test") + var calls = [] + var scheduled = 1 + var send, notifyRedrawn + var fetchRedrawn = new Promise((resolve) => notifyRedrawn = resolve) + var C = makeLazy({ + fetch() { + calls.push("fetch") + return new Promise((_, reject) => send = reject) + }, + pending() { + calls.push("pending") + }, + error(e) { + calls.push("error", e.message) + }, + }, () => { + notifyRedrawn() + calls.push(`scheduled ${scheduled++}`) + }) + + o(calls).deepEquals([]) + + render(root, [ + hyperscript(C, {name: "one"}), + hyperscript(C, {name: "two"}), + ]) + + o(calls).deepEquals([ + "fetch", + "pending", + "pending", + ]) + + send(error) + + return fetchRedrawn.then(() => { + o(calls).deepEquals([ + "fetch", + "pending", + "pending", + "scheduled 1", + ]) + + render(root, [ + hyperscript(C, {name: "one"}), + hyperscript(C, {name: "two"}), + ]) + + o(calls).deepEquals([ + "fetch", + "pending", + "pending", + "scheduled 1", + "error", "test", + "error", "test", + ]) + + render(root, [ + hyperscript(C, {name: "one"}), + hyperscript(C, {name: "two"}), + ]) + + o(calls).deepEquals([ + "fetch", + "pending", + "pending", + "scheduled 1", + "error", "test", + "error", "test", + "error", "test", + "error", "test", + ]) }) }) }) diff --git a/util/use.js b/util/use.js index 77b683fb9..6fa764149 100644 --- a/util/use.js +++ b/util/use.js @@ -4,17 +4,15 @@ var m = require("../render/hyperscript") var Use = () => { var key = 0 - return { - view: (v, o) => { - if (o && !( - v.attrs.d.length === o.attrs.d.length && - v.attrs.d.every((b, i) => Object.is(b, o.attrs.d[i])) - )) { - key++ - } - - return m.key(key, v.children) + return (n, o) => { + if (o && !( + n.d.length === o.d.length && + n.d.every((b, i) => Object.is(b, o.d[i])) + )) { + key++ } + + return m.key(key, n.children) } } From d652f983b4f692ad13f90dc141577bd01c61389c Mon Sep 17 00:00:00 2001 From: Claudia Meadows Date: Thu, 3 Oct 2024 22:10:58 -0700 Subject: [PATCH 39/95] Drop `m.fragment`, try to create attrs object less in hyperscript factory --- api/tests/test-mountRedraw.js | 2 +- render/hyperscript.js | 25 ++- render/tests/test-createFragment.js | 11 +- render/tests/test-createNodes.js | 7 +- render/tests/test-fragment.js | 264 ++++++++++++++++++---------- render/tests/test-oncreate.js | 2 +- render/tests/test-onremove.js | 2 +- render/tests/test-onupdate.js | 4 +- render/tests/test-retain.js | 2 +- render/tests/test-updateFragment.js | 33 ++-- render/tests/test-updateNodes.js | 64 ++++--- tests/test-api.js | 4 +- 12 files changed, 243 insertions(+), 177 deletions(-) diff --git a/api/tests/test-mountRedraw.js b/api/tests/test-mountRedraw.js index ee9ed990d..40e8da1c1 100644 --- a/api/tests/test-mountRedraw.js +++ b/api/tests/test-mountRedraw.js @@ -157,7 +157,7 @@ o.spec("mount/redraw", function() { o("should invoke remove callback on unmount", function() { var onabort = o.spy() - var spy = o.spy(() => h.fragment(h.layout((_, signal) => { signal.onabort = onabort }))) + var spy = o.spy(() => h.layout((_, signal) => { signal.onabort = onabort })) m.mount(root, spy) o(spy.callCount).equals(1) diff --git a/render/hyperscript.js b/render/hyperscript.js index 105c33f46..4d7043f31 100644 --- a/render/hyperscript.js +++ b/render/hyperscript.js @@ -42,6 +42,7 @@ function compileSelector(selector) { } function execSelector(selector, attrs, children) { + attrs = attrs || {} var hasClassName = hasOwn.call(attrs, "className") var dynamicClass = hasClassName ? attrs.className : attrs.class var state = selectorCache.get(selector) @@ -65,7 +66,7 @@ function execSelector(selector, attrs, children) { if (hasClassName) attrs.className = null } - return Vnode(state.tag, undefined, attrs, children) + return Vnode(state.tag, undefined, attrs, m.normalizeChildren(children)) } // Caution is advised when editing this - it's very perf-critical. It's specially designed to avoid @@ -84,15 +85,13 @@ function m(selector, attrs, ...children) { attrs = undefined } - if (attrs == null) attrs = {} - - if (typeof selector !== "string") { + if (typeof selector === "string") { + return execSelector(selector, attrs, children) + } else if (selector === m.Fragment) { + return Vnode("[", undefined, undefined, m.normalizeChildren(children)) + } else { return Vnode(selector, undefined, Object.assign({children}, attrs), undefined) } - - children = m.normalizeChildren(children) - if (selector === "[") return Vnode(selector, undefined, attrs, children) - return execSelector(selector, attrs, children) } // Simple and sweet. Also useful for idioms like `onfoo: m.capture` to drop events without @@ -107,14 +106,12 @@ m.retain = () => Vnode("!", undefined, undefined, undefined) m.layout = (f) => Vnode(">", f, undefined, undefined) -var simpleVnode = (tag, state, ...children) => - Vnode(tag, state, undefined, m.normalizeChildren( +m.Fragment = (attrs) => attrs.children +m.key = (key, ...children) => + Vnode("=", key, undefined, m.normalizeChildren( children.length === 1 && Array.isArray(children[0]) ? children[0].slice() : [...children] )) -m.fragment = (...children) => simpleVnode("[", undefined, ...children) -m.key = (key, ...children) => simpleVnode("=", key, ...children) - m.normalize = (node) => { if (node == null || typeof node === "boolean") return null if (typeof node !== "object") return Vnode("#", undefined, undefined, String(node)) @@ -150,6 +147,4 @@ m.normalizeChildren = (input) => { return input } -m.Fragment = "[" - module.exports = m diff --git a/render/tests/test-createFragment.js b/render/tests/test-createFragment.js index 663326c34..51bfc6099 100644 --- a/render/tests/test-createFragment.js +++ b/render/tests/test-createFragment.js @@ -4,7 +4,6 @@ var o = require("ospec") var domMock = require("../../test-utils/domMock") var render = require("../../render/render") var m = require("../../render/hyperscript") -var fragment = require("../../render/hyperscript").fragment o.spec("createFragment", function() { var $window, root @@ -14,28 +13,28 @@ o.spec("createFragment", function() { }) o("creates fragment", function() { - var vnode = fragment(m("a")) + var vnode = m.normalize([m("a")]) render(root, vnode) o(root.childNodes.length).equals(1) o(root.childNodes[0].nodeName).equals("A") }) o("handles empty fragment", function() { - var vnode = fragment() + var vnode = m.normalize([]) render(root, vnode) o(vnode.dom).equals(null) o(root.childNodes.length).equals(0) }) o("handles childless fragment", function() { - var vnode = fragment() + var vnode = m.normalize([]) render(root, vnode) o(vnode.dom).equals(null) o(root.childNodes.length).equals(0) }) o("handles multiple children", function() { - var vnode = fragment(m("a"), m("b")) + var vnode = m.normalize([m("a"), m("b")]) render(root, vnode) o(root.childNodes.length).equals(2) @@ -44,7 +43,7 @@ o.spec("createFragment", function() { o(vnode.dom).equals(root.childNodes[0]) }) o("handles td", function() { - var vnode = fragment(m("td")) + var vnode = m.normalize([m("td")]) render(root, vnode) o(root.childNodes.length).equals(1) diff --git a/render/tests/test-createNodes.js b/render/tests/test-createNodes.js index b4afdda05..3be9902de 100644 --- a/render/tests/test-createNodes.js +++ b/render/tests/test-createNodes.js @@ -4,7 +4,6 @@ var o = require("ospec") var domMock = require("../../test-utils/domMock") var render = require("../../render/render") var m = require("../../render/hyperscript") -var fragment = require("../../render/hyperscript").fragment o.spec("createNodes", function() { var $window, root @@ -17,7 +16,7 @@ o.spec("createNodes", function() { var vnodes = [ m("a"), "b", - fragment("c"), + ["c"], ] render(root, vnodes) @@ -31,7 +30,7 @@ o.spec("createNodes", function() { m("a"), "b", null, - fragment("c"), + ["c"], ] render(root, vnodes) @@ -45,7 +44,7 @@ o.spec("createNodes", function() { m("a"), "b", undefined, - fragment("c"), + ["c"], ] render(root, vnodes) diff --git a/render/tests/test-fragment.js b/render/tests/test-fragment.js index bfab1ae78..dd16d4e95 100644 --- a/render/tests/test-fragment.js +++ b/render/tests/test-fragment.js @@ -3,101 +3,181 @@ var o = require("ospec") var m = require("../../render/hyperscript") -function runTest(name, fragment) { - o.spec(name, function() { - o("works", function() { - var child = m("p") - var frag = fragment(child) - - o(frag.tag).equals("[") - - o(Array.isArray(frag.children)).equals(true) - o(frag.children.length).equals(1) - o(frag.children[0]).equals(child) - }) - o.spec("children", function() { - o("handles string single child", function() { - var vnode = fragment(["a"]) - - o(vnode.children[0].tag).equals("#") - o(vnode.children[0].children).equals("a") - }) - o("handles falsy string single child", function() { - var vnode = fragment([""]) - - o(vnode.children[0].tag).equals("#") - o(vnode.children[0].children).equals("") - }) - o("handles number single child", function() { - var vnode = fragment([1]) - - o(vnode.children[0].tag).equals("#") - o(vnode.children[0].children).equals("1") - }) - o("handles falsy number single child", function() { - var vnode = fragment([0]) - - o(vnode.children[0].tag).equals("#") - o(vnode.children[0].children).equals("0") - }) - o("handles boolean single child", function() { - var vnode = fragment([true]) - - o(vnode.children).deepEquals([null]) - }) - o("handles falsy boolean single child", function() { - var vnode = fragment([false]) - - o(vnode.children).deepEquals([null]) - }) - o("handles null single child", function() { - var vnode = fragment([null]) - - o(vnode.children[0]).equals(null) - }) - o("handles undefined single child", function() { - var vnode = fragment([undefined]) - - o(vnode.children).deepEquals([null]) - }) - o("handles multiple string children", function() { - var vnode = fragment(["", "a"]) - - o(vnode.children[0].tag).equals("#") - o(vnode.children[0].children).equals("") - o(vnode.children[1].tag).equals("#") - o(vnode.children[1].children).equals("a") - }) - o("handles multiple number children", function() { - var vnode = fragment([0, 1]) - - o(vnode.children[0].tag).equals("#") - o(vnode.children[0].children).equals("0") - o(vnode.children[1].tag).equals("#") - o(vnode.children[1].children).equals("1") - }) - o("handles multiple boolean children", function() { - var vnode = fragment([false, true]) - - o(vnode.children).deepEquals([null, null]) - }) - o("handles multiple null/undefined child", function() { - var vnode = fragment([null, undefined]) - - o(vnode.children).deepEquals([null, null]) - }) - o("handles falsy number single child without attrs", function() { - var vnode = fragment(0) - - o(vnode.children[0].tag).equals("#") - o(vnode.children[0].children).equals("0") - }) +o.spec("fragment literal", function() { + o("works", function() { + var child = m("p") + var frag = m.normalize([child]) + + o(frag.tag).equals("[") + + o(Array.isArray(frag.children)).equals(true) + o(frag.children.length).equals(1) + o(frag.children[0]).equals(child) + }) + o.spec("children", function() { + o("handles string single child", function() { + var vnode = m.normalize(["a"]) + + o(vnode.children[0].tag).equals("#") + o(vnode.children[0].children).equals("a") + }) + o("handles falsy string single child", function() { + var vnode = m.normalize([""]) + + o(vnode.children[0].tag).equals("#") + o(vnode.children[0].children).equals("") + }) + o("handles number single child", function() { + var vnode = m.normalize([1]) + + o(vnode.children[0].tag).equals("#") + o(vnode.children[0].children).equals("1") + }) + o("handles falsy number single child", function() { + var vnode = m.normalize([0]) + + o(vnode.children[0].tag).equals("#") + o(vnode.children[0].children).equals("0") + }) + o("handles boolean single child", function() { + var vnode = m.normalize([true]) + + o(vnode.children).deepEquals([null]) + }) + o("handles falsy boolean single child", function() { + var vnode = m.normalize([false]) + + o(vnode.children).deepEquals([null]) + }) + o("handles null single child", function() { + var vnode = m.normalize([null]) + + o(vnode.children[0]).equals(null) + }) + o("handles undefined single child", function() { + var vnode = m.normalize([undefined]) + + o(vnode.children).deepEquals([null]) + }) + o("handles multiple string children", function() { + var vnode = m.normalize(["", "a"]) + + o(vnode.children[0].tag).equals("#") + o(vnode.children[0].children).equals("") + o(vnode.children[1].tag).equals("#") + o(vnode.children[1].children).equals("a") + }) + o("handles multiple number children", function() { + var vnode = m.normalize([0, 1]) + + o(vnode.children[0].tag).equals("#") + o(vnode.children[0].children).equals("0") + o(vnode.children[1].tag).equals("#") + o(vnode.children[1].children).equals("1") + }) + o("handles multiple boolean children", function() { + var vnode = m.normalize([false, true]) + + o(vnode.children).deepEquals([null, null]) + }) + o("handles multiple null/undefined child", function() { + var vnode = m.normalize([null, undefined]) + + o(vnode.children).deepEquals([null, null]) }) }) -} +}) + +o.spec("fragment component", function() { + o("works", function() { + var child = m("p") + var frag = m(m.Fragment, null, child) + + o(frag.tag).equals("[") -runTest("fragment", m.fragment); -runTest("fragment-string-selector", (...children) => m("[", ...children)); + o(Array.isArray(frag.children)).equals(true) + o(frag.children.length).equals(1) + o(frag.children[0]).equals(child) + }) + o.spec("children", function() { + o("handles string single child", function() { + var vnode = m(m.Fragment, null, ["a"]) + + o(vnode.children[0].tag).equals("#") + o(vnode.children[0].children).equals("a") + }) + o("handles falsy string single child", function() { + var vnode = m(m.Fragment, null, [""]) + + o(vnode.children[0].tag).equals("#") + o(vnode.children[0].children).equals("") + }) + o("handles number single child", function() { + var vnode = m(m.Fragment, null, [1]) + + o(vnode.children[0].tag).equals("#") + o(vnode.children[0].children).equals("1") + }) + o("handles falsy number single child", function() { + var vnode = m(m.Fragment, null, [0]) + + o(vnode.children[0].tag).equals("#") + o(vnode.children[0].children).equals("0") + }) + o("handles boolean single child", function() { + var vnode = m(m.Fragment, null, [true]) + + o(vnode.children).deepEquals([null]) + }) + o("handles falsy boolean single child", function() { + var vnode = m(m.Fragment, null, [false]) + + o(vnode.children).deepEquals([null]) + }) + o("handles null single child", function() { + var vnode = m(m.Fragment, null, [null]) + + o(vnode.children[0]).equals(null) + }) + o("handles undefined single child", function() { + var vnode = m(m.Fragment, null, [undefined]) + + o(vnode.children).deepEquals([null]) + }) + o("handles multiple string children", function() { + var vnode = m(m.Fragment, null, ["", "a"]) + + o(vnode.children[0].tag).equals("#") + o(vnode.children[0].children).equals("") + o(vnode.children[1].tag).equals("#") + o(vnode.children[1].children).equals("a") + }) + o("handles multiple number children", function() { + var vnode = m(m.Fragment, null, [0, 1]) + + o(vnode.children[0].tag).equals("#") + o(vnode.children[0].children).equals("0") + o(vnode.children[1].tag).equals("#") + o(vnode.children[1].children).equals("1") + }) + o("handles multiple boolean children", function() { + var vnode = m(m.Fragment, null, [false, true]) + + o(vnode.children).deepEquals([null, null]) + }) + o("handles multiple null/undefined child", function() { + var vnode = m(m.Fragment, null, [null, undefined]) + + o(vnode.children).deepEquals([null, null]) + }) + o("handles falsy number single child without attrs", function() { + var vnode = m(m.Fragment, null, 0) + + o(vnode.children[0].tag).equals("#") + o(vnode.children[0].children).equals("0") + }) + }) +}) o.spec("key", function() { o("works", function() { diff --git a/render/tests/test-oncreate.js b/render/tests/test-oncreate.js index 5d73491e6..ce91beaf2 100644 --- a/render/tests/test-oncreate.js +++ b/render/tests/test-oncreate.js @@ -35,7 +35,7 @@ o.spec("layout create", function() { }) o("works when creating fragment", function() { var callback = o.spy() - var vnode = m.fragment(m.layout(callback)) + var vnode = [m.layout(callback)] render(root, vnode) diff --git a/render/tests/test-onremove.js b/render/tests/test-onremove.js index 65a9f800d..2fd5f17e0 100644 --- a/render/tests/test-onremove.js +++ b/render/tests/test-onremove.js @@ -48,7 +48,7 @@ o.spec("layout remove", function() { }) o("aborts layout signal when removing fragment", function() { var remove = o.spy() - var vnode = m.fragment(layoutRemove(remove)) + var vnode = [layoutRemove(remove)] render(root, vnode) render(root, []) diff --git a/render/tests/test-onupdate.js b/render/tests/test-onupdate.js index 61cf1549c..1c6db75ab 100644 --- a/render/tests/test-onupdate.js +++ b/render/tests/test-onupdate.js @@ -82,8 +82,8 @@ o.spec("layout update", function() { o("invoked on updating fragment", function() { var layout = o.spy() var update = o.spy() - var vnode = m.fragment(m.layout(layout)) - var updated = m.fragment(m.layout(update)) + var vnode = [m.layout(layout)] + var updated = [m.layout(update)] render(root, vnode) render(root, updated) diff --git a/render/tests/test-retain.js b/render/tests/test-retain.js index 747c6f5f7..43853e20c 100644 --- a/render/tests/test-retain.js +++ b/render/tests/test-retain.js @@ -26,7 +26,7 @@ o.spec("retain", function() { }) o("prevents update in fragment", function() { - var vnode = m.fragment("a") + var vnode = m.normalize(["a"]) var updated = m.retain() render(root, vnode) diff --git a/render/tests/test-updateFragment.js b/render/tests/test-updateFragment.js index 52538f743..2469c318a 100644 --- a/render/tests/test-updateFragment.js +++ b/render/tests/test-updateFragment.js @@ -13,56 +13,53 @@ o.spec("updateFragment", function() { }) o("updates fragment", function() { - var vnode = m.fragment(m("a")) - var updated = m.fragment(m("b")) + var vnode = [m("a")] + var updated = [m("b")] render(root, vnode) render(root, updated) - o(updated.dom).equals(root.firstChild) - o(updated.dom.nodeName).equals("B") + o(updated[0].dom).equals(root.firstChild) + o(updated[0].dom.nodeName).equals("B") }) o("adds els", function() { - var vnode = m.fragment() - var updated = m.fragment(m("a"), m("b")) + var vnode = [] + var updated = [m("a"), m("b")] render(root, vnode) render(root, updated) - o(updated.dom).equals(root.firstChild) + o(updated[0].dom).equals(root.firstChild) o(root.childNodes.length).equals(2) o(root.childNodes[0].nodeName).equals("A") o(root.childNodes[1].nodeName).equals("B") }) o("removes els", function() { - var vnode = m.fragment(m("a"), m("b")) - var updated = m.fragment() + var vnode = [m("a"), m("b")] + var updated = [] render(root, vnode) render(root, updated) - o(updated.dom).equals(null) - o(updated.children).deepEquals([]) o(root.childNodes.length).equals(0) }) o("updates from childless fragment", function() { - var vnode = m.fragment() - var updated = m.fragment(m("a")) + var vnode = [] + var updated = [m("a")] render(root, vnode) render(root, updated) - o(updated.dom).equals(root.firstChild) - o(updated.dom.nodeName).equals("A") + o(updated[0].dom).equals(root.firstChild) + o(updated[0].dom.nodeName).equals("A") }) o("updates to childless fragment", function() { - var vnode = m.fragment(m("a")) - var updated = m.fragment() + var vnode = [m("a")] + var updated = [] render(root, vnode) render(root, updated) - o(updated.dom).equals(null) o(root.childNodes.length).equals(0) }) }) diff --git a/render/tests/test-updateNodes.js b/render/tests/test-updateNodes.js index 1d32b8220..80b0ff20f 100644 --- a/render/tests/test-updateNodes.js +++ b/render/tests/test-updateNodes.js @@ -73,26 +73,26 @@ o.spec("updateNodes", function() { o(root.childNodes[0].nodeValue).equals("0") }) o("handles fragment noop", function() { - var vnodes = m.fragment(m("a")) - var updated = m.fragment(m("a")) + var vnodes = [m("a")] + var updated = [m("a")] render(root, vnodes) render(root, updated) o(root.childNodes.length).equals(1) - o(updated.dom.nodeName).equals("A") - o(updated.dom).equals(root.childNodes[0]) + o(updated[0].dom.nodeName).equals("A") + o(updated[0].dom).equals(root.childNodes[0]) }) o("handles fragment noop w/ text child", function() { - var vnodes = m.fragment("a") - var updated = m.fragment("a") + var vnodes = [m.normalize("a")] + var updated = [m.normalize("a")] render(root, vnodes) render(root, updated) o(root.childNodes.length).equals(1) - o(updated.dom.nodeValue).equals("a") - o(updated.dom).equals(root.childNodes[0]) + o(updated[0].dom.nodeValue).equals("a") + o(updated[0].dom).equals(root.childNodes[0]) }) o("handles undefined to null noop", function() { var vnodes = [null, m("div")] @@ -621,7 +621,7 @@ o.spec("updateNodes", function() { m("#", "a") ) var updated = m("div", - m.fragment(m("#", "b")), + [m("#", "b")], undefined, undefined ) @@ -779,38 +779,34 @@ o.spec("updateNodes", function() { }) o("don't add back elements from fragments that are restored from the pool #1991", function() { render(root, [ - m.fragment(), - m.fragment() + [], + [] ]) render(root, [ - m.fragment(), - m.fragment( - m("div") - ) + [], + [m("div")] ]) render(root, [ - m.fragment(null) + [null] ]) render(root, [ - m.fragment(), - m.fragment() + [], + [] ]) o(root.childNodes.length).equals(0) }) o("don't add back elements from fragments that are being removed #1991", function() { render(root, [ - m.fragment(), + [], m("p"), ]) render(root, [ - m.fragment( - m("div", 5) - ) + [m("div", 5)] ]) render(root, [ - m.fragment(), - m.fragment() + [], + [] ]) o(root.childNodes.length).equals(0) @@ -852,9 +848,9 @@ o.spec("updateNodes", function() { } }) o("don't fetch the nextSibling from the pool", function() { - render(root, [m.fragment(m.key(1, m("div")), m.key(2, m("div"))), m("p")]) - render(root, [m.fragment(), m("p")]) - render(root, [m.fragment(m.key(2, m("div")), m.key(1, m("div"))), m("p")]) + render(root, [[m.key(1, m("div")), m.key(2, m("div"))], m("p")]) + render(root, [[], m("p")]) + render(root, [[m.key(2, m("div")), m.key(1, m("div"))], m("p")]) o([].map.call(root.childNodes, function(el) {return el.nodeName})).deepEquals(["DIV", "DIV", "P"]) }) @@ -999,9 +995,9 @@ o.spec("updateNodes", function() { o("fragment child toggles from null when followed by null component then tag", function() { var component = () => null - var vnodes = [m.fragment(m("a"), m(component), m("b"))] - var temp = [m.fragment(null, m(component), m("b"))] - var updated = [m.fragment(m("a"), m(component), m("b"))] + var vnodes = [[m("a"), m(component), m("b")]] + var temp = [[null, m(component), m("b")]] + var updated = [[m("a"), m(component), m("b")]] render(root, vnodes) render(root, temp) @@ -1015,9 +1011,9 @@ o.spec("updateNodes", function() { var flag = true var a = () => (flag ? m("a") : null) var b = () => null - var vnodes = [m.fragment(m(a), m(b), m("s"))] - var temp = [m.fragment(m(a), m(b), m("s"))] - var updated = [m.fragment(m(a), m(b), m("s"))] + var vnodes = [[m(a), m(b), m("s")]] + var temp = [[m(a), m(b), m("s")]] + var updated = [[m(a), m(b), m("s")]] render(root, vnodes) flag = false @@ -1030,7 +1026,7 @@ o.spec("updateNodes", function() { o(root.childNodes[1].nodeName).equals("S") }) o("removing a component that returns a fragment doesn't throw (regression test for incidental bug introduced while debugging some Flems)", function() { - var component = () => m.fragment(m("a"), m("b")) + var component = () => [m("a"), m("b")] try { render(root, [m(component)]) render(root, []) diff --git a/tests/test-api.js b/tests/test-api.js index 8df62890e..9c8f02c00 100644 --- a/tests/test-api.js +++ b/tests/test-api.js @@ -29,9 +29,9 @@ o.spec("api", function() { o(vnode.tag).equals("div") }) }) - o.spec("m.fragment", function() { + o.spec("m.normalize", function() { o("works", function() { - var vnode = m.fragment([m("div")]) + var vnode = m.normalize([m("div")]) o(vnode.tag).equals("[") o(vnode.children.length).equals(1) From bd09d8ad463259169d6f2ae389c7ca03bd4051b5 Mon Sep 17 00:00:00 2001 From: Claudia Meadows Date: Thu, 3 Oct 2024 23:17:18 -0700 Subject: [PATCH 40/95] Stop propagating hooks, correct the circular rendering check --- api/tests/test-mountRedraw.js | 287 ++++++++++++++++++++++++++-- render/hyperscript.js | 27 +++ render/render.js | 129 +++++++------ render/tests/test-attributes.js | 12 +- render/tests/test-createFragment.js | 6 +- render/tests/test-updateElement.js | 4 +- render/tests/test-updateNodes.js | 8 +- 7 files changed, 374 insertions(+), 99 deletions(-) diff --git a/api/tests/test-mountRedraw.js b/api/tests/test-mountRedraw.js index 40e8da1c1..40ddae64c 100644 --- a/api/tests/test-mountRedraw.js +++ b/api/tests/test-mountRedraw.js @@ -8,30 +8,32 @@ var mountRedraw = require("../../api/mount-redraw") 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(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) + var error = console.error + o.afterEach(() => { + console.error = error }) o("shouldn't error if there are no renderers", function() { + var consoleMock = {error: o.spy()} + var throttleMock = throttleMocker() + var m = mountRedraw(throttleMock.schedule, consoleMock) + var errors = [] + m.redraw() throttleMock.fire() + + o(consoleMock.error.calls.map((c) => c.args[0])).deepEquals(errors) + o(throttleMock.queueLength()).equals(0) }) o("schedules correctly", function() { + var $window = domMock() + var consoleMock = {error: o.spy()} + var throttleMock = throttleMocker() + var root = $window.document.body + var m = mountRedraw(throttleMock.schedule, consoleMock) + var errors = [] + var spy = o.spy() m.mount(root, spy) @@ -40,9 +42,19 @@ o.spec("mount/redraw", function() { o(spy.callCount).equals(1) throttleMock.fire() o(spy.callCount).equals(2) + + o(consoleMock.error.calls.map((c) => c.args[0])).deepEquals(errors) + o(throttleMock.queueLength()).equals(0) }) o("should run a single renderer entry", function() { + var $window = domMock() + var consoleMock = {error: o.spy()} + var throttleMock = throttleMocker() + var root = $window.document.body + var m = mountRedraw(throttleMock.schedule, consoleMock) + var errors = [] + var spy = o.spy() m.mount(root, spy) @@ -56,9 +68,19 @@ o.spec("mount/redraw", function() { o(spy.callCount).equals(1) throttleMock.fire() o(spy.callCount).equals(2) + + o(consoleMock.error.calls.map((c) => c.args[0])).deepEquals(errors) + o(throttleMock.queueLength()).equals(0) }) o("should run all renderer entries", function() { + var $window = domMock() + var consoleMock = {error: o.spy()} + var throttleMock = throttleMocker() + var m = mountRedraw(throttleMock.schedule, consoleMock) + var $document = $window.document + var errors = [] + var el1 = $document.createElement("div") var el2 = $document.createElement("div") var el3 = $document.createElement("div") @@ -87,9 +109,19 @@ o.spec("mount/redraw", function() { o(spy1.callCount).equals(2) o(spy2.callCount).equals(2) o(spy3.callCount).equals(2) + + o(consoleMock.error.calls.map((c) => c.args[0])).deepEquals(errors) + o(throttleMock.queueLength()).equals(0) }) o("should not redraw when mounting another root", function() { + var $window = domMock() + var consoleMock = {error: o.spy()} + var throttleMock = throttleMocker() + var m = mountRedraw(throttleMock.schedule, consoleMock) + var $document = $window.document + var errors = [] + var el1 = $document.createElement("div") var el2 = $document.createElement("div") var el3 = $document.createElement("div") @@ -111,9 +143,19 @@ o.spec("mount/redraw", function() { o(spy1.callCount).equals(1) o(spy2.callCount).equals(1) o(spy3.callCount).equals(1) + + o(consoleMock.error.calls.map((c) => c.args[0])).deepEquals(errors) + o(throttleMock.queueLength()).equals(0) }) o("should stop running after mount null", function() { + var $window = domMock() + var consoleMock = {error: o.spy()} + var throttleMock = throttleMocker() + var root = $window.document.body + var m = mountRedraw(throttleMock.schedule, consoleMock) + var errors = [] + var spy = o.spy() m.mount(root, spy) @@ -125,9 +167,19 @@ o.spec("mount/redraw", function() { o(spy.callCount).equals(1) throttleMock.fire() o(spy.callCount).equals(1) + + o(consoleMock.error.calls.map((c) => c.args[0])).deepEquals(errors) + o(throttleMock.queueLength()).equals(0) }) o("should stop running after mount undefined", function() { + var $window = domMock() + var consoleMock = {error: o.spy()} + var throttleMock = throttleMocker() + var root = $window.document.body + var m = mountRedraw(throttleMock.schedule, consoleMock) + var errors = [] + var spy = o.spy() m.mount(root, spy) @@ -139,9 +191,19 @@ o.spec("mount/redraw", function() { o(spy.callCount).equals(1) throttleMock.fire() o(spy.callCount).equals(1) + + o(consoleMock.error.calls.map((c) => c.args[0])).deepEquals(errors) + o(throttleMock.queueLength()).equals(0) }) o("should stop running after mount no arg", function() { + var $window = domMock() + var consoleMock = {error: o.spy()} + var throttleMock = throttleMocker() + var root = $window.document.body + var m = mountRedraw(throttleMock.schedule, consoleMock) + var errors = [] + var spy = o.spy() m.mount(root, spy) @@ -153,9 +215,19 @@ o.spec("mount/redraw", function() { o(spy.callCount).equals(1) throttleMock.fire() o(spy.callCount).equals(1) + + o(consoleMock.error.calls.map((c) => c.args[0])).deepEquals(errors) + o(throttleMock.queueLength()).equals(0) }) o("should invoke remove callback on unmount", function() { + var $window = domMock() + var consoleMock = {error: o.spy()} + var throttleMock = throttleMocker() + var root = $window.document.body + var m = mountRedraw(throttleMock.schedule, consoleMock) + var errors = [] + var onabort = o.spy() var spy = o.spy(() => h.layout((_, signal) => { signal.onabort = onabort })) @@ -165,9 +237,19 @@ o.spec("mount/redraw", function() { o(spy.callCount).equals(1) o(onabort.callCount).equals(1) + + o(consoleMock.error.calls.map((c) => c.args[0])).deepEquals(errors) + o(throttleMock.queueLength()).equals(0) }) o("should stop running after unsubscribe, even if it occurs after redraw is requested", function() { + var $window = domMock() + var consoleMock = {error: o.spy()} + var throttleMock = throttleMocker() + var root = $window.document.body + var m = mountRedraw(throttleMock.schedule, consoleMock) + var errors = [] + var spy = o.spy() m.mount(root, spy) @@ -178,9 +260,19 @@ o.spec("mount/redraw", function() { o(spy.callCount).equals(1) throttleMock.fire() o(spy.callCount).equals(1) + + o(consoleMock.error.calls.map((c) => c.args[0])).deepEquals(errors) + o(throttleMock.queueLength()).equals(0) }) o("does nothing on invalid unmount", function() { + var $window = domMock() + var consoleMock = {error: o.spy()} + var throttleMock = throttleMocker() + var root = $window.document.body + var m = mountRedraw(throttleMock.schedule, consoleMock) + var errors = [] + var spy = o.spy() m.mount(root, spy) @@ -190,9 +282,19 @@ o.spec("mount/redraw", function() { m.redraw() throttleMock.fire() o(spy.callCount).equals(2) + + o(consoleMock.error.calls.map((c) => c.args[0])).deepEquals(errors) + o(throttleMock.queueLength()).equals(0) }) o("redraw.sync() redraws all roots synchronously", function() { + var $window = domMock() + var consoleMock = {error: o.spy()} + var throttleMock = throttleMocker() + var m = mountRedraw(throttleMock.schedule, consoleMock) + var $document = $window.document + var errors = [] + var el1 = $document.createElement("div") var el2 = $document.createElement("div") var el3 = $document.createElement("div") @@ -219,14 +321,34 @@ o.spec("mount/redraw", function() { o(spy1.callCount).equals(3) o(spy2.callCount).equals(3) o(spy3.callCount).equals(3) + + o(consoleMock.error.calls.map((c) => c.args[0])).deepEquals(errors) + o(throttleMock.queueLength()).equals(0) }) o("throws on invalid view", function() { + var $window = domMock() + var consoleMock = {error: o.spy()} + var throttleMock = throttleMocker() + var root = $window.document.body + var m = mountRedraw(throttleMock.schedule, consoleMock) + var errors = [] + o(function() { m.mount(root, {}) }).throws(TypeError) + + o(consoleMock.error.calls.map((c) => c.args[0])).deepEquals(errors) + o(throttleMock.queueLength()).equals(0) }) o("skips roots that were synchronously unsubscribed before they were visited", function() { + var $window = domMock() + var consoleMock = {error: o.spy()} + var throttleMock = throttleMocker() + var m = mountRedraw(throttleMock.schedule, consoleMock) + var $document = $window.document + var errors = [] + var calls = [] var root1 = $document.createElement("div") var root2 = $document.createElement("div") @@ -247,9 +369,19 @@ o.spec("mount/redraw", function() { "root1", "root2", "root3", "root1", "root3", ]) + + o(consoleMock.error.calls.map((c) => c.args[0])).deepEquals(errors) + o(throttleMock.queueLength()).equals(0) }) o("keeps its place when synchronously unsubscribing previously visited roots", function() { + var $window = domMock() + var consoleMock = {error: o.spy()} + var throttleMock = throttleMocker() + var m = mountRedraw(throttleMock.schedule, consoleMock) + var $document = $window.document + var errors = [] + var calls = [] var root1 = $document.createElement("div") var root2 = $document.createElement("div") @@ -270,10 +402,18 @@ o.spec("mount/redraw", function() { "root1", "root2", "root3", "root1", "root2", "root3", ]) + + o(consoleMock.error.calls.map((c) => c.args[0])).deepEquals(errors) + o(throttleMock.queueLength()).equals(0) }) o("keeps its place when synchronously unsubscribing previously visited roots in the face of errors", function() { - errors = ["fail"] + var $window = domMock() + var consoleMock = {error: console.error = o.spy()} + var throttleMock = throttleMocker() + var m = mountRedraw(throttleMock.schedule, consoleMock) + var $document = $window.document + var errors = ["fail"] var calls = [] var root1 = $document.createElement("div") var root2 = $document.createElement("div") @@ -294,9 +434,19 @@ o.spec("mount/redraw", function() { "root1", "root2", "root3", "root1", "root3", ]) + + o(consoleMock.error.calls.map((c) => c.args[0])).deepEquals(errors) + o(throttleMock.queueLength()).equals(0) }) o("keeps its place when synchronously unsubscribing the current root", function() { + var $window = domMock() + var consoleMock = {error: o.spy()} + var throttleMock = throttleMocker() + var m = mountRedraw(throttleMock.schedule, consoleMock) + var $document = $window.document + var errors = [] + var calls = [] var root1 = $document.createElement("div") var root2 = $document.createElement("div") @@ -317,10 +467,18 @@ o.spec("mount/redraw", function() { "root1", "root2", "root3", "root1", [TypeError, "Node is currently being rendered to and thus is locked."], "root2", "root3", ]) + + o(consoleMock.error.calls.map((c) => c.args[0])).deepEquals(errors) + o(throttleMock.queueLength()).equals(0) }) o("keeps its place when synchronously unsubscribing the current root in the face of an error", function() { - errors = [ + var $window = domMock() + var consoleMock = {error: console.error = o.spy()} + var throttleMock = throttleMocker() + var m = mountRedraw(throttleMock.schedule, consoleMock) + var $document = $window.document + var errors = [ [TypeError, "Node is currently being rendered to and thus is locked."], ] var calls = [] @@ -343,29 +501,67 @@ o.spec("mount/redraw", function() { "root1", "root2", "root3", "root1", "root3", ]) + + o(consoleMock.error.calls.map((c) => c.args[0])).deepEquals(errors) + o(throttleMock.queueLength()).equals(0) }) o("throws on invalid `root` DOM node", function() { + var consoleMock = {error: o.spy()} + var throttleMock = throttleMocker() + var m = mountRedraw(throttleMock.schedule, consoleMock) + var errors = [] + o(function() { m.mount(null, () => {}) }).throws(TypeError) + + o(consoleMock.error.calls.map((c) => c.args[0])).deepEquals(errors) + o(throttleMock.queueLength()).equals(0) }) o("renders into `root` synchronously", function() { + var $window = domMock() + var consoleMock = {error: o.spy()} + var throttleMock = throttleMocker() + var root = $window.document.body + var m = mountRedraw(throttleMock.schedule, consoleMock) + var errors = [] + m.mount(root, () => h("div")) o(root.firstChild.nodeName).equals("DIV") + + o(consoleMock.error.calls.map((c) => c.args[0])).deepEquals(errors) + o(throttleMock.queueLength()).equals(0) }) o("mounting null unmounts", function() { + var $window = domMock() + var consoleMock = {error: o.spy()} + var throttleMock = throttleMocker() + var root = $window.document.body + var m = mountRedraw(throttleMock.schedule, consoleMock) + var errors = [] + m.mount(root, () => h("div")) m.mount(root, null) o(root.childNodes.length).equals(0) + + o(consoleMock.error.calls.map((c) => c.args[0])).deepEquals(errors) + o(throttleMock.queueLength()).equals(0) }) o("Mounting a second root doesn't cause the first one to redraw", function() { + var $window = domMock() + var consoleMock = {error: o.spy()} + var throttleMock = throttleMocker() + var m = mountRedraw(throttleMock.schedule, consoleMock) + var $document = $window.document + var errors = [] + var root1 = $document.createElement("div") var root2 = $document.createElement("div") var view = o.spy() @@ -379,9 +575,20 @@ o.spec("mount/redraw", function() { throttleMock.fire() o(view.callCount).equals(1) + + o(consoleMock.error.calls.map((c) => c.args[0])).deepEquals(errors) + o(throttleMock.queueLength()).equals(0) }) o("redraws on events", function() { + var $window = domMock() + var consoleMock = {error: o.spy()} + var throttleMock = throttleMocker() + var root = $window.document.body + var m = mountRedraw(throttleMock.schedule, consoleMock) + var $document = $window.document + var errors = [] + var layout = o.spy() var onclick = o.spy() var e = $document.createEvent("MouseEvents") @@ -404,9 +611,19 @@ o.spec("mount/redraw", function() { throttleMock.fire() o(layout.calls.map((c) => c.args[2])).deepEquals([true, false]) + + o(consoleMock.error.calls.map((c) => c.args[0])).deepEquals(errors) + o(throttleMock.queueLength()).equals(0) }) o("redraws several mount points on events", function() { + var $window = domMock() + var consoleMock = {error: o.spy()} + var throttleMock = throttleMocker() + var m = mountRedraw(throttleMock.schedule, consoleMock) + var $document = $window.document + var errors = [] + var layout0 = o.spy() var onclick0 = o.spy() var layout1 = o.spy() @@ -448,9 +665,20 @@ o.spec("mount/redraw", function() { o(layout0.calls.map((c) => c.args[2])).deepEquals([true, false, false]) o(layout1.calls.map((c) => c.args[2])).deepEquals([true, false, false]) + + o(consoleMock.error.calls.map((c) => c.args[0])).deepEquals(errors) + o(throttleMock.queueLength()).equals(0) }) o("event handlers can skip redraw", function() { + var $window = domMock() + var consoleMock = {error: o.spy()} + var throttleMock = throttleMocker() + var root = $window.document.body + var m = mountRedraw(throttleMock.schedule, consoleMock) + var $document = $window.document + var errors = [] + var layout = o.spy() var e = $document.createEvent("MouseEvents") @@ -467,9 +695,19 @@ o.spec("mount/redraw", function() { throttleMock.fire() o(layout.calls.map((c) => c.args[2])).deepEquals([true]) + + o(consoleMock.error.calls.map((c) => c.args[0])).deepEquals(errors) + o(throttleMock.queueLength()).equals(0) }) o("redraws when the render function is run", function() { + var $window = domMock() + var consoleMock = {error: o.spy()} + var throttleMock = throttleMocker() + var root = $window.document.body + var m = mountRedraw(throttleMock.schedule, consoleMock) + var errors = [] + var layout = o.spy() m.mount(root, () => h("div", h.layout(layout))) @@ -481,10 +719,18 @@ o.spec("mount/redraw", function() { throttleMock.fire() o(layout.calls.map((c) => c.args[2])).deepEquals([true, false]) + + o(consoleMock.error.calls.map((c) => c.args[0])).deepEquals(errors) + o(throttleMock.queueLength()).equals(0) }) o("emits errors correctly", function() { - errors = ["foo", "bar", "baz"] + var $window = domMock() + var consoleMock = {error: o.spy()} + var throttleMock = throttleMocker() + var root = $window.document.body + var m = mountRedraw(throttleMock.schedule, consoleMock) + var errors = ["foo", "bar", "baz"] var counter = -1 m.mount(root, () => { @@ -499,5 +745,8 @@ o.spec("mount/redraw", function() { throttleMock.fire() m.redraw() throttleMock.fire() + + o(consoleMock.error.calls.map((c) => c.args[0])).deepEquals(errors) + o(throttleMock.queueLength()).equals(0) }) }) diff --git a/render/hyperscript.js b/render/hyperscript.js index 4d7043f31..9efbaa322 100644 --- a/render/hyperscript.js +++ b/render/hyperscript.js @@ -2,6 +2,33 @@ var hasOwn = require("../util/hasOwn") +/* +This same structure is used for several nodes. Here's an explainer for each type. + +Components: +- `tag`: component reference +- `state`: view function, may `=== tag` +- `attrs`: most recently received attributes +- `children`: unused +- `dom`: unused +- `instance`: unused + +DOM elements: +- `tag`: tag name string +- `state`: event listener dictionary, if any events were ever registered +- `attrs`: most recently received attributes +- `children`: virtual DOM children +- `dom`: element reference +- `instance`: unused + +Fragments: +- `tag`: `"[" +- `state`: event listener dictionary, if any events were ever registered +- `attrs`: most recently received attributes +- `children`: virtual DOM children +- `dom`: element reference +- `instance`: unused +*/ function Vnode(tag, state, attrs, children) { return {tag, state, attrs, children, dom: undefined, instance: undefined} } diff --git a/render/render.js b/render/render.js index a6e17a33b..673bae946 100644 --- a/render/render.js +++ b/render/render.js @@ -8,6 +8,7 @@ var nameSpace = { math: "http://www.w3.org/1998/Math/MathML" } +var currentHooks var currentRedraw function getDocument(dom) { @@ -28,45 +29,46 @@ function activeElement(dom) { } } //create -function createNodes(rawParent, parent, vnodes, start, end, hooks, nextSibling, ns) { +function createNodes(rawParent, parent, vnodes, start, end, nextSibling, ns) { for (var i = start; i < end; i++) { var vnode = vnodes[i] if (vnode != null) { - createNode(rawParent, parent, vnode, hooks, ns, nextSibling) + createNode(rawParent, parent, vnode, ns, nextSibling) } } } -function createNode(rawParent, parent, vnode, hooks, ns, nextSibling) { +function createNode(rawParent, parent, vnode, ns, nextSibling) { var tag = vnode.tag if (typeof tag === "string") { switch (tag) { case "!": throw new Error("No node present to retain with `m.retain()`") - case ">": createLayout(rawParent, vnode, hooks); break + case ">": createLayout(rawParent, vnode); break case "#": createText(parent, vnode, nextSibling); break case "=": - case "[": createFragment(rawParent, parent, vnode, hooks, ns, nextSibling); break - default: createElement(parent, vnode, hooks, ns, nextSibling) + case "[": createFragment(rawParent, parent, vnode, ns, nextSibling); break + default: createElement(parent, vnode, ns, nextSibling) } } - else createComponent(rawParent, parent, vnode, hooks, ns, nextSibling) + else createComponent(rawParent, parent, vnode, ns, nextSibling) } -function createLayout(rawParent, vnode, hooks) { - hooks.push(vnode.state.bind(null, rawParent, (vnode.dom = new AbortController()).signal, true)) +function createLayout(rawParent, vnode) { + vnode.dom = new AbortController() + currentHooks.push({v: vnode, p: rawParent, i: true}) } function createText(parent, vnode, nextSibling) { vnode.dom = getDocument(parent).createTextNode(vnode.children) insertDOM(parent, vnode.dom, nextSibling) } -function createFragment(rawParent, parent, vnode, hooks, ns, nextSibling) { +function createFragment(rawParent, parent, vnode, ns, nextSibling) { var fragment = getDocument(parent).createDocumentFragment() if (vnode.children != null) { var children = vnode.children - createNodes(rawParent, fragment, children, 0, children.length, hooks, null, ns) + createNodes(rawParent, fragment, children, 0, children.length, null, ns) } vnode.dom = fragment.firstChild insertDOM(parent, fragment, nextSibling) } -function createElement(parent, vnode, hooks, ns, nextSibling) { +function createElement(parent, vnode, ns, nextSibling) { var tag = vnode.tag var attrs = vnode.attrs var is = attrs && attrs.is @@ -87,18 +89,18 @@ function createElement(parent, vnode, hooks, ns, nextSibling) { if (!maybeSetContentEditable(vnode)) { if (vnode.children != null) { var children = vnode.children - createNodes(element, element, children, 0, children.length, hooks, null, ns) + createNodes(element, element, children, 0, children.length, null, ns) if (vnode.tag === "select" && attrs != null) setLateSelectAttrs(vnode, attrs) } } } -function createComponent(rawParent, parent, vnode, hooks, ns, nextSibling) { +function createComponent(rawParent, parent, vnode, ns, nextSibling) { var tree = (vnode.state = vnode.tag)(vnode.attrs) if (typeof tree === "function") tree = (vnode.state = tree)(vnode.attrs) if (tree === vnode) throw Error("A view cannot return the vnode it received as argument") vnode.instance = hyperscript.normalize(tree) if (vnode.instance != null) { - createNode(rawParent, parent, vnode.instance, hooks, ns, nextSibling) + createNode(rawParent, parent, vnode.instance, ns, nextSibling) } } @@ -201,9 +203,9 @@ function createComponent(rawParent, parent, vnode, hooks, ns, nextSibling) { // the old DOM nodes before updateNode runs because it enables us to use the cached `nextSibling` // variable rather than fetching it using `getNextSibling()`. -function updateNodes(parent, old, vnodes, hooks, nextSibling, ns) { +function updateNodes(parent, old, vnodes, nextSibling, ns) { if (old === vnodes || old == null && vnodes == null) return - else if (old == null || old.length === 0) createNodes(parent, parent, vnodes, 0, vnodes.length, hooks, nextSibling, ns) + else if (old == null || old.length === 0) createNodes(parent, parent, vnodes, 0, vnodes.length, nextSibling, ns) else if (vnodes == null || vnodes.length === 0) removeNodes(parent, old, 0, old.length) else { var isOldKeyed = old[0] != null && old[0].tag === "=" @@ -213,7 +215,7 @@ function updateNodes(parent, old, vnodes, hooks, nextSibling, ns) { if (!isKeyed) while (start < vnodes.length && vnodes[start] == null) start++ if (isOldKeyed !== isKeyed) { removeNodes(parent, old, oldStart, old.length) - createNodes(parent, parent, vnodes, start, vnodes.length, hooks, nextSibling, ns) + createNodes(parent, parent, vnodes, start, vnodes.length, nextSibling, ns) } else if (!isKeyed) { // Don't index past the end of either list (causes deopts). var commonLength = old.length < vnodes.length ? old.length : vnodes.length @@ -225,12 +227,12 @@ function updateNodes(parent, old, vnodes, hooks, nextSibling, ns) { o = old[start] v = vnodes[start] if (o === v || o == null && v == null) continue - else if (o == null) createNode(parent, parent, v, hooks, ns, getNextSibling(old, start + 1, nextSibling)) + else if (o == null) createNode(parent, parent, v, ns, getNextSibling(old, start + 1, nextSibling)) else if (v == null) removeNode(parent, o) - else updateNode(parent, o, v, hooks, getNextSibling(old, start + 1, nextSibling), ns) + else updateNode(parent, o, v, getNextSibling(old, start + 1, nextSibling), ns) } if (old.length > commonLength) removeNodes(parent, old, start, old.length) - if (vnodes.length > commonLength) createNodes(parent, parent, vnodes, start, vnodes.length, hooks, nextSibling, ns) + if (vnodes.length > commonLength) createNodes(parent, parent, vnodes, start, vnodes.length, nextSibling, ns) } else { // keyed diff var oldEnd = old.length - 1, end = vnodes.length - 1, map, o, v, oe, ve, topSibling @@ -240,7 +242,7 @@ function updateNodes(parent, old, vnodes, hooks, nextSibling, ns) { oe = old[oldEnd] ve = vnodes[end] if (oe.state !== ve.state) break - if (oe !== ve) updateNode(parent, oe, ve, hooks, nextSibling, ns) + if (oe !== ve) updateNode(parent, oe, ve, nextSibling, ns) if (ve.dom != null) nextSibling = ve.dom oldEnd--, end-- } @@ -250,7 +252,7 @@ function updateNodes(parent, old, vnodes, hooks, nextSibling, ns) { v = vnodes[start] if (o.state !== v.state) break oldStart++, start++ - if (o !== v) updateNode(parent, o, v, hooks, getNextSibling(old, oldStart, nextSibling), ns) + if (o !== v) updateNode(parent, o, v, getNextSibling(old, oldStart, nextSibling), ns) } // swaps and list reversals while (oldEnd >= oldStart && end >= start) { @@ -258,9 +260,9 @@ function updateNodes(parent, old, vnodes, hooks, nextSibling, ns) { if (o.state !== ve.state || oe.state !== v.state) break topSibling = getNextSibling(old, oldStart, nextSibling) moveDOM(parent, oe, topSibling) - if (oe !== v) updateNode(parent, oe, v, hooks, topSibling, ns) + if (oe !== v) updateNode(parent, oe, v, topSibling, ns) if (++start <= --end) moveDOM(parent, o, nextSibling) - if (o !== ve) updateNode(parent, o, ve, hooks, nextSibling, ns) + if (o !== ve) updateNode(parent, o, ve, nextSibling, ns) if (ve.dom != null) nextSibling = ve.dom oldStart++; oldEnd-- oe = old[oldEnd] @@ -271,14 +273,14 @@ function updateNodes(parent, old, vnodes, hooks, nextSibling, ns) { // bottom up once again while (oldEnd >= oldStart && end >= start) { if (oe.state !== ve.state) break - if (oe !== ve) updateNode(parent, oe, ve, hooks, nextSibling, ns) + if (oe !== ve) updateNode(parent, oe, ve, nextSibling, ns) if (ve.dom != null) nextSibling = ve.dom oldEnd--, end-- oe = old[oldEnd] ve = vnodes[end] } if (start > end) removeNodes(parent, old, oldStart, oldEnd + 1) - else if (oldStart > oldEnd) createNodes(parent, parent, vnodes, start, end + 1, hooks, nextSibling, ns) + else if (oldStart > oldEnd) createNodes(parent, parent, vnodes, start, end + 1, nextSibling, ns) else { // inspired by ivi https://github.com/ivijs/ivi/ by Boris Kaul var originalNextSibling = nextSibling, vnodesLength = end - start + 1, oldIndices = new Array(vnodesLength), li=0, i=0, pos = 2147483647, matched = 0, map, lisIndices @@ -292,14 +294,14 @@ function updateNodes(parent, old, vnodes, hooks, nextSibling, ns) { oldIndices[i-start] = oldIndex oe = old[oldIndex] old[oldIndex] = null - if (oe !== ve) updateNode(parent, oe, ve, hooks, nextSibling, ns) + if (oe !== ve) updateNode(parent, oe, ve, nextSibling, ns) if (ve.dom != null) nextSibling = ve.dom matched++ } } nextSibling = originalNextSibling if (matched !== oldEnd - oldStart + 1) removeNodes(parent, old, oldStart, oldEnd + 1) - if (matched === 0) createNodes(parent, parent, vnodes, start, end + 1, hooks, nextSibling, ns) + if (matched === 0) createNodes(parent, parent, vnodes, start, end + 1, nextSibling, ns) else { if (pos === -1) { // the indices of the indices of the items that are part of the @@ -308,7 +310,7 @@ function updateNodes(parent, old, vnodes, hooks, nextSibling, ns) { li = lisIndices.length - 1 for (i = end; i >= start; i--) { v = vnodes[i] - if (oldIndices[i-start] === -1) createNode(parent, parent, v, hooks, ns, nextSibling) + if (oldIndices[i-start] === -1) createNode(parent, parent, v, ns, nextSibling) else { if (lisIndices[li] === i - start) li-- else moveDOM(parent, v, nextSibling) @@ -318,7 +320,7 @@ function updateNodes(parent, old, vnodes, hooks, nextSibling, ns) { } else { for (i = end; i >= start; i--) { v = vnodes[i] - if (oldIndices[i-start] === -1) createNode(parent, parent, v, hooks, ns, nextSibling) + if (oldIndices[i-start] === -1) createNode(parent, parent, v, ns, nextSibling) if (v.dom != null) nextSibling = vnodes[i].dom } } @@ -327,7 +329,7 @@ function updateNodes(parent, old, vnodes, hooks, nextSibling, ns) { } } } -function updateNode(parent, old, vnode, hooks, nextSibling, ns) { +function updateNode(parent, old, vnode, nextSibling, ns) { var oldTag = old.tag, tag = vnode.tag if (tag === "!") { // If it's a retain node, transmute it into the node it's retaining. Makes it much easier @@ -343,31 +345,30 @@ function updateNode(parent, old, vnode, hooks, nextSibling, ns) { } else if (oldTag === tag && (tag !== "=" || vnode.state === old.state)) { if (typeof oldTag === "string") { switch (oldTag) { - case ">": updateLayout(parent, old, vnode, hooks); break + case ">": updateLayout(parent, old, vnode); break case "#": updateText(old, vnode); break case "=": - case "[": updateFragment(parent, old, vnode, hooks, nextSibling, ns); break - default: updateElement(old, vnode, hooks, ns) + case "[": updateFragment(parent, old, vnode, nextSibling, ns); break + default: updateElement(old, vnode, ns) } } - else updateComponent(parent, old, vnode, hooks, nextSibling, ns) + else updateComponent(parent, old, vnode, nextSibling, ns) } else { removeNode(parent, old) - createNode(parent, parent, vnode, hooks, ns, nextSibling) + createNode(parent, parent, vnode, ns, nextSibling) } } -function updateLayout(parent, old, vnode, hooks) { - hooks.push(vnode.state.bind(null, parent, (vnode.dom = old.dom).signal, false)) +function updateLayout(parent, old, vnode) { + vnode.dom = old.dom + currentHooks.push({v: vnode, p: parent, i: false}) } function updateText(old, vnode) { - if (old.children.toString() !== vnode.children.toString()) { - old.dom.nodeValue = vnode.children - } + if (`${old.children}` !== `${vnode.children}`) old.dom.nodeValue = vnode.children vnode.dom = old.dom } -function updateFragment(parent, old, vnode, hooks, nextSibling, ns) { - updateNodes(parent, old.children, vnode.children, hooks, nextSibling, ns) +function updateFragment(parent, old, vnode, nextSibling, ns) { + updateNodes(parent, old.children, vnode.children, nextSibling, ns) vnode.dom = null if (vnode.children != null) { for (var child of vnode.children) { @@ -377,22 +378,22 @@ function updateFragment(parent, old, vnode, hooks, nextSibling, ns) { } } } -function updateElement(old, vnode, hooks, ns) { +function updateElement(old, vnode, ns) { vnode.state = old.state var element = vnode.dom = old.dom ns = getNameSpace(vnode) || ns updateAttrs(vnode, old.attrs, vnode.attrs, ns) if (!maybeSetContentEditable(vnode)) { - updateNodes(element, old.children, vnode.children, hooks, null, ns) + updateNodes(element, old.children, vnode.children, null, ns) } } -function updateComponent(parent, old, vnode, hooks, nextSibling, ns) { +function updateComponent(parent, old, vnode, nextSibling, ns) { vnode.instance = hyperscript.normalize((vnode.state = old.state)(vnode.attrs, old.attrs)) if (vnode.instance != null) { if (vnode.instance === vnode) throw Error("A view cannot return the vnode it received as argument") - if (old.instance == null) createNode(parent, parent, vnode.instance, hooks, ns, nextSibling) - else updateNode(parent, old.instance, vnode.instance, hooks, nextSibling, ns) + if (old.instance == null) createNode(parent, parent, vnode.instance, ns, nextSibling) + else updateNode(parent, old.instance, vnode.instance, nextSibling, ns) } else if (old.instance != null) { removeNode(parent, old.instance, false) @@ -601,7 +602,7 @@ function setLateSelectAttrs(vnode, attrs) { } function updateAttrs(vnode, old, attrs, ns) { if (old && old === attrs) { - console.warn("Don't reuse attrs object, use new object for every redraw, this will throw in next major") + throw new Error("Attributes object cannot be reused.") } if (attrs != null) { // If you assign an input type that is not supported by IE 11 with an assignment expression, an error will occur. @@ -736,32 +737,40 @@ function updateEvent(vnode, key, value) { } } -var currentDOM +var currentlyRendering = [] module.exports = function(dom, vnodes, redraw) { if (!dom) throw new TypeError("DOM element being rendered to does not exist.") - if (currentDOM != null && dom.contains(currentDOM)) { - throw new TypeError("Node is currently being rendered to and thus is locked.") - } + var prevHooks = currentHooks var prevRedraw = currentRedraw - var prevDOM = currentDOM - var hooks = [] var active = activeElement(dom) var namespace = dom.namespaceURI + var hooks = currentHooks = [] + + if (currentlyRendering.some((d) => d === dom || d.contains(dom))) { + throw new TypeError("Node is currently being rendered to and thus is locked.") + } - currentDOM = dom + currentlyRendering.push(dom) currentRedraw = typeof redraw === "function" ? redraw : undefined try { // First time rendering into a node clears it out if (dom.vnodes == null) dom.textContent = "" vnodes = hyperscript.normalizeChildren(Array.isArray(vnodes) ? vnodes.slice() : [vnodes]) - updateNodes(dom, dom.vnodes, vnodes, hooks, null, namespace === "http://www.w3.org/1999/xhtml" ? undefined : namespace, 0) + updateNodes(dom, dom.vnodes, vnodes, null, namespace === "http://www.w3.org/1999/xhtml" ? undefined : namespace, 0) dom.vnodes = vnodes // `document.activeElement` can return null: https://html.spec.whatwg.org/multipage/interaction.html#dom-document-activeelement if (active != null && activeElement(dom) !== active && typeof active.focus === "function") active.focus() - for (var i = 0; i < hooks.length; i++) hooks[i]() + for (var {v, p, i} of hooks) { + try { + (0, v.state)(p, v.dom.signal, i) + } catch (e) { + console.error(e) + } + } } finally { currentRedraw = prevRedraw - currentDOM = prevDOM + currentHooks = prevHooks + currentlyRendering.pop() } } diff --git a/render/tests/test-attributes.js b/render/tests/test-attributes.js index c68b768ba..83f0531d5 100644 --- a/render/tests/test-attributes.js +++ b/render/tests/test-attributes.js @@ -668,20 +668,12 @@ o.spec("attributes", function() { }) }) o.spec("mutate attr object", function() { - o("warn when reusing attrs object", function() { - const _consoleWarn = console.warn - console.warn = o.spy() - + o("throw when reusing attrs object", function() { const attrs = {className: "on"} render(root, {tag: "input", attrs}) attrs.className = "off" - render(root, {tag: "input", attrs}) - - o(console.warn.callCount).equals(1) - o(console.warn.args[0]).equals("Don't reuse attrs object, use new object for every redraw, this will throw in next major") - - console.warn = _consoleWarn + o(() => render(root, {tag: "input", attrs})).throws(Error) }) }) }) diff --git a/render/tests/test-createFragment.js b/render/tests/test-createFragment.js index 51bfc6099..2d5b0171f 100644 --- a/render/tests/test-createFragment.js +++ b/render/tests/test-createFragment.js @@ -23,14 +23,12 @@ o.spec("createFragment", function() { var vnode = m.normalize([]) render(root, vnode) - o(vnode.dom).equals(null) o(root.childNodes.length).equals(0) }) o("handles childless fragment", function() { var vnode = m.normalize([]) render(root, vnode) - o(vnode.dom).equals(null) o(root.childNodes.length).equals(0) }) o("handles multiple children", function() { @@ -40,7 +38,7 @@ o.spec("createFragment", function() { o(root.childNodes.length).equals(2) o(root.childNodes[0].nodeName).equals("A") o(root.childNodes[1].nodeName).equals("B") - o(vnode.dom).equals(root.childNodes[0]) + o(vnode.children[0].dom).equals(root.childNodes[0]) }) o("handles td", function() { var vnode = m.normalize([m("td")]) @@ -48,6 +46,6 @@ o.spec("createFragment", function() { o(root.childNodes.length).equals(1) o(root.childNodes[0].nodeName).equals("TD") - o(vnode.dom).equals(root.childNodes[0]) + o(vnode.children[0].dom).equals(root.childNodes[0]) }) }) diff --git a/render/tests/test-updateElement.js b/render/tests/test-updateElement.js index 1aa232350..0eb598d9b 100644 --- a/render/tests/test-updateElement.js +++ b/render/tests/test-updateElement.js @@ -289,12 +289,12 @@ o.spec("updateElement", function() { var updated = m.key(2, m("div")) render(root, vnode) - var a = vnode.dom + var a = vnode.children[0].dom render(root, updated) render(root, vnode) - var c = vnode.dom + var c = vnode.children[0].dom o(root.childNodes.length).equals(1) o(a).notEquals(c) // this used to be a recycling pool test diff --git a/render/tests/test-updateNodes.js b/render/tests/test-updateNodes.js index 80b0ff20f..76dac2296 100644 --- a/render/tests/test-updateNodes.js +++ b/render/tests/test-updateNodes.js @@ -24,10 +24,10 @@ o.spec("updateNodes", function() { render(root, updated) o(root.childNodes.length).equals(2) - o(updated[0].dom.nodeName).equals("A") - o(updated[0].dom).equals(root.childNodes[0]) - o(updated[1].dom.nodeName).equals("B") - o(updated[1].dom).equals(root.childNodes[1]) + o(updated[0].children[0].dom.nodeName).equals("A") + o(updated[0].children[0].dom).equals(root.childNodes[0]) + o(updated[1].children[0].dom.nodeName).equals("B") + o(updated[1].children[0].dom).equals(root.childNodes[1]) }) o("handles el noop without key", function() { var vnodes = [m("a"), m("b")] From 933d4b556b183ceb91e3e69d5fabf7f9f8e67dea Mon Sep 17 00:00:00 2001 From: Claudia Meadows Date: Fri, 4 Oct 2024 21:30:57 -0700 Subject: [PATCH 41/95] Improve perf tests They weren't actually testing the update flow, only reliably the create flow. Also, they weren't testing `m.mount`, or `m.redraw` in their totality, which is honestly a very important thing to test. --- performance/test-perf.js | 339 ++++++++++++++++----------------------- 1 file changed, 138 insertions(+), 201 deletions(-) diff --git a/performance/test-perf.js b/performance/test-perf.js index d2a833726..d9bc1661d 100644 --- a/performance/test-perf.js +++ b/performance/test-perf.js @@ -32,10 +32,12 @@ SOFTWARE. // this doesn't require a CommonJS sham polyfill. // I add it globally just so it's visible in the tests. -/* global m, rootElem: true */ +/* global m, window, document, rootElem: true, simpleTree: false, nestedTree: false */ // set up browser env on before running tests var isDOM = typeof window !== "undefined" +// eslint-disable-next-line no-undef +var globalObject = typeof globalThis !== "undefined" ? globalThis : isDOM ? window : global var Benchmark if (isDOM) { @@ -93,226 +95,161 @@ var suite = new Benchmark.Suite("Mithril.js perf", { // eslint-disable-next-line no-unused-vars var xsuite = {add: function(name) { console.log("skipping " + name) }} -suite.add("construct large vnode tree", { - setup: function () { - this.fields = [] - - for(var i=100; i--;) { - this.fields.push((i * 999).toString(36)) - } - }, - fn: function () { - m(".foo.bar[data-foo=bar]", {p: 2}, - m("header", - m("h1.asdf", "a ", "b", " c ", 0, " d"), - m("nav", - m("a[href=/foo]", "Foo"), - m("a[href=/bar]", "Bar") - ) - ), - m("main", - m("form", - {onSubmit: function () {}}, - m("input[type=checkbox][checked]"), - m("input[type=checkbox]"), - m("fieldset", - this.fields.map(function (field) { - return m("label", - field, - ":", - m("input", {placeholder: field}) - ) - }) - ), - m("button-bar", - m("button", - {style: "width:10px; height:10px; border:1px solid #FFF;"}, - "Normal CSS" - ), - m("button", - {style: "top:0 ; right: 20"}, - "Poor CSS" - ), - m("button", - {style: "invalid-prop:1;padding:1px;font:12px/1.1 arial,sans-serif;", icon: true}, - "Poorer CSS" - ), - m("button", - {style: {margin: 0, padding: "10px", overflow: "visible"}}, - "Object CSS" - ) +globalObject.simpleTree = () => m(".foo.bar[data-foo=bar]", {p: 2}, + m("header", + m("h1.asdf", "a ", "b", " c ", 0, " d"), + m("nav", + m("a[href=/foo]", "Foo"), + m("a[href=/bar]", "Bar") + ) + ), + m("main", + m("form", + {onSubmit: function () {}}, + m("input[type=checkbox][checked]"), + m("input[type=checkbox]"), + m("fieldset", + this.fields.map(function (field) { + return m("label", + field, + ":", + m("input", {placeholder: field}) ) + }) + ), + m("button-bar", + m("button", + {style: "width:10px; height:10px; border:1px solid #FFF;"}, + "Normal CSS" + ), + m("button", + {style: "top:0 ; right: 20"}, + "Poor CSS" + ), + m("button", + {style: "invalid-prop:1;padding:1px;font:12px/1.1 arial,sans-serif;", icon: true}, + "Poorer CSS" + ), + m("button", + {style: {margin: 0, padding: "10px", overflow: "visible"}}, + "Object CSS" ) ) ) + ) +) + +globalObject.nestedTree = (() => { +// Result of `JSON.stringify(Array.from({length:100},(_,i)=>((i+1)*999).toString(36)))` +var fields = [ + "rr", "1ji", "2b9", "330", "3ur", "4mi", "5e9", "660", "6xr", "7pi", + "8h9", "990", "a0r", "asi", "bk9", "cc0", "d3r", "dvi", "en9", "ff0", + "g6r", "gyi", "hq9", "ii0", "j9r", "k1i", "kt9", "ll0", "mcr", "n4i", + "nw9", "oo0", "pfr", "q7i", "qz9", "rr0", "sir", "tai", "u29", "uu0", + "vlr", "wdi", "x59", "xx0", "yor", "zgi", "1089", "1100", "11rr", "12ji", + "13b9", "1430", "14ur", "15mi", "16e9", "1760", "17xr", "18pi", "19h9", "1a90", + "1b0r", "1bsi", "1ck9", "1dc0", "1e3r", "1evi", "1fn9", "1gf0", "1h6r", "1hyi", + "1iq9", "1ji0", "1k9r", "1l1i", "1lt9", "1ml0", "1ncr", "1o4i", "1ow9", "1po0", + "1qfr", "1r7i", "1rz9", "1sr0", "1tir", "1uai", "1v29", "1vu0", "1wlr", "1xdi", + "1y59", "1yx0", "1zor", "20gi", "2189", "2200", "22rr", "23ji", "24b9", "2530", +] + +var NestedHeader = () => m("header", + m("h1.asdf", "a ", "b", " c ", 0, " d"), + m("nav", + m("a", {href: "/foo"}, "Foo"), + m("a", {href: "/bar"}, "Bar") + ) +) + +var NestedForm = () => m("form", {onSubmit: function () {}}, + m("input[type=checkbox][checked]"), + m("input[type=checkbox]", {checked: false}), + m("fieldset", + m("label", + m("input[type=radio][checked]") + ), + m("label", + m("input[type=radio]") + ) + ), + m("fieldset", + fields.map(function (field) { + return m("label", + field, + ":", + m("input", {placeholder: field}) + ) + }) + ), + m(NestedButtonBar, null) +) + +var NestedButtonBar = () => m(".button-bar", + m(NestedButton, + {style: "width:10px; height:10px; border:1px solid #FFF;"}, + "Normal CSS" + ), + m(NestedButton, + {style: "top:0 ; right: 20"}, + "Poor CSS" + ), + m(NestedButton, + {style: "invalid-prop:1;padding:1px;font:12px/1.1 arial,sans-serif;", icon: true}, + "Poorer CSS" + ), + m(NestedButton, + {style: {margin: 0, padding: "10px", overflow: "visible"}}, + "Object CSS" + ) +) + +var NestedButton = (attrs) => m("button", attrs) + +var NestedMain = () => m(NestedForm) + +var NestedRoot = () => m("div.foo.bar[data-foo=bar]", + {p: 2}, + m(NestedHeader), + m(NestedMain) +) + +return () => m(NestedRoot) +})() + +suite.add("construct simple tree", { + fn: function () { + simpleTree() + }, +}) + +suite.add("mount simple tree", { + fn: function () { + m.mount(rootElem, simpleTree) }, }) -suite.add("rerender identical vnode", { +suite.add("redraw simple tree", { setup: function () { - this.cached = m(".foo.bar[data-foo=bar]", {p: 2}, - m("header", - m("h1.asdf", "a ", "b", " c ", 0, " d"), - m("nav", - m("a", {href: "/foo"}, "Foo"), - m("a", {href: "/bar"}, "Bar") - ) - ), - m("main", - m("form", {onSubmit: function () {}}, - m("input", {type: "checkbox", checked: true}), - m("input", {type: "checkbox", checked: false}), - m("fieldset", - m("label", - m("input", {type: "radio", checked: true}) - ), - m("label", - m("input", {type: "radio"}) - ) - ), - m("button-bar", - m("button", - {style: "width:10px; height:10px; border:1px solid #FFF;"}, - "Normal CSS" - ), - m("button", - {style: "top:0 ; right: 20"}, - "Poor CSS" - ), - m("button", - {style: "invalid-prop:1;padding:1px;font:12px/1.1 arial,sans-serif;", icon: true}, - "Poorer CSS" - ), - m("button", - {style: {margin: 0, padding: "10px", overflow: "visible"}}, - "Object CSS" - ) - ) - ) - ) - ) + m.mount(rootElem, simpleTree) }, fn: function () { - m.render(rootElem, this.cached) + m.redraw.sync() }, }) -suite.add("rerender same tree", { +suite.add("mount large nested tree", { fn: function () { - m.render(rootElem, m(".foo.bar[data-foo=bar]", {p: 2}, - m("header", - m("h1.asdf", "a ", "b", " c ", 0, " d"), - m("nav", - m("a", {href: "/foo"}, "Foo"), - m("a", {href: "/bar"}, "Bar") - ) - ), - m("main", - m("form", {onSubmit: function () {}}, - m("input", {type: "checkbox", checked: true}), - m("input", {type: "checkbox", checked: false}), - m("fieldset", - m("label", - m("input", {type: "radio", checked: true}) - ), - m("label", - m("input", {type: "radio"}) - ) - ), - m("button-bar", - m("button", - {style: "width:10px; height:10px; border:1px solid #FFF;"}, - "Normal CSS" - ), - m("button", - {style: "top:0 ; right: 20"}, - "Poor CSS" - ), - m("button", - {style: "invalid-prop:1;padding:1px;font:12px/1.1 arial,sans-serif;", icon: true}, - "Poorer CSS" - ), - m("button", - {style: {margin: 0, padding: "10px", overflow: "visible"}}, - "Object CSS" - ) - ) - ) - ) - )) + m.mount(rootElem, nestedTree) }, }) -suite.add("add large nested tree", { +suite.add("redraw large nested tree", { setup: function () { - var fields = [] - - for(var i=100; i--;) { - fields.push((i * 999).toString(36)) - } - - var NestedHeader = () => m("header", - m("h1.asdf", "a ", "b", " c ", 0, " d"), - m("nav", - m("a", {href: "/foo"}, "Foo"), - m("a", {href: "/bar"}, "Bar") - ) - ) - - var NestedForm = () => m("form", {onSubmit: function () {}}, - m("input[type=checkbox][checked]"), - m("input[type=checkbox]", {checked: false}), - m("fieldset", - m("label", - m("input[type=radio][checked]") - ), - m("label", - m("input[type=radio]") - ) - ), - m("fieldset", - fields.map(function (field) { - return m("label", - field, - ":", - m("input", {placeholder: field}) - ) - }) - ), - m(NestedButtonBar, null) - ) - - var NestedButtonBar = () => m(".button-bar", - m(NestedButton, - {style: "width:10px; height:10px; border:1px solid #FFF;"}, - "Normal CSS" - ), - m(NestedButton, - {style: "top:0 ; right: 20"}, - "Poor CSS" - ), - m(NestedButton, - {style: "invalid-prop:1;padding:1px;font:12px/1.1 arial,sans-serif;", icon: true}, - "Poorer CSS" - ), - m(NestedButton, - {style: {margin: 0, padding: "10px", overflow: "visible"}}, - "Object CSS" - ) - ) - - var NestedButton = (attrs) => m("button", attrs) - - var NestedMain = () => m(NestedForm) - - this.NestedRoot = () => m("div.foo.bar[data-foo=bar]", - {p: 2}, - m(NestedHeader), - m(NestedMain) - ) + m.mount(rootElem, nestedTree) }, fn: function () { - m.render(rootElem, m(this.NestedRoot)) + m.redraw.sync() }, }) From d7d8bef4c2d45027efd8f8e9be39cd43b68fea4c Mon Sep 17 00:00:00 2001 From: Claudia Meadows Date: Fri, 4 Oct 2024 21:36:48 -0700 Subject: [PATCH 42/95] Simplify the keyed diff fuzzer It shouldn't be testing particular implementation details, just that it works. Also, this one's more likely to exhaustively hit every possibility. --- render/tests/test-updateNodesFuzzer.js | 192 +++++++------------------ 1 file changed, 55 insertions(+), 137 deletions(-) diff --git a/render/tests/test-updateNodesFuzzer.js b/render/tests/test-updateNodesFuzzer.js index 60eaccded..b490b48bd 100644 --- a/render/tests/test-updateNodesFuzzer.js +++ b/render/tests/test-updateNodesFuzzer.js @@ -5,152 +5,70 @@ var domMock = require("../../test-utils/domMock") var render = require("../../render/render") var m = require("../../render/hyperscript") -// pilfered and adapted from https://github.com/domvm/domvm/blob/7aaec609e4c625b9acf9a22d035d6252a5ca654f/test/src/flat-list-keyed-fuzz.js -o.spec("updateNodes keyed list Fuzzer", function() { - var i = 0, $window, root - o.beforeEach(function() { - $window = domMock() - root = $window.document.createElement("div") - }) +o.spec("updateNodes keyed list Fuzzer", () => { + const maxLength = 12 + const testCount = 250 + const fromUsed = new Set() + const toUsed = new Set() - void [ - {delMax: 0, movMax: 50, insMax: 9}, - {delMax: 3, movMax: 5, insMax: 5}, - {delMax: 7, movMax: 15, insMax: 0}, - {delMax: 5, movMax: 100, insMax: 3}, - {delMax: 5, movMax: 0, insMax: 3}, - ].forEach(function(c) { - var tests = 250 - - while (tests--) { - var test = fuzzTest(c.delMax, c.movMax, c.insMax) - o(i++ + ": " + test.list.join() + " -> " + test.updated.join(), function() { - render(root, test.list.map((x) => m.key(x, m(x)))) - addSpies(root) - render(root, test.updated.map((x) => m.key(x, m(x)))) - - if (root.appendChild.callCount + root.insertBefore.callCount !== test.expected.creations + test.expected.moves) console.log(test, {aC: root.appendChild.callCount, iB: root.insertBefore.callCount}, [].map.call(root.childNodes, function(n){return n.nodeName.toLowerCase()})) - - o(root.appendChild.callCount + root.insertBefore.callCount).equals(test.expected.creations + test.expected.moves)("moves") - o(root.removeChild.callCount).equals(test.expected.deletions)("deletions") - o([].map.call(root.childNodes, function(n){return n.nodeName.toLowerCase()})).deepEquals(test.updated) - }) - } - }) -}) - -// https://en.wikipedia.org/wiki/Longest_increasing_subsequence -// impl borrowed from https://github.com/ivijs/ivi -function longestIncreasingSubsequence(a) { - var p = a.slice() - var result = [] - result.push(0) - var u - var v - - for (var i = 0, il = a.length; i < il; ++i) { - var j = result[result.length - 1] - if (a[j] < a[i]) { - p[i] = j - result.push(i) - continue - } - - u = 0 - v = result.length - 1 + function randomInt(max) { + // eslint-disable-next-line no-bitwise + return (Math.random() * max) | 0 + } - while (u < v) { - var c = ((u + v) / 2) | 0 // eslint-disable-line no-bitwise - if (a[result[c]] < a[i]) { - u = c + 1 - } else { - v = c + function randomUnique(used) { + for (;;) { + let max = randomInt(maxLength) + const keys = Array.from({length: max}, (_, i) => i) + // Perform a simple Fisher-Yates shuffle on the generated key range. + while (max) { + const index = randomInt(max--) + const temp = keys[index] + keys[index] = keys[max] + keys[max] = temp } - } - if (a[i] < a[result[u]]) { - if (u > 0) { - p[i] = result[u - 1] + const serialized = keys.join() + if (!used.has(serialized)) { + used.add(serialized) + return keys } - result[u] = i } } - u = result.length - v = result[u - 1] - - while (u-- > 0) { - result[u] = v - v = p[v] - } - - return result -} - -function rand(min, max) { - return Math.floor(Math.random() * (max - min + 1)) + min -} - -function ins(arr, qty) { - var p = ["a","b","c","d","e","f","g","h","i"] - - while (qty-- > 0) - arr.splice(rand(0, arr.length - 1), 0, p.shift()) -} - -function del(arr, qty) { - while (qty-- > 0) - arr.splice(rand(0, arr.length - 1), 1) -} - -function mov(arr, qty) { - while (qty-- > 0) { - var from = rand(0, arr.length - 1) - var to = rand(0, arr.length - 1) - - arr.splice(to, 0, arr.splice(from, 1)[0]) - } -} - -function fuzzTest(delMax, movMax, insMax) { - var list = ["k0","k1","k2","k3","k4","k5","k6","k7","k8","k9"] - var copy = list.slice() - - var delCount = rand(0, delMax), - movCount = rand(0, movMax), - insCount = rand(0, insMax) - - del(copy, delCount) - mov(copy, movCount) - - var expected = { - creations: insCount, - deletions: delCount, - moves: 0 - } - - if (movCount > 0) { - var newPos = copy.map(function(v) { - return list.indexOf(v) - }).filter(function(i) { - return i != -1 + function fuzzGroup(label, view, assert) { + o.spec(label, () => { + for (let i = 0; i < testCount; i++) { + const from = randomUnique(fromUsed) + const to = randomUnique(toUsed) + o(`${i}: ${from} -> ${to}`, () => { + var $window = domMock() + var root = $window.document.body + + render(root, from.map((x) => m.key(x, view(x)))) + render(root, to.map((x) => m.key(x, view(x)))) + assert(root, to) + }) + } }) - var lis = longestIncreasingSubsequence(newPos) - expected.moves = copy.length - lis.length } - ins(copy, insCount) - - return { - expected: expected, - list: list, - updated: copy - } -} - -function addSpies(node) { - node.appendChild = o.spy(node.appendChild) - node.insertBefore = o.spy(node.insertBefore) - node.removeChild = o.spy(node.removeChild) -} + fuzzGroup( + "element tag", + (i) => m(`t${i}`), + (root, to) => o(Array.from(root.childNodes, (n) => n.nodeName)).deepEquals(to.map((i) => `T${i}`)) + ) + + fuzzGroup( + "text value", + (i) => `${i}`, + (root, to) => o(Array.from(root.childNodes, (n) => n.nodeValue)).deepEquals(to.map((i) => `${i}`)) + ) + + fuzzGroup( + "text value in element", + (i) => m("div", `${i}`), + (root, to) => o(Array.from(root.childNodes, (n) => n.childNodes[0].nodeValue)).deepEquals(to.map((i) => `${i}`)) + ) +}) From 67a30f4ba42b713500239969c003d12e464b0f90 Mon Sep 17 00:00:00 2001 From: Claudia Meadows Date: Fri, 4 Oct 2024 21:38:42 -0700 Subject: [PATCH 43/95] Simplify the renderer, strip outdated and redundant tests, reject reused vnodes --- .eslintrc.js | 5 +- browser.js | 1 + mount-redraw.js | 1 + render/render.js | 610 ++++++++------------------- render/tests/test-component.js | 2 +- render/tests/test-input.js | 8 +- render/tests/test-onupdate.js | 10 +- render/tests/test-updateElement.js | 34 -- render/tests/test-updateNodes.js | 656 ++++++++++++----------------- route.js | 1 + stream/stream.js | 2 + test-utils/domMock.js | 38 ++ tests/test-api.js | 1 + 13 files changed, 499 insertions(+), 870 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index a16e2d8e3..cc4f76a5b 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -2,11 +2,14 @@ module.exports = { "env": { - "browser": true, "commonjs": true, "es6": true, "node": true }, + "globals": { + ReadableStream: true, + Response: true, + }, "parserOptions": { "ecmaVersion": 2015 }, diff --git a/browser.js b/browser.js index 0debb4ea3..4584f50c3 100644 --- a/browser.js +++ b/browser.js @@ -1,4 +1,5 @@ "use strict" +/* global window: false */ var m = require("./index") if (typeof module !== "undefined") module["exports"] = m diff --git a/mount-redraw.js b/mount-redraw.js index d3b68f4ab..2a56328c4 100644 --- a/mount-redraw.js +++ b/mount-redraw.js @@ -1,3 +1,4 @@ "use strict" +/* global requestAnimationFrame: false */ module.exports = require("./api/mount-redraw")(typeof requestAnimationFrame !== "undefined" ? requestAnimationFrame : null, typeof console !== "undefined" ? console : null) diff --git a/render/render.js b/render/render.js index 673bae946..7638a45e9 100644 --- a/render/render.js +++ b/render/render.js @@ -10,481 +10,234 @@ var nameSpace = { var currentHooks var currentRedraw +var currentParent +var currentRefNode +var currentNamespace -function getDocument(dom) { - return dom.ownerDocument; -} - -function getNameSpace(vnode) { - return vnode.attrs && vnode.attrs.xmlns || nameSpace[vnode.tag] -} +// Used for tainting nodes, to assert they aren't being reused. +var vnodeAccepted = new WeakSet() -// IE11 (at least) throws an UnspecifiedError when accessing document.activeElement when -// inside an iframe. Catch and swallow this error, and heavy-handidly return null. -function activeElement(dom) { - try { - return getDocument(dom).activeElement - } catch (e) { - return null +function assertVnodeIsNew(vnode) { + if (vnodeAccepted.has(vnode)) { + throw new TypeError("Vnodes must not be reused") } + vnodeAccepted.add(vnode) } + //create -function createNodes(rawParent, parent, vnodes, start, end, nextSibling, ns) { - for (var i = start; i < end; i++) { - var vnode = vnodes[i] - if (vnode != null) { - createNode(rawParent, parent, vnode, ns, nextSibling) - } - } +function createNodes(vnodes, start) { + for (var i = start; i < vnodes.length; i++) createNode(vnodes[i]) } -function createNode(rawParent, parent, vnode, ns, nextSibling) { +function createNode(vnode) { + if (vnode == null) return + assertVnodeIsNew(vnode) var tag = vnode.tag if (typeof tag === "string") { switch (tag) { case "!": throw new Error("No node present to retain with `m.retain()`") - case ">": createLayout(rawParent, vnode); break - case "#": createText(parent, vnode, nextSibling); break + case ">": createLayout(vnode); break + case "#": createText(vnode); break case "=": - case "[": createFragment(rawParent, parent, vnode, ns, nextSibling); break - default: createElement(parent, vnode, ns, nextSibling) + case "[": createNodes(vnode.children, 0); break + default: createElement(vnode) } } - else createComponent(rawParent, parent, vnode, ns, nextSibling) + else createComponent(vnode) } -function createLayout(rawParent, vnode) { +function createLayout(vnode) { vnode.dom = new AbortController() - currentHooks.push({v: vnode, p: rawParent, i: true}) -} -function createText(parent, vnode, nextSibling) { - vnode.dom = getDocument(parent).createTextNode(vnode.children) - insertDOM(parent, vnode.dom, nextSibling) + currentHooks.push({v: vnode, p: currentParent, i: true}) } -function createFragment(rawParent, parent, vnode, ns, nextSibling) { - var fragment = getDocument(parent).createDocumentFragment() - if (vnode.children != null) { - var children = vnode.children - createNodes(rawParent, fragment, children, 0, children.length, null, ns) - } - vnode.dom = fragment.firstChild - insertDOM(parent, fragment, nextSibling) +function createText(vnode) { + insertAfterCurrentRefNode(vnode.dom = currentParent.ownerDocument.createTextNode(vnode.children)) } -function createElement(parent, vnode, ns, nextSibling) { +function createElement(vnode) { var tag = vnode.tag var attrs = vnode.attrs var is = attrs && attrs.is + var prevParent = currentParent + var document = currentParent.ownerDocument + var prevNamespace = currentNamespace + var ns = attrs && attrs.xmlns || nameSpace[tag] || prevNamespace - ns = getNameSpace(vnode) || ns + var element = vnode.dom = ns ? + is ? document.createElementNS(ns, tag, {is: is}) : document.createElementNS(ns, tag) : + is ? document.createElement(tag, {is: is}) : document.createElement(tag) - var element = ns ? - is ? getDocument(parent).createElementNS(ns, tag, {is: is}) : getDocument(parent).createElementNS(ns, tag) : - is ? getDocument(parent).createElement(tag, {is: is}) : getDocument(parent).createElement(tag) - vnode.dom = element + insertAfterCurrentRefNode(element) - if (attrs != null) { - setAttrs(vnode, attrs, ns) - } + currentParent = element + currentRefNode = null + currentNamespace = ns - insertDOM(parent, element, nextSibling) + try { + if (attrs != null) { + setAttrs(vnode, attrs) + } - if (!maybeSetContentEditable(vnode)) { - if (vnode.children != null) { - var children = vnode.children - createNodes(element, element, children, 0, children.length, null, ns) - if (vnode.tag === "select" && attrs != null) setLateSelectAttrs(vnode, attrs) + if (!maybeSetContentEditable(vnode)) { + if (vnode.children) { + createNodes(vnode.children, 0) + if (vnode.tag === "select" && attrs != null) setLateSelectAttrs(vnode, attrs) + } } + } finally { + currentRefNode = element + currentParent = prevParent + currentNamespace = ns } } -function createComponent(rawParent, parent, vnode, ns, nextSibling) { +function createComponent(vnode) { var tree = (vnode.state = vnode.tag)(vnode.attrs) if (typeof tree === "function") tree = (vnode.state = tree)(vnode.attrs) - if (tree === vnode) throw Error("A view cannot return the vnode it received as argument") - vnode.instance = hyperscript.normalize(tree) - if (vnode.instance != null) { - createNode(rawParent, parent, vnode.instance, ns, nextSibling) - } + if (tree === vnode) throw new Error("A view cannot return the vnode it received as argument") + createNode(vnode.instance = hyperscript.normalize(tree)) } //update -/** - * @param {Element|Fragment} parent - the parent element - * @param {Vnode[] | null} old - the list of vnodes of the last `render()` call for - * this part of the tree - * @param {Vnode[] | null} vnodes - as above, but for the current `render()` call. - * @param {Function[]} hooks - an accumulator of post-render layout hooks - * @param {Element | null} nextSibling - the next DOM node if we're dealing with a - * fragment that is not the last item in its - * parent - * @param {'svg' | 'math' | String | null} ns) - the current XML namespace, if any - * @returns void - */ -// This function diffs and patches lists of vnodes, both keyed and unkeyed. -// -// We will: -// -// 1. describe its general structure -// 2. focus on the diff algorithm optimizations -// 3. discuss DOM node operations. - -// ## Overview: -// -// The updateNodes() function: -// - deals with trivial cases -// - determines whether the lists are keyed or unkeyed based on the first non-null node -// of each list. -// - diffs them and patches the DOM if needed (that's the brunt of the code) -// - manages the leftovers: after diffing, are there: -// - old nodes left to remove? -// - new nodes to insert? -// deal with them! -// -// The lists are only iterated over once, with an exception for the nodes in `old` that -// are visited in the fourth part of the diff and in the `removeNodes` loop. - -// ## Diffing -// -// Reading https://github.com/localvoid/ivi/blob/ddc09d06abaef45248e6133f7040d00d3c6be853/packages/ivi/src/vdom/implementation.ts#L617-L837 -// may be good for context on longest increasing subsequence-based logic for moving nodes. -// -// In order to diff keyed lists, one has to -// -// 1) match nodes in both lists, per key, and update them accordingly -// 2) create the nodes present in the new list, but absent in the old one -// 3) remove the nodes present in the old list, but absent in the new one -// 4) figure out what nodes in 1) to move in order to minimize the DOM operations. -// -// To achieve 1) one can create a dictionary of keys => index (for the old list), then iterate -// over the new list and for each new vnode, find the corresponding vnode in the old list using -// the map. -// 2) is achieved in the same step: if a new node has no corresponding entry in the map, it is new -// and must be created. -// For the removals, we actually remove the nodes that have been updated from the old list. -// The nodes that remain in that list after 1) and 2) have been performed can be safely removed. -// The fourth step is a bit more complex and relies on the longest increasing subsequence (LIS) -// algorithm. -// -// the longest increasing subsequence is the list of nodes that can remain in place. Imagine going -// from `1,2,3,4,5` to `4,5,1,2,3` where the numbers are not necessarily the keys, but the indices -// corresponding to the keyed nodes in the old list (keyed nodes `e,d,c,b,a` => `b,a,e,d,c` would -// match the above lists, for example). -// -// In there are two increasing subsequences: `4,5` and `1,2,3`, the latter being the longest. We -// can update those nodes without moving them, and only call `insertNode` on `4` and `5`. -// -// @localvoid adapted the algo to also support node deletions and insertions (the `lis` is actually -// the longest increasing subsequence *of old nodes still present in the new list*). -// -// It is a general algorithm that is fireproof in all circumstances, but it requires the allocation -// and the construction of a `key => oldIndex` map, and three arrays (one with `newIndex => oldIndex`, -// the `LIS` and a temporary one to create the LIS). -// -// So we cheat where we can: if the tails of the lists are identical, they are guaranteed to be part of -// the LIS and can be updated without moving them. -// -// If two nodes are swapped, they are guaranteed not to be part of the LIS, and must be moved (with -// the exception of the last node if the list is fully reversed). -// -// ## Finding the next sibling. -// -// `updateNode()` and `createNode()` expect a nextSibling parameter to perform DOM operations. -// When the list is being traversed top-down, at any index, the DOM nodes up to the previous -// vnode reflect the content of the new list, whereas the rest of the DOM nodes reflect the old -// list. The next sibling must be looked for in the old list using `getNextSibling(... oldStart + 1 ...)`. -// -// In the other scenarios (swaps, upwards traversal, map-based diff), -// the new vnodes list is traversed upwards. The DOM nodes at the bottom of the list reflect the -// bottom part of the new vnodes list, and we can use the `v.dom` value of the previous node -// as the next sibling (cached in the `nextSibling` variable). - - -// ## DOM node moves -// -// In most scenarios `updateNode()` and `createNode()` perform the DOM operations. However, -// this is not the case if the node moved (second and fourth part of the diff algo). We move -// the old DOM nodes before updateNode runs because it enables us to use the cached `nextSibling` -// variable rather than fetching it using `getNextSibling()`. - -function updateNodes(parent, old, vnodes, nextSibling, ns) { +function updateNodes(old, vnodes) { if (old === vnodes || old == null && vnodes == null) return - else if (old == null || old.length === 0) createNodes(parent, parent, vnodes, 0, vnodes.length, nextSibling, ns) - else if (vnodes == null || vnodes.length === 0) removeNodes(parent, old, 0, old.length) + else if (old == null || old.length === 0) createNodes(vnodes, 0) + else if (vnodes == null || vnodes.length === 0) removeNodes(old, 0) else { var isOldKeyed = old[0] != null && old[0].tag === "=" var isKeyed = vnodes[0] != null && vnodes[0].tag === "=" - var start = 0, oldStart = 0 - if (!isOldKeyed) while (oldStart < old.length && old[oldStart] == null) oldStart++ - if (!isKeyed) while (start < vnodes.length && vnodes[start] == null) start++ if (isOldKeyed !== isKeyed) { - removeNodes(parent, old, oldStart, old.length) - createNodes(parent, parent, vnodes, start, vnodes.length, nextSibling, ns) + // Key state changed. Replace the subtree + removeNodes(old, 0) + createNodes(vnodes, 0) } else if (!isKeyed) { - // Don't index past the end of either list (causes deopts). + // Not keyed. Patch the common prefix, remove the extra in the old, and create the + // extra in the new. + // + // Can't just take the max of both, because out-of-bounds accesses both disrupts + // optimizations and is just generally slower. var commonLength = old.length < vnodes.length ? old.length : vnodes.length - // Rewind if necessary to the first non-null index on either side. - // We could alternatively either explicitly create or remove nodes when `start !== oldStart` - // but that would be optimizing for sparse lists which are more rare than dense ones. - start = start < oldStart ? start : oldStart - for (; start < commonLength; start++) { - o = old[start] - v = vnodes[start] - if (o === v || o == null && v == null) continue - else if (o == null) createNode(parent, parent, v, ns, getNextSibling(old, start + 1, nextSibling)) - else if (v == null) removeNode(parent, o) - else updateNode(parent, o, v, getNextSibling(old, start + 1, nextSibling), ns) + for (var i = 0; i < commonLength; i++) { + updateNode(old[i], vnodes[i]) } - if (old.length > commonLength) removeNodes(parent, old, start, old.length) - if (vnodes.length > commonLength) createNodes(parent, parent, vnodes, start, vnodes.length, nextSibling, ns) + removeNodes(old, commonLength) + createNodes(vnodes, commonLength) } else { - // keyed diff - var oldEnd = old.length - 1, end = vnodes.length - 1, map, o, v, oe, ve, topSibling - - // bottom-up - while (oldEnd >= oldStart && end >= start) { - oe = old[oldEnd] - ve = vnodes[end] - if (oe.state !== ve.state) break - if (oe !== ve) updateNode(parent, oe, ve, nextSibling, ns) - if (ve.dom != null) nextSibling = ve.dom - oldEnd--, end-- - } - // top-down - while (oldEnd >= oldStart && end >= start) { - o = old[oldStart] - v = vnodes[start] - if (o.state !== v.state) break - oldStart++, start++ - if (o !== v) updateNode(parent, o, v, getNextSibling(old, oldStart, nextSibling), ns) - } - // swaps and list reversals - while (oldEnd >= oldStart && end >= start) { - if (start === end) break - if (o.state !== ve.state || oe.state !== v.state) break - topSibling = getNextSibling(old, oldStart, nextSibling) - moveDOM(parent, oe, topSibling) - if (oe !== v) updateNode(parent, oe, v, topSibling, ns) - if (++start <= --end) moveDOM(parent, o, nextSibling) - if (o !== ve) updateNode(parent, o, ve, nextSibling, ns) - if (ve.dom != null) nextSibling = ve.dom - oldStart++; oldEnd-- - oe = old[oldEnd] - ve = vnodes[end] - o = old[oldStart] - v = vnodes[start] - } - // bottom up once again - while (oldEnd >= oldStart && end >= start) { - if (oe.state !== ve.state) break - if (oe !== ve) updateNode(parent, oe, ve, nextSibling, ns) - if (ve.dom != null) nextSibling = ve.dom - oldEnd--, end-- - oe = old[oldEnd] - ve = vnodes[end] - } - if (start > end) removeNodes(parent, old, oldStart, oldEnd + 1) - else if (oldStart > oldEnd) createNodes(parent, parent, vnodes, start, end + 1, nextSibling, ns) - else { - // inspired by ivi https://github.com/ivijs/ivi/ by Boris Kaul - var originalNextSibling = nextSibling, vnodesLength = end - start + 1, oldIndices = new Array(vnodesLength), li=0, i=0, pos = 2147483647, matched = 0, map, lisIndices - for (i = 0; i < vnodesLength; i++) oldIndices[i] = -1 - for (i = end; i >= start; i--) { - if (map == null) map = getKeyMap(old, oldStart, oldEnd + 1) - ve = vnodes[i] - var oldIndex = map[ve.state] - if (oldIndex != null) { - pos = (oldIndex < pos) ? oldIndex : -1 // becomes -1 if nodes were re-ordered - oldIndices[i-start] = oldIndex - oe = old[oldIndex] - old[oldIndex] = null - if (oe !== ve) updateNode(parent, oe, ve, nextSibling, ns) - if (ve.dom != null) nextSibling = ve.dom - matched++ - } - } - nextSibling = originalNextSibling - if (matched !== oldEnd - oldStart + 1) removeNodes(parent, old, oldStart, oldEnd + 1) - if (matched === 0) createNodes(parent, parent, vnodes, start, end + 1, nextSibling, ns) - else { - if (pos === -1) { - // the indices of the indices of the items that are part of the - // longest increasing subsequence in the oldIndices list - lisIndices = makeLisIndices(oldIndices) - li = lisIndices.length - 1 - for (i = end; i >= start; i--) { - v = vnodes[i] - if (oldIndices[i-start] === -1) createNode(parent, parent, v, ns, nextSibling) - else { - if (lisIndices[li] === i - start) li-- - else moveDOM(parent, v, nextSibling) - } - if (v.dom != null) nextSibling = vnodes[i].dom - } - } else { - for (i = end; i >= start; i--) { - v = vnodes[i] - if (oldIndices[i-start] === -1) createNode(parent, parent, v, ns, nextSibling) - if (v.dom != null) nextSibling = vnodes[i].dom - } + // Keyed. I take a pretty straightforward approach here to keep it simple: + // 1. Build a map from old map to old vnode. + // 2. Walk the new vnodes, adding what's missing and patching what's in the old. + // 3. Remove from the old map the keys in the new vnodes, leaving only the keys that + // were removed this run. + // 4. Remove the remaining nodes in the old map that aren't in the new map. Since the + // new keys were already deleted, this is just a simple map iteration. + + var oldMap = new Map() + for (var p of old) oldMap.set(p.state, p) + + for (var n of vnodes) { + var p = oldMap.get(n.state) + if (p == null) { + createNodes(n.children, 0) + } else { + oldMap.delete(n.state) + var prev = currentRefNode + try { + moveToPosition(p) + } finally { + currentRefNode = prev } + updateNodes(p.children, n.children) } } + + oldMap.forEach(removeNode) } } } -function updateNode(parent, old, vnode, nextSibling, ns) { - var oldTag = old.tag, tag = vnode.tag - if (tag === "!") { +function updateNode(old, vnode) { + if (old == null) { + createNode(vnode) + } else if (vnode == null) { + removeNode(old) + } else if (vnode.tag === "!") { + assertVnodeIsNew(vnode) // If it's a retain node, transmute it into the node it's retaining. Makes it much easier // to implement and work with. // // Note: this key list *must* be complete. - vnode.tag = oldTag + vnode.tag = old.tag vnode.state = old.state vnode.attrs = old.attrs vnode.children = old.children vnode.dom = old.dom vnode.instance = old.instance - } else if (oldTag === tag && (tag !== "=" || vnode.state === old.state)) { - if (typeof oldTag === "string") { - switch (oldTag) { - case ">": updateLayout(parent, old, vnode); break + } else if (vnode.tag === old.tag && (vnode.tag !== "=" || vnode.state === old.state)) { + assertVnodeIsNew(vnode) + if (typeof vnode.tag === "string") { + switch (vnode.tag) { + case ">": updateLayout(old, vnode); break case "#": updateText(old, vnode); break case "=": - case "[": updateFragment(parent, old, vnode, nextSibling, ns); break - default: updateElement(old, vnode, ns) + case "[": updateNodes(old.children, vnode.children); break + default: updateElement(old, vnode) } } - else updateComponent(parent, old, vnode, nextSibling, ns) + else updateComponent(old, vnode) } else { - removeNode(parent, old) - createNode(parent, parent, vnode, ns, nextSibling) + removeNode(old) + createNode(vnode) } } -function updateLayout(parent, old, vnode) { +function updateLayout(old, vnode) { vnode.dom = old.dom - currentHooks.push({v: vnode, p: parent, i: false}) + currentHooks.push({v: vnode, p: currentParent, i: false}) } function updateText(old, vnode) { if (`${old.children}` !== `${vnode.children}`) old.dom.nodeValue = vnode.children - vnode.dom = old.dom -} -function updateFragment(parent, old, vnode, nextSibling, ns) { - updateNodes(parent, old.children, vnode.children, nextSibling, ns) - vnode.dom = null - if (vnode.children != null) { - for (var child of vnode.children) { - if (child != null && child.dom != null) { - if (vnode.dom == null) vnode.dom = child.dom - } - } - } + vnode.dom = currentRefNode = old.dom } -function updateElement(old, vnode, ns) { +function updateElement(old, vnode) { vnode.state = old.state - var element = vnode.dom = old.dom - ns = getNameSpace(vnode) || ns + var prevParent = currentParent + var prevNamespace = currentNamespace + var namespace = (currentParent = vnode.dom = old.dom).namespaceURI - updateAttrs(vnode, old.attrs, vnode.attrs, ns) - if (!maybeSetContentEditable(vnode)) { - updateNodes(element, old.children, vnode.children, null, ns) - } -} -function updateComponent(parent, old, vnode, nextSibling, ns) { - vnode.instance = hyperscript.normalize((vnode.state = old.state)(vnode.attrs, old.attrs)) - if (vnode.instance != null) { - if (vnode.instance === vnode) throw Error("A view cannot return the vnode it received as argument") - if (old.instance == null) createNode(parent, parent, vnode.instance, ns, nextSibling) - else updateNode(parent, old.instance, vnode.instance, nextSibling, ns) - } - else if (old.instance != null) { - removeNode(parent, old.instance, false) - } -} -function getKeyMap(vnodes, start, end) { - var map = Object.create(null) - for (; start < end; start++) { - var vnode = vnodes[start] - if (vnode != null) { - map[vnode.state] = start + currentNamespace = namespace === "http://www.w3.org/1999/xhtml" ? null : namespace + currentRefNode = null + try { + updateAttrs(vnode, old.attrs, vnode.attrs) + if (!maybeSetContentEditable(vnode)) { + updateNodes(old.children, vnode.children) } + } finally { + currentParent = prevParent + currentRefNode = vnode.dom + currentNamespace = prevNamespace } - return map } -// Lifted from ivi https://github.com/ivijs/ivi/ -// takes a list of unique numbers (-1 is special and can -// occur multiple times) and returns an array with the indices -// of the items that are part of the longest increasing -// subsequence -var lisTemp = [] -function makeLisIndices(a) { - var result = [0] - var u = 0, v = 0, i = 0 - var il = lisTemp.length = a.length - for (var i = 0; i < il; i++) lisTemp[i] = a[i] - for (var i = 0; i < il; ++i) { - if (a[i] === -1) continue - var j = result[result.length - 1] - if (a[j] < a[i]) { - lisTemp[i] = j - result.push(i) - continue - } - u = 0 - v = result.length - 1 - while (u < v) { - // Fast integer average without overflow. - // eslint-disable-next-line no-bitwise - var c = (u >>> 1) + (v >>> 1) + (u & v & 1) - if (a[result[c]] < a[i]) { - u = c + 1 - } - else { - v = c - } - } - if (a[i] < a[result[u]]) { - if (u > 0) lisTemp[i] = result[u - 1] - result[u] = i - } - } - u = result.length - v = result[u - 1] - while (u-- > 0) { - result[u] = v - v = lisTemp[v] - } - lisTemp.length = 0 - return result +function updateComponent(old, vnode) { + vnode.instance = hyperscript.normalize((vnode.state = old.state)(vnode.attrs, old.attrs)) + if (vnode.instance === vnode) throw new Error("A view cannot return the vnode it received as argument") + updateNode(old.instance, vnode.instance) } -function getNextSibling(vnodes, i, nextSibling) { - for (; i < vnodes.length; i++) { - if (vnodes[i] != null && vnodes[i].dom != null) return vnodes[i].dom +function insertAfterCurrentRefNode(child) { + if (currentRefNode) { + currentRefNode.after(currentRefNode = child) + } else { + currentParent.prepend(currentRefNode = child) } - return nextSibling } -// This moves only the nodes tracked by Mithril -function moveDOM(parent, vnode, nextSibling) { - if (typeof vnode.tag === "function") { - return moveDOM(parent, vnode.instance, nextSibling) - } else if (vnode.tag === "[" || vnode.tag === "=") { - if (Array.isArray(vnode.children)) { - for (var child of vnode.children) { - nextSibling = moveDOM(parent, child, nextSibling) - } - } - return nextSibling +function moveToPosition(vnode) { + while (typeof vnode.tag === "function") { + vnode = vnode.instance + if (!vnode) return + } + if (vnode.tag === "[" || vnode.tag === "=") { + vnode.children.forEach(moveToPosition) } else { - insertDOM(parent, vnode.dom, nextSibling) - return vnode.dom + insertAfterCurrentRefNode(vnode.dom) } } -function insertDOM(parent, dom, nextSibling) { - if (nextSibling != null) parent.insertBefore(dom, nextSibling) - else parent.appendChild(dom) -} - function maybeSetContentEditable(vnode) { if (vnode.attrs == null || ( vnode.attrs.contenteditable == null && // attribute @@ -496,13 +249,13 @@ function maybeSetContentEditable(vnode) { } //remove -function removeNodes(parent, vnodes, start, end) { - for (var i = start; i < end; i++) removeNode(parent, vnodes[i]) +function removeNodes(vnodes, start) { + for (var i = start; i < vnodes.length; i++) removeNode(vnodes[i]) } -function removeNode(parent, vnode) { +function removeNode(vnode) { if (vnode != null) { if (typeof vnode.tag === "function") { - if (vnode.instance != null) removeNode(parent, vnode.instance) + if (vnode.instance != null) removeNode(vnode.instance) } else if (vnode.tag === ">") { try { vnode.dom.abort() @@ -513,33 +266,30 @@ function removeNode(parent, vnode) { var isNode = vnode.tag !== "[" && vnode.tag !== "=" if (vnode.tag !== "#") { - removeNodes( - isNode ? vnode.dom : parent, - vnode.children, 0, vnode.children.length - ) + removeNodes(vnode.children, 0) } - if (isNode) parent.removeChild(vnode.dom) + if (isNode) vnode.dom.remove() } } } //attrs -function setAttrs(vnode, attrs, ns) { +function setAttrs(vnode, attrs) { // The DOM does things to inputs based on the value, so it needs set first. // See: https://github.com/MithrilJS/mithril.js/issues/2622 if (vnode.tag === "input" && attrs.type != null) vnode.dom.type = attrs.type var isFileInput = attrs != null && vnode.tag === "input" && attrs.type === "file" for (var key in attrs) { - setAttr(vnode, key, null, attrs[key], ns, isFileInput) + setAttr(vnode, key, null, attrs[key], isFileInput) } } -function setAttr(vnode, key, old, value, ns, isFileInput) { +function setAttr(vnode, key, old, value, isFileInput) { if (value == null || key === "is" || key === "children" || (old === value && !isFormAttribute(vnode, key)) && typeof value !== "object" || key === "type" && vnode.tag === "input") return if (key.startsWith("on")) updateEvent(vnode, key, value) else if (key.startsWith("xlink:")) vnode.dom.setAttributeNS(xlinkNs, key.slice(6), value) else if (key === "style") updateStyle(vnode.dom, old, value) - else if (hasPropertyKey(vnode, key, ns)) { + else if (hasPropertyKey(vnode, key)) { if (key === "value") { // Only do the coercion if we're actually going to check the value. /* eslint-disable no-implicit-coercion */ @@ -548,7 +298,7 @@ function setAttr(vnode, key, old, value, ns, isFileInput) { //setting input[type=file][value] to same value causes an error to be generated if it's non-empty case "input": case "textarea": - if (vnode.dom.value === "" + value && (isFileInput || vnode.dom === activeElement(vnode.dom))) return + if (vnode.dom.value === "" + value && (isFileInput || vnode.dom === vnode.dom.ownerDocument.activeElement)) return //setting input[type=file][value] to different value is an error if it's non-empty // Not ideal, but it at least works around the most common source of uncaught exceptions for now. if (isFileInput && "" + value !== "") { console.error("`value` is read-only on file inputs!"); return } @@ -567,18 +317,18 @@ function setAttr(vnode, key, old, value, ns, isFileInput) { vnode.dom.setAttribute(key, value === true ? "" : value) } } -function removeAttr(vnode, key, old, ns) { +function removeAttr(vnode, key, old) { if (old == null || key === "is" || key === "children") return if (key.startsWith("on")) updateEvent(vnode, key, undefined) else if (key.startsWith("xlink:")) vnode.dom.removeAttributeNS(xlinkNs, key.slice(6)) else if (key === "style") updateStyle(vnode.dom, old, null) else if ( - hasPropertyKey(vnode, key, ns) + hasPropertyKey(vnode, key) && key !== "class" && key !== "title" // creates "null" as title && !(key === "value" && ( vnode.tag === "option" - || vnode.tag === "select" && vnode.dom.selectedIndex === -1 && vnode.dom === activeElement(vnode.dom) + || vnode.tag === "select" && vnode.dom.selectedIndex === -1 && vnode.dom === vnode.dom.ownerDocument.activeElement )) && !(vnode.tag === "input" && key === "type") ) { @@ -600,7 +350,7 @@ function setLateSelectAttrs(vnode, attrs) { } if ("selectedIndex" in attrs) setAttr(vnode, "selectedIndex", null, attrs.selectedIndex, undefined) } -function updateAttrs(vnode, old, attrs, ns) { +function updateAttrs(vnode, old, attrs) { if (old && old === attrs) { throw new Error("Attributes object cannot be reused.") } @@ -612,14 +362,14 @@ function updateAttrs(vnode, old, attrs, ns) { if (vnode.tag === "input" && attrs.type != null) vnode.dom.setAttribute("type", attrs.type) var isFileInput = vnode.tag === "input" && attrs.type === "file" for (var key in attrs) { - setAttr(vnode, key, old && old[key], attrs[key], ns, isFileInput) + setAttr(vnode, key, old && old[key], attrs[key], isFileInput) } } var val if (old != null) { for (var key in old) { if (((val = old[key]) != null) && (attrs == null || attrs[key] == null)) { - removeAttr(vnode, key, val, ns) + removeAttr(vnode, key, val) } } } @@ -629,12 +379,12 @@ function updateAttrs(vnode, old, attrs, ns) { var propertyMayBeBugged = /^(?:href|list|form|width|height)$/ function isFormAttribute(vnode, attr) { return attr === "value" || attr === "checked" || attr === "selectedIndex" || - attr === "selected" && vnode.dom === activeElement(vnode.dom) || - vnode.tag === "option" && vnode.dom.parentNode === activeElement(vnode.dom) + attr === "selected" && vnode.dom === vnode.dom.ownerDocument.activeElement || + vnode.tag === "option" && vnode.dom.parentNode === vnode.dom.ownerDocument.activeElement } -function hasPropertyKey(vnode, key, ns) { +function hasPropertyKey(vnode, key) { // Filter out namespaced keys - return ns === undefined && ( + return currentNamespace == null && ( // If it's a custom element, just keep it. vnode.tag.indexOf("-") > -1 || vnode.attrs != null && vnode.attrs.is || !propertyMayBeBugged.test(key) @@ -741,26 +491,33 @@ var currentlyRendering = [] module.exports = function(dom, vnodes, redraw) { if (!dom) throw new TypeError("DOM element being rendered to does not exist.") - var prevHooks = currentHooks - var prevRedraw = currentRedraw - var active = activeElement(dom) - var namespace = dom.namespaceURI - var hooks = currentHooks = [] - if (currentlyRendering.some((d) => d === dom || d.contains(dom))) { throw new TypeError("Node is currently being rendered to and thus is locked.") } - currentlyRendering.push(dom) - currentRedraw = typeof redraw === "function" ? redraw : undefined + var active = dom.ownerDocument.activeElement + var namespace = dom.namespaceURI + + var prevHooks = currentHooks + var prevRedraw = currentRedraw + var prevParent = currentParent + var prevRefNode = currentRefNode + var prevNamespace = currentNamespace + var hooks = currentHooks = [] + try { + currentlyRendering.push(currentParent = dom) + currentRedraw = typeof redraw === "function" ? redraw : undefined + currentRefNode = null + currentNamespace = namespace === "http://www.w3.org/1999/xhtml" ? null : namespace + // First time rendering into a node clears it out if (dom.vnodes == null) dom.textContent = "" vnodes = hyperscript.normalizeChildren(Array.isArray(vnodes) ? vnodes.slice() : [vnodes]) - updateNodes(dom, dom.vnodes, vnodes, null, namespace === "http://www.w3.org/1999/xhtml" ? undefined : namespace, 0) + updateNodes(dom.vnodes, vnodes) dom.vnodes = vnodes // `document.activeElement` can return null: https://html.spec.whatwg.org/multipage/interaction.html#dom-document-activeelement - if (active != null && activeElement(dom) !== active && typeof active.focus === "function") active.focus() + if (active != null && dom.ownerDocument.activeElement !== active && typeof active.focus === "function") active.focus() for (var {v, p, i} of hooks) { try { (0, v.state)(p, v.dom.signal, i) @@ -771,6 +528,9 @@ module.exports = function(dom, vnodes, redraw) { } finally { currentRedraw = prevRedraw currentHooks = prevHooks + currentParent = prevParent + currentRefNode = prevRefNode + currentNamespace = prevNamespace currentlyRendering.pop() } } diff --git a/render/tests/test-component.js b/render/tests/test-component.js index c312d6c43..43c81776b 100644 --- a/render/tests/test-component.js +++ b/render/tests/test-component.js @@ -87,8 +87,8 @@ o.spec("component", function() { }) o("removes", function() { var component = () => m("div") + render(root, [m.key(1, m(component)), m.key(2, m("div"))]) var div = m("div") - render(root, [m.key(1, m(component)), m.key(2, div)]) render(root, [m.key(2, div)]) o(root.childNodes.length).equals(1) diff --git a/render/tests/test-input.js b/render/tests/test-input.js index 058d87529..be9e324d6 100644 --- a/render/tests/test-input.js +++ b/render/tests/test-input.js @@ -19,13 +19,11 @@ o.spec("form inputs", function() { o.spec("input", function() { o("maintains focus after move", function() { - var input = m("input") - var a = m("a") - var b = m("b") + var input - render(root, [m.key(1, input), m.key(2, a), m.key(3, b)]) + render(root, [m.key(1, input = m("input")), m.key(2, m("a")), m.key(3, m("b"))]) input.dom.focus() - render(root, [m.key(2, a), m.key(1, input), m.key(3, b)]) + render(root, [m.key(2, m("a")), m.key(1, input = m("input")), m.key(3, m("b"))]) o($window.document.activeElement).equals(input.dom) }) diff --git a/render/tests/test-onupdate.js b/render/tests/test-onupdate.js index 1c6db75ab..f1ed0bf43 100644 --- a/render/tests/test-onupdate.js +++ b/render/tests/test-onupdate.js @@ -34,14 +34,12 @@ o.spec("layout update", function() { }) o("does not call old callback when removing layout vnode from new vnode", function() { var layout = o.spy() - var vnode = m("a", m.layout(layout)) - var updated = m("a") - render(root, vnode) - render(root, vnode) - render(root, updated) + render(root, m("a", m.layout(layout))) + render(root, m("a", m.layout(layout))) + render(root, m("a")) - o(layout.calls.map((c) => c.args[2])).deepEquals([true]) + o(layout.calls.map((c) => c.args[2])).deepEquals([true, false]) }) o("invoked on noop", function() { var layout = o.spy() diff --git a/render/tests/test-updateElement.js b/render/tests/test-updateElement.js index 0eb598d9b..84420c641 100644 --- a/render/tests/test-updateElement.js +++ b/render/tests/test-updateElement.js @@ -284,38 +284,4 @@ o.spec("updateElement", function() { o(updated.dom.firstChild.namespaceURI).equals("http://www.w3.org/2000/svg") }) - o("doesn't restore since we're not recycling", function() { - var vnode = m.key(1, m("div")) - var updated = m.key(2, m("div")) - - render(root, vnode) - var a = vnode.children[0].dom - - render(root, updated) - - render(root, vnode) - var c = vnode.children[0].dom - - o(root.childNodes.length).equals(1) - o(a).notEquals(c) // this used to be a recycling pool test - }) - o("doesn't restore since we're not recycling (via map)", function() { - var a = m.key(1, m("div")) - var b = m.key(2, m("div")) - var c = m.key(3, m("div")) - var d = m.key(4, m("div")) - var e = m.key(5, m("div")) - var f = m.key(6, m("div")) - - render(root, [a, b, c]) - var x = root.childNodes[1] - - render(root, d) - - render(root, [e, b, f]) - var y = root.childNodes[1] - - o(root.childNodes.length).equals(3) - o(x).notEquals(y) // this used to be a recycling pool test - }) }) diff --git a/render/tests/test-updateNodes.js b/render/tests/test-updateNodes.js index 76dac2296..6341c40d0 100644 --- a/render/tests/test-updateNodes.js +++ b/render/tests/test-updateNodes.js @@ -23,10 +23,8 @@ o.spec("updateNodes", function() { render(root, vnodes) render(root, updated) - o(root.childNodes.length).equals(2) - o(updated[0].children[0].dom.nodeName).equals("A") + o(Array.from(root.childNodes, (n) => n.nodeName)).deepEquals(["A", "B"]) o(updated[0].children[0].dom).equals(root.childNodes[0]) - o(updated[1].children[0].dom.nodeName).equals("B") o(updated[1].children[0].dom).equals(root.childNodes[1]) }) o("handles el noop without key", function() { @@ -36,10 +34,8 @@ o.spec("updateNodes", function() { render(root, vnodes) render(root, updated) - o(root.childNodes.length).equals(2) - o(updated[0].dom.nodeName).equals("A") + o(Array.from(root.childNodes, (n) => n.nodeName)).deepEquals(["A", "B"]) o(updated[0].dom).equals(root.childNodes[0]) - o(updated[1].dom.nodeName).equals("B") o(updated[1].dom).equals(root.childNodes[1]) }) o("handles text noop", function() { @@ -49,8 +45,7 @@ o.spec("updateNodes", function() { render(root, vnodes) render(root, updated) - o(root.childNodes.length).equals(1) - o(root.firstChild.nodeValue).equals("a") + o(Array.from(root.childNodes, (n) => n.nodeValue)).deepEquals(["a"]) }) o("handles text noop w/ type casting", function() { var vnodes = 1 @@ -59,8 +54,7 @@ o.spec("updateNodes", function() { render(root, vnodes) render(root, updated) - o(root.childNodes.length).equals(1) - o(root.firstChild.nodeValue).equals("1") + o(Array.from(root.childNodes, (n) => n.nodeValue)).deepEquals(["1"]) }) o("handles falsy text noop w/ type casting", function() { var vnodes = 0 @@ -69,8 +63,7 @@ o.spec("updateNodes", function() { render(root, vnodes) render(root, updated) - o(root.childNodes.length).equals(1) - o(root.childNodes[0].nodeValue).equals("0") + o(Array.from(root.childNodes, (n) => n.nodeValue)).deepEquals(["0"]) }) o("handles fragment noop", function() { var vnodes = [m("a")] @@ -79,8 +72,7 @@ o.spec("updateNodes", function() { render(root, vnodes) render(root, updated) - o(root.childNodes.length).equals(1) - o(updated[0].dom.nodeName).equals("A") + o(Array.from(root.childNodes, (n) => n.nodeName)).deepEquals(["A"]) o(updated[0].dom).equals(root.childNodes[0]) }) o("handles fragment noop w/ text child", function() { @@ -90,8 +82,7 @@ o.spec("updateNodes", function() { render(root, vnodes) render(root, updated) - o(root.childNodes.length).equals(1) - o(updated[0].dom.nodeValue).equals("a") + o(Array.from(root.childNodes, (n) => n.nodeValue)).deepEquals(["a"]) o(updated[0].dom).equals(root.childNodes[0]) }) o("handles undefined to null noop", function() { @@ -110,15 +101,11 @@ o.spec("updateNodes", function() { render(root, vnodes) render(root, updated) - o(root.childNodes.length).equals(4) - o(updated[0].dom.nodeName).equals("S") - o(updated[0].dom).equals(root.childNodes[0]) - o(updated[1].dom.nodeName).equals("I") - o(updated[1].dom).equals(root.childNodes[1]) - o(updated[2].dom.nodeName).equals("B") - o(updated[2].dom).equals(root.childNodes[2]) - o(updated[3].dom.nodeName).equals("A") - o(updated[3].dom).equals(root.childNodes[3]) + o(Array.from(root.childNodes, (n) => n.nodeName)).deepEquals(["S", "I", "B", "A"]) + o(updated[0].children[0].dom).equals(root.childNodes[0]) + o(updated[1].children[0].dom).equals(root.childNodes[1]) + o(updated[2].children[0].dom).equals(root.childNodes[2]) + o(updated[3].children[0].dom).equals(root.childNodes[3]) }) o("reverses els w/ odd count", function() { var vnodes = [m.key(1, m("a")), m.key(2, m("b")), m.key(3, m("i"))] @@ -126,10 +113,7 @@ o.spec("updateNodes", function() { render(root, vnodes) render(root, updated) - o(root.childNodes.length).equals(3) - o(updated[0].dom.nodeName).equals("I") - o(updated[1].dom.nodeName).equals("B") - o(updated[2].dom.nodeName).equals("A") + o(Array.from(root.childNodes, (n) => n.nodeName)).deepEquals(["I", "B", "A"]) }) o("creates el at start", function() { var vnodes = [m.key(1, m("a"))] @@ -138,11 +122,9 @@ o.spec("updateNodes", function() { render(root, vnodes) render(root, updated) - o(root.childNodes.length).equals(2) - o(updated[0].dom.nodeName).equals("B") - o(updated[0].dom).equals(root.childNodes[0]) - o(updated[1].dom.nodeName).equals("A") - o(updated[1].dom).equals(root.childNodes[1]) + o(Array.from(root.childNodes, (n) => n.nodeName)).deepEquals(["B", "A"]) + o(updated[0].children[0].dom).equals(root.childNodes[0]) + o(updated[1].children[0].dom).equals(root.childNodes[1]) }) o("creates el at end", function() { var vnodes = [m.key(1, m("a"))] @@ -151,11 +133,9 @@ o.spec("updateNodes", function() { render(root, vnodes) render(root, updated) - o(root.childNodes.length).equals(2) - o(updated[0].dom.nodeName).equals("A") - o(updated[0].dom).equals(root.childNodes[0]) - o(updated[1].dom.nodeName).equals("B") - o(updated[1].dom).equals(root.childNodes[1]) + o(Array.from(root.childNodes, (n) => n.nodeName)).deepEquals(["A", "B"]) + o(updated[0].children[0].dom).equals(root.childNodes[0]) + o(updated[1].children[0].dom).equals(root.childNodes[1]) }) o("creates el in middle", function() { var vnodes = [m.key(1, m("a")), m.key(2, m("b"))] @@ -164,12 +144,10 @@ o.spec("updateNodes", function() { render(root, vnodes) render(root, updated) - o(updated[0].dom.nodeName).equals("A") - o(updated[0].dom).equals(root.childNodes[0]) - o(updated[1].dom.nodeName).equals("I") - o(updated[1].dom).equals(root.childNodes[1]) - o(updated[2].dom.nodeName).equals("B") - o(updated[2].dom).equals(root.childNodes[2]) + o(Array.from(root.childNodes, (n) => n.nodeName)).deepEquals(["A", "I", "B"]) + o(updated[0].children[0].dom).equals(root.childNodes[0]) + o(updated[1].children[0].dom).equals(root.childNodes[1]) + o(updated[2].children[0].dom).equals(root.childNodes[2]) }) o("creates el while reversing", function() { var vnodes = [m.key(1, m("a")), m.key(2, m("b"))] @@ -178,13 +156,10 @@ o.spec("updateNodes", function() { render(root, vnodes) render(root, updated) - o(root.childNodes.length).equals(3) - o(updated[0].dom.nodeName).equals("B") - o(updated[0].dom).equals(root.childNodes[0]) - o(updated[1].dom.nodeName).equals("I") - o(updated[1].dom).equals(root.childNodes[1]) - o(updated[2].dom.nodeName).equals("A") - o(updated[2].dom).equals(root.childNodes[2]) + o(Array.from(root.childNodes, (n) => n.nodeName)).deepEquals(["B", "I", "A"]) + o(updated[0].children[0].dom).equals(root.childNodes[0]) + o(updated[1].children[0].dom).equals(root.childNodes[1]) + o(updated[2].children[0].dom).equals(root.childNodes[2]) }) o("deletes el at start", function() { var vnodes = [m.key(2, m("b")), m.key(1, m("a"))] @@ -193,9 +168,8 @@ o.spec("updateNodes", function() { render(root, vnodes) render(root, updated) - o(root.childNodes.length).equals(1) - o(updated[0].dom.nodeName).equals("A") - o(updated[0].dom).equals(root.childNodes[0]) + o(Array.from(root.childNodes, (n) => n.nodeName)).deepEquals(["A"]) + o(updated[0].children[0].dom).equals(root.childNodes[0]) }) o("deletes el at end", function() { var vnodes = [m.key(1, m("a")), m.key(2, m("b"))] @@ -204,9 +178,8 @@ o.spec("updateNodes", function() { render(root, vnodes) render(root, updated) - o(root.childNodes.length).equals(1) - o(updated[0].dom.nodeName).equals("A") - o(updated[0].dom).equals(root.childNodes[0]) + o(Array.from(root.childNodes, (n) => n.nodeName)).deepEquals(["A"]) + o(updated[0].children[0].dom).equals(root.childNodes[0]) }) o("deletes el at middle", function() { var vnodes = [m.key(1, m("a")), m.key(3, m("i")), m.key(2, m("b"))] @@ -215,11 +188,9 @@ o.spec("updateNodes", function() { render(root, vnodes) render(root, updated) - o(root.childNodes.length).equals(2) - o(updated[0].dom.nodeName).equals("A") - o(updated[0].dom).equals(root.childNodes[0]) - o(updated[1].dom.nodeName).equals("B") - o(updated[1].dom).equals(root.childNodes[1]) + o(Array.from(root.childNodes, (n) => n.nodeName)).deepEquals(["A", "B"]) + o(updated[0].children[0].dom).equals(root.childNodes[0]) + o(updated[1].children[0].dom).equals(root.childNodes[1]) }) o("deletes el while reversing", function() { var vnodes = [m.key(1, m("a")), m.key(3, m("i")), m.key(2, m("b"))] @@ -228,11 +199,9 @@ o.spec("updateNodes", function() { render(root, vnodes) render(root, updated) - o(root.childNodes.length).equals(2) - o(updated[0].dom.nodeName).equals("B") - o(updated[0].dom).equals(root.childNodes[0]) - o(updated[1].dom.nodeName).equals("A") - o(updated[1].dom).equals(root.childNodes[1]) + o(Array.from(root.childNodes, (n) => n.nodeName)).deepEquals(["B", "A"]) + o(updated[0].children[0].dom).equals(root.childNodes[0]) + o(updated[1].children[0].dom).equals(root.childNodes[1]) }) o("creates, deletes, reverses els at same time", function() { var vnodes = [m.key(1, m("a")), m.key(3, m("i")), m.key(2, m("b"))] @@ -241,13 +210,10 @@ o.spec("updateNodes", function() { render(root, vnodes) render(root, updated) - o(root.childNodes.length).equals(3) - o(updated[0].dom.nodeName).equals("B") - o(updated[0].dom).equals(root.childNodes[0]) - o(updated[1].dom.nodeName).equals("A") - o(updated[1].dom).equals(root.childNodes[1]) - o(updated[2].dom.nodeName).equals("S") - o(updated[2].dom).equals(root.childNodes[2]) + o(Array.from(root.childNodes, (n) => n.nodeName)).deepEquals(["B", "A", "S"]) + o(updated[0].children[0].dom).equals(root.childNodes[0]) + o(updated[1].children[0].dom).equals(root.childNodes[1]) + o(updated[2].children[0].dom).equals(root.childNodes[2]) }) o("creates, deletes, reverses els at same time with '__proto__' key", function() { var vnodes = [m.key("__proto__", m("a")), m.key(3, m("i")), m.key(2, m("b"))] @@ -256,13 +222,10 @@ o.spec("updateNodes", function() { render(root, vnodes) render(root, updated) - o(root.childNodes.length).equals(3) - o(updated[0].dom.nodeName).equals("B") - o(updated[0].dom).equals(root.childNodes[0]) - o(updated[1].dom.nodeName).equals("A") - o(updated[1].dom).equals(root.childNodes[1]) - o(updated[2].dom.nodeName).equals("S") - o(updated[2].dom).equals(root.childNodes[2]) + o(Array.from(root.childNodes, (n) => n.nodeName)).deepEquals(["B", "A", "S"]) + o(updated[0].children[0].dom).equals(root.childNodes[0]) + o(updated[1].children[0].dom).equals(root.childNodes[1]) + o(updated[2].children[0].dom).equals(root.childNodes[2]) }) o("adds to empty fragment followed by el", function() { var vnodes = [m.key(1), m.key(2, m("b"))] @@ -271,11 +234,9 @@ o.spec("updateNodes", function() { render(root, vnodes) render(root, updated) - o(root.childNodes.length).equals(2) - o(updated[0].children[0].dom.nodeName).equals("A") + o(Array.from(root.childNodes, (n) => n.nodeName)).deepEquals(["A", "B"]) o(updated[0].children[0].dom).equals(root.childNodes[0]) - o(updated[1].dom.nodeName).equals("B") - o(updated[1].dom).equals(root.childNodes[1]) + o(updated[1].children[0].dom).equals(root.childNodes[1]) }) o("reverses followed by el", function() { var vnodes = [m.key(1, m.key(2, m("a")), m.key(3, m("b"))), m.key(4, m("i"))] @@ -284,13 +245,10 @@ o.spec("updateNodes", function() { render(root, vnodes) render(root, updated) - o(root.childNodes.length).equals(3) - o(updated[0].children[0].dom.nodeName).equals("B") - o(updated[0].children[0].dom).equals(root.childNodes[0]) - o(updated[0].children[1].dom.nodeName).equals("A") - o(updated[0].children[1].dom).equals(root.childNodes[1]) - o(updated[1].dom.nodeName).equals("I") - o(updated[1].dom).equals(root.childNodes[2]) + o(Array.from(root.childNodes, (n) => n.nodeName)).deepEquals(["B", "A", "I"]) + o(updated[0].children[0].children[0].dom).equals(root.childNodes[0]) + o(updated[0].children[1].children[0].dom).equals(root.childNodes[1]) + o(updated[1].children[0].dom).equals(root.childNodes[2]) }) o("populates fragment followed by el keyed", function() { var vnodes = [m.key(1), m.key(2, m("i"))] @@ -299,12 +257,9 @@ o.spec("updateNodes", function() { render(root, vnodes) render(root, updated) - o(root.childNodes.length).equals(3) - o(updated[0].children[0].dom.nodeName).equals("A") + o(Array.from(root.childNodes, (n) => n.nodeName)).deepEquals(["A", "B", "I"]) o(updated[0].children[0].dom).equals(root.childNodes[0]) - o(updated[0].children[0].dom.nextSibling.nodeName).equals("B") - o(updated[0].children[0].dom.nextSibling).equals(root.childNodes[1]) - o(updated[1].children[0].dom.nodeName).equals("I") + o(updated[0].children[1].dom).equals(root.childNodes[1]) o(updated[1].children[0].dom).equals(root.childNodes[2]) }) o("throws if fragment followed by null then el on first render keyed", function() { @@ -326,12 +281,9 @@ o.spec("updateNodes", function() { render(root, vnodes) render(root, updated) - o(root.childNodes.length).equals(3) - o(updated[0].children[0].dom.nodeName).equals("A") + o(Array.from(root.childNodes, (n) => n.nodeName)).deepEquals(["A", "B", "I"]) o(updated[0].children[0].dom).equals(root.childNodes[0]) - o(updated[0].children[0].dom.nextSibling.nodeName).equals("B") - o(updated[0].children[0].dom.nextSibling).equals(root.childNodes[1]) - o(updated[1].children[0].dom.nodeName).equals("I") + o(updated[0].children[1].dom).equals(root.childNodes[1]) o(updated[1].children[0].dom).equals(root.childNodes[2]) }) o("throws if childless fragment replaced followed by null then el keyed", function() { @@ -348,15 +300,11 @@ o.spec("updateNodes", function() { render(root, vnodes) render(root, updated) - o(root.childNodes.length).equals(4) - o(updated[0].dom.nodeName).equals("S") - o(updated[0].dom).equals(root.childNodes[0]) - o(updated[1].dom.nodeName).equals("A") - o(updated[1].dom).equals(root.childNodes[1]) - o(updated[2].dom.nodeName).equals("B") - o(updated[2].dom).equals(root.childNodes[2]) - o(updated[3].dom.nodeName).equals("I") - o(updated[3].dom).equals(root.childNodes[3]) + o(Array.from(root.childNodes, (n) => n.nodeName)).deepEquals(["S", "A", "B", "I"]) + o(updated[0].children[0].dom).equals(root.childNodes[0]) + o(updated[1].children[0].dom).equals(root.childNodes[1]) + o(updated[2].children[0].dom).equals(root.childNodes[2]) + o(updated[3].children[0].dom).equals(root.childNodes[3]) }) o("moves from start to end", function() { var vnodes = [m.key(1, m("a")), m.key(2, m("b")), m.key(3, m("i")), m.key(4, m("s"))] @@ -365,15 +313,11 @@ o.spec("updateNodes", function() { render(root, vnodes) render(root, updated) - o(root.childNodes.length).equals(4) - o(updated[0].dom.nodeName).equals("B") - o(updated[0].dom).equals(root.childNodes[0]) - o(updated[1].dom.nodeName).equals("I") - o(updated[1].dom).equals(root.childNodes[1]) - o(updated[2].dom.nodeName).equals("S") - o(updated[2].dom).equals(root.childNodes[2]) - o(updated[3].dom.nodeName).equals("A") - o(updated[3].dom).equals(root.childNodes[3]) + o(Array.from(root.childNodes, (n) => n.nodeName)).deepEquals(["B", "I", "S", "A"]) + o(updated[0].children[0].dom).equals(root.childNodes[0]) + o(updated[1].children[0].dom).equals(root.childNodes[1]) + o(updated[2].children[0].dom).equals(root.childNodes[2]) + o(updated[3].children[0].dom).equals(root.childNodes[3]) }) o("removes then recreate", function() { var vnodes = [m.key(1, m("a")), m.key(2, m("b")), m.key(3, m("i")), m.key(4, m("s"))] @@ -384,15 +328,11 @@ o.spec("updateNodes", function() { render(root, temp) render(root, updated) - o(root.childNodes.length).equals(4) - o(updated[0].dom.nodeName).equals("A") - o(updated[0].dom).equals(root.childNodes[0]) - o(updated[1].dom.nodeName).equals("B") - o(updated[1].dom).equals(root.childNodes[1]) - o(updated[2].dom.nodeName).equals("I") - o(updated[2].dom).equals(root.childNodes[2]) - o(updated[3].dom.nodeName).equals("S") - o(updated[3].dom).equals(root.childNodes[3]) + o(Array.from(root.childNodes, (n) => n.nodeName)).deepEquals(["A", "B", "I", "S"]) + o(updated[0].children[0].dom).equals(root.childNodes[0]) + o(updated[1].children[0].dom).equals(root.childNodes[1]) + o(updated[2].children[0].dom).equals(root.childNodes[2]) + o(updated[3].children[0].dom).equals(root.childNodes[3]) }) o("removes then recreate reversed", function() { var vnodes = [m.key(1, m("a")), m.key(2, m("b")), m.key(3, m("i")), m.key(4, m("s"))] @@ -403,15 +343,11 @@ o.spec("updateNodes", function() { render(root, temp) render(root, updated) - o(root.childNodes.length).equals(4) - o(updated[0].dom.nodeName).equals("S") - o(updated[0].dom).equals(root.childNodes[0]) - o(updated[1].dom.nodeName).equals("I") - o(updated[1].dom).equals(root.childNodes[1]) - o(updated[2].dom.nodeName).equals("B") - o(updated[2].dom).equals(root.childNodes[2]) - o(updated[3].dom.nodeName).equals("A") - o(updated[3].dom).equals(root.childNodes[3]) + o(Array.from(root.childNodes, (n) => n.nodeName)).deepEquals(["S", "I", "B", "A"]) + o(updated[0].children[0].dom).equals(root.childNodes[0]) + o(updated[1].children[0].dom).equals(root.childNodes[1]) + o(updated[2].children[0].dom).equals(root.childNodes[2]) + o(updated[3].children[0].dom).equals(root.childNodes[3]) }) o("removes then recreate smaller", function() { var vnodes = [m.key(1, m("a")), m.key(2, m("b"))] @@ -422,9 +358,8 @@ o.spec("updateNodes", function() { render(root, temp) render(root, updated) - o(root.childNodes.length).equals(1) - o(updated[0].dom.nodeName).equals("A") - o(updated[0].dom).equals(root.childNodes[0]) + o(Array.from(root.childNodes, (n) => n.nodeName)).deepEquals(["A"]) + o(updated[0].children[0].dom).equals(root.childNodes[0]) }) o("removes then recreate bigger", function() { var vnodes = [m.key(1, m("a")), m.key(2, m("b"))] @@ -435,13 +370,10 @@ o.spec("updateNodes", function() { render(root, temp) render(root, updated) - o(root.childNodes.length).equals(3) - o(updated[0].dom.nodeName).equals("A") - o(updated[0].dom).equals(root.childNodes[0]) - o(updated[1].dom.nodeName).equals("B") - o(updated[1].dom).equals(root.childNodes[1]) - o(updated[2].dom.nodeName).equals("I") - o(updated[2].dom).equals(root.childNodes[2]) + o(Array.from(root.childNodes, (n) => n.nodeName)).deepEquals(["A", "B", "I"]) + o(updated[0].children[0].dom).equals(root.childNodes[0]) + o(updated[1].children[0].dom).equals(root.childNodes[1]) + o(updated[2].children[0].dom).equals(root.childNodes[2]) }) o("removes then create different", function() { var vnodes = [m.key(1, m("a")), m.key(2, m("b"))] @@ -452,11 +384,9 @@ o.spec("updateNodes", function() { render(root, temp) render(root, updated) - o(root.childNodes.length).equals(2) - o(updated[0].dom.nodeName).equals("I") - o(updated[0].dom).equals(root.childNodes[0]) - o(updated[1].dom.nodeName).equals("S") - o(updated[1].dom).equals(root.childNodes[1]) + o(Array.from(root.childNodes, (n) => n.nodeName)).deepEquals(["I", "S"]) + o(updated[0].children[0].dom).equals(root.childNodes[0]) + o(updated[1].children[0].dom).equals(root.childNodes[1]) }) o("removes then create different smaller", function() { var vnodes = [m.key(1, m("a")), m.key(2, m("b"))] @@ -467,42 +397,8 @@ o.spec("updateNodes", function() { render(root, temp) render(root, updated) - o(root.childNodes.length).equals(1) - o(updated[0].dom.nodeName).equals("I") - o(updated[0].dom).equals(root.childNodes[0]) - }) - o("cached keyed nodes move when the list is reversed", function(){ - var a = m.key("a", m("a")) - var b = m.key("b", m("b")) - var c = m.key("c", m("c")) - var d = m.key("d", m("d")) - - render(root, [a, b, c, d]) - render(root, [d, c, b, a]) - - o(root.childNodes.length).equals(4) - o(root.childNodes[0].nodeName).equals("D") - o(root.childNodes[1].nodeName).equals("C") - o(root.childNodes[2].nodeName).equals("B") - o(root.childNodes[3].nodeName).equals("A") - }) - o("cached keyed nodes move when diffed via the map", function() { - var layout = o.spy() - var a = m.key("a", m("a", m.layout(layout))) - var b = m.key("b", m("b", m.layout(layout))) - var c = m.key("c", m("c", m.layout(layout))) - var d = m.key("d", m("d", m.layout(layout))) - - render(root, [a, b, c, d]) - render(root, [b, d, a, c]) - - o(root.childNodes.length).equals(4) - o(root.childNodes[0].nodeName).equals("B") - o(root.childNodes[1].nodeName).equals("D") - o(root.childNodes[2].nodeName).equals("A") - o(root.childNodes[3].nodeName).equals("C") - - o(layout.calls.map((c) => c.args[2])).deepEquals([true, true, true, true]) + o(Array.from(root.childNodes, (n) => n.nodeName)).deepEquals(["I"]) + o(updated[0].children[0].dom).equals(root.childNodes[0]) }) o("removes then create different bigger", function() { var vnodes = [m.key(1, m("a")), m.key(2, m("b"))] @@ -513,13 +409,10 @@ o.spec("updateNodes", function() { render(root, temp) render(root, updated) - o(root.childNodes.length).equals(3) - o(updated[0].dom.nodeName).equals("I") - o(updated[0].dom).equals(root.childNodes[0]) - o(updated[1].dom.nodeName).equals("S") - o(updated[1].dom).equals(root.childNodes[1]) - o(updated[2].dom.nodeName).equals("DIV") - o(updated[2].dom).equals(root.childNodes[2]) + o(Array.from(root.childNodes, (n) => n.nodeName)).deepEquals(["I", "S", "DIV"]) + o(updated[0].children[0].dom).equals(root.childNodes[0]) + o(updated[1].children[0].dom).equals(root.childNodes[1]) + o(updated[2].children[0].dom).equals(root.childNodes[2]) }) o("removes then create mixed", function() { var vnodes = [m.key(1, m("a")), m.key(2, m("b"))] @@ -530,11 +423,9 @@ o.spec("updateNodes", function() { render(root, temp) render(root, updated) - o(root.childNodes.length).equals(2) - o(updated[0].dom.nodeName).equals("A") - o(updated[0].dom).equals(root.childNodes[0]) - o(updated[1].dom.nodeName).equals("S") - o(updated[1].dom).equals(root.childNodes[1]) + o(Array.from(root.childNodes, (n) => n.nodeName)).deepEquals(["A", "S"]) + o(updated[0].children[0].dom).equals(root.childNodes[0]) + o(updated[1].children[0].dom).equals(root.childNodes[1]) }) o("removes then create mixed reversed", function() { var vnodes = [m.key(1, m("a")), m.key(2, m("b"))] @@ -545,11 +436,9 @@ o.spec("updateNodes", function() { render(root, temp) render(root, updated) - o(root.childNodes.length).equals(2) - o(updated[0].dom.nodeName).equals("S") - o(updated[0].dom).equals(root.childNodes[0]) - o(updated[1].dom.nodeName).equals("A") - o(updated[1].dom).equals(root.childNodes[1]) + o(Array.from(root.childNodes, (n) => n.nodeName)).deepEquals(["S", "A"]) + o(updated[0].children[0].dom).equals(root.childNodes[0]) + o(updated[1].children[0].dom).equals(root.childNodes[1]) }) o("removes then create mixed smaller", function() { var vnodes = [m.key(1, m("a")), m.key(2, m("b")), m.key(3, m("i"))] @@ -560,11 +449,9 @@ o.spec("updateNodes", function() { render(root, temp) render(root, updated) - o(root.childNodes.length).equals(2) - o(updated[0].dom.nodeName).equals("A") - o(updated[0].dom).equals(root.childNodes[0]) - o(updated[1].dom.nodeName).equals("S") - o(updated[1].dom).equals(root.childNodes[1]) + o(Array.from(root.childNodes, (n) => n.nodeName)).deepEquals(["A", "S"]) + o(updated[0].children[0].dom).equals(root.childNodes[0]) + o(updated[1].children[0].dom).equals(root.childNodes[1]) }) o("removes then create mixed smaller reversed", function() { var vnodes = [m.key(1, m("a")), m.key(2, m("b")), m.key(3, m("i"))] @@ -575,11 +462,9 @@ o.spec("updateNodes", function() { render(root, temp) render(root, updated) - o(root.childNodes.length).equals(2) - o(updated[0].dom.nodeName).equals("S") - o(updated[0].dom).equals(root.childNodes[0]) - o(updated[1].dom.nodeName).equals("A") - o(updated[1].dom).equals(root.childNodes[1]) + o(Array.from(root.childNodes, (n) => n.nodeName)).deepEquals(["S", "A"]) + o(updated[0].children[0].dom).equals(root.childNodes[0]) + o(updated[1].children[0].dom).equals(root.childNodes[1]) }) o("removes then create mixed bigger", function() { var vnodes = [m.key(1, m("a")), m.key(2, m("b"))] @@ -590,13 +475,10 @@ o.spec("updateNodes", function() { render(root, temp) render(root, updated) - o(root.childNodes.length).equals(3) - o(updated[0].dom.nodeName).equals("A") - o(updated[0].dom).equals(root.childNodes[0]) - o(updated[1].dom.nodeName).equals("I") - o(updated[1].dom).equals(root.childNodes[1]) - o(updated[2].dom.nodeName).equals("S") - o(updated[2].dom).equals(root.childNodes[2]) + o(Array.from(root.childNodes, (n) => n.nodeName)).deepEquals(["A", "I", "S"]) + o(updated[0].children[0].dom).equals(root.childNodes[0]) + o(updated[1].children[0].dom).equals(root.childNodes[1]) + o(updated[2].children[0].dom).equals(root.childNodes[2]) }) o("removes then create mixed bigger reversed", function() { var vnodes = [m.key(1, m("a")), m.key(2, m("b"))] @@ -607,24 +489,14 @@ o.spec("updateNodes", function() { render(root, temp) render(root, updated) - o(root.childNodes.length).equals(3) - o(updated[0].dom.nodeName).equals("S") - o(updated[0].dom).equals(root.childNodes[0]) - o(updated[1].dom.nodeName).equals("I") - o(updated[1].dom).equals(root.childNodes[1]) - o(updated[2].dom.nodeName).equals("A") - o(updated[2].dom).equals(root.childNodes[2]) + o(Array.from(root.childNodes, (n) => n.nodeName)).deepEquals(["S", "I", "A"]) + o(updated[0].children[0].dom).equals(root.childNodes[0]) + o(updated[1].children[0].dom).equals(root.childNodes[1]) + o(updated[2].children[0].dom).equals(root.childNodes[2]) }) o("change type, position and length", function() { - var vnodes = m("div", - undefined, - m("#", "a") - ) - var updated = m("div", - [m("#", "b")], - undefined, - undefined - ) + var vnodes = m("div", undefined, "a") + var updated = m("div", ["b"], undefined, undefined) render(root, vnodes) render(root, updated) @@ -642,14 +514,12 @@ o.spec("updateNodes", function() { render(root, temp2) render(root, updated) - o(root.childNodes.length).equals(2) - o(updated[0].dom.nodeName).equals("A") - o(updated[0].dom).equals(root.childNodes[0]) - o(updated[1].dom.nodeName).equals("B") - o(updated[1].dom).equals(root.childNodes[1]) - o(updated[0].dom.childNodes.length).equals(2) - o(updated[0].dom.childNodes[0].nodeName).equals("S") - o(updated[0].dom.childNodes[1].nodeName).equals("I") + o(Array.from(root.childNodes, (n) => n.nodeName)).deepEquals(["A", "B"]) + o(Array.from(root.childNodes[0].childNodes, (n) => n.nodeName)).deepEquals(["S", "I"]) + o(updated[0].children[0].dom).equals(root.childNodes[0]) + o(updated[1].children[0].dom).equals(root.childNodes[1]) + o(updated[0].children[0].children[0].children[0].dom).equals(root.childNodes[0].childNodes[0]) + o(updated[0].children[0].children[1].children[0].dom).equals(root.childNodes[0].childNodes[1]) }) o("removes then recreates nested", function() { var vnodes = [m.key(1, m("a", m.key(3, m("a", m.key(5, m("a")))), m.key(4, m("a", m.key(5, m("a")))))), m.key(2, m("a"))] @@ -660,78 +530,142 @@ o.spec("updateNodes", function() { render(root, temp) render(root, updated) - o(root.childNodes.length).equals(2) - o(root.childNodes[0].childNodes.length).equals(2) - o(root.childNodes[0].childNodes[0].childNodes.length).equals(1) - o(root.childNodes[0].childNodes[1].childNodes.length).equals(1) - o(root.childNodes[1].childNodes.length).equals(0) + o(Array.from(root.childNodes, (n) => n.nodeName)).deepEquals(["A", "A"]) + o(Array.from(root.childNodes[0].childNodes, (n) => n.nodeName)).deepEquals(["A", "A"]) + o(Array.from(root.childNodes[0].childNodes[0].childNodes, (n) => n.nodeName)).deepEquals(["A"]) + o(Array.from(root.childNodes[1].childNodes, (n) => n.nodeName)).deepEquals([]) }) - o("cached, non-keyed nodes skip diff", function () { - var layout = o.spy(); - var cached = m("a", m.layout(layout)) + o("reused top-level element children are rejected against the same root", function () { + var cached = m("a") render(root, cached) + o(() => render(root, cached)).throws(Error) + }) + o("reused top-level element children are rejected against a different root", function () { + var cached = m("a") + var otherRoot = $window.document.createElement("div") + render(root, cached) + o(() => render(otherRoot, cached)).throws(Error) + }) + o("reused inner fragment element children are rejected against the same root", function () { + var cached = m("a") + + render(root, [cached]) + o(() => render(root, [cached])).throws(Error) + }) + o("reused inner fragment element children are rejected against a different root", function () { + var cached = m("a") + var otherRoot = $window.document.createElement("div") - o(layout.calls.map((c) => c.args[2])).deepEquals([true]) + render(root, [cached]) + o(() => render(otherRoot, [cached])).throws(Error) + }) + o("reused inner element element children are rejected against the same root", function () { + var cached = m("a") + + render(root, m("div", cached)) + o(() => render(root, m("div", cached))).throws(Error) }) - o("cached, keyed nodes skip diff", function () { - var layout = o.spy() - var cached = m.key("a", m("a", m.layout(layout))) + o("reused inner element element children are rejected against a different root", function () { + var cached = m("a") + var otherRoot = $window.document.createElement("div") + render(root, m("div", cached)) + o(() => render(otherRoot, m("div", cached))).throws(Error) + }) + o("reused top-level retain children are rejected against the same root", function () { + var cached = m.retain() + + render(root, m("a")) render(root, cached) + o(() => render(root, cached)).throws(Error) + }) + o("reused top-level retain children are rejected against a different root", function () { + var cached = m.retain() + var otherRoot = $window.document.createElement("div") + + render(root, m("a")) render(root, cached) + o(() => render(otherRoot, cached)).throws(Error) + }) + o("reused inner fragment retain children are rejected against the same root", function () { + var cached = m.retain() - o(layout.calls.map((c) => c.args[2])).deepEquals([true]) + render(root, [m("a")]) + render(root, [cached]) + o(() => render(root, [cached])).throws(Error) }) - o("keyed cached elements are re-initialized when brought back from the pool (#2003)", function () { - var layout = o.spy() - var cached = m.key(1, m("B", - m("A", m.layout(layout), "A") - )) - render(root, m("div", cached)) - render(root, []) - render(root, m("div", cached)) + o("reused inner fragment retain children are rejected against a different root", function () { + var cached = m.retain() + var otherRoot = $window.document.createElement("div") - o(layout.calls.map((c) => c.args[2])).deepEquals([true, true]) + render(root, [m("a")]) + render(root, [cached]) + o(() => render(otherRoot, [cached])).throws(Error) }) + o("reused inner element retain children are rejected against the same root", function () { + var cached = m.retain() - o("unkeyed cached elements are re-initialized when brought back from the pool (#2003)", function () { - var layout = o.spy() - var cached = m("B", - m("A", m.layout(layout), "A") - ) + render(root, m("div", m("a"))) render(root, m("div", cached)) - render(root, []) + o(() => render(root, m("div", cached))).throws(Error) + }) + o("reused inner element retain children are rejected against a different root", function () { + var cached = m.retain() + var otherRoot = $window.document.createElement("div") + + render(root, m("div", m("a"))) render(root, m("div", cached)) + o(() => render(otherRoot, m("div", cached))).throws(Error) + }) + o("cross-removal reused top-level element children are rejected against the same root", function () { + var cached = m("a") - o(layout.calls.map((c) => c.args[2])).deepEquals([true, true]) + render(root, cached) + render(root, null) + o(() => render(root, cached)).throws(Error) }) + o("cross-removal reused inner fragment element children are rejected against the same root", function () { + var cached = m("a") + + render(root, [cached]) + render(root, null) + o(() => render(root, [cached])).throws(Error) + }) + o("cross-removal reused inner element element children are rejected against the same root", function () { + var cached = m("a") - o("keyed cached elements are re-initialized when brought back from nested pools (#2003)", function () { - var layout = o.spy() - var cached = m.key(1, m("B", - m("A", m.layout(layout), "A") - )) - render(root, m("div", cached)) - render(root, m("div")) - render(root, []) render(root, m("div", cached)) + render(root, null) + o(() => render(root, m("div", cached))).throws(Error) + }) + o("cross-removal reused top-level retain children are rejected against the same root", function () { + var cached = m.retain() - o(layout.calls.map((c) => c.args[2])).deepEquals([true, true]) + render(root, m("a")) + render(root, cached) + render(root, null) + render(root, m("a")) + o(() => render(root, cached)).throws(Error) }) + o("cross-removal reused inner fragment retain children are rejected against the same root", function () { + var cached = m.retain() - o("unkeyed cached elements are re-initialized when brought back from nested pools (#2003)", function () { - var layout = o.spy() - var cached = m("B", - m("A", m.layout(layout), "A") - ) - render(root, m("div", cached)) - render(root, m("div")) - render(root, []) - render(root, m("div", cached)) + render(root, [m("a")]) + render(root, [cached]) + render(root, null) + render(root, [m("a")]) + o(() => render(root, [cached])).throws(Error) + }) + o("cross-removal reused inner element retain children are rejected against the same root", function () { + var cached = m.retain() - o(layout.calls.map((c) => c.args[2])).deepEquals([true, true]) + render(root, m("div", m("a"))) + render(root, m("div", cached)) + render(root, null) + render(root, m("div", m("a"))) + o(() => render(root, m("div", cached))).throws(Error) }) o("null stays in place", function() { @@ -768,14 +702,14 @@ o.spec("updateNodes", function() { o(layout.calls.map((c) => c.args[2])).deepEquals([true, false, false]) o(onabort.callCount).equals(0) }) - o("node is recreated if key changes to undefined", function () { + o("node is recreated if unwrapped from a key", function () { var vnode = m.key(1, m("b")) var updated = m("b") render(root, vnode) render(root, updated) - o(vnode.dom).notEquals(updated.dom) + o(vnode.children[0].dom).notEquals(updated.dom) }) o("don't add back elements from fragments that are restored from the pool #1991", function() { render(root, [ @@ -822,73 +756,37 @@ o.spec("updateNodes", function() { o(onabort.callCount).equals(1) }) o("supports changing the element of a keyed element in a list when traversed bottom-up", function() { - try { - render(root, [m.key(2, m("a"))]) - render(root, [m.key(1, m("b")), m.key(2, m("b"))]) + render(root, [m.key(2, m("a"))]) + render(root, [m.key(1, m("b")), m.key(2, m("b"))]) - o(root.childNodes.length).equals(2) - o(root.childNodes[0].nodeName).equals("B") - o(root.childNodes[1].nodeName).equals("B") - } catch (e) { - o(e).equals(null) - } + o(Array.from(root.childNodes, (n) => n.nodeName)).deepEquals(["B", "B"]) }) o("supports changing the element of a keyed element in a list when looking up nodes using the map", function() { - try { - render(root, [m.key(1, m("x")), m.key(2, m("y")), m.key(3, m("z"))]) - render(root, [m.key(2, m("b")), m.key(1, m("c")), m.key(4, m("d")), m.key(3, m("e"))]) - - o(root.childNodes.length).equals(4) - o(root.childNodes[0].nodeName).equals("B") - o(root.childNodes[1].nodeName).equals("C") - o(root.childNodes[2].nodeName).equals("D") - o(root.childNodes[3].nodeName).equals("E") - } catch (e) { - o(e).equals(null) - } + render(root, [m.key(1, m("x")), m.key(2, m("y")), m.key(3, m("z"))]) + render(root, [m.key(2, m("b")), m.key(1, m("c")), m.key(4, m("d")), m.key(3, m("e"))]) + + o(Array.from(root.childNodes, (n) => n.nodeName)).deepEquals(["B", "C", "D", "E"]) }) o("don't fetch the nextSibling from the pool", function() { render(root, [[m.key(1, m("div")), m.key(2, m("div"))], m("p")]) render(root, [[], m("p")]) render(root, [[m.key(2, m("div")), m.key(1, m("div"))], m("p")]) - o([].map.call(root.childNodes, function(el) {return el.nodeName})).deepEquals(["DIV", "DIV", "P"]) + o(Array.from(root.childNodes, (el) => el.nodeName)).deepEquals(["DIV", "DIV", "P"]) }) - o("minimizes DOM operations when scrambling a keyed lists", function() { - var vnodes = vnodify("a,b,c,d") - var updated = vnodify("b,a,d,c") - var expectedTagNames = updated.map(function(vn) {return vn.children[0].tag}) - - render(root, vnodes) - - root.appendChild = o.spy(root.appendChild) - root.insertBefore = o.spy(root.insertBefore) - - render(root, updated) - - var tagNames = [].map.call(root.childNodes, function(n) {return n.nodeName.toLowerCase()}) - - o(root.appendChild.callCount + root.insertBefore.callCount).equals(2) - o(tagNames).deepEquals(expectedTagNames) - }) - o("minimizes DOM operations when reversing a keyed lists with an odd number of items", function() { + o("reverses a keyed lists with an odd number of items", function() { var vnodes = vnodify("a,b,c,d") var updated = vnodify("d,c,b,a") var expectedTagNames = updated.map(function(vn) {return vn.children[0].tag}) render(root, vnodes) - - root.appendChild = o.spy(root.appendChild) - root.insertBefore = o.spy(root.insertBefore) - render(root, updated) - var tagNames = [].map.call(root.childNodes, function(n) {return n.nodeName.toLowerCase()}) + var tagNames = Array.from(root.childNodes, (n) => n.nodeName.toLowerCase()) - o(root.appendChild.callCount + root.insertBefore.callCount).equals(3) o(tagNames).deepEquals(expectedTagNames) }) - o("minimizes DOM operations when reversing a keyed lists with an even number of items", function() { + o("reverses a keyed lists with an even number of items", function() { var vnodes = vnodify("a,b,c") var updated = vnodify("c,b,a") var vnodes = [m.key("a", m("a")), m.key("b", m("b")), m.key("c", m("c"))] @@ -896,66 +794,46 @@ o.spec("updateNodes", function() { var expectedTagNames = updated.map(function(vn) {return vn.children[0].tag}) render(root, vnodes) - - root.appendChild = o.spy(root.appendChild) - root.insertBefore = o.spy(root.insertBefore) - render(root, updated) - var tagNames = [].map.call(root.childNodes, function(n) {return n.nodeName.toLowerCase()}) + var tagNames = Array.from(root.childNodes, (n) => n.nodeName.toLowerCase()) - o(root.appendChild.callCount + root.insertBefore.callCount).equals(2) o(tagNames).deepEquals(expectedTagNames) }) - o("minimizes DOM operations when scrambling a keyed lists with prefixes and suffixes", function() { + o("scrambles a keyed lists with prefixes and suffixes", function() { var vnodes = vnodify("i,a,b,c,d,j") var updated = vnodify("i,b,a,d,c,j") var expectedTagNames = updated.map(function(vn) {return vn.children[0].tag}) render(root, vnodes) - - root.appendChild = o.spy(root.appendChild) - root.insertBefore = o.spy(root.insertBefore) - render(root, updated) - var tagNames = [].map.call(root.childNodes, function(n) {return n.nodeName.toLowerCase()}) + var tagNames = Array.from(root.childNodes, (n) => n.nodeName.toLowerCase()) - o(root.appendChild.callCount + root.insertBefore.callCount).equals(2) o(tagNames).deepEquals(expectedTagNames) }) - o("minimizes DOM operations when reversing a keyed lists with an odd number of items with prefixes and suffixes", function() { + o("reverses a keyed lists with an odd number of items with prefixes and suffixes", function() { var vnodes = vnodify("i,a,b,c,d,j") var updated = vnodify("i,d,c,b,a,j") var expectedTagNames = updated.map(function(vn) {return vn.children[0].tag}) render(root, vnodes) - - root.appendChild = o.spy(root.appendChild) - root.insertBefore = o.spy(root.insertBefore) - render(root, updated) - var tagNames = [].map.call(root.childNodes, function(n) {return n.nodeName.toLowerCase()}) + var tagNames = Array.from(root.childNodes, (n) => n.nodeName.toLowerCase()) - o(root.appendChild.callCount + root.insertBefore.callCount).equals(3) o(tagNames).deepEquals(expectedTagNames) }) - o("minimizes DOM operations when reversing a keyed lists with an even number of items with prefixes and suffixes", function() { + o("reverses a keyed lists with an even number of items with prefixes and suffixes", function() { var vnodes = vnodify("i,a,b,c,j") var updated = vnodify("i,c,b,a,j") var expectedTagNames = updated.map(function(vn) {return vn.children[0].tag}) render(root, vnodes) - - root.appendChild = o.spy(root.appendChild) - root.insertBefore = o.spy(root.insertBefore) - render(root, updated) - var tagNames = [].map.call(root.childNodes, function(n) {return n.nodeName.toLowerCase()}) + var tagNames = Array.from(root.childNodes, (n) => n.nodeName.toLowerCase()) - o(root.appendChild.callCount + root.insertBefore.callCount).equals(2) o(tagNames).deepEquals(expectedTagNames) }) o("scrambling sample 1", function() { @@ -964,15 +842,10 @@ o.spec("updateNodes", function() { var expectedTagNames = updated.map(function(vn) {return vn.children[0].tag}) render(root, vnodes) - - root.appendChild = o.spy(root.appendChild) - root.insertBefore = o.spy(root.insertBefore) - render(root, updated) - var tagNames = [].map.call(root.childNodes, function(n) {return n.nodeName.toLowerCase()}) + var tagNames = Array.from(root.childNodes, (n) => n.nodeName.toLowerCase()) - o(root.appendChild.callCount + root.insertBefore.callCount).equals(5) o(tagNames).deepEquals(expectedTagNames) }) o("scrambling sample 2", function() { @@ -981,15 +854,10 @@ o.spec("updateNodes", function() { var expectedTagNames = updated.map(function(vn) {return vn.children[0].tag}) render(root, vnodes) - - root.appendChild = o.spy(root.appendChild) - root.insertBefore = o.spy(root.insertBefore) - render(root, updated) - var tagNames = [].map.call(root.childNodes, function(n) {return n.nodeName.toLowerCase()}) + var tagNames = Array.from(root.childNodes, (n) => n.nodeName.toLowerCase()) - o(root.appendChild.callCount + root.insertBefore.callCount).equals(5) o(tagNames).deepEquals(expectedTagNames) }) @@ -1003,9 +871,7 @@ o.spec("updateNodes", function() { render(root, temp) render(root, updated) - o(root.childNodes.length).equals(2) - o(root.childNodes[0].nodeName).equals("A") - o(root.childNodes[1].nodeName).equals("B") + o(Array.from(root.childNodes, (n) => n.nodeName)).deepEquals(["A", "B"]) }) o("fragment child toggles from null in component when followed by null component then tag", function() { var flag = true @@ -1021,19 +887,13 @@ o.spec("updateNodes", function() { flag = true render(root, updated) - o(root.childNodes.length).equals(2) - o(root.childNodes[0].nodeName).equals("A") - o(root.childNodes[1].nodeName).equals("S") + o(Array.from(root.childNodes, (n) => n.nodeName)).deepEquals(["A", "S"]) }) o("removing a component that returns a fragment doesn't throw (regression test for incidental bug introduced while debugging some Flems)", function() { var component = () => [m("a"), m("b")] - try { - render(root, [m(component)]) - render(root, []) - - o(root.childNodes.length).equals(0) - } catch (e) { - o(e).equals(null) - } + render(root, [m(component)]) + render(root, []) + + o(root.childNodes.length).equals(0) }) }) diff --git a/route.js b/route.js index 98bd467f3..7e03cc521 100644 --- a/route.js +++ b/route.js @@ -1,4 +1,5 @@ "use strict" +/* global window: false */ var mountRedraw = require("./mount-redraw") diff --git a/stream/stream.js b/stream/stream.js index 98f41d96a..5e5882aac 100644 --- a/stream/stream.js +++ b/stream/stream.js @@ -2,6 +2,8 @@ ;(function() { "use strict" /* eslint-enable */ +/* global window: false */ + Stream.SKIP = {} Stream.lift = lift Stream.scan = scan diff --git a/test-utils/domMock.js b/test-utils/domMock.js index 4bd2cb466..c45caa1b3 100644 --- a/test-utils/domMock.js +++ b/test-utils/domMock.js @@ -109,6 +109,39 @@ module.exports = function(options) { } else throw new TypeError("Failed to execute 'removeChild', child not found in parent") } + function prepend(child) { + return insertBefore.call(this, child, this.firstChild) + } + function after(child) { + if (this == null || typeof this !== "object" || !("nodeType" in this)) { + throw new TypeError("Failed to execute 'remove', this is not of type 'ChildNode'") + } + if (child == null || typeof child !== "object" || !("nodeType" in child)) { + throw new TypeError("Failed to execute 'remove', parameter is not of type 'ChildNode'") + } + var parent = this.parentNode + if (parent == null) return + var index = parent.childNodes.indexOf(this) + if (index < 0) { + throw new TypeError("BUG: child linked to parent, parent doesn't contain child") + } + remove.call(child) + parent.childNodes.splice(index + 1, 0, child) + child.parentNode = parent + } + function remove() { + if (this == null || typeof this !== "object" || !("nodeType" in this)) { + throw new TypeError("Failed to execute 'remove', this is not of type 'ChildNode'") + } + var parent = this.parentNode + if (parent == null) return + var index = parent.childNodes.indexOf(this) + if (index < 0) { + throw new TypeError("BUG: child linked to parent, parent doesn't contain child") + } + parent.childNodes.splice(index, 1) + this.parentNode = null + } function insertBefore(child, reference) { var ancestor = this while (ancestor !== child && ancestor !== null) ancestor = ancestor.parentNode @@ -302,7 +335,10 @@ module.exports = function(options) { namespaceURI: "http://www.w3.org/1999/xhtml", appendChild: appendChild, removeChild: removeChild, + remove: remove, insertBefore: insertBefore, + prepend: prepend, + after: after, hasAttribute: hasAttribute, getAttribute: getAttribute, setAttribute: setAttribute, @@ -706,6 +742,8 @@ module.exports = function(options) { nodeType: 3, nodeName: "#text", parentNode: null, + remove: remove, + after: after, get childNodes() { return [] }, get firstChild() { return null }, get nodeValue() {return nodeValue}, diff --git a/tests/test-api.js b/tests/test-api.js index 9c8f02c00..1195e0902 100644 --- a/tests/test-api.js +++ b/tests/test-api.js @@ -1,4 +1,5 @@ "use strict" +/* global window: false */ var o = require("ospec") var browserMock = require("../test-utils/browserMock") From 84b095078acf2b2069c755a0cbdb81a7c6b97608 Mon Sep 17 00:00:00 2001 From: Claudia Meadows Date: Fri, 4 Oct 2024 21:48:56 -0700 Subject: [PATCH 44/95] Split code + tests, move source into dedicated directory Aims to ease discoverability. Plus, it's a bit easier to navigate. --- hyperscript.js | 3 --- index.js | 17 ----------------- .../tests/manual => manual-tests}/iframe.html | 2 +- .../tests/manual => manual-tests}/index.html | 0 mount-redraw.js | 4 ---- mount.js | 3 --- package.json | 7 ++++--- redraw.js | 3 --- render.js | 3 --- route.js | 6 ------ browser.js => src/browser.js | 0 {render => src/core}/hyperscript.js | 0 {api => src/core}/mount-redraw.js | 2 +- {render => src/core}/render.js | 0 src/index.js | 18 ++++++++++++++++++ {util => src/std}/init.js | 2 +- {util => src/std}/lazy.js | 4 ++-- {util => src/std}/p.js | 0 {api => src/std}/router.js | 2 +- {util => src/std}/tracked.js | 2 +- {util => src/std}/use.js | 2 +- {util => src/std}/with-progress.js | 0 {util => src/util}/hasOwn.js | 0 .../api/mountRedraw.js | 4 ++-- .../test-router.js => tests/api/router.js | 6 +++--- tests/{test-api.js => exported-api.js} | 0 .../render/attributes.js | 4 ++-- .../render/component.js | 4 ++-- .../render/createElement.js | 4 ++-- .../render/createFragment.js | 4 ++-- .../render/createNodes.js | 4 ++-- .../render/createText.js | 2 +- .../test-event.js => tests/render/event.js | 4 ++-- .../render/fragment.js | 2 +- .../render/hyperscript.js | 2 +- .../test-input.js => tests/render/input.js | 4 ++-- .../render/normalize.js | 2 +- .../render/normalizeChildren.js | 2 +- .../render/normalizeComponentChildren.js | 4 ++-- .../render/oncreate.js | 4 ++-- .../render/onremove.js | 4 ++-- .../render/onupdate.js | 4 ++-- .../render/render-hyperscript-integration.js | 4 ++-- .../test-render.js => tests/render/render.js | 4 ++-- .../test-retain.js => tests/render/retain.js | 4 ++-- .../render/textContent.js | 4 ++-- .../render/updateElement.js | 4 ++-- .../render/updateFragment.js | 4 ++-- .../render/updateNodes.js | 4 ++-- .../render/updateNodesFuzzer.js | 4 ++-- .../render/updateText.js | 2 +- .../tests/test-scan.js => tests/stream/scan.js | 2 +- .../stream/scanMerge.js | 2 +- .../test-stream.js => tests/stream/stream.js | 2 +- .../test-utils/browserMock.js | 0 .../test-utils/callAsync.js | 0 .../test-utils/domMock.js | 0 .../test-utils/parseURL.js | 0 .../test-utils/pushStateMock.js | 0 .../test-utils/throttleMock.js | 0 .../test-utils/xhrMock.js | 0 util/tests/test-init.js => tests/util/init.js | 4 ++-- util/tests/test-lazy.js => tests/util/lazy.js | 6 +++--- util/tests/test-p.js => tests/util/p.js | 2 +- .../test-tracked.js => tests/util/tracked.js | 2 +- util/tests/test-use.js => tests/util/use.js | 6 +++--- .../util/withProgress.js | 2 +- 67 files changed, 93 insertions(+), 113 deletions(-) delete mode 100644 hyperscript.js delete mode 100644 index.js rename {render/tests/manual => manual-tests}/iframe.html (87%) rename {render/tests/manual => manual-tests}/index.html (100%) delete mode 100644 mount-redraw.js delete mode 100644 mount.js delete mode 100644 redraw.js delete mode 100644 render.js delete mode 100644 route.js rename browser.js => src/browser.js (100%) rename {render => src/core}/hyperscript.js (100%) rename {api => src/core}/mount-redraw.js (95%) rename {render => src/core}/render.js (100%) create mode 100644 src/index.js rename {util => src/std}/init.js (78%) rename {util => src/std}/lazy.js (88%) rename {util => src/std}/p.js (100%) rename {api => src/std}/router.js (98%) rename {util => src/std}/tracked.js (98%) rename {util => src/std}/use.js (87%) rename {util => src/std}/with-progress.js (100%) rename {util => src/util}/hasOwn.js (100%) rename api/tests/test-mountRedraw.js => tests/api/mountRedraw.js (99%) rename api/tests/test-router.js => tests/api/router.js (98%) rename tests/{test-api.js => exported-api.js} (100%) rename render/tests/test-attributes.js => tests/render/attributes.js (99%) rename render/tests/test-component.js => tests/render/component.js (99%) rename render/tests/test-createElement.js => tests/render/createElement.js (97%) rename render/tests/test-createFragment.js => tests/render/createFragment.js (93%) rename render/tests/test-createNodes.js => tests/render/createNodes.js (92%) rename render/tests/test-createText.js => tests/render/createText.js (97%) rename render/tests/test-event.js => tests/render/event.js (98%) rename render/tests/test-fragment.js => tests/render/fragment.js (99%) rename render/tests/test-hyperscript.js => tests/render/hyperscript.js (99%) rename render/tests/test-input.js => tests/render/input.js (98%) rename render/tests/test-normalize.js => tests/render/normalize.js (96%) rename render/tests/test-normalizeChildren.js => tests/render/normalizeChildren.js (96%) rename render/tests/test-normalizeComponentChildren.js => tests/render/normalizeComponentChildren.js (88%) rename render/tests/test-oncreate.js => tests/render/oncreate.js (97%) rename render/tests/test-onremove.js => tests/render/onremove.js (96%) rename render/tests/test-onupdate.js => tests/render/onupdate.js (97%) rename render/tests/test-render-hyperscript-integration.js => tests/render/render-hyperscript-integration.js (99%) rename render/tests/test-render.js => tests/render/render.js (98%) rename render/tests/test-retain.js => tests/render/retain.js (95%) rename render/tests/test-textContent.js => tests/render/textContent.js (98%) rename render/tests/test-updateElement.js => tests/render/updateElement.js (98%) rename render/tests/test-updateFragment.js => tests/render/updateFragment.js (93%) rename render/tests/test-updateNodes.js => tests/render/updateNodes.js (99%) rename render/tests/test-updateNodesFuzzer.js => tests/render/updateNodesFuzzer.js (94%) rename render/tests/test-updateText.js => tests/render/updateText.js (97%) rename stream/tests/test-scan.js => tests/stream/scan.js (96%) rename stream/tests/test-scanMerge.js => tests/stream/scanMerge.js (94%) rename stream/tests/test-stream.js => tests/stream/stream.js (99%) rename test-utils/tests/test-browserMock.js => tests/test-utils/browserMock.js (100%) rename test-utils/tests/test-callAsync.js => tests/test-utils/callAsync.js (100%) rename test-utils/tests/test-domMock.js => tests/test-utils/domMock.js (100%) rename test-utils/tests/test-parseURL.js => tests/test-utils/parseURL.js (100%) rename test-utils/tests/test-pushStateMock.js => tests/test-utils/pushStateMock.js (100%) rename test-utils/tests/test-throttleMock.js => tests/test-utils/throttleMock.js (100%) rename test-utils/tests/test-xhrMock.js => tests/test-utils/xhrMock.js (100%) rename util/tests/test-init.js => tests/util/init.js (89%) rename util/tests/test-lazy.js => tests/util/lazy.js (98%) rename util/tests/test-p.js => tests/util/p.js (99%) rename util/tests/test-tracked.js => tests/util/tracked.js (99%) rename util/tests/test-use.js => tests/util/use.js (93%) rename util/tests/test-withProgress.js => tests/util/withProgress.js (96%) diff --git a/hyperscript.js b/hyperscript.js deleted file mode 100644 index 69f4e0882..000000000 --- a/hyperscript.js +++ /dev/null @@ -1,3 +0,0 @@ -"use strict" - -module.exports = require("./render/hyperscript") diff --git a/index.js b/index.js deleted file mode 100644 index 2f4e10b04..000000000 --- a/index.js +++ /dev/null @@ -1,17 +0,0 @@ -"use strict" - -var m = require("./hyperscript") -var mountRedraw = require("./mount-redraw") - -m.mount = mountRedraw.mount -m.route = require("./route") -m.render = require("./render") -m.redraw = mountRedraw.redraw -m.p = require("./util/p") -m.withProgress = require("./util/with-progress") -m.lazy = require("./util/lazy") -m.init = require("./util/init") -m.use = require("./util/use") -m.tracked = require("./util/tracked") - -module.exports = m diff --git a/render/tests/manual/iframe.html b/manual-tests/iframe.html similarity index 87% rename from render/tests/manual/iframe.html rename to manual-tests/iframe.html index 4669615a5..52efd3cb6 100644 --- a/render/tests/manual/iframe.html +++ b/manual-tests/iframe.html @@ -5,7 +5,7 @@
- + + - - + + + Open the browser console. diff --git a/performance/inject-mock-globals.js b/performance/inject-mock-globals.js new file mode 100644 index 000000000..026f10f48 --- /dev/null +++ b/performance/inject-mock-globals.js @@ -0,0 +1,6 @@ +/* global global */ +import "../test-utils/injectBrowserMock.js" +import "../src/browser.js" +import Benchmark from "benchmark" + +global.Benchmark = Benchmark diff --git a/performance/test-perf-impl.js b/performance/test-perf-impl.js new file mode 100644 index 000000000..86731ba60 --- /dev/null +++ b/performance/test-perf-impl.js @@ -0,0 +1,395 @@ +/* Based off of preact's perf tests, so including their MIT license */ +/* +The MIT License (MIT) + +Copyright (c) 2017 Jason Miller + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +// Note: this tests against the generated bundle in browsers, but it tests +// against `index.js` in Node. Please do keep that in mind while testing. +// +// Mithril.js and Benchmark.js are loaded globally via bundle in the browser, so +// this doesn't require a CommonJS sham polyfill. + +// I add it globally just so it's visible in the tests. +/* global m, Benchmark, global, window, document, rootElem: true, simpleTree: false, nestedTree: false */ + +// set up browser env on before running tests +var isDOM = typeof window !== "undefined" +// eslint-disable-next-line no-undef +var globalObject = typeof globalThis !== "undefined" ? globalThis : isDOM ? window : global + +globalObject.rootElem = null + +function cycleRoot() { + if (rootElem) document.body.removeChild(rootElem) + document.body.appendChild(rootElem = document.createElement("div")) +} + +// Initialize benchmark suite +Benchmark.options.async = true +Benchmark.options.initCount = 10 +Benchmark.options.minSamples = 40 + +if (isDOM) { + // Wait long enough for the browser to actually commit the DOM changes to + // the screen before moving on to the next cycle, so things are at least + // reasonably fresh each cycle. + Benchmark.options.delay = 1 / 30 /* frames per second */ +} + +var suite = new Benchmark.Suite("Mithril.js perf", { + onStart: function () { + this.start = Date.now() + }, + + onCycle: function (e) { + console.log(e.target.toString()) + cycleRoot() + }, + + onComplete: function () { + console.log("Completed perf tests in " + (Date.now() - this.start) + "ms") + }, + + onError: function (e) { + console.error(e) + }, +}) +// eslint-disable-next-line no-unused-vars +var xsuite = {add: function(name) { console.log("skipping " + name) }} + +globalObject.simpleTree = () => m(".foo.bar[data-foo=bar]", {p: 2}, + m("header", + m("h1.asdf", "a ", "b", " c ", 0, " d"), + m("nav", + m("a[href=/foo]", "Foo"), + m("a[href=/bar]", "Bar") + ) + ), + m("main", + m("form", + {onSubmit: function () {}}, + m("input[type=checkbox][checked]"), + m("input[type=checkbox]"), + m("fieldset", + this.fields.map(function (field) { + return m("label", + field, + ":", + m("input", {placeholder: field}) + ) + }) + ), + m("button-bar", + m("button", + {style: "width:10px; height:10px; border:1px solid #FFF;"}, + "Normal CSS" + ), + m("button", + {style: "top:0 ; right: 20"}, + "Poor CSS" + ), + m("button", + {style: "invalid-prop:1;padding:1px;font:12px/1.1 arial,sans-serif;", icon: true}, + "Poorer CSS" + ), + m("button", + {style: {margin: 0, padding: "10px", overflow: "visible"}}, + "Object CSS" + ) + ) + ) + ) +) + +globalObject.nestedTree = (() => { +// Result of `JSON.stringify(Array.from({length:100},(_,i)=>((i+1)*999).toString(36)))` +var fields = [ + "rr", "1ji", "2b9", "330", "3ur", "4mi", "5e9", "660", "6xr", "7pi", + "8h9", "990", "a0r", "asi", "bk9", "cc0", "d3r", "dvi", "en9", "ff0", + "g6r", "gyi", "hq9", "ii0", "j9r", "k1i", "kt9", "ll0", "mcr", "n4i", + "nw9", "oo0", "pfr", "q7i", "qz9", "rr0", "sir", "tai", "u29", "uu0", + "vlr", "wdi", "x59", "xx0", "yor", "zgi", "1089", "1100", "11rr", "12ji", + "13b9", "1430", "14ur", "15mi", "16e9", "1760", "17xr", "18pi", "19h9", "1a90", + "1b0r", "1bsi", "1ck9", "1dc0", "1e3r", "1evi", "1fn9", "1gf0", "1h6r", "1hyi", + "1iq9", "1ji0", "1k9r", "1l1i", "1lt9", "1ml0", "1ncr", "1o4i", "1ow9", "1po0", + "1qfr", "1r7i", "1rz9", "1sr0", "1tir", "1uai", "1v29", "1vu0", "1wlr", "1xdi", + "1y59", "1yx0", "1zor", "20gi", "2189", "2200", "22rr", "23ji", "24b9", "2530", +] + +var NestedHeader = () => m("header", + m("h1.asdf", "a ", "b", " c ", 0, " d"), + m("nav", + m("a", {href: "/foo"}, "Foo"), + m("a", {href: "/bar"}, "Bar") + ) +) + +var NestedForm = () => m("form", {onSubmit: function () {}}, + m("input[type=checkbox][checked]"), + m("input[type=checkbox]", {checked: false}), + m("fieldset", + m("label", + m("input[type=radio][checked]") + ), + m("label", + m("input[type=radio]") + ) + ), + m("fieldset", + fields.map(function (field) { + return m("label", + field, + ":", + m("input", {placeholder: field}) + ) + }) + ), + m(NestedButtonBar, null) +) + +var NestedButtonBar = () => m(".button-bar", + m(NestedButton, + {style: "width:10px; height:10px; border:1px solid #FFF;"}, + "Normal CSS" + ), + m(NestedButton, + {style: "top:0 ; right: 20"}, + "Poor CSS" + ), + m(NestedButton, + {style: "invalid-prop:1;padding:1px;font:12px/1.1 arial,sans-serif;", icon: true}, + "Poorer CSS" + ), + m(NestedButton, + {style: {margin: 0, padding: "10px", overflow: "visible"}}, + "Object CSS" + ) +) + +var NestedButton = (attrs) => m("button", attrs) + +var NestedMain = () => m(NestedForm) + +var NestedRoot = () => m("div.foo.bar[data-foo=bar]", + {p: 2}, + m(NestedHeader), + m(NestedMain) +) + +return () => m(NestedRoot) +})() + +suite.add("construct simple tree", { + fn: function () { + simpleTree() + }, +}) + +suite.add("mount simple tree", { + fn: function () { + m.mount(rootElem, simpleTree) + }, +}) + +suite.add("redraw simple tree", { + setup: function () { + m.mount(rootElem, simpleTree) + }, + fn: function () { + m.redrawSync() + }, +}) + +suite.add("mount large nested tree", { + fn: function () { + m.mount(rootElem, nestedTree) + }, +}) + +suite.add("redraw large nested tree", { + setup: function () { + m.mount(rootElem, nestedTree) + }, + fn: function () { + m.redrawSync() + }, +}) + +suite.add("mutate styles/properties", { + setup: function () { + function get(obj, i) { return obj[i % obj.length] } + var counter = 0 + var classes = ["foo", "foo bar", "", "baz-bat", null, "fooga", null, null, undefined] + var styles = [] + var multivalue = ["0 1px", "0 0 1px 0", "0", "1px", "20px 10px", "7em 5px", "1px 0 5em 2px"] + var stylekeys = [ + ["left", function (c) { return c % 3 ? c + "px" : c }], + ["top", function (c) { return c % 2 ? c + "px" : c }], + ["margin", function (c) { return get(multivalue, c).replace("1px", c+"px") }], + ["padding", function (c) { return get(multivalue, c) }], + ["position", function (c) { return c%5 ? c%2 ? "absolute" : "relative" : null }], + ["display", function (c) { return c%10 ? c%2 ? "block" : "inline" : "none" }], + ["color", function (c) { return ("rgba(" + (c%255) + ", " + (255 - c%255) + ", " + (50+c%150) + ", " + (c%50/50) + ")") }], + ["border", function (c) { return c%5 ? (c%10) + "px " + (c%2?"solid":"dotted") + " " + stylekeys[6][1](c) : "" }] + ] + var i, j, style, conf + + for (i=0; i<1000; i++) { + style = {} + for (j=0; j m("header", + m("h1.asdf", "a ", "b", " c ", 0, " d"), + m("nav", + m("a", {href: "/foo"}, "Foo"), + m("a", {href: "/bar"}, "Bar") + ) + ) + + var RepeatedForm = () => m("form", {onSubmit: function () {}}, + m("input", {type: "checkbox", checked: true}), + m("input", {type: "checkbox", checked: false}), + m("fieldset", + m("label", + m("input", {type: "radio", checked: true}) + ), + m("label", + m("input", {type: "radio"}) + ) + ), + m(RepeatedButtonBar, null) + ) + + var RepeatedButtonBar = () => m(".button-bar", + m(RepeatedButton, + {style: "width:10px; height:10px; border:1px solid #FFF;"}, + "Normal CSS" + ), + m(RepeatedButton, + {style: "top:0 ; right: 20"}, + "Poor CSS" + ), + m(RepeatedButton, + {style: "invalid-prop:1;padding:1px;font:12px/1.1 arial,sans-serif;", icon: true}, + "Poorer CSS" + ), + m(RepeatedButton, + {style: {margin: 0, padding: "10px", overflow: "visible"}}, + "Object CSS" + ) + ) + + var RepeatedButton = (attrs) => m("button", attrs) + + var RepeatedMain = () => m(RepeatedForm) + + this.RepeatedRoot = () => m("div.foo.bar[data-foo=bar]", + {p: 2}, + m(RepeatedHeader, null), + m(RepeatedMain, null) + ) + }, + fn: function () { + m.render(rootElem, [m(this.RepeatedRoot)]) + m.render(rootElem, []) + }, +}) + +suite.add("reorder keyed list", { + setup: function () { + const keys = [] + for (let i = 0; i < 1000; i++) keys.push(`key-${i}`) + + function shuffle() { + // Performs a simple Fisher-Yates shuffle. + let current = keys.length + while (current) { + // eslint-disable-next-line no-bitwise + const index = (Math.random() * current--) | 0 + const temp = keys[index] + keys[index] = keys[current] + keys[current] = temp + } + } + + this.app = function () { + shuffle() + var vnodes = [] + for (const key of keys) { + vnodes.push(m("div.item", {key})) + } + return vnodes + } + }, + fn: function () { + m.render(rootElem, this.app()) + }, +}) + +if (isDOM) { + window.onload = function () { + cycleRoot() + suite.run() + } +} else { + cycleRoot() + suite.run() +} diff --git a/performance/test-perf.js b/performance/test-perf.js index d9bc1661d..404284410 100644 --- a/performance/test-perf.js +++ b/performance/test-perf.js @@ -1,5 +1,3 @@ -"use strict" - /* Based off of preact's perf tests, so including their MIT license */ /* The MIT License (MIT) @@ -26,388 +24,12 @@ SOFTWARE. */ // Note: this tests against the generated bundle in browsers, but it tests -// against `index.js` in Node. Please do keep that in mind while testing. +// against `browser.js` in Node. Please do keep that in mind while testing. // // Mithril.js and Benchmark.js are loaded globally via bundle in the browser, so // this doesn't require a CommonJS sham polyfill. -// I add it globally just so it's visible in the tests. -/* global m, window, document, rootElem: true, simpleTree: false, nestedTree: false */ - // set up browser env on before running tests -var isDOM = typeof window !== "undefined" -// eslint-disable-next-line no-undef -var globalObject = typeof globalThis !== "undefined" ? globalThis : isDOM ? window : global -var Benchmark - -if (isDOM) { - Benchmark = window.Benchmark - window.rootElem = null -} else { - /* eslint-disable global-require */ - global.window = require("../test-utils/browserMock")() - global.document = window.document - // We're benchmarking renders, not our throttling. - global.requestAnimationFrame = function () { - throw new Error("This should never be called.") - } - global.m = require("../index.js") - global.rootElem = null - Benchmark = require("benchmark") - /* eslint-enable global-require */ -} - -function cycleRoot() { - if (rootElem) document.body.removeChild(rootElem) - document.body.appendChild(rootElem = document.createElement("div")) -} - -// Initialize benchmark suite -Benchmark.options.async = true -Benchmark.options.initCount = 10 -Benchmark.options.minSamples = 40 - -if (isDOM) { - // Wait long enough for the browser to actually commit the DOM changes to - // the screen before moving on to the next cycle, so things are at least - // reasonably fresh each cycle. - Benchmark.options.delay = 1 / 30 /* frames per second */ -} - -var suite = new Benchmark.Suite("Mithril.js perf", { - onStart: function () { - this.start = Date.now() - }, - - onCycle: function (e) { - console.log(e.target.toString()) - cycleRoot() - }, - - onComplete: function () { - console.log("Completed perf tests in " + (Date.now() - this.start) + "ms") - }, - - onError: function (e) { - console.error(e) - }, -}) -// eslint-disable-next-line no-unused-vars -var xsuite = {add: function(name) { console.log("skipping " + name) }} - -globalObject.simpleTree = () => m(".foo.bar[data-foo=bar]", {p: 2}, - m("header", - m("h1.asdf", "a ", "b", " c ", 0, " d"), - m("nav", - m("a[href=/foo]", "Foo"), - m("a[href=/bar]", "Bar") - ) - ), - m("main", - m("form", - {onSubmit: function () {}}, - m("input[type=checkbox][checked]"), - m("input[type=checkbox]"), - m("fieldset", - this.fields.map(function (field) { - return m("label", - field, - ":", - m("input", {placeholder: field}) - ) - }) - ), - m("button-bar", - m("button", - {style: "width:10px; height:10px; border:1px solid #FFF;"}, - "Normal CSS" - ), - m("button", - {style: "top:0 ; right: 20"}, - "Poor CSS" - ), - m("button", - {style: "invalid-prop:1;padding:1px;font:12px/1.1 arial,sans-serif;", icon: true}, - "Poorer CSS" - ), - m("button", - {style: {margin: 0, padding: "10px", overflow: "visible"}}, - "Object CSS" - ) - ) - ) - ) -) - -globalObject.nestedTree = (() => { -// Result of `JSON.stringify(Array.from({length:100},(_,i)=>((i+1)*999).toString(36)))` -var fields = [ - "rr", "1ji", "2b9", "330", "3ur", "4mi", "5e9", "660", "6xr", "7pi", - "8h9", "990", "a0r", "asi", "bk9", "cc0", "d3r", "dvi", "en9", "ff0", - "g6r", "gyi", "hq9", "ii0", "j9r", "k1i", "kt9", "ll0", "mcr", "n4i", - "nw9", "oo0", "pfr", "q7i", "qz9", "rr0", "sir", "tai", "u29", "uu0", - "vlr", "wdi", "x59", "xx0", "yor", "zgi", "1089", "1100", "11rr", "12ji", - "13b9", "1430", "14ur", "15mi", "16e9", "1760", "17xr", "18pi", "19h9", "1a90", - "1b0r", "1bsi", "1ck9", "1dc0", "1e3r", "1evi", "1fn9", "1gf0", "1h6r", "1hyi", - "1iq9", "1ji0", "1k9r", "1l1i", "1lt9", "1ml0", "1ncr", "1o4i", "1ow9", "1po0", - "1qfr", "1r7i", "1rz9", "1sr0", "1tir", "1uai", "1v29", "1vu0", "1wlr", "1xdi", - "1y59", "1yx0", "1zor", "20gi", "2189", "2200", "22rr", "23ji", "24b9", "2530", -] - -var NestedHeader = () => m("header", - m("h1.asdf", "a ", "b", " c ", 0, " d"), - m("nav", - m("a", {href: "/foo"}, "Foo"), - m("a", {href: "/bar"}, "Bar") - ) -) - -var NestedForm = () => m("form", {onSubmit: function () {}}, - m("input[type=checkbox][checked]"), - m("input[type=checkbox]", {checked: false}), - m("fieldset", - m("label", - m("input[type=radio][checked]") - ), - m("label", - m("input[type=radio]") - ) - ), - m("fieldset", - fields.map(function (field) { - return m("label", - field, - ":", - m("input", {placeholder: field}) - ) - }) - ), - m(NestedButtonBar, null) -) - -var NestedButtonBar = () => m(".button-bar", - m(NestedButton, - {style: "width:10px; height:10px; border:1px solid #FFF;"}, - "Normal CSS" - ), - m(NestedButton, - {style: "top:0 ; right: 20"}, - "Poor CSS" - ), - m(NestedButton, - {style: "invalid-prop:1;padding:1px;font:12px/1.1 arial,sans-serif;", icon: true}, - "Poorer CSS" - ), - m(NestedButton, - {style: {margin: 0, padding: "10px", overflow: "visible"}}, - "Object CSS" - ) -) - -var NestedButton = (attrs) => m("button", attrs) - -var NestedMain = () => m(NestedForm) - -var NestedRoot = () => m("div.foo.bar[data-foo=bar]", - {p: 2}, - m(NestedHeader), - m(NestedMain) -) - -return () => m(NestedRoot) -})() - -suite.add("construct simple tree", { - fn: function () { - simpleTree() - }, -}) - -suite.add("mount simple tree", { - fn: function () { - m.mount(rootElem, simpleTree) - }, -}) - -suite.add("redraw simple tree", { - setup: function () { - m.mount(rootElem, simpleTree) - }, - fn: function () { - m.redraw.sync() - }, -}) - -suite.add("mount large nested tree", { - fn: function () { - m.mount(rootElem, nestedTree) - }, -}) - -suite.add("redraw large nested tree", { - setup: function () { - m.mount(rootElem, nestedTree) - }, - fn: function () { - m.redraw.sync() - }, -}) - -suite.add("mutate styles/properties", { - setup: function () { - function get(obj, i) { return obj[i % obj.length] } - var counter = 0 - var classes = ["foo", "foo bar", "", "baz-bat", null, "fooga", null, null, undefined] - var styles = [] - var multivalue = ["0 1px", "0 0 1px 0", "0", "1px", "20px 10px", "7em 5px", "1px 0 5em 2px"] - var stylekeys = [ - ["left", function (c) { return c % 3 ? c + "px" : c }], - ["top", function (c) { return c % 2 ? c + "px" : c }], - ["margin", function (c) { return get(multivalue, c).replace("1px", c+"px") }], - ["padding", function (c) { return get(multivalue, c) }], - ["position", function (c) { return c%5 ? c%2 ? "absolute" : "relative" : null }], - ["display", function (c) { return c%10 ? c%2 ? "block" : "inline" : "none" }], - ["color", function (c) { return ("rgba(" + (c%255) + ", " + (255 - c%255) + ", " + (50+c%150) + ", " + (c%50/50) + ")") }], - ["border", function (c) { return c%5 ? (c%10) + "px " + (c%2?"solid":"dotted") + " " + stylekeys[6][1](c) : "" }] - ] - var i, j, style, conf - - for (i=0; i<1000; i++) { - style = {} - for (j=0; j m("header", - m("h1.asdf", "a ", "b", " c ", 0, " d"), - m("nav", - m("a", {href: "/foo"}, "Foo"), - m("a", {href: "/bar"}, "Bar") - ) - ) - - var RepeatedForm = () => m("form", {onSubmit: function () {}}, - m("input", {type: "checkbox", checked: true}), - m("input", {type: "checkbox", checked: false}), - m("fieldset", - m("label", - m("input", {type: "radio", checked: true}) - ), - m("label", - m("input", {type: "radio"}) - ) - ), - m(RepeatedButtonBar, null) - ) - - var RepeatedButtonBar = () => m(".button-bar", - m(RepeatedButton, - {style: "width:10px; height:10px; border:1px solid #FFF;"}, - "Normal CSS" - ), - m(RepeatedButton, - {style: "top:0 ; right: 20"}, - "Poor CSS" - ), - m(RepeatedButton, - {style: "invalid-prop:1;padding:1px;font:12px/1.1 arial,sans-serif;", icon: true}, - "Poorer CSS" - ), - m(RepeatedButton, - {style: {margin: 0, padding: "10px", overflow: "visible"}}, - "Object CSS" - ) - ) - - var RepeatedButton = (attrs) => m("button", attrs) - - var RepeatedMain = () => m(RepeatedForm) - - this.RepeatedRoot = () => m("div.foo.bar[data-foo=bar]", - {p: 2}, - m(RepeatedHeader, null), - m(RepeatedMain, null) - ) - }, - fn: function () { - m.render(rootElem, [m(this.RepeatedRoot)]) - m.render(rootElem, []) - }, -}) - -suite.add("reorder keyed list", { - setup: function () { - const keys = [] - for (let i = 0; i < 1000; i++) keys.push(`key-${i}`) - - function shuffle() { - // Performs a simple Fisher-Yates shuffle. - let current = keys.length - while (current) { - // eslint-disable-next-line no-bitwise - const index = (Math.random() * current--) | 0 - const temp = keys[index] - keys[index] = keys[current] - keys[current] = temp - } - } - - this.app = function () { - shuffle() - var vnodes = [] - for (const key of keys) { - vnodes.push(m("div.item", {key})) - } - return vnodes - } - }, - fn: function () { - m.render(rootElem, this.app()) - }, -}) +import "./inject-mock-globals.js" -if (isDOM) { - window.onload = function () { - cycleRoot() - suite.run() - } -} else { - cycleRoot() - suite.run() -} +import "./test-perf-impl.js" diff --git a/scripts/.eslintrc.js b/scripts/.eslintrc.js deleted file mode 100644 index 9e7393319..000000000 --- a/scripts/.eslintrc.js +++ /dev/null @@ -1,16 +0,0 @@ -"use strict" - -module.exports = { - "extends": "../.eslintrc.js", - "env": { - "browser": null, - "node": true, - "es2022": true, - }, - "parserOptions": { - "ecmaVersion": 2022, - }, - "rules": { - "no-process-env": "off", - }, -}; diff --git a/scripts/_bundler-impl.js b/scripts/_bundler-impl.js deleted file mode 100644 index 279a59663..000000000 --- a/scripts/_bundler-impl.js +++ /dev/null @@ -1,183 +0,0 @@ -"use strict" - -const fs = require("fs") -const path = require("path") -const execFileSync = require("child_process").execFileSync -const util = require("util") - -const readFile = util.promisify(fs.readFile) -const access = util.promisify(fs.access) - -function isFile(filepath) { - return access(filepath).then(() => true, () => false) -} -function escapeRegExp(string) { - return string.replace(/[|\\{}()[\]^$+*?.-]/g, "\\$&") -} -function escapeReplace(string) { - return string.replace(/\$/g, "\\$&") -} - -async function resolve(filepath, filename) { - if (filename[0] !== ".") { - // resolve as npm dependency - const packagePath = `./node_modules/${filename}/package.json` - let json, meta - - try { - json = await readFile(packagePath, "utf8") - } catch (e) { - meta = {} - } - - if (json) { - try { - meta = JSON.parse(json) - } - catch (e) { - throw new Error(`invalid JSON for ${packagePath}: ${json}`) - } - } - - const main = `./node_modules/${filename}/${meta.main || `${filename}.js`}` - return path.resolve(await isFile(main) ? main : `./node_modules/${filename}/index.js`) - } - else { - // resolve as local dependency - return path.resolve(path.dirname(filepath), filename + ".js") - } -} - -function matchAll(str, regexp) { - regexp.lastIndex = 0 - const result = [] - let exec - while ((exec = regexp.exec(str)) != null) result.push(exec) - return result -} - -let error -module.exports = async (input) => { - const modules = new Map() - const bindings = new Map() - const declaration = /^\s*(?:var|let|const|function)[\t ]+([\w_$]+)/gm - const include = /(?:((?:var|let|const|,|)[\t ]*)([\w_$\.\[\]"'`]+)(\s*=\s*))?require\(([^\)]+)\)(\s*[`\.\(\[])?/gm - let uuid = 0 - async function process(filepath, data) { - for (const [, binding] of matchAll(data, declaration)) bindings.set(binding, 0) - - const tasks = [] - - for (const [, def = "", variable = "", eq = "", dep, rest = ""] of matchAll(data, include)) { - tasks.push({filename: JSON.parse(dep), def, variable, eq, rest}) - } - - const imports = await Promise.all( - tasks.map((t) => resolve(filepath, t.filename)) - ) - - const results = [] - for (const [i, task] of tasks.entries()) { - const dependency = imports[i] - let pre = "", def = task.def - if (def[0] === ",") def = "\nvar ", pre = "\n" - const localUUID = uuid // global uuid can update from nested `process` call, ensure same id is used on declaration and consumption - const existingModule = modules.get(dependency) - modules.set(dependency, task.rest ? `_${localUUID}` : task.variable) - const code = await process( - dependency, - pre + ( - existingModule == null - ? await exportCode(task.filename, dependency, def, task.variable, task.eq, task.rest, localUUID) - : def + task.variable + task.eq + existingModule - ) - ) - uuid++ - results.push(code + task.rest) - } - - let i = 0 - return data.replace(include, () => results[i++]) - } - - async function exportCode(filename, filepath, def, variable, eq, rest, uuid) { - let code = await readFile(filepath, "utf-8") - // if there's a syntax error, report w/ proper stack trace - try { - new Function(code) - } - catch (e) { - try { - execFileSync("node", ["--check", filepath], { - stdio: "pipe", - }) - } - catch (e) { - if (e.message !== error) { - error = e.message - console.log(`\x1b[31m${e.message}\x1b[0m`) - } - } - } - - // disambiguate collisions - const targetPromises = [] - code.replace(include, (match, def, variable, eq, dep) => { - targetPromises.push(resolve(filepath, JSON.parse(dep))) - }) - - const ignoredTargets = await Promise.all(targetPromises) - const ignored = new Set() - - for (const target of ignoredTargets) { - const binding = modules.get(target) - if (binding != null) ignored.add(binding) - } - - if (new RegExp(`module\\.exports\\s*=\\s*${variable}\s*$`, "m").test(code)) ignored.add(variable) - for (const [binding, count] of bindings) { - if (!ignored.has(binding)) { - const before = code - code = code.replace( - new RegExp(`(\\b)${escapeRegExp(binding)}\\b`, "g"), - escapeReplace(binding) + count - ) - if (before !== code) bindings.set(binding, count + 1) - } - } - - // fix strings that got mangled by collision disambiguation - const string = /(["'])((?:\\\1|.)*?)(\1)/g - const candidates = Array.from(bindings, ([binding, count]) => escapeRegExp(binding) + (count - 1)).join("|") - const variables = new RegExp(candidates, "g") - code = code.replace(string, (match, open, data, close) => { - const fixed = data.replace(variables, (match) => match.replace(/\d+$/, "")) - return open + fixed + close - }) - - //fix props - const props = new RegExp(`((?:[^:]\\/\\/.*)?\\.\\s*)(${candidates})|([\\{,]\\s*)(${candidates})(\\s*:)`, "gm") - code = code.replace(props, (match, dot, a, pre, b, post) => { - // Don't do anything because dot was matched in a comment - if (dot && dot.indexOf("//") === 1) return match - if (dot) return dot + a.replace(/\d+$/, "") - return pre + b.replace(/\d+$/, "") + post - }) - - return code - .replace(/("|')use strict\1;?/gm, "") // remove extraneous "use strict" - .replace(/module\.exports\s*=\s*/gm, escapeReplace(rest ? `var _${uuid}` + eq : def + (rest ? "_" : "") + variable + eq)) // export - + (rest ? `\n${def}${variable}${eq}_${uuid}` : "") // if `rest` is truthy, it means the expression is fluent or higher-order (e.g. require(path).foo or require(path)(foo) - } - - const code = ";(()=>{\n" + - (await process(path.resolve(input), await readFile(input, "utf-8"))) - .replace(/^\s*((?:var|let|const|)[\t ]*)([\w_$\.]+)(\s*=\s*)(\2)(?=[\s]+(\w)|;|$)/gm, "") // remove assignments to self - .replace(/;+(\r|\n|$)/g, ";$1") // remove redundant semicolons - .replace(/(\r|\n)+/g, "\n").replace(/(\r|\n)$/, "") + // remove multiline breaks - "\n})();" - - //try {new Function(code); console.log(`build completed at ${new Date()}`)} catch (e) {} - error = null - return code -} diff --git a/scripts/build.js b/scripts/build.js new file mode 100644 index 000000000..b151bb51a --- /dev/null +++ b/scripts/build.js @@ -0,0 +1,80 @@ +import {fileURLToPath} from "node:url" +import fs from "node:fs/promises" +import {gzipSync} from "node:zlib" +import path from "node:path" + +import {rollup} from "rollup" + +import terser from "@rollup/plugin-terser" + +const dirname = path.dirname(fileURLToPath(import.meta.url)) + +/** @type {{[key: import("rollup").ModuleFormat]: import("rollup").Plugin}} */ +const terserPlugin = terser({ + compress: {passes: 3}, +}) + +function format(n) { + return n.toString().replace(/(\d)(?=(\d\d\d)+(?!\d))/g, "$1,") +} + +/** @param {import("rollup").ModuleFormat} format */ +async function build(name, format) { + const bundle = await rollup({input: path.resolve(dirname, `../src/entry/${name}`)}) + + try { + await Promise.all([ + bundle.write({file: path.resolve(dirname, `../dist/${name}.js`), format}), + bundle.write({file: path.resolve(dirname, `../dist/${name}.min.js`), format, plugins: [terserPlugin]}), + ]) + } finally { + await bundle.close() + } +} + +async function report(file) { + const [original, minified] = await Promise.all([ + fs.readFile(path.resolve(dirname, `../dist/${file}.js`)), + fs.readFile(path.resolve(dirname, `../dist/${file}.min.js`)), + ]) + const originalSize = original.length + const compressedSize = minified.length + const originalGzipSize = gzipSync(original).length + const compressedGzipSize = gzipSync(minified).length + + console.log(`${file}.js:`) + console.log(` Original: ${format(originalGzipSize)} bytes gzipped (${format(originalSize)} bytes uncompressed)`) + console.log(` Minified: ${format(compressedGzipSize)} bytes gzipped (${format(compressedSize)} bytes uncompressed)`) + + return compressedGzipSize +} + +async function saveToReadme(size) { + const readme = await fs.readFile(path.resolve(dirname, "../README.md"), "utf8") + const kb = size / 1000 + + await fs.writeFile(path.resolve(dirname, "../README.md"), + readme.replace( + /()(.+?)()/, + `\$1${kb % 1 ? kb.toFixed(2) : kb} KB\$3` + ) + ) +} + +async function main() { + await Promise.all([ + build("mithril.umd", "iife"), + build("mithril.esm", "esm"), + build("stream.umd", "iife"), + build("stream.esm", "esm"), + ]) + + const mithrilSize = await report("mithril.umd") + await report("mithril.esm") + await report("stream.umd") + await report("stream.esm") + + if (process.argv.includes("--save", 2)) await saveToReadme(mithrilSize) +} + +main() diff --git a/scripts/bundler-readme.md b/scripts/bundler-readme.md deleted file mode 100644 index 949d1ac9c..000000000 --- a/scripts/bundler-readme.md +++ /dev/null @@ -1,22 +0,0 @@ -# bundler.js - -Simplistic CommonJS module bundler - -Version: 0.1 -License: MIT - -## About - -This bundler attempts to aggressively bundle CommonJS modules by assuming the dependency tree is static, similar to what Rollup does for ES6 modules. - -Most browsers don't support ES6 `import/export` syntax, but we can achieve modularity by using CommonJS module syntax and transpiling it. - -Webpack is conservative and treats CommonJS modules as non-statically-analyzable since `require` and `module.exports` are legally allowed everywhere. Therefore, it must generate extra code to resolve dependencies at runtime (i.e. `__webpack_require()`). Rollup only works with ES6 modules. ES6 modules can be bundled more efficiently because they are statically analyzable, but some use cases are difficult to handle due to ES6's support for cyclic dependencies and hosting rules. This bundler assumes code is written in CommonJS style but follows a strict set of rules that emulate statically analyzable code and favors the usage of the factory pattern instead of relying on obscure corners of the JavaScript language (hoisting rules and binding semantics). - -### Caveats - -- Only supports modules that have the `require` and `module.exports` statement declared at the top-level scope before all other code, i.e. it does not support CommonJS modules that rely on dynamic importing/exporting. This means modules should only export a pure function or export a factory function if there are multiple statements and/or internal module state. The factory function pattern allows easier dependency injection in stateful modules, thus making modules testable. -- Changes the semantics of value/binding exporting between unbundled and bundled code, and therefore relying on those semantics is discouraged. -- Top level strictness is infectious (i.e. if entry file is in `"use strict"` mode, all modules inherit strict mode, and conversely, if the entry file is not in strict mode, all modules are pulled out of strict mode) -- Currently only supports assignments to `module.exports` (i.e. `module.exports.foo = bar` will not work) -- It is tiny and dependency-free because it uses regular expressions, and it only supports the narrow range of import/export declaration patterns outlined above. diff --git a/scripts/bundler.js b/scripts/bundler.js deleted file mode 100644 index 2c29a1914..000000000 --- a/scripts/bundler.js +++ /dev/null @@ -1,70 +0,0 @@ -"use strict" - -const fs = require("fs") -const zlib = require("zlib") -const chokidar = require("chokidar") -const Terser = require("terser") -const util = require("util") - -const readFile = util.promisify(fs.readFile) -const writeFile = util.promisify(fs.writeFile) -const gzip = util.promisify(zlib.gzip) - -const bundle = require("./_bundler-impl") - -const aliases = {o: "output", m: "minify", w: "watch", s: "save"} -const params = Object.create(null) -let command -for (let arg of process.argv.slice(2)) { - if (arg[0] === '"') arg = JSON.parse(arg) - if (arg[0] === "-") { - if (command != null) add(true) - command = arg.replace(/\-+/g, "") - } - else if (command != null) add(arg) - else params.input = arg -} -if (command != null) add(true) - -function add(value) { - params[aliases[command] || command] = value - command = null -} - -function format(n) { - return n.toString().replace(/(\d)(?=(\d\d\d)+(?!\d))/g, "$1,") -} - -async function build() { - const original = await bundle(params.input) - if (!params.minify) { - await writeFile(params.output, original, "utf-8") - return - } - console.log("minifying...") - const minified = Terser.minify(original, {ecma: 2015}) - if (minified.error) throw new Error(minified.error) - await writeFile(params.output, minified.code, "utf-8") - const originalSize = Buffer.byteLength(original, "utf-8") - const compressedSize = Buffer.byteLength(minified.code, "utf-8") - const originalGzipSize = (await gzip(original)).byteLength - const compressedGzipSize = (await gzip(minified.code)).byteLength - - console.log("Original size: " + format(originalGzipSize) + " bytes gzipped (" + format(originalSize) + " bytes uncompressed)") - console.log("Compiled size: " + format(compressedGzipSize) + " bytes gzipped (" + format(compressedSize) + " bytes uncompressed)") - - if (params.save) { - const readme = await readFile("./README.md", "utf8") - const kb = compressedGzipSize / 1000 - - await writeFile("./README.md", - readme.replace( - /()(.+?)()/, - "$1" + (kb % 1 ? kb.toFixed(2) : kb) + " KB$3" - ) - ) - } -} - -build() -if (params.watch) chokidar.watch(".", {ignored: params.output}).on("all", build) diff --git a/scripts/minify-stream.js b/scripts/minify-stream.js deleted file mode 100644 index de62a3219..000000000 --- a/scripts/minify-stream.js +++ /dev/null @@ -1,50 +0,0 @@ -#!/usr/bin/env node -/* eslint-disable no-process-exit */ -"use strict" - -process.on("unhandledRejection", (e) => { - process.exitCode = 1 - - if (!e.stdout || !e.stderr) throw e - - console.error(e.stack) - - if (e.stdout?.length) { - console.error(e.stdout.toString("utf-8")) - } - - if (e.stderr?.length) { - console.error(e.stderr.toString("utf-8")) - } - - // eslint-disable-next-line no-process-exit - process.exit() -}) - -const {promises: fs} = require("fs") -const path = require("path") -const zlib = require("zlib") -const Terser = require("terser") - -function format(n) { - return n.toString().replace(/(\d)(?=(\d\d\d)+(?!\d))/g, "$1,") -} - -module.exports = minify -async function minify() { - const input = path.resolve(__dirname, "../stream/stream.js") - const output = path.resolve(__dirname, "../stream/stream.min.js") - const original = await fs.readFile(input, "utf-8") - const minified = Terser.minify(original, {ecma: 2015}) - if (minified.error) throw new Error(minified.error) - await fs.writeFile(output, minified.code, "utf-8") - const originalSize = Buffer.byteLength(original, "utf-8") - const compressedSize = Buffer.byteLength(minified.code, "utf-8") - const originalGzipSize = zlib.gzipSync(original).byteLength - const compressedGzipSize = zlib.gzipSync(minified.code).byteLength - - console.log("Original size: " + format(originalGzipSize) + " bytes gzipped (" + format(originalSize) + " bytes uncompressed)") - console.log("Compiled size: " + format(compressedGzipSize) + " bytes gzipped (" + format(compressedSize) + " bytes uncompressed)") -} - -minify() diff --git a/scripts/tests/test-bundler.js b/scripts/tests/test-bundler.js deleted file mode 100644 index efcb9f5af..000000000 --- a/scripts/tests/test-bundler.js +++ /dev/null @@ -1,296 +0,0 @@ -"use strict" - -const fs = require("fs") -const util = require("util") -const path = require("path") -const access = util.promisify(fs.access) -const writeFile = util.promisify(fs.writeFile) -const unlink = util.promisify(fs.unlink) - -const o = require("ospec") -const bundle = require("../_bundler-impl") - -o.spec("bundler", async () => { - let filesCreated - const root = path.resolve(__dirname, "../..") - const p = (file) => path.join(root, file) - - async function write(filepath, data) { - try { - await access(p(filepath)) - } catch (e) { - return writeFile(p(filepath), data, "utf8") - } - throw new Error(`Don't call \`write('${filepath}')\`. Cannot overwrite file.`) - } - - function setup(files) { - filesCreated = Object.keys(files) - return Promise.all(filesCreated.map((f) => write(f, files[f]))) - } - - o.afterEach(() => Promise.all( - filesCreated.map((filepath) => unlink(p(filepath))) - )) - - o("relative imports works", async () => { - await setup({ - "a.js": 'var b = require("./b")', - "b.js": "module.exports = 1", - }) - - o(await bundle(p("a.js"))).equals(";(()=>{\nvar b = 1\n})();") - }) - o("relative imports works with semicolons", async () => { - await setup({ - "a.js": 'var b = require("./b");', - "b.js": "module.exports = 1;", - }) - - o(await bundle(p("a.js"))).equals(";(()=>{\nvar b = 1;\n})();") - }) - o("relative imports works with let", async () => { - await setup({ - "a.js": 'let b = require("./b")', - "b.js": "module.exports = 1", - }) - - o(await bundle(p("a.js"))).equals(";(()=>{\nlet b = 1\n})();") - }) - o("relative imports works with const", async () => { - await setup({ - "a.js": 'const b = require("./b")', - "b.js": "module.exports = 1", - }) - - o(await bundle(p("a.js"))).equals(";(()=>{\nconst b = 1\n})();") - }) - o("relative imports works with assignment", async () => { - await setup({ - "a.js": 'var a = {}\na.b = require("./b")', - "b.js": "module.exports = 1", - }) - - o(await bundle(p("a.js"))).equals(";(()=>{\nvar a = {}\na.b = 1\n})();") - }) - o("relative imports works with reassignment", async () => { - await setup({ - "a.js": 'var b = {}\nb = require("./b")', - "b.js": "module.exports = 1", - }) - - o(await bundle(p("a.js"))).equals(";(()=>{\nvar b = {}\nb = 1\n})();") - }) - o("relative imports removes extra use strict", async () => { - await setup({ - "a.js": '"use strict"\nvar b = require("./b")', - "b.js": '"use strict"\nmodule.exports = 1', - }) - - o(await bundle(p("a.js"))).equals(';(()=>{\n"use strict"\nvar b = 1\n})();') - }) - o("relative imports removes extra use strict using single quotes", async () => { - await setup({ - "a.js": "'use strict'\nvar b = require(\"./b\")", - "b.js": "'use strict'\nmodule.exports = 1", - }) - - o(await bundle(p("a.js"))).equals(";(()=>{\n'use strict'\nvar b = 1\n})();") - }) - o("relative imports removes extra use strict using mixed quotes", async () => { - await setup({ - "a.js": '"use strict"\nvar b = require("./b")', - "b.js": "'use strict'\nmodule.exports = 1", - }) - - o(await bundle(p("a.js"))).equals(';(()=>{\n"use strict"\nvar b = 1\n})();') - }) - o("works w/ window", async () => { - await setup({ - "a.js": 'window.a = 1\nvar b = require("./b")', - "b.js": "module.exports = function() {return a}", - }) - - o(await bundle(p("a.js"))).equals(";(()=>{\nwindow.a = 1\nvar b = function() {return a}\n})();") - }) - o("works without assignment", async () => { - await setup({ - "a.js": 'require("./b")', - "b.js": "1 + 1", - }) - - o(await bundle(p("a.js"))).equals(";(()=>{\n1 + 1\n})();") - }) - o("works if used fluently", async () => { - await setup({ - "a.js": 'var b = require("./b").toString()', - "b.js": "module.exports = []", - }) - - o(await bundle(p("a.js"))).equals(";(()=>{\nvar _0 = []\nvar b = _0.toString()\n})();") - }) - o("works if used fluently w/ multiline", async () => { - await setup({ - "a.js": 'var b = require("./b")\n\t.toString()', - "b.js": "module.exports = []", - }) - - o(await bundle(p("a.js"))).equals(";(()=>{\nvar _0 = []\nvar b = _0\n\t.toString()\n})();") - }) - o("works if used w/ curry", async () => { - await setup({ - "a.js": 'var b = require("./b")()', - "b.js": "module.exports = function() {}", - }) - - o(await bundle(p("a.js"))).equals(";(()=>{\nvar _0 = function() {}\nvar b = _0()\n})();") - }) - o("works if used w/ curry w/ multiline", async () => { - await setup({ - "a.js": 'var b = require("./b")\n()', - "b.js": "module.exports = function() {}", - }) - - o(await bundle(p("a.js"))).equals(";(()=>{\nvar _0 = function() {}\nvar b = _0\n()\n})();") - }) - o("works if used fluently in one place and not in another", async () => { - await setup({ - "a.js": 'var b = require("./b").toString()\nvar c = require("./c")', - "b.js": "module.exports = []", - "c.js": 'var b = require("./b")\nmodule.exports = function() {return b}', - }) - - o(await bundle(p("a.js"))).equals(";(()=>{\nvar _0 = []\nvar b = _0.toString()\nvar b0 = _0\nvar c = function() {return b0}\n})();") - }) - o("works if used in sequence", async () => { - await setup({ - "a.js": 'var b = require("./b"), c = require("./c")', - "b.js": "module.exports = 1", - "c.js": "var x\nmodule.exports = 2", - }) - - o(await bundle(p("a.js"))).equals(";(()=>{\nvar b = 1\nvar x\nvar c = 2\n})();") - }) - o("works if assigned to property", async () => { - await setup({ - "a.js": 'var x = {}\nx.b = require("./b")\nx.c = require("./c")', - "b.js": "var bb = 1\nmodule.exports = bb", - "c.js": "var cc = 2\nmodule.exports = cc", - }) - - o(await bundle(p("a.js"))).equals(";(()=>{\nvar x = {}\nvar bb = 1\nx.b = bb\nvar cc = 2\nx.c = cc\n})();") - }) - o("works if assigned to property using bracket notation", async () => { - await setup({ - "a.js": 'var x = {}\nx["b"] = require("./b")\nx["c"] = require("./c")', - "b.js": "var bb = 1\nmodule.exports = bb", - "c.js": "var cc = 2\nmodule.exports = cc", - }) - - o(await bundle(p("a.js"))).equals(';(()=>{\nvar x = {}\nvar bb = 1\nx["b"] = bb\nvar cc = 2\nx["c"] = cc\n})();') - }) - o("works if collision", async () => { - await setup({ - "a.js": 'var b = require("./b")', - "b.js": "var b = 1\nmodule.exports = 2", - }) - - o(await bundle(p("a.js"))).equals(";(()=>{\nvar b0 = 1\nvar b = 2\n})();") - }) - o("works if multiple aliases", async () => { - await setup({ - "a.js": 'var b = require("./b")\n', - "b.js": 'var b = require("./c")\nb.x = 1\nmodule.exports = b', - "c.js": "var b = {}\nmodule.exports = b", - }) - - o(await bundle(p("a.js"))).equals(";(()=>{\nvar b = {}\nb.x = 1\n})();") - }) - o("works if multiple collision", async () => { - await setup({ - "a.js": 'var b = require("./b")\nvar c = require("./c")\nvar d = require("./d")', - "b.js": "var a = 1\nmodule.exports = a", - "c.js": "var a = 2\nmodule.exports = a", - "d.js": "var a = 3\nmodule.exports = a", - }) - - o(await bundle(p("a.js"))).equals(";(()=>{\nvar a = 1\nvar b = a\nvar a0 = 2\nvar c = a0\nvar a1 = 3\nvar d = a1\n})();") - }) - o("works if included multiple times", async () => { - await setup({ - "a.js": "module.exports = 123", - "b.js": 'var a = require("./a").toString()\nmodule.exports = a', - "c.js": 'var a = require("./a").toString()\nvar b = require("./b")', - }) - - o(await bundle(p("c.js"))).equals(";(()=>{\nvar _0 = 123\nvar a = _0.toString()\nvar a0 = _0.toString()\nvar b = a0\n})();") - }) - o("works if included multiple times reverse", async () => { - await setup({ - "a.js": "module.exports = 123", - "b.js": 'var a = require("./a").toString()\nmodule.exports = a', - "c.js": 'var b = require("./b")\nvar a = require("./a").toString()', - }) - - o(await bundle(p("c.js"))).equals(";(()=>{\nvar _0 = 123\nvar a0 = _0.toString()\nvar b = a0\nvar a = _0.toString()\n})();") - }) - o("reuses binding if possible", async () => { - await setup({ - "a.js": 'var b = require("./b")\nvar c = require("./c")', - "b.js": 'var d = require("./d")\nmodule.exports = function() {return d + 1}', - "c.js": 'var d = require("./d")\nmodule.exports = function() {return d + 2}', - "d.js": "module.exports = 1", - }) - - o(await bundle(p("a.js"))).equals(";(()=>{\nvar d = 1\nvar b = function() {return d + 1}\nvar c = function() {return d + 2}\n})();") - }) - o("disambiguates conflicts if imported collides with itself", async () => { - await setup({ - "a.js": 'var b = require("./b")', - "b.js": "var b = 1\nmodule.exports = function() {return b}", - }) - - o(await bundle(p("a.js"))).equals(";(()=>{\nvar b0 = 1\nvar b = function() {return b0}\n})();") - }) - o("disambiguates conflicts if imported collides with something else", async () => { - await setup({ - "a.js": 'var a = 1\nvar b = require("./b")', - "b.js": "var a = 2\nmodule.exports = function() {return a}", - }) - - o(await bundle(p("a.js"))).equals(";(()=>{\nvar a = 1\nvar a0 = 2\nvar b = function() {return a0}\n})();") - }) - o("disambiguates conflicts if imported collides with function declaration", async () => { - await setup({ - "a.js": 'function a() {}\nvar b = require("./b")', - "b.js": "var a = 2\nmodule.exports = function() {return a}", - }) - - o(await bundle(p("a.js"))).equals(";(()=>{\nfunction a() {}\nvar a0 = 2\nvar b = function() {return a0}\n})();") - }) - o("disambiguates conflicts if imported collides with another module's private", async () => { - await setup({ - "a.js": 'var b = require("./b")\nvar c = require("./c")', - "b.js": "var a = 1\nmodule.exports = function() {return a}", - "c.js": "var a = 2\nmodule.exports = function() {return a}", - }) - - o(await bundle(p("a.js"))).equals(";(()=>{\nvar a = 1\nvar b = function() {return a}\nvar a0 = 2\nvar c = function() {return a0}\n})();") - }) - o("does not mess up strings", async () => { - await setup({ - "a.js": 'var b = require("./b")', - "b.js": 'var b = "b b b \\" b"\nmodule.exports = function() {return b}', - }) - - o(await bundle(p("a.js"))).equals(';(()=>{\nvar b0 = "b b b \\\" b"\nvar b = function() {return b0}\n})();') - }) - o("does not mess up properties", async () => { - await setup({ - "a.js": 'var b = require("./b")', - "b.js": "var b = {b: 1}\nmodule.exports = function() {return b.b}", - }) - - o(await bundle(p("a.js"))).equals(";(()=>{\nvar b0 = {b: 1}\nvar b = function() {return b0.b}\n})();") - }) -}) diff --git a/src/browser.js b/src/browser.js deleted file mode 100644 index 4584f50c3..000000000 --- a/src/browser.js +++ /dev/null @@ -1,6 +0,0 @@ -"use strict" -/* global window: false */ - -var m = require("./index") -if (typeof module !== "undefined") module["exports"] = m -else window.m = m diff --git a/src/core/hyperscript.js b/src/core/hyperscript.js index 9efbaa322..24e4859a2 100644 --- a/src/core/hyperscript.js +++ b/src/core/hyperscript.js @@ -1,6 +1,4 @@ -"use strict" - -var hasOwn = require("../util/hasOwn") +import {hasOwn} from "../util.js" /* This same structure is used for several nodes. Here's an explainer for each type. @@ -174,4 +172,4 @@ m.normalizeChildren = (input) => { return input } -module.exports = m +export {m as default} diff --git a/src/core/mount-redraw.js b/src/core/mount-redraw.js index 557a8fbf5..5c2f8b8a3 100644 --- a/src/core/mount-redraw.js +++ b/src/core/mount-redraw.js @@ -1,49 +1,45 @@ -"use strict" +import render from "./render.js" -var render = require("./render") - -module.exports = function(schedule, console) { - var subscriptions = [] +function makeMountRedraw(schedule, console) { + var subscriptions = new Map() var pending = false - var offset = -1 - function sync() { - for (offset = 0; offset < subscriptions.length; offset += 2) { - try { render(subscriptions[offset], (0, subscriptions[offset + 1])(), redraw) } - catch (e) { console.error(e) } - } - offset = -1 + function redrawSync() { + subscriptions.forEach((view, root) => { + try { + render(root, view(), redraw) + } catch (e) { + console.error(e) + } + }) } function redraw() { if (!pending) { pending = true - schedule(function() { + schedule(() => { pending = false - sync() + redrawSync() }) } } - redraw.sync = sync - function mount(root, view) { if (view != null && typeof view !== "function") { throw new TypeError("m.mount expects a component, not a vnode.") } - var index = subscriptions.indexOf(root) - if (index >= 0) { - subscriptions.splice(index, 2) - if (index <= offset) offset -= 2 + if (subscriptions.delete(root)) { render(root, []) } - if (view != null) { - subscriptions.push(root, view) + if (typeof view === "function") { + subscriptions.set(root, view) render(root, view(), redraw) } } - return {mount: mount, redraw: redraw} + return {mount, redraw, redrawSync} } + +export {makeMountRedraw as default} diff --git a/src/core/render.js b/src/core/render.js index 7638a45e9..64e46870f 100644 --- a/src/core/render.js +++ b/src/core/render.js @@ -1,6 +1,4 @@ -"use strict" - -var hyperscript = require("./hyperscript") +import hyperscript from "./hyperscript.js" var xlinkNs = "http://www.w3.org/1999/xlink" var nameSpace = { @@ -489,7 +487,7 @@ function updateEvent(vnode, key, value) { var currentlyRendering = [] -module.exports = function(dom, vnodes, redraw) { +function render(dom, vnodes, redraw) { if (!dom) throw new TypeError("DOM element being rendered to does not exist.") if (currentlyRendering.some((d) => d === dom || d.contains(dom))) { throw new TypeError("Node is currently being rendered to and thus is locked.") @@ -534,3 +532,5 @@ module.exports = function(dom, vnodes, redraw) { currentlyRendering.pop() } } + +export {render as default} diff --git a/src/entry/mithril.esm.js b/src/entry/mithril.esm.js new file mode 100644 index 000000000..f5555ff56 --- /dev/null +++ b/src/entry/mithril.esm.js @@ -0,0 +1,28 @@ +/* global window: false, requestAnimationFrame: false */ +import m from "../core/hyperscript.js" +import makeMountRedraw from "../core/mount-redraw.js" +import render from "../core/render.js" + +import init from "../std/init.js" +import lazy from "../std/lazy.js" +import makeRouter from "../std/router.js" +import p from "../std/p.js" +import tracked from "../std/tracked.js" +import use from "../std/use.js" +import withProgress from "../std/with-progress.js" + +var mountRedraw = makeMountRedraw(typeof requestAnimationFrame !== "undefined" ? requestAnimationFrame : null, typeof console !== "undefined" ? console : null) + +m.mount = mountRedraw.mount +m.redraw = mountRedraw.redraw +m.redrawSync = mountRedraw.redrawSync +m.route = makeRouter(typeof window !== "undefined" ? window : null, mountRedraw.redraw) +m.render = render +m.p = p +m.withProgress = withProgress +m.lazy = lazy +m.init = init +m.use = use +m.tracked = tracked + +export default m diff --git a/src/entry/mithril.umd.js b/src/entry/mithril.umd.js new file mode 100644 index 000000000..50c773908 --- /dev/null +++ b/src/entry/mithril.umd.js @@ -0,0 +1,5 @@ +/* global module: false, window: false */ +import m from "./mithril.esm.js" + +if (typeof module !== "undefined") module.exports = m +else window.m = m diff --git a/stream/stream.js b/src/entry/stream.esm.js similarity index 93% rename from stream/stream.js rename to src/entry/stream.esm.js index 5e5882aac..ec581561a 100644 --- a/stream/stream.js +++ b/src/entry/stream.esm.js @@ -1,9 +1,3 @@ -/* eslint-disable */ -;(function() { -"use strict" -/* eslint-enable */ -/* global window: false */ - Stream.SKIP = {} Stream.lift = lift Stream.scan = scan @@ -178,8 +172,4 @@ function open(s) { return s._state === "pending" || s._state === "active" || s._state === "changing" } -if (typeof module !== "undefined") module["exports"] = Stream -else if (typeof window.m === "function" && !("stream" in window.m)) window.m.stream = Stream -else window.m = {stream : Stream} - -}()); +export {Stream as default} diff --git a/src/entry/stream.umd.js b/src/entry/stream.umd.js new file mode 100644 index 000000000..1b88b9de7 --- /dev/null +++ b/src/entry/stream.umd.js @@ -0,0 +1,6 @@ +/* global window: false, module: false */ +import Stream from "./stream.esm.js" + +if (typeof module !== "undefined") module["exports"] = Stream +else if (typeof window.m === "function" && !("stream" in window.m)) window.m.stream = Stream +else window.m = {stream : Stream} diff --git a/src/index.js b/src/index.js deleted file mode 100644 index 919d2f111..000000000 --- a/src/index.js +++ /dev/null @@ -1,18 +0,0 @@ -"use strict" -/* global window: false, requestAnimationFrame: false */ - -var m = require("./core/hyperscript") -var mountRedraw = require("./core/mount-redraw")(typeof requestAnimationFrame !== "undefined" ? requestAnimationFrame : null, typeof console !== "undefined" ? console : null) - -m.mount = mountRedraw.mount -m.route = require("./std/router")(typeof window !== "undefined" ? window : null, mountRedraw.redraw) -m.render = require("./core/render") -m.redraw = mountRedraw.redraw -m.p = require("./std/p") -m.withProgress = require("./std/with-progress") -m.lazy = require("./std/lazy") -m.init = require("./std/init") -m.use = require("./std/use") -m.tracked = require("./std/tracked") - -module.exports = m diff --git a/src/std/init.js b/src/std/init.js index e1d228b23..da2bdd1bb 100644 --- a/src/std/init.js +++ b/src/std/init.js @@ -1,7 +1,8 @@ -"use strict" +import m from "../core/hyperscript.js" -var m = require("../core/hyperscript") +import {p} from "../util.js" -var Init = ({f}, o) => (o ? m.retain() : m.layout((_, signal) => queueMicrotask(() => f(signal)))) +var Init = ({f}) => m.layout((_, signal, isInit) => isInit && p.then(() => f(signal))) +var init = (f) => m(Init, {f}) -module.exports = (f) => m(Init, {f}) +export {init as default} diff --git a/src/std/lazy.js b/src/std/lazy.js index 7819c7f56..26ee635ba 100644 --- a/src/std/lazy.js +++ b/src/std/lazy.js @@ -1,35 +1,29 @@ -"use strict" +import m from "../core/hyperscript.js" -var mountRedraw = require("../core/mount-redraw") -var m = require("../core/hyperscript") - -module.exports = (opts, redraw = mountRedraw.redraw) => { - var fetched = false +var lazy = (opts, redraw = m.redraw) => { + // Capture the error here so stack traces make more sense + var error = new ReferenceError("Component not found") var Comp = () => opts.pending && opts.pending() - var e = new ReferenceError("Component not found") - var ShowError = () => opts.error && opts.error(e) - - return () => { - if (!fetched) { - fetched = true - new Promise((resolve) => resolve(opts.fetch())).then( - (result) => { - Comp = typeof result === "function" - ? result - : result && typeof result.default === "function" - ? result.default - : ShowError - redraw() - }, - (error) => { - Comp = ShowError - e = error - if (!opts.error) console.error(error) - redraw() - } - ) + var init = async () => { + try { + Comp = await opts.fetch() + if (typeof Comp !== "function") { + Comp = Comp.default + if (typeof Comp !== "function") throw error + } + } catch (e) { + console.error(e) + Comp = () => opts.error && opts.error(e) } + redraw() + } - return (attrs) => m(Comp, attrs) + return (attrs) => { + var f = init + init = undefined + if (typeof f === "function") f() + return m(Comp, attrs) } } + +export {lazy as default} diff --git a/src/std/p.js b/src/std/p.js index a3ded987e..16c15d306 100644 --- a/src/std/p.js +++ b/src/std/p.js @@ -1,5 +1,3 @@ -"use strict" - var toString = {}.toString var serializeQueryValue = (key, value) => { @@ -17,7 +15,7 @@ var serializeQueryValue = (key, value) => { var invalidTemplateChars = /:([^\/\.-]+)(\.{3})?:/ // Returns `path` from `template` + `params` -module.exports = (template, params) => { +var p = (template, params) => { if (invalidTemplateChars.test(template)) { throw new SyntaxError("Template parameter names must be separated by either a '/', '-', or '.'.") } @@ -52,3 +50,5 @@ module.exports = (template, params) => { if (newHashIndex >= 0) result += (hashIndex < 0 ? "" : "&") + resolved.slice(newHashIndex) return result } + +export {p as default} diff --git a/src/std/router.js b/src/std/router.js index b2d8362bf..a083e4597 100644 --- a/src/std/router.js +++ b/src/std/router.js @@ -1,8 +1,8 @@ -"use strict" +import m from "../core/hyperscript.js" -var m = require("../core/hyperscript") +import {p} from "../util.js" -module.exports = function($window, redraw) { +function makeRouter($window, redraw) { var mustReplace = false var routePrefix, currentUrl, currentPath, currentHref @@ -32,7 +32,7 @@ module.exports = function($window, redraw) { } if (mustReplace) replace = true mustReplace = true - queueMicrotask(updateRoute) + p.then(updateRoute) redraw() $window.history[replace ? "replaceState" : "pushState"](state, "", routePrefix + path) } @@ -91,3 +91,6 @@ module.exports = function($window, redraw) { }), } } + + +export {makeRouter as default} diff --git a/src/std/tracked.js b/src/std/tracked.js index 9bd824b0f..20b8e5def 100644 --- a/src/std/tracked.js +++ b/src/std/tracked.js @@ -1,6 +1,4 @@ -"use strict" - -var mountRedraw = require("../core/mount-redraw") +import m from "../core/hyperscript.js" /* Here's the intent. @@ -78,7 +76,7 @@ why that was removed in favor of this: * @param {() => void} [onUpdate] * @returns {Tracked} */ -module.exports = (initial, onUpdate = mountRedraw.redraw) => { +var tracked = (initial, onUpdate = m.redraw) => { /** @type {Map & {_: AbortController}>} */ var state = new Map() /** @type {Set>} */ var live = new Set() @@ -139,3 +137,5 @@ module.exports = (initial, onUpdate = mountRedraw.redraw) => { }, } } + +export {tracked as default} diff --git a/src/std/use.js b/src/std/use.js index 072ea4cbf..607e6986f 100644 --- a/src/std/use.js +++ b/src/std/use.js @@ -1,6 +1,4 @@ -"use strict" - -var m = require("../core/hyperscript") +import m from "../core/hyperscript.js" var Use = () => { var key = 0 @@ -16,4 +14,6 @@ var Use = () => { } } -module.exports = (deps, ...children) => m(Use, {d: [...deps]}, ...children) +var use = (deps, ...children) => m(Use, {d: [...deps]}, ...children) + +export {use as default} diff --git a/src/std/with-progress.js b/src/std/with-progress.js index 724b8be32..397bf11c5 100644 --- a/src/std/with-progress.js +++ b/src/std/with-progress.js @@ -1,10 +1,8 @@ -"use strict" - /** * @param {ReadableStream | null} source * @param {(current: number) => void} notify */ -module.exports = (source, notify) => { +export default (source, notify) => { var reader = source && source.getReader() var current = 0 diff --git a/src/util.js b/src/util.js new file mode 100644 index 000000000..9feaf5765 --- /dev/null +++ b/src/util.js @@ -0,0 +1,2 @@ +export var hasOwn = {}.hasOwnProperty +export var p = Promise.resolve() diff --git a/src/util/hasOwn.js b/src/util/hasOwn.js deleted file mode 100644 index c7bd0576f..000000000 --- a/src/util/hasOwn.js +++ /dev/null @@ -1,4 +0,0 @@ -// This exists so I'm only saving it once. -"use strict" - -module.exports = {}.hasOwnProperty diff --git a/stream.js b/stream.js deleted file mode 100644 index 89e5baab0..000000000 --- a/stream.js +++ /dev/null @@ -1,3 +0,0 @@ -"use strict" - -module.exports = require("./stream/stream") diff --git a/stream/stream.min.js b/stream/stream.min.js deleted file mode 100644 index 4260f4d37..000000000 --- a/stream/stream.min.js +++ /dev/null @@ -1 +0,0 @@ -!function(){"use strict";t.SKIP={},t.lift=function(){var n=arguments[0],t=Array.prototype.slice.call(arguments,1);return r(t).map((function(t){return n.apply(void 0,t)}))},t.scan=function(n,e,r){var a=r.map((function(r){var a=n(e,r);return a!==t.SKIP&&(e=a),a}));return a(e),a},t.merge=r,t.combine=e,t.scanMerge=function(n,t){var r=n.map((function(n){return n[0]})),a=e((function(){var e=arguments[arguments.length-1];return r.forEach((function(r,a){e.indexOf(r)>-1&&(t=n[a][1](t,r()))})),t}),r);return a(t),a},t["fantasy-land/of"]=t;var n=!1;function t(n){var r,i=[],u=[];function c(e){return arguments.length&&e!==t.SKIP&&(n=e,a(c)&&(c._changing(),c._state="active",i.slice().forEach((function(t,e){a(t)&&t(this[e](n))}),u.slice()))),n}function o(){return(r=t()).map((function(n){return!0===n&&(c._parents.forEach((function(n){n._unregisterChild(c)})),c._state="ended",c._parents.length=i.length=u.length=0),n})),r}return c.constructor=t,c._state=arguments.length&&n!==t.SKIP?"active":"pending",c._parents=[],c._changing=function(){a(c)&&(c._state="changing"),i.forEach((function(n){n._changing()}))},c._map=function(e,r){var a=r?t():t(e(n));return a._parents.push(c),i.push(a),u.push(e),a},c.map=function(n){return c._map(n,"active"!==c._state)},c.toJSON=function(){return null!=n&&"function"==typeof n.toJSON?n.toJSON():n},c["fantasy-land/map"]=c.map,c["fantasy-land/ap"]=function(n){return e((function(n,t){return n()(t())}),[n,c])},c._unregisterChild=function(n){var t=i.indexOf(n);-1!==t&&(i.splice(t,1),u.splice(t,1))},Object.defineProperty(c,"end",{get:function(){return r||o()}}),c}function e(n,e){var r=e.every((function(n){if(n.constructor!==t)throw new Error("Ensure that each item passed to stream.combine/stream.merge/lift is a stream.");return"active"===n._state})),a=r?t(n.apply(null,e.concat([e]))):t(),i=[],u=e.map((function(t){return t._map((function(u){return i.push(t),(r||e.every((function(n){return"pending"!==n._state})))&&(r=!0,a(n.apply(null,e.concat([i]))),i=[]),u}),!0)})),c=a.end.map((function(n){!0===n&&(u.forEach((function(n){n.end(!0)})),c.end(!0))}));return a}function r(n){return e((function(){return n.map((function(n){return n()}))}),n)}function a(n){return"pending"===n._state||"active"===n._state||"changing"===n._state}Object.defineProperty(t,"HALT",{get:function(){return n||console.log("HALT is deprecated and has been renamed to SKIP"),n=!0,t.SKIP}}),"undefined"!=typeof module?module.exports=t:"function"!=typeof window.m||"stream"in window.m?window.m={stream:t}:window.m.stream=t}(); \ No newline at end of file diff --git a/test-utils/browserMock.js b/test-utils/browserMock.js index ead6e9e45..5460081ee 100644 --- a/test-utils/browserMock.js +++ b/test-utils/browserMock.js @@ -1,10 +1,8 @@ -"use strict" +import domMock from "./domMock.js" +import pushStateMock from "./pushStateMock.js" +import xhrMock from "./xhrMock.js" -var pushStateMock = require("./pushStateMock") -var domMock = require("./domMock") -var xhrMock = require("./xhrMock") - -module.exports = function(env) { +export default function browserMock(env) { env = env || {} var $window = env.window = {} @@ -15,4 +13,4 @@ module.exports = function(env) { pushStateMock(env) return $window -} \ No newline at end of file +} diff --git a/test-utils/callAsync.js b/test-utils/callAsync.js index 426964c99..3670986f7 100644 --- a/test-utils/callAsync.js +++ b/test-utils/callAsync.js @@ -1,3 +1,2 @@ -"use strict" - -module.exports = typeof setImmediate === "function" ? setImmediate : setTimeout +/* global setImmediate */ +export default typeof setImmediate === "function" ? setImmediate : setTimeout diff --git a/test-utils/domMock.js b/test-utils/domMock.js index c45caa1b3..8754e8382 100644 --- a/test-utils/domMock.js +++ b/test-utils/domMock.js @@ -1,5 +1,3 @@ -"use strict" - /* Known limitations: - the innerHTML setter and the DOMParser only support a small subset of the true HTML/XML syntax. @@ -14,7 +12,7 @@ options: - spy:(f: Function) => Function */ -module.exports = function(options) { +export default function domMock(options) { options = options || {} var spy = options.spy || function(f){return f} var spymap = [] diff --git a/test-utils/injectBrowserMock.js b/test-utils/injectBrowserMock.js new file mode 100644 index 000000000..c499ee0b1 --- /dev/null +++ b/test-utils/injectBrowserMock.js @@ -0,0 +1,10 @@ +/* global global: false */ +import browserMock from "../test-utils/browserMock.js" + +const mock = browserMock() +mock.setTimeout = setTimeout +if (typeof global !== "undefined") { + global.window = mock + global.document = mock.document + global.requestAnimationFrame = mock.requestAnimationFrame +} diff --git a/test-utils/parseURL.js b/test-utils/parseURL.js index e60cc531f..2d4d5bac2 100644 --- a/test-utils/parseURL.js +++ b/test-utils/parseURL.js @@ -1,6 +1,4 @@ -"use strict" - -module.exports = function parseURL(url, root) { +export default function parseURL(url, root) { var data = {} var protocolIndex = url.indexOf("://") var pathnameIndex = protocolIndex > -1 ? url.indexOf("/", protocolIndex + 3) : url.indexOf("/") diff --git a/test-utils/pushStateMock.js b/test-utils/pushStateMock.js index e65f79e79..7b5a97506 100644 --- a/test-utils/pushStateMock.js +++ b/test-utils/pushStateMock.js @@ -1,7 +1,5 @@ -"use strict" - -var parseURL = require("../test-utils/parseURL") -var callAsync = require("../test-utils/callAsync") +import callAsync from "../test-utils/callAsync.js" +import parseURL from "../test-utils/parseURL.js" function debouncedAsync(f) { var ref @@ -14,7 +12,7 @@ function debouncedAsync(f) { } } -module.exports = function(options) { +export default function pushStateMock(options) { if (options == null) options = {} var $window = options.window || {} diff --git a/test-utils/throttleMock.js b/test-utils/throttleMock.js index 21eb53be6..dc2c1bec4 100644 --- a/test-utils/throttleMock.js +++ b/test-utils/throttleMock.js @@ -1,6 +1,4 @@ -"use strict" - -module.exports = function() { +export default function throttleMocker() { var queue = [] return { schedule: function(fn) { diff --git a/test-utils/xhrMock.js b/test-utils/xhrMock.js index 0f0177438..4dc1c99b2 100644 --- a/test-utils/xhrMock.js +++ b/test-utils/xhrMock.js @@ -1,9 +1,7 @@ -"use strict" +import callAsync from "../test-utils/callAsync.js" +import parseURL from "../test-utils/parseURL.js" -var callAsync = require("../test-utils/callAsync") -var parseURL = require("../test-utils/parseURL") - -module.exports = function() { +export default function xhrMock() { var routes = {} // var callback = "callback" var serverErrorHandler = function(url) { diff --git a/tests/api/mountRedraw.js b/tests/api/mountRedraw.js index 9328fc304..36e7b7523 100644 --- a/tests/api/mountRedraw.js +++ b/tests/api/mountRedraw.js @@ -1,11 +1,9 @@ -"use strict" +import o from "ospec" -// Low-priority TODO: remove the dependency on the renderer here. -var o = require("ospec") -var domMock = require("../../test-utils/domMock") -var throttleMocker = require("../../test-utils/throttleMock") -var mountRedraw = require("../../src/core/mount-redraw") -var h = require("../../src/core/hyperscript") +import domMock from "../../test-utils/domMock.js" +import h from "../../src/core/hyperscript.js" +import mountRedraw from "../../src/core/mount-redraw.js" +import throttleMocker from "../../test-utils/throttleMock.js" o.spec("mount/redraw", function() { var error = console.error @@ -310,13 +308,13 @@ o.spec("mount/redraw", function() { o(spy2.callCount).equals(1) o(spy3.callCount).equals(1) - m.redraw.sync() + m.redrawSync() o(spy1.callCount).equals(2) o(spy2.callCount).equals(2) o(spy3.callCount).equals(2) - m.redraw.sync() + m.redrawSync() o(spy1.callCount).equals(3) o(spy2.callCount).equals(3) @@ -364,7 +362,7 @@ o.spec("mount/redraw", function() { "root1", "root2", "root3", ]) - m.redraw.sync() + m.redrawSync() o(calls).deepEquals([ "root1", "root2", "root3", "root1", "root3", @@ -397,7 +395,7 @@ o.spec("mount/redraw", function() { "root1", "root2", "root3", ]) - m.redraw.sync() + m.redrawSync() o(calls).deepEquals([ "root1", "root2", "root3", "root1", "root2", "root3", @@ -429,7 +427,7 @@ o.spec("mount/redraw", function() { "root1", "root2", "root3", ]) - m.redraw.sync() + m.redrawSync() o(calls).deepEquals([ "root1", "root2", "root3", "root1", "root3", @@ -462,7 +460,7 @@ o.spec("mount/redraw", function() { "root1", "root2", "root3", ]) - m.redraw.sync() + m.redrawSync() o(calls).deepEquals([ "root1", "root2", "root3", "root1", [TypeError, "Node is currently being rendered to and thus is locked."], "root2", "root3", @@ -496,7 +494,7 @@ o.spec("mount/redraw", function() { "root1", "root2", "root3", ]) - m.redraw.sync() + m.redrawSync() o(calls).deepEquals([ "root1", "root2", "root3", "root1", "root3", diff --git a/tests/api/router.js b/tests/api/router.js index 0acb03c2b..b873bc565 100644 --- a/tests/api/router.js +++ b/tests/api/router.js @@ -1,13 +1,11 @@ -"use strict" +import o from "ospec" -// Low-priority TODO: remove the dependency on the renderer here. -var o = require("ospec") -var browserMock = require("../../test-utils/browserMock") -var throttleMocker = require("../../test-utils/throttleMock") +import browserMock from "../../test-utils/browserMock.js" +import throttleMocker from "../../test-utils/throttleMock.js" -var m = require("../../src/core/hyperscript") -var apiMountRedraw = require("../../src/core/mount-redraw") -var apiRouter = require("../../src/std/router") +import apiMountRedraw from "../../src/core/mount-redraw.js" +import apiRouter from "../../src/std/router.js" +import m from "../../src/core/hyperscript.js" o.spec("route", () => { // Note: the `n` parameter used in calls to this are generally found by diff --git a/tests/exported-api.js b/tests/exported-api.js index 1195e0902..80513fb4b 100644 --- a/tests/exported-api.js +++ b/tests/exported-api.js @@ -1,23 +1,28 @@ -"use strict" -/* global window: false */ +/* global window: false, global: false */ +import o from "ospec" -var o = require("ospec") -var browserMock = require("../test-utils/browserMock") +import browserMock from "../test-utils/browserMock.js" o.spec("api", function() { var FRAME_BUDGET = Math.floor(1000 / 60) - var mock = browserMock(), root - mock.setTimeout = setTimeout - if (typeof global !== "undefined") { - global.window = mock - global.requestAnimationFrame = mock.requestAnimationFrame - } + var root function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)) } - var m = require("..") // eslint-disable-line global-require + var m + + o.before(async () => { + var mock = browserMock() + mock.setTimeout = setTimeout + if (typeof global !== "undefined") { + global.window = mock + global.requestAnimationFrame = mock.requestAnimationFrame + } + const mod = await import("../src/entry/mithril.esm.js") + m = mod.default + }) o.afterEach(function() { if (root) m.mount(root, null) @@ -88,12 +93,15 @@ o.spec("api", function() { o(count).equals(2) }) }) - o("sync", function() { + }) + + o.spec("m.redrawSync", function() { + o("works", function() { root = window.document.createElement("div") var view = o.spy() m.mount(root, view) o(view.callCount).equals(1) - m.redraw.sync() + m.redrawSync() o(view.callCount).equals(2) }) }) diff --git a/tests/render/attributes.js b/tests/render/attributes.js index cd6daf68b..39b1a7202 100644 --- a/tests/render/attributes.js +++ b/tests/render/attributes.js @@ -1,9 +1,8 @@ -"use strict" +import o from "ospec" -var o = require("ospec") -var domMock = require("../../test-utils/domMock") -var render = require("../../src/core/render") -var m = require("../../src/core/hyperscript") +import domMock from "../../test-utils/domMock.js" +import m from "../../src/core/hyperscript.js" +import render from "../../src/core/render.js" o.spec("attributes", function() { var $window, root diff --git a/tests/render/component.js b/tests/render/component.js index 0fcb605d9..00d0a5f6f 100644 --- a/tests/render/component.js +++ b/tests/render/component.js @@ -1,9 +1,8 @@ -"use strict" +import o from "ospec" -var o = require("ospec") -var domMock = require("../../test-utils/domMock") -var render = require("../../src/core/render") -var m = require("../../src/core/hyperscript") +import domMock from "../../test-utils/domMock.js" +import m from "../../src/core/hyperscript.js" +import render from "../../src/core/render.js" o.spec("component", function() { var $window, root diff --git a/tests/render/createElement.js b/tests/render/createElement.js index 02440c8e8..72d91cf39 100644 --- a/tests/render/createElement.js +++ b/tests/render/createElement.js @@ -1,9 +1,8 @@ -"use strict" +import o from "ospec" -var o = require("ospec") -var domMock = require("../../test-utils/domMock") -var render = require("../../src/core/render") -var m = require("../../src/core/hyperscript") +import domMock from "../../test-utils/domMock.js" +import m from "../../src/core/hyperscript.js" +import render from "../../src/core/render.js" o.spec("createElement", function() { var $window, root diff --git a/tests/render/createFragment.js b/tests/render/createFragment.js index a35b561cc..38df643af 100644 --- a/tests/render/createFragment.js +++ b/tests/render/createFragment.js @@ -1,9 +1,8 @@ -"use strict" +import o from "ospec" -var o = require("ospec") -var domMock = require("../../test-utils/domMock") -var render = require("../../src/core/render") -var m = require("../../src/core/hyperscript") +import domMock from "../../test-utils/domMock.js" +import m from "../../src/core/hyperscript.js" +import render from "../../src/core/render.js" o.spec("createFragment", function() { var $window, root diff --git a/tests/render/createNodes.js b/tests/render/createNodes.js index 98b322975..1ae9d4b07 100644 --- a/tests/render/createNodes.js +++ b/tests/render/createNodes.js @@ -1,9 +1,8 @@ -"use strict" +import o from "ospec" -var o = require("ospec") -var domMock = require("../../test-utils/domMock") -var render = require("../../src/core/render") -var m = require("../../src/core/hyperscript") +import domMock from "../../test-utils/domMock.js" +import m from "../../src/core/hyperscript.js" +import render from "../../src/core/render.js" o.spec("createNodes", function() { var $window, root diff --git a/tests/render/createText.js b/tests/render/createText.js index ac817e02d..dfcc06708 100644 --- a/tests/render/createText.js +++ b/tests/render/createText.js @@ -1,8 +1,7 @@ -"use strict" +import o from "ospec" -var o = require("ospec") -var domMock = require("../../test-utils/domMock") -var render = require("../../src/core/render") +import domMock from "../../test-utils/domMock.js" +import render from "../../src/core/render.js" o.spec("createText", function() { var $window, root diff --git a/tests/render/event.js b/tests/render/event.js index 81b58e6db..877659eba 100644 --- a/tests/render/event.js +++ b/tests/render/event.js @@ -1,9 +1,8 @@ -"use strict" +import o from "ospec" -var o = require("ospec") -var domMock = require("../../test-utils/domMock") -var reallyRender = require("../../src/core/render") -var m = require("../../src/core/hyperscript") +import domMock from "../../test-utils/domMock.js" +import m from "../../src/core/hyperscript.js" +import reallyRender from "../../src/core/render.js" o.spec("event", function() { var $window, root, redraw, render diff --git a/tests/render/fragment.js b/tests/render/fragment.js index a7bc9085b..8a761b4ee 100644 --- a/tests/render/fragment.js +++ b/tests/render/fragment.js @@ -1,7 +1,6 @@ -"use strict" +import o from "ospec" -var o = require("ospec") -var m = require("../../src/core/hyperscript") +import m from "../../src/core/hyperscript.js" o.spec("fragment literal", function() { o("works", function() { diff --git a/tests/render/hyperscript.js b/tests/render/hyperscript.js index 1c30f1e11..a8f1da354 100644 --- a/tests/render/hyperscript.js +++ b/tests/render/hyperscript.js @@ -1,8 +1,7 @@ -"use strict" +import o from "ospec" -var o = require("ospec") -var m = require("../../src/core/hyperscript") -var domMock = require("../../test-utils/domMock") +import domMock from "../../test-utils/domMock.js" +import m from "../../src/core/hyperscript.js" o.spec("hyperscript", function() { o.spec("selector", function() { diff --git a/tests/render/input.js b/tests/render/input.js index 9132a805d..a46525e0f 100644 --- a/tests/render/input.js +++ b/tests/render/input.js @@ -1,9 +1,8 @@ -"use strict" +import o from "ospec" -var o = require("ospec") -var domMock = require("../../test-utils/domMock") -var render = require("../../src/core/render") -var m = require("../../src/core/hyperscript") +import domMock from "../../test-utils/domMock.js" +import m from "../../src/core/hyperscript.js" +import render from "../../src/core/render.js" o.spec("form inputs", function() { var $window, root diff --git a/tests/render/normalize.js b/tests/render/normalize.js index 093a37f3c..e05b3bc91 100644 --- a/tests/render/normalize.js +++ b/tests/render/normalize.js @@ -1,7 +1,6 @@ -"use strict" +import o from "ospec" -var o = require("ospec") -var m = require("../../src/core/hyperscript") +import m from "../../src/core/hyperscript.js" o.spec("normalize", function() { o("normalizes array into fragment", function() { diff --git a/tests/render/normalizeChildren.js b/tests/render/normalizeChildren.js index 7adef8b97..df57ac83f 100644 --- a/tests/render/normalizeChildren.js +++ b/tests/render/normalizeChildren.js @@ -1,7 +1,6 @@ -"use strict" +import o from "ospec" -var o = require("ospec") -var m = require("../../src/core/hyperscript") +import m from "../../src/core/hyperscript.js" o.spec("normalizeChildren", function() { o("normalizes arrays into fragments", function() { diff --git a/tests/render/normalizeComponentChildren.js b/tests/render/normalizeComponentChildren.js index e52cc73d1..682b6b6a9 100644 --- a/tests/render/normalizeComponentChildren.js +++ b/tests/render/normalizeComponentChildren.js @@ -1,9 +1,8 @@ -"use strict" +import o from "ospec" -var o = require("ospec") -var m = require("../../src/core/hyperscript") -var domMock = require("../../test-utils/domMock") -var render = require("../../src/core/render") +import domMock from "../../test-utils/domMock.js" +import m from "../../src/core/hyperscript.js" +import render from "../../src/core/render.js" o.spec("component children", function () { var $window = domMock() diff --git a/tests/render/oncreate.js b/tests/render/oncreate.js index 0bb4b5d0a..fe1bfd4e0 100644 --- a/tests/render/oncreate.js +++ b/tests/render/oncreate.js @@ -1,9 +1,8 @@ -"use strict" +import o from "ospec" -var o = require("ospec") -var domMock = require("../../test-utils/domMock") -var render = require("../../src/core/render") -var m = require("../../src/core/hyperscript") +import domMock from "../../test-utils/domMock.js" +import m from "../../src/core/hyperscript.js" +import render from "../../src/core/render.js" o.spec("layout create", function() { var $window, root diff --git a/tests/render/onremove.js b/tests/render/onremove.js index 7fa6323a5..b3aba4f7a 100644 --- a/tests/render/onremove.js +++ b/tests/render/onremove.js @@ -1,9 +1,8 @@ -"use strict" +import o from "ospec" -var o = require("ospec") -var domMock = require("../../test-utils/domMock") -var render = require("../../src/core/render") -var m = require("../../src/core/hyperscript") +import domMock from "../../test-utils/domMock.js" +import m from "../../src/core/hyperscript.js" +import render from "../../src/core/render.js" o.spec("layout remove", function() { var $window, root diff --git a/tests/render/onupdate.js b/tests/render/onupdate.js index d0654b3aa..003d269dc 100644 --- a/tests/render/onupdate.js +++ b/tests/render/onupdate.js @@ -1,9 +1,8 @@ -"use strict" +import o from "ospec" -var o = require("ospec") -var domMock = require("../../test-utils/domMock") -var render = require("../../src/core/render") -var m = require("../../src/core/hyperscript") +import domMock from "../../test-utils/domMock.js" +import m from "../../src/core/hyperscript.js" +import render from "../../src/core/render.js" o.spec("layout update", function() { var $window, root diff --git a/tests/render/render-hyperscript-integration.js b/tests/render/render-hyperscript-integration.js index 44b57b2e0..7e2c52d13 100644 --- a/tests/render/render-hyperscript-integration.js +++ b/tests/render/render-hyperscript-integration.js @@ -1,9 +1,8 @@ -"use strict" +import o from "ospec" -var o = require("ospec") -var m = require("../../src/core/hyperscript") -var domMock = require("../../test-utils/domMock") -var render = require("../../src/core/render") +import domMock from "../../test-utils/domMock.js" +import m from "../../src/core/hyperscript.js" +import render from "../../src/core/render.js" o.spec("render/hyperscript integration", function() { var $window, root diff --git a/tests/render/render.js b/tests/render/render.js index a140b62eb..a39be1463 100644 --- a/tests/render/render.js +++ b/tests/render/render.js @@ -1,9 +1,8 @@ -"use strict" +import o from "ospec" -var o = require("ospec") -var domMock = require("../../test-utils/domMock") -var render = require("../../src/core/render") -var m = require("../../src/core/hyperscript") +import domMock from "../../test-utils/domMock.js" +import m from "../../src/core/hyperscript.js" +import render from "../../src/core/render.js" o.spec("render", function() { var $window, root diff --git a/tests/render/retain.js b/tests/render/retain.js index f04ee0aa6..29dd62e91 100644 --- a/tests/render/retain.js +++ b/tests/render/retain.js @@ -1,9 +1,8 @@ -"use strict" +import o from "ospec" -var o = require("ospec") -var domMock = require("../../test-utils/domMock") -var render = require("../../src/core/render") -var m = require("../../src/core/hyperscript") +import domMock from "../../test-utils/domMock.js" +import m from "../../src/core/hyperscript.js" +import render from "../../src/core/render.js" o.spec("retain", function() { var $window, root diff --git a/tests/render/textContent.js b/tests/render/textContent.js index 095f9ded3..59912dd17 100644 --- a/tests/render/textContent.js +++ b/tests/render/textContent.js @@ -1,9 +1,8 @@ -"use strict" +import o from "ospec" -var o = require("ospec") -var domMock = require("../../test-utils/domMock") -var render = require("../../src/core/render") -var m = require("../../src/core/hyperscript") +import domMock from "../../test-utils/domMock.js" +import m from "../../src/core/hyperscript.js" +import render from "../../src/core/render.js" o.spec("textContent", function() { var $window, root diff --git a/tests/render/updateElement.js b/tests/render/updateElement.js index ff2a48e34..de213ff7a 100644 --- a/tests/render/updateElement.js +++ b/tests/render/updateElement.js @@ -1,9 +1,8 @@ -"use strict" +import o from "ospec" -var o = require("ospec") -var domMock = require("../../test-utils/domMock") -var render = require("../../src/core/render") -var m = require("../../src/core/hyperscript") +import domMock from "../../test-utils/domMock.js" +import m from "../../src/core/hyperscript.js" +import render from "../../src/core/render.js" o.spec("updateElement", function() { var $window, root diff --git a/tests/render/updateFragment.js b/tests/render/updateFragment.js index 0d1e8ecd5..44d822b3c 100644 --- a/tests/render/updateFragment.js +++ b/tests/render/updateFragment.js @@ -1,9 +1,8 @@ -"use strict" +import o from "ospec" -var o = require("ospec") -var domMock = require("../../test-utils/domMock") -var render = require("../../src/core/render") -var m = require("../../src/core/hyperscript") +import domMock from "../../test-utils/domMock.js" +import m from "../../src/core/hyperscript.js" +import render from "../../src/core/render.js" o.spec("updateFragment", function() { var $window, root diff --git a/tests/render/updateNodes.js b/tests/render/updateNodes.js index 3d322a296..36820671e 100644 --- a/tests/render/updateNodes.js +++ b/tests/render/updateNodes.js @@ -1,9 +1,8 @@ -"use strict" +import o from "ospec" -var o = require("ospec") -var domMock = require("../../test-utils/domMock") -var render = require("../../src/core/render") -var m = require("../../src/core/hyperscript") +import domMock from "../../test-utils/domMock.js" +import m from "../../src/core/hyperscript.js" +import render from "../../src/core/render.js" function vnodify(str) { return str.split(",").map((k) => m.key(k, m(k))) diff --git a/tests/render/updateNodesFuzzer.js b/tests/render/updateNodesFuzzer.js index 860831937..7bea61463 100644 --- a/tests/render/updateNodesFuzzer.js +++ b/tests/render/updateNodesFuzzer.js @@ -1,9 +1,8 @@ -"use strict" +import o from "ospec" -var o = require("ospec") -var domMock = require("../../test-utils/domMock") -var render = require("../../src/core/render") -var m = require("../../src/core/hyperscript") +import domMock from "../../test-utils/domMock.js" +import m from "../../src/core/hyperscript.js" +import render from "../../src/core/render.js" o.spec("updateNodes keyed list Fuzzer", () => { const maxLength = 12 diff --git a/tests/render/updateText.js b/tests/render/updateText.js index ffc8cd929..50841a72e 100644 --- a/tests/render/updateText.js +++ b/tests/render/updateText.js @@ -1,8 +1,7 @@ -"use strict" +import o from "ospec" -var o = require("ospec") -var domMock = require("../../test-utils/domMock") -var render = require("../../src/core/render") +import domMock from "../../test-utils/domMock.js" +import render from "../../src/core/render.js" o.spec("updateText", function() { var $window, root diff --git a/tests/stream/scan.js b/tests/stream/scan.js index 7301b0105..d5b222f21 100644 --- a/tests/stream/scan.js +++ b/tests/stream/scan.js @@ -1,7 +1,6 @@ -"use strict" +import o from "ospec" -var o = require("ospec") -var stream = require("../../stream/stream") +import stream from "../../src/entry/stream.esm.js" o.spec("scan", function() { o("defaults to seed", function() { diff --git a/tests/stream/scanMerge.js b/tests/stream/scanMerge.js index a216ee0e9..3d0b4c7f7 100644 --- a/tests/stream/scanMerge.js +++ b/tests/stream/scanMerge.js @@ -1,7 +1,6 @@ -"use strict" +import o from "ospec" -var o = require("ospec") -var stream = require("../../stream/stream") +import stream from "../../src/entry/stream.esm.js" o.spec("scanMerge", function() { o("defaults to seed", function() { diff --git a/tests/stream/stream.js b/tests/stream/stream.js index 5ebfdaa4f..5f6900c7d 100644 --- a/tests/stream/stream.js +++ b/tests/stream/stream.js @@ -1,7 +1,6 @@ -"use strict" +import o from "ospec" -var o = require("ospec") -var Stream = require("../../stream/stream") +import Stream from "../../src/entry/stream.esm.js" o.spec("stream", function() { o.spec("stream", function() { diff --git a/tests/test-utils/browserMock.js b/tests/test-utils/browserMock.js index c08b573fe..d53f4819b 100644 --- a/tests/test-utils/browserMock.js +++ b/tests/test-utils/browserMock.js @@ -1,10 +1,9 @@ -"use strict" +import o from "ospec" -var o = require("ospec") -var browserMock = require("../../test-utils/browserMock") -var callAsync = require("../../test-utils/callAsync") -o.spec("browserMock", function() { +import browserMock from "../../test-utils/browserMock.js" +import callAsync from "../../test-utils/callAsync.js" +o.spec("browserMock", function() { var $window o.beforeEach(function() { $window = browserMock() diff --git a/tests/test-utils/callAsync.js b/tests/test-utils/callAsync.js index 579269e25..5b34d42a4 100644 --- a/tests/test-utils/callAsync.js +++ b/tests/test-utils/callAsync.js @@ -1,7 +1,6 @@ -"use strict" +import o from "ospec" -var o = require("ospec") -var callAsync = require("../../test-utils/callAsync") +import callAsync from "../../test-utils/callAsync.js" o.spec("callAsync", function() { o("works", function(done) { diff --git a/tests/test-utils/domMock.js b/tests/test-utils/domMock.js index fbd965367..c43bcbfb2 100644 --- a/tests/test-utils/domMock.js +++ b/tests/test-utils/domMock.js @@ -1,7 +1,7 @@ -"use strict" +/* global process: false */ +import o from "ospec" -var o = require("ospec") -var domMock = require("../../test-utils/domMock") +import domMock from "../../test-utils/domMock.js" o.spec("domMock", function() { var $document, $window diff --git a/tests/test-utils/parseURL.js b/tests/test-utils/parseURL.js index 5a1e3567d..53a0ac1e4 100644 --- a/tests/test-utils/parseURL.js +++ b/tests/test-utils/parseURL.js @@ -1,7 +1,6 @@ -"use strict" +import o from "ospec" -var o = require("ospec") -var parseURL = require("../../test-utils/parseURL") +import parseURL from "../../test-utils/parseURL.js" o.spec("parseURL", function() { var root = {protocol: "http:", hostname: "localhost", port: "", pathname: "/"} diff --git a/tests/test-utils/pushStateMock.js b/tests/test-utils/pushStateMock.js index 989d1dc21..c9d391c65 100644 --- a/tests/test-utils/pushStateMock.js +++ b/tests/test-utils/pushStateMock.js @@ -1,10 +1,9 @@ -"use strict" +import o from "ospec" -var o = require("ospec") -var pushStateMock = require("../../test-utils/pushStateMock") -var callAsync = require("../../test-utils/callAsync") -o.spec("pushStateMock", function() { +import callAsync from "../../test-utils/callAsync.js" +import pushStateMock from "../../test-utils/pushStateMock.js" +o.spec("pushStateMock", function() { var $window o.beforeEach(function() { $window = pushStateMock() diff --git a/tests/test-utils/throttleMock.js b/tests/test-utils/throttleMock.js index b537b43af..b9bde1f33 100644 --- a/tests/test-utils/throttleMock.js +++ b/tests/test-utils/throttleMock.js @@ -1,7 +1,6 @@ -"use strict" +import o from "ospec" -var o = require("ospec") -var throttleMocker = require("../../test-utils/throttleMock") +import throttleMocker from "../../test-utils/throttleMock.js" o.spec("throttleMock", function() { o("schedules one callback", function() { diff --git a/tests/test-utils/xhrMock.js b/tests/test-utils/xhrMock.js index dee088eb8..511214eed 100644 --- a/tests/test-utils/xhrMock.js +++ b/tests/test-utils/xhrMock.js @@ -1,7 +1,6 @@ -"use strict" +import o from "ospec" -var o = require("ospec") -var xhrMock = require("../../test-utils/xhrMock") +import xhrMock from "../../test-utils/xhrMock.js" o.spec("xhrMock", function() { var $window diff --git a/tests/util/init.js b/tests/util/init.js index c2e0d5177..bd6cbc477 100644 --- a/tests/util/init.js +++ b/tests/util/init.js @@ -1,9 +1,8 @@ -"use strict" +import o from "ospec" -var o = require("ospec") -var init = require("../../src/std/init") -var domMock = require("../../test-utils/domMock") -var render = require("../../src/core/render") +import domMock from "../../test-utils/domMock.js" +import init from "../../src/std/init.js" +import render from "../../src/core/render.js" o.spec("m.init", () => { o("works", () => { diff --git a/tests/util/lazy.js b/tests/util/lazy.js index 1ee33bf3b..9a22846e4 100644 --- a/tests/util/lazy.js +++ b/tests/util/lazy.js @@ -1,10 +1,9 @@ -"use strict" +import o from "ospec" -var o = require("ospec") -var domMock = require("../../test-utils/domMock") -var hyperscript = require("../../src/core/hyperscript") -var makeLazy = require("../../src/std/lazy") -var render = require("../../src/core/render") +import domMock from "../../test-utils/domMock.js" +import hyperscript from "../../src/core/hyperscript.js" +import makeLazy from "../../src/std/lazy.js" +import render from "../../src/core/render.js" o.spec("lazy", () => { var consoleError = console.error @@ -12,6 +11,10 @@ o.spec("lazy", () => { o.beforeEach(() => { $window = domMock() root = $window.document.createElement("div") + console.error = (...args) => { + consoleError.apply(console, args) + throw new Error("should not be called") + } }) o.afterEach(() => { console.error = consoleError @@ -89,7 +92,7 @@ o.spec("lazy", () => { var error = new Error("test") var calls = [] console.error = (e) => { - calls.push("error", e.message) + calls.push("console.error", e.message) } var scheduled = 1 var send, notifyRedrawn @@ -120,7 +123,7 @@ o.spec("lazy", () => { return fetchRedrawn.then(() => { o(calls).deepEquals([ "fetch", - "error", "test", + "console.error", "test", "scheduled 1", ]) @@ -131,7 +134,7 @@ o.spec("lazy", () => { o(calls).deepEquals([ "fetch", - "error", "test", + "console.error", "test", "scheduled 1", ]) @@ -142,7 +145,7 @@ o.spec("lazy", () => { o(calls).deepEquals([ "fetch", - "error", "test", + "console.error", "test", "scheduled 1", ]) }) @@ -229,7 +232,7 @@ o.spec("lazy", () => { var error = new Error("test") var calls = [] console.error = (e) => { - calls.push("error", e.message) + calls.push("console.error", e.message) } var scheduled = 1 var send, notifyRedrawn @@ -267,7 +270,7 @@ o.spec("lazy", () => { "fetch", "pending", "pending", - "error", "test", + "console.error", "test", "scheduled 1", ]) @@ -280,7 +283,7 @@ o.spec("lazy", () => { "fetch", "pending", "pending", - "error", "test", + "console.error", "test", "scheduled 1", ]) @@ -293,7 +296,7 @@ o.spec("lazy", () => { "fetch", "pending", "pending", - "error", "test", + "console.error", "test", "scheduled 1", ]) }) @@ -371,6 +374,9 @@ o.spec("lazy", () => { o("works with fetch + error and failure", () => { var error = new Error("test") var calls = [] + console.error = (e) => { + calls.push("console.error", e.message) + } var scheduled = 1 var send, notifyRedrawn var fetchRedrawn = new Promise((resolve) => notifyRedrawn = resolve) @@ -403,6 +409,7 @@ o.spec("lazy", () => { return fetchRedrawn.then(() => { o(calls).deepEquals([ "fetch", + "console.error", "test", "scheduled 1", ]) @@ -413,6 +420,7 @@ o.spec("lazy", () => { o(calls).deepEquals([ "fetch", + "console.error", "test", "scheduled 1", "error", "test", "error", "test", @@ -425,6 +433,7 @@ o.spec("lazy", () => { o(calls).deepEquals([ "fetch", + "console.error", "test", "scheduled 1", "error", "test", "error", "test", @@ -517,6 +526,9 @@ o.spec("lazy", () => { o("works with all hooks and failure", () => { var error = new Error("test") var calls = [] + console.error = (e) => { + calls.push("console.error", e.message) + } var scheduled = 1 var send, notifyRedrawn var fetchRedrawn = new Promise((resolve) => notifyRedrawn = resolve) @@ -556,6 +568,7 @@ o.spec("lazy", () => { "fetch", "pending", "pending", + "console.error", "test", "scheduled 1", ]) @@ -568,6 +581,7 @@ o.spec("lazy", () => { "fetch", "pending", "pending", + "console.error", "test", "scheduled 1", "error", "test", "error", "test", @@ -582,6 +596,7 @@ o.spec("lazy", () => { "fetch", "pending", "pending", + "console.error", "test", "scheduled 1", "error", "test", "error", "test", diff --git a/tests/util/p.js b/tests/util/p.js index b0ac751d5..dc44496bb 100644 --- a/tests/util/p.js +++ b/tests/util/p.js @@ -1,7 +1,6 @@ -"use strict" +import o from "ospec" -var o = require("ospec") -var p = require("../../src/std/p") +import p from "../../src/std/p.js" o.spec("p", () => { function test(prefix) { diff --git a/tests/util/tracked.js b/tests/util/tracked.js index cc51590f9..8822de67f 100644 --- a/tests/util/tracked.js +++ b/tests/util/tracked.js @@ -1,10 +1,9 @@ -"use strict" +import o from "ospec" -var o = require("ospec") -var makeTracked = require("../../src/std/tracked") +import makeTracked from "../../src/std/tracked.js" o.spec("tracked", () => { - /** @param {import("../tracked").Tracked} t */ + /** @param {import("../tracked.js").Tracked} t */ var live = (t) => t.live().map((h) => [h.key, h.value, h.signal.aborted]) o("initializes values correctly", () => { diff --git a/tests/util/use.js b/tests/util/use.js index 59813b983..ff3a68c04 100644 --- a/tests/util/use.js +++ b/tests/util/use.js @@ -1,10 +1,9 @@ -"use strict" +import o from "ospec" -var o = require("ospec") -var use = require("../../src/std/use") -var domMock = require("../../test-utils/domMock") -var render = require("../../src/core/render") -var m = require("../../src/core/hyperscript") +import domMock from "../../test-utils/domMock.js" +import m from "../../src/core/hyperscript.js" +import render from "../../src/core/render.js" +import use from "../../src/std/use.js" o.spec("m.use", () => { o("works with empty arrays", () => { diff --git a/tests/util/withProgress.js b/tests/util/withProgress.js index 3d6589079..e9062a3a8 100644 --- a/tests/util/withProgress.js +++ b/tests/util/withProgress.js @@ -1,7 +1,6 @@ -"use strict" +import o from "ospec" -var o = require("ospec") -var withProgress = require("../../src/std/with-progress") +import withProgress from "../../src/std/with-progress.js" if (typeof ReadableStream === "function") { o.spec("withProgress", () => { From 55a80718511cefcf67e927f27eaa057ef578d37e Mon Sep 17 00:00:00 2001 From: Claudia Meadows Date: Sat, 5 Oct 2024 20:29:43 -0700 Subject: [PATCH 46/95] Knock out all the intermediate closures, do a better job cleaning up, revise a few more things --- .eslintrc.json | 2 +- package.json | 4 +- performance/components/common.js | 5 + performance/components/mount-nested-tree.js | 5 + performance/components/mount-simple-tree.js | 5 + performance/components/nested-tree.js | 59 ++ performance/components/simple-tree.js | 41 ++ performance/inject-mock-globals.js | 4 +- performance/is-browser.js | 1 + performance/test-perf-impl.js | 163 +---- performance/test-perf.js | 4 + src/{core/render.js => core.js} | 361 ++++++++-- src/core/hyperscript.js | 175 ----- src/core/mount-redraw.js | 45 -- src/entry/mithril.esm.js | 15 +- src/std/init.js | 2 +- src/std/lazy.js | 2 +- src/std/router.js | 172 ++--- src/std/tracked.js | 2 +- src/std/use.js | 2 +- test-utils/browserMock.js | 3 +- test-utils/callAsync.js | 32 +- test-utils/domMock.js | 1 + test-utils/global.js | 45 ++ test-utils/injectBrowserMock.js | 3 + test-utils/pushStateMock.js | 24 +- test-utils/redraw-registry.js | 34 + test-utils/throttleMock.js | 23 +- test-utils/xhrMock.js | 2 +- tests/api/mountRedraw.js | 331 +++------ tests/api/router.js | 458 ++++++------- tests/exported-api.js | 77 +-- tests/render/attributes.js | 161 +++-- tests/render/component.js | 149 ++-- tests/render/createElement.js | 25 +- tests/render/createFragment.js | 13 +- tests/render/createNodes.js | 9 +- tests/render/createText.js | 18 +- tests/render/event.js | 9 +- tests/render/fragment.js | 114 ++-- tests/render/hyperscript.js | 74 +- tests/render/input.js | 55 +- tests/render/normalize.js | 24 +- tests/render/normalizeChildren.js | 38 +- tests/render/normalizeComponentChildren.js | 11 +- tests/render/oncreate.js | 35 +- tests/render/onremove.js | 33 +- tests/render/onupdate.js | 37 +- .../render/render-hyperscript-integration.js | 361 +++++----- tests/render/render.js | 67 +- tests/render/retain.js | 29 +- tests/render/textContent.js | 55 +- tests/render/updateElement.js | 99 ++- tests/render/updateFragment.js | 23 +- tests/render/updateNodes.js | 413 ++++++------ tests/render/updateNodesFuzzer.js | 7 +- tests/render/updateText.js | 38 +- tests/test-utils/browserMock.js | 2 +- tests/test-utils/callAsync.js | 37 +- tests/test-utils/pushStateMock.js | 20 +- tests/util/init.js | 30 +- tests/util/lazy.js | 635 +++++++++--------- tests/util/use.js | 21 +- 63 files changed, 2374 insertions(+), 2370 deletions(-) create mode 100644 performance/components/common.js create mode 100644 performance/components/mount-nested-tree.js create mode 100644 performance/components/mount-simple-tree.js create mode 100644 performance/components/nested-tree.js create mode 100644 performance/components/simple-tree.js create mode 100644 performance/is-browser.js rename src/{core/render.js => core.js} (63%) delete mode 100644 src/core/hyperscript.js delete mode 100644 src/core/mount-redraw.js create mode 100644 test-utils/global.js create mode 100644 test-utils/redraw-registry.js diff --git a/.eslintrc.json b/.eslintrc.json index 606e85280..0c66f9eba 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -2,7 +2,7 @@ "overrides": [ {"files": "*.cjs", "parserOptions": {"sourceType": "script"}}, { - "files": "scripts/**", + "files": ["scripts/**", "performance/**"], "env": { "node": true, "es2022": true diff --git a/package.json b/package.json index c24089fb2..63a1d4de1 100644 --- a/package.json +++ b/package.json @@ -14,9 +14,7 @@ "main": "dist/mithril.umd.js", "module": "dist/mithril.esm.js", "files": [ - "LICENSE", - "package.json", - "/dist/*.js" + "/dist/**" ], "exports": { "./stream.js": { diff --git a/performance/components/common.js b/performance/components/common.js new file mode 100644 index 000000000..da611bb36 --- /dev/null +++ b/performance/components/common.js @@ -0,0 +1,5 @@ +export const fields = [] + +for (let i=100; i--;) { + fields.push((i * 999).toString(36)) +} diff --git a/performance/components/mount-nested-tree.js b/performance/components/mount-nested-tree.js new file mode 100644 index 000000000..a51525fd4 --- /dev/null +++ b/performance/components/mount-nested-tree.js @@ -0,0 +1,5 @@ +/* global m, rootElem */ + +import {nestedTree} from "./nested-tree.js" + +m.mount(rootElem, nestedTree) diff --git a/performance/components/mount-simple-tree.js b/performance/components/mount-simple-tree.js new file mode 100644 index 000000000..27544e631 --- /dev/null +++ b/performance/components/mount-simple-tree.js @@ -0,0 +1,5 @@ +/* global m, rootElem */ + +import {simpleTree} from "./simple-tree.js" + +m.mount(rootElem, simpleTree) diff --git a/performance/components/nested-tree.js b/performance/components/nested-tree.js new file mode 100644 index 000000000..7b618fe90 --- /dev/null +++ b/performance/components/nested-tree.js @@ -0,0 +1,59 @@ +import m from "../../src/entry/mithril.esm.js" + +import {fields} from "./common.js" + +var NestedHeader = () => m("header", + m("h1.asdf", "a ", "b", " c ", 0, " d"), + m("nav", + m("a", {href: "/foo"}, "Foo"), + m("a", {href: "/bar"}, "Bar") + ) +) + +var NestedForm = () => m("form", {onSubmit: function () {}}, + m("input[type=checkbox][checked]"), + m("input[type=checkbox]", {checked: false}), + m("fieldset", + m("label", + m("input[type=radio][checked]") + ), + m("label", + m("input[type=radio]") + ) + ), + m("fieldset", fields.map((field) => + m("label", field, ":", m("input", {placeholder: field})) + )), + m(NestedButtonBar, null) +) + +var NestedButtonBar = () => m(".button-bar", + m(NestedButton, + {style: "width:10px; height:10px; border:1px solid #FFF;"}, + "Normal CSS" + ), + m(NestedButton, + {style: "top:0 ; right: 20"}, + "Poor CSS" + ), + m(NestedButton, + {style: "invalid-prop:1;padding:1px;font:12px/1.1 arial,sans-serif;", icon: true}, + "Poorer CSS" + ), + m(NestedButton, + {style: {margin: 0, padding: "10px", overflow: "visible"}}, + "Object CSS" + ) +) + +var NestedButton = (attrs) => m("button", attrs) + +var NestedMain = () => m(NestedForm) + +var NestedRoot = () => m("div.foo.bar[data-foo=bar]", + {p: 2}, + m(NestedHeader), + m(NestedMain) +) + +export const nestedTree = () => m(NestedRoot) diff --git a/performance/components/simple-tree.js b/performance/components/simple-tree.js new file mode 100644 index 000000000..ef311cdc5 --- /dev/null +++ b/performance/components/simple-tree.js @@ -0,0 +1,41 @@ +import m from "../../src/entry/mithril.esm.js" + +import {fields} from "./common.js" + +export const simpleTree = () => m(".foo.bar[data-foo=bar]", {p: 2}, + m("header", + m("h1.asdf", "a ", "b", " c ", 0, " d"), + m("nav", + m("a[href=/foo]", "Foo"), + m("a[href=/bar]", "Bar") + ) + ), + m("main", + m("form", + {onSubmit: function () {}}, + m("input[type=checkbox][checked]"), + m("input[type=checkbox]"), + m("fieldset", fields.map((field) => + m("label", field, ":", m("input", {placeholder: field})) + )), + m("button-bar", + m("button", + {style: "width:10px; height:10px; border:1px solid #FFF;"}, + "Normal CSS" + ), + m("button", + {style: "top:0 ; right: 20"}, + "Poor CSS" + ), + m("button", + {style: "invalid-prop:1;padding:1px;font:12px/1.1 arial,sans-serif;", icon: true}, + "Poorer CSS" + ), + m("button", + {style: {margin: 0, padding: "10px", overflow: "visible"}}, + "Object CSS" + ) + ) + ) + ) +) diff --git a/performance/inject-mock-globals.js b/performance/inject-mock-globals.js index 026f10f48..18ebe6197 100644 --- a/performance/inject-mock-globals.js +++ b/performance/inject-mock-globals.js @@ -1,6 +1,8 @@ /* global global */ import "../test-utils/injectBrowserMock.js" -import "../src/browser.js" + import Benchmark from "benchmark" +import m from "../src/entry/mithril.esm.js" +global.m = m global.Benchmark = Benchmark diff --git a/performance/is-browser.js b/performance/is-browser.js new file mode 100644 index 000000000..0920d4e56 --- /dev/null +++ b/performance/is-browser.js @@ -0,0 +1 @@ +export default typeof window !== "undefined" diff --git a/performance/test-perf-impl.js b/performance/test-perf-impl.js index 86731ba60..ac0ad7127 100644 --- a/performance/test-perf-impl.js +++ b/performance/test-perf-impl.js @@ -30,10 +30,13 @@ SOFTWARE. // this doesn't require a CommonJS sham polyfill. // I add it globally just so it's visible in the tests. -/* global m, Benchmark, global, window, document, rootElem: true, simpleTree: false, nestedTree: false */ +/* global m, Benchmark, global, window, document, rootElem: true */ + +import isBrowser from "./is-browser.js" + +import {nestedTree} from "./components/nested-tree.js" +import {simpleTree} from "./components/simple-tree.js" -// set up browser env on before running tests -var isDOM = typeof window !== "undefined" // eslint-disable-next-line no-undef var globalObject = typeof globalThis !== "undefined" ? globalThis : isDOM ? window : global @@ -49,7 +52,7 @@ Benchmark.options.async = true Benchmark.options.initCount = 10 Benchmark.options.minSamples = 40 -if (isDOM) { +if (isBrowser) { // Wait long enough for the browser to actually commit the DOM changes to // the screen before moving on to the next cycle, so things are at least // reasonably fresh each cycle. @@ -77,127 +80,25 @@ var suite = new Benchmark.Suite("Mithril.js perf", { // eslint-disable-next-line no-unused-vars var xsuite = {add: function(name) { console.log("skipping " + name) }} -globalObject.simpleTree = () => m(".foo.bar[data-foo=bar]", {p: 2}, - m("header", - m("h1.asdf", "a ", "b", " c ", 0, " d"), - m("nav", - m("a[href=/foo]", "Foo"), - m("a[href=/bar]", "Bar") - ) - ), - m("main", - m("form", - {onSubmit: function () {}}, - m("input[type=checkbox][checked]"), - m("input[type=checkbox]"), - m("fieldset", - this.fields.map(function (field) { - return m("label", - field, - ":", - m("input", {placeholder: field}) - ) - }) - ), - m("button-bar", - m("button", - {style: "width:10px; height:10px; border:1px solid #FFF;"}, - "Normal CSS" - ), - m("button", - {style: "top:0 ; right: 20"}, - "Poor CSS" - ), - m("button", - {style: "invalid-prop:1;padding:1px;font:12px/1.1 arial,sans-serif;", icon: true}, - "Poorer CSS" - ), - m("button", - {style: {margin: 0, padding: "10px", overflow: "visible"}}, - "Object CSS" - ) - ) - ) - ) -) - -globalObject.nestedTree = (() => { -// Result of `JSON.stringify(Array.from({length:100},(_,i)=>((i+1)*999).toString(36)))` -var fields = [ - "rr", "1ji", "2b9", "330", "3ur", "4mi", "5e9", "660", "6xr", "7pi", - "8h9", "990", "a0r", "asi", "bk9", "cc0", "d3r", "dvi", "en9", "ff0", - "g6r", "gyi", "hq9", "ii0", "j9r", "k1i", "kt9", "ll0", "mcr", "n4i", - "nw9", "oo0", "pfr", "q7i", "qz9", "rr0", "sir", "tai", "u29", "uu0", - "vlr", "wdi", "x59", "xx0", "yor", "zgi", "1089", "1100", "11rr", "12ji", - "13b9", "1430", "14ur", "15mi", "16e9", "1760", "17xr", "18pi", "19h9", "1a90", - "1b0r", "1bsi", "1ck9", "1dc0", "1e3r", "1evi", "1fn9", "1gf0", "1h6r", "1hyi", - "1iq9", "1ji0", "1k9r", "1l1i", "1lt9", "1ml0", "1ncr", "1o4i", "1ow9", "1po0", - "1qfr", "1r7i", "1rz9", "1sr0", "1tir", "1uai", "1v29", "1vu0", "1wlr", "1xdi", - "1y59", "1yx0", "1zor", "20gi", "2189", "2200", "22rr", "23ji", "24b9", "2530", -] - -var NestedHeader = () => m("header", - m("h1.asdf", "a ", "b", " c ", 0, " d"), - m("nav", - m("a", {href: "/foo"}, "Foo"), - m("a", {href: "/bar"}, "Bar") - ) -) - -var NestedForm = () => m("form", {onSubmit: function () {}}, - m("input[type=checkbox][checked]"), - m("input[type=checkbox]", {checked: false}), - m("fieldset", - m("label", - m("input[type=radio][checked]") - ), - m("label", - m("input[type=radio]") - ) - ), - m("fieldset", - fields.map(function (field) { - return m("label", - field, - ":", - m("input", {placeholder: field}) - ) - }) - ), - m(NestedButtonBar, null) -) - -var NestedButtonBar = () => m(".button-bar", - m(NestedButton, - {style: "width:10px; height:10px; border:1px solid #FFF;"}, - "Normal CSS" - ), - m(NestedButton, - {style: "top:0 ; right: 20"}, - "Poor CSS" - ), - m(NestedButton, - {style: "invalid-prop:1;padding:1px;font:12px/1.1 arial,sans-serif;", icon: true}, - "Poorer CSS" - ), - m(NestedButton, - {style: {margin: 0, padding: "10px", overflow: "visible"}}, - "Object CSS" - ) -) - -var NestedButton = (attrs) => m("button", attrs) +globalObject.simpleTree = simpleTree +globalObject.nestedTree = nestedTree -var NestedMain = () => m(NestedForm) +var mountVersion = 0 +var messageSet = new Set() -var NestedRoot = () => m("div.foo.bar[data-foo=bar]", - {p: 2}, - m(NestedHeader), - m(NestedMain) -) - -return () => m(NestedRoot) -})() +function doFetch(deferred, path) { + import(`${path}?v=${mountVersion++}`).then( + () => deferred.resolve(), + (e) => { + const key = `${path}:${e.message}` + if (!messageSet.has(key)) { + messageSet.add(key) + console.error(e) + } + deferred.resolve() + } + ) +} suite.add("construct simple tree", { fn: function () { @@ -206,9 +107,10 @@ suite.add("construct simple tree", { }) suite.add("mount simple tree", { - fn: function () { - m.mount(rootElem, simpleTree) - }, + defer: true, + fn: function (deferred) { + doFetch(deferred, "./components/mount-simple-tree.js") + } }) suite.add("redraw simple tree", { @@ -221,9 +123,10 @@ suite.add("redraw simple tree", { }) suite.add("mount large nested tree", { - fn: function () { - m.mount(rootElem, nestedTree) - }, + defer: true, + fn: function (deferred) { + doFetch(deferred, "./components/mount-nested-tree.js") + } }) suite.add("redraw large nested tree", { @@ -384,7 +287,7 @@ suite.add("reorder keyed list", { }, }) -if (isDOM) { +if (isBrowser) { window.onload = function () { cycleRoot() suite.run() diff --git a/performance/test-perf.js b/performance/test-perf.js index 404284410..ef0bffea0 100644 --- a/performance/test-perf.js +++ b/performance/test-perf.js @@ -29,6 +29,10 @@ SOFTWARE. // Mithril.js and Benchmark.js are loaded globally via bundle in the browser, so // this doesn't require a CommonJS sham polyfill. +// So it can tell from Node that it's not actually in a real browser. This gets mucked with by the +// global injection +import "./is-browser.js" + // set up browser env on before running tests import "./inject-mock-globals.js" diff --git a/src/core/render.js b/src/core.js similarity index 63% rename from src/core/render.js rename to src/core.js index 64e46870f..27c59418e 100644 --- a/src/core/render.js +++ b/src/core.js @@ -1,4 +1,209 @@ -import hyperscript from "./hyperscript.js" +import {hasOwn} from "./util.js" + +export {m as default} + +/* +This same structure is used for several nodes. Here's an explainer for each type. + +Components: +- `tag`: component reference +- `state`: view function, may `=== tag` +- `attrs`: most recently received attributes +- `children`: instance vnode +- `dom`: unused + +DOM elements: +- `tag`: tag name string +- `state`: event listener dictionary, if any events were ever registered +- `attrs`: most recently received attributes +- `children`: virtual DOM children +- `dom`: element reference + +Retain: +- `tag`: `RETAIN` +- All other properties are unused +- On ingest, the vnode itself is converted into the type of the element it's retaining. This + includes changing its type. + +Fragments: +- `tag`: `FRAGMENT` +- `state`: unused +- `attrs`: unused +- `children`: virtual DOM children +- `dom`: unused + +Keys: +- `tag`: `KEY` +- `state`: identity key (may be any arbitrary object) +- `attrs`: unused +- `children`: virtual DOM children +- `dom`: unused + +Layout: +- `tag`: `LAYOUT` +- `state`: callback to schedule +- `attrs`: unused +- `children`: unused +- `dom`: abort controller reference + +Text: +- `tag`: `TEXT` +- `state`: text string +- `attrs`: unused +- `children`: unused +- `dom`: abort controller reference +*/ + +var RETAIN = Symbol.for("m.retain") +var FRAGMENT = Symbol.for("m.Fragment") +var KEY = Symbol.for("m.key") +var LAYOUT = Symbol.for("m.layout") +var TEXT = Symbol.for("m.text") + +function Vnode(tag, state, attrs, children) { + return {tag, state, attrs, children, dom: undefined} +} + +var selectorParser = /(?:(^|#|\.)([^#\.\[\]]+))|(\[(.+?)(?:\s*=\s*("|'|)((?:\\["'\]]|.)*?)\5)?\])/g +var selectorUnescape = /\\(["'\\])/g +var selectorCache = /*@__PURE__*/ new Map() + +function compileSelector(selector) { + var match, tag = "div", classes = [], attrs = {}, hasAttrs = false + + while (match = selectorParser.exec(selector)) { + var type = match[1], value = match[2] + if (type === "" && value !== "") { + tag = value + } else { + hasAttrs = true + if (type === "#") { + attrs.id = value + } else if (type === ".") { + classes.push(value) + } else if (match[3][0] === "[") { + var attrValue = match[6] + if (attrValue) attrValue = attrValue.replace(selectorUnescape, "$1") + if (match[4] === "class" || match[4] === "className") classes.push(attrValue) + else attrs[match[4]] = attrValue == null || attrValue + } + } + } + + if (classes.length > 0) { + attrs.class = classes.join(" ") + } + + var state = {tag, attrs: hasAttrs ? attrs : null} + selectorCache.set(selector, state) + return state +} + +function execSelector(selector, attrs, children) { + attrs = attrs || {} + var hasClassName = hasOwn.call(attrs, "className") + var dynamicClass = hasClassName ? attrs.className : attrs.class + var state = selectorCache.get(selector) + var original = attrs + var selectorClass + + if (state == null) { + state = compileSelector(selector) + } + + if (state.attrs != null) { + selectorClass = state.attrs.class + attrs = Object.assign({}, state.attrs, attrs) + } + + if (dynamicClass != null || selectorClass != null) { + if (attrs !== original) attrs = Object.assign({}, attrs) + attrs.class = dynamicClass != null + ? selectorClass != null ? `${selectorClass} ${dynamicClass}` : dynamicClass + : selectorClass + if (hasClassName) attrs.className = null + } + + return Vnode(state.tag, undefined, attrs, normalizeChildren(children)) +} + +// Caution is advised when editing this - it's very perf-critical. It's specially designed to avoid +// allocations in the fast path, especially with fragments. +function m(selector, attrs, ...children) { + if (typeof selector !== "string" && typeof selector !== "function") { + throw new Error("The selector must be either a string or a component."); + } + + if (attrs == null || typeof attrs === "object" && attrs.tag == null && !Array.isArray(attrs)) { + children = children.length === 0 && attrs && hasOwn.call(attrs, "children") && Array.isArray(attrs.children) + ? attrs.children.slice() + : children.length === 1 && Array.isArray(children[0]) ? children[0].slice() : [...children] + } else { + children = children.length === 0 && Array.isArray(attrs) ? attrs.slice() : [attrs, ...children] + attrs = undefined + } + + if (typeof selector === "string") { + return execSelector(selector, attrs, children) + } else if (selector === m.Fragment) { + return Vnode(FRAGMENT, undefined, undefined, normalizeChildren(children)) + } else { + return Vnode(selector, undefined, Object.assign({children}, attrs), undefined) + } +} + +// Simple and sweet. Also useful for idioms like `onfoo: m.capture` to drop events without +// redrawing. +m.capture = (ev) => { + ev.preventDefault() + ev.stopPropagation() + return false +} + +m.retain = () => Vnode(RETAIN, undefined, undefined, undefined) + +m.layout = (f) => Vnode(LAYOUT, f, undefined, undefined) + +m.Fragment = (attrs) => attrs.children +m.key = (key, ...children) => + Vnode(KEY, key, undefined, normalizeChildren( + children.length === 1 && Array.isArray(children[0]) ? children[0].slice() : [...children] + )) + +m.normalize = (node) => { + if (node == null || typeof node === "boolean") return null + if (typeof node !== "object") return Vnode(TEXT, String(node), undefined, undefined) + if (Array.isArray(node)) return Vnode(FRAGMENT, undefined, undefined, normalizeChildren(node.slice())) + return node +} + +function normalizeChildren(input) { + if (input.length) { + input[0] = m.normalize(input[0]) + var isKeyed = input[0] != null && input[0].tag === KEY + var keys = new Set() + // Note: this is a *very* perf-sensitive check. + // Fun fact: merging the loop like this is somehow faster than splitting + // it, noticeably so. + for (var i = 1; i < input.length; i++) { + input[i] = m.normalize(input[i]) + if ((input[i] != null && input[i].tag === KEY) !== isKeyed) { + throw new TypeError( + isKeyed + ? "In fragments, vnodes must either all have keys or none have keys. You may wish to consider using an explicit empty key vnode, `m.key()`, instead of a hole." + : "In fragments, vnodes must either all have keys or none have keys." + ) + } + if (isKeyed) { + if (keys.has(input[i].state)) { + throw new TypeError(`Duplicate key detected: ${input[i].state}`) + } + keys.add(input[i].state) + } + } + } + return input +} var xlinkNs = "http://www.w3.org/1999/xlink" var nameSpace = { @@ -27,19 +232,20 @@ function createNodes(vnodes, start) { for (var i = start; i < vnodes.length; i++) createNode(vnodes[i]) } function createNode(vnode) { - if (vnode == null) return - assertVnodeIsNew(vnode) - var tag = vnode.tag - if (typeof tag === "string") { - switch (tag) { - case "!": throw new Error("No node present to retain with `m.retain()`") - case ">": createLayout(vnode); break - case "#": createText(vnode); break - case "=": - case "[": createNodes(vnode.children, 0); break - default: createElement(vnode) - } + if (vnode != null) { + assertVnodeIsNew(vnode) + innerCreateNode(vnode) } +} +function innerCreateNode(vnode) { + switch (vnode.tag) { + case RETAIN: throw new Error("No node present to retain with `m.retain()`") + case LAYOUT: return createLayout(vnode) + case TEXT: return createText(vnode) + case KEY: + case FRAGMENT: return createNodes(vnode.children, 0) + } + if (typeof vnode.tag === "string") createElement(vnode) else createComponent(vnode) } function createLayout(vnode) { @@ -47,7 +253,7 @@ function createLayout(vnode) { currentHooks.push({v: vnode, p: currentParent, i: true}) } function createText(vnode) { - insertAfterCurrentRefNode(vnode.dom = currentParent.ownerDocument.createTextNode(vnode.children)) + insertAfterCurrentRefNode(vnode.dom = currentParent.ownerDocument.createTextNode(vnode.state)) } function createElement(vnode) { var tag = vnode.tag @@ -89,7 +295,7 @@ function createComponent(vnode) { var tree = (vnode.state = vnode.tag)(vnode.attrs) if (typeof tree === "function") tree = (vnode.state = tree)(vnode.attrs) if (tree === vnode) throw new Error("A view cannot return the vnode it received as argument") - createNode(vnode.instance = hyperscript.normalize(tree)) + createNode(vnode.children = m.normalize(tree)) } //update @@ -98,8 +304,8 @@ function updateNodes(old, vnodes) { else if (old == null || old.length === 0) createNodes(vnodes, 0) else if (vnodes == null || vnodes.length === 0) removeNodes(old, 0) else { - var isOldKeyed = old[0] != null && old[0].tag === "=" - var isKeyed = vnodes[0] != null && vnodes[0].tag === "=" + var isOldKeyed = old[0] != null && old[0].tag === KEY + var isKeyed = vnodes[0] != null && vnodes[0].tag === KEY if (isOldKeyed !== isKeyed) { // Key state changed. Replace the subtree removeNodes(old, 0) @@ -153,34 +359,32 @@ function updateNode(old, vnode) { createNode(vnode) } else if (vnode == null) { removeNode(old) - } else if (vnode.tag === "!") { - assertVnodeIsNew(vnode) - // If it's a retain node, transmute it into the node it's retaining. Makes it much easier - // to implement and work with. - // - // Note: this key list *must* be complete. - vnode.tag = old.tag - vnode.state = old.state - vnode.attrs = old.attrs - vnode.children = old.children - vnode.dom = old.dom - vnode.instance = old.instance - } else if (vnode.tag === old.tag && (vnode.tag !== "=" || vnode.state === old.state)) { + } else { assertVnodeIsNew(vnode) - if (typeof vnode.tag === "string") { + if (vnode.tag === RETAIN) { + // If it's a retain node, transmute it into the node it's retaining. Makes it much easier + // to implement and work with. + // + // Note: this key list *must* be complete. + vnode.tag = old.tag + vnode.state = old.state + vnode.attrs = old.attrs + vnode.children = old.children + vnode.dom = old.dom + } else if (vnode.tag === old.tag && (vnode.tag !== KEY || vnode.state === old.state)) { switch (vnode.tag) { - case ">": updateLayout(old, vnode); break - case "#": updateText(old, vnode); break - case "=": - case "[": updateNodes(old.children, vnode.children); break - default: updateElement(old, vnode) + case LAYOUT: return updateLayout(old, vnode) + case TEXT: return updateText(old, vnode) + case KEY: + case FRAGMENT: return updateNodes(old.children, vnode.children) } + if (typeof vnode.tag === "string") updateElement(old, vnode) + else updateComponent(old, vnode) + } + else { + removeNode(old) + innerCreateNode(vnode) } - else updateComponent(old, vnode) - } - else { - removeNode(old) - createNode(vnode) } } function updateLayout(old, vnode) { @@ -188,7 +392,7 @@ function updateLayout(old, vnode) { currentHooks.push({v: vnode, p: currentParent, i: false}) } function updateText(old, vnode) { - if (`${old.children}` !== `${vnode.children}`) old.dom.nodeValue = vnode.children + if (`${old.state}` !== `${vnode.state}`) old.dom.nodeValue = vnode.state vnode.dom = currentRefNode = old.dom } function updateElement(old, vnode) { @@ -211,9 +415,9 @@ function updateElement(old, vnode) { } } function updateComponent(old, vnode) { - vnode.instance = hyperscript.normalize((vnode.state = old.state)(vnode.attrs, old.attrs)) - if (vnode.instance === vnode) throw new Error("A view cannot return the vnode it received as argument") - updateNode(old.instance, vnode.instance) + vnode.children = m.normalize((vnode.state = old.state)(vnode.attrs, old.attrs)) + if (vnode.children === vnode) throw new Error("A view cannot return the vnode it received as argument") + updateNode(old.children, vnode.children) } function insertAfterCurrentRefNode(child) { @@ -226,10 +430,10 @@ function insertAfterCurrentRefNode(child) { function moveToPosition(vnode) { while (typeof vnode.tag === "function") { - vnode = vnode.instance + vnode = vnode.children if (!vnode) return } - if (vnode.tag === "[" || vnode.tag === "=") { + if (vnode.tag === FRAGMENT || vnode.tag === KEY) { vnode.children.forEach(moveToPosition) } else { insertAfterCurrentRefNode(vnode.dom) @@ -253,21 +457,19 @@ function removeNodes(vnodes, start) { function removeNode(vnode) { if (vnode != null) { if (typeof vnode.tag === "function") { - if (vnode.instance != null) removeNode(vnode.instance) - } else if (vnode.tag === ">") { + removeNode(vnode.children) + } else if (vnode.tag === LAYOUT) { try { vnode.dom.abort() } catch (e) { console.error(e) } } else { - var isNode = vnode.tag !== "[" && vnode.tag !== "=" - - if (vnode.tag !== "#") { + if (vnode.children != null) { removeNodes(vnode.children, 0) } - if (isNode) vnode.dom.remove() + if (vnode.dom != null) vnode.dom.remove() } } } @@ -471,14 +673,14 @@ function updateEvent(vnode, key, value) { vnode.state._ = currentRedraw var prev = vnode.state.get(key) if (prev === value) return - if (value != null && (typeof value === "function" || typeof value === "object")) { + if (typeof value === "function") { if (prev == null) vnode.dom.addEventListener(key.slice(2), vnode.state, false) vnode.state.set(key, value) } else { if (prev != null) vnode.dom.removeEventListener(key.slice(2), vnode.state, false) vnode.state.delete(key) } - } else if (value != null && (typeof value === "function" || typeof value === "object")) { + } else if (typeof value === "function") { vnode.state = new EventDict() vnode.dom.addEventListener(key.slice(2), vnode.state, false) vnode.state.set(key, value) @@ -487,7 +689,7 @@ function updateEvent(vnode, key, value) { var currentlyRendering = [] -function render(dom, vnodes, redraw) { +m.render = (dom, vnodes, redraw) => { if (!dom) throw new TypeError("DOM element being rendered to does not exist.") if (currentlyRendering.some((d) => d === dom || d.contains(dom))) { throw new TypeError("Node is currently being rendered to and thus is locked.") @@ -511,7 +713,7 @@ function render(dom, vnodes, redraw) { // First time rendering into a node clears it out if (dom.vnodes == null) dom.textContent = "" - vnodes = hyperscript.normalizeChildren(Array.isArray(vnodes) ? vnodes.slice() : [vnodes]) + vnodes = normalizeChildren(Array.isArray(vnodes) ? vnodes.slice() : [vnodes]) updateNodes(dom.vnodes, vnodes) dom.vnodes = vnodes // `document.activeElement` can return null: https://html.spec.whatwg.org/multipage/interaction.html#dom-document-activeelement @@ -533,4 +735,47 @@ function render(dom, vnodes, redraw) { } } -export {render as default} +var subscriptions = new Map() +var id = 0 + +function unscheduleFrame() { + if (id) { + // eslint-disable-next-line no-undef + cancelAnimationFrame(id) + id = 0 + } +} + +m.redraw = () => { + // eslint-disable-next-line no-undef + if (!id) id = requestAnimationFrame(m.redrawSync) +} + +m.redrawSync = () => { + unscheduleFrame() + subscriptions.forEach((view, root) => { + try { + m.render(root, view(), m.redraw) + } catch (e) { + console.error(e) + } + }) +} + +m.mount = (root, view) => { + if (!root) throw new TypeError("Root must be an element") + + if (view != null && typeof view !== "function") { + throw new TypeError("View must be a component") + } + + if (subscriptions.delete(root)) { + if (!subscriptions.size) unscheduleFrame() + m.render(root, null) + } + + if (typeof view === "function") { + subscriptions.set(root, view) + m.render(root, view(), m.redraw) + } +} diff --git a/src/core/hyperscript.js b/src/core/hyperscript.js deleted file mode 100644 index 24e4859a2..000000000 --- a/src/core/hyperscript.js +++ /dev/null @@ -1,175 +0,0 @@ -import {hasOwn} from "../util.js" - -/* -This same structure is used for several nodes. Here's an explainer for each type. - -Components: -- `tag`: component reference -- `state`: view function, may `=== tag` -- `attrs`: most recently received attributes -- `children`: unused -- `dom`: unused -- `instance`: unused - -DOM elements: -- `tag`: tag name string -- `state`: event listener dictionary, if any events were ever registered -- `attrs`: most recently received attributes -- `children`: virtual DOM children -- `dom`: element reference -- `instance`: unused - -Fragments: -- `tag`: `"[" -- `state`: event listener dictionary, if any events were ever registered -- `attrs`: most recently received attributes -- `children`: virtual DOM children -- `dom`: element reference -- `instance`: unused -*/ -function Vnode(tag, state, attrs, children) { - return {tag, state, attrs, children, dom: undefined, instance: undefined} -} - -var selectorParser = /(?:(^|#|\.)([^#\.\[\]]+))|(\[(.+?)(?:\s*=\s*("|'|)((?:\\["'\]]|.)*?)\5)?\])/g -var selectorUnescape = /\\(["'\\])/g -var selectorCache = /*@__PURE__*/ new Map() - -function compileSelector(selector) { - var match, tag = "div", classes = [], attrs = {}, hasAttrs = false - - while (match = selectorParser.exec(selector)) { - var type = match[1], value = match[2] - if (type === "" && value !== "") { - tag = value - } else { - hasAttrs = true - if (type === "#") { - attrs.id = value - } else if (type === ".") { - classes.push(value) - } else if (match[3][0] === "[") { - var attrValue = match[6] - if (attrValue) attrValue = attrValue.replace(selectorUnescape, "$1") - if (match[4] === "class" || match[4] === "className") classes.push(attrValue) - else attrs[match[4]] = attrValue == null || attrValue - } - } - } - - if (classes.length > 0) { - attrs.class = classes.join(" ") - } - - var state = {tag, attrs: hasAttrs ? attrs : null} - selectorCache.set(selector, state) - return state -} - -function execSelector(selector, attrs, children) { - attrs = attrs || {} - var hasClassName = hasOwn.call(attrs, "className") - var dynamicClass = hasClassName ? attrs.className : attrs.class - var state = selectorCache.get(selector) - var original = attrs - var selectorClass - - if (state == null) { - state = compileSelector(selector) - } - - if (state.attrs != null) { - selectorClass = state.attrs.class - attrs = Object.assign({}, state.attrs, attrs) - } - - if (dynamicClass != null || selectorClass != null) { - if (attrs !== original) attrs = Object.assign({}, attrs) - attrs.class = dynamicClass != null - ? selectorClass != null ? `${selectorClass} ${dynamicClass}` : dynamicClass - : selectorClass - if (hasClassName) attrs.className = null - } - - return Vnode(state.tag, undefined, attrs, m.normalizeChildren(children)) -} - -// Caution is advised when editing this - it's very perf-critical. It's specially designed to avoid -// allocations in the fast path, especially with fragments. -function m(selector, attrs, ...children) { - if (typeof selector !== "string" && typeof selector !== "function") { - throw new Error("The selector must be either a string or a component."); - } - - if (attrs == null || typeof attrs === "object" && attrs.tag == null && !Array.isArray(attrs)) { - children = children.length === 0 && attrs && hasOwn.call(attrs, "children") && Array.isArray(attrs.children) - ? attrs.children.slice() - : children.length === 1 && Array.isArray(children[0]) ? children[0].slice() : [...children] - } else { - children = children.length === 0 && Array.isArray(attrs) ? attrs.slice() : [attrs, ...children] - attrs = undefined - } - - if (typeof selector === "string") { - return execSelector(selector, attrs, children) - } else if (selector === m.Fragment) { - return Vnode("[", undefined, undefined, m.normalizeChildren(children)) - } else { - return Vnode(selector, undefined, Object.assign({children}, attrs), undefined) - } -} - -// Simple and sweet. Also useful for idioms like `onfoo: m.capture` to drop events without -// redrawing. -m.capture = (ev) => { - ev.preventDefault() - ev.stopPropagation() - return false -} - -m.retain = () => Vnode("!", undefined, undefined, undefined) - -m.layout = (f) => Vnode(">", f, undefined, undefined) - -m.Fragment = (attrs) => attrs.children -m.key = (key, ...children) => - Vnode("=", key, undefined, m.normalizeChildren( - children.length === 1 && Array.isArray(children[0]) ? children[0].slice() : [...children] - )) - -m.normalize = (node) => { - if (node == null || typeof node === "boolean") return null - if (typeof node !== "object") return Vnode("#", undefined, undefined, String(node)) - if (Array.isArray(node)) return Vnode("[", undefined, undefined, m.normalizeChildren(node.slice())) - return node -} - -m.normalizeChildren = (input) => { - if (input.length) { - input[0] = m.normalize(input[0]) - var isKeyed = input[0] != null && input[0].tag === "=" - var keys = new Set() - // Note: this is a *very* perf-sensitive check. - // Fun fact: merging the loop like this is somehow faster than splitting - // it, noticeably so. - for (var i = 1; i < input.length; i++) { - input[i] = m.normalize(input[i]) - if ((input[i] != null && input[i].tag === "=") !== isKeyed) { - throw new TypeError( - isKeyed - ? "In fragments, vnodes must either all have keys or none have keys. You may wish to consider using an explicit empty key vnode, `m.key()`, instead of a hole." - : "In fragments, vnodes must either all have keys or none have keys." - ) - } - if (isKeyed) { - if (keys.has(input[i].state)) { - throw new TypeError(`Duplicate key detected: ${input[i].state}`) - } - keys.add(input[i].state) - } - } - } - return input -} - -export {m as default} diff --git a/src/core/mount-redraw.js b/src/core/mount-redraw.js deleted file mode 100644 index 5c2f8b8a3..000000000 --- a/src/core/mount-redraw.js +++ /dev/null @@ -1,45 +0,0 @@ -import render from "./render.js" - -function makeMountRedraw(schedule, console) { - var subscriptions = new Map() - var pending = false - - function redrawSync() { - subscriptions.forEach((view, root) => { - try { - render(root, view(), redraw) - } catch (e) { - console.error(e) - } - }) - } - - function redraw() { - if (!pending) { - pending = true - schedule(() => { - pending = false - redrawSync() - }) - } - } - - function mount(root, view) { - if (view != null && typeof view !== "function") { - throw new TypeError("m.mount expects a component, not a vnode.") - } - - if (subscriptions.delete(root)) { - render(root, []) - } - - if (typeof view === "function") { - subscriptions.set(root, view) - render(root, view(), redraw) - } - } - - return {mount, redraw, redrawSync} -} - -export {makeMountRedraw as default} diff --git a/src/entry/mithril.esm.js b/src/entry/mithril.esm.js index f5555ff56..21f9d96ec 100644 --- a/src/entry/mithril.esm.js +++ b/src/entry/mithril.esm.js @@ -1,23 +1,14 @@ -/* global window: false, requestAnimationFrame: false */ -import m from "../core/hyperscript.js" -import makeMountRedraw from "../core/mount-redraw.js" -import render from "../core/render.js" +import m from "../core.js" import init from "../std/init.js" import lazy from "../std/lazy.js" -import makeRouter from "../std/router.js" import p from "../std/p.js" +import route from "../std/router.js" import tracked from "../std/tracked.js" import use from "../std/use.js" import withProgress from "../std/with-progress.js" -var mountRedraw = makeMountRedraw(typeof requestAnimationFrame !== "undefined" ? requestAnimationFrame : null, typeof console !== "undefined" ? console : null) - -m.mount = mountRedraw.mount -m.redraw = mountRedraw.redraw -m.redrawSync = mountRedraw.redrawSync -m.route = makeRouter(typeof window !== "undefined" ? window : null, mountRedraw.redraw) -m.render = render +m.route = route m.p = p m.withProgress = withProgress m.lazy = lazy diff --git a/src/std/init.js b/src/std/init.js index da2bdd1bb..17ccdbb05 100644 --- a/src/std/init.js +++ b/src/std/init.js @@ -1,4 +1,4 @@ -import m from "../core/hyperscript.js" +import m from "../core.js" import {p} from "../util.js" diff --git a/src/std/lazy.js b/src/std/lazy.js index 26ee635ba..390c16dc7 100644 --- a/src/std/lazy.js +++ b/src/std/lazy.js @@ -1,4 +1,4 @@ -import m from "../core/hyperscript.js" +import m from "../core.js" var lazy = (opts, redraw = m.redraw) => { // Capture the error here so stack traces make more sense diff --git a/src/std/router.js b/src/std/router.js index a083e4597..414c1a5c1 100644 --- a/src/std/router.js +++ b/src/std/router.js @@ -1,96 +1,102 @@ -import m from "../core/hyperscript.js" +/* global window: false */ +import m from "../core.js" import {p} from "../util.js" -function makeRouter($window, redraw) { - var mustReplace = false - var routePrefix, currentUrl, currentPath, currentHref +var mustReplace = false +var routePrefix, currentUrl, currentPath, currentHref - function updateRoute() { - var href = $window.location.href +function updateRouteWithHref(href, update) { + if (currentHref === href) return + currentHref = href + if (update) m.redraw() - if (currentHref === href) return - currentHref = href - if (currentUrl) redraw() + var url = new URL(href) + var urlPath = url.pathname + url.search + url.hash + var index = urlPath.indexOf(routePrefix) + var prefix = routePrefix + if (index < 0) index = urlPath.indexOf(prefix = encodeURI(prefix)) + if (index >= 0) urlPath = urlPath.slice(index + prefix.length) + if (urlPath[0] !== "/") urlPath = `/${urlPath}` - var url = new URL(href) - var urlPath = url.pathname + url.search + url.hash - var index = urlPath.indexOf(routePrefix) - var prefix = routePrefix - if (index < 0) index = urlPath.indexOf(prefix = encodeURI(prefix)) - if (index >= 0) urlPath = urlPath.slice(index + prefix.length) - if (urlPath[0] !== "/") urlPath = `/${urlPath}` + currentUrl = new URL(urlPath, href) + currentPath = decodeURI(currentUrl.pathname) + mustReplace = false +} - currentUrl = new URL(urlPath, href) - currentPath = decodeURI(currentUrl.pathname) - mustReplace = false - } +function updateRoute() { + updateRouteWithHref(window.location.href, true) +} - function set(path, {replace, state} = {}) { - if (!currentUrl) { - throw new ReferenceError("Route state must be fully initialized first") - } - if (mustReplace) replace = true - mustReplace = true - p.then(updateRoute) - redraw() - $window.history[replace ? "replaceState" : "pushState"](state, "", routePrefix + path) +function set(path, {replace, state} = {}) { + if (!currentUrl) { + throw new ReferenceError("Route state must be fully initialized first") } - - return { - init(prefix = "#!") { - routePrefix = prefix - if ($window) { - $window.addEventListener("popstate", updateRoute, false) - $window.addEventListener("hashchange", updateRoute, false) - updateRoute() - } - }, - set, - get: () => currentPath + currentUrl.search + currentUrl.hash, - get path() { return currentPath }, - get params() { return currentUrl.searchParams }, - // Let's provide a *right* way to manage a route link, rather than letting people screw up - // accessibility on accident. - link: (opts) => ( - opts.disabled - // If you *really* do want add `onclick` on a disabled link, spread this and add it - // explicitly in your code. - ? {disabled: true, "aria-disabled": "true"} - : { - href: routePrefix + opts.href, - onclick(e) { - if (typeof opts.onclick === "function") { - opts.onclick.apply(this, arguments) - } - - // Adapted from React Router's implementation: - // https://github.com/ReactTraining/react-router/blob/520a0acd48ae1b066eb0b07d6d4d1790a1d02482/packages/react-router-dom/modules/Link.js - // - // Try to be flexible and intuitive in how we handle links. - // Fun fact: links aren't as obvious to get right as you - // would expect. There's a lot more valid ways to click a - // link than this, and one might want to not simply click a - // link, but right click or command-click it to copy the - // link target, etc. Nope, this isn't just for blind people. - if ( - // Skip if `onclick` prevented default - !e.defaultPrevented && - // Ignore everything but left clicks - (e.button === 0 || e.which === 0 || e.which === 1) && - // Let the browser handle `target=_blank`, etc. - (!e.currentTarget.target || e.currentTarget.target === "_self") && - // No modifier keys - !e.ctrlKey && !e.metaKey && !e.shiftKey && !e.altKey - ) { - set(opts.href, opts) - // Capture the event, and don't double-call `redraw`. - return m.capture(e) - } - }, - }), + if (mustReplace) replace = true + mustReplace = true + p.then(updateRoute) + if (typeof window === "object") { + m.redraw() + window.history[replace ? "replaceState" : "pushState"](state, "", routePrefix + path) } } +export default { + init(prefix = "#!", href) { + if (!href) { + if (typeof window !== "object") { + throw new TypeError("Outside the DOM, `href` must be provided") + + } + window.addEventListener("popstate", updateRoute, false) + window.addEventListener("hashchange", updateRoute, false) + href = window.location.href + } + + routePrefix = prefix + updateRouteWithHref(href, false) + }, + set, + get: () => currentPath + currentUrl.search + currentUrl.hash, + get path() { return currentPath }, + get params() { return currentUrl.searchParams }, + // Let's provide a *right* way to manage a route link, rather than letting people screw up + // accessibility on accident. + link: (opts) => ( + opts.disabled + // If you *really* do want add `onclick` on a disabled link, spread this and add it + // explicitly in your code. + ? {disabled: true, "aria-disabled": "true"} + : { + href: routePrefix + opts.href, + onclick(e) { + if (typeof opts.onclick === "function") { + opts.onclick.apply(this, arguments) + } -export {makeRouter as default} + // Adapted from React Router's implementation: + // https://github.com/ReactTraining/react-router/blob/520a0acd48ae1b066eb0b07d6d4d1790a1d02482/packages/react-router-dom/modules/Link.js + // + // Try to be flexible and intuitive in how we handle links. + // Fun fact: links aren't as obvious to get right as you + // would expect. There's a lot more valid ways to click a + // link than this, and one might want to not simply click a + // link, but right click or command-click it to copy the + // link target, etc. Nope, this isn't just for blind people. + if ( + // Skip if `onclick` prevented default + !e.defaultPrevented && + // Ignore everything but left clicks + (e.button === 0 || e.which === 0 || e.which === 1) && + // Let the browser handle `target=_blank`, etc. + (!e.currentTarget.target || e.currentTarget.target === "_self") && + // No modifier keys + !e.ctrlKey && !e.metaKey && !e.shiftKey && !e.altKey + ) { + set(opts.href, opts) + // Capture the event, and don't double-call `redraw`. + return m.capture(e) + } + }, + }), +} diff --git a/src/std/tracked.js b/src/std/tracked.js index 20b8e5def..ea7327fa8 100644 --- a/src/std/tracked.js +++ b/src/std/tracked.js @@ -1,4 +1,4 @@ -import m from "../core/hyperscript.js" +import m from "../core.js" /* Here's the intent. diff --git a/src/std/use.js b/src/std/use.js index 607e6986f..b27ce97c5 100644 --- a/src/std/use.js +++ b/src/std/use.js @@ -1,4 +1,4 @@ -import m from "../core/hyperscript.js" +import m from "../core.js" var Use = () => { var key = 0 diff --git a/test-utils/browserMock.js b/test-utils/browserMock.js index 5460081ee..3c6d53aba 100644 --- a/test-utils/browserMock.js +++ b/test-utils/browserMock.js @@ -2,9 +2,10 @@ import domMock from "./domMock.js" import pushStateMock from "./pushStateMock.js" import xhrMock from "./xhrMock.js" -export default function browserMock(env) { +export default function browserMock(env = {}) { env = env || {} var $window = env.window = {} + $window.window = $window var dom = domMock() var xhr = xhrMock() diff --git a/test-utils/callAsync.js b/test-utils/callAsync.js index 3670986f7..f4b93c3e8 100644 --- a/test-utils/callAsync.js +++ b/test-utils/callAsync.js @@ -1,2 +1,30 @@ -/* global setImmediate */ -export default typeof setImmediate === "function" ? setImmediate : setTimeout +/* global setImmediate, clearImmediate */ +const callAsyncRaw = typeof setImmediate === "function" ? setImmediate : setTimeout +const cancelAsyncRaw = typeof clearImmediate === "function" ? clearImmediate : clearTimeout + +const timers = new Set() + +export function callAsync(f) { + const id = callAsyncRaw(() => { + timers.delete(id) + return f() + }) + timers.add(id) +} + +export function waitAsync() { + return new Promise((resolve) => { + const id = callAsyncRaw(() => { + resolve() + timers.delete(id) + }) + timers.add(id) + }) +} + +export function clearPending() { + for (const timer of timers) { + cancelAsyncRaw(timer) + } + timers.clear() +} diff --git a/test-utils/domMock.js b/test-utils/domMock.js index 8754e8382..016fd3824 100644 --- a/test-utils/domMock.js +++ b/test-utils/domMock.js @@ -287,6 +287,7 @@ export default function domMock(options) { last = Date.now() }, delay - elapsed) }, + cancelAnimationFrame: clearTimeout, document: { createElement: function(tag) { var cssText = "" diff --git a/test-utils/global.js b/test-utils/global.js new file mode 100644 index 000000000..c86a5ef45 --- /dev/null +++ b/test-utils/global.js @@ -0,0 +1,45 @@ +import {clearPending} from "./callAsync.js" + +/* global globalThis, window, global */ +export const G = ( + typeof globalThis !== "undefined" + ? globalThis + : typeof window !== "undefined" ? window : global +) + +const keys = [ + "window", + "document", + "requestAnimationFrame", + "setTimeout", + "clearTimeout", +] + +const original = keys.map((k) => G[k]) +const originalConsoleError = console.error + +export function injectGlobals($window, rafMock, throttleMock) { + if ($window) { + for (const k of keys) { + if ({}.hasOwnProperty.call($window, k)) G[k] = $window[k] + } + } + if (rafMock) { + G.requestAnimationFrame = rafMock.schedule + G.cancelAnimationFrame = rafMock.clear + } + if (throttleMock) { + G.setTimeout = throttleMock.schedule + G.clearTimeout = throttleMock.clear + } +} + +export function restoreDOMGlobals() { + for (let i = 0; i < keys.length; i++) G[keys[i]] = original[i] +} + +export function restoreGlobalState() { + restoreDOMGlobals() + clearPending() + console.error = originalConsoleError +} diff --git a/test-utils/injectBrowserMock.js b/test-utils/injectBrowserMock.js index c499ee0b1..f6f2af58e 100644 --- a/test-utils/injectBrowserMock.js +++ b/test-utils/injectBrowserMock.js @@ -7,4 +7,7 @@ if (typeof global !== "undefined") { global.window = mock global.document = mock.document global.requestAnimationFrame = mock.requestAnimationFrame + global.cancelAnimationFrame = mock.cancelAnimationFrame } + +export {mock as default} diff --git a/test-utils/pushStateMock.js b/test-utils/pushStateMock.js index 7b5a97506..242ec4736 100644 --- a/test-utils/pushStateMock.js +++ b/test-utils/pushStateMock.js @@ -1,17 +1,6 @@ -import callAsync from "../test-utils/callAsync.js" +import {callAsync} from "../test-utils/callAsync.js" import parseURL from "../test-utils/parseURL.js" -function debouncedAsync(f) { - var ref - return function() { - if (ref != null) return - ref = callAsync(function(){ - ref = null - f() - }) - } -} - export default function pushStateMock(options) { if (options == null) options = {} @@ -50,10 +39,15 @@ export default function pushStateMock(options) { if (value === "") return "" return (value.charAt(0) !== prefix ? prefix : "") + value } - function _hashchange() { - if (typeof $window.onhashchange === "function") $window.onhashchange({type: "hashchange"}) + var hashchangePending = false + function hashchange() { + if (hashchangePending) return + callAsync(() => { + hashchangePending = false + if (typeof $window.onhashchange === "function") $window.onhashchange({type: "hashchange"}) + }) + hashchangePending = true } - var hashchange = debouncedAsync(_hashchange) function popstate() { if (typeof $window.onpopstate === "function") $window.onpopstate({type: "popstate", state: $window.history.state}) } diff --git a/test-utils/redraw-registry.js b/test-utils/redraw-registry.js new file mode 100644 index 000000000..5e9df8baa --- /dev/null +++ b/test-utils/redraw-registry.js @@ -0,0 +1,34 @@ +// Load order is important for the imports. +/* eslint-disable sort-imports */ +import o from "ospec" +import * as global from "./global.js" +import m from "../src/entry/mithril.esm.js" + +let registeredRoots, currentRafMock, currentThrottleMock + +export function register(root) { + registeredRoots.add(root) + return root +} + +export function injectGlobals($window, rafMock, throttleMock) { + registeredRoots = new Set() + global.injectGlobals($window, rafMock, throttleMock) +} + +export function restoreGlobalState() { + const errors = [] + const roots = registeredRoots + registeredRoots = null + for (const root of roots) { + try { + m.mount(root, null) + } catch (e) { + errors.push(e) + } + } + global.restoreGlobalState() + o(errors).deepEquals([]) + if (currentRafMock) o(currentRafMock.queueLength()).equals(0) + if (currentThrottleMock) o(currentThrottleMock.queueLength()).equals(0) +} diff --git a/test-utils/throttleMock.js b/test-utils/throttleMock.js index dc2c1bec4..b716ea768 100644 --- a/test-utils/throttleMock.js +++ b/test-utils/throttleMock.js @@ -1,16 +1,21 @@ export default function throttleMocker() { - var queue = [] + let queue = new Map() + let id = 0 return { - schedule: function(fn) { - queue.push(fn) + schedule(fn) { + queue.set(++id, fn) + return id }, - fire: function() { - var tasks = queue - queue = [] - tasks.forEach(function(fn) {fn()}) + clear(id) { + queue.delete(id) }, - queueLength: function(){ - return queue.length + fire() { + const tasks = queue + queue = new Map() + for (const fn of tasks.values()) fn() + }, + queueLength() { + return queue.size } } } diff --git a/test-utils/xhrMock.js b/test-utils/xhrMock.js index 4dc1c99b2..77bfb5bce 100644 --- a/test-utils/xhrMock.js +++ b/test-utils/xhrMock.js @@ -1,4 +1,4 @@ -import callAsync from "../test-utils/callAsync.js" +import {callAsync} from "../test-utils/callAsync.js" import parseURL from "../test-utils/parseURL.js" export default function xhrMock() { diff --git a/tests/api/mountRedraw.js b/tests/api/mountRedraw.js index 36e7b7523..ca264b2fa 100644 --- a/tests/api/mountRedraw.js +++ b/tests/api/mountRedraw.js @@ -1,36 +1,36 @@ import o from "ospec" +import {injectGlobals, register, restoreGlobalState} from "../../test-utils/redraw-registry.js" + +import m from "../../src/entry/mithril.esm.js" + import domMock from "../../test-utils/domMock.js" -import h from "../../src/core/hyperscript.js" -import mountRedraw from "../../src/core/mount-redraw.js" import throttleMocker from "../../test-utils/throttleMock.js" o.spec("mount/redraw", function() { - var error = console.error - o.afterEach(() => { - console.error = error + let $window, throttleMock + + o.beforeEach(() => { + $window = domMock() + throttleMock = throttleMocker() + injectGlobals($window, throttleMock) + console.error = o.spy() }) + o.afterEach(restoreGlobalState) + o("shouldn't error if there are no renderers", function() { - var consoleMock = {error: o.spy()} - var throttleMock = throttleMocker() - var m = mountRedraw(throttleMock.schedule, consoleMock) - var errors = [] + console.error = o.spy() m.redraw() throttleMock.fire() - o(consoleMock.error.calls.map((c) => c.args[0])).deepEquals(errors) + o(console.error.calls.map((c) => c.args[0])).deepEquals([]) o(throttleMock.queueLength()).equals(0) }) o("schedules correctly", function() { - var $window = domMock() - var consoleMock = {error: o.spy()} - var throttleMock = throttleMocker() - var root = $window.document.body - var m = mountRedraw(throttleMock.schedule, consoleMock) - var errors = [] + var root = register($window.document.body) var spy = o.spy() @@ -41,17 +41,12 @@ o.spec("mount/redraw", function() { throttleMock.fire() o(spy.callCount).equals(2) - o(consoleMock.error.calls.map((c) => c.args[0])).deepEquals(errors) + o(console.error.calls.map((c) => c.args[0])).deepEquals([]) o(throttleMock.queueLength()).equals(0) }) o("should run a single renderer entry", function() { - var $window = domMock() - var consoleMock = {error: o.spy()} - var throttleMock = throttleMocker() - var root = $window.document.body - var m = mountRedraw(throttleMock.schedule, consoleMock) - var errors = [] + var root = register($window.document.body) var spy = o.spy() @@ -67,17 +62,12 @@ o.spec("mount/redraw", function() { throttleMock.fire() o(spy.callCount).equals(2) - o(consoleMock.error.calls.map((c) => c.args[0])).deepEquals(errors) + o(console.error.calls.map((c) => c.args[0])).deepEquals([]) o(throttleMock.queueLength()).equals(0) }) o("should run all renderer entries", function() { - var $window = domMock() - var consoleMock = {error: o.spy()} - var throttleMock = throttleMocker() - var m = mountRedraw(throttleMock.schedule, consoleMock) var $document = $window.document - var errors = [] var el1 = $document.createElement("div") var el2 = $document.createElement("div") @@ -108,17 +98,12 @@ o.spec("mount/redraw", function() { o(spy2.callCount).equals(2) o(spy3.callCount).equals(2) - o(consoleMock.error.calls.map((c) => c.args[0])).deepEquals(errors) + o(console.error.calls.map((c) => c.args[0])).deepEquals([]) o(throttleMock.queueLength()).equals(0) }) o("should not redraw when mounting another root", function() { - var $window = domMock() - var consoleMock = {error: o.spy()} - var throttleMock = throttleMocker() - var m = mountRedraw(throttleMock.schedule, consoleMock) var $document = $window.document - var errors = [] var el1 = $document.createElement("div") var el2 = $document.createElement("div") @@ -142,17 +127,12 @@ o.spec("mount/redraw", function() { o(spy2.callCount).equals(1) o(spy3.callCount).equals(1) - o(consoleMock.error.calls.map((c) => c.args[0])).deepEquals(errors) + o(console.error.calls.map((c) => c.args[0])).deepEquals([]) o(throttleMock.queueLength()).equals(0) }) o("should stop running after mount null", function() { - var $window = domMock() - var consoleMock = {error: o.spy()} - var throttleMock = throttleMocker() - var root = $window.document.body - var m = mountRedraw(throttleMock.schedule, consoleMock) - var errors = [] + var root = register($window.document.body) var spy = o.spy() @@ -166,17 +146,12 @@ o.spec("mount/redraw", function() { throttleMock.fire() o(spy.callCount).equals(1) - o(consoleMock.error.calls.map((c) => c.args[0])).deepEquals(errors) + o(console.error.calls.map((c) => c.args[0])).deepEquals([]) o(throttleMock.queueLength()).equals(0) }) o("should stop running after mount undefined", function() { - var $window = domMock() - var consoleMock = {error: o.spy()} - var throttleMock = throttleMocker() - var root = $window.document.body - var m = mountRedraw(throttleMock.schedule, consoleMock) - var errors = [] + var root = register($window.document.body) var spy = o.spy() @@ -190,17 +165,12 @@ o.spec("mount/redraw", function() { throttleMock.fire() o(spy.callCount).equals(1) - o(consoleMock.error.calls.map((c) => c.args[0])).deepEquals(errors) + o(console.error.calls.map((c) => c.args[0])).deepEquals([]) o(throttleMock.queueLength()).equals(0) }) o("should stop running after mount no arg", function() { - var $window = domMock() - var consoleMock = {error: o.spy()} - var throttleMock = throttleMocker() - var root = $window.document.body - var m = mountRedraw(throttleMock.schedule, consoleMock) - var errors = [] + var root = register($window.document.body) var spy = o.spy() @@ -214,20 +184,15 @@ o.spec("mount/redraw", function() { throttleMock.fire() o(spy.callCount).equals(1) - o(consoleMock.error.calls.map((c) => c.args[0])).deepEquals(errors) + o(console.error.calls.map((c) => c.args[0])).deepEquals([]) o(throttleMock.queueLength()).equals(0) }) o("should invoke remove callback on unmount", function() { - var $window = domMock() - var consoleMock = {error: o.spy()} - var throttleMock = throttleMocker() - var root = $window.document.body - var m = mountRedraw(throttleMock.schedule, consoleMock) - var errors = [] + var root = register($window.document.body) var onabort = o.spy() - var spy = o.spy(() => h.layout((_, signal) => { signal.onabort = onabort })) + var spy = o.spy(() => m.layout((_, signal) => { signal.onabort = onabort })) m.mount(root, spy) o(spy.callCount).equals(1) @@ -236,17 +201,12 @@ o.spec("mount/redraw", function() { o(spy.callCount).equals(1) o(onabort.callCount).equals(1) - o(consoleMock.error.calls.map((c) => c.args[0])).deepEquals(errors) + o(console.error.calls.map((c) => c.args[0])).deepEquals([]) o(throttleMock.queueLength()).equals(0) }) o("should stop running after unsubscribe, even if it occurs after redraw is requested", function() { - var $window = domMock() - var consoleMock = {error: o.spy()} - var throttleMock = throttleMocker() - var root = $window.document.body - var m = mountRedraw(throttleMock.schedule, consoleMock) - var errors = [] + var root = register($window.document.body) var spy = o.spy() @@ -259,39 +219,29 @@ o.spec("mount/redraw", function() { throttleMock.fire() o(spy.callCount).equals(1) - o(consoleMock.error.calls.map((c) => c.args[0])).deepEquals(errors) + o(console.error.calls.map((c) => c.args[0])).deepEquals([]) o(throttleMock.queueLength()).equals(0) }) - o("does nothing on invalid unmount", function() { - var $window = domMock() - var consoleMock = {error: o.spy()} - var throttleMock = throttleMocker() - var root = $window.document.body - var m = mountRedraw(throttleMock.schedule, consoleMock) - var errors = [] + o("throws invalid unmount", function() { + var root = register($window.document.body) var spy = o.spy() m.mount(root, spy) o(spy.callCount).equals(1) - m.mount(null) + o(() => m.mount(null)).throws(Error) m.redraw() throttleMock.fire() o(spy.callCount).equals(2) - o(consoleMock.error.calls.map((c) => c.args[0])).deepEquals(errors) + o(console.error.calls.map((c) => c.args[0])).deepEquals([]) o(throttleMock.queueLength()).equals(0) }) o("redraw.sync() redraws all roots synchronously", function() { - var $window = domMock() - var consoleMock = {error: o.spy()} - var throttleMock = throttleMocker() - var m = mountRedraw(throttleMock.schedule, consoleMock) var $document = $window.document - var errors = [] var el1 = $document.createElement("div") var el2 = $document.createElement("div") @@ -320,39 +270,29 @@ o.spec("mount/redraw", function() { o(spy2.callCount).equals(3) o(spy3.callCount).equals(3) - o(consoleMock.error.calls.map((c) => c.args[0])).deepEquals(errors) + o(console.error.calls.map((c) => c.args[0])).deepEquals([]) o(throttleMock.queueLength()).equals(0) }) o("throws on invalid view", function() { - var $window = domMock() - var consoleMock = {error: o.spy()} - var throttleMock = throttleMocker() - var root = $window.document.body - var m = mountRedraw(throttleMock.schedule, consoleMock) - var errors = [] + var root = register($window.document.body) o(function() { m.mount(root, {}) }).throws(TypeError) - o(consoleMock.error.calls.map((c) => c.args[0])).deepEquals(errors) + o(console.error.calls.map((c) => c.args[0])).deepEquals([]) o(throttleMock.queueLength()).equals(0) }) o("skips roots that were synchronously unsubscribed before they were visited", function() { - var $window = domMock() - var consoleMock = {error: o.spy()} - var throttleMock = throttleMocker() - var m = mountRedraw(throttleMock.schedule, consoleMock) var $document = $window.document - var errors = [] var calls = [] - var root1 = $document.createElement("div") - var root2 = $document.createElement("div") - var root3 = $document.createElement("div") + var root1 =register($document.createElement("div")) + var root2 =register($document.createElement("div")) + var root3 =register($document.createElement("div")) - m.mount(root1, () => h.layout((_, __, isInit) => { + m.mount(root1, () => m.layout((_, __, isInit) => { if (!isInit) m.mount(root2, null) calls.push("root1") })) @@ -368,25 +308,20 @@ o.spec("mount/redraw", function() { "root1", "root3", ]) - o(consoleMock.error.calls.map((c) => c.args[0])).deepEquals(errors) + o(console.error.calls.map((c) => c.args[0])).deepEquals([]) o(throttleMock.queueLength()).equals(0) }) o("keeps its place when synchronously unsubscribing previously visited roots", function() { - var $window = domMock() - var consoleMock = {error: o.spy()} - var throttleMock = throttleMocker() - var m = mountRedraw(throttleMock.schedule, consoleMock) var $document = $window.document - var errors = [] var calls = [] - var root1 = $document.createElement("div") - var root2 = $document.createElement("div") - var root3 = $document.createElement("div") + var root1 =register($document.createElement("div")) + var root2 =register($document.createElement("div")) + var root3 =register($document.createElement("div")) m.mount(root1, () => { calls.push("root1") }) - m.mount(root2, () => h.layout((_, __, isInit) => { + m.mount(root2, () => m.layout((_, __, isInit) => { if (!isInit) m.mount(root1, null) calls.push("root2") })) @@ -401,24 +336,19 @@ o.spec("mount/redraw", function() { "root1", "root2", "root3", ]) - o(consoleMock.error.calls.map((c) => c.args[0])).deepEquals(errors) + o(console.error.calls.map((c) => c.args[0])).deepEquals([]) o(throttleMock.queueLength()).equals(0) }) - o("keeps its place when synchronously unsubscribing previously visited roots in the face of errors", function() { - var $window = domMock() - var consoleMock = {error: console.error = o.spy()} - var throttleMock = throttleMocker() - var m = mountRedraw(throttleMock.schedule, consoleMock) + o("keeps its place when synchronously unsubscribing previously visited roots in the face of []", function() { var $document = $window.document - var errors = ["fail"] var calls = [] - var root1 = $document.createElement("div") - var root2 = $document.createElement("div") - var root3 = $document.createElement("div") + var root1 =register($document.createElement("div")) + var root2 =register($document.createElement("div")) + var root3 =register($document.createElement("div")) m.mount(root1, () => { calls.push("root1") }) - m.mount(root2, () => h.layout((_, __, isInit) => { + m.mount(root2, () => m.layout((_, __, isInit) => { if (!isInit) { m.mount(root1, null); throw "fail" } calls.push("root2") })) @@ -433,25 +363,20 @@ o.spec("mount/redraw", function() { "root1", "root3", ]) - o(consoleMock.error.calls.map((c) => c.args[0])).deepEquals(errors) + o(console.error.calls.map((c) => c.args[0])).deepEquals(["fail"]) o(throttleMock.queueLength()).equals(0) }) o("keeps its place when synchronously unsubscribing the current root", function() { - var $window = domMock() - var consoleMock = {error: o.spy()} - var throttleMock = throttleMocker() - var m = mountRedraw(throttleMock.schedule, consoleMock) var $document = $window.document - var errors = [] var calls = [] - var root1 = $document.createElement("div") - var root2 = $document.createElement("div") - var root3 = $document.createElement("div") + var root1 =register($document.createElement("div")) + var root2 =register($document.createElement("div")) + var root3 =register($document.createElement("div")) m.mount(root1, () => { calls.push("root1") }) - m.mount(root2, () => h.layout((_, __, isInit) => { + m.mount(root2, () => m.layout((_, __, isInit) => { if (!isInit) try { m.mount(root2, null) } catch (e) { calls.push([e.constructor, e.message]) } calls.push("root2") })) @@ -466,26 +391,19 @@ o.spec("mount/redraw", function() { "root1", [TypeError, "Node is currently being rendered to and thus is locked."], "root2", "root3", ]) - o(consoleMock.error.calls.map((c) => c.args[0])).deepEquals(errors) + o(console.error.calls.map((c) => c.args[0])).deepEquals([]) o(throttleMock.queueLength()).equals(0) }) o("keeps its place when synchronously unsubscribing the current root in the face of an error", function() { - var $window = domMock() - var consoleMock = {error: console.error = o.spy()} - var throttleMock = throttleMocker() - var m = mountRedraw(throttleMock.schedule, consoleMock) var $document = $window.document - var errors = [ - [TypeError, "Node is currently being rendered to and thus is locked."], - ] var calls = [] - var root1 = $document.createElement("div") - var root2 = $document.createElement("div") - var root3 = $document.createElement("div") + var root1 =register($document.createElement("div")) + var root2 =register($document.createElement("div")) + var root3 =register($document.createElement("div")) m.mount(root1, () => { calls.push("root1") }) - m.mount(root2, () => h.layout((_, __, isInit) => { + m.mount(root2, () => m.layout((_, __, isInit) => { if (!isInit) try { m.mount(root2, null) } catch (e) { throw [e.constructor, e.message] } calls.push("root2") })) @@ -500,68 +418,50 @@ o.spec("mount/redraw", function() { "root1", "root3", ]) - o(consoleMock.error.calls.map((c) => c.args[0])).deepEquals(errors) + o(console.error.calls.map((c) => c.args[0])).deepEquals([ + [TypeError, "Node is currently being rendered to and thus is locked."], + ]) o(throttleMock.queueLength()).equals(0) }) o("throws on invalid `root` DOM node", function() { - var consoleMock = {error: o.spy()} - var throttleMock = throttleMocker() - var m = mountRedraw(throttleMock.schedule, consoleMock) - var errors = [] - o(function() { m.mount(null, () => {}) }).throws(TypeError) - o(consoleMock.error.calls.map((c) => c.args[0])).deepEquals(errors) + o(console.error.calls.map((c) => c.args[0])).deepEquals([]) o(throttleMock.queueLength()).equals(0) }) o("renders into `root` synchronously", function() { - var $window = domMock() - var consoleMock = {error: o.spy()} - var throttleMock = throttleMocker() - var root = $window.document.body - var m = mountRedraw(throttleMock.schedule, consoleMock) - var errors = [] + var root = register($window.document.body) - m.mount(root, () => h("div")) + m.mount(root, () => m("div")) o(root.firstChild.nodeName).equals("DIV") - o(consoleMock.error.calls.map((c) => c.args[0])).deepEquals(errors) + o(console.error.calls.map((c) => c.args[0])).deepEquals([]) o(throttleMock.queueLength()).equals(0) }) o("mounting null unmounts", function() { - var $window = domMock() - var consoleMock = {error: o.spy()} - var throttleMock = throttleMocker() - var root = $window.document.body - var m = mountRedraw(throttleMock.schedule, consoleMock) - var errors = [] + var root = register($window.document.body) - m.mount(root, () => h("div")) + m.mount(root, () => m("div")) m.mount(root, null) o(root.childNodes.length).equals(0) - o(consoleMock.error.calls.map((c) => c.args[0])).deepEquals(errors) + o(console.error.calls.map((c) => c.args[0])).deepEquals([]) o(throttleMock.queueLength()).equals(0) }) o("Mounting a second root doesn't cause the first one to redraw", function() { - var $window = domMock() - var consoleMock = {error: o.spy()} - var throttleMock = throttleMocker() - var m = mountRedraw(throttleMock.schedule, consoleMock) var $document = $window.document - var errors = [] - var root1 = $document.createElement("div") - var root2 = $document.createElement("div") + var root1 =register($document.createElement("div")) + var root2 =register($document.createElement("div")) var view = o.spy() m.mount(root1, view) @@ -574,18 +474,13 @@ o.spec("mount/redraw", function() { throttleMock.fire() o(view.callCount).equals(1) - o(consoleMock.error.calls.map((c) => c.args[0])).deepEquals(errors) + o(console.error.calls.map((c) => c.args[0])).deepEquals([]) o(throttleMock.queueLength()).equals(0) }) o("redraws on events", function() { - var $window = domMock() - var consoleMock = {error: o.spy()} - var throttleMock = throttleMocker() - var root = $window.document.body - var m = mountRedraw(throttleMock.schedule, consoleMock) + var root = register($window.document.body) var $document = $window.document - var errors = [] var layout = o.spy() var onclick = o.spy() @@ -593,9 +488,9 @@ o.spec("mount/redraw", function() { e.initEvent("click", true, true) - m.mount(root, () => h("div", { + m.mount(root, () => m("div", { onclick: onclick, - }, h.layout(layout))) + }, m.layout(layout))) root.firstChild.dispatchEvent(e) @@ -610,38 +505,33 @@ o.spec("mount/redraw", function() { o(layout.calls.map((c) => c.args[2])).deepEquals([true, false]) - o(consoleMock.error.calls.map((c) => c.args[0])).deepEquals(errors) + o(console.error.calls.map((c) => c.args[0])).deepEquals([]) o(throttleMock.queueLength()).equals(0) }) o("redraws several mount points on events", function() { - var $window = domMock() - var consoleMock = {error: o.spy()} - var throttleMock = throttleMocker() - var m = mountRedraw(throttleMock.schedule, consoleMock) var $document = $window.document - var errors = [] var layout0 = o.spy() var onclick0 = o.spy() var layout1 = o.spy() var onclick1 = o.spy() - var root1 = $document.createElement("div") - var root2 = $document.createElement("div") + var root1 =register($document.createElement("div")) + var root2 =register($document.createElement("div")) var e = $document.createEvent("MouseEvents") e.initEvent("click", true, true) - m.mount(root1, () => h("div", { + m.mount(root1, () => m("div", { onclick: onclick0, - }, h.layout(layout0))) + }, m.layout(layout0))) o(layout0.calls.map((c) => c.args[2])).deepEquals([true]) - m.mount(root2, () => h("div", { + m.mount(root2, () => m("div", { onclick: onclick1, - }, h.layout(layout1))) + }, m.layout(layout1))) o(layout1.calls.map((c) => c.args[2])).deepEquals([true]) @@ -664,27 +554,22 @@ o.spec("mount/redraw", function() { o(layout0.calls.map((c) => c.args[2])).deepEquals([true, false, false]) o(layout1.calls.map((c) => c.args[2])).deepEquals([true, false, false]) - o(consoleMock.error.calls.map((c) => c.args[0])).deepEquals(errors) + o(console.error.calls.map((c) => c.args[0])).deepEquals([]) o(throttleMock.queueLength()).equals(0) }) o("event handlers can skip redraw", function() { - var $window = domMock() - var consoleMock = {error: o.spy()} - var throttleMock = throttleMocker() - var root = $window.document.body - var m = mountRedraw(throttleMock.schedule, consoleMock) + var root = register($window.document.body) var $document = $window.document - var errors = [] var layout = o.spy() var e = $document.createEvent("MouseEvents") e.initEvent("click", true, true) - m.mount(root, () => h("div", { + m.mount(root, () => m("div", { onclick: () => false, - }, h.layout(layout))) + }, m.layout(layout))) root.firstChild.dispatchEvent(e) @@ -694,21 +579,16 @@ o.spec("mount/redraw", function() { o(layout.calls.map((c) => c.args[2])).deepEquals([true]) - o(consoleMock.error.calls.map((c) => c.args[0])).deepEquals(errors) + o(console.error.calls.map((c) => c.args[0])).deepEquals([]) o(throttleMock.queueLength()).equals(0) }) o("redraws when the render function is run", function() { - var $window = domMock() - var consoleMock = {error: o.spy()} - var throttleMock = throttleMocker() - var root = $window.document.body - var m = mountRedraw(throttleMock.schedule, consoleMock) - var errors = [] + var root = register($window.document.body) var layout = o.spy() - m.mount(root, () => h("div", h.layout(layout))) + m.mount(root, () => m("div", m.layout(layout))) o(layout.calls.map((c) => c.args[2])).deepEquals([true]) @@ -718,23 +598,21 @@ o.spec("mount/redraw", function() { o(layout.calls.map((c) => c.args[2])).deepEquals([true, false]) - o(consoleMock.error.calls.map((c) => c.args[0])).deepEquals(errors) + o(console.error.calls.map((c) => c.args[0])).deepEquals([]) o(throttleMock.queueLength()).equals(0) }) o("emits errors correctly", function() { - var $window = domMock() - var consoleMock = {error: o.spy()} - var throttleMock = throttleMocker() - var root = $window.document.body - var m = mountRedraw(throttleMock.schedule, consoleMock) - var errors = ["foo", "bar", "baz"] - var counter = -1 + var root = register($window.document.body) + var counter = 0 m.mount(root, () => { - var value = errors[counter++] - if (value != null) throw value - return null + switch (++counter) { + case 2: throw "foo" + case 3: throw "bar" + case 4: throw "baz" + default: return null + } }) m.redraw() @@ -744,7 +622,8 @@ o.spec("mount/redraw", function() { m.redraw() throttleMock.fire() - o(consoleMock.error.calls.map((c) => c.args[0])).deepEquals(errors) + o(counter).equals(4) + o(console.error.calls.map((c) => c.args[0])).deepEquals(["foo", "bar", "baz"]) o(throttleMock.queueLength()).equals(0) }) }) diff --git a/tests/api/router.js b/tests/api/router.js index b873bc565..badf90b37 100644 --- a/tests/api/router.js +++ b/tests/api/router.js @@ -1,276 +1,260 @@ import o from "ospec" +import {injectGlobals, register, restoreGlobalState} from "../../test-utils/redraw-registry.js" +import {restoreDOMGlobals} from "../../test-utils/global.js" + +import m from "../../src/entry/mithril.esm.js" + import browserMock from "../../test-utils/browserMock.js" import throttleMocker from "../../test-utils/throttleMock.js" -import apiMountRedraw from "../../src/core/mount-redraw.js" -import apiRouter from "../../src/std/router.js" -import m from "../../src/core/hyperscript.js" - o.spec("route", () => { - // Note: the `n` parameter used in calls to this are generally found by - // either trial-and-error or by studying the source. If tests are failing, - // find the failing assertions, set `n` to about 10 on the preceding call to - // `waitCycles`, then drop them down incrementally until it fails. The last - // one to succeed is the one you want to keep. And just do that for each - // failing assertion, and it'll eventually work. - // - // This is effectively what I did when designing this and hooking everything - // up. (It would be so much easier to just be able to run the calls with a - // different event loop and just turn it until I get what I want, but JS - // lacks that functionality.) - - // Use precisely what `m.route` uses, for consistency and to ensure timings - // are aligned. - function waitCycles(n) { - n = Math.max(n, 1) - return new Promise(function(resolve) { - return loop() - function loop() { - if (n === 0) resolve() - else { n--; setTimeout(loop, 4) } - } - }) - } - void [{protocol: "http:", hostname: "localhost"}, {protocol: "file:", hostname: "/"}, {protocol: "http:", hostname: "ööö"}].forEach((env) => { void ["#", "?", "", "#!", "?!", "/foo", "/föö"].forEach((prefix) => { o.spec(`using prefix \`${prefix}\` starting on ${env.protocol}//${env.hostname}`, () => { var fullHost = `${env.protocol}//${env.hostname === "/" ? "" : env.hostname}` var fullPrefix = `${fullHost}${prefix[0] === "/" ? "" : "/"}${prefix ? `${prefix}/` : ""}` - var $window, root, mountRedraw, route, throttleMock - - // In case it doesn't get reset - var realError = console.error + var $window, root, throttleMock o.beforeEach(() => { $window = browserMock(env) - $window.setTimeout = setTimeout - // $window.setImmediate = setImmediate throttleMock = throttleMocker() - - root = $window.document.body - - mountRedraw = apiMountRedraw(throttleMock.schedule, console) - route = apiRouter($window, mountRedraw.redraw) + injectGlobals($window, throttleMock) + root = register($window.document.body) + var realError = console.error console.error = function () { realError.call(this, new Error("Unexpected `console.error` call")) realError.apply(this, arguments) } }) - o.afterEach(() => { - console.error = realError - }) + o.afterEach(restoreGlobalState) o("returns the right route on init", () => { $window.location.href = `${prefix}/` - route.init(prefix) - o(route.path).equals("/") - o([...route.params]).deepEquals([]) + m.route.init(prefix) + o(m.route.path).equals("/") + o([...m.route.params]).deepEquals([]) o(throttleMock.queueLength()).equals(0) }) o("returns alternate right route on init", () => { $window.location.href = `${prefix}/test` - route.init(prefix) - o(route.path).equals("/test") - o([...route.params]).deepEquals([]) + m.route.init(prefix) + o(m.route.path).equals("/test") + o([...m.route.params]).deepEquals([]) o(throttleMock.queueLength()).equals(0) }) o("returns right route on init with escaped unicode", () => { $window.location.href = `${prefix}/%C3%B6?%C3%B6=%C3%B6` - route.init(prefix) - o(route.path).equals("/ö") - o([...route.params]).deepEquals([["ö", "ö"]]) + m.route.init(prefix) + o(m.route.path).equals("/ö") + o([...m.route.params]).deepEquals([["ö", "ö"]]) o(throttleMock.queueLength()).equals(0) }) o("returns right route on init with unescaped unicode", () => { $window.location.href = `${prefix}/ö?ö=ö` - route.init(prefix) - o(route.path).equals("/ö") - o([...route.params]).deepEquals([["ö", "ö"]]) + m.route.init(prefix) + o(m.route.path).equals("/ö") + o([...m.route.params]).deepEquals([["ö", "ö"]]) o(throttleMock.queueLength()).equals(0) }) - o("sets path asynchronously", () => { + o("sets path asynchronously", async () => { $window.location.href = `${prefix}/a` var spy1 = o.spy() var spy2 = o.spy() - route.init(prefix) - mountRedraw.mount(root, () => { - if (route.path === "/a") { + m.route.init(prefix) + m.mount(root, () => { + if (m.route.path === "/a") { spy1() - } else if (route.path === "/b") { + } else if (m.route.path === "/b") { spy2() } else { - throw new Error(`Unknown path ${route.path}`) + throw new Error(`Unknown path ${m.route.path}`) } }) o(spy1.callCount).equals(1) o(spy2.callCount).equals(0) - route.set("/b") + m.route.set("/b") o(spy1.callCount).equals(1) o(spy2.callCount).equals(0) - return waitCycles(1).then(() => { - throttleMock.fire() - o(spy1.callCount).equals(1) - o(spy2.callCount).equals(1) - o(throttleMock.queueLength()).equals(0) - }) + await Promise.resolve() + throttleMock.fire() + + o(spy1.callCount).equals(1) + o(spy2.callCount).equals(1) + o(throttleMock.queueLength()).equals(0) }) - o("sets route via pushState/onpopstate", () => { + o("sets route via pushState/onpopstate", async () => { $window.location.href = `${prefix}/test` - route.init(prefix) - - return waitCycles(1) - .then(() => { - $window.history.pushState(null, null, `${prefix}/other/x/y/z?c=d#e=f`) - $window.onpopstate() - }) - .then(() => waitCycles(1)) - .then(() => { - // Yep, before even the throttle mechanism takes hold. - o(route.get()).equals("/other/x/y/z?c=d#e=f") - throttleMock.fire() - o(throttleMock.queueLength()).equals(0) - }) + m.route.init(prefix) + + await Promise.resolve() + throttleMock.fire() + + $window.history.pushState(null, null, `${prefix}/other/x/y/z?c=d#e=f`) + $window.onpopstate() + + await Promise.resolve() + throttleMock.fire() + + // Yep, before even the throttle mechanism takes hold. + o(m.route.get()).equals("/other/x/y/z?c=d#e=f") + + await Promise.resolve() + throttleMock.fire() + + o(throttleMock.queueLength()).equals(0) }) - o("`replace: true` works", () => { + o("`replace: true` works", async () => { $window.location.href = `${prefix}/test` - route.init(prefix) + m.route.init(prefix) - route.set("/other", {replace: true}) + m.route.set("/other", {replace: true}) - return waitCycles(1).then(() => { - throttleMock.fire() - $window.history.back() - o($window.location.href).equals(`${fullHost}/`) - throttleMock.fire() - o($window.location.href).equals(`${fullHost}/`) - o(throttleMock.queueLength()).equals(0) - }) + await Promise.resolve() + throttleMock.fire() + + $window.history.back() + o($window.location.href).equals(`${fullHost}/`) + + await Promise.resolve() + throttleMock.fire() + + o($window.location.href).equals(`${fullHost}/`) + o(throttleMock.queueLength()).equals(0) }) - o("`replace: true` works in links", () => { + o("`replace: true` works in links", async () => { $window.location.href = `${prefix}/test` - route.init(prefix) + m.route.init(prefix) var e = $window.document.createEvent("MouseEvents") e.initEvent("click", true, true) e.button = 0 - mountRedraw.mount(root, () => { - if (route.path === "/test") { - return m("a", route.link({href: "/other", replace: true})) - } else if (route.path === "/other") { + m.mount(root, () => { + if (m.route.path === "/test") { + return m("a", m.route.link({href: "/other", replace: true})) + } else if (m.route.path === "/other") { return m("div") - } else if (route.path === "/") { + } else if (m.route.path === "/") { return m("span") } else { - throw new Error(`Unknown route: ${route.path}`) + throw new Error(`Unknown route: ${m.route.path}`) } }) root.firstChild.dispatchEvent(e) - return waitCycles(1).then(() => { - throttleMock.fire() - $window.history.back() - o($window.location.href).equals(`${fullHost}/`) - throttleMock.fire() - o($window.location.href).equals(`${fullHost}/`) - o(throttleMock.queueLength()).equals(0) - }) + await Promise.resolve() + throttleMock.fire() + + $window.history.back() + o($window.location.href).equals(`${fullHost}/`) + + await Promise.resolve() + throttleMock.fire() + + o($window.location.href).equals(`${fullHost}/`) + o(throttleMock.queueLength()).equals(0) }) - o("`replace: false` works", () => { + o("`replace: false` works", async () => { $window.location.href = `${prefix}/test` - route.init(prefix) + m.route.init(prefix) - route.set("/other", {replace: false}) + m.route.set("/other", {replace: false}) - return waitCycles(1).then(() => { - throttleMock.fire() - $window.history.back() - o($window.location.href).equals(`${fullPrefix}test`) - throttleMock.fire() - o($window.location.href).equals(`${fullPrefix}test`) - o(throttleMock.queueLength()).equals(0) - }) + await Promise.resolve() + throttleMock.fire() + + $window.history.back() + o($window.location.href).equals(`${fullPrefix}test`) + + await Promise.resolve() + throttleMock.fire() + + o($window.location.href).equals(`${fullPrefix}test`) + o(throttleMock.queueLength()).equals(0) }) - o("`replace: false` works in links", () => { + o("`replace: false` works in links", async () => { $window.location.href = `${prefix}/test` - route.init(prefix) + m.route.init(prefix) var e = $window.document.createEvent("MouseEvents") e.initEvent("click", true, true) e.button = 0 - mountRedraw.mount(root, () => { - if (route.path === "/test") { - return m("a", route.link({href: "/other", replace: false})) - } else if (route.path === "/other") { + m.mount(root, () => { + if (m.route.path === "/test") { + return m("a", m.route.link({href: "/other", replace: false})) + } else if (m.route.path === "/other") { return m("div") } else { - throw new Error(`Unknown route: ${route.path}`) + throw new Error(`Unknown route: ${m.route.path}`) } }) root.firstChild.dispatchEvent(e) - return waitCycles(1).then(() => { - throttleMock.fire() - $window.history.back() - o($window.location.href).equals(`${fullPrefix}test`) - throttleMock.fire() - o($window.location.href).equals(`${fullPrefix}test`) - o(throttleMock.queueLength()).equals(0) - }) + await Promise.resolve() + throttleMock.fire() + + $window.history.back() + o($window.location.href).equals(`${fullPrefix}test`) + + await Promise.resolve() + throttleMock.fire() + + o($window.location.href).equals(`${fullPrefix}test`) + o(throttleMock.queueLength()).equals(0) }) - o("state works", () => { + o("state works", async () => { $window.location.href = `${prefix}/test` - route.init(prefix) + m.route.init(prefix) - route.set("/other", {state: {a: 1}}) - return waitCycles(1).then(() => { - throttleMock.fire() - o($window.history.state).deepEquals({a: 1}) - o(throttleMock.queueLength()).equals(0) - }) + m.route.set("/other", {state: {a: 1}}) + + await Promise.resolve() + throttleMock.fire() + + o($window.history.state).deepEquals({a: 1}) + o(throttleMock.queueLength()).equals(0) }) o("adds trailing slash where needed", () => { $window.location.href = `${prefix}/test` - route.init(`${prefix}/`) - o(route.path).equals("/test") - o([...route.params]).deepEquals([]) + m.route.init(`${prefix}/`) + o(m.route.path).equals("/test") + o([...m.route.params]).deepEquals([]) o(throttleMock.queueLength()).equals(0) }) o("handles route with search", () => { $window.location.href = `${prefix}/test?a=b&c=d` - route.init(prefix) - o(route.path).equals("/test") - o([...route.params]).deepEquals([["a", "b"], ["c", "d"]]) + m.route.init(prefix) + o(m.route.path).equals("/test") + o([...m.route.params]).deepEquals([["a", "b"], ["c", "d"]]) o(throttleMock.queueLength()).equals(0) }) @@ -278,7 +262,7 @@ o.spec("route", () => { $window.location.href = "http://old.com" $window.location.href = "http://new.com" - route.init(prefix) + m.route.init(prefix) $window.history.back() @@ -287,21 +271,21 @@ o.spec("route", () => { o(throttleMock.queueLength()).equals(0) }) - o("changes location on route.Link", () => { + o("changes location on route.Link", async () => { var e = $window.document.createEvent("MouseEvents") e.initEvent("click", true, true) e.button = 0 $window.location.href = `${prefix}/` - route.init(prefix) - mountRedraw.mount(root, () => { - if (route.path === "/") { - return m("a", route.link({href: "/test"})) - } else if (route.path === "/test") { + m.route.init(prefix) + m.mount(root, () => { + if (m.route.path === "/") { + return m("a", m.route.link({href: "/test"})) + } else if (m.route.path === "/test") { return m("div") } else { - throw new Error(`Unknown route: ${route.path}`) + throw new Error(`Unknown route: ${m.route.path}`) } }) @@ -309,69 +293,68 @@ o.spec("route", () => { root.firstChild.dispatchEvent(e) - return waitCycles(1).then(() => { - throttleMock.fire() - o($window.location.href).equals(`${fullPrefix}test`) - o(throttleMock.queueLength()).equals(0) - }) + await Promise.resolve() + throttleMock.fire() + + o($window.location.href).equals(`${fullPrefix}test`) + o(throttleMock.queueLength()).equals(0) }) - o("passes state on route.Link", () => { + o("passes state on route.Link", async () => { var e = $window.document.createEvent("MouseEvents") e.initEvent("click", true, true) e.button = 0 $window.location.href = `${prefix}/` - route.init(prefix) - mountRedraw.mount(root, () => { - if (route.path === "/") { - return m("a", route.link({href: "/test", state: {a: 1}})) - } else if (route.path === "/test") { + m.route.init(prefix) + m.mount(root, () => { + if (m.route.path === "/") { + return m("a", m.route.link({href: "/test", state: {a: 1}})) + } else if (m.route.path === "/test") { return m("div") } else { - throw new Error(`Unknown route: ${route.path}`) + throw new Error(`Unknown route: ${m.route.path}`) } }) root.firstChild.dispatchEvent(e) - return waitCycles(1).then(() => { - throttleMock.fire() - o($window.history.state).deepEquals({a: 1}) - o(throttleMock.queueLength()).equals(0) - }) + await Promise.resolve() + throttleMock.fire() + + o($window.history.state).deepEquals({a: 1}) + o(throttleMock.queueLength()).equals(0) }) o("route.Link can render without routes or dom access", () => { - $window = browserMock(env) - var route = apiRouter(null, null) - route.init(prefix) + restoreDOMGlobals() + m.route.init(prefix, "https://localhost/") - var enabled = route.link({href: "/test"}) + var enabled = m.route.link({href: "/test"}) o(Object.keys(enabled)).deepEquals(["href", "onclick"]) o(enabled.href).equals(`${prefix}/test`) o(typeof enabled.onclick).equals("function") - var disabled = route.link({disabled: true, href: "/test"}) + var disabled = m.route.link({disabled: true, href: "/test"}) o(disabled).deepEquals({disabled: true, "aria-disabled": "true"}) o(throttleMock.queueLength()).equals(0) }) - o("route.Link doesn't redraw on wrong button", () => { + o("route.Link doesn't redraw on wrong button", async () => { var e = $window.document.createEvent("MouseEvents") e.initEvent("click", true, true) e.button = 10 $window.location.href = `${prefix}/` - route.init(prefix) - mountRedraw.mount(root, () => { - if (route.path === "/") { - return m("a", route.link({href: "/test"})) - } else if (route.path === "/test") { + m.route.init(prefix) + m.mount(root, () => { + if (m.route.path === "/") { + return m("a", m.route.link({href: "/test"})) + } else if (m.route.path === "/test") { return m("div") } else { - throw new Error(`Unknown route: ${route.path}`) + throw new Error(`Unknown route: ${m.route.path}`) } }) @@ -379,28 +362,29 @@ o.spec("route", () => { root.firstChild.dispatchEvent(e) - return waitCycles(1).then(() => { - throttleMock.fire() - o($window.location.href).equals(fullPrefix) - o(throttleMock.queueLength()).equals(0) - }) + + await Promise.resolve() + throttleMock.fire() + + o($window.location.href).equals(fullPrefix) + o(throttleMock.queueLength()).equals(0) }) - o("route.Link doesn't redraw on preventDefault", () => { + o("route.Link doesn't redraw on preventDefault", async () => { var e = $window.document.createEvent("MouseEvents") e.initEvent("click", true, true) e.button = 0 $window.location.href = `${prefix}/` - route.init(prefix) - mountRedraw.mount(root, () => { - if (route.path === "/") { - return m("a", route.link({href: "/test", onclick(e) { e.preventDefault() }})) - } else if (route.path === "/test") { + m.route.init(prefix) + m.mount(root, () => { + if (m.route.path === "/") { + return m("a", m.route.link({href: "/test", onclick(e) { e.preventDefault() }})) + } else if (m.route.path === "/test") { return m("div") } else { - throw new Error(`Unknown route: ${route.path}`) + throw new Error(`Unknown route: ${m.route.path}`) } }) @@ -408,28 +392,28 @@ o.spec("route", () => { root.firstChild.dispatchEvent(e) - return waitCycles(1).then(() => { - throttleMock.fire() - o($window.location.href).equals(fullPrefix) - o(throttleMock.queueLength()).equals(0) - }) + await Promise.resolve() + throttleMock.fire() + + o($window.location.href).equals(fullPrefix) + o(throttleMock.queueLength()).equals(0) }) - o("route.Link ignores `return false`", () => { + o("route.Link ignores `return false`", async () => { var e = $window.document.createEvent("MouseEvents") e.initEvent("click", true, true) e.button = 0 $window.location.href = `${prefix}/` - route.init(prefix) - mountRedraw.mount(root, () => { - if (route.path === "/") { - return m("a", route.link({href: "/test", onclick: () => false})) - } else if (route.path === "/test") { + m.route.init(prefix) + m.mount(root, () => { + if (m.route.path === "/") { + return m("a", m.route.link({href: "/test", onclick: () => false})) + } else if (m.route.path === "/test") { return m("div") } else { - throw new Error(`Unknown route: ${route.path}`) + throw new Error(`Unknown route: ${m.route.path}`) } }) @@ -437,46 +421,50 @@ o.spec("route", () => { root.firstChild.dispatchEvent(e) - return waitCycles(1).then(() => { - throttleMock.fire() - o($window.location.href).equals(`${fullPrefix}test`) - o(throttleMock.queueLength()).equals(0) - }) + await Promise.resolve() + throttleMock.fire() + + o($window.location.href).equals(`${fullPrefix}test`) + o(throttleMock.queueLength()).equals(0) }) - o("m.route.set(m.route.get()) re-runs the resolution logic (#1180)", () => { + o("m.route.set(m.route.get()) re-runs the resolution logic (#1180)", async () => { var render = o.spy(() => m("div")) $window.location.href = `${prefix}/` - route.init(prefix) - mountRedraw.mount(root, render) + m.route.init(prefix) + m.mount(root, render) - return waitCycles(1).then(() => { - throttleMock.fire() - o(render.callCount).equals(1) + o(render.callCount).equals(1) - route.set(route.get()) + await Promise.resolve() + throttleMock.fire() - return waitCycles(2).then(() => { - throttleMock.fire() - o(render.callCount).equals(2) - o(throttleMock.queueLength()).equals(0) - }) - }) + o(render.callCount).equals(1) + + m.route.set(m.route.get()) + + await Promise.resolve() + throttleMock.fire() + await Promise.resolve() + throttleMock.fire() + + o(render.callCount).equals(2) + o(throttleMock.queueLength()).equals(0) }) o("throttles", () => { var i = 0 $window.location.href = `${prefix}/` - route.init(prefix) - mountRedraw.mount(root, () => { i++ }) + m.route.init(prefix) + m.mount(root, () => { i++ }) var before = i - mountRedraw.redraw() - mountRedraw.redraw() - mountRedraw.redraw() - mountRedraw.redraw() + m.redraw() + m.redraw() + m.redraw() + m.redraw() var after = i throttleMock.fire() diff --git a/tests/exported-api.js b/tests/exported-api.js index 80513fb4b..8822281a4 100644 --- a/tests/exported-api.js +++ b/tests/exported-api.js @@ -1,33 +1,21 @@ -/* global window: false, global: false */ import o from "ospec" +import {injectGlobals, register, restoreGlobalState} from "../test-utils/redraw-registry.js" + +import m from "../src/entry/mithril.esm.js" + import browserMock from "../test-utils/browserMock.js" +import throttleMocker from "../test-utils/throttleMock.js" o.spec("api", function() { - var FRAME_BUDGET = Math.floor(1000 / 60) - var root - - function sleep(ms) { - return new Promise((resolve) => setTimeout(resolve, ms)) - } - - var m - - o.before(async () => { - var mock = browserMock() - mock.setTimeout = setTimeout - if (typeof global !== "undefined") { - global.window = mock - global.requestAnimationFrame = mock.requestAnimationFrame - } - const mod = await import("../src/entry/mithril.esm.js") - m = mod.default - }) + var $window, throttleMock, root - o.afterEach(function() { - if (root) m.mount(root, null) + o.beforeEach(() => { + injectGlobals($window = browserMock(), throttleMock = throttleMocker()) }) + o.afterEach(restoreGlobalState) + o.spec("m", function() { o("works", function() { var vnode = m("div") @@ -39,7 +27,7 @@ o.spec("api", function() { o("works", function() { var vnode = m.normalize([m("div")]) - o(vnode.tag).equals("[") + o(vnode.tag).equals(Symbol.for("m.Fragment")) o(vnode.children.length).equals(1) o(vnode.children[0].tag).equals("div") }) @@ -48,7 +36,7 @@ o.spec("api", function() { o("works", function() { var vnode = m.key(123, [m("div")]) - o(vnode.tag).equals("=") + o(vnode.tag).equals(Symbol.for("m.key")) o(vnode.state).equals(123) o(vnode.children.length).equals(1) o(vnode.children[0].tag).equals("div") @@ -63,7 +51,7 @@ o.spec("api", function() { }) o.spec("m.render", function() { o("works", function() { - root = window.document.createElement("div") + root = register($window.document.createElement("div")) m.render(root, m("div")) o(root.childNodes.length).equals(1) @@ -73,7 +61,7 @@ o.spec("api", function() { o.spec("m.mount", function() { o("works", function() { - root = window.document.createElement("div") + root = register($window.document.createElement("div")) m.mount(root, () => m("div")) o(root.childNodes.length).equals(1) @@ -84,20 +72,19 @@ o.spec("api", function() { o.spec("m.redraw", function() { o("works", function() { var count = 0 - root = window.document.createElement("div") + root = register($window.document.createElement("div")) m.mount(root, () => {count++}) o(count).equals(1) m.redraw() o(count).equals(1) - return sleep(FRAME_BUDGET + 10).then(() => { - o(count).equals(2) - }) + throttleMock.fire() + o(count).equals(2) }) }) o.spec("m.redrawSync", function() { o("works", function() { - root = window.document.createElement("div") + root = register($window.document.createElement("div")) var view = o.spy() m.mount(root, view) o(view.callCount).equals(1) @@ -107,8 +94,8 @@ o.spec("api", function() { }) o.spec("m.route", function() { - o("works", function() { - root = window.document.createElement("div") + o("works", async() => { + root = register($window.document.createElement("div")) m.route.init("#") m.mount(root, () => { if (m.route.path === "/a") { @@ -120,15 +107,21 @@ o.spec("api", function() { } }) - return sleep(FRAME_BUDGET + 10) - .then(() => { - o(root.childNodes.length).equals(1) - o(root.firstChild.nodeName).equals("DIV") - o(m.route.get()).equals("/a") - }) - .then(() => { m.route.set("/b") }) - .then(() => sleep(FRAME_BUDGET + 10)) - .then(() => { o(m.route.get()).equals("/b") }) + await Promise.resolve() + throttleMock.fire() + o(throttleMock.queueLength()).equals(0) + + o(root.childNodes.length).equals(1) + o(root.firstChild.nodeName).equals("DIV") + o(m.route.get()).equals("/a") + + m.route.set("/b") + + await Promise.resolve() + throttleMock.fire() + o(throttleMock.queueLength()).equals(0) + + o(m.route.get()).equals("/b") }) }) }) diff --git a/tests/render/attributes.js b/tests/render/attributes.js index 39b1a7202..473016504 100644 --- a/tests/render/attributes.js +++ b/tests/render/attributes.js @@ -1,8 +1,7 @@ import o from "ospec" import domMock from "../../test-utils/domMock.js" -import m from "../../src/core/hyperscript.js" -import render from "../../src/core/render.js" +import m from "../../src/entry/mithril.esm.js" o.spec("attributes", function() { var $window, root @@ -17,15 +16,15 @@ o.spec("attributes", function() { var b = m("div", {id: "test"}) var c = m("div") - render(root, a); + m.render(root, a); o(a.dom.hasAttribute("id")).equals(false) - render(root, b); + m.render(root, b); o(b.dom.getAttribute("id")).equals("test") - render(root, c); + m.render(root, c); o(c.dom.hasAttribute("id")).equals(false) }) @@ -34,17 +33,17 @@ o.spec("attributes", function() { var b = m("div", {id: "test"}) var c = m("div", {id: undefined}) - render(root, a); + m.render(root, a); o(a.dom.hasAttribute("id")).equals(false) - render(root, b); + m.render(root, b); o(b.dom.hasAttribute("id")).equals(true) o(b.dom.getAttribute("id")).equals("test") // #1804 - render(root, c); + m.render(root, c); o(c.dom.hasAttribute("id")).equals(false) }) @@ -64,7 +63,7 @@ o.spec("attributes", function() { return el } - render(root, [ + m.render(root, [ m("input", {value: "hello"}), m("input", {value: "hello"}), m("input", {value: "hello"}), @@ -108,7 +107,7 @@ o.spec("attributes", function() { return el } - render(root, [ + m.render(root, [ m("input", {value: "hello"}), m("input", {value: "hello"}), m("input", {value: "hello"}), @@ -136,14 +135,14 @@ o.spec("attributes", function() { o("when input readonly is true, attribute is present", function() { var a = m("input", {readonly: true}) - render(root, a) + m.render(root, a) o(a.dom.attributes["readonly"].value).equals("") }) o("when input readonly is false, attribute is not present", function() { var a = m("input", {readonly: false}) - render(root, a) + m.render(root, a) o(a.dom.attributes["readonly"]).equals(undefined) }) @@ -152,7 +151,7 @@ o.spec("attributes", function() { o("when input checked is true, attribute is not present", function() { var a = m("input", {checked: true}) - render(root, a) + m.render(root, a) o(a.dom.checked).equals(true) o(a.dom.attributes["checked"]).equals(undefined) @@ -160,7 +159,7 @@ o.spec("attributes", function() { o("when input checked is false, attribute is not present", function() { var a = m("input", {checked: false}) - render(root, a) + m.render(root, a) o(a.dom.checked).equals(false) o(a.dom.attributes["checked"]).equals(undefined) @@ -169,12 +168,12 @@ o.spec("attributes", function() { var a = m("input", {checked: false}) var b = m("input", {checked: true}) - render(root, a) + m.render(root, a) a.dom.checked = true //setting the javascript property makes the value no longer track the state of the attribute a.dom.checked = false - render(root, b) + m.render(root, b) o(a.dom.checked).equals(true) o(a.dom.attributes["checked"]).equals(undefined) @@ -184,7 +183,7 @@ o.spec("attributes", function() { o("can be set as text", function() { var a = m("input", {value: "test"}) - render(root, a); + m.render(root, a); o(a.dom.value).equals("test") }) @@ -193,23 +192,23 @@ o.spec("attributes", function() { var b = m("input", {value: "test"}) var c = m("input") - render(root, a) + m.render(root, a) o(a.dom.value).equals("") - render(root, b) + m.render(root, b) o(a.dom.value).equals("test") // https://github.com/MithrilJS/mithril.js/issues/1804#issuecomment-304521235 - render(root, c) + m.render(root, c) o(a.dom.value).equals("") }) o("can be set as number", function() { var a = m("input", {value: 1}) - render(root, a); + m.render(root, a); o(a.dom.value).equals("1") }) @@ -218,17 +217,17 @@ o.spec("attributes", function() { var b = m("input", {value: "test"}) var c = m("input", {value: null}) - render(root, a); + m.render(root, a); o(a.dom.value).equals("") o(a.dom.getAttribute("value")).equals(null) - render(root, b); + m.render(root, b); o(b.dom.value).equals("test") o(b.dom.getAttribute("value")).equals(null) - render(root, c); + m.render(root, c); o(c.dom.value).equals("") o(c.dom.getAttribute("value")).equals(null) @@ -238,16 +237,16 @@ o.spec("attributes", function() { var b = m("input", {value: ""}) var c = m("input", {value: 0}) - render(root, a); + m.render(root, a); o(a.dom.value).equals("0") - render(root, b); + m.render(root, b); o(b.dom.value).equals("") // #1595 redux - render(root, c); + m.render(root, c); o(c.dom.value).equals("0") }) @@ -261,28 +260,28 @@ o.spec("attributes", function() { var d = m("input", {value: 1}) var e = m("input", {value: 2}) - render(root, a) + m.render(root, a) var spies = $window.__getSpies(a.dom) a.dom.focus() o(spies.valueSetter.callCount).equals(0) - render(root, b) + m.render(root, b) o(b.dom.value).equals("1") o(spies.valueSetter.callCount).equals(1) - render(root, c) + m.render(root, c) o(c.dom.value).equals("1") o(spies.valueSetter.callCount).equals(1) - render(root, d) + m.render(root, d) o(d.dom.value).equals("1") o(spies.valueSetter.callCount).equals(1) - render(root, e) + m.render(root, e) o(d.dom.value).equals("2") o(spies.valueSetter.callCount).equals(2) @@ -297,15 +296,15 @@ o.spec("attributes", function() { var b = m("input", {type: "text"}) var c = m("input") - render(root, a) + m.render(root, a) o(a.dom.getAttribute("type")).equals("radio") - render(root, b) + m.render(root, b) o(b.dom.getAttribute("type")).equals("text") - render(root, c) + m.render(root, c) o(c.dom.hasAttribute("type")).equals(false) }) @@ -315,12 +314,12 @@ o.spec("attributes", function() { var a = m("textarea", {value:"x"}) var b = m("textarea") - render(root, a) + m.render(root, a) o(a.dom.value).equals("x") // https://github.com/MithrilJS/mithril.js/issues/1804#issuecomment-304521235 - render(root, b) + m.render(root, b) o(b.dom.value).equals("") }) @@ -334,28 +333,28 @@ o.spec("attributes", function() { var d = m("textarea", {value: 1}) var e = m("textarea", {value: 2}) - render(root, a) + m.render(root, a) var spies = $window.__getSpies(a.dom) a.dom.focus() o(spies.valueSetter.callCount).equals(0) - render(root, b) + m.render(root, b) o(b.dom.value).equals("1") o(spies.valueSetter.callCount).equals(1) - render(root, c) + m.render(root, c) o(c.dom.value).equals("1") o(spies.valueSetter.callCount).equals(1) - render(root, d) + m.render(root, d) o(d.dom.value).equals("1") o(spies.valueSetter.callCount).equals(1) - render(root, e) + m.render(root, e) o(d.dom.value).equals("2") o(spies.valueSetter.callCount).equals(2) @@ -365,14 +364,14 @@ o.spec("attributes", function() { o("when link href is true, attribute is present", function() { var a = m("a", {href: true}) - render(root, a) + m.render(root, a) o(a.dom.attributes["href"]).notEquals(undefined) }) o("when link href is false, attribute is not present", function() { var a = m("a", {href: false}) - render(root, a) + m.render(root, a) o(a.dom.attributes["href"]).equals(undefined) }) @@ -381,7 +380,7 @@ o.spec("attributes", function() { o("uses attribute API", function() { var canvas = m("canvas", {width: "100%"}) - render(root, canvas) + m.render(root, canvas) o(canvas.dom.attributes["width"].value).equals("100%") o(canvas.dom.width).equals(100) @@ -391,7 +390,7 @@ o.spec("attributes", function() { o("when className is specified then it should be added as a class", function() { var a = m("svg", {className: "test"}) - render(root, a); + m.render(root, a); o(a.dom.attributes["class"].value).equals("test") }) @@ -400,7 +399,7 @@ o.spec("attributes", function() { var vnode = m("svg", {ns: "http://www.w3.org/2000/svg"}, m("a", {ns: "http://www.w3.org/2000/svg", "xlink:href": "javascript:;"}) ) - render(root, vnode) + m.render(root, vnode) o(vnode.dom.nodeName).equals("svg") o(vnode.dom.firstChild.attributes["href"].value).equals("javascript:;") @@ -409,7 +408,7 @@ o.spec("attributes", function() { vnode = m("svg", {ns: "http://www.w3.org/2000/svg"}, m("a", {ns: "http://www.w3.org/2000/svg"}) ) - render(root, vnode) + m.render(root, vnode) o(vnode.dom.nodeName).equals("svg") o("href" in vnode.dom.firstChild.attributes).equals(false) @@ -420,14 +419,14 @@ o.spec("attributes", function() { o("can be set as text", function() { var a = m("option", {value: "test"}) - render(root, a); + m.render(root, a); o(a.dom.value).equals("test") }) o("can be set as number", function() { var a = m("option", {value: 1}) - render(root, a); + m.render(root, a); o(a.dom.value).equals("1") }) @@ -436,17 +435,17 @@ o.spec("attributes", function() { var b = m("option", {value: "test"}) var c = m("option", {value: null}) - render(root, a); + m.render(root, a); o(a.dom.value).equals("") o(a.dom.hasAttribute("value")).equals(false) - render(root, b); + m.render(root, b); o(b.dom.value).equals("test") o(b.dom.getAttribute("value")).equals("test") - render(root, c); + m.render(root, c); o(c.dom.value).equals("") o(c.dom.hasAttribute("value")).equals(false) @@ -456,16 +455,16 @@ o.spec("attributes", function() { var b = m("option", {value: ""}, "") var c = m("option", {value: 0}, "") - render(root, a); + m.render(root, a); o(a.dom.value).equals("0") - render(root, b); + m.render(root, b); o(a.dom.value).equals("") // #1595 redux - render(root, c); + m.render(root, c); o(c.dom.value).equals("0") }) @@ -479,27 +478,27 @@ o.spec("attributes", function() { var d = m("option", {value: 1}) var e = m("option", {value: 2}) - render(root, a) + m.render(root, a) var spies = $window.__getSpies(a.dom) o(spies.valueSetter.callCount).equals(0) - render(root, b) + m.render(root, b) o(b.dom.value).equals("1") o(spies.valueSetter.callCount).equals(1) - render(root, c) + m.render(root, c) o(c.dom.value).equals("1") o(spies.valueSetter.callCount).equals(1) - render(root, d) + m.render(root, d) o(d.dom.value).equals("1") o(spies.valueSetter.callCount).equals(1) - render(root, e) + m.render(root, e) o(d.dom.value).equals("2") o(spies.valueSetter.callCount).equals(2) @@ -526,7 +525,7 @@ o.spec("attributes", function() { var select = m("select", {selectedIndex: 0}, m("option", {value: "1", selected: ""}) ) - render(root, select) + m.render(root, select) }) */ o("can be set as text", function() { @@ -534,17 +533,17 @@ o.spec("attributes", function() { var b = makeSelect("2") var c = makeSelect("a") - render(root, a) + m.render(root, a) o(a.dom.value).equals("1") o(a.dom.selectedIndex).equals(0) - render(root, b) + m.render(root, b) o(b.dom.value).equals("2") o(b.dom.selectedIndex).equals(1) - render(root, c) + m.render(root, c) o(c.dom.value).equals("a") o(c.dom.selectedIndex).equals(2) @@ -552,7 +551,7 @@ o.spec("attributes", function() { o("setting null unsets the value", function() { var a = makeSelect(null) - render(root, a) + m.render(root, a) o(a.dom.value).equals("") o(a.dom.selectedIndex).equals(-1) @@ -561,12 +560,12 @@ o.spec("attributes", function() { var a = makeSelect(1) var b = makeSelect(2) - render(root, a) + m.render(root, a) o(a.dom.value).equals("1") o(a.dom.selectedIndex).equals(0) - render(root, b) + m.render(root, b) o(b.dom.value).equals("2") o(b.dom.selectedIndex).equals(1) @@ -575,13 +574,13 @@ o.spec("attributes", function() { var a = makeSelect("") var b = makeSelect(0) - render(root, a) + m.render(root, a) a.dom.focus() o(a.dom.value).equals("") // #1595 redux - render(root, b) + m.render(root, b) o(b.dom.value).equals("0") }) @@ -590,18 +589,18 @@ o.spec("attributes", function() { var b = makeSelect(null) var c = makeSelect("") - render(root, a) + m.render(root, a) a.dom.focus() o(a.dom.value).equals("") o(a.dom.selectedIndex).equals(4) - render(root, b) + m.render(root, b) o(b.dom.value).equals("") o(b.dom.selectedIndex).equals(-1) - render(root, c) + m.render(root, c) o(c.dom.value).equals("") o(c.dom.selectedIndex).equals(4) @@ -615,24 +614,24 @@ o.spec("attributes", function() { var c = makeSelect(1) var d = makeSelect("2") - render(root, a) + m.render(root, a) var spies = $window.__getSpies(a.dom) a.dom.focus() o(spies.valueSetter.callCount).equals(0) o(a.dom.value).equals("1") - render(root, b) + m.render(root, b) o(spies.valueSetter.callCount).equals(0) o(b.dom.value).equals("1") - render(root, c) + m.render(root, c) o(spies.valueSetter.callCount).equals(0) o(c.dom.value).equals("1") - render(root, d) + m.render(root, d) o(spies.valueSetter.callCount).equals(1) o(d.dom.value).equals("2") @@ -644,7 +643,7 @@ o.spec("attributes", function() { var succeeded = false try { - render(root, div) + m.render(root, div) succeeded = true } @@ -657,7 +656,7 @@ o.spec("attributes", function() { var succeeded = false try { - render(root, div) + m.render(root, div) succeeded = true } @@ -669,10 +668,10 @@ o.spec("attributes", function() { o.spec("mutate attr object", function() { o("throw when reusing attrs object", function() { const attrs = {className: "on"} - render(root, {tag: "input", attrs}) + m.render(root, {tag: "input", attrs}) attrs.className = "off" - o(() => render(root, {tag: "input", attrs})).throws(Error) + o(() => m.render(root, {tag: "input", attrs})).throws(Error) }) }) }) diff --git a/tests/render/component.js b/tests/render/component.js index 00d0a5f6f..9b9886083 100644 --- a/tests/render/component.js +++ b/tests/render/component.js @@ -1,8 +1,7 @@ import o from "ospec" import domMock from "../../test-utils/domMock.js" -import m from "../../src/core/hyperscript.js" -import render from "../../src/core/render.js" +import m from "../../src/entry/mithril.esm.js" o.spec("component", function() { var $window, root @@ -16,7 +15,7 @@ o.spec("component", function() { var component = () => m("div", {id: "a"}, "b") var node = m(component) - render(root, node) + m.render(root, node) o(root.firstChild.nodeName).equals("DIV") o(root.firstChild.attributes["id"].value).equals("a") @@ -26,7 +25,7 @@ o.spec("component", function() { var component = (attrs) => m("div", attrs) var node = m(component, {id: "a"}, "b") - render(root, node) + m.render(root, node) o(root.firstChild.nodeName).equals("DIV") o(root.firstChild.attributes["id"].value).equals("a") @@ -34,8 +33,8 @@ o.spec("component", function() { }) o("updates", function() { var component = (attrs) => m("div", attrs) - render(root, [m(component, {id: "a"}, "b")]) - render(root, [m(component, {id: "c"}, "d")]) + m.render(root, [m(component, {id: "a"}, "b")]) + m.render(root, [m(component, {id: "c"}, "d")]) o(root.firstChild.nodeName).equals("DIV") o(root.firstChild.attributes["id"].value).equals("c") @@ -44,65 +43,65 @@ o.spec("component", function() { o("updates root from null", function() { var visible = false var component = () => (visible ? m("div") : null) - render(root, m(component)) + m.render(root, m(component)) visible = true - render(root, m(component)) + m.render(root, m(component)) o(root.firstChild.nodeName).equals("DIV") }) o("updates root from primitive", function() { var visible = false var component = () => (visible ? m("div") : false) - render(root, m(component)) + m.render(root, m(component)) visible = true - render(root, m(component)) + m.render(root, m(component)) o(root.firstChild.nodeName).equals("DIV") }) o("updates root to null", function() { var visible = true var component = () => (visible ? m("div") : null) - render(root, m(component)) + m.render(root, m(component)) visible = false - render(root, m(component)) + m.render(root, m(component)) o(root.childNodes.length).equals(0) }) o("updates root to primitive", function() { var visible = true var component = () => (visible ? m("div") : false) - render(root, m(component)) + m.render(root, m(component)) visible = false - render(root, m(component)) + m.render(root, m(component)) o(root.childNodes.length).equals(0) }) o("updates root from null to null", function() { var component = () => null - render(root, m(component)) - render(root, m(component)) + m.render(root, m(component)) + m.render(root, m(component)) o(root.childNodes.length).equals(0) }) o("removes", function() { var component = () => m("div") - render(root, [m.key(1, m(component)), m.key(2, m("div"))]) + m.render(root, [m.key(1, m(component)), m.key(2, m("div"))]) var div = m("div") - render(root, [m.key(2, div)]) + m.render(root, [m.key(2, div)]) o(root.childNodes.length).equals(1) o(root.firstChild).equals(div.dom) }) o("svg works when creating across component boundary", function() { var component = () => m("g") - render(root, m("svg", m(component))) + m.render(root, m("svg", m(component))) o(root.firstChild.firstChild.namespaceURI).equals("http://www.w3.org/2000/svg") }) o("svg works when updating across component boundary", function() { var component = () => m("g") - render(root, m("svg", m(component))) - render(root, m("svg", m(component))) + m.render(root, m("svg", m(component))) + m.render(root, m("svg", m(component))) o(root.firstChild.firstChild.namespaceURI).equals("http://www.w3.org/2000/svg") }) @@ -113,7 +112,7 @@ o.spec("component", function() { m("label"), m("input"), ] - render(root, m(component)) + m.render(root, m(component)) o(root.childNodes.length).equals(2) o(root.childNodes[0].nodeName).equals("LABEL") @@ -121,53 +120,53 @@ o.spec("component", function() { }) o("can return string", function() { var component = () => "a" - render(root, m(component)) + m.render(root, m(component)) o(root.firstChild.nodeType).equals(3) o(root.firstChild.nodeValue).equals("a") }) o("can return falsy string", function() { var component = () => "" - render(root, m(component)) + m.render(root, m(component)) o(root.firstChild.nodeType).equals(3) o(root.firstChild.nodeValue).equals("") }) o("can return number", function() { var component = () => 1 - render(root, m(component)) + m.render(root, m(component)) o(root.firstChild.nodeType).equals(3) o(root.firstChild.nodeValue).equals("1") }) o("can return falsy number", function() { var component = () => 0 - render(root, m(component)) + m.render(root, m(component)) o(root.firstChild.nodeType).equals(3) o(root.firstChild.nodeValue).equals("0") }) o("can return `true`", function() { var component = () => true - render(root, m(component)) + m.render(root, m(component)) o(root.childNodes.length).equals(0) }) o("can return `false`", function() { var component = () => false - render(root, m(component)) + m.render(root, m(component)) o(root.childNodes.length).equals(0) }) o("can return null", function() { var component = () => null - render(root, m(component)) + m.render(root, m(component)) o(root.childNodes.length).equals(0) }) o("can return undefined", function() { var component = () => undefined - render(root, m(component)) + m.render(root, m(component)) o(root.childNodes.length).equals(0) }) @@ -177,7 +176,7 @@ o.spec("component", function() { var component = () => vnode var vnode = m(component) try { - render(root, vnode) + m.render(root, vnode) } catch (e) { threw = true @@ -191,13 +190,13 @@ o.spec("component", function() { // A view that returns its vnode would otherwise trigger an infinite loop var threw = false var component = () => vnode - render(root, m(component)) + m.render(root, m(component)) o(root.childNodes.length).equals(0) var vnode = m(component) try { - render(root, m(component)) + m.render(root, m(component)) } catch (e) { threw = true @@ -212,8 +211,8 @@ o.spec("component", function() { m("label"), m("input"), ] - render(root, m(component)) - render(root, m(component)) + m.render(root, m(component)) + m.render(root, m(component)) o(root.childNodes.length).equals(2) o(root.childNodes[0].nodeName).equals("LABEL") @@ -221,16 +220,16 @@ o.spec("component", function() { }) o("can update when returning primitive", function() { var component = () => "a" - render(root, m(component)) - render(root, m(component)) + m.render(root, m(component)) + m.render(root, m(component)) o(root.firstChild.nodeType).equals(3) o(root.firstChild.nodeValue).equals("a") }) o("can update when returning null", function() { var component = () => null - render(root, m(component)) - render(root, m(component)) + m.render(root, m(component)) + m.render(root, m(component)) o(root.childNodes.length).equals(0) }) @@ -240,9 +239,9 @@ o.spec("component", function() { m("input"), ] var div = m("div") - render(root, [m.key(1, m(component)), m.key(2, div)]) + m.render(root, [m.key(1, m(component)), m.key(2, div)]) - render(root, [m.key(2, m("div"))]) + m.render(root, [m.key(2, m("div"))]) o(root.childNodes.length).equals(1) o(root.firstChild).equals(div.dom) @@ -250,9 +249,9 @@ o.spec("component", function() { o("can remove when returning primitive", function() { var component = () => "a" var div = m("div") - render(root, [m.key(1, m(component)), m.key(2, div)]) + m.render(root, [m.key(1, m(component)), m.key(2, div)]) - render(root, [m.key(2, m("div"))]) + m.render(root, [m.key(2, m("div"))]) o(root.childNodes.length).equals(1) o(root.firstChild).equals(div.dom) @@ -269,7 +268,7 @@ o.spec("component", function() { return () => m("div", {id: "a"}, "b") } - render(root, m(component)) + m.render(root, m(component)) o(called).equals(1) o(root.firstChild.nodeName).equals("DIV") @@ -286,7 +285,7 @@ o.spec("component", function() { return () => [m("div", {id: "a"}, "b")] } - render(root, m(component)) + m.render(root, m(component)) o(called).equals(1) o(root.firstChild.nodeName).equals("DIV") @@ -303,7 +302,7 @@ o.spec("component", function() { } } - render(root, m(component)) + m.render(root, m(component)) }) o("does not initialize on redraw", function() { var component = o.spy(() => () => m("div", {id: "a"}, "b")) @@ -312,8 +311,8 @@ o.spec("component", function() { return m(component) } - render(root, view()) - render(root, view()) + m.render(root, view()) + m.render(root, view()) o(component.callCount).equals(1) }) @@ -325,7 +324,7 @@ o.spec("component", function() { m("div", {id: "a"}, "b"), ] - render(root, m(component)) + m.render(root, m(component)) o(layoutSpy.callCount).equals(1) o(layoutSpy.args[0]).equals(root) @@ -344,8 +343,8 @@ o.spec("component", function() { m("div", {id: "a"}, "b"), ] - render(root, m(component)) - render(root, m(component)) + m.render(root, m(component)) + m.render(root, m(component)) o(layoutSpy.callCount).equals(2) o(layoutSpy.args[0]).equals(root) @@ -364,8 +363,8 @@ o.spec("component", function() { m("div", {id: "a"}, "b"), ] - render(root, m(component)) - render(root, null) + m.render(root, m(component)) + m.render(root, null) o(layoutSpy.callCount).equals(1) o(layoutSpy.args[1].aborted).equals(true) @@ -380,9 +379,9 @@ o.spec("component", function() { m("div", {id: "a"}, "b"), ] - render(root, m(component)) - render(root, m(component)) - render(root, null) + m.render(root, m(component)) + m.render(root, m(component)) + m.render(root, null) o(layoutSpy.callCount).equals(2) o(layoutSpy.args[1].aborted).equals(true) @@ -393,7 +392,7 @@ o.spec("component", function() { var onabort = o.spy() var layoutSpy = o.spy((_, signal) => { signal.onabort = onabort }) var component = () => m("div", {id: "a"}, m.layout(layoutSpy), "b") - render(root, m(component)) + m.render(root, m(component)) o(layoutSpy.callCount).equals(1) o(layoutSpy.args[0]).equals(root.firstChild) @@ -408,8 +407,8 @@ o.spec("component", function() { var onabort = o.spy() var layoutSpy = o.spy((_, signal) => { signal.onabort = onabort }) var component = () => m("div", {id: "a"}, m.layout(layoutSpy), "b") - render(root, m(component)) - render(root, m(component)) + m.render(root, m(component)) + m.render(root, m(component)) o(layoutSpy.callCount).equals(2) o(layoutSpy.args[0]).equals(root.firstChild) @@ -424,8 +423,8 @@ o.spec("component", function() { var onabort = o.spy() var layoutSpy = o.spy((_, signal) => { signal.onabort = onabort }) var component = () => m("div", {id: "a"}, m.layout(layoutSpy), "b") - render(root, m(component)) - render(root, null) + m.render(root, m(component)) + m.render(root, null) o(layoutSpy.callCount).equals(1) o(layoutSpy.args[1].aborted).equals(true) @@ -436,9 +435,9 @@ o.spec("component", function() { var onabort = o.spy() var layoutSpy = o.spy((_, signal) => { signal.onabort = onabort }) var component = () => m("div", {id: "a"}, m.layout(layoutSpy), "b") - render(root, m(component)) - render(root, m(component)) - render(root, null) + m.render(root, m(component)) + m.render(root, m(component)) + m.render(root, null) o(layoutSpy.callCount).equals(2) o(layoutSpy.args[1].aborted).equals(true) @@ -449,7 +448,7 @@ o.spec("component", function() { var onabort = o.spy() var layoutSpy = o.spy((_, signal) => { signal.onabort = onabort }) var component = () => m.layout(layoutSpy) - render(root, m(component)) + m.render(root, m(component)) o(layoutSpy.callCount).equals(1) o(layoutSpy.args[0]).equals(root) @@ -462,8 +461,8 @@ o.spec("component", function() { var onabort = o.spy() var layoutSpy = o.spy((_, signal) => { signal.onabort = onabort }) var component = () => m.layout(layoutSpy) - render(root, m(component)) - render(root, m(component)) + m.render(root, m(component)) + m.render(root, m(component)) o(layoutSpy.callCount).equals(2) o(layoutSpy.args[0]).equals(root) @@ -476,8 +475,8 @@ o.spec("component", function() { var onabort = o.spy() var layoutSpy = o.spy((_, signal) => { signal.onabort = onabort }) var component = () => m.layout(layoutSpy) - render(root, m(component)) - render(root, null) + m.render(root, m(component)) + m.render(root, null) o(layoutSpy.callCount).equals(1) o(layoutSpy.args[1].aborted).equals(true) @@ -488,9 +487,9 @@ o.spec("component", function() { var onabort = o.spy() var layoutSpy = o.spy((_, signal) => { signal.onabort = onabort }) var component = () => m.layout(layoutSpy) - render(root, m(component)) - render(root, m(component)) - render(root, null) + m.render(root, m(component)) + m.render(root, m(component)) + m.render(root, null) o(layoutSpy.callCount).equals(2) o(layoutSpy.args[1].aborted).equals(true) @@ -501,10 +500,10 @@ o.spec("component", function() { var layout = o.spy() var component = o.spy(() => m("div", m.layout(layout))) - render(root, [m("div", m.key(1, m(component)))]) + m.render(root, [m("div", m.key(1, m(component)))]) var child = root.firstChild.firstChild - render(root, []) - render(root, [m("div", m.key(1, m(component)))]) + m.render(root, []) + m.render(root, [m("div", m.key(1, m(component)))]) o(child).notEquals(root.firstChild.firstChild) // this used to be a recycling pool test o(component.callCount).equals(2) diff --git a/tests/render/createElement.js b/tests/render/createElement.js index 72d91cf39..2e3df0987 100644 --- a/tests/render/createElement.js +++ b/tests/render/createElement.js @@ -1,8 +1,7 @@ import o from "ospec" import domMock from "../../test-utils/domMock.js" -import m from "../../src/core/hyperscript.js" -import render from "../../src/core/render.js" +import m from "../../src/entry/mithril.esm.js" o.spec("createElement", function() { var $window, root @@ -13,13 +12,13 @@ o.spec("createElement", function() { o("creates element", function() { var vnode = m("div") - render(root, vnode) + m.render(root, vnode) o(vnode.dom.nodeName).equals("DIV") }) o("creates attr", function() { var vnode = m("div", {id: "a", title: "b"}) - render(root, vnode) + m.render(root, vnode) o(vnode.dom.nodeName).equals("DIV") o(vnode.dom.attributes["id"].value).equals("a") @@ -27,33 +26,33 @@ o.spec("createElement", function() { }) o("creates style", function() { var vnode = m("div", {style: {backgroundColor: "red"}}) - render(root, vnode) + m.render(root, vnode) o(vnode.dom.nodeName).equals("DIV") o(vnode.dom.style.backgroundColor).equals("red") }) o("allows css vars in style", function() { var vnode = m("div", {style: {"--css-var": "red"}}) - render(root, vnode) + m.render(root, vnode) o(vnode.dom.style["--css-var"]).equals("red") }) o("allows css vars in style with uppercase letters", function() { var vnode = m("div", {style: {"--cssVar": "red"}}) - render(root, vnode) + m.render(root, vnode) o(vnode.dom.style["--cssVar"]).equals("red") }) o("censors cssFloat to float", function() { var vnode = m("a", {style: {cssFloat: "left"}}) - render(root, vnode) + m.render(root, vnode) o(vnode.dom.style.float).equals("left") }) o("creates children", function() { var vnode = m("div", m("a"), m("b")) - render(root, vnode) + m.render(root, vnode) o(vnode.dom.nodeName).equals("DIV") o(vnode.dom.childNodes.length).equals(2) @@ -62,7 +61,7 @@ o.spec("createElement", function() { }) o("creates attrs and children", function() { var vnode = m("div", {id: "a", title: "b"}, m("a"), m("b")) - render(root, vnode) + m.render(root, vnode) o(vnode.dom.nodeName).equals("DIV") o(vnode.dom.attributes["id"].value).equals("a") @@ -77,7 +76,7 @@ o.spec("createElement", function() { m("a", {"xlink:href": "javascript:;"}), m("foreignObject", m("body", {xmlns: "http://www.w3.org/1999/xhtml"})) ) - render(root, vnode) + m.render(root, vnode) o(vnode.dom.nodeName).equals("svg") o(vnode.dom.namespaceURI).equals("http://www.w3.org/2000/svg") @@ -92,13 +91,13 @@ o.spec("createElement", function() { /* eslint-enable no-script-url */ o("sets attributes correctly for svg", function() { var vnode = m("svg", {viewBox: "0 0 100 100"}) - render(root, vnode) + m.render(root, vnode) o(vnode.dom.attributes["viewBox"].value).equals("0 0 100 100") }) o("creates mathml", function() { var vnode = m("math", m("mrow")) - render(root, vnode) + m.render(root, vnode) o(vnode.dom.nodeName).equals("math") o(vnode.dom.namespaceURI).equals("http://www.w3.org/1998/Math/MathML") diff --git a/tests/render/createFragment.js b/tests/render/createFragment.js index 38df643af..43026a2c2 100644 --- a/tests/render/createFragment.js +++ b/tests/render/createFragment.js @@ -1,8 +1,7 @@ import o from "ospec" import domMock from "../../test-utils/domMock.js" -import m from "../../src/core/hyperscript.js" -import render from "../../src/core/render.js" +import m from "../../src/entry/mithril.esm.js" o.spec("createFragment", function() { var $window, root @@ -13,26 +12,26 @@ o.spec("createFragment", function() { o("creates fragment", function() { var vnode = m.normalize([m("a")]) - render(root, vnode) + m.render(root, vnode) o(root.childNodes.length).equals(1) o(root.childNodes[0].nodeName).equals("A") }) o("handles empty fragment", function() { var vnode = m.normalize([]) - render(root, vnode) + m.render(root, vnode) o(root.childNodes.length).equals(0) }) o("handles childless fragment", function() { var vnode = m.normalize([]) - render(root, vnode) + m.render(root, vnode) o(root.childNodes.length).equals(0) }) o("handles multiple children", function() { var vnode = m.normalize([m("a"), m("b")]) - render(root, vnode) + m.render(root, vnode) o(root.childNodes.length).equals(2) o(root.childNodes[0].nodeName).equals("A") @@ -41,7 +40,7 @@ o.spec("createFragment", function() { }) o("handles td", function() { var vnode = m.normalize([m("td")]) - render(root, vnode) + m.render(root, vnode) o(root.childNodes.length).equals(1) o(root.childNodes[0].nodeName).equals("TD") diff --git a/tests/render/createNodes.js b/tests/render/createNodes.js index 1ae9d4b07..0f7b4a1d8 100644 --- a/tests/render/createNodes.js +++ b/tests/render/createNodes.js @@ -1,8 +1,7 @@ import o from "ospec" import domMock from "../../test-utils/domMock.js" -import m from "../../src/core/hyperscript.js" -import render from "../../src/core/render.js" +import m from "../../src/entry/mithril.esm.js" o.spec("createNodes", function() { var $window, root @@ -17,7 +16,7 @@ o.spec("createNodes", function() { "b", ["c"], ] - render(root, vnodes) + m.render(root, vnodes) o(root.childNodes.length).equals(3) o(root.childNodes[0].nodeName).equals("A") @@ -31,7 +30,7 @@ o.spec("createNodes", function() { null, ["c"], ] - render(root, vnodes) + m.render(root, vnodes) o(root.childNodes.length).equals(3) o(root.childNodes[0].nodeName).equals("A") @@ -45,7 +44,7 @@ o.spec("createNodes", function() { undefined, ["c"], ] - render(root, vnodes) + m.render(root, vnodes) o(root.childNodes.length).equals(3) o(root.childNodes[0].nodeName).equals("A") diff --git a/tests/render/createText.js b/tests/render/createText.js index dfcc06708..b7f92e001 100644 --- a/tests/render/createText.js +++ b/tests/render/createText.js @@ -1,7 +1,7 @@ import o from "ospec" import domMock from "../../test-utils/domMock.js" -import render from "../../src/core/render.js" +import m from "../../src/entry/mithril.esm.js" o.spec("createText", function() { var $window, root @@ -12,54 +12,54 @@ o.spec("createText", function() { o("creates string", function() { var vnode = "a" - render(root, vnode) + m.render(root, vnode) o(root.firstChild.nodeName).equals("#text") o(root.firstChild.nodeValue).equals("a") }) o("creates falsy string", function() { var vnode = "" - render(root, vnode) + m.render(root, vnode) o(root.firstChild.nodeName).equals("#text") o(root.firstChild.nodeValue).equals("") }) o("creates number", function() { var vnode = 1 - render(root, vnode) + m.render(root, vnode) o(root.firstChild.nodeName).equals("#text") o(root.firstChild.nodeValue).equals("1") }) o("creates falsy number", function() { var vnode = 0 - render(root, vnode) + m.render(root, vnode) o(root.firstChild.nodeName).equals("#text") o(root.firstChild.nodeValue).equals("0") }) o("ignores true boolean", function() { var vnode = true - render(root, vnode) + m.render(root, vnode) o(root.childNodes.length).equals(0) }) o("creates false boolean", function() { var vnode = false - render(root, vnode) + m.render(root, vnode) o(root.childNodes.length).equals(0) }) o("creates spaces", function() { var vnode = " " - render(root, vnode) + m.render(root, vnode) o(root.firstChild.nodeName).equals("#text") o(root.firstChild.nodeValue).equals(" ") }) o("ignores html", function() { var vnode = "™" - render(root, vnode) + m.render(root, vnode) o(root.firstChild.nodeName).equals("#text") o(root.firstChild.nodeValue).equals("™") diff --git a/tests/render/event.js b/tests/render/event.js index 877659eba..dda7c3af8 100644 --- a/tests/render/event.js +++ b/tests/render/event.js @@ -1,8 +1,7 @@ import o from "ospec" import domMock from "../../test-utils/domMock.js" -import m from "../../src/core/hyperscript.js" -import reallyRender from "../../src/core/render.js" +import m from "../../src/entry/mithril.esm.js" o.spec("event", function() { var $window, root, redraw, render @@ -11,7 +10,7 @@ o.spec("event", function() { root = $window.document.body redraw = o.spy() render = function(dom, vnode) { - return reallyRender(dom, vnode, redraw) + return m.render(dom, vnode, redraw) } }) @@ -289,7 +288,7 @@ o.spec("event", function() { o("handles changed spy", function() { var div1 = m("div", {ontransitionend: function() {}}) - reallyRender(root, [div1], redraw) + m.render(root, [div1], redraw) var e = $window.document.createEvent("HTMLEvents") e.initEvent("transitionend", true, true) div1.dom.dispatchEvent(e) @@ -301,7 +300,7 @@ o.spec("event", function() { var replacementRedraw = o.spy() var div2 = m("div", {ontransitionend: function() {}}) - reallyRender(root, [div2], replacementRedraw) + m.render(root, [div2], replacementRedraw) var e = $window.document.createEvent("HTMLEvents") e.initEvent("transitionend", true, true) div2.dom.dispatchEvent(e) diff --git a/tests/render/fragment.js b/tests/render/fragment.js index 8a761b4ee..72a1ecac7 100644 --- a/tests/render/fragment.js +++ b/tests/render/fragment.js @@ -1,13 +1,13 @@ import o from "ospec" -import m from "../../src/core/hyperscript.js" +import m from "../../src/entry/mithril.esm.js" o.spec("fragment literal", function() { o("works", function() { var child = m("p") var frag = m.normalize([child]) - o(frag.tag).equals("[") + o(frag.tag).equals(Symbol.for("m.Fragment")) o(Array.isArray(frag.children)).equals(true) o(frag.children.length).equals(1) @@ -17,26 +17,26 @@ o.spec("fragment literal", function() { o("handles string single child", function() { var vnode = m.normalize(["a"]) - o(vnode.children[0].tag).equals("#") - o(vnode.children[0].children).equals("a") + o(vnode.children[0].tag).equals(Symbol.for("m.text")) + o(vnode.children[0].state).equals("a") }) o("handles falsy string single child", function() { var vnode = m.normalize([""]) - o(vnode.children[0].tag).equals("#") - o(vnode.children[0].children).equals("") + o(vnode.children[0].tag).equals(Symbol.for("m.text")) + o(vnode.children[0].state).equals("") }) o("handles number single child", function() { var vnode = m.normalize([1]) - o(vnode.children[0].tag).equals("#") - o(vnode.children[0].children).equals("1") + o(vnode.children[0].tag).equals(Symbol.for("m.text")) + o(vnode.children[0].state).equals("1") }) o("handles falsy number single child", function() { var vnode = m.normalize([0]) - o(vnode.children[0].tag).equals("#") - o(vnode.children[0].children).equals("0") + o(vnode.children[0].tag).equals(Symbol.for("m.text")) + o(vnode.children[0].state).equals("0") }) o("handles boolean single child", function() { var vnode = m.normalize([true]) @@ -61,18 +61,18 @@ o.spec("fragment literal", function() { o("handles multiple string children", function() { var vnode = m.normalize(["", "a"]) - o(vnode.children[0].tag).equals("#") - o(vnode.children[0].children).equals("") - o(vnode.children[1].tag).equals("#") - o(vnode.children[1].children).equals("a") + o(vnode.children[0].tag).equals(Symbol.for("m.text")) + o(vnode.children[0].state).equals("") + o(vnode.children[1].tag).equals(Symbol.for("m.text")) + o(vnode.children[1].state).equals("a") }) o("handles multiple number children", function() { var vnode = m.normalize([0, 1]) - o(vnode.children[0].tag).equals("#") - o(vnode.children[0].children).equals("0") - o(vnode.children[1].tag).equals("#") - o(vnode.children[1].children).equals("1") + o(vnode.children[0].tag).equals(Symbol.for("m.text")) + o(vnode.children[0].state).equals("0") + o(vnode.children[1].tag).equals(Symbol.for("m.text")) + o(vnode.children[1].state).equals("1") }) o("handles multiple boolean children", function() { var vnode = m.normalize([false, true]) @@ -92,7 +92,7 @@ o.spec("fragment component", function() { var child = m("p") var frag = m(m.Fragment, null, child) - o(frag.tag).equals("[") + o(frag.tag).equals(Symbol.for("m.Fragment")) o(Array.isArray(frag.children)).equals(true) o(frag.children.length).equals(1) @@ -102,26 +102,26 @@ o.spec("fragment component", function() { o("handles string single child", function() { var vnode = m(m.Fragment, null, ["a"]) - o(vnode.children[0].tag).equals("#") - o(vnode.children[0].children).equals("a") + o(vnode.children[0].tag).equals(Symbol.for("m.text")) + o(vnode.children[0].state).equals("a") }) o("handles falsy string single child", function() { var vnode = m(m.Fragment, null, [""]) - o(vnode.children[0].tag).equals("#") - o(vnode.children[0].children).equals("") + o(vnode.children[0].tag).equals(Symbol.for("m.text")) + o(vnode.children[0].state).equals("") }) o("handles number single child", function() { var vnode = m(m.Fragment, null, [1]) - o(vnode.children[0].tag).equals("#") - o(vnode.children[0].children).equals("1") + o(vnode.children[0].tag).equals(Symbol.for("m.text")) + o(vnode.children[0].state).equals("1") }) o("handles falsy number single child", function() { var vnode = m(m.Fragment, null, [0]) - o(vnode.children[0].tag).equals("#") - o(vnode.children[0].children).equals("0") + o(vnode.children[0].tag).equals(Symbol.for("m.text")) + o(vnode.children[0].state).equals("0") }) o("handles boolean single child", function() { var vnode = m(m.Fragment, null, [true]) @@ -146,18 +146,18 @@ o.spec("fragment component", function() { o("handles multiple string children", function() { var vnode = m(m.Fragment, null, ["", "a"]) - o(vnode.children[0].tag).equals("#") - o(vnode.children[0].children).equals("") - o(vnode.children[1].tag).equals("#") - o(vnode.children[1].children).equals("a") + o(vnode.children[0].tag).equals(Symbol.for("m.text")) + o(vnode.children[0].state).equals("") + o(vnode.children[1].tag).equals(Symbol.for("m.text")) + o(vnode.children[1].state).equals("a") }) o("handles multiple number children", function() { var vnode = m(m.Fragment, null, [0, 1]) - o(vnode.children[0].tag).equals("#") - o(vnode.children[0].children).equals("0") - o(vnode.children[1].tag).equals("#") - o(vnode.children[1].children).equals("1") + o(vnode.children[0].tag).equals(Symbol.for("m.text")) + o(vnode.children[0].state).equals("0") + o(vnode.children[1].tag).equals(Symbol.for("m.text")) + o(vnode.children[1].state).equals("1") }) o("handles multiple boolean children", function() { var vnode = m(m.Fragment, null, [false, true]) @@ -172,8 +172,8 @@ o.spec("fragment component", function() { o("handles falsy number single child without attrs", function() { var vnode = m(m.Fragment, null, 0) - o(vnode.children[0].tag).equals("#") - o(vnode.children[0].children).equals("0") + o(vnode.children[0].tag).equals(Symbol.for("m.text")) + o(vnode.children[0].state).equals("0") }) }) }) @@ -183,7 +183,7 @@ o.spec("key", function() { var child = m("p") var frag = m.key(undefined, child) - o(frag.tag).equals("=") + o(frag.tag).equals(Symbol.for("m.key")) o(Array.isArray(frag.children)).equals(true) o(frag.children.length).equals(1) @@ -193,7 +193,7 @@ o.spec("key", function() { }) o("supports non-null keys", function() { var frag = m.key(7, []) - o(frag.tag).equals("=") + o(frag.tag).equals(Symbol.for("m.key")) o(Array.isArray(frag.children)).equals(true) o(frag.children.length).equals(0) @@ -204,26 +204,26 @@ o.spec("key", function() { o("handles string single child", function() { var vnode = m.key("foo", ["a"]) - o(vnode.children[0].tag).equals("#") - o(vnode.children[0].children).equals("a") + o(vnode.children[0].tag).equals(Symbol.for("m.text")) + o(vnode.children[0].state).equals("a") }) o("handles falsy string single child", function() { var vnode = m.key("foo", [""]) - o(vnode.children[0].tag).equals("#") - o(vnode.children[0].children).equals("") + o(vnode.children[0].tag).equals(Symbol.for("m.text")) + o(vnode.children[0].state).equals("") }) o("handles number single child", function() { var vnode = m.key("foo", [1]) - o(vnode.children[0].tag).equals("#") - o(vnode.children[0].children).equals("1") + o(vnode.children[0].tag).equals(Symbol.for("m.text")) + o(vnode.children[0].state).equals("1") }) o("handles falsy number single child", function() { var vnode = m.key("foo", [0]) - o(vnode.children[0].tag).equals("#") - o(vnode.children[0].children).equals("0") + o(vnode.children[0].tag).equals(Symbol.for("m.text")) + o(vnode.children[0].state).equals("0") }) o("handles boolean single child", function() { var vnode = m.key("foo", [true]) @@ -248,18 +248,18 @@ o.spec("key", function() { o("handles multiple string children", function() { var vnode = m.key("foo", ["", "a"]) - o(vnode.children[0].tag).equals("#") - o(vnode.children[0].children).equals("") - o(vnode.children[1].tag).equals("#") - o(vnode.children[1].children).equals("a") + o(vnode.children[0].tag).equals(Symbol.for("m.text")) + o(vnode.children[0].state).equals("") + o(vnode.children[1].tag).equals(Symbol.for("m.text")) + o(vnode.children[1].state).equals("a") }) o("handles multiple number children", function() { var vnode = m.key("foo", [0, 1]) - o(vnode.children[0].tag).equals("#") - o(vnode.children[0].children).equals("0") - o(vnode.children[1].tag).equals("#") - o(vnode.children[1].children).equals("1") + o(vnode.children[0].tag).equals(Symbol.for("m.text")) + o(vnode.children[0].state).equals("0") + o(vnode.children[1].tag).equals(Symbol.for("m.text")) + o(vnode.children[1].state).equals("1") }) o("handles multiple boolean children", function() { var vnode = m.key("foo", [false, true]) @@ -274,8 +274,8 @@ o.spec("key", function() { o("handles falsy number single child without attrs", function() { var vnode = m.key("foo", 0) - o(vnode.children[0].tag).equals("#") - o(vnode.children[0].children).equals("0") + o(vnode.children[0].tag).equals(Symbol.for("m.text")) + o(vnode.children[0].state).equals("0") }) }) }) diff --git a/tests/render/hyperscript.js b/tests/render/hyperscript.js index a8f1da354..78a36d203 100644 --- a/tests/render/hyperscript.js +++ b/tests/render/hyperscript.js @@ -1,7 +1,7 @@ import o from "ospec" import domMock from "../../test-utils/domMock.js" -import m from "../../src/core/hyperscript.js" +import m from "../../src/entry/mithril.esm.js" o.spec("hyperscript", function() { o.spec("selector", function() { @@ -370,22 +370,22 @@ o.spec("hyperscript", function() { o("handles string single child", function() { var vnode = m("div", {}, ["a"]) - o(vnode.children[0].children).equals("a") + o(vnode.children[0].state).equals("a") }) o("handles falsy string single child", function() { var vnode = m("div", {}, [""]) - o(vnode.children[0].children).equals("") + o(vnode.children[0].state).equals("") }) o("handles number single child", function() { var vnode = m("div", {}, [1]) - o(vnode.children[0].children).equals("1") + o(vnode.children[0].state).equals("1") }) o("handles falsy number single child", function() { var vnode = m("div", {}, [0]) - o(vnode.children[0].children).equals("0") + o(vnode.children[0].state).equals("0") }) o("handles boolean single child", function() { var vnode = m("div", {}, [true]) @@ -410,18 +410,18 @@ o.spec("hyperscript", function() { o("handles multiple string children", function() { var vnode = m("div", {}, ["", "a"]) - o(vnode.children[0].tag).equals("#") - o(vnode.children[0].children).equals("") - o(vnode.children[1].tag).equals("#") - o(vnode.children[1].children).equals("a") + o(vnode.children[0].tag).equals(Symbol.for("m.text")) + o(vnode.children[0].state).equals("") + o(vnode.children[1].tag).equals(Symbol.for("m.text")) + o(vnode.children[1].state).equals("a") }) o("handles multiple number children", function() { var vnode = m("div", {}, [0, 1]) - o(vnode.children[0].tag).equals("#") - o(vnode.children[0].children).equals("0") - o(vnode.children[1].tag).equals("#") - o(vnode.children[1].children).equals("1") + o(vnode.children[0].tag).equals(Symbol.for("m.text")) + o(vnode.children[0].state).equals("0") + o(vnode.children[1].tag).equals(Symbol.for("m.text")) + o(vnode.children[1].state).equals("1") }) o("handles multiple boolean children", function() { var vnode = m("div", {}, [false, true]) @@ -436,15 +436,15 @@ o.spec("hyperscript", function() { o("handles falsy number single child without attrs", function() { var vnode = m("div", 0) - o(vnode.children[0].children).equals("0") + o(vnode.children[0].state).equals("0") }) o("handles children in attributes", function() { var vnode = m("div", {children: ["", "a"]}) - o(vnode.children[0].tag).equals("#") - o(vnode.children[0].children).equals("") - o(vnode.children[1].tag).equals("#") - o(vnode.children[1].children).equals("a") + o(vnode.children[0].tag).equals(Symbol.for("m.text")) + o(vnode.children[0].state).equals("") + o(vnode.children[1].tag).equals(Symbol.for("m.text")) + o(vnode.children[1].state).equals("a") }) }) o.spec("permutations", function() { @@ -492,34 +492,34 @@ o.spec("hyperscript", function() { var vnode = m("div", {a: "b"}, ["c", "d"]) o(vnode.attrs.a).equals("b") - o(vnode.children[0].tag).equals("#") - o(vnode.children[0].children).equals("c") - o(vnode.children[1].tag).equals("#") - o(vnode.children[1].children).equals("d") + o(vnode.children[0].tag).equals(Symbol.for("m.text")) + o(vnode.children[0].state).equals("c") + o(vnode.children[1].tag).equals(Symbol.for("m.text")) + o(vnode.children[1].state).equals("d") }) o("handles attr and single string text child", function() { var vnode = m("div", {a: "b"}, ["c"]) o(vnode.attrs.a).equals("b") - o(vnode.children[0].children).equals("c") + o(vnode.children[0].state).equals("c") }) o("handles attr and single falsy string text child", function() { var vnode = m("div", {a: "b"}, [""]) o(vnode.attrs.a).equals("b") - o(vnode.children[0].children).equals("") + o(vnode.children[0].state).equals("") }) o("handles attr and single number text child", function() { var vnode = m("div", {a: "b"}, [1]) o(vnode.attrs.a).equals("b") - o(vnode.children[0].children).equals("1") + o(vnode.children[0].state).equals("1") }) o("handles attr and single falsy number text child", function() { var vnode = m("div", {a: "b"}, [0]) o(vnode.attrs.a).equals("b") - o(vnode.children[0].children).equals("0") + o(vnode.children[0].state).equals("0") }) o("handles attr and single boolean text child", function() { var vnode = m("div", {a: "b"}, [true]) @@ -531,7 +531,7 @@ o.spec("hyperscript", function() { var vnode = m("div", {a: "b"}, [0]) o(vnode.attrs.a).equals("b") - o(vnode.children[0].children).equals("0") + o(vnode.children[0].state).equals("0") }) o("handles attr and single false boolean text child", function() { var vnode = m("div", {a: "b"}, [false]) @@ -543,16 +543,16 @@ o.spec("hyperscript", function() { var vnode = m("div", {a: "b"}, "c") o(vnode.attrs.a).equals("b") - o(vnode.children[0].children).equals("c") + o(vnode.children[0].state).equals("c") }) o("handles attr and text children unwrapped", function() { var vnode = m("div", {a: "b"}, "c", "d") o(vnode.attrs.a).equals("b") - o(vnode.children[0].tag).equals("#") - o(vnode.children[0].children).equals("c") - o(vnode.children[1].tag).equals("#") - o(vnode.children[1].children).equals("d") + o(vnode.children[0].tag).equals(Symbol.for("m.text")) + o(vnode.children[0].state).equals("c") + o(vnode.children[1].tag).equals(Symbol.for("m.text")) + o(vnode.children[1].state).equals("d") }) o("handles children without attr", function() { var vnode = m("div", [m("i"), m("s")]) @@ -612,23 +612,23 @@ o.spec("hyperscript", function() { o("handles fragment children without attr unwrapped", function() { var vnode = m("div", [m("i")], [m("s")]) - o(vnode.children[0].tag).equals("[") + o(vnode.children[0].tag).equals(Symbol.for("m.Fragment")) o(vnode.children[0].children[0].tag).equals("i") - o(vnode.children[1].tag).equals("[") + o(vnode.children[1].tag).equals(Symbol.for("m.Fragment")) o(vnode.children[1].children[0].tag).equals("s") }) o("handles children with nested array", function() { var vnode = m("div", [[m("i"), m("s")]]) - o(vnode.children[0].tag).equals("[") + o(vnode.children[0].tag).equals(Symbol.for("m.Fragment")) o(vnode.children[0].children[0].tag).equals("i") o(vnode.children[0].children[1].tag).equals("s") }) o("handles children with deeply nested array", function() { var vnode = m("div", [[[m("i"), m("s")]]]) - o(vnode.children[0].tag).equals("[") - o(vnode.children[0].children[0].tag).equals("[") + o(vnode.children[0].tag).equals(Symbol.for("m.Fragment")) + o(vnode.children[0].children[0].tag).equals(Symbol.for("m.Fragment")) o(vnode.children[0].children[0].children[0].tag).equals("i") o(vnode.children[0].children[0].children[1].tag).equals("s") }) diff --git a/tests/render/input.js b/tests/render/input.js index a46525e0f..ef6fedcbb 100644 --- a/tests/render/input.js +++ b/tests/render/input.js @@ -1,8 +1,7 @@ import o from "ospec" import domMock from "../../test-utils/domMock.js" -import m from "../../src/core/hyperscript.js" -import render from "../../src/core/render.js" +import m from "../../src/entry/mithril.esm.js" o.spec("form inputs", function() { var $window, root @@ -20,9 +19,9 @@ o.spec("form inputs", function() { o("maintains focus after move", function() { var input - render(root, [m.key(1, input = m("input")), m.key(2, m("a")), m.key(3, m("b"))]) + m.render(root, [m.key(1, input = m("input")), m.key(2, m("a")), m.key(3, m("b"))]) input.dom.focus() - render(root, [m.key(2, m("a")), m.key(1, input = m("input")), m.key(3, m("b"))]) + m.render(root, [m.key(2, m("a")), m.key(1, input = m("input")), m.key(3, m("b"))]) o($window.document.activeElement).equals(input.dom) }) @@ -32,7 +31,7 @@ o.spec("form inputs", function() { dom.focus() })); - render(root, input) + m.render(root, input) o($window.document.activeElement).equals(input.dom) }) @@ -42,7 +41,7 @@ o.spec("form inputs", function() { var updated = m("input", {value: "aaa", oninput: function() {}}) var redraw = o.spy() - render(root, input, redraw) + m.render(root, input, redraw) //simulate user typing var e = $window.document.createEvent("KeyboardEvent") @@ -53,7 +52,7 @@ o.spec("form inputs", function() { o(redraw.callCount).equals(1) //re-render may use same vdom value as previous render call - render(root, updated, redraw) + m.render(root, updated, redraw) o(updated.dom.value).equals("aaa") o(redraw.callCount).equals(1) @@ -63,8 +62,8 @@ o.spec("form inputs", function() { var input = m("input", {value: "aaa", oninput: function() {}}) var updated = m("input", {value: undefined, oninput: function() {}}) - render(root, input) - render(root, updated) + m.render(root, input) + m.render(root, updated) o(updated.dom.value).equals("") }) @@ -74,7 +73,7 @@ o.spec("form inputs", function() { var updated = m("input", {type: "checkbox", checked: true, onclick: function() {}}) var redraw = o.spy() - render(root, input, redraw) + m.render(root, input, redraw) //simulate user clicking checkbox var e = $window.document.createEvent("MouseEvents") @@ -84,7 +83,7 @@ o.spec("form inputs", function() { o(redraw.callCount).equals(1) //re-render may use same vdom value as previous render call - render(root, updated, redraw) + m.render(root, updated, redraw) o(updated.dom.checked).equals(true) o(redraw.callCount).equals(1) @@ -96,13 +95,13 @@ o.spec("form inputs", function() { var spy = o.spy() var error = console.error - render(root, input) + m.render(root, input) input.dom.value = "test.png" try { console.error = spy - render(root, updated) + m.render(root, updated) } finally { console.error = error } @@ -117,13 +116,13 @@ o.spec("form inputs", function() { var spy = o.spy() var error = console.error - render(root, input) + m.render(root, input) input.dom.value = "test.png" try { console.error = spy - render(root, updated) + m.render(root, updated) } finally { console.error = error } @@ -142,7 +141,7 @@ o.spec("form inputs", function() { var spy = o.spy() var error = console.error - render(root, input) + m.render(root, input) // Verify our assumptions about the outer element state o($window.__getSpies(input.dom).valueSetter.callCount).equals(0) @@ -151,7 +150,7 @@ o.spec("form inputs", function() { try { console.error = spy - render(root, updated1) + m.render(root, updated1) } finally { console.error = error } @@ -162,7 +161,7 @@ o.spec("form inputs", function() { try { console.error = spy - render(root, updated2) + m.render(root, updated2) } finally { console.error = error } @@ -179,7 +178,7 @@ o.spec("form inputs", function() { m("option", {value: "a"}, "aaa") ) - render(root, select) + m.render(root, select) o(select.dom.value).equals("a") o(select.dom.selectedIndex).equals(0) @@ -190,7 +189,7 @@ o.spec("form inputs", function() { m("option", {value: ""}, "aaa") ) - render(root, select) + m.render(root, select) o(select.dom.firstChild.value).equals("") }) @@ -200,7 +199,7 @@ o.spec("form inputs", function() { m("option", "aaa") ) - render(root, select) + m.render(root, select) o(select.dom.firstChild.value).equals("aaa") o(select.dom.value).equals("aaa") @@ -210,7 +209,7 @@ o.spec("form inputs", function() { m("option", "bbb") ) - render(root, select) + m.render(root, select) o(select.dom.firstChild.value).equals("bbb") o(select.dom.value).equals("bbb") @@ -220,7 +219,7 @@ o.spec("form inputs", function() { m("option", {value: ""}, "aaa") ) - render(root, select) + m.render(root, select) o(select.dom.firstChild.value).equals("") o(select.dom.value).equals("") @@ -230,7 +229,7 @@ o.spec("form inputs", function() { m("option", "aaa") ) - render(root, select) + m.render(root, select) o(select.dom.firstChild.value).equals("aaa") o(select.dom.value).equals("aaa") @@ -239,7 +238,7 @@ o.spec("form inputs", function() { o("select yields invalid value without children", function() { var select = m("select", {value: "a"}) - render(root, select) + m.render(root, select) o(select.dom.value).equals("") o(select.dom.selectedIndex).equals(-1) @@ -252,7 +251,7 @@ o.spec("form inputs", function() { m("option", {value: "c"}, "ccc") ) - render(root, select) + m.render(root, select) o(select.dom.value).equals("b") o(select.dom.selectedIndex).equals(1) @@ -267,14 +266,14 @@ o.spec("form inputs", function() { ) } - render(root, makeSelect()) + m.render(root, makeSelect()) //simulate user selecting option root.firstChild.value = "c" root.firstChild.focus() //re-render may use same vdom value as previous render call - render(root, makeSelect()) + m.render(root, makeSelect()) o(root.firstChild.value).equals("b") o(root.firstChild.selectedIndex).equals(1) diff --git a/tests/render/normalize.js b/tests/render/normalize.js index e05b3bc91..e1bbed4c6 100644 --- a/tests/render/normalize.js +++ b/tests/render/normalize.js @@ -1,45 +1,45 @@ import o from "ospec" -import m from "../../src/core/hyperscript.js" +import m from "../../src/entry/mithril.esm.js" o.spec("normalize", function() { o("normalizes array into fragment", function() { var node = m.normalize([]) - o(node.tag).equals("[") + o(node.tag).equals(Symbol.for("m.Fragment")) o(node.children.length).equals(0) }) o("normalizes nested array into fragment", function() { var node = m.normalize([[]]) - o(node.tag).equals("[") + o(node.tag).equals(Symbol.for("m.Fragment")) o(node.children.length).equals(1) - o(node.children[0].tag).equals("[") + o(node.children[0].tag).equals(Symbol.for("m.Fragment")) o(node.children[0].children.length).equals(0) }) o("normalizes string into text node", function() { var node = m.normalize("a") - o(node.tag).equals("#") - o(node.children).equals("a") + o(node.tag).equals(Symbol.for("m.text")) + o(node.state).equals("a") }) o("normalizes falsy string into text node", function() { var node = m.normalize("") - o(node.tag).equals("#") - o(node.children).equals("") + o(node.tag).equals(Symbol.for("m.text")) + o(node.state).equals("") }) o("normalizes number into text node", function() { var node = m.normalize(1) - o(node.tag).equals("#") - o(node.children).equals("1") + o(node.tag).equals(Symbol.for("m.text")) + o(node.state).equals("1") }) o("normalizes falsy number into text node", function() { var node = m.normalize(0) - o(node.tag).equals("#") - o(node.children).equals("0") + o(node.tag).equals(Symbol.for("m.text")) + o(node.state).equals("0") }) o("normalizes `true` to `null`", function() { var node = m.normalize(true) diff --git a/tests/render/normalizeChildren.js b/tests/render/normalizeChildren.js index df57ac83f..41bca0477 100644 --- a/tests/render/normalizeChildren.js +++ b/tests/render/normalizeChildren.js @@ -1,27 +1,27 @@ import o from "ospec" -import m from "../../src/core/hyperscript.js" +import m from "../../src/entry/mithril.esm.js" o.spec("normalizeChildren", function() { o("normalizes arrays into fragments", function() { - var children = m.normalizeChildren([[]]) + var {children} = m.normalize([[]]) - o(children[0].tag).equals("[") + o(children[0].tag).equals(Symbol.for("m.Fragment")) o(children[0].children.length).equals(0) }) o("normalizes strings into text nodes", function() { - var children = m.normalizeChildren(["a"]) + var {children} = m.normalize(["a"]) - o(children[0].tag).equals("#") - o(children[0].children).equals("a") + o(children[0].tag).equals(Symbol.for("m.text")) + o(children[0].state).equals("a") }) o("normalizes `false` values into `null`s", function() { - var children = m.normalizeChildren([false]) + var {children} = m.normalize([false]) o(children[0]).equals(null) }) o("allows all keys", function() { - var children = m.normalizeChildren([ + var {children} = m.normalize([ m.key(1), m.key(2), ]) @@ -29,7 +29,7 @@ o.spec("normalizeChildren", function() { o(children).deepEquals([m.key(1), m.key(2)]) }) o("allows no keys", function() { - var children = m.normalizeChildren([ + var {children} = m.normalize([ m("foo1"), m("foo2"), ]) @@ -37,19 +37,15 @@ o.spec("normalizeChildren", function() { o(children).deepEquals([m("foo1"), m("foo2")]) }) o("disallows mixed keys, starting with key", function() { - o(function() { - m.normalizeChildren([ - m.key(1), - m("foo2"), - ]) - }).throws(TypeError) + o(() => m.normalize([ + m.key(1), + m("foo2"), + ])).throws(TypeError) }) o("disallows mixed keys, starting with no key", function() { - o(function() { - m.normalizeChildren([ - m("foo1"), - m.key(2), - ]) - }).throws(TypeError) + o(() => m.normalize([ + m("foo1"), + m.key(2), + ])).throws(TypeError) }) }) diff --git a/tests/render/normalizeComponentChildren.js b/tests/render/normalizeComponentChildren.js index 682b6b6a9..959cbab09 100644 --- a/tests/render/normalizeComponentChildren.js +++ b/tests/render/normalizeComponentChildren.js @@ -1,8 +1,7 @@ import o from "ospec" import domMock from "../../test-utils/domMock.js" -import m from "../../src/core/hyperscript.js" -import render from "../../src/core/render.js" +import m from "../../src/entry/mithril.esm.js" o.spec("component children", function () { var $window = domMock() @@ -13,16 +12,16 @@ o.spec("component children", function () { var vnode = m(component, "a") - render(root, vnode) + m.render(root, vnode) o("are not normalized on ingestion", function () { o(vnode.attrs.children[0]).equals("a") }) o("are normalized upon view interpolation", function () { - o(vnode.instance.children.length).equals(1) - o(vnode.instance.children[0].tag).equals("#") - o(vnode.instance.children[0].children).equals("a") + o(vnode.children.children.length).equals(1) + o(vnode.children.children[0].tag).equals(Symbol.for("m.text")) + o(vnode.children.children[0].state).equals("a") }) }) }) diff --git a/tests/render/oncreate.js b/tests/render/oncreate.js index fe1bfd4e0..49e6f6125 100644 --- a/tests/render/oncreate.js +++ b/tests/render/oncreate.js @@ -1,8 +1,7 @@ import o from "ospec" import domMock from "../../test-utils/domMock.js" -import m from "../../src/core/hyperscript.js" -import render from "../../src/core/render.js" +import m from "../../src/entry/mithril.esm.js" o.spec("layout create", function() { var $window, root @@ -15,7 +14,7 @@ o.spec("layout create", function() { var callback = o.spy() var vnode = m.layout(callback) - render(root, vnode) + m.render(root, vnode) o(callback.callCount).equals(1) o(callback.args[0]).equals(root) @@ -26,7 +25,7 @@ o.spec("layout create", function() { var callback = o.spy() var vnode = m("div", m.layout(callback)) - render(root, vnode) + m.render(root, vnode) o(callback.callCount).equals(1) o(callback.args[1].aborted).equals(false) @@ -36,7 +35,7 @@ o.spec("layout create", function() { var callback = o.spy() var vnode = [m.layout(callback)] - render(root, vnode) + m.render(root, vnode) o(callback.callCount).equals(1) o(callback.args[1].aborted).equals(false) @@ -48,8 +47,8 @@ o.spec("layout create", function() { var vnode = m("div", m.layout(createDiv)) var updated = m("a", m.layout(createA)) - render(root, m.key(1, vnode)) - render(root, m.key(1, updated)) + m.render(root, m.key(1, vnode)) + m.render(root, m.key(1, updated)) o(createDiv.callCount).equals(1) o(createDiv.args[1].aborted).equals(true) @@ -62,7 +61,7 @@ o.spec("layout create", function() { var create = o.spy() var vnode = m("div", m.layout(create), m("a")) - render(root, vnode) + m.render(root, vnode) o(create.callCount).equals(1) o(create.args[0]).equals(root.firstChild) @@ -74,7 +73,7 @@ o.spec("layout create", function() { var vnode = m("div", m.layout(create)) var otherVnode = m("a") - render(root, [m.key(1, vnode), m.key(2, otherVnode)]) + m.render(root, [m.key(1, vnode), m.key(2, otherVnode)]) o(create.callCount).equals(1) o(create.args[0]).equals(root.firstChild) @@ -85,12 +84,12 @@ o.spec("layout create", function() { var create = o.spy() var vnode = m("div", m.layout(create)) - render(root, vnode) + m.render(root, vnode) o(create.callCount).equals(1) o(create.args[1].aborted).equals(false) - render(root, []) + m.render(root, []) o(create.callCount).equals(1) o(create.args[1].aborted).equals(true) @@ -102,8 +101,8 @@ o.spec("layout create", function() { var vnode = m("div", m.layout(create)) var updated = m("div", m.layout(update), m("a", m.layout(callback))) - render(root, vnode) - render(root, updated) + m.render(root, vnode) + m.render(root, updated) o(create.callCount).equals(1) o(create.args[0]).equals(root.firstChild) @@ -122,8 +121,8 @@ o.spec("layout create", function() { }) o("works on unkeyed that falls into reverse list diff code path", function() { var create = o.spy() - render(root, [m.key(1, m("p")), m.key(2, m("div"))]) - render(root, [m.key(2, m("div", m.layout(create))), m.key(1, m("p"))]) + m.render(root, [m.key(1, m("p")), m.key(2, m("div"))]) + m.render(root, [m.key(2, m("div", m.layout(create))), m.key(1, m("p"))]) o(create.callCount).equals(1) o(create.args[0]).equals(root.firstChild) @@ -132,8 +131,8 @@ o.spec("layout create", function() { }) o("works on unkeyed that falls into forward list diff code path", function() { var create = o.spy() - render(root, [m("div"), m("p")]) - render(root, [m("div"), m("div", m.layout(create))]) + m.render(root, [m("div"), m("p")]) + m.render(root, [m("div"), m("div", m.layout(create))]) o(create.callCount).equals(1) o(create.args[0]).equals(root.childNodes[1]) @@ -144,7 +143,7 @@ o.spec("layout create", function() { var created = false var vnode = m("div", m("a", m.layout(create), m("b"))) - render(root, vnode) + m.render(root, vnode) function create(dom, _, isInit) { if (!isInit) return diff --git a/tests/render/onremove.js b/tests/render/onremove.js index b3aba4f7a..914849662 100644 --- a/tests/render/onremove.js +++ b/tests/render/onremove.js @@ -1,8 +1,7 @@ import o from "ospec" import domMock from "../../test-utils/domMock.js" -import m from "../../src/core/hyperscript.js" -import render from "../../src/core/render.js" +import m from "../../src/entry/mithril.esm.js" o.spec("layout remove", function() { var $window, root @@ -19,8 +18,8 @@ o.spec("layout remove", function() { var vnode = m("div", layoutRemove(create)) var updated = m("div", layoutRemove(update)) - render(root, vnode) - render(root, updated) + m.render(root, vnode) + m.render(root, updated) o(create.callCount).equals(0) }) @@ -30,8 +29,8 @@ o.spec("layout remove", function() { var vnode = m("div", layoutRemove(create)) var updated = m("div", layoutRemove(update)) - render(root, vnode) - render(root, updated) + m.render(root, vnode) + m.render(root, updated) o(create.callCount).equals(0) o(update.callCount).equals(0) @@ -40,8 +39,8 @@ o.spec("layout remove", function() { var remove = o.spy() var vnode = m("div", layoutRemove(remove)) - render(root, vnode) - render(root, []) + m.render(root, vnode) + m.render(root, []) o(remove.callCount).equals(1) }) @@ -49,8 +48,8 @@ o.spec("layout remove", function() { var remove = o.spy() var vnode = [layoutRemove(remove)] - render(root, vnode) - render(root, []) + m.render(root, vnode) + m.render(root, []) o(remove.callCount).equals(1) }) @@ -60,9 +59,9 @@ o.spec("layout remove", function() { var temp = m("div", layoutRemove(remove)) var updated = m("div") - render(root, m.key(1, vnode)) - render(root, m.key(2, temp)) - render(root, m.key(1, updated)) + m.render(root, m.key(1, vnode)) + m.render(root, m.key(2, temp)) + m.render(root, m.key(1, updated)) o(vnode.dom).notEquals(updated.dom) // this used to be a recycling pool test o(remove.callCount).equals(1) @@ -72,8 +71,8 @@ o.spec("layout remove", function() { var comp = () => m(outer) var outer = () => m(inner) var inner = () => m.layout(spy) - render(root, m(comp)) - render(root, null) + m.render(root, m(comp)) + m.render(root, null) o(spy.callCount).equals(1) }) @@ -82,8 +81,8 @@ o.spec("layout remove", function() { var comp = () => m(outer) var outer = () => m(inner, m("a", layoutRemove(spy))) var inner = (attrs) => m("div", attrs.children) - render(root, m(comp)) - render(root, null) + m.render(root, m(comp)) + m.render(root, null) o(spy.callCount).equals(1) }) diff --git a/tests/render/onupdate.js b/tests/render/onupdate.js index 003d269dc..2ebec40dc 100644 --- a/tests/render/onupdate.js +++ b/tests/render/onupdate.js @@ -1,8 +1,7 @@ import o from "ospec" import domMock from "../../test-utils/domMock.js" -import m from "../../src/core/hyperscript.js" -import render from "../../src/core/render.js" +import m from "../../src/entry/mithril.esm.js" o.spec("layout update", function() { var $window, root @@ -15,8 +14,8 @@ o.spec("layout update", function() { var layout = o.spy() var vnode = m("div", m.layout(layout)) - render(root, vnode) - render(root, []) + m.render(root, vnode) + m.render(root, []) o(layout.calls.map((c) => c.args[2])).deepEquals([true]) }) @@ -25,8 +24,8 @@ o.spec("layout update", function() { var update = o.spy() var vnode = m.key(1, m("div", m.layout(layout))) var updated = m.key(1, m("a", m.layout(update))) - render(root, vnode) - render(root, updated) + m.render(root, vnode) + m.render(root, updated) o(layout.calls.map((c) => c.args[2])).deepEquals([true]) o(update.calls.map((c) => c.args[2])).deepEquals([true]) @@ -34,9 +33,9 @@ o.spec("layout update", function() { o("does not call old callback when removing layout vnode from new vnode", function() { var layout = o.spy() - render(root, m("a", m.layout(layout))) - render(root, m("a", m.layout(layout))) - render(root, m("a")) + m.render(root, m("a", m.layout(layout))) + m.render(root, m("a", m.layout(layout))) + m.render(root, m("a")) o(layout.calls.map((c) => c.args[2])).deepEquals([true, false]) }) @@ -46,8 +45,8 @@ o.spec("layout update", function() { var vnode = m("div", m.layout(layout)) var updated = m("div", m.layout(update)) - render(root, vnode) - render(root, updated) + m.render(root, vnode) + m.render(root, updated) o(layout.calls.map((c) => c.args[2])).deepEquals([true]) o(update.calls.map((c) => c.args[2])).deepEquals([false]) @@ -58,8 +57,8 @@ o.spec("layout update", function() { var vnode = m("div", m.layout(layout)) var updated = m("div", {id: "a"}, m.layout(update)) - render(root, vnode) - render(root, updated) + m.render(root, vnode) + m.render(root, updated) o(layout.calls.map((c) => c.args[2])).deepEquals([true]) o(update.calls.map((c) => c.args[2])).deepEquals([false]) @@ -70,8 +69,8 @@ o.spec("layout update", function() { var vnode = m("div", m.layout(layout), m("a")) var updated = m("div", m.layout(update), m("b")) - render(root, vnode) - render(root, updated) + m.render(root, vnode) + m.render(root, updated) o(layout.calls.map((c) => c.args[2])).deepEquals([true]) o(update.calls.map((c) => c.args[2])).deepEquals([false]) @@ -82,8 +81,8 @@ o.spec("layout update", function() { var vnode = [m.layout(layout)] var updated = [m.layout(update)] - render(root, vnode) - render(root, updated) + m.render(root, vnode) + m.render(root, updated) o(layout.calls.map((c) => c.args[2])).deepEquals([true]) o(update.calls.map((c) => c.args[2])).deepEquals([false]) @@ -101,8 +100,8 @@ o.spec("layout update", function() { ) ) - render(root, vnode) - render(root, updated) + m.render(root, vnode) + m.render(root, updated) function update(dom, _, isInit) { if (isInit) return diff --git a/tests/render/render-hyperscript-integration.js b/tests/render/render-hyperscript-integration.js index 7e2c52d13..6c30e08c4 100644 --- a/tests/render/render-hyperscript-integration.js +++ b/tests/render/render-hyperscript-integration.js @@ -1,8 +1,7 @@ import o from "ospec" import domMock from "../../test-utils/domMock.js" -import m from "../../src/core/hyperscript.js" -import render from "../../src/core/render.js" +import m from "../../src/entry/mithril.esm.js" o.spec("render/hyperscript integration", function() { var $window, root @@ -12,47 +11,47 @@ o.spec("render/hyperscript integration", function() { }) o.spec("setting class", function() { o("selector only", function() { - render(root, m(".foo")) + m.render(root, m(".foo")) o(root.firstChild.className).equals("foo") }) o("class only", function() { - render(root, m("div", {class: "foo"})) + m.render(root, m("div", {class: "foo"})) o(root.firstChild.className).equals("foo") }) o("className only", function() { - render(root, m("div", {className: "foo"})) + m.render(root, m("div", {className: "foo"})) o(root.firstChild.className).equals("foo") }) o("selector and class", function() { - render(root, m(".bar", {class: "foo"})) + m.render(root, m(".bar", {class: "foo"})) o(root.firstChild.className.split(" ").sort()).deepEquals(["bar", "foo"]) }) o("selector and className", function() { - render(root, m(".bar", {className: "foo"})) + m.render(root, m(".bar", {className: "foo"})) o(root.firstChild.className.split(" ").sort()).deepEquals(["bar", "foo"]) }) o("selector and a null class", function() { - render(root, m(".foo", {class: null})) + m.render(root, m(".foo", {class: null})) o(root.firstChild.className).equals("foo") }) o("selector and a null className", function() { - render(root, m(".foo", {className: null})) + m.render(root, m(".foo", {className: null})) o(root.firstChild.className).equals("foo") }) o("selector and an undefined class", function() { - render(root, m(".foo", {class: undefined})) + m.render(root, m(".foo", {class: undefined})) o(root.firstChild.className).equals("foo") }) o("selector and an undefined className", function() { - render(root, m(".foo", {className: undefined})) + m.render(root, m(".foo", {className: undefined})) o(root.firstChild.className).equals("foo") }) @@ -60,550 +59,550 @@ o.spec("render/hyperscript integration", function() { o.spec("updating class", function() { o.spec("from selector only", function() { o("to selector only", function() { - render(root, m(".foo1")) - render(root, m(".foo2")) + m.render(root, m(".foo1")) + m.render(root, m(".foo2")) o(root.firstChild.className).equals("foo2") }) o("to class only", function() { - render(root, m(".foo1")) - render(root, m("div", {class: "foo2"})) + m.render(root, m(".foo1")) + m.render(root, m("div", {class: "foo2"})) o(root.firstChild.className).equals("foo2") }) o("to className only", function() { - render(root, m(".foo1")) - render(root, m("div", {className: "foo2"})) + m.render(root, m(".foo1")) + m.render(root, m("div", {className: "foo2"})) o(root.firstChild.className).equals("foo2") }) o("to selector and class", function() { - render(root, m(".foo1")) - render(root, m(".bar2", {class: "foo2"})) + m.render(root, m(".foo1")) + m.render(root, m(".bar2", {class: "foo2"})) o(root.firstChild.className.split(" ").sort()).deepEquals(["bar2", "foo2"]) }) o("to selector and className", function() { - render(root, m(".foo1")) - render(root, m(".bar2", {className: "foo2"})) + m.render(root, m(".foo1")) + m.render(root, m(".bar2", {className: "foo2"})) o(root.firstChild.className.split(" ").sort()).deepEquals(["bar2", "foo2"]) }) o("to selector and a null class", function() { - render(root, m(".foo1")) - render(root, m(".foo2", {class: null})) + m.render(root, m(".foo1")) + m.render(root, m(".foo2", {class: null})) o(root.firstChild.className).equals("foo2") }) o("to selector and a null className", function() { - render(root, m(".foo1")) - render(root, m(".foo2", {className: null})) + m.render(root, m(".foo1")) + m.render(root, m(".foo2", {className: null})) o(root.firstChild.className).equals("foo2") }) o("to selector and an undefined class", function() { - render(root, m(".foo1")) - render(root, m(".foo2", {class: undefined})) + m.render(root, m(".foo1")) + m.render(root, m(".foo2", {class: undefined})) o(root.firstChild.className).equals("foo2") }) o("to selector and an undefined className", function() { - render(root, m(".foo1")) - render(root, m(".foo2", {className: undefined})) + m.render(root, m(".foo1")) + m.render(root, m(".foo2", {className: undefined})) o(root.firstChild.className).equals("foo2") }) }) o.spec("from class only", function() { o("to selector only", function() { - render(root, m("div", {class: "foo2"})) - render(root, m(".foo2")) + m.render(root, m("div", {class: "foo2"})) + m.render(root, m(".foo2")) o(root.firstChild.className).equals("foo2") }) o("to class only", function() { - render(root, m("div", {class: "foo2"})) - render(root, m("div", {class: "foo2"})) + m.render(root, m("div", {class: "foo2"})) + m.render(root, m("div", {class: "foo2"})) o(root.firstChild.className).equals("foo2") }) o("to className only", function() { - render(root, m("div", {class: "foo2"})) - render(root, m("div", {className: "foo2"})) + m.render(root, m("div", {class: "foo2"})) + m.render(root, m("div", {className: "foo2"})) o(root.firstChild.className).equals("foo2") }) o("to selector and class", function() { - render(root, m("div", {class: "foo2"})) - render(root, m(".bar2", {class: "foo2"})) + m.render(root, m("div", {class: "foo2"})) + m.render(root, m(".bar2", {class: "foo2"})) o(root.firstChild.className.split(" ").sort()).deepEquals(["bar2", "foo2"]) }) o("to selector and className", function() { - render(root, m(".bar2", {className: "foo2"})) + m.render(root, m(".bar2", {className: "foo2"})) o(root.firstChild.className.split(" ").sort()).deepEquals(["bar2", "foo2"]) }) o("to selector and a null class", function() { - render(root, m("div", {class: "foo2"})) - render(root, m(".foo2", {class: null})) + m.render(root, m("div", {class: "foo2"})) + m.render(root, m(".foo2", {class: null})) o(root.firstChild.className).equals("foo2") }) o("to selector and a null className", function() { - render(root, m("div", {class: "foo2"})) - render(root, m(".foo2", {className: null})) + m.render(root, m("div", {class: "foo2"})) + m.render(root, m(".foo2", {className: null})) o(root.firstChild.className).equals("foo2") }) o("to selector and an undefined class", function() { - render(root, m("div", {class: "foo2"})) - render(root, m(".foo2", {class: undefined})) + m.render(root, m("div", {class: "foo2"})) + m.render(root, m(".foo2", {class: undefined})) o(root.firstChild.className).equals("foo2") }) o("to selector and an undefined className", function() { - render(root, m("div", {class: "foo2"})) - render(root, m(".foo2", {className: undefined})) + m.render(root, m("div", {class: "foo2"})) + m.render(root, m(".foo2", {className: undefined})) o(root.firstChild.className).equals("foo2") }) }) o.spec("from ", function() { o("to selector only", function() { - render(root, m(".foo2")) + m.render(root, m(".foo2")) o(root.firstChild.className).equals("foo2") }) o("to class only", function() { - render(root, m("div", {class: "foo2"})) + m.render(root, m("div", {class: "foo2"})) o(root.firstChild.className).equals("foo2") }) o("to className only", function() { - render(root, m("div", {className: "foo2"})) + m.render(root, m("div", {className: "foo2"})) o(root.firstChild.className).equals("foo2") }) o("to selector and class", function() { - render(root, m(".bar2", {class: "foo2"})) + m.render(root, m(".bar2", {class: "foo2"})) o(root.firstChild.className.split(" ").sort()).deepEquals(["bar2", "foo2"]) }) o("to selector and className", function() { - render(root, m(".bar2", {className: "foo2"})) + m.render(root, m(".bar2", {className: "foo2"})) o(root.firstChild.className.split(" ").sort()).deepEquals(["bar2", "foo2"]) }) o("to selector and a null class", function() { - render(root, m(".foo2", {class: null})) + m.render(root, m(".foo2", {class: null})) o(root.firstChild.className).equals("foo2") }) o("to selector and a null className", function() { - render(root, m(".foo2", {className: null})) + m.render(root, m(".foo2", {className: null})) o(root.firstChild.className).equals("foo2") }) o("to selector and an undefined class", function() { - render(root, m(".foo2", {class: undefined})) + m.render(root, m(".foo2", {class: undefined})) o(root.firstChild.className).equals("foo2") }) o("to selector and an undefined className", function() { - render(root, m(".foo2", {className: undefined})) + m.render(root, m(".foo2", {className: undefined})) o(root.firstChild.className).equals("foo2") }) }) o.spec("from className only", function() { o("to selector only", function() { - render(root, m("div", {className: "foo1"})) - render(root, m(".foo2")) + m.render(root, m("div", {className: "foo1"})) + m.render(root, m(".foo2")) o(root.firstChild.className).equals("foo2") }) o("to class only", function() { - render(root, m("div", {className: "foo1"})) - render(root, m("div", {class: "foo2"})) + m.render(root, m("div", {className: "foo1"})) + m.render(root, m("div", {class: "foo2"})) o(root.firstChild.className).equals("foo2") }) o("to className only", function() { - render(root, m("div", {className: "foo1"})) - render(root, m("div", {className: "foo2"})) + m.render(root, m("div", {className: "foo1"})) + m.render(root, m("div", {className: "foo2"})) o(root.firstChild.className).equals("foo2") }) o("to selector and class", function() { - render(root, m("div", {className: "foo1"})) - render(root, m(".bar2", {class: "foo2"})) + m.render(root, m("div", {className: "foo1"})) + m.render(root, m(".bar2", {class: "foo2"})) o(root.firstChild.className.split(" ").sort()).deepEquals(["bar2", "foo2"]) }) o("to selector and className", function() { - render(root, m("div", {className: "foo1"})) - render(root, m(".bar2", {className: "foo2"})) + m.render(root, m("div", {className: "foo1"})) + m.render(root, m(".bar2", {className: "foo2"})) o(root.firstChild.className.split(" ").sort()).deepEquals(["bar2", "foo2"]) }) o("to selector and a null class", function() { - render(root, m("div", {className: "foo1"})) - render(root, m(".foo2", {class: null})) + m.render(root, m("div", {className: "foo1"})) + m.render(root, m(".foo2", {class: null})) o(root.firstChild.className).equals("foo2") }) o("to selector and a null className", function() { - render(root, m("div", {className: "foo1"})) - render(root, m(".foo2", {className: null})) + m.render(root, m("div", {className: "foo1"})) + m.render(root, m(".foo2", {className: null})) o(root.firstChild.className).equals("foo2") }) o("to selector and an undefined class", function() { - render(root, m("div", {className: "foo1"})) - render(root, m(".foo2", {class: undefined})) + m.render(root, m("div", {className: "foo1"})) + m.render(root, m(".foo2", {class: undefined})) o(root.firstChild.className).equals("foo2") }) o("to selector and an undefined className", function() { - render(root, m("div", {className: "foo1"})) - render(root, m(".foo2", {className: undefined})) + m.render(root, m("div", {className: "foo1"})) + m.render(root, m(".foo2", {className: undefined})) o(root.firstChild.className).equals("foo2") }) }) o.spec("from selector and class", function() { o("to selector only", function() { - render(root, m(".bar1", {class: "foo1"})) - render(root, m(".foo2")) + m.render(root, m(".bar1", {class: "foo1"})) + m.render(root, m(".foo2")) o(root.firstChild.className).equals("foo2") }) o("to class only", function() { - render(root, m(".bar1", {class: "foo1"})) - render(root, m("div", {class: "foo2"})) + m.render(root, m(".bar1", {class: "foo1"})) + m.render(root, m("div", {class: "foo2"})) o(root.firstChild.className).equals("foo2") }) o("to className only", function() { - render(root, m(".bar1", {class: "foo1"})) - render(root, m("div", {className: "foo2"})) + m.render(root, m(".bar1", {class: "foo1"})) + m.render(root, m("div", {className: "foo2"})) o(root.firstChild.className).equals("foo2") }) o("to selector and class", function() { - render(root, m(".bar1", {class: "foo1"})) - render(root, m(".bar2", {class: "foo2"})) + m.render(root, m(".bar1", {class: "foo1"})) + m.render(root, m(".bar2", {class: "foo2"})) o(root.firstChild.className.split(" ").sort()).deepEquals(["bar2", "foo2"]) }) o("to selector and className", function() { - render(root, m(".bar1", {class: "foo1"})) - render(root, m(".bar2", {className: "foo2"})) + m.render(root, m(".bar1", {class: "foo1"})) + m.render(root, m(".bar2", {className: "foo2"})) o(root.firstChild.className.split(" ").sort()).deepEquals(["bar2", "foo2"]) }) o("to selector and a null class", function() { - render(root, m(".bar1", {class: "foo1"})) - render(root, m(".foo2", {class: null})) + m.render(root, m(".bar1", {class: "foo1"})) + m.render(root, m(".foo2", {class: null})) o(root.firstChild.className).equals("foo2") }) o("to selector and a null className", function() { - render(root, m(".bar1", {class: "foo1"})) - render(root, m(".foo2", {className: null})) + m.render(root, m(".bar1", {class: "foo1"})) + m.render(root, m(".foo2", {className: null})) o(root.firstChild.className).equals("foo2") }) o("to selector and an undefined class", function() { - render(root, m(".bar1", {class: "foo1"})) - render(root, m(".foo2", {class: undefined})) + m.render(root, m(".bar1", {class: "foo1"})) + m.render(root, m(".foo2", {class: undefined})) o(root.firstChild.className).equals("foo2") }) o("to selector and an undefined className", function() { - render(root, m(".bar1", {class: "foo1"})) - render(root, m(".foo2", {className: undefined})) + m.render(root, m(".bar1", {class: "foo1"})) + m.render(root, m(".foo2", {className: undefined})) o(root.firstChild.className).equals("foo2") }) }) o.spec("from selector and className", function() { o("to selector only", function() { - render(root, m(".bar1", {className: "foo1"})) - render(root, m(".foo2")) + m.render(root, m(".bar1", {className: "foo1"})) + m.render(root, m(".foo2")) o(root.firstChild.className).equals("foo2") }) o("to class only", function() { - render(root, m(".bar1", {className: "foo1"})) - render(root, m("div", {class: "foo2"})) + m.render(root, m(".bar1", {className: "foo1"})) + m.render(root, m("div", {class: "foo2"})) o(root.firstChild.className).equals("foo2") }) o("to className only", function() { - render(root, m(".bar1", {className: "foo1"})) - render(root, m("div", {className: "foo2"})) + m.render(root, m(".bar1", {className: "foo1"})) + m.render(root, m("div", {className: "foo2"})) o(root.firstChild.className).equals("foo2") }) o("to selector and class", function() { - render(root, m(".bar1", {className: "foo1"})) - render(root, m(".bar2", {class: "foo2"})) + m.render(root, m(".bar1", {className: "foo1"})) + m.render(root, m(".bar2", {class: "foo2"})) o(root.firstChild.className.split(" ").sort()).deepEquals(["bar2", "foo2"]) }) o("to selector and className", function() { - render(root, m(".bar1", {className: "foo1"})) - render(root, m(".bar2", {className: "foo2"})) + m.render(root, m(".bar1", {className: "foo1"})) + m.render(root, m(".bar2", {className: "foo2"})) o(root.firstChild.className.split(" ").sort()).deepEquals(["bar2", "foo2"]) }) o("to selector and a null class", function() { - render(root, m(".bar1", {className: "foo1"})) - render(root, m(".foo2", {class: null})) + m.render(root, m(".bar1", {className: "foo1"})) + m.render(root, m(".foo2", {class: null})) o(root.firstChild.className).equals("foo2") }) o("to selector and a null className", function() { - render(root, m(".bar1", {className: "foo1"})) - render(root, m(".foo2", {className: null})) + m.render(root, m(".bar1", {className: "foo1"})) + m.render(root, m(".foo2", {className: null})) o(root.firstChild.className).equals("foo2") }) o("to selector and an undefined class", function() { - render(root, m(".bar1", {className: "foo1"})) - render(root, m(".foo2", {class: undefined})) + m.render(root, m(".bar1", {className: "foo1"})) + m.render(root, m(".foo2", {class: undefined})) o(root.firstChild.className).equals("foo2") }) o("to selector and an undefined className", function() { - render(root, m(".bar1", {className: "foo1"})) - render(root, m(".foo2", {className: undefined})) + m.render(root, m(".bar1", {className: "foo1"})) + m.render(root, m(".foo2", {className: undefined})) o(root.firstChild.className).equals("foo2") }) }) o.spec("from and a null class", function() { o("to selector only", function() { - render(root, m(".foo1", {class: null})) - render(root, m(".foo2")) + m.render(root, m(".foo1", {class: null})) + m.render(root, m(".foo2")) o(root.firstChild.className).equals("foo2") }) o("to class only", function() { - render(root, m(".foo1", {class: null})) - render(root, m("div", {class: "foo2"})) + m.render(root, m(".foo1", {class: null})) + m.render(root, m("div", {class: "foo2"})) o(root.firstChild.className).equals("foo2") }) o("to className only", function() { - render(root, m(".foo1", {class: null})) - render(root, m("div", {className: "foo2"})) + m.render(root, m(".foo1", {class: null})) + m.render(root, m("div", {className: "foo2"})) o(root.firstChild.className).equals("foo2") }) o("to selector and class", function() { - render(root, m(".foo1", {class: null})) - render(root, m(".bar2", {class: "foo2"})) + m.render(root, m(".foo1", {class: null})) + m.render(root, m(".bar2", {class: "foo2"})) o(root.firstChild.className.split(" ").sort()).deepEquals(["bar2", "foo2"]) }) o("to selector and className", function() { - render(root, m(".foo1", {class: null})) - render(root, m(".bar2", {className: "foo2"})) + m.render(root, m(".foo1", {class: null})) + m.render(root, m(".bar2", {className: "foo2"})) o(root.firstChild.className.split(" ").sort()).deepEquals(["bar2", "foo2"]) }) o("to selector and a null class", function() { - render(root, m(".foo1", {class: null})) - render(root, m(".foo2", {class: null})) + m.render(root, m(".foo1", {class: null})) + m.render(root, m(".foo2", {class: null})) o(root.firstChild.className).equals("foo2") }) o("to selector and a null className", function() { - render(root, m(".foo1", {class: null})) - render(root, m(".foo2", {className: null})) + m.render(root, m(".foo1", {class: null})) + m.render(root, m(".foo2", {className: null})) o(root.firstChild.className).equals("foo2") }) o("to selector and an undefined class", function() { - render(root, m(".foo1", {class: null})) - render(root, m(".foo2", {class: undefined})) + m.render(root, m(".foo1", {class: null})) + m.render(root, m(".foo2", {class: undefined})) o(root.firstChild.className).equals("foo2") }) o("to selector and an undefined className", function() { - render(root, m(".foo1", {class: null})) - render(root, m(".foo2", {className: undefined})) + m.render(root, m(".foo1", {class: null})) + m.render(root, m(".foo2", {className: undefined})) o(root.firstChild.className).equals("foo2") }) }) o.spec("from selector and a null className", function() { o("to selector only", function() { - render(root, m(".foo1", {className: null})) - render(root, m(".foo2")) + m.render(root, m(".foo1", {className: null})) + m.render(root, m(".foo2")) o(root.firstChild.className).equals("foo2") }) o("to class only", function() { - render(root, m(".foo1", {className: null})) - render(root, m("div", {class: "foo2"})) + m.render(root, m(".foo1", {className: null})) + m.render(root, m("div", {class: "foo2"})) o(root.firstChild.className).equals("foo2") }) o("to className only", function() { - render(root, m(".foo1", {className: null})) - render(root, m("div", {className: "foo2"})) + m.render(root, m(".foo1", {className: null})) + m.render(root, m("div", {className: "foo2"})) o(root.firstChild.className).equals("foo2") }) o("to selector and class", function() { - render(root, m(".foo1", {className: null})) - render(root, m(".bar2", {class: "foo2"})) + m.render(root, m(".foo1", {className: null})) + m.render(root, m(".bar2", {class: "foo2"})) o(root.firstChild.className.split(" ").sort()).deepEquals(["bar2", "foo2"]) }) o("to selector and className", function() { - render(root, m(".foo1", {className: null})) - render(root, m(".bar2", {className: "foo2"})) + m.render(root, m(".foo1", {className: null})) + m.render(root, m(".bar2", {className: "foo2"})) o(root.firstChild.className.split(" ").sort()).deepEquals(["bar2", "foo2"]) }) o("to selector and a null class", function() { - render(root, m(".foo1", {className: null})) - render(root, m(".foo2", {class: null})) + m.render(root, m(".foo1", {className: null})) + m.render(root, m(".foo2", {class: null})) o(root.firstChild.className).equals("foo2") }) o("to selector and a null className", function() { - render(root, m(".foo1", {className: null})) - render(root, m(".foo2", {className: null})) + m.render(root, m(".foo1", {className: null})) + m.render(root, m(".foo2", {className: null})) o(root.firstChild.className).equals("foo2") }) o("to selector and an undefined class", function() { - render(root, m(".foo1", {className: null})) - render(root, m(".foo2", {class: undefined})) + m.render(root, m(".foo1", {className: null})) + m.render(root, m(".foo2", {class: undefined})) o(root.firstChild.className).equals("foo2") }) o("to selector and an undefined className", function() { - render(root, m(".foo1", {className: null})) - render(root, m(".foo2", {className: undefined})) + m.render(root, m(".foo1", {className: null})) + m.render(root, m(".foo2", {className: undefined})) o(root.firstChild.className).equals("foo2") }) }) o.spec("from selector and an undefined class", function() { o("to selector only", function() { - render(root, m(".foo1", {class: undefined})) - render(root, m(".foo2")) + m.render(root, m(".foo1", {class: undefined})) + m.render(root, m(".foo2")) o(root.firstChild.className).equals("foo2") }) o("to class only", function() { - render(root, m(".foo1", {class: undefined})) - render(root, m("div", {class: "foo2"})) + m.render(root, m(".foo1", {class: undefined})) + m.render(root, m("div", {class: "foo2"})) o(root.firstChild.className).equals("foo2") }) o("to className only", function() { - render(root, m(".foo1", {class: undefined})) - render(root, m("div", {className: "foo2"})) + m.render(root, m(".foo1", {class: undefined})) + m.render(root, m("div", {className: "foo2"})) o(root.firstChild.className).equals("foo2") }) o("to selector and class", function() { - render(root, m(".foo1", {class: undefined})) - render(root, m(".bar2", {class: "foo2"})) + m.render(root, m(".foo1", {class: undefined})) + m.render(root, m(".bar2", {class: "foo2"})) o(root.firstChild.className.split(" ").sort()).deepEquals(["bar2", "foo2"]) }) o("to selector and className", function() { - render(root, m(".foo1", {class: undefined})) - render(root, m(".bar2", {className: "foo2"})) + m.render(root, m(".foo1", {class: undefined})) + m.render(root, m(".bar2", {className: "foo2"})) o(root.firstChild.className.split(" ").sort()).deepEquals(["bar2", "foo2"]) }) o("to selector and a null class", function() { - render(root, m(".foo1", {class: undefined})) - render(root, m(".foo2", {class: null})) + m.render(root, m(".foo1", {class: undefined})) + m.render(root, m(".foo2", {class: null})) o(root.firstChild.className).equals("foo2") }) o("to selector and a null className", function() { - render(root, m(".foo1", {class: undefined})) - render(root, m(".foo2", {className: null})) + m.render(root, m(".foo1", {class: undefined})) + m.render(root, m(".foo2", {className: null})) o(root.firstChild.className).equals("foo2") }) o("to selector and an undefined class", function() { - render(root, m(".foo1", {class: undefined})) - render(root, m(".foo2", {class: undefined})) + m.render(root, m(".foo1", {class: undefined})) + m.render(root, m(".foo2", {class: undefined})) o(root.firstChild.className).equals("foo2") }) o("to selector and an undefined className", function() { - render(root, m(".foo1", {class: undefined})) - render(root, m(".foo2", {className: undefined})) + m.render(root, m(".foo1", {class: undefined})) + m.render(root, m(".foo2", {className: undefined})) o(root.firstChild.className).equals("foo2") }) }) o.spec("from selector and an undefined className", function() { o("to selector only", function() { - render(root, m(".foo1", {className: undefined})) - render(root, m(".foo2")) + m.render(root, m(".foo1", {className: undefined})) + m.render(root, m(".foo2")) o(root.firstChild.className).equals("foo2") }) o("to class only", function() { - render(root, m(".foo1", {className: undefined})) - render(root, m("div", {class: "foo2"})) + m.render(root, m(".foo1", {className: undefined})) + m.render(root, m("div", {class: "foo2"})) o(root.firstChild.className).equals("foo2") }) o("to className only", function() { - render(root, m(".foo1", {className: undefined})) - render(root, m("div", {className: "foo2"})) + m.render(root, m(".foo1", {className: undefined})) + m.render(root, m("div", {className: "foo2"})) o(root.firstChild.className).equals("foo2") }) o("to selector and class", function() { - render(root, m(".foo1", {className: undefined})) - render(root, m(".bar2", {class: "foo2"})) + m.render(root, m(".foo1", {className: undefined})) + m.render(root, m(".bar2", {class: "foo2"})) o(root.firstChild.className.split(" ").sort()).deepEquals(["bar2", "foo2"]) }) o("to selector and className", function() { - render(root, m(".foo1", {className: undefined})) - render(root, m(".bar2", {className: "foo2"})) + m.render(root, m(".foo1", {className: undefined})) + m.render(root, m(".bar2", {className: "foo2"})) o(root.firstChild.className.split(" ").sort()).deepEquals(["bar2", "foo2"]) }) o("to selector and a null class", function() { - render(root, m(".foo1", {className: undefined})) - render(root, m(".foo2", {class: null})) + m.render(root, m(".foo1", {className: undefined})) + m.render(root, m(".foo2", {class: null})) o(root.firstChild.className).equals("foo2") }) o("to selector and a null className", function() { - render(root, m(".foo1", {className: undefined})) - render(root, m(".foo2", {className: null})) + m.render(root, m(".foo1", {className: undefined})) + m.render(root, m(".foo2", {className: null})) o(root.firstChild.className).equals("foo2") }) o("to selector and an undefined class", function() { - render(root, m(".foo1", {className: undefined})) - render(root, m(".foo2", {class: undefined})) + m.render(root, m(".foo1", {className: undefined})) + m.render(root, m(".foo2", {class: undefined})) o(root.firstChild.className).equals("foo2") }) o("to selector and an undefined className", function() { - render(root, m(".foo1", {className: undefined})) - render(root, m(".foo2", {className: undefined})) + m.render(root, m(".foo1", {className: undefined})) + m.render(root, m(".foo2", {className: undefined})) o(root.firstChild.className).equals("foo2") }) diff --git a/tests/render/render.js b/tests/render/render.js index a39be1463..6c15c895f 100644 --- a/tests/render/render.js +++ b/tests/render/render.js @@ -1,8 +1,7 @@ import o from "ospec" import domMock from "../../test-utils/domMock.js" -import m from "../../src/core/hyperscript.js" -import render from "../../src/core/render.js" +import m from "../../src/entry/mithril.esm.js" o.spec("render", function() { var $window, root @@ -12,27 +11,27 @@ o.spec("render", function() { }) o("renders plain text", function() { - render(root, "a") + m.render(root, "a") o(root.childNodes.length).equals(1) o(root.childNodes[0].nodeValue).equals("a") }) o("updates plain text", function() { - render(root, "a") - render(root, "b") + m.render(root, "a") + m.render(root, "b") o(root.childNodes.length).equals(1) o(root.childNodes[0].nodeValue).equals("b") }) o("renders a number", function() { - render(root, 1) + m.render(root, 1) o(root.childNodes.length).equals(1) o(root.childNodes[0].nodeValue).equals("1") }) o("updates a number", function() { - render(root, 1) - render(root, 2) + m.render(root, 1) + m.render(root, 2) o(root.childNodes.length).equals(1) o(root.childNodes[0].nodeValue).equals("2") }) @@ -42,7 +41,7 @@ o.spec("render", function() { root.appendChild($window.document.createElement("div")); - render(root, vnodes) + m.render(root, vnodes) o(root.childNodes.length).equals(0) }) @@ -50,7 +49,7 @@ o.spec("render", function() { o("throws on invalid root node", function() { var threw = false try { - render(null, []) + m.render(null, []) } catch (e) { threw = true } @@ -61,12 +60,12 @@ o.spec("render", function() { var A = o.spy(() => { throw new Error("error") }) var throwCount = 0 - try {render(root, m(A))} catch (e) {throwCount++} + try {m.render(root, m(A))} catch (e) {throwCount++} o(throwCount).equals(1) o(A.callCount).equals(1) - try {render(root, m(A))} catch (e) {throwCount++} + try {m.render(root, m(A))} catch (e) {throwCount++} o(throwCount).equals(2) o(A.callCount).equals(2) @@ -75,13 +74,13 @@ o.spec("render", function() { var A = o.spy(() => view) var view = o.spy(() => { throw new Error("error") }) var throwCount = 0 - try {render(root, m(A))} catch (e) {throwCount++} + try {m.render(root, m(A))} catch (e) {throwCount++} o(throwCount).equals(1) o(A.callCount).equals(1) o(view.callCount).equals(1) - try {render(root, m(A))} catch (e) {throwCount++} + try {m.render(root, m(A))} catch (e) {throwCount++} o(throwCount).equals(2) o(A.callCount).equals(2) @@ -104,11 +103,11 @@ o.spec("render", function() { m.key(22, m("div")) )) } - render(root, a()) + m.render(root, a()) var first = root.firstChild.firstChild - render(root, b()) + m.render(root, b()) var second = root.firstChild.firstChild - render(root, a()) + m.render(root, a()) var third = root.firstChild.firstChild o(layoutA.callCount).equals(2) @@ -142,11 +141,11 @@ o.spec("render", function() { m("div", m.layout(layoutB)) )) } - render(root, a()) + m.render(root, a()) var first = root.firstChild.firstChild - render(root, b()) + m.render(root, b()) var second = root.firstChild.firstChild - render(root, a()) + m.render(root, a()) var third = root.firstChild.firstChild o(layoutA.callCount).equals(2) @@ -181,8 +180,8 @@ o.spec("render", function() { m("div", m.layout(layoutB)) )) } - render(root, a()) - render(root, a()) + m.render(root, a()) + m.render(root, a()) var first = root.firstChild.firstChild o(layoutA.callCount).equals(2) o(layoutA.calls[0].args[0]).equals(first) @@ -193,7 +192,7 @@ o.spec("render", function() { o(layoutA.calls[1].args[2]).equals(false) o(onabortA.callCount).equals(0) - render(root, b()) + m.render(root, b()) var second = root.firstChild.firstChild o(layoutA.callCount).equals(2) o(layoutA.calls[0].args[1].aborted).equals(true) @@ -205,8 +204,8 @@ o.spec("render", function() { o(layoutB.calls[0].args[2]).equals(true) o(onabortB.callCount).equals(0) - render(root, a()) - render(root, a()) + m.render(root, a()) + m.render(root, a()) var third = root.firstChild.firstChild o(layoutB.callCount).equals(1) o(layoutB.calls[0].args[1].aborted).equals(true) @@ -225,7 +224,7 @@ o.spec("render", function() { m.key(0, m("g")), m.key(1, m("g")) ) - render(root, svg) + m.render(root, svg) o(svg.dom.namespaceURI).equals("http://www.w3.org/2000/svg") o(svg.dom.childNodes[0].namespaceURI).equals("http://www.w3.org/2000/svg") @@ -235,38 +234,38 @@ o.spec("render", function() { m.key(1, m("g", {x: 1})), m.key(2, m("g", {x: 2})) ) - render(root, svg) + m.render(root, svg) o(svg.dom.namespaceURI).equals("http://www.w3.org/2000/svg") o(svg.dom.childNodes[0].namespaceURI).equals("http://www.w3.org/2000/svg") o(svg.dom.childNodes[1].namespaceURI).equals("http://www.w3.org/2000/svg") }) o("the namespace of the root is passed to children", function() { - render(root, m("svg")) + m.render(root, m("svg")) o(root.childNodes[0].namespaceURI).equals("http://www.w3.org/2000/svg") - render(root.childNodes[0], m("g")) + m.render(root.childNodes[0], m("g")) o(root.childNodes[0].childNodes[0].namespaceURI).equals("http://www.w3.org/2000/svg") }) o("does not allow reentrant invocations", function() { var thrown = [] function A() { - try {render(root, m(A))} catch (e) {thrown.push("construct")} + try {m.render(root, m(A))} catch (e) {thrown.push("construct")} return () => { - try {render(root, m(A))} catch (e) {thrown.push("view")} + try {m.render(root, m(A))} catch (e) {thrown.push("view")} } } - render(root, m(A)) + m.render(root, m(A)) o(thrown).deepEquals([ "construct", "view", ]) - render(root, m(A)) + m.render(root, m(A)) o(thrown).deepEquals([ "construct", "view", "view", ]) - render(root, []) + m.render(root, []) o(thrown).deepEquals([ "construct", "view", diff --git a/tests/render/retain.js b/tests/render/retain.js index 29dd62e91..2c0008f1e 100644 --- a/tests/render/retain.js +++ b/tests/render/retain.js @@ -1,8 +1,7 @@ import o from "ospec" import domMock from "../../test-utils/domMock.js" -import m from "../../src/core/hyperscript.js" -import render from "../../src/core/render.js" +import m from "../../src/entry/mithril.esm.js" o.spec("retain", function() { var $window, root @@ -15,8 +14,8 @@ o.spec("retain", function() { var vnode = m("div", {id: "a"}, "b") var updated = m.retain() - render(root, vnode) - render(root, updated) + m.render(root, vnode) + m.render(root, updated) o(root.firstChild.attributes["id"].value).equals("a") o(root.firstChild.childNodes.length).equals(1) @@ -28,15 +27,15 @@ o.spec("retain", function() { var vnode = m.normalize(["a"]) var updated = m.retain() - render(root, vnode) - render(root, updated) + m.render(root, vnode) + m.render(root, updated) o(root.firstChild.nodeValue).equals("a") o(updated).deepEquals(vnode) }) o("throws on creation", function() { - o(() => render(root, m.retain())).throws(Error) + o(() => m.render(root, m.retain())).throws(Error) }) o("prevents update in component", function() { @@ -44,11 +43,11 @@ o.spec("retain", function() { var vnode = m(component, "a") var updated = m(component, "b") - render(root, vnode) - render(root, updated) + m.render(root, vnode) + m.render(root, updated) o(root.firstChild.firstChild.nodeValue).equals("a") - o(updated.instance).deepEquals(vnode.instance) + o(updated.children).deepEquals(vnode.children) }) o("prevents update in component and for component", function() { @@ -56,8 +55,8 @@ o.spec("retain", function() { var vnode = m(component, {id: "a"}) var updated = m.retain() - render(root, vnode) - render(root, updated) + m.render(root, vnode) + m.render(root, updated) o(root.firstChild.attributes["id"].value).equals("a") o(updated).deepEquals(vnode) @@ -68,8 +67,8 @@ o.spec("retain", function() { var vnode = m(component, {id: "a"}) var updated = m.retain() - render(root, vnode) - render(root, updated) + m.render(root, vnode) + m.render(root, updated) o(root.firstChild.attributes["id"].value).equals("a") o(updated).deepEquals(vnode) @@ -78,6 +77,6 @@ o.spec("retain", function() { o("throws if used on component creation", function() { var component = () => m.retain() - o(() => render(root, m(component))).throws(Error) + o(() => m.render(root, m(component))).throws(Error) }) }) diff --git a/tests/render/textContent.js b/tests/render/textContent.js index 59912dd17..b141bcf89 100644 --- a/tests/render/textContent.js +++ b/tests/render/textContent.js @@ -1,8 +1,7 @@ import o from "ospec" import domMock from "../../test-utils/domMock.js" -import m from "../../src/core/hyperscript.js" -import render from "../../src/core/render.js" +import m from "../../src/entry/mithril.esm.js" o.spec("textContent", function() { var $window, root @@ -14,7 +13,7 @@ o.spec("textContent", function() { o("ignores null", function() { var vnode = m("a", null) - render(root, vnode) + m.render(root, vnode) o(root.childNodes.length).equals(1) o(vnode.dom.childNodes.length).equals(0) @@ -23,7 +22,7 @@ o.spec("textContent", function() { o("ignores undefined", function() { var vnode = m("a", undefined) - render(root, vnode) + m.render(root, vnode) o(root.childNodes.length).equals(1) o(vnode.dom.childNodes.length).equals(0) @@ -32,7 +31,7 @@ o.spec("textContent", function() { o("creates string", function() { var vnode = m("a", "a") - render(root, vnode) + m.render(root, vnode) o(root.childNodes.length).equals(1) o(vnode.dom.childNodes.length).equals(1) @@ -42,7 +41,7 @@ o.spec("textContent", function() { o("creates falsy string", function() { var vnode = m("a", "") - render(root, vnode) + m.render(root, vnode) o(root.childNodes.length).equals(1) o(vnode.dom.childNodes.length).equals(1) @@ -52,7 +51,7 @@ o.spec("textContent", function() { o("creates number", function() { var vnode = m("a", 1) - render(root, vnode) + m.render(root, vnode) o(root.childNodes.length).equals(1) o(vnode.dom.childNodes.length).equals(1) @@ -62,7 +61,7 @@ o.spec("textContent", function() { o("creates falsy number", function() { var vnode = m("a", 0) - render(root, vnode) + m.render(root, vnode) o(root.childNodes.length).equals(1) o(vnode.dom.childNodes.length).equals(1) @@ -72,7 +71,7 @@ o.spec("textContent", function() { o("creates boolean", function() { var vnode = m("a", true) - render(root, vnode) + m.render(root, vnode) o(root.childNodes.length).equals(1) o(vnode.dom.childNodes.length).equals(0) @@ -81,7 +80,7 @@ o.spec("textContent", function() { o("creates falsy boolean", function() { var vnode = m("a", false) - render(root, vnode) + m.render(root, vnode) o(root.childNodes.length).equals(1) o(vnode.dom.childNodes.length).equals(0) @@ -91,8 +90,8 @@ o.spec("textContent", function() { var vnode = m("a", "a") var updated = m("a", "b") - render(root, vnode) - render(root, updated) + m.render(root, vnode) + m.render(root, updated) o(root.childNodes.length).equals(1) o(vnode.dom.childNodes.length).equals(1) @@ -103,8 +102,8 @@ o.spec("textContent", function() { var vnode = m("a", "a") var updated = m("a", "") - render(root, vnode) - render(root, updated) + m.render(root, vnode) + m.render(root, updated) o(root.childNodes.length).equals(1) o(vnode.dom.childNodes.length).equals(1) @@ -115,8 +114,8 @@ o.spec("textContent", function() { var vnode = m("a", "a") var updated = m("a", 1) - render(root, vnode) - render(root, updated) + m.render(root, vnode) + m.render(root, updated) o(root.childNodes.length).equals(1) o(vnode.dom.childNodes.length).equals(1) @@ -127,8 +126,8 @@ o.spec("textContent", function() { var vnode = m("a", "a") var updated = m("a", 0) - render(root, vnode) - render(root, updated) + m.render(root, vnode) + m.render(root, updated) o(root.childNodes.length).equals(1) o(vnode.dom.childNodes.length).equals(1) @@ -139,8 +138,8 @@ o.spec("textContent", function() { var vnode = m("a", "a") var updated = m("a", true) - render(root, vnode) - render(root, updated) + m.render(root, vnode) + m.render(root, updated) o(root.childNodes.length).equals(1) o(vnode.dom.childNodes.length).equals(0) @@ -150,8 +149,8 @@ o.spec("textContent", function() { var vnode = m("a", "a") var updated = m("a", false) - render(root, vnode) - render(root, updated) + m.render(root, vnode) + m.render(root, updated) o(root.childNodes.length).equals(1) o(vnode.dom.childNodes.length).equals(0) @@ -161,8 +160,8 @@ o.spec("textContent", function() { var vnode = m("a", "1") var updated = m("a", 1) - render(root, vnode) - render(root, updated) + m.render(root, vnode) + m.render(root, updated) o(root.childNodes.length).equals(1) o(vnode.dom.childNodes.length).equals(1) @@ -173,8 +172,8 @@ o.spec("textContent", function() { var vnode = m("a") var updated = m("a", "b") - render(root, vnode) - render(root, updated) + m.render(root, vnode) + m.render(root, updated) o(root.childNodes.length).equals(1) o(vnode.dom.childNodes.length).equals(1) @@ -185,8 +184,8 @@ o.spec("textContent", function() { var vnode = m("a", "a") var updated = m("a") - render(root, vnode) - render(root, updated) + m.render(root, vnode) + m.render(root, updated) o(root.childNodes.length).equals(1) o(vnode.dom.childNodes.length).equals(0) diff --git a/tests/render/updateElement.js b/tests/render/updateElement.js index de213ff7a..74113bc04 100644 --- a/tests/render/updateElement.js +++ b/tests/render/updateElement.js @@ -1,8 +1,7 @@ import o from "ospec" import domMock from "../../test-utils/domMock.js" -import m from "../../src/core/hyperscript.js" -import render from "../../src/core/render.js" +import m from "../../src/entry/mithril.esm.js" o.spec("updateElement", function() { var $window, root @@ -15,8 +14,8 @@ o.spec("updateElement", function() { var vnode = m("a", {id: "b"}) var updated = m("a", {id: "c"}) - render(root, vnode) - render(root, updated) + m.render(root, vnode) + m.render(root, updated) o(updated.dom).equals(vnode.dom) o(updated.dom).equals(root.firstChild) @@ -26,8 +25,8 @@ o.spec("updateElement", function() { var vnode = m("a", {id: "b"}) var updated = m("a", {id: "c", title: "d"}) - render(root, vnode) - render(root, updated) + m.render(root, vnode) + m.render(root, updated) o(updated.dom).equals(vnode.dom) o(updated.dom).equals(root.firstChild) @@ -37,8 +36,8 @@ o.spec("updateElement", function() { var vnode = m("a") var updated = m("a", {title: "d"}) - render(root, vnode) - render(root, updated) + m.render(root, vnode) + m.render(root, updated) o(updated.dom).equals(vnode.dom) o(updated.dom).equals(root.firstChild) @@ -48,8 +47,8 @@ o.spec("updateElement", function() { var vnode = m("a", {id: "b", title: "d"}) var updated = m("a", {id: "c"}) - render(root, vnode) - render(root, updated) + m.render(root, vnode) + m.render(root, updated) o(updated.dom).equals(vnode.dom) o(updated.dom).equals(root.firstChild) @@ -59,8 +58,8 @@ o.spec("updateElement", function() { var vnode = m("a", {id: "b", className: "d"}) var updated = m("a", {id: "c"}) - render(root, vnode) - render(root, updated) + m.render(root, vnode) + m.render(root, updated) o(updated.dom).equals(vnode.dom) o(updated.dom).equals(root.firstChild) @@ -70,8 +69,8 @@ o.spec("updateElement", function() { var vnode = m("a") var updated = m("a", {style: {backgroundColor: "green"}}) - render(root, vnode) - render(root, updated) + m.render(root, vnode) + m.render(root, updated) o(updated.dom.style.backgroundColor).equals("green") }) @@ -79,8 +78,8 @@ o.spec("updateElement", function() { var vnode = m("a") var updated = m("a", {style: "background-color:green"}) - render(root, vnode) - render(root, updated) + m.render(root, vnode) + m.render(root, updated) o(updated.dom.style.backgroundColor).equals("green") }) @@ -88,8 +87,8 @@ o.spec("updateElement", function() { var vnode = m("a", {style: {backgroundColor: "red"}}) var updated = m("a", {style: {backgroundColor: "green"}}) - render(root, vnode) - render(root, updated) + m.render(root, vnode) + m.render(root, updated) o(updated.dom.style.backgroundColor).equals("green") }) @@ -97,8 +96,8 @@ o.spec("updateElement", function() { var vnode = m("a", {style: {backgroundColor: "red"}}) var updated = m("a", {style: "background-color:green;"}) - render(root, vnode) - render(root, updated) + m.render(root, vnode) + m.render(root, updated) o(updated.dom.style.backgroundColor).equals("green") }) @@ -106,8 +105,8 @@ o.spec("updateElement", function() { var vnode = m("a", {style: "background-color:green;"}) var updated = m("a", {style: "background-color:green;"}) - render(root, vnode) - render(root, updated) + m.render(root, vnode) + m.render(root, updated) o(updated.dom.style.backgroundColor).equals("green") }) @@ -115,8 +114,8 @@ o.spec("updateElement", function() { var vnode = m("a", {style: {backgroundColor: "red"}}) var updated = m("a", {style: {backgroundColor: "red"}}) - render(root, vnode) - render(root, updated) + m.render(root, vnode) + m.render(root, updated) o(updated.dom.style.backgroundColor).equals("red") }) @@ -124,8 +123,8 @@ o.spec("updateElement", function() { var vnode = m("a", {style: "background-color:red;"}) var updated = m("a", {style: {backgroundColor: "green"}}) - render(root, vnode) - render(root, updated) + m.render(root, vnode) + m.render(root, updated) o(updated.dom.style.backgroundColor).equals("green") }) @@ -133,8 +132,8 @@ o.spec("updateElement", function() { var vnode = m("a", {style: "background-color:red;"}) var updated = m("a", {style: "background-color:green;"}) - render(root, vnode) - render(root, updated) + m.render(root, vnode) + m.render(root, updated) o(updated.dom.style.backgroundColor).equals("green") }) @@ -142,8 +141,8 @@ o.spec("updateElement", function() { var vnode = m("a", {style: {backgroundColor: "red", border: "1px solid red"}}) var updated = m("a", {style: {backgroundColor: "red"}}) - render(root, vnode) - render(root, updated) + m.render(root, vnode) + m.render(root, updated) o(updated.dom.style.backgroundColor).equals("red") o(updated.dom.style.border).equals("") @@ -152,8 +151,8 @@ o.spec("updateElement", function() { var vnode = m("a", {style: "background-color:red;border:1px solid red"}) var updated = m("a", {style: {backgroundColor: "red"}}) - render(root, vnode) - render(root, updated) + m.render(root, vnode) + m.render(root, updated) o(updated.dom.style.backgroundColor).equals("red") o(updated.dom.style.border).notEquals("1px solid red") @@ -162,8 +161,8 @@ o.spec("updateElement", function() { var vnode = m("a", {style: {backgroundColor: "red", border: "1px solid red"}}) var updated = m("a", {style: "background-color:red"}) - render(root, vnode) - render(root, updated) + m.render(root, vnode) + m.render(root, updated) o(updated.dom.style.backgroundColor).equals("red") o(updated.dom.style.border).equals("") @@ -172,8 +171,8 @@ o.spec("updateElement", function() { var vnode = m("a", {style: "background-color:red;border:1px solid red"}) var updated = m("a", {style: "background-color:red"}) - render(root, vnode) - render(root, updated) + m.render(root, vnode) + m.render(root, updated) o(updated.dom.style.backgroundColor).equals("red") o(updated.dom.style.border).equals("") @@ -182,12 +181,12 @@ o.spec("updateElement", function() { var style = {color: "gold"} var vnode = m("a", {style: style}) - render(root, vnode) + m.render(root, vnode) root.firstChild.style.color = "red" style = {color: "gold"} var updated = m("a", {style: style}) - render(root, updated) + m.render(root, updated) o(updated.dom.style.color).equals("red") }) @@ -195,12 +194,12 @@ o.spec("updateElement", function() { var vnode = m("p", {style: "background-color: red"}) var updated = m("p", {style: null}) - render(root, vnode) + m.render(root, vnode) o("style" in vnode.dom.attributes).equals(true) o(vnode.dom.attributes.style.value).equals("background-color: red;") - render(root, updated) + m.render(root, updated) //browsers disagree here try { @@ -215,12 +214,12 @@ o.spec("updateElement", function() { var vnode = m("p", {style: "background-color: red"}) var updated = m("p", {style: undefined}) - render(root, vnode) + m.render(root, vnode) o("style" in vnode.dom.attributes).equals(true) o(vnode.dom.attributes.style.value).equals("background-color: red;") - render(root, updated) + m.render(root, updated) //browsers disagree here try { @@ -237,12 +236,12 @@ o.spec("updateElement", function() { var vnode = m("p", {style: "background-color: red"}) var updated = m("p") - render(root, vnode) + m.render(root, vnode) o("style" in vnode.dom.attributes).equals(true) o(vnode.dom.attributes.style.value).equals("background-color: red;") - render(root, updated) + m.render(root, updated) //browsers disagree here try { @@ -259,8 +258,8 @@ o.spec("updateElement", function() { var vnode = m("a") var updated = m("b") - render(root, vnode) - render(root, updated) + m.render(root, vnode) + m.render(root, updated) o(updated.dom).equals(root.firstChild) o(updated.dom.nodeName).equals("B") @@ -269,8 +268,8 @@ o.spec("updateElement", function() { var vnode = m("svg", {className: "a"}) var updated = m("svg", {className: "b"}) - render(root, vnode) - render(root, updated) + m.render(root, vnode) + m.render(root, updated) o(updated.dom.attributes["class"].value).equals("b") }) @@ -278,8 +277,8 @@ o.spec("updateElement", function() { var vnode = m("svg", m("circle")) var updated = m("svg", m("line")) - render(root, vnode) - render(root, updated) + m.render(root, vnode) + m.render(root, updated) o(updated.dom.firstChild.namespaceURI).equals("http://www.w3.org/2000/svg") }) diff --git a/tests/render/updateFragment.js b/tests/render/updateFragment.js index 44d822b3c..73220867d 100644 --- a/tests/render/updateFragment.js +++ b/tests/render/updateFragment.js @@ -1,8 +1,7 @@ import o from "ospec" import domMock from "../../test-utils/domMock.js" -import m from "../../src/core/hyperscript.js" -import render from "../../src/core/render.js" +import m from "../../src/entry/mithril.esm.js" o.spec("updateFragment", function() { var $window, root @@ -15,8 +14,8 @@ o.spec("updateFragment", function() { var vnode = [m("a")] var updated = [m("b")] - render(root, vnode) - render(root, updated) + m.render(root, vnode) + m.render(root, updated) o(updated[0].dom).equals(root.firstChild) o(updated[0].dom.nodeName).equals("B") @@ -25,8 +24,8 @@ o.spec("updateFragment", function() { var vnode = [] var updated = [m("a"), m("b")] - render(root, vnode) - render(root, updated) + m.render(root, vnode) + m.render(root, updated) o(updated[0].dom).equals(root.firstChild) o(root.childNodes.length).equals(2) @@ -37,8 +36,8 @@ o.spec("updateFragment", function() { var vnode = [m("a"), m("b")] var updated = [] - render(root, vnode) - render(root, updated) + m.render(root, vnode) + m.render(root, updated) o(root.childNodes.length).equals(0) }) @@ -46,8 +45,8 @@ o.spec("updateFragment", function() { var vnode = [] var updated = [m("a")] - render(root, vnode) - render(root, updated) + m.render(root, vnode) + m.render(root, updated) o(updated[0].dom).equals(root.firstChild) o(updated[0].dom.nodeName).equals("A") @@ -56,8 +55,8 @@ o.spec("updateFragment", function() { var vnode = [m("a")] var updated = [] - render(root, vnode) - render(root, updated) + m.render(root, vnode) + m.render(root, updated) o(root.childNodes.length).equals(0) }) diff --git a/tests/render/updateNodes.js b/tests/render/updateNodes.js index 36820671e..5178d6a25 100644 --- a/tests/render/updateNodes.js +++ b/tests/render/updateNodes.js @@ -1,8 +1,7 @@ import o from "ospec" import domMock from "../../test-utils/domMock.js" -import m from "../../src/core/hyperscript.js" -import render from "../../src/core/render.js" +import m from "../../src/entry/mithril.esm.js" function vnodify(str) { return str.split(",").map((k) => m.key(k, m(k))) @@ -19,8 +18,8 @@ o.spec("updateNodes", function() { var vnodes = [m.key(1, m("a")), m.key(2, m("b"))] var updated = [m.key(1, m("a")), m.key(2, m("b"))] - render(root, vnodes) - render(root, updated) + m.render(root, vnodes) + m.render(root, updated) o(Array.from(root.childNodes, (n) => n.nodeName)).deepEquals(["A", "B"]) o(updated[0].children[0].dom).equals(root.childNodes[0]) @@ -30,8 +29,8 @@ o.spec("updateNodes", function() { var vnodes = [m("a"), m("b")] var updated = [m("a"), m("b")] - render(root, vnodes) - render(root, updated) + m.render(root, vnodes) + m.render(root, updated) o(Array.from(root.childNodes, (n) => n.nodeName)).deepEquals(["A", "B"]) o(updated[0].dom).equals(root.childNodes[0]) @@ -41,8 +40,8 @@ o.spec("updateNodes", function() { var vnodes = "a" var updated = "a" - render(root, vnodes) - render(root, updated) + m.render(root, vnodes) + m.render(root, updated) o(Array.from(root.childNodes, (n) => n.nodeValue)).deepEquals(["a"]) }) @@ -50,8 +49,8 @@ o.spec("updateNodes", function() { var vnodes = 1 var updated = "1" - render(root, vnodes) - render(root, updated) + m.render(root, vnodes) + m.render(root, updated) o(Array.from(root.childNodes, (n) => n.nodeValue)).deepEquals(["1"]) }) @@ -59,8 +58,8 @@ o.spec("updateNodes", function() { var vnodes = 0 var updated = "0" - render(root, vnodes) - render(root, updated) + m.render(root, vnodes) + m.render(root, updated) o(Array.from(root.childNodes, (n) => n.nodeValue)).deepEquals(["0"]) }) @@ -68,8 +67,8 @@ o.spec("updateNodes", function() { var vnodes = [m("a")] var updated = [m("a")] - render(root, vnodes) - render(root, updated) + m.render(root, vnodes) + m.render(root, updated) o(Array.from(root.childNodes, (n) => n.nodeName)).deepEquals(["A"]) o(updated[0].dom).equals(root.childNodes[0]) @@ -78,8 +77,8 @@ o.spec("updateNodes", function() { var vnodes = [m.normalize("a")] var updated = [m.normalize("a")] - render(root, vnodes) - render(root, updated) + m.render(root, vnodes) + m.render(root, updated) o(Array.from(root.childNodes, (n) => n.nodeValue)).deepEquals(["a"]) o(updated[0].dom).equals(root.childNodes[0]) @@ -88,8 +87,8 @@ o.spec("updateNodes", function() { var vnodes = [null, m("div")] var updated = [undefined, m("div")] - render(root, vnodes) - render(root, updated) + m.render(root, vnodes) + m.render(root, updated) o(root.childNodes.length).equals(1) }) @@ -97,8 +96,8 @@ o.spec("updateNodes", function() { var vnodes = [m.key(1, m("a")), m.key(2, m("b")), m.key(3, m("i")), m.key(4, m("s"))] var updated = [m.key(4, m("s")), m.key(3, m("i")), m.key(2, m("b")), m.key(1, m("a"))] - render(root, vnodes) - render(root, updated) + m.render(root, vnodes) + m.render(root, updated) o(Array.from(root.childNodes, (n) => n.nodeName)).deepEquals(["S", "I", "B", "A"]) o(updated[0].children[0].dom).equals(root.childNodes[0]) @@ -109,8 +108,8 @@ o.spec("updateNodes", function() { o("reverses els w/ odd count", function() { var vnodes = [m.key(1, m("a")), m.key(2, m("b")), m.key(3, m("i"))] var updated = [m.key(3, m("i")), m.key(2, m("b")), m.key(1, m("a"))] - render(root, vnodes) - render(root, updated) + m.render(root, vnodes) + m.render(root, updated) o(Array.from(root.childNodes, (n) => n.nodeName)).deepEquals(["I", "B", "A"]) }) @@ -118,8 +117,8 @@ o.spec("updateNodes", function() { var vnodes = [m.key(1, m("a"))] var updated = [m.key(2, m("b")), m.key(1, m("a"))] - render(root, vnodes) - render(root, updated) + m.render(root, vnodes) + m.render(root, updated) o(Array.from(root.childNodes, (n) => n.nodeName)).deepEquals(["B", "A"]) o(updated[0].children[0].dom).equals(root.childNodes[0]) @@ -129,8 +128,8 @@ o.spec("updateNodes", function() { var vnodes = [m.key(1, m("a"))] var updated = [m.key(1, m("a")), m.key(2, m("b"))] - render(root, vnodes) - render(root, updated) + m.render(root, vnodes) + m.render(root, updated) o(Array.from(root.childNodes, (n) => n.nodeName)).deepEquals(["A", "B"]) o(updated[0].children[0].dom).equals(root.childNodes[0]) @@ -140,8 +139,8 @@ o.spec("updateNodes", function() { var vnodes = [m.key(1, m("a")), m.key(2, m("b"))] var updated = [m.key(1, m("a")), m.key(3, m("i")), m.key(2, m("b"))] - render(root, vnodes) - render(root, updated) + m.render(root, vnodes) + m.render(root, updated) o(Array.from(root.childNodes, (n) => n.nodeName)).deepEquals(["A", "I", "B"]) o(updated[0].children[0].dom).equals(root.childNodes[0]) @@ -152,8 +151,8 @@ o.spec("updateNodes", function() { var vnodes = [m.key(1, m("a")), m.key(2, m("b"))] var updated = [m.key(2, m("b")), m.key(3, m("i")), m.key(1, m("a"))] - render(root, vnodes) - render(root, updated) + m.render(root, vnodes) + m.render(root, updated) o(Array.from(root.childNodes, (n) => n.nodeName)).deepEquals(["B", "I", "A"]) o(updated[0].children[0].dom).equals(root.childNodes[0]) @@ -164,8 +163,8 @@ o.spec("updateNodes", function() { var vnodes = [m.key(2, m("b")), m.key(1, m("a"))] var updated = [m.key(1, m("a"))] - render(root, vnodes) - render(root, updated) + m.render(root, vnodes) + m.render(root, updated) o(Array.from(root.childNodes, (n) => n.nodeName)).deepEquals(["A"]) o(updated[0].children[0].dom).equals(root.childNodes[0]) @@ -174,8 +173,8 @@ o.spec("updateNodes", function() { var vnodes = [m.key(1, m("a")), m.key(2, m("b"))] var updated = [m.key(1, m("a"))] - render(root, vnodes) - render(root, updated) + m.render(root, vnodes) + m.render(root, updated) o(Array.from(root.childNodes, (n) => n.nodeName)).deepEquals(["A"]) o(updated[0].children[0].dom).equals(root.childNodes[0]) @@ -184,8 +183,8 @@ o.spec("updateNodes", function() { var vnodes = [m.key(1, m("a")), m.key(3, m("i")), m.key(2, m("b"))] var updated = [m.key(1, m("a")), m.key(2, m("b"))] - render(root, vnodes) - render(root, updated) + m.render(root, vnodes) + m.render(root, updated) o(Array.from(root.childNodes, (n) => n.nodeName)).deepEquals(["A", "B"]) o(updated[0].children[0].dom).equals(root.childNodes[0]) @@ -195,8 +194,8 @@ o.spec("updateNodes", function() { var vnodes = [m.key(1, m("a")), m.key(3, m("i")), m.key(2, m("b"))] var updated = [m.key(2, m("b")), m.key(1, m("a"))] - render(root, vnodes) - render(root, updated) + m.render(root, vnodes) + m.render(root, updated) o(Array.from(root.childNodes, (n) => n.nodeName)).deepEquals(["B", "A"]) o(updated[0].children[0].dom).equals(root.childNodes[0]) @@ -206,8 +205,8 @@ o.spec("updateNodes", function() { var vnodes = [m.key(1, m("a")), m.key(3, m("i")), m.key(2, m("b"))] var updated = [m.key(2, m("b")), m.key(1, m("a")), m.key(4, m("s"))] - render(root, vnodes) - render(root, updated) + m.render(root, vnodes) + m.render(root, updated) o(Array.from(root.childNodes, (n) => n.nodeName)).deepEquals(["B", "A", "S"]) o(updated[0].children[0].dom).equals(root.childNodes[0]) @@ -218,8 +217,8 @@ o.spec("updateNodes", function() { var vnodes = [m.key("__proto__", m("a")), m.key(3, m("i")), m.key(2, m("b"))] var updated = [m.key(2, m("b")), m.key("__proto__", m("a")), m.key(4, m("s"))] - render(root, vnodes) - render(root, updated) + m.render(root, vnodes) + m.render(root, updated) o(Array.from(root.childNodes, (n) => n.nodeName)).deepEquals(["B", "A", "S"]) o(updated[0].children[0].dom).equals(root.childNodes[0]) @@ -230,8 +229,8 @@ o.spec("updateNodes", function() { var vnodes = [m.key(1), m.key(2, m("b"))] var updated = [m.key(1, m("a")), m.key(2, m("b"))] - render(root, vnodes) - render(root, updated) + m.render(root, vnodes) + m.render(root, updated) o(Array.from(root.childNodes, (n) => n.nodeName)).deepEquals(["A", "B"]) o(updated[0].children[0].dom).equals(root.childNodes[0]) @@ -241,8 +240,8 @@ o.spec("updateNodes", function() { var vnodes = [m.key(1, m.key(2, m("a")), m.key(3, m("b"))), m.key(4, m("i"))] var updated = [m.key(1, m.key(3, m("b")), m.key(2, m("a"))), m.key(4, m("i"))] - render(root, vnodes) - render(root, updated) + m.render(root, vnodes) + m.render(root, updated) o(Array.from(root.childNodes, (n) => n.nodeName)).deepEquals(["B", "A", "I"]) o(updated[0].children[0].children[0].dom).equals(root.childNodes[0]) @@ -253,8 +252,8 @@ o.spec("updateNodes", function() { var vnodes = [m.key(1), m.key(2, m("i"))] var updated = [m.key(1, m("a"), m("b")), m.key(2, m("i"))] - render(root, vnodes) - render(root, updated) + m.render(root, vnodes) + m.render(root, updated) o(Array.from(root.childNodes, (n) => n.nodeName)).deepEquals(["A", "B", "I"]) o(updated[0].children[0].dom).equals(root.childNodes[0]) @@ -264,21 +263,21 @@ o.spec("updateNodes", function() { o("throws if fragment followed by null then el on first render keyed", function() { var vnodes = [m.key(1), null, m.key(2, m("i"))] - o(function () { render(root, vnodes) }).throws(TypeError) + o(() => m.render(root, vnodes)).throws(TypeError) }) o("throws if fragment followed by null then el on next render keyed", function() { var vnodes = [m.key(1), m.key(2, m("i"))] var updated = [m.key(1, m("a"), m("b")), null, m.key(2, m("i"))] - render(root, vnodes) - o(function () { render(root, updated) }).throws(TypeError) + m.render(root, vnodes) + o(() => m.render(root, updated)).throws(TypeError) }) o("populates childless fragment replaced followed by el keyed", function() { var vnodes = [m.key(1), m.key(2, m("i"))] var updated = [m.key(1, m("a"), m("b")), m.key(2, m("i"))] - render(root, vnodes) - render(root, updated) + m.render(root, vnodes) + m.render(root, updated) o(Array.from(root.childNodes, (n) => n.nodeName)).deepEquals(["A", "B", "I"]) o(updated[0].children[0].dom).equals(root.childNodes[0]) @@ -289,15 +288,15 @@ o.spec("updateNodes", function() { var vnodes = [m.key(1), m.key(2, m("i"))] var updated = [m.key(1, m("a"), m("b")), null, m.key(2, m("i"))] - render(root, vnodes) - o(function () { render(root, updated) }).throws(TypeError) + m.render(root, vnodes) + o(() => m.render(root, updated)).throws(TypeError) }) o("moves from end to start", function() { var vnodes = [m.key(1, m("a")), m.key(2, m("b")), m.key(3, m("i")), m.key(4, m("s"))] var updated = [m.key(4, m("s")), m.key(1, m("a")), m.key(2, m("b")), m.key(3, m("i"))] - render(root, vnodes) - render(root, updated) + m.render(root, vnodes) + m.render(root, updated) o(Array.from(root.childNodes, (n) => n.nodeName)).deepEquals(["S", "A", "B", "I"]) o(updated[0].children[0].dom).equals(root.childNodes[0]) @@ -309,8 +308,8 @@ o.spec("updateNodes", function() { var vnodes = [m.key(1, m("a")), m.key(2, m("b")), m.key(3, m("i")), m.key(4, m("s"))] var updated = [m.key(2, m("b")), m.key(3, m("i")), m.key(4, m("s")), m.key(1, m("a"))] - render(root, vnodes) - render(root, updated) + m.render(root, vnodes) + m.render(root, updated) o(Array.from(root.childNodes, (n) => n.nodeName)).deepEquals(["B", "I", "S", "A"]) o(updated[0].children[0].dom).equals(root.childNodes[0]) @@ -323,9 +322,9 @@ o.spec("updateNodes", function() { var temp = [] var updated = [m.key(1, m("a")), m.key(2, m("b")), m.key(3, m("i")), m.key(4, m("s"))] - render(root, vnodes) - render(root, temp) - render(root, updated) + m.render(root, vnodes) + m.render(root, temp) + m.render(root, updated) o(Array.from(root.childNodes, (n) => n.nodeName)).deepEquals(["A", "B", "I", "S"]) o(updated[0].children[0].dom).equals(root.childNodes[0]) @@ -338,9 +337,9 @@ o.spec("updateNodes", function() { var temp = [] var updated = [m.key(4, m("s")), m.key(3, m("i")), m.key(2, m("b")), m.key(1, m("a"))] - render(root, vnodes) - render(root, temp) - render(root, updated) + m.render(root, vnodes) + m.render(root, temp) + m.render(root, updated) o(Array.from(root.childNodes, (n) => n.nodeName)).deepEquals(["S", "I", "B", "A"]) o(updated[0].children[0].dom).equals(root.childNodes[0]) @@ -353,9 +352,9 @@ o.spec("updateNodes", function() { var temp = [] var updated = [m.key(1, m("a"))] - render(root, vnodes) - render(root, temp) - render(root, updated) + m.render(root, vnodes) + m.render(root, temp) + m.render(root, updated) o(Array.from(root.childNodes, (n) => n.nodeName)).deepEquals(["A"]) o(updated[0].children[0].dom).equals(root.childNodes[0]) @@ -365,9 +364,9 @@ o.spec("updateNodes", function() { var temp = [] var updated = [m.key(1, m("a")), m.key(2, m("b")), m.key(3, m("i"))] - render(root, vnodes) - render(root, temp) - render(root, updated) + m.render(root, vnodes) + m.render(root, temp) + m.render(root, updated) o(Array.from(root.childNodes, (n) => n.nodeName)).deepEquals(["A", "B", "I"]) o(updated[0].children[0].dom).equals(root.childNodes[0]) @@ -379,9 +378,9 @@ o.spec("updateNodes", function() { var temp = [] var updated = [m.key(3, m("i")), m.key(4, m("s"))] - render(root, vnodes) - render(root, temp) - render(root, updated) + m.render(root, vnodes) + m.render(root, temp) + m.render(root, updated) o(Array.from(root.childNodes, (n) => n.nodeName)).deepEquals(["I", "S"]) o(updated[0].children[0].dom).equals(root.childNodes[0]) @@ -392,9 +391,9 @@ o.spec("updateNodes", function() { var temp = [] var updated = [m.key(3, m("i"))] - render(root, vnodes) - render(root, temp) - render(root, updated) + m.render(root, vnodes) + m.render(root, temp) + m.render(root, updated) o(Array.from(root.childNodes, (n) => n.nodeName)).deepEquals(["I"]) o(updated[0].children[0].dom).equals(root.childNodes[0]) @@ -404,9 +403,9 @@ o.spec("updateNodes", function() { var temp = [] var updated = [m.key(3, m("i")), m.key(4, m("s")), m.key(5, m("div"))] - render(root, vnodes) - render(root, temp) - render(root, updated) + m.render(root, vnodes) + m.render(root, temp) + m.render(root, updated) o(Array.from(root.childNodes, (n) => n.nodeName)).deepEquals(["I", "S", "DIV"]) o(updated[0].children[0].dom).equals(root.childNodes[0]) @@ -418,9 +417,9 @@ o.spec("updateNodes", function() { var temp = [] var updated = [m.key(1, m("a")), m.key(4, m("s"))] - render(root, vnodes) - render(root, temp) - render(root, updated) + m.render(root, vnodes) + m.render(root, temp) + m.render(root, updated) o(Array.from(root.childNodes, (n) => n.nodeName)).deepEquals(["A", "S"]) o(updated[0].children[0].dom).equals(root.childNodes[0]) @@ -431,9 +430,9 @@ o.spec("updateNodes", function() { var temp = [] var updated = [m.key(4, m("s")), m.key(1, m("a"))] - render(root, vnodes) - render(root, temp) - render(root, updated) + m.render(root, vnodes) + m.render(root, temp) + m.render(root, updated) o(Array.from(root.childNodes, (n) => n.nodeName)).deepEquals(["S", "A"]) o(updated[0].children[0].dom).equals(root.childNodes[0]) @@ -444,9 +443,9 @@ o.spec("updateNodes", function() { var temp = [] var updated = [m.key(1, m("a")), m.key(4, m("s"))] - render(root, vnodes) - render(root, temp) - render(root, updated) + m.render(root, vnodes) + m.render(root, temp) + m.render(root, updated) o(Array.from(root.childNodes, (n) => n.nodeName)).deepEquals(["A", "S"]) o(updated[0].children[0].dom).equals(root.childNodes[0]) @@ -457,9 +456,9 @@ o.spec("updateNodes", function() { var temp = [] var updated = [m.key(4, m("s")), m.key(1, m("a"))] - render(root, vnodes) - render(root, temp) - render(root, updated) + m.render(root, vnodes) + m.render(root, temp) + m.render(root, updated) o(Array.from(root.childNodes, (n) => n.nodeName)).deepEquals(["S", "A"]) o(updated[0].children[0].dom).equals(root.childNodes[0]) @@ -470,9 +469,9 @@ o.spec("updateNodes", function() { var temp = [] var updated = [m.key(1, m("a")), m.key(3, m("i")), m.key(4, m("s"))] - render(root, vnodes) - render(root, temp) - render(root, updated) + m.render(root, vnodes) + m.render(root, temp) + m.render(root, updated) o(Array.from(root.childNodes, (n) => n.nodeName)).deepEquals(["A", "I", "S"]) o(updated[0].children[0].dom).equals(root.childNodes[0]) @@ -484,9 +483,9 @@ o.spec("updateNodes", function() { var temp = [] var updated = [m.key(4, m("s")), m.key(3, m("i")), m.key(1, m("a"))] - render(root, vnodes) - render(root, temp) - render(root, updated) + m.render(root, vnodes) + m.render(root, temp) + m.render(root, updated) o(Array.from(root.childNodes, (n) => n.nodeName)).deepEquals(["S", "I", "A"]) o(updated[0].children[0].dom).equals(root.childNodes[0]) @@ -497,8 +496,8 @@ o.spec("updateNodes", function() { var vnodes = m("div", undefined, "a") var updated = m("div", ["b"], undefined, undefined) - render(root, vnodes) - render(root, updated) + m.render(root, vnodes) + m.render(root, updated) o(root.firstChild.childNodes.length).equals(1) }) @@ -508,10 +507,10 @@ o.spec("updateNodes", function() { var temp2 = [m.key(1, m("a", m.key(3, m("i")), m.key(4, m("s")))), m.key(2, m("b"))] var updated = [m.key(1, m("a", m.key(4, m("s")), m.key(3, m("i")))), m.key(2, m("b"))] - render(root, vnodes) - render(root, temp1) - render(root, temp2) - render(root, updated) + m.render(root, vnodes) + m.render(root, temp1) + m.render(root, temp2) + m.render(root, updated) o(Array.from(root.childNodes, (n) => n.nodeName)).deepEquals(["A", "B"]) o(Array.from(root.childNodes[0].childNodes, (n) => n.nodeName)).deepEquals(["S", "I"]) @@ -525,9 +524,9 @@ o.spec("updateNodes", function() { var temp = [] var updated = [m.key(1, m("a", m.key(3, m("a", m.key(5, m("a")))), m.key(4, m("a", m.key(5, m("a")))))), m.key(2, m("a"))] - render(root, vnodes) - render(root, temp) - render(root, updated) + m.render(root, vnodes) + m.render(root, temp) + m.render(root, updated) o(Array.from(root.childNodes, (n) => n.nodeName)).deepEquals(["A", "A"]) o(Array.from(root.childNodes[0].childNodes, (n) => n.nodeName)).deepEquals(["A", "A"]) @@ -537,134 +536,134 @@ o.spec("updateNodes", function() { o("reused top-level element children are rejected against the same root", function () { var cached = m("a") - render(root, cached) - o(() => render(root, cached)).throws(Error) + m.render(root, cached) + o(() => m.render(root, cached)).throws(Error) }) o("reused top-level element children are rejected against a different root", function () { var cached = m("a") var otherRoot = $window.document.createElement("div") - render(root, cached) - o(() => render(otherRoot, cached)).throws(Error) + m.render(root, cached) + o(() => m.render(otherRoot, cached)).throws(Error) }) o("reused inner fragment element children are rejected against the same root", function () { var cached = m("a") - render(root, [cached]) - o(() => render(root, [cached])).throws(Error) + m.render(root, [cached]) + o(() => m.render(root, [cached])).throws(Error) }) o("reused inner fragment element children are rejected against a different root", function () { var cached = m("a") var otherRoot = $window.document.createElement("div") - render(root, [cached]) - o(() => render(otherRoot, [cached])).throws(Error) + m.render(root, [cached]) + o(() => m.render(otherRoot, [cached])).throws(Error) }) o("reused inner element element children are rejected against the same root", function () { var cached = m("a") - render(root, m("div", cached)) - o(() => render(root, m("div", cached))).throws(Error) + m.render(root, m("div", cached)) + o(() => m.render(root, m("div", cached))).throws(Error) }) o("reused inner element element children are rejected against a different root", function () { var cached = m("a") var otherRoot = $window.document.createElement("div") - render(root, m("div", cached)) - o(() => render(otherRoot, m("div", cached))).throws(Error) + m.render(root, m("div", cached)) + o(() => m.render(otherRoot, m("div", cached))).throws(Error) }) o("reused top-level retain children are rejected against the same root", function () { var cached = m.retain() - render(root, m("a")) - render(root, cached) - o(() => render(root, cached)).throws(Error) + m.render(root, m("a")) + m.render(root, cached) + o(() => m.render(root, cached)).throws(Error) }) o("reused top-level retain children are rejected against a different root", function () { var cached = m.retain() var otherRoot = $window.document.createElement("div") - render(root, m("a")) - render(root, cached) - o(() => render(otherRoot, cached)).throws(Error) + m.render(root, m("a")) + m.render(root, cached) + o(() => m.render(otherRoot, cached)).throws(Error) }) o("reused inner fragment retain children are rejected against the same root", function () { var cached = m.retain() - render(root, [m("a")]) - render(root, [cached]) - o(() => render(root, [cached])).throws(Error) + m.render(root, [m("a")]) + m.render(root, [cached]) + o(() => m.render(root, [cached])).throws(Error) }) o("reused inner fragment retain children are rejected against a different root", function () { var cached = m.retain() var otherRoot = $window.document.createElement("div") - render(root, [m("a")]) - render(root, [cached]) - o(() => render(otherRoot, [cached])).throws(Error) + m.render(root, [m("a")]) + m.render(root, [cached]) + o(() => m.render(otherRoot, [cached])).throws(Error) }) o("reused inner element retain children are rejected against the same root", function () { var cached = m.retain() - render(root, m("div", m("a"))) - render(root, m("div", cached)) - o(() => render(root, m("div", cached))).throws(Error) + m.render(root, m("div", m("a"))) + m.render(root, m("div", cached)) + o(() => m.render(root, m("div", cached))).throws(Error) }) o("reused inner element retain children are rejected against a different root", function () { var cached = m.retain() var otherRoot = $window.document.createElement("div") - render(root, m("div", m("a"))) - render(root, m("div", cached)) - o(() => render(otherRoot, m("div", cached))).throws(Error) + m.render(root, m("div", m("a"))) + m.render(root, m("div", cached)) + o(() => m.render(otherRoot, m("div", cached))).throws(Error) }) o("cross-removal reused top-level element children are rejected against the same root", function () { var cached = m("a") - render(root, cached) - render(root, null) - o(() => render(root, cached)).throws(Error) + m.render(root, cached) + m.render(root, null) + o(() => m.render(root, cached)).throws(Error) }) o("cross-removal reused inner fragment element children are rejected against the same root", function () { var cached = m("a") - render(root, [cached]) - render(root, null) - o(() => render(root, [cached])).throws(Error) + m.render(root, [cached]) + m.render(root, null) + o(() => m.render(root, [cached])).throws(Error) }) o("cross-removal reused inner element element children are rejected against the same root", function () { var cached = m("a") - render(root, m("div", cached)) - render(root, null) - o(() => render(root, m("div", cached))).throws(Error) + m.render(root, m("div", cached)) + m.render(root, null) + o(() => m.render(root, m("div", cached))).throws(Error) }) o("cross-removal reused top-level retain children are rejected against the same root", function () { var cached = m.retain() - render(root, m("a")) - render(root, cached) - render(root, null) - render(root, m("a")) - o(() => render(root, cached)).throws(Error) + m.render(root, m("a")) + m.render(root, cached) + m.render(root, null) + m.render(root, m("a")) + o(() => m.render(root, cached)).throws(Error) }) o("cross-removal reused inner fragment retain children are rejected against the same root", function () { var cached = m.retain() - render(root, [m("a")]) - render(root, [cached]) - render(root, null) - render(root, [m("a")]) - o(() => render(root, [cached])).throws(Error) + m.render(root, [m("a")]) + m.render(root, [cached]) + m.render(root, null) + m.render(root, [m("a")]) + o(() => m.render(root, [cached])).throws(Error) }) o("cross-removal reused inner element retain children are rejected against the same root", function () { var cached = m.retain() - render(root, m("div", m("a"))) - render(root, m("div", cached)) - render(root, null) - render(root, m("div", m("a"))) - o(() => render(root, m("div", cached))).throws(Error) + m.render(root, m("div", m("a"))) + m.render(root, m("div", cached)) + m.render(root, null) + m.render(root, m("div", m("a"))) + o(() => m.render(root, m("div", cached))).throws(Error) }) o("null stays in place", function() { @@ -674,10 +673,10 @@ o.spec("updateNodes", function() { var temp = [null, m("a", m.layout(layout))] var updated = [m("div"), m("a", m.layout(layout))] - render(root, vnodes) + m.render(root, vnodes) var before = vnodes[1].dom - render(root, temp) - render(root, updated) + m.render(root, temp) + m.render(root, updated) var after = updated[1].dom o(before).equals(after) @@ -691,10 +690,10 @@ o.spec("updateNodes", function() { var temp = [m("b"), null, m("a", m.layout(layout))] var updated = [m("b"), m("div"), m("a", m.layout(layout))] - render(root, vnodes) + m.render(root, vnodes) var before = vnodes[2].dom - render(root, temp) - render(root, updated) + m.render(root, temp) + m.render(root, updated) var after = updated[2].dom o(before).equals(after) @@ -705,24 +704,24 @@ o.spec("updateNodes", function() { var vnode = m.key(1, m("b")) var updated = m("b") - render(root, vnode) - render(root, updated) + m.render(root, vnode) + m.render(root, updated) o(vnode.children[0].dom).notEquals(updated.dom) }) o("don't add back elements from fragments that are restored from the pool #1991", function() { - render(root, [ + m.render(root, [ [], [] ]) - render(root, [ + m.render(root, [ [], [m("div")] ]) - render(root, [ + m.render(root, [ [null] ]) - render(root, [ + m.render(root, [ [], [] ]) @@ -730,14 +729,14 @@ o.spec("updateNodes", function() { o(root.childNodes.length).equals(0) }) o("don't add back elements from fragments that are being removed #1991", function() { - render(root, [ + m.render(root, [ [], m("p"), ]) - render(root, [ + m.render(root, [ [m("div", 5)] ]) - render(root, [ + m.render(root, [ [], [] ]) @@ -748,28 +747,28 @@ o.spec("updateNodes", function() { var onabort = o.spy() var layout = o.spy((_, signal) => { signal.onabort = onabort }) - render(root, [m("div", m.layout(layout)), null]) - render(root, [null, m("div", m.layout(layout)), null]) + m.render(root, [m("div", m.layout(layout)), null]) + m.render(root, [null, m("div", m.layout(layout)), null]) o(layout.calls.map((c) => c.args[2])).deepEquals([true, true]) o(onabort.callCount).equals(1) }) o("supports changing the element of a keyed element in a list when traversed bottom-up", function() { - render(root, [m.key(2, m("a"))]) - render(root, [m.key(1, m("b")), m.key(2, m("b"))]) + m.render(root, [m.key(2, m("a"))]) + m.render(root, [m.key(1, m("b")), m.key(2, m("b"))]) o(Array.from(root.childNodes, (n) => n.nodeName)).deepEquals(["B", "B"]) }) o("supports changing the element of a keyed element in a list when looking up nodes using the map", function() { - render(root, [m.key(1, m("x")), m.key(2, m("y")), m.key(3, m("z"))]) - render(root, [m.key(2, m("b")), m.key(1, m("c")), m.key(4, m("d")), m.key(3, m("e"))]) + m.render(root, [m.key(1, m("x")), m.key(2, m("y")), m.key(3, m("z"))]) + m.render(root, [m.key(2, m("b")), m.key(1, m("c")), m.key(4, m("d")), m.key(3, m("e"))]) o(Array.from(root.childNodes, (n) => n.nodeName)).deepEquals(["B", "C", "D", "E"]) }) o("don't fetch the nextSibling from the pool", function() { - render(root, [[m.key(1, m("div")), m.key(2, m("div"))], m("p")]) - render(root, [[], m("p")]) - render(root, [[m.key(2, m("div")), m.key(1, m("div"))], m("p")]) + m.render(root, [[m.key(1, m("div")), m.key(2, m("div"))], m("p")]) + m.render(root, [[], m("p")]) + m.render(root, [[m.key(2, m("div")), m.key(1, m("div"))], m("p")]) o(Array.from(root.childNodes, (el) => el.nodeName)).deepEquals(["DIV", "DIV", "P"]) }) @@ -778,8 +777,8 @@ o.spec("updateNodes", function() { var updated = vnodify("d,c,b,a") var expectedTagNames = updated.map(function(vn) {return vn.children[0].tag}) - render(root, vnodes) - render(root, updated) + m.render(root, vnodes) + m.render(root, updated) var tagNames = Array.from(root.childNodes, (n) => n.nodeName.toLowerCase()) @@ -792,8 +791,8 @@ o.spec("updateNodes", function() { var updated = [m.key("c", m("c")), m.key("b", m("b")), m.key("a", m("a"))] var expectedTagNames = updated.map(function(vn) {return vn.children[0].tag}) - render(root, vnodes) - render(root, updated) + m.render(root, vnodes) + m.render(root, updated) var tagNames = Array.from(root.childNodes, (n) => n.nodeName.toLowerCase()) @@ -804,8 +803,8 @@ o.spec("updateNodes", function() { var updated = vnodify("i,b,a,d,c,j") var expectedTagNames = updated.map(function(vn) {return vn.children[0].tag}) - render(root, vnodes) - render(root, updated) + m.render(root, vnodes) + m.render(root, updated) var tagNames = Array.from(root.childNodes, (n) => n.nodeName.toLowerCase()) @@ -816,8 +815,8 @@ o.spec("updateNodes", function() { var updated = vnodify("i,d,c,b,a,j") var expectedTagNames = updated.map(function(vn) {return vn.children[0].tag}) - render(root, vnodes) - render(root, updated) + m.render(root, vnodes) + m.render(root, updated) var tagNames = Array.from(root.childNodes, (n) => n.nodeName.toLowerCase()) @@ -828,8 +827,8 @@ o.spec("updateNodes", function() { var updated = vnodify("i,c,b,a,j") var expectedTagNames = updated.map(function(vn) {return vn.children[0].tag}) - render(root, vnodes) - render(root, updated) + m.render(root, vnodes) + m.render(root, updated) var tagNames = Array.from(root.childNodes, (n) => n.nodeName.toLowerCase()) @@ -840,8 +839,8 @@ o.spec("updateNodes", function() { var updated = vnodify("k4,k1,k2,k9,k0,k3,k6,k5,k8,k7") var expectedTagNames = updated.map(function(vn) {return vn.children[0].tag}) - render(root, vnodes) - render(root, updated) + m.render(root, vnodes) + m.render(root, updated) var tagNames = Array.from(root.childNodes, (n) => n.nodeName.toLowerCase()) @@ -852,8 +851,8 @@ o.spec("updateNodes", function() { var updated = vnodify("b,d,k1,k0,k2,k3,k4,a,c,k5,k6,k7,k8,k9") var expectedTagNames = updated.map(function(vn) {return vn.children[0].tag}) - render(root, vnodes) - render(root, updated) + m.render(root, vnodes) + m.render(root, updated) var tagNames = Array.from(root.childNodes, (n) => n.nodeName.toLowerCase()) @@ -866,9 +865,9 @@ o.spec("updateNodes", function() { var temp = [[null, m(component), m("b")]] var updated = [[m("a"), m(component), m("b")]] - render(root, vnodes) - render(root, temp) - render(root, updated) + m.render(root, vnodes) + m.render(root, temp) + m.render(root, updated) o(Array.from(root.childNodes, (n) => n.nodeName)).deepEquals(["A", "B"]) }) @@ -880,18 +879,18 @@ o.spec("updateNodes", function() { var temp = [[m(a), m(b), m("s")]] var updated = [[m(a), m(b), m("s")]] - render(root, vnodes) + m.render(root, vnodes) flag = false - render(root, temp) + m.render(root, temp) flag = true - render(root, updated) + m.render(root, updated) o(Array.from(root.childNodes, (n) => n.nodeName)).deepEquals(["A", "S"]) }) o("removing a component that returns a fragment doesn't throw (regression test for incidental bug introduced while debugging some Flems)", function() { var component = () => [m("a"), m("b")] - render(root, [m(component)]) - render(root, []) + m.render(root, [m(component)]) + m.render(root, []) o(root.childNodes.length).equals(0) }) diff --git a/tests/render/updateNodesFuzzer.js b/tests/render/updateNodesFuzzer.js index 7bea61463..89014c4e6 100644 --- a/tests/render/updateNodesFuzzer.js +++ b/tests/render/updateNodesFuzzer.js @@ -1,8 +1,7 @@ import o from "ospec" import domMock from "../../test-utils/domMock.js" -import m from "../../src/core/hyperscript.js" -import render from "../../src/core/render.js" +import m from "../../src/entry/mithril.esm.js" o.spec("updateNodes keyed list Fuzzer", () => { const maxLength = 12 @@ -45,8 +44,8 @@ o.spec("updateNodes keyed list Fuzzer", () => { var $window = domMock() var root = $window.document.body - render(root, from.map((x) => m.key(x, view(x)))) - render(root, to.map((x) => m.key(x, view(x)))) + m.render(root, from.map((x) => m.key(x, view(x)))) + m.render(root, to.map((x) => m.key(x, view(x)))) assert(root, to) }) } diff --git a/tests/render/updateText.js b/tests/render/updateText.js index 50841a72e..67e9620a4 100644 --- a/tests/render/updateText.js +++ b/tests/render/updateText.js @@ -1,7 +1,7 @@ import o from "ospec" import domMock from "../../test-utils/domMock.js" -import render from "../../src/core/render.js" +import m from "../../src/entry/mithril.esm.js" o.spec("updateText", function() { var $window, root @@ -14,8 +14,8 @@ o.spec("updateText", function() { var vnode = "a" var updated = "b" - render(root, vnode) - render(root, updated) + m.render(root, vnode) + m.render(root, updated) o(root.firstChild.nodeValue).equals("b") }) @@ -23,8 +23,8 @@ o.spec("updateText", function() { var vnode = "a" var updated = "" - render(root, vnode) - render(root, updated) + m.render(root, vnode) + m.render(root, updated) o(root.firstChild.nodeValue).equals("") }) @@ -32,8 +32,8 @@ o.spec("updateText", function() { var vnode = "" var updated = "b" - render(root, vnode) - render(root, updated) + m.render(root, vnode) + m.render(root, updated) o(root.firstChild.nodeValue).equals("b") }) @@ -41,8 +41,8 @@ o.spec("updateText", function() { var vnode = "a" var updated = 1 - render(root, vnode) - render(root, updated) + m.render(root, vnode) + m.render(root, updated) o(root.firstChild.nodeValue).equals("1") }) @@ -50,8 +50,8 @@ o.spec("updateText", function() { var vnode = "a" var updated = 0 - render(root, vnode) - render(root, updated) + m.render(root, vnode) + m.render(root, updated) o(root.firstChild.nodeValue).equals("0") }) @@ -59,8 +59,8 @@ o.spec("updateText", function() { var vnode = 0 var updated = "b" - render(root, vnode) - render(root, updated) + m.render(root, vnode) + m.render(root, updated) o(root.firstChild.nodeValue).equals("b") }) @@ -68,8 +68,8 @@ o.spec("updateText", function() { var vnode = "a" var updated = true - render(root, vnode) - render(root, updated) + m.render(root, vnode) + m.render(root, updated) o(root.childNodes.length).equals(0) }) @@ -77,8 +77,8 @@ o.spec("updateText", function() { var vnode = "a" var updated = false - render(root, vnode) - render(root, updated) + m.render(root, vnode) + m.render(root, updated) o(root.childNodes.length).equals(0) }) @@ -86,8 +86,8 @@ o.spec("updateText", function() { var vnode = false var updated = "b" - render(root, vnode) - render(root, updated) + m.render(root, vnode) + m.render(root, updated) o(root.firstChild.nodeValue).equals("b") }) diff --git a/tests/test-utils/browserMock.js b/tests/test-utils/browserMock.js index d53f4819b..79d32c1c8 100644 --- a/tests/test-utils/browserMock.js +++ b/tests/test-utils/browserMock.js @@ -1,7 +1,7 @@ import o from "ospec" import browserMock from "../../test-utils/browserMock.js" -import callAsync from "../../test-utils/callAsync.js" +import {callAsync} from "../../test-utils/callAsync.js" o.spec("browserMock", function() { var $window diff --git a/tests/test-utils/callAsync.js b/tests/test-utils/callAsync.js index 5b34d42a4..ede0c72cd 100644 --- a/tests/test-utils/callAsync.js +++ b/tests/test-utils/callAsync.js @@ -1,16 +1,8 @@ import o from "ospec" -import callAsync from "../../test-utils/callAsync.js" +import {callAsync, clearPending, waitAsync} from "../../test-utils/callAsync.js" o.spec("callAsync", function() { - o("works", function(done) { - var count = 0 - callAsync(function() { - o(count).equals(1) - done() - }) - count++ - }) o("gets called before setTimeout", function(done) { var timeout callAsync(function() { @@ -21,4 +13,31 @@ o.spec("callAsync", function() { throw new Error("callAsync was called too slow") }, 5) }) + o("gets cleared", function(done) { + callAsync(function() { + clearTimeout(timeout) + done(new Error("should never have been called")) + }) + const timeout = setTimeout(done, 5) + clearPending() + }) +}) + +o.spec("waitAsync", function() { + function sleep(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)) + } + + o("gets called before setTimeout", () => Promise.race([ + waitAsync(), + sleep(5).then(() => { throw new Error("callAsync was called too slow") }) + ])) + o("gets cleared", () => { + const promise = waitAsync() + clearPending() + return Promise.race([ + promise.then(() => { throw new Error("should never have been called") }), + sleep(5), + ]) + }) }) diff --git a/tests/test-utils/pushStateMock.js b/tests/test-utils/pushStateMock.js index c9d391c65..60da3c167 100644 --- a/tests/test-utils/pushStateMock.js +++ b/tests/test-utils/pushStateMock.js @@ -1,6 +1,6 @@ import o from "ospec" -import callAsync from "../../test-utils/callAsync.js" +import {callAsync, waitAsync} from "../../test-utils/callAsync.js" import pushStateMock from "../../test-utils/pushStateMock.js" o.spec("pushStateMock", function() { @@ -565,18 +565,14 @@ o.spec("pushStateMock", function() { }) }) }) - o("onhashchange triggers once when the hash changes twice in a single tick", function(done) { + o("onhashchange triggers once when the hash changes twice in a single tick", async () => { $window.location.href = "#a" - callAsync(function(){ - $window.onhashchange = o.spy() - $window.history.back() - $window.history.forward() - - callAsync(function(){ - o($window.onhashchange.callCount).equals(1) - done() - }) - }) + await waitAsync() + $window.onhashchange = o.spy() + $window.history.back() + $window.history.forward() + await waitAsync() + o($window.onhashchange.callCount).equals(1) }) o("onhashchange does not trigger on history.back() that causes page change with different hash", function(done) { $window.location.href = "#a" diff --git a/tests/util/init.js b/tests/util/init.js index bd6cbc477..8220ff6ad 100644 --- a/tests/util/init.js +++ b/tests/util/init.js @@ -2,31 +2,29 @@ import o from "ospec" import domMock from "../../test-utils/domMock.js" import init from "../../src/std/init.js" -import render from "../../src/core/render.js" +import m from "../../src/entry/mithril.esm.js" o.spec("m.init", () => { - o("works", () => { + o("works", async () => { var onabort = o.spy() var initializer = o.spy((signal) => { signal.onabort = onabort }) var $window = domMock() - render($window.document.body, init(initializer)) + m.render($window.document.body, init(initializer)) o(initializer.callCount).equals(0) o(onabort.callCount).equals(0) - return Promise.resolve() - .then(() => { - o(initializer.callCount).equals(1) - o(onabort.callCount).equals(0) - render($window.document.body, init(initializer)) - }) - .then(() => { - o(initializer.callCount).equals(1) - o(onabort.callCount).equals(0) - render($window.document.body, null) + await Promise.resolve() + o(initializer.callCount).equals(1) + o(onabort.callCount).equals(0) + m.render($window.document.body, init(initializer)) + + await Promise.resolve() + o(initializer.callCount).equals(1) + o(onabort.callCount).equals(0) + m.render($window.document.body, null) - o(initializer.callCount).equals(1) - o(onabort.callCount).equals(1) - }) + o(initializer.callCount).equals(1) + o(onabort.callCount).equals(1) }) }) diff --git a/tests/util/lazy.js b/tests/util/lazy.js index 9a22846e4..059b6a620 100644 --- a/tests/util/lazy.js +++ b/tests/util/lazy.js @@ -1,9 +1,8 @@ import o from "ospec" import domMock from "../../test-utils/domMock.js" -import hyperscript from "../../src/core/hyperscript.js" +import m from "../../src/entry/mithril.esm.js" import makeLazy from "../../src/std/lazy.js" -import render from "../../src/core/render.js" o.spec("lazy", () => { var consoleError = console.error @@ -22,12 +21,12 @@ o.spec("lazy", () => { void [{name: "direct", wrap: (v) => v}, {name: "in module with default", wrap: (v) => ({default:v})}].forEach(({name, wrap}) => { o.spec(name, () => { - o("works with only fetch and success", () => { + o("works with only fetch and success", async () => { var calls = [] var scheduled = 1 var component = wrap(({name}) => { calls.push(`view ${name}`) - return hyperscript("div", {id: "a"}, "b") + return m("div", {id: "a"}, "b") }) var send, notifyRedrawn var fetchRedrawn = new Promise((resolve) => notifyRedrawn = resolve) @@ -43,9 +42,9 @@ o.spec("lazy", () => { o(calls).deepEquals([]) - render(root, [ - hyperscript(C, {name: "one"}), - hyperscript(C, {name: "two"}), + m.render(root, [ + m(C, {name: "one"}), + m(C, {name: "two"}), ]) o(calls).deepEquals([ @@ -54,41 +53,41 @@ o.spec("lazy", () => { send(component) - return fetchRedrawn.then(() => { - o(calls).deepEquals([ - "fetch", - "scheduled 1", - ]) - - render(root, [ - hyperscript(C, {name: "one"}), - hyperscript(C, {name: "two"}), - ]) - - o(calls).deepEquals([ - "fetch", - "scheduled 1", - "view one", - "view two", - ]) - - render(root, [ - hyperscript(C, {name: "one"}), - hyperscript(C, {name: "two"}), - ]) - - o(calls).deepEquals([ - "fetch", - "scheduled 1", - "view one", - "view two", - "view one", - "view two", - ]) - }) + await fetchRedrawn + + o(calls).deepEquals([ + "fetch", + "scheduled 1", + ]) + + m.render(root, [ + m(C, {name: "one"}), + m(C, {name: "two"}), + ]) + + o(calls).deepEquals([ + "fetch", + "scheduled 1", + "view one", + "view two", + ]) + + m.render(root, [ + m(C, {name: "one"}), + m(C, {name: "two"}), + ]) + + o(calls).deepEquals([ + "fetch", + "scheduled 1", + "view one", + "view two", + "view one", + "view two", + ]) }) - o("works with only fetch and failure", () => { + o("works with only fetch and failure", async () => { var error = new Error("test") var calls = [] console.error = (e) => { @@ -109,9 +108,9 @@ o.spec("lazy", () => { o(calls).deepEquals([]) - render(root, [ - hyperscript(C, {name: "one"}), - hyperscript(C, {name: "two"}), + m.render(root, [ + m(C, {name: "one"}), + m(C, {name: "two"}), ]) o(calls).deepEquals([ @@ -120,43 +119,43 @@ o.spec("lazy", () => { send(error) - return fetchRedrawn.then(() => { - o(calls).deepEquals([ - "fetch", - "console.error", "test", - "scheduled 1", - ]) - - render(root, [ - hyperscript(C, {name: "one"}), - hyperscript(C, {name: "two"}), - ]) - - o(calls).deepEquals([ - "fetch", - "console.error", "test", - "scheduled 1", - ]) - - render(root, [ - hyperscript(C, {name: "one"}), - hyperscript(C, {name: "two"}), - ]) - - o(calls).deepEquals([ - "fetch", - "console.error", "test", - "scheduled 1", - ]) - }) + await fetchRedrawn + + o(calls).deepEquals([ + "fetch", + "console.error", "test", + "scheduled 1", + ]) + + m.render(root, [ + m(C, {name: "one"}), + m(C, {name: "two"}), + ]) + + o(calls).deepEquals([ + "fetch", + "console.error", "test", + "scheduled 1", + ]) + + m.render(root, [ + m(C, {name: "one"}), + m(C, {name: "two"}), + ]) + + o(calls).deepEquals([ + "fetch", + "console.error", "test", + "scheduled 1", + ]) }) - o("works with fetch + pending and success", () => { + o("works with fetch + pending and success", async () => { var calls = [] var scheduled = 1 var component = wrap(({name}) => { calls.push(`view ${name}`) - return hyperscript("div", {id: "a"}, "b") + return m("div", {id: "a"}, "b") }) var send, notifyRedrawn var fetchRedrawn = new Promise((resolve) => notifyRedrawn = resolve) @@ -175,9 +174,9 @@ o.spec("lazy", () => { o(calls).deepEquals([]) - render(root, [ - hyperscript(C, {name: "one"}), - hyperscript(C, {name: "two"}), + m.render(root, [ + m(C, {name: "one"}), + m(C, {name: "two"}), ]) o(calls).deepEquals([ @@ -188,47 +187,47 @@ o.spec("lazy", () => { send(component) - return fetchRedrawn.then(() => { - o(calls).deepEquals([ - "fetch", - "pending", - "pending", - "scheduled 1", - ]) - - render(root, [ - hyperscript(C, {name: "one"}), - hyperscript(C, {name: "two"}), - ]) - - o(calls).deepEquals([ - "fetch", - "pending", - "pending", - "scheduled 1", - "view one", - "view two", - ]) - - render(root, [ - hyperscript(C, {name: "one"}), - hyperscript(C, {name: "two"}), - ]) - - o(calls).deepEquals([ - "fetch", - "pending", - "pending", - "scheduled 1", - "view one", - "view two", - "view one", - "view two", - ]) - }) + await fetchRedrawn + + o(calls).deepEquals([ + "fetch", + "pending", + "pending", + "scheduled 1", + ]) + + m.render(root, [ + m(C, {name: "one"}), + m(C, {name: "two"}), + ]) + + o(calls).deepEquals([ + "fetch", + "pending", + "pending", + "scheduled 1", + "view one", + "view two", + ]) + + m.render(root, [ + m(C, {name: "one"}), + m(C, {name: "two"}), + ]) + + o(calls).deepEquals([ + "fetch", + "pending", + "pending", + "scheduled 1", + "view one", + "view two", + "view one", + "view two", + ]) }) - o("works with fetch + pending and failure", () => { + o("works with fetch + pending and failure", async () => { var error = new Error("test") var calls = [] console.error = (e) => { @@ -252,9 +251,9 @@ o.spec("lazy", () => { o(calls).deepEquals([]) - render(root, [ - hyperscript(C, {name: "one"}), - hyperscript(C, {name: "two"}), + m.render(root, [ + m(C, {name: "one"}), + m(C, {name: "two"}), ]) o(calls).deepEquals([ @@ -265,49 +264,49 @@ o.spec("lazy", () => { send(error) - return fetchRedrawn.then(() => { - o(calls).deepEquals([ - "fetch", - "pending", - "pending", - "console.error", "test", - "scheduled 1", - ]) - - render(root, [ - hyperscript(C, {name: "one"}), - hyperscript(C, {name: "two"}), - ]) - - o(calls).deepEquals([ - "fetch", - "pending", - "pending", - "console.error", "test", - "scheduled 1", - ]) - - render(root, [ - hyperscript(C, {name: "one"}), - hyperscript(C, {name: "two"}), - ]) - - o(calls).deepEquals([ - "fetch", - "pending", - "pending", - "console.error", "test", - "scheduled 1", - ]) - }) + await fetchRedrawn + + o(calls).deepEquals([ + "fetch", + "pending", + "pending", + "console.error", "test", + "scheduled 1", + ]) + + m.render(root, [ + m(C, {name: "one"}), + m(C, {name: "two"}), + ]) + + o(calls).deepEquals([ + "fetch", + "pending", + "pending", + "console.error", "test", + "scheduled 1", + ]) + + m.render(root, [ + m(C, {name: "one"}), + m(C, {name: "two"}), + ]) + + o(calls).deepEquals([ + "fetch", + "pending", + "pending", + "console.error", "test", + "scheduled 1", + ]) }) - o("works with fetch + error and success", () => { + o("works with fetch + error and success", async () => { var calls = [] var scheduled = 1 var component = wrap(({name}) => { calls.push(`view ${name}`) - return hyperscript("div", {id: "a"}, "b") + return m("div", {id: "a"}, "b") }) var send, notifyRedrawn var fetchRedrawn = new Promise((resolve) => notifyRedrawn = resolve) @@ -326,9 +325,9 @@ o.spec("lazy", () => { o(calls).deepEquals([]) - render(root, [ - hyperscript(C, {name: "one"}), - hyperscript(C, {name: "two"}), + m.render(root, [ + m(C, {name: "one"}), + m(C, {name: "two"}), ]) o(calls).deepEquals([ @@ -337,41 +336,41 @@ o.spec("lazy", () => { send(component) - return fetchRedrawn.then(() => { - o(calls).deepEquals([ - "fetch", - "scheduled 1", - ]) - - render(root, [ - hyperscript(C, {name: "one"}), - hyperscript(C, {name: "two"}), - ]) - - o(calls).deepEquals([ - "fetch", - "scheduled 1", - "view one", - "view two", - ]) - - render(root, [ - hyperscript(C, {name: "one"}), - hyperscript(C, {name: "two"}), - ]) - - o(calls).deepEquals([ - "fetch", - "scheduled 1", - "view one", - "view two", - "view one", - "view two", - ]) - }) + await fetchRedrawn + + o(calls).deepEquals([ + "fetch", + "scheduled 1", + ]) + + m.render(root, [ + m(C, {name: "one"}), + m(C, {name: "two"}), + ]) + + o(calls).deepEquals([ + "fetch", + "scheduled 1", + "view one", + "view two", + ]) + + m.render(root, [ + m(C, {name: "one"}), + m(C, {name: "two"}), + ]) + + o(calls).deepEquals([ + "fetch", + "scheduled 1", + "view one", + "view two", + "view one", + "view two", + ]) }) - o("works with fetch + error and failure", () => { + o("works with fetch + error and failure", async () => { var error = new Error("test") var calls = [] console.error = (e) => { @@ -395,9 +394,9 @@ o.spec("lazy", () => { o(calls).deepEquals([]) - render(root, [ - hyperscript(C, {name: "one"}), - hyperscript(C, {name: "two"}), + m.render(root, [ + m(C, {name: "one"}), + m(C, {name: "two"}), ]) o(calls).deepEquals([ @@ -406,49 +405,49 @@ o.spec("lazy", () => { send(error) - return fetchRedrawn.then(() => { - o(calls).deepEquals([ - "fetch", - "console.error", "test", - "scheduled 1", - ]) - - render(root, [ - hyperscript(C, {name: "one"}), - hyperscript(C, {name: "two"}), - ]) - - o(calls).deepEquals([ - "fetch", - "console.error", "test", - "scheduled 1", - "error", "test", - "error", "test", - ]) - - render(root, [ - hyperscript(C, {name: "one"}), - hyperscript(C, {name: "two"}), - ]) - - o(calls).deepEquals([ - "fetch", - "console.error", "test", - "scheduled 1", - "error", "test", - "error", "test", - "error", "test", - "error", "test", - ]) - }) + await fetchRedrawn + + o(calls).deepEquals([ + "fetch", + "console.error", "test", + "scheduled 1", + ]) + + m.render(root, [ + m(C, {name: "one"}), + m(C, {name: "two"}), + ]) + + o(calls).deepEquals([ + "fetch", + "console.error", "test", + "scheduled 1", + "error", "test", + "error", "test", + ]) + + m.render(root, [ + m(C, {name: "one"}), + m(C, {name: "two"}), + ]) + + o(calls).deepEquals([ + "fetch", + "console.error", "test", + "scheduled 1", + "error", "test", + "error", "test", + "error", "test", + "error", "test", + ]) }) - o("works with all hooks and success", () => { + o("works with all hooks and success", async() => { var calls = [] var scheduled = 1 var component = wrap(({name}) => { calls.push(`view ${name}`) - return hyperscript("div", {id: "a"}, "b") + return m("div", {id: "a"}, "b") }) var send, notifyRedrawn var fetchRedrawn = new Promise((resolve) => notifyRedrawn = resolve) @@ -470,9 +469,9 @@ o.spec("lazy", () => { o(calls).deepEquals([]) - render(root, [ - hyperscript(C, {name: "one"}), - hyperscript(C, {name: "two"}), + m.render(root, [ + m(C, {name: "one"}), + m(C, {name: "two"}), ]) o(calls).deepEquals([ @@ -483,47 +482,47 @@ o.spec("lazy", () => { send(component) - return fetchRedrawn.then(() => { - o(calls).deepEquals([ - "fetch", - "pending", - "pending", - "scheduled 1", - ]) - - render(root, [ - hyperscript(C, {name: "one"}), - hyperscript(C, {name: "two"}), - ]) - - o(calls).deepEquals([ - "fetch", - "pending", - "pending", - "scheduled 1", - "view one", - "view two", - ]) - - render(root, [ - hyperscript(C, {name: "one"}), - hyperscript(C, {name: "two"}), - ]) - - o(calls).deepEquals([ - "fetch", - "pending", - "pending", - "scheduled 1", - "view one", - "view two", - "view one", - "view two", - ]) - }) + await fetchRedrawn + + o(calls).deepEquals([ + "fetch", + "pending", + "pending", + "scheduled 1", + ]) + + m.render(root, [ + m(C, {name: "one"}), + m(C, {name: "two"}), + ]) + + o(calls).deepEquals([ + "fetch", + "pending", + "pending", + "scheduled 1", + "view one", + "view two", + ]) + + m.render(root, [ + m(C, {name: "one"}), + m(C, {name: "two"}), + ]) + + o(calls).deepEquals([ + "fetch", + "pending", + "pending", + "scheduled 1", + "view one", + "view two", + "view one", + "view two", + ]) }) - o("works with all hooks and failure", () => { + o("works with all hooks and failure", async () => { var error = new Error("test") var calls = [] console.error = (e) => { @@ -550,9 +549,9 @@ o.spec("lazy", () => { o(calls).deepEquals([]) - render(root, [ - hyperscript(C, {name: "one"}), - hyperscript(C, {name: "two"}), + m.render(root, [ + m(C, {name: "one"}), + m(C, {name: "two"}), ]) o(calls).deepEquals([ @@ -563,47 +562,47 @@ o.spec("lazy", () => { send(error) - return fetchRedrawn.then(() => { - o(calls).deepEquals([ - "fetch", - "pending", - "pending", - "console.error", "test", - "scheduled 1", - ]) - - render(root, [ - hyperscript(C, {name: "one"}), - hyperscript(C, {name: "two"}), - ]) - - o(calls).deepEquals([ - "fetch", - "pending", - "pending", - "console.error", "test", - "scheduled 1", - "error", "test", - "error", "test", - ]) - - render(root, [ - hyperscript(C, {name: "one"}), - hyperscript(C, {name: "two"}), - ]) - - o(calls).deepEquals([ - "fetch", - "pending", - "pending", - "console.error", "test", - "scheduled 1", - "error", "test", - "error", "test", - "error", "test", - "error", "test", - ]) - }) + await fetchRedrawn + + o(calls).deepEquals([ + "fetch", + "pending", + "pending", + "console.error", "test", + "scheduled 1", + ]) + + m.render(root, [ + m(C, {name: "one"}), + m(C, {name: "two"}), + ]) + + o(calls).deepEquals([ + "fetch", + "pending", + "pending", + "console.error", "test", + "scheduled 1", + "error", "test", + "error", "test", + ]) + + m.render(root, [ + m(C, {name: "one"}), + m(C, {name: "two"}), + ]) + + o(calls).deepEquals([ + "fetch", + "pending", + "pending", + "console.error", "test", + "scheduled 1", + "error", "test", + "error", "test", + "error", "test", + "error", "test", + ]) }) }) }) diff --git a/tests/util/use.js b/tests/util/use.js index ff3a68c04..669abdbd5 100644 --- a/tests/util/use.js +++ b/tests/util/use.js @@ -1,8 +1,7 @@ import o from "ospec" import domMock from "../../test-utils/domMock.js" -import m from "../../src/core/hyperscript.js" -import render from "../../src/core/render.js" +import m from "../../src/entry/mithril.esm.js" import use from "../../src/std/use.js" o.spec("m.use", () => { @@ -11,15 +10,15 @@ o.spec("m.use", () => { var initializer = o.spy((_, signal) => { signal.onabort = onabort }) var $window = domMock() - render($window.document.body, use([], m.layout(initializer))) + m.render($window.document.body, use([], m.layout(initializer))) o(initializer.callCount).equals(1) o(onabort.callCount).equals(0) - render($window.document.body, use([], m.layout(initializer))) + m.render($window.document.body, use([], m.layout(initializer))) o(initializer.callCount).equals(2) o(onabort.callCount).equals(0) - render($window.document.body, null) + m.render($window.document.body, null) o(initializer.callCount).equals(2) o(onabort.callCount).equals(1) }) @@ -29,15 +28,15 @@ o.spec("m.use", () => { var initializer = o.spy((_, signal) => { signal.onabort = onabort }) var $window = domMock() - render($window.document.body, use([1], m.layout(initializer))) + m.render($window.document.body, use([1], m.layout(initializer))) o(initializer.callCount).equals(1) o(onabort.callCount).equals(0) - render($window.document.body, use([1], m.layout(initializer))) + m.render($window.document.body, use([1], m.layout(initializer))) o(initializer.callCount).equals(2) o(onabort.callCount).equals(0) - render($window.document.body, null) + m.render($window.document.body, null) o(initializer.callCount).equals(2) o(onabort.callCount).equals(1) }) @@ -47,15 +46,15 @@ o.spec("m.use", () => { var initializer = o.spy((_, signal) => { signal.onabort = onabort }) var $window = domMock() - render($window.document.body, use([1], m.layout(initializer))) + m.render($window.document.body, use([1], m.layout(initializer))) o(initializer.callCount).equals(1) o(onabort.callCount).equals(0) - render($window.document.body, use([2], m.layout(initializer))) + m.render($window.document.body, use([2], m.layout(initializer))) o(initializer.callCount).equals(2) o(onabort.callCount).equals(1) - render($window.document.body, null) + m.render($window.document.body, null) o(initializer.callCount).equals(2) o(onabort.callCount).equals(2) }) From c0821cbd37d51a57874a456c9e9cf92c3326b7a9 Mon Sep 17 00:00:00 2001 From: Claudia Meadows Date: Sat, 5 Oct 2024 22:15:28 -0700 Subject: [PATCH 47/95] Try to get a better profile view Didn't fully think through that experiment of file loading - it doesn't also load Mithril, among other things. Also, remove some redundant conditionals in `src/core.js`. --- performance/components/common.js | 5 ----- performance/components/nested-tree.js | 6 +++++- performance/components/simple-tree.js | 6 +++++- performance/test-perf-impl.js | 29 +++++---------------------- src/core.js | 7 +++---- 5 files changed, 18 insertions(+), 35 deletions(-) delete mode 100644 performance/components/common.js diff --git a/performance/components/common.js b/performance/components/common.js deleted file mode 100644 index da611bb36..000000000 --- a/performance/components/common.js +++ /dev/null @@ -1,5 +0,0 @@ -export const fields = [] - -for (let i=100; i--;) { - fields.push((i * 999).toString(36)) -} diff --git a/performance/components/nested-tree.js b/performance/components/nested-tree.js index 7b618fe90..4472e562a 100644 --- a/performance/components/nested-tree.js +++ b/performance/components/nested-tree.js @@ -1,6 +1,10 @@ import m from "../../src/entry/mithril.esm.js" -import {fields} from "./common.js" +const fields = [] + +for (let i=100; i--;) { + fields.push((i * 999).toString(36)) +} var NestedHeader = () => m("header", m("h1.asdf", "a ", "b", " c ", 0, " d"), diff --git a/performance/components/simple-tree.js b/performance/components/simple-tree.js index ef311cdc5..fa2405ca2 100644 --- a/performance/components/simple-tree.js +++ b/performance/components/simple-tree.js @@ -1,6 +1,10 @@ import m from "../../src/entry/mithril.esm.js" -import {fields} from "./common.js" +const fields = [] + +for (let i=100; i--;) { + fields.push((i * 999).toString(36)) +} export const simpleTree = () => m(".foo.bar[data-foo=bar]", {p: 2}, m("header", diff --git a/performance/test-perf-impl.js b/performance/test-perf-impl.js index ac0ad7127..74785ebbb 100644 --- a/performance/test-perf-impl.js +++ b/performance/test-perf-impl.js @@ -38,7 +38,7 @@ import {nestedTree} from "./components/nested-tree.js" import {simpleTree} from "./components/simple-tree.js" // eslint-disable-next-line no-undef -var globalObject = typeof globalThis !== "undefined" ? globalThis : isDOM ? window : global +var globalObject = typeof globalThis !== "undefined" ? globalThis : isBrowser ? window : global globalObject.rootElem = null @@ -83,23 +83,6 @@ var xsuite = {add: function(name) { console.log("skipping " + name) }} globalObject.simpleTree = simpleTree globalObject.nestedTree = nestedTree -var mountVersion = 0 -var messageSet = new Set() - -function doFetch(deferred, path) { - import(`${path}?v=${mountVersion++}`).then( - () => deferred.resolve(), - (e) => { - const key = `${path}:${e.message}` - if (!messageSet.has(key)) { - messageSet.add(key) - console.error(e) - } - deferred.resolve() - } - ) -} - suite.add("construct simple tree", { fn: function () { simpleTree() @@ -107,9 +90,8 @@ suite.add("construct simple tree", { }) suite.add("mount simple tree", { - defer: true, - fn: function (deferred) { - doFetch(deferred, "./components/mount-simple-tree.js") + fn: function () { + m.mount(rootElem, simpleTree) } }) @@ -123,9 +105,8 @@ suite.add("redraw simple tree", { }) suite.add("mount large nested tree", { - defer: true, - fn: function (deferred) { - doFetch(deferred, "./components/mount-nested-tree.js") + fn: function () { + m.mount(rootElem, nestedTree) } }) diff --git a/src/core.js b/src/core.js index 27c59418e..858c19c5a 100644 --- a/src/core.js +++ b/src/core.js @@ -300,8 +300,7 @@ function createComponent(vnode) { //update function updateNodes(old, vnodes) { - if (old === vnodes || old == null && vnodes == null) return - else if (old == null || old.length === 0) createNodes(vnodes, 0) + if (old == null || old.length === 0) createNodes(vnodes, 0) else if (vnodes == null || vnodes.length === 0) removeNodes(old, 0) else { var isOldKeyed = old[0] != null && old[0].tag === KEY @@ -753,13 +752,13 @@ m.redraw = () => { m.redrawSync = () => { unscheduleFrame() - subscriptions.forEach((view, root) => { + for (const [root, view] of subscriptions) { try { m.render(root, view(), m.redraw) } catch (e) { console.error(e) } - }) + } } m.mount = (root, view) => { From 210d997dc05d7e5575d36b04e70102e18b936690 Mon Sep 17 00:00:00 2001 From: Claudia Meadows Date: Thu, 10 Oct 2024 17:13:46 -0700 Subject: [PATCH 48/95] Improve bundle size, move to type masks to boost performance - `function foo(...) {...}` to `var foo = (...) => {...}` saved a fair bit of space. I also replaced other ES5 `function` functions with arrow functions where possible. - I inlined several functions. Turns out Terser wasn't inlining them properly (they were getting nonsensically inlined as IIFEs), and it was causing performance to suffer. - I dropped the `contenteditable` code, since that was only needed to handle the since-dropped `m.trust`. - I switched from tag names to type masks. This enabled several optimizations. - Size: I switched from a `switch` to function tables for create and update. Now, it just hinges on how well the CPU predicts it, and modern desktop CPUs very much can chase such multi-level indirect branches. - Size: I could blend single-key diffs with element and component tag name diffs, for better performance. - Performance: By using an expando bit, I've avoided needing to read the current namespace in some pretty hot loops, including in `setAttr`. - Performance: In `setAttr` and other places, I've merged as many as 4 tag comparisons to a single bit comparison. - I truncated the vnode properties to single letters. This resulted in a moderate savings. - I moved `redraw` to a return value of `m.mount` and also passed it via a context value to components so they can deal with non-global redraws. - I re-split `m.layout`. I also swapped out the benchmark library for something I rolled myself, and used that to set up the benchmarks. Benchmark.js isn't maintained anymore, its built-in output wasn't all that great, and it was forcing me to use globals where I otherwise didn't really need to, so I decided to roll some statistics stuff myself and make something simple. And while I was at it, I simplified the benchmark code by a lot. I also ripped out the select/option Chrome bug workaround - it doesn't seem to replicate anymore. Removing that brought a slight boost to attribute setting performance and enabled me to factor that code out to something a lot simpler. Try this page for example: ```html ``` --- .eslintrc.json | 35 +- package-lock.json | 2023 +---------------- package.json | 16 +- performance/bench.js | 187 ++ performance/components/mount-nested-tree.js | 5 - performance/components/mount-simple-tree.js | 5 - .../mutate-styles-properties-tree.js | 81 + performance/components/nested-tree.js | 2 +- performance/components/repeated-tree.js | 54 + performance/components/shuffled-keyed-tree.js | 25 + performance/components/simple-tree.js | 4 +- performance/index.html | 3 - performance/inject-mock-globals.js | 8 - performance/is-browser.js | 1 - performance/test-perf-impl.js | 279 --- performance/test-perf.js | 205 +- scripts/build.js | 7 +- scripts/server.js | 77 + src/core.js | 1291 ++++++----- src/std/init.js | 7 +- src/std/lazy.js | 12 +- src/std/router.js | 23 +- src/std/tracked.js | 12 +- src/std/with-progress.js | 5 +- src/util.js | 1 - test-utils/browserMock.js | 12 +- test-utils/callAsync.js | 2 +- test-utils/domMock.js | 1426 ++++++------ test-utils/global.js | 120 +- test-utils/injectBrowserMock.js | 3 - test-utils/pushStateMock.js | 9 +- test-utils/redraw-registry.js | 34 - test-utils/throttleMock.js | 8 +- test-utils/xhrMock.js | 138 -- tests/api/mountRedraw.js | 517 ++--- tests/api/router.js | 291 ++- tests/exported-api.js | 90 +- tests/render/attributes.js | 432 ++-- tests/render/component.js | 466 ++-- tests/render/createElement.js | 101 +- tests/render/createFragment.js | 41 +- tests/render/createNodes.js | 39 +- tests/render/createText.js | 53 +- tests/render/event.js | 176 +- tests/render/fragment.js | 175 +- tests/render/hyperscript.js | 484 ++-- tests/render/input.js | 157 +- tests/render/normalize.js | 29 +- tests/render/normalizeChildren.js | 25 +- tests/render/normalizeComponentChildren.js | 31 +- tests/render/oncreate.js | 76 +- tests/render/onremove.js | 41 +- tests/render/onupdate.js | 108 +- .../render/render-hyperscript-integration.js | 566 +++-- tests/render/render.js | 244 +- tests/render/retain.js | 51 +- tests/render/textContent.js | 183 +- tests/render/updateElement.js | 203 +- tests/render/updateFragment.js | 49 +- tests/render/updateNodes.js | 815 +++---- tests/render/updateNodesFuzzer.js | 14 +- tests/render/updateText.js | 63 +- tests/test-utils/browserMock.js | 40 +- tests/test-utils/callAsync.js | 2 + tests/test-utils/domMock.js | 757 +++--- tests/test-utils/pushStateMock.js | 659 +++--- tests/test-utils/xhrMock.js | 99 - tests/util/init.js | 211 +- tests/util/lazy.js | 186 +- tests/util/tracked.js | 18 +- tests/util/use.js | 51 +- 71 files changed, 5973 insertions(+), 7690 deletions(-) create mode 100644 performance/bench.js delete mode 100644 performance/components/mount-nested-tree.js delete mode 100644 performance/components/mount-simple-tree.js create mode 100644 performance/components/mutate-styles-properties-tree.js create mode 100644 performance/components/repeated-tree.js create mode 100644 performance/components/shuffled-keyed-tree.js delete mode 100644 performance/inject-mock-globals.js delete mode 100644 performance/is-browser.js delete mode 100644 performance/test-perf-impl.js create mode 100644 scripts/server.js delete mode 100644 test-utils/redraw-registry.js delete mode 100644 test-utils/xhrMock.js delete mode 100644 tests/test-utils/xhrMock.js diff --git a/.eslintrc.json b/.eslintrc.json index 0c66f9eba..ec3c29a53 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -16,7 +16,7 @@ } }, { - "files": "tests/**", + "files": ["tests/**", "test-utils/**"], "env": { "node": true }, @@ -24,7 +24,32 @@ "ecmaVersion": 2020 }, "rules": { - "no-process-env": "off" + "no-process-env": "off", + "no-restricted-syntax": ["error", + {"selector": "Literal[bigint]", "message": "BigInts are not supported in ES2018"}, + {"selector": "ChainExpression", "message": "Optional chaining is not supported in ES2018"}, + {"selector": "BinaryExpression[operator='??']", "message": "Nullish coalescing is not supported in ES2018"}, + {"selector": "MetaProperty[meta.name='import'][property.name='meta']", "message": "`import.meta` is not supported in ES2018"}, + {"selector": "ExportAllDeclaration[exported!=null]", "message": "`export * as foo from ...` is not supported in ES2018"}, + {"selector": "CatchClause[param=null]", "message": "Omitted `catch` bindings are not supported in ES2018"}, + {"selector": "ForOfStatement[await=true]", "message": "Async/await is not supported in ES2018"}, + {"selector": "ObjectExpression > SpreadElement", "message": "Object rest/spread is not supported in ES2018"}, + {"selector": "ObjectPattern > SpreadElement", "message": "Object rest/spread is not supported in ES2018"}, + {"selector": "Function[async=true][generator=true]", "message": "Async generators are not supported in ES2018"}, + {"selector": "Literal[regex.flags=/s/]", "message": "`/.../s` is not supported in ES2018"}, + {"selector": "Literal[regex.pattern=/\\(<=|\\(|\\\\k<[\\w$]+>/]", "message": "Named capture groups are not supported in ES2018"}, + {"selector": "Literal[regex.flags=/u/][regex.pattern=/\\\\p/i]", "message": "`\\p{...}` in regexps are not supported in ES2018"}, + {"selector": "Literal[regex.flags=/v/]", "message": "`/.../v` is not supported in ES2018"}, + { + "selector": "TaggedTemplateExpression TemplateElement[value.raw=/\\\\(?![0'\"\\\\nrvtbf\\n\\r\\u2028\\u2029]|x[0-9a-fA-F]{2}|u[0-9a-fA-F]{4}|u\\{([0-9a-fA-F]{1,5}|10[0-9a-fA-F]{0,4})\\})/]", + "message": "Tagged template strings in ES2018 have the same lexical grammar as non-tagged template strings" + }, + + {"selector": "MemberExpression[property.name='matchAll']", "message": "`string.matchAll` is not supported in ES2018"}, + {"selector": "MemberExpression[property.name='trimStart']", "message": "`string.trimStart` is not supported in ES2018"}, + {"selector": "MemberExpression[property.name='finally']", "message": "`promise.finally` is not supported in ES2018"} + ] } } ], @@ -37,8 +62,6 @@ "URL": true, "URLSearchParams": true, "AbortController": true, - "setTimeout": true, - "clearTimeout": true, "console": true }, "parserOptions": { @@ -70,7 +93,9 @@ {"selector": "MemberExpression[property.name='matchAll']", "message": "`string.matchAll` is not supported in ES2018"}, {"selector": "MemberExpression[property.name='trimStart']", "message": "`string.trimStart` is not supported in ES2018"}, - {"selector": "MemberExpression[property.name='finally']", "message": "`promise.finally` is not supported in ES2018"} + {"selector": "MemberExpression[property.name='finally']", "message": "`promise.finally` is not supported in ES2018"}, + + {"selector": "VariableDeclaration[kind!='var']", "message": "Keep to `var` in `src/` to ensure the module compresses better"} ], "no-restricted-properties": ["error", {"object": "Promise", "property": "allSettled", "message": "`Promise.allSettled` is not supported in ES2018"}, diff --git a/package-lock.json b/package-lock.json index 21d76c923..61ef78d4c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,13 +12,9 @@ "@rollup/plugin-commonjs": "^28.0.0", "@rollup/plugin-node-resolve": "^15.3.0", "@rollup/plugin-terser": "^0.4.4", - "benchmark": "^2.1.4", "chokidar": "^4.0.1", "eslint": "^8.9.0", - "glob": "^11.0.0", - "npm-run-all": "^4.1.5", "ospec": "4.2.1", - "rimraf": "^6.0.1", "rollup": "^4.24.0", "terser": "^4.3.4" } @@ -132,53 +128,6 @@ "deprecated": "Use @eslint/object-schema instead", "dev": true }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", @@ -278,17 +227,6 @@ "node": ">= 8" } }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=14" - } - }, "node_modules/@rollup/plugin-commonjs": { "version": "28.0.0", "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-28.0.0.tgz", @@ -722,34 +660,12 @@ "node": ">=8" } }, - "node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", "dev": true }, - "node_modules/benchmark": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/benchmark/-/benchmark-2.1.4.tgz", - "integrity": "sha1-CfPeMckWQl1JjMLuVloOvzwqVik=", - "dev": true, - "dependencies": { - "lodash": "^4.17.4", - "platform": "^1.3.3" - } - }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -775,29 +691,6 @@ "node": ">=6" } }, - "node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/chalk/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, "node_modules/chokidar": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.1.tgz", @@ -813,21 +706,6 @@ "url": "https://paulmillr.com/funding/" } }, - "node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", - "dev": true - }, "node_modules/commander": { "version": "2.20.1", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.1.tgz", @@ -847,32 +725,6 @@ "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", "dev": true }, - "node_modules/cross-spawn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", - "dev": true, - "dependencies": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - }, - "engines": { - "node": ">=4.8" - } - }, - "node_modules/cross-spawn/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver" - } - }, "node_modules/debug": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", @@ -906,18 +758,6 @@ "node": ">=0.10.0" } }, - "node_modules/define-properties": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", - "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", - "dev": true, - "dependencies": { - "object-keys": "^1.0.12" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -930,64 +770,6 @@ "node": ">=6.0.0" } }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true, - "license": "MIT" - }, - "node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true, - "license": "MIT" - }, - "node_modules/error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "dev": true, - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, - "node_modules/es-abstract": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.15.0.tgz", - "integrity": "sha512-bhkEqWJ2t2lMeaJDuk7okMkJWI/yqgH/EoGwpcvv0XW9RWQsRspI4wt6xuyuvMvvQE3gg/D9HXppgk21w78GyQ==", - "dev": true, - "dependencies": { - "es-to-primitive": "^1.2.0", - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.0", - "is-callable": "^1.1.4", - "is-regex": "^1.0.4", - "object-inspect": "^1.6.0", - "object-keys": "^1.1.1", - "string.prototype.trimleft": "^2.1.0", - "string.prototype.trimright": "^2.1.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-to-primitive": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.0.tgz", - "integrity": "sha512-qZryBOJjV//LaxLTV6UC//WewneB3LcXOL9NP++ozKVXsIIIpm/2c13UDiD9Jp2eThsecw9m3jPqDwTyobcdbg==", - "dev": true, - "dependencies": { - "is-callable": "^1.1.4", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/eslint": { "version": "8.57.1", "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", @@ -1487,87 +1269,6 @@ "integrity": "sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg==", "dev": true }, - "node_modules/foreground-child": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", - "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", - "dev": true, - "license": "ISC", - "dependencies": { - "cross-spawn": "^7.0.0", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/foreground-child/node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/foreground-child/node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/foreground-child/node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/foreground-child/node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/foreground-child/node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -1599,93 +1300,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/glob": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.0.tgz", - "integrity": "sha512-9UiX/Bl6J2yaBbxKoEBRm4Cipxgok8kQYcOPEhScPwebu2I0HoQOuYdIO6S3hLuWoZgpDpwQZMzTFxgpkyT76g==", - "dev": true, - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^4.0.1", - "minimatch": "^10.0.0", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^2.0.0" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/glob/node_modules/lru-cache": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.0.1.tgz", - "integrity": "sha512-CgeuL5uom6j/ZVrg7G/+1IXqRY8JXX4Hghfy5YE0EhoYQWvndP1kufu58cmZLNIDKnRhZrXfdS9urVWx98AipQ==", - "dev": true, - "license": "ISC", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/glob/node_modules/minimatch": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz", - "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob/node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/glob/node_modules/path-scurry": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", - "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^11.0.0", - "minipass": "^7.1.2" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/globals": { "version": "13.24.0", "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", @@ -1701,48 +1315,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/graceful-fs": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.2.tgz", - "integrity": "sha512-IItsdsea19BoLC7ELy13q1iJFNmd7ofZH5+X/pJr90/nRoPEX0DJo1dHDbgtYWOhJhcCgMDTOw84RZ72q6lB+Q==", - "dev": true - }, "node_modules/graphemer": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true }, - "node_modules/has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dev": true, - "dependencies": { - "function-bind": "^1.1.1" - }, - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/has-symbols": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.0.tgz", - "integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=", - "dev": true, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -1756,12 +1334,6 @@ "node": ">= 0.4" } }, - "node_modules/hosted-git-info": { - "version": "2.8.9", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", - "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", - "dev": true - }, "node_modules/ignore": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", @@ -1812,21 +1384,6 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "dev": true }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", - "dev": true - }, - "node_modules/is-callable": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.4.tgz", - "integrity": "sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA==", - "dev": true, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/is-core-module": { "version": "2.15.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", @@ -1843,15 +1400,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-date-object": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz", - "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=", - "dev": true, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -1899,61 +1447,12 @@ "@types/estree": "*" } }, - "node_modules/is-regex": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz", - "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=", - "dev": true, - "dependencies": { - "has": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/is-symbol": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.2.tgz", - "integrity": "sha512-HS8bZ9ox60yCJLH9snBpIwv9pYUAkcuLhSA1oero1UB5y9aiQpRA8y2ex945AOtCZL1lJDeIk3G5LthswI46Lw==", - "dev": true, - "dependencies": { - "has-symbols": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", "dev": true }, - "node_modules/jackspeak": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.0.1.tgz", - "integrity": "sha512-cub8rahkh0Q/bw1+GxP7aeSe29hHHn2V4m29nnDlvCdlgU+3UGxkZp7Z53jLUdpX3jdTO0nJZUDl3xvbWc2Xog==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, - "node_modules/json-parse-better-errors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", - "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", - "dev": true - }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -1966,30 +1465,6 @@ "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", "dev": true }, - "node_modules/load-json-file": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", - "integrity": "sha1-L19Fq5HjMhYjT9U62rZo607AmTs=", - "dev": true, - "dependencies": { - "graceful-fs": "^4.1.2", - "parse-json": "^4.0.0", - "pify": "^3.0.0", - "strip-bom": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/load-json-file/node_modules/pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -2005,12 +1480,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true - }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -2034,15 +1503,6 @@ "@jridgewell/sourcemap-codec": "^1.5.0" } }, - "node_modules/memorystream": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz", - "integrity": "sha1-htcJCzDORV1j+64S3aUaR93K+bI=", - "dev": true, - "engines": { - "node": ">= 0.10.0" - } - }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -2078,77 +1538,6 @@ "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", "dev": true }, - "node_modules/nice-try": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", - "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", - "dev": true - }, - "node_modules/normalize-package-data": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", - "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", - "dev": true, - "dependencies": { - "hosted-git-info": "^2.1.4", - "resolve": "^1.10.0", - "semver": "2 || 3 || 4 || 5", - "validate-npm-package-license": "^3.0.1" - } - }, - "node_modules/normalize-package-data/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/npm-run-all": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/npm-run-all/-/npm-run-all-4.1.5.tgz", - "integrity": "sha512-Oo82gJDAVcaMdi3nuoKFavkIHBRVqQ1qvMb+9LHk/cF4P6B2m8aP04hGf7oL6wZ9BuGwX1onlLhpuoofSyoQDQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "chalk": "^2.4.1", - "cross-spawn": "^6.0.5", - "memorystream": "^0.3.1", - "minimatch": "^3.0.4", - "pidtree": "^0.3.0", - "read-pkg": "^3.0.0", - "shell-quote": "^1.6.1", - "string.prototype.padend": "^3.0.0" - }, - "bin": { - "npm-run-all": "bin/npm-run-all/index.js", - "run-p": "bin/run-p/index.js", - "run-s": "bin/run-s/index.js" - }, - "engines": { - "node": ">= 4" - } - }, - "node_modules/object-inspect": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.0.tgz", - "integrity": "sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -2247,13 +1636,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/package-json-from-dist": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz", - "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==", - "dev": true, - "license": "BlueOak-1.0.0" - }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -2266,19 +1648,6 @@ "node": ">=6" } }, - "node_modules/parse-json": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", - "dev": true, - "dependencies": { - "error-ex": "^1.3.1", - "json-parse-better-errors": "^1.0.1" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -2297,15 +1666,6 @@ "node": ">=0.10.0" } }, - "node_modules/path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", @@ -2354,24 +1714,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/pidtree": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.3.0.tgz", - "integrity": "sha512-9CT4NFlDcosssyg8KVFltgokyKZIFjoBxw8CTGy+5F38Y1eQWrt8tRayiUOXE+zVKQnYu5BR8JjCtvK3BcnBhg==", - "dev": true, - "bin": { - "pidtree": "bin/pidtree.js" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/platform": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/platform/-/platform-1.3.5.tgz", - "integrity": "sha512-TuvHS8AOIZNAlE77WUDiR4rySV/VMptyMfcfeoMgs4P8apaZM3JrnbzBiixKUv+XR6i+BXrQh8WAnjaSPFO65Q==", - "dev": true - }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -2411,41 +1753,6 @@ "safe-buffer": "^5.1.0" } }, - "node_modules/read-pkg": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", - "integrity": "sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=", - "dev": true, - "dependencies": { - "load-json-file": "^4.0.0", - "normalize-package-data": "^2.3.2", - "path-type": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/read-pkg/node_modules/path-type": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", - "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", - "dev": true, - "dependencies": { - "pify": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/read-pkg/node_modules/pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/readdirp": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.0.1.tgz", @@ -2496,26 +1803,6 @@ "node": ">=0.10.0" } }, - "node_modules/rimraf": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.0.1.tgz", - "integrity": "sha512-9dkvaxAsk/xNXSJzMgFqqMCuFgt2+KsOFek3TMLfo8NCPfWpBmqwyNn5Y+NX56QUYfCtsyhF3ayiboEoUmJk/A==", - "dev": true, - "license": "ISC", - "dependencies": { - "glob": "^11.0.0", - "package-json-from-dist": "^1.0.0" - }, - "bin": { - "rimraf": "dist/esm/bin.mjs" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/rollup": { "version": "4.24.0", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.24.0.tgz", @@ -2606,46 +1893,6 @@ "randombytes": "^2.1.0" } }, - "node_modules/shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", - "dev": true, - "dependencies": { - "shebang-regex": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/shell-quote": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.3.tgz", - "integrity": "sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw==", - "dev": true - }, - "node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/smob": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/smob/-/smob-1.5.0.tgz", @@ -2673,296 +1920,87 @@ "source-map": "^0.6.0" } }, - "node_modules/spdx-correct": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.0.tgz", - "integrity": "sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q==", + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, "dependencies": { - "spdx-expression-parse": "^3.0.0", - "spdx-license-ids": "^3.0.0" + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" } }, - "node_modules/spdx-exceptions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz", - "integrity": "sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA==", - "dev": true - }, - "node_modules/spdx-expression-parse": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz", - "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==", + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", "dev": true, - "dependencies": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/spdx-license-ids": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz", - "integrity": "sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q==", - "dev": true - }, - "node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", "dev": true, "license": "MIT", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, "engines": { - "node": ">=12" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "node_modules/terser": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-4.8.1.tgz", + "integrity": "sha512-4GnLC0x667eJG0ewJTa6z/yXrbLGv80D9Ru6HIpCQmO+Q4PfEtBFi0ObSckqwL6VyQv/7ENJieXHo2ANmdQwgw==", "dev": true, - "license": "MIT", "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" + "commander": "^2.20.0", + "source-map": "~0.6.1", + "source-map-support": "~0.5.12" + }, + "bin": { + "terser": "bin/terser" }, "engines": { - "node": ">=8" + "node": ">=6.0.0" } }, - "node_modules/string-width-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/string-width-cjs/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", + "dev": true }, - "node_modules/string-width/node_modules/ansi-regex": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", "dev": true, - "license": "MIT", "engines": { - "node": ">=12" + "node": ">=10" }, "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/string-width/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/string.prototype.padend": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/string.prototype.padend/-/string.prototype.padend-3.0.0.tgz", - "integrity": "sha1-86rvfBcZ8XDF6rHDK/eA2W4h8vA=", - "dev": true, - "dependencies": { - "define-properties": "^1.1.2", - "es-abstract": "^1.4.3", - "function-bind": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/string.prototype.trimleft": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.1.0.tgz", - "integrity": "sha512-FJ6b7EgdKxxbDxc79cOlok6Afd++TTs5szo+zJTUyow3ycrRfJVE2pq3vcN53XexvKZu/DJMDfeI/qMiZTrjTw==", - "dev": true, - "dependencies": { - "define-properties": "^1.1.3", - "function-bind": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/string.prototype.trimright": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/string.prototype.trimright/-/string.prototype.trimright-2.1.0.tgz", - "integrity": "sha512-fXZTSV55dNBwv16uw+hh5jkghxSnc5oHq+5K/gXgizHwAvMetdAJlHqqoFC1FSDVPYWLkAKl2cxpUT41sV7nSg==", - "dev": true, - "dependencies": { - "define-properties": "^1.1.3", - "function-bind": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/terser": { - "version": "4.8.1", - "resolved": "https://registry.npmjs.org/terser/-/terser-4.8.1.tgz", - "integrity": "sha512-4GnLC0x667eJG0ewJTa6z/yXrbLGv80D9Ru6HIpCQmO+Q4PfEtBFi0ObSckqwL6VyQv/7ENJieXHo2ANmdQwgw==", - "dev": true, - "dependencies": { - "commander": "^2.20.0", - "source-map": "~0.6.1", - "source-map-support": "~0.5.12" - }, - "bin": { - "terser": "bin/terser" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", - "dev": true - }, - "node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", "dev": true, "dependencies": { "punycode": "^2.1.0" } }, - "node_modules/validate-npm-package-license": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", - "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", - "dev": true, - "dependencies": { - "spdx-correct": "^3.0.0", - "spdx-expression-parse": "^3.0.0" - } - }, - "node_modules/which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dev": true, - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "which": "bin/which" - } - }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -2972,153 +2010,6 @@ "node": ">=0.10.0" } }, - "node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/wrap-ansi-cjs/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-regex": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/wrap-ansi/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -3217,37 +2108,6 @@ "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", "dev": true }, - "@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dev": true, - "requires": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", - "dev": true - }, - "strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, - "requires": { - "ansi-regex": "^6.0.1" - } - } - } - }, "@jridgewell/gen-mapping": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", @@ -3323,13 +2183,6 @@ "fastq": "^1.6.0" } }, - "@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "dev": true, - "optional": true - }, "@rollup/plugin-commonjs": { "version": "28.0.0", "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-28.0.0.tgz", @@ -3571,31 +2424,12 @@ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true }, - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", "dev": true }, - "benchmark": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/benchmark/-/benchmark-2.1.4.tgz", - "integrity": "sha1-CfPeMckWQl1JjMLuVloOvzwqVik=", - "dev": true, - "requires": { - "lodash": "^4.17.4", - "platform": "^1.3.3" - } - }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -3618,25 +2452,6 @@ "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", "dev": true }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "dependencies": { - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", - "dev": true - } - } - }, "chokidar": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.1.tgz", @@ -3646,21 +2461,6 @@ "readdirp": "^4.0.1" } }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", - "dev": true - }, "commander": { "version": "2.20.1", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.1.tgz", @@ -3679,27 +2479,6 @@ "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", "dev": true }, - "cross-spawn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", - "dev": true, - "requires": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - }, - "dependencies": { - "semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", - "dev": true - } - } - }, "debug": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", @@ -3721,15 +2500,6 @@ "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", "dev": true }, - "define-properties": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", - "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", - "dev": true, - "requires": { - "object-keys": "^1.0.12" - } - }, "doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -3739,56 +2509,6 @@ "esutils": "^2.0.2" } }, - "eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true - }, - "emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true - }, - "error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "dev": true, - "requires": { - "is-arrayish": "^0.2.1" - } - }, - "es-abstract": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.15.0.tgz", - "integrity": "sha512-bhkEqWJ2t2lMeaJDuk7okMkJWI/yqgH/EoGwpcvv0XW9RWQsRspI4wt6xuyuvMvvQE3gg/D9HXppgk21w78GyQ==", - "dev": true, - "requires": { - "es-to-primitive": "^1.2.0", - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.0", - "is-callable": "^1.1.4", - "is-regex": "^1.0.4", - "object-inspect": "^1.6.0", - "object-keys": "^1.1.1", - "string.prototype.trimleft": "^2.1.0", - "string.prototype.trimright": "^2.1.0" - } - }, - "es-to-primitive": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.0.tgz", - "integrity": "sha512-qZryBOJjV//LaxLTV6UC//WewneB3LcXOL9NP++ozKVXsIIIpm/2c13UDiD9Jp2eThsecw9m3jPqDwTyobcdbg==", - "dev": true, - "requires": { - "is-callable": "^1.1.4", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.2" - } - }, "eslint": { "version": "8.57.1", "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", @@ -4153,59 +2873,6 @@ "integrity": "sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg==", "dev": true }, - "foreground-child": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", - "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", - "dev": true, - "requires": { - "cross-spawn": "^7.0.0", - "signal-exit": "^4.0.1" - }, - "dependencies": { - "cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dev": true, - "requires": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - } - }, - "path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true - }, - "shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "requires": { - "shebang-regex": "^3.0.0" - } - }, - "shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true - }, - "which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - } - } - }, "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -4219,68 +2886,12 @@ "dev": true, "optional": true }, - "function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true - }, - "glob": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.0.tgz", - "integrity": "sha512-9UiX/Bl6J2yaBbxKoEBRm4Cipxgok8kQYcOPEhScPwebu2I0HoQOuYdIO6S3hLuWoZgpDpwQZMzTFxgpkyT76g==", - "dev": true, - "requires": { - "foreground-child": "^3.1.0", - "jackspeak": "^4.0.1", - "minimatch": "^10.0.0", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^2.0.0" - }, - "dependencies": { - "brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "requires": { - "balanced-match": "^1.0.0" - } - }, - "lru-cache": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.0.1.tgz", - "integrity": "sha512-CgeuL5uom6j/ZVrg7G/+1IXqRY8JXX4Hghfy5YE0EhoYQWvndP1kufu58cmZLNIDKnRhZrXfdS9urVWx98AipQ==", - "dev": true - }, - "minimatch": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz", - "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==", - "dev": true, - "requires": { - "brace-expansion": "^2.0.1" - } - }, - "minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "dev": true - }, - "path-scurry": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", - "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", - "dev": true, - "requires": { - "lru-cache": "^11.0.0", - "minipass": "^7.1.2" - } - } - } - }, + "function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true + }, "globals": { "version": "13.24.0", "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", @@ -4290,39 +2901,12 @@ "type-fest": "^0.20.2" } }, - "graceful-fs": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.2.tgz", - "integrity": "sha512-IItsdsea19BoLC7ELy13q1iJFNmd7ofZH5+X/pJr90/nRoPEX0DJo1dHDbgtYWOhJhcCgMDTOw84RZ72q6lB+Q==", - "dev": true - }, "graphemer": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true }, - "has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dev": true, - "requires": { - "function-bind": "^1.1.1" - } - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true - }, - "has-symbols": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.0.tgz", - "integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=", - "dev": true - }, "hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -4332,12 +2916,6 @@ "function-bind": "^1.1.2" } }, - "hosted-git-info": { - "version": "2.8.9", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", - "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", - "dev": true - }, "ignore": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", @@ -4376,18 +2954,6 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "dev": true }, - "is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", - "dev": true - }, - "is-callable": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.4.tgz", - "integrity": "sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA==", - "dev": true - }, "is-core-module": { "version": "2.15.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", @@ -4397,12 +2963,6 @@ "hasown": "^2.0.2" } }, - "is-date-object": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz", - "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=", - "dev": true - }, "is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -4439,46 +2999,12 @@ "@types/estree": "*" } }, - "is-regex": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz", - "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=", - "dev": true, - "requires": { - "has": "^1.0.1" - } - }, - "is-symbol": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.2.tgz", - "integrity": "sha512-HS8bZ9ox60yCJLH9snBpIwv9pYUAkcuLhSA1oero1UB5y9aiQpRA8y2ex945AOtCZL1lJDeIk3G5LthswI46Lw==", - "dev": true, - "requires": { - "has-symbols": "^1.0.0" - } - }, "isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", "dev": true }, - "jackspeak": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.0.1.tgz", - "integrity": "sha512-cub8rahkh0Q/bw1+GxP7aeSe29hHHn2V4m29nnDlvCdlgU+3UGxkZp7Z53jLUdpX3jdTO0nJZUDl3xvbWc2Xog==", - "dev": true, - "requires": { - "@isaacs/cliui": "^8.0.2", - "@pkgjs/parseargs": "^0.11.0" - } - }, - "json-parse-better-errors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", - "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", - "dev": true - }, "json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -4491,26 +3017,6 @@ "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", "dev": true }, - "load-json-file": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", - "integrity": "sha1-L19Fq5HjMhYjT9U62rZo607AmTs=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "parse-json": "^4.0.0", - "pify": "^3.0.0", - "strip-bom": "^3.0.0" - }, - "dependencies": { - "pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", - "dev": true - } - } - }, "locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -4520,12 +3026,6 @@ "p-locate": "^5.0.0" } }, - "lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true - }, "lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -4547,12 +3047,6 @@ "@jridgewell/sourcemap-codec": "^1.5.0" } }, - "memorystream": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz", - "integrity": "sha1-htcJCzDORV1j+64S3aUaR93K+bI=", - "dev": true - }, "minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -4580,61 +3074,6 @@ "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", "dev": true }, - "nice-try": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", - "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", - "dev": true - }, - "normalize-package-data": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", - "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", - "dev": true, - "requires": { - "hosted-git-info": "^2.1.4", - "resolve": "^1.10.0", - "semver": "2 || 3 || 4 || 5", - "validate-npm-package-license": "^3.0.1" - }, - "dependencies": { - "semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", - "dev": true - } - } - }, - "npm-run-all": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/npm-run-all/-/npm-run-all-4.1.5.tgz", - "integrity": "sha512-Oo82gJDAVcaMdi3nuoKFavkIHBRVqQ1qvMb+9LHk/cF4P6B2m8aP04hGf7oL6wZ9BuGwX1onlLhpuoofSyoQDQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "chalk": "^2.4.1", - "cross-spawn": "^6.0.5", - "memorystream": "^0.3.1", - "minimatch": "^3.0.4", - "pidtree": "^0.3.0", - "read-pkg": "^3.0.0", - "shell-quote": "^1.6.1", - "string.prototype.padend": "^3.0.0" - } - }, - "object-inspect": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.0.tgz", - "integrity": "sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g==", - "dev": true - }, - "object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true - }, "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -4703,12 +3142,6 @@ "p-limit": "^3.0.2" } }, - "package-json-from-dist": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz", - "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==", - "dev": true - }, "parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -4718,16 +3151,6 @@ "callsites": "^3.0.0" } }, - "parse-json": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", - "dev": true, - "requires": { - "error-ex": "^1.3.1", - "json-parse-better-errors": "^1.0.1" - } - }, "path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -4740,12 +3163,6 @@ "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", "dev": true }, - "path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", - "dev": true - }, "path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", @@ -4778,18 +3195,6 @@ "optional": true, "peer": true }, - "pidtree": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.3.0.tgz", - "integrity": "sha512-9CT4NFlDcosssyg8KVFltgokyKZIFjoBxw8CTGy+5F38Y1eQWrt8tRayiUOXE+zVKQnYu5BR8JjCtvK3BcnBhg==", - "dev": true - }, - "platform": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/platform/-/platform-1.3.5.tgz", - "integrity": "sha512-TuvHS8AOIZNAlE77WUDiR4rySV/VMptyMfcfeoMgs4P8apaZM3JrnbzBiixKUv+XR6i+BXrQh8WAnjaSPFO65Q==", - "dev": true - }, "punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -4811,34 +3216,6 @@ "safe-buffer": "^5.1.0" } }, - "read-pkg": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", - "integrity": "sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=", - "dev": true, - "requires": { - "load-json-file": "^4.0.0", - "normalize-package-data": "^2.3.2", - "path-type": "^3.0.0" - }, - "dependencies": { - "path-type": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", - "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", - "dev": true, - "requires": { - "pify": "^3.0.0" - } - }, - "pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", - "dev": true - } - } - }, "readdirp": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.0.1.tgz", @@ -4868,16 +3245,6 @@ "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", "dev": true }, - "rimraf": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.0.1.tgz", - "integrity": "sha512-9dkvaxAsk/xNXSJzMgFqqMCuFgt2+KsOFek3TMLfo8NCPfWpBmqwyNn5Y+NX56QUYfCtsyhF3ayiboEoUmJk/A==", - "dev": true, - "requires": { - "glob": "^11.0.0", - "package-json-from-dist": "^1.0.0" - } - }, "rollup": { "version": "4.24.0", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.24.0.tgz", @@ -4928,33 +3295,6 @@ "randombytes": "^2.1.0" } }, - "shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", - "dev": true, - "requires": { - "shebang-regex": "^1.0.0" - } - }, - "shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", - "dev": true - }, - "shell-quote": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.3.tgz", - "integrity": "sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw==", - "dev": true - }, - "signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true - }, "smob": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/smob/-/smob-1.5.0.tgz", @@ -4977,122 +3317,6 @@ "source-map": "^0.6.0" } }, - "spdx-correct": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.0.tgz", - "integrity": "sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q==", - "dev": true, - "requires": { - "spdx-expression-parse": "^3.0.0", - "spdx-license-ids": "^3.0.0" - } - }, - "spdx-exceptions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz", - "integrity": "sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA==", - "dev": true - }, - "spdx-expression-parse": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz", - "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==", - "dev": true, - "requires": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" - } - }, - "spdx-license-ids": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz", - "integrity": "sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q==", - "dev": true - }, - "string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, - "requires": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "dependencies": { - "ansi-regex": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", - "dev": true - }, - "strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, - "requires": { - "ansi-regex": "^6.0.1" - } - } - } - }, - "string-width-cjs": { - "version": "npm:string-width@4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "dependencies": { - "emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true - } - } - }, - "string.prototype.padend": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/string.prototype.padend/-/string.prototype.padend-3.0.0.tgz", - "integrity": "sha1-86rvfBcZ8XDF6rHDK/eA2W4h8vA=", - "dev": true, - "requires": { - "define-properties": "^1.1.2", - "es-abstract": "^1.4.3", - "function-bind": "^1.0.2" - } - }, - "string.prototype.trimleft": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.1.0.tgz", - "integrity": "sha512-FJ6b7EgdKxxbDxc79cOlok6Afd++TTs5szo+zJTUyow3ycrRfJVE2pq3vcN53XexvKZu/DJMDfeI/qMiZTrjTw==", - "dev": true, - "requires": { - "define-properties": "^1.1.3", - "function-bind": "^1.1.1" - } - }, - "string.prototype.trimright": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/string.prototype.trimright/-/string.prototype.trimright-2.1.0.tgz", - "integrity": "sha512-fXZTSV55dNBwv16uw+hh5jkghxSnc5oHq+5K/gXgizHwAvMetdAJlHqqoFC1FSDVPYWLkAKl2cxpUT41sV7nSg==", - "dev": true, - "requires": { - "define-properties": "^1.1.3", - "function-bind": "^1.1.1" - } - }, "strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -5102,36 +3326,12 @@ "ansi-regex": "^5.0.1" } }, - "strip-ansi-cjs": { - "version": "npm:strip-ansi@6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.1" - } - }, - "strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", - "dev": true - }, "strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", "dev": true }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - }, "supports-preserve-symlinks-flag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", @@ -5170,125 +3370,12 @@ "punycode": "^2.1.0" } }, - "validate-npm-package-license": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", - "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", - "dev": true, - "requires": { - "spdx-correct": "^3.0.0", - "spdx-expression-parse": "^3.0.0" - } - }, - "which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - }, "word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", "dev": true }, - "wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, - "requires": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "dependencies": { - "ansi-regex": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", - "dev": true - }, - "ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "dev": true - }, - "strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, - "requires": { - "ansi-regex": "^6.0.1" - } - } - } - }, - "wrap-ansi-cjs": { - "version": "npm:wrap-ansi@7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "requires": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true - }, - "string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - } - } - } - }, "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", diff --git a/package.json b/package.json index 63a1d4de1..efd0c30aa 100644 --- a/package.json +++ b/package.json @@ -18,27 +18,17 @@ ], "exports": { "./stream.js": { - "production": { - "import": "./dist/stream.esm.min.js", - "require": "./dist/stream.umd.min.js" - }, "import": "./dist/stream.esm.js", "require": "./dist/stream.umd.js" }, ".": { - "production": { - "import": "./dist/mithril.esm.min.js", - "require": "./dist/mithril.umd.min.js" - }, "import": "./dist/mithril.esm.js", "require": "./dist/mithril.umd.js" } }, "scripts": { "build": "node scripts/build.js --save", - "cleanup:lint": "rimraf .eslintcache", - "lint": "run-s -cn lint:**", - "lint:js": "eslint . --cache", + "lint": "eslint . --cache", "perf": "node performance/test-perf.js", "pretest": "npm run lint", "test": "ospec" @@ -47,13 +37,9 @@ "@rollup/plugin-commonjs": "^28.0.0", "@rollup/plugin-node-resolve": "^15.3.0", "@rollup/plugin-terser": "^0.4.4", - "benchmark": "^2.1.4", "chokidar": "^4.0.1", "eslint": "^8.9.0", - "glob": "^11.0.0", - "npm-run-all": "^4.1.5", "ospec": "4.2.1", - "rimraf": "^6.0.1", "rollup": "^4.24.0", "terser": "^4.3.4" } diff --git a/performance/bench.js b/performance/bench.js new file mode 100644 index 000000000..335b1764d --- /dev/null +++ b/performance/bench.js @@ -0,0 +1,187 @@ +/* +Rolling my own benchmark system, so I can minimize overhead and have something actually maintained. + +Things it does better than Benchmark.js: + +- It uses an exposed benchmark loop, so I can precisely control inter-frame delays. +- It prints out much more useful output: a confidence interval-based range and a total run count. +- It works around the low resolution inherent to modern browsers. + +/* global performance */ + +// Note: this should be even. Odd counts will be rounded up to even. +const initSamples = 10 +const minSamples = 40 +const minDuration = 500 +const maxDuration = 5000 +const minConfidence = 0.98 +// I don't feel like doing the calculus, so I've used a confidence interval table for this. +// (They're ridiculously easy to find.) +const criticalValueForMinConfidence = 2.33 + +// I want some level of resolution to be able to have reasonable results. 2ms can still be remotely +// useful, but Firefox's reduced fingerprinting preference clamping of 100ms is far too high. +const minResolutionTolerable = 2 + +let secondMax = 0 +let max = 0 +let min = Infinity + +// Try for 100 samples and dispose the highest if it's considerably higher than the second highest. +for (let i = 0; i < 100; i++) { + const start = performance.now() + let diff = 0 + while (diff <= 0) { + diff = performance.now() - start + } + if (max > minResolutionTolerable && diff > minResolutionTolerable) { + throw new Error("Resolution is too coarse to be useful for measurement") + } + if (secondMax < max) secondMax = max + if (max < diff) max = diff + if (min > diff) min = diff +} + +if (min > 0.999) { + console.log(`Timer resolution detected: ${min > 999 ? min : min.toPrecision(3)}ms`) +} else if (min > 0.000999) { + console.log(`Timer resolution detected: ${(min * 1000).toPrecision(3)}µs`) +} else { + console.log(`Timer resolution detected: ${Math.round(min * 1000000)}ns`) +} + +// Give me at least 15 units of resolution to be useful. +const minDurationPerPass = min * 15 + +// Uses West's weighted variance algorithm for computing variance. Each duration sample is given the. +// Ref: https://doi.org/10.1145%2F359146.359153 + +class BenchState { + constructor(minSamples, minDuration, maxDuration, minConfidence) { + if (minSamples < 2) { + throw new RangeError("At least two samples are required to compute variance.") + } + + // Convert the confidence into a critical value for fast margin of error comparison. + + /** @private */ this._minSamples = minSamples + /** @private */ this._minDuration = minDuration + /** @private */ this._maxDuration = maxDuration + /** @private */ this._minConfidence = minConfidence + + /** @private */ this._testStart = performance.now() + /** @private */ this._multi = 0 + /** @private */ this._start = 0 + + /** @private */ this._mean = 0 + /** @private */ this._count = 0 + /** @private */ this._wsum2 = 0 + /** @private */ this._s = 0 + } + + stats() { + // Find the margin of error. Applies Bessel's correction as it's a frequency weight. + const stdError = Math.sqrt(this._s / ((this._count - 1) * this._count)) + return { + ticks: this._count, + mean: this._mean, + marginOfError: stdError * criticalValueForMinConfidence, + } + } + + done() { + const count = this._count + if (count < this._minSamples) return false + const duration = performance.now() - this._testStart + if (duration >= this._maxDuration) return true + if (duration < this._minDuration) return false + // Find the margin of error. Applies Bessel's correction as it's a frequency weight. + const stdError = Math.sqrt(this._s / ((count - 1) * count)) + const marginOfError = stdError * criticalValueForMinConfidence + return marginOfError / this._mean >= this._minConfidence + } + + start() { + this._start = performance.now() + this._multi = 0 + } + + tick() { + let sample = performance.now() - this._start + this._multi++ + if (sample < minDurationPerPass) return false + + const weight = this._multi + const meanOld = this._mean + sample /= weight + this._count += weight + this._wsum2 += weight * weight + this._mean = meanOld + (weight / this._count) * (sample - meanOld) + this._s += weight * (sample - meanOld) * (sample - this._mean) + return true + } +} + +/** + * @param {{[key: string]: (state: BenchState) => void | PromiseLike}} tests + */ +export async function runBenchmarks(tests) { + const testCount = Object.keys(tests).length + + console.log(`${testCount} test${testCount === 1 ? "" : "s"} loaded`) + + const start = performance.now() + + // Minimize sample count within the warm-up loop, so ICs receive the right runtime + // information. + let failed = false + + for (let i = 0; i < initSamples; i += 2) { + for (const [name, test] of Object.entries(tests)) { + try { + await test(new BenchState(2, 0, Infinity, 0)) + } catch (e) { + failed = true + console.error(`Error while warming up ${name}:`) + console.error(e) + } + } + } + + if (failed) return + + console.log("Tests warmed up") + + for (const [name, test] of Object.entries(tests)) { + const state = new BenchState(minSamples, minDuration, maxDuration, minConfidence) + // Let errors here crash the benchmark. + await test(state) + const {mean, marginOfError, ticks} = state.stats() + + let min = mean - marginOfError + let max = mean + marginOfError + let unit = "ms" + + if (max < 1) { + min *= 1000 + max *= 1000 + unit = "µs" + if (max < 1) { + min *= 1000 + max *= 1000 + unit = "ns" + } + } + + min = min.toPrecision(3) + max = max.toPrecision(3) + + const span = min === max ? min : `${min}-${max}` + + console.log(`${name}: ${span} ${unit}/op, n = ${ticks}`) + } + + const end = performance.now() + + console.log(`Test completed in ${Math.round((end - start) / 1000)}s`) +} diff --git a/performance/components/mount-nested-tree.js b/performance/components/mount-nested-tree.js deleted file mode 100644 index a51525fd4..000000000 --- a/performance/components/mount-nested-tree.js +++ /dev/null @@ -1,5 +0,0 @@ -/* global m, rootElem */ - -import {nestedTree} from "./nested-tree.js" - -m.mount(rootElem, nestedTree) diff --git a/performance/components/mount-simple-tree.js b/performance/components/mount-simple-tree.js deleted file mode 100644 index 27544e631..000000000 --- a/performance/components/mount-simple-tree.js +++ /dev/null @@ -1,5 +0,0 @@ -/* global m, rootElem */ - -import {simpleTree} from "./simple-tree.js" - -m.mount(rootElem, simpleTree) diff --git a/performance/components/mutate-styles-properties-tree.js b/performance/components/mutate-styles-properties-tree.js new file mode 100644 index 000000000..73f8651d1 --- /dev/null +++ b/performance/components/mutate-styles-properties-tree.js @@ -0,0 +1,81 @@ +import m from "../../dist/mithril.esm.min.js" + +const vnodeCount = 300 + +// The length is simply the lowest common multiple of all the modulos for the style generation. +const styleCount = 10200 + +const multivalue = ["0 1px", "0 0 1px 0", "0", "1px", "20px 10px", "7em 5px", "1px 0 5em 2px"] +const classes = ["foo", "foo bar", "", "baz-bat", null, "fooga", null, null, undefined] +const styles = [] + +const toColor = (c) => `rgba(${c % 255},${255 - c % 255},${50 + c % 150},${c % 50 / 50})` + +const get = (array, index) => array[index % array.length] + +for (let i = 0, counter = 0; i < styleCount; i++) { + const c = ++counter + const style = {} + styles.push(style) + switch (i % 8) { + case 7: + style.border = c % 5 ? `${c % 10}px ${c % 2 ? "solid" : "dotted"} ${toColor(c)}` : "" + // falls through + case 6: + style.color = toColor(c) + // falls through + case 5: + style.display = c % 10 ? c % 2 ? "block" : "inline" : "none" + // falls through + case 4: + style.position = c % 5 ? c % 2 ? "absolute" : "relative" : null + // falls through + case 3: + style.padding = get(multivalue, c) + // falls through + case 2: + style.margin = get(multivalue, c).replace("1px", `${c}px`) + // falls through + case 1: + style.top = c % 2 ? `${c}px` : c + // falls through + case 0: + style.left = c % 3 ? `${c}px` : c + // falls through + } +} + +const titles = [ + "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c", "d", "e", "f", "g", "h", + "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", +] + +const inputValues = ["test0", "test1", "test2", "test3"] + +let count = 0 + +export function mutateStylesPropertiesTree() { + count += vnodeCount + var vnodes = [] + for (var i = 0; i < vnodeCount; i++) { + var index = i + count + vnodes.push( + m("div.booga", + { + class: get(classes, index), + "data-index": index, + title: get(titles, index), + }, + m("input.dooga", {type: "checkbox", checked: index % 3 === 0}), + m("input", {value: get(inputValues, index), disabled: index % 10 ? null : true}), + m("div", {class: get(classes, Math.imul(index, 11))}, + m("p", {style: get(styles, index)}, "p1"), + m("p", {style: get(styles, index + 1)}, "p2"), + m("p", {style: get(styles, Math.imul(index, 2))}, "p3"), + m("p.zooga", {style: get(styles, Math.imul(index, 3) + 1), className: get(classes, Math.imul(index, 7))}, "p4") + ) + ) + ) + } + return vnodes +} diff --git a/performance/components/nested-tree.js b/performance/components/nested-tree.js index 4472e562a..b2ba4e694 100644 --- a/performance/components/nested-tree.js +++ b/performance/components/nested-tree.js @@ -1,4 +1,4 @@ -import m from "../../src/entry/mithril.esm.js" +import m from "../../dist/mithril.esm.min.js" const fields = [] diff --git a/performance/components/repeated-tree.js b/performance/components/repeated-tree.js new file mode 100644 index 000000000..6e279d6c4 --- /dev/null +++ b/performance/components/repeated-tree.js @@ -0,0 +1,54 @@ +import m from "../../dist/mithril.esm.min.js" + +const RepeatedHeader = () => m("header", + m("h1.asdf", "a ", "b", " c ", 0, " d"), + m("nav", + m("a", {href: "/foo"}, "Foo"), + m("a", {href: "/bar"}, "Bar") + ) +) + +const RepeatedForm = () => m("form", {onSubmit() {}}, + m("input", {type: "checkbox", checked: true}), + m("input", {type: "checkbox", checked: false}), + m("fieldset", + m("label", + m("input", {type: "radio", checked: true}) + ), + m("label", + m("input", {type: "radio"}) + ) + ), + m(RepeatedButtonBar, null) +) + +const RepeatedButtonBar = () => m(".button-bar", + m(RepeatedButton, + {style: "width:10px; height:10px; border:1px solid #FFF;"}, + "Normal CSS" + ), + m(RepeatedButton, + {style: "top:0 ; right: 20"}, + "Poor CSS" + ), + m(RepeatedButton, + {style: "invalid-prop:1;padding:1px;font:12px/1.1 arial,sans-serif;", icon: true}, + "Poorer CSS" + ), + m(RepeatedButton, + {style: {margin: 0, padding: "10px", overflow: "visible"}}, + "Object CSS" + ) +) + +const RepeatedButton = (attrs) => m("button", attrs) + +const RepeatedMain = () => m(RepeatedForm) + +const RepeatedRoot = () => m("div.foo.bar[data-foo=bar]", + {p: 2}, + m(RepeatedHeader, null), + m(RepeatedMain, null) +) + +export const repeatedTree = () => m(RepeatedRoot) diff --git a/performance/components/shuffled-keyed-tree.js b/performance/components/shuffled-keyed-tree.js new file mode 100644 index 000000000..ca6b25698 --- /dev/null +++ b/performance/components/shuffled-keyed-tree.js @@ -0,0 +1,25 @@ +import m from "../../dist/mithril.esm.min.js" + +const keys = [] +for (let i = 0; i < 1000; i++) keys.push(`key-${i}`) + +function shuffle() { + // Performs a simple Fisher-Yates shuffle. + let current = keys.length + while (current) { + // eslint-disable-next-line no-bitwise + const index = (Math.random() * current--) | 0 + const temp = keys[index] + keys[index] = keys[current] + keys[current] = temp + } +} + +export const shuffledKeyedTree = () => { + shuffle() + var vnodes = [] + for (const key of keys) { + vnodes.push(m("div.item", {key})) + } + return vnodes +} diff --git a/performance/components/simple-tree.js b/performance/components/simple-tree.js index fa2405ca2..70c278135 100644 --- a/performance/components/simple-tree.js +++ b/performance/components/simple-tree.js @@ -1,4 +1,4 @@ -import m from "../../src/entry/mithril.esm.js" +import m from "../../dist/mithril.esm.min.js" const fields = [] @@ -16,7 +16,7 @@ export const simpleTree = () => m(".foo.bar[data-foo=bar]", {p: 2}, ), m("main", m("form", - {onSubmit: function () {}}, + {onSubmit() {}}, m("input[type=checkbox][checked]"), m("input[type=checkbox]"), m("fieldset", fields.map((field) => diff --git a/performance/index.html b/performance/index.html index 42330e600..ed1db1a13 100644 --- a/performance/index.html +++ b/performance/index.html @@ -3,9 +3,6 @@ Performance tests - - - diff --git a/performance/inject-mock-globals.js b/performance/inject-mock-globals.js deleted file mode 100644 index 18ebe6197..000000000 --- a/performance/inject-mock-globals.js +++ /dev/null @@ -1,8 +0,0 @@ -/* global global */ -import "../test-utils/injectBrowserMock.js" - -import Benchmark from "benchmark" -import m from "../src/entry/mithril.esm.js" - -global.m = m -global.Benchmark = Benchmark diff --git a/performance/is-browser.js b/performance/is-browser.js deleted file mode 100644 index 0920d4e56..000000000 --- a/performance/is-browser.js +++ /dev/null @@ -1 +0,0 @@ -export default typeof window !== "undefined" diff --git a/performance/test-perf-impl.js b/performance/test-perf-impl.js deleted file mode 100644 index 74785ebbb..000000000 --- a/performance/test-perf-impl.js +++ /dev/null @@ -1,279 +0,0 @@ -/* Based off of preact's perf tests, so including their MIT license */ -/* -The MIT License (MIT) - -Copyright (c) 2017 Jason Miller - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -*/ - -// Note: this tests against the generated bundle in browsers, but it tests -// against `index.js` in Node. Please do keep that in mind while testing. -// -// Mithril.js and Benchmark.js are loaded globally via bundle in the browser, so -// this doesn't require a CommonJS sham polyfill. - -// I add it globally just so it's visible in the tests. -/* global m, Benchmark, global, window, document, rootElem: true */ - -import isBrowser from "./is-browser.js" - -import {nestedTree} from "./components/nested-tree.js" -import {simpleTree} from "./components/simple-tree.js" - -// eslint-disable-next-line no-undef -var globalObject = typeof globalThis !== "undefined" ? globalThis : isBrowser ? window : global - -globalObject.rootElem = null - -function cycleRoot() { - if (rootElem) document.body.removeChild(rootElem) - document.body.appendChild(rootElem = document.createElement("div")) -} - -// Initialize benchmark suite -Benchmark.options.async = true -Benchmark.options.initCount = 10 -Benchmark.options.minSamples = 40 - -if (isBrowser) { - // Wait long enough for the browser to actually commit the DOM changes to - // the screen before moving on to the next cycle, so things are at least - // reasonably fresh each cycle. - Benchmark.options.delay = 1 / 30 /* frames per second */ -} - -var suite = new Benchmark.Suite("Mithril.js perf", { - onStart: function () { - this.start = Date.now() - }, - - onCycle: function (e) { - console.log(e.target.toString()) - cycleRoot() - }, - - onComplete: function () { - console.log("Completed perf tests in " + (Date.now() - this.start) + "ms") - }, - - onError: function (e) { - console.error(e) - }, -}) -// eslint-disable-next-line no-unused-vars -var xsuite = {add: function(name) { console.log("skipping " + name) }} - -globalObject.simpleTree = simpleTree -globalObject.nestedTree = nestedTree - -suite.add("construct simple tree", { - fn: function () { - simpleTree() - }, -}) - -suite.add("mount simple tree", { - fn: function () { - m.mount(rootElem, simpleTree) - } -}) - -suite.add("redraw simple tree", { - setup: function () { - m.mount(rootElem, simpleTree) - }, - fn: function () { - m.redrawSync() - }, -}) - -suite.add("mount large nested tree", { - fn: function () { - m.mount(rootElem, nestedTree) - } -}) - -suite.add("redraw large nested tree", { - setup: function () { - m.mount(rootElem, nestedTree) - }, - fn: function () { - m.redrawSync() - }, -}) - -suite.add("mutate styles/properties", { - setup: function () { - function get(obj, i) { return obj[i % obj.length] } - var counter = 0 - var classes = ["foo", "foo bar", "", "baz-bat", null, "fooga", null, null, undefined] - var styles = [] - var multivalue = ["0 1px", "0 0 1px 0", "0", "1px", "20px 10px", "7em 5px", "1px 0 5em 2px"] - var stylekeys = [ - ["left", function (c) { return c % 3 ? c + "px" : c }], - ["top", function (c) { return c % 2 ? c + "px" : c }], - ["margin", function (c) { return get(multivalue, c).replace("1px", c+"px") }], - ["padding", function (c) { return get(multivalue, c) }], - ["position", function (c) { return c%5 ? c%2 ? "absolute" : "relative" : null }], - ["display", function (c) { return c%10 ? c%2 ? "block" : "inline" : "none" }], - ["color", function (c) { return ("rgba(" + (c%255) + ", " + (255 - c%255) + ", " + (50+c%150) + ", " + (c%50/50) + ")") }], - ["border", function (c) { return c%5 ? (c%10) + "px " + (c%2?"solid":"dotted") + " " + stylekeys[6][1](c) : "" }] - ] - var i, j, style, conf - - for (i=0; i<1000; i++) { - style = {} - for (j=0; j m("header", - m("h1.asdf", "a ", "b", " c ", 0, " d"), - m("nav", - m("a", {href: "/foo"}, "Foo"), - m("a", {href: "/bar"}, "Bar") - ) - ) - - var RepeatedForm = () => m("form", {onSubmit: function () {}}, - m("input", {type: "checkbox", checked: true}), - m("input", {type: "checkbox", checked: false}), - m("fieldset", - m("label", - m("input", {type: "radio", checked: true}) - ), - m("label", - m("input", {type: "radio"}) - ) - ), - m(RepeatedButtonBar, null) - ) - - var RepeatedButtonBar = () => m(".button-bar", - m(RepeatedButton, - {style: "width:10px; height:10px; border:1px solid #FFF;"}, - "Normal CSS" - ), - m(RepeatedButton, - {style: "top:0 ; right: 20"}, - "Poor CSS" - ), - m(RepeatedButton, - {style: "invalid-prop:1;padding:1px;font:12px/1.1 arial,sans-serif;", icon: true}, - "Poorer CSS" - ), - m(RepeatedButton, - {style: {margin: 0, padding: "10px", overflow: "visible"}}, - "Object CSS" - ) - ) - - var RepeatedButton = (attrs) => m("button", attrs) - - var RepeatedMain = () => m(RepeatedForm) - - this.RepeatedRoot = () => m("div.foo.bar[data-foo=bar]", - {p: 2}, - m(RepeatedHeader, null), - m(RepeatedMain, null) - ) - }, - fn: function () { - m.render(rootElem, [m(this.RepeatedRoot)]) - m.render(rootElem, []) - }, -}) - -suite.add("reorder keyed list", { - setup: function () { - const keys = [] - for (let i = 0; i < 1000; i++) keys.push(`key-${i}`) - - function shuffle() { - // Performs a simple Fisher-Yates shuffle. - let current = keys.length - while (current) { - // eslint-disable-next-line no-bitwise - const index = (Math.random() * current--) | 0 - const temp = keys[index] - keys[index] = keys[current] - keys[current] = temp - } - } - - this.app = function () { - shuffle() - var vnodes = [] - for (const key of keys) { - vnodes.push(m("div.item", {key})) - } - return vnodes - } - }, - fn: function () { - m.render(rootElem, this.app()) - }, -}) - -if (isBrowser) { - window.onload = function () { - cycleRoot() - suite.run() - } -} else { - cycleRoot() - suite.run() -} diff --git a/performance/test-perf.js b/performance/test-perf.js index ef0bffea0..445ce921e 100644 --- a/performance/test-perf.js +++ b/performance/test-perf.js @@ -1,39 +1,166 @@ -/* Based off of preact's perf tests, so including their MIT license */ -/* -The MIT License (MIT) - -Copyright (c) 2017 Jason Miller - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -*/ - -// Note: this tests against the generated bundle in browsers, but it tests -// against `browser.js` in Node. Please do keep that in mind while testing. -// -// Mithril.js and Benchmark.js are loaded globally via bundle in the browser, so -// this doesn't require a CommonJS sham polyfill. - -// So it can tell from Node that it's not actually in a real browser. This gets mucked with by the -// global injection -import "./is-browser.js" - -// set up browser env on before running tests -import "./inject-mock-globals.js" - -import "./test-perf-impl.js" +/* global window, document */ + +import m from "../dist/mithril.esm.min.js" + +import {runBenchmarks} from "./bench.js" + +import {mutateStylesPropertiesTree} from "./components/mutate-styles-properties-tree.js" +import {nestedTree} from "./components/nested-tree.js" +import {repeatedTree} from "./components/repeated-tree.js" +import {shuffledKeyedTree} from "./components/shuffled-keyed-tree.js" +import {simpleTree} from "./components/simple-tree.js" + +/** @type {Parameters[0]} */ +const benchmarks = Object.create(null) + +async function run() { + if (!isBrowser) await import("../test-utils/injectBrowserMock.js") + await runBenchmarks(benchmarks) + cycleRoot() +} + +const isBrowser = typeof process === "undefined" + +function nextFrame() { + return new Promise((resolve) => window.requestAnimationFrame(resolve)) +} + +let rootElem, allElems + +function cycleRoot() { + if (allElems) { + for (const elem of allElems) { + elem.remove() + m.render(elem, null) + } + } + if (rootElem) { + rootElem.remove() + m.render(rootElem, null) + } + document.body.appendChild(rootElem = document.createElement("div")) +} + +const allTrees = [] + +function addTree(name, treeFn) { + allTrees.push(treeFn) + + benchmarks[`construct ${name}`] = (b) => { + do { + b.start() + do { + treeFn() + } while (!b.tick()) + } while (!b.done()) + } + + benchmarks[`render ${name}`] = async (b) => { + do { + cycleRoot() + b.start() + do { + m.render(rootElem, treeFn()) + } while (!b.tick()) + if (isBrowser) await nextFrame() + } while (!b.done()) + } + + benchmarks[`add/remove ${name}`] = async (b) => { + do { + cycleRoot() + b.start() + do { + m.render(rootElem, treeFn()) + m.render(rootElem, null) + } while (!b.tick()) + if (isBrowser) await nextFrame() + } while (!b.done()) + } +} + +benchmarks["null test"] = (b) => { + do { + cycleRoot() + b.start() + do { + // nothing + } while (!b.tick()) + } while (!b.done()) +} + +addTree("simpleTree", simpleTree) +addTree("nestedTree", nestedTree) +addTree("mutateStylesPropertiesTree", mutateStylesPropertiesTree) +addTree("repeatedTree", repeatedTree) +addTree("shuffledKeyedTree", shuffledKeyedTree) + +benchmarks["mount simpleTree"] = async (b) => { + do { + cycleRoot() + b.start() + do { + m.mount(rootElem, simpleTree) + } while (!b.tick()) + if (isBrowser) await nextFrame() + } while (!b.done()) +} + +benchmarks["redraw simpleTree"] = async (b) => { + do { + cycleRoot() + var redraw = m.mount(rootElem, simpleTree) + b.start() + do { + redraw.sync() + } while (!b.tick()) + if (isBrowser) await nextFrame() + } while (!b.done()) +} + +benchmarks["mount all"] = async (b) => { + do { + cycleRoot() + allElems = allTrees.map(() => { + const elem = document.createElement("div") + rootElem.appendChild(elem) + return elem + }) + b.start() + do { + for (let i = 0; i < allTrees.length; i++) { + m.mount(allElems[i], allTrees[i]) + } + } while (!b.tick()) + if (isBrowser) await nextFrame() + } while (!b.done()) +} + +benchmarks["redraw all"] = async (b) => { + do { + cycleRoot() + const allElems = allTrees.map(() => { + const elem = document.createElement("div") + rootElem.appendChild(elem) + return elem + }) + const allRedraws = allElems.map((elem, i) => m.mount(elem, allTrees[i])) + try { + b.start() + do { + for (const redraw of allRedraws) redraw.sync() + } while (!b.tick()) + if (isBrowser) await nextFrame() + } finally { + for (const elem of allElems) { + m.render(elem, null) + } + } + } while (!b.done()) +} + +if (isBrowser) { + window.onload = run +} else { + run() +} diff --git a/scripts/build.js b/scripts/build.js index b151bb51a..028956f10 100644 --- a/scripts/build.js +++ b/scripts/build.js @@ -12,6 +12,7 @@ const dirname = path.dirname(fileURLToPath(import.meta.url)) /** @type {{[key: import("rollup").ModuleFormat]: import("rollup").Plugin}} */ const terserPlugin = terser({ compress: {passes: 3}, + sourceMap: true, }) function format(n) { @@ -24,8 +25,8 @@ async function build(name, format) { try { await Promise.all([ - bundle.write({file: path.resolve(dirname, `../dist/${name}.js`), format}), - bundle.write({file: path.resolve(dirname, `../dist/${name}.min.js`), format, plugins: [terserPlugin]}), + bundle.write({file: path.resolve(dirname, `../dist/${name}.js`), format, sourcemap: true}), + bundle.write({file: path.resolve(dirname, `../dist/${name}.min.js`), format, plugins: [terserPlugin], sourcemap: true}), ]) } finally { await bundle.close() @@ -62,6 +63,8 @@ async function saveToReadme(size) { } async function main() { + await fs.rm(path.resolve(dirname, "../dist"), {recursive: true}) + await Promise.all([ build("mithril.umd", "iife"), build("mithril.esm", "esm"), diff --git a/scripts/server.js b/scripts/server.js new file mode 100644 index 000000000..4358f13a8 --- /dev/null +++ b/scripts/server.js @@ -0,0 +1,77 @@ +import * as fs from "node:fs" +import * as http from "node:http" +import * as path from "node:path" +import {fileURLToPath} from "node:url" + +const root = path.dirname(path.dirname(fileURLToPath(import.meta.url))) + +const port = process.argv[2] || "8080" + +if (!(/^[1-9][0-9]*$/).test(port) || Number(port) > 65535) { + console.error("Port must be a non-zero integer at most 65535 if provided") + // eslint-disable-next-line no-process-exit + process.exit(1) +} + +const url = "http://localhost:8080/" +const headers = { + "cache-control": "no-cache, no-store, must-revalidate", + "access-control-allow-origin": url, + "access-control-allow-headers": "origin, x-requested-with, content-type, accept, range", + "cross-origin-opener-policy": "same-origin", + "cross-origin-embedder-policy": "require-corp", +} + +function isDisconnectError(e) { + return (/^EPIPE$|^ECONN(?:RESET|ABORT|REFUSED)$/).test(e) +} + +const server = http.createServer((req, res) => { + const receivedDate = new Date() + + let parsedUrl + + try { + parsedUrl = new URL(req.url, url) + } catch { + res.writeHead(400, headers).end() + + console.log(`[${receivedDate.toISOString()}] ${res.statusCode} - ${req.method} ${req.url} "${req.headers["user-agent"] || ""}"`) + return + } + + const file = path.resolve(root, "." + path.posix.resolve("/", parsedUrl.pathname)) + + let contentType + + if (file.endsWith(".js")) { + contentType = "application/javascript;charset=utf-8" + } else if (file.endsWith(".html")) { + contentType = "text/html;charset=utf-8" + } + + fs.readFile(file, (err, buf) => { + if (!err) { + res.writeHead(200, contentType ? {...headers, "content-type": contentType} : headers).end(buf) + } else if (err.code === "ENOENT") { + res.writeHead(404, headers).end() + } else { + res.writeHead(500, headers).end() + } + + console.log(`[${receivedDate.toISOString()}] ${res.statusCode} - ${req.method} ${req.url} "${req.headers["user-agent"] || ""}"`) + }) +}) + +server.on("error", (e) => { + if (!isDisconnectError(e)) { + console.error(e) + process.exitCode = 1 + } +}) + +server.on("listening", () => { + console.log(`Listening at ${url}`) +}) + +server.listen(Number(port)) diff --git a/src/core.js b/src/core.js index 858c19c5a..48c12ae83 100644 --- a/src/core.js +++ b/src/core.js @@ -1,75 +1,118 @@ +/* eslint-disable no-bitwise */ import {hasOwn} from "./util.js" export {m as default} +// Caution: be sure to check the minified output. I've noticed an issue with Terser trying to +// inline single-use functions as IIFEs, and this predictably causes perf issues since engines +// don't seem to reliably lower this in either their bytecode generation *or* their optimized code. +// +// Rather than painfully trying to reduce that to an MVC and filing a bug against it, I'm just +// inlining and commenting everything. It also gives me a better idea of the true cost of various +// functions. +// +// In a few places, there's no-inline hints (`/* @__NOINLINE__ */`) to prevent Terser from +// inlining, in code paths where it's relevant. +// +// Also, be aware: I use some bit operations here. Nothing super fancy like find-first-set, just +// mainly ANDs, ORs, and a one-off XOR for inequality. + /* This same structure is used for several nodes. Here's an explainer for each type. -Components: -- `tag`: component reference -- `state`: view function, may `=== tag` -- `attrs`: most recently received attributes -- `children`: instance vnode -- `dom`: unused - -DOM elements: -- `tag`: tag name string -- `state`: event listener dictionary, if any events were ever registered -- `attrs`: most recently received attributes -- `children`: virtual DOM children -- `dom`: element reference - Retain: -- `tag`: `RETAIN` +- `m` bits 0-2: `0` - All other properties are unused - On ingest, the vnode itself is converted into the type of the element it's retaining. This includes changing its type. Fragments: -- `tag`: `FRAGMENT` -- `state`: unused -- `attrs`: unused -- `children`: virtual DOM children -- `dom`: unused +- `m` bits 0-2: `1` +- `t`: `FRAGMENT` +- `s`: unused +- `a`: unused +- `c`: virtual DOM children +- `d`: unused Keys: -- `tag`: `KEY` -- `state`: identity key (may be any arbitrary object) -- `attrs`: unused -- `children`: virtual DOM children -- `dom`: unused +- `m` bits 0-2: `2` +- `t`: `KEY` +- `s`: identity key (may be any arbitrary object) +- `a`: unused +- `c`: virtual DOM children +- `d`: unused Layout: -- `tag`: `LAYOUT` -- `state`: callback to schedule -- `attrs`: unused -- `children`: unused -- `dom`: abort controller reference +- `m` bits 0-2: `3` +- `t`: `LAYOUT` +- `s`: callback to schedule for create +- `a`: callback to schedule for update +- `c`: unused +- `d`: abort controller reference Text: -- `tag`: `TEXT` -- `state`: text string -- `attrs`: unused -- `children`: unused -- `dom`: abort controller reference -*/ +- `m` bits 0-2: `4` +- `t`: `TEXT` +- `s`: text string +- `a`: unused +- `c`: unused +- `d`: abort controller reference + +Components: +- `m` bits 0-2: `5` +- `t`: component reference +- `s`: view function, may be same as component reference +- `a`: most recently received attributes +- `c`: instance vnode +- `d`: unused -var RETAIN = Symbol.for("m.retain") -var FRAGMENT = Symbol.for("m.Fragment") -var KEY = Symbol.for("m.key") -var LAYOUT = Symbol.for("m.layout") -var TEXT = Symbol.for("m.text") +DOM elements: +- `m` bits 0-2: `6` +- `t`: tag name string +- `s`: event listener dictionary, if any events were ever registered +- `a`: most recently received attributes +- `c`: virtual DOM children +- `d`: element reference + +The `m` field is also used for various assertions, that aren't described here. +*/ -function Vnode(tag, state, attrs, children) { - return {tag, state, attrs, children, dom: undefined} -} +var TYPE_MASK = 7 +var TYPE_RETAIN = 0 +var TYPE_FRAGMENT = 1 +var TYPE_KEY = 2 +var TYPE_LAYOUT = 3 +var TYPE_TEXT = 4 +var TYPE_ELEMENT = 5 +var TYPE_COMPONENT = 6 + +var FLAG_KEYED = 1 << 3 +var FLAG_USED = 1 << 4 +var FLAG_IS_REMOVE = 1 << 5 +var FLAG_HTML_ELEMENT = 1 << 6 +var FLAG_CUSTOM_ELEMENT = 1 << 7 +var FLAG_INPUT_ELEMENT = 1 << 8 +var FLAG_SELECT_ELEMENT = 1 << 9 +var FLAG_OPTION_ELEMENT = 1 << 10 +var FLAG_TEXTAREA_ELEMENT = 1 << 11 +var FLAG_IS_FILE_INPUT = 1 << 12 + +var Vnode = (mask, tag, state, attrs, children) => ({ + m: mask, + t: tag, + s: state, + a: attrs, + c: children, + // Think of this as either "data" or "DOM" - it's used for both. + d: null, +}) var selectorParser = /(?:(^|#|\.)([^#\.\[\]]+))|(\[(.+?)(?:\s*=\s*("|'|)((?:\\["'\]]|.)*?)\5)?\])/g var selectorUnescape = /\\(["'\\])/g var selectorCache = /*@__PURE__*/ new Map() -function compileSelector(selector) { - var match, tag = "div", classes = [], attrs = {}, hasAttrs = false +var compileSelector = (selector) => { + var match, tag = "div", classes = [], attrs = {}, className, hasAttrs = false while (match = selectorParser.exec(selector)) { var type = match[1], value = match[2] @@ -91,66 +134,93 @@ function compileSelector(selector) { } if (classes.length > 0) { - attrs.class = classes.join(" ") + className = classes.join(" ") } - var state = {tag, attrs: hasAttrs ? attrs : null} + var state = {t: tag, a: hasAttrs ? attrs : null, c: className} selectorCache.set(selector, state) return state } -function execSelector(selector, attrs, children) { +/* +Edit this with caution and profile every change you make. This comprises about 4% of the total +runtime overhead in benchmarks, and any reduction in performance here will immediately be felt. + +Also, it's specially designed to only allocate the bare minimum it needs to build vnodes, as part +of this optimization process. It doesn't allocate arguments except as needed to build children, it +doesn't allocate attributes except to replace them for modifications, among other things. +*/ +var m = function (selector, attrs) { + if (typeof selector !== "string" && typeof selector !== "function") { + throw new Error("The selector must be either a string or a component."); + } + + var start = 1 + var children + + if (attrs == null || typeof attrs === "object" && typeof attrs.m !== "number" && !Array.isArray(attrs)) { + start = 2 + if (arguments.length < 3 && attrs && Array.isArray(attrs.children)) { + children = attrs.children.slice() + } + } else { + attrs = null + } + + if (children == null) { + if (arguments.length === start + 1 && Array.isArray(arguments[start])) { + children = arguments[start].slice() + } else { + children = [] + while (start < arguments.length) children.push(arguments[start++]) + } + } + + // It may seem expensive to inline elements handling, but it's less expensive than you'd think. + // DOM nodes are about as commonly constructed as vnodes, but fragments are only constructed + // from JSX code (and even then, they aren't common). + + if (typeof selector !== "string") { + if (selector === m.Fragment) { + return createParentVnode(TYPE_FRAGMENT, null, null, null, children) + } else { + return Vnode(TYPE_COMPONENT, selector, null, Object.assign({children}, attrs), null) + } + } + attrs = attrs || {} var hasClassName = hasOwn.call(attrs, "className") var dynamicClass = hasClassName ? attrs.className : attrs.class var state = selectorCache.get(selector) var original = attrs - var selectorClass if (state == null) { - state = compileSelector(selector) + state = /*@__NOINLINE__*/compileSelector(selector) } - if (state.attrs != null) { - selectorClass = state.attrs.class - attrs = Object.assign({}, state.attrs, attrs) + if (state.a != null) { + attrs = Object.assign({}, state.a, attrs) } - if (dynamicClass != null || selectorClass != null) { + if (dynamicClass != null || state.c != null) { if (attrs !== original) attrs = Object.assign({}, attrs) attrs.class = dynamicClass != null - ? selectorClass != null ? `${selectorClass} ${dynamicClass}` : dynamicClass - : selectorClass + ? state.c != null ? `${state.c} ${dynamicClass}` : dynamicClass + : state.c if (hasClassName) attrs.className = null } - return Vnode(state.tag, undefined, attrs, normalizeChildren(children)) + return createParentVnode(TYPE_ELEMENT, selector, null, attrs, children) } -// Caution is advised when editing this - it's very perf-critical. It's specially designed to avoid -// allocations in the fast path, especially with fragments. -function m(selector, attrs, ...children) { - if (typeof selector !== "string" && typeof selector !== "function") { - throw new Error("The selector must be either a string or a component."); - } - - if (attrs == null || typeof attrs === "object" && attrs.tag == null && !Array.isArray(attrs)) { - children = children.length === 0 && attrs && hasOwn.call(attrs, "children") && Array.isArray(attrs.children) - ? attrs.children.slice() - : children.length === 1 && Array.isArray(children[0]) ? children[0].slice() : [...children] - } else { - children = children.length === 0 && Array.isArray(attrs) ? attrs.slice() : [attrs, ...children] - attrs = undefined - } - - if (typeof selector === "string") { - return execSelector(selector, attrs, children) - } else if (selector === m.Fragment) { - return Vnode(FRAGMENT, undefined, undefined, normalizeChildren(children)) - } else { - return Vnode(selector, undefined, Object.assign({children}, attrs), undefined) - } -} +m.TYPE_MASK = TYPE_MASK +m.TYPE_RETAIN = TYPE_RETAIN +m.TYPE_FRAGMENT = TYPE_FRAGMENT +m.TYPE_KEY = TYPE_KEY +m.TYPE_LAYOUT = TYPE_LAYOUT +m.TYPE_TEXT = TYPE_TEXT +m.TYPE_ELEMENT = TYPE_ELEMENT +m.TYPE_COMPONENT = TYPE_COMPONENT // Simple and sweet. Also useful for idioms like `onfoo: m.capture` to drop events without // redrawing. @@ -160,34 +230,43 @@ m.capture = (ev) => { return false } -m.retain = () => Vnode(RETAIN, undefined, undefined, undefined) +m.retain = () => Vnode(TYPE_RETAIN, null, null, null, null) -m.layout = (f) => Vnode(LAYOUT, f, undefined, undefined) +m.layout = (onCreate, onUpdate) => { + if (onCreate != null && typeof onCreate !== "function") { + throw new TypeError("`onCreate` callback must be a function if provided") + } + if (onUpdate != null && typeof onUpdate !== "function") { + throw new TypeError("`onUpdate` callback must be a function if provided") + } + return Vnode(TYPE_LAYOUT, null, onCreate, onUpdate, null) +} m.Fragment = (attrs) => attrs.children m.key = (key, ...children) => - Vnode(KEY, key, undefined, normalizeChildren( + createParentVnode(TYPE_KEY, key, null, null, children.length === 1 && Array.isArray(children[0]) ? children[0].slice() : [...children] - )) + ) m.normalize = (node) => { if (node == null || typeof node === "boolean") return null - if (typeof node !== "object") return Vnode(TEXT, String(node), undefined, undefined) - if (Array.isArray(node)) return Vnode(FRAGMENT, undefined, undefined, normalizeChildren(node.slice())) + if (typeof node !== "object") return Vnode(TYPE_TEXT, null, String(node), null, null) + if (Array.isArray(node)) return createParentVnode(TYPE_FRAGMENT, null, null, null, node.slice()) return node } -function normalizeChildren(input) { +var createParentVnode = (mask, tag, state, attrs, input) => { if (input.length) { input[0] = m.normalize(input[0]) - var isKeyed = input[0] != null && input[0].tag === KEY + var isKeyed = input[0] != null && (input[0].m & TYPE_MASK) === TYPE_KEY var keys = new Set() + mask |= -isKeyed & FLAG_KEYED // Note: this is a *very* perf-sensitive check. // Fun fact: merging the loop like this is somehow faster than splitting // it, noticeably so. for (var i = 1; i < input.length; i++) { input[i] = m.normalize(input[i]) - if ((input[i] != null && input[i].tag === KEY) !== isKeyed) { + if ((input[i] != null && (input[i].m & TYPE_MASK) === TYPE_KEY) !== isKeyed) { throw new TypeError( isKeyed ? "In fragments, vnodes must either all have keys or none have keys. You may wish to consider using an explicit empty key vnode, `m.key()`, instead of a hole." @@ -195,17 +274,18 @@ function normalizeChildren(input) { ) } if (isKeyed) { - if (keys.has(input[i].state)) { - throw new TypeError(`Duplicate key detected: ${input[i].state}`) + if (keys.has(input[i].t)) { + throw new TypeError(`Duplicate key detected: ${input[i].t}`) } - keys.add(input[i].state) + keys.add(input[i].t) } } } - return input + return Vnode(mask, tag, state, attrs, input) } var xlinkNs = "http://www.w3.org/1999/xlink" +var htmlNs = "http://www.w3.org/1999/xhtml" var nameSpace = { svg: "http://www.w3.org/2000/svg", math: "http://www.w3.org/1998/Math/MathML" @@ -216,422 +296,602 @@ var currentRedraw var currentParent var currentRefNode var currentNamespace +var currentDocument -// Used for tainting nodes, to assert they aren't being reused. -var vnodeAccepted = new WeakSet() - -function assertVnodeIsNew(vnode) { - if (vnodeAccepted.has(vnode)) { - throw new TypeError("Vnodes must not be reused") +var insertAfterCurrentRefNode = (child) => { + if (currentRefNode) { + currentRefNode.after(currentRefNode = child) + } else { + currentParent.prepend(currentRefNode = child) } - vnodeAccepted.add(vnode) } -//create -function createNodes(vnodes, start) { - for (var i = start; i < vnodes.length; i++) createNode(vnodes[i]) -} -function createNode(vnode) { - if (vnode != null) { - assertVnodeIsNew(vnode) - innerCreateNode(vnode) +//update +var moveToPosition = (vnode) => { + var type + while ((type = vnode.m & TYPE_MASK) === TYPE_COMPONENT) { + if (!(vnode = vnode.c)) return } -} -function innerCreateNode(vnode) { - switch (vnode.tag) { - case RETAIN: throw new Error("No node present to retain with `m.retain()`") - case LAYOUT: return createLayout(vnode) - case TEXT: return createText(vnode) - case KEY: - case FRAGMENT: return createNodes(vnode.children, 0) - } - if (typeof vnode.tag === "string") createElement(vnode) - else createComponent(vnode) -} -function createLayout(vnode) { - vnode.dom = new AbortController() - currentHooks.push({v: vnode, p: currentParent, i: true}) -} -function createText(vnode) { - insertAfterCurrentRefNode(vnode.dom = currentParent.ownerDocument.createTextNode(vnode.state)) -} -function createElement(vnode) { - var tag = vnode.tag - var attrs = vnode.attrs - var is = attrs && attrs.is - var prevParent = currentParent - var document = currentParent.ownerDocument - var prevNamespace = currentNamespace - var ns = attrs && attrs.xmlns || nameSpace[tag] || prevNamespace - - var element = vnode.dom = ns ? - is ? document.createElementNS(ns, tag, {is: is}) : document.createElementNS(ns, tag) : - is ? document.createElement(tag, {is: is}) : document.createElement(tag) - - insertAfterCurrentRefNode(element) - - currentParent = element - currentRefNode = null - currentNamespace = ns - - try { - if (attrs != null) { - setAttrs(vnode, attrs) - } - - if (!maybeSetContentEditable(vnode)) { - if (vnode.children) { - createNodes(vnode.children, 0) - if (vnode.tag === "select" && attrs != null) setLateSelectAttrs(vnode, attrs) + if ((1 << TYPE_FRAGMENT | 1 << TYPE_KEY) & 1 << type) { + vnode.c.forEach(moveToPosition) + } else { + insertAfterCurrentRefNode(vnode.d) + } +} + +var updateFragment = (old, vnode) => { + // Here's the logic: + // - If `old` or `vnode` is `null`, common length is 0 by default, and it falls back to an + // unkeyed empty fragment. + // - If `old` and `vnode` differ in their keyedness, their children must be wholly replaced. + // - If `old` and `vnode` are both non-keyed, patch their children linearly. + // - If `old` and `vnode` are both keyed, patch their children using a map. + var mask = vnode != null ? vnode.m : 0 + var newLength = vnode != null ? vnode.c.length : 0 + var oldMask = old != null ? old.m : 0 + var oldLength = old != null ? old.c.length : 0 + var commonLength = oldLength < newLength ? oldLength : newLength + if ((oldMask ^ mask) & FLAG_KEYED) { // XOR is equivalent to bit inequality + // Key state changed. Replace the subtree + commonLength = 0 + mask &= ~FLAG_KEYED + } + if (!(mask & FLAG_KEYED)) { + // Not keyed. Patch the common prefix, remove the extra in the old, and create the + // extra in the new. + // + // Can't just take the max of both, because out-of-bounds accesses both disrupts + // optimizations and is just generally slower. + // + // Note: if either `vnode` or `old` is `null`, the common length and its own length are + // both zero, so it can't actually throw. + for (var i = 0; i < commonLength; i++) updateNode(old.c[i], vnode.c[i]) + for (var i = commonLength; i < oldLength; i++) updateNode(old.c[i], null) + for (var i = commonLength; i < newLength; i++) updateNode(null, vnode.c[i]) + } else { + // Keyed. I take a pretty straightforward approach here to keep it simple: + // 1. Build a map from old map to old vnode. + // 2. Walk the new vnodes, adding what's missing and patching what's in the old. + // 3. Remove from the old map the keys in the new vnodes, leaving only the keys that + // were removed this run. + // 4. Remove the remaining nodes in the old map that aren't in the new map. Since the + // new keys were already deleted, this is just a simple map iteration. + + // Note: if either `vnode` or `old` is `null`, they won't get here. The default mask is + // zero, and that causes keyed state to differ and thus a forced linear diff per above. + + var oldMap = new Map() + for (var p of old.c) oldMap.set(p.t, p) + + for (var n of vnode.c) { + var p = oldMap.get(n.t) + if (p == null) { + updateFragment(null, n) + } else { + oldMap.delete(n.t) + var prev = currentRefNode + try { + moveToPosition(p) + } finally { + currentRefNode = prev + } + updateFragment(p, n) } } - } finally { - currentRefNode = element - currentParent = prevParent - currentNamespace = ns + + oldMap.forEach((p) => updateNode(p, null)) } } -function createComponent(vnode) { - var tree = (vnode.state = vnode.tag)(vnode.attrs) - if (typeof tree === "function") tree = (vnode.state = tree)(vnode.attrs) - if (tree === vnode) throw new Error("A view cannot return the vnode it received as argument") - createNode(vnode.children = m.normalize(tree)) -} -//update -function updateNodes(old, vnodes) { - if (old == null || old.length === 0) createNodes(vnodes, 0) - else if (vnodes == null || vnodes.length === 0) removeNodes(old, 0) - else { - var isOldKeyed = old[0] != null && old[0].tag === KEY - var isKeyed = vnodes[0] != null && vnodes[0].tag === KEY - if (isOldKeyed !== isKeyed) { - // Key state changed. Replace the subtree - removeNodes(old, 0) - createNodes(vnodes, 0) - } else if (!isKeyed) { - // Not keyed. Patch the common prefix, remove the extra in the old, and create the - // extra in the new. - // - // Can't just take the max of both, because out-of-bounds accesses both disrupts - // optimizations and is just generally slower. - var commonLength = old.length < vnodes.length ? old.length : vnodes.length - for (var i = 0; i < commonLength; i++) { - updateNode(old[i], vnodes[i]) - } - removeNodes(old, commonLength) - createNodes(vnodes, commonLength) - } else { - // Keyed. I take a pretty straightforward approach here to keep it simple: - // 1. Build a map from old map to old vnode. - // 2. Walk the new vnodes, adding what's missing and patching what's in the old. - // 3. Remove from the old map the keys in the new vnodes, leaving only the keys that - // were removed this run. - // 4. Remove the remaining nodes in the old map that aren't in the new map. Since the - // new keys were already deleted, this is just a simple map iteration. - - var oldMap = new Map() - for (var p of old) oldMap.set(p.state, p) - - for (var n of vnodes) { - var p = oldMap.get(n.state) - if (p == null) { - createNodes(n.children, 0) - } else { - oldMap.delete(n.state) - var prev = currentRefNode - try { - moveToPosition(p) - } finally { - currentRefNode = prev - } - updateNodes(p.children, n.children) +var updateNode = (old, vnode) => { + var type + if (old == null) { + if (vnode == null) return + if (vnode.m & FLAG_USED) { + throw new TypeError("Vnodes must not be reused") + } + type = vnode.m & TYPE_MASK + vnode.m |= FLAG_USED + if (type === TYPE_RETAIN) { + throw new Error("No node present to retain with `m.retain()`") + } + } else { + type = old.m & TYPE_MASK + + if (vnode == null) { + if (type === TYPE_COMPONENT) { + updateNode(old.c, null) + } else if (type === TYPE_LAYOUT) { + try { + old.d.abort() + } catch (e) { + console.error(e) } + } else { + if ((1 << TYPE_TEXT | 1 << TYPE_ELEMENT) & 1 << type) old.d.remove() + if (type !== TYPE_TEXT) updateFragment(old, null) } + return + } - oldMap.forEach(removeNode) + if (vnode.m & FLAG_USED) { + throw new TypeError("Vnodes must not be reused") } - } -} -function updateNode(old, vnode) { - if (old == null) { - createNode(vnode) - } else if (vnode == null) { - removeNode(old) - } else { - assertVnodeIsNew(vnode) - if (vnode.tag === RETAIN) { + + var newType = vnode.m & TYPE_MASK + + if (newType === TYPE_RETAIN) { // If it's a retain node, transmute it into the node it's retaining. Makes it much easier // to implement and work with. // // Note: this key list *must* be complete. - vnode.tag = old.tag - vnode.state = old.state - vnode.attrs = old.attrs - vnode.children = old.children - vnode.dom = old.dom - } else if (vnode.tag === old.tag && (vnode.tag !== KEY || vnode.state === old.state)) { - switch (vnode.tag) { - case LAYOUT: return updateLayout(old, vnode) - case TEXT: return updateText(old, vnode) - case KEY: - case FRAGMENT: return updateNodes(old.children, vnode.children) - } - if (typeof vnode.tag === "string") updateElement(old, vnode) - else updateComponent(old, vnode) + vnode.m = old.m + vnode.t = old.t + vnode.s = old.s + vnode.a = old.a + vnode.c = old.c + vnode.d = old.d + return } - else { - removeNode(old) - innerCreateNode(vnode) + + if (type === newType && vnode.t === old.t) { + vnode.m = old.m & ~FLAG_KEYED | vnode.m & FLAG_KEYED + } else { + updateNode(old, null) + type = newType + old = null } } + + updateNodeDispatch[type](old, vnode) } -function updateLayout(old, vnode) { - vnode.dom = old.dom - currentHooks.push({v: vnode, p: currentParent, i: false}) + +var updateLayout = (old, vnode) => { + vnode.d = old == null ? new AbortController() : old.d + currentHooks.push(old == null ? vnode.s : vnode.a, currentParent, vnode.d.signal) } -function updateText(old, vnode) { - if (`${old.state}` !== `${vnode.state}`) old.dom.nodeValue = vnode.state - vnode.dom = currentRefNode = old.dom + +var updateText = (old, vnode) => { + if (old == null) { + insertAfterCurrentRefNode(vnode.d = currentDocument.createTextNode(vnode.s)) + } else { + if (`${old.s}` !== `${vnode.s}`) old.d.nodeValue = vnode.s + vnode.d = currentRefNode = old.d + } } -function updateElement(old, vnode) { - vnode.state = old.state + +var updateElement = (old, vnode) => { var prevParent = currentParent var prevNamespace = currentNamespace - var namespace = (currentParent = vnode.dom = old.dom).namespaceURI + var mask = vnode.m + var attrs = vnode.a + var element, eventDict, oldAttrs - currentNamespace = namespace === "http://www.w3.org/1999/xhtml" ? null : namespace - currentRefNode = null - try { - updateAttrs(vnode, old.attrs, vnode.attrs) - if (!maybeSetContentEditable(vnode)) { - updateNodes(old.children, vnode.children) + if (old == null) { + var entry = selectorCache.get(vnode.t) + var tag = entry ? entry.t : vnode.t + var is = attrs && attrs.is + var ns = attrs && attrs.xmlns || nameSpace[tag] || prevNamespace + var opts = is ? {is} : null + + insertAfterCurrentRefNode(element = vnode.d = ( + ns + ? currentDocument.createElementNS(ns, tag, opts) + : currentDocument.createElement(tag, opts) + )) + + if (ns == null) { + // Doing it this way since it doesn't seem Terser is smart enough to optimize the `if` with + // every branch doing `a |= value` for differing `value`s to a ternary. It *is* smart + // enough to inline the constants, and the following pass optimizes the rest to just + // integers. + // + // Doing a simple constant-returning ternary also makes it easier for engines to emit the + // right code. + /* eslint-disable indent */ + vnode.m = mask |= ( + is || tag.includes("-") + ? FLAG_HTML_ELEMENT | FLAG_CUSTOM_ELEMENT + : (tag = tag.toUpperCase(), ( + tag === "INPUT" ? FLAG_HTML_ELEMENT | FLAG_INPUT_ELEMENT + : tag === "SELECT" ? FLAG_HTML_ELEMENT | FLAG_SELECT_ELEMENT + : tag === "OPTION" ? FLAG_HTML_ELEMENT | FLAG_OPTION_ELEMENT + : tag === "TEXTAREA" ? FLAG_HTML_ELEMENT | FLAG_TEXTAREA_ELEMENT + : FLAG_HTML_ELEMENT + )) + ) + /* eslint-enable indent */ } - } finally { - currentParent = prevParent - currentRefNode = vnode.dom - currentNamespace = prevNamespace - } -} -function updateComponent(old, vnode) { - vnode.children = m.normalize((vnode.state = old.state)(vnode.attrs, old.attrs)) - if (vnode.children === vnode) throw new Error("A view cannot return the vnode it received as argument") - updateNode(old.children, vnode.children) -} -function insertAfterCurrentRefNode(child) { - if (currentRefNode) { - currentRefNode.after(currentRefNode = child) + currentParent = element + currentNamespace = ns } else { - currentParent.prepend(currentRefNode = child) + eventDict = vnode.s = old.s + oldAttrs = old.a + currentNamespace = (currentParent = element = vnode.d = old.d).namespaceURI + if (currentNamespace === htmlNs) currentNamespace = null } -} -function moveToPosition(vnode) { - while (typeof vnode.tag === "function") { - vnode = vnode.children - if (!vnode) return - } - if (vnode.tag === FRAGMENT || vnode.tag === KEY) { - vnode.children.forEach(moveToPosition) - } else { - insertAfterCurrentRefNode(vnode.dom) - } -} + currentRefNode = null -function maybeSetContentEditable(vnode) { - if (vnode.attrs == null || ( - vnode.attrs.contenteditable == null && // attribute - vnode.attrs.contentEditable == null // property - )) return false - var children = vnode.children - if (children != null && children.length !== 0) throw new Error("Child node of a contenteditable must be trusted.") - return true -} + try { + if (oldAttrs != null && oldAttrs === attrs) { + throw new Error("Attributes object cannot be reused.") + } -//remove -function removeNodes(vnodes, start) { - for (var i = start; i < vnodes.length; i++) removeNode(vnodes[i]) -} -function removeNode(vnode) { - if (vnode != null) { - if (typeof vnode.tag === "function") { - removeNode(vnode.children) - } else if (vnode.tag === LAYOUT) { - try { - vnode.dom.abort() - } catch (e) { - console.error(e) + if (attrs != null) { + // The DOM does things to inputs based on the value, so it needs set first. + // See: https://github.com/MithrilJS/mithril.js/issues/2622 + if (mask & FLAG_INPUT_ELEMENT && attrs.type != null) { + if (attrs.type === "file") mask |= FLAG_IS_FILE_INPUT + element.type = attrs.type } - } else { - if (vnode.children != null) { - removeNodes(vnode.children, 0) + + for (var key in attrs) { + eventDict = setAttr(eventDict, element, mask, key, oldAttrs, attrs) } + } - if (vnode.dom != null) vnode.dom.remove() + for (var key in oldAttrs) { + mask |= FLAG_IS_REMOVE + eventDict = setAttr(eventDict, element, mask, key, oldAttrs, attrs) } - } -} -//attrs -function setAttrs(vnode, attrs) { - // The DOM does things to inputs based on the value, so it needs set first. - // See: https://github.com/MithrilJS/mithril.js/issues/2622 - if (vnode.tag === "input" && attrs.type != null) vnode.dom.type = attrs.type - var isFileInput = attrs != null && vnode.tag === "input" && attrs.type === "file" - for (var key in attrs) { - setAttr(vnode, key, null, attrs[key], isFileInput) - } -} -function setAttr(vnode, key, old, value, isFileInput) { - if (value == null || key === "is" || key === "children" || (old === value && !isFormAttribute(vnode, key)) && typeof value !== "object" || key === "type" && vnode.tag === "input") return - if (key.startsWith("on")) updateEvent(vnode, key, value) - else if (key.startsWith("xlink:")) vnode.dom.setAttributeNS(xlinkNs, key.slice(6), value) - else if (key === "style") updateStyle(vnode.dom, old, value) - else if (hasPropertyKey(vnode, key)) { - if (key === "value") { - // Only do the coercion if we're actually going to check the value. - /* eslint-disable no-implicit-coercion */ - switch (vnode.tag) { - //setting input[value] to same value by typing on focused element moves cursor to end in Chrome - //setting input[type=file][value] to same value causes an error to be generated if it's non-empty - case "input": - case "textarea": - if (vnode.dom.value === "" + value && (isFileInput || vnode.dom === vnode.dom.ownerDocument.activeElement)) return - //setting input[type=file][value] to different value is an error if it's non-empty - // Not ideal, but it at least works around the most common source of uncaught exceptions for now. - if (isFileInput && "" + value !== "") { console.error("`value` is read-only on file inputs!"); return } - break - //setting select[value] or option[value] to same value while having select open blinks select dropdown in Chrome - case "select": - case "option": - if (old !== null && vnode.dom.value === "" + value) return + updateFragment(old, vnode) + + if (mask & FLAG_SELECT_ELEMENT && old == null) { + // This does exactly what I want, so I'm reusing it to save some code + var normalized = getStyleKey(attrs, "value") + if ("value" in attrs) { + if (normalized === null) { + if (element.selectedIndex >= 0) { + element.value = null + } + } else { + if (element.selectedIndex < 0 || element.value !== normalized) { + element.value = normalized + } + } } - /* eslint-enable no-implicit-coercion */ - } - vnode.dom[key] = value - } else if (value === false) { - vnode.dom.removeAttribute(key) - } else { - vnode.dom.setAttribute(key, value === true ? "" : value) - } -} -function removeAttr(vnode, key, old) { - if (old == null || key === "is" || key === "children") return - if (key.startsWith("on")) updateEvent(vnode, key, undefined) - else if (key.startsWith("xlink:")) vnode.dom.removeAttributeNS(xlinkNs, key.slice(6)) - else if (key === "style") updateStyle(vnode.dom, old, null) - else if ( - hasPropertyKey(vnode, key) - && key !== "class" - && key !== "title" // creates "null" as title - && !(key === "value" && ( - vnode.tag === "option" - || vnode.tag === "select" && vnode.dom.selectedIndex === -1 && vnode.dom === vnode.dom.ownerDocument.activeElement - )) - && !(vnode.tag === "input" && key === "type") - ) { - vnode.dom[key] = null - } else { - if (old !== false) vnode.dom.removeAttribute(key) - } -} -function setLateSelectAttrs(vnode, attrs) { - if ("value" in attrs) { - if(attrs.value === null) { - if (vnode.dom.selectedIndex !== -1) vnode.dom.value = null - } else { - var normalized = "" + attrs.value // eslint-disable-line no-implicit-coercion - if (vnode.dom.value !== normalized || vnode.dom.selectedIndex === -1) { - vnode.dom.value = normalized + + if ("selectedIndex" in attrs) { + element.selectedIndex = attrs.selectedIndex } } + } finally { + vnode.s = eventDict + currentParent = prevParent + currentRefNode = element + currentNamespace = prevNamespace } - if ("selectedIndex" in attrs) setAttr(vnode, "selectedIndex", null, attrs.selectedIndex, undefined) } -function updateAttrs(vnode, old, attrs) { - if (old && old === attrs) { - throw new Error("Attributes object cannot be reused.") - } - if (attrs != null) { - // If you assign an input type that is not supported by IE 11 with an assignment expression, an error will occur. - // - // Also, the DOM does things to inputs based on the value, so it needs set first. - // See: https://github.com/MithrilJS/mithril.js/issues/2622 - if (vnode.tag === "input" && attrs.type != null) vnode.dom.setAttribute("type", attrs.type) - var isFileInput = vnode.tag === "input" && attrs.type === "file" - for (var key in attrs) { - setAttr(vnode, key, old && old[key], attrs[key], isFileInput) + +var updateComponent = (old, vnode) => { + var attrs = vnode.a + var context = {redraw: currentRedraw} + var tree, context, oldInstance, oldAttrs + rendered: { + if (old != null) { + tree = old.s + oldInstance = old.c + oldAttrs = old.a + } else if (typeof (tree = (vnode.s = vnode.t)(attrs, null, context)) !== "function") { + break rendered } + tree = (vnode.s = tree)(attrs, oldAttrs, context) } - var val - if (old != null) { - for (var key in old) { - if (((val = old[key]) != null) && (attrs == null || attrs[key] == null)) { - removeAttr(vnode, key, val) - } - } + if (tree === vnode) { + throw new Error("A view cannot return the vnode it received as argument") } + updateNode(oldInstance, vnode.c = m.normalize(tree)) } -// Try to avoid a few browser bugs on normal elements. -// var propertyMayBeBugged = /^(?:href|list|form|width|height|type)$/ -var propertyMayBeBugged = /^(?:href|list|form|width|height)$/ -function isFormAttribute(vnode, attr) { - return attr === "value" || attr === "checked" || attr === "selectedIndex" || - attr === "selected" && vnode.dom === vnode.dom.ownerDocument.activeElement || - vnode.tag === "option" && vnode.dom.parentNode === vnode.dom.ownerDocument.activeElement -} -function hasPropertyKey(vnode, key) { - // Filter out namespaced keys - return currentNamespace == null && ( - // If it's a custom element, just keep it. - vnode.tag.indexOf("-") > -1 || vnode.attrs != null && vnode.attrs.is || - !propertyMayBeBugged.test(key) - // Defer the property check until *after* we check everything. - ) && key in vnode.dom + +// Replaces an otherwise necessary `switch`. +var updateNodeDispatch = [ + null, + updateFragment, + updateFragment, + updateLayout, + updateText, + updateElement, + updateComponent, +] + +//attrs + +/* eslint-disable no-unused-vars */ +var ASCII_COLON = 0x3A +var ASCII_LOWER_A = 0x61 +var ASCII_LOWER_B = 0x62 +var ASCII_LOWER_C = 0x63 +var ASCII_LOWER_D = 0x64 +var ASCII_LOWER_E = 0x65 +var ASCII_LOWER_F = 0x66 +var ASCII_LOWER_G = 0x67 +var ASCII_LOWER_H = 0x68 +var ASCII_LOWER_I = 0x69 +var ASCII_LOWER_J = 0x6A +var ASCII_LOWER_K = 0x6B +var ASCII_LOWER_L = 0x6C +var ASCII_LOWER_M = 0x6D +var ASCII_LOWER_N = 0x6E +var ASCII_LOWER_O = 0x6F +var ASCII_LOWER_P = 0x70 +var ASCII_LOWER_Q = 0x71 +var ASCII_LOWER_R = 0x72 +var ASCII_LOWER_S = 0x73 +var ASCII_LOWER_T = 0x74 +var ASCII_LOWER_U = 0x75 +var ASCII_LOWER_V = 0x76 +var ASCII_LOWER_W = 0x77 +var ASCII_LOWER_X = 0x78 +var ASCII_LOWER_Y = 0x79 +var ASCII_LOWER_Z = 0x7A +/* eslint-enable no-unused-vars */ + +var getPropKey = (host, key) => { + if (host != null && hasOwn.call(host, key)) { + var value = host[key] + if (value !== false && value != null) return value + } + return null +} + +var getStyleKey = (host, key) => { + if (host != null && hasOwn.call(host, key)) { + var value = host[key] + if (value !== false && value != null) return `${value}` + } + return null } -//style var uppercaseRegex = /[A-Z]/g -function toLowerCase(capital) { return "-" + capital.toLowerCase() } -function normalizeKey(key) { - return key[0] === "-" && key[1] === "-" ? key : + +var toLowerCase = (capital) => "-" + capital.toLowerCase() + +var normalizeKey = (key) => ( + key.startsWith("--") ? key : key === "cssFloat" ? "float" : key.replace(uppercaseRegex, toLowerCase) -} -function updateStyle(element, old, style) { - if (old === style) { - // Styles are equivalent, do nothing. - } else if (style == null) { - // New style is missing, just clear it. - element.style = "" - } else if (typeof style !== "object") { - // New style is a string, let engine deal with patching. - element.style = style - } else if (old == null || typeof old !== "object") { - // `old` is missing or a string, `style` is an object. - element.style.cssText = "" - // Add new style properties - for (var key in style) { - var value = style[key] - if (value != null) element.style.setProperty(normalizeKey(key), String(value)) - } - } else { - // Both old & new are (different) objects. - // Update style properties that have changed - for (var key in style) { - var value = style[key] - if (value != null && (value = String(value)) !== String(old[key])) { - element.style.setProperty(normalizeKey(key), value) +) + +var setStyle = (style, old, value, add) => { + for (var propName of Object.keys(value)) { + var propValue = getStyleKey(value, propName) + if (propValue !== null) { + var oldValue = getStyleKey(old, propName) + if (add) { + if (propValue !== oldValue) style.setProperty(normalizeKey(propName), propValue) + } else { + if (oldValue === null) style.removeProperty(normalizeKey(propName)) } } - // Remove style properties that no longer exist - for (var key in old) { - if (old[key] != null && style[key] == null) { - element.style.removeProperty(normalizeKey(key)) + } +} + +/* +Edit this with extreme caution, and profile any change you make. + +Not only is this itself a hot spot (it comprises about 3-5% of runtime overhead), but the way it's +compiled can even sometimes have knock-on performance impacts elsewhere. Per some Turbolizer +experiments, this will generate around 10-15 KiB of assembly in its final optimized form. + +Some of the optimizations it does: + +- For pairs of attributes, I pack them into two integers so I can compare them in + parallel. +- I reuse the same character loads for `xlink:*` and `on*` to check for other nodes. I do not reuse + the last load, as the first 2 characters is usually enough just on its own to know if a special + attribute name is matchable. +- For small attribute names (4 characters or less), the code handles them in full, with no full + string comparison. +- The events object is read prior to calling this, while the rest of the vnode is already in the + CPU cache, and it's just passed as an argument. This ensures it's always in easy access, only + a few cycles of latency away, without becoming too costly for vnodes without events. +- I fuse all the conditions, `hasOwn` and existence checks, and all the add/remove logic into just + this, to reduce startup overhead and keep outer loop code size down. +- I use a lot of labels to reuse as much code as possible, and thus more ICs, to make optimization + easier and better-informed. +- Bit flags are used extensively here to merge as many comparisons as possible. This function is + actually the real reason why I'm using bit flags for stuff like `` in the + first place - it moves the check to just the create flow where it's only done once. +*/ +var setAttr = (eventDict, element, mask, key, old, attrs) => { + var newValue = getPropKey(attrs, key) + var oldValue = getPropKey(old, key) + + if (mask & FLAG_IS_REMOVE && newValue !== null) return eventDict + + forceSetAttribute: { + forceTryProperty: { + skipValueDiff: { + if (key.length > 1) { + var pair1 = key.charCodeAt(0) | key.charCodeAt(1) << 16 + + if (key.length === 2 && pair1 === (ASCII_LOWER_I | ASCII_LOWER_S << 16)) { + return eventDict + } else if (pair1 === (ASCII_LOWER_O | ASCII_LOWER_N << 16)) { + if (newValue === oldValue) return eventDict + // Update the event + if (typeof newValue === "function") { + if (typeof oldValue !== "function") { + if (eventDict == null) eventDict = new EventDict() + element.addEventListener(key.slice(2), eventDict, false) + } + // Save this, so the current redraw is correctly tracked. + eventDict._ = currentRedraw + eventDict.set(key, newValue) + } else if (typeof oldValue === "function") { + element.removeEventListener(key.slice(2), eventDict, false) + eventDict.delete(key) + } + return eventDict + } else if (key.length > 3) { + var pair2 = key.charCodeAt(2) | key.charCodeAt(3) << 16 + if ( + key.length > 6 && + pair1 === (ASCII_LOWER_X | ASCII_LOWER_L << 16) && + pair2 === (ASCII_LOWER_I | ASCII_LOWER_N << 16) && + (key.charCodeAt(4) | key.charCodeAt(5) << 16) === (ASCII_LOWER_K | ASCII_COLON << 16) + ) { + key = key.slice(6) + if (newValue !== null) { + element.setAttributeNS(xlinkNs, key, newValue) + } else { + element.removeAttributeNS(xlinkNs, key) + } + return eventDict + } else if (key.length === 4) { + if ( + pair1 === (ASCII_LOWER_T | ASCII_LOWER_Y << 16) && + pair2 === (ASCII_LOWER_P | ASCII_LOWER_E << 16) + ) { + if (!(mask & FLAG_INPUT_ELEMENT)) break skipValueDiff + if (newValue === null) break forceSetAttribute + break forceTryProperty + } else if ( + // Try to avoid a few browser bugs on normal elements. + pair1 === (ASCII_LOWER_H | ASCII_LOWER_R << 16) && pair2 === (ASCII_LOWER_E | ASCII_LOWER_F << 16) || + pair1 === (ASCII_LOWER_L | ASCII_LOWER_I << 16) && pair2 === (ASCII_LOWER_S | ASCII_LOWER_T << 16) || + pair1 === (ASCII_LOWER_F | ASCII_LOWER_O << 16) && pair2 === (ASCII_LOWER_R | ASCII_LOWER_M << 16) + ) { + // If it's a custom element, just keep it. Otherwise, force the attribute + // to be set. + if (!(mask & FLAG_CUSTOM_ELEMENT)) { + break forceSetAttribute + } + } + } else if (key.length > 4) { + switch (key) { + case "children": + return eventDict + + case "class": + case "className": + case "title": + if (newValue === null) break forceSetAttribute + break forceTryProperty + + case "value": + if ( + // Filter out non-HTML keys and custom elements + (mask & (FLAG_HTML_ELEMENT | FLAG_CUSTOM_ELEMENT)) !== FLAG_HTML_ELEMENT || + !(key in element) + ) { + break + } + + if (newValue === null) { + if (mask & (FLAG_OPTION_ELEMENT | FLAG_SELECT_ELEMENT)) { + break forceSetAttribute + } else { + break forceTryProperty + } + } + + if (!(mask & (FLAG_INPUT_ELEMENT | FLAG_TEXTAREA_ELEMENT | FLAG_SELECT_ELEMENT | FLAG_OPTION_ELEMENT))) { + break + } + + // It's always stringified, so it's okay to always coerce + if (element.value === (newValue = `${newValue}`)) { + // Setting `` to the same value causes an + // error to be generated if it's non-empty + if (mask & FLAG_IS_FILE_INPUT) return eventDict + // Setting `` to the same value by typing on focused + // element moves cursor to end in Chrome + if (mask & (FLAG_INPUT_ELEMENT | FLAG_TEXTAREA_ELEMENT)) { + if (element === currentDocument.activeElement) return eventDict + } else { + if (oldValue != null && oldValue !== false) return eventDict + } + } + + if (mask & FLAG_IS_FILE_INPUT) { + //setting input[type=file][value] to different value is an error if it's non-empty + // Not ideal, but it at least works around the most common source of uncaught exceptions for now. + if (newValue !== "") { + console.error("File input `value` attributes must either mirror the current value or be set to the empty string (to reset).") + return eventDict + } + } + + break forceTryProperty + + case "style": + if (oldValue === newValue) { + // Styles are equivalent, do nothing. + } else if (newValue === null) { + // New style is missing, just clear it. + element.style = "" + } else if (typeof newValue !== "object") { + // New style is a string, let engine deal with patching. + element.style = newValue + } else if (oldValue === null || typeof oldValue !== "object") { + // `old` is missing or a string, `style` is an object. + element.style = "" + // Add new style properties + setStyle(element.style, null, newValue, true) + } else { + // Both old & new are (different) objects, or `old` is missing. + // Update style properties that have changed, or add new style properties + setStyle(element.style, oldValue, newValue, true) + // Remove style properties that no longer exist + setStyle(element.style, newValue, oldValue, false) + } + return eventDict + + case "selected": + var active = currentDocument.activeElement + if ( + element === active || + mask & FLAG_OPTION_ELEMENT && element.parentNode === active + ) { + break + } + // falls through + + case "checked": + case "selectedIndex": + break skipValueDiff + + // Try to avoid a few browser bugs on normal elements. + case "width": + case "height": + // If it's a custom element, just keep it. Otherwise, force the attribute + // to be set. + if (!(mask & FLAG_CUSTOM_ELEMENT)) { + break forceSetAttribute + } + } + } + } + } + + if (newValue !== null && typeof newValue !== "object" && oldValue === newValue) return + } + + // Filter out namespaced keys + if (!(mask & FLAG_HTML_ELEMENT)) { + break forceSetAttribute } } + + // Filter out namespaced keys + // Defer the property check until *after* we check everything. + if (key in element) { + element[key] = newValue + return eventDict + } } + + if (newValue === null) { + if (oldValue !== null) element.removeAttribute(key) + } else { + element.setAttribute(key, newValue === true ? "" : newValue) + } + + return eventDict } // Here's an explanation of how this works: @@ -644,47 +904,18 @@ function updateStyle(element, old, style) { // propagation. Instead of that, we hijack it to control implicit redrawing, and let users // return a promise that resolves to it. class EventDict extends Map { - constructor() { - super() - // Save this, so the current redraw is correctly tracked. - this._ = currentRedraw - } - handleEvent(ev) { + async handleEvent(ev) { var handler = this.get(`on${ev.type}`) if (typeof handler === "function") { var result = handler.call(ev.currentTarget, ev) - if (result !== false) { - if (result && typeof result.then === "function") { - Promise.resolve(result).then((value) => { - if (value !== false) (0, this._)() - }) - } else { - (0, this._)() - } - } + if (result === false) return + if (result && typeof result.then === "function" && (await result) === false) return + (0, this._)() } } } //event -function updateEvent(vnode, key, value) { - if (vnode.state != null) { - vnode.state._ = currentRedraw - var prev = vnode.state.get(key) - if (prev === value) return - if (typeof value === "function") { - if (prev == null) vnode.dom.addEventListener(key.slice(2), vnode.state, false) - vnode.state.set(key, value) - } else { - if (prev != null) vnode.dom.removeEventListener(key.slice(2), vnode.state, false) - vnode.state.delete(key) - } - } else if (typeof value === "function") { - vnode.state = new EventDict() - vnode.dom.addEventListener(key.slice(2), vnode.state, false) - vnode.state.set(key, value) - } -} var currentlyRendering = [] @@ -702,24 +933,31 @@ m.render = (dom, vnodes, redraw) => { var prevParent = currentParent var prevRefNode = currentRefNode var prevNamespace = currentNamespace + var prevDocument = currentDocument var hooks = currentHooks = [] try { currentlyRendering.push(currentParent = dom) - currentRedraw = typeof redraw === "function" ? redraw : undefined + currentRedraw = typeof redraw === "function" ? redraw : null currentRefNode = null - currentNamespace = namespace === "http://www.w3.org/1999/xhtml" ? null : namespace + currentNamespace = namespace === htmlNs ? null : namespace + currentDocument = dom.ownerDocument // First time rendering into a node clears it out if (dom.vnodes == null) dom.textContent = "" - vnodes = normalizeChildren(Array.isArray(vnodes) ? vnodes.slice() : [vnodes]) - updateNodes(dom.vnodes, vnodes) + vnodes = m.normalize(Array.isArray(vnodes) ? vnodes.slice() : [vnodes]) + updateNode(dom.vnodes, vnodes) dom.vnodes = vnodes // `document.activeElement` can return null: https://html.spec.whatwg.org/multipage/interaction.html#dom-document-activeelement - if (active != null && dom.ownerDocument.activeElement !== active && typeof active.focus === "function") active.focus() - for (var {v, p, i} of hooks) { + if (active != null && currentDocument.activeElement !== active && typeof active.focus === "function") { + active.focus() + } + for (var i = 0; i < hooks.length; i += 3) { try { - (0, v.state)(p, v.dom.signal, i) + var f = hooks[i] + var p = hooks[i + 1] + var s = hooks[i + 2] + if (typeof f === "function") f(p, s) } catch (e) { console.error(e) } @@ -730,51 +968,56 @@ m.render = (dom, vnodes, redraw) => { currentParent = prevParent currentRefNode = prevRefNode currentNamespace = prevNamespace + currentDocument = prevDocument currentlyRendering.pop() } } -var subscriptions = new Map() -var id = 0 +m.mount = (root, view, signal) => { + if (!root) throw new TypeError("Root must be an element") -function unscheduleFrame() { - if (id) { - // eslint-disable-next-line no-undef - cancelAnimationFrame(id) - id = 0 + if (typeof view !== "function") { + throw new TypeError("View must be a function") } -} -m.redraw = () => { - // eslint-disable-next-line no-undef - if (!id) id = requestAnimationFrame(m.redrawSync) -} + if (signal) { + signal.throwIfAborted() + } -m.redrawSync = () => { - unscheduleFrame() - for (const [root, view] of subscriptions) { - try { - m.render(root, view(), m.redraw) - } catch (e) { - console.error(e) + var window = root.ownerDocument.defaultView + var id = 0 + var unschedule = () => { + if (id) { + window.cancelAnimationFrame(id) + id = 0 } } -} + var redraw = () => { if (!id) id = window.requestAnimationFrame(redraw.sync) } + var Mount = (_, old) => [ + m.layout((_, signal) => { signal.onabort = unschedule }), + view(!old, redraw) + ] + redraw.sync = () => { + unschedule() + m.render(root, m(Mount), redraw) + } -m.mount = (root, view) => { - if (!root) throw new TypeError("Root must be an element") + m.render(root, null) - if (view != null && typeof view !== "function") { - throw new TypeError("View must be a component") + if (signal) { + signal.throwIfAborted() } - if (subscriptions.delete(root)) { - if (!subscriptions.size) unscheduleFrame() - m.render(root, null) - } + m.render(root, m(Mount), redraw) - if (typeof view === "function") { - subscriptions.set(root, view) - m.render(root, view(), m.redraw) + if (signal) { + if (signal.aborted) { + m.render(root, null) + throw signal.reason + } + + signal.addEventListener("abort", () => m.render(root, null), {once: true}) } + + return redraw } diff --git a/src/std/init.js b/src/std/init.js index 17ccdbb05..29d93f48f 100644 --- a/src/std/init.js +++ b/src/std/init.js @@ -1,8 +1,9 @@ import m from "../core.js" -import {p} from "../util.js" - -var Init = ({f}) => m.layout((_, signal, isInit) => isInit && p.then(() => f(signal))) +var Init = ({f}, _, {redraw}) => m.layout(async (_, signal) => { + await 0 // wait for next microtask + if ((await f(signal)) !== false) redraw() +}) var init = (f) => m(Init, {f}) export {init as default} diff --git a/src/std/lazy.js b/src/std/lazy.js index 390c16dc7..060587d67 100644 --- a/src/std/lazy.js +++ b/src/std/lazy.js @@ -1,9 +1,13 @@ import m from "../core.js" -var lazy = (opts, redraw = m.redraw) => { +var lazy = (opts) => { // Capture the error here so stack traces make more sense var error = new ReferenceError("Component not found") - var Comp = () => opts.pending && opts.pending() + var redraws = new Set() + var Comp = (_, __, context) => { + redraws.add(context.redraw) + return opts.pending && opts.pending() + } var init = async () => { try { Comp = await opts.fetch() @@ -15,7 +19,9 @@ var lazy = (opts, redraw = m.redraw) => { console.error(e) Comp = () => opts.error && opts.error(e) } - redraw() + var r = redraws + redraws = null + for (var f of r) f() } return (attrs) => { diff --git a/src/std/router.js b/src/std/router.js index 414c1a5c1..91320c11c 100644 --- a/src/std/router.js +++ b/src/std/router.js @@ -1,15 +1,13 @@ /* global window: false */ import m from "../core.js" -import {p} from "../util.js" - var mustReplace = false -var routePrefix, currentUrl, currentPath, currentHref +var redraw, routePrefix, currentUrl, currentPath, currentHref -function updateRouteWithHref(href, update) { +var updateRouteWithHref = (href, update) => { if (currentHref === href) return currentHref = href - if (update) m.redraw() + if (update && typeof redraw === "function") redraw() var url = new URL(href) var urlPath = url.pathname + url.search + url.hash @@ -24,29 +22,31 @@ function updateRouteWithHref(href, update) { mustReplace = false } -function updateRoute() { +var updateRoute = () => { updateRouteWithHref(window.location.href, true) } -function set(path, {replace, state} = {}) { +var set = (path, {replace, state} = {}) => { if (!currentUrl) { throw new ReferenceError("Route state must be fully initialized first") } if (mustReplace) replace = true mustReplace = true - p.then(updateRoute) + void (async () => { + await 0 // wait for next microtask + updateRoute() + })() + if (typeof redraw === "function") redraw() if (typeof window === "object") { - m.redraw() window.history[replace ? "replaceState" : "pushState"](state, "", routePrefix + path) } } export default { - init(prefix = "#!", href) { + init(prefix = "#!", redrawFn, href) { if (!href) { if (typeof window !== "object") { throw new TypeError("Outside the DOM, `href` must be provided") - } window.addEventListener("popstate", updateRoute, false) window.addEventListener("hashchange", updateRoute, false) @@ -54,6 +54,7 @@ export default { } routePrefix = prefix + redraw = redrawFn updateRouteWithHref(href, false) }, set, diff --git a/src/std/tracked.js b/src/std/tracked.js index ea7327fa8..3d2d5d255 100644 --- a/src/std/tracked.js +++ b/src/std/tracked.js @@ -1,5 +1,3 @@ -import m from "../core.js" - /* Here's the intent. - Usage in model: @@ -73,10 +71,10 @@ why that was removed in favor of this: /** * @template K, V * @param {Iterable<[K, V]>} [initial] - * @param {() => void} [onUpdate] + * @param {() => void} redraw * @returns {Tracked} */ -var tracked = (initial, onUpdate = m.redraw) => { +var tracked = (redraw, initial) => { /** @type {Map & {_: AbortController}>} */ var state = new Map() /** @type {Set>} */ var live = new Set() @@ -106,7 +104,7 @@ var tracked = (initial, onUpdate = m.redraw) => { if (state.get(handle.key) === handle) { handle._ = null } else if (live.delete(handle)) { - onUpdate() + redraw() } }, } @@ -116,7 +114,7 @@ var tracked = (initial, onUpdate = m.redraw) => { if (bits & 1) live.delete(prev) abort(prev) // eslint-disable-next-line no-bitwise - if (bits & 2) onUpdate() + if (bits & 2) redraw() } for (var [k, v] of initial || []) setHandle(k, v, 1) @@ -132,7 +130,7 @@ var tracked = (initial, onUpdate = m.redraw) => { var prev = state.get(k) var result = state.delete(k) abort(prev) - onUpdate() + redraw() return result }, } diff --git a/src/std/with-progress.js b/src/std/with-progress.js index 397bf11c5..48b681e99 100644 --- a/src/std/with-progress.js +++ b/src/std/with-progress.js @@ -10,7 +10,8 @@ export default (source, notify) => { type: "bytes", start: (ctrl) => reader || ctrl.close(), cancel: (reason) => reader.cancel(reason), - pull: (ctrl) => reader.read().then((result) => { + async pull(ctrl) { + var result = await reader.read() if (result.done) { ctrl.close() } else { @@ -18,6 +19,6 @@ export default (source, notify) => { ctrl.enqueue(result.value) notify(current) } - }), + }, }) } diff --git a/src/util.js b/src/util.js index 9feaf5765..cb7253cb9 100644 --- a/src/util.js +++ b/src/util.js @@ -1,2 +1 @@ export var hasOwn = {}.hasOwnProperty -export var p = Promise.resolve() diff --git a/test-utils/browserMock.js b/test-utils/browserMock.js index 3c6d53aba..7cf3a39b8 100644 --- a/test-utils/browserMock.js +++ b/test-utils/browserMock.js @@ -1,17 +1,11 @@ import domMock from "./domMock.js" import pushStateMock from "./pushStateMock.js" -import xhrMock from "./xhrMock.js" export default function browserMock(env = {}) { - env = env || {} - var $window = env.window = {} - $window.window = $window + var $window = {} - var dom = domMock() - var xhr = xhrMock() - for (var key in dom) if (!$window[key]) $window[key] = dom[key] - for (var key in xhr) if (!$window[key]) $window[key] = xhr[key] - pushStateMock(env) + domMock($window, env) + pushStateMock($window, env) return $window } diff --git a/test-utils/callAsync.js b/test-utils/callAsync.js index f4b93c3e8..368d79435 100644 --- a/test-utils/callAsync.js +++ b/test-utils/callAsync.js @@ -1,4 +1,4 @@ -/* global setImmediate, clearImmediate */ +/* global setTimeout, clearTimeout, setImmediate, clearImmediate */ const callAsyncRaw = typeof setImmediate === "function" ? setImmediate : setTimeout const cancelAsyncRaw = typeof clearImmediate === "function" ? clearImmediate : clearTimeout diff --git a/test-utils/domMock.js b/test-utils/domMock.js index 016fd3824..f94bf8f26 100644 --- a/test-utils/domMock.js +++ b/test-utils/domMock.js @@ -1,6 +1,7 @@ +/* global setTimeout, clearTimeout */ + /* Known limitations: -- the innerHTML setter and the DOMParser only support a small subset of the true HTML/XML syntax. - `option.selected` can't be set/read when the option doesn't have a `select` parent - `element.attributes` is just a map of attribute names => Attr objects stubs - ... @@ -12,10 +13,10 @@ options: - spy:(f: Function) => Function */ -export default function domMock(options) { +export default function domMock($window, options) { options = options || {} - var spy = options.spy || function(f){return f} - var spymap = [] + var spy = options.spy || ((f) => f) + var spymap = new Map() // This way I'm not also implementing a partial `URL` polyfill. Based on the // regexp at https://urlregex.com/, but adapted to allow relative URLs and @@ -32,22 +33,24 @@ export default function domMock(options) { "^" + urlHash + "$" ) - var hasOwn = ({}.hasOwnProperty) + var hasOwn = {}.hasOwnProperty + var registerSpies = () => {} + var getSpies - function registerSpies(element, spies) { - if(options.spy) { - var i = spymap.indexOf(element) - if (i === -1) { - spymap.push(element, spies) + if (options.spy) { + registerSpies = (element, spies) => { + var prev = spymap.get(element) + if (prev) { + Object.assign(prev, spies) } else { - var existing = spymap[i + 1] - for (var k in spies) existing[k] = spies[k] + spymap.set(element, spies) } } - } - function getSpies(element) { - if (element == null || typeof element !== "object") throw new Error("Element expected") - if(options.spy) return spymap[spymap.indexOf(element) + 1] + + getSpies = (element) => { + if (element == null || typeof element !== "object") throw new Error("Element expected") + return spymap.get(element) + } } function isModernEvent(type) { @@ -60,13 +63,14 @@ export default function domMock(options) { stopped = true } e.currentTarget = this - if (this._events[e.type] != null) { - for (var i = 0; i < this._events[e.type].handlers.length; i++) { - var useCapture = this._events[e.type].options[i].capture + const record = this._events.get(e.type) + if (record != null) { + for (var i = 0; i < record.handlers.length; i++) { + var useCapture = record.options[i].capture if (useCapture && e.eventPhase < 3 || !useCapture && e.eventPhase > 1) { - var handler = this._events[e.type].handlers[i] - if (typeof handler === "function") try {handler.call(this, e)} catch(e) {setTimeout(function(){throw e})} - else try {handler.handleEvent(e)} catch(e) {setTimeout(function(){throw e})} + var handler = record.handlers[i] + if (typeof handler === "function") try {handler.call(this, e)} catch(e) {console.error(e)} + else try {handler.handleEvent(e)} catch(e) {console.error(e)} if (stopped) return } } @@ -75,138 +79,27 @@ export default function domMock(options) { // this would require getters/setters for each of them though and we haven't gotten around to // adding them since it would be at a high perf cost or would entail some heavy refactoring of // the mocks (prototypes instead of closures). - if (e.eventPhase > 1 && typeof this["on" + e.type] === "function" && !isModernEvent(e.type)) try {this["on" + e.type](e)} catch(e) {setTimeout(function(){throw e})} - } - function appendChild(child) { - var ancestor = this - while (ancestor !== child && ancestor !== null) ancestor = ancestor.parentNode - if (ancestor === child) throw new Error("Node cannot be inserted at the specified point in the hierarchy") - - if (child.nodeType == null) throw new Error("Argument is not a DOM element") - - var index = this.childNodes.indexOf(child) - if (index > -1) this.childNodes.splice(index, 1) - if (child.nodeType === 11) { - while (child.firstChild != null) appendChild.call(this, child.firstChild) - child.childNodes = [] - } - else { - this.childNodes.push(child) - if (child.parentNode != null && child.parentNode !== this) removeChild.call(child.parentNode, child) - child.parentNode = this - } - } - function removeChild(child) { - if (child == null || typeof child !== "object" || !("nodeType" in child)) { - throw new TypeError("Failed to execute removeChild, parameter is not of type 'Node'") - } - var index = this.childNodes.indexOf(child) - if (index > -1) { - this.childNodes.splice(index, 1) - child.parentNode = null - } - else throw new TypeError("Failed to execute 'removeChild', child not found in parent") - } - function prepend(child) { - return insertBefore.call(this, child, this.firstChild) - } - function after(child) { - if (this == null || typeof this !== "object" || !("nodeType" in this)) { - throw new TypeError("Failed to execute 'remove', this is not of type 'ChildNode'") - } - if (child == null || typeof child !== "object" || !("nodeType" in child)) { - throw new TypeError("Failed to execute 'remove', parameter is not of type 'ChildNode'") - } - var parent = this.parentNode - if (parent == null) return - var index = parent.childNodes.indexOf(this) - if (index < 0) { - throw new TypeError("BUG: child linked to parent, parent doesn't contain child") - } - remove.call(child) - parent.childNodes.splice(index + 1, 0, child) - child.parentNode = parent - } - function remove() { - if (this == null || typeof this !== "object" || !("nodeType" in this)) { - throw new TypeError("Failed to execute 'remove', this is not of type 'ChildNode'") - } - var parent = this.parentNode - if (parent == null) return - var index = parent.childNodes.indexOf(this) - if (index < 0) { - throw new TypeError("BUG: child linked to parent, parent doesn't contain child") - } - parent.childNodes.splice(index, 1) - this.parentNode = null - } - function insertBefore(child, reference) { - var ancestor = this - while (ancestor !== child && ancestor !== null) ancestor = ancestor.parentNode - if (ancestor === child) throw new Error("Node cannot be inserted at the specified point in the hierarchy") - - if (child.nodeType == null) throw new Error("Argument is not a DOM element") - - var refIndex = this.childNodes.indexOf(reference) - var index = this.childNodes.indexOf(child) - if (reference !== null && refIndex < 0) throw new TypeError("Invalid argument") - if (index > -1) this.childNodes.splice(index, 1) - if (reference === null) appendChild.call(this, child) - else { - if (index !== -1 && refIndex > index) refIndex-- - if (child.nodeType === 11) { - this.childNodes.splice.apply(this.childNodes, [refIndex, 0].concat(child.childNodes)) - while (child.firstChild) { - var subchild = child.firstChild - removeChild.call(child, subchild) - subchild.parentNode = this - } - child.childNodes = [] - } - else { - this.childNodes.splice(refIndex, 0, child) - if (child.parentNode != null && child.parentNode !== this) removeChild.call(child.parentNode, child) - child.parentNode = this + if (e.eventPhase > 1 && typeof this["on" + e.type] === "function" && !isModernEvent(e.type)) { + try { + this["on" + e.type](e) + } catch (e) { + console.error(e) } } } - function getAttribute(name) { - if (this.attributes[name] == null) return null - return this.attributes[name].value - } - function setAttribute(name, value) { - /*eslint-disable no-implicit-coercion*/ - // this is the correct kind of conversion, passing a Symbol throws in browsers too. - var nodeValue = "" + value - /*eslint-enable no-implicit-coercion*/ - this.attributes[name] = { - namespaceURI: hasOwn.call(this.attributes, name) ? this.attributes[name].namespaceURI : null, - get value() {return nodeValue}, - set value(value) { - /*eslint-disable no-implicit-coercion*/ - nodeValue = "" + value - /*eslint-enable no-implicit-coercion*/ - }, - get nodeValue() {return nodeValue}, - set nodeValue(value) { - this.value = value - } + + class Attr { + constructor(namespaceURI, value) { + this.namespaceURI = namespaceURI + // this is the correct kind of conversion, passing a Symbol throws in browsers too. + this._value = `${value}` } + get value() { return this._value } + set value(value) { this._value = `${value}` } + get nodeValue() { return this._value } + set nodeValue(value) { this._value = `${value}` } } - function setAttributeNS(ns, name, value) { - this.setAttribute(name, value) - this.attributes[name].namespaceURI = ns - } - function removeAttribute(name) { - delete this.attributes[name] - } - function removeAttributeNS(_ns, name) { - // Namespace is ignored for now - delete this.attributes[name] - } - function hasAttribute(name) { - return name in this.attributes - } + var declListTokenizer = /;|"(?:\\.|[^"\n])*"|'(?:\\.|[^'\n])*'/g /** * This will split a semicolon-separated CSS declaration list into an array of @@ -239,493 +132,732 @@ export default function domMock(options) { res.unshift(declList) return res } - function parseMarkup(value, root, voidElements, xmlns) { - var depth = 0, stack = [root] - value.replace(/<([a-z0-9\-]+?)((?:\s+?[^=]+?=(?:"[^"]*?"|'[^']*?'|[^\s>]*))*?)(\s*\/)?>|<\/([a-z0-9\-]+?)>|([^<]+)/g, function(match, startTag, attrs, selfClosed, endTag, text) { - if (startTag) { - var element = xmlns == null ? $window.document.createElement(startTag) : $window.document.createElementNS(xmlns, startTag) - attrs.replace(/\s+?([^=]+?)=(?:"([^"]*?)"|'([^']*?)'|([^\s>]*))/g, function(match, key, doubleQuoted, singleQuoted, unquoted) { - var keyParts = key.split(":") - var name = keyParts.pop() - var ns = keyParts[0] - var value = doubleQuoted || singleQuoted || unquoted || "" - if (ns != null) element.setAttributeNS(ns, name, value) - else element.setAttribute(name, value) - }) - appendChild.call(stack[depth], element) - if (!selfClosed && voidElements.indexOf(startTag.toLowerCase()) < 0) stack[++depth] = element - } - else if (endTag) { - depth-- - } - else if (text) { - appendChild.call(stack[depth], $window.document.createTextNode(text)) // FIXME handle html entities - } - }) - } - function DOMParser() {} - DOMParser.prototype.parseFromString = function(src, mime) { - if (mime !== "image/svg+xml") throw new Error("The DOMParser mock only supports the \"image/svg+xml\" MIME type") - var match = src.match(/^(.*)<\/svg>$/) - if (!match) throw new Error("Please provide a bare SVG tag with the xmlns as only attribute") - var value = match[1] - var root = $window.document.createElementNS("http://www.w3.org/2000/svg", "svg") - parseMarkup(value, root, [], "http://www.w3.org/2000/svg") - return {documentElement: root} - } function camelCase(string) { - return string.replace(/-\D/g, function(match) {return match[1].toUpperCase()}) + return string.replace(/-[a-z]/g, (match) => match[1].toUpperCase()) } - var activeElement - var delay = 16, last = 0 - var $window = { - DOMParser: DOMParser, - requestAnimationFrame: function(callback) { - var elapsed = Date.now() - last - return setTimeout(function() { - callback() - last = Date.now() - }, delay - elapsed) - }, - cancelAnimationFrame: clearTimeout, - document: { - createElement: function(tag) { - var cssText = "" - var style = {} - Object.defineProperties(style, { - cssText: { - get: function() {return cssText}, - set: function (value) { - var buf = [] - if (typeof value === "string") { - for (var key in style) style[key] = "" - var rules = splitDeclList(value) - for (var i = 0; i < rules.length; i++) { - var rule = rules[i] - var colonIndex = rule.indexOf(":") - if (colonIndex > -1) { - var rawKey = rule.slice(0, colonIndex).trim() - var key = camelCase(rawKey) - var value = rule.slice(colonIndex + 1).trim() - if (key !== "cssText") { - style[key] = style[rawKey] = value - buf.push(rawKey + ": " + value + ";") - } - } - } - element.setAttribute("style", cssText = buf.join(" ")) - } - } - }, - getPropertyValue: {value: function(key){ - return style[key] - }}, - removeProperty: {value: function(key){ - style[key] = style[camelCase(key)] = "" - }}, - setProperty: {value: function(key, value){ - style[key] = style[camelCase(key)] = value - }} - }) - var events = {} - var element = { - nodeType: 1, - nodeName: tag.toUpperCase(), - namespaceURI: "http://www.w3.org/1999/xhtml", - appendChild: appendChild, - removeChild: removeChild, - remove: remove, - insertBefore: insertBefore, - prepend: prepend, - after: after, - hasAttribute: hasAttribute, - getAttribute: getAttribute, - setAttribute: setAttribute, - setAttributeNS: setAttributeNS, - removeAttribute: removeAttribute, - removeAttributeNS: removeAttributeNS, - parentNode: null, - childNodes: [], - attributes: {}, - ownerDocument: $window.document, - contains: function(child) { - while (child != null) { - if (child === this) return true - child = child.parentNode - } - return false - }, - get firstChild() { - return this.childNodes[0] || null - }, - get nextSibling() { - if (this.parentNode == null) return null - var index = this.parentNode.childNodes.indexOf(this) - if (index < 0) throw new TypeError("Parent's childNodes is out of sync") - return this.parentNode.childNodes[index + 1] || null - }, - // eslint-disable-next-line accessor-pairs - set textContent(value) { - this.childNodes = [] - if (value !== "") appendChild.call(this, $window.document.createTextNode(value)) - }, - // eslint-disable-next-line accessor-pairs - set innerHTML(value) { - var voidElements = ["area", "base", "br", "col", "command", "embed", "hr", "img", "input", "keygen", "link", "meta", "param", "source", "track", "wbr"] - while (this.firstChild) removeChild.call(this, this.firstChild) - var match = value.match(/^(.*)<\/svg>$/), root, ns - if (match) { - var value = match[1] - root = $window.document.createElementNS("http://www.w3.org/2000/svg", "svg") - ns = "http://www.w3.org/2000/svg" - appendChild.call(this, root) - } else { - root = this - } - parseMarkup(value, root, voidElements, ns) - }, - get style() { - return style - }, - set style(value){ - this.style.cssText = value - }, - get className() { - return this.attributes["class"] ? this.attributes["class"].value : "" - }, - set className(value) { - if (this.namespaceURI === "http://www.w3.org/2000/svg") throw new Error("Cannot set property className of SVGElement") - else this.setAttribute("class", value) - }, - focus: function() {activeElement = this}, - addEventListener: function(type, handler, options) { - if (arguments.length > 2) { - if (typeof options === "object" && options != null) throw new TypeError("NYI: addEventListener options") - else if (typeof options !== "boolean") throw new TypeError("boolean expected for useCapture") - else options = {capture: options} - } else { - options = {capture: false} - } - if (events[type] == null) events[type] = {handlers: [handler], options: [options]} - else { - var found = false - for (var i = 0; i < events[type].handlers.length; i++) { - if (events[type].handlers[i] === handler && events[type].options[i].capture === options.capture) { - found = true - break - } - } - if (!found) { - events[type].handlers.push(handler) - events[type].options.push(options) - } - } - }, - removeEventListener: function(type, handler, options) { - if (arguments.length > 2) { - if (typeof options === "object" && options != null) throw new TypeError("NYI: addEventListener options") - else if (typeof options !== "boolean") throw new TypeError("boolean expected for useCapture") - else options = {capture: options} - } else { - options = {capture: false} - } - if (events[type] != null) { - for (var i = 0; i < events[type].handlers.length; i++) { - if (events[type].handlers[i] === handler && events[type].options[i].capture === options.capture) { - events[type].handlers.splice(i, 1) - events[type].options.splice(i, 1) - break; - } - } - } - }, - dispatchEvent: function(e) { - var parents = [] - if (this.parentNode != null) { - var parent = this.parentNode - do { - parents.push(parent) - parent = parent.parentNode - } while (parent != null) - } - e.target = this - var prevented = false - e.preventDefault = function() { - prevented = true - } - Object.defineProperty(e, "defaultPrevented", { - configurable: true, - get: function () { return prevented } - }) - var stopped = false - e.stopPropagation = function() { - stopped = true - } - Object.defineProperty(e, "cancelBubble", { - configurable: true, - get: function () { return stopped } - }) - e.eventPhase = 1 - try { - for (var i = parents.length - 1; 0 <= i; i--) { - dispatchEvent.call(parents[i], e) - if (stopped) { - return - } - } - e.eventPhase = 2 - dispatchEvent.call(this, e) - if (stopped) { - return - } - e.eventPhase = 3 - for (var i = 0; i < parents.length; i++) { - dispatchEvent.call(parents[i], e) - if (stopped) { - return - } - } - } finally { - e.eventPhase = 0 - if (!prevented) { - if (this.nodeName === "INPUT" && this.attributes["type"] != null && this.attributes["type"].value === "checkbox" && e.type === "click") { - this.checked = !this.checked - } - } + + class CSSStyleDeclarationHandler { + constructor(element) { + this.element = element + this.style = new Map() + this.raws = new Set() + this.cssText = undefined + } + + preventExtensions() { + return false + } + + _setCSSText(value) { + var buf = [] + if (typeof value === "string") { + for (var key of this.style.keys()) this.style.set(key, "") + const rules = splitDeclList(value) + for (let i = 0; i < rules.length; i++) { + const rule = rules[i] + const colonIndex = rule.indexOf(":") + if (colonIndex > -1) { + const rawKey = rule.slice(0, colonIndex).trim() + const key = camelCase(rawKey) + const value = rule.slice(colonIndex + 1).trim() + if (key !== "cssText") { + this.style.set(rawKey, value) + this.style.set(key, value) + this.raws.add(rawKey) + buf.push(rawKey + ": " + value + ";") } + } + } + this.element.setAttribute("style", this.cssText = buf.join(" ")) + } + } + + _getCSSText() { + if (this.cssText != null) return this.cssText + const result = [] + for (const key of this.raws) { + result.push(`${key}: ${this.style.get(key)};`) + } + return this.cssText = result.join(" ") + } + + get(target, key) { + if (typeof key !== "string") return Reflect.get(target, key) + if (Reflect.has(target, key)) return Reflect.get(target, key) + const value = this.style.get(key) + if (value !== undefined) return value + switch (key) { + case "cssText": return this._getCSSText() + case "cssFloat": return this.style.get("float") + default: return "" + } + } + + set(target, key, value) { + if (typeof key !== "string") return Reflect.set(target, key) + if (Reflect.has(target, key)) return Reflect.set(target, key) + if (value == null) value = "" + switch (key) { + case "cssText": this._setCSSText(value); return true + case "cssFloat": key = "float"; break + } + if (value === "") { + this.style.delete(key) + this.style.delete(camelCase(key)) + this.raws.add(key) + } else { + this.style.set(key, value) + this.style.set(camelCase(key), value) + this.raws.add(key) + } + this.cssText = undefined + return true + } + } + + class CSSStyleDeclaration { + constructor(element) { + return new Proxy(this, new CSSStyleDeclarationHandler(element)) + } + + getPropertyValue(key) { + return this[key] + } + + removeProperty(key) { + this[key] = this[camelCase(key)] = "" + } + + setProperty(key, value) { + this[key] = this[camelCase(key)] = value + } + } + + class ChildNode { + constructor(nodeType, nodeName) { + this.nodeType = nodeType + this.nodeName = nodeName + this.parentNode = null + } + + remove() { + if (this == null || typeof this !== "object" || !("nodeType" in this)) { + throw new TypeError("Failed to execute 'remove', this is not of type 'ChildNode'") + } + var parent = this.parentNode + if (parent == null) return + var index = parent.childNodes.indexOf(this) + if (index < 0) { + throw new TypeError("BUG: child linked to parent, parent doesn't contain child") + } + parent.childNodes.splice(index, 1) + this.parentNode = null + } + + after(child) { + if (this == null || typeof this !== "object" || !("nodeType" in this)) { + throw new TypeError("Failed to execute 'remove', this is not of type 'ChildNode'") + } + if (child == null || typeof child !== "object" || !("nodeType" in child)) { + throw new TypeError("Failed to execute 'remove', parameter is not of type 'ChildNode'") + } + var parent = this.parentNode + if (parent == null) return + var index = parent.childNodes.indexOf(this) + if (index < 0) { + throw new TypeError("BUG: child linked to parent, parent doesn't contain child") + } + child.remove() + parent.childNodes.splice(index + 1, 0, child) + child.parentNode = parent + } + + get nextSibling() { + if (this.parentNode == null) return null + var index = this.parentNode.childNodes.indexOf(this) + if (index < 0) throw new TypeError("Parent's childNodes is out of sync") + return this.parentNode.childNodes[index + 1] || null + } + } + + class Text extends ChildNode { + constructor(value) { + super(3, "#text") + this._value = `${value}` + } + + get childNodes() { + return [] + } + + get firstChild() { + return null + } + + get nodeValue() { + return this._value + } + + set nodeValue(value) { + this._value = `${value}` + } + } + + class Element extends ChildNode { + constructor(nodeName, ns) { + if (ns == null) ns = "http://www.w3.org/1999/xhtml" + super(1, nodeName) + this._style = new CSSStyleDeclaration(this) + this.namespaceURI = ns + this.parentNode = null + this.childNodes = [] + this.attributes = {} + this.ownerDocument = $window.document + this.onclick = null + this._events = new Map() + } + + appendChild(child) { + var ancestor = this + while (ancestor !== child && ancestor !== null) ancestor = ancestor.parentNode + if (ancestor === child) throw new Error("Node cannot be inserted at the specified point in the hierarchy") + + if (child.nodeType == null) throw new Error("Argument is not a DOM element") + + var index = this.childNodes.indexOf(child) + if (index > -1) this.childNodes.splice(index, 1) + this.childNodes.push(child) + if (child.parentNode != null && child.parentNode !== this) child.parentNode.removeChild(child) + child.parentNode = this + } + + removeChild(child) { + if (child == null || typeof child !== "object" || !("nodeType" in child)) { + throw new TypeError("Failed to execute removeChild, parameter is not of type 'Node'") + } + var index = this.childNodes.indexOf(child) + if (index > -1) { + this.childNodes.splice(index, 1) + child.parentNode = null + } + else throw new TypeError("Failed to execute 'removeChild', child not found in parent") + } + + insertBefore(child, refNode) { + var ancestor = this + while (ancestor !== child && ancestor !== null) ancestor = ancestor.parentNode + if (ancestor === child) throw new Error("Node cannot be inserted at the specified point in the hierarchy") + + if (child.nodeType == null) throw new Error("Argument is not a DOM element") + + var refIndex = this.childNodes.indexOf(refNode) + var index = this.childNodes.indexOf(child) + if (refNode !== null && refIndex < 0) throw new TypeError("Invalid argument") + if (index > -1) { + this.childNodes.splice(index, 1) + child.parentNode = null + } + if (refNode === null) this.appendChild(child) + else { + if (index !== -1 && refIndex > index) refIndex-- + this.childNodes.splice(refIndex, 0, child) + if (child.parentNode != null && child.parentNode !== this) child.parentNode.removeChild(child) + child.parentNode = this + } + } + + prepend(child) { + this.insertBefore(child, this.firstChild) + } + + hasAttribute(name) { + return name in this.attributes + } + + getAttribute(name) { + if (this.attributes[name] == null) return null + return this.attributes[name].value + } + + setAttribute(name, value) { + value = `${value}` + if (hasOwn.call(this.attributes, name)) { + this.attributes[name].value = value + } else { + this.attributes[name] = new Attr(null, value) + } + } + + setAttributeNS(ns, name, value) { + if (hasOwn.call(this.attributes, name)) { + this.attributes[name].namespaceURI = ns + this.attributes[name].value = value + } else { + this.attributes[name] = new Attr(ns, value) + } + } + + removeAttribute(name) { + delete this.attributes[name] + } + + removeAttributeNS(ns, name) { + // Namespace is ignored for now + delete this.attributes[name] + } + + contains(child) { + while (child != null) { + if (child === this) return true + child = child.parentNode + } + return false + } + + get firstChild() { + return this.childNodes[0] || null + } + + get nextSibling() { + if (this.parentNode == null) return null + var index = this.parentNode.childNodes.indexOf(this) + if (index < 0) throw new TypeError("Parent's childNodes is out of sync") + return this.parentNode.childNodes[index + 1] || null + } + + // eslint-disable-next-line accessor-pairs + set textContent(value) { + this.childNodes = [] + if (value !== "") this.appendChild($window.document.createTextNode(value)) + } + + get style() { + return this._style + } - }, - onclick: null, - _events: events + set style(value) { + this.style.cssText = value + } + + get className() { + if (this.namespaceURI === "http://www.w3.org/2000/svg") throw new Error("Cannot get property className of SVGElement") + else return this.getAttribute("class") + } + + set className(value) { + if (this.namespaceURI === "http://www.w3.org/2000/svg") throw new Error("Cannot set property className of SVGElement") + else this.setAttribute("class", value) + } + + focus() { + activeElement = this + } + + blur() { + if (activeElement === this) activeElement = null + } + + addEventListener(type, handler, options) { + if (arguments.length > 2) { + if (typeof options === "object" && options != null) throw new TypeError("NYI: addEventListener options") + else if (typeof options !== "boolean") throw new TypeError("boolean expected for useCapture") + else options = {capture: options} + } else { + options = {capture: false} + } + const record = this._events.get(type) + if (record == null) { + this._events.set(type, {handlers: [handler], options: [options]}) + } else { + for (var i = 0; i < record.handlers.length; i++) { + if (record.handlers[i] === handler && record.options[i].capture === options.capture) { + return + } } + record.handlers.push(handler) + record.options.push(options) + } + } - if (element.nodeName === "A") { - Object.defineProperty(element, "href", { - get: function() { - if (this.namespaceURI === "http://www.w3.org/2000/svg") { - var val = this.hasAttribute("href") ? this.attributes.href.value : "" - return {baseVal: val, animVal: val} - } else if (this.namespaceURI === "http://www.w3.org/1999/xhtml") { - if (!this.hasAttribute("href")) return "" - // HACK: if it's valid already, there's nothing to implement. - var value = this.attributes.href.value - if (validURLRegex.test(encodeURI(value))) return value - } - return "[FIXME implement]" - }, - set: function(value) { - // This is a readonly attribute for SVG, todo investigate MathML which may have yet another IDL - if (this.namespaceURI !== "http://www.w3.org/2000/svg") this.setAttribute("href", value) - }, - enumerable: true, - }) + removeEventListener(type, handler, options) { + if (arguments.length > 2) { + if (typeof options === "object" && options != null) throw new TypeError("NYI: addEventListener options") + else if (typeof options !== "boolean") throw new TypeError("boolean expected for useCapture") + else options = {capture: options} + } else { + options = {capture: false} + } + const record = this._events.get(type) + if (record != null) { + for (var i = 0; i < record.handlers.length; i++) { + if (record.handlers[i] === handler && record.options[i].capture === options.capture) { + record.handlers.splice(i, 1) + record.options.splice(i, 1) + break + } } + } + } - if (element.nodeName === "INPUT") { - var checked - Object.defineProperty(element, "checked", { - get: function() {return checked === undefined ? this.attributes["checked"] !== undefined : checked}, - set: function(value) {checked = Boolean(value)}, - enumerable: true, - }) - - var value = "" - var valueSetter = spy(function(v) { - /*eslint-disable no-implicit-coercion*/ - value = v === null ? "" : "" + v - /*eslint-enable no-implicit-coercion*/ - }) - Object.defineProperty(element, "value", { - get: function() { - return value - }, - set: valueSetter, - enumerable: true, - }) - Object.defineProperty(element, "valueAsDate", { - get: function() { - if (this.getAttribute("type") !== "date") return null - return new Date(value).getTime() - }, - set: function(v) { - if (this.getAttribute("type") !== "date") throw new Error("invalid state") - var time = new Date(v).getTime() - valueSetter(isNaN(time) ? "" : new Date(time).toUTCString()) - }, - enumerable: true, - }) - Object.defineProperty(element, "valueAsNumber", { - get: function() { - switch (this.getAttribute("type")) { - case "date": return new Date(value).getTime() - case "number": return new Date(value).getTime() - default: return NaN - } - }, - set: function(v) { - v = Number(v) - if (!isNaN(v) && !isFinite(v)) throw new TypeError("infinite value") - switch (this.getAttribute("type")) { - case "date": valueSetter(isNaN(v) ? "" : new Date(v).toUTCString()); break; - case "number": valueSetter(String(value)); break; - default: throw new Error("invalid state") - } - }, - enumerable: true, - }) - - Object.defineProperty(element, "type", { - get: function() { - if (!this.hasAttribute("type")) return "text" - var type = this.getAttribute("type") - return (/^(?:radio|button|checkbox|color|date|datetime|datetime-local|email|file|hidden|month|number|password|range|research|search|submit|tel|text|url|week|image)$/) - .test(type) - ? type - : "text" - }, - set: function(v) { - this.setAttribute("type", v) - }, - enumerable: true, - }) - registerSpies(element, { - valueSetter: valueSetter, - }) + dispatchEvent(e) { + var parents = [] + if (this.parentNode != null) { + var parent = this.parentNode + do { + parents.push(parent) + parent = parent.parentNode + } while (parent != null) + } + e.target = this + var prevented = false + e.preventDefault = function() { + prevented = true + } + Object.defineProperty(e, "defaultPrevented", { + configurable: true, + get: function () { return prevented } + }) + var stopped = false + e.stopPropagation = function() { + stopped = true + } + Object.defineProperty(e, "cancelBubble", { + configurable: true, + get: function () { return stopped } + }) + e.eventPhase = 1 + try { + for (var i = parents.length - 1; 0 <= i; i--) { + dispatchEvent.call(parents[i], e) + if (stopped) { + return + } + } + e.eventPhase = 2 + dispatchEvent.call(this, e) + if (stopped) { + return + } + e.eventPhase = 3 + for (var i = 0; i < parents.length; i++) { + dispatchEvent.call(parents[i], e) + if (stopped) { + return + } + } + } finally { + e.eventPhase = 0 + if (!prevented) { + if (this.nodeName === "INPUT" && this.attributes["type"] != null && this.attributes["type"].value === "checkbox" && e.type === "click") { + this.checked = !this.checked + } } + } + } + } + class HTMLAnchorElement extends Element { + constructor() { + super("A", null) + } - if (element.nodeName === "TEXTAREA") { - var wasNeverSet = true - var value = "" - var valueSetter = spy(function(v) { - wasNeverSet = false - /*eslint-disable no-implicit-coercion*/ - value = v === null ? "" : "" + v - /*eslint-enable no-implicit-coercion*/ - }) - Object.defineProperty(element, "value", { - get: function() { - return wasNeverSet && this.firstChild ? this.firstChild.nodeValue : value - }, - set: valueSetter, - enumerable: true, - }) - registerSpies(element, { - valueSetter: valueSetter - }) - } + get href() { + if (this.namespaceURI === "http://www.w3.org/2000/svg") { + var val = this.hasAttribute("href") ? this.attributes.href.value : "" + return {baseVal: val, animVal: val} + } else if (this.namespaceURI === "http://www.w3.org/1999/xhtml") { + if (!this.hasAttribute("href")) return "" + // HACK: if it's valid already, there's nothing to implement. + var value = this.attributes.href.value + if (validURLRegex.test(encodeURI(value))) return value + } + return "[FIXME implement]" + } - /* eslint-disable radix */ - - if (element.nodeName === "CANVAS") { - Object.defineProperty(element, "width", { - get: function() {return this.attributes["width"] ? Math.floor(parseInt(this.attributes["width"].value) || 0) : 300}, - set: function(value) {this.setAttribute("width", Math.floor(Number(value) || 0).toString())}, - }) - Object.defineProperty(element, "height", { - get: function() {return this.attributes["height"] ? Math.floor(parseInt(this.attributes["height"].value) || 0) : 300}, - set: function(value) {this.setAttribute("height", Math.floor(Number(value) || 0).toString())}, - }) - } + set href(value) { + // This is a readonly attribute for SVG, todo investigate MathML which may have yet another IDL + if (this.namespaceURI !== "http://www.w3.org/2000/svg") this.setAttribute("href", value) + } + } + + class HTMLInputElement extends Element { + constructor() { + super("INPUT", null) + this._checked = undefined + this._value = "" + + registerSpies(this, { + valueSetter: this._valueSetter = spy(this._setValue), + }) + } + + _setValue(v) { + this._value = v === null ? "" : `${v}` + } + + get checked() { + return this._checked === undefined ? this.hasAttribute("checked") : this._checked + } + + set checked(value) { + this._checked = Boolean(value) + } - /* eslint-enable radix */ + get value() { + return this._value + } + + set value(value) { + this._valueSetter(value) + } + + get valueAsDate() { + if (this.getAttribute("type") !== "date") return null + return new Date(this._value).getTime() + } + + set valueAsDate(v) { + if (this.getAttribute("type") !== "date") throw new Error("invalid state") + var time = new Date(v).getTime() + this._valueSetter(isNaN(time) ? "" : new Date(time).toUTCString()) + } + + get valueAsNumber() { + switch (this.getAttribute("type")) { + case "date": return new Date(this._value).getTime() + case "number": return new Date(this._value).getTime() + default: return NaN + } + } + + set valueAsNumber(v) { + v = Number(v) + if (!isNaN(v) && !isFinite(v)) throw new TypeError("infinite value") + switch (this.getAttribute("type")) { + case "date": this._valueSetter(isNaN(v) ? "" : new Date(v).toUTCString()); break; + case "number": this._valueSetter(`${v}`); break; + default: throw new Error("invalid state") + } + } + + get type() { + var type = this.getAttribute("type") + if (type != null && (/^(?:radio|button|checkbox|color|date|datetime|datetime-local|email|file|hidden|month|number|password|range|research|search|submit|tel|text|url|week|image)$/).test(type)) { + return type + } else { + return "text" + } + } + + set type(value) { + this.setAttribute("type", value) + } + } + + class HTMLTextAreaElement extends Element { + constructor() { + super("TEXTAREA", null) + this._value = undefined + + registerSpies(this, { + valueSetter: this._valueSetter = spy(this._setValue), + }) + } + + _setValue(v) { + this._value = v === null ? "" : `${v}` + } + + get value() { + if (this._value === undefined && this.firstChild) { + return this.firstChild.nodeValue + } else { + return this._value + } + } - function getOptions(element) { - var options = [] - for (var i = 0; i < element.childNodes.length; i++) { - if (element.childNodes[i].nodeName === "OPTION") options.push(element.childNodes[i]) - else if (element.childNodes[i].nodeName === "OPTGROUP") options = options.concat(getOptions(element.childNodes[i])) + set value(value) { + this._valueSetter(value) + } + } + + class HTMLCanvasElement extends Element { + constructor() { + super("CANVAS", null) + } + + get width() { + const value = this.getAttribute("width") + // eslint-disable-next-line radix + return value != null ? Math.floor(parseInt(value) || 0) : 300 + } + + set width(value) { + this.setAttribute("width", Math.floor(Number(value) || 0).toString()) + } + + get height() { + const value = this.getAttribute("height") + // eslint-disable-next-line radix + return value != null ? Math.floor(parseInt(value) || 0) : 300 + } + + set height(value) { + this.setAttribute("height", Math.floor(Number(value) || 0).toString()) + } + } + + function pushOptions(options, element) { + for (const child of element.childNodes) { + if (child.nodeName === "OPTION") { + options.push(child) + } else if (child.nodeName === "OPTGROUP") { + pushOptions(options, child) + } + } + } + + function getOptions(element) { + const options = [] + pushOptions(options, element) + return options + } + + function getOptionValue(element) { + const value = element.getAttribute("value") + if (value != null) return value + const child = element.firstChild + if (child != null) return child.nodeValue + return "" + } + + class HTMLSelectElement extends Element { + constructor() { + super("SELECT", null) + // this._selectedValue = undefined + this._selectedIndex = 0 + + registerSpies(this, { + valueSetter: this._valueSetter = spy(this._setValue) + }) + } + + _setValue(value) { + if (value === null) { + this._selectedIndex = -1 + } else { + var options = getOptions(this) + var stringValue = `${value}` + for (var i = 0; i < options.length; i++) { + if (getOptionValue(options[i]) === stringValue) { + // this._selectedValue = stringValue + this._selectedIndex = i + return } - return options - } - function getOptionValue(element) { - return element.attributes["value"] != null ? - element.attributes["value"].value : - element.firstChild != null ? element.firstChild.nodeValue : "" } - if (element.nodeName === "SELECT") { - // var selectedValue - var selectedIndex = 0 - Object.defineProperty(element, "selectedIndex", { - get: function() {return getOptions(this).length > 0 ? selectedIndex : -1}, - set: function(value) { - var options = getOptions(this) - if (value >= 0 && value < options.length) { - // selectedValue = getOptionValue(options[selectedIndex]) - selectedIndex = value - } - else { - // selectedValue = "" - selectedIndex = -1 - } - }, - enumerable: true, - }) - var valueSetter = spy(function(value) { - if (value === null) { - selectedIndex = -1 - } else { - var options = getOptions(this) - /*eslint-disable no-implicit-coercion*/ - var stringValue = "" + value - /*eslint-enable no-implicit-coercion*/ - for (var i = 0; i < options.length; i++) { - if (getOptionValue(options[i]) === stringValue) { - // selectedValue = stringValue - selectedIndex = i - return - } - } - // selectedValue = stringValue - selectedIndex = -1 - } - }) - Object.defineProperty(element, "value", { - get: function() { - if (this.selectedIndex > -1) return getOptionValue(getOptions(this)[this.selectedIndex]) - return "" - }, - set: valueSetter, - enumerable: true, - }) - registerSpies(element, { - valueSetter: valueSetter - }) + // this._selectedValue = stringValue + this._selectedIndex = -1 + } + } + + get selectedIndex() { + if (getOptions(this).length) { + return this._selectedIndex + } else { + return -1 + } + } + + set selectedIndex(value) { + var options = getOptions(this) + if (value >= 0 && value < options.length) { + // this._selectedValue = getOptionValue(options[selectedIndex]) + this._selectedIndex = value + } else { + // this._selectedValue = "" + this._selectedIndex = -1 + } + } + + get value() { + if (this.selectedIndex > -1) { + return getOptionValue(getOptions(this)[this.selectedIndex]) + } + return "" + } + + set value(value) { + this._valueSetter(value) + } + } + + class HTMLOptionElement extends Element { + constructor() { + super("OPTION", null) + registerSpies(this, { + valueSetter: this._valueSetter = spy(this._setValue) + }) + } + + _setValue(value) { + this.setAttribute("value", value) + } + + get value() { + return getOptionValue(this) + } + + set value(value) { + this._valueSetter(value) + } + + // TODO? handle `selected` without a parent (works in browsers) + get selected() { + var index = getOptions(this.parentNode).indexOf(this) + return index === this.parentNode.selectedIndex + } + + set selected(value) { + if (value) { + var index = getOptions(this.parentNode).indexOf(this) + if (index > -1) this.parentNode.selectedIndex = index + } else { + this.parentNode.selectedIndex = 0 + } + } + } + + var activeElement = null + var delay = 16, last = 0 + Object.assign($window, { + window: $window, + requestAnimationFrame(callback) { + var elapsed = performance.now() - last + return setTimeout(() => { + last = performance.now() + try { + callback() + } catch (e) { + console.error(e) } - if (element.nodeName === "OPTION") { - var valueSetter = spy(function(value) { - /*eslint-disable no-implicit-coercion*/ - this.setAttribute("value", "" + value) - /*eslint-enable no-implicit-coercion*/ - }) - Object.defineProperty(element, "value", { - get: function() {return getOptionValue(this)}, - set: valueSetter, - enumerable: true, - }) - registerSpies(element, { - valueSetter: valueSetter - }) - - Object.defineProperty(element, "selected", { - // TODO? handle `selected` without a parent (works in browsers) - get: function() { - var options = getOptions(this.parentNode) - var index = options.indexOf(this) - return index === this.parentNode.selectedIndex - }, - set: function(value) { - if (value) { - var options = getOptions(this.parentNode) - var index = options.indexOf(this) - if (index > -1) this.parentNode.selectedIndex = index - } - else this.parentNode.selectedIndex = 0 - }, - enumerable: true, - }) + }, delay - elapsed) + }, + cancelAnimationFrame: clearTimeout, + document: { + defaultView: $window, + createElement: function(tag) { + if (!tag) throw new Error("Tag must be provided") + tag = `${tag}`.toUpperCase() + + switch (tag) { + case "A": return new HTMLAnchorElement() + case "INPUT": return new HTMLInputElement() + case "TEXTAREA": return new HTMLTextAreaElement() + case "CANVAS": return new HTMLCanvasElement() + case "SELECT": return new HTMLSelectElement() + case "OPTION": return new HTMLOptionElement() + default: return new Element(tag, null) } - return element }, createElementNS: function(ns, tag, is) { var element = this.createElement(tag, is) @@ -734,45 +866,7 @@ export default function domMock(options) { return element }, createTextNode: function(text) { - /*eslint-disable no-implicit-coercion*/ - var nodeValue = "" + text - /*eslint-enable no-implicit-coercion*/ - return { - nodeType: 3, - nodeName: "#text", - parentNode: null, - remove: remove, - after: after, - get childNodes() { return [] }, - get firstChild() { return null }, - get nodeValue() {return nodeValue}, - set nodeValue(value) { - /*eslint-disable no-implicit-coercion*/ - nodeValue = "" + value - /*eslint-enable no-implicit-coercion*/ - }, - get nextSibling() { - if (this.parentNode == null) return null - var index = this.parentNode.childNodes.indexOf(this) - if (index < 0) throw new TypeError("Parent's childNodes is out of sync") - return this.parentNode.childNodes[index + 1] || null - }, - } - }, - createDocumentFragment: function() { - return { - ownerDocument: $window.document, - nodeType: 11, - nodeName: "#document-fragment", - appendChild: appendChild, - insertBefore: insertBefore, - removeChild: removeChild, - parentNode: null, - childNodes: [], - get firstChild() { - return this.childNodes[0] || null - }, - } + return new Text(text) }, createEvent: function() { return { @@ -780,17 +874,15 @@ export default function domMock(options) { initEvent: function(type) {this.type = type} } }, - get activeElement() {return activeElement}, + get activeElement() { + return activeElement + }, }, - } - $window.document.defaultView = $window - $window.document.documentElement = $window.document.createElement("html") - appendChild.call($window.document.documentElement, $window.document.createElement("head")) - $window.document.body = $window.document.createElement("body") - appendChild.call($window.document.documentElement, $window.document.body) - activeElement = $window.document.body + }) - if (options.spy) $window.__getSpies = getSpies + $window.document.documentElement = new Element("HTML", null) + $window.document.documentElement.appendChild($window.document.head = new Element("HEAD", null)) + $window.document.documentElement.appendChild($window.document.body = new Element("BODY", null)) - return $window + if (options.spy) $window.__getSpies = getSpies } diff --git a/test-utils/global.js b/test-utils/global.js index c86a5ef45..c69e778ed 100644 --- a/test-utils/global.js +++ b/test-utils/global.js @@ -1,45 +1,103 @@ -import {clearPending} from "./callAsync.js" +// Load order is important for the imports. /* global globalThis, window, global */ -export const G = ( + +import o from "ospec" + +import m from "../src/entry/mithril.esm.js" + +import browserMock from "./browserMock.js" +import {clearPending} from "./callAsync.js" +import throttleMocker from "./throttleMock.js" + +const G = ( typeof globalThis !== "undefined" ? globalThis : typeof window !== "undefined" ? window : global ) -const keys = [ - "window", - "document", - "requestAnimationFrame", - "setTimeout", - "clearTimeout", -] - -const original = keys.map((k) => G[k]) +const originalWindow = G.window +const originalDocument = G.document const originalConsoleError = console.error -export function injectGlobals($window, rafMock, throttleMock) { - if ($window) { - for (const k of keys) { - if ({}.hasOwnProperty.call($window, k)) G[k] = $window[k] - } - } - if (rafMock) { - G.requestAnimationFrame = rafMock.schedule - G.cancelAnimationFrame = rafMock.clear +export function restoreDOMGlobals() { + G.window = originalWindow + G.document = originalDocument +} + +export function setupGlobals(env = {}) { + let registeredRoots + /** @type {ReturnType} */ let $window + /** @type {ReturnType} */ let rafMock + + function register(root) { + registeredRoots.add(root) + return root } - if (throttleMock) { - G.setTimeout = throttleMock.schedule - G.clearTimeout = throttleMock.clear + + function initialize(env) { + $window = browserMock(env) + rafMock = throttleMocker() + registeredRoots = new Set([$window.document.body]) + + G.window = $window.window + G.document = $window.document + $window.requestAnimationFrame = rafMock.schedule + $window.cancelAnimationFrame = rafMock.clear + + if (env && env.expectNoConsoleError) { + console.error = (...args) => { + if (typeof process === "function") process.exitCode = 1 + console.trace("Unexpected `console.error` call") + originalConsoleError.apply(console, args) + } + } } -} -export function restoreDOMGlobals() { - for (let i = 0; i < keys.length; i++) G[keys[i]] = original[i] -} + o.beforeEach(() => { + initialize(Object.assign({}, env)) + return env.initialize && env.initialize() + }) -export function restoreGlobalState() { - restoreDOMGlobals() - clearPending() - console.error = originalConsoleError + o.afterEach(() => { + const errors = [] + const roots = registeredRoots + registeredRoots = null + for (const root of roots) { + try { + m.render(root, null) + } catch (e) { + errors.push(e) + } + } + var mock = rafMock + $window = null + rafMock = null + restoreDOMGlobals() + console.error = originalConsoleError + clearPending() + o(errors).deepEquals([]) + errors.length = 0 + o(mock.queueLength()).equals(0) + return env.cleanup && env.cleanup() + }) + + return { + initialize, + register, + + /** @returns {ReturnType} */ + get window() { + return $window + }, + + /** @returns {ReturnType} */ + get rafMock() { + return rafMock + }, + + get root() { + return $window.document.body + }, + } } diff --git a/test-utils/injectBrowserMock.js b/test-utils/injectBrowserMock.js index f6f2af58e..62d04f444 100644 --- a/test-utils/injectBrowserMock.js +++ b/test-utils/injectBrowserMock.js @@ -2,12 +2,9 @@ import browserMock from "../test-utils/browserMock.js" const mock = browserMock() -mock.setTimeout = setTimeout if (typeof global !== "undefined") { global.window = mock global.document = mock.document - global.requestAnimationFrame = mock.requestAnimationFrame - global.cancelAnimationFrame = mock.cancelAnimationFrame } export {mock as default} diff --git a/test-utils/pushStateMock.js b/test-utils/pushStateMock.js index 242ec4736..9f603dcbb 100644 --- a/test-utils/pushStateMock.js +++ b/test-utils/pushStateMock.js @@ -1,10 +1,9 @@ import {callAsync} from "../test-utils/callAsync.js" import parseURL from "../test-utils/parseURL.js" -export default function pushStateMock(options) { +export default function pushStateMock($window, options) { if (options == null) options = {} - var $window = options.window || {} var protocol = options.protocol || "http:" var hostname = options.hostname || "localhost" var port = "" @@ -175,8 +174,8 @@ export default function pushStateMock(options) { return past.length === 0 ? null : past[past.length - 1].state }, } - $window.onpopstate = null, - $window.onhashchange = null, + $window.onpopstate = null + $window.onhashchange = null $window.onunload = null $window.addEventListener = function (name, handler) { @@ -186,6 +185,4 @@ export default function pushStateMock(options) { $window.removeEventListener = function (name, handler) { $window["on" + name] = handler } - - return $window } diff --git a/test-utils/redraw-registry.js b/test-utils/redraw-registry.js deleted file mode 100644 index 5e9df8baa..000000000 --- a/test-utils/redraw-registry.js +++ /dev/null @@ -1,34 +0,0 @@ -// Load order is important for the imports. -/* eslint-disable sort-imports */ -import o from "ospec" -import * as global from "./global.js" -import m from "../src/entry/mithril.esm.js" - -let registeredRoots, currentRafMock, currentThrottleMock - -export function register(root) { - registeredRoots.add(root) - return root -} - -export function injectGlobals($window, rafMock, throttleMock) { - registeredRoots = new Set() - global.injectGlobals($window, rafMock, throttleMock) -} - -export function restoreGlobalState() { - const errors = [] - const roots = registeredRoots - registeredRoots = null - for (const root of roots) { - try { - m.mount(root, null) - } catch (e) { - errors.push(e) - } - } - global.restoreGlobalState() - o(errors).deepEquals([]) - if (currentRafMock) o(currentRafMock.queueLength()).equals(0) - if (currentThrottleMock) o(currentThrottleMock.queueLength()).equals(0) -} diff --git a/test-utils/throttleMock.js b/test-utils/throttleMock.js index b716ea768..247bd7485 100644 --- a/test-utils/throttleMock.js +++ b/test-utils/throttleMock.js @@ -12,7 +12,13 @@ export default function throttleMocker() { fire() { const tasks = queue queue = new Map() - for (const fn of tasks.values()) fn() + for (const fn of tasks.values()) { + try { + fn() + } catch (e) { + console.error(e) + } + } }, queueLength() { return queue.size diff --git a/test-utils/xhrMock.js b/test-utils/xhrMock.js deleted file mode 100644 index 77bfb5bce..000000000 --- a/test-utils/xhrMock.js +++ /dev/null @@ -1,138 +0,0 @@ -import {callAsync} from "../test-utils/callAsync.js" -import parseURL from "../test-utils/parseURL.js" - -export default function xhrMock() { - var routes = {} - // var callback = "callback" - var serverErrorHandler = function(url) { - return {status: 500, responseText: "server error, most likely the URL was not defined " + url} - } - - function FormData() {} - var $window = { - FormData: FormData, - URLSearchParams: URLSearchParams, - XMLHttpRequest: function XMLHttpRequest() { - var args = {} - var headers = {} - var aborted = false - this.setRequestHeader = function(header, value) { - /* - the behavior of setHeader is not your expected setX API. - If the header is already set, it'll merge with whatever you add - rather than overwrite - Source: https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/setRequestHeader - */ - if (headers[header]) { - headers[header] += ", " + value; - } else { - headers[header] = value - } - } - this.getRequestHeader = function(header) { - return headers[header] - } - this.open = function(method, url, async, user, password) { - var urlData = parseURL(url, {protocol: "http:", hostname: "localhost", port: "", pathname: "/"}) - args.rawUrl = url - args.method = method - args.pathname = urlData.pathname - args.search = urlData.search - args.async = async != null ? async : true - args.user = user - args.password = password - } - this.responseType = "" - this.response = null - this.timeout = 0 - Object.defineProperty(this, "responseText", {get: function() { - if (this.responseType === "" || this.responseType === "text") { - return this.response - } else { - throw new Error("Failed to read the 'responseText' property from 'XMLHttpRequest': The value is only accessible if the object's 'responseType' is '' or 'text' (was '" + this.responseType + "').") - } - }}) - this.send = function(body) { - var self = this - - var completeResponse = function (data) { - self._responseCompleted = true - if(!aborted) { - self.status = data.status - // Match spec - if (self.responseType === "json") { - try { self.response = JSON.parse(data.responseText) } - catch (e) { /* ignore */ } - } else { - self.response = data.responseText - } - } else { - self.status = 0 - } - self.readyState = 4 - if (args.async === true) { - callAsync(function() { - if (typeof self.onreadystatechange === "function") self.onreadystatechange({target: self}) - }) - } - } - - var data - if (!aborted) { - var handler = routes[args.method + " " + args.pathname] || serverErrorHandler.bind(null, args.pathname) - data = handler({rawUrl: args.rawUrl, url: args.pathname, query: args.search || {}, body: body || null}) - } - - if (typeof self.timeout === "number" && self.timeout > 0) { - setTimeout(function () { - if (self._responseCompleted) { - return - } - - self.status = 0; - if (typeof self.ontimeout === "function") self.ontimeout({target: self, type:"timeout"}) - }, self.timeout) - } - - if (data instanceof Promise) { - data.then(completeResponse) - } else { - completeResponse(data) - } - } - this.abort = function() { - aborted = true - } - }, - document: { - createElement: function(tag) { - return {nodeName: tag.toUpperCase(), parentNode: null} - }, - documentElement: { - appendChild: function(element) { - element.parentNode = this - if (element.nodeName === "SCRIPT") { - var urlData = parseURL(element.src, {protocol: "http:", hostname: "localhost", port: "", pathname: "/"}) - var handler = routes["GET " + urlData.pathname] || serverErrorHandler.bind(null, element.src) - var data = handler({url: urlData.pathname, query: urlData.search, body: null}) - callAsync(function() { - if (data.status === 200) { - new Function("$window", "with ($window) return " + data.responseText).call($window, $window) - } - else if (typeof element.onerror === "function") { - element.onerror({type: "error"}) - } - }) - } - }, - removeChild: function(element) { - element.parentNode = null - }, - }, - }, - $defineRoutes: function(rules) { - routes = rules - }, - } - return $window -} diff --git a/tests/api/mountRedraw.js b/tests/api/mountRedraw.js index ca264b2fa..cef72841d 100644 --- a/tests/api/mountRedraw.js +++ b/tests/api/mountRedraw.js @@ -1,73 +1,49 @@ import o from "ospec" -import {injectGlobals, register, restoreGlobalState} from "../../test-utils/redraw-registry.js" +import {setupGlobals} from "../../test-utils/global.js" import m from "../../src/entry/mithril.esm.js" -import domMock from "../../test-utils/domMock.js" -import throttleMocker from "../../test-utils/throttleMock.js" - o.spec("mount/redraw", function() { - let $window, throttleMock - - o.beforeEach(() => { - $window = domMock() - throttleMock = throttleMocker() - injectGlobals($window, throttleMock) - console.error = o.spy() - }) - - o.afterEach(restoreGlobalState) - - o("shouldn't error if there are no renderers", function() { - console.error = o.spy() - - m.redraw() - throttleMock.fire() - - o(console.error.calls.map((c) => c.args[0])).deepEquals([]) - o(throttleMock.queueLength()).equals(0) + var G = setupGlobals({ + initialize() { console.error = o.spy() }, }) o("schedules correctly", function() { - var root = register($window.document.body) - var spy = o.spy() - m.mount(root, spy) + var redraw = m.mount(G.root, spy) o(spy.callCount).equals(1) - m.redraw() + redraw() o(spy.callCount).equals(1) - throttleMock.fire() + G.rafMock.fire() o(spy.callCount).equals(2) o(console.error.calls.map((c) => c.args[0])).deepEquals([]) - o(throttleMock.queueLength()).equals(0) + o(G.rafMock.queueLength()).equals(0) }) o("should run a single renderer entry", function() { - var root = register($window.document.body) - var spy = o.spy() - m.mount(root, spy) + var redraw = m.mount(G.root, spy) o(spy.callCount).equals(1) - m.redraw() - m.redraw() - m.redraw() + redraw() + redraw() + redraw() o(spy.callCount).equals(1) - throttleMock.fire() + G.rafMock.fire() o(spy.callCount).equals(2) o(console.error.calls.map((c) => c.args[0])).deepEquals([]) - o(throttleMock.queueLength()).equals(0) + o(G.rafMock.queueLength()).equals(0) }) - o("should run all renderer entries", function() { - var $document = $window.document + o("`redraw()` schedules independent handles independently", function() { + var $document = G.window.document var el1 = $document.createElement("div") var el2 = $document.createElement("div") @@ -76,34 +52,38 @@ o.spec("mount/redraw", function() { var spy2 = o.spy() var spy3 = o.spy() - m.mount(el1, spy1) - m.mount(el2, spy2) - m.mount(el3, spy3) + var redraw1 = m.mount(el1, spy1) + var redraw2 = m.mount(el2, spy2) + var redraw3 = m.mount(el3, spy3) - m.redraw() + redraw1() + redraw2() + redraw3() o(spy1.callCount).equals(1) o(spy2.callCount).equals(1) o(spy3.callCount).equals(1) - m.redraw() + redraw1() + redraw2() + redraw3() o(spy1.callCount).equals(1) o(spy2.callCount).equals(1) o(spy3.callCount).equals(1) - throttleMock.fire() + G.rafMock.fire() o(spy1.callCount).equals(2) o(spy2.callCount).equals(2) o(spy3.callCount).equals(2) o(console.error.calls.map((c) => c.args[0])).deepEquals([]) - o(throttleMock.queueLength()).equals(0) + o(G.rafMock.queueLength()).equals(0) }) o("should not redraw when mounting another root", function() { - var $document = $window.document + var $document = G.window.document var el1 = $document.createElement("div") var el2 = $document.createElement("div") @@ -128,120 +108,42 @@ o.spec("mount/redraw", function() { o(spy3.callCount).equals(1) o(console.error.calls.map((c) => c.args[0])).deepEquals([]) - o(throttleMock.queueLength()).equals(0) - }) - - o("should stop running after mount null", function() { - var root = register($window.document.body) - - var spy = o.spy() - - m.mount(root, 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(console.error.calls.map((c) => c.args[0])).deepEquals([]) - o(throttleMock.queueLength()).equals(0) - }) - - o("should stop running after mount undefined", function() { - var root = register($window.document.body) - - var spy = o.spy() - - m.mount(root, 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(console.error.calls.map((c) => c.args[0])).deepEquals([]) - o(throttleMock.queueLength()).equals(0) - }) - - o("should stop running after mount no arg", function() { - var root = register($window.document.body) - - var spy = o.spy() - - m.mount(root, spy) - o(spy.callCount).equals(1) - m.mount(root) - - m.redraw() - - o(spy.callCount).equals(1) - throttleMock.fire() - o(spy.callCount).equals(1) - - o(console.error.calls.map((c) => c.args[0])).deepEquals([]) - o(throttleMock.queueLength()).equals(0) + o(G.rafMock.queueLength()).equals(0) }) o("should invoke remove callback on unmount", function() { - var root = register($window.document.body) - var onabort = o.spy() var spy = o.spy(() => m.layout((_, signal) => { signal.onabort = onabort })) - m.mount(root, spy) + m.mount(G.root, spy) o(spy.callCount).equals(1) - m.mount(root) + m.render(G.root, null) o(spy.callCount).equals(1) o(onabort.callCount).equals(1) o(console.error.calls.map((c) => c.args[0])).deepEquals([]) - o(throttleMock.queueLength()).equals(0) + o(G.rafMock.queueLength()).equals(0) }) o("should stop running after unsubscribe, even if it occurs after redraw is requested", function() { - var root = register($window.document.body) - var spy = o.spy() - m.mount(root, spy) + var redraw = m.mount(G.root, spy) o(spy.callCount).equals(1) - m.redraw() - m.mount(root) + redraw() + m.render(G.root, null) o(spy.callCount).equals(1) - throttleMock.fire() - o(spy.callCount).equals(1) - - o(console.error.calls.map((c) => c.args[0])).deepEquals([]) - o(throttleMock.queueLength()).equals(0) - }) - - o("throws invalid unmount", function() { - var root = register($window.document.body) - - var spy = o.spy() - - m.mount(root, spy) + G.rafMock.fire() o(spy.callCount).equals(1) - o(() => m.mount(null)).throws(Error) - m.redraw() - throttleMock.fire() - o(spy.callCount).equals(2) - o(console.error.calls.map((c) => c.args[0])).deepEquals([]) - o(throttleMock.queueLength()).equals(0) + o(G.rafMock.queueLength()).equals(0) }) - o("redraw.sync() redraws all roots synchronously", function() { - var $document = $window.document + o("`redraw.sync()` redraws independent roots synchronously", function() { + var $document = G.window.document var el1 = $document.createElement("div") var el2 = $document.createElement("div") @@ -250,178 +152,157 @@ o.spec("mount/redraw", function() { var spy2 = o.spy() var spy3 = o.spy() - m.mount(el1, spy1) - m.mount(el2, spy2) - m.mount(el3, spy3) + var redraw1 = m.mount(el1, spy1) + var redraw2 = m.mount(el2, spy2) + var redraw3 = m.mount(el3, spy3) o(spy1.callCount).equals(1) o(spy2.callCount).equals(1) o(spy3.callCount).equals(1) - m.redrawSync() + redraw1.sync() + redraw2.sync() + redraw3.sync() o(spy1.callCount).equals(2) o(spy2.callCount).equals(2) o(spy3.callCount).equals(2) - m.redrawSync() + redraw1.sync() + redraw2.sync() + redraw3.sync() o(spy1.callCount).equals(3) o(spy2.callCount).equals(3) o(spy3.callCount).equals(3) o(console.error.calls.map((c) => c.args[0])).deepEquals([]) - o(throttleMock.queueLength()).equals(0) + o(G.rafMock.queueLength()).equals(0) }) - o("throws on invalid view", function() { - var root = register($window.document.body) - - o(function() { m.mount(root, {}) }).throws(TypeError) - - o(console.error.calls.map((c) => c.args[0])).deepEquals([]) - o(throttleMock.queueLength()).equals(0) - }) - - o("skips roots that were synchronously unsubscribed before they were visited", function() { - var $document = $window.document - - var calls = [] - var root1 =register($document.createElement("div")) - var root2 =register($document.createElement("div")) - var root3 =register($document.createElement("div")) - - m.mount(root1, () => m.layout((_, __, isInit) => { - if (!isInit) m.mount(root2, null) - calls.push("root1") - })) - m.mount(root2, () => { calls.push("root2") }) - m.mount(root3, () => { calls.push("root3") }) - o(calls).deepEquals([ - "root1", "root2", "root3", - ]) - - m.redrawSync() - o(calls).deepEquals([ - "root1", "root2", "root3", - "root1", "root3", - ]) + o(function() { m.mount(G.root, {}) }).throws(TypeError) o(console.error.calls.map((c) => c.args[0])).deepEquals([]) - o(throttleMock.queueLength()).equals(0) + o(G.rafMock.queueLength()).equals(0) }) o("keeps its place when synchronously unsubscribing previously visited roots", function() { - var $document = $window.document + var $document = G.window.document var calls = [] - var root1 =register($document.createElement("div")) - var root2 =register($document.createElement("div")) - var root3 =register($document.createElement("div")) + var root1 = G.register($document.createElement("div")) + var root2 = G.register($document.createElement("div")) + var root3 = G.register($document.createElement("div")) - m.mount(root1, () => { calls.push("root1") }) - m.mount(root2, () => m.layout((_, __, isInit) => { - if (!isInit) m.mount(root1, null) + var redraw1 = m.mount(root1, () => { calls.push("root1") }) + var redraw2 = m.mount(root2, (isInit) => { + if (!isInit) m.render(root1, null) calls.push("root2") - })) - m.mount(root3, () => { calls.push("root3") }) + }) + var redraw3 = m.mount(root3, () => { calls.push("root3") }) o(calls).deepEquals([ "root1", "root2", "root3", ]) - m.redrawSync() + redraw1.sync() + redraw2.sync() + redraw3.sync() o(calls).deepEquals([ "root1", "root2", "root3", "root1", "root2", "root3", ]) o(console.error.calls.map((c) => c.args[0])).deepEquals([]) - o(throttleMock.queueLength()).equals(0) + o(G.rafMock.queueLength()).equals(0) }) - o("keeps its place when synchronously unsubscribing previously visited roots in the face of []", function() { - var $document = $window.document + o("keeps its place when synchronously unsubscribing previously visited roots in the face of events", function() { + var $document = G.window.document var calls = [] - var root1 =register($document.createElement("div")) - var root2 =register($document.createElement("div")) - var root3 =register($document.createElement("div")) + var root1 = G.register($document.createElement("div")) + var root2 = G.register($document.createElement("div")) + var root3 = G.register($document.createElement("div")) - m.mount(root1, () => { calls.push("root1") }) - m.mount(root2, () => m.layout((_, __, isInit) => { - if (!isInit) { m.mount(root1, null); throw "fail" } + var redraw1 = m.mount(root1, () => { calls.push("root1") }) + var redraw2 = m.mount(root2, (isInit) => { + if (!isInit) { m.render(root1, null); throw new Error("fail") } calls.push("root2") - })) - m.mount(root3, () => { calls.push("root3") }) + }) + var redraw3 = m.mount(root3, () => { calls.push("root3") }) o(calls).deepEquals([ "root1", "root2", "root3", ]) - m.redrawSync() + redraw1.sync() + o(() => redraw2.sync()).throws("fail") + redraw3.sync() o(calls).deepEquals([ "root1", "root2", "root3", "root1", "root3", ]) - o(console.error.calls.map((c) => c.args[0])).deepEquals(["fail"]) - o(throttleMock.queueLength()).equals(0) + o(console.error.calls.map((c) => c.args[0])).deepEquals([]) + o(G.rafMock.queueLength()).equals(0) }) o("keeps its place when synchronously unsubscribing the current root", function() { - var $document = $window.document + var $document = G.window.document var calls = [] - var root1 =register($document.createElement("div")) - var root2 =register($document.createElement("div")) - var root3 =register($document.createElement("div")) + var root1 = G.register($document.createElement("div")) + var root2 = G.register($document.createElement("div")) + var root3 = G.register($document.createElement("div")) - m.mount(root1, () => { calls.push("root1") }) - m.mount(root2, () => m.layout((_, __, isInit) => { - if (!isInit) try { m.mount(root2, null) } catch (e) { calls.push([e.constructor, e.message]) } + var redraw1 = m.mount(root1, () => { calls.push("root1") }) + var redraw2 = m.mount(root2, (isInit) => { + if (!isInit) m.render(root2, null) calls.push("root2") - })) - m.mount(root3, () => { calls.push("root3") }) + }) + var redraw3 = m.mount(root3, () => { calls.push("root3") }) o(calls).deepEquals([ "root1", "root2", "root3", ]) - m.redrawSync() + redraw1.sync() + o(() => redraw2.sync()).throws(TypeError) + redraw3.sync() o(calls).deepEquals([ "root1", "root2", "root3", - "root1", [TypeError, "Node is currently being rendered to and thus is locked."], "root2", "root3", + "root1", "root3", ]) o(console.error.calls.map((c) => c.args[0])).deepEquals([]) - o(throttleMock.queueLength()).equals(0) + o(G.rafMock.queueLength()).equals(0) }) o("keeps its place when synchronously unsubscribing the current root in the face of an error", function() { - var $document = $window.document + var $document = G.window.document var calls = [] - var root1 =register($document.createElement("div")) - var root2 =register($document.createElement("div")) - var root3 =register($document.createElement("div")) + var root1 = G.register($document.createElement("div")) + var root2 = G.register($document.createElement("div")) + var root3 = G.register($document.createElement("div")) - m.mount(root1, () => { calls.push("root1") }) - m.mount(root2, () => m.layout((_, __, isInit) => { - if (!isInit) try { m.mount(root2, null) } catch (e) { throw [e.constructor, e.message] } + var redraw1 = m.mount(root1, () => { calls.push("root1") }) + var redraw2 = m.mount(root2, (isInit) => { + if (!isInit) m.render(root2, null) calls.push("root2") - })) - m.mount(root3, () => { calls.push("root3") }) + }) + var redraw3 = m.mount(root3, () => { calls.push("root3") }) o(calls).deepEquals([ "root1", "root2", "root3", ]) - m.redrawSync() + redraw1.sync() + o(() => redraw2.sync()).throws(TypeError) + redraw3.sync() o(calls).deepEquals([ "root1", "root2", "root3", "root1", "root3", ]) - o(console.error.calls.map((c) => c.args[0])).deepEquals([ - [TypeError, "Node is currently being rendered to and thus is locked."], - ]) - o(throttleMock.queueLength()).equals(0) + o(console.error.calls.map((c) => c.args[0])).deepEquals([]) + o(G.rafMock.queueLength()).equals(0) }) o("throws on invalid `root` DOM node", function() { @@ -430,38 +311,34 @@ o.spec("mount/redraw", function() { }).throws(TypeError) o(console.error.calls.map((c) => c.args[0])).deepEquals([]) - o(throttleMock.queueLength()).equals(0) + o(G.rafMock.queueLength()).equals(0) }) o("renders into `root` synchronously", function() { - var root = register($window.document.body) - - m.mount(root, () => m("div")) + m.mount(G.root, () => m("div")) - o(root.firstChild.nodeName).equals("DIV") + o(G.root.firstChild.nodeName).equals("DIV") o(console.error.calls.map((c) => c.args[0])).deepEquals([]) - o(throttleMock.queueLength()).equals(0) + o(G.rafMock.queueLength()).equals(0) }) o("mounting null unmounts", function() { - var root = register($window.document.body) + m.mount(G.root, () => m("div")) - m.mount(root, () => m("div")) + m.render(G.root, null) - m.mount(root, null) - - o(root.childNodes.length).equals(0) + o(G.root.childNodes.length).equals(0) o(console.error.calls.map((c) => c.args[0])).deepEquals([]) - o(throttleMock.queueLength()).equals(0) + o(G.rafMock.queueLength()).equals(0) }) o("Mounting a second root doesn't cause the first one to redraw", function() { - var $document = $window.document + var $document = G.window.document - var root1 =register($document.createElement("div")) - var root2 =register($document.createElement("div")) + var root1 = G.register($document.createElement("div")) + var root2 = G.register($document.createElement("div")) var view = o.spy() m.mount(root1, view) @@ -471,16 +348,15 @@ o.spec("mount/redraw", function() { o(view.callCount).equals(1) - throttleMock.fire() + G.rafMock.fire() o(view.callCount).equals(1) o(console.error.calls.map((c) => c.args[0])).deepEquals([]) - o(throttleMock.queueLength()).equals(0) + o(G.rafMock.queueLength()).equals(0) }) o("redraws on events", function() { - var root = register($window.document.body) - var $document = $window.document + var $document = G.window.document var layout = o.spy() var onclick = o.spy() @@ -488,126 +364,177 @@ o.spec("mount/redraw", function() { e.initEvent("click", true, true) - m.mount(root, () => m("div", { + m.mount(G.root, () => m("div", { onclick: onclick, - }, m.layout(layout))) + }, m.layout(() => layout(true), () => layout(false)))) - root.firstChild.dispatchEvent(e) + G.root.firstChild.dispatchEvent(e) - o(layout.calls.map((c) => c.args[2])).deepEquals([true]) + o(layout.calls.map((c) => c.args[0])).deepEquals([true]) o(onclick.callCount).equals(1) - o(onclick.this).equals(root.firstChild) + o(onclick.this).equals(G.root.firstChild) o(onclick.args[0].type).equals("click") - o(onclick.args[0].target).equals(root.firstChild) + o(onclick.args[0].target).equals(G.root.firstChild) - throttleMock.fire() + G.rafMock.fire() - o(layout.calls.map((c) => c.args[2])).deepEquals([true, false]) + o(layout.calls.map((c) => c.args[0])).deepEquals([true, false]) o(console.error.calls.map((c) => c.args[0])).deepEquals([]) - o(throttleMock.queueLength()).equals(0) + o(G.rafMock.queueLength()).equals(0) }) - o("redraws several mount points on events", function() { - var $document = $window.document + o("redraws only parent mount point on events", function() { + var $document = G.window.document var layout0 = o.spy() var onclick0 = o.spy() var layout1 = o.spy() var onclick1 = o.spy() - var root1 =register($document.createElement("div")) - var root2 =register($document.createElement("div")) + var root1 = G.register($document.createElement("div")) + var root2 = G.register($document.createElement("div")) var e = $document.createEvent("MouseEvents") e.initEvent("click", true, true) m.mount(root1, () => m("div", { onclick: onclick0, - }, m.layout(layout0))) + }, m.layout(() => layout0(true), () => layout0(false)))) - o(layout0.calls.map((c) => c.args[2])).deepEquals([true]) + o(layout0.calls.map((c) => c.args[0])).deepEquals([true]) m.mount(root2, () => m("div", { onclick: onclick1, - }, m.layout(layout1))) + }, m.layout(() => layout1(true), () => layout1(false)))) - o(layout1.calls.map((c) => c.args[2])).deepEquals([true]) + o(layout1.calls.map((c) => c.args[0])).deepEquals([true]) root1.firstChild.dispatchEvent(e) o(onclick0.callCount).equals(1) o(onclick0.this).equals(root1.firstChild) - throttleMock.fire() + G.rafMock.fire() - o(layout0.calls.map((c) => c.args[2])).deepEquals([true, false]) - o(layout1.calls.map((c) => c.args[2])).deepEquals([true, false]) + o(layout0.calls.map((c) => c.args[0])).deepEquals([true, false]) + o(layout1.calls.map((c) => c.args[0])).deepEquals([true]) root2.firstChild.dispatchEvent(e) o(onclick1.callCount).equals(1) o(onclick1.this).equals(root2.firstChild) - throttleMock.fire() + G.rafMock.fire() - o(layout0.calls.map((c) => c.args[2])).deepEquals([true, false, false]) - o(layout1.calls.map((c) => c.args[2])).deepEquals([true, false, false]) + o(layout0.calls.map((c) => c.args[0])).deepEquals([true, false]) + o(layout1.calls.map((c) => c.args[0])).deepEquals([true, false]) o(console.error.calls.map((c) => c.args[0])).deepEquals([]) - o(throttleMock.queueLength()).equals(0) + o(G.rafMock.queueLength()).equals(0) }) o("event handlers can skip redraw", function() { - var root = register($window.document.body) - var $document = $window.document + var $document = G.window.document var layout = o.spy() var e = $document.createEvent("MouseEvents") e.initEvent("click", true, true) - m.mount(root, () => m("div", { + m.mount(G.root, () => m("div", { onclick: () => false, - }, m.layout(layout))) + }, m.layout(() => layout(true), () => layout(false)))) - root.firstChild.dispatchEvent(e) + G.root.firstChild.dispatchEvent(e) - o(layout.calls.map((c) => c.args[2])).deepEquals([true]) + o(layout.calls.map((c) => c.args[0])).deepEquals([true]) - throttleMock.fire() + G.rafMock.fire() - o(layout.calls.map((c) => c.args[2])).deepEquals([true]) + o(layout.calls.map((c) => c.args[0])).deepEquals([true]) o(console.error.calls.map((c) => c.args[0])).deepEquals([]) - o(throttleMock.queueLength()).equals(0) + o(G.rafMock.queueLength()).equals(0) }) o("redraws when the render function is run", function() { - var root = register($window.document.body) - var layout = o.spy() - m.mount(root, () => m("div", m.layout(layout))) + var redraw = m.mount(G.root, () => m("div", m.layout(() => layout(true), () => layout(false)))) + + o(layout.calls.map((c) => c.args[0])).deepEquals([true]) + + redraw() - o(layout.calls.map((c) => c.args[2])).deepEquals([true]) + G.rafMock.fire() - m.redraw() + o(layout.calls.map((c) => c.args[0])).deepEquals([true, false]) - throttleMock.fire() + o(console.error.calls.map((c) => c.args[0])).deepEquals([]) + o(G.rafMock.queueLength()).equals(0) + }) + + o("remounts after `m.render(G.root, null)` is invoked on the mounted root", function() { + var onabort = o.spy() + var onCreate = o.spy((_, signal) => { signal.onabort = onabort }) + var onUpdate = o.spy((_, signal) => { signal.onabort = onabort }) - o(layout.calls.map((c) => c.args[2])).deepEquals([true, false]) + var redraw = m.mount(G.root, () => m("div", m.layout(onCreate, onUpdate))) + + o(onCreate.callCount).equals(1) + o(onUpdate.callCount).equals(0) + o(onabort.callCount).equals(0) + + m.render(G.root, null) + o(onCreate.callCount).equals(1) + o(onUpdate.callCount).equals(0) + o(onabort.callCount).equals(1) + + redraw() + + G.rafMock.fire() + + o(onCreate.callCount).equals(2) + o(onUpdate.callCount).equals(0) + o(onabort.callCount).equals(1) o(console.error.calls.map((c) => c.args[0])).deepEquals([]) - o(throttleMock.queueLength()).equals(0) + o(G.rafMock.queueLength()).equals(0) + }) + + o("propagates mount errors synchronously", function() { + o(() => m.mount(G.root, () => { throw new Error("foo") })).throws("foo") + }) + + o("propagates redraw errors synchronously", function() { + var counter = 0 + + var redraw = m.mount(G.root, () => { + switch (++counter) { + case 1: return null + case 2: throw new Error("foo") + case 3: throw new Error("bar") + case 4: throw new Error("baz") + default: return null + } + }) + + o(() => redraw.sync()).throws("foo") + o(() => redraw.sync()).throws("bar") + o(() => redraw.sync()).throws("baz") + + o(counter).equals(4) + o(console.error.calls.map((c) => c.args[0])).deepEquals([]) + o(G.rafMock.queueLength()).equals(0) }) - o("emits errors correctly", function() { - var root = register($window.document.body) + o("lets redraw errors fall through to the scheduler", function() { var counter = 0 - m.mount(root, () => { + var redraw = m.mount(G.root, () => { switch (++counter) { + case 1: return null case 2: throw "foo" case 3: throw "bar" case 4: throw "baz" @@ -615,15 +542,15 @@ o.spec("mount/redraw", function() { } }) - m.redraw() - throttleMock.fire() - m.redraw() - throttleMock.fire() - m.redraw() - throttleMock.fire() + redraw() + G.rafMock.fire() + redraw() + G.rafMock.fire() + redraw() + G.rafMock.fire() o(counter).equals(4) o(console.error.calls.map((c) => c.args[0])).deepEquals(["foo", "bar", "baz"]) - o(throttleMock.queueLength()).equals(0) + o(G.rafMock.queueLength()).equals(0) }) }) diff --git a/tests/api/router.js b/tests/api/router.js index badf90b37..a23fbb245 100644 --- a/tests/api/router.js +++ b/tests/api/router.js @@ -1,13 +1,9 @@ import o from "ospec" -import {injectGlobals, register, restoreGlobalState} from "../../test-utils/redraw-registry.js" -import {restoreDOMGlobals} from "../../test-utils/global.js" +import {restoreDOMGlobals, setupGlobals} from "../../test-utils/global.js" import m from "../../src/entry/mithril.esm.js" -import browserMock from "../../test-utils/browserMock.js" -import throttleMocker from "../../test-utils/throttleMock.js" - o.spec("route", () => { void [{protocol: "http:", hostname: "localhost"}, {protocol: "file:", hostname: "/"}, {protocol: "http:", hostname: "ööö"}].forEach((env) => { void ["#", "?", "", "#!", "?!", "/foo", "/föö"].forEach((prefix) => { @@ -15,65 +11,51 @@ o.spec("route", () => { var fullHost = `${env.protocol}//${env.hostname === "/" ? "" : env.hostname}` var fullPrefix = `${fullHost}${prefix[0] === "/" ? "" : "/"}${prefix ? `${prefix}/` : ""}` - var $window, root, throttleMock - - o.beforeEach(() => { - $window = browserMock(env) - throttleMock = throttleMocker() - injectGlobals($window, throttleMock) - root = register($window.document.body) - var realError = console.error - console.error = function () { - realError.call(this, new Error("Unexpected `console.error` call")) - realError.apply(this, arguments) - } - }) - - o.afterEach(restoreGlobalState) + var G = setupGlobals(Object.assign({}, env, {expectNoConsoleError: true})) o("returns the right route on init", () => { - $window.location.href = `${prefix}/` + G.window.location.href = `${prefix}/` m.route.init(prefix) o(m.route.path).equals("/") o([...m.route.params]).deepEquals([]) - o(throttleMock.queueLength()).equals(0) + o(G.rafMock.queueLength()).equals(0) }) o("returns alternate right route on init", () => { - $window.location.href = `${prefix}/test` + G.window.location.href = `${prefix}/test` m.route.init(prefix) o(m.route.path).equals("/test") o([...m.route.params]).deepEquals([]) - o(throttleMock.queueLength()).equals(0) + o(G.rafMock.queueLength()).equals(0) }) o("returns right route on init with escaped unicode", () => { - $window.location.href = `${prefix}/%C3%B6?%C3%B6=%C3%B6` + G.window.location.href = `${prefix}/%C3%B6?%C3%B6=%C3%B6` m.route.init(prefix) o(m.route.path).equals("/ö") o([...m.route.params]).deepEquals([["ö", "ö"]]) - o(throttleMock.queueLength()).equals(0) + o(G.rafMock.queueLength()).equals(0) }) o("returns right route on init with unescaped unicode", () => { - $window.location.href = `${prefix}/ö?ö=ö` + G.window.location.href = `${prefix}/ö?ö=ö` m.route.init(prefix) o(m.route.path).equals("/ö") o([...m.route.params]).deepEquals([["ö", "ö"]]) - o(throttleMock.queueLength()).equals(0) + o(G.rafMock.queueLength()).equals(0) }) o("sets path asynchronously", async () => { - $window.location.href = `${prefix}/a` + G.window.location.href = `${prefix}/a` var spy1 = o.spy() var spy2 = o.spy() - m.route.init(prefix) - m.mount(root, () => { + m.mount(G.root, (isInit, redraw) => { + if (isInit) m.route.init(prefix, redraw) if (m.route.path === "/a") { spy1() } else if (m.route.path === "/b") { @@ -90,64 +72,65 @@ o.spec("route", () => { o(spy2.callCount).equals(0) await Promise.resolve() - throttleMock.fire() + G.rafMock.fire() o(spy1.callCount).equals(1) o(spy2.callCount).equals(1) - o(throttleMock.queueLength()).equals(0) + o(G.rafMock.queueLength()).equals(0) }) o("sets route via pushState/onpopstate", async () => { - $window.location.href = `${prefix}/test` + G.window.location.href = `${prefix}/test` m.route.init(prefix) await Promise.resolve() - throttleMock.fire() + G.rafMock.fire() - $window.history.pushState(null, null, `${prefix}/other/x/y/z?c=d#e=f`) - $window.onpopstate() + G.window.history.pushState(null, null, `${prefix}/other/x/y/z?c=d#e=f`) + G.window.onpopstate() await Promise.resolve() - throttleMock.fire() + G.rafMock.fire() // Yep, before even the throttle mechanism takes hold. o(m.route.get()).equals("/other/x/y/z?c=d#e=f") await Promise.resolve() - throttleMock.fire() + G.rafMock.fire() - o(throttleMock.queueLength()).equals(0) + o(G.rafMock.queueLength()).equals(0) }) o("`replace: true` works", async () => { - $window.location.href = `${prefix}/test` + G.window.location.href = `${prefix}/test` m.route.init(prefix) m.route.set("/other", {replace: true}) await Promise.resolve() - throttleMock.fire() + G.rafMock.fire() - $window.history.back() - o($window.location.href).equals(`${fullHost}/`) + G.window.history.back() + o(G.window.location.href).equals(`${fullHost}/`) await Promise.resolve() - throttleMock.fire() + G.rafMock.fire() - o($window.location.href).equals(`${fullHost}/`) - o(throttleMock.queueLength()).equals(0) + o(G.window.location.href).equals(`${fullHost}/`) + o(G.rafMock.queueLength()).equals(0) }) o("`replace: true` works in links", async () => { - $window.location.href = `${prefix}/test` + G.window.location.href = `${prefix}/test` m.route.init(prefix) - var e = $window.document.createEvent("MouseEvents") + var e = G.window.document.createEvent("MouseEvents") e.initEvent("click", true, true) e.button = 0 - m.mount(root, () => { + m.mount(G.root, (isInit, redraw) => { + if (isInit) m.route.init(prefix, redraw) if (m.route.path === "/test") { return m("a", m.route.link({href: "/other", replace: true})) } else if (m.route.path === "/other") { @@ -159,50 +142,50 @@ o.spec("route", () => { } }) - root.firstChild.dispatchEvent(e) + G.root.firstChild.dispatchEvent(e) await Promise.resolve() - throttleMock.fire() + G.rafMock.fire() - $window.history.back() - o($window.location.href).equals(`${fullHost}/`) + G.window.history.back() + o(G.window.location.href).equals(`${fullHost}/`) await Promise.resolve() - throttleMock.fire() + G.rafMock.fire() - o($window.location.href).equals(`${fullHost}/`) - o(throttleMock.queueLength()).equals(0) + o(G.window.location.href).equals(`${fullHost}/`) + o(G.rafMock.queueLength()).equals(0) }) o("`replace: false` works", async () => { - $window.location.href = `${prefix}/test` + G.window.location.href = `${prefix}/test` m.route.init(prefix) m.route.set("/other", {replace: false}) await Promise.resolve() - throttleMock.fire() + G.rafMock.fire() - $window.history.back() - o($window.location.href).equals(`${fullPrefix}test`) + G.window.history.back() + o(G.window.location.href).equals(`${fullPrefix}test`) await Promise.resolve() - throttleMock.fire() + G.rafMock.fire() - o($window.location.href).equals(`${fullPrefix}test`) - o(throttleMock.queueLength()).equals(0) + o(G.window.location.href).equals(`${fullPrefix}test`) + o(G.rafMock.queueLength()).equals(0) }) o("`replace: false` works in links", async () => { - $window.location.href = `${prefix}/test` - m.route.init(prefix) + G.window.location.href = `${prefix}/test` - var e = $window.document.createEvent("MouseEvents") + var e = G.window.document.createEvent("MouseEvents") e.initEvent("click", true, true) e.button = 0 - m.mount(root, () => { + m.mount(G.root, (isInit, redraw) => { + if (isInit) m.route.init(prefix, redraw) if (m.route.path === "/test") { return m("a", m.route.link({href: "/other", replace: false})) } else if (m.route.path === "/other") { @@ -212,74 +195,74 @@ o.spec("route", () => { } }) - root.firstChild.dispatchEvent(e) + G.root.firstChild.dispatchEvent(e) await Promise.resolve() - throttleMock.fire() + G.rafMock.fire() - $window.history.back() - o($window.location.href).equals(`${fullPrefix}test`) + G.window.history.back() + o(G.window.location.href).equals(`${fullPrefix}test`) await Promise.resolve() - throttleMock.fire() + G.rafMock.fire() - o($window.location.href).equals(`${fullPrefix}test`) - o(throttleMock.queueLength()).equals(0) + o(G.window.location.href).equals(`${fullPrefix}test`) + o(G.rafMock.queueLength()).equals(0) }) o("state works", async () => { - $window.location.href = `${prefix}/test` + G.window.location.href = `${prefix}/test` m.route.init(prefix) m.route.set("/other", {state: {a: 1}}) await Promise.resolve() - throttleMock.fire() + G.rafMock.fire() - o($window.history.state).deepEquals({a: 1}) - o(throttleMock.queueLength()).equals(0) + o(G.window.history.state).deepEquals({a: 1}) + o(G.rafMock.queueLength()).equals(0) }) o("adds trailing slash where needed", () => { - $window.location.href = `${prefix}/test` + G.window.location.href = `${prefix}/test` m.route.init(`${prefix}/`) o(m.route.path).equals("/test") o([...m.route.params]).deepEquals([]) - o(throttleMock.queueLength()).equals(0) + o(G.rafMock.queueLength()).equals(0) }) o("handles route with search", () => { - $window.location.href = `${prefix}/test?a=b&c=d` + G.window.location.href = `${prefix}/test?a=b&c=d` m.route.init(prefix) o(m.route.path).equals("/test") o([...m.route.params]).deepEquals([["a", "b"], ["c", "d"]]) - o(throttleMock.queueLength()).equals(0) + o(G.rafMock.queueLength()).equals(0) }) o("reacts to back button", () => { - $window.location.href = "http://old.com" - $window.location.href = "http://new.com" + G.window.location.href = "http://old.com" + G.window.location.href = "http://new.com" m.route.init(prefix) - $window.history.back() + G.window.history.back() - o($window.location.pathname).equals("/") - o($window.location.hostname).equals("old.com") - o(throttleMock.queueLength()).equals(0) + o(G.window.location.pathname).equals("/") + o(G.window.location.hostname).equals("old.com") + o(G.rafMock.queueLength()).equals(0) }) o("changes location on route.Link", async () => { - var e = $window.document.createEvent("MouseEvents") + var e = G.window.document.createEvent("MouseEvents") e.initEvent("click", true, true) e.button = 0 - $window.location.href = `${prefix}/` - m.route.init(prefix) - m.mount(root, () => { + G.window.location.href = `${prefix}/` + m.mount(G.root, (isInit, redraw) => { + if (isInit) m.route.init(prefix, redraw) if (m.route.path === "/") { return m("a", m.route.link({href: "/test"})) } else if (m.route.path === "/test") { @@ -289,25 +272,25 @@ o.spec("route", () => { } }) - o($window.location.href).equals(fullPrefix) + o(G.window.location.href).equals(fullPrefix) - root.firstChild.dispatchEvent(e) + G.root.firstChild.dispatchEvent(e) await Promise.resolve() - throttleMock.fire() + G.rafMock.fire() - o($window.location.href).equals(`${fullPrefix}test`) - o(throttleMock.queueLength()).equals(0) + o(G.window.location.href).equals(`${fullPrefix}test`) + o(G.rafMock.queueLength()).equals(0) }) o("passes state on route.Link", async () => { - var e = $window.document.createEvent("MouseEvents") + var e = G.window.document.createEvent("MouseEvents") e.initEvent("click", true, true) e.button = 0 - $window.location.href = `${prefix}/` - m.route.init(prefix) - m.mount(root, () => { + G.window.location.href = `${prefix}/` + m.mount(G.root, (isInit, redraw) => { + if (isInit) m.route.init(prefix, redraw) if (m.route.path === "/") { return m("a", m.route.link({href: "/test", state: {a: 1}})) } else if (m.route.path === "/test") { @@ -317,18 +300,18 @@ o.spec("route", () => { } }) - root.firstChild.dispatchEvent(e) + G.root.firstChild.dispatchEvent(e) await Promise.resolve() - throttleMock.fire() + G.rafMock.fire() - o($window.history.state).deepEquals({a: 1}) - o(throttleMock.queueLength()).equals(0) + o(G.window.history.state).deepEquals({a: 1}) + o(G.rafMock.queueLength()).equals(0) }) o("route.Link can render without routes or dom access", () => { restoreDOMGlobals() - m.route.init(prefix, "https://localhost/") + m.route.init(prefix, null, "https://localhost/") var enabled = m.route.link({href: "/test"}) o(Object.keys(enabled)).deepEquals(["href", "onclick"]) @@ -337,18 +320,18 @@ o.spec("route", () => { var disabled = m.route.link({disabled: true, href: "/test"}) o(disabled).deepEquals({disabled: true, "aria-disabled": "true"}) - o(throttleMock.queueLength()).equals(0) + o(G.rafMock.queueLength()).equals(0) }) o("route.Link doesn't redraw on wrong button", async () => { - var e = $window.document.createEvent("MouseEvents") + var e = G.window.document.createEvent("MouseEvents") e.initEvent("click", true, true) e.button = 10 - $window.location.href = `${prefix}/` - m.route.init(prefix) - m.mount(root, () => { + G.window.location.href = `${prefix}/` + m.mount(G.root, (isInit, redraw) => { + if (isInit) m.route.init(prefix, redraw) if (m.route.path === "/") { return m("a", m.route.link({href: "/test"})) } else if (m.route.path === "/test") { @@ -358,27 +341,27 @@ o.spec("route", () => { } }) - o($window.location.href).equals(fullPrefix) + o(G.window.location.href).equals(fullPrefix) - root.firstChild.dispatchEvent(e) + G.root.firstChild.dispatchEvent(e) await Promise.resolve() - throttleMock.fire() + G.rafMock.fire() - o($window.location.href).equals(fullPrefix) - o(throttleMock.queueLength()).equals(0) + o(G.window.location.href).equals(fullPrefix) + o(G.rafMock.queueLength()).equals(0) }) o("route.Link doesn't redraw on preventDefault", async () => { - var e = $window.document.createEvent("MouseEvents") + var e = G.window.document.createEvent("MouseEvents") e.initEvent("click", true, true) e.button = 0 - $window.location.href = `${prefix}/` - m.route.init(prefix) - m.mount(root, () => { + G.window.location.href = `${prefix}/` + m.mount(G.root, (isInit, redraw) => { + if (isInit) m.route.init(prefix, redraw) if (m.route.path === "/") { return m("a", m.route.link({href: "/test", onclick(e) { e.preventDefault() }})) } else if (m.route.path === "/test") { @@ -388,26 +371,26 @@ o.spec("route", () => { } }) - o($window.location.href).equals(fullPrefix) + o(G.window.location.href).equals(fullPrefix) - root.firstChild.dispatchEvent(e) + G.root.firstChild.dispatchEvent(e) await Promise.resolve() - throttleMock.fire() + G.rafMock.fire() - o($window.location.href).equals(fullPrefix) - o(throttleMock.queueLength()).equals(0) + o(G.window.location.href).equals(fullPrefix) + o(G.rafMock.queueLength()).equals(0) }) o("route.Link ignores `return false`", async () => { - var e = $window.document.createEvent("MouseEvents") + var e = G.window.document.createEvent("MouseEvents") e.initEvent("click", true, true) e.button = 0 - $window.location.href = `${prefix}/` - m.route.init(prefix) - m.mount(root, () => { + G.window.location.href = `${prefix}/` + m.mount(G.root, (isInit, redraw) => { + if (isInit) m.route.init(prefix, redraw) if (m.route.path === "/") { return m("a", m.route.link({href: "/test", onclick: () => false})) } else if (m.route.path === "/test") { @@ -417,62 +400,66 @@ o.spec("route", () => { } }) - o($window.location.href).equals(fullPrefix) + o(G.window.location.href).equals(fullPrefix) - root.firstChild.dispatchEvent(e) + G.root.firstChild.dispatchEvent(e) await Promise.resolve() - throttleMock.fire() + G.rafMock.fire() - o($window.location.href).equals(`${fullPrefix}test`) - o(throttleMock.queueLength()).equals(0) + o(G.window.location.href).equals(`${fullPrefix}test`) + o(G.rafMock.queueLength()).equals(0) }) o("m.route.set(m.route.get()) re-runs the resolution logic (#1180)", async () => { - var render = o.spy(() => m("div")) + var render = o.spy((isInit, redraw) => { + if (isInit) m.route.init(prefix, redraw) + return m("div") + }) - $window.location.href = `${prefix}/` - m.route.init(prefix) - m.mount(root, render) + G.window.location.href = `${prefix}/` + m.mount(G.root, render) o(render.callCount).equals(1) await Promise.resolve() - throttleMock.fire() + G.rafMock.fire() o(render.callCount).equals(1) m.route.set(m.route.get()) await Promise.resolve() - throttleMock.fire() + G.rafMock.fire() await Promise.resolve() - throttleMock.fire() + G.rafMock.fire() o(render.callCount).equals(2) - o(throttleMock.queueLength()).equals(0) + o(G.rafMock.queueLength()).equals(0) }) o("throttles", () => { var i = 0 - $window.location.href = `${prefix}/` - m.route.init(prefix) - m.mount(root, () => { i++ }) + G.window.location.href = `${prefix}/` + var redraw = m.mount(G.root, (redraw, isInit) => { + if (isInit) m.route.init(prefix, redraw) + i++ + }) var before = i - m.redraw() - m.redraw() - m.redraw() - m.redraw() + redraw() + redraw() + redraw() + redraw() var after = i - throttleMock.fire() + G.rafMock.fire() o(before).equals(1) // routes synchronously o(after).equals(1) // redraws asynchronously o(i).equals(2) - o(throttleMock.queueLength()).equals(0) + o(G.rafMock.queueLength()).equals(0) }) }) }) diff --git a/tests/exported-api.js b/tests/exported-api.js index 8822281a4..ce331e285 100644 --- a/tests/exported-api.js +++ b/tests/exported-api.js @@ -1,45 +1,38 @@ +/* eslint-disable no-bitwise */ import o from "ospec" -import {injectGlobals, register, restoreGlobalState} from "../test-utils/redraw-registry.js" +import {setupGlobals} from "../test-utils/global.js" import m from "../src/entry/mithril.esm.js" -import browserMock from "../test-utils/browserMock.js" -import throttleMocker from "../test-utils/throttleMock.js" - o.spec("api", function() { - var $window, throttleMock, root - - o.beforeEach(() => { - injectGlobals($window = browserMock(), throttleMock = throttleMocker()) - }) - - o.afterEach(restoreGlobalState) + var G = setupGlobals() o.spec("m", function() { o("works", function() { var vnode = m("div") - o(vnode.tag).equals("div") + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_ELEMENT) + o(vnode.t).equals("div") }) }) o.spec("m.normalize", function() { o("works", function() { var vnode = m.normalize([m("div")]) - o(vnode.tag).equals(Symbol.for("m.Fragment")) - o(vnode.children.length).equals(1) - o(vnode.children[0].tag).equals("div") + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_FRAGMENT) + o(vnode.c.length).equals(1) + o(vnode.c[0].t).equals("div") }) }) o.spec("m.key", function() { o("works", function() { var vnode = m.key(123, [m("div")]) - o(vnode.tag).equals(Symbol.for("m.key")) - o(vnode.state).equals(123) - o(vnode.children.length).equals(1) - o(vnode.children[0].tag).equals("div") + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_KEY) + o(vnode.t).equals(123) + o(vnode.c.length).equals(1) + o(vnode.c[0].t).equals("div") }) }) o.spec("m.p", function() { @@ -51,53 +44,38 @@ o.spec("api", function() { }) o.spec("m.render", function() { o("works", function() { - root = register($window.document.createElement("div")) - m.render(root, m("div")) + m.render(G.root, m("div")) - o(root.childNodes.length).equals(1) - o(root.firstChild.nodeName).equals("DIV") + o(G.root.childNodes.length).equals(1) + o(G.root.firstChild.nodeName).equals("DIV") }) }) o.spec("m.mount", function() { o("works", function() { - root = register($window.document.createElement("div")) - m.mount(root, () => m("div")) + var count = 0 + var redraw = m.mount(G.root, () => { + count++ + return m("div") + }) - o(root.childNodes.length).equals(1) - o(root.firstChild.nodeName).equals("DIV") - }) - }) + o(G.root.childNodes.length).equals(1) + o(G.root.firstChild.nodeName).equals("DIV") - o.spec("m.redraw", function() { - o("works", function() { - var count = 0 - root = register($window.document.createElement("div")) - m.mount(root, () => {count++}) - o(count).equals(1) - m.redraw() + redraw() o(count).equals(1) - throttleMock.fire() + G.rafMock.fire() o(count).equals(2) - }) - }) - o.spec("m.redrawSync", function() { - o("works", function() { - root = register($window.document.createElement("div")) - var view = o.spy() - m.mount(root, view) - o(view.callCount).equals(1) - m.redrawSync() - o(view.callCount).equals(2) + redraw.sync() + o(count).equals(3) }) }) o.spec("m.route", function() { o("works", async() => { - root = register($window.document.createElement("div")) - m.route.init("#") - m.mount(root, () => { + m.mount(G.root, (isInit, redraw) => { + if (isInit) m.route.init("#", redraw) if (m.route.path === "/a") { return m("div") } else if (m.route.path === "/b") { @@ -108,18 +86,18 @@ o.spec("api", function() { }) await Promise.resolve() - throttleMock.fire() - o(throttleMock.queueLength()).equals(0) + G.rafMock.fire() + o(G.rafMock.queueLength()).equals(0) - o(root.childNodes.length).equals(1) - o(root.firstChild.nodeName).equals("DIV") + o(G.root.childNodes.length).equals(1) + o(G.root.firstChild.nodeName).equals("DIV") o(m.route.get()).equals("/a") m.route.set("/b") await Promise.resolve() - throttleMock.fire() - o(throttleMock.queueLength()).equals(0) + G.rafMock.fire() + o(G.rafMock.queueLength()).equals(0) o(m.route.get()).equals("/b") }) diff --git a/tests/render/attributes.js b/tests/render/attributes.js index 473016504..d3d252b07 100644 --- a/tests/render/attributes.js +++ b/tests/render/attributes.js @@ -1,14 +1,12 @@ import o from "ospec" -import domMock from "../../test-utils/domMock.js" +import {setupGlobals} from "../../test-utils/global.js" + import m from "../../src/entry/mithril.esm.js" o.spec("attributes", function() { - var $window, root - o.beforeEach(function() { - $window = domMock() - root = $window.document.body - }) + var G = setupGlobals() + o.spec("basics", function() { o("works (create/update/remove)", function() { @@ -16,45 +14,45 @@ o.spec("attributes", function() { var b = m("div", {id: "test"}) var c = m("div") - m.render(root, a); + m.render(G.root, a); - o(a.dom.hasAttribute("id")).equals(false) + o(a.d.hasAttribute("id")).equals(false) - m.render(root, b); + m.render(G.root, b); - o(b.dom.getAttribute("id")).equals("test") + o(b.d.getAttribute("id")).equals("test") - m.render(root, c); + m.render(G.root, c); - o(c.dom.hasAttribute("id")).equals(false) + o(c.d.hasAttribute("id")).equals(false) }) o("undefined attr is equivalent to a lack of attr", function() { var a = m("div", {id: undefined}) var b = m("div", {id: "test"}) var c = m("div", {id: undefined}) - m.render(root, a); + m.render(G.root, a); - o(a.dom.hasAttribute("id")).equals(false) + o(a.d.hasAttribute("id")).equals(false) - m.render(root, b); + m.render(G.root, b); - o(b.dom.hasAttribute("id")).equals(true) - o(b.dom.getAttribute("id")).equals("test") + o(b.d.hasAttribute("id")).equals(true) + o(b.d.getAttribute("id")).equals("test") // #1804 - m.render(root, c); + m.render(G.root, c); - o(c.dom.hasAttribute("id")).equals(false) + o(c.d.hasAttribute("id")).equals(false) }) }) o.spec("customElements", function(){ o("when vnode is customElement without property, custom setAttribute called", function(){ - var f = $window.document.createElement + var f = G.window.document.createElement var spies = [] - $window.document.createElement = function(tag, is){ + G.window.document.createElement = function(tag, is){ var el = f(tag, is) var spy = o.spy(el.setAttribute) el.setAttribute = spy @@ -63,7 +61,7 @@ o.spec("attributes", function() { return el } - m.render(root, [ + m.render(G.root, [ m("input", {value: "hello"}), m("input", {value: "hello"}), m("input", {value: "hello"}), @@ -81,12 +79,12 @@ o.spec("attributes", function() { }) o("when vnode is customElement with property, custom setAttribute not called", function(){ - var f = $window.document.createElement + var f = G.window.document.createElement var spies = [] var getters = [] var setters = [] - $window.document.createElement = function(tag, is){ + G.window.document.createElement = function(tag, is){ var el = f(tag, is) var spy = o.spy(el.setAttribute) el.setAttribute = spy @@ -107,7 +105,7 @@ o.spec("attributes", function() { return el } - m.render(root, [ + m.render(G.root, [ m("input", {value: "hello"}), m("input", {value: "hello"}), m("input", {value: "hello"}), @@ -135,124 +133,123 @@ o.spec("attributes", function() { o("when input readonly is true, attribute is present", function() { var a = m("input", {readonly: true}) - m.render(root, a) + m.render(G.root, a) - o(a.dom.attributes["readonly"].value).equals("") + o(a.d.attributes["readonly"].value).equals("") }) o("when input readonly is false, attribute is not present", function() { var a = m("input", {readonly: false}) - m.render(root, a) + m.render(G.root, a) - o(a.dom.attributes["readonly"]).equals(undefined) + o(a.d.attributes["readonly"]).equals(undefined) }) }) o.spec("input checked", function() { o("when input checked is true, attribute is not present", function() { var a = m("input", {checked: true}) - m.render(root, a) + m.render(G.root, a) - o(a.dom.checked).equals(true) - o(a.dom.attributes["checked"]).equals(undefined) + o(a.d.checked).equals(true) + o(a.d.attributes["checked"]).equals(undefined) }) o("when input checked is false, attribute is not present", function() { var a = m("input", {checked: false}) - m.render(root, a) + m.render(G.root, a) - o(a.dom.checked).equals(false) - o(a.dom.attributes["checked"]).equals(undefined) + o(a.d.checked).equals(false) + o(a.d.attributes["checked"]).equals(undefined) }) o("after input checked is changed by 3rd party, it can still be changed by render", function() { var a = m("input", {checked: false}) var b = m("input", {checked: true}) - m.render(root, a) + m.render(G.root, a) - a.dom.checked = true //setting the javascript property makes the value no longer track the state of the attribute - a.dom.checked = false + a.d.checked = true //setting the javascript property makes the value no longer track the state of the attribute + a.d.checked = false - m.render(root, b) + m.render(G.root, b) - o(a.dom.checked).equals(true) - o(a.dom.attributes["checked"]).equals(undefined) + o(a.d.checked).equals(true) + o(a.d.attributes["checked"]).equals(undefined) }) }) o.spec("input.value", function() { o("can be set as text", function() { var a = m("input", {value: "test"}) - m.render(root, a); + m.render(G.root, a); - o(a.dom.value).equals("test") + o(a.d.value).equals("test") }) o("a lack of attribute removes `value`", function() { var a = m("input") var b = m("input", {value: "test"}) var c = m("input") - m.render(root, a) + m.render(G.root, a) - o(a.dom.value).equals("") + o(a.d.value).equals("") - m.render(root, b) + m.render(G.root, b) - o(a.dom.value).equals("test") + o(a.d.value).equals("test") // https://github.com/MithrilJS/mithril.js/issues/1804#issuecomment-304521235 - m.render(root, c) + m.render(G.root, c) - o(a.dom.value).equals("") + o(a.d.value).equals("") }) o("can be set as number", function() { var a = m("input", {value: 1}) - m.render(root, a); + m.render(G.root, a); - o(a.dom.value).equals("1") + o(a.d.value).equals("1") }) o("null becomes the empty string", function() { var a = m("input", {value: null}) var b = m("input", {value: "test"}) var c = m("input", {value: null}) - m.render(root, a); + m.render(G.root, a); - o(a.dom.value).equals("") - o(a.dom.getAttribute("value")).equals(null) + o(a.d.value).equals("") + o(a.d.getAttribute("value")).equals(null) - m.render(root, b); + m.render(G.root, b); - o(b.dom.value).equals("test") - o(b.dom.getAttribute("value")).equals(null) + o(b.d.value).equals("test") + o(b.d.getAttribute("value")).equals(null) - m.render(root, c); + m.render(G.root, c); - o(c.dom.value).equals("") - o(c.dom.getAttribute("value")).equals(null) + o(c.d.value).equals("") + o(c.d.getAttribute("value")).equals(null) }) o("'' and 0 are different values", function() { var a = m("input", {value: 0}) var b = m("input", {value: ""}) var c = m("input", {value: 0}) - m.render(root, a); + m.render(G.root, a); - o(a.dom.value).equals("0") + o(a.d.value).equals("0") - m.render(root, b); + m.render(G.root, b); - o(b.dom.value).equals("") + o(b.d.value).equals("") // #1595 redux - m.render(root, c); + m.render(G.root, c); - o(c.dom.value).equals("0") + o(c.d.value).equals("0") }) o("isn't set when equivalent to the previous value and focused", function() { - var $window = domMock({spy: o.spy}) - var root = $window.document.body + G.initialize({spy: o.spy}) var a =m("input") var b = m("input", {value: "1"}) @@ -260,53 +257,50 @@ o.spec("attributes", function() { var d = m("input", {value: 1}) var e = m("input", {value: 2}) - m.render(root, a) - var spies = $window.__getSpies(a.dom) - a.dom.focus() + m.render(G.root, a) + var spies = G.window.__getSpies(a.d) + a.d.focus() o(spies.valueSetter.callCount).equals(0) - m.render(root, b) + m.render(G.root, b) - o(b.dom.value).equals("1") + o(b.d.value).equals("1") o(spies.valueSetter.callCount).equals(1) - m.render(root, c) + m.render(G.root, c) - o(c.dom.value).equals("1") + o(c.d.value).equals("1") o(spies.valueSetter.callCount).equals(1) - m.render(root, d) + m.render(G.root, d) - o(d.dom.value).equals("1") + o(d.d.value).equals("1") o(spies.valueSetter.callCount).equals(1) - m.render(root, e) + m.render(G.root, e) - o(d.dom.value).equals("2") + o(d.d.value).equals("2") o(spies.valueSetter.callCount).equals(2) }) }) o.spec("input.type", function() { o("works", function() { - var $window = domMock() - var root = $window.document.body - var a = m("input", {type: "radio"}) var b = m("input", {type: "text"}) var c = m("input") - m.render(root, a) + m.render(G.root, a) - o(a.dom.getAttribute("type")).equals("radio") + o(a.d.getAttribute("type")).equals("radio") - m.render(root, b) + m.render(G.root, b) - o(b.dom.getAttribute("type")).equals("text") + o(b.d.getAttribute("type")).equals("text") - m.render(root, c) + m.render(G.root, c) - o(c.dom.hasAttribute("type")).equals(false) + o(c.d.hasAttribute("type")).equals(false) }) }) o.spec("textarea.value", function() { @@ -314,18 +308,17 @@ o.spec("attributes", function() { var a = m("textarea", {value:"x"}) var b = m("textarea") - m.render(root, a) + m.render(G.root, a) - o(a.dom.value).equals("x") + o(a.d.value).equals("x") // https://github.com/MithrilJS/mithril.js/issues/1804#issuecomment-304521235 - m.render(root, b) + m.render(G.root, b) - o(b.dom.value).equals("") + o(b.d.value).equals("") }) o("isn't set when equivalent to the previous value and focused", function() { - var $window = domMock({spy: o.spy}) - var root = $window.document.body + G.initialize({spy: o.spy}) var a = m("textarea") var b = m("textarea", {value: "1"}) @@ -333,30 +326,30 @@ o.spec("attributes", function() { var d = m("textarea", {value: 1}) var e = m("textarea", {value: 2}) - m.render(root, a) - var spies = $window.__getSpies(a.dom) - a.dom.focus() + m.render(G.root, a) + var spies = G.window.__getSpies(a.d) + a.d.focus() o(spies.valueSetter.callCount).equals(0) - m.render(root, b) + m.render(G.root, b) - o(b.dom.value).equals("1") + o(b.d.value).equals("1") o(spies.valueSetter.callCount).equals(1) - m.render(root, c) + m.render(G.root, c) - o(c.dom.value).equals("1") + o(c.d.value).equals("1") o(spies.valueSetter.callCount).equals(1) - m.render(root, d) + m.render(G.root, d) - o(d.dom.value).equals("1") + o(d.d.value).equals("1") o(spies.valueSetter.callCount).equals(1) - m.render(root, e) + m.render(G.root, e) - o(d.dom.value).equals("2") + o(d.d.value).equals("2") o(spies.valueSetter.callCount).equals(2) }) }) @@ -364,54 +357,54 @@ o.spec("attributes", function() { o("when link href is true, attribute is present", function() { var a = m("a", {href: true}) - m.render(root, a) + m.render(G.root, a) - o(a.dom.attributes["href"]).notEquals(undefined) + o(a.d.attributes["href"]).notEquals(undefined) }) o("when link href is false, attribute is not present", function() { var a = m("a", {href: false}) - m.render(root, a) + m.render(G.root, a) - o(a.dom.attributes["href"]).equals(undefined) + o(a.d.attributes["href"]).equals(undefined) }) }) o.spec("canvas width and height", function() { o("uses attribute API", function() { var canvas = m("canvas", {width: "100%"}) - m.render(root, canvas) + m.render(G.root, canvas) - o(canvas.dom.attributes["width"].value).equals("100%") - o(canvas.dom.width).equals(100) + o(canvas.d.attributes["width"].value).equals("100%") + o(canvas.d.width).equals(100) }) }) o.spec("svg", function() { o("when className is specified then it should be added as a class", function() { var a = m("svg", {className: "test"}) - m.render(root, a); + m.render(G.root, a); - o(a.dom.attributes["class"].value).equals("test") + o(a.d.attributes["class"].value).equals("test") }) /* eslint-disable no-script-url */ o("handles xlink:href", function() { var vnode = m("svg", {ns: "http://www.w3.org/2000/svg"}, m("a", {ns: "http://www.w3.org/2000/svg", "xlink:href": "javascript:;"}) ) - m.render(root, vnode) + m.render(G.root, vnode) - o(vnode.dom.nodeName).equals("svg") - o(vnode.dom.firstChild.attributes["href"].value).equals("javascript:;") - o(vnode.dom.firstChild.attributes["href"].namespaceURI).equals("http://www.w3.org/1999/xlink") + o(vnode.d.nodeName).equals("svg") + o(vnode.d.firstChild.attributes["href"].value).equals("javascript:;") + o(vnode.d.firstChild.attributes["href"].namespaceURI).equals("http://www.w3.org/1999/xlink") vnode = m("svg", {ns: "http://www.w3.org/2000/svg"}, m("a", {ns: "http://www.w3.org/2000/svg"}) ) - m.render(root, vnode) + m.render(G.root, vnode) - o(vnode.dom.nodeName).equals("svg") - o("href" in vnode.dom.firstChild.attributes).equals(false) + o(vnode.d.nodeName).equals("svg") + o("href" in vnode.d.firstChild.attributes).equals(false) }) /* eslint-enable no-script-url */ }) @@ -419,58 +412,57 @@ o.spec("attributes", function() { o("can be set as text", function() { var a = m("option", {value: "test"}) - m.render(root, a); + m.render(G.root, a); - o(a.dom.value).equals("test") + o(a.d.value).equals("test") }) o("can be set as number", function() { var a = m("option", {value: 1}) - m.render(root, a); + m.render(G.root, a); - o(a.dom.value).equals("1") + o(a.d.value).equals("1") }) o("null removes the attribute", function() { var a = m("option", {value: null}) var b = m("option", {value: "test"}) var c = m("option", {value: null}) - m.render(root, a); + m.render(G.root, a); - o(a.dom.value).equals("") - o(a.dom.hasAttribute("value")).equals(false) + o(a.d.value).equals("") + o(a.d.hasAttribute("value")).equals(false) - m.render(root, b); + m.render(G.root, b); - o(b.dom.value).equals("test") - o(b.dom.getAttribute("value")).equals("test") + o(b.d.value).equals("test") + o(b.d.getAttribute("value")).equals("test") - m.render(root, c); + m.render(G.root, c); - o(c.dom.value).equals("") - o(c.dom.hasAttribute("value")).equals(false) + o(c.d.value).equals("") + o(c.d.hasAttribute("value")).equals(false) }) o("'' and 0 are different values", function() { var a = m("option", {value: 0}, "") var b = m("option", {value: ""}, "") var c = m("option", {value: 0}, "") - m.render(root, a); + m.render(G.root, a); - o(a.dom.value).equals("0") + o(a.d.value).equals("0") - m.render(root, b); + m.render(G.root, b); - o(a.dom.value).equals("") + o(a.d.value).equals("") // #1595 redux - m.render(root, c); + m.render(G.root, c); - o(c.dom.value).equals("0") + o(c.d.value).equals("0") }) o("isn't set when equivalent to the previous value", function() { - var $window = domMock({spy: o.spy}) - var root = $window.document.body + G.initialize({spy: o.spy}) var a = m("option") var b = m("option", {value: "1"}) @@ -478,29 +470,29 @@ o.spec("attributes", function() { var d = m("option", {value: 1}) var e = m("option", {value: 2}) - m.render(root, a) - var spies = $window.__getSpies(a.dom) + m.render(G.root, a) + var spies = G.window.__getSpies(a.d) o(spies.valueSetter.callCount).equals(0) - m.render(root, b) + m.render(G.root, b) - o(b.dom.value).equals("1") + o(b.d.value).equals("1") o(spies.valueSetter.callCount).equals(1) - m.render(root, c) + m.render(G.root, c) - o(c.dom.value).equals("1") + o(c.d.value).equals("1") o(spies.valueSetter.callCount).equals(1) - m.render(root, d) + m.render(G.root, d) - o(d.dom.value).equals("1") + o(d.d.value).equals("1") o(spies.valueSetter.callCount).equals(1) - m.render(root, e) + m.render(G.root, e) - o(d.dom.value).equals("2") + o(d.d.value).equals("2") o(spies.valueSetter.callCount).equals(2) }) }) @@ -525,7 +517,7 @@ o.spec("attributes", function() { var select = m("select", {selectedIndex: 0}, m("option", {value: "1", selected: ""}) ) - m.render(root, select) + m.render(G.root, select) }) */ o("can be set as text", function() { @@ -533,145 +525,51 @@ o.spec("attributes", function() { var b = makeSelect("2") var c = makeSelect("a") - m.render(root, a) + m.render(G.root, a) - o(a.dom.value).equals("1") - o(a.dom.selectedIndex).equals(0) + o(a.d.value).equals("1") + o(a.d.selectedIndex).equals(0) - m.render(root, b) + m.render(G.root, b) - o(b.dom.value).equals("2") - o(b.dom.selectedIndex).equals(1) + o(b.d.value).equals("2") + o(b.d.selectedIndex).equals(1) - m.render(root, c) + m.render(G.root, c) - o(c.dom.value).equals("a") - o(c.dom.selectedIndex).equals(2) + o(c.d.value).equals("a") + o(c.d.selectedIndex).equals(2) }) o("setting null unsets the value", function() { var a = makeSelect(null) - m.render(root, a) + m.render(G.root, a) - o(a.dom.value).equals("") - o(a.dom.selectedIndex).equals(-1) + o(a.d.value).equals("") + o(a.d.selectedIndex).equals(-1) }) o("values are type converted", function() { var a = makeSelect(1) var b = makeSelect(2) - m.render(root, a) - - o(a.dom.value).equals("1") - o(a.dom.selectedIndex).equals(0) - - m.render(root, b) - - o(b.dom.value).equals("2") - o(b.dom.selectedIndex).equals(1) - }) - o("'' and 0 are different values when focused", function() { - var a = makeSelect("") - var b = makeSelect(0) - - m.render(root, a) - a.dom.focus() - - o(a.dom.value).equals("") - - // #1595 redux - m.render(root, b) - - o(b.dom.value).equals("0") - }) - o("'' and null are different values when focused", function() { - var a = makeSelect("") - var b = makeSelect(null) - var c = makeSelect("") - - m.render(root, a) - a.dom.focus() - - o(a.dom.value).equals("") - o(a.dom.selectedIndex).equals(4) - - m.render(root, b) + m.render(G.root, a) - o(b.dom.value).equals("") - o(b.dom.selectedIndex).equals(-1) - - m.render(root, c) - - o(c.dom.value).equals("") - o(c.dom.selectedIndex).equals(4) - }) - 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 a = makeSelect() - var b = makeSelect("1") - var c = makeSelect(1) - var d = makeSelect("2") + o(a.d.value).equals("1") + o(a.d.selectedIndex).equals(0) - m.render(root, a) - var spies = $window.__getSpies(a.dom) - a.dom.focus() - - o(spies.valueSetter.callCount).equals(0) - o(a.dom.value).equals("1") - - m.render(root, b) - - o(spies.valueSetter.callCount).equals(0) - o(b.dom.value).equals("1") - - m.render(root, c) - - o(spies.valueSetter.callCount).equals(0) - o(c.dom.value).equals("1") - - m.render(root, d) - - o(spies.valueSetter.callCount).equals(1) - o(d.dom.value).equals("2") - }) - }) - o.spec("contenteditable throws on untrusted children", function() { - o("including elements", function() { - var div = m("div", {contenteditable: true}, m("script", {src: "http://evil.com"})) - var succeeded = false - - try { - m.render(root, div) - - succeeded = true - } - catch(e){/* ignore */} - - o(succeeded).equals(false) - }) - o("tolerating empty children", function() { - var div = m("div", {contenteditable: true}) - var succeeded = false - - try { - m.render(root, div) - - succeeded = true - } - catch(e){/* ignore */} + m.render(G.root, b) - o(succeeded).equals(true) + o(b.d.value).equals("2") + o(b.d.selectedIndex).equals(1) }) }) o.spec("mutate attr object", function() { o("throw when reusing attrs object", function() { const attrs = {className: "on"} - m.render(root, {tag: "input", attrs}) + m.render(G.root, m("input", attrs)) attrs.className = "off" - o(() => m.render(root, {tag: "input", attrs})).throws(Error) + o(() => m.render(G.root, m("input", attrs))).throws(Error) }) }) }) diff --git a/tests/render/component.js b/tests/render/component.js index 9b9886083..c9e257ccc 100644 --- a/tests/render/component.js +++ b/tests/render/component.js @@ -1,109 +1,106 @@ import o from "ospec" -import domMock from "../../test-utils/domMock.js" +import {setupGlobals} from "../../test-utils/global.js" + import m from "../../src/entry/mithril.esm.js" o.spec("component", function() { - var $window, root - o.beforeEach(function() { - $window = domMock() - root = $window.document.createElement("div") - }) + var G = setupGlobals() o.spec("basics", function() { o("works", function() { var component = () => m("div", {id: "a"}, "b") var node = m(component) - m.render(root, node) + m.render(G.root, node) - o(root.firstChild.nodeName).equals("DIV") - o(root.firstChild.attributes["id"].value).equals("a") - o(root.firstChild.firstChild.nodeValue).equals("b") + o(G.root.firstChild.nodeName).equals("DIV") + o(G.root.firstChild.attributes["id"].value).equals("a") + o(G.root.firstChild.firstChild.nodeValue).equals("b") }) o("receives arguments", function() { var component = (attrs) => m("div", attrs) var node = m(component, {id: "a"}, "b") - m.render(root, node) + m.render(G.root, node) - o(root.firstChild.nodeName).equals("DIV") - o(root.firstChild.attributes["id"].value).equals("a") - o(root.firstChild.firstChild.nodeValue).equals("b") + o(G.root.firstChild.nodeName).equals("DIV") + o(G.root.firstChild.attributes["id"].value).equals("a") + o(G.root.firstChild.firstChild.nodeValue).equals("b") }) o("updates", function() { var component = (attrs) => m("div", attrs) - m.render(root, [m(component, {id: "a"}, "b")]) - m.render(root, [m(component, {id: "c"}, "d")]) + m.render(G.root, [m(component, {id: "a"}, "b")]) + m.render(G.root, [m(component, {id: "c"}, "d")]) - o(root.firstChild.nodeName).equals("DIV") - o(root.firstChild.attributes["id"].value).equals("c") - o(root.firstChild.firstChild.nodeValue).equals("d") + o(G.root.firstChild.nodeName).equals("DIV") + o(G.root.firstChild.attributes["id"].value).equals("c") + o(G.root.firstChild.firstChild.nodeValue).equals("d") }) o("updates root from null", function() { var visible = false var component = () => (visible ? m("div") : null) - m.render(root, m(component)) + m.render(G.root, m(component)) visible = true - m.render(root, m(component)) + m.render(G.root, m(component)) - o(root.firstChild.nodeName).equals("DIV") + o(G.root.firstChild.nodeName).equals("DIV") }) o("updates root from primitive", function() { var visible = false var component = () => (visible ? m("div") : false) - m.render(root, m(component)) + m.render(G.root, m(component)) visible = true - m.render(root, m(component)) + m.render(G.root, m(component)) - o(root.firstChild.nodeName).equals("DIV") + o(G.root.firstChild.nodeName).equals("DIV") }) o("updates root to null", function() { var visible = true var component = () => (visible ? m("div") : null) - m.render(root, m(component)) + m.render(G.root, m(component)) visible = false - m.render(root, m(component)) + m.render(G.root, m(component)) - o(root.childNodes.length).equals(0) + o(G.root.childNodes.length).equals(0) }) o("updates root to primitive", function() { var visible = true var component = () => (visible ? m("div") : false) - m.render(root, m(component)) + m.render(G.root, m(component)) visible = false - m.render(root, m(component)) + m.render(G.root, m(component)) - o(root.childNodes.length).equals(0) + o(G.root.childNodes.length).equals(0) }) o("updates root from null to null", function() { var component = () => null - m.render(root, m(component)) - m.render(root, m(component)) + m.render(G.root, m(component)) + m.render(G.root, m(component)) - o(root.childNodes.length).equals(0) + o(G.root.childNodes.length).equals(0) }) o("removes", function() { var component = () => m("div") - m.render(root, [m.key(1, m(component)), m.key(2, m("div"))]) + m.render(G.root, [m.key(1, m(component)), m.key(2, m("div"))]) var div = m("div") - m.render(root, [m.key(2, div)]) + m.render(G.root, [m.key(2, div)]) - o(root.childNodes.length).equals(1) - o(root.firstChild).equals(div.dom) + o(G.root.childNodes.length).equals(1) + o(G.root.firstChild).equals(div.d) }) o("svg works when creating across component boundary", function() { var component = () => m("g") - m.render(root, m("svg", m(component))) + m.render(G.root, m("svg", m(component))) - o(root.firstChild.firstChild.namespaceURI).equals("http://www.w3.org/2000/svg") + o(G.root.firstChild.firstChild.namespaceURI).equals("http://www.w3.org/2000/svg") }) o("svg works when updating across component boundary", function() { var component = () => m("g") - m.render(root, m("svg", m(component))) - m.render(root, m("svg", m(component))) + m.render(G.root, m("svg", m(component))) + m.render(G.root, m("svg", m(component))) - o(root.firstChild.firstChild.namespaceURI).equals("http://www.w3.org/2000/svg") + o(G.root.firstChild.firstChild.namespaceURI).equals("http://www.w3.org/2000/svg") }) }) o.spec("return value", function() { @@ -112,63 +109,63 @@ o.spec("component", function() { m("label"), m("input"), ] - m.render(root, m(component)) + m.render(G.root, m(component)) - o(root.childNodes.length).equals(2) - o(root.childNodes[0].nodeName).equals("LABEL") - o(root.childNodes[1].nodeName).equals("INPUT") + o(G.root.childNodes.length).equals(2) + o(G.root.childNodes[0].nodeName).equals("LABEL") + o(G.root.childNodes[1].nodeName).equals("INPUT") }) o("can return string", function() { var component = () => "a" - m.render(root, m(component)) + m.render(G.root, m(component)) - o(root.firstChild.nodeType).equals(3) - o(root.firstChild.nodeValue).equals("a") + o(G.root.firstChild.nodeType).equals(3) + o(G.root.firstChild.nodeValue).equals("a") }) o("can return falsy string", function() { var component = () => "" - m.render(root, m(component)) + m.render(G.root, m(component)) - o(root.firstChild.nodeType).equals(3) - o(root.firstChild.nodeValue).equals("") + o(G.root.firstChild.nodeType).equals(3) + o(G.root.firstChild.nodeValue).equals("") }) o("can return number", function() { var component = () => 1 - m.render(root, m(component)) + m.render(G.root, m(component)) - o(root.firstChild.nodeType).equals(3) - o(root.firstChild.nodeValue).equals("1") + o(G.root.firstChild.nodeType).equals(3) + o(G.root.firstChild.nodeValue).equals("1") }) o("can return falsy number", function() { var component = () => 0 - m.render(root, m(component)) + m.render(G.root, m(component)) - o(root.firstChild.nodeType).equals(3) - o(root.firstChild.nodeValue).equals("0") + o(G.root.firstChild.nodeType).equals(3) + o(G.root.firstChild.nodeValue).equals("0") }) o("can return `true`", function() { var component = () => true - m.render(root, m(component)) + m.render(G.root, m(component)) - o(root.childNodes.length).equals(0) + o(G.root.childNodes.length).equals(0) }) o("can return `false`", function() { var component = () => false - m.render(root, m(component)) + m.render(G.root, m(component)) - o(root.childNodes.length).equals(0) + o(G.root.childNodes.length).equals(0) }) o("can return null", function() { var component = () => null - m.render(root, m(component)) + m.render(G.root, m(component)) - o(root.childNodes.length).equals(0) + o(G.root.childNodes.length).equals(0) }) o("can return undefined", function() { var component = () => undefined - m.render(root, m(component)) + m.render(G.root, m(component)) - o(root.childNodes.length).equals(0) + o(G.root.childNodes.length).equals(0) }) o("throws a custom error if it returns itself when created", function() { // A view that returns its vnode would otherwise trigger an infinite loop @@ -176,7 +173,7 @@ o.spec("component", function() { var component = () => vnode var vnode = m(component) try { - m.render(root, vnode) + m.render(G.root, vnode) } catch (e) { threw = true @@ -190,13 +187,13 @@ o.spec("component", function() { // A view that returns its vnode would otherwise trigger an infinite loop var threw = false var component = () => vnode - m.render(root, m(component)) + m.render(G.root, m(component)) - o(root.childNodes.length).equals(0) + o(G.root.childNodes.length).equals(0) var vnode = m(component) try { - m.render(root, m(component)) + m.render(G.root, m(component)) } catch (e) { threw = true @@ -211,27 +208,27 @@ o.spec("component", function() { m("label"), m("input"), ] - m.render(root, m(component)) - m.render(root, m(component)) + m.render(G.root, m(component)) + m.render(G.root, m(component)) - o(root.childNodes.length).equals(2) - o(root.childNodes[0].nodeName).equals("LABEL") - o(root.childNodes[1].nodeName).equals("INPUT") + o(G.root.childNodes.length).equals(2) + o(G.root.childNodes[0].nodeName).equals("LABEL") + o(G.root.childNodes[1].nodeName).equals("INPUT") }) o("can update when returning primitive", function() { var component = () => "a" - m.render(root, m(component)) - m.render(root, m(component)) + m.render(G.root, m(component)) + m.render(G.root, m(component)) - o(root.firstChild.nodeType).equals(3) - o(root.firstChild.nodeValue).equals("a") + o(G.root.firstChild.nodeType).equals(3) + o(G.root.firstChild.nodeValue).equals("a") }) o("can update when returning null", function() { var component = () => null - m.render(root, m(component)) - m.render(root, m(component)) + m.render(G.root, m(component)) + m.render(G.root, m(component)) - o(root.childNodes.length).equals(0) + o(G.root.childNodes.length).equals(0) }) o("can remove when returning fragments", function() { var component = () => [ @@ -239,22 +236,22 @@ o.spec("component", function() { m("input"), ] var div = m("div") - m.render(root, [m.key(1, m(component)), m.key(2, div)]) + m.render(G.root, [m.key(1, m(component)), m.key(2, div)]) - m.render(root, [m.key(2, m("div"))]) + m.render(G.root, [m.key(2, m("div"))]) - o(root.childNodes.length).equals(1) - o(root.firstChild).equals(div.dom) + o(G.root.childNodes.length).equals(1) + o(G.root.firstChild).equals(div.d) }) o("can remove when returning primitive", function() { var component = () => "a" var div = m("div") - m.render(root, [m.key(1, m(component)), m.key(2, div)]) + m.render(G.root, [m.key(1, m(component)), m.key(2, div)]) - m.render(root, [m.key(2, m("div"))]) + m.render(G.root, [m.key(2, m("div"))]) - o(root.childNodes.length).equals(1) - o(root.firstChild).equals(div.dom) + o(G.root.childNodes.length).equals(1) + o(G.root.firstChild).equals(div.d) }) }) o.spec("lifecycle", function() { @@ -263,34 +260,34 @@ o.spec("component", function() { var component = () => { called++ - o(root.childNodes.length).equals(0) + o(G.root.childNodes.length).equals(0) return () => m("div", {id: "a"}, "b") } - m.render(root, m(component)) + m.render(G.root, m(component)) o(called).equals(1) - o(root.firstChild.nodeName).equals("DIV") - o(root.firstChild.attributes["id"].value).equals("a") - o(root.firstChild.firstChild.nodeValue).equals("b") + o(G.root.firstChild.nodeName).equals("DIV") + o(G.root.firstChild.attributes["id"].value).equals("a") + o(G.root.firstChild.firstChild.nodeValue).equals("b") }) o("constructs when returning fragment", function() { var called = 0 var component = () => { called++ - o(root.childNodes.length).equals(0) + o(G.root.childNodes.length).equals(0) return () => [m("div", {id: "a"}, "b")] } - m.render(root, m(component)) + m.render(G.root, m(component)) o(called).equals(1) - o(root.firstChild.nodeName).equals("DIV") - o(root.firstChild.attributes["id"].value).equals("a") - o(root.firstChild.firstChild.nodeValue).equals("b") + o(G.root.firstChild.nodeName).equals("DIV") + o(G.root.firstChild.attributes["id"].value).equals("a") + o(G.root.firstChild.firstChild.nodeValue).equals("b") }) o("can call view function returned on initialization", function() { var viewCalled = false @@ -302,7 +299,7 @@ o.spec("component", function() { } } - m.render(root, m(component)) + m.render(G.root, m(component)) }) o("does not initialize on redraw", function() { var component = o.spy(() => () => m("div", {id: "a"}, "b")) @@ -311,203 +308,220 @@ o.spec("component", function() { return m(component) } - m.render(root, view()) - m.render(root, view()) + m.render(G.root, view()) + m.render(G.root, view()) o(component.callCount).equals(1) }) - o("calls inner `m.layout` as initial on first render", function() { + o("calls inner `m.layout` create callback on first render", function() { + var onabort = o.spy() + var createSpy = o.spy((_, signal) => { signal.onabort = onabort }) + var updateSpy = o.spy((_, signal) => { signal.onabort = onabort }) + var component = () => [ + m.layout(createSpy, updateSpy), + m("div", {id: "a"}, "b"), + ] + + m.render(G.root, m(component)) + + o(createSpy.callCount).equals(1) + o(createSpy.args[0]).equals(G.root) + o(createSpy.args[1].aborted).equals(false) + o(updateSpy.callCount).equals(0) + o(onabort.callCount).equals(0) + o(G.root.firstChild.nodeName).equals("DIV") + o(G.root.firstChild.attributes["id"].value).equals("a") + o(G.root.firstChild.firstChild.nodeValue).equals("b") + }) + o("calls inner `m.layout` update callback on subsequent render", function() { var onabort = o.spy() - var layoutSpy = o.spy((_, signal) => { signal.onabort = onabort }) + var createSpy = o.spy((_, signal) => { signal.onabort = onabort }) + var updateSpy = o.spy((_, signal) => { signal.onabort = onabort }) var component = () => [ - m.layout(layoutSpy), + m.layout(createSpy, updateSpy), m("div", {id: "a"}, "b"), ] - m.render(root, m(component)) + m.render(G.root, m(component)) + m.render(G.root, m(component)) - o(layoutSpy.callCount).equals(1) - o(layoutSpy.args[0]).equals(root) - o(layoutSpy.args[1].aborted).equals(false) + o(createSpy.callCount).equals(1) + o(updateSpy.callCount).equals(1) + o(updateSpy.args[0]).equals(G.root) + o(updateSpy.args[1].aborted).equals(false) + o(updateSpy.args[1]).equals(createSpy.args[1]) o(onabort.callCount).equals(0) - o(layoutSpy.args[2]).equals(true) - o(root.firstChild.nodeName).equals("DIV") - o(root.firstChild.attributes["id"].value).equals("a") - o(root.firstChild.firstChild.nodeValue).equals("b") + o(G.root.firstChild.nodeName).equals("DIV") + o(G.root.firstChild.attributes["id"].value).equals("a") + o(G.root.firstChild.firstChild.nodeValue).equals("b") }) - o("calls inner `m.layout` as non-initial on subsequent render", function() { + o("calls inner `m.layout` update callback on subsequent render without a create callback", function() { var onabort = o.spy() - var layoutSpy = o.spy((_, signal) => { signal.onabort = onabort }) + var updateSpy = o.spy((_, signal) => { signal.onabort = onabort }) var component = () => [ - m.layout(layoutSpy), + m.layout(null, updateSpy), m("div", {id: "a"}, "b"), ] - m.render(root, m(component)) - m.render(root, m(component)) + m.render(G.root, m(component)) + m.render(G.root, m(component)) - o(layoutSpy.callCount).equals(2) - o(layoutSpy.args[0]).equals(root) - o(layoutSpy.args[1].aborted).equals(false) + o(updateSpy.callCount).equals(1) + o(updateSpy.args[0]).equals(G.root) + o(updateSpy.args[1].aborted).equals(false) o(onabort.callCount).equals(0) - o(layoutSpy.args[2]).equals(false) - o(root.firstChild.nodeName).equals("DIV") - o(root.firstChild.attributes["id"].value).equals("a") - o(root.firstChild.firstChild.nodeValue).equals("b") + o(G.root.firstChild.nodeName).equals("DIV") + o(G.root.firstChild.attributes["id"].value).equals("a") + o(G.root.firstChild.firstChild.nodeValue).equals("b") }) o("aborts inner `m.layout` signal after first render", function() { var onabort = o.spy() - var layoutSpy = o.spy((_, signal) => { signal.onabort = onabort }) + var createSpy = o.spy((_, signal) => { signal.onabort = onabort }) var component = () => [ - m.layout(layoutSpy), + m.layout(createSpy), m("div", {id: "a"}, "b"), ] - m.render(root, m(component)) - m.render(root, null) + m.render(G.root, m(component)) + m.render(G.root, null) - o(layoutSpy.callCount).equals(1) - o(layoutSpy.args[1].aborted).equals(true) + o(createSpy.callCount).equals(1) + o(createSpy.args[1].aborted).equals(true) o(onabort.callCount).equals(1) - o(root.childNodes.length).equals(0) + o(G.root.childNodes.length).equals(0) }) o("aborts inner `m.layout` signal after subsequent render", function() { var onabort = o.spy() - var layoutSpy = o.spy((_, signal) => { signal.onabort = onabort }) + var createSpy = o.spy((_, signal) => { signal.onabort = onabort }) var component = () => [ - m.layout(layoutSpy), + m.layout(createSpy), m("div", {id: "a"}, "b"), ] - m.render(root, m(component)) - m.render(root, m(component)) - m.render(root, null) + m.render(G.root, m(component)) + m.render(G.root, m(component)) + m.render(G.root, null) - o(layoutSpy.callCount).equals(2) - o(layoutSpy.args[1].aborted).equals(true) + o(createSpy.callCount).equals(1) + o(createSpy.args[1].aborted).equals(true) o(onabort.callCount).equals(1) - o(root.childNodes.length).equals(0) + o(G.root.childNodes.length).equals(0) }) - o("calls in-element inner `m.layout` as initial on first render", function() { + o("calls in-element inner `m.layout` create callback on first render", function() { var onabort = o.spy() - var layoutSpy = o.spy((_, signal) => { signal.onabort = onabort }) - var component = () => m("div", {id: "a"}, m.layout(layoutSpy), "b") - m.render(root, m(component)) + var createSpy = o.spy((_, signal) => { signal.onabort = onabort }) + var component = () => m("div", {id: "a"}, m.layout(createSpy), "b") + m.render(G.root, m(component)) - o(layoutSpy.callCount).equals(1) - o(layoutSpy.args[0]).equals(root.firstChild) - o(layoutSpy.args[1].aborted).equals(false) + o(createSpy.callCount).equals(1) + o(createSpy.args[0]).equals(G.root.firstChild) + o(createSpy.args[1].aborted).equals(false) o(onabort.callCount).equals(0) - o(layoutSpy.args[2]).equals(true) - o(root.firstChild.nodeName).equals("DIV") - o(root.firstChild.attributes["id"].value).equals("a") - o(root.firstChild.firstChild.nodeValue).equals("b") + o(G.root.firstChild.nodeName).equals("DIV") + o(G.root.firstChild.attributes["id"].value).equals("a") + o(G.root.firstChild.firstChild.nodeValue).equals("b") }) - o("calls in-element inner `m.layout` as non-initial on subsequent render", function() { + o("calls in-element inner `m.layout` update callback on subsequent render", function() { var onabort = o.spy() - var layoutSpy = o.spy((_, signal) => { signal.onabort = onabort }) - var component = () => m("div", {id: "a"}, m.layout(layoutSpy), "b") - m.render(root, m(component)) - m.render(root, m(component)) - - o(layoutSpy.callCount).equals(2) - o(layoutSpy.args[0]).equals(root.firstChild) - o(layoutSpy.args[1].aborted).equals(false) + var updateSpy = o.spy((_, signal) => { signal.onabort = onabort }) + var component = () => m("div", {id: "a"}, m.layout(null, updateSpy), "b") + m.render(G.root, m(component)) + m.render(G.root, m(component)) + + o(updateSpy.callCount).equals(1) + o(updateSpy.args[0]).equals(G.root.firstChild) + o(updateSpy.args[1].aborted).equals(false) o(onabort.callCount).equals(0) - o(layoutSpy.args[2]).equals(false) - o(root.firstChild.nodeName).equals("DIV") - o(root.firstChild.attributes["id"].value).equals("a") - o(root.firstChild.firstChild.nodeValue).equals("b") + o(G.root.firstChild.nodeName).equals("DIV") + o(G.root.firstChild.attributes["id"].value).equals("a") + o(G.root.firstChild.firstChild.nodeValue).equals("b") }) o("aborts in-element inner `m.layout` signal after first render", function() { var onabort = o.spy() - var layoutSpy = o.spy((_, signal) => { signal.onabort = onabort }) - var component = () => m("div", {id: "a"}, m.layout(layoutSpy), "b") - m.render(root, m(component)) - m.render(root, null) + var createSpy = o.spy((_, signal) => { signal.onabort = onabort }) + var component = () => m("div", {id: "a"}, m.layout(createSpy), "b") + m.render(G.root, m(component)) + m.render(G.root, null) - o(layoutSpy.callCount).equals(1) - o(layoutSpy.args[1].aborted).equals(true) + o(createSpy.callCount).equals(1) + o(createSpy.args[1].aborted).equals(true) o(onabort.callCount).equals(1) - o(root.childNodes.length).equals(0) + o(G.root.childNodes.length).equals(0) }) o("aborts in-element inner `m.layout` signal after subsequent render", function() { var onabort = o.spy() - var layoutSpy = o.spy((_, signal) => { signal.onabort = onabort }) - var component = () => m("div", {id: "a"}, m.layout(layoutSpy), "b") - m.render(root, m(component)) - m.render(root, m(component)) - m.render(root, null) - - o(layoutSpy.callCount).equals(2) - o(layoutSpy.args[1].aborted).equals(true) + var updateSpy = o.spy((_, signal) => { signal.onabort = onabort }) + var component = () => m("div", {id: "a"}, m.layout(null, updateSpy), "b") + m.render(G.root, m(component)) + m.render(G.root, m(component)) + m.render(G.root, null) + + o(updateSpy.callCount).equals(1) + o(updateSpy.args[1].aborted).equals(true) o(onabort.callCount).equals(1) - o(root.childNodes.length).equals(0) + o(G.root.childNodes.length).equals(0) }) - o("calls direct inner `m.layout` as initial on first render", function() { + o("calls direct inner `m.layout` create callback on first render", function() { var onabort = o.spy() - var layoutSpy = o.spy((_, signal) => { signal.onabort = onabort }) - var component = () => m.layout(layoutSpy) - m.render(root, m(component)) + var createSpy = o.spy((_, signal) => { signal.onabort = onabort }) + var component = () => m.layout(createSpy) + m.render(G.root, m(component)) - o(layoutSpy.callCount).equals(1) - o(layoutSpy.args[0]).equals(root) - o(layoutSpy.args[1].aborted).equals(false) + o(createSpy.callCount).equals(1) + o(createSpy.args[0]).equals(G.root) + o(createSpy.args[1].aborted).equals(false) o(onabort.callCount).equals(0) - o(layoutSpy.args[2]).equals(true) - o(root.childNodes.length).equals(0) + o(G.root.childNodes.length).equals(0) }) - o("calls direct inner `m.layout` as non-initial on subsequent render", function() { + o("calls direct inner `m.layout` update callback on subsequent render", function() { var onabort = o.spy() - var layoutSpy = o.spy((_, signal) => { signal.onabort = onabort }) - var component = () => m.layout(layoutSpy) - m.render(root, m(component)) - m.render(root, m(component)) - - o(layoutSpy.callCount).equals(2) - o(layoutSpy.args[0]).equals(root) - o(layoutSpy.args[1].aborted).equals(false) - o(layoutSpy.args[2]).equals(false) + var updateSpy = o.spy((_, signal) => { signal.onabort = onabort }) + var component = () => m.layout(updateSpy) + m.render(G.root, m(component)) + m.render(G.root, m(component)) + + o(updateSpy.callCount).equals(1) + o(updateSpy.args[0]).equals(G.root) + o(updateSpy.args[1].aborted).equals(false) o(onabort.callCount).equals(0) - o(root.childNodes.length).equals(0) + o(G.root.childNodes.length).equals(0) }) o("aborts direct inner `m.layout` signal after first render", function() { var onabort = o.spy() - var layoutSpy = o.spy((_, signal) => { signal.onabort = onabort }) - var component = () => m.layout(layoutSpy) - m.render(root, m(component)) - m.render(root, null) + var createSpy = o.spy((_, signal) => { signal.onabort = onabort }) + var component = () => m.layout(createSpy) + m.render(G.root, m(component)) + m.render(G.root, null) - o(layoutSpy.callCount).equals(1) - o(layoutSpy.args[1].aborted).equals(true) + o(createSpy.callCount).equals(1) + o(createSpy.args[1].aborted).equals(true) o(onabort.callCount).equals(1) - o(root.childNodes.length).equals(0) + o(G.root.childNodes.length).equals(0) }) o("aborts direct inner `m.layout` signal after subsequent render", function() { var onabort = o.spy() - var layoutSpy = o.spy((_, signal) => { signal.onabort = onabort }) - var component = () => m.layout(layoutSpy) - m.render(root, m(component)) - m.render(root, m(component)) - m.render(root, null) - - o(layoutSpy.callCount).equals(2) - o(layoutSpy.args[1].aborted).equals(true) + var updateSpy = o.spy((_, signal) => { signal.onabort = onabort }) + var component = () => m.layout(updateSpy) + m.render(G.root, m(component)) + m.render(G.root, m(component)) + m.render(G.root, null) + + o(updateSpy.callCount).equals(1) + o(updateSpy.args[1].aborted).equals(true) o(onabort.callCount).equals(1) - o(root.childNodes.length).equals(0) + o(G.root.childNodes.length).equals(0) }) o("no recycling occurs (was: recycled components get a fresh state)", function() { - var layout = o.spy() - var component = o.spy(() => m("div", m.layout(layout))) + var createSpy = o.spy() + var component = o.spy(() => m("div", m.layout(createSpy))) - m.render(root, [m("div", m.key(1, m(component)))]) - var child = root.firstChild.firstChild - m.render(root, []) - m.render(root, [m("div", m.key(1, m(component)))]) + m.render(G.root, [m("div", m.key(1, m(component)))]) + var child = G.root.firstChild.firstChild + m.render(G.root, []) + m.render(G.root, [m("div", m.key(1, m(component)))]) - o(child).notEquals(root.firstChild.firstChild) // this used to be a recycling pool test + o(child).notEquals(G.root.firstChild.firstChild) // this used to be a recycling pool test o(component.callCount).equals(2) - o(layout.calls.map((c) => c.args[2])).deepEquals([true, true]) }) }) }) diff --git a/tests/render/createElement.js b/tests/render/createElement.js index 2e3df0987..e360ddd09 100644 --- a/tests/render/createElement.js +++ b/tests/render/createElement.js @@ -1,74 +1,71 @@ import o from "ospec" -import domMock from "../../test-utils/domMock.js" +import {setupGlobals} from "../../test-utils/global.js" + import m from "../../src/entry/mithril.esm.js" o.spec("createElement", function() { - var $window, root - o.beforeEach(function() { - $window = domMock() - root = $window.document.createElement("div") - }) + var G = setupGlobals() o("creates element", function() { var vnode = m("div") - m.render(root, vnode) + m.render(G.root, vnode) - o(vnode.dom.nodeName).equals("DIV") + o(vnode.d.nodeName).equals("DIV") }) o("creates attr", function() { var vnode = m("div", {id: "a", title: "b"}) - m.render(root, vnode) + m.render(G.root, vnode) - o(vnode.dom.nodeName).equals("DIV") - o(vnode.dom.attributes["id"].value).equals("a") - o(vnode.dom.attributes["title"].value).equals("b") + o(vnode.d.nodeName).equals("DIV") + o(vnode.d.attributes["id"].value).equals("a") + o(vnode.d.attributes["title"].value).equals("b") }) o("creates style", function() { var vnode = m("div", {style: {backgroundColor: "red"}}) - m.render(root, vnode) + m.render(G.root, vnode) - o(vnode.dom.nodeName).equals("DIV") - o(vnode.dom.style.backgroundColor).equals("red") + o(vnode.d.nodeName).equals("DIV") + o(vnode.d.style.backgroundColor).equals("red") }) o("allows css vars in style", function() { var vnode = m("div", {style: {"--css-var": "red"}}) - m.render(root, vnode) + m.render(G.root, vnode) - o(vnode.dom.style["--css-var"]).equals("red") + o(vnode.d.style["--css-var"]).equals("red") }) o("allows css vars in style with uppercase letters", function() { var vnode = m("div", {style: {"--cssVar": "red"}}) - m.render(root, vnode) + m.render(G.root, vnode) - o(vnode.dom.style["--cssVar"]).equals("red") + o(vnode.d.style["--cssVar"]).equals("red") }) o("censors cssFloat to float", function() { var vnode = m("a", {style: {cssFloat: "left"}}) - m.render(root, vnode) + m.render(G.root, vnode) - o(vnode.dom.style.float).equals("left") + o(vnode.d.style.float).equals("left") }) o("creates children", function() { var vnode = m("div", m("a"), m("b")) - m.render(root, vnode) + m.render(G.root, vnode) - o(vnode.dom.nodeName).equals("DIV") - o(vnode.dom.childNodes.length).equals(2) - o(vnode.dom.childNodes[0].nodeName).equals("A") - o(vnode.dom.childNodes[1].nodeName).equals("B") + o(vnode.d.nodeName).equals("DIV") + o(vnode.d.childNodes.length).equals(2) + o(vnode.d.childNodes[0].nodeName).equals("A") + o(vnode.d.childNodes[1].nodeName).equals("B") }) o("creates attrs and children", function() { var vnode = m("div", {id: "a", title: "b"}, m("a"), m("b")) - m.render(root, vnode) - - o(vnode.dom.nodeName).equals("DIV") - o(vnode.dom.attributes["id"].value).equals("a") - o(vnode.dom.attributes["title"].value).equals("b") - o(vnode.dom.childNodes.length).equals(2) - o(vnode.dom.childNodes[0].nodeName).equals("A") - o(vnode.dom.childNodes[1].nodeName).equals("B") + m.render(G.root, vnode) + + o(vnode.d.nodeName).equals("DIV") + o(vnode.d.attributes["id"].value).equals("a") + o(vnode.d.attributes["title"].value).equals("b") + o(vnode.d.childNodes.length).equals(2) + o(vnode.d.childNodes[0].nodeName).equals("A") + o(vnode.d.childNodes[1].nodeName).equals("B") }) /* eslint-disable no-script-url */ o("creates svg", function() { @@ -76,32 +73,32 @@ o.spec("createElement", function() { m("a", {"xlink:href": "javascript:;"}), m("foreignObject", m("body", {xmlns: "http://www.w3.org/1999/xhtml"})) ) - m.render(root, vnode) - - o(vnode.dom.nodeName).equals("svg") - o(vnode.dom.namespaceURI).equals("http://www.w3.org/2000/svg") - o(vnode.dom.firstChild.nodeName).equals("a") - o(vnode.dom.firstChild.namespaceURI).equals("http://www.w3.org/2000/svg") - o(vnode.dom.firstChild.attributes["href"].value).equals("javascript:;") - o(vnode.dom.firstChild.attributes["href"].namespaceURI).equals("http://www.w3.org/1999/xlink") - o(vnode.dom.childNodes[1].nodeName).equals("foreignObject") - o(vnode.dom.childNodes[1].firstChild.nodeName).equals("body") - o(vnode.dom.childNodes[1].firstChild.namespaceURI).equals("http://www.w3.org/1999/xhtml") + m.render(G.root, vnode) + + o(vnode.d.nodeName).equals("svg") + o(vnode.d.namespaceURI).equals("http://www.w3.org/2000/svg") + o(vnode.d.firstChild.nodeName).equals("a") + o(vnode.d.firstChild.namespaceURI).equals("http://www.w3.org/2000/svg") + o(vnode.d.firstChild.attributes["href"].value).equals("javascript:;") + o(vnode.d.firstChild.attributes["href"].namespaceURI).equals("http://www.w3.org/1999/xlink") + o(vnode.d.childNodes[1].nodeName).equals("foreignObject") + o(vnode.d.childNodes[1].firstChild.nodeName).equals("body") + o(vnode.d.childNodes[1].firstChild.namespaceURI).equals("http://www.w3.org/1999/xhtml") }) /* eslint-enable no-script-url */ o("sets attributes correctly for svg", function() { var vnode = m("svg", {viewBox: "0 0 100 100"}) - m.render(root, vnode) + m.render(G.root, vnode) - o(vnode.dom.attributes["viewBox"].value).equals("0 0 100 100") + o(vnode.d.attributes["viewBox"].value).equals("0 0 100 100") }) o("creates mathml", function() { var vnode = m("math", m("mrow")) - m.render(root, vnode) + m.render(G.root, vnode) - o(vnode.dom.nodeName).equals("math") - o(vnode.dom.namespaceURI).equals("http://www.w3.org/1998/Math/MathML") - o(vnode.dom.firstChild.nodeName).equals("mrow") - o(vnode.dom.firstChild.namespaceURI).equals("http://www.w3.org/1998/Math/MathML") + o(vnode.d.nodeName).equals("math") + o(vnode.d.namespaceURI).equals("http://www.w3.org/1998/Math/MathML") + o(vnode.d.firstChild.nodeName).equals("mrow") + o(vnode.d.firstChild.namespaceURI).equals("http://www.w3.org/1998/Math/MathML") }) }) diff --git a/tests/render/createFragment.js b/tests/render/createFragment.js index 43026a2c2..540808486 100644 --- a/tests/render/createFragment.js +++ b/tests/render/createFragment.js @@ -1,49 +1,46 @@ import o from "ospec" -import domMock from "../../test-utils/domMock.js" +import {setupGlobals} from "../../test-utils/global.js" + import m from "../../src/entry/mithril.esm.js" o.spec("createFragment", function() { - var $window, root - o.beforeEach(function() { - $window = domMock() - root = $window.document.createElement("div") - }) + var G = setupGlobals() o("creates fragment", function() { var vnode = m.normalize([m("a")]) - m.render(root, vnode) + m.render(G.root, vnode) - o(root.childNodes.length).equals(1) - o(root.childNodes[0].nodeName).equals("A") + o(G.root.childNodes.length).equals(1) + o(G.root.childNodes[0].nodeName).equals("A") }) o("handles empty fragment", function() { var vnode = m.normalize([]) - m.render(root, vnode) + m.render(G.root, vnode) - o(root.childNodes.length).equals(0) + o(G.root.childNodes.length).equals(0) }) o("handles childless fragment", function() { var vnode = m.normalize([]) - m.render(root, vnode) + m.render(G.root, vnode) - o(root.childNodes.length).equals(0) + o(G.root.childNodes.length).equals(0) }) o("handles multiple children", function() { var vnode = m.normalize([m("a"), m("b")]) - m.render(root, vnode) + m.render(G.root, vnode) - o(root.childNodes.length).equals(2) - o(root.childNodes[0].nodeName).equals("A") - o(root.childNodes[1].nodeName).equals("B") - o(vnode.children[0].dom).equals(root.childNodes[0]) + o(G.root.childNodes.length).equals(2) + o(G.root.childNodes[0].nodeName).equals("A") + o(G.root.childNodes[1].nodeName).equals("B") + o(vnode.c[0].d).equals(G.root.childNodes[0]) }) o("handles td", function() { var vnode = m.normalize([m("td")]) - m.render(root, vnode) + m.render(G.root, vnode) - o(root.childNodes.length).equals(1) - o(root.childNodes[0].nodeName).equals("TD") - o(vnode.children[0].dom).equals(root.childNodes[0]) + o(G.root.childNodes.length).equals(1) + o(G.root.childNodes[0].nodeName).equals("TD") + o(vnode.c[0].d).equals(G.root.childNodes[0]) }) }) diff --git a/tests/render/createNodes.js b/tests/render/createNodes.js index 0f7b4a1d8..9f99eb7ce 100644 --- a/tests/render/createNodes.js +++ b/tests/render/createNodes.js @@ -1,14 +1,11 @@ import o from "ospec" -import domMock from "../../test-utils/domMock.js" +import {setupGlobals} from "../../test-utils/global.js" + import m from "../../src/entry/mithril.esm.js" o.spec("createNodes", function() { - var $window, root - o.beforeEach(function() { - $window = domMock() - root = $window.document.createElement("div") - }) + var G = setupGlobals() o("creates nodes", function() { var vnodes = [ @@ -16,12 +13,12 @@ o.spec("createNodes", function() { "b", ["c"], ] - m.render(root, vnodes) + m.render(G.root, vnodes) - o(root.childNodes.length).equals(3) - o(root.childNodes[0].nodeName).equals("A") - o(root.childNodes[1].nodeValue).equals("b") - o(root.childNodes[2].nodeValue).equals("c") + o(G.root.childNodes.length).equals(3) + o(G.root.childNodes[0].nodeName).equals("A") + o(G.root.childNodes[1].nodeValue).equals("b") + o(G.root.childNodes[2].nodeValue).equals("c") }) o("ignores null", function() { var vnodes = [ @@ -30,12 +27,12 @@ o.spec("createNodes", function() { null, ["c"], ] - m.render(root, vnodes) + m.render(G.root, vnodes) - o(root.childNodes.length).equals(3) - o(root.childNodes[0].nodeName).equals("A") - o(root.childNodes[1].nodeValue).equals("b") - o(root.childNodes[2].nodeValue).equals("c") + o(G.root.childNodes.length).equals(3) + o(G.root.childNodes[0].nodeName).equals("A") + o(G.root.childNodes[1].nodeValue).equals("b") + o(G.root.childNodes[2].nodeValue).equals("c") }) o("ignores undefined", function() { var vnodes = [ @@ -44,11 +41,11 @@ o.spec("createNodes", function() { undefined, ["c"], ] - m.render(root, vnodes) + m.render(G.root, vnodes) - o(root.childNodes.length).equals(3) - o(root.childNodes[0].nodeName).equals("A") - o(root.childNodes[1].nodeValue).equals("b") - o(root.childNodes[2].nodeValue).equals("c") + o(G.root.childNodes.length).equals(3) + o(G.root.childNodes[0].nodeName).equals("A") + o(G.root.childNodes[1].nodeValue).equals("b") + o(G.root.childNodes[2].nodeValue).equals("c") }) }) diff --git a/tests/render/createText.js b/tests/render/createText.js index b7f92e001..745e94def 100644 --- a/tests/render/createText.js +++ b/tests/render/createText.js @@ -1,67 +1,64 @@ import o from "ospec" -import domMock from "../../test-utils/domMock.js" +import {setupGlobals} from "../../test-utils/global.js" + import m from "../../src/entry/mithril.esm.js" o.spec("createText", function() { - var $window, root - o.beforeEach(function() { - $window = domMock() - root = $window.document.createElement("div") - }) + var G = setupGlobals() o("creates string", function() { var vnode = "a" - m.render(root, vnode) + m.render(G.root, vnode) - o(root.firstChild.nodeName).equals("#text") - o(root.firstChild.nodeValue).equals("a") + o(G.root.firstChild.nodeName).equals("#text") + o(G.root.firstChild.nodeValue).equals("a") }) o("creates falsy string", function() { var vnode = "" - m.render(root, vnode) + m.render(G.root, vnode) - o(root.firstChild.nodeName).equals("#text") - o(root.firstChild.nodeValue).equals("") + o(G.root.firstChild.nodeName).equals("#text") + o(G.root.firstChild.nodeValue).equals("") }) o("creates number", function() { var vnode = 1 - m.render(root, vnode) + m.render(G.root, vnode) - o(root.firstChild.nodeName).equals("#text") - o(root.firstChild.nodeValue).equals("1") + o(G.root.firstChild.nodeName).equals("#text") + o(G.root.firstChild.nodeValue).equals("1") }) o("creates falsy number", function() { var vnode = 0 - m.render(root, vnode) + m.render(G.root, vnode) - o(root.firstChild.nodeName).equals("#text") - o(root.firstChild.nodeValue).equals("0") + o(G.root.firstChild.nodeName).equals("#text") + o(G.root.firstChild.nodeValue).equals("0") }) o("ignores true boolean", function() { var vnode = true - m.render(root, vnode) + m.render(G.root, vnode) - o(root.childNodes.length).equals(0) + o(G.root.childNodes.length).equals(0) }) o("creates false boolean", function() { var vnode = false - m.render(root, vnode) + m.render(G.root, vnode) - o(root.childNodes.length).equals(0) + o(G.root.childNodes.length).equals(0) }) o("creates spaces", function() { var vnode = " " - m.render(root, vnode) + m.render(G.root, vnode) - o(root.firstChild.nodeName).equals("#text") - o(root.firstChild.nodeValue).equals(" ") + o(G.root.firstChild.nodeName).equals("#text") + o(G.root.firstChild.nodeValue).equals(" ") }) o("ignores html", function() { var vnode = "™" - m.render(root, vnode) + m.render(G.root, vnode) - o(root.firstChild.nodeName).equals("#text") - o(root.firstChild.nodeValue).equals("™") + o(G.root.firstChild.nodeName).equals("#text") + o(G.root.firstChild.nodeValue).equals("™") }) }) diff --git a/tests/render/event.js b/tests/render/event.js index dda7c3af8..ccded0cd4 100644 --- a/tests/render/event.js +++ b/tests/render/event.js @@ -1,18 +1,16 @@ import o from "ospec" -import domMock from "../../test-utils/domMock.js" +import {setupGlobals} from "../../test-utils/global.js" + import m from "../../src/entry/mithril.esm.js" o.spec("event", function() { - var $window, root, redraw, render - o.beforeEach(function() { - $window = domMock() - root = $window.document.body - redraw = o.spy() - render = function(dom, vnode) { - return m.render(dom, vnode, redraw) - } - }) + var redraw + var G = setupGlobals({initialize() { redraw = o.spy() }}) + + function render(dom, vnode) { + return m.render(dom, vnode, redraw) + } function eventSpy(fn) { function spy(e) { @@ -31,22 +29,22 @@ o.spec("event", function() { var spyParent = eventSpy() var div = m("div", {onclick: spyDiv}) var parent = m("div", {onclick: spyParent}, div) - var e = $window.document.createEvent("MouseEvents") + var e = G.window.document.createEvent("MouseEvents") e.initEvent("click", true, true) - render(root, parent) - div.dom.dispatchEvent(e) + render(G.root, parent) + div.d.dispatchEvent(e) o(spyDiv.calls.length).equals(1) - o(spyDiv.calls[0].this).equals(div.dom) + o(spyDiv.calls[0].this).equals(div.d) o(spyDiv.calls[0].type).equals("click") - o(spyDiv.calls[0].target).equals(div.dom) - o(spyDiv.calls[0].currentTarget).equals(div.dom) + o(spyDiv.calls[0].target).equals(div.d) + o(spyDiv.calls[0].currentTarget).equals(div.d) o(spyParent.calls.length).equals(1) - o(spyParent.calls[0].this).equals(parent.dom) + o(spyParent.calls[0].this).equals(parent.d) o(spyParent.calls[0].type).equals("click") - o(spyParent.calls[0].target).equals(div.dom) - o(spyParent.calls[0].currentTarget).equals(parent.dom) + o(spyParent.calls[0].target).equals(div.d) + o(spyParent.calls[0].currentTarget).equals(parent.d) o(redraw.callCount).equals(2) o(redraw.this).equals(undefined) o(redraw.args.length).equals(0) @@ -59,22 +57,22 @@ o.spec("event", function() { var spyParent = eventSpy() var div = m("div", {onclick: spyDiv}) var parent = m("div", {onclick: spyParent}, div) - var e = $window.document.createEvent("MouseEvents") + var e = G.window.document.createEvent("MouseEvents") e.initEvent("click", true, true) - render(root, parent) - div.dom.dispatchEvent(e) + render(G.root, parent) + div.d.dispatchEvent(e) o(spyDiv.calls.length).equals(1) - o(spyDiv.calls[0].this).equals(div.dom) + o(spyDiv.calls[0].this).equals(div.d) o(spyDiv.calls[0].type).equals("click") - o(spyDiv.calls[0].target).equals(div.dom) - o(spyDiv.calls[0].currentTarget).equals(div.dom) + o(spyDiv.calls[0].target).equals(div.d) + o(spyDiv.calls[0].currentTarget).equals(div.d) o(spyParent.calls.length).equals(1) - o(spyParent.calls[0].this).equals(parent.dom) + o(spyParent.calls[0].this).equals(parent.d) o(spyParent.calls[0].type).equals("click") - o(spyParent.calls[0].target).equals(div.dom) - o(spyParent.calls[0].currentTarget).equals(parent.dom) + o(spyParent.calls[0].target).equals(div.d) + o(spyParent.calls[0].currentTarget).equals(parent.d) o(redraw.callCount).equals(1) o(redraw.this).equals(undefined) o(redraw.args.length).equals(0) @@ -92,17 +90,17 @@ o.spec("event", function() { var spyParent = eventSpy() var div = m("div", {onclick: spyDiv}) var parent = m("div", {onclick: spyParent}, div) - var e = $window.document.createEvent("MouseEvents") + var e = G.window.document.createEvent("MouseEvents") e.initEvent("click", true, true) - render(root, parent) - div.dom.dispatchEvent(e) + render(G.root, parent) + div.d.dispatchEvent(e) o(spyDiv.calls.length).equals(1) - o(spyDiv.calls[0].this).equals(div.dom) + o(spyDiv.calls[0].this).equals(div.d) o(spyDiv.calls[0].type).equals("click") - o(spyDiv.calls[0].target).equals(div.dom) - o(spyDiv.calls[0].currentTarget).equals(div.dom) + o(spyDiv.calls[0].target).equals(div.d) + o(spyDiv.calls[0].currentTarget).equals(div.d) o(spyParent.calls.length).equals(0) o(redraw.callCount).equals(0) o(e.defaultPrevented).equals(true) @@ -114,17 +112,17 @@ o.spec("event", function() { var spyParent = eventSpy() var div = m("div", {onclick: spyDiv}) var parent = m("div", {onclick: spyParent}, div) - var e = $window.document.createEvent("MouseEvents") + var e = G.window.document.createEvent("MouseEvents") e.initEvent("click", true, true) - render(root, parent) - div.dom.dispatchEvent(e) + render(G.root, parent) + div.d.dispatchEvent(e) o(spyDiv.calls.length).equals(1) - o(spyDiv.calls[0].this).equals(div.dom) + o(spyDiv.calls[0].this).equals(div.d) o(spyDiv.calls[0].type).equals("click") - o(spyDiv.calls[0].target).equals(div.dom) - o(spyDiv.calls[0].currentTarget).equals(div.dom) + o(spyDiv.calls[0].target).equals(div.d) + o(spyDiv.calls[0].currentTarget).equals(div.d) o(spyParent.calls.length).equals(0) o(redraw.callCount).equals(0) o(e.defaultPrevented).equals(true) @@ -139,17 +137,17 @@ o.spec("event", function() { var spyParent = eventSpy() var div = m("div", {onclick: spyDiv}) var parent = m("div", {onclick: spyParent}, div) - var e = $window.document.createEvent("MouseEvents") + var e = G.window.document.createEvent("MouseEvents") e.initEvent("click", true, true) - render(root, parent) - div.dom.dispatchEvent(e) + render(G.root, parent) + div.d.dispatchEvent(e) o(spyDiv.calls.length).equals(1) - o(spyDiv.calls[0].this).equals(div.dom) + o(spyDiv.calls[0].this).equals(div.d) o(spyDiv.calls[0].type).equals("click") - o(spyDiv.calls[0].target).equals(div.dom) - o(spyDiv.calls[0].currentTarget).equals(div.dom) + o(spyDiv.calls[0].target).equals(div.d) + o(spyDiv.calls[0].currentTarget).equals(div.d) o(spyParent.calls.length).equals(1) o(redraw.callCount).equals(1) o(e.defaultPrevented).equals(false) @@ -160,12 +158,12 @@ o.spec("event", function() { var vnode = m("a", {onclick: spy}) var updated = m("a") - render(root, vnode) - render(root, updated) + render(G.root, vnode) + render(G.root, updated) - var e = $window.document.createEvent("MouseEvents") + var e = G.window.document.createEvent("MouseEvents") e.initEvent("click", true, true) - vnode.dom.dispatchEvent(e) + vnode.d.dispatchEvent(e) o(spy.callCount).equals(0) }) @@ -175,12 +173,12 @@ o.spec("event", function() { var vnode = m("a", {onclick: spy}) var updated = m("a", {onclick: null}) - render(root, vnode) - render(root, updated) + render(G.root, vnode) + render(G.root, updated) - var e = $window.document.createEvent("MouseEvents") + var e = G.window.document.createEvent("MouseEvents") e.initEvent("click", true, true) - vnode.dom.dispatchEvent(e) + vnode.d.dispatchEvent(e) o(spy.callCount).equals(0) }) @@ -190,12 +188,12 @@ o.spec("event", function() { var vnode = m("a", {onclick: spy}) var updated = m("a", {onclick: undefined}) - render(root, vnode) - render(root, updated) + render(G.root, vnode) + render(G.root, updated) - var e = $window.document.createEvent("MouseEvents") + var e = G.window.document.createEvent("MouseEvents") e.initEvent("click", true, true) - vnode.dom.dispatchEvent(e) + vnode.d.dispatchEvent(e) o(spy.callCount).equals(0) }) @@ -205,12 +203,12 @@ o.spec("event", function() { var vnode = m("a", {ontouchstart: spy}) var updated = m("a", {ontouchstart: null}) - render(root, vnode) - render(root, updated) + render(G.root, vnode) + render(G.root, updated) - var e = $window.document.createEvent("TouchEvents") + var e = G.window.document.createEvent("TouchEvents") e.initEvent("touchstart", true, true) - vnode.dom.dispatchEvent(e) + vnode.d.dispatchEvent(e) o(spy.callCount).equals(0) }) @@ -220,12 +218,12 @@ o.spec("event", function() { var vnode = m("a", {ontouchstart: spy}) var updated = m("a") - render(root, vnode) - render(root, updated) + render(G.root, vnode) + render(G.root, updated) - var e = $window.document.createEvent("TouchEvents") + var e = G.window.document.createEvent("TouchEvents") e.initEvent("touchstart", true, true) - vnode.dom.dispatchEvent(e) + vnode.d.dispatchEvent(e) o(spy.callCount).equals(0) }) @@ -235,12 +233,12 @@ o.spec("event", function() { var vnode = m("a", {ontouchstart: spy}) var updated = m("a", {ontouchstart: undefined}) - render(root, vnode) - render(root, updated) + render(G.root, vnode) + render(G.root, updated) - var e = $window.document.createEvent("TouchEvents") + var e = G.window.document.createEvent("TouchEvents") e.initEvent("touchstart", true, true) - vnode.dom.dispatchEvent(e) + vnode.d.dispatchEvent(e) o(spy.callCount).equals(0) }) @@ -249,37 +247,37 @@ o.spec("event", function() { var spy = o.spy() var div = m("div", {id: "a", onclick: spy}) var updated = m("div", {id: "b", onclick: spy}) - var e = $window.document.createEvent("MouseEvents") + var e = G.window.document.createEvent("MouseEvents") e.initEvent("click", true, true) - render(root, div) - render(root, updated) - div.dom.dispatchEvent(e) + render(G.root, div) + render(G.root, updated) + div.d.dispatchEvent(e) o(spy.callCount).equals(1) - o(spy.this).equals(div.dom) + o(spy.this).equals(div.d) o(spy.args[0].type).equals("click") - o(spy.args[0].target).equals(div.dom) + o(spy.args[0].target).equals(div.d) o(redraw.callCount).equals(1) o(redraw.this).equals(undefined) o(redraw.args.length).equals(0) - o(div.dom).equals(updated.dom) - o(div.dom.attributes["id"].value).equals("b") + o(div.d).equals(updated.d) + o(div.d.attributes["id"].value).equals("b") }) o("handles ontransitionend", function() { var spy = o.spy() var div = m("div", {ontransitionend: spy}) - var e = $window.document.createEvent("HTMLEvents") + var e = G.window.document.createEvent("HTMLEvents") e.initEvent("transitionend", true, true) - render(root, div) - div.dom.dispatchEvent(e) + render(G.root, div) + div.d.dispatchEvent(e) o(spy.callCount).equals(1) - o(spy.this).equals(div.dom) + o(spy.this).equals(div.d) o(spy.args[0].type).equals("transitionend") - o(spy.args[0].target).equals(div.dom) + o(spy.args[0].target).equals(div.d) o(redraw.callCount).equals(1) o(redraw.this).equals(undefined) o(redraw.args.length).equals(0) @@ -288,10 +286,10 @@ o.spec("event", function() { o("handles changed spy", function() { var div1 = m("div", {ontransitionend: function() {}}) - m.render(root, [div1], redraw) - var e = $window.document.createEvent("HTMLEvents") + m.render(G.root, [div1], redraw) + var e = G.window.document.createEvent("HTMLEvents") e.initEvent("transitionend", true, true) - div1.dom.dispatchEvent(e) + div1.d.dispatchEvent(e) o(redraw.callCount).equals(1) o(redraw.this).equals(undefined) @@ -300,10 +298,10 @@ o.spec("event", function() { var replacementRedraw = o.spy() var div2 = m("div", {ontransitionend: function() {}}) - m.render(root, [div2], replacementRedraw) - var e = $window.document.createEvent("HTMLEvents") + m.render(G.root, [div2], replacementRedraw) + var e = G.window.document.createEvent("HTMLEvents") e.initEvent("transitionend", true, true) - div2.dom.dispatchEvent(e) + div2.d.dispatchEvent(e) o(redraw.callCount).equals(1) o(redraw.this).equals(undefined) diff --git a/tests/render/fragment.js b/tests/render/fragment.js index 72a1ecac7..c3191733f 100644 --- a/tests/render/fragment.js +++ b/tests/render/fragment.js @@ -1,3 +1,4 @@ +/* eslint-disable no-bitwise */ import o from "ospec" import m from "../../src/entry/mithril.esm.js" @@ -7,82 +8,82 @@ o.spec("fragment literal", function() { var child = m("p") var frag = m.normalize([child]) - o(frag.tag).equals(Symbol.for("m.Fragment")) + o(frag.m & m.TYPE_MASK).equals(m.TYPE_FRAGMENT) - o(Array.isArray(frag.children)).equals(true) - o(frag.children.length).equals(1) - o(frag.children[0]).equals(child) + o(Array.isArray(frag.c)).equals(true) + o(frag.c.length).equals(1) + o(frag.c[0]).equals(child) }) o.spec("children", function() { o("handles string single child", function() { var vnode = m.normalize(["a"]) - o(vnode.children[0].tag).equals(Symbol.for("m.text")) - o(vnode.children[0].state).equals("a") + o(vnode.c[0].m & m.TYPE_MASK).equals(m.TYPE_TEXT) + o(vnode.c[0].s).equals("a") }) o("handles falsy string single child", function() { var vnode = m.normalize([""]) - o(vnode.children[0].tag).equals(Symbol.for("m.text")) - o(vnode.children[0].state).equals("") + o(vnode.c[0].m & m.TYPE_MASK).equals(m.TYPE_TEXT) + o(vnode.c[0].s).equals("") }) o("handles number single child", function() { var vnode = m.normalize([1]) - o(vnode.children[0].tag).equals(Symbol.for("m.text")) - o(vnode.children[0].state).equals("1") + o(vnode.c[0].m & m.TYPE_MASK).equals(m.TYPE_TEXT) + o(vnode.c[0].s).equals("1") }) o("handles falsy number single child", function() { var vnode = m.normalize([0]) - o(vnode.children[0].tag).equals(Symbol.for("m.text")) - o(vnode.children[0].state).equals("0") + o(vnode.c[0].m & m.TYPE_MASK).equals(m.TYPE_TEXT) + o(vnode.c[0].s).equals("0") }) o("handles boolean single child", function() { var vnode = m.normalize([true]) - o(vnode.children).deepEquals([null]) + o(vnode.c).deepEquals([null]) }) o("handles falsy boolean single child", function() { var vnode = m.normalize([false]) - o(vnode.children).deepEquals([null]) + o(vnode.c).deepEquals([null]) }) o("handles null single child", function() { var vnode = m.normalize([null]) - o(vnode.children[0]).equals(null) + o(vnode.c[0]).equals(null) }) o("handles undefined single child", function() { var vnode = m.normalize([undefined]) - o(vnode.children).deepEquals([null]) + o(vnode.c).deepEquals([null]) }) o("handles multiple string children", function() { var vnode = m.normalize(["", "a"]) - o(vnode.children[0].tag).equals(Symbol.for("m.text")) - o(vnode.children[0].state).equals("") - o(vnode.children[1].tag).equals(Symbol.for("m.text")) - o(vnode.children[1].state).equals("a") + o(vnode.c[0].m & m.TYPE_MASK).equals(m.TYPE_TEXT) + o(vnode.c[0].s).equals("") + o(vnode.c[1].m & m.TYPE_MASK).equals(m.TYPE_TEXT) + o(vnode.c[1].s).equals("a") }) o("handles multiple number children", function() { var vnode = m.normalize([0, 1]) - o(vnode.children[0].tag).equals(Symbol.for("m.text")) - o(vnode.children[0].state).equals("0") - o(vnode.children[1].tag).equals(Symbol.for("m.text")) - o(vnode.children[1].state).equals("1") + o(vnode.c[0].m & m.TYPE_MASK).equals(m.TYPE_TEXT) + o(vnode.c[0].s).equals("0") + o(vnode.c[1].m & m.TYPE_MASK).equals(m.TYPE_TEXT) + o(vnode.c[1].s).equals("1") }) o("handles multiple boolean children", function() { var vnode = m.normalize([false, true]) - o(vnode.children).deepEquals([null, null]) + o(vnode.c).deepEquals([null, null]) }) o("handles multiple null/undefined child", function() { var vnode = m.normalize([null, undefined]) - o(vnode.children).deepEquals([null, null]) + o(vnode.c).deepEquals([null, null]) }) }) }) @@ -92,88 +93,88 @@ o.spec("fragment component", function() { var child = m("p") var frag = m(m.Fragment, null, child) - o(frag.tag).equals(Symbol.for("m.Fragment")) + o(frag.m & m.TYPE_MASK).equals(m.TYPE_FRAGMENT) - o(Array.isArray(frag.children)).equals(true) - o(frag.children.length).equals(1) - o(frag.children[0]).equals(child) + o(Array.isArray(frag.c)).equals(true) + o(frag.c.length).equals(1) + o(frag.c[0]).equals(child) }) o.spec("children", function() { o("handles string single child", function() { var vnode = m(m.Fragment, null, ["a"]) - o(vnode.children[0].tag).equals(Symbol.for("m.text")) - o(vnode.children[0].state).equals("a") + o(vnode.c[0].m & m.TYPE_MASK).equals(m.TYPE_TEXT) + o(vnode.c[0].s).equals("a") }) o("handles falsy string single child", function() { var vnode = m(m.Fragment, null, [""]) - o(vnode.children[0].tag).equals(Symbol.for("m.text")) - o(vnode.children[0].state).equals("") + o(vnode.c[0].m & m.TYPE_MASK).equals(m.TYPE_TEXT) + o(vnode.c[0].s).equals("") }) o("handles number single child", function() { var vnode = m(m.Fragment, null, [1]) - o(vnode.children[0].tag).equals(Symbol.for("m.text")) - o(vnode.children[0].state).equals("1") + o(vnode.c[0].m & m.TYPE_MASK).equals(m.TYPE_TEXT) + o(vnode.c[0].s).equals("1") }) o("handles falsy number single child", function() { var vnode = m(m.Fragment, null, [0]) - o(vnode.children[0].tag).equals(Symbol.for("m.text")) - o(vnode.children[0].state).equals("0") + o(vnode.c[0].m & m.TYPE_MASK).equals(m.TYPE_TEXT) + o(vnode.c[0].s).equals("0") }) o("handles boolean single child", function() { var vnode = m(m.Fragment, null, [true]) - o(vnode.children).deepEquals([null]) + o(vnode.c).deepEquals([null]) }) o("handles falsy boolean single child", function() { var vnode = m(m.Fragment, null, [false]) - o(vnode.children).deepEquals([null]) + o(vnode.c).deepEquals([null]) }) o("handles null single child", function() { var vnode = m(m.Fragment, null, [null]) - o(vnode.children[0]).equals(null) + o(vnode.c[0]).equals(null) }) o("handles undefined single child", function() { var vnode = m(m.Fragment, null, [undefined]) - o(vnode.children).deepEquals([null]) + o(vnode.c).deepEquals([null]) }) o("handles multiple string children", function() { var vnode = m(m.Fragment, null, ["", "a"]) - o(vnode.children[0].tag).equals(Symbol.for("m.text")) - o(vnode.children[0].state).equals("") - o(vnode.children[1].tag).equals(Symbol.for("m.text")) - o(vnode.children[1].state).equals("a") + o(vnode.c[0].m & m.TYPE_MASK).equals(m.TYPE_TEXT) + o(vnode.c[0].s).equals("") + o(vnode.c[1].m & m.TYPE_MASK).equals(m.TYPE_TEXT) + o(vnode.c[1].s).equals("a") }) o("handles multiple number children", function() { var vnode = m(m.Fragment, null, [0, 1]) - o(vnode.children[0].tag).equals(Symbol.for("m.text")) - o(vnode.children[0].state).equals("0") - o(vnode.children[1].tag).equals(Symbol.for("m.text")) - o(vnode.children[1].state).equals("1") + o(vnode.c[0].m & m.TYPE_MASK).equals(m.TYPE_TEXT) + o(vnode.c[0].s).equals("0") + o(vnode.c[1].m & m.TYPE_MASK).equals(m.TYPE_TEXT) + o(vnode.c[1].s).equals("1") }) o("handles multiple boolean children", function() { var vnode = m(m.Fragment, null, [false, true]) - o(vnode.children).deepEquals([null, null]) + o(vnode.c).deepEquals([null, null]) }) o("handles multiple null/undefined child", function() { var vnode = m(m.Fragment, null, [null, undefined]) - o(vnode.children).deepEquals([null, null]) + o(vnode.c).deepEquals([null, null]) }) o("handles falsy number single child without attrs", function() { var vnode = m(m.Fragment, null, 0) - o(vnode.children[0].tag).equals(Symbol.for("m.text")) - o(vnode.children[0].state).equals("0") + o(vnode.c[0].m & m.TYPE_MASK).equals(m.TYPE_TEXT) + o(vnode.c[0].s).equals("0") }) }) }) @@ -183,99 +184,99 @@ o.spec("key", function() { var child = m("p") var frag = m.key(undefined, child) - o(frag.tag).equals(Symbol.for("m.key")) + o(frag.m & m.TYPE_MASK).equals(m.TYPE_KEY) - o(Array.isArray(frag.children)).equals(true) - o(frag.children.length).equals(1) - o(frag.children[0]).equals(child) + o(Array.isArray(frag.c)).equals(true) + o(frag.c.length).equals(1) + o(frag.c[0]).equals(child) - o(frag.state).equals(undefined) + o(frag.t).equals(undefined) }) o("supports non-null keys", function() { var frag = m.key(7, []) - o(frag.tag).equals(Symbol.for("m.key")) + o(frag.m & m.TYPE_MASK).equals(m.TYPE_KEY) - o(Array.isArray(frag.children)).equals(true) - o(frag.children.length).equals(0) + o(Array.isArray(frag.c)).equals(true) + o(frag.c.length).equals(0) - o(frag.state).equals(7) + o(frag.t).equals(7) }) o.spec("children", function() { o("handles string single child", function() { var vnode = m.key("foo", ["a"]) - o(vnode.children[0].tag).equals(Symbol.for("m.text")) - o(vnode.children[0].state).equals("a") + o(vnode.c[0].m & m.TYPE_MASK).equals(m.TYPE_TEXT) + o(vnode.c[0].s).equals("a") }) o("handles falsy string single child", function() { var vnode = m.key("foo", [""]) - o(vnode.children[0].tag).equals(Symbol.for("m.text")) - o(vnode.children[0].state).equals("") + o(vnode.c[0].m & m.TYPE_MASK).equals(m.TYPE_TEXT) + o(vnode.c[0].s).equals("") }) o("handles number single child", function() { var vnode = m.key("foo", [1]) - o(vnode.children[0].tag).equals(Symbol.for("m.text")) - o(vnode.children[0].state).equals("1") + o(vnode.c[0].m & m.TYPE_MASK).equals(m.TYPE_TEXT) + o(vnode.c[0].s).equals("1") }) o("handles falsy number single child", function() { var vnode = m.key("foo", [0]) - o(vnode.children[0].tag).equals(Symbol.for("m.text")) - o(vnode.children[0].state).equals("0") + o(vnode.c[0].m & m.TYPE_MASK).equals(m.TYPE_TEXT) + o(vnode.c[0].s).equals("0") }) o("handles boolean single child", function() { var vnode = m.key("foo", [true]) - o(vnode.children).deepEquals([null]) + o(vnode.c).deepEquals([null]) }) o("handles falsy boolean single child", function() { var vnode = m.key("foo", [false]) - o(vnode.children).deepEquals([null]) + o(vnode.c).deepEquals([null]) }) o("handles null single child", function() { var vnode = m.key("foo", [null]) - o(vnode.children[0]).equals(null) + o(vnode.c[0]).equals(null) }) o("handles undefined single child", function() { var vnode = m.key("foo", [undefined]) - o(vnode.children).deepEquals([null]) + o(vnode.c).deepEquals([null]) }) o("handles multiple string children", function() { var vnode = m.key("foo", ["", "a"]) - o(vnode.children[0].tag).equals(Symbol.for("m.text")) - o(vnode.children[0].state).equals("") - o(vnode.children[1].tag).equals(Symbol.for("m.text")) - o(vnode.children[1].state).equals("a") + o(vnode.c[0].m & m.TYPE_MASK).equals(m.TYPE_TEXT) + o(vnode.c[0].s).equals("") + o(vnode.c[1].m & m.TYPE_MASK).equals(m.TYPE_TEXT) + o(vnode.c[1].s).equals("a") }) o("handles multiple number children", function() { var vnode = m.key("foo", [0, 1]) - o(vnode.children[0].tag).equals(Symbol.for("m.text")) - o(vnode.children[0].state).equals("0") - o(vnode.children[1].tag).equals(Symbol.for("m.text")) - o(vnode.children[1].state).equals("1") + o(vnode.c[0].m & m.TYPE_MASK).equals(m.TYPE_TEXT) + o(vnode.c[0].s).equals("0") + o(vnode.c[1].m & m.TYPE_MASK).equals(m.TYPE_TEXT) + o(vnode.c[1].s).equals("1") }) o("handles multiple boolean children", function() { var vnode = m.key("foo", [false, true]) - o(vnode.children).deepEquals([null, null]) + o(vnode.c).deepEquals([null, null]) }) o("handles multiple null/undefined child", function() { var vnode = m.key("foo", [null, undefined]) - o(vnode.children).deepEquals([null, null]) + o(vnode.c).deepEquals([null, null]) }) o("handles falsy number single child without attrs", function() { var vnode = m.key("foo", 0) - o(vnode.children[0].tag).equals(Symbol.for("m.text")) - o(vnode.children[0].state).equals("0") + o(vnode.c[0].m & m.TYPE_MASK).equals(m.TYPE_TEXT) + o(vnode.c[0].s).equals("0") }) }) }) diff --git a/tests/render/hyperscript.js b/tests/render/hyperscript.js index 78a36d203..d4f61a0f1 100644 --- a/tests/render/hyperscript.js +++ b/tests/render/hyperscript.js @@ -1,6 +1,8 @@ +/* eslint-disable no-bitwise */ import o from "ospec" -import domMock from "../../test-utils/domMock.js" +import {setupGlobals} from "../../test-utils/global.js" + import m from "../../src/entry/mithril.esm.js" o.spec("hyperscript", function() { @@ -14,92 +16,93 @@ o.spec("hyperscript", function() { o("handles tag in selector", function() { var vnode = m("a") - o(vnode.tag).equals("a") + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_ELEMENT) + o(vnode.t).equals("a") }) o("class and className normalization", function(){ o(m("a", { class: null - }).attrs).deepEquals({ + }).a).deepEquals({ class: null }) o(m("a", { class: undefined - }).attrs).deepEquals({ + }).a).deepEquals({ class: undefined }) o(m("a", { class: false - }).attrs).deepEquals({ + }).a).deepEquals({ class: false }) o(m("a", { class: true - }).attrs).deepEquals({ + }).a).deepEquals({ class: true }) o(m("a.x", { class: null - }).attrs).deepEquals({ + }).a).deepEquals({ class: "x" }) o(m("a.x", { class: undefined - }).attrs).deepEquals({ + }).a).deepEquals({ class: "x" }) o(m("a.x", { class: false - }).attrs).deepEquals({ + }).a).deepEquals({ class: "x false" }) o(m("a.x", { class: true - }).attrs).deepEquals({ + }).a).deepEquals({ class: "x true" }) o(m("a", { className: null - }).attrs).deepEquals({ + }).a).deepEquals({ className: null }) o(m("a", { className: undefined - }).attrs).deepEquals({ + }).a).deepEquals({ className: undefined }) o(m("a", { className: false - }).attrs).deepEquals({ + }).a).deepEquals({ className: null, class: false }) o(m("a", { className: true - }).attrs).deepEquals({ + }).a).deepEquals({ className: null, class: true }) o(m("a.x", { className: null - }).attrs).deepEquals({ + }).a).deepEquals({ className: null, class: "x" }) o(m("a.x", { className: undefined - }).attrs).deepEquals({ + }).a).deepEquals({ className: null, class: "x" }) o(m("a.x", { className: false - }).attrs).deepEquals({ + }).a).deepEquals({ className: null, class: "x false" }) o(m("a.x", { className: true - }).attrs).deepEquals({ + }).a).deepEquals({ className: null, class: "x true" }) @@ -107,254 +110,289 @@ o.spec("hyperscript", function() { o("handles class in selector", function() { var vnode = m(".a") - o(vnode.tag).equals("div") - o(vnode.attrs.class).equals("a") + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_ELEMENT) + o(vnode.t).equals(".a") + o(vnode.a.class).equals("a") }) o("handles many classes in selector", function() { var vnode = m(".a.b.c") - o(vnode.tag).equals("div") - o(vnode.attrs.class).equals("a b c") + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_ELEMENT) + o(vnode.t).equals(".a.b.c") + o(vnode.a.class).equals("a b c") }) o("handles id in selector", function() { var vnode = m("#a") - o(vnode.tag).equals("div") - o(vnode.attrs.id).equals("a") + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_ELEMENT) + o(vnode.t).equals("#a") + o(vnode.a.id).equals("a") }) o("handles attr in selector", function() { var vnode = m("[a=b]") - o(vnode.tag).equals("div") - o(vnode.attrs.a).equals("b") + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_ELEMENT) + o(vnode.t).equals("[a=b]") + o(vnode.a.a).equals("b") }) o("handles many attrs in selector", function() { var vnode = m("[a=b][c=d]") - o(vnode.tag).equals("div") - o(vnode.attrs.a).equals("b") - o(vnode.attrs.c).equals("d") + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_ELEMENT) + o(vnode.t).equals("[a=b][c=d]") + o(vnode.a.a).equals("b") + o(vnode.a.c).equals("d") }) o("handles attr w/ spaces in selector", function() { var vnode = m("[a = b]") - o(vnode.tag).equals("div") - o(vnode.attrs.a).equals("b") + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_ELEMENT) + o(vnode.t).equals("[a = b]") + o(vnode.a.a).equals("b") }) o("handles attr w/ quotes in selector", function() { var vnode = m("[a='b']") - o(vnode.tag).equals("div") - o(vnode.attrs.a).equals("b") + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_ELEMENT) + o(vnode.t).equals("[a='b']") + o(vnode.a.a).equals("b") }) o("handles attr w/ quoted square bracket", function() { var vnode = m("[x][a='[b]'].c") - o(vnode.tag).equals("div") - o(vnode.attrs.x).equals(true) - o(vnode.attrs.a).equals("[b]") - o(vnode.attrs.class).equals("c") + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_ELEMENT) + o(vnode.t).equals("[x][a='[b]'].c") + o(vnode.a.x).equals(true) + o(vnode.a.a).equals("[b]") + o(vnode.a.class).equals("c") }) o("handles attr w/ unmatched square bracket", function() { var vnode = m("[a=']'].c") - o(vnode.tag).equals("div") - o(vnode.attrs.a).equals("]") - o(vnode.attrs.class).equals("c") + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_ELEMENT) + o(vnode.t).equals("[a=']'].c") + o(vnode.a.a).equals("]") + o(vnode.a.class).equals("c") }) o("handles attr w/ quoted square bracket and quote", function() { var vnode = m("[a='[b\"\\']'].c") // `[a='[b"\']']` - o(vnode.tag).equals("div") - o(vnode.attrs.a).equals("[b\"']") // `[b"']` - o(vnode.attrs.class).equals("c") + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_ELEMENT) + o(vnode.t).equals("[a='[b\"\\']'].c") + o(vnode.a.a).equals("[b\"']") // `[b"']` + o(vnode.a.class).equals("c") }) o("handles attr w/ quoted square containing escaped square bracket", function() { var vnode = m("[a='[\\]]'].c") // `[a='[\]]']` - o(vnode.tag).equals("div") - o(vnode.attrs.a).equals("[\\]]") // `[\]]` - o(vnode.attrs.class).equals("c") + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_ELEMENT) + o(vnode.t).equals("[a='[\\]]'].c") + o(vnode.a.a).equals("[\\]]") // `[\]]` + o(vnode.a.class).equals("c") }) o("handles attr w/ backslashes", function() { var vnode = m("[a='\\\\'].c") // `[a='\\']` - o(vnode.tag).equals("div") - o(vnode.attrs.a).equals("\\") - o(vnode.attrs.class).equals("c") + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_ELEMENT) + o(vnode.t).equals("[a='\\\\'].c") + o(vnode.a.a).equals("\\") + o(vnode.a.class).equals("c") }) o("handles attr w/ quotes and spaces in selector", function() { var vnode = m("[a = 'b']") - o(vnode.tag).equals("div") - o(vnode.attrs.a).equals("b") + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_ELEMENT) + o(vnode.t).equals("[a = 'b']") + o(vnode.a.a).equals("b") }) o("handles many attr w/ quotes and spaces in selector", function() { var vnode = m("[a = 'b'][c = 'd']") - o(vnode.tag).equals("div") - o(vnode.attrs.a).equals("b") - o(vnode.attrs.c).equals("d") + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_ELEMENT) + o(vnode.t).equals("[a = 'b'][c = 'd']") + o(vnode.a.a).equals("b") + o(vnode.a.c).equals("d") }) o("handles tag, class, attrs in selector", function() { var vnode = m("a.b[c = 'd']") - o(vnode.tag).equals("a") - o(vnode.attrs.class).equals("b") - o(vnode.attrs.c).equals("d") + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_ELEMENT) + o(vnode.t).equals("a.b[c = 'd']") + o(vnode.a.class).equals("b") + o(vnode.a.c).equals("d") }) o("handles tag, mixed classes, attrs in selector", function() { var vnode = m("a.b[c = 'd'].e[f = 'g']") - o(vnode.tag).equals("a") - o(vnode.attrs.class).equals("b e") - o(vnode.attrs.c).equals("d") - o(vnode.attrs.f).equals("g") + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_ELEMENT) + o(vnode.t).equals("a.b[c = 'd'].e[f = 'g']") + o(vnode.a.class).equals("b e") + o(vnode.a.c).equals("d") + o(vnode.a.f).equals("g") }) o("handles attr without value", function() { var vnode = m("[a]") - o(vnode.tag).equals("div") - o(vnode.attrs.a).equals(true) + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_ELEMENT) + o(vnode.t).equals("[a]") + o(vnode.a.a).equals(true) }) o("handles explicit empty string value for input", function() { var vnode = m('input[value=""]') - o(vnode.tag).equals("input") - o(vnode.attrs.value).equals("") + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_ELEMENT) + o(vnode.t).equals('input[value=""]') + o(vnode.a.value).equals("") }) o("handles explicit empty string value for option", function() { var vnode = m('option[value=""]') - o(vnode.tag).equals("option") - o(vnode.attrs.value).equals("") + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_ELEMENT) + o(vnode.t).equals('option[value=""]') + o(vnode.a.value).equals("") }) }) o.spec("attrs", function() { o("handles string attr", function() { var vnode = m("div", {a: "b"}) - o(vnode.tag).equals("div") - o(vnode.attrs.a).equals("b") + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_ELEMENT) + o(vnode.t).equals("div") + o(vnode.a.a).equals("b") }) o("handles falsy string attr", function() { var vnode = m("div", {a: ""}) - o(vnode.tag).equals("div") - o(vnode.attrs.a).equals("") + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_ELEMENT) + o(vnode.t).equals("div") + o(vnode.a.a).equals("") }) o("handles number attr", function() { var vnode = m("div", {a: 1}) - o(vnode.tag).equals("div") - o(vnode.attrs.a).equals(1) + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_ELEMENT) + o(vnode.t).equals("div") + o(vnode.a.a).equals(1) }) o("handles falsy number attr", function() { var vnode = m("div", {a: 0}) - o(vnode.tag).equals("div") - o(vnode.attrs.a).equals(0) + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_ELEMENT) + o(vnode.t).equals("div") + o(vnode.a.a).equals(0) }) o("handles boolean attr", function() { var vnode = m("div", {a: true}) - o(vnode.tag).equals("div") - o(vnode.attrs.a).equals(true) + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_ELEMENT) + o(vnode.t).equals("div") + o(vnode.a.a).equals(true) }) o("handles falsy boolean attr", function() { var vnode = m("div", {a: false}) - o(vnode.tag).equals("div") - o(vnode.attrs.a).equals(false) + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_ELEMENT) + o(vnode.t).equals("div") + o(vnode.a.a).equals(false) }) o("handles only key in attrs", function() { var vnode = m("div", {key: "a"}) - o(vnode.tag).equals("div") - o(vnode.attrs).deepEquals({key: "a"}) + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_ELEMENT) + o(vnode.t).equals("div") + o(vnode.a).deepEquals({key: "a"}) }) o("handles many attrs", function() { var vnode = m("div", {a: "b", c: "d"}) - o(vnode.tag).equals("div") - o(vnode.attrs.a).equals("b") - o(vnode.attrs.c).equals("d") + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_ELEMENT) + o(vnode.t).equals("div") + o(vnode.a.a).equals("b") + o(vnode.a.c).equals("d") }) o("handles className attrs property", function() { var vnode = m("div", {className: "a"}) - o(vnode.attrs.class).equals("a") + o(vnode.a.class).equals("a") }) o("handles 'class' as a verbose attribute declaration", function() { var vnode = m("[class=a]") - o(vnode.attrs.class).equals("a") + o(vnode.a.class).equals("a") }) o("handles merging classes w/ class property", function() { var vnode = m(".a", {class: "b"}) - o(vnode.attrs.class).equals("a b") + o(vnode.a.class).equals("a b") }) o("handles merging classes w/ className property", function() { var vnode = m(".a", {className: "b"}) - o(vnode.attrs.class).equals("a b") + o(vnode.a.class).equals("a b") }) }) o.spec("custom element attrs", function() { o("handles string attr", function() { var vnode = m("custom-element", {a: "b"}) - o(vnode.tag).equals("custom-element") - o(vnode.attrs.a).equals("b") + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_ELEMENT) + o(vnode.t).equals("custom-element") + o(vnode.a.a).equals("b") }) o("handles falsy string attr", function() { var vnode = m("custom-element", {a: ""}) - o(vnode.tag).equals("custom-element") - o(vnode.attrs.a).equals("") + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_ELEMENT) + o(vnode.t).equals("custom-element") + o(vnode.a.a).equals("") }) o("handles number attr", function() { var vnode = m("custom-element", {a: 1}) - o(vnode.tag).equals("custom-element") - o(vnode.attrs.a).equals(1) + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_ELEMENT) + o(vnode.t).equals("custom-element") + o(vnode.a.a).equals(1) }) o("handles falsy number attr", function() { var vnode = m("custom-element", {a: 0}) - o(vnode.tag).equals("custom-element") - o(vnode.attrs.a).equals(0) + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_ELEMENT) + o(vnode.t).equals("custom-element") + o(vnode.a.a).equals(0) }) o("handles boolean attr", function() { var vnode = m("custom-element", {a: true}) - o(vnode.tag).equals("custom-element") - o(vnode.attrs.a).equals(true) + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_ELEMENT) + o(vnode.t).equals("custom-element") + o(vnode.a.a).equals(true) }) o("handles falsy boolean attr", function() { var vnode = m("custom-element", {a: false}) - o(vnode.tag).equals("custom-element") - o(vnode.attrs.a).equals(false) + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_ELEMENT) + o(vnode.t).equals("custom-element") + o(vnode.a.a).equals(false) }) o("handles only key in attrs", function() { var vnode = m("custom-element", {key:"a"}) - o(vnode.tag).equals("custom-element") - o(vnode.attrs).deepEquals({key:"a"}) + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_ELEMENT) + o(vnode.t).equals("custom-element") + o(vnode.a).deepEquals({key:"a"}) }) o("handles many attrs", function() { var vnode = m("custom-element", {a: "b", c: "d"}) - o(vnode.tag).equals("custom-element") - o(vnode.attrs.a).equals("b") - o(vnode.attrs.c).equals("d") + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_ELEMENT) + o(vnode.t).equals("custom-element") + o(vnode.a.a).equals("b") + o(vnode.a.c).equals("d") }) o("handles className attrs property", function() { var vnode = m("custom-element", {className: "a"}) - o(vnode.attrs.class).equals("a") + o(vnode.a.class).equals("a") }) o("casts className using toString like browsers", function() { const className = { @@ -363,216 +401,231 @@ o.spec("hyperscript", function() { } var vnode = m("custom-element" + className, {className: className}) - o(vnode.attrs.class).equals("valueOf toString") + o(vnode.a.class).equals("valueOf toString") }) }) o.spec("children", function() { o("handles string single child", function() { var vnode = m("div", {}, ["a"]) - o(vnode.children[0].state).equals("a") + o(vnode.c[0].s).equals("a") }) o("handles falsy string single child", function() { var vnode = m("div", {}, [""]) - o(vnode.children[0].state).equals("") + o(vnode.c[0].s).equals("") }) o("handles number single child", function() { var vnode = m("div", {}, [1]) - o(vnode.children[0].state).equals("1") + o(vnode.c[0].s).equals("1") }) o("handles falsy number single child", function() { var vnode = m("div", {}, [0]) - o(vnode.children[0].state).equals("0") + o(vnode.c[0].s).equals("0") }) o("handles boolean single child", function() { var vnode = m("div", {}, [true]) - o(vnode.children).deepEquals([null]) + o(vnode.c).deepEquals([null]) }) o("handles falsy boolean single child", function() { var vnode = m("div", {}, [false]) - o(vnode.children).deepEquals([null]) + o(vnode.c).deepEquals([null]) }) o("handles null single child", function() { var vnode = m("div", {}, [null]) - o(vnode.children).deepEquals([null]) + o(vnode.c).deepEquals([null]) }) o("handles undefined single child", function() { var vnode = m("div", {}, [undefined]) - o(vnode.children).deepEquals([null]) + o(vnode.c).deepEquals([null]) }) o("handles multiple string children", function() { var vnode = m("div", {}, ["", "a"]) - o(vnode.children[0].tag).equals(Symbol.for("m.text")) - o(vnode.children[0].state).equals("") - o(vnode.children[1].tag).equals(Symbol.for("m.text")) - o(vnode.children[1].state).equals("a") + o(vnode.c[0].m & m.TYPE_MASK).equals(m.TYPE_TEXT) + o(vnode.c[0].s).equals("") + o(vnode.c[1].m & m.TYPE_MASK).equals(m.TYPE_TEXT) + o(vnode.c[1].s).equals("a") }) o("handles multiple number children", function() { var vnode = m("div", {}, [0, 1]) - o(vnode.children[0].tag).equals(Symbol.for("m.text")) - o(vnode.children[0].state).equals("0") - o(vnode.children[1].tag).equals(Symbol.for("m.text")) - o(vnode.children[1].state).equals("1") + o(vnode.c[0].m & m.TYPE_MASK).equals(m.TYPE_TEXT) + o(vnode.c[0].s).equals("0") + o(vnode.c[1].m & m.TYPE_MASK).equals(m.TYPE_TEXT) + o(vnode.c[1].s).equals("1") }) o("handles multiple boolean children", function() { var vnode = m("div", {}, [false, true]) - o(vnode.children).deepEquals([null, null]) + o(vnode.c).deepEquals([null, null]) }) o("handles multiple null/undefined child", function() { var vnode = m("div", {}, [null, undefined]) - o(vnode.children).deepEquals([null, null]) + o(vnode.c).deepEquals([null, null]) }) o("handles falsy number single child without attrs", function() { var vnode = m("div", 0) - o(vnode.children[0].state).equals("0") + o(vnode.c[0].s).equals("0") }) o("handles children in attributes", function() { var vnode = m("div", {children: ["", "a"]}) - o(vnode.children[0].tag).equals(Symbol.for("m.text")) - o(vnode.children[0].state).equals("") - o(vnode.children[1].tag).equals(Symbol.for("m.text")) - o(vnode.children[1].state).equals("a") + o(vnode.c[0].m & m.TYPE_MASK).equals(m.TYPE_TEXT) + o(vnode.c[0].s).equals("") + o(vnode.c[1].m & m.TYPE_MASK).equals(m.TYPE_TEXT) + o(vnode.c[1].s).equals("a") }) }) o.spec("permutations", function() { o("handles null attr and children", function() { var vnode = m("div", null, [m("a"), m("b")]) - o(vnode.children.length).equals(2) - o(vnode.children[0].tag).equals("a") - o(vnode.children[1].tag).equals("b") + o(vnode.c.length).equals(2) + o(vnode.c[0].m & m.TYPE_MASK).equals(m.TYPE_ELEMENT) + o(vnode.c[0].t).equals("a") + o(vnode.c[1].m & m.TYPE_MASK).equals(m.TYPE_ELEMENT) + o(vnode.c[1].t).equals("b") }) o("handles null attr and child unwrapped", function() { var vnode = m("div", null, m("a")) - o(vnode.children.length).equals(1) - o(vnode.children[0].tag).equals("a") + o(vnode.c.length).equals(1) + o(vnode.c[0].m & m.TYPE_MASK).equals(m.TYPE_ELEMENT) + o(vnode.c[0].t).equals("a") }) o("handles null attr and children unwrapped", function() { var vnode = m("div", null, m("a"), m("b")) - o(vnode.children.length).equals(2) - o(vnode.children[0].tag).equals("a") - o(vnode.children[1].tag).equals("b") + o(vnode.c.length).equals(2) + o(vnode.c[0].m & m.TYPE_MASK).equals(m.TYPE_ELEMENT) + o(vnode.c[0].t).equals("a") + o(vnode.c[1].m & m.TYPE_MASK).equals(m.TYPE_ELEMENT) + o(vnode.c[1].t).equals("b") }) o("handles attr and children", function() { var vnode = m("div", {a: "b"}, [m("i"), m("s")]) - o(vnode.attrs.a).equals("b") - o(vnode.children[0].tag).equals("i") - o(vnode.children[1].tag).equals("s") + o(vnode.a.a).equals("b") + o(vnode.c[0].m & m.TYPE_MASK).equals(m.TYPE_ELEMENT) + o(vnode.c[0].t).equals("i") + o(vnode.c[1].m & m.TYPE_MASK).equals(m.TYPE_ELEMENT) + o(vnode.c[1].t).equals("s") }) o("handles attr and child unwrapped", function() { var vnode = m("div", {a: "b"}, m("i")) - o(vnode.attrs.a).equals("b") - o(vnode.children[0].tag).equals("i") + o(vnode.a.a).equals("b") + o(vnode.c[0].m & m.TYPE_MASK).equals(m.TYPE_ELEMENT) + o(vnode.c[0].t).equals("i") }) o("handles attr and children unwrapped", function() { var vnode = m("div", {a: "b"}, m("i"), m("s")) - o(vnode.attrs.a).equals("b") - o(vnode.children[0].tag).equals("i") - o(vnode.children[1].tag).equals("s") + o(vnode.a.a).equals("b") + o(vnode.c[0].m & m.TYPE_MASK).equals(m.TYPE_ELEMENT) + o(vnode.c[0].t).equals("i") + o(vnode.c[1].m & m.TYPE_MASK).equals(m.TYPE_ELEMENT) + o(vnode.c[1].t).equals("s") }) o("handles attr and text children", function() { var vnode = m("div", {a: "b"}, ["c", "d"]) - o(vnode.attrs.a).equals("b") - o(vnode.children[0].tag).equals(Symbol.for("m.text")) - o(vnode.children[0].state).equals("c") - o(vnode.children[1].tag).equals(Symbol.for("m.text")) - o(vnode.children[1].state).equals("d") + o(vnode.a.a).equals("b") + o(vnode.c[0].m & m.TYPE_MASK).equals(m.TYPE_TEXT) + o(vnode.c[0].s).equals("c") + o(vnode.c[1].m & m.TYPE_MASK).equals(m.TYPE_TEXT) + o(vnode.c[1].s).equals("d") }) o("handles attr and single string text child", function() { var vnode = m("div", {a: "b"}, ["c"]) - o(vnode.attrs.a).equals("b") - o(vnode.children[0].state).equals("c") + o(vnode.a.a).equals("b") + o(vnode.c[0].s).equals("c") }) o("handles attr and single falsy string text child", function() { var vnode = m("div", {a: "b"}, [""]) - o(vnode.attrs.a).equals("b") - o(vnode.children[0].state).equals("") + o(vnode.a.a).equals("b") + o(vnode.c[0].s).equals("") }) o("handles attr and single number text child", function() { var vnode = m("div", {a: "b"}, [1]) - o(vnode.attrs.a).equals("b") - o(vnode.children[0].state).equals("1") + o(vnode.a.a).equals("b") + o(vnode.c[0].s).equals("1") }) o("handles attr and single falsy number text child", function() { var vnode = m("div", {a: "b"}, [0]) - o(vnode.attrs.a).equals("b") - o(vnode.children[0].state).equals("0") + o(vnode.a.a).equals("b") + o(vnode.c[0].s).equals("0") }) o("handles attr and single boolean text child", function() { var vnode = m("div", {a: "b"}, [true]) - o(vnode.attrs.a).equals("b") - o(vnode.children).deepEquals([null]) + o(vnode.a.a).equals("b") + o(vnode.c).deepEquals([null]) }) o("handles attr and single falsy boolean text child", function() { var vnode = m("div", {a: "b"}, [0]) - o(vnode.attrs.a).equals("b") - o(vnode.children[0].state).equals("0") + o(vnode.a.a).equals("b") + o(vnode.c[0].s).equals("0") }) o("handles attr and single false boolean text child", function() { var vnode = m("div", {a: "b"}, [false]) - o(vnode.attrs.a).equals("b") - o(vnode.children).deepEquals([null]) + o(vnode.a.a).equals("b") + o(vnode.c).deepEquals([null]) }) o("handles attr and single text child unwrapped", function() { var vnode = m("div", {a: "b"}, "c") - o(vnode.attrs.a).equals("b") - o(vnode.children[0].state).equals("c") + o(vnode.a.a).equals("b") + o(vnode.c[0].s).equals("c") }) o("handles attr and text children unwrapped", function() { var vnode = m("div", {a: "b"}, "c", "d") - o(vnode.attrs.a).equals("b") - o(vnode.children[0].tag).equals(Symbol.for("m.text")) - o(vnode.children[0].state).equals("c") - o(vnode.children[1].tag).equals(Symbol.for("m.text")) - o(vnode.children[1].state).equals("d") + o(vnode.a.a).equals("b") + o(vnode.c[0].m & m.TYPE_MASK).equals(m.TYPE_TEXT) + o(vnode.c[0].s).equals("c") + o(vnode.c[1].m & m.TYPE_MASK).equals(m.TYPE_TEXT) + o(vnode.c[1].s).equals("d") }) o("handles children without attr", function() { var vnode = m("div", [m("i"), m("s")]) - o(vnode.attrs).deepEquals({}) - o(vnode.children[0].tag).equals("i") - o(vnode.children[1].tag).equals("s") + o(vnode.a).deepEquals({}) + o(vnode.c[0].m & m.TYPE_MASK).equals(m.TYPE_ELEMENT) + o(vnode.c[0].t).equals("i") + o(vnode.c[1].m & m.TYPE_MASK).equals(m.TYPE_ELEMENT) + o(vnode.c[1].t).equals("s") }) o("handles child without attr unwrapped", function() { var vnode = m("div", m("i")) - o(vnode.attrs).deepEquals({}) - o(vnode.children[0].tag).equals("i") + o(vnode.a).deepEquals({}) + o(vnode.c[0].m & m.TYPE_MASK).equals(m.TYPE_ELEMENT) + o(vnode.c[0].t).equals("i") }) o("handles children without attr unwrapped", function() { var vnode = m("div", m("i"), m("s")) - o(vnode.attrs).deepEquals({}) - o(vnode.children[0].tag).equals("i") - o(vnode.children[1].tag).equals("s") + o(vnode.a).deepEquals({}) + o(vnode.c[0].m & m.TYPE_MASK).equals(m.TYPE_ELEMENT) + o(vnode.c[0].t).equals("i") + o(vnode.c[1].m & m.TYPE_MASK).equals(m.TYPE_ELEMENT) + o(vnode.c[1].t).equals("s") }) o("handles shared attrs", function() { var attrs = {a: "b"} @@ -580,11 +633,11 @@ o.spec("hyperscript", function() { var nodeA = m(".a", attrs) var nodeB = m(".b", attrs) - o(nodeA.attrs.class).equals("a") - o(nodeA.attrs.a).equals("b") + o(nodeA.a.class).equals("a") + o(nodeA.a.a).equals("b") - o(nodeB.attrs.class).equals("b") - o(nodeB.attrs.a).equals("b") + o(nodeB.a.class).equals("b") + o(nodeB.a.a).equals("b") }) o("handles shared empty attrs (#2821)", function() { var attrs = {} @@ -592,8 +645,8 @@ o.spec("hyperscript", function() { var nodeA = m(".a", attrs) var nodeB = m(".b", attrs) - o(nodeA.attrs.class).equals("a") - o(nodeB.attrs.class).equals("b") + o(nodeA.a.class).equals("a") + o(nodeB.a.class).equals("b") }) o("doesnt modify passed attributes object", function() { var attrs = {a: "b"} @@ -601,36 +654,42 @@ o.spec("hyperscript", function() { o(attrs).deepEquals({a: "b"}) }) o("non-nullish attr takes precedence over selector", function() { - o(m("[a=b]", {a: "c"}).attrs).deepEquals({a: "c"}) + o(m("[a=b]", {a: "c"}).a).deepEquals({a: "c"}) }) o("null attr takes precedence over selector", function() { - o(m("[a=b]", {a: null}).attrs).deepEquals({a: null}) + o(m("[a=b]", {a: null}).a).deepEquals({a: null}) }) o("undefined attr takes precedence over selector", function() { - o(m("[a=b]", {a: undefined}).attrs).deepEquals({a: undefined}) + o(m("[a=b]", {a: undefined}).a).deepEquals({a: undefined}) }) o("handles fragment children without attr unwrapped", function() { var vnode = m("div", [m("i")], [m("s")]) - o(vnode.children[0].tag).equals(Symbol.for("m.Fragment")) - o(vnode.children[0].children[0].tag).equals("i") - o(vnode.children[1].tag).equals(Symbol.for("m.Fragment")) - o(vnode.children[1].children[0].tag).equals("s") + o(vnode.c[0].m & m.TYPE_MASK).equals(m.TYPE_FRAGMENT) + o(vnode.c[0].c[0].m & m.TYPE_MASK).equals(m.TYPE_ELEMENT) + o(vnode.c[0].c[0].t).equals("i") + o(vnode.c[1].m & m.TYPE_MASK).equals(m.TYPE_FRAGMENT) + o(vnode.c[1].c[0].m & m.TYPE_MASK).equals(m.TYPE_ELEMENT) + o(vnode.c[1].c[0].t).equals("s") }) o("handles children with nested array", function() { var vnode = m("div", [[m("i"), m("s")]]) - o(vnode.children[0].tag).equals(Symbol.for("m.Fragment")) - o(vnode.children[0].children[0].tag).equals("i") - o(vnode.children[0].children[1].tag).equals("s") + o(vnode.c[0].m & m.TYPE_MASK).equals(m.TYPE_FRAGMENT) + o(vnode.c[0].c[0].m & m.TYPE_MASK).equals(m.TYPE_ELEMENT) + o(vnode.c[0].c[0].t).equals("i") + o(vnode.c[0].c[1].m & m.TYPE_MASK).equals(m.TYPE_ELEMENT) + o(vnode.c[0].c[1].t).equals("s") }) o("handles children with deeply nested array", function() { var vnode = m("div", [[[m("i"), m("s")]]]) - o(vnode.children[0].tag).equals(Symbol.for("m.Fragment")) - o(vnode.children[0].children[0].tag).equals(Symbol.for("m.Fragment")) - o(vnode.children[0].children[0].children[0].tag).equals("i") - o(vnode.children[0].children[0].children[1].tag).equals("s") + o(vnode.c[0].m & m.TYPE_MASK).equals(m.TYPE_FRAGMENT) + o(vnode.c[0].c[0].m & m.TYPE_MASK).equals(m.TYPE_FRAGMENT) + o(vnode.c[0].c[0].c[0].m & m.TYPE_MASK).equals(m.TYPE_ELEMENT) + o(vnode.c[0].c[0].c[0].t).equals("i") + o(vnode.c[0].c[0].c[1].m & m.TYPE_MASK).equals(m.TYPE_ELEMENT) + o(vnode.c[0].c[0].c[1].t).equals("s") }) }) o.spec("components", function() { @@ -642,10 +701,11 @@ o.spec("hyperscript", function() { o(component.callCount).equals(0) - o(vnode.tag).equals(component) - o(vnode.attrs.id).equals("a") - o(vnode.attrs.children.length).equals(1) - o(vnode.attrs.children[0]).equals("b") + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_COMPONENT) + o(vnode.t).equals(component) + o(vnode.a.id).equals("a") + o(vnode.a.children.length).equals(1) + o(vnode.a.children[0]).equals("b") }) o("works with closures", function () { var component = o.spy() @@ -654,21 +714,23 @@ o.spec("hyperscript", function() { o(component.callCount).equals(0) - o(vnode.tag).equals(component) - o(vnode.attrs.id).equals("a") - o(vnode.attrs.children.length).equals(1) - o(vnode.attrs.children[0]).equals("b") + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_COMPONENT) + o(vnode.t).equals(component) + o(vnode.a.id).equals("a") + o(vnode.a.children.length).equals(1) + o(vnode.a.children[0]).equals("b") }) }) o.spec("capture", () => { + var G = setupGlobals() + o("works", () => { - var $window = domMock() - var e = $window.document.createEvent("MouseEvents") + var e = G.window.document.createEvent("MouseEvents") e.initEvent("click", true, true) // Only doing this for the sake of initializing the required fields in the mock. - $window.document.body.dispatchEvent(e) + G.root.dispatchEvent(e) o(m.capture(e)).equals(false) o(e.defaultPrevented).equals(true) diff --git a/tests/render/input.js b/tests/render/input.js index ef6fedcbb..1047dd2bc 100644 --- a/tests/render/input.js +++ b/tests/render/input.js @@ -1,29 +1,21 @@ import o from "ospec" -import domMock from "../../test-utils/domMock.js" +import {setupGlobals} from "../../test-utils/global.js" + import m from "../../src/entry/mithril.esm.js" o.spec("form inputs", function() { - var $window, root - o.beforeEach(function() { - $window = domMock() - root = $window.document.createElement("div") - $window.document.body.appendChild(root) - }) - o.afterEach(function() { - while (root.firstChild) root.removeChild(root.firstChild) - root.vnodes = null - }) + var G = setupGlobals() o.spec("input", function() { o("maintains focus after move", function() { var input - m.render(root, [m.key(1, input = m("input")), m.key(2, m("a")), m.key(3, m("b"))]) - input.dom.focus() - m.render(root, [m.key(2, m("a")), m.key(1, input = m("input")), m.key(3, m("b"))]) + m.render(G.root, [m.key(1, input = m("input")), m.key(2, m("a")), m.key(3, m("b"))]) + input.d.focus() + m.render(G.root, [m.key(2, m("a")), m.key(1, input = m("input")), m.key(3, m("b"))]) - o($window.document.activeElement).equals(input.dom) + o(G.window.document.activeElement).equals(input.d) }) o("maintains focus when changed manually in hook", function() { @@ -31,9 +23,9 @@ o.spec("form inputs", function() { dom.focus() })); - m.render(root, input) + m.render(G.root, input) - o($window.document.activeElement).equals(input.dom) + o(G.window.document.activeElement).equals(input.d) }) o("syncs input value if DOM value differs from vdom value", function() { @@ -41,20 +33,20 @@ o.spec("form inputs", function() { var updated = m("input", {value: "aaa", oninput: function() {}}) var redraw = o.spy() - m.render(root, input, redraw) + m.render(G.root, input, redraw) //simulate user typing - var e = $window.document.createEvent("KeyboardEvent") + var e = G.window.document.createEvent("KeyboardEvent") e.initEvent("input", true, true) - input.dom.focus() - input.dom.value += "a" - input.dom.dispatchEvent(e) + input.d.focus() + input.d.value += "a" + input.d.dispatchEvent(e) o(redraw.callCount).equals(1) //re-render may use same vdom value as previous render call - m.render(root, updated, redraw) + m.render(G.root, updated, redraw) - o(updated.dom.value).equals("aaa") + o(updated.d.value).equals("aaa") o(redraw.callCount).equals(1) }) @@ -62,10 +54,10 @@ o.spec("form inputs", function() { var input = m("input", {value: "aaa", oninput: function() {}}) var updated = m("input", {value: undefined, oninput: function() {}}) - m.render(root, input) - m.render(root, updated) + m.render(G.root, input) + m.render(G.root, updated) - o(updated.dom.value).equals("") + o(updated.d.value).equals("") }) o("syncs input checked attribute if DOM value differs from vdom value", function() { @@ -73,19 +65,19 @@ o.spec("form inputs", function() { var updated = m("input", {type: "checkbox", checked: true, onclick: function() {}}) var redraw = o.spy() - m.render(root, input, redraw) + m.render(G.root, input, redraw) //simulate user clicking checkbox - var e = $window.document.createEvent("MouseEvents") + var e = G.window.document.createEvent("MouseEvents") e.initEvent("click", true, true) - input.dom.focus() - input.dom.dispatchEvent(e) + input.d.focus() + input.d.dispatchEvent(e) o(redraw.callCount).equals(1) //re-render may use same vdom value as previous render call - m.render(root, updated, redraw) + m.render(G.root, updated, redraw) - o(updated.dom.checked).equals(true) + o(updated.d.checked).equals(true) o(redraw.callCount).equals(1) }) @@ -95,18 +87,18 @@ o.spec("form inputs", function() { var spy = o.spy() var error = console.error - m.render(root, input) + m.render(G.root, input) - input.dom.value = "test.png" + input.d.value = "test.png" try { console.error = spy - m.render(root, updated) + m.render(G.root, updated) } finally { console.error = error } - o(updated.dom.value).equals("") + o(updated.d.value).equals("") o(spy.callCount).equals(0) }) @@ -116,59 +108,58 @@ o.spec("form inputs", function() { var spy = o.spy() var error = console.error - m.render(root, input) + m.render(G.root, input) - input.dom.value = "test.png" + input.d.value = "test.png" try { console.error = spy - m.render(root, updated) + m.render(G.root, updated) } finally { console.error = error } - o(updated.dom.value).equals("test.png") + o(updated.d.value).equals("test.png") o(spy.callCount).equals(1) }) o("retains file input value attribute if DOM value is the same as vdom value and is non-empty", function() { - var $window = domMock(o) - var root = $window.document.createElement("div") - $window.document.body.appendChild(root) + G.initialize({spy: o.spy}) + var input = m("input", {type: "file", value: "", onclick: function() {}}) var updated1 = m("input", {type: "file", value: "test.png", onclick: function() {}}) var updated2 = m("input", {type: "file", value: "test.png", onclick: function() {}}) var spy = o.spy() var error = console.error - m.render(root, input) + m.render(G.root, input) // Verify our assumptions about the outer element state - o($window.__getSpies(input.dom).valueSetter.callCount).equals(0) - input.dom.value = "test.png" - o($window.__getSpies(input.dom).valueSetter.callCount).equals(1) + o(G.window.__getSpies(input.d).valueSetter.callCount).equals(0) + input.d.value = "test.png" + o(G.window.__getSpies(input.d).valueSetter.callCount).equals(1) try { console.error = spy - m.render(root, updated1) + m.render(G.root, updated1) } finally { console.error = error } - o(updated1.dom.value).equals("test.png") + o(updated1.d.value).equals("test.png") o(spy.callCount).equals(0) - o($window.__getSpies(updated1.dom).valueSetter.callCount).equals(1) + o(G.window.__getSpies(updated1.d).valueSetter.callCount).equals(1) try { console.error = spy - m.render(root, updated2) + m.render(G.root, updated2) } finally { console.error = error } - o(updated2.dom.value).equals("test.png") + o(updated2.d.value).equals("test.png") o(spy.callCount).equals(0) - o($window.__getSpies(updated2.dom).valueSetter.callCount).equals(1) + o(G.window.__getSpies(updated2.d).valueSetter.callCount).equals(1) }) }) @@ -178,10 +169,10 @@ o.spec("form inputs", function() { m("option", {value: "a"}, "aaa") ) - m.render(root, select) + m.render(G.root, select) - o(select.dom.value).equals("a") - o(select.dom.selectedIndex).equals(0) + o(select.d.value).equals("a") + o(select.d.selectedIndex).equals(0) }) o("select option can have empty string value", function() { @@ -189,9 +180,9 @@ o.spec("form inputs", function() { m("option", {value: ""}, "aaa") ) - m.render(root, select) + m.render(G.root, select) - o(select.dom.firstChild.value).equals("") + o(select.d.firstChild.value).equals("") }) o("option value defaults to textContent unless explicitly set", function() { @@ -199,49 +190,49 @@ o.spec("form inputs", function() { m("option", "aaa") ) - m.render(root, select) + m.render(G.root, select) - o(select.dom.firstChild.value).equals("aaa") - o(select.dom.value).equals("aaa") + o(select.d.firstChild.value).equals("aaa") + o(select.d.value).equals("aaa") //test that value changes when content changes select = m("select", m("option", "bbb") ) - m.render(root, select) + m.render(G.root, select) - o(select.dom.firstChild.value).equals("bbb") - o(select.dom.value).equals("bbb") + o(select.d.firstChild.value).equals("bbb") + o(select.d.value).equals("bbb") //test that value can be set to "" in subsequent render select = m("select", m("option", {value: ""}, "aaa") ) - m.render(root, select) + m.render(G.root, select) - o(select.dom.firstChild.value).equals("") - o(select.dom.value).equals("") + o(select.d.firstChild.value).equals("") + o(select.d.value).equals("") //test that value reverts to textContent when value omitted select = m("select", m("option", "aaa") ) - m.render(root, select) + m.render(G.root, select) - o(select.dom.firstChild.value).equals("aaa") - o(select.dom.value).equals("aaa") + o(select.d.firstChild.value).equals("aaa") + o(select.d.value).equals("aaa") }) o("select yields invalid value without children", function() { var select = m("select", {value: "a"}) - m.render(root, select) + m.render(G.root, select) - o(select.dom.value).equals("") - o(select.dom.selectedIndex).equals(-1) + o(select.d.value).equals("") + o(select.d.selectedIndex).equals(-1) }) o("select value is set correctly on first render", function() { @@ -251,10 +242,10 @@ o.spec("form inputs", function() { m("option", {value: "c"}, "ccc") ) - m.render(root, select) + m.render(G.root, select) - o(select.dom.value).equals("b") - o(select.dom.selectedIndex).equals(1) + o(select.d.value).equals("b") + o(select.d.selectedIndex).equals(1) }) o("syncs select value if DOM value differs from vdom value", function() { @@ -266,17 +257,17 @@ o.spec("form inputs", function() { ) } - m.render(root, makeSelect()) + m.render(G.root, makeSelect()) //simulate user selecting option - root.firstChild.value = "c" - root.firstChild.focus() + G.root.firstChild.value = "c" + G.root.firstChild.focus() //re-render may use same vdom value as previous render call - m.render(root, makeSelect()) + m.render(G.root, makeSelect()) - o(root.firstChild.value).equals("b") - o(root.firstChild.selectedIndex).equals(1) + o(G.root.firstChild.value).equals("b") + o(G.root.firstChild.selectedIndex).equals(1) }) }) }) diff --git a/tests/render/normalize.js b/tests/render/normalize.js index e1bbed4c6..88003feff 100644 --- a/tests/render/normalize.js +++ b/tests/render/normalize.js @@ -1,3 +1,4 @@ +/* eslint-disable no-bitwise */ import o from "ospec" import m from "../../src/entry/mithril.esm.js" @@ -6,40 +7,40 @@ o.spec("normalize", function() { o("normalizes array into fragment", function() { var node = m.normalize([]) - o(node.tag).equals(Symbol.for("m.Fragment")) - o(node.children.length).equals(0) + o(node.m & m.TYPE_MASK).equals(m.TYPE_FRAGMENT) + o(node.c.length).equals(0) }) o("normalizes nested array into fragment", function() { var node = m.normalize([[]]) - o(node.tag).equals(Symbol.for("m.Fragment")) - o(node.children.length).equals(1) - o(node.children[0].tag).equals(Symbol.for("m.Fragment")) - o(node.children[0].children.length).equals(0) + o(node.m & m.TYPE_MASK).equals(m.TYPE_FRAGMENT) + o(node.c.length).equals(1) + o(node.c[0].m & m.TYPE_MASK).equals(m.TYPE_FRAGMENT) + o(node.c[0].c.length).equals(0) }) o("normalizes string into text node", function() { var node = m.normalize("a") - o(node.tag).equals(Symbol.for("m.text")) - o(node.state).equals("a") + o(node.m & m.TYPE_MASK).equals(m.TYPE_TEXT) + o(node.s).equals("a") }) o("normalizes falsy string into text node", function() { var node = m.normalize("") - o(node.tag).equals(Symbol.for("m.text")) - o(node.state).equals("") + o(node.m & m.TYPE_MASK).equals(m.TYPE_TEXT) + o(node.s).equals("") }) o("normalizes number into text node", function() { var node = m.normalize(1) - o(node.tag).equals(Symbol.for("m.text")) - o(node.state).equals("1") + o(node.m & m.TYPE_MASK).equals(m.TYPE_TEXT) + o(node.s).equals("1") }) o("normalizes falsy number into text node", function() { var node = m.normalize(0) - o(node.tag).equals(Symbol.for("m.text")) - o(node.state).equals("0") + o(node.m & m.TYPE_MASK).equals(m.TYPE_TEXT) + o(node.s).equals("0") }) o("normalizes `true` to `null`", function() { var node = m.normalize(true) diff --git a/tests/render/normalizeChildren.js b/tests/render/normalizeChildren.js index 41bca0477..90cefdc6e 100644 --- a/tests/render/normalizeChildren.js +++ b/tests/render/normalizeChildren.js @@ -1,40 +1,41 @@ +/* eslint-disable no-bitwise */ import o from "ospec" import m from "../../src/entry/mithril.esm.js" o.spec("normalizeChildren", function() { o("normalizes arrays into fragments", function() { - var {children} = m.normalize([[]]) + var vnode = m.normalize([[]]) - o(children[0].tag).equals(Symbol.for("m.Fragment")) - o(children[0].children.length).equals(0) + o(vnode.c[0].m & m.TYPE_MASK).equals(m.TYPE_FRAGMENT) + o(vnode.c[0].c.length).equals(0) }) o("normalizes strings into text nodes", function() { - var {children} = m.normalize(["a"]) + var vnode = m.normalize(["a"]) - o(children[0].tag).equals(Symbol.for("m.text")) - o(children[0].state).equals("a") + o(vnode.c[0].m & m.TYPE_MASK).equals(m.TYPE_TEXT) + o(vnode.c[0].s).equals("a") }) o("normalizes `false` values into `null`s", function() { - var {children} = m.normalize([false]) + var vnode = m.normalize([false]) - o(children[0]).equals(null) + o(vnode.c[0]).equals(null) }) o("allows all keys", function() { - var {children} = m.normalize([ + var vnode = m.normalize([ m.key(1), m.key(2), ]) - o(children).deepEquals([m.key(1), m.key(2)]) + o(vnode.c).deepEquals([m.key(1), m.key(2)]) }) o("allows no keys", function() { - var {children} = m.normalize([ + var vnode = m.normalize([ m("foo1"), m("foo2"), ]) - o(children).deepEquals([m("foo1"), m("foo2")]) + o(vnode.c).deepEquals([m("foo1"), m("foo2")]) }) o("disallows mixed keys, starting with key", function() { o(() => m.normalize([ diff --git a/tests/render/normalizeComponentChildren.js b/tests/render/normalizeComponentChildren.js index 959cbab09..267f5a084 100644 --- a/tests/render/normalizeComponentChildren.js +++ b/tests/render/normalizeComponentChildren.js @@ -1,27 +1,26 @@ import o from "ospec" -import domMock from "../../test-utils/domMock.js" +import {setupGlobals} from "../../test-utils/global.js" + import m from "../../src/entry/mithril.esm.js" o.spec("component children", function () { - var $window = domMock() - var root = $window.document.createElement("div") + var G = setupGlobals() - o.spec("component children", function () { + o("are not normalized on ingestion", function () { var component = (attrs) => attrs.children - var vnode = m(component, "a") + m.render(G.root, vnode) + o(vnode.a.children[0]).equals("a") + }) - m.render(root, vnode) - - o("are not normalized on ingestion", function () { - o(vnode.attrs.children[0]).equals("a") - }) - - o("are normalized upon view interpolation", function () { - o(vnode.children.children.length).equals(1) - o(vnode.children.children[0].tag).equals(Symbol.for("m.text")) - o(vnode.children.children[0].state).equals("a") - }) + o("are normalized upon view interpolation", function () { + var component = (attrs) => attrs.children + var vnode = m(component, "a") + m.render(G.root, vnode) + o(vnode.c.c.length).equals(1) + // eslint-disable-next-line no-bitwise + o(vnode.c.c[0].m & m.TYPE_MASK).equals(m.TYPE_TEXT) + o(vnode.c.c[0].s).equals("a") }) }) diff --git a/tests/render/oncreate.js b/tests/render/oncreate.js index 49e6f6125..f34fa8e10 100644 --- a/tests/render/oncreate.js +++ b/tests/render/oncreate.js @@ -1,45 +1,39 @@ import o from "ospec" -import domMock from "../../test-utils/domMock.js" +import {setupGlobals} from "../../test-utils/global.js" + import m from "../../src/entry/mithril.esm.js" o.spec("layout create", function() { - var $window, root - o.beforeEach(function() { - $window = domMock() - root = $window.document.createElement("div") - }) + var G = setupGlobals() o("works when rendered directly", function() { var callback = o.spy() var vnode = m.layout(callback) - m.render(root, vnode) + m.render(G.root, vnode) o(callback.callCount).equals(1) - o(callback.args[0]).equals(root) + o(callback.args[0]).equals(G.root) o(callback.args[1].aborted).equals(false) - o(callback.args[2]).equals(true) }) o("works when creating element", function() { var callback = o.spy() var vnode = m("div", m.layout(callback)) - m.render(root, vnode) + m.render(G.root, vnode) o(callback.callCount).equals(1) o(callback.args[1].aborted).equals(false) - o(callback.args[2]).equals(true) }) o("works when creating fragment", function() { var callback = o.spy() var vnode = [m.layout(callback)] - m.render(root, vnode) + m.render(G.root, vnode) o(callback.callCount).equals(1) o(callback.args[1].aborted).equals(false) - o(callback.args[2]).equals(true) }) o("works when replacing same-keyed", function() { var createDiv = o.spy() @@ -47,49 +41,45 @@ o.spec("layout create", function() { var vnode = m("div", m.layout(createDiv)) var updated = m("a", m.layout(createA)) - m.render(root, m.key(1, vnode)) - m.render(root, m.key(1, updated)) + m.render(G.root, m.key(1, vnode)) + m.render(G.root, m.key(1, updated)) o(createDiv.callCount).equals(1) o(createDiv.args[1].aborted).equals(true) - o(createDiv.args[2]).equals(true) o(createA.callCount).equals(1) o(createA.args[1].aborted).equals(false) - o(createA.args[2]).equals(true) }) o("works when creating other children", function() { var create = o.spy() var vnode = m("div", m.layout(create), m("a")) - m.render(root, vnode) + m.render(G.root, vnode) o(create.callCount).equals(1) - o(create.args[0]).equals(root.firstChild) + o(create.args[0]).equals(G.root.firstChild) o(create.args[1].aborted).equals(false) - o(create.args[2]).equals(true) }) o("works inside keyed", function() { var create = o.spy() var vnode = m("div", m.layout(create)) var otherVnode = m("a") - m.render(root, [m.key(1, vnode), m.key(2, otherVnode)]) + m.render(G.root, [m.key(1, vnode), m.key(2, otherVnode)]) o(create.callCount).equals(1) - o(create.args[0]).equals(root.firstChild) + o(create.args[0]).equals(G.root.firstChild) o(create.args[1].aborted).equals(false) - o(create.args[2]).equals(true) }) o("does not invoke callback when removing, but aborts the provided signal", function() { var create = o.spy() var vnode = m("div", m.layout(create)) - m.render(root, vnode) + m.render(G.root, vnode) o(create.callCount).equals(1) o(create.args[1].aborted).equals(false) - m.render(root, []) + m.render(G.root, []) o(create.callCount).equals(1) o(create.args[1].aborted).equals(true) @@ -99,57 +89,51 @@ o.spec("layout create", function() { var update = o.spy() var callback = o.spy() var vnode = m("div", m.layout(create)) - var updated = m("div", m.layout(update), m("a", m.layout(callback))) + var updated = m("div", m.layout(null, update), m("a", m.layout(callback))) - m.render(root, vnode) - m.render(root, updated) + m.render(G.root, vnode) + m.render(G.root, updated) o(create.callCount).equals(1) - o(create.args[0]).equals(root.firstChild) + o(create.args[0]).equals(G.root.firstChild) o(create.args[1].aborted).equals(false) - o(create.args[2]).equals(true) o(update.callCount).equals(1) - o(update.args[0]).equals(root.firstChild) + o(update.args[0]).equals(G.root.firstChild) o(update.args[1].aborted).equals(false) - o(update.args[2]).equals(false) o(callback.callCount).equals(1) - o(callback.args[0]).equals(root.firstChild.firstChild) + o(callback.args[0]).equals(G.root.firstChild.firstChild) o(callback.args[1].aborted).equals(false) - o(callback.args[2]).equals(true) }) o("works on unkeyed that falls into reverse list diff code path", function() { var create = o.spy() - m.render(root, [m.key(1, m("p")), m.key(2, m("div"))]) - m.render(root, [m.key(2, m("div", m.layout(create))), m.key(1, m("p"))]) + m.render(G.root, [m.key(1, m("p")), m.key(2, m("div"))]) + m.render(G.root, [m.key(2, m("div", m.layout(create))), m.key(1, m("p"))]) o(create.callCount).equals(1) - o(create.args[0]).equals(root.firstChild) + o(create.args[0]).equals(G.root.firstChild) o(create.args[1].aborted).equals(false) - o(create.args[2]).equals(true) }) o("works on unkeyed that falls into forward list diff code path", function() { var create = o.spy() - m.render(root, [m("div"), m("p")]) - m.render(root, [m("div"), m("div", m.layout(create))]) + m.render(G.root, [m("div"), m("p")]) + m.render(G.root, [m("div"), m("div", m.layout(create))]) o(create.callCount).equals(1) - o(create.args[0]).equals(root.childNodes[1]) + o(create.args[0]).equals(G.root.childNodes[1]) o(create.args[1].aborted).equals(false) - o(create.args[2]).equals(true) }) o("works after full DOM creation", function() { var created = false var vnode = m("div", m("a", m.layout(create), m("b"))) - m.render(root, vnode) + m.render(G.root, vnode) - function create(dom, _, isInit) { - if (!isInit) return + function create(dom) { created = true - o(dom.parentNode).equals(root.firstChild) + o(dom.parentNode).equals(G.root.firstChild) o(dom.childNodes.length).equals(1) } o(created).equals(true) diff --git a/tests/render/onremove.js b/tests/render/onremove.js index 914849662..36b6f3d7d 100644 --- a/tests/render/onremove.js +++ b/tests/render/onremove.js @@ -1,14 +1,11 @@ import o from "ospec" -import domMock from "../../test-utils/domMock.js" +import {setupGlobals} from "../../test-utils/global.js" + import m from "../../src/entry/mithril.esm.js" o.spec("layout remove", function() { - var $window, root - o.beforeEach(function() { - $window = domMock() - root = $window.document.createElement("div") - }) + var G = setupGlobals() var layoutRemove = (onabort) => m.layout((_, signal) => { signal.onabort = onabort }) @@ -18,8 +15,8 @@ o.spec("layout remove", function() { var vnode = m("div", layoutRemove(create)) var updated = m("div", layoutRemove(update)) - m.render(root, vnode) - m.render(root, updated) + m.render(G.root, vnode) + m.render(G.root, updated) o(create.callCount).equals(0) }) @@ -29,8 +26,8 @@ o.spec("layout remove", function() { var vnode = m("div", layoutRemove(create)) var updated = m("div", layoutRemove(update)) - m.render(root, vnode) - m.render(root, updated) + m.render(G.root, vnode) + m.render(G.root, updated) o(create.callCount).equals(0) o(update.callCount).equals(0) @@ -39,8 +36,8 @@ o.spec("layout remove", function() { var remove = o.spy() var vnode = m("div", layoutRemove(remove)) - m.render(root, vnode) - m.render(root, []) + m.render(G.root, vnode) + m.render(G.root, []) o(remove.callCount).equals(1) }) @@ -48,8 +45,8 @@ o.spec("layout remove", function() { var remove = o.spy() var vnode = [layoutRemove(remove)] - m.render(root, vnode) - m.render(root, []) + m.render(G.root, vnode) + m.render(G.root, []) o(remove.callCount).equals(1) }) @@ -59,11 +56,11 @@ o.spec("layout remove", function() { var temp = m("div", layoutRemove(remove)) var updated = m("div") - m.render(root, m.key(1, vnode)) - m.render(root, m.key(2, temp)) - m.render(root, m.key(1, updated)) + m.render(G.root, m.key(1, vnode)) + m.render(G.root, m.key(2, temp)) + m.render(G.root, m.key(1, updated)) - o(vnode.dom).notEquals(updated.dom) // this used to be a recycling pool test + o(vnode.d).notEquals(updated.d) // this used to be a recycling pool test o(remove.callCount).equals(1) }) o("aborts layout signal on nested component", function() { @@ -71,8 +68,8 @@ o.spec("layout remove", function() { var comp = () => m(outer) var outer = () => m(inner) var inner = () => m.layout(spy) - m.render(root, m(comp)) - m.render(root, null) + m.render(G.root, m(comp)) + m.render(G.root, null) o(spy.callCount).equals(1) }) @@ -81,8 +78,8 @@ o.spec("layout remove", function() { var comp = () => m(outer) var outer = () => m(inner, m("a", layoutRemove(spy))) var inner = (attrs) => m("div", attrs.children) - m.render(root, m(comp)) - m.render(root, null) + m.render(G.root, m(comp)) + m.render(G.root, null) o(spy.callCount).equals(1) }) diff --git a/tests/render/onupdate.js b/tests/render/onupdate.js index 2ebec40dc..145731317 100644 --- a/tests/render/onupdate.js +++ b/tests/render/onupdate.js @@ -1,110 +1,104 @@ import o from "ospec" -import domMock from "../../test-utils/domMock.js" +import {setupGlobals} from "../../test-utils/global.js" + import m from "../../src/entry/mithril.esm.js" o.spec("layout update", function() { - var $window, root - o.beforeEach(function() { - $window = domMock() - root = $window.document.createElement("div") - }) + var G = setupGlobals() o("is not invoked when removing element", function() { - var layout = o.spy() - var vnode = m("div", m.layout(layout)) + var update = o.spy() + var vnode = m("div", m.layout(null, update)) - m.render(root, vnode) - m.render(root, []) + m.render(G.root, vnode) + m.render(G.root, []) - o(layout.calls.map((c) => c.args[2])).deepEquals([true]) + o(update.callCount).equals(0) }) o("is not updated when replacing keyed element", function() { - var layout = o.spy() var update = o.spy() - var vnode = m.key(1, m("div", m.layout(layout))) - var updated = m.key(1, m("a", m.layout(update))) - m.render(root, vnode) - m.render(root, updated) + var vnode = m.key(1, m("div", m.layout(null, update))) + var updated = m.key(1, m("a", m.layout(null, update))) + m.render(G.root, vnode) + m.render(G.root, updated) - o(layout.calls.map((c) => c.args[2])).deepEquals([true]) - o(update.calls.map((c) => c.args[2])).deepEquals([true]) + o(update.callCount).equals(0) }) o("does not call old callback when removing layout vnode from new vnode", function() { - var layout = o.spy() + var update = o.spy() - m.render(root, m("a", m.layout(layout))) - m.render(root, m("a", m.layout(layout))) - m.render(root, m("a")) + m.render(G.root, m("a", m.layout(null, update))) + m.render(G.root, m("a", m.layout(null, update))) + m.render(G.root, m("a")) - o(layout.calls.map((c) => c.args[2])).deepEquals([true, false]) + o(update.callCount).equals(1) }) o("invoked on noop", function() { - var layout = o.spy() + var preUpdate = o.spy() var update = o.spy() - var vnode = m("div", m.layout(layout)) - var updated = m("div", m.layout(update)) + var vnode = m("div", m.layout(null, preUpdate)) + var updated = m("div", m.layout(null, update)) - m.render(root, vnode) - m.render(root, updated) + m.render(G.root, vnode) + m.render(G.root, updated) - o(layout.calls.map((c) => c.args[2])).deepEquals([true]) - o(update.calls.map((c) => c.args[2])).deepEquals([false]) + o(preUpdate.callCount).equals(0) + o(update.callCount).equals(1) }) o("invoked on updating attr", function() { - var layout = o.spy() + var preUpdate = o.spy() var update = o.spy() - var vnode = m("div", m.layout(layout)) - var updated = m("div", {id: "a"}, m.layout(update)) + var vnode = m("div", m.layout(null, preUpdate)) + var updated = m("div", {id: "a"}, m.layout(null, update)) - m.render(root, vnode) - m.render(root, updated) + m.render(G.root, vnode) + m.render(G.root, updated) - o(layout.calls.map((c) => c.args[2])).deepEquals([true]) - o(update.calls.map((c) => c.args[2])).deepEquals([false]) + o(preUpdate.callCount).equals(0) + o(update.callCount).equals(1) }) o("invoked on updating children", function() { - var layout = o.spy() + var preUpdate = o.spy() var update = o.spy() - var vnode = m("div", m.layout(layout), m("a")) - var updated = m("div", m.layout(update), m("b")) + var vnode = m("div", m.layout(null, preUpdate), m("a")) + var updated = m("div", m.layout(null, update), m("b")) - m.render(root, vnode) - m.render(root, updated) + m.render(G.root, vnode) + m.render(G.root, updated) - o(layout.calls.map((c) => c.args[2])).deepEquals([true]) - o(update.calls.map((c) => c.args[2])).deepEquals([false]) + o(preUpdate.callCount).equals(0) + o(update.callCount).equals(1) }) o("invoked on updating fragment", function() { - var layout = o.spy() + var preUpdate = o.spy() var update = o.spy() - var vnode = [m.layout(layout)] - var updated = [m.layout(update)] + var vnode = [m.layout(null, preUpdate)] + var updated = [m.layout(null, update)] - m.render(root, vnode) - m.render(root, updated) + m.render(G.root, vnode) + m.render(G.root, updated) - o(layout.calls.map((c) => c.args[2])).deepEquals([true]) - o(update.calls.map((c) => c.args[2])).deepEquals([false]) + o(preUpdate.callCount).equals(0) + o(update.callCount).equals(1) }) o("invoked on full DOM update", function() { var called = false var vnode = m("div", {id: "1"}, - m("a", {id: "2"}, m.layout(() => {}), + m("a", {id: "2"}, m.layout(null, null), m("b", {id: "3"}) ) ) var updated = m("div", {id: "11"}, - m("a", {id: "22"}, m.layout(update), + m("a", {id: "22"}, m.layout(null, update), m("b", {id: "33"}) ) ) - m.render(root, vnode) - m.render(root, updated) + m.render(G.root, vnode) + m.render(G.root, updated) - function update(dom, _, isInit) { - if (isInit) return + function update(dom) { called = true o(dom.parentNode.attributes["id"].value).equals("11") diff --git a/tests/render/render-hyperscript-integration.js b/tests/render/render-hyperscript-integration.js index 6c30e08c4..a4922bb4c 100644 --- a/tests/render/render-hyperscript-integration.js +++ b/tests/render/render-hyperscript-integration.js @@ -1,610 +1,608 @@ import o from "ospec" -import domMock from "../../test-utils/domMock.js" +import {setupGlobals} from "../../test-utils/global.js" + import m from "../../src/entry/mithril.esm.js" o.spec("render/hyperscript integration", function() { - var $window, root - o.beforeEach(function() { - $window = domMock() - root = $window.document.createElement("div") - }) + var G = setupGlobals() + o.spec("setting class", function() { o("selector only", function() { - m.render(root, m(".foo")) + m.render(G.root, m(".foo")) - o(root.firstChild.className).equals("foo") + o(G.root.firstChild.className).equals("foo") }) o("class only", function() { - m.render(root, m("div", {class: "foo"})) + m.render(G.root, m("div", {class: "foo"})) - o(root.firstChild.className).equals("foo") + o(G.root.firstChild.className).equals("foo") }) o("className only", function() { - m.render(root, m("div", {className: "foo"})) + m.render(G.root, m("div", {className: "foo"})) - o(root.firstChild.className).equals("foo") + o(G.root.firstChild.className).equals("foo") }) o("selector and class", function() { - m.render(root, m(".bar", {class: "foo"})) + m.render(G.root, m(".bar", {class: "foo"})) - o(root.firstChild.className.split(" ").sort()).deepEquals(["bar", "foo"]) + o(G.root.firstChild.className.split(" ").sort()).deepEquals(["bar", "foo"]) }) o("selector and className", function() { - m.render(root, m(".bar", {className: "foo"})) + m.render(G.root, m(".bar", {className: "foo"})) - o(root.firstChild.className.split(" ").sort()).deepEquals(["bar", "foo"]) + o(G.root.firstChild.className.split(" ").sort()).deepEquals(["bar", "foo"]) }) o("selector and a null class", function() { - m.render(root, m(".foo", {class: null})) + m.render(G.root, m(".foo", {class: null})) - o(root.firstChild.className).equals("foo") + o(G.root.firstChild.className).equals("foo") }) o("selector and a null className", function() { - m.render(root, m(".foo", {className: null})) + m.render(G.root, m(".foo", {className: null})) - o(root.firstChild.className).equals("foo") + o(G.root.firstChild.className).equals("foo") }) o("selector and an undefined class", function() { - m.render(root, m(".foo", {class: undefined})) + m.render(G.root, m(".foo", {class: undefined})) - o(root.firstChild.className).equals("foo") + o(G.root.firstChild.className).equals("foo") }) o("selector and an undefined className", function() { - m.render(root, m(".foo", {className: undefined})) + m.render(G.root, m(".foo", {className: undefined})) - o(root.firstChild.className).equals("foo") + o(G.root.firstChild.className).equals("foo") }) }) o.spec("updating class", function() { o.spec("from selector only", function() { o("to selector only", function() { - m.render(root, m(".foo1")) - m.render(root, m(".foo2")) + m.render(G.root, m(".foo1")) + m.render(G.root, m(".foo2")) - o(root.firstChild.className).equals("foo2") + o(G.root.firstChild.className).equals("foo2") }) o("to class only", function() { - m.render(root, m(".foo1")) - m.render(root, m("div", {class: "foo2"})) + m.render(G.root, m(".foo1")) + m.render(G.root, m("div", {class: "foo2"})) - o(root.firstChild.className).equals("foo2") + o(G.root.firstChild.className).equals("foo2") }) o("to className only", function() { - m.render(root, m(".foo1")) - m.render(root, m("div", {className: "foo2"})) + m.render(G.root, m(".foo1")) + m.render(G.root, m("div", {className: "foo2"})) - o(root.firstChild.className).equals("foo2") + o(G.root.firstChild.className).equals("foo2") }) o("to selector and class", function() { - m.render(root, m(".foo1")) - m.render(root, m(".bar2", {class: "foo2"})) + m.render(G.root, m(".foo1")) + m.render(G.root, m(".bar2", {class: "foo2"})) - o(root.firstChild.className.split(" ").sort()).deepEquals(["bar2", "foo2"]) + o(G.root.firstChild.className.split(" ").sort()).deepEquals(["bar2", "foo2"]) }) o("to selector and className", function() { - m.render(root, m(".foo1")) - m.render(root, m(".bar2", {className: "foo2"})) + m.render(G.root, m(".foo1")) + m.render(G.root, m(".bar2", {className: "foo2"})) - o(root.firstChild.className.split(" ").sort()).deepEquals(["bar2", "foo2"]) + o(G.root.firstChild.className.split(" ").sort()).deepEquals(["bar2", "foo2"]) }) o("to selector and a null class", function() { - m.render(root, m(".foo1")) - m.render(root, m(".foo2", {class: null})) + m.render(G.root, m(".foo1")) + m.render(G.root, m(".foo2", {class: null})) - o(root.firstChild.className).equals("foo2") + o(G.root.firstChild.className).equals("foo2") }) o("to selector and a null className", function() { - m.render(root, m(".foo1")) - m.render(root, m(".foo2", {className: null})) + m.render(G.root, m(".foo1")) + m.render(G.root, m(".foo2", {className: null})) - o(root.firstChild.className).equals("foo2") + o(G.root.firstChild.className).equals("foo2") }) o("to selector and an undefined class", function() { - m.render(root, m(".foo1")) - m.render(root, m(".foo2", {class: undefined})) + m.render(G.root, m(".foo1")) + m.render(G.root, m(".foo2", {class: undefined})) - o(root.firstChild.className).equals("foo2") + o(G.root.firstChild.className).equals("foo2") }) o("to selector and an undefined className", function() { - m.render(root, m(".foo1")) - m.render(root, m(".foo2", {className: undefined})) + m.render(G.root, m(".foo1")) + m.render(G.root, m(".foo2", {className: undefined})) - o(root.firstChild.className).equals("foo2") + o(G.root.firstChild.className).equals("foo2") }) }) o.spec("from class only", function() { o("to selector only", function() { - m.render(root, m("div", {class: "foo2"})) - m.render(root, m(".foo2")) + m.render(G.root, m("div", {class: "foo2"})) + m.render(G.root, m(".foo2")) - o(root.firstChild.className).equals("foo2") + o(G.root.firstChild.className).equals("foo2") }) o("to class only", function() { - m.render(root, m("div", {class: "foo2"})) - m.render(root, m("div", {class: "foo2"})) + m.render(G.root, m("div", {class: "foo2"})) + m.render(G.root, m("div", {class: "foo2"})) - o(root.firstChild.className).equals("foo2") + o(G.root.firstChild.className).equals("foo2") }) o("to className only", function() { - m.render(root, m("div", {class: "foo2"})) - m.render(root, m("div", {className: "foo2"})) + m.render(G.root, m("div", {class: "foo2"})) + m.render(G.root, m("div", {className: "foo2"})) - o(root.firstChild.className).equals("foo2") + o(G.root.firstChild.className).equals("foo2") }) o("to selector and class", function() { - m.render(root, m("div", {class: "foo2"})) - m.render(root, m(".bar2", {class: "foo2"})) + m.render(G.root, m("div", {class: "foo2"})) + m.render(G.root, m(".bar2", {class: "foo2"})) - o(root.firstChild.className.split(" ").sort()).deepEquals(["bar2", "foo2"]) + o(G.root.firstChild.className.split(" ").sort()).deepEquals(["bar2", "foo2"]) }) o("to selector and className", function() { - m.render(root, m(".bar2", {className: "foo2"})) + m.render(G.root, m(".bar2", {className: "foo2"})) - o(root.firstChild.className.split(" ").sort()).deepEquals(["bar2", "foo2"]) + o(G.root.firstChild.className.split(" ").sort()).deepEquals(["bar2", "foo2"]) }) o("to selector and a null class", function() { - m.render(root, m("div", {class: "foo2"})) - m.render(root, m(".foo2", {class: null})) + m.render(G.root, m("div", {class: "foo2"})) + m.render(G.root, m(".foo2", {class: null})) - o(root.firstChild.className).equals("foo2") + o(G.root.firstChild.className).equals("foo2") }) o("to selector and a null className", function() { - m.render(root, m("div", {class: "foo2"})) - m.render(root, m(".foo2", {className: null})) + m.render(G.root, m("div", {class: "foo2"})) + m.render(G.root, m(".foo2", {className: null})) - o(root.firstChild.className).equals("foo2") + o(G.root.firstChild.className).equals("foo2") }) o("to selector and an undefined class", function() { - m.render(root, m("div", {class: "foo2"})) - m.render(root, m(".foo2", {class: undefined})) + m.render(G.root, m("div", {class: "foo2"})) + m.render(G.root, m(".foo2", {class: undefined})) - o(root.firstChild.className).equals("foo2") + o(G.root.firstChild.className).equals("foo2") }) o("to selector and an undefined className", function() { - m.render(root, m("div", {class: "foo2"})) - m.render(root, m(".foo2", {className: undefined})) + m.render(G.root, m("div", {class: "foo2"})) + m.render(G.root, m(".foo2", {className: undefined})) - o(root.firstChild.className).equals("foo2") + o(G.root.firstChild.className).equals("foo2") }) }) o.spec("from ", function() { o("to selector only", function() { - m.render(root, m(".foo2")) + m.render(G.root, m(".foo2")) - o(root.firstChild.className).equals("foo2") + o(G.root.firstChild.className).equals("foo2") }) o("to class only", function() { - m.render(root, m("div", {class: "foo2"})) + m.render(G.root, m("div", {class: "foo2"})) - o(root.firstChild.className).equals("foo2") + o(G.root.firstChild.className).equals("foo2") }) o("to className only", function() { - m.render(root, m("div", {className: "foo2"})) + m.render(G.root, m("div", {className: "foo2"})) - o(root.firstChild.className).equals("foo2") + o(G.root.firstChild.className).equals("foo2") }) o("to selector and class", function() { - m.render(root, m(".bar2", {class: "foo2"})) + m.render(G.root, m(".bar2", {class: "foo2"})) - o(root.firstChild.className.split(" ").sort()).deepEquals(["bar2", "foo2"]) + o(G.root.firstChild.className.split(" ").sort()).deepEquals(["bar2", "foo2"]) }) o("to selector and className", function() { - m.render(root, m(".bar2", {className: "foo2"})) + m.render(G.root, m(".bar2", {className: "foo2"})) - o(root.firstChild.className.split(" ").sort()).deepEquals(["bar2", "foo2"]) + o(G.root.firstChild.className.split(" ").sort()).deepEquals(["bar2", "foo2"]) }) o("to selector and a null class", function() { - m.render(root, m(".foo2", {class: null})) + m.render(G.root, m(".foo2", {class: null})) - o(root.firstChild.className).equals("foo2") + o(G.root.firstChild.className).equals("foo2") }) o("to selector and a null className", function() { - m.render(root, m(".foo2", {className: null})) + m.render(G.root, m(".foo2", {className: null})) - o(root.firstChild.className).equals("foo2") + o(G.root.firstChild.className).equals("foo2") }) o("to selector and an undefined class", function() { - m.render(root, m(".foo2", {class: undefined})) + m.render(G.root, m(".foo2", {class: undefined})) - o(root.firstChild.className).equals("foo2") + o(G.root.firstChild.className).equals("foo2") }) o("to selector and an undefined className", function() { - m.render(root, m(".foo2", {className: undefined})) + m.render(G.root, m(".foo2", {className: undefined})) - o(root.firstChild.className).equals("foo2") + o(G.root.firstChild.className).equals("foo2") }) }) o.spec("from className only", function() { o("to selector only", function() { - m.render(root, m("div", {className: "foo1"})) - m.render(root, m(".foo2")) + m.render(G.root, m("div", {className: "foo1"})) + m.render(G.root, m(".foo2")) - o(root.firstChild.className).equals("foo2") + o(G.root.firstChild.className).equals("foo2") }) o("to class only", function() { - m.render(root, m("div", {className: "foo1"})) - m.render(root, m("div", {class: "foo2"})) + m.render(G.root, m("div", {className: "foo1"})) + m.render(G.root, m("div", {class: "foo2"})) - o(root.firstChild.className).equals("foo2") + o(G.root.firstChild.className).equals("foo2") }) o("to className only", function() { - m.render(root, m("div", {className: "foo1"})) - m.render(root, m("div", {className: "foo2"})) + m.render(G.root, m("div", {className: "foo1"})) + m.render(G.root, m("div", {className: "foo2"})) - o(root.firstChild.className).equals("foo2") + o(G.root.firstChild.className).equals("foo2") }) o("to selector and class", function() { - m.render(root, m("div", {className: "foo1"})) - m.render(root, m(".bar2", {class: "foo2"})) + m.render(G.root, m("div", {className: "foo1"})) + m.render(G.root, m(".bar2", {class: "foo2"})) - o(root.firstChild.className.split(" ").sort()).deepEquals(["bar2", "foo2"]) + o(G.root.firstChild.className.split(" ").sort()).deepEquals(["bar2", "foo2"]) }) o("to selector and className", function() { - m.render(root, m("div", {className: "foo1"})) - m.render(root, m(".bar2", {className: "foo2"})) + m.render(G.root, m("div", {className: "foo1"})) + m.render(G.root, m(".bar2", {className: "foo2"})) - o(root.firstChild.className.split(" ").sort()).deepEquals(["bar2", "foo2"]) + o(G.root.firstChild.className.split(" ").sort()).deepEquals(["bar2", "foo2"]) }) o("to selector and a null class", function() { - m.render(root, m("div", {className: "foo1"})) - m.render(root, m(".foo2", {class: null})) + m.render(G.root, m("div", {className: "foo1"})) + m.render(G.root, m(".foo2", {class: null})) - o(root.firstChild.className).equals("foo2") + o(G.root.firstChild.className).equals("foo2") }) o("to selector and a null className", function() { - m.render(root, m("div", {className: "foo1"})) - m.render(root, m(".foo2", {className: null})) + m.render(G.root, m("div", {className: "foo1"})) + m.render(G.root, m(".foo2", {className: null})) - o(root.firstChild.className).equals("foo2") + o(G.root.firstChild.className).equals("foo2") }) o("to selector and an undefined class", function() { - m.render(root, m("div", {className: "foo1"})) - m.render(root, m(".foo2", {class: undefined})) + m.render(G.root, m("div", {className: "foo1"})) + m.render(G.root, m(".foo2", {class: undefined})) - o(root.firstChild.className).equals("foo2") + o(G.root.firstChild.className).equals("foo2") }) o("to selector and an undefined className", function() { - m.render(root, m("div", {className: "foo1"})) - m.render(root, m(".foo2", {className: undefined})) + m.render(G.root, m("div", {className: "foo1"})) + m.render(G.root, m(".foo2", {className: undefined})) - o(root.firstChild.className).equals("foo2") + o(G.root.firstChild.className).equals("foo2") }) }) o.spec("from selector and class", function() { o("to selector only", function() { - m.render(root, m(".bar1", {class: "foo1"})) - m.render(root, m(".foo2")) + m.render(G.root, m(".bar1", {class: "foo1"})) + m.render(G.root, m(".foo2")) - o(root.firstChild.className).equals("foo2") + o(G.root.firstChild.className).equals("foo2") }) o("to class only", function() { - m.render(root, m(".bar1", {class: "foo1"})) - m.render(root, m("div", {class: "foo2"})) + m.render(G.root, m(".bar1", {class: "foo1"})) + m.render(G.root, m("div", {class: "foo2"})) - o(root.firstChild.className).equals("foo2") + o(G.root.firstChild.className).equals("foo2") }) o("to className only", function() { - m.render(root, m(".bar1", {class: "foo1"})) - m.render(root, m("div", {className: "foo2"})) + m.render(G.root, m(".bar1", {class: "foo1"})) + m.render(G.root, m("div", {className: "foo2"})) - o(root.firstChild.className).equals("foo2") + o(G.root.firstChild.className).equals("foo2") }) o("to selector and class", function() { - m.render(root, m(".bar1", {class: "foo1"})) - m.render(root, m(".bar2", {class: "foo2"})) + m.render(G.root, m(".bar1", {class: "foo1"})) + m.render(G.root, m(".bar2", {class: "foo2"})) - o(root.firstChild.className.split(" ").sort()).deepEquals(["bar2", "foo2"]) + o(G.root.firstChild.className.split(" ").sort()).deepEquals(["bar2", "foo2"]) }) o("to selector and className", function() { - m.render(root, m(".bar1", {class: "foo1"})) - m.render(root, m(".bar2", {className: "foo2"})) + m.render(G.root, m(".bar1", {class: "foo1"})) + m.render(G.root, m(".bar2", {className: "foo2"})) - o(root.firstChild.className.split(" ").sort()).deepEquals(["bar2", "foo2"]) + o(G.root.firstChild.className.split(" ").sort()).deepEquals(["bar2", "foo2"]) }) o("to selector and a null class", function() { - m.render(root, m(".bar1", {class: "foo1"})) - m.render(root, m(".foo2", {class: null})) + m.render(G.root, m(".bar1", {class: "foo1"})) + m.render(G.root, m(".foo2", {class: null})) - o(root.firstChild.className).equals("foo2") + o(G.root.firstChild.className).equals("foo2") }) o("to selector and a null className", function() { - m.render(root, m(".bar1", {class: "foo1"})) - m.render(root, m(".foo2", {className: null})) + m.render(G.root, m(".bar1", {class: "foo1"})) + m.render(G.root, m(".foo2", {className: null})) - o(root.firstChild.className).equals("foo2") + o(G.root.firstChild.className).equals("foo2") }) o("to selector and an undefined class", function() { - m.render(root, m(".bar1", {class: "foo1"})) - m.render(root, m(".foo2", {class: undefined})) + m.render(G.root, m(".bar1", {class: "foo1"})) + m.render(G.root, m(".foo2", {class: undefined})) - o(root.firstChild.className).equals("foo2") + o(G.root.firstChild.className).equals("foo2") }) o("to selector and an undefined className", function() { - m.render(root, m(".bar1", {class: "foo1"})) - m.render(root, m(".foo2", {className: undefined})) + m.render(G.root, m(".bar1", {class: "foo1"})) + m.render(G.root, m(".foo2", {className: undefined})) - o(root.firstChild.className).equals("foo2") + o(G.root.firstChild.className).equals("foo2") }) }) o.spec("from selector and className", function() { o("to selector only", function() { - m.render(root, m(".bar1", {className: "foo1"})) - m.render(root, m(".foo2")) + m.render(G.root, m(".bar1", {className: "foo1"})) + m.render(G.root, m(".foo2")) - o(root.firstChild.className).equals("foo2") + o(G.root.firstChild.className).equals("foo2") }) o("to class only", function() { - m.render(root, m(".bar1", {className: "foo1"})) - m.render(root, m("div", {class: "foo2"})) + m.render(G.root, m(".bar1", {className: "foo1"})) + m.render(G.root, m("div", {class: "foo2"})) - o(root.firstChild.className).equals("foo2") + o(G.root.firstChild.className).equals("foo2") }) o("to className only", function() { - m.render(root, m(".bar1", {className: "foo1"})) - m.render(root, m("div", {className: "foo2"})) + m.render(G.root, m(".bar1", {className: "foo1"})) + m.render(G.root, m("div", {className: "foo2"})) - o(root.firstChild.className).equals("foo2") + o(G.root.firstChild.className).equals("foo2") }) o("to selector and class", function() { - m.render(root, m(".bar1", {className: "foo1"})) - m.render(root, m(".bar2", {class: "foo2"})) + m.render(G.root, m(".bar1", {className: "foo1"})) + m.render(G.root, m(".bar2", {class: "foo2"})) - o(root.firstChild.className.split(" ").sort()).deepEquals(["bar2", "foo2"]) + o(G.root.firstChild.className.split(" ").sort()).deepEquals(["bar2", "foo2"]) }) o("to selector and className", function() { - m.render(root, m(".bar1", {className: "foo1"})) - m.render(root, m(".bar2", {className: "foo2"})) + m.render(G.root, m(".bar1", {className: "foo1"})) + m.render(G.root, m(".bar2", {className: "foo2"})) - o(root.firstChild.className.split(" ").sort()).deepEquals(["bar2", "foo2"]) + o(G.root.firstChild.className.split(" ").sort()).deepEquals(["bar2", "foo2"]) }) o("to selector and a null class", function() { - m.render(root, m(".bar1", {className: "foo1"})) - m.render(root, m(".foo2", {class: null})) + m.render(G.root, m(".bar1", {className: "foo1"})) + m.render(G.root, m(".foo2", {class: null})) - o(root.firstChild.className).equals("foo2") + o(G.root.firstChild.className).equals("foo2") }) o("to selector and a null className", function() { - m.render(root, m(".bar1", {className: "foo1"})) - m.render(root, m(".foo2", {className: null})) + m.render(G.root, m(".bar1", {className: "foo1"})) + m.render(G.root, m(".foo2", {className: null})) - o(root.firstChild.className).equals("foo2") + o(G.root.firstChild.className).equals("foo2") }) o("to selector and an undefined class", function() { - m.render(root, m(".bar1", {className: "foo1"})) - m.render(root, m(".foo2", {class: undefined})) + m.render(G.root, m(".bar1", {className: "foo1"})) + m.render(G.root, m(".foo2", {class: undefined})) - o(root.firstChild.className).equals("foo2") + o(G.root.firstChild.className).equals("foo2") }) o("to selector and an undefined className", function() { - m.render(root, m(".bar1", {className: "foo1"})) - m.render(root, m(".foo2", {className: undefined})) + m.render(G.root, m(".bar1", {className: "foo1"})) + m.render(G.root, m(".foo2", {className: undefined})) - o(root.firstChild.className).equals("foo2") + o(G.root.firstChild.className).equals("foo2") }) }) o.spec("from and a null class", function() { o("to selector only", function() { - m.render(root, m(".foo1", {class: null})) - m.render(root, m(".foo2")) + m.render(G.root, m(".foo1", {class: null})) + m.render(G.root, m(".foo2")) - o(root.firstChild.className).equals("foo2") + o(G.root.firstChild.className).equals("foo2") }) o("to class only", function() { - m.render(root, m(".foo1", {class: null})) - m.render(root, m("div", {class: "foo2"})) + m.render(G.root, m(".foo1", {class: null})) + m.render(G.root, m("div", {class: "foo2"})) - o(root.firstChild.className).equals("foo2") + o(G.root.firstChild.className).equals("foo2") }) o("to className only", function() { - m.render(root, m(".foo1", {class: null})) - m.render(root, m("div", {className: "foo2"})) + m.render(G.root, m(".foo1", {class: null})) + m.render(G.root, m("div", {className: "foo2"})) - o(root.firstChild.className).equals("foo2") + o(G.root.firstChild.className).equals("foo2") }) o("to selector and class", function() { - m.render(root, m(".foo1", {class: null})) - m.render(root, m(".bar2", {class: "foo2"})) + m.render(G.root, m(".foo1", {class: null})) + m.render(G.root, m(".bar2", {class: "foo2"})) - o(root.firstChild.className.split(" ").sort()).deepEquals(["bar2", "foo2"]) + o(G.root.firstChild.className.split(" ").sort()).deepEquals(["bar2", "foo2"]) }) o("to selector and className", function() { - m.render(root, m(".foo1", {class: null})) - m.render(root, m(".bar2", {className: "foo2"})) + m.render(G.root, m(".foo1", {class: null})) + m.render(G.root, m(".bar2", {className: "foo2"})) - o(root.firstChild.className.split(" ").sort()).deepEquals(["bar2", "foo2"]) + o(G.root.firstChild.className.split(" ").sort()).deepEquals(["bar2", "foo2"]) }) o("to selector and a null class", function() { - m.render(root, m(".foo1", {class: null})) - m.render(root, m(".foo2", {class: null})) + m.render(G.root, m(".foo1", {class: null})) + m.render(G.root, m(".foo2", {class: null})) - o(root.firstChild.className).equals("foo2") + o(G.root.firstChild.className).equals("foo2") }) o("to selector and a null className", function() { - m.render(root, m(".foo1", {class: null})) - m.render(root, m(".foo2", {className: null})) + m.render(G.root, m(".foo1", {class: null})) + m.render(G.root, m(".foo2", {className: null})) - o(root.firstChild.className).equals("foo2") + o(G.root.firstChild.className).equals("foo2") }) o("to selector and an undefined class", function() { - m.render(root, m(".foo1", {class: null})) - m.render(root, m(".foo2", {class: undefined})) + m.render(G.root, m(".foo1", {class: null})) + m.render(G.root, m(".foo2", {class: undefined})) - o(root.firstChild.className).equals("foo2") + o(G.root.firstChild.className).equals("foo2") }) o("to selector and an undefined className", function() { - m.render(root, m(".foo1", {class: null})) - m.render(root, m(".foo2", {className: undefined})) + m.render(G.root, m(".foo1", {class: null})) + m.render(G.root, m(".foo2", {className: undefined})) - o(root.firstChild.className).equals("foo2") + o(G.root.firstChild.className).equals("foo2") }) }) o.spec("from selector and a null className", function() { o("to selector only", function() { - m.render(root, m(".foo1", {className: null})) - m.render(root, m(".foo2")) + m.render(G.root, m(".foo1", {className: null})) + m.render(G.root, m(".foo2")) - o(root.firstChild.className).equals("foo2") + o(G.root.firstChild.className).equals("foo2") }) o("to class only", function() { - m.render(root, m(".foo1", {className: null})) - m.render(root, m("div", {class: "foo2"})) + m.render(G.root, m(".foo1", {className: null})) + m.render(G.root, m("div", {class: "foo2"})) - o(root.firstChild.className).equals("foo2") + o(G.root.firstChild.className).equals("foo2") }) o("to className only", function() { - m.render(root, m(".foo1", {className: null})) - m.render(root, m("div", {className: "foo2"})) + m.render(G.root, m(".foo1", {className: null})) + m.render(G.root, m("div", {className: "foo2"})) - o(root.firstChild.className).equals("foo2") + o(G.root.firstChild.className).equals("foo2") }) o("to selector and class", function() { - m.render(root, m(".foo1", {className: null})) - m.render(root, m(".bar2", {class: "foo2"})) + m.render(G.root, m(".foo1", {className: null})) + m.render(G.root, m(".bar2", {class: "foo2"})) - o(root.firstChild.className.split(" ").sort()).deepEquals(["bar2", "foo2"]) + o(G.root.firstChild.className.split(" ").sort()).deepEquals(["bar2", "foo2"]) }) o("to selector and className", function() { - m.render(root, m(".foo1", {className: null})) - m.render(root, m(".bar2", {className: "foo2"})) + m.render(G.root, m(".foo1", {className: null})) + m.render(G.root, m(".bar2", {className: "foo2"})) - o(root.firstChild.className.split(" ").sort()).deepEquals(["bar2", "foo2"]) + o(G.root.firstChild.className.split(" ").sort()).deepEquals(["bar2", "foo2"]) }) o("to selector and a null class", function() { - m.render(root, m(".foo1", {className: null})) - m.render(root, m(".foo2", {class: null})) + m.render(G.root, m(".foo1", {className: null})) + m.render(G.root, m(".foo2", {class: null})) - o(root.firstChild.className).equals("foo2") + o(G.root.firstChild.className).equals("foo2") }) o("to selector and a null className", function() { - m.render(root, m(".foo1", {className: null})) - m.render(root, m(".foo2", {className: null})) + m.render(G.root, m(".foo1", {className: null})) + m.render(G.root, m(".foo2", {className: null})) - o(root.firstChild.className).equals("foo2") + o(G.root.firstChild.className).equals("foo2") }) o("to selector and an undefined class", function() { - m.render(root, m(".foo1", {className: null})) - m.render(root, m(".foo2", {class: undefined})) + m.render(G.root, m(".foo1", {className: null})) + m.render(G.root, m(".foo2", {class: undefined})) - o(root.firstChild.className).equals("foo2") + o(G.root.firstChild.className).equals("foo2") }) o("to selector and an undefined className", function() { - m.render(root, m(".foo1", {className: null})) - m.render(root, m(".foo2", {className: undefined})) + m.render(G.root, m(".foo1", {className: null})) + m.render(G.root, m(".foo2", {className: undefined})) - o(root.firstChild.className).equals("foo2") + o(G.root.firstChild.className).equals("foo2") }) }) o.spec("from selector and an undefined class", function() { o("to selector only", function() { - m.render(root, m(".foo1", {class: undefined})) - m.render(root, m(".foo2")) + m.render(G.root, m(".foo1", {class: undefined})) + m.render(G.root, m(".foo2")) - o(root.firstChild.className).equals("foo2") + o(G.root.firstChild.className).equals("foo2") }) o("to class only", function() { - m.render(root, m(".foo1", {class: undefined})) - m.render(root, m("div", {class: "foo2"})) + m.render(G.root, m(".foo1", {class: undefined})) + m.render(G.root, m("div", {class: "foo2"})) - o(root.firstChild.className).equals("foo2") + o(G.root.firstChild.className).equals("foo2") }) o("to className only", function() { - m.render(root, m(".foo1", {class: undefined})) - m.render(root, m("div", {className: "foo2"})) + m.render(G.root, m(".foo1", {class: undefined})) + m.render(G.root, m("div", {className: "foo2"})) - o(root.firstChild.className).equals("foo2") + o(G.root.firstChild.className).equals("foo2") }) o("to selector and class", function() { - m.render(root, m(".foo1", {class: undefined})) - m.render(root, m(".bar2", {class: "foo2"})) + m.render(G.root, m(".foo1", {class: undefined})) + m.render(G.root, m(".bar2", {class: "foo2"})) - o(root.firstChild.className.split(" ").sort()).deepEquals(["bar2", "foo2"]) + o(G.root.firstChild.className.split(" ").sort()).deepEquals(["bar2", "foo2"]) }) o("to selector and className", function() { - m.render(root, m(".foo1", {class: undefined})) - m.render(root, m(".bar2", {className: "foo2"})) + m.render(G.root, m(".foo1", {class: undefined})) + m.render(G.root, m(".bar2", {className: "foo2"})) - o(root.firstChild.className.split(" ").sort()).deepEquals(["bar2", "foo2"]) + o(G.root.firstChild.className.split(" ").sort()).deepEquals(["bar2", "foo2"]) }) o("to selector and a null class", function() { - m.render(root, m(".foo1", {class: undefined})) - m.render(root, m(".foo2", {class: null})) + m.render(G.root, m(".foo1", {class: undefined})) + m.render(G.root, m(".foo2", {class: null})) - o(root.firstChild.className).equals("foo2") + o(G.root.firstChild.className).equals("foo2") }) o("to selector and a null className", function() { - m.render(root, m(".foo1", {class: undefined})) - m.render(root, m(".foo2", {className: null})) + m.render(G.root, m(".foo1", {class: undefined})) + m.render(G.root, m(".foo2", {className: null})) - o(root.firstChild.className).equals("foo2") + o(G.root.firstChild.className).equals("foo2") }) o("to selector and an undefined class", function() { - m.render(root, m(".foo1", {class: undefined})) - m.render(root, m(".foo2", {class: undefined})) + m.render(G.root, m(".foo1", {class: undefined})) + m.render(G.root, m(".foo2", {class: undefined})) - o(root.firstChild.className).equals("foo2") + o(G.root.firstChild.className).equals("foo2") }) o("to selector and an undefined className", function() { - m.render(root, m(".foo1", {class: undefined})) - m.render(root, m(".foo2", {className: undefined})) + m.render(G.root, m(".foo1", {class: undefined})) + m.render(G.root, m(".foo2", {className: undefined})) - o(root.firstChild.className).equals("foo2") + o(G.root.firstChild.className).equals("foo2") }) }) o.spec("from selector and an undefined className", function() { o("to selector only", function() { - m.render(root, m(".foo1", {className: undefined})) - m.render(root, m(".foo2")) + m.render(G.root, m(".foo1", {className: undefined})) + m.render(G.root, m(".foo2")) - o(root.firstChild.className).equals("foo2") + o(G.root.firstChild.className).equals("foo2") }) o("to class only", function() { - m.render(root, m(".foo1", {className: undefined})) - m.render(root, m("div", {class: "foo2"})) + m.render(G.root, m(".foo1", {className: undefined})) + m.render(G.root, m("div", {class: "foo2"})) - o(root.firstChild.className).equals("foo2") + o(G.root.firstChild.className).equals("foo2") }) o("to className only", function() { - m.render(root, m(".foo1", {className: undefined})) - m.render(root, m("div", {className: "foo2"})) + m.render(G.root, m(".foo1", {className: undefined})) + m.render(G.root, m("div", {className: "foo2"})) - o(root.firstChild.className).equals("foo2") + o(G.root.firstChild.className).equals("foo2") }) o("to selector and class", function() { - m.render(root, m(".foo1", {className: undefined})) - m.render(root, m(".bar2", {class: "foo2"})) + m.render(G.root, m(".foo1", {className: undefined})) + m.render(G.root, m(".bar2", {class: "foo2"})) - o(root.firstChild.className.split(" ").sort()).deepEquals(["bar2", "foo2"]) + o(G.root.firstChild.className.split(" ").sort()).deepEquals(["bar2", "foo2"]) }) o("to selector and className", function() { - m.render(root, m(".foo1", {className: undefined})) - m.render(root, m(".bar2", {className: "foo2"})) + m.render(G.root, m(".foo1", {className: undefined})) + m.render(G.root, m(".bar2", {className: "foo2"})) - o(root.firstChild.className.split(" ").sort()).deepEquals(["bar2", "foo2"]) + o(G.root.firstChild.className.split(" ").sort()).deepEquals(["bar2", "foo2"]) }) o("to selector and a null class", function() { - m.render(root, m(".foo1", {className: undefined})) - m.render(root, m(".foo2", {class: null})) + m.render(G.root, m(".foo1", {className: undefined})) + m.render(G.root, m(".foo2", {class: null})) - o(root.firstChild.className).equals("foo2") + o(G.root.firstChild.className).equals("foo2") }) o("to selector and a null className", function() { - m.render(root, m(".foo1", {className: undefined})) - m.render(root, m(".foo2", {className: null})) + m.render(G.root, m(".foo1", {className: undefined})) + m.render(G.root, m(".foo2", {className: null})) - o(root.firstChild.className).equals("foo2") + o(G.root.firstChild.className).equals("foo2") }) o("to selector and an undefined class", function() { - m.render(root, m(".foo1", {className: undefined})) - m.render(root, m(".foo2", {class: undefined})) + m.render(G.root, m(".foo1", {className: undefined})) + m.render(G.root, m(".foo2", {class: undefined})) - o(root.firstChild.className).equals("foo2") + o(G.root.firstChild.className).equals("foo2") }) o("to selector and an undefined className", function() { - m.render(root, m(".foo1", {className: undefined})) - m.render(root, m(".foo2", {className: undefined})) + m.render(G.root, m(".foo1", {className: undefined})) + m.render(G.root, m(".foo2", {className: undefined})) - o(root.firstChild.className).equals("foo2") + o(G.root.firstChild.className).equals("foo2") }) }) }) diff --git a/tests/render/render.js b/tests/render/render.js index 6c15c895f..59e2e23cc 100644 --- a/tests/render/render.js +++ b/tests/render/render.js @@ -1,49 +1,46 @@ import o from "ospec" -import domMock from "../../test-utils/domMock.js" +import {setupGlobals} from "../../test-utils/global.js" + import m from "../../src/entry/mithril.esm.js" o.spec("render", function() { - var $window, root - o.beforeEach(function() { - $window = domMock() - root = $window.document.createElement("div") - }) + var G = setupGlobals() o("renders plain text", function() { - m.render(root, "a") - o(root.childNodes.length).equals(1) - o(root.childNodes[0].nodeValue).equals("a") + m.render(G.root, "a") + o(G.root.childNodes.length).equals(1) + o(G.root.childNodes[0].nodeValue).equals("a") }) o("updates plain text", function() { - m.render(root, "a") - m.render(root, "b") - o(root.childNodes.length).equals(1) - o(root.childNodes[0].nodeValue).equals("b") + m.render(G.root, "a") + m.render(G.root, "b") + o(G.root.childNodes.length).equals(1) + o(G.root.childNodes[0].nodeValue).equals("b") }) o("renders a number", function() { - m.render(root, 1) - o(root.childNodes.length).equals(1) - o(root.childNodes[0].nodeValue).equals("1") + m.render(G.root, 1) + o(G.root.childNodes.length).equals(1) + o(G.root.childNodes[0].nodeValue).equals("1") }) o("updates a number", function() { - m.render(root, 1) - m.render(root, 2) - o(root.childNodes.length).equals(1) - o(root.childNodes[0].nodeValue).equals("2") + m.render(G.root, 1) + m.render(G.root, 2) + o(G.root.childNodes.length).equals(1) + o(G.root.childNodes[0].nodeValue).equals("2") }) o("overwrites existing content", function() { var vnodes = [] - root.appendChild($window.document.createElement("div")); + G.root.appendChild(G.window.document.createElement("div")); - m.render(root, vnodes) + m.render(G.root, vnodes) - o(root.childNodes.length).equals(0) + o(G.root.childNodes.length).equals(0) }) o("throws on invalid root node", function() { @@ -60,12 +57,12 @@ o.spec("render", function() { var A = o.spy(() => { throw new Error("error") }) var throwCount = 0 - try {m.render(root, m(A))} catch (e) {throwCount++} + try {m.render(G.root, m(A))} catch (e) {throwCount++} o(throwCount).equals(1) o(A.callCount).equals(1) - try {m.render(root, m(A))} catch (e) {throwCount++} + try {m.render(G.root, m(A))} catch (e) {throwCount++} o(throwCount).equals(2) o(A.callCount).equals(2) @@ -74,13 +71,13 @@ o.spec("render", function() { var A = o.spy(() => view) var view = o.spy(() => { throw new Error("error") }) var throwCount = 0 - try {m.render(root, m(A))} catch (e) {throwCount++} + try {m.render(G.root, m(A))} catch (e) {throwCount++} o(throwCount).equals(1) o(A.callCount).equals(1) o(view.callCount).equals(1) - try {m.render(root, m(A))} catch (e) {throwCount++} + try {m.render(G.root, m(A))} catch (e) {throwCount++} o(throwCount).equals(2) o(A.callCount).equals(2) @@ -89,133 +86,136 @@ o.spec("render", function() { o("lifecycle methods work in keyed children of recycled keyed", function() { var onabortA = o.spy() var onabortB = o.spy() - var layoutA = o.spy((_, signal) => { signal.onabort = onabortA }) - var layoutB = o.spy((_, signal) => { signal.onabort = onabortB }) + var createA = o.spy((_, signal) => { signal.onabort = onabortA }) + var updateA = o.spy((_, signal) => { signal.onabort = onabortA }) + var createB = o.spy((_, signal) => { signal.onabort = onabortB }) + var updateB = o.spy((_, signal) => { signal.onabort = onabortB }) var a = function() { return m.key(1, m("div", - m.key(11, m("div", m.layout(layoutA))), + m.key(11, m("div", m.layout(createA, updateA))), m.key(12, m("div")) )) } var b = function() { return m.key(2, m("div", - m.key(21, m("div", m.layout(layoutB))), + m.key(21, m("div", m.layout(createB, updateB))), m.key(22, m("div")) )) } - m.render(root, a()) - var first = root.firstChild.firstChild - m.render(root, b()) - var second = root.firstChild.firstChild - m.render(root, a()) - var third = root.firstChild.firstChild - - o(layoutA.callCount).equals(2) - o(layoutA.calls[0].args[0]).equals(first) - o(layoutA.calls[0].args[1].aborted).equals(true) - o(layoutA.calls[0].args[2]).equals(true) - o(layoutA.calls[1].args[0]).equals(third) - o(layoutA.calls[1].args[1].aborted).equals(false) - o(layoutA.calls[1].args[2]).equals(true) + m.render(G.root, a()) + var first = G.root.firstChild.firstChild + m.render(G.root, b()) + var second = G.root.firstChild.firstChild + m.render(G.root, a()) + var third = G.root.firstChild.firstChild + + o(createA.callCount).equals(2) + o(createA.calls[0].args[0]).equals(first) + o(createA.calls[0].args[1].aborted).equals(true) + o(createA.calls[1].args[0]).equals(third) + o(createA.calls[1].args[1].aborted).equals(false) + o(updateA.callCount).equals(0) o(onabortA.callCount).equals(1) - o(layoutB.callCount).equals(1) - o(layoutB.calls[0].args[0]).equals(second) - o(layoutB.calls[0].args[1]).notEquals(layoutA.calls[0].args[1]) - o(layoutB.calls[0].args[1].aborted).equals(true) - o(layoutB.calls[0].args[2]).equals(true) + o(createB.callCount).equals(1) + o(createB.calls[0].args[0]).equals(second) + o(createB.calls[0].args[1]).notEquals(createA.calls[0].args[1]) + o(createB.calls[0].args[1].aborted).equals(true) + o(updateB.callCount).equals(0) o(onabortB.callCount).equals(1) }) o("lifecycle methods work in unkeyed children of recycled keyed", function() { var onabortA = o.spy() var onabortB = o.spy() - var layoutA = o.spy((_, signal) => { signal.onabort = onabortA }) - var layoutB = o.spy((_, signal) => { signal.onabort = onabortB }) + var createA = o.spy((_, signal) => { signal.onabort = onabortA }) + var updateA = o.spy((_, signal) => { signal.onabort = onabortA }) + var createB = o.spy((_, signal) => { signal.onabort = onabortB }) + var updateB = o.spy((_, signal) => { signal.onabort = onabortB }) var a = function() { return m.key(1, m("div", - m("div", m.layout(layoutA)) + m("div", m.layout(createA, updateA)) )) } var b = function() { return m.key(2, m("div", - m("div", m.layout(layoutB)) + m("div", m.layout(createB, updateB)) )) } - m.render(root, a()) - var first = root.firstChild.firstChild - m.render(root, b()) - var second = root.firstChild.firstChild - m.render(root, a()) - var third = root.firstChild.firstChild - - o(layoutA.callCount).equals(2) - o(layoutA.calls[0].args[0]).equals(first) - o(layoutA.calls[0].args[1].aborted).equals(true) - o(layoutA.calls[0].args[2]).equals(true) - o(layoutA.calls[1].args[0]).equals(third) - o(layoutA.calls[1].args[1].aborted).equals(false) - o(layoutA.calls[1].args[2]).equals(true) + m.render(G.root, a()) + var first = G.root.firstChild.firstChild + m.render(G.root, b()) + var second = G.root.firstChild.firstChild + m.render(G.root, a()) + var third = G.root.firstChild.firstChild + + o(createA.callCount).equals(2) + o(createA.calls[0].args[0]).equals(first) + o(createA.calls[0].args[1].aborted).equals(true) + o(createA.calls[1].args[0]).equals(third) + o(createA.calls[1].args[1].aborted).equals(false) o(onabortA.callCount).equals(1) - o(layoutB.callCount).equals(1) - o(layoutB.calls[0].args[0]).equals(second) - o(layoutB.calls[0].args[1]).notEquals(layoutA.calls[0].args[1]) - o(layoutB.calls[0].args[1].aborted).equals(true) - o(layoutB.calls[0].args[2]).equals(true) + o(createB.callCount).equals(1) + o(createB.calls[0].args[0]).equals(second) + o(createB.calls[0].args[1]).notEquals(createA.calls[0].args[1]) + o(createB.calls[0].args[1].aborted).equals(true) o(onabortB.callCount).equals(1) }) o("update lifecycle methods work on children of recycled keyed", function() { var onabortA = o.spy() var onabortB = o.spy() - var layoutA = o.spy((_, signal) => { signal.onabort = onabortA }) - var layoutB = o.spy((_, signal) => { signal.onabort = onabortB }) + var createA = o.spy((_, signal) => { signal.onabort = onabortA }) + var updateA = o.spy((_, signal) => { signal.onabort = onabortA }) + var createB = o.spy((_, signal) => { signal.onabort = onabortB }) + var updateB = o.spy((_, signal) => { signal.onabort = onabortB }) var a = function() { return m.key(1, m("div", - m("div", m.layout(layoutA)) + m("div", m.layout(createA, updateA)) )) } var b = function() { return m.key(2, m("div", - m("div", m.layout(layoutB)) + m("div", m.layout(createB, updateB)) )) } - m.render(root, a()) - m.render(root, a()) - var first = root.firstChild.firstChild - o(layoutA.callCount).equals(2) - o(layoutA.calls[0].args[0]).equals(first) - o(layoutA.calls[1].args[0]).equals(first) - o(layoutA.calls[0].args[1]).equals(layoutA.calls[1].args[1]) - o(layoutA.calls[0].args[1].aborted).equals(false) - o(layoutA.calls[0].args[2]).equals(true) - o(layoutA.calls[1].args[2]).equals(false) + m.render(G.root, a()) + m.render(G.root, a()) + var first = G.root.firstChild.firstChild + o(createA.callCount).equals(1) + o(updateA.callCount).equals(1) + o(createA.calls[0].args[0]).equals(first) + o(updateA.calls[0].args[0]).equals(first) + o(createA.calls[0].args[1]).equals(updateA.calls[0].args[1]) + o(createA.calls[0].args[1].aborted).equals(false) o(onabortA.callCount).equals(0) - m.render(root, b()) - var second = root.firstChild.firstChild - o(layoutA.callCount).equals(2) - o(layoutA.calls[0].args[1].aborted).equals(true) + m.render(G.root, b()) + var second = G.root.firstChild.firstChild + o(createA.callCount).equals(1) + o(updateA.callCount).equals(1) + o(createA.calls[0].args[1].aborted).equals(true) o(onabortA.callCount).equals(1) - o(layoutB.callCount).equals(1) - o(layoutB.calls[0].args[0]).equals(second) - o(layoutB.calls[0].args[1].aborted).equals(false) - o(layoutB.calls[0].args[2]).equals(true) + o(createB.callCount).equals(1) + o(updateB.callCount).equals(0) + o(createB.calls[0].args[0]).equals(second) + o(createB.calls[0].args[1].aborted).equals(false) o(onabortB.callCount).equals(0) - m.render(root, a()) - m.render(root, a()) - var third = root.firstChild.firstChild - o(layoutB.callCount).equals(1) - o(layoutB.calls[0].args[1].aborted).equals(true) + m.render(G.root, a()) + m.render(G.root, a()) + var third = G.root.firstChild.firstChild + o(createB.callCount).equals(1) + o(updateB.callCount).equals(0) + o(createB.calls[0].args[1].aborted).equals(true) o(onabortB.callCount).equals(1) - o(layoutA.callCount).equals(4) - o(layoutA.calls[2].args[0]).equals(third) - o(layoutA.calls[2].args[1]).notEquals(layoutA.calls[1].args[1]) - o(layoutA.calls[2].args[1].aborted).equals(false) - o(layoutA.calls[2].args[2]).equals(true) + o(createA.callCount).equals(2) + o(updateA.callCount).equals(2) + o(createA.calls[1].args[0]).equals(third) + o(createA.calls[1].args[1]).notEquals(updateA.calls[0].args[1]) + o(createA.calls[1].args[1].aborted).equals(false) o(onabortA.callCount).equals(1) }) o("svg namespace is preserved in keyed diff (#1820)", function(){ @@ -224,48 +224,48 @@ o.spec("render", function() { m.key(0, m("g")), m.key(1, m("g")) ) - m.render(root, svg) + m.render(G.root, svg) - o(svg.dom.namespaceURI).equals("http://www.w3.org/2000/svg") - o(svg.dom.childNodes[0].namespaceURI).equals("http://www.w3.org/2000/svg") - o(svg.dom.childNodes[1].namespaceURI).equals("http://www.w3.org/2000/svg") + o(svg.d.namespaceURI).equals("http://www.w3.org/2000/svg") + o(svg.d.childNodes[0].namespaceURI).equals("http://www.w3.org/2000/svg") + o(svg.d.childNodes[1].namespaceURI).equals("http://www.w3.org/2000/svg") svg = m("svg", m.key(1, m("g", {x: 1})), m.key(2, m("g", {x: 2})) ) - m.render(root, svg) + m.render(G.root, svg) - o(svg.dom.namespaceURI).equals("http://www.w3.org/2000/svg") - o(svg.dom.childNodes[0].namespaceURI).equals("http://www.w3.org/2000/svg") - o(svg.dom.childNodes[1].namespaceURI).equals("http://www.w3.org/2000/svg") + o(svg.d.namespaceURI).equals("http://www.w3.org/2000/svg") + o(svg.d.childNodes[0].namespaceURI).equals("http://www.w3.org/2000/svg") + o(svg.d.childNodes[1].namespaceURI).equals("http://www.w3.org/2000/svg") }) o("the namespace of the root is passed to children", function() { - m.render(root, m("svg")) - o(root.childNodes[0].namespaceURI).equals("http://www.w3.org/2000/svg") - m.render(root.childNodes[0], m("g")) - o(root.childNodes[0].childNodes[0].namespaceURI).equals("http://www.w3.org/2000/svg") + m.render(G.root, m("svg")) + o(G.root.childNodes[0].namespaceURI).equals("http://www.w3.org/2000/svg") + m.render(G.root.childNodes[0], m("g")) + o(G.root.childNodes[0].childNodes[0].namespaceURI).equals("http://www.w3.org/2000/svg") }) o("does not allow reentrant invocations", function() { var thrown = [] function A() { - try {m.render(root, m(A))} catch (e) {thrown.push("construct")} + try {m.render(G.root, m(A))} catch (e) {thrown.push("construct")} return () => { - try {m.render(root, m(A))} catch (e) {thrown.push("view")} + try {m.render(G.root, m(A))} catch (e) {thrown.push("view")} } } - m.render(root, m(A)) + m.render(G.root, m(A)) o(thrown).deepEquals([ "construct", "view", ]) - m.render(root, m(A)) + m.render(G.root, m(A)) o(thrown).deepEquals([ "construct", "view", "view", ]) - m.render(root, []) + m.render(G.root, []) o(thrown).deepEquals([ "construct", "view", diff --git a/tests/render/retain.js b/tests/render/retain.js index 2c0008f1e..da84cfad5 100644 --- a/tests/render/retain.js +++ b/tests/render/retain.js @@ -1,25 +1,22 @@ import o from "ospec" -import domMock from "../../test-utils/domMock.js" +import {setupGlobals} from "../../test-utils/global.js" + import m from "../../src/entry/mithril.esm.js" o.spec("retain", function() { - var $window, root - o.beforeEach(function() { - $window = domMock() - root = $window.document.createElement("div") - }) + var G = setupGlobals() o("prevents update in element", function() { var vnode = m("div", {id: "a"}, "b") var updated = m.retain() - m.render(root, vnode) - m.render(root, updated) + m.render(G.root, vnode) + m.render(G.root, updated) - o(root.firstChild.attributes["id"].value).equals("a") - o(root.firstChild.childNodes.length).equals(1) - o(root.firstChild.childNodes[0].nodeValue).equals("b") + o(G.root.firstChild.attributes["id"].value).equals("a") + o(G.root.firstChild.childNodes.length).equals(1) + o(G.root.firstChild.childNodes[0].nodeValue).equals("b") o(updated).deepEquals(vnode) }) @@ -27,27 +24,27 @@ o.spec("retain", function() { var vnode = m.normalize(["a"]) var updated = m.retain() - m.render(root, vnode) - m.render(root, updated) + m.render(G.root, vnode) + m.render(G.root, updated) - o(root.firstChild.nodeValue).equals("a") + o(G.root.firstChild.nodeValue).equals("a") o(updated).deepEquals(vnode) }) o("throws on creation", function() { - o(() => m.render(root, m.retain())).throws(Error) + o(() => m.render(G.root, m.retain())).throws(Error) }) o("prevents update in component", function() { - var component = (vnode, old) => (old ? m.retain() : m("div", vnode.children)) + var component = (attrs, old) => (old ? m.retain() : m("div", attrs.children)) var vnode = m(component, "a") var updated = m(component, "b") - m.render(root, vnode) - m.render(root, updated) + m.render(G.root, vnode) + m.render(G.root, updated) - o(root.firstChild.firstChild.nodeValue).equals("a") - o(updated.children).deepEquals(vnode.children) + o(G.root.firstChild.firstChild.nodeValue).equals("a") + o(updated.c).deepEquals(vnode.c) }) o("prevents update in component and for component", function() { @@ -55,10 +52,10 @@ o.spec("retain", function() { var vnode = m(component, {id: "a"}) var updated = m.retain() - m.render(root, vnode) - m.render(root, updated) + m.render(G.root, vnode) + m.render(G.root, updated) - o(root.firstChild.attributes["id"].value).equals("a") + o(G.root.firstChild.attributes["id"].value).equals("a") o(updated).deepEquals(vnode) }) @@ -67,16 +64,16 @@ o.spec("retain", function() { var vnode = m(component, {id: "a"}) var updated = m.retain() - m.render(root, vnode) - m.render(root, updated) + m.render(G.root, vnode) + m.render(G.root, updated) - o(root.firstChild.attributes["id"].value).equals("a") + o(G.root.firstChild.attributes["id"].value).equals("a") o(updated).deepEquals(vnode) }) o("throws if used on component creation", function() { var component = () => m.retain() - o(() => m.render(root, m(component))).throws(Error) + o(() => m.render(G.root, m(component))).throws(Error) }) }) diff --git a/tests/render/textContent.js b/tests/render/textContent.js index b141bcf89..1dfe9d5f8 100644 --- a/tests/render/textContent.js +++ b/tests/render/textContent.js @@ -1,194 +1,191 @@ import o from "ospec" -import domMock from "../../test-utils/domMock.js" +import {setupGlobals} from "../../test-utils/global.js" + import m from "../../src/entry/mithril.esm.js" o.spec("textContent", function() { - var $window, root - o.beforeEach(function() { - $window = domMock() - root = $window.document.createElement("div") - }) + var G = setupGlobals() o("ignores null", function() { var vnode = m("a", null) - m.render(root, vnode) + m.render(G.root, vnode) - o(root.childNodes.length).equals(1) - o(vnode.dom.childNodes.length).equals(0) - o(vnode.dom).equals(root.childNodes[0]) + o(G.root.childNodes.length).equals(1) + o(vnode.d.childNodes.length).equals(0) + o(vnode.d).equals(G.root.childNodes[0]) }) o("ignores undefined", function() { var vnode = m("a", undefined) - m.render(root, vnode) + m.render(G.root, vnode) - o(root.childNodes.length).equals(1) - o(vnode.dom.childNodes.length).equals(0) - o(vnode.dom).equals(root.childNodes[0]) + o(G.root.childNodes.length).equals(1) + o(vnode.d.childNodes.length).equals(0) + o(vnode.d).equals(G.root.childNodes[0]) }) o("creates string", function() { var vnode = m("a", "a") - m.render(root, vnode) + m.render(G.root, vnode) - o(root.childNodes.length).equals(1) - o(vnode.dom.childNodes.length).equals(1) - o(vnode.dom.childNodes[0].nodeValue).equals("a") - o(vnode.dom).equals(root.childNodes[0]) + o(G.root.childNodes.length).equals(1) + o(vnode.d.childNodes.length).equals(1) + o(vnode.d.childNodes[0].nodeValue).equals("a") + o(vnode.d).equals(G.root.childNodes[0]) }) o("creates falsy string", function() { var vnode = m("a", "") - m.render(root, vnode) + m.render(G.root, vnode) - o(root.childNodes.length).equals(1) - o(vnode.dom.childNodes.length).equals(1) - o(vnode.dom.childNodes[0].nodeValue).equals("") - o(vnode.dom).equals(root.childNodes[0]) + o(G.root.childNodes.length).equals(1) + o(vnode.d.childNodes.length).equals(1) + o(vnode.d.childNodes[0].nodeValue).equals("") + o(vnode.d).equals(G.root.childNodes[0]) }) o("creates number", function() { var vnode = m("a", 1) - m.render(root, vnode) + m.render(G.root, vnode) - o(root.childNodes.length).equals(1) - o(vnode.dom.childNodes.length).equals(1) - o(vnode.dom.childNodes[0].nodeValue).equals("1") - o(vnode.dom).equals(root.childNodes[0]) + o(G.root.childNodes.length).equals(1) + o(vnode.d.childNodes.length).equals(1) + o(vnode.d.childNodes[0].nodeValue).equals("1") + o(vnode.d).equals(G.root.childNodes[0]) }) o("creates falsy number", function() { var vnode = m("a", 0) - m.render(root, vnode) + m.render(G.root, vnode) - o(root.childNodes.length).equals(1) - o(vnode.dom.childNodes.length).equals(1) - o(vnode.dom.childNodes[0].nodeValue).equals("0") - o(vnode.dom).equals(root.childNodes[0]) + o(G.root.childNodes.length).equals(1) + o(vnode.d.childNodes.length).equals(1) + o(vnode.d.childNodes[0].nodeValue).equals("0") + o(vnode.d).equals(G.root.childNodes[0]) }) o("creates boolean", function() { var vnode = m("a", true) - m.render(root, vnode) + m.render(G.root, vnode) - o(root.childNodes.length).equals(1) - o(vnode.dom.childNodes.length).equals(0) - o(vnode.dom).equals(root.childNodes[0]) + o(G.root.childNodes.length).equals(1) + o(vnode.d.childNodes.length).equals(0) + o(vnode.d).equals(G.root.childNodes[0]) }) o("creates falsy boolean", function() { var vnode = m("a", false) - m.render(root, vnode) + m.render(G.root, vnode) - o(root.childNodes.length).equals(1) - o(vnode.dom.childNodes.length).equals(0) - o(vnode.dom).equals(root.childNodes[0]) + o(G.root.childNodes.length).equals(1) + o(vnode.d.childNodes.length).equals(0) + o(vnode.d).equals(G.root.childNodes[0]) }) o("updates to string", function() { var vnode = m("a", "a") var updated = m("a", "b") - m.render(root, vnode) - m.render(root, updated) + m.render(G.root, vnode) + m.render(G.root, updated) - o(root.childNodes.length).equals(1) - o(vnode.dom.childNodes.length).equals(1) - o(vnode.dom.childNodes[0].nodeValue).equals("b") - o(updated.dom).equals(root.childNodes[0]) + o(G.root.childNodes.length).equals(1) + o(vnode.d.childNodes.length).equals(1) + o(vnode.d.childNodes[0].nodeValue).equals("b") + o(updated.d).equals(G.root.childNodes[0]) }) o("updates to falsy string", function() { var vnode = m("a", "a") var updated = m("a", "") - m.render(root, vnode) - m.render(root, updated) + m.render(G.root, vnode) + m.render(G.root, updated) - o(root.childNodes.length).equals(1) - o(vnode.dom.childNodes.length).equals(1) - o(vnode.dom.childNodes[0].nodeValue).equals("") - o(updated.dom).equals(root.childNodes[0]) + o(G.root.childNodes.length).equals(1) + o(vnode.d.childNodes.length).equals(1) + o(vnode.d.childNodes[0].nodeValue).equals("") + o(updated.d).equals(G.root.childNodes[0]) }) o("updates to number", function() { var vnode = m("a", "a") var updated = m("a", 1) - m.render(root, vnode) - m.render(root, updated) + m.render(G.root, vnode) + m.render(G.root, updated) - o(root.childNodes.length).equals(1) - o(vnode.dom.childNodes.length).equals(1) - o(vnode.dom.childNodes[0].nodeValue).equals("1") - o(updated.dom).equals(root.childNodes[0]) + o(G.root.childNodes.length).equals(1) + o(vnode.d.childNodes.length).equals(1) + o(vnode.d.childNodes[0].nodeValue).equals("1") + o(updated.d).equals(G.root.childNodes[0]) }) o("updates to falsy number", function() { var vnode = m("a", "a") var updated = m("a", 0) - m.render(root, vnode) - m.render(root, updated) + m.render(G.root, vnode) + m.render(G.root, updated) - o(root.childNodes.length).equals(1) - o(vnode.dom.childNodes.length).equals(1) - o(vnode.dom.childNodes[0].nodeValue).equals("0") - o(updated.dom).equals(root.childNodes[0]) + o(G.root.childNodes.length).equals(1) + o(vnode.d.childNodes.length).equals(1) + o(vnode.d.childNodes[0].nodeValue).equals("0") + o(updated.d).equals(G.root.childNodes[0]) }) o("updates true to nothing", function() { var vnode = m("a", "a") var updated = m("a", true) - m.render(root, vnode) - m.render(root, updated) + m.render(G.root, vnode) + m.render(G.root, updated) - o(root.childNodes.length).equals(1) - o(vnode.dom.childNodes.length).equals(0) - o(updated.dom).equals(root.childNodes[0]) + o(G.root.childNodes.length).equals(1) + o(vnode.d.childNodes.length).equals(0) + o(updated.d).equals(G.root.childNodes[0]) }) o("updates false to nothing", function() { var vnode = m("a", "a") var updated = m("a", false) - m.render(root, vnode) - m.render(root, updated) + m.render(G.root, vnode) + m.render(G.root, updated) - o(root.childNodes.length).equals(1) - o(vnode.dom.childNodes.length).equals(0) - o(updated.dom).equals(root.childNodes[0]) + o(G.root.childNodes.length).equals(1) + o(vnode.d.childNodes.length).equals(0) + o(updated.d).equals(G.root.childNodes[0]) }) o("updates with typecasting", function() { var vnode = m("a", "1") var updated = m("a", 1) - m.render(root, vnode) - m.render(root, updated) + m.render(G.root, vnode) + m.render(G.root, updated) - o(root.childNodes.length).equals(1) - o(vnode.dom.childNodes.length).equals(1) - o(vnode.dom.childNodes[0].nodeValue).equals("1") - o(updated.dom).equals(root.childNodes[0]) + o(G.root.childNodes.length).equals(1) + o(vnode.d.childNodes.length).equals(1) + o(vnode.d.childNodes[0].nodeValue).equals("1") + o(updated.d).equals(G.root.childNodes[0]) }) o("updates from without text to with text", function() { var vnode = m("a") var updated = m("a", "b") - m.render(root, vnode) - m.render(root, updated) + m.render(G.root, vnode) + m.render(G.root, updated) - o(root.childNodes.length).equals(1) - o(vnode.dom.childNodes.length).equals(1) - o(vnode.dom.childNodes[0].nodeValue).equals("b") - o(updated.dom).equals(root.childNodes[0]) + o(G.root.childNodes.length).equals(1) + o(vnode.d.childNodes.length).equals(1) + o(vnode.d.childNodes[0].nodeValue).equals("b") + o(updated.d).equals(G.root.childNodes[0]) }) o("updates from with text to without text", function() { var vnode = m("a", "a") var updated = m("a") - m.render(root, vnode) - m.render(root, updated) + m.render(G.root, vnode) + m.render(G.root, updated) - o(root.childNodes.length).equals(1) - o(vnode.dom.childNodes.length).equals(0) - o(updated.dom).equals(root.childNodes[0]) + o(G.root.childNodes.length).equals(1) + o(vnode.d.childNodes.length).equals(0) + o(updated.d).equals(G.root.childNodes[0]) }) }) diff --git a/tests/render/updateElement.js b/tests/render/updateElement.js index 74113bc04..565e7a83b 100644 --- a/tests/render/updateElement.js +++ b/tests/render/updateElement.js @@ -1,212 +1,209 @@ import o from "ospec" -import domMock from "../../test-utils/domMock.js" +import {setupGlobals} from "../../test-utils/global.js" + import m from "../../src/entry/mithril.esm.js" o.spec("updateElement", function() { - var $window, root - o.beforeEach(function() { - $window = domMock() - root = $window.document.createElement("div") - }) + var G = setupGlobals() o("updates attr", function() { var vnode = m("a", {id: "b"}) var updated = m("a", {id: "c"}) - m.render(root, vnode) - m.render(root, updated) + m.render(G.root, vnode) + m.render(G.root, updated) - o(updated.dom).equals(vnode.dom) - o(updated.dom).equals(root.firstChild) - o(updated.dom.attributes["id"].value).equals("c") + o(updated.d).equals(vnode.d) + o(updated.d).equals(G.root.firstChild) + o(updated.d.attributes["id"].value).equals("c") }) o("adds attr", function() { var vnode = m("a", {id: "b"}) var updated = m("a", {id: "c", title: "d"}) - m.render(root, vnode) - m.render(root, updated) + m.render(G.root, vnode) + m.render(G.root, updated) - o(updated.dom).equals(vnode.dom) - o(updated.dom).equals(root.firstChild) - o(updated.dom.attributes["title"].value).equals("d") + o(updated.d).equals(vnode.d) + o(updated.d).equals(G.root.firstChild) + o(updated.d.attributes["title"].value).equals("d") }) o("adds attr from empty attrs", function() { var vnode = m("a") var updated = m("a", {title: "d"}) - m.render(root, vnode) - m.render(root, updated) + m.render(G.root, vnode) + m.render(G.root, updated) - o(updated.dom).equals(vnode.dom) - o(updated.dom).equals(root.firstChild) - o(updated.dom.attributes["title"].value).equals("d") + o(updated.d).equals(vnode.d) + o(updated.d).equals(G.root.firstChild) + o(updated.d.attributes["title"].value).equals("d") }) o("removes attr", function() { var vnode = m("a", {id: "b", title: "d"}) var updated = m("a", {id: "c"}) - m.render(root, vnode) - m.render(root, updated) + m.render(G.root, vnode) + m.render(G.root, updated) - o(updated.dom).equals(vnode.dom) - o(updated.dom).equals(root.firstChild) - o("title" in updated.dom.attributes).equals(false) + o(updated.d).equals(vnode.d) + o(updated.d).equals(G.root.firstChild) + o("title" in updated.d.attributes).equals(false) }) o("removes class", function() { var vnode = m("a", {id: "b", className: "d"}) var updated = m("a", {id: "c"}) - m.render(root, vnode) - m.render(root, updated) + m.render(G.root, vnode) + m.render(G.root, updated) - o(updated.dom).equals(vnode.dom) - o(updated.dom).equals(root.firstChild) - o("class" in updated.dom.attributes).equals(false) + o(updated.d).equals(vnode.d) + o(updated.d).equals(G.root.firstChild) + o("class" in updated.d.attributes).equals(false) }) o("creates style object", function() { var vnode = m("a") var updated = m("a", {style: {backgroundColor: "green"}}) - m.render(root, vnode) - m.render(root, updated) + m.render(G.root, vnode) + m.render(G.root, updated) - o(updated.dom.style.backgroundColor).equals("green") + o(updated.d.style.backgroundColor).equals("green") }) o("creates style string", function() { var vnode = m("a") var updated = m("a", {style: "background-color:green"}) - m.render(root, vnode) - m.render(root, updated) + m.render(G.root, vnode) + m.render(G.root, updated) - o(updated.dom.style.backgroundColor).equals("green") + o(updated.d.style.backgroundColor).equals("green") }) o("updates style from object to object", function() { var vnode = m("a", {style: {backgroundColor: "red"}}) var updated = m("a", {style: {backgroundColor: "green"}}) - m.render(root, vnode) - m.render(root, updated) + m.render(G.root, vnode) + m.render(G.root, updated) - o(updated.dom.style.backgroundColor).equals("green") + o(updated.d.style.backgroundColor).equals("green") }) o("updates style from object to string", function() { var vnode = m("a", {style: {backgroundColor: "red"}}) var updated = m("a", {style: "background-color:green;"}) - m.render(root, vnode) - m.render(root, updated) + m.render(G.root, vnode) + m.render(G.root, updated) - o(updated.dom.style.backgroundColor).equals("green") + o(updated.d.style.backgroundColor).equals("green") }) o("handles noop style change when style is string", function() { var vnode = m("a", {style: "background-color:green;"}) var updated = m("a", {style: "background-color:green;"}) - m.render(root, vnode) - m.render(root, updated) + m.render(G.root, vnode) + m.render(G.root, updated) - o(updated.dom.style.backgroundColor).equals("green") + o(updated.d.style.backgroundColor).equals("green") }) o("handles noop style change when style is object", function() { var vnode = m("a", {style: {backgroundColor: "red"}}) var updated = m("a", {style: {backgroundColor: "red"}}) - m.render(root, vnode) - m.render(root, updated) + m.render(G.root, vnode) + m.render(G.root, updated) - o(updated.dom.style.backgroundColor).equals("red") + o(updated.d.style.backgroundColor).equals("red") }) o("updates style from string to object", function() { var vnode = m("a", {style: "background-color:red;"}) var updated = m("a", {style: {backgroundColor: "green"}}) - m.render(root, vnode) - m.render(root, updated) + m.render(G.root, vnode) + m.render(G.root, updated) - o(updated.dom.style.backgroundColor).equals("green") + o(updated.d.style.backgroundColor).equals("green") }) o("updates style from string to string", function() { var vnode = m("a", {style: "background-color:red;"}) var updated = m("a", {style: "background-color:green;"}) - m.render(root, vnode) - m.render(root, updated) + m.render(G.root, vnode) + m.render(G.root, updated) - o(updated.dom.style.backgroundColor).equals("green") + o(updated.d.style.backgroundColor).equals("green") }) o("removes style from object to object", function() { var vnode = m("a", {style: {backgroundColor: "red", border: "1px solid red"}}) var updated = m("a", {style: {backgroundColor: "red"}}) - m.render(root, vnode) - m.render(root, updated) + m.render(G.root, vnode) + m.render(G.root, updated) - o(updated.dom.style.backgroundColor).equals("red") - o(updated.dom.style.border).equals("") + o(updated.d.style.backgroundColor).equals("red") + o(updated.d.style.border).equals("") }) o("removes style from string to object", function() { var vnode = m("a", {style: "background-color:red;border:1px solid red"}) var updated = m("a", {style: {backgroundColor: "red"}}) - m.render(root, vnode) - m.render(root, updated) + m.render(G.root, vnode) + m.render(G.root, updated) - o(updated.dom.style.backgroundColor).equals("red") - o(updated.dom.style.border).notEquals("1px solid red") + o(updated.d.style.backgroundColor).equals("red") + o(updated.d.style.border).notEquals("1px solid red") }) o("removes style from object to string", function() { var vnode = m("a", {style: {backgroundColor: "red", border: "1px solid red"}}) var updated = m("a", {style: "background-color:red"}) - m.render(root, vnode) - m.render(root, updated) + m.render(G.root, vnode) + m.render(G.root, updated) - o(updated.dom.style.backgroundColor).equals("red") - o(updated.dom.style.border).equals("") + o(updated.d.style.backgroundColor).equals("red") + o(updated.d.style.border).equals("") }) o("removes style from string to string", function() { var vnode = m("a", {style: "background-color:red;border:1px solid red"}) var updated = m("a", {style: "background-color:red"}) - m.render(root, vnode) - m.render(root, updated) + m.render(G.root, vnode) + m.render(G.root, updated) - o(updated.dom.style.backgroundColor).equals("red") - o(updated.dom.style.border).equals("") + o(updated.d.style.backgroundColor).equals("red") + o(updated.d.style.border).equals("") }) o("does not re-render element styles for equivalent style objects", function() { var style = {color: "gold"} var vnode = m("a", {style: style}) - m.render(root, vnode) + m.render(G.root, vnode) - root.firstChild.style.color = "red" + G.root.firstChild.style.color = "red" style = {color: "gold"} var updated = m("a", {style: style}) - m.render(root, updated) + m.render(G.root, updated) - o(updated.dom.style.color).equals("red") + o(updated.d.style.color).equals("red") }) o("setting style to `null` removes all styles", function() { var vnode = m("p", {style: "background-color: red"}) var updated = m("p", {style: null}) - m.render(root, vnode) + m.render(G.root, vnode) - o("style" in vnode.dom.attributes).equals(true) - o(vnode.dom.attributes.style.value).equals("background-color: red;") + o("style" in vnode.d.attributes).equals(true) + o(vnode.d.attributes.style.value).equals("background-color: red;") - m.render(root, updated) + m.render(G.root, updated) //browsers disagree here try { - o(updated.dom.attributes.style.value).equals("") + o(updated.d.attributes.style.value).equals("") } catch (e) { - o("style" in updated.dom.attributes).equals(false) + o("style" in updated.d.attributes).equals(false) } }) @@ -214,21 +211,21 @@ o.spec("updateElement", function() { var vnode = m("p", {style: "background-color: red"}) var updated = m("p", {style: undefined}) - m.render(root, vnode) + m.render(G.root, vnode) - o("style" in vnode.dom.attributes).equals(true) - o(vnode.dom.attributes.style.value).equals("background-color: red;") + o("style" in vnode.d.attributes).equals(true) + o(vnode.d.attributes.style.value).equals("background-color: red;") - m.render(root, updated) + m.render(G.root, updated) //browsers disagree here try { - o(updated.dom.attributes.style.value).equals("") + o(updated.d.attributes.style.value).equals("") } catch (e) { - o("style" in updated.dom.attributes).equals(false) + o("style" in updated.d.attributes).equals(false) } }) @@ -236,21 +233,21 @@ o.spec("updateElement", function() { var vnode = m("p", {style: "background-color: red"}) var updated = m("p") - m.render(root, vnode) + m.render(G.root, vnode) - o("style" in vnode.dom.attributes).equals(true) - o(vnode.dom.attributes.style.value).equals("background-color: red;") + o("style" in vnode.d.attributes).equals(true) + o(vnode.d.attributes.style.value).equals("background-color: red;") - m.render(root, updated) + m.render(G.root, updated) //browsers disagree here try { - o(updated.dom.attributes.style.value).equals("") + o(updated.d.attributes.style.value).equals("") } catch (e) { - o("style" in updated.dom.attributes).equals(false) + o("style" in updated.d.attributes).equals(false) } }) @@ -258,28 +255,28 @@ o.spec("updateElement", function() { var vnode = m("a") var updated = m("b") - m.render(root, vnode) - m.render(root, updated) + m.render(G.root, vnode) + m.render(G.root, updated) - o(updated.dom).equals(root.firstChild) - o(updated.dom.nodeName).equals("B") + o(updated.d).equals(G.root.firstChild) + o(updated.d.nodeName).equals("B") }) o("updates svg class", function() { var vnode = m("svg", {className: "a"}) var updated = m("svg", {className: "b"}) - m.render(root, vnode) - m.render(root, updated) + m.render(G.root, vnode) + m.render(G.root, updated) - o(updated.dom.attributes["class"].value).equals("b") + o(updated.d.attributes["class"].value).equals("b") }) o("updates svg child", function() { var vnode = m("svg", m("circle")) var updated = m("svg", m("line")) - m.render(root, vnode) - m.render(root, updated) + m.render(G.root, vnode) + m.render(G.root, updated) - o(updated.dom.firstChild.namespaceURI).equals("http://www.w3.org/2000/svg") + o(updated.d.firstChild.namespaceURI).equals("http://www.w3.org/2000/svg") }) }) diff --git a/tests/render/updateFragment.js b/tests/render/updateFragment.js index 73220867d..daf4ab9b7 100644 --- a/tests/render/updateFragment.js +++ b/tests/render/updateFragment.js @@ -1,63 +1,60 @@ import o from "ospec" -import domMock from "../../test-utils/domMock.js" +import {setupGlobals} from "../../test-utils/global.js" + import m from "../../src/entry/mithril.esm.js" o.spec("updateFragment", function() { - var $window, root - o.beforeEach(function() { - $window = domMock() - root = $window.document.createElement("div") - }) + var G = setupGlobals() o("updates fragment", function() { var vnode = [m("a")] var updated = [m("b")] - m.render(root, vnode) - m.render(root, updated) + m.render(G.root, vnode) + m.render(G.root, updated) - o(updated[0].dom).equals(root.firstChild) - o(updated[0].dom.nodeName).equals("B") + o(updated[0].d).equals(G.root.firstChild) + o(updated[0].d.nodeName).equals("B") }) o("adds els", function() { var vnode = [] var updated = [m("a"), m("b")] - m.render(root, vnode) - m.render(root, updated) + m.render(G.root, vnode) + m.render(G.root, updated) - o(updated[0].dom).equals(root.firstChild) - o(root.childNodes.length).equals(2) - o(root.childNodes[0].nodeName).equals("A") - o(root.childNodes[1].nodeName).equals("B") + o(updated[0].d).equals(G.root.firstChild) + o(G.root.childNodes.length).equals(2) + o(G.root.childNodes[0].nodeName).equals("A") + o(G.root.childNodes[1].nodeName).equals("B") }) o("removes els", function() { var vnode = [m("a"), m("b")] var updated = [] - m.render(root, vnode) - m.render(root, updated) + m.render(G.root, vnode) + m.render(G.root, updated) - o(root.childNodes.length).equals(0) + o(G.root.childNodes.length).equals(0) }) o("updates from childless fragment", function() { var vnode = [] var updated = [m("a")] - m.render(root, vnode) - m.render(root, updated) + m.render(G.root, vnode) + m.render(G.root, updated) - o(updated[0].dom).equals(root.firstChild) - o(updated[0].dom.nodeName).equals("A") + o(updated[0].d).equals(G.root.firstChild) + o(updated[0].d.nodeName).equals("A") }) o("updates to childless fragment", function() { var vnode = [m("a")] var updated = [] - m.render(root, vnode) - m.render(root, updated) + m.render(G.root, vnode) + m.render(G.root, updated) - o(root.childNodes.length).equals(0) + o(G.root.childNodes.length).equals(0) }) }) diff --git a/tests/render/updateNodes.js b/tests/render/updateNodes.js index 5178d6a25..0cd92bcfe 100644 --- a/tests/render/updateNodes.js +++ b/tests/render/updateNodes.js @@ -1,6 +1,7 @@ import o from "ospec" -import domMock from "../../test-utils/domMock.js" +import {setupGlobals} from "../../test-utils/global.js" + import m from "../../src/entry/mithril.esm.js" function vnodify(str) { @@ -8,498 +9,512 @@ function vnodify(str) { } o.spec("updateNodes", function() { - var $window, root - o.beforeEach(function() { - $window = domMock() - root = $window.document.createElement("div") - }) + var G = setupGlobals() o("handles el noop", function() { var vnodes = [m.key(1, m("a")), m.key(2, m("b"))] var updated = [m.key(1, m("a")), m.key(2, m("b"))] - m.render(root, vnodes) - m.render(root, updated) + m.render(G.root, vnodes) + m.render(G.root, updated) - o(Array.from(root.childNodes, (n) => n.nodeName)).deepEquals(["A", "B"]) - o(updated[0].children[0].dom).equals(root.childNodes[0]) - o(updated[1].children[0].dom).equals(root.childNodes[1]) + o(Array.from(G.root.childNodes, (n) => n.nodeName)).deepEquals(["A", "B"]) + o(updated[0].c[0].d).equals(G.root.childNodes[0]) + o(updated[1].c[0].d).equals(G.root.childNodes[1]) }) o("handles el noop without key", function() { var vnodes = [m("a"), m("b")] var updated = [m("a"), m("b")] - m.render(root, vnodes) - m.render(root, updated) + m.render(G.root, vnodes) + m.render(G.root, updated) - o(Array.from(root.childNodes, (n) => n.nodeName)).deepEquals(["A", "B"]) - o(updated[0].dom).equals(root.childNodes[0]) - o(updated[1].dom).equals(root.childNodes[1]) + o(Array.from(G.root.childNodes, (n) => n.nodeName)).deepEquals(["A", "B"]) + o(updated[0].d).equals(G.root.childNodes[0]) + o(updated[1].d).equals(G.root.childNodes[1]) }) o("handles text noop", function() { var vnodes = "a" var updated = "a" - m.render(root, vnodes) - m.render(root, updated) + m.render(G.root, vnodes) + m.render(G.root, updated) - o(Array.from(root.childNodes, (n) => n.nodeValue)).deepEquals(["a"]) + o(Array.from(G.root.childNodes, (n) => n.nodeValue)).deepEquals(["a"]) }) o("handles text noop w/ type casting", function() { var vnodes = 1 var updated = "1" - m.render(root, vnodes) - m.render(root, updated) + m.render(G.root, vnodes) + m.render(G.root, updated) - o(Array.from(root.childNodes, (n) => n.nodeValue)).deepEquals(["1"]) + o(Array.from(G.root.childNodes, (n) => n.nodeValue)).deepEquals(["1"]) }) o("handles falsy text noop w/ type casting", function() { var vnodes = 0 var updated = "0" - m.render(root, vnodes) - m.render(root, updated) + m.render(G.root, vnodes) + m.render(G.root, updated) - o(Array.from(root.childNodes, (n) => n.nodeValue)).deepEquals(["0"]) + o(Array.from(G.root.childNodes, (n) => n.nodeValue)).deepEquals(["0"]) }) o("handles fragment noop", function() { var vnodes = [m("a")] var updated = [m("a")] - m.render(root, vnodes) - m.render(root, updated) + m.render(G.root, vnodes) + m.render(G.root, updated) - o(Array.from(root.childNodes, (n) => n.nodeName)).deepEquals(["A"]) - o(updated[0].dom).equals(root.childNodes[0]) + o(Array.from(G.root.childNodes, (n) => n.nodeName)).deepEquals(["A"]) + o(updated[0].d).equals(G.root.childNodes[0]) }) o("handles fragment noop w/ text child", function() { var vnodes = [m.normalize("a")] var updated = [m.normalize("a")] - m.render(root, vnodes) - m.render(root, updated) + m.render(G.root, vnodes) + m.render(G.root, updated) - o(Array.from(root.childNodes, (n) => n.nodeValue)).deepEquals(["a"]) - o(updated[0].dom).equals(root.childNodes[0]) + o(Array.from(G.root.childNodes, (n) => n.nodeValue)).deepEquals(["a"]) + o(updated[0].d).equals(G.root.childNodes[0]) }) o("handles undefined to null noop", function() { var vnodes = [null, m("div")] var updated = [undefined, m("div")] - m.render(root, vnodes) - m.render(root, updated) + m.render(G.root, vnodes) + m.render(G.root, updated) - o(root.childNodes.length).equals(1) + o(G.root.childNodes.length).equals(1) }) o("reverses els w/ even count", function() { var vnodes = [m.key(1, m("a")), m.key(2, m("b")), m.key(3, m("i")), m.key(4, m("s"))] var updated = [m.key(4, m("s")), m.key(3, m("i")), m.key(2, m("b")), m.key(1, m("a"))] - m.render(root, vnodes) - m.render(root, updated) + m.render(G.root, vnodes) + m.render(G.root, updated) - o(Array.from(root.childNodes, (n) => n.nodeName)).deepEquals(["S", "I", "B", "A"]) - o(updated[0].children[0].dom).equals(root.childNodes[0]) - o(updated[1].children[0].dom).equals(root.childNodes[1]) - o(updated[2].children[0].dom).equals(root.childNodes[2]) - o(updated[3].children[0].dom).equals(root.childNodes[3]) + o(Array.from(G.root.childNodes, (n) => n.nodeName)).deepEquals(["S", "I", "B", "A"]) + o(updated[0].c[0].d).equals(G.root.childNodes[0]) + o(updated[1].c[0].d).equals(G.root.childNodes[1]) + o(updated[2].c[0].d).equals(G.root.childNodes[2]) + o(updated[3].c[0].d).equals(G.root.childNodes[3]) }) o("reverses els w/ odd count", function() { var vnodes = [m.key(1, m("a")), m.key(2, m("b")), m.key(3, m("i"))] var updated = [m.key(3, m("i")), m.key(2, m("b")), m.key(1, m("a"))] - m.render(root, vnodes) - m.render(root, updated) + m.render(G.root, vnodes) + m.render(G.root, updated) - o(Array.from(root.childNodes, (n) => n.nodeName)).deepEquals(["I", "B", "A"]) + o(Array.from(G.root.childNodes, (n) => n.nodeName)).deepEquals(["I", "B", "A"]) }) o("creates el at start", function() { var vnodes = [m.key(1, m("a"))] var updated = [m.key(2, m("b")), m.key(1, m("a"))] - m.render(root, vnodes) - m.render(root, updated) + m.render(G.root, vnodes) + m.render(G.root, updated) - o(Array.from(root.childNodes, (n) => n.nodeName)).deepEquals(["B", "A"]) - o(updated[0].children[0].dom).equals(root.childNodes[0]) - o(updated[1].children[0].dom).equals(root.childNodes[1]) + o(Array.from(G.root.childNodes, (n) => n.nodeName)).deepEquals(["B", "A"]) + o(updated[0].c[0].d).equals(G.root.childNodes[0]) + o(updated[1].c[0].d).equals(G.root.childNodes[1]) }) o("creates el at end", function() { var vnodes = [m.key(1, m("a"))] var updated = [m.key(1, m("a")), m.key(2, m("b"))] - m.render(root, vnodes) - m.render(root, updated) + m.render(G.root, vnodes) + m.render(G.root, updated) - o(Array.from(root.childNodes, (n) => n.nodeName)).deepEquals(["A", "B"]) - o(updated[0].children[0].dom).equals(root.childNodes[0]) - o(updated[1].children[0].dom).equals(root.childNodes[1]) + o(Array.from(G.root.childNodes, (n) => n.nodeName)).deepEquals(["A", "B"]) + o(updated[0].c[0].d).equals(G.root.childNodes[0]) + o(updated[1].c[0].d).equals(G.root.childNodes[1]) }) o("creates el in middle", function() { var vnodes = [m.key(1, m("a")), m.key(2, m("b"))] var updated = [m.key(1, m("a")), m.key(3, m("i")), m.key(2, m("b"))] - m.render(root, vnodes) - m.render(root, updated) + m.render(G.root, vnodes) + m.render(G.root, updated) - o(Array.from(root.childNodes, (n) => n.nodeName)).deepEquals(["A", "I", "B"]) - o(updated[0].children[0].dom).equals(root.childNodes[0]) - o(updated[1].children[0].dom).equals(root.childNodes[1]) - o(updated[2].children[0].dom).equals(root.childNodes[2]) + o(Array.from(G.root.childNodes, (n) => n.nodeName)).deepEquals(["A", "I", "B"]) + o(updated[0].c[0].d).equals(G.root.childNodes[0]) + o(updated[1].c[0].d).equals(G.root.childNodes[1]) + o(updated[2].c[0].d).equals(G.root.childNodes[2]) }) o("creates el while reversing", function() { var vnodes = [m.key(1, m("a")), m.key(2, m("b"))] var updated = [m.key(2, m("b")), m.key(3, m("i")), m.key(1, m("a"))] - m.render(root, vnodes) - m.render(root, updated) + m.render(G.root, vnodes) + m.render(G.root, updated) - o(Array.from(root.childNodes, (n) => n.nodeName)).deepEquals(["B", "I", "A"]) - o(updated[0].children[0].dom).equals(root.childNodes[0]) - o(updated[1].children[0].dom).equals(root.childNodes[1]) - o(updated[2].children[0].dom).equals(root.childNodes[2]) + o(Array.from(G.root.childNodes, (n) => n.nodeName)).deepEquals(["B", "I", "A"]) + o(updated[0].c[0].d).equals(G.root.childNodes[0]) + o(updated[1].c[0].d).equals(G.root.childNodes[1]) + o(updated[2].c[0].d).equals(G.root.childNodes[2]) }) o("deletes el at start", function() { var vnodes = [m.key(2, m("b")), m.key(1, m("a"))] var updated = [m.key(1, m("a"))] - m.render(root, vnodes) - m.render(root, updated) + m.render(G.root, vnodes) + m.render(G.root, updated) - o(Array.from(root.childNodes, (n) => n.nodeName)).deepEquals(["A"]) - o(updated[0].children[0].dom).equals(root.childNodes[0]) + o(Array.from(G.root.childNodes, (n) => n.nodeName)).deepEquals(["A"]) + o(updated[0].c[0].d).equals(G.root.childNodes[0]) }) o("deletes el at end", function() { var vnodes = [m.key(1, m("a")), m.key(2, m("b"))] var updated = [m.key(1, m("a"))] - m.render(root, vnodes) - m.render(root, updated) + m.render(G.root, vnodes) + m.render(G.root, updated) - o(Array.from(root.childNodes, (n) => n.nodeName)).deepEquals(["A"]) - o(updated[0].children[0].dom).equals(root.childNodes[0]) + o(Array.from(G.root.childNodes, (n) => n.nodeName)).deepEquals(["A"]) + o(updated[0].c[0].d).equals(G.root.childNodes[0]) }) o("deletes el at middle", function() { var vnodes = [m.key(1, m("a")), m.key(3, m("i")), m.key(2, m("b"))] var updated = [m.key(1, m("a")), m.key(2, m("b"))] - m.render(root, vnodes) - m.render(root, updated) + m.render(G.root, vnodes) + m.render(G.root, updated) - o(Array.from(root.childNodes, (n) => n.nodeName)).deepEquals(["A", "B"]) - o(updated[0].children[0].dom).equals(root.childNodes[0]) - o(updated[1].children[0].dom).equals(root.childNodes[1]) + o(Array.from(G.root.childNodes, (n) => n.nodeName)).deepEquals(["A", "B"]) + o(updated[0].c[0].d).equals(G.root.childNodes[0]) + o(updated[1].c[0].d).equals(G.root.childNodes[1]) }) o("deletes el while reversing", function() { var vnodes = [m.key(1, m("a")), m.key(3, m("i")), m.key(2, m("b"))] var updated = [m.key(2, m("b")), m.key(1, m("a"))] - m.render(root, vnodes) - m.render(root, updated) + m.render(G.root, vnodes) + m.render(G.root, updated) - o(Array.from(root.childNodes, (n) => n.nodeName)).deepEquals(["B", "A"]) - o(updated[0].children[0].dom).equals(root.childNodes[0]) - o(updated[1].children[0].dom).equals(root.childNodes[1]) + o(Array.from(G.root.childNodes, (n) => n.nodeName)).deepEquals(["B", "A"]) + o(updated[0].c[0].d).equals(G.root.childNodes[0]) + o(updated[1].c[0].d).equals(G.root.childNodes[1]) }) o("creates, deletes, reverses els at same time", function() { var vnodes = [m.key(1, m("a")), m.key(3, m("i")), m.key(2, m("b"))] var updated = [m.key(2, m("b")), m.key(1, m("a")), m.key(4, m("s"))] - m.render(root, vnodes) - m.render(root, updated) + m.render(G.root, vnodes) + m.render(G.root, updated) - o(Array.from(root.childNodes, (n) => n.nodeName)).deepEquals(["B", "A", "S"]) - o(updated[0].children[0].dom).equals(root.childNodes[0]) - o(updated[1].children[0].dom).equals(root.childNodes[1]) - o(updated[2].children[0].dom).equals(root.childNodes[2]) + o(Array.from(G.root.childNodes, (n) => n.nodeName)).deepEquals(["B", "A", "S"]) + o(updated[0].c[0].d).equals(G.root.childNodes[0]) + o(updated[1].c[0].d).equals(G.root.childNodes[1]) + o(updated[2].c[0].d).equals(G.root.childNodes[2]) }) o("creates, deletes, reverses els at same time with '__proto__' key", function() { var vnodes = [m.key("__proto__", m("a")), m.key(3, m("i")), m.key(2, m("b"))] var updated = [m.key(2, m("b")), m.key("__proto__", m("a")), m.key(4, m("s"))] - m.render(root, vnodes) - m.render(root, updated) + m.render(G.root, vnodes) + m.render(G.root, updated) - o(Array.from(root.childNodes, (n) => n.nodeName)).deepEquals(["B", "A", "S"]) - o(updated[0].children[0].dom).equals(root.childNodes[0]) - o(updated[1].children[0].dom).equals(root.childNodes[1]) - o(updated[2].children[0].dom).equals(root.childNodes[2]) + o(Array.from(G.root.childNodes, (n) => n.nodeName)).deepEquals(["B", "A", "S"]) + o(updated[0].c[0].d).equals(G.root.childNodes[0]) + o(updated[1].c[0].d).equals(G.root.childNodes[1]) + o(updated[2].c[0].d).equals(G.root.childNodes[2]) }) o("adds to empty fragment followed by el", function() { var vnodes = [m.key(1), m.key(2, m("b"))] var updated = [m.key(1, m("a")), m.key(2, m("b"))] - m.render(root, vnodes) - m.render(root, updated) + m.render(G.root, vnodes) + m.render(G.root, updated) - o(Array.from(root.childNodes, (n) => n.nodeName)).deepEquals(["A", "B"]) - o(updated[0].children[0].dom).equals(root.childNodes[0]) - o(updated[1].children[0].dom).equals(root.childNodes[1]) + o(Array.from(G.root.childNodes, (n) => n.nodeName)).deepEquals(["A", "B"]) + o(updated[0].c[0].d).equals(G.root.childNodes[0]) + o(updated[1].c[0].d).equals(G.root.childNodes[1]) }) o("reverses followed by el", function() { var vnodes = [m.key(1, m.key(2, m("a")), m.key(3, m("b"))), m.key(4, m("i"))] var updated = [m.key(1, m.key(3, m("b")), m.key(2, m("a"))), m.key(4, m("i"))] - m.render(root, vnodes) - m.render(root, updated) + m.render(G.root, vnodes) + m.render(G.root, updated) - o(Array.from(root.childNodes, (n) => n.nodeName)).deepEquals(["B", "A", "I"]) - o(updated[0].children[0].children[0].dom).equals(root.childNodes[0]) - o(updated[0].children[1].children[0].dom).equals(root.childNodes[1]) - o(updated[1].children[0].dom).equals(root.childNodes[2]) + o(Array.from(G.root.childNodes, (n) => n.nodeName)).deepEquals(["B", "A", "I"]) + o(updated[0].c[0].c[0].d).equals(G.root.childNodes[0]) + o(updated[0].c[1].c[0].d).equals(G.root.childNodes[1]) + o(updated[1].c[0].d).equals(G.root.childNodes[2]) }) o("populates fragment followed by el keyed", function() { var vnodes = [m.key(1), m.key(2, m("i"))] var updated = [m.key(1, m("a"), m("b")), m.key(2, m("i"))] - m.render(root, vnodes) - m.render(root, updated) + m.render(G.root, vnodes) + m.render(G.root, updated) - o(Array.from(root.childNodes, (n) => n.nodeName)).deepEquals(["A", "B", "I"]) - o(updated[0].children[0].dom).equals(root.childNodes[0]) - o(updated[0].children[1].dom).equals(root.childNodes[1]) - o(updated[1].children[0].dom).equals(root.childNodes[2]) + o(Array.from(G.root.childNodes, (n) => n.nodeName)).deepEquals(["A", "B", "I"]) + o(updated[0].c[0].d).equals(G.root.childNodes[0]) + o(updated[0].c[1].d).equals(G.root.childNodes[1]) + o(updated[1].c[0].d).equals(G.root.childNodes[2]) }) o("throws if fragment followed by null then el on first render keyed", function() { var vnodes = [m.key(1), null, m.key(2, m("i"))] - o(() => m.render(root, vnodes)).throws(TypeError) + o(() => m.render(G.root, vnodes)).throws(TypeError) }) o("throws if fragment followed by null then el on next render keyed", function() { var vnodes = [m.key(1), m.key(2, m("i"))] var updated = [m.key(1, m("a"), m("b")), null, m.key(2, m("i"))] - m.render(root, vnodes) - o(() => m.render(root, updated)).throws(TypeError) + m.render(G.root, vnodes) + o(() => m.render(G.root, updated)).throws(TypeError) }) o("populates childless fragment replaced followed by el keyed", function() { var vnodes = [m.key(1), m.key(2, m("i"))] var updated = [m.key(1, m("a"), m("b")), m.key(2, m("i"))] - m.render(root, vnodes) - m.render(root, updated) + m.render(G.root, vnodes) + m.render(G.root, updated) - o(Array.from(root.childNodes, (n) => n.nodeName)).deepEquals(["A", "B", "I"]) - o(updated[0].children[0].dom).equals(root.childNodes[0]) - o(updated[0].children[1].dom).equals(root.childNodes[1]) - o(updated[1].children[0].dom).equals(root.childNodes[2]) + o(Array.from(G.root.childNodes, (n) => n.nodeName)).deepEquals(["A", "B", "I"]) + o(updated[0].c[0].d).equals(G.root.childNodes[0]) + o(updated[0].c[1].d).equals(G.root.childNodes[1]) + o(updated[1].c[0].d).equals(G.root.childNodes[2]) }) o("throws if childless fragment replaced followed by null then el keyed", function() { var vnodes = [m.key(1), m.key(2, m("i"))] var updated = [m.key(1, m("a"), m("b")), null, m.key(2, m("i"))] - m.render(root, vnodes) - o(() => m.render(root, updated)).throws(TypeError) + m.render(G.root, vnodes) + o(() => m.render(G.root, updated)).throws(TypeError) }) o("moves from end to start", function() { var vnodes = [m.key(1, m("a")), m.key(2, m("b")), m.key(3, m("i")), m.key(4, m("s"))] var updated = [m.key(4, m("s")), m.key(1, m("a")), m.key(2, m("b")), m.key(3, m("i"))] - m.render(root, vnodes) - m.render(root, updated) + m.render(G.root, vnodes) + m.render(G.root, updated) - o(Array.from(root.childNodes, (n) => n.nodeName)).deepEquals(["S", "A", "B", "I"]) - o(updated[0].children[0].dom).equals(root.childNodes[0]) - o(updated[1].children[0].dom).equals(root.childNodes[1]) - o(updated[2].children[0].dom).equals(root.childNodes[2]) - o(updated[3].children[0].dom).equals(root.childNodes[3]) + o(Array.from(G.root.childNodes, (n) => n.nodeName)).deepEquals(["S", "A", "B", "I"]) + o(updated[0].c[0].d).equals(G.root.childNodes[0]) + o(updated[1].c[0].d).equals(G.root.childNodes[1]) + o(updated[2].c[0].d).equals(G.root.childNodes[2]) + o(updated[3].c[0].d).equals(G.root.childNodes[3]) }) o("moves from start to end", function() { var vnodes = [m.key(1, m("a")), m.key(2, m("b")), m.key(3, m("i")), m.key(4, m("s"))] var updated = [m.key(2, m("b")), m.key(3, m("i")), m.key(4, m("s")), m.key(1, m("a"))] - m.render(root, vnodes) - m.render(root, updated) + m.render(G.root, vnodes) + m.render(G.root, updated) - o(Array.from(root.childNodes, (n) => n.nodeName)).deepEquals(["B", "I", "S", "A"]) - o(updated[0].children[0].dom).equals(root.childNodes[0]) - o(updated[1].children[0].dom).equals(root.childNodes[1]) - o(updated[2].children[0].dom).equals(root.childNodes[2]) - o(updated[3].children[0].dom).equals(root.childNodes[3]) + o(Array.from(G.root.childNodes, (n) => n.nodeName)).deepEquals(["B", "I", "S", "A"]) + o(updated[0].c[0].d).equals(G.root.childNodes[0]) + o(updated[1].c[0].d).equals(G.root.childNodes[1]) + o(updated[2].c[0].d).equals(G.root.childNodes[2]) + o(updated[3].c[0].d).equals(G.root.childNodes[3]) }) o("removes then recreate", function() { var vnodes = [m.key(1, m("a")), m.key(2, m("b")), m.key(3, m("i")), m.key(4, m("s"))] var temp = [] var updated = [m.key(1, m("a")), m.key(2, m("b")), m.key(3, m("i")), m.key(4, m("s"))] - m.render(root, vnodes) - m.render(root, temp) - m.render(root, updated) + m.render(G.root, vnodes) + m.render(G.root, temp) + m.render(G.root, updated) - o(Array.from(root.childNodes, (n) => n.nodeName)).deepEquals(["A", "B", "I", "S"]) - o(updated[0].children[0].dom).equals(root.childNodes[0]) - o(updated[1].children[0].dom).equals(root.childNodes[1]) - o(updated[2].children[0].dom).equals(root.childNodes[2]) - o(updated[3].children[0].dom).equals(root.childNodes[3]) + o(Array.from(G.root.childNodes, (n) => n.nodeName)).deepEquals(["A", "B", "I", "S"]) + o(updated[0].c[0].d).equals(G.root.childNodes[0]) + o(updated[1].c[0].d).equals(G.root.childNodes[1]) + o(updated[2].c[0].d).equals(G.root.childNodes[2]) + o(updated[3].c[0].d).equals(G.root.childNodes[3]) }) o("removes then recreate reversed", function() { var vnodes = [m.key(1, m("a")), m.key(2, m("b")), m.key(3, m("i")), m.key(4, m("s"))] var temp = [] var updated = [m.key(4, m("s")), m.key(3, m("i")), m.key(2, m("b")), m.key(1, m("a"))] - m.render(root, vnodes) - m.render(root, temp) - m.render(root, updated) + m.render(G.root, vnodes) + m.render(G.root, temp) + m.render(G.root, updated) - o(Array.from(root.childNodes, (n) => n.nodeName)).deepEquals(["S", "I", "B", "A"]) - o(updated[0].children[0].dom).equals(root.childNodes[0]) - o(updated[1].children[0].dom).equals(root.childNodes[1]) - o(updated[2].children[0].dom).equals(root.childNodes[2]) - o(updated[3].children[0].dom).equals(root.childNodes[3]) + o(Array.from(G.root.childNodes, (n) => n.nodeName)).deepEquals(["S", "I", "B", "A"]) + o(updated[0].c[0].d).equals(G.root.childNodes[0]) + o(updated[1].c[0].d).equals(G.root.childNodes[1]) + o(updated[2].c[0].d).equals(G.root.childNodes[2]) + o(updated[3].c[0].d).equals(G.root.childNodes[3]) }) o("removes then recreate smaller", function() { var vnodes = [m.key(1, m("a")), m.key(2, m("b"))] var temp = [] var updated = [m.key(1, m("a"))] - m.render(root, vnodes) - m.render(root, temp) - m.render(root, updated) + m.render(G.root, vnodes) + m.render(G.root, temp) + m.render(G.root, updated) - o(Array.from(root.childNodes, (n) => n.nodeName)).deepEquals(["A"]) - o(updated[0].children[0].dom).equals(root.childNodes[0]) + o(Array.from(G.root.childNodes, (n) => n.nodeName)).deepEquals(["A"]) + o(updated[0].c[0].d).equals(G.root.childNodes[0]) }) o("removes then recreate bigger", function() { var vnodes = [m.key(1, m("a")), m.key(2, m("b"))] var temp = [] var updated = [m.key(1, m("a")), m.key(2, m("b")), m.key(3, m("i"))] - m.render(root, vnodes) - m.render(root, temp) - m.render(root, updated) + m.render(G.root, vnodes) + m.render(G.root, temp) + m.render(G.root, updated) - o(Array.from(root.childNodes, (n) => n.nodeName)).deepEquals(["A", "B", "I"]) - o(updated[0].children[0].dom).equals(root.childNodes[0]) - o(updated[1].children[0].dom).equals(root.childNodes[1]) - o(updated[2].children[0].dom).equals(root.childNodes[2]) + o(Array.from(G.root.childNodes, (n) => n.nodeName)).deepEquals(["A", "B", "I"]) + o(updated[0].c[0].d).equals(G.root.childNodes[0]) + o(updated[1].c[0].d).equals(G.root.childNodes[1]) + o(updated[2].c[0].d).equals(G.root.childNodes[2]) }) o("removes then create different", function() { var vnodes = [m.key(1, m("a")), m.key(2, m("b"))] var temp = [] var updated = [m.key(3, m("i")), m.key(4, m("s"))] - m.render(root, vnodes) - m.render(root, temp) - m.render(root, updated) + m.render(G.root, vnodes) + m.render(G.root, temp) + m.render(G.root, updated) - o(Array.from(root.childNodes, (n) => n.nodeName)).deepEquals(["I", "S"]) - o(updated[0].children[0].dom).equals(root.childNodes[0]) - o(updated[1].children[0].dom).equals(root.childNodes[1]) + o(Array.from(G.root.childNodes, (n) => n.nodeName)).deepEquals(["I", "S"]) + o(updated[0].c[0].d).equals(G.root.childNodes[0]) + o(updated[1].c[0].d).equals(G.root.childNodes[1]) }) o("removes then create different smaller", function() { var vnodes = [m.key(1, m("a")), m.key(2, m("b"))] var temp = [] var updated = [m.key(3, m("i"))] - m.render(root, vnodes) - m.render(root, temp) - m.render(root, updated) + m.render(G.root, vnodes) + m.render(G.root, temp) + m.render(G.root, updated) - o(Array.from(root.childNodes, (n) => n.nodeName)).deepEquals(["I"]) - o(updated[0].children[0].dom).equals(root.childNodes[0]) + o(Array.from(G.root.childNodes, (n) => n.nodeName)).deepEquals(["I"]) + o(updated[0].c[0].d).equals(G.root.childNodes[0]) }) o("removes then create different bigger", function() { var vnodes = [m.key(1, m("a")), m.key(2, m("b"))] var temp = [] var updated = [m.key(3, m("i")), m.key(4, m("s")), m.key(5, m("div"))] - m.render(root, vnodes) - m.render(root, temp) - m.render(root, updated) + m.render(G.root, vnodes) + m.render(G.root, temp) + m.render(G.root, updated) - o(Array.from(root.childNodes, (n) => n.nodeName)).deepEquals(["I", "S", "DIV"]) - o(updated[0].children[0].dom).equals(root.childNodes[0]) - o(updated[1].children[0].dom).equals(root.childNodes[1]) - o(updated[2].children[0].dom).equals(root.childNodes[2]) + o(Array.from(G.root.childNodes, (n) => n.nodeName)).deepEquals(["I", "S", "DIV"]) + o(updated[0].c[0].d).equals(G.root.childNodes[0]) + o(updated[1].c[0].d).equals(G.root.childNodes[1]) + o(updated[2].c[0].d).equals(G.root.childNodes[2]) }) o("removes then create mixed", function() { var vnodes = [m.key(1, m("a")), m.key(2, m("b"))] var temp = [] var updated = [m.key(1, m("a")), m.key(4, m("s"))] - m.render(root, vnodes) - m.render(root, temp) - m.render(root, updated) + m.render(G.root, vnodes) + m.render(G.root, temp) + m.render(G.root, updated) - o(Array.from(root.childNodes, (n) => n.nodeName)).deepEquals(["A", "S"]) - o(updated[0].children[0].dom).equals(root.childNodes[0]) - o(updated[1].children[0].dom).equals(root.childNodes[1]) + o(Array.from(G.root.childNodes, (n) => n.nodeName)).deepEquals(["A", "S"]) + o(updated[0].c[0].d).equals(G.root.childNodes[0]) + o(updated[1].c[0].d).equals(G.root.childNodes[1]) }) o("removes then create mixed reversed", function() { var vnodes = [m.key(1, m("a")), m.key(2, m("b"))] var temp = [] var updated = [m.key(4, m("s")), m.key(1, m("a"))] - m.render(root, vnodes) - m.render(root, temp) - m.render(root, updated) + m.render(G.root, vnodes) + m.render(G.root, temp) + m.render(G.root, updated) - o(Array.from(root.childNodes, (n) => n.nodeName)).deepEquals(["S", "A"]) - o(updated[0].children[0].dom).equals(root.childNodes[0]) - o(updated[1].children[0].dom).equals(root.childNodes[1]) + o(Array.from(G.root.childNodes, (n) => n.nodeName)).deepEquals(["S", "A"]) + o(updated[0].c[0].d).equals(G.root.childNodes[0]) + o(updated[1].c[0].d).equals(G.root.childNodes[1]) }) o("removes then create mixed smaller", function() { var vnodes = [m.key(1, m("a")), m.key(2, m("b")), m.key(3, m("i"))] var temp = [] var updated = [m.key(1, m("a")), m.key(4, m("s"))] - m.render(root, vnodes) - m.render(root, temp) - m.render(root, updated) + m.render(G.root, vnodes) + m.render(G.root, temp) + m.render(G.root, updated) - o(Array.from(root.childNodes, (n) => n.nodeName)).deepEquals(["A", "S"]) - o(updated[0].children[0].dom).equals(root.childNodes[0]) - o(updated[1].children[0].dom).equals(root.childNodes[1]) + o(Array.from(G.root.childNodes, (n) => n.nodeName)).deepEquals(["A", "S"]) + o(updated[0].c[0].d).equals(G.root.childNodes[0]) + o(updated[1].c[0].d).equals(G.root.childNodes[1]) }) o("removes then create mixed smaller reversed", function() { var vnodes = [m.key(1, m("a")), m.key(2, m("b")), m.key(3, m("i"))] var temp = [] var updated = [m.key(4, m("s")), m.key(1, m("a"))] - m.render(root, vnodes) - m.render(root, temp) - m.render(root, updated) + m.render(G.root, vnodes) + m.render(G.root, temp) + m.render(G.root, updated) - o(Array.from(root.childNodes, (n) => n.nodeName)).deepEquals(["S", "A"]) - o(updated[0].children[0].dom).equals(root.childNodes[0]) - o(updated[1].children[0].dom).equals(root.childNodes[1]) + o(Array.from(G.root.childNodes, (n) => n.nodeName)).deepEquals(["S", "A"]) + o(updated[0].c[0].d).equals(G.root.childNodes[0]) + o(updated[1].c[0].d).equals(G.root.childNodes[1]) }) o("removes then create mixed bigger", function() { var vnodes = [m.key(1, m("a")), m.key(2, m("b"))] var temp = [] var updated = [m.key(1, m("a")), m.key(3, m("i")), m.key(4, m("s"))] - m.render(root, vnodes) - m.render(root, temp) - m.render(root, updated) + m.render(G.root, vnodes) + m.render(G.root, temp) + m.render(G.root, updated) - o(Array.from(root.childNodes, (n) => n.nodeName)).deepEquals(["A", "I", "S"]) - o(updated[0].children[0].dom).equals(root.childNodes[0]) - o(updated[1].children[0].dom).equals(root.childNodes[1]) - o(updated[2].children[0].dom).equals(root.childNodes[2]) + o(Array.from(G.root.childNodes, (n) => n.nodeName)).deepEquals(["A", "I", "S"]) + o(updated[0].c[0].d).equals(G.root.childNodes[0]) + o(updated[1].c[0].d).equals(G.root.childNodes[1]) + o(updated[2].c[0].d).equals(G.root.childNodes[2]) }) o("removes then create mixed bigger reversed", function() { var vnodes = [m.key(1, m("a")), m.key(2, m("b"))] var temp = [] var updated = [m.key(4, m("s")), m.key(3, m("i")), m.key(1, m("a"))] - m.render(root, vnodes) - m.render(root, temp) - m.render(root, updated) + m.render(G.root, vnodes) + m.render(G.root, temp) + m.render(G.root, updated) + + o(Array.from(G.root.childNodes, (n) => n.nodeName)).deepEquals(["S", "I", "A"]) + o(updated[0].c[0].d).equals(G.root.childNodes[0]) + o(updated[1].c[0].d).equals(G.root.childNodes[1]) + o(updated[2].c[0].d).equals(G.root.childNodes[2]) + }) + o("in fragment, nest text inside fragment and add hole", function() { + var vnodes = ["a"] + var updated = [["b"], undefined] + + m.render(G.root, vnodes) + m.render(G.root, updated) + + o(G.root.childNodes.length).equals(1) + }) + o("in element, nest text inside fragment and add hole", function() { + var vnodes = m("div", "a") + var updated = m("div", ["b"], undefined) + + m.render(G.root, vnodes) + m.render(G.root, updated) - o(Array.from(root.childNodes, (n) => n.nodeName)).deepEquals(["S", "I", "A"]) - o(updated[0].children[0].dom).equals(root.childNodes[0]) - o(updated[1].children[0].dom).equals(root.childNodes[1]) - o(updated[2].children[0].dom).equals(root.childNodes[2]) + o(G.root.firstChild.childNodes.length).equals(1) }) o("change type, position and length", function() { - var vnodes = m("div", undefined, "a") - var updated = m("div", ["b"], undefined, undefined) + var vnodes = m("div", {}, undefined, "a") + var updated = m("div", {}, ["b"], undefined, undefined) - m.render(root, vnodes) - m.render(root, updated) + m.render(G.root, vnodes) + m.render(G.root, updated) - o(root.firstChild.childNodes.length).equals(1) + o(G.root.firstChild.childNodes.length).equals(1) }) o("removes then recreates then reverses children", function() { var vnodes = [m.key(1, m("a", m.key(3, m("i")), m.key(4, m("s")))), m.key(2, m("b"))] @@ -507,280 +522,306 @@ o.spec("updateNodes", function() { var temp2 = [m.key(1, m("a", m.key(3, m("i")), m.key(4, m("s")))), m.key(2, m("b"))] var updated = [m.key(1, m("a", m.key(4, m("s")), m.key(3, m("i")))), m.key(2, m("b"))] - m.render(root, vnodes) - m.render(root, temp1) - m.render(root, temp2) - m.render(root, updated) + m.render(G.root, vnodes) + m.render(G.root, temp1) + m.render(G.root, temp2) + m.render(G.root, updated) - o(Array.from(root.childNodes, (n) => n.nodeName)).deepEquals(["A", "B"]) - o(Array.from(root.childNodes[0].childNodes, (n) => n.nodeName)).deepEquals(["S", "I"]) - o(updated[0].children[0].dom).equals(root.childNodes[0]) - o(updated[1].children[0].dom).equals(root.childNodes[1]) - o(updated[0].children[0].children[0].children[0].dom).equals(root.childNodes[0].childNodes[0]) - o(updated[0].children[0].children[1].children[0].dom).equals(root.childNodes[0].childNodes[1]) + o(Array.from(G.root.childNodes, (n) => n.nodeName)).deepEquals(["A", "B"]) + o(Array.from(G.root.childNodes[0].childNodes, (n) => n.nodeName)).deepEquals(["S", "I"]) + o(updated[0].c[0].d).equals(G.root.childNodes[0]) + o(updated[1].c[0].d).equals(G.root.childNodes[1]) + o(updated[0].c[0].c[0].c[0].d).equals(G.root.childNodes[0].childNodes[0]) + o(updated[0].c[0].c[1].c[0].d).equals(G.root.childNodes[0].childNodes[1]) }) o("removes then recreates nested", function() { var vnodes = [m.key(1, m("a", m.key(3, m("a", m.key(5, m("a")))), m.key(4, m("a", m.key(5, m("a")))))), m.key(2, m("a"))] var temp = [] var updated = [m.key(1, m("a", m.key(3, m("a", m.key(5, m("a")))), m.key(4, m("a", m.key(5, m("a")))))), m.key(2, m("a"))] - m.render(root, vnodes) - m.render(root, temp) - m.render(root, updated) + m.render(G.root, vnodes) + m.render(G.root, temp) + m.render(G.root, updated) - o(Array.from(root.childNodes, (n) => n.nodeName)).deepEquals(["A", "A"]) - o(Array.from(root.childNodes[0].childNodes, (n) => n.nodeName)).deepEquals(["A", "A"]) - o(Array.from(root.childNodes[0].childNodes[0].childNodes, (n) => n.nodeName)).deepEquals(["A"]) - o(Array.from(root.childNodes[1].childNodes, (n) => n.nodeName)).deepEquals([]) + o(Array.from(G.root.childNodes, (n) => n.nodeName)).deepEquals(["A", "A"]) + o(Array.from(G.root.childNodes[0].childNodes, (n) => n.nodeName)).deepEquals(["A", "A"]) + o(Array.from(G.root.childNodes[0].childNodes[0].childNodes, (n) => n.nodeName)).deepEquals(["A"]) + o(Array.from(G.root.childNodes[1].childNodes, (n) => n.nodeName)).deepEquals([]) }) o("reused top-level element children are rejected against the same root", function () { var cached = m("a") - m.render(root, cached) - o(() => m.render(root, cached)).throws(Error) + m.render(G.root, cached) + o(() => m.render(G.root, cached)).throws(Error) }) o("reused top-level element children are rejected against a different root", function () { var cached = m("a") - var otherRoot = $window.document.createElement("div") + var otherRoot = G.window.document.createElement("div") - m.render(root, cached) + m.render(G.root, cached) o(() => m.render(otherRoot, cached)).throws(Error) }) o("reused inner fragment element children are rejected against the same root", function () { var cached = m("a") - m.render(root, [cached]) - o(() => m.render(root, [cached])).throws(Error) + m.render(G.root, [cached]) + o(() => m.render(G.root, [cached])).throws(Error) }) o("reused inner fragment element children are rejected against a different root", function () { var cached = m("a") - var otherRoot = $window.document.createElement("div") + var otherRoot = G.window.document.createElement("div") - m.render(root, [cached]) + m.render(G.root, [cached]) o(() => m.render(otherRoot, [cached])).throws(Error) }) o("reused inner element element children are rejected against the same root", function () { var cached = m("a") - m.render(root, m("div", cached)) - o(() => m.render(root, m("div", cached))).throws(Error) + m.render(G.root, m("div", cached)) + o(() => m.render(G.root, m("div", cached))).throws(Error) }) o("reused inner element element children are rejected against a different root", function () { var cached = m("a") - var otherRoot = $window.document.createElement("div") + var otherRoot = G.window.document.createElement("div") - m.render(root, m("div", cached)) + m.render(G.root, m("div", cached)) o(() => m.render(otherRoot, m("div", cached))).throws(Error) }) o("reused top-level retain children are rejected against the same root", function () { var cached = m.retain() - m.render(root, m("a")) - m.render(root, cached) - o(() => m.render(root, cached)).throws(Error) + m.render(G.root, m("a")) + m.render(G.root, cached) + o(() => m.render(G.root, cached)).throws(Error) }) o("reused top-level retain children are rejected against a different root", function () { var cached = m.retain() - var otherRoot = $window.document.createElement("div") + var otherRoot = G.window.document.createElement("div") - m.render(root, m("a")) - m.render(root, cached) + m.render(G.root, m("a")) + m.render(G.root, cached) o(() => m.render(otherRoot, cached)).throws(Error) }) o("reused inner fragment retain children are rejected against the same root", function () { var cached = m.retain() - m.render(root, [m("a")]) - m.render(root, [cached]) - o(() => m.render(root, [cached])).throws(Error) + m.render(G.root, [m("a")]) + m.render(G.root, [cached]) + o(() => m.render(G.root, [cached])).throws(Error) }) o("reused inner fragment retain children are rejected against a different root", function () { var cached = m.retain() - var otherRoot = $window.document.createElement("div") + var otherRoot = G.window.document.createElement("div") - m.render(root, [m("a")]) - m.render(root, [cached]) + m.render(G.root, [m("a")]) + m.render(G.root, [cached]) o(() => m.render(otherRoot, [cached])).throws(Error) }) o("reused inner element retain children are rejected against the same root", function () { var cached = m.retain() - m.render(root, m("div", m("a"))) - m.render(root, m("div", cached)) - o(() => m.render(root, m("div", cached))).throws(Error) + m.render(G.root, m("div", m("a"))) + m.render(G.root, m("div", cached)) + o(() => m.render(G.root, m("div", cached))).throws(Error) }) o("reused inner element retain children are rejected against a different root", function () { var cached = m.retain() - var otherRoot = $window.document.createElement("div") + var otherRoot = G.window.document.createElement("div") - m.render(root, m("div", m("a"))) - m.render(root, m("div", cached)) + m.render(G.root, m("div", m("a"))) + m.render(G.root, m("div", cached)) o(() => m.render(otherRoot, m("div", cached))).throws(Error) }) o("cross-removal reused top-level element children are rejected against the same root", function () { var cached = m("a") - m.render(root, cached) - m.render(root, null) - o(() => m.render(root, cached)).throws(Error) + m.render(G.root, cached) + m.render(G.root, null) + o(() => m.render(G.root, cached)).throws(Error) }) o("cross-removal reused inner fragment element children are rejected against the same root", function () { var cached = m("a") - m.render(root, [cached]) - m.render(root, null) - o(() => m.render(root, [cached])).throws(Error) + m.render(G.root, [cached]) + m.render(G.root, null) + o(() => m.render(G.root, [cached])).throws(Error) }) o("cross-removal reused inner element element children are rejected against the same root", function () { var cached = m("a") - m.render(root, m("div", cached)) - m.render(root, null) - o(() => m.render(root, m("div", cached))).throws(Error) + m.render(G.root, m("div", cached)) + m.render(G.root, null) + o(() => m.render(G.root, m("div", cached))).throws(Error) }) o("cross-removal reused top-level retain children are rejected against the same root", function () { var cached = m.retain() - m.render(root, m("a")) - m.render(root, cached) - m.render(root, null) - m.render(root, m("a")) - o(() => m.render(root, cached)).throws(Error) + m.render(G.root, m("a")) + m.render(G.root, cached) + m.render(G.root, null) + m.render(G.root, m("a")) + o(() => m.render(G.root, cached)).throws(Error) }) o("cross-removal reused inner fragment retain children are rejected against the same root", function () { var cached = m.retain() - m.render(root, [m("a")]) - m.render(root, [cached]) - m.render(root, null) - m.render(root, [m("a")]) - o(() => m.render(root, [cached])).throws(Error) + m.render(G.root, [m("a")]) + m.render(G.root, [cached]) + m.render(G.root, null) + m.render(G.root, [m("a")]) + o(() => m.render(G.root, [cached])).throws(Error) }) o("cross-removal reused inner element retain children are rejected against the same root", function () { var cached = m.retain() - m.render(root, m("div", m("a"))) - m.render(root, m("div", cached)) - m.render(root, null) - m.render(root, m("div", m("a"))) - o(() => m.render(root, m("div", cached))).throws(Error) + m.render(G.root, m("div", m("a"))) + m.render(G.root, m("div", cached)) + m.render(G.root, null) + m.render(G.root, m("div", m("a"))) + o(() => m.render(G.root, m("div", cached))).throws(Error) }) o("null stays in place", function() { var onabort = o.spy() - var layout = o.spy((_, signal) => { signal.onabort = onabort }) - var vnodes = [m("div"), m("a", m.layout(layout))] - var temp = [null, m("a", m.layout(layout))] - var updated = [m("div"), m("a", m.layout(layout))] + var create = o.spy((_, signal) => { signal.onabort = onabort }) + var update = o.spy((_, signal) => { signal.onabort = onabort }) + var vnodes = [m("div"), m("a", m.layout(create, update))] + var temp = [null, m("a", m.layout(create, update))] + var updated = [m("div"), m("a", m.layout(create, update))] - m.render(root, vnodes) - var before = vnodes[1].dom - m.render(root, temp) - m.render(root, updated) - var after = updated[1].dom + m.render(G.root, vnodes) + var before = vnodes[1].d - o(before).equals(after) - o(layout.calls.map((c) => c.args[2])).deepEquals([true, false, false]) + o(create.callCount).equals(1) + o(update.callCount).equals(0) + o(onabort.callCount).equals(0) + + m.render(G.root, temp) + + o(create.callCount).equals(1) + o(update.callCount).equals(1) o(onabort.callCount).equals(0) + + m.render(G.root, updated) + var after = updated[1].d + + o(create.callCount).equals(1) + o(update.callCount).equals(2) + o(onabort.callCount).equals(0) + o(before).equals(after) }) o("null stays in place if not first", function() { var onabort = o.spy() - var layout = o.spy((_, signal) => { signal.onabort = onabort }) - var vnodes = [m("b"), m("div"), m("a", m.layout(layout))] - var temp = [m("b"), null, m("a", m.layout(layout))] - var updated = [m("b"), m("div"), m("a", m.layout(layout))] + var create = o.spy((_, signal) => { signal.onabort = onabort }) + var update = o.spy((_, signal) => { signal.onabort = onabort }) + var vnodes = [m("b"), m("div"), m("a", m.layout(create, update))] + var temp = [m("b"), null, m("a", m.layout(create, update))] + var updated = [m("b"), m("div"), m("a", m.layout(create, update))] - m.render(root, vnodes) - var before = vnodes[2].dom - m.render(root, temp) - m.render(root, updated) - var after = updated[2].dom + m.render(G.root, vnodes) + var before = vnodes[2].d - o(before).equals(after) - o(layout.calls.map((c) => c.args[2])).deepEquals([true, false, false]) + o(create.callCount).equals(1) + o(update.callCount).equals(0) + o(onabort.callCount).equals(0) + + m.render(G.root, temp) + + o(create.callCount).equals(1) + o(update.callCount).equals(1) + o(onabort.callCount).equals(0) + + m.render(G.root, updated) + var after = updated[2].d + + o(create.callCount).equals(1) + o(update.callCount).equals(2) o(onabort.callCount).equals(0) + o(before).equals(after) }) o("node is recreated if unwrapped from a key", function () { var vnode = m.key(1, m("b")) var updated = m("b") - m.render(root, vnode) - m.render(root, updated) + m.render(G.root, vnode) + m.render(G.root, updated) - o(vnode.children[0].dom).notEquals(updated.dom) + o(vnode.c[0].d).notEquals(updated.d) }) o("don't add back elements from fragments that are restored from the pool #1991", function() { - m.render(root, [ + m.render(G.root, [ [], [] ]) - m.render(root, [ + m.render(G.root, [ [], [m("div")] ]) - m.render(root, [ + m.render(G.root, [ [null] ]) - m.render(root, [ + m.render(G.root, [ [], [] ]) - o(root.childNodes.length).equals(0) + o(G.root.childNodes.length).equals(0) }) o("don't add back elements from fragments that are being removed #1991", function() { - m.render(root, [ + m.render(G.root, [ [], m("p"), ]) - m.render(root, [ + m.render(G.root, [ [m("div", 5)] ]) - m.render(root, [ + m.render(G.root, [ [], [] ]) - o(root.childNodes.length).equals(0) + o(G.root.childNodes.length).equals(0) }) o("handles null values in unkeyed lists of different length (#2003)", function() { var onabort = o.spy() - var layout = o.spy((_, signal) => { signal.onabort = onabort }) + var create = o.spy((_, signal) => { signal.onabort = onabort }) + var update = o.spy((_, signal) => { signal.onabort = onabort }) - m.render(root, [m("div", m.layout(layout)), null]) - m.render(root, [null, m("div", m.layout(layout)), null]) + m.render(G.root, [m("div", m.layout(create, update)), null]) + m.render(G.root, [null, m("div", m.layout(create, update)), null]) - o(layout.calls.map((c) => c.args[2])).deepEquals([true, true]) + o(create.callCount).equals(2) + o(update.callCount).equals(0) o(onabort.callCount).equals(1) }) o("supports changing the element of a keyed element in a list when traversed bottom-up", function() { - m.render(root, [m.key(2, m("a"))]) - m.render(root, [m.key(1, m("b")), m.key(2, m("b"))]) + m.render(G.root, [m.key(2, m("a"))]) + m.render(G.root, [m.key(1, m("b")), m.key(2, m("b"))]) - o(Array.from(root.childNodes, (n) => n.nodeName)).deepEquals(["B", "B"]) + o(Array.from(G.root.childNodes, (n) => n.nodeName)).deepEquals(["B", "B"]) }) o("supports changing the element of a keyed element in a list when looking up nodes using the map", function() { - m.render(root, [m.key(1, m("x")), m.key(2, m("y")), m.key(3, m("z"))]) - m.render(root, [m.key(2, m("b")), m.key(1, m("c")), m.key(4, m("d")), m.key(3, m("e"))]) + m.render(G.root, [m.key(1, m("x")), m.key(2, m("y")), m.key(3, m("z"))]) + m.render(G.root, [m.key(2, m("b")), m.key(1, m("c")), m.key(4, m("d")), m.key(3, m("e"))]) - o(Array.from(root.childNodes, (n) => n.nodeName)).deepEquals(["B", "C", "D", "E"]) + o(Array.from(G.root.childNodes, (n) => n.nodeName)).deepEquals(["B", "C", "D", "E"]) }) o("don't fetch the nextSibling from the pool", function() { - m.render(root, [[m.key(1, m("div")), m.key(2, m("div"))], m("p")]) - m.render(root, [[], m("p")]) - m.render(root, [[m.key(2, m("div")), m.key(1, m("div"))], m("p")]) + m.render(G.root, [[m.key(1, m("div")), m.key(2, m("div"))], m("p")]) + m.render(G.root, [[], m("p")]) + m.render(G.root, [[m.key(2, m("div")), m.key(1, m("div"))], m("p")]) - o(Array.from(root.childNodes, (el) => el.nodeName)).deepEquals(["DIV", "DIV", "P"]) + o(Array.from(G.root.childNodes, (el) => el.nodeName)).deepEquals(["DIV", "DIV", "P"]) }) o("reverses a keyed lists with an odd number of items", function() { var vnodes = vnodify("a,b,c,d") var updated = vnodify("d,c,b,a") - var expectedTagNames = updated.map(function(vn) {return vn.children[0].tag}) + var expectedTagNames = updated.map((vn) => vn.c[0].t) - m.render(root, vnodes) - m.render(root, updated) + m.render(G.root, vnodes) + m.render(G.root, updated) - var tagNames = Array.from(root.childNodes, (n) => n.nodeName.toLowerCase()) + var tagNames = Array.from(G.root.childNodes, (n) => n.nodeName.toLowerCase()) o(tagNames).deepEquals(expectedTagNames) }) @@ -789,72 +830,72 @@ o.spec("updateNodes", function() { var updated = vnodify("c,b,a") var vnodes = [m.key("a", m("a")), m.key("b", m("b")), m.key("c", m("c"))] var updated = [m.key("c", m("c")), m.key("b", m("b")), m.key("a", m("a"))] - var expectedTagNames = updated.map(function(vn) {return vn.children[0].tag}) + var expectedTagNames = updated.map((vn) => vn.c[0].t) - m.render(root, vnodes) - m.render(root, updated) + m.render(G.root, vnodes) + m.render(G.root, updated) - var tagNames = Array.from(root.childNodes, (n) => n.nodeName.toLowerCase()) + var tagNames = Array.from(G.root.childNodes, (n) => n.nodeName.toLowerCase()) o(tagNames).deepEquals(expectedTagNames) }) o("scrambles a keyed lists with prefixes and suffixes", function() { var vnodes = vnodify("i,a,b,c,d,j") var updated = vnodify("i,b,a,d,c,j") - var expectedTagNames = updated.map(function(vn) {return vn.children[0].tag}) + var expectedTagNames = updated.map((vn) => vn.c[0].t) - m.render(root, vnodes) - m.render(root, updated) + m.render(G.root, vnodes) + m.render(G.root, updated) - var tagNames = Array.from(root.childNodes, (n) => n.nodeName.toLowerCase()) + var tagNames = Array.from(G.root.childNodes, (n) => n.nodeName.toLowerCase()) o(tagNames).deepEquals(expectedTagNames) }) o("reverses a keyed lists with an odd number of items with prefixes and suffixes", function() { var vnodes = vnodify("i,a,b,c,d,j") var updated = vnodify("i,d,c,b,a,j") - var expectedTagNames = updated.map(function(vn) {return vn.children[0].tag}) + var expectedTagNames = updated.map((vn) => vn.c[0].t) - m.render(root, vnodes) - m.render(root, updated) + m.render(G.root, vnodes) + m.render(G.root, updated) - var tagNames = Array.from(root.childNodes, (n) => n.nodeName.toLowerCase()) + var tagNames = Array.from(G.root.childNodes, (n) => n.nodeName.toLowerCase()) o(tagNames).deepEquals(expectedTagNames) }) o("reverses a keyed lists with an even number of items with prefixes and suffixes", function() { var vnodes = vnodify("i,a,b,c,j") var updated = vnodify("i,c,b,a,j") - var expectedTagNames = updated.map(function(vn) {return vn.children[0].tag}) + var expectedTagNames = updated.map((vn) => vn.c[0].t) - m.render(root, vnodes) - m.render(root, updated) + m.render(G.root, vnodes) + m.render(G.root, updated) - var tagNames = Array.from(root.childNodes, (n) => n.nodeName.toLowerCase()) + var tagNames = Array.from(G.root.childNodes, (n) => n.nodeName.toLowerCase()) o(tagNames).deepEquals(expectedTagNames) }) o("scrambling sample 1", function() { var vnodes = vnodify("k0,k1,k2,k3,k4,k5,k6,k7,k8,k9") var updated = vnodify("k4,k1,k2,k9,k0,k3,k6,k5,k8,k7") - var expectedTagNames = updated.map(function(vn) {return vn.children[0].tag}) + var expectedTagNames = updated.map((vn) => vn.c[0].t) - m.render(root, vnodes) - m.render(root, updated) + m.render(G.root, vnodes) + m.render(G.root, updated) - var tagNames = Array.from(root.childNodes, (n) => n.nodeName.toLowerCase()) + var tagNames = Array.from(G.root.childNodes, (n) => n.nodeName.toLowerCase()) o(tagNames).deepEquals(expectedTagNames) }) o("scrambling sample 2", function() { var vnodes = vnodify("k0,k1,k2,k3,k4,k5,k6,k7,k8,k9") var updated = vnodify("b,d,k1,k0,k2,k3,k4,a,c,k5,k6,k7,k8,k9") - var expectedTagNames = updated.map(function(vn) {return vn.children[0].tag}) + var expectedTagNames = updated.map((vn) => vn.c[0].t) - m.render(root, vnodes) - m.render(root, updated) + m.render(G.root, vnodes) + m.render(G.root, updated) - var tagNames = Array.from(root.childNodes, (n) => n.nodeName.toLowerCase()) + var tagNames = Array.from(G.root.childNodes, (n) => n.nodeName.toLowerCase()) o(tagNames).deepEquals(expectedTagNames) }) @@ -865,11 +906,11 @@ o.spec("updateNodes", function() { var temp = [[null, m(component), m("b")]] var updated = [[m("a"), m(component), m("b")]] - m.render(root, vnodes) - m.render(root, temp) - m.render(root, updated) + m.render(G.root, vnodes) + m.render(G.root, temp) + m.render(G.root, updated) - o(Array.from(root.childNodes, (n) => n.nodeName)).deepEquals(["A", "B"]) + o(Array.from(G.root.childNodes, (n) => n.nodeName)).deepEquals(["A", "B"]) }) o("fragment child toggles from null in component when followed by null component then tag", function() { var flag = true @@ -879,19 +920,19 @@ o.spec("updateNodes", function() { var temp = [[m(a), m(b), m("s")]] var updated = [[m(a), m(b), m("s")]] - m.render(root, vnodes) + m.render(G.root, vnodes) flag = false - m.render(root, temp) + m.render(G.root, temp) flag = true - m.render(root, updated) + m.render(G.root, updated) - o(Array.from(root.childNodes, (n) => n.nodeName)).deepEquals(["A", "S"]) + o(Array.from(G.root.childNodes, (n) => n.nodeName)).deepEquals(["A", "S"]) }) o("removing a component that returns a fragment doesn't throw (regression test for incidental bug introduced while debugging some Flems)", function() { var component = () => [m("a"), m("b")] - m.render(root, [m(component)]) - m.render(root, []) + m.render(G.root, [m(component)]) + m.render(G.root, []) - o(root.childNodes.length).equals(0) + o(G.root.childNodes.length).equals(0) }) }) diff --git a/tests/render/updateNodesFuzzer.js b/tests/render/updateNodesFuzzer.js index 89014c4e6..4e6f6c72e 100644 --- a/tests/render/updateNodesFuzzer.js +++ b/tests/render/updateNodesFuzzer.js @@ -1,6 +1,7 @@ import o from "ospec" -import domMock from "../../test-utils/domMock.js" +import {setupGlobals} from "../../test-utils/global.js" + import m from "../../src/entry/mithril.esm.js" o.spec("updateNodes keyed list Fuzzer", () => { @@ -35,18 +36,17 @@ o.spec("updateNodes keyed list Fuzzer", () => { } } + var G = setupGlobals() + function fuzzGroup(label, view, assert) { o.spec(label, () => { for (let i = 0; i < testCount; i++) { const from = randomUnique(fromUsed) const to = randomUnique(toUsed) o(`${i}: ${from} -> ${to}`, () => { - var $window = domMock() - var root = $window.document.body - - m.render(root, from.map((x) => m.key(x, view(x)))) - m.render(root, to.map((x) => m.key(x, view(x)))) - assert(root, to) + m.render(G.root, from.map((x) => m.key(x, view(x)))) + m.render(G.root, to.map((x) => m.key(x, view(x)))) + assert(G.root, to) }) } }) diff --git a/tests/render/updateText.js b/tests/render/updateText.js index 67e9620a4..2f21b5084 100644 --- a/tests/render/updateText.js +++ b/tests/render/updateText.js @@ -1,94 +1,91 @@ import o from "ospec" -import domMock from "../../test-utils/domMock.js" +import {setupGlobals} from "../../test-utils/global.js" + import m from "../../src/entry/mithril.esm.js" o.spec("updateText", function() { - var $window, root - o.beforeEach(function() { - $window = domMock() - root = $window.document.createElement("div") - }) + var G = setupGlobals() o("updates to string", function() { var vnode = "a" var updated = "b" - m.render(root, vnode) - m.render(root, updated) + m.render(G.root, vnode) + m.render(G.root, updated) - o(root.firstChild.nodeValue).equals("b") + o(G.root.firstChild.nodeValue).equals("b") }) o("updates to falsy string", function() { var vnode = "a" var updated = "" - m.render(root, vnode) - m.render(root, updated) + m.render(G.root, vnode) + m.render(G.root, updated) - o(root.firstChild.nodeValue).equals("") + o(G.root.firstChild.nodeValue).equals("") }) o("updates from falsy string", function() { var vnode = "" var updated = "b" - m.render(root, vnode) - m.render(root, updated) + m.render(G.root, vnode) + m.render(G.root, updated) - o(root.firstChild.nodeValue).equals("b") + o(G.root.firstChild.nodeValue).equals("b") }) o("updates to number", function() { var vnode = "a" var updated = 1 - m.render(root, vnode) - m.render(root, updated) + m.render(G.root, vnode) + m.render(G.root, updated) - o(root.firstChild.nodeValue).equals("1") + o(G.root.firstChild.nodeValue).equals("1") }) o("updates to falsy number", function() { var vnode = "a" var updated = 0 - m.render(root, vnode) - m.render(root, updated) + m.render(G.root, vnode) + m.render(G.root, updated) - o(root.firstChild.nodeValue).equals("0") + o(G.root.firstChild.nodeValue).equals("0") }) o("updates from falsy number", function() { var vnode = 0 var updated = "b" - m.render(root, vnode) - m.render(root, updated) + m.render(G.root, vnode) + m.render(G.root, updated) - o(root.firstChild.nodeValue).equals("b") + o(G.root.firstChild.nodeValue).equals("b") }) o("updates to boolean", function() { var vnode = "a" var updated = true - m.render(root, vnode) - m.render(root, updated) + m.render(G.root, vnode) + m.render(G.root, updated) - o(root.childNodes.length).equals(0) + o(G.root.childNodes.length).equals(0) }) o("updates to falsy boolean", function() { var vnode = "a" var updated = false - m.render(root, vnode) - m.render(root, updated) + m.render(G.root, vnode) + m.render(G.root, updated) - o(root.childNodes.length).equals(0) + o(G.root.childNodes.length).equals(0) }) o("updates from falsy boolean", function() { var vnode = false var updated = "b" - m.render(root, vnode) - m.render(root, updated) + m.render(G.root, vnode) + m.render(G.root, updated) - o(root.firstChild.nodeValue).equals("b") + o(G.root.firstChild.nodeValue).equals("b") }) }) diff --git a/tests/test-utils/browserMock.js b/tests/test-utils/browserMock.js index 79d32c1c8..b27104f83 100644 --- a/tests/test-utils/browserMock.js +++ b/tests/test-utils/browserMock.js @@ -1,39 +1,35 @@ import o from "ospec" -import browserMock from "../../test-utils/browserMock.js" import {callAsync} from "../../test-utils/callAsync.js" +import {setupGlobals} from "../../test-utils/global.js" o.spec("browserMock", function() { - var $window - o.beforeEach(function() { - $window = browserMock() - }) + var G = setupGlobals() - o("Mocks DOM, pushState and XHR", function() { - o($window.location).notEquals(undefined) - o($window.document).notEquals(undefined) - o($window.XMLHttpRequest).notEquals(undefined) + o("Mocks DOM and pushState", function() { + o(G.window.location).notEquals(undefined) + o(G.window.document).notEquals(undefined) }) - o("$window.onhashchange can be reached from the pushStateMock functions", function(done) { - $window.onhashchange = o.spy() - $window.location.hash = "#a" + o("G.window.onhashchange can be reached from the pushStateMock functions", function(done) { + G.window.onhashchange = o.spy() + G.window.location.hash = "#a" callAsync(function(){ - o($window.onhashchange.callCount).equals(1) + o(G.window.onhashchange.callCount).equals(1) done() }) }) - o("$window.onpopstate can be reached from the pushStateMock functions", function() { - $window.onpopstate = o.spy() - $window.history.pushState(null, null, "#a") - $window.history.back() + o("G.window.onpopstate can be reached from the pushStateMock functions", function() { + G.window.onpopstate = o.spy() + G.window.history.pushState(null, null, "#a") + G.window.history.back() - o($window.onpopstate.callCount).equals(1) + o(G.window.onpopstate.callCount).equals(1) }) - o("$window.onunload can be reached from the pushStateMock functions", function() { - $window.onunload = o.spy() - $window.location.href = "/a" + o("G.window.onunload can be reached from the pushStateMock functions", function() { + G.window.onunload = o.spy() + G.window.location.href = "/a" - o($window.onunload.callCount).equals(1) + o(G.window.onunload.callCount).equals(1) }) }) diff --git a/tests/test-utils/callAsync.js b/tests/test-utils/callAsync.js index ede0c72cd..ded82203d 100644 --- a/tests/test-utils/callAsync.js +++ b/tests/test-utils/callAsync.js @@ -1,3 +1,5 @@ +/* global setTimeout, clearTimeout */ + import o from "ospec" import {callAsync, clearPending, waitAsync} from "../../test-utils/callAsync.js" diff --git a/tests/test-utils/domMock.js b/tests/test-utils/domMock.js index c43bcbfb2..c43f288f4 100644 --- a/tests/test-utils/domMock.js +++ b/tests/test-utils/domMock.js @@ -1,18 +1,13 @@ -/* global process: false */ import o from "ospec" -import domMock from "../../test-utils/domMock.js" +import {setupGlobals} from "../../test-utils/global.js" o.spec("domMock", function() { - var $document, $window - o.beforeEach(function() { - $window = domMock() - $document = $window.document - }) + var G = setupGlobals() o.spec("createElement", function() { o("works", function() { - var node = $document.createElement("div") + var node = G.window.document.createElement("div") o(node.nodeType).equals(1) o(node.nodeName).equals("DIV") @@ -26,7 +21,7 @@ o.spec("domMock", function() { o.spec("createElementNS", function() { o("works", function() { - var node = $document.createElementNS("http://www.w3.org/2000/svg", "svg") + var node = G.window.document.createElementNS("http://www.w3.org/2000/svg", "svg") o(node.nodeType).equals(1) o(node.nodeName).equals("svg") @@ -40,7 +35,7 @@ o.spec("domMock", function() { o.spec("createTextNode", function() { o("works", function() { - var node = $document.createTextNode("abc") + var node = G.window.document.createTextNode("abc") o(node.nodeType).equals(3) o(node.nodeName).equals("#text") @@ -48,32 +43,32 @@ o.spec("domMock", function() { o(node.nodeValue).equals("abc") }) o("works w/ number", function() { - var node = $document.createTextNode(123) + var node = G.window.document.createTextNode(123) o(node.nodeValue).equals("123") }) o("works w/ null", function() { - var node = $document.createTextNode(null) + var node = G.window.document.createTextNode(null) o(node.nodeValue).equals("null") }) o("works w/ undefined", function() { - var node = $document.createTextNode(undefined) + var node = G.window.document.createTextNode(undefined) o(node.nodeValue).equals("undefined") }) o("works w/ object", function() { - var node = $document.createTextNode({}) + var node = G.window.document.createTextNode({}) o(node.nodeValue).equals("[object Object]") }) o("does not unescape HTML", function() { - var node = $document.createTextNode("&") + var node = G.window.document.createTextNode("&") o(node.nodeValue).equals("&") }) o("nodeValue casts to string", function() { - var node = $document.createTextNode("a") + var node = G.window.document.createTextNode("a") node.nodeValue = true o(node.nodeValue).equals("true") @@ -82,7 +77,7 @@ o.spec("domMock", function() { o("doesn't work with symbols", function(){ var threw = false try { - $document.createTextNode(Symbol("nono")) + G.window.document.createTextNode(Symbol("nono")) } catch(e) { threw = true } @@ -91,7 +86,7 @@ o.spec("domMock", function() { o("symbols can't be used as nodeValue", function(){ var threw = false try { - var node = $document.createTextNode("a") + var node = G.window.document.createTextNode("a") node.nodeValue = Symbol("nono") } catch(e) { threw = true @@ -101,22 +96,10 @@ o.spec("domMock", function() { } }) - o.spec("createDocumentFragment", function() { - o("works", function() { - var node = $document.createDocumentFragment() - - o(node.nodeType).equals(11) - o(node.nodeName).equals("#document-fragment") - o(node.parentNode).equals(null) - o(node.childNodes.length).equals(0) - o(node.firstChild).equals(null) - }) - }) - o.spec("appendChild", function() { o("works", function() { - var parent = $document.createElement("div") - var child = $document.createElement("a") + var parent = G.window.document.createElement("div") + var child = G.window.document.createElement("a") parent.appendChild(child) o(parent.childNodes.length).equals(1) @@ -125,9 +108,9 @@ o.spec("domMock", function() { o(child.parentNode).equals(parent) }) o("moves existing", function() { - var parent = $document.createElement("div") - var a = $document.createElement("a") - var b = $document.createElement("b") + var parent = G.window.document.createElement("div") + var a = G.window.document.createElement("a") + var b = G.window.document.createElement("b") parent.appendChild(a) parent.appendChild(b) parent.appendChild(a) @@ -141,50 +124,30 @@ o.spec("domMock", function() { o(b.parentNode).equals(parent) }) o("removes from old parent", function() { - var parent = $document.createElement("div") - var source = $document.createElement("span") - var a = $document.createElement("a") - var b = $document.createElement("b") + var parent = G.window.document.createElement("div") + var source = G.window.document.createElement("span") + var a = G.window.document.createElement("a") + var b = G.window.document.createElement("b") parent.appendChild(a) source.appendChild(b) parent.appendChild(b) o(source.childNodes.length).equals(0) }) - o("transfers from fragment", function() { - var parent = $document.createElement("div") - var a = $document.createDocumentFragment("a") - var b = $document.createElement("b") - var c = $document.createElement("c") - a.appendChild(b) - a.appendChild(c) - parent.appendChild(a) - - o(parent.childNodes.length).equals(2) - o(parent.childNodes[0]).equals(b) - o(parent.childNodes[1]).equals(c) - o(parent.firstChild).equals(b) - o(parent.firstChild.nextSibling).equals(c) - o(a.childNodes.length).equals(0) - o(a.firstChild).equals(null) - o(a.parentNode).equals(null) - o(b.parentNode).equals(parent) - o(c.parentNode).equals(parent) - }) o("throws if appended to self", function(done) { - var div = $document.createElement("div") + var div = G.window.document.createElement("div") try {div.appendChild(div)} catch (e) {done()} }) o("throws if appended to child", function(done) { - var parent = $document.createElement("div") - var child = $document.createElement("a") + var parent = G.window.document.createElement("div") + var child = G.window.document.createElement("a") parent.appendChild(child) try {child.appendChild(parent)} catch (e) {done()} }) o("throws if child is not element", function(done) { - var parent = $document.createElement("div") + var parent = G.window.document.createElement("div") var child = 1 try {parent.appendChild(child)} catch (e) {done()} @@ -193,8 +156,8 @@ o.spec("domMock", function() { o.spec("removeChild", function() { o("works", function() { - var parent = $document.createElement("div") - var child = $document.createElement("a") + var parent = G.window.document.createElement("div") + var child = G.window.document.createElement("a") parent.appendChild(child) parent.removeChild(child) @@ -203,8 +166,8 @@ o.spec("domMock", function() { o(child.parentNode).equals(null) }) o("throws if not a child", function(done) { - var parent = $document.createElement("div") - var child = $document.createElement("a") + var parent = G.window.document.createElement("div") + var child = G.window.document.createElement("a") try {parent.removeChild(child)} catch (e) {done()} }) @@ -212,9 +175,9 @@ o.spec("domMock", function() { o.spec("insertBefore", function() { o("works", function() { - var parent = $document.createElement("div") - var a = $document.createElement("a") - var b = $document.createElement("b") + var parent = G.window.document.createElement("div") + var a = G.window.document.createElement("a") + var b = G.window.document.createElement("b") parent.appendChild(a) parent.insertBefore(b, a) @@ -227,9 +190,9 @@ o.spec("domMock", function() { o(b.parentNode).equals(parent) }) o("moves existing", function() { - var parent = $document.createElement("div") - var a = $document.createElement("a") - var b = $document.createElement("b") + var parent = G.window.document.createElement("div") + var a = G.window.document.createElement("a") + var b = G.window.document.createElement("b") parent.appendChild(a) parent.appendChild(b) parent.insertBefore(b, a) @@ -243,10 +206,10 @@ o.spec("domMock", function() { o(b.parentNode).equals(parent) }) o("moves existing node forward but not at the end", function() { - var parent = $document.createElement("div") - var a = $document.createElement("a") - var b = $document.createElement("b") - var c = $document.createElement("c") + var parent = G.window.document.createElement("div") + var a = G.window.document.createElement("a") + var b = G.window.document.createElement("b") + var c = G.window.document.createElement("c") parent.appendChild(a) parent.appendChild(b) parent.appendChild(c) @@ -265,44 +228,20 @@ o.spec("domMock", function() { }) o("removes from old parent", function() { - var parent = $document.createElement("div") - var source = $document.createElement("span") - var a = $document.createElement("a") - var b = $document.createElement("b") + var parent = G.window.document.createElement("div") + var source = G.window.document.createElement("span") + var a = G.window.document.createElement("a") + var b = G.window.document.createElement("b") parent.appendChild(a) source.appendChild(b) parent.insertBefore(b, a) o(source.childNodes.length).equals(0) }) - o("transfers from fragment", function() { - var parent = $document.createElement("div") - var ref = $document.createElement("span") - var a = $document.createDocumentFragment("a") - var b = $document.createElement("b") - var c = $document.createElement("c") - parent.appendChild(ref) - a.appendChild(b) - a.appendChild(c) - parent.insertBefore(a, ref) - - o(parent.childNodes.length).equals(3) - o(parent.childNodes[0]).equals(b) - o(parent.childNodes[1]).equals(c) - o(parent.childNodes[2]).equals(ref) - o(parent.firstChild).equals(b) - o(parent.firstChild.nextSibling).equals(c) - o(parent.firstChild.nextSibling.nextSibling).equals(ref) - o(a.childNodes.length).equals(0) - o(a.firstChild).equals(null) - o(a.parentNode).equals(null) - o(b.parentNode).equals(parent) - o(c.parentNode).equals(parent) - }) o("appends if second arg is null", function() { - var parent = $document.createElement("div") - var a = $document.createElement("a") - var b = $document.createElement("b") + var parent = G.window.document.createElement("div") + var a = G.window.document.createElement("a") + var b = G.window.document.createElement("b") parent.appendChild(a) parent.insertBefore(b, null) @@ -314,44 +253,44 @@ o.spec("domMock", function() { o(a.parentNode).equals(parent) }) o("throws if appended to self", function(done) { - var div = $document.createElement("div") - var a = $document.createElement("a") + var div = G.window.document.createElement("div") + var a = G.window.document.createElement("a") div.appendChild(a) try {div.isnertBefore(div, a)} catch (e) {done()} }) o("throws if appended to child", function(done) { - var parent = $document.createElement("div") - var a = $document.createElement("a") - var b = $document.createElement("b") + var parent = G.window.document.createElement("div") + var a = G.window.document.createElement("a") + var b = G.window.document.createElement("b") parent.appendChild(a) a.appendChild(b) try {a.insertBefore(parent, b)} catch (e) {done()} }) o("throws if child is not element", function(done) { - var parent = $document.createElement("div") - var a = $document.createElement("a") + var parent = G.window.document.createElement("div") + var a = G.window.document.createElement("a") parent.appendChild(a) try {parent.insertBefore(1, a)} catch (e) {done()} }) o("throws if inserted before itself", function(done) { - var parent = $document.createElement("div") - var a = $document.createElement("a") + var parent = G.window.document.createElement("div") + var a = G.window.document.createElement("a") try {parent.insertBefore(a, a)} catch (e) {done()} }) o("throws if second arg is undefined", function(done) { - var parent = $document.createElement("div") - var a = $document.createElement("a") + var parent = G.window.document.createElement("div") + var a = G.window.document.createElement("a") try {parent.insertBefore(a)} catch (e) {done()} }) o("throws if reference is not child", function(done) { - var parent = $document.createElement("div") - var a = $document.createElement("a") - var b = $document.createElement("b") + var parent = G.window.document.createElement("div") + var a = G.window.document.createElement("a") + var b = G.window.document.createElement("b") try {parent.insertBefore(a, b)} catch (e) {done()} }) @@ -359,13 +298,13 @@ o.spec("domMock", function() { o.spec("getAttribute", function() { o("works", function() { - var div = $document.createElement("div") + var div = G.window.document.createElement("div") div.setAttribute("id", "aaa") o(div.getAttribute("id")).equals("aaa") }) o("works for attributes with a namespace", function() { - var div = $document.createElement("div") + var div = G.window.document.createElement("div") div.setAttributeNS("http://www.w3.org/1999/xlink", "href", "aaa") o(div.getAttribute("href")).equals("aaa") @@ -374,7 +313,7 @@ o.spec("domMock", function() { o.spec("setAttribute", function() { o("works", function() { - var div = $document.createElement("div") + var div = G.window.document.createElement("div") div.setAttribute("id", "aaa") o(div.attributes["id"].value).equals("aaa") @@ -382,31 +321,31 @@ o.spec("domMock", function() { o(div.attributes["id"].namespaceURI).equals(null) }) o("works w/ number", function() { - var div = $document.createElement("div") + var div = G.window.document.createElement("div") div.setAttribute("id", 123) o(div.attributes["id"].value).equals("123") }) o("works w/ null", function() { - var div = $document.createElement("div") + var div = G.window.document.createElement("div") div.setAttribute("id", null) o(div.attributes["id"].value).equals("null") }) o("works w/ undefined", function() { - var div = $document.createElement("div") + var div = G.window.document.createElement("div") div.setAttribute("id", undefined) o(div.attributes["id"].value).equals("undefined") }) o("works w/ object", function() { - var div = $document.createElement("div") + var div = G.window.document.createElement("div") div.setAttribute("id", {}) o(div.attributes["id"].value).equals("[object Object]") }) o("setting via attributes map stringifies", function() { - var div = $document.createElement("div") + var div = G.window.document.createElement("div") div.setAttribute("id", "a") div.attributes["id"].value = 123 @@ -419,7 +358,7 @@ o.spec("domMock", function() { }) o.spec("hasAttribute", function() { o("works", function() { - var div = $document.createElement("div") + var div = G.window.document.createElement("div") o(div.hasAttribute("id")).equals(false) @@ -435,7 +374,7 @@ o.spec("domMock", function() { o.spec("setAttributeNS", function() { o("works", function() { - var a = $document.createElementNS("http://www.w3.org/2000/svg", "a") + var a = G.window.document.createElementNS("http://www.w3.org/2000/svg", "a") a.setAttributeNS("http://www.w3.org/1999/xlink", "href", "/aaa") o(a.href).deepEquals({baseVal: "/aaa", animVal: "/aaa"}) @@ -443,7 +382,7 @@ o.spec("domMock", function() { o(a.attributes["href"].namespaceURI).equals("http://www.w3.org/1999/xlink") }) o("works w/ number", function() { - var a = $document.createElementNS("http://www.w3.org/2000/svg", "a") + var a = G.window.document.createElementNS("http://www.w3.org/2000/svg", "a") a.setAttributeNS("http://www.w3.org/1999/xlink", "href", 123) o(a.href).deepEquals({baseVal: "123", animVal: "123"}) @@ -451,7 +390,7 @@ o.spec("domMock", function() { o(a.attributes["href"].namespaceURI).equals("http://www.w3.org/1999/xlink") }) o("attributes with a namespace can be querried, updated and removed with non-NS functions", function() { - var a = $document.createElementNS("http://www.w3.org/2000/svg", "a") + var a = G.window.document.createElementNS("http://www.w3.org/2000/svg", "a") a.setAttributeNS("http://www.w3.org/1999/xlink", "href", "/aaa") o(a.hasAttribute("href")).equals(true) @@ -474,7 +413,7 @@ o.spec("domMock", function() { o.spec("removeAttribute", function() { o("works", function() { - var div = $document.createElement("div") + var div = G.window.document.createElement("div") div.setAttribute("id", "aaa") div.removeAttribute("id") @@ -484,7 +423,7 @@ o.spec("domMock", function() { o.spec("textContent", function() { o("works", function() { - var div = $document.createElement("div") + var div = G.window.document.createElement("div") div.textContent = "aaa" o(div.childNodes.length).equals(1) @@ -492,128 +431,39 @@ o.spec("domMock", function() { o(div.firstChild.nodeValue).equals("aaa") }) o("works with empty string", function() { - var div = $document.createElement("div") + var div = G.window.document.createElement("div") div.textContent = "" o(div.childNodes.length).equals(0) }) }) - o.spec("innerHTML", function() { - o("works", function() { - var div = $document.createElement("div") - div.innerHTML = "
123234
345
" - o(div.childNodes.length).equals(2) - o(div.childNodes[0].nodeType).equals(1) - o(div.childNodes[0].nodeName).equals("BR") - o(div.childNodes[1].nodeType).equals(1) - o(div.childNodes[1].nodeName).equals("A") - o(div.childNodes[1].attributes["class"].value).equals("aaa") - o(div.childNodes[1].attributes["id"].value).equals("xyz") - o(div.childNodes[1].childNodes[0].nodeType).equals(3) - o(div.childNodes[1].childNodes[0].nodeValue).equals("123") - o(div.childNodes[1].childNodes[1].nodeType).equals(1) - o(div.childNodes[1].childNodes[1].nodeName).equals("B") - o(div.childNodes[1].childNodes[1].attributes["class"].value).equals("bbb") - o(div.childNodes[1].childNodes[2].nodeType).equals(3) - o(div.childNodes[1].childNodes[2].nodeValue).equals("234") - o(div.childNodes[1].childNodes[3].nodeType).equals(1) - o(div.childNodes[1].childNodes[3].nodeName).equals("BR") - o(div.childNodes[1].childNodes[3].attributes["class"].value).equals("ccc") - o(div.childNodes[1].childNodes[4].nodeType).equals(3) - o(div.childNodes[1].childNodes[4].nodeValue).equals("345") - }) - o("headers work", function() { - var div = $document.createElement("div") - div.innerHTML = "

" - o(div.childNodes.length).equals(6) - o(div.childNodes[0].nodeType).equals(1) - o(div.childNodes[0].nodeName).equals("H1") - o(div.childNodes[1].nodeType).equals(1) - o(div.childNodes[1].nodeName).equals("H2") - o(div.childNodes[2].nodeType).equals(1) - o(div.childNodes[2].nodeName).equals("H3") - o(div.childNodes[3].nodeType).equals(1) - o(div.childNodes[3].nodeName).equals("H4") - o(div.childNodes[4].nodeType).equals(1) - o(div.childNodes[4].nodeName).equals("H5") - o(div.childNodes[5].nodeType).equals(1) - o(div.childNodes[5].nodeName).equals("H6") - }) - o("detaches old elements", function() { - var div = $document.createElement("div") - var a = $document.createElement("a") - div.appendChild(a) - div.innerHTML = "" - - o(a.parentNode).equals(null) - }) - o("empty SVG document", function() { - var div = $document.createElement("div") - div.innerHTML = "" - - o(typeof div.firstChild).notEquals(undefined) - o(div.firstChild.nodeName).equals("svg") - o(div.firstChild.namespaceURI).equals("http://www.w3.org/2000/svg") - o(div.firstChild.childNodes.length).equals(0) - }) - o("text elements", function() { - var div = $document.createElement("div") - div.innerHTML = - "" - + "hello" - + " " - + "world" - + "" - - o(div.firstChild.nodeName).equals("svg") - o(div.firstChild.namespaceURI).equals("http://www.w3.org/2000/svg") - - var nodes = div.firstChild.childNodes - o(nodes.length).equals(3) - o(nodes[0].nodeName).equals("text") - o(nodes[0].namespaceURI).equals("http://www.w3.org/2000/svg") - o(nodes[0].childNodes.length).equals(1) - o(nodes[0].childNodes[0].nodeName).equals("#text") - o(nodes[0].childNodes[0].nodeValue).equals("hello") - o(nodes[1].nodeName).equals("text") - o(nodes[1].namespaceURI).equals("http://www.w3.org/2000/svg") - o(nodes[1].childNodes.length).equals(1) - o(nodes[1].childNodes[0].nodeName).equals("#text") - o(nodes[1].childNodes[0].nodeValue).equals(" ") - o(nodes[2].nodeName).equals("text") - o(nodes[2].namespaceURI).equals("http://www.w3.org/2000/svg") - o(nodes[2].childNodes.length).equals(1) - o(nodes[2].childNodes[0].nodeName).equals("#text") - o(nodes[2].childNodes[0].nodeValue).equals("world") - }) - }) o.spec("focus", function() { o("body is active by default", function() { - o($document.documentElement.nodeName).equals("HTML") - o($document.body.nodeName).equals("BODY") - o($document.documentElement.firstChild.nodeName).equals("HEAD") - o($document.documentElement).equals($document.body.parentNode) - o($document.activeElement).equals($document.body) + o(G.window.document.documentElement.nodeName).equals("HTML") + o(G.window.document.body.nodeName).equals("BODY") + o(G.window.document.documentElement.firstChild.nodeName).equals("HEAD") + o(G.window.document.documentElement).equals(G.window.document.body.parentNode) + o(G.window.document.activeElement).equals(null) }) o("focus changes activeElement", function() { - var input = $document.createElement("input") - $document.body.appendChild(input) + var input = G.window.document.createElement("input") + G.window.document.body.appendChild(input) input.focus() - o($document.activeElement).equals(input) + o(G.window.document.activeElement).equals(input) - $document.body.removeChild(input) + G.window.document.body.removeChild(input) }) }) o.spec("style", function() { o("has style property", function() { - var div = $document.createElement("div") + var div = G.window.document.createElement("div") o(typeof div.style).equals("object") }) o("setting style.cssText string works", function() { - var div = $document.createElement("div") + var div = G.window.document.createElement("div") div.style.cssText = "background-color: red; border-bottom: 1px solid red;" o(div.style.backgroundColor).equals("red") @@ -621,7 +471,7 @@ o.spec("domMock", function() { o(div.attributes.style.value).equals("background-color: red; border-bottom: 1px solid red;") }) o("removing via setting style.cssText string works", function() { - var div = $document.createElement("div") + var div = G.window.document.createElement("div") div.style.cssText = "background: red;" div.style.cssText = "" @@ -629,7 +479,7 @@ o.spec("domMock", function() { o(div.attributes.style.value).equals("") }) o("the final semicolon is optional when setting style.cssText", function() { - var div = $document.createElement("div") + var div = G.window.document.createElement("div") div.style.cssText = "background: red" o(div.style.background).equals("red") @@ -637,13 +487,13 @@ o.spec("domMock", function() { o(div.attributes.style.value).equals("background: red;") }) o("'cssText' as a property name is ignored when setting style.cssText", function(){ - var div = $document.createElement("div") + var div = G.window.document.createElement("div") div.style.cssText = "cssText: red;" o(div.style.cssText).equals("") }) o("setting style.cssText that has a semi-colon in a strings", function(){ - var div = $document.createElement("div") + var div = G.window.document.createElement("div") div.style.cssText = "background: url(';'); font-family: \";\"" o(div.style.background).equals("url(';')") @@ -651,7 +501,7 @@ o.spec("domMock", function() { o(div.style.cssText).equals("background: url(';'); font-family: \";\";") }) o("comments in style.cssText are stripped", function(){ - var div = $document.createElement("div") + var div = G.window.document.createElement("div") div.style.cssText = "/**/background/*:*/: /*>;)*/red/**/;/**/" o(div.style.background).equals("red") @@ -659,14 +509,14 @@ o.spec("domMock", function() { }) o("comments in strings in style.cssText are preserved", function(){ - var div = $document.createElement("div") + var div = G.window.document.createElement("div") div.style.cssText = "background: url('/*foo*/')" o(div.style.background).equals("url('/*foo*/')") }) o("setting style updates style.cssText", function () { - var div = $document.createElement("div") + var div = G.window.document.createElement("div") div.style = "background: red;" o(div.style.background).equals("red") @@ -679,14 +529,14 @@ o.spec("domMock", function() { var spy, div, e o.beforeEach(function() { spy = o.spy() - div = $document.createElement("div") - e = $document.createEvent("MouseEvents") + div = G.window.document.createElement("div") + e = G.window.document.createEvent("MouseEvents") e.initEvent("click", true, true) - $document.body.appendChild(div) + G.window.document.body.appendChild(div) }) o.afterEach(function() { - $document.body.removeChild(div) + G.window.document.body.removeChild(div) }) o("has onclick", function() { @@ -737,18 +587,18 @@ o.spec("domMock", function() { }) o("removeEventListener only removes the handler related to a given phase (1/2)", function() { spy = o.spy(function(e) {o(e.eventPhase).equals(3)}) - $document.body.addEventListener("click", spy, true) - $document.body.addEventListener("click", spy, false) - $document.body.removeEventListener("click", spy, true) + G.window.document.body.addEventListener("click", spy, true) + G.window.document.body.addEventListener("click", spy, false) + G.window.document.body.removeEventListener("click", spy, true) div.dispatchEvent(e) o(spy.callCount).equals(1) }) o("removeEventListener only removes the handler related to a given phase (2/2)", function() { spy = o.spy(function(e) {o(e.eventPhase).equals(1)}) - $document.body.addEventListener("click", spy, true) - $document.body.addEventListener("click", spy, false) - $document.body.removeEventListener("click", spy, false) + G.window.document.body.addEventListener("click", spy, true) + G.window.document.body.addEventListener("click", spy, false) + G.window.document.body.removeEventListener("click", spy, false) div.dispatchEvent(e) o(spy.callCount).equals(1) @@ -771,14 +621,14 @@ o.spec("domMock", function() { var spy, div, e o.beforeEach(function() { spy = o.spy() - div = $document.createElement("div") - e = $document.createEvent("HTMLEvents") + div = G.window.document.createElement("div") + e = G.window.document.createEvent("HTMLEvents") e.initEvent("transitionend", true, true) - $document.body.appendChild(div) + G.window.document.body.appendChild(div) }) o.afterEach(function() { - $document.body.removeChild(div) + G.window.document.body.removeChild(div) }) o("ontransitionend does not fire", function(done) { @@ -792,14 +642,14 @@ o.spec("domMock", function() { o.spec("capture and bubbling phases", function() { var div, e o.beforeEach(function() { - div = $document.createElement("div") - e = $document.createEvent("MouseEvents") + div = G.window.document.createElement("div") + e = G.window.document.createEvent("MouseEvents") e.initEvent("click", true, true) - $document.body.appendChild(div) + G.window.document.body.appendChild(div) }) o.afterEach(function() { - $document.body.removeChild(div) + G.window.document.body.removeChild(div) }) o("capture and bubbling events both fire on the target in the order they were defined, regardless of the phase", function () { var sequence = [] @@ -836,7 +686,7 @@ o.spec("domMock", function() { o(ev).equals(e) o(ev.eventPhase).equals(1) o(ev.target).equals(div) - o(ev.currentTarget).equals($document.body) + o(ev.currentTarget).equals(G.window.document.body) }) var bubble = o.spy(function(ev){ sequence.push("bubble") @@ -844,11 +694,11 @@ o.spec("domMock", function() { o(ev).equals(e) o(ev.eventPhase).equals(3) o(ev.target).equals(div) - o(ev.currentTarget).equals($document.body) + o(ev.currentTarget).equals(G.window.document.body) }) - $document.body.addEventListener("click", bubble, false) - $document.body.addEventListener("click", capture, true) + G.window.document.body.addEventListener("click", bubble, false) + G.window.document.body.addEventListener("click", capture, true) div.dispatchEvent(e) o(capture.callCount).equals(1) @@ -863,7 +713,7 @@ o.spec("domMock", function() { o(ev).equals(e) o(ev.eventPhase).equals(3) o(ev.target).equals(div) - o(ev.currentTarget).equals($document.body) + o(ev.currentTarget).equals(G.window.document.body) }) var target = o.spy(function(ev){ sequence.push("target") @@ -874,7 +724,7 @@ o.spec("domMock", function() { o(ev.currentTarget).equals(div) }) - $document.body.addEventListener("click", parent) + G.window.document.body.addEventListener("click", parent) div.addEventListener("click", target) div.dispatchEvent(e) @@ -890,7 +740,7 @@ o.spec("domMock", function() { o(ev).equals(e) o(ev.eventPhase).equals(3) o(ev.target).equals(div) - o(ev.currentTarget).equals($document.body) + o(ev.currentTarget).equals(G.window.document.body) }) var target = o.spy(function(ev){ sequence.push("target") @@ -901,8 +751,8 @@ o.spec("domMock", function() { o(ev.currentTarget).equals(div) }) - $document.body.addEventListener("click", parent) - $document.body.onclick = parent + G.window.document.body.addEventListener("click", parent) + G.window.document.body.onclick = parent div.addEventListener("click", target) div.dispatchEvent(e) @@ -914,15 +764,15 @@ o.spec("domMock", function() { var target = o.spy(function(ev){ o(ev).equals(e) o(ev.eventPhase).equals(2) - o(ev.target).equals($document.body) - o(ev.currentTarget).equals($document.body) + o(ev.target).equals(G.window.document.body) + o(ev.currentTarget).equals(G.window.document.body) }) var child = o.spy(function(){ }) - $document.body.addEventListener("click", target) + G.window.document.body.addEventListener("click", target) div.addEventListener("click", child) - $document.body.dispatchEvent(e) + G.window.document.body.dispatchEvent(e) o(target.callCount).equals(1) o(child.callCount).equals(0) @@ -935,9 +785,9 @@ o.spec("domMock", function() { var bubParent = o.spy() var legacyParent = o.spy() - $document.body.addEventListener("click", capParent, true) - $document.body.addEventListener("click", bubParent, false) - $document.body.onclick = legacyParent + G.window.document.body.addEventListener("click", capParent, true) + G.window.document.body.addEventListener("click", bubParent, false) + G.window.document.body.onclick = legacyParent div.addEventListener("click", capTarget, true) div.addEventListener("click", bubTarget, false) @@ -960,9 +810,9 @@ o.spec("domMock", function() { var bubParent = o.spy() var legacyParent = o.spy() - $document.body.addEventListener("click", capParent, true) - $document.body.addEventListener("click", bubParent, false) - $document.body.onclick = legacyParent + G.window.document.body.addEventListener("click", capParent, true) + G.window.document.body.addEventListener("click", bubParent, false) + G.window.document.body.onclick = legacyParent div.addEventListener("click", capTarget, true) div.addEventListener("click", bubTarget, false) @@ -986,9 +836,9 @@ o.spec("domMock", function() { var bubParent = o.spy() var legacyParent = o.spy() - $document.body.addEventListener("click", capParent, true) - $document.body.addEventListener("click", bubParent, false) - $document.body.onclick = legacyParent + G.window.document.body.addEventListener("click", capParent, true) + G.window.document.body.addEventListener("click", bubParent, false) + G.window.document.body.onclick = legacyParent div.addEventListener("click", capTarget, true) div.addEventListener("click", bubTarget, false) @@ -1011,9 +861,9 @@ o.spec("domMock", function() { var bubParent = o.spy() var legacyParent = o.spy() - $document.body.addEventListener("click", capParent, true) - $document.body.addEventListener("click", bubParent, false) - $document.body.onclick = legacyParent + G.window.document.body.addEventListener("click", capParent, true) + G.window.document.body.addEventListener("click", bubParent, false) + G.window.document.body.onclick = legacyParent div.addEventListener("click", capTarget, true) div.addEventListener("click", bubTarget, false) @@ -1036,9 +886,9 @@ o.spec("domMock", function() { var bubParent = o.spy(function(e){e.stopPropagation()}) var legacyParent = o.spy() - $document.body.addEventListener("click", capParent, true) - $document.body.addEventListener("click", bubParent, false) - $document.body.onclick = legacyParent + G.window.document.body.addEventListener("click", capParent, true) + G.window.document.body.addEventListener("click", bubParent, false) + G.window.document.body.onclick = legacyParent div.addEventListener("click", capTarget, true) div.addEventListener("click", bubTarget, false) @@ -1061,9 +911,9 @@ o.spec("domMock", function() { var bubParent = o.spy() var legacyParent = o.spy(function(e){e.stopPropagation()}) - $document.body.addEventListener("click", capParent, true) - $document.body.addEventListener("click", bubParent, false) - $document.body.onclick = legacyParent + G.window.document.body.addEventListener("click", capParent, true) + G.window.document.body.addEventListener("click", bubParent, false) + G.window.document.body.onclick = legacyParent div.addEventListener("click", capTarget, true) div.addEventListener("click", bubTarget, false) @@ -1086,9 +936,9 @@ o.spec("domMock", function() { var bubParent = o.spy() var legacyParent = o.spy() - $document.body.addEventListener("click", capParent, true) - $document.body.addEventListener("click", bubParent, false) - $document.body.onclick = legacyParent + G.window.document.body.addEventListener("click", capParent, true) + G.window.document.body.addEventListener("click", bubParent, false) + G.window.document.body.onclick = legacyParent div.addEventListener("click", capTarget, true) div.addEventListener("click", bubTarget, false) @@ -1111,9 +961,9 @@ o.spec("domMock", function() { var bubParent = o.spy() var legacyParent = o.spy() - $document.body.addEventListener("click", capParent, true) - $document.body.addEventListener("click", bubParent, false) - $document.body.onclick = legacyParent + G.window.document.body.addEventListener("click", capParent, true) + G.window.document.body.addEventListener("click", bubParent, false) + G.window.document.body.onclick = legacyParent div.addEventListener("click", capTarget, true) div.addEventListener("click", bubTarget, false) @@ -1137,9 +987,9 @@ o.spec("domMock", function() { var bubParent = o.spy() var legacyParent = o.spy() - $document.body.addEventListener("click", capParent, true) - $document.body.addEventListener("click", bubParent, false) - $document.body.onclick = legacyParent + G.window.document.body.addEventListener("click", capParent, true) + G.window.document.body.addEventListener("click", bubParent, false) + G.window.document.body.onclick = legacyParent div.addEventListener("click", capTarget, true) div.addEventListener("click", bubTarget, false) @@ -1162,9 +1012,9 @@ o.spec("domMock", function() { var bubParent = o.spy() var legacyParent = o.spy() - $document.body.addEventListener("click", capParent, true) - $document.body.addEventListener("click", bubParent, false) - $document.body.onclick = legacyParent + G.window.document.body.addEventListener("click", capParent, true) + G.window.document.body.addEventListener("click", bubParent, false) + G.window.document.body.onclick = legacyParent div.addEventListener("click", capTarget, true) div.addEventListener("click", bubTarget, false) @@ -1187,9 +1037,9 @@ o.spec("domMock", function() { var bubParent = o.spy(function(e){e.stopImmediatePropagation()}) var legacyParent = o.spy() - $document.body.addEventListener("click", capParent, true) - $document.body.addEventListener("click", bubParent, false) - $document.body.onclick = legacyParent + G.window.document.body.addEventListener("click", capParent, true) + G.window.document.body.addEventListener("click", bubParent, false) + G.window.document.body.onclick = legacyParent div.addEventListener("click", capTarget, true) div.addEventListener("click", bubTarget, false) @@ -1212,9 +1062,9 @@ o.spec("domMock", function() { var bubParent = o.spy() var legacyParent = o.spy(function(e){e.stopImmediatePropagation()}) - $document.body.addEventListener("click", capParent, true) - $document.body.addEventListener("click", bubParent, false) - $document.body.onclick = legacyParent + G.window.document.body.addEventListener("click", capParent, true) + G.window.document.body.addEventListener("click", bubParent, false) + G.window.document.body.onclick = legacyParent div.addEventListener("click", capTarget, true) div.addEventListener("click", bubTarget, false) @@ -1229,13 +1079,14 @@ o.spec("domMock", function() { o(bubParent.callCount).equals(1) o(legacyParent.callCount).equals(1) }) - o("errors thrown in handlers don't interrupt the chain", function(done) { - var errMsg = "The presence of these six errors in the log is expected in non-Node.js environments" - var handler = o.spy(function(){throw errMsg}) + o("errors thrown in handlers don't interrupt the chain", function() { + var handler = o.spy(() => {throw "fail"}) - $document.body.addEventListener("click", handler, true) - $document.body.addEventListener("click", handler, false) - $document.body.onclick = handler + console.error = o.spy() + + G.window.document.body.addEventListener("click", handler, true) + G.window.document.body.addEventListener("click", handler, false) + G.window.document.body.onclick = handler div.addEventListener("click", handler, true) div.addEventListener("click", handler, false) @@ -1245,57 +1096,37 @@ o.spec("domMock", function() { o(handler.callCount).equals(6) - // Swallow the async errors in NodeJS - if (typeof process !== "undefined" && typeof process.once === "function"){ - process.once("uncaughtException", function(e) { - if (e !== errMsg) throw e - process.once("uncaughtException", function(e) { - if (e !== errMsg) throw e - process.once("uncaughtException", function(e) { - if (e !== errMsg) throw e - process.once("uncaughtException", function(e) { - if (e !== errMsg) throw e - process.once("uncaughtException", function(e) { - if (e !== errMsg) throw e - process.once("uncaughtException", function(e) { - if (e !== errMsg) throw e - done() - }) - }) - }) - }) - }) - }) - } else { - done() - } + o(console.error.calls.map((c) => c.args)).deepEquals([ + ["fail"], ["fail"], ["fail"], + ["fail"], ["fail"], ["fail"], + ]) }) }) }) o.spec("attributes", function() { o.spec("a[href]", function() { o("is empty string if no attribute", function() { - var a = $document.createElement("a") + var a = G.window.document.createElement("a") o(a.href).equals("") o(a.attributes["href"]).equals(undefined) }) o("is path if attribute is set", function() { - var a = $document.createElement("a") + var a = G.window.document.createElement("a") a.setAttribute("href", "") o(a.href).notEquals("") o(a.attributes["href"].value).equals("") }) o("is path if property is set", function() { - var a = $document.createElement("a") + var a = G.window.document.createElement("a") a.href = "" o(a.href).notEquals("") o(a.attributes["href"].value).equals("") }) o("property is read-only for SVG elements", function() { - var a = $document.createElementNS("http://www.w3.org/2000/svg", "a") + var a = G.window.document.createElementNS("http://www.w3.org/2000/svg", "a") a.href = "/foo" o(a.href).deepEquals({baseVal: "", animVal: ""}) @@ -1304,14 +1135,14 @@ o.spec("domMock", function() { }) o.spec("input[checked]", function() { o("only exists in input elements", function() { - var input = $document.createElement("input") - var a = $document.createElement("a") + var input = G.window.document.createElement("input") + var a = G.window.document.createElement("a") o("checked" in input).equals(true) o("checked" in a).equals(false) }) o("tracks attribute value when unset", function() { - var input = $document.createElement("input") + var input = G.window.document.createElement("input") input.setAttribute("type", "checkbox") o(input.checked).equals(false) @@ -1328,7 +1159,7 @@ o.spec("domMock", function() { o(input.attributes["checked"]).equals(undefined) }) o("does not track attribute value when set", function() { - var input = $document.createElement("input") + var input = G.window.document.createElement("input") input.setAttribute("type", "checkbox") input.checked = true @@ -1344,23 +1175,23 @@ o.spec("domMock", function() { o(input.checked).equals(true) }) o("toggles on click", function() { - var input = $document.createElement("input") + var input = G.window.document.createElement("input") input.setAttribute("type", "checkbox") input.checked = false - var e = $document.createEvent("MouseEvents") + var e = G.window.document.createEvent("MouseEvents") e.initEvent("click", true, true) input.dispatchEvent(e) o(input.checked).equals(true) }) o("doesn't toggle on click when preventDefault() is used", function() { - var input = $document.createElement("input") + var input = G.window.document.createElement("input") input.setAttribute("type", "checkbox") input.checked = false input.onclick = function(e) {e.preventDefault()} - var e = $document.createEvent("MouseEvents") + var e = G.window.document.createEvent("MouseEvents") e.initEvent("click", true, true) input.dispatchEvent(e) @@ -1369,14 +1200,14 @@ o.spec("domMock", function() { }) o.spec("input[value]", function() { o("only exists in input elements", function() { - var input = $document.createElement("input") - var a = $document.createElement("a") + var input = G.window.document.createElement("input") + var a = G.window.document.createElement("a") o("value" in input).equals(true) o("value" in a).equals(false) }) o("converts null to ''", function() { - var input = $document.createElement("input") + var input = G.window.document.createElement("input") input.value = "x" o(input.value).equals("x") @@ -1386,7 +1217,7 @@ o.spec("domMock", function() { o(input.value).equals("") }) o("converts values to strings", function() { - var input = $document.createElement("input") + var input = G.window.document.createElement("input") input.value = 5 o(input.value).equals("5") @@ -1401,7 +1232,7 @@ o.spec("domMock", function() { }) if (typeof Symbol === "function") o("throws when set to a symbol", function() { var threw = false - var input = $document.createElement("input") + var input = G.window.document.createElement("input") try { input.value = Symbol("") } catch (e) { @@ -1415,28 +1246,28 @@ o.spec("domMock", function() { }) o.spec("input[type]", function(){ o("only exists in input elements", function() { - var input = $document.createElement("input") - var a = $document.createElement("a") + var input = G.window.document.createElement("input") + var a = G.window.document.createElement("a") o("type" in input).equals(true) o("type" in a).equals(false) }) o("is 'text' by default", function() { - var input = $document.createElement("input") + var input = G.window.document.createElement("input") o(input.type).equals("text") }) "radio|button|checkbox|color|date|datetime|datetime-local|email|file|hidden|month|number|password|range|research|search|submit|tel|text|url|week|image" .split("|").forEach(function(type) { o("can be set to " + type, function(){ - var input = $document.createElement("input") + var input = G.window.document.createElement("input") input.type = type o(input.getAttribute("type")).equals(type) o(input.type).equals(type) }) o("bad values set the attribute, but the getter corrects to 'text', " + type, function(){ - var input = $document.createElement("input") + var input = G.window.document.createElement("input") input.type = "badbad" + type o(input.getAttribute("type")).equals("badbad" + type) @@ -1446,20 +1277,20 @@ o.spec("domMock", function() { }) o.spec("textarea[value]", function() { o("reads from child if no value was ever set", function() { - var textarea = $document.createElement("textarea") - textarea.appendChild($document.createTextNode("aaa")) + var textarea = G.window.document.createElement("textarea") + textarea.appendChild(G.window.document.createTextNode("aaa")) o(textarea.value).equals("aaa") }) o("ignores child if value set", function() { - var textarea = $document.createElement("textarea") + var textarea = G.window.document.createElement("textarea") textarea.value = null - textarea.appendChild($document.createTextNode("aaa")) + textarea.appendChild(G.window.document.createTextNode("aaa")) o(textarea.value).equals("") }) o("textarea[value] doesn't reflect `attributes.value`", function() { - var textarea = $document.createElement("textarea") + var textarea = G.window.document.createElement("textarea") textarea.value = "aaa" textarea.setAttribute("value", "bbb") @@ -1468,8 +1299,8 @@ o.spec("domMock", function() { }) o.spec("select[value] and select[selectedIndex]", function() { o("only exist in select elements", function() { - var select = $document.createElement("select") - var a = $document.createElement("a") + var select = G.window.document.createElement("select") + var a = G.window.document.createElement("a") o("value" in select).equals(true) o("value" in a).equals(false) @@ -1478,13 +1309,13 @@ o.spec("domMock", function() { o("selectedIndex" in a).equals(false) }) o("value defaults to value at first index", function() { - var select = $document.createElement("select") + var select = G.window.document.createElement("select") - var option1 = $document.createElement("option") + var option1 = G.window.document.createElement("option") option1.setAttribute("value", "a") select.appendChild(option1) - var option2 = $document.createElement("option") + var option2 = G.window.document.createElement("option") option2.setAttribute("value", "b") select.appendChild(option2) @@ -1492,12 +1323,12 @@ o.spec("domMock", function() { o(select.selectedIndex).equals(0) }) o("value falls back to child nodeValue if no attribute", function() { - var select = $document.createElement("select") + var select = G.window.document.createElement("select") - var option1 = $document.createElement("option") - option1.appendChild($document.createTextNode("a")) - var option2 = $document.createElement("option") - option2.appendChild($document.createTextNode("b")) + var option1 = G.window.document.createElement("option") + option1.appendChild(G.window.document.createTextNode("a")) + var option2 = G.window.document.createElement("option") + option2.appendChild(G.window.document.createTextNode("b")) select.appendChild(option1) select.appendChild(option2) @@ -1508,27 +1339,27 @@ o.spec("domMock", function() { o(select.childNodes[1].value).equals("b") }) o("value defaults to invalid if no options", function() { - var select = $document.createElement("select") + var select = G.window.document.createElement("select") o(select.value).equals("") o(select.selectedIndex).equals(-1) }) o("setting valid value works", function() { - var select = $document.createElement("select") + var select = G.window.document.createElement("select") - var option1 = $document.createElement("option") + var option1 = G.window.document.createElement("option") option1.setAttribute("value", "a") select.appendChild(option1) - var option2 = $document.createElement("option") + var option2 = G.window.document.createElement("option") option2.setAttribute("value", "b") select.appendChild(option2) - var option3 = $document.createElement("option") + var option3 = G.window.document.createElement("option") option3.setAttribute("value", "") select.appendChild(option3) - var option4 = $document.createElement("option") + var option4 = G.window.document.createElement("option") option4.setAttribute("value", "null") select.appendChild(option4) @@ -1553,17 +1384,17 @@ o.spec("domMock", function() { o(select.selectedIndex).equals(-1) }) o("setting valid value works with type conversion", function() { - var select = $document.createElement("select") + var select = G.window.document.createElement("select") - var option1 = $document.createElement("option") + var option1 = G.window.document.createElement("option") option1.setAttribute("value", "0") select.appendChild(option1) - var option2 = $document.createElement("option") + var option2 = G.window.document.createElement("option") option2.setAttribute("value", "undefined") select.appendChild(option2) - var option3 = $document.createElement("option") + var option3 = G.window.document.createElement("option") option3.setAttribute("value", "") select.appendChild(option3) @@ -1590,24 +1421,24 @@ o.spec("domMock", function() { } }) o("option.value = null is converted to 'null'", function() { - var option = $document.createElement("option") + var option = G.window.document.createElement("option") option.value = null o(option.value).equals("null") }) o("setting valid value works with optgroup", function() { - var select = $document.createElement("select") + var select = G.window.document.createElement("select") - var option1 = $document.createElement("option") + var option1 = G.window.document.createElement("option") option1.setAttribute("value", "a") - var option2 = $document.createElement("option") + var option2 = G.window.document.createElement("option") option2.setAttribute("value", "b") - var option3 = $document.createElement("option") + var option3 = G.window.document.createElement("option") option3.setAttribute("value", "c") - var optgroup = $document.createElement("optgroup") + var optgroup = G.window.document.createElement("optgroup") optgroup.appendChild(option1) optgroup.appendChild(option2) select.appendChild(optgroup) @@ -1619,13 +1450,13 @@ o.spec("domMock", function() { o(select.selectedIndex).equals(1) }) o("setting valid selectedIndex works", function() { - var select = $document.createElement("select") + var select = G.window.document.createElement("select") - var option1 = $document.createElement("option") + var option1 = G.window.document.createElement("option") option1.setAttribute("value", "a") select.appendChild(option1) - var option2 = $document.createElement("option") + var option2 = G.window.document.createElement("option") option2.setAttribute("value", "b") select.appendChild(option2) @@ -1635,13 +1466,13 @@ o.spec("domMock", function() { o(select.selectedIndex).equals(1) }) o("setting option[selected] works", function() { - var select = $document.createElement("select") + var select = G.window.document.createElement("select") - var option1 = $document.createElement("option") + var option1 = G.window.document.createElement("option") option1.setAttribute("value", "a") select.appendChild(option1) - var option2 = $document.createElement("option") + var option2 = G.window.document.createElement("option") option2.setAttribute("value", "b") select.appendChild(option2) @@ -1651,13 +1482,13 @@ o.spec("domMock", function() { o(select.selectedIndex).equals(1) }) o("unsetting option[selected] works", function() { - var select = $document.createElement("select") + var select = G.window.document.createElement("select") - var option1 = $document.createElement("option") + var option1 = G.window.document.createElement("option") option1.setAttribute("value", "a") select.appendChild(option1) - var option2 = $document.createElement("option") + var option2 = G.window.document.createElement("option") option2.setAttribute("value", "b") select.appendChild(option2) @@ -1668,13 +1499,13 @@ o.spec("domMock", function() { o(select.selectedIndex).equals(0) }) o("setting invalid value yields a selectedIndex of -1 and value of empty string", function() { - var select = $document.createElement("select") + var select = G.window.document.createElement("select") - var option1 = $document.createElement("option") + var option1 = G.window.document.createElement("option") option1.setAttribute("value", "a") select.appendChild(option1) - var option2 = $document.createElement("option") + var option2 = G.window.document.createElement("option") option2.setAttribute("value", "b") select.appendChild(option2) @@ -1684,13 +1515,13 @@ o.spec("domMock", function() { o(select.selectedIndex).equals(-1) }) o("setting invalid selectedIndex yields a selectedIndex of -1 and value of empty string", function() { - var select = $document.createElement("select") + var select = G.window.document.createElement("select") - var option1 = $document.createElement("option") + var option1 = G.window.document.createElement("option") option1.setAttribute("value", "a") select.appendChild(option1) - var option2 = $document.createElement("option") + var option2 = G.window.document.createElement("option") option2.setAttribute("value", "b") select.appendChild(option2) @@ -1700,13 +1531,13 @@ o.spec("domMock", function() { o(select.selectedIndex).equals(-1) }) o("setting invalid value yields a selectedIndex of -1 and value of empty string even when there's an option whose value is empty string", function() { - var select = $document.createElement("select") + var select = G.window.document.createElement("select") - var option1 = $document.createElement("option") + var option1 = G.window.document.createElement("option") option1.setAttribute("value", "a") select.appendChild(option1) - var option2 = $document.createElement("option") + var option2 = G.window.document.createElement("option") option2.setAttribute("value", "") select.appendChild(option2) @@ -1716,13 +1547,13 @@ o.spec("domMock", function() { o(select.selectedIndex).equals(-1) }) o("setting invalid selectedIndex yields a selectedIndex of -1 and value of empty string even when there's an option whose value is empty string", function() { - var select = $document.createElement("select") + var select = G.window.document.createElement("select") - var option1 = $document.createElement("option") + var option1 = G.window.document.createElement("option") option1.setAttribute("value", "a") select.appendChild(option1) - var option2 = $document.createElement("option") + var option2 = G.window.document.createElement("option") option2.setAttribute("value", "") select.appendChild(option2) @@ -1734,7 +1565,7 @@ o.spec("domMock", function() { }) o.spec("canvas width and height", function() { o("setting property works", function() { - var canvas = $document.createElement("canvas") + var canvas = G.window.document.createElement("canvas") canvas.width = 100 o(canvas.attributes["width"].value).equals("100") @@ -1745,7 +1576,7 @@ o.spec("domMock", function() { o(canvas.height).equals(100) }) o("setting string casts to number", function() { - var canvas = $document.createElement("canvas") + var canvas = G.window.document.createElement("canvas") canvas.width = "100" o(canvas.attributes["width"].value).equals("100") @@ -1756,7 +1587,7 @@ o.spec("domMock", function() { o(canvas.height).equals(100) }) o("setting float casts to int", function() { - var canvas = $document.createElement("canvas") + var canvas = G.window.document.createElement("canvas") canvas.width = 1.2 o(canvas.attributes["width"].value).equals("1") @@ -1767,7 +1598,7 @@ o.spec("domMock", function() { o(canvas.height).equals(1) }) o("setting percentage fails", function() { - var canvas = $document.createElement("canvas") + var canvas = G.window.document.createElement("canvas") canvas.width = "100%" o(canvas.attributes["width"].value).equals("0") @@ -1778,7 +1609,7 @@ o.spec("domMock", function() { o(canvas.height).equals(0) }) o("setting attribute works", function() { - var canvas = $document.createElement("canvas") + var canvas = G.window.document.createElement("canvas") canvas.setAttribute("width", "100%") o(canvas.attributes["width"].value).equals("100%") @@ -1792,14 +1623,14 @@ o.spec("domMock", function() { }) o.spec("className", function() { o("works", function() { - var el = $document.createElement("div") + var el = G.window.document.createElement("div") el.className = "a" o(el.className).equals("a") o(el.attributes["class"].value).equals("a") }) o("setter throws in svg", function(done) { - var el = $document.createElementNS("http://www.w3.org/2000/svg", "svg") + var el = G.window.document.createElementNS("http://www.w3.org/2000/svg", "svg") try { el.className = "a" } @@ -1809,18 +1640,18 @@ o.spec("domMock", function() { }) }) o.spec("spies", function() { - var $window o.beforeEach(function() { - $window = domMock({spy: o.spy}) + G.initialize({spy: o.spy}) }) o("basics", function() { - o(typeof $window.__getSpies).equals("function") - o("__getSpies" in domMock()).equals(false) + o(typeof G.window.__getSpies).equals("function") + G.initialize() + o("__getSpies" in G.window).equals(false) }) o("input elements have spies on value setters", function() { - var input = $window.document.createElement("input") + var input = G.window.document.createElement("input") - var spies = $window.__getSpies(input) + var spies = G.window.__getSpies(input) o(typeof spies).equals("object") o(spies).notEquals(null) @@ -1834,9 +1665,9 @@ o.spec("domMock", function() { o(spies.valueSetter.args[0]).equals("aaa") }) o("select elements have spies on value setters", function() { - var select = $window.document.createElement("select") + var select = G.window.document.createElement("select") - var spies = $window.__getSpies(select) + var spies = G.window.__getSpies(select) o(typeof spies).equals("object") o(spies).notEquals(null) @@ -1850,9 +1681,9 @@ o.spec("domMock", function() { o(spies.valueSetter.args[0]).equals("aaa") }) o("option elements have spies on value setters", function() { - var option = $window.document.createElement("option") + var option = G.window.document.createElement("option") - var spies = $window.__getSpies(option) + var spies = G.window.__getSpies(option) o(typeof spies).equals("object") o(spies).notEquals(null) @@ -1866,9 +1697,9 @@ o.spec("domMock", function() { o(spies.valueSetter.args[0]).equals("aaa") }) o("textarea elements have spies on value setters", function() { - var textarea = $window.document.createElement("textarea") + var textarea = G.window.document.createElement("textarea") - var spies = $window.__getSpies(textarea) + var spies = G.window.__getSpies(textarea) o(typeof spies).equals("object") o(spies).notEquals(null) @@ -1882,62 +1713,4 @@ o.spec("domMock", function() { o(spies.valueSetter.args[0]).equals("aaa") }) }) - o.spec("DOMParser for SVG", function(){ - var $DOMParser - o.beforeEach(function() { - $DOMParser = $window.DOMParser - }) - o("basics", function(){ - o(typeof $DOMParser).equals("function") - - var parser = new $DOMParser() - - o(parser instanceof $DOMParser).equals(true) - o(typeof parser.parseFromString).equals("function") - }) - o("empty document", function() { - var parser = new $DOMParser() - var doc = parser.parseFromString( - "", - "image/svg+xml" - ) - - o(typeof doc.documentElement).notEquals(undefined) - o(doc.documentElement.nodeName).equals("svg") - o(doc.documentElement.namespaceURI).equals("http://www.w3.org/2000/svg") - o(doc.documentElement.childNodes.length).equals(0) - }) - o("text elements", function() { - var parser = new $DOMParser() - var doc = parser.parseFromString( - "" - + "hello" - + " " - + "world" - + "", - "image/svg+xml" - ) - - o(doc.documentElement.nodeName).equals("svg") - o(doc.documentElement.namespaceURI).equals("http://www.w3.org/2000/svg") - - var nodes = doc.documentElement.childNodes - o(nodes.length).equals(3) - o(nodes[0].nodeName).equals("text") - o(nodes[0].namespaceURI).equals("http://www.w3.org/2000/svg") - o(nodes[0].childNodes.length).equals(1) - o(nodes[0].childNodes[0].nodeName).equals("#text") - o(nodes[0].childNodes[0].nodeValue).equals("hello") - o(nodes[1].nodeName).equals("text") - o(nodes[1].namespaceURI).equals("http://www.w3.org/2000/svg") - o(nodes[1].childNodes.length).equals(1) - o(nodes[1].childNodes[0].nodeName).equals("#text") - o(nodes[1].childNodes[0].nodeValue).equals(" ") - o(nodes[2].nodeName).equals("text") - o(nodes[2].namespaceURI).equals("http://www.w3.org/2000/svg") - o(nodes[2].childNodes.length).equals(1) - o(nodes[2].childNodes[0].nodeName).equals("#text") - o(nodes[2].childNodes[0].nodeValue).equals("world") - }) - }) }) diff --git a/tests/test-utils/pushStateMock.js b/tests/test-utils/pushStateMock.js index 60da3c167..413dce4a2 100644 --- a/tests/test-utils/pushStateMock.js +++ b/tests/test-utils/pushStateMock.js @@ -1,174 +1,171 @@ import o from "ospec" import {callAsync, waitAsync} from "../../test-utils/callAsync.js" -import pushStateMock from "../../test-utils/pushStateMock.js" +import {setupGlobals} from "../../test-utils/global.js" o.spec("pushStateMock", function() { - var $window - o.beforeEach(function() { - $window = pushStateMock() - }) + var G = setupGlobals() o.spec("initial state", function() { o("has url on page load", function() { - o($window.location.href).equals("http://localhost/") + o(G.window.location.href).equals("http://localhost/") }) }) o.spec("set href", function() { o("changes url on location.href change", function() { - var old = $window.location.href - $window.location.href = "http://localhost/a" + var old = G.window.location.href + G.window.location.href = "http://localhost/a" o(old).equals("http://localhost/") - o($window.location.href).equals("http://localhost/a") + o(G.window.location.href).equals("http://localhost/a") }) o("changes url on relative location.href change", function() { - var old = $window.location.href - $window.location.href = "a" + var old = G.window.location.href + G.window.location.href = "a" o(old).equals("http://localhost/") - o($window.location.href).equals("http://localhost/a") - o($window.location.pathname).equals("/a") + o(G.window.location.href).equals("http://localhost/a") + o(G.window.location.pathname).equals("/a") }) o("changes url on dotdot location.href change", function() { - $window.location.href = "a" - var old = $window.location.href - $window.location.href = ".." + G.window.location.href = "a" + var old = G.window.location.href + G.window.location.href = ".." o(old).equals("http://localhost/a") - o($window.location.href).equals("http://localhost/") - o($window.location.pathname).equals("/") + o(G.window.location.href).equals("http://localhost/") + o(G.window.location.pathname).equals("/") }) o("changes url on deep dotdot location.href change", function() { - $window.location.href = "a/b/c" - var old = $window.location.href - $window.location.href = ".." + G.window.location.href = "a/b/c" + var old = G.window.location.href + G.window.location.href = ".." o(old).equals("http://localhost/a/b/c") - o($window.location.href).equals("http://localhost/a") - o($window.location.pathname).equals("/a") + o(G.window.location.href).equals("http://localhost/a") + o(G.window.location.pathname).equals("/a") }) o("does not change url on dotdot location.href change from root", function() { - var old = $window.location.href - $window.location.href = ".." + var old = G.window.location.href + G.window.location.href = ".." o(old).equals("http://localhost/") - o($window.location.href).equals("http://localhost/") - o($window.location.pathname).equals("/") + o(G.window.location.href).equals("http://localhost/") + o(G.window.location.pathname).equals("/") }) o("changes url on dot relative location.href change", function() { - var old = $window.location.href - $window.location.href = "a" - $window.location.href = "./b" + var old = G.window.location.href + G.window.location.href = "a" + G.window.location.href = "./b" o(old).equals("http://localhost/") - o($window.location.href).equals("http://localhost/b") - o($window.location.pathname).equals("/b") + o(G.window.location.href).equals("http://localhost/b") + o(G.window.location.pathname).equals("/b") }) o("does not change url on dot location.href change", function() { - var old = $window.location.href - $window.location.href = "a" - $window.location.href = "." + var old = G.window.location.href + G.window.location.href = "a" + G.window.location.href = "." o(old).equals("http://localhost/") - o($window.location.href).equals("http://localhost/a") - o($window.location.pathname).equals("/a") + o(G.window.location.href).equals("http://localhost/a") + o(G.window.location.pathname).equals("/a") }) o("changes url on hash-only location.href change", function() { - var old = $window.location.href - $window.location.href = "#a" + var old = G.window.location.href + G.window.location.href = "#a" o(old).equals("http://localhost/") - o($window.location.href).equals("http://localhost/#a") - o($window.location.hash).equals("#a") + o(G.window.location.href).equals("http://localhost/#a") + o(G.window.location.hash).equals("#a") }) o("changes url on search-only location.href change", function() { - var old = $window.location.href - $window.location.href = "?a" + var old = G.window.location.href + G.window.location.href = "?a" o(old).equals("http://localhost/") - o($window.location.href).equals("http://localhost/?a") - o($window.location.search).equals("?a") + o(G.window.location.href).equals("http://localhost/?a") + o(G.window.location.search).equals("?a") }) o("changes hash on location.href change", function() { - var old = $window.location.href - $window.location.href = "http://localhost/a#b" + var old = G.window.location.href + G.window.location.href = "http://localhost/a#b" o(old).equals("http://localhost/") - o($window.location.href).equals("http://localhost/a#b") - o($window.location.hash).equals("#b") + o(G.window.location.href).equals("http://localhost/a#b") + o(G.window.location.hash).equals("#b") }) o("changes search on location.href change", function() { - var old = $window.location.href - $window.location.href = "http://localhost/a?b" + var old = G.window.location.href + G.window.location.href = "http://localhost/a?b" o(old).equals("http://localhost/") - o($window.location.href).equals("http://localhost/a?b") - o($window.location.search).equals("?b") + o(G.window.location.href).equals("http://localhost/a?b") + o(G.window.location.search).equals("?b") }) o("changes search and hash on location.href change", function() { - var old = $window.location.href - $window.location.href = "http://localhost/a?b#c" + var old = G.window.location.href + G.window.location.href = "http://localhost/a?b#c" o(old).equals("http://localhost/") - o($window.location.href).equals("http://localhost/a?b#c") - o($window.location.search).equals("?b") - o($window.location.hash).equals("#c") + o(G.window.location.href).equals("http://localhost/a?b#c") + o(G.window.location.search).equals("?b") + o(G.window.location.hash).equals("#c") }) o("handles search with search and hash", function() { - var old = $window.location.href - $window.location.href = "http://localhost/a?b?c#d" + var old = G.window.location.href + G.window.location.href = "http://localhost/a?b?c#d" o(old).equals("http://localhost/") - o($window.location.href).equals("http://localhost/a?b?c#d") - o($window.location.search).equals("?b?c") - o($window.location.hash).equals("#d") + o(G.window.location.href).equals("http://localhost/a?b?c#d") + o(G.window.location.search).equals("?b?c") + o(G.window.location.hash).equals("#d") }) o("handles hash with search and hash", function() { - var old = $window.location.href - $window.location.href = "http://localhost/a#b?c#d" + var old = G.window.location.href + G.window.location.href = "http://localhost/a#b?c#d" o(old).equals("http://localhost/") - o($window.location.href).equals("http://localhost/a#b?c#d") - o($window.location.search).equals("") - o($window.location.hash).equals("#b?c#d") + o(G.window.location.href).equals("http://localhost/a#b?c#d") + o(G.window.location.search).equals("") + o(G.window.location.hash).equals("#b?c#d") }) }) o.spec("set search", function() { o("changes url on location.search change", function() { - var old = $window.location.href - $window.location.search = "?b" + var old = G.window.location.href + G.window.location.search = "?b" o(old).equals("http://localhost/") - o($window.location.href).equals("http://localhost/?b") - o($window.location.search).equals("?b") + o(G.window.location.href).equals("http://localhost/?b") + o(G.window.location.search).equals("?b") }) }) o.spec("set hash", function() { o("changes url on location.hash change", function() { - var old = $window.location.href - $window.location.hash = "#b" + var old = G.window.location.href + G.window.location.hash = "#b" o(old).equals("http://localhost/") - o($window.location.href).equals("http://localhost/#b") - o($window.location.hash).equals("#b") + o(G.window.location.href).equals("http://localhost/#b") + o(G.window.location.hash).equals("#b") }) }) o.spec("set pathname", function() { o("changes url on location.pathname change", function() { - var old = $window.location.href - $window.location.pathname = "/a" + var old = G.window.location.href + G.window.location.pathname = "/a" o(old).equals("http://localhost/") - o($window.location.href).equals("http://localhost/a") - o($window.location.pathname).equals("/a") + o(G.window.location.href).equals("http://localhost/a") + o(G.window.location.pathname).equals("/a") }) }) o.spec("set protocol", function() { o("setting protocol throws", function(done) { try { - $window.location.protocol = "https://" + G.window.location.protocol = "https://" } catch (e) { return done() @@ -178,452 +175,452 @@ o.spec("pushStateMock", function() { }) o.spec("set port", function() { o("setting origin changes href", function() { - var old = $window.location.href - $window.location.port = "81" + var old = G.window.location.href + G.window.location.port = "81" o(old).equals("http://localhost/") - o($window.location.port).equals("81") - o($window.location.href).equals("http://localhost:81/") + o(G.window.location.port).equals("81") + o(G.window.location.href).equals("http://localhost:81/") }) }) o.spec("set hostname", function() { o("setting hostname changes href", function() { - var old = $window.location.href - $window.location.hostname = "127.0.0.1" + var old = G.window.location.href + G.window.location.hostname = "127.0.0.1" o(old).equals("http://localhost/") - o($window.location.hostname).equals("127.0.0.1") - o($window.location.href).equals("http://127.0.0.1/") + o(G.window.location.hostname).equals("127.0.0.1") + o(G.window.location.href).equals("http://127.0.0.1/") }) }) o.spec("set origin", function() { o("setting origin is ignored", function() { - var old = $window.location.href - $window.location.origin = "http://127.0.0.1" + var old = G.window.location.href + G.window.location.origin = "http://127.0.0.1" o(old).equals("http://localhost/") - o($window.location.origin).equals("http://localhost") + o(G.window.location.origin).equals("http://localhost") }) }) o.spec("set host", function() { o("setting host is ignored", function() { - var old = $window.location.href - $window.location.host = "http://127.0.0.1" + var old = G.window.location.href + G.window.location.host = "http://127.0.0.1" o(old).equals("http://localhost/") - o($window.location.host).equals("localhost") + o(G.window.location.host).equals("localhost") }) }) o.spec("pushState", function() { o("changes url on pushstate", function() { - var old = $window.location.href - $window.history.pushState(null, null, "http://localhost/a") + var old = G.window.location.href + G.window.history.pushState(null, null, "http://localhost/a") o(old).equals("http://localhost/") - o($window.location.href).equals("http://localhost/a") + o(G.window.location.href).equals("http://localhost/a") }) o("changes search on pushstate", function() { - var old = $window.location.href - $window.history.pushState(null, null, "http://localhost/?a") + var old = G.window.location.href + G.window.history.pushState(null, null, "http://localhost/?a") o(old).equals("http://localhost/") - o($window.location.href).equals("http://localhost/?a") - o($window.location.search).equals("?a") + o(G.window.location.href).equals("http://localhost/?a") + o(G.window.location.search).equals("?a") }) o("changes search on relative pushstate", function() { - var old = $window.location.href - $window.history.pushState(null, null, "?a") + var old = G.window.location.href + G.window.history.pushState(null, null, "?a") o(old).equals("http://localhost/") - o($window.location.href).equals("http://localhost/?a") - o($window.location.search).equals("?a") + o(G.window.location.href).equals("http://localhost/?a") + o(G.window.location.search).equals("?a") }) o("changes hash on pushstate", function() { - var old = $window.location.href - $window.history.pushState(null, null, "http://localhost/#a") + var old = G.window.location.href + G.window.history.pushState(null, null, "http://localhost/#a") o(old).equals("http://localhost/") - o($window.location.href).equals("http://localhost/#a") - o($window.location.hash).equals("#a") + o(G.window.location.href).equals("http://localhost/#a") + o(G.window.location.hash).equals("#a") }) o("changes hash on relative pushstate", function() { - var old = $window.location.href - $window.history.pushState(null, null, "#a") + var old = G.window.location.href + G.window.history.pushState(null, null, "#a") o(old).equals("http://localhost/") - o($window.location.href).equals("http://localhost/#a") - o($window.location.hash).equals("#a") + o(G.window.location.href).equals("http://localhost/#a") + o(G.window.location.hash).equals("#a") }) }) o.spec("onpopstate", function() { o("history.back() without history does not trigger onpopstate", function() { - $window.onpopstate = o.spy() - $window.history.back() + G.window.onpopstate = o.spy() + G.window.history.back() - o($window.onpopstate.callCount).equals(0) + o(G.window.onpopstate.callCount).equals(0) }) o("history.back() after pushstate triggers onpopstate", function() { - $window.onpopstate = o.spy() - $window.history.pushState(null, null, "http://localhost/a") - $window.history.back() + G.window.onpopstate = o.spy() + G.window.history.pushState(null, null, "http://localhost/a") + G.window.history.back() - o($window.onpopstate.callCount).equals(1) - o($window.onpopstate.args[0].type).equals("popstate") + o(G.window.onpopstate.callCount).equals(1) + o(G.window.onpopstate.args[0].type).equals("popstate") }) o("history.back() after relative pushstate triggers onpopstate", function() { - $window.onpopstate = o.spy() - $window.history.pushState(null, null, "a") - $window.history.back() + G.window.onpopstate = o.spy() + G.window.history.pushState(null, null, "a") + G.window.history.back() - o($window.onpopstate.callCount).equals(1) + o(G.window.onpopstate.callCount).equals(1) }) o("history.back() after search pushstate triggers onpopstate", function() { - $window.onpopstate = o.spy() - $window.history.pushState(null, null, "http://localhost/?a") - $window.history.back() + G.window.onpopstate = o.spy() + G.window.history.pushState(null, null, "http://localhost/?a") + G.window.history.back() - o($window.onpopstate.callCount).equals(1) + o(G.window.onpopstate.callCount).equals(1) }) o("history.back() after relative search pushstate triggers onpopstate", function() { - $window.onpopstate = o.spy() - $window.history.pushState(null, null, "?a") - $window.history.back() + G.window.onpopstate = o.spy() + G.window.history.pushState(null, null, "?a") + G.window.history.back() - o($window.onpopstate.callCount).equals(1) + o(G.window.onpopstate.callCount).equals(1) }) o("history.back() after hash pushstate triggers onpopstate", function() { - $window.onpopstate = o.spy() - $window.history.pushState(null, null, "http://localhost/#a") - $window.history.back() + G.window.onpopstate = o.spy() + G.window.history.pushState(null, null, "http://localhost/#a") + G.window.history.back() - o($window.onpopstate.callCount).equals(1) + o(G.window.onpopstate.callCount).equals(1) }) o("history.back() after relative hash pushstate triggers onpopstate", function() { - $window.onpopstate = o.spy() - $window.history.pushState(null, null, "#a") - $window.history.back() + G.window.onpopstate = o.spy() + G.window.history.pushState(null, null, "#a") + G.window.history.back() - o($window.onpopstate.callCount).equals(1) + o(G.window.onpopstate.callCount).equals(1) }) o("history.back() after replacestate does not trigger onpopstate", function() { - $window.onpopstate = o.spy() - $window.history.replaceState(null, null, "http://localhost/a") - $window.history.back() + G.window.onpopstate = o.spy() + G.window.history.replaceState(null, null, "http://localhost/a") + G.window.history.back() - o($window.onpopstate.callCount).equals(0) + o(G.window.onpopstate.callCount).equals(0) }) o("history.back() after relative replacestate does not trigger onpopstate", function() { - $window.onpopstate = o.spy() - $window.history.replaceState(null, null, "a") - $window.history.back() + G.window.onpopstate = o.spy() + G.window.history.replaceState(null, null, "a") + G.window.history.back() - o($window.onpopstate.callCount).equals(0) + o(G.window.onpopstate.callCount).equals(0) }) o("history.back() after relative search replacestate does not trigger onpopstate", function() { - $window.onpopstate = o.spy() - $window.history.replaceState(null, null, "?a") - $window.history.back() + G.window.onpopstate = o.spy() + G.window.history.replaceState(null, null, "?a") + G.window.history.back() - o($window.onpopstate.callCount).equals(0) + o(G.window.onpopstate.callCount).equals(0) }) o("history.back() after relative hash replacestate does not trigger onpopstate", function() { - $window.onpopstate = o.spy() - $window.history.replaceState(null, null, "#a") - $window.history.back() + G.window.onpopstate = o.spy() + G.window.history.replaceState(null, null, "#a") + G.window.history.back() - o($window.onpopstate.callCount).equals(0) + o(G.window.onpopstate.callCount).equals(0) }) o("history.forward() after pushstate triggers onpopstate", function() { - $window.onpopstate = o.spy() - $window.history.pushState(null, null, "http://localhost/a") - $window.history.back() - $window.history.forward() + G.window.onpopstate = o.spy() + G.window.history.pushState(null, null, "http://localhost/a") + G.window.history.back() + G.window.history.forward() - o($window.onpopstate.callCount).equals(2) + o(G.window.onpopstate.callCount).equals(2) }) o("history.forward() after relative pushstate triggers onpopstate", function() { - $window.onpopstate = o.spy() - $window.history.pushState(null, null, "a") - $window.history.back() - $window.history.forward() + G.window.onpopstate = o.spy() + G.window.history.pushState(null, null, "a") + G.window.history.back() + G.window.history.forward() - o($window.onpopstate.callCount).equals(2) + o(G.window.onpopstate.callCount).equals(2) }) o("history.forward() after search pushstate triggers onpopstate", function() { - $window.onpopstate = o.spy() - $window.history.pushState(null, null, "http://localhost/?a") - $window.history.back() - $window.history.forward() + G.window.onpopstate = o.spy() + G.window.history.pushState(null, null, "http://localhost/?a") + G.window.history.back() + G.window.history.forward() - o($window.onpopstate.callCount).equals(2) + o(G.window.onpopstate.callCount).equals(2) }) o("history.forward() after relative search pushstate triggers onpopstate", function() { - $window.onpopstate = o.spy() - $window.history.pushState(null, null, "?a") - $window.history.back() - $window.history.forward() + G.window.onpopstate = o.spy() + G.window.history.pushState(null, null, "?a") + G.window.history.back() + G.window.history.forward() - o($window.onpopstate.callCount).equals(2) + o(G.window.onpopstate.callCount).equals(2) }) o("history.forward() after hash pushstate triggers onpopstate", function() { - $window.onpopstate = o.spy() - $window.history.pushState(null, null, "http://localhost/#a") - $window.history.back() - $window.history.forward() + G.window.onpopstate = o.spy() + G.window.history.pushState(null, null, "http://localhost/#a") + G.window.history.back() + G.window.history.forward() - o($window.onpopstate.callCount).equals(2) + o(G.window.onpopstate.callCount).equals(2) }) o("history.forward() after relative hash pushstate triggers onpopstate", function() { - $window.onpopstate = o.spy() - $window.history.pushState(null, null, "#a") - $window.history.back() - $window.history.forward() + G.window.onpopstate = o.spy() + G.window.history.pushState(null, null, "#a") + G.window.history.back() + G.window.history.forward() - o($window.onpopstate.callCount).equals(2) + o(G.window.onpopstate.callCount).equals(2) }) o("history.forward() without history does not trigger onpopstate", function() { - $window.onpopstate = o.spy() - $window.history.forward() + G.window.onpopstate = o.spy() + G.window.history.forward() - o($window.onpopstate.callCount).equals(0) + o(G.window.onpopstate.callCount).equals(0) }) o("history navigation without history does not trigger onpopstate", function() { - $window.onpopstate = o.spy() - $window.history.back() - $window.history.forward() + G.window.onpopstate = o.spy() + G.window.history.back() + G.window.history.forward() - o($window.onpopstate.callCount).equals(0) + o(G.window.onpopstate.callCount).equals(0) }) o("reverse history navigation without history does not trigger onpopstate", function() { - $window.onpopstate = o.spy() - $window.history.forward() - $window.history.back() + G.window.onpopstate = o.spy() + G.window.history.forward() + G.window.history.back() - o($window.onpopstate.callCount).equals(0) + o(G.window.onpopstate.callCount).equals(0) }) o("onpopstate has correct url during call", function(done) { - $window.location.href = "a" - $window.onpopstate = function() { - o($window.location.href).equals("http://localhost/a") + G.window.location.href = "a" + G.window.onpopstate = function() { + o(G.window.location.href).equals("http://localhost/a") done() } - $window.history.pushState(null, null, "b") - $window.history.back() + G.window.history.pushState(null, null, "b") + G.window.history.back() }) o("replaceState does not break forward history", function() { - $window.onpopstate = o.spy() + G.window.onpopstate = o.spy() - $window.history.pushState(null, null, "b") - $window.history.back() + G.window.history.pushState(null, null, "b") + G.window.history.back() - o($window.onpopstate.callCount).equals(1) - o($window.location.href).equals("http://localhost/") + o(G.window.onpopstate.callCount).equals(1) + o(G.window.location.href).equals("http://localhost/") - $window.history.replaceState(null, null, "a") + G.window.history.replaceState(null, null, "a") - o($window.location.href).equals("http://localhost/a") + o(G.window.location.href).equals("http://localhost/a") - $window.history.forward() + G.window.history.forward() - o($window.onpopstate.callCount).equals(2) - o($window.location.href).equals("http://localhost/b") + o(G.window.onpopstate.callCount).equals(2) + o(G.window.location.href).equals("http://localhost/b") }) o("pushstate retains state", function() { - $window.onpopstate = o.spy() + G.window.onpopstate = o.spy() - $window.history.pushState({a: 1}, null, "#a") - $window.history.pushState({b: 2}, null, "#b") + G.window.history.pushState({a: 1}, null, "#a") + G.window.history.pushState({b: 2}, null, "#b") - o($window.onpopstate.callCount).equals(0) + o(G.window.onpopstate.callCount).equals(0) - $window.history.back() + G.window.history.back() - o($window.onpopstate.callCount).equals(1) - o($window.onpopstate.args[0].type).equals("popstate") - o($window.onpopstate.args[0].state).deepEquals({a: 1}) + o(G.window.onpopstate.callCount).equals(1) + o(G.window.onpopstate.args[0].type).equals("popstate") + o(G.window.onpopstate.args[0].state).deepEquals({a: 1}) - $window.history.back() + G.window.history.back() - o($window.onpopstate.callCount).equals(2) - o($window.onpopstate.args[0].type).equals("popstate") - o($window.onpopstate.args[0].state).equals(null) + o(G.window.onpopstate.callCount).equals(2) + o(G.window.onpopstate.args[0].type).equals("popstate") + o(G.window.onpopstate.args[0].state).equals(null) - $window.history.forward() + G.window.history.forward() - o($window.onpopstate.callCount).equals(3) - o($window.onpopstate.args[0].type).equals("popstate") - o($window.onpopstate.args[0].state).deepEquals({a: 1}) + o(G.window.onpopstate.callCount).equals(3) + o(G.window.onpopstate.args[0].type).equals("popstate") + o(G.window.onpopstate.args[0].state).deepEquals({a: 1}) - $window.history.forward() + G.window.history.forward() - o($window.onpopstate.callCount).equals(4) - o($window.onpopstate.args[0].type).equals("popstate") - o($window.onpopstate.args[0].state).deepEquals({b: 2}) + o(G.window.onpopstate.callCount).equals(4) + o(G.window.onpopstate.args[0].type).equals("popstate") + o(G.window.onpopstate.args[0].state).deepEquals({b: 2}) }) o("replacestate replaces state", function() { - $window.onpopstate = o.spy(pop) + G.window.onpopstate = o.spy(pop) - $window.history.replaceState({a: 1}, null, "a") + G.window.history.replaceState({a: 1}, null, "a") - o($window.history.state).deepEquals({a: 1}) + o(G.window.history.state).deepEquals({a: 1}) - $window.history.pushState(null, null, "a") - $window.history.back() + G.window.history.pushState(null, null, "a") + G.window.history.back() function pop(e) { o(e.state).deepEquals({a: 1}) - o($window.history.state).deepEquals({a: 1}) + o(G.window.history.state).deepEquals({a: 1}) } }) }) o.spec("onhashchance", function() { o("onhashchange triggers on location.href change", function(done) { - $window.onhashchange = o.spy() - $window.location.href = "http://localhost/#a" + G.window.onhashchange = o.spy() + G.window.location.href = "http://localhost/#a" callAsync(function(){ - o($window.onhashchange.callCount).equals(1) - o($window.onhashchange.args[0].type).equals("hashchange") + o(G.window.onhashchange.callCount).equals(1) + o(G.window.onhashchange.args[0].type).equals("hashchange") done() }) }) o("onhashchange triggers on relative location.href change", function(done) { - $window.onhashchange = o.spy() - $window.location.href = "#a" + G.window.onhashchange = o.spy() + G.window.location.href = "#a" callAsync(function(){ - o($window.onhashchange.callCount).equals(1) + o(G.window.onhashchange.callCount).equals(1) done() }) }) o("onhashchange triggers on location.hash change", function(done) { - $window.onhashchange = o.spy() - $window.location.hash = "#a" + G.window.onhashchange = o.spy() + G.window.location.hash = "#a" callAsync(function(){ - o($window.onhashchange.callCount).equals(1) + o(G.window.onhashchange.callCount).equals(1) done() }) }) o("onhashchange does not trigger on page change", function(done) { - $window.onhashchange = o.spy() - $window.location.href = "http://localhost/a" + G.window.onhashchange = o.spy() + G.window.location.href = "http://localhost/a" callAsync(function(){ - o($window.onhashchange.callCount).equals(0) + o(G.window.onhashchange.callCount).equals(0) done() }) }) o("onhashchange does not trigger on page change with different hash", function(done) { - $window.location.href = "http://localhost/#a" + G.window.location.href = "http://localhost/#a" callAsync(function(){ - $window.onhashchange = o.spy() - $window.location.href = "http://localhost/a#b" + G.window.onhashchange = o.spy() + G.window.location.href = "http://localhost/a#b" callAsync(function(){ - o($window.onhashchange.callCount).equals(0) + o(G.window.onhashchange.callCount).equals(0) done() }) }) }) o("onhashchange does not trigger on page change with same hash", function(done) { - $window.location.href = "http://localhost/#b" + G.window.location.href = "http://localhost/#b" callAsync(function(){ - $window.onhashchange = o.spy() - $window.location.href = "http://localhost/a#b" + G.window.onhashchange = o.spy() + G.window.location.href = "http://localhost/a#b" callAsync(function(){ - o($window.onhashchange.callCount).equals(0) + o(G.window.onhashchange.callCount).equals(0) done() }) }) }) o("onhashchange triggers on history.back()", function(done) { - $window.location.href = "#a" + G.window.location.href = "#a" callAsync(function(){ - $window.onhashchange = o.spy() - $window.history.back() + G.window.onhashchange = o.spy() + G.window.history.back() callAsync(function(){ - o($window.onhashchange.callCount).equals(1) + o(G.window.onhashchange.callCount).equals(1) done() }) }) }) o("onhashchange triggers on history.forward()", function(done) { - $window.location.href = "#a" + G.window.location.href = "#a" callAsync(function(){ - $window.onhashchange = o.spy() - $window.history.back() + G.window.onhashchange = o.spy() + G.window.history.back() callAsync(function(){ - $window.history.forward() + G.window.history.forward() callAsync(function(){ - o($window.onhashchange.callCount).equals(2) + o(G.window.onhashchange.callCount).equals(2) done() }) }) }) }) o("onhashchange triggers once when the hash changes twice in a single tick", async () => { - $window.location.href = "#a" + G.window.location.href = "#a" await waitAsync() - $window.onhashchange = o.spy() - $window.history.back() - $window.history.forward() + G.window.onhashchange = o.spy() + G.window.history.back() + G.window.history.forward() await waitAsync() - o($window.onhashchange.callCount).equals(1) + o(G.window.onhashchange.callCount).equals(1) }) o("onhashchange does not trigger on history.back() that causes page change with different hash", function(done) { - $window.location.href = "#a" - $window.location.href = "a#b" + G.window.location.href = "#a" + G.window.location.href = "a#b" callAsync(function(){ - $window.onhashchange = o.spy() - $window.history.back() + G.window.onhashchange = o.spy() + G.window.history.back() callAsync(function(){ - o($window.onhashchange.callCount).equals(0) + o(G.window.onhashchange.callCount).equals(0) done() }) }) }) o("onhashchange does not trigger on history.back() that causes page change with same hash", function(done) { - $window.location.href = "#a" - $window.location.href = "a#a" + G.window.location.href = "#a" + G.window.location.href = "a#a" callAsync(function(){ - $window.onhashchange = o.spy() - $window.history.back() + G.window.onhashchange = o.spy() + G.window.history.back() callAsync(function(){ - o($window.onhashchange.callCount).equals(0) + o(G.window.onhashchange.callCount).equals(0) done() }) }) }) o("onhashchange does not trigger on history.forward() that causes page change with different hash", function(done) { - $window.location.href = "#a" - $window.location.href = "a#b" + G.window.location.href = "#a" + G.window.location.href = "a#b" callAsync(function(){ - $window.onhashchange = o.spy() - $window.history.back() - $window.history.forward() + G.window.onhashchange = o.spy() + G.window.history.back() + G.window.history.forward() callAsync(function(){ - o($window.onhashchange.callCount).equals(0) + o(G.window.onhashchange.callCount).equals(0) done() }) }) }) o("onhashchange does not trigger on history.forward() that causes page change with same hash", function(done) { - $window.location.href = "#a" - $window.location.href = "a#b" + G.window.location.href = "#a" + G.window.location.href = "a#b" callAsync(function(){ - $window.onhashchange = o.spy() - $window.history.back() - $window.history.forward() + G.window.onhashchange = o.spy() + G.window.history.back() + G.window.history.forward() callAsync(function(){ - o($window.onhashchange.callCount).equals(0) + o(G.window.onhashchange.callCount).equals(0) done() }) }) @@ -631,70 +628,70 @@ o.spec("pushStateMock", function() { }) o.spec("onunload", function() { o("onunload triggers on location.href change", function() { - $window.onunload = o.spy() - $window.location.href = "http://localhost/a" + G.window.onunload = o.spy() + G.window.location.href = "http://localhost/a" - o($window.onunload.callCount).equals(1) - o($window.onunload.args[0].type).equals("unload") + o(G.window.onunload.callCount).equals(1) + o(G.window.onunload.args[0].type).equals("unload") }) o("onunload triggers on relative location.href change", function() { - $window.onunload = o.spy() - $window.location.href = "a" + G.window.onunload = o.spy() + G.window.location.href = "a" - o($window.onunload.callCount).equals(1) + o(G.window.onunload.callCount).equals(1) }) o("onunload triggers on search change via location.href", function() { - $window.onunload = o.spy() - $window.location.href = "http://localhost/?a" + G.window.onunload = o.spy() + G.window.location.href = "http://localhost/?a" - o($window.onunload.callCount).equals(1) + o(G.window.onunload.callCount).equals(1) }) o("onunload triggers on relative search change via location.href", function() { - $window.onunload = o.spy() - $window.location.href = "?a" + G.window.onunload = o.spy() + G.window.location.href = "?a" - o($window.onunload.callCount).equals(1) + o(G.window.onunload.callCount).equals(1) }) o("onunload does not trigger on hash change via location.href", function() { - $window.onunload = o.spy() - $window.location.href = "http://localhost/#a" + G.window.onunload = o.spy() + G.window.location.href = "http://localhost/#a" - o($window.onunload.callCount).equals(0) + o(G.window.onunload.callCount).equals(0) }) o("onunload does not trigger on relative hash change via location.href", function() { - $window.onunload = o.spy() - $window.location.href = "#a" + G.window.onunload = o.spy() + G.window.location.href = "#a" - o($window.onunload.callCount).equals(0) + o(G.window.onunload.callCount).equals(0) }) o("onunload does not trigger on hash-only history.back()", function() { - $window.location.href = "#a" - $window.onunload = o.spy() - $window.history.back() + G.window.location.href = "#a" + G.window.onunload = o.spy() + G.window.history.back() - o($window.onunload.callCount).equals(0) + o(G.window.onunload.callCount).equals(0) }) o("onunload does not trigger on hash-only history.forward()", function() { - $window.location.href = "#a" - $window.history.back() - $window.onunload = o.spy() - $window.history.forward() + G.window.location.href = "#a" + G.window.history.back() + G.window.onunload = o.spy() + G.window.history.forward() - o($window.onunload.callCount).equals(0) + o(G.window.onunload.callCount).equals(0) }) o("onunload has correct url during call via location.href change", function(done) { - $window.onunload = function() { - o($window.location.href).equals("http://localhost/") + G.window.onunload = function() { + o(G.window.location.href).equals("http://localhost/") done() } - $window.location.href = "a" + G.window.location.href = "a" }) o("onunload has correct url during call via location.search change", function(done) { - $window.onunload = function() { - o($window.location.href).equals("http://localhost/") + G.window.onunload = function() { + o(G.window.location.href).equals("http://localhost/") done() } - $window.location.search = "?a" + G.window.location.search = "?a" }) }) }) diff --git a/tests/test-utils/xhrMock.js b/tests/test-utils/xhrMock.js deleted file mode 100644 index 511214eed..000000000 --- a/tests/test-utils/xhrMock.js +++ /dev/null @@ -1,99 +0,0 @@ -import o from "ospec" - -import xhrMock from "../../test-utils/xhrMock.js" - -o.spec("xhrMock", function() { - var $window - o.beforeEach(function() { - $window = xhrMock() - }) - - o.spec("xhr", function() { - o("works", function(done) { - $window.$defineRoutes({ - "GET /item": function(request) { - o(request.url).equals("/item") - return {status: 200, responseText: "test"} - } - }) - var xhr = new $window.XMLHttpRequest() - xhr.open("GET", "/item") - xhr.onreadystatechange = function() { - if (xhr.readyState === 4) { - o(xhr.status).equals(200) - o(xhr.responseText).equals("test") - done() - } - } - xhr.send() - }) - o("works w/ search", function(done) { - $window.$defineRoutes({ - "GET /item": function(request) { - o(request.query).equals("?a=b") - return {status: 200, responseText: "test"} - } - }) - var xhr = new $window.XMLHttpRequest() - xhr.open("GET", "/item?a=b") - xhr.onreadystatechange = function() { - if (xhr.readyState === 4) { - done() - } - } - xhr.send() - }) - o("works w/ body", function(done) { - $window.$defineRoutes({ - "POST /item": function(request) { - o(request.body).equals("a=b") - return {status: 200, responseText: "test"} - } - }) - var xhr = new $window.XMLHttpRequest() - xhr.open("POST", "/item") - xhr.onreadystatechange = function() { - if (xhr.readyState === 4) { - done() - } - } - xhr.send("a=b") - }) - o("passes event to onreadystatechange", function(done) { - $window.$defineRoutes({ - "GET /item": function(request) { - o(request.url).equals("/item") - return {status: 200, responseText: "test"} - } - }) - var xhr = new $window.XMLHttpRequest() - xhr.open("GET", "/item") - xhr.onreadystatechange = function(ev) { - o(ev.target).equals(xhr) - if (xhr.readyState === 4) { - done() - } - } - xhr.send() - }) - o("handles routing error", function(done) { - var xhr = new $window.XMLHttpRequest() - xhr.open("GET", "/nonexistent") - xhr.onreadystatechange = function() { - if (xhr.readyState === 4) { - o(xhr.status).equals(500) - done() - } - } - xhr.send("a=b") - }) - o("Setting a header twice merges the header", function() { - // Source: https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/setRequestHeader - var xhr = new $window.XMLHttpRequest() - xhr.open("POST", "/test") - xhr.setRequestHeader("Content-Type", "foo") - xhr.setRequestHeader("Content-Type", "bar") - o(xhr.getRequestHeader("Content-Type")).equals("foo, bar") - }) - }) -}) diff --git a/tests/util/init.js b/tests/util/init.js index 8220ff6ad..c81d1d7dc 100644 --- a/tests/util/init.js +++ b/tests/util/init.js @@ -1,30 +1,225 @@ import o from "ospec" -import domMock from "../../test-utils/domMock.js" -import init from "../../src/std/init.js" +import {setupGlobals} from "../../test-utils/global.js" + import m from "../../src/entry/mithril.esm.js" o.spec("m.init", () => { - o("works", async () => { + var G = setupGlobals() + + o("works when returning `undefined`", async () => { + var onabort = o.spy() + var initializer = o.spy((signal) => { signal.onabort = onabort; return undefined }) + var redraw = o.spy() + + m.render(G.window.document.body, m.init(initializer), redraw) + o(initializer.callCount).equals(0) + o(onabort.callCount).equals(0) + o(redraw.callCount).equals(0) + + await Promise.resolve() + o(initializer.callCount).equals(1) + o(onabort.callCount).equals(0) + o(redraw.callCount).equals(0) + m.render(G.window.document.body, m.init(initializer), redraw) + + await Promise.resolve() + o(initializer.callCount).equals(1) + o(onabort.callCount).equals(0) + o(redraw.callCount).equals(1) + m.render(G.window.document.body, null, redraw) + + o(initializer.callCount).equals(1) + o(onabort.callCount).equals(1) + o(redraw.callCount).equals(1) + }) + + o("works when resolving to `undefined`", async () => { + var onabort = o.spy() + var initializer = o.spy((signal) => { signal.onabort = onabort; return Promise.resolve(undefined) }) + var redraw = o.spy() + + m.render(G.window.document.body, m.init(initializer), redraw) + o(initializer.callCount).equals(0) + o(onabort.callCount).equals(0) + o(redraw.callCount).equals(0) + + await Promise.resolve() + o(initializer.callCount).equals(1) + o(onabort.callCount).equals(0) + o(redraw.callCount).equals(0) + m.render(G.window.document.body, m.init(initializer), redraw) + + await Promise.resolve() + o(initializer.callCount).equals(1) + o(onabort.callCount).equals(0) + o(redraw.callCount).equals(1) + m.render(G.window.document.body, null, redraw) + + o(initializer.callCount).equals(1) + o(onabort.callCount).equals(1) + o(redraw.callCount).equals(1) + }) + + o("works when returning `null`", async () => { + var onabort = o.spy() + var initializer = o.spy((signal) => { signal.onabort = onabort; return null }) + var redraw = o.spy() + + m.render(G.window.document.body, m.init(initializer), redraw) + o(initializer.callCount).equals(0) + o(onabort.callCount).equals(0) + o(redraw.callCount).equals(0) + + await Promise.resolve() + o(initializer.callCount).equals(1) + o(onabort.callCount).equals(0) + o(redraw.callCount).equals(0) + m.render(G.window.document.body, m.init(initializer), redraw) + + await Promise.resolve() + o(initializer.callCount).equals(1) + o(onabort.callCount).equals(0) + o(redraw.callCount).equals(1) + m.render(G.window.document.body, null, redraw) + + o(initializer.callCount).equals(1) + o(onabort.callCount).equals(1) + o(redraw.callCount).equals(1) + }) + + o("works when resolving to `null`", async () => { + var onabort = o.spy() + var initializer = o.spy((signal) => { signal.onabort = onabort; return Promise.resolve(null) }) + var redraw = o.spy() + + m.render(G.window.document.body, m.init(initializer), redraw) + o(initializer.callCount).equals(0) + o(onabort.callCount).equals(0) + o(redraw.callCount).equals(0) + + await Promise.resolve() + o(initializer.callCount).equals(1) + o(onabort.callCount).equals(0) + o(redraw.callCount).equals(0) + m.render(G.window.document.body, m.init(initializer), redraw) + + await Promise.resolve() + o(initializer.callCount).equals(1) + o(onabort.callCount).equals(0) + o(redraw.callCount).equals(1) + m.render(G.window.document.body, null, redraw) + + o(initializer.callCount).equals(1) + o(onabort.callCount).equals(1) + o(redraw.callCount).equals(1) + }) + + o("works when returning `true`", async () => { + var onabort = o.spy() + var initializer = o.spy((signal) => { signal.onabort = onabort; return true }) + var redraw = o.spy() + + m.render(G.window.document.body, m.init(initializer), redraw) + o(initializer.callCount).equals(0) + o(onabort.callCount).equals(0) + o(redraw.callCount).equals(0) + + await Promise.resolve() + o(initializer.callCount).equals(1) + o(onabort.callCount).equals(0) + o(redraw.callCount).equals(0) + m.render(G.window.document.body, m.init(initializer), redraw) + + await Promise.resolve() + o(initializer.callCount).equals(1) + o(onabort.callCount).equals(0) + o(redraw.callCount).equals(1) + m.render(G.window.document.body, null, redraw) + + o(initializer.callCount).equals(1) + o(onabort.callCount).equals(1) + o(redraw.callCount).equals(1) + }) + + o("works when resolving to `true`", async () => { + var onabort = o.spy() + var initializer = o.spy((signal) => { signal.onabort = onabort; return Promise.resolve(true) }) + var redraw = o.spy() + + m.render(G.window.document.body, m.init(initializer), redraw) + o(initializer.callCount).equals(0) + o(onabort.callCount).equals(0) + o(redraw.callCount).equals(0) + + await Promise.resolve() + o(initializer.callCount).equals(1) + o(onabort.callCount).equals(0) + o(redraw.callCount).equals(0) + m.render(G.window.document.body, m.init(initializer), redraw) + + await Promise.resolve() + o(initializer.callCount).equals(1) + o(onabort.callCount).equals(0) + o(redraw.callCount).equals(1) + m.render(G.window.document.body, null, redraw) + + o(initializer.callCount).equals(1) + o(onabort.callCount).equals(1) + o(redraw.callCount).equals(1) + }) + + o("works when returning `false`", async () => { + var onabort = o.spy() + var initializer = o.spy((signal) => { signal.onabort = onabort; return false }) + var redraw = o.spy() + + m.render(G.window.document.body, m.init(initializer), redraw) + o(initializer.callCount).equals(0) + o(onabort.callCount).equals(0) + o(redraw.callCount).equals(0) + + await Promise.resolve() + o(initializer.callCount).equals(1) + o(onabort.callCount).equals(0) + o(redraw.callCount).equals(0) + m.render(G.window.document.body, m.init(initializer), redraw) + + await Promise.resolve() + o(initializer.callCount).equals(1) + o(onabort.callCount).equals(0) + o(redraw.callCount).equals(0) + m.render(G.window.document.body, null, redraw) + + o(initializer.callCount).equals(1) + o(onabort.callCount).equals(1) + o(redraw.callCount).equals(0) + }) + + o("works when resolving to `false`", async () => { var onabort = o.spy() - var initializer = o.spy((signal) => { signal.onabort = onabort }) - var $window = domMock() + var initializer = o.spy((signal) => { signal.onabort = onabort; return Promise.resolve(false) }) + var redraw = o.spy() - m.render($window.document.body, init(initializer)) + m.render(G.window.document.body, m.init(initializer), redraw) o(initializer.callCount).equals(0) o(onabort.callCount).equals(0) + o(redraw.callCount).equals(0) await Promise.resolve() o(initializer.callCount).equals(1) o(onabort.callCount).equals(0) - m.render($window.document.body, init(initializer)) + o(redraw.callCount).equals(0) + m.render(G.window.document.body, m.init(initializer), redraw) await Promise.resolve() o(initializer.callCount).equals(1) o(onabort.callCount).equals(0) - m.render($window.document.body, null) + o(redraw.callCount).equals(0) + m.render(G.window.document.body, null, redraw) o(initializer.callCount).equals(1) o(onabort.callCount).equals(1) + o(redraw.callCount).equals(0) }) }) diff --git a/tests/util/lazy.js b/tests/util/lazy.js index 059b6a620..124a5e97d 100644 --- a/tests/util/lazy.js +++ b/tests/util/lazy.js @@ -1,23 +1,11 @@ import o from "ospec" -import domMock from "../../test-utils/domMock.js" +import {setupGlobals} from "../../test-utils/global.js" + import m from "../../src/entry/mithril.esm.js" -import makeLazy from "../../src/std/lazy.js" o.spec("lazy", () => { - var consoleError = console.error - var $window, root - o.beforeEach(() => { - $window = domMock() - root = $window.document.createElement("div") - console.error = (...args) => { - consoleError.apply(console, args) - throw new Error("should not be called") - } - }) - o.afterEach(() => { - console.error = consoleError - }) + var G = setupGlobals({expectNoConsoleError: true}) void [{name: "direct", wrap: (v) => v}, {name: "in module with default", wrap: (v) => ({default:v})}].forEach(({name, wrap}) => { o.spec(name, () => { @@ -30,22 +18,23 @@ o.spec("lazy", () => { }) var send, notifyRedrawn var fetchRedrawn = new Promise((resolve) => notifyRedrawn = resolve) - var C = makeLazy({ + var redraw = () => { + notifyRedrawn() + calls.push(`scheduled ${scheduled++}`) + } + var C = m.lazy({ fetch() { calls.push("fetch") return new Promise((resolve) => send = resolve) }, - }, () => { - notifyRedrawn() - calls.push(`scheduled ${scheduled++}`) }) o(calls).deepEquals([]) - m.render(root, [ + m.render(G.root, [ m(C, {name: "one"}), m(C, {name: "two"}), - ]) + ], redraw) o(calls).deepEquals([ "fetch", @@ -60,10 +49,10 @@ o.spec("lazy", () => { "scheduled 1", ]) - m.render(root, [ + m.render(G.root, [ m(C, {name: "one"}), m(C, {name: "two"}), - ]) + ], redraw) o(calls).deepEquals([ "fetch", @@ -72,10 +61,10 @@ o.spec("lazy", () => { "view two", ]) - m.render(root, [ + m.render(G.root, [ m(C, {name: "one"}), m(C, {name: "two"}), - ]) + ], redraw) o(calls).deepEquals([ "fetch", @@ -96,22 +85,23 @@ o.spec("lazy", () => { var scheduled = 1 var send, notifyRedrawn var fetchRedrawn = new Promise((resolve) => notifyRedrawn = resolve) - var C = makeLazy({ + var redraw = () => { + notifyRedrawn() + calls.push(`scheduled ${scheduled++}`) + } + var C = m.lazy({ fetch() { calls.push("fetch") return new Promise((_, reject) => send = reject) }, - }, () => { - notifyRedrawn() - calls.push(`scheduled ${scheduled++}`) }) o(calls).deepEquals([]) - m.render(root, [ + m.render(G.root, [ m(C, {name: "one"}), m(C, {name: "two"}), - ]) + ], redraw) o(calls).deepEquals([ "fetch", @@ -127,10 +117,10 @@ o.spec("lazy", () => { "scheduled 1", ]) - m.render(root, [ + m.render(G.root, [ m(C, {name: "one"}), m(C, {name: "two"}), - ]) + ], redraw) o(calls).deepEquals([ "fetch", @@ -138,10 +128,10 @@ o.spec("lazy", () => { "scheduled 1", ]) - m.render(root, [ + m.render(G.root, [ m(C, {name: "one"}), m(C, {name: "two"}), - ]) + ], redraw) o(calls).deepEquals([ "fetch", @@ -159,7 +149,11 @@ o.spec("lazy", () => { }) var send, notifyRedrawn var fetchRedrawn = new Promise((resolve) => notifyRedrawn = resolve) - var C = makeLazy({ + var redraw = () => { + notifyRedrawn() + calls.push(`scheduled ${scheduled++}`) + } + var C = m.lazy({ fetch() { calls.push("fetch") return new Promise((resolve) => send = resolve) @@ -167,17 +161,14 @@ o.spec("lazy", () => { pending() { calls.push("pending") }, - }, () => { - notifyRedrawn() - calls.push(`scheduled ${scheduled++}`) }) o(calls).deepEquals([]) - m.render(root, [ + m.render(G.root, [ m(C, {name: "one"}), m(C, {name: "two"}), - ]) + ], redraw) o(calls).deepEquals([ "fetch", @@ -196,10 +187,10 @@ o.spec("lazy", () => { "scheduled 1", ]) - m.render(root, [ + m.render(G.root, [ m(C, {name: "one"}), m(C, {name: "two"}), - ]) + ], redraw) o(calls).deepEquals([ "fetch", @@ -210,10 +201,10 @@ o.spec("lazy", () => { "view two", ]) - m.render(root, [ + m.render(G.root, [ m(C, {name: "one"}), m(C, {name: "two"}), - ]) + ], redraw) o(calls).deepEquals([ "fetch", @@ -236,7 +227,11 @@ o.spec("lazy", () => { var scheduled = 1 var send, notifyRedrawn var fetchRedrawn = new Promise((resolve) => notifyRedrawn = resolve) - var C = makeLazy({ + var redraw = () => { + notifyRedrawn() + calls.push(`scheduled ${scheduled++}`) + } + var C = m.lazy({ fetch() { calls.push("fetch") return new Promise((_, reject) => send = reject) @@ -244,17 +239,14 @@ o.spec("lazy", () => { pending() { calls.push("pending") }, - }, () => { - notifyRedrawn() - calls.push(`scheduled ${scheduled++}`) }) o(calls).deepEquals([]) - m.render(root, [ + m.render(G.root, [ m(C, {name: "one"}), m(C, {name: "two"}), - ]) + ], redraw) o(calls).deepEquals([ "fetch", @@ -274,10 +266,10 @@ o.spec("lazy", () => { "scheduled 1", ]) - m.render(root, [ + m.render(G.root, [ m(C, {name: "one"}), m(C, {name: "two"}), - ]) + ], redraw) o(calls).deepEquals([ "fetch", @@ -287,10 +279,10 @@ o.spec("lazy", () => { "scheduled 1", ]) - m.render(root, [ + m.render(G.root, [ m(C, {name: "one"}), m(C, {name: "two"}), - ]) + ], redraw) o(calls).deepEquals([ "fetch", @@ -310,7 +302,11 @@ o.spec("lazy", () => { }) var send, notifyRedrawn var fetchRedrawn = new Promise((resolve) => notifyRedrawn = resolve) - var C = makeLazy({ + var redraw = () => { + notifyRedrawn() + calls.push(`scheduled ${scheduled++}`) + } + var C = m.lazy({ fetch() { calls.push("fetch") return new Promise((resolve) => send = resolve) @@ -318,17 +314,14 @@ o.spec("lazy", () => { error() { calls.push("error") }, - }, () => { - notifyRedrawn() - calls.push(`scheduled ${scheduled++}`) }) o(calls).deepEquals([]) - m.render(root, [ + m.render(G.root, [ m(C, {name: "one"}), m(C, {name: "two"}), - ]) + ], redraw) o(calls).deepEquals([ "fetch", @@ -343,10 +336,10 @@ o.spec("lazy", () => { "scheduled 1", ]) - m.render(root, [ + m.render(G.root, [ m(C, {name: "one"}), m(C, {name: "two"}), - ]) + ], redraw) o(calls).deepEquals([ "fetch", @@ -355,10 +348,10 @@ o.spec("lazy", () => { "view two", ]) - m.render(root, [ + m.render(G.root, [ m(C, {name: "one"}), m(C, {name: "two"}), - ]) + ], redraw) o(calls).deepEquals([ "fetch", @@ -379,7 +372,11 @@ o.spec("lazy", () => { var scheduled = 1 var send, notifyRedrawn var fetchRedrawn = new Promise((resolve) => notifyRedrawn = resolve) - var C = makeLazy({ + var redraw = () => { + notifyRedrawn() + calls.push(`scheduled ${scheduled++}`) + } + var C = m.lazy({ fetch() { calls.push("fetch") return new Promise((_, reject) => send = reject) @@ -387,17 +384,14 @@ o.spec("lazy", () => { error(e) { calls.push("error", e.message) }, - }, () => { - notifyRedrawn() - calls.push(`scheduled ${scheduled++}`) }) o(calls).deepEquals([]) - m.render(root, [ + m.render(G.root, [ m(C, {name: "one"}), m(C, {name: "two"}), - ]) + ], redraw) o(calls).deepEquals([ "fetch", @@ -413,10 +407,10 @@ o.spec("lazy", () => { "scheduled 1", ]) - m.render(root, [ + m.render(G.root, [ m(C, {name: "one"}), m(C, {name: "two"}), - ]) + ], redraw) o(calls).deepEquals([ "fetch", @@ -426,10 +420,10 @@ o.spec("lazy", () => { "error", "test", ]) - m.render(root, [ + m.render(G.root, [ m(C, {name: "one"}), m(C, {name: "two"}), - ]) + ], redraw) o(calls).deepEquals([ "fetch", @@ -451,7 +445,11 @@ o.spec("lazy", () => { }) var send, notifyRedrawn var fetchRedrawn = new Promise((resolve) => notifyRedrawn = resolve) - var C = makeLazy({ + var redraw = () => { + notifyRedrawn() + calls.push(`scheduled ${scheduled++}`) + } + var C = m.lazy({ fetch() { calls.push("fetch") return new Promise((resolve) => send = resolve) @@ -462,17 +460,14 @@ o.spec("lazy", () => { error() { calls.push("error") }, - }, () => { - notifyRedrawn() - calls.push(`scheduled ${scheduled++}`) }) o(calls).deepEquals([]) - m.render(root, [ + m.render(G.root, [ m(C, {name: "one"}), m(C, {name: "two"}), - ]) + ], redraw) o(calls).deepEquals([ "fetch", @@ -491,10 +486,10 @@ o.spec("lazy", () => { "scheduled 1", ]) - m.render(root, [ + m.render(G.root, [ m(C, {name: "one"}), m(C, {name: "two"}), - ]) + ], redraw) o(calls).deepEquals([ "fetch", @@ -505,10 +500,10 @@ o.spec("lazy", () => { "view two", ]) - m.render(root, [ + m.render(G.root, [ m(C, {name: "one"}), m(C, {name: "two"}), - ]) + ], redraw) o(calls).deepEquals([ "fetch", @@ -531,7 +526,11 @@ o.spec("lazy", () => { var scheduled = 1 var send, notifyRedrawn var fetchRedrawn = new Promise((resolve) => notifyRedrawn = resolve) - var C = makeLazy({ + var redraw = () => { + notifyRedrawn() + calls.push(`scheduled ${scheduled++}`) + } + var C = m.lazy({ fetch() { calls.push("fetch") return new Promise((_, reject) => send = reject) @@ -542,17 +541,14 @@ o.spec("lazy", () => { error(e) { calls.push("error", e.message) }, - }, () => { - notifyRedrawn() - calls.push(`scheduled ${scheduled++}`) }) o(calls).deepEquals([]) - m.render(root, [ + m.render(G.root, [ m(C, {name: "one"}), m(C, {name: "two"}), - ]) + ], redraw) o(calls).deepEquals([ "fetch", @@ -572,10 +568,10 @@ o.spec("lazy", () => { "scheduled 1", ]) - m.render(root, [ + m.render(G.root, [ m(C, {name: "one"}), m(C, {name: "two"}), - ]) + ], redraw) o(calls).deepEquals([ "fetch", @@ -587,10 +583,10 @@ o.spec("lazy", () => { "error", "test", ]) - m.render(root, [ + m.render(G.root, [ m(C, {name: "one"}), m(C, {name: "two"}), - ]) + ], redraw) o(calls).deepEquals([ "fetch", diff --git a/tests/util/tracked.js b/tests/util/tracked.js index 8822de67f..c0fb57195 100644 --- a/tests/util/tracked.js +++ b/tests/util/tracked.js @@ -1,6 +1,6 @@ import o from "ospec" -import makeTracked from "../../src/std/tracked.js" +import m from "../../src/entry/mithril.esm.js" o.spec("tracked", () => { /** @param {import("../tracked.js").Tracked} t */ @@ -8,7 +8,7 @@ o.spec("tracked", () => { o("initializes values correctly", () => { var calls = 0 - var t = makeTracked([[1, "one"], [2, "two"]], () => calls++) + var t = m.tracked(() => calls++, [[1, "one"], [2, "two"]]) o(live(t)).deepEquals([[1, "one", false], [2, "two", false]]) o(t.list()).deepEquals([[1, "one"], [2, "two"]]) @@ -23,7 +23,7 @@ o.spec("tracked", () => { o("tracks values correctly", () => { var calls = 0 - var t = makeTracked(undefined, () => calls++) + var t = m.tracked(() => calls++) t.set(1, "one") o(calls).equals(1) @@ -92,7 +92,7 @@ o.spec("tracked", () => { var live1Aborted = false var live2Aborted = false var call = 0 - var t = makeTracked(undefined, () => { + var t = m.tracked(() => { switch (++call) { case 1: o(live(t)).deepEquals([[1, "one", false]]) @@ -207,7 +207,7 @@ o.spec("tracked", () => { o("tracks parallel removes correctly", () => { var calls = 0 - var t = makeTracked(undefined, () => calls++) + var t = m.tracked(() => calls++) t.set(1, "one") var live1 = t.live()[0] @@ -261,7 +261,7 @@ o.spec("tracked", () => { o("tolerates release before abort", () => { var calls = 0 - var t = makeTracked(undefined, () => calls++) + var t = m.tracked(() => calls++) t.set(1, "one") o(calls).equals(1) @@ -288,7 +288,7 @@ o.spec("tracked", () => { o("tolerates double release before abort", () => { var calls = 0 - var t = makeTracked(undefined, () => calls++) + var t = m.tracked(() => calls++) t.set(1, "one") var live1 = t.live()[0] @@ -311,7 +311,7 @@ o.spec("tracked", () => { o("tolerates double release spanning delete", () => { var calls = 0 - var t = makeTracked(undefined, () => calls++) + var t = m.tracked(() => calls++) t.set(1, "one") var live1 = t.live()[0] @@ -328,7 +328,7 @@ o.spec("tracked", () => { o("tracks double release after delete", () => { var calls = 0 - var t = makeTracked(undefined, () => calls++) + var t = m.tracked(() => calls++) t.set(1, "one") var live1 = t.live()[0] diff --git a/tests/util/use.js b/tests/util/use.js index 669abdbd5..4133b1237 100644 --- a/tests/util/use.js +++ b/tests/util/use.js @@ -1,60 +1,67 @@ import o from "ospec" -import domMock from "../../test-utils/domMock.js" +import {setupGlobals} from "../../test-utils/global.js" + import m from "../../src/entry/mithril.esm.js" -import use from "../../src/std/use.js" o.spec("m.use", () => { + var G = setupGlobals() + o("works with empty arrays", () => { var onabort = o.spy() - var initializer = o.spy((_, signal) => { signal.onabort = onabort }) - var $window = domMock() + var create = o.spy((_, signal) => { signal.onabort = onabort }) + var update = o.spy((_, signal) => { signal.onabort = onabort }) - m.render($window.document.body, use([], m.layout(initializer))) - o(initializer.callCount).equals(1) + m.render(G.root, m.use([], m.layout(create, update))) + o(create.callCount).equals(1) + o(update.callCount).equals(0) o(onabort.callCount).equals(0) - m.render($window.document.body, use([], m.layout(initializer))) - o(initializer.callCount).equals(2) + m.render(G.root, m.use([], m.layout(create, update))) + o(create.callCount).equals(1) + o(update.callCount).equals(1) o(onabort.callCount).equals(0) - m.render($window.document.body, null) - o(initializer.callCount).equals(2) + m.render(G.root, null) + o(create.callCount).equals(1) + o(update.callCount).equals(1) o(onabort.callCount).equals(1) }) o("works with equal non-empty arrays", () => { var onabort = o.spy() - var initializer = o.spy((_, signal) => { signal.onabort = onabort }) - var $window = domMock() + var create = o.spy((_, signal) => { signal.onabort = onabort }) + var update = o.spy((_, signal) => { signal.onabort = onabort }) - m.render($window.document.body, use([1], m.layout(initializer))) - o(initializer.callCount).equals(1) + m.render(G.root, m.use([1], m.layout(create, update))) + o(create.callCount).equals(1) + o(update.callCount).equals(0) o(onabort.callCount).equals(0) - m.render($window.document.body, use([1], m.layout(initializer))) - o(initializer.callCount).equals(2) + m.render(G.root, m.use([1], m.layout(create, update))) + o(create.callCount).equals(1) + o(update.callCount).equals(1) o(onabort.callCount).equals(0) - m.render($window.document.body, null) - o(initializer.callCount).equals(2) + m.render(G.root, null) + o(create.callCount).equals(1) + o(update.callCount).equals(1) o(onabort.callCount).equals(1) }) o("works with non-equal same-length non-empty arrays", () => { var onabort = o.spy() var initializer = o.spy((_, signal) => { signal.onabort = onabort }) - var $window = domMock() - m.render($window.document.body, use([1], m.layout(initializer))) + m.render(G.root, m.use([1], m.layout(initializer))) o(initializer.callCount).equals(1) o(onabort.callCount).equals(0) - m.render($window.document.body, use([2], m.layout(initializer))) + m.render(G.root, m.use([2], m.layout(initializer))) o(initializer.callCount).equals(2) o(onabort.callCount).equals(1) - m.render($window.document.body, null) + m.render(G.root, null) o(initializer.callCount).equals(2) o(onabort.callCount).equals(2) }) From 044d1b8f631df3bc25eb7f417286c5728da88984 Mon Sep 17 00:00:00 2001 From: Claudia Meadows Date: Thu, 10 Oct 2024 19:32:41 -0700 Subject: [PATCH 49/95] Switch from `m.layout` signal out `m.remove`, merge create/update callbacks Also re-added the `vnode === old` optimization, out of necessity for other utilities (there's a long code comment explaining why). Before this `m.layout` change, code would've looked like this: ```js // For libraries that bind state to the element itself function ThirdParty(attrs, old) { return m("div", m.layout((elem, signal) => { // Do DOM initialization Library.initialize(elem) // Schedule DOM cleanup signal.onabort = () => Library.dispose(elem) })) } // For libraries that return an instance function ThirdParty() { return m("div", m.layout((elem, signal) => { // Do DOM initialization let instance = new Library(elem) // Schedule DOM cleanup signal.onabort = () => instance.dispose() })) } ``` The problem with this is it makes logic that only needs to care about removal a lot more complicated than it otherwise needs to be. (See `src/std/init.js` for a concrete example of this, and how this change simplifies those use cases a lot.) It also requires not only an extra closure that necessarily has to lay around until removal, but an entire `AbortController`, so this change ultimately saves a little over 200 bytes per layout/remove pair. (If you have a lot of such elements, this could be noticeable.) After this change, third-party integration code (the other main motivator for `m.layout` in the first place) would look something like this: ```js // For libraries that bind state to the element itself function ThirdParty(attrs, old) { return m("div", [ // Do DOM initialization !old && m.layout((elem) => Library.initialize(elem)), // Schedule DOM cleanup m.remove((elem) => Library.dispose(elem)), ]) } // For libraries that return an instance function ThirdParty() { let instance return (attrs, old) => m("div", [ // Do DOM initialization !old && m.layout((elem) => instance = new Library(elem)), // Schedule DOM cleanup m.remove(() => instance.dispose()), ]) } ``` It was already possible to initialize an element outside of its scope by saving a reference to it: ```js function ThirdParty(attrs, old) { let label, root return [ label = m("label", "Some text"), root = m("div.library"), m.layout((elem, signal) => { let instance = new Library(root.d, label.d) signal.onabort = () => instance.dispose() }), ] } ``` This change makes that a little cleaner to wire up: ```js function ThirdParty() { let instance return (attrs, old) => { let label, root return [ label = m("label", "Some text"), root = m("div.library"), !old && m.layout(() => instance = new Library(root.d, label.d)), m.remove(() => instance.dispose()), ] } } ``` It also fits in a JSX world a little more nicely, but this wasn't something I was specifically seeking out: ```jsx // Old unified function ThirdParty() { return
{m.layout((elem, signal) => { let instance = new Library(elem) signal.onabort = () => instance.dispose() })}
} // Old split function ThirdParty(attrs, old) { let label, root return <> {label = } {root =
} {m.layout((elem, signal) => { let instance = new Library(root.d, label.d) signal.onabort = () => instance.dispose() })} } // New unified function ThirdParty() { let instance return (attrs, old) =>
{!old && m.layout((elem) => instance = new Library(elem))} {m.remove(() => instance.dispose())}
} // New split function ThirdParty() { let instance return (attrs, old) => { let label, root return <> {label = } {root =
} {!old && m.layout(() => instance = new Library(root.d, label.d))} {m.remove(() => instance.dispose())} } } ``` --- src/core.js | 172 +++++++++--------- src/std/init.js | 13 +- test-utils/global.js | 10 +- tests/api/mountRedraw.js | 62 +++---- tests/render/component.js | 191 +++++++------------- tests/render/oncreate.js | 350 +++++++++++++++++++++++------------- tests/render/onremove.js | 18 +- tests/render/onupdate.js | 44 ++--- tests/render/render.js | 127 ++++++------- tests/render/updateNodes.js | 142 +++++++++------ tests/util/use.js | 68 ++++--- 11 files changed, 628 insertions(+), 569 deletions(-) diff --git a/src/core.js b/src/core.js index 48c12ae83..c1f79b571 100644 --- a/src/core.js +++ b/src/core.js @@ -21,45 +21,37 @@ export {m as default} This same structure is used for several nodes. Here's an explainer for each type. Retain: -- `m` bits 0-2: `0` +- `m`: `-1` - All other properties are unused - On ingest, the vnode itself is converted into the type of the element it's retaining. This includes changing its type. Fragments: -- `m` bits 0-2: `1` -- `t`: `FRAGMENT` +- `m` bits 0-2: `0` +- `t`: unused - `s`: unused - `a`: unused - `c`: virtual DOM children - `d`: unused Keys: -- `m` bits 0-2: `2` +- `m` bits 0-2: `1` - `t`: `KEY` - `s`: identity key (may be any arbitrary object) - `a`: unused - `c`: virtual DOM children - `d`: unused -Layout: -- `m` bits 0-2: `3` -- `t`: `LAYOUT` -- `s`: callback to schedule for create -- `a`: callback to schedule for update -- `c`: unused -- `d`: abort controller reference - Text: -- `m` bits 0-2: `4` -- `t`: `TEXT` +- `m` bits 0-2: `2` +- `t`: unused - `s`: text string - `a`: unused - `c`: unused - `d`: abort controller reference Components: -- `m` bits 0-2: `5` +- `m` bits 0-2: `3` - `t`: component reference - `s`: view function, may be same as component reference - `a`: most recently received attributes @@ -67,24 +59,41 @@ Components: - `d`: unused DOM elements: -- `m` bits 0-2: `6` +- `m` bits 0-2: `4` - `t`: tag name string - `s`: event listener dictionary, if any events were ever registered - `a`: most recently received attributes - `c`: virtual DOM children - `d`: element reference +Layout: +- `m` bits 0-2: `5` +- `t`: unused +- `s`: callback to schedule +- `a`: unused +- `c`: unused +- `d`: parent DOM reference, for easier queueing + +Remove: +- `m` bits 0-2: `6` +- `t`: unused +- `s`: callback to schedule +- `a`: unused +- `c`: unused +- `d`: parent DOM reference, for easier queueing + The `m` field is also used for various assertions, that aren't described here. */ var TYPE_MASK = 7 -var TYPE_RETAIN = 0 -var TYPE_FRAGMENT = 1 -var TYPE_KEY = 2 -var TYPE_LAYOUT = 3 -var TYPE_TEXT = 4 -var TYPE_ELEMENT = 5 -var TYPE_COMPONENT = 6 +var TYPE_RETAIN = -1 +var TYPE_FRAGMENT = 0 +var TYPE_KEY = 1 +var TYPE_TEXT = 2 +var TYPE_ELEMENT = 3 +var TYPE_COMPONENT = 4 +var TYPE_LAYOUT = 5 +var TYPE_REMOVE = 6 var FLAG_KEYED = 1 << 3 var FLAG_USED = 1 << 4 @@ -232,14 +241,18 @@ m.capture = (ev) => { m.retain = () => Vnode(TYPE_RETAIN, null, null, null, null) -m.layout = (onCreate, onUpdate) => { - if (onCreate != null && typeof onCreate !== "function") { - throw new TypeError("`onCreate` callback must be a function if provided") +m.layout = (callback) => { + if (typeof callback !== "function") { + throw new TypeError("Callback must be a function if provided") } - if (onUpdate != null && typeof onUpdate !== "function") { - throw new TypeError("`onUpdate` callback must be a function if provided") + return Vnode(TYPE_LAYOUT, null, callback, null, null) +} + +m.remove = (callback) => { + if (typeof callback !== "function") { + throw new TypeError("Callback must be a function if provided") } - return Vnode(TYPE_LAYOUT, null, onCreate, onUpdate, null) + return Vnode(TYPE_REMOVE, null, callback, null, null) } m.Fragment = (attrs) => attrs.children @@ -384,43 +397,36 @@ var updateFragment = (old, vnode) => { } var updateNode = (old, vnode) => { + // This is important. Declarative state bindings that rely on dependency tracking, like + // https://github.com/tc39/proposal-signals and related, memoize their results, but that's the + // absolute extent of what they necessarily reuse. They don't pool anything. That means all I + // need to do to support components based on them is just add this neat single line of code + // here. + // + // Code based on streams (see this repo here) will also potentially need this depending on how + // they do their combinators. + if (old === vnode) return + var type if (old == null) { if (vnode == null) return + if (vnode.m < 0) { + throw new Error("No node present to retain with `m.retain()`") + } if (vnode.m & FLAG_USED) { throw new TypeError("Vnodes must not be reused") } type = vnode.m & TYPE_MASK vnode.m |= FLAG_USED - if (type === TYPE_RETAIN) { - throw new Error("No node present to retain with `m.retain()`") - } } else { type = old.m & TYPE_MASK if (vnode == null) { - if (type === TYPE_COMPONENT) { - updateNode(old.c, null) - } else if (type === TYPE_LAYOUT) { - try { - old.d.abort() - } catch (e) { - console.error(e) - } - } else { - if ((1 << TYPE_TEXT | 1 << TYPE_ELEMENT) & 1 << type) old.d.remove() - if (type !== TYPE_TEXT) updateFragment(old, null) - } + removeNodeDispatch[type](old) return } - if (vnode.m & FLAG_USED) { - throw new TypeError("Vnodes must not be reused") - } - - var newType = vnode.m & TYPE_MASK - - if (newType === TYPE_RETAIN) { + if (vnode.m < 0) { // If it's a retain node, transmute it into the node it's retaining. Makes it much easier // to implement and work with. // @@ -434,10 +440,16 @@ var updateNode = (old, vnode) => { return } + if (vnode.m & FLAG_USED) { + throw new TypeError("Vnodes must not be reused") + } + + var newType = vnode.m & TYPE_MASK + if (type === newType && vnode.t === old.t) { vnode.m = old.m & ~FLAG_KEYED | vnode.m & FLAG_KEYED } else { - updateNode(old, null) + removeNodeDispatch[type](old) type = newType old = null } @@ -446,9 +458,13 @@ var updateNode = (old, vnode) => { updateNodeDispatch[type](old, vnode) } -var updateLayout = (old, vnode) => { - vnode.d = old == null ? new AbortController() : old.d - currentHooks.push(old == null ? vnode.s : vnode.a, currentParent, vnode.d.signal) +var updateLayout = (_, vnode) => { + vnode.d = currentParent + currentHooks.push(vnode) +} + +var updateRemove = (_, vnode) => { + vnode.d = currentParent } var updateText = (old, vnode) => { @@ -588,13 +604,26 @@ var updateComponent = (old, vnode) => { // Replaces an otherwise necessary `switch`. var updateNodeDispatch = [ - null, updateFragment, updateFragment, - updateLayout, updateText, updateElement, updateComponent, + updateLayout, + updateRemove, +] + +var removeNodeDispatch = [ + (old) => updateFragment(old, null), + (old) => updateFragment(old, null), + (old) => old.d.remove(), + (old) => { + old.d.remove() + updateFragment(old, null) + }, + (old) => updateNode(old.c, null), + () => {}, + (old) => currentHooks.push(old), ] //attrs @@ -952,12 +981,9 @@ m.render = (dom, vnodes, redraw) => { if (active != null && currentDocument.activeElement !== active && typeof active.focus === "function") { active.focus() } - for (var i = 0; i < hooks.length; i += 3) { + for (var v of hooks) { try { - var f = hooks[i] - var p = hooks[i + 1] - var s = hooks[i + 2] - if (typeof f === "function") f(p, s) + (0, v.s)(v.d) } catch (e) { console.error(e) } @@ -973,17 +999,13 @@ m.render = (dom, vnodes, redraw) => { } } -m.mount = (root, view, signal) => { +m.mount = (root, view) => { if (!root) throw new TypeError("Root must be an element") if (typeof view !== "function") { throw new TypeError("View must be a function") } - if (signal) { - signal.throwIfAborted() - } - var window = root.ownerDocument.defaultView var id = 0 var unschedule = () => { @@ -994,7 +1016,7 @@ m.mount = (root, view, signal) => { } var redraw = () => { if (!id) id = window.requestAnimationFrame(redraw.sync) } var Mount = (_, old) => [ - m.layout((_, signal) => { signal.onabort = unschedule }), + m.remove(unschedule), view(!old, redraw) ] redraw.sync = () => { @@ -1003,21 +1025,7 @@ m.mount = (root, view, signal) => { } m.render(root, null) - - if (signal) { - signal.throwIfAborted() - } - m.render(root, m(Mount), redraw) - if (signal) { - if (signal.aborted) { - m.render(root, null) - throw signal.reason - } - - signal.addEventListener("abort", () => m.render(root, null), {once: true}) - } - return redraw } diff --git a/src/std/init.js b/src/std/init.js index 29d93f48f..7d12e6dc5 100644 --- a/src/std/init.js +++ b/src/std/init.js @@ -1,9 +1,14 @@ import m from "../core.js" -var Init = ({f}, _, {redraw}) => m.layout(async (_, signal) => { - await 0 // wait for next microtask - if ((await f(signal)) !== false) redraw() -}) +var Init = ({f}, old, {redraw}) => { + if (old) return m.retain() + var ctrl = new AbortController() + void (async () => { + await 0 // wait for next microtask + if ((await f(ctrl.signal)) !== false) redraw() + })() + return m.remove(() => ctrl.abort()) +} var init = (f) => m(Init, {f}) export {init as default} diff --git a/test-utils/global.js b/test-utils/global.js index c69e778ed..40ea6bdde 100644 --- a/test-utils/global.js +++ b/test-utils/global.js @@ -48,7 +48,15 @@ export function setupGlobals(env = {}) { if (env && env.expectNoConsoleError) { console.error = (...args) => { if (typeof process === "function") process.exitCode = 1 - console.trace("Unexpected `console.error` call") + var replacement = console.error + // Node's `console.trace` delegates to `console.error` as a property. Have it + // actually call what it intended to call. + try { + console.error = originalConsoleError + console.trace("Unexpected `console.error` call") + } finally { + console.error = replacement + } originalConsoleError.apply(console, args) } } diff --git a/tests/api/mountRedraw.js b/tests/api/mountRedraw.js index cef72841d..5046c12e1 100644 --- a/tests/api/mountRedraw.js +++ b/tests/api/mountRedraw.js @@ -112,15 +112,15 @@ o.spec("mount/redraw", function() { }) o("should invoke remove callback on unmount", function() { - var onabort = o.spy() - var spy = o.spy(() => m.layout((_, signal) => { signal.onabort = onabort })) + var onRemove = o.spy() + var spy = o.spy(() => m.remove(onRemove)) m.mount(G.root, spy) o(spy.callCount).equals(1) m.render(G.root, null) o(spy.callCount).equals(1) - o(onabort.callCount).equals(1) + o(onRemove.callCount).equals(1) o(console.error.calls.map((c) => c.args[0])).deepEquals([]) o(G.rafMock.queueLength()).equals(0) @@ -366,11 +366,11 @@ o.spec("mount/redraw", function() { m.mount(G.root, () => m("div", { onclick: onclick, - }, m.layout(() => layout(true), () => layout(false)))) + }, m.layout(layout))) G.root.firstChild.dispatchEvent(e) - o(layout.calls.map((c) => c.args[0])).deepEquals([true]) + o(layout.callCount).equals(1) o(onclick.callCount).equals(1) o(onclick.this).equals(G.root.firstChild) @@ -379,7 +379,7 @@ o.spec("mount/redraw", function() { G.rafMock.fire() - o(layout.calls.map((c) => c.args[0])).deepEquals([true, false]) + o(layout.callCount).equals(2) o(console.error.calls.map((c) => c.args[0])).deepEquals([]) o(G.rafMock.queueLength()).equals(0) @@ -401,15 +401,15 @@ o.spec("mount/redraw", function() { m.mount(root1, () => m("div", { onclick: onclick0, - }, m.layout(() => layout0(true), () => layout0(false)))) + }, m.layout(layout0))) - o(layout0.calls.map((c) => c.args[0])).deepEquals([true]) + o(layout0.callCount).equals(1) m.mount(root2, () => m("div", { onclick: onclick1, - }, m.layout(() => layout1(true), () => layout1(false)))) + }, m.layout(layout1))) - o(layout1.calls.map((c) => c.args[0])).deepEquals([true]) + o(layout1.callCount).equals(1) root1.firstChild.dispatchEvent(e) o(onclick0.callCount).equals(1) @@ -417,8 +417,8 @@ o.spec("mount/redraw", function() { G.rafMock.fire() - o(layout0.calls.map((c) => c.args[0])).deepEquals([true, false]) - o(layout1.calls.map((c) => c.args[0])).deepEquals([true]) + o(layout0.callCount).equals(2) + o(layout1.callCount).equals(1) root2.firstChild.dispatchEvent(e) @@ -427,8 +427,8 @@ o.spec("mount/redraw", function() { G.rafMock.fire() - o(layout0.calls.map((c) => c.args[0])).deepEquals([true, false]) - o(layout1.calls.map((c) => c.args[0])).deepEquals([true, false]) + o(layout0.callCount).equals(2) + o(layout1.callCount).equals(2) o(console.error.calls.map((c) => c.args[0])).deepEquals([]) o(G.rafMock.queueLength()).equals(0) @@ -444,15 +444,15 @@ o.spec("mount/redraw", function() { m.mount(G.root, () => m("div", { onclick: () => false, - }, m.layout(() => layout(true), () => layout(false)))) + }, m.layout(layout))) G.root.firstChild.dispatchEvent(e) - o(layout.calls.map((c) => c.args[0])).deepEquals([true]) + o(layout.callCount).equals(1) G.rafMock.fire() - o(layout.calls.map((c) => c.args[0])).deepEquals([true]) + o(layout.callCount).equals(1) o(console.error.calls.map((c) => c.args[0])).deepEquals([]) o(G.rafMock.queueLength()).equals(0) @@ -461,43 +461,39 @@ o.spec("mount/redraw", function() { o("redraws when the render function is run", function() { var layout = o.spy() - var redraw = m.mount(G.root, () => m("div", m.layout(() => layout(true), () => layout(false)))) + var redraw = m.mount(G.root, () => m("div", m.layout(layout))) - o(layout.calls.map((c) => c.args[0])).deepEquals([true]) + o(layout.callCount).equals(1) redraw() G.rafMock.fire() - o(layout.calls.map((c) => c.args[0])).deepEquals([true, false]) + o(layout.callCount).equals(2) o(console.error.calls.map((c) => c.args[0])).deepEquals([]) o(G.rafMock.queueLength()).equals(0) }) o("remounts after `m.render(G.root, null)` is invoked on the mounted root", function() { - var onabort = o.spy() - var onCreate = o.spy((_, signal) => { signal.onabort = onabort }) - var onUpdate = o.spy((_, signal) => { signal.onabort = onabort }) + var onRemove = o.spy() + var onLayout = o.spy() - var redraw = m.mount(G.root, () => m("div", m.layout(onCreate, onUpdate))) + var redraw = m.mount(G.root, () => m("div", m.layout(onLayout), m.remove(onRemove))) - o(onCreate.callCount).equals(1) - o(onUpdate.callCount).equals(0) - o(onabort.callCount).equals(0) + o(onLayout.callCount).equals(1) + o(onRemove.callCount).equals(0) m.render(G.root, null) - o(onCreate.callCount).equals(1) - o(onUpdate.callCount).equals(0) - o(onabort.callCount).equals(1) + o(onLayout.callCount).equals(1) + o(onRemove.callCount).equals(1) redraw() G.rafMock.fire() - o(onCreate.callCount).equals(2) - o(onUpdate.callCount).equals(0) - o(onabort.callCount).equals(1) + o(onLayout.callCount).equals(2) + o(onRemove.callCount).equals(1) o(console.error.calls.map((c) => c.args[0])).deepEquals([]) o(G.rafMock.queueLength()).equals(0) diff --git a/tests/render/component.js b/tests/render/component.js index c9e257ccc..d06c21bca 100644 --- a/tests/render/component.js +++ b/tests/render/component.js @@ -313,88 +313,47 @@ o.spec("component", function() { o(component.callCount).equals(1) }) - o("calls inner `m.layout` create callback on first render", function() { - var onabort = o.spy() - var createSpy = o.spy((_, signal) => { signal.onabort = onabort }) - var updateSpy = o.spy((_, signal) => { signal.onabort = onabort }) + o("calls inner `m.layout` callback on render", function() { + var layoutSpy = o.spy() var component = () => [ - m.layout(createSpy, updateSpy), + m.layout(layoutSpy), m("div", {id: "a"}, "b"), ] m.render(G.root, m(component)) - o(createSpy.callCount).equals(1) - o(createSpy.args[0]).equals(G.root) - o(createSpy.args[1].aborted).equals(false) - o(updateSpy.callCount).equals(0) - o(onabort.callCount).equals(0) + o(layoutSpy.callCount).equals(1) + o(layoutSpy.calls[0].args[0]).equals(G.root) o(G.root.firstChild.nodeName).equals("DIV") o(G.root.firstChild.attributes["id"].value).equals("a") o(G.root.firstChild.firstChild.nodeValue).equals("b") - }) - o("calls inner `m.layout` update callback on subsequent render", function() { - var onabort = o.spy() - var createSpy = o.spy((_, signal) => { signal.onabort = onabort }) - var updateSpy = o.spy((_, signal) => { signal.onabort = onabort }) - var component = () => [ - m.layout(createSpy, updateSpy), - m("div", {id: "a"}, "b"), - ] - m.render(G.root, m(component)) m.render(G.root, m(component)) - o(createSpy.callCount).equals(1) - o(updateSpy.callCount).equals(1) - o(updateSpy.args[0]).equals(G.root) - o(updateSpy.args[1].aborted).equals(false) - o(updateSpy.args[1]).equals(createSpy.args[1]) - o(onabort.callCount).equals(0) + o(layoutSpy.callCount).equals(2) + o(layoutSpy.calls[1].args[0]).equals(G.root) o(G.root.firstChild.nodeName).equals("DIV") o(G.root.firstChild.attributes["id"].value).equals("a") o(G.root.firstChild.firstChild.nodeValue).equals("b") }) - o("calls inner `m.layout` update callback on subsequent render without a create callback", function() { - var onabort = o.spy() - var updateSpy = o.spy((_, signal) => { signal.onabort = onabort }) + o("calls inner `m.remove` callback after first render", function() { + var removeSpy = o.spy() var component = () => [ - m.layout(null, updateSpy), - m("div", {id: "a"}, "b"), - ] - - m.render(G.root, m(component)) - m.render(G.root, m(component)) - - o(updateSpy.callCount).equals(1) - o(updateSpy.args[0]).equals(G.root) - o(updateSpy.args[1].aborted).equals(false) - o(onabort.callCount).equals(0) - o(G.root.firstChild.nodeName).equals("DIV") - o(G.root.firstChild.attributes["id"].value).equals("a") - o(G.root.firstChild.firstChild.nodeValue).equals("b") - }) - o("aborts inner `m.layout` signal after first render", function() { - var onabort = o.spy() - var createSpy = o.spy((_, signal) => { signal.onabort = onabort }) - var component = () => [ - m.layout(createSpy), + m.remove(removeSpy), m("div", {id: "a"}, "b"), ] m.render(G.root, m(component)) m.render(G.root, null) - o(createSpy.callCount).equals(1) - o(createSpy.args[1].aborted).equals(true) - o(onabort.callCount).equals(1) + o(removeSpy.callCount).equals(1) + o(removeSpy.args[0]).equals(G.root) o(G.root.childNodes.length).equals(0) }) - o("aborts inner `m.layout` signal after subsequent render", function() { - var onabort = o.spy() - var createSpy = o.spy((_, signal) => { signal.onabort = onabort }) + o("calls inner `m.remove` callback after subsequent render", function() { + var removeSpy = o.spy() var component = () => [ - m.layout(createSpy), + m.remove(removeSpy), m("div", {id: "a"}, "b"), ] @@ -402,116 +361,90 @@ o.spec("component", function() { m.render(G.root, m(component)) m.render(G.root, null) - o(createSpy.callCount).equals(1) - o(createSpy.args[1].aborted).equals(true) - o(onabort.callCount).equals(1) + o(removeSpy.callCount).equals(1) + o(removeSpy.args[0]).equals(G.root) o(G.root.childNodes.length).equals(0) }) - o("calls in-element inner `m.layout` create callback on first render", function() { - var onabort = o.spy() - var createSpy = o.spy((_, signal) => { signal.onabort = onabort }) - var component = () => m("div", {id: "a"}, m.layout(createSpy), "b") + o("calls in-element inner `m.layout` callback on render", function() { + var layoutSpy = o.spy() + var component = () => m("div", {id: "a"}, m.layout(layoutSpy), "b") m.render(G.root, m(component)) - o(createSpy.callCount).equals(1) - o(createSpy.args[0]).equals(G.root.firstChild) - o(createSpy.args[1].aborted).equals(false) - o(onabort.callCount).equals(0) + o(layoutSpy.callCount).equals(1) + o(layoutSpy.calls[0].args[0]).equals(G.root.firstChild) o(G.root.firstChild.nodeName).equals("DIV") o(G.root.firstChild.attributes["id"].value).equals("a") o(G.root.firstChild.firstChild.nodeValue).equals("b") - }) - o("calls in-element inner `m.layout` update callback on subsequent render", function() { - var onabort = o.spy() - var updateSpy = o.spy((_, signal) => { signal.onabort = onabort }) - var component = () => m("div", {id: "a"}, m.layout(null, updateSpy), "b") - m.render(G.root, m(component)) + m.render(G.root, m(component)) - o(updateSpy.callCount).equals(1) - o(updateSpy.args[0]).equals(G.root.firstChild) - o(updateSpy.args[1].aborted).equals(false) - o(onabort.callCount).equals(0) + o(layoutSpy.callCount).equals(2) + o(layoutSpy.calls[1].args[0]).equals(G.root.firstChild) o(G.root.firstChild.nodeName).equals("DIV") o(G.root.firstChild.attributes["id"].value).equals("a") o(G.root.firstChild.firstChild.nodeValue).equals("b") }) - o("aborts in-element inner `m.layout` signal after first render", function() { - var onabort = o.spy() - var createSpy = o.spy((_, signal) => { signal.onabort = onabort }) - var component = () => m("div", {id: "a"}, m.layout(createSpy), "b") + o("calls in-element inner `m.remove` callback after first render", function() { + var removeSpy = o.spy() + var component = () => m("div", {id: "a"}, m.remove(removeSpy), "b") m.render(G.root, m(component)) + var firstChild = G.root.firstChild m.render(G.root, null) - o(createSpy.callCount).equals(1) - o(createSpy.args[1].aborted).equals(true) - o(onabort.callCount).equals(1) + o(removeSpy.callCount).equals(1) + o(removeSpy.args[0]).equals(firstChild) o(G.root.childNodes.length).equals(0) }) - o("aborts in-element inner `m.layout` signal after subsequent render", function() { - var onabort = o.spy() - var updateSpy = o.spy((_, signal) => { signal.onabort = onabort }) - var component = () => m("div", {id: "a"}, m.layout(null, updateSpy), "b") + o("calls in-element inner `m.remove` callback after subsequent render", function() { + var removeSpy = o.spy() + var component = () => m("div", {id: "a"}, m.remove(removeSpy), "b") m.render(G.root, m(component)) m.render(G.root, m(component)) + var firstChild = G.root.firstChild m.render(G.root, null) - o(updateSpy.callCount).equals(1) - o(updateSpy.args[1].aborted).equals(true) - o(onabort.callCount).equals(1) + o(removeSpy.callCount).equals(1) + o(removeSpy.args[0]).equals(firstChild) o(G.root.childNodes.length).equals(0) }) - o("calls direct inner `m.layout` create callback on first render", function() { - var onabort = o.spy() - var createSpy = o.spy((_, signal) => { signal.onabort = onabort }) + o("calls direct inner `m.layout` callback on render", function() { + var createSpy = o.spy() var component = () => m.layout(createSpy) + m.render(G.root, m(component)) o(createSpy.callCount).equals(1) - o(createSpy.args[0]).equals(G.root) - o(createSpy.args[1].aborted).equals(false) - o(onabort.callCount).equals(0) + o(createSpy.calls[0].args[0]).equals(G.root) o(G.root.childNodes.length).equals(0) - }) - o("calls direct inner `m.layout` update callback on subsequent render", function() { - var onabort = o.spy() - var updateSpy = o.spy((_, signal) => { signal.onabort = onabort }) - var component = () => m.layout(updateSpy) - m.render(G.root, m(component)) + m.render(G.root, m(component)) - o(updateSpy.callCount).equals(1) - o(updateSpy.args[0]).equals(G.root) - o(updateSpy.args[1].aborted).equals(false) - o(onabort.callCount).equals(0) + o(createSpy.callCount).equals(2) + o(createSpy.calls[1].args[0]).equals(G.root) o(G.root.childNodes.length).equals(0) }) - o("aborts direct inner `m.layout` signal after first render", function() { - var onabort = o.spy() - var createSpy = o.spy((_, signal) => { signal.onabort = onabort }) - var component = () => m.layout(createSpy) + o("calls direct inner `m.remove` callback after first render", function() { + var removeSpy = o.spy() + var component = () => m.layout(removeSpy) m.render(G.root, m(component)) m.render(G.root, null) - o(createSpy.callCount).equals(1) - o(createSpy.args[1].aborted).equals(true) - o(onabort.callCount).equals(1) + o(removeSpy.callCount).equals(1) + o(removeSpy.args[0]).equals(G.root) o(G.root.childNodes.length).equals(0) }) - o("aborts direct inner `m.layout` signal after subsequent render", function() { - var onabort = o.spy() - var updateSpy = o.spy((_, signal) => { signal.onabort = onabort }) - var component = () => m.layout(updateSpy) + o("calls direct inner `m.remove` callback after subsequent render", function() { + var removeSpy = o.spy() + var component = () => m.remove(removeSpy) m.render(G.root, m(component)) m.render(G.root, m(component)) m.render(G.root, null) - o(updateSpy.callCount).equals(1) - o(updateSpy.args[1].aborted).equals(true) - o(onabort.callCount).equals(1) + o(removeSpy.callCount).equals(1) + o(removeSpy.args[0]).equals(G.root) o(G.root.childNodes.length).equals(0) }) - o("no recycling occurs (was: recycled components get a fresh state)", function() { + o("no recycling observable with `m.layout` (was: recycled components get a fresh state)", function() { var createSpy = o.spy() var component = o.spy(() => m("div", m.layout(createSpy))) @@ -523,5 +456,19 @@ o.spec("component", function() { o(child).notEquals(G.root.firstChild.firstChild) // this used to be a recycling pool test o(component.callCount).equals(2) }) + o("no recycling observable with `m.remove` (was: recycled components get a fresh state)", function() { + var createSpy = o.spy() + var component = o.spy(() => m("div", m.remove(createSpy))) + + m.render(G.root, [m("div", m.key(1, m(component)))]) + var child = G.root.firstChild.firstChild + m.render(G.root, []) + m.render(G.root, [m("div", m.key(1, m(component)))]) + var found = G.root.firstChild.firstChild + m.render(G.root, []) + + o(child).notEquals(found) // this used to be a recycling pool test + o(component.callCount).equals(2) + }) }) }) diff --git a/tests/render/oncreate.js b/tests/render/oncreate.js index f34fa8e10..4d109f0f7 100644 --- a/tests/render/oncreate.js +++ b/tests/render/oncreate.js @@ -7,135 +7,229 @@ import m from "../../src/entry/mithril.esm.js" o.spec("layout create", function() { var G = setupGlobals() - o("works when rendered directly", function() { - var callback = o.spy() - var vnode = m.layout(callback) - - m.render(G.root, vnode) - - o(callback.callCount).equals(1) - o(callback.args[0]).equals(G.root) - o(callback.args[1].aborted).equals(false) + o.spec("m.layout", () => { + o("works when rendered directly", function() { + var layoutSpy = o.spy() + var vnode = m.layout(layoutSpy) + + m.render(G.root, vnode) + + o(layoutSpy.callCount).equals(1) + o(layoutSpy.args[0]).equals(G.root) + }) + o("works when creating element", function() { + var layoutSpy = o.spy() + var vnode = m("div", m.layout(layoutSpy)) + + m.render(G.root, vnode) + + o(layoutSpy.callCount).equals(1) + }) + o("works when creating fragment", function() { + var layoutSpy = o.spy() + var vnode = [m.layout(layoutSpy)] + + m.render(G.root, vnode) + + o(layoutSpy.callCount).equals(1) + }) + o("works when replacing same-keyed", function() { + var createDiv = o.spy() + var createA = o.spy() + var vnode = m("div", m.layout(createDiv)) + var updated = m("a", m.layout(createA)) + + m.render(G.root, m.key(1, vnode)) + m.render(G.root, m.key(1, updated)) + + o(createDiv.callCount).equals(1) + o(createA.callCount).equals(1) + }) + o("works when creating other children", function() { + var create = o.spy() + var vnode = m("div", m.layout(create), m("a")) + + m.render(G.root, vnode) + + o(create.callCount).equals(1) + o(create.args[0]).equals(G.root.firstChild) + }) + o("works inside keyed", function() { + var create = o.spy() + var vnode = m("div", m.layout(create)) + var otherVnode = m("a") + + m.render(G.root, [m.key(1, vnode), m.key(2, otherVnode)]) + + o(create.callCount).equals(1) + o(create.args[0]).equals(G.root.firstChild) + }) + o("does not invoke callback when removing, but aborts the provided signal", function() { + var create = o.spy() + var vnode = m("div", m.layout(create)) + + m.render(G.root, vnode) + + o(create.callCount).equals(1) + + m.render(G.root, []) + + o(create.callCount).equals(1) + }) + o("works at the same step as layout update", function() { + var create = o.spy() + var update = o.spy() + var layoutSpy = o.spy() + var vnode = m("div", m.layout(create)) + var updated = m("div", m.layout(update), m("a", m.layout(layoutSpy))) + + m.render(G.root, vnode) + m.render(G.root, updated) + + o(create.callCount).equals(1) + o(create.args[0]).equals(G.root.firstChild) + + o(update.callCount).equals(1) + o(update.args[0]).equals(G.root.firstChild) + + o(layoutSpy.callCount).equals(1) + o(layoutSpy.args[0]).equals(G.root.firstChild.firstChild) + }) + o("works on unkeyed that falls into reverse list diff code path", function() { + var create = o.spy() + m.render(G.root, [m.key(1, m("p")), m.key(2, m("div"))]) + m.render(G.root, [m.key(2, m("div", m.layout(create))), m.key(1, m("p"))]) + + o(create.callCount).equals(1) + o(create.args[0]).equals(G.root.firstChild) + }) + o("works on unkeyed that falls into forward list diff code path", function() { + var create = o.spy() + m.render(G.root, [m("div"), m("p")]) + m.render(G.root, [m("div"), m("div", m.layout(create))]) + + o(create.callCount).equals(1) + o(create.args[0]).equals(G.root.childNodes[1]) + }) + o("works after full DOM creation", function() { + var created = false + var vnode = m("div", m("a", m.layout(create), m("b"))) + + m.render(G.root, vnode) + + function create(dom) { + created = true + + o(dom.parentNode).equals(G.root.firstChild) + o(dom.childNodes.length).equals(1) + } + o(created).equals(true) + }) }) - o("works when creating element", function() { - var callback = o.spy() - var vnode = m("div", m.layout(callback)) - - m.render(G.root, vnode) - - o(callback.callCount).equals(1) - o(callback.args[1].aborted).equals(false) - }) - o("works when creating fragment", function() { - var callback = o.spy() - var vnode = [m.layout(callback)] - - m.render(G.root, vnode) - - o(callback.callCount).equals(1) - o(callback.args[1].aborted).equals(false) - }) - o("works when replacing same-keyed", function() { - var createDiv = o.spy() - var createA = o.spy() - var vnode = m("div", m.layout(createDiv)) - var updated = m("a", m.layout(createA)) - - m.render(G.root, m.key(1, vnode)) - m.render(G.root, m.key(1, updated)) - - o(createDiv.callCount).equals(1) - o(createDiv.args[1].aborted).equals(true) - o(createA.callCount).equals(1) - o(createA.args[1].aborted).equals(false) - }) - o("works when creating other children", function() { - var create = o.spy() - var vnode = m("div", m.layout(create), m("a")) - - m.render(G.root, vnode) - - o(create.callCount).equals(1) - o(create.args[0]).equals(G.root.firstChild) - o(create.args[1].aborted).equals(false) - }) - o("works inside keyed", function() { - var create = o.spy() - var vnode = m("div", m.layout(create)) - var otherVnode = m("a") - - m.render(G.root, [m.key(1, vnode), m.key(2, otherVnode)]) - - o(create.callCount).equals(1) - o(create.args[0]).equals(G.root.firstChild) - o(create.args[1].aborted).equals(false) - }) - o("does not invoke callback when removing, but aborts the provided signal", function() { - var create = o.spy() - var vnode = m("div", m.layout(create)) - - m.render(G.root, vnode) - - o(create.callCount).equals(1) - o(create.args[1].aborted).equals(false) - - m.render(G.root, []) - - o(create.callCount).equals(1) - o(create.args[1].aborted).equals(true) - }) - o("works at the same step as layout update", function() { - var create = o.spy() - var update = o.spy() - var callback = o.spy() - var vnode = m("div", m.layout(create)) - var updated = m("div", m.layout(null, update), m("a", m.layout(callback))) - - m.render(G.root, vnode) - m.render(G.root, updated) - - o(create.callCount).equals(1) - o(create.args[0]).equals(G.root.firstChild) - o(create.args[1].aborted).equals(false) - - o(update.callCount).equals(1) - o(update.args[0]).equals(G.root.firstChild) - o(update.args[1].aborted).equals(false) - - o(callback.callCount).equals(1) - o(callback.args[0]).equals(G.root.firstChild.firstChild) - o(callback.args[1].aborted).equals(false) - }) - o("works on unkeyed that falls into reverse list diff code path", function() { - var create = o.spy() - m.render(G.root, [m.key(1, m("p")), m.key(2, m("div"))]) - m.render(G.root, [m.key(2, m("div", m.layout(create))), m.key(1, m("p"))]) - - o(create.callCount).equals(1) - o(create.args[0]).equals(G.root.firstChild) - o(create.args[1].aborted).equals(false) - }) - o("works on unkeyed that falls into forward list diff code path", function() { - var create = o.spy() - m.render(G.root, [m("div"), m("p")]) - m.render(G.root, [m("div"), m("div", m.layout(create))]) - - o(create.callCount).equals(1) - o(create.args[0]).equals(G.root.childNodes[1]) - o(create.args[1].aborted).equals(false) - }) - o("works after full DOM creation", function() { - var created = false - var vnode = m("div", m("a", m.layout(create), m("b"))) - - m.render(G.root, vnode) - - function create(dom) { - created = true - o(dom.parentNode).equals(G.root.firstChild) - o(dom.childNodes.length).equals(1) - } - o(created).equals(true) + o.spec("m.remove", () => { + o("works when rendered directly", function() { + var removeSpy = o.spy() + var vnode = m.remove(removeSpy) + + m.render(G.root, vnode) + + o(removeSpy.callCount).equals(0) + }) + o("works when creating element", function() { + var removeSpy = o.spy() + var vnode = m("div", m.remove(removeSpy)) + + m.render(G.root, vnode) + + o(removeSpy.callCount).equals(0) + }) + o("works when creating fragment", function() { + var removeSpy = o.spy() + var vnode = [m.remove(removeSpy)] + + m.render(G.root, vnode) + + o(removeSpy.callCount).equals(0) + }) + o("works when replacing same-keyed", function() { + var createDiv = o.spy() + var createA = o.spy() + var vnode = m("div", m.remove(createDiv)) + var updated = m("a", m.remove(createA)) + + m.render(G.root, m.key(1, vnode)) + m.render(G.root, m.key(1, updated)) + + o(createDiv.callCount).equals(1) + o(createDiv.args[0]).equals(vnode.d) + o(createA.callCount).equals(0) + }) + o("works when creating other children", function() { + var create = o.spy() + var vnode = m("div", m.remove(create), m("a")) + + m.render(G.root, vnode) + + o(create.callCount).equals(0) + }) + o("works inside keyed", function() { + var create = o.spy() + var vnode = m("div", m.remove(create)) + var otherVnode = m("a") + + m.render(G.root, [m.key(1, vnode), m.key(2, otherVnode)]) + + o(create.callCount).equals(0) + }) + o("does not invoke callback when removing, but aborts the provided signal", function() { + var create = o.spy() + var vnode = m("div", m.remove(create)) + + m.render(G.root, vnode) + + o(create.callCount).equals(0) + + m.render(G.root, []) + + o(create.callCount).equals(1) + }) + o("works at the same step as layout update", function() { + var create = o.spy() + var update = o.spy() + var removeSpy = o.spy() + var vnode = m("div", m.remove(create)) + var updated = m("div", m.remove(update), m("a", m.remove(removeSpy))) + + m.render(G.root, vnode) + m.render(G.root, updated) + + o(create.callCount).equals(0) + + o(update.callCount).equals(0) + + o(removeSpy.callCount).equals(0) + }) + o("works on unkeyed that falls into reverse list diff code path", function() { + var create = o.spy() + m.render(G.root, [m.key(1, m("p")), m.key(2, m("div"))]) + m.render(G.root, [m.key(2, m("div", m.remove(create))), m.key(1, m("p"))]) + + o(create.callCount).equals(0) + }) + o("works on unkeyed that falls into forward list diff code path", function() { + var create = o.spy() + m.render(G.root, [m("div"), m("p")]) + m.render(G.root, [m("div"), m("div", m.remove(create))]) + + o(create.callCount).equals(0) + }) + o("works after full DOM creation", function() { + var created = false + var vnode = m("div", m("a", m.remove(() => created = true), m("b"))) + + m.render(G.root, vnode) + o(created).equals(false) + }) }) }) diff --git a/tests/render/onremove.js b/tests/render/onremove.js index 36b6f3d7d..725d83096 100644 --- a/tests/render/onremove.js +++ b/tests/render/onremove.js @@ -7,13 +7,11 @@ import m from "../../src/entry/mithril.esm.js" o.spec("layout remove", function() { var G = setupGlobals() - var layoutRemove = (onabort) => m.layout((_, signal) => { signal.onabort = onabort }) - o("does not abort layout signal when creating", function() { var create = o.spy() var update = o.spy() - var vnode = m("div", layoutRemove(create)) - var updated = m("div", layoutRemove(update)) + var vnode = m("div", m.remove(create)) + var updated = m("div", m.remove(update)) m.render(G.root, vnode) m.render(G.root, updated) @@ -23,8 +21,8 @@ o.spec("layout remove", function() { o("does not abort layout signal when updating", function() { var create = o.spy() var update = o.spy() - var vnode = m("div", layoutRemove(create)) - var updated = m("div", layoutRemove(update)) + var vnode = m("div", m.remove(create)) + var updated = m("div", m.remove(update)) m.render(G.root, vnode) m.render(G.root, updated) @@ -34,7 +32,7 @@ o.spec("layout remove", function() { }) o("aborts layout signal when removing element", function() { var remove = o.spy() - var vnode = m("div", layoutRemove(remove)) + var vnode = m("div", m.remove(remove)) m.render(G.root, vnode) m.render(G.root, []) @@ -43,7 +41,7 @@ o.spec("layout remove", function() { }) o("aborts layout signal when removing fragment", function() { var remove = o.spy() - var vnode = [layoutRemove(remove)] + var vnode = [m.remove(remove)] m.render(G.root, vnode) m.render(G.root, []) @@ -53,7 +51,7 @@ o.spec("layout remove", function() { o("aborts layout signal on keyed nodes", function() { var remove = o.spy() var vnode = m("div") - var temp = m("div", layoutRemove(remove)) + var temp = m("div", m.remove(remove)) var updated = m("div") m.render(G.root, m.key(1, vnode)) @@ -76,7 +74,7 @@ o.spec("layout remove", function() { o("aborts layout signal on nested component child", function() { var spy = o.spy() var comp = () => m(outer) - var outer = () => m(inner, m("a", layoutRemove(spy))) + var outer = () => m(inner, m("a", m.remove(spy))) var inner = (attrs) => m("div", attrs.children) m.render(G.root, m(comp)) m.render(G.root, null) diff --git a/tests/render/onupdate.js b/tests/render/onupdate.js index 145731317..4714275de 100644 --- a/tests/render/onupdate.js +++ b/tests/render/onupdate.js @@ -9,88 +9,88 @@ o.spec("layout update", function() { o("is not invoked when removing element", function() { var update = o.spy() - var vnode = m("div", m.layout(null, update)) + var vnode = m("div", m.layout(update)) m.render(G.root, vnode) m.render(G.root, []) - o(update.callCount).equals(0) + o(update.callCount).equals(1) }) o("is not updated when replacing keyed element", function() { var update = o.spy() - var vnode = m.key(1, m("div", m.layout(null, update))) - var updated = m.key(1, m("a", m.layout(null, update))) + var vnode = m.key(1, m("div", m.layout(update))) + var updated = m.key(1, m("a", m.layout(update))) m.render(G.root, vnode) m.render(G.root, updated) - o(update.callCount).equals(0) + o(update.callCount).equals(2) }) o("does not call old callback when removing layout vnode from new vnode", function() { var update = o.spy() - m.render(G.root, m("a", m.layout(null, update))) - m.render(G.root, m("a", m.layout(null, update))) + m.render(G.root, m("a", m.layout(update))) + m.render(G.root, m("a", m.layout(update))) m.render(G.root, m("a")) - o(update.callCount).equals(1) + o(update.callCount).equals(2) }) o("invoked on noop", function() { var preUpdate = o.spy() var update = o.spy() - var vnode = m("div", m.layout(null, preUpdate)) - var updated = m("div", m.layout(null, update)) + var vnode = m("div", m.layout(preUpdate)) + var updated = m("div", m.layout(update)) m.render(G.root, vnode) m.render(G.root, updated) - o(preUpdate.callCount).equals(0) + o(preUpdate.callCount).equals(1) o(update.callCount).equals(1) }) o("invoked on updating attr", function() { var preUpdate = o.spy() var update = o.spy() - var vnode = m("div", m.layout(null, preUpdate)) - var updated = m("div", {id: "a"}, m.layout(null, update)) + var vnode = m("div", m.layout(preUpdate)) + var updated = m("div", {id: "a"}, m.layout(update)) m.render(G.root, vnode) m.render(G.root, updated) - o(preUpdate.callCount).equals(0) + o(preUpdate.callCount).equals(1) o(update.callCount).equals(1) }) o("invoked on updating children", function() { var preUpdate = o.spy() var update = o.spy() - var vnode = m("div", m.layout(null, preUpdate), m("a")) - var updated = m("div", m.layout(null, update), m("b")) + var vnode = m("div", m.layout(preUpdate), m("a")) + var updated = m("div", m.layout(update), m("b")) m.render(G.root, vnode) m.render(G.root, updated) - o(preUpdate.callCount).equals(0) + o(preUpdate.callCount).equals(1) o(update.callCount).equals(1) }) o("invoked on updating fragment", function() { var preUpdate = o.spy() var update = o.spy() - var vnode = [m.layout(null, preUpdate)] - var updated = [m.layout(null, update)] + var vnode = [m.layout(preUpdate)] + var updated = [m.layout(update)] m.render(G.root, vnode) m.render(G.root, updated) - o(preUpdate.callCount).equals(0) + o(preUpdate.callCount).equals(1) o(update.callCount).equals(1) }) o("invoked on full DOM update", function() { var called = false var vnode = m("div", {id: "1"}, - m("a", {id: "2"}, m.layout(null, null), + m("a", {id: "2"}, m.layout(() => {}), m("b", {id: "3"}) ) ) var updated = m("div", {id: "11"}, - m("a", {id: "22"}, m.layout(null, update), + m("a", {id: "22"}, m.layout(update), m("b", {id: "33"}) ) ) diff --git a/tests/render/render.js b/tests/render/render.js index 59e2e23cc..bb673fb86 100644 --- a/tests/render/render.js +++ b/tests/render/render.js @@ -84,21 +84,19 @@ o.spec("render", function() { o(view.callCount).equals(2) }) o("lifecycle methods work in keyed children of recycled keyed", function() { - var onabortA = o.spy() - var onabortB = o.spy() - var createA = o.spy((_, signal) => { signal.onabort = onabortA }) - var updateA = o.spy((_, signal) => { signal.onabort = onabortA }) - var createB = o.spy((_, signal) => { signal.onabort = onabortB }) - var updateB = o.spy((_, signal) => { signal.onabort = onabortB }) + var removeA = o.spy() + var removeB = o.spy() + var layoutA = o.spy() + var layoutB = o.spy() var a = function() { return m.key(1, m("div", - m.key(11, m("div", m.layout(createA, updateA))), + m.key(11, m("div", m.layout(layoutA), m.remove(removeA))), m.key(12, m("div")) )) } var b = function() { return m.key(2, m("div", - m.key(21, m("div", m.layout(createB, updateB))), + m.key(21, m("div", m.layout(layoutB), m.remove(removeB))), m.key(22, m("div")) )) } @@ -109,36 +107,28 @@ o.spec("render", function() { m.render(G.root, a()) var third = G.root.firstChild.firstChild - o(createA.callCount).equals(2) - o(createA.calls[0].args[0]).equals(first) - o(createA.calls[0].args[1].aborted).equals(true) - o(createA.calls[1].args[0]).equals(third) - o(createA.calls[1].args[1].aborted).equals(false) - o(updateA.callCount).equals(0) - o(onabortA.callCount).equals(1) - - o(createB.callCount).equals(1) - o(createB.calls[0].args[0]).equals(second) - o(createB.calls[0].args[1]).notEquals(createA.calls[0].args[1]) - o(createB.calls[0].args[1].aborted).equals(true) - o(updateB.callCount).equals(0) - o(onabortB.callCount).equals(1) + o(layoutA.callCount).equals(2) + o(layoutA.calls[0].args[0]).equals(first) + o(layoutA.calls[1].args[0]).equals(third) + o(removeA.callCount).equals(1) + + o(layoutB.callCount).equals(1) + o(layoutB.calls[0].args[0]).equals(second) + o(removeB.callCount).equals(1) }) o("lifecycle methods work in unkeyed children of recycled keyed", function() { - var onabortA = o.spy() - var onabortB = o.spy() - var createA = o.spy((_, signal) => { signal.onabort = onabortA }) - var updateA = o.spy((_, signal) => { signal.onabort = onabortA }) - var createB = o.spy((_, signal) => { signal.onabort = onabortB }) - var updateB = o.spy((_, signal) => { signal.onabort = onabortB }) + var removeA = o.spy() + var removeB = o.spy() + var layoutA = o.spy() + var layoutB = o.spy() var a = function() { return m.key(1, m("div", - m("div", m.layout(createA, updateA)) + m("div", m.layout(layoutA), m.remove(removeA)) )) } var b = function() { return m.key(2, m("div", - m("div", m.layout(createB, updateB)) + m("div", m.layout(layoutB), m.remove(removeB)) )) } m.render(G.root, a()) @@ -148,75 +138,58 @@ o.spec("render", function() { m.render(G.root, a()) var third = G.root.firstChild.firstChild - o(createA.callCount).equals(2) - o(createA.calls[0].args[0]).equals(first) - o(createA.calls[0].args[1].aborted).equals(true) - o(createA.calls[1].args[0]).equals(third) - o(createA.calls[1].args[1].aborted).equals(false) - o(onabortA.callCount).equals(1) - - o(createB.callCount).equals(1) - o(createB.calls[0].args[0]).equals(second) - o(createB.calls[0].args[1]).notEquals(createA.calls[0].args[1]) - o(createB.calls[0].args[1].aborted).equals(true) - o(onabortB.callCount).equals(1) + o(layoutA.callCount).equals(2) + o(layoutA.calls[0].args[0]).equals(first) + o(layoutA.calls[1].args[0]).equals(third) + o(removeA.callCount).equals(1) + + o(layoutB.callCount).equals(1) + o(layoutB.calls[0].args[0]).equals(second) + o(removeB.callCount).equals(1) }) o("update lifecycle methods work on children of recycled keyed", function() { - var onabortA = o.spy() - var onabortB = o.spy() - var createA = o.spy((_, signal) => { signal.onabort = onabortA }) - var updateA = o.spy((_, signal) => { signal.onabort = onabortA }) - var createB = o.spy((_, signal) => { signal.onabort = onabortB }) - var updateB = o.spy((_, signal) => { signal.onabort = onabortB }) + var removeA = o.spy() + var removeB = o.spy() + var layoutA = o.spy() + var layoutB = o.spy() var a = function() { return m.key(1, m("div", - m("div", m.layout(createA, updateA)) + m("div", m.layout(layoutA), m.remove(removeA)) )) } var b = function() { return m.key(2, m("div", - m("div", m.layout(createB, updateB)) + m("div", m.layout(layoutB), m.remove(removeB)) )) } m.render(G.root, a()) m.render(G.root, a()) var first = G.root.firstChild.firstChild - o(createA.callCount).equals(1) - o(updateA.callCount).equals(1) - o(createA.calls[0].args[0]).equals(first) - o(updateA.calls[0].args[0]).equals(first) - o(createA.calls[0].args[1]).equals(updateA.calls[0].args[1]) - o(createA.calls[0].args[1].aborted).equals(false) - o(onabortA.callCount).equals(0) + o(layoutA.callCount).equals(2) + o(layoutA.calls[0].args[0]).equals(first) + o(layoutA.calls[1].args[0]).equals(first) + o(removeA.callCount).equals(0) m.render(G.root, b()) var second = G.root.firstChild.firstChild - o(createA.callCount).equals(1) - o(updateA.callCount).equals(1) - o(createA.calls[0].args[1].aborted).equals(true) - o(onabortA.callCount).equals(1) + o(layoutA.callCount).equals(2) + o(removeA.callCount).equals(1) - o(createB.callCount).equals(1) - o(updateB.callCount).equals(0) - o(createB.calls[0].args[0]).equals(second) - o(createB.calls[0].args[1].aborted).equals(false) - o(onabortB.callCount).equals(0) + o(layoutB.callCount).equals(1) + o(layoutB.calls[0].args[0]).equals(second) + o(removeB.callCount).equals(0) m.render(G.root, a()) m.render(G.root, a()) var third = G.root.firstChild.firstChild - o(createB.callCount).equals(1) - o(updateB.callCount).equals(0) - o(createB.calls[0].args[1].aborted).equals(true) - o(onabortB.callCount).equals(1) - - o(createA.callCount).equals(2) - o(updateA.callCount).equals(2) - o(createA.calls[1].args[0]).equals(third) - o(createA.calls[1].args[1]).notEquals(updateA.calls[0].args[1]) - o(createA.calls[1].args[1].aborted).equals(false) - o(onabortA.callCount).equals(1) + o(layoutB.callCount).equals(1) + o(removeB.callCount).equals(1) + + o(layoutA.callCount).equals(4) + o(layoutA.calls[2].args[0]).equals(third) + o(layoutA.calls[3].args[0]).equals(third) + o(removeA.callCount).equals(1) }) o("svg namespace is preserved in keyed diff (#1820)", function(){ // note that this only exerciese one branch of the keyed diff algo diff --git a/tests/render/updateNodes.js b/tests/render/updateNodes.js index 0cd92bcfe..2e7866761 100644 --- a/tests/render/updateNodes.js +++ b/tests/render/updateNodes.js @@ -548,11 +548,11 @@ o.spec("updateNodes", function() { o(Array.from(G.root.childNodes[0].childNodes[0].childNodes, (n) => n.nodeName)).deepEquals(["A"]) o(Array.from(G.root.childNodes[1].childNodes, (n) => n.nodeName)).deepEquals([]) }) - o("reused top-level element children are rejected against the same root", function () { + o("reused top-level element children are retained if against the same root and from the most recent render", function () { var cached = m("a") m.render(G.root, cached) - o(() => m.render(G.root, cached)).throws(Error) + m.render(G.root, cached) }) o("reused top-level element children are rejected against a different root", function () { var cached = m("a") @@ -561,11 +561,11 @@ o.spec("updateNodes", function() { m.render(G.root, cached) o(() => m.render(otherRoot, cached)).throws(Error) }) - o("reused inner fragment element children are rejected against the same root", function () { + o("reused inner fragment element children are retained if against the same root and from the most recent render", function () { var cached = m("a") m.render(G.root, [cached]) - o(() => m.render(G.root, [cached])).throws(Error) + m.render(G.root, [cached]) }) o("reused inner fragment element children are rejected against a different root", function () { var cached = m("a") @@ -574,11 +574,11 @@ o.spec("updateNodes", function() { m.render(G.root, [cached]) o(() => m.render(otherRoot, [cached])).throws(Error) }) - o("reused inner element element children are rejected against the same root", function () { + o("reused inner element element children are retained if against the same root and from the most recent render", function () { var cached = m("a") m.render(G.root, m("div", cached)) - o(() => m.render(G.root, m("div", cached))).throws(Error) + m.render(G.root, m("div", cached)) }) o("reused inner element element children are rejected against a different root", function () { var cached = m("a") @@ -587,12 +587,12 @@ o.spec("updateNodes", function() { m.render(G.root, m("div", cached)) o(() => m.render(otherRoot, m("div", cached))).throws(Error) }) - o("reused top-level retain children are rejected against the same root", function () { + o("reused top-level retain children are retained if against the same root and from the most recent render", function () { var cached = m.retain() m.render(G.root, m("a")) m.render(G.root, cached) - o(() => m.render(G.root, cached)).throws(Error) + m.render(G.root, cached) }) o("reused top-level retain children are rejected against a different root", function () { var cached = m.retain() @@ -602,12 +602,12 @@ o.spec("updateNodes", function() { m.render(G.root, cached) o(() => m.render(otherRoot, cached)).throws(Error) }) - o("reused inner fragment retain children are rejected against the same root", function () { + o("reused inner fragment retain children are retained if against the same root and from the most recent render", function () { var cached = m.retain() m.render(G.root, [m("a")]) m.render(G.root, [cached]) - o(() => m.render(G.root, [cached])).throws(Error) + m.render(G.root, [cached]) }) o("reused inner fragment retain children are rejected against a different root", function () { var cached = m.retain() @@ -617,12 +617,12 @@ o.spec("updateNodes", function() { m.render(G.root, [cached]) o(() => m.render(otherRoot, [cached])).throws(Error) }) - o("reused inner element retain children are rejected against the same root", function () { + o("reused inner element retain children are retained if against the same root and from the most recent render", function () { var cached = m.retain() m.render(G.root, m("div", m("a"))) m.render(G.root, m("div", cached)) - o(() => m.render(G.root, m("div", cached))).throws(Error) + m.render(G.root, m("div", cached)) }) o("reused inner element retain children are rejected against a different root", function () { var cached = m.retain() @@ -643,7 +643,7 @@ o.spec("updateNodes", function() { var cached = m("a") m.render(G.root, [cached]) - m.render(G.root, null) + m.render(G.root, [null]) o(() => m.render(G.root, [cached])).throws(Error) }) o("cross-removal reused inner element element children are rejected against the same root", function () { @@ -667,13 +667,61 @@ o.spec("updateNodes", function() { m.render(G.root, [m("a")]) m.render(G.root, [cached]) - m.render(G.root, null) + m.render(G.root, [null]) m.render(G.root, [m("a")]) o(() => m.render(G.root, [cached])).throws(Error) }) o("cross-removal reused inner element retain children are rejected against the same root", function () { var cached = m.retain() + m.render(G.root, m("div", m("a"))) + m.render(G.root, m("div", cached)) + m.render(G.root, m("b")) + m.render(G.root, m("div", m("a"))) + o(() => m.render(G.root, m("div", cached))).throws(Error) + }) + o("cross-replacement reused top-level element children are rejected against the same root", function () { + var cached = m("a") + + m.render(G.root, cached) + m.render(G.root, m("b")) + o(() => m.render(G.root, cached)).throws(Error) + }) + o("cross-replacement reused inner fragment element children are rejected against the same root", function () { + var cached = m("a") + + m.render(G.root, [cached]) + m.render(G.root, [m("b")]) + o(() => m.render(G.root, [cached])).throws(Error) + }) + o("cross-replacement reused inner element element children are rejected against the same root", function () { + var cached = m("a") + + m.render(G.root, m("div", cached)) + m.render(G.root, m("b")) + o(() => m.render(G.root, m("div", cached))).throws(Error) + }) + o("cross-replacement reused top-level retain children are rejected against the same root", function () { + var cached = m.retain() + + m.render(G.root, m("a")) + m.render(G.root, cached) + m.render(G.root, m("b")) + m.render(G.root, m("a")) + o(() => m.render(G.root, cached)).throws(Error) + }) + o("cross-replacement reused inner fragment retain children are rejected against the same root", function () { + var cached = m.retain() + + m.render(G.root, [m("a")]) + m.render(G.root, [cached]) + m.render(G.root, [m("b")]) + m.render(G.root, [m("a")]) + o(() => m.render(G.root, [cached])).throws(Error) + }) + o("cross-replacement reused inner element retain children are rejected against the same root", function () { + var cached = m.retain() + m.render(G.root, m("div", m("a"))) m.render(G.root, m("div", cached)) m.render(G.root, null) @@ -682,61 +730,53 @@ o.spec("updateNodes", function() { }) o("null stays in place", function() { - var onabort = o.spy() - var create = o.spy((_, signal) => { signal.onabort = onabort }) - var update = o.spy((_, signal) => { signal.onabort = onabort }) - var vnodes = [m("div"), m("a", m.layout(create, update))] - var temp = [null, m("a", m.layout(create, update))] - var updated = [m("div"), m("a", m.layout(create, update))] + var remove = o.spy() + var layout = o.spy() + var vnodes = [m("div"), m("a", m.layout(layout), m.remove(remove))] + var temp = [null, m("a", m.layout(layout), m.remove(remove))] + var updated = [m("div"), m("a", m.layout(layout), m.remove(remove))] m.render(G.root, vnodes) var before = vnodes[1].d - o(create.callCount).equals(1) - o(update.callCount).equals(0) - o(onabort.callCount).equals(0) + o(layout.callCount).equals(1) + o(remove.callCount).equals(0) m.render(G.root, temp) - o(create.callCount).equals(1) - o(update.callCount).equals(1) - o(onabort.callCount).equals(0) + o(layout.callCount).equals(2) + o(remove.callCount).equals(0) m.render(G.root, updated) var after = updated[1].d - o(create.callCount).equals(1) - o(update.callCount).equals(2) - o(onabort.callCount).equals(0) + o(layout.callCount).equals(3) + o(remove.callCount).equals(0) o(before).equals(after) }) o("null stays in place if not first", function() { - var onabort = o.spy() - var create = o.spy((_, signal) => { signal.onabort = onabort }) - var update = o.spy((_, signal) => { signal.onabort = onabort }) - var vnodes = [m("b"), m("div"), m("a", m.layout(create, update))] - var temp = [m("b"), null, m("a", m.layout(create, update))] - var updated = [m("b"), m("div"), m("a", m.layout(create, update))] + var remove = o.spy() + var layout = o.spy() + var vnodes = [m("b"), m("div"), m("a", m.layout(layout), m.remove(remove))] + var temp = [m("b"), null, m("a", m.layout(layout), m.remove(remove))] + var updated = [m("b"), m("div"), m("a", m.layout(layout), m.remove(remove))] m.render(G.root, vnodes) var before = vnodes[2].d - o(create.callCount).equals(1) - o(update.callCount).equals(0) - o(onabort.callCount).equals(0) + o(layout.callCount).equals(1) + o(remove.callCount).equals(0) m.render(G.root, temp) - o(create.callCount).equals(1) - o(update.callCount).equals(1) - o(onabort.callCount).equals(0) + o(layout.callCount).equals(2) + o(remove.callCount).equals(0) m.render(G.root, updated) var after = updated[2].d - o(create.callCount).equals(1) - o(update.callCount).equals(2) - o(onabort.callCount).equals(0) + o(layout.callCount).equals(3) + o(remove.callCount).equals(0) o(before).equals(after) }) o("node is recreated if unwrapped from a key", function () { @@ -783,16 +823,14 @@ o.spec("updateNodes", function() { o(G.root.childNodes.length).equals(0) }) o("handles null values in unkeyed lists of different length (#2003)", function() { - var onabort = o.spy() - var create = o.spy((_, signal) => { signal.onabort = onabort }) - var update = o.spy((_, signal) => { signal.onabort = onabort }) + var remove = o.spy() + var layout = o.spy() - m.render(G.root, [m("div", m.layout(create, update)), null]) - m.render(G.root, [null, m("div", m.layout(create, update)), null]) + m.render(G.root, [m("div", m.layout(layout), m.remove(remove)), null]) + m.render(G.root, [null, m("div", m.layout(layout), m.remove(remove)), null]) - o(create.callCount).equals(2) - o(update.callCount).equals(0) - o(onabort.callCount).equals(1) + o(layout.callCount).equals(2) + o(remove.callCount).equals(1) }) o("supports changing the element of a keyed element in a list when traversed bottom-up", function() { m.render(G.root, [m.key(2, m("a"))]) diff --git a/tests/util/use.js b/tests/util/use.js index 4133b1237..2a9ac734c 100644 --- a/tests/util/use.js +++ b/tests/util/use.js @@ -8,61 +8,53 @@ o.spec("m.use", () => { var G = setupGlobals() o("works with empty arrays", () => { - var onabort = o.spy() - var create = o.spy((_, signal) => { signal.onabort = onabort }) - var update = o.spy((_, signal) => { signal.onabort = onabort }) + var layout = o.spy() + var remove = o.spy() - m.render(G.root, m.use([], m.layout(create, update))) - o(create.callCount).equals(1) - o(update.callCount).equals(0) - o(onabort.callCount).equals(0) + m.render(G.root, m.use([], m.layout(layout), m.remove(remove))) + o(layout.callCount).equals(1) + o(remove.callCount).equals(0) - m.render(G.root, m.use([], m.layout(create, update))) - o(create.callCount).equals(1) - o(update.callCount).equals(1) - o(onabort.callCount).equals(0) + m.render(G.root, m.use([], m.layout(layout), m.remove(remove))) + o(layout.callCount).equals(2) + o(remove.callCount).equals(0) m.render(G.root, null) - o(create.callCount).equals(1) - o(update.callCount).equals(1) - o(onabort.callCount).equals(1) + o(layout.callCount).equals(2) + o(remove.callCount).equals(1) }) o("works with equal non-empty arrays", () => { - var onabort = o.spy() - var create = o.spy((_, signal) => { signal.onabort = onabort }) - var update = o.spy((_, signal) => { signal.onabort = onabort }) + var layout = o.spy() + var remove = o.spy() - m.render(G.root, m.use([1], m.layout(create, update))) - o(create.callCount).equals(1) - o(update.callCount).equals(0) - o(onabort.callCount).equals(0) + m.render(G.root, m.use([1], m.layout(layout), m.remove(remove))) + o(layout.callCount).equals(1) + o(remove.callCount).equals(0) - m.render(G.root, m.use([1], m.layout(create, update))) - o(create.callCount).equals(1) - o(update.callCount).equals(1) - o(onabort.callCount).equals(0) + m.render(G.root, m.use([1], m.layout(layout), m.remove(remove))) + o(layout.callCount).equals(2) + o(remove.callCount).equals(0) m.render(G.root, null) - o(create.callCount).equals(1) - o(update.callCount).equals(1) - o(onabort.callCount).equals(1) + o(layout.callCount).equals(2) + o(remove.callCount).equals(1) }) o("works with non-equal same-length non-empty arrays", () => { - var onabort = o.spy() - var initializer = o.spy((_, signal) => { signal.onabort = onabort }) + var remove = o.spy() + var layout = o.spy() - m.render(G.root, m.use([1], m.layout(initializer))) - o(initializer.callCount).equals(1) - o(onabort.callCount).equals(0) + m.render(G.root, m.use([1], m.layout(layout), m.remove(remove))) + o(layout.callCount).equals(1) + o(remove.callCount).equals(0) - m.render(G.root, m.use([2], m.layout(initializer))) - o(initializer.callCount).equals(2) - o(onabort.callCount).equals(1) + m.render(G.root, m.use([2], m.layout(layout), m.remove(remove))) + o(layout.callCount).equals(2) + o(remove.callCount).equals(1) m.render(G.root, null) - o(initializer.callCount).equals(2) - o(onabort.callCount).equals(2) + o(layout.callCount).equals(2) + o(remove.callCount).equals(2) }) }) From 8ddc288c3b2ac19989587516dd747d64f807d24c Mon Sep 17 00:00:00 2001 From: Claudia Meadows Date: Thu, 10 Oct 2024 22:08:40 -0700 Subject: [PATCH 50/95] Add context, migrate router to use it --- src/core.js | 62 ++++++-- src/entry/mithril.esm.js | 5 +- src/std/router.js | 192 +++++++++++++----------- tests/api/router.js | 308 +++++++++++++++++++-------------------- tests/exported-api.js | 25 ++-- tests/render/context.js | 119 +++++++++++++++ 6 files changed, 446 insertions(+), 265 deletions(-) create mode 100644 tests/render/context.js diff --git a/src/core.js b/src/core.js index c1f79b571..4a3e5f85f 100644 --- a/src/core.js +++ b/src/core.js @@ -94,6 +94,7 @@ var TYPE_ELEMENT = 3 var TYPE_COMPONENT = 4 var TYPE_LAYOUT = 5 var TYPE_REMOVE = 6 +var TYPE_SET_CONTEXT = 7 var FLAG_KEYED = 1 << 3 var FLAG_USED = 1 << 4 @@ -226,10 +227,12 @@ m.TYPE_MASK = TYPE_MASK m.TYPE_RETAIN = TYPE_RETAIN m.TYPE_FRAGMENT = TYPE_FRAGMENT m.TYPE_KEY = TYPE_KEY -m.TYPE_LAYOUT = TYPE_LAYOUT m.TYPE_TEXT = TYPE_TEXT m.TYPE_ELEMENT = TYPE_ELEMENT m.TYPE_COMPONENT = TYPE_COMPONENT +m.TYPE_LAYOUT = TYPE_LAYOUT +m.TYPE_REMOVE = TYPE_REMOVE +m.TYPE_SET_CONTEXT = TYPE_SET_CONTEXT // Simple and sweet. Also useful for idioms like `onfoo: m.capture` to drop events without // redrawing. @@ -256,11 +259,17 @@ m.remove = (callback) => { } m.Fragment = (attrs) => attrs.children + m.key = (key, ...children) => createParentVnode(TYPE_KEY, key, null, null, children.length === 1 && Array.isArray(children[0]) ? children[0].slice() : [...children] ) +m.set = (entries, ...children) => + createParentVnode(TYPE_SET_CONTEXT, null, null, entries, + children.length === 1 && Array.isArray(children[0]) ? children[0].slice() : [...children] + ) + m.normalize = (node) => { if (node == null || typeof node === "boolean") return null if (typeof node !== "object") return Vnode(TYPE_TEXT, null, String(node), null, null) @@ -310,6 +319,7 @@ var currentParent var currentRefNode var currentNamespace var currentDocument +var currentContext var insertAfterCurrentRefNode = (child) => { if (currentRefNode) { @@ -325,9 +335,9 @@ var moveToPosition = (vnode) => { while ((type = vnode.m & TYPE_MASK) === TYPE_COMPONENT) { if (!(vnode = vnode.c)) return } - if ((1 << TYPE_FRAGMENT | 1 << TYPE_KEY) & 1 << type) { + if ((1 << TYPE_FRAGMENT | 1 << TYPE_KEY | 1 << TYPE_SET_CONTEXT) & 1 << type) { vnode.c.forEach(moveToPosition) - } else { + } else if ((1 << TYPE_TEXT | 1 << TYPE_ELEMENT) & 1 << type) { insertAfterCurrentRefNode(vnode.d) } } @@ -467,6 +477,26 @@ var updateRemove = (_, vnode) => { vnode.d = currentParent } +var emptyObject = {} + +var updateSet = (old, vnode) => { + var descs = Object.getOwnPropertyDescriptors(vnode.a) + for (var key of Reflect.ownKeys(descs)) { + // Drop the descriptor entirely if it's not enumerable. Setting it to an empty object + // avoids changing its shape, which is useful. + if (!descs[key].enumerable) descs[key] = emptyObject + // Drop the setter if one is present, to keep it read-only. + else if ("set" in descs[key]) descs[key].set = undefined + } + var prevContext = currentContext + try { + currentContext = Object.freeze(Object.create(prevContext, descs)) + updateFragment(old, vnode) + } finally { + currentContext = prevContext + } +} + var updateText = (old, vnode) => { if (old == null) { insertAfterCurrentRefNode(vnode.d = currentDocument.createTextNode(vnode.s)) @@ -584,17 +614,16 @@ var updateElement = (old, vnode) => { var updateComponent = (old, vnode) => { var attrs = vnode.a - var context = {redraw: currentRedraw} - var tree, context, oldInstance, oldAttrs + var tree, oldInstance, oldAttrs rendered: { if (old != null) { tree = old.s oldInstance = old.c oldAttrs = old.a - } else if (typeof (tree = (vnode.s = vnode.t)(attrs, null, context)) !== "function") { + } else if (typeof (tree = (vnode.s = vnode.t)(attrs, oldAttrs, currentContext)) !== "function") { break rendered } - tree = (vnode.s = tree)(attrs, oldAttrs, context) + tree = (vnode.s = tree)(attrs, oldAttrs, currentContext) } if (tree === vnode) { throw new Error("A view cannot return the vnode it received as argument") @@ -602,6 +631,8 @@ var updateComponent = (old, vnode) => { updateNode(oldInstance, vnode.c = m.normalize(tree)) } +var removeFragment = (old) => updateFragment(old, null) + // Replaces an otherwise necessary `switch`. var updateNodeDispatch = [ updateFragment, @@ -611,11 +642,12 @@ var updateNodeDispatch = [ updateComponent, updateLayout, updateRemove, + updateSet, ] var removeNodeDispatch = [ - (old) => updateFragment(old, null), - (old) => updateFragment(old, null), + removeFragment, + removeFragment, (old) => old.d.remove(), (old) => { old.d.remove() @@ -624,6 +656,7 @@ var removeNodeDispatch = [ (old) => updateNode(old.c, null), () => {}, (old) => currentHooks.push(old), + removeFragment, ] //attrs @@ -954,6 +987,10 @@ m.render = (dom, vnodes, redraw) => { throw new TypeError("Node is currently being rendered to and thus is locked.") } + if (redraw != null && typeof redraw !== "function") { + throw new TypeError("Redraw must be a function if given.") + } + var active = dom.ownerDocument.activeElement var namespace = dom.namespaceURI @@ -963,6 +1000,7 @@ m.render = (dom, vnodes, redraw) => { var prevRefNode = currentRefNode var prevNamespace = currentNamespace var prevDocument = currentDocument + var prevContext = currentContext var hooks = currentHooks = [] try { @@ -971,6 +1009,7 @@ m.render = (dom, vnodes, redraw) => { currentRefNode = null currentNamespace = namespace === htmlNs ? null : namespace currentDocument = dom.ownerDocument + currentContext = {redraw} // First time rendering into a node clears it out if (dom.vnodes == null) dom.textContent = "" @@ -981,9 +1020,9 @@ m.render = (dom, vnodes, redraw) => { if (active != null && currentDocument.activeElement !== active && typeof active.focus === "function") { active.focus() } - for (var v of hooks) { + for (var {s, d} of hooks) { try { - (0, v.s)(v.d) + s(d) } catch (e) { console.error(e) } @@ -995,6 +1034,7 @@ m.render = (dom, vnodes, redraw) => { currentRefNode = prevRefNode currentNamespace = prevNamespace currentDocument = prevDocument + currentContext = prevContext currentlyRendering.pop() } } diff --git a/src/entry/mithril.esm.js b/src/entry/mithril.esm.js index 21f9d96ec..bd11e2020 100644 --- a/src/entry/mithril.esm.js +++ b/src/entry/mithril.esm.js @@ -1,14 +1,15 @@ import m from "../core.js" +import {Link, WithRouter} from "../std/router.js" import init from "../std/init.js" import lazy from "../std/lazy.js" import p from "../std/p.js" -import route from "../std/router.js" import tracked from "../std/tracked.js" import use from "../std/use.js" import withProgress from "../std/with-progress.js" -m.route = route +m.WithRouter = WithRouter +m.Link = Link m.p = p m.withProgress = withProgress m.lazy = lazy diff --git a/src/std/router.js b/src/std/router.js index 91320c11c..6ab17ed57 100644 --- a/src/std/router.js +++ b/src/std/router.js @@ -1,103 +1,121 @@ /* global window: false */ import m from "../core.js" -var mustReplace = false -var redraw, routePrefix, currentUrl, currentPath, currentHref +export function WithRouter({prefix, initial: href}) { + if (prefix == null) prefix = "" -var updateRouteWithHref = (href, update) => { - if (currentHref === href) return - currentHref = href - if (update && typeof redraw === "function") redraw() + if (typeof prefix !== "string") { + throw new TypeError("The route prefix must be a string if given") + } - var url = new URL(href) - var urlPath = url.pathname + url.search + url.hash - var index = urlPath.indexOf(routePrefix) - var prefix = routePrefix - if (index < 0) index = urlPath.indexOf(prefix = encodeURI(prefix)) - if (index >= 0) urlPath = urlPath.slice(index + prefix.length) - if (urlPath[0] !== "/") urlPath = `/${urlPath}` + var mustReplace, redraw, currentUrl, currentPath - currentUrl = new URL(urlPath, href) - currentPath = decodeURI(currentUrl.pathname) - mustReplace = false -} + var updateRouteWithHref = () => { + var url = new URL(href) + var urlPath = url.pathname + url.search + url.hash + var decodedPrefix = prefix + var index = urlPath.indexOf(decodedPrefix) + if (index < 0) index = urlPath.indexOf(decodedPrefix = encodeURI(decodedPrefix)) + if (index >= 0) urlPath = urlPath.slice(index + decodedPrefix.length) + if (urlPath[0] !== "/") urlPath = `/${urlPath}` -var updateRoute = () => { - updateRouteWithHref(window.location.href, true) -} + currentUrl = new URL(urlPath, href) + currentPath = decodeURI(currentUrl.pathname) + mustReplace = false + } -var set = (path, {replace, state} = {}) => { - if (!currentUrl) { - throw new ReferenceError("Route state must be fully initialized first") + var updateRoute = () => { + if (href === window.location.href) return + href = window.location.href + redraw() + updateRouteWithHref() } - if (mustReplace) replace = true - mustReplace = true - void (async () => { - await 0 // wait for next microtask - updateRoute() - })() - if (typeof redraw === "function") redraw() - if (typeof window === "object") { - window.history[replace ? "replaceState" : "pushState"](state, "", routePrefix + path) + + var set = (path, {replace, state} = {}) => { + if (mustReplace) replace = true + mustReplace = true + void (async () => { + await 0 // wait for next microtask + updateRoute() + })() + redraw() + if (typeof window === "object") { + window.history[replace ? "replaceState" : "pushState"](state, "", prefix + path) + } } -} -export default { - init(prefix = "#!", redrawFn, href) { - if (!href) { - if (typeof window !== "object") { - throw new TypeError("Outside the DOM, `href` must be provided") - } - window.addEventListener("popstate", updateRoute, false) - window.addEventListener("hashchange", updateRoute, false) - href = window.location.href + if (!href) { + if (typeof window !== "object") { + throw new TypeError("Outside the DOM, `href` must be set") } + href = window.location.href + window.addEventListener("popstate", updateRoute, false) + window.addEventListener("hashchange", updateRoute, false) + } + + updateRouteWithHref() - routePrefix = prefix - redraw = redrawFn - updateRouteWithHref(href, false) - }, - set, - get: () => currentPath + currentUrl.search + currentUrl.hash, - get path() { return currentPath }, - get params() { return currentUrl.searchParams }, - // Let's provide a *right* way to manage a route link, rather than letting people screw up - // accessibility on accident. - link: (opts) => ( - opts.disabled - // If you *really* do want add `onclick` on a disabled link, spread this and add it - // explicitly in your code. - ? {disabled: true, "aria-disabled": "true"} - : { - href: routePrefix + opts.href, - onclick(e) { - if (typeof opts.onclick === "function") { - opts.onclick.apply(this, arguments) - } + return ({children}, _, context) => { + redraw = context.redraw - // Adapted from React Router's implementation: - // https://github.com/ReactTraining/react-router/blob/520a0acd48ae1b066eb0b07d6d4d1790a1d02482/packages/react-router-dom/modules/Link.js - // - // Try to be flexible and intuitive in how we handle links. - // Fun fact: links aren't as obvious to get right as you - // would expect. There's a lot more valid ways to click a - // link than this, and one might want to not simply click a - // link, but right click or command-click it to copy the - // link target, etc. Nope, this isn't just for blind people. - if ( - // Skip if `onclick` prevented default - !e.defaultPrevented && - // Ignore everything but left clicks - (e.button === 0 || e.which === 0 || e.which === 1) && - // Let the browser handle `target=_blank`, etc. - (!e.currentTarget.target || e.currentTarget.target === "_self") && - // No modifier keys - !e.ctrlKey && !e.metaKey && !e.shiftKey && !e.altKey - ) { - set(opts.href, opts) - // Capture the event, and don't double-call `redraw`. - return m.capture(e) - } - }, + return m.set({ + route: { + prefix, + path: currentPath, + params: currentUrl.searchParams, + current: currentPath + currentUrl.search + currentUrl.hash, + set, + }, + }, children) + } +} + +// Let's provide a *right* way to manage a route link, rather than letting people screw up +// accessibility on accident. +// +// Note: this does *not* support disabling. Instead, consider more accessible alternatives like not +// showing the link in the first place. If you absolutely have to disable the link, disable it by +// removing this component (like via `m("div", {disabled}, !disabled && m(Link))`). There's +// friction here for a reason. +export function Link() { + var opts, setRoute + var listener = (ev) => { + // Adapted from React Router's implementation: + // https://github.com/ReactTraining/react-router/blob/520a0acd48ae1b066eb0b07d6d4d1790a1d02482/packages/react-router-dom/modules/Link.js + // + // Try to be flexible and intuitive in how we handle links. + // Fun fact: links aren't as obvious to get right as you + // would expect. There's a lot more valid ways to click a + // link than this, and one might want to not simply click a + // link, but right click or command-click it to copy the + // link target, etc. Nope, this isn't just for blind people. + if ( + // Skip if `onclick` prevented default + !ev.defaultPrevented && + // Ignore everything but left clicks + (ev.button === 0 || ev.which === 0 || ev.which === 1) && + // Let the browser handle `target=_blank`, etc. + (!ev.currentTarget.target || ev.currentTarget.target === "_self") && + // No modifier keys + !ev.ctrlKey && !ev.metaKey && !ev.shiftKey && !ev.altKey + ) { + setRoute(opts.href, opts) + // Capture the event, and don't double-call `redraw`. + return m.capture(ev) + } + } + + return (attrs, old, {route: {prefix, set}}) => { + setRoute = set + opts = attrs + return [ + m.layout((dom) => { + dom.href = prefix + opts.href + if (!old) dom.addEventListener("click", listener) }), + m.remove((dom) => { + dom.removeEventListener("click", listener) + }), + ] + } } diff --git a/tests/api/router.js b/tests/api/router.js index a23fbb245..728c1cf52 100644 --- a/tests/api/router.js +++ b/tests/api/router.js @@ -1,6 +1,6 @@ import o from "ospec" -import {restoreDOMGlobals, setupGlobals} from "../../test-utils/global.js" +import {setupGlobals} from "../../test-utils/global.js" import m from "../../src/entry/mithril.esm.js" @@ -16,36 +16,52 @@ o.spec("route", () => { o("returns the right route on init", () => { G.window.location.href = `${prefix}/` - m.route.init(prefix) - o(m.route.path).equals("/") - o([...m.route.params]).deepEquals([]) + var App = o.spy() + + m.render(G.root, m(m.WithRouter, {prefix}, m(App))) + + o(App.callCount).equals(1) + o(App.args[2].route.path).equals("/") + o([...App.args[2].route.params]).deepEquals([]) o(G.rafMock.queueLength()).equals(0) }) o("returns alternate right route on init", () => { G.window.location.href = `${prefix}/test` - m.route.init(prefix) - o(m.route.path).equals("/test") - o([...m.route.params]).deepEquals([]) + var App = o.spy() + + m.render(G.root, m(m.WithRouter, {prefix}, m(App))) + + o(App.callCount).equals(1) + o(App.args[2].route.path).equals("/test") + o([...App.args[2].route.params]).deepEquals([]) o(G.rafMock.queueLength()).equals(0) }) o("returns right route on init with escaped unicode", () => { G.window.location.href = `${prefix}/%C3%B6?%C3%B6=%C3%B6` - m.route.init(prefix) - o(m.route.path).equals("/ö") - o([...m.route.params]).deepEquals([["ö", "ö"]]) + var App = o.spy() + + m.render(G.root, m(m.WithRouter, {prefix}, m(App))) + + o(App.callCount).equals(1) + o(App.args[2].route.path).equals("/ö") + o([...App.args[2].route.params]).deepEquals([["ö", "ö"]]) o(G.rafMock.queueLength()).equals(0) }) o("returns right route on init with unescaped unicode", () => { G.window.location.href = `${prefix}/ö?ö=ö` - m.route.init(prefix) - o(m.route.path).equals("/ö") - o([...m.route.params]).deepEquals([["ö", "ö"]]) + var App = o.spy() + + m.render(G.root, m(m.WithRouter, {prefix}, m(App))) + + o(App.callCount).equals(1) + o(App.args[2].route.path).equals("/ö") + o([...App.args[2].route.params]).deepEquals([["ö", "ö"]]) o(G.rafMock.queueLength()).equals(0) }) @@ -53,21 +69,24 @@ o.spec("route", () => { G.window.location.href = `${prefix}/a` var spy1 = o.spy() var spy2 = o.spy() + var route - m.mount(G.root, (isInit, redraw) => { - if (isInit) m.route.init(prefix, redraw) - if (m.route.path === "/a") { + var App = (_attrs, _old, context) => { + route = context.route + if (route.path === "/a") { spy1() - } else if (m.route.path === "/b") { + } else if (route.path === "/b") { spy2() } else { - throw new Error(`Unknown path ${m.route.path}`) + throw new Error(`Unknown path ${route.path}`) } - }) + } + + m.mount(G.root, () => m(m.WithRouter, {prefix}, m(App))) o(spy1.callCount).equals(1) o(spy2.callCount).equals(0) - m.route.set("/b") + route.set("/b") o(spy1.callCount).equals(1) o(spy2.callCount).equals(0) @@ -81,7 +100,13 @@ o.spec("route", () => { o("sets route via pushState/onpopstate", async () => { G.window.location.href = `${prefix}/test` - m.route.init(prefix) + + var route + var App = (_attrs, _old, context) => { + route = context.route + } + + m.mount(G.root, () => m(m.WithRouter, {prefix}, m(App))) await Promise.resolve() G.rafMock.fire() @@ -93,7 +118,7 @@ o.spec("route", () => { G.rafMock.fire() // Yep, before even the throttle mechanism takes hold. - o(m.route.get()).equals("/other/x/y/z?c=d#e=f") + o(route.current).equals("/other/x/y/z?c=d#e=f") await Promise.resolve() G.rafMock.fire() @@ -103,9 +128,15 @@ o.spec("route", () => { o("`replace: true` works", async () => { G.window.location.href = `${prefix}/test` - m.route.init(prefix) - m.route.set("/other", {replace: true}) + var route + var App = (_attrs, _old, context) => { + route = context.route + } + + m.mount(G.root, () => m(m.WithRouter, {prefix}, m(App))) + + route.set("/other", {replace: true}) await Promise.resolve() G.rafMock.fire() @@ -122,25 +153,25 @@ o.spec("route", () => { o("`replace: true` works in links", async () => { G.window.location.href = `${prefix}/test` - m.route.init(prefix) var e = G.window.document.createEvent("MouseEvents") e.initEvent("click", true, true) e.button = 0 - m.mount(G.root, (isInit, redraw) => { - if (isInit) m.route.init(prefix, redraw) - if (m.route.path === "/test") { - return m("a", m.route.link({href: "/other", replace: true})) - } else if (m.route.path === "/other") { + var App = (_attrs, _old, {route}) => { + if (route.path === "/test") { + return m("a", m(m.Link, {href: "/other", replace: true})) + } else if (route.path === "/other") { return m("div") - } else if (m.route.path === "/") { + } else if (route.path === "/") { return m("span") } else { - throw new Error(`Unknown route: ${m.route.path}`) + throw new Error(`Unknown route: ${route.path}`) } - }) + } + + m.mount(G.root, () => m(m.WithRouter, {prefix}, m(App))) G.root.firstChild.dispatchEvent(e) @@ -159,9 +190,15 @@ o.spec("route", () => { o("`replace: false` works", async () => { G.window.location.href = `${prefix}/test` - m.route.init(prefix) - m.route.set("/other", {replace: false}) + var route + var App = (_attrs, _old, context) => { + route = context.route + } + + m.mount(G.root, () => m(m.WithRouter, {prefix}, m(App))) + + route.set("/other", {replace: false}) await Promise.resolve() G.rafMock.fire() @@ -184,16 +221,17 @@ o.spec("route", () => { e.initEvent("click", true, true) e.button = 0 - m.mount(G.root, (isInit, redraw) => { - if (isInit) m.route.init(prefix, redraw) - if (m.route.path === "/test") { - return m("a", m.route.link({href: "/other", replace: false})) - } else if (m.route.path === "/other") { + var App = (_attrs, _old, {route}) => { + if (route.path === "/test") { + return m("a", m(m.Link, {href: "/other", replace: false})) + } else if (route.path === "/other") { return m("div") } else { - throw new Error(`Unknown route: ${m.route.path}`) + throw new Error(`Unknown route: ${route.path}`) } - }) + } + + m.mount(G.root, () => m(m.WithRouter, {prefix}, m(App))) G.root.firstChild.dispatchEvent(e) @@ -212,9 +250,15 @@ o.spec("route", () => { o("state works", async () => { G.window.location.href = `${prefix}/test` - m.route.init(prefix) - m.route.set("/other", {state: {a: 1}}) + var route + var App = (_attrs, _old, context) => { + route = context.route + } + + m.mount(G.root, () => m(m.WithRouter, {prefix}, m(App))) + + route.set("/other", {state: {a: 1}}) await Promise.resolve() G.rafMock.fire() @@ -226,18 +270,30 @@ o.spec("route", () => { o("adds trailing slash where needed", () => { G.window.location.href = `${prefix}/test` - m.route.init(`${prefix}/`) - o(m.route.path).equals("/test") - o([...m.route.params]).deepEquals([]) + var route + var App = (_attrs, _old, context) => { + route = context.route + } + + m.mount(G.root, () => m(m.WithRouter, {prefix: `${prefix}/`}, m(App))) + + o(route.path).equals("/test") + o([...route.params]).deepEquals([]) o(G.rafMock.queueLength()).equals(0) }) o("handles route with search", () => { G.window.location.href = `${prefix}/test?a=b&c=d` - m.route.init(prefix) - o(m.route.path).equals("/test") - o([...m.route.params]).deepEquals([["a", "b"], ["c", "d"]]) + var route + var App = (_attrs, _old, context) => { + route = context.route + } + + m.mount(G.root, () => m(m.WithRouter, {prefix}, m(App))) + + o(route.path).equals("/test") + o([...route.params]).deepEquals([["a", "b"], ["c", "d"]]) o(G.rafMock.queueLength()).equals(0) }) @@ -245,7 +301,9 @@ o.spec("route", () => { G.window.location.href = "http://old.com" G.window.location.href = "http://new.com" - m.route.init(prefix) + var App = () => {} + + m.mount(G.root, () => m(m.WithRouter, {prefix}, m(App))) G.window.history.back() @@ -261,16 +319,18 @@ o.spec("route", () => { e.button = 0 G.window.location.href = `${prefix}/` - m.mount(G.root, (isInit, redraw) => { - if (isInit) m.route.init(prefix, redraw) - if (m.route.path === "/") { - return m("a", m.route.link({href: "/test"})) - } else if (m.route.path === "/test") { + + var App = (_attrs, _old, {route}) => { + if (route.path === "/") { + return m("a", m(m.Link, {href: "/test"})) + } else if (route.path === "/test") { return m("div") } else { - throw new Error(`Unknown route: ${m.route.path}`) + throw new Error(`Unknown route: ${route.path}`) } - }) + } + + m.mount(G.root, () => m(m.WithRouter, {prefix}, m(App))) o(G.window.location.href).equals(fullPrefix) @@ -289,16 +349,18 @@ o.spec("route", () => { e.initEvent("click", true, true) e.button = 0 G.window.location.href = `${prefix}/` - m.mount(G.root, (isInit, redraw) => { - if (isInit) m.route.init(prefix, redraw) - if (m.route.path === "/") { - return m("a", m.route.link({href: "/test", state: {a: 1}})) - } else if (m.route.path === "/test") { + + var App = (_attrs, _old, {route}) => { + if (route.path === "/") { + return m("a", m(m.Link, {href: "/test", state: {a: 1}})) + } else if (route.path === "/test") { return m("div") } else { - throw new Error(`Unknown route: ${m.route.path}`) + throw new Error(`Unknown route: ${route.path}`) } - }) + } + + m.mount(G.root, () => m(m.WithRouter, {prefix}, m(App))) G.root.firstChild.dispatchEvent(e) @@ -309,20 +371,6 @@ o.spec("route", () => { o(G.rafMock.queueLength()).equals(0) }) - o("route.Link can render without routes or dom access", () => { - restoreDOMGlobals() - m.route.init(prefix, null, "https://localhost/") - - var enabled = m.route.link({href: "/test"}) - o(Object.keys(enabled)).deepEquals(["href", "onclick"]) - o(enabled.href).equals(`${prefix}/test`) - o(typeof enabled.onclick).equals("function") - - var disabled = m.route.link({disabled: true, href: "/test"}) - o(disabled).deepEquals({disabled: true, "aria-disabled": "true"}) - o(G.rafMock.queueLength()).equals(0) - }) - o("route.Link doesn't redraw on wrong button", async () => { var e = G.window.document.createEvent("MouseEvents") @@ -330,22 +378,23 @@ o.spec("route", () => { e.button = 10 G.window.location.href = `${prefix}/` - m.mount(G.root, (isInit, redraw) => { - if (isInit) m.route.init(prefix, redraw) - if (m.route.path === "/") { - return m("a", m.route.link({href: "/test"})) - } else if (m.route.path === "/test") { + + var App = (_attrs, _old, {route}) => { + if (route.path === "/") { + return m("a", m(m.Link, {href: "/test"})) + } else if (route.path === "/test") { return m("div") } else { - throw new Error(`Unknown route: ${m.route.path}`) + throw new Error(`Unknown route: ${route.path}`) } - }) + } + + m.mount(G.root, () => m(m.WithRouter, {prefix}, m(App))) o(G.window.location.href).equals(fullPrefix) G.root.firstChild.dispatchEvent(e) - await Promise.resolve() G.rafMock.fire() @@ -360,16 +409,18 @@ o.spec("route", () => { e.button = 0 G.window.location.href = `${prefix}/` - m.mount(G.root, (isInit, redraw) => { - if (isInit) m.route.init(prefix, redraw) - if (m.route.path === "/") { - return m("a", m.route.link({href: "/test", onclick(e) { e.preventDefault() }})) - } else if (m.route.path === "/test") { + + var App = (_attrs, _old, {route}) => { + if (route.path === "/") { + return m("a", {onclick(e) { e.preventDefault() }}, m(m.Link, {href: "/test"})) + } else if (route.path === "/test") { return m("div") } else { - throw new Error(`Unknown route: ${m.route.path}`) + throw new Error(`Unknown route: ${route.path}`) } - }) + } + + m.mount(G.root, () => m(m.WithRouter, {prefix}, m(App))) o(G.window.location.href).equals(fullPrefix) @@ -382,83 +433,32 @@ o.spec("route", () => { o(G.rafMock.queueLength()).equals(0) }) - o("route.Link ignores `return false`", async () => { - var e = G.window.document.createEvent("MouseEvents") - - e.initEvent("click", true, true) - e.button = 0 - + o("`route.set(m.route.current)` re-runs the resolution logic (#1180)", async () => { G.window.location.href = `${prefix}/` - m.mount(G.root, (isInit, redraw) => { - if (isInit) m.route.init(prefix, redraw) - if (m.route.path === "/") { - return m("a", m.route.link({href: "/test", onclick: () => false})) - } else if (m.route.path === "/test") { - return m("div") - } else { - throw new Error(`Unknown route: ${m.route.path}`) - } - }) - - o(G.window.location.href).equals(fullPrefix) - - G.root.firstChild.dispatchEvent(e) - - await Promise.resolve() - G.rafMock.fire() - - o(G.window.location.href).equals(`${fullPrefix}test`) - o(G.rafMock.queueLength()).equals(0) - }) - o("m.route.set(m.route.get()) re-runs the resolution logic (#1180)", async () => { - var render = o.spy((isInit, redraw) => { - if (isInit) m.route.init(prefix, redraw) + var route + var App = o.spy((_attrs, _old, context) => { + route = context.route return m("div") }) - G.window.location.href = `${prefix}/` - m.mount(G.root, render) + m.mount(G.root, () => m(m.WithRouter, {prefix}, m(App))) - o(render.callCount).equals(1) + o(App.callCount).equals(1) await Promise.resolve() G.rafMock.fire() - o(render.callCount).equals(1) + o(App.callCount).equals(1) - m.route.set(m.route.get()) + route.set(route.current) await Promise.resolve() G.rafMock.fire() await Promise.resolve() G.rafMock.fire() - o(render.callCount).equals(2) - o(G.rafMock.queueLength()).equals(0) - }) - - o("throttles", () => { - var i = 0 - - G.window.location.href = `${prefix}/` - var redraw = m.mount(G.root, (redraw, isInit) => { - if (isInit) m.route.init(prefix, redraw) - i++ - }) - var before = i - - redraw() - redraw() - redraw() - redraw() - var after = i - - G.rafMock.fire() - - o(before).equals(1) // routes synchronously - o(after).equals(1) // redraws asynchronously - o(i).equals(2) + o(App.callCount).equals(2) o(G.rafMock.queueLength()).equals(0) }) }) diff --git a/tests/exported-api.js b/tests/exported-api.js index ce331e285..62e7dee44 100644 --- a/tests/exported-api.js +++ b/tests/exported-api.js @@ -72,18 +72,21 @@ o.spec("api", function() { }) }) - o.spec("m.route", function() { + o.spec("m.WithRouter, m.Link", function() { o("works", async() => { - m.mount(G.root, (isInit, redraw) => { - if (isInit) m.route.init("#", redraw) - if (m.route.path === "/a") { + var route + var App = (_attrs, _old, context) => { + route = context.route + if (route.path === "/a") { return m("div") - } else if (m.route.path === "/b") { - return m("span") + } else if (route.path === "/b") { + return m("a", m(m.Link, {href: "/a"})) } else { - m.route.set("/a") + route.set("/a") } - }) + } + + m.mount(G.root, () => m(m.WithRouter, {prefix: "#"}, m(App))) await Promise.resolve() G.rafMock.fire() @@ -91,15 +94,15 @@ o.spec("api", function() { o(G.root.childNodes.length).equals(1) o(G.root.firstChild.nodeName).equals("DIV") - o(m.route.get()).equals("/a") + o(route.current).equals("/a") - m.route.set("/b") + route.set("/b") await Promise.resolve() G.rafMock.fire() o(G.rafMock.queueLength()).equals(0) - o(m.route.get()).equals("/b") + o(route.current).equals("/b") }) }) }) diff --git a/tests/render/context.js b/tests/render/context.js new file mode 100644 index 000000000..d0a12f6ac --- /dev/null +++ b/tests/render/context.js @@ -0,0 +1,119 @@ +import o from "ospec" + +import {setupGlobals} from "../../test-utils/global.js" + +import m from "../../src/entry/mithril.esm.js" + +o.spec("context", () => { + var G = setupGlobals() + + function symbolsToStrings(object) { + var result = {} + for (const key of Reflect.ownKeys(object)) { + // Intentionally using `String(key)` to stringify symbols from `Symbol("foo")` to + // `"Symbol(foo)"` for deep equality. + Object.defineProperty(result, String(key), Object.getOwnPropertyDescriptor(object, key)) + } + return result + } + + function allKeys(context) { + if (context === null || typeof context !== "object") return undefined + const chain = [] + while (context !== null && context !== Object.prototype) { + chain.push(context) + context = Object.getPrototypeOf(context) + } + return symbolsToStrings(chain.reduceRight((a, b) => Object.assign(a, b), {})) + } + + o("string keys are set in context", () => { + var redraw = () => {} + var Comp = o.spy() + var vnode = m.set({key: "value", one: "two"}, m(Comp)) + + m.render(G.root, vnode, redraw) + + o(Comp.callCount).equals(1) + o(allKeys(Comp.args[2])).deepEquals({ + redraw, + key: "value", + one: "two", + }) + + var vnode = m.set({key: "updated", two: "three"}, m(Comp)) + + m.render(G.root, vnode, redraw) + + o(Comp.callCount).equals(2) + o(allKeys(Comp.args[2])).deepEquals({ + redraw, + key: "updated", + two: "three", + }) + + m.render(G.root, null) + }) + + o("symbol keys are set in context", () => { + var key = Symbol("key") + var one = Symbol("one") + var two = Symbol("two") + + var redraw = () => {} + var Comp = o.spy() + var vnode = m.set({[key]: "value", [one]: "two"}, m(Comp)) + + m.render(G.root, vnode, redraw) + + o(Comp.callCount).equals(1) + o(allKeys(Comp.args[2])).deepEquals(symbolsToStrings({ + redraw, + [key]: "value", + [one]: "two", + })) + + var vnode = m.set({[key]: "updated", [two]: "three"}, m(Comp)) + + m.render(G.root, vnode, redraw) + + o(Comp.callCount).equals(2) + o(allKeys(Comp.args[2])).deepEquals(symbolsToStrings({ + redraw, + [key]: "updated", + [two]: "three", + })) + + m.render(G.root, null) + }) + + o("mixed keys are set in context", () => { + var key = Symbol("key") + + var redraw = () => {} + var Comp = o.spy() + var vnode = m.set({[key]: "value", one: "two"}, m(Comp)) + + m.render(G.root, vnode, redraw) + + o(Comp.callCount).equals(1) + o(allKeys(Comp.args[2])).deepEquals(symbolsToStrings({ + redraw, + [key]: "value", + one: "two", + })) + + var vnode = m.set({[key]: "updated", two: "three"}, m(Comp)) + + m.render(G.root, vnode, redraw) + + o(Comp.callCount).equals(2) + o(allKeys(Comp.args[2])).deepEquals(symbolsToStrings({ + redraw, + [key]: "updated", + two: "three", + })) + + m.render(G.root, null) + }) +}) From 21085ceb635fdaa9c5c9dee696d1256d748b495f Mon Sep 17 00:00:00 2001 From: Claudia Meadows Date: Thu, 10 Oct 2024 22:46:22 -0700 Subject: [PATCH 51/95] Rearrange tests to mirror source --- tests/{render => core}/attributes.js | 0 tests/{render => core}/component.js | 0 tests/{render => core}/context.js | 0 tests/{render => core}/createElement.js | 0 tests/{render => core}/createFragment.js | 0 tests/{render => core}/createNodes.js | 0 tests/{render => core}/createText.js | 0 tests/{render => core}/event.js | 0 tests/{render => core}/fragment.js | 0 tests/{render => core}/hyperscript.js | 0 tests/{render => core}/input.js | 0 tests/{api => core}/mountRedraw.js | 0 tests/{render => core}/normalize.js | 0 tests/{render => core}/normalizeChildren.js | 0 .../normalizeComponentChildren.js | 0 tests/{render => core}/oncreate.js | 0 tests/{render => core}/onremove.js | 0 tests/{render => core}/onupdate.js | 0 .../render-hyperscript-integration.js | 0 tests/{render => core}/render.js | 0 tests/{render => core}/retain.js | 0 tests/{render => core}/textContent.js | 0 tests/{render => core}/updateElement.js | 0 tests/{render => core}/updateFragment.js | 0 tests/{render => core}/updateNodes.js | 0 tests/{render => core}/updateNodesFuzzer.js | 0 tests/{render => core}/updateText.js | 0 tests/{util => std}/init.js | 0 tests/{util => std}/lazy.js | 0 tests/{util => std}/p.js | 0 tests/{api => std}/router.js | 0 tests/{ => std}/stream/scan.js | 2 +- tests/{ => std}/stream/scanMerge.js | 2 +- tests/{ => std}/stream/stream.js | 22 +------------------ tests/{util => std}/tracked.js | 0 tests/{util => std}/use.js | 0 tests/{util => std}/withProgress.js | 0 37 files changed, 3 insertions(+), 23 deletions(-) rename tests/{render => core}/attributes.js (100%) rename tests/{render => core}/component.js (100%) rename tests/{render => core}/context.js (100%) rename tests/{render => core}/createElement.js (100%) rename tests/{render => core}/createFragment.js (100%) rename tests/{render => core}/createNodes.js (100%) rename tests/{render => core}/createText.js (100%) rename tests/{render => core}/event.js (100%) rename tests/{render => core}/fragment.js (100%) rename tests/{render => core}/hyperscript.js (100%) rename tests/{render => core}/input.js (100%) rename tests/{api => core}/mountRedraw.js (100%) rename tests/{render => core}/normalize.js (100%) rename tests/{render => core}/normalizeChildren.js (100%) rename tests/{render => core}/normalizeComponentChildren.js (100%) rename tests/{render => core}/oncreate.js (100%) rename tests/{render => core}/onremove.js (100%) rename tests/{render => core}/onupdate.js (100%) rename tests/{render => core}/render-hyperscript-integration.js (100%) rename tests/{render => core}/render.js (100%) rename tests/{render => core}/retain.js (100%) rename tests/{render => core}/textContent.js (100%) rename tests/{render => core}/updateElement.js (100%) rename tests/{render => core}/updateFragment.js (100%) rename tests/{render => core}/updateNodes.js (100%) rename tests/{render => core}/updateNodesFuzzer.js (100%) rename tests/{render => core}/updateText.js (100%) rename tests/{util => std}/init.js (100%) rename tests/{util => std}/lazy.js (100%) rename tests/{util => std}/p.js (100%) rename tests/{api => std}/router.js (100%) rename tests/{ => std}/stream/scan.js (96%) rename tests/{ => std}/stream/scanMerge.js (92%) rename tests/{ => std}/stream/stream.js (95%) rename tests/{util => std}/tracked.js (100%) rename tests/{util => std}/use.js (100%) rename tests/{util => std}/withProgress.js (100%) diff --git a/tests/render/attributes.js b/tests/core/attributes.js similarity index 100% rename from tests/render/attributes.js rename to tests/core/attributes.js diff --git a/tests/render/component.js b/tests/core/component.js similarity index 100% rename from tests/render/component.js rename to tests/core/component.js diff --git a/tests/render/context.js b/tests/core/context.js similarity index 100% rename from tests/render/context.js rename to tests/core/context.js diff --git a/tests/render/createElement.js b/tests/core/createElement.js similarity index 100% rename from tests/render/createElement.js rename to tests/core/createElement.js diff --git a/tests/render/createFragment.js b/tests/core/createFragment.js similarity index 100% rename from tests/render/createFragment.js rename to tests/core/createFragment.js diff --git a/tests/render/createNodes.js b/tests/core/createNodes.js similarity index 100% rename from tests/render/createNodes.js rename to tests/core/createNodes.js diff --git a/tests/render/createText.js b/tests/core/createText.js similarity index 100% rename from tests/render/createText.js rename to tests/core/createText.js diff --git a/tests/render/event.js b/tests/core/event.js similarity index 100% rename from tests/render/event.js rename to tests/core/event.js diff --git a/tests/render/fragment.js b/tests/core/fragment.js similarity index 100% rename from tests/render/fragment.js rename to tests/core/fragment.js diff --git a/tests/render/hyperscript.js b/tests/core/hyperscript.js similarity index 100% rename from tests/render/hyperscript.js rename to tests/core/hyperscript.js diff --git a/tests/render/input.js b/tests/core/input.js similarity index 100% rename from tests/render/input.js rename to tests/core/input.js diff --git a/tests/api/mountRedraw.js b/tests/core/mountRedraw.js similarity index 100% rename from tests/api/mountRedraw.js rename to tests/core/mountRedraw.js diff --git a/tests/render/normalize.js b/tests/core/normalize.js similarity index 100% rename from tests/render/normalize.js rename to tests/core/normalize.js diff --git a/tests/render/normalizeChildren.js b/tests/core/normalizeChildren.js similarity index 100% rename from tests/render/normalizeChildren.js rename to tests/core/normalizeChildren.js diff --git a/tests/render/normalizeComponentChildren.js b/tests/core/normalizeComponentChildren.js similarity index 100% rename from tests/render/normalizeComponentChildren.js rename to tests/core/normalizeComponentChildren.js diff --git a/tests/render/oncreate.js b/tests/core/oncreate.js similarity index 100% rename from tests/render/oncreate.js rename to tests/core/oncreate.js diff --git a/tests/render/onremove.js b/tests/core/onremove.js similarity index 100% rename from tests/render/onremove.js rename to tests/core/onremove.js diff --git a/tests/render/onupdate.js b/tests/core/onupdate.js similarity index 100% rename from tests/render/onupdate.js rename to tests/core/onupdate.js diff --git a/tests/render/render-hyperscript-integration.js b/tests/core/render-hyperscript-integration.js similarity index 100% rename from tests/render/render-hyperscript-integration.js rename to tests/core/render-hyperscript-integration.js diff --git a/tests/render/render.js b/tests/core/render.js similarity index 100% rename from tests/render/render.js rename to tests/core/render.js diff --git a/tests/render/retain.js b/tests/core/retain.js similarity index 100% rename from tests/render/retain.js rename to tests/core/retain.js diff --git a/tests/render/textContent.js b/tests/core/textContent.js similarity index 100% rename from tests/render/textContent.js rename to tests/core/textContent.js diff --git a/tests/render/updateElement.js b/tests/core/updateElement.js similarity index 100% rename from tests/render/updateElement.js rename to tests/core/updateElement.js diff --git a/tests/render/updateFragment.js b/tests/core/updateFragment.js similarity index 100% rename from tests/render/updateFragment.js rename to tests/core/updateFragment.js diff --git a/tests/render/updateNodes.js b/tests/core/updateNodes.js similarity index 100% rename from tests/render/updateNodes.js rename to tests/core/updateNodes.js diff --git a/tests/render/updateNodesFuzzer.js b/tests/core/updateNodesFuzzer.js similarity index 100% rename from tests/render/updateNodesFuzzer.js rename to tests/core/updateNodesFuzzer.js diff --git a/tests/render/updateText.js b/tests/core/updateText.js similarity index 100% rename from tests/render/updateText.js rename to tests/core/updateText.js diff --git a/tests/util/init.js b/tests/std/init.js similarity index 100% rename from tests/util/init.js rename to tests/std/init.js diff --git a/tests/util/lazy.js b/tests/std/lazy.js similarity index 100% rename from tests/util/lazy.js rename to tests/std/lazy.js diff --git a/tests/util/p.js b/tests/std/p.js similarity index 100% rename from tests/util/p.js rename to tests/std/p.js diff --git a/tests/api/router.js b/tests/std/router.js similarity index 100% rename from tests/api/router.js rename to tests/std/router.js diff --git a/tests/stream/scan.js b/tests/std/stream/scan.js similarity index 96% rename from tests/stream/scan.js rename to tests/std/stream/scan.js index d5b222f21..b48678276 100644 --- a/tests/stream/scan.js +++ b/tests/std/stream/scan.js @@ -1,6 +1,6 @@ import o from "ospec" -import stream from "../../src/entry/stream.esm.js" +import stream from "../../../src/entry/stream.esm.js" o.spec("scan", function() { o("defaults to seed", function() { diff --git a/tests/stream/scanMerge.js b/tests/std/stream/scanMerge.js similarity index 92% rename from tests/stream/scanMerge.js rename to tests/std/stream/scanMerge.js index 3d0b4c7f7..652e492e5 100644 --- a/tests/stream/scanMerge.js +++ b/tests/std/stream/scanMerge.js @@ -1,6 +1,6 @@ import o from "ospec" -import stream from "../../src/entry/stream.esm.js" +import stream from "../../../src/entry/stream.esm.js" o.spec("scanMerge", function() { o("defaults to seed", function() { diff --git a/tests/stream/stream.js b/tests/std/stream/stream.js similarity index 95% rename from tests/stream/stream.js rename to tests/std/stream/stream.js index 5f6900c7d..e31bf4a3e 100644 --- a/tests/stream/stream.js +++ b/tests/std/stream/stream.js @@ -1,6 +1,6 @@ import o from "ospec" -import Stream from "../../src/entry/stream.esm.js" +import Stream from "../../../src/entry/stream.esm.js" o.spec("stream", function() { o.spec("stream", function() { @@ -41,26 +41,6 @@ o.spec("stream", function() { o(b()).equals(2) }) - // NOTE: this *must* be the *only* uses of `Stream.HALT` in the entire - // test suite. - o("HALT is a deprecated alias of SKIP and warns once", function() { - var log = console.log - var warnings = [] - console.log = function(a) { - warnings.push(a) - } - - try { - o(Stream.HALT).equals(Stream.SKIP) - o(warnings).deepEquals(["HALT is deprecated and has been renamed to SKIP"]) - o(Stream.HALT).equals(Stream.SKIP) - o(warnings).deepEquals(["HALT is deprecated and has been renamed to SKIP"]) - o(Stream.HALT).equals(Stream.SKIP) - o(warnings).deepEquals(["HALT is deprecated and has been renamed to SKIP"]) - } finally { - console.log = log - } - }) }) o.spec("combine", function() { o("transforms value", function() { diff --git a/tests/util/tracked.js b/tests/std/tracked.js similarity index 100% rename from tests/util/tracked.js rename to tests/std/tracked.js diff --git a/tests/util/use.js b/tests/std/use.js similarity index 100% rename from tests/util/use.js rename to tests/std/use.js diff --git a/tests/util/withProgress.js b/tests/std/withProgress.js similarity index 100% rename from tests/util/withProgress.js rename to tests/std/withProgress.js From beaad3b1c491f3176446b48d21f2e83b2f37fa36 Mon Sep 17 00:00:00 2001 From: Claudia Meadows Date: Thu, 10 Oct 2024 23:35:37 -0700 Subject: [PATCH 52/95] Optimize stream, remove deprecated bit, simplify `combine` - Did some baseline optimization to it to reduce overhead and memory requirements. Won't be too much of a deal with smaller apps, but users using a lot of streams will appreciate the reduced overhead. - Removed the runtime-deprecated `Stream.HALT`. - Dropped the source list from `Stream.combine` callbacks' operands. It's almost always available in context anyways. --- src/entry/stream.esm.js | 239 +++++++++++++++++-------------------- tests/std/stream/stream.js | 46 +++---- 2 files changed, 131 insertions(+), 154 deletions(-) diff --git a/src/entry/stream.esm.js b/src/entry/stream.esm.js index ec581561a..f8c89b267 100644 --- a/src/entry/stream.esm.js +++ b/src/entry/stream.esm.js @@ -1,125 +1,115 @@ -Stream.SKIP = {} -Stream.lift = lift -Stream.scan = scan -Stream.merge = merge -Stream.combine = combine -Stream.scanMerge = scanMerge -Stream["fantasy-land/of"] = Stream - -var warnedHalt = false -Object.defineProperty(Stream, "HALT", { - get: function() { - warnedHalt || console.log("HALT is deprecated and has been renamed to SKIP"); - warnedHalt = true - return Stream.SKIP - } -}) - -function Stream(value) { - var dependentStreams = [] - var dependentFns = [] - - function stream(v) { - if (arguments.length && v !== Stream.SKIP) { - value = v - if (open(stream)) { - stream._changing() - stream._state = "active" - // Cloning the list to ensure it's still iterated in intended - // order - dependentStreams.slice().forEach(function(s, i) { - if (open(s)) s(this[i](value)) - }, dependentFns.slice()) +var STATE_PENDING = 1 +var STATE_ACTIVE = 2 +var STATE_CHANGING = 3 +var STATE_ENDED = 4 + +var streamSet = (stream, value) => { + if (value !== SKIP) { + stream._v = value + if (stream._s !== STATE_ENDED) { + streamChanging(stream) + stream._s = STATE_ACTIVE + // Cloning the list to ensure it's still iterated in intended + // order + var streams = stream._d.slice() + var fns = stream._f.slice() + for (var i = 0; i < streams.length; i++) { + if (streams[i]._s !== STATE_ENDED) { + streamSet(streams[i], fns[i](stream._v)) + } } } - - return value } - stream.constructor = Stream - stream._state = arguments.length && value !== Stream.SKIP ? "active" : "pending" - stream._parents = [] + return stream._v +} - stream._changing = function() { - if (open(stream)) stream._state = "changing" - dependentStreams.forEach(function(s) { - s._changing() - }) - } +var streamChanging = (stream) => { + if (stream._s !== STATE_ENDED) stream._s = STATE_CHANGING + for (var s of stream._d) streamChanging(s) +} - stream._map = function(fn, ignoreInitial) { - var target = ignoreInitial ? Stream() : Stream(fn(value)) - target._parents.push(stream) - dependentStreams.push(target) - dependentFns.push(fn) - return target - } +var streamMap = (stream, fn, ignoreInitial) => { + var target = ignoreInitial ? Stream() : Stream(fn(stream._v)) + target._p.push(stream) + stream._d.push(target) + stream._f.push(fn) + return target +} - stream.map = function(fn) { - return stream._map(fn, stream._state !== "active") - } +var Stream = (...args) => { + var stream = (...args) => streamSet(stream, args.length ? args[0] : SKIP) - var end - function createEnd() { - end = Stream() - end.map(function(value) { - if (value === true) { - stream._parents.forEach(function (p) {p._unregisterChild(stream)}) - stream._state = "ended" - stream._parents.length = dependentStreams.length = dependentFns.length = 0 - } - return value - }) - return end - } + Object.setPrototypeOf(stream, Stream.prototype) - stream.toJSON = function() { return value != null && typeof value.toJSON === "function" ? value.toJSON() : value } + stream._s = args.length && args[0] !== SKIP ? STATE_ACTIVE : STATE_PENDING + stream._v = args.length ? args[0] : undefined + stream._d = [] + stream._f = [] + stream._p = [] + stream._e = null - stream["fantasy-land/map"] = stream.map - stream["fantasy-land/ap"] = function(x) { return combine(function(s1, s2) { return s1()(s2()) }, [x, stream]) } + return stream +} + +Stream["fantasy-land/of"] = Stream - stream._unregisterChild = function(child) { - var childIndex = dependentStreams.indexOf(child) - if (childIndex !== -1) { - dependentStreams.splice(childIndex, 1) - dependentFns.splice(childIndex, 1) +Stream.prototype = Object.create(Function.prototype, Object.getOwnPropertyDescriptors({ + constructor: Stream, + map(fn) { return streamMap(this, fn, this._s !== STATE_ACTIVE) }, + "fantasy-land/ap"(x) { return combine(() => (0, x._v)(this._v), [x, this]) }, + toJSON() { + var value = this._v + return (value != null && typeof value.toJSON === "function" ? value.toJSON() : value) + }, + get end() { + if (!this._e) { + this._e = Stream() + streamMap(this._e, (value) => { + if (value === true) { + for (var p of this._p) { + var childIndex = p._d.indexOf(this) + if (childIndex >= 0) { + p._d.splice(childIndex, 1) + p._f.splice(childIndex, 1) + } + } + this._s = STATE_ENDED + this._p.length = this._d.length = this._f.length = 0 + } + return value + }, true) } - } + return this._e + }, +})) - Object.defineProperty(stream, "end", { - get: function() { return end || createEnd() } - }) +Stream.prototype["fantasy-land/map"] = Stream.prototype.map - return stream -} +var SKIP = Stream.SKIP = {} -function combine(fn, streams) { - var ready = streams.every(function(s) { - if (s.constructor !== Stream) - throw new Error("Ensure that each item passed to stream.combine/stream.merge/lift is a stream.") - return s._state === "active" - }) - var stream = ready - ? Stream(fn.apply(null, streams.concat([streams]))) - : Stream() +var combine = Stream.combine = (fn, streams) => { + if (streams.some((s) => s.constructor !== Stream)) { + throw new Error("Ensure that each item passed to stream.combine/stream.merge/lift is a stream.") + } + var ready = streams.every((s) => s._s === STATE_ACTIVE) + var stream = ready ? Stream(fn(streams)) : Stream() var changed = [] - var mappers = streams.map(function(s) { - return s._map(function(value) { - changed.push(s) - if (ready || streams.every(function(s) { return s._state !== "pending" })) { - ready = true - stream(fn.apply(null, streams.concat([changed]))) - changed = [] - } - return value - }, true) - }) + var mappers = streams.map((s) => streamMap(s, (value) => { + changed.push(s) + if (ready || streams.every((s) => s._s !== STATE_PENDING)) { + ready = true + streamSet(stream, fn(changed)) + changed = [] + } + return value + }, true)) - var endStream = stream.end.map(function(value) { + var endStream = stream.end.map((value) => { if (value === true) { - mappers.forEach(function(mapper) { mapper.end(true) }) + for (var mapper of mappers) mapper.end(true) endStream.end(true) } return undefined @@ -128,48 +118,35 @@ function combine(fn, streams) { return stream } -function merge(streams) { - return combine(function() { return streams.map(function(s) { return s() }) }, streams) -} +Stream.merge = (streams) => combine(() => streams.map((s) => s._v), streams) -function scan(fn, acc, origin) { - var stream = origin.map(function(v) { +Stream.scan = (fn, acc, origin) => { + var stream = streamMap(origin, (v) => { var next = fn(acc, v) - if (next !== Stream.SKIP) acc = next + if (next !== SKIP) acc = next return next - }) - stream(acc) + }, origin._s !== STATE_ACTIVE) + streamSet(stream, acc) return stream } -function scanMerge(tuples, seed) { - var streams = tuples.map(function(tuple) { return tuple[0] }) - - var stream = combine(function() { - var changed = arguments[arguments.length - 1] - streams.forEach(function(stream, i) { - if (changed.indexOf(stream) > -1) - seed = tuples[i][1](seed, stream()) - }) +Stream.scanMerge = (tuples, seed) => { + var streams = tuples.map((tuple) => tuple[0]) + var stream = combine((changed) => { + for (var i = 0; i < streams.length; i++) { + if (changed.includes(streams[i])) { + seed = tuples[i][1](seed, streams[i]._v) + } + } return seed }, streams) - stream(seed) + streamSet(stream, seed) return stream } -function lift() { - var fn = arguments[0] - var streams = Array.prototype.slice.call(arguments, 1) - return merge(streams).map(function(streams) { - return fn.apply(undefined, streams) - }) -} - -function open(s) { - return s._state === "pending" || s._state === "active" || s._state === "changing" -} +Stream.lift = (fn, ...streams) => combine(() => fn(...streams.map((s) => s._v)), streams) export {Stream as default} diff --git a/tests/std/stream/stream.js b/tests/std/stream/stream.js index e31bf4a3e..360e59e2e 100644 --- a/tests/std/stream/stream.js +++ b/tests/std/stream/stream.js @@ -45,7 +45,7 @@ o.spec("stream", function() { o.spec("combine", function() { o("transforms value", function() { var stream = Stream() - var doubled = Stream.combine(function(s) {return s() * 2}, [stream]) + var doubled = Stream.combine(function() {return stream() * 2}, [stream]) stream(2) @@ -53,14 +53,14 @@ o.spec("stream", function() { }) o("transforms default value", function() { var stream = Stream(2) - var doubled = Stream.combine(function(s) {return s() * 2}, [stream]) + var doubled = Stream.combine(function() {return stream() * 2}, [stream]) o(doubled()).equals(4) }) o("transforms multiple values", function() { var s1 = Stream() var s2 = Stream() - var added = Stream.combine(function(s1, s2) {return s1() + s2()}, [s1, s2]) + var added = Stream.combine(function() {return s1() + s2()}, [s1, s2]) s1(2) s2(3) @@ -70,14 +70,14 @@ o.spec("stream", function() { o("transforms multiple default values", function() { var s1 = Stream(2) var s2 = Stream(3) - var added = Stream.combine(function(s1, s2) {return s1() + s2()}, [s1, s2]) + var added = Stream.combine(function() {return s1() + s2()}, [s1, s2]) o(added()).equals(5) }) o("transforms mixed default and late-bound values", function() { var s1 = Stream(2) var s2 = Stream() - var added = Stream.combine(function(s1, s2) {return s1() + s2()}, [s1, s2]) + var added = Stream.combine(function() {return s1() + s2()}, [s1, s2]) s2(3) @@ -86,9 +86,9 @@ o.spec("stream", function() { o("combines atomically", function() { var count = 0 var a = Stream() - var b = Stream.combine(function(a) {return a() * 2}, [a]) - var c = Stream.combine(function(a) {return a() * a()}, [a]) - var d = Stream.combine(function(b, c) { + var b = Stream.combine(function() {return a() * 2}, [a]) + var c = Stream.combine(function() {return a() * a()}, [a]) + var d = Stream.combine(function() { count++ return b() + c() }, [b, c]) @@ -102,9 +102,9 @@ o.spec("stream", function() { o("combines default value atomically", function() { var count = 0 var a = Stream(3) - var b = Stream.combine(function(a) {return a() * 2}, [a]) - var c = Stream.combine(function(a) {return a() * a()}, [a]) - var d = Stream.combine(function(b, c) { + var b = Stream.combine(function() {return a() * 2}, [a]) + var c = Stream.combine(function() {return a() * a()}, [a]) + var d = Stream.combine(function() { count++ return b() + c() }, [b, c]) @@ -115,11 +115,11 @@ o.spec("stream", function() { o("combines and maps nested streams atomically", function() { var count = 0 var a = Stream(3) - var b = Stream.combine(function(a) {return a() * 2}, [a]) - var c = Stream.combine(function(a) {return a() * a()}, [a]) + var b = Stream.combine(function() {return a() * 2}, [a]) + var c = Stream.combine(function() {return a() * a()}, [a]) var d = c.map(function(x){return x}) - var e = Stream.combine(function(x) {return x()}, [d]) - var f = Stream.combine(function(b, e) { + var e = Stream.combine(function() {return d()}, [d]) + var f = Stream.combine(function() { count++ return b() + e() }, [b, e]) @@ -131,7 +131,7 @@ o.spec("stream", function() { var streams = [] var a = Stream() var b = Stream() - Stream.combine(function(a, b, changed) { + Stream.combine(function(changed) { streams = changed }, [a, b]) @@ -145,7 +145,7 @@ o.spec("stream", function() { o("combine continues with ended streams", function() { var a = Stream() var b = Stream() - var combined = Stream.combine(function(a, b) { + var combined = Stream.combine(function() { return a() + b() }, [a, b]) @@ -159,7 +159,7 @@ o.spec("stream", function() { var streams = [] var a = Stream(3) var b = Stream(5) - Stream.combine(function(a, b, changed) { + Stream.combine(function(changed) { streams = changed }, [a, b]) @@ -209,7 +209,7 @@ o.spec("stream", function() { var count = 0 var skip = false var a = Stream(1) - var b = Stream.combine(function(a) { + var b = Stream.combine(function() { if (skip) { return Stream.SKIP } @@ -418,7 +418,7 @@ o.spec("stream", function() { o.spec("end", function() { o("end stream works", function() { var stream = Stream() - var doubled = Stream.combine(function(stream) {return stream() * 2}, [stream]) + var doubled = Stream.combine(function() {return stream() * 2}, [stream]) stream.end(true) @@ -428,7 +428,7 @@ o.spec("stream", function() { }) o("end stream works with default value", function() { var stream = Stream(2) - var doubled = Stream.combine(function(stream) {return stream() * 2}, [stream]) + var doubled = Stream.combine(function() {return stream() * 2}, [stream]) stream.end(true) @@ -440,14 +440,14 @@ o.spec("stream", function() { var stream = Stream(2) stream.end(true) - var doubled = Stream.combine(function(stream) {return stream() * 2}, [stream]) + var doubled = Stream.combine(function() {return stream() * 2}, [stream]) stream(3) o(doubled()).equals(undefined) }) o("upstream does not affect ended stream", function() { var stream = Stream(2) - var doubled = Stream.combine(function(stream) {return stream() * 2}, [stream]) + var doubled = Stream.combine(function() {return stream() * 2}, [stream]) doubled.end(true) From 84472c45061dc881babfde6f9749c6b0830554a4 Mon Sep 17 00:00:00 2001 From: Claudia Meadows Date: Fri, 11 Oct 2024 01:53:07 -0700 Subject: [PATCH 53/95] Move `redraw` to an options bag This prepares for future possible render flags, like a "remove on error" flag that might get exposed in the future to enable userland error boundaries. --- src/core.js | 6 ++--- tests/core/context.js | 12 +++++----- tests/core/event.js | 6 ++--- tests/core/input.js | 8 +++---- tests/core/mountRedraw.js | 2 +- tests/std/init.js | 48 +++++++++++++++++++-------------------- tests/std/lazy.js | 48 +++++++++++++++++++-------------------- 7 files changed, 65 insertions(+), 65 deletions(-) diff --git a/src/core.js b/src/core.js index 4a3e5f85f..841cdd22d 100644 --- a/src/core.js +++ b/src/core.js @@ -981,7 +981,7 @@ class EventDict extends Map { var currentlyRendering = [] -m.render = (dom, vnodes, redraw) => { +m.render = (dom, vnodes, {redraw} = {}) => { if (!dom) throw new TypeError("DOM element being rendered to does not exist.") if (currentlyRendering.some((d) => d === dom || d.contains(dom))) { throw new TypeError("Node is currently being rendered to and thus is locked.") @@ -1061,11 +1061,11 @@ m.mount = (root, view) => { ] redraw.sync = () => { unschedule() - m.render(root, m(Mount), redraw) + m.render(root, m(Mount), {redraw}) } m.render(root, null) - m.render(root, m(Mount), redraw) + redraw.sync() return redraw } diff --git a/tests/core/context.js b/tests/core/context.js index d0a12f6ac..b279f0753 100644 --- a/tests/core/context.js +++ b/tests/core/context.js @@ -32,7 +32,7 @@ o.spec("context", () => { var Comp = o.spy() var vnode = m.set({key: "value", one: "two"}, m(Comp)) - m.render(G.root, vnode, redraw) + m.render(G.root, vnode, {redraw}) o(Comp.callCount).equals(1) o(allKeys(Comp.args[2])).deepEquals({ @@ -43,7 +43,7 @@ o.spec("context", () => { var vnode = m.set({key: "updated", two: "three"}, m(Comp)) - m.render(G.root, vnode, redraw) + m.render(G.root, vnode, {redraw}) o(Comp.callCount).equals(2) o(allKeys(Comp.args[2])).deepEquals({ @@ -64,7 +64,7 @@ o.spec("context", () => { var Comp = o.spy() var vnode = m.set({[key]: "value", [one]: "two"}, m(Comp)) - m.render(G.root, vnode, redraw) + m.render(G.root, vnode, {redraw}) o(Comp.callCount).equals(1) o(allKeys(Comp.args[2])).deepEquals(symbolsToStrings({ @@ -75,7 +75,7 @@ o.spec("context", () => { var vnode = m.set({[key]: "updated", [two]: "three"}, m(Comp)) - m.render(G.root, vnode, redraw) + m.render(G.root, vnode, {redraw}) o(Comp.callCount).equals(2) o(allKeys(Comp.args[2])).deepEquals(symbolsToStrings({ @@ -94,7 +94,7 @@ o.spec("context", () => { var Comp = o.spy() var vnode = m.set({[key]: "value", one: "two"}, m(Comp)) - m.render(G.root, vnode, redraw) + m.render(G.root, vnode, {redraw}) o(Comp.callCount).equals(1) o(allKeys(Comp.args[2])).deepEquals(symbolsToStrings({ @@ -105,7 +105,7 @@ o.spec("context", () => { var vnode = m.set({[key]: "updated", two: "three"}, m(Comp)) - m.render(G.root, vnode, redraw) + m.render(G.root, vnode, {redraw}) o(Comp.callCount).equals(2) o(allKeys(Comp.args[2])).deepEquals(symbolsToStrings({ diff --git a/tests/core/event.js b/tests/core/event.js index ccded0cd4..fe0845f5b 100644 --- a/tests/core/event.js +++ b/tests/core/event.js @@ -9,7 +9,7 @@ o.spec("event", function() { var G = setupGlobals({initialize() { redraw = o.spy() }}) function render(dom, vnode) { - return m.render(dom, vnode, redraw) + return m.render(dom, vnode, {redraw}) } function eventSpy(fn) { @@ -286,7 +286,7 @@ o.spec("event", function() { o("handles changed spy", function() { var div1 = m("div", {ontransitionend: function() {}}) - m.render(G.root, [div1], redraw) + m.render(G.root, [div1], {redraw}) var e = G.window.document.createEvent("HTMLEvents") e.initEvent("transitionend", true, true) div1.d.dispatchEvent(e) @@ -298,7 +298,7 @@ o.spec("event", function() { var replacementRedraw = o.spy() var div2 = m("div", {ontransitionend: function() {}}) - m.render(G.root, [div2], replacementRedraw) + m.render(G.root, [div2], {redraw: replacementRedraw}) var e = G.window.document.createEvent("HTMLEvents") e.initEvent("transitionend", true, true) div2.d.dispatchEvent(e) diff --git a/tests/core/input.js b/tests/core/input.js index 1047dd2bc..d2fefe873 100644 --- a/tests/core/input.js +++ b/tests/core/input.js @@ -33,7 +33,7 @@ o.spec("form inputs", function() { var updated = m("input", {value: "aaa", oninput: function() {}}) var redraw = o.spy() - m.render(G.root, input, redraw) + m.render(G.root, input, {redraw}) //simulate user typing var e = G.window.document.createEvent("KeyboardEvent") @@ -44,7 +44,7 @@ o.spec("form inputs", function() { o(redraw.callCount).equals(1) //re-render may use same vdom value as previous render call - m.render(G.root, updated, redraw) + m.render(G.root, updated, {redraw}) o(updated.d.value).equals("aaa") o(redraw.callCount).equals(1) @@ -65,7 +65,7 @@ o.spec("form inputs", function() { var updated = m("input", {type: "checkbox", checked: true, onclick: function() {}}) var redraw = o.spy() - m.render(G.root, input, redraw) + m.render(G.root, input, {redraw}) //simulate user clicking checkbox var e = G.window.document.createEvent("MouseEvents") @@ -75,7 +75,7 @@ o.spec("form inputs", function() { o(redraw.callCount).equals(1) //re-render may use same vdom value as previous render call - m.render(G.root, updated, redraw) + m.render(G.root, updated, {redraw}) o(updated.d.checked).equals(true) o(redraw.callCount).equals(1) diff --git a/tests/core/mountRedraw.js b/tests/core/mountRedraw.js index 5046c12e1..0a3173b78 100644 --- a/tests/core/mountRedraw.js +++ b/tests/core/mountRedraw.js @@ -475,7 +475,7 @@ o.spec("mount/redraw", function() { o(G.rafMock.queueLength()).equals(0) }) - o("remounts after `m.render(G.root, null)` is invoked on the mounted root", function() { + o("remounts after `m.render(root, null)` is invoked on the mounted root", function() { var onRemove = o.spy() var onLayout = o.spy() diff --git a/tests/std/init.js b/tests/std/init.js index c81d1d7dc..60f37cd5f 100644 --- a/tests/std/init.js +++ b/tests/std/init.js @@ -12,7 +12,7 @@ o.spec("m.init", () => { var initializer = o.spy((signal) => { signal.onabort = onabort; return undefined }) var redraw = o.spy() - m.render(G.window.document.body, m.init(initializer), redraw) + m.render(G.root, m.init(initializer), {redraw}) o(initializer.callCount).equals(0) o(onabort.callCount).equals(0) o(redraw.callCount).equals(0) @@ -21,13 +21,13 @@ o.spec("m.init", () => { o(initializer.callCount).equals(1) o(onabort.callCount).equals(0) o(redraw.callCount).equals(0) - m.render(G.window.document.body, m.init(initializer), redraw) + m.render(G.root, m.init(initializer), {redraw}) await Promise.resolve() o(initializer.callCount).equals(1) o(onabort.callCount).equals(0) o(redraw.callCount).equals(1) - m.render(G.window.document.body, null, redraw) + m.render(G.root, null, {redraw}) o(initializer.callCount).equals(1) o(onabort.callCount).equals(1) @@ -39,7 +39,7 @@ o.spec("m.init", () => { var initializer = o.spy((signal) => { signal.onabort = onabort; return Promise.resolve(undefined) }) var redraw = o.spy() - m.render(G.window.document.body, m.init(initializer), redraw) + m.render(G.root, m.init(initializer), {redraw}) o(initializer.callCount).equals(0) o(onabort.callCount).equals(0) o(redraw.callCount).equals(0) @@ -48,13 +48,13 @@ o.spec("m.init", () => { o(initializer.callCount).equals(1) o(onabort.callCount).equals(0) o(redraw.callCount).equals(0) - m.render(G.window.document.body, m.init(initializer), redraw) + m.render(G.root, m.init(initializer), {redraw}) await Promise.resolve() o(initializer.callCount).equals(1) o(onabort.callCount).equals(0) o(redraw.callCount).equals(1) - m.render(G.window.document.body, null, redraw) + m.render(G.root, null, {redraw}) o(initializer.callCount).equals(1) o(onabort.callCount).equals(1) @@ -66,7 +66,7 @@ o.spec("m.init", () => { var initializer = o.spy((signal) => { signal.onabort = onabort; return null }) var redraw = o.spy() - m.render(G.window.document.body, m.init(initializer), redraw) + m.render(G.root, m.init(initializer), {redraw}) o(initializer.callCount).equals(0) o(onabort.callCount).equals(0) o(redraw.callCount).equals(0) @@ -75,13 +75,13 @@ o.spec("m.init", () => { o(initializer.callCount).equals(1) o(onabort.callCount).equals(0) o(redraw.callCount).equals(0) - m.render(G.window.document.body, m.init(initializer), redraw) + m.render(G.root, m.init(initializer), {redraw}) await Promise.resolve() o(initializer.callCount).equals(1) o(onabort.callCount).equals(0) o(redraw.callCount).equals(1) - m.render(G.window.document.body, null, redraw) + m.render(G.root, null, {redraw}) o(initializer.callCount).equals(1) o(onabort.callCount).equals(1) @@ -93,7 +93,7 @@ o.spec("m.init", () => { var initializer = o.spy((signal) => { signal.onabort = onabort; return Promise.resolve(null) }) var redraw = o.spy() - m.render(G.window.document.body, m.init(initializer), redraw) + m.render(G.root, m.init(initializer), {redraw}) o(initializer.callCount).equals(0) o(onabort.callCount).equals(0) o(redraw.callCount).equals(0) @@ -102,13 +102,13 @@ o.spec("m.init", () => { o(initializer.callCount).equals(1) o(onabort.callCount).equals(0) o(redraw.callCount).equals(0) - m.render(G.window.document.body, m.init(initializer), redraw) + m.render(G.root, m.init(initializer), {redraw}) await Promise.resolve() o(initializer.callCount).equals(1) o(onabort.callCount).equals(0) o(redraw.callCount).equals(1) - m.render(G.window.document.body, null, redraw) + m.render(G.root, null, {redraw}) o(initializer.callCount).equals(1) o(onabort.callCount).equals(1) @@ -120,7 +120,7 @@ o.spec("m.init", () => { var initializer = o.spy((signal) => { signal.onabort = onabort; return true }) var redraw = o.spy() - m.render(G.window.document.body, m.init(initializer), redraw) + m.render(G.root, m.init(initializer), {redraw}) o(initializer.callCount).equals(0) o(onabort.callCount).equals(0) o(redraw.callCount).equals(0) @@ -129,13 +129,13 @@ o.spec("m.init", () => { o(initializer.callCount).equals(1) o(onabort.callCount).equals(0) o(redraw.callCount).equals(0) - m.render(G.window.document.body, m.init(initializer), redraw) + m.render(G.root, m.init(initializer), {redraw}) await Promise.resolve() o(initializer.callCount).equals(1) o(onabort.callCount).equals(0) o(redraw.callCount).equals(1) - m.render(G.window.document.body, null, redraw) + m.render(G.root, null, {redraw}) o(initializer.callCount).equals(1) o(onabort.callCount).equals(1) @@ -147,7 +147,7 @@ o.spec("m.init", () => { var initializer = o.spy((signal) => { signal.onabort = onabort; return Promise.resolve(true) }) var redraw = o.spy() - m.render(G.window.document.body, m.init(initializer), redraw) + m.render(G.root, m.init(initializer), {redraw}) o(initializer.callCount).equals(0) o(onabort.callCount).equals(0) o(redraw.callCount).equals(0) @@ -156,13 +156,13 @@ o.spec("m.init", () => { o(initializer.callCount).equals(1) o(onabort.callCount).equals(0) o(redraw.callCount).equals(0) - m.render(G.window.document.body, m.init(initializer), redraw) + m.render(G.root, m.init(initializer), {redraw}) await Promise.resolve() o(initializer.callCount).equals(1) o(onabort.callCount).equals(0) o(redraw.callCount).equals(1) - m.render(G.window.document.body, null, redraw) + m.render(G.root, null, {redraw}) o(initializer.callCount).equals(1) o(onabort.callCount).equals(1) @@ -174,7 +174,7 @@ o.spec("m.init", () => { var initializer = o.spy((signal) => { signal.onabort = onabort; return false }) var redraw = o.spy() - m.render(G.window.document.body, m.init(initializer), redraw) + m.render(G.root, m.init(initializer), {redraw}) o(initializer.callCount).equals(0) o(onabort.callCount).equals(0) o(redraw.callCount).equals(0) @@ -183,13 +183,13 @@ o.spec("m.init", () => { o(initializer.callCount).equals(1) o(onabort.callCount).equals(0) o(redraw.callCount).equals(0) - m.render(G.window.document.body, m.init(initializer), redraw) + m.render(G.root, m.init(initializer), {redraw}) await Promise.resolve() o(initializer.callCount).equals(1) o(onabort.callCount).equals(0) o(redraw.callCount).equals(0) - m.render(G.window.document.body, null, redraw) + m.render(G.root, null, {redraw}) o(initializer.callCount).equals(1) o(onabort.callCount).equals(1) @@ -201,7 +201,7 @@ o.spec("m.init", () => { var initializer = o.spy((signal) => { signal.onabort = onabort; return Promise.resolve(false) }) var redraw = o.spy() - m.render(G.window.document.body, m.init(initializer), redraw) + m.render(G.root, m.init(initializer), {redraw}) o(initializer.callCount).equals(0) o(onabort.callCount).equals(0) o(redraw.callCount).equals(0) @@ -210,13 +210,13 @@ o.spec("m.init", () => { o(initializer.callCount).equals(1) o(onabort.callCount).equals(0) o(redraw.callCount).equals(0) - m.render(G.window.document.body, m.init(initializer), redraw) + m.render(G.root, m.init(initializer), {redraw}) await Promise.resolve() o(initializer.callCount).equals(1) o(onabort.callCount).equals(0) o(redraw.callCount).equals(0) - m.render(G.window.document.body, null, redraw) + m.render(G.root, null, {redraw}) o(initializer.callCount).equals(1) o(onabort.callCount).equals(1) diff --git a/tests/std/lazy.js b/tests/std/lazy.js index 124a5e97d..953499fe7 100644 --- a/tests/std/lazy.js +++ b/tests/std/lazy.js @@ -34,7 +34,7 @@ o.spec("lazy", () => { m.render(G.root, [ m(C, {name: "one"}), m(C, {name: "two"}), - ], redraw) + ], {redraw}) o(calls).deepEquals([ "fetch", @@ -52,7 +52,7 @@ o.spec("lazy", () => { m.render(G.root, [ m(C, {name: "one"}), m(C, {name: "two"}), - ], redraw) + ], {redraw}) o(calls).deepEquals([ "fetch", @@ -64,7 +64,7 @@ o.spec("lazy", () => { m.render(G.root, [ m(C, {name: "one"}), m(C, {name: "two"}), - ], redraw) + ], {redraw}) o(calls).deepEquals([ "fetch", @@ -101,7 +101,7 @@ o.spec("lazy", () => { m.render(G.root, [ m(C, {name: "one"}), m(C, {name: "two"}), - ], redraw) + ], {redraw}) o(calls).deepEquals([ "fetch", @@ -120,7 +120,7 @@ o.spec("lazy", () => { m.render(G.root, [ m(C, {name: "one"}), m(C, {name: "two"}), - ], redraw) + ], {redraw}) o(calls).deepEquals([ "fetch", @@ -131,7 +131,7 @@ o.spec("lazy", () => { m.render(G.root, [ m(C, {name: "one"}), m(C, {name: "two"}), - ], redraw) + ], {redraw}) o(calls).deepEquals([ "fetch", @@ -168,7 +168,7 @@ o.spec("lazy", () => { m.render(G.root, [ m(C, {name: "one"}), m(C, {name: "two"}), - ], redraw) + ], {redraw}) o(calls).deepEquals([ "fetch", @@ -190,7 +190,7 @@ o.spec("lazy", () => { m.render(G.root, [ m(C, {name: "one"}), m(C, {name: "two"}), - ], redraw) + ], {redraw}) o(calls).deepEquals([ "fetch", @@ -204,7 +204,7 @@ o.spec("lazy", () => { m.render(G.root, [ m(C, {name: "one"}), m(C, {name: "two"}), - ], redraw) + ], {redraw}) o(calls).deepEquals([ "fetch", @@ -246,7 +246,7 @@ o.spec("lazy", () => { m.render(G.root, [ m(C, {name: "one"}), m(C, {name: "two"}), - ], redraw) + ], {redraw}) o(calls).deepEquals([ "fetch", @@ -269,7 +269,7 @@ o.spec("lazy", () => { m.render(G.root, [ m(C, {name: "one"}), m(C, {name: "two"}), - ], redraw) + ], {redraw}) o(calls).deepEquals([ "fetch", @@ -282,7 +282,7 @@ o.spec("lazy", () => { m.render(G.root, [ m(C, {name: "one"}), m(C, {name: "two"}), - ], redraw) + ], {redraw}) o(calls).deepEquals([ "fetch", @@ -321,7 +321,7 @@ o.spec("lazy", () => { m.render(G.root, [ m(C, {name: "one"}), m(C, {name: "two"}), - ], redraw) + ], {redraw}) o(calls).deepEquals([ "fetch", @@ -339,7 +339,7 @@ o.spec("lazy", () => { m.render(G.root, [ m(C, {name: "one"}), m(C, {name: "two"}), - ], redraw) + ], {redraw}) o(calls).deepEquals([ "fetch", @@ -351,7 +351,7 @@ o.spec("lazy", () => { m.render(G.root, [ m(C, {name: "one"}), m(C, {name: "two"}), - ], redraw) + ], {redraw}) o(calls).deepEquals([ "fetch", @@ -391,7 +391,7 @@ o.spec("lazy", () => { m.render(G.root, [ m(C, {name: "one"}), m(C, {name: "two"}), - ], redraw) + ], {redraw}) o(calls).deepEquals([ "fetch", @@ -410,7 +410,7 @@ o.spec("lazy", () => { m.render(G.root, [ m(C, {name: "one"}), m(C, {name: "two"}), - ], redraw) + ], {redraw}) o(calls).deepEquals([ "fetch", @@ -423,7 +423,7 @@ o.spec("lazy", () => { m.render(G.root, [ m(C, {name: "one"}), m(C, {name: "two"}), - ], redraw) + ], {redraw}) o(calls).deepEquals([ "fetch", @@ -467,7 +467,7 @@ o.spec("lazy", () => { m.render(G.root, [ m(C, {name: "one"}), m(C, {name: "two"}), - ], redraw) + ], {redraw}) o(calls).deepEquals([ "fetch", @@ -489,7 +489,7 @@ o.spec("lazy", () => { m.render(G.root, [ m(C, {name: "one"}), m(C, {name: "two"}), - ], redraw) + ], {redraw}) o(calls).deepEquals([ "fetch", @@ -503,7 +503,7 @@ o.spec("lazy", () => { m.render(G.root, [ m(C, {name: "one"}), m(C, {name: "two"}), - ], redraw) + ], {redraw}) o(calls).deepEquals([ "fetch", @@ -548,7 +548,7 @@ o.spec("lazy", () => { m.render(G.root, [ m(C, {name: "one"}), m(C, {name: "two"}), - ], redraw) + ], {redraw}) o(calls).deepEquals([ "fetch", @@ -571,7 +571,7 @@ o.spec("lazy", () => { m.render(G.root, [ m(C, {name: "one"}), m(C, {name: "two"}), - ], redraw) + ], {redraw}) o(calls).deepEquals([ "fetch", @@ -586,7 +586,7 @@ o.spec("lazy", () => { m.render(G.root, [ m(C, {name: "one"}), m(C, {name: "two"}), - ], redraw) + ], {redraw}) o(calls).deepEquals([ "fetch", From 70cc05bfced194f5a6186492d93b547e883dad1f Mon Sep 17 00:00:00 2001 From: Claudia Meadows Date: Fri, 11 Oct 2024 02:01:04 -0700 Subject: [PATCH 54/95] Move router functions to arrow functions to save a little space --- src/std/router.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/std/router.js b/src/std/router.js index 6ab17ed57..24138cbc0 100644 --- a/src/std/router.js +++ b/src/std/router.js @@ -1,7 +1,7 @@ /* global window: false */ import m from "../core.js" -export function WithRouter({prefix, initial: href}) { +export var WithRouter = ({prefix, initial: href}) => { if (prefix == null) prefix = "" if (typeof prefix !== "string") { @@ -77,7 +77,7 @@ export function WithRouter({prefix, initial: href}) { // showing the link in the first place. If you absolutely have to disable the link, disable it by // removing this component (like via `m("div", {disabled}, !disabled && m(Link))`). There's // friction here for a reason. -export function Link() { +export var Link = () => { var opts, setRoute var listener = (ev) => { // Adapted from React Router's implementation: From 1cf9de3db6a0b4998d1b014748facb604baeaedf Mon Sep 17 00:00:00 2001 From: Claudia Meadows Date: Fri, 11 Oct 2024 21:53:05 -0700 Subject: [PATCH 55/95] `undefined` -> `null` to save a few bytes --- src/std/lazy.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/std/lazy.js b/src/std/lazy.js index 060587d67..308445e39 100644 --- a/src/std/lazy.js +++ b/src/std/lazy.js @@ -26,7 +26,7 @@ var lazy = (opts) => { return (attrs) => { var f = init - init = undefined + init = null if (typeof f === "function") f() return m(Comp, attrs) } From 455765acd13610771b2c7e51fc03b2fa1851d113 Mon Sep 17 00:00:00 2001 From: Claudia Meadows Date: Fri, 11 Oct 2024 23:00:16 -0700 Subject: [PATCH 56/95] Add bi-edge debouncing + throttling rate limiters Proper debouncing and throttling is easy to screw up, and the usual implementations that invoke a provided callback don't compose well. Instead, this provides functions that return promises you can filter on. This is far more composable and far easier to use. Also, the thing people usually do in a pinch, leading edge throttling, is usually the wrong thing to do. For example, if you're implementing an interactive search, you want the first to go immediately, but still incrementally update while the user is typing. --- src/entry/mithril.esm.js | 3 + src/std/rate-limit.js | 188 +++++++++++++++++++++++++++++++++++++++ tests/std/debouncer.js | 170 +++++++++++++++++++++++++++++++++++ tests/std/throttler.js | 170 +++++++++++++++++++++++++++++++++++ 4 files changed, 531 insertions(+) create mode 100644 src/std/rate-limit.js create mode 100644 tests/std/debouncer.js create mode 100644 tests/std/throttler.js diff --git a/src/entry/mithril.esm.js b/src/entry/mithril.esm.js index bd11e2020..e89e5ccad 100644 --- a/src/entry/mithril.esm.js +++ b/src/entry/mithril.esm.js @@ -1,6 +1,7 @@ import m from "../core.js" import {Link, WithRouter} from "../std/router.js" +import {debouncer, throttler} from "../std/rate-limit.js" import init from "../std/init.js" import lazy from "../std/lazy.js" import p from "../std/p.js" @@ -16,5 +17,7 @@ m.lazy = lazy m.init = init m.use = use m.tracked = tracked +m.throttler = throttler +m.debouncer = debouncer export default m diff --git a/src/std/rate-limit.js b/src/std/rate-limit.js new file mode 100644 index 000000000..14e779373 --- /dev/null +++ b/src/std/rate-limit.js @@ -0,0 +1,188 @@ +/* global performance, setTimeout, clearTimeout */ + +var validateDelay = (delay) => { + if (!Number.isFinite(delay) || delay <= 0) { + throw new RangeError("Timer delay must be finite and positive") + } +} + +var rateLimiterImpl = (delay = 500, isThrottler) => { + validateDelay(delay) + + var closed = false + var start = 0 + var timer = 0 + var resolveNext + + var callback = () => { + timer = undefined + if (typeof resolveNext === "function") { + resolveNext(false) + resolveNext = undefined + } + } + + var rateLimiter = async (ignoreLeading) => { + if (closed) { + return true + } + + if (typeof resolveNext === "function") { + resolveNext(true) + resolveNext = null + } + + if (timer) { + if (isThrottler) { + return new Promise((resolve) => resolveNext = resolve) + } + + clearTimeout(timer) + ignoreLeading = true + } + + start = performance.now() + timer = setTimeout(callback, delay) + + if (!ignoreLeading) { + return + } + + return new Promise((resolve) => resolveNext = resolve) + } + + rateLimiter.update = (newDelay) => { + validateDelay(newDelay) + delay = newDelay + + if (closed) return + if (timer) { + clearTimeout(timer) + timer = setTimeout(callback, (start - performance.now()) + delay) + } + } + + rateLimiter.dispose = () => { + if (closed) return + closed = true + clearTimeout(timer) + if (typeof resolveNext === "function") { + resolveNext(true) + resolveNext = null + } + } + + return rateLimiter +} + +/** + * A general-purpose bi-edge throttler, with a dynamically configurable limit. It's much better + * than your typical `throttle(f, ms)` because it lets you easily separate the trigger and reaction + * using a single shared, encapsulated state object. That same separation is also used to make the + * rate limit dynamically reconfigurable on hit. + * + * Create as `throttled = m.throttler(ms)` and do `if (await throttled()) return` to rate-limit + * the code that follows. The result is one of three values, to allow you to identify edges: + * + * - Leading edge: `undefined` + * - Trailing edge: `false`, returned only if a second call was made + * - No edge: `true` + * + * Call `throttled.update(ms)` to update the interval. This not only impacts future delays, but also any current one. + * + * To dispose, like on component removal, call `throttled.dispose()`. + * + * If you don't sepecify a delay, it defaults to 500ms on creation, which works well enough for + * most needs. There is no default for `throttled.update(...)` - you must specify one explicitly. + * + * Example usage: + * + * ```js + * const throttled = m.throttler() + * let results, error + * return (_attrs, _old, {redraw}) => [ + * m.remove(throttled.dispose), + * m("input[type=search]", { + * async oninput(ev) { + * if (await throttled()) return false // Skip redraw if rate limited - it's pointless + * error = results = null + * redraw() + * try { + * const response = await fetch(m.p("/search", {q: ev.target.value})) + * if (response.ok) { + * results = await response.json() + * } else { + * error = await response.text() + * } + * } catch (e) { + * error = e.message + * } + * }, + * }), + * results.map((result) => m(SearchResult, {result})), + * !error || m(ErrorDisplay, {error})), + * ] + * ``` + * + * Important note: due to the way this is implemented in basically all runtimes, the throttler's + * clock might not tick during sleep, so if you do `await throttled()` and immediately sleep in a + * low-power state for 5 minutes, you might have to wait another 10 minutes after resuming to a + * high-power state. + */ +var throttler = (delay) => rateLimiterImpl(delay, 1) + +/** + * A general-purpose bi-edge debouncer, with a dynamically configurable limit. It's much better + * than your typical `debounce(f, ms)` because it lets you easily separate the trigger and reaction + * using a single shared, encapsulated state object. That same separation is also used to make the + * rate limit dynamically reconfigurable on hit. + * + * Create as `debounced = m.debouncer(ms)` and do `if (await debounced()) return` to rate-limit + * the code that follows. The result is one of three values, to allow you to identify edges: + * + * - Leading edge: `undefined` + * - Trailing edge: `false`, returned only if a second call was made + * - No edge: `true` + * + * Call `debounced.update(ms)` to update the interval. This not only impacts future delays, but also any current one. + * + * To dispose, like on component removal, call `debounced.dispose()`. + * + * If you don't sepecify a delay, it defaults to 500ms on creation, which works well enough for + * most needs. There is no default for `debounced.update(...)` - you must specify one explicitly. + * + * Example usage: + * + * ```js + * const debounced = m.debouncer() + * let results, error + * return (attrs, _, {redraw}) => [ + * m.remove(debounced.dispose), + * m("input[type=text].value", { + * async oninput(ev) { + * if ((await debounced()) !== false) return + * try { + * const response = await fetch(m.p("/save/:id", {id: attrs.id}), { + * body: JSON.stringify({value: ev.target.value}), + * }) + * if (!response.ok) { + * error = await response.text() + * } + * } catch (e) { + * error = e.message + * } + * }, + * }), + * results.map((result) => m(SearchResult, {result})), + * !error || m(ErrorDisplay, {error})), + * ] + * ``` + * + * Important note: due to the way this is implemented in basically all runtimes, the debouncer's + * clock might not tick during sleep, so if you do `await debounced()` and immediately sleep in a + * low-power state for 5 minutes, you might have to wait another 10 minutes after resuming to a + * high-power state. + */ +var debouncer = (delay) => rateLimiterImpl(delay, 0) + +export {throttler, debouncer} diff --git a/tests/std/debouncer.js b/tests/std/debouncer.js new file mode 100644 index 000000000..b6d1a2e6a --- /dev/null +++ b/tests/std/debouncer.js @@ -0,0 +1,170 @@ +import o from "ospec" + +import m from "../../src/entry/mithril.esm.js" + +o.spec("debouncer", () => { + function sleep(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)) + } + + var debounced + + o.afterEach(() => { + if (debounced) debounced.dispose() + }) + + o("validates create input", () => { + o(() => m.debouncer(NaN)).throws(RangeError) + o(() => m.debouncer(+1/0)).throws(RangeError) + o(() => m.debouncer(-1/0)).throws(RangeError) + o(() => m.debouncer("")).throws(RangeError) + o(() => m.debouncer("123")).throws(RangeError) + o(() => m.debouncer(true)).throws(RangeError) + o(() => m.debouncer(false)).throws(RangeError) + o(() => m.debouncer(null)).throws(RangeError) + o(() => m.debouncer([])).throws(RangeError) + o(() => m.debouncer({})).throws(RangeError) + o(() => m.debouncer(Symbol("wat"))).throws(RangeError) + m.debouncer() + m.debouncer(100) + }) + + o("validates update input", () => { + debounced = m.debouncer() + + o(() => debounced.update(NaN)).throws(RangeError) + o(() => debounced.update(+1/0)).throws(RangeError) + o(() => debounced.update(-1/0)).throws(RangeError) + o(() => debounced.update("")).throws(RangeError) + o(() => debounced.update("123")).throws(RangeError) + o(() => debounced.update(true)).throws(RangeError) + o(() => debounced.update(false)).throws(RangeError) + o(() => debounced.update(null)).throws(RangeError) + o(() => debounced.update([])).throws(RangeError) + o(() => debounced.update({})).throws(RangeError) + o(() => debounced.update(Symbol("wat"))).throws(RangeError) + o(() => debounced.update()).throws(RangeError) + debounced.update(100) + }) + + o("detects edges correctly", async () => { + o.timeout(1000) + + debounced = m.debouncer(100) + + var p1 = debounced() + var p2 = debounced() + await sleep(10) + var p3 = debounced() + await sleep(140) + var p4 = debounced() + o(await p1).equals(undefined) + o(await p2).equals(true) + o(await p3).equals(false) + o(await p4).equals(undefined) + + var p5 = debounced() + await sleep(150) + var p6 = debounced() + o(await p5).equals(false) + o(await p6).equals(undefined) + }) + + o("resets the timer on early hit", async () => { + o.timeout(1000) + + debounced = m.debouncer(100) + + var slept = false + setTimeout(() => { slept = true }, 125) + void debounced() + await sleep(50) + await debounced() + o(slept).equals(true) + }) + + o("handles dynamic changes to higher delays", async () => { + o.timeout(1000) + + debounced = m.debouncer(100) + + var p1 = debounced() + var p2 = debounced() + await sleep(10) + var p3 = debounced() + debounced.update(200) + await sleep(140) + var p4 = debounced() + o(await p1).equals(undefined) + o(await p2).equals(true) + o(await p3).equals(true) + o(await p4).equals(false) + + var p5 = debounced() + await sleep(250) + var p6 = debounced() + o(await p5).equals(undefined) + o(await p6).equals(undefined) + }) + + o("handles dynamic changes to lower delays", async () => { + o.timeout(1000) + + debounced = m.debouncer(100) + + var p1 = debounced() + var p2 = debounced() + await sleep(10) + var p3 = debounced() + debounced.update(50) + await sleep(100) + var p4 = debounced() + o(await p1).equals(undefined) + o(await p2).equals(true) + o(await p3).equals(false) + o(await p4).equals(undefined) + + var p5 = debounced() + await sleep(100) + var p6 = debounced() + o(await p5).equals(false) + o(await p6).equals(undefined) + }) + + o("handles same-duration changes", async () => { + o.timeout(1000) + + debounced = m.debouncer(100) + + var p1 = debounced() + debounced.update(100) + var p2 = debounced() + debounced.update(100) + await sleep(10) + debounced.update(100) + var p3 = debounced() + debounced.update(100) + await sleep(140) + debounced.update(100) + var p4 = debounced() + debounced.update(100) + o(await p1).equals(undefined) + debounced.update(100) + o(await p2).equals(true) + debounced.update(100) + o(await p3).equals(false) + debounced.update(100) + o(await p4).equals(undefined) + debounced.update(100) + + var p5 = debounced() + debounced.update(100) + await sleep(150) + debounced.update(100) + var p6 = debounced() + debounced.update(100) + o(await p5).equals(false) + debounced.update(100) + o(await p6).equals(undefined) + }) +}) diff --git a/tests/std/throttler.js b/tests/std/throttler.js new file mode 100644 index 000000000..c85402472 --- /dev/null +++ b/tests/std/throttler.js @@ -0,0 +1,170 @@ +import o from "ospec" + +import m from "../../src/entry/mithril.esm.js" + +o.spec("throttler", () => { + function sleep(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)) + } + + var throttled + + o.afterEach(() => { + if (throttled) throttled.dispose() + }) + + o("validates create input", () => { + o(() => m.throttler(NaN)).throws(RangeError) + o(() => m.throttler(+1/0)).throws(RangeError) + o(() => m.throttler(-1/0)).throws(RangeError) + o(() => m.throttler("")).throws(RangeError) + o(() => m.throttler("123")).throws(RangeError) + o(() => m.throttler(true)).throws(RangeError) + o(() => m.throttler(false)).throws(RangeError) + o(() => m.throttler(null)).throws(RangeError) + o(() => m.throttler([])).throws(RangeError) + o(() => m.throttler({})).throws(RangeError) + o(() => m.throttler(Symbol("wat"))).throws(RangeError) + m.throttler() + m.throttler(100) + }) + + o("validates update input", () => { + throttled = m.throttler() + + o(() => throttled.update(NaN)).throws(RangeError) + o(() => throttled.update(+1/0)).throws(RangeError) + o(() => throttled.update(-1/0)).throws(RangeError) + o(() => throttled.update("")).throws(RangeError) + o(() => throttled.update("123")).throws(RangeError) + o(() => throttled.update(true)).throws(RangeError) + o(() => throttled.update(false)).throws(RangeError) + o(() => throttled.update(null)).throws(RangeError) + o(() => throttled.update([])).throws(RangeError) + o(() => throttled.update({})).throws(RangeError) + o(() => throttled.update(Symbol("wat"))).throws(RangeError) + o(() => throttled.update()).throws(RangeError) + throttled.update(100) + }) + + o("detects edges correctly", async () => { + o.timeout(1000) + + throttled = m.throttler(100) + + var p1 = throttled() + var p2 = throttled() + await sleep(10) + var p3 = throttled() + await sleep(140) + var p4 = throttled() + o(await p1).equals(undefined) + o(await p2).equals(true) + o(await p3).equals(false) + o(await p4).equals(undefined) + + var p5 = throttled() + await sleep(150) + var p6 = throttled() + o(await p5).equals(false) + o(await p6).equals(undefined) + }) + + o("retains the timer on early hit", async () => { + o.timeout(1000) + + throttled = m.throttler(100) + + var slept = false + setTimeout(() => { slept = true }, 125) + void throttled() + await sleep(50) + await throttled() + o(slept).equals(false) + }) + + o("handles dynamic changes to higher delays", async () => { + o.timeout(1000) + + throttled = m.throttler(100) + + var p1 = throttled() + var p2 = throttled() + await sleep(10) + var p3 = throttled() + throttled.update(200) + await sleep(140) + var p4 = throttled() + o(await p1).equals(undefined) + o(await p2).equals(true) + o(await p3).equals(true) + o(await p4).equals(false) + + var p5 = throttled() + await sleep(250) + var p6 = throttled() + o(await p5).equals(undefined) + o(await p6).equals(undefined) + }) + + o("handles dynamic changes to lower delays", async () => { + o.timeout(1000) + + throttled = m.throttler(100) + + var p1 = throttled() + var p2 = throttled() + await sleep(10) + var p3 = throttled() + throttled.update(50) + await sleep(100) + var p4 = throttled() + o(await p1).equals(undefined) + o(await p2).equals(true) + o(await p3).equals(false) + o(await p4).equals(undefined) + + var p5 = throttled() + await sleep(100) + var p6 = throttled() + o(await p5).equals(false) + o(await p6).equals(undefined) + }) + + o("handles same-duration changes", async () => { + o.timeout(1000) + + throttled = m.throttler(100) + + var p1 = throttled() + throttled.update(100) + var p2 = throttled() + throttled.update(100) + await sleep(10) + throttled.update(100) + var p3 = throttled() + throttled.update(100) + await sleep(140) + throttled.update(100) + var p4 = throttled() + throttled.update(100) + o(await p1).equals(undefined) + throttled.update(100) + o(await p2).equals(true) + throttled.update(100) + o(await p3).equals(false) + throttled.update(100) + o(await p4).equals(undefined) + throttled.update(100) + + var p5 = throttled() + throttled.update(100) + await sleep(150) + throttled.update(100) + var p6 = throttled() + throttled.update(100) + o(await p5).equals(false) + throttled.update(100) + o(await p6).equals(undefined) + }) +}) From 118d748e5e18685046bcb0421f8752a07e8a6440 Mon Sep 17 00:00:00 2001 From: Claudia Meadows Date: Sat, 12 Oct 2024 00:34:14 -0700 Subject: [PATCH 57/95] Stub out error handling Also made some small (but needed) changes to the entry points and minified scripts. No specific tests for the error handling yet aside from the existing error-related tests that needed to change. But here's the going idea: error boundaries can be done in userland. Users would do something like this: ```js function slurpContext(context) { const chain = [] while (context !== null && context !== Object.prototype) { chain.push(context) context = Object.getPrototypeOf(context) } return chain.reduceRight((a, b) => Object.defineProperties(a, Object.getOwnPropertyDescriptors(b)), {}) } function ErrorBoundary(attrs, _, context) { return m("div.boundary", [ m.layout((dom) => { try { m.render(dom, m.set(slurpContext(context), attrs.view()), { render: context.render, removeOnThrow: true, }) } catch (e) { attrs.onerror(e) } }), m.remove((dom) => m.render(dom, null)), ]) } ``` --- package.json | 10 +- scripts/build.js | 24 +- src/core.js | 494 +++++++++++++++++++++----------------- tests/core/component.js | 39 ++- tests/core/mountRedraw.js | 33 +-- tests/core/oncreate.js | 3 +- tests/core/render.js | 27 ++- 7 files changed, 357 insertions(+), 273 deletions(-) diff --git a/package.json b/package.json index efd0c30aa..6632889d0 100644 --- a/package.json +++ b/package.json @@ -19,11 +19,17 @@ "exports": { "./stream.js": { "import": "./dist/stream.esm.js", - "require": "./dist/stream.umd.js" + "require": "./dist/stream.umd.js", + "production": { + "import": "./dist/stream.esm.min.js" + } }, ".": { "import": "./dist/mithril.esm.js", - "require": "./dist/mithril.umd.js" + "require": "./dist/mithril.umd.js", + "production": { + "import": "./dist/mithril.esm.min.js" + } } }, "scripts": { diff --git a/scripts/build.js b/scripts/build.js index 028956f10..24092ad64 100644 --- a/scripts/build.js +++ b/scripts/build.js @@ -9,11 +9,23 @@ import terser from "@rollup/plugin-terser" const dirname = path.dirname(fileURLToPath(import.meta.url)) -/** @type {{[key: import("rollup").ModuleFormat]: import("rollup").Plugin}} */ -const terserPlugin = terser({ - compress: {passes: 3}, - sourceMap: true, -}) +/** @type {Partial>} */ +const terserMinify = { + iife: terser({ + compress: {passes: 3}, + format: {wrap_func_args: false}, + sourceMap: true, + }), + // See the comment in `src/core.js` + esm: terser({ + compress: {passes: 3}, + format: { + preserve_annotations: true, + wrap_func_args: false, + }, + sourceMap: true, + }), +} function format(n) { return n.toString().replace(/(\d)(?=(\d\d\d)+(?!\d))/g, "$1,") @@ -26,7 +38,7 @@ async function build(name, format) { try { await Promise.all([ bundle.write({file: path.resolve(dirname, `../dist/${name}.js`), format, sourcemap: true}), - bundle.write({file: path.resolve(dirname, `../dist/${name}.min.js`), format, plugins: [terserPlugin], sourcemap: true}), + bundle.write({file: path.resolve(dirname, `../dist/${name}.min.js`), format, plugins: [terserMinify[format]], sourcemap: true}), ]) } finally { await bundle.close() diff --git a/src/core.js b/src/core.js index 841cdd22d..ca2e1d153 100644 --- a/src/core.js +++ b/src/core.js @@ -3,19 +3,36 @@ import {hasOwn} from "./util.js" export {m as default} -// Caution: be sure to check the minified output. I've noticed an issue with Terser trying to -// inline single-use functions as IIFEs, and this predictably causes perf issues since engines -// don't seem to reliably lower this in either their bytecode generation *or* their optimized code. -// -// Rather than painfully trying to reduce that to an MVC and filing a bug against it, I'm just -// inlining and commenting everything. It also gives me a better idea of the true cost of various -// functions. -// -// In a few places, there's no-inline hints (`/* @__NOINLINE__ */`) to prevent Terser from -// inlining, in code paths where it's relevant. -// -// Also, be aware: I use some bit operations here. Nothing super fancy like find-first-set, just -// mainly ANDs, ORs, and a one-off XOR for inequality. +/* +Caution: be sure to check the minified output. I've noticed an issue with Terser trying to inline +single-use functions as IIFEs, and this predictably causes perf issues since engines don't seem to +reliably lower this in either their bytecode generation *or* their optimized code. + +Rather than painfully trying to reduce that to an MVC and filing a bug against it, I'm just +inlining and commenting everything. It also gives me a better idea of the true cost of various +functions. + +In `m`, I do use a no-inline hints (the `__NOINLINE__` in an inline block comment there) to +prevent Terser from inlining a cold function in a very hot code path, to try to squeeze a little +more performance out of the framework. Likewise, to try to preserve this through build scripts, +Terser annotations are preserved in the ESM production bundle (but not the UMD bundle). + +Also, be aware: I use some bit operations here. Nothing super fancy like find-first-set, just +mainly ANDs, ORs, and a one-off XOR for inequality. +*/ + +/* +State note: + +If remove on throw is `true` and an error occurs: +- All visited vnodes' new versions are removed. +- All unvisited vnodes' old versions are removed. + +If remove on throw is `false` and an error occurs: +- Attribute modification errors are logged. +- Views that throw retain the previous version and log their error. +- Errors other than the above cause the tree to be torn down as if remove on throw was `true`. +*/ /* This same structure is used for several nodes. Here's an explainer for each type. @@ -320,6 +337,7 @@ var currentRefNode var currentNamespace var currentDocument var currentContext +var currentRemoveOnThrow var insertAfterCurrentRefNode = (child) => { if (currentRefNode) { @@ -368,9 +386,16 @@ var updateFragment = (old, vnode) => { // // Note: if either `vnode` or `old` is `null`, the common length and its own length are // both zero, so it can't actually throw. - for (var i = 0; i < commonLength; i++) updateNode(old.c[i], vnode.c[i]) - for (var i = commonLength; i < oldLength; i++) updateNode(old.c[i], null) - for (var i = commonLength; i < newLength; i++) updateNode(null, vnode.c[i]) + try { + for (var i = 0; i < commonLength; i++) updateNode(old.c[i], vnode.c[i]) + for (var i = commonLength; i < newLength; i++) updateNode(null, vnode.c[i]) + } finally { + if (i < newLength) { + commonLength = i + for (var i = 0; i < commonLength; i++) updateNode(vnode.c[i], null) + } + for (var i = commonLength; i < oldLength; i++) updateNode(old.c[i], null) + } } else { // Keyed. I take a pretty straightforward approach here to keep it simple: // 1. Build a map from old map to old vnode. @@ -386,23 +411,30 @@ var updateFragment = (old, vnode) => { var oldMap = new Map() for (var p of old.c) oldMap.set(p.t, p) - for (var n of vnode.c) { - var p = oldMap.get(n.t) - if (p == null) { - updateFragment(null, n) - } else { - oldMap.delete(n.t) - var prev = currentRefNode - try { - moveToPosition(p) - } finally { - currentRefNode = prev + try { + for (var i = 0; i < newLength; i++) { + var n = vnode.c[i] + var p = oldMap.get(n.t) + if (p == null) { + updateFragment(null, n) + } else { + oldMap.delete(n.t) + var prev = currentRefNode + try { + moveToPosition(p) + } finally { + currentRefNode = prev + } + updateFragment(p, n) } - updateFragment(p, n) } + } finally { + if (i < newLength) { + for (var j = 0; j < i; j++) updateNode(vnode.c[j], null) + updateNode(old.c[j], null) + } + oldMap.forEach((p) => updateNode(p, null)) } - - oldMap.forEach((p) => updateNode(p, null)) } } @@ -432,7 +464,11 @@ var updateNode = (old, vnode) => { type = old.m & TYPE_MASK if (vnode == null) { - removeNodeDispatch[type](old) + try { + removeNodeDispatch[type](old) + } catch (e) { + console.error(e) + } return } @@ -459,13 +495,18 @@ var updateNode = (old, vnode) => { if (type === newType && vnode.t === old.t) { vnode.m = old.m & ~FLAG_KEYED | vnode.m & FLAG_KEYED } else { - removeNodeDispatch[type](old) + updateNode(old, null) type = newType old = null } } - updateNodeDispatch[type](old, vnode) + try { + updateNodeDispatch[type](old, vnode) + } catch (e) { + updateNode(old, null) + throw e + } } var updateLayout = (_, vnode) => { @@ -613,22 +654,29 @@ var updateElement = (old, vnode) => { } var updateComponent = (old, vnode) => { - var attrs = vnode.a - var tree, oldInstance, oldAttrs - rendered: { - if (old != null) { - tree = old.s - oldInstance = old.c - oldAttrs = old.a - } else if (typeof (tree = (vnode.s = vnode.t)(attrs, oldAttrs, currentContext)) !== "function") { - break rendered + try { + var attrs = vnode.a + var tree, oldInstance, oldAttrs + rendered: { + if (old != null) { + tree = old.s + oldInstance = old.c + oldAttrs = old.a + } else if (typeof (tree = (vnode.s = vnode.t)(attrs, oldAttrs, currentContext)) !== "function") { + break rendered + } + tree = (vnode.s = tree)(attrs, oldAttrs, currentContext) } - tree = (vnode.s = tree)(attrs, oldAttrs, currentContext) - } - if (tree === vnode) { - throw new Error("A view cannot return the vnode it received as argument") + if (tree === vnode) { + throw new Error("A view cannot return the vnode it received as argument") + } + tree = m.normalize(tree) + } catch (e) { + if (currentRemoveOnThrow) throw e + console.error(e) + return } - updateNode(oldInstance, vnode.c = m.normalize(tree)) + updateNode(oldInstance, vnode.c = tree) } var removeFragment = (old) => updateFragment(old, null) @@ -648,10 +696,19 @@ var updateNodeDispatch = [ var removeNodeDispatch = [ removeFragment, removeFragment, - (old) => old.d.remove(), (old) => { + if (!old.d) return old.d.remove() - updateFragment(old, null) + old.d = null + }, + (old) => { + try { + if (!old.d) return + old.d.remove() + old.d = null + } finally { + updateFragment(old, null) + } }, (old) => updateNode(old.c, null), () => {}, @@ -759,198 +816,203 @@ Some of the optimizations it does: first place - it moves the check to just the create flow where it's only done once. */ var setAttr = (eventDict, element, mask, key, old, attrs) => { - var newValue = getPropKey(attrs, key) - var oldValue = getPropKey(old, key) - - if (mask & FLAG_IS_REMOVE && newValue !== null) return eventDict - - forceSetAttribute: { - forceTryProperty: { - skipValueDiff: { - if (key.length > 1) { - var pair1 = key.charCodeAt(0) | key.charCodeAt(1) << 16 - - if (key.length === 2 && pair1 === (ASCII_LOWER_I | ASCII_LOWER_S << 16)) { - return eventDict - } else if (pair1 === (ASCII_LOWER_O | ASCII_LOWER_N << 16)) { - if (newValue === oldValue) return eventDict - // Update the event - if (typeof newValue === "function") { - if (typeof oldValue !== "function") { - if (eventDict == null) eventDict = new EventDict() - element.addEventListener(key.slice(2), eventDict, false) - } - // Save this, so the current redraw is correctly tracked. - eventDict._ = currentRedraw - eventDict.set(key, newValue) - } else if (typeof oldValue === "function") { - element.removeEventListener(key.slice(2), eventDict, false) - eventDict.delete(key) - } - return eventDict - } else if (key.length > 3) { - var pair2 = key.charCodeAt(2) | key.charCodeAt(3) << 16 - if ( - key.length > 6 && - pair1 === (ASCII_LOWER_X | ASCII_LOWER_L << 16) && - pair2 === (ASCII_LOWER_I | ASCII_LOWER_N << 16) && - (key.charCodeAt(4) | key.charCodeAt(5) << 16) === (ASCII_LOWER_K | ASCII_COLON << 16) - ) { - key = key.slice(6) - if (newValue !== null) { - element.setAttributeNS(xlinkNs, key, newValue) - } else { - element.removeAttributeNS(xlinkNs, key) + try { + var newValue = getPropKey(attrs, key) + var oldValue = getPropKey(old, key) + + if (mask & FLAG_IS_REMOVE && newValue !== null) return eventDict + + forceSetAttribute: { + forceTryProperty: { + skipValueDiff: { + if (key.length > 1) { + var pair1 = key.charCodeAt(0) | key.charCodeAt(1) << 16 + + if (key.length === 2 && pair1 === (ASCII_LOWER_I | ASCII_LOWER_S << 16)) { + return eventDict + } else if (pair1 === (ASCII_LOWER_O | ASCII_LOWER_N << 16)) { + if (newValue === oldValue) return eventDict + // Update the event + if (typeof newValue === "function") { + if (typeof oldValue !== "function") { + if (eventDict == null) eventDict = new EventDict() + element.addEventListener(key.slice(2), eventDict, false) + } + // Save this, so the current redraw is correctly tracked. + eventDict._ = currentRedraw + eventDict.set(key, newValue) + } else if (typeof oldValue === "function") { + element.removeEventListener(key.slice(2), eventDict, false) + eventDict.delete(key) } return eventDict - } else if (key.length === 4) { + } else if (key.length > 3) { + var pair2 = key.charCodeAt(2) | key.charCodeAt(3) << 16 if ( - pair1 === (ASCII_LOWER_T | ASCII_LOWER_Y << 16) && - pair2 === (ASCII_LOWER_P | ASCII_LOWER_E << 16) - ) { - if (!(mask & FLAG_INPUT_ELEMENT)) break skipValueDiff - if (newValue === null) break forceSetAttribute - break forceTryProperty - } else if ( - // Try to avoid a few browser bugs on normal elements. - pair1 === (ASCII_LOWER_H | ASCII_LOWER_R << 16) && pair2 === (ASCII_LOWER_E | ASCII_LOWER_F << 16) || - pair1 === (ASCII_LOWER_L | ASCII_LOWER_I << 16) && pair2 === (ASCII_LOWER_S | ASCII_LOWER_T << 16) || - pair1 === (ASCII_LOWER_F | ASCII_LOWER_O << 16) && pair2 === (ASCII_LOWER_R | ASCII_LOWER_M << 16) + key.length > 6 && + pair1 === (ASCII_LOWER_X | ASCII_LOWER_L << 16) && + pair2 === (ASCII_LOWER_I | ASCII_LOWER_N << 16) && + (key.charCodeAt(4) | key.charCodeAt(5) << 16) === (ASCII_LOWER_K | ASCII_COLON << 16) ) { - // If it's a custom element, just keep it. Otherwise, force the attribute - // to be set. - if (!(mask & FLAG_CUSTOM_ELEMENT)) { - break forceSetAttribute + key = key.slice(6) + if (newValue !== null) { + element.setAttributeNS(xlinkNs, key, newValue) + } else { + element.removeAttributeNS(xlinkNs, key) } - } - } else if (key.length > 4) { - switch (key) { - case "children": - return eventDict - - case "class": - case "className": - case "title": + return eventDict + } else if (key.length === 4) { + if ( + pair1 === (ASCII_LOWER_T | ASCII_LOWER_Y << 16) && + pair2 === (ASCII_LOWER_P | ASCII_LOWER_E << 16) + ) { + if (!(mask & FLAG_INPUT_ELEMENT)) break skipValueDiff if (newValue === null) break forceSetAttribute break forceTryProperty - - case "value": - if ( - // Filter out non-HTML keys and custom elements - (mask & (FLAG_HTML_ELEMENT | FLAG_CUSTOM_ELEMENT)) !== FLAG_HTML_ELEMENT || - !(key in element) - ) { - break + } else if ( + // Try to avoid a few browser bugs on normal elements. + pair1 === (ASCII_LOWER_H | ASCII_LOWER_R << 16) && pair2 === (ASCII_LOWER_E | ASCII_LOWER_F << 16) || + pair1 === (ASCII_LOWER_L | ASCII_LOWER_I << 16) && pair2 === (ASCII_LOWER_S | ASCII_LOWER_T << 16) || + pair1 === (ASCII_LOWER_F | ASCII_LOWER_O << 16) && pair2 === (ASCII_LOWER_R | ASCII_LOWER_M << 16) + ) { + // If it's a custom element, just keep it. Otherwise, force the attribute + // to be set. + if (!(mask & FLAG_CUSTOM_ELEMENT)) { + break forceSetAttribute } - - if (newValue === null) { - if (mask & (FLAG_OPTION_ELEMENT | FLAG_SELECT_ELEMENT)) { - break forceSetAttribute - } else { - break forceTryProperty + } + } else if (key.length > 4) { + switch (key) { + case "children": + return eventDict + + case "class": + case "className": + case "title": + if (newValue === null) break forceSetAttribute + break forceTryProperty + + case "value": + if ( + // Filter out non-HTML keys and custom elements + (mask & (FLAG_HTML_ELEMENT | FLAG_CUSTOM_ELEMENT)) !== FLAG_HTML_ELEMENT || + !(key in element) + ) { + break } - } - - if (!(mask & (FLAG_INPUT_ELEMENT | FLAG_TEXTAREA_ELEMENT | FLAG_SELECT_ELEMENT | FLAG_OPTION_ELEMENT))) { - break - } - // It's always stringified, so it's okay to always coerce - if (element.value === (newValue = `${newValue}`)) { - // Setting `` to the same value causes an - // error to be generated if it's non-empty - if (mask & FLAG_IS_FILE_INPUT) return eventDict - // Setting `` to the same value by typing on focused - // element moves cursor to end in Chrome - if (mask & (FLAG_INPUT_ELEMENT | FLAG_TEXTAREA_ELEMENT)) { - if (element === currentDocument.activeElement) return eventDict - } else { - if (oldValue != null && oldValue !== false) return eventDict + if (newValue === null) { + if (mask & (FLAG_OPTION_ELEMENT | FLAG_SELECT_ELEMENT)) { + break forceSetAttribute + } else { + break forceTryProperty + } } - } - if (mask & FLAG_IS_FILE_INPUT) { - //setting input[type=file][value] to different value is an error if it's non-empty - // Not ideal, but it at least works around the most common source of uncaught exceptions for now. - if (newValue !== "") { - console.error("File input `value` attributes must either mirror the current value or be set to the empty string (to reset).") - return eventDict + if (!(mask & (FLAG_INPUT_ELEMENT | FLAG_TEXTAREA_ELEMENT | FLAG_SELECT_ELEMENT | FLAG_OPTION_ELEMENT))) { + break } - } - - break forceTryProperty - case "style": - if (oldValue === newValue) { - // Styles are equivalent, do nothing. - } else if (newValue === null) { - // New style is missing, just clear it. - element.style = "" - } else if (typeof newValue !== "object") { - // New style is a string, let engine deal with patching. - element.style = newValue - } else if (oldValue === null || typeof oldValue !== "object") { - // `old` is missing or a string, `style` is an object. - element.style = "" - // Add new style properties - setStyle(element.style, null, newValue, true) - } else { - // Both old & new are (different) objects, or `old` is missing. - // Update style properties that have changed, or add new style properties - setStyle(element.style, oldValue, newValue, true) - // Remove style properties that no longer exist - setStyle(element.style, newValue, oldValue, false) - } - return eventDict - - case "selected": - var active = currentDocument.activeElement - if ( - element === active || - mask & FLAG_OPTION_ELEMENT && element.parentNode === active - ) { - break - } - // falls through + // It's always stringified, so it's okay to always coerce + if (element.value === (newValue = `${newValue}`)) { + // Setting `` to the same value causes an + // error to be generated if it's non-empty + if (mask & FLAG_IS_FILE_INPUT) return eventDict + // Setting `` to the same value by typing on focused + // element moves cursor to end in Chrome + if (mask & (FLAG_INPUT_ELEMENT | FLAG_TEXTAREA_ELEMENT)) { + if (element === currentDocument.activeElement) return eventDict + } else { + if (oldValue != null && oldValue !== false) return eventDict + } + } - case "checked": - case "selectedIndex": - break skipValueDiff + if (mask & FLAG_IS_FILE_INPUT) { + //setting input[type=file][value] to different value is an error if it's non-empty + // Not ideal, but it at least works around the most common source of uncaught exceptions for now. + if (newValue !== "") { + console.error("File input `value` attributes must either mirror the current value or be set to the empty string (to reset).") + return eventDict + } + } - // Try to avoid a few browser bugs on normal elements. - case "width": - case "height": - // If it's a custom element, just keep it. Otherwise, force the attribute - // to be set. - if (!(mask & FLAG_CUSTOM_ELEMENT)) { - break forceSetAttribute - } + break forceTryProperty + + case "style": + if (oldValue === newValue) { + // Styles are equivalent, do nothing. + } else if (newValue === null) { + // New style is missing, just clear it. + element.style = "" + } else if (typeof newValue !== "object") { + // New style is a string, let engine deal with patching. + element.style = newValue + } else if (oldValue === null || typeof oldValue !== "object") { + // `old` is missing or a string, `style` is an object. + element.style = "" + // Add new style properties + setStyle(element.style, null, newValue, true) + } else { + // Both old & new are (different) objects, or `old` is missing. + // Update style properties that have changed, or add new style properties + setStyle(element.style, oldValue, newValue, true) + // Remove style properties that no longer exist + setStyle(element.style, newValue, oldValue, false) + } + return eventDict + + case "selected": + var active = currentDocument.activeElement + if ( + element === active || + mask & FLAG_OPTION_ELEMENT && element.parentNode === active + ) { + break + } + // falls through + + case "checked": + case "selectedIndex": + break skipValueDiff + + // Try to avoid a few browser bugs on normal elements. + case "width": + case "height": + // If it's a custom element, just keep it. Otherwise, force the attribute + // to be set. + if (!(mask & FLAG_CUSTOM_ELEMENT)) { + break forceSetAttribute + } + } } } } + + if (newValue !== null && typeof newValue !== "object" && oldValue === newValue) return } - if (newValue !== null && typeof newValue !== "object" && oldValue === newValue) return + // Filter out namespaced keys + if (!(mask & FLAG_HTML_ELEMENT)) { + break forceSetAttribute + } } // Filter out namespaced keys - if (!(mask & FLAG_HTML_ELEMENT)) { - break forceSetAttribute + // Defer the property check until *after* we check everything. + if (key in element) { + element[key] = newValue + return eventDict } } - // Filter out namespaced keys - // Defer the property check until *after* we check everything. - if (key in element) { - element[key] = newValue - return eventDict + if (newValue === null) { + if (oldValue !== null) element.removeAttribute(key) + } else { + element.setAttribute(key, newValue === true ? "" : newValue) } - } - - if (newValue === null) { - if (oldValue !== null) element.removeAttribute(key) - } else { - element.setAttribute(key, newValue === true ? "" : newValue) + } catch (e) { + if (currentRemoveOnThrow) throw e + console.error(e) } return eventDict @@ -981,7 +1043,7 @@ class EventDict extends Map { var currentlyRendering = [] -m.render = (dom, vnodes, {redraw} = {}) => { +m.render = (dom, vnodes, {redraw, removeOnThrow} = {}) => { if (!dom) throw new TypeError("DOM element being rendered to does not exist.") if (currentlyRendering.some((d) => d === dom || d.contains(dom))) { throw new TypeError("Node is currently being rendered to and thus is locked.") @@ -1001,6 +1063,7 @@ m.render = (dom, vnodes, {redraw} = {}) => { var prevNamespace = currentNamespace var prevDocument = currentDocument var prevContext = currentContext + var prevRemoveOnThrow = currentRemoveOnThrow var hooks = currentHooks = [] try { @@ -1010,6 +1073,8 @@ m.render = (dom, vnodes, {redraw} = {}) => { currentNamespace = namespace === htmlNs ? null : namespace currentDocument = dom.ownerDocument currentContext = {redraw} + // eslint-disable-next-line no-implicit-coercion + currentRemoveOnThrow = !!removeOnThrow // First time rendering into a node clears it out if (dom.vnodes == null) dom.textContent = "" @@ -1035,6 +1100,7 @@ m.render = (dom, vnodes, {redraw} = {}) => { currentNamespace = prevNamespace currentDocument = prevDocument currentContext = prevContext + currentRemoveOnThrow = prevRemoveOnThrow currentlyRendering.pop() } } diff --git a/tests/core/component.js b/tests/core/component.js index d06c21bca..832706860 100644 --- a/tests/core/component.js +++ b/tests/core/component.js @@ -169,39 +169,34 @@ o.spec("component", function() { }) o("throws a custom error if it returns itself when created", function() { // A view that returns its vnode would otherwise trigger an infinite loop - var threw = false var component = () => vnode + + console.error = o.spy() + var vnode = m(component) - try { - m.render(G.root, vnode) - } - catch (e) { - threw = true - o(e instanceof Error).equals(true) - // Call stack exception is a RangeError - o(e instanceof RangeError).equals(false) - } - o(threw).equals(true) + m.render(G.root, vnode) + + o(console.error.callCount).equals(1) + o(console.error.args[0] instanceof Error).equals(true) + // Call stack exception is a RangeError + o(console.error.args[0] instanceof RangeError).equals(false) }) o("throws a custom error if it returns itself when updated", function() { // A view that returns its vnode would otherwise trigger an infinite loop - var threw = false var component = () => vnode m.render(G.root, m(component)) o(G.root.childNodes.length).equals(0) + console.error = o.spy() + var vnode = m(component) - try { - m.render(G.root, m(component)) - } - catch (e) { - threw = true - o(e instanceof Error).equals(true) - // Call stack exception is a RangeError - o(e instanceof RangeError).equals(false) - } - o(threw).equals(true) + m.render(G.root, m(component)) + + o(console.error.callCount).equals(1) + o(console.error.args[0] instanceof Error).equals(true) + // Call stack exception is a RangeError + o(console.error.args[0] instanceof RangeError).equals(false) }) o("can update when returning fragments", function() { var component = () => [ diff --git a/tests/core/mountRedraw.js b/tests/core/mountRedraw.js index 0a3173b78..7ce9df97a 100644 --- a/tests/core/mountRedraw.js +++ b/tests/core/mountRedraw.js @@ -226,7 +226,7 @@ o.spec("mount/redraw", function() { var redraw1 = m.mount(root1, () => { calls.push("root1") }) var redraw2 = m.mount(root2, (isInit) => { - if (!isInit) { m.render(root1, null); throw new Error("fail") } + if (!isInit) { m.render(root1, null); throw "fail" } calls.push("root2") }) var redraw3 = m.mount(root3, () => { calls.push("root3") }) @@ -235,14 +235,14 @@ o.spec("mount/redraw", function() { ]) redraw1.sync() - o(() => redraw2.sync()).throws("fail") + redraw2.sync() redraw3.sync() o(calls).deepEquals([ "root1", "root2", "root3", "root1", "root3", ]) - o(console.error.calls.map((c) => c.args[0])).deepEquals([]) + o(console.error.calls.map((c) => c.args[0])).deepEquals(["fail"]) o(G.rafMock.queueLength()).equals(0) }) @@ -265,14 +265,15 @@ o.spec("mount/redraw", function() { ]) redraw1.sync() - o(() => redraw2.sync()).throws(TypeError) + redraw2.sync() redraw3.sync() o(calls).deepEquals([ "root1", "root2", "root3", "root1", "root3", ]) - o(console.error.calls.map((c) => c.args[0])).deepEquals([]) + o(console.error.callCount).equals(1) + o(console.error.args[0] instanceof TypeError).equals(true) o(G.rafMock.queueLength()).equals(0) }) @@ -294,14 +295,15 @@ o.spec("mount/redraw", function() { ]) redraw1.sync() - o(() => redraw2.sync()).throws(TypeError) + redraw2.sync() redraw3.sync() o(calls).deepEquals([ "root1", "root2", "root3", "root1", "root3", ]) - o(console.error.calls.map((c) => c.args[0])).deepEquals([]) + o(console.error.callCount).equals(1) + o(console.error.args[0] instanceof TypeError).equals(true) o(G.rafMock.queueLength()).equals(0) }) @@ -500,7 +502,8 @@ o.spec("mount/redraw", function() { }) o("propagates mount errors synchronously", function() { - o(() => m.mount(G.root, () => { throw new Error("foo") })).throws("foo") + m.mount(G.root, () => { throw "foo" }) + o(console.error.calls.map((c) => c.args[0])).deepEquals(["foo"]) }) o("propagates redraw errors synchronously", function() { @@ -509,19 +512,19 @@ o.spec("mount/redraw", function() { var redraw = m.mount(G.root, () => { switch (++counter) { case 1: return null - case 2: throw new Error("foo") - case 3: throw new Error("bar") - case 4: throw new Error("baz") + case 2: throw "foo" + case 3: throw "bar" + case 4: throw "baz" default: return null } }) - o(() => redraw.sync()).throws("foo") - o(() => redraw.sync()).throws("bar") - o(() => redraw.sync()).throws("baz") + redraw.sync() + redraw.sync() + redraw.sync() o(counter).equals(4) - o(console.error.calls.map((c) => c.args[0])).deepEquals([]) + o(console.error.calls.map((c) => c.args[0])).deepEquals(["foo", "bar", "baz"]) o(G.rafMock.queueLength()).equals(0) }) diff --git a/tests/core/oncreate.js b/tests/core/oncreate.js index 4d109f0f7..b26af3ac8 100644 --- a/tests/core/oncreate.js +++ b/tests/core/oncreate.js @@ -159,10 +159,11 @@ o.spec("layout create", function() { var updated = m("a", m.remove(createA)) m.render(G.root, m.key(1, vnode)) + var dom = vnode.d m.render(G.root, m.key(1, updated)) o(createDiv.callCount).equals(1) - o(createDiv.args[0]).equals(vnode.d) + o(createDiv.args[0]).equals(dom) o(createA.callCount).equals(0) }) o("works when creating other children", function() { diff --git a/tests/core/render.js b/tests/core/render.js index bb673fb86..b51018b6b 100644 --- a/tests/core/render.js +++ b/tests/core/render.js @@ -54,34 +54,35 @@ o.spec("render", function() { }) o("tries to re-initialize a component that threw on create", function() { - var A = o.spy(() => { throw new Error("error") }) - var throwCount = 0 + var A = o.spy(() => { throw "error" }) + console.error = o.spy() - try {m.render(G.root, m(A))} catch (e) {throwCount++} + m.render(G.root, m(A)) - o(throwCount).equals(1) o(A.callCount).equals(1) - try {m.render(G.root, m(A))} catch (e) {throwCount++} + m.render(G.root, m(A)) - o(throwCount).equals(2) o(A.callCount).equals(2) + + o(console.error.calls.map((c) => c.args[0])).deepEquals(["error", "error"]) }) o("tries to re-initialize a stateful component whose view threw on create", function() { var A = o.spy(() => view) - var view = o.spy(() => { throw new Error("error") }) - var throwCount = 0 - try {m.render(G.root, m(A))} catch (e) {throwCount++} + var view = o.spy(() => { throw "error" }) + console.error = o.spy() + + m.render(G.root, m(A)) - o(throwCount).equals(1) o(A.callCount).equals(1) o(view.callCount).equals(1) - try {m.render(G.root, m(A))} catch (e) {throwCount++} + m.render(G.root, m(A)) - o(throwCount).equals(2) - o(A.callCount).equals(2) + o(A.callCount).equals(1) o(view.callCount).equals(2) + + o(console.error.calls.map((c) => c.args[0])).deepEquals(["error", "error"]) }) o("lifecycle methods work in keyed children of recycled keyed", function() { var removeA = o.spy() From 4137f9769ce077c082995f94405a094efe4a8d7a Mon Sep 17 00:00:00 2001 From: Claudia Meadows Date: Sat, 12 Oct 2024 00:48:34 -0700 Subject: [PATCH 58/95] Fix stray outdated benchmark reference --- performance/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/performance/index.html b/performance/index.html index ed1db1a13..a685e32eb 100644 --- a/performance/index.html +++ b/performance/index.html @@ -3,7 +3,7 @@ Performance tests - + Open the browser console. From 313d0040756f8c5f969026b443d7222fc9dd6481 Mon Sep 17 00:00:00 2001 From: Claudia Meadows Date: Sat, 12 Oct 2024 00:49:17 -0700 Subject: [PATCH 59/95] Add ops per sec counter --- performance/bench.js | 31 +++++++++++++++++++------------ 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/performance/bench.js b/performance/bench.js index 335b1764d..3b731f381 100644 --- a/performance/bench.js +++ b/performance/bench.js @@ -158,27 +158,34 @@ export async function runBenchmarks(tests) { await test(state) const {mean, marginOfError, ticks} = state.stats() - let min = mean - marginOfError - let max = mean + marginOfError + const min = mean - marginOfError + const max = mean + marginOfError + + const maxOps = Math.floor(1000 / min).toLocaleString(undefined, {useGrouping: true}) + const minOps = Math.floor(1000 / max).toLocaleString(undefined, {useGrouping: true}) + + let minDisplay = min + let maxDisplay = max let unit = "ms" - if (max < 1) { - min *= 1000 - max *= 1000 + if (maxDisplay < 1) { + minDisplay *= 1000 + maxDisplay *= 1000 unit = "µs" - if (max < 1) { - min *= 1000 - max *= 1000 + if (maxDisplay < 1) { + minDisplay *= 1000 + maxDisplay *= 1000 unit = "ns" } } - min = min.toPrecision(3) - max = max.toPrecision(3) + minDisplay = minDisplay.toPrecision(3) + maxDisplay = maxDisplay.toPrecision(3) - const span = min === max ? min : `${min}-${max}` + const timeSpan = minDisplay === maxDisplay ? minDisplay : `${minDisplay}-${maxDisplay}` + const opsSpan = minOps === maxOps ? minOps : `${minOps}-${maxOps}` - console.log(`${name}: ${span} ${unit}/op, n = ${ticks}`) + console.log(`${name}: ${timeSpan} ${unit}/op, ${opsSpan} op/s, n = ${ticks}`) } const end = performance.now() From 726e79fc7659ec493f6ad118c5790e694867e7ec Mon Sep 17 00:00:00 2001 From: Claudia Meadows Date: Sat, 12 Oct 2024 01:35:20 -0700 Subject: [PATCH 60/95] Optimize error handling `finally` is apparently really slow. This improved benchmarks by about 3-4x, bringing them back to roughly what they were when I first cut the branch. --- src/core.js | 153 +++++++++++++++++++++++++++------------------------- 1 file changed, 81 insertions(+), 72 deletions(-) diff --git a/src/core.js b/src/core.js index ca2e1d153..f272c48cb 100644 --- a/src/core.js +++ b/src/core.js @@ -389,13 +389,13 @@ var updateFragment = (old, vnode) => { try { for (var i = 0; i < commonLength; i++) updateNode(old.c[i], vnode.c[i]) for (var i = commonLength; i < newLength; i++) updateNode(null, vnode.c[i]) - } finally { - if (i < newLength) { - commonLength = i - for (var i = 0; i < commonLength; i++) updateNode(vnode.c[i], null) - } + } catch (e) { + commonLength = i + for (var i = 0; i < commonLength; i++) updateNode(vnode.c[i], null) for (var i = commonLength; i < oldLength; i++) updateNode(old.c[i], null) + throw e } + for (var i = commonLength; i < oldLength; i++) updateNode(old.c[i], null) } else { // Keyed. I take a pretty straightforward approach here to keep it simple: // 1. Build a map from old map to old vnode. @@ -420,21 +420,18 @@ var updateFragment = (old, vnode) => { } else { oldMap.delete(n.t) var prev = currentRefNode - try { - moveToPosition(p) - } finally { - currentRefNode = prev - } + moveToPosition(p) + currentRefNode = prev updateFragment(p, n) } } - } finally { - if (i < newLength) { - for (var j = 0; j < i; j++) updateNode(vnode.c[j], null) - updateNode(old.c[j], null) - } + } catch (e) { + for (var j = 0; j < i; j++) updateNode(vnode.c[j], null) + updateNode(old.c[j], null) oldMap.forEach((p) => updateNode(p, null)) + throw e } + oldMap.forEach((p) => updateNode(p, null)) } } @@ -530,12 +527,9 @@ var updateSet = (old, vnode) => { else if ("set" in descs[key]) descs[key].set = undefined } var prevContext = currentContext - try { - currentContext = Object.freeze(Object.create(prevContext, descs)) - updateFragment(old, vnode) - } finally { - currentContext = prevContext - } + currentContext = Object.freeze(Object.create(prevContext, descs)) + updateFragment(old, vnode) + currentContext = prevContext } var updateText = (old, vnode) => { @@ -547,12 +541,21 @@ var updateText = (old, vnode) => { } } +var handleAttributeError = (old, e, force) => { + if (currentRemoveOnThrow || force) { + removeNode(old) + updateFragment(old, null) + throw e + } + console.error(e) +} + var updateElement = (old, vnode) => { var prevParent = currentParent var prevNamespace = currentNamespace var mask = vnode.m var attrs = vnode.a - var element, eventDict, oldAttrs + var element , oldAttrs if (old == null) { var entry = selectorCache.get(vnode.t) @@ -593,7 +596,7 @@ var updateElement = (old, vnode) => { currentParent = element currentNamespace = ns } else { - eventDict = vnode.s = old.s + vnode.s = old.s oldAttrs = old.a currentNamespace = (currentParent = element = vnode.d = old.d).namespaceURI if (currentNamespace === htmlNs) currentNamespace = null @@ -615,18 +618,22 @@ var updateElement = (old, vnode) => { } for (var key in attrs) { - eventDict = setAttr(eventDict, element, mask, key, oldAttrs, attrs) + setAttr(vnode, element, mask, key, oldAttrs, attrs) } } for (var key in oldAttrs) { mask |= FLAG_IS_REMOVE - eventDict = setAttr(eventDict, element, mask, key, oldAttrs, attrs) + setAttr(vnode, element, mask, key, oldAttrs, attrs) } + } catch (e) { + return handleAttributeError(old, e, true) + } - updateFragment(old, vnode) + updateFragment(old, vnode) - if (mask & FLAG_SELECT_ELEMENT && old == null) { + if (mask & FLAG_SELECT_ELEMENT && old == null) { + try { // This does exactly what I want, so I'm reusing it to save some code var normalized = getStyleKey(attrs, "value") if ("value" in attrs) { @@ -640,17 +647,24 @@ var updateElement = (old, vnode) => { } } } + } catch (e) { + handleAttributeError(old, e, false) + } - if ("selectedIndex" in attrs) { - element.selectedIndex = attrs.selectedIndex + try { + // This does exactly what I want, so I'm reusing it to save some code + var normalized = getPropKey(attrs, "selectedIndex") + if (normalized !== null) { + element.selectedIndex = normalized } + } catch (e) { + handleAttributeError(old, e, false) } - } finally { - vnode.s = eventDict - currentParent = prevParent - currentRefNode = element - currentNamespace = prevNamespace } + + currentParent = prevParent + currentRefNode = element + currentNamespace = prevNamespace } var updateComponent = (old, vnode) => { @@ -681,6 +695,16 @@ var updateComponent = (old, vnode) => { var removeFragment = (old) => updateFragment(old, null) +var removeNode = (old) => { + try { + if (!old.d) return + old.d.remove() + old.d = null + } catch (e) { + console.error(e) + } +} + // Replaces an otherwise necessary `switch`. var updateNodeDispatch = [ updateFragment, @@ -696,19 +720,10 @@ var updateNodeDispatch = [ var removeNodeDispatch = [ removeFragment, removeFragment, + removeNode, (old) => { - if (!old.d) return - old.d.remove() - old.d = null - }, - (old) => { - try { - if (!old.d) return - old.d.remove() - old.d = null - } finally { - updateFragment(old, null) - } + removeNode(old) + updateFragment(old, null) }, (old) => updateNode(old.c, null), () => {}, @@ -804,9 +819,6 @@ Some of the optimizations it does: attribute name is matchable. - For small attribute names (4 characters or less), the code handles them in full, with no full string comparison. -- The events object is read prior to calling this, while the rest of the vnode is already in the - CPU cache, and it's just passed as an argument. This ensures it's always in easy access, only - a few cycles of latency away, without becoming too costly for vnodes without events. - I fuse all the conditions, `hasOwn` and existence checks, and all the add/remove logic into just this, to reduce startup overhead and keep outer loop code size down. - I use a lot of labels to reuse as much code as possible, and thus more ICs, to make optimization @@ -815,12 +827,12 @@ Some of the optimizations it does: actually the real reason why I'm using bit flags for stuff like `` in the first place - it moves the check to just the create flow where it's only done once. */ -var setAttr = (eventDict, element, mask, key, old, attrs) => { +var setAttr = (vnode, element, mask, key, old, attrs) => { try { var newValue = getPropKey(attrs, key) var oldValue = getPropKey(old, key) - if (mask & FLAG_IS_REMOVE && newValue !== null) return eventDict + if (mask & FLAG_IS_REMOVE && newValue !== null) return forceSetAttribute: { forceTryProperty: { @@ -829,23 +841,23 @@ var setAttr = (eventDict, element, mask, key, old, attrs) => { var pair1 = key.charCodeAt(0) | key.charCodeAt(1) << 16 if (key.length === 2 && pair1 === (ASCII_LOWER_I | ASCII_LOWER_S << 16)) { - return eventDict + return } else if (pair1 === (ASCII_LOWER_O | ASCII_LOWER_N << 16)) { - if (newValue === oldValue) return eventDict + if (newValue === oldValue) return // Update the event if (typeof newValue === "function") { if (typeof oldValue !== "function") { - if (eventDict == null) eventDict = new EventDict() - element.addEventListener(key.slice(2), eventDict, false) + if (vnode.s == null) vnode.s = new EventDict() + element.addEventListener(key.slice(2), vnode.s, false) } // Save this, so the current redraw is correctly tracked. - eventDict._ = currentRedraw - eventDict.set(key, newValue) + vnode.s._ = currentRedraw + vnode.s.set(key, newValue) } else if (typeof oldValue === "function") { - element.removeEventListener(key.slice(2), eventDict, false) - eventDict.delete(key) + element.removeEventListener(key.slice(2), vnode.s, false) + vnode.s.delete(key) } - return eventDict + return } else if (key.length > 3) { var pair2 = key.charCodeAt(2) | key.charCodeAt(3) << 16 if ( @@ -860,7 +872,7 @@ var setAttr = (eventDict, element, mask, key, old, attrs) => { } else { element.removeAttributeNS(xlinkNs, key) } - return eventDict + return } else if (key.length === 4) { if ( pair1 === (ASCII_LOWER_T | ASCII_LOWER_Y << 16) && @@ -884,7 +896,7 @@ var setAttr = (eventDict, element, mask, key, old, attrs) => { } else if (key.length > 4) { switch (key) { case "children": - return eventDict + return case "class": case "className": @@ -917,13 +929,13 @@ var setAttr = (eventDict, element, mask, key, old, attrs) => { if (element.value === (newValue = `${newValue}`)) { // Setting `` to the same value causes an // error to be generated if it's non-empty - if (mask & FLAG_IS_FILE_INPUT) return eventDict + if (mask & FLAG_IS_FILE_INPUT) return // Setting `` to the same value by typing on focused // element moves cursor to end in Chrome if (mask & (FLAG_INPUT_ELEMENT | FLAG_TEXTAREA_ELEMENT)) { - if (element === currentDocument.activeElement) return eventDict + if (element === currentDocument.activeElement) return } else { - if (oldValue != null && oldValue !== false) return eventDict + if (oldValue != null && oldValue !== false) return } } @@ -932,7 +944,7 @@ var setAttr = (eventDict, element, mask, key, old, attrs) => { // Not ideal, but it at least works around the most common source of uncaught exceptions for now. if (newValue !== "") { console.error("File input `value` attributes must either mirror the current value or be set to the empty string (to reset).") - return eventDict + return } } @@ -959,7 +971,7 @@ var setAttr = (eventDict, element, mask, key, old, attrs) => { // Remove style properties that no longer exist setStyle(element.style, newValue, oldValue, false) } - return eventDict + return case "selected": var active = currentDocument.activeElement @@ -1001,7 +1013,7 @@ var setAttr = (eventDict, element, mask, key, old, attrs) => { // Defer the property check until *after* we check everything. if (key in element) { element[key] = newValue - return eventDict + return } } @@ -1011,11 +1023,8 @@ var setAttr = (eventDict, element, mask, key, old, attrs) => { element.setAttribute(key, newValue === true ? "" : newValue) } } catch (e) { - if (currentRemoveOnThrow) throw e - console.error(e) + handleAttributeError(old, e, false) } - - return eventDict } // Here's an explanation of how this works: From 2a60973de9fdb82472c340420b675cc7d21ac076 Mon Sep 17 00:00:00 2001 From: Claudia Meadows Date: Sat, 12 Oct 2024 02:07:28 -0700 Subject: [PATCH 61/95] Ensure `is` is always set in custom elements This matches HTML more closely, and makes for a much better experience with custom elements. --- src/core.js | 7 +++++-- tests/core/attributes.js | 20 +++++++++++++++++--- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/src/core.js b/src/core.js index f272c48cb..f0669d258 100644 --- a/src/core.js +++ b/src/core.js @@ -560,7 +560,8 @@ var updateElement = (old, vnode) => { if (old == null) { var entry = selectorCache.get(vnode.t) var tag = entry ? entry.t : vnode.t - var is = attrs && attrs.is + var customTag = tag.includes("-") + var is = !customTag && attrs && attrs.is var ns = attrs && attrs.xmlns || nameSpace[tag] || prevNamespace var opts = is ? {is} : null @@ -580,7 +581,7 @@ var updateElement = (old, vnode) => { // right code. /* eslint-disable indent */ vnode.m = mask |= ( - is || tag.includes("-") + is || customTag ? FLAG_HTML_ELEMENT | FLAG_CUSTOM_ELEMENT : (tag = tag.toUpperCase(), ( tag === "INPUT" ? FLAG_HTML_ELEMENT | FLAG_INPUT_ELEMENT @@ -591,6 +592,8 @@ var updateElement = (old, vnode) => { )) ) /* eslint-enable indent */ + + if (is) element.setAttribute("is", is) } currentParent = element diff --git a/tests/core/attributes.js b/tests/core/attributes.js index d3d252b07..5c1c6bf43 100644 --- a/tests/core/attributes.js +++ b/tests/core/attributes.js @@ -74,11 +74,14 @@ o.spec("attributes", function() { o(spies[0].callCount).equals(0) o(spies[2].callCount).equals(0) o(spies[3].calls).deepEquals([{this: spies[3].elem, args: ["custom", "x"]}]) - o(spies[4].calls).deepEquals([{this: spies[4].elem, args: ["custom", "x"]}]) + o(spies[4].calls).deepEquals([ + {this: spies[4].elem, args: ["is", "something-special"]}, + {this: spies[4].elem, args: ["custom", "x"]}, + ]) o(spies[5].calls).deepEquals([{this: spies[5].elem, args: ["custom", "x"]}]) }) - o("when vnode is customElement with property, custom setAttribute not called", function(){ + o("when vnode is customElement with property, custom setAttribute only called for `is`", function(){ var f = G.window.document.createElement var spies = [] var getters = [] @@ -118,7 +121,8 @@ o.spec("attributes", function() { o(spies[1].callCount).equals(0) o(spies[2].callCount).equals(0) o(spies[3].callCount).equals(0) - o(spies[4].callCount).equals(0) + o(spies[4].callCount).equals(1) + o(spies[4].args[0]).equals("is") o(spies[5].callCount).equals(0) o(getters[0].callCount).equals(0) o(getters[1].callCount).equals(0) @@ -128,6 +132,16 @@ o.spec("attributes", function() { o(setters[2].calls).deepEquals([{this: spies[5].elem, args: ["x"]}]) }) + o("`is` attribute is not removed when the attribute is removed from hyperscript", function(){ + var vnode = m("input") + + m.render(G.root, m("input", {is: "something-special"})) + m.render(G.root, vnode) + + o(G.root.firstChild).equals(vnode.d) + o(G.root.firstChild.attributes["is"].value).equals("something-special") + }) + }) o.spec("input readonly", function() { o("when input readonly is true, attribute is present", function() { From f48becac591f4cc4cc7f1f31065f54330a10a8ef Mon Sep 17 00:00:00 2001 From: Claudia Meadows Date: Sat, 12 Oct 2024 02:16:14 -0700 Subject: [PATCH 62/95] Normalize the render vnode input to a single unitary vnode, not an array It's more predictable that way, and I expect this to break approximately nobody. (If anything, it's likely to *unbreak* some users, as they may have been assuming this to have been the case from the beginning.) --- src/core.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/core.js b/src/core.js index f0669d258..6605c7bb3 100644 --- a/src/core.js +++ b/src/core.js @@ -1055,7 +1055,7 @@ class EventDict extends Map { var currentlyRendering = [] -m.render = (dom, vnodes, {redraw, removeOnThrow} = {}) => { +m.render = (dom, vnode, {redraw, removeOnThrow} = {}) => { if (!dom) throw new TypeError("DOM element being rendered to does not exist.") if (currentlyRendering.some((d) => d === dom || d.contains(dom))) { throw new TypeError("Node is currently being rendered to and thus is locked.") @@ -1090,9 +1090,8 @@ m.render = (dom, vnodes, {redraw, removeOnThrow} = {}) => { // First time rendering into a node clears it out if (dom.vnodes == null) dom.textContent = "" - vnodes = m.normalize(Array.isArray(vnodes) ? vnodes.slice() : [vnodes]) - updateNode(dom.vnodes, vnodes) - dom.vnodes = vnodes + updateNode(dom.vnodes, vnode = m.normalize(vnode)) + dom.vnodes = vnode // `document.activeElement` can return null: https://html.spec.whatwg.org/multipage/interaction.html#dom-document-activeelement if (active != null && currentDocument.activeElement !== active && typeof active.focus === "function") { active.focus() From b889448a54eb6f59a52092c769c83de5d2d5795e Mon Sep 17 00:00:00 2001 From: Claudia Meadows Date: Sat, 12 Oct 2024 02:26:02 -0700 Subject: [PATCH 63/95] Clean up the router on remove, remove unneeded hashchange event --- src/std/router.js | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/src/std/router.js b/src/std/router.js index 24138cbc0..4c9e0d7b9 100644 --- a/src/std/router.js +++ b/src/std/router.js @@ -50,7 +50,6 @@ export var WithRouter = ({prefix, initial: href}) => { } href = window.location.href window.addEventListener("popstate", updateRoute, false) - window.addEventListener("hashchange", updateRoute, false) } updateRouteWithHref() @@ -58,15 +57,18 @@ export var WithRouter = ({prefix, initial: href}) => { return ({children}, _, context) => { redraw = context.redraw - return m.set({ - route: { - prefix, - path: currentPath, - params: currentUrl.searchParams, - current: currentPath + currentUrl.search + currentUrl.hash, - set, - }, - }, children) + return [ + m.remove(() => window.removeEventListener("popstate", updateRoute, false)), + m.set({ + route: { + prefix, + path: currentPath, + params: currentUrl.searchParams, + current: currentPath + currentUrl.search + currentUrl.hash, + set, + }, + }, children), + ] } } From 3b7788bbe117af958e2d907ec5feb96953082143 Mon Sep 17 00:00:00 2001 From: Claudia Meadows Date: Sat, 12 Oct 2024 02:29:04 -0700 Subject: [PATCH 64/95] Remove the no-longer-necessary options parameter to `{add,remove}EventListener` --- src/core.js | 4 ++-- src/std/router.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/core.js b/src/core.js index 6605c7bb3..0409baa98 100644 --- a/src/core.js +++ b/src/core.js @@ -851,13 +851,13 @@ var setAttr = (vnode, element, mask, key, old, attrs) => { if (typeof newValue === "function") { if (typeof oldValue !== "function") { if (vnode.s == null) vnode.s = new EventDict() - element.addEventListener(key.slice(2), vnode.s, false) + element.addEventListener(key.slice(2), vnode.s) } // Save this, so the current redraw is correctly tracked. vnode.s._ = currentRedraw vnode.s.set(key, newValue) } else if (typeof oldValue === "function") { - element.removeEventListener(key.slice(2), vnode.s, false) + element.removeEventListener(key.slice(2), vnode.s) vnode.s.delete(key) } return diff --git a/src/std/router.js b/src/std/router.js index 4c9e0d7b9..6f02108e4 100644 --- a/src/std/router.js +++ b/src/std/router.js @@ -49,7 +49,7 @@ export var WithRouter = ({prefix, initial: href}) => { throw new TypeError("Outside the DOM, `href` must be set") } href = window.location.href - window.addEventListener("popstate", updateRoute, false) + window.addEventListener("popstate", updateRoute) } updateRouteWithHref() @@ -58,7 +58,7 @@ export var WithRouter = ({prefix, initial: href}) => { redraw = context.redraw return [ - m.remove(() => window.removeEventListener("popstate", updateRoute, false)), + m.remove(() => window.removeEventListener("popstate", updateRoute)), m.set({ route: { prefix, From 95897a32b1d77759ce3033a31304cf76686a9c42 Mon Sep 17 00:00:00 2001 From: Claudia Meadows Date: Sat, 12 Oct 2024 02:37:45 -0700 Subject: [PATCH 65/95] Diff final URL before redrawing, to prevent unnecessary redraws --- src/std/router.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/std/router.js b/src/std/router.js index 6f02108e4..3605fa619 100644 --- a/src/std/router.js +++ b/src/std/router.js @@ -27,8 +27,9 @@ export var WithRouter = ({prefix, initial: href}) => { var updateRoute = () => { if (href === window.location.href) return href = window.location.href - redraw() + var prevUrl = currentUrl updateRouteWithHref() + if (currentUrl.href !== prevUrl.href) redraw() } var set = (path, {replace, state} = {}) => { From 966d1c02338a57a807558eceb667ffa9c13f6159 Mon Sep 17 00:00:00 2001 From: Claudia Meadows Date: Sat, 12 Oct 2024 13:45:32 -0700 Subject: [PATCH 66/95] Allow building query strings independently of paths (again) --- src/entry/mithril.esm.js | 3 +- src/std/{p.js => path-query.js} | 6 ++- tests/std/p.js | 78 ++++++++++++++-------------- tests/std/q.js | 91 +++++++++++++++++++++++++++++++++ 4 files changed, 136 insertions(+), 42 deletions(-) rename src/std/{p.js => path-query.js} (94%) create mode 100644 tests/std/q.js diff --git a/src/entry/mithril.esm.js b/src/entry/mithril.esm.js index e89e5ccad..4ae4d2d1b 100644 --- a/src/entry/mithril.esm.js +++ b/src/entry/mithril.esm.js @@ -2,9 +2,9 @@ import m from "../core.js" import {Link, WithRouter} from "../std/router.js" import {debouncer, throttler} from "../std/rate-limit.js" +import {p, q} from "../std/path-query.js" import init from "../std/init.js" import lazy from "../std/lazy.js" -import p from "../std/p.js" import tracked from "../std/tracked.js" import use from "../std/use.js" import withProgress from "../std/with-progress.js" @@ -12,6 +12,7 @@ import withProgress from "../std/with-progress.js" m.WithRouter = WithRouter m.Link = Link m.p = p +m.q = q m.withProgress = withProgress m.lazy = lazy m.init = init diff --git a/src/std/p.js b/src/std/path-query.js similarity index 94% rename from src/std/p.js rename to src/std/path-query.js index 16c15d306..4e559cf23 100644 --- a/src/std/p.js +++ b/src/std/path-query.js @@ -12,6 +12,8 @@ var serializeQueryValue = (key, value) => { } } +var q = (params) => Object.entries(params).map(([k, v]) => serializeQueryValue(k, v)).join("&") + var invalidTemplateChars = /:([^\/\.-]+)(\.{3})?:/ // Returns `path` from `template` + `params` @@ -44,11 +46,11 @@ var p = (template, params) => { if (queryIndex >= 0) result += template.slice(queryIndex, queryEnd) if (newQueryIndex >= 0) result += (queryIndex < 0 ? "?" : "&") + resolved.slice(newQueryIndex, newQueryEnd) - var querystring = Object.entries(query).map(([k, v]) => serializeQueryValue(k, v)).join("&") + var querystring = q(query) if (querystring) result += (queryIndex < 0 && newQueryIndex < 0 ? "?" : "&") + querystring if (hashIndex >= 0) result += template.slice(hashIndex) if (newHashIndex >= 0) result += (hashIndex < 0 ? "" : "&") + resolved.slice(newHashIndex) return result } -export {p as default} +export {p, q} diff --git a/tests/std/p.js b/tests/std/p.js index dc44496bb..78045d0b3 100644 --- a/tests/std/p.js +++ b/tests/std/p.js @@ -1,91 +1,91 @@ import o from "ospec" -import p from "../../src/std/p.js" +import m from "../../src/entry/mithril.esm.js" o.spec("p", () => { function test(prefix) { o("returns path if no params", () => { - var string = p(prefix + "/route/foo", undefined) + var string = m.p(prefix + "/route/foo", undefined) o(string).equals(`${prefix}/route/foo`) }) o("skips interpolation if no params", () => { - var string = p(prefix + "/route/:id", undefined) + var string = m.p(prefix + "/route/:id", undefined) o(string).equals(`${prefix}/route/:id`) }) o("appends query strings", () => { - var string = p(prefix + "/route/foo", {a: "b", c: 1}) + var string = m.p(prefix + "/route/foo", {a: "b", c: 1}) o(string).equals(`${prefix}/route/foo?a=b&c=1`) }) o("inserts template parameters at end", () => { - var string = p(prefix + "/route/:id", {id: "1"}) + var string = m.p(prefix + "/route/:id", {id: "1"}) o(string).equals(`${prefix}/route/1`) }) o("inserts template parameters at beginning", () => { - var string = p(prefix + "/:id/foo", {id: "1"}) + var string = m.p(prefix + "/:id/foo", {id: "1"}) o(string).equals(`${prefix}/1/foo`) }) o("inserts template parameters at middle", () => { - var string = p(prefix + "/route/:id/foo", {id: "1"}) + var string = m.p(prefix + "/route/:id/foo", {id: "1"}) o(string).equals(`${prefix}/route/1/foo`) }) o("inserts variadic paths", () => { - var string = p(prefix + "/route/:foo...", {foo: "id/1"}) + var string = m.p(prefix + "/route/:foo...", {foo: "id/1"}) o(string).equals(`${prefix}/route/id/1`) }) o("inserts variadic paths with initial slashes", () => { - var string = p(prefix + "/route/:foo...", {foo: "/id/1"}) + var string = m.p(prefix + "/route/:foo...", {foo: "/id/1"}) o(string).equals(`${prefix}/route//id/1`) }) o("skips template parameters at end if param missing", () => { - var string = p(prefix + "/route/:id", {param: 1}) + var string = m.p(prefix + "/route/:id", {param: 1}) o(string).equals(`${prefix}/route/:id?param=1`) }) o("skips template parameters at beginning if param missing", () => { - var string = p(prefix + "/:id/foo", {param: 1}) + var string = m.p(prefix + "/:id/foo", {param: 1}) o(string).equals(`${prefix}/:id/foo?param=1`) }) o("skips template parameters at middle if param missing", () => { - var string = p(prefix + "/route/:id/foo", {param: 1}) + var string = m.p(prefix + "/route/:id/foo", {param: 1}) o(string).equals(`${prefix}/route/:id/foo?param=1`) }) o("skips variadic template parameters if param missing", () => { - var string = p(prefix + "/route/:foo...", {param: "/id/1"}) + var string = m.p(prefix + "/route/:foo...", {param: "/id/1"}) o(string).equals(`${prefix}/route/:foo...?param=%2Fid%2F1`) }) o("handles escaped values", () => { - var data = p(prefix + "/route/:foo", {"foo": ";:@&=+$,/?%#"}) + var data = m.p(prefix + "/route/:foo", {"foo": ";:@&=+$,/?%#"}) o(data).equals(`${prefix}/route/%3B%3A%40%26%3D%2B%24%2C%2F%3F%25%23`) }) o("handles unicode", () => { - var data = p(prefix + "/route/:ö", {"ö": "ö"}) + var data = m.p(prefix + "/route/:ö", {"ö": "ö"}) o(data).equals(`${prefix}/route/%C3%B6`) }) o("handles zero", () => { - var string = p(prefix + "/route/:a", {a: 0}) + var string = m.p(prefix + "/route/:a", {a: 0}) o(string).equals(`${prefix}/route/0`) }) o("handles false", () => { - var string = p(prefix + "/route/:a", {a: false}) + var string = m.p(prefix + "/route/:a", {a: false}) o(string).equals(`${prefix}/route/false`) }) o("handles dashes", () => { - var string = p(prefix + "/:lang-:region/route", { + var string = m.p(prefix + "/:lang-:region/route", { lang: "en", region: "US" }) @@ -93,7 +93,7 @@ o.spec("p", () => { o(string).equals(`${prefix}/en-US/route`) }) o("handles dots", () => { - var string = p(prefix + "/:file.:ext/view", { + var string = m.p(prefix + "/:file.:ext/view", { file: "image", ext: "png" }) @@ -101,102 +101,102 @@ o.spec("p", () => { o(string).equals(`${prefix}/image.png/view`) }) o("merges query strings", () => { - var string = p(prefix + "/item?a=1&b=2", {c: 3}) + var string = m.p(prefix + "/item?a=1&b=2", {c: 3}) o(string).equals(`${prefix}/item?a=1&b=2&c=3`) }) o("merges query strings with other parameters", () => { - var string = p(prefix + "/item/:id?a=1&b=2", {id: "foo", c: 3}) + var string = m.p(prefix + "/item/:id?a=1&b=2", {id: "foo", c: 3}) o(string).equals(`${prefix}/item/foo?a=1&b=2&c=3`) }) o("consumes template parameters without modifying query string", () => { - var string = p(prefix + "/item/:id?a=1&b=2", {id: "foo"}) + var string = m.p(prefix + "/item/:id?a=1&b=2", {id: "foo"}) o(string).equals(`${prefix}/item/foo?a=1&b=2`) }) o("handles flat object in query string", () => { - var string = p(prefix, {a: "b", c: 1}) + var string = m.p(prefix, {a: "b", c: 1}) o(string).equals(`${prefix}?a=b&c=1`) }) o("handles escaped values in query string", () => { - var data = p(prefix, {";:@&=+$,/?%#": ";:@&=+$,/?%#"}) + var data = m.p(prefix, {";:@&=+$,/?%#": ";:@&=+$,/?%#"}) o(data).equals(`${prefix}?%3B%3A%40%26%3D%2B%24%2C%2F%3F%25%23=%3B%3A%40%26%3D%2B%24%2C%2F%3F%25%23`) }) o("handles unicode in query string", () => { - var data = p(prefix, {"ö": "ö"}) + var data = m.p(prefix, {"ö": "ö"}) o(data).equals(`${prefix}?%C3%B6=%C3%B6`) }) o("handles nested object in query string", () => { - var string = p(prefix, {a: {b: 1, c: 2}}) + var string = m.p(prefix, {a: {b: 1, c: 2}}) o(string).equals(`${prefix}?a%5Bb%5D=1&a%5Bc%5D=2`) }) o("handles deep nested object in query string", () => { - var string = p(prefix, {a: {b: {c: 1, d: 2}}}) + var string = m.p(prefix, {a: {b: {c: 1, d: 2}}}) o(string).equals(`${prefix}?a%5Bb%5D%5Bc%5D=1&a%5Bb%5D%5Bd%5D=2`) }) o("handles nested array in query string", () => { - var string = p(prefix, {a: ["x", "y"]}) + var string = m.p(prefix, {a: ["x", "y"]}) o(string).equals(`${prefix}?a%5B%5D=x&a%5B%5D=y`) }) o("handles array w/ dupe values in query string", () => { - var string = p(prefix, {a: ["x", "x"]}) + var string = m.p(prefix, {a: ["x", "x"]}) o(string).equals(`${prefix}?a%5B%5D=x&a%5B%5D=x`) }) o("handles deep nested array in query string", () => { - var string = p(prefix, {a: [["x", "y"]]}) + var string = m.p(prefix, {a: [["x", "y"]]}) o(string).equals(`${prefix}?a%5B%5D%5B%5D=x&a%5B%5D%5B%5D=y`) }) o("handles deep nested array in object in query string", () => { - var string = p(prefix, {a: {b: ["x", "y"]}}) + var string = m.p(prefix, {a: {b: ["x", "y"]}}) o(string).equals(`${prefix}?a%5Bb%5D%5B%5D=x&a%5Bb%5D%5B%5D=y`) }) o("handles deep nested object in array in query string", () => { - var string = p(prefix, {a: [{b: 1, c: 2}]}) + var string = m.p(prefix, {a: [{b: 1, c: 2}]}) o(string).equals(`${prefix}?a%5B%5D%5Bb%5D=1&a%5B%5D%5Bc%5D=2`) }) o("handles date in query string", () => { - var string = p(prefix, {a: new Date(0)}) + var string = m.p(prefix, {a: new Date(0)}) o(string).equals(`${prefix}?a=${encodeURIComponent(new Date(0).toString())}`) }) o("handles zero in query string", () => { - var string = p(prefix, {a: 0}) + var string = m.p(prefix, {a: 0}) o(string).equals(`${prefix}?a=0`) }) o("retains empty string literally", () => { - var string = p(prefix, {a: ""}) + var string = m.p(prefix, {a: ""}) o(string).equals(`${prefix}?a=`) }) o("drops `null` from query string", () => { - var string = p(prefix, {a: null}) + var string = m.p(prefix, {a: null}) o(string).equals(prefix) }) o("drops `undefined` from query string", () => { - var string = p(prefix, {a: undefined}) + var string = m.p(prefix, {a: undefined}) o(string).equals(prefix) }) o("turns `true` into value-less string in query string", () => { - var string = p(prefix, {a: true}) + var string = m.p(prefix, {a: true}) o(string).equals(`${prefix}?a`) }) o("drops `false` from query string", () => { - var string = p(prefix, {a: false}) + var string = m.p(prefix, {a: false}) o(string).equals(prefix) }) diff --git a/tests/std/q.js b/tests/std/q.js new file mode 100644 index 000000000..eb821e338 --- /dev/null +++ b/tests/std/q.js @@ -0,0 +1,91 @@ +import o from "ospec" + +import m from "../../src/entry/mithril.esm.js" + +o.spec("q", () => { + o("handles flat object", () => { + var string = m.q({a: "b", c: 1}) + + o(string).equals("a=b&c=1") + }) + o("handles escaped values", () => { + var data = m.q({";:@&=+$,/?%#": ";:@&=+$,/?%#"}) + + o(data).equals("%3B%3A%40%26%3D%2B%24%2C%2F%3F%25%23=%3B%3A%40%26%3D%2B%24%2C%2F%3F%25%23") + }) + o("handles unicode", () => { + var data = m.q({"ö": "ö"}) + + o(data).equals("%C3%B6=%C3%B6") + }) + o("handles nested object in query string", () => { + var string = m.q({a: {b: 1, c: 2}}) + + o(string).equals("a%5Bb%5D=1&a%5Bc%5D=2") + }) + o("handles deep nested object in query string", () => { + var string = m.q({a: {b: {c: 1, d: 2}}}) + + o(string).equals("a%5Bb%5D%5Bc%5D=1&a%5Bb%5D%5Bd%5D=2") + }) + o("handles nested array in query string", () => { + var string = m.q({a: ["x", "y"]}) + + o(string).equals("a%5B%5D=x&a%5B%5D=y") + }) + o("handles array w/ dupe values in query string", () => { + var string = m.q({a: ["x", "x"]}) + + o(string).equals("a%5B%5D=x&a%5B%5D=x") + }) + o("handles deep nested array in query string", () => { + var string = m.q({a: [["x", "y"]]}) + + o(string).equals("a%5B%5D%5B%5D=x&a%5B%5D%5B%5D=y") + }) + o("handles deep nested array in object in query string", () => { + var string = m.q({a: {b: ["x", "y"]}}) + + o(string).equals("a%5Bb%5D%5B%5D=x&a%5Bb%5D%5B%5D=y") + }) + o("handles deep nested object in array in query string", () => { + var string = m.q({a: [{b: 1, c: 2}]}) + + o(string).equals("a%5B%5D%5Bb%5D=1&a%5B%5D%5Bc%5D=2") + }) + o("handles date in query string", () => { + var string = m.q({a: new Date(0)}) + + o(string).equals(`a=${encodeURIComponent(new Date(0).toString())}`) + }) + o("handles zero in query string", () => { + var string = m.q({a: 0}) + + o(string).equals("a=0") + }) + o("retains empty string literally", () => { + var string = m.q({a: ""}) + + o(string).equals("a=") + }) + o("drops `null` from query string", () => { + var string = m.q({a: null}) + + o(string).equals("") + }) + o("drops `undefined` from query string", () => { + var string = m.q({a: undefined}) + + o(string).equals("") + }) + o("turns `true` into value-less string in query string", () => { + var string = m.q({a: true}) + + o(string).equals("a") + }) + o("drops `false` from query string", () => { + var string = m.q({a: false}) + + o(string).equals("") + }) +}) From 30e5ed124a0bc314c3c023940120577ca2adb4e4 Mon Sep 17 00:00:00 2001 From: Claudia Meadows Date: Sat, 12 Oct 2024 14:18:48 -0700 Subject: [PATCH 67/95] Restore current size Some point along the way, this got erroneously changed and saved. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5db1ace57..f4bc9bee8 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ ## What is Mithril.js? -A modern client-side JavaScript framework for building Single Page Applications. It's small (8.71 KB gzipped), fast and provides routing and XHR utilities out of the box. +A modern client-side JavaScript framework for building Single Page Applications. It's small (9.05 KB gzipped), fast and provides routing and XHR utilities out of the box. Mithril.js is used by companies like Vimeo and Nike, and open source platforms like Lichess 👍. From 86b3c3153811ed777276f5dc93a5ca1084a1aaed Mon Sep 17 00:00:00 2001 From: Claudia Meadows Date: Sat, 12 Oct 2024 14:27:11 -0700 Subject: [PATCH 68/95] Update the copy a bit, drop official IE support --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f4bc9bee8..694aeda1e 100644 --- a/README.md +++ b/README.md @@ -17,9 +17,9 @@ A modern client-side JavaScript framework for building Single Page Applications. It's small (9.05 KB gzipped), fast and provides routing and XHR utilities out of the box. -Mithril.js is used by companies like Vimeo and Nike, and open source platforms like Lichess 👍. +Mithril.js has been used by companies like Vimeo, Nike, and Amazon, and open source platforms like Lichess. 👍 -Mithril.js supports IE11, Firefox ESR, and the last two versions of Firefox, Edge, Safari, and Chrome. No polyfills required. 👌 +Mithril.js supports Firefox ESR and the last two versions of Firefox, Edge, Safari, and Chrome. No polyfills required. 👌 ## Installation From 2872ca20664de2e7cf1b7e05c87ecb6dd9700323 Mon Sep 17 00:00:00 2001 From: Claudia Meadows Date: Sat, 12 Oct 2024 14:29:16 -0700 Subject: [PATCH 69/95] Stop displaying size in the README It's going to unnecessarily complicate the release process, especially now that artifacts aren't saved to the repo anymore. --- README.md | 2 +- package.json | 2 +- scripts/build.js | 18 +----------------- 3 files changed, 3 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 694aeda1e..194245803 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ ## What is Mithril.js? -A modern client-side JavaScript framework for building Single Page Applications. It's small (9.05 KB gzipped), fast and provides routing and XHR utilities out of the box. +A modern client-side JavaScript framework for web applications big and small. It's small, fast, and highly stable. And it offers utilities for routing, state management, and more right out of the box. Mithril.js has been used by companies like Vimeo, Nike, and Amazon, and open source platforms like Lichess. 👍 diff --git a/package.json b/package.json index 6632889d0..3858f090d 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ } }, "scripts": { - "build": "node scripts/build.js --save", + "build": "node scripts/build.js", "lint": "eslint . --cache", "perf": "node performance/test-perf.js", "pretest": "npm run lint", diff --git a/scripts/build.js b/scripts/build.js index 24092ad64..50c28b80b 100644 --- a/scripts/build.js +++ b/scripts/build.js @@ -58,20 +58,6 @@ async function report(file) { console.log(`${file}.js:`) console.log(` Original: ${format(originalGzipSize)} bytes gzipped (${format(originalSize)} bytes uncompressed)`) console.log(` Minified: ${format(compressedGzipSize)} bytes gzipped (${format(compressedSize)} bytes uncompressed)`) - - return compressedGzipSize -} - -async function saveToReadme(size) { - const readme = await fs.readFile(path.resolve(dirname, "../README.md"), "utf8") - const kb = size / 1000 - - await fs.writeFile(path.resolve(dirname, "../README.md"), - readme.replace( - /()(.+?)()/, - `\$1${kb % 1 ? kb.toFixed(2) : kb} KB\$3` - ) - ) } async function main() { @@ -84,12 +70,10 @@ async function main() { build("stream.esm", "esm"), ]) - const mithrilSize = await report("mithril.umd") + await report("mithril.umd") await report("mithril.esm") await report("stream.umd") await report("stream.esm") - - if (process.argv.includes("--save", 2)) await saveToReadme(mithrilSize) } main() From 9099bcca4f3c8a4a1b30057520c862679dfd2db1 Mon Sep 17 00:00:00 2001 From: Claudia Meadows Date: Sat, 12 Oct 2024 15:56:48 -0700 Subject: [PATCH 70/95] Forgot to port the actual key assignment over in the benchmark --- performance/components/shuffled-keyed-tree.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/performance/components/shuffled-keyed-tree.js b/performance/components/shuffled-keyed-tree.js index ca6b25698..13b3a2cfe 100644 --- a/performance/components/shuffled-keyed-tree.js +++ b/performance/components/shuffled-keyed-tree.js @@ -19,7 +19,7 @@ export const shuffledKeyedTree = () => { shuffle() var vnodes = [] for (const key of keys) { - vnodes.push(m("div.item", {key})) + vnodes.push(m.key(key, m("div.item"))) } return vnodes } From 1fb51040e5a6081ccbc0dd35b38c2181438eb731 Mon Sep 17 00:00:00 2001 From: Claudia Meadows Date: Sat, 19 Oct 2024 22:06:21 -0700 Subject: [PATCH 71/95] Unrestrict a bunch of actually valid syntax I clearly didn't check this bit properly --- .eslintrc.json | 29 ++--------------------------- 1 file changed, 2 insertions(+), 27 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index ec3c29a53..6ac7e9f2a 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -32,23 +32,11 @@ {"selector": "MetaProperty[meta.name='import'][property.name='meta']", "message": "`import.meta` is not supported in ES2018"}, {"selector": "ExportAllDeclaration[exported!=null]", "message": "`export * as foo from ...` is not supported in ES2018"}, {"selector": "CatchClause[param=null]", "message": "Omitted `catch` bindings are not supported in ES2018"}, - {"selector": "ForOfStatement[await=true]", "message": "Async/await is not supported in ES2018"}, - {"selector": "ObjectExpression > SpreadElement", "message": "Object rest/spread is not supported in ES2018"}, - {"selector": "ObjectPattern > SpreadElement", "message": "Object rest/spread is not supported in ES2018"}, - {"selector": "Function[async=true][generator=true]", "message": "Async generators are not supported in ES2018"}, {"selector": "Literal[regex.flags=/s/]", "message": "`/.../s` is not supported in ES2018"}, - {"selector": "Literal[regex.pattern=/\\(<=|\\(|\\\\k<[\\w$]+>/]", "message": "Named capture groups are not supported in ES2018"}, - {"selector": "Literal[regex.flags=/u/][regex.pattern=/\\\\p/i]", "message": "`\\p{...}` in regexps are not supported in ES2018"}, {"selector": "Literal[regex.flags=/v/]", "message": "`/.../v` is not supported in ES2018"}, - { - "selector": "TaggedTemplateExpression TemplateElement[value.raw=/\\\\(?![0'\"\\\\nrvtbf\\n\\r\\u2028\\u2029]|x[0-9a-fA-F]{2}|u[0-9a-fA-F]{4}|u\\{([0-9a-fA-F]{1,5}|10[0-9a-fA-F]{0,4})\\})/]", - "message": "Tagged template strings in ES2018 have the same lexical grammar as non-tagged template strings" - }, {"selector": "MemberExpression[property.name='matchAll']", "message": "`string.matchAll` is not supported in ES2018"}, - {"selector": "MemberExpression[property.name='trimStart']", "message": "`string.trimStart` is not supported in ES2018"}, - {"selector": "MemberExpression[property.name='finally']", "message": "`promise.finally` is not supported in ES2018"} + {"selector": "MemberExpression[property.name='trimStart']", "message": "`string.trimStart` is not supported in ES2018"} ] } } @@ -77,30 +65,17 @@ {"selector": "MetaProperty[meta.name='import'][property.name='meta']", "message": "`import.meta` is not supported in ES2018"}, {"selector": "ExportAllDeclaration[exported!=null]", "message": "`export * as foo from ...` is not supported in ES2018"}, {"selector": "CatchClause[param=null]", "message": "Omitted `catch` bindings are not supported in ES2018"}, - {"selector": "ForOfStatement[await=true]", "message": "Async/await is not supported in ES2018"}, - {"selector": "ObjectExpression > SpreadElement", "message": "Object rest/spread is not supported in ES2018"}, - {"selector": "ObjectPattern > SpreadElement", "message": "Object rest/spread is not supported in ES2018"}, {"selector": "Function[async=true][generator=true]", "message": "Async generators are not supported in ES2018"}, - {"selector": "Literal[regex.flags=/s/]", "message": "`/.../s` is not supported in ES2018"}, - {"selector": "Literal[regex.pattern=/\\(<=|\\(|\\\\k<[\\w$]+>/]", "message": "Named capture groups are not supported in ES2018"}, - {"selector": "Literal[regex.flags=/u/][regex.pattern=/\\\\p/i]", "message": "`\\p{...}` in regexps are not supported in ES2018"}, {"selector": "Literal[regex.flags=/v/]", "message": "`/.../v` is not supported in ES2018"}, - { - "selector": "TaggedTemplateExpression TemplateElement[value.raw=/\\\\(?![0'\"\\\\nrvtbf\\n\\r\\u2028\\u2029]|x[0-9a-fA-F]{2}|u[0-9a-fA-F]{4}|u\\{([0-9a-fA-F]{1,5}|10[0-9a-fA-F]{0,4})\\})/]", - "message": "Tagged template strings in ES2018 have the same lexical grammar as non-tagged template strings" - }, {"selector": "MemberExpression[property.name='matchAll']", "message": "`string.matchAll` is not supported in ES2018"}, {"selector": "MemberExpression[property.name='trimStart']", "message": "`string.trimStart` is not supported in ES2018"}, - {"selector": "MemberExpression[property.name='finally']", "message": "`promise.finally` is not supported in ES2018"}, {"selector": "VariableDeclaration[kind!='var']", "message": "Keep to `var` in `src/` to ensure the module compresses better"} ], "no-restricted-properties": ["error", {"object": "Promise", "property": "allSettled", "message": "`Promise.allSettled` is not supported in ES2018"}, - {"object": "Object", "property": "fromEntries", "message": "`Object.fromEntries` is not supported in ES2018"}, - {"object": "Symbol", "property": "asyncIterator", "message": "Async/await is not supported in ES2018"} + {"object": "Object", "property": "fromEntries", "message": "`Object.fromEntries` is not supported in ES2018"} ], "accessor-pairs": "error", From c5c4b2fd1537c5cc5c40b3a10a1750570d77208a Mon Sep 17 00:00:00 2001 From: Claudia Meadows Date: Sat, 19 Oct 2024 22:08:57 -0700 Subject: [PATCH 72/95] Add `m.match`, optimize `m.p` a ton Did some math and some benchmarking and...it's actually a potential perf bottleneck for apps and so I had to optimize it a ton. Also rolled some new benchmarks for it. --- performance/bench.js | 2 +- performance/test-perf.js | 84 ++++++++++++++++ src/entry/mithril.esm.js | 3 +- src/std/path-query.js | 200 +++++++++++++++++++++++++++++++++------ tests/std/match.js | 130 +++++++++++++++++++++++++ tests/std/p.js | 69 +++++++++----- 6 files changed, 435 insertions(+), 53 deletions(-) create mode 100644 tests/std/match.js diff --git a/performance/bench.js b/performance/bench.js index 3b731f381..53a8db41a 100644 --- a/performance/bench.js +++ b/performance/bench.js @@ -185,7 +185,7 @@ export async function runBenchmarks(tests) { const timeSpan = minDisplay === maxDisplay ? minDisplay : `${minDisplay}-${maxDisplay}` const opsSpan = minOps === maxOps ? minOps : `${minOps}-${maxOps}` - console.log(`${name}: ${timeSpan} ${unit}/op, ${opsSpan} op/s, n = ${ticks}`) + console.log(`${name}: ${timeSpan} ${unit}/op, ${opsSpan} op/s, n = ${ticks.toLocaleString()}`) } const end = performance.now() diff --git a/performance/test-perf.js b/performance/test-perf.js index 445ce921e..7c02c566c 100644 --- a/performance/test-perf.js +++ b/performance/test-perf.js @@ -89,6 +89,90 @@ benchmarks["null test"] = (b) => { } while (!b.done()) } +const {routes, vars, templates} = (() => { +const routes = [] +const vars = [] +const templates = [] + +for (let i = 0; i < 16; i++) { + for (let j = 0; j < 16; j++) { + templates.push(`/foo${i}/:id${i}/bar${j}/:sub${j}`) + routes.push(`/foo${i}/${i}/bar${j}/${j}`) + vars.push({ + [`id${i}`]: `${i}`, + [`sub${j}`]: `${j}`, + }) + } +} + +return { + // Flatten everything, since they're usually flat strings in practice. + routes: JSON.parse(JSON.stringify(routes)).map((path) => ({path, params: new URLSearchParams()})), + templates: JSON.parse(JSON.stringify(templates)), + vars: JSON.parse(JSON.stringify(vars)), +} +})() + + +// This just needs to be sub-millisecond +benchmarks["route match"] = (b) => { + let i = 0 + do { + cycleRoot() + do { + // eslint-disable-next-line no-bitwise + i = (i - 1) & 255 + globalThis.test = m.match(routes[i], templates[i]) + } while (!b.tick()) + } while (!b.done()) +} + +// This needs to be at most a few microseconds, as 300 of these * 3 us/op = 0.9 ms. (And yes, while +// 300 may seem like a lot, I've worked with apps that exceeded 100, and for 60 FPS, you only truly +// have room for about 5ms total for logic.) +benchmarks["route non-match"] = (b) => { + let i = 0 + do { + cycleRoot() + do { + const j = i + // eslint-disable-next-line no-bitwise + i = (i - 1) & 255 + globalThis.test = m.match(routes[i], templates[j]) + } while (!b.tick()) + } while (!b.done()) +} + +// This needs to be at most a few microseconds, as 300 of these * 3 us/op = 0.9 ms. (And yes, while +// 300 may seem like a lot, I've worked with apps that exceeded 100, and for 60 FPS, you only truly +// have room for about 5ms total for logic.) +benchmarks["path generate with vars"] = (b) => { + let i = 0 + do { + cycleRoot() + do { + // eslint-disable-next-line no-bitwise + i = (i - 1) & 255 + globalThis.test = m.p(templates[i], vars[i]) + } while (!b.tick()) + } while (!b.done()) +} + +// This needs to be at most a few microseconds, as 300 of these * 3 us/op = 0.9 ms. (And yes, while +// 300 may seem like a lot, I've worked with apps that exceeded 100, and for 60 FPS, you only truly +// have room for about 5ms total for logic.) +benchmarks["path generate no vars"] = (b) => { + let i = 0 + do { + cycleRoot() + do { + // eslint-disable-next-line no-bitwise + i = (i - 1) & 255 + globalThis.test = m.p(templates[i]) + } while (!b.tick()) + } while (!b.done()) +} + addTree("simpleTree", simpleTree) addTree("nestedTree", nestedTree) addTree("mutateStylesPropertiesTree", mutateStylesPropertiesTree) diff --git a/src/entry/mithril.esm.js b/src/entry/mithril.esm.js index 4ae4d2d1b..bbcbabafe 100644 --- a/src/entry/mithril.esm.js +++ b/src/entry/mithril.esm.js @@ -2,7 +2,7 @@ import m from "../core.js" import {Link, WithRouter} from "../std/router.js" import {debouncer, throttler} from "../std/rate-limit.js" -import {p, q} from "../std/path-query.js" +import {match, p, q} from "../std/path-query.js" import init from "../std/init.js" import lazy from "../std/lazy.js" import tracked from "../std/tracked.js" @@ -13,6 +13,7 @@ m.WithRouter = WithRouter m.Link = Link m.p = p m.q = q +m.match = match m.withProgress = withProgress m.lazy = lazy m.init = init diff --git a/src/std/path-query.js b/src/std/path-query.js index 4e559cf23..010279fe6 100644 --- a/src/std/path-query.js +++ b/src/std/path-query.js @@ -1,56 +1,198 @@ +// Allowed terminators for `m.match`: +// - `.` for `:file.:ext` +// - `-` for `:lang-:locale` +// - `/` for `/:some/:path/` +// - end for `/:some/:path` +// Escape with `\\` +// Use `*rest` for rest + +// Caution: `m.p` and the failure path of `m.match` are both perf-sensitive. It only takes a couple +// hundred `m.p` calls with parameters to amount to about 1ms, and there's only about 10ms total +// that one can reasonably use to render stuff. And it's reasonable to expect several dozen `m.p` +// calls with parameters even in a medium-sized app. +// +// The more complicated one, `m.match`, fortunately is fairly cheap in the common case of mismatch. +// However, `m.p`'s common case is *with variables*, and getting the runtime of that down wasn't +// easy. (`m.p` for context was designed with usage like `m(m.Link, {href: m.p(...)})` in mind.) + +import {hasOwn} from "../util.js" + var toString = {}.toString -var serializeQueryValue = (key, value) => { - if (value == null || value === false) { - return "" - } else if (Array.isArray(value)) { - return value.map((i) => serializeQueryValue(`${key}[]`, i)).join("&") - } else if (toString.call(value) !== "[object Object]") { - return `${encodeURIComponent(key)}${value === true ? "" : `=${encodeURIComponent(value)}`}` +var invalidTemplateChars = /[:*][$_\p{IDS}](?![$\p{IDC}]*(?![:*]))/u +var invalidMatchTemplate = /\/\/|[:*](?![$_\p{IDS}][$\p{IDC}]*(?![:*]))|\*.*?[^$\p{IDC}]|:([$_\p{IDS}][$\p{IDC}]*)[^$\p{IDC}].*?[:*]\1(?![$\p{IDC}])/u +var escapeOrParameter = /\\.|[:*][$_\p{IDS}][$\p{IDC}]*/ug +var escapeOnly = /\\(.)/g +// I escape literal text so people can use things like `:file.:ext` or `:lang-:locale` in routes. +// This is all merged into one pass so I don't also accidentally escape `-` and make it harder to +// detect it to ban it from template parameters. +var matcherCompile = /([:*])([$_\p{IDS}][$\p{IDC}]*)|\\\\|\\?([$^*+.()|[\]{}])|\\(.)/ug + +var serializeQueryValue = (qs, prefix, value) => { + if (value == null || value === false) return + if (Array.isArray(value)) { + for (var i of value) { + serializeQueryValue(qs, `${prefix}[]`, i) + } } else { - return Object.entries(value).map(([k, v]) => serializeQueryValue(`${key}[${k}]`, v)).join("&") + if (typeof value === "object") { + var proto = Object.getPrototypeOf(value) + if (proto == null || proto === Object.prototype || toString.call(value) === "[object Object]") { + for (var k in value) { + if (hasOwn.call(value, k)) { + serializeQueryValue(qs, `${prefix}[${k}]`, value[k]) + } + } + return + } + } + qs.v += qs.s + encodeURIComponent(prefix) + (value === true ? "" : `=${encodeURIComponent(value)}`) + qs.s = "&" } } -var q = (params) => Object.entries(params).map(([k, v]) => serializeQueryValue(k, v)).join("&") +var makeQueryBuilder = (sep, value) => ({s: sep, v: value}) -var invalidTemplateChars = /:([^\/\.-]+)(\.{3})?:/ +var q = (params) => { + var qs = makeQueryBuilder("", "") + for (var key in params) { + if (hasOwn.call(params, key)) serializeQueryValue(qs, key, params[key]) + } + return qs.v +} // Returns `path` from `template` + `params` var p = (template, params) => { if (invalidTemplateChars.test(template)) { throw new SyntaxError("Template parameter names must be separated by either a '/', '-', or '.'.") } - if (params == null) return template + if (params == null) return template.replace(escapeOnly, "$1") var queryIndex = template.indexOf("?") var hashIndex = template.indexOf("#") var queryEnd = hashIndex < 0 ? template.length : hashIndex var pathEnd = queryIndex < 0 ? queryEnd : queryIndex var path = template.slice(0, pathEnd) - var query = Object.assign({}, params) + var inTemplate = new Set() + var resolved = "" + var start = escapeOrParameter.lastIndex = 0 + var exec + + while ((exec = escapeOrParameter.exec(path)) != null) { + var index = exec.index + resolved += path.slice(start, index) + start = escapeOrParameter.lastIndex + if (path[index] === "\\") { + start = index + 1 + } else { + var key = path.slice(index + 1, start) + inTemplate.add(key) + key = params[key] + resolved += ( + key != null + // Escape normal parameters, but not variadic ones. + ? (path[index] === "*" ? key : encodeURIComponent(`${key}`)) + // If no such parameter exists, don't interpolate it. + : path.slice(index, start) + ) + } + } - var resolved = path.replace(/:([^\/\.-]+)(\.{3})?/g, (m, key, variadic) => { - delete query[key] - // If no such parameter exists, don't interpolate it. - if (params[key] == null) return m - // Escape normal parameters, but not variadic ones. - return variadic ? params[key] : encodeURIComponent(String(params[key])) - }) + resolved += path.slice(start) // In case the template substitution adds new query/hash parameters. var newQueryIndex = resolved.indexOf("?") var newHashIndex = resolved.indexOf("#") var newQueryEnd = newHashIndex < 0 ? resolved.length : newHashIndex var newPathEnd = newQueryIndex < 0 ? newQueryEnd : newQueryIndex - var result = resolved.slice(0, newPathEnd) - - if (queryIndex >= 0) result += template.slice(queryIndex, queryEnd) - if (newQueryIndex >= 0) result += (queryIndex < 0 ? "?" : "&") + resolved.slice(newQueryIndex, newQueryEnd) - var querystring = q(query) - if (querystring) result += (queryIndex < 0 && newQueryIndex < 0 ? "?" : "&") + querystring - if (hashIndex >= 0) result += template.slice(hashIndex) - if (newHashIndex >= 0) result += (hashIndex < 0 ? "" : "&") + resolved.slice(newHashIndex) - return result + var qs = makeQueryBuilder("?", resolved.slice(0, newPathEnd)) + + if (queryIndex >= 0) { + qs.v += template.slice(queryIndex, queryEnd) + qs.s = "&" + } + + if (newQueryIndex >= 0) { + qs.v += qs.s + resolved.slice(newQueryIndex, newQueryEnd) + qs.s = "&" + } + + for (var key in params) { + if (hasOwn.call(params, key) && !inTemplate.has(key)) { + serializeQueryValue(qs, key, params[key]) + } + } + + if (hashIndex >= 0) { + qs.v += template.slice(hashIndex) + } else { + qs.s = "" + } + + if (newHashIndex >= 0) { + qs.v += qs.s + resolved.slice(newHashIndex) + } + + return qs.v +} + +/** @typedef {RegExp & {r: number, p: URLSearchParams}} Matcher */ + +/** @type {Map} */ +var cache = new Map() + +/** @param {string} pattern @returns {Matcher} */ +var compile = (pattern) => { + if (invalidMatchTemplate.test(pattern)) { + throw new SyntaxError("Invalid pattern") + } + + var queryIndex = pattern.indexOf("?") + var hashIndex = pattern.indexOf("#") + var index = queryIndex < hashIndex ? queryIndex : hashIndex + var rest + var re = new RegExp(`^${pattern.slice(0, index < 0 ? undefined : index).replace( + matcherCompile, + (_, p, name, esc1, esc2) => { + if (p === "*") { + rest = name + return `(?<${name}>.*)` + } else if (p === ":") { + return `(?<${name}>[^/]+)` + } else { + return esc2 || `\\${esc1 || "\\"}` + } + } + )}$`, "u") + cache.set(pattern, re) + re.r = rest + re.p = new URLSearchParams(index < 0 ? "" : pattern.slice(index, hashIndex < 0 ? undefined : hashIndex)) + return re +} + +/** @param {{path: string, params: URLSearchParams}} route */ +var match = ({path, params}, pattern) => { + var re = cache.get(pattern) + if (!re) { + re = /*@__NOINLINE__*/compile(pattern) + } + + var exec = re.exec(path) + var restIndex = re.r + if (!exec) return + + for (var [k, v] of re.p) { + if (params.get(k) !== v) return + } + + // Taking advantage of guaranteed insertion order and group iteration order here to reduce the + // condition to a simple numeric comparison. + for (var k in exec.groups) { + if (restIndex--) { + exec.groups[k] = decodeURIComponent(exec.groups[k]) + } + } + + return {...exec.groups} } -export {p, q} +export {p, q, match} diff --git a/tests/std/match.js b/tests/std/match.js new file mode 100644 index 000000000..44686379f --- /dev/null +++ b/tests/std/match.js @@ -0,0 +1,130 @@ +import o from "ospec" + +import m from "../../src/entry/mithril.esm.js" + +o.spec("match", () => { + var match = (path, pattern) => { + var url = new URL(path, "http://localhost/") + return m.match({path: url.pathname, params: url.searchParams}, pattern) + } + + o("checks empty string", function() { + o(match("/", "/")).deepEquals({}) + }) + o("checks identical match", function() { + o(match("/foo", "/foo")).deepEquals({}) + }) + o("checks identical mismatch", function() { + o(match("/bar", "/foo")).deepEquals(undefined) + }) + o("checks single parameter", function() { + o(match("/1", "/:id")).deepEquals({id: "1"}) + }) + o("checks single variadic parameter", function() { + o(match("/some/path", "/*id")).deepEquals({id: "some/path"}) + }) + o("checks single parameter with extra match", function() { + o(match("/1/foo", "/:id/foo")).deepEquals({id: "1"}) + }) + o("checks single parameter with extra mismatch", function() { + o(match("/1/bar", "/:id/foo")).deepEquals(undefined) + }) + o("rejects single variadic parameter with extra match", function() { + o(() => match("/some/path/foo", "/*id/foo")).throws(SyntaxError) + }) + o("checks single variadic parameter with extra mismatch", function() { + o(() => match("/some/path/bar", "/*id2/foo")).throws(SyntaxError) + }) + o("checks multiple parameters", function() { + o(match("/1/2", "/:id/:name")).deepEquals({id: "1", name: "2"}) + }) + o("checks incomplete multiple parameters", function() { + o(match("/1", "/:id/:name")).deepEquals(undefined) + }) + o("checks multiple parameters with extra match", function() { + o(match("/1/2/foo", "/:id/:name/foo")).deepEquals({id: "1", name: "2"}) + }) + o("checks multiple parameters with extra mismatch", function() { + o(match("/1/2/bar", "/:id/:name/foo")).deepEquals(undefined) + }) + o("checks multiple parameters, last variadic, with extra match", function() { + o(() => match("/1/some/path/foo", "/:id/*name/foo")).throws(SyntaxError) + }) + o("checks multiple parameters, last variadic, with extra mismatch", function() { + o(() => match("/1/some/path/bar", "/:id/*name2/foo")).throws(SyntaxError) + }) + o("checks multiple separated parameters", function() { + o(match("/1/sep/2", "/:id/sep/:name")).deepEquals({id: "1", name: "2"}) + }) + o("checks incomplete multiple separated parameters", function() { + o(match("/1", "/:id/sep/:name")).deepEquals(undefined) + o(match("/1/sep", "/:id/sep/:name")).deepEquals(undefined) + }) + o("checks multiple separated parameters missing sep", function() { + o(match("/1/2", "/:id/sep/:name")).deepEquals(undefined) + }) + o("checks multiple separated parameters with extra match", function() { + o(match("/1/sep/2/foo", "/:id/sep/:name/foo")).deepEquals({id: "1", name: "2"}) + }) + o("checks multiple separated parameters with extra mismatch", function() { + o(match("/1/sep/2/bar", "/:id/sep/:name/foo")).deepEquals(undefined) + }) + o("checks multiple separated parameters, last variadic, with extra match", function() { + o(() => match("/1/sep/some/path/foo", "/:id/sep/*name/foo")).throws(SyntaxError) + }) + o("checks multiple separated parameters, last variadic, with extra mismatch", function() { + o(() => match("/1/sep/some/path/bar", "/:id/sep/*name2/foo")).throws(SyntaxError) + }) + o("checks multiple parameters + prefix", function() { + o(match("/route/1/2", "/route/:id/:name")).deepEquals({id: "1", name: "2"}) + }) + o("checks incomplete multiple parameters + prefix", function() { + o(match("/route/1", "/route/:id/:name")).deepEquals(undefined) + }) + o("checks multiple parameters + prefix with extra match", function() { + o(match("/route/1/2/foo", "/route/:id/:name/foo")).deepEquals({id: "1", name: "2"}) + }) + o("checks multiple parameters + prefix with extra mismatch", function() { + o(match("/route/1/2/bar", "/route/:id/:name/foo")).deepEquals(undefined) + }) + o("checks multiple parameters + prefix, last variadic, with extra match", function() { + o(() => match("/route/1/some/path/foo", "/route/:id/*name/foo")).throws(SyntaxError) + }) + o("checks multiple parameters + prefix, last variadic, with extra mismatch", function() { + o(() => match("/route/1/some/path/bar", "/route/:id/*name/foo")).throws(SyntaxError) + }) + o("checks multiple separated parameters + prefix", function() { + o(match("/route/1/sep/2", "/route/:id/sep/:name")).deepEquals({id: "1", name: "2"}) + }) + o("checks incomplete multiple separated parameters + prefix", function() { + o(match("/route/1", "/route/:id/sep/:name")).deepEquals(undefined) + o(match("/route/1/sep", "/route/:id/sep/:name")).deepEquals(undefined) + }) + o("checks multiple separated parameters + prefix missing sep", function() { + o(match("/route/1/2", "/route/:id/sep/:name")).deepEquals(undefined) + }) + o("checks multiple separated parameters + prefix with extra match", function() { + o(match("/route/1/sep/2/foo", "/route/:id/sep/:name/foo")).deepEquals({id: "1", name: "2"}) + }) + o("checks multiple separated parameters + prefix with extra mismatch", function() { + o(match("/route/1/sep/2/bar", "/route/:id/sep/:name/foo")).deepEquals(undefined) + }) + o("checks multiple separated parameters + prefix, last variadic, with extra match", function() { + o(() => match("/route/1/sep/some/path/foo", "/route/:id/sep/*name/foo")).throws(SyntaxError) + }) + o("checks multiple separated parameters + prefix, last variadic, with extra mismatch", function() { + o(() => match("/route/1/sep/some/path/bar", "/route/:id/sep/*name2/foo")).throws(SyntaxError) + }) + o("checks dot before dot", function() { + o(match("/file.test.png/edit", "/:file.:ext/edit")).deepEquals({file: "file.test", ext: "png"}) + }) + o("checks dash before dot", function() { + o(match("/file-test.png/edit", "/:file.:ext/edit")).deepEquals({file: "file-test", ext: "png"}) + }) + o("checks dot before dash", function() { + o(match("/file.test-png/edit", "/:file-:ext/edit")).deepEquals({file: "file.test", ext: "png"}) + }) + o("checks dash before dash", function() { + o(match("/file-test-png/edit", "/:file-:ext/edit")).deepEquals({file: "file-test", ext: "png"}) + }) +}) diff --git a/tests/std/p.js b/tests/std/p.js index 78045d0b3..c1b65b7f5 100644 --- a/tests/std/p.js +++ b/tests/std/p.js @@ -5,87 +5,112 @@ import m from "../../src/entry/mithril.esm.js" o.spec("p", () => { function test(prefix) { o("returns path if no params", () => { - var string = m.p(prefix + "/route/foo", undefined) + var string = m.p(`${prefix}/route/foo`, undefined) o(string).equals(`${prefix}/route/foo`) }) o("skips interpolation if no params", () => { - var string = m.p(prefix + "/route/:id", undefined) + var string = m.p(`${prefix}/route/:id`, undefined) o(string).equals(`${prefix}/route/:id`) }) o("appends query strings", () => { - var string = m.p(prefix + "/route/foo", {a: "b", c: 1}) + var string = m.p(`${prefix}/route/foo`, {a: "b", c: 1}) o(string).equals(`${prefix}/route/foo?a=b&c=1`) }) o("inserts template parameters at end", () => { - var string = m.p(prefix + "/route/:id", {id: "1"}) + var string = m.p(`${prefix}/route/:id`, {id: "1"}) o(string).equals(`${prefix}/route/1`) }) o("inserts template parameters at beginning", () => { - var string = m.p(prefix + "/:id/foo", {id: "1"}) + var string = m.p(`${prefix}/:id/foo`, {id: "1"}) o(string).equals(`${prefix}/1/foo`) }) o("inserts template parameters at middle", () => { - var string = m.p(prefix + "/route/:id/foo", {id: "1"}) + var string = m.p(`${prefix}/route/:id/foo`, {id: "1"}) o(string).equals(`${prefix}/route/1/foo`) }) + o("inserts non-special escapes", () => { + var string = m.p(`${prefix}/route/\\a`) + + o(string).equals(`${prefix}/route/a`) + }) + o("inserts normal interpolation escapes without parameters", () => { + var string = m.p(`${prefix}/route/\\:id/foo`) + + o(string).equals(`${prefix}/route/:id/foo`) + }) + o("inserts raw interpolation escapes without parameters", () => { + var string = m.p(`${prefix}/route/\\*foo`) + + o(string).equals(`${prefix}/route/*foo`) + }) + o("inserts normal interpolation escapes", () => { + var string = m.p(`${prefix}/route/\\:id/foo`, {id: "1"}) + + o(string).equals(`${prefix}/route/:id/foo?id=1`) + }) + o("inserts raw interpolation escapes", () => { + var string = m.p(`${prefix}/route/\\*foo`, {foo: "id/1"}) + + o(string).equals(`${prefix}/route/*foo?foo=id%2F1`) + }) o("inserts variadic paths", () => { - var string = m.p(prefix + "/route/:foo...", {foo: "id/1"}) + var string = m.p(`${prefix}/route/*foo`, {foo: "id/1"}) o(string).equals(`${prefix}/route/id/1`) }) o("inserts variadic paths with initial slashes", () => { - var string = m.p(prefix + "/route/:foo...", {foo: "/id/1"}) + var string = m.p(`${prefix}/route/*foo`, {foo: "/id/1"}) o(string).equals(`${prefix}/route//id/1`) }) o("skips template parameters at end if param missing", () => { - var string = m.p(prefix + "/route/:id", {param: 1}) + var string = m.p(`${prefix}/route/:id`, {param: 1}) o(string).equals(`${prefix}/route/:id?param=1`) }) o("skips template parameters at beginning if param missing", () => { - var string = m.p(prefix + "/:id/foo", {param: 1}) + var string = m.p(`${prefix}/:id/foo`, {param: 1}) o(string).equals(`${prefix}/:id/foo?param=1`) }) o("skips template parameters at middle if param missing", () => { - var string = m.p(prefix + "/route/:id/foo", {param: 1}) + var string = m.p(`${prefix}/route/:id/foo`, {param: 1}) o(string).equals(`${prefix}/route/:id/foo?param=1`) }) o("skips variadic template parameters if param missing", () => { - var string = m.p(prefix + "/route/:foo...", {param: "/id/1"}) + var string = m.p(`${prefix}/route/*foo`, {param: "/id/1"}) - o(string).equals(`${prefix}/route/:foo...?param=%2Fid%2F1`) + o(string).equals(`${prefix}/route/*foo?param=%2Fid%2F1`) }) o("handles escaped values", () => { - var data = m.p(prefix + "/route/:foo", {"foo": ";:@&=+$,/?%#"}) + var data = m.p(`${prefix}/route/:foo`, {"foo": ";:@&=+$,/?%#"}) o(data).equals(`${prefix}/route/%3B%3A%40%26%3D%2B%24%2C%2F%3F%25%23`) }) o("handles unicode", () => { - var data = m.p(prefix + "/route/:ö", {"ö": "ö"}) + var data = m.p(`${prefix}/route/:ö`, {"ö": "ö"}) o(data).equals(`${prefix}/route/%C3%B6`) }) o("handles zero", () => { - var string = m.p(prefix + "/route/:a", {a: 0}) + var string = m.p(`${prefix}/route/:a`, {a: 0}) o(string).equals(`${prefix}/route/0`) }) o("handles false", () => { - var string = m.p(prefix + "/route/:a", {a: false}) + var string = m.p(`${prefix}/route/:a`, {a: false}) o(string).equals(`${prefix}/route/false`) }) o("handles dashes", () => { - var string = m.p(prefix + "/:lang-:region/route", { + var string = m.p(`${prefix}/:lang-:region/route`, { lang: "en", region: "US" }) @@ -93,7 +118,7 @@ o.spec("p", () => { o(string).equals(`${prefix}/en-US/route`) }) o("handles dots", () => { - var string = m.p(prefix + "/:file.:ext/view", { + var string = m.p(`${prefix}/:file.:ext/view`, { file: "image", ext: "png" }) @@ -101,17 +126,17 @@ o.spec("p", () => { o(string).equals(`${prefix}/image.png/view`) }) o("merges query strings", () => { - var string = m.p(prefix + "/item?a=1&b=2", {c: 3}) + var string = m.p(`${prefix}/item?a=1&b=2`, {c: 3}) o(string).equals(`${prefix}/item?a=1&b=2&c=3`) }) o("merges query strings with other parameters", () => { - var string = m.p(prefix + "/item/:id?a=1&b=2", {id: "foo", c: 3}) + var string = m.p(`${prefix}/item/:id?a=1&b=2`, {id: "foo", c: 3}) o(string).equals(`${prefix}/item/foo?a=1&b=2&c=3`) }) o("consumes template parameters without modifying query string", () => { - var string = m.p(prefix + "/item/:id?a=1&b=2", {id: "foo"}) + var string = m.p(`${prefix}/item/:id?a=1&b=2`, {id: "foo"}) o(string).equals(`${prefix}/item/foo?a=1&b=2`) }) From fa39b988cf01c3235ed582bbcce2e0fcc38177af Mon Sep 17 00:00:00 2001 From: Claudia Meadows Date: Sat, 19 Oct 2024 22:29:33 -0700 Subject: [PATCH 73/95] Remove some useless `Object.assign`s Within the hyperscript side, it's a significant (roughly 10%) perf boost. --- src/core.js | 6 +++--- test-utils/global.js | 2 +- tests/core/context.js | 6 +++--- tests/std/router.js | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/core.js b/src/core.js index 0409baa98..3864d9ada 100644 --- a/src/core.js +++ b/src/core.js @@ -211,7 +211,7 @@ var m = function (selector, attrs) { if (selector === m.Fragment) { return createParentVnode(TYPE_FRAGMENT, null, null, null, children) } else { - return Vnode(TYPE_COMPONENT, selector, null, Object.assign({children}, attrs), null) + return Vnode(TYPE_COMPONENT, selector, null, {children, ...attrs}, null) } } @@ -226,11 +226,11 @@ var m = function (selector, attrs) { } if (state.a != null) { - attrs = Object.assign({}, state.a, attrs) + attrs = {...state.a, ...attrs} } if (dynamicClass != null || state.c != null) { - if (attrs !== original) attrs = Object.assign({}, attrs) + if (attrs !== original) attrs = {...attrs} attrs.class = dynamicClass != null ? state.c != null ? `${state.c} ${dynamicClass}` : dynamicClass : state.c diff --git a/test-utils/global.js b/test-utils/global.js index 40ea6bdde..e92b25c12 100644 --- a/test-utils/global.js +++ b/test-utils/global.js @@ -63,7 +63,7 @@ export function setupGlobals(env = {}) { } o.beforeEach(() => { - initialize(Object.assign({}, env)) + initialize({...env}) return env.initialize && env.initialize() }) diff --git a/tests/core/context.js b/tests/core/context.js index b279f0753..b41cb39e2 100644 --- a/tests/core/context.js +++ b/tests/core/context.js @@ -19,12 +19,12 @@ o.spec("context", () => { function allKeys(context) { if (context === null || typeof context !== "object") return undefined - const chain = [] + let result = {...context} while (context !== null && context !== Object.prototype) { - chain.push(context) context = Object.getPrototypeOf(context) + result = {...context, ...result} } - return symbolsToStrings(chain.reduceRight((a, b) => Object.assign(a, b), {})) + return symbolsToStrings(result) } o("string keys are set in context", () => { diff --git a/tests/std/router.js b/tests/std/router.js index 728c1cf52..e12507190 100644 --- a/tests/std/router.js +++ b/tests/std/router.js @@ -11,7 +11,7 @@ o.spec("route", () => { var fullHost = `${env.protocol}//${env.hostname === "/" ? "" : env.hostname}` var fullPrefix = `${fullHost}${prefix[0] === "/" ? "" : "/"}${prefix ? `${prefix}/` : ""}` - var G = setupGlobals(Object.assign({}, env, {expectNoConsoleError: true})) + var G = setupGlobals({...env, expectNoConsoleError: true}) o("returns the right route on init", () => { G.window.location.href = `${prefix}/` From 310a32bcb12f1c66c9098669ac25a0c65b9a5d65 Mon Sep 17 00:00:00 2001 From: Claudia Meadows Date: Sat, 19 Oct 2024 22:35:29 -0700 Subject: [PATCH 74/95] Remove unnecessary `finally` block --- performance/test-perf.js | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/performance/test-perf.js b/performance/test-perf.js index 7c02c566c..1a56b6fe6 100644 --- a/performance/test-perf.js +++ b/performance/test-perf.js @@ -223,23 +223,17 @@ benchmarks["mount all"] = async (b) => { benchmarks["redraw all"] = async (b) => { do { cycleRoot() - const allElems = allTrees.map(() => { + allElems = allTrees.map(() => { const elem = document.createElement("div") rootElem.appendChild(elem) return elem }) const allRedraws = allElems.map((elem, i) => m.mount(elem, allTrees[i])) - try { - b.start() - do { - for (const redraw of allRedraws) redraw.sync() - } while (!b.tick()) - if (isBrowser) await nextFrame() - } finally { - for (const elem of allElems) { - m.render(elem, null) - } - } + b.start() + do { + for (const redraw of allRedraws) redraw.sync() + } while (!b.tick()) + if (isBrowser) await nextFrame() } while (!b.done()) } From b33349a8baab4f34f1bb3065a619c3dbc7dc23b6 Mon Sep 17 00:00:00 2001 From: Claudia Meadows Date: Sat, 19 Oct 2024 22:56:52 -0700 Subject: [PATCH 75/95] Improve post-perf UI a little --- performance/test-perf.js | 1 + 1 file changed, 1 insertion(+) diff --git a/performance/test-perf.js b/performance/test-perf.js index 1a56b6fe6..d8e17a7aa 100644 --- a/performance/test-perf.js +++ b/performance/test-perf.js @@ -17,6 +17,7 @@ async function run() { if (!isBrowser) await import("../test-utils/injectBrowserMock.js") await runBenchmarks(benchmarks) cycleRoot() + if (isBrowser) document.body.innerHTML = "Benchmarks completed. See console." } const isBrowser = typeof process === "undefined" From 3da7a1d34dc3449bc8ad1f67fc416ef594be5ee3 Mon Sep 17 00:00:00 2001 From: Claudia Meadows Date: Sat, 19 Oct 2024 22:58:05 -0700 Subject: [PATCH 76/95] Pre-render tree in benchmark so it's only checking update in `render FACTORY` --- performance/test-perf.js | 1 + 1 file changed, 1 insertion(+) diff --git a/performance/test-perf.js b/performance/test-perf.js index d8e17a7aa..594b05fff 100644 --- a/performance/test-perf.js +++ b/performance/test-perf.js @@ -59,6 +59,7 @@ function addTree(name, treeFn) { benchmarks[`render ${name}`] = async (b) => { do { cycleRoot() + m.render(rootElem, treeFn()) b.start() do { m.render(rootElem, treeFn()) From 01fc41ead3c12bed480b733a4fc104da6fc28fd6 Mon Sep 17 00:00:00 2001 From: Claudia Meadows Date: Sat, 19 Oct 2024 23:02:11 -0700 Subject: [PATCH 77/95] ES6ify an event handler (and fix its name) --- performance/components/nested-tree.js | 2 +- performance/components/repeated-tree.js | 2 +- performance/components/simple-tree.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/performance/components/nested-tree.js b/performance/components/nested-tree.js index b2ba4e694..de1b392be 100644 --- a/performance/components/nested-tree.js +++ b/performance/components/nested-tree.js @@ -14,7 +14,7 @@ var NestedHeader = () => m("header", ) ) -var NestedForm = () => m("form", {onSubmit: function () {}}, +var NestedForm = () => m("form", {onsubmit() {}}, m("input[type=checkbox][checked]"), m("input[type=checkbox]", {checked: false}), m("fieldset", diff --git a/performance/components/repeated-tree.js b/performance/components/repeated-tree.js index 6e279d6c4..e4e2730b1 100644 --- a/performance/components/repeated-tree.js +++ b/performance/components/repeated-tree.js @@ -8,7 +8,7 @@ const RepeatedHeader = () => m("header", ) ) -const RepeatedForm = () => m("form", {onSubmit() {}}, +const RepeatedForm = () => m("form", {onsubmit() {}}, m("input", {type: "checkbox", checked: true}), m("input", {type: "checkbox", checked: false}), m("fieldset", diff --git a/performance/components/simple-tree.js b/performance/components/simple-tree.js index 70c278135..fea83c5b1 100644 --- a/performance/components/simple-tree.js +++ b/performance/components/simple-tree.js @@ -16,7 +16,7 @@ export const simpleTree = () => m(".foo.bar[data-foo=bar]", {p: 2}, ), m("main", m("form", - {onSubmit() {}}, + {onsubmit() {}}, m("input[type=checkbox][checked]"), m("input[type=checkbox]"), m("fieldset", fields.map((field) => From 9dd8d17f1fb17b0bf0201e30cc8c5b921ee114a5 Mon Sep 17 00:00:00 2001 From: Claudia Meadows Date: Sun, 20 Oct 2024 22:45:39 -0700 Subject: [PATCH 78/95] Improve benchmark more --- performance/test-perf.js | 45 ++++++++++++++++++++++++++-------------- 1 file changed, 29 insertions(+), 16 deletions(-) diff --git a/performance/test-perf.js b/performance/test-perf.js index 594b05fff..c0d28fca8 100644 --- a/performance/test-perf.js +++ b/performance/test-perf.js @@ -91,19 +91,24 @@ benchmarks["null test"] = (b) => { } while (!b.done()) } -const {routes, vars, templates} = (() => { +const {routes, stringVars, numVars, templates} = (() => { const routes = [] -const vars = [] +const stringVars = [] +const numVars = [] const templates = [] for (let i = 0; i < 16; i++) { for (let j = 0; j < 16; j++) { templates.push(`/foo${i}/:id${i}/bar${j}/:sub${j}`) routes.push(`/foo${i}/${i}/bar${j}/${j}`) - vars.push({ + stringVars.push({ [`id${i}`]: `${i}`, [`sub${j}`]: `${j}`, }) + numVars.push({ + [`id${i}`]: i, + [`sub${j}`]: j, + }) } } @@ -111,7 +116,8 @@ return { // Flatten everything, since they're usually flat strings in practice. routes: JSON.parse(JSON.stringify(routes)).map((path) => ({path, params: new URLSearchParams()})), templates: JSON.parse(JSON.stringify(templates)), - vars: JSON.parse(JSON.stringify(vars)), + stringVars: JSON.parse(JSON.stringify(stringVars)), + numVars: JSON.parse(JSON.stringify(numVars)), } })() @@ -129,9 +135,10 @@ benchmarks["route match"] = (b) => { } while (!b.done()) } -// This needs to be at most a few microseconds, as 300 of these * 3 us/op = 0.9 ms. (And yes, while -// 300 may seem like a lot, I've worked with apps that exceeded 100, and for 60 FPS, you only truly -// have room for about 5ms total for logic.) +// These four need to be at most a few microseconds, as 300 of these * 3 us/op = 0.9 ms. (And yes, +// while 300 may seem like a lot, I've worked with apps that exceeded 100, and for 60 FPS, you only +// truly have room for about 5ms total for logic.) + benchmarks["route non-match"] = (b) => { let i = 0 do { @@ -145,25 +152,31 @@ benchmarks["route non-match"] = (b) => { } while (!b.done()) } -// This needs to be at most a few microseconds, as 300 of these * 3 us/op = 0.9 ms. (And yes, while -// 300 may seem like a lot, I've worked with apps that exceeded 100, and for 60 FPS, you only truly -// have room for about 5ms total for logic.) -benchmarks["path generate with vars"] = (b) => { +benchmarks["path generate with string interpolations"] = (b) => { + let i = 0 + do { + cycleRoot() + do { + // eslint-disable-next-line no-bitwise + i = (i - 1) & 255 + globalThis.test = m.p(templates[i], stringVars[i]) + } while (!b.tick()) + } while (!b.done()) +} + +benchmarks["path generate with number interpolations"] = (b) => { let i = 0 do { cycleRoot() do { // eslint-disable-next-line no-bitwise i = (i - 1) & 255 - globalThis.test = m.p(templates[i], vars[i]) + globalThis.test = m.p(templates[i], numVars[i]) } while (!b.tick()) } while (!b.done()) } -// This needs to be at most a few microseconds, as 300 of these * 3 us/op = 0.9 ms. (And yes, while -// 300 may seem like a lot, I've worked with apps that exceeded 100, and for 60 FPS, you only truly -// have room for about 5ms total for logic.) -benchmarks["path generate no vars"] = (b) => { +benchmarks["path generate no interpolations"] = (b) => { let i = 0 do { cycleRoot() From fee0cca857d4da6da6216310c9f523f3c9ea4847 Mon Sep 17 00:00:00 2001 From: Claudia Meadows Date: Sun, 20 Oct 2024 22:48:42 -0700 Subject: [PATCH 79/95] Boost `m.p` performance by a lot Turns out this was way more perf-sensitive than I initially thought. Also included a long comment to explain everything, as it's not immediately obvious just looking at it that it'd be perf-sensitive. --- src/std/path-query.js | 300 ++++++++++++++++++++++++++++-------------- 1 file changed, 204 insertions(+), 96 deletions(-) diff --git a/src/std/path-query.js b/src/std/path-query.js index 010279fe6..38b1a6bb1 100644 --- a/src/std/path-query.js +++ b/src/std/path-query.js @@ -1,3 +1,46 @@ +/* +Caution: `m.p` and the failure path of `m.match` are both perf-sensitive. More so than you might +think. And unfortunately, string indexing is incredibly slow. + +Suppose we're in a large CRUD app with 20 resources and 10 pages for each resource, for a total of +200 routes. And further, suppose we're on a complicated management page (like a domain management +page) with a grid of 50 rows and 8 routed icon links each. Each link has its URL constructed via +`m.p(...)`, for a total of 400 calls. (This is high, but still realistic. At the time of writing, +Namesilo's UI for selecting domains and performing batch operations on them is designed as a table +with about that many icon links and up to 100 domains per page.) + +To meet 60 FPS, we generally have to have the whole page rendered in under 10ms for the browser to +not skip frames. To give the user some buffer for view inefficiency, let's aim for 2ms of overhead +for all the `m.match` and `m.p` calls. From some local benchmarking, the failure path of `m.match` +requires about 1us/op, so 200 routes would come out to about 0.2ms. (The success path is well under +0.1ms, so its overhead is negligible.) That leaves us about 1.8ms for 400 calls to `m.p(...)`. Do +the math, and that comes out to a whopping 4.5 us/call for us to meet our deadline. + +I've tried the following for `m.p`, and most of them ended up being too slow. Times are for calls +with two string interpolation parameters (the slow path), measured on an older laptop. The laptop +experiences a roughly 30-60% perf boost when charging over when running from battery. The lower end +is while charging, the higher end is while on battery. + +- A direct port of v2's `m.buildPathname`: 15-25 us + - This provides headroom for up to about 70 calls per frame. +- Replace its inner `template.replace` with a `re.exec(template)` loop: 12-18 microseconds + - This provides headroom for up to about 100 calls per frame. +- Switch from using match strings to computing positions from `exec.index`: 6.5-12 microseconds + - This provides headroom for up to about 150 calls per frame. +- Iterate string directly: 2-3.5 microseconds + - This provides headroom for up to about 500 calls per frame. + +I've tried optimizing it further, but I'm running into the limits of string performance at this +point. And the computing positions from `exec.index` is about the fastest I could get any +regexp-based solution to go. + +Also, I tried at first restricting parameters to JS identifiers (like `m.match` parameters are, as +I use named groups to generate the properties), but that, just on the regexp side, cut performance +in more than half. The `exec.match` form, the ideal one for regexp-based solutions, slowed down +from 12 microseconds to about 35-40 microseconds. And that would reduce headroom down to only about +45-50 calls per frame. This rate is simply too slow to even be viable for some smaller apps. +*/ + // Allowed terminators for `m.match`: // - `.` for `:file.:ext` // - `-` for `:lang-:locale` @@ -6,133 +49,198 @@ // Escape with `\\` // Use `*rest` for rest -// Caution: `m.p` and the failure path of `m.match` are both perf-sensitive. It only takes a couple -// hundred `m.p` calls with parameters to amount to about 1ms, and there's only about 10ms total -// that one can reasonably use to render stuff. And it's reasonable to expect several dozen `m.p` -// calls with parameters even in a medium-sized app. -// -// The more complicated one, `m.match`, fortunately is fairly cheap in the common case of mismatch. -// However, `m.p`'s common case is *with variables*, and getting the runtime of that down wasn't -// easy. (`m.p` for context was designed with usage like `m(m.Link, {href: m.p(...)})` in mind.) - import {hasOwn} from "../util.js" var toString = {}.toString -var invalidTemplateChars = /[:*][$_\p{IDS}](?![$\p{IDC}]*(?![:*]))/u -var invalidMatchTemplate = /\/\/|[:*](?![$_\p{IDS}][$\p{IDC}]*(?![:*]))|\*.*?[^$\p{IDC}]|:([$_\p{IDS}][$\p{IDC}]*)[^$\p{IDC}].*?[:*]\1(?![$\p{IDC}])/u -var escapeOrParameter = /\\.|[:*][$_\p{IDS}][$\p{IDC}]*/ug -var escapeOnly = /\\(.)/g +var invalidMatchTemplate = /\/\/|[:*][^$_\p{IDS}]|[:*].[$\p{IDC}]*[:*]|\*.*?[^$\p{IDC}]|:([$_\p{IDS}][$\p{IDC}]*)[^$\p{IDC}].*?[:*]\1(?![$\p{IDC}])/u // I escape literal text so people can use things like `:file.:ext` or `:lang-:locale` in routes. // This is all merged into one pass so I don't also accidentally escape `-` and make it harder to // detect it to ban it from template parameters. var matcherCompile = /([:*])([$_\p{IDS}][$\p{IDC}]*)|\\\\|\\?([$^*+.()|[\]{}])|\\(.)/ug -var serializeQueryValue = (qs, prefix, value) => { - if (value == null || value === false) return - if (Array.isArray(value)) { - for (var i of value) { - serializeQueryValue(qs, `${prefix}[]`, i) - } - } else { - if (typeof value === "object") { - var proto = Object.getPrototypeOf(value) - if (proto == null || proto === Object.prototype || toString.call(value) === "[object Object]") { - for (var k in value) { - if (hasOwn.call(value, k)) { - serializeQueryValue(qs, `${prefix}[${k}]`, value[k]) - } +var serializeQueryValue = (pq, result, prefix, value) => { + var proto + + if (value != null && value !== false) { + if (Array.isArray(value)) { + for (var i of value) { + result = serializeQueryValue(pq, result, `${prefix}[]`, i) + } + } else if ( + typeof value === "object" && + ((proto = Object.getPrototypeOf(value)) == null || proto === Object.prototype || toString.call(value) === "[object Object]") + ) { + for (var k in value) { + if (hasOwn.call(value, k)) { + result = serializeQueryValue(pq, result, `${prefix}[${k}]`, value[k]) } - return } + } else { + var sep = pq.s + pq.s = "&" + result += sep + encodeURIComponent(prefix) + (value === true ? "" : `=${ + typeof value === "number" || typeof value === "bigint" + ? value + : encodeURIComponent(value) + }`) } - qs.v += qs.s + encodeURIComponent(prefix) + (value === true ? "" : `=${encodeURIComponent(value)}`) - qs.s = "&" } -} -var makeQueryBuilder = (sep, value) => ({s: sep, v: value}) + return result +} -var q = (params) => { - var qs = makeQueryBuilder("", "") +var serializeQueryParams = (sep, value, exclude, params) => { + var pq = {s: sep} for (var key in params) { - if (hasOwn.call(params, key)) serializeQueryValue(qs, key, params[key]) + if (hasOwn.call(params, key) && !exclude.includes(key)) { + value = serializeQueryValue(pq, value, key, params[key]) + } } - return qs.v + return value } +var q = (params) => serializeQueryParams("", "", [], params) + +var QUERY = 0 +var ESCAPE = 1 +var CHAR = 2 +// Structure: +// Bit 0: is raw +// Bit 1: is next +// Bit 2: always set +var VAR_START = 4 +// var RAW_VAR_START = 5 +var VAR_NEXT = 6 +// var RAW_VAR_NEXT = 7 +var STATE_IS_RAW = 1 +var STATE_IS_NEXT = 2 + + // Returns `path` from `template` + `params` +/** + * @param {string} template + * @param {undefined | null | Record} params + */ var p = (template, params) => { - if (invalidTemplateChars.test(template)) { - throw new SyntaxError("Template parameter names must be separated by either a '/', '-', or '.'.") - } - if (params == null) return template.replace(escapeOnly, "$1") - var queryIndex = template.indexOf("?") - var hashIndex = template.indexOf("#") - var queryEnd = hashIndex < 0 ? template.length : hashIndex - var pathEnd = queryIndex < 0 ? queryEnd : queryIndex - var path = template.slice(0, pathEnd) - var inTemplate = new Set() - var resolved = "" - var start = escapeOrParameter.lastIndex = 0 - var exec - - while ((exec = escapeOrParameter.exec(path)) != null) { - var index = exec.index - resolved += path.slice(start, index) - start = escapeOrParameter.lastIndex - if (path[index] === "\\") { - start = index + 1 - } else { - var key = path.slice(index + 1, start) - inTemplate.add(key) - key = params[key] - resolved += ( - key != null - // Escape normal parameters, but not variadic ones. - ? (path[index] === "*" ? key : encodeURIComponent(`${key}`)) - // If no such parameter exists, don't interpolate it. - : path.slice(index, start) - ) + // This carefully only iterates the template once. + var prev = 0 + var start = 0 + var state = CHAR + // An array is fine. It's almost never large enough for the overhead of hashing to pay off. + var inTemplate = [] + // Used for later. + var hash = "" + var queryIndex = -1 + var hashIndex = -1 + var result = "" + var sep = "?" + + var NOT_VAR_NEXT = VAR_NEXT - 1 + + // Using `for ... of` so the engine can do bounds check elimination more easily. + for (var i = 0;; i++) { + var ch = template.charAt(i) + + if ( + state > NOT_VAR_NEXT && + (ch === "" || ch === "#" || ch === "?" || ch === "\\" || ch === "/" || ch === "." || ch === "-") + ) { + var segment = template.slice(start + 1, i) + + // If no such parameter exists, don't interpolate it. + if (params != null && params[segment] != null) { + inTemplate.push(segment) + segment = `${params[segment]}` + + // Escape normal parameters, but not variadic ones. + // eslint-disable-next-line no-bitwise + if (state & STATE_IS_RAW) { + var newHashIndex = segment.indexOf("#") + var newQueryIndex = (newHashIndex < 0 ? segment : segment.slice(0, newHashIndex)).indexOf("?") + if (newQueryIndex >= 0) { + sep = "&" + queryIndex = result.length + (prev - start) + newQueryIndex + } + if (newHashIndex >= 0) { + hashIndex = result.length + (prev - start) + newHashIndex + } + } else { + segment = encodeURIComponent(segment) + } + + // Drop the preceding `:`/`*`/`\` character from the appended segment + if (prev !== start) { + result += template.slice(prev, start) + } + + result += segment + + // Start from the next end + prev = i + } } - } - resolved += path.slice(start) + if (ch === "#") { + if (hashIndex < 0) hashIndex = i + } else if (ch !== "") { + if (state === QUERY) { + // do nothing + } else if (ch === "?") { + // The query start cannot be escaped. It's a proper URL delimiter. + if (queryIndex < 0) { + queryIndex = i + sep = "&" + } else { + // Inject an `&` in place of a `?`. Note that `sep === "&"` + if (prev !== i) result += template.slice(prev, i) + result += "&" + prev = i + 1 + } + state = QUERY + } else if (state === ESCAPE) { + // Drop the preceding `\` character from the appended segment + if (prev !== start) { + result += template.slice(prev, start) + } - // In case the template substitution adds new query/hash parameters. - var newQueryIndex = resolved.indexOf("?") - var newHashIndex = resolved.indexOf("#") - var newQueryEnd = newHashIndex < 0 ? resolved.length : newHashIndex - var newPathEnd = newQueryIndex < 0 ? newQueryEnd : newQueryIndex - var qs = makeQueryBuilder("?", resolved.slice(0, newPathEnd)) + state = CHAR + start = prev = i + } else if (ch === "\\") { + start = i + state = ESCAPE + } else if (ch === ":" || ch === "*") { + if (state > CHAR) { + throw new SyntaxError("Template parameter names must be separated by either a '/', '-', or '.'.") + } + // eslint-disable-next-line no-bitwise + state = VAR_START | (ch === "*") + start = i + } else if (ch === "/" || ch === "." || ch === "-") { + state = CHAR + } else if (state > CHAR) { + // eslint-disable-next-line no-bitwise + state |= STATE_IS_NEXT + } - if (queryIndex >= 0) { - qs.v += template.slice(queryIndex, queryEnd) - qs.s = "&" - } + continue + } - if (newQueryIndex >= 0) { - qs.v += qs.s + resolved.slice(newQueryIndex, newQueryEnd) - qs.s = "&" - } + if (prev === 0 && params == null) { + return template + } - for (var key in params) { - if (hasOwn.call(params, key) && !inTemplate.has(key)) { - serializeQueryValue(qs, key, params[key]) + if (prev < template.length) { + result += template.slice(prev) } - } - if (hashIndex >= 0) { - qs.v += template.slice(hashIndex) - } else { - qs.s = "" - } + if (hashIndex >= 0) { + hash = result.slice(hashIndex) + result = result.slice(0, hashIndex) + } - if (newHashIndex >= 0) { - qs.v += qs.s + resolved.slice(newHashIndex) + return serializeQueryParams(sep, result, inTemplate, params) + hash } - - return qs.v } /** @typedef {RegExp & {r: number, p: URLSearchParams}} Matcher */ From 451aa18dc88ad53a95260b5138633f6c270eb75b Mon Sep 17 00:00:00 2001 From: Claudia Meadows Date: Sun, 20 Oct 2024 23:00:50 -0700 Subject: [PATCH 80/95] Tolerate absence of `/dist` --- scripts/build.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/build.js b/scripts/build.js index 50c28b80b..0b41b5a08 100644 --- a/scripts/build.js +++ b/scripts/build.js @@ -61,7 +61,7 @@ async function report(file) { } async function main() { - await fs.rm(path.resolve(dirname, "../dist"), {recursive: true}) + await fs.rm(path.resolve(dirname, "../dist"), {recursive: true, force: true}) await Promise.all([ build("mithril.umd", "iife"), From 36d4cb331be8e626acb16ebe11af0df094728a97 Mon Sep 17 00:00:00 2001 From: Claudia Meadows Date: Tue, 29 Oct 2024 01:57:51 -0700 Subject: [PATCH 81/95] Resolve remaining issues with benchmarks, explain with a lengthy justification. --- .eslintrc.json | 28 +- .gitignore | 3 + package.json | 1 - performance/README.md | 727 ++++++++++++++++++++++++++++++++++++++ performance/bench.js | 433 ++++++++++++++++------- performance/chart.css | 30 ++ performance/chart.js | 262 ++++++++++++++ performance/index.html | 3 +- performance/routes.js | 24 ++ performance/serialized.js | 34 ++ performance/stats.js | 451 +++++++++++++++++++++++ performance/test-perf.js | 424 +++++++++++----------- src/std/path-query.js | 2 + 13 files changed, 2080 insertions(+), 342 deletions(-) create mode 100644 performance/README.md create mode 100644 performance/chart.css create mode 100644 performance/chart.js create mode 100644 performance/routes.js create mode 100644 performance/serialized.js create mode 100644 performance/stats.js diff --git a/.eslintrc.json b/.eslintrc.json index 6ac7e9f2a..c65afb355 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -2,7 +2,7 @@ "overrides": [ {"files": "*.cjs", "parserOptions": {"sourceType": "script"}}, { - "files": ["scripts/**", "performance/**"], + "files": ["scripts/**"], "env": { "node": true, "es2022": true @@ -15,14 +15,32 @@ "no-restricted-syntax": "off" } }, + { + "files": ["performance/**"], + "env": { + "browser": true + }, + "rules": { + "no-restricted-syntax": ["error", + {"selector": "Literal[bigint]", "message": "BigInts are not supported in ES2018"}, + {"selector": "ChainExpression", "message": "Optional chaining is not supported in ES2018"}, + {"selector": "BinaryExpression[operator='??']", "message": "Nullish coalescing is not supported in ES2018"}, + {"selector": "MetaProperty[meta.name='import'][property.name='meta']", "message": "`import.meta` is not supported in ES2018"}, + {"selector": "ExportAllDeclaration[exported!=null]", "message": "`export * as foo from ...` is not supported in ES2018"}, + {"selector": "CatchClause[param=null]", "message": "Omitted `catch` bindings are not supported in ES2018"}, + {"selector": "Literal[regex.flags=/s/]", "message": "`/.../s` is not supported in ES2018"}, + {"selector": "Literal[regex.flags=/v/]", "message": "`/.../v` is not supported in ES2018"}, + + {"selector": "MemberExpression[property.name='matchAll']", "message": "`string.matchAll` is not supported in ES2018"}, + {"selector": "MemberExpression[property.name='trimStart']", "message": "`string.trimStart` is not supported in ES2018"} + ] + } + }, { "files": ["tests/**", "test-utils/**"], "env": { "node": true }, - "parserOptions": { - "ecmaVersion": 2020 - }, "rules": { "no-process-env": "off", "no-restricted-syntax": ["error", @@ -42,7 +60,7 @@ } ], "env": { - "es6": true + "es2018": true }, "globals": { "ReadableStream": true, diff --git a/.gitignore b/.gitignore index 246b19b25..e3d2fc37d 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,6 @@ # Exclude the compiled output. It's available through prerelease publishes anyways, so there's no # need to keep a spare copy in the repo. /dist + +# For an easier time working with console logs from profiles. +/*.log diff --git a/package.json b/package.json index 3858f090d..fdb00b753 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,6 @@ "scripts": { "build": "node scripts/build.js", "lint": "eslint . --cache", - "perf": "node performance/test-perf.js", "pretest": "npm run lint", "test": "ospec" }, diff --git a/performance/README.md b/performance/README.md new file mode 100644 index 000000000..267c3ad35 --- /dev/null +++ b/performance/README.md @@ -0,0 +1,727 @@ +# Benchmarks + +## Usage + +Usage is extremely simple and designed to put the ultimate focus on the benchmark code itself: + +```js +import {setupBenchmarks} from "./bench.js" + +async function setup() { + // Test suite setup. May be async. +} + +async function cleanup() { + // Post-benchmark cleanup. May be async and always performed, even on error. +} + +await setupBenchmarks(setup, cleanup, { + "foo": { + // Before every measurement interval. `fn` may be called multiple times in a single + // interval, so be aware. + tick() { + // ... + }, + // The actual benchmarked code. Be sure to return a result so it doesn't get optimized out. + fn() { + // ... + }, + }, +}) +``` + +Then, make sure the script is loaded in an HTML file like follows: + +```html + + + + + Performance tests + + + + Initializing tests. If this text doesn't change, open the browser console and check for + load errors. + + +``` + +To run the benchmarks: + +1. Spin up a server using `node scripts/server.js` in the repo root. +2. Open or whatever the path to your benchmark is. +3. Run the tests and wait for them to complete. They'll then show a bunch of benchmark graphs for detailed inspection. If you want a simpler view without all the graphs or if you just want to copy all the samples out, open the console. + +## Rationale + +Why are we using a home-rolled system instead of a pre-made framework? Two reasons: + +1. It won't have maintenance problems. If there's an issue with the benchmark, it can be patched in-repo. +2. I can better account for overhead. +3. It correctly accounts for timer accuracy problems. + +Things it does better than Benchmark.js: + +- It uses an exposed benchmark loop, so I can precisely control inter-frame delays. +- It prints out much more useful output: a confidence interval-based range and a total run count. +- It works around the low resolution inherent to modern browsers. + +Before I go further, I'd like to explain some theory, theory that might not be obvious to most. + +### What timers actually measure + +Timers do *not* measure raw intervals directly. They simply measure the number of clock ticks that +have occurred since a given point of time. What benchmarks actually do when they compare two +timestamps is count the number of clock ticks that occurred between them. + +Operating systems hide this low-level detail by exposing a monotonic "clock". This clock is based +on two things: a CPU cycle counter and the CPU's current frequency. It optionally can also track +system sleep time using a built-in counter (usually plugged into a small quartz crystal). "Sleep" +here just means the CPU is in a low-power state, clock disabled, while waiting for either I/O or a +hardware timer to go off, not that the system itself is physically powered off. While it's useful +to expose this as a duration offset (usually relative to some point during the boot process), this +is only an abstraction. + +### Browser interactions + +Browsers, for privacy and security reasons, limit the granularity of timers. + +- https://w3c.github.io/hr-time/#dfn-coarsen-time +- https://github.com/w3c/hr-time/issues/79 +- https://github.com/w3c/hr-time/issues/56 + +The spec requires a mandatory maximum of 20 microseconds of granularity for isolated frames, but +some browsers coarsen the timers even further: + +- Firefox: 20 microseconds +- Chrome: 100 microseconds with 100 microseconds jitter +- Safari: 1000 microseconds +- Edge pre-Chromium: 20 microseconds with 20 microseconds jitter +- Brave: 100 microseconds + +Once again, keep in mind, the monotonic clock is an abstraction. What this clock coarsening does +isn't directly reducing resolution. What it really does from a theoretical perspective is establish +a different kind of tick, one that's closer to that of a wall clock's ticking. It creates a clock +that ticks at the granularity interval, but relies on the OS's CPU frequency tracking to detect if +it's supposed to tick. (In practice, instead of having the OS directly manage that in memory, it +instead uses a mathematical formula to derive it. And this is also how it's actually specified.) + +The induced jitter some browsers use mean that there's some uncertainty, a chance the clock may +wait up to that long to tick. For instance, Chrome's clock may wait anywhere from 100 to 200 +microseconds to generate a tick. + +So, in reality, comparing times between two `performance.now()` calls is simply counting the number +of times the clock ticked. Or to put it another way, you could assume it's really doing something like this pseudocode, assuming no jitter: + +```js +// Firefox in isolated contexts +const resolution = 0.02 // milliseconds + +let ticks = 0 + +spawnThread(() => { + while (true) { + sleepMs(resolution) + ticks++ + } +}) + +performance.now = () => { + return ticks * resolution +} +``` + +> For performance reasons, it doesn't work like this in browsers, but you get the point. And it very much *does* work this way at the hardware and OS level. + +Jitter is more involved, as you'd need partial ticks. But it otherwise works similarly: + +```js +// Chrome in isolated contexts +const resolution = 0.1 // milliseconds +const jitter = 0.1 // milliseconds + +let ticks = 0 + +spawnThread(() => { + while (true) { + const jitterAmount = Math.random() + const jitterTicks = (resolution / jitter) * jitterAmount + sleepMs(resolution + jitter * jitterAmount) + ticks += 1 + jitterTicks + } +}) + +performance.now = () => { + return ticks * resolution +} +``` + +### How the coarseness impacts accuracy + +Durations in tests are usually measured like this: + +```js +const start = performance.now() +// ... +const end = performance.now() +const duration = end - start +``` + +Suppose resolution for the clock is 100 microseconds, and the span between the two calls is precisely 550 microseconds. You might expect to get a `duration` of `0.5` or `0.6` as that's the rounded duration. But this isn't necessarily the case, not with jitter involved. Let's go back to the timer-as-ticking-clock model to see how this could wildly differ. + +1. Get `start`. For the sake of example, let's let the tick be 10, resulting in a return value of `1.0`. +2. Ticker sleeps for 100 microseconds = 1 tick. Random jitter ends up as 28 microseconds = 0.28 ticks, so after it sleeps, it would have slept for a total of 1.28 ticks. +3. Ticker sleeps for 100 microseconds = 1 tick. Random jitter ends up as 52 microseconds = 0.52 ticks, so after it sleeps, it would have slept for a total of 2.8 ticks. +4. Ticker sleeps for 100 microseconds = 1 tick. Random jitter ends up as 44 microseconds = 0.44 ticks, so after it sleeps, it would have slept for a total of 4.24 ticks. +5. Ticker sleeps for 100 microseconds = 1 tick. Random jitter ends up as 88 microseconds = 0.88 ticks, so after it sleeps, it would have slept for a total of 6.12 ticks. +6. Block completes to `end = performance.now()`. The previous ticker sleep hasn't completed, so it returns based on a tick of 10 + 4.24 = 14.24, or a return value of `1.424`. +7. `duration` evaluates to `1.424 - 1.0` = `0.42399999999999993`. + +Browsers, in a quest for optimization, implement this a bit differently, and so for longer spans between successive `performance.now()` calls, you see less jitter. + +```js +const resolution = 0.1 // milliseconds +const jitter = 0.1 // milliseconds + +let lastTimestamp = -1 + +performance.now = () => { + const rawTimestamp = getUnsafeTimestamp() + const coarsened = Math.round(rawTimestamp / resolution) * resolution + const coarsenedWithJitter = coarsened + Math.random() * jitter + if (coarsenedWithJitter < lastTimestamp) return lastTimestamp + lastTimestamp = coarsenedWithJitter + return coarsenedWithJitter +} +``` + +> Chrome takes a subtly different approach (this assumes it's not cross-origin isolated, the case where it uses 100us + 100us jitter): +> +> ```js +> // This is approximate. Chrome operates on integer microseconds and uses a (weak) scrambler instead +> // of a proper (pseudo-)random number generator to compute its jitter. +> +> const resolution = 0.1 // milliseconds +> const jitter = 0.1 // milliseconds +> +> const clamperSecret = randomBigInt64() +> +> const clampedTimeOrigin = getClampedTimeOrigin() +> +> performance.now = () => { +> const rawTimestamp = getUnsafeTimestamp() +> const sign = Math.sign(rawTimestamp) +> const time = Math.abs(rawTimestamp) +> const lowerDigits = time % 10_000_000 +> const upperDigits = time - lowerDigits +> +> let clampedTime = lowerDigits - lowerDigits % resolution +> if (lowerDigits >= jitter * Math.random()) clampedTime += jitter +> clampedTime = (clampedTime + upperDigits) * sign +> return Math.max(clampedTime - clampedTimeOrigin, 0) +> } +> ``` +> +> Or, in math notation: +> - $R$ is the raw millisecond timestamp. +> - $O$ is the clamped time origin. +> - $j$ is the jitter for the returned timestamp. +> - $d$ is the result duration. +> +> $$ +> \begin{align*} +> R &\in \reals \\ +> j &\in [0, 1) \\ +> j & \text{ is uniformly random} \\ +> t &= |R| \\ +> l &= t - 1000000 \left\lfloor \frac{t}{1000000} \right\rfloor \\ +> d &= \text{max}\left(\text{sgn}(R)\left(t-100\left\lfloor \frac{l}{100} \right\rfloor+1_{[-\infty, l]}(0.1j)~0.1\right) - O\right) \\ +> \end{align*} +> $$ + +In this case, the sequence would look like this: + +1. Get `start`. For the sake of example, let's let the raw timestamp be `1.00` and assume the random value for the jitter comes out to `0.1`, thus giving us a return value of `1.01`. +2. System clock sleeps for sufficient nanoseconds to equal 550 microseconds. +3. Block completes to `end = performance.now()`. The raw timestamp would now be `1.55`. Let's assume the random value for the jitter is `0.8`, which would give us a value of `1.68`. +4. `duration` evaluates to `1.68-1.01` = `0.67`, representing a duration of 670 microseconds. + +In theory, `duration` could go as low as `0.45` (`0.95` + max jitter start, `1.50` + min jitter end) or as high as `0.65` (`1.00` + min jitter start, `1.55` + max jitter end), or an uncertainty of 10% with this model. + +Thing is, this is just with longer times between polls. In reality, we're often calling `performance.now()` multiple times in a single interval. Here's one way that could shake out with the above pseudocode, assuming the time between calls is 20 microseconds: + +1. Get `start`. For the sake of example, let's let the raw timestamp be `1.00` and assume the random value for the jitter comes out to `0.1`, thus giving us a return value of `1.01`. +2. System clock sleeps for sufficient nanoseconds to equal 20 microseconds. +3. Block completes to `end = performance.now()`. The raw timestamp would now be `1.02`. Let's assume the random value for the jitter is `0.8`, which would give us a value of `1.10`. +4. `duration` evaluates to `1.10-1.01` = `0.09`, representing a duration of 100 microseconds. + +Here's another way it could shake out: + +1. Get `start`. For the sake of example, let's let the raw timestamp be `1.00` and assume the random value for the jitter comes out to `0.8`, thus giving us a return value of `1.08`. +2. System clock sleeps for sufficient nanoseconds to equal 20 microseconds. +3. Block completes to `end = performance.now()`. The raw timestamp would now be `1.02`. Let's assume the random value for the jitter is `0.2`, which would give us a value of `1.04`. As this is less than the previous jittered timestamp of `1.08` returned, it returns that instead. +4. `duration` evaluates to `1.08-1.08` = `0`, representing a duration of 0 microseconds. + +Here's a third way it could shake out: + +1. Get `start`. For the sake of example, let's let the raw timestamp be `0.94` and assume the random value for the jitter comes out to `0.2`, thus giving us a return value of `0.96`. +2. System clock sleeps for sufficient nanoseconds to equal 20 microseconds. +3. Block completes to `end = performance.now()`. The raw timestamp would now be `0.96`. Let's assume the random value for the jitter is `0.8`, which would give us a value of `1.08`. +4. `duration` evaluates to `1.08-0.96` = `0.1200000000000001`, representing a duration of 120 microseconds. + +Here's a fourth way it could shake out: + +1. Get `start`. For the sake of example, let's let the raw timestamp be `0.94` and assume the random value for the jitter comes out to `0.8`, thus giving us a return value of `1.02`. +2. System clock sleeps for sufficient nanoseconds to equal 20 microseconds. +3. Block completes to `end = performance.now()`. The raw timestamp would now be `0.96`. Let's assume the random value for the jitter is `0.2`, which would give us a value of `1.02`. +4. `duration` evaluates to `1.02-1.02` = `0`, representing a duration of 0 microseconds. + +That's a lot of examples. So let's actually apply some statistics to it all. Given that 20 microsecond span between calls: + +- `duration` is within the range `0` to `0.15000000000000002` inclusive. +- The chance that `duration === 0` is 30% +- The chance that `duration > 0.02` is about 31%. This implies the average duration must be below 0.2. +- The expected value of `duration` is 0. This is the result of taking the mean of all (the infinitely many) possible durations and weighting them all by their probabilities. + +
+Proof for the probability claims + +This is so others can verify that my math is right. Stats has admittedly never been my strong suit. + +> Warning, lots of calculus. +> +> Also, note that this is using my simplified model, *not* the version Chrome uses (that is actually potentially non-monotonic). + +1. Start with the function call. + + ``` + const resolution = 0.1 // milliseconds + const jitter = 0.1 // milliseconds + + let lastTimestamp = -1 + + performance.now = () => { + const rawTimestamp = getUnsafeTimestamp() + const coarsened = Math.round(rawTimestamp / resolution) * resolution + const coarsenedWithJitter = coarsened + Math.random() * jitter + if (coarsenedWithJitter < lastTimestamp) return lastTimestamp + lastTimestamp = coarsenedWithJitter + return coarsenedWithJitter + } + + const start = performance.now() + const end = performance.now() + const duration = end - start + + assert(duration === 0) + ``` + +2. Inline the resolution and jitter constants. + + ``` + let lastTimestamp = -1 + + performance.now = () => { + const rawTimestamp = getUnsafeTimestamp() + const coarsened = Math.round(rawTimestamp * 10) / 10 + const coarsenedWithJitter = coarsened + Math.random() / 10 + if (coarsenedWithJitter < lastTimestamp) return lastTimestamp + lastTimestamp = coarsenedWithJitter + return coarsenedWithJitter + } + + const start = performance.now() + const end = performance.now() + const duration = end - start + + assert(duration === 0) + ``` + +3. Inline the `performance.now()` calls and simplify. + + ``` + let start = Math.round(getUnsafeTimestamp() * 10) / 10 + Math.random() / 10 + let end = Math.round(getUnsafeTimestamp() * 10) / 10 + Math.random() / 10 + if (end < start) end = start + + const duration = end - start + assert(duration === 0) + ``` + +4. Reduce to pure math and encode the call offset. $p_0$ is our probability for `duration === 0`, and $p_a$ is our probability for `duration > 0.02`. + + - $R_s$ is the raw start millisecond timestamp. + - $R_e$ is the raw end millisecond timestamp. + - $j_s$ is the jitter for the returned start timestamp. + - $j_e$ is the jitter for the returned end timestamp. + + $$ + \begin{align*} + R_s, j_s, j_e & \text{ are uniformly random} \\ + j_s, j_e &\in [0, 1] \\ + R_e &= R_s + 0.02 \\ + T_s &= \frac{\mathrm{round}(10 R_s)}{10} + \frac{j_s}{10} & \\ + &= \frac{\mathrm{round}(10 R_s) + j_s}{10} & \\ + T_e &= \frac{\mathrm{round}(10 R_s)}{10} + \frac{j_s}{10} & \\ + &= \frac{\mathrm{round}(10 R_s) + j_s}{10} & \\ + p_0 &= \mathrm{P}(\mathrm{max}(T_s, T_e) - T_s = 0) \\ + p_a &= \mathrm{P}(\mathrm{max}(T_s, T_e) - T_s \ge 0.02) \\ + \end{align*} + $$ + +5. Simplify the inequality, simplify $\mathrm{max}(a, b)$ in each piecewise variant. + + - $\mathrm{max}(a, b) - a = 0$ simplifies to $b - a \le 0$. + - $\mathrm{max}(a, b) - a \gt 0.02$ simplifies to $b - a \gt 0.02$. + + For readability, $v$ will represent the compared-to duration and $p_v$ refers to $p_b$ when $v=0$ and $p_a$ when $v=a$. The rest of the proof holds for both equally. + + $$ + \begin{align*} + R_s, j_s, j_e & \text{ are uniformly random} \\ + j_s, j_e &\in [0, 1] \\ + R_e &= R_s + 0.02 \\ + T_s &= \frac{\mathrm{round}(10 R_s) + j_s}{10} & \\ + T_e &= \frac{\mathrm{round}(10 R_e) + j_e}{10} & \\ + p_0 &= \mathrm{P}(T_s - T_e \le 0) \\ + &= 1 - \mathrm{P}(T_s - T_e \gt 0) \\ + p_a &= \mathrm{P}(T_s - T_e \gt 0.02) \\ + \end{align*} + $$ + + To simplify later steps, let $p_b = 1 - p_0$. This lets me define $p_v = \mathrm{P}(T_s - T_e \gt v)$ for $v \in \{0, 0.02\}$. + +6. Inline $T_s$, $T_e$, and $R_e$ into the inequalities and simplify. + + $$ + \begin{align*} + R_s, j_s, j_e & \text{ are uniformly random} \\ + j_s, j_e &\in [0, 1] \\ + v &\in \{0, 0.02\} \\ + p_v &= \mathrm{P} \left( \frac{\mathrm{round}(10 R_s) + j_s}{10} - \frac{\mathrm{round}(10 (R_s + 0.02)) + j_e}{10} \ge v \right) \\ + &= \mathrm{P} \left( \frac{(\mathrm{round}(10 R_s) + j_s) - (\mathrm{round}(10 R_s + 0.2)) + j_e)}{10} \ge v \right) \\ + &= \mathrm{P}\left((\mathrm{round}(10 R_s) + j_s) - (\mathrm{round}(10 R_s + 0.2) + j_e) \ge v \right) \\ + \end{align*} + $$ + +7. Separate the rounding from the jitter combination. This makes subsequent steps clearer. + + $$ + \begin{align*} + R_s, j_s, j_e & \text{ are uniformly random} \\ + j_s, j_e &\in [0, 1] \\ + v &\in \{0, 0.02\} \\ + p_v &= \mathrm{P}\left((\mathrm{round}(10 R_s) - \mathrm{round}(10 R_s + 0.2)) + (j_s - j_e) \ge v \right) \\ + \end{align*} + $$ + +8. Split the two "round" operations into their piecewise floor and ceiling components. + + $$ + \begin{align*} + R_s, j_s, j_e & \text{ are uniformly random} \\ + j_s, j_e &\in [0, 1] \\ + v &\in \{0, 0.02\} \\ + p_v &= P\left( \begin{cases} + (\lfloor 10 R_s \rfloor - \lfloor 10 R_s + 0.2 \rfloor) + (j_s - j_e) \ge v& \text{if } R_s \in [0.00, 0.03) \mod 0.1 \\ + (\lfloor 10 R_s \rfloor - \lceil 10 R_s + 0.2 \rceil) + (j_s - j_e) \ge v& \text{if } R_s \in [0.03, 0.05) \mod 0.1 \\ + (\lceil 10 R_s \rceil - \lceil 10 R_s + 0.2 \rceil) + (j_s - j_e) \ge v& \text{if } R_s \in [0.05, 0.08) \mod 0.1 \\ + (\lceil 10 R_s \rceil - \lfloor 10 R_s + 0.2 \rfloor) + (j_s - j_e) \ge v& \text{if } R_s \in [0.08, 0.10) \mod 0.1 \\ + \end{cases} \right) \\ + \end{align*} + $$ + +9. Simplify the round operations by taking advantage of their domains. + + $$ + \begin{align*} + R_s, j_s, j_e & \text{ are uniformly random} \\ + j_s, j_e &\in [0, 1] \\ + v &\in \{0, 0.02\} \\ + p_v &= P\left( \begin{cases} + (0 - 0) + (j_s - j_e) \ge v& \text{if } R_s \in [0.00, 0.03) \mod 0.1 \\ + (0 - 1) + (j_s - j_e) \ge v& \text{if } R_s \in [0.03, 0.05) \mod 0.1 \\ + (1 - 1) + (j_s - j_e) \ge v& \text{if } R_s \in [0.05, 0.08) \mod 0.1 \\ + (1 - 0) + (j_s - j_e) \ge v& \text{if } R_s \in [0.08, 0.10) \mod 0.1 \\ + \end{cases} \right) \\ + &= P\left( \begin{cases} + 0 + (j_s - j_e) \ge v& \text{if } R_s \in [0.00, 0.03) \mod 0.1 \\ + -1 + (j_s - j_e) \ge v& \text{if } R_s \in [0.03, 0.05) \mod 0.1 \\ + 0 + (j_s - j_e) \ge v& \text{if } R_s \in [0.05, 0.08) \mod 0.1 \\ + 1 + (j_s - j_e) \ge v& \text{if } R_s \in [0.08, 0.10) \mod 0.1 \\ + \end{cases} \right) \\ + &= P\left( \begin{cases} + (j_s - j_e) \ge v& \text{if } R_s \in [0.00, 0.03) \mod 0.1 \\ + (j_s - j_e) \ge v + 1& \text{if } R_s \in [0.03, 0.05) \mod 0.1 \\ + (j_s - j_e) \ge v& \text{if } R_s \in [0.05, 0.08) \mod 0.1 \\ + (j_s - j_e) \ge v - 1& \text{if } R_s \in [0.08, 0.10) \mod 0.1 \\ + \end{cases} \right) \\ + \end{align*} + $$ + +10. Merge everything into a single unified equation of probability. + + This uses the [indicator function](https://en.wikipedia.org/wiki/Indicator_function), as signified by $1_{\text{set}}$. + + $$ + \begin{align*} + R_s, j_s, j_e & \text{ are uniformly random} \\ + j_s, j_e &\in [0, 1] \\ + v &\in \{0, 0.02\} \\ + S &= R_s \mod 0.1 \\ + &= \frac{10 R_s - \lfloor 10 R_s \rfloor}{10} \\ + D_v &= 1_{[0.00, 0.03) \cup [0.05, 0.08)}(S) ~ \mathrm{P}((j_s - j_e) \ge v) ~ + \\ + &\phantom{=} 1_{[0.03, 0.05)}(S) ~ \mathrm{P}((j_s - j_e) \ge v + 1) ~ + \\ + &\phantom{=} 1_{[0.08, 0.10)}(S) ~ \mathrm{P}((j_s - j_e) \ge v - 1) \\ + p_v &= P(D_v) \\ + \end{align*} + $$ + + > $D_v$ is the random condition we're checking the probability of. Easier than fighting LaTeX, and also a bit clearer that this *is* a variable, just not the actual probability. + + Using [Iverson bracket](https://en.wikipedia.org/wiki/Iverson_bracket) notation (in which $[\text{cond}]$ roughly translates to `cond ? 1 : 0` in JS and most C-like languages), it'd read more like this: + + $$ + \begin{align*} + S =& R_s \mod 0.1 \\ + =& \frac{10 R_s - \lfloor 10 R_s \rfloor}{10} \\ + p =& ([0.00 \le x < 0.02 \text{ or } 0.05 \le x < 0.08]) ~ \mathrm{P}((j_s - j_e) \ge 0) \\ + D_v &= [0.00 \le x < 0.02 \text{ or } 0.05 \le x < 0.08] ~ \mathrm{P}((j_s - j_e) \ge v) ~ + \\ + &\phantom{=} [0.03 \le x < 0.05] ~ \mathrm{P}((j_s - j_e) \ge v + 1) ~ + \\ + &\phantom{=} [0.08 \le x < 0.10] ~ \mathrm{P}((j_s - j_e) \ge v - 1) \\ + p_v &= P(D_v) \\ + \end{align*} + $$ + +11. Figure out the probability function for $\mathrm{P}((j_s - j_e) \ge x)$. + + This is a multi-step process that involves a bit of calculus. It's used across all three components, so best to do it once. + + For the PDF of $(j_s - j_e) \ge 0$, I'll go by [this answer](https://math.stackexchange.com/a/345047) and spare you the math mess. It comes out to $f_X(t) = (1 - |t|) 1_{[-1,1]}(t)$, using the same indicator function. (The math checks out, trust me.) + + Now, we need to figure out what $P((j_s - j_e) \ge x)$ is. Fortunately, this is (almost) precisely what the [cumulative distribution function](https://en.wikipedia.org/wiki/Cumulative_distribution_function) returns. The definition of that, given a distribution function $X$, is as follows, where $f_X$ is the probability density function: + + $$ + \begin{align*} + F_X(x) &= P(X \le x) \\ + &= \int_{-\infty}^x f_X(t) ~ dt \\ + \end{align*} + $$ + + That looks a bit scary, but we know the probability density function for our distribution already, from before. Let's plug it in. + + $$ + F(x) = \int_{-\infty}^x (1 - |t|) ~ 1_{[-1,1]}(t) ~ dt + $$ + + Conveniently, that interval for the indicator function, $-1 \le t \le 1$, is the only interval we care about. All other values we already know aren't possible to get. Knowing that, let's tighten the range of the integral and substitute that value in. (This also avoids the need to do integration by parts.) + + $$ + \begin{align*} + F(x) &= \int_{-1}^x (1 - |t|) ~ 1_{[-1,1]}(t) ~ dt \\ + &= \int_{-1}^x (1 - |t|) ~ 1 ~ dt \\ + &= \int_{-1}^x (1 - |t|) ~ dt \\ + \end{align*} + $$ + + The antiderivative of $|x|$ is $\frac{1}{2} \text{sgn}(x) x^2 + C$. But let's not try to calculate that. If you look at the graph of $1-|x|$, it's just a triangle with vertices at $(0, 1)$, $(1, 0)$, and $(-1, 0)$. (I'll leave the actual plot of this as an exercise for the reader.) It's symmetric across the $y$-axis, so in reality, we're just looking at two triangles with width and height of 1. Triangle area's just $\frac{1}{2}bh$, and so the area of each triangle is just $0.5$. + + The first is easy, but the second requires some extra work. We still don't need to fuss with complicated integrals, though. + + - If $x \le 0$, the shape is a whole triangle, with height and width equal to $1 - (-x)$, so we can just do $A = \frac{1}{2}bh = \frac{1}{2} (1+x)^2$. + - If $x \ge 0$, we can take the area of both triangles and subtract the area of the triangle not included. This triangle has height and width $1-x$, and so we can do $A = 1 - \frac{1}{2} (1-x)^2$. + - You can merge these two into a single formula using the indicator function and the signum function. That results in an equation of $A = 1_{[-1, 0)}(x) + \frac{1}{2} \text{sgn}(x) (1-|x|)^2$. The piecewise form is easier, though. + + Now that we have a general formula, let's plug it in: + + $$ + \begin{align*} + F(x) &= 1_{[-1, 1]}(x) ~ (1 - 1_{[-1, 0)}(x) - \frac{1}{2} \text{sgn}(x) (1-|x|)^2) \\ + &= 1_{[-1, 1]}(x) ~ \left( 1_{[0, 1]}(x) - \text{sgn}(x) \frac{(1-|x|)^2}{2} \right) \\ + &= 1_{[-1, 1]}(x) ~ \text{sgn}(x) \left(\frac{x^2-2|x|+1}{2} \right) \\ + \end{align*} + $$ + + Or piecewise: + + $$ + F(x) = \begin{cases} + \frac{(1+x)^2}{2}& \text{if } -1 \le x \lt 0 \\ + 1 - \frac{(1-x)^2}{2}& \text{if } 0 \le x \le 1 + \end{cases} + $$ + + And this is our probability function for $P((j_s - j_e) \ge x) = F(x)$. + +12. And finally, compute the probabilities for each $v$. + + Remember the probability value: + + $$ + \begin{align*} + R_s, j_s, j_e & \text{ are uniformly random} \\ + j_s, j_e &\in [0, 1] \\ + v &\in \{0, 0.02\} \\ + S &= R_s \mod 0.1 \\ + D_v &= 1_{[0.00, 0.03) \cup [0.05, 0.08)}(S) ~ \mathrm{P}((j_s - j_e) \ge v) ~ + \\ + &\phantom{=} 1_{[0.03, 0.05)}(S) ~ \mathrm{P}((j_s - j_e) \ge v + 1) ~ + \\ + &\phantom{=} 1_{[0.08, 0.10)}(S) ~ \mathrm{P}((j_s - j_e) \ge v - 1) \\ + p_v &= P(D_v) \\ + \end{align*} + $$ + + First, let's substitute in $P((j_s - j_e) \ge x) = F(x)$: + + $$ + \begin{align*} + D_v &= 1_{[0.00, 0.03) \cup [0.05, 0.08)}(S) ~ F(v) ~ + \\ + &\phantom{=} 1_{[0.03, 0.05)}(S) ~ F(v + 1) ~ + \\ + &\phantom{=} 1_{[0.08, 0.10)}(S) ~ F(v - 1) \\ + \end{align*} + $$ + + Now, let's resolve it for each $v$ and $p_v$: + + $$ + \begin{align*} + D_b &= 1_{[0.00, 0.03) \cup [0.05, 0.08)}(S) ~ F(0) + 1_{[0.03, 0.05)}(S) ~ F(1) + 1_{[0.08, 0.10)}(S) ~ F(-1) \\ + D_a &= 1_{[0.00, 0.03) \cup [0.05, 0.08)}(S) ~ F(0.2) + 1_{[0.03, 0.05)}(S) ~ F(1.02) + 1_{[0.08, 0.10)}(S) ~ F(-0.98) \\ + \end{align*} + $$ + + Each of those $F(x)$s need evaluated: + + $$ + \begin{align*} + F(-1) &= \frac{(1+(-1))^2}{2} \\ + &= 0 \\ + F(-0.98) &= \frac{(1+(-0.98))^2}{2} \\ + &= 0.0002 \\ + F(0) &= \frac{(1+0)^2}{2} \\ + &= 0.5 \\ + F(0.02) &= 1 - \frac{(1-0.02)^2}{2} \\ + &= 0.5198 \\ + F(1) &= 1 - \frac{(1-1)^2}{2} \\ + &= 1 \\ + \end{align*} + $$ + + $F(1.02)$ is 0 as it's out of the range of possibilities. + + Now, to plug them in: + + $$ + \begin{align*} + D_b &= 1_{[0.00, 0.03) \cup [0.05, 0.08)}(S) ~ 0.5 + 1_{[0.03, 0.05)}(S) ~ 1 + 1_{[0.08, 0.10)}(S) ~ 0 \\ + &= \begin{cases} + 1& \text{if } 0.00 \le S \lt 0.03 \\ + 0.5& \text{if } 0.03 \le S \lt 0.05 \\ + 1& \text{if } 0.05 \le S \lt 0.08 \\ + 0& \text{if } 0.08 \le S \lt 0.10 \\ + \end{cases} \\ + D_a &= 1_{[0.00, 0.03) \cup [0.05, 0.08)}(S) ~ 0.5198 + 1_{[0.03, 0.05)}(S) ~ 0 + 1_{[0.08, 0.10)}(S) ~ 0.0002 \\ + &= \begin{cases} + 0.5198& \text{if } 0.00 \le S \lt 0.03 \\ + 0& \text{if } 0.03 \le S \lt 0.05 \\ + 0.5198& \text{if } 0.05 \le S \lt 0.08 \\ + 0.0002& \text{if } 0.08 \le S \lt 0.10 \\ + \end{cases} \\ + \end{align*} + $$ + + And now, we can take these piecewise variables and compute the total probability from them. $S$ is uniform, so it's as simple as multiplying each probability by the span as their weight. + + $$ + \begin{align*} + p_b &= P \left( \begin{cases} + 1& \text{if } 0.00 \le S \lt 0.03 \\ + 0.5& \text{if } 0.03 \le S \lt 0.05 \\ + 1& \text{if } 0.05 \le S \lt 0.08 \\ + 0& \text{if } 0.08 \le S \lt 0.10 \\ + \end{cases} \right) \\ + &= \frac{1 (0.03-0.00) + 0.5 (0.05-0.03) + 1 (0.08-0.05) + 0 (0.10-0.08)}{0.10} \\ + &= 0.7 \\ + p_0 &= 1 - p_b \\ + &= 0.3 \\ + p_a &= P \left( \begin{cases} + 0.5198& \text{if } 0.00 \le S \lt 0.03 \\ + 0& \text{if } 0.03 \le S \lt 0.05 \\ + 0.5198& \text{if } 0.05 \le S \lt 0.08 \\ + 0.0002& \text{if } 0.08 \le S \lt 0.10 \\ + \end{cases} \right) \\ + &= \frac{0.5198 (0.03-0.00) + 0 (0.05-0.03) + 0.5198 (0.08-0.05) + 0.0002 (0.10-0.08)}{0.10} \\ + &= 0.31192 \\ + \end{align*} + $$ + +13. Compute the expected value of the probability distribution. + + The expected value of a continuous distribution is $E[X] = \int_{-\infty}^\infty x ~ \mathrm{d}F(x) = \int_{-\infty}^\infty x f(x) ~ \mathrm{d}x$, where $f(x)$ is the probability density function and $F(x)$ is the corresponding cumulative distribution function. The expected value is a generalization of the weighted average, where you're taking a mean of all the possible values, weighted by their individual probabilities. + + Remember our cumulative distribution function? + + $$ + \begin{align*} + F(x) &= \begin{cases} + \frac{(1+x)^2}{2}& \text{if } -1 \le x \lt 0 \\ + 1 - \frac{(1-x)^2}{2}& \text{if } 0 \le x \le 1 \\ + 0& \text{otherwise} + \end{cases} \\ + &= 1_{[-1, 1]}(x) ~ \text{sgn}(x) \left(\frac{x^2-2|x|+1}{2} \right) \\ + \end{align*} + $$ + + Well, the probability density function is the derivative of that: $f(x) = \frac{\mathrm{d}}{\mathrm{d}x} F(x)$. And it just so happens that we know that derivative already: it's $f_X(t) = (1 - |t|) ~ 1_{[-1,1]}(t)$. + + We can use this and integration by parts to sidestep a lot of work here. Here's the formula for that (remember that $f'(x)$ is shorthand for the derivative of $f(x)$): + + $$ + \begin{align*} + \int_a^b u(x) v'(x) ~ \mathrm{d}x &= \left[ u(x) v(x) \right]_a^b &- \int_a^b u'(x) v(x) \\ + &= u(b)v(b) - u(a)v(a) &- \int_a^b u'(x) v(x) \\ + \end{align*} + $$ + + While the interval is from $-\infty$ to $\infty$, we know the probability is only non-zero from $-1$ to $1$, so we can let $a=-1$ and $b=1$. Chopping up the intervals like this (integrals do have sum and difference rules) lets us cut out even more work. + + So, let's let $u(x) = x$ and $v(x) = F(x)$ (and thus $u'(x) = 1$ and $v'(x) = f_X(x)$), so that when we plug it in to $\int_a^b u(x) v'(x) ~ \mathrm{d}x$, it just happens to come out to $E[X] = \int_a^b x f_X(x) ~ \mathrm{d}x$. Plugging everything in gives us this: + + $$ + \begin{align*} + \int_a^b x f_X(x) ~ \mathrm{d}x &= b F(b) - a F(a) - \int_a^b 1 F(x) ~ \mathrm{d}x \\ + &= 1 F(1) - (-1) F(-1) - \int_{-1}^1 F(x) ~ \mathrm{d}x \\ + &= 1 (1) - (-1) 0 - \int_{-1}^1 F(x) ~ \mathrm{d}x \\ + &= 1 - \int_{-1}^1 F(x) ~ \mathrm{d}x \\ + \end{align*} + $$ + + And to work that integral out: + + $$ + \begin{align*} + E[X] &= \int_a^b x f_X(x) ~ \mathrm{d}x \\ + &= 1 - \int_{-1}^1 F(x) ~ \mathrm{d}x \\ + &= 1 - \int_{-1}^1 \left( \begin{cases} + \frac{(1+x)^2}{2}& \text{if } -1 \le x \lt 0 \\ + 1 - \frac{(1-x)^2}{2}& \text{if } 0 \le x \le 1 + \end{cases} \right) ~ \mathrm{d}x \\ + &= 1 - \left( \int_{-1}^0 \frac{(1+x)^2}{2} ~ \mathrm{d}x + \int_0^1 1 - \frac{(1-x)^2}{2} ~ \mathrm{d}x \right) \\ + &= 1 - \left( \frac{1}{2} \int_{-1}^0 (1+x)^2 ~ \mathrm{d}x + 1 - \frac{1}{2} \int_{-1}^0 x^2 ~ \mathrm{d}x \right) \\ + &= 1 - 1 + \frac{1}{2} \left( \int_0^1 x^2 ~ \mathrm{d}x - \int_{-1}^0 (1-x)^2 ~ \mathrm{d}x \right) \\ + &= 1 - 1 - \frac{1}{2} \left( \int_{-1}^0 (1+x)^2 ~ \mathrm{d}x - \int_0^1 (1-x)^2 ~ \mathrm{d}x \right) \\ + &= \frac{1}{2} \left( \int_0^1 (1-x)^2 ~ \mathrm{d}x - \int_{-1}^0 (1+x)^2 ~ \mathrm{d}x \right) \\ + &= \frac{1}{2} \left( \left[ -\frac{1}{3} (1-x)^3 \right]_0^1 - \left[ \frac{1}{3} (1+x)^3 \right]_{-1}^0 \right) \\ + &= \frac{1}{2} \left( \left( 0 - (-\frac{1}{3}) \right) - \left( \frac{1}{3} - 0 \right) \right) \\ + &= 0 \\ + \end{align*} + $$ + + And thus, $E[X] = 0$. + + > Notes: + > - [Cavalieri's quadrature formula](https://en.wikipedia.org/wiki/Cavalieri%27s_quadrature_formula) states that $\int x^n ~ \mathrm{d}x = x^{n+1} / (n+1) + C$ for $n \ge 0$, or $\int x^2 ~ \mathrm{d}x = x^3/3 + C$ in this case. This uses a minor variant based on that, $\int (ax+b)^n ~ \mathrm{d}x = (ax+b)^{n+1} / a(n+1) + C$. + > - This uses the fundamental theorem of calculus, $\int_a^b f(x) ~ \mathrm{d}x = F(b) - F(a)$ where $\frac{\mathrm{d}}{\mathrm{d}x} F(x) = f(x)$ or, equivalently, $\int f(x) ~ \mathrm{d}x = F(x)$. $[F(x)]_a^b = F(b) - F(a)$ is a common shorthand. +
+ +So in other words, we can't just blindly rely on the obvious way of measuring time spans. It can give us a lot of outliers, and for sufficiently fast code segments, it just outright breaks down. diff --git a/performance/bench.js b/performance/bench.js index 53a8db41a..43512a5ee 100644 --- a/performance/bench.js +++ b/performance/bench.js @@ -1,194 +1,361 @@ -/* -Rolling my own benchmark system, so I can minimize overhead and have something actually maintained. - -Things it does better than Benchmark.js: - -- It uses an exposed benchmark loop, so I can precisely control inter-frame delays. -- It prints out much more useful output: a confidence interval-based range and a total run count. -- It works around the low resolution inherent to modern browsers. +// See the README for some details about the design of this. /* global performance */ +import {currentDisplayStats, getSamples, pushSample, resetStats} from "./stats.js" +import {serializeNumber, serializeRate, serializeTime} from "./serialized.js" +import {renderCompleted} from "./chart.js" + // Note: this should be even. Odd counts will be rounded up to even. -const initSamples = 10 -const minSamples = 40 -const minDuration = 500 -const maxDuration = 5000 -const minConfidence = 0.98 -// I don't feel like doing the calculus, so I've used a confidence interval table for this. -// (They're ridiculously easy to find.) -const criticalValueForMinConfidence = 2.33 +const initSamples = 50 + +// Don't want to wait all day for this if the browser has some sort of bug or if the system's too +// loaded. This comes out to a minimum frame rate of 1 FPS - not even E-ink displays under heavy +// load are normally that slow. +// +// As the frame initialization loop waits for 100 samples, this enforces an upper bound of 20 +// seconds. +// +// Browsers do throttle their tests when de-focused, though, so this may trip in those cases. +const maxFrameDeltaTolerable = 1000 // I want some level of resolution to be able to have reasonable results. 2ms can still be remotely // useful, but Firefox's reduced fingerprinting preference clamping of 100ms is far too high. const minResolutionTolerable = 2 -let secondMax = 0 -let max = 0 -let min = Infinity +const runtimeMinSamples = 100 +const runtimeMinDuration = 1000 +const runtimeMaxDuration = 5000 +const runtimeMinConfidence = 0.99 +// Give me at least 15 units of granularity per tick, to avoid risking precision issues. +const runtimeMinGranularityUnitsPerTick = 15 -// Try for 100 samples and dispose the highest if it's considerably higher than the second highest. -for (let i = 0; i < 100; i++) { - const start = performance.now() - let diff = 0 - while (diff <= 0) { - diff = performance.now() - start +async function checkResolution(sampleCount, assumedQuantile, maxTolerable, errorTemplate, fetchSample) { + if (Math.log10(sampleCount - 1) % 1 !== 0) { + throw new Error("Expected sample count to be one greater than a power of 10") } - if (max > minResolutionTolerable && diff > minResolutionTolerable) { - throw new Error("Resolution is too coarse to be useful for measurement") + + const samples = new Float64Array(sampleCount) + maxTolerable = Number(maxTolerable) + errorTemplate = `${errorTemplate}` + let size = 0 + + for (;;) { + const sample = await fetchSample() + if (sample > maxTolerable) { + throw new RangeError(errorTemplate.replace("%", sample)) + } + + samples[size++] = sample + if (size === samples.length) { + samples.sort() + + const max = size - 1 + + return { + min: samples[0], + max: samples[max], + median: samples[max * 0.5], + assumed: samples[Math.round(max * assumedQuantile)], + } + } } - if (secondMax < max) secondMax = max - if (max < diff) max = diff - if (min > diff) min = diff } -if (min > 0.999) { - console.log(`Timer resolution detected: ${min > 999 ? min : min.toPrecision(3)}ms`) -} else if (min > 0.000999) { - console.log(`Timer resolution detected: ${(min * 1000).toPrecision(3)}µs`) -} else { - console.log(`Timer resolution detected: ${Math.round(min * 1000000)}ns`) +function getTimerGranularity() { + return checkResolution( + 1001, + // Grab the 90th percentile to base the granularity on, as that should reasonably + // represent the worst case in practice. + 0.9, + minResolutionTolerable, + "Resolution of % ms is too coarse to be useful for measurement.", + () => { + const start = performance.now() + let diff = 0 + while (diff <= 0) { + diff = performance.now() - start + } + return diff + } + ) +} + +function nextFrame() { + return new Promise((resolve) => requestAnimationFrame(resolve)) } -// Give me at least 15 units of resolution to be useful. -const minDurationPerPass = min * 15 +async function getFrameInterval() { + if (typeof requestAnimationFrame !== "function") return + let storedTimestamp = await nextFrame() + return checkResolution( + // Only wait for 100 frames. + // - 240Hz comes out to about 0.41 seconds + // - 144Hz comes out to about 0.69 seconds + // - 60Hz comes out to about 1.67 seconds + // - 30Hz comes out to about 3.33 seconds + 101, + // Grab the 10th percentile to base the interval on, as that should reasonably + // represent the worst case in practice. + 0.1, + maxFrameDeltaTolerable, + "Frame delta of % ms will take too long to initialize with.", + async () => { + const prev = storedTimestamp + const next = storedTimestamp = await nextFrame() + return next - prev + } + ) +} // Uses West's weighted variance algorithm for computing variance. Each duration sample is given the. // Ref: https://doi.org/10.1145%2F359146.359153 -class BenchState { - constructor(minSamples, minDuration, maxDuration, minConfidence) { - if (minSamples < 2) { - throw new RangeError("At least two samples are required to compute variance.") - } - - // Convert the confidence into a critical value for fast margin of error comparison. +/** + * @typedef BenchSpec + * @property {() => void} [tick] + * This is run before each tick loop. + * @property {() => void} fn + * This is the test being benchmarked. + */ - /** @private */ this._minSamples = minSamples - /** @private */ this._minDuration = minDuration - /** @private */ this._maxDuration = maxDuration - /** @private */ this._minConfidence = minConfidence +/** + * @typedef BenchOptions + * @property {number} minSamples + * @property {number} minDuration + * @property {number} maxDuration + * @property {number} minConfidence + * @property {number} minDurationPerPass + */ - /** @private */ this._testStart = performance.now() - /** @private */ this._multi = 0 - /** @private */ this._start = 0 +// To serve as a black hole for benchmarks, with as little overhead as pragmatically possible. +// Gets added and immediately removed from the `performance` global at the very end, but prevents +// the function body from being able to develop an IC to remove its result. +const benchResultSym = Symbol() +let benchResult - /** @private */ this._mean = 0 - /** @private */ this._count = 0 - /** @private */ this._wsum2 = 0 - /** @private */ this._s = 0 +/** + * @param {BenchSpec} spec + * @param {BenchOptions} options + */ +async function runSpec({tick, fn}, options) { + if (options.minSamples < 2) { + throw new RangeError("At least two samples are required to compute variance.") } - stats() { - // Find the margin of error. Applies Bessel's correction as it's a frequency weight. - const stdError = Math.sqrt(this._s / ((this._count - 1) * this._count)) - return { - ticks: this._count, - mean: this._mean, - marginOfError: stdError * criticalValueForMinConfidence, + const testStart = performance.now() + resetStats(options) + const minDurationPerPass = options.minDurationPerPass + + for (;;) { + if (typeof tick === "function") { + tick() } - } - done() { - const count = this._count - if (count < this._minSamples) return false - const duration = performance.now() - this._testStart - if (duration >= this._maxDuration) return true - if (duration < this._minDuration) return false - // Find the margin of error. Applies Bessel's correction as it's a frequency weight. - const stdError = Math.sqrt(this._s / ((count - 1) * count)) - const marginOfError = stdError * criticalValueForMinConfidence - return marginOfError / this._mean >= this._minConfidence - } + // Yield for I/O and give an opportunity for GC. Also, in browsers, give a chance for the + // frame to render. + await nextFrame() - start() { - this._start = performance.now() - this._multi = 0 - } + const start = performance.now() + let multi = 0 + let sample, now - tick() { - let sample = performance.now() - this._start - this._multi++ - if (sample < minDurationPerPass) return false - - const weight = this._multi - const meanOld = this._mean - sample /= weight - this._count += weight - this._wsum2 += weight * weight - this._mean = meanOld + (weight / this._count) * (sample - meanOld) - this._s += weight * (sample - meanOld) * (sample - this._mean) - return true + do { + benchResult = fn() + now = performance.now() + multi++ + } while ((sample = now - start) < minDurationPerPass) + + if (pushSample(multi, sample, now - testStart)) return } } /** - * @param {{[key: string]: (state: BenchState) => void | PromiseLike}} tests + * @param {{[key: string]: BenchSpec}} tests */ -export async function runBenchmarks(tests) { +async function runSpecs(tests) { + const start = performance.now() + + // Options must be passed in the query string in browsers (like `?print-raw`) and passed via a + // command line argument in Node and Deno. + // + // (Why add Deno compatibility? Just a bit of future proofing, that's all.) + const testCount = Object.keys(tests).length console.log(`${testCount} test${testCount === 1 ? "" : "s"} loaded`) - const start = performance.now() + const granularity = await getTimerGranularity() + + console.log( + "Timer resolution detected:" + + `\n- min: ${serializeRate(granularity.min, "tick")}` + + `\n- max: ${serializeRate(granularity.max, "tick")}` + + `\n- median: ${serializeRate(granularity.median, "tick")}` + + `\n- assumed: ${serializeRate(granularity.assumed, "tick")}` + ) + + const frameInterval = await getFrameInterval() + + if (frameInterval) { + console.log( + "Frame interval detected:" + + `\n- min: ${serializeRate(frameInterval.min, "frame")}` + + `\n- max: ${serializeRate(frameInterval.max, "frame")}` + + `\n- median: ${serializeRate(frameInterval.median, "frame")}` + + `\n- assumed: ${serializeRate(frameInterval.assumed, "frame")}` + ) + } + + /** @type {BenchOptions} */ + const options = { + minSamples: 2, + minDuration: 0, + maxDuration: 0, + minConfidence: 0, + minDurationPerPass: 0, + } + + /** @type {Array<[string, BenchSpec]>} */ + const specList = [ + ["*** null test ***", { + tick() {}, + fn: () => "test", + }], + ...Object.entries(tests), + ] + + // Adjust their names for easier debugging of errors + for (const [name, spec] of specList) { + if (typeof spec.tick === "function") { + Object.defineProperty(spec.tick, "name", {value: `${name} (tick)`}) + } + Object.defineProperty(spec.fn, "name", {value: name}) + } // Minimize sample count within the warm-up loop, so ICs receive the right runtime // information. - let failed = false + const failed = new Set() for (let i = 0; i < initSamples; i += 2) { - for (const [name, test] of Object.entries(tests)) { + for (const entry of specList) { + if (failed.has(entry)) continue try { - await test(new BenchState(2, 0, Infinity, 0)) + await runSpec(entry[1], options) } catch (e) { - failed = true - console.error(`Error while warming up ${name}:`) + failed.add(entry) console.error(e) } } } - if (failed) return + if (failed.size) return + + // Update the options in-place, so they can retain the same shape and not cause `runSpec` to + // recompile. + options.minSamples = runtimeMinSamples + options.minDuration = runtimeMinDuration + options.maxDuration = runtimeMaxDuration + options.minConfidence = runtimeMinConfidence + // Give me at least 15 units of granularity per tick, to avoid risking precision issues. + options.minDurationPerPass = granularity.assumed * runtimeMinGranularityUnitsPerTick + if (frameInterval) { + options.minDurationPerPass = Math.max(options.minDurationPerPass, frameInterval.assumed * 0.7) + } + + console.log( + "Tests warmed up, starting benchmark" + + `\n- min confidence level: ${options.minConfidence}` + + `\n- min samples/test: ${options.minSamples}` + + `\n- min duration/test: ${serializeTime(options.minDuration)}` + + `\n- max duration/test: ${serializeTime(options.maxDuration)}` + + `\n- min duration/pass: ${serializeTime(options.minDurationPerPass)}` + ) - console.log("Tests warmed up") + /** @type {import("./chart.js").StatEntry[]} */ + const statEntries = [] + let nullStats - for (const [name, test] of Object.entries(tests)) { - const state = new BenchState(minSamples, minDuration, maxDuration, minConfidence) + for (const [name, spec] of specList) { // Let errors here crash the benchmark. - await test(state) - const {mean, marginOfError, ticks} = state.stats() - - const min = mean - marginOfError - const max = mean + marginOfError - - const maxOps = Math.floor(1000 / min).toLocaleString(undefined, {useGrouping: true}) - const minOps = Math.floor(1000 / max).toLocaleString(undefined, {useGrouping: true}) - - let minDisplay = min - let maxDisplay = max - let unit = "ms" - - if (maxDisplay < 1) { - minDisplay *= 1000 - maxDisplay *= 1000 - unit = "µs" - if (maxDisplay < 1) { - minDisplay *= 1000 - maxDisplay *= 1000 - unit = "ns" - } + await runSpec(spec, options) + const stats = currentDisplayStats() + + console.log(`[${name}]: +- mean: ${serializeRate(stats.mean, "op")} +- median: ${serializeRate(stats.median, "op")} +- expected: ${serializeRate(stats.expMin, "op")} to ${serializeRate(stats.expMax, "op")} +- range: ${serializeRate(stats.min, "op")} to ${serializeRate(stats.max, "op")} +- CI: ${serializeRate(stats.confidenceMin, "op")} to ${serializeRate(stats.confidenceMax, "op")}${ + // Not perfect adjustment, but good enough to work as a heuristic. The non-adjusted + // variants are the true unbiased statistics. + !nullStats ? "" : ` +- null-adjusted mean: ${serializeRate(stats.mean - nullStats.mean, "op")} +- null-adjusted median: ${serializeRate(stats.median - nullStats.median, "op")} +- null-adjusted expected: ${serializeRate(stats.expMin - nullStats.expMax, "op")} to ${serializeRate(stats.expMax - nullStats.expMin, "op")} +- null-adjusted range: ${serializeRate(stats.min - nullStats.max, "op")} to ${serializeRate(stats.max - nullStats.min, "op")} +- null-adjusted CI: ${serializeRate(stats.confidenceMin - nullStats.confidenceMax, "op")} to ${serializeRate(stats.confidenceMax - nullStats.confidenceMin, "op")}`} +- MOE: ${serializeTime(stats.moe)}/op +- N: ${serializeNumber(stats.n)} +- pop: ${serializeNumber(stats.pop)}`) + + if (statEntries) { + statEntries.push({ + name, + stats, + samples: getSamples(), + }) } - minDisplay = minDisplay.toPrecision(3) - maxDisplay = maxDisplay.toPrecision(3) + nullStats = stats + } + + performance[benchResultSym] = benchResult + delete performance[benchResultSym] - const timeSpan = minDisplay === maxDisplay ? minDisplay : `${minDisplay}-${maxDisplay}` - const opsSpan = minOps === maxOps ? minOps : `${minOps}-${maxOps}` + console.log(`Benchmark run completed in ${serializeTime(performance.now() - start)}`) - console.log(`${name}: ${timeSpan} ${unit}/op, ${opsSpan} op/s, n = ${ticks.toLocaleString()}`) + if (statEntries) { + let result = "Sample CSVs:\n" + for (const entry of statEntries) { + result = `${result}>${entry.name}\ncount,sum\n` + for (const sample of entry.samples) { + result = `${result}${sample.count},${sample.sum}\n` + } + } + console.log(result) } - const end = performance.now() + return statEntries +} + +/** + * @param {{[key: string]: BenchSpec}} tests + */ +export function setupBenchmarks(setup, cleanup, benchmarks) { + async function run() { + document.body.innerHTML = "Benchmarks in progress. Leave console closed." + + await setup() + let completed = 0 + let statEntries + try { + statEntries = await runSpecs(benchmarks) + completed++ + } finally { + try { + await cleanup() + completed++ + } finally { + document.body.innerHTML = + `Benchmarks ${completed < 2 ? "errored" : "completed"}. See console.` + + if (statEntries) { + renderCompleted(statEntries) + } + } + } + } - console.log(`Test completed in ${Math.round((end - start) / 1000)}s`) + window.addEventListener("load", run, {once: true}) } diff --git a/performance/chart.css b/performance/chart.css new file mode 100644 index 000000000..7a372ae5d --- /dev/null +++ b/performance/chart.css @@ -0,0 +1,30 @@ +@charset "utf-8"; + +.root, +.entry { + display: flex; + flex-flow: column nowrap; + align-items: center; +} + +.entry:not(:first-child) { + margin: 1em; +} + +.entry h2 { + text-align: center; +} + +.entry table { + margin-top: 1em; + border-collapse: collapse; +} + +.entry td { + border: 1px solid black; + padding: 0.2em 0.5em; +} + +.entry td:first-child { + text-align: right; +} diff --git a/performance/chart.js b/performance/chart.js new file mode 100644 index 000000000..653b477a9 --- /dev/null +++ b/performance/chart.js @@ -0,0 +1,262 @@ +// As much as I'd like to use Mithril here, I can't. This part is specially desiged to allow for +// past and customized versions of Mithril to also be used in private benchmarks. +/* eslint-env browser */ + +import {serializeNumber, serializeRate, serializeTime} from "./serialized.js" + +const OUTLIER_THRESHOLD = 1.5 + +/** + * @typedef StatEntry + * @property {string} name + * @property {ReturnType} stats + * @property {ReturnType} samples + */ + +function toDecimal(value) { + // Drop whatever roundoff error might exist. + return Number(value).toFixed(value < 10 ? 2 : value < 100 ? 1 : 0) +} + +function genLabelSet(count, fn) { + const labels = Array.from({length: count}, (_, i) => fn(i)) + if (labels.every((l) => (/\d(?:\.0+)?(?:\D|$)/).test(l))) { + return labels.map((l) => l.replace(/\.0+(\D|$)/, "$1")) + } else if (labels.every((l) => (/\d\.[1-9]0+\D/).test(l))) { + return labels.map((l) => l.replace(/(\.[1-9])0+(\D|$)/, "$1$2")) + } else { + return labels + } +} + +/** @param {StatEntry} entry */ +function generateChart(entry) { + let i = entry.samples.length - 2 + let maxDuration = entry.samples[i + 1].sum / entry.samples[i + 1].count + let nextDuration = entry.samples[i].sum / entry.samples[i].count + + while (maxDuration > nextDuration * OUTLIER_THRESHOLD) { + if (i === 0) { + // This should never occur in practice. + throw new Error("Failed to find max duration - all data points are too sparse.") + } + i-- + maxDuration = nextDuration + nextDuration = entry.samples[i].sum / entry.samples[i].count + } + + let unit = "ms" + let scale = 1 + + if (maxDuration < 0.001) { + unit = "ns" + scale = 1000000.0 + } else if (maxDuration < 1) { + unit = "µs" + scale = 1000.0 + } else if (maxDuration >= 1000) { + unit = "s" + scale = 0.001 + } + + // This performs a linear interpolation/extrapolation from point (len=50, value=4) to point + // (len=100,value=2), clamps it to the interval 0.5 <= x <= 4, and returns the square root + // of it. This scales by area rather than by radius, making for a nicer and more readable + // chart regardless of point count. + // + // Since it's always the same points, I plugged one of them and simplified it so it's just a + // one-liner. Here's the relevant formulas - I'll let you the reader (re-)derive it if you + // really want to: + // - Slope betwen two points: m = (y2-y1)/(x2-x1) + // - Point slope: y-y1 = m*(x-x1) + // - Slope intercept: y = m*x + b + const size = Math.sqrt(Math.max(0.5, Math.min(4, 6 - entry.samples.length / 25))) + + const $canvas = document.createElement("canvas") + + // 360p is a nice canvas size to work with. Not too wide, not too narrow, not too tall, and not + // too short. + const height = 360 + const width = 640 + + const ctx = $canvas.getContext("2d") + + if (!ctx) { + throw new Error("2D context not available") + } + + // Quick dance to deal with high-DPI devices, so 1px lines don't look blurry. + $canvas.height = height * devicePixelRatio + $canvas.width = width * devicePixelRatio + $canvas.style.height = `${height}px` + $canvas.style.width = `${width}px` + ctx.scale(devicePixelRatio, devicePixelRatio) + + const segmentCount = 10 + + const topPad = 10 + const rightPad = 25 + const yLabelPadding = 10 + const xLabelPadding = 10 + const tickOverhang = 5 + + const xSegmentScale = entry.stats.pop / (segmentCount - 1) + const ySegmentScale = maxDuration / (segmentCount - 1) * scale + + const xLabels = genLabelSet(segmentCount, (i) => { + const value = xSegmentScale * i + // This is for an integer count. Even if everything else is decimal, I at least want this + // to be a fixed integer. Makes the chart look better IMHO. + if (value === 0) return "0" + if (value < 1000) return toDecimal(value) + if (value < 1000000) return `${toDecimal(value / 1000)}K` + return `${toDecimal(value / 1000000)}M` + }) + + const yLabels = genLabelSet(segmentCount, (i) => ( + `${toDecimal(ySegmentScale * i)} ${unit}` + )) + + const xLabelHeight = 12 + let yLabelWidth = 0 + + ctx.font = `${xLabelHeight}px sans-serif` + + for (const label of yLabels) { + yLabelWidth = Math.max(yLabelWidth, ctx.measureText(label).width) + } + + const chartWidth = width - tickOverhang - yLabelWidth - yLabelPadding * 2 - rightPad + const chartHeight = height - tickOverhang - xLabelHeight - xLabelPadding * 2 - topPad + const xSegmentSize = chartWidth / (segmentCount - 1) + const ySegmentSize = chartHeight / (segmentCount - 1) + const chartXOffset = yLabelWidth + yLabelPadding * 2 + + ctx.beginPath() + ctx.lineWidth = 1 + ctx.strokeStyle = "#bbb" + ctx.fillStyle = "none" + + for (let i = 0; i < segmentCount; i++) { + ctx.moveTo(chartXOffset + tickOverhang + i * xSegmentSize, topPad) + ctx.lineTo(chartXOffset + tickOverhang + i * xSegmentSize, topPad + chartHeight + tickOverhang) + ctx.moveTo(chartXOffset, topPad + i * ySegmentSize) + ctx.lineTo(chartXOffset + tickOverhang + chartWidth, topPad + i * ySegmentSize) + } + + ctx.stroke() + ctx.closePath() + + ctx.fillStyle = "#888" + ctx.textBaseline = "middle" + ctx.textAlign = "right" + const yLabelOffset = chartXOffset - yLabelPadding + yLabels.reverse() + for (let i = 0; i < segmentCount; i++) { + ctx.fillText(yLabels[i], yLabelOffset, topPad + i * ySegmentSize) + } + ctx.textAlign = "center" + ctx.textBaseline = "bottom" + const xLabelOffset = height - xLabelPadding + for (let i = 0; i < segmentCount; i++) { + ctx.fillText(xLabels[i], chartXOffset + tickOverhang + i * xSegmentSize, xLabelOffset) + } + + ctx.beginPath() + + const xMin = chartXOffset + tickOverhang + const yMin = height - tickOverhang - xLabelPadding * 2 - xLabelHeight + const xMax = width - rightPad + const yMax = topPad + const sx = (xMax - xMin) / (xSegmentScale * (segmentCount - 1)) + const sy = (yMax - yMin) / (ySegmentScale * (segmentCount - 1)) + + ctx.fillStyle = "#c00" + + let index = 0 + + for (const {count, sum} of entry.samples) { + const x0 = index + index += count + const x1 = index + const x = (x0 + x1) / 2 + const y = sum / count + const px = xMin + sx * x + const py = yMin + sy * y * scale + ctx.moveTo(px, py) + ctx.ellipse(px, py, size, size, 0, 0, 2*Math.PI) + } + + ctx.fill() + ctx.closePath() + + return $canvas +} + +function metricRow(label, values) { + const $result = document.createElement("tr") + const $label = document.createElement("td") + const $values = document.createElement("td") + $label.textContent = label + $values.textContent = values + $result.append($label, $values) + return $result +} + +/** + * @param {StatEntry[]} statEntries + */ +export function renderCompleted(statEntries) { + const style = document.createElement("link") + style.rel = "stylesheet" + style.href = "/performance/chart.css" + document.head.append(style) + + const $root = document.createElement("div") + $root.className = "root" + + const nullEntry = statEntries[0] + + for (const entry of statEntries) { + const stats = entry.stats + const nullStats = nullEntry.stats + + const $chart = generateChart(entry) + const $entry = document.createElement("div") + const $header = document.createElement("h2") + const $table = document.createElement("table") + + $entry.className = "entry" + + $header.textContent = entry.name + + $table.append( + metricRow("Mean (raw)", `${serializeRate(stats.mean, "op")}`), + metricRow("Median (raw)", `${serializeRate(stats.median, "op")}`), + metricRow("Expected (raw)", `${serializeRate(stats.expMin, "op")} to ${serializeRate(stats.expMax, "op")}`), + metricRow("Range (raw)", `${serializeRate(stats.min, "op")} to ${serializeRate(stats.max, "op")}`), + metricRow("Confidence interval (raw)", `${serializeRate(stats.confidenceMin, "op")} to ${serializeRate(stats.confidenceMax, "op")}`) + ) + + if (entry !== nullEntry) { + $table.append( + metricRow("Mean (null-adjusted)", `${serializeRate(stats.mean - nullStats.mean, "op")}`), + metricRow("Median (null-adjusted)", `${serializeRate(stats.median - nullStats.median, "op")}`), + metricRow("Expected (null-adjusted)", `${serializeRate(stats.expMin - nullStats.expMax, "op")} to ${serializeRate(stats.expMax - nullStats.expMin, "op")}`), + metricRow("Range (null-adjusted)", `${serializeRate(stats.min - nullStats.max, "op")} to ${serializeRate(stats.max - nullStats.min, "op")}`), + metricRow("Confidence interval (null-adjusted)", `${serializeRate(stats.confidenceMin - nullStats.confidenceMax, "op")} to ${serializeRate(stats.confidenceMax - nullStats.confidenceMin, "op")}`) + ) + } + + $table.append( + metricRow("Margin of error", `${serializeTime(stats.moe)}/op`), + metricRow("Sample count", `${serializeNumber(stats.n)}`), + metricRow("Population size", `${serializeNumber(stats.pop)}`) + ) + + $entry.append($header, $chart, $table) + $root.append($entry) + } + + document.body.append($root) +} diff --git a/performance/index.html b/performance/index.html index a685e32eb..ae98ccb61 100644 --- a/performance/index.html +++ b/performance/index.html @@ -6,6 +6,7 @@ - Open the browser console. + Initializing tests. If this text doesn't change, open the browser console and check for + load errors. diff --git a/performance/routes.js b/performance/routes.js new file mode 100644 index 000000000..fb0a293ee --- /dev/null +++ b/performance/routes.js @@ -0,0 +1,24 @@ +// This makes this reusable across both the standard benchmark test set and any local ports and +// throwaway benchmark scripts that may be created. + +let routesJson = "" +let stringVarsJson = "" +let numVarsJson = "" +let templatesJson = "" + +for (let i = 0; i < 16; i++) { + for (let j = 0; j < 16; j++) { + templatesJson += `,"/foo${i}/:id${i}/bar${j}/:sub${j}"` + routesJson += `,"/foo${i}/${i}/bar${j}/${j}"` + stringVarsJson += `,{"id${i}":"${i}","sub${j}":"${j}"}` + numVarsJson += `,{"id${i}":${i},"sub${j}":${j}}` + } +} + +// Flatten everything, since they're usually flat strings in practice. +export const {routes, stringVars, numVars, templates} = JSON.parse(`{ +"routes":[${routesJson.slice(1)}], +"templates":[${templatesJson.slice(1)}], +"stringVars":[${stringVarsJson.slice(1)}], +"numVars":[${numVarsJson.slice(1)}] +}`) diff --git a/performance/serialized.js b/performance/serialized.js new file mode 100644 index 000000000..0720fb2ac --- /dev/null +++ b/performance/serialized.js @@ -0,0 +1,34 @@ +/** @param {number} value */ +export function serializeTime(value) { + let sign = "" + let unit = "ms" + let precision = 0 + + if (value < 0) { + sign = "-" + value = -value + } + + if (value >= 1000) { + value /= 1000 + unit = "s" + } else if (value >= 0.995) { + precision = 3 + } else if (value >= 0.000995) { + precision = 3 + value *= 1000 + unit = "µs" + } else { + precision = value >= 0.000000995 ? 3 : 2 + value *= 1000000 + unit = "ns" + } + + return `${sign}${precision ? value.toPrecision(precision) : `${Math.round(value)}`} ${unit}` +} + +export const serializeNumber = new Intl.NumberFormat(undefined, {useGrouping: true}).format + +export function serializeRate(value, op) { + return `${serializeTime(value)}/${op} (${serializeNumber(Math.round(1000 / value))} Hz)` +} diff --git a/performance/stats.js b/performance/stats.js new file mode 100644 index 000000000..32a04d56b --- /dev/null +++ b/performance/stats.js @@ -0,0 +1,451 @@ +/** @type {Array<{count: number, sum: number}>} */ +const samples = [] +let ticks = 0 +let minSamples = 0 +let minDuration = 0 +let maxDuration = 0 +let minConfidence = 0 +let meanSum = 0 + +/** @param {import("./bench.js").BenchOptions} options */ +export function resetStats(options) { + samples.length = 0 + ticks = 0 + minSamples = options.minSamples + minDuration = options.minDuration + maxDuration = options.maxDuration + minConfidence = options.minConfidence + meanSum = 0 +} + +// Returns the population mean. The population mean is what you get when you take all the +// samples and multiply them by their probabilities. +// +// We don't have all the samples here, and for some, there's a lot of holes, possibly thousands +// of them per sample. Fortunately, math comes to the rescue, letting us not try to impute all +// that missing data. +// +// - The probability of each tick is literally just `P(v) = 1 / N`, where `N` is the total +// number of ticks. +// - Each sample represents a group of ticks that sums up to `value`, giving us a formula of +// `value = sum(0 <= i < N: x[i])`. +// - The population mean is the sample sum weighted by its probability. For this, the formula +// would be `mean = sum(0 <= i < N: x[i]) * P(x[i])`. Substituting `P(v)` in, that becomes +// `mean = sum(0 <= i < N: x[i] / N)`. This happens to be precisely the arithmetic mean. +// - The whole mean is `mean = sum(0 <= j < N: y[j] * P(y[j]))`, or as per above, +// `mean = sum(0 <= j < N: y[j] / N)`. This also is the same as the arithmetic mean of the +// inner data. +// - Merging unweighted arithmetic means (like the two above) together is very simple: take a +// weighted arithmetic mean of the means, with their weights being their sample counts. +// +// So, the population mean is just that weighted arithmetic mean. Or, in other words, where `s` +// is the list of samples: +// +// ``` +// weightSum = sum(0 <= i < count(s): count(s[i])) +// = N +// weight(v) = count(v) / weightSum +// mean = sum(0 <= i < count(s): sum(s[i]) * weight(s[i])) / N +// ``` +// +// That of course can be optimized a bit, and it's what this formula uses: +// +// ``` +// mean = sum(0 <= i < count(s): sum(s[i]) * count(s[i])) / (N^2) +// ``` +// +// Note that the above sum is built incrementally in `pushSample`. It's not generated here in this +// function - this function's intentionally cheap. +function mean() { + return meanSum / (ticks * ticks) +} + +/** + * @param {number} count + * @param {number} sum + * @param {number} duration + */ +export function pushSample(count, sum, duration) { + // Performs binary search to find the index to insert into. After the loop, the number of + // elements greater than the value is `sampleCount - R`, and the number of elements less or + // equal to the value is `R`. `R` is thus the index we want to insert at, to retain + // insertion order while still remaining sorted. + + /* eslint-disable no-bitwise */ + + let L = 0 + let R = samples.length + + while (L < R) { + // eslint-disable-next-line no-bitwise + const m = (L >>> 1) + (R >>> 1) + (L & R & 1) + if (samples[m].sum / samples[m].count > sum / count) { + R = m + } else { + L = m + 1 + } + } + + /* eslint-enable no-bitwise */ + + samples.splice(R, 0, {count, sum}) + + ticks += count + meanSum += sum * count + + if (samples.length >= minSamples) { + if (duration >= maxDuration) return true + if (duration >= minDuration) { + if (marginOfError() / mean() >= minConfidence) return true + } + } + + return false +} + +// The quantile is required for the margin of error calculation and the median both, and it's +// pretty easy to compute when all samples are known: +// +// ``` +// i = q * count(s) +// j = floor(i) +// k = ceil(i) +// quantile[q] = s[i], if j = k +// = s[j] * (i - j) + s[k] * (k - i), if j ≠ k +// ``` +// +// We don't have all samples, so we're estimating it through linear interpolation. Each sample +// value is treated as a midpoint, and the count is the span the midpoint covers. Given a sample +// list of `{count: 1, mean: 0.5}, {count: 3, mean: 1}, {count: 2, mean: 2}`, we'll have a domain +// from 0 inclusive to 6 exclusive. (In reality, the values are stored as `mean * count`, not as +// `mean` directly. But means are easier to explain here) Here's a graph of this list, with each +// span's midpoints: +// +// +-----------------------------------+ +// | | +// 2 | +-----*-----| +// | | +// | | +// | | +// 1 | +--------*--------+ | +// | | +// |--*--+ | +// | | +// 0 +-----------------------------------+ +// 0 1 2 3 4 5 6 +// +// What we're actually calculating is the linear interpolation of the midpoints. At the edges, +// we'll extrapolate the previous line to the sides. Samples are always sorted by value, so this +// is safe and will never be negative on the right side. +// +// +-----------------------------------+ +// | _-¯| +// 2 | _*¯ | +// | _-¯ | +// | _-¯ | +// | _-¯ | +// 1 | _ *¯ | +// | _ - ¯ | +// | * ¯ | +// | | +// 0 +-----------------------------------+ +// 0 1 2 3 4 5 6 +// +// Suppose we're looking for a quantile of 0.5, the estimated median. That would reside at offset +// 3. The midpoints we'd be interpolating are (2.5, 1) and (5, 2), giving us a segment span of 3.5. +// Interpolating this gives us a formula of `1*((3-2.5)/3.5)+2*((5-3)/3.5)`, which comes out to +// 9/7 or about 1.2857142857142856. +// +// +-----------------------------------+ +// | _-¯| +// 2 | _*¯ | +// | _-¯ | +// | _-¯ | +// | _@¯ | +// 1 | _ *¯ | +// | _ - ¯ | +// | * ¯ | +// | | +// 0 +-----------------------------------+ +// 0 1 2 3 4 5 6 +// +// Suppose we're looking for a quantile of 0.98 instead. That would reside at offset 0.98*6 = 5.88, +// exceeding the midpoint (5) of the last span (4 to 6). In this case, we need to take the last two +// points, (2.5, 1) and (5, 2), and extrapolate the line they form out to x=5.88. +// +// +-----------------------------------+ +// | _-@| +// 2 | _*¯ | +// | _-¯ | +// | _-¯ | +// | _-¯ | +// 1 | _ *¯ | +// | _ - ¯ | +// | * ¯ | +// | | +// 0 +-----------------------------------+ +// 0 1 2 3 4 5 6 +// +// The slope between those two points is `m=(2-1)/(5-2.5)`, or 0.4. We could plug this into +// point-slope form and get an equation right away, but we need a direct formula for `y` in terms +// of `x` for the code, so we need to solve for that. +// +// ``` +// y = m*x+c <- What we want +// y-y1 = m*(x-x1) <- What we have +// y = m*(x-x1)+y1 <- We can use this as our formula +// ``` +// +// Our `(x1, y1)` can be either (2.5, 1) or (5, 2), doesn't matter. Applying this formula using the +// point (5, 2) comes out to exactly 2.352. +// +// How does this translate to code? Well, each count is a relative offset from the previous index. +// We need to scan and do an incremental sum. The span start value is the sum before adding the +// count, and the span end is the sum after adding the count. This below generates all the +// coordinates. +// +// ``` +// let sum = 0 +// for (const sample of samples) { +// const start = sum +// const end = sum + getCount(sample) +// const value = getValue(sample) +// const coordinate = {x: (end + start) / 2, y: value} +// +// sum += getCount(sample) +// } +// ``` +// +// To do the interpolation, we need to track the previous coordinate. +// +// ``` +// let lastCoordinate +// // ... +// for (const sample of samples) { +// // ... +// lastCoordinate = coordinate +// } +// ``` +// +// If the target X value equals the coordinate, it's the coordinate's value. +// +// ``` +// const targetX = Q * ticks +// // ... +// for (const sample of samples) { +// // ... +// if (coordinate.x === targetX) return coordinate.y +// // ... +// } +// ``` +// +// If the target X value is within the span between the current and previous coordiate, we perform +// linear interpolation. +// +// ``` +// const targetX = Q * ticks +// let lastCoordinate = {x: NaN, y: NaN} +// // ... +// for (const sample of samples) { +// // ... +// if (coordinate.x > targetX) { +// const dx = coordinate.x - lastCoordinate.x +// return lastCoordinate.y * ((targetX - lastCoordinate.x) / dx) + +// coordinate.y * ((coordinate.x - targetX) / dx) +// } +// // ... +// } +// ``` +// +// To do the right extrapolation, we need to track the two previous coordinates. Using the last two +// coordinates, we compute the slope and perform linear extrapolation. (Left extrapolation is +// similar, but I'm omitting it for brevity.) +// +// ``` +// const targetX = Q * ticks +// let secondLastCoordinate +// // ... +// for (const sample of samples) { +// secondLastCoordinate = lastCoordinate +// // ... +// } +// +// const m = (lastCoordinate.y - secondLastCoordinate.y) / +// (lastCoordinate.x - secondLastCoordinate.x) +// return m * (targetX - lastCoordinate.x) + lastCoordinate.y +// ``` +// +// Put all together, it looks like this. The actual code differs, but that's due to three things: +// samples are stored and accessed differently than the above code, it also implements the left +// extrapolation, and I've added a few small optimizations to things like operation order. +// +// ``` +// const targetX = Q * ticks +// let lastCoordinate = {x: NaN, y: NaN} +// let secondLastCoordinate +// let sum = 0 +// for (const sample of samples) { +// const start = sum +// const end = sum + getCount(sample) +// const value = getValue(sample) +// const coordinate = {x: (end + start) / 2, y: value} +// +// if (coordinate.x === targetX) return coordinate.y +// if (lastCoordinate.x < targetX) { +// const dx = coordinate.x - lastCoordinate.x +// return lastCoordinate.y * ((targetX - lastCoordinate.x) / dx) + +// coordinate.y * ((coordinate.x - targetX) / dx) +// } +// +// secondLastCoordinate = lastCoordinate +// lastCoordinate = coordinate +// +// sum += getCount(sample) +// } +// +// const m = (lastCoordinate.y - secondLastCoordinate.y) / +// (lastCoordinate.x - secondLastCoordinate.x) +// return m * (targetX - lastCoordinate.x) + lastCoordinate.y +// ``` +/** @param {number} Q */ +function quantile(Q) { + if (Q <= 0) { + throw new RangeError("Quantile is undefined for Q <= 0") + } + if (Q >= 1) { + throw new RangeError("Quantile is undefined for Q >= 1") + } + if (samples.length < 2) { + throw new RangeError("Quantile is undefined for N < 2") + } + + const targetX = Q * ticks + let lastX1 = NaN + let lastY1 = NaN + let lastX2 = NaN + let lastY2 = NaN + let sum = 0 + let extrapolateBack = false + + for (const sample of samples) { + const x1 = lastX1 = lastX2 + const y1 = lastY1 = lastY2 + const start = sum + const end = sum + sample.count + const x2 = lastX2 = (end + start) / 2 + const y2 = lastY2 = sample.sum / sample.count + + sum = end + + if (x2 === targetX) return y2 + if (x2 > targetX) { + // Interval: x1 <= targetX < x2 + // Interpolate from (x1, y1) to (x2, y2) + // eslint-disable-next-line no-self-compare + if (x1 === x1) { + const dx = x2 - x1 + return (y1 / dx) * (targetX - x1) + (y2 / dx) * (x2 - targetX) + } + // Interval: 0 <= targetX < first coordinate's X + // Extrapolate the line (x1, y1) to (x2, y2) backwards to targetX + if (extrapolateBack) break + extrapolateBack = true + } + } + + // Interval: last coordinate's X <= targetX < N + // Extrapolate the line (lastX1, lastY1) to (lastX2, lastY2) out to targetX + return (lastY2 - lastY1) / (lastX2 - lastX1) * (targetX - lastX2) + lastY2 +} + +// Returns the (estimated) population variance. +// +// The population variance (squared population standard deviation) is very simple: take +// each sample, subtract each one by the mean, and take the mean of the squares of those +// differences. The general formula for that is this: +// +// ``` +// variance = sum(0 <= i < N: (x[i] - mean)^2) / N +// ``` +// +// Unfortunately, we don't have all the samples. So we need to use an alternate formula to +// estimate it (and apply Bessel's correction as the weights are frequency weights). +// +// ``` +// variance = sum(0 <= i < count(s): count(s[i]) * (avg(s[i]) - mean)^2) / +// (sum(0 <= i < count(s): count(s[i])) - 1) +// = sum(0 <= i < count(s): count(s[i]) * (avg(s[i]) - mean)^2) / (N - 1) +// = sum(0 <= i < count(s): count(s[i]) * (sum(s[i]) / count(s[i]) - mean)^2) / (N - 1) +// ``` +function variance() { + if (samples.length < 2) { + throw new RangeError("Variance is undefined for N < 2") + } + + if (ticks < 2) { + throw new RangeError("Variance is undefined for population < 2") + } + + const m = mean() + let sum = 0 + + for (const sample of samples) { + const delta = sample.sum / sample.count - m + sum += sample.count * delta * delta + } + + return sum / (ticks - 1) +} + +// Returns the margin of error, with finite population correction applied. +// +// The formula for the finite population correction is this, where `n` is the sample count: +// +// ``` +// FPC = sqrt((N - n) / (N - 1)) +// ``` +// +// It's needed because it's quite common that the number of samples is pretty close to the +// total population count. Only in very fast tests is it not. +// +// The margin of error formula is this, where `0 <= q <= 1`: +// +// ``` +// MOE[q] = quantile[q] * sqrt(variance / N) * FPC +// ``` +// +// (The quantile function is computed using a separate helper function.) +// +// These two calculations can be combined, since `sqrt(a) * sqrt(b) = sqrt(a * b)`. +// +// ``` +// MOE[q] = quantile[q] * sqrt(variance / N) * sqrt((N - n) / (N - 1)) +// = quantile[q] * sqrt((variance / N) * (N - n) / (N - 1)) +// = quantile[q] * sqrt((variance * (N - n)) / (N * (N - 1))) +// ``` +function marginOfError() { + return quantile(minConfidence) * Math.sqrt( + (variance() / ticks) * + ((ticks - samples.length) / (ticks - 1)) + ) +} + +export function currentDisplayStats() { + return { + pop: ticks, + n: samples.length, + moe: marginOfError(), + mean: mean(), + median: quantile(0.5), + min: samples[0].sum / samples[0].count, + max: samples[samples.length - 1].sum / samples[samples.length - 1].count, + confidenceMin: quantile(1 - minConfidence), + confidenceMax: quantile(minConfidence), + expMin: quantile(0.3173), + expMax: quantile(0.6827), + } +} + +export function getSamples() { + return samples.slice() +} diff --git a/performance/test-perf.js b/performance/test-perf.js index c0d28fca8..dbdd1a808 100644 --- a/performance/test-perf.js +++ b/performance/test-perf.js @@ -2,7 +2,7 @@ import m from "../dist/mithril.esm.min.js" -import {runBenchmarks} from "./bench.js" +import {setupBenchmarks} from "./bench.js" import {mutateStylesPropertiesTree} from "./components/mutate-styles-properties-tree.js" import {nestedTree} from "./components/nested-tree.js" @@ -10,23 +10,32 @@ import {repeatedTree} from "./components/repeated-tree.js" import {shuffledKeyedTree} from "./components/shuffled-keyed-tree.js" import {simpleTree} from "./components/simple-tree.js" -/** @type {Parameters[0]} */ -const benchmarks = Object.create(null) +import {numVars, routes, stringVars, templates} from "./routes.js" -async function run() { - if (!isBrowser) await import("../test-utils/injectBrowserMock.js") - await runBenchmarks(benchmarks) - cycleRoot() - if (isBrowser) document.body.innerHTML = "Benchmarks completed. See console." +async function setup() { + if (typeof window === "undefined") { + // Gotta use `eval` here for Node. + // eslint-disable-next-line no-eval + await eval('import("../test-utils/injectBrowserMock.js")') + } } -const isBrowser = typeof process === "undefined" +const allTrees = [ + simpleTree, + nestedTree, + mutateStylesPropertiesTree, + repeatedTree, + shuffledKeyedTree, +] -function nextFrame() { - return new Promise((resolve) => window.requestAnimationFrame(resolve)) -} +// 1. `m.match` requires a `{path, params}` object to be given (to match). +// 2. In practice, these have a shared shape, and this ensures it has that shared shape. +const routeObjects = routes.map((path) => ({path, params: new URLSearchParams()})) -let rootElem, allElems +// For route selection +let i = 0 + +let rootElem, allElems, redraw, allRedraws function cycleRoot() { if (allElems) { @@ -42,218 +51,229 @@ function cycleRoot() { document.body.appendChild(rootElem = document.createElement("div")) } -const allTrees = [] - -function addTree(name, treeFn) { - allTrees.push(treeFn) - - benchmarks[`construct ${name}`] = (b) => { - do { - b.start() - do { - treeFn() - } while (!b.tick()) - } while (!b.done()) - } - - benchmarks[`render ${name}`] = async (b) => { - do { - cycleRoot() - m.render(rootElem, treeFn()) - b.start() - do { - m.render(rootElem, treeFn()) - } while (!b.tick()) - if (isBrowser) await nextFrame() - } while (!b.done()) - } - - benchmarks[`add/remove ${name}`] = async (b) => { - do { - cycleRoot() - b.start() - do { - m.render(rootElem, treeFn()) - m.render(rootElem, null) - } while (!b.tick()) - if (isBrowser) await nextFrame() - } while (!b.done()) - } -} - -benchmarks["null test"] = (b) => { - do { - cycleRoot() - b.start() - do { - // nothing - } while (!b.tick()) - } while (!b.done()) -} - -const {routes, stringVars, numVars, templates} = (() => { -const routes = [] -const stringVars = [] -const numVars = [] -const templates = [] - -for (let i = 0; i < 16; i++) { - for (let j = 0; j < 16; j++) { - templates.push(`/foo${i}/:id${i}/bar${j}/:sub${j}`) - routes.push(`/foo${i}/${i}/bar${j}/${j}`) - stringVars.push({ - [`id${i}`]: `${i}`, - [`sub${j}`]: `${j}`, - }) - numVars.push({ - [`id${i}`]: i, - [`sub${j}`]: j, - }) - } -} - -return { - // Flatten everything, since they're usually flat strings in practice. - routes: JSON.parse(JSON.stringify(routes)).map((path) => ({path, params: new URLSearchParams()})), - templates: JSON.parse(JSON.stringify(templates)), - stringVars: JSON.parse(JSON.stringify(stringVars)), - numVars: JSON.parse(JSON.stringify(numVars)), -} -})() - - -// This just needs to be sub-millisecond -benchmarks["route match"] = (b) => { - let i = 0 - do { - cycleRoot() - do { +setupBenchmarks(setup, cycleRoot, { + // This just needs to be sub-millisecond + "route match": { + fn() { // eslint-disable-next-line no-bitwise i = (i - 1) & 255 - globalThis.test = m.match(routes[i], templates[i]) - } while (!b.tick()) - } while (!b.done()) -} + return m.match(routeObjects[i], templates[i]) + }, + }, -// These four need to be at most a few microseconds, as 300 of these * 3 us/op = 0.9 ms. (And yes, -// while 300 may seem like a lot, I've worked with apps that exceeded 100, and for 60 FPS, you only -// truly have room for about 5ms total for logic.) + // These four need to be at most a few microseconds, as 300 of these * 3 us/op = 0.9 ms. (And yes, + // while 300 may seem like a lot, I've worked with apps that exceeded 100, and for 60 FPS, you only + // truly have room for about 5ms total for logic.) -benchmarks["route non-match"] = (b) => { - let i = 0 - do { - cycleRoot() - do { + "route non-match": { + fn() { const j = i // eslint-disable-next-line no-bitwise i = (i - 1) & 255 - globalThis.test = m.match(routes[i], templates[j]) - } while (!b.tick()) - } while (!b.done()) -} + return m.match(routeObjects[i], templates[j]) + }, + }, -benchmarks["path generate with string interpolations"] = (b) => { - let i = 0 - do { - cycleRoot() - do { + "path generate with string interpolations": { + fn() { // eslint-disable-next-line no-bitwise i = (i - 1) & 255 - globalThis.test = m.p(templates[i], stringVars[i]) - } while (!b.tick()) - } while (!b.done()) -} + return m.p(templates[i], stringVars[i]) + }, + }, -benchmarks["path generate with number interpolations"] = (b) => { - let i = 0 - do { - cycleRoot() - do { + "path generate with number interpolations": { + fn() { // eslint-disable-next-line no-bitwise i = (i - 1) & 255 - globalThis.test = m.p(templates[i], numVars[i]) - } while (!b.tick()) - } while (!b.done()) -} + return m.p(templates[i], numVars[i]) + }, + }, -benchmarks["path generate no interpolations"] = (b) => { - let i = 0 - do { - cycleRoot() - do { + "path generate no interpolations": { + fn() { // eslint-disable-next-line no-bitwise i = (i - 1) & 255 - globalThis.test = m.p(templates[i]) - } while (!b.tick()) - } while (!b.done()) -} + return m.p(templates[i]) + }, + }, + + "construct `simpleTree`": { + fn: simpleTree, + }, + + "render `simpleTree`": { + tick() { + cycleRoot() + m.render(rootElem, simpleTree()) + }, + fn() { + m.render(rootElem, simpleTree()) + }, + }, + + "add/remove `simpleTree`": { + tick() { + cycleRoot() + m.render(rootElem, null) + }, + fn() { + m.render(rootElem, simpleTree()) + m.render(rootElem, null) + }, + }, -addTree("simpleTree", simpleTree) -addTree("nestedTree", nestedTree) -addTree("mutateStylesPropertiesTree", mutateStylesPropertiesTree) -addTree("repeatedTree", repeatedTree) -addTree("shuffledKeyedTree", shuffledKeyedTree) - -benchmarks["mount simpleTree"] = async (b) => { - do { - cycleRoot() - b.start() - do { + "construct `nestedTree`": { + fn: nestedTree, + }, + + "render `nestedTree`": { + tick() { + cycleRoot() + m.render(rootElem, nestedTree()) + }, + fn() { + m.render(rootElem, nestedTree()) + }, + }, + + "add/remove `nestedTree`": { + tick() { + cycleRoot() + m.render(rootElem, null) + }, + fn() { + m.render(rootElem, nestedTree()) + m.render(rootElem, null) + }, + }, + + "construct `mutateStylesPropertiesTree`": { + fn: mutateStylesPropertiesTree, + }, + + "render `mutateStylesPropertiesTree`": { + tick() { + cycleRoot() + m.render(rootElem, mutateStylesPropertiesTree()) + }, + fn() { + m.render(rootElem, mutateStylesPropertiesTree()) + }, + }, + + "add/remove `mutateStylesPropertiesTree`": { + tick() { + cycleRoot() + m.render(rootElem, null) + }, + fn() { + m.render(rootElem, mutateStylesPropertiesTree()) + m.render(rootElem, null) + }, + }, + + "construct `repeatedTree`": { + fn: repeatedTree, + }, + + "render `repeatedTree`": { + tick() { + cycleRoot() + m.render(rootElem, repeatedTree()) + }, + fn() { + m.render(rootElem, repeatedTree()) + }, + }, + + "add/remove `repeatedTree`": { + tick() { + cycleRoot() + m.render(rootElem, null) + }, + fn() { + m.render(rootElem, repeatedTree()) + m.render(rootElem, null) + }, + }, + + "construct `shuffledKeyedTree`": { + fn: shuffledKeyedTree, + }, + + "render `shuffledKeyedTree`": { + tick() { + cycleRoot() + m.render(rootElem, shuffledKeyedTree()) + }, + fn() { + m.render(rootElem, shuffledKeyedTree()) + }, + }, + + "add/remove `shuffledKeyedTree`": { + tick() { + cycleRoot() + m.render(rootElem, null) + }, + fn() { + m.render(rootElem, shuffledKeyedTree()) + m.render(rootElem, null) + }, + }, + + "mount simpleTree": { + tick() { + cycleRoot() + // For consistency across the interval m.mount(rootElem, simpleTree) - } while (!b.tick()) - if (isBrowser) await nextFrame() - } while (!b.done()) -} + }, + fn() { + m.mount(rootElem, simpleTree) + }, + }, -benchmarks["redraw simpleTree"] = async (b) => { - do { - cycleRoot() - var redraw = m.mount(rootElem, simpleTree) - b.start() - do { + "redraw simpleTree": { + tick() { + cycleRoot() + redraw = m.mount(rootElem, simpleTree) + }, + fn() { redraw.sync() - } while (!b.tick()) - if (isBrowser) await nextFrame() - } while (!b.done()) -} + }, + }, -benchmarks["mount all"] = async (b) => { - do { - cycleRoot() - allElems = allTrees.map(() => { - const elem = document.createElement("div") - rootElem.appendChild(elem) - return elem - }) - b.start() - do { + "mount all": { + tick() { + cycleRoot() + allElems = allTrees.map((tree) => { + const elem = document.createElement("div") + rootElem.appendChild(elem) + // For consistency across the interval + m.mount(elem, tree) + return elem + }) + }, + fn() { for (let i = 0; i < allTrees.length; i++) { m.mount(allElems[i], allTrees[i]) } - } while (!b.tick()) - if (isBrowser) await nextFrame() - } while (!b.done()) -} + }, + }, -benchmarks["redraw all"] = async (b) => { - do { - cycleRoot() - allElems = allTrees.map(() => { - const elem = document.createElement("div") - rootElem.appendChild(elem) - return elem - }) - const allRedraws = allElems.map((elem, i) => m.mount(elem, allTrees[i])) - b.start() - do { + "redraw all": { + tick() { + cycleRoot() + allElems = allTrees.map(() => { + const elem = document.createElement("div") + rootElem.appendChild(elem) + return elem + }) + allRedraws = allElems.map((elem, i) => m.mount(elem, allTrees[i])) + }, + fn() { for (const redraw of allRedraws) redraw.sync() - } while (!b.tick()) - if (isBrowser) await nextFrame() - } while (!b.done()) -} - -if (isBrowser) { - window.onload = run -} else { - run() -} + }, + }, +}) diff --git a/src/std/path-query.js b/src/std/path-query.js index 38b1a6bb1..2b2cdaf28 100644 --- a/src/std/path-query.js +++ b/src/std/path-query.js @@ -27,6 +27,8 @@ is while charging, the higher end is while on battery. - This provides headroom for up to about 100 calls per frame. - Switch from using match strings to computing positions from `exec.index`: 6.5-12 microseconds - This provides headroom for up to about 150 calls per frame. +- Switch from using match strings to computing positions from `exec.index`: 6.5-12 microseconds + - This provides headroom for up to about 150 calls per frame. - Iterate string directly: 2-3.5 microseconds - This provides headroom for up to about 500 calls per frame. From 2e75897e8e91372babe46831701500f9c079fe2e Mon Sep 17 00:00:00 2001 From: Claudia Meadows Date: Tue, 29 Oct 2024 20:23:01 -0700 Subject: [PATCH 82/95] Address some feedback around naming and (indirectly) `m.key`, optimize `m.use` `m.use` is specially designed to be used a fair amount, and simulating it is relatively slow, so it's worth making a built-in. `m.key` -> `m.keyed` provides for a simpler interface, one that you don't have to type `key` out explicitly for anymore. (Most frameworks require you to explicitly add a "key" attribute, while this approaches it more like a glorified `Map`.) --- performance/components/shuffled-keyed-tree.js | 6 +- src/core.js | 333 +++++----- src/entry/mithril.esm.js | 6 +- src/std/path-query.js | 4 +- src/std/tracked.js | 10 +- src/std/use.js | 19 - tests/core/component.js | 20 +- tests/core/fragment.js | 136 +--- tests/core/hyperscript.js | 42 +- tests/core/input.js | 4 +- tests/core/keyed.js | 604 ++++++++++++++++++ tests/core/normalize.js | 36 +- tests/core/normalizeChildren.js | 52 -- tests/core/normalizeComponentChildren.js | 2 +- tests/core/oncreate.js | 20 +- tests/core/onremove.js | 6 +- tests/core/onupdate.js | 4 +- tests/core/render.js | 69 +- tests/core/updateNodes.js | 393 ++++++------ tests/core/updateNodesFuzzer.js | 4 +- tests/exported-api.js | 13 +- tests/std/{q.js => query.js} | 36 +- 22 files changed, 1127 insertions(+), 692 deletions(-) delete mode 100644 src/std/use.js create mode 100644 tests/core/keyed.js delete mode 100644 tests/core/normalizeChildren.js rename tests/std/{q.js => query.js} (71%) diff --git a/performance/components/shuffled-keyed-tree.js b/performance/components/shuffled-keyed-tree.js index 13b3a2cfe..a49d4ead8 100644 --- a/performance/components/shuffled-keyed-tree.js +++ b/performance/components/shuffled-keyed-tree.js @@ -17,9 +17,5 @@ function shuffle() { export const shuffledKeyedTree = () => { shuffle() - var vnodes = [] - for (const key of keys) { - vnodes.push(m.key(key, m("div.item"))) - } - return vnodes + return m.keyed(keys, (key) => [key, m("div.item")]) } diff --git a/src/core.js b/src/core.js index 3864d9ada..5b2443e09 100644 --- a/src/core.js +++ b/src/core.js @@ -51,19 +51,19 @@ Fragments: - `c`: virtual DOM children - `d`: unused -Keys: +Keyed: - `m` bits 0-2: `1` -- `t`: `KEY` -- `s`: identity key (may be any arbitrary object) -- `a`: unused +- `t`: unused +- `s`: unused +- `a`: key array - `c`: virtual DOM children - `d`: unused Text: - `m` bits 0-2: `2` - `t`: unused -- `s`: text string -- `a`: unused +- `s`: unused +- `a`: text string - `c`: unused - `d`: abort controller reference @@ -86,34 +86,34 @@ DOM elements: Layout: - `m` bits 0-2: `5` - `t`: unused -- `s`: callback to schedule -- `a`: unused +- `s`: uncaught +- `a`: callback to schedule - `c`: unused - `d`: parent DOM reference, for easier queueing Remove: - `m` bits 0-2: `6` - `t`: unused -- `s`: callback to schedule -- `a`: unused +- `s`: unused +- `a`: callback to schedule - `c`: unused - `d`: parent DOM reference, for easier queueing The `m` field is also used for various assertions, that aren't described here. */ -var TYPE_MASK = 7 +var TYPE_MASK = 15 var TYPE_RETAIN = -1 var TYPE_FRAGMENT = 0 -var TYPE_KEY = 1 +var TYPE_KEYED = 1 var TYPE_TEXT = 2 var TYPE_ELEMENT = 3 var TYPE_COMPONENT = 4 var TYPE_LAYOUT = 5 var TYPE_REMOVE = 6 var TYPE_SET_CONTEXT = 7 +var TYPE_USE = 8 -var FLAG_KEYED = 1 << 3 var FLAG_USED = 1 << 4 var FLAG_IS_REMOVE = 1 << 5 var FLAG_HTML_ELEMENT = 1 << 6 @@ -124,13 +124,12 @@ var FLAG_OPTION_ELEMENT = 1 << 10 var FLAG_TEXTAREA_ELEMENT = 1 << 11 var FLAG_IS_FILE_INPUT = 1 << 12 -var Vnode = (mask, tag, state, attrs, children) => ({ +var Vnode = (mask, tag, attrs, children) => ({ m: mask, t: tag, - s: state, a: attrs, c: children, - // Think of this as either "data" or "DOM" - it's used for both. + s: null, d: null, }) @@ -178,13 +177,18 @@ of this optimization process. It doesn't allocate arguments except as needed to doesn't allocate attributes except to replace them for modifications, among other things. */ var m = function (selector, attrs) { - if (typeof selector !== "string" && typeof selector !== "function") { - throw new Error("The selector must be either a string or a component."); - } - + var type = TYPE_ELEMENT var start = 1 var children + if (typeof selector !== "string") { + if (typeof selector !== "function") { + throw new Error("The selector must be either a string or a component."); + } + type = selector === m.Fragment ? TYPE_FRAGMENT : TYPE_COMPONENT + } + + if (attrs == null || typeof attrs === "object" && typeof attrs.m !== "number" && !Array.isArray(attrs)) { start = 2 if (arguments.length < 3 && attrs && Array.isArray(attrs.children)) { @@ -207,49 +211,51 @@ var m = function (selector, attrs) { // DOM nodes are about as commonly constructed as vnodes, but fragments are only constructed // from JSX code (and even then, they aren't common). - if (typeof selector !== "string") { - if (selector === m.Fragment) { - return createParentVnode(TYPE_FRAGMENT, null, null, null, children) - } else { - return Vnode(TYPE_COMPONENT, selector, null, {children, ...attrs}, null) - } - } + if (type === TYPE_ELEMENT) { + attrs = attrs || {} + var hasClassName = hasOwn.call(attrs, "className") + var dynamicClass = hasClassName ? attrs.className : attrs.class + var state = selectorCache.get(selector) + var original = attrs - attrs = attrs || {} - var hasClassName = hasOwn.call(attrs, "className") - var dynamicClass = hasClassName ? attrs.className : attrs.class - var state = selectorCache.get(selector) - var original = attrs + if (state == null) { + state = /*@__NOINLINE__*/compileSelector(selector) + } - if (state == null) { - state = /*@__NOINLINE__*/compileSelector(selector) - } + if (state.a != null) { + attrs = {...state.a, ...attrs} + } - if (state.a != null) { - attrs = {...state.a, ...attrs} + if (dynamicClass != null || state.c != null) { + if (attrs !== original) attrs = {...attrs} + attrs.class = dynamicClass != null + ? state.c != null ? `${state.c} ${dynamicClass}` : dynamicClass + : state.c + if (hasClassName) attrs.className = null + } } - if (dynamicClass != null || state.c != null) { - if (attrs !== original) attrs = {...attrs} - attrs.class = dynamicClass != null - ? state.c != null ? `${state.c} ${dynamicClass}` : dynamicClass - : state.c - if (hasClassName) attrs.className = null + if (type === TYPE_COMPONENT) { + attrs = {children, ...attrs} + children = null + } else { + for (var i = 0; i < children.length; i++) children[i] = m.normalize(children[i]) } - return createParentVnode(TYPE_ELEMENT, selector, null, attrs, children) + return Vnode(type, selector, attrs, children) } m.TYPE_MASK = TYPE_MASK m.TYPE_RETAIN = TYPE_RETAIN m.TYPE_FRAGMENT = TYPE_FRAGMENT -m.TYPE_KEY = TYPE_KEY +m.TYPE_KEYED = TYPE_KEYED m.TYPE_TEXT = TYPE_TEXT m.TYPE_ELEMENT = TYPE_ELEMENT m.TYPE_COMPONENT = TYPE_COMPONENT m.TYPE_LAYOUT = TYPE_LAYOUT m.TYPE_REMOVE = TYPE_REMOVE m.TYPE_SET_CONTEXT = TYPE_SET_CONTEXT +m.TYPE_USE = TYPE_USE // Simple and sweet. Also useful for idioms like `onfoo: m.capture` to drop events without // redrawing. @@ -259,68 +265,59 @@ m.capture = (ev) => { return false } -m.retain = () => Vnode(TYPE_RETAIN, null, null, null, null) +m.retain = () => Vnode(TYPE_RETAIN, null, null, null) m.layout = (callback) => { if (typeof callback !== "function") { throw new TypeError("Callback must be a function if provided") } - return Vnode(TYPE_LAYOUT, null, callback, null, null) + return Vnode(TYPE_LAYOUT, null, callback, null) } m.remove = (callback) => { if (typeof callback !== "function") { throw new TypeError("Callback must be a function if provided") } - return Vnode(TYPE_REMOVE, null, callback, null, null) + return Vnode(TYPE_REMOVE, null, callback, null) } m.Fragment = (attrs) => attrs.children -m.key = (key, ...children) => - createParentVnode(TYPE_KEY, key, null, null, - children.length === 1 && Array.isArray(children[0]) ? children[0].slice() : [...children] - ) +m.keyed = (values, view) => { + if (view != null && typeof view !== "function") { + throw new TypeError("Callback must be a function if provided") + } + var map = new Map() + for (var value of values) { + if (typeof view === "function") value = view(value) + if (value != null && typeof value !== "boolean") { + if (!Array.isArray(value) || value.length < 1) { + throw new TypeError("Returned value must be a `[key, value]` array") + } + if (map.has(value[0])) { + // Coerce to string so symbols don't throw + throw new TypeError(`Duplicate key detected: ${String(value[0])}`) + } + map.set(value[0], m.normalize(value[1])) + } + } + return Vnode(TYPE_KEYED, null, map, null) +} -m.set = (entries, ...children) => - createParentVnode(TYPE_SET_CONTEXT, null, null, entries, - children.length === 1 && Array.isArray(children[0]) ? children[0].slice() : [...children] - ) +m.set = (entries, ...children) => resolveSpecialFragment(TYPE_SET_CONTEXT, entries, ...children) +m.use = (deps, ...children) => resolveSpecialFragment(TYPE_USE, [...deps], ...children) m.normalize = (node) => { if (node == null || typeof node === "boolean") return null - if (typeof node !== "object") return Vnode(TYPE_TEXT, null, String(node), null, null) - if (Array.isArray(node)) return createParentVnode(TYPE_FRAGMENT, null, null, null, node.slice()) + if (typeof node !== "object") return Vnode(TYPE_TEXT, null, String(node), null) + if (Array.isArray(node)) return Vnode(TYPE_FRAGMENT, null, null, node.map(m.normalize)) return node } -var createParentVnode = (mask, tag, state, attrs, input) => { - if (input.length) { - input[0] = m.normalize(input[0]) - var isKeyed = input[0] != null && (input[0].m & TYPE_MASK) === TYPE_KEY - var keys = new Set() - mask |= -isKeyed & FLAG_KEYED - // Note: this is a *very* perf-sensitive check. - // Fun fact: merging the loop like this is somehow faster than splitting - // it, noticeably so. - for (var i = 1; i < input.length; i++) { - input[i] = m.normalize(input[i]) - if ((input[i] != null && (input[i].m & TYPE_MASK) === TYPE_KEY) !== isKeyed) { - throw new TypeError( - isKeyed - ? "In fragments, vnodes must either all have keys or none have keys. You may wish to consider using an explicit empty key vnode, `m.key()`, instead of a hole." - : "In fragments, vnodes must either all have keys or none have keys." - ) - } - if (isKeyed) { - if (keys.has(input[i].t)) { - throw new TypeError(`Duplicate key detected: ${input[i].t}`) - } - keys.add(input[i].t) - } - } - } - return Vnode(mask, tag, state, attrs, input) +var resolveSpecialFragment = (type, attrs, ...children) => { + var resolved = children.length === 1 && Array.isArray(children[0]) ? [...children[0]] : [...children] + for (var i = 0; i < resolved.length; i++) resolved[i] = m.normalize(resolved[i]) + return Vnode(type, null, attrs, resolved) } var xlinkNs = "http://www.w3.org/1999/xlink" @@ -353,85 +350,99 @@ var moveToPosition = (vnode) => { while ((type = vnode.m & TYPE_MASK) === TYPE_COMPONENT) { if (!(vnode = vnode.c)) return } - if ((1 << TYPE_FRAGMENT | 1 << TYPE_KEY | 1 << TYPE_SET_CONTEXT) & 1 << type) { + if ((1 << TYPE_FRAGMENT | 1 << TYPE_USE | 1 << TYPE_SET_CONTEXT) & 1 << type) { vnode.c.forEach(moveToPosition) } else if ((1 << TYPE_TEXT | 1 << TYPE_ELEMENT) & 1 << type) { insertAfterCurrentRefNode(vnode.d) + } else if (type === TYPE_KEYED) { + vnode.a.forEach(moveToPosition) } } var updateFragment = (old, vnode) => { - // Here's the logic: - // - If `old` or `vnode` is `null`, common length is 0 by default, and it falls back to an - // unkeyed empty fragment. - // - If `old` and `vnode` differ in their keyedness, their children must be wholly replaced. - // - If `old` and `vnode` are both non-keyed, patch their children linearly. - // - If `old` and `vnode` are both keyed, patch their children using a map. - var mask = vnode != null ? vnode.m : 0 + // Patch the common prefix, remove the extra in the old, and create the extra in the new. + // + // Can't just take the max of both, because out-of-bounds accesses both disrupts + // optimizations and is just generally slower. + // + // Note: if either `vnode` or `old` is `null`, the common length and its own length are + // both zero, so it can't actually throw. var newLength = vnode != null ? vnode.c.length : 0 - var oldMask = old != null ? old.m : 0 var oldLength = old != null ? old.c.length : 0 var commonLength = oldLength < newLength ? oldLength : newLength - if ((oldMask ^ mask) & FLAG_KEYED) { // XOR is equivalent to bit inequality - // Key state changed. Replace the subtree - commonLength = 0 - mask &= ~FLAG_KEYED - } - if (!(mask & FLAG_KEYED)) { - // Not keyed. Patch the common prefix, remove the extra in the old, and create the - // extra in the new. - // - // Can't just take the max of both, because out-of-bounds accesses both disrupts - // optimizations and is just generally slower. - // - // Note: if either `vnode` or `old` is `null`, the common length and its own length are - // both zero, so it can't actually throw. - try { - for (var i = 0; i < commonLength; i++) updateNode(old.c[i], vnode.c[i]) - for (var i = commonLength; i < newLength; i++) updateNode(null, vnode.c[i]) - } catch (e) { - commonLength = i - for (var i = 0; i < commonLength; i++) updateNode(vnode.c[i], null) - for (var i = commonLength; i < oldLength; i++) updateNode(old.c[i], null) - throw e - } + try { + for (var i = 0; i < commonLength; i++) updateNode(old.c[i], vnode.c[i]) + for (var i = commonLength; i < newLength; i++) updateNode(null, vnode.c[i]) + } catch (e) { + commonLength = i + for (var i = 0; i < commonLength; i++) updateNode(vnode.c[i], null) for (var i = commonLength; i < oldLength; i++) updateNode(old.c[i], null) - } else { - // Keyed. I take a pretty straightforward approach here to keep it simple: - // 1. Build a map from old map to old vnode. - // 2. Walk the new vnodes, adding what's missing and patching what's in the old. - // 3. Remove from the old map the keys in the new vnodes, leaving only the keys that - // were removed this run. - // 4. Remove the remaining nodes in the old map that aren't in the new map. Since the - // new keys were already deleted, this is just a simple map iteration. - - // Note: if either `vnode` or `old` is `null`, they won't get here. The default mask is - // zero, and that causes keyed state to differ and thus a forced linear diff per above. + throw e + } + for (var i = commonLength; i < oldLength; i++) updateNode(old.c[i], null) +} - var oldMap = new Map() - for (var p of old.c) oldMap.set(p.t, p) +var updateUse = (old, vnode) => { + if ( + old != null && old.length !== 0 && + vnode != null && vnode.length !== 0 && + ( + vnode.a.length !== old.a.length || + vnode.a.some((b, i) => !Object.is(b, old.a[i])) + ) + ) { + updateFragment(old, null) + old = null + } + updateFragment(old, vnode) +} - try { - for (var i = 0; i < newLength; i++) { - var n = vnode.c[i] - var p = oldMap.get(n.t) - if (p == null) { - updateFragment(null, n) - } else { - oldMap.delete(n.t) - var prev = currentRefNode - moveToPosition(p) - currentRefNode = prev - updateFragment(p, n) - } +var updateKeyed = (old, vnode) => { + // I take a pretty straightforward approach here to keep it simple: + // 1. Build a map from old map to old vnode. + // 2. Walk the new vnodes, adding what's missing and patching what's in the old. + // 3. Remove from the old map the keys in the new vnodes, leaving only the keys that + // were removed this run. + // 4. Remove the remaining nodes in the old map that aren't in the new map. Since the + // new keys were already deleted, this is just a simple map iteration. + + // Note: if either `vnode` or `old` is `null`, they won't get here. The default mask is + // zero, and that causes keyed state to differ and thus a forced linear diff per above. + + var added = 0 + // It's a value that 1. isn't user-providable and 2. isn't likely to go away in future changes. + // Works well enough as a sentinel. + var error = selectorCache + try { + // Iterate the map. I get keys for free that way, and insertion order is guaranteed to be + // preserved in any spec-conformant engine. + vnode.a.forEach((n, k) => { + var p = old != null ? old.a.get(k) : null + if (p == null) { + updateNode(null, n) + } else { + var prev = currentRefNode + moveToPosition(p) + currentRefNode = prev + updateNode(p, n) + // Delete from the state set, but only after it's been successfully moved. This + // avoids needing to specially remove `p` on failure. + old.a.delete(k) } - } catch (e) { - for (var j = 0; j < i; j++) updateNode(vnode.c[j], null) - updateNode(old.c[j], null) - oldMap.forEach((p) => updateNode(p, null)) - throw e + added++ + }) + added = -1 + } catch (e) { + error = e + } + if (old != null) removeKeyed(old) + // Either `added === 0` from the `catch` block or `added === -1` from completing the loop. + if (error !== selectorCache) { + for (var n of vnode.a.values()) { + if (--added) break + updateNode(n, null) } - oldMap.forEach((p) => updateNode(p, null)) + throw error } } @@ -487,15 +498,13 @@ var updateNode = (old, vnode) => { throw new TypeError("Vnodes must not be reused") } - var newType = vnode.m & TYPE_MASK - - if (type === newType && vnode.t === old.t) { - vnode.m = old.m & ~FLAG_KEYED | vnode.m & FLAG_KEYED + if (type === (vnode.m & TYPE_MASK) && vnode.t === old.t) { + vnode.m = old.m } else { updateNode(old, null) - type = newType old = null } + type = vnode.m & TYPE_MASK } try { @@ -534,9 +543,9 @@ var updateSet = (old, vnode) => { var updateText = (old, vnode) => { if (old == null) { - insertAfterCurrentRefNode(vnode.d = currentDocument.createTextNode(vnode.s)) + insertAfterCurrentRefNode(vnode.d = currentDocument.createTextNode(vnode.a)) } else { - if (`${old.s}` !== `${vnode.s}`) old.d.nodeValue = vnode.s + if (`${old.a}` !== `${vnode.a}`) old.d.nodeValue = vnode.a vnode.d = currentRefNode = old.d } } @@ -698,6 +707,8 @@ var updateComponent = (old, vnode) => { var removeFragment = (old) => updateFragment(old, null) +var removeKeyed = (old) => old.a.forEach((p) => updateNode(p, null)) + var removeNode = (old) => { try { if (!old.d) return @@ -711,18 +722,19 @@ var removeNode = (old) => { // Replaces an otherwise necessary `switch`. var updateNodeDispatch = [ updateFragment, - updateFragment, + updateKeyed, updateText, updateElement, updateComponent, updateLayout, updateRemove, updateSet, + updateUse, ] var removeNodeDispatch = [ removeFragment, - removeFragment, + removeKeyed, removeNode, (old) => { removeNode(old) @@ -732,6 +744,7 @@ var removeNodeDispatch = [ () => {}, (old) => currentHooks.push(old), removeFragment, + removeFragment, ] //attrs @@ -1096,9 +1109,9 @@ m.render = (dom, vnode, {redraw, removeOnThrow} = {}) => { if (active != null && currentDocument.activeElement !== active && typeof active.focus === "function") { active.focus() } - for (var {s, d} of hooks) { + for (var {a, d} of hooks) { try { - s(d) + a(d) } catch (e) { console.error(e) } diff --git a/src/entry/mithril.esm.js b/src/entry/mithril.esm.js index bbcbabafe..13d544f5e 100644 --- a/src/entry/mithril.esm.js +++ b/src/entry/mithril.esm.js @@ -2,22 +2,20 @@ import m from "../core.js" import {Link, WithRouter} from "../std/router.js" import {debouncer, throttler} from "../std/rate-limit.js" -import {match, p, q} from "../std/path-query.js" +import {match, p, query} from "../std/path-query.js" import init from "../std/init.js" import lazy from "../std/lazy.js" import tracked from "../std/tracked.js" -import use from "../std/use.js" import withProgress from "../std/with-progress.js" m.WithRouter = WithRouter m.Link = Link m.p = p -m.q = q +m.query = query m.match = match m.withProgress = withProgress m.lazy = lazy m.init = init -m.use = use m.tracked = tracked m.throttler = throttler m.debouncer = debouncer diff --git a/src/std/path-query.js b/src/std/path-query.js index 2b2cdaf28..4bb05806a 100644 --- a/src/std/path-query.js +++ b/src/std/path-query.js @@ -102,7 +102,7 @@ var serializeQueryParams = (sep, value, exclude, params) => { return value } -var q = (params) => serializeQueryParams("", "", [], params) +var query = (params) => serializeQueryParams("", "", [], params) var QUERY = 0 var ESCAPE = 1 @@ -305,4 +305,4 @@ var match = ({path, params}, pattern) => { return {...exec.groups} } -export {p, q, match} +export {p, query, match} diff --git a/src/std/tracked.js b/src/std/tracked.js index 3d2d5d255..47a6c2bce 100644 --- a/src/std/tracked.js +++ b/src/std/tracked.js @@ -18,17 +18,17 @@ Models can do basic CRUD operations on the collection. - They can replace the current value, deleting a value that's already there. In the view, they use handles to abstract over the concept of a key. Duplicates are theoretically -possible, so they should use the handle itself as the key for `m.key(...)`. It might look something -like this: +possible, so they should use the handle itself as the key for `m.keyed(...)`. It might look +something like this: ```js -return t.live().map((handle) => ( - m.key(handle, m(Entry, { +return m.keyed(t.live(), (handle) => ( + [handle.key, m(Entry, { name: handle.key, value: handle.value, removed: handle.signal.aborted, onremovaltransitionended: () => handle.release(), - })) + })] )) ``` diff --git a/src/std/use.js b/src/std/use.js deleted file mode 100644 index b27ce97c5..000000000 --- a/src/std/use.js +++ /dev/null @@ -1,19 +0,0 @@ -import m from "../core.js" - -var Use = () => { - var key = 0 - return (n, o) => { - if (o && !( - n.d.length === o.d.length && - n.d.every((b, i) => Object.is(b, o.d[i])) - )) { - key++ - } - - return m.key(key, n.children) - } -} - -var use = (deps, ...children) => m(Use, {d: [...deps]}, ...children) - -export {use as default} diff --git a/tests/core/component.js b/tests/core/component.js index 832706860..de45b2f58 100644 --- a/tests/core/component.js +++ b/tests/core/component.js @@ -82,9 +82,9 @@ o.spec("component", function() { }) o("removes", function() { var component = () => m("div") - m.render(G.root, [m.key(1, m(component)), m.key(2, m("div"))]) + m.render(G.root, m.keyed([[1, m(component)], [2, m("div")]])) var div = m("div") - m.render(G.root, [m.key(2, div)]) + m.render(G.root, m.keyed([[2, div]])) o(G.root.childNodes.length).equals(1) o(G.root.firstChild).equals(div.d) @@ -231,9 +231,9 @@ o.spec("component", function() { m("input"), ] var div = m("div") - m.render(G.root, [m.key(1, m(component)), m.key(2, div)]) + m.render(G.root, m.keyed([[1, m(component)], [2, div]])) - m.render(G.root, [m.key(2, m("div"))]) + m.render(G.root, m.keyed([[2, m("div")]])) o(G.root.childNodes.length).equals(1) o(G.root.firstChild).equals(div.d) @@ -241,9 +241,9 @@ o.spec("component", function() { o("can remove when returning primitive", function() { var component = () => "a" var div = m("div") - m.render(G.root, [m.key(1, m(component)), m.key(2, div)]) + m.render(G.root, m.keyed([[1, m(component)], [2, div]])) - m.render(G.root, [m.key(2, m("div"))]) + m.render(G.root, m.keyed([[2, m("div")]])) o(G.root.childNodes.length).equals(1) o(G.root.firstChild).equals(div.d) @@ -443,10 +443,10 @@ o.spec("component", function() { var createSpy = o.spy() var component = o.spy(() => m("div", m.layout(createSpy))) - m.render(G.root, [m("div", m.key(1, m(component)))]) + m.render(G.root, [m("div", m.keyed([[1, m(component)]]))]) var child = G.root.firstChild.firstChild m.render(G.root, []) - m.render(G.root, [m("div", m.key(1, m(component)))]) + m.render(G.root, [m("div", m.keyed([[1, m(component)]]))]) o(child).notEquals(G.root.firstChild.firstChild) // this used to be a recycling pool test o(component.callCount).equals(2) @@ -455,10 +455,10 @@ o.spec("component", function() { var createSpy = o.spy() var component = o.spy(() => m("div", m.remove(createSpy))) - m.render(G.root, [m("div", m.key(1, m(component)))]) + m.render(G.root, [m("div", m.keyed([[1, m(component)]]))]) var child = G.root.firstChild.firstChild m.render(G.root, []) - m.render(G.root, [m("div", m.key(1, m(component)))]) + m.render(G.root, [m("div", m.keyed([[1, m(component)]]))]) var found = G.root.firstChild.firstChild m.render(G.root, []) diff --git a/tests/core/fragment.js b/tests/core/fragment.js index c3191733f..c7dd366a2 100644 --- a/tests/core/fragment.js +++ b/tests/core/fragment.js @@ -19,25 +19,25 @@ o.spec("fragment literal", function() { var vnode = m.normalize(["a"]) o(vnode.c[0].m & m.TYPE_MASK).equals(m.TYPE_TEXT) - o(vnode.c[0].s).equals("a") + o(vnode.c[0].a).equals("a") }) o("handles falsy string single child", function() { var vnode = m.normalize([""]) o(vnode.c[0].m & m.TYPE_MASK).equals(m.TYPE_TEXT) - o(vnode.c[0].s).equals("") + o(vnode.c[0].a).equals("") }) o("handles number single child", function() { var vnode = m.normalize([1]) o(vnode.c[0].m & m.TYPE_MASK).equals(m.TYPE_TEXT) - o(vnode.c[0].s).equals("1") + o(vnode.c[0].a).equals("1") }) o("handles falsy number single child", function() { var vnode = m.normalize([0]) o(vnode.c[0].m & m.TYPE_MASK).equals(m.TYPE_TEXT) - o(vnode.c[0].s).equals("0") + o(vnode.c[0].a).equals("0") }) o("handles boolean single child", function() { var vnode = m.normalize([true]) @@ -63,17 +63,17 @@ o.spec("fragment literal", function() { var vnode = m.normalize(["", "a"]) o(vnode.c[0].m & m.TYPE_MASK).equals(m.TYPE_TEXT) - o(vnode.c[0].s).equals("") + o(vnode.c[0].a).equals("") o(vnode.c[1].m & m.TYPE_MASK).equals(m.TYPE_TEXT) - o(vnode.c[1].s).equals("a") + o(vnode.c[1].a).equals("a") }) o("handles multiple number children", function() { var vnode = m.normalize([0, 1]) o(vnode.c[0].m & m.TYPE_MASK).equals(m.TYPE_TEXT) - o(vnode.c[0].s).equals("0") + o(vnode.c[0].a).equals("0") o(vnode.c[1].m & m.TYPE_MASK).equals(m.TYPE_TEXT) - o(vnode.c[1].s).equals("1") + o(vnode.c[1].a).equals("1") }) o("handles multiple boolean children", function() { var vnode = m.normalize([false, true]) @@ -104,25 +104,25 @@ o.spec("fragment component", function() { var vnode = m(m.Fragment, null, ["a"]) o(vnode.c[0].m & m.TYPE_MASK).equals(m.TYPE_TEXT) - o(vnode.c[0].s).equals("a") + o(vnode.c[0].a).equals("a") }) o("handles falsy string single child", function() { var vnode = m(m.Fragment, null, [""]) o(vnode.c[0].m & m.TYPE_MASK).equals(m.TYPE_TEXT) - o(vnode.c[0].s).equals("") + o(vnode.c[0].a).equals("") }) o("handles number single child", function() { var vnode = m(m.Fragment, null, [1]) o(vnode.c[0].m & m.TYPE_MASK).equals(m.TYPE_TEXT) - o(vnode.c[0].s).equals("1") + o(vnode.c[0].a).equals("1") }) o("handles falsy number single child", function() { var vnode = m(m.Fragment, null, [0]) o(vnode.c[0].m & m.TYPE_MASK).equals(m.TYPE_TEXT) - o(vnode.c[0].s).equals("0") + o(vnode.c[0].a).equals("0") }) o("handles boolean single child", function() { var vnode = m(m.Fragment, null, [true]) @@ -148,17 +148,17 @@ o.spec("fragment component", function() { var vnode = m(m.Fragment, null, ["", "a"]) o(vnode.c[0].m & m.TYPE_MASK).equals(m.TYPE_TEXT) - o(vnode.c[0].s).equals("") + o(vnode.c[0].a).equals("") o(vnode.c[1].m & m.TYPE_MASK).equals(m.TYPE_TEXT) - o(vnode.c[1].s).equals("a") + o(vnode.c[1].a).equals("a") }) o("handles multiple number children", function() { var vnode = m(m.Fragment, null, [0, 1]) o(vnode.c[0].m & m.TYPE_MASK).equals(m.TYPE_TEXT) - o(vnode.c[0].s).equals("0") + o(vnode.c[0].a).equals("0") o(vnode.c[1].m & m.TYPE_MASK).equals(m.TYPE_TEXT) - o(vnode.c[1].s).equals("1") + o(vnode.c[1].a).equals("1") }) o("handles multiple boolean children", function() { var vnode = m(m.Fragment, null, [false, true]) @@ -174,109 +174,7 @@ o.spec("fragment component", function() { var vnode = m(m.Fragment, null, 0) o(vnode.c[0].m & m.TYPE_MASK).equals(m.TYPE_TEXT) - o(vnode.c[0].s).equals("0") - }) - }) -}) - -o.spec("key", function() { - o("works", function() { - var child = m("p") - var frag = m.key(undefined, child) - - o(frag.m & m.TYPE_MASK).equals(m.TYPE_KEY) - - o(Array.isArray(frag.c)).equals(true) - o(frag.c.length).equals(1) - o(frag.c[0]).equals(child) - - o(frag.t).equals(undefined) - }) - o("supports non-null keys", function() { - var frag = m.key(7, []) - o(frag.m & m.TYPE_MASK).equals(m.TYPE_KEY) - - o(Array.isArray(frag.c)).equals(true) - o(frag.c.length).equals(0) - - o(frag.t).equals(7) - }) - o.spec("children", function() { - o("handles string single child", function() { - var vnode = m.key("foo", ["a"]) - - o(vnode.c[0].m & m.TYPE_MASK).equals(m.TYPE_TEXT) - o(vnode.c[0].s).equals("a") - }) - o("handles falsy string single child", function() { - var vnode = m.key("foo", [""]) - - o(vnode.c[0].m & m.TYPE_MASK).equals(m.TYPE_TEXT) - o(vnode.c[0].s).equals("") - }) - o("handles number single child", function() { - var vnode = m.key("foo", [1]) - - o(vnode.c[0].m & m.TYPE_MASK).equals(m.TYPE_TEXT) - o(vnode.c[0].s).equals("1") - }) - o("handles falsy number single child", function() { - var vnode = m.key("foo", [0]) - - o(vnode.c[0].m & m.TYPE_MASK).equals(m.TYPE_TEXT) - o(vnode.c[0].s).equals("0") - }) - o("handles boolean single child", function() { - var vnode = m.key("foo", [true]) - - o(vnode.c).deepEquals([null]) - }) - o("handles falsy boolean single child", function() { - var vnode = m.key("foo", [false]) - - o(vnode.c).deepEquals([null]) - }) - o("handles null single child", function() { - var vnode = m.key("foo", [null]) - - o(vnode.c[0]).equals(null) - }) - o("handles undefined single child", function() { - var vnode = m.key("foo", [undefined]) - - o(vnode.c).deepEquals([null]) - }) - o("handles multiple string children", function() { - var vnode = m.key("foo", ["", "a"]) - - o(vnode.c[0].m & m.TYPE_MASK).equals(m.TYPE_TEXT) - o(vnode.c[0].s).equals("") - o(vnode.c[1].m & m.TYPE_MASK).equals(m.TYPE_TEXT) - o(vnode.c[1].s).equals("a") - }) - o("handles multiple number children", function() { - var vnode = m.key("foo", [0, 1]) - - o(vnode.c[0].m & m.TYPE_MASK).equals(m.TYPE_TEXT) - o(vnode.c[0].s).equals("0") - o(vnode.c[1].m & m.TYPE_MASK).equals(m.TYPE_TEXT) - o(vnode.c[1].s).equals("1") - }) - o("handles multiple boolean children", function() { - var vnode = m.key("foo", [false, true]) - - o(vnode.c).deepEquals([null, null]) - }) - o("handles multiple null/undefined child", function() { - var vnode = m.key("foo", [null, undefined]) - - o(vnode.c).deepEquals([null, null]) - }) - o("handles falsy number single child without attrs", function() { - var vnode = m.key("foo", 0) - - o(vnode.c[0].m & m.TYPE_MASK).equals(m.TYPE_TEXT) - o(vnode.c[0].s).equals("0") + o(vnode.c[0].a).equals("0") }) }) }) diff --git a/tests/core/hyperscript.js b/tests/core/hyperscript.js index d4f61a0f1..1a0d9c1b7 100644 --- a/tests/core/hyperscript.js +++ b/tests/core/hyperscript.js @@ -408,22 +408,22 @@ o.spec("hyperscript", function() { o("handles string single child", function() { var vnode = m("div", {}, ["a"]) - o(vnode.c[0].s).equals("a") + o(vnode.c[0].a).equals("a") }) o("handles falsy string single child", function() { var vnode = m("div", {}, [""]) - o(vnode.c[0].s).equals("") + o(vnode.c[0].a).equals("") }) o("handles number single child", function() { var vnode = m("div", {}, [1]) - o(vnode.c[0].s).equals("1") + o(vnode.c[0].a).equals("1") }) o("handles falsy number single child", function() { var vnode = m("div", {}, [0]) - o(vnode.c[0].s).equals("0") + o(vnode.c[0].a).equals("0") }) o("handles boolean single child", function() { var vnode = m("div", {}, [true]) @@ -449,17 +449,17 @@ o.spec("hyperscript", function() { var vnode = m("div", {}, ["", "a"]) o(vnode.c[0].m & m.TYPE_MASK).equals(m.TYPE_TEXT) - o(vnode.c[0].s).equals("") + o(vnode.c[0].a).equals("") o(vnode.c[1].m & m.TYPE_MASK).equals(m.TYPE_TEXT) - o(vnode.c[1].s).equals("a") + o(vnode.c[1].a).equals("a") }) o("handles multiple number children", function() { var vnode = m("div", {}, [0, 1]) o(vnode.c[0].m & m.TYPE_MASK).equals(m.TYPE_TEXT) - o(vnode.c[0].s).equals("0") + o(vnode.c[0].a).equals("0") o(vnode.c[1].m & m.TYPE_MASK).equals(m.TYPE_TEXT) - o(vnode.c[1].s).equals("1") + o(vnode.c[1].a).equals("1") }) o("handles multiple boolean children", function() { var vnode = m("div", {}, [false, true]) @@ -474,15 +474,15 @@ o.spec("hyperscript", function() { o("handles falsy number single child without attrs", function() { var vnode = m("div", 0) - o(vnode.c[0].s).equals("0") + o(vnode.c[0].a).equals("0") }) o("handles children in attributes", function() { var vnode = m("div", {children: ["", "a"]}) o(vnode.c[0].m & m.TYPE_MASK).equals(m.TYPE_TEXT) - o(vnode.c[0].s).equals("") + o(vnode.c[0].a).equals("") o(vnode.c[1].m & m.TYPE_MASK).equals(m.TYPE_TEXT) - o(vnode.c[1].s).equals("a") + o(vnode.c[1].a).equals("a") }) }) o.spec("permutations", function() { @@ -541,33 +541,33 @@ o.spec("hyperscript", function() { o(vnode.a.a).equals("b") o(vnode.c[0].m & m.TYPE_MASK).equals(m.TYPE_TEXT) - o(vnode.c[0].s).equals("c") + o(vnode.c[0].a).equals("c") o(vnode.c[1].m & m.TYPE_MASK).equals(m.TYPE_TEXT) - o(vnode.c[1].s).equals("d") + o(vnode.c[1].a).equals("d") }) o("handles attr and single string text child", function() { var vnode = m("div", {a: "b"}, ["c"]) o(vnode.a.a).equals("b") - o(vnode.c[0].s).equals("c") + o(vnode.c[0].a).equals("c") }) o("handles attr and single falsy string text child", function() { var vnode = m("div", {a: "b"}, [""]) o(vnode.a.a).equals("b") - o(vnode.c[0].s).equals("") + o(vnode.c[0].a).equals("") }) o("handles attr and single number text child", function() { var vnode = m("div", {a: "b"}, [1]) o(vnode.a.a).equals("b") - o(vnode.c[0].s).equals("1") + o(vnode.c[0].a).equals("1") }) o("handles attr and single falsy number text child", function() { var vnode = m("div", {a: "b"}, [0]) o(vnode.a.a).equals("b") - o(vnode.c[0].s).equals("0") + o(vnode.c[0].a).equals("0") }) o("handles attr and single boolean text child", function() { var vnode = m("div", {a: "b"}, [true]) @@ -579,7 +579,7 @@ o.spec("hyperscript", function() { var vnode = m("div", {a: "b"}, [0]) o(vnode.a.a).equals("b") - o(vnode.c[0].s).equals("0") + o(vnode.c[0].a).equals("0") }) o("handles attr and single false boolean text child", function() { var vnode = m("div", {a: "b"}, [false]) @@ -591,16 +591,16 @@ o.spec("hyperscript", function() { var vnode = m("div", {a: "b"}, "c") o(vnode.a.a).equals("b") - o(vnode.c[0].s).equals("c") + o(vnode.c[0].a).equals("c") }) o("handles attr and text children unwrapped", function() { var vnode = m("div", {a: "b"}, "c", "d") o(vnode.a.a).equals("b") o(vnode.c[0].m & m.TYPE_MASK).equals(m.TYPE_TEXT) - o(vnode.c[0].s).equals("c") + o(vnode.c[0].a).equals("c") o(vnode.c[1].m & m.TYPE_MASK).equals(m.TYPE_TEXT) - o(vnode.c[1].s).equals("d") + o(vnode.c[1].a).equals("d") }) o("handles children without attr", function() { var vnode = m("div", [m("i"), m("s")]) diff --git a/tests/core/input.js b/tests/core/input.js index d2fefe873..d78e128fe 100644 --- a/tests/core/input.js +++ b/tests/core/input.js @@ -11,9 +11,9 @@ o.spec("form inputs", function() { o("maintains focus after move", function() { var input - m.render(G.root, [m.key(1, input = m("input")), m.key(2, m("a")), m.key(3, m("b"))]) + m.render(G.root, m.keyed([[1, input = m("input")], [2, m("a")], [3, m("b")]])) input.d.focus() - m.render(G.root, [m.key(2, m("a")), m.key(1, input = m("input")), m.key(3, m("b"))]) + m.render(G.root, m.keyed([[2, m("a")], [1, input = m("input")], [3, m("b")]])) o(G.window.document.activeElement).equals(input.d) }) diff --git a/tests/core/keyed.js b/tests/core/keyed.js new file mode 100644 index 000000000..6b6bb26d5 --- /dev/null +++ b/tests/core/keyed.js @@ -0,0 +1,604 @@ +/* eslint-disable no-bitwise */ +import o from "ospec" + +import m from "../../src/entry/mithril.esm.js" + +o.spec("keyed with view", function() { + o("works empty", function() { + var view = o.spy(() => {}) + var vnode = m.keyed([], view) + + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_KEYED) + + o(view.callCount).equals(0) + o(vnode.a.size).equals(0) + }) + o("supports `undefined` keys", function() { + var child = m("p") + var view = o.spy(() => [undefined, child]) + var vnode = m.keyed([1], view) + + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_KEYED) + + o(view.callCount).equals(1) + o(vnode.a.size).equals(1) + o([...vnode.a][0][0]).equals(undefined) + o([...vnode.a][0][1]).equals(child) + }) + o("supports `null` keys", function() { + var child = m("p") + var view = o.spy(() => [null, child]) + var vnode = m.keyed([1], view) + + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_KEYED) + + o(view.callCount).equals(1) + o(vnode.a.size).equals(1) + o([...vnode.a][0][0]).equals(null) + o([...vnode.a][0][1]).equals(child) + }) + o("supports `false` keys", function() { + var child = m("p") + var view = o.spy(() => [false, child]) + var vnode = m.keyed([1], view) + + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_KEYED) + + o(view.callCount).equals(1) + o(vnode.a.size).equals(1) + o([...vnode.a][0][0]).equals(false) + o([...vnode.a][0][1]).equals(child) + }) + o("supports `true` keys", function() { + var child = m("p") + var view = o.spy(() => [true, child]) + var vnode = m.keyed([1], view) + + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_KEYED) + + o(view.callCount).equals(1) + o(vnode.a.size).equals(1) + o([...vnode.a][0][0]).equals(true) + o([...vnode.a][0][1]).equals(child) + }) + o("supports empty string keys", function() { + var child = m("p") + var view = o.spy(() => ["", child]) + var vnode = m.keyed([1], view) + + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_KEYED) + + o(view.callCount).equals(1) + o(vnode.a.size).equals(1) + o([...vnode.a][0][0]).equals("") + o([...vnode.a][0][1]).equals(child) + }) + o("supports non-empty string keys", function() { + var child = m("p") + var view = o.spy(() => ["a", child]) + var vnode = m.keyed([1], view) + + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_KEYED) + + o(view.callCount).equals(1) + o(vnode.a.size).equals(1) + o([...vnode.a][0][0]).equals("a") + o([...vnode.a][0][1]).equals(child) + }) + o("supports falsy number keys", function() { + var child = m("p") + var view = o.spy(() => [0, child]) + var vnode = m.keyed([1], view) + + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_KEYED) + + o(view.callCount).equals(1) + o(vnode.a.size).equals(1) + o([...vnode.a][0][0]).equals(0) + o([...vnode.a][0][1]).equals(child) + }) + o("supports truthy number keys", function() { + var child = m("p") + var view = o.spy(() => [123, child]) + var vnode = m.keyed([1], view) + + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_KEYED) + + o(view.callCount).equals(1) + o(vnode.a.size).equals(1) + o([...vnode.a][0][0]).equals(123) + o([...vnode.a][0][1]).equals(child) + }) + if (typeof BigInt === "function") { + // eslint-disable-next-line no-undef + const B = BigInt + o("supports falsy bigint keys", function() { + var child = m("p") + var view = o.spy(() => [B(0), child]) + var vnode = m.keyed([1], view) + + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_KEYED) + + o(view.callCount).equals(1) + o(vnode.a.size).equals(1) + o([...vnode.a][0][0]).equals(B(0)) + o([...vnode.a][0][1]).equals(child) + }) + o("supports truthy bigint keys", function() { + var child = m("p") + var view = o.spy(() => [B(123), child]) + var vnode = m.keyed([1], view) + + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_KEYED) + + o(view.callCount).equals(1) + o(vnode.a.size).equals(1) + o([...vnode.a][0][0]).equals(B(123)) + o([...vnode.a][0][1]).equals(child) + }) + } + o("supports symbol keys", function() { + var key = Symbol("test") + var child = m("p") + var view = o.spy(() => [key, child]) + var vnode = m.keyed([1], view) + + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_KEYED) + + o(view.callCount).equals(1) + o(vnode.a.size).equals(1) + o([...vnode.a][0][0]).equals(key) + o([...vnode.a][0][1]).equals(child) + }) + o("supports object keys", function() { + var key = {} + var child = m("p") + var view = o.spy(() => [key, child]) + var vnode = m.keyed([1], view) + + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_KEYED) + + o(view.callCount).equals(1) + o(vnode.a.size).equals(1) + o([...vnode.a][0][0]).equals(key) + o([...vnode.a][0][1]).equals(child) + }) + o("rejects duplicate `undefined` keys", function() { + var child = m("p") + var view = o.spy(() => [undefined, child]) + + o(() => m.keyed([1, 2], view)).throws(TypeError) + }) + o("rejects duplicate `null` keys", function() { + var child = m("p") + var view = o.spy(() => [null, child]) + + o(() => m.keyed([1, 2], view)).throws(TypeError) + }) + o("rejects duplicate `false` keys", function() { + var child = m("p") + var view = o.spy(() => [false, child]) + + o(() => m.keyed([1, 2], view)).throws(TypeError) + }) + o("rejects duplicate `true` keys", function() { + var child = m("p") + var view = o.spy(() => [true, child]) + + o(() => m.keyed([1, 2], view)).throws(TypeError) + }) + o("rejects duplicate empty string keys", function() { + var child = m("p") + var view = o.spy(() => ["", child]) + + o(() => m.keyed([1, 2], view)).throws(TypeError) + }) + o("rejects duplicate non-empty string keys", function() { + var child = m("p") + var view = o.spy(() => ["a", child]) + + o(() => m.keyed([1, 2], view)).throws(TypeError) + }) + o("rejects duplicate falsy number keys", function() { + var child = m("p") + var view = o.spy(() => [0, child]) + + o(() => m.keyed([1, 2], view)).throws(TypeError) + }) + o("rejects duplicate truthy number keys", function() { + var child = m("p") + var view = o.spy(() => [123, child]) + + o(() => m.keyed([1, 2], view)).throws(TypeError) + }) + if (typeof BigInt === "function") { + // eslint-disable-next-line no-undef + const B = BigInt + o("rejects duplicate falsy bigint keys", function() { + var child = m("p") + var view = o.spy(() => [B(0), child]) + + o(() => m.keyed([1, 2], view)).throws(TypeError) + }) + o("rejects duplicate truthy bigint keys", function() { + var child = m("p") + var view = o.spy(() => [B(123), child]) + + o(() => m.keyed([1, 2], view)).throws(TypeError) + }) + } + o("rejects duplicate symbol keys", function() { + var key = Symbol("test") + var child = m("p") + var view = o.spy(() => [key, child]) + + o(() => m.keyed([1, 2], view)).throws(TypeError) + }) + o("rejects duplicate object keys", function() { + var key = {} + var child = m("p") + var view = o.spy(() => [key, child]) + + o(() => m.keyed([1, 2], view)).throws(TypeError) + }) + o("handles `undefined` hole", function() { + var vnode = m.keyed(["foo", "bar"], (key) => (key === "foo" ? undefined : [key, "a"])) + + o(vnode.a.size).equals(1) + o([...vnode.a][0][1].m & m.TYPE_MASK).equals(m.TYPE_TEXT) + o([...vnode.a][0][1].a).equals("a") + o([...vnode.a][0][0]).equals("bar") + }) + o("handles `null` hole", function() { + var vnode = m.keyed(["foo", "bar"], (key) => (key === "foo" ? null : [key, "a"])) + + o(vnode.a.size).equals(1) + o([...vnode.a][0][1].m & m.TYPE_MASK).equals(m.TYPE_TEXT) + o([...vnode.a][0][1].a).equals("a") + o([...vnode.a][0][0]).equals("bar") + }) + o("handles `false` hole", function() { + var vnode = m.keyed(["foo", "bar"], (key) => (key === "foo" ? false : [key, "a"])) + + o(vnode.a.size).equals(1) + o([...vnode.a][0][1].m & m.TYPE_MASK).equals(m.TYPE_TEXT) + o([...vnode.a][0][1].a).equals("a") + o([...vnode.a][0][0]).equals("bar") + }) + o("handles `true` hole", function() { + var vnode = m.keyed(["foo", "bar"], (key) => (key === "foo" ? true : [key, "a"])) + + o(vnode.a.size).equals(1) + o([...vnode.a][0][1].m & m.TYPE_MASK).equals(m.TYPE_TEXT) + o([...vnode.a][0][1].a).equals("a") + o([...vnode.a][0][0]).equals("bar") + }) + o("handles `undefined` child", function() { + var vnode = m.keyed(["foo"], (key) => [key, undefined]) + + o([...vnode.a]).deepEquals([["foo", null]]) + }) + o("handles `null` child", function() { + var vnode = m.keyed(["foo"], (key) => [key, null]) + + o([...vnode.a]).deepEquals([["foo", null]]) + }) + o("handles `false child", function() { + var vnode = m.keyed(["foo"], (key) => [key, false]) + + o([...vnode.a]).deepEquals([["foo", null]]) + }) + o("handles `true` child", function() { + var vnode = m.keyed(["foo"], (key) => [key, true]) + + o([...vnode.a]).deepEquals([["foo", null]]) + }) + o("handles string child", function() { + var vnode = m.keyed(["foo"], (key) => [key, "a"]) + + o(vnode.a.size).equals(1) + o([...vnode.a][0][1].m & m.TYPE_MASK).equals(m.TYPE_TEXT) + o([...vnode.a][0][1].a).equals("a") + }) + o("handles falsy string child", function() { + var vnode = m.keyed(["foo"], (key) => [key, ""]) + + o(vnode.a.size).equals(1) + o([...vnode.a][0][1].m & m.TYPE_MASK).equals(m.TYPE_TEXT) + o([...vnode.a][0][1].a).equals("") + }) + o("handles number child", function() { + var vnode = m.keyed(["foo"], (key) => [key, 1]) + + o(vnode.a.size).equals(1) + o([...vnode.a][0][1].m & m.TYPE_MASK).equals(m.TYPE_TEXT) + o([...vnode.a][0][1].a).equals("1") + }) + o("handles falsy number child", function() { + var vnode = m.keyed(["foo"], (key) => [key, 0]) + + o(vnode.a.size).equals(1) + o([...vnode.a][0][1].m & m.TYPE_MASK).equals(m.TYPE_TEXT) + o([...vnode.a][0][1].a).equals("0") + }) + o("handles fragment", function() { + var vnode = m.keyed(["foo"], (key) => [key, ["", "a"]]) + + o(vnode.a.size).equals(1) + o([...vnode.a][0][1].m & m.TYPE_MASK).equals(m.TYPE_FRAGMENT) + o([...vnode.a][0][1].c.length).equals(2) + o([...vnode.a][0][1].c[0].m & m.TYPE_MASK).equals(m.TYPE_TEXT) + o([...vnode.a][0][1].c[0].a).equals("") + o([...vnode.a][0][1].c[1].m & m.TYPE_MASK).equals(m.TYPE_TEXT) + o([...vnode.a][0][1].c[1].a).equals("a") + }) +}) + +o.spec("keyed direct", function() { + o("works empty", function() { + var vnode = m.keyed([]) + + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_KEYED) + + o(vnode.a.size).equals(0) + }) + o("supports `undefined` keys", function() { + var child = m("p") + var vnode = m.keyed([[undefined, child]]) + + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_KEYED) + + o(vnode.a.size).equals(1) + o([...vnode.a][0][0]).equals(undefined) + o([...vnode.a][0][1]).equals(child) + }) + o("supports `null` keys", function() { + var child = m("p") + var vnode = m.keyed([[null, child]]) + + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_KEYED) + + o(vnode.a.size).equals(1) + o([...vnode.a][0][0]).equals(null) + o([...vnode.a][0][1]).equals(child) + }) + o("supports `false` keys", function() { + var child = m("p") + var vnode = m.keyed([[false, child]]) + + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_KEYED) + + o(vnode.a.size).equals(1) + o([...vnode.a][0][0]).equals(false) + o([...vnode.a][0][1]).equals(child) + }) + o("supports `true` keys", function() { + var child = m("p") + var vnode = m.keyed([[true, child]]) + + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_KEYED) + + o(vnode.a.size).equals(1) + o([...vnode.a][0][0]).equals(true) + o([...vnode.a][0][1]).equals(child) + }) + o("supports empty string keys", function() { + var child = m("p") + var vnode = m.keyed([["", child]]) + + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_KEYED) + + o(vnode.a.size).equals(1) + o([...vnode.a][0][0]).equals("") + o([...vnode.a][0][1]).equals(child) + }) + o("supports non-empty string keys", function() { + var child = m("p") + var vnode = m.keyed([["a", child]]) + + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_KEYED) + + o(vnode.a.size).equals(1) + o([...vnode.a][0][0]).equals("a") + o([...vnode.a][0][1]).equals(child) + }) + o("supports falsy number keys", function() { + var child = m("p") + var vnode = m.keyed([[0, child]]) + + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_KEYED) + + o(vnode.a.size).equals(1) + o([...vnode.a][0][0]).equals(0) + o([...vnode.a][0][1]).equals(child) + }) + o("supports truthy number keys", function() { + var child = m("p") + var vnode = m.keyed([[123, child]]) + + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_KEYED) + + o(vnode.a.size).equals(1) + o([...vnode.a][0][0]).equals(123) + o([...vnode.a][0][1]).equals(child) + }) + if (typeof BigInt === "function") { + // eslint-disable-next-line no-undef + const B = BigInt + o("supports falsy bigint keys", function() { + var child = m("p") + var vnode = m.keyed([[B(0), child]]) + + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_KEYED) + + o(vnode.a.size).equals(1) + o([...vnode.a][0][0]).equals(B(0)) + o([...vnode.a][0][1]).equals(child) + }) + o("supports truthy bigint keys", function() { + var child = m("p") + var vnode = m.keyed([[B(123), child]]) + + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_KEYED) + + o(vnode.a.size).equals(1) + o([...vnode.a][0][0]).equals(B(123)) + o([...vnode.a][0][1]).equals(child) + }) + } + o("supports symbol keys", function() { + var key = Symbol("test") + var child = m("p") + var vnode = m.keyed([[key, child]]) + + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_KEYED) + + o(vnode.a.size).equals(1) + o([...vnode.a][0][0]).equals(key) + o([...vnode.a][0][1]).equals(child) + }) + o("supports object keys", function() { + var key = {} + var child = m("p") + var vnode = m.keyed([[key, child]]) + + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_KEYED) + + o(vnode.a.size).equals(1) + o([...vnode.a][0][0]).equals(key) + o([...vnode.a][0][1]).equals(child) + }) + o("rejects duplicate `undefined` keys", function() { + o(() => m.keyed([[undefined, m("p")], [undefined, m("p")]])).throws(TypeError) + }) + o("rejects duplicate `null` keys", function() { + o(() => m.keyed([[null, m("p")], [null, m("p")]])).throws(TypeError) + }) + o("rejects duplicate `false` keys", function() { + o(() => m.keyed([[false, m("p")], [false, m("p")]])).throws(TypeError) + }) + o("rejects duplicate `true` keys", function() { + o(() => m.keyed([[true, m("p")], [true, m("p")]])).throws(TypeError) + }) + o("rejects duplicate empty string keys", function() { + o(() => m.keyed([["", m("p")], ["", m("p")]])).throws(TypeError) + }) + o("rejects duplicate non-empty string keys", function() { + o(() => m.keyed([["a", m("p")], ["a", m("p")]])).throws(TypeError) + }) + o("rejects duplicate falsy number keys", function() { + o(() => m.keyed([[0, m("p")], [0, m("p")]])).throws(TypeError) + }) + o("rejects duplicate truthy number keys", function() { + o(() => m.keyed([[123, m("p")], [123, m("p")]])).throws(TypeError) + }) + if (typeof BigInt === "function") { + // eslint-disable-next-line no-undef + const B = BigInt + o("rejects duplicate falsy bigint keys", function() { + o(() => m.keyed([[B(0), m("p")], [B(0), m("p")]])).throws(TypeError) + }) + o("rejects duplicate truthy bigint keys", function() { + o(() => m.keyed([[B(123), m("p")], [B(123), m("p")]])).throws(TypeError) + }) + } + o("rejects duplicate symbol keys", function() { + var key = Symbol("test") + o(() => m.keyed([[key, m("p")], [key, m("p")]])).throws(TypeError) + }) + o("rejects duplicate object keys", function() { + var key = {} + o(() => m.keyed([[key, m("p")], [key, m("p")]])).throws(TypeError) + }) + o("handles `undefined` hole", function() { + var vnode = m.keyed([undefined, ["bar", "a"]]) + + o(vnode.a.size).equals(1) + o([...vnode.a][0][1].m & m.TYPE_MASK).equals(m.TYPE_TEXT) + o([...vnode.a][0][1].a).equals("a") + o([...vnode.a][0][0]).equals("bar") + }) + o("handles `null` hole", function() { + var vnode = m.keyed([null, ["bar", "a"]]) + + o(vnode.a.size).equals(1) + o([...vnode.a][0][1].m & m.TYPE_MASK).equals(m.TYPE_TEXT) + o([...vnode.a][0][1].a).equals("a") + o([...vnode.a][0][0]).equals("bar") + }) + o("handles `false` hole", function() { + var vnode = m.keyed([false, ["bar", "a"]]) + + o(vnode.a.size).equals(1) + o([...vnode.a][0][1].m & m.TYPE_MASK).equals(m.TYPE_TEXT) + o([...vnode.a][0][1].a).equals("a") + o([...vnode.a][0][0]).equals("bar") + }) + o("handles `true` hole", function() { + var vnode = m.keyed([true, ["bar", "a"]]) + + o(vnode.a.size).equals(1) + o([...vnode.a][0][1].m & m.TYPE_MASK).equals(m.TYPE_TEXT) + o([...vnode.a][0][1].a).equals("a") + o([...vnode.a][0][0]).equals("bar") + }) + o("handles `undefined` child", function() { + var vnode = m.keyed([["foo", undefined]]) + + o([...vnode.a]).deepEquals([["foo", null]]) + }) + o("handles `null` child", function() { + var vnode = m.keyed([["foo", null]]) + + o([...vnode.a]).deepEquals([["foo", null]]) + }) + o("handles `false child", function() { + var vnode = m.keyed([["foo", false]]) + + o([...vnode.a]).deepEquals([["foo", null]]) + }) + o("handles `true` child", function() { + var vnode = m.keyed([["foo", true]]) + + o([...vnode.a]).deepEquals([["foo", null]]) + }) + o("handles string child", function() { + var vnode = m.keyed([["foo", "a"]]) + + o(vnode.a.size).equals(1) + o([...vnode.a][0][1].m & m.TYPE_MASK).equals(m.TYPE_TEXT) + o([...vnode.a][0][1].a).equals("a") + }) + o("handles falsy string child", function() { + var vnode = m.keyed([["foo", ""]]) + + o(vnode.a.size).equals(1) + o([...vnode.a][0][1].m & m.TYPE_MASK).equals(m.TYPE_TEXT) + o([...vnode.a][0][1].a).equals("") + }) + o("handles number child", function() { + var vnode = m.keyed([["foo", 1]]) + + o(vnode.a.size).equals(1) + o([...vnode.a][0][1].m & m.TYPE_MASK).equals(m.TYPE_TEXT) + o([...vnode.a][0][1].a).equals("1") + }) + o("handles falsy number child", function() { + var vnode = m.keyed([["foo", 0]]) + + o(vnode.a.size).equals(1) + o([...vnode.a][0][1].m & m.TYPE_MASK).equals(m.TYPE_TEXT) + o([...vnode.a][0][1].a).equals("0") + }) + o("handles fragment", function() { + var vnode = m.keyed([["foo", ["", "a"]]]) + + o(vnode.a.size).equals(1) + o([...vnode.a][0][1].m & m.TYPE_MASK).equals(m.TYPE_FRAGMENT) + o([...vnode.a][0][1].c.length).equals(2) + o([...vnode.a][0][1].c[0].m & m.TYPE_MASK).equals(m.TYPE_TEXT) + o([...vnode.a][0][1].c[0].a).equals("") + o([...vnode.a][0][1].c[1].m & m.TYPE_MASK).equals(m.TYPE_TEXT) + o([...vnode.a][0][1].c[1].a).equals("a") + }) +}) diff --git a/tests/core/normalize.js b/tests/core/normalize.js index 88003feff..22795754b 100644 --- a/tests/core/normalize.js +++ b/tests/core/normalize.js @@ -22,25 +22,25 @@ o.spec("normalize", function() { var node = m.normalize("a") o(node.m & m.TYPE_MASK).equals(m.TYPE_TEXT) - o(node.s).equals("a") + o(node.a).equals("a") }) o("normalizes falsy string into text node", function() { var node = m.normalize("") o(node.m & m.TYPE_MASK).equals(m.TYPE_TEXT) - o(node.s).equals("") + o(node.a).equals("") }) o("normalizes number into text node", function() { var node = m.normalize(1) o(node.m & m.TYPE_MASK).equals(m.TYPE_TEXT) - o(node.s).equals("1") + o(node.a).equals("1") }) o("normalizes falsy number into text node", function() { var node = m.normalize(0) o(node.m & m.TYPE_MASK).equals(m.TYPE_TEXT) - o(node.s).equals("0") + o(node.a).equals("0") }) o("normalizes `true` to `null`", function() { var node = m.normalize(true) @@ -52,4 +52,32 @@ o.spec("normalize", function() { o(node).equals(null) }) + o("normalizes nested arrays into nested fragments", function() { + var vnode = m.normalize([[]]) + + o(vnode.c[0].m & m.TYPE_MASK).equals(m.TYPE_FRAGMENT) + o(vnode.c[0].c.length).equals(0) + }) + o("normalizes nested strings into nested text nodes", function() { + var vnode = m.normalize(["a"]) + + o(vnode.c[0].m & m.TYPE_MASK).equals(m.TYPE_TEXT) + o(vnode.c[0].a).equals("a") + }) + o("normalizes nested `false` values into nested `null`s", function() { + var vnode = m.normalize([false]) + + o(vnode.c[0]).equals(null) + }) + o("retains nested element vnodes in arrays", function() { + var elem1, elem2 + var vnode = m.normalize([ + elem1 = m("foo1"), + elem2 = m("foo2"), + ]) + + o(vnode.c.length).equals(2) + o(vnode.c[0]).equals(elem1) + o(vnode.c[1]).equals(elem2) + }) }) diff --git a/tests/core/normalizeChildren.js b/tests/core/normalizeChildren.js deleted file mode 100644 index 90cefdc6e..000000000 --- a/tests/core/normalizeChildren.js +++ /dev/null @@ -1,52 +0,0 @@ -/* eslint-disable no-bitwise */ -import o from "ospec" - -import m from "../../src/entry/mithril.esm.js" - -o.spec("normalizeChildren", function() { - o("normalizes arrays into fragments", function() { - var vnode = m.normalize([[]]) - - o(vnode.c[0].m & m.TYPE_MASK).equals(m.TYPE_FRAGMENT) - o(vnode.c[0].c.length).equals(0) - }) - o("normalizes strings into text nodes", function() { - var vnode = m.normalize(["a"]) - - o(vnode.c[0].m & m.TYPE_MASK).equals(m.TYPE_TEXT) - o(vnode.c[0].s).equals("a") - }) - o("normalizes `false` values into `null`s", function() { - var vnode = m.normalize([false]) - - o(vnode.c[0]).equals(null) - }) - o("allows all keys", function() { - var vnode = m.normalize([ - m.key(1), - m.key(2), - ]) - - o(vnode.c).deepEquals([m.key(1), m.key(2)]) - }) - o("allows no keys", function() { - var vnode = m.normalize([ - m("foo1"), - m("foo2"), - ]) - - o(vnode.c).deepEquals([m("foo1"), m("foo2")]) - }) - o("disallows mixed keys, starting with key", function() { - o(() => m.normalize([ - m.key(1), - m("foo2"), - ])).throws(TypeError) - }) - o("disallows mixed keys, starting with no key", function() { - o(() => m.normalize([ - m("foo1"), - m.key(2), - ])).throws(TypeError) - }) -}) diff --git a/tests/core/normalizeComponentChildren.js b/tests/core/normalizeComponentChildren.js index 267f5a084..759a9b75a 100644 --- a/tests/core/normalizeComponentChildren.js +++ b/tests/core/normalizeComponentChildren.js @@ -21,6 +21,6 @@ o.spec("component children", function () { o(vnode.c.c.length).equals(1) // eslint-disable-next-line no-bitwise o(vnode.c.c[0].m & m.TYPE_MASK).equals(m.TYPE_TEXT) - o(vnode.c.c[0].s).equals("a") + o(vnode.c.c[0].a).equals("a") }) }) diff --git a/tests/core/oncreate.js b/tests/core/oncreate.js index b26af3ac8..dcaab8287 100644 --- a/tests/core/oncreate.js +++ b/tests/core/oncreate.js @@ -39,8 +39,8 @@ o.spec("layout create", function() { var vnode = m("div", m.layout(createDiv)) var updated = m("a", m.layout(createA)) - m.render(G.root, m.key(1, vnode)) - m.render(G.root, m.key(1, updated)) + m.render(G.root, m.keyed([[1, vnode]])) + m.render(G.root, m.keyed([[1, updated]])) o(createDiv.callCount).equals(1) o(createA.callCount).equals(1) @@ -59,7 +59,7 @@ o.spec("layout create", function() { var vnode = m("div", m.layout(create)) var otherVnode = m("a") - m.render(G.root, [m.key(1, vnode), m.key(2, otherVnode)]) + m.render(G.root, m.keyed([[1, vnode], [2, otherVnode]])) o(create.callCount).equals(1) o(create.args[0]).equals(G.root.firstChild) @@ -97,8 +97,8 @@ o.spec("layout create", function() { }) o("works on unkeyed that falls into reverse list diff code path", function() { var create = o.spy() - m.render(G.root, [m.key(1, m("p")), m.key(2, m("div"))]) - m.render(G.root, [m.key(2, m("div", m.layout(create))), m.key(1, m("p"))]) + m.render(G.root, m.keyed([[1, m("p")], [2, m("div")]])) + m.render(G.root, m.keyed([[2, m("div", m.layout(create))], [1, m("p")]])) o(create.callCount).equals(1) o(create.args[0]).equals(G.root.firstChild) @@ -158,9 +158,9 @@ o.spec("layout create", function() { var vnode = m("div", m.remove(createDiv)) var updated = m("a", m.remove(createA)) - m.render(G.root, m.key(1, vnode)) + m.render(G.root, m.keyed([[1, vnode]])) var dom = vnode.d - m.render(G.root, m.key(1, updated)) + m.render(G.root, m.keyed([[1, updated]])) o(createDiv.callCount).equals(1) o(createDiv.args[0]).equals(dom) @@ -179,7 +179,7 @@ o.spec("layout create", function() { var vnode = m("div", m.remove(create)) var otherVnode = m("a") - m.render(G.root, [m.key(1, vnode), m.key(2, otherVnode)]) + m.render(G.root, m.keyed([[1, vnode], [2, otherVnode]])) o(create.callCount).equals(0) }) @@ -213,8 +213,8 @@ o.spec("layout create", function() { }) o("works on unkeyed that falls into reverse list diff code path", function() { var create = o.spy() - m.render(G.root, [m.key(1, m("p")), m.key(2, m("div"))]) - m.render(G.root, [m.key(2, m("div", m.remove(create))), m.key(1, m("p"))]) + m.render(G.root, m.keyed([[1, m("p")], [2, m("div")]])) + m.render(G.root, m.keyed([[2, m("div", m.remove(create))], [1, m("p")]])) o(create.callCount).equals(0) }) diff --git a/tests/core/onremove.js b/tests/core/onremove.js index 725d83096..276a93b79 100644 --- a/tests/core/onremove.js +++ b/tests/core/onremove.js @@ -54,9 +54,9 @@ o.spec("layout remove", function() { var temp = m("div", m.remove(remove)) var updated = m("div") - m.render(G.root, m.key(1, vnode)) - m.render(G.root, m.key(2, temp)) - m.render(G.root, m.key(1, updated)) + m.render(G.root, m.keyed([[1, vnode]])) + m.render(G.root, m.keyed([[2, temp]])) + m.render(G.root, m.keyed([[1, updated]])) o(vnode.d).notEquals(updated.d) // this used to be a recycling pool test o(remove.callCount).equals(1) diff --git a/tests/core/onupdate.js b/tests/core/onupdate.js index 4714275de..dcbb5daa8 100644 --- a/tests/core/onupdate.js +++ b/tests/core/onupdate.js @@ -18,8 +18,8 @@ o.spec("layout update", function() { }) o("is not updated when replacing keyed element", function() { var update = o.spy() - var vnode = m.key(1, m("div", m.layout(update))) - var updated = m.key(1, m("a", m.layout(update))) + var vnode = m.keyed([[1, m("div", m.layout(update))]]) + var updated = m.keyed([[1, m("a", m.layout(update))]]) m.render(G.root, vnode) m.render(G.root, updated) diff --git a/tests/core/render.js b/tests/core/render.js index b51018b6b..9d66006ae 100644 --- a/tests/core/render.js +++ b/tests/core/render.js @@ -89,18 +89,14 @@ o.spec("render", function() { var removeB = o.spy() var layoutA = o.spy() var layoutB = o.spy() - var a = function() { - return m.key(1, m("div", - m.key(11, m("div", m.layout(layoutA), m.remove(removeA))), - m.key(12, m("div")) - )) - } - var b = function() { - return m.key(2, m("div", - m.key(21, m("div", m.layout(layoutB), m.remove(removeB))), - m.key(22, m("div")) - )) - } + var a = () => m.keyed([[1, m("div", m.keyed([ + [11, m("div", m.layout(layoutA), m.remove(removeA))], + [12, m("div")], + ]))]]) + var b = () => m.keyed([[2, m("div", m.keyed([ + [21, m("div", m.layout(layoutB), m.remove(removeB))], + [22, m("div")], + ]))]]) m.render(G.root, a()) var first = G.root.firstChild.firstChild m.render(G.root, b()) @@ -122,16 +118,12 @@ o.spec("render", function() { var removeB = o.spy() var layoutA = o.spy() var layoutB = o.spy() - var a = function() { - return m.key(1, m("div", - m("div", m.layout(layoutA), m.remove(removeA)) - )) - } - var b = function() { - return m.key(2, m("div", - m("div", m.layout(layoutB), m.remove(removeB)) - )) - } + var a = () => m.keyed([[1, m("div", + m("div", m.layout(layoutA), m.remove(removeA)) + )]]) + var b = () => m.keyed([[2, m("div", + m("div", m.layout(layoutB), m.remove(removeB)) + )]]) m.render(G.root, a()) var first = G.root.firstChild.firstChild m.render(G.root, b()) @@ -154,16 +146,12 @@ o.spec("render", function() { var layoutA = o.spy() var layoutB = o.spy() - var a = function() { - return m.key(1, m("div", - m("div", m.layout(layoutA), m.remove(removeA)) - )) - } - var b = function() { - return m.key(2, m("div", - m("div", m.layout(layoutB), m.remove(removeB)) - )) - } + var a = () => m.keyed([[1, m("div", + m("div", m.layout(layoutA), m.remove(removeA)) + )]]) + var b = () => m.keyed([[2, m("div", + m("div", m.layout(layoutB), m.remove(removeB)) + )]]) m.render(G.root, a()) m.render(G.root, a()) var first = G.root.firstChild.firstChild @@ -193,21 +181,20 @@ o.spec("render", function() { o(removeA.callCount).equals(1) }) o("svg namespace is preserved in keyed diff (#1820)", function(){ - // note that this only exerciese one branch of the keyed diff algo - var svg = m("svg", - m.key(0, m("g")), - m.key(1, m("g")) - ) + var svg = m("svg", m.keyed([ + [0, m("g")], + [1, m("g")], + ])) m.render(G.root, svg) o(svg.d.namespaceURI).equals("http://www.w3.org/2000/svg") o(svg.d.childNodes[0].namespaceURI).equals("http://www.w3.org/2000/svg") o(svg.d.childNodes[1].namespaceURI).equals("http://www.w3.org/2000/svg") - svg = m("svg", - m.key(1, m("g", {x: 1})), - m.key(2, m("g", {x: 2})) - ) + svg = m("svg", m.keyed([ + [1, m("g", {x: 1})], + [2, m("g", {x: 2})], + ])) m.render(G.root, svg) o(svg.d.namespaceURI).equals("http://www.w3.org/2000/svg") diff --git a/tests/core/updateNodes.js b/tests/core/updateNodes.js index 2e7866761..fa5fd49e9 100644 --- a/tests/core/updateNodes.js +++ b/tests/core/updateNodes.js @@ -5,22 +5,22 @@ import {setupGlobals} from "../../test-utils/global.js" import m from "../../src/entry/mithril.esm.js" function vnodify(str) { - return str.split(",").map((k) => m.key(k, m(k))) + return m.keyed(str.split(","), (k) => [k, m(k)]) } o.spec("updateNodes", function() { var G = setupGlobals() - o("handles el noop", function() { - var vnodes = [m.key(1, m("a")), m.key(2, m("b"))] - var updated = [m.key(1, m("a")), m.key(2, m("b"))] + o("handles keyed noop", function() { + var vnodes = m.keyed([[1, m("a")], [2, m("b")]]) + var updated = m.keyed([[1, m("a")], [2, m("b")]]) m.render(G.root, vnodes) m.render(G.root, updated) o(Array.from(G.root.childNodes, (n) => n.nodeName)).deepEquals(["A", "B"]) - o(updated[0].c[0].d).equals(G.root.childNodes[0]) - o(updated[1].c[0].d).equals(G.root.childNodes[1]) + o([...updated.a][0][1].d).equals(G.root.childNodes[0]) + o([...updated.a][1][1].d).equals(G.root.childNodes[1]) }) o("handles el noop without key", function() { var vnodes = [m("a"), m("b")] @@ -90,404 +90,385 @@ o.spec("updateNodes", function() { o(G.root.childNodes.length).equals(1) }) o("reverses els w/ even count", function() { - var vnodes = [m.key(1, m("a")), m.key(2, m("b")), m.key(3, m("i")), m.key(4, m("s"))] - var updated = [m.key(4, m("s")), m.key(3, m("i")), m.key(2, m("b")), m.key(1, m("a"))] + var vnodes = m.keyed([[1, m("a")], [2, m("b")], [3, m("i")], [4, m("s")]]) + var updated = m.keyed([[4, m("s")], [3, m("i")], [2, m("b")], [1, m("a")]]) m.render(G.root, vnodes) m.render(G.root, updated) o(Array.from(G.root.childNodes, (n) => n.nodeName)).deepEquals(["S", "I", "B", "A"]) - o(updated[0].c[0].d).equals(G.root.childNodes[0]) - o(updated[1].c[0].d).equals(G.root.childNodes[1]) - o(updated[2].c[0].d).equals(G.root.childNodes[2]) - o(updated[3].c[0].d).equals(G.root.childNodes[3]) + o([...updated.a][0][1].d).equals(G.root.childNodes[0]) + o([...updated.a][1][1].d).equals(G.root.childNodes[1]) + o([...updated.a][2][1].d).equals(G.root.childNodes[2]) + o([...updated.a][3][1].d).equals(G.root.childNodes[3]) }) o("reverses els w/ odd count", function() { - var vnodes = [m.key(1, m("a")), m.key(2, m("b")), m.key(3, m("i"))] - var updated = [m.key(3, m("i")), m.key(2, m("b")), m.key(1, m("a"))] + var vnodes = m.keyed([[1, m("a")], [2, m("b")], [3, m("i")]]) + var updated = m.keyed([[3, m("i")], [2, m("b")], [1, m("a")]]) m.render(G.root, vnodes) m.render(G.root, updated) o(Array.from(G.root.childNodes, (n) => n.nodeName)).deepEquals(["I", "B", "A"]) }) o("creates el at start", function() { - var vnodes = [m.key(1, m("a"))] - var updated = [m.key(2, m("b")), m.key(1, m("a"))] + var vnodes = m.keyed([[1, m("a")]]) + var updated = m.keyed([[2, m("b")], [1, m("a")]]) m.render(G.root, vnodes) m.render(G.root, updated) o(Array.from(G.root.childNodes, (n) => n.nodeName)).deepEquals(["B", "A"]) - o(updated[0].c[0].d).equals(G.root.childNodes[0]) - o(updated[1].c[0].d).equals(G.root.childNodes[1]) + o([...updated.a][0][1].d).equals(G.root.childNodes[0]) + o([...updated.a][1][1].d).equals(G.root.childNodes[1]) }) o("creates el at end", function() { - var vnodes = [m.key(1, m("a"))] - var updated = [m.key(1, m("a")), m.key(2, m("b"))] + var vnodes = m.keyed([[1, m("a")]]) + var updated = m.keyed([[1, m("a")], [2, m("b")]]) m.render(G.root, vnodes) m.render(G.root, updated) o(Array.from(G.root.childNodes, (n) => n.nodeName)).deepEquals(["A", "B"]) - o(updated[0].c[0].d).equals(G.root.childNodes[0]) - o(updated[1].c[0].d).equals(G.root.childNodes[1]) + o([...updated.a][0][1].d).equals(G.root.childNodes[0]) + o([...updated.a][1][1].d).equals(G.root.childNodes[1]) }) o("creates el in middle", function() { - var vnodes = [m.key(1, m("a")), m.key(2, m("b"))] - var updated = [m.key(1, m("a")), m.key(3, m("i")), m.key(2, m("b"))] + var vnodes = m.keyed([[1, m("a")], [2, m("b")]]) + var updated = m.keyed([[1, m("a")], [3, m("i")], [2, m("b")]]) m.render(G.root, vnodes) m.render(G.root, updated) o(Array.from(G.root.childNodes, (n) => n.nodeName)).deepEquals(["A", "I", "B"]) - o(updated[0].c[0].d).equals(G.root.childNodes[0]) - o(updated[1].c[0].d).equals(G.root.childNodes[1]) - o(updated[2].c[0].d).equals(G.root.childNodes[2]) + o([...updated.a][0][1].d).equals(G.root.childNodes[0]) + o([...updated.a][1][1].d).equals(G.root.childNodes[1]) + o([...updated.a][2][1].d).equals(G.root.childNodes[2]) }) o("creates el while reversing", function() { - var vnodes = [m.key(1, m("a")), m.key(2, m("b"))] - var updated = [m.key(2, m("b")), m.key(3, m("i")), m.key(1, m("a"))] + var vnodes = m.keyed([[1, m("a")], [2, m("b")]]) + var updated = m.keyed([[2, m("b")], [3, m("i")], [1, m("a")]]) m.render(G.root, vnodes) m.render(G.root, updated) o(Array.from(G.root.childNodes, (n) => n.nodeName)).deepEquals(["B", "I", "A"]) - o(updated[0].c[0].d).equals(G.root.childNodes[0]) - o(updated[1].c[0].d).equals(G.root.childNodes[1]) - o(updated[2].c[0].d).equals(G.root.childNodes[2]) + o([...updated.a][0][1].d).equals(G.root.childNodes[0]) + o([...updated.a][1][1].d).equals(G.root.childNodes[1]) + o([...updated.a][2][1].d).equals(G.root.childNodes[2]) }) o("deletes el at start", function() { - var vnodes = [m.key(2, m("b")), m.key(1, m("a"))] - var updated = [m.key(1, m("a"))] + var vnodes = m.keyed([[2, m("b")], [1, m("a")]]) + var updated = m.keyed([[1, m("a")]]) m.render(G.root, vnodes) m.render(G.root, updated) o(Array.from(G.root.childNodes, (n) => n.nodeName)).deepEquals(["A"]) - o(updated[0].c[0].d).equals(G.root.childNodes[0]) + o([...updated.a][0][1].d).equals(G.root.childNodes[0]) }) o("deletes el at end", function() { - var vnodes = [m.key(1, m("a")), m.key(2, m("b"))] - var updated = [m.key(1, m("a"))] + var vnodes = m.keyed([[1, m("a")], [2, m("b")]]) + var updated = m.keyed([[1, m("a")]]) m.render(G.root, vnodes) m.render(G.root, updated) o(Array.from(G.root.childNodes, (n) => n.nodeName)).deepEquals(["A"]) - o(updated[0].c[0].d).equals(G.root.childNodes[0]) + o([...updated.a][0][1].d).equals(G.root.childNodes[0]) }) o("deletes el at middle", function() { - var vnodes = [m.key(1, m("a")), m.key(3, m("i")), m.key(2, m("b"))] - var updated = [m.key(1, m("a")), m.key(2, m("b"))] + var vnodes = m.keyed([[1, m("a")], [3, m("i")], [2, m("b")]]) + var updated = m.keyed([[1, m("a")], [2, m("b")]]) m.render(G.root, vnodes) m.render(G.root, updated) o(Array.from(G.root.childNodes, (n) => n.nodeName)).deepEquals(["A", "B"]) - o(updated[0].c[0].d).equals(G.root.childNodes[0]) - o(updated[1].c[0].d).equals(G.root.childNodes[1]) + o([...updated.a][0][1].d).equals(G.root.childNodes[0]) + o([...updated.a][1][1].d).equals(G.root.childNodes[1]) }) o("deletes el while reversing", function() { - var vnodes = [m.key(1, m("a")), m.key(3, m("i")), m.key(2, m("b"))] - var updated = [m.key(2, m("b")), m.key(1, m("a"))] + var vnodes = m.keyed([[1, m("a")], [3, m("i")], [2, m("b")]]) + var updated = m.keyed([[2, m("b")], [1, m("a")]]) m.render(G.root, vnodes) m.render(G.root, updated) o(Array.from(G.root.childNodes, (n) => n.nodeName)).deepEquals(["B", "A"]) - o(updated[0].c[0].d).equals(G.root.childNodes[0]) - o(updated[1].c[0].d).equals(G.root.childNodes[1]) + o([...updated.a][0][1].d).equals(G.root.childNodes[0]) + o([...updated.a][1][1].d).equals(G.root.childNodes[1]) }) o("creates, deletes, reverses els at same time", function() { - var vnodes = [m.key(1, m("a")), m.key(3, m("i")), m.key(2, m("b"))] - var updated = [m.key(2, m("b")), m.key(1, m("a")), m.key(4, m("s"))] + var vnodes = m.keyed([[1, m("a")], [3, m("i")], [2, m("b")]]) + var updated = m.keyed([[2, m("b")], [1, m("a")], [4, m("s")]]) m.render(G.root, vnodes) m.render(G.root, updated) o(Array.from(G.root.childNodes, (n) => n.nodeName)).deepEquals(["B", "A", "S"]) - o(updated[0].c[0].d).equals(G.root.childNodes[0]) - o(updated[1].c[0].d).equals(G.root.childNodes[1]) - o(updated[2].c[0].d).equals(G.root.childNodes[2]) + o([...updated.a][0][1].d).equals(G.root.childNodes[0]) + o([...updated.a][1][1].d).equals(G.root.childNodes[1]) + o([...updated.a][2][1].d).equals(G.root.childNodes[2]) }) o("creates, deletes, reverses els at same time with '__proto__' key", function() { - var vnodes = [m.key("__proto__", m("a")), m.key(3, m("i")), m.key(2, m("b"))] - var updated = [m.key(2, m("b")), m.key("__proto__", m("a")), m.key(4, m("s"))] + var vnodes = m.keyed([["__proto__", m("a")], [3, m("i")], [2, m("b")]]) + var updated = m.keyed([[2, m("b")], ["__proto__", m("a")], [4, m("s")]]) m.render(G.root, vnodes) m.render(G.root, updated) o(Array.from(G.root.childNodes, (n) => n.nodeName)).deepEquals(["B", "A", "S"]) - o(updated[0].c[0].d).equals(G.root.childNodes[0]) - o(updated[1].c[0].d).equals(G.root.childNodes[1]) - o(updated[2].c[0].d).equals(G.root.childNodes[2]) + o([...updated.a][0][1].d).equals(G.root.childNodes[0]) + o([...updated.a][1][1].d).equals(G.root.childNodes[1]) + o([...updated.a][2][1].d).equals(G.root.childNodes[2]) }) o("adds to empty fragment followed by el", function() { - var vnodes = [m.key(1), m.key(2, m("b"))] - var updated = [m.key(1, m("a")), m.key(2, m("b"))] + var vnodes = m.keyed([[1, []], [2, m("b")]]) + var updated = m.keyed([[1, m("a")], [2, m("b")]]) m.render(G.root, vnodes) m.render(G.root, updated) o(Array.from(G.root.childNodes, (n) => n.nodeName)).deepEquals(["A", "B"]) - o(updated[0].c[0].d).equals(G.root.childNodes[0]) - o(updated[1].c[0].d).equals(G.root.childNodes[1]) + o([...updated.a][0][1].d).equals(G.root.childNodes[0]) + o([...updated.a][1][1].d).equals(G.root.childNodes[1]) }) o("reverses followed by el", function() { - var vnodes = [m.key(1, m.key(2, m("a")), m.key(3, m("b"))), m.key(4, m("i"))] - var updated = [m.key(1, m.key(3, m("b")), m.key(2, m("a"))), m.key(4, m("i"))] + var vnodes = m.keyed([[1, m.keyed([[2, m("a")], [3, m("b")]])], [4, m("i")]]) + var updated = m.keyed([[1, m.keyed([[3, m("b")], [2, m("a")]])], [4, m("i")]]) m.render(G.root, vnodes) m.render(G.root, updated) o(Array.from(G.root.childNodes, (n) => n.nodeName)).deepEquals(["B", "A", "I"]) - o(updated[0].c[0].c[0].d).equals(G.root.childNodes[0]) - o(updated[0].c[1].c[0].d).equals(G.root.childNodes[1]) - o(updated[1].c[0].d).equals(G.root.childNodes[2]) + o([...[...updated.a][0][1].a][0][1].d).equals(G.root.childNodes[0]) + o([...[...updated.a][0][1].a][1][1].d).equals(G.root.childNodes[1]) + o([...updated.a][1][1].d).equals(G.root.childNodes[2]) }) o("populates fragment followed by el keyed", function() { - var vnodes = [m.key(1), m.key(2, m("i"))] - var updated = [m.key(1, m("a"), m("b")), m.key(2, m("i"))] + var vnodes = m.keyed([[1, []], [2, m("i")]]) + var updated = m.keyed([[1, [m("a"), m("b")]], [2, m("i")]]) m.render(G.root, vnodes) m.render(G.root, updated) o(Array.from(G.root.childNodes, (n) => n.nodeName)).deepEquals(["A", "B", "I"]) - o(updated[0].c[0].d).equals(G.root.childNodes[0]) - o(updated[0].c[1].d).equals(G.root.childNodes[1]) - o(updated[1].c[0].d).equals(G.root.childNodes[2]) - }) - o("throws if fragment followed by null then el on first render keyed", function() { - var vnodes = [m.key(1), null, m.key(2, m("i"))] - - o(() => m.render(G.root, vnodes)).throws(TypeError) - }) - o("throws if fragment followed by null then el on next render keyed", function() { - var vnodes = [m.key(1), m.key(2, m("i"))] - var updated = [m.key(1, m("a"), m("b")), null, m.key(2, m("i"))] - - m.render(G.root, vnodes) - o(() => m.render(G.root, updated)).throws(TypeError) + o([...updated.a][0][1].c[0].d).equals(G.root.childNodes[0]) + o([...updated.a][0][1].c[1].d).equals(G.root.childNodes[1]) + o([...updated.a][1][1].d).equals(G.root.childNodes[2]) }) o("populates childless fragment replaced followed by el keyed", function() { - var vnodes = [m.key(1), m.key(2, m("i"))] - var updated = [m.key(1, m("a"), m("b")), m.key(2, m("i"))] + var vnodes = m.keyed([[1, []], [2, m("i")]]) + var updated = m.keyed([[1, [m("a"), m("b")]], [2, m("i")]]) m.render(G.root, vnodes) m.render(G.root, updated) o(Array.from(G.root.childNodes, (n) => n.nodeName)).deepEquals(["A", "B", "I"]) - o(updated[0].c[0].d).equals(G.root.childNodes[0]) - o(updated[0].c[1].d).equals(G.root.childNodes[1]) - o(updated[1].c[0].d).equals(G.root.childNodes[2]) - }) - o("throws if childless fragment replaced followed by null then el keyed", function() { - var vnodes = [m.key(1), m.key(2, m("i"))] - var updated = [m.key(1, m("a"), m("b")), null, m.key(2, m("i"))] - - m.render(G.root, vnodes) - o(() => m.render(G.root, updated)).throws(TypeError) + o([...updated.a][0][1].c[0].d).equals(G.root.childNodes[0]) + o([...updated.a][0][1].c[1].d).equals(G.root.childNodes[1]) + o([...updated.a][1][1].d).equals(G.root.childNodes[2]) }) o("moves from end to start", function() { - var vnodes = [m.key(1, m("a")), m.key(2, m("b")), m.key(3, m("i")), m.key(4, m("s"))] - var updated = [m.key(4, m("s")), m.key(1, m("a")), m.key(2, m("b")), m.key(3, m("i"))] + var vnodes = m.keyed([[1, m("a")], [2, m("b")], [3, m("i")], [4, m("s")]]) + var updated = m.keyed([[4, m("s")], [1, m("a")], [2, m("b")], [3, m("i")]]) m.render(G.root, vnodes) m.render(G.root, updated) o(Array.from(G.root.childNodes, (n) => n.nodeName)).deepEquals(["S", "A", "B", "I"]) - o(updated[0].c[0].d).equals(G.root.childNodes[0]) - o(updated[1].c[0].d).equals(G.root.childNodes[1]) - o(updated[2].c[0].d).equals(G.root.childNodes[2]) - o(updated[3].c[0].d).equals(G.root.childNodes[3]) + o([...updated.a][0][1].d).equals(G.root.childNodes[0]) + o([...updated.a][1][1].d).equals(G.root.childNodes[1]) + o([...updated.a][2][1].d).equals(G.root.childNodes[2]) + o([...updated.a][3][1].d).equals(G.root.childNodes[3]) }) o("moves from start to end", function() { - var vnodes = [m.key(1, m("a")), m.key(2, m("b")), m.key(3, m("i")), m.key(4, m("s"))] - var updated = [m.key(2, m("b")), m.key(3, m("i")), m.key(4, m("s")), m.key(1, m("a"))] + var vnodes = m.keyed([[1, m("a")], [2, m("b")], [3, m("i")], [4, m("s")]]) + var updated = m.keyed([[2, m("b")], [3, m("i")], [4, m("s")], [1, m("a")]]) m.render(G.root, vnodes) m.render(G.root, updated) o(Array.from(G.root.childNodes, (n) => n.nodeName)).deepEquals(["B", "I", "S", "A"]) - o(updated[0].c[0].d).equals(G.root.childNodes[0]) - o(updated[1].c[0].d).equals(G.root.childNodes[1]) - o(updated[2].c[0].d).equals(G.root.childNodes[2]) - o(updated[3].c[0].d).equals(G.root.childNodes[3]) + o([...updated.a][0][1].d).equals(G.root.childNodes[0]) + o([...updated.a][1][1].d).equals(G.root.childNodes[1]) + o([...updated.a][2][1].d).equals(G.root.childNodes[2]) + o([...updated.a][3][1].d).equals(G.root.childNodes[3]) }) o("removes then recreate", function() { - var vnodes = [m.key(1, m("a")), m.key(2, m("b")), m.key(3, m("i")), m.key(4, m("s"))] - var temp = [] - var updated = [m.key(1, m("a")), m.key(2, m("b")), m.key(3, m("i")), m.key(4, m("s"))] + var vnodes = m.keyed([[1, m("a")], [2, m("b")], [3, m("i")], [4, m("s")]]) + var temp = m.keyed([]) + var updated = m.keyed([[1, m("a")], [2, m("b")], [3, m("i")], [4, m("s")]]) m.render(G.root, vnodes) m.render(G.root, temp) m.render(G.root, updated) o(Array.from(G.root.childNodes, (n) => n.nodeName)).deepEquals(["A", "B", "I", "S"]) - o(updated[0].c[0].d).equals(G.root.childNodes[0]) - o(updated[1].c[0].d).equals(G.root.childNodes[1]) - o(updated[2].c[0].d).equals(G.root.childNodes[2]) - o(updated[3].c[0].d).equals(G.root.childNodes[3]) + o([...updated.a][0][1].d).equals(G.root.childNodes[0]) + o([...updated.a][1][1].d).equals(G.root.childNodes[1]) + o([...updated.a][2][1].d).equals(G.root.childNodes[2]) + o([...updated.a][3][1].d).equals(G.root.childNodes[3]) }) o("removes then recreate reversed", function() { - var vnodes = [m.key(1, m("a")), m.key(2, m("b")), m.key(3, m("i")), m.key(4, m("s"))] - var temp = [] - var updated = [m.key(4, m("s")), m.key(3, m("i")), m.key(2, m("b")), m.key(1, m("a"))] + var vnodes = m.keyed([[1, m("a")], [2, m("b")], [3, m("i")], [4, m("s")]]) + var temp = m.keyed([]) + var updated = m.keyed([[4, m("s")], [3, m("i")], [2, m("b")], [1, m("a")]]) m.render(G.root, vnodes) m.render(G.root, temp) m.render(G.root, updated) o(Array.from(G.root.childNodes, (n) => n.nodeName)).deepEquals(["S", "I", "B", "A"]) - o(updated[0].c[0].d).equals(G.root.childNodes[0]) - o(updated[1].c[0].d).equals(G.root.childNodes[1]) - o(updated[2].c[0].d).equals(G.root.childNodes[2]) - o(updated[3].c[0].d).equals(G.root.childNodes[3]) + o([...updated.a][0][1].d).equals(G.root.childNodes[0]) + o([...updated.a][1][1].d).equals(G.root.childNodes[1]) + o([...updated.a][2][1].d).equals(G.root.childNodes[2]) + o([...updated.a][3][1].d).equals(G.root.childNodes[3]) }) o("removes then recreate smaller", function() { - var vnodes = [m.key(1, m("a")), m.key(2, m("b"))] - var temp = [] - var updated = [m.key(1, m("a"))] + var vnodes = m.keyed([[1, m("a")], [2, m("b")]]) + var temp = m.keyed([]) + var updated = m.keyed([[1, m("a")]]) m.render(G.root, vnodes) m.render(G.root, temp) m.render(G.root, updated) o(Array.from(G.root.childNodes, (n) => n.nodeName)).deepEquals(["A"]) - o(updated[0].c[0].d).equals(G.root.childNodes[0]) + o([...updated.a][0][1].d).equals(G.root.childNodes[0]) }) o("removes then recreate bigger", function() { - var vnodes = [m.key(1, m("a")), m.key(2, m("b"))] - var temp = [] - var updated = [m.key(1, m("a")), m.key(2, m("b")), m.key(3, m("i"))] + var vnodes = m.keyed([[1, m("a")], [2, m("b")]]) + var temp = m.keyed([]) + var updated = m.keyed([[1, m("a")], [2, m("b")], [3, m("i")]]) m.render(G.root, vnodes) m.render(G.root, temp) m.render(G.root, updated) o(Array.from(G.root.childNodes, (n) => n.nodeName)).deepEquals(["A", "B", "I"]) - o(updated[0].c[0].d).equals(G.root.childNodes[0]) - o(updated[1].c[0].d).equals(G.root.childNodes[1]) - o(updated[2].c[0].d).equals(G.root.childNodes[2]) + o([...updated.a][0][1].d).equals(G.root.childNodes[0]) + o([...updated.a][1][1].d).equals(G.root.childNodes[1]) + o([...updated.a][2][1].d).equals(G.root.childNodes[2]) }) o("removes then create different", function() { - var vnodes = [m.key(1, m("a")), m.key(2, m("b"))] - var temp = [] - var updated = [m.key(3, m("i")), m.key(4, m("s"))] + var vnodes = m.keyed([[1, m("a")], [2, m("b")]]) + var temp = m.keyed([]) + var updated = m.keyed([[3, m("i")], [4, m("s")]]) m.render(G.root, vnodes) m.render(G.root, temp) m.render(G.root, updated) o(Array.from(G.root.childNodes, (n) => n.nodeName)).deepEquals(["I", "S"]) - o(updated[0].c[0].d).equals(G.root.childNodes[0]) - o(updated[1].c[0].d).equals(G.root.childNodes[1]) + o([...updated.a][0][1].d).equals(G.root.childNodes[0]) + o([...updated.a][1][1].d).equals(G.root.childNodes[1]) }) o("removes then create different smaller", function() { - var vnodes = [m.key(1, m("a")), m.key(2, m("b"))] - var temp = [] - var updated = [m.key(3, m("i"))] + var vnodes = m.keyed([[1, m("a")], [2, m("b")]]) + var temp = m.keyed([]) + var updated = m.keyed([[3, m("i")]]) m.render(G.root, vnodes) m.render(G.root, temp) m.render(G.root, updated) o(Array.from(G.root.childNodes, (n) => n.nodeName)).deepEquals(["I"]) - o(updated[0].c[0].d).equals(G.root.childNodes[0]) + o([...updated.a][0][1].d).equals(G.root.childNodes[0]) }) o("removes then create different bigger", function() { - var vnodes = [m.key(1, m("a")), m.key(2, m("b"))] - var temp = [] - var updated = [m.key(3, m("i")), m.key(4, m("s")), m.key(5, m("div"))] + var vnodes = m.keyed([[1, m("a")], [2, m("b")]]) + var temp = m.keyed([]) + var updated = m.keyed([[3, m("i")], [4, m("s")], [5, m("div")]]) m.render(G.root, vnodes) m.render(G.root, temp) m.render(G.root, updated) o(Array.from(G.root.childNodes, (n) => n.nodeName)).deepEquals(["I", "S", "DIV"]) - o(updated[0].c[0].d).equals(G.root.childNodes[0]) - o(updated[1].c[0].d).equals(G.root.childNodes[1]) - o(updated[2].c[0].d).equals(G.root.childNodes[2]) + o([...updated.a][0][1].d).equals(G.root.childNodes[0]) + o([...updated.a][1][1].d).equals(G.root.childNodes[1]) + o([...updated.a][2][1].d).equals(G.root.childNodes[2]) }) o("removes then create mixed", function() { - var vnodes = [m.key(1, m("a")), m.key(2, m("b"))] - var temp = [] - var updated = [m.key(1, m("a")), m.key(4, m("s"))] + var vnodes = m.keyed([[1, m("a")], [2, m("b")]]) + var temp = m.keyed([]) + var updated = m.keyed([[1, m("a")], [4, m("s")]]) m.render(G.root, vnodes) m.render(G.root, temp) m.render(G.root, updated) o(Array.from(G.root.childNodes, (n) => n.nodeName)).deepEquals(["A", "S"]) - o(updated[0].c[0].d).equals(G.root.childNodes[0]) - o(updated[1].c[0].d).equals(G.root.childNodes[1]) + o([...updated.a][0][1].d).equals(G.root.childNodes[0]) + o([...updated.a][1][1].d).equals(G.root.childNodes[1]) }) o("removes then create mixed reversed", function() { - var vnodes = [m.key(1, m("a")), m.key(2, m("b"))] - var temp = [] - var updated = [m.key(4, m("s")), m.key(1, m("a"))] + var vnodes = m.keyed([[1, m("a")], [2, m("b")]]) + var temp = m.keyed([]) + var updated = m.keyed([[4, m("s")], [1, m("a")]]) m.render(G.root, vnodes) m.render(G.root, temp) m.render(G.root, updated) o(Array.from(G.root.childNodes, (n) => n.nodeName)).deepEquals(["S", "A"]) - o(updated[0].c[0].d).equals(G.root.childNodes[0]) - o(updated[1].c[0].d).equals(G.root.childNodes[1]) + o([...updated.a][0][1].d).equals(G.root.childNodes[0]) + o([...updated.a][1][1].d).equals(G.root.childNodes[1]) }) o("removes then create mixed smaller", function() { - var vnodes = [m.key(1, m("a")), m.key(2, m("b")), m.key(3, m("i"))] - var temp = [] - var updated = [m.key(1, m("a")), m.key(4, m("s"))] + var vnodes = m.keyed([[1, m("a")], [2, m("b")], [3, m("i")]]) + var temp = m.keyed([]) + var updated = m.keyed([[1, m("a")], [4, m("s")]]) m.render(G.root, vnodes) m.render(G.root, temp) m.render(G.root, updated) o(Array.from(G.root.childNodes, (n) => n.nodeName)).deepEquals(["A", "S"]) - o(updated[0].c[0].d).equals(G.root.childNodes[0]) - o(updated[1].c[0].d).equals(G.root.childNodes[1]) + o([...updated.a][0][1].d).equals(G.root.childNodes[0]) + o([...updated.a][1][1].d).equals(G.root.childNodes[1]) }) o("removes then create mixed smaller reversed", function() { - var vnodes = [m.key(1, m("a")), m.key(2, m("b")), m.key(3, m("i"))] - var temp = [] - var updated = [m.key(4, m("s")), m.key(1, m("a"))] + var vnodes = m.keyed([[1, m("a")], [2, m("b")], [3, m("i")]]) + var temp = m.keyed([]) + var updated = m.keyed([[4, m("s")], [1, m("a")]]) m.render(G.root, vnodes) m.render(G.root, temp) m.render(G.root, updated) o(Array.from(G.root.childNodes, (n) => n.nodeName)).deepEquals(["S", "A"]) - o(updated[0].c[0].d).equals(G.root.childNodes[0]) - o(updated[1].c[0].d).equals(G.root.childNodes[1]) + o([...updated.a][0][1].d).equals(G.root.childNodes[0]) + o([...updated.a][1][1].d).equals(G.root.childNodes[1]) }) o("removes then create mixed bigger", function() { - var vnodes = [m.key(1, m("a")), m.key(2, m("b"))] - var temp = [] - var updated = [m.key(1, m("a")), m.key(3, m("i")), m.key(4, m("s"))] + var vnodes = m.keyed([[1, m("a")], [2, m("b")]]) + var temp = m.keyed([]) + var updated = m.keyed([[1, m("a")], [3, m("i")], [4, m("s")]]) m.render(G.root, vnodes) m.render(G.root, temp) m.render(G.root, updated) o(Array.from(G.root.childNodes, (n) => n.nodeName)).deepEquals(["A", "I", "S"]) - o(updated[0].c[0].d).equals(G.root.childNodes[0]) - o(updated[1].c[0].d).equals(G.root.childNodes[1]) - o(updated[2].c[0].d).equals(G.root.childNodes[2]) + o([...updated.a][0][1].d).equals(G.root.childNodes[0]) + o([...updated.a][1][1].d).equals(G.root.childNodes[1]) + o([...updated.a][2][1].d).equals(G.root.childNodes[2]) }) o("removes then create mixed bigger reversed", function() { - var vnodes = [m.key(1, m("a")), m.key(2, m("b"))] - var temp = [] - var updated = [m.key(4, m("s")), m.key(3, m("i")), m.key(1, m("a"))] + var vnodes = m.keyed([[1, m("a")], [2, m("b")]]) + var temp = m.keyed([]) + var updated = m.keyed([[4, m("s")], [3, m("i")], [1, m("a")]]) m.render(G.root, vnodes) m.render(G.root, temp) m.render(G.root, updated) o(Array.from(G.root.childNodes, (n) => n.nodeName)).deepEquals(["S", "I", "A"]) - o(updated[0].c[0].d).equals(G.root.childNodes[0]) - o(updated[1].c[0].d).equals(G.root.childNodes[1]) - o(updated[2].c[0].d).equals(G.root.childNodes[2]) + o([...updated.a][0][1].d).equals(G.root.childNodes[0]) + o([...updated.a][1][1].d).equals(G.root.childNodes[1]) + o([...updated.a][2][1].d).equals(G.root.childNodes[2]) }) o("in fragment, nest text inside fragment and add hole", function() { var vnodes = ["a"] @@ -517,10 +498,10 @@ o.spec("updateNodes", function() { o(G.root.firstChild.childNodes.length).equals(1) }) o("removes then recreates then reverses children", function() { - var vnodes = [m.key(1, m("a", m.key(3, m("i")), m.key(4, m("s")))), m.key(2, m("b"))] - var temp1 = [] - var temp2 = [m.key(1, m("a", m.key(3, m("i")), m.key(4, m("s")))), m.key(2, m("b"))] - var updated = [m.key(1, m("a", m.key(4, m("s")), m.key(3, m("i")))), m.key(2, m("b"))] + var vnodes = m.keyed([[1, m("a", m.keyed([[3, m("i")], [4, m("s")]]))], [2, m("b")]]) + var temp1 = m.keyed([]) + var temp2 = m.keyed([[1, m("a", m.keyed([[3, m("i")], [4, m("s")]]))], [2, m("b")]]) + var updated = m.keyed([[1, m("a", m.keyed([[4, m("s")], [3, m("i")]]))], [2, m("b")]]) m.render(G.root, vnodes) m.render(G.root, temp1) @@ -529,15 +510,15 @@ o.spec("updateNodes", function() { o(Array.from(G.root.childNodes, (n) => n.nodeName)).deepEquals(["A", "B"]) o(Array.from(G.root.childNodes[0].childNodes, (n) => n.nodeName)).deepEquals(["S", "I"]) - o(updated[0].c[0].d).equals(G.root.childNodes[0]) - o(updated[1].c[0].d).equals(G.root.childNodes[1]) - o(updated[0].c[0].c[0].c[0].d).equals(G.root.childNodes[0].childNodes[0]) - o(updated[0].c[0].c[1].c[0].d).equals(G.root.childNodes[0].childNodes[1]) + o([...updated.a][0][1].d).equals(G.root.childNodes[0]) + o([...updated.a][1][1].d).equals(G.root.childNodes[1]) + o([...[...updated.a][0][1].c[0].a][0][1].d).equals(G.root.childNodes[0].childNodes[0]) + o([...[...updated.a][0][1].c[0].a][1][1].d).equals(G.root.childNodes[0].childNodes[1]) }) o("removes then recreates nested", function() { - var vnodes = [m.key(1, m("a", m.key(3, m("a", m.key(5, m("a")))), m.key(4, m("a", m.key(5, m("a")))))), m.key(2, m("a"))] - var temp = [] - var updated = [m.key(1, m("a", m.key(3, m("a", m.key(5, m("a")))), m.key(4, m("a", m.key(5, m("a")))))), m.key(2, m("a"))] + var vnodes = m.keyed([[1, m("a", m.keyed([[3, m("a", m.keyed([[5, m("a")]]))], [4, m("a", m.keyed([[5, m("a")]]))]]))], [2, m("a")]]) + var temp = m.keyed([]) + var updated = m.keyed([[1, m("a", m.keyed([[3, m("a", m.keyed([[5, m("a")]]))], [4, m("a", m.keyed([[5, m("a")]]))]]))], [2, m("a")]]) m.render(G.root, vnodes) m.render(G.root, temp) @@ -780,13 +761,13 @@ o.spec("updateNodes", function() { o(before).equals(after) }) o("node is recreated if unwrapped from a key", function () { - var vnode = m.key(1, m("b")) + var vnode = m.keyed([[1, m("b")]]) var updated = m("b") m.render(G.root, vnode) m.render(G.root, updated) - o(vnode.c[0].d).notEquals(updated.d) + o([...vnode.a][0][1].d).notEquals(updated.d) }) o("don't add back elements from fragments that are restored from the pool #1991", function() { m.render(G.root, [ @@ -833,28 +814,28 @@ o.spec("updateNodes", function() { o(remove.callCount).equals(1) }) o("supports changing the element of a keyed element in a list when traversed bottom-up", function() { - m.render(G.root, [m.key(2, m("a"))]) - m.render(G.root, [m.key(1, m("b")), m.key(2, m("b"))]) + m.render(G.root, m.keyed([[2, m("a")]])) + m.render(G.root, m.keyed([[1, m("b")], [2, m("b")]])) o(Array.from(G.root.childNodes, (n) => n.nodeName)).deepEquals(["B", "B"]) }) o("supports changing the element of a keyed element in a list when looking up nodes using the map", function() { - m.render(G.root, [m.key(1, m("x")), m.key(2, m("y")), m.key(3, m("z"))]) - m.render(G.root, [m.key(2, m("b")), m.key(1, m("c")), m.key(4, m("d")), m.key(3, m("e"))]) + m.render(G.root, m.keyed([[1, m("x")], [2, m("y")], [3, m("z")]])) + m.render(G.root, m.keyed([[2, m("b")], [1, m("c")], [4, m("d")], [3, m("e")]])) o(Array.from(G.root.childNodes, (n) => n.nodeName)).deepEquals(["B", "C", "D", "E"]) }) o("don't fetch the nextSibling from the pool", function() { - m.render(G.root, [[m.key(1, m("div")), m.key(2, m("div"))], m("p")]) - m.render(G.root, [[], m("p")]) - m.render(G.root, [[m.key(2, m("div")), m.key(1, m("div"))], m("p")]) + m.render(G.root, [m.keyed([[1, m("div")], [2, m("div")]]), m("p")]) + m.render(G.root, [m.keyed([]), m("p")]) + m.render(G.root, [m.keyed([[2, m("div")], [1, m("div")]]), m("p")]) o(Array.from(G.root.childNodes, (el) => el.nodeName)).deepEquals(["DIV", "DIV", "P"]) }) o("reverses a keyed lists with an odd number of items", function() { var vnodes = vnodify("a,b,c,d") var updated = vnodify("d,c,b,a") - var expectedTagNames = updated.map((vn) => vn.c[0].t) + var expectedTagNames = [...updated.a.keys()] m.render(G.root, vnodes) m.render(G.root, updated) @@ -866,9 +847,9 @@ o.spec("updateNodes", function() { o("reverses a keyed lists with an even number of items", function() { var vnodes = vnodify("a,b,c") var updated = vnodify("c,b,a") - var vnodes = [m.key("a", m("a")), m.key("b", m("b")), m.key("c", m("c"))] - var updated = [m.key("c", m("c")), m.key("b", m("b")), m.key("a", m("a"))] - var expectedTagNames = updated.map((vn) => vn.c[0].t) + var vnodes = m.keyed([["a", m("a")], ["b", m("b")], ["c", m("c")]]) + var updated = m.keyed([["c", m("c")], ["b", m("b")], ["a", m("a")]]) + var expectedTagNames = [...updated.a.keys()] m.render(G.root, vnodes) m.render(G.root, updated) @@ -880,7 +861,7 @@ o.spec("updateNodes", function() { o("scrambles a keyed lists with prefixes and suffixes", function() { var vnodes = vnodify("i,a,b,c,d,j") var updated = vnodify("i,b,a,d,c,j") - var expectedTagNames = updated.map((vn) => vn.c[0].t) + var expectedTagNames = [...updated.a.keys()] m.render(G.root, vnodes) m.render(G.root, updated) @@ -892,7 +873,7 @@ o.spec("updateNodes", function() { o("reverses a keyed lists with an odd number of items with prefixes and suffixes", function() { var vnodes = vnodify("i,a,b,c,d,j") var updated = vnodify("i,d,c,b,a,j") - var expectedTagNames = updated.map((vn) => vn.c[0].t) + var expectedTagNames = [...updated.a.keys()] m.render(G.root, vnodes) m.render(G.root, updated) @@ -904,7 +885,7 @@ o.spec("updateNodes", function() { o("reverses a keyed lists with an even number of items with prefixes and suffixes", function() { var vnodes = vnodify("i,a,b,c,j") var updated = vnodify("i,c,b,a,j") - var expectedTagNames = updated.map((vn) => vn.c[0].t) + var expectedTagNames = [...updated.a.keys()] m.render(G.root, vnodes) m.render(G.root, updated) @@ -916,7 +897,7 @@ o.spec("updateNodes", function() { o("scrambling sample 1", function() { var vnodes = vnodify("k0,k1,k2,k3,k4,k5,k6,k7,k8,k9") var updated = vnodify("k4,k1,k2,k9,k0,k3,k6,k5,k8,k7") - var expectedTagNames = updated.map((vn) => vn.c[0].t) + var expectedTagNames = [...updated.a.keys()] m.render(G.root, vnodes) m.render(G.root, updated) @@ -928,7 +909,7 @@ o.spec("updateNodes", function() { o("scrambling sample 2", function() { var vnodes = vnodify("k0,k1,k2,k3,k4,k5,k6,k7,k8,k9") var updated = vnodify("b,d,k1,k0,k2,k3,k4,a,c,k5,k6,k7,k8,k9") - var expectedTagNames = updated.map((vn) => vn.c[0].t) + var expectedTagNames = [...updated.a.keys()] m.render(G.root, vnodes) m.render(G.root, updated) diff --git a/tests/core/updateNodesFuzzer.js b/tests/core/updateNodesFuzzer.js index 4e6f6c72e..46b527631 100644 --- a/tests/core/updateNodesFuzzer.js +++ b/tests/core/updateNodesFuzzer.js @@ -44,8 +44,8 @@ o.spec("updateNodes keyed list Fuzzer", () => { const from = randomUnique(fromUsed) const to = randomUnique(toUsed) o(`${i}: ${from} -> ${to}`, () => { - m.render(G.root, from.map((x) => m.key(x, view(x)))) - m.render(G.root, to.map((x) => m.key(x, view(x)))) + m.render(G.root, m.keyed(from, (x) => [x, view(x)])) + m.render(G.root, m.keyed(to, (x) => [x, view(x)])) assert(G.root, to) }) } diff --git a/tests/exported-api.js b/tests/exported-api.js index 62e7dee44..193f9b44a 100644 --- a/tests/exported-api.js +++ b/tests/exported-api.js @@ -25,14 +25,15 @@ o.spec("api", function() { o(vnode.c[0].t).equals("div") }) }) - o.spec("m.key", function() { + o.spec("m.keyed", function() { o("works", function() { - var vnode = m.key(123, [m("div")]) + var vnode = m.keyed([123], (k) => [k, [m("div")]]) - o(vnode.m & m.TYPE_MASK).equals(m.TYPE_KEY) - o(vnode.t).equals(123) - o(vnode.c.length).equals(1) - o(vnode.c[0].t).equals("div") + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_KEYED) + o(vnode.a.size).equals(1) + o([...vnode.a][0][0]).equals(123) + o([...vnode.a][0][1].c.length).equals(1) + o([...vnode.a][0][1].c[0].t).equals("div") }) }) o.spec("m.p", function() { diff --git a/tests/std/q.js b/tests/std/query.js similarity index 71% rename from tests/std/q.js rename to tests/std/query.js index eb821e338..f7a507f13 100644 --- a/tests/std/q.js +++ b/tests/std/query.js @@ -2,89 +2,89 @@ import o from "ospec" import m from "../../src/entry/mithril.esm.js" -o.spec("q", () => { +o.spec("query", () => { o("handles flat object", () => { - var string = m.q({a: "b", c: 1}) + var string = m.query({a: "b", c: 1}) o(string).equals("a=b&c=1") }) o("handles escaped values", () => { - var data = m.q({";:@&=+$,/?%#": ";:@&=+$,/?%#"}) + var data = m.query({";:@&=+$,/?%#": ";:@&=+$,/?%#"}) o(data).equals("%3B%3A%40%26%3D%2B%24%2C%2F%3F%25%23=%3B%3A%40%26%3D%2B%24%2C%2F%3F%25%23") }) o("handles unicode", () => { - var data = m.q({"ö": "ö"}) + var data = m.query({"ö": "ö"}) o(data).equals("%C3%B6=%C3%B6") }) o("handles nested object in query string", () => { - var string = m.q({a: {b: 1, c: 2}}) + var string = m.query({a: {b: 1, c: 2}}) o(string).equals("a%5Bb%5D=1&a%5Bc%5D=2") }) o("handles deep nested object in query string", () => { - var string = m.q({a: {b: {c: 1, d: 2}}}) + var string = m.query({a: {b: {c: 1, d: 2}}}) o(string).equals("a%5Bb%5D%5Bc%5D=1&a%5Bb%5D%5Bd%5D=2") }) o("handles nested array in query string", () => { - var string = m.q({a: ["x", "y"]}) + var string = m.query({a: ["x", "y"]}) o(string).equals("a%5B%5D=x&a%5B%5D=y") }) o("handles array w/ dupe values in query string", () => { - var string = m.q({a: ["x", "x"]}) + var string = m.query({a: ["x", "x"]}) o(string).equals("a%5B%5D=x&a%5B%5D=x") }) o("handles deep nested array in query string", () => { - var string = m.q({a: [["x", "y"]]}) + var string = m.query({a: [["x", "y"]]}) o(string).equals("a%5B%5D%5B%5D=x&a%5B%5D%5B%5D=y") }) o("handles deep nested array in object in query string", () => { - var string = m.q({a: {b: ["x", "y"]}}) + var string = m.query({a: {b: ["x", "y"]}}) o(string).equals("a%5Bb%5D%5B%5D=x&a%5Bb%5D%5B%5D=y") }) o("handles deep nested object in array in query string", () => { - var string = m.q({a: [{b: 1, c: 2}]}) + var string = m.query({a: [{b: 1, c: 2}]}) o(string).equals("a%5B%5D%5Bb%5D=1&a%5B%5D%5Bc%5D=2") }) o("handles date in query string", () => { - var string = m.q({a: new Date(0)}) + var string = m.query({a: new Date(0)}) o(string).equals(`a=${encodeURIComponent(new Date(0).toString())}`) }) o("handles zero in query string", () => { - var string = m.q({a: 0}) + var string = m.query({a: 0}) o(string).equals("a=0") }) o("retains empty string literally", () => { - var string = m.q({a: ""}) + var string = m.query({a: ""}) o(string).equals("a=") }) o("drops `null` from query string", () => { - var string = m.q({a: null}) + var string = m.query({a: null}) o(string).equals("") }) o("drops `undefined` from query string", () => { - var string = m.q({a: undefined}) + var string = m.query({a: undefined}) o(string).equals("") }) o("turns `true` into value-less string in query string", () => { - var string = m.q({a: true}) + var string = m.query({a: true}) o(string).equals("a") }) o("drops `false` from query string", () => { - var string = m.q({a: false}) + var string = m.query({a: false}) o(string).equals("") }) From 5018ad0168c4023e765fd2cc1efd0fc0a2b35ceb Mon Sep 17 00:00:00 2001 From: Claudia Meadows Date: Tue, 29 Oct 2024 20:45:31 -0700 Subject: [PATCH 83/95] Move context to `this` argument It's very common to want context and not care about attributes. What's *not* common is wanting the old attributes and not the new. So that's what this optimizes for. --- src/core.js | 4 +- src/std/init.js | 5 ++- src/std/lazy.js | 4 +- src/std/rate-limit.js | 47 ++++++++++--------- src/std/router.js | 10 ++--- tests/core/context.js | 12 ++--- tests/exported-api.js | 4 +- tests/std/router.js | 102 +++++++++++++++++++++--------------------- 8 files changed, 96 insertions(+), 92 deletions(-) diff --git a/src/core.js b/src/core.js index 5b2443e09..68feb615d 100644 --- a/src/core.js +++ b/src/core.js @@ -688,10 +688,10 @@ var updateComponent = (old, vnode) => { tree = old.s oldInstance = old.c oldAttrs = old.a - } else if (typeof (tree = (vnode.s = vnode.t)(attrs, oldAttrs, currentContext)) !== "function") { + } else if (typeof (tree = (vnode.s = vnode.t).call(currentContext, attrs, oldAttrs)) !== "function") { break rendered } - tree = (vnode.s = tree)(attrs, oldAttrs, currentContext) + tree = (vnode.s = tree).call(currentContext, attrs, oldAttrs) } if (tree === vnode) { throw new Error("A view cannot return the vnode it received as argument") diff --git a/src/std/init.js b/src/std/init.js index 7d12e6dc5..0a7a76390 100644 --- a/src/std/init.js +++ b/src/std/init.js @@ -1,14 +1,15 @@ import m from "../core.js" -var Init = ({f}, old, {redraw}) => { +function Init({f}, old) { if (old) return m.retain() var ctrl = new AbortController() void (async () => { await 0 // wait for next microtask - if ((await f(ctrl.signal)) !== false) redraw() + if ((await f(ctrl.signal)) !== false) this.redraw() })() return m.remove(() => ctrl.abort()) } + var init = (f) => m(Init, {f}) export {init as default} diff --git a/src/std/lazy.js b/src/std/lazy.js index 308445e39..6da411e1e 100644 --- a/src/std/lazy.js +++ b/src/std/lazy.js @@ -4,8 +4,8 @@ var lazy = (opts) => { // Capture the error here so stack traces make more sense var error = new ReferenceError("Component not found") var redraws = new Set() - var Comp = (_, __, context) => { - redraws.add(context.redraw) + var Comp = function () { + redraws.add(this.redraw) return opts.pending && opts.pending() } var init = async () => { diff --git a/src/std/rate-limit.js b/src/std/rate-limit.js index 14e779373..6252cbec3 100644 --- a/src/std/rate-limit.js +++ b/src/std/rate-limit.js @@ -100,28 +100,31 @@ var rateLimiterImpl = (delay = 500, isThrottler) => { * ```js * const throttled = m.throttler() * let results, error - * return (_attrs, _old, {redraw}) => [ - * m.remove(throttled.dispose), - * m("input[type=search]", { - * async oninput(ev) { - * if (await throttled()) return false // Skip redraw if rate limited - it's pointless - * error = results = null - * redraw() - * try { - * const response = await fetch(m.p("/search", {q: ev.target.value})) - * if (response.ok) { - * results = await response.json() - * } else { - * error = await response.text() + * return function () { + * return [ + * m.remove(throttled.dispose), + * m("input[type=search]", { + * oninput: async (ev) => { + * // Skip redraw if rate limited - it's pointless + * if (await throttled()) return false + * error = results = null + * this.redraw() + * try { + * const response = await fetch(m.p("/search", {q: ev.target.value})) + * if (response.ok) { + * results = await response.json() + * } else { + * error = await response.text() + * } + * } catch (e) { + * error = e.message * } - * } catch (e) { - * error = e.message - * } - * }, - * }), - * results.map((result) => m(SearchResult, {result})), - * !error || m(ErrorDisplay, {error})), - * ] + * }, + * }), + * results.map((result) => m(SearchResult, {result})), + * !error || m(ErrorDisplay, {error})), + * ] + * } * ``` * * Important note: due to the way this is implemented in basically all runtimes, the throttler's @@ -156,7 +159,7 @@ var throttler = (delay) => rateLimiterImpl(delay, 1) * ```js * const debounced = m.debouncer() * let results, error - * return (attrs, _, {redraw}) => [ + * return (attrs) => [ * m.remove(debounced.dispose), * m("input[type=text].value", { * async oninput(ev) { diff --git a/src/std/router.js b/src/std/router.js index 3605fa619..a87b273a0 100644 --- a/src/std/router.js +++ b/src/std/router.js @@ -55,8 +55,8 @@ export var WithRouter = ({prefix, initial: href}) => { updateRouteWithHref() - return ({children}, _, context) => { - redraw = context.redraw + return function ({children}) { + redraw = this.redraw return [ m.remove(() => window.removeEventListener("popstate", updateRoute)), @@ -108,12 +108,12 @@ export var Link = () => { } } - return (attrs, old, {route: {prefix, set}}) => { - setRoute = set + return function (attrs, old) { + setRoute = this.route.set opts = attrs return [ m.layout((dom) => { - dom.href = prefix + opts.href + dom.href = this.route.prefix + opts.href if (!old) dom.addEventListener("click", listener) }), m.remove((dom) => { diff --git a/tests/core/context.js b/tests/core/context.js index b41cb39e2..c9942f589 100644 --- a/tests/core/context.js +++ b/tests/core/context.js @@ -35,7 +35,7 @@ o.spec("context", () => { m.render(G.root, vnode, {redraw}) o(Comp.callCount).equals(1) - o(allKeys(Comp.args[2])).deepEquals({ + o(allKeys(Comp.this)).deepEquals({ redraw, key: "value", one: "two", @@ -46,7 +46,7 @@ o.spec("context", () => { m.render(G.root, vnode, {redraw}) o(Comp.callCount).equals(2) - o(allKeys(Comp.args[2])).deepEquals({ + o(allKeys(Comp.this)).deepEquals({ redraw, key: "updated", two: "three", @@ -67,7 +67,7 @@ o.spec("context", () => { m.render(G.root, vnode, {redraw}) o(Comp.callCount).equals(1) - o(allKeys(Comp.args[2])).deepEquals(symbolsToStrings({ + o(allKeys(Comp.this)).deepEquals(symbolsToStrings({ redraw, [key]: "value", [one]: "two", @@ -78,7 +78,7 @@ o.spec("context", () => { m.render(G.root, vnode, {redraw}) o(Comp.callCount).equals(2) - o(allKeys(Comp.args[2])).deepEquals(symbolsToStrings({ + o(allKeys(Comp.this)).deepEquals(symbolsToStrings({ redraw, [key]: "updated", [two]: "three", @@ -97,7 +97,7 @@ o.spec("context", () => { m.render(G.root, vnode, {redraw}) o(Comp.callCount).equals(1) - o(allKeys(Comp.args[2])).deepEquals(symbolsToStrings({ + o(allKeys(Comp.this)).deepEquals(symbolsToStrings({ redraw, [key]: "value", one: "two", @@ -108,7 +108,7 @@ o.spec("context", () => { m.render(G.root, vnode, {redraw}) o(Comp.callCount).equals(2) - o(allKeys(Comp.args[2])).deepEquals(symbolsToStrings({ + o(allKeys(Comp.this)).deepEquals(symbolsToStrings({ redraw, [key]: "updated", two: "three", diff --git a/tests/exported-api.js b/tests/exported-api.js index 193f9b44a..95d2b2bd9 100644 --- a/tests/exported-api.js +++ b/tests/exported-api.js @@ -76,8 +76,8 @@ o.spec("api", function() { o.spec("m.WithRouter, m.Link", function() { o("works", async() => { var route - var App = (_attrs, _old, context) => { - route = context.route + var App = function () { + route = this.route if (route.path === "/a") { return m("div") } else if (route.path === "/b") { diff --git a/tests/std/router.js b/tests/std/router.js index e12507190..6b5fdcd4d 100644 --- a/tests/std/router.js +++ b/tests/std/router.js @@ -21,8 +21,8 @@ o.spec("route", () => { m.render(G.root, m(m.WithRouter, {prefix}, m(App))) o(App.callCount).equals(1) - o(App.args[2].route.path).equals("/") - o([...App.args[2].route.params]).deepEquals([]) + o(App.this.route.path).equals("/") + o([...App.this.route.params]).deepEquals([]) o(G.rafMock.queueLength()).equals(0) }) @@ -34,8 +34,8 @@ o.spec("route", () => { m.render(G.root, m(m.WithRouter, {prefix}, m(App))) o(App.callCount).equals(1) - o(App.args[2].route.path).equals("/test") - o([...App.args[2].route.params]).deepEquals([]) + o(App.this.route.path).equals("/test") + o([...App.this.route.params]).deepEquals([]) o(G.rafMock.queueLength()).equals(0) }) @@ -47,8 +47,8 @@ o.spec("route", () => { m.render(G.root, m(m.WithRouter, {prefix}, m(App))) o(App.callCount).equals(1) - o(App.args[2].route.path).equals("/ö") - o([...App.args[2].route.params]).deepEquals([["ö", "ö"]]) + o(App.this.route.path).equals("/ö") + o([...App.this.route.params]).deepEquals([["ö", "ö"]]) o(G.rafMock.queueLength()).equals(0) }) @@ -60,8 +60,8 @@ o.spec("route", () => { m.render(G.root, m(m.WithRouter, {prefix}, m(App))) o(App.callCount).equals(1) - o(App.args[2].route.path).equals("/ö") - o([...App.args[2].route.params]).deepEquals([["ö", "ö"]]) + o(App.this.route.path).equals("/ö") + o([...App.this.route.params]).deepEquals([["ö", "ö"]]) o(G.rafMock.queueLength()).equals(0) }) @@ -71,11 +71,11 @@ o.spec("route", () => { var spy2 = o.spy() var route - var App = (_attrs, _old, context) => { - route = context.route - if (route.path === "/a") { + var App = function () { + route = this.route + if (this.route.path === "/a") { spy1() - } else if (route.path === "/b") { + } else if (this.route.path === "/b") { spy2() } else { throw new Error(`Unknown path ${route.path}`) @@ -102,8 +102,8 @@ o.spec("route", () => { G.window.location.href = `${prefix}/test` var route - var App = (_attrs, _old, context) => { - route = context.route + var App = function () { + route = this.route } m.mount(G.root, () => m(m.WithRouter, {prefix}, m(App))) @@ -130,8 +130,8 @@ o.spec("route", () => { G.window.location.href = `${prefix}/test` var route - var App = (_attrs, _old, context) => { - route = context.route + var App = function () { + route = this.route } m.mount(G.root, () => m(m.WithRouter, {prefix}, m(App))) @@ -159,15 +159,15 @@ o.spec("route", () => { e.initEvent("click", true, true) e.button = 0 - var App = (_attrs, _old, {route}) => { - if (route.path === "/test") { + var App = function () { + if (this.route.path === "/test") { return m("a", m(m.Link, {href: "/other", replace: true})) - } else if (route.path === "/other") { + } else if (this.route.path === "/other") { return m("div") - } else if (route.path === "/") { + } else if (this.route.path === "/") { return m("span") } else { - throw new Error(`Unknown route: ${route.path}`) + throw new Error(`Unknown route: ${this.route.path}`) } } @@ -192,8 +192,8 @@ o.spec("route", () => { G.window.location.href = `${prefix}/test` var route - var App = (_attrs, _old, context) => { - route = context.route + var App = function () { + route = this.route } m.mount(G.root, () => m(m.WithRouter, {prefix}, m(App))) @@ -221,13 +221,13 @@ o.spec("route", () => { e.initEvent("click", true, true) e.button = 0 - var App = (_attrs, _old, {route}) => { - if (route.path === "/test") { + var App = function () { + if (this.route.path === "/test") { return m("a", m(m.Link, {href: "/other", replace: false})) - } else if (route.path === "/other") { + } else if (this.route.path === "/other") { return m("div") } else { - throw new Error(`Unknown route: ${route.path}`) + throw new Error(`Unknown route: ${this.route.path}`) } } @@ -252,8 +252,8 @@ o.spec("route", () => { G.window.location.href = `${prefix}/test` var route - var App = (_attrs, _old, context) => { - route = context.route + var App = function () { + route = this.route } m.mount(G.root, () => m(m.WithRouter, {prefix}, m(App))) @@ -271,8 +271,8 @@ o.spec("route", () => { G.window.location.href = `${prefix}/test` var route - var App = (_attrs, _old, context) => { - route = context.route + var App = function () { + route = this.route } m.mount(G.root, () => m(m.WithRouter, {prefix: `${prefix}/`}, m(App))) @@ -286,8 +286,8 @@ o.spec("route", () => { G.window.location.href = `${prefix}/test?a=b&c=d` var route - var App = (_attrs, _old, context) => { - route = context.route + var App = function () { + route = this.route } m.mount(G.root, () => m(m.WithRouter, {prefix}, m(App))) @@ -320,13 +320,13 @@ o.spec("route", () => { G.window.location.href = `${prefix}/` - var App = (_attrs, _old, {route}) => { - if (route.path === "/") { + var App = function () { + if (this.route.path === "/") { return m("a", m(m.Link, {href: "/test"})) - } else if (route.path === "/test") { + } else if (this.route.path === "/test") { return m("div") } else { - throw new Error(`Unknown route: ${route.path}`) + throw new Error(`Unknown route: ${this.route.path}`) } } @@ -350,13 +350,13 @@ o.spec("route", () => { e.button = 0 G.window.location.href = `${prefix}/` - var App = (_attrs, _old, {route}) => { - if (route.path === "/") { + var App = function () { + if (this.route.path === "/") { return m("a", m(m.Link, {href: "/test", state: {a: 1}})) - } else if (route.path === "/test") { + } else if (this.route.path === "/test") { return m("div") } else { - throw new Error(`Unknown route: ${route.path}`) + throw new Error(`Unknown route: ${this.route.path}`) } } @@ -379,13 +379,13 @@ o.spec("route", () => { G.window.location.href = `${prefix}/` - var App = (_attrs, _old, {route}) => { - if (route.path === "/") { + var App = function () { + if (this.route.path === "/") { return m("a", m(m.Link, {href: "/test"})) - } else if (route.path === "/test") { + } else if (this.route.path === "/test") { return m("div") } else { - throw new Error(`Unknown route: ${route.path}`) + throw new Error(`Unknown route: ${this.route.path}`) } } @@ -410,13 +410,13 @@ o.spec("route", () => { G.window.location.href = `${prefix}/` - var App = (_attrs, _old, {route}) => { - if (route.path === "/") { + var App = function () { + if (this.route.path === "/") { return m("a", {onclick(e) { e.preventDefault() }}, m(m.Link, {href: "/test"})) - } else if (route.path === "/test") { + } else if (this.route.path === "/test") { return m("div") } else { - throw new Error(`Unknown route: ${route.path}`) + throw new Error(`Unknown route: ${this.route.path}`) } } @@ -437,8 +437,8 @@ o.spec("route", () => { G.window.location.href = `${prefix}/` var route - var App = o.spy((_attrs, _old, context) => { - route = context.route + var App = o.spy(function () { + route = this.route return m("div") }) From 5d79b07491abfc1520abc753a3047c69ab3293d4 Mon Sep 17 00:00:00 2001 From: Claudia Meadows Date: Tue, 29 Oct 2024 23:12:35 -0700 Subject: [PATCH 84/95] Make routed links more concise, stop throwing when retaining nothing, tweak comment --- src/core.js | 50 ++++++++++++++++++++++++++-------------- src/entry/mithril.esm.js | 4 ++-- src/std/rate-limit.js | 3 ++- src/std/router.js | 13 +++++++---- tests/core/retain.js | 19 +++++++++++---- tests/exported-api.js | 2 +- tests/std/router.js | 12 +++++----- 7 files changed, 66 insertions(+), 37 deletions(-) diff --git a/src/core.js b/src/core.js index 68feb615d..23416b1c1 100644 --- a/src/core.js +++ b/src/core.js @@ -44,7 +44,7 @@ Retain: includes changing its type. Fragments: -- `m` bits 0-2: `0` +- `m` bits 0-3: `0` - `t`: unused - `s`: unused - `a`: unused @@ -52,15 +52,15 @@ Fragments: - `d`: unused Keyed: -- `m` bits 0-2: `1` +- `m` bits 0-3: `1` - `t`: unused - `s`: unused -- `a`: key array -- `c`: virtual DOM children +- `a`: key to child map, also holds children +- `c`: unused - `d`: unused Text: -- `m` bits 0-2: `2` +- `m` bits 0-3: `2` - `t`: unused - `s`: unused - `a`: text string @@ -68,7 +68,7 @@ Text: - `d`: abort controller reference Components: -- `m` bits 0-2: `3` +- `m` bits 0-3: `3` - `t`: component reference - `s`: view function, may be same as component reference - `a`: most recently received attributes @@ -76,7 +76,7 @@ Components: - `d`: unused DOM elements: -- `m` bits 0-2: `4` +- `m` bits 0-3: `4` - `t`: tag name string - `s`: event listener dictionary, if any events were ever registered - `a`: most recently received attributes @@ -84,7 +84,7 @@ DOM elements: - `d`: element reference Layout: -- `m` bits 0-2: `5` +- `m` bits 0-3: `5` - `t`: unused - `s`: uncaught - `a`: callback to schedule @@ -92,13 +92,29 @@ Layout: - `d`: parent DOM reference, for easier queueing Remove: -- `m` bits 0-2: `6` +- `m` bits 0-3: `6` - `t`: unused - `s`: unused - `a`: callback to schedule - `c`: unused - `d`: parent DOM reference, for easier queueing +Set context: +- `m` bits 0-3: `7` +- `t`: unused +- `s`: unused +- `a`: unused +- `c`: virtual DOM children +- `d`: unused + +Use dependencies: +- `m` bits 0-3: `8` +- `t`: unused +- `s`: unused +- `a`: Dependency array +- `c`: virtual DOM children +- `d`: unused + The `m` field is also used for various assertions, that aren't described here. */ @@ -113,6 +129,7 @@ var TYPE_LAYOUT = 5 var TYPE_REMOVE = 6 var TYPE_SET_CONTEXT = 7 var TYPE_USE = 8 +// var TYPE_RETAIN = 15 var FLAG_USED = 1 << 4 var FLAG_IS_REMOVE = 1 << 5 @@ -123,6 +140,8 @@ var FLAG_SELECT_ELEMENT = 1 << 9 var FLAG_OPTION_ELEMENT = 1 << 10 var FLAG_TEXTAREA_ELEMENT = 1 << 11 var FLAG_IS_FILE_INPUT = 1 << 12 +// Implicitly used as part of checking for `m.retain()`. +// var FLAG_IS_RETAIN = 1 << 31 var Vnode = (mask, tag, attrs, children) => ({ m: mask, @@ -460,9 +479,7 @@ var updateNode = (old, vnode) => { var type if (old == null) { if (vnode == null) return - if (vnode.m < 0) { - throw new Error("No node present to retain with `m.retain()`") - } + if (vnode.m < 0) return if (vnode.m & FLAG_USED) { throw new TypeError("Vnodes must not be reused") } @@ -473,7 +490,7 @@ var updateNode = (old, vnode) => { if (vnode == null) { try { - removeNodeDispatch[type](old) + if (type !== (TYPE_RETAIN & TYPE_MASK)) removeNodeDispatch[type](old) } catch (e) { console.error(e) } @@ -1145,10 +1162,9 @@ m.mount = (root, view) => { } } var redraw = () => { if (!id) id = window.requestAnimationFrame(redraw.sync) } - var Mount = (_, old) => [ - m.remove(unschedule), - view(!old, redraw) - ] + var Mount = function (_, old) { + return [m.remove(unschedule), view.call(this, !old)] + } redraw.sync = () => { unschedule() m.render(root, m(Mount), {redraw}) diff --git a/src/entry/mithril.esm.js b/src/entry/mithril.esm.js index 13d544f5e..dabddc617 100644 --- a/src/entry/mithril.esm.js +++ b/src/entry/mithril.esm.js @@ -1,6 +1,6 @@ import m from "../core.js" -import {Link, WithRouter} from "../std/router.js" +import {WithRouter, link} from "../std/router.js" import {debouncer, throttler} from "../std/rate-limit.js" import {match, p, query} from "../std/path-query.js" import init from "../std/init.js" @@ -9,7 +9,7 @@ import tracked from "../std/tracked.js" import withProgress from "../std/with-progress.js" m.WithRouter = WithRouter -m.Link = Link +m.link = link m.p = p m.query = query m.match = match diff --git a/src/std/rate-limit.js b/src/std/rate-limit.js index 6252cbec3..272ae1ab9 100644 --- a/src/std/rate-limit.js +++ b/src/std/rate-limit.js @@ -163,7 +163,8 @@ var throttler = (delay) => rateLimiterImpl(delay, 1) * m.remove(debounced.dispose), * m("input[type=text].value", { * async oninput(ev) { - * if ((await debounced()) !== false) return + * // Skip redraw if rate limited - it's pointless + * if ((await debounced()) !== false) return false * try { * const response = await fetch(m.p("/save/:id", {id: attrs.id}), { * body: JSON.stringify({value: ev.target.value}), diff --git a/src/std/router.js b/src/std/router.js index a87b273a0..a7630a385 100644 --- a/src/std/router.js +++ b/src/std/router.js @@ -80,8 +80,8 @@ export var WithRouter = ({prefix, initial: href}) => { // showing the link in the first place. If you absolutely have to disable the link, disable it by // removing this component (like via `m("div", {disabled}, !disabled && m(Link))`). There's // friction here for a reason. -export var Link = () => { - var opts, setRoute +var Link = () => { + var href, opts, setRoute var listener = (ev) => { // Adapted from React Router's implementation: // https://github.com/ReactTraining/react-router/blob/520a0acd48ae1b066eb0b07d6d4d1790a1d02482/packages/react-router-dom/modules/Link.js @@ -102,7 +102,7 @@ export var Link = () => { // No modifier keys !ev.ctrlKey && !ev.metaKey && !ev.shiftKey && !ev.altKey ) { - setRoute(opts.href, opts) + setRoute(href, opts) // Capture the event, and don't double-call `redraw`. return m.capture(ev) } @@ -110,10 +110,11 @@ export var Link = () => { return function (attrs, old) { setRoute = this.route.set - opts = attrs + href = attrs.h + opts = attrs.o return [ m.layout((dom) => { - dom.href = this.route.prefix + opts.href + dom.href = this.route.prefix + href if (!old) dom.addEventListener("click", listener) }), m.remove((dom) => { @@ -122,3 +123,5 @@ export var Link = () => { ] } } + +export var link = (href, opts) => m(Link, {h: `${href}`, o: opts}) diff --git a/tests/core/retain.js b/tests/core/retain.js index da84cfad5..dc1cd746e 100644 --- a/tests/core/retain.js +++ b/tests/core/retain.js @@ -31,8 +31,13 @@ o.spec("retain", function() { o(updated).deepEquals(vnode) }) - o("throws on creation", function() { - o(() => m.render(G.root, m.retain())).throws(Error) + o("ignored if used on creation", function() { + var retain = m.retain() + + m.render(G.root, retain) + + o(G.root.childNodes.length).equals(0) + o(retain.m).equals(-1) }) o("prevents update in component", function() { @@ -71,9 +76,13 @@ o.spec("retain", function() { o(updated).deepEquals(vnode) }) - o("throws if used on component creation", function() { - var component = () => m.retain() + o("ignored if used on component creation", function() { + var retain = m.retain() + var component = () => retain + + m.render(G.root, m(component)) - o(() => m.render(G.root, m(component))).throws(Error) + o(G.root.childNodes.length).equals(0) + o(retain.m).equals(-1) }) }) diff --git a/tests/exported-api.js b/tests/exported-api.js index 95d2b2bd9..fad88ecb2 100644 --- a/tests/exported-api.js +++ b/tests/exported-api.js @@ -81,7 +81,7 @@ o.spec("api", function() { if (route.path === "/a") { return m("div") } else if (route.path === "/b") { - return m("a", m(m.Link, {href: "/a"})) + return m("a", m.link("/a")) } else { route.set("/a") } diff --git a/tests/std/router.js b/tests/std/router.js index 6b5fdcd4d..bec98ddad 100644 --- a/tests/std/router.js +++ b/tests/std/router.js @@ -161,7 +161,7 @@ o.spec("route", () => { var App = function () { if (this.route.path === "/test") { - return m("a", m(m.Link, {href: "/other", replace: true})) + return m("a", m.link("/other", {replace: true})) } else if (this.route.path === "/other") { return m("div") } else if (this.route.path === "/") { @@ -223,7 +223,7 @@ o.spec("route", () => { var App = function () { if (this.route.path === "/test") { - return m("a", m(m.Link, {href: "/other", replace: false})) + return m("a", m.link("/other", {replace: false})) } else if (this.route.path === "/other") { return m("div") } else { @@ -322,7 +322,7 @@ o.spec("route", () => { var App = function () { if (this.route.path === "/") { - return m("a", m(m.Link, {href: "/test"})) + return m("a", m.link("/test")) } else if (this.route.path === "/test") { return m("div") } else { @@ -352,7 +352,7 @@ o.spec("route", () => { var App = function () { if (this.route.path === "/") { - return m("a", m(m.Link, {href: "/test", state: {a: 1}})) + return m("a", m.link("/test", {state: {a: 1}})) } else if (this.route.path === "/test") { return m("div") } else { @@ -381,7 +381,7 @@ o.spec("route", () => { var App = function () { if (this.route.path === "/") { - return m("a", m(m.Link, {href: "/test"})) + return m("a", m.link("/test")) } else if (this.route.path === "/test") { return m("div") } else { @@ -412,7 +412,7 @@ o.spec("route", () => { var App = function () { if (this.route.path === "/") { - return m("a", {onclick(e) { e.preventDefault() }}, m(m.Link, {href: "/test"})) + return m("a", {onclick(e) { e.preventDefault() }}, m.link("/test")) } else if (this.route.path === "/test") { return m("div") } else { From f99850e9aebe9e01d2db4be98bc1e72003415ec6 Mon Sep 17 00:00:00 2001 From: Claudia Meadows Date: Wed, 30 Oct 2024 01:05:48 -0700 Subject: [PATCH 85/95] Further simplify routing --- src/core.js | 45 ++++++++++++++++++++++++------- src/entry/mithril.esm.js | 4 +-- src/std/router.js | 57 ++++++++++++++++++++++++---------------- tests/exported-api.js | 4 +-- tests/std/router.js | 38 +++++++++++++-------------- 5 files changed, 92 insertions(+), 56 deletions(-) diff --git a/src/core.js b/src/core.js index 23416b1c1..64e9fd3ce 100644 --- a/src/core.js +++ b/src/core.js @@ -115,6 +115,14 @@ Use dependencies: - `c`: virtual DOM children - `d`: unused +Inline: +- `m` bits 0-3: `8` +- `t`: unused +- `s`: unused +- `a`: view function +- `c`: instance vnode +- `d`: unused + The `m` field is also used for various assertions, that aren't described here. */ @@ -129,6 +137,7 @@ var TYPE_LAYOUT = 5 var TYPE_REMOVE = 6 var TYPE_SET_CONTEXT = 7 var TYPE_USE = 8 +var TYPE_INLINE = 9 // var TYPE_RETAIN = 15 var FLAG_USED = 1 << 4 @@ -275,6 +284,7 @@ m.TYPE_LAYOUT = TYPE_LAYOUT m.TYPE_REMOVE = TYPE_REMOVE m.TYPE_SET_CONTEXT = TYPE_SET_CONTEXT m.TYPE_USE = TYPE_USE +m.TYPE_INLINE = TYPE_INLINE // Simple and sweet. Also useful for idioms like `onfoo: m.capture` to drop events without // redrawing. @@ -286,16 +296,23 @@ m.capture = (ev) => { m.retain = () => Vnode(TYPE_RETAIN, null, null, null) +m.inline = (callback) => { + if (typeof callback !== "function") { + throw new TypeError("Callback must be a function.") + } + return Vnode(TYPE_INLINE, null, callback, null) +} + m.layout = (callback) => { if (typeof callback !== "function") { - throw new TypeError("Callback must be a function if provided") + throw new TypeError("Callback must be a function.") } return Vnode(TYPE_LAYOUT, null, callback, null) } m.remove = (callback) => { if (typeof callback !== "function") { - throw new TypeError("Callback must be a function if provided") + throw new TypeError("Callback must be a function.") } return Vnode(TYPE_REMOVE, null, callback, null) } @@ -366,7 +383,7 @@ var insertAfterCurrentRefNode = (child) => { //update var moveToPosition = (vnode) => { var type - while ((type = vnode.m & TYPE_MASK) === TYPE_COMPONENT) { + while ((1 << TYPE_COMPONENT | 1 << TYPE_INLINE) & 1 << (type = vnode.m & TYPE_MASK)) { if (!(vnode = vnode.c)) return } if ((1 << TYPE_FRAGMENT | 1 << TYPE_USE | 1 << TYPE_SET_CONTEXT) & 1 << type) { @@ -710,16 +727,20 @@ var updateComponent = (old, vnode) => { } tree = (vnode.s = tree).call(currentContext, attrs, oldAttrs) } - if (tree === vnode) { - throw new Error("A view cannot return the vnode it received as argument") - } - tree = m.normalize(tree) + updateNode(oldInstance, vnode.c = m.normalize(tree)) + } catch (e) { + if (currentRemoveOnThrow) throw e + console.error(e) + } +} + +var updateInline = (old, vnode) => { + try { + updateNode(old != null ? old.c : null, vnode.c = m.normalize(vnode.a.call(currentContext, currentContext))) } catch (e) { if (currentRemoveOnThrow) throw e console.error(e) - return } - updateNode(oldInstance, vnode.c = tree) } var removeFragment = (old) => updateFragment(old, null) @@ -736,6 +757,8 @@ var removeNode = (old) => { } } +var removeInstance = (old) => updateNode(old.c, null) + // Replaces an otherwise necessary `switch`. var updateNodeDispatch = [ updateFragment, @@ -747,6 +770,7 @@ var updateNodeDispatch = [ updateRemove, updateSet, updateUse, + updateInline, ] var removeNodeDispatch = [ @@ -757,11 +781,12 @@ var removeNodeDispatch = [ removeNode(old) updateFragment(old, null) }, - (old) => updateNode(old.c, null), + removeInstance, () => {}, (old) => currentHooks.push(old), removeFragment, removeFragment, + removeInstance, ] //attrs diff --git a/src/entry/mithril.esm.js b/src/entry/mithril.esm.js index dabddc617..cf0364fb9 100644 --- a/src/entry/mithril.esm.js +++ b/src/entry/mithril.esm.js @@ -1,14 +1,14 @@ import m from "../core.js" -import {WithRouter, link} from "../std/router.js" import {debouncer, throttler} from "../std/rate-limit.js" +import {link, route} from "../std/router.js" import {match, p, query} from "../std/path-query.js" import init from "../std/init.js" import lazy from "../std/lazy.js" import tracked from "../std/tracked.js" import withProgress from "../std/with-progress.js" -m.WithRouter = WithRouter +m.route = route m.link = link m.p = p m.query = query diff --git a/src/std/router.js b/src/std/router.js index a7630a385..39c4a3a6a 100644 --- a/src/std/router.js +++ b/src/std/router.js @@ -1,14 +1,10 @@ /* global window: false */ import m from "../core.js" -export var WithRouter = ({prefix, initial: href}) => { - if (prefix == null) prefix = "" - - if (typeof prefix !== "string") { - throw new TypeError("The route prefix must be a string if given") - } - - var mustReplace, redraw, currentUrl, currentPath +var Route = function ({p: prefix}) { + var href = this.href + var mustReplace, redraw, currentParsedHref + var currentRoute var updateRouteWithHref = () => { var url = new URL(href) @@ -19,17 +15,24 @@ export var WithRouter = ({prefix, initial: href}) => { if (index >= 0) urlPath = urlPath.slice(index + decodedPrefix.length) if (urlPath[0] !== "/") urlPath = `/${urlPath}` - currentUrl = new URL(urlPath, href) - currentPath = decodeURI(currentUrl.pathname) + var parsedUrl = new URL(urlPath, href) + var path = decodeURI(parsedUrl.pathname) mustReplace = false + currentRoute = { + prefix, + path, + params: parsedUrl.searchParams, + current: path + parsedUrl.search + parsedUrl.hash, + set, + match, + } + return currentParsedHref = parsedUrl.href } var updateRoute = () => { if (href === window.location.href) return href = window.location.href - var prevUrl = currentUrl - updateRouteWithHref() - if (currentUrl.href !== prevUrl.href) redraw() + if (currentParsedHref !== updateRouteWithHref()) redraw() } var set = (path, {replace, state} = {}) => { @@ -45,34 +48,42 @@ export var WithRouter = ({prefix, initial: href}) => { } } + var match = (path) => m.match(currentRoute, path) + if (!href) { if (typeof window !== "object") { throw new TypeError("Outside the DOM, `href` must be set") } href = window.location.href window.addEventListener("popstate", updateRoute) + } else if (typeof href !== "string") { + throw new TypeError("The initial route href must be a string if given") } updateRouteWithHref() - return function ({children}) { + return function ({v: view}) { redraw = this.redraw return [ m.remove(() => window.removeEventListener("popstate", updateRoute)), - m.set({ - route: { - prefix, - path: currentPath, - params: currentUrl.searchParams, - current: currentPath + currentUrl.search + currentUrl.hash, - set, - }, - }, children), + m.set({route: currentRoute}, m.inline(view)), ] } } +export var route = (prefix, view) => { + if (typeof prefix !== "string") { + throw new TypeError("The route prefix must be a string") + } + + if (typeof view !== "function") { + throw new TypeError("Router view must be a function.") + } + + return m(Route, {v: view, p: prefix}) +} + // Let's provide a *right* way to manage a route link, rather than letting people screw up // accessibility on accident. // diff --git a/tests/exported-api.js b/tests/exported-api.js index fad88ecb2..64a3fe87f 100644 --- a/tests/exported-api.js +++ b/tests/exported-api.js @@ -73,7 +73,7 @@ o.spec("api", function() { }) }) - o.spec("m.WithRouter, m.Link", function() { + o.spec("m.route, m.link", function() { o("works", async() => { var route var App = function () { @@ -87,7 +87,7 @@ o.spec("api", function() { } } - m.mount(G.root, () => m(m.WithRouter, {prefix: "#"}, m(App))) + m.mount(G.root, () => m.route("#", () => m(App))) await Promise.resolve() G.rafMock.fire() diff --git a/tests/std/router.js b/tests/std/router.js index bec98ddad..46f12d12c 100644 --- a/tests/std/router.js +++ b/tests/std/router.js @@ -18,7 +18,7 @@ o.spec("route", () => { var App = o.spy() - m.render(G.root, m(m.WithRouter, {prefix}, m(App))) + m.render(G.root, m.route(prefix, App)) o(App.callCount).equals(1) o(App.this.route.path).equals("/") @@ -31,7 +31,7 @@ o.spec("route", () => { var App = o.spy() - m.render(G.root, m(m.WithRouter, {prefix}, m(App))) + m.render(G.root, m.route(prefix, App)) o(App.callCount).equals(1) o(App.this.route.path).equals("/test") @@ -44,7 +44,7 @@ o.spec("route", () => { var App = o.spy() - m.render(G.root, m(m.WithRouter, {prefix}, m(App))) + m.render(G.root, m.route(prefix, App)) o(App.callCount).equals(1) o(App.this.route.path).equals("/ö") @@ -57,7 +57,7 @@ o.spec("route", () => { var App = o.spy() - m.render(G.root, m(m.WithRouter, {prefix}, m(App))) + m.render(G.root, m.route(prefix, App)) o(App.callCount).equals(1) o(App.this.route.path).equals("/ö") @@ -82,7 +82,7 @@ o.spec("route", () => { } } - m.mount(G.root, () => m(m.WithRouter, {prefix}, m(App))) + m.mount(G.root, () => m.route(prefix, App)) o(spy1.callCount).equals(1) o(spy2.callCount).equals(0) @@ -106,7 +106,7 @@ o.spec("route", () => { route = this.route } - m.mount(G.root, () => m(m.WithRouter, {prefix}, m(App))) + m.mount(G.root, () => m.route(prefix, App)) await Promise.resolve() G.rafMock.fire() @@ -134,7 +134,7 @@ o.spec("route", () => { route = this.route } - m.mount(G.root, () => m(m.WithRouter, {prefix}, m(App))) + m.mount(G.root, () => m.route(prefix, App)) route.set("/other", {replace: true}) @@ -171,7 +171,7 @@ o.spec("route", () => { } } - m.mount(G.root, () => m(m.WithRouter, {prefix}, m(App))) + m.mount(G.root, () => m.route(prefix, App)) G.root.firstChild.dispatchEvent(e) @@ -196,7 +196,7 @@ o.spec("route", () => { route = this.route } - m.mount(G.root, () => m(m.WithRouter, {prefix}, m(App))) + m.mount(G.root, () => m.route(prefix, App)) route.set("/other", {replace: false}) @@ -231,7 +231,7 @@ o.spec("route", () => { } } - m.mount(G.root, () => m(m.WithRouter, {prefix}, m(App))) + m.mount(G.root, () => m.route(prefix, App)) G.root.firstChild.dispatchEvent(e) @@ -256,7 +256,7 @@ o.spec("route", () => { route = this.route } - m.mount(G.root, () => m(m.WithRouter, {prefix}, m(App))) + m.mount(G.root, () => m.route(prefix, App)) route.set("/other", {state: {a: 1}}) @@ -275,7 +275,7 @@ o.spec("route", () => { route = this.route } - m.mount(G.root, () => m(m.WithRouter, {prefix: `${prefix}/`}, m(App))) + m.mount(G.root, () => m.route(`${prefix}/`, App)) o(route.path).equals("/test") o([...route.params]).deepEquals([]) @@ -290,7 +290,7 @@ o.spec("route", () => { route = this.route } - m.mount(G.root, () => m(m.WithRouter, {prefix}, m(App))) + m.mount(G.root, () => m.route(prefix, App)) o(route.path).equals("/test") o([...route.params]).deepEquals([["a", "b"], ["c", "d"]]) @@ -303,7 +303,7 @@ o.spec("route", () => { var App = () => {} - m.mount(G.root, () => m(m.WithRouter, {prefix}, m(App))) + m.mount(G.root, () => m.route(prefix, App)) G.window.history.back() @@ -330,7 +330,7 @@ o.spec("route", () => { } } - m.mount(G.root, () => m(m.WithRouter, {prefix}, m(App))) + m.mount(G.root, () => m.route(prefix, App)) o(G.window.location.href).equals(fullPrefix) @@ -360,7 +360,7 @@ o.spec("route", () => { } } - m.mount(G.root, () => m(m.WithRouter, {prefix}, m(App))) + m.mount(G.root, () => m.route(prefix, App)) G.root.firstChild.dispatchEvent(e) @@ -389,7 +389,7 @@ o.spec("route", () => { } } - m.mount(G.root, () => m(m.WithRouter, {prefix}, m(App))) + m.mount(G.root, () => m.route(prefix, App)) o(G.window.location.href).equals(fullPrefix) @@ -420,7 +420,7 @@ o.spec("route", () => { } } - m.mount(G.root, () => m(m.WithRouter, {prefix}, m(App))) + m.mount(G.root, () => m.route(prefix, App)) o(G.window.location.href).equals(fullPrefix) @@ -442,7 +442,7 @@ o.spec("route", () => { return m("div") }) - m.mount(G.root, () => m(m.WithRouter, {prefix}, m(App))) + m.mount(G.root, () => m.route(prefix, App)) o(App.callCount).equals(1) From 0c99734a644b68a873c5065b6f8197173990c616 Mon Sep 17 00:00:00 2001 From: Claudia Meadows Date: Wed, 30 Oct 2024 01:08:43 -0700 Subject: [PATCH 86/95] Use `queueMicrotask` to avoid some awkward `await 0`s. --- .eslintrc.json | 1 + src/std/init.js | 5 ++--- src/std/router.js | 5 +---- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index c65afb355..ceac4bc19 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -65,6 +65,7 @@ "globals": { "ReadableStream": true, "Response": true, + "queueMicrotask": true, "URL": true, "URLSearchParams": true, "AbortController": true, diff --git a/src/std/init.js b/src/std/init.js index 0a7a76390..64d683bbd 100644 --- a/src/std/init.js +++ b/src/std/init.js @@ -3,10 +3,9 @@ import m from "../core.js" function Init({f}, old) { if (old) return m.retain() var ctrl = new AbortController() - void (async () => { - await 0 // wait for next microtask + queueMicrotask(async () => { if ((await f(ctrl.signal)) !== false) this.redraw() - })() + }) return m.remove(() => ctrl.abort()) } diff --git a/src/std/router.js b/src/std/router.js index 39c4a3a6a..441fad39b 100644 --- a/src/std/router.js +++ b/src/std/router.js @@ -38,10 +38,7 @@ var Route = function ({p: prefix}) { var set = (path, {replace, state} = {}) => { if (mustReplace) replace = true mustReplace = true - void (async () => { - await 0 // wait for next microtask - updateRoute() - })() + queueMicrotask(updateRoute) redraw() if (typeof window === "object") { window.history[replace ? "replaceState" : "pushState"](state, "", prefix + path) From 814261f6517be1a346eea704013c6fb9ef8151c9 Mon Sep 17 00:00:00 2001 From: Claudia Meadows Date: Wed, 30 Oct 2024 13:00:51 -0700 Subject: [PATCH 87/95] Add more type checks, unify auto-redraw logic, optimize element creation better --- src/core.js | 88 +++++++++++++++++---------------------- src/std/init.js | 8 ++-- src/std/lazy.js | 8 +++- src/std/router.js | 10 ++--- src/std/tracked.js | 4 ++ src/std/with-progress.js | 4 ++ src/util.js | 17 ++++++++ tests/core/event.js | 12 +++--- tests/core/hyperscript.js | 2 +- tests/core/mountRedraw.js | 2 +- tests/std/init.js | 14 +++---- tests/std/router.js | 16 +++++-- 12 files changed, 105 insertions(+), 80 deletions(-) diff --git a/src/core.js b/src/core.js index 64e9fd3ce..df272ece3 100644 --- a/src/core.js +++ b/src/core.js @@ -1,5 +1,5 @@ /* eslint-disable no-bitwise */ -import {hasOwn} from "./util.js" +import {checkCallback, hasOwn, invokeRedrawable} from "./util.js" export {m as default} @@ -286,43 +286,23 @@ m.TYPE_SET_CONTEXT = TYPE_SET_CONTEXT m.TYPE_USE = TYPE_USE m.TYPE_INLINE = TYPE_INLINE -// Simple and sweet. Also useful for idioms like `onfoo: m.capture` to drop events without -// redrawing. +// Simple and sweet. Also useful for idioms like `onfoo: m.capture` to completely drop events while +// otherwise ignoring them. m.capture = (ev) => { ev.preventDefault() ev.stopPropagation() - return false + return "skip-redraw" } m.retain = () => Vnode(TYPE_RETAIN, null, null, null) - -m.inline = (callback) => { - if (typeof callback !== "function") { - throw new TypeError("Callback must be a function.") - } - return Vnode(TYPE_INLINE, null, callback, null) -} - -m.layout = (callback) => { - if (typeof callback !== "function") { - throw new TypeError("Callback must be a function.") - } - return Vnode(TYPE_LAYOUT, null, callback, null) -} - -m.remove = (callback) => { - if (typeof callback !== "function") { - throw new TypeError("Callback must be a function.") - } - return Vnode(TYPE_REMOVE, null, callback, null) -} +m.inline = (view) => Vnode(TYPE_INLINE, null, checkCallback(view, false, "view"), null) +m.layout = (callback) => Vnode(TYPE_LAYOUT, null, checkCallback(callback), null) +m.remove = (callback) => Vnode(TYPE_REMOVE, null, checkCallback(callback), null) m.Fragment = (attrs) => attrs.children m.keyed = (values, view) => { - if (view != null && typeof view !== "function") { - throw new TypeError("Callback must be a function if provided") - } + view = checkCallback(view, true, "view") var map = new Map() for (var value of values) { if (typeof view === "function") value = view(value) @@ -586,8 +566,7 @@ var updateText = (old, vnode) => { var handleAttributeError = (old, e, force) => { if (currentRemoveOnThrow || force) { - removeNode(old) - updateFragment(old, null) + if (old) removeElement(old) throw e } console.error(e) @@ -595,10 +574,11 @@ var handleAttributeError = (old, e, force) => { var updateElement = (old, vnode) => { var prevParent = currentParent + var prevRefNode = currentRefNode var prevNamespace = currentNamespace var mask = vnode.m var attrs = vnode.a - var element , oldAttrs + var element, oldAttrs if (old == null) { var entry = selectorCache.get(vnode.t) @@ -608,11 +588,11 @@ var updateElement = (old, vnode) => { var ns = attrs && attrs.xmlns || nameSpace[tag] || prevNamespace var opts = is ? {is} : null - insertAfterCurrentRefNode(element = vnode.d = ( + element = ( ns ? currentDocument.createElementNS(ns, tag, opts) : currentDocument.createElement(tag, opts) - )) + ) if (ns == null) { // Doing it this way since it doesn't seem Terser is smart enough to optimize the `if` with @@ -709,8 +689,17 @@ var updateElement = (old, vnode) => { } currentParent = prevParent - currentRefNode = element + currentRefNode = prevRefNode currentNamespace = prevNamespace + + // Do this as late as possible to reduce how much work browsers have to do to reduce style + // recalcs during initial (sub)tree construction. Also will defer `adoptNode` callbacks in + // custom elements until the last possible point (which will help accelerate some of them). + if (old == null) { + insertAfterCurrentRefNode(vnode.d = element) + } + + currentRefNode = element } var updateComponent = (old, vnode) => { @@ -757,6 +746,11 @@ var removeNode = (old) => { } } +var removeElement = (old) => { + removeNode(old) + updateFragment(old, null) +} + var removeInstance = (old) => updateNode(old.c, null) // Replaces an otherwise necessary `switch`. @@ -777,10 +771,7 @@ var removeNodeDispatch = [ removeFragment, removeKeyed, removeNode, - (old) => { - removeNode(old) - updateFragment(old, null) - }, + removeElement, removeInstance, () => {}, (old) => currentHooks.push(old), @@ -1096,13 +1087,7 @@ var setAttr = (vnode, element, mask, key, old, attrs) => { // return a promise that resolves to it. class EventDict extends Map { async handleEvent(ev) { - var handler = this.get(`on${ev.type}`) - if (typeof handler === "function") { - var result = handler.call(ev.currentTarget, ev) - if (result === false) return - if (result && typeof result.then === "function" && (await result) === false) return - (0, this._)() - } + invokeRedrawable(this._, this.get(`on${ev.type}`), ev.currentTarget, ev) } } @@ -1111,13 +1096,16 @@ class EventDict extends Map { var currentlyRendering = [] m.render = (dom, vnode, {redraw, removeOnThrow} = {}) => { - if (!dom) throw new TypeError("DOM element being rendered to does not exist.") - if (currentlyRendering.some((d) => d === dom || d.contains(dom))) { - throw new TypeError("Node is currently being rendered to and thus is locked.") + if (!dom) { + throw new TypeError("DOM element being rendered to does not exist.") } - if (redraw != null && typeof redraw !== "function") { - throw new TypeError("Redraw must be a function if given.") + checkCallback(redraw, true, "redraw") + + for (var root of currentlyRendering) { + if (dom.contains(root)) { + throw new TypeError("Node is currently being rendered to and thus is locked.") + } } var active = dom.ownerDocument.activeElement diff --git a/src/std/init.js b/src/std/init.js index 64d683bbd..61d288960 100644 --- a/src/std/init.js +++ b/src/std/init.js @@ -1,14 +1,14 @@ import m from "../core.js" +import {checkCallback, invokeRedrawable} from "../util.js" + function Init({f}, old) { if (old) return m.retain() var ctrl = new AbortController() - queueMicrotask(async () => { - if ((await f(ctrl.signal)) !== false) this.redraw() - }) + queueMicrotask(() => invokeRedrawable(this.redraw, f, undefined, ctrl.signal)) return m.remove(() => ctrl.abort()) } -var init = (f) => m(Init, {f}) +var init = (f) => m(Init, {f: checkCallback(f)}) export {init as default} diff --git a/src/std/lazy.js b/src/std/lazy.js index 6da411e1e..20aac285a 100644 --- a/src/std/lazy.js +++ b/src/std/lazy.js @@ -1,11 +1,17 @@ import m from "../core.js" +import {checkCallback} from "../util.js" + var lazy = (opts) => { + checkCallback(opts.fetch, false, "opts.fetch") + checkCallback(opts.pending, true, "opts.pending") + checkCallback(opts.error, true, "opts.error") + // Capture the error here so stack traces make more sense var error = new ReferenceError("Component not found") var redraws = new Set() var Comp = function () { - redraws.add(this.redraw) + redraws.add(checkCallback(this.redraw, false, "context.redraw")) return opts.pending && opts.pending() } var init = async () => { diff --git a/src/std/router.js b/src/std/router.js index 441fad39b..39015aaac 100644 --- a/src/std/router.js +++ b/src/std/router.js @@ -1,6 +1,8 @@ /* global window: false */ import m from "../core.js" +import {checkCallback} from "../util.js" + var Route = function ({p: prefix}) { var href = this.href var mustReplace, redraw, currentParsedHref @@ -60,7 +62,7 @@ var Route = function ({p: prefix}) { updateRouteWithHref() return function ({v: view}) { - redraw = this.redraw + redraw = checkCallback(this.redraw, false, "context.redraw") return [ m.remove(() => window.removeEventListener("popstate", updateRoute)), @@ -74,11 +76,7 @@ export var route = (prefix, view) => { throw new TypeError("The route prefix must be a string") } - if (typeof view !== "function") { - throw new TypeError("Router view must be a function.") - } - - return m(Route, {v: view, p: prefix}) + return m(Route, {v: checkCallback(view, false, "view"), p: prefix}) } // Let's provide a *right* way to manage a route link, rather than letting people screw up diff --git a/src/std/tracked.js b/src/std/tracked.js index 47a6c2bce..a4d0a6bb1 100644 --- a/src/std/tracked.js +++ b/src/std/tracked.js @@ -45,6 +45,8 @@ why that was removed in favor of this: work around it, you'd have to do something like this anyways. */ +import {checkCallback} from "../util.js" + /** * @template K, V * @typedef TrackedHandle @@ -75,6 +77,8 @@ why that was removed in favor of this: * @returns {Tracked} */ var tracked = (redraw, initial) => { + checkCallback(redraw, false, "redraw") + /** @type {Map & {_: AbortController}>} */ var state = new Map() /** @type {Set>} */ var live = new Set() diff --git a/src/std/with-progress.js b/src/std/with-progress.js index 48b681e99..37561b18f 100644 --- a/src/std/with-progress.js +++ b/src/std/with-progress.js @@ -1,8 +1,12 @@ +import {checkCallback} from "../util.js" + /** * @param {ReadableStream | null} source * @param {(current: number) => void} notify */ export default (source, notify) => { + checkCallback(notify, false, "notify") + var reader = source && source.getReader() var current = 0 diff --git a/src/util.js b/src/util.js index cb7253cb9..c8bb2f257 100644 --- a/src/util.js +++ b/src/util.js @@ -1 +1,18 @@ export var hasOwn = {}.hasOwnProperty + +export var invokeRedrawable = async (redraw, fn, thisValue, ...args) => { + if (typeof fn === "function") { + thisValue = Reflect.apply(fn, thisValue, args) + if (thisValue === "skip-redraw") return + if (thisValue && typeof thisValue.then === "function" && (await thisValue) === "skip-redraw") return + redraw() + } +} + +export var checkCallback = (callback, allowNull, label = "callback") => { + if (allowNull && callback == null || typeof callback === "function") { + return callback + } + + throw new TypeError(`\`${label}\` must be a function${allowNull ? " if provided." : "."}`) +} diff --git a/tests/core/event.js b/tests/core/event.js index fe0845f5b..75767ba01 100644 --- a/tests/core/event.js +++ b/tests/core/event.js @@ -85,8 +85,8 @@ o.spec("event", function() { }) }) - o("handles onclick returning false", function() { - var spyDiv = eventSpy((e) => { m.capture(e); return false }) + o("handles onclick returning `\"skip-redraw\"`", function() { + var spyDiv = eventSpy((e) => { m.capture(e); return "skip-redraw" }) var spyParent = eventSpy() var div = m("div", {onclick: spyDiv}) var parent = m("div", {onclick: spyParent}, div) @@ -106,9 +106,9 @@ o.spec("event", function() { o(e.defaultPrevented).equals(true) }) - o("handles onclick asynchronously returning false", function() { + o("handles onclick asynchronously returning `\"skip-redraw\"`", function() { var promise - var spyDiv = eventSpy((e) => { m.capture(e); return promise = Promise.resolve(false) }) + var spyDiv = eventSpy((e) => { m.capture(e); return promise = Promise.resolve("skip-redraw") }) var spyParent = eventSpy() var div = m("div", {onclick: spyDiv}) var parent = m("div", {onclick: spyParent}, div) @@ -132,8 +132,8 @@ o.spec("event", function() { }) }) - o("handles onclick returning false in child then bubbling to parent and not returning false", function() { - var spyDiv = eventSpy(() => false) + o("handles onclick returning `\"skip-redraw\"` in child then bubbling to parent and not returning `\"skip-redraw\"`", function() { + var spyDiv = eventSpy(() => "skip-redraw") var spyParent = eventSpy() var div = m("div", {onclick: spyDiv}) var parent = m("div", {onclick: spyParent}, div) diff --git a/tests/core/hyperscript.js b/tests/core/hyperscript.js index 1a0d9c1b7..64ef0e995 100644 --- a/tests/core/hyperscript.js +++ b/tests/core/hyperscript.js @@ -732,7 +732,7 @@ o.spec("hyperscript", function() { // Only doing this for the sake of initializing the required fields in the mock. G.root.dispatchEvent(e) - o(m.capture(e)).equals(false) + o(m.capture(e)).equals("skip-redraw") o(e.defaultPrevented).equals(true) o(e.cancelBubble).equals(true) }) diff --git a/tests/core/mountRedraw.js b/tests/core/mountRedraw.js index 7ce9df97a..9cebce596 100644 --- a/tests/core/mountRedraw.js +++ b/tests/core/mountRedraw.js @@ -445,7 +445,7 @@ o.spec("mount/redraw", function() { e.initEvent("click", true, true) m.mount(G.root, () => m("div", { - onclick: () => false, + onclick: () => "skip-redraw", }, m.layout(layout))) G.root.firstChild.dispatchEvent(e) diff --git a/tests/std/init.js b/tests/std/init.js index 60f37cd5f..413f2b472 100644 --- a/tests/std/init.js +++ b/tests/std/init.js @@ -20,7 +20,7 @@ o.spec("m.init", () => { await Promise.resolve() o(initializer.callCount).equals(1) o(onabort.callCount).equals(0) - o(redraw.callCount).equals(0) + o(redraw.callCount).equals(1) m.render(G.root, m.init(initializer), {redraw}) await Promise.resolve() @@ -74,7 +74,7 @@ o.spec("m.init", () => { await Promise.resolve() o(initializer.callCount).equals(1) o(onabort.callCount).equals(0) - o(redraw.callCount).equals(0) + o(redraw.callCount).equals(1) m.render(G.root, m.init(initializer), {redraw}) await Promise.resolve() @@ -128,7 +128,7 @@ o.spec("m.init", () => { await Promise.resolve() o(initializer.callCount).equals(1) o(onabort.callCount).equals(0) - o(redraw.callCount).equals(0) + o(redraw.callCount).equals(1) m.render(G.root, m.init(initializer), {redraw}) await Promise.resolve() @@ -169,9 +169,9 @@ o.spec("m.init", () => { o(redraw.callCount).equals(1) }) - o("works when returning `false`", async () => { + o("works when returning `\"skip-redraw\"`", async () => { var onabort = o.spy() - var initializer = o.spy((signal) => { signal.onabort = onabort; return false }) + var initializer = o.spy((signal) => { signal.onabort = onabort; return "skip-redraw" }) var redraw = o.spy() m.render(G.root, m.init(initializer), {redraw}) @@ -196,9 +196,9 @@ o.spec("m.init", () => { o(redraw.callCount).equals(0) }) - o("works when resolving to `false`", async () => { + o("works when resolving to `\"skip-redraw\"`", async () => { var onabort = o.spy() - var initializer = o.spy((signal) => { signal.onabort = onabort; return Promise.resolve(false) }) + var initializer = o.spy((signal) => { signal.onabort = onabort; return Promise.resolve("skip-redraw") }) var redraw = o.spy() m.render(G.root, m.init(initializer), {redraw}) diff --git a/tests/std/router.js b/tests/std/router.js index 46f12d12c..d7c8da359 100644 --- a/tests/std/router.js +++ b/tests/std/router.js @@ -17,52 +17,60 @@ o.spec("route", () => { G.window.location.href = `${prefix}/` var App = o.spy() + var redraw = o.spy() - m.render(G.root, m.route(prefix, App)) + m.render(G.root, m.route(prefix, App), {redraw}) o(App.callCount).equals(1) o(App.this.route.path).equals("/") o([...App.this.route.params]).deepEquals([]) o(G.rafMock.queueLength()).equals(0) + o(redraw.callCount).equals(0) }) o("returns alternate right route on init", () => { G.window.location.href = `${prefix}/test` var App = o.spy() + var redraw = o.spy() - m.render(G.root, m.route(prefix, App)) + m.render(G.root, m.route(prefix, App), {redraw}) o(App.callCount).equals(1) o(App.this.route.path).equals("/test") o([...App.this.route.params]).deepEquals([]) o(G.rafMock.queueLength()).equals(0) + o(redraw.callCount).equals(0) }) o("returns right route on init with escaped unicode", () => { G.window.location.href = `${prefix}/%C3%B6?%C3%B6=%C3%B6` var App = o.spy() + var redraw = o.spy() - m.render(G.root, m.route(prefix, App)) + m.render(G.root, m.route(prefix, App), {redraw}) o(App.callCount).equals(1) o(App.this.route.path).equals("/ö") o([...App.this.route.params]).deepEquals([["ö", "ö"]]) o(G.rafMock.queueLength()).equals(0) + o(redraw.callCount).equals(0) }) o("returns right route on init with unescaped unicode", () => { G.window.location.href = `${prefix}/ö?ö=ö` var App = o.spy() + var redraw = o.spy() - m.render(G.root, m.route(prefix, App)) + m.render(G.root, m.route(prefix, App), {redraw}) o(App.callCount).equals(1) o(App.this.route.path).equals("/ö") o([...App.this.route.params]).deepEquals([["ö", "ö"]]) o(G.rafMock.queueLength()).equals(0) + o(redraw.callCount).equals(0) }) o("sets path asynchronously", async () => { From cd260975454d38c77016680f3582014abb2a8618 Mon Sep 17 00:00:00 2001 From: Claudia Meadows Date: Wed, 30 Oct 2024 13:09:01 -0700 Subject: [PATCH 88/95] Revise an outdated comment, cheat a little bit on redraw context --- src/core.js | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/core.js b/src/core.js index df272ece3..d5b8cddc3 100644 --- a/src/core.js +++ b/src/core.js @@ -1083,16 +1083,14 @@ var setAttr = (vnode, element, mask, key, old, attrs) => { // 4. The event name is remapped to the handler before calling it. // 5. In function-based event handlers, `ev.currentTarget === this`. We replicate that below. // 6. In function-based event handlers, `return false` prevents the default action and stops event -// propagation. Instead of that, we hijack it to control implicit redrawing, and let users -// return a promise that resolves to it. +// propagation. Instead of that, we hijack the return value, so we can have it auto-redraw if +// the user returns `"skip-redraw"` or a promise that resolves to it. class EventDict extends Map { async handleEvent(ev) { invokeRedrawable(this._, this.get(`on${ev.type}`), ev.currentTarget, ev) } } -//event - var currentlyRendering = [] m.render = (dom, vnode, {redraw, removeOnThrow} = {}) => { @@ -1175,9 +1173,8 @@ m.mount = (root, view) => { } } var redraw = () => { if (!id) id = window.requestAnimationFrame(redraw.sync) } - var Mount = function (_, old) { - return [m.remove(unschedule), view.call(this, !old)] - } + // Cheating with context access for a minor bundle size win. + var Mount = (_, old) => [m.remove(unschedule), view.call(currentContext, !old)] redraw.sync = () => { unschedule() m.render(root, m(Mount), {redraw}) From 8cafcfec37a1d54ca86dad98503dd57599df93de Mon Sep 17 00:00:00 2001 From: Claudia Meadows Date: Wed, 30 Oct 2024 13:31:17 -0700 Subject: [PATCH 89/95] Fix a type comment, make things use a shared noop where helpful --- src/core.js | 4 ++-- src/std/lazy.js | 7 +++---- src/std/rate-limit.js | 22 +++++++++------------- src/std/tracked.js | 2 +- src/util.js | 2 ++ 5 files changed, 17 insertions(+), 20 deletions(-) diff --git a/src/core.js b/src/core.js index d5b8cddc3..5da0f82ca 100644 --- a/src/core.js +++ b/src/core.js @@ -1,5 +1,5 @@ /* eslint-disable no-bitwise */ -import {checkCallback, hasOwn, invokeRedrawable} from "./util.js" +import {checkCallback, hasOwn, invokeRedrawable, noop} from "./util.js" export {m as default} @@ -773,7 +773,7 @@ var removeNodeDispatch = [ removeNode, removeElement, removeInstance, - () => {}, + noop, (old) => currentHooks.push(old), removeFragment, removeFragment, diff --git a/src/std/lazy.js b/src/std/lazy.js index 20aac285a..d1e93e552 100644 --- a/src/std/lazy.js +++ b/src/std/lazy.js @@ -1,6 +1,6 @@ import m from "../core.js" -import {checkCallback} from "../util.js" +import {checkCallback, noop} from "../util.js" var lazy = (opts) => { checkCallback(opts.fetch, false, "opts.fetch") @@ -15,6 +15,7 @@ var lazy = (opts) => { return opts.pending && opts.pending() } var init = async () => { + init = noop try { Comp = await opts.fetch() if (typeof Comp !== "function") { @@ -31,9 +32,7 @@ var lazy = (opts) => { } return (attrs) => { - var f = init - init = null - if (typeof f === "function") f() + init() return m(Comp, attrs) } } diff --git a/src/std/rate-limit.js b/src/std/rate-limit.js index 272ae1ab9..89bf832bf 100644 --- a/src/std/rate-limit.js +++ b/src/std/rate-limit.js @@ -1,5 +1,7 @@ /* global performance, setTimeout, clearTimeout */ +import {noop} from "../util.js" + var validateDelay = (delay) => { if (!Number.isFinite(delay) || delay <= 0) { throw new RangeError("Timer delay must be finite and positive") @@ -12,14 +14,12 @@ var rateLimiterImpl = (delay = 500, isThrottler) => { var closed = false var start = 0 var timer = 0 - var resolveNext + var resolveNext = noop var callback = () => { timer = undefined - if (typeof resolveNext === "function") { - resolveNext(false) - resolveNext = undefined - } + resolveNext(false) + resolveNext = noop } var rateLimiter = async (ignoreLeading) => { @@ -27,10 +27,8 @@ var rateLimiterImpl = (delay = 500, isThrottler) => { return true } - if (typeof resolveNext === "function") { - resolveNext(true) - resolveNext = null - } + resolveNext(true) + resolveNext = noop if (timer) { if (isThrottler) { @@ -66,10 +64,8 @@ var rateLimiterImpl = (delay = 500, isThrottler) => { if (closed) return closed = true clearTimeout(timer) - if (typeof resolveNext === "function") { - resolveNext(true) - resolveNext = null - } + resolveNext(true) + resolveNext = noop } return rateLimiter diff --git a/src/std/tracked.js b/src/std/tracked.js index a4d0a6bb1..bb2553481 100644 --- a/src/std/tracked.js +++ b/src/std/tracked.js @@ -65,7 +65,7 @@ import {checkCallback} from "../util.js" * @property {() => Array<[K, V]>} list * @property {(key: K) => boolean} has * @property {(key: K) => undefined | V} get - * @property {(key: K, value: V) => void} track + * @property {(key: K, value: V) => void} set * @property {(key: K, value: V) => void} replace * @property {(key: K) => boolean} delete */ diff --git a/src/util.js b/src/util.js index c8bb2f257..53dea3e9f 100644 --- a/src/util.js +++ b/src/util.js @@ -16,3 +16,5 @@ export var checkCallback = (callback, allowNull, label = "callback") => { throw new TypeError(`\`${label}\` must be a function${allowNull ? " if provided." : "."}`) } + +export var noop = () => {} From 953665e876dcd622113822284bf662d2cad45185 Mon Sep 17 00:00:00 2001 From: Claudia Meadows Date: Wed, 30 Oct 2024 18:27:07 -0700 Subject: [PATCH 90/95] Add a single-value tracker, optimize tracked handle. --- src/entry/mithril.esm.js | 3 +- src/std/tracked.js | 116 +++++++++++++++++++--------- test-utils/global.js | 4 +- tests/std/tracked.js | 161 ++++++++++++++++++++++++--------------- 4 files changed, 184 insertions(+), 100 deletions(-) diff --git a/src/entry/mithril.esm.js b/src/entry/mithril.esm.js index cf0364fb9..a45d2ad6e 100644 --- a/src/entry/mithril.esm.js +++ b/src/entry/mithril.esm.js @@ -3,9 +3,9 @@ import m from "../core.js" import {debouncer, throttler} from "../std/rate-limit.js" import {link, route} from "../std/router.js" import {match, p, query} from "../std/path-query.js" +import {tracked, trackedList} from "../std/tracked.js" import init from "../std/init.js" import lazy from "../std/lazy.js" -import tracked from "../std/tracked.js" import withProgress from "../std/with-progress.js" m.route = route @@ -17,6 +17,7 @@ m.withProgress = withProgress m.lazy = lazy m.init = init m.tracked = tracked +m.trackedList = trackedList m.throttler = throttler m.debouncer = debouncer diff --git a/src/std/tracked.js b/src/std/tracked.js index bb2553481..2b4e7e2ef 100644 --- a/src/std/tracked.js +++ b/src/std/tracked.js @@ -43,9 +43,15 @@ why that was removed in favor of this: want to clear some state and not other state. You might want to preserve some elements of a sibling's state. Embedding it in the renderer would force an opinion on you, and in order to work around it, you'd have to do something like this anyways. + +As for the difference between `m.trackedList()` and `m.tracked()`, the first is for tracking lists +(and is explained above), and `m.tracked()` is for single values (but uses `m.trackedList()` +internally to avoid a ton of code duplication). */ -import {checkCallback} from "../util.js" +import m from "../core.js" + +import {checkCallback, noop} from "../util.js" /** * @template K, V @@ -55,6 +61,7 @@ import {checkCallback} from "../util.js" * @property {V} value * @property {AbortSignal} signal * @property {() => void} release + * @property {() => void} remove */ /** @@ -70,47 +77,64 @@ import {checkCallback} from "../util.js" * @property {(key: K) => boolean} delete */ -/** - * @template K, V - * @param {Iterable<[K, V]>} [initial] - * @param {() => void} redraw - * @returns {Tracked} - */ -var tracked = (redraw, initial) => { +var trackedState = (redraw) => { checkCallback(redraw, false, "redraw") - - /** @type {Map & {_: AbortController}>} */ var state = new Map() + /** @type {Map>} */ + var state = new Map() + var removed = new WeakSet() /** @type {Set>} */ var live = new Set() + /** @param {null | AbortController & TrackedHandle} prev */ var abort = (prev) => { try { if (prev) { - if (prev._) prev._.abort() - else live.delete(prev) + if (removed.has(prev)) { + live.delete(prev) + } else { + prev.abort() + } } } catch (e) { console.error(e) } } - // Bit 1 forcibly releases the old handle, and bit 2 causes an update notification to be sent - // (something that's unwanted during initialization). + /** @param {K} k */ + var remove = (k, r) => { + var prev = state.get(k) + var result = state.delete(k) + abort(prev) + if (r) redraw() + return result + } + + /** + * @param {K} k + * @param {V} v + * @param {number} bits + * Bit 1 forcibly releases the old handle, and bit 2 causes an update notification to be sent + * (something that's unwanted during initialization). + */ var setHandle = (k, v, bits) => { var prev = state.get(k) - var ctrl = new AbortController() - /** @type {TrackedHandle} */ - var handle = { - _: ctrl, - key: k, - value: v, - signal: ctrl.signal, - release() { - if (state.get(handle.key) === handle) { - handle._ = null - } else if (live.delete(handle)) { - redraw() - } - }, + // Note: it extending `AbortController` is an implementation detail. It exposing a `signal` + // property is *not*. + var handle = /** @type {AbortController & TrackedHandle} */ (new AbortController()) + handle.key = k + handle.value = v + handle.release = (ev) => { + if (ev) m.capture(ev) + if (!handle) return + if (state.get(handle.key) === handle) { + removed.add(handle) + handle = null + } else if (live.delete(handle)) { + redraw() + } + } + handle.remove = (ev) => { + if (ev) m.capture(ev) + remove(handle.key, 0) } state.set(k, handle) live.add(handle) @@ -121,6 +145,18 @@ var tracked = (redraw, initial) => { if (bits & 2) redraw() } + return {s: state, l: live, h: setHandle, r: remove} +} + +/** + * @template K, V + * @param {Iterable<[K, V]>} [initial] + * @param {() => void} redraw + * @returns {TrackedList} + */ +var trackedList = (redraw, initial) => { + var {s: state, l: live, h: setHandle, r: remove} = trackedState(redraw) + for (var [k, v] of initial || []) setHandle(k, v, 1) return { @@ -130,14 +166,22 @@ var tracked = (redraw, initial) => { get: (k) => (k = state.get(k)) && k.value, set: (k, v) => setHandle(k, v, 3), replace: (k, v) => setHandle(k, v, 2), - delete(k) { - var prev = state.get(k) - var result = state.delete(k) - abort(prev) - redraw() - return result - }, + delete: (k) => remove(k, 1), + forget: (k) => (k = state.get(k)) && k.release(), + } +} + +var tracked = (redraw) => { + var {l: live, h: setHandle, r: remove} = trackedState(redraw) + var initial = noop + var id = -1 + return (state) => { + if (!Object.is(initial, initial = state)) { + remove(id++, 0) + setHandle(id, state, 1) + } + return [...live] } } -export {tracked as default} +export {tracked, trackedList} diff --git a/test-utils/global.js b/test-utils/global.js index e92b25c12..4dbedafe1 100644 --- a/test-utils/global.js +++ b/test-utils/global.js @@ -64,7 +64,7 @@ export function setupGlobals(env = {}) { o.beforeEach(() => { initialize({...env}) - return env.initialize && env.initialize() + return env.initialize && env.initialize(G) }) o.afterEach(() => { @@ -87,7 +87,7 @@ export function setupGlobals(env = {}) { o(errors).deepEquals([]) errors.length = 0 o(mock.queueLength()).equals(0) - return env.cleanup && env.cleanup() + return env.cleanup && env.cleanup(G) }) return { diff --git a/tests/std/tracked.js b/tests/std/tracked.js index c0fb57195..d30e61408 100644 --- a/tests/std/tracked.js +++ b/tests/std/tracked.js @@ -2,15 +2,14 @@ import o from "ospec" import m from "../../src/entry/mithril.esm.js" -o.spec("tracked", () => { - /** @param {import("../tracked.js").Tracked} t */ - var live = (t) => t.live().map((h) => [h.key, h.value, h.signal.aborted]) +const readState = (list) => list.map((h) => [h.key, h.value, h.signal.aborted]) +o.spec("trackedList", () => { o("initializes values correctly", () => { - var calls = 0 - var t = m.tracked(() => calls++, [[1, "one"], [2, "two"]]) + let calls = 0 + const t = m.trackedList(() => calls++, [[1, "one"], [2, "two"]]) - o(live(t)).deepEquals([[1, "one", false], [2, "two", false]]) + o(readState(t.live())).deepEquals([[1, "one", false], [2, "two", false]]) o(t.list()).deepEquals([[1, "one"], [2, "two"]]) o(t.has(1)).equals(true) @@ -22,31 +21,31 @@ o.spec("tracked", () => { }) o("tracks values correctly", () => { - var calls = 0 - var t = m.tracked(() => calls++) + let calls = 0 + const t = m.trackedList(() => calls++) t.set(1, "one") o(calls).equals(1) - o(live(t)).deepEquals([[1, "one", false]]) + o(readState(t.live())).deepEquals([[1, "one", false]]) o(t.list()).deepEquals([[1, "one"]]) o(t.has(1)).equals(true) o(t.get(1)).equals("one") - var live1 = t.live()[0] + const live1 = t.live()[0] t.set(2, "two") o(calls).equals(2) - o(live(t)).deepEquals([[1, "one", false], [2, "two", false]]) + o(readState(t.live())).deepEquals([[1, "one", false], [2, "two", false]]) o(t.live()[0]).equals(live1) o(t.list()).deepEquals([[1, "one"], [2, "two"]]) o(t.has(1)).equals(true) o(t.get(1)).equals("one") o(t.has(2)).equals(true) o(t.get(2)).equals("two") - var live2 = t.live()[1] + const live2 = t.live()[1] t.delete(1) o(calls).equals(3) - o(live(t)).deepEquals([[1, "one", true], [2, "two", false]]) + o(readState(t.live())).deepEquals([[1, "one", true], [2, "two", false]]) o(t.live()[0]).equals(live1) o(t.live()[1]).equals(live2) o(t.list()).deepEquals([[2, "two"]]) @@ -57,7 +56,7 @@ o.spec("tracked", () => { live1.release() o(calls).equals(4) - o(live(t)).deepEquals([[2, "two", false]]) + o(readState(t.live())).deepEquals([[2, "two", false]]) o(t.live()[0]).equals(live2) o(t.list()).deepEquals([[2, "two"]]) o(t.has(1)).equals(false) @@ -67,18 +66,18 @@ o.spec("tracked", () => { t.replace(2, "dos") o(calls).equals(5) - o(live(t)).deepEquals([[2, "two", true], [2, "dos", false]]) + o(readState(t.live())).deepEquals([[2, "two", true], [2, "dos", false]]) o(t.live()[0]).equals(live2) o(t.list()).deepEquals([[2, "dos"]]) o(t.has(1)).equals(false) o(t.get(1)).equals(undefined) o(t.has(2)).equals(true) o(t.get(2)).equals("dos") - var live3 = t.live()[1] + const live3 = t.live()[1] live2.release() o(calls).equals(6) - o(live(t)).deepEquals([[2, "dos", false]]) + o(readState(t.live())).deepEquals([[2, "dos", false]]) o(t.live()[0]).equals(live3) o(t.list()).deepEquals([[2, "dos"]]) o(t.has(1)).equals(false) @@ -87,15 +86,15 @@ o.spec("tracked", () => { o(t.get(2)).equals("dos") }) - o("invokes `onUpdate()` after the update is fully completed, including any and all signal aborts", () => { - var live1, live2, live3 - var live1Aborted = false - var live2Aborted = false - var call = 0 - var t = m.tracked(() => { + o("invokes `redraw()` after the update is fully completed, including any and all signal aborts", () => { + let live1, live2, live3 + let live1Aborted = false + let live2Aborted = false + let call = 0 + const t = m.trackedList(() => { switch (++call) { case 1: - o(live(t)).deepEquals([[1, "one", false]]) + o(readState(t.live())).deepEquals([[1, "one", false]]) o(t.list()).deepEquals([[1, "one"]]) o(t.has(1)).equals(true) o(t.get(1)).equals("one") @@ -103,7 +102,7 @@ o.spec("tracked", () => { break case 2: - o(live(t)).deepEquals([[1, "one", false], [2, "two", false]]) + o(readState(t.live())).deepEquals([[1, "one", false], [2, "two", false]]) o(t.live()[0]).equals(live1) o(t.list()).deepEquals([[1, "one"], [2, "two"]]) o(t.has(1)).equals(true) @@ -114,7 +113,7 @@ o.spec("tracked", () => { break case 3: - o(live(t)).deepEquals([[1, "one", true], [2, "two", false]]) + o(readState(t.live())).deepEquals([[1, "one", true], [2, "two", false]]) o(t.live()[0]).equals(live1) o(t.live()[1]).equals(live2) o(t.list()).deepEquals([[2, "two"]]) @@ -125,7 +124,7 @@ o.spec("tracked", () => { break case 4: - o(live(t)).deepEquals([[2, "two", false]]) + o(readState(t.live())).deepEquals([[2, "two", false]]) o(t.live()[0]).equals(live2) o(t.list()).deepEquals([[2, "two"]]) o(t.has(1)).equals(false) @@ -135,7 +134,7 @@ o.spec("tracked", () => { break case 5: - o(live(t)).deepEquals([[2, "two", true], [2, "dos", false]]) + o(readState(t.live())).deepEquals([[2, "two", true], [2, "dos", false]]) o(t.live()[0]).equals(live2) o(t.list()).deepEquals([[2, "dos"]]) o(t.has(1)).equals(false) @@ -146,7 +145,7 @@ o.spec("tracked", () => { break case 6: - o(live(t)).deepEquals([[2, "dos", false]]) + o(readState(t.live())).deepEquals([[2, "dos", false]]) o(t.live()[0]).equals(live3) o(t.list()).deepEquals([[2, "dos"]]) o(t.has(1)).equals(false) @@ -164,7 +163,7 @@ o.spec("tracked", () => { o(call).equals(1) o(live1Aborted).equals(false) o(live2Aborted).equals(false) - var deleteOneStarted = false + let deleteOneStarted = false live1.signal.onabort = () => { live1Aborted = true o(call).equals(2) @@ -175,7 +174,7 @@ o.spec("tracked", () => { o(call).equals(2) o(live1Aborted).equals(false) o(live2Aborted).equals(false) - var deleteTwoStarted = false + let deleteTwoStarted = false live2.signal.onabort = () => { live2Aborted = true o(call).equals(4) @@ -206,18 +205,18 @@ o.spec("tracked", () => { }) o("tracks parallel removes correctly", () => { - var calls = 0 - var t = m.tracked(() => calls++) + let calls = 0 + const t = m.trackedList(() => calls++) t.set(1, "one") - var live1 = t.live()[0] + const live1 = t.live()[0] t.set(2, "two") - var live2 = t.live()[1] + const live2 = t.live()[1] t.delete(1) o(calls).equals(3) - o(live(t)).deepEquals([[1, "one", true], [2, "two", false]]) + o(readState(t.live())).deepEquals([[1, "one", true], [2, "two", false]]) o(t.live()[0]).equals(live1) o(t.live()[1]).equals(live2) o(t.list()).deepEquals([[2, "two"]]) @@ -228,7 +227,7 @@ o.spec("tracked", () => { t.replace(2, "dos") o(calls).equals(4) - o(live(t)).deepEquals([[1, "one", true], [2, "two", true], [2, "dos", false]]) + o(readState(t.live())).deepEquals([[1, "one", true], [2, "two", true], [2, "dos", false]]) o(t.live()[0]).equals(live1) o(t.live()[1]).equals(live2) o(t.list()).deepEquals([[2, "dos"]]) @@ -236,11 +235,11 @@ o.spec("tracked", () => { o(t.get(1)).equals(undefined) o(t.has(2)).equals(true) o(t.get(2)).equals("dos") - var live3 = t.live()[2] + const live3 = t.live()[2] live1.release() o(calls).equals(5) - o(live(t)).deepEquals([[2, "two", true], [2, "dos", false]]) + o(readState(t.live())).deepEquals([[2, "two", true], [2, "dos", false]]) o(t.live()[0]).equals(live2) o(t.list()).deepEquals([[2, "dos"]]) o(t.has(1)).equals(false) @@ -250,7 +249,7 @@ o.spec("tracked", () => { live2.release() o(calls).equals(6) - o(live(t)).deepEquals([[2, "dos", false]]) + o(readState(t.live())).deepEquals([[2, "dos", false]]) o(t.live()[0]).equals(live3) o(t.list()).deepEquals([[2, "dos"]]) o(t.has(1)).equals(false) @@ -260,97 +259,137 @@ o.spec("tracked", () => { }) o("tolerates release before abort", () => { - var calls = 0 - var t = m.tracked(() => calls++) + let calls = 0 + const t = m.trackedList(() => calls++) t.set(1, "one") o(calls).equals(1) - o(live(t)).deepEquals([[1, "one", false]]) + o(readState(t.live())).deepEquals([[1, "one", false]]) o(t.list()).deepEquals([[1, "one"]]) o(t.has(1)).equals(true) o(t.get(1)).equals("one") - var live1 = t.live()[0] + const live1 = t.live()[0] live1.release() o(calls).equals(1) - o(live(t)).deepEquals([[1, "one", false]]) + o(readState(t.live())).deepEquals([[1, "one", false]]) o(t.list()).deepEquals([[1, "one"]]) o(t.has(1)).equals(true) o(t.get(1)).equals("one") t.delete(1) o(calls).equals(2) - o(live(t)).deepEquals([]) + o(readState(t.live())).deepEquals([]) o(t.list()).deepEquals([]) o(t.has(1)).equals(false) o(t.get(1)).equals(undefined) }) o("tolerates double release before abort", () => { - var calls = 0 - var t = m.tracked(() => calls++) + let calls = 0 + const t = m.trackedList(() => calls++) t.set(1, "one") - var live1 = t.live()[0] + const live1 = t.live()[0] live1.release() live1.release() o(calls).equals(1) - o(live(t)).deepEquals([[1, "one", false]]) + o(readState(t.live())).deepEquals([[1, "one", false]]) o(t.list()).deepEquals([[1, "one"]]) o(t.has(1)).equals(true) o(t.get(1)).equals("one") t.delete(1) o(calls).equals(2) - o(live(t)).deepEquals([]) + o(readState(t.live())).deepEquals([]) o(t.list()).deepEquals([]) o(t.has(1)).equals(false) o(t.get(1)).equals(undefined) }) o("tolerates double release spanning delete", () => { - var calls = 0 - var t = m.tracked(() => calls++) + let calls = 0 + const t = m.trackedList(() => calls++) t.set(1, "one") - var live1 = t.live()[0] + const live1 = t.live()[0] live1.release() t.delete(1) live1.release() o(calls).equals(2) - o(live(t)).deepEquals([]) + o(readState(t.live())).deepEquals([]) o(t.list()).deepEquals([]) o(t.has(1)).equals(false) o(t.get(1)).equals(undefined) }) o("tracks double release after delete", () => { - var calls = 0 - var t = m.tracked(() => calls++) + let calls = 0 + const t = m.trackedList(() => calls++) t.set(1, "one") - var live1 = t.live()[0] + const live1 = t.live()[0] t.delete(1) o(calls).equals(2) - o(live(t)).deepEquals([[1, "one", true]]) + o(readState(t.live())).deepEquals([[1, "one", true]]) o(t.list()).deepEquals([]) o(t.has(1)).equals(false) o(t.get(1)).equals(undefined) live1.release() o(calls).equals(3) - o(live(t)).deepEquals([]) + o(readState(t.live())).deepEquals([]) o(t.list()).deepEquals([]) o(t.has(1)).equals(false) o(t.get(1)).equals(undefined) live1.release() o(calls).equals(3) - o(live(t)).deepEquals([]) + o(readState(t.live())).deepEquals([]) o(t.list()).deepEquals([]) o(t.has(1)).equals(false) o(t.get(1)).equals(undefined) }) }) + +o.spec("tracked", () => { + o("tracks values correctly", () => { + let calls = 0 + const trackHit = m.tracked(() => calls++) + + o(readState(trackHit("a"))).deepEquals([[0, "a", false]]) + o(readState(trackHit("a"))).deepEquals([[0, "a", false]]) + o(calls).equals(0) + + const list1 = trackHit("b") + o(readState(list1)).deepEquals([[0, "a", true], [1, "b", false]]) + o(calls).equals(0) + list1[0].release() + o(calls).equals(1) + + o(readState(trackHit("b"))).deepEquals([[1, "b", false]]) + o(calls).equals(1) + o(readState(trackHit("c"))).deepEquals([[1, "b", true], [2, "c", false]]) + o(calls).equals(1) + o(readState(trackHit("d"))).deepEquals([[1, "b", true], [2, "c", true], [3, "d", false]]) + o(calls).equals(1) + + const list2 = trackHit("d") + o(readState(list2)).deepEquals([[1, "b", true], [2, "c", true], [3, "d", false]]) + o(calls).equals(1) + list2[2].remove() + o(calls).equals(1) + + o(readState(trackHit("d"))).deepEquals([[1, "b", true], [2, "c", true], [3, "d", true]]) + o(calls).equals(1) + + list2[0].release() + o(calls).equals(2) + list2[1].release() + o(calls).equals(3) + list2[2].release() + o(calls).equals(4) + }) +}) From 3220a25e3bb5ffe6d88af01862d64c6401238257 Mon Sep 17 00:00:00 2001 From: Claudia Meadows Date: Wed, 30 Oct 2024 21:56:14 -0700 Subject: [PATCH 91/95] Bring back `m.request` - it's too useful. It mostly delegates to `fetch`. Also, kill `m.withProgress` as that only existed because there was no `m.request`. --- src/entry/mithril.esm.js | 4 +- src/std/fetch.js | 71 ++ src/std/with-progress.js | 28 - tests/std/fetch.js | 2007 +++++++++++++++++++++++++++++++++++++ tests/std/withProgress.js | 67 -- 5 files changed, 2080 insertions(+), 97 deletions(-) create mode 100644 src/std/fetch.js delete mode 100644 src/std/with-progress.js create mode 100644 tests/std/fetch.js delete mode 100644 tests/std/withProgress.js diff --git a/src/entry/mithril.esm.js b/src/entry/mithril.esm.js index a45d2ad6e..b51937f03 100644 --- a/src/entry/mithril.esm.js +++ b/src/entry/mithril.esm.js @@ -4,16 +4,16 @@ import {debouncer, throttler} from "../std/rate-limit.js" import {link, route} from "../std/router.js" import {match, p, query} from "../std/path-query.js" import {tracked, trackedList} from "../std/tracked.js" +import fetch from "../std/fetch.js" import init from "../std/init.js" import lazy from "../std/lazy.js" -import withProgress from "../std/with-progress.js" m.route = route m.link = link m.p = p m.query = query m.match = match -m.withProgress = withProgress +m.fetch = fetch m.lazy = lazy m.init = init m.tracked = tracked diff --git a/src/std/fetch.js b/src/std/fetch.js new file mode 100644 index 000000000..a9ac8f0f1 --- /dev/null +++ b/src/std/fetch.js @@ -0,0 +1,71 @@ +/* global fetch */ + +import {checkCallback} from "../util.js" + +var mfetch = async (url, opts = {}) => { + checkCallback(opts.onprogress, true, "opts.onprogress") + checkCallback(opts.extract, true, "opts.extract") + + try { + var response = await fetch(url, opts) + + if (opts.onprogress && response.body) { + var reader = response.body.getReader() + var rawLength = response.headers.get("content-length") || "" + // This is explicit coercion, but ESLint is frequently too dumb to detect it correctly. + // Another example: https://github.com/eslint/eslint/issues/14623 + // eslint-disable-next-line no-implicit-coercion + var total = (/^\d+$/).test(rawLength) ? +rawLength : -1 + var current = 0 + + response = new Response(new ReadableStream({ + type: "bytes", + start: (ctrl) => reader || ctrl.close(), + cancel: (reason) => reader.cancel(reason), + async pull(ctrl) { + var result = await reader.read() + if (result.done) { + ctrl.close() + } else { + current += result.value.length + ctrl.enqueue(result.value) + opts.onprogress(current, total) + } + }, + }), response) + } + + if (response.ok) { + if (opts.extract) { + return await opts.extract(response) + } + + switch (opts.responseType || "json") { + case "json": return await response.json() + case "formdata": return await response.formData() + case "arraybuffer": return await response.arrayBuffer() + case "blob": return await response.blob() + case "text": return await response.text() + case "document": + // eslint-disable-next-line no-undef + return new DOMParser() + .parseFromString(await response.text(), response.headers.get("content-type") || "text/html") + default: + throw new TypeError(`Unknown response type: ${opts.responseType}`) + } + } + + var message = (await response.text()) || response.statusText + } catch (e) { + var cause = e + var message = e.message + } + + var e = new Error(message) + e.status = response ? response.status : 0 + e.response = response + e.cause = cause + throw e +} + +export {mfetch as default} diff --git a/src/std/with-progress.js b/src/std/with-progress.js deleted file mode 100644 index 37561b18f..000000000 --- a/src/std/with-progress.js +++ /dev/null @@ -1,28 +0,0 @@ -import {checkCallback} from "../util.js" - -/** - * @param {ReadableStream | null} source - * @param {(current: number) => void} notify - */ -export default (source, notify) => { - checkCallback(notify, false, "notify") - - var reader = source && source.getReader() - var current = 0 - - return new ReadableStream({ - type: "bytes", - start: (ctrl) => reader || ctrl.close(), - cancel: (reason) => reader.cancel(reason), - async pull(ctrl) { - var result = await reader.read() - if (result.done) { - ctrl.close() - } else { - current += result.value.length - ctrl.enqueue(result.value) - notify(current) - } - }, - }) -} diff --git a/tests/std/fetch.js b/tests/std/fetch.js new file mode 100644 index 000000000..dbcc1d45f --- /dev/null +++ b/tests/std/fetch.js @@ -0,0 +1,2007 @@ +/* global FormData */ + +// This alone amounts to over 200k assertions total, but that's because it almost fully +// exhaustively tests the function. (Turns out it's not all that hard.) The function's pretty +// simple, so it doesn't take as long as you'd think. + +import o from "ospec" + +import m from "../../src/entry/mithril.esm.js" +import {setupGlobals} from "../../test-utils/global.js" + +o.spec("fetch", () => { + let global, oldFetch + setupGlobals({ + initialize(g) { + global = g + oldFetch = g.fetch + }, + cleanup(g) { + global = null + g.fetch = oldFetch + }, + }) + + const methods = [ + "HEAD", + "GET", + "PATCH", + "POST", + "PUT", + "DELETE", + ] + + const okStatuses = { + 200: "OK", + 201: "Created", + 202: "Accepted", + 203: "Non-authoritative Information", + 206: "Partial Content", + 207: "Multi-Status", + 208: "Already Reported", + 226: "IM Used", + } + + const emptyStatuses = { + 204: "No Content", + 205: "Reset Content", + } + + const emptyErrorStatuses = { + // 1xx statuses aren't supported: https://github.com/whatwg/fetch/issues/1759 + // It's likely that in the future, 101 may be supported, but not 103. + // 101: "Switching Protocols", + // 103: "Early Hints", + 304: "Not Modified", + } + + const errorStatuses = { + // 1xx statuses aren't supported: https://github.com/whatwg/fetch/issues/1759 + // 100: "Continue", + // 102: "Processing", + 300: "Multiple Choices", + 301: "Moved Permanently", + 302: "Found", + 303: "See Other", + 305: "Use Proxy", + 307: "Temporary Redirect", + 308: "Permanent Redirect", + 400: "Bad Request", + 401: "Unauthorized", + 402: "Payment Required", + 403: "Forbidden", + 404: "Not Found", + 405: "Method Not Allowed", + 406: "Not Acceptable", + 407: "Proxy Authentication Required", + 408: "Request Timeout", + 409: "Conflict", + 410: "Gone", + 411: "Length Required", + 412: "Precondition Failed", + 413: "Payload Too Large", + 414: "Request-URI Too Long", + 415: "Unsupported Media Type", + 416: "Requested Range Not Satisfiable", + 417: "Expectation Failed", + 418: "I'm a teapot", + 421: "Misdirected Request", + 422: "Unprocessable Entity", + 423: "Locked", + 424: "Failed Dependency", + 425: "Too Early", + 426: "Upgrade Required", + 428: "Precondition Required", + 429: "Too Many Requests", + 431: "Request Header Fields Too Large", + 444: "Connection Closed Without Response", + 451: "Unavailable For Legal Reasons", + 499: "Client Closed Request", + 500: "Internal Server Error", + 501: "Not Implemented", + 502: "Bad Gateway", + 503: "Service Unavailable", + 504: "Gateway Timeout", + 505: "HTTP Version Not Supported", + 506: "Variant Also Negotiates", + 507: "Insufficient Storage", + 508: "Loop Detected", + 510: "Not Extended", + 511: "Network Authentication Required", + 599: "Network Connect Timeout Error", + } + + const allStatuses = {...okStatuses, ...emptyStatuses, ...emptyErrorStatuses, ...errorStatuses} + + const allResponseTypes = ["json", "formdata", "arraybuffer", "blob", "text", "document"] + + /** + * @param {object} options + * @param {number} options.status + * @param {string} [options.contentType] + * @param {boolean} [options.contentLength] + * @param {null | Array} options.body + */ + const setupFetch = ({status, headers = {}, contentLength, body}) => { + global.fetch = o.spy(() => { + const encoder = new TextEncoder() + const chunks = body == null ? null : body.map((chunk) => ( + typeof chunk === "string" ? encoder.encode(chunk) : Uint8Array.from(chunk) + )) + if (contentLength) headers["content-length"] = chunks == null ? 0 : chunks.reduce((s, c) => s + c.length, 0) + let i = 0 + return new Response(body == null ? null : new ReadableStream({ + type: "bytes", + pull(ctrl) { + if (i === chunks.length) { + ctrl.close() + } else { + ctrl.enqueue(Uint8Array.from(chunks[i++])) + } + }, + }), {status, statusText: allStatuses[status], headers}) + }) + } + + const bufferToArray = (v) => [...new Uint8Array(v)] + + for (const method of methods) { + for (const status of Object.keys(okStatuses)) { + o.spec(`method ${method}, status ${status}`, () => { + o.spec("arraybuffer, no content length", () => { + o("null body", async () => { + setupFetch({status: Number(status), body: null}) + + const result = await m.fetch("/url", { + method, + responseType: "arraybuffer", + }) + + o(result instanceof ArrayBuffer).equals(true) + o(bufferToArray(result)).deepEquals([]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("empty body", async () => { + setupFetch({status: Number(status), body: []}) + + const result = await m.fetch("/url", { + method, + responseType: "arraybuffer", + }) + + o(result instanceof ArrayBuffer).equals(true) + o(bufferToArray(result)).deepEquals([]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("single non-empty chunk", async () => { + setupFetch({status: Number(status), body: [[10]]}) + + const result = await m.fetch("/url", { + method, + responseType: "arraybuffer", + }) + + o(result instanceof ArrayBuffer).equals(true) + o(bufferToArray(result)).deepEquals([10]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("two non-empty chunks", async () => { + setupFetch({status: Number(status), body: [[10], [20]]}) + + const result = await m.fetch("/url", { + method, + responseType: "arraybuffer", + }) + + o(result instanceof ArrayBuffer).equals(true) + o(bufferToArray(result)).deepEquals([10, 20]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("null body + `onprogress` listener", async () => { + setupFetch({status: Number(status), body: null}) + + const reports = [] + const result = await m.fetch("/url", { + method, + responseType: "arraybuffer", + onprogress: (current, total) => reports.push([current, total]), + }) + + o(result instanceof ArrayBuffer).equals(true) + o(bufferToArray(result)).deepEquals([]) + o(reports).deepEquals([]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("empty body + `onprogress` listener", async () => { + setupFetch({status: Number(status), body: []}) + + const reports = [] + const result = await m.fetch("/url", { + method, + responseType: "arraybuffer", + onprogress: (current, total) => reports.push([current, total]), + }) + + o(result instanceof ArrayBuffer).equals(true) + o(bufferToArray(result)).deepEquals([]) + o(reports).deepEquals([]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("single non-empty chunk + `onprogress` listener", async () => { + setupFetch({status: Number(status), body: [[10]]}) + + const reports = [] + const result = await m.fetch("/url", { + method, + responseType: "arraybuffer", + onprogress: (current, total) => reports.push([current, total]), + }) + + o(result instanceof ArrayBuffer).equals(true) + o(bufferToArray(result)).deepEquals([10]) + o(reports).deepEquals([[1, -1]]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("two non-empty chunks + `onprogress` listener", async () => { + setupFetch({status: Number(status), body: [[10], [20]]}) + + const reports = [] + const result = await m.fetch("/url", { + method, + responseType: "arraybuffer", + onprogress: (current, total) => reports.push([current, total]), + }) + + o(result instanceof ArrayBuffer).equals(true) + o(bufferToArray(result)).deepEquals([10, 20]) + o(reports).deepEquals([[1, -1], [2, -1]]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + }) + + o.spec("arraybuffer, has content length", () => { + o("null body", async () => { + setupFetch({status: Number(status), contentLength: true, body: null}) + + const result = await m.fetch("/url", { + method, + responseType: "arraybuffer", + }) + + o(result instanceof ArrayBuffer).equals(true) + o(bufferToArray(result)).deepEquals([]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("empty body", async () => { + setupFetch({status: Number(status), contentLength: true, body: []}) + + const result = await m.fetch("/url", { + method, + responseType: "arraybuffer", + }) + + o(result instanceof ArrayBuffer).equals(true) + o(bufferToArray(result)).deepEquals([]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("single non-empty chunk", async () => { + setupFetch({status: Number(status), contentLength: true, body: [[10]]}) + + const result = await m.fetch("/url", { + method, + responseType: "arraybuffer", + }) + + o(result instanceof ArrayBuffer).equals(true) + o(bufferToArray(result)).deepEquals([10]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("two non-empty chunks", async () => { + setupFetch({status: Number(status), contentLength: true, body: [[10], [20]]}) + + const result = await m.fetch("/url", { + method, + responseType: "arraybuffer", + }) + + o(result instanceof ArrayBuffer).equals(true) + o(bufferToArray(result)).deepEquals([10, 20]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("null body + `onprogress` listener", async () => { + setupFetch({status: Number(status), contentLength: true, body: null}) + + const reports = [] + const result = await m.fetch("/url", { + method, + responseType: "arraybuffer", + onprogress: (current, total) => reports.push([current, total]), + }) + + o(result instanceof ArrayBuffer).equals(true) + o(bufferToArray(result)).deepEquals([]) + o(reports).deepEquals([]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("empty body + `onprogress` listener", async () => { + setupFetch({status: Number(status), contentLength: true, body: []}) + + const reports = [] + const result = await m.fetch("/url", { + method, + responseType: "arraybuffer", + onprogress: (current, total) => reports.push([current, total]), + }) + + o(result instanceof ArrayBuffer).equals(true) + o(bufferToArray(result)).deepEquals([]) + o(reports).deepEquals([]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("single non-empty chunk + `onprogress` listener", async () => { + setupFetch({status: Number(status), contentLength: true, body: [[10]]}) + + const reports = [] + const result = await m.fetch("/url", { + method, + responseType: "arraybuffer", + onprogress: (current, total) => reports.push([current, total]), + }) + + o(result instanceof ArrayBuffer).equals(true) + o(bufferToArray(result)).deepEquals([10]) + o(reports).deepEquals([[1, 1]]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("two non-empty chunks + `onprogress` listener", async () => { + setupFetch({status: Number(status), contentLength: true, body: [[10], [20]]}) + + const reports = [] + const result = await m.fetch("/url", { + method, + responseType: "arraybuffer", + onprogress: (current, total) => reports.push([current, total]), + }) + + o(result instanceof ArrayBuffer).equals(true) + o(bufferToArray(result)).deepEquals([10, 20]) + o(reports).deepEquals([[1, 2], [2, 2]]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + }) + + o.spec("text, no content length", () => { + o("null body", async () => { + setupFetch({status: Number(status), body: null}) + + const result = await m.fetch("/url", { + method, + responseType: "text", + }) + + o(result).equals("") + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("empty body", async () => { + setupFetch({status: Number(status), body: []}) + + const result = await m.fetch("/url", { + method, + responseType: "text", + }) + + o(result).equals("") + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("single non-empty chunk", async () => { + setupFetch({status: Number(status), body: [[10]]}) + + const result = await m.fetch("/url", { + method, + responseType: "text", + }) + + o(result).equals("\x0A") + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("two non-empty chunks", async () => { + setupFetch({status: Number(status), body: [[10], [20]]}) + + const result = await m.fetch("/url", { + method, + responseType: "text", + }) + + o(result).equals("\x0A\x14") + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("null body + `onprogress` listener", async () => { + setupFetch({status: Number(status), body: null}) + + const reports = [] + const result = await m.fetch("/url", { + method, + responseType: "text", + onprogress: (current, total) => reports.push([current, total]), + }) + + o(result).equals("") + o(reports).deepEquals([]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("empty body + `onprogress` listener", async () => { + setupFetch({status: Number(status), body: []}) + + const reports = [] + const result = await m.fetch("/url", { + method, + responseType: "text", + onprogress: (current, total) => reports.push([current, total]), + }) + + o(result).equals("") + o(reports).deepEquals([]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("single non-empty chunk + `onprogress` listener", async () => { + setupFetch({status: Number(status), body: [[10]]}) + + const reports = [] + const result = await m.fetch("/url", { + method, + responseType: "text", + onprogress: (current, total) => reports.push([current, total]), + }) + + o(result).equals("\x0A") + o(reports).deepEquals([[1, -1]]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("two non-empty chunks + `onprogress` listener", async () => { + setupFetch({status: Number(status), body: [[10], [20]]}) + + const reports = [] + const result = await m.fetch("/url", { + method, + responseType: "text", + onprogress: (current, total) => reports.push([current, total]), + }) + + o(result).equals("\x0A\x14") + o(reports).deepEquals([[1, -1], [2, -1]]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + }) + + o.spec("text, has content length", () => { + o("null body", async () => { + setupFetch({status: Number(status), contentLength: true, body: null}) + + const result = await m.fetch("/url", { + method, + responseType: "text", + }) + + o(result).equals("") + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("empty body", async () => { + setupFetch({status: Number(status), contentLength: true, body: []}) + + const result = await m.fetch("/url", { + method, + responseType: "text", + }) + + o(result).equals("") + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("single non-empty chunk", async () => { + setupFetch({status: Number(status), contentLength: true, body: [[10]]}) + + const result = await m.fetch("/url", { + method, + responseType: "text", + }) + + o(result).equals("\x0A") + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("two non-empty chunks", async () => { + setupFetch({status: Number(status), contentLength: true, body: [[10], [20]]}) + + const result = await m.fetch("/url", { + method, + responseType: "text", + }) + + o(result).equals("\x0A\x14") + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("null body + `onprogress` listener", async () => { + setupFetch({status: Number(status), contentLength: true, body: null}) + + const reports = [] + const result = await m.fetch("/url", { + method, + responseType: "text", + onprogress: (current, total) => reports.push([current, total]), + }) + + o(result).equals("") + o(reports).deepEquals([]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("empty body + `onprogress` listener", async () => { + setupFetch({status: Number(status), contentLength: true, body: []}) + + const reports = [] + const result = await m.fetch("/url", { + method, + responseType: "text", + onprogress: (current, total) => reports.push([current, total]), + }) + + o(result).equals("") + o(reports).deepEquals([]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("single non-empty chunk + `onprogress` listener", async () => { + setupFetch({status: Number(status), contentLength: true, body: [[10]]}) + + const reports = [] + const result = await m.fetch("/url", { + method, + responseType: "text", + onprogress: (current, total) => reports.push([current, total]), + }) + + o(result).equals("\x0A") + o(reports).deepEquals([[1, 1]]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("two non-empty chunks + `onprogress` listener", async () => { + setupFetch({status: Number(status), contentLength: true, body: [[10], [20]]}) + + const reports = [] + const result = await m.fetch("/url", { + method, + responseType: "text", + onprogress: (current, total) => reports.push([current, total]), + }) + + o(result).equals("\x0A\x14") + o(reports).deepEquals([[1, 2], [2, 2]]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + }) + + o.spec("blob, no content length", () => { + o("null body", async () => { + setupFetch({status: Number(status), body: null}) + + const blob = await m.fetch("/url", { + method, + responseType: "blob", + }) + const result = await blob.text() + + o(result).equals("") + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("empty body", async () => { + setupFetch({status: Number(status), body: []}) + + const blob = await m.fetch("/url", { + method, + responseType: "blob", + }) + const result = await blob.text() + + o(result).equals("") + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("single non-empty chunk", async () => { + setupFetch({status: Number(status), body: [[10]]}) + + const blob = await m.fetch("/url", { + method, + responseType: "blob", + }) + const result = await blob.text() + + o(result).equals("\x0A") + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("two non-empty chunks", async () => { + setupFetch({status: Number(status), body: [[10], [20]]}) + + const blob = await m.fetch("/url", { + method, + responseType: "blob", + }) + const result = await blob.text() + + o(result).equals("\x0A\x14") + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("null body + `onprogress` listener", async () => { + setupFetch({status: Number(status), body: null}) + + const reports = [] + const blob = await m.fetch("/url", { + method, + responseType: "blob", + onprogress: (current, total) => reports.push([current, total]), + }) + const result = await blob.text() + + o(result).equals("") + o(reports).deepEquals([]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("empty body + `onprogress` listener", async () => { + setupFetch({status: Number(status), body: []}) + + const reports = [] + const blob = await m.fetch("/url", { + method, + responseType: "blob", + onprogress: (current, total) => reports.push([current, total]), + }) + const result = await blob.text() + + o(result).equals("") + o(reports).deepEquals([]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("single non-empty chunk + `onprogress` listener", async () => { + setupFetch({status: Number(status), body: [[10]]}) + + const reports = [] + const blob = await m.fetch("/url", { + method, + responseType: "blob", + onprogress: (current, total) => reports.push([current, total]), + }) + const result = await blob.text() + + o(result).equals("\x0A") + o(reports).deepEquals([[1, -1]]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("two non-empty chunks + `onprogress` listener", async () => { + setupFetch({status: Number(status), body: [[10], [20]]}) + + const reports = [] + const blob = await m.fetch("/url", { + method, + responseType: "blob", + onprogress: (current, total) => reports.push([current, total]), + }) + const result = await blob.text() + + o(result).equals("\x0A\x14") + o(reports).deepEquals([[1, -1], [2, -1]]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + }) + + o.spec("blob, has content length", () => { + o("null body", async () => { + setupFetch({status: Number(status), contentLength: true, body: null}) + + const blob = await m.fetch("/url", { + method, + responseType: "blob", + }) + const result = await blob.text() + + o(result).equals("") + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("empty body", async () => { + setupFetch({status: Number(status), contentLength: true, body: []}) + + const blob = await m.fetch("/url", { + method, + responseType: "blob", + }) + const result = await blob.text() + + o(result).equals("") + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("single non-empty chunk", async () => { + setupFetch({status: Number(status), contentLength: true, body: [[10]]}) + + const blob = await m.fetch("/url", { + method, + responseType: "blob", + }) + const result = await blob.text() + + o(result).equals("\x0A") + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("two non-empty chunks", async () => { + setupFetch({status: Number(status), contentLength: true, body: [[10], [20]]}) + + const blob = await m.fetch("/url", { + method, + responseType: "blob", + }) + const result = await blob.text() + + o(result).equals("\x0A\x14") + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("null body + `onprogress` listener", async () => { + setupFetch({status: Number(status), contentLength: true, body: null}) + + const reports = [] + const blob = await m.fetch("/url", { + method, + responseType: "blob", + onprogress: (current, total) => reports.push([current, total]), + }) + const result = await blob.text() + + o(result).equals("") + o(reports).deepEquals([]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("empty body + `onprogress` listener", async () => { + setupFetch({status: Number(status), contentLength: true, body: []}) + + const reports = [] + const blob = await m.fetch("/url", { + method, + responseType: "blob", + onprogress: (current, total) => reports.push([current, total]), + }) + const result = await blob.text() + + o(result).equals("") + o(reports).deepEquals([]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("single non-empty chunk + `onprogress` listener", async () => { + setupFetch({status: Number(status), contentLength: true, body: [[10]]}) + + const reports = [] + const blob = await m.fetch("/url", { + method, + responseType: "blob", + onprogress: (current, total) => reports.push([current, total]), + }) + const result = await blob.text() + + o(result).equals("\x0A") + o(reports).deepEquals([[1, 1]]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("two non-empty chunks + `onprogress` listener", async () => { + setupFetch({status: Number(status), contentLength: true, body: [[10], [20]]}) + + const reports = [] + const blob = await m.fetch("/url", { + method, + responseType: "blob", + onprogress: (current, total) => reports.push([current, total]), + }) + const result = await blob.text() + + o(result).equals("\x0A\x14") + o(reports).deepEquals([[1, 2], [2, 2]]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + }) + + o.spec("json, no content length", () => { + o("null body", async () => { + setupFetch({status: Number(status), body: null}) + + let error + try { + await m.fetch("/url", { + method, + responseType: "json", + }) + } catch (e) { + error = e + } + + o(error).notEquals(undefined) + o(error.cause).notEquals(undefined) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("empty body", async () => { + setupFetch({status: Number(status), body: []}) + + let error + try { + await m.fetch("/url", { + method, + responseType: "json", + }) + } catch (e) { + error = e + } + + o(error).notEquals(undefined) + o(error.cause).notEquals(undefined) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("single non-empty chunk", async () => { + setupFetch({status: Number(status), body: ["123"]}) + + const result = await m.fetch("/url", { + method, + responseType: "json", + }) + + o(result).equals(123) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("two non-empty chunks", async () => { + setupFetch({status: Number(status), body: ["123", "456"]}) + + const result = await m.fetch("/url", { + method, + responseType: "json", + }) + + o(result).equals(123456) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("null body + `onprogress` listener", async () => { + setupFetch({status: Number(status), body: null}) + + const reports = [] + + let error + try { + await m.fetch("/url", { + method, + responseType: "json", + }) + } catch (e) { + error = e + } + + o(error).notEquals(undefined) + o(error.cause).notEquals(undefined) + o(reports).deepEquals([]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("empty body + `onprogress` listener", async () => { + setupFetch({status: Number(status), body: []}) + + const reports = [] + + let error + try { + await m.fetch("/url", { + method, + responseType: "json", + }) + } catch (e) { + error = e + } + + o(error).notEquals(undefined) + o(error.cause).notEquals(undefined) + o(reports).deepEquals([]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("single non-empty chunk + `onprogress` listener", async () => { + setupFetch({status: Number(status), body: ["123"]}) + + const reports = [] + const result = await m.fetch("/url", { + method, + responseType: "json", + onprogress: (current, total) => reports.push([current, total]), + }) + + o(result).equals(123) + o(reports).deepEquals([[3, -1]]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("two non-empty chunks + `onprogress` listener", async () => { + setupFetch({status: Number(status), body: ["123", "456"]}) + + const reports = [] + const result = await m.fetch("/url", { + method, + responseType: "json", + onprogress: (current, total) => reports.push([current, total]), + }) + + o(result).equals(123456) + o(reports).deepEquals([[3, -1], [6, -1]]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + }) + + o.spec("json, has content length", () => { + o("null body", async () => { + setupFetch({status: Number(status), contentLength: true, body: null}) + + let error + try { + await m.fetch("/url", { + method, + responseType: "json", + }) + } catch (e) { + error = e + } + + o(error).notEquals(undefined) + o(error.cause).notEquals(undefined) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("empty body", async () => { + setupFetch({status: Number(status), contentLength: true, body: []}) + + let error + try { + await m.fetch("/url", { + method, + responseType: "json", + }) + } catch (e) { + error = e + } + + o(error).notEquals(undefined) + o(error.cause).notEquals(undefined) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("single non-empty chunk", async () => { + setupFetch({status: Number(status), contentLength: true, body: ["123"]}) + + const result = await m.fetch("/url", { + method, + responseType: "json", + }) + + o(result).equals(123) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("two non-empty chunks", async () => { + setupFetch({status: Number(status), contentLength: true, body: ["123", "456"]}) + + const result = await m.fetch("/url", { + method, + responseType: "json", + }) + + o(result).equals(123456) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("null body + `onprogress` listener", async () => { + setupFetch({status: Number(status), contentLength: true, body: null}) + + const reports = [] + + let error + try { + await m.fetch("/url", { + method, + responseType: "json", + }) + } catch (e) { + error = e + } + + o(error).notEquals(undefined) + o(error.cause).notEquals(undefined) + o(reports).deepEquals([]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("empty body + `onprogress` listener", async () => { + setupFetch({status: Number(status), contentLength: true, body: []}) + + const reports = [] + + let error + try { + await m.fetch("/url", { + method, + responseType: "json", + }) + } catch (e) { + error = e + } + + o(error).notEquals(undefined) + o(error.cause).notEquals(undefined) + o(reports).deepEquals([]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("single non-empty chunk + `onprogress` listener", async () => { + setupFetch({status: Number(status), contentLength: true, body: ["123"]}) + + const reports = [] + const result = await m.fetch("/url", { + method, + responseType: "json", + onprogress: (current, total) => reports.push([current, total]), + }) + + o(result).equals(123) + o(reports).deepEquals([[3, 3]]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("two non-empty chunks + `onprogress` listener", async () => { + setupFetch({status: Number(status), contentLength: true, body: ["123", "456"]}) + + const reports = [] + const result = await m.fetch("/url", { + method, + responseType: "json", + onprogress: (current, total) => reports.push([current, total]), + }) + + o(result).equals(123456) + o(reports).deepEquals([[3, 6], [6, 6]]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + }) + + if (typeof FormData === "function") { + o.spec("form data", () => { + o("works", async () => { + setupFetch({ + status: Number(status), + headers: { + "content-type": "multipart/form-data; boundary=123456", + }, + contentLength: true, + body: [ + "--123456\r\n", + "Content-Disposition: form-data; name=\"test\"\r\n", + "\r\n", + "value\r\n", + "--123456--\r\n", + ], + }) + + const reports = [] + const result = await m.fetch("/url", { + method, + responseType: "formdata", + onprogress: (current, total) => reports.push([current, total]), + }) + + o([...result]).deepEquals([ + ["test", "value"], + ]) + o(reports).deepEquals([ + [10, 76], + [55, 76], + [57, 76], + [64, 76], + [76, 76], + ]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + }) + } + + if (typeof DOMParser === "function") { + o.spec("document", () => { + o("works without content type", async () => { + setupFetch({ + status: Number(status), + body: ["
"], + }) + + const reports = [] + const result = await m.fetch("/url", { + method, + responseType: "formdata", + onprogress: (current, total) => reports.push([current, total]), + }) + + o(result.getElementById("foo")).notEquals(null) + o(reports).deepEquals([[33, -1]]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("works with content type text/html", async () => { + setupFetch({ + status: Number(status), + headers: { + "content-type": "text/html", + }, + body: ["
"], + }) + + const reports = [] + const result = await m.fetch("/url", { + method, + responseType: "formdata", + onprogress: (current, total) => reports.push([current, total]), + }) + + o(result.getElementById("foo")).notEquals(null) + o(reports).deepEquals([[33, -1]]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("works with content type application/xhtml+xml", async () => { + setupFetch({ + status: Number(status), + headers: { + "content-type": "application/xhtml+xml", + }, + body: ['test
'], + }) + + const reports = [] + const result = await m.fetch("/url", { + method, + responseType: "formdata", + onprogress: (current, total) => reports.push([current, total]), + }) + + o(result.getElementById("foo")).notEquals(null) + o(reports).deepEquals([[33, -1]]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("works with content type application/xml", async () => { + setupFetch({ + status: Number(status), + headers: { + "content-type": "application/xml", + }, + body: ['test
'], + }) + + const reports = [] + const result = await m.fetch("/url", { + method, + responseType: "formdata", + onprogress: (current, total) => reports.push([current, total]), + }) + + o(result.getElementById("foo")).notEquals(null) + o(reports).deepEquals([[33, -1]]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("works with content type text/xml", async () => { + setupFetch({ + status: Number(status), + headers: { + "content-type": "text/xml", + }, + body: ['test
'], + }) + + const reports = [] + const result = await m.fetch("/url", { + method, + responseType: "formdata", + onprogress: (current, total) => reports.push([current, total]), + }) + + o(result.getElementById("foo")).notEquals(null) + o(reports).deepEquals([[33, -1]]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("works with content type image/svg+xml", async () => { + setupFetch({ + status: Number(status), + headers: { + "content-type": "image/svg+xml", + }, + body: [''], + }) + + const reports = [] + const result = await m.fetch("/url", { + method, + responseType: "formdata", + onprogress: (current, total) => reports.push([current, total]), + }) + + o(result.getElementById("foo")).notEquals(null) + o(reports).deepEquals([[33, -1]]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + }) + } + + o.spec("custom extract", () => { + o("works", async () => { + setupFetch({ + status: Number(status), + body: ["123"], + }) + + const reports = [] + const result = await m.fetch("/url", { + method, + onprogress: (current, total) => reports.push([current, total]), + extract: async (response) => `${await response.text()}456`, + }) + + o(result).equals("123456") + o(reports).deepEquals([ + [3, -1], + ]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + }) + }) + } + + for (const status of Object.keys(emptyStatuses)) { + o.spec(`method ${method}, status ${status}`, () => { + o.spec("arraybuffer", () => { + o("no `onprogress` listener", async () => { + setupFetch({status: Number(status), body: null}) + + const result = await m.fetch("/url", { + method, + responseType: "arraybuffer", + }) + + o(result instanceof ArrayBuffer).equals(true) + o(bufferToArray(result)).deepEquals([]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("with `onprogress` listener", async () => { + setupFetch({status: Number(status), body: null}) + + const reports = [] + const result = await m.fetch("/url", { + method, + responseType: "arraybuffer", + onprogress: (current, total) => reports.push([current, total]), + }) + + o(result instanceof ArrayBuffer).equals(true) + o(bufferToArray(result)).deepEquals([]) + o(reports).deepEquals([]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + }) + + o.spec("text", () => { + o("no `onprogress` listener", async () => { + setupFetch({status: Number(status), body: null}) + + const result = await m.fetch("/url", { + method, + responseType: "text", + }) + + o(result).equals("") + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("with `onprogress` listener", async () => { + setupFetch({status: Number(status), body: null}) + + const reports = [] + const result = await m.fetch("/url", { + method, + responseType: "text", + onprogress: (current, total) => reports.push([current, total]), + }) + + o(result).equals("") + o(reports).deepEquals([]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + }) + + o.spec("blob", () => { + o("no `onprogress` listener", async () => { + setupFetch({status: Number(status), body: null}) + + const blob = await m.fetch("/url", { + method, + responseType: "blob", + }) + const result = await blob.text() + + o(result).equals("") + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("with `onprogress` listener", async () => { + setupFetch({status: Number(status), body: null}) + + const reports = [] + const blob = await m.fetch("/url", { + method, + responseType: "blob", + onprogress: (current, total) => reports.push([current, total]), + }) + const result = await blob.text() + + o(result).equals("") + o(reports).deepEquals([]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + }) + + o.spec("json", () => { + o("no `onprogress` listener", async () => { + setupFetch({status: Number(status), body: null}) + + let error + try { + await m.fetch("/url", { + method, + responseType: "json", + }) + } catch (e) { + error = e + } + + o(error).notEquals(undefined) + o(error.cause).notEquals(undefined) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("with `onprogress` listener", async () => { + setupFetch({status: Number(status), body: null}) + + const reports = [] + + let error + try { + await m.fetch("/url", { + method, + responseType: "json", + }) + } catch (e) { + error = e + } + + o(error).notEquals(undefined) + o(error.cause).notEquals(undefined) + o(reports).deepEquals([]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + }) + + o.spec("custom extract", () => { + o("works", async () => { + setupFetch({ + status: Number(status), + body: null, + }) + + const reports = [] + const result = await m.fetch("/url", { + method, + onprogress: (current, total) => reports.push([current, total]), + extract: async (response) => `${await response.text()}456`, + }) + + o(result).equals("456") + o(reports).deepEquals([]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + }) + }) + } + + for (const status of Object.keys(emptyErrorStatuses)) { + o.spec(`method ${method}, status ${status}`, () => { + for (const responseType of allResponseTypes) { + o.spec(responseType, () => { + o("no `onprogress` listener", async () => { + setupFetch({status: Number(status), body: null}) + + let error + try { + await m.fetch("/url", { + method, + responseType, + }) + } catch (e) { + error = e + } + + o(error.message).equals(emptyErrorStatuses[status]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("with `onprogress` listener", async () => { + setupFetch({status: Number(status), body: null}) + + const reports = [] + let error + try { + await m.fetch("/url", { + method, + responseType, + onprogress: (current, total) => reports.push([current, total]), + }) + } catch (e) { + error = e + } + + o(error.message).equals(emptyErrorStatuses[status]) + o(reports).deepEquals([]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + }) + } + }) + } + + for (const status of Object.keys(errorStatuses)) { + o.spec(`method ${method}, status ${status}`, () => { + for (const responseType of allResponseTypes) { + o.spec(`${responseType}, no content length`, () => { + o("null body", async () => { + setupFetch({status: Number(status), body: null}) + + let error + try { + await m.fetch("/url", { + method, + responseType, + }) + } catch (e) { + error = e + } + + o(error.message).equals(errorStatuses[status]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("empty body", async () => { + setupFetch({status: Number(status), body: []}) + + let error + try { + await m.fetch("/url", { + method, + responseType, + }) + } catch (e) { + error = e + } + + o(error.message).equals(errorStatuses[status]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("single non-empty chunk", async () => { + setupFetch({status: Number(status), body: [[10]]}) + + let error + try { + await m.fetch("/url", { + method, + responseType, + }) + } catch (e) { + error = e + } + + o(error.message).equals("\x0A") + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("two non-empty chunks", async () => { + setupFetch({status: Number(status), body: [[10], [20]]}) + + let error + try { + await m.fetch("/url", { + method, + responseType, + }) + } catch (e) { + error = e + } + + o(error.message).equals("\x0A\x14") + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("null body + `onprogress` listener", async () => { + setupFetch({status: Number(status), body: null}) + + const reports = [] + let error + try { + await m.fetch("/url", { + method, + responseType, + onprogress: (current, total) => reports.push([current, total]), + }) + } catch (e) { + error = e + } + + o(error.message).equals(errorStatuses[status]) + o(reports).deepEquals([]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("empty body + `onprogress` listener", async () => { + setupFetch({status: Number(status), body: []}) + + const reports = [] + let error + try { + await m.fetch("/url", { + method, + responseType, + onprogress: (current, total) => reports.push([current, total]), + }) + } catch (e) { + error = e + } + + o(error.message).equals(errorStatuses[status]) + o(reports).deepEquals([]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("single non-empty chunk + `onprogress` listener", async () => { + setupFetch({status: Number(status), body: [[10]]}) + + const reports = [] + let error + try { + await m.fetch("/url", { + method, + responseType, + onprogress: (current, total) => reports.push([current, total]), + }) + } catch (e) { + error = e + } + + o(error.message).equals("\x0A") + o(reports).deepEquals([[1, -1]]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("two non-empty chunks + `onprogress` listener", async () => { + setupFetch({status: Number(status), body: [[10], [20]]}) + + const reports = [] + let error + try { + await m.fetch("/url", { + method, + responseType, + onprogress: (current, total) => reports.push([current, total]), + }) + } catch (e) { + error = e + } + + o(error.message).equals("\x0A\x14") + o(reports).deepEquals([[1, -1], [2, -1]]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + }) + + o.spec(`${responseType}, has content length`, () => { + o("null body", async () => { + setupFetch({status: Number(status), contentLength: true, body: null}) + + let error + try { + await m.fetch("/url", { + method, + responseType, + }) + } catch (e) { + error = e + } + + o(error.message).equals(errorStatuses[status]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("empty body", async () => { + setupFetch({status: Number(status), contentLength: true, body: []}) + + let error + try { + await m.fetch("/url", { + method, + responseType, + }) + } catch (e) { + error = e + } + + o(error.message).equals(errorStatuses[status]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("single non-empty chunk", async () => { + setupFetch({status: Number(status), contentLength: true, body: [[10]]}) + + let error + try { + await m.fetch("/url", { + method, + responseType, + }) + } catch (e) { + error = e + } + + o(error.message).equals("\x0A") + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("two non-empty chunks", async () => { + setupFetch({status: Number(status), contentLength: true, body: [[10], [20]]}) + + let error + try { + await m.fetch("/url", { + method, + responseType, + }) + } catch (e) { + error = e + } + + o(error.message).equals("\x0A\x14") + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("null body + `onprogress` listener", async () => { + setupFetch({status: Number(status), contentLength: true, body: null}) + + const reports = [] + let error + try { + await m.fetch("/url", { + method, + responseType, + onprogress: (current, total) => reports.push([current, total]), + }) + } catch (e) { + error = e + } + + o(error.message).equals(errorStatuses[status]) + o(reports).deepEquals([]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("empty body + `onprogress` listener", async () => { + setupFetch({status: Number(status), contentLength: true, body: []}) + + const reports = [] + let error + try { + await m.fetch("/url", { + method, + responseType, + onprogress: (current, total) => reports.push([current, total]), + }) + } catch (e) { + error = e + } + + o(error.message).equals(errorStatuses[status]) + o(reports).deepEquals([]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("single non-empty chunk + `onprogress` listener", async () => { + setupFetch({status: Number(status), contentLength: true, body: [[10]]}) + + const reports = [] + let error + try { + await m.fetch("/url", { + method, + responseType, + onprogress: (current, total) => reports.push([current, total]), + }) + } catch (e) { + error = e + } + + o(error.message).equals("\x0A") + o(reports).deepEquals([[1, 1]]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("two non-empty chunks + `onprogress` listener", async () => { + setupFetch({status: Number(status), contentLength: true, body: [[10], [20]]}) + + const reports = [] + let error + try { + await m.fetch("/url", { + method, + responseType, + onprogress: (current, total) => reports.push([current, total]), + }) + } catch (e) { + error = e + } + + o(error.message).equals("\x0A\x14") + o(reports).deepEquals([[1, 2], [2, 2]]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + }) + } + }) + } + } +}) diff --git a/tests/std/withProgress.js b/tests/std/withProgress.js deleted file mode 100644 index e9062a3a8..000000000 --- a/tests/std/withProgress.js +++ /dev/null @@ -1,67 +0,0 @@ -import o from "ospec" - -import withProgress from "../../src/std/with-progress.js" - -if (typeof ReadableStream === "function") { - o.spec("withProgress", () => { - function sequence(chunks) { - let i = 0 - return new ReadableStream({ - type: "bytes", - pull(ctrl) { - if (i === chunks.length) { - ctrl.close() - } else { - ctrl.enqueue(Uint8Array.from(chunks[i++])) - } - }, - }) - } - - function drain(stream) { - return new Response(stream).arrayBuffer().then((buf) => [...new Uint8Array(buf)]) - } - - o("handles null body", () => { - var reports = [] - var watched = withProgress(null, (current) => reports.push(current)) - - return drain(watched).then((result) => { - o(result).deepEquals([]) - o(reports).deepEquals([]) - }) - }) - - o("handles empty body", () => { - var reports = [] - var watched = withProgress(sequence([]), (current) => reports.push(current)) - - return drain(watched).then((result) => { - o(result).deepEquals([]) - o(reports).deepEquals([]) - }) - }) - - o("adds single non-empty chunk", () => { - var reports = [] - var watched = withProgress(sequence([[10]]), (current) => reports.push(current)) - - return drain(watched).then((result) => { - o(result).deepEquals([10]) - o(reports).deepEquals([1]) - }) - }) - - o("adds multiple non-empty chunks", () => { - var reports = [] - var watched = withProgress(sequence([[10], [20]]), (current) => reports.push(current)) - - return drain(watched).then((result) => { - o(result).deepEquals([10, 20]) - o(reports).deepEquals([1, 2]) - }) - }) - }) -} else { - console.log("Skipping `withProgress` as `ReadableStream` is missing.") -} From 76389d4f0dd84e837c43a4e3b2da8c6ba4a0b411 Mon Sep 17 00:00:00 2001 From: Claudia Meadows Date: Mon, 4 Nov 2024 00:35:37 -0800 Subject: [PATCH 92/95] Avoid splicing samples I want a hard guarantee that it doesn't unnecessarily allocate here. To do otherwise could introduce uncertainty into tests. --- performance/stats.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/performance/stats.js b/performance/stats.js index 32a04d56b..0d2146222 100644 --- a/performance/stats.js +++ b/performance/stats.js @@ -88,7 +88,16 @@ export function pushSample(count, sum, duration) { /* eslint-enable no-bitwise */ - samples.splice(R, 0, {count, sum}) + // Avoid the overhead of `.splice`, since that creates an array. I don't want to trust the + // engine's ability to elide the allocation. The test operation could potentially mess with + // that. + + const sample = {count, sum} + const prevLen = samples.length + // Ensure the engine can only see the sample array as a dense sample object array. + samples.push(sample) + samples.copyWithin(R + 1, R, prevLen) + samples[R] = sample ticks += count meanSum += sum * count From 17e89eaddac0f7ab7c80d390660c52e82b73f2bd Mon Sep 17 00:00:00 2001 From: Claudia Meadows Date: Mon, 4 Nov 2024 14:58:43 -0800 Subject: [PATCH 93/95] Add Node.js types for a better autocomplete experience in scripts --- package-lock.json | 33 +++++++++++++++++++++++++++++++++ package.json | 1 + 2 files changed, 34 insertions(+) diff --git a/package-lock.json b/package-lock.json index 61ef78d4c..7dd6f4df1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@rollup/plugin-commonjs": "^28.0.0", "@rollup/plugin-node-resolve": "^15.3.0", "@rollup/plugin-terser": "^0.4.4", + "@types/node": "^22.8.7", "chokidar": "^4.0.1", "eslint": "^8.9.0", "ospec": "4.2.1", @@ -601,6 +602,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/node": { + "version": "22.8.7", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.8.7.tgz", + "integrity": "sha512-LidcG+2UeYIWcMuMUpBKOnryBWG/rnmOHQR5apjn8myTQcx3rinFRn7DcIFhMnS0PPFSC6OafdIKEad0lj6U0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.19.8" + } + }, "node_modules/@types/resolve": { "version": "1.20.2", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", @@ -1992,6 +2003,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true, + "license": "MIT" + }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -2381,6 +2399,15 @@ "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", "dev": true }, + "@types/node": { + "version": "22.8.7", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.8.7.tgz", + "integrity": "sha512-LidcG+2UeYIWcMuMUpBKOnryBWG/rnmOHQR5apjn8myTQcx3rinFRn7DcIFhMnS0PPFSC6OafdIKEad0lj6U0Q==", + "dev": true, + "requires": { + "undici-types": "~6.19.8" + } + }, "@types/resolve": { "version": "1.20.2", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", @@ -3361,6 +3388,12 @@ "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", "dev": true }, + "undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true + }, "uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", diff --git a/package.json b/package.json index fdb00b753..6362b86d4 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "@rollup/plugin-commonjs": "^28.0.0", "@rollup/plugin-node-resolve": "^15.3.0", "@rollup/plugin-terser": "^0.4.4", + "@types/node": "^22.8.7", "chokidar": "^4.0.1", "eslint": "^8.9.0", "ospec": "4.2.1", From 584069f60de66c24533ec763c3fdd4251938674f Mon Sep 17 00:00:00 2001 From: Claudia Meadows Date: Wed, 6 Nov 2024 01:52:34 -0800 Subject: [PATCH 94/95] Optimize the style property update flow and reduce its overhead Slightly boosts performance (by around 5-10%) and slightly saves on bundle size --- src/core.js | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/src/core.js b/src/core.js index 5da0f82ca..c74ef5d85 100644 --- a/src/core.js +++ b/src/core.js @@ -783,6 +783,7 @@ var removeNodeDispatch = [ //attrs /* eslint-disable no-unused-vars */ +var ASCII_HYPHEN = 0x2D var ASCII_COLON = 0x3A var ASCII_LOWER_A = 0x61 var ASCII_LOWER_B = 0x62 @@ -828,25 +829,28 @@ var getStyleKey = (host, key) => { return null } -var uppercaseRegex = /[A-Z]/g - -var toLowerCase = (capital) => "-" + capital.toLowerCase() - -var normalizeKey = (key) => ( - key.startsWith("--") ? key : - key === "cssFloat" ? "float" : - key.replace(uppercaseRegex, toLowerCase) -) - var setStyle = (style, old, value, add) => { - for (var propName of Object.keys(value)) { + for (var propName in value) { + var preferSetter = propName.charCodeAt(0) === ASCII_HYPHEN var propValue = getStyleKey(value, propName) if (propValue !== null) { var oldValue = getStyleKey(old, propName) if (add) { - if (propValue !== oldValue) style.setProperty(normalizeKey(propName), propValue) + if (propValue !== oldValue) { + if (preferSetter) { + style[propName] = propValue + } else { + style.setProperty(propName, propValue) + } + } } else { - if (oldValue === null) style.removeProperty(normalizeKey(propName)) + if (oldValue === null) { + if (preferSetter) { + style[propName] = "" + } else { + style.removeProperty(propName) + } + } } } } From 7c5d9b7faa5df280978f3c342567b40083fbd05f Mon Sep 17 00:00:00 2001 From: Claudia Meadows Date: Wed, 6 Nov 2024 18:02:28 -0800 Subject: [PATCH 95/95] Revise some test names --- performance/test-perf.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/performance/test-perf.js b/performance/test-perf.js index dbdd1a808..84139f49c 100644 --- a/performance/test-perf.js +++ b/performance/test-perf.js @@ -223,7 +223,7 @@ setupBenchmarks(setup, cycleRoot, { }, }, - "mount simpleTree": { + "mount `simpleTree`": { tick() { cycleRoot() // For consistency across the interval @@ -234,7 +234,7 @@ setupBenchmarks(setup, cycleRoot, { }, }, - "redraw simpleTree": { + "redraw `simpleTree`": { tick() { cycleRoot() redraw = m.mount(rootElem, simpleTree)