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 <m.weghorn@posteo.de> Tested-by: Jenkins
This commit is contained in:
committed by
Michael Weghorn
parent
c15412eb96
commit
0ccea0dd6e
@@ -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;
|
||||
|
||||
@@ -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:
|
||||
@@ -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 \
|
||||
))
|
||||
|
||||
|
||||
@@ -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: */
|
||||
@@ -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: */
|
||||
|
||||
Reference in New Issue
Block a user