forked from ungleich-public/dynamicweb
688 lines
29 KiB
JavaScript
688 lines
29 KiB
JavaScript
/*
|
|
* 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 = '<span>Text</span>';
|
|
|
|
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 = '<ul><li>Item 1</li><li>Item 2</li></ul>';
|
|
|
|
const obj = nodeToObj(div);
|
|
|
|
expect(obj.nodeName).toBe('DIV');
|
|
expect(obj.childNodes.length).toBe(1);
|
|
expect(obj.childNodes[0].nodeName).toBe('UL');
|
|
expect(obj.childNodes[0].childNodes.length).toBe(2);
|
|
});
|
|
});
|
|
|
|
describe('diff and apply', function() {
|
|
it('applies simple HTML string to empty container', function() {
|
|
const diff = dd.diff(container, '<div><p>Hello</p></div>');
|
|
dd.apply(container, diff);
|
|
|
|
expect(container.children.length).toBe(1);
|
|
expect(container.children[0].tagName).toBe('P');
|
|
expect(container.children[0].textContent).toBe('Hello');
|
|
});
|
|
|
|
it('replaces existing content with new HTML', function() {
|
|
container.innerHTML = '<p>Old content</p>';
|
|
|
|
const diff = dd.diff(container, '<div><span>New content</span></div>');
|
|
dd.apply(container, diff);
|
|
|
|
expect(container.children.length).toBe(1);
|
|
expect(container.children[0].tagName).toBe('SPAN');
|
|
expect(container.children[0].textContent).toBe('New content');
|
|
});
|
|
|
|
it('handles multiple child elements', function() {
|
|
const diff = dd.diff(container, '<div><p>First</p><p>Second</p><p>Third</p></div>');
|
|
dd.apply(container, diff);
|
|
|
|
expect(container.children.length).toBe(3);
|
|
expect(container.children[0].textContent).toBe('First');
|
|
expect(container.children[1].textContent).toBe('Second');
|
|
expect(container.children[2].textContent).toBe('Third');
|
|
});
|
|
|
|
it('preserves attributes when applying diff', function() {
|
|
const html = '<div><a href="/test" class="link" data-id="123">Link</a></div>';
|
|
const diff = dd.diff(container, html);
|
|
dd.apply(container, diff);
|
|
|
|
const link = container.querySelector('a');
|
|
expect(link).not.toBeNull();
|
|
expect(link.getAttribute('href')).toBe('/test');
|
|
expect(link.getAttribute('class')).toBe('link');
|
|
expect(link.getAttribute('data-id')).toBe('123');
|
|
expect(link.textContent).toBe('Link');
|
|
});
|
|
|
|
it('handles complex nested structures', function() {
|
|
const html = `<div>
|
|
<div class="header">
|
|
<h1>Title</h1>
|
|
<nav>
|
|
<ul>
|
|
<li><a href="#">Home</a></li>
|
|
<li><a href="#">About</a></li>
|
|
</ul>
|
|
</nav>
|
|
</div>
|
|
<div class="content">
|
|
<p>Paragraph 1</p>
|
|
<p>Paragraph 2</p>
|
|
</div>
|
|
</div>`;
|
|
|
|
const diff = dd.diff(container, html);
|
|
dd.apply(container, diff);
|
|
|
|
expect(container.querySelector('.header')).not.toBeNull();
|
|
expect(container.querySelector('.content')).not.toBeNull();
|
|
expect(container.querySelector('h1').textContent).toBe('Title');
|
|
expect(container.querySelectorAll('li').length).toBe(2);
|
|
expect(container.querySelectorAll('.content p').length).toBe(2);
|
|
});
|
|
|
|
it('handles script tags correctly', function() {
|
|
const html = '<div><script>console.log("test");</script><p>Content</p></div>';
|
|
const diff = dd.diff(container, html);
|
|
dd.apply(container, diff);
|
|
|
|
expect(container.querySelector('script')).not.toBeNull();
|
|
expect(container.querySelector('p')).not.toBeNull();
|
|
});
|
|
|
|
it('clears container when applying empty content', function() {
|
|
container.innerHTML = '<p>Old</p><p>Content</p>';
|
|
|
|
const diff = dd.diff(container, '<div></div>');
|
|
dd.apply(container, diff);
|
|
|
|
expect(container.children.length).toBe(0);
|
|
});
|
|
|
|
it('works with nodeToObj conversion', function() {
|
|
const original = document.createElement('div');
|
|
original.innerHTML = '<span class="test">Text content</span>';
|
|
|
|
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 = '<div><p> Text with spaces </p></div>';
|
|
const diff = dd.diff(container, html);
|
|
dd.apply(container, diff);
|
|
|
|
expect(container.querySelector('p').textContent).toBe(' Text with spaces ');
|
|
});
|
|
|
|
it('handles special characters in text', function() {
|
|
const html = '<div><p><script>alert("xss")</script></p></div>';
|
|
const diff = dd.diff(container, html);
|
|
dd.apply(container, diff);
|
|
|
|
expect(container.querySelector('p').textContent).toContain('<script>');
|
|
expect(container.querySelector('p').textContent).toContain('</script>');
|
|
});
|
|
});
|
|
|
|
describe('real-world use cases from cms.structureboard', function() {
|
|
it('simulates updating a plugin container', function() {
|
|
// Initial state: empty container
|
|
const pluginContainer = document.createElement('div');
|
|
pluginContainer.className = 'cms-plugin';
|
|
document.body.appendChild(pluginContainer);
|
|
|
|
// Simulate server response with new plugin content
|
|
const serverHTML = `<div>
|
|
<div class="cms-plugin-wrapper">
|
|
<div class="cms-plugin-content">
|
|
<h2>New Plugin</h2>
|
|
<p>Updated content from server</p>
|
|
</div>
|
|
</div>
|
|
</div>`;
|
|
|
|
const diff = dd.diff(pluginContainer, serverHTML);
|
|
dd.apply(pluginContainer, diff);
|
|
|
|
expect(pluginContainer.querySelector('.cms-plugin-wrapper')).not.toBeNull();
|
|
expect(pluginContainer.querySelector('h2').textContent).toBe('New Plugin');
|
|
expect(pluginContainer.querySelector('p').textContent).toBe('Updated content from server');
|
|
|
|
pluginContainer.parentNode.removeChild(pluginContainer);
|
|
});
|
|
|
|
it('simulates updating document head metadata', function() {
|
|
// Create a mock head element
|
|
const mockHead = document.createElement('div');
|
|
mockHead.innerHTML = `
|
|
<title>Old Title</title>
|
|
<meta name="description" content="Old description">
|
|
`;
|
|
|
|
// New head content from server
|
|
const newHeadObj = {
|
|
nodeName: 'HEAD',
|
|
attributes: {},
|
|
childNodes: [
|
|
{
|
|
nodeName: 'TITLE',
|
|
attributes: {},
|
|
childNodes: [
|
|
{
|
|
nodeName: '#text',
|
|
data: 'New Title'
|
|
}
|
|
]
|
|
},
|
|
{
|
|
nodeName: 'META',
|
|
attributes: {
|
|
name: 'description',
|
|
content: 'New description'
|
|
},
|
|
childNodes: []
|
|
},
|
|
{
|
|
nodeName: 'LINK',
|
|
attributes: {
|
|
rel: 'stylesheet',
|
|
href: '/static/new.css'
|
|
},
|
|
childNodes: []
|
|
}
|
|
]
|
|
};
|
|
|
|
const diff = dd.diff(mockHead, newHeadObj);
|
|
dd.apply(mockHead, diff);
|
|
|
|
expect(mockHead.querySelector('title').textContent).toBe('New Title');
|
|
expect(mockHead.querySelector('meta[name="description"]').getAttribute('content')).toBe('New description');
|
|
expect(mockHead.querySelector('link[rel="stylesheet"]').getAttribute('href')).toBe('/static/new.css');
|
|
});
|
|
|
|
it('handles forms with input elements', function() {
|
|
const formHTML = `<div>
|
|
<form id="test-form" action="/submit" method="post">
|
|
<input type="text" name="username" value="testuser" required>
|
|
<input type="email" name="email" placeholder="email@example.com">
|
|
<textarea name="message">Default message</textarea>
|
|
<select name="country">
|
|
<option value="de">Germany</option>
|
|
<option value="us" selected>USA</option>
|
|
</select>
|
|
<button type="submit">Submit</button>
|
|
</form>
|
|
</div>`;
|
|
|
|
const diff = dd.diff(container, formHTML);
|
|
dd.apply(container, diff);
|
|
|
|
const form = container.querySelector('form');
|
|
expect(form.getAttribute('action')).toBe('/submit');
|
|
expect(form.getAttribute('method')).toBe('post');
|
|
|
|
const usernameInput = container.querySelector('input[name="username"]');
|
|
expect(usernameInput.getAttribute('value')).toBe('testuser');
|
|
expect(usernameInput.hasAttribute('required')).toBe(true);
|
|
|
|
const textarea = container.querySelector('textarea');
|
|
expect(textarea.textContent).toBe('Default message');
|
|
|
|
const select = container.querySelector('select');
|
|
expect(select.querySelectorAll('option').length).toBe(2);
|
|
});
|
|
});
|
|
|
|
describe('edge cases and error handling', function() {
|
|
it('handles null or undefined gracefully', function() {
|
|
expect(function() {
|
|
nodeToObj(null);
|
|
}).not.toThrow();
|
|
});
|
|
|
|
it('handles malformed HTML', function() {
|
|
// Parser should handle malformed HTML gracefully
|
|
const malformedHTML = '<div><p>Unclosed paragraph<div>Nested</div>';
|
|
const diff = dd.diff(container, malformedHTML);
|
|
|
|
expect(function() {
|
|
dd.apply(container, diff);
|
|
}).not.toThrow();
|
|
});
|
|
|
|
it('does not re-execute identical script elements', function() {
|
|
// Set up container with a script that sets a global counter
|
|
var script = document.createElement('script');
|
|
script.textContent = 'window.__domDiffTestCounter = (window.__domDiffTestCounter || 0) + 1;';
|
|
container.appendChild(script);
|
|
|
|
// Script executed once on initial insert
|
|
expect(window.__domDiffTestCounter).toBe(1);
|
|
|
|
// Build a new node with an identical script
|
|
var wrapper = document.createElement('div');
|
|
var identicalScript = document.createElement('script');
|
|
identicalScript.textContent = 'window.__domDiffTestCounter = (window.__domDiffTestCounter || 0) + 1;';
|
|
wrapper.appendChild(identicalScript);
|
|
|
|
var diff = dd.diff(container, nodeToObj(wrapper));
|
|
dd.apply(container, diff);
|
|
|
|
// Counter should still be 1 — the identical script was reused, not re-executed
|
|
expect(window.__domDiffTestCounter).toBe(1);
|
|
|
|
delete window.__domDiffTestCounter;
|
|
});
|
|
|
|
it('executes new script elements that differ from existing ones', function() {
|
|
var script = document.createElement('script');
|
|
script.textContent = 'window.__domDiffNewCounter = 1;';
|
|
container.appendChild(script);
|
|
|
|
expect(window.__domDiffNewCounter).toBe(1);
|
|
|
|
// Apply diff with a different script
|
|
var wrapper = document.createElement('div');
|
|
var newScript = document.createElement('script');
|
|
newScript.textContent = 'window.__domDiffNewCounter = 42;';
|
|
wrapper.appendChild(newScript);
|
|
|
|
var diff = dd.diff(container, nodeToObj(wrapper));
|
|
dd.apply(container, diff);
|
|
|
|
// New script should have executed
|
|
expect(window.__domDiffNewCounter).toBe(42);
|
|
|
|
delete window.__domDiffNewCounter;
|
|
});
|
|
|
|
it('reuses unchanged nodes (same DOM reference) and removes changed ones', function() {
|
|
// Set up container with three children
|
|
var meta = document.createElement('meta');
|
|
meta.setAttribute('name', 'description');
|
|
meta.setAttribute('content', 'Original');
|
|
meta.marker = 'meta'; // Custom property to identify this node
|
|
|
|
var link = document.createElement('link');
|
|
link.setAttribute('rel', 'stylesheet');
|
|
link.setAttribute('href', '/static/style.css');
|
|
|
|
var title = document.createElement('title');
|
|
title.textContent = 'Old Title';
|
|
|
|
container.appendChild(meta);
|
|
container.appendChild(link);
|
|
container.appendChild(title);
|
|
|
|
// Keep references to the original DOM nodes
|
|
var originalMeta = container.childNodes[0];
|
|
var originalLink = container.childNodes[1];
|
|
|
|
|
|
// Build new content: link unchanged, meta changed, title removed, script added
|
|
var wrapper = document.createElement('div');
|
|
|
|
var newMeta = document.createElement('meta');
|
|
newMeta.setAttribute('name', 'description');
|
|
newMeta.setAttribute('content', 'Updated');
|
|
wrapper.appendChild(newMeta);
|
|
|
|
var sameLinkCopy = document.createElement('link');
|
|
sameLinkCopy.setAttribute('rel', 'stylesheet');
|
|
sameLinkCopy.setAttribute('href', '/static/style.css');
|
|
wrapper.appendChild(sameLinkCopy);
|
|
|
|
var newScript = document.createElement('script');
|
|
newScript.setAttribute('src', '/static/app.js');
|
|
wrapper.appendChild(newScript);
|
|
|
|
var diff = dd.diff(container, nodeToObj(wrapper));
|
|
dd.apply(container, diff);
|
|
|
|
// Link was unchanged — must be the exact same DOM node (not a clone)
|
|
expect(container.childNodes.length).toBe(3);
|
|
expect(container.childNodes[1]).toBe(originalLink);
|
|
|
|
// Meta had its content attribute changed — with recursive diffing the
|
|
// outer element is reused (shallow match on tag + name attr) and the
|
|
// attribute is synced in place.
|
|
expect(container.childNodes[0]).toBe(originalMeta);
|
|
expect(container.childNodes[0].getAttribute('content')).toBe('Updated');
|
|
expect(container.childNodes[0].marker).toBe('meta'); // Custom property preserved
|
|
|
|
// Title was removed
|
|
expect(container.querySelector('title')).toBeNull();
|
|
|
|
// Script was added
|
|
expect(container.querySelector('script')).not.toBeNull();
|
|
expect(container.querySelector('script').getAttribute('src')).toBe('/static/app.js');
|
|
});
|
|
|
|
it('removes all unmatched nodes even when keys collide', function() {
|
|
// Multiple identical whitespace text nodes — a common case in <head>
|
|
var p1 = document.createElement('p');
|
|
p1.textContent = 'Keep';
|
|
p1.marker = 'marked'; // Custom property to identify this node
|
|
var p2 = document.createElement('p');
|
|
p2.textContent = 'Remove';
|
|
|
|
container.appendChild(document.createTextNode('\n'));
|
|
container.appendChild(p1);
|
|
container.appendChild(document.createTextNode('\n'));
|
|
container.appendChild(p2);
|
|
container.appendChild(document.createTextNode('\n'));
|
|
|
|
var originalP1 = p1;
|
|
|
|
// New content: only one <p> 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 = '<p>Old</p>';
|
|
|
|
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 = '<p>Old</p>';
|
|
|
|
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 = `<div>
|
|
<svg width="100" height="100">
|
|
<circle cx="50" cy="50" r="40" fill="red" />
|
|
</svg>
|
|
</div>`;
|
|
|
|
const diff = dd.diff(container, svgHTML);
|
|
dd.apply(container, diff);
|
|
|
|
expect(container.querySelector('svg')).not.toBeNull();
|
|
expect(container.querySelector('circle')).not.toBeNull();
|
|
});
|
|
});
|
|
});
|