Modern QA2026Проверки интерактивности: Как Vibium гарантирует надёжные взаимодействия
Join

Course01 Agent Skills for Browser Automation

Cutting-edge · Chapter 01

Проверки интерактивности: Как Vibium гарантирует надёжные взаимодействия

Updated Jul 2026

Проблема

Веб-страницы динамичны. Когда вы говорите инструменту автоматизации «нажми кнопку отправки», кнопка может:

  • Ещё не существовать (всё ещё загружается)
  • Быть невидимой (скрыта CSS)
  • Перемещаться (CSS-анимация в процессе)
  • Быть закрыта другим элементом (модальное окно, прилипающий заголовок, всплывающая подсказка)
  • Быть отключена (валидация формы не пройдена)

Примитивный инструмент кликает немедленно и терпит неудачу. Продвинутый инструмент ожидает готовности элемента.

Пять проверок

Vibium реализует пять проверок интерактивности на стороне сервера в Go (не в клиентских библиотеках). Это критически важное архитектурное решение — оно означает, что все клиенты (JS, Python, CLI) получают идентичное поведение.

Проверка 1: Visible

Вопрос: Имеет ли элемент физические размеры и не скрыт ли он CSS?

(selector) => {
  const el = document.querySelector(selector);
  if (!el) return JSON.stringify({ error: 'not found' });

  const rect = el.getBoundingClientRect();
  if (rect.width === 0 || rect.height === 0) {
    return JSON.stringify({ visible: false, reason: 'zero size' });
  }

  const style = window.getComputedStyle(el);
  if (style.visibility === 'hidden') {
    return JSON.stringify({ visible: false, reason: 'visibility hidden' });
  }
  if (style.display === 'none') {
    return JSON.stringify({ visible: false, reason: 'display none' });
  }

  return JSON.stringify({ visible: true });
}

Что обнаруживает:

  • Элементы с display: none или visibility: hidden
  • Элементы с нулевой шириной или высотой (свёрнутые контейнеры)
  • Элементы, которые ещё не отрисовались

Проверка 2: Stable

Вопрос: Перестал ли элемент двигаться? (Позиция не изменилась за 50 мс)

// Запустить getBoundingClientRect() в момент T
// Подождать 50 мс
// Запустить getBoundingClientRect() в момент T+50 мс
// Сравнить: если позиция/размер изменились, элемент НЕ стабилен

Go-код запускает getBoundingClientRect() дважды с промежутком в 50 мс и сравнивает результаты.

Что обнаруживает:

  • CSS-переходы (выдвигающиеся панели, раскрывающиеся аккордеоны)
  • CSS-анимации (пульсирующие кнопки, вращающиеся спиннеры)
  • Анимации, управляемые JavaScript
  • Сдвиги макета от асинхронной загрузки контента

Почему 50 мс? Это достаточно долго, чтобы поймать большинство анимаций (которые работают на 16 мс/кадр), но достаточно коротко, чтобы не добавлять заметную задержку.

Проверка 3: ReceivesEvents

Вопрос: Если мы кликнем в центр элемента, достигнет ли клик его?

(selector) => {
  const el = document.querySelector(selector);
  if (!el) return JSON.stringify({ error: 'not found' });

  const rect = el.getBoundingClientRect();
  const centerX = rect.x + rect.width / 2;
  const centerY = rect.y + rect.height / 2;

  // Какой элемент на самом деле находится в этой точке?
  const hitTarget = document.elementFromPoint(centerX, centerY);
  if (!hitTarget) {
    return JSON.stringify({ receivesEvents: false, reason: 'no element at point' });
  }

  // Это наш элемент или дочерний элемент нашего элемента?
  if (el === hitTarget || el.contains(hitTarget)) {
    return JSON.stringify({ receivesEvents: true });
  }

  // Что-то другое его перекрывает
  return JSON.stringify({
    receivesEvents: false,
    reason: 'obscured by ' + hitTarget.tagName.toLowerCase()
  });
}

Что обнаруживает — самые коварные баги:

  • Модальные окна, перекрывающие кнопки
  • Прилипающие заголовки, перекрывающие контент при прокрутке
  • Баннеры согласия на использование куки, блокирующие страницу
  • Всплывающие подсказки, расположенные поверх кликабельных элементов
  • Невидимые <div>-оверлеи с высоким z-index

Проверка el.contains(hitTarget): Если <button> содержит <span> с текстом, клик в центр может попасть в <span>. Это нормально — клик всплывёт до кнопки. Поэтому мы проверяем, является ли целевой элемент дочерним элементом нашей цели.

Проверка 4: Enabled

Вопрос: Будет ли элемент реагировать на взаимодействие?

(selector) => {
  const el = document.querySelector(selector);
  if (!el) return JSON.stringify({ error: 'not found' });

  if (el.disabled === true) {
    return JSON.stringify({ enabled: false, reason: 'disabled attribute' });
  }

  if (el.getAttribute('aria-disabled') === 'true') {
    return JSON.stringify({ enabled: false, reason: 'aria-disabled' });
  }

  // Проверка, находится ли внутри отключённого fieldset
  const fieldset = el.closest('fieldset[disabled]');
  if (fieldset) {
    const legend = fieldset.querySelector('legend');
    if (!legend || !legend.contains(el)) {
      return JSON.stringify({ enabled: false, reason: 'inside disabled fieldset' });
    }
  }

  return JSON.stringify({ enabled: true });
}

Что обнаруживает:

  • Элементы <button disabled>
  • aria-disabled="true" (отключённое состояние с приоритетом доступности)
  • Элементы внутри <fieldset disabled> (кроме тех, что в первом <legend>)

Проверка 5: Editable (только для Type)

Вопрос: Принимает ли этот элемент текстовый ввод?

(selector) => {
  const el = document.querySelector(selector);
  if (!el) return JSON.stringify({ error: 'not found' });

  if (el.readOnly === true) {
    return JSON.stringify({ editable: false, reason: 'readonly attribute' });
  }
  if (el.getAttribute('aria-readonly') === 'true') {
    return JSON.stringify({ editable: false, reason: 'aria-readonly' });
  }

  const tag = el.tagName.toLowerCase();
  if (tag === 'input') {
    const type = (el.type || 'text').toLowerCase();
    const textTypes = ['text', 'password', 'email', 'number', 'search', 'tel', 'url'];
    if (!textTypes.includes(type)) {
      return JSON.stringify({ editable: false, reason: 'input type ' + type + ' not editable' });
    }
  }

  if (el.isContentEditable) return JSON.stringify({ editable: true });
  if (tag === 'input' || tag === 'textarea') return JSON.stringify({ editable: true });

  return JSON.stringify({ editable: false, reason: 'not a form element or contenteditable' });
}

Выполняется только для команд type. Не нужна для click, hover и т.д.

Какие проверки выполняются для каких действий?

Действие Visible Stable ReceivesEvents Enabled Editable
Click Да Да Да Да Нет
Type Да Да Да Да Да
Hover Да Да Да Нет Нет

Цикл автоожидания

Проверки выполняются не один раз — они работают в цикле опроса:

deadline = now + timeout (по умолчанию 30 секунд)

loop:
    for each check in required_checks:
        if check fails:
            if now > deadline:
                return TimeoutError("timeout after 30s waiting for
                    'button.submit': check 'ReceivesEvents' failed —
                    obscured by div.modal-overlay")
            sleep 100ms
            continue loop  ← перезапустить все проверки

    // ВСЕ проверки пройдены на ОДНОЙ итерации
    perform action

Ключевые особенности поведения:

  • Интервал опроса 100 мс — достаточно быстро, чтобы ловить изменения состояния, но не настолько, чтобы перегружать браузер
  • Тайм-аут по умолчанию 30 с — настраивается для каждого действия через --timeout
  • Все проверки должны пройти на одной итерации — если Visible прошла, но Stable не прошла, обе проверяются заново
  • Описательные ошибки тайм-аута — сообщают, КАКАЯ проверка не прошла и ПОЧЕМУ

Почему на стороне сервера (Go), а не клиента?

Причина Объяснение
Единственная реализация Написано один раз на Go, не дублируется в JS + Python + CLI
Снижение задержки Опрос происходит по локальному WebSocket, а не по маршруту клиент→прокси→браузер
Простые клиенты Клиентский код тривиален: отправить команду, ожидать успех/ошибку
Единообразное поведение Все клиенты получают идентичное время — нет багов «работает в JS, не работает в Python»

Клиентский код для клика становится просто:

async click(options?: { timeout?: number }): Promise<void> {
  await this.client.send('vibium:click', {
    context: this.context,
    selector: this.selector,
    timeout: options?.timeout,
  });
}

Вся сложность живёт в бинарнике Go.

Сравнение с Playwright

Концепция интерактивности Vibium пришла непосредственно от Playwright, который стал пионером этого подхода:

Аспект Playwright Vibium
Где выполняются проверки Клиентская библиотека (JS/Python) Сервер (бинарник Go)
Список проверок Те же пять проверок Те же пять проверок
Тайм-аут по умолчанию 30 с 30 с
Настраиваемость Для каждого действия Для каждого действия
Интервал повтора ~100 мс ~100 мс

Ключевое различие в том, где расположена логика. Playwright реализует её в каждой клиентской библиотеке. Vibium реализует её один раз в бинарнике Go, делая все клиенты автоматически единообразными.

Тезис для собеседования

«Vibium реализует проверки интерактивности в стиле Playwright — visible, stable, receives-events, enabled и editable — но делает это на стороне сервера в бинарнике Go, а не в клиентских библиотеках. Это означает, что каждый клиент (JavaScript, Python, CLI) получает идентичное поведение без дублирования логики. Проверки выполняются в цикле опроса с интервалом 100 мс и тайм-аутом по умолчанию 30 секунд. Когда проверка не проходит при тайм-ауте, сообщение об ошибке точно указывает, какая проверка не прошла и почему — например, "obscured by div.modal-overlay" — что делает отладку тривиальной. Это тот же паттерн, что установил Playwright, но с более поддерживаемой архитектурой.»