FormControlRange-programmatic-updates.html (15352B)
1 <!DOCTYPE html> 2 <meta charset="utf-8"> 3 <script src="/resources/testharness.js"></script> 4 <script src="/resources/testharnessreport.js"></script> 5 <body></body> 6 <script> 7 'use strict'; 8 9 // Verifies that FormControlRange updates its start/end offsets for 10 // programmatic text mutations mirroring DOM Range behavior in the controlโs 11 // value space. Each test first exercises a DOM Range in a contenteditable div, 12 // then performs the equivalent operation with FormControlRange to ensure 13 // behavior parity. 14 15 const controls = ['input', 'textarea']; 16 17 function setup(control, value) { 18 document.body.innerHTML = control === 'input' ? '<input type="text">' : '<textarea></textarea>'; 19 const element = document.body.firstElementChild; 20 element.value = value; 21 element.focus(); 22 return element; 23 } 24 25 function makeFormControlRange(element, start, end) { 26 const range = new FormControlRange(); 27 range.setFormControlRange(element, start, end); 28 return range; 29 } 30 31 function setupEditable(value) { 32 // Create a contenteditable div with a single text node. 33 const editable = document.createElement('div'); 34 editable.setAttribute('contenteditable', 'true'); 35 editable.textContent = value; 36 document.body.appendChild(editable); 37 const text = editable.firstChild; 38 return { editable, text }; 39 } 40 41 function makeDomRange(text, start, end) { 42 // Construct a DOM Range in the given text node. 43 const r = document.createRange(); 44 r.setStart(text, start); 45 r.setEnd(text, end); 46 return r; 47 } 48 49 controls.forEach(control => { 50 test(() => { 51 // DOM: full replace (shorter) collapses to [0,0]. 52 const d1 = setupEditable('ABCDEFG'); 53 const domRange1 = makeDomRange(d1.text, 2, 5); 54 d1.text.replaceData(0, 7, 'XY'); 55 assert_equals(domRange1.startOffset, 0, 'DOM shorter: start collapsed to 0'); 56 assert_equals(domRange1.endOffset, 0, 'DOM shorter: end collapsed to 0'); 57 58 // FormControlRange: full .value replacement (shorter) collapses to [0,0]. 59 let element = setup(control, 'ABCDEFG'); 60 let range = makeFormControlRange(element, 2, 5); 61 element.value = 'XY'; 62 assert_equals(range.startOffset, 0, 'FormControlRange shorter: start collapsed to 0'); 63 assert_equals(range.endOffset, 0, 'FormControlRange shorter: end collapsed to 0'); 64 65 // DOM: full replace (longer) collapses to [0,0]. 66 const d2 = setupEditable('ABC'); 67 const domRange2 = makeDomRange(d2.text, 1, 3); 68 d2.text.replaceData(0, 3, 'ABCDEFGHIJKLMNOP'); 69 assert_equals(domRange2.startOffset, 0, 'DOM longer: start collapsed to 0'); 70 assert_equals(domRange2.endOffset, 0, 'DOM longer: end collapsed to 0'); 71 72 // FormControlRange: full .value replacement (longer) collapses to [0,0]. 73 element = setup(control, 'ABC'); 74 range = makeFormControlRange(element, 1, 3); 75 element.value = 'ABCDEFGHIJKLMNOP'; 76 assert_equals(range.startOffset, 0, 'FormControlRange longer: start collapsed to 0'); 77 assert_equals(range.endOffset, 0, 'FormControlRange longer: end collapsed to 0'); 78 }, `Full replace collapses to start (shorter & longer) (${control})`); 79 80 test(() => { 81 // DOM: full replace when prior range spans whole old value; collapse to [0,0]. 82 const d = setupEditable('ABCDE'); 83 const domRange = makeDomRange(d.text, 0, 5); 84 d.text.replaceData(0, 5, 'VWXYZ'); 85 assert_equals(domRange.startOffset, 0, 'DOM whole-old: start collapsed to 0'); 86 assert_equals(domRange.endOffset, 0, 'DOM whole-old: end collapsed to 0'); 87 88 // FormControlRange: same scenario. 89 const element = setup(control, 'ABCDE'); 90 const range = makeFormControlRange(element, 0, 5); 91 element.value = 'VWXYZ'; 92 assert_equals(range.startOffset, 0, 'FormControlRange whole-old: start collapsed to 0'); 93 assert_equals(range.endOffset, 0, 'FormControlRange whole-old: end collapsed to 0'); 94 }, `Full replace from whole-old range collapses to 0 (${control})`); 95 96 test(() => { 97 // DOM: full replace with equal length; collapse to [0,0]. 98 const d = setupEditable('ABCDE'); 99 const domRange = makeDomRange(d.text, 1, 4); 100 d.text.replaceData(0, 5, 'VWXYZ'); 101 assert_equals(domRange.startOffset, 0, 'DOM equal-length: start collapsed to 0'); 102 assert_equals(domRange.endOffset, 0, 'DOM equal-length: end collapsed to 0'); 103 104 // FormControlRange: same scenario. 105 const element = setup(control, 'ABCDE'); 106 const range = makeFormControlRange(element, 1, 4); 107 element.value = 'VWXYZ'; 108 assert_equals(range.startOffset, 0, 'FormControlRange equal-length: start collapsed to 0'); 109 assert_equals(range.endOffset, 0, 'FormControlRange equal-length: end collapsed to 0'); 110 }, `Full replace (equal length) collapses to 0 (${control})`); 111 112 test(() => { 113 // DOM: replace [3,7) with "XX". 114 const d = setupEditable('0123456789'); 115 const domRange = makeDomRange(d.text, 2, 8); 116 d.text.replaceData(3, 4, 'XX'); 117 assert_equals(d.editable.textContent, '012XX789', 'DOM value reflects replace'); 118 assert_equals(domRange.startOffset, 2, 'DOM start unchanged before replaced segment'); 119 assert_equals(domRange.endOffset, 6, 'DOM end adjusted by net delta'); 120 121 // FormControlRange: same replace via setRangeText. 122 const element = setup(control, '0123456789'); 123 const range = makeFormControlRange(element, 2, 8); 124 element.setRangeText('XX', 3, 7); 125 assert_equals(element.value, '012XX789', 'FormControlRange value reflects setRangeText replace'); 126 assert_equals(range.startOffset, 2, 'FormControlRange start unchanged before replaced segment'); 127 assert_equals(range.endOffset, 6, 'FormControlRange end adjusted by net delta'); 128 }, `Partial replacement adjusts end (${control})`); 129 130 test(() => { 131 // DOM: no mutation; range unchanged. 132 const d = setupEditable('HELLO'); 133 const domRange = makeDomRange(d.text, 1, 4); 134 assert_equals(domRange.startOffset, 1, 'DOM start unchanged on no-op'); 135 assert_equals(domRange.endOffset, 4, 'DOM end unchanged on no-op'); 136 137 // FormControlRange: setting same value (no-op); range unchanged. 138 const element = setup(control, 'HELLO'); 139 const range = makeFormControlRange(element, 1, 4); 140 element.value = 'HELLO'; 141 assert_equals(range.startOffset, 1, 'FormControlRange start unchanged on no-op'); 142 assert_equals(range.endOffset, 4, 'FormControlRange end unchanged on no-op'); 143 }, `No-op leaves range unchanged (${control})`); 144 145 test(() => { 146 // DOM: insert before the range; shift both endpoints by +1. 147 const d = setupEditable('ABCDE'); 148 const domRange = makeDomRange(d.text, 2, 4); 149 d.text.insertData(1, 'Q'); 150 assert_equals(d.editable.textContent, 'AQBCDE', 'DOM value after insertion before range'); 151 assert_equals(domRange.startOffset, 3, 'DOM start +1'); 152 assert_equals(domRange.endOffset, 5, 'DOM end +1'); 153 154 // FormControlRange: same insert via setRangeText. 155 const element = setup(control, 'ABCDE'); 156 const range = makeFormControlRange(element, 2, 4); 157 element.setRangeText('Q', 1, 1); 158 assert_equals(element.value, 'AQBCDE', 'FormControlRange value after insertion before range'); 159 assert_equals(range.startOffset, 3, 'FormControlRange start +1'); 160 assert_equals(range.endOffset, 5, 'FormControlRange end +1'); 161 assert_equals(range.toString(), 'CD', 'FormControlRange range text stable'); 162 }, `Insertion before range shifts both endpoints (${control})`); 163 164 test(() => { 165 // DOM: delete [2,3) inside the range; end shrinks by 1. 166 const d = setupEditable('ABCDE'); 167 const domRange = makeDomRange(d.text, 1, 5); 168 d.text.deleteData(2, 1); 169 assert_equals(d.editable.textContent, 'ABDE', 'DOM value after interior deletion'); 170 assert_equals(domRange.startOffset, 1, 'DOM start unchanged'); 171 assert_equals(domRange.endOffset, 4, 'DOM end -1'); 172 173 // FormControlRange: same delete via setRangeText. 174 const element = setup(control, 'ABCDE'); 175 const range = makeFormControlRange(element, 1, 5); 176 element.setRangeText('', 2, 3); 177 assert_equals(element.value, 'ABDE', 'FormControlRange value after interior deletion'); 178 assert_equals(range.startOffset, 1, 'FormControlRange start unchanged'); 179 assert_equals(range.endOffset, 4, 'FormControlRange end -1'); 180 assert_equals(range.toString(), 'BDE', 'FormControlRange range reflects deletion'); 181 }, `Interior deletion shrinks end (${control})`); 182 183 test(() => { 184 // DOM: replace before the range with net -2. 185 const d = setupEditable('ABCDEFGHIJ'); 186 const domRange = makeDomRange(d.text, 7, 10); 187 d.text.replaceData(2, 3, 'Z'); 188 assert_equals(d.editable.textContent, 'ABZFGHIJ', 'DOM value after before-range shrink'); 189 assert_equals(domRange.startOffset, 5, 'DOM start -2'); 190 assert_equals(domRange.endOffset, 8, 'DOM end -2'); 191 192 // FormControlRange: same replacement via setRangeText. 193 const element = setup(control, 'ABCDEFGHIJ'); 194 const range = makeFormControlRange(element, 7, 10); 195 element.setRangeText('Z', 2, 5); 196 assert_equals(element.value, 'ABZFGHIJ', 'FormControlRange value after before-range shrink'); 197 assert_equals(range.startOffset, 5, 'FormControlRange start -2'); 198 assert_equals(range.endOffset, 8, 'FormControlRange end -2'); 199 assert_equals(range.toString(), 'HIJ', 'FormControlRange text unchanged'); 200 }, `Before-range shrink shifts left (${control})`); 201 202 test(() => { 203 // DOM: edits after the range; unchanged. 204 const d = setupEditable('ABCDEFGHIJ'); 205 const domRange = makeDomRange(d.text, 2, 5); 206 d.text.replaceData(7, 2, 'WXYZ'); 207 assert_equals(d.editable.textContent, 'ABCDEFGWXYZJ', 'DOM value after after-range grow'); 208 assert_equals(domRange.startOffset, 2, 'DOM start unchanged'); 209 assert_equals(domRange.endOffset, 5, 'DOM end unchanged'); 210 211 // FormControlRange: same replacement via setRangeText. 212 const element = setup(control, 'ABCDEFGHIJ'); 213 const range = makeFormControlRange(element, 2, 5); 214 element.setRangeText('WXYZ', 7, 9); 215 assert_equals(element.value, 'ABCDEFGWXYZJ', 'FormControlRange value after after-range grow'); 216 assert_equals(range.startOffset, 2, 'FormControlRange start unchanged'); 217 assert_equals(range.endOffset, 5, 'FormControlRange end unchanged'); 218 assert_equals(range.toString(), 'CDE', 'FormControlRange text unchanged'); 219 }, `After-range grow leaves range unchanged (${control})`); 220 221 test(() => { 222 // DOM: superset replacement collapses to start of change. 223 const d = setupEditable('ABCDEFG'); 224 const domRange = makeDomRange(d.text, 2, 5); 225 d.text.replaceData(1, 5, 'Q'); 226 assert_equals(d.editable.textContent, 'AQG', 'DOM value after superset replacement'); 227 assert_equals(domRange.startOffset, 1, 'DOM collapsed to change start'); 228 assert_equals(domRange.endOffset, 1, 'DOM collapsed'); 229 230 // FormControlRange: same replacement via setRangeText. 231 const element = setup(control, 'ABCDEFG'); 232 const range = makeFormControlRange(element, 2, 5); 233 element.setRangeText('Q', 1, 6); 234 assert_equals(element.value, 'AQG', 'FormControlRange value after superset replacement'); 235 assert_equals(range.startOffset, 1, 'FormControlRange collapsed to change start'); 236 assert_equals(range.endOffset, 1, 'FormControlRange collapsed'); 237 assert_true(range.collapsed, 'FormControlRange collapsed flag true'); 238 }, `Superset replacement collapses to change start (${control})`); 239 240 test(() => { 241 // DOM: insert exactly at range.start; start unchanged, end advances. 242 const d = setupEditable('ABCDE'); 243 const domRange = makeDomRange(d.text, 2, 4); 244 d.text.insertData(2, 'QQ'); 245 assert_equals(d.editable.textContent, 'ABQQCDE', 'DOM value after insert at start boundary'); 246 assert_equals(domRange.startOffset, 2, 'DOM start unchanged at boundary'); 247 assert_equals(domRange.endOffset, 6, 'DOM end +2'); 248 249 // FormControlRange: same insertion via setRangeText. 250 const element = setup(control, 'ABCDE'); 251 const range = makeFormControlRange(element, 2, 4); 252 element.setRangeText('QQ', 2, 2); 253 assert_equals(element.value, 'ABQQCDE', 'FormControlRange value after insert at start boundary'); 254 assert_equals(range.startOffset, 2, 'FormControlRange start unchanged at boundary'); 255 assert_equals(range.endOffset, 6, 'FormControlRange end +2'); 256 }, `Insert at range.start extends end (${control})`); 257 258 test(() => { 259 // DOM: insert exactly at range.end; both boundaries unchanged. 260 const d = setupEditable('ABCDE'); 261 const domRange = makeDomRange(d.text, 2, 4); 262 d.text.insertData(4, 'QQ'); 263 assert_equals(d.editable.textContent, 'ABCDQQE', 'DOM value after insert at end boundary'); 264 assert_equals(domRange.startOffset, 2, 'DOM start unchanged'); 265 assert_equals(domRange.endOffset, 4, 'DOM end unchanged at boundary'); 266 267 // FormControlRange: same insertion via setRangeText. 268 const element = setup(control, 'ABCDE'); 269 const range = makeFormControlRange(element, 2, 4); 270 element.setRangeText('QQ', 4, 4); 271 assert_equals(element.value, 'ABCDQQE', 'FormControlRange value after insert at end boundary'); 272 assert_equals(range.startOffset, 2, 'FormControlRange start unchanged'); 273 assert_equals(range.endOffset, 4, 'FormControlRange end unchanged at boundary'); 274 }, `Insert at range.end leaves range unchanged (${control})`); 275 276 test(() => { 277 // DOM: replace [1,3) (emoji is 2 code units) with two emojis (4 code units). 278 const d = setupEditable('A๐BC'); 279 const domRange = makeDomRange(d.text, 1, 4); 280 d.text.replaceData(1, 2, '๐๐'); 281 assert_equals(domRange.startOffset, 1, 'DOM start unchanged inside growth'); 282 assert_equals(domRange.endOffset, 6, 'DOM end +2 code units'); 283 284 // FormControlRange: same replacement via setRangeText. 285 const element = setup(control, 'A๐BC'); 286 const range = makeFormControlRange(element, 1, 4); 287 element.setRangeText('๐๐', 1, 3); 288 assert_equals(range.startOffset, 1, 'FormControlRange start unchanged inside growth'); 289 assert_equals(range.endOffset, 6, 'FormControlRange end +2 code units'); 290 assert_true(range.toString().length >= 3, 'FormControlRange range text non-empty after expansion'); 291 }, `Surrogate pair expansion grows end (${control})`); 292 293 test(() => { 294 // DOM: chained interior edits accumulate deltas. 295 const d = setupEditable('012345'); 296 const domRange = makeDomRange(d.text, 2, 5); 297 d.text.replaceData(3, 1, 'XX'); // [3,4) -> "XX" 298 d.text.deleteData(3, 2); // delete the "XX" 299 assert_equals(d.editable.textContent, '01245', 'DOM final value after chained edits'); 300 assert_equals(domRange.startOffset, 2, 'DOM start unchanged across interior edits'); 301 assert_equals(domRange.endOffset, 4, 'DOM end reflects cumulative delta'); 302 303 // FormControlRange: same edits via setRangeText. 304 const element = setup(control, '012345'); 305 const range = makeFormControlRange(element, 2, 5); 306 element.setRangeText('XX', 3, 4); 307 element.setRangeText('', 3, 5); 308 assert_equals(element.value, '01245', 'FormControlRange final value after chained edits'); 309 assert_equals(range.startOffset, 2, 'FormControlRange start unchanged across interior edits'); 310 assert_equals(range.endOffset, 4, 'FormControlRange end reflects cumulative delta'); 311 assert_equals(range.toString(), '24', 'FormControlRange final range text'); 312 }, `Chained interior edits cumulatively adjust range (${control})`); 313 }); 314 </script>