diff options
-rw-r--r-- | include/test/a11y/accessibletestbase.hxx | 99 | ||||
-rw-r--r-- | test/CppunitTest_test_a11y.mk | 31 | ||||
-rw-r--r-- | test/Module_test.mk | 1 | ||||
-rw-r--r-- | test/qa/cppunit/dialog.cxx | 66 | ||||
-rw-r--r-- | test/source/a11y/accessibletestbase.cxx | 156 |
5 files changed, 353 insertions, 0 deletions
diff --git a/include/test/a11y/accessibletestbase.hxx b/include/test/a11y/accessibletestbase.hxx index 50a39f63a7dd..913e24221353 100644 --- a/include/test/a11y/accessibletestbase.hxx +++ b/include/test/a11y/accessibletestbase.hxx @@ -24,6 +24,7 @@ #include <com/sun/star/uno/Reference.hxx> #include <vcl/ITiledRenderable.hxx> +#include <vcl/window.hxx> #include <rtl/ustring.hxx> #include <test/bootstrapfixture.hxx> @@ -129,6 +130,104 @@ protected: return activateMenuItem(menuBar, names...); } + /* Dialog handling */ + class Dialog + { + friend class AccessibleTestBase; + + private: + VclPtr<vcl::Window> mxWindow; + bool mbAutoClose; + + Dialog(vcl::Window* pWindow, bool bAutoClose = true); + + public: + virtual ~Dialog(); + + explicit operator bool() const { return mxWindow && !mxWindow->isDisposed(); } + bool operator!() const { return !bool(*this); } + + void setAutoClose(bool bAutoClose) { mbAutoClose = bAutoClose; } + + css::uno::Reference<css::accessibility::XAccessible> getAccessible() const + { + return mxWindow ? mxWindow->GetAccessible() : nullptr; + } + + bool close(sal_Int32 result = VclResponseType::RET_CANCEL); + }; + + class DialogWaiter + { + public: + virtual ~DialogWaiter() {} + + /** + * @brief Waits for the associated dialog to close + * @param nTimeoutMs Maximum delay to wait the dialog for + * @returns @c true if the dialog closed, @c false if timeout was reached + * + * @throws css::uno::RuntimeException if an unexpected dialog poped up instead of the + * expected one. + * @throws Any exception that the user callback supplied to awaitDialog() might have thrown. + */ + virtual bool waitEndDialog(sal_uInt64 nTimeoutMs = 3000) = 0; + }; + + /** + * @brief Helper to call user code when a given dialog opens + * @param name The title of the dialog window to wait for + * @param callback The user code to run when the given dialog opens + * @param bAutoClose Whether to automatically cancel the dialog after the user code finished, if + * the dialog is still there. You should leave this to @c true unless you + * know exactly what you are doing, see below. + * @returns A @c DialogWaiter wrapper on which call waitEndDialog() after having triggered the + * dialog in some way. + * + * This function makes it fairly easy and safe to execute code once a dialog pops up: + * @code + * auto waiter = awaitDialog(u"Special Characters", [this](Dialog &dialog) { + * // for example, something like this: + * // something(); + * // CPPUNIT_ASSERT(somethingElse); + * }); + * CPPUNIT_ASSERT(activateMenuItem(u"Some menu", u"Some Item Triggering a Dialog...")); + * CPPUNIT_ASSERT(waiter->waitEndDialog()); + * @endcode + * + * @note The user code might actually be executed before DialogWaiter::waitEndDialog() is + * called. It is actually likely to be called at the time the call that triggers the + * dialog happens. However, as letting an exception slip in a event handler is likely to + * cause problems, exceptions are forwarded to the DialogWaiter::waitEndDialog() call. + * However, note that you cannot rely on something like this: + * @code + * int foo = 0; + * auto waiter = awaitDialog(u"Some Dialog", [&foo](Dialog&) { + * CPPUNIT_ASSERT_EQUAL(1, foo); + * }); + * CPPUNIT_ASSERT(activateMenuItem(u"Some menu", u"Some Item Triggering a Dialog...")); + * foo = 1; // here, the callback likely already ran as a result of the + * // Scheduler::ProcessEventsToIdle() call that activateMenuItem() did. + * CPPUNIT_ASSERT(waiter->waitEndDialog()); + * @endcode + * + * @warning You should almost certainly always leave @p bAutoClose to @c true. If it is set to + * @c false, you have to take extreme care: + * - The dialog will not be canceled if the user code raises an exception. + * - If the dialog is run through Dialog::Execute(), control won't return to the test + * body until the dialog is closed. This means that the only ways to execute code + * until then is a separate thread or via code dispatched by the main loop. + * Thus, you have to make sure you DO close the dialog some way or another yourself + * in order for the test code to terminate at some point. + * - If the dialog doesn't use Dialog::Execute() but is rather similar to a second + * separate window (e.g. non-modal), you might still have to close the dialog before + * closing the test document is possible without a CloseVetoException -- which might + * badly break the test run. + */ + static std::shared_ptr<DialogWaiter> awaitDialog(const std::u16string_view name, + std::function<void(Dialog&)> callback, + bool bAutoClose = true); + public: virtual void setUp() override; virtual void tearDown() override; diff --git a/test/CppunitTest_test_a11y.mk b/test/CppunitTest_test_a11y.mk new file mode 100644 index 000000000000..22d1c8bc5576 --- /dev/null +++ b/test/CppunitTest_test_a11y.mk @@ -0,0 +1,31 @@ +# -*- 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,test_a11y)) + +$(eval $(call gb_CppunitTest_add_exception_objects,test_a11y, \ + test/qa/cppunit/dialog \ +)) + +$(eval $(call gb_CppunitTest_use_libraries,test_a11y, \ + sal \ + cppu \ + subsequenttest \ + test \ +)) + +$(eval $(call gb_CppunitTest_use_sdk_api,test_a11y)) +$(eval $(call gb_CppunitTest_use_rdb,test_a11y,services)) +$(eval $(call gb_CppunitTest_use_ure,test_a11y)) +$(eval $(call gb_CppunitTest_use_vcl,test_a11y)) + +$(eval $(call gb_CppunitTest_use_instdir_configuration,test_a11y)) +$(eval $(call gb_CppunitTest_use_common_configuration,test_a11y)) + +# vim: set noet sw=4 ts=4: diff --git a/test/Module_test.mk b/test/Module_test.mk index 080cc855b28c..99e722905151 100644 --- a/test/Module_test.mk +++ b/test/Module_test.mk @@ -20,6 +20,7 @@ $(eval $(call gb_Module_add_targets,test,\ )) $(eval $(call gb_Module_add_check_targets,test,\ + CppunitTest_test_a11y \ CppunitTest_test_xpath \ )) diff --git a/test/qa/cppunit/dialog.cxx b/test/qa/cppunit/dialog.cxx new file mode 100644 index 000000000000..f64e7d13a68c --- /dev/null +++ b/test/qa/cppunit/dialog.cxx @@ -0,0 +1,66 @@ +/* -*- 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> + +// FIXME: dialog doesn't pop up on macos and doesn't close on win32... +#if !defined(MACOSX) && !defined(_WIN32) +/* Checks an unexpected dialog opening (instead of the expected one) is properly caught, as it would + * otherwise block the test potentially indefinitely */ +CPPUNIT_TEST_FIXTURE(test::AccessibleTestBase, SelfTestIncorrectDialog) +{ + load(u"private:factory/swriter"); + + auto dialogWaiter = awaitDialog(u"This Dialog Does Not Exist", [](Dialog&) { + CPPUNIT_ASSERT_MESSAGE("This code should not be reached", false); + }); + + CPPUNIT_ASSERT(activateMenuItem(u"Insert", u"Section...")); + /* Make sure an incorrect dialog poping up is caught and raises. The exception is thrown in + * waitEndDialog() for consistency even though the error itself is likely to have been triggered + * by the activateMenuItem() call above */ + CPPUNIT_ASSERT_THROW(dialogWaiter->waitEndDialog(), css::uno::RuntimeException); +} +#endif + +// FIXME: dialog doesn't pop up on macos and doesn't close on win32... +#if !defined(MACOSX) && !defined(_WIN32) +/* Checks that an exception in the dialog callback code is properly handled and won't disturb + * subsequent tests if caught -- especially that DialogWaiter::waitEndDialog() won't timeout. */ +CPPUNIT_TEST_FIXTURE(test::AccessibleTestBase, SelfTestThrowInDialogCallback) +{ + load(u"private:factory/swriter"); + + class DummyException : public std::exception + { + }; + + auto dialogWaiter = awaitDialog(u"Hyperlink", [](Dialog&) { throw DummyException(); }); + + CPPUNIT_ASSERT(activateMenuItem(u"Insert", u"Hyperlink...")); + CPPUNIT_ASSERT_THROW(dialogWaiter->waitEndDialog(), DummyException); +} +#endif + +// Checks timeout if dialog does not show up as expected +CPPUNIT_TEST_FIXTURE(test::AccessibleTestBase, SelfTestNoDialog) +{ + load(u"private:factory/swriter"); + + auto dialogWaiter = awaitDialog(u"This Dialog Did Not Show Up", [](Dialog&) { + CPPUNIT_ASSERT_MESSAGE("This code should not be reached", false); + }); + + // as we don't actually call any dialog up, this should fail after a timeout + CPPUNIT_ASSERT(!dialogWaiter->waitEndDialog()); +} + +CPPUNIT_PLUGIN_IMPLEMENT(); + +/* vim:set shiftwidth=4 softtabstop=4 expandtab cinoptions=b1,g0,N-s cinkeys+=0=break: */ diff --git a/test/source/a11y/accessibletestbase.cxx b/test/source/a11y/accessibletestbase.cxx index e7732e0d6a7d..5566eb6cd9a0 100644 --- a/test/source/a11y/accessibletestbase.cxx +++ b/test/source/a11y/accessibletestbase.cxx @@ -16,6 +16,7 @@ #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/XDialog2.hpp> #include <com/sun/star/awt/XTopWindow.hpp> #include <com/sun/star/frame/Desktop.hpp> #include <com/sun/star/frame/FrameSearchFlag.hpp> @@ -23,9 +24,12 @@ #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/uno/RuntimeException.hpp> #include <com/sun/star/util/XCloseable.hpp> #include <vcl/scheduler.hxx> +#include <vcl/svapp.hxx> +#include <vcl/window.hxx> #include <test/a11y/AccessibilityTools.hxx> @@ -231,4 +235,156 @@ bool test::AccessibleTestBase::activateMenuItem( return false; } +/* Dialog handling */ + +test::AccessibleTestBase::Dialog::Dialog(vcl::Window* pWindow, bool bAutoClose) + : mxWindow(pWindow) + , mbAutoClose(bAutoClose) +{ + CPPUNIT_ASSERT(pWindow); + CPPUNIT_ASSERT(pWindow->IsDialog()); +} + +test::AccessibleTestBase::Dialog::~Dialog() +{ + if (mbAutoClose) + close(); +} + +bool test::AccessibleTestBase::Dialog::close(sal_Int32 result) +{ + if (mxWindow && !mxWindow->isDisposed()) + { + uno::Reference<awt::XDialog2> xDialog2(mxWindow->GetComponentInterface(), + uno::UNO_QUERY_THROW); + xDialog2->endDialog(result); + return mxWindow->isDisposed(); + } + return true; +} + +std::shared_ptr<test::AccessibleTestBase::DialogWaiter> +test::AccessibleTestBase::awaitDialog(const std::u16string_view name, + std::function<void(Dialog&)> callback, bool bAutoClose) +{ + /* Helper class to wait on a dialog to pop up and to close, running user code between the + * two. This has to work both for "other window"-style dialogues (non-modal), as well as + * for modal dialogues using Dialog::Execute() (which runs a nested main loop, hence + * blocking our test flow execution. + * The approach here is to wait on the WindowActivate event for the dialog, and run the + * test code in there. Then, close the dialog if not already done, resuming normal flow to + * the caller. */ + class ListenerHelper : public DialogWaiter + { + DialogCancelMode miPreviousDialogCancelMode; + Link<VclSimpleEvent&, void> mLink; + bool mbWaitingForDialog; + std::exception_ptr mpException; + std::u16string_view msName; + std::function<void(Dialog&)> mCallback; + bool mbAutoClose; + + public: + virtual ~ListenerHelper() + { + Application::SetDialogCancelMode(miPreviousDialogCancelMode); + Application::RemoveEventListener(mLink); + } + + ListenerHelper(const std::u16string_view& name, std::function<void(Dialog&)> callback, + bool bAutoClose) + : mbWaitingForDialog(true) + , msName(name) + , mCallback(callback) + , mbAutoClose(bAutoClose) + { + mLink = LINK(this, ListenerHelper, eventListener); + Application::AddEventListener(mLink); + + miPreviousDialogCancelMode = Application::GetDialogCancelMode(); + Application::SetDialogCancelMode(DialogCancelMode::Off); + } + + private: + // mimic IMPL_LINK inline + static void LinkStubeventListener(void* instance, VclSimpleEvent& event) + { + static_cast<ListenerHelper*>(instance)->eventListener(event); + } + + void eventListener(VclSimpleEvent& event) + { + assert(mbWaitingForDialog); + + if (event.GetId() != VclEventId::WindowActivate) + return; + + auto pWin = static_cast<VclWindowEvent*>(&event)->GetWindow(); + + if (!pWin->IsDialog()) + return; + + mbWaitingForDialog = false; + + // remove ourselves, we don't want to run again + Application::RemoveEventListener(mLink); + + /* bind the dialog before checking its name so auto-close can kick in if anything + * fails/throws */ + Dialog dialog(pWin, true); + + /* The poping up dialog ought to be the right one, or something's fishy and + * we're bound to failure (e.g. waiting on a dialog that either will never come, or + * that will not run after the current one -- deadlock style) */ + if (msName != pWin->GetText()) + { + mpException = std::make_exception_ptr(css::uno::RuntimeException( + "Unexpected dialog '" + pWin->GetText() + "' opened instead of the expected '" + + msName + "'")); + } + else + { + std::cout << "found dialog, calling user callback" << std::endl; + + // set the real requested auto close now we're just calling the user callback + dialog.setAutoClose(mbAutoClose); + + try + { + mCallback(dialog); + } + catch (...) + { + mpException = std::current_exception(); + } + } + } + + public: + virtual bool waitEndDialog(sal_uInt64 nTimeoutMs) override + { + /* Usually this loop will actually never run at all because a previous + * Scheduler::ProcessEventsToIdle() would have triggered the dialog already, but we + * can't be sure of that or of delays, so be safe and wait with a timeout. */ + if (mbWaitingForDialog) + { + Timer aTimer("wait for dialog"); + aTimer.SetTimeout(nTimeoutMs); + aTimer.Start(); + do + { + Application::Yield(); + } while (mbWaitingForDialog && aTimer.IsActive()); + } + + if (mpException) + std::rethrow_exception(mpException); + + return !mbWaitingForDialog; + } + }; + + return std::make_shared<ListenerHelper>(name, callback, bAutoClose); +} + /* vim:set shiftwidth=4 softtabstop=4 expandtab cinoptions=b1,g0,N-s cinkeys+=0=break: */ |