"""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) 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 __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") self._like_button = SmartLocator.by_testid("like-button") @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) def delete(self) -> None: """Click the delete button and accept the confirmation dialog.""" self.page.on("dialog", lambda dialog: dialog.accept()) self._delete_btn.click(self.page) 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._like_button.click(self.page)