From 95ec3d28aa866a75050983c62aa1b2186a59150a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre-Yves=20G=C3=A9rardy?= Date: Fri, 18 May 2018 21:12:52 +0200 Subject: [PATCH] test for the LIS-based diff --- render/tests/test-updateNodes.js | 151 ++++++++++++++++++++++- render/tests/test-updateNodesFuzzer.js | 159 +++++++++++++++++++++++++ 2 files changed, 306 insertions(+), 4 deletions(-) create mode 100644 render/tests/test-updateNodesFuzzer.js diff --git a/render/tests/test-updateNodes.js b/render/tests/test-updateNodes.js index 85a5ffd3..d464eda9 100644 --- a/render/tests/test-updateNodes.js +++ b/render/tests/test-updateNodes.js @@ -134,17 +134,17 @@ o.spec("updateNodes", function() { o("reverses els w/ odd count", function() { var vnodes = [{tag: "a", key: 1}, {tag: "b", key: 2}, {tag: "i", key: 3}] var updated = [{tag: "i", key: 3}, {tag: "b", key: 2}, {tag: "a", key: 1}] - + var expectedTags = updated.map(function(vn) {return vn.tag}) 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[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("A") - o(updated[2].dom).equals(root.childNodes[2]) + o(tagNames).deepEquals(expectedTags) }) o("creates el at start", function() { var vnodes = [{tag: "a", key: 1}] @@ -1100,6 +1100,149 @@ o.spec("updateNodes", function() { 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 = [{tag: "a", key: "a"}, {tag: "b", key: "b"}, {tag: "c", key: "c"}, {tag: "d", key: "d"}] + var updated = [{tag: "b", key: "b"}, {tag: "a", key: "a"}, {tag: "d", key: "d"}, {tag: "c", key: "c"}] + var expectedTagNames = updated.map(function(vn) {return vn.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() { + var vnodes = [{tag: "a", key: "a"}, {tag: "b", key: "b"}, {tag: "c", key: "c"}, {tag: "d", key: "d"}] + var updated = [{tag: "d", key: "d"}, {tag: "c", key: "c"}, {tag: "b", key: "b"}, {tag: "a", key: "a"}] + var expectedTagNames = updated.map(function(vn) {return vn.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(3) + o(tagNames).deepEquals(expectedTagNames) + }) + o("minimizes DOM operations when reversing a keyed lists with an even number of items", function() { + var vnodes = [{tag: "a", key: "a"}, {tag: "b", key: "b"}, {tag: "c", key: "c"}] + var updated = [{tag: "c", key: "c"}, {tag: "b", key: "b"}, {tag: "a", key: "a"}] + var expectedTagNames = updated.map(function(vn) {return vn.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 scrambling a keyed lists with prefixes and suffixes", function() { + var vnodes = [{tag: "i", key: "i"}, {tag: "a", key: "a"}, {tag: "b", key: "b"}, {tag: "c", key: "c"}, {tag: "d", key: "d"}, {tag: "j", key: "j"}] + var updated = [{tag: "i", key: "i"}, {tag: "b", key: "b"}, {tag: "a", key: "a"}, {tag: "d", key: "d"}, {tag: "c", key: "c"}, {tag: "j", key: "j"}] + var expectedTagNames = updated.map(function(vn) {return vn.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 with prefixes and suffixes", function() { + var vnodes = [{tag: "i", key: "i"}, {tag: "a", key: "a"}, {tag: "b", key: "b"}, {tag: "c", key: "c"}, {tag: "d", key: "d"}, {tag: "j", key: "j"}] + var updated = [{tag: "i", key: "i"}, {tag: "d", key: "d"}, {tag: "c", key: "c"}, {tag: "b", key: "b"}, {tag: "a", key: "a"}, {tag: "j", key: "j"}] + var expectedTagNames = updated.map(function(vn) {return vn.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(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() { + var vnodes = [{tag: "i", key: "i"}, {tag: "a", key: "a"}, {tag: "b", key: "b"}, {tag: "c", key: "c"}, {tag: "j", key: "j"}] + var updated = [{tag: "i", key: "i"}, {tag: "c", key: "c"}, {tag: "b", key: "b"}, {tag: "a", key: "a"}, {tag: "j", key: "j"}] + var expectedTagNames = updated.map(function(vn) {return vn.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("scrambling sample 1", function() { + function vnodify(str) { + return str.split(",").map(function(k) {return {tag: k, key: k}}) + } + 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}) + + 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(5) + o(tagNames).deepEquals(expectedTagNames) + }) + o("scrambling sample 2", function() { + function vnodify(str) { + return str.split(",").map(function(k) {return {tag: k, key: k}}) + } + 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}) + + 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(5) + o(tagNames).deepEquals(expectedTagNames) + }) + components.forEach(function(cmp){ o.spec(cmp.kind, function(){ var createComponent = cmp.create diff --git a/render/tests/test-updateNodesFuzzer.js b/render/tests/test-updateNodesFuzzer.js new file mode 100644 index 00000000..db5cafa7 --- /dev/null +++ b/render/tests/test-updateNodesFuzzer.js @@ -0,0 +1,159 @@ +"use strict" + +var o = require("../../ospec/ospec") +var domMock = require("../../test-utils/domMock") +var vdom = require("../../render/render") + +// 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 + o.beforeEach(function() { + $window = domMock() + root = $window.document.createElement("div") + render = vdom($window).render + }) + + + 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(function(x){return {tag: x, key: x}})) + addSpies(root) + render(root, test.updated.map(function(x){return {tag: x, key: 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 + + while (u < v) { + /*eslint-disable no-bitwise*/ + var c = ((u + v) / 2) | 0 + /*eslint-enable no-bitwise*/ + if (a[result[c]] < a[i]) { + u = c + 1 + } else { + v = c + } + } + + if (a[i] < a[result[u]]) { + if (u > 0) { + p[i] = result[u - 1] + } + 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 + }) + 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) +} +