Modern QA2026Команды-расширения Vibium поверх BiDi
Join

Course01 Agent Skills for Browser Automation

Cutting-edge · Chapter 01

Команды-расширения 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
  }
}

Что делает прокси внутри:

  1. Запускает цикл опроса (интервал 100 мс)
  2. На каждой итерации отправляет script.callFunction в Chrome:
    {
      "method": "script.callFunction",
      "params": {
        "functionDeclaration": "(s) => !!document.querySelector(s)",
        "arguments": [{"type": "string", "value": "button.submit"}],
        "target": {"context": "browsing-context-id"}
      }
    }
    
  3. Если 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 явно поддерживает модули расширений с соглашением об именовании через двоеточие.»