diff options
author | Colomban Wendling <cwendling@hypra.fr> | 2022-07-21 21:51:21 +0200 |
---|---|---|
committer | Michael Weghorn <m.weghorn@posteo.de> | 2022-08-01 17:03:40 +0200 |
commit | 0185ddd6d5f0324ba57b3fa36229103a6b27138e (patch) | |
tree | 81a2249571acbfa25391bb1fcfa05e8d8c5e602d /test | |
parent | 56d5a31d6ec2b45e43659be4b5fd4964de29e61f (diff) |
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.weghorn@posteo.de>
Diffstat (limited to 'test')
-rw-r--r-- | test/Library_subsequenttest.mk | 2 | ||||
-rw-r--r-- | test/source/a11y/AccessibilityTools.cxx | 50 | ||||
-rw-r--r-- | test/source/a11y/accessibletestbase.cxx | 260 | ||||
-rw-r--r-- | test/source/a11y/swaccessibletestbase.cxx | 135 |
4 files changed, 434 insertions, 13 deletions
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""") + "\""); + } + 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: */ |