1 /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4; fill-column: 100 -*- */
3 * This file is part of the LibreOffice project.
5 * This Source Code Form is subject to the terms of the Mozilla Public
6 * License, v. 2.0. If a copy of the MPL was not distributed with this
7 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
10 #include <test/a11y/accessibletestbase.hxx>
14 #include <com/sun/star/accessibility/AccessibleRole.hpp>
15 #include <com/sun/star/accessibility/AccessibleStateType.hpp>
16 #include <com/sun/star/accessibility/XAccessible.hpp>
17 #include <com/sun/star/accessibility/XAccessibleAction.hpp>
18 #include <com/sun/star/accessibility/XAccessibleContext.hpp>
19 #include <com/sun/star/awt/XDialog2.hpp>
20 #include <com/sun/star/awt/XExtendedToolkit.hpp>
21 #include <com/sun/star/awt/XTopWindow.hpp>
22 #include <com/sun/star/awt/XTopWindowListener.hpp>
23 #include <com/sun/star/frame/Desktop.hpp>
24 #include <com/sun/star/frame/FrameSearchFlag.hpp>
25 #include <com/sun/star/frame/XFrame.hpp>
26 #include <com/sun/star/frame/XFrame2.hpp>
27 #include <com/sun/star/frame/XModel.hpp>
28 #include <com/sun/star/uno/Reference.hxx>
29 #include <com/sun/star/uno/RuntimeException.hpp>
30 #include <com/sun/star/util/XCloseable.hpp>
32 #include <vcl/idle.hxx>
33 #include <vcl/scheduler.hxx>
34 #include <vcl/svapp.hxx>
36 #include <cppuhelper/implbase.hxx>
38 #include <test/a11y/AccessibilityTools.hxx>
42 void test::AccessibleTestBase::setUp()
44 test::BootstrapFixture::setUp();
46 mxDesktop
= frame::Desktop::create(mxComponentContext
);
49 void test::AccessibleTestBase::close()
53 uno::Reference
<util::XCloseable
> xCloseable(mxDocument
, uno::UNO_QUERY_THROW
);
54 xCloseable
->close(false);
59 void test::AccessibleTestBase::tearDown() { close(); }
61 void test::AccessibleTestBase::load(const rtl::OUString
& sURL
)
63 // make sure there is no open document in case it is called more than once
65 mxDocument
= mxDesktop
->loadComponentFromURL(sURL
, "_blank", frame::FrameSearchFlag::AUTO
, {});
67 uno::Reference
<frame::XModel
> xModel(mxDocument
, uno::UNO_QUERY_THROW
);
68 mxWindow
.set(xModel
->getCurrentController()->getFrame()->getContainerWindow());
70 // bring window to front
71 uno::Reference
<awt::XTopWindow
> xTopWindow(mxWindow
, uno::UNO_QUERY_THROW
);
72 xTopWindow
->toFront();
75 void test::AccessibleTestBase::loadFromSrc(const rtl::OUString
& sSrcPath
)
77 load(m_directories
.getURLFromSrc(sSrcPath
));
80 uno::Reference
<accessibility::XAccessibleContext
>
81 test::AccessibleTestBase::getWindowAccessibleContext()
83 uno::Reference
<accessibility::XAccessible
> xAccessible(mxWindow
, uno::UNO_QUERY_THROW
);
85 return xAccessible
->getAccessibleContext();
88 bool test::AccessibleTestBase::isDocumentRole(const sal_Int16 role
)
90 return (role
== accessibility::AccessibleRole::DOCUMENT
91 || role
== accessibility::AccessibleRole::DOCUMENT_PRESENTATION
92 || role
== accessibility::AccessibleRole::DOCUMENT_SPREADSHEET
93 || role
== accessibility::AccessibleRole::DOCUMENT_TEXT
);
96 uno::Reference
<accessibility::XAccessibleContext
>
97 test::AccessibleTestBase::getDocumentAccessibleContext()
99 uno::Reference
<frame::XModel
> xModel(mxDocument
, uno::UNO_QUERY_THROW
);
100 uno::Reference
<accessibility::XAccessible
> xAccessible(
101 xModel
->getCurrentController()->getFrame()->getComponentWindow(), uno::UNO_QUERY_THROW
);
103 return AccessibilityTools::getAccessibleObjectForPredicate(
104 xAccessible
->getAccessibleContext(),
105 [](const uno::Reference
<accessibility::XAccessibleContext
>& xCtx
) {
106 return (isDocumentRole(xCtx
->getAccessibleRole())
107 && xCtx
->getAccessibleStateSet() & accessibility::AccessibleStateType::SHOWING
);
111 uno::Reference
<accessibility::XAccessibleContext
>
112 test::AccessibleTestBase::getFirstRelationTargetOfType(
113 const uno::Reference
<accessibility::XAccessibleContext
>& xContext
, sal_Int16 relationType
)
115 auto relset
= xContext
->getAccessibleRelationSet();
119 for (sal_Int32 i
= 0; i
< relset
->getRelationCount(); ++i
)
121 const auto& rel
= relset
->getRelation(i
);
122 if (rel
.RelationType
== relationType
)
124 for (auto& target
: rel
.TargetSet
)
126 uno::Reference
<accessibility::XAccessible
> targetAccessible(target
,
128 if (targetAccessible
.is())
129 return targetAccessible
->getAccessibleContext();
138 std::deque
<uno::Reference
<accessibility::XAccessibleContext
>>
139 test::AccessibleTestBase::getAllChildren(
140 const uno::Reference
<accessibility::XAccessibleContext
>& xContext
)
142 std::deque
<uno::Reference
<accessibility::XAccessibleContext
>> children
;
143 auto childCount
= xContext
->getAccessibleChildCount();
145 for (sal_Int64 i
= 0; i
< childCount
&& i
< AccessibilityTools::MAX_CHILDREN
; i
++)
147 auto child
= xContext
->getAccessibleChild(i
);
148 children
.push_back(child
->getAccessibleContext());
154 /** Prints the tree of accessible objects starting at @p xContext to stdout */
155 void test::AccessibleTestBase::dumpA11YTree(
156 const uno::Reference
<accessibility::XAccessibleContext
>& xContext
, const int depth
)
158 Scheduler::ProcessEventsToIdle();
159 auto xRelSet
= xContext
->getAccessibleRelationSet();
161 std::cout
<< AccessibilityTools::debugString(xContext
);
162 /* relation set is not included in AccessibilityTools::debugString(), but might be useful in
163 * this context, so we compute it here */
166 auto relCount
= xRelSet
->getRelationCount();
169 std::cout
<< " rels=[";
170 for (sal_Int32 i
= 0; i
< relCount
; ++i
)
175 const auto& rel
= xRelSet
->getRelation(i
);
176 std::cout
<< "(type=" << AccessibilityTools::getRelationTypeName(rel
.RelationType
)
177 << " (" << rel
.RelationType
<< ")";
178 std::cout
<< " targets=[";
180 for (auto& target
: rel
.TargetSet
)
184 uno::Reference
<accessibility::XAccessible
> ta(target
, uno::UNO_QUERY_THROW
);
185 std::cout
<< AccessibilityTools::debugString(ta
);
192 std::cout
<< std::endl
;
195 for (auto& child
: getAllChildren(xContext
))
197 for (int j
= 0; j
< depth
; j
++)
199 std::cout
<< " * child " << i
++ << ": ";
200 dumpA11YTree(child
, depth
+ 1);
204 /** Gets a child by name (usually in a menu) */
205 uno::Reference
<accessibility::XAccessibleContext
> test::AccessibleTestBase::getItemFromName(
206 const uno::Reference
<accessibility::XAccessibleContext
>& xMenuCtx
, std::u16string_view name
)
208 auto childCount
= xMenuCtx
->getAccessibleChildCount();
210 std::cout
<< "looking up item " << OUString(name
) << " in "
211 << AccessibilityTools::debugString(xMenuCtx
) << std::endl
;
212 for (sal_Int64 i
= 0; i
< childCount
&& i
< AccessibilityTools::MAX_CHILDREN
; i
++)
214 auto item
= xMenuCtx
->getAccessibleChild(i
)->getAccessibleContext();
215 if (AccessibilityTools::nameEquals(item
, name
))
217 std::cout
<< "-> found " << AccessibilityTools::debugString(item
) << std::endl
;
222 std::cout
<< "-> NOT FOUND!" << std::endl
;
223 std::cout
<< " Contents was: ";
224 dumpA11YTree(xMenuCtx
, 1);
226 return uno::Reference
<accessibility::XAccessibleContext
>();
229 bool test::AccessibleTestBase::activateMenuItem(
230 const uno::Reference
<accessibility::XAccessibleAction
>& xAction
)
232 // assume first action is the right one, there's not description anyway
233 CPPUNIT_ASSERT_EQUAL(sal_Int32(1), xAction
->getAccessibleActionCount());
234 if (xAction
->doAccessibleAction(0))
236 Scheduler::ProcessEventsToIdle();
242 uno::Reference
<accessibility::XAccessibleContext
> test::AccessibleTestBase::getFocusedObject(
243 const uno::Reference
<accessibility::XAccessibleContext
>& xCtx
)
245 return AccessibilityTools::getAccessibleObjectForPredicate(
246 xCtx
, [](const uno::Reference
<accessibility::XAccessibleContext
>& xCandidateCtx
) {
247 const auto states
= (accessibility::AccessibleStateType::FOCUSED
248 | accessibility::AccessibleStateType::SHOWING
);
249 return (xCandidateCtx
->getAccessibleStateSet() & states
) == states
;
253 uno::Reference
<accessibility::XAccessibleContext
>
254 test::AccessibleTestBase::tabTo(const uno::Reference
<accessibility::XAccessible
>& xRoot
,
255 const sal_Int16 role
, const std::u16string_view name
,
256 const EventPosterHelperBase
* pEventPosterHelper
)
258 AccessibleEventPosterHelper eventHelper
;
259 if (!pEventPosterHelper
)
261 eventHelper
.setWindow(xRoot
);
262 pEventPosterHelper
= &eventHelper
;
265 auto xOriginalFocus
= getFocusedObject(xRoot
);
266 auto xFocus
= xOriginalFocus
;
269 std::cout
<< "Tabbing to '" << OUString(name
) << "'..." << std::endl
;
270 while (xFocus
&& (nSteps
== 0 || xFocus
!= xOriginalFocus
))
272 std::cout
<< " focused object is: " << AccessibilityTools::debugString(xFocus
)
274 if (xFocus
->getAccessibleRole() == role
&& AccessibilityTools::nameEquals(xFocus
, name
))
276 std::cout
<< " -> OK, focus matches" << std::endl
;
281 std::cerr
<< "Object not found after tabbing 100 times! bailing out" << std::endl
;
285 std::cout
<< " -> no match, sending <TAB>" << std::endl
;
286 pEventPosterHelper
->postKeyEventAsync(0, awt::Key::TAB
);
287 Scheduler::ProcessEventsToIdle();
289 const auto xPrevFocus
= xFocus
;
290 xFocus
= getFocusedObject(xRoot
);
292 std::cerr
<< "Focus lost after sending <TAB>!" << std::endl
;
293 else if (xPrevFocus
== xFocus
)
295 std::cerr
<< "Focus didn't move after sending <TAB>! bailing out" << std::endl
;
296 std::cerr
<< "Focused object(s):" << std::endl
;
297 int iFocusedCount
= 0;
298 // count and print out objects with focused state
299 AccessibilityTools::getAccessibleObjectForPredicate(
301 [&iFocusedCount
](const uno::Reference
<accessibility::XAccessibleContext
>& xCtx
) {
302 const auto states
= (accessibility::AccessibleStateType::FOCUSED
303 | accessibility::AccessibleStateType::SHOWING
);
304 if ((xCtx
->getAccessibleStateSet() & states
) == states
)
306 std::cerr
<< " * " << AccessibilityTools::debugString(xCtx
) << std::endl
;
309 return false; // keep going
311 std::cerr
<< "Total focused element(s): " << iFocusedCount
<< std::endl
;
312 if (iFocusedCount
> 1)
313 std::cerr
<< "WARNING: there are more than one focused object! This usually means "
314 "there is a BUG in the focus handling of that accessibility tree."
320 std::cerr
<< "NOT FOUND" << std::endl
;
324 bool test::AccessibleTestBase::tabTo(
325 const uno::Reference
<accessibility::XAccessible
>& xRoot
,
326 const uno::Reference
<accessibility::XAccessibleContext
>& xChild
,
327 const EventPosterHelperBase
* pEventPosterHelper
)
329 AccessibleEventPosterHelper eventHelper
;
330 if (!pEventPosterHelper
)
332 eventHelper
.setWindow(xRoot
);
333 pEventPosterHelper
= &eventHelper
;
336 std::cout
<< "Tabbing to " << AccessibilityTools::debugString(xChild
) << "..." << std::endl
;
337 for (int i
= 0; i
< 100; i
++)
339 if (xChild
->getAccessibleStateSet() & accessibility::AccessibleStateType::FOCUSED
)
342 std::cout
<< " no match, sending <TAB>" << std::endl
;
343 pEventPosterHelper
->postKeyEventAsync(0, awt::Key::TAB
);
344 Scheduler::ProcessEventsToIdle();
347 std::cerr
<< "NOT FOUND" << std::endl
;
354 * For now this doesn't actually work under macos, so the API is not available there not to create
355 * confusion. The problem there is we don't get notified of new dialogs, so we can't manage them
356 * or interact with them.
359 test::AccessibleTestBase::Dialog::Dialog(uno::Reference
<awt::XDialog2
>& xDialog2
, bool bAutoClose
)
360 : mbAutoClose(bAutoClose
)
361 , mxDialog2(xDialog2
)
363 CPPUNIT_ASSERT(xDialog2
.is());
365 mxAccessible
.set(xDialog2
, uno::UNO_QUERY
);
367 setWindow(mxAccessible
);
370 std::cerr
<< "WARNING: AccessibleTestBase::Dialog() constructed with awt::XDialog2 '"
371 << xDialog2
->getTitle()
372 << "' not implementing accessibility::XAccessible. Event delivery will not work."
377 test::AccessibleTestBase::Dialog::~Dialog()
383 void test::AccessibleTestBase::Dialog::close(sal_Int32 result
)
387 mxDialog2
->endDialog(result
);
392 std::shared_ptr
<test::AccessibleTestBase::DialogWaiter
>
393 test::AccessibleTestBase::awaitDialog(const std::u16string_view name
,
394 std::function
<void(Dialog
&)> callback
, bool bAutoClose
)
396 /* Helper class to wait on a dialog to pop up and to close, running user code between the
397 * two. This has to work both for "other window"-style dialogues (non-modal), as well as
398 * for modal dialogues using Dialog::Execute() (which runs a nested main loop, hence
399 * blocking our test flow execution.
400 * The approach here is to wait on the WindowActivate event for the dialog, and run the
401 * test code in there. Then, close the dialog if not already done, resuming normal flow to
403 class ListenerHelper
: public DialogWaiter
405 DialogCancelMode miPreviousDialogCancelMode
;
406 uno::Reference
<awt::XExtendedToolkit
> mxToolkit
;
407 bool mbWaitingForDialog
;
408 std::exception_ptr mpException
;
409 std::u16string_view msName
;
410 std::function
<void(Dialog
&)> mCallback
;
412 Timer maTimeoutTimer
;
414 uno::Reference
<awt::XTopWindowListener
> mxTopWindowListener
;
415 std::unique_ptr
<Dialog
> mxDialog
;
418 virtual ~ListenerHelper()
420 Application::SetDialogCancelMode(miPreviousDialogCancelMode
);
421 mxToolkit
->removeTopWindowListener(mxTopWindowListener
);
422 maTimeoutTimer
.Stop();
423 maIdleHandler
.Stop();
426 ListenerHelper(const std::u16string_view
& name
, std::function
<void(Dialog
&)> callback
,
428 : mbWaitingForDialog(true)
430 , mCallback(callback
)
431 , mbAutoClose(bAutoClose
)
432 , maTimeoutTimer("workaround timer if we don't catch WindowActivate")
433 , maIdleHandler("runs user callback in idle time")
435 mxTopWindowListener
.set(new MyTopWindowListener(this));
436 mxToolkit
.set(Application::GetVCLToolkit(), uno::UNO_QUERY_THROW
);
437 mxToolkit
->addTopWindowListener(mxTopWindowListener
);
439 maTimeoutTimer
.SetInvokeHandler(LINK(this, ListenerHelper
, timeoutTimerHandler
));
440 maTimeoutTimer
.SetTimeout(60000);
441 maTimeoutTimer
.Start();
443 maIdleHandler
.SetInvokeHandler(LINK(this, ListenerHelper
, idleHandler
));
444 maIdleHandler
.SetPriority(TaskPriority::DEFAULT_IDLE
);
446 miPreviousDialogCancelMode
= Application::GetDialogCancelMode();
447 Application::SetDialogCancelMode(DialogCancelMode::Off
);
451 // mimic IMPL_LINK inline
452 static void LinkStubtimeoutTimerHandler(void* instance
, Timer
* timer
)
454 static_cast<ListenerHelper
*>(instance
)->timeoutTimerHandler(timer
);
457 void timeoutTimerHandler(Timer
*)
459 std::cerr
<< "timeout waiting for dialog '" << OUString(msName
) << "' to show up"
462 assert(mbWaitingForDialog
);
464 // This is not very nice, but it should help fail earlier if we never catch the dialog
465 // yet we're in a sub-loop and waitEndDialog() didn't have a chance to run yet.
466 throw new css::uno::RuntimeException("Timeout waiting for dialog");
469 class MyTopWindowListener
: public ::cppu::WeakImplHelper
<awt::XTopWindowListener
>
472 ListenerHelper
* mpHelper
;
475 MyTopWindowListener(ListenerHelper
* pHelper
)
481 // XTopWindowListener
482 virtual void SAL_CALL
windowOpened(const lang::EventObject
&) override
{}
483 virtual void SAL_CALL
windowClosing(const lang::EventObject
&) override
{}
484 virtual void SAL_CALL
windowClosed(const lang::EventObject
&) override
{}
485 virtual void SAL_CALL
windowMinimized(const lang::EventObject
&) override
{}
486 virtual void SAL_CALL
windowNormalized(const lang::EventObject
&) override
{}
487 virtual void SAL_CALL
windowDeactivated(const lang::EventObject
&) override
{}
488 virtual void SAL_CALL
windowActivated(const lang::EventObject
& xEvent
) override
490 assert(mpHelper
->mbWaitingForDialog
);
495 uno::Reference
<awt::XDialog2
> xDialog(xEvent
.Source
, uno::UNO_QUERY
);
499 // remove ourselves, we don't want to run again
500 mpHelper
->mxToolkit
->removeTopWindowListener(this);
502 mpHelper
->mxDialog
= std::make_unique
<Dialog
>(xDialog
, true);
504 mpHelper
->maIdleHandler
.Start();
508 virtual void SAL_CALL
disposing(const lang::EventObject
&) override
{}
511 // mimic IMPL_LINK inline
512 static void LinkStubidleHandler(void* instance
, Timer
* idle
)
514 static_cast<ListenerHelper
*>(instance
)->idleHandler(idle
);
517 void idleHandler(Timer
*)
519 mbWaitingForDialog
= false;
521 maTimeoutTimer
.ClearInvokeHandler();
522 maTimeoutTimer
.Stop();
524 /* The popping up dialog ought to be the right one, or something's fishy and
525 * we're bound to failure (e.g. waiting on a dialog that either will never come, or
526 * that will not run after the current one -- deadlock style) */
527 if (msName
!= mxDialog
->getWindow()->GetText())
529 mpException
= std::make_exception_ptr(css::uno::RuntimeException(
530 "Unexpected dialog '" + mxDialog
->getWindow()->GetText()
531 + "' opened instead of the expected '" + msName
+ "'"));
535 std::cout
<< "found dialog, calling user callback" << std::endl
;
537 // set the real requested auto close now we're just calling the user callback
538 mxDialog
->setAutoClose(mbAutoClose
);
542 mCallback(*mxDialog
);
546 mpException
= std::current_exception();
554 virtual bool waitEndDialog(sal_uInt64 nTimeoutMs
) override
556 /* Usually this loop will actually never run at all because a previous
557 * Scheduler::ProcessEventsToIdle() would have triggered the dialog already, but we
558 * can't be sure of that or of delays, so be safe and wait with a timeout. */
559 if (mbWaitingForDialog
)
561 Timer
aTimer("wait for dialog");
562 aTimer
.SetTimeout(nTimeoutMs
);
566 Application::Yield();
567 } while (mbWaitingForDialog
&& aTimer
.IsActive());
571 std::rethrow_exception(mpException
);
573 return !mbWaitingForDialog
;
577 return std::make_shared
<ListenerHelper
>(name
, callback
, bAutoClose
);
579 #endif //defined(MACOSX)
581 /* vim:set shiftwidth=4 softtabstop=4 expandtab cinoptions=b1,g0,N-s cinkeys+=0=break: */