Pārlūkot izejas kodu

LibWeb: Compute accessible names for hidden/hidden-but-referenced nodes

This change implements full support for the “A. Hidden Not Referenced”
step at https://w3c.github.io/accname/#step2A in the “Accessible Name
and Description Computation” spec — including handling all hidden nodes
that must be ignored, as well as handling hidden nodes that, for the
purposes of accessible-name computation, must not be ignored (due to
having aria-labelledby/aria-describedby references from other nodes).

Otherwise, without this change, not all cases of hidden nodes get
ignored as expected, while cases of nodes that are hidden but that have
aria-labelledby/aria-describedby references from other nodes get
unexpectedly ignored.
sideshowbarker 8 mēneši atpakaļ
vecāks
revīzija
314e5d6bb7

+ 48 - 0
Libraries/LibWeb/DOM/Element.cpp

@@ -1874,6 +1874,54 @@ void Element::invalidate_style_after_attribute_change(FlyString const& attribute
     invalidate_style(StyleInvalidationReason::ElementAttributeChange);
 }
 
+bool Element::is_hidden() const
+{
+    if (layout_node() == nullptr)
+        return true;
+    if (layout_node()->computed_values().visibility() == CSS::Visibility::Hidden || layout_node()->computed_values().visibility() == CSS::Visibility::Collapse || layout_node()->computed_values().content_visibility() == CSS::ContentVisibility::Hidden)
+        return true;
+    for (ParentNode const* self_or_ancestor = this; self_or_ancestor; self_or_ancestor = self_or_ancestor->parent_or_shadow_host()) {
+        if (self_or_ancestor->is_element() && static_cast<DOM::Element const*>(self_or_ancestor)->aria_hidden() == "true")
+            return true;
+    }
+    return false;
+}
+
+bool Element::has_hidden_ancestor() const
+{
+    for (ParentNode const* self_or_ancestor = this; self_or_ancestor; self_or_ancestor = self_or_ancestor->parent_or_shadow_host()) {
+        if (self_or_ancestor->is_element() && static_cast<DOM::Element const*>(self_or_ancestor)->is_hidden())
+            return true;
+    }
+    return false;
+}
+
+bool Element::is_referenced() const
+{
+    bool is_referenced = false;
+    if (id().has_value()) {
+        root().for_each_in_subtree_of_type<HTML::HTMLElement>([&](auto& element) {
+            auto aria_data = MUST(Web::ARIA::AriaData::build_data(element));
+            if (aria_data->aria_labelled_by_or_default().contains_slow(id().value())) {
+                is_referenced = true;
+                return TraversalDecision::Break;
+            }
+            return TraversalDecision::Continue;
+        });
+    }
+    return is_referenced;
+}
+
+bool Element::has_referenced_and_hidden_ancestor() const
+{
+    for (auto const* ancestor = parent_or_shadow_host(); ancestor; ancestor = ancestor->parent_or_shadow_host()) {
+        if (ancestor->is_element())
+            if (auto const* element = static_cast<DOM::Element const*>(ancestor); element->is_referenced() && element->is_hidden())
+                return true;
+    }
+    return false;
+}
+
 // https://www.w3.org/TR/wai-aria-1.2/#tree_exclusion
 bool Element::exclude_from_accessibility_tree() const
 {

+ 6 - 0
Libraries/LibWeb/DOM/Element.h

@@ -348,6 +348,12 @@ public:
 
     virtual bool include_in_accessibility_tree() const override;
 
+    bool is_hidden() const;
+    bool has_hidden_ancestor() const;
+
+    bool is_referenced() const;
+    bool has_referenced_and_hidden_ancestor() const;
+
     void enqueue_a_custom_element_upgrade_reaction(HTML::CustomElementDefinition& custom_element_definition);
     void enqueue_a_custom_element_callback_reaction(FlyString const& callback_name, GC::MarkedVector<JS::Value> arguments);
 

+ 46 - 21
Libraries/LibWeb/DOM/Node.cpp

@@ -2216,23 +2216,26 @@ ErrorOr<String> Node::name_or_description(NameOrDescription target, Document con
     if (is_element()) {
         auto const* element = static_cast<DOM::Element const*>(this);
         auto role = element->role_or_default();
-        bool is_referenced = false;
-        auto id = element->id();
-        if (id.has_value()) {
-            this->root().for_each_in_inclusive_subtree_of_type<HTML::HTMLElement>([&](auto& element) {
-                auto aria_data = MUST(Web::ARIA::AriaData::build_data(element));
-                if (aria_data->aria_labelled_by_or_default().contains_slow(id.value())) {
-                    is_referenced = true;
-                    return TraversalDecision::Break;
-                }
-                return TraversalDecision::Continue;
-            });
-        }
         // 2. Compute the text alternative for the current node:
-        // A. If the current node is hidden and is not directly referenced by aria-labelledby or aria-describedby, nor directly referenced by a native host language text alternative element (e.g. label in HTML) or attribute, return the empty string.
-        // FIXME: Check for references
-        if (element->aria_hidden() == "true")
-            return String {};
+
+        // A. Hidden Not Referenced: If the current node is hidden and is:
+        // i. Not part of an aria-labelledby or aria-describedby traversal, where the node directly referenced by that
+        // relation was hidden.
+        // ii. Nor part of a native host language text alternative element (e.g. label in HTML) or attribute traversal,
+        // where the root of that traversal was hidden.
+        // Return the empty string.
+        // NOTE: Nodes with CSS properties display:none, visibility:hidden, visibility:collapse or
+        // content-visibility:hidden: They are considered hidden, as they match the guidelines "not perceivable" and
+        // "explicitly hidden".
+        //
+        // AD-HOC: We don’t implement this step here — because strictly implementing this would cause us to return early
+        // whenever encountering a node (element, actually) that “is hidden and is not directly referenced by
+        // aria-labelledby or aria-describedby”, without traversing down through that element’s subtree to see if it has
+        // (1) any descendant elements that are directly referenced and/or (2) any un-hidden nodes. So we instead (in
+        // substep G below) traverse upward through ancestor nodes of every text node, and check in that way to do the
+        // equivalent of what this step seems to have been intended to do.
+        // https://github.com/w3c/aria/issues/2387
+
         // B. Otherwise:
         // - if computing a name, and the current node has an aria-labelledby attribute that contains at least one valid IDREF, and the current node is not already part of an aria-labelledby traversal,
         //   process its IDREFs in the order they occur:
@@ -2260,6 +2263,7 @@ ErrorOr<String> Node::name_or_description(NameOrDescription target, Document con
                 // AD-HOC: The “For each IDREF” substep in the spec doesn’t seem to explicitly require the following
                 // check for an aria-label value; but the “div group explicitly labelledby self and heading” subtest at
                 // https://wpt.fyi/results/accname/name/comp_labelledby.html won’t pass unless we do this check.
+                // https://github.com/w3c/aria/issues/2388
                 if (target == NameOrDescription::Name && node->aria_label().has_value() && !node->aria_label()->is_empty() && !node->aria_label()->bytes_as_string_view().is_whitespace()) {
                     total_accumulated_text.append(' ');
                     total_accumulated_text.append(node->aria_label().value());
@@ -2275,6 +2279,13 @@ ErrorOr<String> Node::name_or_description(NameOrDescription target, Document con
                 total_accumulated_text.append(result);
             }
             // iii. Return the accumulated text.
+            // AD-HOC: This substep in the spec doesn’t seem to explicitly require the following check for an aria-label
+            // value; but the “button's hidden referenced name (visibility:hidden) with hidden aria-labelledby traversal
+            // falls back to aria-label” subtest at https://wpt.fyi/results/accname/name/comp_labelledby.html won’t pass
+            // unless we do this check.
+            // https://github.com/w3c/aria/issues/2388
+            if (total_accumulated_text.string_view().is_whitespace() && target == NameOrDescription::Name && element->aria_label().has_value() && !element->aria_label()->is_empty() && !element->aria_label()->bytes_as_string_view().is_whitespace())
+                return element->aria_label().release_value();
             return total_accumulated_text.to_string();
         }
         // C. Embedded Control: Otherwise, if the current node is a control embedded
@@ -2297,6 +2308,7 @@ ErrorOr<String> Node::name_or_description(NameOrDescription target, Document con
                     // it ancestor <label> must instead be skipped and not included. The HTML-AAM spec seems to maybe
                     // be trying to achieve that result by expressing specific steps for each particular type of form
                     // control. But what all that reduces/optimizes/simplifies down to is just, “skip over self”.
+                    // https://github.com/w3c/aria/issues/2389
                     if (node == this)
                         continue;
                     if (node->is_element()) {
@@ -2427,8 +2439,10 @@ ErrorOr<String> Node::name_or_description(NameOrDescription target, Document con
                     return alt.release_value();
         }
 
-        // F. Otherwise, if the current node's role allows name from content, or if the current node is referenced by aria-labelledby, aria-describedby, or is a native host language text alternative element (e.g. label in HTML), or is a descendant of a native host language text alternative element:
-        if ((role.has_value() && ARIA::allows_name_from_content(role.value())) || is_referenced || is_descendant == IsDescendant::Yes) {
+        // F. Name From Content: Otherwise, if the current node's role allows name from content, or if the current node
+        // is referenced by aria-labelledby, aria-describedby, or is a native host language text alternative element
+        // (e.g. label in HTML), or is a descendant of a native host language text alternative element:
+        if ((role.has_value() && ARIA::allows_name_from_content(role.value())) || element->is_referenced() || is_descendant == IsDescendant::Yes) {
             // i. Set the accumulated text to the empty string.
             total_accumulated_text.clear();
             // ii. Name From Generated Content: Check for CSS generated textual content associated with the current node and include
@@ -2500,13 +2514,24 @@ ErrorOr<String> Node::name_or_description(NameOrDescription target, Document con
     }
 
     // G. Text Node: Otherwise, if the current node is a Text Node, return its textual contents.
-    if (is_text()) {
+    // AD-HOC: The spec doesn’t require ascending through the parent node and ancestor nodes of every text node we
+    // reach — the way we’re doing there. But we implement it this way because the spec algorithm as written doesn’t
+    // appear to achieve what it seems to be intended to achieve. Specifically, the spec algorithm as written doesn’t
+    // cause traversal through element subtrees in way that’s necessary to check for descendants that are referenced by
+    // aria-labelledby or aria-describedby and/or un-hidden. See the comment for substep A above.
+    if (is_text() && (!parent_element() || (parent_element()->is_referenced() || !parent_element()->is_hidden() || !parent_element()->has_hidden_ancestor() || parent_element()->has_referenced_and_hidden_ancestor()))) {
         if (layout_node() && layout_node()->is_text_node())
             return verify_cast<Layout::TextNode>(layout_node())->text_for_rendering();
-        return text_content().value();
+        return text_content().release_value();
     }
 
-    // TODO: H. Otherwise, if the current node is a descendant of an element whose Accessible Name or Accessible Description is being computed, and contains descendants, proceed to 2F.i.
+    // H. Otherwise, if the current node is a descendant of an element whose Accessible Name or Accessible Description
+    // is being computed, and contains descendants, proceed to 2F.i.
+    // AD-HOC: We don’t implement this step here — because is essentially unreachable code in the spec algorithm.
+    // We could never get here without descending through every subtree of an element whose Accessible Name or
+    // Accessible Description is being computed. And in our implementation of substep F about, we’re anyway already
+    // recursively descending through all the child nodes of every element whose Accessible Name or Accessible
+    // Description is being computed, in a way that never leads to this substep H every being hit.
 
     // I. Otherwise, if the current node has a Tooltip attribute, return its value.
     // https://www.w3.org/TR/accname-1.2/#dfn-tooltip-attribute

+ 15 - 0
Tests/LibWeb/Text/expected/wpt-import/accname/name/comp_hidden_not_referenced.txt

@@ -0,0 +1,15 @@
+Summary
+
+Harness status: OK
+
+Rerun
+
+Found 5 tests
+
+5 Pass
+Details
+Result	Test Name	MessagePass	button containing a rendered, unreferenced element that is aria-hidden=true, an unreferenced element with the hidden host language attribute, and an unreferenced element that is unconditionally rendered	
+Pass	button labelled by element that is aria-hidden=true	
+Pass	button labelled by element with the hidden host language attribute	
+Pass	link labelled by elements with assorted visibility and a11y tree exposure	
+Pass	heading with name from content, containing element that is visibility:hidden with nested content that is visibility:visible	

+ 37 - 0
Tests/LibWeb/Text/expected/wpt-import/accname/name/comp_labelledby_hidden_nodes.txt

@@ -0,0 +1,37 @@
+Summary
+
+Harness status: OK
+
+Rerun
+
+Found 27 tests
+
+27 Pass
+Details
+Result	Test Name	MessagePass	button with aria-labelledby using display:none hidden span (with nested span)	
+Pass	button with aria-labelledby using display:none hidden span (with nested spans, depth 2)	
+Pass	button with aria-labelledby using span without display:none (with nested display:none spans, depth 2)	
+Pass	button with aria-labelledby using display:none hidden span (with nested sibling spans)	
+Pass	button with aria-labelledby using span without display:none (with nested display:none sibling spans)	
+Pass	button with aria-labelledby using span with display:none (with nested display:inline sibling spans)	
+Pass	button with aria-labelledby using visibility:hidden span (with nested span)	
+Pass	button with aria-labelledby using visibility:hidden span (with nested spans, depth 2)	
+Pass	button with aria-labelledby using span without visibility:hidden (with nested visibility:hidden spans, depth 2)	
+Pass	button with aria-labelledby using visibility:hidden hidden span (with nested sibling spans)	
+Pass	button with aria-labelledby using span without visibility:hidden (with nested visibility:hidden sibling spans)	
+Pass	button with aria-labelledby using span with visibility:hidden (with nested visibility:visible sibling spans)	
+Pass	button with aria-labelledby using visibility:collapse span (with nested span)	
+Pass	button with aria-labelledby using visibility:collapse span (with nested spans, depth 2)	
+Pass	button with aria-labelledby using span without visibility:collapse (with nested visibility:visible spans, depth 2)	
+Pass	button with aria-labelledby using visibility:collapse span (with nested sibling spans)	
+Pass	button with aria-labelledby using span without visibility:collapse (with nested visibility:collapse sibling spans)	
+Pass	button with aria-labelledby using span with visibility:collapse (with nested visible sibling spans)	
+Pass	button with aria-labelledby using aria-hidden span (with nested span)	
+Pass	button with aria-labelledby using aria-hidden span (with nested spans, depth 2)	
+Pass	button with aria-labelledby using span without aria-hidden (with nested aria-hidden spans, depth 2)	
+Pass	button with aria-labelledby using aria-hidden hidden span (with nested sibling spans)	
+Pass	button with aria-labelledby using HTML5 hidden span (with nested span)	
+Pass	button with aria-labelledby using HTML5 hidden span (with nested spans, depth 2)	
+Pass	button with aria-labelledby using span without HTML5 hidden (with nested HTML5 hidden spans, depth 2)	
+Pass	button with aria-labelledby using HTML5 hidden span (with nested hidden sibling spans)	
+Pass	button with aria-labelledby using span without HTML5 hidden (with nested hidden sibling spans)	

+ 92 - 0
Tests/LibWeb/Text/input/wpt-import/accname/name/comp_hidden_not_referenced.html

@@ -0,0 +1,92 @@
+<!doctype html>
+<html>
+<head>
+  <meta charset="utf-8">
+  <title>Name Comp: Hidden Not Referenced</title>
+  <script src="../../resources/testharness.js"></script>
+  <script src="../../resources/testharnessreport.js"></script>
+  <script src="../../resources/testdriver.js"></script>
+  <script src="../../resources/testdriver-vendor.js"></script>
+  <script src="../../resources/testdriver-actions.js"></script>
+  <script src="../../wai-aria/scripts/aria-utils.js"></script>
+</head>
+<body>
+
+<p>Tests the <a href="https://w3c.github.io/accname/#comp_hidden_not_referenced">#comp_hidden_not_referenced</a> portions of the AccName <em>Name Computation</em> algorithm.</p>
+
+<button
+  class="ex"
+  data-expectedlabel="visible to all users"
+  data-testname="button containing a rendered, unreferenced element that is aria-hidden=true, an unreferenced element with the hidden host language attribute, and an unreferenced element that is unconditionally rendered"
+>
+  <span aria-hidden="true">hidden,</span>
+  <span hidden>hidden from all users,</span>
+  <span>visible to all users</span>
+</button>
+
+<button
+  class="ex"
+  data-expectedlabel="hidden but referenced,"
+  data-testname="button labelled by element that is aria-hidden=true"
+  aria-labelledby="button-label-2"
+>
+  <span aria-hidden="true" id="button-label-2">hidden but referenced,</span>
+  <span hidden>hidden from all users,</span>
+  <span>visible to all users</span>
+</button>
+
+<button
+  class="ex"
+  data-expectedlabel="hidden from all users but referenced,"
+  data-testname="button labelled by element with the hidden host language attribute"
+  aria-labelledby="button-label-3"
+>
+  <span aria-hidden="true">hidden,</span>
+  <span hidden id="button-label-3">hidden from all users but referenced,</span>
+  <span>visible to all users</span>
+</button>
+
+<a
+  class="ex"
+  data-testname="link labelled by elements with assorted visibility and a11y tree exposure"
+  data-expectedlabel="visible to all users, hidden but referenced, hidden from all users but referenced"
+  href="#"
+  aria-labelledby="link-label-1a link-label-1b link-label-1c"
+>
+  <span id="link-label-1a">
+    <span>visible to all users,</span>
+    <span aria-hidden="true">hidden,</span>
+  </span>
+  <span aria-hidden="true" id="link-label-1b">hidden but referenced,</span>
+  <span hidden id="link-label-1c">hidden from all users but referenced</span>
+</a>
+
+<h2
+  class="ex"
+  data-testname="heading with name from content, containing element that is visibility:hidden with nested content that is visibility:visible"
+  data-expectedlabel="visible to all users, un-hidden for all users"
+>
+  visible to all users,
+  <span style="visibility: hidden;">
+    hidden from all users,
+    <span style="visibility: visible;">un-hidden for all users</span>
+  </span>
+</h2>
+
+<!-- TODO: Test cases once https://github.com/w3c/aria/issues/1256 resolved: -->
+<!--       - button labelled by an element that is aria-hidden=true which contains a nested child that is aria-hidden=false -->
+<!--       - button labelled by an element that is aria-hidden=false which belongs to a parent that is aria-hidden=true -->
+<!--       - heading with name from content, containing rendered content that is aria-hidden=true with nested, rendered content that is aria-hidden=false -->
+<!--       - heading with name from content, containing element with the hidden host language attribute with nested content that is aria-hidden=false -->
+
+<!-- TODO: New test case?
+<!--       What is the expectation for a details element when it’s given an -->
+<!--       explicit role that allows name from contents (e.g., `comment`) -->
+<!--       but is also not in the open state, and therefore has contents -->
+<!--       that are both not rendered and excluded from the a11y tree. -->
+
+<script>
+AriaUtils.verifyLabelsBySelector(".ex");
+</script>
+</body>
+</html>

+ 245 - 0
Tests/LibWeb/Text/input/wpt-import/accname/name/comp_labelledby_hidden_nodes.html

@@ -0,0 +1,245 @@
+<!doctype html>
+<html>
+<head>
+  <title>Name Comp: Labelledby & Hidden Nodes</title>
+  <script src="../../resources/testharness.js"></script>
+  <script src="../../resources/testharnessreport.js"></script>
+  <script src="../../resources/testdriver.js"></script>
+  <script src="../../resources/testdriver-vendor.js"></script>
+  <script src="../../resources/testdriver-actions.js"></script>
+  <script src="../../wai-aria/scripts/aria-utils.js"></script>
+</head>
+<body>
+
+<p>Tests hidden node name computation as part of the <a href="https://w3c.github.io/accname/#comp_labelledby">#comp_labelledby</a> portion of the AccName <em>Name Computation</em> algorithm.</p>
+
+<!--
+
+  These tests verify browser conformance with the following note as part of accName computation Step 2B:
+
+  "The result of LabelledBy Recursion in combination with Hidden Not Referenced means
+  that user agents MUST include all nodes in the subtree as part of
+  the accessible name or accessible description, when the node referenced
+  by aria-labelledby or aria-describedby is hidden."
+
+-->
+
+<h2>Testing with <code>display:none</code></h2>
+
+    <button aria-labelledby="a11" data-expectedlabel="foo bar" data-testname="button with aria-labelledby using display:none hidden span (with nested span)" class="ex">x</button>
+    <span id="a11" style="display: none;">
+        foo
+        <span id="a12">bar</span>
+    </span>
+
+    <button aria-labelledby="a21" data-expectedlabel="foo bar baz" data-testname="button with aria-labelledby using display:none hidden span (with nested spans, depth 2)" class="ex">x</button>
+    <span id="a21" style="display: none;">
+        foo
+        <span id="a22">
+            bar
+            <span id="a23">baz</span>
+        </span>
+    </span>
+
+    <button aria-labelledby="a31" data-expectedlabel="foo" data-testname="button with aria-labelledby using span without display:none (with nested display:none spans, depth 2)" class="ex">x</button>
+    <span id="a31">
+        foo
+        <span id="a32" style="display: none;">
+            bar
+            <span id="a33">baz</span>
+        </span>
+    </span>
+
+    <button aria-labelledby="a41" data-expectedlabel="foo bar baz" data-testname="button with aria-labelledby using display:none hidden span (with nested sibling spans)" class="ex">x</button>
+    <span id="a41" style="display: none;">
+        foo
+        <span id="a42">bar</span>
+        <span id="a43">baz</span>
+    </span>
+
+    <button aria-labelledby="a51" data-expectedlabel="foo" data-testname="button with aria-labelledby using span without display:none (with nested display:none sibling spans)" class="ex">x</button>
+    <span id="a51">
+        foo
+        <span id="a52" style="display: none;">bar</span>
+        <span id="a53" style="display: none;">baz</span>
+    </span>
+
+    <button aria-labelledby="a61" data-expectedlabel="foo bar baz" data-testname="button with aria-labelledby using span with display:none (with nested display:inline sibling spans)" class="ex">x</button>
+    <span id="a61" style="display: none;">
+        foo
+        <span id="a62" style="display: inline;">bar</span>
+        <span id="a63" style="display: inline;">baz</span>
+    </span>
+
+<h2>Testing with <code>visibility:hidden</code></h2>
+
+    <button aria-labelledby="b11" data-expectedlabel="foo bar" data-testname="button with aria-labelledby using visibility:hidden span (with nested span)" class="ex">x</button>
+    <span id="b11" style="visibility: hidden;">
+        foo
+        <span id="b12">bar</span>
+    </span>
+
+    <button aria-labelledby="b21" data-expectedlabel="foo bar baz" data-testname="button with aria-labelledby using visibility:hidden span (with nested spans, depth 2)" class="ex">x</button>
+    <span id="b21" style="visibility: hidden;">
+        foo
+        <span id="b22">
+            bar
+            <span id="b23">baz</span>
+        </span>
+    </span>
+
+    <button aria-labelledby="b31" data-expectedlabel="foo" data-testname="button with aria-labelledby using span without visibility:hidden (with nested visibility:hidden spans, depth 2)" class="ex">x</button>
+    <span id="b31">
+        foo
+        <span id="b32" style="visibility: hidden;">
+            bar
+            <span id="b33">baz</span>
+        </span>
+    </span>
+
+    <button aria-labelledby="b41" data-expectedlabel="foo bar baz" data-testname="button with aria-labelledby using visibility:hidden hidden span (with nested sibling spans)" class="ex">x</button>
+    <span id="b41" style="visibility: hidden;">
+        foo
+        <span id="b42">bar</span>
+        <span id="b43">baz</span>
+    </span>
+
+    <button aria-labelledby="b51" data-expectedlabel="foo" data-testname="button with aria-labelledby using span without visibility:hidden (with nested visibility:hidden sibling spans)" class="ex">x</button>
+    <span id="b51">
+        foo
+        <span id="b52" style="visibility: hidden;">bar</span>
+        <span id="b53" style="visibility: hidden;">baz</span>
+    </span>
+
+    <button aria-labelledby="b61" data-expectedlabel="foo bar baz" data-testname="button with aria-labelledby using span with visibility:hidden (with nested visibility:visible sibling spans)" class="ex">x</button>
+    <span id="b61" style="visibility: hidden;">
+        foo
+        <span id="b62" style="visibility: visible;">bar</span>
+        <span id="b63" style="visibility: visible;">baz</span>
+    </span>
+
+<h2>Testing with <code>visibility:collapse</code></h2>
+
+    <button aria-labelledby="c11" data-expectedlabel="foo bar" data-testname="button with aria-labelledby using visibility:collapse span (with nested span)" class="ex">x</button>
+    <span id="c11" style="visibility: collapse;">
+        foo
+        <span id="c12">bar</span>
+    </span>
+
+    <button aria-labelledby="c21" data-expectedlabel="foo bar baz" data-testname="button with aria-labelledby using visibility:collapse span (with nested spans, depth 2)" class="ex">x</button>
+    <span id="c21" style="visibility: collapse;">
+        foo
+        <span id="c22">
+            bar
+            <span id="c23">baz</span>
+        </span>
+    </span>
+
+    <button aria-labelledby="c31" data-expectedlabel="foo bar baz" data-testname="button with aria-labelledby using span without visibility:collapse (with nested visibility:visible spans, depth 2)" class="ex">x</button>
+    <span id="c31">
+        foo
+        <span id="c32" style="visibility: visible;">
+            bar
+            <span id="c33" style="visibility: visible;">baz</span>
+        </span>
+    </span>
+
+    <button aria-labelledby="c41" data-expectedlabel="foo bar baz" data-testname="button with aria-labelledby using visibility:collapse span (with nested sibling spans)" class="ex">x</button>
+    <span id="c41" style="visibility: collapse;">
+        foo
+        <span id="c42">bar</span>
+        <span id="c43">baz</span>
+    </span>
+
+    <button aria-labelledby="c51" data-expectedlabel="foo" data-testname="button with aria-labelledby using span without visibility:collapse (with nested visibility:collapse sibling spans)" class="ex">x</button>
+    <span id="c51">
+        foo
+        <span id="c52" style="visibility: collapse;">bar</span>
+        <span id="c53" style="visibility: collapse;">baz</span>
+    </span>
+
+    <button aria-labelledby="c61" data-expectedlabel="foo bar baz" data-testname="button with aria-labelledby using span with visibility:collapse (with nested visible sibling spans)" class="ex">x</button>
+    <span id="c61" style="visibility: collapse;">
+        foo
+        <span id="c62" style="visibility: visible;">bar</span>
+        <span id="c63" style="visibility: visible;">baz</span>
+    </span>
+
+<h2>Testing with <code>aria-hidden</code></h2>
+
+    <button aria-labelledby="d11" data-expectedlabel="foo bar" data-testname="button with aria-labelledby using aria-hidden span (with nested span)" class="ex">x</button>
+    <span id="d11" aria-hidden="true">
+        foo
+        <span id="d12">bar</span>
+    </span>
+
+    <button aria-labelledby="d21" data-expectedlabel="foo bar baz" data-testname="button with aria-labelledby using aria-hidden span (with nested spans, depth 2)" class="ex">x</button>
+    <span id="d21" aria-hidden="true">
+        foo
+        <span id="d22">
+            bar
+            <span id="d23">baz</span>
+        </span>
+    </span>
+
+    <button aria-labelledby="d31" data-expectedlabel="foo" data-testname="button with aria-labelledby using span without aria-hidden (with nested aria-hidden spans, depth 2)" class="ex">x</button>
+    <span id="d31">
+        foo
+        <span id="d32" aria-hidden="true">
+            bar
+            <span id="d33">baz</span>
+        </span>
+    </span>
+
+    <button aria-labelledby="d41" data-expectedlabel="foo bar baz" data-testname="button with aria-labelledby using aria-hidden hidden span (with nested sibling spans)" class="ex">x</button>
+    <span id="d41" aria-hidden="true">
+        foo
+        <span id="d42">bar</span>
+        <span id="d43">baz</span>
+    </span>
+
+<h2>Testing with <code>hidden</code> attribute</h2>
+
+    <button aria-labelledby="e11" data-expectedlabel="foo bar" data-testname="button with aria-labelledby using HTML5 hidden span (with nested span)" class="ex">x</button>
+    <span id="e11" hidden>
+        foo
+        <span id="e12">bar</span>
+    </span>
+
+    <button aria-labelledby="e21" data-expectedlabel="foo bar baz" data-testname="button with aria-labelledby using HTML5 hidden span (with nested spans, depth 2)" class="ex">x</button>
+    <span id="e21" hidden>
+        foo
+        <span id="e22">
+            bar
+            <span id="e23">baz</span>
+        </span>
+    </span>
+
+    <button aria-labelledby="e31" data-expectedlabel="foo" data-testname="button with aria-labelledby using span without HTML5 hidden (with nested HTML5 hidden spans, depth 2)" class="ex">x</button>
+    <span id="e31">
+        foo
+        <span id="e32" hidden>
+            bar
+            <span id="e33">baz</span>
+        </span>
+    </span>
+
+    <button aria-labelledby="e41" data-expectedlabel="foo bar baz" data-testname="button with aria-labelledby using HTML5 hidden span (with nested hidden sibling spans)" class="ex">x</button>
+    <span id="e41" hidden>
+        foo
+        <span id="e42">bar</span>
+        <span id="e43">baz</span>
+    </span>
+
+    <button aria-labelledby="e51" data-expectedlabel="foo" data-testname="button with aria-labelledby using span without HTML5 hidden (with nested hidden sibling spans)" class="ex">x</button>
+    <span id="e51">
+        foo
+        <span id="e52" hidden>bar</span>
+        <span id="e53" hidden>baz</span>
+    </span>
+
+<script>
+AriaUtils.verifyLabelsBySelector(".ex");
+</script>
+</body>
+</html>