tor-browser

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

FormControlRange-geometry-multiline-and-mutations.html (9283B)


      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 const controls = ['input','textarea'];
      9 
     10 function setupControl(control, value) {
     11  document.body.innerHTML = control === 'input' ? '<input type="text" id="test">'
     12                                                : '<textarea id="test"></textarea>';
     13  const element = document.getElementById('test');
     14  element.value = value;
     15  element.focus();
     16  element.setSelectionRange(0, 0);
     17 
     18  // Stabilize layout for cross-platform consistency.
     19  element.style.fontFamily = 'monospace';
     20  element.style.fontSize = '16px';
     21  element.style.lineHeight = '20px';
     22  element.style.padding = '0';
     23  element.style.border = '0';
     24  element.style.margin = '8px';
     25  element.style.boxSizing = 'content-box';
     26  // Zero scroll offsets so client rects are relative to a known origin.
     27  if ('scrollLeft' in element) element.scrollLeft = 0;
     28  return element;
     29 }
     30 function setupFormControlRange(element, startOffset, endOffset){
     31  const range = new FormControlRange();
     32  range.setFormControlRange(element, startOffset, endOffset);
     33  return range;
     34 }
     35 
     36 function assert_rect_inside(inner, outer, msg = '') {
     37  const left_overflow = outer.left > inner.left ? outer.left - inner.left : 0;
     38  const right_overflow = inner.right > outer.right ? inner.right - outer.right : 0;
     39  assert_approx_equals(left_overflow, 0, 0.5, msg + 'left inside');
     40  assert_approx_equals(right_overflow, 0, 0.5, msg + 'right inside');
     41 }
     42 
     43 function rect(range, element, label='') {
     44  const r = range.getBoundingClientRect();
     45  assert_rect_inside(r, element.getBoundingClientRect(), label);
     46  return r;
     47 }
     48 
     49 test(() => {
     50  // Partial selection spanning a hard newline produces non-zero geometry and at least one client rect.
     51  const textareaElement = setupControl('textarea', 'first line\nSECOND LINE\nthird');
     52  const firstLine = 'first line';
     53  const range = setupFormControlRange(textareaElement, firstLine.length - 4, firstLine.length + 5);
     54  const boundingRect = rect(range, textareaElement, 'Bounding box inside: ');
     55  assert_greater_than(boundingRect.width, 0);
     56  assert_greater_than(boundingRect.height, 0);
     57  assert_greater_than_equal(range.getClientRects().length, 1);
     58 }, 'Partial hard-newline selection (textarea)');
     59 
     60 test(() => {
     61  // Selection that spans a blank line should still produce non-zero geometry.
     62  const textareaElement = setupControl('textarea', 'line1\n\nline3');
     63  const range = setupFormControlRange(textareaElement, 0, textareaElement.value.length);
     64  const boundingRect = rect(range, textareaElement, 'Bounding box inside: ');
     65  assert_greater_than(boundingRect.height, 0);
     66  assert_greater_than(boundingRect.width, 0);
     67 }, 'Selection spanning blank line (textarea)');
     68 
     69 test(() => {
     70  // Soft-wrapped long logical line yields multiple client rects due to wrapping.
     71  const textareaElement = setupControl('textarea', 'ABCDEFGHIJKLMNOPQRSTUVWXYZ 0123456789 abcdefghijklmnopqrstuvwxyz');
     72  textareaElement.style.whiteSpace = 'pre-wrap';
     73  textareaElement.style.width = '160px';
     74  const range = setupFormControlRange(textareaElement, 0, textareaElement.value.length);
     75  const boundingRect = rect(range, textareaElement, 'Bounding box inside: ');
     76  assert_greater_than_equal(range.getClientRects().length, 2, 'soft wrap multiple rects');
     77 }, 'Soft-wrapped long single line (textarea)');
     78 
     79 test(() => {
     80  // Caret before newline and start of next line have non-decreasing y and expected x ordering.
     81  const value = '123456\n789012';
     82  const textareaElement = setupControl('textarea', value);
     83  const caretBeforeNewlineRect = rect(setupFormControlRange(textareaElement, 6, 6), textareaElement, 'caret before newline inside: ');
     84  const caretNextLineRect = rect(setupFormControlRange(textareaElement, 7, 7), textareaElement, 'caret next line inside: ');
     85  // Allow caret before newline to be aligned or slightly (sub-pixel) to the right.
     86  assert_greater_than_equal(caretBeforeNewlineRect.x + 0.5, caretNextLineRect.x, 'caret before newline at or to the right (epsilon) of next line start');
     87  assert_less_than_equal(caretBeforeNewlineRect.y, caretNextLineRect.y, 'next line lower or equal y');
     88 }, 'Collapsed caret across newline parity (textarea)');
     89 
     90 test(() => {
     91  // Live insert/delete operations adjust selection left/width as expected (grow/shift/shrink).
     92  const inputElement = setupControl('input', 'ABCDE');
     93  const range = setupFormControlRange(inputElement, 1, 3);
     94 
     95  const leftBeforeInsertion = rect(range, inputElement).left;
     96  inputElement.setRangeText('ZZ', 0, 0);
     97  assert_greater_than(rect(range, inputElement).left, leftBeforeInsertion, 'insert before shifts right');
     98 
     99  const widthBeforeInteriorInsertion = rect(range, inputElement).width;
    100  inputElement.setRangeText('QQ', range.startOffset, range.startOffset);
    101  assert_greater_than(rect(range, inputElement).width, widthBeforeInteriorInsertion, 'insert inside expands width');
    102 
    103  const leftBeforeAppend = rect(range, inputElement).left;
    104  inputElement.setRangeText('TT', inputElement.value.length, inputElement.value.length);
    105  assert_approx_equals(rect(range, inputElement).left, leftBeforeAppend, 0.5, 'append does not shift left edge');
    106 
    107  const leftBeforeDeletion = rect(range, inputElement).left;
    108  inputElement.setRangeText('', 0, 2);
    109  assert_less_than(rect(range, inputElement).left, leftBeforeDeletion, 'delete before shifts left');
    110 
    111  const widthBeforeOverlapDeletion = rect(range, inputElement).width;
    112  inputElement.setRangeText('', range.startOffset - 1, range.startOffset + 1);
    113  assert_less_than(rect(range, inputElement).width, widthBeforeOverlapDeletion, 'overlap delete shrinks width');
    114 }, 'Input live insertion/deletion adjustments');
    115 
    116 test(() => {
    117  // Interior deletion shrinks width; insertion before selection shifts selection right in textarea.
    118  const textareaElement = setupControl('textarea', 'ABCDE');
    119  const range = setupFormControlRange(textareaElement, 1, 4);
    120  const widthBeforeDeletion = rect(range, textareaElement).width;
    121  textareaElement.setRangeText('', 2, 3);
    122  assert_less_than(rect(range, textareaElement).width, widthBeforeDeletion, 'textarea interior deletion shrinks width');
    123  const leftBeforeInsertion = rect(range, textareaElement).left;
    124  textareaElement.setRangeText('ZZ', 0, 0);
    125  assert_greater_than(rect(range, textareaElement).left, leftBeforeInsertion, 'textarea insertion before shifts right');
    126 }, 'Textarea interior deletion & insertion adjustments');
    127 
    128 controls.forEach(controlType => {
    129  test(() => {
    130    // Inserting text inside selection increases its width.
    131    const element = setupControl(controlType, 'ABCDE');
    132    const range = setupFormControlRange(element, 1, 4);
    133    const widthBeforeInsertion = rect(range, element).width;
    134    element.setRangeText('ZZ', 2, 2);
    135    assert_greater_than(rect(range, element).width, widthBeforeInsertion, 'insertion inside expands width');
    136  }, `Insertion inside expands width (${controlType})`);
    137 
    138  test(() => {
    139    // Deleting full selection collapses to a caret.
    140    const element = setupControl(controlType, 'ABCDE');
    141    const range = setupFormControlRange(element, 1, 4);
    142    assert_greater_than(rect(range, element).width, 0, 'pre width greater than 0');
    143    element.setRangeText('', 1, 4);
    144    assert_true(range.collapsed, 'collapsed after deletion');
    145    assert_equals(range.getClientRects().length, 0, 'no client rects');
    146    assert_approx_equals(rect(range, element).width, 0, 0.05, 'caret width should be 0');
    147  }, `Deletion collapses geometry (${controlType})`);
    148 
    149  test(() => {
    150    // Shrink near end clamps range end; subsequent insertion at end does not auto-extend.
    151    const element = setupControl(controlType, 'HelloWorld');
    152    const range = setupFormControlRange(element, 0, element.value.length);
    153    element.setRangeText('', 7, 10);
    154    const endAfterShrink = range.endOffset;
    155    assert_equals(endAfterShrink, element.value.length, 'end clamped after shrink');
    156    element.setRangeText('XYZ', 7, 7);
    157    assert_equals(range.endOffset, endAfterShrink, 'end remains stable after grow at end (does not auto-extend)');
    158  }, `Shrink/grow end offset clamping (${controlType})`);
    159 
    160  test(() => {
    161    // Tail deletion clamps end offset while remaining selection geometry stays non-zero.
    162    const element = setupControl(controlType, 'HelloWorld');
    163    const range = setupFormControlRange(element, 0, element.value.length);
    164    element.setRangeText('', 5, 10);
    165    assert_equals(element.value, 'Hello', 'value shrunk to Hello');
    166    assert_equals(range.endOffset, element.value.length, 'end offset clamped to new value length');
    167    const rectBox = rect(range, element);
    168    assert_greater_than(rectBox.width, 0, 'geometry still non-zero after tail deletion');
    169  }, `Tail deletion clamps end offset (${controlType})`);
    170 });
    171 
    172 test(() => {
    173  // Deleting selected text collapses to a caret with zero width.
    174  const inputElement = setupControl('input', 'ABCDE');
    175  const range = setupFormControlRange(inputElement, 1, 4);
    176  inputElement.setRangeText('', 1, 4);
    177  assert_true(range.collapsed, 'collapsed after deletion');
    178  assert_approx_equals(rect(range, inputElement).width, 0, 0.05, 'width should be 0');
    179 }, 'Deletion collapses selection (input explicit)');
    180 </script>