Compare commits
8 Commits
365796452f
...
d156fa0664
| Author | SHA1 | Date | |
|---|---|---|---|
| d156fa0664 | |||
| 932d0594a9 | |||
| 3df40240e7 | |||
| 255f0d46b9 | |||
| 02176b5ccd | |||
| e718bf48d4 | |||
| c47aeff07d | |||
| 642ee27588 |
19
.env.example
Normal file
19
.env.example
Normal file
@ -0,0 +1,19 @@
|
||||
# ============================================================
|
||||
# ШАБЛОН .env — скопируй в .env и заполни реальными данными
|
||||
# Команда: cp .env.example .env
|
||||
# ============================================================
|
||||
|
||||
# Роль: Управление ветеринарии
|
||||
VETDEPT_LOGIN=
|
||||
VETDEPT_PASSWORD=
|
||||
|
||||
# Роль: Ветеринарная станция
|
||||
VETSTATION_LOGIN=
|
||||
VETSTATION_PASSWORD=
|
||||
|
||||
# Роль: Администрация (префектура)
|
||||
PREFECTURE_LOGIN=
|
||||
PREFECTURE_PASSWORD=
|
||||
|
||||
# Базовый URL портала
|
||||
BASE_URL=https://vet.goznak.ru
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@ -174,3 +174,5 @@ cython_debug/
|
||||
# PyPI configuration file
|
||||
.pypirc
|
||||
|
||||
# Test reports
|
||||
html-report/
|
||||
|
||||
417
README.md
417
README.md
@ -1,2 +1,417 @@
|
||||
# e2e_Pets
|
||||
# e2e_Pets — End-to-End тесты портала учёта животных
|
||||
|
||||
Автоматизированное тестирование портала [vet.goznak.ru](https://vet.goznak.ru)
|
||||
на базе **Playwright + pytest**.
|
||||
|
||||
---
|
||||
|
||||
## Содержание
|
||||
|
||||
- [Структура проекта](#структура-проекта)
|
||||
- [Быстрый старт](#быстрый-старт)
|
||||
- [Запуск тестов](#запуск-тестов)
|
||||
- [Создание нового теста](#создание-нового-теста)
|
||||
- [Добавление генератора данных (ФЛК)](#добавление-генератора-данных-флк)
|
||||
- [Добавление статического артефакта](#добавление-статического-артефакта)
|
||||
|
||||
---
|
||||
|
||||
## Структура проекта
|
||||
|
||||
```
|
||||
e2e_Pets/
|
||||
├── .env # Учётные данные (не в git)
|
||||
├── .env.example # Шаблон .env
|
||||
├── pytest.ini # Конфигурация pytest
|
||||
├── requirements.txt # Зависимости
|
||||
│
|
||||
└── tests/
|
||||
├── conftest.py # Глобальные фикстуры (browser_page, bp_state)
|
||||
├── testdata/
|
||||
│ └── generators.py # Генераторы случайных данных по ФЛК
|
||||
│
|
||||
├── 01_vetdept/ # ЛК: Управление ветеринарии
|
||||
│ ├── conftest.py # fixtures_dir, shared_state
|
||||
│ ├── fixtures/ # Статические артефакты (фото, PDF)
|
||||
│ └── BP01_navigation/ # Бизнес-процесс 01
|
||||
│ └── test_bp01_navigation.py
|
||||
│
|
||||
└── 02_vetstation/ # ЛК: Ветеринарная станция
|
||||
├── conftest.py
|
||||
├── fixtures/
|
||||
└── BP01_dashboard/ # Бизнес-процесс 01
|
||||
└── test_bp01_dashboard.py
|
||||
```
|
||||
|
||||
**Иерархия тестов:** Проект → ЛК (роль) → БП (`BP01_...`) → Шаги (`test_s01_...`, `test_s02_...`)
|
||||
|
||||
---
|
||||
|
||||
## Быстрый старт
|
||||
|
||||
### 1. Клонировать репозиторий
|
||||
|
||||
```powershell
|
||||
git clone <url>
|
||||
cd e2e_Pets
|
||||
```
|
||||
|
||||
### 2. Создать виртуальное окружение и установить зависимости
|
||||
|
||||
```powershell
|
||||
python -m venv venv
|
||||
.\venv\Scripts\pip.exe install -r requirements.txt
|
||||
.\venv\Scripts\playwright.exe install chromium
|
||||
```
|
||||
|
||||
### 3. Настроить учётные данные
|
||||
|
||||
```powershell
|
||||
Copy-Item .env.example .env
|
||||
# Открыть .env и заполнить логины/пароли
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Запуск тестов
|
||||
|
||||
> Все команды выполняются из корня проекта (`e2e_Pets/`).
|
||||
> HTML-отчёт автоматически создаётся в `html-report/report.html`.
|
||||
|
||||
---
|
||||
|
||||
### Запустить ВСЕ тесты
|
||||
|
||||
```powershell
|
||||
.\venv\Scripts\pytest.exe
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Запустить тесты конкретного ЛК (роли)
|
||||
|
||||
```powershell
|
||||
# ЛК Управление ветеринарии
|
||||
.\venv\Scripts\pytest.exe tests/01_vetdept/
|
||||
|
||||
# ЛК Ветеринарная станция
|
||||
.\venv\Scripts\pytest.exe tests/02_vetstation/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Запустить конкретный бизнес-процесс
|
||||
|
||||
```powershell
|
||||
# BP01 навигации в ЛК Управление ветеринарии
|
||||
.\venv\Scripts\pytest.exe tests/01_vetdept/BP01_navigation/
|
||||
|
||||
# BP01 дашборда в ЛК Ветеринарная станция
|
||||
.\venv\Scripts\pytest.exe tests/02_vetstation/BP01_dashboard/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Запустить конкретный шаг
|
||||
|
||||
```powershell
|
||||
# Только шаг 3 из BP01 ветстанции
|
||||
.\venv\Scripts\pytest.exe tests/02_vetstation/BP01_dashboard/ -k "s03"
|
||||
|
||||
# Только шаги 1 и 2
|
||||
.\venv\Scripts\pytest.exe tests/02_vetstation/BP01_dashboard/ -k "s01 or s02"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Полезные флаги
|
||||
|
||||
| Флаг | Описание |
|
||||
|---|---|
|
||||
| `-v` | Подробный вывод с именами шагов |
|
||||
| `--headed` | Показывать браузер (по умолчанию включён в `pytest.ini`) |
|
||||
| `--headless` | Запустить без браузера (быстрее) |
|
||||
| `--slowmo 500` | Замедление действий на 500мс (для отладки) |
|
||||
| `-k "BP01"` | Запустить только тесты содержащие "BP01" в имени |
|
||||
| `--co -q` | Только показать список тестов без запуска |
|
||||
|
||||
```powershell
|
||||
# Показать список всех тестов (без запуска)
|
||||
.\venv\Scripts\pytest.exe --co -q
|
||||
|
||||
# Запустить без браузера и без замедления
|
||||
.\venv\Scripts\pytest.exe --headless --slowmo 0
|
||||
|
||||
# Запустить в headless-режиме только vetdept
|
||||
.\venv\Scripts\pytest.exe tests/01_vetdept/ --headless --slowmo 0
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Создание нового теста
|
||||
|
||||
### Нужен новый бизнес-процесс?
|
||||
|
||||
Опиши его в свободной форме — например:
|
||||
|
||||
> *«Нужен тест для ветстанции: открыть список животных, применить фильтр
|
||||
> по кошкам, убедиться что список отфильтровался, открыть карточку
|
||||
> первого животного, проверить наличие кнопки редактирования»*
|
||||
|
||||
ИИ-ассистент создаст файл в нужном месте, следуя архитектуре проекта.
|
||||
|
||||
---
|
||||
|
||||
### Если хочешь создать вручную — шаблон:
|
||||
|
||||
#### 1. Создать папку бизнес-процесса
|
||||
|
||||
```powershell
|
||||
# Для роли vetstation, новый бизнес-процесс BP02
|
||||
mkdir tests\02_vetstation\BP02_animals
|
||||
New-Item tests\02_vetstation\BP02_animals\__init__.py
|
||||
```
|
||||
|
||||
#### 2. Создать файл теста
|
||||
|
||||
Имя файла: `test_bp02_<название>.py`
|
||||
|
||||
```python
|
||||
import os
|
||||
import pytest
|
||||
from playwright.sync_api import Page, expect
|
||||
|
||||
BASE_URL = os.getenv("BASE_URL", "https://vet.goznak.ru")
|
||||
|
||||
|
||||
@pytest.mark.vetstation # <- маркер роли
|
||||
@pytest.mark.usefixtures("browser_page") # <- авторизация и браузер
|
||||
class TestBP02Animals:
|
||||
"""
|
||||
BP02: Краткое описание бизнес-процесса
|
||||
|
||||
Проверяет что пользователь может:
|
||||
- Шаг 1: ...
|
||||
- Шаг 2: ...
|
||||
"""
|
||||
|
||||
@pytest.mark.dependency(name="BP02::s01")
|
||||
def test_s01_<название>(self, browser_page: Page, bp_state: dict):
|
||||
"""Шаг 1: Описание шага — именно это появится в отчёте"""
|
||||
# ... код теста ...
|
||||
bp_state["key"] = "value" # передать данные следующему шагу
|
||||
|
||||
@pytest.mark.dependency(name="BP02::s02", depends=["BP02::s01"])
|
||||
def test_s02_<название>(self, browser_page: Page, bp_state: dict):
|
||||
"""Шаг 2: Описание шага"""
|
||||
value = bp_state["key"] # получить данные от предыдущего шага
|
||||
# ... код теста ...
|
||||
```
|
||||
|
||||
#### 3. Правила именования
|
||||
|
||||
| Элемент | Формат | Пример |
|
||||
|---|---|---|
|
||||
| Папка БП | `BP<NN>_<описание>` | `BP02_animals` |
|
||||
| Файл теста | `test_bp<NN>_<описание>.py` | `test_bp02_animals.py` |
|
||||
| Класс | `TestBP<NN><Описание>` | `TestBP02Animals` |
|
||||
| Метод (шаг) | `test_s<NN>_<описание>` | `test_s01_open_list` |
|
||||
| Зависимость | `BP<NN>::s<NN>` | `BP02::s01` |
|
||||
|
||||
#### 4. Маркеры ролей
|
||||
|
||||
| Маркер | Роль |
|
||||
|---|---|
|
||||
| `@pytest.mark.vetdept` | Управление ветеринарии |
|
||||
| `@pytest.mark.vetstation` | Ветеринарная станция |
|
||||
| `@pytest.mark.prefecture` | Администрация (префектура) |
|
||||
|
||||
---
|
||||
|
||||
## Добавление генератора данных (ФЛК)
|
||||
|
||||
Файл: [`tests/testdata/generators.py`](tests/testdata/generators.py)
|
||||
|
||||
Содержит пошаговое руководство по добавлению нового генератора прямо в начале файла. Коротко:
|
||||
|
||||
```python
|
||||
# Шаблон новой функции-генератора:
|
||||
|
||||
def gen_<сущность>_<поле>() -> str:
|
||||
"""
|
||||
Краткое описание.
|
||||
|
||||
ФЛК:
|
||||
- Правило 1
|
||||
- Правило 2
|
||||
|
||||
Пример вывода: 'AB1234'
|
||||
"""
|
||||
# ... реализация ...
|
||||
```
|
||||
|
||||
Использование в тесте:
|
||||
|
||||
```python
|
||||
from tests.testdata.generators import gen_microchip, gen_animal_name
|
||||
|
||||
def test_s01_fill_form(self, browser_page, bp_state):
|
||||
name = gen_animal_name()
|
||||
chip = gen_microchip()
|
||||
browser_page.fill("#name", name)
|
||||
browser_page.fill("#chip", chip)
|
||||
bp_state["name"] = name # передаём следующему шагу
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Добавление статического артефакта
|
||||
|
||||
1. Положи файл в папку `fixtures/` нужной роли:
|
||||
```
|
||||
tests/01_vetdept/fixtures/images/cat_photo.jpg
|
||||
```
|
||||
|
||||
2. Раскомментируй шаблон фикстуры в `tests/<роль>/conftest.py`:
|
||||
```python
|
||||
@pytest.fixture(scope="session")
|
||||
def cat_photo(fixtures_dir) -> Path:
|
||||
"""Фото кошки для теста загрузки."""
|
||||
return fixtures_dir / "images" / "cat_photo.jpg"
|
||||
```
|
||||
|
||||
3. Используй в тесте:
|
||||
```python
|
||||
def test_s02_upload(self, browser_page, bp_state, cat_photo):
|
||||
browser_page.set_input_files("input[type=file]", str(cat_photo))
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Создание теста через визуальный рекордер (Codegen)
|
||||
|
||||
Playwright Codegen — встроенный инструмент записи действий в браузере.
|
||||
Ты кликаешь, вводишь текст и переходишь по страницам — Codegen генерирует
|
||||
Python-код, который воспроизводит твои действия.
|
||||
|
||||
### Запуск рекордера
|
||||
|
||||
```powershell
|
||||
# Открыть браузер с рекордером (без авторизации)
|
||||
.\venv\Scripts\playwright.exe codegen https://vet.goznak.ru
|
||||
|
||||
# Открыть браузер с рекордером и сохранить результат в файл
|
||||
.\venv\Scripts\playwright.exe codegen https://vet.goznak.ru --output tests\codegen_draft.py
|
||||
```
|
||||
|
||||
Откроются два окна:
|
||||
- **Браузер** — управляй им как обычно: кликай, заполняй формы, переходи по разделам
|
||||
- **Playwright Inspector** — в реальном времени показывает сгенерированный код
|
||||
|
||||
### Порядок работы
|
||||
|
||||
1. **Запусти рекордер** командой выше
|
||||
2. **Авторизуйся** под нужной ролью (введи логин/пароль из `.env`)
|
||||
3. **Выполни нужный бизнес-процесс** — все действия записываются автоматически
|
||||
4. **Скопируй сгенерированный код** из Playwright Inspector
|
||||
5. **Адаптируй код** под архитектуру проекта (см. ниже)
|
||||
|
||||
---
|
||||
|
||||
### Как адаптировать код из Codegen в архитектуру проекта
|
||||
|
||||
Codegen генерирует **плоский скрипт**, проект использует **класс с шагами**.
|
||||
Ниже показано как преобразовать одно в другое.
|
||||
|
||||
#### До (вывод Codegen):
|
||||
|
||||
```python
|
||||
from playwright.sync_api import Playwright, sync_playwright, expect
|
||||
|
||||
def run(playwright: Playwright) -> None:
|
||||
browser = playwright.chromium.launch(headless=False)
|
||||
context = browser.new_context()
|
||||
page = context.new_page()
|
||||
page.goto("https://vet.goznak.ru/auth")
|
||||
page.get_by_role("textbox", name="Email").fill("gz_vetstation@vet.goznak.ru")
|
||||
page.get_by_role("textbox", name="Введите пароль").fill("A6wrpzpD5T")
|
||||
page.get_by_role("button", name="Войти в аккаунт").click()
|
||||
page.get_by_role("link", name="Животные").click()
|
||||
expect(page).to_have_url("https://vet.goznak.ru/animals")
|
||||
page.get_by_role("button", name="Добавить животное").click()
|
||||
page.get_by_label("Кличка").fill("Мурка")
|
||||
page.get_by_role("button", name="Сохранить").click()
|
||||
expect(page.get_by_text("Мурка")).to_be_visible()
|
||||
context.close()
|
||||
browser.close()
|
||||
|
||||
with sync_playwright() as playwright:
|
||||
run(playwright)
|
||||
```
|
||||
|
||||
#### После (архитектура проекта):
|
||||
|
||||
```python
|
||||
import os
|
||||
import pytest
|
||||
from playwright.sync_api import Page, expect
|
||||
|
||||
BASE_URL = os.getenv("BASE_URL", "https://vet.goznak.ru")
|
||||
|
||||
|
||||
@pytest.mark.vetstation
|
||||
@pytest.mark.usefixtures("browser_page")
|
||||
class TestBP02AddAnimal:
|
||||
"""
|
||||
BP02: Добавление нового животного
|
||||
|
||||
Проверяет что пользователь может:
|
||||
- Перейти в раздел Животные
|
||||
- Открыть форму добавления
|
||||
- Заполнить кличку и сохранить
|
||||
- Убедиться что животное появилось в списке
|
||||
"""
|
||||
|
||||
# Авторизация и логин/пароль — убраны полностью:
|
||||
# этим занимается фикстура browser_page (см. tests/conftest.py)
|
||||
|
||||
@pytest.mark.dependency(name="BP02::s01")
|
||||
def test_s01_go_to_animals(self, browser_page: Page, bp_state: dict):
|
||||
"""Шаг 1: Перейти в раздел Животные"""
|
||||
browser_page.get_by_role("link", name="Животные").click()
|
||||
expect(browser_page).to_have_url(BASE_URL + "/animals", timeout=10000)
|
||||
|
||||
@pytest.mark.dependency(name="BP02::s02", depends=["BP02::s01"])
|
||||
def test_s02_open_add_form(self, browser_page: Page, bp_state: dict):
|
||||
"""Шаг 2: Открыть форму добавления животного"""
|
||||
browser_page.get_by_role("button", name="Добавить животное").click()
|
||||
expect(browser_page.get_by_label("Кличка")).to_be_visible(timeout=10000)
|
||||
|
||||
@pytest.mark.dependency(name="BP02::s03", depends=["BP02::s02"])
|
||||
def test_s03_fill_and_save(self, browser_page: Page, bp_state: dict):
|
||||
"""Шаг 3: Заполнить кличку и сохранить"""
|
||||
from tests.testdata.generators import gen_animal_name
|
||||
name = gen_animal_name() # случайное имя по ФЛК
|
||||
browser_page.get_by_label("Кличка").fill(name)
|
||||
browser_page.get_by_role("button", name="Сохранить").click()
|
||||
bp_state["animal_name"] = name # передаём шагу 4
|
||||
|
||||
@pytest.mark.dependency(name="BP02::s04", depends=["BP02::s03"])
|
||||
def test_s04_verify_in_list(self, browser_page: Page, bp_state: dict):
|
||||
"""Шаг 4: Проверить что животное появилось в списке"""
|
||||
expect(
|
||||
browser_page.get_by_text(bp_state["animal_name"])
|
||||
).to_be_visible(timeout=10000)
|
||||
```
|
||||
|
||||
### Что изменилось при адаптации
|
||||
|
||||
| Было в Codegen | Стало в проекте |
|
||||
|---|---|
|
||||
| `browser.launch()` + `context.new_page()` | Фикстура `browser_page` (авто) |
|
||||
| Явный логин/пароль в коде | Берётся из `.env` через фикстуру |
|
||||
| Один плоский `def run()` | Класс с методами-шагами |
|
||||
| Нет зависимостей между шагами | `@pytest.mark.dependency(depends=[...])` |
|
||||
| Хардкод данных (`"Мурка"`) | Генераторы ФЛК (`gen_animal_name()`) |
|
||||
| Нет передачи данных между шагами | `bp_state["key"] = value` |
|
||||
| Нет отображения в отчёте | Docstring шага → колонка Test в отчёте |
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
[pytest]
|
||||
addopts = --browser chromium --headed --slowmo 1000
|
||||
html_report_dir = html-report
|
||||
addopts = --browser chromium --headed --slowmo 1000 --html=html-report/report.html --self-contained-html
|
||||
testpaths = tests
|
||||
markers =
|
||||
vetdept: тесты для роли Управление ветеринарии
|
||||
vetstation: тесты для роли Ветеринарная станци
|
||||
vetstation: тесты для роли Ветеринарная станция
|
||||
prefecture: тесты для роли Администрация
|
||||
contractor_admin: тесты для роли Администратор организации отлова
|
||||
smoke: критически важные тесты для быстрого прогона
|
||||
smoke: критически важные тесты для быстрого прогона
|
||||
dependency: зависимость шага от предыдущего (pytest-dependency)
|
||||
@ -2,4 +2,7 @@ playwright
|
||||
pytest
|
||||
pytest-playwright
|
||||
pytest-xdist
|
||||
pytest-html==3.2.0
|
||||
pytest-html
|
||||
pytest-dependency
|
||||
faker
|
||||
python-dotenv
|
||||
@ -1,53 +0,0 @@
|
||||
import pytest
|
||||
from playwright.sync_api import Page, expect
|
||||
|
||||
|
||||
@pytest.mark.vetdept
|
||||
def test_dashboard(role_page: Page):
|
||||
print("\n[TEST] ТЕСТ: Проверка навигации по разделам портала")
|
||||
print("=" * 60)
|
||||
|
||||
# Шаг 1
|
||||
print("\n[1/6] Переход на главную страницу (Dashboard)...")
|
||||
role_page.goto("https://vet.goznak.ru/dashboard")
|
||||
print(" [OK] Страница загружена")
|
||||
|
||||
# Шаг 2
|
||||
print("[2/6] Проверка URL главной страницы...")
|
||||
expect(role_page).to_have_url("https://vet.goznak.ru/dashboard")
|
||||
print(" [OK] URL соответствует ожидаемому")
|
||||
|
||||
# Шаг 3
|
||||
print("[3/6] Проверка заголовка организации...")
|
||||
expect(role_page.locator("#app")).to_contain_text("Управление Ветеринарии Гознак")
|
||||
print(" [OK] Заголовок 'Управление Ветеринарии Гознак' найден")
|
||||
|
||||
# Шаг 4
|
||||
print("[4/6] Переход в раздел 'Организации'...")
|
||||
role_page.get_by_role("link", name="Организации").click()
|
||||
print(" [OK] Клик выполнен, ожидание перехода...")
|
||||
|
||||
print(" Проверка URL раздела 'Организации'...")
|
||||
expect(role_page).to_have_url("https://vet.goznak.ru/organizations", timeout=10000)
|
||||
print(" [OK] URL раздела верный")
|
||||
|
||||
print(" Проверка заголовка страницы...")
|
||||
expect(role_page.get_by_text("Подведомственные организации")).to_be_visible(timeout=10000)
|
||||
print(" [OK] Заголовок 'Подведомственные организации' найден")
|
||||
|
||||
# Шаг 5
|
||||
print("[5/6] Переход в раздел 'Заявки на отлов'...")
|
||||
role_page.get_by_role("link", name="Заявки на отлов").click()
|
||||
print(" [OK] Клик выполнен, ожидание перехода...")
|
||||
|
||||
# Шаг 6
|
||||
print("[6/6] Проверка страницы 'Заявки на отлов'...")
|
||||
expect(role_page).to_have_url("https://vet.goznak.ru/claims-list", timeout=10000)
|
||||
print(" [OK] URL раздела верный")
|
||||
|
||||
expect(role_page.get_by_text("Мои заявки")).to_be_visible(timeout=10000)
|
||||
print(" [OK] Заголовок 'Мои заявки' найден")
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("🎉 ВСЕ ПРОВЕРКИ ПРОЙДЕНУ УСПЕШНО!")
|
||||
print("=" * 60)
|
||||
1
tests/01_vetdept/BP01_navigation/__init__.py
Normal file
1
tests/01_vetdept/BP01_navigation/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
# BP01: Навигация по разделам портала
|
||||
56
tests/01_vetdept/BP01_navigation/test_bp01_navigation.py
Normal file
56
tests/01_vetdept/BP01_navigation/test_bp01_navigation.py
Normal file
@ -0,0 +1,56 @@
|
||||
import os
|
||||
import pytest
|
||||
# pyrefly: ignore [missing-import]
|
||||
from playwright.sync_api import Page, expect
|
||||
|
||||
BASE_URL = os.getenv("BASE_URL", "https://vet.goznak.ru")
|
||||
|
||||
|
||||
@pytest.mark.vetdept
|
||||
@pytest.mark.usefixtures("browser_page")
|
||||
class TestBP01Navigation:
|
||||
"""
|
||||
BP01: Навигация по разделам портала
|
||||
|
||||
Проверяет что пользователь с ролью 'Управление Ветеринарии' может:
|
||||
- Открыть главную страницу (Dashboard)
|
||||
- Видеть заголовок своей организации
|
||||
- Перейти в раздел Организации
|
||||
- Перейти в раздел Заявки на отлов
|
||||
"""
|
||||
|
||||
@pytest.mark.dependency(name="BP01::s01")
|
||||
def test_s01_open_dashboard(self, browser_page: Page, bp_state: dict):
|
||||
"""Шаг 1: Открыть главную страницу (Dashboard)"""
|
||||
browser_page.goto(BASE_URL + "/dashboard")
|
||||
expect(browser_page).to_have_url(BASE_URL + "/dashboard")
|
||||
bp_state["start_url"] = browser_page.url
|
||||
|
||||
@pytest.mark.dependency(name="BP01::s02", depends=["BP01::s01"])
|
||||
def test_s02_check_org_header(self, browser_page: Page, bp_state: dict):
|
||||
"""Шаг 2: Проверить заголовок организации в интерфейсе"""
|
||||
expect(browser_page.locator("#app")).to_contain_text(
|
||||
"Управление Ветеринарии Гознак"
|
||||
)
|
||||
|
||||
@pytest.mark.dependency(name="BP01::s03", depends=["BP01::s02"])
|
||||
def test_s03_go_to_organizations(self, browser_page: Page, bp_state: dict):
|
||||
"""Шаг 3: Перейти в раздел Организации"""
|
||||
browser_page.get_by_role("link", name="Организации").click()
|
||||
expect(browser_page).to_have_url(
|
||||
BASE_URL + "/organizations", timeout=10000
|
||||
)
|
||||
expect(
|
||||
browser_page.get_by_text("Подведомственные организации")
|
||||
).to_be_visible(timeout=10000)
|
||||
|
||||
@pytest.mark.dependency(name="BP01::s04", depends=["BP01::s03"])
|
||||
def test_s04_go_to_claims(self, browser_page: Page, bp_state: dict):
|
||||
"""Шаг 4: Перейти в раздел Заявки на отлов"""
|
||||
browser_page.get_by_role("link", name="Заявки на отлов").click()
|
||||
expect(browser_page).to_have_url(
|
||||
BASE_URL + "/claims-list", timeout=10000
|
||||
)
|
||||
expect(browser_page.get_by_text("Мои заявки")).to_be_visible(
|
||||
timeout=10000
|
||||
)
|
||||
63
tests/01_vetdept/conftest.py
Normal file
63
tests/01_vetdept/conftest.py
Normal file
@ -0,0 +1,63 @@
|
||||
# pyrefly: ignore [missing-import]
|
||||
import pytest
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Пути к статическим артефактам роли (фото, документы, файлы)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def fixtures_dir() -> Path:
|
||||
"""
|
||||
Путь к папке со статическими артефактами роли vetdept.
|
||||
Структура папки:
|
||||
fixtures/
|
||||
├── images/ ← фотографии животных, документы-сканы
|
||||
└── docs/ ← PDF, сертификаты и прочие документы
|
||||
|
||||
Используй как базовый путь для других фикстур-артефактов:
|
||||
def test_s02_upload(self, browser_page, fixtures_dir):
|
||||
photo = fixtures_dir / "images" / "cat_photo.jpg"
|
||||
browser_page.set_input_files("input[type=file]", str(photo))
|
||||
"""
|
||||
return Path(__file__).parent / "fixtures"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Готовые фикстуры для конкретных артефактов
|
||||
# Добавляй новые по мере появления новых файлов в папке fixtures/
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# Пример: раскомментируй и адаптируй когда нужен конкретный файл
|
||||
#
|
||||
# @pytest.fixture(scope="session")
|
||||
# def cat_photo(fixtures_dir) -> Path:
|
||||
# """Фотография кошки для теста загрузки фото животного."""
|
||||
# return fixtures_dir / "images" / "cat_photo.jpg"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Контейнер для передачи данных между разными БП одной роли (scope=session)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def shared_state() -> dict:
|
||||
"""
|
||||
Словарь для передачи данных между разными бизнес-процессами
|
||||
в рамках одной тестовой сессии.
|
||||
|
||||
Используй когда результат одного БП нужен в другом.
|
||||
Например: BP01 создал заявку → BP02 работает с ней.
|
||||
|
||||
Пример:
|
||||
# В BP01:
|
||||
def test_s03_create(self, browser_page, bp_state, shared_state):
|
||||
bp_state["claim_id"] = "12345"
|
||||
shared_state["claim_id"] = "12345" # передаём в BP02
|
||||
|
||||
# В BP02:
|
||||
def test_s01_open(self, browser_page, shared_state):
|
||||
claim_id = shared_state["claim_id"]
|
||||
"""
|
||||
return {}
|
||||
1
tests/01_vetdept/fixtures/docs/.gitkeep
Normal file
1
tests/01_vetdept/fixtures/docs/.gitkeep
Normal file
@ -0,0 +1 @@
|
||||
# Эта папка хранит PDF, сертификаты и прочие документы для тестов загрузки файлов.
|
||||
2
tests/01_vetdept/fixtures/images/.gitkeep
Normal file
2
tests/01_vetdept/fixtures/images/.gitkeep
Normal file
@ -0,0 +1,2 @@
|
||||
# Эта папка хранит фотографии животных и другие изображения для тестов.
|
||||
# Добавляй реальные файлы сюда и создавай фикстуры в ../conftest.py
|
||||
@ -1,13 +0,0 @@
|
||||
import re
|
||||
from playwright.sync_api import Page, expect
|
||||
|
||||
|
||||
def test_example(page: Page) -> None:
|
||||
page.goto("https://vet.goznak.ru/auth")
|
||||
page.get_by_role("textbox", name="Email").click()
|
||||
page.get_by_role("textbox", name="Email").click()
|
||||
page.get_by_role("textbox", name="Email").fill("gz_vetstation@vet.goznak.ru")
|
||||
page.get_by_role("textbox", name="Введите пароль").click()
|
||||
page.get_by_role("textbox", name="Введите пароль").fill("A6wrpzpD5T")
|
||||
page.get_by_role("button", name="Войти в аккаунт").click()
|
||||
expect(page.locator("#app")).to_match_aria_snapshot("- text: Ветеринарная станция \"ГОЗНАК\" - Демо\n- combobox: /\\d+\\.\\d+\\.\\d+ - \\d+\\.\\d+\\.\\d+/\n- button \"Создать отчет\":\n - img\n - text: \"\"\n- text: Статистика регистрации домашних животных\n- img\n- text: 0 всего особей Всего особей\n- paragraph: \"0\"\n- list:\n - listitem:\n - img\n - text: \"0\"\n - img\n - text: \"0\"\n- text: Из них с биометрией\n- paragraph: \"0\"\n- list:\n - listitem:\n - img\n - text: \"0\"\n - img\n - text: \"0\"\n- text: Динамика регистраций в ветстанции\n- img\n- text: Последние зарегистрированные особи\n- button \"Перейти к списку\"\n- table:\n - rowgroup:\n - row \"ID особи Дата № регистрации Тип маркировки № маркировки Кличка Тип владелеца\":\n - columnheader \"ID особи\"\n - columnheader \"Дата\"\n - columnheader \"№ регистрации\"\n - columnheader \"Тип маркировки\"\n - columnheader \"№ маркировки\"\n - columnheader \"Кличка\"\n - columnheader \"Тип владелеца\"\n - columnheader\n - rowgroup:\n - row /homeless \\d+ \\d+\\.\\d+\\.\\d+ \\d+ С \\d+ \\d+ \\d+ Нет информации о маркировке Без номера Кличко Юридическое лицо/:\n - cell /homeless \\d+/:\n - img \"homeless\"\n - img\n - img\n - text: \"\"\n - cell /\\d+\\.\\d+\\.\\d+/\n - cell /\\d+ С \\d+ \\d+ \\d+/\n - cell \"Нет информации о маркировке\"\n - cell \"Без номера\"\n - cell \"Кличко\"\n - cell \"Юридическое лицо\"\n - cell:\n - button:\n - img\n - row /homeless \\d+ \\d+\\.\\d+\\.\\d+ \\d+ К \\d+ \\d+ \\d+ Нет информации о маркировке Без номера Звезда Юридическое лицо/:\n - cell /homeless \\d+/:\n - img \"homeless\"\n - img\n - img\n - text: \"\"\n - cell /\\d+\\.\\d+\\.\\d+/\n - cell /\\d+ К \\d+ \\d+ \\d+/\n - cell \"Нет информации о маркировке\"\n - cell \"Без номера\"\n - cell \"Звезда\"\n - cell \"Юридическое лицо\"\n - cell:\n - button:\n - img\n - row /homeless \\d+ \\d+\\.\\d+\\.\\d+ \\d+ К \\d+ \\d+ \\d+ Клеймо К-\\d+ Симба Физическое лицо/:\n - cell /homeless \\d+/:\n - img \"homeless\"\n - img\n - img\n - text: \"\"\n - cell /\\d+\\.\\d+\\.\\d+/\n - cell /\\d+ К \\d+ \\d+ \\d+/\n - cell \"Клеймо\"\n - cell /К-\\d+/\n - cell \"Симба\"\n - cell \"Физическое лицо\"\n - cell:\n - button:\n - img\n - row /homeless \\d+ \\d+\\.\\d+\\.\\d+ \\d+ С \\d+ \\d+ \\d+ Микрочип Ч-\\d+ Жорик Юридическое лицо/:\n - cell /homeless \\d+/:\n - img \"homeless\"\n - img\n - img\n - text: \"\"\n - cell /\\d+\\.\\d+\\.\\d+/\n - cell /\\d+ С \\d+ \\d+ \\d+/\n - cell \"Микрочип\"\n - cell /Ч-\\d+/\n - cell \"Жорик\"\n - cell \"Юридическое лицо\"\n - cell:\n - button:\n - img\n - row /homeless \\d+ \\d+\\.\\d+\\.\\d+ \\d+ С \\d+ \\d+ \\d+ Нет информации о маркировке Без номера Дружок Физическое лицо/:\n - cell /homeless \\d+/:\n - img \"homeless\"\n - img\n - img\n - text: \"\"\n - cell /\\d+\\.\\d+\\.\\d+/\n - cell /\\d+ С \\d+ \\d+ \\d+/\n - cell \"Нет информации о маркировке\"\n - cell \"Без номера\"\n - cell \"Дружок\"\n - cell \"Физическое лицо\"\n - cell:\n - button:\n - img")
|
||||
1
tests/02_vetstation/BP01_dashboard/__init__.py
Normal file
1
tests/02_vetstation/BP01_dashboard/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
# BP01: Навигация по разделам ЛК ветеринарной станции
|
||||
85
tests/02_vetstation/BP01_dashboard/test_bp01_dashboard.py
Normal file
85
tests/02_vetstation/BP01_dashboard/test_bp01_dashboard.py
Normal file
@ -0,0 +1,85 @@
|
||||
import os
|
||||
# pyrefly: ignore [missing-import]
|
||||
import pytest
|
||||
# pyrefly: ignore [missing-import]
|
||||
from playwright.sync_api import Page, expect
|
||||
|
||||
BASE_URL = os.getenv("BASE_URL", "https://vet.goznak.ru")
|
||||
|
||||
|
||||
@pytest.mark.vetstation
|
||||
@pytest.mark.usefixtures("browser_page")
|
||||
class TestBP01Dashboard:
|
||||
"""
|
||||
BP01: Навигация по разделам личного кабинета ветеринарной станции
|
||||
|
||||
Проверяет что пользователь с ролью 'Ветеринарная станция' может:
|
||||
- Открыть главную страницу (Статистика/Dashboard)
|
||||
- Видеть заголовок своей организации
|
||||
- Видеть блок статистики регистрации животных
|
||||
- Видеть таблицу последних зарегистрированных особей
|
||||
- Перейти в раздел Животные
|
||||
- Перейти в раздел Владельцы животных
|
||||
- Перейти в раздел Единый реестр
|
||||
"""
|
||||
|
||||
@pytest.mark.dependency(name="VSBP01::s01")
|
||||
def test_s01_open_dashboard(self, browser_page: Page, bp_state: dict):
|
||||
"""Шаг 1: Открыть главную страницу (Статистика)"""
|
||||
browser_page.goto(BASE_URL + "/dashboard")
|
||||
expect(browser_page).to_have_url(BASE_URL + "/dashboard")
|
||||
|
||||
@pytest.mark.dependency(name="VSBP01::s02", depends=["VSBP01::s01"])
|
||||
def test_s02_check_org_header(self, browser_page: Page, bp_state: dict):
|
||||
"""Шаг 2: Проверить заголовок организации"""
|
||||
expect(browser_page.locator("#app")).to_contain_text(
|
||||
"Ветеринарная станция"
|
||||
)
|
||||
|
||||
@pytest.mark.dependency(name="VSBP01::s03", depends=["VSBP01::s02"])
|
||||
def test_s03_check_stats_block(self, browser_page: Page, bp_state: dict):
|
||||
"""Шаг 3: Проверить блок статистики регистрации животных"""
|
||||
expect(
|
||||
browser_page.get_by_text("Статистика регистрации домашних животных")
|
||||
).to_be_visible(timeout=10000)
|
||||
expect(
|
||||
browser_page.get_by_text("Всего особей").first
|
||||
).to_be_visible(timeout=10000)
|
||||
|
||||
|
||||
@pytest.mark.dependency(name="VSBP01::s04", depends=["VSBP01::s03"])
|
||||
def test_s04_check_animals_table(self, browser_page: Page, bp_state: dict):
|
||||
"""Шаг 4: Проверить таблицу последних зарегистрированных особей"""
|
||||
expect(
|
||||
browser_page.get_by_text("Последние зарегистрированные особи")
|
||||
).to_be_visible(timeout=10000)
|
||||
# Проверяем наличие колонок таблицы
|
||||
expect(browser_page.get_by_role("columnheader", name="Кличка")).to_be_visible()
|
||||
expect(browser_page.get_by_role("columnheader", name="Дата")).to_be_visible()
|
||||
|
||||
@pytest.mark.dependency(name="VSBP01::s05", depends=["VSBP01::s04"])
|
||||
def test_s05_go_to_animals(self, browser_page: Page, bp_state: dict):
|
||||
"""Шаг 5: Перейти в раздел Животные"""
|
||||
browser_page.get_by_role("link", name="Животные").click()
|
||||
expect(browser_page).to_have_url(BASE_URL + "/animals", timeout=10000)
|
||||
expect(browser_page.locator("#app")).to_be_visible(timeout=10000)
|
||||
|
||||
@pytest.mark.dependency(name="VSBP01::s06", depends=["VSBP01::s05"])
|
||||
def test_s06_go_to_owners(self, browser_page: Page, bp_state: dict):
|
||||
"""Шаг 6: Перейти в раздел Владельцы животных"""
|
||||
import re
|
||||
browser_page.get_by_role("link", name="Владельцы животных").click()
|
||||
# Страница открывается с query-параметром (?type=individual), поэтому regex
|
||||
expect(browser_page).to_have_url(
|
||||
re.compile(r".*/owners.*"), timeout=10000
|
||||
)
|
||||
expect(browser_page.locator("#app")).to_be_visible(timeout=10000)
|
||||
|
||||
|
||||
|
||||
@pytest.mark.dependency(name="VSBP01::s07", depends=["VSBP01::s06"])
|
||||
def test_s07_go_to_registry(self, browser_page: Page, bp_state: dict):
|
||||
"""Шаг 7: Перейти в раздел Единый реестр"""
|
||||
browser_page.get_by_role("link", name="Единый реестр").click()
|
||||
expect(browser_page).to_have_url(BASE_URL + "/registry", timeout=10000)
|
||||
expect(browser_page.locator("#app")).to_be_visible(timeout=10000)
|
||||
47
tests/02_vetstation/conftest.py
Normal file
47
tests/02_vetstation/conftest.py
Normal file
@ -0,0 +1,47 @@
|
||||
# pyrefly: ignore [missing-import]
|
||||
import pytest
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Пути к статическим артефактам роли (фото, документы, файлы)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def fixtures_dir() -> Path:
|
||||
"""
|
||||
Путь к папке со статическими артефактами роли vetstation.
|
||||
Структура папки:
|
||||
fixtures/
|
||||
├── images/ ← фотографии животных, документы-сканы
|
||||
└── docs/ ← PDF, сертификаты и прочие документы
|
||||
"""
|
||||
return Path(__file__).parent / "fixtures"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Готовые фикстуры для конкретных артефактов
|
||||
# Добавляй новые по мере появления новых файлов в папке fixtures/
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# @pytest.fixture(scope="session")
|
||||
# def animal_photo(fixtures_dir) -> Path:
|
||||
# """Фото животного для теста регистрации."""
|
||||
# return fixtures_dir / "images" / "animal_photo.jpg"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Контейнер для передачи данных между разными БП одной роли (scope=session)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def shared_state() -> dict:
|
||||
"""
|
||||
Словарь для передачи данных между разными бизнес-процессами
|
||||
в рамках одной тестовой сессии для роли vetstation.
|
||||
|
||||
Пример:
|
||||
# В BP02 создаём животное → BP03 проверяет его в реестре
|
||||
shared_state["animal_id"] = "96416"
|
||||
"""
|
||||
return {}
|
||||
1
tests/02_vetstation/fixtures/docs/.gitkeep
Normal file
1
tests/02_vetstation/fixtures/docs/.gitkeep
Normal file
@ -0,0 +1 @@
|
||||
# PDF, сертификаты, ветпаспорта для роли vetstation
|
||||
1
tests/02_vetstation/fixtures/images/.gitkeep
Normal file
1
tests/02_vetstation/fixtures/images/.gitkeep
Normal file
@ -0,0 +1 @@
|
||||
# Фотографии животных и документы-сканы для роли vetstation
|
||||
@ -1,41 +1,133 @@
|
||||
import os
|
||||
# pyrefly: ignore [missing-import]
|
||||
import pytest
|
||||
# pyrefly: ignore [missing-import]
|
||||
from dotenv import load_dotenv
|
||||
# pyrefly: ignore [missing-import]
|
||||
from playwright.sync_api import Page
|
||||
|
||||
# Словарь с учетными данными для разных ролей
|
||||
# Загружаем переменные из .env (файл находится в корне проекта)
|
||||
load_dotenv()
|
||||
|
||||
# Базовый URL портала — берётся из .env или дефолт
|
||||
BASE_URL = os.getenv("BASE_URL", "https://vet.goznak.ru")
|
||||
|
||||
# Учётные данные для разных ролей — берутся из .env
|
||||
CREDENTIALS = {
|
||||
"vetdept": {"login": "gz_vetdept@vet.goznak.ru", "password": "y2pyBAn7zw"},
|
||||
"vetstation": {"login": "gz_vetstation@vet.goznak.ru", "password": "A6wrpzpD5T"},
|
||||
"prefecture": {"login": "gz_prefecture@vet.goznak.ru", "password": "sbT9shGsbe"},
|
||||
"vetdept": {
|
||||
"login": os.getenv("VETDEPT_LOGIN"),
|
||||
"password": os.getenv("VETDEPT_PASSWORD"),
|
||||
},
|
||||
"vetstation": {
|
||||
"login": os.getenv("VETSTATION_LOGIN"),
|
||||
"password": os.getenv("VETSTATION_PASSWORD"),
|
||||
},
|
||||
"prefecture": {
|
||||
"login": os.getenv("PREFECTURE_LOGIN"),
|
||||
"password": os.getenv("PREFECTURE_PASSWORD"),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _get_role(request) -> str:
|
||||
"""Определяет роль теста по pytest-маркеру."""
|
||||
for marker in request.node.iter_markers():
|
||||
if marker.name in CREDENTIALS:
|
||||
return marker.name
|
||||
pytest.fail("ОШИБКА: У теста нет маркера роли (например, @pytest.mark.vetdept)!")
|
||||
|
||||
|
||||
def _login(page: Page, role: str) -> None:
|
||||
"""Выполняет авторизацию на портале под указанной ролью."""
|
||||
creds = CREDENTIALS[role]
|
||||
if not creds["login"] or not creds["password"]:
|
||||
pytest.fail(
|
||||
f"ОШИБКА: Переменные окружения для роли '{role}' не заданы. "
|
||||
f"Скопируй .env.example в .env и заполни данные."
|
||||
)
|
||||
print(f"\n[АВТОЛОГИН] Вход под ролью: {role}")
|
||||
page.goto(BASE_URL)
|
||||
page.get_by_role("textbox", name="Email").fill(creds["login"])
|
||||
page.get_by_role("textbox", name="Введите пароль").fill(creds["password"])
|
||||
page.get_by_role("button", name="Войти в аккаунт").click()
|
||||
page.wait_for_selector("#app", timeout=10000)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Фикстура для бизнес-процессов (scope=class)
|
||||
# Один браузер на весь класс — авторизация выполняется один раз.
|
||||
# Создаёт собственный context и page, чтобы избежать ScopeMismatch
|
||||
# с function-scoped page фикстурой pytest-playwright.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@pytest.fixture(scope="class")
|
||||
def browser_page(browser, request):
|
||||
"""
|
||||
Авторизованная страница с жизненным циклом на весь класс (бизнес-процесс).
|
||||
Все шаги внутри класса работают в одной вкладке браузера.
|
||||
"""
|
||||
role = _get_role(request)
|
||||
context = browser.new_context()
|
||||
page = context.new_page()
|
||||
_login(page, role)
|
||||
yield page
|
||||
context.close()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Контейнер для передачи данных между шагами одного БП (scope=class)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@pytest.fixture(scope="class")
|
||||
def bp_state() -> dict:
|
||||
"""
|
||||
Словарь для передачи данных между шагами одного бизнес-процесса.
|
||||
Создаётся при старте класса, уничтожается после его завершения.
|
||||
|
||||
Пример:
|
||||
def test_s01_create(self, browser_page, bp_state):
|
||||
bp_state["claim_id"] = "12345"
|
||||
|
||||
def test_s02_verify(self, browser_page, bp_state):
|
||||
assert bp_state["claim_id"] == "12345"
|
||||
"""
|
||||
return {}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Устаревшая фикстура — оставлена для обратной совместимости
|
||||
# Используй browser_page для новых тестов
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def role_page(page: Page, request):
|
||||
"""
|
||||
Фикстура, которая определяет роль теста и автоматически авторизует пользователя.
|
||||
[DEPRECATED] Авторизованная страница с жизненным циклом на один тест.
|
||||
Для новых тестов используй browser_page (scope=class).
|
||||
"""
|
||||
# 2. Выполняем логин
|
||||
role = None
|
||||
|
||||
for marker in request.node.iter_markers():
|
||||
if marker.name in CREDENTIALS:
|
||||
role = marker.name
|
||||
break
|
||||
|
||||
# 3. Если маркер не найден, честно говорим об этом (вместо молчаливого NameError)
|
||||
if role is None:
|
||||
pytest.fail("ОШИБКА: У теста нет маркера роли (например, @pytest.mark.vetstation)!")
|
||||
|
||||
print(f"\n[АВТОЛОГИН] Выполняется вход под ролью: {role}")
|
||||
|
||||
page.goto("https://vet.goznak.ru")
|
||||
creds = CREDENTIALS[role]
|
||||
|
||||
page.get_by_role("textbox", name="Email").fill(creds["login"])
|
||||
page.get_by_role("textbox", name="Введите пароль").fill(creds["password"])
|
||||
page.get_by_role("button", name="Войти в аккаунт").click()
|
||||
|
||||
page.wait_for_selector("#app", timeout=10000)
|
||||
|
||||
role = _get_role(request)
|
||||
_login(page, role)
|
||||
yield page
|
||||
page.context.clear_cookies()
|
||||
|
||||
page.context.clear_cookies()
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Хук для отображения читаемых имён шагов в pytest-html отчёте
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@pytest.hookimpl(hookwrapper=True)
|
||||
def pytest_runtest_makereport(item, call):
|
||||
"""
|
||||
Заменяет технический путь к тесту в колонке Test отчёта pytest-html
|
||||
на docstring метода (например, 'Шаг 1: Открыть главную страницу').
|
||||
|
||||
Если docstring отсутствует — оставляет оригинальный nodeid.
|
||||
"""
|
||||
outcome = yield
|
||||
report = outcome.get_result()
|
||||
|
||||
# Берём docstring тестовой функции
|
||||
doc = getattr(item.function, "__doc__", None)
|
||||
if doc:
|
||||
description = doc.strip().splitlines()[0] # только первая строка
|
||||
report.nodeid = description
|
||||
3
tests/testdata/__init__.py
vendored
Normal file
3
tests/testdata/__init__.py
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
# Пакет тестовых данных
|
||||
# Содержит генераторы случайных данных по правилам ФЛК
|
||||
from .generators import * # noqa: F401, F403
|
||||
198
tests/testdata/generators.py
vendored
Normal file
198
tests/testdata/generators.py
vendored
Normal file
@ -0,0 +1,198 @@
|
||||
"""
|
||||
Генераторы тестовых данных по правилам ФЛК (Форматно-Логический Контроль).
|
||||
|
||||
=============================================================================
|
||||
КАК ДОБАВИТЬ НОВЫЙ ГЕНЕРАТОР
|
||||
=============================================================================
|
||||
|
||||
1. Определи раздел (секцию) к которому относится данное поле.
|
||||
Разделы обозначены комментариями: # === РАЗДЕЛ: ... ===
|
||||
|
||||
2. Если нужного раздела нет — добавь новый в конце файла по шаблону:
|
||||
|
||||
# =============================================================================
|
||||
# === РАЗДЕЛ: Название раздела ===
|
||||
# =============================================================================
|
||||
|
||||
3. Напиши функцию-генератор в нужном разделе:
|
||||
|
||||
def gen_<сущность>_<поле>(<параметры>) -> <тип>:
|
||||
\"\"\"
|
||||
Краткое описание что генерирует функция.
|
||||
|
||||
ФЛК:
|
||||
- Правило 1 (например: только цифры)
|
||||
- Правило 2 (например: длина ровно 15 символов)
|
||||
- Правило 3 (например: начинается с '643')
|
||||
|
||||
Аргументы:
|
||||
param1 — описание параметра и его диапазон
|
||||
|
||||
Пример вывода: '643094100123456'
|
||||
\"\"\"
|
||||
...
|
||||
|
||||
4. Если функция использует Faker — используй уже созданный экземпляр `fake`
|
||||
(не создавай новые экземпляры в каждой функции).
|
||||
|
||||
5. Если данные статические (например, список имён) — помести их как константу
|
||||
перед функцией с именем _DATA_<НАЗНАЧЕНИЕ>.
|
||||
|
||||
=============================================================================
|
||||
ПРИМЕР ИСПОЛЬЗОВАНИЯ В ТЕСТЕ
|
||||
=============================================================================
|
||||
|
||||
from tests.testdata.generators import gen_microchip, gen_animal_name
|
||||
|
||||
class TestBP02RegisterAnimal:
|
||||
def test_s01_fill_form(self, browser_page, bp_state):
|
||||
name = gen_animal_name()
|
||||
microchip = gen_microchip()
|
||||
|
||||
browser_page.fill("#animal-name", name)
|
||||
browser_page.fill("#microchip", microchip)
|
||||
|
||||
bp_state["animal_name"] = name # передаём следующему шагу
|
||||
bp_state["microchip"] = microchip
|
||||
|
||||
=============================================================================
|
||||
"""
|
||||
|
||||
import random
|
||||
import string
|
||||
from datetime import date, timedelta
|
||||
# pyrefly: ignore [missing-import]
|
||||
from faker import Faker
|
||||
|
||||
# Единственный экземпляр Faker на весь модуль — не создавай новые в функциях
|
||||
fake = Faker("ru_RU")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# === РАЗДЕЛ: Животное ===
|
||||
# =============================================================================
|
||||
|
||||
_DATA_ANIMAL_NAMES = [
|
||||
"Мурка", "Барсик", "Шарик", "Рекс", "Симба",
|
||||
"Дружок", "Звезда", "Кличко", "Жорик", "Буся",
|
||||
"Тузик", "Маркиз", "Пуговка", "Снежок", "Рыжик",
|
||||
]
|
||||
|
||||
|
||||
def gen_animal_name() -> str:
|
||||
"""
|
||||
Кличка животного.
|
||||
|
||||
ФЛК:
|
||||
- Только кириллица
|
||||
- Первая буква заглавная
|
||||
- От 2 до 20 символов
|
||||
|
||||
Пример вывода: 'Мурка'
|
||||
"""
|
||||
return random.choice(_DATA_ANIMAL_NAMES)
|
||||
|
||||
|
||||
def gen_birth_date(min_years: int = 0, max_years: int = 20) -> str:
|
||||
"""
|
||||
Дата рождения животного.
|
||||
|
||||
ФЛК:
|
||||
- Не позднее сегодняшнего дня
|
||||
- Не ранее max_years лет назад
|
||||
- Формат: ДД.ММ.ГГГГ
|
||||
|
||||
Аргументы:
|
||||
min_years — минимальный возраст в годах (по умолчанию 0 = только что рождён)
|
||||
max_years — максимальный возраст в годах (по умолчанию 20)
|
||||
|
||||
Пример вывода: '15.03.2021'
|
||||
"""
|
||||
min_days = min_years * 365
|
||||
max_days = max_years * 365
|
||||
delta = random.randint(min_days, max_days)
|
||||
birth = date.today() - timedelta(days=delta)
|
||||
return birth.strftime("%d.%m.%Y")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# === РАЗДЕЛ: Маркировка животного ===
|
||||
# =============================================================================
|
||||
|
||||
def gen_microchip() -> str:
|
||||
"""
|
||||
Номер микрочипа.
|
||||
|
||||
ФЛК:
|
||||
- Ровно 15 цифр
|
||||
- Стандарт ISO 11784/85
|
||||
- Первые 3 цифры — код страны (643 = Россия) [опционально]
|
||||
|
||||
Пример вывода: '643094100123456'
|
||||
"""
|
||||
return "".join(str(random.randint(0, 9)) for _ in range(15))
|
||||
|
||||
|
||||
def gen_tattoo() -> str:
|
||||
"""
|
||||
Номер клейма (тату-маркировка).
|
||||
|
||||
ФЛК:
|
||||
- 2 заглавные латинские буквы
|
||||
- 4 цифры
|
||||
- Итого 6 символов
|
||||
|
||||
Пример вывода: 'AB1234'
|
||||
"""
|
||||
letters = "".join(random.choices(string.ascii_uppercase, k=2))
|
||||
digits = "".join(str(random.randint(0, 9)) for _ in range(4))
|
||||
return letters + digits
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# === РАЗДЕЛ: Владелец животного ===
|
||||
# =============================================================================
|
||||
|
||||
def gen_owner_full_name() -> str:
|
||||
"""
|
||||
ФИО владельца.
|
||||
|
||||
ФЛК:
|
||||
- Только кириллица
|
||||
- Формат: Фамилия Имя Отчество
|
||||
|
||||
Пример вывода: 'Иванов Иван Иванович'
|
||||
"""
|
||||
return fake.name()
|
||||
|
||||
|
||||
def gen_owner_phone() -> str:
|
||||
"""
|
||||
Мобильный телефон владельца.
|
||||
|
||||
ФЛК:
|
||||
- Российский номер
|
||||
- Начинается с +7
|
||||
- Код оператора: 9XX (мобильные)
|
||||
- Формат: +7(9XX)XXX-XX-XX
|
||||
|
||||
Пример вывода: '+7(916)543-21-09'
|
||||
"""
|
||||
code = random.randint(900, 999)
|
||||
part1 = random.randint(100, 999)
|
||||
part2 = random.randint(10, 99)
|
||||
part3 = random.randint(10, 99)
|
||||
return f"+7({code}){part1}-{part2}-{part3}"
|
||||
|
||||
|
||||
def gen_owner_email() -> str:
|
||||
"""
|
||||
Email владельца.
|
||||
|
||||
ФЛК:
|
||||
- Валидный формат email
|
||||
- Латинские символы
|
||||
|
||||
Пример вывода: 'ivanov.ivan@example.com'
|
||||
"""
|
||||
return fake.email()
|
||||
Reference in New Issue
Block a user