diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
commit | 36d22d82aa202bb199967e9512281e9a53db42c9 (patch) | |
tree | 105e8c98ddea1c1e4784a60a5a6410fa416be2de /testing/web-platform/tests/html/dom/aria-element-reflection.html | |
parent | Initial commit. (diff) | |
download | firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.tar.xz firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.zip |
Adding upstream version 115.7.0esr.upstream/115.7.0esr
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'testing/web-platform/tests/html/dom/aria-element-reflection.html')
-rw-r--r-- | testing/web-platform/tests/html/dom/aria-element-reflection.html | 807 |
1 files changed, 807 insertions, 0 deletions
diff --git a/testing/web-platform/tests/html/dom/aria-element-reflection.html b/testing/web-platform/tests/html/dom/aria-element-reflection.html new file mode 100644 index 0000000000..8d4d4b8f6a --- /dev/null +++ b/testing/web-platform/tests/html/dom/aria-element-reflection.html @@ -0,0 +1,807 @@ +<!DOCTYPE HTML> +<html> + <head> + <meta charset="utf-8" /> + <title>Element Reflection for aria-activedescendant and aria-errormessage</title> + <link rel=help href="https://whatpr.org/html/3917/common-dom-interfaces.html#reflecting-content-attributes-in-idl-attributes:element"> + <link rel="author" title="Meredith Lane" href="meredithl@chromium.org"> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + </head> + + <div id="activedescendant" aria-activedescendant="x"></div> + + <div id="parentListbox" role="listbox" aria-activedescendant="i1"> + <div role="option" id="i1">Item 1</div> + <div role="option" id="i2">Item 2</div> + </div> + + <script> + test(function(t) { + assert_equals(activedescendant.ariaActiveDescendantElement, null, + "invalid ID for relationship returns null"); + + // Element reference should be set if the content attribute was included. + assert_equals(parentListbox.getAttribute("aria-activedescendant"), "i1", "check content attribute after parsing."); + assert_equals(parentListbox.ariaActiveDescendantElement, i1, "check idl attribute after parsing."); + assert_equals(parentListbox.ariaActiveDescendantElement, parentListbox.ariaActiveDescendantElement, "check idl attribute caching after parsing."); + + // If we set the content attribute, the element reference should reflect this. + parentListbox.setAttribute("aria-activedescendant", "i2"); + assert_equals(parentListbox.ariaActiveDescendantElement, i2, "setting the content attribute updates the element reference."); + assert_equals(parentListbox.ariaActiveDescendantElement, parentListbox.ariaActiveDescendantElement, "check idl attribute caching after update."); + + // Setting the element reference should set the empty string in the content attribute. + parentListbox.ariaActiveDescendantElement = i1; + assert_equals(parentListbox.ariaActiveDescendantElement, i1, "getter should return the right element reference."); + assert_equals(parentListbox.getAttribute("aria-activedescendant"), "", "content attribute should be empty."); + + // Both content and IDL attribute should be nullable. + parentListbox.ariaActiveDescendantElement = null; + assert_equals(parentListbox.ariaActiveDescendantElement, null); + assert_false(parentListbox.hasAttribute("aria-activedescendant")); + assert_equals(parentListbox.getAttribute("aria-activedescendant"), null, "nullifying the idl attribute removes the content attribute."); + + // Setting content attribute to non-existent or non compatible element should nullify the IDL attribute. + // Reset the element to an existant one. + parentListbox.setAttribute("aria-activedescendant", "i1"); + assert_equals(parentListbox.ariaActiveDescendantElement, i1, "reset attribute."); + + parentListbox.setAttribute("aria-activedescendant", "non-existent-element"); + assert_equals(parentListbox.getAttribute("aria-activedescendant"), "non-existent-element"); + assert_equals(parentListbox.ariaActiveDescendantElement, null,"non-DOM content attribute should null the element reference"); + }, "aria-activedescendant element reflection"); + </script> + + <div id="parentListbox2" role="listbox" aria-activedescendant="option1"> + <div role="option" id="option1">Item 1</div> + <div role="option" id="option2">Item 2</div> + </div> + + <script> + test(function(t) { + const option1 = document.getElementById("option1"); + const option2 = document.getElementById("option2"); + assert_equals(parentListbox2.ariaActiveDescendantElement, option1); + option1.removeAttribute("id"); + option2.setAttribute("id", "option1"); + const option2Duplicate = document.getElementById("option1"); + assert_equals(option2, option2Duplicate); + + assert_equals(parentListbox2.ariaActiveDescendantElement, option2); + }, "If the content attribute is set directly, the IDL attribute getter always returns the first element whose ID matches the content attribute."); + </script> + + <div id="blankIdParent" role="listbox"> + <div role="option" id="multiple-id"></div> + <div role="option" id="multiple-id"></div> + </div> + + <script> + test(function(t) { + // Get second child of parent. This violates the setting of a reflected element + // as it will not be the first child of the parent with that ID, which should + // result in an empty string for the content attribute. + blankIdParent.ariaActiveDescendantElement = blankIdParent.children[1]; + assert_true(blankIdParent.hasAttribute("aria-activedescendant")); + assert_equals(blankIdParent.getAttribute("aria-activedescendant"), ""); + assert_equals(blankIdParent.ariaActiveDescendantElement, blankIdParent.children[1]); + }, "Setting the IDL attribute to an element which is not the first element in DOM order with its ID causes the content attribute to be an empty string"); + </script> + + <div id="outerContainer"> + <p id="lightParagraph">Hello world!</p> + <span id="shadowHost"> + </span> + </div> + + <script> + test(function(t) { + const shadow = shadowHost.attachShadow({mode: "open"}); + const link = document.createElement("a"); + shadow.appendChild(link); + + assert_equals(lightParagraph.ariaActiveDescendantElement, null); + + // The given element crosses a shadow dom boundary, so it cannot be + // set as an element reference. + lightParagraph.ariaActiveDescendantElement = link; + assert_equals(lightParagraph.ariaActiveDescendantElement, null); + + // The given element crosses a shadow dom boundary (upwards), so + // can be used as an element reference, but the content attribute + // should reflect the empty string. + link.ariaActiveDescendantElement = lightParagraph; + assert_equals(link.ariaActiveDescendantElement, lightParagraph); + assert_equals(link.getAttribute("aria-activedescendant"), ""); + }, "Setting an element reference that crosses into a shadow tree is disallowed, but setting one that is in a shadow inclusive ancestor is allowed."); + </script> + + <input id="startTime" ></input> + <span id="errorMessage">Invalid Time</span> + + <script> + test(function(t) { + startTime.ariaErrorMessageElements = [errorMessage]; + assert_equals(startTime.getAttribute("aria-errormessage"), ""); + assert_array_equals(startTime.ariaErrorMessageElements, [errorMessage]); + + startTime.ariaErrorMessageElements = []; + assert_array_equals(startTime.ariaErrorMessageElements, []); + assert_equals(startTime.getAttribute("aria-errormessage"), ""); + + startTime.setAttribute("aria-errormessage", "errorMessage"); + assert_array_equals(startTime.ariaErrorMessageElements, [errorMessage]); + + }, "aria-errormessage"); + + test(function (t) { + assert_false('ariaErrorMessageElement' in startTime); + }, 'ariaErrorMessageElement is not defined') + + </script> + + <label> + Password: + <input id="passwordField" type="password" aria-details="pw"> + </label> + + <ul> + <li id="listItem1">First description.</li> + <li id="listItem2">Second description.</li> + </ul> + + <script> + + test(function(t) { + assert_array_equals(passwordField.ariaDetailsElements, []); + passwordField.ariaDetailsElements = [ listItem1 ]; + assert_equals(passwordField.getAttribute("aria-details"), ""); + assert_array_equals(passwordField.ariaDetailsElements, [ listItem1 ]); + + passwordField.ariaDetailsElements = [ listItem2 ]; + assert_equals(passwordField.getAttribute("aria-details"), ""); + assert_array_equals(passwordField.ariaDetailsElements, [ listItem2 ]); + }, "aria-details"); + </script> + + <div id="deletionParent" role="listbox" aria-activedescendant="contentAttrElement"> + <div role="option" id="contentAttrElement">Item 1</div> + <div role="option" id="idlAttrElement">Item 2</div> + </div> + + <script> + + test(function(t) { + const contentAttrElement = document.getElementById("contentAttrElement"); + const idlAttrElement = document.getElementById("idlAttrElement"); + + assert_equals(deletionParent.getAttribute("aria-activedescendant"), "contentAttrElement"); + assert_equals(deletionParent.ariaActiveDescendantElement, contentAttrElement); + + // Deleting an element set via the content attribute. + deletionParent.removeChild(contentAttrElement); + assert_equals(deletionParent.getAttribute("aria-activedescendant"), "contentAttrElement"); + + // As it was not explitly set, the attr-associated-element is computed from the content attribute, + // and since descendant1 has been removed from the DOM, it is not valid. + assert_equals(deletionParent.ariaActiveDescendantElement, null); + + // Deleting an element set via the IDL attribute. + deletionParent.ariaActiveDescendantElement = idlAttrElement; + assert_equals(deletionParent.getAttribute("aria-activedescendant"), ""); + + deletionParent.removeChild(idlAttrElement); + assert_equals(deletionParent.ariaActiveDescendantElement, null); + + // The content attribute is still empty. + assert_equals(deletionParent.getAttribute("aria-activedescendant"), ""); + }, "Deleting a reflected element should return null for the IDL attribute and the content attribute will be empty."); + </script> + + <div id="parentNode" role="listbox" aria-activedescendant="changingIdElement"> + <div role="option" id="changingIdElement">Item 1</div> + <div role="option" id="persistantIDElement">Item 2</div> + </div> + + <script> + test(function(t) { + const changingIdElement = document.getElementById("changingIdElement"); + assert_equals(parentNode.ariaActiveDescendantElement, changingIdElement); + + // Modify the id attribute. + changingIdElement.setAttribute("id", "new-id"); + + // The content attribute still reflects the old id, and we expect the + // Element reference to be null as there is no DOM node with id "original" + assert_equals(parentNode.getAttribute("aria-activedescendant"), "changingIdElement"); + assert_equals(parentNode.ariaActiveDescendantElement, null, "Element set via content attribute with a changed id will return null on getting"); + + parentNode.ariaActiveDescendantElement = changingIdElement; + assert_equals(parentNode.getAttribute("aria-activedescendant"), ""); + assert_equals(parentNode.ariaActiveDescendantElement, changingIdElement); + + // The explicitly set element takes precendance over the content attribute. + // This means that we still return the same element reference, but the + // content attribute is empty. + changingIdElement.setAttribute("id", "newer-id"); + assert_equals(parentNode.ariaActiveDescendantElement, changingIdElement, "explicitly set element is still present even after the id has been changed"); + assert_equals(parentNode.getAttribute("aria-activedescendant"), "", "content attribute is empty."); + }, "Changing the ID of an element doesn't lose the reference."); + </script> + + <!-- TODO(chrishall): change naming scheme to inner/outer --> + <div id="lightParent" role="listbox"> + <div id="lightElement" role="option">Hello world!</div> + </div> + <div id="shadowHostElement"></div> + + <script> + test(function(t) { + const lightElement = document.getElementById("lightElement"); + const shadowRoot = shadowHostElement.attachShadow({mode: "open"}); + + assert_equals(lightParent.ariaActiveDescendantElement, null, 'null before'); + assert_equals(lightParent.getAttribute('aria-activedescendant'), null, 'null before'); + + lightParent.ariaActiveDescendantElement = lightElement; + assert_equals(lightParent.ariaActiveDescendantElement, lightElement); + assert_equals(lightParent.getAttribute('aria-activedescendant'), ""); + + // Move the referenced element into shadow DOM. + // This will cause the computed attr-associated element to be null as the + // referenced element will no longer be in a valid scope. + // The underlying reference is kept intact, so if the referenced element is + // later restored to a valid scope the computed attr-associated element will + // then reflect + shadowRoot.appendChild(lightElement); + assert_equals(lightParent.ariaActiveDescendantElement, null, "computed attr-assoc element should be null as referenced element is in an invalid scope"); + assert_equals(lightParent.getAttribute("aria-activedescendant"), ""); + + // Move the referenced element back into light DOM. + // Since the underlying reference was kept intact, after moving the + // referenced element back to a valid scope should be reflected in the + // computed attr-associated element. + lightParent.appendChild(lightElement); + assert_equals(lightParent.ariaActiveDescendantElement, lightElement, "computed attr-assoc element should be restored as referenced element is back in a valid scope"); + assert_equals(lightParent.getAttribute("aria-activedescendant"), ""); + }, "Reparenting an element into a descendant shadow scope hides the element reference."); + </script> + + <div id='fruitbowl' role='listbox'> + <div id='apple' role='option'>I am an apple</div> + <div id='pear' role='option'>I am a pear</div> + <div id='banana' role='option'>I am a banana</div> + </div> + <div id='shadowFridge'></div> + + <script> + test(function(t) { + const shadowRoot = shadowFridge.attachShadow({mode: "open"}); + const banana = document.getElementById("banana"); + + fruitbowl.ariaActiveDescendantElement = apple; + assert_equals(fruitbowl.ariaActiveDescendantElement, apple); + assert_equals(fruitbowl.getAttribute("aria-activedescendant"), ""); + + // Move the referenced element into shadow DOM. + shadowRoot.appendChild(apple); + assert_equals(fruitbowl.ariaActiveDescendantElement, null, "computed attr-assoc element should be null as referenced element is in an invalid scope"); + // The content attribute is still empty. + assert_equals(fruitbowl.getAttribute("aria-activedescendant"), ""); + + // let us rename our banana to an apple + banana.setAttribute("id", "apple"); + const lyingBanana = document.getElementById("apple"); + assert_equals(lyingBanana, banana); + + // our ariaActiveDescendantElement thankfully isn't tricked. + // this is thanks to the underlying reference being kept intact, it is + // checked and found to be in an invalid scope. + assert_equals(fruitbowl.ariaActiveDescendantElement, null); + // our content attribute is empty. + assert_equals(fruitbowl.getAttribute("aria-activedescendant"), ""); + + // when we remove our IDL attribute, the content attribute is also thankfully cleared. + fruitbowl.ariaActiveDescendantElement = null; + assert_equals(fruitbowl.ariaActiveDescendantElement, null); + assert_equals(fruitbowl.getAttribute("aria-activedescendant"), null); + }, "Reparenting referenced element cannot cause retargeting of reference."); + </script> + + <div id='toaster' role='listbox'></div> + <div id='shadowPantry'></div> + + <script> + test(function(t) { + const shadowRoot = shadowPantry.attachShadow({mode: "open"}); + + // Our toast starts in the shadowPantry. + const toast = document.createElement("div"); + toast.setAttribute("id", "toast"); + shadowRoot.appendChild(toast); + + // Prepare my toast for toasting + toaster.ariaActiveDescendantElement = toast; + assert_equals(toaster.ariaActiveDescendantElement, null); + assert_equals(toaster.getAttribute("aria-activedescendant"), ""); + + // Time to make some toast + toaster.appendChild(toast); + assert_equals(toaster.ariaActiveDescendantElement, toast); + // Current spec behaviour: + assert_equals(toaster.getAttribute("aria-activedescendant"), ""); + }, "Element reference set in invalid scope remains intact throughout move to valid scope."); + </script> + + <div id="billingElementContainer"> + <div id="billingElement">Billing</div> + </div> + <div> + <div id="nameElement">Name</div> + <input type="text" id="input1" aria-labelledby="billingElement nameElement"/> + </div> + <div> + <div id="addressElement">Address</div> + <input type="text" id="input2"/> + </div> + + <script> + test(function(t) { + const billingElement = document.getElementById("billingElement") + assert_array_equals(input1.ariaLabelledByElements, [billingElement, nameElement], "parsed content attribute sets element references."); + assert_equals(input1.ariaLabelledByElements, input1.ariaLabelledByElements, "check idl attribute caching after parsing"); + assert_equals(input2.ariaLabelledByElements, null, "Testing missing content attribute after parsing."); + + input2.ariaLabelledByElements = [billingElement, addressElement]; + assert_array_equals(input2.ariaLabelledByElements, [billingElement, addressElement], "Testing IDL setter/getter."); + assert_equals(input1.ariaLabelledByElements, input1.ariaLabelledByElements, "check idl attribute caching after update"); + assert_equals(input2.getAttribute("aria-labelledby"), ""); + + // Remove the billingElement from the DOM. + // As it was explicitly set the underlying association will remain intact, + // but it will be hidden until the element is moved back into a valid scope. + billingElement.remove(); + assert_array_equals(input2.ariaLabelledByElements, [addressElement], "Computed ariaLabelledByElements shouldn't include billing when out of scope."); + + // Insert the billingElement back into the DOM and check that it is visible + // again, as the underlying association should have been kept intact. + billingElementContainer.appendChild(billingElement); + assert_array_equals(input2.ariaLabelledByElements, [billingElement, addressElement], "Billing element back in scope."); + + input2.ariaLabelledByElements = []; + assert_array_equals(input2.ariaLabelledByElements, [], "Testing IDL setter/getter for empty array."); + assert_equals(input2.getAttribute("aria-labelledby"), ""); + + input1.removeAttribute("aria-labelledby"); + assert_equals(input1.ariaLabelledByElements, null); + + input1.setAttribute("aria-labelledby", "nameElement addressElement"); + assert_array_equals(input1.ariaLabelledByElements, [nameElement, addressElement], + "computed value after setting attribute directly"); + + input1.ariaLabelledByElements = null; + assert_false(input1.hasAttribute("aria-labelledby", "Nullifying the IDL attribute should remove the content attribute.")); + }, "aria-labelledby."); + </script> + + <ul role="tablist"> + <li role="presentation"><a id="link1" role="tab" aria-controls="panel1">Tab 1</a></li> + <li role="presentation"><a id="link2" role="tab">Tab 2</a></li> + </ul> + + <div role="tabpanel" id="panel1"></div> + <div role="tabpanel" id="panel2"></div> + + <script> + test(function(t) { + assert_array_equals(link1.ariaControlsElements, [panel1]); + assert_equals(link2.ariaControlsElements, null); + + link2.setAttribute("aria-controls", "panel1 panel2"); + assert_array_equals(link2.ariaControlsElements, [panel1, panel2]); + + link1.ariaControlsElements = []; + assert_equals(link1.getAttribute("aria-controls"), ""); + + link2.ariaControlsElements = [panel1, panel2]; + assert_equals(link2.getAttribute("aria-controls"), ""); + assert_array_equals(link2.ariaControlsElements, [panel1, panel2]); + + link2.removeAttribute("aria-controls"); + assert_equals(link2.ariaControlsElements, null); + + link2.ariaControlsElements = [panel1, panel2]; + assert_equals(link2.getAttribute("aria-controls"), ""); + assert_array_equals(link2.ariaControlsElements, [panel1, panel2]); + + link2.ariaControlsElements = null; + assert_false(link2.hasAttribute("aria-controls", "Nullifying the IDL attribute should remove the content attribute.")); + }, "aria-controls."); + </script> + + <a id="describedLink" aria-describedby="description1 description2">Fruit</a> + <div id="description1">Delicious</div> + <div id="description2">Nutritious</div> + + <script> + test(function(t) { + assert_array_equals(describedLink.ariaDescribedByElements, [description1, description2]); + + describedLink.ariaDescribedByElements = [description1, description2]; + assert_equals(describedLink.getAttribute("aria-describedby"), ""); + assert_array_equals(describedLink.ariaDescribedByElements, [description1, description2]); + + describedLink.ariaDescribedByElements = []; + assert_equals(describedLink.getAttribute("aria-describedby"), ""); + + describedLink.setAttribute("aria-describedby", "description1"); + assert_array_equals(describedLink.ariaDescribedByElements, [description1]); + + describedLink.removeAttribute("aria-describedby"); + assert_equals(describedLink.ariaDescribedByElements, null); + + describedLink.ariaDescribedByElements = [description1, description2]; + assert_equals(describedLink.getAttribute("aria-describedby"), ""); + assert_array_equals(describedLink.ariaDescribedByElements, [description1, description2]); + + describedLink.ariaDescribedByElements = null; + assert_false(describedLink.hasAttribute("aria-describedby", "Nullifying the IDL attribute should remove the content attribute.")); + }, "aria-describedby."); + </script> + + <h2 id="titleHeading" aria-flowto="article1 article2">Title</h2> + <div>Next</div> + <article id="article2">Content2</article> + <article id="article1">Content1</article> + + <script> + test(function(t) { + const article1 = document.getElementById("article1"); + const article2 = document.getElementById("article2"); + + assert_array_equals(titleHeading.ariaFlowToElements, [article1, article2]); + + titleHeading.ariaFlowToElements = [article1, article2]; + assert_equals(titleHeading.getAttribute("aria-flowto"), ""); + assert_array_equals(titleHeading.ariaFlowToElements, [article1, article2]); + + titleHeading.ariaFlowToElements = []; + assert_equals(titleHeading.getAttribute("aria-flowto"), ""); + + titleHeading.setAttribute("aria-flowto", "article1"); + assert_array_equals(titleHeading.ariaFlowToElements, [article1]); + + titleHeading.removeAttribute("aria-flowto"); + assert_equals(titleHeading.ariaFlowToElements, null); + + titleHeading.ariaFlowToElements = [article1, article2]; + assert_equals(titleHeading.getAttribute("aria-flowto"), ""); + assert_array_equals(titleHeading.ariaFlowToElements, [article1, article2]); + + titleHeading.ariaFlowToElements = null; + assert_false(titleHeading.hasAttribute("aria-flowto", "Nullifying the IDL attribute should remove the content attribute.")); + }, "aria-flowto."); + </script> + + <ul> + <li id="listItemOwner" aria-owns="child1 child2">Parent</li> + </ul> + <ul> + <li id="child1">Child 1</li> + <li id="child2">Child 2</li> + </ul> + <script> + test(function(t) { + assert_array_equals(listItemOwner.ariaOwnsElements, [child1, child2]); + + listItemOwner.removeAttribute("aria-owns"); + assert_equals(listItemOwner.ariaOwnsElements, null); + + listItemOwner.ariaOwnsElements = [child1, child2]; + assert_equals(listItemOwner.getAttribute("aria-owns"), ""); + assert_array_equals(listItemOwner.ariaOwnsElements, [child1, child2]); + + listItemOwner.ariaOwnsElements = []; + assert_equals(listItemOwner.getAttribute("aria-owns"), ""); + + listItemOwner.setAttribute("aria-owns", "child1"); + assert_array_equals(listItemOwner.ariaOwnsElements, [child1]); + + listItemOwner.ariaOwnsElements = [child1, child2]; + assert_equals(listItemOwner.getAttribute("aria-owns"), ""); + assert_array_equals(listItemOwner.ariaOwnsElements, [child1, child2]); + + listItemOwner.ariaOwnsElements = null; + assert_false(listItemOwner.hasAttribute("aria-owns", "Nullifying the IDL attribute should remove the content attribute.")); + }, "aria-owns."); + </script> + + <div id="lightDomContainer"> + <h2 id="lightDomHeading" aria-flowto="shadowChild1 shadowChild2">Light DOM Heading</h2> + <div id="host"></div> + <p id="lightDomText1">Light DOM text</p> + <p id="lightDomText2">Light DOM text</p> + </div> + + <script> + test(function(t) { + const shadowRoot = host.attachShadow({mode: "open"}); + const shadowChild1 = document.createElement("article"); + shadowChild1.setAttribute("id", "shadowChild1"); + shadowRoot.appendChild(shadowChild1); + const shadowChild2 = document.createElement("article"); + shadowChild2.setAttribute("id", "shadowChild1"); + shadowRoot.appendChild(shadowChild2); + + // The elements in the content attribute are in a "darker" tree - they + // enter a shadow encapsulation boundary, so not be associated any more. + assert_array_equals(lightDomHeading.ariaFlowToElements, []); + + // These elements are in a shadow including ancestor, i.e "lighter" tree. + // Valid for the IDL attribute, but content attribute should be null. + shadowChild1.ariaFlowToElements = [lightDomText1, lightDomText2]; + assert_equals(shadowChild1.getAttribute("aria-flowto"), "", "empty content attribute for elements that cross shadow boundaries."); + + // These IDs belong to a different scope, so the attr-associated-element + // cannot be computed. + shadowChild2.setAttribute("aria-flowto", "lightDomText1 lightDomText2"); + assert_array_equals(shadowChild2.ariaFlowToElements, []); + + // Elements that cross into shadow DOM are dropped, only reflect the valid + // elements in IDL and in the content attribute. + lightDomHeading.ariaFlowToElements = [shadowChild1, shadowChild2, lightDomText1, lightDomText2]; + assert_array_equals(lightDomHeading.ariaFlowToElements, [lightDomText1, lightDomText2], "IDL should only include valid elements"); + assert_equals(lightDomHeading.getAttribute("aria-flowto"), "", "empty content attribute if any given elements cross shadow boundaries"); + + // Using a mixture of elements in the same scope and in a shadow including + // ancestor should set the IDL attribute, but should reflect the empty + // string in the content attribute. + shadowChild1.removeAttribute("aria-flowto"); + shadowChild1.ariaFlowToElements = [shadowChild1, lightDomText1]; + assert_equals(shadowChild1.getAttribute("aria-flowto"), "", "Setting IDL elements with a mix of scopes should reflect an empty string in the content attribute") + + }, "shadow DOM behaviour for FrozenArray element reflection."); + </script> + + <div id="describedButtonContainer"> + <div id="buttonDescription1">Delicious</div> + <div id="buttonDescription2">Nutritious</div> + <div id="outerShadowHost"></div> + </div> + + <script> + test(function(t) { + const description1 = document.getElementById("buttonDescription1"); + const description2 = document.getElementById("buttonDescription2"); + const outerShadowRoot = outerShadowHost.attachShadow({mode: "open"}); + const innerShadowHost = document.createElement("div"); + outerShadowRoot.appendChild(innerShadowHost); + const innerShadowRoot = innerShadowHost.attachShadow({mode: "open"}); + + // Create an element, add some attr associated light DOM elements and append it to the outer shadow root. + const describedElement = document.createElement("button"); + describedButtonContainer.appendChild(describedElement); + describedElement.ariaDescribedByElements = [description1, description2]; + + // All elements were in the same scope, so elements are gettable and the content attribute is empty. + assert_array_equals(describedElement.ariaDescribedByElements, [description1, description2], "same scope reference"); + assert_equals(describedElement.getAttribute("aria-describedby"), ""); + + outerShadowRoot.appendChild(describedElement); + + // Explicitly set attr-associated-elements should still be gettable because we are referencing elements in a lighter scope. + // The content attr is empty. + assert_array_equals(describedElement.ariaDescribedByElements, [description1, description2], "lighter scope reference"); + assert_equals(describedElement.getAttribute("aria-describedby"), ""); + + // Move the explicitly set elements into a deeper shadow DOM to test the relationship should not be gettable. + innerShadowRoot.appendChild(description1); + innerShadowRoot.appendChild(description2); + + // Explicitly set elements are no longer retrievable, because they are no longer in a valid scope. + assert_array_equals(describedElement.ariaDescribedByElements, [], "invalid scope reference"); + assert_equals(describedElement.getAttribute("aria-describedby"), ""); + + // Move into the same shadow scope as the explicitly set elements to test that the elements are gettable. + innerShadowRoot.appendChild(describedElement); + assert_array_equals(describedElement.ariaDescribedByElements, [description1, description2], "restored valid scope reference"); + assert_equals(describedElement.getAttribute("aria-describedby"), ""); + }, "Moving explicitly set elements across shadow DOM boundaries."); + </script> + + <div id="sameScopeContainer"> + <div id="labeledby" aria-labeledby="headingLabel1 headingLabel2">Misspelling</div> + <div id="headingLabel1">Wonderful</div> + <div id="headingLabel2">Fantastic</div> + + <div id="headingShadowHost"></div> + </div> + + <script> + test(function(t) { + const shadowRoot = headingShadowHost.attachShadow({mode: "open"}); + const headingElement = document.createElement("h1"); + const headingLabel1 = document.getElementById("headingLabel1") + const headingLabel2 = document.getElementById("headingLabel2") + shadowRoot.appendChild(headingElement); + + assert_array_equals(labeledby.ariaLabelledByElements, [headingLabel1, headingLabel2], "aria-labeled by is supported by IDL getter."); + + // Explicitly set elements are in a lighter shadow DOM, so that's ok. + headingElement.ariaLabelledByElements = [headingLabel1, headingLabel2]; + assert_array_equals(headingElement.ariaLabelledByElements, [headingLabel1, headingLabel2], "Lighter elements are gettable when explicitly set."); + assert_equals(headingElement.getAttribute("aria-labelledby"), ""); + + // Move into Light DOM, explicitly set elements should still be gettable. + // Note that the content attribute is still empty. + sameScopeContainer.appendChild(headingElement); + assert_array_equals(headingElement.ariaLabelledByElements, [headingLabel1, headingLabel2], "Elements are all in same scope, so gettable."); + assert_equals(headingElement.getAttribute("aria-labelledby"), "", "Content attribute is empty."); + + // Reset the association, the content attribute is sitll empty. + headingElement.ariaLabelledByElements = [headingLabel1, headingLabel2]; + assert_equals(headingElement.getAttribute("aria-labelledby"), ""); + + // Remove the referring element from the DOM, elements are no longer longer exposed, + // underlying internal reference is still kept intact. + headingElement.remove(); + assert_array_equals(headingElement.ariaLabelledByElements, [], "Element is no longer in the document, so references should no longer be exposed."); + assert_equals(headingElement.getAttribute("aria-labelledby"), ""); + + // Insert it back in. + sameScopeContainer.appendChild(headingElement); + assert_array_equals(headingElement.ariaLabelledByElements, [headingLabel1, headingLabel2], "Element is restored to valid scope, so should be gettable."); + assert_equals(headingElement.getAttribute("aria-labelledby"), ""); + + // Remove everything from the DOM, nothing is exposed again. + headingLabel1.remove(); + headingLabel2.remove(); + assert_array_equals(headingElement.ariaLabelledByElements, []); + assert_equals(headingElement.getAttribute("aria-labelledby"), ""); + assert_equals(document.getElementById("headingLabel1"), null); + assert_equals(document.getElementById("headingLabel2"), null); + + // Reset the association. + headingElement.ariaLabelledByElements = [headingLabel1, headingLabel2]; + assert_array_equals(headingElement.ariaLabelledByElements, []); + assert_equals(headingElement.getAttribute("aria-labelledby"), ""); + }, "Moving explicitly set elements around within the same scope, and removing from the DOM."); + </script> + + <input id="input"> + <optgroup> + <option id="first">First option</option> + <option id="second">Second option</option> + </optgroup> + + <script> + test(function(t) { + input.ariaActiveDescendantElement = first; + first.parentElement.appendChild(first); + + assert_equals(input.ariaActiveDescendantElement, first); + }, "Reparenting."); + </script> + + <div id='fromDiv'></div> + + <script> + test(function(t) { + const toSpan = document.createElement('span'); + toSpan.setAttribute("id", "toSpan"); + fromDiv.ariaActiveDescendantElement = toSpan; + + assert_equals(fromDiv.ariaActiveDescendantElement, null, "Referenced element not inserted into document, so is in an invalid scope."); + assert_equals(fromDiv.getAttribute("aria-activedescendant"), "", "Invalid scope, so content attribute not set."); + + fromDiv.appendChild(toSpan); + assert_equals(fromDiv.ariaActiveDescendantElement, toSpan, "Referenced element now inserted into the document."); + assert_equals(fromDiv.getAttribute("aria-activedescendant"), "", "Content attribute remains empty, as it is only updated at set time."); + + }, "Attaching element reference before it's inserted into the DOM."); + </script> + + <div id='originalDocumentDiv'></div> + + <script> + test(function(t) { + const newDoc = document.implementation.createHTMLDocument('new document'); + const newDocSpan = newDoc.createElement('span'); + newDoc.body.appendChild(newDocSpan); + + // Create a reference across documents. + originalDocumentDiv.ariaActiveDescendantElement = newDocSpan; + + assert_equals(originalDocumentDiv.ariaActiveDescendantElement, null, "Cross-document is an invalid scope, so reference will not be visible."); + assert_equals(fromDiv.getAttribute("aria-activedescendant"), "", "Invalid scope when set, so content attribute not set."); + + // "Move" span to first document. + originalDocumentDiv.appendChild(newDocSpan); + + // Implementation defined: moving object into same document from other document may cause reference to become visible. + assert_equals(originalDocumentDiv.ariaActiveDescendantElement, newDocSpan, "Implementation defined: moving object back *may* make reference visible."); + assert_equals(fromDiv.getAttribute("aria-activedescendant"), "", "Invalid scope when set, so content attribute not set."); + }, "Cross-document references and moves."); + </script> + + + <script> + test(function(t) { + const otherDoc = document.implementation.createHTMLDocument('otherDoc'); + const otherDocDiv = otherDoc.createElement('div'); + const otherDocSpan = otherDoc.createElement('span'); + otherDocDiv.appendChild(otherDocSpan); + otherDoc.body.appendChild(otherDocDiv); + + otherDocDiv.ariaActiveDescendantElement = otherDocSpan; + assert_equals(otherDocDiv.ariaActiveDescendantElement, otherDocSpan, "Setting reference on a different document."); + + // Adopt element from other oducment. + document.body.appendChild(document.adoptNode(otherDocDiv)); + assert_equals(otherDocDiv.ariaActiveDescendantElement, otherDocSpan, "Reference should be kept on the new document too."); + }, "Adopting element keeps references."); + </script> + + <div id="cachingInvariantMain"></div> + <div id="cachingInvariantElement1"></div> + <div id="cachingInvariantElement2"></div> + <div id="cachingInvariantElement3"></div> + <div id="cachingInvariantElement4"></div> + <div id="cachingInvariantElement5"></div> + + <script> + test(function(t) { + cachingInvariantMain.ariaControlsElements = [cachingInvariantElement1, cachingInvariantElement2]; + cachingInvariantMain.ariaDescribedByElements = [cachingInvariantElement3, cachingInvariantElement4]; + cachingInvariantMain.ariaDetailsElements = [cachingInvariantElement5]; + cachingInvariantMain.ariaFlowToElements = [cachingInvariantElement1, cachingInvariantElement3]; + cachingInvariantMain.ariaLabelledByElements = [cachingInvariantElement2, cachingInvariantElement4]; + cachingInvariantMain.ariaOwnsElements = [cachingInvariantElement1, cachingInvariantElement2, cachingInvariantElement3]; + + let ariaControlsElementsArray = cachingInvariantMain.ariaControlsElements; + let ariaDescribedByElementsArray = cachingInvariantMain.ariaDescribedByElements; + let ariaDetailsElementsArray = cachingInvariantMain.ariaDetailsElements; + let ariaFlowToElementsArray = cachingInvariantMain.ariaFlowToElements; + let ariaLabelledByElementsArray = cachingInvariantMain.ariaLabelledByElements; + let ariaOwnsElementsArray = cachingInvariantMain.ariaOwnsElements; + + assert_equals(ariaControlsElementsArray, cachingInvariantMain.ariaControlsElements, "Caching invariant for ariaControlsElements"); + assert_equals(ariaDescribedByElementsArray, cachingInvariantMain.ariaDescribedByElements, "Caching invariant for ariaDescribedByElements"); + assert_equals(ariaDetailsElementsArray, cachingInvariantMain.ariaDetailsElements, "Caching invariant for ariaDetailsElements"); + assert_equals(ariaFlowToElementsArray, cachingInvariantMain.ariaFlowToElements, "Caching invariant for ariaFlowToElements"); + assert_equals(ariaLabelledByElementsArray, cachingInvariantMain.ariaLabelledByElements, "Caching invariant for ariaLabelledByElements"); + assert_equals(ariaOwnsElementsArray, cachingInvariantMain.ariaOwnsElements, "Caching invariant for ariaOwnsElements"); + }, "Caching invariant different attributes."); + </script> + + <div id="cachingInvariantMain1"></div> + <div id="cachingInvariantMain2"></div> + + <script> + test(function(t) { + cachingInvariantMain1.ariaDescribedByElements = [cachingInvariantElement1, cachingInvariantElement2]; + cachingInvariantMain2.ariaDescribedByElements = [cachingInvariantElement3, cachingInvariantElement4]; + + let ariaDescribedByElementsArray1 = cachingInvariantMain1.ariaDescribedByElements; + let ariaDescribedByElementsArray2 = cachingInvariantMain2.ariaDescribedByElements; + + assert_equals(ariaDescribedByElementsArray1, cachingInvariantMain1.ariaDescribedByElements, "Caching invariant for ariaDescribedByElements in one elemnt"); + assert_equals(ariaDescribedByElementsArray2, cachingInvariantMain2.ariaDescribedByElements, "Caching invariant for ariaDescribedByElements in onother elemnt"); + }, "Caching invariant different elements."); + </script> + + <!-- TODO(chrishall): add additional GC test covering: + if an element is in an invalid scope but attached to the document, it's + not GC'd; + --> + + <!-- TODO(chrishall): add additional GC test covering: + if an element is not attached to the document, but is in a tree fragment + which is not GC'd because there is a script reference to another element + in the tree fragment, and the relationship is valid because it is between + two elements in that tree fragment, the relationship is exposed *and* the + element is not GC'd + --> + +</html> |