1
0
Fork 0
libreoffice/vcl/qa/cppunit/a11y/atspi2/atspi2.cxx
Daniel Baumann 8e63e14cf6
Adding upstream version 4:25.2.3.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
2025-06-22 16:20:04 +02:00

504 lines
20 KiB
C++

/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4; fill-column: 100 -*- */
/*
* This file is part of the LibreOffice project.
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
#include "atspi2.hxx"
#include <com/sun/star/accessibility/AccessibleRelationType.hpp>
#include <com/sun/star/accessibility/AccessibleStateType.hpp>
#include <com/sun/star/accessibility/XAccessibleExtendedAttributes.hpp>
#include <comphelper/propertyvalue.hxx>
#include <o3tl/string_view.hxx>
#include <sfx2/zoomitem.hxx>
#include <unotest/macros_test.hxx>
#include <test/a11y/AccessibilityTools.hxx>
#include "atspiwrapper.hxx"
using namespace css;
// from gtk3/a11y/atkwrapper.cxx
static AtspiRole mapToAtspiRole(sal_Int16 nRole, sal_Int64 nStates)
{
switch (nRole)
{
#define MAP(lo, atspi) \
case accessibility::AccessibleRole::lo: \
return ATSPI_ROLE_##atspi
#define MAP_DIRECT(a) MAP(a, a)
MAP_DIRECT(UNKNOWN);
MAP_DIRECT(ALERT);
MAP_DIRECT(BLOCK_QUOTE);
MAP_DIRECT(COLUMN_HEADER);
MAP_DIRECT(CANVAS);
MAP_DIRECT(CHECK_BOX);
MAP_DIRECT(CHECK_MENU_ITEM);
MAP_DIRECT(COLOR_CHOOSER);
MAP_DIRECT(COMBO_BOX);
MAP_DIRECT(DATE_EDITOR);
MAP_DIRECT(DESKTOP_ICON);
MAP(DESKTOP_PANE, DESKTOP_FRAME);
MAP_DIRECT(DIRECTORY_PANE);
MAP_DIRECT(DIALOG);
MAP(DOCUMENT, DOCUMENT_FRAME);
MAP(EMBEDDED_OBJECT, EMBEDDED);
MAP(END_NOTE, FOOTNOTE);
MAP_DIRECT(FILE_CHOOSER);
MAP_DIRECT(FILLER);
MAP_DIRECT(FONT_CHOOSER);
MAP_DIRECT(FOOTER);
MAP_DIRECT(FOOTNOTE);
MAP_DIRECT(FRAME);
MAP_DIRECT(GLASS_PANE);
MAP(GRAPHIC, IMAGE);
MAP(GROUP_BOX, GROUPING);
MAP_DIRECT(HEADER);
MAP_DIRECT(HEADING);
MAP(HYPER_LINK, LINK);
MAP_DIRECT(ICON);
MAP_DIRECT(INTERNAL_FRAME);
MAP_DIRECT(LABEL);
MAP_DIRECT(LAYERED_PANE);
MAP_DIRECT(LIST);
MAP_DIRECT(LIST_ITEM);
MAP_DIRECT(MENU);
MAP_DIRECT(MENU_BAR);
MAP_DIRECT(MENU_ITEM);
MAP_DIRECT(OPTION_PANE);
MAP_DIRECT(PAGE_TAB);
MAP_DIRECT(PAGE_TAB_LIST);
MAP_DIRECT(PANEL);
MAP_DIRECT(PARAGRAPH);
MAP_DIRECT(PASSWORD_TEXT);
MAP_DIRECT(POPUP_MENU);
MAP_DIRECT(PUSH_BUTTON);
MAP_DIRECT(PROGRESS_BAR);
MAP_DIRECT(RADIO_BUTTON);
MAP_DIRECT(RADIO_MENU_ITEM);
MAP_DIRECT(ROW_HEADER);
MAP_DIRECT(ROOT_PANE);
MAP_DIRECT(SCROLL_BAR);
MAP_DIRECT(SCROLL_PANE);
MAP(SHAPE, PANEL);
MAP_DIRECT(SEPARATOR);
MAP_DIRECT(SLIDER);
MAP(SPIN_BOX, SPIN_BUTTON);
MAP_DIRECT(SPLIT_PANE);
MAP_DIRECT(STATUS_BAR);
MAP_DIRECT(TABLE);
MAP_DIRECT(TABLE_CELL);
MAP_DIRECT(TEXT);
MAP(TEXT_FRAME, PANEL);
MAP_DIRECT(TOGGLE_BUTTON);
MAP_DIRECT(TOOL_BAR);
MAP_DIRECT(TOOL_TIP);
MAP_DIRECT(TREE);
MAP(VIEW_PORT, VIEWPORT);
MAP_DIRECT(WINDOW);
#if ATSPI_ROLE_COUNT > 130 /* ATSPI_ROLE_PUSH_BUTTON_MENU is 129 */
MAP(BUTTON_MENU, PUSH_BUTTON_MENU);
#else
MAP(BUTTON_MENU, PUSH_BUTTON);
#endif
MAP_DIRECT(CAPTION);
MAP_DIRECT(CHART);
MAP(EDIT_BAR, EDITBAR);
MAP_DIRECT(FORM);
MAP_DIRECT(IMAGE_MAP);
MAP(NOTE, COMMENT);
MAP_DIRECT(PAGE);
MAP_DIRECT(RULER);
MAP_DIRECT(SECTION);
MAP_DIRECT(TREE_ITEM);
MAP_DIRECT(TREE_TABLE);
MAP_DIRECT(COMMENT);
MAP(COMMENT_END, UNKNOWN);
MAP_DIRECT(DOCUMENT_PRESENTATION);
MAP_DIRECT(DOCUMENT_SPREADSHEET);
MAP_DIRECT(DOCUMENT_TEXT);
MAP_DIRECT(STATIC);
MAP_DIRECT(NOTIFICATION);
#undef MAP_DIRECT
#undef MAP
case css::accessibility::AccessibleRole::BUTTON_DROPDOWN:
if (nStates & css::accessibility::AccessibleStateType::CHECKABLE)
return ATSPI_ROLE_TOGGLE_BUTTON;
return ATSPI_ROLE_PUSH_BUTTON;
default:
SAL_WARN("vcl.gtk", "Unmapped accessible role: " << nRole);
return ATSPI_ROLE_UNKNOWN;
}
}
static AtspiStateType mapAtspiState(sal_Int64 nState)
{
// A perfect / complete mapping ...
switch (nState)
{
#define MAP(lo, atspi) \
case accessibility::AccessibleStateType::lo: \
return ATSPI_STATE_##atspi
#define MAP_DIRECT(a) MAP(a, a)
MAP_DIRECT(INVALID);
MAP_DIRECT(ACTIVE);
MAP_DIRECT(ARMED);
MAP_DIRECT(BUSY);
MAP_DIRECT(CHECKABLE);
MAP_DIRECT(CHECKED);
MAP_DIRECT(EDITABLE);
MAP_DIRECT(ENABLED);
MAP_DIRECT(EXPANDABLE);
MAP_DIRECT(EXPANDED);
MAP_DIRECT(FOCUSABLE);
MAP_DIRECT(FOCUSED);
MAP_DIRECT(HORIZONTAL);
MAP_DIRECT(ICONIFIED);
MAP_DIRECT(INDETERMINATE);
MAP_DIRECT(MANAGES_DESCENDANTS);
MAP_DIRECT(MODAL);
MAP_DIRECT(MULTI_LINE);
MAP(MULTI_SELECTABLE, MULTISELECTABLE);
MAP_DIRECT(OPAQUE);
MAP_DIRECT(PRESSED);
MAP_DIRECT(RESIZABLE);
MAP_DIRECT(SELECTABLE);
MAP_DIRECT(SELECTED);
MAP_DIRECT(SENSITIVE);
MAP_DIRECT(SHOWING);
MAP_DIRECT(SINGLE_LINE);
MAP_DIRECT(STALE);
MAP_DIRECT(TRANSIENT);
MAP_DIRECT(VERTICAL);
MAP_DIRECT(VISIBLE);
MAP(DEFAULT, IS_DEFAULT);
// a spelling error ...
MAP(DEFUNC, DEFUNCT);
#undef MAP_DIRECT
#undef MAP
default:
//Mis-use ATK_STATE_LAST_DEFINED to check if a state is unmapped
//NOTE! Do not report it
return ATSPI_STATE_LAST_DEFINED;
}
}
static AtspiRelationType mapRelationType(AccessibleRelationType eRelation)
{
switch (eRelation)
{
case accessibility::AccessibleRelationType_CONTENT_FLOWS_FROM:
return ATSPI_RELATION_FLOWS_FROM;
case accessibility::AccessibleRelationType_CONTENT_FLOWS_TO:
return ATSPI_RELATION_FLOWS_TO;
case accessibility::AccessibleRelationType_CONTROLLED_BY:
return ATSPI_RELATION_CONTROLLED_BY;
case accessibility::AccessibleRelationType_CONTROLLER_FOR:
return ATSPI_RELATION_CONTROLLER_FOR;
case accessibility::AccessibleRelationType_DESCRIBED_BY:
return ATSPI_RELATION_DESCRIBED_BY;
case accessibility::AccessibleRelationType_LABEL_FOR:
return ATSPI_RELATION_LABEL_FOR;
case accessibility::AccessibleRelationType_LABELED_BY:
return ATSPI_RELATION_LABELLED_BY;
case accessibility::AccessibleRelationType_MEMBER_OF:
return ATSPI_RELATION_MEMBER_OF;
case accessibility::AccessibleRelationType_SUB_WINDOW_OF:
return ATSPI_RELATION_SUBWINDOW_OF;
case accessibility::AccessibleRelationType_NODE_CHILD_OF:
return ATSPI_RELATION_NODE_CHILD_OF;
default:
return ATSPI_RELATION_NULL;
}
}
static std::string debugString(const Atspi::Accessible& pAtspiAccessible)
{
CPPUNIT_NS::OStringStream ost;
ost << "(" << static_cast<const void*>(pAtspiAccessible.get()) << ")";
if (pAtspiAccessible)
{
ost << " role=\"" << pAtspiAccessible.getRoleName() << '"';
ost << " name=\"" << pAtspiAccessible.getName() << '"';
ost << " description=\"" << pAtspiAccessible.getDescription() << '"';
}
return ost.str();
}
static void dumpAtspiTree(const Atspi::Accessible& pAcc, const int depth = 0)
{
std::cout << debugString(pAcc) << std::endl;
sal_Int32 i = 0;
for (const auto pChild : pAcc)
{
for (auto j = decltype(depth){ 0 }; j < depth; j++)
std::cout << " ";
std::cout << " * child " << i++ << ": ";
dumpAtspiTree(pChild, depth + 1);
}
}
void Atspi2TestTree::compareObjects(const uno::Reference<accessibility::XAccessible>& xLOAccessible,
const Atspi::Accessible& pAtspiAccessible,
const sal_uInt16 recurseFlags)
{
if (recurseFlags != RecurseFlags::NONE)
std::cout << "checking " << debugString(pAtspiAccessible) << " against "
<< AccessibilityTools::debugString(xLOAccessible) << std::endl;
CPPUNIT_ASSERT(xLOAccessible);
CPPUNIT_ASSERT(pAtspiAccessible);
auto xLOContext = xLOAccessible->getAccessibleContext();
/* role: we translate to ATSPI role, because the value was created by LO already and converted
* to ATK, which in turn converts it to ATSPI. However, ATK and ATSPI are roughly equivalent
* (ATK basically follows ATSPI), but LO's internal might have more complex mappings that can't
* be represented with a round trip. */
const AtspiRole nLORole
= mapToAtspiRole(xLOContext->getAccessibleRole(), xLOContext->getAccessibleStateSet());
const auto nAtspiRole = pAtspiAccessible.getRole();
CPPUNIT_ASSERT_EQUAL(nLORole, nAtspiRole);
/* name (no need to worry about debugging suffixes as AccessibilityTools::nameEquals does, as
* that will also be part of the name sent to ATSPI) */
CPPUNIT_ASSERT_EQUAL(xLOContext->getAccessibleName(),
OUString::fromUtf8(pAtspiAccessible.getName()));
// description
CPPUNIT_ASSERT_EQUAL(xLOContext->getAccessibleDescription(),
OUString::fromUtf8(pAtspiAccessible.getDescription()));
// parent relationship (this is conditional as the ATSPI tree has additional parents, as well as
// because we don't want to recurse up the tree)
if (recurseFlags & RecurseFlags::PARENT)
{
// index in parent
CPPUNIT_ASSERT_EQUAL(xLOContext->getAccessibleIndexInParent(),
sal_Int64(pAtspiAccessible.getIndexInParent()));
// parent (well, that's making things a lot more expensive...)
compareObjects(xLOContext->getAccessibleParent(), pAtspiAccessible.getParent(),
RecurseFlags::NONE);
}
// state set
const auto loStateSet = xLOContext->getAccessibleStateSet();
const auto atspiStateSet = pAtspiAccessible.getStateSet();
const auto nBits
= (sizeof(decltype(loStateSet)) * 8) - (std::is_signed_v<decltype(loStateSet)> ? 1 : 0);
for (auto shift = decltype(nBits){ 0 }; shift < nBits; shift++)
{
const auto loState = decltype(loStateSet){ 1 } << shift;
const auto atspiState = mapAtspiState(loState);
// ignore a state that does not map to Atspi
if (atspiState == ATSPI_STATE_LAST_DEFINED)
continue;
/* FIXME: The ATK implementation in LO adds FOCUSED if the obj == atk_get_focus_object()
* (see atkwrapper.cxx::wrapper_ref_state_set()), but there seem to be some bug (or delay?
* as it's done in idle) in the tracking, so we can end up with extra FOCUSED states on the
* Atspi side. To work around that, we skip the case where it's not set on LO's side */
if (atspiState == ATSPI_STATE_FOCUSED && !(loStateSet & loState))
continue;
CPPUNIT_ASSERT_EQUAL_MESSAGE("Unmatched state: " + Atspi::State::getName(atspiState),
(loStateSet & loState) != 0,
atspiStateSet.contains(atspiState));
}
// attributes
if (auto xLOAttrs
= uno::Reference<accessibility::XAccessibleExtendedAttributes>(xLOContext, uno::UNO_QUERY))
{
// see atktextattributes.cxx:attribute_set_new_from_extended_attributes
const uno::Any anyVal = xLOAttrs->getExtendedAttributes();
OUString sExtendedAttrs;
anyVal >>= sExtendedAttrs;
sal_Int32 nIndex = 0;
const auto atspiAttrs = pAtspiAccessible.getAttributes();
do
{
OUString sProperty = sExtendedAttrs.getToken(0, ';', nIndex);
sal_Int32 nColonPos = 0;
const OString sPropertyName = OUStringToOString(
o3tl::getToken(sProperty, 0, ':', nColonPos), RTL_TEXTENCODING_UTF8);
const OString sPropertyValue = OUStringToOString(
o3tl::getToken(sProperty, 0, ':', nColonPos), RTL_TEXTENCODING_UTF8);
const auto atspiAttrIter = atspiAttrs.find(std::string(sPropertyName));
CPPUNIT_ASSERT_MESSAGE(std::string("Missing attribute: ") + sPropertyName.getStr(),
atspiAttrIter != atspiAttrs.end());
CPPUNIT_ASSERT_EQUAL(std::string_view(sPropertyName),
std::string_view(atspiAttrIter->first));
CPPUNIT_ASSERT_EQUAL(std::string_view(sPropertyValue),
std::string_view(atspiAttrIter->second));
} while (nIndex >= 0 && nIndex < sExtendedAttrs.getLength());
}
// relations
const auto xLORelationSet = xLOContext->getAccessibleRelationSet();
const auto aAtspiRelationSet = pAtspiAccessible.getRelationSet();
const auto nLORelationCount = xLORelationSet.is() ? xLORelationSet->getRelationCount() : 0;
CPPUNIT_ASSERT_EQUAL(nLORelationCount, sal_Int32(aAtspiRelationSet.size()));
for (auto i = decltype(nLORelationCount){ 0 }; i < nLORelationCount; i++)
{
const auto xLORelation = xLORelationSet->getRelation(i);
const auto pAtspiRelation = aAtspiRelationSet[i];
const auto nLOTargetsCount = xLORelation.TargetSet.getLength();
CPPUNIT_ASSERT_EQUAL(mapRelationType(xLORelation.RelationType),
pAtspiRelation.getRelationType());
CPPUNIT_ASSERT_EQUAL(nLOTargetsCount, static_cast<sal_Int32>(pAtspiRelation.getNTargets()));
if (recurseFlags & RecurseFlags::RELATIONS_TARGETS)
{
for (auto j = decltype(nLOTargetsCount){ 0 }; j < nLOTargetsCount; j++)
{
uno::Reference<accessibility::XAccessible> xLOTarget = xLORelation.TargetSet[j];
compareObjects(xLOTarget, pAtspiRelation.getTarget(j), RecurseFlags::NONE);
}
}
}
// other interfaces
if (auto xLOText = uno::Reference<accessibility::XAccessibleText>(xLOContext, uno::UNO_QUERY))
{
Atspi::Text pAtspiText;
CPPUNIT_ASSERT_NO_THROW(pAtspiText = pAtspiAccessible.queryText());
compareTextObjects(xLOText, pAtspiText);
}
// TODO: more checks here...
}
void Atspi2TestTree::compareTrees(const uno::Reference<accessibility::XAccessible>& xLOAccessible,
const Atspi::Accessible& xAtspiAccessible, const int depth)
{
sal_uInt16 recurseFlags = RecurseFlags::ALL;
if (depth == 0)
recurseFlags ^= RecurseFlags::PARENT;
compareObjects(xLOAccessible, xAtspiAccessible, recurseFlags);
if (!xLOAccessible || !xAtspiAccessible)
return;
auto xLOContext = xLOAccessible->getAccessibleContext();
CPPUNIT_ASSERT(xLOContext);
const auto nLOChildCount = xLOContext->getAccessibleChildCount();
const auto nAtspiChildCount = decltype(nLOChildCount){ xAtspiAccessible.getChildCount() };
/* We use >= instead of == because GTK exposes scrollbar objects LO doesn't. We possibly
* should check better than merely accept more children, but it's probably OK if there are
* *more* children as viewed by ATSPI, rather than less. And we're comparing them anyway. */
CPPUNIT_ASSERT_GREATEREQUAL(nLOChildCount, nAtspiChildCount);
for (auto nthChild = decltype(nLOChildCount){ 0 }; nthChild < nLOChildCount; nthChild++)
{
for (auto i = decltype(depth){ 0 }; i < depth; i++)
std::cout << " ";
std::cout << "* child " << nthChild << ": ";
compareTrees(xLOContext->getAccessibleChild(nthChild),
xAtspiAccessible.getChildAtIndex(nthChild), depth + 1);
}
/* We need to scrolling test here, because they might modify the tree and invalidate children,
* so we can't do it from the children themselves as they might get disposed during the test */
if (nLOChildCount > 0
&& accessibility::AccessibleRole::DOCUMENT_TEXT == xLOContext->getAccessibleRole())
{
testSwScroll(xLOContext, xAtspiAccessible);
}
}
// gets the nth child of @p pAcc and check its role is @p role
static Atspi::Accessible getDescendentAtPath(const Atspi::Accessible& xAcc, int nthChild,
AtspiRole role)
{
CPPUNIT_ASSERT(xAcc);
CPPUNIT_ASSERT_GREATER(nthChild, xAcc.getChildCount());
auto xChild = xAcc.getChildAtIndex(nthChild);
CPPUNIT_ASSERT(xChild);
CPPUNIT_ASSERT_EQUAL(role, xChild.getRole());
return xChild;
}
// gets the nth child of @p pAcc and check its role is @p role, then gets the nth child of that one, etc.
template <typename... Ts>
static Atspi::Accessible getDescendentAtPath(const Atspi::Accessible& xAcc, int nthChild,
AtspiRole role, Ts... args)
{
return getDescendentAtPath(getDescendentAtPath(xAcc, nthChild, role), args...);
}
CPPUNIT_TEST_FIXTURE(Atspi2TestTree, Test1)
{
loadFromSrc(u"vcl/qa/cppunit/a11y/atspi2/testdocuments/ecclectic.fodt"_ustr);
/* FIXME: We zoom out for everything to fit in the view not to have off-screen children
* that the controller code fails to clean up properly in some situations.
* Once the root issue is fixed in LO, remove this.
* Note that zooming out like so, and not having off-screen children, renders the
* Atspi2TestTree::testSwScroll() test useless as it has nothing to scroll into view. */
unotest::MacrosTest::dispatchCommand(mxDocument, ".uno:ZoomPage", {});
unotest::MacrosTest::dispatchCommand(
mxDocument, ".uno:ViewLayout",
{
comphelper::makePropertyValue("ViewLayout.Columns", sal_Int16(2)),
comphelper::makePropertyValue("ViewLayout.BookMode", false),
});
/* HACK: verify the whole content of the document is actually visible (nothing overflows)
* after zooming out above */
const auto xLODocContext = getDocumentAccessibleContext();
const auto xLODocFirstChild = xLODocContext->getAccessibleChild(0);
CPPUNIT_ASSERT(xLODocFirstChild.is());
CPPUNIT_ASSERT(
!getFirstRelationTargetOfType(xLODocFirstChild->getAccessibleContext(),
accessibility::AccessibleRelationType_CONTENT_FLOWS_FROM));
const auto nLODocChildCount = xLODocContext->getAccessibleChildCount();
const auto xLODocLastChild = xLODocContext->getAccessibleChild(nLODocChildCount - 1);
CPPUNIT_ASSERT(xLODocLastChild.is());
CPPUNIT_ASSERT(
!getFirstRelationTargetOfType(xLODocLastChild->getAccessibleContext(),
accessibility::AccessibleRelationType_CONTENT_FLOWS_TO));
// END HACK
auto xContext = getWindowAccessibleContext();
CPPUNIT_ASSERT(xContext.is());
//~ dumpA11YTree(xContext);
// get the window manager frame
auto xAtspiWindow = getDescendentAtPath(m_pAtspiApp, 0, ATSPI_ROLE_FRAME);
CPPUNIT_ASSERT(xAtspiWindow);
dumpAtspiTree(xAtspiWindow);
/* The ATSPI representation has extra nodes around the relevant ones, which look like leftovers
* from the start center. Ignore those and dive directly to the meaningful node (which is the
* 1st child of the 2nd child of the 1st child -- ask me how I know) */
auto xAtspiPane = getDescendentAtPath(xAtspiWindow, 0, ATSPI_ROLE_PANEL, 1, ATSPI_ROLE_PANEL, 0,
ATSPI_ROLE_ROOT_PANE);
compareTrees(uno::Reference<accessibility::XAccessible>(mxWindow, uno::UNO_QUERY_THROW),
xAtspiPane);
}
CPPUNIT_PLUGIN_IMPLEMENT();
/* vim:set shiftwidth=4 softtabstop=4 expandtab cinoptions=b1,g0,N-s cinkeys+=0=break: */