Files
animaltrack/tests/e2e/pages/harvest.py
Petru Paler 51e502ed10
All checks were successful
Deploy / deploy (push) Successful in 1m49s
Add Playwright e2e tests for all 8 spec acceptance scenarios
Implement browser-based e2e tests covering:
- Tests 1-5: Stats progression (cohort, feed, eggs, moves, backdating)
- Test 6: Event viewing and deletion UI
- Test 7: Harvest outcomes with yield items
- Test 8: Optimistic lock selection validation

Includes page objects for reusable form interactions and fresh_db
fixtures for tests requiring isolated database state.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-21 17:30:26 +00:00

177 lines
5.6 KiB
Python

# ABOUTME: Page object for animal outcome (harvest/death) pages.
# ABOUTME: Encapsulates navigation and form interactions for recording outcomes.
from playwright.sync_api import Page, expect
class HarvestPage:
"""Page object for animal outcome (harvest) pages."""
def __init__(self, page: Page, base_url: str):
self.page = page
self.base_url = base_url
def goto_outcome_page(self, filter_str: str = ""):
"""Navigate to the record outcome page.
Args:
filter_str: Optional filter DSL query to pre-populate
"""
url = f"{self.base_url}/actions/outcome"
if filter_str:
url += f"?filter={filter_str}"
self.page.goto(url)
expect(self.page.locator("body")).to_be_visible()
def set_filter(self, filter_str: str):
"""Set the filter field and wait for selection preview.
Args:
filter_str: Filter DSL query (e.g., 'location:"Strip 2" sex:female')
"""
self.page.fill("#filter", filter_str)
# Trigger change event and wait for HTMX preview
self.page.keyboard.press("Tab")
# Wait for selection container to update
self.page.wait_for_selector("#selection-container", state="visible")
self.page.wait_for_load_state("networkidle")
def get_selection_count(self) -> int:
"""Get the count of selected animals from the preview."""
container = self.page.locator("#selection-container")
if container.count() == 0:
return 0
text = container.text_content() or ""
import re
match = re.search(r"(\d+)\s*animal", text.lower())
if match:
return int(match.group(1))
checkboxes = container.locator('input[type="checkbox"]')
return checkboxes.count()
def select_specific_animals(self, animal_ids: list[str]):
"""Select specific animals from checkbox list.
Args:
animal_ids: List of animal IDs to select
"""
for animal_id in animal_ids:
checkbox = self.page.locator(f'input[type="checkbox"][value="{animal_id}"]')
if checkbox.count() > 0:
checkbox.check()
def record_harvest(
self,
*,
filter_str: str = "",
animal_ids: list[str] | None = None,
reason: str = "",
yield_product_code: str = "",
yield_unit: str = "",
yield_quantity: int | None = None,
yield_weight_kg: float | None = None,
notes: str = "",
):
"""Record a harvest outcome.
Args:
filter_str: Filter DSL query (optional if using animal_ids)
animal_ids: Specific animal IDs to select (optional)
reason: Reason for harvest
yield_product_code: Product code for yield (e.g., "meat.part.breast.duck")
yield_unit: Unit for yield (e.g., "kg")
yield_quantity: Quantity of yield items
yield_weight_kg: Weight in kg
notes: Optional notes
"""
self.goto_outcome_page()
if filter_str:
self.set_filter(filter_str)
if animal_ids:
self.select_specific_animals(animal_ids)
# Select harvest outcome
self.page.select_option("#outcome", "harvest")
if reason:
self.page.fill("#reason", reason)
# Fill yield fields if provided
if yield_product_code and yield_product_code != "-":
self.page.select_option("#yield_product_code", yield_product_code)
if yield_unit:
self.page.fill("#yield_unit", yield_unit)
if yield_quantity is not None:
self.page.fill("#yield_quantity", str(yield_quantity))
if yield_weight_kg is not None:
self.page.fill("#yield_weight_kg", str(yield_weight_kg))
if notes:
self.page.fill("#notes", notes)
# Submit
self.page.click('button[type="submit"]')
self.page.wait_for_load_state("networkidle")
def record_death(
self,
*,
filter_str: str = "",
animal_ids: list[str] | None = None,
outcome: str = "died",
reason: str = "",
notes: str = "",
):
"""Record a death/loss outcome.
Args:
filter_str: Filter DSL query (optional)
animal_ids: Specific animal IDs (optional)
outcome: Outcome type (e.g., "died", "escaped", "predated")
reason: Reason for outcome
notes: Optional notes
"""
self.goto_outcome_page()
if filter_str:
self.set_filter(filter_str)
if animal_ids:
self.select_specific_animals(animal_ids)
# Select outcome
self.page.select_option("#outcome", outcome)
if reason:
self.page.fill("#reason", reason)
if notes:
self.page.fill("#notes", notes)
# Submit
self.page.click('button[type="submit"]')
self.page.wait_for_load_state("networkidle")
def has_mismatch_error(self) -> bool:
"""Check if a selection mismatch (409) error is displayed."""
body_text = self.page.locator("body").text_content() or ""
return any(
indicator in body_text.lower()
for indicator in ["mismatch", "conflict", "changed", "removed", "added"]
)
def confirm_mismatch(self):
"""Click confirm button to proceed despite mismatch."""
confirm_btn = self.page.locator('button:has-text("Confirm")')
if confirm_btn.count() > 0:
confirm_btn.click()
self.page.wait_for_load_state("networkidle")