Files
blog.pyaqa.ru/tests/e2e/pages/__init__.py
Sergey Vanyushkin 46cc06b596 feat: RBAC E2E тесты и фикс admin-прав для редактирования постов
Основные изменения:
- Добавлены E2E тесты для проверки ownership (TC-E2E-102/103):
  * test_admin_can_edit_any_post — admin может редактировать любой пост
  * test_user_cannot_edit_other_users_post — user не может редактировать чужой пост
- Исправлены use cases (UpdatePost, DeletePost, PublishPost) — добавлена проверка роли admin
- Обновлены web routes и API routes для передачи роли в use cases
- Добавлены unit тесты для admin-сценариев

Реструктуризация тестов:
- Удалены старые API тесты (tests/api/) — требуют переработки
- Удалены старые integration тесты (tests/integration/)
- Переработаны E2E тесты: удалены старые, добавлены новые с POM
- Добавлена документация тестов: FEATURE_*.md, TEST_MODEL.md, AGENTS.md

Инфраструктура:
- Добавлен MockKeycloakClient для dev-режима
- Добавлены статические файлы: EasyMDE, Highlight.js, стили markdown
- Обновлены шаблоны: base.html, post_form.html, post_detail.html
- Обновлена DI конфигурация и провайдеры

Документация:
- tests/FEATURE_RBAC.md — матрица тестов RBAC
- tests/FEATURE_POST_LIFECYCLE.md — тесты жизненного цикла поста
- tests/FEATURE_DOMAIN_FOUNDATION.md — тесты доменного слоя
- tests/FEATURE_INFRASTRUCTURE.md — тесты инфраструктуры
- tests/TEST_MODEL.md — глобальная матрица покрытия
- app/presentation/web/AGENTS.md — гайд по Web UI
- tests/AGENTS.md — гайд по тестированию
2026-05-07 19:55:15 +03:00

226 lines
6.7 KiB
Python

"""Page Object Models for blog web UI.
Provides POM classes for home page, post form, and post detail pages.
"""
from __future__ import annotations
from typing import TYPE_CHECKING
from pytfm.web import BasePage, SmartLocator
if TYPE_CHECKING:
from playwright.sync_api import Page
class HomePage(BasePage):
"""Page object for the blog home/posts listing page.
Attributes:
path: URL path for the home page.
"""
path = "/web/"
def __init__(self, page: Page, base_url: str) -> None:
"""Initialize the home page object.
Args:
page: Playwright Page instance.
base_url: Application base URL.
"""
super().__init__(page, base_url)
self._create_post_btn = SmartLocator.by_testid("btn-create-post-header")
self._post_list = SmartLocator.by_testid("post-list")
self._empty_state = SmartLocator.by_testid("empty-state")
def create_post(self) -> None:
"""Click the 'Write a Post' button to navigate to the form."""
self._create_post_btn.click(self.page)
def has_post_with_title(self, title: str) -> bool:
"""Check if a post with the given title is present in the list.
Args:
title: Post title to search for.
Returns:
True if the post title is found on the page.
"""
safe_title = title.replace('"', '\\"')
selector = f'[data-testid^="post-title-link-"]:has-text("{safe_title}")'
return self.page.locator(selector).count() > 0
def has_no_post_with_title(self, title: str) -> bool:
"""Check that no post with the given title is present in the list.
Args:
title: Post title to search for.
Returns:
True if the post title is not found on the page.
"""
safe_title = title.replace('"', '\\"')
selector = f'[data-testid^="post-title-link-"]:has-text("{safe_title}")'
return self.page.locator(selector).count() == 0
def open_post(self, title: str) -> None:
"""Click on a post title link to open the detail page.
Args:
title: Post title to click.
"""
locator = self.page.locator("[data-testid^='post-title-link-']").filter(has_text=title)
locator.click()
def is_empty(self) -> bool:
"""Check if the posts list is empty.
Returns:
True if the empty state is visible.
"""
return self._empty_state.is_visible(self.page)
class PostFormPage(BasePage):
"""Page object for the new post / edit post form.
Attributes:
path: URL path for the new post form.
"""
path = "/web/posts/new"
def __init__(self, page: Page, base_url: str) -> None:
"""Initialize the post form page object.
Args:
page: Playwright Page instance.
base_url: Application base URL.
"""
super().__init__(page, base_url)
self._title_input = SmartLocator.by_testid("input-title")
self._content_input = SmartLocator.by_testid("textarea-content")
self._tags_input = SmartLocator.by_testid("input-tags")
self._publish_btn = SmartLocator.by_testid("btn-publish-post")
self._save_draft_btn = SmartLocator.by_testid("btn-save-draft")
def fill_form(self, title: str, content: str, tags: str) -> None:
"""Fill the post creation form.
Args:
title: Post title.
content: Post content (markdown).
tags: Comma-separated tags string.
"""
self._title_input.fill(self.page, title)
self._tags_input.fill(self.page, tags)
self.page.evaluate(
"(content) => {"
" const cm = document.querySelector('.CodeMirror');"
" if (cm && cm.CodeMirror) {"
" cm.CodeMirror.setValue(content);"
" }"
" const textarea = document.querySelector('[data-testid=\"textarea-content\"]');"
" if (textarea) textarea.value = content;"
"}",
content,
)
def publish(self) -> None:
"""Click the publish button to submit the form."""
self._publish_btn.click(self.page)
def save_draft(self) -> None:
"""Click the 'Save as Draft' button."""
self._save_draft_btn.click(self.page)
class PostDetailPage(BasePage):
"""Page object for the post detail page.
Attributes:
path_template: URL path template with {slug} placeholder.
"""
path_template = "/web/posts/{slug}"
def __init__(self, page: Page, base_url: str, slug: str) -> None:
"""Initialize the post detail page object.
Args:
page: Playwright Page instance.
base_url: Application base URL.
slug: Post URL slug.
"""
super().__init__(page, base_url)
self.slug = slug
self._title = SmartLocator.by_testid("post-detail-title")
self._status = SmartLocator.by_testid("post-detail-status")
self._content = SmartLocator.by_testid("post-detail-content")
self._edit_btn = SmartLocator.by_testid("btn-edit-post")
self._delete_btn = SmartLocator.by_testid("btn-delete-post")
@property
def url(self) -> str:
"""Return the full URL for this post detail page.
Returns:
Full post detail URL.
"""
return f"{self.base_url}{self.path_template.format(slug=self.slug)}"
def open(self) -> PostDetailPage:
"""Navigate to the post detail page.
Returns:
Self for method chaining.
"""
self.page.goto(self.url)
return self
def get_title(self) -> str:
"""Get the post title text.
Returns:
Post title string.
"""
return self._title.get_text(self.page)
def get_status(self) -> str:
"""Get the post status badge text.
Returns:
Status text ('Published' or 'Draft').
"""
return self._status.get_text(self.page)
def is_published(self) -> bool:
"""Check if the post status is 'Published'.
Returns:
True if status badge reads 'Published'.
"""
return self.get_status() == "Published"
def edit(self) -> None:
"""Click the edit button to navigate to the edit form."""
self._edit_btn.click(self.page)
def can_edit(self) -> bool:
"""Check if the edit button is visible.
Returns:
True if edit button is present.
"""
return self._edit_btn.is_visible(self.page)
def can_delete(self) -> bool:
"""Check if the delete button is visible.
Returns:
True if delete button is present.
"""
return self._delete_btn.is_visible(self.page)