// Copyright (C) 2016 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "bus_interface.h" #define COMPARE3(v1, v2, v3) QCOMPARE(v1, v3); QCOMPARE(v2, v3); class AccessibleTestWindow : public QWidget { Q_OBJECT public: AccessibleTestWindow() { new QHBoxLayout(this); } void addWidget(QWidget* widget) { layout()->addWidget(widget); widget->show(); QVERIFY(QTest::qWaitForWindowExposed(widget)); } void clearChildren() { qDeleteAll(children()); new QHBoxLayout(this); } }; class tst_QAccessibilityLinux : public QObject { Q_OBJECT public: tst_QAccessibilityLinux() : m_window(0), root(0), rootApplication(0), mainWindow(0) { qputenv("QT_LINUX_ACCESSIBILITY_ALWAYS_ON", "1"); dbus = new DBusConnection(); } ~tst_QAccessibilityLinux() { delete dbus; } private slots: void initTestCase(); void testLabel(); void testLineEdit(); void testListWidget(); void testTreeWidget(); void testTextEdit(); void testSlider(); void testFocus(); void cleanupTestCase(); private: void registerDbus(); static QString getParent(QDBusInterface *interface); static QStringList getChildren(QDBusInterface *interface); QDBusInterface *getInterface(const QString &path, const QString &interfaceName); AccessibleTestWindow *m_window; QString address; QDBusInterface *root; // the root object on dbus (for the app) QDBusInterface *rootApplication; QDBusInterface *mainWindow; DBusConnection *dbus; }; // helper to find children of a dbus object QStringList tst_QAccessibilityLinux::getChildren(QDBusInterface *interface) { QSpiObjectReferenceArray list; const QList args = interface->call(QDBus::Block, "GetChildren").arguments(); Q_ASSERT(args.size() == 1); Q_ASSERT(args.first().isValid()); args.first().value() >> list; Q_ASSERT(interface->property("ChildCount").toInt() == list.count()); QStringList children; Q_FOREACH (const QSpiObjectReference &ref, list) children << ref.path.path(); return children; } QString tst_QAccessibilityLinux::getParent(QDBusInterface *interface) { if (!interface->isValid()) return QString(); QVariant var = interface->property("Parent"); if (!var.canConvert()) { qWarning() << "Invalid parent"; return QString(); } QSpiObjectReference parent = var.value(); return parent.path.path(); } // helper to get dbus object QDBusInterface *tst_QAccessibilityLinux::getInterface(const QString &path, const QString &interfaceName) { return new QDBusInterface(address, path, interfaceName, dbus->connection(), this); } void tst_QAccessibilityLinux::initTestCase() { // Oxygen style creates many extra items, it's simply unusable here qApp->setStyle("fusion"); qApp->setApplicationName("tst_QAccessibilityLinux app"); // trigger launching of at-spi if it isn't running already QDBusConnection c = QDBusConnection::sessionBus(); OrgA11yStatusInterface *a11yStatus = new OrgA11yStatusInterface(QStringLiteral("org.a11y.Bus"), QStringLiteral("/org/a11y/bus"), c, this); // don't care about the result, calling any function on "org.a11y.Bus" will launch the service a11yStatus->isEnabled(); for (int i = 0; i < 5; ++i) { if (!dbus->isEnabled()) QTest::qWait(100); } if (!dbus->isEnabled()) QSKIP("Could not connect to AT-SPI, make sure lib atspi2 is installed."); QTRY_VERIFY(dbus->isEnabled()); QTRY_VERIFY(dbus->connection().isConnected()); address = dbus->connection().baseService().toLatin1().data(); QVERIFY(!address.isEmpty()); m_window = new AccessibleTestWindow(); m_window->setObjectName("mainWindow"_L1); m_window->show(); QVERIFY(QTest::qWaitForWindowExposed(m_window)); registerDbus(); } void tst_QAccessibilityLinux::cleanupTestCase() { delete mainWindow; delete rootApplication; delete root; delete m_window; } void tst_QAccessibilityLinux::registerDbus() { QVERIFY(dbus->connection().isConnected()); root = getInterface("/org/a11y/atspi/accessible/root", "org.a11y.atspi.Accessible"); rootApplication = getInterface("/org/a11y/atspi/accessible/root", "org.a11y.atspi.Application"); QVERIFY(root->isValid()); QVERIFY(rootApplication->isValid()); QStringList appChildren = getChildren(root); QString window = appChildren.at(0); mainWindow = getInterface(window, "org.a11y.atspi.Accessible"); } quint64 getAtspiState(QDBusInterface *interface) { QDBusMessage msg = interface->call(QDBus::Block, "GetState"); const QDBusArgument arg = msg.arguments().at(0).value(); quint32 state1 = 0; quint64 state2 = 0; arg.beginArray(); arg >> state1; arg >> state2; arg.endArray(); state2 = state2 << 32; return state2 | state1; } bool hasState(QDBusInterface *interface, AtspiStateType state) { quint64 intState = quint64(1) << state; return getAtspiState(interface) & intState; } #define ROOTPATH "/org/a11y/atspi/accessible" void tst_QAccessibilityLinux::testLabel() { QLabel *l = new QLabel(m_window); l->setObjectName("theObjectName"_L1); l->setText("Hello A11y"); m_window->addWidget(l); auto a11yEmpty = new QLabel(m_window); m_window->addWidget(l); // Application QCOMPARE(getParent(mainWindow), QLatin1String(ATSPI_DBUS_PATH_ROOT)); QStringList children = getChildren(mainWindow); QDBusInterface *labelInterface = getInterface(children.at(0), "org.a11y.atspi.Accessible"); QVERIFY(labelInterface->isValid()); QCOMPARE(labelInterface->property("Name").toString(), QLatin1String("Hello A11y")); QCOMPARE(getChildren(labelInterface).count(), 0); QCOMPARE(labelInterface->call(QDBus::Block, "GetRoleName").arguments().first().toString(), QLatin1String("label")); QCOMPARE(labelInterface->call(QDBus::Block, "GetRole").arguments().first().toUInt(), 29u); QCOMPARE(labelInterface->call(QDBus::Block, "GetAccessibleId").arguments().first().toString(), "mainWindow.theObjectName"_L1); QCOMPARE(getParent(labelInterface), mainWindow->path()); QVERIFY(!hasState(labelInterface, ATSPI_STATE_EDITABLE)); QVERIFY(hasState(labelInterface, ATSPI_STATE_READ_ONLY)); l->setText("New text"); QCOMPARE(labelInterface->property("Name").toString(), l->text()); auto *a11yEmptyInterface = getInterface(children.at(1), "org.a11y.atspi.Accessible"); QCOMPARE(a11yEmptyInterface->call(QDBus::Block, "GetAccessibleId").arguments().first().toString(), "mainWindow.QLabel"_L1); m_window->clearChildren(); delete a11yEmptyInterface; delete labelInterface; } void tst_QAccessibilityLinux::testLineEdit() { QLineEdit *lineEdit = new QLineEdit(m_window); lineEdit->setText("a11y test QLineEdit"); m_window->addWidget(lineEdit); QStringList children = getChildren(mainWindow); QDBusInterface *accessibleInterface = getInterface(children.at(0), "org.a11y.atspi.Accessible"); QDBusInterface *editableTextInterface = getInterface(children.at(0), "org.a11y.atspi.EditableText"); QDBusInterface *textInterface = getInterface(children.at(0), "org.a11y.atspi.Text"); QVERIFY(accessibleInterface->isValid()); QVERIFY(editableTextInterface->isValid()); QVERIFY(textInterface->isValid()); QCOMPARE(accessibleInterface->call(QDBus::Block, "GetRoleName").arguments().first().toString(), QLatin1String("text")); QCOMPARE(textInterface->call(QDBus::Block,"GetText", 5, -1).arguments().first().toString(), QLatin1String("test QLineEdit")); QString newText = "Text has changed!"; editableTextInterface->call(QDBus::Block, "SetTextContents", newText); COMPARE3(lineEdit->text(), textInterface->call(QDBus::Block, "GetText", 0, -1).arguments().first().toString(), newText); QCOMPARE(textInterface->call(QDBus::Block, "GetText", 0, 4).arguments().first().toString(), QLatin1String("Text")); editableTextInterface->call(QDBus::Block, "DeleteText", 4, 8); COMPARE3(lineEdit->text(), "Te" + textInterface->call(QDBus::Block, "GetText", 2, 10).arguments().first().toString() + "ed!", QLatin1String("Text changed!")); editableTextInterface->call(QDBus::Block, "InsertText", 12, " again ", 6); QCOMPARE(lineEdit->text(), QLatin1String("Text changed again!")); COMPARE3(lineEdit->text().length(), textInterface->property("CharacterCount").toInt(), 19); textInterface->call(QDBus::Block, "SetCaretOffset", 4); COMPARE3(lineEdit->cursorPosition(), textInterface->property("CaretOffset").toInt(), 4); textInterface->call(QDBus::Block, "AddSelection", 1, 4); QList data = textInterface->call(QDBus::Block, "GetSelection", 0).arguments(); COMPARE3(data.at(0).toInt(), lineEdit->selectionStart(), 1); QCOMPARE(data.at(1).toInt(), 4); QCOMPARE(lineEdit->selectedText().length(), 3); QCOMPARE(textInterface->call(QDBus::Block, "GetNSelections").arguments().first().toInt(), 1); textInterface->call(QDBus::Block, "SetSelection", 0, 0, 5); data = textInterface->call(QDBus::Block, "GetSelection", 0).arguments(); COMPARE3(data.at(0).toInt(), lineEdit->selectionStart(), 0); COMPARE3(data.at(1).toInt(), lineEdit->selectedText().length(), 5); textInterface->call(QDBus::Block, "RemoveSelection", 0); QCOMPARE(lineEdit->selectionStart(), -1); QCOMPARE(textInterface->call(QDBus::Block, "GetNSelections").arguments().first().toInt(), 0); QVERIFY(hasState(accessibleInterface, ATSPI_STATE_EDITABLE)); QVERIFY(!hasState(accessibleInterface, ATSPI_STATE_READ_ONLY)); lineEdit->setReadOnly(true); QVERIFY(hasState(accessibleInterface, ATSPI_STATE_EDITABLE)); QVERIFY(hasState(accessibleInterface, ATSPI_STATE_READ_ONLY)); m_window->clearChildren(); delete accessibleInterface; delete textInterface; delete editableTextInterface; } void tst_QAccessibilityLinux::testListWidget() { QListWidget *lw = new QListWidget; lw->addItem("Hello"); lw->addItem("Good morning"); lw->addItem("Good bye"); m_window->addWidget(lw); QStringList children = getChildren(mainWindow); QDBusInterface *listIface = getInterface(children.at(0), "org.a11y.atspi.Accessible"); QCOMPARE(listIface->call(QDBus::Block, "GetRoleName").arguments().first().toString(), QLatin1String("list")); QStringList tableChildren = getChildren(listIface); QCOMPARE(tableChildren.size(), 3); QDBusInterface *cell1 = getInterface(tableChildren.at(0), "org.a11y.atspi.Accessible"); QCOMPARE(cell1->call(QDBus::Block, "GetRoleName").arguments().first().toString(), QLatin1String("list item")); QCOMPARE(cell1->property("Name").toString(), QLatin1String("Hello")); QDBusInterface *cell2 = getInterface(tableChildren.at(1), "org.a11y.atspi.Accessible"); QCOMPARE(cell2->call(QDBus::Block, "GetRoleName").arguments().first().toString(), QLatin1String("list item")); QCOMPARE(cell2->property("Name").toString(), QLatin1String("Good morning")); QDBusInterface *cell3 = getInterface(tableChildren.at(2), "org.a11y.atspi.Accessible"); QCOMPARE(cell3->call(QDBus::Block, "GetRoleName").arguments().first().toString(), QLatin1String("list item")); QCOMPARE(cell3->property("Name").toString(), QLatin1String("Good bye")); delete cell1; delete cell2; delete cell3; m_window->clearChildren(); delete listIface; } void tst_QAccessibilityLinux::testTreeWidget() { QTreeWidget *tree = new QTreeWidget; tree->setColumnCount(2); tree->setHeaderLabels(QStringList() << "Header 1" << "Header 2"); QTreeWidgetItem *top1 = new QTreeWidgetItem(QStringList() << "0.0" << "0.1"); tree->addTopLevelItem(top1); QTreeWidgetItem *top2 = new QTreeWidgetItem(QStringList() << "1.0" << "1.1"); tree->addTopLevelItem(top2); QTreeWidgetItem *child1 = new QTreeWidgetItem(QStringList() << "1.0 0.0" << "1.0 0.1"); top2->addChild(child1); m_window->addWidget(tree); QStringList children = getChildren(mainWindow); QDBusInterface *treeIface = getInterface(children.at(0), "org.a11y.atspi.Accessible"); QCOMPARE(treeIface->call(QDBus::Block, "GetRoleName").arguments().first().toString(), QLatin1String("tree")); QStringList tableChildren = getChildren(treeIface); QCOMPARE(tableChildren.size(), 6); QDBusInterface *cell1 = getInterface(tableChildren.at(0), "org.a11y.atspi.Accessible"); QCOMPARE(cell1->call(QDBus::Block, "GetRoleName").arguments().first().toString(), QLatin1String("column header")); QCOMPARE(cell1->property("Name").toString(), QLatin1String("Header 1")); QDBusInterface *cell2 = getInterface(tableChildren.at(1), "org.a11y.atspi.Accessible"); QCOMPARE(cell2->call(QDBus::Block, "GetRoleName").arguments().first().toString(), QLatin1String("column header")); QCOMPARE(cell2->property("Name").toString(), QLatin1String("Header 2")); QDBusInterface *cell3 = getInterface(tableChildren.at(2), "org.a11y.atspi.Accessible"); QCOMPARE(cell3->property("Name").toString(), QLatin1String("0.0")); QVERIFY(!hasState(cell3, ATSPI_STATE_EXPANDABLE)); QVERIFY(!hasState(cell3, ATSPI_STATE_EXPANDED)); QDBusInterface *cell4 = getInterface(tableChildren.at(3), "org.a11y.atspi.Accessible"); QCOMPARE(cell4->property("Name").toString(), QLatin1String("0.1")); QDBusInterface *dbus_top2 = getInterface(tableChildren.at(4), "org.a11y.atspi.Accessible"); QCOMPARE(dbus_top2->property("Name").toString(), QLatin1String("1.0")); QVERIFY(hasState(dbus_top2, ATSPI_STATE_EXPANDABLE)); QVERIFY(!hasState(dbus_top2, ATSPI_STATE_EXPANDED)); tree->expandItem(top2); tableChildren = getChildren(treeIface); QCOMPARE(tableChildren.size(), 8); QVERIFY(hasState(dbus_top2, ATSPI_STATE_EXPANDED)); QDBusInterface *cell5 = getInterface(tableChildren.at(6), "org.a11y.atspi.Accessible"); QCOMPARE(cell5->property("Name").toString(), QLatin1String("1.0 0.0")); QDBusInterface *cell6 = getInterface(tableChildren.at(7), "org.a11y.atspi.Accessible"); QCOMPARE(cell6->property("Name").toString(), QLatin1String("1.0 0.1")); QDBusInterface *treeTableIface = getInterface(children.at(0), "org.a11y.atspi.Table"); QCOMPARE(treeTableIface->call(QDBus::Block, "GetRowAtIndex", 0).arguments().first().toInt(), -1); QCOMPARE(treeTableIface->call(QDBus::Block, "GetRowAtIndex", 1).arguments().first().toInt(), -1); QCOMPARE(treeTableIface->call(QDBus::Block, "GetRowAtIndex", 2).arguments().first().toInt(), 0); QCOMPARE(treeTableIface->call(QDBus::Block, "GetRowAtIndex", 3).arguments().first().toInt(), 0); QCOMPARE(treeTableIface->call(QDBus::Block, "GetRowAtIndex", 4).arguments().first().toInt(), 1); QCOMPARE(treeTableIface->call(QDBus::Block, "GetRowAtIndex", 5).arguments().first().toInt(), 1); QCOMPARE(treeTableIface->call(QDBus::Block, "GetRowAtIndex", 6).arguments().first().toInt(), 2); QCOMPARE(treeTableIface->call(QDBus::Block, "GetRowAtIndex", 7).arguments().first().toInt(), 2); QCOMPARE(treeTableIface->call(QDBus::Block, "GetColumnAtIndex", 0).arguments().first().toInt(), 0); QCOMPARE(treeTableIface->call(QDBus::Block, "GetColumnAtIndex", 1).arguments().first().toInt(), 1); QCOMPARE(treeTableIface->call(QDBus::Block, "GetColumnAtIndex", 2).arguments().first().toInt(), 0); QCOMPARE(treeTableIface->call(QDBus::Block, "GetColumnAtIndex", 3).arguments().first().toInt(), 1); QCOMPARE(treeTableIface->call(QDBus::Block, "GetColumnAtIndex", 4).arguments().first().toInt(), 0); QCOMPARE(treeTableIface->call(QDBus::Block, "GetColumnAtIndex", 5).arguments().first().toInt(), 1); QCOMPARE(treeTableIface->call(QDBus::Block, "GetColumnAtIndex", 6).arguments().first().toInt(), 0); QCOMPARE(treeTableIface->call(QDBus::Block, "GetColumnAtIndex", 7).arguments().first().toInt(), 1); delete treeTableIface; delete cell1; delete cell2; delete cell3; delete cell4; delete cell5; delete cell6; m_window->clearChildren(); delete treeIface; } void tst_QAccessibilityLinux::testTextEdit() { QTextEdit *textEdit = new QTextEdit(m_window); textEdit->setText("This is a sample text.
" "How are you today"); textEdit->show(); m_window->addWidget(textEdit); QStringList children = getChildren(mainWindow); QDBusInterface *accessibleInterface = getInterface(children.at(0), "org.a11y.atspi.Accessible"); QDBusInterface *editableTextInterface = getInterface(children.at(0), "org.a11y.atspi.EditableText"); QDBusInterface *textInterface = getInterface(children.at(0), "org.a11y.atspi.Text"); QVERIFY(accessibleInterface->isValid()); QVERIFY(editableTextInterface->isValid()); QVERIFY(textInterface->isValid()); QList callResult; QDBusMessage msg = textInterface->call(QDBus::Block, "GetText", 0, 5); callResult = msg.arguments(); QCOMPARE(callResult.at(0).toString(), QLatin1String("This ")); msg = textInterface->call(QDBus::Block, "GetTextAtOffset", 12, (uint) ATSPI_TEXT_BOUNDARY_WORD_START); callResult = msg.arguments(); QEXPECT_FAIL("", "Word should contain space at end according to atspi.", Continue); QCOMPARE(callResult.at(0).toString(), QLatin1String("sample ")); QCOMPARE(callResult.at(1).toInt(), 10); QEXPECT_FAIL("", "Due to missing space the count is off by one.", Continue); QCOMPARE(callResult.at(2).toInt(), 17); // Check if at least CharacterExtents and RangeExtents give a consistent result QDBusMessage replyRect20 = textInterface->call(QDBus::Block, "GetCharacterExtents", 20, ATSPI_COORD_TYPE_SCREEN); QCOMPARE(replyRect20.type(), QDBusMessage::ReplyMessage); QCOMPARE(replyRect20.signature(), QStringLiteral("iiii")); callResult = replyRect20.arguments(); QRect r1 = QRect(callResult.at(0).toInt(), callResult.at(1).toInt(), callResult.at(2).toInt(), callResult.at(3).toInt()); QDBusMessage replyRect21 = textInterface->call(QDBus::Block, "GetCharacterExtents", 21, ATSPI_COORD_TYPE_SCREEN); QCOMPARE(replyRect21.type(), QDBusMessage::ReplyMessage); QCOMPARE(replyRect21.signature(), QStringLiteral("iiii")); callResult = replyRect21.arguments(); QRect r2 = QRect(callResult.at(0).toInt(), callResult.at(1).toInt(), callResult.at(2).toInt(), callResult.at(3).toInt()); QDBusMessage replyRange = textInterface->call(QDBus::Block, "GetRangeExtents", 20, 21, ATSPI_COORD_TYPE_SCREEN); callResult = replyRange.arguments(); QRect rectRangeExtents = QRect(callResult.at(0).toInt(), callResult.at(1).toInt(), callResult.at(2).toInt(), callResult.at(3).toInt()); QCOMPARE(rectRangeExtents, r1|r2); m_window->clearChildren(); delete textInterface; } void tst_QAccessibilityLinux::testSlider() { QSlider *slider = new QSlider(m_window); slider->setMinimum(2); slider->setMaximum(5); slider->setValue(3); m_window->addWidget(slider); QStringList children = getChildren(mainWindow); QDBusInterface *accessibleInterface = getInterface(children.at(0), "org.a11y.atspi.Accessible"); QDBusInterface *valueInterface = getInterface(children.at(0), "org.a11y.atspi.Value"); QVERIFY(accessibleInterface->isValid()); QVERIFY(valueInterface->isValid()); QCOMPARE(valueInterface->property("CurrentValue").toInt(), 3); QCOMPARE(valueInterface->property("MinimumValue").toInt(), 2); QCOMPARE(valueInterface->property("MaximumValue").toInt(), 5); valueInterface->setProperty("CurrentValue", 4); QCOMPARE(valueInterface->property("CurrentValue").toInt(), 4); m_window->clearChildren(); } void tst_QAccessibilityLinux::testFocus() { m_window->activateWindow(); QVERIFY(QTest::qWaitForWindowActive(m_window)); QLineEdit *lineEdit1 = new QLineEdit(m_window); lineEdit1->setText("lineEdit 1"); QLineEdit *lineEdit2 = new QLineEdit(m_window); lineEdit2->setText("lineEdit 2"); m_window->addWidget(lineEdit1); m_window->addWidget(lineEdit2); lineEdit1->setFocus(); QStringList children = getChildren(mainWindow); QCOMPARE(children.length(), 2); QDBusInterface *accessibleInterfaceLineEdit1 = getInterface(children.at(0), "org.a11y.atspi.Accessible"); QVERIFY(accessibleInterfaceLineEdit1->isValid()); QDBusInterface *accessibleInterfaceLineEdit2 = getInterface(children.at(1), "org.a11y.atspi.Accessible"); QVERIFY(accessibleInterfaceLineEdit2->isValid()); QDBusInterface *componentInterfaceLineEdit1 = getInterface(children.at(0), "org.a11y.atspi.Component"); QVERIFY(componentInterfaceLineEdit1->isValid()); QDBusInterface *componentInterfaceLineEdit2 = getInterface(children.at(1), "org.a11y.atspi.Component"); QVERIFY(componentInterfaceLineEdit2->isValid()); QVERIFY(hasState(accessibleInterfaceLineEdit1, ATSPI_STATE_FOCUSED)); QVERIFY(!hasState(accessibleInterfaceLineEdit2, ATSPI_STATE_FOCUSED)); QDBusMessage focusReply = componentInterfaceLineEdit2->call(QDBus::Block, "GrabFocus"); QVERIFY(focusReply.arguments().at(0).toBool()); QVERIFY(lineEdit2->hasFocus()); QVERIFY(!hasState(accessibleInterfaceLineEdit1, ATSPI_STATE_FOCUSED)); QVERIFY(hasState(accessibleInterfaceLineEdit2, ATSPI_STATE_FOCUSED)); m_window->clearChildren(); delete accessibleInterfaceLineEdit1; delete accessibleInterfaceLineEdit2; delete componentInterfaceLineEdit1; delete componentInterfaceLineEdit2; } QTEST_MAIN(tst_QAccessibilityLinux) #include "tst_qaccessibilitylinux.moc"