<!DOCTYPE HTML>
<meta charset=UTF-8>
<title>Test for the name attribute creating exclusive accordions from details elements</title>
<link rel="author" title="L. David Baron" href="https://dbaron.org/">
<link rel="author" title="Google" href="http://www.google.com/">
<link rel="help" href="https://html.spec.whatwg.org/multipage/#the-details-element">
<link rel="help" href="https://open-ui.org/components/accordion.explainer">
<link rel="help" href="https://github.com/openui/open-ui/issues/725">
<link rel="help" href="https://bugs.chromium.org/p/chromium/issues/detail?id=1444057">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>

<div id="container">
</div>

<script>

function assert_element_states(elements, expectations, description) {
  assert_array_equals(elements.map(e => Number(e.open)), expectations, description);
}

let container = document.getElementById("container");

promise_test(async t => {
  container.innerHTML = `
    <details name="a">
      <summary>1</summary>
      This is the first item.
    </details>

    <details name="a">
      <summary>2</summary>
      This is the second item.
    </details>
  `;
  let first = container.firstElementChild;
  let second = first.nextElementSibling;
  assert_false(first.open);
  assert_false(second.open);
  first.open = true;
  assert_true(first.open);
  assert_false(second.open);
  second.open = true;
  assert_false(first.open);
  assert_true(second.open);
  second.open = true;
  assert_false(first.open);
  assert_true(second.open);
  second.open = false;
  assert_false(first.open);
  assert_false(second.open);
}, "basic handling of mutually exclusive details");

promise_test(async t => {
  container.innerHTML = `
    <details name="a" open>
      <summary>1</summary>
      This is the first item.
    </details>

    <details name="a">
      <summary>2</summary>
      This is the second item.
    </details>

    <details name="a" open>
      <summary>3</summary>
      This is the third item.
    </details>
  `;
  let first = container.firstElementChild;
  let second = first.nextElementSibling;
  let third = second.nextElementSibling;
  function assert_states(expected_first, expected_second, expected_third, description) {
    assert_array_equals([first.open, second.open, third.open], [expected_first, expected_second, expected_third], description);
  }

  assert_states(true, false, true, "initial states from open attribute");
  first.open = true;
  assert_states(true, false, true, "non-mutation doesn't change state");
  second.open = true;
  assert_states(false, true, false, "mutation closes multiple open elements");
  third.setAttribute("open", "");
  assert_states(false, false, true, "setAttribute closes other open element");
}, "more complex handling of mutually exclusive details");

promise_test(async t => {
  let details_elements_string = `
    <details name="a"></details>
    <details name="a" open></details>
    <details name="b"></details>
    <details name="b"></details>
  `;
  container.innerHTML = `
    ${details_elements_string}
    <div id="shadow_host"></div>
  `;
  let shadow_root = document.getElementById("shadow_host").attachShadow({ mode: "open" });
  shadow_root.innerHTML = details_elements_string;
  let elements = Array.from(container.querySelectorAll("details")).concat(Array.from(shadow_root.querySelectorAll("details")));

  assert_element_states(elements, [0, 1, 0, 0, 0, 1, 0, 0], "initial states from open attribute");
  elements[4].open = true;
  assert_element_states(elements, [0, 1, 0, 0, 1, 0, 0, 0], "after mutation in shadow tree");
  for (let i = 0; i < 8; ++i) {
    elements[i].open = true;
  }
  assert_element_states(elements, [0, 1, 0, 1, 0, 1, 0, 1], "after setting all elements open");
  elements[0].open = true;
  assert_element_states(elements, [1, 0, 0, 1, 0, 1, 0, 1], "after final mutation");
}, "mutually exclusive details across multiple names and multiple tree scopes");

promise_test(async t => {
  container.innerHTML = `
    <details name="a" id="e0" open></details>
    <details name="a" id="e1"></details>
    <details name="a" id="e3" open></details>
  `;
  let e2 = document.createElement("details");
  e2.id = "e2";
  e2.name = "a";
  e2.open = true;
  let elements = [ document.getElementById("e0"),
                   document.getElementById("e1"),
                   e2,
                   document.getElementById("e3") ];
  container.insertBefore(e2, elements[3]);

  let mutation_event_received_ids = [];
  let mutation_listener = event => {
    assert_equals(event.type, "DOMSubtreeModified");
    assert_equals(event.target.nodeType, Node.ELEMENT_NODE);
    let element = event.target;
    assert_equals(element.localName, "details");
    mutation_event_received_ids.push(element.id);
  };
  let toggle_event_received_ids = [];
  let toggle_event_promises = [];
  for (let element of elements) {
    element.addEventListener("DOMSubtreeModified", mutation_listener);
    toggle_event_promises.push(new Promise((resolve, reject) => {
      element.addEventListener("toggle", event => {
        assert_equals(event.type, "toggle");
        assert_equals(event.target, element);
        toggle_event_received_ids.push(element.id);
        resolve(undefined);
      });
    }));
  }
  assert_array_equals(mutation_event_received_ids, []);
  assert_element_states(elements, [1, 0, 1, 1], "states before mutation");
  elements[1].open = true;
  if (mutation_event_received_ids.length == 0) {
    // ok if mutation events are not supported
  } else {
    assert_array_equals(mutation_event_received_ids, ["e0", "e2", "e3", "e1"],
                        "removal events received in tree order, followed by addition event");
  }
  assert_element_states(elements, [0, 1, 0, 0], "states after mutation");
  assert_array_equals(toggle_event_received_ids, [], "toggle events received before awaiting promises");
  await Promise.all(toggle_event_promises);
  assert_array_equals(toggle_event_received_ids, ["e1", "e0", "e2", "e3"], "toggle events received after awaiting promises");
}, "mutation event and toggle event order matches tree order");

// This function is used to guard tests that test behavior that is
// relevant only because of Mutation Events.  If mutation events (for
// attribute addition/removal) are removed from the web, the tests using
// this function can be removed.
function mutation_events_for_attribute_removal_supported() {
  if (!("MutationEvent" in window)) {
    return false;
  }
  container.innerHTML = `<div id="event-removal-test"></div>`;
  let element = container.firstChild;
  let event_fired = false;
  element.addEventListener("DOMSubtreeModified", event => event_fired = true);
  element.removeAttribute("id");
  return event_fired;
}

promise_test(async t => {
  if (!mutation_events_for_attribute_removal_supported()) {
    return;
  }
  container.innerHTML = `
    <details name="a" id="e0" open></details>
    <details name="a" id="e1"></details>
    <details name="a" id="e2" open></details>
  `;
  let elements = [ document.getElementById("e0"),
                   document.getElementById("e1"),
                   document.getElementById("e2") ];

  let received_ids = [];
  let listener = event => {
    received_ids.push(event.target.id);
    let i = 0;
    for (let element of elements) {
      element.setAttribute("name", `b${i++}`);
    }
  };
  for (let element of elements) {
    element.addEventListener("DOMSubtreeModified", listener);
  }
  assert_array_equals(received_ids, []);
  assert_element_states(elements, [1, 0, 1], "states before mutation");
  elements[1].open = true;
  assert_array_equals(received_ids, ["e0", "e2", "e1"],
                      "removal events received in tree order, followed by addition event, despite changes to name during mutation event");
  assert_element_states(elements, [0, 1, 0], "states after mutation");
}, "interaction of open attribute changes with mutation events");

promise_test(async t => {
  container.innerHTML = `
    <details></details>
    <details></details>
    <details name></details>
    <details name></details>
    <details name=""></details>
    <details name=""></details>
  `;
  let elements = Array.from(container.querySelectorAll("details"));

  assert_element_states(elements, [0, 0, 0, 0, 0, 0], "initial states from open attribute");
  for (let i = 0; i < 6; ++i) {
    elements[i].open = true;
  }
  assert_element_states(elements, [1, 1, 1, 1, 1, 1], "after setting all elements open");
}, "empty and missing name attributes do not create groups");

</script>
