diff --git a/index.js b/index.js index 34e84981..1cf5c6c0 100644 --- a/index.js +++ b/index.js @@ -3,6 +3,7 @@ var hyperscript = require("./hyperscript") var request = require("./request") var mountRedraw = require("./mount-redraw") +var domFor = require("./render/dom-for") var m = function m() { return hyperscript.apply(this, arguments) } m.m = hyperscript @@ -20,5 +21,6 @@ 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/package-lock.json b/package-lock.json index 10f345d3..672b205b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,9 +8,6 @@ "name": "mithril", "version": "2.2.2", "license": "MIT", - "dependencies": { - "ospec": "4.0.1" - }, "bin": { "ospec": "ospec/bin/ospec" }, @@ -30,7 +27,7 @@ "marked": "^4.0.10", "minimist": "^1.2.0", "npm-run-all": "^4.1.5", - "ospec": "^4.0.1", + "ospec": "4.1.6", "pinpoint": "^1.1.0", "request": "^2.88.0", "request-promise-native": "^1.0.7", @@ -347,7 +344,8 @@ "node_modules/balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true }, "node_modules/bcrypt-pbkdf": { "version": "1.0.2", @@ -381,6 +379,7 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -564,7 +563,8 @@ "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true }, "node_modules/core-util-is": { "version": "1.0.2", @@ -1429,7 +1429,8 @@ "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true }, "node_modules/fsevents": { "version": "2.1.0", @@ -1518,6 +1519,7 @@ "version": "7.1.4", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", + "dev": true, "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -1773,6 +1775,7 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, "dependencies": { "once": "^1.3.0", "wrappy": "1" @@ -1781,7 +1784,8 @@ "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true }, "node_modules/is-arrayish": { "version": "0.2.1", @@ -2504,6 +2508,7 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -2714,6 +2719,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, "dependencies": { "wrappy": "1" } @@ -2751,9 +2757,11 @@ } }, "node_modules/ospec": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/ospec/-/ospec-4.0.1.tgz", - "integrity": "sha512-iHx5jkuXh/hU4eEFFf6FH/4vj5RH7041jhuJw7WnqIPHDJgEXkSBdmuD644I/qQm8s5aZEPOpSoVagGCr8ebag==", + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/ospec/-/ospec-4.1.6.tgz", + "integrity": "sha512-Rq+kpRz/ombmIy+g0fAV7mwehtHyQT/J7IjmwVEBw6nbYPxycWgJS3c8BQ0n36kgWOIP5I2SE124cEdunqSH+g==", + "dev": true, + "hasInstallScript": true, "license": "MIT", "dependencies": { "glob": "^7.1.3" @@ -2815,6 +2823,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true, "engines": { "node": ">=0.10.0" } @@ -3841,7 +3850,8 @@ "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true }, "node_modules/yaml": { "version": "1.10.2", @@ -4088,7 +4098,8 @@ "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true }, "bcrypt-pbkdf": { "version": "1.0.2", @@ -4119,6 +4130,7 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -4269,7 +4281,8 @@ "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true }, "core-util-is": { "version": "1.0.2", @@ -4921,7 +4934,8 @@ "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true }, "fsevents": { "version": "2.1.0", @@ -4988,6 +5002,7 @@ "version": "7.1.4", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", + "dev": true, "requires": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -5170,6 +5185,7 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, "requires": { "once": "^1.3.0", "wrappy": "1" @@ -5178,7 +5194,8 @@ "inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true }, "is-arrayish": { "version": "0.2.1", @@ -5730,6 +5747,7 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, "requires": { "brace-expansion": "^1.1.7" } @@ -5900,6 +5918,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, "requires": { "wrappy": "1" } @@ -5928,9 +5947,10 @@ } }, "ospec": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/ospec/-/ospec-4.0.1.tgz", - "integrity": "sha512-iHx5jkuXh/hU4eEFFf6FH/4vj5RH7041jhuJw7WnqIPHDJgEXkSBdmuD644I/qQm8s5aZEPOpSoVagGCr8ebag==", + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/ospec/-/ospec-4.1.6.tgz", + "integrity": "sha512-Rq+kpRz/ombmIy+g0fAV7mwehtHyQT/J7IjmwVEBw6nbYPxycWgJS3c8BQ0n36kgWOIP5I2SE124cEdunqSH+g==", + "dev": true, "requires": { "glob": "^7.1.3" } @@ -5975,7 +5995,8 @@ "path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true }, "path-key": { "version": "2.0.1", @@ -6753,7 +6774,8 @@ "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true }, "yaml": { "version": "1.10.2", diff --git a/package.json b/package.json index a5ef3470..47f440ab 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ "marked": "^4.0.10", "minimist": "^1.2.0", "npm-run-all": "^4.1.5", - "ospec": "^4.0.1", + "ospec": "4.1.6", "pinpoint": "^1.1.0", "request": "^2.88.0", "request-promise-native": "^1.0.7", diff --git a/render/dom-for.js b/render/dom-for.js new file mode 100644 index 00000000..11c252c4 --- /dev/null +++ b/render/dom-for.js @@ -0,0 +1,24 @@ +"use strict" + +var delayedRemoval = module.exports.delayedRemoval = new WeakMap + +module.exports.domFor = function *domFor(vnode, {generation} = {generation: undefined}) { + let {dom, domSize} = vnode + if (dom != null) { + if (domSize == null) { + if (delayedRemoval.get(dom) === generation) { + yield dom + } + } else { + let i = 0, next + while (i < domSize) { + next = dom.nextSibling + if (delayedRemoval.get(dom) === generation) { + yield dom + i++ + } + dom = next + } + } + } +} diff --git a/render/render.js b/render/render.js index 59ef9a67..8d0ea0a7 100644 --- a/render/render.js +++ b/render/render.js @@ -1,16 +1,20 @@ "use strict" -var Vnode = require("../render/vnode") +const Vnode = require("../render/vnode") +const {domFor, delayedRemoval} = require("../render/dom-for") module.exports = function($window) { - var $doc = $window && $window.document - var currentRedraw + const $doc = $window && $window.document - var nameSpace = { + const nameSpace = { svg: "http://www.w3.org/2000/svg", math: "http://www.w3.org/1998/Math/MathML" } + let currentRedraw + let currentDOM + let currentRender + function getNameSpace(vnode) { return vnode.attrs && vnode.attrs.xmlns || nameSpace[vnode.tag] } @@ -87,11 +91,9 @@ module.exports = function($window) { vnode.dom = temp.firstChild vnode.domSize = temp.childNodes.length // Capture nodes to remove, so we don't confuse them. - vnode.instance = [] var fragment = $doc.createDocumentFragment() var child while (child = temp.firstChild) { - vnode.instance.push(child) fragment.appendChild(child) } insertNode(parent, fragment, nextSibling) @@ -421,13 +423,12 @@ module.exports = function($window) { } function updateHTML(parent, old, vnode, ns, nextSibling) { if (old.children !== vnode.children) { - removeHTML(parent, old) + removeDOM(parent, old, undefined) createHTML(parent, vnode, ns, nextSibling) } else { vnode.dom = old.dom vnode.domSize = old.domSize - vnode.instance = old.instance } } function updateFragment(parent, old, vnode, hooks, nextSibling, ns) { @@ -543,42 +544,18 @@ module.exports = function($window) { return nextSibling } - // This covers a really specific edge case: - // - Parent node is keyed and contains child - // - Child is removed, returns unresolved promise in `onbeforeremove` - // - Parent node is moved in keyed diff - // - Remaining children still need moved appropriately - // - // Ideally, I'd track removed nodes as well, but that introduces a lot more - // complexity and I'm not exactly interested in doing that. + // This handles fragments with zombie children (removed from vdom, but persisted in DOM throug onbeforeremove) function moveNodes(parent, vnode, nextSibling) { - var frag = $doc.createDocumentFragment() - moveChildToFrag(parent, frag, vnode) - insertNode(parent, frag, nextSibling) - } - function moveChildToFrag(parent, frag, vnode) { - // Dodge the recursion overhead in a few of the most common cases. - while (vnode.dom != null && vnode.dom.parentNode === parent) { - if (typeof vnode.tag !== "string") { - vnode = vnode.instance - if (vnode != null) continue - } else if (vnode.tag === "<") { - for (var i = 0; i < vnode.instance.length; i++) { - frag.appendChild(vnode.instance[i]) - } - } else if (vnode.tag !== "[") { - // Don't recurse for text nodes *or* elements, just fragments - frag.appendChild(vnode.dom) - } else if (vnode.children.length === 1) { - vnode = vnode.children[0] - if (vnode != null) continue + if (vnode.dom != null) { + var target + if (vnode.domSize == null) { + // don't allocate for the common case + target = vnode.dom } else { - for (var i = 0; i < vnode.children.length; i++) { - var child = vnode.children[i] - if (child != null) moveChildToFrag(parent, frag, child) - } + target = $doc.createDocumentFragment() + for (const dom of domFor(vnode)) target.appendChild(dom) } - break + insertNode(parent, target, nextSibling) } } @@ -628,65 +605,42 @@ module.exports = function($window) { } } checkState(vnode, original) - + var generation // If we can, try to fast-path it and avoid all the overhead of awaiting if (!mask) { onremove(vnode) - removeChild(parent, vnode) + removeDOM(parent, vnode, undefined) } else { - if (stateResult != null) { - var next = function () { + generation = currentRender + for (const dom of domFor(vnode)) delayedRemoval.set(dom, generation) + function finalizer(a, b) { + return function () { // eslint-disable-next-line no-bitwise - if (mask & 1) { mask &= 2; if (!mask) reallyRemove() } + if (mask & a) { mask &= b; if (!mask) { + checkState(vnode, original) + onremove(vnode) + removeDOM(parent, vnode, generation) + } } } - stateResult.then(next, next) + } + if (stateResult != null) { + stateResult.finally(finalizer(1, 2)) } if (attrsResult != null) { - var next = function () { - // eslint-disable-next-line no-bitwise - if (mask & 2) { mask &= 1; if (!mask) reallyRemove() } - } - attrsResult.then(next, next) + attrsResult.finally(finalizer(2, 1)) } } + } + 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 (const dom of domFor(vnode, {generation})) parent.removeChild(dom) + } + } - function reallyRemove() { - checkState(vnode, original) - onremove(vnode) - removeChild(parent, vnode) - } - } - function removeHTML(parent, vnode) { - for (var i = 0; i < vnode.instance.length; i++) { - parent.removeChild(vnode.instance[i]) - } - } - function removeChild(parent, vnode) { - // Dodge the recursion overhead in a few of the most common cases. - while (vnode.dom != null && vnode.dom.parentNode === parent) { - if (typeof vnode.tag !== "string") { - vnode = vnode.instance - if (vnode != null) continue - } else if (vnode.tag === "<") { - removeHTML(parent, vnode) - } else { - if (vnode.tag !== "[") { - parent.removeChild(vnode.dom) - if (!Array.isArray(vnode.children)) break - } - if (vnode.children.length === 1) { - vnode = vnode.children[0] - if (vnode != null) continue - } else { - for (var i = 0; i < vnode.children.length; i++) { - var child = vnode.children[i] - if (child != null) removeChild(parent, child) - } - } - } - break - } - } 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) @@ -946,8 +900,6 @@ module.exports = function($window) { 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)) { @@ -961,6 +913,7 @@ module.exports = function($window) { currentDOM = dom currentRedraw = typeof redraw === "function" ? redraw : undefined + currentRender = {} try { // First time rendering into a node clears it out if (dom.vnodes == null) dom.textContent = "" diff --git a/render/tests/test-dom-for.js b/render/tests/test-dom-for.js new file mode 100644 index 00000000..d36df4a2 --- /dev/null +++ b/render/tests/test-dom-for.js @@ -0,0 +1,176 @@ +var o = require("ospec") +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 domFor = require('../../render/dom-for').domFor + +o.spec("domFor(vnode)", function() { + var $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){ + o.spec(cmp.kind, function(){ + var createComponent = cmp.create + 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) + }) + }) + }) +}) \ No newline at end of file diff --git a/render/tests/test-onremove.js b/render/tests/test-onremove.js index e01f8309..68331728 100644 --- a/render/tests/test-onremove.js +++ b/render/tests/test-onremove.js @@ -195,13 +195,13 @@ o.spec("onremove", function() { }) // Warning: this test is complicated because it's replicating a race condition. o("removes correct nodes when child delays removal, parent removes, then child resolves", function () { - // Sugar over the complexity - I need to test the entire tree for consistency. - function expect(expectedPairs) { + // Custom assertion - we need to test the entire tree for consistency. + function template(tpl) {return function (root) { var expected = [] - for (var i = 0; i < expectedPairs.length; i++) { - var name = expectedPairs[i][0] - var text = expectedPairs[i][1] + 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", @@ -222,23 +222,30 @@ o.spec("onremove", function() { text: textNode.nodeValue, }) } + actual = JSON.stringify(actual, null, ' ') + expected = JSON.stringify(expected, null, ' ') + return { + pass: actual === expected, + message: +`${expected} + expected, got +${actual}` + } + }} - o(actual).deepEquals(expected) - } - - var resolve - + var finallyCB function update(id, showParent, showChild) { render(root, m("div", showParent && fragment( "", // Required - showChild && fragment({ - onbeforeremove: function () { - return {then: function (r) { resolve = r }} + showChild && fragment( + { + onbeforeremove: function () { + return {then(){}, finally: function (fcb) { finallyCB = fcb }} + }, }, - }, - m("div", id) + m("div", id) ) ) ) @@ -246,170 +253,61 @@ o.spec("onremove", function() { } update("1", true, true) - expect([ + o(root).satisfies(template([ ["#text", ""], ["DIV", "1"], - ]) - o(resolve).equals(undefined) + ])) + o(finallyCB).equals(undefined) update("2", true, false) - expect([ + + o(root).satisfies(template([ ["#text", ""], ["DIV", "1"], - ]) - o(typeof resolve).equals("function") - var original = resolve + ])) + o(typeof finallyCB).equals("function") + + var original = finallyCB update("3", true, true) - expect([ + + o(root).satisfies(template([ ["#text", ""], ["DIV", "1"], ["DIV", "3"], - ]) - o(resolve).equals(original) + ])) + o(finallyCB).equals(original) update("4", false, true) - expect([ + + o(root).satisfies(template([ ["DIV", "1"], - ]) - o(resolve).equals(original) + ])) + o(finallyCB).equals(original) update("5", true, true) - expect([ + + o(root).satisfies(template([ ["DIV", "1"], ["#text", ""], ["DIV", "5"], - ]) - o(resolve).equals(original) + ])) + o(finallyCB).equals(original) - resolve() - expect([ + finallyCB() + + o(root).satisfies(template([ ["#text", ""], ["DIV", "5"], - ]) - o(resolve).equals(original) + ])) + o(finallyCB).equals(original) update("6", true, true) - expect([ + o(root).satisfies(template([ ["#text", ""], ["DIV", "6"], - ]) - o(resolve).equals(original) - }) - // Warning: this test is complicated because it's replicating a race condition. - o("removes correct nodes when child delays removal, parent removes, then child resolves + rejects both", function () { - // Sugar over the complexity - I need to test the entire tree for consistency. - function expect(expectedPairs) { - var expected = [] - - for (var i = 0; i < expectedPairs.length; i++) { - var name = expectedPairs[i][0] - var text = expectedPairs[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, - }) - } - - o(actual).deepEquals(expected) - } - - var resolve, reject - - function update(id, showParent, showChild) { - render(root, - m("div", - showParent && fragment( - "", // Required - showChild && fragment({ - onbeforeremove: function () { - return {then: function (res, rej) { - resolve = res - reject = rej - }} - }, - }, - m("div", id) - ) - ) - ) - ) - } - - update("1", true, true) - expect([ - ["#text", ""], - ["DIV", "1"], - ]) - o(resolve).equals(undefined) - - update("2", true, false) - expect([ - ["#text", ""], - ["DIV", "1"], - ]) - o(typeof resolve).equals("function") - var originalResolve = resolve - var originalReject = reject - - update("3", true, true) - expect([ - ["#text", ""], - ["DIV", "1"], - ["DIV", "3"], - ]) - o(resolve).equals(originalResolve) - o(reject).equals(originalReject) - - update("4", false, true) - expect([ - ["DIV", "1"], - ]) - o(resolve).equals(originalResolve) - o(reject).equals(originalReject) - - update("5", true, true) - expect([ - ["DIV", "1"], - ["#text", ""], - ["DIV", "5"], - ]) - o(resolve).equals(originalResolve) - o(reject).equals(originalReject) - - resolve() - reject() - reject() - resolve() - expect([ - ["#text", ""], - ["DIV", "5"], - ]) - o(resolve).equals(originalResolve) - o(reject).equals(originalReject) - - update("6", true, true) - expect([ - ["#text", ""], - ["DIV", "6"], - ]) - o(resolve).equals(originalResolve) - o(reject).equals(originalReject) + ])) + o(finallyCB).equals(original) }) }) }) diff --git a/test-utils/domMock.js b/test-utils/domMock.js index 7d284a47..18c839cd 100644 --- a/test-utils/domMock.js +++ b/test-utils/domMock.js @@ -99,12 +99,15 @@ module.exports = function(options) { } } 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'") + else throw new TypeError("Failed to execute 'removeChild', child not found in parent") } function insertBefore(child, reference) { var ancestor = this