<!DOCTYPE html>
<meta charset=utf-8>
<meta name="timeout" content="long">
<title>text field selection: select()</title>
<link rel="author" title="Domenic Denicola" href="mailto:d@domenic.me">
<link rel=help href="https://html.spec.whatwg.org/multipage/#textFieldSelection">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<div id="log"></div>

<textarea>foobar</textarea>
<input type="text" value="foobar">
<input type="search" value="foobar">
<input type="tel" value="1234">
<input type="url" value="https://example.com/">
<input type="password" value="hunter2">

<script>
"use strict";

const els = [document.querySelector("textarea"), ...document.querySelectorAll("input")];

const actions = [
  {
    label: "select()",
    action: el => el.select()
  },
  {
    label: "selectionStart",
    action: el => el.selectionStart = 1
  },
  {
    label: "selectionEnd",
    action: el => el.selectionEnd = el.value.length - 1
  },
  {
    label: "selectionDirection",
    action: el => el.selectionDirection = "backward"
  },
  {
    label: "setSelectionRange()",
    action: el => el.setSelectionRange(1, el.value.length - 1) // changes direction implicitly to none/forward
  },
  {
    label: "setRangeText()",
    action: el => el.setRangeText("newmiddle", el.selectionStart, el.selectionEnd, "select")
  },
  {
    label: "selectionStart out of range",
    action: el => el.selectionStart = 1000
  },
  {
    label: "selectionEnd out of range",
    action: el => el.selectionEnd = 1000
  },
  {
    label: "setSelectionRange out of range",
    action: el => el.setSelectionRange(1000, 2000)
  }
];

function waitForRender() {
  return new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)));
}

function initialize(el) {
  el.setRangeText("foobar", 0, el.value.length, "start");
  // Make sure to flush async dispatches
  return waitForRender();
}

els.forEach((el) => {
  const elLabel = el.localName === "textarea" ? "textarea" : "input type " + el.type;

  actions.forEach((action) => {
    // promise_test instead of async_test is important because these need to happen in sequence (to test that events
    // fire if and only if the selection changes).
    promise_test(async t => {
      await initialize(el);

      const watcher = new EventWatcher(t, el, "select");

      const promise = watcher.wait_for("select").then(e => {
        assert_true(e.isTrusted, "isTrusted must be true");
        assert_true(e.bubbles, "bubbles must be true");
        assert_false(e.cancelable, "cancelable must be false");
      });

      action.action(el);

      return promise;
    }, `${elLabel}: ${action.label}`);

    promise_test(async t => {
      el.onselect = t.unreached_func("the select event must not fire the second time");

      action.action(el);

      await waitForRender();
      el.onselect = null;
    }, `${elLabel}: ${action.label} a second time (must not fire select)`);

    promise_test(async t => {
      const element = el.cloneNode(true);
      let fired = false;
      element.addEventListener('select', () => fired = true, { once: true });

      action.action(element);

      await waitForRender();
      assert_true(fired, "event didn't fire");

    }, `${elLabel}: ${action.label} disconnected node`);

    // Intentionally still using promise_test, as assert_unreachable does not
    // make the test fail inside a listener while t.unreached_func() does.
    promise_test(async t => {
      const element = el.cloneNode(true);
      let fired = false;
      element.addEventListener('select', () => fired = true, { once: true });

      action.action(element);

      assert_false(fired, "the select event must not fire synchronously");
      await waitForRender();
      assert_true(fired, "event didn't fire");
    }, `${elLabel}: ${action.label} event queue`);

    promise_test(async t => {
      const element = el.cloneNode(true);
      let selectCount = 0;
      element.addEventListener('select', () => ++selectCount);
      assert_equals(element.selectionEnd, 0);

      action.action(element);
      action.action(element);

      await waitForRender();
      assert_equals(selectCount, 1, "the select event must not fire twice");
    }, `${elLabel}: ${action.label} twice in disconnected node (must fire select only once)`);
  });
});
</script>
