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