include/test/a11y/accessibletestbase.hxx |   99 +++++++++++++++++++
 test/CppunitTest_test_a11y.mk            |   31 ++++++
 test/Module_test.mk                      |    1 
 test/qa/cppunit/dialog.cxx               |   66 +++++++++++++
 test/source/a11y/accessibletestbase.cxx  |  156 +++++++++++++++++++++++++++++++
 5 files changed, 353 insertions(+)

New commits:
commit 0ccea0dd6e50199af4a7aae75d691b32c853b177
Author:     Colomban Wendling <[email protected]>
AuthorDate: Thu Oct 27 19:07:44 2022 +0200
Commit:     Michael Weghorn <[email protected]>
CommitDate: Fri Feb 24 15:13:39 2023 +0000

    test: Add accessibility test dialog infrastructure
    
    Interacting with dialogues in tests is non-trivial, so introduce
    helpers to make it simpler and less error-prone.
    
    Add tests for the infrastructure itself as well.
    
    Change-Id: I8ea6087a61380194eb2b5ec9f25091db00f5a550
    Reviewed-on: https://gerrit.libreoffice.org/c/core/+/142258
    Reviewed-by: Michael Weghorn <[email protected]>
    Tested-by: Jenkins

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: */

Reply via email to