diff --git a/build.json b/build.json index 9808f3c..f2cd87e 100644 --- a/build.json +++ b/build.json @@ -1,4 +1,5 @@ [ + "../observe-js/src/observe.js", "src/sidetable.js", "src/wrappers.js", "src/wrappers/events.js", diff --git a/shadowdom.js b/shadowdom.js index 55691ef..3ceef66 100644 --- a/shadowdom.js +++ b/shadowdom.js @@ -13,32 +13,32 @@ base = src.slice(0, -match[0].length); } }); - base += 'src/'; [ - 'sidetable.js', - 'wrappers.js', - 'wrappers/events.js', - 'wrappers/NodeList.js', - 'wrappers/Node.js', - 'querySelector.js', - 'wrappers/node-interfaces.js', - 'wrappers/CharacterData.js', - 'wrappers/Element.js', - 'wrappers/HTMLElement.js', - 'wrappers/HTMLContentElement.js', - 'wrappers/HTMLShadowElement.js', - 'wrappers/HTMLTemplateElement.js', - 'wrappers/HTMLUnknownElement.js', - 'wrappers/generic.js', - 'wrappers/ShadowRoot.js', - 'ShadowRenderer.js', - 'wrappers/elements-with-form-property.js', - 'wrappers/Document.js', - 'wrappers/Window.js', - 'wrappers/MutationObserver.js', - 'wrappers/Range.js', - 'wrappers/override-constructors.js' + '../observe-js/src/observe.js', + 'src/sidetable.js', + 'src/wrappers.js', + 'src/wrappers/events.js', + 'src/wrappers/NodeList.js', + 'src/wrappers/Node.js', + 'src/querySelector.js', + 'src/wrappers/node-interfaces.js', + 'src/wrappers/CharacterData.js', + 'src/wrappers/Element.js', + 'src/wrappers/HTMLElement.js', + 'src/wrappers/HTMLContentElement.js', + 'src/wrappers/HTMLShadowElement.js', + 'src/wrappers/HTMLTemplateElement.js', + 'src/wrappers/HTMLUnknownElement.js', + 'src/wrappers/generic.js', + 'src/wrappers/ShadowRoot.js', + 'src/ShadowRenderer.js', + 'src/wrappers/elements-with-form-property.js', + 'src/wrappers/Document.js', + 'src/wrappers/Window.js', + 'src/wrappers/MutationObserver.js', + 'src/wrappers/Range.js', + 'src/wrappers/override-constructors.js' ].forEach(function(src) { document.write(''); }); diff --git a/src/ShadowRenderer.js b/src/ShadowRenderer.js index a533848..fac6c52 100644 --- a/src/ShadowRenderer.js +++ b/src/ShadowRenderer.js @@ -48,64 +48,52 @@ updateWrapperDown(parentNodeWrapper); } - // This object groups DOM operations. This is supposed to be the DOM as the - // browser/render tree sees it. - // When changes are done to the visual DOM the logical DOM needs to be updated - // to reflect the correct tree. - function removeAllChildNodes(parentNodeWrapper) { + function insertBefore(parentNodeWrapper, newChildWrapper, refChildWrapper) { var parentNode = unwrap(parentNodeWrapper); - updateAllChildNodes(parentNodeWrapper); - if (parentNode.firstChild) - parentNode.textContent = ''; - } + var newChild = unwrap(newChildWrapper); + var refChild = refChildWrapper && unwrap(refChildWrapper); - function appendChild(parentNodeWrapper, childWrapper) { - var parentNode = unwrap(parentNodeWrapper); - var child = unwrap(childWrapper); - if (child.nodeType === Node.DOCUMENT_FRAGMENT_NODE) { - updateAllChildNodes(childWrapper); + remove(newChildWrapper); + updateWrapperUpAndSideways(newChildWrapper); - } else { - remove(childWrapper); - updateWrapperUpAndSideways(childWrapper); - } + if (!refChildWrapper) { + parentNodeWrapper.lastChild_ = parentNodeWrapper.lastChild; + if (parentNodeWrapper.lastChild === parentNodeWrapper.firstChild) + parentNodeWrapper.firstChild_ = parentNodeWrapper.firstChild; - parentNodeWrapper.lastChild_ = parentNodeWrapper.lastChild; - if (parentNodeWrapper.lastChild === parentNodeWrapper.firstChild) - parentNodeWrapper.firstChild_ = parentNodeWrapper.firstChild; + var lastChildWrapper = wrap(parentNode.lastChild); + if (lastChildWrapper) + lastChildWrapper.nextSibling_ = lastChildWrapper.nextSibling; + } else { + if (parentNodeWrapper.firstChild === refChildWrapper) + parentNodeWrapper.firstChild_ = refChildWrapper; - var lastChildWrapper = wrap(parentNode.lastChild); - if (lastChildWrapper) { - lastChildWrapper.nextSibling_ = lastChildWrapper.nextSibling; + refChildWrapper.previousSibling_ = refChildWrapper.previousSibling; } - parentNode.appendChild(child); + parentNode.insertBefore(newChild, refChild); } - function removeChild(parentNodeWrapper, childWrapper) { - var parentNode = unwrap(parentNodeWrapper); - var child = unwrap(childWrapper); - - updateWrapperUpAndSideways(childWrapper); + function remove(nodeWrapper) { + var node = unwrap(nodeWrapper) + var parentNode = node.parentNode; + if (!parentNode) + return; - if (childWrapper.previousSibling) - childWrapper.previousSibling.nextSibling_ = childWrapper; - if (childWrapper.nextSibling) - childWrapper.nextSibling.previousSibling_ = childWrapper; + var parentNodeWrapper = wrap(parentNode); + updateWrapperUpAndSideways(nodeWrapper); - if (parentNodeWrapper.lastChild === childWrapper) - parentNodeWrapper.lastChild_ = childWrapper; - if (parentNodeWrapper.firstChild === childWrapper) - parentNodeWrapper.firstChild_ = childWrapper; + if (nodeWrapper.previousSibling) + nodeWrapper.previousSibling.nextSibling_ = nodeWrapper; + if (nodeWrapper.nextSibling) + nodeWrapper.nextSibling.previousSibling_ = nodeWrapper; - parentNode.removeChild(child); - } + if (parentNodeWrapper.lastChild === nodeWrapper) + parentNodeWrapper.lastChild_ = nodeWrapper; + if (parentNodeWrapper.firstChild === nodeWrapper) + parentNodeWrapper.firstChild_ = nodeWrapper; - function remove(nodeWrapper) { - var node = unwrap(nodeWrapper) - var parentNode = node.parentNode; - if (parentNode) - removeChild(wrap(parentNode), nodeWrapper); + parentNode.removeChild(node); } var distributedChildNodesTable = new SideTable(); @@ -114,14 +102,6 @@ var rendererForHostTable = new SideTable(); var shadowDOMRendererTable = new SideTable(); - var reprCounter = 0; - - function repr(node) { - if (!node.displayName) - node.displayName = node.nodeName + '-' + ++reprCounter; - return node.displayName; - } - function distributeChildToInsertionPoint(child, insertionPoint) { getDistributedChildNodes(insertionPoint).push(child); assignToInsertionPoint(child, insertionPoint); @@ -239,18 +219,15 @@ var renderTimer; function renderAllPending() { - renderTimer = null; pendingDirtyRenderers.forEach(function(owner) { owner.render(); }); pendingDirtyRenderers = []; } - function ShadowRenderer(host) { - this.host = host; - this.dirty = false; - this.invalidateAttributes(); - this.associateNode(host); + function handleRequestAnimationFrame() { + renderTimer = null; + renderAllPending(); } /** @@ -279,10 +256,88 @@ return getRendererForHost(getHostForShadowRoot(shadowRoot)); } + var spliceDiff = new ArraySplice(); + spliceDiff.equals = function(renderNode, rawNode) { + return unwrap(renderNode.node) === rawNode; + }; + + /** + * RenderNode is used as an in memory "render tree". When we render the + * composed tree we create a tree of RenderNodes, then we diff this against + * the real DOM tree and make minimal changes as needed. + */ + function RenderNode(node) { + this.node = node; + this.childNodes = []; + } + + RenderNode.prototype = { + append: function(node) { + var rv = new RenderNode(node); + this.childNodes.push(rv); + return rv; + }, + + sync: function(opt_added) { + var nodeWrapper = this.node; + // plain array of RenderNodes + var newChildren = this.childNodes; + // plain array of real nodes. + var oldChildren = getChildNodesSnapshot(unwrap(nodeWrapper)); + var added = opt_added || new SideTable(); + + var splices = spliceDiff.calculateSplices(newChildren, oldChildren); + + var newIndex = 0, oldIndex = 0; + var lastIndex = 0; + for (var i = 0; i < splices.length; i++) { + var splice = splices[i]; + for (; lastIndex < splice.index; lastIndex++) { + oldIndex++; + newChildren[newIndex++].sync(added); + } + + var removedCount = splice.removed.length; + for (var j = 0; j < removedCount; j++) { + var wrapper = wrap(oldChildren[oldIndex++]); + if (!added.get(wrapper)) + remove(wrapper); + } + + var addedCount = splice.addedCount; + var refNode = oldChildren[oldIndex] && wrap(oldChildren[oldIndex]); + for (var j = 0; j < addedCount; j++) { + var newChildRenderNode = newChildren[newIndex++]; + var newChildWrapper = newChildRenderNode.node; + insertBefore(nodeWrapper, newChildWrapper, refNode); + + // Keep track of added so that we do not remove the node after it + // has been added. + added.set(newChildWrapper, true); + + newChildRenderNode.sync(added); + } + + lastIndex += addedCount; + } + + for (var i = lastIndex; i < newChildren.length; i++) { + newChildren[i++].sync(added); + } + } + }; + + function ShadowRenderer(host) { + this.host = host; + this.dirty = false; + this.invalidateAttributes(); + this.associateNode(host); + } + ShadowRenderer.prototype = { // http://dvcs.w3.org/hg/webcomponents/raw-file/tip/spec/shadow/index.html#rendering-shadow-trees - render: function() { + render: function(opt_renderNode) { if (!this.dirty) return; @@ -290,15 +345,20 @@ this.treeComposition(); var host = this.host; - var shadowDOM = host.shadowRoot; + var shadowRoot = host.shadowRoot; - this.removeAllChildNodes(this.host); + this.associateNode(host); + var topMostRenderer = !renderNode; + var renderNode = opt_renderNode || new RenderNode(host); - var shadowDOMChildNodes = getChildNodesSnapshot(shadowDOM); + var shadowDOMChildNodes = getChildNodesSnapshot(shadowRoot); shadowDOMChildNodes.forEach(function(node) { - this.renderNode(host, shadowDOM, node, false); + this.renderNode(shadowRoot, renderNode, node, false); }, this); + if (topMostRenderer) + renderNode.sync(); + this.dirty = false; }, @@ -308,80 +368,84 @@ pendingDirtyRenderers.push(this); if (renderTimer) return; - renderTimer = window[request](renderAllPending, 0); + renderTimer = window[request](handleRequestAnimationFrame, 0); } }, - renderNode: function(visualParent, tree, node, isNested) { + renderNode: function(shadowRoot, renderNode, node, isNested) { if (isShadowHost(node)) { - this.appendChild(visualParent, node); + renderNode = renderNode.append(node); var renderer = getRendererForHost(node); renderer.dirty = true; // Need to rerender due to reprojection. - renderer.render(); + renderer.render(renderNode); } else if (isInsertionPoint(node)) { - this.renderInsertionPoint(visualParent, tree, node, isNested); + this.renderInsertionPoint(shadowRoot, renderNode, node, isNested); } else if (isShadowInsertionPoint(node)) { - this.renderShadowInsertionPoint(visualParent, tree, node); + this.renderShadowInsertionPoint(shadowRoot, renderNode, node); } else { - this.renderAsAnyDomTree(visualParent, tree, node, isNested); + this.renderAsAnyDomTree(shadowRoot, renderNode, node, isNested); } }, - renderAsAnyDomTree: function(visualParent, tree, node, isNested) { - this.appendChild(visualParent, node); + renderAsAnyDomTree: function(shadowRoot, renderNode, node, isNested) { + renderNode = renderNode.append(node); if (isShadowHost(node)) { - render(node); + var renderer = getRendererForHost(node); + renderer.render(renderNode); } else { var parent = node; var logicalChildNodes = getChildNodesSnapshot(parent); // We associate the parent of a content/shadow with the renderer // because we may need to remove stale childNodes. if (shadowDOMRendererTable.get(parent)) - this.removeAllChildNodes(parent); + this.associateNode(parent); logicalChildNodes.forEach(function(node) { - this.renderNode(parent, tree, node, isNested); + this.renderNode(shadowRoot, renderNode, node, isNested); }, this); } }, - renderInsertionPoint: function(visualParent, tree, insertionPoint, isNested) { + renderInsertionPoint: function(shadowRoot, renderNode, insertionPoint, + isNested) { var distributedChildNodes = getDistributedChildNodes(insertionPoint); if (distributedChildNodes.length) { - this.removeAllChildNodes(insertionPoint); + this.associateNode(insertionPoint); distributedChildNodes.forEach(function(child) { if (isInsertionPoint(child) && isNested) - this.renderInsertionPoint(visualParent, tree, child, isNested); + this.renderInsertionPoint(shadowRoot, renderNode, child, isNested); else - this.renderAsAnyDomTree(visualParent, tree, child, isNested); + this.renderAsAnyDomTree(shadowRoot, renderNode, child, isNested); }, this); } else { - this.renderFallbackContent(visualParent, insertionPoint); + this.renderFallbackContent(shadowRoot, renderNode, insertionPoint); } - this.remove(insertionPoint); + this.associateNode(insertionPoint.parentNode); }, - renderShadowInsertionPoint: function(visualParent, tree, shadowInsertionPoint) { - var nextOlderTree = tree.olderShadowRoot; + renderShadowInsertionPoint: function(shadowRoot, renderNode, + shadowInsertionPoint) { + var nextOlderTree = shadowRoot.olderShadowRoot; if (nextOlderTree) { assignToInsertionPoint(nextOlderTree, shadowInsertionPoint); - this.remove(shadowInsertionPoint); + this.associateNode(shadowInsertionPoint.parentNode); var shadowDOMChildNodes = getChildNodesSnapshot(nextOlderTree); shadowDOMChildNodes.forEach(function(node) { - this.renderNode(visualParent, nextOlderTree, node, true); + this.renderNode(nextOlderTree, renderNode, node, true); }, this); } else { - this.renderFallbackContent(visualParent, shadowInsertionPoint); + this.renderFallbackContent(shadowRoot, renderNode, + shadowInsertionPoint); } }, - renderFallbackContent: function (visualParent, fallbackHost) { + renderFallbackContent: function(shadowRoot, renderNode, fallbackHost) { var logicalChildNodes = getChildNodesSnapshot(fallbackHost); this.associateNode(fallbackHost); - this.remove(fallbackHost); + this.associateNode(fallbackHost.parentNode); logicalChildNodes.forEach(function(node) { - this.appendChild(visualParent, node); + this.renderAsAnyDomTree(shadowRoot, renderNode, node, false); }, this); }, @@ -498,23 +562,6 @@ } }, - appendChild: function(parent, child) { - // this.associateNode(child); - this.associateNode(parent); - appendChild(parent, child); - }, - - remove: function(node) { - // this.associateNode(node); - this.associateNode(node.parentNode); - remove(node); - }, - - removeAllChildNodes: function(parent) { - this.associateNode(parent); - removeAllChildNodes(parent); - }, - associateNode: function(node) { shadowDOMRendererTable.set(node, this); } @@ -613,9 +660,8 @@ // Exposed for testing scope.visual = { - removeAllChildNodes: removeAllChildNodes, - appendChild: appendChild, - removeChild: removeChild + insertBefore: insertBefore, + remove: remove, }; })(this.ShadowDOMPolyfill); \ No newline at end of file diff --git a/test/js/paralleltrees.js b/test/js/paralleltrees.js index c062ed8..10a1561 100644 --- a/test/js/paralleltrees.js +++ b/test/js/paralleltrees.js @@ -28,7 +28,8 @@ suite('Parallel Trees', function() { div.textContent = 'a'; var textNode = div.firstChild; - visual.removeAllChildNodes(div); + div.createShadowRoot(); + div.offsetWidth; expectStructure(unwrap(div), {}); expectStructure(unwrap(textNode), {}); @@ -50,7 +51,8 @@ suite('Parallel Trees', function() { var b = a.nextSibling; var c = div.lastChild; - visual.removeAllChildNodes(div); + div.createShadowRoot(); + div.offsetWidth; expectStructure(unwrap(div), {}); expectStructure(unwrap(a), {}); @@ -88,7 +90,7 @@ suite('Parallel Trees', function() { unwrapAndExpectStructure(div, {}); unwrapAndExpectStructure(textNode, {}); - visual.appendChild(div, textNode); + visual.insertBefore(div, textNode, null); unwrapAndExpectStructure(div, { firstChild: textNode, @@ -109,7 +111,7 @@ suite('Parallel Trees', function() { var a = div.firstChild; var b = document.createElement('b'); - visual.appendChild(div, b); + visual.insertBefore(div, b, null); unwrapAndExpectStructure(div, { firstChild: a, @@ -144,7 +146,7 @@ suite('Parallel Trees', function() { var b = div.lastChild; var c = document.createElement('c'); - visual.appendChild(div, c); + visual.insertBefore(div, c, null); unwrapAndExpectStructure(div, { firstChild: a, @@ -185,49 +187,6 @@ suite('Parallel Trees', function() { expectStructure(c, {}); }); - test('appendChild with document fragment', function() { - var div = document.createElement('div'); - var df = document.createDocumentFragment(); - var a = df.appendChild(document.createElement('a')); - var b = df.appendChild(document.createElement('b')); - - visual.appendChild(div, df); - - unwrapAndExpectStructure(div, { - firstChild: a, - lastChild: b - }); - - unwrapAndExpectStructure(df, {}); - - unwrapAndExpectStructure(a, { - parentNode: div, - nextSibling: b - }); - - unwrapAndExpectStructure(b, { - parentNode: div, - previousSibling: a - }); - - expectStructure(div, {}); - - expectStructure(df, { - firstChild: a, - lastChild: b - }); - - expectStructure(a, { - parentNode: df, - nextSibling: b - }); - - expectStructure(b, { - parentNode: df, - previousSibling: a - }); - }); - test('appendChild with document fragment again', function() { var div = document.createElement('div'); div.innerHTML = ''; @@ -314,12 +273,160 @@ suite('Parallel Trees', function() { }); }); + test('insertBefore', function() { + var a = document.createElement('a'); + var b = document.createElement('b'); + a.appendChild(b); + + unwrapAndExpectStructure(a, { + firstChild: b, + lastChild: b + }); + unwrapAndExpectStructure(b, { + parentNode: a + }); + + expectStructure(a, { + firstChild: b, + lastChild: b + }); + expectStructure(b, { + parentNode: a + }); + + var c = document.createElement('c'); + visual.insertBefore(a, c, b); + + unwrapAndExpectStructure(a, { + firstChild: c, + lastChild: b + }); + unwrapAndExpectStructure(b, { + parentNode: a, + previousSibling: c + }); + unwrapAndExpectStructure(c, { + parentNode: a, + nextSibling: b + }); + + expectStructure(a, { + firstChild: b, + lastChild: b + }); + expectStructure(b, { + parentNode: a + }); + expectStructure(c, {}); + + var d = document.createElement('d'); + visual.insertBefore(a, d, b); + + unwrapAndExpectStructure(a, { + firstChild: c, + lastChild: b + }); + unwrapAndExpectStructure(b, { + parentNode: a, + previousSibling: d + }); + unwrapAndExpectStructure(c, { + parentNode: a, + nextSibling: d + }); + unwrapAndExpectStructure(d, { + parentNode: a, + nextSibling: b, + previousSibling: c + }); + + expectStructure(a, { + firstChild: b, + lastChild: b + }); + expectStructure(b, { + parentNode: a + }); + expectStructure(c, {}); + expectStructure(d, {}); + }); + + test('insertBefore 2', function() { + var a = document.createElement('a'); + var b = document.createElement('b'); + var c = document.createElement('b'); + a.appendChild(b); + a.appendChild(c); + + unwrapAndExpectStructure(a, { + firstChild: b, + lastChild: c + }); + unwrapAndExpectStructure(b, { + parentNode: a, + nextSibling: c + }); + unwrapAndExpectStructure(c, { + parentNode: a, + previousSibling: b + }); + + expectStructure(a, { + firstChild: b, + lastChild: c + }); + expectStructure(b, { + parentNode: a, + nextSibling: c + }); + expectStructure(c, { + parentNode: a, + previousSibling: b + }); + + // b d c + var d = document.createElement('d'); + visual.insertBefore(a, d, c); + + unwrapAndExpectStructure(a, { + firstChild: b, + lastChild: c + }); + unwrapAndExpectStructure(b, { + parentNode: a, + nextSibling: d + }); + unwrapAndExpectStructure(c, { + parentNode: a, + previousSibling: d + }); + unwrapAndExpectStructure(d, { + parentNode: a, + previousSibling: b, + nextSibling: c + }); + + expectStructure(a, { + firstChild: b, + lastChild: c + }); + expectStructure(b, { + parentNode: a, + nextSibling: c + }); + expectStructure(c, { + parentNode: a, + previousSibling: b + }); + expectStructure(d, {}); + }); + test('removeChild, start with one child', function() { var div = document.createElement('div'); div.innerHTML = ''; var a = div.firstChild; - visual.removeChild(div, a); + visual.remove(a); unwrapAndExpectStructure(div, {}); unwrapAndExpectStructure(a, {}); @@ -340,7 +447,7 @@ suite('Parallel Trees', function() { var a = div.firstChild; var b = div.lastChild; - visual.removeChild(div, a); + visual.remove(a); unwrapAndExpectStructure(div, { firstChild: b, @@ -375,7 +482,7 @@ suite('Parallel Trees', function() { var a = div.firstChild; var b = div.lastChild; - visual.removeChild(div, b); + visual.remove(b); unwrapAndExpectStructure(div, { firstChild: a, @@ -411,7 +518,7 @@ suite('Parallel Trees', function() { var b = a.nextSibling; var c = div.lastChild; - visual.removeChild(div, b); + visual.remove(b); unwrapAndExpectStructure(div, { firstChild: a, @@ -503,7 +610,8 @@ suite('Parallel Trees', function() { var b = a.nextSibling; var c = div.lastChild; - visual.removeAllChildNodes(div); + div.createShadowRoot(); + div.offsetWidth; unwrapAndExpectStructure(div, {}); unwrapAndExpectStructure(a, {}); @@ -619,7 +727,8 @@ suite('Parallel Trees', function() { var b = a.nextSibling; var c = document.createElement('c'); - visual.removeAllChildNodes(div); + div.createShadowRoot(); + div.offsetWidth; div.appendChild(c); @@ -753,7 +862,8 @@ suite('Parallel Trees', function() { var c = a.nextSibling; var b = document.createElement('b'); - visual.removeAllChildNodes(div); + div.createShadowRoot(); + div.offsetWidth; div.insertBefore(b, c); @@ -831,8 +941,8 @@ suite('Parallel Trees', function() { var a = df.appendChild(document.createElement('a')); var b = df.appendChild(document.createElement('b')); - visual.removeAllChildNodes(div); - visual.removeAllChildNodes(df); + div.createShadowRoot(); + div.offsetWidth; div.insertBefore(df, c); @@ -944,7 +1054,8 @@ suite('Parallel Trees', function() { var a = div.firstChild; var b = div.lastChild; - visual.removeAllChildNodes(div); + div.createShadowRoot(); + div.offsetWidth; expectStructure(div, { firstChild: a, @@ -1075,7 +1186,8 @@ suite('Parallel Trees', function() { var c = a.nextSibling; var b = document.createElement('b'); - visual.removeAllChildNodes(div); + div.createShadowRoot(); + div.offsetWidth; div.replaceChild(b, c); @@ -1207,7 +1319,10 @@ suite('Parallel Trees', function() { var doc = wrap(document); var div = doc.createElement('div'); div.innerHTML = ''; - visual.removeAllChildNodes(div); + + div.createShadowRoot(); + div.offsetWidth; + var a = div.firstChild; div.innerHTML = ''; diff --git a/test/js/rerender.js b/test/js/rerender.js index fdef024..5afffe9 100644 --- a/test/js/rerender.js +++ b/test/js/rerender.js @@ -427,7 +427,6 @@ suite('Shadow DOM rerender', function() { var b = document.createElement('b'); - host.insertBefore(b, a); assert.strictEqual(getVisualInnerHtml(host), 'Hello'); @@ -462,7 +461,6 @@ suite('Shadow DOM rerender', function() { var b = document.createElement('b'); - host.replaceChild(b, a); assert.strictEqual(getVisualInnerHtml(host), ''); @@ -538,4 +536,113 @@ suite('Shadow DOM rerender', function() { host.offsetWidth; assert.equal(host.impl.innerHTML, 'wvz'); }); + + test('minimal dom changes', function() { + var div = document.createElement('div'); + var OriginalNodePrototype = + // Node.prototype + Object.getPrototypeOf( + // Element.prototype + Object.getPrototypeOf( + // HTMLElement.prototype + Object.getPrototypeOf( + // HTMLDivElement.prototype + Object.getPrototypeOf(unwrap(div))))); + + var originalInsertBefore = OriginalNodePrototype.insertBefore; + var originalRemoveChild = OriginalNodePrototype.removeChild; + + var insertBeforeCount = 0; + var removeChildCount = 0; + + OriginalNodePrototype.insertBefore = function(newChild, refChild) { + insertBeforeCount++; + return originalInsertBefore.call(this, newChild, refChild); + }; + + OriginalNodePrototype.removeChild = function(child) { + removeChildCount++; + return originalRemoveChild.call(this, child); + }; + + function reset() { + insertBeforeCount = 0; + removeChildCount = 0; + } + + try { + + var div = document.createElement('div'); + var a = div.appendChild(document.createElement('a')); + + var sr = div.createShadowRoot(); + var content = sr.appendChild(document.createElement('content')); + var b = sr.appendChild(document.createElement('b')); + + assert.equal(getVisualInnerHtml(div), ''); + + assert.equal(insertBeforeCount, 1); + assert.equal(removeChildCount, 1); + + reset(); + + // Invalidates but does not change the rendered tree. + content.select = '*'; + + assert.equal(getVisualInnerHtml(div), ''); + assert.equal(insertBeforeCount, 0); + assert.equal(removeChildCount, 0); + + // Does not use our overridden appendChild + var c = div.appendChild(document.createElement('c')); + assert.equal(insertBeforeCount, 0); + assert.equal(removeChildCount, 0); + + assert.equal(getVisualInnerHtml(div), ''); + assert.equal(insertBeforeCount, 1); + assert.equal(removeChildCount, 1); + + content.select = 'c'; + reset(); + assert.equal(getVisualInnerHtml(div), ''); + assert.equal(insertBeforeCount, 0); + assert.equal(removeChildCount, 1); + + content.select = '*'; + reset(); + assert.equal(getVisualInnerHtml(div), ''); + assert.equal(insertBeforeCount, 1); + assert.equal(removeChildCount, 0); + + content.select = 'x'; + reset(); + assert.equal(getVisualInnerHtml(div), ''); + assert.equal(insertBeforeCount, 0); + assert.equal(removeChildCount, 2); + + content.appendChild(document.createTextNode('fallback')); + reset(); + assert.equal(getVisualInnerHtml(div), 'fallback'); + assert.equal(insertBeforeCount, 1); + assert.equal(removeChildCount, 1); // moved from content + + content.insertBefore(document.createTextNode('more '), + content.firstChild); + reset(); + assert.equal(getVisualInnerHtml(div), 'more fallback'); + assert.equal(insertBeforeCount, 0); // already inserted before "fallback" + assert.equal(removeChildCount, 0); + + content.select = '*'; + reset(); + assert.equal(getVisualInnerHtml(div), ''); + assert.equal(insertBeforeCount, 2); + assert.equal(removeChildCount, 2); + + + } finally { + OriginalNodePrototype.insertBefore = originalInsertBefore; + OriginalNodePrototype.removeChild = originalRemoveChild; + } + }); }); \ No newline at end of file