Files
blog.pyaqa.ru/tests/e2e/pages/__init__.py
Sergey Vanyushkin 96ecad0c6f
All checks were successful
ci/woodpecker/pr/pipeline Pipeline was successful
refactor: update e2e page objects to use SmartLocator .loc() API
Ultraworked with Sisyphus(https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-05-15 20:28:55 +03:00

260 lines
7.5 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
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 create_post(self) -> None:
"""Click the 'Write a Post' button to navigate to the form."""
self.loc("btn-create-post-header").click()
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.loc("empty-state")._get_locator().is_visible()
def count_posts(self) -> int:
"""Count the number of post cards on the page.
Returns:
Number of visible post cards.
"""
return self.page.locator('article[data-testid^="post-card-"]').count()
def get_current_page(self) -> int:
"""Get the current pagination page number.
Returns:
Current page number as integer.
"""
text = self.page.locator('[data-testid="pagination-current"]').text_content()
return int(text) if text else 1
def can_go_next(self) -> bool:
"""Check if the next page link is enabled.
Returns:
True if the next pagination control is a clickable link.
"""
tag = self.page.locator('[data-testid="pagination-next"]').evaluate(
"el => el.tagName.toLowerCase()"
)
return bool(tag == "a")
def can_go_prev(self) -> bool:
"""Check if the previous page link is enabled.
Returns:
True if the previous pagination control is a clickable link.
"""
tag = self.page.locator('[data-testid="pagination-prev"]').evaluate(
"el => el.tagName.toLowerCase()"
)
return bool(tag == "a")
def go_to_next_page(self) -> None:
"""Click the next page pagination link."""
self.page.locator('[data-testid="pagination-next"]').click()
def go_to_prev_page(self) -> None:
"""Click the previous page pagination link."""
self.page.locator('[data-testid="pagination-prev"]').click()
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 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.loc("input-title").fill(title)
self.loc("input-tags").fill(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.loc("btn-publish-post").click()
def save_draft(self) -> None:
"""Click the 'Save as Draft' button."""
self.loc("btn-save-draft").click()
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
@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.loc("post-detail-title").text_content() or ""
def get_status(self) -> str:
"""Get the post status badge text.
Returns:
Status text ('Published' or 'Draft').
"""
return self.loc("post-detail-status").text_content() or ""
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.loc("btn-edit-post").click()
def can_edit(self) -> bool:
"""Check if the edit button is visible.
Returns:
True if edit button is present.
"""
return self.loc("btn-edit-post")._get_locator().is_visible()
def can_delete(self) -> bool:
"""Check if the delete button is visible.
Returns:
True if delete button is present.
"""
return self.loc("btn-delete-post")._get_locator().is_visible()
def delete(self) -> None:
"""Click the delete button and accept the confirmation dialog."""
self.page.on("dialog", lambda dialog: dialog.accept())
self.loc("btn-delete-post").click()
def get_like_count(self) -> int:
"""Get the current like count from the detail page.
Returns:
Current like count as integer.
"""
text = self.page.locator("#like-count").text_content()
return int(text.strip()) if text else 0
def click_like(self) -> None:
"""Click the like/unlike button to toggle the like state."""
self.loc("like-button").click()