Перейти к содержимому

Файл флоу и селекторы

Флоу — один YAML-файл. Ключи верхнего уровня:

Ключ Кратко
appId Приложение по умолчанию — bundle / package id, или URL для web
commands Упорядоченный список шагов (единственный обязательный ключ)
tags Теги для фильтрации --tags / --exclude-tags
before Шаги подготовки или clean; ошибка здесь пропускает commands (onFlowStart — старое имя)
after Шаги уборки — выполняются всегда, что бы ни случилось (onFlowComplete — старое имя)
inheritHooks false отключает before/after, унаследованные от проекта и testspace
env Переменные флоу, используются как ${VAR}; config env:, .env, -e их перекрывают
params Объявленные входы: description, required, default, options; вызывающие передают их через env: в runFlow или -e
device none = чисто API-флоу: устройство не ищется и не поднимается
db Строка подключения по умолчанию для шагов sql — обычно ${DATABASE_URL}
http Общие настройки шагов request: baseUrl, headers (${} на каждом шаге), cookies; http: из config.yaml лежит под ним
vscode Цель — расширение VS Code: testspace, vsix, workspace, settings
appId: https://staging.example.com
tags: [smoke]
env:
USER: qa@example.com
before:
- launchApp: https://staging.example.com
commands:
- tapOn: "Log in"
- inputText: "${USER}"
- assertVisible: "Welcome"
after:
- takeScreenshot: end

Команды с целью-элементом принимают голую строку (точный видимый текст) или объект-селектор. Матчеры, фильтры и отношения комбинируются; не больше одного реляционного ключа на селектор, а css отрабатывает первым — остальные ключи селектора фильтруют то, что он нашёл. Значения — строки: числа берите в кавычки (text: "2025").

above, below, leftOf и rightOf смотрят на собственную линию якоря: строку для leftOf/rightOf, колонку для above/below. Поэтому leftOf подписи — это контрол рядом с этой подписью, а не всё, что левее её на странице, и подпись следующей строки не отвечает за эту. Совпадения упорядочены по близости к якорю — в этом же порядке считает index, — так что { role: switch, leftOf: "Draft" } — ближайший к Draft переключатель. Если на линии якоря не стоит вообще ничего, работает прежнее, более широкое правило (только центр якоря, в порядке обхода дерева), поэтому селектор, написанный против подписи сбоку, продолжает разрешаться.

В браузере contains читает и сквозь строчные теги: <p>Signed in as <strong>ann@x.test</strong></p> — одно предложение, и contains: "as ann@x" находит <p>, а фраза, которую целиком держит ребёнок, по-прежнему находит этого ребёнка. text по-прежнему означает собственный текст элемента.

text и contains читаются дважды. Сначала точно так, как написано во флоу; и только если на экране так не отвечает ничего, экран читается второй раз со схлопнутыми пробелами (включая неразрывный) и без учёта регистра — поэтому "log in" находит LOG IN, а подпись, в которой появился перенос строки, продолжает совпадать. Точное совпадение всегда важнее, index за пределами списка не повод ослаблять текст, а шаг, совпавший только вторым чтением, несёт пометку matched ignoring case/spacing. Второе чтение — для шагов, которые ищут элемент: assertNotVisible, waitForNotVisible и условие notVisible читают текст только так, как он написан, потому что экран, который пишет его иначе, этого текста на себе уже не держит.

of — это то, что делает index проверяемым: он записывает, сколько элементов совпало на момент написания шага, и ничего не фильтрует. { text: "Log In", index: 1, of: 2 } на странице, где теперь совпадает три, даст пометку 2 matched when this was recorded, 3 match now — the index may point at the wrong one, а пока число совпадает — промолчит. Индексированный селектор без of отмечается один раз за прогон. Шаг с css считает так же и, как и обход дерева, не берёт скрытые элементы ни в счёт, ни в нумерацию, по которой идёт index. Совпадение по css — обычный узел этого обхода: отношения, scrollUntilVisible и centerElement читают его как любое другое, а index считает страницу целиком — сначала главный документ, затем каждый её фрейм, в одной нумерации. { css: ".row", text: "Save" } — это найденные CSS строки, из которых осталась та, чей текст Save; если после фильтра не осталось ничего, шаг падает как «не найдено» и называет оба ключа.

Имя, которое достаёт и до контрола, и до подписи с теми же словами, — это одна вещь, на которую нажимают, а не две. Шаг, который действует на элемент (tapOn, doubleTapOn, longPressOn, hover, check/uncheck, selectOption, uploadFile, dragAndDrop, scrollUntilVisible), разрешается только в контролы; шаг, который читает слово (copyText) или доказывает, что оно на экране (assertVisible, waitForVisible, assertNotVisible, условие when:), совпадает со всеми элементами, до которых достаёт имя. Если контролов осталось несколько — это та же неоднозначность, что и всегда: первое совпадение, index, отношение. Если имени не отвечает ни один контрол, слово нажимается всё равно, а шаг несёт пометку matched only text, nothing to press — её никогда не несёт селектор, назвавший role, потому что он уже сказал, какой элемент имеет в виду. Контрол, который носит своё имя на обёрнутом элементе (<button><span>Save</span></button>, где у самой кнопки текста нет), — контрол: нажимают слово внутри него, а не подпись рядом. index выключает правило: число уже выбрало, какое совпадение имелось в виду, и считает все элементы, включая текстовые узлы.

or даёт селектору до трёх запасных. Каждый — полноценный селектор со своими index/of; вкладывать альтернативы нельзя. Сначала пробуется основной, и только на устоявшейся странице — и только если основной не совпал ни с чем — альтернативы читаются по порядку. Попадание в одну из них проводит шаг и помечает строку drifted: в заметке будет found by alternative 2 (…) — the primary selector no longer matches, в JSON-отчёте — поле drifted, в JUnit — <property name="drifted">, в HTML-отчёте — чип, а итог прогона их считает. Код возврата не меняется, пока не задан tapstep test --strict-selectors, который валит прогон, опершийся на запасной селектор. Экспортёры пишут только основной, а альтернативы называют в комментарии на той же строке.

or принадлежит селектору, а не одной команде: assertNotVisible зелёный, только когда на экране нет ни основного, ни одной из альтернатив; условие when: / while: открывается по любой из них, а scrollUntilVisible останавливается, как только одна из них попала в видимую область.

Ключ Кратко
text Точное совпадение с любым именем элемента — собственный текст, accessibility label, текущее значение или плейсхолдер (голая строка-селектор — это text)
id Accessibility id / resource id / DOM id
testId Тестовый хук, проставленный приложением: data-testid/data-test/data-qa/data-cy в вебе, accessibility identifier в нативе — самый стабильный матчер (только на web, vscode)
role Роль в стиле ARIA, обычно вместе с name (нативные классы виджетов ложатся на тот же словарь) — движок называет: button | checkbox | radio | switch | menuitem | tab | option | link | textbox | searchbox | combobox | spinbutton | slider | list | listitem | dialog | navigation
name Доступное имя — label, если он есть, иначе собственный текст; пробелы схлопываются. Используйте вместе с role
label Accessibility label / текст <label> — продолжает совпадать после заполнения поля
placeholder Плейсхолдер / подсказка, видимая пока поле пустое
value Текущее содержимое поля
contains Подстрока любого имени элемента
regex Регулярное выражение по любому имени элемента (без подстановки ${})
index Взять N-е совпадение (с нуля)
of Сколько элементов совпало на момент записи шага — ничего не фильтрует; прогон отметит, если страница отвечает другим числом
enabled Фильтр по состоянию enabled (true/false)
checked Фильтр по состоянию checked
focused Фильтр по фокусу
selected Фильтр по состоянию selected
traits Подсказки формы: text, long-text, square (только на ios)
width Ожидаемая ширина в px (с tolerance)
height Ожидаемая высота в px (с tolerance)
tolerance Допуск ± px для width / height
css CSS-селектор; остальные ключи сужают то, что он нашёл (только на web, vscode)
or До 3 запасных селекторов, которые пробуются по порядку, когда основной перестал совпадать на устоявшейся странице; шаг проходит, а отчёт помечает строку как drifted
above Отношение: совпадение выше этого селектора, в его колонке
below Отношение: ниже этого селектора, в его колонке
leftOf Отношение: левее этого селектора, в его строке
rightOf Отношение: правее этого селектора, в его строке
childOf Отношение: потомок этого селектора
containsChild Отношение: имеет этот селектор прямым ребёнком
containsDescendants Отношение: содержит все эти селекторы (список)
commands:
- tapOn:
text: "Delete"
index: 1
of: 3
- assertVisible:
contains: "items"
below:
id: cart-title
- tapOn:
css: "button[data-testid=pay]"
- tapOn:
text: "Place order"
or:
- testId: place-order
- role: button
rightOf: "Total"
- check:
role: switch
leftOf: "Published"
index: 0
- assertVisible:
id: card
containsDescendants:
- text: "Visa"
- contains: "4242"

В обоих можно писать те же шаги, что и в commands: — runFlow, request, sql, launchApp, openLink, vscodeCommand, — и все пошаговые возможности (when, retry, repeat, ${}) работают внутри без изменений. onFlowStart: / onFlowComplete: — старые имена этих двух ключей, они всё ещё работают.

before выполняется перед шагом 1. after выполняется после последнего шага всегда: после упавшего шага, после before, сломавшегося на половине, после остановки — и каждая строка многошагового after выполняется, даже если одна из них упала. Падение в before валит флоу отдельным вердиктом — setup failed at B2: …, — и пронумерованные шаги не выполняются; падение в after отчитывается рядом с вердиктом как teardown failed at A1: … и не меняет его. В отчётах и в экране прогона десктопа это блоки Setup и Teardown со строками B1.. / A1..; пронумерованные шаги остаются 1..N.

Свои before: / after: может объявить config.yaml проекта — они применяются ко всем флоу в нём — и testspace VS Code (vscode.testspaces.<name>.before / .after). Они оборачивают флоу: внешние входят первыми и выходят последними:

project.before → testspace.before → flow.before → шаги
→ flow.after → testspace.after → project.after

before: [] во флоу значит «своих нет», а не «нет вообще»; чтобы отключить унаследованные, напишите в шапке inheritHooks: false. Именно эта собранная цепочка попадает в test.beforeEach / test.afterEach спеки, которую пишет tapstep export playwright.

Хук любого слоя может вызвать хелпер, как и всякий блок. Путь его runFlow: — и исходник runScript: — разрешается относительно папки того слоя, который его написал (корня проекта для config.yaml, папки конфига для testspace), а затем от корня проекта — это правило для всех include (см. Хелперы). Отсутствующий хелпер ловится до того, как что-либо начнёт выполняться, — и командой validate, и командой test.

before: clean — строка вместо списка — возвращает окно VS Code в известное состояние перед шагами: vscodeCommand × workbench.action.closeAllEditors, workbench.action.closePanel, workbench.action.closeAuxiliaryBar, notifications.clearAll, workbench.action.terminal.killAll, workbench.action.closeSidebar. Боковая панель закрывается последней, потому что иконка в activity bar — переключатель: нажатие открывает вид, только если панель была скрыта, а в удерживаемом окне панель переживает прогон, который её открыл. На любой другой платформе clean — ошибка валидации, которая называет, что писать вместо него (web: openLink и clearState; мобильные: launchApp с clearState: true).

vscode: { testspace: pd-full }
before: clean
commands:
- vscodeCommand: { title: "My Ext: Generate Config" }
- assertFile: gen/config.yaml
after:
- vscodeCommand: workbench.action.closePanel

vscodeCommand выполняет команду VS Code напрямую: по id (голое workbench.action.closeAllEditors или { id, args } — args только с id) через helper-расширение, которое кладёт рядом драйвер и которое отвечает, когда команда завершилась; по { title: … } — через палитру команд. Экспорт в Playwright умеет только печатать заголовки, поэтому id без известного заголовка палитры остаётся // TODO, а не превращается в команду, делающую другое.

Любое строковое значение может содержать ${NAME}. Имя ищется по порядку: регистр копирования (copiedText), переменные флоу (env:, дефолты params:, значения из copyText, extractTextWithAI, request.extract, JS output.*), затем окружение процесса; неизвестное имя становится пустой строкой. Значения слоятся — полный порядок в config.yaml (дефолты params: < env: флоу < env: конфига < environments.<текущее> < .env < .env.<текущее> < --env-file < -e). --env-set NAME в этой цепочке не стоит: он выбирает, каким будет <текущее> у двух слоёв окружения, а выбранное имя — это ${TAPSTEP_ENV}, переменная того же слоя окружения, так что -e TAPSTEP_ENV=… всё равно её перебьёт. В regex: подстановки нет.

JS-выражение внутри значения env: — EMAIL: "qa+${Date.now()}@example.com" — вычисляется один раз, до первого шага, и результат запоминается: значение свежее на каждый прогон и одинаковое во всех шагах, которые его читают. То же выражение, написанное прямо в команде, вычисляется на каждой команде. env: у runFlow разрешается так же на входе в блок (один раз на область, включая вложенные), а сломанное выражение валит прогон с именем ключа.

params: объявляет входы флоу, чтобы вызывающие и десктоп могли их проверить:

params:
USER:
description: Логин для входа
required: true
PLAN:
default: free
options: [free, pro]
RETRIES: "3" # сокращение для { default: "3" } — голое число не принимается

Отсутствующий required-параметр роняет прогон до первого шага; options ограничивает значение. Передаются через env: в runFlow, -e NAME=value или цепочку конфига выше.

Три команды задают структуру: runFlow (вставить хелпер или вложенный блок, опционально под when), repeat (times или while) и retry. Блоки свободно вкладываются. Условия: { visible: <селектор> }, { notVisible: <селектор> } или { platform: web | android | ios | vscode | [список] } (while принимает только формы видимости). Окно VS Code отвечает vscode, а не web, — поэтому блок, который должен идти и в браузере, и в редакторе, пишется как { platform: [web, vscode] }.

Хелпер — файл флоу с именем *.helper.yaml. Устроен как флоу (свои params:/env:), но в место вызова runFlow вставляются только его commands; appId, tags и хуки игнорируются. Правила:

  • runFlow: <путь> принимает только .helper.yaml. Путь ищется сначала в папке вызывающего файла, затем от корня проекта (папки ближайшего config.yaml); \ читается как /. Пишите путь от корня — helpers/login.helper.yaml, — и та же строка работает во флоу на любой глубине: шаги, скопированные между тестами, не ломаются. Хелперы могут вызывать хелперы, циклы — ошибка.
  • То же правило действует для всех файлов, которые называет шаг: исходник runScript:, схема request:, файл uploadFile:, пакет installApp: и хелперы хуков before:/after:.
  • tapstep validate загружает каждый include оттуда, где лежит файл, и по строкам сообщает: хелпера нет по этому пути (и какой файл с таким именем нашёлся в проекте), обязательные параметры не переданы (все, по именам), цикл, а предупреждением — хелпер за пределами проекта. tapstep fix-paths переписывает старые пути с ../ от корня.
  • tapstep test не запускает хелпер напрямую, а прогон по папке их пропускает.
  • Обязательные params: хелпера проверяются при загрузке и берутся из env: вызова, переменных вызывающего флоу или окружения процесса.
  • В отчёте вызов хелпера — один шаг с вложенными шагами хелпера.

evalScript, runScript и JS-форма assertTrue делят один QuickJS-контекст на прогон. Внутри доступны:

  • все переменные флоу с именем-идентификатором как глобалы (плюс copiedText);
  • output — объект; каждый ключ становится ${output.<ключ>} для следующих шагов;
  • console.log/info/warn/error — попадают в отчёт шага;
  • json(text), maestro.copiedText и блокирующий http.get/post/put/delete/request(url, { headers, body }) с ответом { status, ok, body } (таймаут 30 с).

device: none запускает флоу без поиска и загрузки устройства: разрешены только request, sql, скрипты, assertTrue и управление потоком. См. API-тестирование.

Заголовок vscode: делает флоу тестом расширения VS Code и выбирает VS Code-драйвер:

vscode:
testspace: default # именованная запись из config.yaml vscode.testspaces
vsix: [dist/my-ext.vsix] # или один путь; ${} подставляется
workspace: fixtures/ws
settings:
editor.minimap.enabled: false

Явные поля перекрывают поля testspace. Про vscode.testspaces — в config.yaml.

  • Файл флоу — один YAML-документ; commands — единственный обязательный ключ.
  • Неизвестные ключи верхнего уровня молча игнорируются — опечатка вроде platfrom: не даст ошибки. Подмапы params и vscode строгие.