summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--include/test/a11y/accessibletestbase.hxx99
-rw-r--r--test/CppunitTest_test_a11y.mk31
-rw-r--r--test/Module_test.mk1
-rw-r--r--test/qa/cppunit/dialog.cxx66
-rw-r--r--test/source/a11y/accessibletestbase.cxx156
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: */