Source code
Revision control
Copy as Markdown
Other Tools
Test Info:
- This WPT test may be referenced by the following Test IDs:
- /html/semantics/interactive-elements/the-details-element/name-attribute.html - WPT Dashboard Interop Dashboard
<!DOCTYPE HTML>
<meta charset=UTF-8>
<title>Test for the name attribute creating exclusive accordions from details elements</title>
<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);
};
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");
promise_test(async t => {
container.remove();
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 when the element isn't connected");
</script>