# 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")