Проверки интерактивности: Как 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, но с более поддерживаемой архитектурой.»