summaryrefslogtreecommitdiffstats
path: root/test/source/a11y
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-15 05:54:39 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-15 05:54:39 +0000
commit267c6f2ac71f92999e969232431ba04678e7437e (patch)
tree358c9467650e1d0a1d7227a21dac2e3d08b622b2 /test/source/a11y
parentInitial commit. (diff)
downloadlibreoffice-267c6f2ac71f92999e969232431ba04678e7437e.tar.xz
libreoffice-267c6f2ac71f92999e969232431ba04678e7437e.zip
Adding upstream version 4:24.2.0.upstream/4%24.2.0
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'test/source/a11y')
-rw-r--r--test/source/a11y/AccessibilityTools.cxx753
-rw-r--r--test/source/a11y/accessibletestbase.cxx581
-rw-r--r--test/source/a11y/eventposter.cxx46
-rw-r--r--test/source/a11y/swaccessibletestbase.cxx135
4 files changed, 1515 insertions, 0 deletions
diff --git a/test/source/a11y/AccessibilityTools.cxx b/test/source/a11y/AccessibilityTools.cxx
new file mode 100644
index 0000000000..f0a28d4cda
--- /dev/null
+++ b/test/source/a11y/AccessibilityTools.cxx
@@ -0,0 +1,753 @@
+/* -*- 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/.
+ *
+ * This file incorporates work covered by the following license notice:
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed
+ * with this work for additional information regarding copyright
+ * ownership. The ASF licenses this file to you under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of
+ * the License at http://www.apache.org/licenses/LICENSE-2.0 .
+ */
+
+#include <test/a11y/AccessibilityTools.hxx>
+
+#include <com/sun/star/accessibility/AccessibleEventId.hpp>
+#include <com/sun/star/accessibility/AccessibleRelationType.hpp>
+#include <com/sun/star/accessibility/AccessibleRole.hpp>
+#include <com/sun/star/accessibility/AccessibleStateType.hpp>
+#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/KeyModifier.hpp>
+
+#include <rtl/ustrbuf.hxx>
+#include <sal/log.hxx>
+#include <toolkit/awt/vclxaccessiblecomponent.hxx>
+#include <vcl/scheduler.hxx>
+#include <vcl/timer.hxx>
+#include <vcl/window.hxx>
+#include <o3tl/string_view.hxx>
+
+using namespace css;
+
+uno::Reference<accessibility::XAccessibleContext>
+AccessibilityTools::getAccessibleObjectForPredicate(
+ const uno::Reference<accessibility::XAccessibleContext>& xCtx,
+ const std::function<bool(const uno::Reference<accessibility::XAccessibleContext>&)>& cPredicate)
+{
+ if (cPredicate(xCtx))
+ {
+ return xCtx;
+ }
+ else
+ {
+ sal_Int64 count = xCtx->getAccessibleChildCount();
+
+ for (sal_Int64 i = 0; i < count && i < AccessibilityTools::MAX_CHILDREN; i++)
+ {
+ uno::Reference<accessibility::XAccessibleContext> xCtx2
+ = getAccessibleObjectForPredicate(xCtx->getAccessibleChild(i), cPredicate);
+ if (xCtx2.is())
+ return xCtx2;
+ }
+ }
+ return nullptr;
+}
+
+uno::Reference<accessibility::XAccessibleContext>
+AccessibilityTools::getAccessibleObjectForPredicate(
+ const uno::Reference<accessibility::XAccessible>& xAcc,
+ const std::function<bool(const uno::Reference<accessibility::XAccessibleContext>&)>& cPredicate)
+{
+ return getAccessibleObjectForPredicate(xAcc->getAccessibleContext(), cPredicate);
+}
+
+uno::Reference<accessibility::XAccessibleContext> AccessibilityTools::getAccessibleObjectForRole(
+ const uno::Reference<accessibility::XAccessibleContext>& xCtx, sal_Int16 role)
+{
+ return getAccessibleObjectForPredicate(
+ xCtx, [&role](const uno::Reference<accessibility::XAccessibleContext>& xObjCtx) {
+ return (xObjCtx->getAccessibleRole() == role
+ && xObjCtx->getAccessibleStateSet()
+ & accessibility::AccessibleStateType::SHOWING);
+ });
+}
+
+css::uno::Reference<css::accessibility::XAccessibleContext>
+AccessibilityTools::getAccessibleObjectForRole(
+ const css::uno::Reference<css::accessibility::XAccessible>& xacc, sal_Int16 role)
+{
+ return getAccessibleObjectForRole(xacc->getAccessibleContext(), role);
+}
+
+/* this is basically the same as getAccessibleObjectForPredicate() but specialized for efficiency,
+ * and because the template version will not work with getAccessibleObjectForPredicate() anyway */
+css::uno::Reference<css::accessibility::XAccessibleContext>
+AccessibilityTools::getAccessibleObjectForName(
+ const css::uno::Reference<css::accessibility::XAccessibleContext>& xCtx, const sal_Int16 role,
+ std::u16string_view name)
+{
+ if (xCtx->getAccessibleRole() == role && nameEquals(xCtx, name))
+ return xCtx;
+
+ auto nChildren = xCtx->getAccessibleChildCount();
+ for (decltype(nChildren) i = 0; i < nChildren && i < AccessibilityTools::MAX_CHILDREN; i++)
+ {
+ if (auto xMatchChild = getAccessibleObjectForName(xCtx->getAccessibleChild(i), role, name))
+ return xMatchChild;
+ }
+
+ return nullptr;
+}
+
+bool AccessibilityTools::equals(const uno::Reference<accessibility::XAccessible>& xacc1,
+ const uno::Reference<accessibility::XAccessible>& xacc2)
+{
+ if (!xacc1.is() || !xacc2.is())
+ return xacc1.is() == xacc2.is();
+ return equals(xacc1->getAccessibleContext(), xacc2->getAccessibleContext());
+}
+
+bool AccessibilityTools::equals(const uno::Reference<accessibility::XAccessibleContext>& xctx1,
+ const uno::Reference<accessibility::XAccessibleContext>& xctx2)
+{
+ if (!xctx1.is() || !xctx2.is())
+ return xctx1.is() == xctx2.is();
+
+ if (xctx1->getAccessibleRole() != xctx2->getAccessibleRole())
+ return false;
+
+ if (xctx1->getAccessibleName() != xctx2->getAccessibleName())
+ return false;
+
+ if (xctx1->getAccessibleDescription() != xctx2->getAccessibleDescription())
+ return false;
+
+ if (xctx1->getAccessibleChildCount() != xctx2->getAccessibleChildCount())
+ return false;
+
+ /* this one was not in the Java version */
+ if (xctx1->getAccessibleIndexInParent() != xctx2->getAccessibleIndexInParent())
+ return false;
+
+ /* because in Writer at least some children only are referenced by their relations to others
+ * objects, we need to account for that as their index in parent is incorrect (so not
+ * necessarily unique) */
+ auto relset1 = xctx1->getAccessibleRelationSet();
+ auto relset2 = xctx2->getAccessibleRelationSet();
+ if (relset1.is() != relset2.is())
+ return false;
+ else if (relset1.is())
+ {
+ auto relCount1 = relset1->getRelationCount();
+ auto relCount2 = relset2->getRelationCount();
+ if (relCount1 != relCount2)
+ return false;
+
+ for (sal_Int32 i = 0; i < relCount1; ++i)
+ {
+ if (relset1->getRelation(i) != relset2->getRelation(i))
+ return false;
+ }
+ }
+
+ return equals(xctx1->getAccessibleParent(), xctx2->getAccessibleParent());
+}
+
+bool AccessibilityTools::nameEquals(const uno::Reference<accessibility::XAccessibleContext>& xCtx,
+ const std::u16string_view name)
+{
+ auto ctxName = xCtx->getAccessibleName();
+ std::u16string_view rest;
+
+ if (!o3tl::starts_with(ctxName, name, &rest))
+ return false;
+ if (rest == u"")
+ return true;
+
+#if defined(_WIN32)
+ // see OAccessibleMenuItemComponent::GetAccessibleName():
+ // on Win32, ignore a \tSHORTCUT suffix on a menu item
+ switch (xCtx->getAccessibleRole())
+ {
+ case accessibility::AccessibleRole::MENU_ITEM:
+ case accessibility::AccessibleRole::RADIO_MENU_ITEM:
+ case accessibility::AccessibleRole::CHECK_MENU_ITEM:
+ return rest[0] == '\t';
+
+ default:
+ break;
+ }
+#endif
+
+#if OSL_DEBUG_LEVEL > 0
+ // see VCLXAccessibleComponent::getAccessibleName()
+ static const char* pEnvAppendType = getenv("LIBO_APPEND_WINDOW_TYPE_TO_ACCESSIBLE_NAME");
+ if (pEnvAppendType && OUString::createFromAscii(pEnvAppendType) != u"0")
+ {
+ auto pVCLXAccessibleComponent = dynamic_cast<VCLXAccessibleComponent*>(xCtx.get());
+ if (pVCLXAccessibleComponent)
+ {
+ auto windowType = pVCLXAccessibleComponent->GetWindow()->GetType();
+ if (rest
+ == Concat2View(u" (Type = " + OUString::number(static_cast<sal_Int32>(windowType))
+ + ")"))
+ return true;
+ }
+ }
+#endif
+ return false;
+}
+
+static OUString unknownName(const sal_Int64 value)
+{
+ return "unknown (" + OUString::number(value) + ")";
+}
+
+OUString AccessibilityTools::getRoleName(const sal_Int16 role)
+{
+ switch (role)
+ {
+ case accessibility::AccessibleRole::UNKNOWN:
+ return "UNKNOWN";
+ case accessibility::AccessibleRole::ALERT:
+ return "ALERT";
+ case accessibility::AccessibleRole::BLOCK_QUOTE:
+ return "BLOCK_QUOTE";
+ case accessibility::AccessibleRole::BUTTON_DROPDOWN:
+ return "BUTTON_DROPDOWN";
+ case accessibility::AccessibleRole::BUTTON_MENU:
+ return "BUTTON_MENU";
+ case accessibility::AccessibleRole::CANVAS:
+ return "CANVAS";
+ case accessibility::AccessibleRole::CAPTION:
+ return "CAPTION";
+ case accessibility::AccessibleRole::CHART:
+ return "CHART";
+ case accessibility::AccessibleRole::CHECK_BOX:
+ return "CHECK_BOX";
+ case accessibility::AccessibleRole::CHECK_MENU_ITEM:
+ return "CHECK_MENU_ITEM";
+ case accessibility::AccessibleRole::COLOR_CHOOSER:
+ return "COLOR_CHOOSER";
+ case accessibility::AccessibleRole::COLUMN_HEADER:
+ return "COLUMN_HEADER";
+ case accessibility::AccessibleRole::COMBO_BOX:
+ return "COMBO_BOX";
+ case accessibility::AccessibleRole::COMMENT:
+ return "COMMENT";
+ case accessibility::AccessibleRole::COMMENT_END:
+ return "COMMENT_END";
+ case accessibility::AccessibleRole::DATE_EDITOR:
+ return "DATE_EDITOR";
+ case accessibility::AccessibleRole::DESKTOP_ICON:
+ return "DESKTOP_ICON";
+ case accessibility::AccessibleRole::DESKTOP_PANE:
+ return "DESKTOP_PANE";
+ case accessibility::AccessibleRole::DIALOG:
+ return "DIALOG";
+ case accessibility::AccessibleRole::DIRECTORY_PANE:
+ return "DIRECTORY_PANE";
+ case accessibility::AccessibleRole::DOCUMENT:
+ return "DOCUMENT";
+ case accessibility::AccessibleRole::DOCUMENT_PRESENTATION:
+ return "DOCUMENT_PRESENTATION";
+ case accessibility::AccessibleRole::DOCUMENT_SPREADSHEET:
+ return "DOCUMENT_SPREADSHEET";
+ case accessibility::AccessibleRole::DOCUMENT_TEXT:
+ return "DOCUMENT_TEXT";
+ case accessibility::AccessibleRole::EDIT_BAR:
+ return "EDIT_BAR";
+ case accessibility::AccessibleRole::EMBEDDED_OBJECT:
+ return "EMBEDDED_OBJECT";
+ case accessibility::AccessibleRole::END_NOTE:
+ return "END_NOTE";
+ case accessibility::AccessibleRole::FILE_CHOOSER:
+ return "FILE_CHOOSER";
+ case accessibility::AccessibleRole::FILLER:
+ return "FILLER";
+ case accessibility::AccessibleRole::FONT_CHOOSER:
+ return "FONT_CHOOSER";
+ case accessibility::AccessibleRole::FOOTER:
+ return "FOOTER";
+ case accessibility::AccessibleRole::FOOTNOTE:
+ return "FOOTNOTE";
+ case accessibility::AccessibleRole::FORM:
+ return "FORM";
+ case accessibility::AccessibleRole::FRAME:
+ return "FRAME";
+ case accessibility::AccessibleRole::GLASS_PANE:
+ return "GLASS_PANE";
+ case accessibility::AccessibleRole::GRAPHIC:
+ return "GRAPHIC";
+ case accessibility::AccessibleRole::GROUP_BOX:
+ return "GROUP_BOX";
+ case accessibility::AccessibleRole::HEADER:
+ return "HEADER";
+ case accessibility::AccessibleRole::HEADING:
+ return "HEADING";
+ case accessibility::AccessibleRole::HYPER_LINK:
+ return "HYPER_LINK";
+ case accessibility::AccessibleRole::ICON:
+ return "ICON";
+ case accessibility::AccessibleRole::IMAGE_MAP:
+ return "IMAGE_MAP";
+ case accessibility::AccessibleRole::INTERNAL_FRAME:
+ return "INTERNAL_FRAME";
+ case accessibility::AccessibleRole::LABEL:
+ return "LABEL";
+ case accessibility::AccessibleRole::LAYERED_PANE:
+ return "LAYERED_PANE";
+ case accessibility::AccessibleRole::LIST:
+ return "LIST";
+ case accessibility::AccessibleRole::LIST_ITEM:
+ return "LIST_ITEM";
+ case accessibility::AccessibleRole::MENU:
+ return "MENU";
+ case accessibility::AccessibleRole::MENU_BAR:
+ return "MENU_BAR";
+ case accessibility::AccessibleRole::MENU_ITEM:
+ return "MENU_ITEM";
+ case accessibility::AccessibleRole::NOTE:
+ return "NOTE";
+ case accessibility::AccessibleRole::OPTION_PANE:
+ return "OPTION_PANE";
+ case accessibility::AccessibleRole::PAGE:
+ return "PAGE";
+ case accessibility::AccessibleRole::PAGE_TAB:
+ return "PAGE_TAB";
+ case accessibility::AccessibleRole::PAGE_TAB_LIST:
+ return "PAGE_TAB_LIST";
+ case accessibility::AccessibleRole::PANEL:
+ return "PANEL";
+ case accessibility::AccessibleRole::PARAGRAPH:
+ return "PARAGRAPH";
+ case accessibility::AccessibleRole::PASSWORD_TEXT:
+ return "PASSWORD_TEXT";
+ case accessibility::AccessibleRole::POPUP_MENU:
+ return "POPUP_MENU";
+ case accessibility::AccessibleRole::PROGRESS_BAR:
+ return "PROGRESS_BAR";
+ case accessibility::AccessibleRole::PUSH_BUTTON:
+ return "PUSH_BUTTON";
+ case accessibility::AccessibleRole::RADIO_BUTTON:
+ return "RADIO_BUTTON";
+ case accessibility::AccessibleRole::RADIO_MENU_ITEM:
+ return "RADIO_MENU_ITEM";
+ case accessibility::AccessibleRole::ROOT_PANE:
+ return "ROOT_PANE";
+ case accessibility::AccessibleRole::ROW_HEADER:
+ return "ROW_HEADER";
+ case accessibility::AccessibleRole::RULER:
+ return "RULER";
+ case accessibility::AccessibleRole::SCROLL_BAR:
+ return "SCROLL_BAR";
+ case accessibility::AccessibleRole::SCROLL_PANE:
+ return "SCROLL_PANE";
+ case accessibility::AccessibleRole::SECTION:
+ return "SECTION";
+ case accessibility::AccessibleRole::SEPARATOR:
+ return "SEPARATOR";
+ case accessibility::AccessibleRole::SHAPE:
+ return "SHAPE";
+ case accessibility::AccessibleRole::SLIDER:
+ return "SLIDER";
+ case accessibility::AccessibleRole::SPIN_BOX:
+ return "SPIN_BOX";
+ case accessibility::AccessibleRole::SPLIT_PANE:
+ return "SPLIT_PANE";
+ case accessibility::AccessibleRole::STATIC:
+ return "STATIC";
+ case accessibility::AccessibleRole::STATUS_BAR:
+ return "STATUS_BAR";
+ case accessibility::AccessibleRole::TABLE:
+ return "TABLE";
+ case accessibility::AccessibleRole::TABLE_CELL:
+ return "TABLE_CELL";
+ case accessibility::AccessibleRole::TEXT:
+ return "TEXT";
+ case accessibility::AccessibleRole::TEXT_FRAME:
+ return "TEXT_FRAME";
+ case accessibility::AccessibleRole::TOGGLE_BUTTON:
+ return "TOGGLE_BUTTON";
+ case accessibility::AccessibleRole::TOOL_BAR:
+ return "TOOL_BAR";
+ case accessibility::AccessibleRole::TOOL_TIP:
+ return "TOOL_TIP";
+ case accessibility::AccessibleRole::TREE:
+ return "TREE";
+ case accessibility::AccessibleRole::TREE_ITEM:
+ return "TREE_ITEM";
+ case accessibility::AccessibleRole::TREE_TABLE:
+ return "TREE_TABLE";
+ case accessibility::AccessibleRole::VIEW_PORT:
+ return "VIEW_PORT";
+ case accessibility::AccessibleRole::WINDOW:
+ return "WINDOW";
+ };
+ return unknownName(role);
+}
+
+OUString AccessibilityTools::debugAccessibleStateSet(const sal_Int64 nCombinedState)
+{
+ OUString combinedName;
+
+ for (int i = 0; i < 63; i++)
+ {
+ sal_Int64 state = sal_Int64(1) << i;
+ if (!(state & nCombinedState))
+ continue;
+ OUString name;
+ switch (state)
+ {
+ case accessibility::AccessibleStateType::ACTIVE:
+ name = "ACTIVE";
+ break;
+ case accessibility::AccessibleStateType::ARMED:
+ name = "ARMED";
+ break;
+ case accessibility::AccessibleStateType::BUSY:
+ name = "BUSY";
+ break;
+ case accessibility::AccessibleStateType::CHECKABLE:
+ name = "CHECKABLE";
+ break;
+ case accessibility::AccessibleStateType::CHECKED:
+ name = "CHECKED";
+ break;
+ case accessibility::AccessibleStateType::COLLAPSE:
+ name = "COLLAPSE";
+ break;
+ case accessibility::AccessibleStateType::DEFAULT:
+ name = "DEFAULT";
+ break;
+ case accessibility::AccessibleStateType::DEFUNC:
+ name = "DEFUNC";
+ break;
+ case accessibility::AccessibleStateType::EDITABLE:
+ name = "EDITABLE";
+ break;
+ case accessibility::AccessibleStateType::ENABLED:
+ name = "ENABLED";
+ break;
+ case accessibility::AccessibleStateType::EXPANDABLE:
+ name = "EXPANDABLE";
+ break;
+ case accessibility::AccessibleStateType::EXPANDED:
+ name = "EXPANDED";
+ break;
+ case accessibility::AccessibleStateType::FOCUSABLE:
+ name = "FOCUSABLE";
+ break;
+ case accessibility::AccessibleStateType::FOCUSED:
+ name = "FOCUSED";
+ break;
+ case accessibility::AccessibleStateType::HORIZONTAL:
+ name = "HORIZONTAL";
+ break;
+ case accessibility::AccessibleStateType::ICONIFIED:
+ name = "ICONIFIED";
+ break;
+ case accessibility::AccessibleStateType::INDETERMINATE:
+ name = "INDETERMINATE";
+ break;
+ case accessibility::AccessibleStateType::INVALID:
+ name = "INVALID";
+ break;
+ case accessibility::AccessibleStateType::MANAGES_DESCENDANTS:
+ name = "MANAGES_DESCENDANTS";
+ break;
+ case accessibility::AccessibleStateType::MODAL:
+ name = "MODAL";
+ break;
+ case accessibility::AccessibleStateType::MOVEABLE:
+ name = "MOVEABLE";
+ break;
+ case accessibility::AccessibleStateType::MULTI_LINE:
+ name = "MULTI_LINE";
+ break;
+ case accessibility::AccessibleStateType::MULTI_SELECTABLE:
+ name = "MULTI_SELECTABLE";
+ break;
+ case accessibility::AccessibleStateType::OFFSCREEN:
+ name = "OFFSCREEN";
+ break;
+ case accessibility::AccessibleStateType::OPAQUE:
+ name = "OPAQUE";
+ break;
+ case accessibility::AccessibleStateType::PRESSED:
+ name = "PRESSED";
+ break;
+ case accessibility::AccessibleStateType::RESIZABLE:
+ name = "RESIZABLE";
+ break;
+ case accessibility::AccessibleStateType::SELECTABLE:
+ name = "SELECTABLE";
+ break;
+ case accessibility::AccessibleStateType::SELECTED:
+ name = "SELECTED";
+ break;
+ case accessibility::AccessibleStateType::SENSITIVE:
+ name = "SENSITIVE";
+ break;
+ case accessibility::AccessibleStateType::SHOWING:
+ name = "SHOWING";
+ break;
+ case accessibility::AccessibleStateType::SINGLE_LINE:
+ name = "SINGLE_LINE";
+ break;
+ case accessibility::AccessibleStateType::STALE:
+ name = "STALE";
+ break;
+ case accessibility::AccessibleStateType::TRANSIENT:
+ name = "TRANSIENT";
+ break;
+ case accessibility::AccessibleStateType::VERTICAL:
+ name = "VERTICAL";
+ break;
+ case accessibility::AccessibleStateType::VISIBLE:
+ name = "VISIBLE";
+ break;
+ default:
+ name = unknownName(state);
+ break;
+ }
+ if (combinedName.getLength())
+ combinedName += " | ";
+ combinedName += name;
+ }
+
+ if (combinedName.isEmpty())
+ return "unknown";
+ return combinedName;
+}
+
+OUString AccessibilityTools::getEventIdName(const sal_Int16 event_id)
+{
+ switch (event_id)
+ {
+ case accessibility::AccessibleEventId::ACTION_CHANGED:
+ return "ACTION_CHANGED";
+ case accessibility::AccessibleEventId::ACTIVE_DESCENDANT_CHANGED:
+ return "ACTIVE_DESCENDANT_CHANGED";
+ case accessibility::AccessibleEventId::ACTIVE_DESCENDANT_CHANGED_NOFOCUS:
+ return "ACTIVE_DESCENDANT_CHANGED_NOFOCUS";
+ case accessibility::AccessibleEventId::BOUNDRECT_CHANGED:
+ return "BOUNDRECT_CHANGED";
+ case accessibility::AccessibleEventId::CARET_CHANGED:
+ return "CARET_CHANGED";
+ case accessibility::AccessibleEventId::CHILD:
+ return "CHILD";
+ case accessibility::AccessibleEventId::COLUMN_CHANGED:
+ return "COLUMN_CHANGED";
+ case accessibility::AccessibleEventId::CONTENT_FLOWS_FROM_RELATION_CHANGED:
+ return "CONTENT_FLOWS_FROM_RELATION_CHANGED";
+ case accessibility::AccessibleEventId::CONTENT_FLOWS_TO_RELATION_CHANGED:
+ return "CONTENT_FLOWS_TO_RELATION_CHANGED";
+ case accessibility::AccessibleEventId::CONTROLLED_BY_RELATION_CHANGED:
+ return "CONTROLLED_BY_RELATION_CHANGED";
+ case accessibility::AccessibleEventId::CONTROLLER_FOR_RELATION_CHANGED:
+ return "CONTROLLER_FOR_RELATION_CHANGED";
+ case accessibility::AccessibleEventId::DESCRIPTION_CHANGED:
+ return "DESCRIPTION_CHANGED";
+ case accessibility::AccessibleEventId::HYPERTEXT_CHANGED:
+ return "HYPERTEXT_CHANGED";
+ case accessibility::AccessibleEventId::INVALIDATE_ALL_CHILDREN:
+ return "INVALIDATE_ALL_CHILDREN";
+ case accessibility::AccessibleEventId::LABELED_BY_RELATION_CHANGED:
+ return "LABELED_BY_RELATION_CHANGED";
+ case accessibility::AccessibleEventId::LABEL_FOR_RELATION_CHANGED:
+ return "LABEL_FOR_RELATION_CHANGED";
+ case accessibility::AccessibleEventId::LISTBOX_ENTRY_COLLAPSED:
+ return "LISTBOX_ENTRY_COLLAPSED";
+ case accessibility::AccessibleEventId::LISTBOX_ENTRY_EXPANDED:
+ return "LISTBOX_ENTRY_EXPANDED";
+ case accessibility::AccessibleEventId::MEMBER_OF_RELATION_CHANGED:
+ return "MEMBER_OF_RELATION_CHANGED";
+ case accessibility::AccessibleEventId::NAME_CHANGED:
+ return "NAME_CHANGED";
+ case accessibility::AccessibleEventId::PAGE_CHANGED:
+ return "PAGE_CHANGED";
+ case accessibility::AccessibleEventId::ROLE_CHANGED:
+ return "ROLE_CHANGED";
+ case accessibility::AccessibleEventId::SECTION_CHANGED:
+ return "SECTION_CHANGED";
+ case accessibility::AccessibleEventId::SELECTION_CHANGED:
+ return "SELECTION_CHANGED";
+ case accessibility::AccessibleEventId::SELECTION_CHANGED_ADD:
+ return "SELECTION_CHANGED_ADD";
+ case accessibility::AccessibleEventId::SELECTION_CHANGED_REMOVE:
+ return "SELECTION_CHANGED_REMOVE";
+ case accessibility::AccessibleEventId::SELECTION_CHANGED_WITHIN:
+ return "SELECTION_CHANGED_WITHIN";
+ case accessibility::AccessibleEventId::STATE_CHANGED:
+ return "STATE_CHANGED";
+ case accessibility::AccessibleEventId::SUB_WINDOW_OF_RELATION_CHANGED:
+ return "SUB_WINDOW_OF_RELATION_CHANGED";
+ case accessibility::AccessibleEventId::TABLE_CAPTION_CHANGED:
+ return "TABLE_CAPTION_CHANGED";
+ case accessibility::AccessibleEventId::TABLE_COLUMN_DESCRIPTION_CHANGED:
+ return "TABLE_COLUMN_DESCRIPTION_CHANGED";
+ case accessibility::AccessibleEventId::TABLE_COLUMN_HEADER_CHANGED:
+ return "TABLE_COLUMN_HEADER_CHANGED";
+ case accessibility::AccessibleEventId::TABLE_MODEL_CHANGED:
+ return "TABLE_MODEL_CHANGED";
+ case accessibility::AccessibleEventId::TABLE_ROW_DESCRIPTION_CHANGED:
+ return "TABLE_ROW_DESCRIPTION_CHANGED";
+ case accessibility::AccessibleEventId::TABLE_ROW_HEADER_CHANGED:
+ return "TABLE_ROW_HEADER_CHANGED";
+ case accessibility::AccessibleEventId::TABLE_SUMMARY_CHANGED:
+ return "TABLE_SUMMARY_CHANGED";
+ case accessibility::AccessibleEventId::TEXT_ATTRIBUTE_CHANGED:
+ return "TEXT_ATTRIBUTE_CHANGED";
+ case accessibility::AccessibleEventId::TEXT_CHANGED:
+ return "TEXT_CHANGED";
+ case accessibility::AccessibleEventId::TEXT_SELECTION_CHANGED:
+ return "TEXT_SELECTION_CHANGED";
+ case accessibility::AccessibleEventId::VALUE_CHANGED:
+ return "VALUE_CHANGED";
+ case accessibility::AccessibleEventId::VISIBLE_DATA_CHANGED:
+ return "VISIBLE_DATA_CHANGED";
+ }
+ return unknownName(event_id);
+}
+
+OUString AccessibilityTools::getRelationTypeName(const sal_Int16 rel_type)
+{
+ switch (rel_type)
+ {
+ case accessibility::AccessibleRelationType::CONTENT_FLOWS_FROM:
+ return "CONTENT_FLOWS_FROM";
+ case accessibility::AccessibleRelationType::CONTENT_FLOWS_TO:
+ return "CONTENT_FLOWS_TO";
+ case accessibility::AccessibleRelationType::CONTROLLED_BY:
+ return "CONTROLLED_BY";
+ case accessibility::AccessibleRelationType::CONTROLLER_FOR:
+ return "CONTROLLER_FOR";
+ case accessibility::AccessibleRelationType::DESCRIBED_BY:
+ return "DESCRIBED_BY";
+ case accessibility::AccessibleRelationType::INVALID:
+ return "INVALID";
+ case accessibility::AccessibleRelationType::LABELED_BY:
+ return "LABELED_BY";
+ case accessibility::AccessibleRelationType::LABEL_FOR:
+ return "LABEL_FOR";
+ case accessibility::AccessibleRelationType::MEMBER_OF:
+ return "MEMBER_OF";
+ case accessibility::AccessibleRelationType::NODE_CHILD_OF:
+ return "NODE_CHILD_OF";
+ case accessibility::AccessibleRelationType::SUB_WINDOW_OF:
+ return "SUB_WINDOW_OF";
+ }
+ return unknownName(rel_type);
+}
+
+OUString AccessibilityTools::debugName(accessibility::XAccessibleContext* ctx)
+{
+ return "role=" + AccessibilityTools::getRoleName(ctx->getAccessibleRole()) + " name=\""
+ + ctx->getAccessibleName() + "\" description=\"" + ctx->getAccessibleDescription()
+ + "\"";
+}
+
+OUString AccessibilityTools::debugName(accessibility::XAccessible* acc)
+{
+ return debugName(acc->getAccessibleContext().get());
+}
+
+OUString AccessibilityTools::debugName(accessibility::XAccessibleAction* xAct)
+{
+ OUStringBuffer r = "actions=[";
+
+ const sal_Int32 nActions = xAct->getAccessibleActionCount();
+ for (sal_Int32 i = 0; i < nActions; i++)
+ {
+ if (i > 0)
+ r.append(", ");
+
+ r.append("description=\"" + xAct->getAccessibleActionDescription(i) + "\"");
+
+ const auto& xKeyBinding = xAct->getAccessibleActionKeyBinding(i);
+ if (xKeyBinding)
+ {
+ r.append(" keybindings=[");
+ const sal_Int32 nKeyBindings = xKeyBinding->getAccessibleKeyBindingCount();
+ for (sal_Int32 j = 0; j < nKeyBindings; j++)
+ {
+ if (j > 0)
+ r.append(", ");
+
+ int k = 0;
+ for (const auto& keyStroke : xKeyBinding->getAccessibleKeyBinding(j))
+ {
+ if (k++ > 0)
+ r.append(", ");
+
+ r.append('"');
+ if (keyStroke.Modifiers & awt::KeyModifier::MOD1)
+ r.append("<Mod1>");
+ if (keyStroke.Modifiers & awt::KeyModifier::MOD2)
+ r.append("<Mod2>");
+ if (keyStroke.Modifiers & awt::KeyModifier::MOD3)
+ r.append("<Mod3>");
+ if (keyStroke.Modifiers & awt::KeyModifier::SHIFT)
+ r.append("<Shift>");
+ r.append(OUStringChar(keyStroke.KeyChar) + "\"");
+ }
+ }
+ r.append("]");
+ }
+ }
+ r.append("]");
+ return r.makeStringAndClear();
+}
+
+OUString AccessibilityTools::debugName(accessibility::XAccessibleText* xTxt)
+{
+ uno::Reference<accessibility::XAccessibleContext> xCtx(xTxt, uno::UNO_QUERY);
+ return debugName(xCtx.get());
+}
+
+OUString AccessibilityTools::debugName(const accessibility::AccessibleEventObject* evobj)
+{
+ return "(AccessibleEventObject) { id=" + getEventIdName(evobj->EventId)
+ + " old=" + evobj->OldValue.getValueTypeName()
+ + " new=" + evobj->NewValue.getValueTypeName() + " }";
+}
+
+bool AccessibilityTools::Await(const std::function<bool()>& cUntilCallback, sal_uInt64 nTimeoutMs)
+{
+ bool success = false;
+ Timer aTimer("wait for event");
+ aTimer.SetTimeout(nTimeoutMs);
+ aTimer.Start();
+ do
+ {
+ Scheduler::ProcessEventsToIdle();
+ success = cUntilCallback();
+ } while (!success && aTimer.IsActive());
+ SAL_WARN_IF(!success, "test", "timeout reached");
+ return success;
+}
+
+void AccessibilityTools::Wait(sal_uInt64 nTimeoutMs)
+{
+ Timer aTimer("wait for event");
+ aTimer.SetTimeout(nTimeoutMs);
+ aTimer.Start();
+ std::cout << "waiting for " << nTimeoutMs << "ms... ";
+ do
+ {
+ Scheduler::ProcessEventsToIdle();
+ } while (aTimer.IsActive());
+ std::cout << "ok." << std::endl;
+}
+
+/* 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
new file mode 100644
index 0000000000..92a40e87f7
--- /dev/null
+++ b/test/source/a11y/accessibletestbase.cxx
@@ -0,0 +1,581 @@
+/* -*- 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>
+
+#include <string>
+
+#include <com/sun/star/accessibility/AccessibleRole.hpp>
+#include <com/sun/star/accessibility/AccessibleStateType.hpp>
+#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/XExtendedToolkit.hpp>
+#include <com/sun/star/awt/XTopWindow.hpp>
+#include <com/sun/star/awt/XTopWindowListener.hpp>
+#include <com/sun/star/frame/Desktop.hpp>
+#include <com/sun/star/frame/FrameSearchFlag.hpp>
+#include <com/sun/star/frame/XFrame.hpp>
+#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/idle.hxx>
+#include <vcl/scheduler.hxx>
+#include <vcl/svapp.hxx>
+
+#include <cppuhelper/implbase.hxx>
+
+#include <test/a11y/AccessibilityTools.hxx>
+
+using namespace css;
+
+void test::AccessibleTestBase::setUp()
+{
+ test::BootstrapFixture::setUp();
+
+ mxDesktop = frame::Desktop::create(mxComponentContext);
+}
+
+void test::AccessibleTestBase::close()
+{
+ if (mxDocument.is())
+ {
+ uno::Reference<util::XCloseable> xCloseable(mxDocument, uno::UNO_QUERY_THROW);
+ xCloseable->close(false);
+ mxDocument.clear();
+ }
+}
+
+void test::AccessibleTestBase::tearDown() { close(); }
+
+void test::AccessibleTestBase::load(const rtl::OUString& sURL)
+{
+ // make sure there is no open document in case it is called more than once
+ close();
+ mxDocument = mxDesktop->loadComponentFromURL(sURL, "_blank", frame::FrameSearchFlag::AUTO, {});
+
+ uno::Reference<frame::XModel> xModel(mxDocument, uno::UNO_QUERY_THROW);
+ mxWindow.set(xModel->getCurrentController()->getFrame()->getContainerWindow());
+
+ // bring window to front
+ uno::Reference<awt::XTopWindow> xTopWindow(mxWindow, uno::UNO_QUERY_THROW);
+ xTopWindow->toFront();
+}
+
+void test::AccessibleTestBase::loadFromSrc(const rtl::OUString& sSrcPath)
+{
+ load(m_directories.getURLFromSrc(sSrcPath));
+}
+
+uno::Reference<accessibility::XAccessibleContext>
+test::AccessibleTestBase::getWindowAccessibleContext()
+{
+ uno::Reference<accessibility::XAccessible> xAccessible(mxWindow, uno::UNO_QUERY_THROW);
+
+ return xAccessible->getAccessibleContext();
+}
+
+bool test::AccessibleTestBase::isDocumentRole(const sal_Int16 role)
+{
+ return (role == accessibility::AccessibleRole::DOCUMENT
+ || role == accessibility::AccessibleRole::DOCUMENT_PRESENTATION
+ || role == accessibility::AccessibleRole::DOCUMENT_SPREADSHEET
+ || role == accessibility::AccessibleRole::DOCUMENT_TEXT);
+}
+
+uno::Reference<accessibility::XAccessibleContext>
+test::AccessibleTestBase::getDocumentAccessibleContext()
+{
+ uno::Reference<frame::XModel> xModel(mxDocument, uno::UNO_QUERY_THROW);
+ uno::Reference<accessibility::XAccessible> xAccessible(
+ xModel->getCurrentController()->getFrame()->getComponentWindow(), uno::UNO_QUERY_THROW);
+
+ return AccessibilityTools::getAccessibleObjectForPredicate(
+ xAccessible->getAccessibleContext(),
+ [](const uno::Reference<accessibility::XAccessibleContext>& xCtx) {
+ return (isDocumentRole(xCtx->getAccessibleRole())
+ && xCtx->getAccessibleStateSet() & accessibility::AccessibleStateType::SHOWING);
+ });
+}
+
+uno::Reference<accessibility::XAccessibleContext>
+test::AccessibleTestBase::getFirstRelationTargetOfType(
+ const uno::Reference<accessibility::XAccessibleContext>& xContext, sal_Int16 relationType)
+{
+ auto relset = xContext->getAccessibleRelationSet();
+
+ if (relset.is())
+ {
+ for (sal_Int32 i = 0; i < relset->getRelationCount(); ++i)
+ {
+ const auto& rel = relset->getRelation(i);
+ if (rel.RelationType == relationType)
+ {
+ for (auto& target : rel.TargetSet)
+ {
+ uno::Reference<accessibility::XAccessible> targetAccessible(target,
+ uno::UNO_QUERY);
+ if (targetAccessible.is())
+ return targetAccessible->getAccessibleContext();
+ }
+ }
+ }
+ }
+
+ return nullptr;
+}
+
+std::deque<uno::Reference<accessibility::XAccessibleContext>>
+test::AccessibleTestBase::getAllChildren(
+ const uno::Reference<accessibility::XAccessibleContext>& xContext)
+{
+ std::deque<uno::Reference<accessibility::XAccessibleContext>> children;
+ auto childCount = xContext->getAccessibleChildCount();
+
+ for (sal_Int64 i = 0; i < childCount && i < AccessibilityTools::MAX_CHILDREN; i++)
+ {
+ auto child = xContext->getAccessibleChild(i);
+ children.push_back(child->getAccessibleContext());
+ }
+
+ return children;
+}
+
+/** Prints the tree of accessible objects starting at @p xContext to stdout */
+void test::AccessibleTestBase::dumpA11YTree(
+ const uno::Reference<accessibility::XAccessibleContext>& xContext, const int depth)
+{
+ Scheduler::ProcessEventsToIdle();
+ auto xRelSet = xContext->getAccessibleRelationSet();
+
+ std::cout << AccessibilityTools::debugString(xContext);
+ /* relation set is not included in AccessibilityTools::debugString(), but might be useful in
+ * this context, so we compute it here */
+ if (xRelSet.is())
+ {
+ auto relCount = xRelSet->getRelationCount();
+ if (relCount)
+ {
+ std::cout << " rels=[";
+ for (sal_Int32 i = 0; i < relCount; ++i)
+ {
+ if (i > 0)
+ std::cout << ", ";
+
+ const auto& rel = xRelSet->getRelation(i);
+ std::cout << "(type=" << AccessibilityTools::getRelationTypeName(rel.RelationType)
+ << " (" << rel.RelationType << ")";
+ std::cout << " targets=[";
+ int j = 0;
+ for (auto& target : rel.TargetSet)
+ {
+ if (j++ > 0)
+ std::cout << ", ";
+ uno::Reference<accessibility::XAccessible> ta(target, uno::UNO_QUERY_THROW);
+ std::cout << AccessibilityTools::debugString(ta);
+ }
+ std::cout << "])";
+ }
+ std::cout << "]";
+ }
+ }
+ std::cout << std::endl;
+
+ sal_Int32 i = 0;
+ for (auto& child : getAllChildren(xContext))
+ {
+ for (int j = 0; j < depth; j++)
+ std::cout << " ";
+ std::cout << " * child " << i++ << ": ";
+ dumpA11YTree(child, depth + 1);
+ }
+}
+
+/** Gets a child by name (usually in a menu) */
+uno::Reference<accessibility::XAccessibleContext> test::AccessibleTestBase::getItemFromName(
+ const uno::Reference<accessibility::XAccessibleContext>& xMenuCtx, std::u16string_view name)
+{
+ auto childCount = xMenuCtx->getAccessibleChildCount();
+
+ std::cout << "looking up item " << OUString(name) << " in "
+ << AccessibilityTools::debugString(xMenuCtx) << std::endl;
+ for (sal_Int64 i = 0; i < childCount && i < AccessibilityTools::MAX_CHILDREN; i++)
+ {
+ auto item = xMenuCtx->getAccessibleChild(i)->getAccessibleContext();
+ if (AccessibilityTools::nameEquals(item, name))
+ {
+ std::cout << "-> found " << AccessibilityTools::debugString(item) << std::endl;
+ return item;
+ }
+ }
+
+ std::cout << "-> NOT FOUND!" << std::endl;
+ std::cout << " Contents was: ";
+ dumpA11YTree(xMenuCtx, 1);
+
+ return uno::Reference<accessibility::XAccessibleContext>();
+}
+
+bool test::AccessibleTestBase::activateMenuItem(
+ const uno::Reference<accessibility::XAccessibleAction>& xAction)
+{
+ // assume first action is the right one, there's not description anyway
+ CPPUNIT_ASSERT_EQUAL(sal_Int32(1), xAction->getAccessibleActionCount());
+ if (xAction->doAccessibleAction(0))
+ {
+ Scheduler::ProcessEventsToIdle();
+ return true;
+ }
+ return false;
+}
+
+uno::Reference<accessibility::XAccessibleContext> test::AccessibleTestBase::getFocusedObject(
+ const uno::Reference<accessibility::XAccessibleContext>& xCtx)
+{
+ return AccessibilityTools::getAccessibleObjectForPredicate(
+ xCtx, [](const uno::Reference<accessibility::XAccessibleContext>& xCandidateCtx) {
+ const auto states = (accessibility::AccessibleStateType::FOCUSED
+ | accessibility::AccessibleStateType::SHOWING);
+ return (xCandidateCtx->getAccessibleStateSet() & states) == states;
+ });
+}
+
+uno::Reference<accessibility::XAccessibleContext>
+test::AccessibleTestBase::tabTo(const uno::Reference<accessibility::XAccessible>& xRoot,
+ const sal_Int16 role, const std::u16string_view name,
+ const EventPosterHelperBase* pEventPosterHelper)
+{
+ AccessibleEventPosterHelper eventHelper;
+ if (!pEventPosterHelper)
+ {
+ eventHelper.setWindow(xRoot);
+ pEventPosterHelper = &eventHelper;
+ }
+
+ auto xOriginalFocus = getFocusedObject(xRoot);
+ auto xFocus = xOriginalFocus;
+ int nSteps = 0;
+
+ std::cout << "Tabbing to '" << OUString(name) << "'..." << std::endl;
+ while (xFocus && (nSteps == 0 || xFocus != xOriginalFocus))
+ {
+ std::cout << " focused object is: " << AccessibilityTools::debugString(xFocus)
+ << std::endl;
+ if (xFocus->getAccessibleRole() == role && AccessibilityTools::nameEquals(xFocus, name))
+ {
+ std::cout << " -> OK, focus matches" << std::endl;
+ return xFocus;
+ }
+ if (++nSteps > 100)
+ {
+ std::cerr << "Object not found after tabbing 100 times! bailing out" << std::endl;
+ break;
+ }
+
+ std::cout << " -> no match, sending <TAB>" << std::endl;
+ pEventPosterHelper->postKeyEventAsync(0, awt::Key::TAB);
+ Scheduler::ProcessEventsToIdle();
+
+ const auto xPrevFocus = xFocus;
+ xFocus = getFocusedObject(xRoot);
+ if (!xFocus)
+ std::cerr << "Focus lost after sending <TAB>!" << std::endl;
+ else if (xPrevFocus == xFocus)
+ {
+ std::cerr << "Focus didn't move after sending <TAB>! bailing out" << std::endl;
+ std::cerr << "Focused object(s):" << std::endl;
+ int iFocusedCount = 0;
+ // count and print out objects with focused state
+ AccessibilityTools::getAccessibleObjectForPredicate(
+ xRoot,
+ [&iFocusedCount](const uno::Reference<accessibility::XAccessibleContext>& xCtx) {
+ const auto states = (accessibility::AccessibleStateType::FOCUSED
+ | accessibility::AccessibleStateType::SHOWING);
+ if ((xCtx->getAccessibleStateSet() & states) == states)
+ {
+ std::cerr << " * " << AccessibilityTools::debugString(xCtx) << std::endl;
+ iFocusedCount++;
+ }
+ return false; // keep going
+ });
+ std::cerr << "Total focused element(s): " << iFocusedCount << std::endl;
+ if (iFocusedCount > 1)
+ std::cerr << "WARNING: there are more than one focused object! This usually means "
+ "there is a BUG in the focus handling of that accessibility tree."
+ << std::endl;
+ break;
+ }
+ }
+
+ std::cerr << "NOT FOUND" << std::endl;
+ return nullptr;
+}
+
+bool test::AccessibleTestBase::tabTo(
+ const uno::Reference<accessibility::XAccessible>& xRoot,
+ const uno::Reference<accessibility::XAccessibleContext>& xChild,
+ const EventPosterHelperBase* pEventPosterHelper)
+{
+ AccessibleEventPosterHelper eventHelper;
+ if (!pEventPosterHelper)
+ {
+ eventHelper.setWindow(xRoot);
+ pEventPosterHelper = &eventHelper;
+ }
+
+ std::cout << "Tabbing to " << AccessibilityTools::debugString(xChild) << "..." << std::endl;
+ for (int i = 0; i < 100; i++)
+ {
+ if (xChild->getAccessibleStateSet() & accessibility::AccessibleStateType::FOCUSED)
+ return true;
+
+ std::cout << " no match, sending <TAB>" << std::endl;
+ pEventPosterHelper->postKeyEventAsync(0, awt::Key::TAB);
+ Scheduler::ProcessEventsToIdle();
+ }
+
+ std::cerr << "NOT FOUND" << std::endl;
+ return false;
+}
+
+#if !defined(MACOSX)
+/* Dialog handling
+ *
+ * For now this doesn't actually work under macos, so the API is not available there not to create
+ * confusion. The problem there is we don't get notified of new dialogs, so we can't manage them
+ * or interact with them.
+ */
+
+test::AccessibleTestBase::Dialog::Dialog(uno::Reference<awt::XDialog2>& xDialog2, bool bAutoClose)
+ : mbAutoClose(bAutoClose)
+ , mxDialog2(xDialog2)
+{
+ CPPUNIT_ASSERT(xDialog2.is());
+
+ mxAccessible.set(xDialog2, uno::UNO_QUERY);
+ if (mxAccessible)
+ setWindow(mxAccessible);
+ else
+ {
+ std::cerr << "WARNING: AccessibleTestBase::Dialog() constructed with awt::XDialog2 '"
+ << xDialog2->getTitle()
+ << "' not implementing accessibility::XAccessible. Event delivery will not work."
+ << std::endl;
+ }
+}
+
+test::AccessibleTestBase::Dialog::~Dialog()
+{
+ if (mbAutoClose)
+ close();
+}
+
+void test::AccessibleTestBase::Dialog::close(sal_Int32 result)
+{
+ if (mxDialog2)
+ {
+ mxDialog2->endDialog(result);
+ mxDialog2.clear();
+ }
+}
+
+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;
+ uno::Reference<awt::XExtendedToolkit> mxToolkit;
+ bool mbWaitingForDialog;
+ std::exception_ptr mpException;
+ std::u16string_view msName;
+ std::function<void(Dialog&)> mCallback;
+ bool mbAutoClose;
+ Timer maTimeoutTimer;
+ Idle maIdleHandler;
+ uno::Reference<awt::XTopWindowListener> mxTopWindowListener;
+ std::unique_ptr<Dialog> mxDialog;
+
+ public:
+ virtual ~ListenerHelper()
+ {
+ Application::SetDialogCancelMode(miPreviousDialogCancelMode);
+ mxToolkit->removeTopWindowListener(mxTopWindowListener);
+ maTimeoutTimer.Stop();
+ maIdleHandler.Stop();
+ }
+
+ ListenerHelper(const std::u16string_view& name, std::function<void(Dialog&)> callback,
+ bool bAutoClose)
+ : mbWaitingForDialog(true)
+ , msName(name)
+ , mCallback(callback)
+ , mbAutoClose(bAutoClose)
+ , maTimeoutTimer("workaround timer if we don't catch WindowActivate")
+ , maIdleHandler("runs user callback in idle time")
+ {
+ mxTopWindowListener.set(new MyTopWindowListener(this));
+ mxToolkit.set(Application::GetVCLToolkit(), uno::UNO_QUERY_THROW);
+ mxToolkit->addTopWindowListener(mxTopWindowListener);
+
+ maTimeoutTimer.SetInvokeHandler(LINK(this, ListenerHelper, timeoutTimerHandler));
+ maTimeoutTimer.SetTimeout(60000);
+ maTimeoutTimer.Start();
+
+ maIdleHandler.SetInvokeHandler(LINK(this, ListenerHelper, idleHandler));
+ maIdleHandler.SetPriority(TaskPriority::DEFAULT_IDLE);
+
+ miPreviousDialogCancelMode = Application::GetDialogCancelMode();
+ Application::SetDialogCancelMode(DialogCancelMode::Off);
+ }
+
+ private:
+ // mimic IMPL_LINK inline
+ static void LinkStubtimeoutTimerHandler(void* instance, Timer* timer)
+ {
+ static_cast<ListenerHelper*>(instance)->timeoutTimerHandler(timer);
+ }
+
+ void timeoutTimerHandler(Timer*)
+ {
+ std::cerr << "timeout waiting for dialog '" << OUString(msName) << "' to show up"
+ << std::endl;
+
+ assert(mbWaitingForDialog);
+
+ // This is not very nice, but it should help fail earlier if we never catch the dialog
+ // yet we're in a sub-loop and waitEndDialog() didn't have a chance to run yet.
+ throw new css::uno::RuntimeException("Timeout waiting for dialog");
+ }
+
+ class MyTopWindowListener : public ::cppu::WeakImplHelper<awt::XTopWindowListener>
+ {
+ private:
+ ListenerHelper* mpHelper;
+
+ public:
+ MyTopWindowListener(ListenerHelper* pHelper)
+ : mpHelper(pHelper)
+ {
+ assert(mpHelper);
+ }
+
+ // XTopWindowListener
+ virtual void SAL_CALL windowOpened(const lang::EventObject&) override {}
+ virtual void SAL_CALL windowClosing(const lang::EventObject&) override {}
+ virtual void SAL_CALL windowClosed(const lang::EventObject&) override {}
+ virtual void SAL_CALL windowMinimized(const lang::EventObject&) override {}
+ virtual void SAL_CALL windowNormalized(const lang::EventObject&) override {}
+ virtual void SAL_CALL windowDeactivated(const lang::EventObject&) override {}
+ virtual void SAL_CALL windowActivated(const lang::EventObject& xEvent) override
+ {
+ assert(mpHelper->mbWaitingForDialog);
+
+ if (!xEvent.Source)
+ return;
+
+ uno::Reference<awt::XDialog2> xDialog(xEvent.Source, uno::UNO_QUERY);
+ if (!xDialog)
+ return;
+
+ // remove ourselves, we don't want to run again
+ mpHelper->mxToolkit->removeTopWindowListener(this);
+
+ mpHelper->mxDialog = std::make_unique<Dialog>(xDialog, true);
+
+ mpHelper->maIdleHandler.Start();
+ }
+
+ // XEventListener
+ virtual void SAL_CALL disposing(const lang::EventObject&) override {}
+ };
+
+ // mimic IMPL_LINK inline
+ static void LinkStubidleHandler(void* instance, Timer* idle)
+ {
+ static_cast<ListenerHelper*>(instance)->idleHandler(idle);
+ }
+
+ void idleHandler(Timer*)
+ {
+ mbWaitingForDialog = false;
+
+ maTimeoutTimer.ClearInvokeHandler();
+ maTimeoutTimer.Stop();
+
+ /* The popping 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 != mxDialog->getWindow()->GetText())
+ {
+ mpException = std::make_exception_ptr(css::uno::RuntimeException(
+ "Unexpected dialog '" + mxDialog->getWindow()->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
+ mxDialog->setAutoClose(mbAutoClose);
+
+ try
+ {
+ mCallback(*mxDialog);
+ }
+ catch (...)
+ {
+ mpException = std::current_exception();
+ }
+ }
+
+ mxDialog.reset();
+ }
+
+ 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);
+}
+#endif //defined(MACOSX)
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab cinoptions=b1,g0,N-s cinkeys+=0=break: */
diff --git a/test/source/a11y/eventposter.cxx b/test/source/a11y/eventposter.cxx
new file mode 100644
index 0000000000..39e178e227
--- /dev/null
+++ b/test/source/a11y/eventposter.cxx
@@ -0,0 +1,46 @@
+/* -*- 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/eventposter.hxx>
+
+#include <com/sun/star/accessibility/XAccessible.hpp>
+#include <com/sun/star/accessibility/XAccessibleContext.hpp>
+#include <com/sun/star/uno/Reference.hxx>
+
+#include <sfx2/lokhelper.hxx>
+#include <test/a11y/AccessibilityTools.hxx>
+#include <toolkit/awt/vclxwindow.hxx>
+
+void test::EventPosterHelper::postKeyEventAsync(int nType, int nCharCode, int nKeyCode) const
+{
+ SfxLokHelper::postKeyEventAsync(mxWindow, nType, nCharCode, nKeyCode);
+}
+
+void test::EventPosterHelper::postExtTextEventAsync(int nType, const OUString& rText) const
+{
+ SfxLokHelper::postExtTextEventAsync(mxWindow, nType, rText);
+}
+
+void test::AccessibleEventPosterHelper::setWindow(
+ css::uno::Reference<css::accessibility::XAccessible> xAcc)
+{
+ while (auto xParent = xAcc->getAccessibleContext()->getAccessibleParent())
+ xAcc = xParent;
+ auto vclXWindow = dynamic_cast<VCLXWindow*>(xAcc.get());
+ if (!vclXWindow)
+ {
+ std::cerr << "WARNING: AccessibleEventPosterHelper::setWindow() called on "
+ "unsupported object "
+ << AccessibilityTools::debugString(xAcc) << ". Event delivery will not work."
+ << std::endl;
+ }
+ mxWindow = vclXWindow ? vclXWindow->GetWindow() : nullptr;
+}
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab cinoptions=b1,g0,N-s cinkeys+=0=break: */
diff --git a/test/source/a11y/swaccessibletestbase.cxx b/test/source/a11y/swaccessibletestbase.cxx
new file mode 100644
index 0000000000..b43d65c0cf
--- /dev/null
+++ b/test/source/a11y/swaccessibletestbase.cxx
@@ -0,0 +1,135 @@
+/* -*- 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/swaccessibletestbase.hxx>
+
+#include <com/sun/star/accessibility/AccessibleRelationType.hpp>
+#include <com/sun/star/accessibility/XAccessibleContext.hpp>
+#include <com/sun/star/accessibility/XAccessibleText.hpp>
+#include <com/sun/star/uno/Reference.hxx>
+
+#include <rtl/ustrbuf.hxx>
+
+#include <test/a11y/AccessibilityTools.hxx>
+
+using namespace css;
+
+uno::Reference<accessibility::XAccessibleContext>
+test::SwAccessibleTestBase::getPreviousFlowingSibling(
+ const uno::Reference<accessibility::XAccessibleContext>& xContext)
+{
+ return getFirstRelationTargetOfType(xContext,
+ accessibility::AccessibleRelationType::CONTENT_FLOWS_FROM);
+}
+
+uno::Reference<accessibility::XAccessibleContext> test::SwAccessibleTestBase::getNextFlowingSibling(
+ const uno::Reference<accessibility::XAccessibleContext>& xContext)
+{
+ return getFirstRelationTargetOfType(xContext,
+ accessibility::AccessibleRelationType::CONTENT_FLOWS_TO);
+}
+
+/* Care has to be taken not to walk sideways as the relation is also used
+ * with children of nested containers (possibly as the "natural"/"perceived" flow?). */
+std::deque<uno::Reference<accessibility::XAccessibleContext>>
+test::SwAccessibleTestBase::getAllChildren(
+ const uno::Reference<accessibility::XAccessibleContext>& xContext)
+{
+ /* first, get all "natural" children */
+ auto children = AccessibleTestBase::getAllChildren(xContext);
+ if (!children.size())
+ return children;
+
+ /* then, try and find flowing siblings at the same levels that are not included in the list */
+ /* first, backwards: */
+ auto child = getPreviousFlowingSibling(children.front());
+ while (child.is() && children.size() < AccessibilityTools::MAX_CHILDREN)
+ {
+ auto childParent = child->getAccessibleParent();
+ if (childParent.is()
+ && AccessibilityTools::equals(xContext, childParent->getAccessibleContext()))
+ children.push_front(child);
+ child = getPreviousFlowingSibling(child);
+ }
+ /* then forward */
+ child = getNextFlowingSibling(children.back());
+ while (child.is() && children.size() < AccessibilityTools::MAX_CHILDREN)
+ {
+ auto childParent = child->getAccessibleParent();
+ if (childParent.is()
+ && AccessibilityTools::equals(xContext, childParent->getAccessibleContext()))
+ children.push_back(child);
+ child = getNextFlowingSibling(child);
+ }
+
+ return children;
+}
+
+void test::SwAccessibleTestBase::collectText(
+ const uno::Reference<accessibility::XAccessibleContext>& xContext, rtl::OUStringBuffer& buffer,
+ bool onlyChildren)
+{
+ const auto& roleName = AccessibilityTools::getRoleName(xContext->getAccessibleRole());
+
+ std::cout << "collecting text for child of role " << roleName << "..." << std::endl;
+
+ if (!onlyChildren)
+ {
+ const struct
+ {
+ std::u16string_view name;
+ rtl::OUString value;
+ } attrs[] = {
+ { u"name", xContext->getAccessibleName() },
+ { u"description", xContext->getAccessibleDescription() },
+ };
+
+ buffer.append('<');
+ buffer.append(roleName);
+ for (auto& attr : attrs)
+ {
+ if (attr.value.getLength() == 0)
+ continue;
+ buffer.append(' ');
+ buffer.append(attr.name);
+ buffer.append(u"=\"" + attr.value.replaceAll(u"\"", u"&quot;") + "\"");
+ }
+ buffer.append('>');
+ }
+ auto openTagLength = buffer.getLength();
+
+ uno::Reference<accessibility::XAccessibleText> xText(xContext, uno::UNO_QUERY);
+ if (xText.is())
+ buffer.append(xText->getText());
+
+ for (auto& childContext : getAllChildren(xContext))
+ collectText(childContext, buffer);
+
+ if (!onlyChildren)
+ {
+ if (buffer.getLength() != openTagLength)
+ buffer.append("</" + roleName + ">");
+ else
+ {
+ /* there was no content, so make is a short tag for more concise output */
+ buffer[openTagLength - 1] = '/';
+ buffer.append('>');
+ }
+ }
+}
+
+OUString test::SwAccessibleTestBase::collectText(
+ const uno::Reference<accessibility::XAccessibleContext>& xContext)
+{
+ rtl::OUStringBuffer buf;
+ collectText(xContext, buf, isDocumentRole(xContext->getAccessibleRole()));
+ return buf.makeStringAndClear();
+}
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab cinoptions=b1,g0,N-s cinkeys+=0=break: */