Paragraph 1
Paragraph 2
/* * Copyright https://github.com/divio/django-cms * Tests for dom-diff module (native browser API replacement for diff-dom) */ /* global describe, it, expect, beforeEach, afterEach */ 'use strict'; import { DiffDOM, nodeToObj } from '../../../static/cms/js/modules/dom-diff'; describe('DiffDOM', function() { let dd; let container; beforeEach(function() { dd = new DiffDOM(); container = document.createElement('div'); document.body.appendChild(container); }); afterEach(function() { if (container && container.parentNode) { container.parentNode.removeChild(container); } }); describe('nodeToObj', function() { it('converts a text node to object', function() { const textNode = document.createTextNode('Hello World'); const obj = nodeToObj(textNode); expect(obj.nodeName).toBe('#text'); expect(obj.data).toBe('Hello World'); }); it('converts an element node to object', function() { const div = document.createElement('div'); div.setAttribute('id', 'test'); div.setAttribute('class', 'example'); div.innerHTML = 'Text'; const obj = nodeToObj(div); expect(obj.nodeName).toBe('DIV'); expect(obj.attributes.id).toBe('test'); expect(obj.attributes.class).toBe('example'); expect(obj.childNodes.length).toBe(1); expect(obj.childNodes[0].nodeName).toBe('SPAN'); }); it('converts a comment node to object', function() { var comment = document.createComment(' a comment '); var obj = nodeToObj(comment); expect(obj.nodeName).toBe('#comment'); expect(obj.data).toBe(' a comment '); }); it('preserves comments inside elements', function() { var div = document.createElement('div'); div.appendChild(document.createComment('before')); div.appendChild(document.createElement('p')); div.appendChild(document.createComment('after')); var obj = nodeToObj(div); // All three children should be present, no nulls expect(obj.childNodes.length).toBe(3); expect(obj.childNodes[0].nodeName).toBe('#comment'); expect(obj.childNodes[0].data).toBe('before'); expect(obj.childNodes[1].nodeName).toBe('P'); expect(obj.childNodes[2].nodeName).toBe('#comment'); expect(obj.childNodes[2].data).toBe('after'); }); it('handles nested elements', function() { const div = document.createElement('div'); div.innerHTML = '
Hello
Old content
'; const diff = dd.diff(container, 'First
Second
Third
Paragraph 1
Paragraph 2
Content
Old
Content
'; const diff = dd.diff(container, ''); dd.apply(container, diff); expect(container.children.length).toBe(0); }); it('works with nodeToObj conversion', function() { const original = document.createElement('div'); original.innerHTML = 'Text content'; const obj = nodeToObj(original); const diff = dd.diff(container, obj); dd.apply(container, diff); expect(container.querySelector('span')).not.toBeNull(); expect(container.querySelector('span').className).toBe('test'); expect(container.querySelector('span').textContent).toBe('Text content'); }); it('preserves comment nodes through round-trip', function() { var original = document.createElement('div'); original.appendChild(document.createComment(' server-rendered ')); original.appendChild(document.createElement('p')); original.appendChild(document.createComment(' end ')); var obj = nodeToObj(original); var diff = dd.diff(container, obj); dd.apply(container, diff); // Comments should survive the nodeToObj -> _objToNode -> apply round-trip expect(container.childNodes.length).toBe(3); expect(container.childNodes[0].nodeType).toBe(Node.COMMENT_NODE); expect(container.childNodes[0].data).toBe(' server-rendered '); expect(container.childNodes[1].nodeName).toBe('P'); expect(container.childNodes[2].nodeType).toBe(Node.COMMENT_NODE); expect(container.childNodes[2].data).toBe(' end '); }); it('preserves text nodes with whitespace', function() { const html = 'Text with spaces
<script>alert("xss")</script>
Updated content from server
Unclosed paragraph
and two text nodes var wrapper = document.createElement('div'); wrapper.appendChild(document.createTextNode('\n')); var pKeep = document.createElement('p'); pKeep.textContent = 'Keep'; wrapper.appendChild(pKeep); wrapper.appendChild(document.createTextNode('\n')); var diff = dd.diff(container, nodeToObj(wrapper)); dd.apply(container, diff); // Should have exactly 3 children: text, p, text expect(container.childNodes.length).toBe(3); expect(container.childNodes[1].textContent).toBe('Keep'); expect(container.childNodes[1]).toBe(originalP1); // "Remove" paragraph must be gone expect(container.querySelector('p:last-of-type').textContent).toBe('Keep'); expect(container.querySelector('p:last-of-type').marker).toBe('marked'); }); it('does not re-execute nested scripts when outer element changes', function() { // Set up container with a div wrapping a script var wrapper = document.createElement('div'); wrapper.setAttribute('class', 'plugin'); var script = document.createElement('script'); script.textContent = 'window.__nestedScriptCounter = (window.__nestedScriptCounter || 0) + 1;'; wrapper.appendChild(script); var p = document.createElement('p'); p.textContent = 'Old text'; wrapper.appendChild(p); container.appendChild(wrapper); // Script executed once on initial insert expect(window.__nestedScriptCounter).toBe(1); var originalScript = container.querySelector('script'); var originalWrapper = container.firstChild; // Build new content: same div wrapper, same script, but paragraph text changed var newOuter = document.createElement('div'); var newWrapper = document.createElement('div'); newWrapper.setAttribute('class', 'plugin'); var sameScript = document.createElement('script'); sameScript.textContent = 'window.__nestedScriptCounter = (window.__nestedScriptCounter || 0) + 1;'; newWrapper.appendChild(sameScript); var newP = document.createElement('p'); newP.textContent = 'New text'; newWrapper.appendChild(newP); newOuter.appendChild(newWrapper); var diff = dd.diff(container, nodeToObj(newOuter)); dd.apply(container, diff); // Script should NOT have re-executed — counter still 1 expect(window.__nestedScriptCounter).toBe(1); // Script should be the exact same DOM node expect(container.querySelector('script')).toBe(originalScript); // Outer wrapper should be preserved (shallow match, same tag + class synced) expect(container.firstChild).toBe(originalWrapper); // Paragraph text should be updated expect(container.querySelector('p').textContent).toBe('New text'); delete window.__nestedScriptCounter; }); it('preserves outer element when only inner content changes', function() { var outer = document.createElement('div'); outer.setAttribute('class', 'outer'); outer.marker = 'original-outer'; var inner = document.createElement('span'); inner.textContent = 'old'; outer.appendChild(inner); container.appendChild(outer); var originalOuter = container.firstChild; // Build new content: same outer div, different inner text var newRoot = document.createElement('div'); var newOuter = document.createElement('div'); newOuter.setAttribute('class', 'outer'); var newInner = document.createElement('span'); newInner.textContent = 'new'; newOuter.appendChild(newInner); newRoot.appendChild(newOuter); var diff = dd.diff(container, nodeToObj(newRoot)); dd.apply(container, diff); // Outer element should be the same DOM node (preserved via shallow match) expect(container.firstChild).toBe(originalOuter); expect(container.firstChild.marker).toBe('original-outer'); // Inner content should be updated expect(container.querySelector('span').textContent).toBe('new'); }); it('updates attributes on outer element while preserving it', function() { var outer = document.createElement('div'); outer.setAttribute('class', 'old-class'); outer.setAttribute('data-keep', 'yes'); outer.marker = 'same-node'; var inner = document.createElement('p'); inner.textContent = 'content'; outer.appendChild(inner); container.appendChild(outer); var originalOuter = container.firstChild; // Build new content: same div, different class, removed data-keep, added data-new var newRoot = document.createElement('div'); var newOuter = document.createElement('div'); newOuter.setAttribute('class', 'new-class'); newOuter.setAttribute('data-new', 'added'); var newInner = document.createElement('p'); newInner.textContent = 'content'; newOuter.appendChild(newInner); newRoot.appendChild(newOuter); var diff = dd.diff(container, nodeToObj(newRoot)); dd.apply(container, diff); // Same DOM node preserved expect(container.firstChild).toBe(originalOuter); expect(container.firstChild.marker).toBe('same-node'); // Attributes synced expect(container.firstChild.getAttribute('class')).toBe('new-class'); expect(container.firstChild.getAttribute('data-new')).toBe('added'); expect(container.firstChild.getAttribute('data-keep')).toBeNull(); }); it('does not re-execute deeply nested scripts', function() { // Three levels deep: container > div.a > div.b > script var a = document.createElement('div'); a.setAttribute('class', 'a'); var b = document.createElement('div'); b.setAttribute('class', 'b'); var script = document.createElement('script'); script.textContent = 'window.__deepCounter = (window.__deepCounter || 0) + 1;'; b.appendChild(script); a.appendChild(b); container.appendChild(a); expect(window.__deepCounter).toBe(1); var originalScript = container.querySelector('script'); // Build new content: add a sibling to div.b, script unchanged var newRoot = document.createElement('div'); var newA = document.createElement('div'); newA.setAttribute('class', 'a'); var newB = document.createElement('div'); newB.setAttribute('class', 'b'); var sameScript = document.createElement('script'); sameScript.textContent = 'window.__deepCounter = (window.__deepCounter || 0) + 1;'; newB.appendChild(sameScript); var extra = document.createElement('p'); extra.textContent = 'added'; newB.appendChild(extra); newA.appendChild(newB); newRoot.appendChild(newA); var diff = dd.diff(container, nodeToObj(newRoot)); dd.apply(container, diff); // Script must not have re-executed expect(window.__deepCounter).toBe(1); expect(container.querySelector('script')).toBe(originalScript); // New paragraph should be present expect(container.querySelector('p').textContent).toBe('added'); delete window.__deepCounter; }); it('applies plain text string without losing content', function() { // When newContent is a plain string like 'hello', DOMParser produces // a text node as doc.body.firstChild. apply() must handle this and // set the target's textContent rather than clearing it. (#8556 related) container.innerHTML = '
Old
'; var diff = dd.diff(container, 'hello'); dd.apply(container, diff); expect(container.textContent).toBe('hello'); }); it('applies text-node object without losing content', function() { // When newContent is a #text object, _objToNode returns a text node. // apply() must handle this and preserve the text. container.innerHTML = 'Old
'; var diff = dd.diff(container, { nodeName: '#text', data: 'from object' }); dd.apply(container, diff); expect(container.textContent).toBe('from object'); }); it('handles SVG elements', function() { const svgHTML = `