tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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>