Version 7.6.3.2-android, tag libreoffice-7.6.3.2-android
[LibreOffice.git] / test / source / a11y / accessibletestbase.cxx
blob92a40e87f7794199261d41b71f7c5d01d030997b
1 /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4; fill-column: 100 -*- */
2 /*
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/.
8 */
10 #include <test/a11y/accessibletestbase.hxx>
12 #include <string>
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>
40 using namespace css;
42 void test::AccessibleTestBase::setUp()
44 test::BootstrapFixture::setUp();
46 mxDesktop = frame::Desktop::create(mxComponentContext);
49 void test::AccessibleTestBase::close()
51 if (mxDocument.is())
53 uno::Reference<util::XCloseable> xCloseable(mxDocument, uno::UNO_QUERY_THROW);
54 xCloseable->close(false);
55 mxDocument.clear();
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
64 close();
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();
117 if (relset.is())
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,
127 uno::UNO_QUERY);
128 if (targetAccessible.is())
129 return targetAccessible->getAccessibleContext();
135 return nullptr;
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());
151 return children;
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 */
164 if (xRelSet.is())
166 auto relCount = xRelSet->getRelationCount();
167 if (relCount)
169 std::cout << " rels=[";
170 for (sal_Int32 i = 0; i < relCount; ++i)
172 if (i > 0)
173 std::cout << ", ";
175 const auto& rel = xRelSet->getRelation(i);
176 std::cout << "(type=" << AccessibilityTools::getRelationTypeName(rel.RelationType)
177 << " (" << rel.RelationType << ")";
178 std::cout << " targets=[";
179 int j = 0;
180 for (auto& target : rel.TargetSet)
182 if (j++ > 0)
183 std::cout << ", ";
184 uno::Reference<accessibility::XAccessible> ta(target, uno::UNO_QUERY_THROW);
185 std::cout << AccessibilityTools::debugString(ta);
187 std::cout << "])";
189 std::cout << "]";
192 std::cout << std::endl;
194 sal_Int32 i = 0;
195 for (auto& child : getAllChildren(xContext))
197 for (int j = 0; j < depth; j++)
198 std::cout << " ";
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;
218 return item;
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();
237 return true;
239 return false;
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;
267 int nSteps = 0;
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)
273 << std::endl;
274 if (xFocus->getAccessibleRole() == role && AccessibilityTools::nameEquals(xFocus, name))
276 std::cout << " -> OK, focus matches" << std::endl;
277 return xFocus;
279 if (++nSteps > 100)
281 std::cerr << "Object not found after tabbing 100 times! bailing out" << std::endl;
282 break;
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);
291 if (!xFocus)
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(
300 xRoot,
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;
307 iFocusedCount++;
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."
315 << std::endl;
316 break;
320 std::cerr << "NOT FOUND" << std::endl;
321 return nullptr;
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)
340 return true;
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;
348 return false;
351 #if !defined(MACOSX)
352 /* Dialog handling
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);
366 if (mxAccessible)
367 setWindow(mxAccessible);
368 else
370 std::cerr << "WARNING: AccessibleTestBase::Dialog() constructed with awt::XDialog2 '"
371 << xDialog2->getTitle()
372 << "' not implementing accessibility::XAccessible. Event delivery will not work."
373 << std::endl;
377 test::AccessibleTestBase::Dialog::~Dialog()
379 if (mbAutoClose)
380 close();
383 void test::AccessibleTestBase::Dialog::close(sal_Int32 result)
385 if (mxDialog2)
387 mxDialog2->endDialog(result);
388 mxDialog2.clear();
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
402 * the caller. */
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;
411 bool mbAutoClose;
412 Timer maTimeoutTimer;
413 Idle maIdleHandler;
414 uno::Reference<awt::XTopWindowListener> mxTopWindowListener;
415 std::unique_ptr<Dialog> mxDialog;
417 public:
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,
427 bool bAutoClose)
428 : mbWaitingForDialog(true)
429 , msName(name)
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);
450 private:
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"
460 << std::endl;
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>
471 private:
472 ListenerHelper* mpHelper;
474 public:
475 MyTopWindowListener(ListenerHelper* pHelper)
476 : mpHelper(pHelper)
478 assert(mpHelper);
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);
492 if (!xEvent.Source)
493 return;
495 uno::Reference<awt::XDialog2> xDialog(xEvent.Source, uno::UNO_QUERY);
496 if (!xDialog)
497 return;
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();
507 // XEventListener
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 + "'"));
533 else
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);
544 catch (...)
546 mpException = std::current_exception();
550 mxDialog.reset();
553 public:
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);
563 aTimer.Start();
566 Application::Yield();
567 } while (mbWaitingForDialog && aTimer.IsActive());
570 if (mpException)
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: */