Раздел 08 — Автотесты UI: Playwright

Программа, которая сама нажимает кнопки в браузере и ждёт, пока страница ответит

Прогресс курса Раздел 8 из 12

Что вы освоите в этом разделе

8 академических часов: 2 теории, 6 практики. Playwright и браузер Chromium уже стоят, если вы выполнили установку из раздела 01. Какие элементы есть на страницах «Задачника» и как их искать в DevTools, разобрано в разделе 08 курса ручного тестирования.
01

Playwright без тестов

Playwright запускает браузер и управляет им: открывает адреса, заполняет поля, нажимает кнопки, читает текст. По умолчанию браузер работает без окна (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):

tasks.png
Список задач пользователя ivan в Задачнике 1.0: счётчики, форма новой задачи, фильтры, таблица задач

with sync_playwright() as p запускает Playwright и останавливает его в конце блока. Дальше цепочка: браузер → страница (вкладка) → действия. В тестах эту цепочку берёт на себя плагин pytest-playwright — ниже.

02

Локаторы

Локатор — описание, как найти элемент. От выбора локатора больше всего зависит, переживёт ли тест следующую правку вёрстки. 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")  # статус в этой строке, а не первый на странице
03

Действия и ожидания

ДействиеЧто делает
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()Кнопка доступна, флажок отмечен
04

UI-тесты на pytest

Плагин 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
05

Когда ожидание не сбылось

Учебный тест ждёт, что после входа 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» — значит, неверен сам локатор.

06

Запись сценария: codegen

Команда playwright codegen http://127.0.0.1:8000/login открывает браузер и окно Playwright Inspector. Всё, что вы делаете в браузере, превращается в код: щелчки, ввод, выбор в списке. Кнопка «Assert» в панели добавляет проверки: видимость, текст, значение. Язык кода выбирается в списке Target — нужен «Pytest».

Записанный код — черновик, а не тест:

Хороший способ работы: записать сценарий, чтобы узнать локаторы, а тест написать самому по тест-кейсу. Для подбора одного локатора есть кнопка «Pick locator»: наводите на элемент — Inspector показывает, как Playwright предлагает его найти.

Codegen нужен экран: на сервере без графики он не запустится. Запускайте его у себя на компьютере, а тесты — где угодно.

Ключевые выводы раздела

Запомните главное

  • Локаторы — по роли, подписи, тексту, data-testid; CSS и XPath — последнее средство
  • Действия и expect сами ждут нужного состояния — sleep в UI-тестах не нужен
  • pytest-playwright даёт чистую вкладку page на каждый тест; адрес — из base_url
  • Отчёт об упавшем ожидании показывает, нашёлся ли элемент и что в нём было
  • Codegen — черновик: локаторы проверить, проверки и данные дописать самим
Практические задания — на учебном портале. Задания, критерии оценивания, сдача работ и оценки преподавателя — в курсе на portal.nevabit.ru. Учётную запись выдаёт преподаватель.
Раздел 07: Автотесты API — данные и негативные случаи Все разделы Раздел 09: UI — страница-объект и стабильность