Accessibility Unit Testing with CPPUnit
TDF LibreOffice Document Liberation Project Community Blogs Weblate Nextcloud Redmine Ask LibreOffice Donate
This page is a work in progress documenting writing and porting tests for the UI accessibility layer in LibreOffice.
Writing a new test
You can look at e.g. Writer or Calc basic accessibility tests for inspiration.
Writing a new test requires basic knowledge of how to create a CppUnit test. The Cpp Unit Tests page gives some useful insight on the matter.
We will use the concise CPPUNIT_TEST_FIXTURE()
approach, but you can just as well create your class manually and use CPPUNIT_TEST_SUITE_REGISTRATION()
if it better suits your needs.
Most accessible tests will inherit from test::AccessibleTestBase
, or a subclass thereof (e.g. test::SwAccessibleTestBase
which has additional helpers specifics to Writer). This base class provides most of the boilerplate and helpers useful to accessibility tests. In addition to those, the helpers from test::AccessibilityTools
will also be useful.
For a Writer test, you will then usually inherit from test::SwAccessibleTestBase
. To declare a test doing so, use CPPUNIT_TEST_FIXTURE(test::SwAccessibleTestBase, MyTestCase) { body here }
.
Here is a simple example, extracted from sw/qa/extras/accessibility/basics.cxx:
// for contants declarations used as arguments to documentPostKeyEvent()
#include <com/sun/star/awt/Key.hpp>
#include <LibreOfficeKit/LibreOfficeKitEnums.h>
// if using Scheduler::ProcessEventsToIdle()
#include <vcl/scheduler.hxx>
#include <test/a11y/swaccessibletestbase.hxx>
// helpers, including better handling of accessible objects in CPPUNIT_ASSERT()s
#include <test/a11y/AccessibilityTools.hxx>
using namespace css;
// declare the test using CPPUNIT_TEST_FIXTURE()
CPPUNIT_TEST_FIXTURE(test::SwAccessibleTestBase, TestMenuInsertPageBreak)
{
// The actual test body. Here it insert page numbers and verifies they lead to an expected representation
load(u"private:factory/swriter");
CPPUNIT_ASSERT(activateMenuItem(u"Insert", u"Field", u"Page Number"));
CPPUNIT_ASSERT(activateMenuItem(u"Insert", u"Page Break"));
// we need to move focus to the paragraph after the page break to insert the page number there
documentPostKeyEvent(LOK_KEYEVENT_KEYINPUT, 0, awt::Key::DOWN);
CPPUNIT_ASSERT(activateMenuItem(u"Insert", u"Field", u"Page Number"));
CPPUNIT_ASSERT_EQUAL(rtl::OUString("<PARAGRAPH>1</PARAGRAPH><PARAGRAPH>2</PARAGRAPH>"),
collectText());
}
// generate boilerplate main() for the tests.
// This must only be done once in a linked unit, so whether or not you need it depends
// if you're adding your test to an existing link unit or not.
CPPUNIT_PLUGIN_IMPLEMENT();
What to test for
This highly depends on the goal, but there are basically two kinds of tests:
- Tests for a specific bug. This will usually try to exercise a very specific code path to validate a fix and prevent future breakage. They might mix any number of LO APIs, not limited to the accessibility ones. Example of such tests include TestUnicodeSP or tdf150064.
- Tests for user interface accessibility. This will rather only focus on how things are presented and how they can be interacted with. Such tests might not enforce specific objects so much, and rather make sure a broader pattern is followed. They will minimize usage of internal APIs to stay close to what a user of keyboard input and assistive technologies would have available. One example is Writer's BasicTestSpecialCharactersDialog accessibility test.
There are of course many variants of this, and many tests will have a little bit of both. In general however, a test for a specific bug will only be introduced (and it should be introduced) when such a bug gets found and fixed. A more general usage test is always useful, and can help avoiding inadvertent degradation of the accessibility experience. Such tests can be added at any time to augment the coverage, regardless of a specific bug being found or not. Of course, such tests should ideally only rely on what's actually useful and avoid depending on irrelevant implementations details, not to put an unnecessary burden on future changes in the area.
API
The test::AccessibleTestBase
class
The test::AccessibleTestBase
class, or one of its subclasses, is the most convenient basis for any accessibility test case.
In addition to setting up the environment for the test (although most, yet not all, of it comes from test::BootstrapFixture
), it provides quite a few useful helpers.
The XDesktop2
used to load components is available as the mxDesktop
member.
Document management
Most tests will need a document open for them to work. The simplest solution is to use on of the load()
methods.
load()
: Loads a document given its URL. A URL can point to a document, or more usually is a special URL likeprivate:factory/swriter
to create a new document. See alsoXComponentLoader::loadComponentFromURL()
.loadFromSrc()
: Similar toload()
, but receives a path relative to the source directory, which is more useful for opening a test file. See alsotest::Directories::getURLFromSrc()
.close()
: Closes the current document, if any. This is not usually required, and documents will be closed automatically as needed.
Once a document is loaded, it is available as the mxDocument
member, and its container window is available as the mxWindow
member.
Accessible context retrieval
Accessible objects and contexts are the main objects accessibility tests will interact with. They form a tree of parents and children, which represents the user interface to accessibility tools (ATs), such as screen readers.
There are a few methods helping retrieving some of the most commonly useful accessible objects easily. The main ones are getWindowAccessibleContext()
and getDocumentAccessibleContext()
that will allow to use the accessible APIs to further work with the interface or the document. There are others that aim at simplifying common tasks.
getWindowAccessibleContext()
: Gets acss::uno::Reference
to theXAccessibleContext
for the main window.getDocumentAccessibleContext()
: Gets a reference to theXAccessibleContext
for the active document.getFocusedObject()
: Gets the object with the focused state. There should usually only be one, but if there are more (which would usually denote a bug), the first found is returned.getItemFromName()
: Helpers to get a menu item from its name, or from a sequence of menu and item names leading to that item.getAllChildren()
: Helper to get all children in a given context. See its documentation for details.getFirstRelationTargetOfType()
: A fairly specialized helper, to get the first target for a particular relation type. It can come in handy in some situations, but some others will require properly handling all targets, if there are more than one. See alsoXAccessibleContext::getAccessibleRelationSet()
andXAccessibleRelationSet
.
Interface interaction
Some tests will only check the accessible representation of the interface or the document, but others will need to interact with the user interface. A few methods help with some common interactions, in addition to directly using the accessibility API:
documentPostKeyEvent()
: Sends a key event to the document. See alsolok::Document::postKeyEvent()
.activateMenuItem()
: Helpers to activate a menu item. There are various overloads, the most commonly useful just receiving menus and item names. See alsogetItemFromName()
.tabTo()
: Helper to navigate through focusable components using the keyboard's Tab key. This is often very useful to reach a particular component, or check it is properly reachable using the keyboard. There are two variants of this: one looking for the focus going to a component matching a role and a name, and one looking for a specified component gaining focus.awaitDialog()
: Helper to interact with dialogs, see dealing with dialogs below.
Miscellaneous
dumpA11YTree()
: Helper to write debugging information about anXAccessibleContext
and its children recursively.isDocumentRole()
: Helper to check if an accessible role is one used for documents of any kind.
The test::SwAccessibleTestBase
class
This is a specialized class for Writer, inheriting from test::AccessibleTestBase
.
In addition to overriding a few methods for better handling Writer documents, it also provides a couple methods of its own. The most useful is certainly collectText()
.
collectText()
: Retrieve the structure and text of anXAccessibleContext
(default to the one returned bygetDocumentAccessibleContext()
) as a pseudo-XML representation. This is handy to compare actual structures to their expected equivalent.
The test::AccessibilityTools
"namespace"
AccessibilityTools
is an abstract class hoisting static methods only, it is not instanciated. It contains several helpers to work with accessible objects, hiding away some complexity, or simply providing commonly useful code.
Among other things, it provides CppUnit assertion_traits
for some css::accessibility
classes. Including the AccessibilityTools.hxx
header is enough for assertions like CPPUNIT_ASSERT_EQUAL()
and CPPUNIT_ASSERT_EQUAL_MESSAGE()
to make use of it, which means they will be able to better compare and display differences between accessible objects.
Retrieving objects
getAccessibleObjectForPredicate()
: generic helper to locate a child in an accessible tree. The predicate is anstd::function
that receives an accessible object and returns whether it is a match.getAccessibleObjectForRole()
: variant ofgetAccessibleObjectForPredicate()
that retrieves a object with a given role that is also showing. This is quick and easy way to retrieve an object in a hierarchy when there is only one matching a role.getAccessibleObjectForName()
: Similar togetAccessibleObjectForRole()
, but matches both a role and a name. There is also a template version allowing for more than one pair of role and name: the target object is the last pair, and previous pairs denote intermediate objects that must match in the branch, allowing to resolve most ambiguities if several objects could match.
Comparisons
It is not always possible to compare accessible objects directly, as sometimes more than one object is created to represent the same thing, depending on how the object was obtained. Here are a few helpers to perform comparisons.
equals()
: Compares two accessible objects and returns whether they are equivalent (e.g. represents the same thing). This does *not* mean they are the same C++ object.nameEquals()
: Compares an accessible object's name with a string. This method should always be used instead of direct string comparisons, because several special cases lead to the actual accessible name to have an unexpected suffix (this includes some debug builds, Windows menu items, etc.).
Waiting and awaiting
It is sometimes needed to wait for changes to happen. When it is not possible or convenient to have a fully-fledged listener for some event, the class provides tools to poll for a change or wait for some time. These should only be used when a single call to Scheduler::ProcessEventsToIdle()
is not enough. See the section awaiting changes for more details.
Await()
: Dispatch events while polling until a predicate returnstrue
or a timeout expires. This is useful to dispatch events while waiting for a change to happen.Wait()
: Dispatch events for a given amount of time. This should be used very sparingly, because straight up waiting is rarely a good solution. It can however be needed still, e.g. when waiting for something *not* to happen (imagine verifying some state does not change, or some event does not fire).
Debugging
To help logging details of the tests, this class also provides several helpers to get a human readable string representation of various types.
getRoleName()
: Gets the name of an AccessibleRole.getEventIdName()
: Gets the name of an AccessibleEventId.getRelationTypeName()
: Gets the name of an AccessibleRelationType.debugAccessibleStateSet()
: Gets a string representation of anAccessibleStateSet
(which is a bitfield internally).debugString()
: Gets a string representation of any supported type (this currently includesXAccessible
,XAccessibleContext
,XAccessibleAction
andAccessibleEventObject
), either as a pointer, or a C++ or UNO reference.
Awaiting changes
It is often required to wait for events being dispatched before some changes actually happen when interacting with user interfaces.
The naive solution is to use Scheduler::ProcessEventsToIdle()
, which is almost a good one, but if the change might happen after the next idle state (e.g. being triggered by a timeout or similar).
Instead, one can use the AccessibilityTools::Await()
family of helper functions. They are basically identical to Scheduler::ProcessEventsToIdle()
, but they process events until a condition is met, or a timeout limit is reached, whichever comes first.
As an example, an excerpt of XAccessibleEventBroadcasterTester
:
mxBroadcaster->addAccessibleEventListener(xListener); // add an event listener
fireEvent(); // produce an event the listener is supposed to receive
AccessibilityTools::Await([&xListener]() { return xListener->mbGotEvent; }); // await the event
AccessibilityTools::Await()
takes a callable that is used to poll for the expected state, dispatching events in-between. In the example above, a lambda is used to check whether the listener's mbGotEvent
member got set. It is a good idea to limit the amount of work done in the callable as it might be called numerous times; yet it's not a critical path as it's a test scenario.
Awaiting an accessible state could be achieved like this:
uno::Reference<accessibility::XAccessibleContext> xContext = xAccessible->getAccessibleContext();
uno::Reference<accessibility::XAccessibleComponent> xComponent(xContext, uno::UNO_QUERY_THROW);
xComponent->grabFocus();
AccessibilityTools::Await([&xContext]() {
return xContext->getAccessibleStateSet() & accessibility::AccessibleStateType::FOCUSED;
});
AccessibilityTools::Await()
returns the value from the callable, so it is suitable as an assertion value: if the call terminated because it reached the timeout without the condition being satisfied, it will return false
. So, this could be used as such:
CPPUNIT_ASSERT_MESSAGE("Component didn't obtain focus", AccessibilityTools::Await([&xContext]() {
return xContext->getAccessibleStateSet() & accessibility::AccessibleStateType::FOCUSED;
}));
Dealing with dialogs
WARNING: This API is currently not available under macOS. Tests using it should either be entirely or partially guarded with #if !defined(MACOSX)
or similar.
Dialogs are special as they require interacting with a base window element separate from the document window, and may be run in a nested loop blocking the test code until the dialog gets closed. Because of this, opening a dialog is blocked by default inside CppUnit test runs, and trying to do so will auto-cancel the dialog and log a warning. To circumvent this, there is a specific API to run test code inside a dialog, and make sure it is properly closed in all cases, not risking blocking the test.
This is test::AccessibleTestBase::awaitDialog()
. To use it, your test needs to inherit from test::AccessibleTestBase
. Your code will use the awaitDialog()
method to register a callback that will run in the dialog's context (shortly after the WindowActivate
event), and perform any testing required there as it would otherwise. This method returns an object that will be used to wait for the dialog to close. Then, you'll trigger the dialog by e.g. activating the relevant menu item, and finally wait for the dialog run to end and collect any exception.
// register a method to be called when the dialog runs, with a handle to that dialog
auto dialogWaiter = awaitDialog(u"Hyperlink", [this](Dialog& dialog) {
// perform the tests here, using dialog helper to access the content
});
// Activate the Insert->Hyperlink... menu item to open the Hyperlink dialog
CPPUNIT_ASSERT(activateMenuItem(u"Insert", u"Hyperlink..."));
// wait for the dialog to close
CPPUNIT_ASSERT(dialogWaiter->waitEndDialog());
// now back in document context, you can check whatever the dialog should have altered, if anything
awaitDialog()
takes two relevant parameters: the name (title) of the dialog that is expected, and the callback to run when that dialog opens up. Usually the callback parameter will be a lambda because of its convenience, but it can be any compatible std::function
. If a window with a different name than the one awaited opens, an exception will be raised instead of calling the callback.
awaitDialog()
returns a test::AccessibleTestBase::DialogWaiter
object that is used to await the dialog, with the sole relevant method waitEndDialog()
. Any exception raised in the callback context will have actually been collected and will be raised when calling waitEndDialog()
.
Note that awaitDialog()
does not actually run any dialog, it merely registers a handler that will be run when a dialog runs. You still need to trigger the dialog manually, e.g. by activating the relevant menu item, like Insert → Hyperlink... in the example above.
Note that given that some dialogs are synchronous and others are asynchronous, it is not guaranteed whether the call triggering the dialog returns before the dialog terminates or not. Specifically, this means that if any code is run between triggering the dialog and calling waitEndDialog()
, there is no guarantee to whether the callback code has run already, or whether the dialog is open or not at this point (it will depend on how the dialog is implemented internally). It however should not usually matter, as waitEndDialog()
makes sure to do what's needed to wait for the dialog to close.
The callback receives a Dialog&
parameter: it is a helper object wrapping the dialog window object, and providing a few helpers. Useful methods include:
postKeyEventAsync()
andpostExtTextEventAsync()
to post events in the context of the dialog;getAccessible()
to get a hold on the dialog's accessible tree root;tabTo()
convenience callingtest::AccessibleTestBase::tabTo()
on the dialog's contextclose()
to explicitly close the dialog, possibly specifying a response type. Most tests will however usually rather close the dialog by activating the relevant control in the dialog itself rather than calling this method, as it better mimics user interaction.
Some simple examples can be found in Writer basic dialog tests.
Converting Java tests
Here we will use toolkit/qa/complex/toolkit/AccessibleStatusBar.java as an example (and you can check out the actual conversion to CppUnit). This is a test of the actual UI in action.
Overview
Translating from Java is fairly straightforward for the most part, but some key points require to be familiar with the UNO integration for each language.
Replace calls like XAccessible xAcc = UnoRuntime.queryInterface(XAccessible.class, oObj)
with css::uno::Reference<css::accessibility::XAccessible> xAcc(oObj, css::uno::UNO_SET_THROW)
.
You can use css::uno::UNO_QUERY
if the result is optional. This works for any XInterface
the object implements. To get a hold on the accessible interfaces, go through the associated accessible context on which they are implemented: css::uno::Reference<css::accessibility::XAccessibleComponent> xAccComponent(xAcc->getAccessibleContext(), css::uno::UNO_SET_THROW)
.
The other main difference is the test infrastructure, that is the boilerplate around the actual test code that actually runs the tests. The Cpp Unit Tests page gives some useful insight on the matter. Specifics for our example will be discussed below.
Use assertions whenever an issue is encountered, don't needlessly propagate return values. Doing so simply makes it harder to find the root location of the issue. Some of the JUnit tests did so, but it's not really an example to follow. Instead, convert manual checks and error messages to assertions (see below).
Make use of AccessibilityTools
when useful. Among other things, it provides CppUnit assertion_traits
for some css::accessibility
classes. Including the AccessibilityTools.hxx
header is enough for assertions like CPPUNIT_ASSERT_EQUAL()
and CPPUNIT_ASSERT_EQUAL_MESSAGE()
to make use of it, which means they will be able to better compare and display differences between accessible objects.
Setup
To better understand how the setup works compared to the Java version, we'll discuss the manual approach, which is closer to the Java version. Ultimately however we'll be able to use a more powerful helper taking care of most of this setup complexity.
First off, the CppUnit test class will inherit from test::BootstrapFixture
for all the heavy lifting setting up the environment.
We will want to load documents (or more specifically, create new ones), so we'll need to set up the XDesktop2
for that purpose, and test::BootstrapFixture
has the XComponentContext
we'll need. So, in our overridden setUp()
we'll chain up and then call frame::Desktop::create(mxComponentContext)
.
With this we can call loadComponentFromURL()
to create our documents and thus the frames and windows that come with it. The Java version made use of a helper we don't have, but basically it's a matter of calling loadComponentFromURL("private:factory/swriter", "_blank" /*...*/)
or similar.
We'll then need a bit of shenanigans to get the containing window so to access elements and their accessible, and possibly bring the new window to the front in case we need it.
uno::Reference<frame::XModel> xModel(xDocument, uno::UNO_QUERY_THROW);
uno::Reference<awt::XWindow> xWindow(xModel->getCurrentController()->getFrame()->getContainerWindow());
// and if we also need to bring the window to the front:
uno::Reference<awt::XTopWindow> xTopWindow(xWindow, uno::UNO_QUERY_THROW);
xTopWindow->toFront();
Then, we will want to get the XAccessible
interface on some object, depending on what the test actually is. To do so, simply query this interface on a relevant object, e.g. uno::Reference<accessibility::XAccessible> xAccessible(xWindow, uno::UNO_QUERY_THROW)
. You can get the XAccessibleContext
from there using getAccessibleContext()
, and then any more specific accessible interfaces the object might implement by querying that context for it (e.g. XAccessibleComponent
).
Once done with the test, we'll close the document using it's XCloseable
interface:
uno::Reference<util::XCloseable> xCloseable(xDocument, uno::UNO_QUERY_THROW);
xCloseable->close(false);
This gives a test class looking somewhat like this:
#include <com/sun/star/awt/XWindow.hpp>
#include <com/sun/star/awt/XTopWindow.hpp>
#include <com/sun/star/frame/Desktop.hpp>
#include <com/sun/star/frame/XFrame.hpp>
#include <com/sun/star/lang/XComponent.hpp>
#include <com/sun/star/uno/Reference.hxx>
#include <com/sun/star/util/XCloseable.hpp>
#include <test/bootstrapfixture.hxx>
using namespace css;
class AccessibleStatusBarTest : public test::BootstrapFixture
{
private:
uno::Reference<frame::XDesktop2> mxDesktop;
void testDocument();
public:
virtual void setUp() override;
CPPUNIT_TEST_SUITE(AccessibleStatusBarTest);
CPPUNIT_TEST(testDocument);
CPPUNIT_TEST_SUITE_END();
};
void AccessibleStatusBarTest::setUp()
{
test::BootstrapFixture::setUp();
mxDesktop = frame::Desktop::create(mxComponentContext);
}
void AccessibleStatusBarTest::testDocument()
{
uno::Reference<lang::XComponent> xDocument = mxDesktop->loadComponentFromURL(
"private:factory/swriter", "_blank", 40, {});
uno::Reference<frame::XModel> xModel(xDocument, uno::UNO_QUERY_THROW);
uno::Reference<awt::XWindow> xWindow
= xModel->getCurrentController()->getFrame()->getContainerWindow();
uno::Reference<awt::XTopWindow> xTopWindow(xWindow, uno::UNO_QUERY_THROW);
xTopWindow->toFront();
// actual test steps here
// close document
uno::Reference<util::XCloseable> xCloseable(xDocument, uno::UNO_QUERY_THROW);
xCloseable->close(false);
}
CPPUNIT_TEST_SUITE_REGISTRATION(AccessibleStatusBarTest);
Simplified version
This can actually be simplified a lot using the test::AccessibleTestBase class instead of test::BootstrapFixture
directly. test::AccessibleTestBase
provides all the facilities needed to load a document, fetch its accessible node and close it, as well as several other helpers.
Making use of it leads to our code looking a lot leaner:
#include <test/accessibletestbase.hxx>
using namespace css;
class AccessibleStatusBarTest : public test::AccessibleTestBase
{
private:
void testDocument();
public:
CPPUNIT_TEST_SUITE(AccessibleStatusBarTest);
CPPUNIT_TEST(testDocument);
CPPUNIT_TEST_SUITE_END();
};
void AccessibleStatusBarTest::testDocument()
{
// sets mxWindow as the container window, like xWindow in the previous example
// and mxDocument is the XComponent for the loaded document
load("private:factory/swriter");
// actual test steps here:
// close document (optional, and done implicitly otherwise)
close();
}
CPPUNIT_TEST_SUITE_REGISTRATION(AccessibleStatusBarTest);
Actually testing something
We are not really going to reimplement the whole Java example here, but simply a subset we're going to fold in the main test here instead of using helper classes as done in the real example, for simplicity's sake. As an example, we're going to implement test for XAccessibleComponent::getBounds()
. We're going to do this on the window itself, as it's slightly simpler than locating the status bar first, but once done it should be fairly obvious how to port what the actual Java version does.
First, we need to query the object to get the XAccessibleComponent
interface (we assume that interface ought to be implemented, which is the case for an awt::XWindow
):
uno::Reference<accessibility::XAccessibleComponent> xComponent(xWindow, uno::UNO_QUERY_THROW);
Then, we can implement the test:
awt::Rectangle bounds = xComponent->getBounds();
CPPUNIT_ASSERT_GREATEREQUAL(0, bounds.X);
CPPUNIT_ASSERT_GREATEREQUAL(0, bounds.Y);
CPPUNIT_ASSERT_GREATER(0, bounds.Width);
CPPUNIT_ASSERT_GREATER(0, bounds.Height);
…and that's all there is to it.
Platform accessibility tests
In addition to the internal implementation, it is useful to test the interface with the platform layer (IA2, UIA, AT-SPI, etc.), to verify that information is properly transmitted to the end user. One approach to do so is to compare the internal representation with the information available through the platform API.
AT-SPI2 (*NIX) with GTK3
Implementation for the Linux (and other Unices) AT-SPI2 platform layer can be found in vcl/qa/cppunit/a11y/atspi2/:
- atspiwrapper.hxx and atspiwrapper.cxx: As the name suggests, those provide wrappers around the libatspi C interface, mostly to hide the memory management complexity and make the interface more "C++-y".
- atspi2testbase.hxx contains the bare minimum for interacting with AT-SPI2 and getting a hold on its representation of the test case app.
- atspi2* are the tests implementation. What they do is basically traverse the internal a11y tree and compare each node to the corresponding AT-SPI2 one. Many cases are trivial, but some require translating the values for comparison, because the platform layer did a conversion itself.
In order to run, the test require being able to run the relevant VCL backend. AT-SPI2 also requires a DBus session. To reproduce this in a headless test environment, the tests are run inside an Xvfb virtual display running a separate DBus session (akin to xvfb-run dbus-launch --exit-with-session TESTCASE
).
libatspi wrappers
The code contains fairly extensive documentation, but here is the broad overview:
All the wrappers are very close to the libatspi C API, and the wrapper layer is very thin. Usually C numeric types are reported as-is, C strings as std::string
s, C arrays as std::vector
s, and GHashTable
s as std::unordered_map
s.
C++ objects are used to represent the C objects providing proper polymorphism. Most method implementations are merely calling the C counterpart using a templated invoker to transform types and exceptions to C++.
Objects are as follows:
Atspi::StateSet
: wraps anAtspiStateSet
object from libatspi. This is an object holding a similar information asaccessibility::XAccessibleContext::getAccessibleStateSet()
. This is returned by e.g.Atspi::Accessible::getStateSet()
.Atspi::Relation
: wraps anAtspiRelation
, which is similar toaccessibility::AccessibleRelation
. This is returned by e.g.Atspi::Accessible::getRelationSet()
.Atspi::Accessible
: wraps anAtspiAccessible
, which is similar toaccessibility::XAccessibleContext
, and has often similar methods. It also acts as the proxy for obtaining other interfaces the object implements, using thequery*()
methods, likequeryText()
to obtain theAtspi::Text
interface. This is similar to querying anXInterface
usinguno::Reference<XSomeInterface>(xContext, uno::UNO_QUERY_THROW)
. Note that it throwsuno::RuntimeException
if the object doesn't implement the queried interface.Atspi::Component
: wraps anAtspiComponent
, which is similar toaccessibility::XAccessibleComponent
, and has often similar methods.Atspi::Text
: wraps anAtspiText
, which is similar toaccessibility::XAccessibleText
, and has often similar methods. It also includes parts ofaccessibility::XAccessibleEditableText
,accessibility::XAccessibleTextAttributes
andaccessibility::XAccessibleTextMarkup
Atspi2TestBase
This is test base class inherting from test::AccessibleTestBase
and providing the base libatspi integration including initialization and fetching of the AT-SPI2 object representing the application under test and its top-level windows.
Note that the AT-SPI2 tree contains a couple more a11y nodes than the internal tree. This is due to some node being created by the toolkit itself (here, GTK3), instead of being exposed by LibreOffice itself. This is especially visible with LibreOffice's root a11y node which is some depth inside AT-SPI2's tree. Similarly, some controls have extra children in AT-SPI2, like scrollable areas having extra nodes for their scrollbars. This is however not usually an issue and can most of the time be ignored.
Atspi2TestTree
This is currently the only actual test using libatspi. It recursively walks the LibreOffice's internal a11y tree and compares the corresponding AT-SPI2 representation for equality. This means comparing the different properties and interfaces and checking they are consistent on both ends. As mentioned above, it often is a trivial equality comparison, but sometimes requires more complex code because the values are transformed or created at the platform layer level (e.g. in the GTK3 VCL plugin) to match the platform's expectations.
As a few examples, compareObjects()
compares accessibility::XAccessibleContext
against Atspi::Accessible
and compareTextObjects()
compares accessibility::XAccessibleContext
against Atspi::Text
. Those are called by compareTrees()
which walks down the a11y tree recursively.
One important element to keep in mind when performing tests is that any internal state change might alter the a11y tree. In the case of at least Writer, this includes scrolling the document which might create new objects and delete old ones. This means that tests that modify the state have to be carefully written for them not to interfere with the bottom-up tree walking -- e.g. a call should not affect which or how many children its parent has.
When needed, special cases can be implemented taking this into account; see e.g. testSwScroll()
.