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)