tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

aria-element-reflection.html (41864B)


      1 <!DOCTYPE HTML>
      2 <html>
      3  <head>
      4    <meta charset="utf-8" />
      5    <title>Element Reflection for aria-activedescendant and aria-errormessage</title>
      6    <link rel=help href="https://whatpr.org/html/3917/common-dom-interfaces.html#reflecting-content-attributes-in-idl-attributes:element">
      7    <link rel="author" title="Meredith Lane" href="meredithl@chromium.org">
      8    <script src="/resources/testharness.js"></script>
      9    <script src="/resources/testharnessreport.js"></script>
     10  </head>
     11 
     12  <div id="activedescendant" aria-activedescendant="x"></div>
     13 
     14  <div id="parentListbox" role="listbox" aria-activedescendant="i1">
     15    <div role="option" id="i1">Item 1</div>
     16    <div role="option" id="i2">Item 2</div>
     17  </div>
     18 
     19  <script>
     20  test(function(t) {
     21    assert_equals(activedescendant.ariaActiveDescendantElement, null,
     22                  "invalid ID for relationship returns null");
     23 
     24    // Element reference should be set if the content attribute was included.
     25    assert_equals(parentListbox.getAttribute("aria-activedescendant"), "i1", "check content attribute after parsing.");
     26    assert_equals(parentListbox.ariaActiveDescendantElement, i1, "check idl attribute after parsing.");
     27    assert_equals(parentListbox.ariaActiveDescendantElement, parentListbox.ariaActiveDescendantElement, "check idl attribute caching after parsing.");
     28 
     29    // If we set the content attribute, the element reference should reflect this.
     30    parentListbox.setAttribute("aria-activedescendant", "i2");
     31    assert_equals(parentListbox.ariaActiveDescendantElement, i2, "setting the content attribute updates the element reference.");
     32    assert_equals(parentListbox.ariaActiveDescendantElement, parentListbox.ariaActiveDescendantElement, "check idl attribute caching after update.");
     33 
     34    // Setting the element reference should set the empty string in the content attribute.
     35    parentListbox.ariaActiveDescendantElement = i1;
     36    assert_equals(parentListbox.ariaActiveDescendantElement, i1, "getter should return the right element reference.");
     37    assert_equals(parentListbox.getAttribute("aria-activedescendant"), "", "content attribute should be empty.");
     38 
     39    // Both content and IDL attribute should be nullable.
     40    parentListbox.ariaActiveDescendantElement = null;
     41    assert_equals(parentListbox.ariaActiveDescendantElement, null);
     42    assert_false(parentListbox.hasAttribute("aria-activedescendant"));
     43    assert_equals(parentListbox.getAttribute("aria-activedescendant"), null, "nullifying the idl attribute removes the content attribute.");
     44 
     45    // Setting content attribute to non-existent or non compatible element should nullify the IDL attribute.
     46    // Reset the element to an existant one.
     47    parentListbox.setAttribute("aria-activedescendant", "i1");
     48    assert_equals(parentListbox.ariaActiveDescendantElement, i1, "reset attribute.");
     49 
     50    parentListbox.setAttribute("aria-activedescendant", "non-existent-element");
     51    assert_equals(parentListbox.getAttribute("aria-activedescendant"), "non-existent-element");
     52    assert_equals(parentListbox.ariaActiveDescendantElement, null,"non-DOM content attribute should null the element reference");
     53  }, "aria-activedescendant element reflection");
     54  </script>
     55 
     56  <div id="parentListbox2" role="listbox" aria-activedescendant="option1">
     57    <div role="option" id="option1">Item 1</div>
     58    <div role="option" id="option2">Item 2</div>
     59  </div>
     60 
     61  <script>
     62  test(function(t) {
     63    const option1 = document.getElementById("option1");
     64    const option2 = document.getElementById("option2");
     65    assert_equals(parentListbox2.ariaActiveDescendantElement, option1);
     66    option1.removeAttribute("id");
     67    option2.setAttribute("id", "option1");
     68    const option2Duplicate = document.getElementById("option1");
     69    assert_equals(option2, option2Duplicate);
     70 
     71    assert_equals(parentListbox2.ariaActiveDescendantElement, option2);
     72  }, "If the content attribute is set directly, the IDL attribute getter always returns the first element whose ID matches the content attribute.");
     73  </script>
     74 
     75  <div id="blankIdParent" role="listbox">
     76    <div role="option" id="multiple-id"></div>
     77    <div role="option" id="multiple-id"></div>
     78  </div>
     79 
     80  <script>
     81  test(function(t) {
     82    // Get second child of parent. This violates the setting of a reflected element
     83    // as it will not be the first child of the parent with that ID, which should
     84    // result in an empty string for the content attribute.
     85    blankIdParent.ariaActiveDescendantElement = blankIdParent.children[1];
     86    assert_true(blankIdParent.hasAttribute("aria-activedescendant"));
     87    assert_equals(blankIdParent.getAttribute("aria-activedescendant"), "");
     88    assert_equals(blankIdParent.ariaActiveDescendantElement, blankIdParent.children[1]);
     89  }, "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");
     90  </script>
     91 
     92  <div id="outerContainer">
     93    <p id="lightParagraph">Hello world!</p>
     94    <span id="shadowHost">
     95    </span>
     96  </div>
     97 
     98  <script>
     99  test(function(t) {
    100    const shadow = shadowHost.attachShadow({mode: "open"});
    101    const link = document.createElement("a");
    102    shadow.appendChild(link);
    103 
    104    assert_equals(lightParagraph.ariaActiveDescendantElement, null);
    105 
    106    // The given element crosses a shadow dom boundary, so it cannot be
    107    // set as an element reference.
    108    lightParagraph.ariaActiveDescendantElement = link;
    109    assert_equals(lightParagraph.ariaActiveDescendantElement, null);
    110 
    111    // The given element crosses a shadow dom boundary (upwards), so
    112    // can be used as an element reference, but the content attribute
    113    // should reflect the empty string.
    114    link.ariaActiveDescendantElement = lightParagraph;
    115    assert_equals(link.ariaActiveDescendantElement, lightParagraph);
    116    assert_equals(link.getAttribute("aria-activedescendant"), "");
    117  }, "Setting an element reference that crosses into a shadow tree is disallowed, but setting one that is in a shadow inclusive ancestor is allowed.");
    118  </script>
    119 
    120  <input id="startTime" ></input>
    121  <span id="errorMessage">Invalid Time</span>
    122 
    123  <script>
    124  test(function(t) {
    125    startTime.ariaErrorMessageElements = [errorMessage];
    126    assert_equals(startTime.getAttribute("aria-errormessage"), "");
    127    assert_array_equals(startTime.ariaErrorMessageElements, [errorMessage]);
    128 
    129    startTime.ariaErrorMessageElements = [];
    130    assert_array_equals(startTime.ariaErrorMessageElements, []);
    131    assert_equals(startTime.getAttribute("aria-errormessage"), "");
    132 
    133    startTime.setAttribute("aria-errormessage", "errorMessage");
    134    assert_array_equals(startTime.ariaErrorMessageElements, [errorMessage]);
    135 
    136  }, "aria-errormessage");
    137 
    138  test(function (t) {
    139      assert_false('ariaErrorMessageElement' in startTime);
    140  }, 'ariaErrorMessageElement is not defined')
    141 
    142  </script>
    143 
    144  <label>
    145    Password:
    146    <input id="passwordField" type="password" aria-details="pw">
    147  </label>
    148 
    149  <ul>
    150    <li id="listItem1">First description.</li>
    151    <li id="listItem2">Second description.</li>
    152  </ul>
    153 
    154  <script>
    155 
    156  test(function(t) {
    157    assert_array_equals(passwordField.ariaDetailsElements, []);
    158    passwordField.ariaDetailsElements = [ listItem1 ];
    159    assert_equals(passwordField.getAttribute("aria-details"), "");
    160    assert_array_equals(passwordField.ariaDetailsElements, [ listItem1 ]);
    161 
    162    passwordField.ariaDetailsElements = [ listItem2 ];
    163    assert_equals(passwordField.getAttribute("aria-details"), "");
    164    assert_array_equals(passwordField.ariaDetailsElements, [ listItem2 ]);
    165  }, "aria-details");
    166  </script>
    167 
    168  <div id="deletionParent" role="listbox" aria-activedescendant="contentAttrElement">
    169    <div role="option" id="contentAttrElement">Item 1</div>
    170    <div role="option" id="idlAttrElement">Item 2</div>
    171  </div>
    172 
    173  <script>
    174 
    175  test(function(t) {
    176    const contentAttrElement = document.getElementById("contentAttrElement");
    177    const idlAttrElement = document.getElementById("idlAttrElement");
    178 
    179    assert_equals(deletionParent.getAttribute("aria-activedescendant"), "contentAttrElement");
    180    assert_equals(deletionParent.ariaActiveDescendantElement, contentAttrElement);
    181 
    182    // Deleting an element set via the content attribute.
    183    deletionParent.removeChild(contentAttrElement);
    184    assert_equals(deletionParent.getAttribute("aria-activedescendant"), "contentAttrElement");
    185 
    186    // As it was not explitly set, the attr-associated-element is computed from the content attribute,
    187    // and since descendant1 has been removed from the DOM, it is not valid.
    188    assert_equals(deletionParent.ariaActiveDescendantElement, null);
    189 
    190    // Deleting an element set via the IDL attribute.
    191    deletionParent.ariaActiveDescendantElement = idlAttrElement;
    192    assert_equals(deletionParent.getAttribute("aria-activedescendant"), "");
    193 
    194    deletionParent.removeChild(idlAttrElement);
    195    assert_equals(deletionParent.ariaActiveDescendantElement, null);
    196 
    197    // The content attribute is still empty.
    198    assert_equals(deletionParent.getAttribute("aria-activedescendant"), "");
    199  }, "Deleting a reflected element should return null for the IDL attribute and the content attribute will be empty.");
    200  </script>
    201 
    202  <div id="parentNode" role="listbox" aria-activedescendant="changingIdElement">
    203    <div role="option" id="changingIdElement">Item 1</div>
    204    <div role="option" id="persistantIDElement">Item 2</div>
    205  </div>
    206 
    207  <script>
    208  test(function(t) {
    209    const changingIdElement = document.getElementById("changingIdElement");
    210    assert_equals(parentNode.ariaActiveDescendantElement, changingIdElement);
    211 
    212    // Modify the id attribute.
    213    changingIdElement.setAttribute("id", "new-id");
    214 
    215    // The content attribute still reflects the old id, and we expect the
    216    // Element reference to be null as there is no DOM node with id "original"
    217    assert_equals(parentNode.getAttribute("aria-activedescendant"), "changingIdElement");
    218    assert_equals(parentNode.ariaActiveDescendantElement, null, "Element set via content attribute with a changed id will return null on getting");
    219 
    220    parentNode.ariaActiveDescendantElement = changingIdElement;
    221    assert_equals(parentNode.getAttribute("aria-activedescendant"), "");
    222    assert_equals(parentNode.ariaActiveDescendantElement, changingIdElement);
    223 
    224    // The explicitly set element takes precendance over the content attribute.
    225    // This means that we still return the same element reference, but the
    226    // content attribute is empty.
    227    changingIdElement.setAttribute("id", "newer-id");
    228    assert_equals(parentNode.ariaActiveDescendantElement, changingIdElement, "explicitly set element is still present even after the id has been changed");
    229    assert_equals(parentNode.getAttribute("aria-activedescendant"), "", "content attribute is empty.");
    230  }, "Changing the ID of an element doesn't lose the reference.");
    231  </script>
    232 
    233  <!-- TODO(chrishall): change naming scheme to inner/outer -->
    234  <div id="lightParent" role="listbox">
    235    <div id="lightElement" role="option">Hello world!</div>
    236  </div>
    237  <div id="shadowHostElement"></div>
    238 
    239  <script>
    240  test(function(t) {
    241    const lightElement = document.getElementById("lightElement");
    242    const shadowRoot = shadowHostElement.attachShadow({mode: "open"});
    243 
    244    assert_equals(lightParent.ariaActiveDescendantElement, null, 'null before');
    245    assert_equals(lightParent.getAttribute('aria-activedescendant'), null, 'null before');
    246 
    247    lightParent.ariaActiveDescendantElement = lightElement;
    248    assert_equals(lightParent.ariaActiveDescendantElement, lightElement);
    249    assert_equals(lightParent.getAttribute('aria-activedescendant'), "");
    250 
    251    // Move the referenced element into shadow DOM.
    252    // This will cause the computed attr-associated element to be null as the
    253    // referenced element will no longer be in a valid scope.
    254    // The underlying reference is kept intact, so if the referenced element is
    255    // later restored to a valid scope the computed attr-associated element will
    256    // then reflect
    257    shadowRoot.appendChild(lightElement);
    258    assert_equals(lightParent.ariaActiveDescendantElement, null, "computed attr-assoc element should be null as referenced element is in an invalid scope");
    259    assert_equals(lightParent.getAttribute("aria-activedescendant"), "");
    260 
    261    // Move the referenced element back into light DOM.
    262    // Since the underlying reference was kept intact, after moving the
    263    // referenced element back to a valid scope should be reflected in the
    264    // computed attr-associated element.
    265    lightParent.appendChild(lightElement);
    266    assert_equals(lightParent.ariaActiveDescendantElement, lightElement, "computed attr-assoc element should be restored as referenced element is back in a valid scope");
    267    assert_equals(lightParent.getAttribute("aria-activedescendant"), "");
    268  }, "Reparenting an element into a descendant shadow scope hides the element reference.");
    269  </script>
    270 
    271  <div id='fruitbowl' role='listbox'>
    272    <div id='apple' role='option'>I am an apple</div>
    273    <div id='pear' role='option'>I am a pear</div>
    274    <div id='banana' role='option'>I am a banana</div>
    275  </div>
    276  <div id='shadowFridge'></div>
    277 
    278  <script>
    279  test(function(t) {
    280    const shadowRoot = shadowFridge.attachShadow({mode: "open"});
    281    const banana = document.getElementById("banana");
    282 
    283    fruitbowl.ariaActiveDescendantElement = apple;
    284    assert_equals(fruitbowl.ariaActiveDescendantElement, apple);
    285    assert_equals(fruitbowl.getAttribute("aria-activedescendant"), "");
    286 
    287    // Move the referenced element into shadow DOM.
    288    shadowRoot.appendChild(apple);
    289    assert_equals(fruitbowl.ariaActiveDescendantElement, null, "computed attr-assoc element should be null as referenced element is in an invalid scope");
    290    // The content attribute is still empty.
    291    assert_equals(fruitbowl.getAttribute("aria-activedescendant"), "");
    292 
    293    // let us rename our banana to an apple
    294    banana.setAttribute("id", "apple");
    295    const lyingBanana = document.getElementById("apple");
    296    assert_equals(lyingBanana, banana);
    297 
    298    // our ariaActiveDescendantElement thankfully isn't tricked.
    299    // this is thanks to the underlying reference being kept intact, it is
    300    // checked and found to be in an invalid scope.
    301    assert_equals(fruitbowl.ariaActiveDescendantElement, null);
    302    // our content attribute is empty.
    303    assert_equals(fruitbowl.getAttribute("aria-activedescendant"), "");
    304 
    305    // when we remove our IDL attribute, the content attribute is also thankfully cleared.
    306    fruitbowl.ariaActiveDescendantElement = null;
    307    assert_equals(fruitbowl.ariaActiveDescendantElement, null);
    308    assert_equals(fruitbowl.getAttribute("aria-activedescendant"), null);
    309  }, "Reparenting referenced element cannot cause retargeting of reference.");
    310  </script>
    311 
    312  <div id='toaster' role='listbox'></div>
    313  <div id='shadowPantry'></div>
    314 
    315  <script>
    316  test(function(t) {
    317    const shadowRoot = shadowPantry.attachShadow({mode: "open"});
    318 
    319    // Our toast starts in the shadowPantry.
    320    const toast = document.createElement("div");
    321    toast.setAttribute("id", "toast");
    322    shadowRoot.appendChild(toast);
    323 
    324    // Prepare my toast for toasting
    325    toaster.ariaActiveDescendantElement = toast;
    326    assert_equals(toaster.ariaActiveDescendantElement, null);
    327    assert_equals(toaster.getAttribute("aria-activedescendant"), "");
    328 
    329    // Time to make some toast
    330    toaster.appendChild(toast);
    331    assert_equals(toaster.ariaActiveDescendantElement, toast);
    332    // Current spec behaviour:
    333    assert_equals(toaster.getAttribute("aria-activedescendant"), "");
    334  }, "Element reference set in invalid scope remains intact throughout move to valid scope.");
    335  </script>
    336 
    337  <div id="billingElementContainer">
    338      <div id="billingElement">Billing</div>
    339  </div>
    340  <div>
    341      <div id="nameElement">Name</div>
    342      <input type="text" id="input1" aria-labelledby="billingElement nameElement"/>
    343  </div>
    344  <div>
    345      <div id="addressElement">Address</div>
    346      <input type="text" id="input2"/>
    347  </div>
    348 
    349  <script>
    350  test(function(t) {
    351    const billingElement = document.getElementById("billingElement")
    352    assert_array_equals(input1.ariaLabelledByElements, [billingElement, nameElement], "parsed content attribute sets element references.");
    353    assert_equals(input1.ariaLabelledByElements, input1.ariaLabelledByElements, "check idl attribute caching after parsing");
    354    assert_equals(input2.ariaLabelledByElements, null, "Testing missing content attribute after parsing.");
    355 
    356    input2.ariaLabelledByElements = [billingElement, addressElement];
    357    assert_array_equals(input2.ariaLabelledByElements, [billingElement, addressElement], "Testing IDL setter/getter.");
    358    assert_equals(input1.ariaLabelledByElements, input1.ariaLabelledByElements, "check idl attribute caching after update");
    359    assert_equals(input2.getAttribute("aria-labelledby"), "");
    360 
    361    // Remove the billingElement from the DOM.
    362    // As it was explicitly set the underlying association will remain intact,
    363    // but it will be hidden until the element is moved back into a valid scope.
    364    billingElement.remove();
    365    assert_array_equals(input2.ariaLabelledByElements, [addressElement], "Computed ariaLabelledByElements shouldn't include billing when out of scope.");
    366 
    367    // Insert the billingElement back into the DOM and check that it is visible
    368    // again, as the underlying association should have been kept intact.
    369    billingElementContainer.appendChild(billingElement);
    370    assert_array_equals(input2.ariaLabelledByElements, [billingElement, addressElement], "Billing element back in scope.");
    371 
    372    input2.ariaLabelledByElements = [];
    373    assert_array_equals(input2.ariaLabelledByElements, [], "Testing IDL setter/getter for empty array.");
    374    assert_equals(input2.getAttribute("aria-labelledby"), "");
    375 
    376    input1.removeAttribute("aria-labelledby");
    377    assert_equals(input1.ariaLabelledByElements, null);
    378 
    379    input1.setAttribute("aria-labelledby", "nameElement addressElement");
    380    assert_array_equals(input1.ariaLabelledByElements, [nameElement, addressElement],
    381      "computed value after setting attribute directly");
    382 
    383    input1.ariaLabelledByElements = null;
    384    assert_false(input1.hasAttribute("aria-labelledby", "Nullifying the IDL attribute should remove the content attribute."));
    385  }, "aria-labelledby.");
    386  </script>
    387 
    388  <ul role="tablist">
    389    <li role="presentation"><a id="link1" role="tab" aria-controls="panel1">Tab 1</a></li>
    390    <li role="presentation"><a id="link2" role="tab">Tab 2</a></li>
    391  </ul>
    392 
    393  <div role="tabpanel" id="panel1"></div>
    394  <div role="tabpanel" id="panel2"></div>
    395 
    396  <script>
    397  test(function(t) {
    398    assert_array_equals(link1.ariaControlsElements, [panel1]);
    399    assert_equals(link2.ariaControlsElements, null);
    400 
    401    link2.setAttribute("aria-controls", "panel1 panel2");
    402    assert_array_equals(link2.ariaControlsElements, [panel1, panel2]);
    403 
    404    link1.ariaControlsElements = [];
    405    assert_equals(link1.getAttribute("aria-controls"), "");
    406 
    407    link2.ariaControlsElements = [panel1, panel2];
    408    assert_equals(link2.getAttribute("aria-controls"), "");
    409    assert_array_equals(link2.ariaControlsElements, [panel1, panel2]);
    410 
    411    link2.removeAttribute("aria-controls");
    412    assert_equals(link2.ariaControlsElements, null);
    413 
    414    link2.ariaControlsElements = [panel1, panel2];
    415    assert_equals(link2.getAttribute("aria-controls"), "");
    416    assert_array_equals(link2.ariaControlsElements, [panel1, panel2]);
    417 
    418    link2.ariaControlsElements = null;
    419    assert_false(link2.hasAttribute("aria-controls", "Nullifying the IDL attribute should remove the content attribute."));
    420  }, "aria-controls.");
    421  </script>
    422 
    423  <a id="describedLink" aria-describedby="description1 description2">Fruit</a>
    424  <div id="description1">Delicious</div>
    425  <div id="description2">Nutritious</div>
    426 
    427  <script>
    428  test(function(t) {
    429    assert_array_equals(describedLink.ariaDescribedByElements, [description1, description2]);
    430 
    431    describedLink.ariaDescribedByElements = [description1, description2];
    432    assert_equals(describedLink.getAttribute("aria-describedby"), "");
    433    assert_array_equals(describedLink.ariaDescribedByElements, [description1, description2]);
    434 
    435    describedLink.ariaDescribedByElements = [];
    436    assert_equals(describedLink.getAttribute("aria-describedby"), "");
    437 
    438    describedLink.setAttribute("aria-describedby", "description1");
    439    assert_array_equals(describedLink.ariaDescribedByElements, [description1]);
    440 
    441    describedLink.removeAttribute("aria-describedby");
    442    assert_equals(describedLink.ariaDescribedByElements, null);
    443 
    444    describedLink.ariaDescribedByElements = [description1, description2];
    445    assert_equals(describedLink.getAttribute("aria-describedby"), "");
    446    assert_array_equals(describedLink.ariaDescribedByElements, [description1, description2]);
    447 
    448    describedLink.ariaDescribedByElements = null;
    449    assert_false(describedLink.hasAttribute("aria-describedby", "Nullifying the IDL attribute should remove the content attribute."));
    450  }, "aria-describedby.");
    451  </script>
    452 
    453  <h2 id="titleHeading" aria-flowto="article1 article2">Title</h2>
    454  <div>Next</div>
    455  <article id="article2">Content2</article>
    456  <article id="article1">Content1</article>
    457 
    458  <script>
    459  test(function(t) {
    460    const article1 = document.getElementById("article1");
    461    const article2 = document.getElementById("article2");
    462 
    463    assert_array_equals(titleHeading.ariaFlowToElements, [article1, article2]);
    464 
    465    titleHeading.ariaFlowToElements = [article1, article2];
    466    assert_equals(titleHeading.getAttribute("aria-flowto"), "");
    467    assert_array_equals(titleHeading.ariaFlowToElements, [article1, article2]);
    468 
    469    titleHeading.ariaFlowToElements = [];
    470    assert_equals(titleHeading.getAttribute("aria-flowto"), "");
    471 
    472    titleHeading.setAttribute("aria-flowto", "article1");
    473    assert_array_equals(titleHeading.ariaFlowToElements, [article1]);
    474 
    475    titleHeading.removeAttribute("aria-flowto");
    476    assert_equals(titleHeading.ariaFlowToElements, null);
    477 
    478    titleHeading.ariaFlowToElements = [article1, article2];
    479    assert_equals(titleHeading.getAttribute("aria-flowto"), "");
    480    assert_array_equals(titleHeading.ariaFlowToElements, [article1, article2]);
    481 
    482    titleHeading.ariaFlowToElements = null;
    483    assert_false(titleHeading.hasAttribute("aria-flowto", "Nullifying the IDL attribute should remove the content attribute."));
    484  }, "aria-flowto.");
    485  </script>
    486 
    487  <ul>
    488    <li id="listItemOwner" aria-owns="child1 child2">Parent</li>
    489  </ul>
    490  <ul>
    491    <li id="child1">Child 1</li>
    492    <li id="child2">Child 2</li>
    493  </ul>
    494  <script>
    495  test(function(t) {
    496    assert_array_equals(listItemOwner.ariaOwnsElements, [child1, child2]);
    497 
    498    listItemOwner.removeAttribute("aria-owns");
    499    assert_equals(listItemOwner.ariaOwnsElements, null);
    500 
    501    listItemOwner.ariaOwnsElements = [child1, child2];
    502    assert_equals(listItemOwner.getAttribute("aria-owns"), "");
    503    assert_array_equals(listItemOwner.ariaOwnsElements, [child1, child2]);
    504 
    505    listItemOwner.ariaOwnsElements = [];
    506    assert_equals(listItemOwner.getAttribute("aria-owns"), "");
    507 
    508    listItemOwner.setAttribute("aria-owns", "child1");
    509    assert_array_equals(listItemOwner.ariaOwnsElements, [child1]);
    510 
    511    listItemOwner.ariaOwnsElements = [child1, child2];
    512    assert_equals(listItemOwner.getAttribute("aria-owns"), "");
    513    assert_array_equals(listItemOwner.ariaOwnsElements, [child1, child2]);
    514 
    515    listItemOwner.ariaOwnsElements = null;
    516    assert_false(listItemOwner.hasAttribute("aria-owns", "Nullifying the IDL attribute should remove the content attribute."));
    517  }, "aria-owns.");
    518  </script>
    519 
    520  <div id="lightDomContainer">
    521    <h2 id="lightDomHeading" aria-flowto="shadowChild1 shadowChild2">Light DOM Heading</h2>
    522    <div id="host"></div>
    523    <p id="lightDomText1">Light DOM text</p>
    524    <p id="lightDomText2">Light DOM text</p>
    525  </div>
    526 
    527  <script>
    528  test(function(t) {
    529    const shadowRoot = host.attachShadow({mode: "open"});
    530    const shadowChild1 = document.createElement("article");
    531    shadowChild1.setAttribute("id", "shadowChild1");
    532    shadowRoot.appendChild(shadowChild1);
    533    const shadowChild2 = document.createElement("article");
    534    shadowChild2.setAttribute("id", "shadowChild1");
    535    shadowRoot.appendChild(shadowChild2);
    536 
    537    // The elements in the content attribute are in a "darker" tree - they
    538    // enter a shadow encapsulation boundary, so not be associated any more.
    539    assert_array_equals(lightDomHeading.ariaFlowToElements, []);
    540 
    541    // These elements are in a shadow including ancestor, i.e "lighter" tree.
    542    // Valid for the IDL attribute, but content attribute should be null.
    543    shadowChild1.ariaFlowToElements = [lightDomText1, lightDomText2];
    544    assert_equals(shadowChild1.getAttribute("aria-flowto"), "", "empty content attribute for elements that cross shadow boundaries.");
    545 
    546    // These IDs belong to a different scope, so the attr-associated-element
    547    // cannot be computed.
    548    shadowChild2.setAttribute("aria-flowto", "lightDomText1 lightDomText2");
    549    assert_array_equals(shadowChild2.ariaFlowToElements, []);
    550 
    551    // Elements that cross into shadow DOM are dropped, only reflect the valid
    552    // elements in IDL and in the content attribute.
    553    lightDomHeading.ariaFlowToElements = [shadowChild1, shadowChild2, lightDomText1, lightDomText2];
    554    assert_array_equals(lightDomHeading.ariaFlowToElements, [lightDomText1, lightDomText2], "IDL should only include valid elements");
    555    assert_equals(lightDomHeading.getAttribute("aria-flowto"), "", "empty content attribute if any given elements cross shadow boundaries");
    556 
    557    // Using a mixture of elements in the same scope and in a shadow including
    558    // ancestor should set the IDL attribute, but should reflect the empty
    559    // string in the content attribute.
    560    shadowChild1.removeAttribute("aria-flowto");
    561    shadowChild1.ariaFlowToElements = [shadowChild1, lightDomText1];
    562    assert_equals(shadowChild1.getAttribute("aria-flowto"), "", "Setting IDL elements with a mix of scopes should reflect an empty string in the content attribute")
    563 
    564  }, "shadow DOM behaviour for FrozenArray element reflection.");
    565  </script>
    566 
    567  <div id="describedButtonContainer">
    568    <div id="buttonDescription1">Delicious</div>
    569    <div id="buttonDescription2">Nutritious</div>
    570    <div id="outerShadowHost"></div>
    571  </div>
    572 
    573  <script>
    574  test(function(t) {
    575    const description1 = document.getElementById("buttonDescription1");
    576    const description2 = document.getElementById("buttonDescription2");
    577    const outerShadowRoot = outerShadowHost.attachShadow({mode: "open"});
    578    const innerShadowHost = document.createElement("div");
    579    outerShadowRoot.appendChild(innerShadowHost);
    580    const innerShadowRoot = innerShadowHost.attachShadow({mode: "open"});
    581 
    582    // Create an element, add some attr associated light DOM elements and append it to the outer shadow root.
    583    const describedElement = document.createElement("button");
    584    describedButtonContainer.appendChild(describedElement);
    585    describedElement.ariaDescribedByElements = [description1, description2];
    586 
    587    // All elements were in the same scope, so elements are gettable and the content attribute is empty.
    588    assert_array_equals(describedElement.ariaDescribedByElements, [description1, description2], "same scope reference");
    589    assert_equals(describedElement.getAttribute("aria-describedby"), "");
    590 
    591    outerShadowRoot.appendChild(describedElement);
    592 
    593    // Explicitly set attr-associated-elements should still be gettable because we are referencing elements in a lighter scope.
    594    // The content attr is empty.
    595    assert_array_equals(describedElement.ariaDescribedByElements, [description1, description2], "lighter scope reference");
    596    assert_equals(describedElement.getAttribute("aria-describedby"), "");
    597 
    598    // Move the explicitly set elements into a deeper shadow DOM to test the relationship should not be gettable.
    599    innerShadowRoot.appendChild(description1);
    600    innerShadowRoot.appendChild(description2);
    601 
    602    // Explicitly set elements are no longer retrievable, because they are no longer in a valid scope.
    603    assert_array_equals(describedElement.ariaDescribedByElements, [], "invalid scope reference");
    604    assert_equals(describedElement.getAttribute("aria-describedby"), "");
    605 
    606    // Move into the same shadow scope as the explicitly set elements to test that the elements are gettable.
    607    innerShadowRoot.appendChild(describedElement);
    608    assert_array_equals(describedElement.ariaDescribedByElements, [description1, description2], "restored valid scope reference");
    609    assert_equals(describedElement.getAttribute("aria-describedby"), "");
    610  }, "Moving explicitly set elements across shadow DOM boundaries.");
    611  </script>
    612 
    613  <div id="sameScopeContainer">
    614    <div id="labelledby" aria-labelledby="headingLabel1 headingLabel2">Misspelling</div>
    615    <div id="headingLabel1">Wonderful</div>
    616    <div id="headingLabel2">Fantastic</div>
    617 
    618    <div id="headingShadowHost"></div>
    619  </div>
    620 
    621    <script>
    622    test(function(t) {
    623      const shadowRoot = headingShadowHost.attachShadow({mode: "open"});
    624      const headingElement = document.createElement("h1");
    625      const headingLabel1 = document.getElementById("headingLabel1")
    626      const headingLabel2 = document.getElementById("headingLabel2")
    627      shadowRoot.appendChild(headingElement);
    628 
    629      assert_array_equals(labelledby.ariaLabelledByElements, [headingLabel1, headingLabel2], "aria-labelledby is supported by IDL getter.");
    630 
    631      // Explicitly set elements are in a lighter shadow DOM, so that's ok.
    632      headingElement.ariaLabelledByElements = [headingLabel1, headingLabel2];
    633      assert_array_equals(headingElement.ariaLabelledByElements, [headingLabel1, headingLabel2], "Lighter elements are gettable when explicitly set.");
    634      assert_equals(headingElement.getAttribute("aria-labelledby"), "");
    635 
    636      // Move into Light DOM, explicitly set elements should still be gettable.
    637      // Note that the content attribute is still empty.
    638      sameScopeContainer.appendChild(headingElement);
    639      assert_array_equals(headingElement.ariaLabelledByElements, [headingLabel1, headingLabel2], "Elements are all in same scope, so gettable.");
    640      assert_equals(headingElement.getAttribute("aria-labelledby"), "", "Content attribute is empty.");
    641 
    642      // Reset the association, the content attribute is sitll empty.
    643      headingElement.ariaLabelledByElements = [headingLabel1, headingLabel2];
    644      assert_equals(headingElement.getAttribute("aria-labelledby"), "");
    645 
    646      // Remove the referring element from the DOM, elements are no longer longer exposed,
    647      // underlying internal reference is still kept intact.
    648      headingElement.remove();
    649      assert_array_equals(headingElement.ariaLabelledByElements, [], "Element is no longer in the document, so references should no longer be exposed.");
    650      assert_equals(headingElement.getAttribute("aria-labelledby"), "");
    651 
    652      // Insert it back in.
    653      sameScopeContainer.appendChild(headingElement);
    654      assert_array_equals(headingElement.ariaLabelledByElements, [headingLabel1, headingLabel2], "Element is restored to valid scope, so should be gettable.");
    655      assert_equals(headingElement.getAttribute("aria-labelledby"), "");
    656 
    657      // Remove everything from the DOM, nothing is exposed again.
    658      headingLabel1.remove();
    659      headingLabel2.remove();
    660      assert_array_equals(headingElement.ariaLabelledByElements, []);
    661      assert_equals(headingElement.getAttribute("aria-labelledby"), "");
    662      assert_equals(document.getElementById("headingLabel1"), null);
    663      assert_equals(document.getElementById("headingLabel2"), null);
    664 
    665      // Reset the association.
    666      headingElement.ariaLabelledByElements = [headingLabel1, headingLabel2];
    667      assert_array_equals(headingElement.ariaLabelledByElements, []);
    668      assert_equals(headingElement.getAttribute("aria-labelledby"), "");
    669    }, "Moving explicitly set elements around within the same scope, and removing from the DOM.");
    670    </script>
    671 
    672  <input id="input">
    673    <optgroup>
    674      <option id="first">First option</option>
    675      <option id="second">Second option</option>
    676    </optgroup>
    677 
    678  <script>
    679    test(function(t) {
    680      input.ariaActiveDescendantElement = first;
    681      first.parentElement.appendChild(first);
    682 
    683      assert_equals(input.ariaActiveDescendantElement, first);
    684    }, "Reparenting.");
    685  </script>
    686 
    687  <div id='fromDiv'></div>
    688 
    689  <script>
    690    test(function(t) {
    691      const toSpan = document.createElement('span');
    692      toSpan.setAttribute("id", "toSpan");
    693      fromDiv.ariaActiveDescendantElement = toSpan;
    694 
    695      assert_equals(fromDiv.ariaActiveDescendantElement, null, "Referenced element not inserted into document, so is in an invalid scope.");
    696      assert_equals(fromDiv.getAttribute("aria-activedescendant"), "", "Invalid scope, so content attribute not set.");
    697 
    698      fromDiv.appendChild(toSpan);
    699      assert_equals(fromDiv.ariaActiveDescendantElement, toSpan, "Referenced element now inserted into the document.");
    700      assert_equals(fromDiv.getAttribute("aria-activedescendant"), "", "Content attribute remains empty, as it is only updated at set time.");
    701 
    702    }, "Attaching element reference before it's inserted into the DOM.");
    703  </script>
    704 
    705  <div id='originalDocumentDiv'></div>
    706 
    707  <script>
    708    test(function(t) {
    709      const newDoc = document.implementation.createHTMLDocument('new document');
    710      const newDocSpan = newDoc.createElement('span');
    711      newDoc.body.appendChild(newDocSpan);
    712 
    713      // Create a reference across documents.
    714      originalDocumentDiv.ariaActiveDescendantElement = newDocSpan;
    715 
    716      assert_equals(originalDocumentDiv.ariaActiveDescendantElement, null, "Cross-document is an invalid scope, so reference will not be visible.");
    717      assert_equals(fromDiv.getAttribute("aria-activedescendant"), "", "Invalid scope when set, so content attribute not set.");
    718 
    719      // "Move" span to first document.
    720      originalDocumentDiv.appendChild(newDocSpan);
    721 
    722      // Implementation defined: moving object into same document from other document may cause reference to become visible.
    723      assert_equals(originalDocumentDiv.ariaActiveDescendantElement, newDocSpan, "Implementation defined: moving object back *may* make reference visible.");
    724      assert_equals(fromDiv.getAttribute("aria-activedescendant"), "", "Invalid scope when set, so content attribute not set.");
    725    }, "Cross-document references and moves.");
    726  </script>
    727 
    728 
    729  <script>
    730    test(function(t) {
    731      const otherDoc = document.implementation.createHTMLDocument('otherDoc');
    732      const otherDocDiv = otherDoc.createElement('div');
    733      const otherDocSpan = otherDoc.createElement('span');
    734      otherDocDiv.appendChild(otherDocSpan);
    735      otherDoc.body.appendChild(otherDocDiv);
    736 
    737      otherDocDiv.ariaActiveDescendantElement = otherDocSpan;
    738      assert_equals(otherDocDiv.ariaActiveDescendantElement, otherDocSpan, "Setting reference on a different document.");
    739 
    740      // Adopt element from other oducment.
    741      document.body.appendChild(document.adoptNode(otherDocDiv));
    742      assert_equals(otherDocDiv.ariaActiveDescendantElement, otherDocSpan, "Reference should be kept on the new document too.");
    743    }, "Adopting element keeps references.");
    744  </script>
    745 
    746  <div id="cachingInvariantMain"></div>
    747  <div id="cachingInvariantElement1"></div>
    748  <div id="cachingInvariantElement2"></div>
    749  <div id="cachingInvariantElement3"></div>
    750  <div id="cachingInvariantElement4"></div>
    751  <div id="cachingInvariantElement5"></div>
    752 
    753  <script>
    754    test(function(t) {
    755      cachingInvariantMain.ariaControlsElements = [cachingInvariantElement1, cachingInvariantElement2];
    756      cachingInvariantMain.ariaDescribedByElements = [cachingInvariantElement3, cachingInvariantElement4];
    757      cachingInvariantMain.ariaDetailsElements = [cachingInvariantElement5];
    758      cachingInvariantMain.ariaFlowToElements = [cachingInvariantElement1, cachingInvariantElement3];
    759      cachingInvariantMain.ariaLabelledByElements = [cachingInvariantElement2, cachingInvariantElement4];
    760      cachingInvariantMain.ariaOwnsElements = [cachingInvariantElement1, cachingInvariantElement2, cachingInvariantElement3];
    761 
    762      let ariaControlsElementsArray = cachingInvariantMain.ariaControlsElements;
    763      let ariaDescribedByElementsArray = cachingInvariantMain.ariaDescribedByElements;
    764      let ariaDetailsElementsArray = cachingInvariantMain.ariaDetailsElements;
    765      let ariaFlowToElementsArray = cachingInvariantMain.ariaFlowToElements;
    766      let ariaLabelledByElementsArray = cachingInvariantMain.ariaLabelledByElements;
    767      let ariaOwnsElementsArray = cachingInvariantMain.ariaOwnsElements;
    768 
    769      assert_equals(ariaControlsElementsArray, cachingInvariantMain.ariaControlsElements, "Caching invariant for ariaControlsElements");
    770      assert_equals(ariaDescribedByElementsArray, cachingInvariantMain.ariaDescribedByElements, "Caching invariant for ariaDescribedByElements");
    771      assert_equals(ariaDetailsElementsArray, cachingInvariantMain.ariaDetailsElements, "Caching invariant for ariaDetailsElements");
    772      assert_equals(ariaFlowToElementsArray, cachingInvariantMain.ariaFlowToElements, "Caching invariant for ariaFlowToElements");
    773      assert_equals(ariaLabelledByElementsArray, cachingInvariantMain.ariaLabelledByElements, "Caching invariant for ariaLabelledByElements");
    774      assert_equals(ariaOwnsElementsArray, cachingInvariantMain.ariaOwnsElements, "Caching invariant for ariaOwnsElements");
    775 
    776      // Ensure that stale values don't continue to be cached
    777      cachingInvariantMain.ariaControlsElements = [cachingInvariantElement4, cachingInvariantElement5];
    778      cachingInvariantMain.ariaDescribedByElements = [cachingInvariantElement1, cachingInvariantElement2];
    779      cachingInvariantMain.ariaDetailsElements = [cachingInvariantElement3];
    780      cachingInvariantMain.ariaFlowToElements = [cachingInvariantElement4, cachingInvariantElement5];
    781      cachingInvariantMain.ariaLabelledByElements = [cachingInvariantElement1, cachingInvariantElement2];
    782      cachingInvariantMain.ariaOwnsElements = [cachingInvariantElement3, cachingInvariantElement4, cachingInvariantElement1];
    783 
    784      ariaControlsElementsArray = cachingInvariantMain.ariaControlsElements;
    785      ariaDescribedByElementsArray = cachingInvariantMain.ariaDescribedByElements;
    786      ariaDetailsElementsArray = cachingInvariantMain.ariaDetailsElements;
    787      ariaFlowToElementsArray = cachingInvariantMain.ariaFlowToElements;
    788      ariaLabelledByElementsArray = cachingInvariantMain.ariaLabelledByElements;
    789      ariaOwnsElementsArray = cachingInvariantMain.ariaOwnsElements;
    790 
    791      assert_equals(ariaControlsElementsArray, cachingInvariantMain.ariaControlsElements, "Caching invariant for ariaControlsElements");
    792      assert_equals(ariaDescribedByElementsArray, cachingInvariantMain.ariaDescribedByElements, "Caching invariant for ariaDescribedByElements");
    793      assert_equals(ariaDetailsElementsArray, cachingInvariantMain.ariaDetailsElements, "Caching invariant for ariaDetailsElements");
    794      assert_equals(ariaFlowToElementsArray, cachingInvariantMain.ariaFlowToElements, "Caching invariant for ariaFlowToElements");
    795      assert_equals(ariaLabelledByElementsArray, cachingInvariantMain.ariaLabelledByElements, "Caching invariant for ariaLabelledByElements");
    796      assert_equals(ariaOwnsElementsArray, cachingInvariantMain.ariaOwnsElements, "Caching invariant for ariaOwnsElements");
    797 
    798    }, "Caching invariant different attributes.");
    799  </script>
    800 
    801  <div id="cachingInvariantMain1"></div>
    802  <div id="cachingInvariantMain2"></div>
    803 
    804  <script>
    805    test(function(t) {
    806      cachingInvariantMain1.ariaDescribedByElements = [cachingInvariantElement1, cachingInvariantElement2];
    807      cachingInvariantMain2.ariaDescribedByElements = [cachingInvariantElement3, cachingInvariantElement4];
    808 
    809      let ariaDescribedByElementsArray1 = cachingInvariantMain1.ariaDescribedByElements;
    810      let ariaDescribedByElementsArray2 = cachingInvariantMain2.ariaDescribedByElements;
    811 
    812      assert_equals(ariaDescribedByElementsArray1, cachingInvariantMain1.ariaDescribedByElements, "Caching invariant for ariaDescribedByElements in one elemnt");
    813      assert_equals(ariaDescribedByElementsArray2, cachingInvariantMain2.ariaDescribedByElements, "Caching invariant for ariaDescribedByElements in onother elemnt");
    814    }, "Caching invariant different elements.");
    815  </script>
    816 
    817  <div id="badInputValues"></div>
    818  <div id="badInputValues2"></div>
    819 
    820  <script>
    821    test(function(t) {
    822      assert_throws_js(TypeError, () => { badInputValues.ariaActiveDescendantElement = "a string"; });
    823      assert_throws_js(TypeError, () => { badInputValues.ariaActiveDescendantElement = 1; });
    824      assert_throws_js(TypeError, () => { badInputValues.ariaActiveDescendantElement = [ badInputValues2 ]; });
    825 
    826      assert_throws_js(TypeError, () => { badInputValues.ariaControlsElements = "a string" });
    827      assert_throws_js(TypeError, () => { badInputValues.ariaControlsElements = 1 });
    828      assert_throws_js(TypeError, () => { badInputValues.ariaControlsElements = [1, 2, 3] });
    829      assert_throws_js(TypeError, () => { badInputValues.ariaControlsElements = badInputValues2 });
    830    }, "Passing values of the wrong type should throw a TypeError");
    831  </script>
    832 
    833  <!-- TODO(chrishall): add additional GC test covering:
    834       if an element is in an invalid scope but attached to the document, it's
    835       not GC'd;
    836  -->
    837 
    838  <!-- TODO(chrishall): add additional GC test covering:
    839       if an element is not attached to the document, but is in a tree fragment
    840       which is not GC'd because there is a script reference to another element
    841       in the tree fragment, and the relationship is valid because it is between
    842       two elements in that tree fragment, the relationship is exposed *and* the
    843       element is not GC'd
    844  -->
    845 
    846 </html>