Compare commits

...

4 Commits

Author SHA1 Message Date
99acd9d287 Merge pull request 'refactor: update e2e page objects to use SmartLocator .loc() API' (#20) from feature/e2e-smartlocator-update into dev
All checks were successful
ci/woodpecker/push/pipeline Pipeline was successful
Reviewed-on: #20
2026-05-15 18:10:07 +00:00
96ecad0c6f refactor: update e2e page objects to use SmartLocator .loc() API
All checks were successful
ci/woodpecker/pr/pipeline Pipeline was successful
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
ca84bd7fac Pr commenting (#19)
All checks were successful
ci/woodpecker/push/pipeline Pipeline was successful
2026-05-13 19:21:43 +00:00
9124aa17d5 feat: add pr-comment step to post CI results on pull requests
All checks were successful
ci/woodpecker/pr/pipeline Pipeline was successful
Adds a Woodpecker pipeline step that posts a formatted comment with test results and coverage to Gitea PRs after CI completes.

Comment includes: commit SHA (linked), source/target branches, pipeline link, and a status table for lint, type check, unit tests, integration tests, e2e tests, and coverage percentage.
2026-05-13 21:22:17 +03:00
2 changed files with 44 additions and 46 deletions

View File

@@ -115,3 +115,33 @@ steps:
- uv run --no-sync coverage combine .coverage.unit .coverage.integration
- uv run --no-sync coverage report --fail-under=70 --include=app/*
- uv run --no-sync coverage html
- name: pr-comment
image: python:3.13
volumes:
- /tmp/uv-cache:/root/.cache/uv
environment:
UV_CACHE_DIR: /root/.cache/uv
UV_LINK_MODE: copy
UV_PYTHON: "3.13"
GITEA_API_TOKEN:
from_secret: gitea_api_token
depends_on: [coverage, lint, type]
when:
event: [pull_request]
commands:
- pip install uv
- |
SHA7=$(printf '%.7s' "${CI_COMMIT_SHA:-unknown}")
COMMIT_URL="${CI_FORGE_URL}/${CI_REPO_OWNER}/${CI_REPO_NAME}/commit/${CI_COMMIT_SHA}"
SOURCE="${CI_COMMIT_SOURCE:-${CI_COMMIT_SOURCE_BRANCH:-?}}"
TARGET="${CI_COMMIT_TARGET:-${CI_COMMIT_TARGET_BRANCH:-?}}"
PIPELINE_URL="${CI_PIPELINE_URL:-}"
COVER=$(uv run --no-sync coverage report --include='app/*' | tail -1 | awk '{print $NF}')
if [ -z "$GITEA_API_TOKEN" ]; then
echo "pr-comment: GITEA_API_TOKEN not set, skipping"
exit 0
fi
FMT='{"body": "## CI Summary\n\n**Commit:** [`%s`](%s)\n**Branch:** `%s` → `%s`\n**Pipeline:** [View](%s)\n\n### Checks\n\n| Check | Status |\n|-------|--------|\n| Lint (ruff + isort) | ✅ |\n| Type check (mypy) | ✅ |\n| Unit tests | ✅ |\n| Integration tests | ✅ |\n| E2E tests | ✅ |\n| Coverage | **%s** |\n\n---\n*Reported by Woodpecker CI*"}'
BODY=$(printf "$FMT" "$SHA7" "$COMMIT_URL" "$SOURCE" "$TARGET" "$PIPELINE_URL" "$COVER")
curl -s -X POST "${CI_FORGE_URL}/api/v1/repos/${CI_REPO_OWNER}/${CI_REPO_NAME}/issues/${CI_COMMIT_PULL_REQUEST}/comments" -H "Authorization: token $${GITEA_API_TOKEN}" -H "Content-Type: application/json" --data-binary "$BODY"

View File

@@ -7,7 +7,7 @@ from __future__ import annotations
from typing import TYPE_CHECKING
from pytfm.web import BasePage, SmartLocator
from pytfm.web import BasePage
if TYPE_CHECKING:
from playwright.sync_api import Page
@@ -22,21 +22,9 @@ class HomePage(BasePage):
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)
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.
@@ -79,7 +67,7 @@ class HomePage(BasePage):
Returns:
True if the empty state is visible.
"""
return self._empty_state.is_visible(self.page)
return self.loc("empty-state")._get_locator().is_visible()
def count_posts(self) -> int:
"""Count the number of post cards on the page.
@@ -138,20 +126,6 @@ class PostFormPage(BasePage):
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.
@@ -160,8 +134,8 @@ class PostFormPage(BasePage):
content: Post content (markdown).
tags: Comma-separated tags string.
"""
self._title_input.fill(self.page, title)
self._tags_input.fill(self.page, tags)
self.loc("input-title").fill(title)
self.loc("input-tags").fill(tags)
self.page.evaluate(
"(content) => {"
@@ -177,11 +151,11 @@ class PostFormPage(BasePage):
def publish(self) -> None:
"""Click the publish button to submit the form."""
self._publish_btn.click(self.page)
self.loc("btn-publish-post").click()
def save_draft(self) -> None:
"""Click the 'Save as Draft' button."""
self._save_draft_btn.click(self.page)
self.loc("btn-save-draft").click()
class PostDetailPage(BasePage):
@@ -203,12 +177,6 @@ class PostDetailPage(BasePage):
"""
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:
@@ -234,7 +202,7 @@ class PostDetailPage(BasePage):
Returns:
Post title string.
"""
return self._title.get_text(self.page)
return self.loc("post-detail-title").text_content() or ""
def get_status(self) -> str:
"""Get the post status badge text.
@@ -242,7 +210,7 @@ class PostDetailPage(BasePage):
Returns:
Status text ('Published' or 'Draft').
"""
return self._status.get_text(self.page)
return self.loc("post-detail-status").text_content() or ""
def is_published(self) -> bool:
"""Check if the post status is 'Published'.
@@ -254,7 +222,7 @@ class PostDetailPage(BasePage):
def edit(self) -> None:
"""Click the edit button to navigate to the edit form."""
self._edit_btn.click(self.page)
self.loc("btn-edit-post").click()
def can_edit(self) -> bool:
"""Check if the edit button is visible.
@@ -262,7 +230,7 @@ class PostDetailPage(BasePage):
Returns:
True if edit button is present.
"""
return self._edit_btn.is_visible(self.page)
return self.loc("btn-edit-post")._get_locator().is_visible()
def can_delete(self) -> bool:
"""Check if the delete button is visible.
@@ -270,12 +238,12 @@ class PostDetailPage(BasePage):
Returns:
True if delete button is present.
"""
return self._delete_btn.is_visible(self.page)
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._delete_btn.click(self.page)
self.loc("btn-delete-post").click()
def get_like_count(self) -> int:
"""Get the current like count from the detail page.
@@ -288,4 +256,4 @@ class PostDetailPage(BasePage):
def click_like(self) -> None:
"""Click the like/unlike button to toggle the like state."""
self._like_button.click(self.page)
self.loc("like-button").click()