Пользовательский интерфейс в Qt

Тестирование доступности приложений: инструменты и чек-листы

Введение

Как и любой другой аспект качества, доступность нуждается в систематической проверке — не только при первоначальной разработке, но и при каждом значимом изменении интерфейса, чтобы не допустить регрессии. В статье разберём конкретные инструменты для тестирования доступности Qt-приложений на разных платформах, и составим практический чек-лист, применимый при ручном тестировании.

Концепция

Тестирование доступности проводится на нескольких уровнях: автоматизированные проверки структуры доступности (есть ли у элементов имена, роли, состояния — это можно частично автоматизировать), ручное тестирование с реальным screen reader (NVDA на Windows — бесплатный и широко используемый, VoiceOver встроен в macOS, Orca — стандартный screen reader для GNOME на Linux), и тестирование исключительно клавиатурной навигации без использования мыши вовсе. Полностью автоматизированная проверка доступности невозможна — многие критичные аспекты (логичность порядка озвучивания, осмысленность текстовых описаний) требуют человеческой оценки.

Пример кода: Тестирование доступности приложений

// Инструмент инспекции дерева доступности приложения для отладки —
// выводит структуру в консоль, аналогично Accessibility Inspector на macOS
#include <QAccessible>
#include <QAccessibleInterface>
#include <QDebug>

void dumpAccessibilityTree(QAccessibleInterface *iface, int depth = 0)
{
    if (!iface || !iface->isValid()) return;

    const QString indent(depth * 2, ' ');
    qDebug().noquote() << indent
        << "Роль:" << static_cast<int>(iface->role())
        << "| Имя:" << iface->text(QAccessible::Name)
        << "| Значение:" << iface->text(QAccessible::Value)
        << "| Фокусируемый:" << iface->state().focusable
        << "| Невалидный:" << iface->state().invalid;

    for (int i = 0; i < iface->childCount(); ++i) {
        dumpAccessibilityTree(iface->child(i), depth + 1);
    }
}

void inspectApplicationAccessibility(QWidget *rootWidget)
{
    QAccessibleInterface *rootInterface = QAccessible::queryAccessibleInterface(rootWidget);
    qDebug() << "=== Дерево доступности приложения ===";
    dumpAccessibilityTree(rootInterface);
}
// Автоматизированная проверка базовых требований доступности как часть тестов
#include <QtTest/QtTest>
#include <QAccessible>

class TestAccessibilityBasics : public QObject
{
    Q_OBJECT
private slots:
    void testAllInteractiveWidgetsHaveAccessibleName()
    {
        MainWindow window;
        QAccessibleInterface *rootInterface = QAccessible::queryAccessibleInterface(&window);

        QStringList widgetsWithoutName;
        checkNamesRecursively(rootInterface, widgetsWithoutName);

        QVERIFY2(widgetsWithoutName.isEmpty(),
                  qPrintable("Виджеты без accessibleName: " + widgetsWithoutName.join(", ")));
    }

private:
    void checkNamesRecursively(QAccessibleInterface *iface, QStringList &problems)
    {
        if (!iface || !iface->isValid()) return;

        const bool isInteractive = iface->role() == QAccessible::Button
                                 || iface->role() == QAccessible::CheckBox
                                 || iface->role() == QAccessible::EditableText;

        if (isInteractive && iface->text(QAccessible::Name).isEmpty()) {
            problems.append(QString("Объект с ролью %1 без имени").arg(static_cast<int>(iface->role())));
        }

        for (int i = 0; i < iface->childCount(); ++i) {
            checkNamesRecursively(iface->child(i), problems);
        }
    }
};

QTEST_MAIN(TestAccessibilityBasics)
#include "test_accessibilitybasics.moc"
Чек-лист ручного тестирования доступности (для каждого нового экрана/диалога):

[ ] Все интерактивные элементы достижимы через Tab без использования мыши
[ ] Порядок обхода Tab соответствует логичному визуальному порядку
[ ] Каждый элемент имеет осмысленное, понятное имя при озвучивании screen reader
[ ] Динамические изменения (ошибки, обновления статуса) озвучиваются автоматически
[ ] Действие можно выполнить и завершить без единого использования мыши
[ ] Цветовая индикация состояния дублируется текстом или иконкой
[ ] Коэффициент контрастности текста/фона соответствует WCAG AA (см. статью 204)
[ ] Увеличение масштаба текста системой не ломает раскладку интерфейса
[ ] При закрытии диалога фокус корректно возвращается на вызвавший его элемент

Пояснения к коду

dumpAccessibilityTree() рекурсивно обходит дерево объектов доступности через QAccessibleInterface::childCount()/child(), выводя в консоль ключевые атрибуты каждого узла — это практичный, быстрый способ визуально проверить, что реально «видит» подсистема доступности, без необходимости запускать полноценный screen reader для каждой мелкой проверки во время итеративной разработки.

TestAccessibilityBasics показывает пример автоматизированной, хотя и не исчерпывающей проверки — она ловит самый базовый класс проблем (полностью отсутствующее имя у интерактивного элемента), но не может проверить осмысленность этого имени для реального пользователя («Кнопка 1» формально не пустое имя, но абсолютно неинформативно) — такая автоматизированная проверка дополняет, но не заменяет ручное тестирование по чек-листу, приведённому в третьем блоке примера.

Подводные камни

  • Восприятие автоматизированных проверок (как TestAccessibilityBasics) как достаточного доказательства полной доступности приложения. Такие тесты эффективно ловят явные пробелы (отсутствие имени вовсе), но совершенно не способны оценить качество и осмысленность предоставленной информации, логичность порядка навигации или реальное удобство использования с конкретным screen reader — они снижают регрессию совсем базовых проблем, но не заменяют периодическое ручное тестирование.
  • Тестирование только с одним screen reader на одной платформе. Разные screen reader (NVDA, JAWS, VoiceOver, Orca) по-разному интерпретируют одни и те же события и атрибуты доступности, и приложение, прекрасно работающее с NVDA на Windows, может иметь специфичные проблемы при использовании VoiceOver на macOS — для кросс-платформенного приложения желательно протестировать хотя бы по одному циклу с основным screen reader каждой целевой платформы.
  • Игнорирование тестирования доступности до момента, когда интерфейс уже полностью спроектирован и реализован. Исправление фундаментальных проблем доступности (неверная структура фокуса, отсутствие декларативной поддержки в самой архитектуре кастомных компонентов) обходится значительно дороже на позднем этапе разработки, чем встраивание этих требований с самого начала проектирования компонентов — чек-лист из примера эффективнее применять для каждого нового экрана сразу после его реализации, а не как финальную проверку перед релизом всего продукта.
  • Тестирование исключительно силами разработчиков без привлечения реальных пользователей вспомогательных технологий. Разработчик, тестирующий собственное приложение с screen reader, неизбежно знает, что и где искать, и может не заметить проблемы, очевидные для пользователя, который видит интерфейс впервые и не имеет этого контекста — там, где возможно, привлечение реальных пользователей с инвалидностью для тестирования даёт качественно иной, более достоверный уровень обратной связи, чем самотестирование команды разработки.