tor-browser

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

2d.text.measure.selection-rects.tentative.html (85753B)


      1 <!DOCTYPE html>
      2 <!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
      3 <meta charset="UTF-8">
      4 <title>OffscreenCanvas test: 2d.text.measure.selection-rects.tentative</title>
      5 <script src="/resources/testharness.js"></script>
      6 <script src="/resources/testharnessreport.js"></script>
      7 <script src="/html/canvas/resources/canvas-tests.js"></script>
      8 
      9 <h1>2d.text.measure.selection-rects.tentative</h1>
     10 
     11 <script>
     12 
     13 test(t => {
     14  const canvas = new OffscreenCanvas(100, 50);
     15  const ctx = canvas.getContext('2d');
     16 
     17  function placeAndSelectTextInDOM(text, from, to) {
     18    const el = document.createElement("div");
     19    el.innerHTML = text;
     20    el.style.font = '50px sans-serif';
     21    el.style.direction = 'ltr';
     22    el.style.textAlign = 'left';
     23    el.style.letterSpacing = '0px';
     24    document.body.appendChild(el);
     25 
     26    let range = document.createRange();
     27 
     28    range.setStart(el.childNodes[0], 0);
     29    range.setEnd(el.childNodes[0], text.length);
     30    const parent = range.getClientRects()[0];
     31    let width = 0;
     32    for (const rect of range.getClientRects()) {
     33      width += rect.width;
     34    }
     35 
     36    range.setStart(el.childNodes[0], from);
     37    range.setEnd(el.childNodes[0], to);
     38 
     39    let sel = window.getSelection();
     40    sel.removeAllRanges();
     41    sel.addRange(range);
     42 
     43    let sel_rects = sel.getRangeAt(0).getClientRects();
     44 
     45    document.body.removeChild(el);
     46 
     47    // Offset to the alignment point determined by textAlign.
     48    let text_align_dx;
     49    switch (el.style.textAlign) {
     50      case 'right':
     51        text_align_dx = width;
     52        break;
     53      case 'center':
     54        text_align_dx = width / 2;
     55        break;
     56      default:
     57        text_align_dx = 0;
     58    }
     59 
     60    for(let i = 0 ; i < sel_rects.length ; ++i) {
     61      sel_rects[i].x -= parent.x + text_align_dx;
     62      sel_rects[i].y -= parent.y;
     63    }
     64 
     65    return sel_rects;
     66  }
     67 
     68  function checkRectListsMatch(list_a, list_b) {
     69    _assertSame(list_a.length, list_b.length, "list_a.length", "list_b.length");
     70    for(let i = 0 ; i < list_a.length ; ++i) {
     71      assert_approx_equals(list_a[i].x, list_b[i].x, 1.0);
     72      assert_approx_equals(list_a[i].width, list_b[i].width, 1.0);
     73      assert_approx_equals(list_a[i].height, list_b[i].height, 1.0);
     74      // Y-position not tested here as getting the baseline for text in the
     75      // DOM is not straightforward.
     76    }
     77  }
     78 
     79  ctx.font = '50px sans-serif';
     80  ctx.direction = 'ltr';
     81  ctx.textAlign = 'left';
     82  ctx.letterSpacing = '0px';
     83 
     84  const kTexts = [
     85    'UNAVAILABLE',
     86    '🏁🎶🏁',
     87    ')(あ)(',
     88    '-abcd_',
     89    'איפה הספרייה?',
     90    'bidiמתמטיקה'
     91  ]
     92 
     93  for (text of kTexts) {
     94    const tm = ctx.measureText(text);
     95    // First character.
     96    checkRectListsMatch(
     97      tm.getSelectionRects(0, 1),
     98      placeAndSelectTextInDOM(text, 0, 1)
     99    );
    100    // Last character.
    101    checkRectListsMatch(
    102      tm.getSelectionRects(text.length - 1, text.length),
    103      placeAndSelectTextInDOM(text, text.length - 1, text.length)
    104    );
    105    // Whole string.
    106    checkRectListsMatch(
    107      tm.getSelectionRects(0, text.length),
    108      placeAndSelectTextInDOM(text, 0, text.length)
    109    );
    110    // Intermediate string.
    111    checkRectListsMatch(
    112      tm.getSelectionRects(1, text.length - 1),
    113      placeAndSelectTextInDOM(text, 1, text.length - 1)
    114    );
    115    // Invalid start > end string. Creates 0 width rectangle.
    116    checkRectListsMatch(
    117      tm.getSelectionRects(3, 2),
    118      placeAndSelectTextInDOM(text, 3, 2)
    119    );
    120    checkRectListsMatch(
    121      tm.getSelectionRects(1, 0),
    122      placeAndSelectTextInDOM(text, 1, 0)
    123    );
    124  }
    125 }, "Check that TextMetrics::getSelectionRects() matches its DOM equivalent, with direction ltr, text align left, 0px letter spacing, and no-directional-override.");
    126 
    127 test(t => {
    128  const canvas = new OffscreenCanvas(100, 50);
    129  const ctx = canvas.getContext('2d');
    130 
    131  function placeAndSelectTextInDOM(text, from, to) {
    132    const el = document.createElement("div");
    133    el.innerHTML = text;
    134    el.style.font = '50px sans-serif';
    135    el.style.direction = 'rtl';
    136    el.style.textAlign = 'left';
    137    el.style.letterSpacing = '0px';
    138    document.body.appendChild(el);
    139 
    140    let range = document.createRange();
    141 
    142    range.setStart(el.childNodes[0], 0);
    143    range.setEnd(el.childNodes[0], text.length);
    144    const parent = range.getClientRects()[0];
    145    let width = 0;
    146    for (const rect of range.getClientRects()) {
    147      width += rect.width;
    148    }
    149 
    150    range.setStart(el.childNodes[0], from);
    151    range.setEnd(el.childNodes[0], to);
    152 
    153    let sel = window.getSelection();
    154    sel.removeAllRanges();
    155    sel.addRange(range);
    156 
    157    let sel_rects = sel.getRangeAt(0).getClientRects();
    158 
    159    document.body.removeChild(el);
    160 
    161    // Offset to the alignment point determined by textAlign.
    162    let text_align_dx;
    163    switch (el.style.textAlign) {
    164      case 'right':
    165        text_align_dx = width;
    166        break;
    167      case 'center':
    168        text_align_dx = width / 2;
    169        break;
    170      default:
    171        text_align_dx = 0;
    172    }
    173 
    174    for(let i = 0 ; i < sel_rects.length ; ++i) {
    175      sel_rects[i].x -= parent.x + text_align_dx;
    176      sel_rects[i].y -= parent.y;
    177    }
    178 
    179    return sel_rects;
    180  }
    181 
    182  function checkRectListsMatch(list_a, list_b) {
    183    _assertSame(list_a.length, list_b.length, "list_a.length", "list_b.length");
    184    for(let i = 0 ; i < list_a.length ; ++i) {
    185      assert_approx_equals(list_a[i].x, list_b[i].x, 1.0);
    186      assert_approx_equals(list_a[i].width, list_b[i].width, 1.0);
    187      assert_approx_equals(list_a[i].height, list_b[i].height, 1.0);
    188      // Y-position not tested here as getting the baseline for text in the
    189      // DOM is not straightforward.
    190    }
    191  }
    192 
    193  ctx.font = '50px sans-serif';
    194  ctx.direction = 'rtl';
    195  ctx.textAlign = 'left';
    196  ctx.letterSpacing = '0px';
    197 
    198  const kTexts = [
    199    'UNAVAILABLE',
    200    '🏁🎶🏁',
    201    ')(あ)(',
    202    '-abcd_',
    203    'איפה הספרייה?',
    204    'bidiמתמטיקה'
    205  ]
    206 
    207  for (text of kTexts) {
    208    const tm = ctx.measureText(text);
    209    // First character.
    210    checkRectListsMatch(
    211      tm.getSelectionRects(0, 1),
    212      placeAndSelectTextInDOM(text, 0, 1)
    213    );
    214    // Last character.
    215    checkRectListsMatch(
    216      tm.getSelectionRects(text.length - 1, text.length),
    217      placeAndSelectTextInDOM(text, text.length - 1, text.length)
    218    );
    219    // Whole string.
    220    checkRectListsMatch(
    221      tm.getSelectionRects(0, text.length),
    222      placeAndSelectTextInDOM(text, 0, text.length)
    223    );
    224    // Intermediate string.
    225    checkRectListsMatch(
    226      tm.getSelectionRects(1, text.length - 1),
    227      placeAndSelectTextInDOM(text, 1, text.length - 1)
    228    );
    229    // Invalid start > end string. Creates 0 width rectangle.
    230    checkRectListsMatch(
    231      tm.getSelectionRects(3, 2),
    232      placeAndSelectTextInDOM(text, 3, 2)
    233    );
    234    checkRectListsMatch(
    235      tm.getSelectionRects(1, 0),
    236      placeAndSelectTextInDOM(text, 1, 0)
    237    );
    238  }
    239 }, "Check that TextMetrics::getSelectionRects() matches its DOM equivalent, with direction rtl, text align left, 0px letter spacing, and no-directional-override.");
    240 
    241 test(t => {
    242  const canvas = new OffscreenCanvas(100, 50);
    243  const ctx = canvas.getContext('2d');
    244 
    245  function placeAndSelectTextInDOM(text, from, to) {
    246    const el = document.createElement("div");
    247    el.innerHTML = text;
    248    el.style.font = '50px sans-serif';
    249    el.style.direction = 'ltr';
    250    el.style.textAlign = 'center';
    251    el.style.letterSpacing = '0px';
    252    document.body.appendChild(el);
    253 
    254    let range = document.createRange();
    255 
    256    range.setStart(el.childNodes[0], 0);
    257    range.setEnd(el.childNodes[0], text.length);
    258    const parent = range.getClientRects()[0];
    259    let width = 0;
    260    for (const rect of range.getClientRects()) {
    261      width += rect.width;
    262    }
    263 
    264    range.setStart(el.childNodes[0], from);
    265    range.setEnd(el.childNodes[0], to);
    266 
    267    let sel = window.getSelection();
    268    sel.removeAllRanges();
    269    sel.addRange(range);
    270 
    271    let sel_rects = sel.getRangeAt(0).getClientRects();
    272 
    273    document.body.removeChild(el);
    274 
    275    // Offset to the alignment point determined by textAlign.
    276    let text_align_dx;
    277    switch (el.style.textAlign) {
    278      case 'right':
    279        text_align_dx = width;
    280        break;
    281      case 'center':
    282        text_align_dx = width / 2;
    283        break;
    284      default:
    285        text_align_dx = 0;
    286    }
    287 
    288    for(let i = 0 ; i < sel_rects.length ; ++i) {
    289      sel_rects[i].x -= parent.x + text_align_dx;
    290      sel_rects[i].y -= parent.y;
    291    }
    292 
    293    return sel_rects;
    294  }
    295 
    296  function checkRectListsMatch(list_a, list_b) {
    297    _assertSame(list_a.length, list_b.length, "list_a.length", "list_b.length");
    298    for(let i = 0 ; i < list_a.length ; ++i) {
    299      assert_approx_equals(list_a[i].x, list_b[i].x, 1.0);
    300      assert_approx_equals(list_a[i].width, list_b[i].width, 1.0);
    301      assert_approx_equals(list_a[i].height, list_b[i].height, 1.0);
    302      // Y-position not tested here as getting the baseline for text in the
    303      // DOM is not straightforward.
    304    }
    305  }
    306 
    307  ctx.font = '50px sans-serif';
    308  ctx.direction = 'ltr';
    309  ctx.textAlign = 'center';
    310  ctx.letterSpacing = '0px';
    311 
    312  const kTexts = [
    313    'UNAVAILABLE',
    314    '🏁🎶🏁',
    315    ')(あ)(',
    316    '-abcd_',
    317    'איפה הספרייה?',
    318    'bidiמתמטיקה'
    319  ]
    320 
    321  for (text of kTexts) {
    322    const tm = ctx.measureText(text);
    323    // First character.
    324    checkRectListsMatch(
    325      tm.getSelectionRects(0, 1),
    326      placeAndSelectTextInDOM(text, 0, 1)
    327    );
    328    // Last character.
    329    checkRectListsMatch(
    330      tm.getSelectionRects(text.length - 1, text.length),
    331      placeAndSelectTextInDOM(text, text.length - 1, text.length)
    332    );
    333    // Whole string.
    334    checkRectListsMatch(
    335      tm.getSelectionRects(0, text.length),
    336      placeAndSelectTextInDOM(text, 0, text.length)
    337    );
    338    // Intermediate string.
    339    checkRectListsMatch(
    340      tm.getSelectionRects(1, text.length - 1),
    341      placeAndSelectTextInDOM(text, 1, text.length - 1)
    342    );
    343    // Invalid start > end string. Creates 0 width rectangle.
    344    checkRectListsMatch(
    345      tm.getSelectionRects(3, 2),
    346      placeAndSelectTextInDOM(text, 3, 2)
    347    );
    348    checkRectListsMatch(
    349      tm.getSelectionRects(1, 0),
    350      placeAndSelectTextInDOM(text, 1, 0)
    351    );
    352  }
    353 }, "Check that TextMetrics::getSelectionRects() matches its DOM equivalent, with direction ltr, text align center, 0px letter spacing, and no-directional-override.");
    354 
    355 test(t => {
    356  const canvas = new OffscreenCanvas(100, 50);
    357  const ctx = canvas.getContext('2d');
    358 
    359  function placeAndSelectTextInDOM(text, from, to) {
    360    const el = document.createElement("div");
    361    el.innerHTML = text;
    362    el.style.font = '50px sans-serif';
    363    el.style.direction = 'rtl';
    364    el.style.textAlign = 'center';
    365    el.style.letterSpacing = '0px';
    366    document.body.appendChild(el);
    367 
    368    let range = document.createRange();
    369 
    370    range.setStart(el.childNodes[0], 0);
    371    range.setEnd(el.childNodes[0], text.length);
    372    const parent = range.getClientRects()[0];
    373    let width = 0;
    374    for (const rect of range.getClientRects()) {
    375      width += rect.width;
    376    }
    377 
    378    range.setStart(el.childNodes[0], from);
    379    range.setEnd(el.childNodes[0], to);
    380 
    381    let sel = window.getSelection();
    382    sel.removeAllRanges();
    383    sel.addRange(range);
    384 
    385    let sel_rects = sel.getRangeAt(0).getClientRects();
    386 
    387    document.body.removeChild(el);
    388 
    389    // Offset to the alignment point determined by textAlign.
    390    let text_align_dx;
    391    switch (el.style.textAlign) {
    392      case 'right':
    393        text_align_dx = width;
    394        break;
    395      case 'center':
    396        text_align_dx = width / 2;
    397        break;
    398      default:
    399        text_align_dx = 0;
    400    }
    401 
    402    for(let i = 0 ; i < sel_rects.length ; ++i) {
    403      sel_rects[i].x -= parent.x + text_align_dx;
    404      sel_rects[i].y -= parent.y;
    405    }
    406 
    407    return sel_rects;
    408  }
    409 
    410  function checkRectListsMatch(list_a, list_b) {
    411    _assertSame(list_a.length, list_b.length, "list_a.length", "list_b.length");
    412    for(let i = 0 ; i < list_a.length ; ++i) {
    413      assert_approx_equals(list_a[i].x, list_b[i].x, 1.0);
    414      assert_approx_equals(list_a[i].width, list_b[i].width, 1.0);
    415      assert_approx_equals(list_a[i].height, list_b[i].height, 1.0);
    416      // Y-position not tested here as getting the baseline for text in the
    417      // DOM is not straightforward.
    418    }
    419  }
    420 
    421  ctx.font = '50px sans-serif';
    422  ctx.direction = 'rtl';
    423  ctx.textAlign = 'center';
    424  ctx.letterSpacing = '0px';
    425 
    426  const kTexts = [
    427    'UNAVAILABLE',
    428    '🏁🎶🏁',
    429    ')(あ)(',
    430    '-abcd_',
    431    'איפה הספרייה?',
    432    'bidiמתמטיקה'
    433  ]
    434 
    435  for (text of kTexts) {
    436    const tm = ctx.measureText(text);
    437    // First character.
    438    checkRectListsMatch(
    439      tm.getSelectionRects(0, 1),
    440      placeAndSelectTextInDOM(text, 0, 1)
    441    );
    442    // Last character.
    443    checkRectListsMatch(
    444      tm.getSelectionRects(text.length - 1, text.length),
    445      placeAndSelectTextInDOM(text, text.length - 1, text.length)
    446    );
    447    // Whole string.
    448    checkRectListsMatch(
    449      tm.getSelectionRects(0, text.length),
    450      placeAndSelectTextInDOM(text, 0, text.length)
    451    );
    452    // Intermediate string.
    453    checkRectListsMatch(
    454      tm.getSelectionRects(1, text.length - 1),
    455      placeAndSelectTextInDOM(text, 1, text.length - 1)
    456    );
    457    // Invalid start > end string. Creates 0 width rectangle.
    458    checkRectListsMatch(
    459      tm.getSelectionRects(3, 2),
    460      placeAndSelectTextInDOM(text, 3, 2)
    461    );
    462    checkRectListsMatch(
    463      tm.getSelectionRects(1, 0),
    464      placeAndSelectTextInDOM(text, 1, 0)
    465    );
    466  }
    467 }, "Check that TextMetrics::getSelectionRects() matches its DOM equivalent, with direction rtl, text align center, 0px letter spacing, and no-directional-override.");
    468 
    469 test(t => {
    470  const canvas = new OffscreenCanvas(100, 50);
    471  const ctx = canvas.getContext('2d');
    472 
    473  function placeAndSelectTextInDOM(text, from, to) {
    474    const el = document.createElement("div");
    475    el.innerHTML = text;
    476    el.style.font = '50px sans-serif';
    477    el.style.direction = 'ltr';
    478    el.style.textAlign = 'right';
    479    el.style.letterSpacing = '0px';
    480    document.body.appendChild(el);
    481 
    482    let range = document.createRange();
    483 
    484    range.setStart(el.childNodes[0], 0);
    485    range.setEnd(el.childNodes[0], text.length);
    486    const parent = range.getClientRects()[0];
    487    let width = 0;
    488    for (const rect of range.getClientRects()) {
    489      width += rect.width;
    490    }
    491 
    492    range.setStart(el.childNodes[0], from);
    493    range.setEnd(el.childNodes[0], to);
    494 
    495    let sel = window.getSelection();
    496    sel.removeAllRanges();
    497    sel.addRange(range);
    498 
    499    let sel_rects = sel.getRangeAt(0).getClientRects();
    500 
    501    document.body.removeChild(el);
    502 
    503    // Offset to the alignment point determined by textAlign.
    504    let text_align_dx;
    505    switch (el.style.textAlign) {
    506      case 'right':
    507        text_align_dx = width;
    508        break;
    509      case 'center':
    510        text_align_dx = width / 2;
    511        break;
    512      default:
    513        text_align_dx = 0;
    514    }
    515 
    516    for(let i = 0 ; i < sel_rects.length ; ++i) {
    517      sel_rects[i].x -= parent.x + text_align_dx;
    518      sel_rects[i].y -= parent.y;
    519    }
    520 
    521    return sel_rects;
    522  }
    523 
    524  function checkRectListsMatch(list_a, list_b) {
    525    _assertSame(list_a.length, list_b.length, "list_a.length", "list_b.length");
    526    for(let i = 0 ; i < list_a.length ; ++i) {
    527      assert_approx_equals(list_a[i].x, list_b[i].x, 1.0);
    528      assert_approx_equals(list_a[i].width, list_b[i].width, 1.0);
    529      assert_approx_equals(list_a[i].height, list_b[i].height, 1.0);
    530      // Y-position not tested here as getting the baseline for text in the
    531      // DOM is not straightforward.
    532    }
    533  }
    534 
    535  ctx.font = '50px sans-serif';
    536  ctx.direction = 'ltr';
    537  ctx.textAlign = 'right';
    538  ctx.letterSpacing = '0px';
    539 
    540  const kTexts = [
    541    'UNAVAILABLE',
    542    '🏁🎶🏁',
    543    ')(あ)(',
    544    '-abcd_',
    545    'איפה הספרייה?',
    546    'bidiמתמטיקה'
    547  ]
    548 
    549  for (text of kTexts) {
    550    const tm = ctx.measureText(text);
    551    // First character.
    552    checkRectListsMatch(
    553      tm.getSelectionRects(0, 1),
    554      placeAndSelectTextInDOM(text, 0, 1)
    555    );
    556    // Last character.
    557    checkRectListsMatch(
    558      tm.getSelectionRects(text.length - 1, text.length),
    559      placeAndSelectTextInDOM(text, text.length - 1, text.length)
    560    );
    561    // Whole string.
    562    checkRectListsMatch(
    563      tm.getSelectionRects(0, text.length),
    564      placeAndSelectTextInDOM(text, 0, text.length)
    565    );
    566    // Intermediate string.
    567    checkRectListsMatch(
    568      tm.getSelectionRects(1, text.length - 1),
    569      placeAndSelectTextInDOM(text, 1, text.length - 1)
    570    );
    571    // Invalid start > end string. Creates 0 width rectangle.
    572    checkRectListsMatch(
    573      tm.getSelectionRects(3, 2),
    574      placeAndSelectTextInDOM(text, 3, 2)
    575    );
    576    checkRectListsMatch(
    577      tm.getSelectionRects(1, 0),
    578      placeAndSelectTextInDOM(text, 1, 0)
    579    );
    580  }
    581 }, "Check that TextMetrics::getSelectionRects() matches its DOM equivalent, with direction ltr, text align right, 0px letter spacing, and no-directional-override.");
    582 
    583 test(t => {
    584  const canvas = new OffscreenCanvas(100, 50);
    585  const ctx = canvas.getContext('2d');
    586 
    587  function placeAndSelectTextInDOM(text, from, to) {
    588    const el = document.createElement("div");
    589    el.innerHTML = text;
    590    el.style.font = '50px sans-serif';
    591    el.style.direction = 'rtl';
    592    el.style.textAlign = 'right';
    593    el.style.letterSpacing = '0px';
    594    document.body.appendChild(el);
    595 
    596    let range = document.createRange();
    597 
    598    range.setStart(el.childNodes[0], 0);
    599    range.setEnd(el.childNodes[0], text.length);
    600    const parent = range.getClientRects()[0];
    601    let width = 0;
    602    for (const rect of range.getClientRects()) {
    603      width += rect.width;
    604    }
    605 
    606    range.setStart(el.childNodes[0], from);
    607    range.setEnd(el.childNodes[0], to);
    608 
    609    let sel = window.getSelection();
    610    sel.removeAllRanges();
    611    sel.addRange(range);
    612 
    613    let sel_rects = sel.getRangeAt(0).getClientRects();
    614 
    615    document.body.removeChild(el);
    616 
    617    // Offset to the alignment point determined by textAlign.
    618    let text_align_dx;
    619    switch (el.style.textAlign) {
    620      case 'right':
    621        text_align_dx = width;
    622        break;
    623      case 'center':
    624        text_align_dx = width / 2;
    625        break;
    626      default:
    627        text_align_dx = 0;
    628    }
    629 
    630    for(let i = 0 ; i < sel_rects.length ; ++i) {
    631      sel_rects[i].x -= parent.x + text_align_dx;
    632      sel_rects[i].y -= parent.y;
    633    }
    634 
    635    return sel_rects;
    636  }
    637 
    638  function checkRectListsMatch(list_a, list_b) {
    639    _assertSame(list_a.length, list_b.length, "list_a.length", "list_b.length");
    640    for(let i = 0 ; i < list_a.length ; ++i) {
    641      assert_approx_equals(list_a[i].x, list_b[i].x, 1.0);
    642      assert_approx_equals(list_a[i].width, list_b[i].width, 1.0);
    643      assert_approx_equals(list_a[i].height, list_b[i].height, 1.0);
    644      // Y-position not tested here as getting the baseline for text in the
    645      // DOM is not straightforward.
    646    }
    647  }
    648 
    649  ctx.font = '50px sans-serif';
    650  ctx.direction = 'rtl';
    651  ctx.textAlign = 'right';
    652  ctx.letterSpacing = '0px';
    653 
    654  const kTexts = [
    655    'UNAVAILABLE',
    656    '🏁🎶🏁',
    657    ')(あ)(',
    658    '-abcd_',
    659    'איפה הספרייה?',
    660    'bidiמתמטיקה'
    661  ]
    662 
    663  for (text of kTexts) {
    664    const tm = ctx.measureText(text);
    665    // First character.
    666    checkRectListsMatch(
    667      tm.getSelectionRects(0, 1),
    668      placeAndSelectTextInDOM(text, 0, 1)
    669    );
    670    // Last character.
    671    checkRectListsMatch(
    672      tm.getSelectionRects(text.length - 1, text.length),
    673      placeAndSelectTextInDOM(text, text.length - 1, text.length)
    674    );
    675    // Whole string.
    676    checkRectListsMatch(
    677      tm.getSelectionRects(0, text.length),
    678      placeAndSelectTextInDOM(text, 0, text.length)
    679    );
    680    // Intermediate string.
    681    checkRectListsMatch(
    682      tm.getSelectionRects(1, text.length - 1),
    683      placeAndSelectTextInDOM(text, 1, text.length - 1)
    684    );
    685    // Invalid start > end string. Creates 0 width rectangle.
    686    checkRectListsMatch(
    687      tm.getSelectionRects(3, 2),
    688      placeAndSelectTextInDOM(text, 3, 2)
    689    );
    690    checkRectListsMatch(
    691      tm.getSelectionRects(1, 0),
    692      placeAndSelectTextInDOM(text, 1, 0)
    693    );
    694  }
    695 }, "Check that TextMetrics::getSelectionRects() matches its DOM equivalent, with direction rtl, text align right, 0px letter spacing, and no-directional-override.");
    696 
    697 test(t => {
    698  const canvas = new OffscreenCanvas(100, 50);
    699  const ctx = canvas.getContext('2d');
    700 
    701  function placeAndSelectTextInDOM(text, from, to) {
    702    const el = document.createElement("div");
    703    el.innerHTML = text;
    704    el.style.font = '50px sans-serif';
    705    el.style.direction = 'ltr';
    706    el.style.textAlign = 'left';
    707    el.style.letterSpacing = '10px';
    708    document.body.appendChild(el);
    709 
    710    let range = document.createRange();
    711 
    712    range.setStart(el.childNodes[0], 0);
    713    range.setEnd(el.childNodes[0], text.length);
    714    const parent = range.getClientRects()[0];
    715    let width = 0;
    716    for (const rect of range.getClientRects()) {
    717      width += rect.width;
    718    }
    719 
    720    range.setStart(el.childNodes[0], from);
    721    range.setEnd(el.childNodes[0], to);
    722 
    723    let sel = window.getSelection();
    724    sel.removeAllRanges();
    725    sel.addRange(range);
    726 
    727    let sel_rects = sel.getRangeAt(0).getClientRects();
    728 
    729    document.body.removeChild(el);
    730 
    731    // Offset to the alignment point determined by textAlign.
    732    let text_align_dx;
    733    switch (el.style.textAlign) {
    734      case 'right':
    735        text_align_dx = width;
    736        break;
    737      case 'center':
    738        text_align_dx = width / 2;
    739        break;
    740      default:
    741        text_align_dx = 0;
    742    }
    743 
    744    for(let i = 0 ; i < sel_rects.length ; ++i) {
    745      sel_rects[i].x -= parent.x + text_align_dx;
    746      sel_rects[i].y -= parent.y;
    747    }
    748 
    749    return sel_rects;
    750  }
    751 
    752  function checkRectListsMatch(list_a, list_b) {
    753    _assertSame(list_a.length, list_b.length, "list_a.length", "list_b.length");
    754    for(let i = 0 ; i < list_a.length ; ++i) {
    755      assert_approx_equals(list_a[i].x, list_b[i].x, 1.0);
    756      assert_approx_equals(list_a[i].width, list_b[i].width, 1.0);
    757      assert_approx_equals(list_a[i].height, list_b[i].height, 1.0);
    758      // Y-position not tested here as getting the baseline for text in the
    759      // DOM is not straightforward.
    760    }
    761  }
    762 
    763  ctx.font = '50px sans-serif';
    764  ctx.direction = 'ltr';
    765  ctx.textAlign = 'left';
    766  ctx.letterSpacing = '10px';
    767 
    768  const kTexts = [
    769    'UNAVAILABLE',
    770    '🏁🎶🏁',
    771    ')(あ)(',
    772    '-abcd_',
    773    'איפה הספרייה?',
    774    'bidiמתמטיקה'
    775  ]
    776 
    777  for (text of kTexts) {
    778    const tm = ctx.measureText(text);
    779    // First character.
    780    checkRectListsMatch(
    781      tm.getSelectionRects(0, 1),
    782      placeAndSelectTextInDOM(text, 0, 1)
    783    );
    784    // Last character.
    785    checkRectListsMatch(
    786      tm.getSelectionRects(text.length - 1, text.length),
    787      placeAndSelectTextInDOM(text, text.length - 1, text.length)
    788    );
    789    // Whole string.
    790    checkRectListsMatch(
    791      tm.getSelectionRects(0, text.length),
    792      placeAndSelectTextInDOM(text, 0, text.length)
    793    );
    794    // Intermediate string.
    795    checkRectListsMatch(
    796      tm.getSelectionRects(1, text.length - 1),
    797      placeAndSelectTextInDOM(text, 1, text.length - 1)
    798    );
    799    // Invalid start > end string. Creates 0 width rectangle.
    800    checkRectListsMatch(
    801      tm.getSelectionRects(3, 2),
    802      placeAndSelectTextInDOM(text, 3, 2)
    803    );
    804    checkRectListsMatch(
    805      tm.getSelectionRects(1, 0),
    806      placeAndSelectTextInDOM(text, 1, 0)
    807    );
    808  }
    809 }, "Check that TextMetrics::getSelectionRects() matches its DOM equivalent, with direction ltr, text align left, 10px letter spacing, and no-directional-override.");
    810 
    811 test(t => {
    812  const canvas = new OffscreenCanvas(100, 50);
    813  const ctx = canvas.getContext('2d');
    814 
    815  function placeAndSelectTextInDOM(text, from, to) {
    816    const el = document.createElement("div");
    817    el.innerHTML = text;
    818    el.style.font = '50px sans-serif';
    819    el.style.direction = 'rtl';
    820    el.style.textAlign = 'left';
    821    el.style.letterSpacing = '10px';
    822    document.body.appendChild(el);
    823 
    824    let range = document.createRange();
    825 
    826    range.setStart(el.childNodes[0], 0);
    827    range.setEnd(el.childNodes[0], text.length);
    828    const parent = range.getClientRects()[0];
    829    let width = 0;
    830    for (const rect of range.getClientRects()) {
    831      width += rect.width;
    832    }
    833 
    834    range.setStart(el.childNodes[0], from);
    835    range.setEnd(el.childNodes[0], to);
    836 
    837    let sel = window.getSelection();
    838    sel.removeAllRanges();
    839    sel.addRange(range);
    840 
    841    let sel_rects = sel.getRangeAt(0).getClientRects();
    842 
    843    document.body.removeChild(el);
    844 
    845    // Offset to the alignment point determined by textAlign.
    846    let text_align_dx;
    847    switch (el.style.textAlign) {
    848      case 'right':
    849        text_align_dx = width;
    850        break;
    851      case 'center':
    852        text_align_dx = width / 2;
    853        break;
    854      default:
    855        text_align_dx = 0;
    856    }
    857 
    858    for(let i = 0 ; i < sel_rects.length ; ++i) {
    859      sel_rects[i].x -= parent.x + text_align_dx;
    860      sel_rects[i].y -= parent.y;
    861    }
    862 
    863    return sel_rects;
    864  }
    865 
    866  function checkRectListsMatch(list_a, list_b) {
    867    _assertSame(list_a.length, list_b.length, "list_a.length", "list_b.length");
    868    for(let i = 0 ; i < list_a.length ; ++i) {
    869      assert_approx_equals(list_a[i].x, list_b[i].x, 1.0);
    870      assert_approx_equals(list_a[i].width, list_b[i].width, 1.0);
    871      assert_approx_equals(list_a[i].height, list_b[i].height, 1.0);
    872      // Y-position not tested here as getting the baseline for text in the
    873      // DOM is not straightforward.
    874    }
    875  }
    876 
    877  ctx.font = '50px sans-serif';
    878  ctx.direction = 'rtl';
    879  ctx.textAlign = 'left';
    880  ctx.letterSpacing = '10px';
    881 
    882  const kTexts = [
    883    'UNAVAILABLE',
    884    '🏁🎶🏁',
    885    ')(あ)(',
    886    '-abcd_',
    887    'איפה הספרייה?',
    888    'bidiמתמטיקה'
    889  ]
    890 
    891  for (text of kTexts) {
    892    const tm = ctx.measureText(text);
    893    // First character.
    894    checkRectListsMatch(
    895      tm.getSelectionRects(0, 1),
    896      placeAndSelectTextInDOM(text, 0, 1)
    897    );
    898    // Last character.
    899    checkRectListsMatch(
    900      tm.getSelectionRects(text.length - 1, text.length),
    901      placeAndSelectTextInDOM(text, text.length - 1, text.length)
    902    );
    903    // Whole string.
    904    checkRectListsMatch(
    905      tm.getSelectionRects(0, text.length),
    906      placeAndSelectTextInDOM(text, 0, text.length)
    907    );
    908    // Intermediate string.
    909    checkRectListsMatch(
    910      tm.getSelectionRects(1, text.length - 1),
    911      placeAndSelectTextInDOM(text, 1, text.length - 1)
    912    );
    913    // Invalid start > end string. Creates 0 width rectangle.
    914    checkRectListsMatch(
    915      tm.getSelectionRects(3, 2),
    916      placeAndSelectTextInDOM(text, 3, 2)
    917    );
    918    checkRectListsMatch(
    919      tm.getSelectionRects(1, 0),
    920      placeAndSelectTextInDOM(text, 1, 0)
    921    );
    922  }
    923 }, "Check that TextMetrics::getSelectionRects() matches its DOM equivalent, with direction rtl, text align left, 10px letter spacing, and no-directional-override.");
    924 
    925 test(t => {
    926  const canvas = new OffscreenCanvas(100, 50);
    927  const ctx = canvas.getContext('2d');
    928 
    929  function placeAndSelectTextInDOM(text, from, to) {
    930    const el = document.createElement("div");
    931    el.innerHTML = text;
    932    el.style.font = '50px sans-serif';
    933    el.style.direction = 'ltr';
    934    el.style.textAlign = 'center';
    935    el.style.letterSpacing = '10px';
    936    document.body.appendChild(el);
    937 
    938    let range = document.createRange();
    939 
    940    range.setStart(el.childNodes[0], 0);
    941    range.setEnd(el.childNodes[0], text.length);
    942    const parent = range.getClientRects()[0];
    943    let width = 0;
    944    for (const rect of range.getClientRects()) {
    945      width += rect.width;
    946    }
    947 
    948    range.setStart(el.childNodes[0], from);
    949    range.setEnd(el.childNodes[0], to);
    950 
    951    let sel = window.getSelection();
    952    sel.removeAllRanges();
    953    sel.addRange(range);
    954 
    955    let sel_rects = sel.getRangeAt(0).getClientRects();
    956 
    957    document.body.removeChild(el);
    958 
    959    // Offset to the alignment point determined by textAlign.
    960    let text_align_dx;
    961    switch (el.style.textAlign) {
    962      case 'right':
    963        text_align_dx = width;
    964        break;
    965      case 'center':
    966        text_align_dx = width / 2;
    967        break;
    968      default:
    969        text_align_dx = 0;
    970    }
    971 
    972    for(let i = 0 ; i < sel_rects.length ; ++i) {
    973      sel_rects[i].x -= parent.x + text_align_dx;
    974      sel_rects[i].y -= parent.y;
    975    }
    976 
    977    return sel_rects;
    978  }
    979 
    980  function checkRectListsMatch(list_a, list_b) {
    981    _assertSame(list_a.length, list_b.length, "list_a.length", "list_b.length");
    982    for(let i = 0 ; i < list_a.length ; ++i) {
    983      assert_approx_equals(list_a[i].x, list_b[i].x, 1.0);
    984      assert_approx_equals(list_a[i].width, list_b[i].width, 1.0);
    985      assert_approx_equals(list_a[i].height, list_b[i].height, 1.0);
    986      // Y-position not tested here as getting the baseline for text in the
    987      // DOM is not straightforward.
    988    }
    989  }
    990 
    991  ctx.font = '50px sans-serif';
    992  ctx.direction = 'ltr';
    993  ctx.textAlign = 'center';
    994  ctx.letterSpacing = '10px';
    995 
    996  const kTexts = [
    997    'UNAVAILABLE',
    998    '🏁🎶🏁',
    999    ')(あ)(',
   1000    '-abcd_',
   1001    'איפה הספרייה?',
   1002    'bidiמתמטיקה'
   1003  ]
   1004 
   1005  for (text of kTexts) {
   1006    const tm = ctx.measureText(text);
   1007    // First character.
   1008    checkRectListsMatch(
   1009      tm.getSelectionRects(0, 1),
   1010      placeAndSelectTextInDOM(text, 0, 1)
   1011    );
   1012    // Last character.
   1013    checkRectListsMatch(
   1014      tm.getSelectionRects(text.length - 1, text.length),
   1015      placeAndSelectTextInDOM(text, text.length - 1, text.length)
   1016    );
   1017    // Whole string.
   1018    checkRectListsMatch(
   1019      tm.getSelectionRects(0, text.length),
   1020      placeAndSelectTextInDOM(text, 0, text.length)
   1021    );
   1022    // Intermediate string.
   1023    checkRectListsMatch(
   1024      tm.getSelectionRects(1, text.length - 1),
   1025      placeAndSelectTextInDOM(text, 1, text.length - 1)
   1026    );
   1027    // Invalid start > end string. Creates 0 width rectangle.
   1028    checkRectListsMatch(
   1029      tm.getSelectionRects(3, 2),
   1030      placeAndSelectTextInDOM(text, 3, 2)
   1031    );
   1032    checkRectListsMatch(
   1033      tm.getSelectionRects(1, 0),
   1034      placeAndSelectTextInDOM(text, 1, 0)
   1035    );
   1036  }
   1037 }, "Check that TextMetrics::getSelectionRects() matches its DOM equivalent, with direction ltr, text align center, 10px letter spacing, and no-directional-override.");
   1038 
   1039 test(t => {
   1040  const canvas = new OffscreenCanvas(100, 50);
   1041  const ctx = canvas.getContext('2d');
   1042 
   1043  function placeAndSelectTextInDOM(text, from, to) {
   1044    const el = document.createElement("div");
   1045    el.innerHTML = text;
   1046    el.style.font = '50px sans-serif';
   1047    el.style.direction = 'rtl';
   1048    el.style.textAlign = 'center';
   1049    el.style.letterSpacing = '10px';
   1050    document.body.appendChild(el);
   1051 
   1052    let range = document.createRange();
   1053 
   1054    range.setStart(el.childNodes[0], 0);
   1055    range.setEnd(el.childNodes[0], text.length);
   1056    const parent = range.getClientRects()[0];
   1057    let width = 0;
   1058    for (const rect of range.getClientRects()) {
   1059      width += rect.width;
   1060    }
   1061 
   1062    range.setStart(el.childNodes[0], from);
   1063    range.setEnd(el.childNodes[0], to);
   1064 
   1065    let sel = window.getSelection();
   1066    sel.removeAllRanges();
   1067    sel.addRange(range);
   1068 
   1069    let sel_rects = sel.getRangeAt(0).getClientRects();
   1070 
   1071    document.body.removeChild(el);
   1072 
   1073    // Offset to the alignment point determined by textAlign.
   1074    let text_align_dx;
   1075    switch (el.style.textAlign) {
   1076      case 'right':
   1077        text_align_dx = width;
   1078        break;
   1079      case 'center':
   1080        text_align_dx = width / 2;
   1081        break;
   1082      default:
   1083        text_align_dx = 0;
   1084    }
   1085 
   1086    for(let i = 0 ; i < sel_rects.length ; ++i) {
   1087      sel_rects[i].x -= parent.x + text_align_dx;
   1088      sel_rects[i].y -= parent.y;
   1089    }
   1090 
   1091    return sel_rects;
   1092  }
   1093 
   1094  function checkRectListsMatch(list_a, list_b) {
   1095    _assertSame(list_a.length, list_b.length, "list_a.length", "list_b.length");
   1096    for(let i = 0 ; i < list_a.length ; ++i) {
   1097      assert_approx_equals(list_a[i].x, list_b[i].x, 1.0);
   1098      assert_approx_equals(list_a[i].width, list_b[i].width, 1.0);
   1099      assert_approx_equals(list_a[i].height, list_b[i].height, 1.0);
   1100      // Y-position not tested here as getting the baseline for text in the
   1101      // DOM is not straightforward.
   1102    }
   1103  }
   1104 
   1105  ctx.font = '50px sans-serif';
   1106  ctx.direction = 'rtl';
   1107  ctx.textAlign = 'center';
   1108  ctx.letterSpacing = '10px';
   1109 
   1110  const kTexts = [
   1111    'UNAVAILABLE',
   1112    '🏁🎶🏁',
   1113    ')(あ)(',
   1114    '-abcd_',
   1115    'איפה הספרייה?',
   1116    'bidiמתמטיקה'
   1117  ]
   1118 
   1119  for (text of kTexts) {
   1120    const tm = ctx.measureText(text);
   1121    // First character.
   1122    checkRectListsMatch(
   1123      tm.getSelectionRects(0, 1),
   1124      placeAndSelectTextInDOM(text, 0, 1)
   1125    );
   1126    // Last character.
   1127    checkRectListsMatch(
   1128      tm.getSelectionRects(text.length - 1, text.length),
   1129      placeAndSelectTextInDOM(text, text.length - 1, text.length)
   1130    );
   1131    // Whole string.
   1132    checkRectListsMatch(
   1133      tm.getSelectionRects(0, text.length),
   1134      placeAndSelectTextInDOM(text, 0, text.length)
   1135    );
   1136    // Intermediate string.
   1137    checkRectListsMatch(
   1138      tm.getSelectionRects(1, text.length - 1),
   1139      placeAndSelectTextInDOM(text, 1, text.length - 1)
   1140    );
   1141    // Invalid start > end string. Creates 0 width rectangle.
   1142    checkRectListsMatch(
   1143      tm.getSelectionRects(3, 2),
   1144      placeAndSelectTextInDOM(text, 3, 2)
   1145    );
   1146    checkRectListsMatch(
   1147      tm.getSelectionRects(1, 0),
   1148      placeAndSelectTextInDOM(text, 1, 0)
   1149    );
   1150  }
   1151 }, "Check that TextMetrics::getSelectionRects() matches its DOM equivalent, with direction rtl, text align center, 10px letter spacing, and no-directional-override.");
   1152 
   1153 test(t => {
   1154  const canvas = new OffscreenCanvas(100, 50);
   1155  const ctx = canvas.getContext('2d');
   1156 
   1157  function placeAndSelectTextInDOM(text, from, to) {
   1158    const el = document.createElement("div");
   1159    el.innerHTML = text;
   1160    el.style.font = '50px sans-serif';
   1161    el.style.direction = 'ltr';
   1162    el.style.textAlign = 'right';
   1163    el.style.letterSpacing = '10px';
   1164    document.body.appendChild(el);
   1165 
   1166    let range = document.createRange();
   1167 
   1168    range.setStart(el.childNodes[0], 0);
   1169    range.setEnd(el.childNodes[0], text.length);
   1170    const parent = range.getClientRects()[0];
   1171    let width = 0;
   1172    for (const rect of range.getClientRects()) {
   1173      width += rect.width;
   1174    }
   1175 
   1176    range.setStart(el.childNodes[0], from);
   1177    range.setEnd(el.childNodes[0], to);
   1178 
   1179    let sel = window.getSelection();
   1180    sel.removeAllRanges();
   1181    sel.addRange(range);
   1182 
   1183    let sel_rects = sel.getRangeAt(0).getClientRects();
   1184 
   1185    document.body.removeChild(el);
   1186 
   1187    // Offset to the alignment point determined by textAlign.
   1188    let text_align_dx;
   1189    switch (el.style.textAlign) {
   1190      case 'right':
   1191        text_align_dx = width;
   1192        break;
   1193      case 'center':
   1194        text_align_dx = width / 2;
   1195        break;
   1196      default:
   1197        text_align_dx = 0;
   1198    }
   1199 
   1200    for(let i = 0 ; i < sel_rects.length ; ++i) {
   1201      sel_rects[i].x -= parent.x + text_align_dx;
   1202      sel_rects[i].y -= parent.y;
   1203    }
   1204 
   1205    return sel_rects;
   1206  }
   1207 
   1208  function checkRectListsMatch(list_a, list_b) {
   1209    _assertSame(list_a.length, list_b.length, "list_a.length", "list_b.length");
   1210    for(let i = 0 ; i < list_a.length ; ++i) {
   1211      assert_approx_equals(list_a[i].x, list_b[i].x, 1.0);
   1212      assert_approx_equals(list_a[i].width, list_b[i].width, 1.0);
   1213      assert_approx_equals(list_a[i].height, list_b[i].height, 1.0);
   1214      // Y-position not tested here as getting the baseline for text in the
   1215      // DOM is not straightforward.
   1216    }
   1217  }
   1218 
   1219  ctx.font = '50px sans-serif';
   1220  ctx.direction = 'ltr';
   1221  ctx.textAlign = 'right';
   1222  ctx.letterSpacing = '10px';
   1223 
   1224  const kTexts = [
   1225    'UNAVAILABLE',
   1226    '🏁🎶🏁',
   1227    ')(あ)(',
   1228    '-abcd_',
   1229    'איפה הספרייה?',
   1230    'bidiמתמטיקה'
   1231  ]
   1232 
   1233  for (text of kTexts) {
   1234    const tm = ctx.measureText(text);
   1235    // First character.
   1236    checkRectListsMatch(
   1237      tm.getSelectionRects(0, 1),
   1238      placeAndSelectTextInDOM(text, 0, 1)
   1239    );
   1240    // Last character.
   1241    checkRectListsMatch(
   1242      tm.getSelectionRects(text.length - 1, text.length),
   1243      placeAndSelectTextInDOM(text, text.length - 1, text.length)
   1244    );
   1245    // Whole string.
   1246    checkRectListsMatch(
   1247      tm.getSelectionRects(0, text.length),
   1248      placeAndSelectTextInDOM(text, 0, text.length)
   1249    );
   1250    // Intermediate string.
   1251    checkRectListsMatch(
   1252      tm.getSelectionRects(1, text.length - 1),
   1253      placeAndSelectTextInDOM(text, 1, text.length - 1)
   1254    );
   1255    // Invalid start > end string. Creates 0 width rectangle.
   1256    checkRectListsMatch(
   1257      tm.getSelectionRects(3, 2),
   1258      placeAndSelectTextInDOM(text, 3, 2)
   1259    );
   1260    checkRectListsMatch(
   1261      tm.getSelectionRects(1, 0),
   1262      placeAndSelectTextInDOM(text, 1, 0)
   1263    );
   1264  }
   1265 }, "Check that TextMetrics::getSelectionRects() matches its DOM equivalent, with direction ltr, text align right, 10px letter spacing, and no-directional-override.");
   1266 
   1267 test(t => {
   1268  const canvas = new OffscreenCanvas(100, 50);
   1269  const ctx = canvas.getContext('2d');
   1270 
   1271  function placeAndSelectTextInDOM(text, from, to) {
   1272    const el = document.createElement("div");
   1273    el.innerHTML = text;
   1274    el.style.font = '50px sans-serif';
   1275    el.style.direction = 'rtl';
   1276    el.style.textAlign = 'right';
   1277    el.style.letterSpacing = '10px';
   1278    document.body.appendChild(el);
   1279 
   1280    let range = document.createRange();
   1281 
   1282    range.setStart(el.childNodes[0], 0);
   1283    range.setEnd(el.childNodes[0], text.length);
   1284    const parent = range.getClientRects()[0];
   1285    let width = 0;
   1286    for (const rect of range.getClientRects()) {
   1287      width += rect.width;
   1288    }
   1289 
   1290    range.setStart(el.childNodes[0], from);
   1291    range.setEnd(el.childNodes[0], to);
   1292 
   1293    let sel = window.getSelection();
   1294    sel.removeAllRanges();
   1295    sel.addRange(range);
   1296 
   1297    let sel_rects = sel.getRangeAt(0).getClientRects();
   1298 
   1299    document.body.removeChild(el);
   1300 
   1301    // Offset to the alignment point determined by textAlign.
   1302    let text_align_dx;
   1303    switch (el.style.textAlign) {
   1304      case 'right':
   1305        text_align_dx = width;
   1306        break;
   1307      case 'center':
   1308        text_align_dx = width / 2;
   1309        break;
   1310      default:
   1311        text_align_dx = 0;
   1312    }
   1313 
   1314    for(let i = 0 ; i < sel_rects.length ; ++i) {
   1315      sel_rects[i].x -= parent.x + text_align_dx;
   1316      sel_rects[i].y -= parent.y;
   1317    }
   1318 
   1319    return sel_rects;
   1320  }
   1321 
   1322  function checkRectListsMatch(list_a, list_b) {
   1323    _assertSame(list_a.length, list_b.length, "list_a.length", "list_b.length");
   1324    for(let i = 0 ; i < list_a.length ; ++i) {
   1325      assert_approx_equals(list_a[i].x, list_b[i].x, 1.0);
   1326      assert_approx_equals(list_a[i].width, list_b[i].width, 1.0);
   1327      assert_approx_equals(list_a[i].height, list_b[i].height, 1.0);
   1328      // Y-position not tested here as getting the baseline for text in the
   1329      // DOM is not straightforward.
   1330    }
   1331  }
   1332 
   1333  ctx.font = '50px sans-serif';
   1334  ctx.direction = 'rtl';
   1335  ctx.textAlign = 'right';
   1336  ctx.letterSpacing = '10px';
   1337 
   1338  const kTexts = [
   1339    'UNAVAILABLE',
   1340    '🏁🎶🏁',
   1341    ')(あ)(',
   1342    '-abcd_',
   1343    'איפה הספרייה?',
   1344    'bidiמתמטיקה'
   1345  ]
   1346 
   1347  for (text of kTexts) {
   1348    const tm = ctx.measureText(text);
   1349    // First character.
   1350    checkRectListsMatch(
   1351      tm.getSelectionRects(0, 1),
   1352      placeAndSelectTextInDOM(text, 0, 1)
   1353    );
   1354    // Last character.
   1355    checkRectListsMatch(
   1356      tm.getSelectionRects(text.length - 1, text.length),
   1357      placeAndSelectTextInDOM(text, text.length - 1, text.length)
   1358    );
   1359    // Whole string.
   1360    checkRectListsMatch(
   1361      tm.getSelectionRects(0, text.length),
   1362      placeAndSelectTextInDOM(text, 0, text.length)
   1363    );
   1364    // Intermediate string.
   1365    checkRectListsMatch(
   1366      tm.getSelectionRects(1, text.length - 1),
   1367      placeAndSelectTextInDOM(text, 1, text.length - 1)
   1368    );
   1369    // Invalid start > end string. Creates 0 width rectangle.
   1370    checkRectListsMatch(
   1371      tm.getSelectionRects(3, 2),
   1372      placeAndSelectTextInDOM(text, 3, 2)
   1373    );
   1374    checkRectListsMatch(
   1375      tm.getSelectionRects(1, 0),
   1376      placeAndSelectTextInDOM(text, 1, 0)
   1377    );
   1378  }
   1379 }, "Check that TextMetrics::getSelectionRects() matches its DOM equivalent, with direction rtl, text align right, 10px letter spacing, and no-directional-override.");
   1380 
   1381 test(t => {
   1382  const canvas = new OffscreenCanvas(100, 50);
   1383  const ctx = canvas.getContext('2d');
   1384 
   1385  function placeAndSelectTextInDOM(text, from, to) {
   1386    const el = document.createElement("div");
   1387    el.innerHTML = text;
   1388    el.style.font = '50px sans-serif';
   1389    el.style.direction = 'ltr';
   1390    el.style.textAlign = 'left';
   1391    el.style.letterSpacing = '0px';
   1392    document.body.appendChild(el);
   1393 
   1394    let range = document.createRange();
   1395 
   1396    range.setStart(el.childNodes[0], 0);
   1397    range.setEnd(el.childNodes[0], text.length);
   1398    const parent = range.getClientRects()[0];
   1399    let width = 0;
   1400    for (const rect of range.getClientRects()) {
   1401      width += rect.width;
   1402    }
   1403 
   1404    range.setStart(el.childNodes[0], from);
   1405    range.setEnd(el.childNodes[0], to);
   1406 
   1407    let sel = window.getSelection();
   1408    sel.removeAllRanges();
   1409    sel.addRange(range);
   1410 
   1411    let sel_rects = sel.getRangeAt(0).getClientRects();
   1412 
   1413    document.body.removeChild(el);
   1414 
   1415    // Offset to the alignment point determined by textAlign.
   1416    let text_align_dx;
   1417    switch (el.style.textAlign) {
   1418      case 'right':
   1419        text_align_dx = width;
   1420        break;
   1421      case 'center':
   1422        text_align_dx = width / 2;
   1423        break;
   1424      default:
   1425        text_align_dx = 0;
   1426    }
   1427 
   1428    for(let i = 0 ; i < sel_rects.length ; ++i) {
   1429      sel_rects[i].x -= parent.x + text_align_dx;
   1430      sel_rects[i].y -= parent.y;
   1431    }
   1432 
   1433    return sel_rects;
   1434  }
   1435 
   1436  function checkRectListsMatch(list_a, list_b) {
   1437    _assertSame(list_a.length, list_b.length, "list_a.length", "list_b.length");
   1438    for(let i = 0 ; i < list_a.length ; ++i) {
   1439      assert_approx_equals(list_a[i].x, list_b[i].x, 1.0);
   1440      assert_approx_equals(list_a[i].width, list_b[i].width, 1.0);
   1441      assert_approx_equals(list_a[i].height, list_b[i].height, 1.0);
   1442      // Y-position not tested here as getting the baseline for text in the
   1443      // DOM is not straightforward.
   1444    }
   1445  }
   1446 
   1447  function addDirectionalOverrideCharacters(text, direction_is_ltr) {
   1448    // source: www.w3.org/International/questions/qa-bidi-unicode-controls.en
   1449    const LTR_OVERRIDE = '\u202D';
   1450    const RTL_OVERRIDE = '\u202E';
   1451    const OVERRIDE_END = '\u202C';
   1452    if (direction_is_ltr) {
   1453      return LTR_OVERRIDE + text + OVERRIDE_END;
   1454    }
   1455    return RTL_OVERRIDE + text + OVERRIDE_END;
   1456  }
   1457 
   1458  ctx.font = '50px sans-serif';
   1459  ctx.direction = 'ltr';
   1460  ctx.textAlign = 'left';
   1461  ctx.letterSpacing = '0px';
   1462 
   1463  const kTexts = [
   1464    'UNAVAILABLE',
   1465    '🏁🎶🏁',
   1466    ')(あ)(',
   1467    '-abcd_',
   1468    'איפה הספרייה?',
   1469    'bidiמתמטיקה'
   1470  ]
   1471 
   1472  for (text of kTexts) {
   1473    text = addDirectionalOverrideCharacters(text, ctx.direction == 'ltr');
   1474    const tm = ctx.measureText(text);
   1475    // First character.
   1476    checkRectListsMatch(
   1477      tm.getSelectionRects(0, 1),
   1478      placeAndSelectTextInDOM(text, 0, 1)
   1479    );
   1480    // Last character.
   1481    checkRectListsMatch(
   1482      tm.getSelectionRects(text.length - 1, text.length),
   1483      placeAndSelectTextInDOM(text, text.length - 1, text.length)
   1484    );
   1485    // Whole string.
   1486    checkRectListsMatch(
   1487      tm.getSelectionRects(0, text.length),
   1488      placeAndSelectTextInDOM(text, 0, text.length)
   1489    );
   1490    // Intermediate string.
   1491    checkRectListsMatch(
   1492      tm.getSelectionRects(1, text.length - 1),
   1493      placeAndSelectTextInDOM(text, 1, text.length - 1)
   1494    );
   1495    // Invalid start > end string. Creates 0 width rectangle.
   1496    checkRectListsMatch(
   1497      tm.getSelectionRects(3, 2),
   1498      placeAndSelectTextInDOM(text, 3, 2)
   1499    );
   1500    checkRectListsMatch(
   1501      tm.getSelectionRects(1, 0),
   1502      placeAndSelectTextInDOM(text, 1, 0)
   1503    );
   1504  }
   1505 }, "Check that TextMetrics::getSelectionRects() matches its DOM equivalent, with direction ltr, text align left, 0px letter spacing, and directional-override.");
   1506 
   1507 test(t => {
   1508  const canvas = new OffscreenCanvas(100, 50);
   1509  const ctx = canvas.getContext('2d');
   1510 
   1511  function placeAndSelectTextInDOM(text, from, to) {
   1512    const el = document.createElement("div");
   1513    el.innerHTML = text;
   1514    el.style.font = '50px sans-serif';
   1515    el.style.direction = 'rtl';
   1516    el.style.textAlign = 'left';
   1517    el.style.letterSpacing = '0px';
   1518    document.body.appendChild(el);
   1519 
   1520    let range = document.createRange();
   1521 
   1522    range.setStart(el.childNodes[0], 0);
   1523    range.setEnd(el.childNodes[0], text.length);
   1524    const parent = range.getClientRects()[0];
   1525    let width = 0;
   1526    for (const rect of range.getClientRects()) {
   1527      width += rect.width;
   1528    }
   1529 
   1530    range.setStart(el.childNodes[0], from);
   1531    range.setEnd(el.childNodes[0], to);
   1532 
   1533    let sel = window.getSelection();
   1534    sel.removeAllRanges();
   1535    sel.addRange(range);
   1536 
   1537    let sel_rects = sel.getRangeAt(0).getClientRects();
   1538 
   1539    document.body.removeChild(el);
   1540 
   1541    // Offset to the alignment point determined by textAlign.
   1542    let text_align_dx;
   1543    switch (el.style.textAlign) {
   1544      case 'right':
   1545        text_align_dx = width;
   1546        break;
   1547      case 'center':
   1548        text_align_dx = width / 2;
   1549        break;
   1550      default:
   1551        text_align_dx = 0;
   1552    }
   1553 
   1554    for(let i = 0 ; i < sel_rects.length ; ++i) {
   1555      sel_rects[i].x -= parent.x + text_align_dx;
   1556      sel_rects[i].y -= parent.y;
   1557    }
   1558 
   1559    return sel_rects;
   1560  }
   1561 
   1562  function checkRectListsMatch(list_a, list_b) {
   1563    _assertSame(list_a.length, list_b.length, "list_a.length", "list_b.length");
   1564    for(let i = 0 ; i < list_a.length ; ++i) {
   1565      assert_approx_equals(list_a[i].x, list_b[i].x, 1.0);
   1566      assert_approx_equals(list_a[i].width, list_b[i].width, 1.0);
   1567      assert_approx_equals(list_a[i].height, list_b[i].height, 1.0);
   1568      // Y-position not tested here as getting the baseline for text in the
   1569      // DOM is not straightforward.
   1570    }
   1571  }
   1572 
   1573  function addDirectionalOverrideCharacters(text, direction_is_ltr) {
   1574    // source: www.w3.org/International/questions/qa-bidi-unicode-controls.en
   1575    const LTR_OVERRIDE = '\u202D';
   1576    const RTL_OVERRIDE = '\u202E';
   1577    const OVERRIDE_END = '\u202C';
   1578    if (direction_is_ltr) {
   1579      return LTR_OVERRIDE + text + OVERRIDE_END;
   1580    }
   1581    return RTL_OVERRIDE + text + OVERRIDE_END;
   1582  }
   1583 
   1584  ctx.font = '50px sans-serif';
   1585  ctx.direction = 'rtl';
   1586  ctx.textAlign = 'left';
   1587  ctx.letterSpacing = '0px';
   1588 
   1589  const kTexts = [
   1590    'UNAVAILABLE',
   1591    '🏁🎶🏁',
   1592    ')(あ)(',
   1593    '-abcd_',
   1594    'איפה הספרייה?',
   1595    'bidiמתמטיקה'
   1596  ]
   1597 
   1598  for (text of kTexts) {
   1599    text = addDirectionalOverrideCharacters(text, ctx.direction == 'ltr');
   1600    const tm = ctx.measureText(text);
   1601    // First character.
   1602    checkRectListsMatch(
   1603      tm.getSelectionRects(0, 1),
   1604      placeAndSelectTextInDOM(text, 0, 1)
   1605    );
   1606    // Last character.
   1607    checkRectListsMatch(
   1608      tm.getSelectionRects(text.length - 1, text.length),
   1609      placeAndSelectTextInDOM(text, text.length - 1, text.length)
   1610    );
   1611    // Whole string.
   1612    checkRectListsMatch(
   1613      tm.getSelectionRects(0, text.length),
   1614      placeAndSelectTextInDOM(text, 0, text.length)
   1615    );
   1616    // Intermediate string.
   1617    checkRectListsMatch(
   1618      tm.getSelectionRects(1, text.length - 1),
   1619      placeAndSelectTextInDOM(text, 1, text.length - 1)
   1620    );
   1621    // Invalid start > end string. Creates 0 width rectangle.
   1622    checkRectListsMatch(
   1623      tm.getSelectionRects(3, 2),
   1624      placeAndSelectTextInDOM(text, 3, 2)
   1625    );
   1626    checkRectListsMatch(
   1627      tm.getSelectionRects(1, 0),
   1628      placeAndSelectTextInDOM(text, 1, 0)
   1629    );
   1630  }
   1631 }, "Check that TextMetrics::getSelectionRects() matches its DOM equivalent, with direction rtl, text align left, 0px letter spacing, and directional-override.");
   1632 
   1633 test(t => {
   1634  const canvas = new OffscreenCanvas(100, 50);
   1635  const ctx = canvas.getContext('2d');
   1636 
   1637  function placeAndSelectTextInDOM(text, from, to) {
   1638    const el = document.createElement("div");
   1639    el.innerHTML = text;
   1640    el.style.font = '50px sans-serif';
   1641    el.style.direction = 'ltr';
   1642    el.style.textAlign = 'center';
   1643    el.style.letterSpacing = '0px';
   1644    document.body.appendChild(el);
   1645 
   1646    let range = document.createRange();
   1647 
   1648    range.setStart(el.childNodes[0], 0);
   1649    range.setEnd(el.childNodes[0], text.length);
   1650    const parent = range.getClientRects()[0];
   1651    let width = 0;
   1652    for (const rect of range.getClientRects()) {
   1653      width += rect.width;
   1654    }
   1655 
   1656    range.setStart(el.childNodes[0], from);
   1657    range.setEnd(el.childNodes[0], to);
   1658 
   1659    let sel = window.getSelection();
   1660    sel.removeAllRanges();
   1661    sel.addRange(range);
   1662 
   1663    let sel_rects = sel.getRangeAt(0).getClientRects();
   1664 
   1665    document.body.removeChild(el);
   1666 
   1667    // Offset to the alignment point determined by textAlign.
   1668    let text_align_dx;
   1669    switch (el.style.textAlign) {
   1670      case 'right':
   1671        text_align_dx = width;
   1672        break;
   1673      case 'center':
   1674        text_align_dx = width / 2;
   1675        break;
   1676      default:
   1677        text_align_dx = 0;
   1678    }
   1679 
   1680    for(let i = 0 ; i < sel_rects.length ; ++i) {
   1681      sel_rects[i].x -= parent.x + text_align_dx;
   1682      sel_rects[i].y -= parent.y;
   1683    }
   1684 
   1685    return sel_rects;
   1686  }
   1687 
   1688  function checkRectListsMatch(list_a, list_b) {
   1689    _assertSame(list_a.length, list_b.length, "list_a.length", "list_b.length");
   1690    for(let i = 0 ; i < list_a.length ; ++i) {
   1691      assert_approx_equals(list_a[i].x, list_b[i].x, 1.0);
   1692      assert_approx_equals(list_a[i].width, list_b[i].width, 1.0);
   1693      assert_approx_equals(list_a[i].height, list_b[i].height, 1.0);
   1694      // Y-position not tested here as getting the baseline for text in the
   1695      // DOM is not straightforward.
   1696    }
   1697  }
   1698 
   1699  function addDirectionalOverrideCharacters(text, direction_is_ltr) {
   1700    // source: www.w3.org/International/questions/qa-bidi-unicode-controls.en
   1701    const LTR_OVERRIDE = '\u202D';
   1702    const RTL_OVERRIDE = '\u202E';
   1703    const OVERRIDE_END = '\u202C';
   1704    if (direction_is_ltr) {
   1705      return LTR_OVERRIDE + text + OVERRIDE_END;
   1706    }
   1707    return RTL_OVERRIDE + text + OVERRIDE_END;
   1708  }
   1709 
   1710  ctx.font = '50px sans-serif';
   1711  ctx.direction = 'ltr';
   1712  ctx.textAlign = 'center';
   1713  ctx.letterSpacing = '0px';
   1714 
   1715  const kTexts = [
   1716    'UNAVAILABLE',
   1717    '🏁🎶🏁',
   1718    ')(あ)(',
   1719    '-abcd_',
   1720    'איפה הספרייה?',
   1721    'bidiמתמטיקה'
   1722  ]
   1723 
   1724  for (text of kTexts) {
   1725    text = addDirectionalOverrideCharacters(text, ctx.direction == 'ltr');
   1726    const tm = ctx.measureText(text);
   1727    // First character.
   1728    checkRectListsMatch(
   1729      tm.getSelectionRects(0, 1),
   1730      placeAndSelectTextInDOM(text, 0, 1)
   1731    );
   1732    // Last character.
   1733    checkRectListsMatch(
   1734      tm.getSelectionRects(text.length - 1, text.length),
   1735      placeAndSelectTextInDOM(text, text.length - 1, text.length)
   1736    );
   1737    // Whole string.
   1738    checkRectListsMatch(
   1739      tm.getSelectionRects(0, text.length),
   1740      placeAndSelectTextInDOM(text, 0, text.length)
   1741    );
   1742    // Intermediate string.
   1743    checkRectListsMatch(
   1744      tm.getSelectionRects(1, text.length - 1),
   1745      placeAndSelectTextInDOM(text, 1, text.length - 1)
   1746    );
   1747    // Invalid start > end string. Creates 0 width rectangle.
   1748    checkRectListsMatch(
   1749      tm.getSelectionRects(3, 2),
   1750      placeAndSelectTextInDOM(text, 3, 2)
   1751    );
   1752    checkRectListsMatch(
   1753      tm.getSelectionRects(1, 0),
   1754      placeAndSelectTextInDOM(text, 1, 0)
   1755    );
   1756  }
   1757 }, "Check that TextMetrics::getSelectionRects() matches its DOM equivalent, with direction ltr, text align center, 0px letter spacing, and directional-override.");
   1758 
   1759 test(t => {
   1760  const canvas = new OffscreenCanvas(100, 50);
   1761  const ctx = canvas.getContext('2d');
   1762 
   1763  function placeAndSelectTextInDOM(text, from, to) {
   1764    const el = document.createElement("div");
   1765    el.innerHTML = text;
   1766    el.style.font = '50px sans-serif';
   1767    el.style.direction = 'rtl';
   1768    el.style.textAlign = 'center';
   1769    el.style.letterSpacing = '0px';
   1770    document.body.appendChild(el);
   1771 
   1772    let range = document.createRange();
   1773 
   1774    range.setStart(el.childNodes[0], 0);
   1775    range.setEnd(el.childNodes[0], text.length);
   1776    const parent = range.getClientRects()[0];
   1777    let width = 0;
   1778    for (const rect of range.getClientRects()) {
   1779      width += rect.width;
   1780    }
   1781 
   1782    range.setStart(el.childNodes[0], from);
   1783    range.setEnd(el.childNodes[0], to);
   1784 
   1785    let sel = window.getSelection();
   1786    sel.removeAllRanges();
   1787    sel.addRange(range);
   1788 
   1789    let sel_rects = sel.getRangeAt(0).getClientRects();
   1790 
   1791    document.body.removeChild(el);
   1792 
   1793    // Offset to the alignment point determined by textAlign.
   1794    let text_align_dx;
   1795    switch (el.style.textAlign) {
   1796      case 'right':
   1797        text_align_dx = width;
   1798        break;
   1799      case 'center':
   1800        text_align_dx = width / 2;
   1801        break;
   1802      default:
   1803        text_align_dx = 0;
   1804    }
   1805 
   1806    for(let i = 0 ; i < sel_rects.length ; ++i) {
   1807      sel_rects[i].x -= parent.x + text_align_dx;
   1808      sel_rects[i].y -= parent.y;
   1809    }
   1810 
   1811    return sel_rects;
   1812  }
   1813 
   1814  function checkRectListsMatch(list_a, list_b) {
   1815    _assertSame(list_a.length, list_b.length, "list_a.length", "list_b.length");
   1816    for(let i = 0 ; i < list_a.length ; ++i) {
   1817      assert_approx_equals(list_a[i].x, list_b[i].x, 1.0);
   1818      assert_approx_equals(list_a[i].width, list_b[i].width, 1.0);
   1819      assert_approx_equals(list_a[i].height, list_b[i].height, 1.0);
   1820      // Y-position not tested here as getting the baseline for text in the
   1821      // DOM is not straightforward.
   1822    }
   1823  }
   1824 
   1825  function addDirectionalOverrideCharacters(text, direction_is_ltr) {
   1826    // source: www.w3.org/International/questions/qa-bidi-unicode-controls.en
   1827    const LTR_OVERRIDE = '\u202D';
   1828    const RTL_OVERRIDE = '\u202E';
   1829    const OVERRIDE_END = '\u202C';
   1830    if (direction_is_ltr) {
   1831      return LTR_OVERRIDE + text + OVERRIDE_END;
   1832    }
   1833    return RTL_OVERRIDE + text + OVERRIDE_END;
   1834  }
   1835 
   1836  ctx.font = '50px sans-serif';
   1837  ctx.direction = 'rtl';
   1838  ctx.textAlign = 'center';
   1839  ctx.letterSpacing = '0px';
   1840 
   1841  const kTexts = [
   1842    'UNAVAILABLE',
   1843    '🏁🎶🏁',
   1844    ')(あ)(',
   1845    '-abcd_',
   1846    'איפה הספרייה?',
   1847    'bidiמתמטיקה'
   1848  ]
   1849 
   1850  for (text of kTexts) {
   1851    text = addDirectionalOverrideCharacters(text, ctx.direction == 'ltr');
   1852    const tm = ctx.measureText(text);
   1853    // First character.
   1854    checkRectListsMatch(
   1855      tm.getSelectionRects(0, 1),
   1856      placeAndSelectTextInDOM(text, 0, 1)
   1857    );
   1858    // Last character.
   1859    checkRectListsMatch(
   1860      tm.getSelectionRects(text.length - 1, text.length),
   1861      placeAndSelectTextInDOM(text, text.length - 1, text.length)
   1862    );
   1863    // Whole string.
   1864    checkRectListsMatch(
   1865      tm.getSelectionRects(0, text.length),
   1866      placeAndSelectTextInDOM(text, 0, text.length)
   1867    );
   1868    // Intermediate string.
   1869    checkRectListsMatch(
   1870      tm.getSelectionRects(1, text.length - 1),
   1871      placeAndSelectTextInDOM(text, 1, text.length - 1)
   1872    );
   1873    // Invalid start > end string. Creates 0 width rectangle.
   1874    checkRectListsMatch(
   1875      tm.getSelectionRects(3, 2),
   1876      placeAndSelectTextInDOM(text, 3, 2)
   1877    );
   1878    checkRectListsMatch(
   1879      tm.getSelectionRects(1, 0),
   1880      placeAndSelectTextInDOM(text, 1, 0)
   1881    );
   1882  }
   1883 }, "Check that TextMetrics::getSelectionRects() matches its DOM equivalent, with direction rtl, text align center, 0px letter spacing, and directional-override.");
   1884 
   1885 test(t => {
   1886  const canvas = new OffscreenCanvas(100, 50);
   1887  const ctx = canvas.getContext('2d');
   1888 
   1889  function placeAndSelectTextInDOM(text, from, to) {
   1890    const el = document.createElement("div");
   1891    el.innerHTML = text;
   1892    el.style.font = '50px sans-serif';
   1893    el.style.direction = 'ltr';
   1894    el.style.textAlign = 'right';
   1895    el.style.letterSpacing = '0px';
   1896    document.body.appendChild(el);
   1897 
   1898    let range = document.createRange();
   1899 
   1900    range.setStart(el.childNodes[0], 0);
   1901    range.setEnd(el.childNodes[0], text.length);
   1902    const parent = range.getClientRects()[0];
   1903    let width = 0;
   1904    for (const rect of range.getClientRects()) {
   1905      width += rect.width;
   1906    }
   1907 
   1908    range.setStart(el.childNodes[0], from);
   1909    range.setEnd(el.childNodes[0], to);
   1910 
   1911    let sel = window.getSelection();
   1912    sel.removeAllRanges();
   1913    sel.addRange(range);
   1914 
   1915    let sel_rects = sel.getRangeAt(0).getClientRects();
   1916 
   1917    document.body.removeChild(el);
   1918 
   1919    // Offset to the alignment point determined by textAlign.
   1920    let text_align_dx;
   1921    switch (el.style.textAlign) {
   1922      case 'right':
   1923        text_align_dx = width;
   1924        break;
   1925      case 'center':
   1926        text_align_dx = width / 2;
   1927        break;
   1928      default:
   1929        text_align_dx = 0;
   1930    }
   1931 
   1932    for(let i = 0 ; i < sel_rects.length ; ++i) {
   1933      sel_rects[i].x -= parent.x + text_align_dx;
   1934      sel_rects[i].y -= parent.y;
   1935    }
   1936 
   1937    return sel_rects;
   1938  }
   1939 
   1940  function checkRectListsMatch(list_a, list_b) {
   1941    _assertSame(list_a.length, list_b.length, "list_a.length", "list_b.length");
   1942    for(let i = 0 ; i < list_a.length ; ++i) {
   1943      assert_approx_equals(list_a[i].x, list_b[i].x, 1.0);
   1944      assert_approx_equals(list_a[i].width, list_b[i].width, 1.0);
   1945      assert_approx_equals(list_a[i].height, list_b[i].height, 1.0);
   1946      // Y-position not tested here as getting the baseline for text in the
   1947      // DOM is not straightforward.
   1948    }
   1949  }
   1950 
   1951  function addDirectionalOverrideCharacters(text, direction_is_ltr) {
   1952    // source: www.w3.org/International/questions/qa-bidi-unicode-controls.en
   1953    const LTR_OVERRIDE = '\u202D';
   1954    const RTL_OVERRIDE = '\u202E';
   1955    const OVERRIDE_END = '\u202C';
   1956    if (direction_is_ltr) {
   1957      return LTR_OVERRIDE + text + OVERRIDE_END;
   1958    }
   1959    return RTL_OVERRIDE + text + OVERRIDE_END;
   1960  }
   1961 
   1962  ctx.font = '50px sans-serif';
   1963  ctx.direction = 'ltr';
   1964  ctx.textAlign = 'right';
   1965  ctx.letterSpacing = '0px';
   1966 
   1967  const kTexts = [
   1968    'UNAVAILABLE',
   1969    '🏁🎶🏁',
   1970    ')(あ)(',
   1971    '-abcd_',
   1972    'איפה הספרייה?',
   1973    'bidiמתמטיקה'
   1974  ]
   1975 
   1976  for (text of kTexts) {
   1977    text = addDirectionalOverrideCharacters(text, ctx.direction == 'ltr');
   1978    const tm = ctx.measureText(text);
   1979    // First character.
   1980    checkRectListsMatch(
   1981      tm.getSelectionRects(0, 1),
   1982      placeAndSelectTextInDOM(text, 0, 1)
   1983    );
   1984    // Last character.
   1985    checkRectListsMatch(
   1986      tm.getSelectionRects(text.length - 1, text.length),
   1987      placeAndSelectTextInDOM(text, text.length - 1, text.length)
   1988    );
   1989    // Whole string.
   1990    checkRectListsMatch(
   1991      tm.getSelectionRects(0, text.length),
   1992      placeAndSelectTextInDOM(text, 0, text.length)
   1993    );
   1994    // Intermediate string.
   1995    checkRectListsMatch(
   1996      tm.getSelectionRects(1, text.length - 1),
   1997      placeAndSelectTextInDOM(text, 1, text.length - 1)
   1998    );
   1999    // Invalid start > end string. Creates 0 width rectangle.
   2000    checkRectListsMatch(
   2001      tm.getSelectionRects(3, 2),
   2002      placeAndSelectTextInDOM(text, 3, 2)
   2003    );
   2004    checkRectListsMatch(
   2005      tm.getSelectionRects(1, 0),
   2006      placeAndSelectTextInDOM(text, 1, 0)
   2007    );
   2008  }
   2009 }, "Check that TextMetrics::getSelectionRects() matches its DOM equivalent, with direction ltr, text align right, 0px letter spacing, and directional-override.");
   2010 
   2011 test(t => {
   2012  const canvas = new OffscreenCanvas(100, 50);
   2013  const ctx = canvas.getContext('2d');
   2014 
   2015  function placeAndSelectTextInDOM(text, from, to) {
   2016    const el = document.createElement("div");
   2017    el.innerHTML = text;
   2018    el.style.font = '50px sans-serif';
   2019    el.style.direction = 'rtl';
   2020    el.style.textAlign = 'right';
   2021    el.style.letterSpacing = '0px';
   2022    document.body.appendChild(el);
   2023 
   2024    let range = document.createRange();
   2025 
   2026    range.setStart(el.childNodes[0], 0);
   2027    range.setEnd(el.childNodes[0], text.length);
   2028    const parent = range.getClientRects()[0];
   2029    let width = 0;
   2030    for (const rect of range.getClientRects()) {
   2031      width += rect.width;
   2032    }
   2033 
   2034    range.setStart(el.childNodes[0], from);
   2035    range.setEnd(el.childNodes[0], to);
   2036 
   2037    let sel = window.getSelection();
   2038    sel.removeAllRanges();
   2039    sel.addRange(range);
   2040 
   2041    let sel_rects = sel.getRangeAt(0).getClientRects();
   2042 
   2043    document.body.removeChild(el);
   2044 
   2045    // Offset to the alignment point determined by textAlign.
   2046    let text_align_dx;
   2047    switch (el.style.textAlign) {
   2048      case 'right':
   2049        text_align_dx = width;
   2050        break;
   2051      case 'center':
   2052        text_align_dx = width / 2;
   2053        break;
   2054      default:
   2055        text_align_dx = 0;
   2056    }
   2057 
   2058    for(let i = 0 ; i < sel_rects.length ; ++i) {
   2059      sel_rects[i].x -= parent.x + text_align_dx;
   2060      sel_rects[i].y -= parent.y;
   2061    }
   2062 
   2063    return sel_rects;
   2064  }
   2065 
   2066  function checkRectListsMatch(list_a, list_b) {
   2067    _assertSame(list_a.length, list_b.length, "list_a.length", "list_b.length");
   2068    for(let i = 0 ; i < list_a.length ; ++i) {
   2069      assert_approx_equals(list_a[i].x, list_b[i].x, 1.0);
   2070      assert_approx_equals(list_a[i].width, list_b[i].width, 1.0);
   2071      assert_approx_equals(list_a[i].height, list_b[i].height, 1.0);
   2072      // Y-position not tested here as getting the baseline for text in the
   2073      // DOM is not straightforward.
   2074    }
   2075  }
   2076 
   2077  function addDirectionalOverrideCharacters(text, direction_is_ltr) {
   2078    // source: www.w3.org/International/questions/qa-bidi-unicode-controls.en
   2079    const LTR_OVERRIDE = '\u202D';
   2080    const RTL_OVERRIDE = '\u202E';
   2081    const OVERRIDE_END = '\u202C';
   2082    if (direction_is_ltr) {
   2083      return LTR_OVERRIDE + text + OVERRIDE_END;
   2084    }
   2085    return RTL_OVERRIDE + text + OVERRIDE_END;
   2086  }
   2087 
   2088  ctx.font = '50px sans-serif';
   2089  ctx.direction = 'rtl';
   2090  ctx.textAlign = 'right';
   2091  ctx.letterSpacing = '0px';
   2092 
   2093  const kTexts = [
   2094    'UNAVAILABLE',
   2095    '🏁🎶🏁',
   2096    ')(あ)(',
   2097    '-abcd_',
   2098    'איפה הספרייה?',
   2099    'bidiמתמטיקה'
   2100  ]
   2101 
   2102  for (text of kTexts) {
   2103    text = addDirectionalOverrideCharacters(text, ctx.direction == 'ltr');
   2104    const tm = ctx.measureText(text);
   2105    // First character.
   2106    checkRectListsMatch(
   2107      tm.getSelectionRects(0, 1),
   2108      placeAndSelectTextInDOM(text, 0, 1)
   2109    );
   2110    // Last character.
   2111    checkRectListsMatch(
   2112      tm.getSelectionRects(text.length - 1, text.length),
   2113      placeAndSelectTextInDOM(text, text.length - 1, text.length)
   2114    );
   2115    // Whole string.
   2116    checkRectListsMatch(
   2117      tm.getSelectionRects(0, text.length),
   2118      placeAndSelectTextInDOM(text, 0, text.length)
   2119    );
   2120    // Intermediate string.
   2121    checkRectListsMatch(
   2122      tm.getSelectionRects(1, text.length - 1),
   2123      placeAndSelectTextInDOM(text, 1, text.length - 1)
   2124    );
   2125    // Invalid start > end string. Creates 0 width rectangle.
   2126    checkRectListsMatch(
   2127      tm.getSelectionRects(3, 2),
   2128      placeAndSelectTextInDOM(text, 3, 2)
   2129    );
   2130    checkRectListsMatch(
   2131      tm.getSelectionRects(1, 0),
   2132      placeAndSelectTextInDOM(text, 1, 0)
   2133    );
   2134  }
   2135 }, "Check that TextMetrics::getSelectionRects() matches its DOM equivalent, with direction rtl, text align right, 0px letter spacing, and directional-override.");
   2136 
   2137 test(t => {
   2138  const canvas = new OffscreenCanvas(100, 50);
   2139  const ctx = canvas.getContext('2d');
   2140 
   2141  function placeAndSelectTextInDOM(text, from, to) {
   2142    const el = document.createElement("div");
   2143    el.innerHTML = text;
   2144    el.style.font = '50px sans-serif';
   2145    el.style.direction = 'ltr';
   2146    el.style.textAlign = 'left';
   2147    el.style.letterSpacing = '10px';
   2148    document.body.appendChild(el);
   2149 
   2150    let range = document.createRange();
   2151 
   2152    range.setStart(el.childNodes[0], 0);
   2153    range.setEnd(el.childNodes[0], text.length);
   2154    const parent = range.getClientRects()[0];
   2155    let width = 0;
   2156    for (const rect of range.getClientRects()) {
   2157      width += rect.width;
   2158    }
   2159 
   2160    range.setStart(el.childNodes[0], from);
   2161    range.setEnd(el.childNodes[0], to);
   2162 
   2163    let sel = window.getSelection();
   2164    sel.removeAllRanges();
   2165    sel.addRange(range);
   2166 
   2167    let sel_rects = sel.getRangeAt(0).getClientRects();
   2168 
   2169    document.body.removeChild(el);
   2170 
   2171    // Offset to the alignment point determined by textAlign.
   2172    let text_align_dx;
   2173    switch (el.style.textAlign) {
   2174      case 'right':
   2175        text_align_dx = width;
   2176        break;
   2177      case 'center':
   2178        text_align_dx = width / 2;
   2179        break;
   2180      default:
   2181        text_align_dx = 0;
   2182    }
   2183 
   2184    for(let i = 0 ; i < sel_rects.length ; ++i) {
   2185      sel_rects[i].x -= parent.x + text_align_dx;
   2186      sel_rects[i].y -= parent.y;
   2187    }
   2188 
   2189    return sel_rects;
   2190  }
   2191 
   2192  function checkRectListsMatch(list_a, list_b) {
   2193    _assertSame(list_a.length, list_b.length, "list_a.length", "list_b.length");
   2194    for(let i = 0 ; i < list_a.length ; ++i) {
   2195      assert_approx_equals(list_a[i].x, list_b[i].x, 1.0);
   2196      assert_approx_equals(list_a[i].width, list_b[i].width, 1.0);
   2197      assert_approx_equals(list_a[i].height, list_b[i].height, 1.0);
   2198      // Y-position not tested here as getting the baseline for text in the
   2199      // DOM is not straightforward.
   2200    }
   2201  }
   2202 
   2203  function addDirectionalOverrideCharacters(text, direction_is_ltr) {
   2204    // source: www.w3.org/International/questions/qa-bidi-unicode-controls.en
   2205    const LTR_OVERRIDE = '\u202D';
   2206    const RTL_OVERRIDE = '\u202E';
   2207    const OVERRIDE_END = '\u202C';
   2208    if (direction_is_ltr) {
   2209      return LTR_OVERRIDE + text + OVERRIDE_END;
   2210    }
   2211    return RTL_OVERRIDE + text + OVERRIDE_END;
   2212  }
   2213 
   2214  ctx.font = '50px sans-serif';
   2215  ctx.direction = 'ltr';
   2216  ctx.textAlign = 'left';
   2217  ctx.letterSpacing = '10px';
   2218 
   2219  const kTexts = [
   2220    'UNAVAILABLE',
   2221    '🏁🎶🏁',
   2222    ')(あ)(',
   2223    '-abcd_',
   2224    'איפה הספרייה?',
   2225    'bidiמתמטיקה'
   2226  ]
   2227 
   2228  for (text of kTexts) {
   2229    text = addDirectionalOverrideCharacters(text, ctx.direction == 'ltr');
   2230    const tm = ctx.measureText(text);
   2231    // First character.
   2232    checkRectListsMatch(
   2233      tm.getSelectionRects(0, 1),
   2234      placeAndSelectTextInDOM(text, 0, 1)
   2235    );
   2236    // Last character.
   2237    checkRectListsMatch(
   2238      tm.getSelectionRects(text.length - 1, text.length),
   2239      placeAndSelectTextInDOM(text, text.length - 1, text.length)
   2240    );
   2241    // Whole string.
   2242    checkRectListsMatch(
   2243      tm.getSelectionRects(0, text.length),
   2244      placeAndSelectTextInDOM(text, 0, text.length)
   2245    );
   2246    // Intermediate string.
   2247    checkRectListsMatch(
   2248      tm.getSelectionRects(1, text.length - 1),
   2249      placeAndSelectTextInDOM(text, 1, text.length - 1)
   2250    );
   2251    // Invalid start > end string. Creates 0 width rectangle.
   2252    checkRectListsMatch(
   2253      tm.getSelectionRects(3, 2),
   2254      placeAndSelectTextInDOM(text, 3, 2)
   2255    );
   2256    checkRectListsMatch(
   2257      tm.getSelectionRects(1, 0),
   2258      placeAndSelectTextInDOM(text, 1, 0)
   2259    );
   2260  }
   2261 }, "Check that TextMetrics::getSelectionRects() matches its DOM equivalent, with direction ltr, text align left, 10px letter spacing, and directional-override.");
   2262 
   2263 test(t => {
   2264  const canvas = new OffscreenCanvas(100, 50);
   2265  const ctx = canvas.getContext('2d');
   2266 
   2267  function placeAndSelectTextInDOM(text, from, to) {
   2268    const el = document.createElement("div");
   2269    el.innerHTML = text;
   2270    el.style.font = '50px sans-serif';
   2271    el.style.direction = 'rtl';
   2272    el.style.textAlign = 'left';
   2273    el.style.letterSpacing = '10px';
   2274    document.body.appendChild(el);
   2275 
   2276    let range = document.createRange();
   2277 
   2278    range.setStart(el.childNodes[0], 0);
   2279    range.setEnd(el.childNodes[0], text.length);
   2280    const parent = range.getClientRects()[0];
   2281    let width = 0;
   2282    for (const rect of range.getClientRects()) {
   2283      width += rect.width;
   2284    }
   2285 
   2286    range.setStart(el.childNodes[0], from);
   2287    range.setEnd(el.childNodes[0], to);
   2288 
   2289    let sel = window.getSelection();
   2290    sel.removeAllRanges();
   2291    sel.addRange(range);
   2292 
   2293    let sel_rects = sel.getRangeAt(0).getClientRects();
   2294 
   2295    document.body.removeChild(el);
   2296 
   2297    // Offset to the alignment point determined by textAlign.
   2298    let text_align_dx;
   2299    switch (el.style.textAlign) {
   2300      case 'right':
   2301        text_align_dx = width;
   2302        break;
   2303      case 'center':
   2304        text_align_dx = width / 2;
   2305        break;
   2306      default:
   2307        text_align_dx = 0;
   2308    }
   2309 
   2310    for(let i = 0 ; i < sel_rects.length ; ++i) {
   2311      sel_rects[i].x -= parent.x + text_align_dx;
   2312      sel_rects[i].y -= parent.y;
   2313    }
   2314 
   2315    return sel_rects;
   2316  }
   2317 
   2318  function checkRectListsMatch(list_a, list_b) {
   2319    _assertSame(list_a.length, list_b.length, "list_a.length", "list_b.length");
   2320    for(let i = 0 ; i < list_a.length ; ++i) {
   2321      assert_approx_equals(list_a[i].x, list_b[i].x, 1.0);
   2322      assert_approx_equals(list_a[i].width, list_b[i].width, 1.0);
   2323      assert_approx_equals(list_a[i].height, list_b[i].height, 1.0);
   2324      // Y-position not tested here as getting the baseline for text in the
   2325      // DOM is not straightforward.
   2326    }
   2327  }
   2328 
   2329  function addDirectionalOverrideCharacters(text, direction_is_ltr) {
   2330    // source: www.w3.org/International/questions/qa-bidi-unicode-controls.en
   2331    const LTR_OVERRIDE = '\u202D';
   2332    const RTL_OVERRIDE = '\u202E';
   2333    const OVERRIDE_END = '\u202C';
   2334    if (direction_is_ltr) {
   2335      return LTR_OVERRIDE + text + OVERRIDE_END;
   2336    }
   2337    return RTL_OVERRIDE + text + OVERRIDE_END;
   2338  }
   2339 
   2340  ctx.font = '50px sans-serif';
   2341  ctx.direction = 'rtl';
   2342  ctx.textAlign = 'left';
   2343  ctx.letterSpacing = '10px';
   2344 
   2345  const kTexts = [
   2346    'UNAVAILABLE',
   2347    '🏁🎶🏁',
   2348    ')(あ)(',
   2349    '-abcd_',
   2350    'איפה הספרייה?',
   2351    'bidiמתמטיקה'
   2352  ]
   2353 
   2354  for (text of kTexts) {
   2355    text = addDirectionalOverrideCharacters(text, ctx.direction == 'ltr');
   2356    const tm = ctx.measureText(text);
   2357    // First character.
   2358    checkRectListsMatch(
   2359      tm.getSelectionRects(0, 1),
   2360      placeAndSelectTextInDOM(text, 0, 1)
   2361    );
   2362    // Last character.
   2363    checkRectListsMatch(
   2364      tm.getSelectionRects(text.length - 1, text.length),
   2365      placeAndSelectTextInDOM(text, text.length - 1, text.length)
   2366    );
   2367    // Whole string.
   2368    checkRectListsMatch(
   2369      tm.getSelectionRects(0, text.length),
   2370      placeAndSelectTextInDOM(text, 0, text.length)
   2371    );
   2372    // Intermediate string.
   2373    checkRectListsMatch(
   2374      tm.getSelectionRects(1, text.length - 1),
   2375      placeAndSelectTextInDOM(text, 1, text.length - 1)
   2376    );
   2377    // Invalid start > end string. Creates 0 width rectangle.
   2378    checkRectListsMatch(
   2379      tm.getSelectionRects(3, 2),
   2380      placeAndSelectTextInDOM(text, 3, 2)
   2381    );
   2382    checkRectListsMatch(
   2383      tm.getSelectionRects(1, 0),
   2384      placeAndSelectTextInDOM(text, 1, 0)
   2385    );
   2386  }
   2387 }, "Check that TextMetrics::getSelectionRects() matches its DOM equivalent, with direction rtl, text align left, 10px letter spacing, and directional-override.");
   2388 
   2389 test(t => {
   2390  const canvas = new OffscreenCanvas(100, 50);
   2391  const ctx = canvas.getContext('2d');
   2392 
   2393  function placeAndSelectTextInDOM(text, from, to) {
   2394    const el = document.createElement("div");
   2395    el.innerHTML = text;
   2396    el.style.font = '50px sans-serif';
   2397    el.style.direction = 'ltr';
   2398    el.style.textAlign = 'center';
   2399    el.style.letterSpacing = '10px';
   2400    document.body.appendChild(el);
   2401 
   2402    let range = document.createRange();
   2403 
   2404    range.setStart(el.childNodes[0], 0);
   2405    range.setEnd(el.childNodes[0], text.length);
   2406    const parent = range.getClientRects()[0];
   2407    let width = 0;
   2408    for (const rect of range.getClientRects()) {
   2409      width += rect.width;
   2410    }
   2411 
   2412    range.setStart(el.childNodes[0], from);
   2413    range.setEnd(el.childNodes[0], to);
   2414 
   2415    let sel = window.getSelection();
   2416    sel.removeAllRanges();
   2417    sel.addRange(range);
   2418 
   2419    let sel_rects = sel.getRangeAt(0).getClientRects();
   2420 
   2421    document.body.removeChild(el);
   2422 
   2423    // Offset to the alignment point determined by textAlign.
   2424    let text_align_dx;
   2425    switch (el.style.textAlign) {
   2426      case 'right':
   2427        text_align_dx = width;
   2428        break;
   2429      case 'center':
   2430        text_align_dx = width / 2;
   2431        break;
   2432      default:
   2433        text_align_dx = 0;
   2434    }
   2435 
   2436    for(let i = 0 ; i < sel_rects.length ; ++i) {
   2437      sel_rects[i].x -= parent.x + text_align_dx;
   2438      sel_rects[i].y -= parent.y;
   2439    }
   2440 
   2441    return sel_rects;
   2442  }
   2443 
   2444  function checkRectListsMatch(list_a, list_b) {
   2445    _assertSame(list_a.length, list_b.length, "list_a.length", "list_b.length");
   2446    for(let i = 0 ; i < list_a.length ; ++i) {
   2447      assert_approx_equals(list_a[i].x, list_b[i].x, 1.0);
   2448      assert_approx_equals(list_a[i].width, list_b[i].width, 1.0);
   2449      assert_approx_equals(list_a[i].height, list_b[i].height, 1.0);
   2450      // Y-position not tested here as getting the baseline for text in the
   2451      // DOM is not straightforward.
   2452    }
   2453  }
   2454 
   2455  function addDirectionalOverrideCharacters(text, direction_is_ltr) {
   2456    // source: www.w3.org/International/questions/qa-bidi-unicode-controls.en
   2457    const LTR_OVERRIDE = '\u202D';
   2458    const RTL_OVERRIDE = '\u202E';
   2459    const OVERRIDE_END = '\u202C';
   2460    if (direction_is_ltr) {
   2461      return LTR_OVERRIDE + text + OVERRIDE_END;
   2462    }
   2463    return RTL_OVERRIDE + text + OVERRIDE_END;
   2464  }
   2465 
   2466  ctx.font = '50px sans-serif';
   2467  ctx.direction = 'ltr';
   2468  ctx.textAlign = 'center';
   2469  ctx.letterSpacing = '10px';
   2470 
   2471  const kTexts = [
   2472    'UNAVAILABLE',
   2473    '🏁🎶🏁',
   2474    ')(あ)(',
   2475    '-abcd_',
   2476    'איפה הספרייה?',
   2477    'bidiמתמטיקה'
   2478  ]
   2479 
   2480  for (text of kTexts) {
   2481    text = addDirectionalOverrideCharacters(text, ctx.direction == 'ltr');
   2482    const tm = ctx.measureText(text);
   2483    // First character.
   2484    checkRectListsMatch(
   2485      tm.getSelectionRects(0, 1),
   2486      placeAndSelectTextInDOM(text, 0, 1)
   2487    );
   2488    // Last character.
   2489    checkRectListsMatch(
   2490      tm.getSelectionRects(text.length - 1, text.length),
   2491      placeAndSelectTextInDOM(text, text.length - 1, text.length)
   2492    );
   2493    // Whole string.
   2494    checkRectListsMatch(
   2495      tm.getSelectionRects(0, text.length),
   2496      placeAndSelectTextInDOM(text, 0, text.length)
   2497    );
   2498    // Intermediate string.
   2499    checkRectListsMatch(
   2500      tm.getSelectionRects(1, text.length - 1),
   2501      placeAndSelectTextInDOM(text, 1, text.length - 1)
   2502    );
   2503    // Invalid start > end string. Creates 0 width rectangle.
   2504    checkRectListsMatch(
   2505      tm.getSelectionRects(3, 2),
   2506      placeAndSelectTextInDOM(text, 3, 2)
   2507    );
   2508    checkRectListsMatch(
   2509      tm.getSelectionRects(1, 0),
   2510      placeAndSelectTextInDOM(text, 1, 0)
   2511    );
   2512  }
   2513 }, "Check that TextMetrics::getSelectionRects() matches its DOM equivalent, with direction ltr, text align center, 10px letter spacing, and directional-override.");
   2514 
   2515 test(t => {
   2516  const canvas = new OffscreenCanvas(100, 50);
   2517  const ctx = canvas.getContext('2d');
   2518 
   2519  function placeAndSelectTextInDOM(text, from, to) {
   2520    const el = document.createElement("div");
   2521    el.innerHTML = text;
   2522    el.style.font = '50px sans-serif';
   2523    el.style.direction = 'rtl';
   2524    el.style.textAlign = 'center';
   2525    el.style.letterSpacing = '10px';
   2526    document.body.appendChild(el);
   2527 
   2528    let range = document.createRange();
   2529 
   2530    range.setStart(el.childNodes[0], 0);
   2531    range.setEnd(el.childNodes[0], text.length);
   2532    const parent = range.getClientRects()[0];
   2533    let width = 0;
   2534    for (const rect of range.getClientRects()) {
   2535      width += rect.width;
   2536    }
   2537 
   2538    range.setStart(el.childNodes[0], from);
   2539    range.setEnd(el.childNodes[0], to);
   2540 
   2541    let sel = window.getSelection();
   2542    sel.removeAllRanges();
   2543    sel.addRange(range);
   2544 
   2545    let sel_rects = sel.getRangeAt(0).getClientRects();
   2546 
   2547    document.body.removeChild(el);
   2548 
   2549    // Offset to the alignment point determined by textAlign.
   2550    let text_align_dx;
   2551    switch (el.style.textAlign) {
   2552      case 'right':
   2553        text_align_dx = width;
   2554        break;
   2555      case 'center':
   2556        text_align_dx = width / 2;
   2557        break;
   2558      default:
   2559        text_align_dx = 0;
   2560    }
   2561 
   2562    for(let i = 0 ; i < sel_rects.length ; ++i) {
   2563      sel_rects[i].x -= parent.x + text_align_dx;
   2564      sel_rects[i].y -= parent.y;
   2565    }
   2566 
   2567    return sel_rects;
   2568  }
   2569 
   2570  function checkRectListsMatch(list_a, list_b) {
   2571    _assertSame(list_a.length, list_b.length, "list_a.length", "list_b.length");
   2572    for(let i = 0 ; i < list_a.length ; ++i) {
   2573      assert_approx_equals(list_a[i].x, list_b[i].x, 1.0);
   2574      assert_approx_equals(list_a[i].width, list_b[i].width, 1.0);
   2575      assert_approx_equals(list_a[i].height, list_b[i].height, 1.0);
   2576      // Y-position not tested here as getting the baseline for text in the
   2577      // DOM is not straightforward.
   2578    }
   2579  }
   2580 
   2581  function addDirectionalOverrideCharacters(text, direction_is_ltr) {
   2582    // source: www.w3.org/International/questions/qa-bidi-unicode-controls.en
   2583    const LTR_OVERRIDE = '\u202D';
   2584    const RTL_OVERRIDE = '\u202E';
   2585    const OVERRIDE_END = '\u202C';
   2586    if (direction_is_ltr) {
   2587      return LTR_OVERRIDE + text + OVERRIDE_END;
   2588    }
   2589    return RTL_OVERRIDE + text + OVERRIDE_END;
   2590  }
   2591 
   2592  ctx.font = '50px sans-serif';
   2593  ctx.direction = 'rtl';
   2594  ctx.textAlign = 'center';
   2595  ctx.letterSpacing = '10px';
   2596 
   2597  const kTexts = [
   2598    'UNAVAILABLE',
   2599    '🏁🎶🏁',
   2600    ')(あ)(',
   2601    '-abcd_',
   2602    'איפה הספרייה?',
   2603    'bidiמתמטיקה'
   2604  ]
   2605 
   2606  for (text of kTexts) {
   2607    text = addDirectionalOverrideCharacters(text, ctx.direction == 'ltr');
   2608    const tm = ctx.measureText(text);
   2609    // First character.
   2610    checkRectListsMatch(
   2611      tm.getSelectionRects(0, 1),
   2612      placeAndSelectTextInDOM(text, 0, 1)
   2613    );
   2614    // Last character.
   2615    checkRectListsMatch(
   2616      tm.getSelectionRects(text.length - 1, text.length),
   2617      placeAndSelectTextInDOM(text, text.length - 1, text.length)
   2618    );
   2619    // Whole string.
   2620    checkRectListsMatch(
   2621      tm.getSelectionRects(0, text.length),
   2622      placeAndSelectTextInDOM(text, 0, text.length)
   2623    );
   2624    // Intermediate string.
   2625    checkRectListsMatch(
   2626      tm.getSelectionRects(1, text.length - 1),
   2627      placeAndSelectTextInDOM(text, 1, text.length - 1)
   2628    );
   2629    // Invalid start > end string. Creates 0 width rectangle.
   2630    checkRectListsMatch(
   2631      tm.getSelectionRects(3, 2),
   2632      placeAndSelectTextInDOM(text, 3, 2)
   2633    );
   2634    checkRectListsMatch(
   2635      tm.getSelectionRects(1, 0),
   2636      placeAndSelectTextInDOM(text, 1, 0)
   2637    );
   2638  }
   2639 }, "Check that TextMetrics::getSelectionRects() matches its DOM equivalent, with direction rtl, text align center, 10px letter spacing, and directional-override.");
   2640 
   2641 test(t => {
   2642  const canvas = new OffscreenCanvas(100, 50);
   2643  const ctx = canvas.getContext('2d');
   2644 
   2645  function placeAndSelectTextInDOM(text, from, to) {
   2646    const el = document.createElement("div");
   2647    el.innerHTML = text;
   2648    el.style.font = '50px sans-serif';
   2649    el.style.direction = 'ltr';
   2650    el.style.textAlign = 'right';
   2651    el.style.letterSpacing = '10px';
   2652    document.body.appendChild(el);
   2653 
   2654    let range = document.createRange();
   2655 
   2656    range.setStart(el.childNodes[0], 0);
   2657    range.setEnd(el.childNodes[0], text.length);
   2658    const parent = range.getClientRects()[0];
   2659    let width = 0;
   2660    for (const rect of range.getClientRects()) {
   2661      width += rect.width;
   2662    }
   2663 
   2664    range.setStart(el.childNodes[0], from);
   2665    range.setEnd(el.childNodes[0], to);
   2666 
   2667    let sel = window.getSelection();
   2668    sel.removeAllRanges();
   2669    sel.addRange(range);
   2670 
   2671    let sel_rects = sel.getRangeAt(0).getClientRects();
   2672 
   2673    document.body.removeChild(el);
   2674 
   2675    // Offset to the alignment point determined by textAlign.
   2676    let text_align_dx;
   2677    switch (el.style.textAlign) {
   2678      case 'right':
   2679        text_align_dx = width;
   2680        break;
   2681      case 'center':
   2682        text_align_dx = width / 2;
   2683        break;
   2684      default:
   2685        text_align_dx = 0;
   2686    }
   2687 
   2688    for(let i = 0 ; i < sel_rects.length ; ++i) {
   2689      sel_rects[i].x -= parent.x + text_align_dx;
   2690      sel_rects[i].y -= parent.y;
   2691    }
   2692 
   2693    return sel_rects;
   2694  }
   2695 
   2696  function checkRectListsMatch(list_a, list_b) {
   2697    _assertSame(list_a.length, list_b.length, "list_a.length", "list_b.length");
   2698    for(let i = 0 ; i < list_a.length ; ++i) {
   2699      assert_approx_equals(list_a[i].x, list_b[i].x, 1.0);
   2700      assert_approx_equals(list_a[i].width, list_b[i].width, 1.0);
   2701      assert_approx_equals(list_a[i].height, list_b[i].height, 1.0);
   2702      // Y-position not tested here as getting the baseline for text in the
   2703      // DOM is not straightforward.
   2704    }
   2705  }
   2706 
   2707  function addDirectionalOverrideCharacters(text, direction_is_ltr) {
   2708    // source: www.w3.org/International/questions/qa-bidi-unicode-controls.en
   2709    const LTR_OVERRIDE = '\u202D';
   2710    const RTL_OVERRIDE = '\u202E';
   2711    const OVERRIDE_END = '\u202C';
   2712    if (direction_is_ltr) {
   2713      return LTR_OVERRIDE + text + OVERRIDE_END;
   2714    }
   2715    return RTL_OVERRIDE + text + OVERRIDE_END;
   2716  }
   2717 
   2718  ctx.font = '50px sans-serif';
   2719  ctx.direction = 'ltr';
   2720  ctx.textAlign = 'right';
   2721  ctx.letterSpacing = '10px';
   2722 
   2723  const kTexts = [
   2724    'UNAVAILABLE',
   2725    '🏁🎶🏁',
   2726    ')(あ)(',
   2727    '-abcd_',
   2728    'איפה הספרייה?',
   2729    'bidiמתמטיקה'
   2730  ]
   2731 
   2732  for (text of kTexts) {
   2733    text = addDirectionalOverrideCharacters(text, ctx.direction == 'ltr');
   2734    const tm = ctx.measureText(text);
   2735    // First character.
   2736    checkRectListsMatch(
   2737      tm.getSelectionRects(0, 1),
   2738      placeAndSelectTextInDOM(text, 0, 1)
   2739    );
   2740    // Last character.
   2741    checkRectListsMatch(
   2742      tm.getSelectionRects(text.length - 1, text.length),
   2743      placeAndSelectTextInDOM(text, text.length - 1, text.length)
   2744    );
   2745    // Whole string.
   2746    checkRectListsMatch(
   2747      tm.getSelectionRects(0, text.length),
   2748      placeAndSelectTextInDOM(text, 0, text.length)
   2749    );
   2750    // Intermediate string.
   2751    checkRectListsMatch(
   2752      tm.getSelectionRects(1, text.length - 1),
   2753      placeAndSelectTextInDOM(text, 1, text.length - 1)
   2754    );
   2755    // Invalid start > end string. Creates 0 width rectangle.
   2756    checkRectListsMatch(
   2757      tm.getSelectionRects(3, 2),
   2758      placeAndSelectTextInDOM(text, 3, 2)
   2759    );
   2760    checkRectListsMatch(
   2761      tm.getSelectionRects(1, 0),
   2762      placeAndSelectTextInDOM(text, 1, 0)
   2763    );
   2764  }
   2765 }, "Check that TextMetrics::getSelectionRects() matches its DOM equivalent, with direction ltr, text align right, 10px letter spacing, and directional-override.");
   2766 
   2767 test(t => {
   2768  const canvas = new OffscreenCanvas(100, 50);
   2769  const ctx = canvas.getContext('2d');
   2770 
   2771  function placeAndSelectTextInDOM(text, from, to) {
   2772    const el = document.createElement("div");
   2773    el.innerHTML = text;
   2774    el.style.font = '50px sans-serif';
   2775    el.style.direction = 'rtl';
   2776    el.style.textAlign = 'right';
   2777    el.style.letterSpacing = '10px';
   2778    document.body.appendChild(el);
   2779 
   2780    let range = document.createRange();
   2781 
   2782    range.setStart(el.childNodes[0], 0);
   2783    range.setEnd(el.childNodes[0], text.length);
   2784    const parent = range.getClientRects()[0];
   2785    let width = 0;
   2786    for (const rect of range.getClientRects()) {
   2787      width += rect.width;
   2788    }
   2789 
   2790    range.setStart(el.childNodes[0], from);
   2791    range.setEnd(el.childNodes[0], to);
   2792 
   2793    let sel = window.getSelection();
   2794    sel.removeAllRanges();
   2795    sel.addRange(range);
   2796 
   2797    let sel_rects = sel.getRangeAt(0).getClientRects();
   2798 
   2799    document.body.removeChild(el);
   2800 
   2801    // Offset to the alignment point determined by textAlign.
   2802    let text_align_dx;
   2803    switch (el.style.textAlign) {
   2804      case 'right':
   2805        text_align_dx = width;
   2806        break;
   2807      case 'center':
   2808        text_align_dx = width / 2;
   2809        break;
   2810      default:
   2811        text_align_dx = 0;
   2812    }
   2813 
   2814    for(let i = 0 ; i < sel_rects.length ; ++i) {
   2815      sel_rects[i].x -= parent.x + text_align_dx;
   2816      sel_rects[i].y -= parent.y;
   2817    }
   2818 
   2819    return sel_rects;
   2820  }
   2821 
   2822  function checkRectListsMatch(list_a, list_b) {
   2823    _assertSame(list_a.length, list_b.length, "list_a.length", "list_b.length");
   2824    for(let i = 0 ; i < list_a.length ; ++i) {
   2825      assert_approx_equals(list_a[i].x, list_b[i].x, 1.0);
   2826      assert_approx_equals(list_a[i].width, list_b[i].width, 1.0);
   2827      assert_approx_equals(list_a[i].height, list_b[i].height, 1.0);
   2828      // Y-position not tested here as getting the baseline for text in the
   2829      // DOM is not straightforward.
   2830    }
   2831  }
   2832 
   2833  function addDirectionalOverrideCharacters(text, direction_is_ltr) {
   2834    // source: www.w3.org/International/questions/qa-bidi-unicode-controls.en
   2835    const LTR_OVERRIDE = '\u202D';
   2836    const RTL_OVERRIDE = '\u202E';
   2837    const OVERRIDE_END = '\u202C';
   2838    if (direction_is_ltr) {
   2839      return LTR_OVERRIDE + text + OVERRIDE_END;
   2840    }
   2841    return RTL_OVERRIDE + text + OVERRIDE_END;
   2842  }
   2843 
   2844  ctx.font = '50px sans-serif';
   2845  ctx.direction = 'rtl';
   2846  ctx.textAlign = 'right';
   2847  ctx.letterSpacing = '10px';
   2848 
   2849  const kTexts = [
   2850    'UNAVAILABLE',
   2851    '🏁🎶🏁',
   2852    ')(あ)(',
   2853    '-abcd_',
   2854    'איפה הספרייה?',
   2855    'bidiמתמטיקה'
   2856  ]
   2857 
   2858  for (text of kTexts) {
   2859    text = addDirectionalOverrideCharacters(text, ctx.direction == 'ltr');
   2860    const tm = ctx.measureText(text);
   2861    // First character.
   2862    checkRectListsMatch(
   2863      tm.getSelectionRects(0, 1),
   2864      placeAndSelectTextInDOM(text, 0, 1)
   2865    );
   2866    // Last character.
   2867    checkRectListsMatch(
   2868      tm.getSelectionRects(text.length - 1, text.length),
   2869      placeAndSelectTextInDOM(text, text.length - 1, text.length)
   2870    );
   2871    // Whole string.
   2872    checkRectListsMatch(
   2873      tm.getSelectionRects(0, text.length),
   2874      placeAndSelectTextInDOM(text, 0, text.length)
   2875    );
   2876    // Intermediate string.
   2877    checkRectListsMatch(
   2878      tm.getSelectionRects(1, text.length - 1),
   2879      placeAndSelectTextInDOM(text, 1, text.length - 1)
   2880    );
   2881    // Invalid start > end string. Creates 0 width rectangle.
   2882    checkRectListsMatch(
   2883      tm.getSelectionRects(3, 2),
   2884      placeAndSelectTextInDOM(text, 3, 2)
   2885    );
   2886    checkRectListsMatch(
   2887      tm.getSelectionRects(1, 0),
   2888      placeAndSelectTextInDOM(text, 1, 0)
   2889    );
   2890  }
   2891 }, "Check that TextMetrics::getSelectionRects() matches its DOM equivalent, with direction rtl, text align right, 10px letter spacing, and directional-override.");
   2892 
   2893 </script>