test_range_bounds.html (15106B)
1 <!DOCTYPE HTML> 2 <html> 3 <!-- 4 https://bugzilla.mozilla.org/show_bug.cgi?id=421640 5 --> 6 <head> 7 <title>Test for Bug 396392</title> 8 <meta http-equiv="content-type" content="text/html; charset=UTF-8"> 9 <script src="/tests/SimpleTest/SimpleTest.js"></script> 10 <script src="/tests/SimpleTest/EventUtils.js"></script> 11 <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> 12 </head> 13 <body> 14 <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=396392">Mozilla Bug Range getClientRects and getBoundingClientRect</a> 15 <div id="content" style="font-family:monospace;font-size:12px;width:100px"> 16 <p>000000<span>0</span></p><div>00000<span>0</span></div><p>0000<span>0000</span>0000</p><div><span>000000000000 00000000000000 000000</span></div><div>000000000000 00000000000003 100305</div> 17 </div> 18 <div id="mixeddir" style="font-family:monospace;font-size:12px;width:100px"><span>english <bdo id="bdo" dir="rtl">rtl-overide english</bdo> word</span></div> 19 <div id="mixeddir2" style="font-family:monospace;font-size:12px"><span>english <bdo id="bdo2" dir="rtl">rtl-override english</bdo> word</span></div> 20 <pre id="test"> 21 <script class="testbody" type="text/javascript"> 22 23 var isLTR = true; 24 var isTransformed = false; 25 26 function annotateName(name) { 27 return (isLTR ? 'isLTR ' : 'isRTL ') + 28 (isTransformed ? 'transformed ' : '') + name; 29 } 30 31 function isEmptyRect(rect, name) { 32 name = annotateName(name); 33 is(rect.left, 0, name+'empty rect should have left = 0'); 34 is(rect.right, 0, name+'empty rect should have right = 0'); 35 is(rect.top, 0, name+'empty rect should have top = 0'); 36 is(rect.bottom, 0, name+'empty rect should have bottom = 0'); 37 is(rect.width, 0, name+'empty rect should have width = 0'); 38 is(rect.height, 0, name+'empty rect should have height = 0'); 39 } 40 41 function getTextBoundingClientRect(node) { 42 const quads = node.getBoxQuads()[0]; 43 return DOMRect.fromRect({ 44 x: quads.p1.x, 45 y: quads.p1.y, 46 width: quads.p2.x - quads.p1.x, 47 height: quads.p3.y - quads.p2.y 48 }); 49 } 50 51 function sortRectList(rectlist) { 52 return Array.prototype.slice.call(rectlist, 0).sort(function(a, b) { 53 return a.top - b.top || a.left - b.left; 54 }); 55 } 56 57 function isEmptyRectList(rectlist, name) { 58 name = annotateName(name); 59 is(rectlist.length, 0, name + 'empty rectlist should have zero rects'); 60 } 61 62 // round coordinates to the nearest 1/256 of a pixel 63 function roundCoord(x) { 64 return Math.round(x * 256) / 256; 65 } 66 67 function _getRect(r) { 68 if (r.length) //array 69 return "{left:"+roundCoord(r[0])+",right:"+roundCoord(r[1])+ 70 ",top:" +roundCoord(r[2])+",bottom:"+roundCoord(r[3])+ 71 ",width:"+roundCoord(r[4])+",height:"+roundCoord(r[5])+"}"; 72 else 73 return "{left:"+roundCoord(r.left)+",right:"+roundCoord(r.right)+ 74 ",top:"+roundCoord(r.top)+",bottom:"+roundCoord(r.bottom)+ 75 ",width:"+roundCoord(r.width)+",height:"+roundCoord(r.height)+"}"; 76 } 77 78 function runATest(obj) { 79 var range = document.createRange(); 80 try { 81 range.setStart(obj.range[0],obj.range[1]); 82 if (obj.range.length>2) { 83 range.setEnd(obj.range[2]||obj.range[0], obj.range[3]); 84 } 85 //test getBoundingClientRect() 86 var rect = range.getBoundingClientRect(); 87 var testname = 'range.getBoundingClientRect for ' + obj.name; 88 if (obj.rect) { 89 is(_getRect(rect),_getRect(obj.rect), annotateName(testname)); 90 } else { 91 isEmptyRect(rect,testname+": "); 92 } 93 //test getClientRects() 94 var rectlist = range.getClientRects(); 95 testname = 'range.getClientRects for ' + obj.name; 96 if (!obj.rectList) { 97 //rectList is not specified, use obj.rect to figure out rectList 98 obj.rectList = obj.rect?[obj.rect]:[]; 99 } 100 if (!obj.rectList.length) { 101 isEmptyRectList(rectlist, testname+": "); 102 } else { 103 is(rectlist.length, obj.rectList.length, 104 annotateName(testname+' should return '+obj.rectList.length+' rects.')); 105 if(!obj.rectList.forEach){ 106 //convert RectList to a real array 107 obj.rectList=Array.prototype.slice.call(obj.rectList, 0); 108 } 109 if (obj.mustSortBeforeComparing) { 110 rectlist = sortRectList(rectlist); 111 } 112 obj.rectList.forEach(function(r,i) { 113 is(_getRect(rectlist[i]),_getRect(r), 114 annotateName(testname+": item at "+i)); 115 }); 116 } 117 } finally { 118 range.detach(); 119 } 120 } 121 /** Test for Bug 396392 */ 122 function doTest(){ 123 var root = document.getElementById('content'); 124 var firstP = root.firstElementChild, spanInFirstP = firstP.childNodes[1], 125 firstDiv = root.childNodes[2], spanInFirstDiv = firstDiv.childNodes[1], 126 secondP = root.childNodes[3], spanInSecondP = secondP.childNodes[1], 127 secondDiv = root.childNodes[4], spanInSecondDiv = secondDiv.firstChild, 128 thirdDiv = root.childNodes[5]; 129 var firstPRect = firstP.getBoundingClientRect(), 130 spanInFirstPRect = spanInFirstP.getBoundingClientRect(), 131 textInFirstPRect = getTextBoundingClientRect(firstP.firstChild), 132 textInSpanInFirstPRect = getTextBoundingClientRect(spanInFirstP.firstChild), 133 firstDivRect = firstDiv.getBoundingClientRect(), 134 textInFirstDivRect = getTextBoundingClientRect(firstDiv.firstChild), 135 spanInFirstDivRect = spanInFirstDiv.getBoundingClientRect(), 136 textInSpanInFirstDivRect = getTextBoundingClientRect(spanInFirstDiv.firstChild), 137 secondPRect = secondP.getBoundingClientRect(), 138 secondDivRect = secondDiv.getBoundingClientRect(), 139 spanInSecondPRect = spanInSecondP.getBoundingClientRect(), 140 textInSpanInSecondPRect = getTextBoundingClientRect(spanInSecondP.firstChild), 141 spanInSecondDivRect = spanInSecondDiv.getBoundingClientRect(), 142 spanInSecondDivRectList = spanInSecondDiv.getClientRects(); 143 var widthPerchar = spanInSecondPRect.width / spanInSecondP.firstChild.length; 144 var testcases = [ 145 {name:'nodesNotInDocument', range:[document.createTextNode('abc'), 1], 146 rect:null}, 147 {name:'collapsedInBlockNode', range:[firstP, 2], rect:null}, 148 {name:'collapsedAtBeginningOfTextNode', range:[firstP.firstChild, 0], 149 rect:[spanInFirstPRect.left - 6 * widthPerchar, 150 spanInFirstPRect.left - 6 * widthPerchar, spanInFirstPRect.top, 151 spanInFirstPRect.bottom, 0, spanInFirstPRect.height]}, 152 {name:'collapsedWithinTextNode', range:[firstP.firstChild, 1], 153 rect:[spanInFirstPRect.left - 5 * widthPerchar, 154 spanInFirstPRect.left - 5 * widthPerchar, 155 spanInFirstPRect.top, spanInFirstPRect.bottom, 0, spanInFirstPRect.height]}, 156 {name:'collapsedAtEndOfTextNode', range:[firstP.firstChild, 6], 157 rect:[spanInFirstPRect.left, spanInFirstPRect.left, 158 spanInFirstPRect.top, spanInFirstPRect.bottom, 0, spanInFirstPRect.height]}, 159 {name:'singleBlockNode', range:[root, 1, root, 2], rect:firstPRect, 160 rectList:[firstPRect, textInFirstPRect, spanInFirstPRect]}, 161 {name:'twoBlockNodes', range:[root, 1, root, 3], 162 rect:[firstPRect.left, firstPRect.right, firstPRect.top, 163 firstDivRect.bottom, firstPRect.width, 164 firstDivRect.bottom - firstPRect.top], 165 rectList:[firstPRect, textInFirstPRect, textInSpanInFirstPRect, 166 firstDivRect, textInFirstDivRect, textInSpanInFirstDivRect]}, 167 {name:'endOfTextNodeToEndOfAnotherTextNodeInAnotherBlock', 168 range:[spanInFirstP.firstChild, 1, firstDiv.firstChild, 5], 169 rect:[spanInFirstDivRect.left - 5*widthPerchar, spanInFirstDivRect.left, 170 spanInFirstDivRect.top, spanInFirstDivRect.bottom, 5 * widthPerchar, 171 spanInFirstDivRect.height]}, 172 {name:'startOfTextNodeToStartOfAnotherTextNodeInAnotherBlock', 173 range:[spanInFirstP.firstChild, 0, firstDiv.firstChild, 0], 174 rect:[spanInFirstPRect.left, spanInFirstPRect.left + widthPerchar, spanInFirstPRect.top, 175 spanInFirstPRect.bottom, widthPerchar, spanInFirstPRect.height]}, 176 {name:'endPortionOfATextNode', range:[firstP.firstChild, 3, 177 firstP.firstChild, 6], 178 rect:[spanInFirstPRect.left - 3*widthPerchar, spanInFirstPRect.left, 179 spanInFirstPRect.top, spanInFirstPRect.bottom, 3*widthPerchar, spanInFirstPRect.height]}, 180 {name:'startPortionOfATextNode', range:[firstP.firstChild, 0, 181 firstP.firstChild, 3], 182 rect:[spanInFirstPRect.left - 6*widthPerchar, 183 spanInFirstPRect.left - 3*widthPerchar, spanInFirstPRect.top, 184 spanInFirstPRect.bottom, 3 * widthPerchar, spanInFirstPRect.height]}, 185 {name:'spanTextNodes', range:[secondP.firstChild, 1, secondP.lastChild, 1], 186 rect:[spanInSecondPRect.left - 3*widthPerchar, spanInSecondPRect.right + 187 widthPerchar, spanInSecondPRect.top, spanInSecondPRect.bottom, 188 spanInSecondPRect.width + 4*widthPerchar, spanInSecondPRect.height], 189 rectList:[[spanInSecondPRect.left - 3*widthPerchar, spanInSecondPRect.left, 190 spanInSecondPRect.top, spanInSecondPRect.bottom, 3 * widthPerchar, 191 spanInSecondPRect.height], 192 spanInSecondPRect, textInSpanInSecondPRect, 193 [spanInSecondPRect.right, spanInSecondPRect.right + widthPerchar, 194 spanInSecondPRect.top, spanInSecondPRect.bottom, widthPerchar, 195 spanInSecondPRect.height]]} 196 ]; 197 testcases.forEach(runATest); 198 199 // testcases that have different ranges in LTR and RTL 200 var directionDependentTestcases; 201 if (isLTR) { 202 directionDependentTestcases = [ 203 {name:'spanAcrossLines',range:[spanInSecondDiv.firstChild, 1, spanInSecondDiv.firstChild, 30], 204 rect: spanInSecondDivRect, 205 rectList:[[spanInSecondDivRectList[0].left+widthPerchar, 206 spanInSecondDivRectList[0].right, spanInSecondDivRectList[0].top, 207 spanInSecondDivRectList[0].bottom, spanInSecondDivRectList[0].width - widthPerchar, 208 spanInSecondDivRectList[0].height], 209 spanInSecondDivRectList[1], 210 [spanInSecondDivRectList[2].left, 211 spanInSecondDivRectList[2].right - 4 * widthPerchar, spanInSecondDivRectList[2].top, 212 spanInSecondDivRectList[2].bottom, 213 spanInSecondDivRectList[2].width - 4 * widthPerchar, 214 spanInSecondDivRectList[2].height]]}, 215 {name:'textAcrossLines',range:[thirdDiv.firstChild, 13, thirdDiv.firstChild, 28], 216 rect: [spanInSecondDivRectList[1].left, spanInSecondDivRectList[1].right, 217 spanInSecondDivRectList[1].top + secondDivRect.height, 218 spanInSecondDivRectList[1].bottom + secondDivRect.height, 219 spanInSecondDivRectList[1].width, spanInSecondDivRectList[1].height]} 220 ]; 221 } else { 222 directionDependentTestcases = [ 223 {name:'spanAcrossLines',range:[spanInSecondDiv.firstChild, 1, spanInSecondDiv.firstChild, 30], 224 rect: spanInSecondDivRect, 225 rectList:[[spanInSecondDivRectList[0].left+widthPerchar, 226 spanInSecondDivRectList[0].right, spanInSecondDivRectList[0].top, 227 spanInSecondDivRectList[0].bottom, spanInSecondDivRectList[0].width - widthPerchar, 228 spanInSecondDivRectList[0].height], 229 spanInSecondDivRectList[1], 230 spanInSecondDivRectList[2], 231 spanInSecondDivRectList[3], 232 [spanInSecondDivRectList[4].left, 233 spanInSecondDivRectList[4].right - 4 * widthPerchar, 234 spanInSecondDivRectList[4].top, 235 spanInSecondDivRectList[4].bottom, 236 spanInSecondDivRectList[4].width - 4 * widthPerchar, 237 spanInSecondDivRectList[4].height]]}, 238 {name:'textAcrossLines',range:[thirdDiv.firstChild, 13, thirdDiv.firstChild, 28], 239 rect: [spanInSecondDivRectList[2].left, spanInSecondDivRectList[2].right, 240 spanInSecondDivRectList[2].top + secondDivRect.height, 241 spanInSecondDivRectList[2].bottom + secondDivRect.height, 242 spanInSecondDivRectList[2].width, spanInSecondDivRectList[2].height], 243 rectList:[[spanInSecondDivRectList[2].left, spanInSecondDivRectList[2].right, 244 spanInSecondDivRectList[2].top + secondDivRect.height, 245 spanInSecondDivRectList[2].bottom + secondDivRect.height, 246 spanInSecondDivRectList[2].width, spanInSecondDivRectList[2].height], 247 [spanInSecondDivRectList[2].left, spanInSecondDivRectList[2].left, 248 spanInSecondDivRectList[2].top + secondDivRect.height, 249 spanInSecondDivRectList[2].bottom + secondDivRect.height, 250 0, spanInSecondDivRectList[2].height]]} 251 ]; 252 } 253 directionDependentTestcases.forEach(runATest); 254 } 255 function testMixedDir(){ 256 var root = document.getElementById('mixeddir'); 257 var bdo = document.getElementById('bdo'); 258 var firstSpan = root.firstElementChild, firstSpanRect=firstSpan.getBoundingClientRect(), 259 firstSpanWithInnerTextRectList = Array.from(firstSpan.getClientRects()); 260 firstSpanWithInnerTextRectList.push(...bdo.getClientRects()); 261 262 // Depending on the font rendering, the order of the rects composing the bdo 263 // element may vary. We need to sort the list of rects before comparing it to 264 // the expected list. 265 firstSpanWithInnerTextRectList = sortRectList(firstSpanWithInnerTextRectList); 266 runATest({name:'mixeddir',range:[firstSpan.firstChild,0,firstSpan.lastChild,firstSpan.lastChild.length], 267 rect: firstSpanRect, rectList:firstSpanWithInnerTextRectList, mustSortBeforeComparing: true}); 268 269 root = document.getElementById('mixeddir2'); 270 firstSpan = root.firstElementChild; 271 firstSpanRect = firstSpan.getBoundingClientRect(); 272 bdo = document.getElementById('bdo2'); 273 bdoRect=bdo.getBoundingClientRect(); 274 var widthPerChar = bdoRect.width / bdo.firstChild.length; 275 runATest({name:'mixeddirPartial', range:[firstSpan.firstChild, 3, 276 bdo.firstChild, 7], 277 rect: [firstSpanRect.left + 3*widthPerChar, bdoRect.right, 278 bdoRect.top, bdoRect.bottom, 279 (firstSpan.firstChild.length + bdo.firstChild.length - 3) * 280 widthPerChar, 281 bdoRect.height], 282 rectList:[[firstSpanRect.left + 3*widthPerChar, 283 bdoRect.left, 284 firstSpanRect.top, firstSpanRect.bottom, 285 (firstSpan.firstChild.length - 3) * widthPerChar, 286 firstSpanRect.height], 287 [bdoRect.right - 7 * widthPerChar, bdoRect.right, 288 bdoRect.top, bdoRect.bottom, 289 7*widthPerChar, bdoRect.height]]}); 290 } 291 292 function testShadowDOM() { 293 var ifr = document.createElement("iframe"); 294 document.body.appendChild(ifr); 295 var doc = ifr.contentDocument; 296 var d = doc.createElement("div"); 297 var sr = d.attachShadow({mode: "open"}); 298 sr.innerHTML = "<div>inside shadow DOM</div>"; 299 doc.body.appendChild(d); 300 var r = new ifr.contentWindow.Range(); 301 r.selectNode(sr.firstChild); 302 var rect = r.getBoundingClientRect(); 303 isnot(rect.width, 0, "Div element inside shadow shouldn't have zero size."); 304 isnot(rect.height, 0, "Div element inside shadow shouldn't have zero size."); 305 } 306 307 async function test(){ 308 // We use getBoxQuads to get some text nodes bounding rects. 309 await SpecialPowers.pushPrefEnv({"set": [["layout.css.getBoxQuads.enabled", true]]}); 310 311 //test ltr 312 doTest(); 313 314 //test rtl 315 isLTR = false; 316 var root = document.getElementById('content'); 317 root.dir = 'rtl'; 318 doTest(); 319 isLTR = true; 320 root.dir = 'ltr'; 321 322 testMixedDir(); 323 324 //test transforms 325 isTransformed = true; 326 root.style.transform = "translate(30px,50px)"; 327 doTest(); 328 329 testShadowDOM(); 330 SimpleTest.finish(); 331 } 332 333 window.onload = function() { 334 SimpleTest.waitForExplicitFinish(); 335 setTimeout(test, 0); 336 }; 337 338 </script> 339 </pre> 340 </body> 341 </html>