summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/html/semantics/interactive-elements/the-details-element/name-attribute.html
diff options
context:
space:
mode:
Diffstat (limited to 'testing/web-platform/tests/html/semantics/interactive-elements/the-details-element/name-attribute.html')
-rw-r--r--testing/web-platform/tests/html/semantics/interactive-elements/the-details-element/name-attribute.html469
1 files changed, 469 insertions, 0 deletions
diff --git a/testing/web-platform/tests/html/semantics/interactive-elements/the-details-element/name-attribute.html b/testing/web-platform/tests/html/semantics/interactive-elements/the-details-element/name-attribute.html
new file mode 100644
index 0000000000..2685546e9b
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/interactive-elements/the-details-element/name-attribute.html
@@ -0,0 +1,469 @@
+<!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, false, "initial states from open attribute");
+ first.open = true;
+ assert_states(true, false, false, "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, 0, 0], "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, ["e1"],
+ "mutation events received only for open attribute mutation and not for closing other element");
+ }
+ 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, ["e3", "e2", "e1", "e0"], "toggle events received after awaiting promises, including toggle events from parser insertion");
+}, "mutation event and toggle event 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, 0], "states before mutation");
+ elements[1].open = true;
+ assert_array_equals(received_ids, ["e1"],
+ "mutation events received only for open attribute mutation and not for closing other element");
+ 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");
+
+const connected_scenarios = {
+ "connected": {
+ "create": data => container,
+ "cleanup": data => {},
+ },
+ "disconnected": {
+ "create": data => document.createElement("div"),
+ "cleanup": data => {},
+ },
+ "shadow": {
+ "create": data => {
+ let e = document.createElement("div");
+ container.appendChild(e);
+ data.wrapper = e;
+ let shadowRoot = e.attachShadow({ mode: "open" });
+ let d = document.createElement("div");
+ shadowRoot.appendChild(d);
+ return d;
+ },
+ "cleanup": data => { data.wrapper.remove(); },
+ },
+ "shadow-in-disconnected": {
+ "create": data => {
+ let e = document.createElement("div");
+ let shadowRoot = e.attachShadow({ mode: "open" });
+ let d = document.createElement("div");
+ shadowRoot.appendChild(d);
+ return d;
+ },
+ "cleanup": data => {},
+ },
+ "template-in-disconnected": {
+ "create": data => {
+ let e = document.createElement("div");
+ e.innerHTML = `
+ <template>
+ <div></div>
+ </template>
+ `;
+ return e.firstElementChild.content.firstElementChild;
+ },
+ "cleanup": data => {},
+ },
+ "connected-in-xhr-response": {
+ "create": data => new Promise((resolve, reject) => {
+ let xhr = new XMLHttpRequest();
+ xhr.open("GET", "support/empty-html-document.html");
+ xhr.responseType = "document";
+ xhr.send();
+ xhr.addEventListener("load", event => { resolve(xhr.response.body); });
+ let reject_with_type =
+ event => { reject(`${event.type} event received`); }
+ xhr.addEventListener("error", reject_with_type);
+ xhr.addEventListener("abort", reject_with_type);
+ }),
+ "cleanup": data => {},
+ },
+ "connected-in-implementation-create-document": {
+ "create": data => {
+ let doc = document.implementation.createHTMLDocument("impl-created");
+ return doc.body;
+ },
+ "cleanup": data => {},
+ },
+ "connected-in-template": {
+ "create": data => {
+ container.innerHTML = `
+ <template>
+ <div></div>
+ </template>
+ `;
+ return container.firstElementChild.content.firstElementChild;
+ },
+ "cleanup": data => { container.innerHTML = ""; },
+ },
+};
+
+for (const [scenario, scenario_callbacks] of Object.entries(connected_scenarios)) {
+ promise_test(async t => {
+ let data = {};
+ let container = await scenario_callbacks.create(data);
+ t.add_cleanup(async () => await scenario_callbacks.cleanup(data));
+ assert_true(container instanceof HTMLDivElement ||
+ container instanceof HTMLBodyElement,
+ "error in test setup");
+
+ container.innerHTML = `
+ <details name="scenariotest" open></details>
+ <details name="scenariotest"></details>
+ `;
+
+ let elements = Array.from(container.querySelectorAll("details[name='scenariotest']"));
+ assert_element_states(elements, [1, 0], "state before toggle");
+ elements[1].open = true;
+ assert_element_states(elements, [0, 1], "state after toggle enforces exclusivity");
+ }, `exclusivity enforcement with attachment scenario ${scenario}`);
+}
+
+promise_test(async t => {
+ container.innerHTML = `
+ <details name="a" id="e0" open></details>
+ <details name="a" id="e1"></details>
+ <details name="b" id="e2" open></details>
+ `;
+ let elements = [ document.getElementById("e0"),
+ document.getElementById("e1"),
+ document.getElementById("e2") ];
+
+ let mutation_received_ids = [];
+ let listener = event => {
+ mutation_received_ids.push(event.target.id);
+ };
+ for (let element of elements) {
+ element.addEventListener("DOMSubtreeModified", listener);
+ }
+
+ assert_element_states(elements, [1, 0, 1], "states before first mutation");
+ assert_array_equals(mutation_received_ids, [], "mutation events received before first mutation");
+ elements[2].name = "a";
+ assert_element_states(elements, [1, 0, 0], "states after first mutation");
+ if (mutation_received_ids.length != 0) {
+ // OK to not support mutation events, or to send DOMSubtreeModified
+ // only for attribute addition/removal (open) but not for attribute
+ // change (name)
+ assert_array_equals(mutation_received_ids, ["e2"], "mutation events received after first mutation");
+ }
+ elements[0].name = "c";
+ elements[2].open = true;
+ assert_element_states(elements, [1, 0, 1], "states before second mutation");
+ if (mutation_received_ids.length != 0) { // OK to not support mutation events
+ if (mutation_received_ids.length == 1) {
+ // OK to receive DOMSubtreeModified for attribute addition/removal
+ // (open) but not for attribute change (name)
+ assert_array_equals(mutation_received_ids, ["e2"], "mutation events received before second mutation");
+ } else {
+ assert_array_equals(mutation_received_ids, ["e2", "e0", "e2"], "mutation events received before second mutation");
+ }
+ }
+ elements[0].name = "a";
+ assert_element_states(elements, [0, 0, 1], "states after second mutation");
+ if (mutation_received_ids.length != 0) { // OK to not support mutation events
+ if (mutation_received_ids.length == 1) {
+ // OK to receive DOMSubtreeModified for attribute addition/removal
+ // (open) but not for attribute change (name)
+ assert_array_equals(mutation_received_ids, ["e2"], "mutation events received before second mutation");
+ } else {
+ assert_array_equals(mutation_received_ids, ["e2", "e0", "e2", "e0"], "mutation events received after second mutation");
+ }
+ }
+}, "handling of name attribute changes");
+
+promise_test(async t => {
+ container.innerHTML = `
+ <details name="a" id="e0" open></details>
+ <details name="a" id="e1" open></details>
+ <details open name="a" id="e2"></details>
+ `;
+ let elements = [ document.getElementById("e0"),
+ document.getElementById("e1"),
+ document.getElementById("e2") ];
+
+ assert_element_states(elements, [1, 0, 0], "states after insertion by parser");
+}, "closing as a result of parsing doesn't depend on attribute order");
+
+promise_test(async t => {
+ container.innerHTML = `
+ <details name="a" id="e0" open></details>
+ <details name="a" id="e1"></details>
+ `;
+ let elements = [ document.getElementById("e0"),
+ document.getElementById("e1") ];
+
+ assert_element_states(elements, [1, 0], "states before first mutation");
+
+ let make_details = () => {
+ let e = document.createElement("details");
+ e.setAttribute("name", "a");
+ return e;
+ };
+
+ let watch_e0 = new EventWatcher(t, elements[0], ['toggle']);
+ let watch_e1 = new EventWatcher(t, elements[1], ['toggle']);
+
+ let expect_opening = async (watcher) => {
+ await watcher.wait_for(['toggle'], {record: 'all'}).then((events) => {
+ assert_equals(events[0].oldState, "closed");
+ assert_equals(events[0].newState, "open");
+ });
+ };
+
+ let expect_closing = async (watcher) => {
+ await watcher.wait_for(['toggle'], {record: 'all'}).then((events) => {
+ assert_equals(events[0].oldState, "open");
+ assert_equals(events[0].newState, "closed");
+ });
+ };
+
+ let track_mutations = (element) => {
+ let result = { count: 0 };
+ let listener = event => {
+ ++result.count;
+ };
+ element.addEventListener("DOMSubtreeModified", listener);
+ return result;
+ }
+
+ await expect_opening(watch_e0);
+
+ // Test appending an open element in the group.
+ let new1 = make_details();
+ let mutations1 = track_mutations(new1);
+ let watch_new1 = new EventWatcher(t, new1, ['toggle']);
+ new1.open = true;
+ assert_in_array(mutations1.count, [0, 1], "mutation events count before inserting new1");
+ await expect_opening(watch_new1);
+ container.appendChild(new1);
+ await expect_closing(watch_new1);
+ assert_in_array(mutations1.count, [0, 1], "mutation events count after inserting new1");
+
+ // Test appending a closed element in the group.
+ let new2 = make_details();
+ let mutations2 = track_mutations(new2);
+ let watch_new2 = new EventWatcher(t, new2, ['toggle']);
+ container.appendChild(new2);
+ assert_equals(mutations2.count, 0, "mutation events count after inserting new2");
+
+ // Test inserting an open element at the start of the group.
+ let new3 = make_details();
+ let mutations3 = track_mutations(new3);
+ new3.open = true; // this time do this before creating the EventWatcher
+ let watch_new3 = new EventWatcher(t, new3, ['toggle']);
+ assert_in_array(mutations3.count, [0, 1], "mutation events count before inserting new3");
+ await expect_opening(watch_new3);
+ container.insertBefore(new3, elements[0]);
+ await expect_closing(watch_new3);
+ assert_in_array(mutations3.count, [0, 1], "mutation events count after inserting new3");
+}, "handling of insertion of elements into group");
+
+</script>