summaryrefslogtreecommitdiff
path: root/include/test/a11y/accessibletestbase.hxx
blob: e23c2e12467ebbea565e60c2f2e2d39575eb48a4 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
/* -*- 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/.
 */

#pragma once

#include <test/testdllapi.hxx>

#include <deque>
#include <string>

#include <com/sun/star/accessibility/AccessibleRole.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/XWindow.hpp>
#include <com/sun/star/frame/Desktop.hpp>
#include <com/sun/star/lang/XComponent.hpp>
#include <com/sun/star/uno/Reference.hxx>

#include <vcl/ITiledRenderable.hxx>
#include <vcl/window.hxx>

#include <rtl/ustring.hxx>
#include <test/bootstrapfixture.hxx>
#include <test/a11y/eventposter.hxx>

#include "AccessibilityTools.hxx"

namespace test
{
class OOO_DLLPUBLIC_TEST AccessibleTestBase : public test::BootstrapFixture
{
protected:
    css::uno::Reference<css::frame::XDesktop2> mxDesktop;
    css::uno::Reference<css::lang::XComponent> mxDocument;
    css::uno::Reference<css::awt::XWindow> mxWindow;

    static bool isDocumentRole(const sal_Int16 role);

    virtual void load(const rtl::OUString& sURL);
    virtual void loadFromSrc(const rtl::OUString& sSrcPath);
    void close();
    css::uno::Reference<css::accessibility::XAccessibleContext> getWindowAccessibleContext();
    virtual css::uno::Reference<css::accessibility::XAccessibleContext>
    getDocumentAccessibleContext();

    void documentPostKeyEvent(int nType, int nCharCode, int nKeyCode)
    {
        vcl::ITiledRenderable* pTiledRenderable
            = dynamic_cast<vcl::ITiledRenderable*>(mxDocument.get());
        CPPUNIT_ASSERT(pTiledRenderable);
        pTiledRenderable->postKeyEvent(nType, nCharCode, nKeyCode);
    }

    static css::uno::Reference<css::accessibility::XAccessibleContext> getFirstRelationTargetOfType(
        const css::uno::Reference<css::accessibility::XAccessibleContext>& xContext,
        sal_Int16 relationType);

    /**
     * @brief Tries to list all children of an accessible
     * @param xContext An XAccessibleContext object
     * @returns The list of all children (but no more than @c AccessibilityTools::MAX_CHILDREN)
     *
     * This fetches children of @p xContext.  This would ideally just be the same than iterating
     * over children the regular way up to @c AccessibilityTools::MAX_CHILDREN, but unfortunately
     * some components (Writer, Impress, ...) do not provide all their children the regular way and
     * require specifics to include them.
     *
     * There is no guarantee on *which* children are returned if there are more than
     * @c AccessibilityTools::MAX_CHILDREN -- yet they will always be the same in a given context.
     */
    virtual std::deque<css::uno::Reference<css::accessibility::XAccessibleContext>>
    getAllChildren(const css::uno::Reference<css::accessibility::XAccessibleContext>& xContext);

    void dumpA11YTree(const css::uno::Reference<css::accessibility::XAccessibleContext>& xContext,
                      const int depth = 0);

    css::uno::Reference<css::accessibility::XAccessibleContext>
    getItemFromName(const css::uno::Reference<css::accessibility::XAccessibleContext>& xMenuCtx,
                    std::u16string_view name);
    bool
    activateMenuItem(const css::uno::Reference<css::accessibility::XAccessibleAction>& xAction);
    /* just convenience not to have to query accessibility::XAccessibleAction manually */
    bool activateMenuItem(const css::uno::Reference<css::accessibility::XAccessibleContext>& xCtx)
    {
        return activateMenuItem(css::uno::Reference<css::accessibility::XAccessibleAction>(
            xCtx, css::uno::UNO_QUERY_THROW));
    }

    /* convenience to get a menu item from a list of menu item names.  Unlike
     * getItemFromName(context, name), this requires subsequently found items to implement
     * XAccessibleAction, as each but the last item will be activated before looking for
     * the next one, to account for the fact menus might not be fully populated before being
     * activated. */
    template <typename... Ts>
    css::uno::Reference<css::accessibility::XAccessibleContext>
    getItemFromName(const css::uno::Reference<css::accessibility::XAccessibleContext>& xMenuCtx,
                    std::u16string_view name, Ts... names)
    {
        auto item = getItemFromName(xMenuCtx, name);
        CPPUNIT_ASSERT(item.is());
        activateMenuItem(item);
        return getItemFromName(item, names...);
    }

    /* convenience to activate an item by its name and all its parent menus up to xMenuCtx.
     * @see getItemFromName() */
    template <typename... Ts>
    bool
    activateMenuItem(const css::uno::Reference<css::accessibility::XAccessibleContext>& xMenuCtx,
                     Ts... names)
    {
        auto item = getItemFromName(xMenuCtx, names...);
        CPPUNIT_ASSERT(item.is());
        return activateMenuItem(item);
    }

    /* convenience to activate an item by its name and all its parent menus up to the main window
     * menu bar */
    template <typename... Ts> bool activateMenuItem(Ts... names)
    {
        auto menuBar = AccessibilityTools::getAccessibleObjectForRole(
            getWindowAccessibleContext(), css::accessibility::AccessibleRole::MENU_BAR);
        CPPUNIT_ASSERT(menuBar.is());
        return activateMenuItem(menuBar, names...);
    }

    /**
     * @brief Gets the focused accessible object at @p xAcc level or below
     * @param xAcc An accessible object
     * @returns The accessible context of the focused object, or @c nullptr
     *
     * Finds the accessible object context at or under @p xAcc that has the focused state (and is
     * showing).  Normally only one such object should exist in a given hierarchy, but in all cases
     * this function will return the first one found.
     *
     * @see AccessibilityTools::getAccessibleObjectForPredicate()
     */
    static css::uno::Reference<css::accessibility::XAccessibleContext>
    getFocusedObject(const css::uno::Reference<css::accessibility::XAccessibleContext>& xCtx);

    static inline css::uno::Reference<css::accessibility::XAccessibleContext>
    getFocusedObject(const css::uno::Reference<css::accessibility::XAccessible>& xAcc)
    {
        return getFocusedObject(xAcc->getAccessibleContext());
    }

    /**
     * @brief Navigates through focusable elements using the Tab keyboard shortcut.
     * @param xRoot The root element to look for focused elements in.
     * @param role The accessible role of the element to tab to.
     * @param name The accessible name of the element to tab to.
     * @param pEventPosterHelper Pointer to a @c EventPosterHelper instance, or @c nullptr to obtain
     *                           it from @p xRoot.
     * @returns The element tabbed to, or @c nullptr if not found.
     *
     * Navigates through focusable elements in the top level containing @p xRoot using the Tab
     * keyboard key until the focused elements matches @p role and @p name.
     *
     * Note that usually @p xRoot should be the toplevel accessible, or at least contain all
     * focusable elements within that window.  It is however *not* a requirement, but only elements
     * actually inside it will be candidate for a match, and thus if focus goes outside it, it might
     * lead to not finding the target element.
     *
     * If @p pEventPosterHelper is @c nullptr, this function will try to construct one from
     * @p xRoot.  @see EventPosterHelper.
     */
    static css::uno::Reference<css::accessibility::XAccessibleContext>
    tabTo(const css::uno::Reference<css::accessibility::XAccessible>& xRoot, const sal_Int16 role,
          const std::u16string_view name,
          const EventPosterHelperBase* pEventPosterHelper = nullptr);

    static bool tabTo(const css::uno::Reference<css::accessibility::XAccessible>& xRoot,
                      const css::uno::Reference<css::accessibility::XAccessibleContext>& xChild,
                      const EventPosterHelperBase* pEventPosterHelper = nullptr);

#if !defined(MACOSX)
    /* Dialog handling */
    class Dialog : public test::AccessibleEventPosterHelper
    {
    private:
        bool mbAutoClose;
        css::uno::Reference<css::awt::XDialog2> mxDialog2;
        css::uno::Reference<css::accessibility::XAccessible> mxAccessible;

    public:
        Dialog(css::uno::Reference<css::awt::XDialog2>& xDialog2, bool bAutoClose = true);
        virtual ~Dialog();

        void setAutoClose(bool bAutoClose) { mbAutoClose = bAutoClose; }

        css::uno::Reference<css::accessibility::XAccessible> getAccessible() const
        {
            return mxAccessible;
        }

        void close(sal_Int32 result = VclResponseType::RET_CANCEL);

        css::uno::Reference<css::accessibility::XAccessibleContext>
        tabTo(const sal_Int16 role, const std::u16string_view name)
        {
            return AccessibleTestBase::tabTo(getAccessible(), role, name, this);
        }

        bool tabTo(const css::uno::Reference<css::accessibility::XAccessibleContext>& xChild)
        {
            return AccessibleTestBase::tabTo(getAccessible(), xChild, this);
        }
    };

    class DialogWaiter
    {
    public:
        virtual ~DialogWaiter() {}

        /**
         * @brief Waits for the associated dialog to close
         * @param nTimeoutMs Maximum delay to wait the dialog for
         * @returns @c true if the dialog closed, @c false if timeout was reached
         *
         * @throws css::uno::RuntimeException if an unexpected dialog popped up instead of the
         *         expected one.
         * @throws Any exception that the user callback supplied to awaitDialog() might have thrown.
         */
        virtual bool waitEndDialog(sal_uInt64 nTimeoutMs = 3000) = 0;
    };

    /**
     * @brief Helper to call user code when a given dialog opens
     * @param name The title of the dialog window to wait for
     * @param callback The user code to run when the given dialog opens
     * @param bAutoClose Whether to automatically cancel the dialog after the user code finished, if
     *                   the dialog is still there.  You should leave this to @c true unless you
     *                   know exactly what you are doing, see below.
     * @returns A @c DialogWaiter wrapper on which call waitEndDialog() after having triggered the
     *          dialog in some way.
     *
     * This function makes it fairly easy and safe to execute code once a dialog pops up:
     * @code
     * auto waiter = awaitDialog(u"Special Characters", [this](Dialog &dialog) {
     *     // for example, something like this:
     *     // something();
     *     // CPPUNIT_ASSERT(dialog.tabTo(...));
     *     // CPPUNIT_ASSERT(somethingElse);
     *     // dialog.postKeyEventAsync(0, awt::Key::RETURN);
     * });
     * CPPUNIT_ASSERT(activateMenuItem(u"Some menu", u"Some Item Triggering a Dialog..."));
     * CPPUNIT_ASSERT(waiter->waitEndDialog());
     * @endcode
     *
     * @note The user code might actually be executed before DialogWaiter::waitEndDialog() is
     *       called.  It is actually likely to be called at the time the call that triggers the
     *       dialog happens.  However, as letting an exception slip in a event handler is likely to
     *       cause problems, exceptions are forwarded to the DialogWaiter::waitEndDialog() call.
     *       However, note that you cannot rely on something like this:
     *       @code
     *       int foo = 0;
     *       auto waiter = awaitDialog(u"Some Dialog", [&foo](Dialog&) {
     *           CPPUNIT_ASSERT_EQUAL(1, foo);
     *       });
     *       CPPUNIT_ASSERT(activateMenuItem(u"Some menu", u"Some Item Triggering a Dialog..."));
     *       foo = 1; // here, the callback likely already ran as a result of the
     *                // Scheduler::ProcessEventsToIdle() call that activateMenuItem() did.
     *       CPPUNIT_ASSERT(waiter->waitEndDialog());
     *       @endcode
     *
     * @warning You should almost certainly always leave @p bAutoClose to @c true. If it is set to
     *          @c false, you have to take extreme care:
     *          - The dialog will not be canceled if the user code raises an exception.
     *          - If the dialog is run through Dialog::Execute(), control won't return to the test
     *            body until the dialog is closed.  This means that the only ways to execute code
     *            until then is a separate thread or via code dispatched by the main loop.
     *            Thus, you have to make sure you DO close the dialog some way or another yourself
     *            in order for the test code to terminate at some point.
     *          - If the dialog doesn't use Dialog::Execute() but is rather similar to a second
     *            separate window (e.g. non-modal), you might still have to close the dialog before
     *            closing the test document is possible without a CloseVetoException -- which might
     *            badly break the test run.
     */
    static std::shared_ptr<DialogWaiter> awaitDialog(const std::u16string_view name,
                                                     std::function<void(Dialog&)> callback,
                                                     bool bAutoClose = true);
#endif //defined(MACOSX)

public:
    virtual void setUp() override;
    virtual void tearDown() override;
};
}

/* vim:set shiftwidth=4 softtabstop=4 expandtab cinoptions=b1,g0,N-s cinkeys+=0=break: */