chromium/third_party/blink/web_tests/external/wpt/html/canvas/offscreen/text/2d.text.measure.selection-rects.tentative.html

<!DOCTYPE html>
<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
<title>OffscreenCanvas test: 2d.text.measure.selection-rects.tentative</title>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="/html/canvas/resources/canvas-tests.js"></script>

<h1>2d.text.measure.selection-rects.tentative</h1>

<script>

test(t => {
  const canvas = new OffscreenCanvas(100, 50);
  const ctx = canvas.getContext('2d');

  function placeAndSelectTextInDOM(text, from, to) {
    const el = document.createElement("div");
    el.innerHTML = text;
    el.style.font = '50px sans-serif';
    el.style.direction = 'ltr';
    el.style.textAlign = 'left';
    el.style.letterSpacing = '0px';
    document.body.appendChild(el);

    let range = document.createRange();

    range.setStart(el.childNodes[0], 0);
    range.setEnd(el.childNodes[0], text.length);
    const parent = range.getClientRects()[0];
    let width = 0;
    for (const rect of range.getClientRects()) {
      width += rect.width;
    }

    range.setStart(el.childNodes[0], from);
    range.setEnd(el.childNodes[0], to);

    let sel = window.getSelection();
    sel.removeAllRanges();
    sel.addRange(range);

    let sel_rects = sel.getRangeAt(0).getClientRects();

    document.body.removeChild(el);

    // Offset to the alignment point determined by textAlign.
    let text_align_dx;
    switch (el.style.textAlign) {
      case 'right':
        text_align_dx = width;
        break;
      case 'center':
        text_align_dx = width / 2;
        break;
      default:
        text_align_dx = 0;
    }

    for(let i = 0 ; i < sel_rects.length ; ++i) {
      sel_rects[i].x -= parent.x + text_align_dx;
      sel_rects[i].y -= parent.y;
    }

    return sel_rects;
  }

  function checkRectListsMatch(list_a, list_b) {
    _assertSame(list_a.length, list_b.length, "list_a.length", "list_b.length");
    for(let i = 0 ; i < list_a.length ; ++i) {
      assert_approx_equals(list_a[i].x, list_b[i].x, 1.0);
      assert_approx_equals(list_a[i].width, list_b[i].width, 1.0);
      assert_approx_equals(list_a[i].height, list_b[i].height, 1.0);
      // Y-position not tested here as getting the baseline for text in the
      // DOM is not straightforward.
    }
  }

  ctx.font = '50px sans-serif';
  ctx.direction = 'ltr';
  ctx.textAlign = 'left';
  ctx.letterSpacing = '0px';

  const kTexts = [
    'UNAVAILABLE',
    '🏁🎢🏁',
    'οΌ‰οΌˆγ‚οΌ‰οΌˆ',
    '-abcd_'
  ]

  for (text of kTexts) {
    const tm = ctx.measureText(text);
    // First character.
    checkRectListsMatch(
      tm.getSelectionRects(0, 1),
      placeAndSelectTextInDOM(text, 0, 1)
    );
    // Last character.
    checkRectListsMatch(
      tm.getSelectionRects(text.length - 1, text.length),
      placeAndSelectTextInDOM(text, text.length - 1, text.length)
    );
    // Whole string.
    checkRectListsMatch(
      tm.getSelectionRects(0, text.length),
      placeAndSelectTextInDOM(text, 0, text.length)
    );
    // Intermediate string.
    checkRectListsMatch(
      tm.getSelectionRects(1, text.length - 1),
      placeAndSelectTextInDOM(text, 1, text.length - 1)
    );
    // Invalid start > end string. Creates 0 width rectangle.
    checkRectListsMatch(
      tm.getSelectionRects(3, 2),
      placeAndSelectTextInDOM(text, 3, 2)
    );
    checkRectListsMatch(
      tm.getSelectionRects(1, 0),
      placeAndSelectTextInDOM(text, 1, 0)
    );
  }
}, "Check that TextMetrics::getSelectionRects() matches its DOM equivalent, with direction ltr, text align left, 0px letter spacing, and no-directional-override.");

test(t => {
  const canvas = new OffscreenCanvas(100, 50);
  const ctx = canvas.getContext('2d');

  function placeAndSelectTextInDOM(text, from, to) {
    const el = document.createElement("div");
    el.innerHTML = text;
    el.style.font = '50px sans-serif';
    el.style.direction = 'rtl';
    el.style.textAlign = 'left';
    el.style.letterSpacing = '0px';
    document.body.appendChild(el);

    let range = document.createRange();

    range.setStart(el.childNodes[0], 0);
    range.setEnd(el.childNodes[0], text.length);
    const parent = range.getClientRects()[0];
    let width = 0;
    for (const rect of range.getClientRects()) {
      width += rect.width;
    }

    range.setStart(el.childNodes[0], from);
    range.setEnd(el.childNodes[0], to);

    let sel = window.getSelection();
    sel.removeAllRanges();
    sel.addRange(range);

    let sel_rects = sel.getRangeAt(0).getClientRects();

    document.body.removeChild(el);

    // Offset to the alignment point determined by textAlign.
    let text_align_dx;
    switch (el.style.textAlign) {
      case 'right':
        text_align_dx = width;
        break;
      case 'center':
        text_align_dx = width / 2;
        break;
      default:
        text_align_dx = 0;
    }

    for(let i = 0 ; i < sel_rects.length ; ++i) {
      sel_rects[i].x -= parent.x + text_align_dx;
      sel_rects[i].y -= parent.y;
    }

    return sel_rects;
  }

  function checkRectListsMatch(list_a, list_b) {
    _assertSame(list_a.length, list_b.length, "list_a.length", "list_b.length");
    for(let i = 0 ; i < list_a.length ; ++i) {
      assert_approx_equals(list_a[i].x, list_b[i].x, 1.0);
      assert_approx_equals(list_a[i].width, list_b[i].width, 1.0);
      assert_approx_equals(list_a[i].height, list_b[i].height, 1.0);
      // Y-position not tested here as getting the baseline for text in the
      // DOM is not straightforward.
    }
  }

  ctx.font = '50px sans-serif';
  ctx.direction = 'rtl';
  ctx.textAlign = 'left';
  ctx.letterSpacing = '0px';

  const kTexts = [
    'UNAVAILABLE',
    '🏁🎢🏁',
    'οΌ‰οΌˆγ‚οΌ‰οΌˆ',
    '-abcd_'
  ]

  for (text of kTexts) {
    const tm = ctx.measureText(text);
    // First character.
    checkRectListsMatch(
      tm.getSelectionRects(0, 1),
      placeAndSelectTextInDOM(text, 0, 1)
    );
    // Last character.
    checkRectListsMatch(
      tm.getSelectionRects(text.length - 1, text.length),
      placeAndSelectTextInDOM(text, text.length - 1, text.length)
    );
    // Whole string.
    checkRectListsMatch(
      tm.getSelectionRects(0, text.length),
      placeAndSelectTextInDOM(text, 0, text.length)
    );
    // Intermediate string.
    checkRectListsMatch(
      tm.getSelectionRects(1, text.length - 1),
      placeAndSelectTextInDOM(text, 1, text.length - 1)
    );
    // Invalid start > end string. Creates 0 width rectangle.
    checkRectListsMatch(
      tm.getSelectionRects(3, 2),
      placeAndSelectTextInDOM(text, 3, 2)
    );
    checkRectListsMatch(
      tm.getSelectionRects(1, 0),
      placeAndSelectTextInDOM(text, 1, 0)
    );
  }
}, "Check that TextMetrics::getSelectionRects() matches its DOM equivalent, with direction rtl, text align left, 0px letter spacing, and no-directional-override.");

test(t => {
  const canvas = new OffscreenCanvas(100, 50);
  const ctx = canvas.getContext('2d');

  function placeAndSelectTextInDOM(text, from, to) {
    const el = document.createElement("div");
    el.innerHTML = text;
    el.style.font = '50px sans-serif';
    el.style.direction = 'ltr';
    el.style.textAlign = 'center';
    el.style.letterSpacing = '0px';
    document.body.appendChild(el);

    let range = document.createRange();

    range.setStart(el.childNodes[0], 0);
    range.setEnd(el.childNodes[0], text.length);
    const parent = range.getClientRects()[0];
    let width = 0;
    for (const rect of range.getClientRects()) {
      width += rect.width;
    }

    range.setStart(el.childNodes[0], from);
    range.setEnd(el.childNodes[0], to);

    let sel = window.getSelection();
    sel.removeAllRanges();
    sel.addRange(range);

    let sel_rects = sel.getRangeAt(0).getClientRects();

    document.body.removeChild(el);

    // Offset to the alignment point determined by textAlign.
    let text_align_dx;
    switch (el.style.textAlign) {
      case 'right':
        text_align_dx = width;
        break;
      case 'center':
        text_align_dx = width / 2;
        break;
      default:
        text_align_dx = 0;
    }

    for(let i = 0 ; i < sel_rects.length ; ++i) {
      sel_rects[i].x -= parent.x + text_align_dx;
      sel_rects[i].y -= parent.y;
    }

    return sel_rects;
  }

  function checkRectListsMatch(list_a, list_b) {
    _assertSame(list_a.length, list_b.length, "list_a.length", "list_b.length");
    for(let i = 0 ; i < list_a.length ; ++i) {
      assert_approx_equals(list_a[i].x, list_b[i].x, 1.0);
      assert_approx_equals(list_a[i].width, list_b[i].width, 1.0);
      assert_approx_equals(list_a[i].height, list_b[i].height, 1.0);
      // Y-position not tested here as getting the baseline for text in the
      // DOM is not straightforward.
    }
  }

  ctx.font = '50px sans-serif';
  ctx.direction = 'ltr';
  ctx.textAlign = 'center';
  ctx.letterSpacing = '0px';

  const kTexts = [
    'UNAVAILABLE',
    '🏁🎢🏁',
    'οΌ‰οΌˆγ‚οΌ‰οΌˆ',
    '-abcd_'
  ]

  for (text of kTexts) {
    const tm = ctx.measureText(text);
    // First character.
    checkRectListsMatch(
      tm.getSelectionRects(0, 1),
      placeAndSelectTextInDOM(text, 0, 1)
    );
    // Last character.
    checkRectListsMatch(
      tm.getSelectionRects(text.length - 1, text.length),
      placeAndSelectTextInDOM(text, text.length - 1, text.length)
    );
    // Whole string.
    checkRectListsMatch(
      tm.getSelectionRects(0, text.length),
      placeAndSelectTextInDOM(text, 0, text.length)
    );
    // Intermediate string.
    checkRectListsMatch(
      tm.getSelectionRects(1, text.length - 1),
      placeAndSelectTextInDOM(text, 1, text.length - 1)
    );
    // Invalid start > end string. Creates 0 width rectangle.
    checkRectListsMatch(
      tm.getSelectionRects(3, 2),
      placeAndSelectTextInDOM(text, 3, 2)
    );
    checkRectListsMatch(
      tm.getSelectionRects(1, 0),
      placeAndSelectTextInDOM(text, 1, 0)
    );
  }
}, "Check that TextMetrics::getSelectionRects() matches its DOM equivalent, with direction ltr, text align center, 0px letter spacing, and no-directional-override.");

test(t => {
  const canvas = new OffscreenCanvas(100, 50);
  const ctx = canvas.getContext('2d');

  function placeAndSelectTextInDOM(text, from, to) {
    const el = document.createElement("div");
    el.innerHTML = text;
    el.style.font = '50px sans-serif';
    el.style.direction = 'rtl';
    el.style.textAlign = 'center';
    el.style.letterSpacing = '0px';
    document.body.appendChild(el);

    let range = document.createRange();

    range.setStart(el.childNodes[0], 0);
    range.setEnd(el.childNodes[0], text.length);
    const parent = range.getClientRects()[0];
    let width = 0;
    for (const rect of range.getClientRects()) {
      width += rect.width;
    }

    range.setStart(el.childNodes[0], from);
    range.setEnd(el.childNodes[0], to);

    let sel = window.getSelection();
    sel.removeAllRanges();
    sel.addRange(range);

    let sel_rects = sel.getRangeAt(0).getClientRects();

    document.body.removeChild(el);

    // Offset to the alignment point determined by textAlign.
    let text_align_dx;
    switch (el.style.textAlign) {
      case 'right':
        text_align_dx = width;
        break;
      case 'center':
        text_align_dx = width / 2;
        break;
      default:
        text_align_dx = 0;
    }

    for(let i = 0 ; i < sel_rects.length ; ++i) {
      sel_rects[i].x -= parent.x + text_align_dx;
      sel_rects[i].y -= parent.y;
    }

    return sel_rects;
  }

  function checkRectListsMatch(list_a, list_b) {
    _assertSame(list_a.length, list_b.length, "list_a.length", "list_b.length");
    for(let i = 0 ; i < list_a.length ; ++i) {
      assert_approx_equals(list_a[i].x, list_b[i].x, 1.0);
      assert_approx_equals(list_a[i].width, list_b[i].width, 1.0);
      assert_approx_equals(list_a[i].height, list_b[i].height, 1.0);
      // Y-position not tested here as getting the baseline for text in the
      // DOM is not straightforward.
    }
  }

  ctx.font = '50px sans-serif';
  ctx.direction = 'rtl';
  ctx.textAlign = 'center';
  ctx.letterSpacing = '0px';

  const kTexts = [
    'UNAVAILABLE',
    '🏁🎢🏁',
    'οΌ‰οΌˆγ‚οΌ‰οΌˆ',
    '-abcd_'
  ]

  for (text of kTexts) {
    const tm = ctx.measureText(text);
    // First character.
    checkRectListsMatch(
      tm.getSelectionRects(0, 1),
      placeAndSelectTextInDOM(text, 0, 1)
    );
    // Last character.
    checkRectListsMatch(
      tm.getSelectionRects(text.length - 1, text.length),
      placeAndSelectTextInDOM(text, text.length - 1, text.length)
    );
    // Whole string.
    checkRectListsMatch(
      tm.getSelectionRects(0, text.length),
      placeAndSelectTextInDOM(text, 0, text.length)
    );
    // Intermediate string.
    checkRectListsMatch(
      tm.getSelectionRects(1, text.length - 1),
      placeAndSelectTextInDOM(text, 1, text.length - 1)
    );
    // Invalid start > end string. Creates 0 width rectangle.
    checkRectListsMatch(
      tm.getSelectionRects(3, 2),
      placeAndSelectTextInDOM(text, 3, 2)
    );
    checkRectListsMatch(
      tm.getSelectionRects(1, 0),
      placeAndSelectTextInDOM(text, 1, 0)
    );
  }
}, "Check that TextMetrics::getSelectionRects() matches its DOM equivalent, with direction rtl, text align center, 0px letter spacing, and no-directional-override.");

test(t => {
  const canvas = new OffscreenCanvas(100, 50);
  const ctx = canvas.getContext('2d');

  function placeAndSelectTextInDOM(text, from, to) {
    const el = document.createElement("div");
    el.innerHTML = text;
    el.style.font = '50px sans-serif';
    el.style.direction = 'ltr';
    el.style.textAlign = 'right';
    el.style.letterSpacing = '0px';
    document.body.appendChild(el);

    let range = document.createRange();

    range.setStart(el.childNodes[0], 0);
    range.setEnd(el.childNodes[0], text.length);
    const parent = range.getClientRects()[0];
    let width = 0;
    for (const rect of range.getClientRects()) {
      width += rect.width;
    }

    range.setStart(el.childNodes[0], from);
    range.setEnd(el.childNodes[0], to);

    let sel = window.getSelection();
    sel.removeAllRanges();
    sel.addRange(range);

    let sel_rects = sel.getRangeAt(0).getClientRects();

    document.body.removeChild(el);

    // Offset to the alignment point determined by textAlign.
    let text_align_dx;
    switch (el.style.textAlign) {
      case 'right':
        text_align_dx = width;
        break;
      case 'center':
        text_align_dx = width / 2;
        break;
      default:
        text_align_dx = 0;
    }

    for(let i = 0 ; i < sel_rects.length ; ++i) {
      sel_rects[i].x -= parent.x + text_align_dx;
      sel_rects[i].y -= parent.y;
    }

    return sel_rects;
  }

  function checkRectListsMatch(list_a, list_b) {
    _assertSame(list_a.length, list_b.length, "list_a.length", "list_b.length");
    for(let i = 0 ; i < list_a.length ; ++i) {
      assert_approx_equals(list_a[i].x, list_b[i].x, 1.0);
      assert_approx_equals(list_a[i].width, list_b[i].width, 1.0);
      assert_approx_equals(list_a[i].height, list_b[i].height, 1.0);
      // Y-position not tested here as getting the baseline for text in the
      // DOM is not straightforward.
    }
  }

  ctx.font = '50px sans-serif';
  ctx.direction = 'ltr';
  ctx.textAlign = 'right';
  ctx.letterSpacing = '0px';

  const kTexts = [
    'UNAVAILABLE',
    '🏁🎢🏁',
    'οΌ‰οΌˆγ‚οΌ‰οΌˆ',
    '-abcd_'
  ]

  for (text of kTexts) {
    const tm = ctx.measureText(text);
    // First character.
    checkRectListsMatch(
      tm.getSelectionRects(0, 1),
      placeAndSelectTextInDOM(text, 0, 1)
    );
    // Last character.
    checkRectListsMatch(
      tm.getSelectionRects(text.length - 1, text.length),
      placeAndSelectTextInDOM(text, text.length - 1, text.length)
    );
    // Whole string.
    checkRectListsMatch(
      tm.getSelectionRects(0, text.length),
      placeAndSelectTextInDOM(text, 0, text.length)
    );
    // Intermediate string.
    checkRectListsMatch(
      tm.getSelectionRects(1, text.length - 1),
      placeAndSelectTextInDOM(text, 1, text.length - 1)
    );
    // Invalid start > end string. Creates 0 width rectangle.
    checkRectListsMatch(
      tm.getSelectionRects(3, 2),
      placeAndSelectTextInDOM(text, 3, 2)
    );
    checkRectListsMatch(
      tm.getSelectionRects(1, 0),
      placeAndSelectTextInDOM(text, 1, 0)
    );
  }
}, "Check that TextMetrics::getSelectionRects() matches its DOM equivalent, with direction ltr, text align right, 0px letter spacing, and no-directional-override.");

test(t => {
  const canvas = new OffscreenCanvas(100, 50);
  const ctx = canvas.getContext('2d');

  function placeAndSelectTextInDOM(text, from, to) {
    const el = document.createElement("div");
    el.innerHTML = text;
    el.style.font = '50px sans-serif';
    el.style.direction = 'rtl';
    el.style.textAlign = 'right';
    el.style.letterSpacing = '0px';
    document.body.appendChild(el);

    let range = document.createRange();

    range.setStart(el.childNodes[0], 0);
    range.setEnd(el.childNodes[0], text.length);
    const parent = range.getClientRects()[0];
    let width = 0;
    for (const rect of range.getClientRects()) {
      width += rect.width;
    }

    range.setStart(el.childNodes[0], from);
    range.setEnd(el.childNodes[0], to);

    let sel = window.getSelection();
    sel.removeAllRanges();
    sel.addRange(range);

    let sel_rects = sel.getRangeAt(0).getClientRects();

    document.body.removeChild(el);

    // Offset to the alignment point determined by textAlign.
    let text_align_dx;
    switch (el.style.textAlign) {
      case 'right':
        text_align_dx = width;
        break;
      case 'center':
        text_align_dx = width / 2;
        break;
      default:
        text_align_dx = 0;
    }

    for(let i = 0 ; i < sel_rects.length ; ++i) {
      sel_rects[i].x -= parent.x + text_align_dx;
      sel_rects[i].y -= parent.y;
    }

    return sel_rects;
  }

  function checkRectListsMatch(list_a, list_b) {
    _assertSame(list_a.length, list_b.length, "list_a.length", "list_b.length");
    for(let i = 0 ; i < list_a.length ; ++i) {
      assert_approx_equals(list_a[i].x, list_b[i].x, 1.0);
      assert_approx_equals(list_a[i].width, list_b[i].width, 1.0);
      assert_approx_equals(list_a[i].height, list_b[i].height, 1.0);
      // Y-position not tested here as getting the baseline for text in the
      // DOM is not straightforward.
    }
  }

  ctx.font = '50px sans-serif';
  ctx.direction = 'rtl';
  ctx.textAlign = 'right';
  ctx.letterSpacing = '0px';

  const kTexts = [
    'UNAVAILABLE',
    '🏁🎢🏁',
    'οΌ‰οΌˆγ‚οΌ‰οΌˆ',
    '-abcd_'
  ]

  for (text of kTexts) {
    const tm = ctx.measureText(text);
    // First character.
    checkRectListsMatch(
      tm.getSelectionRects(0, 1),
      placeAndSelectTextInDOM(text, 0, 1)
    );
    // Last character.
    checkRectListsMatch(
      tm.getSelectionRects(text.length - 1, text.length),
      placeAndSelectTextInDOM(text, text.length - 1, text.length)
    );
    // Whole string.
    checkRectListsMatch(
      tm.getSelectionRects(0, text.length),
      placeAndSelectTextInDOM(text, 0, text.length)
    );
    // Intermediate string.
    checkRectListsMatch(
      tm.getSelectionRects(1, text.length - 1),
      placeAndSelectTextInDOM(text, 1, text.length - 1)
    );
    // Invalid start > end string. Creates 0 width rectangle.
    checkRectListsMatch(
      tm.getSelectionRects(3, 2),
      placeAndSelectTextInDOM(text, 3, 2)
    );
    checkRectListsMatch(
      tm.getSelectionRects(1, 0),
      placeAndSelectTextInDOM(text, 1, 0)
    );
  }
}, "Check that TextMetrics::getSelectionRects() matches its DOM equivalent, with direction rtl, text align right, 0px letter spacing, and no-directional-override.");

test(t => {
  const canvas = new OffscreenCanvas(100, 50);
  const ctx = canvas.getContext('2d');

  function placeAndSelectTextInDOM(text, from, to) {
    const el = document.createElement("div");
    el.innerHTML = text;
    el.style.font = '50px sans-serif';
    el.style.direction = 'ltr';
    el.style.textAlign = 'left';
    el.style.letterSpacing = '10px';
    document.body.appendChild(el);

    let range = document.createRange();

    range.setStart(el.childNodes[0], 0);
    range.setEnd(el.childNodes[0], text.length);
    const parent = range.getClientRects()[0];
    let width = 0;
    for (const rect of range.getClientRects()) {
      width += rect.width;
    }

    range.setStart(el.childNodes[0], from);
    range.setEnd(el.childNodes[0], to);

    let sel = window.getSelection();
    sel.removeAllRanges();
    sel.addRange(range);

    let sel_rects = sel.getRangeAt(0).getClientRects();

    document.body.removeChild(el);

    // Offset to the alignment point determined by textAlign.
    let text_align_dx;
    switch (el.style.textAlign) {
      case 'right':
        text_align_dx = width;
        break;
      case 'center':
        text_align_dx = width / 2;
        break;
      default:
        text_align_dx = 0;
    }

    for(let i = 0 ; i < sel_rects.length ; ++i) {
      sel_rects[i].x -= parent.x + text_align_dx;
      sel_rects[i].y -= parent.y;
    }

    return sel_rects;
  }

  function checkRectListsMatch(list_a, list_b) {
    _assertSame(list_a.length, list_b.length, "list_a.length", "list_b.length");
    for(let i = 0 ; i < list_a.length ; ++i) {
      assert_approx_equals(list_a[i].x, list_b[i].x, 1.0);
      assert_approx_equals(list_a[i].width, list_b[i].width, 1.0);
      assert_approx_equals(list_a[i].height, list_b[i].height, 1.0);
      // Y-position not tested here as getting the baseline for text in the
      // DOM is not straightforward.
    }
  }

  ctx.font = '50px sans-serif';
  ctx.direction = 'ltr';
  ctx.textAlign = 'left';
  ctx.letterSpacing = '10px';

  const kTexts = [
    'UNAVAILABLE',
    '🏁🎢🏁',
    'οΌ‰οΌˆγ‚οΌ‰οΌˆ',
    '-abcd_'
  ]

  for (text of kTexts) {
    const tm = ctx.measureText(text);
    // First character.
    checkRectListsMatch(
      tm.getSelectionRects(0, 1),
      placeAndSelectTextInDOM(text, 0, 1)
    );
    // Last character.
    checkRectListsMatch(
      tm.getSelectionRects(text.length - 1, text.length),
      placeAndSelectTextInDOM(text, text.length - 1, text.length)
    );
    // Whole string.
    checkRectListsMatch(
      tm.getSelectionRects(0, text.length),
      placeAndSelectTextInDOM(text, 0, text.length)
    );
    // Intermediate string.
    checkRectListsMatch(
      tm.getSelectionRects(1, text.length - 1),
      placeAndSelectTextInDOM(text, 1, text.length - 1)
    );
    // Invalid start > end string. Creates 0 width rectangle.
    checkRectListsMatch(
      tm.getSelectionRects(3, 2),
      placeAndSelectTextInDOM(text, 3, 2)
    );
    checkRectListsMatch(
      tm.getSelectionRects(1, 0),
      placeAndSelectTextInDOM(text, 1, 0)
    );
  }
}, "Check that TextMetrics::getSelectionRects() matches its DOM equivalent, with direction ltr, text align left, 10px letter spacing, and no-directional-override.");

test(t => {
  const canvas = new OffscreenCanvas(100, 50);
  const ctx = canvas.getContext('2d');

  function placeAndSelectTextInDOM(text, from, to) {
    const el = document.createElement("div");
    el.innerHTML = text;
    el.style.font = '50px sans-serif';
    el.style.direction = 'rtl';
    el.style.textAlign = 'left';
    el.style.letterSpacing = '10px';
    document.body.appendChild(el);

    let range = document.createRange();

    range.setStart(el.childNodes[0], 0);
    range.setEnd(el.childNodes[0], text.length);
    const parent = range.getClientRects()[0];
    let width = 0;
    for (const rect of range.getClientRects()) {
      width += rect.width;
    }

    range.setStart(el.childNodes[0], from);
    range.setEnd(el.childNodes[0], to);

    let sel = window.getSelection();
    sel.removeAllRanges();
    sel.addRange(range);

    let sel_rects = sel.getRangeAt(0).getClientRects();

    document.body.removeChild(el);

    // Offset to the alignment point determined by textAlign.
    let text_align_dx;
    switch (el.style.textAlign) {
      case 'right':
        text_align_dx = width;
        break;
      case 'center':
        text_align_dx = width / 2;
        break;
      default:
        text_align_dx = 0;
    }

    for(let i = 0 ; i < sel_rects.length ; ++i) {
      sel_rects[i].x -= parent.x + text_align_dx;
      sel_rects[i].y -= parent.y;
    }

    return sel_rects;
  }

  function checkRectListsMatch(list_a, list_b) {
    _assertSame(list_a.length, list_b.length, "list_a.length", "list_b.length");
    for(let i = 0 ; i < list_a.length ; ++i) {
      assert_approx_equals(list_a[i].x, list_b[i].x, 1.0);
      assert_approx_equals(list_a[i].width, list_b[i].width, 1.0);
      assert_approx_equals(list_a[i].height, list_b[i].height, 1.0);
      // Y-position not tested here as getting the baseline for text in the
      // DOM is not straightforward.
    }
  }

  ctx.font = '50px sans-serif';
  ctx.direction = 'rtl';
  ctx.textAlign = 'left';
  ctx.letterSpacing = '10px';

  const kTexts = [
    'UNAVAILABLE',
    '🏁🎢🏁',
    'οΌ‰οΌˆγ‚οΌ‰οΌˆ',
    '-abcd_'
  ]

  for (text of kTexts) {
    const tm = ctx.measureText(text);
    // First character.
    checkRectListsMatch(
      tm.getSelectionRects(0, 1),
      placeAndSelectTextInDOM(text, 0, 1)
    );
    // Last character.
    checkRectListsMatch(
      tm.getSelectionRects(text.length - 1, text.length),
      placeAndSelectTextInDOM(text, text.length - 1, text.length)
    );
    // Whole string.
    checkRectListsMatch(
      tm.getSelectionRects(0, text.length),
      placeAndSelectTextInDOM(text, 0, text.length)
    );
    // Intermediate string.
    checkRectListsMatch(
      tm.getSelectionRects(1, text.length - 1),
      placeAndSelectTextInDOM(text, 1, text.length - 1)
    );
    // Invalid start > end string. Creates 0 width rectangle.
    checkRectListsMatch(
      tm.getSelectionRects(3, 2),
      placeAndSelectTextInDOM(text, 3, 2)
    );
    checkRectListsMatch(
      tm.getSelectionRects(1, 0),
      placeAndSelectTextInDOM(text, 1, 0)
    );
  }
}, "Check that TextMetrics::getSelectionRects() matches its DOM equivalent, with direction rtl, text align left, 10px letter spacing, and no-directional-override.");

test(t => {
  const canvas = new OffscreenCanvas(100, 50);
  const ctx = canvas.getContext('2d');

  function placeAndSelectTextInDOM(text, from, to) {
    const el = document.createElement("div");
    el.innerHTML = text;
    el.style.font = '50px sans-serif';
    el.style.direction = 'ltr';
    el.style.textAlign = 'center';
    el.style.letterSpacing = '10px';
    document.body.appendChild(el);

    let range = document.createRange();

    range.setStart(el.childNodes[0], 0);
    range.setEnd(el.childNodes[0], text.length);
    const parent = range.getClientRects()[0];
    let width = 0;
    for (const rect of range.getClientRects()) {
      width += rect.width;
    }

    range.setStart(el.childNodes[0], from);
    range.setEnd(el.childNodes[0], to);

    let sel = window.getSelection();
    sel.removeAllRanges();
    sel.addRange(range);

    let sel_rects = sel.getRangeAt(0).getClientRects();

    document.body.removeChild(el);

    // Offset to the alignment point determined by textAlign.
    let text_align_dx;
    switch (el.style.textAlign) {
      case 'right':
        text_align_dx = width;
        break;
      case 'center':
        text_align_dx = width / 2;
        break;
      default:
        text_align_dx = 0;
    }

    for(let i = 0 ; i < sel_rects.length ; ++i) {
      sel_rects[i].x -= parent.x + text_align_dx;
      sel_rects[i].y -= parent.y;
    }

    return sel_rects;
  }

  function checkRectListsMatch(list_a, list_b) {
    _assertSame(list_a.length, list_b.length, "list_a.length", "list_b.length");
    for(let i = 0 ; i < list_a.length ; ++i) {
      assert_approx_equals(list_a[i].x, list_b[i].x, 1.0);
      assert_approx_equals(list_a[i].width, list_b[i].width, 1.0);
      assert_approx_equals(list_a[i].height, list_b[i].height, 1.0);
      // Y-position not tested here as getting the baseline for text in the
      // DOM is not straightforward.
    }
  }

  ctx.font = '50px sans-serif';
  ctx.direction = 'ltr';
  ctx.textAlign = 'center';
  ctx.letterSpacing = '10px';

  const kTexts = [
    'UNAVAILABLE',
    '🏁🎢🏁',
    'οΌ‰οΌˆγ‚οΌ‰οΌˆ',
    '-abcd_'
  ]

  for (text of kTexts) {
    const tm = ctx.measureText(text);
    // First character.
    checkRectListsMatch(
      tm.getSelectionRects(0, 1),
      placeAndSelectTextInDOM(text, 0, 1)
    );
    // Last character.
    checkRectListsMatch(
      tm.getSelectionRects(text.length - 1, text.length),
      placeAndSelectTextInDOM(text, text.length - 1, text.length)
    );
    // Whole string.
    checkRectListsMatch(
      tm.getSelectionRects(0, text.length),
      placeAndSelectTextInDOM(text, 0, text.length)
    );
    // Intermediate string.
    checkRectListsMatch(
      tm.getSelectionRects(1, text.length - 1),
      placeAndSelectTextInDOM(text, 1, text.length - 1)
    );
    // Invalid start > end string. Creates 0 width rectangle.
    checkRectListsMatch(
      tm.getSelectionRects(3, 2),
      placeAndSelectTextInDOM(text, 3, 2)
    );
    checkRectListsMatch(
      tm.getSelectionRects(1, 0),
      placeAndSelectTextInDOM(text, 1, 0)
    );
  }
}, "Check that TextMetrics::getSelectionRects() matches its DOM equivalent, with direction ltr, text align center, 10px letter spacing, and no-directional-override.");

test(t => {
  const canvas = new OffscreenCanvas(100, 50);
  const ctx = canvas.getContext('2d');

  function placeAndSelectTextInDOM(text, from, to) {
    const el = document.createElement("div");
    el.innerHTML = text;
    el.style.font = '50px sans-serif';
    el.style.direction = 'rtl';
    el.style.textAlign = 'center';
    el.style.letterSpacing = '10px';
    document.body.appendChild(el);

    let range = document.createRange();

    range.setStart(el.childNodes[0], 0);
    range.setEnd(el.childNodes[0], text.length);
    const parent = range.getClientRects()[0];
    let width = 0;
    for (const rect of range.getClientRects()) {
      width += rect.width;
    }

    range.setStart(el.childNodes[0], from);
    range.setEnd(el.childNodes[0], to);

    let sel = window.getSelection();
    sel.removeAllRanges();
    sel.addRange(range);

    let sel_rects = sel.getRangeAt(0).getClientRects();

    document.body.removeChild(el);

    // Offset to the alignment point determined by textAlign.
    let text_align_dx;
    switch (el.style.textAlign) {
      case 'right':
        text_align_dx = width;
        break;
      case 'center':
        text_align_dx = width / 2;
        break;
      default:
        text_align_dx = 0;
    }

    for(let i = 0 ; i < sel_rects.length ; ++i) {
      sel_rects[i].x -= parent.x + text_align_dx;
      sel_rects[i].y -= parent.y;
    }

    return sel_rects;
  }

  function checkRectListsMatch(list_a, list_b) {
    _assertSame(list_a.length, list_b.length, "list_a.length", "list_b.length");
    for(let i = 0 ; i < list_a.length ; ++i) {
      assert_approx_equals(list_a[i].x, list_b[i].x, 1.0);
      assert_approx_equals(list_a[i].width, list_b[i].width, 1.0);
      assert_approx_equals(list_a[i].height, list_b[i].height, 1.0);
      // Y-position not tested here as getting the baseline for text in the
      // DOM is not straightforward.
    }
  }

  ctx.font = '50px sans-serif';
  ctx.direction = 'rtl';
  ctx.textAlign = 'center';
  ctx.letterSpacing = '10px';

  const kTexts = [
    'UNAVAILABLE',
    '🏁🎢🏁',
    'οΌ‰οΌˆγ‚οΌ‰οΌˆ',
    '-abcd_'
  ]

  for (text of kTexts) {
    const tm = ctx.measureText(text);
    // First character.
    checkRectListsMatch(
      tm.getSelectionRects(0, 1),
      placeAndSelectTextInDOM(text, 0, 1)
    );
    // Last character.
    checkRectListsMatch(
      tm.getSelectionRects(text.length - 1, text.length),
      placeAndSelectTextInDOM(text, text.length - 1, text.length)
    );
    // Whole string.
    checkRectListsMatch(
      tm.getSelectionRects(0, text.length),
      placeAndSelectTextInDOM(text, 0, text.length)
    );
    // Intermediate string.
    checkRectListsMatch(
      tm.getSelectionRects(1, text.length - 1),
      placeAndSelectTextInDOM(text, 1, text.length - 1)
    );
    // Invalid start > end string. Creates 0 width rectangle.
    checkRectListsMatch(
      tm.getSelectionRects(3, 2),
      placeAndSelectTextInDOM(text, 3, 2)
    );
    checkRectListsMatch(
      tm.getSelectionRects(1, 0),
      placeAndSelectTextInDOM(text, 1, 0)
    );
  }
}, "Check that TextMetrics::getSelectionRects() matches its DOM equivalent, with direction rtl, text align center, 10px letter spacing, and no-directional-override.");

test(t => {
  const canvas = new OffscreenCanvas(100, 50);
  const ctx = canvas.getContext('2d');

  function placeAndSelectTextInDOM(text, from, to) {
    const el = document.createElement("div");
    el.innerHTML = text;
    el.style.font = '50px sans-serif';
    el.style.direction = 'ltr';
    el.style.textAlign = 'right';
    el.style.letterSpacing = '10px';
    document.body.appendChild(el);

    let range = document.createRange();

    range.setStart(el.childNodes[0], 0);
    range.setEnd(el.childNodes[0], text.length);
    const parent = range.getClientRects()[0];
    let width = 0;
    for (const rect of range.getClientRects()) {
      width += rect.width;
    }

    range.setStart(el.childNodes[0], from);
    range.setEnd(el.childNodes[0], to);

    let sel = window.getSelection();
    sel.removeAllRanges();
    sel.addRange(range);

    let sel_rects = sel.getRangeAt(0).getClientRects();

    document.body.removeChild(el);

    // Offset to the alignment point determined by textAlign.
    let text_align_dx;
    switch (el.style.textAlign) {
      case 'right':
        text_align_dx = width;
        break;
      case 'center':
        text_align_dx = width / 2;
        break;
      default:
        text_align_dx = 0;
    }

    for(let i = 0 ; i < sel_rects.length ; ++i) {
      sel_rects[i].x -= parent.x + text_align_dx;
      sel_rects[i].y -= parent.y;
    }

    return sel_rects;
  }

  function checkRectListsMatch(list_a, list_b) {
    _assertSame(list_a.length, list_b.length, "list_a.length", "list_b.length");
    for(let i = 0 ; i < list_a.length ; ++i) {
      assert_approx_equals(list_a[i].x, list_b[i].x, 1.0);
      assert_approx_equals(list_a[i].width, list_b[i].width, 1.0);
      assert_approx_equals(list_a[i].height, list_b[i].height, 1.0);
      // Y-position not tested here as getting the baseline for text in the
      // DOM is not straightforward.
    }
  }

  ctx.font = '50px sans-serif';
  ctx.direction = 'ltr';
  ctx.textAlign = 'right';
  ctx.letterSpacing = '10px';

  const kTexts = [
    'UNAVAILABLE',
    '🏁🎢🏁',
    'οΌ‰οΌˆγ‚οΌ‰οΌˆ',
    '-abcd_'
  ]

  for (text of kTexts) {
    const tm = ctx.measureText(text);
    // First character.
    checkRectListsMatch(
      tm.getSelectionRects(0, 1),
      placeAndSelectTextInDOM(text, 0, 1)
    );
    // Last character.
    checkRectListsMatch(
      tm.getSelectionRects(text.length - 1, text.length),
      placeAndSelectTextInDOM(text, text.length - 1, text.length)
    );
    // Whole string.
    checkRectListsMatch(
      tm.getSelectionRects(0, text.length),
      placeAndSelectTextInDOM(text, 0, text.length)
    );
    // Intermediate string.
    checkRectListsMatch(
      tm.getSelectionRects(1, text.length - 1),
      placeAndSelectTextInDOM(text, 1, text.length - 1)
    );
    // Invalid start > end string. Creates 0 width rectangle.
    checkRectListsMatch(
      tm.getSelectionRects(3, 2),
      placeAndSelectTextInDOM(text, 3, 2)
    );
    checkRectListsMatch(
      tm.getSelectionRects(1, 0),
      placeAndSelectTextInDOM(text, 1, 0)
    );
  }
}, "Check that TextMetrics::getSelectionRects() matches its DOM equivalent, with direction ltr, text align right, 10px letter spacing, and no-directional-override.");

test(t => {
  const canvas = new OffscreenCanvas(100, 50);
  const ctx = canvas.getContext('2d');

  function placeAndSelectTextInDOM(text, from, to) {
    const el = document.createElement("div");
    el.innerHTML = text;
    el.style.font = '50px sans-serif';
    el.style.direction = 'rtl';
    el.style.textAlign = 'right';
    el.style.letterSpacing = '10px';
    document.body.appendChild(el);

    let range = document.createRange();

    range.setStart(el.childNodes[0], 0);
    range.setEnd(el.childNodes[0], text.length);
    const parent = range.getClientRects()[0];
    let width = 0;
    for (const rect of range.getClientRects()) {
      width += rect.width;
    }

    range.setStart(el.childNodes[0], from);
    range.setEnd(el.childNodes[0], to);

    let sel = window.getSelection();
    sel.removeAllRanges();
    sel.addRange(range);

    let sel_rects = sel.getRangeAt(0).getClientRects();

    document.body.removeChild(el);

    // Offset to the alignment point determined by textAlign.
    let text_align_dx;
    switch (el.style.textAlign) {
      case 'right':
        text_align_dx = width;
        break;
      case 'center':
        text_align_dx = width / 2;
        break;
      default:
        text_align_dx = 0;
    }

    for(let i = 0 ; i < sel_rects.length ; ++i) {
      sel_rects[i].x -= parent.x + text_align_dx;
      sel_rects[i].y -= parent.y;
    }

    return sel_rects;
  }

  function checkRectListsMatch(list_a, list_b) {
    _assertSame(list_a.length, list_b.length, "list_a.length", "list_b.length");
    for(let i = 0 ; i < list_a.length ; ++i) {
      assert_approx_equals(list_a[i].x, list_b[i].x, 1.0);
      assert_approx_equals(list_a[i].width, list_b[i].width, 1.0);
      assert_approx_equals(list_a[i].height, list_b[i].height, 1.0);
      // Y-position not tested here as getting the baseline for text in the
      // DOM is not straightforward.
    }
  }

  ctx.font = '50px sans-serif';
  ctx.direction = 'rtl';
  ctx.textAlign = 'right';
  ctx.letterSpacing = '10px';

  const kTexts = [
    'UNAVAILABLE',
    '🏁🎢🏁',
    'οΌ‰οΌˆγ‚οΌ‰οΌˆ',
    '-abcd_'
  ]

  for (text of kTexts) {
    const tm = ctx.measureText(text);
    // First character.
    checkRectListsMatch(
      tm.getSelectionRects(0, 1),
      placeAndSelectTextInDOM(text, 0, 1)
    );
    // Last character.
    checkRectListsMatch(
      tm.getSelectionRects(text.length - 1, text.length),
      placeAndSelectTextInDOM(text, text.length - 1, text.length)
    );
    // Whole string.
    checkRectListsMatch(
      tm.getSelectionRects(0, text.length),
      placeAndSelectTextInDOM(text, 0, text.length)
    );
    // Intermediate string.
    checkRectListsMatch(
      tm.getSelectionRects(1, text.length - 1),
      placeAndSelectTextInDOM(text, 1, text.length - 1)
    );
    // Invalid start > end string. Creates 0 width rectangle.
    checkRectListsMatch(
      tm.getSelectionRects(3, 2),
      placeAndSelectTextInDOM(text, 3, 2)
    );
    checkRectListsMatch(
      tm.getSelectionRects(1, 0),
      placeAndSelectTextInDOM(text, 1, 0)
    );
  }
}, "Check that TextMetrics::getSelectionRects() matches its DOM equivalent, with direction rtl, text align right, 10px letter spacing, and no-directional-override.");

test(t => {
  const canvas = new OffscreenCanvas(100, 50);
  const ctx = canvas.getContext('2d');

  function placeAndSelectTextInDOM(text, from, to) {
    const el = document.createElement("div");
    el.innerHTML = text;
    el.style.font = '50px sans-serif';
    el.style.direction = 'ltr';
    el.style.textAlign = 'left';
    el.style.letterSpacing = '0px';
    document.body.appendChild(el);

    let range = document.createRange();

    range.setStart(el.childNodes[0], 0);
    range.setEnd(el.childNodes[0], text.length);
    const parent = range.getClientRects()[0];
    let width = 0;
    for (const rect of range.getClientRects()) {
      width += rect.width;
    }

    range.setStart(el.childNodes[0], from);
    range.setEnd(el.childNodes[0], to);

    let sel = window.getSelection();
    sel.removeAllRanges();
    sel.addRange(range);

    let sel_rects = sel.getRangeAt(0).getClientRects();

    document.body.removeChild(el);

    // Offset to the alignment point determined by textAlign.
    let text_align_dx;
    switch (el.style.textAlign) {
      case 'right':
        text_align_dx = width;
        break;
      case 'center':
        text_align_dx = width / 2;
        break;
      default:
        text_align_dx = 0;
    }

    for(let i = 0 ; i < sel_rects.length ; ++i) {
      sel_rects[i].x -= parent.x + text_align_dx;
      sel_rects[i].y -= parent.y;
    }

    return sel_rects;
  }

  function checkRectListsMatch(list_a, list_b) {
    _assertSame(list_a.length, list_b.length, "list_a.length", "list_b.length");
    for(let i = 0 ; i < list_a.length ; ++i) {
      assert_approx_equals(list_a[i].x, list_b[i].x, 1.0);
      assert_approx_equals(list_a[i].width, list_b[i].width, 1.0);
      assert_approx_equals(list_a[i].height, list_b[i].height, 1.0);
      // Y-position not tested here as getting the baseline for text in the
      // DOM is not straightforward.
    }
  }

  function addDirectionalOverrideCharacters(text, direction_is_ltr) {
    // source: www.w3.org/International/questions/qa-bidi-unicode-controls.en
    const LTR_OVERRIDE = '\u202D';
    const RTL_OVERRIDE = '\u202E';
    const OVERRIDE_END = '\u202C';
    if (direction_is_ltr) {
      return LTR_OVERRIDE + text + OVERRIDE_END;
    }
    return RTL_OVERRIDE + text + OVERRIDE_END;
  }

  ctx.font = '50px sans-serif';
  ctx.direction = 'ltr';
  ctx.textAlign = 'left';
  ctx.letterSpacing = '0px';

  const kTexts = [
    'UNAVAILABLE',
    '🏁🎢🏁',
    'οΌ‰οΌˆγ‚οΌ‰οΌˆ',
    '-abcd_'
  ]

  for (text of kTexts) {
    text = addDirectionalOverrideCharacters(text, ctx.direction == 'ltr');
    const tm = ctx.measureText(text);
    // First character.
    checkRectListsMatch(
      tm.getSelectionRects(0, 1),
      placeAndSelectTextInDOM(text, 0, 1)
    );
    // Last character.
    checkRectListsMatch(
      tm.getSelectionRects(text.length - 1, text.length),
      placeAndSelectTextInDOM(text, text.length - 1, text.length)
    );
    // Whole string.
    checkRectListsMatch(
      tm.getSelectionRects(0, text.length),
      placeAndSelectTextInDOM(text, 0, text.length)
    );
    // Intermediate string.
    checkRectListsMatch(
      tm.getSelectionRects(1, text.length - 1),
      placeAndSelectTextInDOM(text, 1, text.length - 1)
    );
    // Invalid start > end string. Creates 0 width rectangle.
    checkRectListsMatch(
      tm.getSelectionRects(3, 2),
      placeAndSelectTextInDOM(text, 3, 2)
    );
    checkRectListsMatch(
      tm.getSelectionRects(1, 0),
      placeAndSelectTextInDOM(text, 1, 0)
    );
  }
}, "Check that TextMetrics::getSelectionRects() matches its DOM equivalent, with direction ltr, text align left, 0px letter spacing, and directional-override.");

test(t => {
  const canvas = new OffscreenCanvas(100, 50);
  const ctx = canvas.getContext('2d');

  function placeAndSelectTextInDOM(text, from, to) {
    const el = document.createElement("div");
    el.innerHTML = text;
    el.style.font = '50px sans-serif';
    el.style.direction = 'rtl';
    el.style.textAlign = 'left';
    el.style.letterSpacing = '0px';
    document.body.appendChild(el);

    let range = document.createRange();

    range.setStart(el.childNodes[0], 0);
    range.setEnd(el.childNodes[0], text.length);
    const parent = range.getClientRects()[0];
    let width = 0;
    for (const rect of range.getClientRects()) {
      width += rect.width;
    }

    range.setStart(el.childNodes[0], from);
    range.setEnd(el.childNodes[0], to);

    let sel = window.getSelection();
    sel.removeAllRanges();
    sel.addRange(range);

    let sel_rects = sel.getRangeAt(0).getClientRects();

    document.body.removeChild(el);

    // Offset to the alignment point determined by textAlign.
    let text_align_dx;
    switch (el.style.textAlign) {
      case 'right':
        text_align_dx = width;
        break;
      case 'center':
        text_align_dx = width / 2;
        break;
      default:
        text_align_dx = 0;
    }

    for(let i = 0 ; i < sel_rects.length ; ++i) {
      sel_rects[i].x -= parent.x + text_align_dx;
      sel_rects[i].y -= parent.y;
    }

    return sel_rects;
  }

  function checkRectListsMatch(list_a, list_b) {
    _assertSame(list_a.length, list_b.length, "list_a.length", "list_b.length");
    for(let i = 0 ; i < list_a.length ; ++i) {
      assert_approx_equals(list_a[i].x, list_b[i].x, 1.0);
      assert_approx_equals(list_a[i].width, list_b[i].width, 1.0);
      assert_approx_equals(list_a[i].height, list_b[i].height, 1.0);
      // Y-position not tested here as getting the baseline for text in the
      // DOM is not straightforward.
    }
  }

  function addDirectionalOverrideCharacters(text, direction_is_ltr) {
    // source: www.w3.org/International/questions/qa-bidi-unicode-controls.en
    const LTR_OVERRIDE = '\u202D';
    const RTL_OVERRIDE = '\u202E';
    const OVERRIDE_END = '\u202C';
    if (direction_is_ltr) {
      return LTR_OVERRIDE + text + OVERRIDE_END;
    }
    return RTL_OVERRIDE + text + OVERRIDE_END;
  }

  ctx.font = '50px sans-serif';
  ctx.direction = 'rtl';
  ctx.textAlign = 'left';
  ctx.letterSpacing = '0px';

  const kTexts = [
    'UNAVAILABLE',
    '🏁🎢🏁',
    'οΌ‰οΌˆγ‚οΌ‰οΌˆ',
    '-abcd_'
  ]

  for (text of kTexts) {
    text = addDirectionalOverrideCharacters(text, ctx.direction == 'ltr');
    const tm = ctx.measureText(text);
    // First character.
    checkRectListsMatch(
      tm.getSelectionRects(0, 1),
      placeAndSelectTextInDOM(text, 0, 1)
    );
    // Last character.
    checkRectListsMatch(
      tm.getSelectionRects(text.length - 1, text.length),
      placeAndSelectTextInDOM(text, text.length - 1, text.length)
    );
    // Whole string.
    checkRectListsMatch(
      tm.getSelectionRects(0, text.length),
      placeAndSelectTextInDOM(text, 0, text.length)
    );
    // Intermediate string.
    checkRectListsMatch(
      tm.getSelectionRects(1, text.length - 1),
      placeAndSelectTextInDOM(text, 1, text.length - 1)
    );
    // Invalid start > end string. Creates 0 width rectangle.
    checkRectListsMatch(
      tm.getSelectionRects(3, 2),
      placeAndSelectTextInDOM(text, 3, 2)
    );
    checkRectListsMatch(
      tm.getSelectionRects(1, 0),
      placeAndSelectTextInDOM(text, 1, 0)
    );
  }
}, "Check that TextMetrics::getSelectionRects() matches its DOM equivalent, with direction rtl, text align left, 0px letter spacing, and directional-override.");

test(t => {
  const canvas = new OffscreenCanvas(100, 50);
  const ctx = canvas.getContext('2d');

  function placeAndSelectTextInDOM(text, from, to) {
    const el = document.createElement("div");
    el.innerHTML = text;
    el.style.font = '50px sans-serif';
    el.style.direction = 'ltr';
    el.style.textAlign = 'center';
    el.style.letterSpacing = '0px';
    document.body.appendChild(el);

    let range = document.createRange();

    range.setStart(el.childNodes[0], 0);
    range.setEnd(el.childNodes[0], text.length);
    const parent = range.getClientRects()[0];
    let width = 0;
    for (const rect of range.getClientRects()) {
      width += rect.width;
    }

    range.setStart(el.childNodes[0], from);
    range.setEnd(el.childNodes[0], to);

    let sel = window.getSelection();
    sel.removeAllRanges();
    sel.addRange(range);

    let sel_rects = sel.getRangeAt(0).getClientRects();

    document.body.removeChild(el);

    // Offset to the alignment point determined by textAlign.
    let text_align_dx;
    switch (el.style.textAlign) {
      case 'right':
        text_align_dx = width;
        break;
      case 'center':
        text_align_dx = width / 2;
        break;
      default:
        text_align_dx = 0;
    }

    for(let i = 0 ; i < sel_rects.length ; ++i) {
      sel_rects[i].x -= parent.x + text_align_dx;
      sel_rects[i].y -= parent.y;
    }

    return sel_rects;
  }

  function checkRectListsMatch(list_a, list_b) {
    _assertSame(list_a.length, list_b.length, "list_a.length", "list_b.length");
    for(let i = 0 ; i < list_a.length ; ++i) {
      assert_approx_equals(list_a[i].x, list_b[i].x, 1.0);
      assert_approx_equals(list_a[i].width, list_b[i].width, 1.0);
      assert_approx_equals(list_a[i].height, list_b[i].height, 1.0);
      // Y-position not tested here as getting the baseline for text in the
      // DOM is not straightforward.
    }
  }

  function addDirectionalOverrideCharacters(text, direction_is_ltr) {
    // source: www.w3.org/International/questions/qa-bidi-unicode-controls.en
    const LTR_OVERRIDE = '\u202D';
    const RTL_OVERRIDE = '\u202E';
    const OVERRIDE_END = '\u202C';
    if (direction_is_ltr) {
      return LTR_OVERRIDE + text + OVERRIDE_END;
    }
    return RTL_OVERRIDE + text + OVERRIDE_END;
  }

  ctx.font = '50px sans-serif';
  ctx.direction = 'ltr';
  ctx.textAlign = 'center';
  ctx.letterSpacing = '0px';

  const kTexts = [
    'UNAVAILABLE',
    '🏁🎢🏁',
    'οΌ‰οΌˆγ‚οΌ‰οΌˆ',
    '-abcd_'
  ]

  for (text of kTexts) {
    text = addDirectionalOverrideCharacters(text, ctx.direction == 'ltr');
    const tm = ctx.measureText(text);
    // First character.
    checkRectListsMatch(
      tm.getSelectionRects(0, 1),
      placeAndSelectTextInDOM(text, 0, 1)
    );
    // Last character.
    checkRectListsMatch(
      tm.getSelectionRects(text.length - 1, text.length),
      placeAndSelectTextInDOM(text, text.length - 1, text.length)
    );
    // Whole string.
    checkRectListsMatch(
      tm.getSelectionRects(0, text.length),
      placeAndSelectTextInDOM(text, 0, text.length)
    );
    // Intermediate string.
    checkRectListsMatch(
      tm.getSelectionRects(1, text.length - 1),
      placeAndSelectTextInDOM(text, 1, text.length - 1)
    );
    // Invalid start > end string. Creates 0 width rectangle.
    checkRectListsMatch(
      tm.getSelectionRects(3, 2),
      placeAndSelectTextInDOM(text, 3, 2)
    );
    checkRectListsMatch(
      tm.getSelectionRects(1, 0),
      placeAndSelectTextInDOM(text, 1, 0)
    );
  }
}, "Check that TextMetrics::getSelectionRects() matches its DOM equivalent, with direction ltr, text align center, 0px letter spacing, and directional-override.");

test(t => {
  const canvas = new OffscreenCanvas(100, 50);
  const ctx = canvas.getContext('2d');

  function placeAndSelectTextInDOM(text, from, to) {
    const el = document.createElement("div");
    el.innerHTML = text;
    el.style.font = '50px sans-serif';
    el.style.direction = 'rtl';
    el.style.textAlign = 'center';
    el.style.letterSpacing = '0px';
    document.body.appendChild(el);

    let range = document.createRange();

    range.setStart(el.childNodes[0], 0);
    range.setEnd(el.childNodes[0], text.length);
    const parent = range.getClientRects()[0];
    let width = 0;
    for (const rect of range.getClientRects()) {
      width += rect.width;
    }

    range.setStart(el.childNodes[0], from);
    range.setEnd(el.childNodes[0], to);

    let sel = window.getSelection();
    sel.removeAllRanges();
    sel.addRange(range);

    let sel_rects = sel.getRangeAt(0).getClientRects();

    document.body.removeChild(el);

    // Offset to the alignment point determined by textAlign.
    let text_align_dx;
    switch (el.style.textAlign) {
      case 'right':
        text_align_dx = width;
        break;
      case 'center':
        text_align_dx = width / 2;
        break;
      default:
        text_align_dx = 0;
    }

    for(let i = 0 ; i < sel_rects.length ; ++i) {
      sel_rects[i].x -= parent.x + text_align_dx;
      sel_rects[i].y -= parent.y;
    }

    return sel_rects;
  }

  function checkRectListsMatch(list_a, list_b) {
    _assertSame(list_a.length, list_b.length, "list_a.length", "list_b.length");
    for(let i = 0 ; i < list_a.length ; ++i) {
      assert_approx_equals(list_a[i].x, list_b[i].x, 1.0);
      assert_approx_equals(list_a[i].width, list_b[i].width, 1.0);
      assert_approx_equals(list_a[i].height, list_b[i].height, 1.0);
      // Y-position not tested here as getting the baseline for text in the
      // DOM is not straightforward.
    }
  }

  function addDirectionalOverrideCharacters(text, direction_is_ltr) {
    // source: www.w3.org/International/questions/qa-bidi-unicode-controls.en
    const LTR_OVERRIDE = '\u202D';
    const RTL_OVERRIDE = '\u202E';
    const OVERRIDE_END = '\u202C';
    if (direction_is_ltr) {
      return LTR_OVERRIDE + text + OVERRIDE_END;
    }
    return RTL_OVERRIDE + text + OVERRIDE_END;
  }

  ctx.font = '50px sans-serif';
  ctx.direction = 'rtl';
  ctx.textAlign = 'center';
  ctx.letterSpacing = '0px';

  const kTexts = [
    'UNAVAILABLE',
    '🏁🎢🏁',
    'οΌ‰οΌˆγ‚οΌ‰οΌˆ',
    '-abcd_'
  ]

  for (text of kTexts) {
    text = addDirectionalOverrideCharacters(text, ctx.direction == 'ltr');
    const tm = ctx.measureText(text);
    // First character.
    checkRectListsMatch(
      tm.getSelectionRects(0, 1),
      placeAndSelectTextInDOM(text, 0, 1)
    );
    // Last character.
    checkRectListsMatch(
      tm.getSelectionRects(text.length - 1, text.length),
      placeAndSelectTextInDOM(text, text.length - 1, text.length)
    );
    // Whole string.
    checkRectListsMatch(
      tm.getSelectionRects(0, text.length),
      placeAndSelectTextInDOM(text, 0, text.length)
    );
    // Intermediate string.
    checkRectListsMatch(
      tm.getSelectionRects(1, text.length - 1),
      placeAndSelectTextInDOM(text, 1, text.length - 1)
    );
    // Invalid start > end string. Creates 0 width rectangle.
    checkRectListsMatch(
      tm.getSelectionRects(3, 2),
      placeAndSelectTextInDOM(text, 3, 2)
    );
    checkRectListsMatch(
      tm.getSelectionRects(1, 0),
      placeAndSelectTextInDOM(text, 1, 0)
    );
  }
}, "Check that TextMetrics::getSelectionRects() matches its DOM equivalent, with direction rtl, text align center, 0px letter spacing, and directional-override.");

test(t => {
  const canvas = new OffscreenCanvas(100, 50);
  const ctx = canvas.getContext('2d');

  function placeAndSelectTextInDOM(text, from, to) {
    const el = document.createElement("div");
    el.innerHTML = text;
    el.style.font = '50px sans-serif';
    el.style.direction = 'ltr';
    el.style.textAlign = 'right';
    el.style.letterSpacing = '0px';
    document.body.appendChild(el);

    let range = document.createRange();

    range.setStart(el.childNodes[0], 0);
    range.setEnd(el.childNodes[0], text.length);
    const parent = range.getClientRects()[0];
    let width = 0;
    for (const rect of range.getClientRects()) {
      width += rect.width;
    }

    range.setStart(el.childNodes[0], from);
    range.setEnd(el.childNodes[0], to);

    let sel = window.getSelection();
    sel.removeAllRanges();
    sel.addRange(range);

    let sel_rects = sel.getRangeAt(0).getClientRects();

    document.body.removeChild(el);

    // Offset to the alignment point determined by textAlign.
    let text_align_dx;
    switch (el.style.textAlign) {
      case 'right':
        text_align_dx = width;
        break;
      case 'center':
        text_align_dx = width / 2;
        break;
      default:
        text_align_dx = 0;
    }

    for(let i = 0 ; i < sel_rects.length ; ++i) {
      sel_rects[i].x -= parent.x + text_align_dx;
      sel_rects[i].y -= parent.y;
    }

    return sel_rects;
  }

  function checkRectListsMatch(list_a, list_b) {
    _assertSame(list_a.length, list_b.length, "list_a.length", "list_b.length");
    for(let i = 0 ; i < list_a.length ; ++i) {
      assert_approx_equals(list_a[i].x, list_b[i].x, 1.0);
      assert_approx_equals(list_a[i].width, list_b[i].width, 1.0);
      assert_approx_equals(list_a[i].height, list_b[i].height, 1.0);
      // Y-position not tested here as getting the baseline for text in the
      // DOM is not straightforward.
    }
  }

  function addDirectionalOverrideCharacters(text, direction_is_ltr) {
    // source: www.w3.org/International/questions/qa-bidi-unicode-controls.en
    const LTR_OVERRIDE = '\u202D';
    const RTL_OVERRIDE = '\u202E';
    const OVERRIDE_END = '\u202C';
    if (direction_is_ltr) {
      return LTR_OVERRIDE + text + OVERRIDE_END;
    }
    return RTL_OVERRIDE + text + OVERRIDE_END;
  }

  ctx.font = '50px sans-serif';
  ctx.direction = 'ltr';
  ctx.textAlign = 'right';
  ctx.letterSpacing = '0px';

  const kTexts = [
    'UNAVAILABLE',
    '🏁🎢🏁',
    'οΌ‰οΌˆγ‚οΌ‰οΌˆ',
    '-abcd_'
  ]

  for (text of kTexts) {
    text = addDirectionalOverrideCharacters(text, ctx.direction == 'ltr');
    const tm = ctx.measureText(text);
    // First character.
    checkRectListsMatch(
      tm.getSelectionRects(0, 1),
      placeAndSelectTextInDOM(text, 0, 1)
    );
    // Last character.
    checkRectListsMatch(
      tm.getSelectionRects(text.length - 1, text.length),
      placeAndSelectTextInDOM(text, text.length - 1, text.length)
    );
    // Whole string.
    checkRectListsMatch(
      tm.getSelectionRects(0, text.length),
      placeAndSelectTextInDOM(text, 0, text.length)
    );
    // Intermediate string.
    checkRectListsMatch(
      tm.getSelectionRects(1, text.length - 1),
      placeAndSelectTextInDOM(text, 1, text.length - 1)
    );
    // Invalid start > end string. Creates 0 width rectangle.
    checkRectListsMatch(
      tm.getSelectionRects(3, 2),
      placeAndSelectTextInDOM(text, 3, 2)
    );
    checkRectListsMatch(
      tm.getSelectionRects(1, 0),
      placeAndSelectTextInDOM(text, 1, 0)
    );
  }
}, "Check that TextMetrics::getSelectionRects() matches its DOM equivalent, with direction ltr, text align right, 0px letter spacing, and directional-override.");

test(t => {
  const canvas = new OffscreenCanvas(100, 50);
  const ctx = canvas.getContext('2d');

  function placeAndSelectTextInDOM(text, from, to) {
    const el = document.createElement("div");
    el.innerHTML = text;
    el.style.font = '50px sans-serif';
    el.style.direction = 'rtl';
    el.style.textAlign = 'right';
    el.style.letterSpacing = '0px';
    document.body.appendChild(el);

    let range = document.createRange();

    range.setStart(el.childNodes[0], 0);
    range.setEnd(el.childNodes[0], text.length);
    const parent = range.getClientRects()[0];
    let width = 0;
    for (const rect of range.getClientRects()) {
      width += rect.width;
    }

    range.setStart(el.childNodes[0], from);
    range.setEnd(el.childNodes[0], to);

    let sel = window.getSelection();
    sel.removeAllRanges();
    sel.addRange(range);

    let sel_rects = sel.getRangeAt(0).getClientRects();

    document.body.removeChild(el);

    // Offset to the alignment point determined by textAlign.
    let text_align_dx;
    switch (el.style.textAlign) {
      case 'right':
        text_align_dx = width;
        break;
      case 'center':
        text_align_dx = width / 2;
        break;
      default:
        text_align_dx = 0;
    }

    for(let i = 0 ; i < sel_rects.length ; ++i) {
      sel_rects[i].x -= parent.x + text_align_dx;
      sel_rects[i].y -= parent.y;
    }

    return sel_rects;
  }

  function checkRectListsMatch(list_a, list_b) {
    _assertSame(list_a.length, list_b.length, "list_a.length", "list_b.length");
    for(let i = 0 ; i < list_a.length ; ++i) {
      assert_approx_equals(list_a[i].x, list_b[i].x, 1.0);
      assert_approx_equals(list_a[i].width, list_b[i].width, 1.0);
      assert_approx_equals(list_a[i].height, list_b[i].height, 1.0);
      // Y-position not tested here as getting the baseline for text in the
      // DOM is not straightforward.
    }
  }

  function addDirectionalOverrideCharacters(text, direction_is_ltr) {
    // source: www.w3.org/International/questions/qa-bidi-unicode-controls.en
    const LTR_OVERRIDE = '\u202D';
    const RTL_OVERRIDE = '\u202E';
    const OVERRIDE_END = '\u202C';
    if (direction_is_ltr) {
      return LTR_OVERRIDE + text + OVERRIDE_END;
    }
    return RTL_OVERRIDE + text + OVERRIDE_END;
  }

  ctx.font = '50px sans-serif';
  ctx.direction = 'rtl';
  ctx.textAlign = 'right';
  ctx.letterSpacing = '0px';

  const kTexts = [
    'UNAVAILABLE',
    '🏁🎢🏁',
    'οΌ‰οΌˆγ‚οΌ‰οΌˆ',
    '-abcd_'
  ]

  for (text of kTexts) {
    text = addDirectionalOverrideCharacters(text, ctx.direction == 'ltr');
    const tm = ctx.measureText(text);
    // First character.
    checkRectListsMatch(
      tm.getSelectionRects(0, 1),
      placeAndSelectTextInDOM(text, 0, 1)
    );
    // Last character.
    checkRectListsMatch(
      tm.getSelectionRects(text.length - 1, text.length),
      placeAndSelectTextInDOM(text, text.length - 1, text.length)
    );
    // Whole string.
    checkRectListsMatch(
      tm.getSelectionRects(0, text.length),
      placeAndSelectTextInDOM(text, 0, text.length)
    );
    // Intermediate string.
    checkRectListsMatch(
      tm.getSelectionRects(1, text.length - 1),
      placeAndSelectTextInDOM(text, 1, text.length - 1)
    );
    // Invalid start > end string. Creates 0 width rectangle.
    checkRectListsMatch(
      tm.getSelectionRects(3, 2),
      placeAndSelectTextInDOM(text, 3, 2)
    );
    checkRectListsMatch(
      tm.getSelectionRects(1, 0),
      placeAndSelectTextInDOM(text, 1, 0)
    );
  }
}, "Check that TextMetrics::getSelectionRects() matches its DOM equivalent, with direction rtl, text align right, 0px letter spacing, and directional-override.");

test(t => {
  const canvas = new OffscreenCanvas(100, 50);
  const ctx = canvas.getContext('2d');

  function placeAndSelectTextInDOM(text, from, to) {
    const el = document.createElement("div");
    el.innerHTML = text;
    el.style.font = '50px sans-serif';
    el.style.direction = 'ltr';
    el.style.textAlign = 'left';
    el.style.letterSpacing = '10px';
    document.body.appendChild(el);

    let range = document.createRange();

    range.setStart(el.childNodes[0], 0);
    range.setEnd(el.childNodes[0], text.length);
    const parent = range.getClientRects()[0];
    let width = 0;
    for (const rect of range.getClientRects()) {
      width += rect.width;
    }

    range.setStart(el.childNodes[0], from);
    range.setEnd(el.childNodes[0], to);

    let sel = window.getSelection();
    sel.removeAllRanges();
    sel.addRange(range);

    let sel_rects = sel.getRangeAt(0).getClientRects();

    document.body.removeChild(el);

    // Offset to the alignment point determined by textAlign.
    let text_align_dx;
    switch (el.style.textAlign) {
      case 'right':
        text_align_dx = width;
        break;
      case 'center':
        text_align_dx = width / 2;
        break;
      default:
        text_align_dx = 0;
    }

    for(let i = 0 ; i < sel_rects.length ; ++i) {
      sel_rects[i].x -= parent.x + text_align_dx;
      sel_rects[i].y -= parent.y;
    }

    return sel_rects;
  }

  function checkRectListsMatch(list_a, list_b) {
    _assertSame(list_a.length, list_b.length, "list_a.length", "list_b.length");
    for(let i = 0 ; i < list_a.length ; ++i) {
      assert_approx_equals(list_a[i].x, list_b[i].x, 1.0);
      assert_approx_equals(list_a[i].width, list_b[i].width, 1.0);
      assert_approx_equals(list_a[i].height, list_b[i].height, 1.0);
      // Y-position not tested here as getting the baseline for text in the
      // DOM is not straightforward.
    }
  }

  function addDirectionalOverrideCharacters(text, direction_is_ltr) {
    // source: www.w3.org/International/questions/qa-bidi-unicode-controls.en
    const LTR_OVERRIDE = '\u202D';
    const RTL_OVERRIDE = '\u202E';
    const OVERRIDE_END = '\u202C';
    if (direction_is_ltr) {
      return LTR_OVERRIDE + text + OVERRIDE_END;
    }
    return RTL_OVERRIDE + text + OVERRIDE_END;
  }

  ctx.font = '50px sans-serif';
  ctx.direction = 'ltr';
  ctx.textAlign = 'left';
  ctx.letterSpacing = '10px';

  const kTexts = [
    'UNAVAILABLE',
    '🏁🎢🏁',
    'οΌ‰οΌˆγ‚οΌ‰οΌˆ',
    '-abcd_'
  ]

  for (text of kTexts) {
    text = addDirectionalOverrideCharacters(text, ctx.direction == 'ltr');
    const tm = ctx.measureText(text);
    // First character.
    checkRectListsMatch(
      tm.getSelectionRects(0, 1),
      placeAndSelectTextInDOM(text, 0, 1)
    );
    // Last character.
    checkRectListsMatch(
      tm.getSelectionRects(text.length - 1, text.length),
      placeAndSelectTextInDOM(text, text.length - 1, text.length)
    );
    // Whole string.
    checkRectListsMatch(
      tm.getSelectionRects(0, text.length),
      placeAndSelectTextInDOM(text, 0, text.length)
    );
    // Intermediate string.
    checkRectListsMatch(
      tm.getSelectionRects(1, text.length - 1),
      placeAndSelectTextInDOM(text, 1, text.length - 1)
    );
    // Invalid start > end string. Creates 0 width rectangle.
    checkRectListsMatch(
      tm.getSelectionRects(3, 2),
      placeAndSelectTextInDOM(text, 3, 2)
    );
    checkRectListsMatch(
      tm.getSelectionRects(1, 0),
      placeAndSelectTextInDOM(text, 1, 0)
    );
  }
}, "Check that TextMetrics::getSelectionRects() matches its DOM equivalent, with direction ltr, text align left, 10px letter spacing, and directional-override.");

test(t => {
  const canvas = new OffscreenCanvas(100, 50);
  const ctx = canvas.getContext('2d');

  function placeAndSelectTextInDOM(text, from, to) {
    const el = document.createElement("div");
    el.innerHTML = text;
    el.style.font = '50px sans-serif';
    el.style.direction = 'rtl';
    el.style.textAlign = 'left';
    el.style.letterSpacing = '10px';
    document.body.appendChild(el);

    let range = document.createRange();

    range.setStart(el.childNodes[0], 0);
    range.setEnd(el.childNodes[0], text.length);
    const parent = range.getClientRects()[0];
    let width = 0;
    for (const rect of range.getClientRects()) {
      width += rect.width;
    }

    range.setStart(el.childNodes[0], from);
    range.setEnd(el.childNodes[0], to);

    let sel = window.getSelection();
    sel.removeAllRanges();
    sel.addRange(range);

    let sel_rects = sel.getRangeAt(0).getClientRects();

    document.body.removeChild(el);

    // Offset to the alignment point determined by textAlign.
    let text_align_dx;
    switch (el.style.textAlign) {
      case 'right':
        text_align_dx = width;
        break;
      case 'center':
        text_align_dx = width / 2;
        break;
      default:
        text_align_dx = 0;
    }

    for(let i = 0 ; i < sel_rects.length ; ++i) {
      sel_rects[i].x -= parent.x + text_align_dx;
      sel_rects[i].y -= parent.y;
    }

    return sel_rects;
  }

  function checkRectListsMatch(list_a, list_b) {
    _assertSame(list_a.length, list_b.length, "list_a.length", "list_b.length");
    for(let i = 0 ; i < list_a.length ; ++i) {
      assert_approx_equals(list_a[i].x, list_b[i].x, 1.0);
      assert_approx_equals(list_a[i].width, list_b[i].width, 1.0);
      assert_approx_equals(list_a[i].height, list_b[i].height, 1.0);
      // Y-position not tested here as getting the baseline for text in the
      // DOM is not straightforward.
    }
  }

  function addDirectionalOverrideCharacters(text, direction_is_ltr) {
    // source: www.w3.org/International/questions/qa-bidi-unicode-controls.en
    const LTR_OVERRIDE = '\u202D';
    const RTL_OVERRIDE = '\u202E';
    const OVERRIDE_END = '\u202C';
    if (direction_is_ltr) {
      return LTR_OVERRIDE + text + OVERRIDE_END;
    }
    return RTL_OVERRIDE + text + OVERRIDE_END;
  }

  ctx.font = '50px sans-serif';
  ctx.direction = 'rtl';
  ctx.textAlign = 'left';
  ctx.letterSpacing = '10px';

  const kTexts = [
    'UNAVAILABLE',
    '🏁🎢🏁',
    'οΌ‰οΌˆγ‚οΌ‰οΌˆ',
    '-abcd_'
  ]

  for (text of kTexts) {
    text = addDirectionalOverrideCharacters(text, ctx.direction == 'ltr');
    const tm = ctx.measureText(text);
    // First character.
    checkRectListsMatch(
      tm.getSelectionRects(0, 1),
      placeAndSelectTextInDOM(text, 0, 1)
    );
    // Last character.
    checkRectListsMatch(
      tm.getSelectionRects(text.length - 1, text.length),
      placeAndSelectTextInDOM(text, text.length - 1, text.length)
    );
    // Whole string.
    checkRectListsMatch(
      tm.getSelectionRects(0, text.length),
      placeAndSelectTextInDOM(text, 0, text.length)
    );
    // Intermediate string.
    checkRectListsMatch(
      tm.getSelectionRects(1, text.length - 1),
      placeAndSelectTextInDOM(text, 1, text.length - 1)
    );
    // Invalid start > end string. Creates 0 width rectangle.
    checkRectListsMatch(
      tm.getSelectionRects(3, 2),
      placeAndSelectTextInDOM(text, 3, 2)
    );
    checkRectListsMatch(
      tm.getSelectionRects(1, 0),
      placeAndSelectTextInDOM(text, 1, 0)
    );
  }
}, "Check that TextMetrics::getSelectionRects() matches its DOM equivalent, with direction rtl, text align left, 10px letter spacing, and directional-override.");

test(t => {
  const canvas = new OffscreenCanvas(100, 50);
  const ctx = canvas.getContext('2d');

  function placeAndSelectTextInDOM(text, from, to) {
    const el = document.createElement("div");
    el.innerHTML = text;
    el.style.font = '50px sans-serif';
    el.style.direction = 'ltr';
    el.style.textAlign = 'center';
    el.style.letterSpacing = '10px';
    document.body.appendChild(el);

    let range = document.createRange();

    range.setStart(el.childNodes[0], 0);
    range.setEnd(el.childNodes[0], text.length);
    const parent = range.getClientRects()[0];
    let width = 0;
    for (const rect of range.getClientRects()) {
      width += rect.width;
    }

    range.setStart(el.childNodes[0], from);
    range.setEnd(el.childNodes[0], to);

    let sel = window.getSelection();
    sel.removeAllRanges();
    sel.addRange(range);

    let sel_rects = sel.getRangeAt(0).getClientRects();

    document.body.removeChild(el);

    // Offset to the alignment point determined by textAlign.
    let text_align_dx;
    switch (el.style.textAlign) {
      case 'right':
        text_align_dx = width;
        break;
      case 'center':
        text_align_dx = width / 2;
        break;
      default:
        text_align_dx = 0;
    }

    for(let i = 0 ; i < sel_rects.length ; ++i) {
      sel_rects[i].x -= parent.x + text_align_dx;
      sel_rects[i].y -= parent.y;
    }

    return sel_rects;
  }

  function checkRectListsMatch(list_a, list_b) {
    _assertSame(list_a.length, list_b.length, "list_a.length", "list_b.length");
    for(let i = 0 ; i < list_a.length ; ++i) {
      assert_approx_equals(list_a[i].x, list_b[i].x, 1.0);
      assert_approx_equals(list_a[i].width, list_b[i].width, 1.0);
      assert_approx_equals(list_a[i].height, list_b[i].height, 1.0);
      // Y-position not tested here as getting the baseline for text in the
      // DOM is not straightforward.
    }
  }

  function addDirectionalOverrideCharacters(text, direction_is_ltr) {
    // source: www.w3.org/International/questions/qa-bidi-unicode-controls.en
    const LTR_OVERRIDE = '\u202D';
    const RTL_OVERRIDE = '\u202E';
    const OVERRIDE_END = '\u202C';
    if (direction_is_ltr) {
      return LTR_OVERRIDE + text + OVERRIDE_END;
    }
    return RTL_OVERRIDE + text + OVERRIDE_END;
  }

  ctx.font = '50px sans-serif';
  ctx.direction = 'ltr';
  ctx.textAlign = 'center';
  ctx.letterSpacing = '10px';

  const kTexts = [
    'UNAVAILABLE',
    '🏁🎢🏁',
    'οΌ‰οΌˆγ‚οΌ‰οΌˆ',
    '-abcd_'
  ]

  for (text of kTexts) {
    text = addDirectionalOverrideCharacters(text, ctx.direction == 'ltr');
    const tm = ctx.measureText(text);
    // First character.
    checkRectListsMatch(
      tm.getSelectionRects(0, 1),
      placeAndSelectTextInDOM(text, 0, 1)
    );
    // Last character.
    checkRectListsMatch(
      tm.getSelectionRects(text.length - 1, text.length),
      placeAndSelectTextInDOM(text, text.length - 1, text.length)
    );
    // Whole string.
    checkRectListsMatch(
      tm.getSelectionRects(0, text.length),
      placeAndSelectTextInDOM(text, 0, text.length)
    );
    // Intermediate string.
    checkRectListsMatch(
      tm.getSelectionRects(1, text.length - 1),
      placeAndSelectTextInDOM(text, 1, text.length - 1)
    );
    // Invalid start > end string. Creates 0 width rectangle.
    checkRectListsMatch(
      tm.getSelectionRects(3, 2),
      placeAndSelectTextInDOM(text, 3, 2)
    );
    checkRectListsMatch(
      tm.getSelectionRects(1, 0),
      placeAndSelectTextInDOM(text, 1, 0)
    );
  }
}, "Check that TextMetrics::getSelectionRects() matches its DOM equivalent, with direction ltr, text align center, 10px letter spacing, and directional-override.");

test(t => {
  const canvas = new OffscreenCanvas(100, 50);
  const ctx = canvas.getContext('2d');

  function placeAndSelectTextInDOM(text, from, to) {
    const el = document.createElement("div");
    el.innerHTML = text;
    el.style.font = '50px sans-serif';
    el.style.direction = 'rtl';
    el.style.textAlign = 'center';
    el.style.letterSpacing = '10px';
    document.body.appendChild(el);

    let range = document.createRange();

    range.setStart(el.childNodes[0], 0);
    range.setEnd(el.childNodes[0], text.length);
    const parent = range.getClientRects()[0];
    let width = 0;
    for (const rect of range.getClientRects()) {
      width += rect.width;
    }

    range.setStart(el.childNodes[0], from);
    range.setEnd(el.childNodes[0], to);

    let sel = window.getSelection();
    sel.removeAllRanges();
    sel.addRange(range);

    let sel_rects = sel.getRangeAt(0).getClientRects();

    document.body.removeChild(el);

    // Offset to the alignment point determined by textAlign.
    let text_align_dx;
    switch (el.style.textAlign) {
      case 'right':
        text_align_dx = width;
        break;
      case 'center':
        text_align_dx = width / 2;
        break;
      default:
        text_align_dx = 0;
    }

    for(let i = 0 ; i < sel_rects.length ; ++i) {
      sel_rects[i].x -= parent.x + text_align_dx;
      sel_rects[i].y -= parent.y;
    }

    return sel_rects;
  }

  function checkRectListsMatch(list_a, list_b) {
    _assertSame(list_a.length, list_b.length, "list_a.length", "list_b.length");
    for(let i = 0 ; i < list_a.length ; ++i) {
      assert_approx_equals(list_a[i].x, list_b[i].x, 1.0);
      assert_approx_equals(list_a[i].width, list_b[i].width, 1.0);
      assert_approx_equals(list_a[i].height, list_b[i].height, 1.0);
      // Y-position not tested here as getting the baseline for text in the
      // DOM is not straightforward.
    }
  }

  function addDirectionalOverrideCharacters(text, direction_is_ltr) {
    // source: www.w3.org/International/questions/qa-bidi-unicode-controls.en
    const LTR_OVERRIDE = '\u202D';
    const RTL_OVERRIDE = '\u202E';
    const OVERRIDE_END = '\u202C';
    if (direction_is_ltr) {
      return LTR_OVERRIDE + text + OVERRIDE_END;
    }
    return RTL_OVERRIDE + text + OVERRIDE_END;
  }

  ctx.font = '50px sans-serif';
  ctx.direction = 'rtl';
  ctx.textAlign = 'center';
  ctx.letterSpacing = '10px';

  const kTexts = [
    'UNAVAILABLE',
    '🏁🎢🏁',
    'οΌ‰οΌˆγ‚οΌ‰οΌˆ',
    '-abcd_'
  ]

  for (text of kTexts) {
    text = addDirectionalOverrideCharacters(text, ctx.direction == 'ltr');
    const tm = ctx.measureText(text);
    // First character.
    checkRectListsMatch(
      tm.getSelectionRects(0, 1),
      placeAndSelectTextInDOM(text, 0, 1)
    );
    // Last character.
    checkRectListsMatch(
      tm.getSelectionRects(text.length - 1, text.length),
      placeAndSelectTextInDOM(text, text.length - 1, text.length)
    );
    // Whole string.
    checkRectListsMatch(
      tm.getSelectionRects(0, text.length),
      placeAndSelectTextInDOM(text, 0, text.length)
    );
    // Intermediate string.
    checkRectListsMatch(
      tm.getSelectionRects(1, text.length - 1),
      placeAndSelectTextInDOM(text, 1, text.length - 1)
    );
    // Invalid start > end string. Creates 0 width rectangle.
    checkRectListsMatch(
      tm.getSelectionRects(3, 2),
      placeAndSelectTextInDOM(text, 3, 2)
    );
    checkRectListsMatch(
      tm.getSelectionRects(1, 0),
      placeAndSelectTextInDOM(text, 1, 0)
    );
  }
}, "Check that TextMetrics::getSelectionRects() matches its DOM equivalent, with direction rtl, text align center, 10px letter spacing, and directional-override.");

test(t => {
  const canvas = new OffscreenCanvas(100, 50);
  const ctx = canvas.getContext('2d');

  function placeAndSelectTextInDOM(text, from, to) {
    const el = document.createElement("div");
    el.innerHTML = text;
    el.style.font = '50px sans-serif';
    el.style.direction = 'ltr';
    el.style.textAlign = 'right';
    el.style.letterSpacing = '10px';
    document.body.appendChild(el);

    let range = document.createRange();

    range.setStart(el.childNodes[0], 0);
    range.setEnd(el.childNodes[0], text.length);
    const parent = range.getClientRects()[0];
    let width = 0;
    for (const rect of range.getClientRects()) {
      width += rect.width;
    }

    range.setStart(el.childNodes[0], from);
    range.setEnd(el.childNodes[0], to);

    let sel = window.getSelection();
    sel.removeAllRanges();
    sel.addRange(range);

    let sel_rects = sel.getRangeAt(0).getClientRects();

    document.body.removeChild(el);

    // Offset to the alignment point determined by textAlign.
    let text_align_dx;
    switch (el.style.textAlign) {
      case 'right':
        text_align_dx = width;
        break;
      case 'center':
        text_align_dx = width / 2;
        break;
      default:
        text_align_dx = 0;
    }

    for(let i = 0 ; i < sel_rects.length ; ++i) {
      sel_rects[i].x -= parent.x + text_align_dx;
      sel_rects[i].y -= parent.y;
    }

    return sel_rects;
  }

  function checkRectListsMatch(list_a, list_b) {
    _assertSame(list_a.length, list_b.length, "list_a.length", "list_b.length");
    for(let i = 0 ; i < list_a.length ; ++i) {
      assert_approx_equals(list_a[i].x, list_b[i].x, 1.0);
      assert_approx_equals(list_a[i].width, list_b[i].width, 1.0);
      assert_approx_equals(list_a[i].height, list_b[i].height, 1.0);
      // Y-position not tested here as getting the baseline for text in the
      // DOM is not straightforward.
    }
  }

  function addDirectionalOverrideCharacters(text, direction_is_ltr) {
    // source: www.w3.org/International/questions/qa-bidi-unicode-controls.en
    const LTR_OVERRIDE = '\u202D';
    const RTL_OVERRIDE = '\u202E';
    const OVERRIDE_END = '\u202C';
    if (direction_is_ltr) {
      return LTR_OVERRIDE + text + OVERRIDE_END;
    }
    return RTL_OVERRIDE + text + OVERRIDE_END;
  }

  ctx.font = '50px sans-serif';
  ctx.direction = 'ltr';
  ctx.textAlign = 'right';
  ctx.letterSpacing = '10px';

  const kTexts = [
    'UNAVAILABLE',
    '🏁🎢🏁',
    'οΌ‰οΌˆγ‚οΌ‰οΌˆ',
    '-abcd_'
  ]

  for (text of kTexts) {
    text = addDirectionalOverrideCharacters(text, ctx.direction == 'ltr');
    const tm = ctx.measureText(text);
    // First character.
    checkRectListsMatch(
      tm.getSelectionRects(0, 1),
      placeAndSelectTextInDOM(text, 0, 1)
    );
    // Last character.
    checkRectListsMatch(
      tm.getSelectionRects(text.length - 1, text.length),
      placeAndSelectTextInDOM(text, text.length - 1, text.length)
    );
    // Whole string.
    checkRectListsMatch(
      tm.getSelectionRects(0, text.length),
      placeAndSelectTextInDOM(text, 0, text.length)
    );
    // Intermediate string.
    checkRectListsMatch(
      tm.getSelectionRects(1, text.length - 1),
      placeAndSelectTextInDOM(text, 1, text.length - 1)
    );
    // Invalid start > end string. Creates 0 width rectangle.
    checkRectListsMatch(
      tm.getSelectionRects(3, 2),
      placeAndSelectTextInDOM(text, 3, 2)
    );
    checkRectListsMatch(
      tm.getSelectionRects(1, 0),
      placeAndSelectTextInDOM(text, 1, 0)
    );
  }
}, "Check that TextMetrics::getSelectionRects() matches its DOM equivalent, with direction ltr, text align right, 10px letter spacing, and directional-override.");

test(t => {
  const canvas = new OffscreenCanvas(100, 50);
  const ctx = canvas.getContext('2d');

  function placeAndSelectTextInDOM(text, from, to) {
    const el = document.createElement("div");
    el.innerHTML = text;
    el.style.font = '50px sans-serif';
    el.style.direction = 'rtl';
    el.style.textAlign = 'right';
    el.style.letterSpacing = '10px';
    document.body.appendChild(el);

    let range = document.createRange();

    range.setStart(el.childNodes[0], 0);
    range.setEnd(el.childNodes[0], text.length);
    const parent = range.getClientRects()[0];
    let width = 0;
    for (const rect of range.getClientRects()) {
      width += rect.width;
    }

    range.setStart(el.childNodes[0], from);
    range.setEnd(el.childNodes[0], to);

    let sel = window.getSelection();
    sel.removeAllRanges();
    sel.addRange(range);

    let sel_rects = sel.getRangeAt(0).getClientRects();

    document.body.removeChild(el);

    // Offset to the alignment point determined by textAlign.
    let text_align_dx;
    switch (el.style.textAlign) {
      case 'right':
        text_align_dx = width;
        break;
      case 'center':
        text_align_dx = width / 2;
        break;
      default:
        text_align_dx = 0;
    }

    for(let i = 0 ; i < sel_rects.length ; ++i) {
      sel_rects[i].x -= parent.x + text_align_dx;
      sel_rects[i].y -= parent.y;
    }

    return sel_rects;
  }

  function checkRectListsMatch(list_a, list_b) {
    _assertSame(list_a.length, list_b.length, "list_a.length", "list_b.length");
    for(let i = 0 ; i < list_a.length ; ++i) {
      assert_approx_equals(list_a[i].x, list_b[i].x, 1.0);
      assert_approx_equals(list_a[i].width, list_b[i].width, 1.0);
      assert_approx_equals(list_a[i].height, list_b[i].height, 1.0);
      // Y-position not tested here as getting the baseline for text in the
      // DOM is not straightforward.
    }
  }

  function addDirectionalOverrideCharacters(text, direction_is_ltr) {
    // source: www.w3.org/International/questions/qa-bidi-unicode-controls.en
    const LTR_OVERRIDE = '\u202D';
    const RTL_OVERRIDE = '\u202E';
    const OVERRIDE_END = '\u202C';
    if (direction_is_ltr) {
      return LTR_OVERRIDE + text + OVERRIDE_END;
    }
    return RTL_OVERRIDE + text + OVERRIDE_END;
  }

  ctx.font = '50px sans-serif';
  ctx.direction = 'rtl';
  ctx.textAlign = 'right';
  ctx.letterSpacing = '10px';

  const kTexts = [
    'UNAVAILABLE',
    '🏁🎢🏁',
    'οΌ‰οΌˆγ‚οΌ‰οΌˆ',
    '-abcd_'
  ]

  for (text of kTexts) {
    text = addDirectionalOverrideCharacters(text, ctx.direction == 'ltr');
    const tm = ctx.measureText(text);
    // First character.
    checkRectListsMatch(
      tm.getSelectionRects(0, 1),
      placeAndSelectTextInDOM(text, 0, 1)
    );
    // Last character.
    checkRectListsMatch(
      tm.getSelectionRects(text.length - 1, text.length),
      placeAndSelectTextInDOM(text, text.length - 1, text.length)
    );
    // Whole string.
    checkRectListsMatch(
      tm.getSelectionRects(0, text.length),
      placeAndSelectTextInDOM(text, 0, text.length)
    );
    // Intermediate string.
    checkRectListsMatch(
      tm.getSelectionRects(1, text.length - 1),
      placeAndSelectTextInDOM(text, 1, text.length - 1)
    );
    // Invalid start > end string. Creates 0 width rectangle.
    checkRectListsMatch(
      tm.getSelectionRects(3, 2),
      placeAndSelectTextInDOM(text, 3, 2)
    );
    checkRectListsMatch(
      tm.getSelectionRects(1, 0),
      placeAndSelectTextInDOM(text, 1, 0)
    );
  }
}, "Check that TextMetrics::getSelectionRects() matches its DOM equivalent, with direction rtl, text align right, 10px letter spacing, and directional-override.");

</script>