Программа, которая сама нажимает кнопки в браузере и ждёт, пока страница ответит
expect, который сам дожидается нужного состоянияpagecodegenPlaywright запускает браузер и управляет им: открывает адреса, заполняет поля, нажимает кнопки, читает текст. По умолчанию браузер работает без окна (headless) — так быстрее, и так тесты идут на сервере CI. Скрипт входа в «Задачник», запущенный командой python tasker.py --reset:
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page(viewport={"width": 1000, "height": 720})
page.goto("http://127.0.0.1:8000/login")
print(page.title())
page.get_by_label("Логин").fill("ivan")
page.get_by_label("Пароль").fill("Ivan2026pass")
page.get_by_role("button", name="Войти").click()
print(page.url)
print(page.get_by_test_id("task").count(), "задач на странице")
page.screenshot(path="tasks.png")
browser.close()
$ python first.py
Вход — Задачник
http://127.0.0.1:8000/tasks
8 задач на странице
Снимок, который сделал скрипт (page.screenshot):

with sync_playwright() as p запускает Playwright и останавливает его в конце блока. Дальше цепочка: браузер → страница (вкладка) → действия. В тестах эту цепочку берёт на себя плагин pytest-playwright — ниже.
Локатор — описание, как найти элемент. От выбора локатора больше всего зависит, переживёт ли тест следующую правку вёрстки. Playwright советует искать элементы так, как их находит человек: «кнопка Войти», «поле Логин».
| Локатор | Находит | Пример в «Задачнике» |
|---|---|---|
get_by_role("button", name="Войти") | Элемент по роли и доступному имени — как его объявит программа чтения с экрана | Кнопки, ссылки ("link"), заголовки ("heading"), сообщение об ошибке ("alert") |
get_by_label("Логин") | Поле по тексту его подписи <label> или aria-label | Логин, Пароль, Название, Срок, Статус |
get_by_text("Задач нет.") | Элемент по видимому тексту | Сообщения, ячейки |
get_by_placeholder("Поиск") | Поле по подсказке внутри | Когда подписи нет |
get_by_test_id("task") | По атрибуту data-testid, который разработчики ставят специально для тестов | Строка задачи, счётчики, статус в строке |
locator("#tasks tr:nth-child(2) td") | По CSS или XPath | Последнее средство |
Почему CSS-цепочки — последнее средство: #tasks tr:nth-child(2) td:nth-child(4) сломается, если добавят колонку или поменяют порядок строк, хотя для пользователя ничего не изменилось. get_by_role("button", name="Войти") переживёт любую перестановку, пока на странице есть кнопка «Войти». Бонус: если элемент нельзя найти по роли или подписи, его не найдёт и незрячий пользователь — локаторы заодно проверяют доступность.
Локаторы уточняют цепочкой и фильтром:
row = page.get_by_test_id("task").filter(has_text="Купить бумагу") # строка задачи с этим текстом
status = row.get_by_test_id("status") # статус в этой строке, а не первый на странице
| Действие | Что делает |
|---|---|
page.goto("/login") | Открыть адрес (относительно base_url) |
.fill("текст") | Очистить поле и ввести текст |
.click() | Щёлкнуть |
.select_option("in_progress") | Выбрать пункт списка по значению |
.check() / .uncheck() | Флажок |
.press("Enter") | Нажать клавишу в элементе |
Перед каждым действием Playwright сам ждёт, пока элемент появится, станет видимым, доступным и перестанет двигаться. Поэтому в тестах не нужно time.sleep: после click() на «Войти» следующая строка дождётся новой страницы.
Проверки — функция expect. Она тоже ждёт: expect(локатор).to_have_text("ivan") повторяет проверку, пока текст не станет нужным или не выйдет время (по умолчанию 5 секунд). Обычный assert локатор.inner_text() == "ivan" прочитал бы текст один раз — возможно, до того, как страница обновилась.
| Проверка | Ждёт, пока |
|---|---|
expect(page).to_have_url(…), to_have_title(…) | Адрес или заголовок станет таким (строка или регулярное выражение) |
to_be_visible(), to_be_hidden() | Элемент станет видимым или скрытым |
to_have_text("…"), to_contain_text("…") | Текст совпадёт целиком или будет содержать; список — для нескольких элементов |
to_have_count(8) | Элементов станет столько |
to_have_value("…") | В поле будет это значение |
to_be_enabled(), to_be_checked() | Кнопка доступна, флажок отмечен |
Плагин pytest-playwright (стоит из requirements.txt) даёт фикстуру page — новую вкладку в чистом браузере для каждого теста: без cookie и данных предыдущих тестов. Адрес «Задачника» он берёт из фикстуры base_url стартового проекта, поэтому в goto пишут только путь.
"""Первые UI-тесты «Задачника»: вход, создание задачи, смена статуса, фильтр."""
import re
from playwright.sync_api import Page, expect
def login(page: Page, login="ivan", password="Ivan2026pass"):
page.goto("/login")
page.get_by_label("Логин").fill(login)
page.get_by_label("Пароль").fill(password)
page.get_by_role("button", name="Войти").click()
def test_login(page: Page):
login(page)
expect(page).to_have_url(re.compile(r"/tasks$"))
expect(page.get_by_test_id("user")).to_have_text("ivan")
expect(page.get_by_test_id("task")).to_have_count(8)
def test_wrong_password_stays_on_login(page: Page):
login(page, password="wrong-pass1")
expect(page.get_by_role("alert")).to_be_visible()
expect(page.get_by_role("heading", name="Вход")).to_be_visible()
def test_create_task(page: Page, user):
login(page, user["login"], user["password"])
page.get_by_label("Название").fill("Купить бумагу")
page.get_by_role("button", name="Добавить").click()
row = page.get_by_test_id("task").filter(has_text="Купить бумагу")
expect(row).to_have_count(1)
expect(row.get_by_test_id("status")).to_have_text("Новая")
expect(page.get_by_test_id("stat-total")).to_have_text("1")
def test_take_in_work(page: Page, user):
login(page, user["login"], user["password"])
page.get_by_label("Название").fill("Позвонить в банк")
page.get_by_role("button", name="Добавить").click()
page.get_by_role("button", name="В работу").click()
expect(page.get_by_test_id("status")).to_have_text("В работе")
expect(page.get_by_test_id("stat-in-progress")).to_have_text("1")
def test_filter_in_progress(page: Page):
login(page)
page.get_by_label("Статус").select_option("in_progress")
page.get_by_role("button", name="Показать").click()
expect(page.get_by_test_id("status")).to_have_text(["В работе", "В работе"])
$ pytest -v tests/test_ui_first.py
============================= test session starts ==============================
platform linux -- Python 3.13.5, pytest-9.2.0, pluggy-1.7.0 -- ~/qa/.venv/bin/python3
cachedir: .pytest_cache
metadata: {'Python': '3.13.5', 'Platform': 'Linux-6.12.95+deb13-amd64-x86_64-with-glibc2.41', 'Packages': {'pytest': '9.2.0', 'pluggy': '1.7.0'}, 'Plugins': {'metadata': '3.1.1', 'playwright': '0.10.0', 'locust': '2.47.0', 'html': '4.2.0'}}
rootdir: ~/qa
configfile: pytest.ini
plugins: metadata-3.1.1, playwright-0.10.0, locust-2.47.0, html-4.2.0
collecting ... collected 5 items
tests/test_ui_first.py::test_login[chromium] PASSED [ 20%]
tests/test_ui_first.py::test_wrong_password_stays_on_login[chromium] PASSED [ 40%]
tests/test_ui_first.py::test_create_task[chromium] PASSED [ 60%]
tests/test_ui_first.py::test_take_in_work[chromium] PASSED [ 80%]
tests/test_ui_first.py::test_filter_in_progress[chromium] PASSED [100%]
============================== 5 passed in 3.66s ===============================
Тесты, которые создают задачи, входят под новым пользователем (фикстура user), чтобы не трогать учебные данные. Тесты, которые только смотрят, — под ivan. Вход повторяется в каждом тесте — это займёт место и время; в разделе 09 вынесем его в страницу-объект и сделаем быстрее через API.
Полезные ключи pytest-playwright:
$ pytest --help | grep -E "^ --(headed|browser|slowmo|screenshot|video|tracing)"
--browser={chromium,firefox,webkit}
--headed Run tests in headed mode.
--browser-channel=BROWSER_CHANNEL
--slowmo=SLOWMO Run tests with slow mo
--tracing={on,off,retain-on-failure}
--video={on,off,retain-on-failure}
--screenshot={on,off,only-on-failure}
| Ключ | Зачем |
|---|---|
--headed | Показать окно браузера — посмотреть, что делает тест |
--slowmo 500 | Пауза 500 мс между действиями, чтобы успеть рассмотреть |
--browser firefox | Другой браузер (сначала playwright install firefox); ключ можно повторить |
--browser-channel msedge | Установленный в системе Edge (или chrome) вместо скачанного Chromium — запасной путь, если playwright install не может скачать браузер |
--screenshot only-on-failure, --tracing retain-on-failure | Снимок и трасса упавших тестов — раздел 09 |
Учебный тест ждёт, что после входа ivan в шапке будет «maria», и ждёт не 5 секунд, а 2 — параметр timeout:
from playwright.sync_api import Page, expect
def test_user_name_wrong(page: Page):
page.goto("/login")
page.get_by_label("Логин").fill("ivan")
page.get_by_label("Пароль").fill("Ivan2026pass")
page.get_by_role("button", name="Войти").click()
expect(page.get_by_test_id("user")).to_have_text("maria", timeout=2000)
$ pytest -q --tb=short tests/test_ui_wrong.py
F [100%]
=================================== FAILURES ===================================
________________________ test_user_name_wrong[chromium] ________________________
tests/test_ui_wrong.py:9: in test_user_name_wrong
expect(page.get_by_test_id("user")).to_have_text("maria", timeout=2000)
E AssertionError: Locator expected to have text 'maria'
E Actual value: ivan
E Call log:
E - Expect "to_have_text" get_by_test_id("user") with timeout 2000ms
E - waiting for get_by_test_id("user")
E 20 × locator resolved to <span data-testid="user">ivan</span>
E - unexpected value "ivan"
E
E Aria snapshot:
E - text: ivan
=========================== short test summary info ============================
FAILED tests/test_ui_wrong.py::test_user_name_wrong[chromium] - AssertionErro...
1 failed in 3.58s
Отчёт Playwright говорит больше, чем assert: что ожидалось, что было на самом деле, какой элемент найден по локатору (<span data-testid="user">ivan</span>) и сколько раз проверка повторялась, пока не вышло время. Если элемент не найден вовсе, в журнале будет «waiting for …» без «locator resolved» — значит, неверен сам локатор.
Команда playwright codegen http://127.0.0.1:8000/login открывает браузер и окно Playwright Inspector. Всё, что вы делаете в браузере, превращается в код: щелчки, ввод, выбор в списке. Кнопка «Assert» в панели добавляет проверки: видимость, текст, значение. Язык кода выбирается в списке Target — нужен «Pytest».
Записанный код — черновик, а не тест:
Хороший способ работы: записать сценарий, чтобы узнать локаторы, а тест написать самому по тест-кейсу. Для подбора одного локатора есть кнопка «Pick locator»: наводите на элемент — Inspector показывает, как Playwright предлагает его найти.
data-testid; CSS и XPath — последнее средствоexpect сами ждут нужного состояния — sleep в UI-тестах не нуженpage на каждый тест; адрес — из base_url