RESOLVED FIXED231914
AX: Any addition of children should funnel through AccessibilityObject::addChild
https://bugs.webkit.org/show_bug.cgi?id=231914
Summary AX: Any addition of children should funnel through AccessibilityObject::addChild
Tyler Wilcock
Reported 2021-10-18 15:51:12 PDT
Centralizing this logic in one place unlocks the ability to reliably set child state based on parent state (e.g. "Do I have any ancestors with a subrole of document?").
Attachments
Patch (18.00 KB, patch)
2021-10-18 18:38 PDT, Tyler Wilcock
ews-feeder: commit-queue-
Patch (18.00 KB, patch)
2021-10-18 19:18 PDT, Tyler Wilcock
no flags
Patch (17.09 KB, patch)
2021-10-19 10:27 PDT, Tyler Wilcock
no flags
Patch (16.79 KB, patch)
2021-10-20 14:16 PDT, Tyler Wilcock
ews-feeder: commit-queue-
Patch (16.81 KB, patch)
2021-10-20 14:38 PDT, Tyler Wilcock
no flags
Patch (27.24 KB, patch)
2021-10-23 21:07 PDT, Tyler Wilcock
no flags
Radar WebKit Bug Importer
Comment 1 2021-10-18 15:51:23 PDT
Tyler Wilcock
Comment 2 2021-10-18 18:38:58 PDT
Tyler Wilcock
Comment 3 2021-10-18 19:18:50 PDT
Andres Gonzalez
Comment 4 2021-10-19 07:56:18 PDT
(In reply to Tyler Wilcock from comment #3) > Created attachment 441675 [details] > Patch --- a/Source/WebCore/accessibility/AccessibilityObject.cpp +++ a/Source/WebCore/accessibility/AccessibilityObject.cpp - ASSERT(child->parentObject() == this); +#ifndef NDEBUG + // Autofill buttons are dynamically created elements by WebKit, and aren't assigned as a child + // to their proper DOM parent (see TextFieldInputType::createAutoFillButton), so exclude them from this ASSERT. + // Table component child-parent relationships also often doesn't line up properly, hence the need for methods + // like parentTable() and parentRow(). Exclude them from this ASSERT too. + if (!isAutofillButton(*this) && !isTableComponent(*child) && !isTableComponent(*this)) + ASSERT(child->parentObject() == this); +#endif I'm not convinced we should special case this sanity check. The child-parent relationship of the corresponding DOM elements may not match the AX hierarchy, but the AX hierarchy should be consistent with itself in all cases. I.e., if we expose child as a child of parent, then child->parentObject() should be parent.
Andres Gonzalez
Comment 5 2021-10-19 07:58:32 PDT
(In reply to Andres Gonzalez from comment #4) > (In reply to Tyler Wilcock from comment #3) > > Created attachment 441675 [details] > > Patch > > --- a/Source/WebCore/accessibility/AccessibilityObject.cpp > +++ a/Source/WebCore/accessibility/AccessibilityObject.cpp > > - ASSERT(child->parentObject() == this); > +#ifndef NDEBUG > + // Autofill buttons are dynamically created elements by WebKit, and > aren't assigned as a child > + // to their proper DOM parent (see > TextFieldInputType::createAutoFillButton), so exclude them from this ASSERT. > + // Table component child-parent relationships also often doesn't > line up properly, hence the need for methods > + // like parentTable() and parentRow(). Exclude them from this > ASSERT too. > + if (!isAutofillButton(*this) && !isTableComponent(*child) && > !isTableComponent(*this)) > + ASSERT(child->parentObject() == this); > +#endif > > I'm not convinced we should special case this sanity check. The child-parent > relationship of the corresponding DOM elements may not match the AX > hierarchy, but the AX hierarchy should be consistent with itself in all > cases. I.e., if we expose child as a child of parent, then > child->parentObject() should be parent. Perhaps part of the problem is that parentObject() is returning the parent based on the DOM hierarchy and not on the AX hierarchy?
Andres Gonzalez
Comment 6 2021-10-19 08:07:53 PDT
(In reply to Tyler Wilcock from comment #3) > Created attachment 441675 [details] > Patch --- a/Source/WebCore/accessibility/AccessibilityRenderObject.cpp +++ a/Source/WebCore/accessibility/AccessibilityRenderObject.cpp - if (HTMLElement* autoFillElement = input.autoFillButtonElement()) { - if (AccessibilityObject* axAutoFill = axObjectCache()->getOrCreate(autoFillElement)) - m_children.append(axAutoFill); - } + if (HTMLElement* autoFillElement = input.autoFillButtonElement()) if (auto* autoFillElement = input.autoFillButtonElement()) + addChild(axObjectCache()->getOrCreate(autoFillElement)); We were checking for nullity of axObjectCache() before. I wouldn't remove that check yet, until we solidify the life times of the objects respect to the cache.
Andres Gonzalez
Comment 7 2021-10-19 08:14:02 PDT
(In reply to Tyler Wilcock from comment #3) > Created attachment 441675 [details] > Patch @@ -3381,12 +3379,7 @@ void AccessibilityRenderObject::addRemoteSVGChildren() // In order to connect the AX hierarchy from the SVG root element from the loaded resource // the parent must be set, because there's no other way to get back to who created the image. root->setParent(this); - - if (root->accessibilityIsIgnored()) { - for (const auto& child : root->children()) - m_children.append(child); - } else - m_children.append(root); + addChild(root); addChild would need to do the same type of deep append, i.e., if the root is ignored, try to add its children.
Andres Gonzalez
Comment 8 2021-10-19 08:22:24 PDT
(In reply to Tyler Wilcock from comment #3) > Created attachment 441675 [details] > Patch --- a/Source/WebCore/accessibility/AccessibilityTable.cpp +++ a/Source/WebCore/accessibility/AccessibilityTable.cpp + // While `addChild` won't insert ignored children, we still need this accessibilityIsIgnored + // check so that `addChild` doesn't try to add the caption's children in its stead. Basically, + // explicitly checking accessibilityIsIgnored() ignores the caption and any of its children. if (axCaption && !axCaption->accessibilityIsIgnored()) - m_children.append(axCaption); + addChild(axCaption); Considering this and the SVG case, perhaps it is cleaner to add a bool recursive param to addChild.
Tyler Wilcock
Comment 9 2021-10-19 08:29:05 PDT
(In reply to Andres Gonzalez from comment #5) > (In reply to Andres Gonzalez from comment #4) > > (In reply to Tyler Wilcock from comment #3) > > > Created attachment 441675 [details] > > > Patch > > > > --- a/Source/WebCore/accessibility/AccessibilityObject.cpp > > +++ a/Source/WebCore/accessibility/AccessibilityObject.cpp > > > > - ASSERT(child->parentObject() == this); > > +#ifndef NDEBUG > > + // Autofill buttons are dynamically created elements by WebKit, and > > aren't assigned as a child > > + // to their proper DOM parent (see > > TextFieldInputType::createAutoFillButton), so exclude them from this ASSERT. > > + // Table component child-parent relationships also often doesn't > > line up properly, hence the need for methods > > + // like parentTable() and parentRow(). Exclude them from this > > ASSERT too. > > + if (!isAutofillButton(*this) && !isTableComponent(*child) && > > !isTableComponent(*this)) > > + ASSERT(child->parentObject() == this); > > +#endif > > > > I'm not convinced we should special case this sanity check. The child-parent > > relationship of the corresponding DOM elements may not match the AX > > hierarchy, but the AX hierarchy should be consistent with itself in all > > cases. I.e., if we expose child as a child of parent, then > > child->parentObject() should be parent. > > Perhaps part of the problem is that parentObject() is returning the parent > based on the DOM hierarchy and not on the AX hierarchy? Yes, that's exactly the problem. This patch doesn't change any of these "faulty" relationships with autofill buttons and tables, just documents them more clearly in this assert.
Tyler Wilcock
Comment 10 2021-10-19 08:29:40 PDT
(In reply to Andres Gonzalez from comment #7) > (In reply to Tyler Wilcock from comment #3) > > Created attachment 441675 [details] > > Patch > > @@ -3381,12 +3379,7 @@ void AccessibilityRenderObject::addRemoteSVGChildren() > // In order to connect the AX hierarchy from the SVG root element from > the loaded resource > // the parent must be set, because there's no other way to get back to > who created the image. > root->setParent(this); > - > - if (root->accessibilityIsIgnored()) { > - for (const auto& child : root->children()) > - m_children.append(child); > - } else > - m_children.append(root); > + addChild(root); > > addChild would need to do the same type of deep append, i.e., if the root is > ignored, try to add its children. `AccessibilityObject::addChild` calls `AccessibilityObject::insertChild`, which does exactly this :)
Tyler Wilcock
Comment 11 2021-10-19 08:37:38 PDT
(In reply to Andres Gonzalez from comment #6) > (In reply to Tyler Wilcock from comment #3) > > Created attachment 441675 [details] > > Patch > > --- a/Source/WebCore/accessibility/AccessibilityRenderObject.cpp > +++ a/Source/WebCore/accessibility/AccessibilityRenderObject.cpp > > - if (HTMLElement* autoFillElement = input.autoFillButtonElement()) { > - if (AccessibilityObject* axAutoFill = > axObjectCache()->getOrCreate(autoFillElement)) > - m_children.append(axAutoFill); > - } > + if (HTMLElement* autoFillElement = input.autoFillButtonElement()) > > if (auto* autoFillElement = input.autoFillButtonElement()) > > + addChild(axObjectCache()->getOrCreate(autoFillElement)); > > We were checking for nullity of axObjectCache() before. I wouldn't remove > that check yet, until we solidify the life times of the objects respect to > the cache. I might be mis-reading it, but I don't think the code before checked the nullity of the cache before dereferencing it. Old: if (HTMLElement* autoFillElement = input.autoFillButtonElement()) { if (AccessibilityObject* axAutoFill = axObjectCache()->getOrCreate(autoFillElement)) m_children.append(axAutoFill); } New: if (HTMLElement* autoFillElement = input.autoFillButtonElement()) addChild(axObjectCache()->getOrCreate(autoFillElement)); If the new object the cache returns is null, `addChild` does nothing. I can add a null check of the cache, though. Can also use an auto* here.
chris fleizach
Comment 12 2021-10-19 08:54:26 PDT
Comment on attachment 441675 [details] Patch View in context: https://bugs.webkit.org/attachment.cgi?id=441675&action=review > Source/WebCore/accessibility/AccessibilityObject.cpp:-518 > - ASSERT(child->parentObject() == this); do we still want this parent object assert too? > Source/WebCore/accessibility/AccessibilityTableRow.cpp:155 > + if (ariaOwnedElements.size()) { this size check seems unnecessary now
Tyler Wilcock
Comment 13 2021-10-19 10:08:29 PDT
(In reply to chris fleizach from comment #12) > Comment on attachment 441675 [details] > Patch > > View in context: > https://bugs.webkit.org/attachment.cgi?id=441675&action=review > > > Source/WebCore/accessibility/AccessibilityObject.cpp:-518 > > - ASSERT(child->parentObject() == this); > > do we still want this parent object assert too? Yeah, spoke to Andres and we think this ASSERT might not be that useful in its current form, since it is often verifying DOM parent-child relationship rather than AX tree parent-child relationships. There are several cases where DOM parent-child relationships are validly not how the AX tree is structured (i.e. aria-owns, tables, autofill buttons). I'm just going to remove it. > > Source/WebCore/accessibility/AccessibilityTableRow.cpp:155 > > + if (ariaOwnedElements.size()) { > > this size check seems unnecessary now That would be a change in behavior from the current: Either add "aria-owns" elements as m_children, _or_ AccessibilityRenderObject::addChildren() (aka DOM children) as m_children. Your suggestion would be unconditionally adding both aria-owns children and DOM children to m_children. Based on the definition of aria-owns (https://www.w3.org/TR/2017/REC-wai-aria-1.1-20171214/#aria-owns), your suggestion seems right. > If an element has both aria-owns and DOM children then the order of the child elements with respect to the parent/child relationship is the DOM children first, then the elements referenced in aria-owns. But I think I'd rather make that behavior change in a separate patch, since other classes do the same thing.
chris fleizach
Comment 14 2021-10-19 10:18:54 PDT
Comment on attachment 441675 [details] Patch View in context: https://bugs.webkit.org/attachment.cgi?id=441675&action=review >>> Source/WebCore/accessibility/AccessibilityObject.cpp:-518 >>> - ASSERT(child->parentObject() == this); >> >> do we still want this parent object assert too? > > Yeah, spoke to Andres and we think this ASSERT might not be that useful in its current form, since it is often verifying DOM parent-child relationship rather than AX tree parent-child relationships. There are several cases where DOM parent-child relationships are validly not how the AX tree is structured (i.e. aria-owns, tables, autofill buttons). I'm just going to remove it. I’ve seen this assert to be useful in the past when we mess up the parent child hierarchy for mock elements or other kinds of custom sub classes Is it asserting now in your tests? >>> Source/WebCore/accessibility/AccessibilityTableRow.cpp:155 >>> + if (ariaOwnedElements.size()) { >> >> this size check seems unnecessary now > > That would be a change in behavior from the current: > > Either add "aria-owns" elements as m_children, _or_ AccessibilityRenderObject::addChildren() (aka DOM children) as m_children. > > Your suggestion would be unconditionally adding both aria-owns children and DOM children to m_children. > > Based on the definition of aria-owns (https://www.w3.org/TR/2017/REC-wai-aria-1.1-20171214/#aria-owns), your suggestion seems right. Agreed based on that reading. We need to make that change. And we’ll need a test for it. Could be separate patch
Tyler Wilcock
Comment 15 2021-10-19 10:27:12 PDT
Tyler Wilcock
Comment 16 2021-10-19 10:40:45 PDT
> I’ve seen this assert to be useful in the past when we mess up the parent > child hierarchy for mock elements or other kinds of custom sub classes > Is it asserting now in your tests? It is, yeah — specifically for anything table related (parent or child), and for autofill buttons. If it's been useful in the context of mock objects, maybe we can keep it, but would need the conditionals around the ASSERT I added in https://bugs.webkit.org/attachment.cgi?id=441675&action=diff. It seems we've experienced issues in the past with table parent-child relationships — see AccessibiltyTableRow::parentTable(): AccessibilityTable* AccessibilityTableRow::parentTable() const { // The parent table might not be the direct ancestor of the row unfortunately. ARIA states that role="grid" should // only have "row" elements, but if not, we still should handle it gracefully by finding the right table. for (AccessibilityObject* parent = parentObject(); parent; parent = parent->parentObject()) { ...truncated... }
chris fleizach
Comment 17 2021-10-19 10:56:29 PDT
(In reply to Tyler Wilcock from comment #16) > > I’ve seen this assert to be useful in the past when we mess up the parent > > child hierarchy for mock elements or other kinds of custom sub classes > > Is it asserting now in your tests? > > It is, yeah — specifically for anything table related (parent or child), and > for autofill buttons. If it's been useful in the context of mock objects, > maybe we can keep it, but would need the conditionals around the ASSERT I > added in https://bugs.webkit.org/attachment.cgi?id=441675&action=diff. > It worries me in those cases. it sounds like auto fill buttons parent/child hierarchy might be broken actually > It seems we've experienced issues in the past with table parent-child > relationships — see AccessibiltyTableRow::parentTable(): > > AccessibilityTable* AccessibilityTableRow::parentTable() const > { > // The parent table might not be the direct ancestor of the row > unfortunately. ARIA states that role="grid" should > // only have "row" elements, but if not, we still should handle it > gracefully by finding the right table. > for (AccessibilityObject* parent = parentObject(); parent; parent = > parent->parentObject()) { > ...truncated... > }
Tyler Wilcock
Comment 18 2021-10-19 11:38:20 PDT
I tested it out with a login form at discover.com and the autofill button is focusable and clickable via VoiceOver. Though navigation to the autofill button is not easy — required a lot of VO-right, VO-left, VO-Shift-Down into the text field, and VO-Shift-Right to get to. Activating the button isn't overly useful, either — it just re-opened the autofill menu that was already open.
Tyler Wilcock
Comment 19 2021-10-20 14:16:35 PDT
Tyler Wilcock
Comment 20 2021-10-20 14:38:32 PDT
EWS
Comment 21 2021-10-21 09:27:36 PDT
Committed r284606 (243335@main): <https://commits.webkit.org/243335@main> All reviewed patches have been landed. Closing bug and clearing flags on attachment 441940 [details].
ayumi_kojima
Comment 22 2021-10-22 09:23:32 PDT
Reverted r284606 for reason: Reverting because this commit caused accessibility/ios-simulator/accessibility-aria-table-children.html to fail Committed r284687 (243407@main): <https://commits.webkit.org/243407@main>
Tyler Wilcock
Comment 23 2021-10-23 21:07:27 PDT
Tyler Wilcock
Comment 24 2021-10-23 22:58:56 PDT
The previous patch was reverted because it caused a failure in three iOS tests. The first two (accessibility/ios-simulator/disabled-states.html and accessibility/ios-simulator/popup-button-value-label.html) triggered ASSERT(child->parentObject() == this) in insertChild. This was because AccessibilityMenuListOption didn’t have a parentObject() method (therefore falling back to the default AccessibilityObject::parentObject implementation, which returns nullptr). I fixed this. The other failing test was accessibility/ios-simulator/accessibility-aria-table-children.html. This test exposed a bug which indicates that some of our classes rely on addChild not descending into the child’s children when said child is ignored. As suggested by Andres above, I added a parameter to addChild and insertChild to control this behavior on a case-by-case basis. Link showing failed test results: https://build.webkit.org/results/Apple-iOS-15-Simulator-Debug-WK2-Tests/r284606%20(253)/results.html
EWS
Comment 25 2021-10-24 10:14:06 PDT
Committed r284760 (243469@main): <https://commits.webkit.org/243469@main> All reviewed patches have been landed. Closing bug and clearing flags on attachment 442292 [details].
Note You need to log in before you can comment on or make changes to this bug.