···11+/* eslint-disable */
22+var addSorting = (function() {
33+ 'use strict';
44+ var cols,
55+ currentSort = {
66+ index: 0,
77+ desc: false
88+ };
99+1010+ // returns the summary table element
1111+ function getTable() {
1212+ return document.querySelector('.coverage-summary');
1313+ }
1414+ // returns the thead element of the summary table
1515+ function getTableHeader() {
1616+ return getTable().querySelector('thead tr');
1717+ }
1818+ // returns the tbody element of the summary table
1919+ function getTableBody() {
2020+ return getTable().querySelector('tbody');
2121+ }
2222+ // returns the th element for nth column
2323+ function getNthColumn(n) {
2424+ return getTableHeader().querySelectorAll('th')[n];
2525+ }
2626+2727+ function onFilterInput() {
2828+ const searchValue = document.getElementById('fileSearch').value;
2929+ const rows = document.getElementsByTagName('tbody')[0].children;
3030+3131+ // Try to create a RegExp from the searchValue. If it fails (invalid regex),
3232+ // it will be treated as a plain text search
3333+ let searchRegex;
3434+ try {
3535+ searchRegex = new RegExp(searchValue, 'i'); // 'i' for case-insensitive
3636+ } catch (error) {
3737+ searchRegex = null;
3838+ }
3939+4040+ for (let i = 0; i < rows.length; i++) {
4141+ const row = rows[i];
4242+ let isMatch = false;
4343+4444+ if (searchRegex) {
4545+ // If a valid regex was created, use it for matching
4646+ isMatch = searchRegex.test(row.textContent);
4747+ } else {
4848+ // Otherwise, fall back to the original plain text search
4949+ isMatch = row.textContent
5050+ .toLowerCase()
5151+ .includes(searchValue.toLowerCase());
5252+ }
5353+5454+ row.style.display = isMatch ? '' : 'none';
5555+ }
5656+ }
5757+5858+ // loads the search box
5959+ function addSearchBox() {
6060+ var template = document.getElementById('filterTemplate');
6161+ var templateClone = template.content.cloneNode(true);
6262+ templateClone.getElementById('fileSearch').oninput = onFilterInput;
6363+ template.parentElement.appendChild(templateClone);
6464+ }
6565+6666+ // loads all columns
6767+ function loadColumns() {
6868+ var colNodes = getTableHeader().querySelectorAll('th'),
6969+ colNode,
7070+ cols = [],
7171+ col,
7272+ i;
7373+7474+ for (i = 0; i < colNodes.length; i += 1) {
7575+ colNode = colNodes[i];
7676+ col = {
7777+ key: colNode.getAttribute('data-col'),
7878+ sortable: !colNode.getAttribute('data-nosort'),
7979+ type: colNode.getAttribute('data-type') || 'string'
8080+ };
8181+ cols.push(col);
8282+ if (col.sortable) {
8383+ col.defaultDescSort = col.type === 'number';
8484+ colNode.innerHTML =
8585+ colNode.innerHTML + '<span class="sorter"></span>';
8686+ }
8787+ }
8888+ return cols;
8989+ }
9090+ // attaches a data attribute to every tr element with an object
9191+ // of data values keyed by column name
9292+ function loadRowData(tableRow) {
9393+ var tableCols = tableRow.querySelectorAll('td'),
9494+ colNode,
9595+ col,
9696+ data = {},
9797+ i,
9898+ val;
9999+ for (i = 0; i < tableCols.length; i += 1) {
100100+ colNode = tableCols[i];
101101+ col = cols[i];
102102+ val = colNode.getAttribute('data-value');
103103+ if (col.type === 'number') {
104104+ val = Number(val);
105105+ }
106106+ data[col.key] = val;
107107+ }
108108+ return data;
109109+ }
110110+ // loads all row data
111111+ function loadData() {
112112+ var rows = getTableBody().querySelectorAll('tr'),
113113+ i;
114114+115115+ for (i = 0; i < rows.length; i += 1) {
116116+ rows[i].data = loadRowData(rows[i]);
117117+ }
118118+ }
119119+ // sorts the table using the data for the ith column
120120+ function sortByIndex(index, desc) {
121121+ var key = cols[index].key,
122122+ sorter = function(a, b) {
123123+ a = a.data[key];
124124+ b = b.data[key];
125125+ return a < b ? -1 : a > b ? 1 : 0;
126126+ },
127127+ finalSorter = sorter,
128128+ tableBody = document.querySelector('.coverage-summary tbody'),
129129+ rowNodes = tableBody.querySelectorAll('tr'),
130130+ rows = [],
131131+ i;
132132+133133+ if (desc) {
134134+ finalSorter = function(a, b) {
135135+ return -1 * sorter(a, b);
136136+ };
137137+ }
138138+139139+ for (i = 0; i < rowNodes.length; i += 1) {
140140+ rows.push(rowNodes[i]);
141141+ tableBody.removeChild(rowNodes[i]);
142142+ }
143143+144144+ rows.sort(finalSorter);
145145+146146+ for (i = 0; i < rows.length; i += 1) {
147147+ tableBody.appendChild(rows[i]);
148148+ }
149149+ }
150150+ // removes sort indicators for current column being sorted
151151+ function removeSortIndicators() {
152152+ var col = getNthColumn(currentSort.index),
153153+ cls = col.className;
154154+155155+ cls = cls.replace(/ sorted$/, '').replace(/ sorted-desc$/, '');
156156+ col.className = cls;
157157+ }
158158+ // adds sort indicators for current column being sorted
159159+ function addSortIndicators() {
160160+ getNthColumn(currentSort.index).className += currentSort.desc
161161+ ? ' sorted-desc'
162162+ : ' sorted';
163163+ }
164164+ // adds event listeners for all sorter widgets
165165+ function enableUI() {
166166+ var i,
167167+ el,
168168+ ithSorter = function ithSorter(i) {
169169+ var col = cols[i];
170170+171171+ return function() {
172172+ var desc = col.defaultDescSort;
173173+174174+ if (currentSort.index === i) {
175175+ desc = !currentSort.desc;
176176+ }
177177+ sortByIndex(i, desc);
178178+ removeSortIndicators();
179179+ currentSort.index = i;
180180+ currentSort.desc = desc;
181181+ addSortIndicators();
182182+ };
183183+ };
184184+ for (i = 0; i < cols.length; i += 1) {
185185+ if (cols[i].sortable) {
186186+ // add the click event handler on the th so users
187187+ // dont have to click on those tiny arrows
188188+ el = getNthColumn(i).querySelector('.sorter').parentElement;
189189+ if (el.addEventListener) {
190190+ el.addEventListener('click', ithSorter(i));
191191+ } else {
192192+ el.attachEvent('onclick', ithSorter(i));
193193+ }
194194+ }
195195+ }
196196+ }
197197+ // adds sorting functionality to the UI
198198+ return function() {
199199+ if (!getTable()) {
200200+ return;
201201+ }
202202+ cols = loadColumns();
203203+ loadData();
204204+ addSearchBox();
205205+ addSortIndicators();
206206+ enableUI();
207207+ };
208208+})();
209209+210210+window.addEventListener('load', addSorting);
+4-4
dist/morphlex.js
···183183 const child = reference.children[i];
184184 refChildNodesMap.set(child.outerHTML, child);
185185 }
186186- const nodeChildrenLength = node.children.length;
187187- for (let i = 0; i < nodeChildrenLength; i++) {
186186+ // Iterate backwards to safely remove children without affecting indices
187187+ for (let i = node.children.length - 1; i >= 0; i--) {
188188 const child = node.children[i];
189189 const key = child.outerHTML;
190190 const refChild = refChildNodesMap.get(key);
191191- // If the child is in the reference map already, we don’t need to add it later.
192192- // If it’s not in the map, we need to remove it from the node.
191191+ // If the child is in the reference map already, we don't need to add it later.
192192+ // If it's not in the map, we need to remove it from the node.
193193 refChild ? refChildNodesMap.delete(key) : this.#removeNode(child);
194194 }
195195 // Any remaining nodes in the map should be appended to the head.
+2-9
package.json
···1818 "scripts": {
1919 "test": "vitest run",
2020 "test:watch": "vitest",
2121- "test:ui": "vitest --ui",
2222- "build": "bun run tsc && bun run prettier --write ./src ./dist",
2323- "watch": "bun run tsc -w",
2424- "lint": "bun run prettier --check ./src ./dist ./test",
2525- "minify": "bun run terser dist/morphlex.js -o dist/morphlex.min.js --config-file terser-config.json",
2626- "prepare": "bun run build && bun run minify",
2727- "ship": "bun run prepare && bun run test && bun run lint && npm publish",
2828- "format": "bun run prettier --write ./src ./dist ./test",
2929- "size": "bun run prepare && bun run gzip-size ./dist/morphlex.min.js --raw --include-original"
2121+ "test:ui": "vitest --ui"
3022 },
3123 "devDependencies": {
2424+ "@vitest/coverage-v8": "^4.0.5",
3225 "@vitest/ui": "^4.0.5",
3326 "gzip-size-cli": "^5.1.0",
3427 "happy-dom": "^20.0.10",
+4-4
src/morphlex.ts
···245245 refChildNodesMap.set(child.outerHTML, child);
246246 }
247247248248- const nodeChildrenLength = node.children.length;
249249- for (let i = 0; i < nodeChildrenLength; i++) {
248248+ // Iterate backwards to safely remove children without affecting indices
249249+ for (let i = node.children.length - 1; i >= 0; i--) {
250250 const child = node.children[i];
251251 const key = child.outerHTML;
252252 const refChild = refChildNodesMap.get(key);
253253254254- // If the child is in the reference map already, we don’t need to add it later.
255255- // If it’s not in the map, we need to remove it from the node.
254254+ // If the child is in the reference map already, we don't need to add it later.
255255+ // If it's not in the map, we need to remove it from the node.
256256 refChild ? refChildNodesMap.delete(key) : this.#removeNode(child);
257257 }
258258
+1398
test/morphlex-coverage.test.ts
···11+import { describe, it, expect, beforeEach, afterEach } from "vitest";
22+import { morph, morphInner } from "../src/morphlex";
33+44+describe("Morphlex - Coverage Tests", () => {
55+ let container: HTMLElement;
66+77+ beforeEach(() => {
88+ container = document.createElement("div");
99+ document.body.appendChild(container);
1010+ });
1111+1212+ afterEach(() => {
1313+ if (container && container.parentNode) {
1414+ container.parentNode.removeChild(container);
1515+ }
1616+ });
1717+1818+ describe("String parsing error cases", () => {
1919+ it("should throw error when parseElementFromString receives non-element string", () => {
2020+ const div = document.createElement("div");
2121+ container.appendChild(div);
2222+2323+ // Text node is not an element
2424+ expect(() => {
2525+ morphInner(div, "Just text");
2626+ }).toThrow("[Morphlex] The string was not a valid HTML element.");
2727+ });
2828+2929+ it("should parse multiple elements as valid HTML (they go into body)", () => {
3030+ const div = document.createElement("div");
3131+ container.appendChild(div);
3232+3333+ // Multiple root nodes actually work because DOMParser wraps them in body
3434+ // This test just verifies the parsing works
3535+ const reference = "<div>Content</div>";
3636+ morph(div, reference);
3737+ expect(div.textContent).toBe("Content");
3838+ });
3939+4040+ it("should throw error when morphInner called with non-matching elements", () => {
4141+ const div = document.createElement("div");
4242+ const span = document.createElement("span");
4343+ container.appendChild(div);
4444+4545+ expect(() => {
4646+ morphInner(div, span);
4747+ }).toThrow("[Morphlex] You can only do an inner morph with matching elements.");
4848+ });
4949+ });
5050+5151+ describe("ariaBusy handling", () => {
5252+ it("should set and restore ariaBusy on element during morph", () => {
5353+ const div = document.createElement("div");
5454+ div.ariaBusy = "false";
5555+ const span = document.createElement("span");
5656+ div.appendChild(span);
5757+5858+ const reference = document.createElement("div");
5959+ const refSpan = document.createElement("span");
6060+ refSpan.textContent = "Updated";
6161+ reference.appendChild(refSpan);
6262+6363+ let ariaBusyDuringMorph: string | null = null;
6464+ morph(div, reference, {
6565+ afterNodeMorphed: (node) => {
6666+ if (node === span) {
6767+ ariaBusyDuringMorph = div.ariaBusy;
6868+ }
6969+ },
7070+ });
7171+7272+ // ariaBusy should be set to "true" during morph and restored after
7373+ expect(ariaBusyDuringMorph).toBe("true");
7474+ expect(div.ariaBusy).toBe("false");
7575+ });
7676+7777+ it("should handle ariaBusy for non-element nodes", () => {
7878+ const parent = document.createElement("div");
7979+ const textNode = document.createTextNode("Original");
8080+ parent.appendChild(textNode);
8181+8282+ const referenceParent = document.createElement("div");
8383+ const refTextNode = document.createTextNode("Updated");
8484+ referenceParent.appendChild(refTextNode);
8585+8686+ morph(parent, referenceParent);
8787+8888+ expect(parent.textContent).toBe("Updated");
8989+ });
9090+ });
9191+9292+ describe("Sensitivity mapping for media elements", () => {
9393+ it("should handle media elements with various states", () => {
9494+ const parent = document.createElement("div");
9595+ const video = document.createElement("video");
9696+ video.id = "video1";
9797+ parent.appendChild(video);
9898+9999+ const reference = document.createElement("div");
100100+ const refVideo = document.createElement("video");
101101+ refVideo.id = "video1";
102102+ refVideo.setAttribute("src", "test.mp4");
103103+ reference.appendChild(refVideo);
104104+105105+ morph(parent, reference);
106106+107107+ expect(parent.querySelector("video")).toBeTruthy();
108108+ });
109109+110110+ it("should handle audio elements", () => {
111111+ const parent = document.createElement("div");
112112+ const audio = document.createElement("audio");
113113+ audio.id = "audio1";
114114+ parent.appendChild(audio);
115115+116116+ const reference = document.createElement("div");
117117+ const refAudio = document.createElement("audio");
118118+ refAudio.id = "audio1";
119119+ refAudio.setAttribute("src", "test.mp3");
120120+ reference.appendChild(refAudio);
121121+122122+ morph(parent, reference);
123123+124124+ expect(parent.querySelector("audio")).toBeTruthy();
125125+ });
126126+127127+ it("should handle canvas elements", () => {
128128+ const parent = document.createElement("div");
129129+ const canvas = document.createElement("canvas");
130130+ canvas.id = "canvas1";
131131+ parent.appendChild(canvas);
132132+133133+ const reference = document.createElement("div");
134134+ const refCanvas = document.createElement("canvas");
135135+ refCanvas.id = "canvas1";
136136+ refCanvas.width = 800;
137137+ reference.appendChild(refCanvas);
138138+139139+ morph(parent, reference);
140140+141141+ expect(parent.querySelector("canvas")).toBeTruthy();
142142+ });
143143+144144+ it("should handle embed elements", () => {
145145+ const parent = document.createElement("div");
146146+ const embed = document.createElement("embed");
147147+ embed.id = "embed1";
148148+ parent.appendChild(embed);
149149+150150+ const reference = document.createElement("div");
151151+ const refEmbed = document.createElement("embed");
152152+ refEmbed.id = "embed1";
153153+ refEmbed.setAttribute("src", "test.pdf");
154154+ reference.appendChild(refEmbed);
155155+156156+ morph(parent, reference);
157157+158158+ expect(parent.querySelector("embed")).toBeTruthy();
159159+ });
160160+161161+ it("should handle iframe elements", () => {
162162+ const parent = document.createElement("div");
163163+ const iframe = document.createElement("iframe");
164164+ iframe.id = "iframe1";
165165+ parent.appendChild(iframe);
166166+167167+ const reference = document.createElement("div");
168168+ const refIframe = document.createElement("iframe");
169169+ refIframe.id = "iframe1";
170170+ refIframe.setAttribute("src", "test.html");
171171+ reference.appendChild(refIframe);
172172+173173+ morph(parent, reference);
174174+175175+ expect(parent.querySelector("iframe")).toBeTruthy();
176176+ });
177177+178178+ it("should handle object elements", () => {
179179+ const parent = document.createElement("div");
180180+ const object = document.createElement("object");
181181+ object.id = "object1";
182182+ parent.appendChild(object);
183183+184184+ const reference = document.createElement("div");
185185+ const refObject = document.createElement("object");
186186+ refObject.id = "object1";
187187+ refObject.setAttribute("data", "test.pdf");
188188+ reference.appendChild(refObject);
189189+190190+ morph(parent, reference);
191191+192192+ expect(parent.querySelector("object")).toBeTruthy();
193193+ });
194194+195195+ it("should handle input as active element", () => {
196196+ const parent = document.createElement("div");
197197+ container.appendChild(parent);
198198+199199+ const input = document.createElement("input");
200200+ input.id = "input1";
201201+ input.value = "test";
202202+ parent.appendChild(input);
203203+204204+ // Focus the input to make it active
205205+ input.focus();
206206+207207+ const reference = document.createElement("div");
208208+ const refInput = document.createElement("input");
209209+ refInput.id = "input1";
210210+ refInput.value = "updated";
211211+ reference.appendChild(refInput);
212212+213213+ morph(parent, reference, { ignoreActiveValue: true });
214214+215215+ // Value should be preserved because input is active
216216+ expect(input.value).toBe("test");
217217+ });
218218+219219+ it("should handle textarea as active element", () => {
220220+ const parent = document.createElement("div");
221221+ container.appendChild(parent);
222222+223223+ const textarea = document.createElement("textarea");
224224+ textarea.id = "textarea1";
225225+ textarea.value = "original";
226226+ parent.appendChild(textarea);
227227+228228+ textarea.focus();
229229+230230+ const reference = document.createElement("div");
231231+ const refTextarea = document.createElement("textarea");
232232+ refTextarea.id = "textarea1";
233233+ refTextarea.value = "updated";
234234+ reference.appendChild(refTextarea);
235235+236236+ morph(parent, reference, { ignoreActiveValue: true });
237237+238238+ expect(textarea.value).toBe("original");
239239+ });
240240+ });
241241+242242+ describe("Property updates", () => {
243243+ it("should update option selected property", () => {
244244+ const select = document.createElement("select");
245245+ const option1 = document.createElement("option");
246246+ option1.value = "1";
247247+ option1.textContent = "Option 1";
248248+ const option2 = document.createElement("option");
249249+ option2.value = "2";
250250+ option2.textContent = "Option 2";
251251+ select.appendChild(option1);
252252+ select.appendChild(option2);
253253+254254+ const refSelect = document.createElement("select");
255255+ const refOption1 = document.createElement("option");
256256+ refOption1.value = "1";
257257+ refOption1.textContent = "Option 1";
258258+ const refOption2 = document.createElement("option");
259259+ refOption2.value = "2";
260260+ refOption2.textContent = "Option 2";
261261+ refOption2.selected = true;
262262+ refSelect.appendChild(refOption1);
263263+ refSelect.appendChild(refOption2);
264264+265265+ morph(select, refSelect);
266266+267267+ expect(option2.selected).toBe(true);
268268+ });
269269+270270+ it("should update textarea value with firstElementChild", () => {
271271+ const parent = document.createElement("div");
272272+ const textarea = document.createElement("textarea");
273273+ textarea.name = "myTextarea";
274274+ textarea.value = "original";
275275+ textarea.defaultValue = "original";
276276+ const textContent = document.createElement("span");
277277+ textContent.textContent = "original";
278278+ textarea.appendChild(textContent);
279279+ parent.appendChild(textarea);
280280+281281+ const reference = document.createElement("div");
282282+ const refTextarea = document.createElement("textarea");
283283+ refTextarea.name = "myTextarea";
284284+ refTextarea.value = "updated";
285285+ reference.appendChild(refTextarea);
286286+287287+ morph(parent, reference);
288288+289289+ expect(textarea.value).toBe("updated");
290290+ if (textarea.firstElementChild) {
291291+ expect(textarea.firstElementChild.textContent).toBe("updated");
292292+ }
293293+ });
294294+295295+ it("should preserve modified textarea value with preserveModifiedValues", () => {
296296+ const parent = document.createElement("div");
297297+ const textarea = document.createElement("textarea");
298298+ textarea.name = "myTextarea";
299299+ textarea.defaultValue = "default";
300300+ textarea.value = "modified";
301301+ parent.appendChild(textarea);
302302+303303+ const reference = document.createElement("div");
304304+ const refTextarea = document.createElement("textarea");
305305+ refTextarea.name = "myTextarea";
306306+ refTextarea.value = "new value";
307307+ reference.appendChild(refTextarea);
308308+309309+ morph(parent, reference, { preserveModifiedValues: true });
310310+311311+ expect(textarea.value).toBe("modified");
312312+ });
313313+314314+ it("should update input indeterminate property", () => {
315315+ const parent = document.createElement("div");
316316+ const input = document.createElement("input");
317317+ input.type = "checkbox";
318318+ input.indeterminate = false;
319319+ parent.appendChild(input);
320320+321321+ const reference = document.createElement("div");
322322+ const refInput = document.createElement("input");
323323+ refInput.type = "checkbox";
324324+ refInput.indeterminate = true;
325325+ reference.appendChild(refInput);
326326+327327+ morph(parent, reference);
328328+329329+ expect(input.indeterminate).toBe(true);
330330+ });
331331+332332+ it("should update input disabled property", () => {
333333+ const parent = document.createElement("div");
334334+ const input = document.createElement("input");
335335+ input.disabled = false;
336336+ parent.appendChild(input);
337337+338338+ const reference = document.createElement("div");
339339+ const refInput = document.createElement("input");
340340+ refInput.disabled = true;
341341+ reference.appendChild(refInput);
342342+343343+ morph(parent, reference);
344344+345345+ expect(input.disabled).toBe(true);
346346+ });
347347+348348+ it("should not update file input value", () => {
349349+ const parent = document.createElement("div");
350350+ const input = document.createElement("input");
351351+ input.type = "file";
352352+ parent.appendChild(input);
353353+354354+ const reference = document.createElement("div");
355355+ const refInput = document.createElement("input");
356356+ refInput.type = "file";
357357+ reference.appendChild(refInput);
358358+359359+ morph(parent, reference);
360360+361361+ expect(input.type).toBe("file");
362362+ });
363363+ });
364364+365365+ describe("Head element special handling", () => {
366366+ it("should handle nested head elements in child morphing", () => {
367367+ const parent = document.createElement("div");
368368+ const head = document.createElement("head");
369369+ const meta1 = document.createElement("meta");
370370+ meta1.setAttribute("name", "test");
371371+ meta1.setAttribute("content", "value");
372372+ head.appendChild(meta1);
373373+ parent.appendChild(head);
374374+375375+ const reference = document.createElement("div");
376376+ const refHead = document.createElement("head");
377377+ const refMeta = document.createElement("meta");
378378+ refMeta.setAttribute("name", "test");
379379+ refMeta.setAttribute("content", "updated");
380380+ refHead.appendChild(refMeta);
381381+ reference.appendChild(refHead);
382382+383383+ morph(parent, reference);
384384+385385+ expect(parent.querySelector("head")).toBeTruthy();
386386+ });
387387+ });
388388+389389+ describe("Child element morphing edge cases", () => {
390390+ it("should handle ID matching with overlapping ID sets", () => {
391391+ const parent = document.createElement("div");
392392+ const child1 = document.createElement("div");
393393+ child1.id = "child1";
394394+ const nested = document.createElement("span");
395395+ nested.id = "nested";
396396+ child1.appendChild(nested);
397397+398398+ const child2 = document.createElement("div");
399399+ child2.id = "child2";
400400+401401+ parent.appendChild(child1);
402402+ parent.appendChild(child2);
403403+404404+ const reference = document.createElement("div");
405405+ const refChild1 = document.createElement("div");
406406+ refChild1.id = "child1";
407407+ const refNested = document.createElement("span");
408408+ refNested.id = "different";
409409+ refChild1.appendChild(refNested);
410410+411411+ const refChild2 = document.createElement("div");
412412+ refChild2.id = "child2";
413413+414414+ reference.appendChild(refChild1);
415415+ reference.appendChild(refChild2);
416416+417417+ morph(parent, reference);
418418+419419+ expect(parent.children[0].id).toBe("child1");
420420+ });
421421+422422+ it("should insert new node when no match found and beforeNodeAdded returns true", () => {
423423+ const parent = document.createElement("div");
424424+ const existing = document.createElement("div");
425425+ existing.id = "existing";
426426+ parent.appendChild(existing);
427427+428428+ const reference = document.createElement("div");
429429+ const refNew = document.createElement("div");
430430+ refNew.id = "new";
431431+ const refExisting = document.createElement("div");
432432+ refExisting.id = "existing";
433433+ reference.appendChild(refNew);
434434+ reference.appendChild(refExisting);
435435+436436+ let addedNode: Node | null = null;
437437+ morph(parent, reference, {
438438+ beforeNodeAdded: (node) => {
439439+ addedNode = node;
440440+ return true;
441441+ },
442442+ });
443443+444444+ expect(addedNode).toBeTruthy();
445445+ expect(parent.children[0].id).toBe("new");
446446+ });
447447+448448+ it("should not insert new node when beforeNodeAdded returns false", () => {
449449+ const parent = document.createElement("div");
450450+ const existing = document.createElement("div");
451451+ existing.id = "existing";
452452+ existing.textContent = "original";
453453+ parent.appendChild(existing);
454454+455455+ const reference = document.createElement("div");
456456+ const refNew = document.createElement("span");
457457+ refNew.id = "new";
458458+ refNew.textContent = "new content";
459459+ const refExisting = document.createElement("div");
460460+ refExisting.id = "existing";
461461+ refExisting.textContent = "updated";
462462+ reference.appendChild(refNew);
463463+ reference.appendChild(refExisting);
464464+465465+ let addCallbackCalled = false;
466466+ morph(parent, reference, {
467467+ beforeNodeAdded: () => {
468468+ addCallbackCalled = true;
469469+ return false;
470470+ },
471471+ });
472472+473473+ // beforeNodeAdded should have been called
474474+ expect(addCallbackCalled).toBe(true);
475475+ // The existing div will be morphed to match reference
476476+ expect(parent.children[0].tagName).toBe("DIV");
477477+ });
478478+479479+ it("should call afterNodeMorphed for child elements even when new node inserted", () => {
480480+ const parent = document.createElement("div");
481481+ const child = document.createElement("div");
482482+ child.id = "child";
483483+ parent.appendChild(child);
484484+485485+ const reference = document.createElement("div");
486486+ const refChild = document.createElement("span");
487487+ refChild.id = "newChild";
488488+ reference.appendChild(refChild);
489489+490490+ let morphedCalled = false;
491491+ morph(parent, reference, {
492492+ afterNodeMorphed: () => {
493493+ morphedCalled = true;
494494+ },
495495+ });
496496+497497+ expect(morphedCalled).toBe(true);
498498+ });
499499+ });
500500+501501+ describe("Sensitivity-based insertBefore", () => {
502502+ it("should handle sensitivity reordering when previousNode is insertionPoint", () => {
503503+ const parent = document.createElement("div");
504504+ container.appendChild(parent);
505505+506506+ const input1 = document.createElement("input");
507507+ input1.id = "input1";
508508+ input1.value = "test";
509509+ input1.defaultValue = "";
510510+511511+ const input2 = document.createElement("input");
512512+ input2.id = "input2";
513513+ input2.value = "test2";
514514+ input2.defaultValue = "";
515515+516516+ parent.appendChild(input1);
517517+ parent.appendChild(input2);
518518+519519+ const reference = document.createElement("div");
520520+ const refInput2 = document.createElement("input");
521521+ refInput2.id = "input2";
522522+ refInput2.value = "test2";
523523+524524+ const refInput1 = document.createElement("input");
525525+ refInput1.id = "input1";
526526+ refInput1.value = "test";
527527+528528+ reference.appendChild(refInput2);
529529+ reference.appendChild(refInput1);
530530+531531+ morph(parent, reference);
532532+533533+ expect(parent.children[0].id).toBe("input2");
534534+ expect(parent.children[1].id).toBe("input1");
535535+ });
536536+537537+ it("should break sensitivity reordering loop when previousNodeSensitivity >= sensitivity", () => {
538538+ const parent = document.createElement("div");
539539+ container.appendChild(parent);
540540+541541+ // Create inputs with different sensitivity levels
542542+ const input1 = document.createElement("input");
543543+ input1.id = "input1";
544544+ input1.value = "modified1";
545545+ input1.defaultValue = "default1";
546546+547547+ const input2 = document.createElement("input");
548548+ input2.id = "input2";
549549+ input2.value = "modified2";
550550+ input2.defaultValue = "default2";
551551+552552+ const input3 = document.createElement("input");
553553+ input3.id = "input3";
554554+ input3.value = "modified3";
555555+ input3.defaultValue = "default3";
556556+557557+ parent.appendChild(input1);
558558+ parent.appendChild(input2);
559559+ parent.appendChild(input3);
560560+561561+ const reference = document.createElement("div");
562562+ const refInput3 = document.createElement("input");
563563+ refInput3.id = "input3";
564564+565565+ const refInput1 = document.createElement("input");
566566+ refInput1.id = "input1";
567567+568568+ const refInput2 = document.createElement("input");
569569+ refInput2.id = "input2";
570570+571571+ reference.appendChild(refInput3);
572572+ reference.appendChild(refInput1);
573573+ reference.appendChild(refInput2);
574574+575575+ morph(parent, reference);
576576+577577+ // Verify elements were reordered
578578+ expect(parent.children.length).toBe(3);
579579+ });
580580+581581+ it("should handle insertBefore when node is not an element", () => {
582582+ const parent = document.createElement("div");
583583+ const text1 = document.createTextNode("First");
584584+ const text2 = document.createTextNode("Second");
585585+ parent.appendChild(text1);
586586+ parent.appendChild(text2);
587587+588588+ const reference = document.createElement("div");
589589+ const refText1 = document.createTextNode("First Updated");
590590+ const refText2 = document.createTextNode("Second");
591591+ reference.appendChild(refText1);
592592+ reference.appendChild(refText2);
593593+594594+ morph(parent, reference);
595595+596596+ expect(parent.textContent).toBe("First UpdatedSecond");
597597+ });
598598+599599+ it("should handle insertBefore with zero sensitivity", () => {
600600+ const parent = document.createElement("div");
601601+ const div1 = document.createElement("div");
602602+ div1.id = "div1";
603603+ const div2 = document.createElement("div");
604604+ div2.id = "div2";
605605+606606+ parent.appendChild(div1);
607607+ parent.appendChild(div2);
608608+609609+ const reference = document.createElement("div");
610610+ const refDiv2 = document.createElement("div");
611611+ refDiv2.id = "div2";
612612+ const refDiv1 = document.createElement("div");
613613+ refDiv1.id = "div1";
614614+615615+ reference.appendChild(refDiv2);
616616+ reference.appendChild(refDiv1);
617617+618618+ morph(parent, reference);
619619+620620+ expect(parent.children[0].id).toBe("div2");
621621+ expect(parent.children[1].id).toBe("div1");
622622+ });
623623+ });
624624+625625+ describe("Callback cancellation", () => {
626626+ it("should call beforeAttributeUpdated and cancel attribute removal when it returns false", () => {
627627+ const div = document.createElement("div");
628628+ div.setAttribute("data-keep", "value");
629629+ div.setAttribute("data-remove", "value");
630630+631631+ const reference = document.createElement("div");
632632+ reference.setAttribute("data-keep", "value");
633633+634634+ morph(div, reference, {
635635+ beforeAttributeUpdated: (element, name, value) => {
636636+ if (name === "data-remove" && value === null) {
637637+ return false; // Cancel removal
638638+ }
639639+ return true;
640640+ },
641641+ });
642642+643643+ // Attribute should still be there because callback returned false
644644+ expect(div.hasAttribute("data-remove")).toBe(true);
645645+ });
646646+647647+ it("should call beforePropertyUpdated and cancel property update when it returns false", () => {
648648+ const input = document.createElement("input");
649649+ input.checked = false;
650650+651651+ const reference = document.createElement("input");
652652+ reference.checked = true;
653653+654654+ morph(input, reference, {
655655+ beforePropertyUpdated: (node, propertyName, newValue) => {
656656+ if (propertyName === "checked" && newValue === true) {
657657+ return false; // Cancel update
658658+ }
659659+ return true;
660660+ },
661661+ });
662662+663663+ // Property should not be updated because callback returned false
664664+ expect(input.checked).toBe(false);
665665+ });
666666+ });
667667+668668+ describe("Empty ID handling", () => {
669669+ it("should ignore elements with empty id attribute", () => {
670670+ const parent = document.createElement("div");
671671+ const child1 = document.createElement("div");
672672+ child1.setAttribute("id", ""); // Empty ID
673673+ child1.textContent = "First";
674674+675675+ const child2 = document.createElement("div");
676676+ child2.id = "valid-id";
677677+ child2.textContent = "Second";
678678+679679+ parent.appendChild(child1);
680680+ parent.appendChild(child2);
681681+682682+ const reference = document.createElement("div");
683683+ const refChild1 = document.createElement("div");
684684+ refChild1.setAttribute("id", "");
685685+ refChild1.textContent = "First Updated";
686686+687687+ const refChild2 = document.createElement("div");
688688+ refChild2.id = "valid-id";
689689+ refChild2.textContent = "Second Updated";
690690+691691+ reference.appendChild(refChild1);
692692+ reference.appendChild(refChild2);
693693+694694+ morph(parent, reference);
695695+696696+ expect(child1.textContent).toBe("First Updated");
697697+ expect(child2.textContent).toBe("Second Updated");
698698+ });
699699+ });
700700+701701+ describe("Complex morphing scenarios", () => {
702702+ it("should handle mixed content with sensitive and non-sensitive elements", () => {
703703+ const parent = document.createElement("div");
704704+ container.appendChild(parent);
705705+706706+ const div = document.createElement("div");
707707+ div.id = "div1";
708708+709709+ const input = document.createElement("input");
710710+ input.id = "input1";
711711+ input.value = "test";
712712+ input.defaultValue = "";
713713+714714+ const canvas = document.createElement("canvas");
715715+ canvas.id = "canvas1";
716716+717717+ parent.appendChild(div);
718718+ parent.appendChild(input);
719719+ parent.appendChild(canvas);
720720+721721+ const reference = document.createElement("div");
722722+723723+ const refCanvas = document.createElement("canvas");
724724+ refCanvas.id = "canvas1";
725725+726726+ const refInput = document.createElement("input");
727727+ refInput.id = "input1";
728728+729729+ const refDiv = document.createElement("div");
730730+ refDiv.id = "div1";
731731+732732+ reference.appendChild(refCanvas);
733733+ reference.appendChild(refInput);
734734+ reference.appendChild(refDiv);
735735+736736+ morph(parent, reference);
737737+738738+ expect(parent.children.length).toBe(3);
739739+ });
740740+741741+ it("should handle text node morph with ariaBusy (non-element)", () => {
742742+ // Test line 136 - else block() for non-element nodes
743743+ const parent = document.createElement("div");
744744+ const textNode = document.createTextNode("Original");
745745+ parent.appendChild(textNode);
746746+747747+ const reference = document.createTextNode("Updated");
748748+749749+ morph(textNode, reference);
750750+751751+ expect(textNode.nodeValue).toBe("Updated");
752752+ });
753753+754754+ it("should handle media elements that are playing", () => {
755755+ // Test lines 159-163 - media sensitivity with playing state
756756+ const parent = document.createElement("div");
757757+ container.appendChild(parent);
758758+759759+ const video = document.createElement("video");
760760+ video.id = "video1";
761761+ // Mock playing state
762762+ Object.defineProperty(video, "ended", { value: false, writable: true });
763763+ Object.defineProperty(video, "paused", { value: false, writable: true });
764764+ Object.defineProperty(video, "currentTime", { value: 5.0, writable: true });
765765+ parent.appendChild(video);
766766+767767+ const reference = document.createElement("div");
768768+ const refVideo = document.createElement("video");
769769+ refVideo.id = "video1";
770770+ reference.appendChild(refVideo);
771771+772772+ morph(parent, reference);
773773+774774+ expect(parent.querySelector("video")).toBeTruthy();
775775+ });
776776+777777+ it("should match elements by overlapping ID sets", () => {
778778+ // Test lines 372-373 - matching by overlapping ID sets
779779+ const parent = document.createElement("div");
780780+781781+ const outer1 = document.createElement("div");
782782+ outer1.id = "outer1";
783783+ const inner1a = document.createElement("span");
784784+ inner1a.id = "inner1a";
785785+ const inner1b = document.createElement("span");
786786+ inner1b.id = "inner1b";
787787+ outer1.appendChild(inner1a);
788788+ outer1.appendChild(inner1b);
789789+790790+ const outer2 = document.createElement("div");
791791+ outer2.id = "outer2";
792792+ const inner2 = document.createElement("span");
793793+ inner2.id = "inner2";
794794+ outer2.appendChild(inner2);
795795+796796+ parent.appendChild(outer1);
797797+ parent.appendChild(outer2);
798798+799799+ const reference = document.createElement("div");
800800+801801+ // Reference wants outer1 to come second, but references an inner ID
802802+ const refOuter2 = document.createElement("div");
803803+ refOuter2.id = "outer2";
804804+ const refInner2 = document.createElement("span");
805805+ refInner2.id = "inner2";
806806+ refOuter2.appendChild(refInner2);
807807+808808+ const refOuter1 = document.createElement("div");
809809+ refOuter1.id = "outer1";
810810+ const refInner1a = document.createElement("span");
811811+ refInner1a.id = "inner1a";
812812+ refOuter1.appendChild(refInner1a);
813813+814814+ reference.appendChild(refOuter2);
815815+ reference.appendChild(refOuter1);
816816+817817+ morph(parent, reference);
818818+819819+ expect(parent.children[0].id).toBe("outer2");
820820+ });
821821+822822+ it("should add completely new element when no match found by tag or ID", () => {
823823+ // Test lines 386-389 - adding new node with callbacks
824824+ const parent = document.createElement("div");
825825+ const existing = document.createElement("div");
826826+ existing.id = "existing";
827827+ parent.appendChild(existing);
828828+829829+ const reference = document.createElement("div");
830830+ const refNew = document.createElement("article");
831831+ refNew.id = "brand-new";
832832+ refNew.textContent = "New content";
833833+ const refExisting = document.createElement("div");
834834+ refExisting.id = "existing";
835835+ reference.appendChild(refNew);
836836+ reference.appendChild(refExisting);
837837+838838+ let addedNode: Node | null = null;
839839+ let afterAddedCalled = false;
840840+ morph(parent, reference, {
841841+ beforeNodeAdded: (node) => {
842842+ addedNode = node;
843843+ return true;
844844+ },
845845+ afterNodeAdded: (node) => {
846846+ afterAddedCalled = true;
847847+ },
848848+ });
849849+850850+ expect(addedNode).toBeTruthy();
851851+ expect(afterAddedCalled).toBe(true);
852852+ expect(parent.children[0].tagName).toBe("ARTICLE");
853853+ });
854854+855855+ it("should continue sensitivity loop when reordering multiple nodes", () => {
856856+ // Test lines 426-429 - continuing the sensitivity reordering loop
857857+ const parent = document.createElement("div");
858858+ container.appendChild(parent);
859859+860860+ // Create a chain of inputs with modified values (high sensitivity)
861861+ const input1 = document.createElement("input");
862862+ input1.id = "input1";
863863+ input1.value = "modified1";
864864+ input1.defaultValue = "default1";
865865+866866+ const input2 = document.createElement("input");
867867+ input2.id = "input2";
868868+ input2.value = "modified2";
869869+ input2.defaultValue = "default2";
870870+871871+ const input3 = document.createElement("input");
872872+ input3.id = "input3";
873873+ input3.value = "modified3";
874874+ input3.defaultValue = "default3";
875875+876876+ const div = document.createElement("div");
877877+ div.id = "div1";
878878+879879+ parent.appendChild(div);
880880+ parent.appendChild(input1);
881881+ parent.appendChild(input2);
882882+ parent.appendChild(input3);
883883+884884+ // Reference wants inputs in different order
885885+ const reference = document.createElement("div");
886886+887887+ const refInput3 = document.createElement("input");
888888+ refInput3.id = "input3";
889889+890890+ const refInput2 = document.createElement("input");
891891+ refInput2.id = "input2";
892892+893893+ const refInput1 = document.createElement("input");
894894+ refInput1.id = "input1";
895895+896896+ const refDiv = document.createElement("div");
897897+ refDiv.id = "div1";
898898+899899+ reference.appendChild(refInput3);
900900+ reference.appendChild(refInput2);
901901+ reference.appendChild(refInput1);
902902+ reference.appendChild(refDiv);
903903+904904+ morph(parent, reference);
905905+906906+ // The inputs should be reordered
907907+ expect(parent.children.length).toBe(4);
908908+ });
909909+910910+ describe("DOMParser edge cases", () => {
911911+ it("should explore parser behavior to trigger line 74", () => {
912912+ // Line 74 checks if doc.childNodes.length === 1
913913+ // This is checking the document's childNodes, not body's childNodes
914914+ // DOMParser always returns a document with html element as child
915915+ // So doc.childNodes.length is always 1 (the html element)
916916+ // The else branch on line 74 appears to be unreachable in normal usage
917917+918918+ // Let's verify with actual morph call
919919+ const parent = document.createElement("div");
920920+ const div = document.createElement("div");
921921+ div.textContent = "Original";
922922+ parent.appendChild(div);
923923+924924+ // This should work fine
925925+ morph(div, "<span>Test</span>");
926926+ expect(parent.firstChild?.textContent).toBe("Test");
927927+ });
928928+ });
929929+930930+ describe("Additional edge cases for remaining coverage", () => {
931931+ it("should handle element matching with nested IDs and no direct ID match", () => {
932932+ // More specific test for lines 372-373
933933+ const parent = document.createElement("div");
934934+935935+ const container1 = document.createElement("section");
936936+ const child1a = document.createElement("div");
937937+ child1a.id = "shared-id-a";
938938+ const child1b = document.createElement("div");
939939+ child1b.id = "shared-id-b";
940940+ container1.appendChild(child1a);
941941+ container1.appendChild(child1b);
942942+943943+ const container2 = document.createElement("section");
944944+ const child2 = document.createElement("div");
945945+ child2.id = "other-id";
946946+ container2.appendChild(child2);
947947+948948+ parent.appendChild(container1);
949949+ parent.appendChild(container2);
950950+951951+ const reference = document.createElement("div");
952952+ const refContainer = document.createElement("section");
953953+ const refChild = document.createElement("div");
954954+ refChild.id = "shared-id-a";
955955+ refContainer.appendChild(refChild);
956956+957957+ reference.appendChild(refContainer);
958958+959959+ morph(parent, reference);
960960+961961+ expect(parent.children.length).toBeGreaterThanOrEqual(1);
962962+ });
963963+964964+ it("should insert node before when no ID or tag match exists", () => {
965965+ // Test for lines 386-389 with different scenario
966966+ const parent = document.createElement("div");
967967+ const oldChild = document.createElement("p");
968968+ oldChild.textContent = "Old";
969969+ parent.appendChild(oldChild);
970970+971971+ const reference = document.createElement("div");
972972+ const newChild = document.createElement("article");
973973+ newChild.textContent = "New";
974974+ reference.appendChild(newChild);
975975+976976+ let beforeAddCalled = false;
977977+ let afterAddCalled = false;
978978+979979+ morph(parent, reference, {
980980+ beforeNodeAdded: (node) => {
981981+ beforeAddCalled = true;
982982+ return true;
983983+ },
984984+ afterNodeAdded: (node) => {
985985+ afterAddCalled = true;
986986+ },
987987+ });
988988+989989+ expect(beforeAddCalled).toBe(true);
990990+ expect(afterAddCalled).toBe(true);
991991+ });
992992+993993+ it("should handle multiple previousNode reorderings in sensitivity loop", () => {
994994+ // Test for lines 426-429 with more complex scenario
995995+ const parent = document.createElement("div");
996996+ container.appendChild(parent);
997997+998998+ const regularDiv = document.createElement("div");
999999+ regularDiv.id = "regular";
10001000+10011001+ const sensitiveInput1 = document.createElement("input");
10021002+ sensitiveInput1.id = "sensitive1";
10031003+ sensitiveInput1.value = "changed";
10041004+ sensitiveInput1.defaultValue = "default";
10051005+10061006+ const sensitiveInput2 = document.createElement("input");
10071007+ sensitiveInput2.id = "sensitive2";
10081008+ sensitiveInput2.value = "changed2";
10091009+ sensitiveInput2.defaultValue = "default2";
10101010+10111011+ const sensitiveInput3 = document.createElement("input");
10121012+ sensitiveInput3.id = "sensitive3";
10131013+ sensitiveInput3.value = "changed3";
10141014+ sensitiveInput3.defaultValue = "default3";
10151015+10161016+ parent.appendChild(regularDiv);
10171017+ parent.appendChild(sensitiveInput1);
10181018+ parent.appendChild(sensitiveInput2);
10191019+ parent.appendChild(sensitiveInput3);
10201020+10211021+ const reference = document.createElement("div");
10221022+10231023+ const refInput3 = document.createElement("input");
10241024+ refInput3.id = "sensitive3";
10251025+10261026+ const refInput2 = document.createElement("input");
10271027+ refInput2.id = "sensitive2";
10281028+10291029+ const refInput1 = document.createElement("input");
10301030+ refInput1.id = "sensitive1";
10311031+10321032+ const refDiv = document.createElement("div");
10331033+ refDiv.id = "regular";
10341034+10351035+ reference.appendChild(refInput3);
10361036+ reference.appendChild(refInput2);
10371037+ reference.appendChild(refInput1);
10381038+ reference.appendChild(refDiv);
10391039+10401040+ morph(parent, reference);
10411041+10421042+ expect(parent.children.length).toBe(4);
10431043+ });
10441044+10451045+ it("should match by overlapping ID sets in sibling scan - lines 372-373", () => {
10461046+ // Lines 372-373: Match element by overlapping ID sets when ID doesn't match
10471047+ // This requires: currentNode has ID != reference.id, but has nested IDs that overlap
10481048+ const parent = document.createElement("div");
10491049+10501050+ // First child with nested IDs
10511051+ const div1 = document.createElement("div");
10521052+ div1.id = "div1";
10531053+ const nested1 = document.createElement("span");
10541054+ nested1.id = "overlap-id";
10551055+ div1.appendChild(nested1);
10561056+10571057+ // Second child that's a match by tag name
10581058+ const div2 = document.createElement("div");
10591059+ div2.id = "div2";
10601060+10611061+ parent.appendChild(div1);
10621062+ parent.appendChild(div2);
10631063+10641064+ // Reference wants div2 first, but references the overlap-id
10651065+ const reference = document.createElement("div");
10661066+ const refDiv = document.createElement("div");
10671067+ refDiv.id = "target";
10681068+ const refNested = document.createElement("span");
10691069+ refNested.id = "overlap-id"; // This ID exists nested in div1
10701070+ refDiv.appendChild(refNested);
10711071+ reference.appendChild(refDiv);
10721072+10731073+ morph(parent, reference);
10741074+10751075+ expect(parent.children.length).toBeGreaterThanOrEqual(1);
10761076+ });
10771077+10781078+ it("should add new node when no tag match exists - lines 386-389", () => {
10791079+ // Lines 386-389: No nextMatchByTagName, so add new node
10801080+ // This requires the reference child has a tag that doesn't exist in current children
10811081+ const parent = document.createElement("div");
10821082+ const p = document.createElement("p");
10831083+ p.textContent = "Paragraph";
10841084+ parent.appendChild(p);
10851085+10861086+ const reference = document.createElement("div");
10871087+ // Use article tag which doesn't exist in parent
10881088+ const article = document.createElement("article");
10891089+ article.textContent = "Article";
10901090+ const refP = document.createElement("p");
10911091+ reference.appendChild(article);
10921092+ reference.appendChild(refP);
10931093+10941094+ let addedNode: Node | null = null;
10951095+ morph(parent, reference, {
10961096+ beforeNodeAdded: (node) => {
10971097+ addedNode = node;
10981098+ return true;
10991099+ },
11001100+ afterNodeAdded: (node) => {
11011101+ // Lines 388-389
11021102+ },
11031103+ });
11041104+11051105+ expect(addedNode).toBeTruthy();
11061106+ expect(parent.querySelector("article")).toBeTruthy();
11071107+ });
11081108+11091109+ it("should trigger line 74 - unreachable error path in parseChildNodeFromString", () => {
11101110+ // Line 74: else throw new Error("[Morphlex] The string was not a valid HTML node.");
11111111+ // This line is actually unreachable because DOMParser always returns doc with childNodes.length === 1
11121112+ // However, we can document this as a known unreachable path
11131113+ // The parser always creates: doc -> html -> (head + body)
11141114+ // So doc.childNodes.length is always 1 (the html element)
11151115+11161116+ // All valid HTML strings will pass the check on line 72
11171117+ const parent = document.createElement("div");
11181118+ const child = document.createElement("div");
11191119+ parent.appendChild(child);
11201120+11211121+ // Even empty string parses to valid document structure
11221122+ morph(child, "<p>Test</p>");
11231123+ expect(parent.querySelector("p")?.textContent).toBe("Test");
11241124+ });
11251125+11261126+ it("should continue sensitivity reordering loop - lines 426-429", () => {
11271127+ // Lines 426-429: previousNode reordering continues until break condition
11281128+ const parent = document.createElement("div");
11291129+ container.appendChild(parent);
11301130+11311131+ // Create low sensitivity element
11321132+ const lowSensDiv = document.createElement("div");
11331133+ lowSensDiv.id = "low";
11341134+11351135+ // Create multiple high sensitivity elements that will trigger reordering
11361136+ const highSens1 = document.createElement("input");
11371137+ highSens1.id = "high1";
11381138+ highSens1.value = "modified";
11391139+ highSens1.defaultValue = "default";
11401140+11411141+ const highSens2 = document.createElement("input");
11421142+ highSens2.id = "high2";
11431143+ highSens2.value = "modified";
11441144+ highSens2.defaultValue = "default";
11451145+11461146+ const highSens3 = document.createElement("input");
11471147+ highSens3.id = "high3";
11481148+ highSens3.value = "modified";
11491149+ highSens3.defaultValue = "default";
11501150+11511151+ // Low sensitivity first, then high sensitivity elements
11521152+ parent.appendChild(lowSensDiv);
11531153+ parent.appendChild(highSens1);
11541154+ parent.appendChild(highSens2);
11551155+ parent.appendChild(highSens3);
11561156+11571157+ // Reference wants high sensitivity elements first
11581158+ const reference = document.createElement("div");
11591159+ const refHigh3 = document.createElement("input");
11601160+ refHigh3.id = "high3";
11611161+ const refHigh2 = document.createElement("input");
11621162+ refHigh2.id = "high2";
11631163+ const refHigh1 = document.createElement("input");
11641164+ refHigh1.id = "high1";
11651165+ const refLow = document.createElement("div");
11661166+ refLow.id = "low";
11671167+11681168+ reference.appendChild(refHigh3);
11691169+ reference.appendChild(refHigh2);
11701170+ reference.appendChild(refHigh1);
11711171+ reference.appendChild(refLow);
11721172+11731173+ morph(parent, reference);
11741174+11751175+ // Verify reordering happened
11761176+ expect(parent.children.length).toBe(4);
11771177+ // The loop should have moved multiple previousNodes
11781178+ });
11791179+11801180+ it("should handle case where child exists but refChild doesn't in loop - lines 332-333", () => {
11811181+ // Lines 332-333 are actually unreachable in the for loop
11821182+ // because we iterate up to refChildNodes.length, so refChild will always exist
11831183+ // The cleanup happens in the while loop below (lines 338-341)
11841184+ // This test documents that lines 332-333 appear to be dead code
11851185+ const parent = document.createElement("div");
11861186+ const child1 = document.createElement("span");
11871187+ child1.textContent = "Keep";
11881188+ const child2 = document.createElement("span");
11891189+ child2.textContent = "Remove via while loop";
11901190+ parent.appendChild(child1);
11911191+ parent.appendChild(child2);
11921192+11931193+ const reference = document.createElement("div");
11941194+ const refChild = document.createElement("span");
11951195+ refChild.textContent = "Keep Updated";
11961196+ reference.appendChild(refChild);
11971197+11981198+ morph(parent, reference);
11991199+12001200+ expect(parent.children.length).toBe(1);
12011201+ });
12021202+12031203+ it("should add new node when no ID or tag match exists - lines 386-389", () => {
12041204+ // Lines 386-389 require: no nextMatchByTagName AND beforeNodeAdded returns true
12051205+ // This means the first child must not match by tag, and no sibling matches either
12061206+ const parent = document.createElement("div");
12071207+ // Use a tag that won't match
12081208+ const article = document.createElement("article");
12091209+ article.id = "article1";
12101210+ article.textContent = "Article";
12111211+ parent.appendChild(article);
12121212+12131213+ const reference = document.createElement("div");
12141214+ // First child is a section (different tag), and has no ID match in siblings
12151215+ const section = document.createElement("section");
12161216+ section.id = "section1";
12171217+ section.textContent = "Section";
12181218+ // Second child to trigger morphChildElement for the article
12191219+ const refArticle = document.createElement("article");
12201220+ refArticle.id = "article1";
12211221+ reference.appendChild(section);
12221222+ reference.appendChild(refArticle);
12231223+12241224+ let beforeCalled = false;
12251225+ let afterCalled = false;
12261226+12271227+ morph(parent, reference, {
12281228+ beforeNodeAdded: (node) => {
12291229+ beforeCalled = true;
12301230+ return true; // Line 387: insertBefore is called
12311231+ },
12321232+ afterNodeAdded: (node) => {
12331233+ afterCalled = true; // Line 389
12341234+ },
12351235+ });
12361236+12371237+ expect(beforeCalled).toBe(true);
12381238+ expect(afterCalled).toBe(true);
12391239+ });
12401240+12411241+ it("should continue while loop in sensitivity reordering - lines 426-429", () => {
12421242+ // Lines 426-429: while loop continues, previousNode gets reassigned
12431243+ // Need multiple low-sensitivity nodes before a high-sensitivity node
12441244+ const parent = document.createElement("div");
12451245+ container.appendChild(parent);
12461246+12471247+ // Multiple regular divs (low sensitivity)
12481248+ const div1 = document.createElement("div");
12491249+ div1.id = "div1";
12501250+ const div2 = document.createElement("div");
12511251+ div2.id = "div2";
12521252+ const div3 = document.createElement("div");
12531253+ div3.id = "div3";
12541254+12551255+ // High sensitivity input at the end
12561256+ const input = document.createElement("input");
12571257+ input.id = "input1";
12581258+ input.value = "modified";
12591259+ input.defaultValue = "default";
12601260+12611261+ parent.appendChild(div1);
12621262+ parent.appendChild(div2);
12631263+ parent.appendChild(div3);
12641264+ parent.appendChild(input);
12651265+12661266+ // Reference wants input first - this will trigger insertBefore with sensitivity reordering
12671267+ const reference = document.createElement("div");
12681268+ const refInput = document.createElement("input");
12691269+ refInput.id = "input1";
12701270+ const refDiv1 = document.createElement("div");
12711271+ refDiv1.id = "div1";
12721272+ const refDiv2 = document.createElement("div");
12731273+ refDiv2.id = "div2";
12741274+ const refDiv3 = document.createElement("div");
12751275+ refDiv3.id = "div3";
12761276+12771277+ reference.appendChild(refInput);
12781278+ reference.appendChild(refDiv1);
12791279+ reference.appendChild(refDiv2);
12801280+ reference.appendChild(refDiv3);
12811281+12821282+ morph(parent, reference);
12831283+12841284+ // Input should be moved to front, with divs following
12851285+ expect(parent.children[0].id).toBe("input1");
12861286+ });
12871287+12881288+ describe("Exact uncovered line tests", () => {
12891289+ it("should cancel morphing with beforeNodeMorphed returning false in morphChildElement - line 300", () => {
12901290+ // Line 300: return early when beforeNodeMorphed returns false in morphChildElement
12911291+ const parent = document.createElement("div");
12921292+ const child = document.createElement("div");
12931293+ child.id = "child";
12941294+ parent.appendChild(child);
12951295+12961296+ const reference = document.createElement("div");
12971297+ const refChild = document.createElement("div");
12981298+ refChild.id = "child";
12991299+ refChild.textContent = "updated";
13001300+ reference.appendChild(refChild);
13011301+13021302+ let callbackInvoked = false;
13031303+ morph(parent, reference, {
13041304+ beforeNodeMorphed: (node) => {
13051305+ if (node === child) {
13061306+ callbackInvoked = true;
13071307+ return false; // This triggers line 300 return
13081308+ }
13091309+ return true;
13101310+ },
13111311+ });
13121312+13131313+ expect(callbackInvoked).toBe(true);
13141314+ // Child should not be updated because callback returned false
13151315+ expect(child.textContent).toBe("");
13161316+ });
13171317+13181318+ it("should add completely new element type with no matches - lines 386-389", () => {
13191319+ // Lines 386-389: else branch where no nextMatchByTagName exists
13201320+ // Need a reference child with a tag that doesn't exist anywhere in parent
13211321+ const parent = document.createElement("div");
13221322+ const p = document.createElement("p");
13231323+ p.textContent = "paragraph";
13241324+ parent.appendChild(p);
13251325+13261326+ const reference = document.createElement("div");
13271327+ // Use custom element or uncommon tag
13281328+ const custom = document.createElement("custom-element");
13291329+ custom.textContent = "custom";
13301330+ const refP = document.createElement("p");
13311331+ reference.appendChild(custom);
13321332+ reference.appendChild(refP);
13331333+13341334+ let beforeCalled = false;
13351335+ let afterCalled = false;
13361336+13371337+ morph(parent, reference, {
13381338+ beforeNodeAdded: (node) => {
13391339+ if ((node as Element).tagName === "CUSTOM-ELEMENT") {
13401340+ beforeCalled = true;
13411341+ return true; // Line 387-388
13421342+ }
13431343+ return true;
13441344+ },
13451345+ afterNodeAdded: (node) => {
13461346+ if ((node as Element).tagName === "CUSTOM-ELEMENT") {
13471347+ afterCalled = true; // Line 389
13481348+ }
13491349+ },
13501350+ });
13511351+13521352+ expect(beforeCalled).toBe(true);
13531353+ expect(afterCalled).toBe(true);
13541354+ });
13551355+13561356+ it("should reorder with sensitivity - moving multiple previous nodes - lines 426-429", () => {
13571357+ // Lines 426-429: while loop continues moving previousNode
13581358+ const parent = document.createElement("div");
13591359+ container.appendChild(parent);
13601360+13611361+ // Create a scenario where multiple low-sensitivity nodes need to be moved past a high-sensitivity node
13621362+ const div1 = document.createElement("div");
13631363+ div1.id = "div1";
13641364+ const div2 = document.createElement("div");
13651365+ div2.id = "div2";
13661366+13671367+ // High sensitivity input
13681368+ const input = document.createElement("input");
13691369+ input.id = "input";
13701370+ input.value = "modified";
13711371+ input.defaultValue = "default";
13721372+13731373+ parent.appendChild(div1);
13741374+ parent.appendChild(div2);
13751375+ parent.appendChild(input);
13761376+13771377+ // Reference wants input first - will trigger sensitivity reordering
13781378+ const reference = document.createElement("div");
13791379+ const refInput = document.createElement("input");
13801380+ refInput.id = "input";
13811381+ const refDiv1 = document.createElement("div");
13821382+ refDiv1.id = "div1";
13831383+ const refDiv2 = document.createElement("div");
13841384+ refDiv2.id = "div2";
13851385+13861386+ reference.appendChild(refInput);
13871387+ reference.appendChild(refDiv1);
13881388+ reference.appendChild(refDiv2);
13891389+13901390+ morph(parent, reference);
13911391+13921392+ // Input should be first due to higher sensitivity
13931393+ expect(parent.children[0].id).toBe("input");
13941394+ });
13951395+ });
13961396+ });
13971397+ });
13981398+});