compilerplugins/clang/staticmethods.cxx                 |    4 
 config_host.mk.in                                       |    5 
 config_host/config_atspi.h.in                           |   14 
 configure.ac                                            |   62 
 distro-configs/Jenkins/linux_clang_dbgutil_64           |    1 
 distro-configs/Jenkins/linux_gcc_release_64             |    1 
 include/test/a11y/AccessibilityTools.hxx                |    2 
 test/source/a11y/AccessibilityTools.cxx                 |    6 
 vcl/CppunitTest_vcl_gtk3_a11y.mk                        |   61 
 vcl/Module_vcl.mk                                       |    6 
 vcl/qa/cppunit/a11y/atspi2/atspi2.cxx                   |  498 +++++++
 vcl/qa/cppunit/a11y/atspi2/atspi2.hxx                   |   45 
 vcl/qa/cppunit/a11y/atspi2/atspi2testbase.hxx           |   94 +
 vcl/qa/cppunit/a11y/atspi2/atspi2text.cxx               | 1017 ++++++++++++++++
 vcl/qa/cppunit/a11y/atspi2/atspiwrapper.cxx             |   22 
 vcl/qa/cppunit/a11y/atspi2/atspiwrapper.hxx             |  784 ++++++++++++
 vcl/qa/cppunit/a11y/atspi2/testdocuments/ecclectic.fodt |  258 ++++
 17 files changed, 2879 insertions(+), 1 deletion(-)

New commits:
commit 3426dcfec2b4d5c755024c355f323ecc9f656e4a
Author:     Colomban Wendling <cwendl...@hypra.fr>
AuthorDate: Wed Apr 5 15:39:25 2023 +0200
Commit:     Michael Weghorn <m.wegh...@posteo.de>
CommitDate: Thu Jul 27 20:01:17 2023 +0200

    vcl gtk3: Introduce AT-SPI2 tests for the GTK3 accessibility layer
    
    Add tests for the GTK3 accessibility platform layer.  These tests
    compare the internal LO representation with what is visible to the
    platform, and thus the user's accessibility tools.
    
    In most cases the tests are fairly trivial as LO's internals are not
    far off AT-SPI2's expectations.  There are however notable exceptions
    like for example the text attributes, that have a wildly different
    representation and require more complex checks matching what LO's
    platform layer does, the other way around.
    
    These tests use libatspi2 directly, but as the C API is awful to work
    with regarding resource management, there are wrappers to handle the
    complexity using RAII.  The resulting API is fairly trivial to use.
    
    As these tests require using the GTK3 VCL plugin and for the a11y tree
    to be visible to AT-SPI2, they are run under XVFB using a separate dbus
    session through dbus-launch.
    
    Working on this has already lead to reporting and/or solving some
    issues:
    
    * https://gerrit.libreoffice.org/c/core/+/151303
    * https://gerrit.libreoffice.org/c/core/+/151650
    * https://gerrit.libreoffice.org/c/core/+/152456
    * https://gerrit.libreoffice.org/c/core/+/152457
    * https://bugs.documentfoundation.org/show_bug.cgi?id=155625
    * https://bugs.documentfoundation.org/show_bug.cgi?id=155705
    * https://gerrit.libreoffice.org/c/core/+/152748
    
    Only a subset of the a11y APIs are covered for the moment, but the
    current state should make it easy to extend upon.
    
    Change-Id: I1a047864ce8dc1f1bc3056ad00159f7fd5e5b7d3
    Reviewed-on: https://gerrit.libreoffice.org/c/core/+/153069
    Tested-by: Jenkins
    Reviewed-by: Michael Weghorn <m.wegh...@posteo.de>

diff --git a/compilerplugins/clang/staticmethods.cxx 
b/compilerplugins/clang/staticmethods.cxx
index 4651a4a3d060..38180c1daa2c 100644
--- a/compilerplugins/clang/staticmethods.cxx
+++ b/compilerplugins/clang/staticmethods.cxx
@@ -184,7 +184,9 @@ bool StaticMethods::TraverseCXXMethodDecl(const 
CXXMethodDecl * pCXXMethodDecl)
             .GlobalNamespace())
         || (fdc.Function("Read_Majority").Class("SwWW8ImplReader")
             .GlobalNamespace())
-        || fdc.Function("Ignore").Class("SwWrtShell").GlobalNamespace())
+        || fdc.Function("Ignore").Class("SwWrtShell").GlobalNamespace()
+        || 
(cdc.Class("AttributesChecker").AnonymousNamespace().GlobalNamespace()
+            && startsWith(pCXXMethodDecl->getNameAsString(), "check")))
     {
         return true;
     }
diff --git a/config_host.mk.in b/config_host.mk.in
index acb868b8f05f..85089efb254e 100644
--- a/config_host.mk.in
+++ b/config_host.mk.in
@@ -31,6 +31,8 @@ export ASSERT_ALWAYS_ABORT=@ASSERT_ALWAYS_ABORT@
 export ATL_INCLUDE=@ATL_INCLUDE@
 export ATL_LIB=@ATL_LIB@
 export ATOMIC_LIB=@ATOMIC_LIB@
+export ATSPI2_CFLAGS=$(gb_SPACE)@ATSPI2_CFLAGS@
+export ATSPI2_LIBS=$(gb_SPACE)@ATSPI2_LIBS@
 export AVAHI_CFLAGS=$(gb_SPACE)@AVAHI_CFLAGS@
 export AVAHI_LIBS=$(gb_SPACE)@AVAHI_LIBS@
 export LIBATOMIC_OPS_CFLAGS=$(gb_SPACE)@LIBATOMIC_OPS_CFLAGS@
@@ -123,6 +125,7 @@ export DBUS_LIBS=$(gb_SPACE)@DBUS_LIBS@
 export DBUS_GLIB_CFLAGS=$(gb_SPACE)@DBUS_GLIB_CFLAGS@
 export DBUS_GLIB_LIBS=$(gb_SPACE)@DBUS_GLIB_LIBS@
 export DBUS_HAVE_GLIB=@DBUS_HAVE_GLIB@
+export DBUS_LAUNCH=@DBUS_LAUNCH@
 export DCONF_CFLAGS=@DCONF_CFLAGS@
 export DCONF_LIBS=@DCONF_LIBS@
 export DEFAULT_BRAND_IMAGES=@DEFAULT_BRAND_IMAGES@
@@ -153,6 +156,7 @@ export ENABLE_COINMP=@ENABLE_COINMP@
 SYSTEM_COINMP=@SYSTEM_COINMP@
 export COINMP_CFLAGS=@COINMP_CFLAGS@
 export COINMP_LIBS=@COINMP_LIBS@
+export ENABLE_ATSPI_TESTS=@ENABLE_ATSPI_TESTS@
 export ENABLE_CUPS=@ENABLE_CUPS@
 export ENABLE_CURL=@ENABLE_CURL@
 export ENABLE_DBGUTIL=@ENABLE_DBGUTIL@
@@ -757,6 +761,7 @@ export XRANDR_LIBS=$(gb_SPACE)@XRANDR_LIBS@
 export XRENDER_CFLAGS=$(gb_SPACE)@XRENDER_CFLAGS@
 export XRENDER_LIBS=$(gb_SPACE)@XRENDER_LIBS@
 export XSLTPROC=@XSLTPROC@
+export XVFB_RUN=@XVFB_RUN@
 export ZLIB_CFLAGS=$(gb_SPACE)@ZLIB_CFLAGS@
 export ZLIB_LIBS=$(gb_SPACE)@ZLIB_LIBS@
 export ZMF_CFLAGS=$(gb_SPACE)@ZMF_CFLAGS@
diff --git a/config_host/config_atspi.h.in b/config_host/config_atspi.h.in
new file mode 100644
index 000000000000..7a4a48f2d2f1
--- /dev/null
+++ b/config_host/config_atspi.h.in
@@ -0,0 +1,14 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/*
+ * 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/.
+ */
+
+#pragma once
+
+#define HAVE_ATSPI2_SCROLL_TO 0
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/configure.ac b/configure.ac
index 3dea1ce2bd47..ca0bdbcadfe7 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1199,6 +1199,8 @@ test "${test_gtk3_kde5+set}" != set -a "$test_kf5" = yes 
-a "$test_gtk3" = yes &
 test "${test_system_fontconfig+set}" != set -a "${test_system_freetype+set}" = 
set && test_system_fontconfig="$test_system_freetype"
 test "${test_system_freetype+set}" != set -a "${test_system_fontconfig+set}" = 
set && test_system_freetype="$test_system_fontconfig"
 
+test "${test_atspi_tests+set}" = set || test_atspi_tests="$test_gtk3"
+
 # convenience / platform overriding "fixes"
 # Don't sort!
 test "$test_kf5" = yes -a "$test_qt5" = no && test_kf5=no
@@ -1711,6 +1713,10 @@ AC_ARG_ENABLE(gtk4,
     AS_HELP_STRING([--enable-gtk4],
         [Determines whether to use Gtk+ 4.0 vclplug on platforms where Gtk+ 
4.0 is available.]))
 
+AC_ARG_ENABLE(atspi-tests,
+    AS_HELP_STRING([--disable-atspi-tests],
+        [Determines whether to enable AT-SPI2 tests for the GTK3 vclplug.]))
+
 AC_ARG_ENABLE(introspection,
     AS_HELP_STRING([--enable-introspection],
         [Generate files for GObject introspection.  Requires --enable-gtk3.  
(Typically used by
@@ -11776,6 +11782,61 @@ if test "$enable_introspection" = yes; then
     fi
 fi
 
+# AT-SPI2 tests require gtk3, xvfb-run, dbus-launch and atspi-2
+test enable_atspi_tests = yes && test_atspi_tests=yes
+if test "$test_atspi_tests" = yes && ! test "$ENABLE_GTK3" = TRUE; then
+    if test "$enable_atspi_tests" = yes; then
+        AC_MSG_ERROR([--enable-atspi-tests requires --enable-gtk3])
+    fi
+    test_atspi_tests=no
+fi
+if test "$test_atspi_tests" = yes; then
+    AC_PATH_PROGS([XVFB_RUN], [xvfb-run], no)
+    if ! test "$XVFB_RUN" = no; then
+        dnl make sure the found xvfb-run actually works
+        AC_MSG_CHECKING([whether $XVFB_RUN works...])
+        if $XVFB_RUN true >&AS_MESSAGE_LOG_FD 2>&AS_MESSAGE_LOG_FD; then
+            AC_MSG_RESULT([yes])
+        else
+            AC_MSG_RESULT([no])
+            XVFB_RUN=no
+        fi
+    fi
+    if test "$XVFB_RUN" = no; then
+        if test "$enable_atspi_tests" = yes; then
+            AC_MSG_ERROR([xvfb-run required by --enable-atspi-tests not found])
+        fi
+        test_atspi_tests=no
+    fi
+fi
+if test "$test_atspi_tests" = yes; then
+    AC_PATH_PROGS([DBUS_LAUNCH], [dbus-launch], no)
+    if test "$DBUS_LAUNCH" = no; then
+        if test "$enable_atspi_tests" = yes; then
+            AC_MSG_ERROR([dbus-launch required by --enable-atspi-tests not 
found])
+        fi
+        test_atspi_tests=no
+    fi
+fi
+if test "$test_atspi_tests" = yes; then
+    PKG_CHECK_MODULES([ATSPI2], [atspi-2 gobject-2.0],,
+                      [if test "$enable_atspi_tests" = yes; then
+                           AC_MSG_ERROR([$ATSPI2_PKG_ERRORS])
+                       else
+                           test_atspi_tests=no
+                       fi])
+fi
+if test "x$test_atspi_tests" = xyes; then
+    PKG_CHECK_MODULES([ATSPI2_2_32], [atspi-2 >= 2.32],
+                      [have_atspi_scroll_to=1],
+                      [have_atspi_scroll_to=0])
+    AC_DEFINE_UNQUOTED([HAVE_ATSPI2_SCROLL_TO], [$have_atspi_scroll_to],
+                       [Whether AT-SPI2 has the scrollTo API])
+fi
+ENABLE_ATSPI_TESTS=
+test "$test_atspi_tests" = yes && ENABLE_ATSPI_TESTS=TRUE
+AC_SUBST([ENABLE_ATSPI_TESTS])
+
 dnl ===================================================================
 dnl check for dbus support
 dnl ===================================================================
@@ -14936,6 +14997,7 @@ AC_CONFIG_FILES([config_host.mk
                  sysui/desktop/macosx/Info.plist
                  
vs-code.code-workspace.template:.vscode/vs-code-template.code-workspace.in])
 
+AC_CONFIG_HEADERS([config_host/config_atspi.h])
 AC_CONFIG_HEADERS([config_host/config_buildconfig.h])
 AC_CONFIG_HEADERS([config_host/config_buildid.h])
 AC_CONFIG_HEADERS([config_host/config_box2d.h])
diff --git a/distro-configs/Jenkins/linux_clang_dbgutil_64 
b/distro-configs/Jenkins/linux_clang_dbgutil_64
index 6262dae9276e..3409189e151c 100644
--- a/distro-configs/Jenkins/linux_clang_dbgutil_64
+++ b/distro-configs/Jenkins/linux_clang_dbgutil_64
@@ -8,3 +8,4 @@
 --enable-odk
 --disable-dconf
 --enable-python=internal
+--enable-atspi-tests
diff --git a/distro-configs/Jenkins/linux_gcc_release_64 
b/distro-configs/Jenkins/linux_gcc_release_64
index 37f0b4b46946..c4b24f68b007 100644
--- a/distro-configs/Jenkins/linux_gcc_release_64
+++ b/distro-configs/Jenkins/linux_gcc_release_64
@@ -7,3 +7,4 @@ CXX=/opt/rh/devtoolset-7/root/usr/bin/g++
 --enable-mergelibs
 --disable-dconf
 --enable-python=internal
+--enable-atspi-tests
diff --git a/include/test/a11y/AccessibilityTools.hxx 
b/include/test/a11y/AccessibilityTools.hxx
index 38a76ce407f1..5235faedd377 100644
--- a/include/test/a11y/AccessibilityTools.hxx
+++ b/include/test/a11y/AccessibilityTools.hxx
@@ -30,6 +30,7 @@
 #include <com/sun/star/accessibility/XAccessible.hpp>
 #include <com/sun/star/accessibility/XAccessibleAction.hpp>
 #include <com/sun/star/accessibility/XAccessibleContext.hpp>
+#include <com/sun/star/accessibility/XAccessibleText.hpp>
 
 class OOO_DLLPUBLIC_TEST AccessibilityTools
 {
@@ -258,6 +259,7 @@ private:
     static OUString debugName(css::accessibility::XAccessible* xacc);
     static OUString debugName(const css::accessibility::AccessibleEventObject* 
evobj);
     static OUString debugName(css::accessibility::XAccessibleAction* xAct);
+    static OUString debugName(css::accessibility::XAccessibleText* xTxt);
 };
 
 CPPUNIT_NS_BEGIN
diff --git a/test/source/a11y/AccessibilityTools.cxx 
b/test/source/a11y/AccessibilityTools.cxx
index 33270505f3a6..b51c7cbac239 100644
--- a/test/source/a11y/AccessibilityTools.cxx
+++ b/test/source/a11y/AccessibilityTools.cxx
@@ -700,6 +700,12 @@ OUString 
AccessibilityTools::debugName(accessibility::XAccessibleAction* xAct)
     return r.makeStringAndClear();
 }
 
+OUString AccessibilityTools::debugName(accessibility::XAccessibleText* xTxt)
+{
+    uno::Reference<accessibility::XAccessibleContext> xCtx(xTxt, 
uno::UNO_QUERY);
+    return debugName(xCtx.get());
+}
+
 OUString AccessibilityTools::debugName(const 
accessibility::AccessibleEventObject* evobj)
 {
     return "(AccessibleEventObject) { id=" + getEventIdName(evobj->EventId)
diff --git a/vcl/CppunitTest_vcl_gtk3_a11y.mk b/vcl/CppunitTest_vcl_gtk3_a11y.mk
new file mode 100644
index 000000000000..0981a21857d2
--- /dev/null
+++ b/vcl/CppunitTest_vcl_gtk3_a11y.mk
@@ -0,0 +1,61 @@
+# -*- Mode: makefile-gmake; tab-width: 4; indent-tabs-mode: t -*-
+#
+# 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/.
+#
+
+# hack plugging into the CppunitTest machinery yet using a xvfb-wrapper GTK3 
run
+$(call gb_CppunitTest_get_target,vcl_gtk3_a11y) : gb_TEST_ENV_VARS += 
SAL_USE_VCLPLUGIN=gtk3
+ifeq (,$(VCL_GTK3_TESTS_NO_XVFB))
+$(call gb_CppunitTest_get_target,vcl_gtk3_a11y) : \
+       ICECREAM_RUN += $(XVFB_RUN) $(DBUS_LAUNCH) --exit-with-session
+endif
+
+$(eval $(call gb_CppunitTest_CppunitTest,vcl_gtk3_a11y))
+
+$(eval $(call gb_CppunitTest_add_exception_objects,vcl_gtk3_a11y, \
+       vcl/qa/cppunit/a11y/atspi2/atspiwrapper \
+       vcl/qa/cppunit/a11y/atspi2/atspi2 \
+       vcl/qa/cppunit/a11y/atspi2/atspi2text \
+))
+
+$(eval $(call gb_CppunitTest_set_include,vcl_gtk3_a11y,\
+       $$(INCLUDE) \
+       $$(ATSPI2_CFLAGS) \
+))
+
+$(eval $(call gb_CppunitTest_add_libs,vcl_gtk3_a11y,\
+       $$(ATSPI2_LIBS) \
+))
+
+$(eval $(call gb_CppunitTest_use_libraries,vcl_gtk3_a11y, \
+       sal \
+       cppu \
+       subsequenttest \
+       test \
+       i18nlangtag \
+       unotest \
+       vcl \
+))
+
+$(eval $(call gb_CppunitTest_use_externals,vcl_gtk3_a11y,\
+       boost_headers \
+))
+
+$(eval $(call gb_CppunitTest_use_api,vcl_gtk3_a11y,\
+       offapi \
+       udkapi \
+))
+
+$(eval $(call gb_CppunitTest_use_sdk_api,vcl_gtk3_a11y))
+$(eval $(call gb_CppunitTest_use_rdb,vcl_gtk3_a11y,services))
+$(eval $(call gb_CppunitTest_use_ure,vcl_gtk3_a11y))
+$(eval $(call gb_CppunitTest_use_vcl,vcl_gtk3_a11y))
+
+$(eval $(call gb_CppunitTest_use_instdir_configuration,vcl_gtk3_a11y))
+$(eval $(call gb_CppunitTest_use_common_configuration,vcl_gtk3_a11y))
+
+# vim: set noet sw=4 ts=4:
diff --git a/vcl/Module_vcl.mk b/vcl/Module_vcl.mk
index 5c6fbe987430..704b324622fe 100644
--- a/vcl/Module_vcl.mk
+++ b/vcl/Module_vcl.mk
@@ -82,6 +82,12 @@ ifneq ($(ENABLE_GTK3),)
 $(eval $(call gb_Module_add_targets,vcl,\
     Library_vclplug_gtk3 \
 ))
+
+ifneq ($(ENABLE_ATSPI_TESTS),)
+$(eval $(call gb_Module_add_check_targets,vcl,\
+    CppunitTest_vcl_gtk3_a11y \
+))
+endif
 endif
 
 ifneq ($(ENABLE_GTK4),)
diff --git a/vcl/qa/cppunit/a11y/atspi2/atspi2.cxx 
b/vcl/qa/cppunit/a11y/atspi2/atspi2.cxx
new file mode 100644
index 000000000000..bca752e85cbe
--- /dev/null
+++ b/vcl/qa/cppunit/a11y/atspi2/atspi2.cxx
@@ -0,0 +1,498 @@
+/* -*- 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)
+{
+    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(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);
+        MAP(BUTTON_DROPDOWN, PUSH_BUTTON);
+#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
+
+        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(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(sal_Int16 nRelation)
+{
+    switch (nRelation)
+    {
+        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::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;
+    }
+
+    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 auto nLORole = mapToAtspiRole(xLOContext->getAccessibleRole());
+    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, pAtspiRelation.getNTargets());
+
+        if (recurseFlags & RecurseFlags::RELATIONS_TARGETS)
+        {
+            for (auto j = decltype(nLOTargetsCount){ 0 }; j < nLOTargetsCount; 
j++)
+            {
+                uno::Reference<accessibility::XAccessible> 
xLOTarget(xLORelation.TargetSet[j],
+                                                                     
uno::UNO_QUERY_THROW);
+                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");
+
+    /* 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: */
diff --git a/vcl/qa/cppunit/a11y/atspi2/atspi2.hxx 
b/vcl/qa/cppunit/a11y/atspi2/atspi2.hxx
new file mode 100644
index 000000000000..87c0b698f3ab
--- /dev/null
+++ b/vcl/qa/cppunit/a11y/atspi2/atspi2.hxx
@@ -0,0 +1,45 @@
+/* -*- 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/.
+ */
+
+#pragma once
+
+#include <com/sun/star/accessibility/XAccessible.hpp>
+#include <com/sun/star/accessibility/XAccessibleContext.hpp>
+#include <com/sun/star/accessibility/XAccessibleText.hpp>
+#include <com/sun/star/uno/Reference.h>
+
+#include "atspi2testbase.hxx"
+#include "atspiwrapper.hxx"
+
+class Atspi2TestTree : public Atspi2TestBase
+{
+protected:
+    enum RecurseFlags : sal_uInt16
+    {
+        NONE = 0,
+        PARENT = 1 << 0,
+        RELATIONS_TARGETS = 1 << 1,
+        ALL = 0xffff
+    };
+
+    static sal_Int64
+    swChildIndex(css::uno::Reference<css::accessibility::XAccessibleContext> 
xContext);
+    static void
+    testSwScroll(const 
css::uno::Reference<css::accessibility::XAccessibleContext>& xLOContext,
+                 const Atspi::Accessible& pAtspiAccessible);
+    static void
+    compareObjects(const css::uno::Reference<css::accessibility::XAccessible>& 
xLOAccessible,
+                   const Atspi::Accessible& pAtspiAccessible, const sal_uInt16 
recurseFlags);
+    static void
+    compareTrees(const css::uno::Reference<css::accessibility::XAccessible>& 
xLOAccessible,
+                 const Atspi::Accessible& xAtspiAccessible, const int depth = 
0);
+    static void
+    compareTextObjects(const 
css::uno::Reference<css::accessibility::XAccessibleText>& xLOText,
+                       const Atspi::Text& pAtspiText);
+};
diff --git a/vcl/qa/cppunit/a11y/atspi2/atspi2testbase.hxx 
b/vcl/qa/cppunit/a11y/atspi2/atspi2testbase.hxx
new file mode 100644
index 000000000000..12a58da79d4b
--- /dev/null
+++ b/vcl/qa/cppunit/a11y/atspi2/atspi2testbase.hxx
@@ -0,0 +1,94 @@
+/* -*- 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/.
+ */
+
+#pragma once
+
+#include <vcl/svapp.hxx>
+
+#include <test/a11y/accessibletestbase.hxx>
+#include <test/a11y/AccessibilityTools.hxx>
+
+#include "atspiwrapper.hxx"
+
+/**
+ * @brief Base class for AT-SPI2 tests.
+ *
+ * This provides the basis for interacting with AT-SPI2, including getting the 
object representing
+ * our application, and facility for obtaining a top-level window of that app.
+ */
+class Atspi2TestBase : public test::AccessibleTestBase
+{
+protected:
+    Atspi::Accessible m_pAtspiApp;
+
+    static Atspi::Accessible getApp(const std::string_view appName)
+    {
+        std::cout << "Looking for AT-SPI application \"" << appName << "\"" << 
std::endl;
+        const auto nDesktops = atspi_get_desktop_count();
+        for (auto desktopId = decltype(nDesktops){ 0 }; desktopId < nDesktops; 
desktopId++)
+        {
+            Atspi::Accessible desktop(atspi_get_desktop(desktopId));
+
+            for (auto&& child : desktop)
+            {
+                if (!child) // is that useful?
+                    continue;
+                if (child.getRole() != ATSPI_ROLE_APPLICATION)
+                    continue;
+                const auto name = child.getName();
+                std::cout << "Found desktop child: " << name << std::endl;
+                if (appName != name)
+                {
+                    continue;
+                }
+                return std::move(child);
+            }
+        }
+        return nullptr;
+    }
+
+    static Atspi::Accessible getSelfApp()
+    {
+        const auto appFileName = Application::GetAppFileName();
+        const auto slash = appFileName.lastIndexOf('/');
+        const auto baseName = (slash >= 0) ? 
OUString(appFileName.subView(slash + 1)) : appFileName;
+        return getApp(baseName.getLength() > 0 ? baseName.toUtf8().getStr() : 
"cppunittester");
+    }
+
+protected:
+    Atspi::Accessible getWindow(const std::string_view windowName)
+    {
+        for (auto&& child : m_pAtspiApp)
+        {
+            const auto name = child.getName();
+            std::cout << "Found window: " << name << std::endl;
+            if (windowName == name)
+                return std::move(child);
+        }
+        return nullptr;
+    }
+
+public:
+    Atspi2TestBase()
+    {
+        if (!atspi_is_initialized())
+            atspi_init();
+    }
+
+    virtual void setUp() override
+    {
+        test::AccessibleTestBase::setUp();
+
+        AccessibilityTools::Await([this]() {
+            m_pAtspiApp = getSelfApp();
+            return bool(m_pAtspiApp);
+        });
+        assert(m_pAtspiApp);
+    }
+};
diff --git a/vcl/qa/cppunit/a11y/atspi2/atspi2text.cxx 
b/vcl/qa/cppunit/a11y/atspi2/atspi2text.cxx
new file mode 100644
index 000000000000..68ac59e6bf62
--- /dev/null
+++ b/vcl/qa/cppunit/a11y/atspi2/atspi2text.cxx
@@ -0,0 +1,1017 @@
+/* -*- 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 <com/sun/star/accessibility/AccessibleRelationType.hpp>
+#include <com/sun/star/accessibility/AccessibleTextType.hpp>
+#include <com/sun/star/accessibility/XAccessibleComponent.hpp>
+#include <com/sun/star/accessibility/XAccessibleText.hpp>
+#include <com/sun/star/accessibility/XAccessibleTextAttributes.hpp>
+#include <com/sun/star/accessibility/XAccessibleTextMarkup.hpp>
+
+#include <com/sun/star/awt/FontSlant.hpp>
+#include <com/sun/star/awt/FontStrikeout.hpp>
+#include <com/sun/star/awt/FontUnderline.hpp>
+#include <com/sun/star/style/CaseMap.hpp>
+#include <com/sun/star/style/LineSpacing.hpp>
+#include <com/sun/star/style/LineSpacingMode.hpp>
+#include <com/sun/star/style/ParagraphAdjust.hpp>
+#include <com/sun/star/style/TabStop.hpp>
+#include <com/sun/star/text/FontRelief.hpp>
+#include <com/sun/star/text/WritingMode2.hpp>
+#include <com/sun/star/text/TextMarkupType.hpp>
+
+#include <i18nlangtag/languagetag.hxx>
+#include <tools/UnitConversion.hxx>
+#include <rtl/character.hxx>
+
+#include <test/a11y/AccessibilityTools.hxx>
+
+#include "atspi2.hxx"
+#include "atspiwrapper.hxx"
+
+using namespace css;
+
+namespace
+{
+/** @brief Helper class to check text attributes are properly exported to 
Atspi.
+ *
+ * This kind of duplicates most of the logic in atktextattributes.cxx, but if 
we want to check the
+ * values are correct (which includes whether they are properly updated for 
example), we have to do
+ * this, even though it means quite some processing for some of the attributes.
+ * This has to be kept in sync with how atktextattributes.cxx exposes those 
attributes. */
+class AttributesChecker
+{
+private:
+    uno::Reference<accessibility::XAccessibleText> mxLOText;
+    Atspi::Text mxAtspiText;
+
+public:
+    AttributesChecker(const uno::Reference<accessibility::XAccessibleText>& 
xLOText,
+                      const Atspi::Text& xAtspiText)
+        : mxLOText(xLOText)
+        , mxAtspiText(xAtspiText)
+    {
+    }
+
+private:
+    // helper to validate a value represented as a single float in ATSPI
+    static bool implCheckFloat(std::string_view atspiValue, float expected)
+    {
+        float f;
+        char dummy;
+
+        CPPUNIT_ASSERT_EQUAL(1, sscanf(atspiValue.data(), "%g%c", &f, &dummy));
+        CPPUNIT_ASSERT_DOUBLES_EQUAL(expected, f, 1e-4);
+
+        return true;
+    }
+
+    // helper to check simple mappings between LO and ATSPI
+    template <typename T>
+    static bool implCheckMapping(const T loValue, const std::string_view 
atspiValue,
+                                 const std::unordered_map<T, 
std::string_view>& map,
+                                 const bool retIfMissing = false)
+    {
+        const auto& iter = map.find(loValue);
+        if (iter != map.end())
+        {
+            CPPUNIT_ASSERT_EQUAL(iter->second, atspiValue);
+            return true;
+        }
+        return retIfMissing;
+    }
+
+    // checkers, see atktextattributes.cxx
+    bool checkBoolean(std::string_view atspiValue, const beans::PropertyValue& 
property,
+                      const uno::Sequence<beans::PropertyValue>&)
+    {
+        if (property.Value.get<bool>())
+            CPPUNIT_ASSERT_EQUAL(std::string_view("true"), atspiValue);
+        else
+            CPPUNIT_ASSERT_EQUAL(std::string_view("false"), atspiValue);
+
+        return true;
+    }
+
+    bool checkString(std::string_view atspiValue, const beans::PropertyValue& 
property,
+                     const uno::Sequence<beans::PropertyValue>&)
+    {
+        CPPUNIT_ASSERT_EQUAL(property.Value.get<OUString>(), 
OUString::fromUtf8(atspiValue));
+        return true;
+    }
+
+    bool checkFloat(std::string_view atspiValue, const beans::PropertyValue& 
property,
+                    const uno::Sequence<beans::PropertyValue>&)
+    {
+        return implCheckFloat(atspiValue, property.Value.get<float>());
+    }
+
+    bool checkVariant(std::string_view atspiValue, const beans::PropertyValue& 
property,
+                      const uno::Sequence<beans::PropertyValue>&)
+    {
+        if (property.Value.get<short>() == style::CaseMap::SMALLCAPS)
+            CPPUNIT_ASSERT_EQUAL(std::string_view("small_caps"), atspiValue);
+        else
+            CPPUNIT_ASSERT_EQUAL(std::string_view("normal"), atspiValue);
+
+        return true;
+    }
+
+    // See Scale2String
+    bool checkScale(std::string_view atspiValue, const beans::PropertyValue& 
property,
+                    const uno::Sequence<beans::PropertyValue>&)
+    {
+        double v;
+        char dummy;
+
+        CPPUNIT_ASSERT_EQUAL(1, sscanf(atspiValue.data(), "%lg%c", &v, 
&dummy));
+        CPPUNIT_ASSERT_EQUAL(property.Value.get<sal_Int16>(), sal_Int16(v * 
100));
+
+        return true;
+    }
+
+    // see Escapement2VerticalAlign
+    bool checkVerticalAlign(std::string_view atspiValue, const 
beans::PropertyValue& property,
+                            const uno::Sequence<beans::PropertyValue>&)
+    {
+        const sal_Int16 n = property.Value.get<sal_Int16>();
+
+        if (n == 0)
+            CPPUNIT_ASSERT_EQUAL(std::string_view("baseline"), atspiValue);
+        else if (n == -101)
+            CPPUNIT_ASSERT_EQUAL(std::string_view("sub"), atspiValue);
+        else if (n == 101)
+            CPPUNIT_ASSERT_EQUAL(std::string_view("super"), atspiValue);
+        else
+        {
+            int v;
+            char dummy;
+            CPPUNIT_ASSERT_EQUAL(1, sscanf(atspiValue.data(), "%d%%%c", &v, 
&dummy));
+            CPPUNIT_ASSERT_EQUAL(int(n), v);
+        }
+
+        return true;
+    }
+
+    bool checkColor(std::string_view atspiValue, const beans::PropertyValue& 
property,
+                    const uno::Sequence<beans::PropertyValue>&)
+    {
+        auto color = property.Value.get<sal_Int32>();
+
+        if (color == -1) // automatic, use the component's color
+        {
+            uno::Reference<accessibility::XAccessibleComponent> 
xComponent(mxLOText,
+                                                                           
uno::UNO_QUERY);
+            if (xComponent.is())
+            {
+                if (property.Name == u"CharBackColor")
+                    color = xComponent->getBackground();
+                else if (property.Name == u"CharColor")
+                    color = xComponent->getForeground();
+            }
+        }
+
+        if (color != -1)
+        {
+            unsigned int r, g, b;
+            char dummy;
+
+            CPPUNIT_ASSERT_EQUAL(3, sscanf(atspiValue.data(), "%u,%u,%u%c", 
&r, &g, &b, &dummy));
+            CPPUNIT_ASSERT_EQUAL((color & 0xFFFFFF),
+                                 (static_cast<sal_Int32>(r) << 16 | 
static_cast<sal_Int32>(g) << 8
+                                  | static_cast<sal_Int32>(b)));
+            return true;
+        }
+
+        return false;
+    }
+
+    // See LineSpacing2LineHeight
+    bool checkLineHeight(std::string_view atspiValue, const 
beans::PropertyValue& property,
+                         const uno::Sequence<beans::PropertyValue>&)
+    {
+        const auto lineSpacing = property.Value.get<style::LineSpacing>();
+        char dummy;
+
+        if (lineSpacing.Mode == style::LineSpacingMode::PROP)
+        {
+            int h;
+
+            CPPUNIT_ASSERT_EQUAL(1, sscanf(atspiValue.data(), "%d%%%c", &h, 
&dummy));
+            CPPUNIT_ASSERT_EQUAL(lineSpacing.Height, sal_Int16(h));
+        }
+        else if (lineSpacing.Mode == style::LineSpacingMode::FIX)
+        {
+            double pt;
+
+            CPPUNIT_ASSERT_EQUAL(1, sscanf(atspiValue.data(), "%lgpt%c", &pt, 
&dummy));
+            
CPPUNIT_ASSERT_DOUBLES_EQUAL(convertMm100ToPoint<double>(lineSpacing.Height), 
pt, 1e-4);
+            CPPUNIT_ASSERT_EQUAL(lineSpacing.Height, 
sal_Int16(convertPointToMm100(pt)));
+        }
+        else
+            return false;
+
+        return true;
+    }
+
+    bool checkStretch(std::string_view atspiValue, const beans::PropertyValue& 
property,
+                      const uno::Sequence<beans::PropertyValue>&)
+    {
+        const auto n = property.Value.get<sal_Int16>();
+
+        if (n < 0)
+            CPPUNIT_ASSERT_EQUAL(std::string_view("condensed"), atspiValue);
+        else if (n > 0)
+            CPPUNIT_ASSERT_EQUAL(std::string_view("expanded"), atspiValue);
+        else
+            CPPUNIT_ASSERT_EQUAL(std::string_view("normal"), atspiValue);
+
+        return true;
+    }
+
+    bool checkStyle(std::string_view atspiValue, const beans::PropertyValue& 
property,
+                    const uno::Sequence<beans::PropertyValue>&)
+    {
+        return implCheckMapping(
+            property.Value.get<awt::FontSlant>(), atspiValue,
+            { { awt::FontSlant_NONE, std::string_view("normal") },
+              { awt::FontSlant_OBLIQUE, std::string_view("oblique") },
+              { awt::FontSlant_ITALIC, std::string_view("italic") },
+              { awt::FontSlant_REVERSE_OBLIQUE, std::string_view("reverse 
oblique") },
+              { awt::FontSlant_REVERSE_ITALIC, std::string_view("reverse 
italic") } });
+    }
+
+    bool checkJustification(std::string_view atspiValue, const 
beans::PropertyValue& property,
+                            const uno::Sequence<beans::PropertyValue>&)
+    {
+        return 
implCheckMapping(static_cast<style::ParagraphAdjust>(property.Value.get<short>()),
+                                atspiValue,
+                                { { style::ParagraphAdjust_LEFT, 
std::string_view("left") },
+                                  { style::ParagraphAdjust_RIGHT, 
std::string_view("right") },
+                                  { style::ParagraphAdjust_BLOCK, 
std::string_view("fill") },
+                                  { style::ParagraphAdjust_STRETCH, 
std::string_view("fill") },
+                                  { style::ParagraphAdjust_CENTER, 
std::string_view("center") } });
+    }
+
+    bool checkShadow(std::string_view atspiValue, const beans::PropertyValue& 
property,
+                     const uno::Sequence<beans::PropertyValue>&)
+    {
+        if (property.Value.get<bool>())
+            CPPUNIT_ASSERT_EQUAL(std::string_view("black"), atspiValue);
+        else
+            CPPUNIT_ASSERT_EQUAL(std::string_view("none"), atspiValue);
+
+        return true;
+    }
+
+    bool checkLanguage(std::string_view atspiValue, const 
beans::PropertyValue& property,
+                       const uno::Sequence<beans::PropertyValue>&)
+    {
+        auto aLocale = property.Value.get<lang::Locale>();
+        LanguageTag aLanguageTag(aLocale);
+
+        CPPUNIT_ASSERT_EQUAL(OUString(aLanguageTag.getLanguage() + "-"
+                                      + 
aLanguageTag.getCountry().toAsciiLowerCase()),
+                             OUString::fromUtf8(atspiValue));
+
+        return true;
+    }
+
+    bool checkTextRotation(std::string_view atspiValue, const 
beans::PropertyValue& property,
+                           const uno::Sequence<beans::PropertyValue>&)
+    {
+        return implCheckFloat(atspiValue, property.Value.get<sal_Int16>() / 
10.0f);
+    }
+
+    bool checkWeight(std::string_view atspiValue, const beans::PropertyValue& 
property,
+                     const uno::Sequence<beans::PropertyValue>&)
+    {
+        return implCheckFloat(atspiValue, property.Value.get<float>() * 4);
+    }
+
+    bool checkCMMValue(std::string_view atspiValue, const 
beans::PropertyValue& property,
+                       const uno::Sequence<beans::PropertyValue>&)
+    {
+        double v;
+        char dummy;
+
+        // CMM is 1/100th of a mm
+        CPPUNIT_ASSERT_EQUAL(1, sscanf(atspiValue.data(), "%lgmm%c", &v, 
&dummy));
+        CPPUNIT_ASSERT_DOUBLES_EQUAL(property.Value.get<sal_Int32>() * 0.01, 
v, 1e-4);
+
+        return true;
+    }
+
+    bool checkDirection(std::string_view atspiValue, const 
beans::PropertyValue& property,
+                        const uno::Sequence<beans::PropertyValue>&)
+    {
+        return implCheckMapping(property.Value.get<sal_Int16>(), atspiValue,
+                                { { text::WritingMode2::TB_LR, 
std::string_view("ltr") },
+                                  { text::WritingMode2::LR_TB, 
std::string_view("ltr") },
+                                  { text::WritingMode2::TB_RL, 
std::string_view("rtl") },
+                                  { text::WritingMode2::RL_TB, 
std::string_view("rtl") },
+                                  { text::WritingMode2::PAGE, 
std::string_view("none") } });
+    }
+
+    bool checkWritingMode(std::string_view atspiValue, const 
beans::PropertyValue& property,
+                          const uno::Sequence<beans::PropertyValue>&)
+    {
+        return implCheckMapping(property.Value.get<sal_Int16>(), atspiValue,
+                                { { text::WritingMode2::TB_LR, 
std::string_view("tb-lr") },
+                                  { text::WritingMode2::LR_TB, 
std::string_view("lr-tb") },
+                                  { text::WritingMode2::TB_RL, 
std::string_view("tb-rl") },
+                                  { text::WritingMode2::RL_TB, 
std::string_view("rl-tb") },
+                                  { text::WritingMode2::PAGE, 
std::string_view("none") } });
+    }
+
+    static const beans::PropertyValue*
+    findProperty(const uno::Sequence<beans::PropertyValue>& properties, 
std::u16string_view name)
+    {
+        auto prop = std::find_if(properties.begin(), properties.end(),
+                                 [name](auto& p) { return p.Name == name; });
+        if (prop == properties.end())
+            prop = nullptr;
+        return prop;
+    }
+
+    // same as findProperty() above, but with a fast path is @p property is a 
match
+    static const beans::PropertyValue*
+    findProperty(const beans::PropertyValue* property,
+                 const uno::Sequence<beans::PropertyValue>& properties, 
std::u16string_view name)
+    {
+        if (property->Name == name)
+            return property;
+        return findProperty(properties, name);
+    }
+
+    bool checkFontEffect(std::string_view atspiValue, const 
beans::PropertyValue& property,
+                         const uno::Sequence<beans::PropertyValue>& 
loProperties)
+    {
+        if (auto charContoured = findProperty(&property, loProperties, 
u"CharContoured");
+            charContoured && charContoured->Value.get<bool>())
+        {
+            CPPUNIT_ASSERT_EQUAL(std::string_view("outline"), atspiValue);
+            return true;
+        }
+
+        if (auto charRelief = findProperty(&property, loProperties, 
u"CharRelief"))
+        {
+            return implCheckMapping(charRelief->Value.get<sal_Int16>(), 
atspiValue,
+                                    { { text::FontRelief::NONE, 
std::string_view("none") },
+                                      { text::FontRelief::EMBOSSED, 
std::string_view("emboss") },
+                                      { text::FontRelief::ENGRAVED, 
std::string_view("engrave") } },
+                                    true);
+        }
+
+        return false;
+    }
+
+    bool checkTextDecoration(std::string_view atspiValue, const 
beans::PropertyValue&,
+                             const uno::Sequence<beans::PropertyValue>& 
loProperties)
+    {
+        if (atspiValue == "none")
+        {
+            if (auto prop = findProperty(loProperties, u"CharFlash"))
+                CPPUNIT_ASSERT_EQUAL(false, prop->Value.get<bool>());
+            if (auto prop = findProperty(loProperties, u"CharUnderline"))
+                CPPUNIT_ASSERT_EQUAL(css::awt::FontUnderline::NONE, 
prop->Value.get<sal_Int16>());
+            if (auto prop = findProperty(loProperties, u"CharStrikeout"))
+                CPPUNIT_ASSERT(prop->Value.get<sal_Int16>() == 
css::awt::FontStrikeout::NONE
+                               || prop->Value.get<sal_Int16>()
+                                      == css::awt::FontStrikeout::DONTKNOW);
+        }
+        else
+        {
+            sal_Int32 nIndex = 0;
+            const auto atspiValueString = OUString::fromUtf8(atspiValue);
+
+            do
+            {
+                OUString atspiToken = atspiValueString.getToken(0, ' ', 
nIndex);
+                const beans::PropertyValue* prop;
+
+                if (atspiToken == "blink")
+                {
+                    CPPUNIT_ASSERT((prop = findProperty(loProperties, 
u"CharFlash")));
+                    CPPUNIT_ASSERT_EQUAL(true, prop->Value.get<bool>());
+                }
+                else if (atspiToken == "underline")
+                {
+                    CPPUNIT_ASSERT((prop = findProperty(loProperties, 
u"CharUnderline")));
+                    CPPUNIT_ASSERT(prop->Value.get<sal_Int16>() != 
css::awt::FontUnderline::NONE);
+                }
+                else if (atspiToken == "underline")
+                {
+                    CPPUNIT_ASSERT((prop = findProperty(loProperties, 
u"CharStrikeout")));
+                    CPPUNIT_ASSERT(prop->Value.get<sal_Int16>() != 
css::awt::FontStrikeout::NONE);
+                    CPPUNIT_ASSERT(prop->Value.get<sal_Int16>()
+                                   != css::awt::FontStrikeout::DONTKNOW);
+                }
+                else
+                {
+                    CPPUNIT_ASSERT_MESSAGE(
+                        OUString("Unknown text decoration \"" + 
atspiToken).toUtf8().getStr(),
+                        false);
+                }
+            } while (nIndex > 0);
+        }
+
+        return true;
+    }
+
+    static bool implCheckTabStops(std::string_view atspiValue, const 
beans::PropertyValue& property,
+                                  const bool defaultTabs)
+    {
+        uno::Sequence<style::TabStop> theTabStops;
+
+        if (property.Value >>= theTabStops)
+        {
+            sal_Unicode lastFillChar = ' ';
+            const char* p = atspiValue.data();
+
+            for (const auto& rTabStop : std::as_const(theTabStops))
+            {
+                if ((style::TabAlign_DEFAULT == rTabStop.Alignment) != 
defaultTabs)
+                    continue;
+
+                const char* tab_align = "";
+                switch (rTabStop.Alignment)
+                {
+                    case style::TabAlign_LEFT:
+                        tab_align = "left ";
+                        break;
+                    case style::TabAlign_CENTER:
+                        tab_align = "center ";
+                        break;
+                    case style::TabAlign_RIGHT:
+                        tab_align = "right ";
+                        break;
+                    case style::TabAlign_DECIMAL:
+                        tab_align = "decimal ";
+                        break;
+                    default:
+                        break;
+                }
+
+                const char* lead_char = "";
+                if (rTabStop.FillChar != lastFillChar)
+                {
+                    lastFillChar = rTabStop.FillChar;
+                    switch (lastFillChar)
+                    {
+                        case ' ':
+                            lead_char = "blank ";
+                            break;
+
+                        case '.':
+                            lead_char = "dotted ";
+                            break;
+
+                        case '-':
+                            lead_char = "dashed ";
+                            break;
+
+                        case '_':
+                            lead_char = "lined ";
+                            break;
+
+                        default:
+                            lead_char = "custom ";
+                            break;
+                    }
+                }
+
+                // check this matches "<lead_char><tab_align><position>mm"
+                CPPUNIT_ASSERT_EQUAL(0, strncmp(p, lead_char, 
strlen(lead_char)));
+                p += strlen(lead_char);
+                CPPUNIT_ASSERT_EQUAL(0, strncmp(p, tab_align, 
strlen(tab_align)));
+                p += strlen(tab_align);
+                float atspiPosition;
+                int nConsumed;
+                CPPUNIT_ASSERT_EQUAL(1, sscanf(p, "%gmm%n", &atspiPosition, 
&nConsumed));
+                CPPUNIT_ASSERT_DOUBLES_EQUAL(float(rTabStop.Position * 0.01f), 
atspiPosition, 1e-4);
+                p += nConsumed;
+
+                if (*p)
+                {
+                    CPPUNIT_ASSERT_EQUAL(' ', *p);
+                    p++;
+                }
+            }
+
+            // make sure there isn't garbage at the end
+            CPPUNIT_ASSERT_EQUAL(char(0), *p);
+
+            return true;
+        }
+
+        return false;
+    }
+
+    bool checkDefaultTabStops(std::string_view atspiValue, const 
beans::PropertyValue& property,
+                              const uno::Sequence<beans::PropertyValue>&)
+    {
+        return implCheckTabStops(atspiValue, property, true);
+    }
+
+    bool checkTabStops(std::string_view atspiValue, const 
beans::PropertyValue& property,
+                       const uno::Sequence<beans::PropertyValue>&)
+    {
+        return implCheckTabStops(atspiValue, property, false);
+    }
+
+public:
+    // runner code
+    bool check(const uno::Sequence<beans::PropertyValue>& xLOAttributeList,
+               const std::unordered_map<std::string, std::string>& 
xAtspiAttributeList)
+    {
+        const struct
+        {
+            const char* loName;
+            const char* atspiName;
+            bool (AttributesChecker::*checkValue)(
+                std::string_view atspiValue, const beans::PropertyValue& 
property,
+                const uno::Sequence<beans::PropertyValue>& loAttributeList);
+        } atspiMap[]
+            = { //  LO name        AT-SPI name       check function
+                { "CharBackColor", "bg-color", &AttributesChecker::checkColor 
},
+                { "CharCaseMap", "variant", &AttributesChecker::checkVariant },
+                { "CharColor", "fg-color", &AttributesChecker::checkColor },
+                { "CharContoured", "font-effect", 
&AttributesChecker::checkFontEffect },
+                { "CharEscapement", "vertical-align", 
&AttributesChecker::checkVerticalAlign },
+                { "CharFlash", "text-decoration", 
&AttributesChecker::checkTextDecoration },
+                { "CharFontName", "family-name", 
&AttributesChecker::checkString },
+                { "CharHeight", "size", &AttributesChecker::checkFloat },
+                { "CharHidden", "invisible", &AttributesChecker::checkBoolean 
},
+                { "CharKerning", "stretch", &AttributesChecker::checkStretch },
+                { "CharLocale", "language", &AttributesChecker::checkLanguage 
},
+                { "CharPosture", "style", &AttributesChecker::checkStyle },
+                { "CharRelief", "font-effect", 
&AttributesChecker::checkFontEffect },
+                { "CharRotation", "text-rotation", 
&AttributesChecker::checkTextRotation },
+                { "CharScaleWidth", "scale", &AttributesChecker::checkScale },
+                { "CharShadowed", "text-shadow", 
&AttributesChecker::checkShadow },
+                { "CharStrikeout", "text-decoration", 
&AttributesChecker::checkTextDecoration },
+                { "CharUnderline", "text-decoration", 
&AttributesChecker::checkTextDecoration },
+                { "CharWeight", "weight", &AttributesChecker::checkWeight },
+                { "MMToPixelRatio", "mm-to-pixel-ratio", 
&AttributesChecker::checkFloat },
+                { "ParaAdjust", "justification", 
&AttributesChecker::checkJustification },
+                { "ParaBottomMargin", "pixels-below-lines", 
&AttributesChecker::checkCMMValue },
+                { "ParaFirstLineIndent", "indent", 
&AttributesChecker::checkCMMValue },
+                { "ParaLeftMargin", "left-margin", 
&AttributesChecker::checkCMMValue },
+                { "ParaLineSpacing", "line-height", 
&AttributesChecker::checkLineHeight },
+                { "ParaRightMargin", "right-margin", 
&AttributesChecker::checkCMMValue },
+                { "ParaStyleName", "paragraph-style", 
&AttributesChecker::checkString },
+                { "ParaTabStops", "tab-interval", 
&AttributesChecker::checkDefaultTabStops },
+                { "ParaTabStops", "tab-stops", 
&AttributesChecker::checkTabStops },
+                { "ParaTopMargin", "pixels-above-lines", 
&AttributesChecker::checkCMMValue },
+                { "WritingMode", "direction", 
&AttributesChecker::checkDirection },
+                { "WritingMode", "writing-mode", 
&AttributesChecker::checkWritingMode }
+              };
+
+        for (const auto& prop : xLOAttributeList)
+        {
+            std::cout << "found run attribute: " << prop.Name << "=" << 
prop.Value << std::endl;
+
+            /* we need to loop on all entries because there might be more than 
one for a single
+             * property */
+            for (const auto& entry : atspiMap)
+            {
+                if (!prop.Name.equalsAscii(entry.loName))
+                    continue;
+
+                const auto atspiIter = 
xAtspiAttributeList.find(entry.atspiName);
+                /* we use an empty value if there isn't one, which can happen 
if the value cannot
+                 * be represented by Atspi, or if the actual LO value is also 
empty */
+                std::string atspiValue;
+                if (atspiIter != xAtspiAttributeList.end())
+                    atspiValue = atspiIter->second;
+
+                std::cout << "  matching atspi attribute is: " << 
entry.atspiName << "="
+                          << atspiValue << std::endl;
+                CPPUNIT_ASSERT(
+                    std::invoke(entry.checkValue, this, atspiValue, prop, 
xLOAttributeList));
+            }
+        }
+
+        return true;
+    }
+};
+}
+
+/* LO doesn't implement it itself, but ATK provides a fallback.  Add a test 
here merely for the
+ * future when we have a direct AT-SPI implementation for e.g. GTK4.
+ * Just like atk-adaptor, we compute the bounding box by combining extents for 
each character
+ * in the range */
+static awt::Rectangle getRangeBounds(const 
uno::Reference<accessibility::XAccessibleText>& xText,
+                                     sal_Int32 startOffset, sal_Int32 
endOffset)
+{
+    awt::Rectangle bounds;
+
+    for (auto offset = startOffset; offset < endOffset; offset++)
+    {
+        const auto chBounds = xText->getCharacterBounds(offset);
+        if (offset == 0)
+            bounds = chBounds;
+        else
+        {
+            const auto x = std::min(bounds.X, chBounds.X);
+            const auto y = std::min(bounds.Y, chBounds.Y);
+            bounds.Width = std::max(bounds.X + bounds.Width, chBounds.X + 
chBounds.Width) - x;
+            bounds.Height = std::max(bounds.Y + bounds.Height, chBounds.Y + 
chBounds.Height) - y;
+            bounds.X = x;
+            bounds.Y = y;
+        }
+    }
+
+    return bounds;
+}
+
+void Atspi2TestTree::compareTextObjects(
+    const uno::Reference<accessibility::XAccessibleText>& xLOText, const 
Atspi::Text& pAtspiText)
+{
+    CPPUNIT_ASSERT_EQUAL(xLOText->getCharacterCount(), 
sal_Int32(pAtspiText.getCharacterCount()));
+    CPPUNIT_ASSERT_EQUAL(xLOText->getCaretPosition(), 
sal_Int32(pAtspiText.getCaretOffset()));
+    CPPUNIT_ASSERT_EQUAL(xLOText->getText(), 
OUString::fromUtf8(pAtspiText.getText(0, -1)));
+
+    const auto characterCount = xLOText->getCharacterCount();
+    auto offset = decltype(characterCount){ 0 };
+    auto atspiPosition = Atspi::Point{ 0, 0 };
+
+    AttributesChecker attributesChecker(xLOText, pAtspiText);
+
+    auto xLOTextAttrs
+        = uno::Reference<accessibility::XAccessibleTextAttributes>(xLOText, 
uno::UNO_QUERY);
+    // default text attributes
+    if (xLOTextAttrs.is())
+    {
+        const auto aAttributeList = 
xLOTextAttrs->getDefaultAttributes(uno::Sequence<OUString>());
+        const auto atspiAttributeList = pAtspiText.getDefaultAttributes();
+
+        attributesChecker.check(aAttributeList, atspiAttributeList);
+    }
+
+    if (characterCount > 0)
+    {
+        const auto atspiComponent = pAtspiText.queryComponent();
+        atspiPosition = atspiComponent.getPosition(ATSPI_COORD_TYPE_WINDOW);
+    }
+
+    // text run attributes
+    uno::Reference<accessibility::XAccessibleTextMarkup> xTextMarkup(xLOText, 
uno::UNO_QUERY);
+    while (offset < characterCount)
+    {
+        // message for the assertions so we know where it comes from
+        OString offsetMsg(OString::Concat("in ") + 
AccessibilityTools::debugString(xLOText).c_str()
+                          + " at offset " + OString::number(offset));
+
+        uno::Sequence<beans::PropertyValue> aAttributeList;
+
+        if (xLOTextAttrs.is())
+            aAttributeList = xLOTextAttrs->getRunAttributes(offset, 
uno::Sequence<OUString>());
+        else
+            aAttributeList = xLOText->getCharacterAttributes(offset, 
uno::Sequence<OUString>());
+
+        int atspiStartOffset = 0, atspiEndOffset = 0;
+        const auto atspiAttributeList
+            = pAtspiText.getAttributeRun(offset, false, &atspiStartOffset, 
&atspiEndOffset);
+
+        accessibility::TextSegment aTextSegment
+            = xLOText->getTextAtIndex(offset, 
accessibility::AccessibleTextType::ATTRIBUTE_RUN);
+
+        /* Handle misspelled text and tracked changes as atktext.cxx does as 
it affects the run
+         * boundaries.  Also check the attributes are properly forwarded. */
+        if (xTextMarkup.is())
+        {
+            const struct
+            {
+                sal_Int32 markupType;
+                const char* atspiAttribute;
+                const char* atspiValue;
+            } aTextMarkupTypes[]
+                = { { text::TextMarkupType::SPELLCHECK, "text-spelling", 
"misspelled" },
+                    { text::TextMarkupType::TRACK_CHANGE_INSERTION, 
"text-tracked-change",
+                      "insertion" },
+                    { text::TextMarkupType::TRACK_CHANGE_DELETION, 
"text-tracked-change",
+                      "deletion" },
+                    { text::TextMarkupType::TRACK_CHANGE_FORMATCHANGE, 
"text-tracked-change",
+                      "attribute-change" } };
+
+            for (const auto& aTextMarkupType : aTextMarkupTypes)
+            {
+                const auto nTextMarkupCount
+                    = 
xTextMarkup->getTextMarkupCount(aTextMarkupType.markupType);
+                if (nTextMarkupCount <= 0)
+                    continue;
+
+                for (auto nTextMarkupIndex = decltype(nTextMarkupCount){ 0 };
+                     nTextMarkupIndex < nTextMarkupCount; ++nTextMarkupIndex)
+                {
+                    const auto aMarkupTextSegment
+                        = xTextMarkup->getTextMarkup(nTextMarkupIndex, 
aTextMarkupType.markupType);
+                    if (aMarkupTextSegment.SegmentStart > offset)
+                    {
+                        aTextSegment.SegmentEnd
+                            = ::std::min(aTextSegment.SegmentEnd, 
aMarkupTextSegment.SegmentStart);
+                        break; // no further iteration.
+                    }
+                    else if (offset < aMarkupTextSegment.SegmentEnd)
+                    {
+                        // text markup at <offset>
+                        aTextSegment.SegmentStart = 
::std::max(aTextSegment.SegmentStart,
+                                                               
aMarkupTextSegment.SegmentStart);
+                        aTextSegment.SegmentEnd
+                            = ::std::min(aTextSegment.SegmentEnd, 
aMarkupTextSegment.SegmentEnd);
+                        // check the attribute is set
+                        const auto atspiIter
+                            = 
atspiAttributeList.find(aTextMarkupType.atspiAttribute);
+                        CPPUNIT_ASSERT_MESSAGE(offsetMsg.getStr(),
+                                               atspiIter != 
atspiAttributeList.end());
+                        CPPUNIT_ASSERT_EQUAL_MESSAGE(offsetMsg.getStr(),
+                                                     
std::string_view(aTextMarkupType.atspiValue),
+                                                     
std::string_view(atspiIter->second));
+                        break; // no further iteration needed.
+                    }
+                    else
+                    {
+                        aTextSegment.SegmentStart
+                            = ::std::max(aTextSegment.SegmentStart, 
aMarkupTextSegment.SegmentEnd);
+                        // continue iteration.
+                    }
+                }
+            }
+        }
+
+        CPPUNIT_ASSERT_EQUAL_MESSAGE(offsetMsg.getStr(), 
aTextSegment.SegmentStart,
+                                     sal_Int32(atspiStartOffset));
+        CPPUNIT_ASSERT_EQUAL_MESSAGE(offsetMsg.getStr(), 
aTextSegment.SegmentEnd,
+                                     sal_Int32(atspiEndOffset));
+
+        attributesChecker.check(aAttributeList, atspiAttributeList);
+
+        CPPUNIT_ASSERT_MESSAGE(offsetMsg.getStr(), aTextSegment.SegmentEnd > 
offset);
+        offset = aTextSegment.SegmentEnd;
+    }
+
+    // loop over each character
+    for (offset = 0; offset < characterCount;)
+    {
+        const auto aTextSegment
+            = xLOText->getTextAtIndex(offset, 
accessibility::AccessibleTextType::CHARACTER);
+        OString offsetMsg(OString::Concat("in ") + 
AccessibilityTools::debugString(xLOText).c_str()
+                          + " at offset " + OString::number(offset));
+
+        // getCharacterAtOffset()
+        sal_Int32 nChOffset = 0;
+        sal_Int32 cp = aTextSegment.SegmentText.iterateCodePoints(&nChOffset);
+        /* do not check unpaired surrogates, because they are unlikely to make 
any sense and LO's
+         * GTK VCL doesn't like them */
+        if (!rtl::isSurrogate(cp))
+            CPPUNIT_ASSERT_EQUAL_MESSAGE(offsetMsg.getStr(), cp,
+                                         
pAtspiText.getCharacterAtOffset(offset));
+
+        // getTextAtOffset()
+        const struct
+        {
+            sal_Int16 loTextType;
+            AtspiTextBoundaryType atspiBoundaryType;
+        } textTypeMap[] = {
+            { accessibility::AccessibleTextType::CHARACTER, 
ATSPI_TEXT_BOUNDARY_CHAR },
+            { accessibility::AccessibleTextType::WORD, 
ATSPI_TEXT_BOUNDARY_WORD_START },
+            { accessibility::AccessibleTextType::SENTENCE, 
ATSPI_TEXT_BOUNDARY_SENTENCE_START },
+            { accessibility::AccessibleTextType::LINE, 
ATSPI_TEXT_BOUNDARY_LINE_START },
+        };
+        for (const auto& pair : textTypeMap)
+        {
+            auto loTextSegment = xLOText->getTextAtIndex(offset, 
pair.loTextType);
+            const auto atspiTextRange = pAtspiText.getTextAtOffset(offset, 
pair.atspiBoundaryType);
+
+            // for WORD there's adjustments to be made, see 
atktext.cxx:adjust_boundaries()
+            if (pair.loTextType == accessibility::AccessibleTextType::WORD
+                && !loTextSegment.SegmentText.isEmpty())
+            {
+                // Determine the start index of the next segment
+                const auto loTextSegmentBehind
+                    = xLOText->getTextBehindIndex(loTextSegment.SegmentEnd, 
pair.loTextType);
+                if (!loTextSegmentBehind.SegmentText.isEmpty())
+                    loTextSegment.SegmentEnd = 
loTextSegmentBehind.SegmentStart;
+                else
+                    loTextSegment.SegmentEnd = xLOText->getCharacterCount();
+
+                loTextSegment.SegmentText
+                    = xLOText->getTextRange(loTextSegment.SegmentStart, 
loTextSegment.SegmentEnd);
+            }
+
+            OString boundaryMsg(offsetMsg + " with boundary type "
+                                + 
Atspi::TextBoundaryType::getName(pair.atspiBoundaryType).c_str());
+            CPPUNIT_ASSERT_EQUAL_MESSAGE(boundaryMsg.getStr(), 
loTextSegment.SegmentText,
+                                         
OUString::fromUtf8(atspiTextRange.content));
+            /* if the segment is empty, LO API gives -1 offsets, but maps to 0 
for AT-SPI.  This is
+             * fine, AT-SPI doesn't really say what the offsets should be when 
the text is empty */
+            if (!loTextSegment.SegmentText.isEmpty())
+            {
+                CPPUNIT_ASSERT_EQUAL_MESSAGE(boundaryMsg.getStr(), 
loTextSegment.SegmentStart,
+                                             
sal_Int32(atspiTextRange.startOffset));
+                CPPUNIT_ASSERT_EQUAL_MESSAGE(boundaryMsg.getStr(), 
loTextSegment.SegmentEnd,
+                                             
sal_Int32(atspiTextRange.endOffset));
+            }
+        }
+
+        // character bounds
+        const auto loRect = xLOText->getCharacterBounds(offset);
+        auto atspiRect = pAtspiText.getCharacterExtents(offset, 
ATSPI_COORD_TYPE_WINDOW);
+        atspiRect.x -= atspiPosition.x;
+        atspiRect.y -= atspiPosition.y;
+        CPPUNIT_ASSERT_EQUAL_MESSAGE(offsetMsg.getStr(), loRect.Y, 
sal_Int32(atspiRect.y));
+        CPPUNIT_ASSERT_EQUAL_MESSAGE(offsetMsg.getStr(), loRect.Height,
+                                     sal_Int32(atspiRect.height));
+        /* for some reason getCharacterBounds() might return negative widths 
in some cases
+         * (including a space at the end of a right-justified line), and ATK 
will then then adjust
+         * the X and width values to positive to workaround RTL issues (see
+         * https://bugzilla.gnome.org/show_bug.cgi?id=102954), so we work 
around that */
+        if (loRect.Width < 0)
+        {
+            /* ATK will make x += width; width *= -1, but we don't really want 
to depend on the
+             * ATK behavior so we allow it to match as well */
+            CPPUNIT_ASSERT_MESSAGE(offsetMsg.getStr(),
+                                   loRect.X == sal_Int32(atspiRect.x)
+                                       || loRect.X + loRect.Width == 
sal_Int32(atspiRect.x));
+            CPPUNIT_ASSERT_MESSAGE(offsetMsg.getStr(),
+                                   loRect.Width == sal_Int32(atspiRect.width)
+                                       || -loRect.Width == 
sal_Int32(atspiRect.width));
+        }
+        else
+        {
+            // normal case
+            CPPUNIT_ASSERT_EQUAL_MESSAGE(offsetMsg.getStr(), loRect.X, 
sal_Int32(atspiRect.x));
+            CPPUNIT_ASSERT_EQUAL_MESSAGE(offsetMsg.getStr(), loRect.Width,
+                                         sal_Int32(atspiRect.width));
+        }
+
+        // indexAtPoint()
+        CPPUNIT_ASSERT_EQUAL_MESSAGE(
+            offsetMsg.getStr(), xLOText->getIndexAtPoint(awt::Point(loRect.X, 
loRect.Y)),
+            sal_Int32(pAtspiText.getOffsetAtPoint(
+                atspiPosition.x + loRect.X, atspiPosition.y + loRect.Y, 
ATSPI_COORD_TYPE_WINDOW)));
+
+        CPPUNIT_ASSERT_MESSAGE(offsetMsg.getStr(), aTextSegment.SegmentEnd > 
offset);
+        offset = aTextSegment.SegmentEnd;
+    }
+
+    // getRangeExtents() -- ATK doesn't like empty ranges, so only test when 
not empty
+    if (characterCount > 0)
+    {
+        const auto loRangeBounds = getRangeBounds(xLOText, 0, characterCount);
+        const auto atspiRangeExtents
+            = pAtspiText.getRangeExtents(0, characterCount, 
ATSPI_COORD_TYPE_WINDOW);
+        CPPUNIT_ASSERT_EQUAL(loRangeBounds.X, sal_Int32(atspiRangeExtents.x - 
atspiPosition.x));
+        CPPUNIT_ASSERT_EQUAL(loRangeBounds.Y, sal_Int32(atspiRangeExtents.y - 
atspiPosition.y));
+        CPPUNIT_ASSERT_EQUAL(loRangeBounds.Width, 
sal_Int32(atspiRangeExtents.width));
+        CPPUNIT_ASSERT_EQUAL(loRangeBounds.Height, 
sal_Int32(atspiRangeExtents.height));
+    }
+
+    // selection (LO only have one selection, so some of the API doesn't 
really make sense)
+    CPPUNIT_ASSERT_EQUAL(xLOText->getSelectionEnd() != 
xLOText->getSelectionStart() ? 1 : 0,
+                         pAtspiText.getNSelections());
+
+    const auto atspiSelection = pAtspiText.getSelection(0);
+    CPPUNIT_ASSERT_EQUAL(xLOText->getSelectionStart(), 
sal_Int32(atspiSelection.startOffset));
+    CPPUNIT_ASSERT_EQUAL(xLOText->getSelectionEnd(), 
sal_Int32(atspiSelection.endOffset));
+
+    /* We need to take extra care with setSelection() because it could result 
to scrolling, which
+     * might result in node destruction, which can mess up the parent's 
children enumeration.
+     * So we only test nodes that are neither the first nor last child in its 
parent, hoping that
+     * means it won't require scrolling to show the end of the selection. */
+    uno::Reference<accessibility::XAccessibleContext> xLOContext(xLOText, 
uno::UNO_QUERY_THROW);
+    const auto nIndexInParent = xLOContext->getAccessibleIndexInParent();
+    if (characterCount && nIndexInParent > 0
+        && nIndexInParent + 1 < xLOContext->getAccessibleParent()
+                                    ->getAccessibleContext()
+                                    ->getAccessibleChildCount()
+        && pAtspiText.setSelection(0, 0, characterCount))
+    {
+        CPPUNIT_ASSERT_EQUAL(sal_Int32(0), xLOText->getSelectionStart());
+        CPPUNIT_ASSERT_EQUAL(characterCount, xLOText->getSelectionEnd());
+        // try and restore previous selection, if any
+        CPPUNIT_ASSERT(xLOText->setSelection(std::max(0, 
atspiSelection.startOffset),
+                                             std::max(0, 
atspiSelection.endOffset)));
+    }
+
+    // scrollSubstringTo() is tested in the parent, because it might dispose 
ourselves otherwise.
+
+    // TODO: more checks here...
+}
+
+#if HAVE_ATSPI2_SCROLL_TO
+// like getFirstRelationTargetOfType() but for Atspi objects
+static Atspi::Accessible
+atspiGetFirstRelationTargetOfType(const Atspi::Accessible& pAtspiAccessible,
+                                  const AtspiRelationType relationType)
+{
+    for (const auto& rel : pAtspiAccessible.getRelationSet())
+    {
+        if (rel.getRelationType() == relationType && rel.getNTargets() > 0)
+            return rel.getTarget(0);
+    }
+
+    return nullptr;
+}
+#endif // HAVE_ATSPI2_SCROLL_TO
+
+/**
+ * @brief Gets the index of a Writer child hopping through flows-from 
relationships
+ * @param xContext The accessible context to locate
+ * @returns The index of @c xContext in the flows-from chain
+ *
+ * Gets the index of a child in its parent regardless of whether it is on 
screen or not.
+ *
+ * @warning This relying on the flows-from relationships, it only works for 
the connected nodes,
+ *          and might not work for e.g. frames.
+ */
+sal_Int64 
Atspi2TestTree::swChildIndex(uno::Reference<accessibility::XAccessibleContext> 
xContext)
+{
+    for (sal_Int64 n = 0;; n++)
+    {
+        auto xPrev = getFirstRelationTargetOfType(
+            xContext, 
accessibility::AccessibleRelationType::CONTENT_FLOWS_FROM);
+        if (!xPrev.is())
+            return n;
+        xContext = xPrev;
+    }
+}
+
+/**
+ * @brief tests scrolling in Writer.
+ * @param xLOContext The @c XAccessibleContext for the writer document
+ * @param xAtspiAccessible The AT-SPI2 equivalent of @c xLOContext.
+ *
+ * Test scrolling (currently XAccessibleText::scrollSubstringTo()) in Writer.
+ */
+void Atspi2TestTree::testSwScroll(
+    const uno::Reference<accessibility::XAccessibleContext>& xLOContext,
+    const Atspi::Accessible& xAtspiAccessible)
+{
+#if HAVE_ATSPI2_SCROLL_TO
+    /* Currently LO only implements SCROLL_ANYWHERE, so to be sure we need to 
find something
+     * offscreen and try and bring it in.  LO only has implementation for 
SwAccessibleParagraph,
+     * so we find the last child, and then try and find a FLOWS_TO 
relationship -- that's a hack
+     * based on how LO exposes offscreen children, e.g. not as "real" 
children.  Once done so, we
+     * have to make sure the child is now on screen, so we should find it in 
the children list.  We
+     * cannot rely on anything we had still being visible, as it could very 
well have scrolled it to
+     * the top. */
+    assert(accessibility::AccessibleRole::DOCUMENT_TEXT == 
xLOContext->getAccessibleRole());
+
+    auto nLOChildCount = xLOContext->getAccessibleChildCount();
+    if (nLOChildCount <= 0)
+        return;
+
+    // find the first off-screen text child
+    auto xLONextContext = xLOContext->getAccessibleChild(nLOChildCount - 
1)->getAccessibleContext();
+    uno::Reference<accessibility::XAccessibleText> xLONextText;
+    unsigned int nAfterLast = 0;
+    do
+    {
+        xLONextContext = getFirstRelationTargetOfType(
+            xLONextContext, 
accessibility::AccessibleRelationType::CONTENT_FLOWS_TO);
+        xLONextText.set(xLONextContext, uno::UNO_QUERY);
+        nAfterLast++;
+    } while (xLONextContext.is() && !xLONextText.is());
+
+    if (!xLONextText.is())
+        return; // we have nothing off-screen to scroll to
+
+    // get the global index of the off-screen child so we can match it later
+    auto nLOChildIndex = swChildIndex(xLONextContext);
+
+    // find the corresponding Atspi child to call the API on
+    auto xAtspiNextChild = xAtspiAccessible.getChildAtIndex(nLOChildCount - 1);
+    while (nAfterLast-- > 0 && xAtspiNextChild)
+        xAtspiNextChild
+            = atspiGetFirstRelationTargetOfType(xAtspiNextChild, 
ATSPI_RELATION_FLOWS_TO);
+    /* the child ought to be found and implement the same interfaces, 
otherwise there's a problem
+     * in LO <> Atspi child mapping  */
+    CPPUNIT_ASSERT(xAtspiNextChild);
+    const auto xAtspiNextText = xAtspiNextChild.queryText();
+
+    // scroll the child into view
+    CPPUNIT_ASSERT(xAtspiNextText.scrollSubstringTo(0, 1, 
ATSPI_SCROLL_ANYWHERE));
+
+    // now, check that the nLOChildIndex is in the visible area (among the 
regular children)
+    nLOChildCount = xLOContext->getAccessibleChildCount();
+    CPPUNIT_ASSERT_GREATER(sal_Int64(0), nLOChildCount);
+    const auto nLOFirstChildIndex
+        = 
swChildIndex(xLOContext->getAccessibleChild(0)->getAccessibleContext());
+
+    CPPUNIT_ASSERT_LESSEQUAL(nLOChildIndex, nLOFirstChildIndex);
+    CPPUNIT_ASSERT_GREATER(nLOChildIndex, nLOFirstChildIndex + nLOChildCount);
+#else // !HAVE_ATSPI2_SCROLL_TO
+    // unused
+    (void)xLOContext;
+    (void)xAtspiAccessible;
+#endif // !HAVE_ATSPI2_SCROLL_TO
+}
diff --git a/vcl/qa/cppunit/a11y/atspi2/atspiwrapper.cxx 
b/vcl/qa/cppunit/a11y/atspi2/atspiwrapper.cxx
new file mode 100644
index 000000000000..fd8017426810
--- /dev/null
+++ b/vcl/qa/cppunit/a11y/atspi2/atspiwrapper.cxx
@@ -0,0 +1,22 @@
+/* -*- 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 "atspiwrapper.hxx"
+
+using namespace Atspi;
+
+Accessible Relation::getTarget(int i) const { return 
invoke(atspi_relation_get_target, i); }
+
+Component Accessible::queryComponent() const
+{
+    return queryInterface<Component>(atspi_accessible_get_component_iface);
+}
+Text Accessible::queryText() const { return 
queryInterface<Text>(atspi_accessible_get_text_iface); }
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab cinoptions=b1,g0,N-s 
cinkeys+=0=break: */
diff --git a/vcl/qa/cppunit/a11y/atspi2/atspiwrapper.hxx 
b/vcl/qa/cppunit/a11y/atspi2/atspiwrapper.hxx
new file mode 100644
index 000000000000..209495e826ae
--- /dev/null
+++ b/vcl/qa/cppunit/a11y/atspi2/atspiwrapper.hxx
@@ -0,0 +1,784 @@
+/* -*- 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/.
+ */
+
+/* C++ wrapper for libatspi, so to make it less obnoxious to use */
+
+/**
+ * Adding a new wrapper
+ *
+ * To wrap a new Atspi type (let's say, AtspiCollection), you need to:
+ *
+ * 1. Add <tt>DEFINE_GOBJECT_CAST(AtspiCollection, ATSPI_TYPE_COLLECTION)</tt> 
near the similar
+ *    ones. This creates <tt>Atspi::cast<AtspiCollection*>(p)</tt> so that 
such a cast based on the
+ *    C++ type is checked using the GType type system.
+ * 2. Add a declaration for the new wrapper class above Atspi::Accessible
+ *    (<tt>class Collection;</tt>) so it can be used in step 3.
+ * 3. Add <tt>Atspi::Accessible::queryCollection()</tt> method.  Its 
definition has to be in the
+ *    source file as it requires a complete type for the wrapper class.  The 
body just calls
+ *    
<tt>queryInterface<Collection>(atspi_accessible_get_collection_iface);</tt> and 
returns
+ *    its value.
+ * 4. Add the definition of the new wrapper class:
+ *    <tt>class Collection : public Accessible { ... }</tt>
+ *    Use the existing wrappers as inspiration, but basically:
+ *    - Define the constructor that only chains up to the parent
+ *    - Define each wrapper method, which generally only have to call one of 
the <tt>invoke()</tt>
+ *      helpers to wrap the C calls.  There are a few, depending on some 
details of the C call:
+ *      - @c GObjectWrapperBase::invoke(): this is the most basic one, that 
just calls the C method
+ *           on @c GObjectWrapperBase::get() with the given arguments.  Use 
this for calls not
+ *           throwing an exception and either returning a plain value, or 
something not handled by
+ *           one of the others below.
+ *      - @c AtspiWrapperBase::invokeThrow(): like @c 
GObjectWrapperBase::invoke(), but for C calls
+ *           that take a @c GError argument for throwing exceptions.  @c 
invokeThrow() will
+ *           transform any C exception into a a C++ exception (@c 
css::uno::RuntimeException)
+ *      - @c AtspiWrapperBase::strInvoke(): like @c 
AtspiWrapperBase::invokeThrow(), but manages a
+ *           C string (@c char*) return value as an @c std::string.  Use this 
for C calls returning
+ *           a C string.
+ *      - @c AtspiWrapperBase::garrayInvoke(): like @c 
AtspiWrapperBase::invokeThrow(), but manages
+ *           a @c GArray return value as an @c std::vector.  Use this for C 
calls returning a
+ *           @p GArray.
+ *      - @c AtspiWrapperBase::hashMapInvoke(): like @c 
AtspiWrapperBase::invokeThrow(), but manages
+ *           a @c GHashTable return value as an @c std::unordered_map.  Use 
this for C calls
+ *           returning a @p GHashTable.
+ *      - @c AtspiWrapperBase::strHashMapInvoke(): identical to @c 
AtspiWrapperBase::hashMapInvoke()
+ *           using C strings for keys and values.
+ *      .
+ *     If none of those match the exact return type of the C call to wrap, use
+ *     @c AtspiWrapperBase::invokeThrow() or even @c 
GObjectWrapperBase::invoke(), and manually
+ *     manage the result value.  You can use Atspi::gmem functions to help.  
Basically the idea is
+ *     that you always return a self-managing object to make memory management 
easy (whereas it's
+ *     obnoxiously hard with plain C Atspi API).
+ */
+
+#pragma once
+
+#include <vector>
+#include <unordered_map>
+#include <boost/type_traits/function_traits.hpp>
+
+#include <atspi/atspi.h>
+#include <com/sun/star/container/NoSuchElementException.hpp>
+#include <com/sun/star/uno/RuntimeException.hpp>
+
+#include <cppunit/TestAssert.h>
+
+#include <config_atspi.h>
+
+namespace Atspi
+{
+/** @brief Helpers for managing GLib memory in a more C++-style */
+namespace gmem
+{
+/** @brief Wraps a pointer to free with @c g_free() in a @c std::unique_ptr */
+template <typename T> static inline auto unique_gmem(T* ptr)
+{
+    return std::unique_ptr<T, decltype(&g_free)>(ptr, &g_free);
+}
+
+/** @brief Wraps a @c GArray to free with @c g_array_unref() in a @c 
std::unique_ptr */
+static inline auto unique_garray(GArray* p)
+{
+    return std::unique_ptr<GArray, decltype(&g_array_unref)>(p, 
&g_array_unref);
+}
+
+/** @brief Wraps a @c GHashTable to free with @c g_hash_table_unref() in a @c 
std::unique_ptr */
+static inline auto unique_ghashtable(GHashTable* p)
+{
+    return std::unique_ptr<GHashTable, decltype(&g_hash_table_unref)>(p, 
&g_hash_table_unref);
+}
+}
+
+// --- GObject cast wrappers based on type: usage is 
cast<AtspiAccessible*>(pCInstance)
+#define DEFINE_GOBJECT_CAST(CType, GType)                                      
                    \
+    template <typename P, typename T, std::enable_if_t<std::is_same_v<P, 
CType*>, int> = 1>        \
+    P cast(T* pInstance)                                                       
                    \
+    {                                                                          
                    \
+        return G_TYPE_CHECK_INSTANCE_CAST(pInstance, GType, 
std::remove_pointer_t<P>);             \
+    }
+
+DEFINE_GOBJECT_CAST(AtspiStateSet, ATSPI_TYPE_STATE_SET)
+DEFINE_GOBJECT_CAST(AtspiRelation, ATSPI_TYPE_RELATION)
+DEFINE_GOBJECT_CAST(AtspiAccessible, ATSPI_TYPE_ACCESSIBLE)
+DEFINE_GOBJECT_CAST(AtspiComponent, ATSPI_TYPE_COMPONENT)
+DEFINE_GOBJECT_CAST(AtspiText, ATSPI_TYPE_TEXT)
+
+#undef DEFINE_GOBJECT_CAST
+// --- end GObject cast wrappers
+
+class GLibEnumBase
+{
+protected:
+    /**
+     * @brief Retrieves the string representation of an enumeration value
+     * @param gt The @c GType for the enumeration
+     * @param value The enumeration value for which to get the name for
+     * @param fallback Fallback value in case @p values falls outside the 
enumeration
+     * @returns A string representing @p value
+     */
+    static std::string glibEnumValueName(GType gt, gint value,
+                                         std::string_view fallback = "unknown")
+    {
+        auto klass = static_cast<GEnumClass*>(g_type_class_ref(gt));
+        auto enum_value = g_enum_get_value(klass, value);
+        std::string ret(enum_value ? enum_value->value_name : fallback);
+        g_type_class_unref(klass);
+        return ret;
+    }
+};
+
+class Role : private GLibEnumBase
+{
+public:
+    static std::string getName(AtspiRole role)
+    {
+        return glibEnumValueName(atspi_role_get_type(), role);
+    }
+};
+
+class State : private GLibEnumBase
+{
+public:
+    static std::string getName(AtspiStateType state)
+    {
+        return glibEnumValueName(atspi_state_type_get_type(), state);
+    }
+};
+
+class TextGranularity : private GLibEnumBase
+{
+public:
+    static std::string getName(AtspiTextGranularity granularity)
+    {
+        return glibEnumValueName(atspi_text_granularity_get_type(), 
granularity);
+    }
+};
+
+class TextBoundaryType : private GLibEnumBase
+{
+public:
+    static std::string getName(AtspiTextBoundaryType boundaryType)
+    {
+        return glibEnumValueName(atspi_text_boundary_type_get_type(), 
boundaryType);
+    }
+};
+
+/**
+ * @brief Base class for GObject wrappers
+ *
+ * This leverages std::shared_ptr as a cheap way of wrapping a raw pointer, 
and its deleter as a
+ * mean of using g_object_unref() to cleanup.  This is sub-optimal as it 
maintains a separate
+ * refcount to the GObject one, but it's easy.
+ */
+template <class T> class GObjectWrapperBase : public std::shared_ptr<T>
+{
+public:
+    /* this is the boundary of C++ type safety, so we can have inheritance 
working
+     * properly with the C types.  This should still be safe as it uses cast() 
which should be
+     * defined for each using type with DEFINE_GOBJECT_CAST(), which uses 
GType validation */
+    template <typename P = T*> P get() const { return 
cast<P>(std::shared_ptr<T>::get()); }
+
+protected:
+    /**
+     * @brief Calls the C function @p f on the C object wrapped by @p this
+     * @param f The C function to call
+     * @param args Additional arguments to @p f
+     * @returns The return value from @p f
+     *
+     * Calls the C function @p f similar to <tt>f(get(), args...)</tt>.  Care 
is taken of
+     * transforming @c get() to the type actually expected as the first 
argument of @p f, using
+     * @c get<TypeOfFsFirstArgument>(), which performs a runtime verification 
of the conversion.
+     *
+     * @note The type verification on whether @p f actually takes what get() 
returns is performed
+     *       at runtime, so there will be no compilation error or warning if 
trying to use an
+     *       incompatible C function.  A check will however be performed at 
runtime, at least
+     *       helping diagnose a possible invalid conversion.
+     */
+    template <typename F, typename... Ts> inline auto invoke(F f, Ts... args) 
const
+    {
+        using FT = std::remove_pointer_t<F>;
+        const auto p = get<typename boost::function_traits<FT>::arg1_type>();
+        return f(p, args...);
+    }
+
+private:
+    static void deleter(T* p)
+    {
+        if (p)
+            g_object_unref(p);
+    }
+
+public:
+    /**
+     * @param pObj The raw GObject to wrap
+     * @param takeRef Whether to take ownership of the object or not.  If set 
to @c false, it will
+     *                call @c g_object_ref(pAcc) to acquire a new reference to 
the GObject.
+     */
+    GObjectWrapperBase(T* pObj = nullptr, bool takeRef = true)
+        : std::shared_ptr<T>(pObj, deleter)
+    {
+        if (pObj && !takeRef)
+            g_object_ref(pObj);
+    }
+};
+
+/** @brief AtspiStateSet C++ wrapper */
+class StateSet : public GObjectWrapperBase<AtspiStateSet>
+{
+public:
+    using GObjectWrapperBase::GObjectWrapperBase;
+
+    void add(const AtspiStateType t) { invoke(atspi_state_set_add, t); }
+    StateSet compare(const StateSet& other) const
+    {
+        return StateSet(invoke(atspi_state_set_compare, other.get()));
+    }
+    bool contains(const AtspiStateType t) const { return 
invoke(atspi_state_set_contains, t); }
+    bool operator==(const StateSet& other) const
+    {
+        return invoke(atspi_state_set_equals, other.get());
+    }
+    std::vector<AtspiStateType> getStates() const
+    {
+        auto garray = gmem::unique_garray(invoke(atspi_state_set_get_states));
+        std::vector<AtspiStateType> states;
+        for (auto i = decltype(garray->len){ 0 }; i < garray->len; i++)
+        {
+            states.push_back(g_array_index(garray, 
decltype(states)::value_type, i));
+        }
+        return states;
+    }
+    bool empty() const { return invoke(atspi_state_set_is_empty); }
+    void remove(AtspiStateType t) { invoke(atspi_state_set_remove, t); }
+    void setByName(const std::string_view name, bool enable)
+    {
+        invoke(atspi_state_set_set_by_name, name.data(), enable);
+    }
+};
+
+class Accessible;
+
+/** @brief AtspiRelation C++ wrapper */
+class Relation : public GObjectWrapperBase<AtspiRelation>
+{
+public:
+    using GObjectWrapperBase::GObjectWrapperBase;
+
+    AtspiRelationType getRelationType() const { return 
invoke(atspi_relation_get_relation_type); }
+    int getNTargets() const { return invoke(atspi_relation_get_n_targets); }
+    Accessible getTarget(int i) const;
+};
+
+/* intermediate base just for splitting out the *invoke* helpers 
implementations, so the actual
+ * user-targeted class can hold only the actual API */
+template <class T> class AtspiWrapperBase : public GObjectWrapperBase<T>
+{
+protected:
+    using GObjectWrapperBase<T>::invoke;
+
+    /**
+     * @brief Calls the throwing C function @p f on the C object wrapped by @p 
this
+     * @param f The C function to call
+     * @param args Additional arguments to @p f
+     * @returns The raw return value from @p f
+     * @throws css::uno::RuntimeException Exception @c GError are translated to
+     *

... etc. - the rest is truncated

Reply via email to