/*
 * Copyright (C) 2012, 2019 Igalia S.L.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2,1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Library General Public License for more details.
 *
 * You should have received a copy of the GNU Library General Public License
 * along with this library; see the file COPYING.LIB.  If not, write to
 * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
 * Boston, MA 02110-1301, USA.
 */

#include "config.h"

#include "WebViewTest.h"
#include <wtf/MonotonicTime.h>

// The libatspi headers don't use G_BEGIN_DECLS
extern "C" {
#include <atspi/atspi.h>
}

struct AtspiEventDeleter {
    void operator()(AtspiEvent* event) const
    {
        g_boxed_free(ATSPI_TYPE_EVENT, event);
    }
};

struct AtspiTextRangeDeleter {
    void operator()(AtspiTextRange* range) const
    {
        g_boxed_free(ATSPI_TYPE_TEXT_RANGE, range);
    }
};

using UniqueAtspiEvent = std::unique_ptr<AtspiEvent, AtspiEventDeleter>;
using UniqueAtspiTextRange = std::unique_ptr<AtspiTextRange, AtspiTextRangeDeleter>;

class AccessibilityTest : public WebViewTest {
public:
    MAKE_GLIB_TEST_FIXTURE(AccessibilityTest);

    GRefPtr<AtspiAccessible> findTestApplication()
    {
        // Only one desktop is supported by ATSPI at the moment.
        GRefPtr<AtspiAccessible> desktop = adoptGRef(atspi_get_desktop(0));

        // We can get warnings from atspi when trying to connect to applications.
        Test::removeLogFatalFlag(G_LOG_LEVEL_WARNING);
        int childCount = atspi_accessible_get_child_count(desktop.get(), nullptr);
        Test::addLogFatalFlag(G_LOG_LEVEL_WARNING);
        for (int i = childCount - 1; i >= 0; --i)  {
            GRefPtr<AtspiAccessible> current = adoptGRef(atspi_accessible_get_child_at_index(desktop.get(), i, nullptr));
            GUniquePtr<char> name(atspi_accessible_get_name(current.get(), nullptr));
            if (!g_strcmp0(name.get(), "TestWebKitAccessibility"))
                return current;
        }

        return 0;
    }

    GRefPtr<AtspiAccessible> findDocumentWeb(AtspiAccessible* accessible)
    {
        int childCount = atspi_accessible_get_child_count(accessible, nullptr);
        for (int i = 0; i < childCount; ++i) {
            GRefPtr<AtspiAccessible> child = adoptGRef(atspi_accessible_get_child_at_index(accessible, i, nullptr));
            if (atspi_accessible_get_role(child.get(), nullptr) == ATSPI_ROLE_DOCUMENT_WEB)
                return child;

            if (auto documentWeb = findDocumentWeb(child.get()))
                return documentWeb;
        }
        return nullptr;
    }

    GRefPtr<AtspiAccessible> findRootObject(AtspiAccessible* application)
    {
        // Find the document web, its parent is the scroll view (WebCore root object) and its parent is
        // the GtkPlug (WebProcess root element).
        auto documentWeb = findDocumentWeb(application);
        if (!documentWeb)
            return nullptr;

        auto parent = adoptGRef(atspi_accessible_get_parent(documentWeb.get(), nullptr));
        return parent ? adoptGRef(atspi_accessible_get_parent(parent.get(), nullptr)) : nullptr;
    }

    void waitUntilChildrenRemoved(AtspiAccessible* accessible)
    {
        m_eventSource = accessible;
        GRefPtr<AtspiEventListener> listener = adoptGRef(atspi_event_listener_new(
            [](AtspiEvent* event, gpointer userData) {
                auto* test = static_cast<AccessibilityTest*>(userData);
                if (event->source == test->m_eventSource)
                    g_main_loop_quit(test->m_mainLoop);
        }, this, nullptr));
        atspi_event_listener_register(listener.get(), "object:children-changed:remove", nullptr);
        g_main_loop_run(m_mainLoop);
        m_eventSource = nullptr;
    }

    static bool accessibleApplicationIsTestProgram(AtspiAccessible* accessible)
    {
        GRefPtr<AtspiAccessible> application = adoptGRef(atspi_accessible_get_application(accessible, nullptr));
        GUniquePtr<char> applicationName(atspi_accessible_get_name(application.get(), nullptr));
        return !g_strcmp0(applicationName.get(), "TestWebKitAccessibility");
    }

    bool shouldProcessEvent(AtspiEvent* event)
    {
        // source = std::nullopt -> no filter.
        if (!m_eventMonitor.source)
            return true;

        // source = nullptr -> filter by application.
        if (!m_eventMonitor.source.value())
            return accessibleApplicationIsTestProgram(event->source);

        // source != nullptr -> filter by accessible.
        return m_eventMonitor.source.value() == event->source;
    }

    void startEventMonitor(std::optional<AtspiAccessible*> source, Vector<CString>&& events)
    {
        m_eventMonitor.source = source;
        m_eventMonitor.eventTypes = WTFMove(events);
        m_eventMonitor.listener = adoptGRef(atspi_event_listener_new([](AtspiEvent* event, gpointer userData) {
            auto* test = static_cast<AccessibilityTest*>(userData);
            if (test->shouldProcessEvent(event))
                test->m_eventMonitor.events.append(static_cast<AtspiEvent*>(g_boxed_copy(ATSPI_TYPE_EVENT, event)));
        }, this, nullptr));

        for (const auto& event : m_eventMonitor.eventTypes)
            atspi_event_listener_register(m_eventMonitor.listener.get(), event.data(), nullptr);
    }

    Vector<UniqueAtspiEvent> stopEventMonitor(unsigned expectedEvents, std::optional<Seconds> timeout = std::nullopt)
    {
        // If events is empty wait for the events.
        auto startTime = MonotonicTime::now();
        while (m_eventMonitor.events.size() < expectedEvents && MonotonicTime::now() - startTime < timeout.value_or(Seconds::infinity()))
            g_main_context_iteration(nullptr, timeout ? FALSE : TRUE);

        auto events = WTFMove(m_eventMonitor.events);
        for (const auto& event : m_eventMonitor.eventTypes)
            atspi_event_listener_deregister(m_eventMonitor.listener.get(), event.data(), nullptr);
        m_eventMonitor = { nullptr, { }, { }, nullptr };
        return events;
    }

    static AtspiEvent* findEvent(const Vector<UniqueAtspiEvent>& events, const char* eventType)
    {
        for (const auto& event : events) {
            if (!g_strcmp0(event->type, eventType))
                return event.get();
        }

        return nullptr;
    }

    static unsigned stateSetSize(AtspiStateSet* stateSet)
    {
        GArray* states = atspi_state_set_get_states(stateSet);
        unsigned length = states->len;
        g_array_free(states, TRUE);
        return length;
    }

    static bool isSelected(AtspiAccessible* element)
    {
        GRefPtr<AtspiStateSet> set = adoptGRef(atspi_accessible_get_state_set(element));
        return atspi_state_set_contains(set.get(), ATSPI_STATE_SELECTED);
    }

private:
    AtspiAccessible* m_eventSource { nullptr };

    struct {
        GRefPtr<AtspiEventListener> listener;
        Vector<CString> eventTypes;
        Vector<UniqueAtspiEvent> events;
        std::optional<AtspiAccessible*> source;
    } m_eventMonitor;
};

static void testAccessibleBasicHierarchy(AccessibilityTest* test, gconstpointer)
{
    test->showInWindow();
    test->loadHtml(
        "<html>"
        "  <body>"
        "   <h1>This is a test</h1>"
        "   <p>This is a paragraph with some plain text.</p>"
        "   <p>This paragraph contains <a href=\"http://www.webkitgtk.org\">a link</a> in the middle.</p>"
        "  </body>"
        "</html>",
        nullptr);
    test->waitUntilLoadFinished();

    auto testApp = test->findTestApplication();
    g_assert_true(ATSPI_IS_ACCESSIBLE(testApp.get()));
    GUniquePtr<char> name(atspi_accessible_get_name(testApp.get(), nullptr));
    g_assert_cmpstr(name.get(), ==, "TestWebKitAccessibility");
    g_assert_cmpint(atspi_accessible_get_role(testApp.get(), nullptr), ==, ATSPI_ROLE_APPLICATION);

    auto rootObject = test->findRootObject(testApp.get());
    g_assert_true(ATSPI_IS_ACCESSIBLE(rootObject.get()));
    g_assert_cmpint(atspi_accessible_get_role(rootObject.get(), nullptr), ==, ATSPI_ROLE_FILLER);

    auto scrollView = adoptGRef(atspi_accessible_get_child_at_index(rootObject.get(), 0, nullptr));
    g_assert_true(ATSPI_IS_ACCESSIBLE(scrollView.get()));
    g_assert_cmpint(atspi_accessible_get_role(scrollView.get(), nullptr), ==, ATSPI_ROLE_SCROLL_PANE);

    auto documentWeb = adoptGRef(atspi_accessible_get_child_at_index(scrollView.get(), 0, nullptr));
    g_assert_true(ATSPI_IS_ACCESSIBLE(documentWeb.get()));
    g_assert_cmpint(atspi_accessible_get_role(documentWeb.get(), nullptr), ==, ATSPI_ROLE_DOCUMENT_WEB);

    auto h1 = adoptGRef(atspi_accessible_get_child_at_index(documentWeb.get(), 0, nullptr));
    g_assert_true(ATSPI_IS_ACCESSIBLE(h1.get()));
    name.reset(atspi_accessible_get_name(h1.get(), nullptr));
    g_assert_cmpstr(name.get(), ==, "This is a test");
    g_assert_cmpint(atspi_accessible_get_role(h1.get(), nullptr), ==, ATSPI_ROLE_HEADING);
    name.reset(atspi_accessible_get_localized_role_name(h1.get(), nullptr));
    g_assert_cmpstr(name.get(), ==, "heading");

    auto p1 = adoptGRef(atspi_accessible_get_child_at_index(documentWeb.get(), 1, nullptr));
    g_assert_true(ATSPI_IS_ACCESSIBLE(p1.get()));
    g_assert_cmpint(atspi_accessible_get_role(p1.get(), nullptr), ==, ATSPI_ROLE_PARAGRAPH);
    name.reset(atspi_accessible_get_localized_role_name(p1.get(), nullptr));
    g_assert_cmpstr(name.get(), ==, "paragraph");

    auto p2 = adoptGRef(atspi_accessible_get_child_at_index(documentWeb.get(), 2, nullptr));
    g_assert_true(ATSPI_IS_ACCESSIBLE(p2.get()));
    g_assert_cmpint(atspi_accessible_get_role(p2.get(), nullptr), ==, ATSPI_ROLE_PARAGRAPH);
    name.reset(atspi_accessible_get_localized_role_name(p2.get(), nullptr));
    g_assert_cmpstr(name.get(), ==, "paragraph");

    auto link = adoptGRef(atspi_accessible_get_child_at_index(p2.get(), 0, nullptr));
    g_assert_true(ATSPI_IS_ACCESSIBLE(link.get()));
    name.reset(atspi_accessible_get_name(link.get(), nullptr));
    g_assert_cmpstr(name.get(), ==, "a link");
    g_assert_cmpint(atspi_accessible_get_role(link.get(), nullptr), ==, ATSPI_ROLE_LINK);
    name.reset(atspi_accessible_get_localized_role_name(link.get(), nullptr));
    g_assert_cmpstr(name.get(), ==, "link");

    test->loadHtml(
        "<html>"
        "  <body>"
        "   <h1>This is another test</h1>"
        "   <img src='data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAIAAAD91JpzAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3AYWDTMVwnSZnwAAAB1pVFh0Q29tbWVudAAAAAAAQ3JlYXRlZCB3aXRoIEdJTVBkLmUHAAAAFklEQVQI12P8z8DAwMDAxMDAwMDAAAANHQEDK+mmyAAAAABJRU5ErkJggg=='/>"
        "  </body>"
        "</html>",
        nullptr);
    // Check that children-changed::remove is emitted on the root object on navigation,
    // and the a11y hierarchy is updated.
    test->waitUntilChildrenRemoved(rootObject.get());

    documentWeb = test->findDocumentWeb(testApp.get());
    g_assert_true(ATSPI_IS_ACCESSIBLE(documentWeb.get()));
    g_assert_cmpint(atspi_accessible_get_role(documentWeb.get(), nullptr), ==, ATSPI_ROLE_DOCUMENT_WEB);

    h1 = adoptGRef(atspi_accessible_get_child_at_index(documentWeb.get(), 0, nullptr));
    g_assert_true(ATSPI_IS_ACCESSIBLE(h1.get()));
    name.reset(atspi_accessible_get_name(h1.get(), nullptr));
    g_assert_cmpstr(name.get(), ==, "This is another test");
    g_assert_cmpint(atspi_accessible_get_role(h1.get(), nullptr), ==, ATSPI_ROLE_HEADING);

    auto section = adoptGRef(atspi_accessible_get_child_at_index(documentWeb.get(), 1, nullptr));
    g_assert_true(ATSPI_IS_ACCESSIBLE(section.get()));
    g_assert_cmpint(atspi_accessible_get_role(section.get(), nullptr), ==, ATSPI_ROLE_SECTION);

    auto img = adoptGRef(atspi_accessible_get_child_at_index(section.get(), 0, nullptr));
    g_assert_true(ATSPI_IS_ACCESSIBLE(img.get()));
    g_assert_cmpint(atspi_accessible_get_role(img.get(), nullptr), ==, ATSPI_ROLE_IMAGE);
}

static void testAccessibleIgnoredObjects(AccessibilityTest* test, gconstpointer)
{
    test->showInWindow();
    test->loadHtml(
        "<html>"
        "  <body>"
        "    <div>"
        "      <p>Static text nodes are ignored, so this paragraph shouldn't have a child</p>"
        "    </div>"
        "  </body>"
        "</html>",
        nullptr);
    test->waitUntilLoadFinished();

    auto testApp = test->findTestApplication();
    g_assert_true(ATSPI_IS_ACCESSIBLE(testApp.get()));

    auto documentWeb = test->findDocumentWeb(testApp.get());
    g_assert_true(ATSPI_IS_ACCESSIBLE(documentWeb.get()));
    g_assert_cmpint(atspi_accessible_get_child_count(documentWeb.get(), nullptr), ==, 1);

    auto p = adoptGRef(atspi_accessible_get_child_at_index(documentWeb.get(), 0, nullptr));
    g_assert_true(ATSPI_IS_ACCESSIBLE(p.get()));
    g_assert_cmpint(atspi_accessible_get_role(p.get(), nullptr), ==, ATSPI_ROLE_PARAGRAPH);
    g_assert_cmpint(atspi_accessible_get_child_count(p.get(), nullptr), ==, 0);
}

static void testAccessibleChildrenChanged(AccessibilityTest* test, gconstpointer)
{
    test->showInWindow();
    test->loadHtml(
        "<html>"
        "  <body>"
        "    <div id='grandparent'>"
        "      <div id='parent'>"
        "        <p>Foo</p><p>Bar</p>"
        "      </div>"
        "    </div>"
        "  </body>"
        "</html>",
        nullptr);
    test->waitUntilLoadFinished();

    auto testApp = test->findTestApplication();
    g_assert_true(ATSPI_IS_ACCESSIBLE(testApp.get()));

    auto documentWeb = test->findDocumentWeb(testApp.get());
    g_assert_true(ATSPI_IS_ACCESSIBLE(documentWeb.get()));
    // The divs are not exposed to ATs.
    g_assert_cmpint(atspi_accessible_get_child_count(documentWeb.get(), nullptr), ==, 2);

    auto foo = adoptGRef(atspi_accessible_get_child_at_index(documentWeb.get(), 0, nullptr));
    g_assert_true(ATSPI_IS_ACCESSIBLE(foo.get()));
    g_assert_cmpint(atspi_accessible_get_child_count(foo.get(), nullptr), ==, 0);

    auto bar = adoptGRef(atspi_accessible_get_child_at_index(documentWeb.get(), 1, nullptr));
    g_assert_true(ATSPI_IS_ACCESSIBLE(bar.get()));
    g_assert_cmpint(atspi_accessible_get_child_count(bar.get(), nullptr), ==, 0);

    // Add a new paragraph.
    test->startEventMonitor(documentWeb.get(), { "object:children-changed:add", "object:children-changed:remove" });
    test->runJavaScriptAndWaitUntilFinished("let p = document.createElement('p'); p.innerText = 'Baz'; document.getElementById('parent').appendChild(p);", nullptr);
    auto events = test->stopEventMonitor(1);
    g_assert_cmpuint(events.size(), ==, 1);
    g_assert_cmpstr(events[0]->type, ==, "object:children-changed:add");
    g_assert_cmpuint(events[0]->detail1, ==, 2); // New node added to position 2.
    g_assert_true(G_VALUE_HOLDS_OBJECT(&events[0]->any_data));
    auto baz = adoptGRef(static_cast<AtspiAccessible*>(g_value_dup_object(&events[0]->any_data)));
    g_assert_true(ATSPI_IS_ACCESSIBLE(baz.get()));
    g_assert_cmpint(atspi_accessible_get_role(baz.get(), nullptr), ==, ATSPI_ROLE_PARAGRAPH);
    g_assert_true(atspi_accessible_get_parent(baz.get(), nullptr) == documentWeb.get());
    g_assert_cmpint(atspi_accessible_get_child_count(documentWeb.get(), nullptr), ==, 3);
    GRefPtr<AtspiAccessible> child = adoptGRef(atspi_accessible_get_child_at_index(documentWeb.get(), 0, nullptr));
    g_assert_true(foo.get() == child.get());
    child = adoptGRef(atspi_accessible_get_child_at_index(documentWeb.get(), 1, nullptr));
    g_assert_true(bar.get() == child.get());
    child = adoptGRef(atspi_accessible_get_child_at_index(documentWeb.get(), 2, nullptr));
    g_assert_true(baz.get() == child.get());
    child = nullptr;
    events = { };

    // Remove one of the paragraphs.
    test->startEventMonitor(documentWeb.get(), { "object:children-changed:add", "object:children-changed:remove" });
    test->runJavaScriptAndWaitUntilFinished("let div = document.getElementById('parent'); div.removeChild(div.children[0]);", nullptr);
    events = test->stopEventMonitor(1);
    g_assert_cmpuint(events.size(), ==, 1);
    g_assert_cmpstr(events[0]->type, ==, "object:children-changed:remove");
    g_assert_cmpuint(events[0]->detail1, ==, 0); // Remove node was at position 0.
    g_assert_true(G_VALUE_HOLDS_OBJECT(&events[0]->any_data));
    g_assert_true(g_value_get_object(&events[0]->any_data) == foo.get());
    GRefPtr<AtspiStateSet> set = adoptGRef(atspi_accessible_get_state_set(foo.get()));
    g_assert_true(atspi_state_set_contains(set.get(), ATSPI_STATE_DEFUNCT));
    g_assert_cmpint(atspi_accessible_get_child_count(documentWeb.get(), nullptr), ==, 2);
    child = adoptGRef(atspi_accessible_get_child_at_index(documentWeb.get(), 0, nullptr));
    g_assert_true(bar.get() == child.get());
    child = adoptGRef(atspi_accessible_get_child_at_index(documentWeb.get(), 1, nullptr));
    g_assert_true(baz.get() == child.get());
    child = nullptr;
    events = { };

    // Changing role causes an internal detach + attach that shouldn't be exposed to ATs.
    test->startEventMonitor(documentWeb.get(), { "object:children-changed:add", "object:children-changed:remove" });
    test->runJavaScriptAndWaitUntilFinished("document.getElementById('parent').children[0].role='button'", nullptr);
    events = test->stopEventMonitor(0, 0.5_s);
    g_assert_cmpuint(events.size(), ==, 0);
    g_assert_cmpint(atspi_accessible_get_role(bar.get(), nullptr), ==, ATSPI_ROLE_PUSH_BUTTON);
    events = { };

    // Remove the container.
    test->startEventMonitor(documentWeb.get(), { "object:children-changed:add", "object:children-changed:remove" });
    test->runJavaScriptAndWaitUntilFinished("document.getElementById('grandparent').removeChild(document.getElementById('parent'));", nullptr);
    events = test->stopEventMonitor(2);
    g_assert_cmpuint(events.size(), ==, 2);
    g_assert_cmpstr(events[0]->type, ==, "object:children-changed:remove");
    g_assert_cmpstr(events[1]->type, ==, "object:children-changed:remove");
    g_assert_true(G_VALUE_HOLDS_OBJECT(&events[0]->any_data));
    g_assert_true(G_VALUE_HOLDS_OBJECT(&events[1]->any_data));
    g_assert_true(g_value_get_object(&events[0]->any_data) == bar.get() || g_value_get_object(&events[0]->any_data) == baz.get());
    g_assert_true(g_value_get_object(&events[1]->any_data) == bar.get() || g_value_get_object(&events[1]->any_data) == baz.get());
    g_assert_true(g_value_get_object(&events[0]->any_data) != g_value_get_object(&events[1]->any_data));
    set = adoptGRef(atspi_accessible_get_state_set(bar.get()));
    g_assert_true(atspi_state_set_contains(set.get(), ATSPI_STATE_DEFUNCT));
    set = adoptGRef(atspi_accessible_get_state_set(baz.get()));
    g_assert_true(atspi_state_set_contains(set.get(), ATSPI_STATE_DEFUNCT));
    g_assert_cmpint(atspi_accessible_get_child_count(documentWeb.get(), nullptr), ==, 0);
}

static void testAccessibleAttributes(AccessibilityTest* test, gconstpointer)
{
    test->showInWindow();
    test->loadHtml(
        "<html>"
        "  <body>"
        "    <h2>Heading level 2</h2>"
        "    <a id=\"webkitgtk\" href=\"http://www.webkitgtk.org\">A link</a>"
        "  </body>"
        "</html>",
        nullptr);
    test->waitUntilLoadFinished();

    static const char* toolkitName = "WebKitGTK";

    auto testApp = test->findTestApplication();
    g_assert_true(ATSPI_IS_ACCESSIBLE(testApp.get()));

    auto documentWeb = test->findDocumentWeb(testApp.get());
    g_assert_true(ATSPI_IS_ACCESSIBLE(documentWeb.get()));
    g_assert_cmpint(atspi_accessible_get_child_count(documentWeb.get(), nullptr), ==, 2);
    auto attributes = adoptGRef(atspi_accessible_get_attributes(documentWeb.get(), nullptr));
    g_assert_cmpuint(g_hash_table_size(attributes.get()), ==, 1);
    g_assert_cmpstr(static_cast<const char*>(g_hash_table_lookup(attributes.get(), "toolkit")), ==, toolkitName);

    auto h2 = adoptGRef(atspi_accessible_get_child_at_index(documentWeb.get(), 0, nullptr));
    g_assert_true(ATSPI_IS_ACCESSIBLE(h2.get()));
    attributes = adoptGRef(atspi_accessible_get_attributes(h2.get(), nullptr));
    g_assert_cmpuint(g_hash_table_size(attributes.get()), ==, 4);
    g_assert_cmpstr(static_cast<const char*>(g_hash_table_lookup(attributes.get(), "toolkit")), ==, toolkitName);
    g_assert_cmpstr(static_cast<const char*>(g_hash_table_lookup(attributes.get(), "computed-role")), ==, "heading");
    g_assert_cmpstr(static_cast<const char*>(g_hash_table_lookup(attributes.get(), "level")), ==, "2");
    g_assert_cmpstr(static_cast<const char*>(g_hash_table_lookup(attributes.get(), "tag")), ==, "h2");

    auto section = adoptGRef(atspi_accessible_get_child_at_index(documentWeb.get(), 1, nullptr));
    g_assert_true(ATSPI_IS_ACCESSIBLE(section.get()));
    g_assert_cmpint(atspi_accessible_get_child_count(section.get(), nullptr), ==, 1);

    auto a = adoptGRef(atspi_accessible_get_child_at_index(section.get(), 0, nullptr));
    g_assert_true(ATSPI_IS_ACCESSIBLE(a.get()));
    attributes = adoptGRef(atspi_accessible_get_attributes(a.get(), nullptr));
    g_assert_cmpuint(g_hash_table_size(attributes.get()), ==, 4);
    g_assert_cmpstr(static_cast<const char*>(g_hash_table_lookup(attributes.get(), "toolkit")), ==, toolkitName);
    g_assert_cmpstr(static_cast<const char*>(g_hash_table_lookup(attributes.get(), "computed-role")), ==, "link");
    g_assert_cmpstr(static_cast<const char*>(g_hash_table_lookup(attributes.get(), "id")), ==, "webkitgtk");
    g_assert_cmpstr(static_cast<const char*>(g_hash_table_lookup(attributes.get(), "tag")), ==, "a");
}

static void testAccessibleState(AccessibilityTest* test, gconstpointer)
{
    test->showInWindow(800, 600);
    test->loadHtml(
        "<html>"
        "  <body>"
        "    <h1>State</h1>"
        "    <button>Button</button>"
        "    <button disabled>Disabled</button>"
        "    <button aria-pressed='true'>Toggle button</button>"
        "    <input type='radio' name='radio'/>Radio"
        "    <input type='radio' name='radio' checked/>Radio Checked"
        "    <input type='radio' name='radio' disabled/>Radio disabled"
        "    <input type='checkbox' name='check-disabled' checked disabled/>Checked Disabled"
        "    <input value='Entry' aria-required='true'>"
        "    <input value='Disabled' disabled>"
        "    <input value='Read only' readonly>"
        "    <textarea rows=5 autofocus></textarea>"
        "    <ul><li aria-current='step'>Current</li></ul>"
        "  </body>"
        "</html>",
        nullptr);
    test->waitUntilLoadFinished();

    auto testApp = test->findTestApplication();
    g_assert_true(ATSPI_IS_ACCESSIBLE(testApp.get()));

    auto documentWeb = test->findDocumentWeb(testApp.get());
    g_assert_true(ATSPI_IS_ACCESSIBLE(documentWeb.get()));
    g_assert_cmpint(atspi_accessible_get_child_count(documentWeb.get(), nullptr), ==, 3);

    auto h1 = adoptGRef(atspi_accessible_get_child_at_index(documentWeb.get(), 0, nullptr));
    g_assert_true(ATSPI_IS_ACCESSIBLE(h1.get()));
    GRefPtr<AtspiStateSet> stateSet = adoptGRef(atspi_accessible_get_state_set(h1.get()));
    g_assert_cmpuint(AccessibilityTest::stateSetSize(stateSet.get()), ==, 4);
    g_assert_true(atspi_state_set_contains(stateSet.get(), ATSPI_STATE_ENABLED));
    g_assert_true(atspi_state_set_contains(stateSet.get(), ATSPI_STATE_SENSITIVE));
    g_assert_true(atspi_state_set_contains(stateSet.get(), ATSPI_STATE_VISIBLE));
    g_assert_true(atspi_state_set_contains(stateSet.get(), ATSPI_STATE_SHOWING));

    auto section = adoptGRef(atspi_accessible_get_child_at_index(documentWeb.get(), 1, nullptr));
    g_assert_true(ATSPI_IS_ACCESSIBLE(section.get()));
    g_assert_cmpint(atspi_accessible_get_child_count(section.get(), nullptr), ==, 11);

    unsigned nextChild = 0;
    auto button = adoptGRef(atspi_accessible_get_child_at_index(section.get(), nextChild++, nullptr));
    g_assert_true(ATSPI_IS_ACCESSIBLE(button.get()));
    stateSet = adoptGRef(atspi_accessible_get_state_set(button.get()));
    g_assert_cmpuint(AccessibilityTest::stateSetSize(stateSet.get()), ==, 5);
    g_assert_true(atspi_state_set_contains(stateSet.get(), ATSPI_STATE_ENABLED));
    g_assert_true(atspi_state_set_contains(stateSet.get(), ATSPI_STATE_SENSITIVE));
    g_assert_true(atspi_state_set_contains(stateSet.get(), ATSPI_STATE_VISIBLE));
    g_assert_true(atspi_state_set_contains(stateSet.get(), ATSPI_STATE_SHOWING));
    g_assert_true(atspi_state_set_contains(stateSet.get(), ATSPI_STATE_FOCUSABLE));

    auto buttonDisabled = adoptGRef(atspi_accessible_get_child_at_index(section.get(), nextChild++, nullptr));
    g_assert_true(ATSPI_IS_ACCESSIBLE(buttonDisabled.get()));
    stateSet = adoptGRef(atspi_accessible_get_state_set(buttonDisabled.get()));
    g_assert_cmpuint(AccessibilityTest::stateSetSize(stateSet.get()), ==, 2);
    g_assert_true(atspi_state_set_contains(stateSet.get(), ATSPI_STATE_VISIBLE));
    g_assert_true(atspi_state_set_contains(stateSet.get(), ATSPI_STATE_SHOWING));

    auto toggleButton = adoptGRef(atspi_accessible_get_child_at_index(section.get(), nextChild++, nullptr));
    g_assert_true(ATSPI_IS_ACCESSIBLE(toggleButton.get()));
    stateSet = adoptGRef(atspi_accessible_get_state_set(toggleButton.get()));
    g_assert_cmpuint(AccessibilityTest::stateSetSize(stateSet.get()), ==, 6);
    g_assert_true(atspi_state_set_contains(stateSet.get(), ATSPI_STATE_ENABLED));
    g_assert_true(atspi_state_set_contains(stateSet.get(), ATSPI_STATE_SENSITIVE));
    g_assert_true(atspi_state_set_contains(stateSet.get(), ATSPI_STATE_VISIBLE));
    g_assert_true(atspi_state_set_contains(stateSet.get(), ATSPI_STATE_SHOWING));
    g_assert_true(atspi_state_set_contains(stateSet.get(), ATSPI_STATE_FOCUSABLE));
    g_assert_true(atspi_state_set_contains(stateSet.get(), ATSPI_STATE_PRESSED));

    auto radio = adoptGRef(atspi_accessible_get_child_at_index(section.get(), nextChild++, nullptr));
    g_assert_true(ATSPI_IS_ACCESSIBLE(radio.get()));
    stateSet = adoptGRef(atspi_accessible_get_state_set(radio.get()));
    g_assert_cmpuint(AccessibilityTest::stateSetSize(stateSet.get()), ==, 6);
    g_assert_true(atspi_state_set_contains(stateSet.get(), ATSPI_STATE_ENABLED));
    g_assert_true(atspi_state_set_contains(stateSet.get(), ATSPI_STATE_SENSITIVE));
    g_assert_true(atspi_state_set_contains(stateSet.get(), ATSPI_STATE_VISIBLE));
    g_assert_true(atspi_state_set_contains(stateSet.get(), ATSPI_STATE_SHOWING));
    g_assert_true(atspi_state_set_contains(stateSet.get(), ATSPI_STATE_FOCUSABLE));
    g_assert_true(atspi_state_set_contains(stateSet.get(), ATSPI_STATE_CHECKABLE));

    auto radioChecked = adoptGRef(atspi_accessible_get_child_at_index(section.get(), nextChild++, nullptr));
    g_assert_true(ATSPI_IS_ACCESSIBLE(radioChecked.get()));
    stateSet = adoptGRef(atspi_accessible_get_state_set(radioChecked.get()));
    g_assert_cmpuint(AccessibilityTest::stateSetSize(stateSet.get()), ==, 7);
    g_assert_true(atspi_state_set_contains(stateSet.get(), ATSPI_STATE_ENABLED));
    g_assert_true(atspi_state_set_contains(stateSet.get(), ATSPI_STATE_SENSITIVE));
    g_assert_true(atspi_state_set_contains(stateSet.get(), ATSPI_STATE_VISIBLE));
    g_assert_true(atspi_state_set_contains(stateSet.get(), ATSPI_STATE_SHOWING));
    g_assert_true(atspi_state_set_contains(stateSet.get(), ATSPI_STATE_FOCUSABLE));
    g_assert_true(atspi_state_set_contains(stateSet.get(), ATSPI_STATE_CHECKABLE));
    g_assert_true(atspi_state_set_contains(stateSet.get(), ATSPI_STATE_CHECKED));

    auto radioDisabled = adoptGRef(atspi_accessible_get_child_at_index(section.get(), nextChild++, nullptr));
    g_assert_true(ATSPI_IS_ACCESSIBLE(radioDisabled.get()));
    stateSet = adoptGRef(atspi_accessible_get_state_set(radioDisabled.get()));
    g_assert_cmpuint(AccessibilityTest::stateSetSize(stateSet.get()), ==, 3);
    g_assert_true(atspi_state_set_contains(stateSet.get(), ATSPI_STATE_VISIBLE));
    g_assert_true(atspi_state_set_contains(stateSet.get(), ATSPI_STATE_SHOWING));
    g_assert_true(atspi_state_set_contains(stateSet.get(), ATSPI_STATE_CHECKABLE));

    auto checkbox = adoptGRef(atspi_accessible_get_child_at_index(section.get(), nextChild++, nullptr));
    g_assert_true(ATSPI_IS_ACCESSIBLE(checkbox.get()));
    stateSet = adoptGRef(atspi_accessible_get_state_set(checkbox.get()));
    g_assert_cmpuint(AccessibilityTest::stateSetSize(stateSet.get()), ==, 4);
    g_assert_true(atspi_state_set_contains(stateSet.get(), ATSPI_STATE_VISIBLE));
    g_assert_true(atspi_state_set_contains(stateSet.get(), ATSPI_STATE_SHOWING));
    g_assert_true(atspi_state_set_contains(stateSet.get(), ATSPI_STATE_CHECKABLE));
    g_assert_true(atspi_state_set_contains(stateSet.get(), ATSPI_STATE_CHECKED));

    auto entry = adoptGRef(atspi_accessible_get_child_at_index(section.get(), nextChild++, nullptr));
    g_assert_true(ATSPI_IS_ACCESSIBLE(entry.get()));
    stateSet = adoptGRef(atspi_accessible_get_state_set(entry.get()));
    g_assert_cmpuint(AccessibilityTest::stateSetSize(stateSet.get()), ==, 9);
    g_assert_true(atspi_state_set_contains(stateSet.get(), ATSPI_STATE_ENABLED));
    g_assert_true(atspi_state_set_contains(stateSet.get(), ATSPI_STATE_SENSITIVE));
    g_assert_true(atspi_state_set_contains(stateSet.get(), ATSPI_STATE_VISIBLE));
    g_assert_true(atspi_state_set_contains(stateSet.get(), ATSPI_STATE_SHOWING));
    g_assert_true(atspi_state_set_contains(stateSet.get(), ATSPI_STATE_FOCUSABLE));
    g_assert_true(atspi_state_set_contains(stateSet.get(), ATSPI_STATE_EDITABLE));
    g_assert_true(atspi_state_set_contains(stateSet.get(), ATSPI_STATE_SINGLE_LINE));
    g_assert_true(atspi_state_set_contains(stateSet.get(), ATSPI_STATE_SELECTABLE_TEXT));
    g_assert_true(atspi_state_set_contains(stateSet.get(), ATSPI_STATE_REQUIRED));

    auto entryDisabled = adoptGRef(atspi_accessible_get_child_at_index(section.get(), nextChild++, nullptr));
    g_assert_true(ATSPI_IS_ACCESSIBLE(entryDisabled.get()));
    stateSet = adoptGRef(atspi_accessible_get_state_set(entryDisabled.get()));
    g_assert_cmpuint(AccessibilityTest::stateSetSize(stateSet.get()), ==, 5);
    g_assert_true(atspi_state_set_contains(stateSet.get(), ATSPI_STATE_VISIBLE));
    g_assert_true(atspi_state_set_contains(stateSet.get(), ATSPI_STATE_SHOWING));
    g_assert_true(atspi_state_set_contains(stateSet.get(), ATSPI_STATE_EDITABLE));
    g_assert_true(atspi_state_set_contains(stateSet.get(), ATSPI_STATE_SINGLE_LINE));
    g_assert_true(atspi_state_set_contains(stateSet.get(), ATSPI_STATE_SELECTABLE_TEXT));

    auto entryReadOnly = adoptGRef(atspi_accessible_get_child_at_index(section.get(), nextChild++, nullptr));
    g_assert_true(ATSPI_IS_ACCESSIBLE(entryReadOnly.get()));
    stateSet = adoptGRef(atspi_accessible_get_state_set(entryReadOnly.get()));
    g_assert_cmpuint(AccessibilityTest::stateSetSize(stateSet.get()), ==, 8);
    g_assert_true(atspi_state_set_contains(stateSet.get(), ATSPI_STATE_ENABLED));
    g_assert_true(atspi_state_set_contains(stateSet.get(), ATSPI_STATE_SENSITIVE));
    g_assert_true(atspi_state_set_contains(stateSet.get(), ATSPI_STATE_VISIBLE));
    g_assert_true(atspi_state_set_contains(stateSet.get(), ATSPI_STATE_SHOWING));
    g_assert_true(atspi_state_set_contains(stateSet.get(), ATSPI_STATE_FOCUSABLE));
    g_assert_true(atspi_state_set_contains(stateSet.get(), ATSPI_STATE_SINGLE_LINE));
    g_assert_true(atspi_state_set_contains(stateSet.get(), ATSPI_STATE_SELECTABLE_TEXT));
    g_assert_true(atspi_state_set_contains(stateSet.get(), ATSPI_STATE_READ_ONLY));

    auto textArea = adoptGRef(atspi_accessible_get_child_at_index(section.get(), nextChild++, nullptr));
    g_assert_true(ATSPI_IS_ACCESSIBLE(textArea.get()));
    stateSet = adoptGRef(atspi_accessible_get_state_set(textArea.get()));
    g_assert_cmpuint(AccessibilityTest::stateSetSize(stateSet.get()), ==, 9);
    g_assert_true(atspi_state_set_contains(stateSet.get(), ATSPI_STATE_ENABLED));
    g_assert_true(atspi_state_set_contains(stateSet.get(), ATSPI_STATE_SENSITIVE));
    g_assert_true(atspi_state_set_contains(stateSet.get(), ATSPI_STATE_VISIBLE));
    g_assert_true(atspi_state_set_contains(stateSet.get(), ATSPI_STATE_SHOWING));
    g_assert_true(atspi_state_set_contains(stateSet.get(), ATSPI_STATE_FOCUSABLE));
    g_assert_true(atspi_state_set_contains(stateSet.get(), ATSPI_STATE_FOCUSED));
    g_assert_true(atspi_state_set_contains(stateSet.get(), ATSPI_STATE_EDITABLE));
    g_assert_true(atspi_state_set_contains(stateSet.get(), ATSPI_STATE_MULTI_LINE));
    g_assert_true(atspi_state_set_contains(stateSet.get(), ATSPI_STATE_SELECTABLE_TEXT));

    auto ul = adoptGRef(atspi_accessible_get_child_at_index(documentWeb.get(), 2, nullptr));
    g_assert_true(ATSPI_IS_ACCESSIBLE(textArea.get()));
    auto li = adoptGRef(atspi_accessible_get_child_at_index(ul.get(), 0, nullptr));
    stateSet = adoptGRef(atspi_accessible_get_state_set(li.get()));
    g_assert_cmpuint(AccessibilityTest::stateSetSize(stateSet.get()), ==, 5);
    g_assert_true(atspi_state_set_contains(stateSet.get(), ATSPI_STATE_ACTIVE));
    g_assert_true(atspi_state_set_contains(stateSet.get(), ATSPI_STATE_ENABLED));
    g_assert_true(atspi_state_set_contains(stateSet.get(), ATSPI_STATE_SENSITIVE));
    g_assert_true(atspi_state_set_contains(stateSet.get(), ATSPI_STATE_VISIBLE));
    g_assert_true(atspi_state_set_contains(stateSet.get(), ATSPI_STATE_SHOWING));
}

static void testAccessibleStateChanged(AccessibilityTest* test, gconstpointer)
{
    test->showInWindow(800, 600);
    test->loadHtml(
        "<html>"
        "  <body>"
        "    <input id='check' type='checkbox'/>Checkbox"
        "    <button id='toggle' aria-pressed='true'>Toggle button</button>"
        "    <input id='entry' value='Entry'>"
        "    <select id='list' size='2'><option>1</option><option>2</option></select>"
        "    <select id='combo'><option>1</option><option>2</option></select>"
        "  </body>"
        "</html>",
        nullptr);
    test->waitUntilLoadFinished();

    auto testApp = test->findTestApplication();
    g_assert_true(ATSPI_IS_ACCESSIBLE(testApp.get()));

    auto documentWeb = test->findDocumentWeb(testApp.get());
    g_assert_true(ATSPI_IS_ACCESSIBLE(documentWeb.get()));
    g_assert_cmpint(atspi_accessible_get_child_count(documentWeb.get(), nullptr), ==, 1);

    auto section = adoptGRef(atspi_accessible_get_child_at_index(documentWeb.get(), 0, nullptr));
    g_assert_true(ATSPI_IS_ACCESSIBLE(section.get()));
    g_assert_cmpint(atspi_accessible_get_child_count(section.get(), nullptr), ==, 5);

    unsigned nextChild = 0;
    auto checkbox = adoptGRef(atspi_accessible_get_child_at_index(section.get(), nextChild++, nullptr));
    g_assert_true(ATSPI_IS_ACCESSIBLE(checkbox.get()));
    test->startEventMonitor(checkbox.get(), { "object:state-changed" });
    test->runJavaScriptAndWaitUntilFinished("document.getElementById('check').checked = true;", nullptr);
    auto events = test->stopEventMonitor(1);
    g_assert_cmpuint(events.size(), ==, 1);
    g_assert_cmpstr(events[0]->type, ==, "object:state-changed:checked");
    g_assert_cmpuint(events[0]->detail1, ==, 1);
    events = { };

    auto toggleButton = adoptGRef(atspi_accessible_get_child_at_index(section.get(), nextChild++, nullptr));
    g_assert_true(ATSPI_IS_ACCESSIBLE(toggleButton.get()));
    test->startEventMonitor(toggleButton.get(), { "object:state-changed" });
    test->runJavaScriptAndWaitUntilFinished("document.getElementById('toggle').ariaPressed = false;", nullptr);
    events = test->stopEventMonitor(1);
    g_assert_cmpuint(events.size(), ==, 1);
    g_assert_cmpstr(events[0]->type, ==, "object:state-changed:pressed");
    g_assert_cmpuint(events[0]->detail1, ==, 0);
    events = { };

    auto entry = adoptGRef(atspi_accessible_get_child_at_index(section.get(), nextChild++, nullptr));
    g_assert_true(ATSPI_IS_ACCESSIBLE(entry.get()));
    test->startEventMonitor(entry.get(), { "object:state-changed" });
    test->runJavaScriptAndWaitUntilFinished("let e = document.getElementById('entry'); e.ariaRequired = true; e.focus();", nullptr);
    events = test->stopEventMonitor(2);
    g_assert_cmpuint(events.size(), ==, 2);
    if (!g_strcmp0(events[0]->type, "object:state-changed:focused")) {
        g_assert_cmpuint(events[0]->detail1, ==, 1);

        g_assert_cmpstr(events[1]->type, ==, "object:state-changed:required");
        g_assert_cmpuint(events[1]->detail1, ==, 1);
    } else if (!g_strcmp0(events[0]->type, "object:state-changed:required")) {
        g_assert_cmpuint(events[0]->detail1, ==, 1);

        g_assert_cmpstr(events[1]->type, ==, "object:state-changed:focused");
        g_assert_cmpuint(events[1]->detail1, ==, 1);
    } else
        g_assert_not_reached();
    events = { };

    test->runJavaScriptAndWaitUntilFinished("document.getElementById('list').focus();", nullptr);
    auto listBox = adoptGRef(atspi_accessible_get_child_at_index(section.get(), nextChild++, nullptr));
    g_assert_true(ATSPI_IS_ACCESSIBLE(listBox.get()));
    auto option1 = adoptGRef(atspi_accessible_get_child_at_index(listBox.get(), 0, nullptr));
    g_assert_true(ATSPI_IS_ACCESSIBLE(option1.get()));
    auto option2 = adoptGRef(atspi_accessible_get_child_at_index(listBox.get(), 1, nullptr));
    g_assert_true(ATSPI_IS_ACCESSIBLE(option2.get()));
    g_assert_false(AccessibilityTest::isSelected(option1.get()));
    g_assert_false(AccessibilityTest::isSelected(option2.get()));
    test->startEventMonitor(option1.get(), { "object:state-changed" });
    test->runJavaScriptAndWaitUntilFinished("document.getElementById('list').selectedIndex = 0;", nullptr);
    events = test->stopEventMonitor(1);
    g_assert_cmpuint(events.size(), ==, 1);
    g_assert_cmpstr(events[0]->type, ==, "object:state-changed:selected");
    g_assert_cmpuint(events[0]->detail1, ==, 1);
    events = { };
    g_assert_true(AccessibilityTest::isSelected(option1.get()));
    g_assert_false(AccessibilityTest::isSelected(option2.get()));
    test->startEventMonitor(option1.get(), { "object:state-changed" });
    test->runJavaScriptAndWaitUntilFinished("document.getElementById('list').selectedIndex = 1;", nullptr);
    events = test->stopEventMonitor(1);
    g_assert_cmpuint(events.size(), ==, 1);
    g_assert_cmpstr(events[0]->type, ==, "object:state-changed:selected");
    g_assert_cmpuint(events[0]->detail1, ==, 0);
    events = { };
    g_assert_false(AccessibilityTest::isSelected(option1.get()));
    g_assert_true(AccessibilityTest::isSelected(option2.get()));

    test->runJavaScriptAndWaitUntilFinished("document.getElementById('combo').focus();", nullptr);
    auto combo = adoptGRef(atspi_accessible_get_child_at_index(section.get(), nextChild++, nullptr));
    g_assert_true(ATSPI_IS_ACCESSIBLE(combo.get()));
    auto menuList = adoptGRef(atspi_accessible_get_child_at_index(combo.get(), 0, nullptr));
    g_assert_true(ATSPI_IS_ACCESSIBLE(menuList.get()));
    option1 = adoptGRef(atspi_accessible_get_child_at_index(menuList.get(), 0, nullptr));
    g_assert_true(ATSPI_IS_ACCESSIBLE(option1.get()));
    option2 = adoptGRef(atspi_accessible_get_child_at_index(menuList.get(), 1, nullptr));
    g_assert_true(ATSPI_IS_ACCESSIBLE(option2.get()));
    g_assert_true(AccessibilityTest::isSelected(option1.get()));
    g_assert_false(AccessibilityTest::isSelected(option2.get()));
    test->startEventMonitor(option2.get(), { "object:state-changed" });
    test->runJavaScriptAndWaitUntilFinished("document.getElementById('combo').selectedIndex = 1;", nullptr);
    events = test->stopEventMonitor(1);
    g_assert_cmpuint(events.size(), ==, 1);
    g_assert_cmpstr(events[0]->type, ==, "object:state-changed:selected");
    g_assert_cmpuint(events[0]->detail1, ==, 1);
    events = { };
    g_assert_true(AccessibilityTest::isSelected(option2.get()));
    g_assert_false(AccessibilityTest::isSelected(option1.get()));
    test->startEventMonitor(option2.get(), { "object:state-changed" });
    test->runJavaScriptAndWaitUntilFinished("document.getElementById('combo').selectedIndex = 0", nullptr);
    events = test->stopEventMonitor(1);
    g_assert_cmpuint(events.size(), ==, 1);
    g_assert_cmpstr(events[0]->type, ==, "object:state-changed:selected");
    g_assert_cmpuint(events[0]->detail1, ==, 0);
    events = { };
    g_assert_true(AccessibilityTest::isSelected(option1.get()));
    g_assert_false(AccessibilityTest::isSelected(option2.get()));
}

static void testAccessibleEventListener(AccessibilityTest* test, gconstpointer)
{
    test->startEventMonitor(nullptr, { "object:state-changed:focused" });
    test->showInWindow();
    test->loadHtml(
        "<html>"
        "  <body>"
        "    <input id='entry' type='text'/>"
        "  </body>"
        "</html>",
        nullptr);
    test->waitUntilLoadFinished();

    test->runJavaScriptAndWaitUntilFinished("document.getElementById('entry').focus();", nullptr);

    auto events = test->stopEventMonitor(1);
    g_assert_cmpuint(events.size(), ==, 1);
    g_assert_cmpstr(events[0]->type, ==, "object:state-changed:focused");
    auto* entry = events[0]->source;
    g_assert_true(ATSPI_IS_ACCESSIBLE(entry));
    g_assert_cmpint(atspi_accessible_get_role(entry, nullptr), ==, ATSPI_ROLE_ENTRY);

    auto panel = adoptGRef(atspi_accessible_get_parent(entry, nullptr));
    g_assert_true(ATSPI_IS_ACCESSIBLE(panel.get()));
    g_assert_cmpint(atspi_accessible_get_role(panel.get(), nullptr), ==, ATSPI_ROLE_PANEL);

    auto documentWeb = adoptGRef(atspi_accessible_get_parent(panel.get(), nullptr));
    g_assert_true(ATSPI_IS_ACCESSIBLE(documentWeb.get()));
    g_assert_cmpint(atspi_accessible_get_role(documentWeb.get(), nullptr), ==, ATSPI_ROLE_DOCUMENT_WEB);

    auto scrollView = adoptGRef(atspi_accessible_get_parent(documentWeb.get(), nullptr));
    g_assert_true(ATSPI_IS_ACCESSIBLE(scrollView.get()));
    g_assert_cmpint(atspi_accessible_get_role(scrollView.get(), nullptr), ==, ATSPI_ROLE_SCROLL_PANE);

    auto rootObject = adoptGRef(atspi_accessible_get_parent(scrollView.get(), nullptr));
    g_assert_true(ATSPI_IS_ACCESSIBLE(rootObject.get()));
    g_assert_cmpint(atspi_accessible_get_role(rootObject.get(), nullptr), ==, ATSPI_ROLE_FILLER);
}

static void testAccessibleListMarkers(AccessibilityTest* test, gconstpointer)
{
    GUniquePtr<char> baseDir(g_strdup_printf("file://%s/", Test::getResourcesDir().data()));
    test->showInWindow(800, 600);
    test->loadHtml(
        "<html>"
        "  <head>"
        "    <style>"
        "      .img { list-style-image: url(blank.ico) }"
        "    </style>"
        "  </head>"
        "  <body>"
        "    <ol>"
        "      <li>List item 1</li>"
        "    </ol>"
        "    <ul class='img'>"
        "      <li>List item 1</li>"
        "    </ul>"
        "  </body>"
        "</html>",
        baseDir.get());
    test->waitUntilLoadFinished();

    auto testApp = test->findTestApplication();
    g_assert_true(ATSPI_IS_ACCESSIBLE(testApp.get()));

    auto documentWeb = test->findDocumentWeb(testApp.get());
    g_assert_true(ATSPI_IS_ACCESSIBLE(documentWeb.get()));
    g_assert_cmpint(atspi_accessible_get_child_count(documentWeb.get(), nullptr), ==, 2);

    auto ol = adoptGRef(atspi_accessible_get_child_at_index(documentWeb.get(), 0, nullptr));
    g_assert_true(ATSPI_IS_ACCESSIBLE(ol.get()));
    g_assert_cmpint(atspi_accessible_get_role(ol.get(), nullptr), ==, ATSPI_ROLE_LIST);
    g_assert_cmpint(atspi_accessible_get_child_count(ol.get(), nullptr), ==, 1);

    auto li = adoptGRef(atspi_accessible_get_child_at_index(ol.get(), 0, nullptr));
    g_assert_true(ATSPI_IS_ACCESSIBLE(li.get()));
    g_assert_cmpint(atspi_accessible_get_role(li.get(), nullptr), ==, ATSPI_ROLE_LIST_ITEM);
    g_assert_cmpint(atspi_accessible_get_child_count(li.get(), nullptr), ==, 1);
    auto marker = adoptGRef(atspi_accessible_get_child_at_index(li.get(), 0, nullptr));
    g_assert_true(ATSPI_IS_ACCESSIBLE(marker.get()));
    g_assert_cmpint(atspi_accessible_get_role(marker.get(), nullptr), ==, ATSPI_ROLE_TEXT);
    GUniquePtr<char> name(atspi_accessible_get_role_name(marker.get(), nullptr));
    g_assert_cmpstr(name.get(), ==, "text");
    name.reset(atspi_accessible_get_localized_role_name(marker.get(), nullptr));
    g_assert_cmpstr(name.get(), ==, "text");
    GRefPtr<AtspiText> text = adoptGRef(atspi_accessible_get_text_iface(marker.get()));
    g_assert_nonnull(text.get());

    auto ul = adoptGRef(atspi_accessible_get_child_at_index(documentWeb.get(), 1, nullptr));
    g_assert_true(ATSPI_IS_ACCESSIBLE(ul.get()));
    g_assert_cmpint(atspi_accessible_get_role(ul.get(), nullptr), ==, ATSPI_ROLE_LIST);
    g_assert_cmpint(atspi_accessible_get_child_count(ul.get(), nullptr), ==, 1);

    li = adoptGRef(atspi_accessible_get_child_at_index(ul.get(), 0, nullptr));
    g_assert_true(ATSPI_IS_ACCESSIBLE(li.get()));
    g_assert_cmpint(atspi_accessible_get_role(li.get(), nullptr), ==, ATSPI_ROLE_LIST_ITEM);
    g_assert_cmpint(atspi_accessible_get_child_count(li.get(), nullptr), ==, 1);
    marker = adoptGRef(atspi_accessible_get_child_at_index(li.get(), 0, nullptr));
    g_assert_true(ATSPI_IS_ACCESSIBLE(marker.get()));
    g_assert_cmpint(atspi_accessible_get_role(marker.get(), nullptr), ==, ATSPI_ROLE_IMAGE);
    name.reset(atspi_accessible_get_role_name(marker.get(), nullptr));
    g_assert_cmpstr(name.get(), ==, "image");
    name.reset(atspi_accessible_get_localized_role_name(marker.get(), nullptr));
    g_assert_cmpstr(name.get(), ==, "image");
    GRefPtr<AtspiImage> image = adoptGRef(atspi_accessible_get_image_iface(marker.get()));
    g_assert_nonnull(image.get());
}

static void testComponentHitTest(AccessibilityTest* test, gconstpointer)
{
    test->showInWindow();
    GUniquePtr<char> baseDir(g_strdup_printf("file://%s/", Test::getResourcesDir().data()));
    test->loadHtml(
        "<html>"
        "  <body>"
        "    <img style='position:absolute; left:1; top:1' src='blank.ico' width=5 height=5></img>"
        "  </body>"
        "</html>",
        baseDir.get());
    test->waitUntilLoadFinished();

    auto testApp = test->findTestApplication();
    g_assert_true(ATSPI_IS_ACCESSIBLE(testApp.get()));

    auto documentWeb = test->findDocumentWeb(testApp.get());
    g_assert_true(ATSPI_IS_ACCESSIBLE(documentWeb.get()));
    g_assert_cmpint(atspi_accessible_get_child_count(documentWeb.get(), nullptr), ==, 1);

    auto img = adoptGRef(atspi_accessible_get_child_at_index(documentWeb.get(), 0, nullptr));
    g_assert_true(ATSPI_IS_COMPONENT(img.get()));
    GUniquePtr<AtspiRect> rect(atspi_component_get_extents(ATSPI_COMPONENT(img.get()), ATSPI_COORD_TYPE_WINDOW, nullptr));
    g_assert_nonnull(rect.get());
    g_assert_cmpuint(rect->x, ==, 1);
    g_assert_cmpuint(rect->y, ==, 1);
    g_assert_cmpuint(rect->width, ==, 5);
    g_assert_cmpuint(rect->height, ==, 5);
    GUniquePtr<AtspiPoint> point(atspi_component_get_position(ATSPI_COMPONENT(img.get()), ATSPI_COORD_TYPE_WINDOW, nullptr));
    g_assert_nonnull(point.get());
    g_assert_cmpuint(rect->x, ==, point->x);
    g_assert_cmpuint(rect->y, ==, point->y);
    GUniquePtr<AtspiPoint> size(atspi_component_get_size(ATSPI_COMPONENT(img.get()), nullptr));
    g_assert_nonnull(size.get());
    g_assert_cmpuint(size->x, ==, rect->width);
    g_assert_cmpuint(size->y, ==, rect->height);
    g_assert_true(atspi_component_contains(ATSPI_COMPONENT(img.get()), rect->x, rect->y, ATSPI_COORD_TYPE_WINDOW, nullptr));
    g_assert_false(atspi_component_contains(ATSPI_COMPONENT(img.get()), rect->x + rect->width, rect->y + rect->height, ATSPI_COORD_TYPE_WINDOW, nullptr));
    auto accessible = adoptGRef(atspi_component_get_accessible_at_point(ATSPI_COMPONENT(documentWeb.get()), rect->x, rect->y, ATSPI_COORD_TYPE_WINDOW, nullptr));
    g_assert_true(accessible.get() == img.get());
    accessible = adoptGRef(atspi_component_get_accessible_at_point(ATSPI_COMPONENT(documentWeb.get()), rect->x + rect->width, rect->y + rect->height, ATSPI_COORD_TYPE_WINDOW, nullptr));
    g_assert_true(accessible.get() == documentWeb.get());
}

#ifdef ATSPI_SCROLLTYPE_COUNT
static void testComponentScrollTo(AccessibilityTest* test, gconstpointer)
{
    test->showInWindow(640, 480);
    GUniquePtr<char> baseDir(g_strdup_printf("file://%s/", Test::getResourcesDir().data()));
    test->loadHtml(
        "<html>"
        "  <body>"
        "    <p>Top</p>"
        "    <img src='blank.ico' width=200 height=600></img>"
        "    <p>Bottom</p>"
        "  </body>"
        "</html>",
        baseDir.get());
    test->waitUntilLoadFinished();

    auto testApp = test->findTestApplication();
    g_assert_true(ATSPI_IS_ACCESSIBLE(testApp.get()));

    auto documentWeb = test->findDocumentWeb(testApp.get());
    g_assert_true(ATSPI_IS_ACCESSIBLE(documentWeb.get()));
    g_assert_cmpint(atspi_accessible_get_child_count(documentWeb.get(), nullptr), ==, 3);

    auto top = adoptGRef(atspi_accessible_get_child_at_index(documentWeb.get(), 0, nullptr));
    g_assert_true(ATSPI_IS_COMPONENT(top.get()));
    GUniquePtr<AtspiPoint> topPositionBeforeScrolling(atspi_component_get_position(ATSPI_COMPONENT(top.get()), ATSPI_COORD_TYPE_WINDOW, nullptr));
    g_assert_cmpint(topPositionBeforeScrolling->y, >, 0);
    g_assert_cmpint(topPositionBeforeScrolling->y, <, 480);

    auto bottom = adoptGRef(atspi_accessible_get_child_at_index(documentWeb.get(), 2, nullptr));
    g_assert_true(ATSPI_IS_COMPONENT(bottom.get()));
    GUniquePtr<AtspiPoint> bottomPositionBeforeScrolling(atspi_component_get_position(ATSPI_COMPONENT(bottom.get()), ATSPI_COORD_TYPE_WINDOW, nullptr));
    g_assert_cmpint(bottomPositionBeforeScrolling->y, >, 480);

    atspi_component_scroll_to(ATSPI_COMPONENT(bottom.get()), ATSPI_SCROLL_ANYWHERE, nullptr);

    GUniquePtr<AtspiPoint> topPositionAfterScrolling(atspi_component_get_position(ATSPI_COMPONENT(top.get()), ATSPI_COORD_TYPE_WINDOW, nullptr));
    g_assert_cmpint(topPositionAfterScrolling->y, <, 0);
    GUniquePtr<AtspiPoint> bottomPositionAfterScrolling(atspi_component_get_position(ATSPI_COMPONENT(bottom.get()), ATSPI_COORD_TYPE_WINDOW, nullptr));
    g_assert_cmpint(bottomPositionAfterScrolling->y, <, 480);

    atspi_component_scroll_to_point(ATSPI_COMPONENT(top.get()), ATSPI_COORD_TYPE_WINDOW, topPositionBeforeScrolling->x, topPositionBeforeScrolling->y, nullptr);
    topPositionAfterScrolling.reset(atspi_component_get_position(ATSPI_COMPONENT(top.get()), ATSPI_COORD_TYPE_WINDOW, nullptr));
    g_assert_cmpint(topPositionBeforeScrolling->x, ==, topPositionAfterScrolling->x);
    g_assert_cmpint(topPositionBeforeScrolling->y, ==, topPositionAfterScrolling->y);
}
#endif

static void testTextBasic(AccessibilityTest* test, gconstpointer)
{
    test->showInWindow();
    test->loadHtml(
        "<html>"
        "  <body>"
        "    <p>This is a line of text</p>"
        "  </body>"
        "</html>",
        nullptr);
    test->waitUntilLoadFinished();

    auto testApp = test->findTestApplication();
    g_assert_true(ATSPI_IS_ACCESSIBLE(testApp.get()));

    auto documentWeb = test->findDocumentWeb(testApp.get());
    g_assert_true(ATSPI_IS_ACCESSIBLE(documentWeb.get()));
    g_assert_cmpint(atspi_accessible_get_child_count(documentWeb.get(), nullptr), ==, 1);

    auto p = adoptGRef(atspi_accessible_get_child_at_index(documentWeb.get(), 0, nullptr));
    g_assert_true(ATSPI_IS_TEXT(p.get()));
    auto length = atspi_text_get_character_count(ATSPI_TEXT(p.get()), nullptr);
    g_assert_cmpint(length, ==, 22);
    GUniquePtr<char> text(atspi_text_get_text(ATSPI_TEXT(p.get()), 0, length, nullptr));
    g_assert_cmpstr(text.get(), ==, "This is a line of text");
    text.reset(atspi_text_get_text(ATSPI_TEXT(p.get()), 0, -1, nullptr));
    g_assert_cmpstr(text.get(), ==, "This is a line of text");
    text.reset(atspi_text_get_text(ATSPI_TEXT(p.get()), 5, 7, nullptr));
    g_assert_cmpstr(text.get(), ==, "is");
    text.reset(atspi_text_get_text(ATSPI_TEXT(p.get()), 0, -2, nullptr));
    g_assert_cmpstr(text.get(), ==, "");
    text.reset(atspi_text_get_text(ATSPI_TEXT(p.get()), -5, 23, nullptr));
    g_assert_cmpstr(text.get(), ==, "");
}

static void testTextSurrogatePair(AccessibilityTest* test, gconstpointer)
{
    test->showInWindow();
    test->loadHtml(
        "<html>"
        "  <body>"
        "    <p>This contains a &#x1D306; symbol</p>"
        "    <input value='This contains a &#x1D306; symbol'/>"
        "  </body>"
        "</html>",
        nullptr);
    test->waitUntilLoadFinished();

    auto testApp = test->findTestApplication();
    g_assert_true(ATSPI_IS_ACCESSIBLE(testApp.get()));

    auto documentWeb = test->findDocumentWeb(testApp.get());
    g_assert_true(ATSPI_IS_ACCESSIBLE(documentWeb.get()));
    g_assert_cmpint(atspi_accessible_get_child_count(documentWeb.get(), nullptr), ==, 2);

    auto p = adoptGRef(atspi_accessible_get_child_at_index(documentWeb.get(), 0, nullptr));
    g_assert_true(ATSPI_IS_TEXT(p.get()));
    auto length = atspi_text_get_character_count(ATSPI_TEXT(p.get()), nullptr);
    g_assert_cmpint(length, ==, 24);
    GUniquePtr<char> text(atspi_text_get_text(ATSPI_TEXT(p.get()), 0, length, nullptr));
    g_assert_cmpstr(text.get(), ==, "This contains a 𝌆 symbol");
    text.reset(atspi_text_get_text(ATSPI_TEXT(p.get()), 16, 17, nullptr));
    g_assert_cmpstr(text.get(), ==, "𝌆");

    auto section = adoptGRef(atspi_accessible_get_child_at_index(documentWeb.get(), 1, nullptr));
    g_assert_true(ATSPI_IS_ACCESSIBLE(section.get()));
    g_assert_cmpint(atspi_accessible_get_child_count(section.get(), nullptr), ==, 1);
    auto input = adoptGRef(atspi_accessible_get_child_at_index(section.get(), 0, nullptr));
    g_assert_true(ATSPI_IS_TEXT(input.get()));
    length = atspi_text_get_character_count(ATSPI_TEXT(input.get()), nullptr);
    g_assert_cmpint(length, ==, 24);
    text.reset(atspi_text_get_text(ATSPI_TEXT(input.get()), 0, length, nullptr));
    g_assert_cmpstr(text.get(), ==, "This contains a 𝌆 symbol");
    text.reset(atspi_text_get_text(ATSPI_TEXT(input.get()), 16, 17, nullptr));
    g_assert_cmpstr(text.get(), ==, "𝌆");
}

static void testTextIterator(AccessibilityTest* test, gconstpointer)
{
    test->showInWindow(800, 600);
    test->loadHtml(
        "<html>"
        "  <body>"
        "    <p>Text of first sentence.   This is the second sentence.<br>And this is the next paragraph.</p>"
        "  </body>"
        "</html>",
        nullptr);
    test->waitUntilLoadFinished();

    auto testApp = test->findTestApplication();
    g_assert_true(ATSPI_IS_ACCESSIBLE(testApp.get()));

    auto documentWeb = test->findDocumentWeb(testApp.get());
    g_assert_true(ATSPI_IS_ACCESSIBLE(documentWeb.get()));
    g_assert_cmpint(atspi_accessible_get_child_count(documentWeb.get(), nullptr), ==, 1);

    auto p = adoptGRef(atspi_accessible_get_child_at_index(documentWeb.get(), 0, nullptr));
    g_assert_true(ATSPI_IS_TEXT(p.get()));
    auto length = atspi_text_get_character_count(ATSPI_TEXT(p.get()), nullptr);
    g_assert_cmpint(length, ==, 84);
    GUniquePtr<char> text(atspi_text_get_text(ATSPI_TEXT(p.get()), 0, length, nullptr));
    g_assert_cmpstr(text.get(), ==, "Text of first sentence. This is the second sentence.\nAnd this is the next paragraph.");

    // Character granularity.
    UniqueAtspiTextRange range(atspi_text_get_string_at_offset(ATSPI_TEXT(p.get()), 0, ATSPI_TEXT_GRANULARITY_CHAR, nullptr));
    g_assert_cmpstr(range->content, ==, "T");
    g_assert_cmpint(range->start_offset, ==, 0);
    g_assert_cmpint(range->end_offset, ==, 1);
    range.reset(atspi_text_get_string_at_offset(ATSPI_TEXT(p.get()), 83, ATSPI_TEXT_GRANULARITY_CHAR, nullptr));
    g_assert_cmpstr(range->content, ==, ".");
    g_assert_cmpint(range->start_offset, ==, 83);
    g_assert_cmpint(range->end_offset, ==, 84);
    range.reset(atspi_text_get_string_at_offset(ATSPI_TEXT(p.get()), 84, ATSPI_TEXT_GRANULARITY_CHAR, nullptr));
    g_assert_cmpstr(range->content, ==, "");
    g_assert_cmpint(range->start_offset, ==, 84);
    g_assert_cmpint(range->end_offset, ==, 84);
    range.reset(atspi_text_get_string_at_offset(ATSPI_TEXT(p.get()), 85, ATSPI_TEXT_GRANULARITY_CHAR, nullptr));
    g_assert_cmpstr(range->content, ==, "");
    g_assert_cmpint(range->start_offset, ==, 0);
    g_assert_cmpint(range->end_offset, ==, 0);
    range.reset(atspi_text_get_string_at_offset(ATSPI_TEXT(p.get()), -1, ATSPI_TEXT_GRANULARITY_CHAR, nullptr));
    g_assert_cmpstr(range->content, ==, "");
    g_assert_cmpint(range->start_offset, ==, 0);
    g_assert_cmpint(range->end_offset, ==, 0);

    // Word granularity.
    range.reset(atspi_text_get_string_at_offset(ATSPI_TEXT(p.get()), 0, ATSPI_TEXT_GRANULARITY_WORD, nullptr));
    g_assert_cmpstr(range->content, ==, "Text ");
    g_assert_cmpint(range->start_offset, ==, 0);
    g_assert_cmpint(range->end_offset, ==, 5);
    range.reset(atspi_text_get_string_at_offset(ATSPI_TEXT(p.get()), 4, ATSPI_TEXT_GRANULARITY_WORD, nullptr));
    g_assert_cmpstr(range->content, ==, "Text ");
    g_assert_cmpint(range->start_offset, ==, 0);
    g_assert_cmpint(range->end_offset, ==, 5);
    range.reset(atspi_text_get_string_at_offset(ATSPI_TEXT(p.get()), 5, ATSPI_TEXT_GRANULARITY_WORD, nullptr));
    g_assert_cmpstr(range->content, ==, "of ");
    g_assert_cmpint(range->start_offset, ==, 5);
    g_assert_cmpint(range->end_offset, ==, 8);
    range.reset(atspi_text_get_string_at_offset(ATSPI_TEXT(p.get()), 40, ATSPI_TEXT_GRANULARITY_WORD, nullptr));
    g_assert_cmpstr(range->content, ==, "second ");
    g_assert_cmpint(range->start_offset, ==, 36);
    g_assert_cmpint(range->end_offset, ==, 43);
    range.reset(atspi_text_get_string_at_offset(ATSPI_TEXT(p.get()), 16, ATSPI_TEXT_GRANULARITY_WORD, nullptr));
    g_assert_cmpstr(range->content, ==, "sentence. ");
    g_assert_cmpint(range->start_offset, ==, 14);
    g_assert_cmpint(range->end_offset, ==, 24);
    range.reset(atspi_text_get_string_at_offset(ATSPI_TEXT(p.get()), 74, ATSPI_TEXT_GRANULARITY_WORD, nullptr));
    g_assert_cmpstr(range->content, ==, "paragraph.");
    g_assert_cmpint(range->start_offset, ==, 74);
    g_assert_cmpint(range->end_offset, ==, 84);
    range.reset(atspi_text_get_string_at_offset(ATSPI_TEXT(p.get()), 83, ATSPI_TEXT_GRANULARITY_WORD, nullptr));
    g_assert_cmpstr(range->content, ==, "paragraph.");
    g_assert_cmpint(range->start_offset, ==, 74);
    g_assert_cmpint(range->end_offset, ==, 84);
    range.reset(atspi_text_get_string_at_offset(ATSPI_TEXT(p.get()), 80, ATSPI_TEXT_GRANULARITY_WORD, nullptr));
    g_assert_cmpstr(range->content, ==, "paragraph.");
    g_assert_cmpint(range->start_offset, ==, 74);
    g_assert_cmpint(range->end_offset, ==, 84);

    // Sentence granularity.
    range.reset(atspi_text_get_string_at_offset(ATSPI_TEXT(p.get()), 0, ATSPI_TEXT_GRANULARITY_SENTENCE, nullptr));
    g_assert_cmpstr(range->content, ==, "Text of first sentence. ");
    g_assert_cmpint(range->start_offset, ==, 0);
    g_assert_cmpint(range->end_offset, ==, 24);
    range.reset(atspi_text_get_string_at_offset(ATSPI_TEXT(p.get()), 23, ATSPI_TEXT_GRANULARITY_SENTENCE, nullptr));
    g_assert_cmpstr(range->content, ==, "Text of first sentence. ");
    g_assert_cmpint(range->start_offset, ==, 0);
    g_assert_cmpint(range->end_offset, ==, 24);
    range.reset(atspi_text_get_string_at_offset(ATSPI_TEXT(p.get()), 24, ATSPI_TEXT_GRANULARITY_SENTENCE, nullptr));
    g_assert_cmpstr(range->content, ==, "This is the second sentence.\n");
    g_assert_cmpint(range->start_offset, ==, 24);
    g_assert_cmpint(range->end_offset, ==, 53);
    range.reset(atspi_text_get_string_at_offset(ATSPI_TEXT(p.get()), 40, ATSPI_TEXT_GRANULARITY_SENTENCE, nullptr));
    g_assert_cmpstr(range->content, ==, "This is the second sentence.\n");
    g_assert_cmpint(range->start_offset, ==, 24);
    g_assert_cmpint(range->end_offset, ==, 53);
    range.reset(atspi_text_get_string_at_offset(ATSPI_TEXT(p.get()), 53, ATSPI_TEXT_GRANULARITY_SENTENCE, nullptr));
    g_assert_cmpstr(range->content, ==, "And this is the next paragraph.");
    g_assert_cmpint(range->start_offset, ==, 53);
    g_assert_cmpint(range->end_offset, ==, 84);
    range.reset(atspi_text_get_string_at_offset(ATSPI_TEXT(p.get()), 83, ATSPI_TEXT_GRANULARITY_SENTENCE, nullptr));
    g_assert_cmpstr(range->content, ==, "And this is the next paragraph.");
    g_assert_cmpint(range->start_offset, ==, 53);
    g_assert_cmpint(range->end_offset, ==, 84);

    // Line granularity.
    range.reset(atspi_text_get_string_at_offset(ATSPI_TEXT(p.get()), 0, ATSPI_TEXT_GRANULARITY_LINE, nullptr));
    g_assert_cmpstr(range->content, ==, "Text of first sentence. This is the second sentence.\n");
    g_assert_cmpint(range->start_offset, ==, 0);
    g_assert_cmpint(range->end_offset, ==, 53);
    range.reset(atspi_text_get_string_at_offset(ATSPI_TEXT(p.get()), 83, ATSPI_TEXT_GRANULARITY_LINE, nullptr));
    g_assert_cmpstr(range->content, ==, "And this is the next paragraph.");
    g_assert_cmpint(range->start_offset, ==, 53);
    g_assert_cmpint(range->end_offset, ==, 84);

    // Paragraph granularity.
    range.reset(atspi_text_get_string_at_offset(ATSPI_TEXT(p.get()), 0, ATSPI_TEXT_GRANULARITY_PARAGRAPH, nullptr));
    g_assert_cmpstr(range->content, ==, "Text of first sentence. This is the second sentence.");
    g_assert_cmpint(range->start_offset, ==, 0);
    g_assert_cmpint(range->end_offset, ==, 52);
    range.reset(atspi_text_get_string_at_offset(ATSPI_TEXT(p.get()), 83, ATSPI_TEXT_GRANULARITY_PARAGRAPH, nullptr));
    g_assert_cmpstr(range->content, ==, "And this is the next paragraph.");
    g_assert_cmpint(range->start_offset, ==, 53);
    g_assert_cmpint(range->end_offset, ==, 84);

    // Using a text control now.
    test->loadHtml(
        "<html>"
        "  <body>"
        "    <input value='Text of input field'/>"
        "  </body>"
        "</html>",
        nullptr);
    test->waitUntilLoadFinished();

    documentWeb = test->findDocumentWeb(testApp.get());
    g_assert_true(ATSPI_IS_ACCESSIBLE(documentWeb.get()));
    g_assert_cmpint(atspi_accessible_get_child_count(documentWeb.get(), nullptr), ==, 1);

    auto section = adoptGRef(atspi_accessible_get_child_at_index(documentWeb.get(), 0, nullptr));
    g_assert_true(ATSPI_IS_ACCESSIBLE(section.get()));
    g_assert_cmpint(atspi_accessible_get_child_count(section.get(), nullptr), ==, 1);
    auto input = adoptGRef(atspi_accessible_get_child_at_index(section.get(), 0, nullptr));
    g_assert_true(ATSPI_IS_TEXT(input.get()));
    length = atspi_text_get_character_count(ATSPI_TEXT(input.get()), nullptr);
    g_assert_cmpint(length, ==, 19);
    text.reset(atspi_text_get_text(ATSPI_TEXT(input.get()), 0, length, nullptr));
    g_assert_cmpstr(text.get(), ==, "Text of input field");

    range.reset(atspi_text_get_string_at_offset(ATSPI_TEXT(input.get()), 0, ATSPI_TEXT_GRANULARITY_CHAR, nullptr));
    g_assert_cmpstr(range->content, ==, "T");
    g_assert_cmpint(range->start_offset, ==, 0);
    g_assert_cmpint(range->end_offset, ==, 1);
    range.reset(atspi_text_get_string_at_offset(ATSPI_TEXT(input.get()), 18, ATSPI_TEXT_GRANULARITY_CHAR, nullptr));
    g_assert_cmpstr(range->content, ==, "d");
    g_assert_cmpint(range->start_offset, ==, 18);
    g_assert_cmpint(range->end_offset, ==, 19);

    range.reset(atspi_text_get_string_at_offset(ATSPI_TEXT(input.get()), 0, ATSPI_TEXT_GRANULARITY_WORD, nullptr));
    g_assert_cmpstr(range->content, ==, "Text ");
    g_assert_cmpint(range->start_offset, ==, 0);
    g_assert_cmpint(range->end_offset, ==, 5);
    range.reset(atspi_text_get_string_at_offset(ATSPI_TEXT(input.get()), 16, ATSPI_TEXT_GRANULARITY_WORD, nullptr));
    g_assert_cmpstr(range->content, ==, "field");
    g_assert_cmpint(range->start_offset, ==, 14);
    g_assert_cmpint(range->end_offset, ==, 19);

    // Password field now.
    test->loadHtml(
        "<html>"
        "  <body>"
        "    <input type='password' value='password value'/>"
        "  </body>"
        "</html>",
        nullptr);
    test->waitUntilLoadFinished();

    documentWeb = test->findDocumentWeb(testApp.get());
    g_assert_true(ATSPI_IS_ACCESSIBLE(documentWeb.get()));
    g_assert_cmpint(atspi_accessible_get_child_count(documentWeb.get(), nullptr), ==, 1);

    section = adoptGRef(atspi_accessible_get_child_at_index(documentWeb.get(), 0, nullptr));
    g_assert_true(ATSPI_IS_ACCESSIBLE(section.get()));
    g_assert_cmpint(atspi_accessible_get_child_count(section.get(), nullptr), ==, 1);
    input = adoptGRef(atspi_accessible_get_child_at_index(section.get(), 0, nullptr));
    g_assert_true(ATSPI_IS_TEXT(input.get()));
    length = atspi_text_get_character_count(ATSPI_TEXT(input.get()), nullptr);
    g_assert_cmpint(length, ==, 14);
    text.reset(atspi_text_get_text(ATSPI_TEXT(input.get()), 0, length, nullptr));
    g_assert_cmpstr(text.get(), ==, "••••••••••••••");

    range.reset(atspi_text_get_string_at_offset(ATSPI_TEXT(input.get()), 0, ATSPI_TEXT_GRANULARITY_CHAR, nullptr));
    g_assert_cmpstr(range->content, ==, "•");
    g_assert_cmpint(range->start_offset, ==, 0);
    g_assert_cmpint(range->end_offset, ==, 1);
    range.reset(atspi_text_get_string_at_offset(ATSPI_TEXT(input.get()), 13, ATSPI_TEXT_GRANULARITY_CHAR, nullptr));
    g_assert_cmpstr(range->content, ==, "•");
    g_assert_cmpint(range->start_offset, ==, 13);
    g_assert_cmpint(range->end_offset, ==, 14);

    range.reset(atspi_text_get_string_at_offset(ATSPI_TEXT(input.get()), 0, ATSPI_TEXT_GRANULARITY_WORD, nullptr));
    g_assert_cmpstr(range->content, ==, "••••••••••••••");
    g_assert_cmpint(range->start_offset, ==, 0);
    g_assert_cmpint(range->end_offset, ==, 14);
}

static void testTextExtents(AccessibilityTest* test, gconstpointer)
{
    test->showInWindow();
    test->loadHtml(
        "<html>"
        "  <body>"
        "    <p>Text</p>"
        "  </body>"
        "</html>",
        nullptr);
    test->waitUntilLoadFinished();

    auto testApp = test->findTestApplication();
    g_assert_true(ATSPI_IS_ACCESSIBLE(testApp.get()));

    auto documentWeb = test->findDocumentWeb(testApp.get());
    g_assert_true(ATSPI_IS_ACCESSIBLE(documentWeb.get()));
    g_assert_cmpint(atspi_accessible_get_child_count(documentWeb.get(), nullptr), ==, 1);

    auto p = adoptGRef(atspi_accessible_get_child_at_index(documentWeb.get(), 0, nullptr));
    g_assert_true(ATSPI_IS_TEXT(p.get()));
    GUniquePtr<AtspiRect> firstCharRect(atspi_text_get_character_extents(ATSPI_TEXT(p.get()), 0, ATSPI_COORD_TYPE_WINDOW, nullptr));
    g_assert_nonnull(firstCharRect.get());
    g_assert_cmpuint(firstCharRect->x, >, 0);
    g_assert_cmpuint(firstCharRect->y, >, 0);
    g_assert_cmpuint(firstCharRect->width, >, 0);
    g_assert_cmpuint(firstCharRect->height, >, 0);
    GUniquePtr<AtspiRect> lastCharRect(atspi_text_get_character_extents(ATSPI_TEXT(p.get()), 3, ATSPI_COORD_TYPE_WINDOW, nullptr));
    g_assert_nonnull(lastCharRect.get());
    g_assert_cmpuint(lastCharRect->x, >, firstCharRect->x);
    g_assert_cmpuint(lastCharRect->y, ==, firstCharRect->y);
    g_assert_cmpuint(lastCharRect->width, >, 0);
    g_assert_cmpuint(lastCharRect->height, >, 0);
    GUniquePtr<AtspiRect> rangeRect(atspi_text_get_range_extents(ATSPI_TEXT(p.get()), 0, 4, ATSPI_COORD_TYPE_WINDOW, nullptr));
    g_assert_nonnull(rangeRect.get());
    g_assert_cmpuint(rangeRect->x, ==, firstCharRect->x);
    g_assert_cmpuint(rangeRect->y, ==, firstCharRect->y);
    g_assert_cmpuint(rangeRect->width, >, 0);
    g_assert_cmpuint(rangeRect->height, >, 0);

    auto offset = atspi_text_get_offset_at_point(ATSPI_TEXT(p.get()), firstCharRect->x, firstCharRect->y, ATSPI_COORD_TYPE_WINDOW, nullptr);
    g_assert_cmpint(offset, ==, 0);
    offset = atspi_text_get_offset_at_point(ATSPI_TEXT(p.get()), firstCharRect->x + firstCharRect->width, firstCharRect->y, ATSPI_COORD_TYPE_WINDOW, nullptr);
    g_assert_cmpint(offset, ==, 1);
    offset = atspi_text_get_offset_at_point(ATSPI_TEXT(p.get()), lastCharRect->x, lastCharRect->y, ATSPI_COORD_TYPE_WINDOW, nullptr);
    g_assert_cmpint(offset, ==, 3);
    offset = atspi_text_get_offset_at_point(ATSPI_TEXT(p.get()), lastCharRect->x + lastCharRect->width, lastCharRect->y, ATSPI_COORD_TYPE_WINDOW, nullptr);
    g_assert_cmpint(offset, ==, 4);
}

static void testTextSelections(AccessibilityTest* test, gconstpointer)
{
    test->showInWindow(800, 600);
    test->loadHtml(
        "<html>"
        "  <body>"
        "    <p>This is a line of text</p>"
        "    <input value='This is text input value'/>"
        "    <input type='password' value='secret'/>"
        "  </body>"
        "</html>",
        nullptr);
    test->waitUntilLoadFinished();

    auto testApp = test->findTestApplication();
    g_assert_true(ATSPI_IS_ACCESSIBLE(testApp.get()));

    auto documentWeb = test->findDocumentWeb(testApp.get());
    g_assert_true(ATSPI_IS_ACCESSIBLE(documentWeb.get()));
    g_assert_cmpint(atspi_accessible_get_child_count(documentWeb.get(), nullptr), ==, 2);

    auto p = adoptGRef(atspi_accessible_get_child_at_index(documentWeb.get(), 0, nullptr));
    g_assert_true(ATSPI_IS_TEXT(p.get()));

    auto selectionCount = atspi_text_get_n_selections(ATSPI_TEXT(p.get()), nullptr);
    g_assert_cmpuint(selectionCount, ==, 0);
    auto caretOffset = atspi_text_get_caret_offset(ATSPI_TEXT(p.get()), nullptr);
    g_assert_cmpint(caretOffset, ==, -1);

    GUniquePtr<AtspiRange> selection(atspi_text_get_selection(ATSPI_TEXT(p.get()), 0, nullptr));
    g_assert_nonnull(selection.get());
    g_assert_cmpint(selection->start_offset, ==, 0);
    g_assert_cmpint(selection->end_offset, ==, 0);

    g_assert_true(atspi_text_set_selection(ATSPI_TEXT(p.get()), 0, 5, 14, nullptr));
    selectionCount = atspi_text_get_n_selections(ATSPI_TEXT(p.get()), nullptr);
    g_assert_cmpuint(selectionCount, ==, 1);
    caretOffset = atspi_text_get_caret_offset(ATSPI_TEXT(p.get()), nullptr);
    g_assert_cmpint(caretOffset, ==, 14);
    selection.reset(atspi_text_get_selection(ATSPI_TEXT(p.get()), 0, nullptr));
    g_assert_nonnull(selection.get());
    g_assert_cmpint(selection->start_offset, ==, 5);
    g_assert_cmpint(selection->end_offset, ==, 14);
    GUniquePtr<char> text(atspi_text_get_text(ATSPI_TEXT(p.get()), selection->start_offset, selection->end_offset, nullptr));
    g_assert_cmpstr(text.get(), ==, "is a line");

    g_assert_true(atspi_text_remove_selection(ATSPI_TEXT(p.get()), 0, nullptr));
    selectionCount = atspi_text_get_n_selections(ATSPI_TEXT(p.get()), nullptr);
    g_assert_cmpuint(selectionCount, ==, 0);
    caretOffset = atspi_text_get_caret_offset(ATSPI_TEXT(p.get()), nullptr);
    g_assert_cmpint(caretOffset, ==, 14);
    g_assert_true(atspi_text_set_caret_offset(ATSPI_TEXT(p.get()), 0, nullptr));
    caretOffset = atspi_text_get_caret_offset(ATSPI_TEXT(p.get()), nullptr);
    g_assert_cmpint(caretOffset, ==, 0);

    auto section = adoptGRef(atspi_accessible_get_child_at_index(documentWeb.get(), 1, nullptr));
    g_assert_true(ATSPI_IS_ACCESSIBLE(section.get()));
    g_assert_cmpint(atspi_accessible_get_child_count(section.get(), nullptr), ==, 2);
    auto input = adoptGRef(atspi_accessible_get_child_at_index(section.get(), 0, nullptr));
    g_assert_true(ATSPI_IS_TEXT(input.get()));
    g_assert_true(atspi_text_set_caret_offset(ATSPI_TEXT(input.get()), 5, nullptr));
    caretOffset = atspi_text_get_caret_offset(ATSPI_TEXT(input.get()), nullptr);
    g_assert_cmpint(caretOffset, ==, 5);
    g_assert_true(atspi_text_set_selection(ATSPI_TEXT(input.get()), 0, 5, 12, nullptr));
    selectionCount = atspi_text_get_n_selections(ATSPI_TEXT(input.get()), nullptr);
    g_assert_cmpuint(selectionCount, ==, 1);
    caretOffset = atspi_text_get_caret_offset(ATSPI_TEXT(input.get()), nullptr);
    g_assert_cmpint(caretOffset, ==, 12);
    selection.reset(atspi_text_get_selection(ATSPI_TEXT(input.get()), 0, nullptr));
    g_assert_nonnull(selection.get());
    g_assert_cmpint(selection->start_offset, ==, 5);
    g_assert_cmpint(selection->end_offset, ==, 12);
    text.reset(atspi_text_get_text(ATSPI_TEXT(input.get()), selection->start_offset, selection->end_offset, nullptr));
    g_assert_cmpstr(text.get(), ==, "is text");

    auto password = adoptGRef(atspi_accessible_get_child_at_index(section.get(), 1, nullptr));
    g_assert_true(ATSPI_IS_TEXT(password.get()));
    g_assert_true(atspi_text_set_caret_offset(ATSPI_TEXT(password.get()), 2, nullptr));
    caretOffset = atspi_text_get_caret_offset(ATSPI_TEXT(password.get()), nullptr);
    g_assert_cmpint(caretOffset, ==, 2);
    g_assert_true(atspi_text_set_selection(ATSPI_TEXT(password.get()), 0, 2, 5, nullptr));
    selectionCount = atspi_text_get_n_selections(ATSPI_TEXT(password.get()), nullptr);
    g_assert_cmpuint(selectionCount, ==, 1);
    caretOffset = atspi_text_get_caret_offset(ATSPI_TEXT(password.get()), nullptr);
    g_assert_cmpint(caretOffset, ==, 5);
    selection.reset(atspi_text_get_selection(ATSPI_TEXT(password.get()), 0, nullptr));
    g_assert_nonnull(selection.get());
    g_assert_cmpint(selection->start_offset, ==, 2);
    g_assert_cmpint(selection->end_offset, ==, 5);
    text.reset(atspi_text_get_text(ATSPI_TEXT(password.get()), selection->start_offset, selection->end_offset, nullptr));
    g_assert_cmpstr(text.get(), ==, "•••");
}

static void testTextAttributes(AccessibilityTest* test, gconstpointer)
{
    test->showInWindow(800, 600);
    test->loadHtml(
        "<html>"
        "  <body>"
        "    <p>This is a <b>line</b> of text</p>"
        "  </body>"
        "</html>",
        nullptr);
    test->waitUntilLoadFinished();

    auto testApp = test->findTestApplication();
    g_assert_true(ATSPI_IS_ACCESSIBLE(testApp.get()));

    auto documentWeb = test->findDocumentWeb(testApp.get());
    g_assert_true(ATSPI_IS_ACCESSIBLE(documentWeb.get()));
    g_assert_cmpint(atspi_accessible_get_child_count(documentWeb.get(), nullptr), ==, 1);

    auto p = adoptGRef(atspi_accessible_get_child_at_index(documentWeb.get(), 0, nullptr));
    g_assert_true(ATSPI_IS_TEXT(p.get()));

    // Including default attributes.
    int startOffset, endOffset;
    GRefPtr<GHashTable> attributes = adoptGRef(atspi_text_get_attribute_run(ATSPI_TEXT(p.get()), 0, TRUE, &startOffset, &endOffset, nullptr));
    g_assert_nonnull(attributes.get());
    g_assert_cmpuint(g_hash_table_size(attributes.get()), >, 1);
    g_assert_cmpstr(static_cast<const char*>(g_hash_table_lookup(attributes.get(), "weight")), ==, "400");
    g_assert_cmpint(startOffset, ==, 0);
    g_assert_cmpint(endOffset, ==, 10);

    attributes = adoptGRef(atspi_text_get_attribute_run(ATSPI_TEXT(p.get()), 12, TRUE, &startOffset, &endOffset, nullptr));
    g_assert_nonnull(attributes.get());
    g_assert_cmpuint(g_hash_table_size(attributes.get()), >, 1);
    g_assert_cmpstr(static_cast<const char*>(g_hash_table_lookup(attributes.get(), "weight")), ==, "700");
    GUniquePtr<char> value(atspi_text_get_attribute_value(ATSPI_TEXT(p.get()), 11, const_cast<char*>("weight"), nullptr));
    g_assert_cmpstr(value.get(), ==, "700");
    g_assert_cmpint(startOffset, ==, 10);
    g_assert_cmpint(endOffset, ==, 14);

    attributes = adoptGRef(atspi_text_get_attribute_run(ATSPI_TEXT(p.get()), 16, TRUE, &startOffset, &endOffset, nullptr));
    g_assert_nonnull(attributes.get());
    g_assert_cmpuint(g_hash_table_size(attributes.get()), >, 1);
    g_assert_cmpstr(static_cast<const char*>(g_hash_table_lookup(attributes.get(), "weight")), ==, "400");
    g_assert_cmpint(startOffset, ==, 14);
    g_assert_cmpint(endOffset, ==, 22);

    // Without default attributes.
    attributes = adoptGRef(atspi_text_get_attribute_run(ATSPI_TEXT(p.get()), 0, FALSE, &startOffset, &endOffset, nullptr));
    g_assert_nonnull(attributes.get());
    g_assert_cmpuint(g_hash_table_size(attributes.get()), ==, 0);
    g_assert_cmpint(startOffset, ==, 0);
    g_assert_cmpint(endOffset, ==, 10);

    attributes = adoptGRef(atspi_text_get_attribute_run(ATSPI_TEXT(p.get()), 12, FALSE, &startOffset, &endOffset, nullptr));
    g_assert_nonnull(attributes.get());
    g_assert_cmpuint(g_hash_table_size(attributes.get()), ==, 1);
    g_assert_cmpstr(static_cast<const char*>(g_hash_table_lookup(attributes.get(), "weight")), ==, "700");
    g_assert_cmpint(startOffset, ==, 10);
    g_assert_cmpint(endOffset, ==, 14);

    attributes = adoptGRef(atspi_text_get_attribute_run(ATSPI_TEXT(p.get()), 16, FALSE, &startOffset, &endOffset, nullptr));
    g_assert_nonnull(attributes.get());
    g_assert_cmpuint(g_hash_table_size(attributes.get()), ==, 0);
    g_assert_cmpint(startOffset, ==, 14);
    g_assert_cmpint(endOffset, ==, 22);

    // Only default attributes.
    attributes = adoptGRef(atspi_text_get_default_attributes(ATSPI_TEXT(p.get()), nullptr));
    g_assert_nonnull(attributes.get());
    g_assert_cmpuint(g_hash_table_size(attributes.get()), >, 1);
    g_assert_cmpstr(static_cast<const char*>(g_hash_table_lookup(attributes.get(), "weight")), ==, "400");

    // Atspi implementation handles ranges with the same attributes as one run.
    test->loadHtml(
        "<html>"
        "  <body>"
        "    <p>This is a <b>li</b><b>ne</b> of text</p>"
        "  </body>"
        "</html>",
        nullptr);
    test->waitUntilLoadFinished();

    documentWeb = test->findDocumentWeb(testApp.get());
    g_assert_true(ATSPI_IS_ACCESSIBLE(documentWeb.get()));
    g_assert_cmpint(atspi_accessible_get_child_count(documentWeb.get(), nullptr), ==, 1);

    p = adoptGRef(atspi_accessible_get_child_at_index(documentWeb.get(), 0, nullptr));
    g_assert_true(ATSPI_IS_TEXT(p.get()));

    attributes = adoptGRef(atspi_text_get_attribute_run(ATSPI_TEXT(p.get()), 0, TRUE, &startOffset, &endOffset, nullptr));
    g_assert_nonnull(attributes.get());
    g_assert_cmpuint(g_hash_table_size(attributes.get()), >, 1);
    g_assert_cmpstr(static_cast<const char*>(g_hash_table_lookup(attributes.get(), "weight")), ==, "400");
    g_assert_cmpint(startOffset, ==, 0);
    g_assert_cmpint(endOffset, ==, 10);

    attributes = adoptGRef(atspi_text_get_attribute_run(ATSPI_TEXT(p.get()), 12, TRUE, &startOffset, &endOffset, nullptr));
    g_assert_nonnull(attributes.get());
    g_assert_cmpuint(g_hash_table_size(attributes.get()), >, 1);
    g_assert_cmpstr(static_cast<const char*>(g_hash_table_lookup(attributes.get(), "weight")), ==, "700");
    g_assert_cmpint(startOffset, ==, 10);
    g_assert_cmpint(endOffset, ==, 14);

    attributes = adoptGRef(atspi_text_get_attribute_run(ATSPI_TEXT(p.get()), 16, TRUE, &startOffset, &endOffset, nullptr));
    g_assert_nonnull(attributes.get());
    g_assert_cmpuint(g_hash_table_size(attributes.get()), >, 1);
    g_assert_cmpstr(static_cast<const char*>(g_hash_table_lookup(attributes.get(), "weight")), ==, "400");
    g_assert_cmpint(startOffset, ==, 14);
    g_assert_cmpint(endOffset, ==, 22);
}

static void testTextStateChanged(AccessibilityTest* test, gconstpointer)
{
    test->showInWindow(800, 600);
    test->loadHtml(
        "<html>"
        "  <body>"
        "    <p>This is a line of text</p>"
        "    <input value='This is a text field'/>"
        "    <div contenteditable=true>This is content editable</div>"
        "    <input type='password' value='123'/>"
        "  </body>"
        "</html>",
        nullptr);
    test->waitUntilLoadFinished();

    auto testApp = test->findTestApplication();
    g_assert_true(ATSPI_IS_ACCESSIBLE(testApp.get()));

    auto documentWeb = test->findDocumentWeb(testApp.get());
    g_assert_true(ATSPI_IS_ACCESSIBLE(documentWeb.get()));
    g_assert_cmpint(atspi_accessible_get_child_count(documentWeb.get(), nullptr), ==, 4);

    // Text caret moved.
    auto p = adoptGRef(atspi_accessible_get_child_at_index(documentWeb.get(), 0, nullptr));
    g_assert_true(ATSPI_IS_TEXT(p.get()));
    test->startEventMonitor(p.get(), { "object:text-caret-moved" });
    g_assert_true(atspi_text_set_caret_offset(ATSPI_TEXT(p.get()), 10, nullptr));
    auto events = test->stopEventMonitor(1);
    g_assert_cmpuint(events.size(), ==, 1);
    g_assert_cmpstr(events[0]->type, ==, "object:text-caret-moved");
    g_assert_cmpuint(events[0]->detail1, ==, 10);

    auto section = adoptGRef(atspi_accessible_get_child_at_index(documentWeb.get(), 1, nullptr));
    g_assert_true(ATSPI_IS_ACCESSIBLE(section.get()));
    g_assert_cmpint(atspi_accessible_get_child_count(section.get(), nullptr), ==, 1);
    auto input = adoptGRef(atspi_accessible_get_child_at_index(section.get(), 0, nullptr));
    g_assert_true(ATSPI_IS_TEXT(input.get()));
    test->startEventMonitor(input.get(), { "object:text-caret-moved" });
    g_assert_true(atspi_text_set_caret_offset(ATSPI_TEXT(input.get()), 5, nullptr));
    events = test->stopEventMonitor(1);
    g_assert_cmpuint(events.size(), ==, 1);
    g_assert_cmpstr(events[0]->type, ==, "object:text-caret-moved");
    g_assert_cmpuint(events[0]->detail1, ==, 5);

    auto div = adoptGRef(atspi_accessible_get_child_at_index(documentWeb.get(), 2, nullptr));
    g_assert_true(ATSPI_IS_TEXT(div.get()));
    test->startEventMonitor(div.get(), { "object:text-caret-moved" });
    g_assert_true(atspi_text_set_caret_offset(ATSPI_TEXT(div.get()), 15, nullptr));
    events = test->stopEventMonitor(1);
    g_assert_cmpuint(events.size(), ==, 1);
    g_assert_cmpstr(events[0]->type, ==, "object:text-caret-moved");
    g_assert_cmpuint(events[0]->detail1, ==, 15);

    section = adoptGRef(atspi_accessible_get_child_at_index(documentWeb.get(), 3, nullptr));
    g_assert_true(ATSPI_IS_ACCESSIBLE(section.get()));
    g_assert_cmpint(atspi_accessible_get_child_count(section.get(), nullptr), ==, 1);
    auto password = adoptGRef(atspi_accessible_get_child_at_index(section.get(), 0, nullptr));
    g_assert_true(ATSPI_IS_TEXT(password.get()));
    test->startEventMonitor(password.get(), { "object:text-caret-moved" });
    g_assert_true(atspi_text_set_caret_offset(ATSPI_TEXT(password.get()), 2, nullptr));
    events = test->stopEventMonitor(1);
    g_assert_cmpuint(events.size(), ==, 1);
    g_assert_cmpstr(events[0]->type, ==, "object:text-caret-moved");
    g_assert_cmpuint(events[0]->detail1, ==, 2);

    // Selection changed.
    test->startEventMonitor(p.get(), { "object:text-selection-changed", "object:text-caret-moved" });
    g_assert_true(atspi_text_set_selection(ATSPI_TEXT(p.get()), 0, 10, 15, nullptr));
    events = test->stopEventMonitor(2);
    g_assert_cmpuint(events.size(), ==, 2);
    auto* event = AccessibilityTest::findEvent(events, "object:text-caret-moved");
    g_assert_nonnull(event);
    g_assert_cmpuint(event->detail1, ==, 15);
    g_assert_nonnull(AccessibilityTest::findEvent(events, "object:text-selection-changed"));

    test->startEventMonitor(input.get(), { "object:text-selection-changed", "object:text-caret-moved" });
    g_assert_true(atspi_text_set_selection(ATSPI_TEXT(input.get()), 0, 5, 10, nullptr));
    events = test->stopEventMonitor(2);
    g_assert_cmpuint(events.size(), ==, 2);
    event = AccessibilityTest::findEvent(events, "object:text-caret-moved");
    g_assert_nonnull(event);
    g_assert_cmpuint(event->detail1, ==, 10);
    g_assert_nonnull(AccessibilityTest::findEvent(events, "object:text-selection-changed"));

    test->startEventMonitor(div.get(), { "object:text-selection-changed", "object:text-caret-moved" });
    g_assert_true(atspi_text_set_selection(ATSPI_TEXT(div.get()), 0, 15, 18, nullptr));
    events = test->stopEventMonitor(2);
    g_assert_cmpuint(events.size(), ==, 2);
    event = AccessibilityTest::findEvent(events, "object:text-caret-moved");
    g_assert_nonnull(event);
    g_assert_cmpuint(event->detail1, ==, 18);
    g_assert_nonnull(AccessibilityTest::findEvent(events, "object:text-selection-changed"));

    test->startEventMonitor(password.get(), { "object:text-selection-changed", "object:text-caret-moved" });
    g_assert_true(atspi_text_set_selection(ATSPI_TEXT(password.get()), 0, 2, 3, nullptr));
    events = test->stopEventMonitor(2);
    g_assert_cmpuint(events.size(), ==, 2);
    event = AccessibilityTest::findEvent(events, "object:text-caret-moved");
    g_assert_nonnull(event);
    g_assert_cmpuint(event->detail1, ==, 3);
    g_assert_nonnull(AccessibilityTest::findEvent(events, "object:text-selection-changed"));

    // Text changed.
    g_assert_true(atspi_text_set_caret_offset(ATSPI_TEXT(input.get()), 5, nullptr));
    test->startEventMonitor(input.get(), { "object:text-changed:insert", "object:text-changed:delete" });
    test->keyStroke(GDK_KEY_a);
    events = test->stopEventMonitor(1);
    g_assert_cmpuint(events.size(), ==, 1);
    g_assert_cmpstr(events[0]->type, ==, "object:text-changed:insert");
    g_assert_cmpuint(events[0]->detail1, ==, 5);
    g_assert_cmpuint(events[0]->detail2, ==, 1);
    g_assert_true(G_VALUE_HOLDS_STRING(&events[0]->any_data));
    g_assert_cmpstr(g_value_get_string(&events[0]->any_data), ==, "a");
    g_assert_true(atspi_text_set_caret_offset(ATSPI_TEXT(input.get()), 0, nullptr));
    test->startEventMonitor(input.get(), { "object:text-changed:insert", "object:text-changed:delete" });
    test->keyStroke(GDK_KEY_Delete);
    events = test->stopEventMonitor(1);
    g_assert_cmpuint(events.size(), ==, 1);
    g_assert_cmpstr(events[0]->type, ==, "object:text-changed:delete");
    g_assert_cmpuint(events[0]->detail1, ==, 0);
    g_assert_cmpuint(events[0]->detail2, ==, 1);
    g_assert_true(G_VALUE_HOLDS_STRING(&events[0]->any_data));
    g_assert_cmpstr(g_value_get_string(&events[0]->any_data), ==, "T");

    g_assert_true(atspi_text_set_caret_offset(ATSPI_TEXT(div.get()), 10, nullptr));
    test->startEventMonitor(div.get(), { "object:text-changed:insert", "object:text-changed:delete" });
    test->keyStroke(GDK_KEY_b);
    events = test->stopEventMonitor(1);
    g_assert_cmpuint(events.size(), ==, 1);
    g_assert_cmpstr(events[0]->type, ==, "object:text-changed:insert");
    g_assert_cmpuint(events[0]->detail1, ==, 10);
    g_assert_cmpuint(events[0]->detail2, ==, 1);
    g_assert_true(G_VALUE_HOLDS_STRING(&events[0]->any_data));
    g_assert_cmpstr(g_value_get_string(&events[0]->any_data), ==, "b");
    g_assert_true(atspi_text_set_caret_offset(ATSPI_TEXT(div.get()), 15, nullptr));
    test->startEventMonitor(div.get(), { "object:text-changed:insert", "object:text-changed:delete" });
    test->keyStroke(GDK_KEY_BackSpace);
    events = test->stopEventMonitor(1);
    g_assert_cmpuint(events.size(), ==, 1);
    g_assert_cmpstr(events[0]->type, ==, "object:text-changed:delete");
    g_assert_cmpuint(events[0]->detail1, ==, 14);
    g_assert_cmpuint(events[0]->detail2, ==, 1);
    g_assert_true(G_VALUE_HOLDS_STRING(&events[0]->any_data));
    g_assert_cmpstr(g_value_get_string(&events[0]->any_data), ==, "n");

    g_assert_true(atspi_text_set_caret_offset(ATSPI_TEXT(password.get()), 2, nullptr));
    test->startEventMonitor(password.get(), { "object:text-changed:insert", "object:text-changed:delete" });
    test->keyStroke(GDK_KEY_a);
    events = test->stopEventMonitor(1);
    g_assert_cmpstr(events[0]->type, ==, "object:text-changed:insert");
    g_assert_cmpuint(events[0]->detail1, ==, 2);
    g_assert_cmpuint(events[0]->detail2, ==, 1);
    g_assert_true(G_VALUE_HOLDS_STRING(&events[0]->any_data));
    g_assert_cmpstr(g_value_get_string(&events[0]->any_data), ==, "•");
    test->startEventMonitor(password.get(), { "object:text-changed:insert", "object:text-changed:delete" });
    test->keyStroke(GDK_KEY_BackSpace);
    events = test->stopEventMonitor(1);
    g_assert_cmpuint(events.size(), ==, 1);
    g_assert_cmpstr(events[0]->type, ==, "object:text-changed:delete");
    g_assert_cmpuint(events[0]->detail1, ==, 2);
    g_assert_cmpuint(events[0]->detail2, ==, 1);
    g_assert_true(G_VALUE_HOLDS_STRING(&events[0]->any_data));
    g_assert_cmpstr(g_value_get_string(&events[0]->any_data), ==, "•");

    // Text replaced.
    g_assert_true(atspi_text_set_selection(ATSPI_TEXT(input.get()), 0, 5, 10, nullptr));
    test->startEventMonitor(input.get(), { "object:text-changed:insert", "object:text-changed:delete" });
    test->keyStroke(GDK_KEY_c);
    events = test->stopEventMonitor(2);
    g_assert_cmpuint(events.size(), ==, 2);
    g_assert_cmpstr(events[0]->type, ==, "object:text-changed:delete");
    g_assert_cmpuint(events[0]->detail1, ==, 6);
    g_assert_cmpuint(events[0]->detail2, ==, 5);
    g_assert_true(G_VALUE_HOLDS_STRING(&events[0]->any_data));
    g_assert_cmpstr(g_value_get_string(&events[0]->any_data), ==, "is a ");
    g_assert_cmpstr(events[1]->type, ==, "object:text-changed:insert");
    g_assert_cmpuint(events[1]->detail1, ==, 5);
    g_assert_cmpuint(events[1]->detail2, ==, 1);
    g_assert_true(G_VALUE_HOLDS_STRING(&events[1]->any_data));
    g_assert_cmpstr(g_value_get_string(&events[1]->any_data), ==, "c");

    g_assert_true(atspi_text_set_selection(ATSPI_TEXT(div.get()), 0, 10, 18, nullptr));
    test->startEventMonitor(div.get(), { "object:text-changed:insert", "object:text-changed:delete" });
    test->keyStroke(GDK_KEY_Delete);
    events = test->stopEventMonitor(1);
    g_assert_cmpuint(events.size(), ==, 1);
    g_assert_cmpstr(events[0]->type, ==, "object:text-changed:delete");
    g_assert_cmpuint(events[0]->detail1, ==, 10);
    g_assert_cmpuint(events[0]->detail2, ==, 8);
    g_assert_true(G_VALUE_HOLDS_STRING(&events[0]->any_data));
    g_assert_cmpstr(g_value_get_string(&events[0]->any_data), ==, "bntet ed");

    g_assert_true(atspi_text_set_selection(ATSPI_TEXT(password.get()), 0, 0, 3, nullptr));
    test->startEventMonitor(password.get(), { "object:text-changed:insert", "object:text-changed:delete" });
    test->keyStroke(GDK_KEY_z);
    events = test->stopEventMonitor(2);
    g_assert_cmpuint(events.size(), ==, 2);
    g_assert_cmpstr(events[0]->type, ==, "object:text-changed:delete");
    g_assert_cmpuint(events[0]->detail1, ==, 1);
    g_assert_cmpuint(events[0]->detail2, ==, 3);
    g_assert_true(G_VALUE_HOLDS_STRING(&events[0]->any_data));
    g_assert_cmpstr(g_value_get_string(&events[0]->any_data), ==, "•••");
    g_assert_cmpstr(events[1]->type, ==, "object:text-changed:insert");
    g_assert_cmpuint(events[1]->detail1, ==, 0);
    g_assert_cmpuint(events[1]->detail2, ==, 1);
    g_assert_true(G_VALUE_HOLDS_STRING(&events[1]->any_data));
    g_assert_cmpstr(g_value_get_string(&events[1]->any_data), ==, "•");

    // Text input value changed.
    test->startEventMonitor(input.get(), { "object:text-changed:insert", "object:text-changed:delete" });
    test->runJavaScriptAndWaitUntilFinished("document.getElementsByTagName('input')[0].value = 'foo';", nullptr);
    events = test->stopEventMonitor(2);
    g_assert_cmpuint(events.size(), ==, 2);
    g_assert_cmpstr(events[0]->type, ==, "object:text-changed:delete");
    g_assert_cmpuint(events[0]->detail1, ==, 0);
    g_assert_cmpuint(events[0]->detail2, ==, 16);
    g_assert_true(G_VALUE_HOLDS_STRING(&events[0]->any_data));
    g_assert_cmpstr(g_value_get_string(&events[0]->any_data), ==, "his actext field");
    g_assert_cmpstr(events[1]->type, ==, "object:text-changed:insert");
    g_assert_cmpuint(events[1]->detail1, ==, 0);
    g_assert_cmpuint(events[1]->detail2, ==, 3);
    g_assert_true(G_VALUE_HOLDS_STRING(&events[1]->any_data));
    g_assert_cmpstr(g_value_get_string(&events[1]->any_data), ==, "foo");

    test->startEventMonitor(password.get(), { "object:text-changed:insert", "object:text-changed:delete" });
    test->runJavaScriptAndWaitUntilFinished("document.getElementsByTagName('input')[1].value = '7890';", nullptr);
    events = test->stopEventMonitor(2);
    g_assert_cmpuint(events.size(), ==, 2);
    g_assert_cmpstr(events[0]->type, ==, "object:text-changed:delete");
    g_assert_cmpuint(events[0]->detail1, ==, 0);
    g_assert_cmpuint(events[0]->detail2, ==, 1);
    g_assert_true(G_VALUE_HOLDS_STRING(&events[0]->any_data));
    g_assert_cmpstr(g_value_get_string(&events[0]->any_data), ==, "•");
    g_assert_cmpstr(events[1]->type, ==, "object:text-changed:insert");
    g_assert_cmpuint(events[1]->detail1, ==, 0);
    g_assert_cmpuint(events[1]->detail2, ==, 4);
    g_assert_true(G_VALUE_HOLDS_STRING(&events[1]->any_data));
    g_assert_cmpstr(g_value_get_string(&events[1]->any_data), ==, "••••");
}

static void testTextReplacedObjects(AccessibilityTest* test, gconstpointer)
{
    test->showInWindow(800, 600);
    test->loadHtml(
        "<html>"
        "  <body>"
        "    <p>This is <button>button1</button> and <button>button2</button> in paragraph</p>"
        "    <p>This is <a href='#'>link1</a> and <a href='#'>link2</a> in paragraph</p>"
        "  </body>"
        "</html>",
        nullptr);
    test->waitUntilLoadFinished();

    auto testApp = test->findTestApplication();
    g_assert_true(ATSPI_IS_ACCESSIBLE(testApp.get()));

    auto documentWeb = test->findDocumentWeb(testApp.get());
    g_assert_true(ATSPI_IS_ACCESSIBLE(documentWeb.get()));
    g_assert_cmpint(atspi_accessible_get_child_count(documentWeb.get(), nullptr), ==, 2);

    auto p = adoptGRef(atspi_accessible_get_child_at_index(documentWeb.get(), 0, nullptr));
    g_assert_true(ATSPI_IS_TEXT(p.get()));
    auto length = atspi_text_get_character_count(ATSPI_TEXT(p.get()), nullptr);
    g_assert_cmpint(length, ==, 28);
    GUniquePtr<char> text(atspi_text_get_text(ATSPI_TEXT(p.get()), 0, -1, nullptr));
    g_assert_cmpstr(text.get(), ==, "This is \357\277\274 and \357\277\274 in paragraph");
    g_assert_cmpint(atspi_accessible_get_child_count(p.get(), nullptr), ==, 2);

    auto button1 = adoptGRef(atspi_accessible_get_child_at_index(p.get(), 0, nullptr));
    g_assert_true(ATSPI_IS_ACCESSIBLE(button1.get()));
    text.reset(atspi_accessible_get_name(button1.get(), nullptr));
    g_assert_cmpstr(text.get(), ==, "button1");

    auto button2 = adoptGRef(atspi_accessible_get_child_at_index(p.get(), 1, nullptr));
    g_assert_true(ATSPI_IS_ACCESSIBLE(button2.get()));
    text.reset(atspi_accessible_get_name(button2.get(), nullptr));
    g_assert_cmpstr(text.get(), ==, "button2");

    UniqueAtspiTextRange range(atspi_text_get_string_at_offset(ATSPI_TEXT(p.get()), 8, ATSPI_TEXT_GRANULARITY_CHAR, nullptr));
    g_assert_cmpstr(range->content, ==, "\357\277\274");
    g_assert_cmpint(range->start_offset, ==, 8);
    g_assert_cmpint(range->end_offset, ==, 9);
    range.reset(atspi_text_get_string_at_offset(ATSPI_TEXT(p.get()), 14, ATSPI_TEXT_GRANULARITY_CHAR, nullptr));
    g_assert_cmpstr(range->content, ==, "\357\277\274");
    g_assert_cmpint(range->start_offset, ==, 14);
    g_assert_cmpint(range->end_offset, ==, 15);

    range.reset(atspi_text_get_string_at_offset(ATSPI_TEXT(p.get()), 8, ATSPI_TEXT_GRANULARITY_WORD, nullptr));
    g_assert_cmpstr(range->content, ==, "\357\277\274 ");
    g_assert_cmpint(range->start_offset, ==, 8);
    g_assert_cmpint(range->end_offset, ==, 10);
    range.reset(atspi_text_get_string_at_offset(ATSPI_TEXT(p.get()), 14, ATSPI_TEXT_GRANULARITY_WORD, nullptr));
    g_assert_cmpstr(range->content, ==, "\357\277\274 ");
    g_assert_cmpint(range->start_offset, ==, 14);
    g_assert_cmpint(range->end_offset, ==, 16);

    int startOffset, endOffset;
    GRefPtr<GHashTable> attributes = adoptGRef(atspi_text_get_attribute_run(ATSPI_TEXT(p.get()), 0, FALSE, &startOffset, &endOffset, nullptr));
    g_assert_nonnull(attributes.get());
    g_assert_cmpuint(g_hash_table_size(attributes.get()), ==, 0);
    g_assert_cmpint(startOffset, ==, 0);
    g_assert_cmpint(endOffset, ==, 8);

    attributes = adoptGRef(atspi_text_get_attribute_run(ATSPI_TEXT(p.get()), 9, FALSE, &startOffset, &endOffset, nullptr));
    g_assert_nonnull(attributes.get());
    g_assert_cmpuint(g_hash_table_size(attributes.get()), >, 0);
    g_assert_cmpint(startOffset, ==, 8);
    g_assert_cmpint(endOffset, ==, 9);

    attributes = adoptGRef(atspi_text_get_attribute_run(ATSPI_TEXT(p.get()), 11, FALSE, &startOffset, &endOffset, nullptr));
    g_assert_nonnull(attributes.get());
    g_assert_cmpuint(g_hash_table_size(attributes.get()), ==, 0);
    g_assert_cmpint(startOffset, ==, 9);
    g_assert_cmpint(endOffset, ==, 14);

    attributes = adoptGRef(atspi_text_get_attribute_run(ATSPI_TEXT(p.get()), 15, FALSE, &startOffset, &endOffset, nullptr));
    g_assert_nonnull(attributes.get());
    g_assert_cmpuint(g_hash_table_size(attributes.get()), >, 0);
    g_assert_cmpint(startOffset, ==, 14);
    g_assert_cmpint(endOffset, ==, 15);

    attributes = adoptGRef(atspi_text_get_attribute_run(ATSPI_TEXT(p.get()), 18, FALSE, &startOffset, &endOffset, nullptr));
    g_assert_nonnull(attributes.get());
    g_assert_cmpuint(g_hash_table_size(attributes.get()), ==, 0);
    g_assert_cmpint(startOffset, ==, 15);
    g_assert_cmpint(endOffset, ==, 28);

    // Links are also replaced elements.
    p = adoptGRef(atspi_accessible_get_child_at_index(documentWeb.get(), 1, nullptr));
    g_assert_true(ATSPI_IS_TEXT(p.get()));
    g_assert_cmpint(atspi_text_get_character_count(ATSPI_TEXT(p.get()), nullptr), ==, 28);
    text.reset(atspi_text_get_text(ATSPI_TEXT(p.get()), 0, -1, nullptr));
    g_assert_cmpstr(text.get(), ==, "This is \357\277\274 and \357\277\274 in paragraph");
    g_assert_cmpint(atspi_accessible_get_child_count(p.get(), nullptr), ==, 2);

    auto link1 = adoptGRef(atspi_accessible_get_child_at_index(p.get(), 0, nullptr));
    g_assert_true(ATSPI_IS_TEXT(link1.get()));
    text.reset(atspi_text_get_text(ATSPI_TEXT(link1.get()), 0, -1, nullptr));
    g_assert_cmpstr(text.get(), ==, "link1");

    auto link2 = adoptGRef(atspi_accessible_get_child_at_index(p.get(), 1, nullptr));
    g_assert_true(ATSPI_IS_TEXT(link2.get()));
    text.reset(atspi_text_get_text(ATSPI_TEXT(link2.get()), 0, -1, nullptr));
    g_assert_cmpstr(text.get(), ==, "link2");
}

static void testTextListMarkers(AccessibilityTest* test, gconstpointer)
{
    test->showInWindow(800, 600);
    test->loadHtml(
        "<html>"
        "  <body>"
        "    <ul>"
        "      <li>List <b>item</b> 1</li>"
        "    </ul>"
        "    <ol style='direction:rtl;'>"
        "      <li>List <b>item</b> 1</li>"
        "    </ol>"
        "  </body>"
        "</html>",
        nullptr);
    test->waitUntilLoadFinished();

    auto testApp = test->findTestApplication();
    g_assert_true(ATSPI_IS_ACCESSIBLE(testApp.get()));

    auto documentWeb = test->findDocumentWeb(testApp.get());
    g_assert_true(ATSPI_IS_ACCESSIBLE(documentWeb.get()));
    g_assert_cmpint(atspi_accessible_get_child_count(documentWeb.get(), nullptr), ==, 2);

    auto ul = adoptGRef(atspi_accessible_get_child_at_index(documentWeb.get(), 0, nullptr));
    g_assert_true(ATSPI_IS_ACCESSIBLE(ul.get()));
    g_assert_cmpint(atspi_accessible_get_child_count(ul.get(), nullptr), ==, 1);

    auto li = adoptGRef(atspi_accessible_get_child_at_index(ul.get(), 0, nullptr));
    g_assert_true(ATSPI_IS_TEXT(li.get()));
    g_assert_cmpint(atspi_accessible_get_child_count(li.get(), nullptr), ==, 1);

    auto length = atspi_text_get_character_count(ATSPI_TEXT(li.get()), nullptr);
    g_assert_cmpint(length, ==, 12);

    GUniquePtr<char> text(atspi_text_get_text(ATSPI_TEXT(li.get()), 0, -1, nullptr));
    g_assert_cmpstr(text.get(), ==, "\357\277\274List item 1");

    UniqueAtspiTextRange range(atspi_text_get_string_at_offset(ATSPI_TEXT(li.get()), 0, ATSPI_TEXT_GRANULARITY_CHAR, nullptr));
    g_assert_cmpstr(range->content, ==, "\357\277\274");
    g_assert_cmpint(range->start_offset, ==, 0);
    g_assert_cmpint(range->end_offset, ==, 1);
    range.reset(atspi_text_get_string_at_offset(ATSPI_TEXT(li.get()), 1, ATSPI_TEXT_GRANULARITY_CHAR, nullptr));
    g_assert_cmpstr(range->content, ==, "L");
    g_assert_cmpint(range->start_offset, ==, 1);
    g_assert_cmpint(range->end_offset, ==, 2);
    range.reset(atspi_text_get_string_at_offset(ATSPI_TEXT(li.get()), 0, ATSPI_TEXT_GRANULARITY_WORD, nullptr));
    g_assert_cmpstr(range->content, ==, "\357\277\274");
    g_assert_cmpint(range->start_offset, ==, 0);
    g_assert_cmpint(range->end_offset, ==, 1);
    range.reset(atspi_text_get_string_at_offset(ATSPI_TEXT(li.get()), 1, ATSPI_TEXT_GRANULARITY_WORD, nullptr));
    g_assert_cmpstr(range->content, ==, "List ");
    g_assert_cmpint(range->start_offset, ==, 1);
    g_assert_cmpint(range->end_offset, ==, 6);
    range.reset(atspi_text_get_string_at_offset(ATSPI_TEXT(li.get()), 0, ATSPI_TEXT_GRANULARITY_LINE, nullptr));
    g_assert_cmpstr(range->content, ==, "\357\277\274List item 1");
    g_assert_cmpint(range->start_offset, ==, 0);
    g_assert_cmpint(range->end_offset, ==, 12);
    range.reset(atspi_text_get_string_at_offset(ATSPI_TEXT(li.get()), 1, ATSPI_TEXT_GRANULARITY_LINE, nullptr));
    g_assert_cmpstr(range->content, ==, "\357\277\274List item 1");
    g_assert_cmpint(range->start_offset, ==, 0);
    g_assert_cmpint(range->end_offset, ==, 12);

    int startOffset, endOffset;
    GRefPtr<GHashTable> attributes = adoptGRef(atspi_text_get_attribute_run(ATSPI_TEXT(li.get()), 0, FALSE, &startOffset, &endOffset, nullptr));
    g_assert_nonnull(attributes.get());
    g_assert_cmpuint(g_hash_table_size(attributes.get()), ==, 0);
    g_assert_cmpint(startOffset, ==, 0);
    g_assert_cmpint(endOffset, ==, 1);
    attributes = adoptGRef(atspi_text_get_attribute_run(ATSPI_TEXT(li.get()), 3, FALSE, &startOffset, &endOffset, nullptr));
    g_assert_nonnull(attributes.get());
    g_assert_cmpuint(g_hash_table_size(attributes.get()), ==, 0);
    g_assert_cmpint(startOffset, ==, 1);
    g_assert_cmpint(endOffset, ==, 6);
    attributes = adoptGRef(atspi_text_get_attribute_run(ATSPI_TEXT(li.get()), 8, FALSE, &startOffset, &endOffset, nullptr));
    g_assert_nonnull(attributes.get());
    g_assert_cmpuint(g_hash_table_size(attributes.get()), >, 0);
    g_assert_cmpint(startOffset, ==, 6);
    g_assert_cmpint(endOffset, ==, 10);
    attributes = adoptGRef(atspi_text_get_attribute_run(ATSPI_TEXT(li.get()), 11, FALSE, &startOffset, &endOffset, nullptr));
    g_assert_nonnull(attributes.get());
    g_assert_cmpuint(g_hash_table_size(attributes.get()), ==, 0);
    g_assert_cmpint(startOffset, ==, 10);
    g_assert_cmpint(endOffset, ==, 12);

    auto marker = adoptGRef(atspi_accessible_get_child_at_index(li.get(), 0, nullptr));
    g_assert_true(ATSPI_IS_TEXT(marker.get()));
    text.reset(atspi_text_get_text(ATSPI_TEXT(marker.get()), 0, -1, nullptr));
    g_assert_cmpstr(text.get(), ==, "• ");

    auto ol = adoptGRef(atspi_accessible_get_child_at_index(documentWeb.get(), 1, nullptr));
    g_assert_true(ATSPI_IS_ACCESSIBLE(ol.get()));
    g_assert_cmpint(atspi_accessible_get_child_count(ol.get(), nullptr), ==, 1);

    li = adoptGRef(atspi_accessible_get_child_at_index(ol.get(), 0, nullptr));
    g_assert_true(ATSPI_IS_TEXT(li.get()));
    g_assert_cmpint(atspi_accessible_get_child_count(li.get(), nullptr), ==, 1);

    length = atspi_text_get_character_count(ATSPI_TEXT(li.get()), nullptr);
    g_assert_cmpint(length, ==, 12);

    text.reset(atspi_text_get_text(ATSPI_TEXT(li.get()), 0, -1, nullptr));
    g_assert_cmpstr(text.get(), ==, "List item 1\357\277\274");

    marker = adoptGRef(atspi_accessible_get_child_at_index(li.get(), 0, nullptr));
    g_assert_true(ATSPI_IS_TEXT(marker.get()));
    text.reset(atspi_text_get_text(ATSPI_TEXT(marker.get()), 0, -1, nullptr));
    g_assert_cmpstr(text.get(), ==, "1. ");
}

static void testValueBasic(AccessibilityTest* test, gconstpointer)
{
    test->showInWindow(800, 600);
    test->loadHtml(
        "<html>"
        "  <body>"
        "    <input type='range' min='0' max='100' value='50' step='25'/>"
        "  </body>"
        "</html>",
        nullptr);
    test->waitUntilLoadFinished();

    auto testApp = test->findTestApplication();
    g_assert_true(ATSPI_IS_ACCESSIBLE(testApp.get()));

    auto documentWeb = test->findDocumentWeb(testApp.get());
    g_assert_true(ATSPI_IS_ACCESSIBLE(documentWeb.get()));
    g_assert_cmpint(atspi_accessible_get_child_count(documentWeb.get(), nullptr), ==, 1);

    auto panel = adoptGRef(atspi_accessible_get_child_at_index(documentWeb.get(), 0, nullptr));
    g_assert_true(ATSPI_IS_ACCESSIBLE(panel.get()));
    g_assert_cmpint(atspi_accessible_get_role(panel.get(), nullptr), ==, ATSPI_ROLE_PANEL);

    auto slider = adoptGRef(atspi_accessible_get_child_at_index(panel.get(), 0, nullptr));
    g_assert_true(ATSPI_IS_VALUE(slider.get()));
    g_assert_cmpfloat(atspi_value_get_current_value(ATSPI_VALUE(slider.get()), nullptr), ==, 50);
    g_assert_cmpfloat(atspi_value_get_minimum_value(ATSPI_VALUE(slider.get()), nullptr), ==, 0);
    g_assert_cmpfloat(atspi_value_get_maximum_value(ATSPI_VALUE(slider.get()), nullptr), ==, 100);
    g_assert_cmpfloat(atspi_value_get_minimum_increment(ATSPI_VALUE(slider.get()), nullptr), ==, 25);

    test->startEventMonitor(slider.get(), { "object:property-change:accessible-value" });
    g_assert_true(atspi_value_set_current_value(ATSPI_VALUE(slider.get()), 75, nullptr));
    auto events = test->stopEventMonitor(1);
    g_assert_cmpuint(events.size(), ==, 1);
    g_assert_cmpstr(events[0]->type, ==, "object:property-change:accessible-value");
    g_assert_cmpfloat(atspi_value_get_current_value(ATSPI_VALUE(slider.get()), nullptr), ==, 75);

    test->startEventMonitor(slider.get(), { "object:property-change:accessible-value" });
    g_assert_true(atspi_value_set_current_value(ATSPI_VALUE(slider.get()), 125, nullptr));
    events = test->stopEventMonitor(1);
    g_assert_cmpuint(events.size(), ==, 1);
    g_assert_cmpstr(events[0]->type, ==, "object:property-change:accessible-value");
    g_assert_cmpfloat(atspi_value_get_current_value(ATSPI_VALUE(slider.get()), nullptr), ==, 100);
    test->startEventMonitor(slider.get(), { "object:property-change:accessible-value" });
    g_assert_true(atspi_value_set_current_value(ATSPI_VALUE(slider.get()), -25, nullptr));
    events = test->stopEventMonitor(1);
    g_assert_cmpuint(events.size(), ==, 1);
    g_assert_cmpstr(events[0]->type, ==, "object:property-change:accessible-value");
    g_assert_cmpfloat(atspi_value_get_current_value(ATSPI_VALUE(slider.get()), nullptr), ==, 0);
}

static void testHyperlinkBasic(AccessibilityTest* test, gconstpointer)
{
    test->showInWindow(800, 600);
    test->loadHtml(
        "<html>"
        "  <body>"
        "    <a href='https://www.webkitgtk.org'>WebKitGTK</a>"
        "    <div role='link'>Link</div>"
        "    <p>This is <button>button1</button> and <button>button2</button> in a paragraph</p>"
        "    <p>This is <a href='https://www.webkitgtk.org'>link1</a> and <a href='https://www.gnome.org'>link2</a> in paragraph</p>"
        "    <ul><li>List item</li></ul>"
        "  </body>"
        "</html>",
        nullptr);
    test->waitUntilLoadFinished();

    auto testApp = test->findTestApplication();
    g_assert_true(ATSPI_IS_ACCESSIBLE(testApp.get()));

    auto documentWeb = test->findDocumentWeb(testApp.get());
    g_assert_true(ATSPI_IS_ACCESSIBLE(documentWeb.get()));
    g_assert_cmpint(atspi_accessible_get_child_count(documentWeb.get(), nullptr), ==, 5);

    auto section = adoptGRef(atspi_accessible_get_child_at_index(documentWeb.get(), 0, nullptr));
    g_assert_true(ATSPI_IS_ACCESSIBLE(section.get()));
    g_assert_cmpint(atspi_accessible_get_role(section.get(), nullptr), ==, ATSPI_ROLE_SECTION);
    g_assert_cmpint(atspi_accessible_get_child_count(section.get(), nullptr), ==, 1);

    auto a = adoptGRef(atspi_accessible_get_child_at_index(section.get(), 0, nullptr));
    g_assert_true(ATSPI_IS_ACCESSIBLE(a.get()));
    g_assert_cmpint(atspi_accessible_get_role(a.get(), nullptr), ==, ATSPI_ROLE_LINK);
    auto link = adoptGRef(atspi_accessible_get_hyperlink(a.get()));
    g_assert_true(ATSPI_IS_HYPERLINK(link.get()));
    g_assert_cmpint(atspi_hyperlink_get_n_anchors(ATSPI_HYPERLINK(link.get()), nullptr), ==, 1);
    g_assert_true(atspi_hyperlink_is_valid(ATSPI_HYPERLINK(link.get()), nullptr));
    g_assert_cmpint(atspi_hyperlink_get_start_index(ATSPI_HYPERLINK(link.get()), nullptr), ==, 0);
    g_assert_cmpint(atspi_hyperlink_get_end_index(ATSPI_HYPERLINK(link.get()), nullptr), ==, 1);
    GUniquePtr<char> uri(atspi_hyperlink_get_uri(ATSPI_HYPERLINK(link.get()), 0, nullptr));
    g_assert_cmpstr(uri.get(), ==, "https://www.webkitgtk.org/");
    g_assert_true(atspi_hyperlink_get_object(ATSPI_HYPERLINK(link.get()), 0, nullptr) == a.get());

    auto div = adoptGRef(atspi_accessible_get_child_at_index(documentWeb.get(), 1, nullptr));
    g_assert_true(ATSPI_IS_ACCESSIBLE(div.get()));
    g_assert_cmpint(atspi_accessible_get_role(div.get(), nullptr), ==, ATSPI_ROLE_LINK);
    link = adoptGRef(atspi_accessible_get_hyperlink(div.get()));
    g_assert_true(ATSPI_IS_HYPERLINK(link.get()));
    g_assert_cmpint(atspi_hyperlink_get_n_anchors(ATSPI_HYPERLINK(link.get()), nullptr), ==, 1);
    g_assert_true(atspi_hyperlink_is_valid(ATSPI_HYPERLINK(link.get()), nullptr));
    g_assert_cmpint(atspi_hyperlink_get_start_index(ATSPI_HYPERLINK(link.get()), nullptr), ==, 0);
    g_assert_cmpint(atspi_hyperlink_get_end_index(ATSPI_HYPERLINK(link.get()), nullptr), ==, 1);
    uri.reset(atspi_hyperlink_get_uri(ATSPI_HYPERLINK(link.get()), 0, nullptr));
    g_assert_cmpstr(uri.get(), ==, "");
    g_assert_true(atspi_hyperlink_get_object(ATSPI_HYPERLINK(link.get()), 0, nullptr) == div.get());

    auto p = adoptGRef(atspi_accessible_get_child_at_index(documentWeb.get(), 2, nullptr));
    g_assert_true(ATSPI_IS_ACCESSIBLE(p.get()));
    g_assert_cmpint(atspi_accessible_get_child_count(p.get(), nullptr), ==, 2);
    auto button1 = adoptGRef(atspi_accessible_get_child_at_index(p.get(), 0, nullptr));
    g_assert_true(ATSPI_IS_ACCESSIBLE(button1.get()));
    link = adoptGRef(atspi_accessible_get_hyperlink(button1.get()));
    g_assert_true(ATSPI_IS_HYPERLINK(link.get()));
    g_assert_cmpint(atspi_hyperlink_get_n_anchors(ATSPI_HYPERLINK(link.get()), nullptr), ==, 1);
    g_assert_true(atspi_hyperlink_is_valid(ATSPI_HYPERLINK(link.get()), nullptr));
    g_assert_cmpint(atspi_hyperlink_get_start_index(ATSPI_HYPERLINK(link.get()), nullptr), ==, 8);
    g_assert_cmpint(atspi_hyperlink_get_end_index(ATSPI_HYPERLINK(link.get()), nullptr), ==, 9);
    uri.reset(atspi_hyperlink_get_uri(ATSPI_HYPERLINK(link.get()), 0, nullptr));
    g_assert_cmpstr(uri.get(), ==, "");
    g_assert_true(atspi_hyperlink_get_object(ATSPI_HYPERLINK(link.get()), 0, nullptr) == button1.get());
    auto button2 = adoptGRef(atspi_accessible_get_child_at_index(p.get(), 1, nullptr));
    g_assert_true(ATSPI_IS_ACCESSIBLE(button2.get()));
    link = adoptGRef(atspi_accessible_get_hyperlink(button2.get()));
    g_assert_true(ATSPI_IS_HYPERLINK(link.get()));
    g_assert_cmpint(atspi_hyperlink_get_n_anchors(ATSPI_HYPERLINK(link.get()), nullptr), ==, 1);
    g_assert_true(atspi_hyperlink_is_valid(ATSPI_HYPERLINK(link.get()), nullptr));
    g_assert_cmpint(atspi_hyperlink_get_start_index(ATSPI_HYPERLINK(link.get()), nullptr), ==, 14);
    g_assert_cmpint(atspi_hyperlink_get_end_index(ATSPI_HYPERLINK(link.get()), nullptr), ==, 15);
    uri.reset(atspi_hyperlink_get_uri(ATSPI_HYPERLINK(link.get()), 0, nullptr));
    g_assert_cmpstr(uri.get(), ==, "");
    g_assert_true(atspi_hyperlink_get_object(ATSPI_HYPERLINK(link.get()), 0, nullptr) == button2.get());

    p = adoptGRef(atspi_accessible_get_child_at_index(documentWeb.get(), 3, nullptr));
    g_assert_true(ATSPI_IS_ACCESSIBLE(p.get()));
    g_assert_cmpint(atspi_accessible_get_child_count(p.get(), nullptr), ==, 2);
    auto link1 = adoptGRef(atspi_accessible_get_child_at_index(p.get(), 0, nullptr));
    g_assert_true(ATSPI_IS_ACCESSIBLE(link1.get()));
    link = adoptGRef(atspi_accessible_get_hyperlink(link1.get()));
    g_assert_true(ATSPI_IS_HYPERLINK(link.get()));
    g_assert_cmpint(atspi_hyperlink_get_n_anchors(ATSPI_HYPERLINK(link.get()), nullptr), ==, 1);
    g_assert_true(atspi_hyperlink_is_valid(ATSPI_HYPERLINK(link.get()), nullptr));
    g_assert_cmpint(atspi_hyperlink_get_start_index(ATSPI_HYPERLINK(link.get()), nullptr), ==, 8);
    g_assert_cmpint(atspi_hyperlink_get_end_index(ATSPI_HYPERLINK(link.get()), nullptr), ==, 9);
    uri.reset(atspi_hyperlink_get_uri(ATSPI_HYPERLINK(link.get()), 0, nullptr));
    g_assert_cmpstr(uri.get(), ==, "https://www.webkitgtk.org/");
    g_assert_true(atspi_hyperlink_get_object(ATSPI_HYPERLINK(link.get()), 0, nullptr) == link1.get());
    auto link2 = adoptGRef(atspi_accessible_get_child_at_index(p.get(), 1, nullptr));
    g_assert_true(ATSPI_IS_ACCESSIBLE(link2.get()));
    link = adoptGRef(atspi_accessible_get_hyperlink(link2.get()));
    g_assert_true(ATSPI_IS_HYPERLINK(link.get()));
    g_assert_cmpint(atspi_hyperlink_get_n_anchors(ATSPI_HYPERLINK(link.get()), nullptr), ==, 1);
    g_assert_true(atspi_hyperlink_is_valid(ATSPI_HYPERLINK(link.get()), nullptr));
    g_assert_cmpint(atspi_hyperlink_get_start_index(ATSPI_HYPERLINK(link.get()), nullptr), ==, 14);
    g_assert_cmpint(atspi_hyperlink_get_end_index(ATSPI_HYPERLINK(link.get()), nullptr), ==, 15);
    uri.reset(atspi_hyperlink_get_uri(ATSPI_HYPERLINK(link.get()), 0, nullptr));
    g_assert_cmpstr(uri.get(), ==, "https://www.gnome.org/");
    g_assert_true(atspi_hyperlink_get_object(ATSPI_HYPERLINK(link.get()), 0, nullptr) == link2.get());

    auto ul = adoptGRef(atspi_accessible_get_child_at_index(documentWeb.get(), 4, nullptr));
    g_assert_true(ATSPI_IS_ACCESSIBLE(ul.get()));
    g_assert_cmpint(atspi_accessible_get_child_count(ul.get(), nullptr), ==, 1);
    auto li = adoptGRef(atspi_accessible_get_child_at_index(ul.get(), 0, nullptr));
    g_assert_true(ATSPI_IS_ACCESSIBLE(li.get()));
    g_assert_cmpint(atspi_accessible_get_child_count(li.get(), nullptr), ==, 1);
    auto marker = adoptGRef(atspi_accessible_get_child_at_index(li.get(), 0, nullptr));
    g_assert_true(ATSPI_IS_ACCESSIBLE(marker.get()));
    link = adoptGRef(atspi_accessible_get_hyperlink(marker.get()));
    g_assert_true(ATSPI_IS_HYPERLINK(link.get()));
    g_assert_cmpint(atspi_hyperlink_get_n_anchors(ATSPI_HYPERLINK(link.get()), nullptr), ==, 1);
    g_assert_true(atspi_hyperlink_is_valid(ATSPI_HYPERLINK(link.get()), nullptr));
    g_assert_cmpint(atspi_hyperlink_get_start_index(ATSPI_HYPERLINK(link.get()), nullptr), ==, 0);
    g_assert_cmpint(atspi_hyperlink_get_end_index(ATSPI_HYPERLINK(link.get()), nullptr), ==, 1);
    uri.reset(atspi_hyperlink_get_uri(ATSPI_HYPERLINK(link.get()), 0, nullptr));
    g_assert_cmpstr(uri.get(), ==, "");
    g_assert_true(atspi_hyperlink_get_object(ATSPI_HYPERLINK(link.get()), 0, nullptr) == marker.get());
}

static void testHypertextBasic(AccessibilityTest* test, gconstpointer)
{
    test->showInWindow(800, 600);
    test->loadHtml(
        "<html>"
        "  <body>"
        "    <p>This is <button>button</button> and <a href='https://www.webkitgtk.org'>link</a> in a paragraph</p>"
        "    <ol><li>List with a <a href='https://www.webkit.org'>link</a></li></ol>"
        "  </body>"
        "</html>",
        nullptr);
    test->waitUntilLoadFinished();

    auto testApp = test->findTestApplication();
    g_assert_true(ATSPI_IS_ACCESSIBLE(testApp.get()));

    auto documentWeb = test->findDocumentWeb(testApp.get());
    g_assert_true(ATSPI_IS_ACCESSIBLE(documentWeb.get()));
    g_assert_cmpint(atspi_accessible_get_child_count(documentWeb.get(), nullptr), ==, 2);

    auto p = adoptGRef(atspi_accessible_get_child_at_index(documentWeb.get(), 0, nullptr));
    g_assert_true(ATSPI_IS_HYPERTEXT(p.get()));
    g_assert_cmpint(atspi_hypertext_get_n_links(ATSPI_HYPERTEXT(p.get()), nullptr), ==, 2);

    auto link = adoptGRef(atspi_hypertext_get_link(ATSPI_HYPERTEXT(p.get()), 0, nullptr));
    g_assert_true(ATSPI_IS_HYPERLINK(link.get()));
    g_assert_cmpint(atspi_hyperlink_get_n_anchors(ATSPI_HYPERLINK(link.get()), nullptr), ==, 1);
    g_assert_true(atspi_hyperlink_is_valid(ATSPI_HYPERLINK(link.get()), nullptr));
    g_assert_cmpint(atspi_hyperlink_get_start_index(ATSPI_HYPERLINK(link.get()), nullptr), ==, 8);
    g_assert_cmpint(atspi_hyperlink_get_end_index(ATSPI_HYPERLINK(link.get()), nullptr), ==, 9);
    GUniquePtr<char> uri(atspi_hyperlink_get_uri(ATSPI_HYPERLINK(link.get()), 0, nullptr));
    g_assert_cmpstr(uri.get(), ==, "");
    auto button = adoptGRef(atspi_accessible_get_child_at_index(p.get(), 0, nullptr));
    g_assert_true(atspi_hyperlink_get_object(ATSPI_HYPERLINK(link.get()), 0, nullptr) == button.get());

    link = adoptGRef(atspi_hypertext_get_link(ATSPI_HYPERTEXT(p.get()), 1, nullptr));
    g_assert_true(ATSPI_IS_HYPERLINK(link.get()));
    g_assert_cmpint(atspi_hyperlink_get_n_anchors(ATSPI_HYPERLINK(link.get()), nullptr), ==, 1);
    g_assert_true(atspi_hyperlink_is_valid(ATSPI_HYPERLINK(link.get()), nullptr));
    g_assert_cmpint(atspi_hyperlink_get_start_index(ATSPI_HYPERLINK(link.get()), nullptr), ==, 14);
    g_assert_cmpint(atspi_hyperlink_get_end_index(ATSPI_HYPERLINK(link.get()), nullptr), ==, 15);
    uri.reset(atspi_hyperlink_get_uri(ATSPI_HYPERLINK(link.get()), 0, nullptr));
    g_assert_cmpstr(uri.get(), ==, "https://www.webkitgtk.org/");
    auto a = adoptGRef(atspi_accessible_get_child_at_index(p.get(), 1, nullptr));
    g_assert_true(atspi_hyperlink_get_object(ATSPI_HYPERLINK(link.get()), 0, nullptr) == a.get());

    g_assert_cmpint(atspi_hypertext_get_link_index(ATSPI_HYPERTEXT(p.get()), 0, nullptr), ==, -1);
    g_assert_cmpint(atspi_hypertext_get_link_index(ATSPI_HYPERTEXT(p.get()), 8, nullptr), ==, 0);
    g_assert_cmpint(atspi_hypertext_get_link_index(ATSPI_HYPERTEXT(p.get()), 9, nullptr), ==, -1);
    g_assert_cmpint(atspi_hypertext_get_link_index(ATSPI_HYPERTEXT(p.get()), 14, nullptr), ==, 1);
    g_assert_cmpint(atspi_hypertext_get_link_index(ATSPI_HYPERTEXT(p.get()), 15, nullptr), ==, -1);

    auto ol = adoptGRef(atspi_accessible_get_child_at_index(documentWeb.get(), 1, nullptr));
    g_assert_true(ATSPI_IS_ACCESSIBLE(ol.get()));
    g_assert_cmpint(atspi_accessible_get_child_count(ol.get(), nullptr), ==, 1);
    auto li = adoptGRef(atspi_accessible_get_child_at_index(ol.get(), 0, nullptr));
    g_assert_true(ATSPI_IS_HYPERTEXT(li.get()));
    g_assert_cmpint(atspi_hypertext_get_n_links(ATSPI_HYPERTEXT(li.get()), nullptr), ==, 2);
    link = adoptGRef(atspi_hypertext_get_link(ATSPI_HYPERTEXT(li.get()), 0, nullptr));
    g_assert_true(ATSPI_IS_HYPERLINK(link.get()));
    g_assert_cmpint(atspi_hyperlink_get_n_anchors(ATSPI_HYPERLINK(link.get()), nullptr), ==, 1);
    g_assert_true(atspi_hyperlink_is_valid(ATSPI_HYPERLINK(link.get()), nullptr));
    g_assert_cmpint(atspi_hyperlink_get_start_index(ATSPI_HYPERLINK(link.get()), nullptr), ==, 0);
    g_assert_cmpint(atspi_hyperlink_get_end_index(ATSPI_HYPERLINK(link.get()), nullptr), ==, 1);
    uri.reset(atspi_hyperlink_get_uri(ATSPI_HYPERLINK(link.get()), 0, nullptr));
    g_assert_cmpstr(uri.get(), ==, "");
    auto marker = adoptGRef(atspi_accessible_get_child_at_index(li.get(), 0, nullptr));
    g_assert_true(atspi_hyperlink_get_object(ATSPI_HYPERLINK(link.get()), 0, nullptr) == marker.get());
    link = adoptGRef(atspi_hypertext_get_link(ATSPI_HYPERTEXT(li.get()), 1, nullptr));
    g_assert_true(ATSPI_IS_HYPERLINK(link.get()));
    g_assert_cmpint(atspi_hyperlink_get_n_anchors(ATSPI_HYPERLINK(link.get()), nullptr), ==, 1);
    g_assert_true(atspi_hyperlink_is_valid(ATSPI_HYPERLINK(link.get()), nullptr));
    g_assert_cmpint(atspi_hyperlink_get_start_index(ATSPI_HYPERLINK(link.get()), nullptr), ==, 13);
    g_assert_cmpint(atspi_hyperlink_get_end_index(ATSPI_HYPERLINK(link.get()), nullptr), ==, 14);
    uri.reset(atspi_hyperlink_get_uri(ATSPI_HYPERLINK(link.get()), 0, nullptr));
    g_assert_cmpstr(uri.get(), ==, "https://www.webkit.org/");
    a = adoptGRef(atspi_accessible_get_child_at_index(li.get(), 1, nullptr));
    g_assert_true(atspi_hyperlink_get_object(ATSPI_HYPERLINK(link.get()), 0, nullptr) == a.get());

    g_assert_cmpint(atspi_hypertext_get_link_index(ATSPI_HYPERTEXT(li.get()), 0, nullptr), ==, 0);
    g_assert_cmpint(atspi_hypertext_get_link_index(ATSPI_HYPERTEXT(li.get()), 1, nullptr), ==, -1);
    g_assert_cmpint(atspi_hypertext_get_link_index(ATSPI_HYPERTEXT(li.get()), 13, nullptr), ==, 1);
    g_assert_cmpint(atspi_hypertext_get_link_index(ATSPI_HYPERTEXT(li.get()), 14, nullptr), ==, -1);
}

static void testActionBasic(AccessibilityTest* test, gconstpointer)
{
    test->showInWindow(800, 600);
    test->loadHtml(
        "<html>"
        "  <body>"
        "    <p>This is <button accessKey='p'>button</button> and <a href='https://www.webkitgtk.org'>link</a> in a paragraph</p>"
        "  </body>"
        "</html>",
        nullptr);
    test->waitUntilLoadFinished();

    auto testApp = test->findTestApplication();
    g_assert_true(ATSPI_IS_ACCESSIBLE(testApp.get()));

    auto documentWeb = test->findDocumentWeb(testApp.get());
    g_assert_true(ATSPI_IS_ACCESSIBLE(documentWeb.get()));
    g_assert_cmpint(atspi_accessible_get_child_count(documentWeb.get(), nullptr), ==, 1);

    auto p = adoptGRef(atspi_accessible_get_child_at_index(documentWeb.get(), 0, nullptr));
    g_assert_true(ATSPI_IS_ACTION(p.get()));
    // Paragraph implements action interface, but it does nothing.
    g_assert_cmpint(atspi_action_get_n_actions(ATSPI_ACTION(p.get()), nullptr), ==, 1);
    GUniquePtr<char> name(atspi_action_get_action_name(ATSPI_ACTION(p.get()), 0, nullptr));
    g_assert_cmpstr(name.get(), ==, "");
    GUniquePtr<char> localizedName(atspi_action_get_localized_name(ATSPI_ACTION(p.get()), 0, nullptr));
    g_assert_cmpstr(localizedName.get(), ==, "");
    GUniquePtr<char> keyBinding(atspi_action_get_key_binding(ATSPI_ACTION(p.get()), 0, nullptr));
    g_assert_cmpstr(keyBinding.get(), ==, "");

    auto button = adoptGRef(atspi_accessible_get_child_at_index(p.get(), 0, nullptr));
    g_assert_true(ATSPI_IS_ACTION(button.get()));
    g_assert_cmpint(atspi_action_get_n_actions(ATSPI_ACTION(button.get()), nullptr), ==, 1);
    name.reset(atspi_action_get_action_name(ATSPI_ACTION(button.get()), 0, nullptr));
    g_assert_cmpstr(name.get(), ==, "press");
    localizedName.reset(atspi_action_get_localized_name(ATSPI_ACTION(button.get()), 0, nullptr));
    g_assert_cmpstr(localizedName.get(), ==, "press");
    keyBinding.reset(atspi_action_get_key_binding(ATSPI_ACTION(button.get()), 0, nullptr));
    g_assert_cmpstr(keyBinding.get(), ==, "p");

    auto a = adoptGRef(atspi_accessible_get_child_at_index(p.get(), 1, nullptr));
    g_assert_true(ATSPI_IS_ACTION(a.get()));
    g_assert_cmpint(atspi_action_get_n_actions(ATSPI_ACTION(a.get()), nullptr), ==, 1);
    name.reset(atspi_action_get_action_name(ATSPI_ACTION(a.get()), 0, nullptr));
    g_assert_cmpstr(name.get(), ==, "jump");
    localizedName.reset(atspi_action_get_localized_name(ATSPI_ACTION(a.get()), 0, nullptr));
    g_assert_cmpstr(localizedName.get(), ==, "jump");
    keyBinding.reset(atspi_action_get_key_binding(ATSPI_ACTION(a.get()), 0, nullptr));
    g_assert_cmpstr(keyBinding.get(), ==, "");
}

static void testDocumentBasic(AccessibilityTest* test, gconstpointer)
{
    test->showInWindow(800, 600);
    test->loadHtml(
        "<!doctype html>"
        "<html>"
        "  <head>"
        "    <meta http-equiv='content-language' content='en-us'/>"
        "    <meta http-equiv='content-type' content='text/html; charset=UTF-8'/>"
        "    <title>Document attributes</title>"
        "  </head>"
        "  <body>"
        "    <p>This is a paragraph</p>"
        "    <p lang='es'>Spanish</p>"
        "  </body>"
        "</html>",
        "http://example.org");
    test->waitUntilLoadFinished();

    auto testApp = test->findTestApplication();
    g_assert_true(ATSPI_IS_ACCESSIBLE(testApp.get()));

    auto documentWeb = test->findDocumentWeb(testApp.get());
    g_assert_true(ATSPI_IS_DOCUMENT(documentWeb.get()));
    g_assert_cmpint(atspi_accessible_get_child_count(documentWeb.get(), nullptr), ==, 2);

    GUniquePtr<char> language(atspi_document_get_locale(ATSPI_DOCUMENT(documentWeb.get()), nullptr));
    g_assert_cmpstr(language.get(), ==, "en-us");
    // Elements with no language attribute inhewir the document one.
    auto p1 = adoptGRef(atspi_accessible_get_child_at_index(documentWeb.get(), 0, nullptr));
    g_assert_true(ATSPI_IS_ACCESSIBLE(p1.get()));
    g_assert_cmpstr(atspi_accessible_get_object_locale(p1.get(), nullptr), ==, "en-us");
    auto p2 = adoptGRef(atspi_accessible_get_child_at_index(documentWeb.get(), 1, nullptr));
    g_assert_true(ATSPI_IS_ACCESSIBLE(p2.get()));
    g_assert_cmpstr(atspi_accessible_get_object_locale(p2.get(), nullptr), ==, "es");

    GRefPtr<GHashTable> attributes = adoptGRef(atspi_document_get_attributes(ATSPI_DOCUMENT(documentWeb.get()), nullptr));
    g_assert_nonnull(attributes.get());
    g_assert_cmpuint(g_hash_table_size(attributes.get()), ==, 5);
    g_assert_cmpstr(static_cast<const char*>(g_hash_table_lookup(attributes.get(), "DocType")), ==, "html");
    GUniquePtr<char> value(atspi_document_get_document_attribute_value(ATSPI_DOCUMENT(documentWeb.get()), const_cast<char*>("DocType"), nullptr));
    g_assert_cmpstr(value.get(), ==, "html");
    g_assert_cmpstr(static_cast<const char*>(g_hash_table_lookup(attributes.get(), "Encoding")), ==, "UTF-8");
    value.reset(atspi_document_get_document_attribute_value(ATSPI_DOCUMENT(documentWeb.get()), const_cast<char*>("Encoding"), nullptr));
    g_assert_cmpstr(value.get(), ==, "UTF-8");
    g_assert_cmpstr(static_cast<const char*>(g_hash_table_lookup(attributes.get(), "URI")), ==, "http://example.org/");
    value.reset(atspi_document_get_document_attribute_value(ATSPI_DOCUMENT(documentWeb.get()), const_cast<char*>("URI"), nullptr));
    g_assert_cmpstr(value.get(), ==, "http://example.org/");
    g_assert_cmpstr(static_cast<const char*>(g_hash_table_lookup(attributes.get(), "MimeType")), ==, "text/html");
    value.reset(atspi_document_get_document_attribute_value(ATSPI_DOCUMENT(documentWeb.get()), const_cast<char*>("MimeType"), nullptr));
    g_assert_cmpstr(value.get(), ==, "text/html");
    g_assert_cmpstr(static_cast<const char*>(g_hash_table_lookup(attributes.get(), "Title")), ==, "Document attributes");
    value.reset(atspi_document_get_document_attribute_value(ATSPI_DOCUMENT(documentWeb.get()), const_cast<char*>("Title"), nullptr));
    g_assert_cmpstr(value.get(), ==, "Document attributes");
}

static void testDocumentLoadEvents(AccessibilityTest* test, gconstpointer)
{
    test->showInWindow();
    test->loadURI("about:blank");
    test->waitUntilLoadFinished();
    test->startEventMonitor(std::nullopt, { "document:", "object:state-changed:busy" });
    test->loadHtml(
        "<html>"
        "  <body>"
        "    <p>Loading events test</p>"
        "  </body>"
        "</html>",
        nullptr);
    test->waitUntilLoadFinished();
    auto events = test->stopEventMonitor(2);
    g_assert_cmpuint(events.size(), ==, 2);
    g_assert_cmpstr(events[0]->type, ==, "object:state-changed:busy");
    g_assert_cmpuint(events[0]->detail1, ==, 0);
    g_assert_true(ATSPI_IS_ACCESSIBLE(events[0]->source));
    g_assert_cmpint(atspi_accessible_get_role(events[0]->source, nullptr), ==, ATSPI_ROLE_DOCUMENT_WEB);
    g_assert_cmpstr(events[1]->type, ==, "document:load-complete");
    g_assert_true(events[0]->source == events[1]->source);
    g_assert_true(ATSPI_IS_ACCESSIBLE(events[1]->source));
    g_assert_cmpint(atspi_accessible_get_role(events[1]->source, nullptr), ==, ATSPI_ROLE_DOCUMENT_WEB);
    events = { };

    test->startEventMonitor(std::nullopt, { "document:", "object:state-changed:busy" });
    webkit_web_view_reload(test->m_webView);
    test->waitUntilLoadFinished();
    events = test->stopEventMonitor(4);
    g_assert_cmpuint(events.size(), ==, 4);
    g_assert_cmpstr(events[0]->type, ==, "object:state-changed:busy");
    g_assert_cmpuint(events[0]->detail1, ==, 1);
    g_assert_cmpstr(events[1]->type, ==, "document:reload");
    g_assert_true(events[0]->source == events[1]->source);
    g_assert_true(ATSPI_IS_ACCESSIBLE(events[0]->source));
    g_assert_cmpint(atspi_accessible_get_role(events[0]->source, nullptr), ==, ATSPI_ROLE_DOCUMENT_WEB);
    g_assert_cmpstr(events[2]->type, ==, "object:state-changed:busy");
    g_assert_cmpuint(events[2]->detail1, ==, 0);
    g_assert_false(events[1]->source == events[2]->source);
    g_assert_cmpstr(events[3]->type, ==, "document:load-complete");
    g_assert_true(events[2]->source == events[3]->source);
    g_assert_true(ATSPI_IS_ACCESSIBLE(events[2]->source));
    g_assert_cmpint(atspi_accessible_get_role(events[2]->source, nullptr), ==, ATSPI_ROLE_DOCUMENT_WEB);
    events = { };
}

static void testImageBasic(AccessibilityTest* test, gconstpointer)
{
    test->showInWindow(800, 600);
    GUniquePtr<char> baseDir(g_strdup_printf("file://%s/", Test::getResourcesDir().data()));
    test->loadHtml(
        "<html>"
        "  <body>"
        "    <img style='position:absolute; left:1; top:1' src='blank.ico' width=5 height=5 alt='This is a blank icon' lang='en'></img>"
        "  </body>"
        "</html>",
        baseDir.get());
    test->waitUntilLoadFinished();

    auto testApp = test->findTestApplication();
    g_assert_true(ATSPI_IS_ACCESSIBLE(testApp.get()));

    auto documentWeb = test->findDocumentWeb(testApp.get());
    g_assert_true(ATSPI_IS_ACCESSIBLE(documentWeb.get()));
    g_assert_cmpint(atspi_accessible_get_child_count(documentWeb.get(), nullptr), ==, 1);

    auto img = adoptGRef(atspi_accessible_get_child_at_index(documentWeb.get(), 0, nullptr));
    g_assert_true(ATSPI_IS_IMAGE(img.get()));
    GUniquePtr<AtspiRect> rect(atspi_image_get_image_extents(ATSPI_IMAGE(img.get()), ATSPI_COORD_TYPE_WINDOW, nullptr));
    g_assert_nonnull(rect.get());
    g_assert_cmpuint(rect->x, ==, 1);
    g_assert_cmpuint(rect->y, ==, 1);
    g_assert_cmpuint(rect->width, ==, 5);
    g_assert_cmpuint(rect->height, ==, 5);
    GUniquePtr<AtspiPoint> point(atspi_image_get_image_position(ATSPI_IMAGE(img.get()), ATSPI_COORD_TYPE_WINDOW, nullptr));
    g_assert_nonnull(point.get());
    g_assert_cmpuint(rect->x, ==, point->x);
    g_assert_cmpuint(rect->y, ==, point->y);
    GUniquePtr<AtspiPoint> size(atspi_image_get_image_size(ATSPI_IMAGE(img.get()), nullptr));
    g_assert_nonnull(size.get());
    g_assert_cmpuint(size->x, ==, rect->width);
    g_assert_cmpuint(size->y, ==, rect->height);

    GUniquePtr<char> description(atspi_image_get_image_description(ATSPI_IMAGE(img.get()), nullptr));
    g_assert_cmpstr(description.get(), ==, "This is a blank icon");
    GUniquePtr<char> locale(atspi_image_get_image_locale(ATSPI_IMAGE(img.get()), nullptr));
    g_assert_cmpstr(locale.get(), ==, "en");
}

static void testSelectionListBox(AccessibilityTest* test, gconstpointer)
{
    test->showInWindow(800, 600);
    test->loadHtml(
        "<html>"
        "  <body>"
        "    <select id='single' size='3'>"
        "      <option>Option 1</option>"
        "      <option disabled>Option 2</option>"
        "      <option>Option 3</option>"
        "    </select>"
        "    <select id='multiple' size='3' multiple>"
        "      <option>Option 1</option>"
        "      <option selected>Option 2</option>"
        "      <option>Option 3</option>"
        "    </select>"
        "  </body>"
        "</html>",
        nullptr);
    test->waitUntilLoadFinished();

    auto testApp = test->findTestApplication();
    g_assert_true(ATSPI_IS_ACCESSIBLE(testApp.get()));

    auto documentWeb = test->findDocumentWeb(testApp.get());
    g_assert_true(ATSPI_IS_ACCESSIBLE(documentWeb.get()));
    g_assert_cmpint(atspi_accessible_get_child_count(documentWeb.get(), nullptr), ==, 1);

    auto panel = adoptGRef(atspi_accessible_get_child_at_index(documentWeb.get(), 0, nullptr));
    g_assert_true(ATSPI_IS_ACCESSIBLE(panel.get()));
    g_assert_cmpint(atspi_accessible_get_role(panel.get(), nullptr), ==, ATSPI_ROLE_PANEL);
    g_assert_cmpint(atspi_accessible_get_child_count(panel.get(), nullptr), ==, 2);

    test->runJavaScriptAndWaitUntilFinished("document.getElementById('single').focus();", nullptr);

    auto listBox = adoptGRef(atspi_accessible_get_child_at_index(panel.get(), 0, nullptr));
    g_assert_true(ATSPI_IS_SELECTION(listBox.get()));
    g_assert_cmpint(atspi_accessible_get_role(listBox.get(), nullptr), ==, ATSPI_ROLE_LIST_BOX);
    g_assert_cmpint(atspi_accessible_get_child_count(listBox.get(), nullptr), ==, 3);
    g_assert_cmpint(atspi_selection_get_n_selected_children(ATSPI_SELECTION(listBox.get()), nullptr), ==, 0);
    g_assert_false(atspi_selection_is_child_selected(ATSPI_SELECTION(listBox.get()), 0, nullptr));
    g_assert_false(atspi_selection_is_child_selected(ATSPI_SELECTION(listBox.get()), 1, nullptr));
    g_assert_false(atspi_selection_is_child_selected(ATSPI_SELECTION(listBox.get()), 2, nullptr));
    g_assert_false(atspi_selection_select_child(ATSPI_SELECTION(listBox.get()), 1, nullptr));
    g_assert_cmpint(atspi_selection_get_n_selected_children(ATSPI_SELECTION(listBox.get()), nullptr), ==, 0);
    g_assert_false(atspi_selection_is_child_selected(ATSPI_SELECTION(listBox.get()), 1, nullptr));
    test->startEventMonitor(listBox.get(), { "object:selection-changed" });
    g_assert_true(atspi_selection_select_child(ATSPI_SELECTION(listBox.get()), 0, nullptr));
    auto events = test->stopEventMonitor(1);
    g_assert_cmpuint(events.size(), ==, 1);
    g_assert_cmpstr(events[0]->type, ==, "object:selection-changed");
    events = { };
    g_assert_cmpint(atspi_selection_get_n_selected_children(ATSPI_SELECTION(listBox.get()), nullptr), ==, 1);
    g_assert_true(atspi_selection_is_child_selected(ATSPI_SELECTION(listBox.get()), 0, nullptr));
    auto selectedChild = adoptGRef(atspi_selection_get_selected_child(ATSPI_SELECTION(listBox.get()), 0, nullptr));
    g_assert_true(ATSPI_IS_ACCESSIBLE(selectedChild.get()));
    auto option1 = adoptGRef(atspi_accessible_get_child_at_index(listBox.get(), 0, nullptr));
    g_assert_true(selectedChild.get() == option1.get());
    g_assert_true(AccessibilityTest::isSelected(option1.get()));
    test->startEventMonitor(listBox.get(), { "object:selection-changed" });
    g_assert_true(atspi_selection_select_child(ATSPI_SELECTION(listBox.get()), 2, nullptr));
    events = test->stopEventMonitor(1);
    g_assert_cmpuint(events.size(), ==, 1);
    g_assert_cmpstr(events[0]->type, ==, "object:selection-changed");
    events = { };
    g_assert_cmpint(atspi_selection_get_n_selected_children(ATSPI_SELECTION(listBox.get()), nullptr), ==, 1);
    g_assert_true(atspi_selection_is_child_selected(ATSPI_SELECTION(listBox.get()), 2, nullptr));
    selectedChild = adoptGRef(atspi_selection_get_selected_child(ATSPI_SELECTION(listBox.get()), 0, nullptr));
    g_assert_true(ATSPI_IS_ACCESSIBLE(selectedChild.get()));
    auto option3 = adoptGRef(atspi_accessible_get_child_at_index(listBox.get(), 2, nullptr));
    g_assert_true(selectedChild.get() == option3.get());
    g_assert_true(AccessibilityTest::isSelected(option3.get()));
    g_assert_false(AccessibilityTest::isSelected(option1.get()));
    g_assert_false(atspi_selection_deselect_selected_child(ATSPI_SELECTION(listBox.get()), 1, nullptr));
    g_assert_cmpint(atspi_selection_get_n_selected_children(ATSPI_SELECTION(listBox.get()), nullptr), ==, 1);
    g_assert_true(atspi_selection_deselect_selected_child(ATSPI_SELECTION(listBox.get()), 0, nullptr));
    g_assert_cmpint(atspi_selection_get_n_selected_children(ATSPI_SELECTION(listBox.get()), nullptr), ==, 0);
    g_assert_false(atspi_selection_is_child_selected(ATSPI_SELECTION(listBox.get()), 2, nullptr));
    g_assert_false(atspi_selection_select_all(ATSPI_SELECTION(listBox.get()), nullptr));
    g_assert_false(AccessibilityTest::isSelected(option1.get()));
    g_assert_false(AccessibilityTest::isSelected(option3.get()));
    g_assert_true(atspi_selection_select_child(ATSPI_SELECTION(listBox.get()), 0, nullptr));
    g_assert_cmpint(atspi_selection_get_n_selected_children(ATSPI_SELECTION(listBox.get()), nullptr), ==, 1);
    g_assert_true(atspi_selection_is_child_selected(ATSPI_SELECTION(listBox.get()), 0, nullptr));
    test->startEventMonitor(listBox.get(), { "object:selection-changed" });
    g_assert_true(atspi_selection_clear_selection(ATSPI_SELECTION(listBox.get()), nullptr));
    events = test->stopEventMonitor(1);
    g_assert_cmpuint(events.size(), ==, 1);
    g_assert_cmpstr(events[0]->type, ==, "object:selection-changed");
    events = { };
    g_assert_cmpint(atspi_selection_get_n_selected_children(ATSPI_SELECTION(listBox.get()), nullptr), ==, 0);
    g_assert_false(atspi_selection_is_child_selected(ATSPI_SELECTION(listBox.get()), 0, nullptr));

    test->runJavaScriptAndWaitUntilFinished("document.getElementById('multiple').focus();", nullptr);

    listBox = adoptGRef(atspi_accessible_get_child_at_index(panel.get(), 1, nullptr));
    g_assert_true(ATSPI_IS_SELECTION(listBox.get()));
    g_assert_cmpint(atspi_accessible_get_role(listBox.get(), nullptr), ==, ATSPI_ROLE_LIST_BOX);
    g_assert_cmpint(atspi_accessible_get_child_count(listBox.get(), nullptr), ==, 3);
    g_assert_cmpint(atspi_selection_get_n_selected_children(ATSPI_SELECTION(listBox.get()), nullptr), ==, 1);
    g_assert_false(atspi_selection_is_child_selected(ATSPI_SELECTION(listBox.get()), 0, nullptr));
    g_assert_true(atspi_selection_is_child_selected(ATSPI_SELECTION(listBox.get()), 1, nullptr));
    g_assert_false(atspi_selection_is_child_selected(ATSPI_SELECTION(listBox.get()), 2, nullptr));
    selectedChild = adoptGRef(atspi_selection_get_selected_child(ATSPI_SELECTION(listBox.get()), 0, nullptr));
    g_assert_true(ATSPI_IS_ACCESSIBLE(selectedChild.get()));
    auto option2 = adoptGRef(atspi_accessible_get_child_at_index(listBox.get(), 1, nullptr));
    g_assert_true(selectedChild.get() == option2.get());
    g_assert_true(AccessibilityTest::isSelected(option2.get()));
    g_assert_true(atspi_selection_select_child(ATSPI_SELECTION(listBox.get()), 0, nullptr));
    g_assert_cmpint(atspi_selection_get_n_selected_children(ATSPI_SELECTION(listBox.get()), nullptr), ==, 2);
    g_assert_true(atspi_selection_is_child_selected(ATSPI_SELECTION(listBox.get()), 0, nullptr));
    g_assert_true(atspi_selection_is_child_selected(ATSPI_SELECTION(listBox.get()), 1, nullptr));
    selectedChild = adoptGRef(atspi_selection_get_selected_child(ATSPI_SELECTION(listBox.get()), 0, nullptr));
    option1 = adoptGRef(atspi_accessible_get_child_at_index(listBox.get(), 0, nullptr));
    g_assert_true(selectedChild.get() == option1.get());
    g_assert_true(AccessibilityTest::isSelected(option1.get()));
    g_assert_true(AccessibilityTest::isSelected(option2.get()));
    selectedChild = adoptGRef(atspi_selection_get_selected_child(ATSPI_SELECTION(listBox.get()), 1, nullptr));
    g_assert_true(selectedChild.get() == option2.get());
    g_assert_true(atspi_selection_deselect_child(ATSPI_SELECTION(listBox.get()), 1, nullptr));
    g_assert_cmpint(atspi_selection_get_n_selected_children(ATSPI_SELECTION(listBox.get()), nullptr), ==, 1);
    g_assert_true(atspi_selection_is_child_selected(ATSPI_SELECTION(listBox.get()), 0, nullptr));
    g_assert_false(atspi_selection_is_child_selected(ATSPI_SELECTION(listBox.get()), 1, nullptr));
    test->startEventMonitor(listBox.get(), { "object:selection-changed" });
    g_assert_true(atspi_selection_select_all(ATSPI_SELECTION(listBox.get()), nullptr));
    events = test->stopEventMonitor(1);
    g_assert_cmpuint(events.size(), ==, 1);
    g_assert_cmpstr(events[0]->type, ==, "object:selection-changed");
    events = { };
    g_assert_cmpint(atspi_selection_get_n_selected_children(ATSPI_SELECTION(listBox.get()), nullptr), ==, 3);
    g_assert_true(atspi_selection_is_child_selected(ATSPI_SELECTION(listBox.get()), 0, nullptr));
    g_assert_true(atspi_selection_is_child_selected(ATSPI_SELECTION(listBox.get()), 1, nullptr));
    g_assert_true(atspi_selection_is_child_selected(ATSPI_SELECTION(listBox.get()), 2, nullptr));
    g_assert_true(atspi_selection_clear_selection(ATSPI_SELECTION(listBox.get()), nullptr));
    g_assert_cmpint(atspi_selection_get_n_selected_children(ATSPI_SELECTION(listBox.get()), nullptr), ==, 0);
    g_assert_false(atspi_selection_is_child_selected(ATSPI_SELECTION(listBox.get()), 0, nullptr));
    g_assert_false(atspi_selection_is_child_selected(ATSPI_SELECTION(listBox.get()), 1, nullptr));
    g_assert_false(atspi_selection_is_child_selected(ATSPI_SELECTION(listBox.get()), 2, nullptr));
}

static void testSelectionMenuList(AccessibilityTest* test, gconstpointer)
{
    test->showInWindow(800, 600);
    test->loadHtml(
        "<html>"
        "  <body>"
        "    <select id='combo'>"
        "      <option>Option 1</option>"
        "      <option selected>Option 2</option>"
        "      <option>Option 3</option>"
        "      <option disabled>Option 4</option>"
        "      <option>Option 5</option>"
        "    </select>"
        "  </body>"
        "</html>",
        nullptr);
    test->waitUntilLoadFinished();

    auto testApp = test->findTestApplication();
    g_assert_true(ATSPI_IS_ACCESSIBLE(testApp.get()));

    auto documentWeb = test->findDocumentWeb(testApp.get());
    g_assert_true(ATSPI_IS_ACCESSIBLE(documentWeb.get()));
    g_assert_cmpint(atspi_accessible_get_child_count(documentWeb.get(), nullptr), ==, 1);

    auto panel = adoptGRef(atspi_accessible_get_child_at_index(documentWeb.get(), 0, nullptr));
    g_assert_true(ATSPI_IS_ACCESSIBLE(panel.get()));
    g_assert_cmpint(atspi_accessible_get_role(panel.get(), nullptr), ==, ATSPI_ROLE_PANEL);
    g_assert_cmpint(atspi_accessible_get_child_count(panel.get(), nullptr), ==, 1);

    test->runJavaScriptAndWaitUntilFinished("document.getElementById('combo').focus();", nullptr);

    auto combo = adoptGRef(atspi_accessible_get_child_at_index(panel.get(), 0, nullptr));
    g_assert_true(ATSPI_IS_ACCESSIBLE(combo.get()));
    g_assert_cmpint(atspi_accessible_get_role(combo.get(), nullptr), ==, ATSPI_ROLE_COMBO_BOX);
    g_assert_cmpint(atspi_accessible_get_child_count(combo.get(), nullptr), ==, 1);

    auto menuList = adoptGRef(atspi_accessible_get_child_at_index(combo.get(), 0, nullptr));
    g_assert_true(ATSPI_IS_SELECTION(menuList.get()));
    g_assert_cmpint(atspi_accessible_get_role(menuList.get(), nullptr), ==, ATSPI_ROLE_MENU);
    g_assert_cmpint(atspi_accessible_get_child_count(menuList.get(), nullptr), ==, 5);

    g_assert_cmpint(atspi_selection_get_n_selected_children(ATSPI_SELECTION(menuList.get()), nullptr), ==, 1);
    g_assert_false(atspi_selection_is_child_selected(ATSPI_SELECTION(menuList.get()), 0, nullptr));
    g_assert_true(atspi_selection_is_child_selected(ATSPI_SELECTION(menuList.get()), 1, nullptr));
    g_assert_false(atspi_selection_is_child_selected(ATSPI_SELECTION(menuList.get()), 2, nullptr));
    g_assert_false(atspi_selection_is_child_selected(ATSPI_SELECTION(menuList.get()), 3, nullptr));
    g_assert_false(atspi_selection_is_child_selected(ATSPI_SELECTION(menuList.get()), 4, nullptr));
    auto selectedChild = adoptGRef(atspi_selection_get_selected_child(ATSPI_SELECTION(menuList.get()), 0, nullptr));
    g_assert_true(ATSPI_IS_ACCESSIBLE(selectedChild.get()));
    auto option2 = adoptGRef(atspi_accessible_get_child_at_index(menuList.get(), 1, nullptr));
    g_assert_true(selectedChild.get() == option2.get());
    g_assert_true(AccessibilityTest::isSelected(option2.get()));
    auto option1 = adoptGRef(atspi_accessible_get_child_at_index(menuList.get(), 0, nullptr));
    g_assert_false(AccessibilityTest::isSelected(option1.get()));
    g_assert_false(atspi_selection_select_child(ATSPI_SELECTION(menuList.get()), 3, nullptr));
    test->startEventMonitor(menuList.get(), { "object:selection-changed" });
    g_assert_true(atspi_selection_select_child(ATSPI_SELECTION(menuList.get()), 0, nullptr));
    auto events = test->stopEventMonitor(1);
    g_assert_cmpuint(events.size(), ==, 1);
    g_assert_cmpstr(events[0]->type, ==, "object:selection-changed");
    events = { };
    g_assert_true(atspi_selection_is_child_selected(ATSPI_SELECTION(menuList.get()), 0, nullptr));
    selectedChild = adoptGRef(atspi_selection_get_selected_child(ATSPI_SELECTION(menuList.get()), 0, nullptr));
    g_assert_true(ATSPI_IS_ACCESSIBLE(selectedChild.get()));
    g_assert_true(selectedChild.get() == option1.get());
    g_assert_true(AccessibilityTest::isSelected(option1.get()));
    g_assert_false(AccessibilityTest::isSelected(option2.get()));
    g_assert_false(atspi_selection_deselect_selected_child(ATSPI_SELECTION(menuList.get()), 0, nullptr));
    g_assert_false(atspi_selection_deselect_child(ATSPI_SELECTION(menuList.get()), 0, nullptr));
    g_assert_false(atspi_selection_select_all(ATSPI_SELECTION(menuList.get()), nullptr));
    g_assert_false(atspi_selection_clear_selection(ATSPI_SELECTION(menuList.get()), nullptr));
}

static void testTableBasic(AccessibilityTest* test, gconstpointer)
{
    test->showInWindow(800, 600);
    test->loadHtml(
        "<html>"
        "  <body>"
        "    <table>"
        "      <caption>Table Caption</caption>"
        "      <tr><th>Column 1</th><th>Column 2</th><th>Column 3</th></tr>"
        "      <tr><th rowspan='2'>Row 1 Cell 1</th><td>Row 1 Cell 2</td><td>Row 1 Cell 3</td></tr>"
        "      <tr><td>Row 2 Cell 2</td><td>Row 2 Cell 3</td></tr>"
        "      <tr><td colspan='3'>Row 3 Cell 1</td></tr>"
        "    </table>"
        "  </body>"
        "</html>",
        nullptr);
    test->waitUntilLoadFinished();

    auto testApp = test->findTestApplication();
    g_assert_true(ATSPI_IS_ACCESSIBLE(testApp.get()));

    auto documentWeb = test->findDocumentWeb(testApp.get());
    g_assert_true(ATSPI_IS_ACCESSIBLE(documentWeb.get()));
    g_assert_cmpint(atspi_accessible_get_child_count(documentWeb.get(), nullptr), ==, 1);

    auto table = adoptGRef(atspi_accessible_get_child_at_index(documentWeb.get(), 0, nullptr));
    g_assert_true(ATSPI_IS_TABLE(table.get()));
    g_assert_cmpint(atspi_accessible_get_role(table.get(), nullptr), ==, ATSPI_ROLE_TABLE);
    g_assert_cmpint(atspi_accessible_get_child_count(table.get(), nullptr), ==, 5);
    g_assert_cmpint(atspi_table_get_n_rows(ATSPI_TABLE(table.get()), nullptr), ==, 4);
    g_assert_cmpint(atspi_table_get_n_columns(ATSPI_TABLE(table.get()), nullptr), ==, 3);

    auto caption = adoptGRef(atspi_table_get_caption(ATSPI_TABLE(table.get()), nullptr));
    g_assert_true(ATSPI_IS_ACCESSIBLE(caption.get()));
    g_assert_cmpint(atspi_accessible_get_role(caption.get(), nullptr), ==, ATSPI_ROLE_CAPTION);
    g_assert_true(ATSPI_IS_TEXT(caption.get()));
    GUniquePtr<char> text(atspi_text_get_text(ATSPI_TEXT(caption.get()), 0, -1, nullptr));
    g_assert_cmpstr(text.get(), ==, "Table Caption");

    text.reset(atspi_table_get_row_description(ATSPI_TABLE(table.get()), 0, nullptr));
    g_assert_cmpstr(text.get(), ==, "");
    g_assert_null(atspi_table_get_row_header(ATSPI_TABLE(table.get()), 0, nullptr));
    text.reset(atspi_table_get_row_description(ATSPI_TABLE(table.get()), 1, nullptr));
    g_assert_cmpstr(text.get(), ==, "Row 1 Cell 1");
    auto rowHeader = adoptGRef(atspi_table_get_row_header(ATSPI_TABLE(table.get()), 1, nullptr));
    g_assert_true(ATSPI_IS_ACCESSIBLE(rowHeader.get()));
    text.reset(atspi_table_get_row_description(ATSPI_TABLE(table.get()), 2, nullptr));
    g_assert_cmpstr(text.get(), ==, "Row 1 Cell 1");
    auto header = adoptGRef(atspi_table_get_row_header(ATSPI_TABLE(table.get()), 2, nullptr));
    g_assert_true(rowHeader.get() == header.get());
    text.reset(atspi_table_get_row_description(ATSPI_TABLE(table.get()), 3, nullptr));
    g_assert_cmpstr(text.get(), ==, "");
    g_assert_null(atspi_table_get_row_header(ATSPI_TABLE(table.get()), 3, nullptr));

    text.reset(atspi_table_get_column_description(ATSPI_TABLE(table.get()), 0, nullptr));
    g_assert_cmpstr(text.get(), ==, "Column 1");
    auto columnHeader0 = adoptGRef(atspi_table_get_column_header(ATSPI_TABLE(table.get()), 0, nullptr));
    g_assert_true(ATSPI_IS_ACCESSIBLE(columnHeader0.get()));
    text.reset(atspi_table_get_column_description(ATSPI_TABLE(table.get()), 1, nullptr));
    g_assert_cmpstr(text.get(), ==, "Column 2");
    auto columnHeader1 = adoptGRef(atspi_table_get_column_header(ATSPI_TABLE(table.get()), 1, nullptr));
    g_assert_true(ATSPI_IS_ACCESSIBLE(columnHeader1.get()));
    text.reset(atspi_table_get_column_description(ATSPI_TABLE(table.get()), 2, nullptr));
    g_assert_cmpstr(text.get(), ==, "Column 3");
    auto columnHeader2 = adoptGRef(atspi_table_get_column_header(ATSPI_TABLE(table.get()), 2, nullptr));
    g_assert_true(ATSPI_IS_ACCESSIBLE(columnHeader2.get()));

    g_assert_cmpint(atspi_table_get_index_at(ATSPI_TABLE(table.get()), 0, 0, nullptr), ==, 0);
    g_assert_cmpint(atspi_table_get_row_at_index(ATSPI_TABLE(table.get()), 0, nullptr), ==, 0);
    g_assert_cmpint(atspi_table_get_column_at_index(ATSPI_TABLE(table.get()), 0, nullptr), ==, 0);
    g_assert_cmpint(atspi_table_get_row_extent_at(ATSPI_TABLE(table.get()), 0, 0, nullptr), ==, 1);
    g_assert_cmpint(atspi_table_get_column_extent_at(ATSPI_TABLE(table.get()), 0, 0, nullptr), ==, 1);
    int row, column, rowSpan, columnSpan;
    gboolean isSelected;
    g_assert_true(atspi_table_get_row_column_extents_at_index(ATSPI_TABLE(table.get()), 0, &row, &column, &rowSpan, &columnSpan, &isSelected, nullptr));
    g_assert_cmpint(row, ==, 0);
    g_assert_cmpint(column, ==, 0);
    g_assert_cmpint(rowSpan, ==, 1);
    g_assert_cmpint(columnSpan, ==, 1);
    g_assert_false(isSelected);
    auto cell0 = adoptGRef(atspi_table_get_accessible_at(ATSPI_TABLE(table.get()), 0, 0, nullptr));
    g_assert_true(ATSPI_IS_TABLE_CELL(cell0.get()));
    g_assert_true(cell0.get() == columnHeader0.get());
    g_assert_cmpint(atspi_table_cell_get_row_span(ATSPI_TABLE_CELL(cell0.get()), nullptr), ==, 1);
    g_assert_cmpint(atspi_table_cell_get_column_span(ATSPI_TABLE_CELL(cell0.get()), nullptr), ==, 1);
    g_assert_true(atspi_table_cell_get_position(ATSPI_TABLE_CELL(cell0.get()), &row, &column, nullptr));
    g_assert_cmpint(row, ==, 0);
    g_assert_cmpint(column, ==, 0);
    atspi_table_cell_get_row_column_span(ATSPI_TABLE_CELL(cell0.get()), &row, &column, &rowSpan, &columnSpan, nullptr);
    g_assert_cmpint(row, ==, 0);
    g_assert_cmpint(column, ==, 0);
    g_assert_cmpint(rowSpan, ==, 1);
    g_assert_cmpint(columnSpan, ==, 1);
    auto cellTable = adoptGRef(atspi_table_cell_get_table(ATSPI_TABLE_CELL(cell0.get()), nullptr));
    g_assert_true(table.get() == cellTable.get());
    GRefPtr<GPtrArray> rowHeaders = adoptGRef(atspi_table_cell_get_row_header_cells(ATSPI_TABLE_CELL(cell0.get()), nullptr));
    g_assert_cmpint(rowHeaders->len, ==, 0);
    GRefPtr<GPtrArray> columnHeaders = adoptGRef(atspi_table_cell_get_column_header_cells(ATSPI_TABLE_CELL(cell0.get()), nullptr));
    g_assert_cmpint(columnHeaders->len, ==, 0);
    g_assert_true(ATSPI_IS_TEXT(cell0.get()));
    text.reset(atspi_text_get_text(ATSPI_TEXT(cell0.get()), 0, -1, nullptr));
    g_assert_cmpstr(text.get(), ==, "Column 1");

    g_assert_cmpint(atspi_table_get_index_at(ATSPI_TABLE(table.get()), 0, 1, nullptr), ==, 1);
    g_assert_cmpint(atspi_table_get_row_at_index(ATSPI_TABLE(table.get()), 1, nullptr), ==, 0);
    g_assert_cmpint(atspi_table_get_column_at_index(ATSPI_TABLE(table.get()), 1, nullptr), ==, 1);
    g_assert_cmpint(atspi_table_get_row_extent_at(ATSPI_TABLE(table.get()), 0, 1, nullptr), ==, 1);
    g_assert_cmpint(atspi_table_get_column_extent_at(ATSPI_TABLE(table.get()), 0, 1, nullptr), ==, 1);
    g_assert_true(atspi_table_get_row_column_extents_at_index(ATSPI_TABLE(table.get()), 1, &row, &column, &rowSpan, &columnSpan, &isSelected, nullptr));
    g_assert_cmpint(row, ==, 0);
    g_assert_cmpint(column, ==, 1);
    g_assert_cmpint(rowSpan, ==, 1);
    g_assert_cmpint(columnSpan, ==, 1);
    g_assert_false(isSelected);
    auto cell1 = adoptGRef(atspi_table_get_accessible_at(ATSPI_TABLE(table.get()), 0, 1, nullptr));
    g_assert_true(ATSPI_IS_TABLE_CELL(cell1.get()));
    g_assert_true(cell1.get() == columnHeader1.get());
    g_assert_cmpint(atspi_table_cell_get_row_span(ATSPI_TABLE_CELL(cell1.get()), nullptr), ==, 1);
    g_assert_cmpint(atspi_table_cell_get_column_span(ATSPI_TABLE_CELL(cell1.get()), nullptr), ==, 1);
    g_assert_true(atspi_table_cell_get_position(ATSPI_TABLE_CELL(cell1.get()), &row, &column, nullptr));
    g_assert_cmpint(row, ==, 0);
    g_assert_cmpint(column, ==, 1);
    atspi_table_cell_get_row_column_span(ATSPI_TABLE_CELL(cell1.get()), &row, &column, &rowSpan, &columnSpan, nullptr);
    g_assert_cmpint(row, ==, 0);
    g_assert_cmpint(column, ==, 1);
    g_assert_cmpint(rowSpan, ==, 1);
    g_assert_cmpint(columnSpan, ==, 1);
    cellTable = adoptGRef(atspi_table_cell_get_table(ATSPI_TABLE_CELL(cell1.get()), nullptr));
    g_assert_true(table.get() == cellTable.get());
    rowHeaders = adoptGRef(atspi_table_cell_get_row_header_cells(ATSPI_TABLE_CELL(cell1.get()), nullptr));
    g_assert_cmpint(rowHeaders->len, ==, 1);
    g_assert_true(rowHeaders->pdata[0] == columnHeader0.get());
    g_ptr_array_foreach(rowHeaders.get(), reinterpret_cast<GFunc>(reinterpret_cast<GCallback>(g_object_unref)), nullptr);
    columnHeaders = adoptGRef(atspi_table_cell_get_column_header_cells(ATSPI_TABLE_CELL(cell1.get()), nullptr));
    g_assert_cmpint(columnHeaders->len, ==, 0);
    g_assert_true(ATSPI_IS_TEXT(cell1.get()));
    text.reset(atspi_text_get_text(ATSPI_TEXT(cell1.get()), 0, -1, nullptr));
    g_assert_cmpstr(text.get(), ==, "Column 2");

    g_assert_cmpint(atspi_table_get_index_at(ATSPI_TABLE(table.get()), 0, 2, nullptr), ==, 2);
    g_assert_cmpint(atspi_table_get_row_at_index(ATSPI_TABLE(table.get()), 2, nullptr), ==, 0);
    g_assert_cmpint(atspi_table_get_column_at_index(ATSPI_TABLE(table.get()), 2, nullptr), ==, 2);
    g_assert_cmpint(atspi_table_get_row_extent_at(ATSPI_TABLE(table.get()), 0, 2, nullptr), ==, 1);
    g_assert_cmpint(atspi_table_get_column_extent_at(ATSPI_TABLE(table.get()), 0, 2, nullptr), ==, 1);
    g_assert_true(atspi_table_get_row_column_extents_at_index(ATSPI_TABLE(table.get()), 2, &row, &column, &rowSpan, &columnSpan, &isSelected, nullptr));
    g_assert_cmpint(row, ==, 0);
    g_assert_cmpint(column, ==, 2);
    g_assert_cmpint(rowSpan, ==, 1);
    g_assert_cmpint(columnSpan, ==, 1);
    g_assert_false(isSelected);
    auto cell2 = adoptGRef(atspi_table_get_accessible_at(ATSPI_TABLE(table.get()), 0, 2, nullptr));
    g_assert_true(ATSPI_IS_TABLE_CELL(cell2.get()));
    g_assert_true(cell2.get() == columnHeader2.get());
    g_assert_cmpint(atspi_table_cell_get_row_span(ATSPI_TABLE_CELL(cell2.get()), nullptr), ==, 1);
    g_assert_cmpint(atspi_table_cell_get_column_span(ATSPI_TABLE_CELL(cell2.get()), nullptr), ==, 1);
    g_assert_true(atspi_table_cell_get_position(ATSPI_TABLE_CELL(cell2.get()), &row, &column, nullptr));
    g_assert_cmpint(row, ==, 0);
    g_assert_cmpint(column, ==, 2);
    atspi_table_cell_get_row_column_span(ATSPI_TABLE_CELL(cell2.get()), &row, &column, &rowSpan, &columnSpan, nullptr);
    g_assert_cmpint(row, ==, 0);
    g_assert_cmpint(column, ==, 2);
    g_assert_cmpint(rowSpan, ==, 1);
    g_assert_cmpint(columnSpan, ==, 1);
    cellTable = adoptGRef(atspi_table_cell_get_table(ATSPI_TABLE_CELL(cell2.get()), nullptr));
    g_assert_true(table.get() == cellTable.get());
    rowHeaders = adoptGRef(atspi_table_cell_get_row_header_cells(ATSPI_TABLE_CELL(cell2.get()), nullptr));
    g_assert_cmpint(rowHeaders->len, ==, 1);
    g_assert_true(rowHeaders->pdata[0] == columnHeader0.get());
    g_ptr_array_foreach(rowHeaders.get(), reinterpret_cast<GFunc>(reinterpret_cast<GCallback>(g_object_unref)), nullptr);
    columnHeaders = adoptGRef(atspi_table_cell_get_column_header_cells(ATSPI_TABLE_CELL(cell2.get()), nullptr));
    g_assert_cmpint(columnHeaders->len, ==, 0);
    g_assert_true(ATSPI_IS_TEXT(cell2.get()));
    text.reset(atspi_text_get_text(ATSPI_TEXT(cell2.get()), 0, -1, nullptr));
    g_assert_cmpstr(text.get(), ==, "Column 3");

    g_assert_cmpint(atspi_table_get_index_at(ATSPI_TABLE(table.get()), 1, 0, nullptr), ==, 3);
    g_assert_cmpint(atspi_table_get_row_at_index(ATSPI_TABLE(table.get()), 3, nullptr), ==, 1);
    g_assert_cmpint(atspi_table_get_column_at_index(ATSPI_TABLE(table.get()), 3, nullptr), ==, 0);
    g_assert_cmpint(atspi_table_get_row_extent_at(ATSPI_TABLE(table.get()), 1, 0, nullptr), ==, 2);
    g_assert_cmpint(atspi_table_get_column_extent_at(ATSPI_TABLE(table.get()), 1, 0, nullptr), ==, 1);
    g_assert_true(atspi_table_get_row_column_extents_at_index(ATSPI_TABLE(table.get()), 3, &row, &column, &rowSpan, &columnSpan, &isSelected, nullptr));
    g_assert_cmpint(row, ==, 1);
    g_assert_cmpint(column, ==, 0);
    g_assert_cmpint(rowSpan, ==, 2);
    g_assert_cmpint(columnSpan, ==, 1);
    g_assert_false(isSelected);
    auto cell3 = adoptGRef(atspi_table_get_accessible_at(ATSPI_TABLE(table.get()), 1, 0, nullptr));
    g_assert_true(ATSPI_IS_TABLE_CELL(cell3.get()));
    g_assert_true(cell3.get() == rowHeader.get());
    g_assert_cmpint(atspi_table_cell_get_row_span(ATSPI_TABLE_CELL(cell3.get()), nullptr), ==, 2);
    g_assert_cmpint(atspi_table_cell_get_column_span(ATSPI_TABLE_CELL(cell3.get()), nullptr), ==, 1);
    g_assert_true(atspi_table_cell_get_position(ATSPI_TABLE_CELL(cell3.get()), &row, &column, nullptr));
    g_assert_cmpint(row, ==, 1);
    g_assert_cmpint(column, ==, 0);
    atspi_table_cell_get_row_column_span(ATSPI_TABLE_CELL(cell3.get()), &row, &column, &rowSpan, &columnSpan, nullptr);
    g_assert_cmpint(row, ==, 1);
    g_assert_cmpint(column, ==, 0);
    g_assert_cmpint(rowSpan, ==, 2);
    g_assert_cmpint(columnSpan, ==, 1);
    cellTable = adoptGRef(atspi_table_cell_get_table(ATSPI_TABLE_CELL(cell3.get()), nullptr));
    g_assert_true(table.get() == cellTable.get());
    rowHeaders = adoptGRef(atspi_table_cell_get_row_header_cells(ATSPI_TABLE_CELL(cell3.get()), nullptr));
    g_assert_cmpint(rowHeaders->len, ==, 0);
    columnHeaders = adoptGRef(atspi_table_cell_get_column_header_cells(ATSPI_TABLE_CELL(cell3.get()), nullptr));
    g_assert_cmpint(columnHeaders->len, ==, 1);
    g_assert_true(columnHeaders->pdata[0] == columnHeader0.get());
    g_ptr_array_foreach(columnHeaders.get(), reinterpret_cast<GFunc>(reinterpret_cast<GCallback>(g_object_unref)), nullptr);
    g_assert_true(ATSPI_IS_TEXT(cell3.get()));
    text.reset(atspi_text_get_text(ATSPI_TEXT(cell3.get()), 0, -1, nullptr));
    g_assert_cmpstr(text.get(), ==, "Row 1 Cell 1");

    g_assert_cmpint(atspi_table_get_index_at(ATSPI_TABLE(table.get()), 1, 1, nullptr), ==, 4);
    g_assert_cmpint(atspi_table_get_row_at_index(ATSPI_TABLE(table.get()), 4, nullptr), ==, 1);
    g_assert_cmpint(atspi_table_get_column_at_index(ATSPI_TABLE(table.get()), 4, nullptr), ==, 1);
    g_assert_cmpint(atspi_table_get_row_extent_at(ATSPI_TABLE(table.get()), 1, 1, nullptr), ==, 1);
    g_assert_cmpint(atspi_table_get_column_extent_at(ATSPI_TABLE(table.get()), 1, 1, nullptr), ==, 1);
    g_assert_true(atspi_table_get_row_column_extents_at_index(ATSPI_TABLE(table.get()), 4, &row, &column, &rowSpan, &columnSpan, &isSelected, nullptr));
    g_assert_cmpint(row, ==, 1);
    g_assert_cmpint(column, ==, 1);
    g_assert_cmpint(rowSpan, ==, 1);
    g_assert_cmpint(columnSpan, ==, 1);
    g_assert_false(isSelected);
    auto cell4 = adoptGRef(atspi_table_get_accessible_at(ATSPI_TABLE(table.get()), 1, 1, nullptr));
    g_assert_true(ATSPI_IS_TABLE_CELL(cell4.get()));
    g_assert_cmpint(atspi_table_cell_get_row_span(ATSPI_TABLE_CELL(cell4.get()), nullptr), ==, 1);
    g_assert_cmpint(atspi_table_cell_get_column_span(ATSPI_TABLE_CELL(cell4.get()), nullptr), ==, 1);
    g_assert_true(atspi_table_cell_get_position(ATSPI_TABLE_CELL(cell4.get()), &row, &column, nullptr));
    g_assert_cmpint(row, ==, 1);
    g_assert_cmpint(column, ==, 1);
    atspi_table_cell_get_row_column_span(ATSPI_TABLE_CELL(cell4.get()), &row, &column, &rowSpan, &columnSpan, nullptr);
    g_assert_cmpint(row, ==, 1);
    g_assert_cmpint(column, ==, 1);
    g_assert_cmpint(rowSpan, ==, 1);
    g_assert_cmpint(columnSpan, ==, 1);
    cellTable = adoptGRef(atspi_table_cell_get_table(ATSPI_TABLE_CELL(cell4.get()), nullptr));
    g_assert_true(table.get() == cellTable.get());
    rowHeaders = adoptGRef(atspi_table_cell_get_row_header_cells(ATSPI_TABLE_CELL(cell4.get()), nullptr));
    g_assert_cmpint(rowHeaders->len, ==, 1);
    g_assert_true(rowHeaders->pdata[0] == rowHeader.get());
    g_ptr_array_foreach(rowHeaders.get(), reinterpret_cast<GFunc>(reinterpret_cast<GCallback>(g_object_unref)), nullptr);
    columnHeaders = adoptGRef(atspi_table_cell_get_column_header_cells(ATSPI_TABLE_CELL(cell4.get()), nullptr));
    g_assert_cmpint(columnHeaders->len, ==, 1);
    g_assert_true(columnHeaders->pdata[0] == columnHeader1.get());
    g_ptr_array_foreach(columnHeaders.get(), reinterpret_cast<GFunc>(reinterpret_cast<GCallback>(g_object_unref)), nullptr);
    g_assert_true(ATSPI_IS_TEXT(cell4.get()));
    text.reset(atspi_text_get_text(ATSPI_TEXT(cell4.get()), 0, -1, nullptr));
    g_assert_cmpstr(text.get(), ==, "Row 1 Cell 2");

    g_assert_cmpint(atspi_table_get_index_at(ATSPI_TABLE(table.get()), 1, 2, nullptr), ==, 5);
    g_assert_cmpint(atspi_table_get_row_at_index(ATSPI_TABLE(table.get()), 5, nullptr), ==, 1);
    g_assert_cmpint(atspi_table_get_column_at_index(ATSPI_TABLE(table.get()), 5, nullptr), ==, 2);
    g_assert_cmpint(atspi_table_get_row_extent_at(ATSPI_TABLE(table.get()), 1, 2, nullptr), ==, 1);
    g_assert_cmpint(atspi_table_get_column_extent_at(ATSPI_TABLE(table.get()), 1, 2, nullptr), ==, 1);
    g_assert_true(atspi_table_get_row_column_extents_at_index(ATSPI_TABLE(table.get()), 5, &row, &column, &rowSpan, &columnSpan, &isSelected, nullptr));
    g_assert_cmpint(row, ==, 1);
    g_assert_cmpint(column, ==, 2);
    g_assert_cmpint(rowSpan, ==, 1);
    g_assert_cmpint(columnSpan, ==, 1);
    g_assert_false(isSelected);
    auto cell5 = adoptGRef(atspi_table_get_accessible_at(ATSPI_TABLE(table.get()), 1, 2, nullptr));
    g_assert_true(ATSPI_IS_TABLE_CELL(cell5.get()));
    g_assert_cmpint(atspi_table_cell_get_row_span(ATSPI_TABLE_CELL(cell5.get()), nullptr), ==, 1);
    g_assert_cmpint(atspi_table_cell_get_column_span(ATSPI_TABLE_CELL(cell5.get()), nullptr), ==, 1);
    g_assert_true(atspi_table_cell_get_position(ATSPI_TABLE_CELL(cell5.get()), &row, &column, nullptr));
    g_assert_cmpint(row, ==, 1);
    g_assert_cmpint(column, ==, 2);
    atspi_table_cell_get_row_column_span(ATSPI_TABLE_CELL(cell5.get()), &row, &column, &rowSpan, &columnSpan, nullptr);
    g_assert_cmpint(row, ==, 1);
    g_assert_cmpint(column, ==, 2);
    g_assert_cmpint(rowSpan, ==, 1);
    g_assert_cmpint(columnSpan, ==, 1);
    cellTable = adoptGRef(atspi_table_cell_get_table(ATSPI_TABLE_CELL(cell5.get()), nullptr));
    g_assert_true(table.get() == cellTable.get());
    rowHeaders = adoptGRef(atspi_table_cell_get_row_header_cells(ATSPI_TABLE_CELL(cell5.get()), nullptr));
    g_assert_cmpint(rowHeaders->len, ==, 1);
    g_assert_true(rowHeaders->pdata[0] == rowHeader.get());
    g_ptr_array_foreach(rowHeaders.get(), reinterpret_cast<GFunc>(reinterpret_cast<GCallback>(g_object_unref)), nullptr);
    columnHeaders = adoptGRef(atspi_table_cell_get_column_header_cells(ATSPI_TABLE_CELL(cell5.get()), nullptr));
    g_assert_cmpint(columnHeaders->len, ==, 1);
    g_assert_true(columnHeaders->pdata[0] == columnHeader2.get());
    g_ptr_array_foreach(columnHeaders.get(), reinterpret_cast<GFunc>(reinterpret_cast<GCallback>(g_object_unref)), nullptr);
    g_assert_true(ATSPI_IS_TEXT(cell5.get()));
    text.reset(atspi_text_get_text(ATSPI_TEXT(cell5.get()), 0, -1, nullptr));
    g_assert_cmpstr(text.get(), ==, "Row 1 Cell 3");

    g_assert_cmpint(atspi_table_get_index_at(ATSPI_TABLE(table.get()), 2, 0, nullptr), ==, 3);
    g_assert_cmpint(atspi_table_get_row_extent_at(ATSPI_TABLE(table.get()), 2, 0, nullptr), ==, 2);
    g_assert_cmpint(atspi_table_get_column_extent_at(ATSPI_TABLE(table.get()), 2, 0, nullptr), ==, 1);
    auto cell = adoptGRef(atspi_table_get_accessible_at(ATSPI_TABLE(table.get()), 2, 0, nullptr));
    g_assert_true(cell3.get() == cell.get());

    g_assert_cmpint(atspi_table_get_index_at(ATSPI_TABLE(table.get()), 2, 1, nullptr), ==, 6);
    g_assert_cmpint(atspi_table_get_row_at_index(ATSPI_TABLE(table.get()), 6, nullptr), ==, 2);
    g_assert_cmpint(atspi_table_get_column_at_index(ATSPI_TABLE(table.get()), 6, nullptr), ==, 1);
    g_assert_cmpint(atspi_table_get_row_extent_at(ATSPI_TABLE(table.get()), 2, 1, nullptr), ==, 1);
    g_assert_cmpint(atspi_table_get_column_extent_at(ATSPI_TABLE(table.get()), 2, 1, nullptr), ==, 1);
    g_assert_true(atspi_table_get_row_column_extents_at_index(ATSPI_TABLE(table.get()), 6, &row, &column, &rowSpan, &columnSpan, &isSelected, nullptr));
    g_assert_cmpint(row, ==, 2);
    g_assert_cmpint(column, ==, 1);
    g_assert_cmpint(rowSpan, ==, 1);
    g_assert_cmpint(columnSpan, ==, 1);
    g_assert_false(isSelected);
    auto cell6 = adoptGRef(atspi_table_get_accessible_at(ATSPI_TABLE(table.get()), 2, 1, nullptr));
    g_assert_true(ATSPI_IS_TABLE_CELL(cell6.get()));
    g_assert_cmpint(atspi_table_cell_get_row_span(ATSPI_TABLE_CELL(cell6.get()), nullptr), ==, 1);
    g_assert_cmpint(atspi_table_cell_get_column_span(ATSPI_TABLE_CELL(cell6.get()), nullptr), ==, 1);
    g_assert_true(atspi_table_cell_get_position(ATSPI_TABLE_CELL(cell6.get()), &row, &column, nullptr));
    g_assert_cmpint(row, ==, 2);
    g_assert_cmpint(column, ==, 1);
    atspi_table_cell_get_row_column_span(ATSPI_TABLE_CELL(cell6.get()), &row, &column, &rowSpan, &columnSpan, nullptr);
    g_assert_cmpint(row, ==, 2);
    g_assert_cmpint(column, ==, 1);
    g_assert_cmpint(rowSpan, ==, 1);
    g_assert_cmpint(columnSpan, ==, 1);
    cellTable = adoptGRef(atspi_table_cell_get_table(ATSPI_TABLE_CELL(cell6.get()), nullptr));
    g_assert_true(table.get() == cellTable.get());
    rowHeaders = adoptGRef(atspi_table_cell_get_row_header_cells(ATSPI_TABLE_CELL(cell6.get()), nullptr));
    g_assert_cmpint(rowHeaders->len, ==, 1);
    g_assert_true(rowHeaders->pdata[0] == rowHeader.get());
    g_ptr_array_foreach(rowHeaders.get(), reinterpret_cast<GFunc>(reinterpret_cast<GCallback>(g_object_unref)), nullptr);
    columnHeaders = adoptGRef(atspi_table_cell_get_column_header_cells(ATSPI_TABLE_CELL(cell6.get()), nullptr));
    g_assert_cmpint(columnHeaders->len, ==, 1);
    g_assert_true(columnHeaders->pdata[0] == columnHeader1.get());
    g_ptr_array_foreach(columnHeaders.get(), reinterpret_cast<GFunc>(reinterpret_cast<GCallback>(g_object_unref)), nullptr);
    g_assert_true(ATSPI_IS_TEXT(cell6.get()));
    text.reset(atspi_text_get_text(ATSPI_TEXT(cell6.get()), 0, -1, nullptr));
    g_assert_cmpstr(text.get(), ==, "Row 2 Cell 2");

    g_assert_cmpint(atspi_table_get_index_at(ATSPI_TABLE(table.get()), 2, 2, nullptr), ==, 7);
    g_assert_cmpint(atspi_table_get_row_at_index(ATSPI_TABLE(table.get()), 7, nullptr), ==, 2);
    g_assert_cmpint(atspi_table_get_column_at_index(ATSPI_TABLE(table.get()), 7, nullptr), ==, 2);
    g_assert_cmpint(atspi_table_get_row_extent_at(ATSPI_TABLE(table.get()), 2, 2, nullptr), ==, 1);
    g_assert_cmpint(atspi_table_get_column_extent_at(ATSPI_TABLE(table.get()), 2, 2, nullptr), ==, 1);
    g_assert_true(atspi_table_get_row_column_extents_at_index(ATSPI_TABLE(table.get()), 7, &row, &column, &rowSpan, &columnSpan, &isSelected, nullptr));
    g_assert_cmpint(row, ==, 2);
    g_assert_cmpint(column, ==, 2);
    g_assert_cmpint(rowSpan, ==, 1);
    g_assert_cmpint(columnSpan, ==, 1);
    g_assert_false(isSelected);
    auto cell7 = adoptGRef(atspi_table_get_accessible_at(ATSPI_TABLE(table.get()), 2, 2, nullptr));
    g_assert_true(ATSPI_IS_TABLE_CELL(cell7.get()));
    g_assert_cmpint(atspi_table_cell_get_row_span(ATSPI_TABLE_CELL(cell7.get()), nullptr), ==, 1);
    g_assert_cmpint(atspi_table_cell_get_column_span(ATSPI_TABLE_CELL(cell7.get()), nullptr), ==, 1);
    g_assert_true(atspi_table_cell_get_position(ATSPI_TABLE_CELL(cell7.get()), &row, &column, nullptr));
    g_assert_cmpint(row, ==, 2);
    g_assert_cmpint(column, ==, 2);
    atspi_table_cell_get_row_column_span(ATSPI_TABLE_CELL(cell7.get()), &row, &column, &rowSpan, &columnSpan, nullptr);
    g_assert_cmpint(row, ==, 2);
    g_assert_cmpint(column, ==, 2);
    g_assert_cmpint(rowSpan, ==, 1);
    g_assert_cmpint(columnSpan, ==, 1);
    cellTable = adoptGRef(atspi_table_cell_get_table(ATSPI_TABLE_CELL(cell7.get()), nullptr));
    g_assert_true(table.get() == cellTable.get());
    rowHeaders = adoptGRef(atspi_table_cell_get_row_header_cells(ATSPI_TABLE_CELL(cell7.get()), nullptr));
    g_assert_cmpint(rowHeaders->len, ==, 1);
    g_assert_true(rowHeaders->pdata[0] == rowHeader.get());
    g_ptr_array_foreach(rowHeaders.get(), reinterpret_cast<GFunc>(reinterpret_cast<GCallback>(g_object_unref)), nullptr);
    columnHeaders = adoptGRef(atspi_table_cell_get_column_header_cells(ATSPI_TABLE_CELL(cell7.get()), nullptr));
    g_assert_cmpint(columnHeaders->len, ==, 1);
    g_assert_true(columnHeaders->pdata[0] == columnHeader2.get());
    g_ptr_array_foreach(columnHeaders.get(), reinterpret_cast<GFunc>(reinterpret_cast<GCallback>(g_object_unref)), nullptr);
    g_assert_true(ATSPI_IS_TEXT(cell7.get()));
    text.reset(atspi_text_get_text(ATSPI_TEXT(cell7.get()), 0, -1, nullptr));
    g_assert_cmpstr(text.get(), ==, "Row 2 Cell 3");

    g_assert_cmpint(atspi_table_get_index_at(ATSPI_TABLE(table.get()), 3, 0, nullptr), ==, 8);
    g_assert_cmpint(atspi_table_get_row_at_index(ATSPI_TABLE(table.get()), 8, nullptr), ==, 3);
    g_assert_cmpint(atspi_table_get_column_at_index(ATSPI_TABLE(table.get()), 8, nullptr), ==, 0);
    g_assert_cmpint(atspi_table_get_row_extent_at(ATSPI_TABLE(table.get()), 3, 0, nullptr), ==, 1);
    g_assert_cmpint(atspi_table_get_column_extent_at(ATSPI_TABLE(table.get()), 3, 0, nullptr), ==, 3);
    g_assert_true(atspi_table_get_row_column_extents_at_index(ATSPI_TABLE(table.get()), 8, &row, &column, &rowSpan, &columnSpan, &isSelected, nullptr));
    g_assert_cmpint(row, ==, 3);
    g_assert_cmpint(column, ==, 0);
    g_assert_cmpint(rowSpan, ==, 1);
    g_assert_cmpint(columnSpan, ==, 3);
    g_assert_false(isSelected);
    auto cell8 = adoptGRef(atspi_table_get_accessible_at(ATSPI_TABLE(table.get()), 3, 0, nullptr));
    g_assert_true(ATSPI_IS_TABLE_CELL(cell8.get()));
    g_assert_cmpint(atspi_table_cell_get_row_span(ATSPI_TABLE_CELL(cell8.get()), nullptr), ==, 1);
    g_assert_cmpint(atspi_table_cell_get_column_span(ATSPI_TABLE_CELL(cell8.get()), nullptr), ==, 3);
    g_assert_true(atspi_table_cell_get_position(ATSPI_TABLE_CELL(cell8.get()), &row, &column, nullptr));
    g_assert_cmpint(row, ==, 3);
    g_assert_cmpint(column, ==, 0);
    atspi_table_cell_get_row_column_span(ATSPI_TABLE_CELL(cell8.get()), &row, &column, &rowSpan, &columnSpan, nullptr);
    g_assert_cmpint(row, ==, 3);
    g_assert_cmpint(column, ==, 0);
    g_assert_cmpint(rowSpan, ==, 1);
    g_assert_cmpint(columnSpan, ==, 3);
    cellTable = adoptGRef(atspi_table_cell_get_table(ATSPI_TABLE_CELL(cell8.get()), nullptr));
    g_assert_true(table.get() == cellTable.get());
    rowHeaders = adoptGRef(atspi_table_cell_get_row_header_cells(ATSPI_TABLE_CELL(cell8.get()), nullptr));
    g_assert_cmpint(rowHeaders->len, ==, 0);
    g_ptr_array_foreach(rowHeaders.get(), reinterpret_cast<GFunc>(reinterpret_cast<GCallback>(g_object_unref)), nullptr);
    columnHeaders = adoptGRef(atspi_table_cell_get_column_header_cells(ATSPI_TABLE_CELL(cell8.get()), nullptr));
    g_assert_cmpint(columnHeaders->len, ==, 1);
    g_assert_true(columnHeaders->pdata[0] == columnHeader0.get());
    g_ptr_array_foreach(columnHeaders.get(), reinterpret_cast<GFunc>(reinterpret_cast<GCallback>(g_object_unref)), nullptr);
    g_assert_true(ATSPI_IS_TEXT(cell8.get()));
    text.reset(atspi_text_get_text(ATSPI_TEXT(cell8.get()), 0, -1, nullptr));
    g_assert_cmpstr(text.get(), ==, "Row 3 Cell 1");

    g_assert_cmpint(atspi_table_get_index_at(ATSPI_TABLE(table.get()), 3, 1, nullptr), ==, 8);
    g_assert_cmpint(atspi_table_get_row_extent_at(ATSPI_TABLE(table.get()), 3, 1, nullptr), ==, 1);
    g_assert_cmpint(atspi_table_get_column_extent_at(ATSPI_TABLE(table.get()), 3, 1, nullptr), ==, 3);
    cell = adoptGRef(atspi_table_get_accessible_at(ATSPI_TABLE(table.get()), 3, 1, nullptr));
    g_assert_true(cell8.get() == cell.get());

    g_assert_cmpint(atspi_table_get_index_at(ATSPI_TABLE(table.get()), 3, 2, nullptr), ==, 8);
    g_assert_cmpint(atspi_table_get_row_extent_at(ATSPI_TABLE(table.get()), 3, 2, nullptr), ==, 1);
    g_assert_cmpint(atspi_table_get_column_extent_at(ATSPI_TABLE(table.get()), 3, 2, nullptr), ==, 3);
    cell = adoptGRef(atspi_table_get_accessible_at(ATSPI_TABLE(table.get()), 3, 2, nullptr));
    g_assert_true(cell8.get() == cell.get());
}

void beforeAll()
{
    AccessibilityTest::add("WebKitAccessibility", "accessible/basic-hierarchy", testAccessibleBasicHierarchy);
    AccessibilityTest::add("WebKitAccessibility", "accessible/ignored-objects", testAccessibleIgnoredObjects);
    AccessibilityTest::add("WebKitAccessibility", "accessible/children-changed", testAccessibleChildrenChanged);
    AccessibilityTest::add("WebKitAccessibility", "accessible/attributes", testAccessibleAttributes);
    AccessibilityTest::add("WebKitAccessibility", "accessible/state", testAccessibleState);
    AccessibilityTest::add("WebKitAccessibility", "accessible/state-changed", testAccessibleStateChanged);
    AccessibilityTest::add("WebKitAccessibility", "accessible/event-listener", testAccessibleEventListener);
    AccessibilityTest::add("WebKitAccessibility", "accessible/list-markers", testAccessibleListMarkers);
    AccessibilityTest::add("WebKitAccessibility", "component/hit-test", testComponentHitTest);
#ifdef ATSPI_SCROLLTYPE_COUNT
    AccessibilityTest::add("WebKitAccessibility", "component/scroll-to", testComponentScrollTo);
#endif
    AccessibilityTest::add("WebKitAccessibility", "text/basic", testTextBasic);
    AccessibilityTest::add("WebKitAccessibility", "text/surrogate-pair", testTextSurrogatePair);
    AccessibilityTest::add("WebKitAccessibility", "text/iterator", testTextIterator);
    AccessibilityTest::add("WebKitAccessibility", "text/extents", testTextExtents);
    AccessibilityTest::add("WebKitAccessibility", "text/selections", testTextSelections);
    AccessibilityTest::add("WebKitAccessibility", "text/attributes", testTextAttributes);
    AccessibilityTest::add("WebKitAccessibility", "text/state-changed", testTextStateChanged);
    AccessibilityTest::add("WebKitAccessibility", "text/replaced-objects", testTextReplacedObjects);
    AccessibilityTest::add("WebKitAccessibility", "text/list-markers", testTextListMarkers);
    AccessibilityTest::add("WebKitAccessibility", "value/basic", testValueBasic);
    AccessibilityTest::add("WebKitAccessibility", "hyperlink/basic", testHyperlinkBasic);
    AccessibilityTest::add("WebKitAccessibility", "hypertext/basic", testHypertextBasic);
    AccessibilityTest::add("WebKitAccessibility", "action/basic", testActionBasic);
    AccessibilityTest::add("WebKitAccessibility", "document/basic", testDocumentBasic);
    AccessibilityTest::add("WebKitAccessibility", "document/load-events", testDocumentLoadEvents);
    AccessibilityTest::add("WebKitAccessibility", "image/basic", testImageBasic);
    AccessibilityTest::add("WebKitAccessibility", "selection/listbox", testSelectionListBox);
    AccessibilityTest::add("WebKitAccessibility", "selection/menulist", testSelectionMenuList);
    AccessibilityTest::add("WebKitAccessibility", "table/basic", testTableBasic);
}

void afterAll()
{
}
