# Элементы и ссылки

> Что приходит в elements, почему ref — не селектор и что заказывать в include

Источник: https://docs.parreq.com/browser-elements/

Агент не видит пикселей. Вместо снимка с координатами Browser API отдаёт список
того, на что можно нажать, — с устойчивой ссылкой на каждый элемент.

Список приходит в **каждом** ответе: и от `open`, и от `click`, и от `fill`.
Отдельной ручки «дай список» нет — она была бы платным шагом, который ничего не
меняет на странице.

## Что заказывать в `include`

| Значение | Что приходит | Цена |
|---|---|---|
| `buttons` | кнопки, ссылки и всё, у чего роль кнопки | входит в шаг |
| `fields` | поля ввода, `textarea`, `select` | входит в шаг |
| `text` | видимый текст страницы | входит в шаг |
| `html` | разметка целиком | платная надстройка |

Значения перечисляются через запятую: `"include": "buttons,fields,text"`.
Умолчание — `buttons,fields`. Неизвестные значения молча отбрасываются.

Заказывается раздельно и намеренно: полная разметка большой страницы — сотни
килобайт, из которых агенту нужны десять строк со списком кнопок. Для агента это
не только деньги, но и место в контексте.

> **Совет.**
>   Начинайте с `buttons,fields`. Добавляйте `text`, когда нужно прочитать, что
>   написано на странице, и `html` — только когда нужен разбор разметки, которого
>   списком элементов не сделать.

## Что считается кнопкой

Сперва берётся то, что кнопкой названо в разметке: `button`, `a[href]`,
`input[type=submit|button]` и всё с `role="button"`.

Потом — нажимаемое, которое кнопкой назваться не удосужилось. Живые сайты полны
такого: `<div class="ps-btn-float">` с обработчиком вместо честного `<button>`.
Берём только то, у чего намерение видно из разметки — обработчик прямо на
элементе либо `btn`/`button` отдельным словом в классе, — и только если элемент
не вложен в другого кандидата и сам не содержит кандидата. Иначе одна кнопка
приезжала бы трижды, слоями.

Из вложенных друг в друга кандидатов берётся **внутренний**. Обёртка кнопок на
карточке товара зовётся `buy_block btn-actions__inner`: слово `btn` в классе
есть, значит она кандидат, и в обходе документа идёт раньше своих детей. Отдать
её значило бы отдать агенту одну кнопку с подписью «В корзину Купить в 1 клик»,
нажатие в середину которой попадает не туда, куда он собирался.

> **Примечание.**
>   Обработчик, повешенный из скрипта, из разметки не виден никак, и такие
>   элементы в список не попадают. Если нужная кнопка не пришла — проверьте, есть
>   ли рядом `a[href]`, который делает то же самое.

## Откуда берётся подпись

Собранная кнопка без имени бесполезна: агент видит `ref` и не знает, что
нажимает. А имя на живых сайтах лежит где угодно, только не на самом элементе.
Вёрстка вроде такой разносит фон, волну, текст и значок по отдельным слоям:

```html
<div class="ps-btn-float">
  <div class="ps-btn-float-background"></div>
  <div class="ps-btn-float-wave"></div>
  <div class="ps-btn-float-text">Онлайн запись</div>
  <div class="ps-btn-float-icon"></div>
</div>
```

Поэтому подпись ищется по порядку:

**1. Свой текст**

    Видимый текст элемента вместе с потомками — здесь и находится «Онлайн
    запись».

**2. Свои подписи**

    `aria-label`, `title`, `alt` на самом элементе.

**3. Подписи потомков**

    Те же атрибуты и `alt` у картинок внутри — так именуются кнопки, у которых
    вместо текста один значок.

**4. Имя значка**

    `<svg><use href="#icon-basket">` — подписи нет нигде, кроме ссылки на сам
    значок. Такая кнопка приходит как «значок basket»: подписью это не
    является, но агенту говорит больше пустоты.

**5. Родители, до трёх уровней**

    Их `aria-label` и `title`, а текст — только если он короткий, до 40 знаков,
    **и только если нажимаемое в этом блоке одно**. Иначе текст относится ко
    всему блоку: восемь точек карусели рядом с заголовком раздела получали имя
    этого заголовка — все восемь одинаковое и все восемь неверное.

**6. Класс, как последняя зацепка**

    `class="owl-dot"` превращается в «owl dot». Тоже не подпись, но список из
    восьми пустых строк бесполезен: по нему нельзя отличить одну кнопку ни от
    другой, ни от чего-то важного.

> **Примечание.**
>   Кнопка без имени не приходит вовсе: у каждой в списке есть, чем её назвать.
>   Два последних правила — честная догадка, а не подпись, и читаются они как
>   догадка: «значок basket», «owl dot».

## Как устроен элемент

```json
{
  "ref": "pq1-k3f9x",
  "kind": "field",
  "field": "email",
  "tag": "input",
  "type": "email",
  "name": "email",
  "text": "Почта",
  "value": "",
  "required": true
}
```

- **`ref`** (`string`):
  Ссылка на элемент. Её и передают в `click`, `fill` и `form`.

- **`kind`** (`string`):
  `button` или `field` — на что это годится: нажать или заполнить.

- **`field`** (`string`):
  Что за поле: `text`, `tel`, `email`, `number`, `password`, `textarea`,
  `select`, `radio`, `checkbox`. По нему и решается, **как** его заполнять —
  см. [form](https://docs.parreq.com/browser-form). У кнопок этого поля нет.

- **`tag`** (`string`):
  Настоящий тег: `button`, `a`, `input`, `select`, `textarea`.

- **`type`** (`string`):
  Атрибут `type` как он написан в разметке. У `select` и `textarea` его нет —
  для них смотрите `field`.

- **`name`** (`string`):
  Имя поля из разметки. Помогает опознать поле, когда подпись у него невнятная.

- **`text`** (`string`):
  Подпись, которую видит человек: надпись на кнопке, метка поля.

- **`value`** (`string`):
  Что сейчас в поле. У `select` — подпись выбранного пункта, а не служебное
  значение. У переключателей не приходит: там его заменяет `checked`.

- **`checked`** (`boolean`):
  Выбран ли переключатель. Только у `radio` и `checkbox`.

- **`group`** (`string`):
  Имя группы переключателей. У радиокнопок одной группы оно общее, и выбор
  одной снимает выбор с остальных: `PAY_SYSTEM_ID`, `DELIVERY_ID`.

- **`options`** (`array`):
  Пункты списка, подписями. Только у `select`; не больше сорока.

- **`required`** (`boolean`):
  Помечено ли поле обязательным.

## Переключатели и списки

Способ доставки и способ оплаты в интернет-магазине — это переключатели, а не
поля ввода. Отличать их надо до заполнения: в радиокнопку нельзя напечатать
текст, а уже выбранную нельзя нажать повторно — нажатие её снимет.

> **Внимание.**
>   Переключатель на витрине почти всегда **спрятан**: сам `<input>` нулевого
>   размера, а кружок или галочка нарисованы стилями поверх него. Такие поля в
>   список попадают — по видимой подписи, а не по своему размеру. Иначе со
>   страницы оформления заказа пропадало бы ровно то, ради чего на неё приходят.

Подпись переключателя ищется тремя способами подряд: по `label[for]`, по
вложенности в `<label>` и по соседству — Битрикс кладёт поле и его подпись
рядом, не связывая их ничем:

```html
<input type="radio" name="PAY_SYSTEM_ID" value="1" checked>
<label><span>Наличными, картой</span><span class="box"></span></label>
```

Без третьего способа такое поле приезжало бы с именем `PAY_SYSTEM_ID` — по
нему оплату при получении не выбрать.

## Почему `ref`, а не селектор

`ref` — это не селектор и не путь в разметке. Селектор ломается от первой же
перерисовки: у современных сайтов классы генерируются сборщиком и меняются между
выкладками, а порядок узлов меняется прямо во время работы.

Поэтому каждому элементу проставляется собственная метка, и по ней он находится
дальше — независимо от того, что случилось с его классами и соседями.

> **Примечание.**
>   Метки живут в пределах сессии и меняются от шага к шагу. Не сохраняйте `ref`
>   надолго и не переносите его между сессиями — берите список из последнего
>   ответа.

## Если элемент исчез

Приходит `element_not_found`. Это не сбой, а сигнал: страница перерисовалась,
и список пора запросить заново.

```python
res = click(session, ref)
if res.get("error", {}).get("code") == "element_not_found":
    # список из предыдущего ответа устарел — берём свежий
    res = click(session, find_ref(last_elements, "Войти"))
```

Ищите элемент по подписи `text` или по `name`, а не по позиции в массиве:
порядок элементов не обещан и меняется вместе со страницей.

## Дальше

**[click](https://docs.parreq.com/browser-click)**
    Нажать на элемент из списка.

**[fill](https://docs.parreq.com/browser-fill)**
    Заполнить поле из списка.
