include/test/a11y/AccessibilityTools.hxx   |   12 +
 include/test/a11y/accessibletestbase.hxx   |  138 +++++++++++++++
 include/test/a11y/swaccessibletestbase.hxx |   58 ++++++
 sc/CppunitTest_sc_a11y.mk                  |   39 ++++
 sc/Module_sc.mk                            |    1 
 sc/qa/extras/accessibility/basics.cxx      |   90 ++++++++++
 sw/CppunitTest_sw_a11y.mk                  |   38 ++++
 sw/Module_sw.mk                            |    1 
 sw/qa/extras/accessibility/basics.cxx      |   96 ++++++++++
 test/Library_subsequenttest.mk             |    2 
 test/source/a11y/AccessibilityTools.cxx    |   50 ++++-
 test/source/a11y/accessibletestbase.cxx    |  260 +++++++++++++++++++++++++++++
 test/source/a11y/swaccessibletestbase.cxx  |  135 +++++++++++++++
 13 files changed, 907 insertions(+), 13 deletions(-)

New commits:
commit 0185ddd6d5f0324ba57b3fa36229103a6b27138e
Author:     Colomban Wendling <cwendl...@hypra.fr>
AuthorDate: Thu Jul 21 21:51:21 2022 +0200
Commit:     Michael Weghorn <m.wegh...@posteo.de>
CommitDate: Mon Aug 1 17:03:40 2022 +0200

    Add infrastructure and basic tests including slight UI interaction
    
    This introduces a couple helper base classes for implementing
    accessibility tests more easily, and includes a few tests as examples,
    including basic document layout check and minimal UI interaction
    through menus.
    
    Change-Id: I8961af8be1e7d52dc55fe27c758806d9b4c3c5d9
    Reviewed-on: https://gerrit.libreoffice.org/c/core/+/137337
    Tested-by: Jenkins
    Reviewed-by: Michael Weghorn <m.wegh...@posteo.de>

diff --git a/include/test/a11y/AccessibilityTools.hxx 
b/include/test/a11y/AccessibilityTools.hxx
index e56c68c9bebb..1efd0b9f2960 100644
--- a/include/test/a11y/AccessibilityTools.hxx
+++ b/include/test/a11y/AccessibilityTools.hxx
@@ -37,6 +37,18 @@ public:
      * Calc which has a million elements, if not more. */
     static const sal_Int32 MAX_CHILDREN = 500;
 
+    static css::uno::Reference<css::accessibility::XAccessibleContext>
+    getAccessibleObjectForPredicate(
+        const css::uno::Reference<css::accessibility::XAccessibleContext>& 
xCtx,
+        const std::function<
+            bool(const 
css::uno::Reference<css::accessibility::XAccessibleContext>&)>& cPredicate);
+    static css::uno::Reference<css::accessibility::XAccessibleContext>
+    getAccessibleObjectForPredicate(
+        const css::uno::Reference<css::accessibility::XAccessible>& xAcc,
+        const std::function<
+            bool(const 
css::uno::Reference<css::accessibility::XAccessibleContext>&)>& cPredicate);
+    static css::uno::Reference<css::accessibility::XAccessibleContext> 
getAccessibleObjectForRole(
+        const css::uno::Reference<css::accessibility::XAccessibleContext>& 
xCtx, sal_Int16 role);
     static css::uno::Reference<css::accessibility::XAccessibleContext>
     getAccessibleObjectForRole(const 
css::uno::Reference<css::accessibility::XAccessible>& xacc,
                                sal_Int16 role);
diff --git a/include/test/a11y/accessibletestbase.hxx 
b/include/test/a11y/accessibletestbase.hxx
new file mode 100644
index 000000000000..50a39f63a7dd
--- /dev/null
+++ b/include/test/a11y/accessibletestbase.hxx
@@ -0,0 +1,138 @@
+/* -*- 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 <test/testdllapi.hxx>
+
+#include <deque>
+#include <string>
+
+#include <com/sun/star/accessibility/AccessibleRole.hpp>
+#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/awt/XWindow.hpp>
+#include <com/sun/star/frame/Desktop.hpp>
+#include <com/sun/star/lang/XComponent.hpp>
+#include <com/sun/star/uno/Reference.hxx>
+
+#include <vcl/ITiledRenderable.hxx>
+
+#include <rtl/ustring.hxx>
+#include <test/bootstrapfixture.hxx>
+
+#include "AccessibilityTools.hxx"
+
+namespace test
+{
+class OOO_DLLPUBLIC_TEST AccessibleTestBase : public test::BootstrapFixture
+{
+protected:
+    css::uno::Reference<css::frame::XDesktop2> mxDesktop;
+    css::uno::Reference<css::lang::XComponent> mxDocument;
+    css::uno::Reference<css::awt::XWindow> mxWindow;
+
+    static bool isDocumentRole(const sal_Int16 role);
+
+    virtual void load(const rtl::OUString& sURL);
+    virtual void loadFromSrc(const rtl::OUString& sSrcPath);
+    void close();
+    css::uno::Reference<css::accessibility::XAccessibleContext> 
getWindowAccessibleContext();
+    virtual css::uno::Reference<css::accessibility::XAccessibleContext>
+    getDocumentAccessibleContext();
+
+    void documentPostKeyEvent(int nType, int nCharCode, int nKeyCode)
+    {
+        vcl::ITiledRenderable* pTiledRenderable
+            = dynamic_cast<vcl::ITiledRenderable*>(mxDocument.get());
+        CPPUNIT_ASSERT(pTiledRenderable);
+        pTiledRenderable->postKeyEvent(nType, nCharCode, nKeyCode);
+    }
+
+    static css::uno::Reference<css::accessibility::XAccessibleContext> 
getFirstRelationTargetOfType(
+        const css::uno::Reference<css::accessibility::XAccessibleContext>& 
xContext,
+        sal_Int16 relationType);
+
+    /**
+     * @brief Tries to list all children of an accessible
+     * @param xContext An XAccessibleContext object
+     * @returns The list of all children (but no more than @c 
AccessibilityTools::MAX_CHILDREN)
+     *
+     * This fetches children of @p xContext.  This would ideally just be the 
same than iterating
+     * over children the regular way up to @c 
AccessibilityTools::MAX_CHILDREN, but unfortunately
+     * some components (Writer, Impress, ...) do not provide all their 
children the regular way and
+     * require specifics to include them.
+     *
+     * There is no guarantee on *which* children are returned if there are 
more than
+     * @c AccessibilityTools::MAX_CHILDREN -- yet they will always be the same 
in a given context.
+     */
+    virtual 
std::deque<css::uno::Reference<css::accessibility::XAccessibleContext>>
+    getAllChildren(const 
css::uno::Reference<css::accessibility::XAccessibleContext>& xContext);
+
+    void dumpA11YTree(const 
css::uno::Reference<css::accessibility::XAccessibleContext>& xContext,
+                      const int depth = 0);
+
+    css::uno::Reference<css::accessibility::XAccessibleContext>
+    getItemFromName(const 
css::uno::Reference<css::accessibility::XAccessibleContext>& xMenuCtx,
+                    std::u16string_view name);
+    bool
+    activateMenuItem(const 
css::uno::Reference<css::accessibility::XAccessibleAction>& xAction);
+    /* just convenience not to have to query accessibility::XAccessibleAction 
manually */
+    bool activateMenuItem(const 
css::uno::Reference<css::accessibility::XAccessibleContext>& xCtx)
+    {
+        return 
activateMenuItem(css::uno::Reference<css::accessibility::XAccessibleAction>(
+            xCtx, css::uno::UNO_QUERY_THROW));
+    }
+
+    /* convenience to get a menu item from a list of menu item names.  Unlike
+     * getItemFromName(context, name), this requires subsequently found items 
to implement
+     * XAccessibleAction, as each but the last item will be activated before 
looking for
+     * the next one, to account for the fact menus might not be fully 
populated before being
+     * activated. */
+    template <typename... Ts>
+    css::uno::Reference<css::accessibility::XAccessibleContext>
+    getItemFromName(const 
css::uno::Reference<css::accessibility::XAccessibleContext>& xMenuCtx,
+                    std::u16string_view name, Ts... names)
+    {
+        auto item = getItemFromName(xMenuCtx, name);
+        CPPUNIT_ASSERT(item.is());
+        activateMenuItem(item);
+        return getItemFromName(item, names...);
+    }
+
+    /* convenience to activate an item by its name and all its parent menus up 
to xMenuCtx.
+     * @see getItemFromName() */
+    template <typename... Ts>
+    bool
+    activateMenuItem(const 
css::uno::Reference<css::accessibility::XAccessibleContext>& xMenuCtx,
+                     Ts... names)
+    {
+        auto item = getItemFromName(xMenuCtx, names...);
+        CPPUNIT_ASSERT(item.is());
+        return activateMenuItem(item);
+    }
+
+    /* convenience to activate an item by its name and all its parent menus up 
to the main window
+     * menu bar */
+    template <typename... Ts> bool activateMenuItem(Ts... names)
+    {
+        auto menuBar = AccessibilityTools::getAccessibleObjectForRole(
+            getWindowAccessibleContext(), 
css::accessibility::AccessibleRole::MENU_BAR);
+        CPPUNIT_ASSERT(menuBar.is());
+        return activateMenuItem(menuBar, names...);
+    }
+
+public:
+    virtual void setUp() override;
+    virtual void tearDown() override;
+};
+}
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab cinoptions=b1,g0,N-s 
cinkeys+=0=break: */
diff --git a/include/test/a11y/swaccessibletestbase.hxx 
b/include/test/a11y/swaccessibletestbase.hxx
new file mode 100644
index 000000000000..a8ed42a4dcef
--- /dev/null
+++ b/include/test/a11y/swaccessibletestbase.hxx
@@ -0,0 +1,58 @@
+/* -*- 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 <test/testdllapi.hxx>
+
+#include <deque>
+
+#include <com/sun/star/accessibility/XAccessibleContext.hpp>
+#include <com/sun/star/uno/Reference.hxx>
+
+#include <rtl/ustrbuf.hxx>
+#include <rtl/ustring.hxx>
+
+#include "accessibletestbase.hxx"
+
+namespace test
+{
+class OOO_DLLPUBLIC_TEST SwAccessibleTestBase : public AccessibleTestBase
+{
+private:
+    void collectText(const 
css::uno::Reference<css::accessibility::XAccessibleContext>& xContext,
+                     rtl::OUStringBuffer& buffer, bool onlyChildren = false);
+
+protected:
+    static css::uno::Reference<css::accessibility::XAccessibleContext> 
getPreviousFlowingSibling(
+        const css::uno::Reference<css::accessibility::XAccessibleContext>& 
xContext);
+    static css::uno::Reference<css::accessibility::XAccessibleContext> 
getNextFlowingSibling(
+        const css::uno::Reference<css::accessibility::XAccessibleContext>& 
xContext);
+
+    /**
+     * This fetches regular children plus siblings linked with 
FLOWS_TO/FLOWS_FROM which are not
+     * already in the regular children set.  This is required because most 
offscreen children of the
+     * document contents are not listed as part of their parent children, but 
as FLOWS_* reference
+     * from one to the next.
+     * There is currently no guarantee all children will be listed, and it is 
fairly likely
+     * offscreen frames and tables might be missing for example.
+     */
+    virtual 
std::deque<css::uno::Reference<css::accessibility::XAccessibleContext>> 
getAllChildren(
+        const css::uno::Reference<css::accessibility::XAccessibleContext>& 
xContext) override;
+
+    /** Collects contents of @p xContext in a dummy markup form */
+    OUString
+    collectText(const 
css::uno::Reference<css::accessibility::XAccessibleContext>& xContext);
+
+    /** Collects contents of the current document */
+    OUString collectText() { return 
collectText(getDocumentAccessibleContext()); }
+};
+}
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab cinoptions=b1,g0,N-s 
cinkeys+=0=break: */
diff --git a/sc/CppunitTest_sc_a11y.mk b/sc/CppunitTest_sc_a11y.mk
new file mode 100644
index 000000000000..e013beb987cc
--- /dev/null
+++ b/sc/CppunitTest_sc_a11y.mk
@@ -0,0 +1,39 @@
+# -*- 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/.
+#
+
+$(eval $(call gb_CppunitTest_CppunitTest,sc_a11y))
+
+$(eval $(call gb_CppunitTest_add_exception_objects,sc_a11y, \
+       sc/qa/extras/accessibility/basics \
+))
+
+$(eval $(call gb_CppunitTest_use_libraries,sc_a11y, \
+       sal \
+       cppu \
+       subsequenttest \
+       test \
+       tl \
+       unotest \
+       vcl \
+))
+
+$(eval $(call gb_CppunitTest_use_api,sc_a11y,\
+       offapi \
+       udkapi \
+))
+
+$(eval $(call gb_CppunitTest_use_sdk_api,sc_a11y))
+$(eval $(call gb_CppunitTest_use_rdb,sc_a11y,services))
+$(eval $(call gb_CppunitTest_use_ure,sc_a11y))
+$(eval $(call gb_CppunitTest_use_vcl,sc_a11y))
+
+$(eval $(call gb_CppunitTest_use_instdir_configuration,sc_a11y))
+$(eval $(call gb_CppunitTest_use_common_configuration,sc_a11y))
+
+# vim: set noet sw=4 ts=4:
diff --git a/sc/Module_sc.mk b/sc/Module_sc.mk
index 5a0d0c13f40e..5323b6031d58 100644
--- a/sc/Module_sc.mk
+++ b/sc/Module_sc.mk
@@ -83,6 +83,7 @@ $(eval $(call gb_Module_add_slowcheck_targets,sc, \
        CppunitTest_sc_subsequent_export_test2 \
        CppunitTest_sc_uicalc \
        CppunitTest_sc_vba_macro_test \
+       CppunitTest_sc_a11y \
 ))
 
 ifneq ($(ENABLE_JUMBO_SHEETS),)
diff --git a/sc/qa/extras/accessibility/basics.cxx 
b/sc/qa/extras/accessibility/basics.cxx
new file mode 100644
index 000000000000..ec55ea04ed26
--- /dev/null
+++ b/sc/qa/extras/accessibility/basics.cxx
@@ -0,0 +1,90 @@
+/* -*- 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/XAccessibleTable.hpp>
+#include <com/sun/star/accessibility/XAccessibleText.hpp>
+#include <com/sun/star/accessibility/XAccessibleValue.hpp>
+#include <com/sun/star/util/Date.hpp>
+#include <com/sun/star/util/XNumberFormatsSupplier.hpp>
+
+#include <com/sun/star/awt/Key.hpp>
+#include <LibreOfficeKit/LibreOfficeKitEnums.h>
+#include <vcl/scheduler.hxx>
+
+#include <tools/date.hxx>
+#include <tools/time.hxx>
+
+#include <test/a11y/accessibletestbase.hxx>
+#include <test/a11y/AccessibilityTools.hxx>
+
+using namespace css;
+
+CPPUNIT_TEST_FIXTURE(test::AccessibleTestBase, TestCalcMenu)
+{
+    load(u"private:factory/scalc");
+
+    const Date beforeDate(Date::SYSTEM);
+    const double beforeTime = tools::Time(tools::Time::SYSTEM).GetTimeInDays();
+
+    // in cell A1, insert the date
+    CPPUNIT_ASSERT(activateMenuItem(u"Insert", u"Date"));
+    // move down to A2
+    documentPostKeyEvent(LOK_KEYEVENT_KEYINPUT, 0, css::awt::Key::DOWN);
+    // in cell A2, insert the time
+    CPPUNIT_ASSERT(activateMenuItem(u"Insert", u"Time"));
+
+    uno::Reference<accessibility::XAccessibleTable> sheet(
+        
getDocumentAccessibleContext()->getAccessibleChild(0)->getAccessibleContext(), 
// sheet 1
+        uno::UNO_QUERY_THROW);
+
+    /* As it's very tricky to check the dates and times are correct in text 
format (imagine running
+     * on 1970-12-31 23:59:59.99, it's gonna shift *everything* in a 100th of 
a second) because
+     * clock can have changed between generating the two values to compare.  
So instead we just
+     * check the text is not empty, and the underlying value (representing the 
date or time) is
+     * between the time it was before and after the call. */
+
+    // cell A1 contains a date
+    auto xCell = sheet->getAccessibleCellAt(0, 0)->getAccessibleContext();
+    uno::Reference<accessibility::XAccessibleText> xText(xCell, 
uno::UNO_QUERY_THROW);
+    std::cout << "A1 (text):  " << xText->getText() << std::endl;
+    CPPUNIT_ASSERT(!xText->getText().isEmpty());
+    uno::Reference<accessibility::XAccessibleValue> xValue(xCell, 
uno::UNO_QUERY_THROW);
+    double value;
+    CPPUNIT_ASSERT(xValue->getCurrentValue() >>= value);
+    std::cout << "A1 (value): " << value << std::endl;
+    uno::Reference<util::XNumberFormatsSupplier> xSupplier(mxDocument, 
uno::UNO_QUERY_THROW);
+    util::Date nullDate;
+    
CPPUNIT_ASSERT(xSupplier->getNumberFormatSettings()->getPropertyValue("NullDate")
 >>= nullDate);
+    const Date afterDate(Date::SYSTEM);
+    CPPUNIT_ASSERT_GREATEREQUAL(double(beforeDate - nullDate), value);
+    CPPUNIT_ASSERT_LESSEQUAL(double(afterDate - nullDate), value);
+
+    // cell A2 contains time, no date, so we have to be careful passing 
midnight
+    xCell = sheet->getAccessibleCellAt(1, 0)->getAccessibleContext();
+    xText.set(xCell, uno::UNO_QUERY_THROW);
+    std::cout << "A2 (text):  " << xText->getText() << std::endl;
+    CPPUNIT_ASSERT(!xText->getText().isEmpty());
+    xValue.set(xCell, uno::UNO_QUERY_THROW);
+    CPPUNIT_ASSERT(xValue->getCurrentValue() >>= value);
+    std::cout << "A2 (value): " << value << std::endl;
+    double afterTime = tools::Time(tools::Time::SYSTEM).GetTimeInDays();
+    // in case day changed -- assuming no more than 24 hours passed
+    if (afterTime < beforeTime)
+    {
+        afterTime += 1;
+        if (value < beforeTime)
+            value += 1;
+    }
+    CPPUNIT_ASSERT_GREATEREQUAL(beforeTime, value);
+    CPPUNIT_ASSERT_LESSEQUAL(afterTime, value);
+}
+
+CPPUNIT_PLUGIN_IMPLEMENT();
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab cinoptions=b1,g0,N-s 
cinkeys+=0=break: */
diff --git a/sw/CppunitTest_sw_a11y.mk b/sw/CppunitTest_sw_a11y.mk
new file mode 100644
index 000000000000..67c527d1179c
--- /dev/null
+++ b/sw/CppunitTest_sw_a11y.mk
@@ -0,0 +1,38 @@
+# -*- 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/.
+#
+
+$(eval $(call gb_CppunitTest_CppunitTest,sw_a11y))
+
+$(eval $(call gb_CppunitTest_add_exception_objects,sw_a11y, \
+       sw/qa/extras/accessibility/basics \
+))
+
+$(eval $(call gb_CppunitTest_use_libraries,sw_a11y, \
+       sal \
+       cppu \
+       subsequenttest \
+       test \
+       unotest \
+       vcl \
+))
+
+$(eval $(call gb_CppunitTest_use_api,sw_a11y,\
+       offapi \
+       udkapi \
+))
+
+$(eval $(call gb_CppunitTest_use_sdk_api,sw_a11y))
+$(eval $(call gb_CppunitTest_use_rdb,sw_a11y,services))
+$(eval $(call gb_CppunitTest_use_ure,sw_a11y))
+$(eval $(call gb_CppunitTest_use_vcl,sw_a11y))
+
+$(eval $(call gb_CppunitTest_use_instdir_configuration,sw_a11y))
+$(eval $(call gb_CppunitTest_use_common_configuration,sw_a11y))
+
+# vim: set noet sw=4 ts=4:
diff --git a/sw/Module_sw.mk b/sw/Module_sw.mk
index dc88c465553d..318f8581d7df 100644
--- a/sw/Module_sw.mk
+++ b/sw/Module_sw.mk
@@ -148,6 +148,7 @@ $(eval $(call gb_Module_add_slowcheck_targets,sw,\
     CppunitTest_sw_core_view \
     CppunitTest_sw_core_attr \
     CppunitTest_sw_filter_ww8 \
+    CppunitTest_sw_a11y \
 ))
 
 ifneq ($(DISABLE_GUI),TRUE)
diff --git a/sw/qa/extras/accessibility/basics.cxx 
b/sw/qa/extras/accessibility/basics.cxx
new file mode 100644
index 000000000000..44e835ab533a
--- /dev/null
+++ b/sw/qa/extras/accessibility/basics.cxx
@@ -0,0 +1,96 @@
+/* -*- 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/awt/Key.hpp>
+#include <LibreOfficeKit/LibreOfficeKitEnums.h>
+#include <vcl/scheduler.hxx>
+
+#include <test/a11y/swaccessibletestbase.hxx>
+#include <test/a11y/AccessibilityTools.hxx>
+
+using namespace css;
+
+CPPUNIT_TEST_FIXTURE(test::SwAccessibleTestBase, TestBasicStructure)
+{
+    loadFromSrc(u"/sw/qa/python/testdocuments/xtextcontent.odt");
+    auto xContext = getDocumentAccessibleContext();
+    CPPUNIT_ASSERT(xContext.is());
+
+    dumpA11YTree(xContext);
+    CPPUNIT_ASSERT_EQUAL(
+        
rtl::OUString("<PARAGRAPH>String1</PARAGRAPH><PARAGRAPH/><PARAGRAPH/><PARAGRAPH/>"
+                      "<TABLE name=\"Table1-1\" description=\"Table1 on page 
1\">"
+                      "<TABLE_CELL name=\"A1\" description=\"A1\">"
+                      "<PARAGRAPH>String2</PARAGRAPH>"
+                      "</TABLE_CELL>"
+                      "</TABLE>"
+                      "<PARAGRAPH/>"
+                      "<TEXT_FRAME 
name=\"Frame1\"><PARAGRAPH>Frame1</PARAGRAPH></TEXT_FRAME>"
+                      "<TEXT_FRAME 
name=\"Frame2\"><PARAGRAPH>Frame2</PARAGRAPH></TEXT_FRAME>"),
+        collectText(xContext));
+}
+
+CPPUNIT_TEST_FIXTURE(test::SwAccessibleTestBase, TestTypeSimple)
+{
+    load(u"private:factory/swriter");
+    auto xContext = getDocumentAccessibleContext();
+    CPPUNIT_ASSERT(xContext.is());
+
+    documentPostKeyEvent(LOK_KEYEVENT_KEYINPUT, 'h', 0);
+    documentPostKeyEvent(LOK_KEYEVENT_KEYINPUT, 'e', 0);
+    documentPostKeyEvent(LOK_KEYEVENT_KEYINPUT, 'l', 0);
+    documentPostKeyEvent(LOK_KEYEVENT_KEYINPUT, 'l', 0);
+    documentPostKeyEvent(LOK_KEYEVENT_KEYINPUT, 'o', 0);
+    Scheduler::ProcessEventsToIdle();
+
+    CPPUNIT_ASSERT_EQUAL(rtl::OUString("<PARAGRAPH>hello</PARAGRAPH>"), 
collectText(xContext));
+}
+
+CPPUNIT_TEST_FIXTURE(test::SwAccessibleTestBase, TestTypeMultiPara)
+{
+    load(u"private:factory/swriter");
+    auto xContext = getDocumentAccessibleContext();
+    CPPUNIT_ASSERT(xContext.is());
+
+    documentPostKeyEvent(LOK_KEYEVENT_KEYINPUT, 'A', 0);
+    documentPostKeyEvent(LOK_KEYEVENT_KEYINPUT, 0, awt::Key::RETURN);
+    documentPostKeyEvent(LOK_KEYEVENT_KEYINPUT, 'B', 0);
+    documentPostKeyEvent(LOK_KEYEVENT_KEYINPUT, 0, awt::Key::RETURN);
+    documentPostKeyEvent(LOK_KEYEVENT_KEYINPUT, 'C', 0);
+    Scheduler::ProcessEventsToIdle();
+
+    CPPUNIT_ASSERT_EQUAL(
+        
rtl::OUString("<PARAGRAPH>A</PARAGRAPH><PARAGRAPH>B</PARAGRAPH><PARAGRAPH>C</PARAGRAPH>"),
+        collectText(xContext));
+}
+
+CPPUNIT_TEST_FIXTURE(test::SwAccessibleTestBase, TestMenuInsertPageNumber)
+{
+    load(u"private:factory/swriter");
+    CPPUNIT_ASSERT(activateMenuItem(u"Insert", u"Page Number"));
+    CPPUNIT_ASSERT_EQUAL(rtl::OUString("<PARAGRAPH>1</PARAGRAPH>"), 
collectText());
+}
+
+CPPUNIT_TEST_FIXTURE(test::SwAccessibleTestBase, TestMenuInsertPageBreak)
+{
+    load(u"private:factory/swriter");
+
+    CPPUNIT_ASSERT(activateMenuItem(u"Insert", u"Page Number"));
+    CPPUNIT_ASSERT(activateMenuItem(u"Insert", u"Page Break"));
+    // we need to move focus to the paragraph after the page break to insert 
the page number there
+    documentPostKeyEvent(LOK_KEYEVENT_KEYINPUT, 0, awt::Key::DOWN);
+    CPPUNIT_ASSERT(activateMenuItem(u"Insert", u"Page Number"));
+
+    
CPPUNIT_ASSERT_EQUAL(rtl::OUString("<PARAGRAPH>1</PARAGRAPH><PARAGRAPH>2</PARAGRAPH>"),
+                         collectText());
+}
+
+CPPUNIT_PLUGIN_IMPLEMENT();
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab cinoptions=b1,g0,N-s 
cinkeys+=0=break: */
diff --git a/test/Library_subsequenttest.mk b/test/Library_subsequenttest.mk
index 9b35eda003ba..813b61ec538f 100644
--- a/test/Library_subsequenttest.mk
+++ b/test/Library_subsequenttest.mk
@@ -43,6 +43,8 @@ $(eval $(call 
gb_Library_add_exception_objects,subsequenttest,\
        test/source/unoapi_test \
        test/source/calc_unoapi_test \
        test/source/a11y/AccessibilityTools \
+       test/source/a11y/accessibletestbase \
+       test/source/a11y/swaccessibletestbase \
        test/source/beans/xpropertyset \
        test/source/chart/xchartdata \
        test/source/container/xchild \
diff --git a/test/source/a11y/AccessibilityTools.cxx 
b/test/source/a11y/AccessibilityTools.cxx
index 44b168b54a94..991089dcb3e0 100644
--- a/test/source/a11y/AccessibilityTools.cxx
+++ b/test/source/a11y/AccessibilityTools.cxx
@@ -32,32 +32,56 @@
 
 using namespace css;
 
-css::uno::Reference<css::accessibility::XAccessibleContext>
-AccessibilityTools::getAccessibleObjectForRole(
-    const css::uno::Reference<css::accessibility::XAccessible>& xacc, 
sal_Int16 role)
+uno::Reference<accessibility::XAccessibleContext>
+AccessibilityTools::getAccessibleObjectForPredicate(
+    const uno::Reference<accessibility::XAccessibleContext>& xCtx,
+    const std::function<bool(const 
uno::Reference<accessibility::XAccessibleContext>&)>& cPredicate)
 {
-    css::uno::Reference<css::accessibility::XAccessibleContext> ac = 
xacc->getAccessibleContext();
-    bool isShowing = ac->getAccessibleStateSet() & 
accessibility::AccessibleStateType::SHOWING;
-
-    if ((ac->getAccessibleRole() == role) && isShowing)
+    if (cPredicate(xCtx))
     {
-        return ac;
+        return xCtx;
     }
     else
     {
-        int count = ac->getAccessibleChildCount();
+        int count = xCtx->getAccessibleChildCount();
 
         for (int i = 0; i < count && i < AccessibilityTools::MAX_CHILDREN; i++)
         {
-            css::uno::Reference<css::accessibility::XAccessibleContext> ac2
-                = 
AccessibilityTools::getAccessibleObjectForRole(ac->getAccessibleChild(i), role);
-            if (ac2.is())
-                return ac2;
+            uno::Reference<accessibility::XAccessibleContext> xCtx2
+                = getAccessibleObjectForPredicate(xCtx->getAccessibleChild(i), 
cPredicate);
+            if (xCtx2.is())
+                return xCtx2;
         }
     }
     return nullptr;
 }
 
+uno::Reference<accessibility::XAccessibleContext>
+AccessibilityTools::getAccessibleObjectForPredicate(
+    const uno::Reference<accessibility::XAccessible>& xAcc,
+    const std::function<bool(const 
uno::Reference<accessibility::XAccessibleContext>&)>& cPredicate)
+{
+    return getAccessibleObjectForPredicate(xAcc->getAccessibleContext(), 
cPredicate);
+}
+
+uno::Reference<accessibility::XAccessibleContext> 
AccessibilityTools::getAccessibleObjectForRole(
+    const uno::Reference<accessibility::XAccessibleContext>& xCtx, sal_Int16 
role)
+{
+    return getAccessibleObjectForPredicate(
+        xCtx, [&role](const uno::Reference<accessibility::XAccessibleContext>& 
xObjCtx) {
+            return (xObjCtx->getAccessibleRole() == role
+                    && xObjCtx->getAccessibleStateSet()
+                           & accessibility::AccessibleStateType::SHOWING);
+        });
+}
+
+css::uno::Reference<css::accessibility::XAccessibleContext>
+AccessibilityTools::getAccessibleObjectForRole(
+    const css::uno::Reference<css::accessibility::XAccessible>& xacc, 
sal_Int16 role)
+{
+    return getAccessibleObjectForRole(xacc->getAccessibleContext(), role);
+}
+
 bool AccessibilityTools::equals(const 
uno::Reference<accessibility::XAccessible>& xacc1,
                                 const 
uno::Reference<accessibility::XAccessible>& xacc2)
 {
diff --git a/test/source/a11y/accessibletestbase.cxx 
b/test/source/a11y/accessibletestbase.cxx
new file mode 100644
index 000000000000..3968c32fa08d
--- /dev/null
+++ b/test/source/a11y/accessibletestbase.cxx
@@ -0,0 +1,260 @@
+/* -*- 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 <test/a11y/accessibletestbase.hxx>
+
+#include <string>
+
+#include <com/sun/star/accessibility/AccessibleRole.hpp>
+#include <com/sun/star/accessibility/AccessibleStateType.hpp>
+#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/awt/XTopWindow.hpp>
+#include <com/sun/star/frame/Desktop.hpp>
+#include <com/sun/star/frame/FrameSearchFlag.hpp>
+#include <com/sun/star/frame/XFrame.hpp>
+#include <com/sun/star/frame/XFrame2.hpp>
+#include <com/sun/star/frame/XModel.hpp>
+#include <com/sun/star/uno/Reference.hxx>
+#include <com/sun/star/util/XCloseable.hpp>
+
+#include <vcl/scheduler.hxx>
+
+#include <test/a11y/AccessibilityTools.hxx>
+
+using namespace css;
+
+void test::AccessibleTestBase::setUp()
+{
+    test::BootstrapFixture::setUp();
+
+    mxDesktop = frame::Desktop::create(mxComponentContext);
+}
+
+void test::AccessibleTestBase::close()
+{
+    if (mxDocument.is())
+    {
+        uno::Reference<util::XCloseable> xCloseable(mxDocument, 
uno::UNO_QUERY_THROW);
+        xCloseable->close(false);
+        mxDocument.clear();
+    }
+}
+
+void test::AccessibleTestBase::tearDown() { close(); }
+
+void test::AccessibleTestBase::load(const rtl::OUString& sURL)
+{
+    // make sure there is no open document in case it is called more than once
+    close();
+    mxDocument = mxDesktop->loadComponentFromURL(sURL, "_blank", 
frame::FrameSearchFlag::AUTO, {});
+
+    uno::Reference<frame::XModel> xModel(mxDocument, uno::UNO_QUERY_THROW);
+    
mxWindow.set(xModel->getCurrentController()->getFrame()->getContainerWindow());
+
+    // bring window to front
+    uno::Reference<awt::XTopWindow> xTopWindow(mxWindow, uno::UNO_QUERY_THROW);
+    xTopWindow->toFront();
+}
+
+void test::AccessibleTestBase::loadFromSrc(const rtl::OUString& sSrcPath)
+{
+    load(m_directories.getURLFromSrc(sSrcPath));
+}
+
+uno::Reference<accessibility::XAccessibleContext>
+test::AccessibleTestBase::getWindowAccessibleContext()
+{
+    uno::Reference<accessibility::XAccessible> xAccessible(mxWindow, 
uno::UNO_QUERY_THROW);
+
+    return xAccessible->getAccessibleContext();
+}
+
+bool test::AccessibleTestBase::isDocumentRole(const sal_Int16 role)
+{
+    return (role == accessibility::AccessibleRole::DOCUMENT
+            || role == accessibility::AccessibleRole::DOCUMENT_PRESENTATION
+            || role == accessibility::AccessibleRole::DOCUMENT_SPREADSHEET
+            || role == accessibility::AccessibleRole::DOCUMENT_TEXT);
+}
+
+uno::Reference<accessibility::XAccessibleContext>
+test::AccessibleTestBase::getDocumentAccessibleContext()
+{
+    uno::Reference<frame::XModel> xModel(mxDocument, uno::UNO_QUERY_THROW);
+    uno::Reference<accessibility::XAccessible> xAccessible(
+        xModel->getCurrentController()->getFrame()->getComponentWindow(), 
uno::UNO_QUERY_THROW);
+
+    return AccessibilityTools::getAccessibleObjectForPredicate(
+        xAccessible->getAccessibleContext(),
+        [](const uno::Reference<accessibility::XAccessibleContext>& xCtx) {
+            return (isDocumentRole(xCtx->getAccessibleRole())
+                    && xCtx->getAccessibleStateSet() & 
accessibility::AccessibleStateType::SHOWING);
+        });
+}
+
+uno::Reference<accessibility::XAccessibleContext>
+test::AccessibleTestBase::getFirstRelationTargetOfType(
+    const uno::Reference<accessibility::XAccessibleContext>& xContext, 
sal_Int16 relationType)
+{
+    auto relset = xContext->getAccessibleRelationSet();
+
+    if (relset.is())
+    {
+        for (sal_Int32 i = 0; i < relset->getRelationCount(); ++i)
+        {
+            const auto& rel = relset->getRelation(i);
+            if (rel.RelationType == relationType)
+            {
+                for (auto& target : rel.TargetSet)
+                {
+                    uno::Reference<accessibility::XAccessible> 
targetAccessible(target,
+                                                                               
 uno::UNO_QUERY);
+                    if (targetAccessible.is())
+                        return targetAccessible->getAccessibleContext();
+                }
+            }
+        }
+    }
+
+    return nullptr;
+}
+
+std::deque<uno::Reference<accessibility::XAccessibleContext>>
+test::AccessibleTestBase::getAllChildren(
+    const uno::Reference<accessibility::XAccessibleContext>& xContext)
+{
+    std::deque<uno::Reference<accessibility::XAccessibleContext>> children;
+    auto childCount = xContext->getAccessibleChildCount();
+
+    for (sal_Int32 i = 0; i < childCount && i < 
AccessibilityTools::MAX_CHILDREN; i++)
+    {
+        auto child = xContext->getAccessibleChild(i);
+        children.push_back(child->getAccessibleContext());
+    }
+
+    return children;
+}
+
+/** Prints the tree of accessible objects starting at @p xContext to stdout */
+void test::AccessibleTestBase::dumpA11YTree(
+    const uno::Reference<accessibility::XAccessibleContext>& xContext, const 
int depth)
+{
+    Scheduler::ProcessEventsToIdle();
+    auto xRelSet = xContext->getAccessibleRelationSet();
+
+    std::cout << AccessibilityTools::debugString(xContext);
+    /* relation set is not included in AccessibilityTools::debugString(), but 
might be useful in
+     * this context, so we compute it here */
+    if (xRelSet.is())
+    {
+        auto relCount = xRelSet->getRelationCount();
+        if (relCount)
+        {
+            std::cout << " rels=[";
+            for (sal_Int32 i = 0; i < relCount; ++i)
+            {
+                if (i > 0)
+                    std::cout << ", ";
+
+                const auto& rel = xRelSet->getRelation(i);
+                std::cout << "(type=" << 
AccessibilityTools::getRelationTypeName(rel.RelationType)
+                          << " (" << rel.RelationType << ")";
+                std::cout << " targets=[";
+                int j = 0;
+                for (auto& target : rel.TargetSet)
+                {
+                    if (j++ > 0)
+                        std::cout << ", ";
+                    uno::Reference<accessibility::XAccessible> ta(target, 
uno::UNO_QUERY_THROW);
+                    std::cout << AccessibilityTools::debugString(ta);
+                }
+                std::cout << "])";
+            }
+            std::cout << "]";
+        }
+    }
+    std::cout << std::endl;
+
+    sal_Int32 i = 0;
+    for (auto& child : getAllChildren(xContext))
+    {
+        for (int j = 0; j < depth; j++)
+            std::cout << "  ";
+        std::cout << " * child " << i++ << ": ";
+        dumpA11YTree(child, depth + 1);
+    }
+}
+
+/* see OAccessibleMenuItemComponent::GetAccessibleName() */
+static bool accessibleNameMatches(const 
uno::Reference<accessibility::XAccessibleContext>& xContext,
+                                  std::u16string_view name)
+{
+    const OUString actualName = xContext->getAccessibleName();
+
+    if (actualName == name)
+        return true;
+
+#if defined(_WIN32)
+    /* on Win32, ignore a \tSHORTCUT suffix on a menu item */
+    switch (xContext->getAccessibleRole())
+    {
+        case accessibility::AccessibleRole::MENU_ITEM:
+        case accessibility::AccessibleRole::RADIO_MENU_ITEM:
+        case accessibility::AccessibleRole::CHECK_MENU_ITEM:
+            return actualName.startsWith(name) && actualName[name.length()] == 
'\t';
+
+        default:
+            break;
+    }
+#endif
+
+    return false;
+}
+
+/** Gets a child by name (usually in a menu) */
+uno::Reference<accessibility::XAccessibleContext> 
test::AccessibleTestBase::getItemFromName(
+    const uno::Reference<accessibility::XAccessibleContext>& xMenuCtx, 
std::u16string_view name)
+{
+    auto childCount = xMenuCtx->getAccessibleChildCount();
+
+    std::cout << "looking up item " << OUString(name) << " in "
+              << AccessibilityTools::debugString(xMenuCtx) << std::endl;
+    for (sal_Int32 i = 0; i < childCount && i < 
AccessibilityTools::MAX_CHILDREN; i++)
+    {
+        auto item = xMenuCtx->getAccessibleChild(i)->getAccessibleContext();
+        if (accessibleNameMatches(item, name))
+        {
+            std::cout << "-> found " << AccessibilityTools::debugString(item) 
<< std::endl;
+            return item;
+        }
+    }
+
+    std::cout << "-> NOT FOUND!" << std::endl;
+    std::cout << "   Contents was: ";
+    dumpA11YTree(xMenuCtx, 1);
+
+    return uno::Reference<accessibility::XAccessibleContext>();
+}
+
+bool test::AccessibleTestBase::activateMenuItem(
+    const uno::Reference<accessibility::XAccessibleAction>& xAction)
+{
+    // assume first action is the right one, there's not description anyway
+    CPPUNIT_ASSERT_EQUAL(sal_Int32(1), xAction->getAccessibleActionCount());
+    if (xAction->doAccessibleAction(0))
+    {
+        Scheduler::ProcessEventsToIdle();
+        return true;
+    }
+    return false;
+}
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab cinoptions=b1,g0,N-s 
cinkeys+=0=break: */
diff --git a/test/source/a11y/swaccessibletestbase.cxx 
b/test/source/a11y/swaccessibletestbase.cxx
new file mode 100644
index 000000000000..b43d65c0cf78
--- /dev/null
+++ b/test/source/a11y/swaccessibletestbase.cxx
@@ -0,0 +1,135 @@
+/* -*- 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 <test/a11y/swaccessibletestbase.hxx>
+
+#include <com/sun/star/accessibility/AccessibleRelationType.hpp>
+#include <com/sun/star/accessibility/XAccessibleContext.hpp>
+#include <com/sun/star/accessibility/XAccessibleText.hpp>
+#include <com/sun/star/uno/Reference.hxx>
+
+#include <rtl/ustrbuf.hxx>
+
+#include <test/a11y/AccessibilityTools.hxx>
+
+using namespace css;
+
+uno::Reference<accessibility::XAccessibleContext>
+test::SwAccessibleTestBase::getPreviousFlowingSibling(
+    const uno::Reference<accessibility::XAccessibleContext>& xContext)
+{
+    return getFirstRelationTargetOfType(xContext,
+                                        
accessibility::AccessibleRelationType::CONTENT_FLOWS_FROM);
+}
+
+uno::Reference<accessibility::XAccessibleContext> 
test::SwAccessibleTestBase::getNextFlowingSibling(
+    const uno::Reference<accessibility::XAccessibleContext>& xContext)
+{
+    return getFirstRelationTargetOfType(xContext,
+                                        
accessibility::AccessibleRelationType::CONTENT_FLOWS_TO);
+}
+
+/* Care has to be taken not to walk sideways as the relation is also used
+ * with children of nested containers (possibly as the "natural"/"perceived" 
flow?). */
+std::deque<uno::Reference<accessibility::XAccessibleContext>>
+test::SwAccessibleTestBase::getAllChildren(
+    const uno::Reference<accessibility::XAccessibleContext>& xContext)
+{
+    /* first, get all "natural" children */
+    auto children = AccessibleTestBase::getAllChildren(xContext);
+    if (!children.size())
+        return children;
+
+    /* then, try and find flowing siblings at the same levels that are not 
included in the list */
+    /* first, backwards: */
+    auto child = getPreviousFlowingSibling(children.front());
+    while (child.is() && children.size() < AccessibilityTools::MAX_CHILDREN)
+    {
+        auto childParent = child->getAccessibleParent();
+        if (childParent.is()
+            && AccessibilityTools::equals(xContext, 
childParent->getAccessibleContext()))
+            children.push_front(child);
+        child = getPreviousFlowingSibling(child);
+    }
+    /* then forward */
+    child = getNextFlowingSibling(children.back());
+    while (child.is() && children.size() < AccessibilityTools::MAX_CHILDREN)
+    {
+        auto childParent = child->getAccessibleParent();
+        if (childParent.is()
+            && AccessibilityTools::equals(xContext, 
childParent->getAccessibleContext()))
+            children.push_back(child);
+        child = getNextFlowingSibling(child);
+    }
+
+    return children;
+}
+
+void test::SwAccessibleTestBase::collectText(
+    const uno::Reference<accessibility::XAccessibleContext>& xContext, 
rtl::OUStringBuffer& buffer,
+    bool onlyChildren)
+{
+    const auto& roleName = 
AccessibilityTools::getRoleName(xContext->getAccessibleRole());
+
+    std::cout << "collecting text for child of role " << roleName << "..." << 
std::endl;
+
+    if (!onlyChildren)
+    {
+        const struct
+        {
+            std::u16string_view name;
+            rtl::OUString value;
+        } attrs[] = {
+            { u"name", xContext->getAccessibleName() },
+            { u"description", xContext->getAccessibleDescription() },
+        };
+
+        buffer.append('<');
+        buffer.append(roleName);
+        for (auto& attr : attrs)
+        {
+            if (attr.value.getLength() == 0)
+                continue;
+            buffer.append(' ');
+            buffer.append(attr.name);
+            buffer.append(u"=\"" + attr.value.replaceAll(u"\"", u"&quot;") + 
"\"");
+        }
+        buffer.append('>');
+    }
+    auto openTagLength = buffer.getLength();
+
+    uno::Reference<accessibility::XAccessibleText> xText(xContext, 
uno::UNO_QUERY);
+    if (xText.is())
+        buffer.append(xText->getText());
+
+    for (auto& childContext : getAllChildren(xContext))
+        collectText(childContext, buffer);
+
+    if (!onlyChildren)
+    {
+        if (buffer.getLength() != openTagLength)
+            buffer.append("</" + roleName + ">");
+        else
+        {
+            /* there was no content, so make is a short tag for more concise 
output */
+            buffer[openTagLength - 1] = '/';
+            buffer.append('>');
+        }
+    }
+}
+
+OUString test::SwAccessibleTestBase::collectText(
+    const uno::Reference<accessibility::XAccessibleContext>& xContext)
+{
+    rtl::OUStringBuffer buf;
+    collectText(xContext, buf, isDocumentRole(xContext->getAccessibleRole()));
+    return buf.makeStringAndClear();
+}
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab cinoptions=b1,g0,N-s 
cinkeys+=0=break: */

Reply via email to