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""") + "\""); + } + 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: */