All checks were successful
Deploy / deploy (push) Successful in 1m49s
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>
135 lines
4.5 KiB
Python
135 lines
4.5 KiB
Python
# ABOUTME: Page object for animal move page with selection handling.
|
|
# ABOUTME: Encapsulates navigation, filter, selection, and optimistic lock handling.
|
|
|
|
from playwright.sync_api import Page, expect
|
|
|
|
|
|
class MovePage:
|
|
"""Page object for animal move page."""
|
|
|
|
def __init__(self, page: Page, base_url: str):
|
|
self.page = page
|
|
self.base_url = base_url
|
|
|
|
def goto_move_page(self, filter_str: str = ""):
|
|
"""Navigate to the move animals page.
|
|
|
|
Args:
|
|
filter_str: Optional filter DSL query to pre-populate
|
|
"""
|
|
url = f"{self.base_url}/move"
|
|
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 1"')
|
|
"""
|
|
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.
|
|
|
|
Returns number of animals in selection, or 0 if not found.
|
|
"""
|
|
container = self.page.locator("#selection-container")
|
|
if container.count() == 0:
|
|
return 0
|
|
|
|
# Try to find count text (e.g., "5 animals selected")
|
|
text = container.text_content() or ""
|
|
import re
|
|
|
|
match = re.search(r"(\d+)\s*animal", text.lower())
|
|
if match:
|
|
return int(match.group(1))
|
|
|
|
# Count checkboxes if present
|
|
checkboxes = container.locator('input[type="checkbox"]')
|
|
return checkboxes.count()
|
|
|
|
def move_to_location(self, destination_name: str, notes: str = ""):
|
|
"""Select destination and submit move.
|
|
|
|
Args:
|
|
destination_name: Human-readable location name
|
|
notes: Optional notes
|
|
"""
|
|
self.page.select_option("#to_location_id", label=destination_name)
|
|
|
|
if notes:
|
|
self.page.fill("#notes", notes)
|
|
|
|
self.page.click('button[type="submit"]')
|
|
self.page.wait_for_load_state("networkidle")
|
|
|
|
def move_animals(
|
|
self,
|
|
*,
|
|
filter_str: str,
|
|
destination_name: str,
|
|
notes: str = "",
|
|
):
|
|
"""Complete move flow: set filter, select destination, submit.
|
|
|
|
Args:
|
|
filter_str: Filter DSL query
|
|
destination_name: Human-readable destination location
|
|
notes: Optional notes
|
|
"""
|
|
self.goto_move_page()
|
|
self.set_filter(filter_str)
|
|
self.move_to_location(destination_name, notes)
|
|
|
|
def has_mismatch_error(self) -> bool:
|
|
"""Check if a selection mismatch (409) error is displayed."""
|
|
# Look for mismatch/conflict panel indicators
|
|
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 get_mismatch_diff(self) -> dict:
|
|
"""Get the diff information from a mismatch panel.
|
|
|
|
Returns dict with removed/added counts if mismatch found.
|
|
"""
|
|
# This depends on actual UI structure of mismatch panel
|
|
return {}
|
|
|
|
def confirm_mismatch(self):
|
|
"""Click confirm button to proceed despite mismatch."""
|
|
# Look for confirm button - text varies
|
|
confirm_btn = self.page.locator('button:has-text("Confirm")')
|
|
if confirm_btn.count() > 0:
|
|
confirm_btn.click()
|
|
self.page.wait_for_load_state("networkidle")
|
|
return
|
|
|
|
# Try alternative selectors
|
|
confirm_btn = self.page.locator('button:has-text("Proceed")')
|
|
if confirm_btn.count() > 0:
|
|
confirm_btn.click()
|
|
self.page.wait_for_load_state("networkidle")
|
|
|
|
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()
|