Команды-расширения Vibium поверх BiDi
Updated Jul 2026
Механизм расширений
Спецификация WebDriver BiDi явно поддерживает модули расширений, определяемые реализацией. Соглашение об именовании требует разделитель-двоеточие:
standard: browsingContext.navigate
extension: vibium:click
Это не хак — это спроектированная точка расширения в спецификации W3C.
Три команды-расширения Vibium
vibium:find
Назначение: Ожидание появления элемента в DOM.
Запрос:
{
"id": 1,
"method": "vibium:find",
"params": {
"context": "browsing-context-id",
"selector": "button.submit",
"timeout": 30000
}
}
Что делает прокси внутри:
- Запускает цикл опроса (интервал 100 мс)
- На каждой итерации отправляет
script.callFunctionв Chrome:{ "method": "script.callFunction", "params": { "functionDeclaration": "(s) => !!document.querySelector(s)", "arguments": [{"type": "string", "value": "button.submit"}], "target": {"context": "browsing-context-id"} } } - Если
true— возвращает успех. Еслиfalse— повторяет до тайм-аута.
Ответ при успехе:
{"id": 1, "type": "success", "result": {"found": true}}
Ответ при тайм-ауте:
{
"id": 1,
"type": "error",
"error": {"error": "timeout", "message": "timeout after 30s waiting for 'button.submit'"}
}
vibium:click
Назначение: Ожидание, пока элемент станет интерактивным, затем клик по нему.
Запрос:
{
"id": 2,
"method": "vibium:click",
"params": {
"context": "browsing-context-id",
"selector": "button.submit",
"timeout": 30000
}
}
Что делает прокси внутри:
Step 1: Find element (vibium:find behavior)
→ script.callFunction: document.querySelector(selector) exists?
→ Repeat until found or timeout
Step 2: Check Visible
→ script.callFunction: getBoundingClientRect + getComputedStyle
→ Must have non-zero size, not hidden
Step 3: Check Stable
→ script.callFunction: getBoundingClientRect at T
→ Wait 50ms
→ script.callFunction: getBoundingClientRect at T+50ms
→ Compare: must be identical
Step 4: Check ReceivesEvents
→ script.callFunction: elementFromPoint at center
→ Must hit the target element (or its child)
Step 5: Check Enabled
→ script.callFunction: check disabled, aria-disabled, fieldset
→ Must not be disabled
Step 6: Get bounding box
→ script.callFunction: getBoundingClientRect
→ Calculate center coordinates: (x + width/2, y + height/2)
Step 7: Perform click
→ input.performActions:
pointerMove to (centerX, centerY)
pointerDown button 0
pointerUp button 0
Все шаги 1-5 выполняются в цикле опроса. Если любая проверка не проходит, ожидание 100 мс и перезапуск с шага 1.
Ответ при успехе:
{"id": 2, "type": "success", "result": {"clicked": true}}
vibium:type
Назначение: Ожидание, пока элемент станет интерактивным И редактируемым, затем ввод текста.
Запрос:
{
"id": 3,
"method": "vibium:type",
"params": {
"context": "browsing-context-id",
"selector": "input[name=email]",
"text": "user@example.com",
"timeout": 30000
}
}
Что делает прокси внутри:
То же, что и шаги 1-5 vibium:click, ПЛЮС:
Step 5b: Check Editable
→ script.callFunction: check readonly, aria-readonly, input type
→ Must accept text input
Step 6: Focus element
→ script.callFunction: document.querySelector(selector).focus()
Step 7: Clear existing text (if any)
→ input.performActions: Ctrl+A, then Delete
Step 8: Type text character by character
→ input.performActions:
For each character in "user@example.com":
keyDown character
keyUp character
Посимвольный ввод вызывает все ожидаемые события DOM: keydown, keypress, input, keyup. Это критически важно для:
- Валидации форм, работающей по событиям
input - Автодополнения, срабатывающего при нажатии клавиш
- Ограничений на количество символов
- Поиска в реальном времени
Почему не стандартные команды BiDi?
Стандартный BiDi имеет input.performActions для кликов и ввода с клавиатуры. Зачем добавлять пользовательские команды?
Без команд-расширений (клиент должен реализовать сам)
Client Chrome
│ │
│─ script.callFunction ─────────►│ (check if element exists)
│◄─ result: false ───────────────│
│ wait 100ms │
│─ script.callFunction ─────────►│ (check again)
│◄─ result: true ────────────────│
│─ script.callFunction ─────────►│ (check visible)
│◄─ result ──────────────────────│
│─ script.callFunction ─────────►│ (check stable T1)
│◄─ result ──────────────────────│
│ wait 50ms │
│─ script.callFunction ─────────►│ (check stable T2)
│◄─ result ──────────────────────│
│─ script.callFunction ─────────►│ (check receivesEvents)
│◄─ result ──────────────────────│
│─ script.callFunction ─────────►│ (check enabled)
│◄─ result ──────────────────────│
│─ script.callFunction ─────────►│ (get bounding box)
│◄─ result ──────────────────────│
│─ input.performActions ─────────►│ (click)
│◄─ result ──────────────────────│
Минимум 9 round trip — каждый добавляет сетевую задержку, если клиент удалённый.
С командами-расширениями (прокси обрабатывает)
Client Proxy Chrome
│ │ │
│─ vibium:click ───►│ │
│ │─ script.call ───►│ (all checks happen locally)
│ │◄─ result ────────│
│ │─ script.call ───►│
│ │◄─ result ────────│
│ │ ... (local loop)│
│ │─ input.perform ─►│
│ │◄─ result ────────│
│◄─ success ────────│ │
Клиент отправляет 1 сообщение, получает 1 ответ. Вся сложность находится в прокси, который взаимодействует с Chrome по локальному WebSocket (по сути нулевая задержка).
Реализация на Go
Расположена в clicker/internal/proxy/router.go:
func (r *Router) OnClientMessage(msg []byte) {
var req BiDiMessage
json.Unmarshal(msg, &req)
switch req.Method {
case "vibium:find":
r.handleVibiumFind(req)
case "vibium:click":
r.handleVibiumClick(req)
case "vibium:type":
r.handleVibiumType(req)
default:
r.forwardToChrome(msg) // Standard command: pass through
}
}
func (r *Router) handleVibiumClick(req BiDiMessage) {
selector := req.Params.Selector
timeout := req.Params.Timeout
deadline := time.Now().Add(time.Duration(timeout) * time.Millisecond)
for {
if time.Now().After(deadline) {
r.sendError(req.ID, "timeout", fmt.Sprintf(
"timeout after %dms waiting for '%s': check '%s' failed",
timeout, selector, lastFailedCheck))
return
}
// Run all actionability checks via script.callFunction
if !r.checkVisible(selector) { sleep(100ms); continue }
if !r.checkStable(selector) { sleep(100ms); continue }
if !r.checkReceivesEvents(selector) { sleep(100ms); continue }
if !r.checkEnabled(selector) { sleep(100ms); continue }
// All checks passed — perform the click
box := r.getBoundingBox(selector)
r.performClick(box.CenterX, box.CenterY)
r.sendSuccess(req.ID, map[string]bool{"clicked": true})
return
}
}
Тезис для собеседования
«Vibium использует механизм расширений WebDriver BiDi —
vibium:find,vibium:click,vibium:type— для переноса логики проверки интерактивности в прокси-сервер. Без этого каждый клик потребовал бы 8-9 WebSocket round trip между клиентом и браузером только для проверок интерактивности. С командами-расширениями клиент отправляет одно сообщение и получает один ответ. Прокси обрабатывает цикл опроса локально, где задержка пренебрежимо мала. Это ключевой архитектурный вывод: размещая логику вместе с подключением к браузеру, вы получаете и более простых клиентов, и меньшую задержку. И это не хак протокола — BiDi явно поддерживает модули расширений с соглашением об именовании через двоеточие.»