include/test/a11y/AccessibilityTools.hxx |   97 +++++++++++++++++++++++++++++++
 include/test/a11y/accessibletestbase.hxx |    9 ++
 sw/qa/extras/accessibility/dialogs.cxx   |   81 +++++++++++++++++++++++++
 test/source/a11y/AccessibilityTools.cxx  |   20 ++++++
 test/source/a11y/accessibletestbase.cxx  |   27 ++++++++
 5 files changed, 234 insertions(+)

New commits:
commit 7801b5f7562a8d1660053a2745b4f6e97b555bb2
Author:     Colomban Wendling <cwendl...@hypra.fr>
AuthorDate: Thu Nov 3 15:39:38 2022 +0100
Commit:     Michael Weghorn <m.wegh...@posteo.de>
CommitDate: Fri Mar 3 10:49:20 2023 +0000

    test: Add helpers to get a specific object and tab to it
    
    Add a tabTo() variant that accepts a target object that should gain
    focus. This is useful to work around focus issues in the implementation
    (although they should be reported and fixed), and it's a simpler and
    more efficient API if the caller happens to already have a reference to
    the target object.
    
    This also adds AccessibilityTools::getAccessibleObjectForName() as a
    usually more useful alternative to
    AccessibilityTools::getAccessibleObjectForRole() as it allows to easily
    match both role and name. There is also a template version accepting
    multiple role and name pairs to further refine the selected object.
    
    Together, it makes it easy to obtain the target object and tab to it,
    in situations where the other tabTo() variant either doesn't work for
    some reason (as mentioned above), or is not the slickest solution.
    
    Change-Id: I6a41b147331132711ac84776bb43ad24a091ba24
    Reviewed-on: https://gerrit.libreoffice.org/c/core/+/142260
    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 88276bac700d..749be0635198 100644
--- a/include/test/a11y/AccessibilityTools.hxx
+++ b/include/test/a11y/AccessibilityTools.hxx
@@ -53,6 +53,103 @@ public:
     getAccessibleObjectForRole(const 
css::uno::Reference<css::accessibility::XAccessible>& xacc,
                                sal_Int16 role);
 
+    /**
+     * @brief Gets a descendant of @p xCtx (or @p xCtx itself) that matches 
the given role and name.
+     * @param xCtx An accessible context object to start the search from
+     * @param role The role of the object to look up.
+     * @param name The name of the object to look up.
+     * @returns The found object, or @c nullptr if not found.
+     *
+     * Finds a descendant of @p xCtx (or @p xCtx itself) that matches @p role 
and @p name.
+     * @code
+     * AccessibilityTools::getAccessibleObjectForName(
+     *     css::accessibility::AccessibleRole::PUSH_BUTTON, u"Insert");
+     * @endcode
+     *
+     * @see AccessibilityTools::getAccessibleObjectForPredicate() */
+    static css::uno::Reference<css::accessibility::XAccessibleContext> 
getAccessibleObjectForName(
+        const css::uno::Reference<css::accessibility::XAccessibleContext>& 
xCtx,
+        const sal_Int16 role, std::u16string_view name);
+    static inline css::uno::Reference<css::accessibility::XAccessibleContext>
+    getAccessibleObjectForName(const 
css::uno::Reference<css::accessibility::XAccessible>& xAcc,
+                               const sal_Int16 role, std::u16string_view name)
+    {
+        return getAccessibleObjectForName(xAcc->getAccessibleContext(), role, 
name);
+    }
+
+    /**
+     * @brief Gets a descendant of @p xCtx (or @p xCtx itself) that matches 
the last given role and
+     *        name pair, and has ancestors matching the leading pairs in the 
given order.
+     * @param xCtx An accessible context to start the search from.
+     * @param role The role of the first ancestor to match.
+     * @param name The name of the first ancestor to match.
+     * @param Ts...args Additional role and name pairs of ancestors, ending 
with the role and name
+     *                  pair of the target object to match.
+     * @returns The found object, or @c nullptr if not found.
+     *
+     * Specialized version allowing specifying arbitrary objects on the path 
to the target one. Not
+     * all objects have to be matched, but there have to be ancestors matching 
in the given order.
+     * This is useful to easily solve conflicts if there are more than one 
possible match.
+     *
+     * This can be used to find an "Insert" push button inside a panel named 
"Some group" for
+     * example, as shown below:
+     *
+     * @code
+     * AccessibilityTools::getAccessibleObjectForName(
+     *     css::accessibility::AccessibleRole::PANEL, u"Some group",
+     *     css::accessibility::AccessibleRole::PUSH_BUTTON, u"Insert");
+     * @endcode
+     *
+     * @note This returns the first match in the object tree when walking it 
depth-first.  Depending
+     *       on the tree, this might not be able to find the expected match, 
e.g. if there is a
+     *       first match with intermediate unmatched objects, and the target 
has the same tree but
+     *       without intermediate objects that can be used to refine the 
search and prevent the
+     *       unwanted tree to match.  The same issue arises with two identical 
trees, yet in that
+     *       case no walking scenario could solve it automatically anyway.
+     *       In such situations, a custom @c getAccessibleObjectForPredicate() 
call, or successive
+     *       lookups interleaved with specific child lookups are likely the 
best solution.
+     *
+     * @see getAccessibleObjectForPredicate().
+     */
+    /* TODO: reimplement as IDDFS or BFS?  Not sure the additional 
complexity/performance costs
+     *       warrant it. */
+    template <typename... Ts>
+    static css::uno::Reference<css::accessibility::XAccessibleContext> 
getAccessibleObjectForName(
+        const css::uno::Reference<css::accessibility::XAccessibleContext>& 
xCtx,
+        const sal_Int16 role, std::u16string_view name, Ts... args)
+    {
+        auto nChildren = xCtx->getAccessibleChildCount();
+
+        // try self first
+        if (xCtx->getAccessibleRole() == role && nameEquals(xCtx, name))
+        {
+            for (decltype(nChildren) i = 0; i < nChildren && i < MAX_CHILDREN; 
i++)
+            {
+                if (auto xMatchChild
+                    = getAccessibleObjectForName(xCtx->getAccessibleChild(i), 
args...))
+                    return xMatchChild;
+            }
+        }
+
+        // if not found, try at a deeper level
+        for (decltype(nChildren) i = 0; i < nChildren && i < MAX_CHILDREN; i++)
+        {
+            if (auto xMatchChild
+                = getAccessibleObjectForName(xCtx->getAccessibleChild(i), 
role, name, args...))
+                return xMatchChild;
+        }
+
+        return nullptr;
+    }
+
+    template <typename... Ts>
+    static inline css::uno::Reference<css::accessibility::XAccessibleContext>
+    getAccessibleObjectForName(const 
css::uno::Reference<css::accessibility::XAccessible>& xAcc,
+                               const sal_Int16 role, std::u16string_view name, 
Ts... args)
+    {
+        return getAccessibleObjectForName(xAcc->getAccessibleContext(), role, 
name, args...);
+    }
+
     static bool equals(const 
css::uno::Reference<css::accessibility::XAccessible>& xacc1,
                        const 
css::uno::Reference<css::accessibility::XAccessible>& xacc2);
     static bool equals(const 
css::uno::Reference<css::accessibility::XAccessibleContext>& xctx1,
diff --git a/include/test/a11y/accessibletestbase.hxx 
b/include/test/a11y/accessibletestbase.hxx
index 745f9fae2458..0048edcd8589 100644
--- a/include/test/a11y/accessibletestbase.hxx
+++ b/include/test/a11y/accessibletestbase.hxx
@@ -176,6 +176,10 @@ protected:
           const std::u16string_view name,
           const EventPosterHelperBase* pEventPosterHelper = nullptr);
 
+    static bool tabTo(const 
css::uno::Reference<css::accessibility::XAccessible>& xRoot,
+                      const 
css::uno::Reference<css::accessibility::XAccessibleContext>& xChild,
+                      const EventPosterHelperBase* pEventPosterHelper = 
nullptr);
+
     /* Dialog handling */
     class Dialog : public test::EventPosterHelper
     {
@@ -203,6 +207,11 @@ protected:
         {
             return AccessibleTestBase::tabTo(getAccessible(), role, name, 
this);
         }
+
+        bool tabTo(const 
css::uno::Reference<css::accessibility::XAccessibleContext>& xChild)
+        {
+            return AccessibleTestBase::tabTo(getAccessible(), xChild, this);
+        }
     };
 
     class DialogWaiter
diff --git a/sw/qa/extras/accessibility/dialogs.cxx 
b/sw/qa/extras/accessibility/dialogs.cxx
index ab03af5e22db..13c2fd0cb750 100644
--- a/sw/qa/extras/accessibility/dialogs.cxx
+++ b/sw/qa/extras/accessibility/dialogs.cxx
@@ -17,6 +17,87 @@
 
 using namespace css;
 
+// FIXME: dialog doesn't pop up on macos and doesn't close on win32...
+#if !defined(_WIN32) && !defined(MACOSX)
+CPPUNIT_TEST_FIXTURE(test::SwAccessibleTestBase, 
BasicTestSpecialCharactersDialog)
+{
+    load(u"private:factory/swriter");
+
+    auto dialogWaiter = awaitDialog(u"Special Characters", [this](Dialog& 
dialog) {
+        dumpA11YTree(dialog.getAccessible()->getAccessibleContext());
+
+        CPPUNIT_ASSERT_EQUAL(
+            AccessibilityTools::getAccessibleObjectForName(
+                dialog.getAccessible(), accessibility::AccessibleRole::TEXT, 
u"Search:"),
+            getFocusedObject(dialog.getAccessible()));
+
+        // search for (c) symbol
+        dialog.postExtTextEventAsync(u"copyright");
+        Scheduler::ProcessEventsToIdle();
+
+        CPPUNIT_ASSERT(dialog.tabTo(accessibility::AccessibleRole::TABLE_CELL, 
u"©"));
+
+        /* here there is a bug that the "insert" action is not working right 
away, even though we
+         * have the right character selected.  Move around the table to 
actually trigger the
+         * selection logic.
+         * See https://bugs.documentfoundation.org/show_bug.cgi?id=153806 */
+        dialog.postKeyEventAsync(0, awt::Key::RIGHT);
+        dialog.postKeyEventAsync(0, awt::Key::LEFT);
+
+        /* there was a focus issue in this dialog: the table holding the 
characters always had the
+         * selected element as focused, even when tabbing outside.
+         * Fixed with https://gerrit.libreoffice.org/c/core/+/147660.
+         * Anyway, we still use the target element match API to also exercise 
it. */
+        auto xChild = AccessibilityTools::getAccessibleObjectForName(
+            dialog.getAccessible(), 
accessibility::AccessibleRole::PUSH_BUTTON, u"Insert");
+        CPPUNIT_ASSERT(xChild);
+        CPPUNIT_ASSERT(dialog.tabTo(xChild));
+        dialog.postKeyEventAsync(0, awt::Key::RETURN);
+
+        Scheduler::ProcessEventsToIdle();
+    });
+
+    CPPUNIT_ASSERT(activateMenuItem(u"Insert", u"Special Character..."));
+    CPPUNIT_ASSERT(dialogWaiter->waitEndDialog());
+
+    CPPUNIT_ASSERT_EQUAL(rtl::OUString(u"<PARAGRAPH>©</PARAGRAPH>"), 
collectText());
+}
+#endif
+
+// FIXME: dialog doesn't pop up on macos and doesn't close on win32...
+#if !defined(_WIN32) && !defined(MACOSX)
+/* checks for the fix from https://gerrit.libreoffice.org/c/core/+/147660 */
+CPPUNIT_TEST_FIXTURE(test::SwAccessibleTestBase, 
TestSpecialCharactersDialogFocus)
+{
+    load(u"private:factory/swriter");
+
+    auto dialogWaiter = awaitDialog(u"Special Characters", [](Dialog& dialog) {
+        CPPUNIT_ASSERT(dialog.tabTo(accessibility::AccessibleRole::TABLE_CELL, 
u" "));
+
+        /* as there is a bug that focusing the character table doesn't enable 
the Insert button
+         * (https://bugs.documentfoundation.org/show_bug.cgi?id=153806), we 
move to another cell
+         * so it works -- and we actually don't care which one it is */
+        dialog.postKeyEventAsync(0, awt::Key::DOWN);
+        Scheduler::ProcessEventsToIdle();
+
+        CPPUNIT_ASSERT_EQUAL(
+            AccessibilityTools::getAccessibleObjectForName(
+                dialog.getAccessible(), 
accessibility::AccessibleRole::TABLE_CELL, u"0"),
+            getFocusedObject(dialog.getAccessible()));
+
+        
CPPUNIT_ASSERT(dialog.tabTo(accessibility::AccessibleRole::PUSH_BUTTON, 
u"Insert"));
+        dialog.postKeyEventAsync(0, awt::Key::RETURN);
+
+        Scheduler::ProcessEventsToIdle();
+    });
+
+    CPPUNIT_ASSERT(activateMenuItem(u"Insert", u"Special Character..."));
+    CPPUNIT_ASSERT(dialogWaiter->waitEndDialog());
+
+    CPPUNIT_ASSERT_EQUAL(rtl::OUString(u"<PARAGRAPH>0</PARAGRAPH>"), 
collectText());
+}
+#endif
+
 // FIXME: dialog doesn't pop up on macos and doesn't close on win32...
 #if !defined(_WIN32) && !defined(MACOSX)
 CPPUNIT_TEST_FIXTURE(test::SwAccessibleTestBase, BasicTestHyperlinkDialog)
diff --git a/test/source/a11y/AccessibilityTools.cxx 
b/test/source/a11y/AccessibilityTools.cxx
index 8afc1687c889..137b1bdc3a97 100644
--- a/test/source/a11y/AccessibilityTools.cxx
+++ b/test/source/a11y/AccessibilityTools.cxx
@@ -84,6 +84,26 @@ AccessibilityTools::getAccessibleObjectForRole(
     return getAccessibleObjectForRole(xacc->getAccessibleContext(), role);
 }
 
+/* this is basically the same as getAccessibleObjectForPredicate() but 
specialized for efficiency,
+ * and because the template version will not work with 
getAccessibleObjectForPredicate() anyway */
+css::uno::Reference<css::accessibility::XAccessibleContext>
+AccessibilityTools::getAccessibleObjectForName(
+    const css::uno::Reference<css::accessibility::XAccessibleContext>& xCtx, 
const sal_Int16 role,
+    std::u16string_view name)
+{
+    if (xCtx->getAccessibleRole() == role && nameEquals(xCtx, name))
+        return xCtx;
+
+    auto nChildren = xCtx->getAccessibleChildCount();
+    for (decltype(nChildren) i = 0; i < nChildren && i < 
AccessibilityTools::MAX_CHILDREN; i++)
+    {
+        if (auto xMatchChild = 
getAccessibleObjectForName(xCtx->getAccessibleChild(i), role, name))
+            return xMatchChild;
+    }
+
+    return nullptr;
+}
+
 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
index d9cfaa731f95..579d4ba2bfd4 100644
--- a/test/source/a11y/accessibletestbase.cxx
+++ b/test/source/a11y/accessibletestbase.cxx
@@ -317,6 +317,33 @@ test::AccessibleTestBase::tabTo(const 
uno::Reference<accessibility::XAccessible>
     return nullptr;
 }
 
+bool test::AccessibleTestBase::tabTo(
+    const uno::Reference<accessibility::XAccessible>& xRoot,
+    const uno::Reference<accessibility::XAccessibleContext>& xChild,
+    const EventPosterHelperBase* pEventPosterHelper)
+{
+    AccessibleEventPosterHelper eventHelper;
+    if (!pEventPosterHelper)
+    {
+        eventHelper.setWindow(xRoot);
+        pEventPosterHelper = &eventHelper;
+    }
+
+    std::cout << "Tabbing to " << AccessibilityTools::debugString(xChild) << 
"..." << std::endl;
+    for (int i = 0; i < 100; i++)
+    {
+        if (xChild->getAccessibleStateSet() & 
accessibility::AccessibleStateType::FOCUSED)
+            return true;
+
+        std::cout << "  no match, sending <TAB>" << std::endl;
+        pEventPosterHelper->postKeyEventAsync(0, awt::Key::TAB);
+        Scheduler::ProcessEventsToIdle();
+    }
+
+    std::cerr << "NOT FOUND" << std::endl;
+    return false;
+}
+
 /* Dialog handling */
 
 test::AccessibleTestBase::Dialog::Dialog(vcl::Window* pWindow, bool bAutoClose)

Reply via email to