Skip to content

Commit fce591c

Browse files
committed
SimpleRange containedNodes() now works also if startNode and endNode are the same
SimpleRange containedNodes() no longer changes the node's text (stupid c&p from delete) Range getBoundingClientRect() and getClientRects() supporting DomText node's also. Range getBoundingClientRect() now sets the X and Y value of the rect instead of setting the Y value twice
1 parent 0c5606e commit fce591c

4 files changed

Lines changed: 165 additions & 12 deletions

File tree

src/changes/changes.xml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,18 @@
88

99
<body>
1010
<release version="5.3.0" date="July xx, 2026" description="Bugfixes">
11+
<action type="update" dev="rbri">
12+
SimpleRange containedNodes() now works also if startNode and endNode are the same.
13+
</action>
14+
<action type="fix" dev="rbri">
15+
SimpleRange containedNodes() no longer changes the node's text (stupid c&p from delete).
16+
</action>
17+
<action type="update" dev="rbri">
18+
Range getBoundingClientRect() and getClientRects() supporting DomText node's also.
19+
</action>
20+
<action type="fix" dev="rbri">
21+
Range getBoundingClientRect() now sets the X and Y value of the rect instead of setting the Y value twice.
22+
</action>
1123
<action type="fix" dev="rbri" issue="#1141">
1224
DomNode cloneNode() fixed to ensure isBodyParsed() returns true for the clone.
1325
</action>

src/main/java/org/htmlunit/html/impl/SimpleRange.java

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -556,29 +556,27 @@ public List<DomNode> containedNodes() {
556556
return Collections.emptyList();
557557
}
558558

559+
// When start == end (same text node), just return it directly
560+
if (startContainer_ == endContainer_ && isOffsetChars(startContainer_)) {
561+
return Collections.singletonList(startContainer_);
562+
}
563+
564+
// Resolve start node without mutating
559565
final DomNode start;
560-
final DomNode end;
561566
if (isOffsetChars(startContainer_)) {
562567
start = startContainer_;
563-
String text = getText(start);
564-
if (startOffset_ > -1 && startOffset_ < text.length()) {
565-
text = text.substring(0, startOffset_);
566-
}
567-
setText(start, text);
568568
}
569569
else if (startContainer_.getChildNodes().getLength() > startOffset_) {
570570
start = (DomNode) startContainer_.getChildNodes().item(startOffset_);
571571
}
572572
else {
573573
start = startContainer_.getNextSibling();
574574
}
575+
576+
// Resolve end node without mutating
577+
final DomNode end;
575578
if (isOffsetChars(endContainer_)) {
576579
end = endContainer_;
577-
String text = getText(end);
578-
if (endOffset_ > -1 && endOffset_ < text.length()) {
579-
text = text.substring(endOffset_);
580-
}
581-
setText(end, text);
582580
}
583581
else if (endContainer_.getChildNodes().getLength() > endOffset_) {
584582
end = (DomNode) endContainer_.getChildNodes().item(endOffset_);

src/main/java/org/htmlunit/javascript/host/dom/Range.java

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import org.htmlunit.WebClient;
2222
import org.htmlunit.html.DomDocumentFragment;
2323
import org.htmlunit.html.DomNode;
24+
import org.htmlunit.html.DomText;
2425
import org.htmlunit.html.impl.SimpleRange;
2526
import org.htmlunit.javascript.HtmlUnitScriptable;
2627
import org.htmlunit.javascript.JavaScriptEngine;
@@ -455,6 +456,19 @@ public DOMRectList getClientRects() {
455456
rect.setPrototype(getPrototype(rect.getClass()));
456457
rectList.add(rect);
457458
}
459+
else if (node instanceof DomText) {
460+
// Text nodes have no scriptable; delegate to the parent element
461+
final DomNode parent = node.getParentNode();
462+
if (parent != null) {
463+
final HtmlUnitScriptable parentScriptable = parent.getScriptableObject();
464+
if (parentScriptable instanceof HTMLElement parentElement) {
465+
final DOMRect rect = parentElement.getBoundingClientRect();
466+
rect.setParentScope(getParentScope());
467+
rect.setPrototype(getPrototype(rect.getClass()));
468+
rectList.add(rect);
469+
}
470+
}
471+
}
458472
}
459473

460474
return rectList;
@@ -481,11 +495,25 @@ public DOMRect getBoundingClientRect() {
481495
final HtmlUnitScriptable scriptable = node.getScriptableObject();
482496
if (scriptable instanceof HTMLElement element) {
483497
final DOMRect childRect = element.getBoundingClientRect();
484-
rect.setY(Math.min(rect.getX(), childRect.getX()));
498+
rect.setX(Math.min(rect.getX(), childRect.getX()));
485499
rect.setY(Math.min(rect.getY(), childRect.getY()));
486500
rect.setWidth(Math.max(rect.getWidth(), childRect.getWidth()));
487501
rect.setHeight(Math.max(rect.getHeight(), childRect.getHeight()));
488502
}
503+
else if (node instanceof DomText) {
504+
// Text nodes have no scriptable; delegate to the parent element
505+
final DomNode parent = node.getParentNode();
506+
if (parent != null) {
507+
final HtmlUnitScriptable parentScriptable = parent.getScriptableObject();
508+
if (parentScriptable instanceof HTMLElement parentElement) {
509+
final DOMRect childRect = parentElement.getBoundingClientRect();
510+
rect.setX(Math.min(rect.getX(), childRect.getX()));
511+
rect.setY(Math.min(rect.getY(), childRect.getY()));
512+
rect.setWidth(Math.max(rect.getWidth(), childRect.getWidth()));
513+
rect.setHeight(Math.max(rect.getHeight(), childRect.getHeight()));
514+
}
515+
}
516+
}
489517
}
490518

491519
return rect;

src/test/java/org/htmlunit/javascript/host/dom/RangeTest.java

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -504,5 +504,120 @@ public void testToString() throws Exception {
504504
+ " log(r.toString());\n"
505505
+ "</script></body></html>";
506506
loadPageVerifyTitle2(html);
507+
508+
}
509+
/**
510+
* Test getClientRects for a range over a text node (setStart/setEnd on a text node with char offsets).
511+
*
512+
* @throws Exception if an error occurs
513+
*/
514+
@Test
515+
@Alerts("1")
516+
public void getClientRectsOnTextNode() throws Exception {
517+
final String html = DOCTYPE_HTML
518+
+ "<html><body><div id='d'>Hello World</div>\n"
519+
+ "<script>\n"
520+
+ LOG_TITLE_FUNCTION
521+
+ " var d = document.getElementById('d');\n"
522+
+ " var textNode = d.firstChild;\n"
523+
+ " var r = document.createRange();\n"
524+
+ " r.setStart(textNode, 0);\n"
525+
+ " r.setEnd(textNode, 5);\n"
526+
+ " log(r.getClientRects().length);\n"
527+
+ "</script></body></html>";
528+
loadPageVerifyTitle2(html);
529+
}
530+
531+
/**
532+
* Test that getClientRects does not mutate the text content of the DOM.
533+
*
534+
* @throws Exception if an error occurs
535+
*/
536+
@Test
537+
@Alerts({"Hello World", "Hello World"})
538+
public void getClientRectsDoesNotMutateTextContent() throws Exception {
539+
final String html = DOCTYPE_HTML
540+
+ "<html><body><div id='d'>Hello World</div>\n"
541+
+ "<script>\n"
542+
+ LOG_TITLE_FUNCTION
543+
+ " var d = document.getElementById('d');\n"
544+
+ " var textNode = d.firstChild;\n"
545+
+ " var r = document.createRange();\n"
546+
+ " r.setStart(textNode, 0);\n"
547+
+ " r.setEnd(textNode, 5);\n"
548+
+ " log(d.textContent);\n"
549+
+ " r.getClientRects();\n"
550+
+ " log(d.textContent);\n" // must be unchanged
551+
+ "</script></body></html>";
552+
loadPageVerifyTitle2(html);
553+
}
554+
555+
/**
556+
* Test getBoundingClientRect for a range over a text node.
557+
*
558+
* @throws Exception if an error occurs
559+
*/
560+
@Test
561+
@Alerts("true")
562+
public void getBoundingClientRectOnTextNode() throws Exception {
563+
final String html = DOCTYPE_HTML
564+
+ "<html><body><div id='d'>Hello World</div>\n"
565+
+ "<script>\n"
566+
+ LOG_TITLE_FUNCTION
567+
+ " var d = document.getElementById('d');\n"
568+
+ " var textNode = d.firstChild;\n"
569+
+ " var r = document.createRange();\n"
570+
+ " r.setStart(textNode, 0);\n"
571+
+ " r.setEnd(textNode, 5);\n"
572+
+ " var rect = r.getBoundingClientRect();\n"
573+
+ " log(rect.width > 0 && rect.height > 0);\n"
574+
+ "</script></body></html>";
575+
loadPageVerifyTitle2(html);
576+
}
577+
578+
/**
579+
* Test that getBoundingClientRect does not mutate text content of the DOM.
580+
*
581+
* @throws Exception if an error occurs
582+
*/
583+
@Test
584+
@Alerts({"Hello World", "Hello World"})
585+
public void getBoundingClientRectDoesNotMutateTextContent() throws Exception {
586+
final String html = DOCTYPE_HTML
587+
+ "<html><body><div id='d'>Hello World</div>\n"
588+
+ "<script>\n"
589+
+ LOG_TITLE_FUNCTION
590+
+ " var d = document.getElementById('d');\n"
591+
+ " var textNode = d.firstChild;\n"
592+
+ " var r = document.createRange();\n"
593+
+ " r.setStart(textNode, 0);\n"
594+
+ " r.setEnd(textNode, 5);\n"
595+
+ " log(d.textContent);\n"
596+
+ " r.getBoundingClientRect();\n"
597+
+ " log(d.textContent);\n" // must be unchanged
598+
+ "</script></body></html>";
599+
loadPageVerifyTitle2(html);
600+
}
601+
602+
/**
603+
* Test getClientRects for a mid-string text range (non-zero start offset).
604+
* Verifies containedNodes() handles same-node text ranges at any offset.
605+
* @throws Exception if an error occurs
606+
*/
607+
@Test
608+
@Alerts("1")
609+
public void getClientRectsOnTextNodeMidString() throws Exception {
610+
final String html = DOCTYPE_HTML
611+
+ "<html><body><div id='d'>Hello World</div>\n"
612+
+ "<script>\n"
613+
+ LOG_TITLE_FUNCTION
614+
+ " var d = document.getElementById('d');\n"
615+
+ " var textNode = d.firstChild;\n"
616+
+ " var r = document.createRange();\n"
617+
+ " r.setStart(textNode, 3);\n"
618+
+ " r.setEnd(textNode, 8);\n"
619+
+ " log(r.getClientRects().length);\n"
620+
+ "</script></body></html>";
621+
loadPageVerifyTitle2(html);
507622
}
508623
}

0 commit comments

Comments
 (0)