Add Playwright e2e tests for all 8 spec acceptance scenarios
All checks were successful
Deploy / deploy (push) Successful in 1m49s
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>
This commit is contained in:
16
tests/e2e/pages/__init__.py
Normal file
16
tests/e2e/pages/__init__.py
Normal file
@@ -0,0 +1,16 @@
|
||||
# ABOUTME: Page object module exports for Playwright e2e tests.
|
||||
# ABOUTME: Provides clean imports for all page objects.
|
||||
|
||||
from .animals import AnimalsPage
|
||||
from .eggs import EggsPage
|
||||
from .feed import FeedPage
|
||||
from .harvest import HarvestPage
|
||||
from .move import MovePage
|
||||
|
||||
__all__ = [
|
||||
"AnimalsPage",
|
||||
"EggsPage",
|
||||
"FeedPage",
|
||||
"HarvestPage",
|
||||
"MovePage",
|
||||
]
|
||||
72
tests/e2e/pages/animals.py
Normal file
72
tests/e2e/pages/animals.py
Normal file
@@ -0,0 +1,72 @@
|
||||
# ABOUTME: Page object for animal-related pages (cohort creation, registry).
|
||||
# ABOUTME: Encapsulates navigation and form interactions for animal management.
|
||||
|
||||
from playwright.sync_api import Page, expect
|
||||
|
||||
|
||||
class AnimalsPage:
|
||||
"""Page object for animal management pages."""
|
||||
|
||||
def __init__(self, page: Page, base_url: str):
|
||||
self.page = page
|
||||
self.base_url = base_url
|
||||
|
||||
def goto_cohort_form(self):
|
||||
"""Navigate to the create cohort form."""
|
||||
self.page.goto(f"{self.base_url}/actions/cohort")
|
||||
expect(self.page.locator("body")).to_be_visible()
|
||||
|
||||
def create_cohort(
|
||||
self,
|
||||
*,
|
||||
species: str,
|
||||
location_name: str,
|
||||
count: int,
|
||||
life_stage: str,
|
||||
sex: str,
|
||||
origin: str = "purchased",
|
||||
notes: str = "",
|
||||
):
|
||||
"""Fill and submit the create cohort form.
|
||||
|
||||
Args:
|
||||
species: "duck" or "goose"
|
||||
location_name: Human-readable location name (e.g., "Strip 1")
|
||||
count: Number of animals
|
||||
life_stage: "hatchling", "juvenile", or "adult"
|
||||
sex: "unknown", "female", or "male"
|
||||
origin: "hatched", "purchased", "rescued", or "unknown"
|
||||
notes: Optional notes
|
||||
"""
|
||||
self.goto_cohort_form()
|
||||
|
||||
# Fill form fields
|
||||
self.page.select_option("#species", species)
|
||||
self.page.select_option("#location_id", label=location_name)
|
||||
self.page.fill("#count", str(count))
|
||||
self.page.select_option("#life_stage", life_stage)
|
||||
self.page.select_option("#sex", sex)
|
||||
self.page.select_option("#origin", origin)
|
||||
|
||||
if notes:
|
||||
self.page.fill("#notes", notes)
|
||||
|
||||
# Submit the form
|
||||
self.page.click('button[type="submit"]')
|
||||
|
||||
# Wait for navigation/response
|
||||
self.page.wait_for_load_state("networkidle")
|
||||
|
||||
def goto_registry(self, filter_str: str = ""):
|
||||
"""Navigate to the animal registry with optional filter."""
|
||||
url = f"{self.base_url}/registry"
|
||||
if filter_str:
|
||||
url += f"?filter={filter_str}"
|
||||
self.page.goto(url)
|
||||
expect(self.page.locator("body")).to_be_visible()
|
||||
|
||||
def get_animal_count_in_registry(self) -> int:
|
||||
"""Get the count of animals currently displayed in registry."""
|
||||
# Registry shows animal rows - count them
|
||||
rows = self.page.locator("table tbody tr")
|
||||
return rows.count()
|
||||
137
tests/e2e/pages/eggs.py
Normal file
137
tests/e2e/pages/eggs.py
Normal file
@@ -0,0 +1,137 @@
|
||||
# ABOUTME: Page object for egg collection and sales pages.
|
||||
# ABOUTME: Encapsulates navigation and form interactions for product operations.
|
||||
|
||||
from playwright.sync_api import Page, expect
|
||||
|
||||
|
||||
class EggsPage:
|
||||
"""Page object for egg collection and sales pages."""
|
||||
|
||||
def __init__(self, page: Page, base_url: str):
|
||||
self.page = page
|
||||
self.base_url = base_url
|
||||
|
||||
def goto_eggs_page(self):
|
||||
"""Navigate to the eggs (home) page."""
|
||||
self.page.goto(self.base_url)
|
||||
expect(self.page.locator("body")).to_be_visible()
|
||||
|
||||
def collect_eggs(
|
||||
self,
|
||||
*,
|
||||
location_name: str,
|
||||
quantity: int,
|
||||
notes: str = "",
|
||||
):
|
||||
"""Fill and submit the egg harvest (collect) form.
|
||||
|
||||
Args:
|
||||
location_name: Human-readable location name (e.g., "Strip 1")
|
||||
quantity: Number of eggs collected
|
||||
notes: Optional notes
|
||||
"""
|
||||
self.goto_eggs_page()
|
||||
|
||||
# Fill harvest form
|
||||
self.page.select_option("#location_id", label=location_name)
|
||||
self.page.fill("#quantity", str(quantity))
|
||||
|
||||
if notes:
|
||||
self.page.fill("#notes", notes)
|
||||
|
||||
# Submit the harvest form
|
||||
self.page.click('form[hx-post*="product-collected"] button[type="submit"]')
|
||||
|
||||
# Wait for HTMX response
|
||||
self.page.wait_for_load_state("networkidle")
|
||||
|
||||
def collect_eggs_backdated(
|
||||
self,
|
||||
*,
|
||||
location_name: str,
|
||||
quantity: int,
|
||||
datetime_local: str,
|
||||
notes: str = "",
|
||||
):
|
||||
"""Collect eggs with a backdated timestamp.
|
||||
|
||||
Args:
|
||||
location_name: Human-readable location name
|
||||
quantity: Number of eggs
|
||||
datetime_local: Datetime string in format "YYYY-MM-DDTHH:MM"
|
||||
notes: Optional notes
|
||||
"""
|
||||
self.goto_eggs_page()
|
||||
|
||||
# Fill harvest form
|
||||
self.page.select_option("#location_id", label=location_name)
|
||||
self.page.fill("#quantity", str(quantity))
|
||||
|
||||
if notes:
|
||||
self.page.fill("#notes", notes)
|
||||
|
||||
# Expand datetime picker and set backdated time
|
||||
# Click the datetime toggle to expand
|
||||
datetime_toggle = self.page.locator("[data-datetime-picker]")
|
||||
if datetime_toggle.count() > 0:
|
||||
datetime_toggle.first.click()
|
||||
# Fill the datetime-local input
|
||||
self.page.fill('input[type="datetime-local"]', datetime_local)
|
||||
|
||||
# Submit the harvest form
|
||||
self.page.click('form[hx-post*="product-collected"] button[type="submit"]')
|
||||
|
||||
# Wait for HTMX response
|
||||
self.page.wait_for_load_state("networkidle")
|
||||
|
||||
def sell_eggs(
|
||||
self,
|
||||
*,
|
||||
product_code: str = "egg.duck",
|
||||
quantity: int,
|
||||
total_price_cents: int,
|
||||
buyer: str = "",
|
||||
notes: str = "",
|
||||
):
|
||||
"""Fill and submit the egg sale form.
|
||||
|
||||
Args:
|
||||
product_code: Product code (e.g., "egg.duck")
|
||||
quantity: Number of eggs sold
|
||||
total_price_cents: Total price in cents
|
||||
buyer: Optional buyer name
|
||||
notes: Optional notes
|
||||
"""
|
||||
self.goto_eggs_page()
|
||||
|
||||
# Switch to sell tab if needed
|
||||
sell_tab = self.page.locator('text="Sell"')
|
||||
if sell_tab.count() > 0:
|
||||
sell_tab.click()
|
||||
self.page.wait_for_load_state("networkidle")
|
||||
|
||||
# Fill sell form
|
||||
self.page.select_option("#product_code", product_code)
|
||||
self.page.fill("#sell_quantity", str(quantity))
|
||||
self.page.fill("#total_price_cents", str(total_price_cents))
|
||||
|
||||
if buyer:
|
||||
self.page.fill("#buyer", buyer)
|
||||
|
||||
if notes:
|
||||
self.page.fill("#sell_notes", notes)
|
||||
|
||||
# Submit the sell form
|
||||
self.page.click('form[hx-post*="product-sold"] button[type="submit"]')
|
||||
|
||||
# Wait for HTMX response
|
||||
self.page.wait_for_load_state("networkidle")
|
||||
|
||||
def get_egg_stats(self) -> dict:
|
||||
"""Get egg statistics from the page.
|
||||
|
||||
Returns dict with stats like eggs_per_day, cost_per_egg, etc.
|
||||
"""
|
||||
# This depends on how stats are displayed on the page
|
||||
# May need to parse text content from stats section
|
||||
return {}
|
||||
100
tests/e2e/pages/feed.py
Normal file
100
tests/e2e/pages/feed.py
Normal file
@@ -0,0 +1,100 @@
|
||||
# ABOUTME: Page object for feed management pages (purchase, give feed).
|
||||
# ABOUTME: Encapsulates navigation and form interactions for feed operations.
|
||||
|
||||
from playwright.sync_api import Page, expect
|
||||
|
||||
|
||||
class FeedPage:
|
||||
"""Page object for feed management pages."""
|
||||
|
||||
def __init__(self, page: Page, base_url: str):
|
||||
self.page = page
|
||||
self.base_url = base_url
|
||||
|
||||
def goto_feed_page(self):
|
||||
"""Navigate to the feed quick capture page."""
|
||||
self.page.goto(f"{self.base_url}/feed")
|
||||
expect(self.page.locator("body")).to_be_visible()
|
||||
|
||||
def purchase_feed(
|
||||
self,
|
||||
*,
|
||||
feed_type: str = "layer",
|
||||
bag_size_kg: int,
|
||||
bags_count: int,
|
||||
bag_price_euros: float,
|
||||
vendor: str = "",
|
||||
notes: str = "",
|
||||
):
|
||||
"""Fill and submit the feed purchase form.
|
||||
|
||||
Args:
|
||||
feed_type: Feed type code (e.g., "layer")
|
||||
bag_size_kg: Size of each bag in kg
|
||||
bags_count: Number of bags
|
||||
bag_price_euros: Price per bag in EUR
|
||||
vendor: Optional vendor name
|
||||
notes: Optional notes
|
||||
"""
|
||||
self.goto_feed_page()
|
||||
|
||||
# The purchase form uses specific IDs
|
||||
self.page.select_option("#purchase_feed_type_code", feed_type)
|
||||
self.page.fill("#bag_size_kg", str(bag_size_kg))
|
||||
self.page.fill("#bags_count", str(bags_count))
|
||||
self.page.fill("#bag_price_euros", str(bag_price_euros))
|
||||
|
||||
if vendor:
|
||||
self.page.fill("#vendor", vendor)
|
||||
|
||||
if notes:
|
||||
self.page.fill("#purchase_notes", notes)
|
||||
|
||||
# Submit the purchase form (second form on page)
|
||||
self.page.click('form[hx-post*="feed-purchased"] button[type="submit"]')
|
||||
|
||||
# Wait for HTMX response
|
||||
self.page.wait_for_load_state("networkidle")
|
||||
|
||||
def give_feed(
|
||||
self,
|
||||
*,
|
||||
location_name: str,
|
||||
feed_type: str = "layer",
|
||||
amount_kg: int,
|
||||
notes: str = "",
|
||||
):
|
||||
"""Fill and submit the feed given form.
|
||||
|
||||
Args:
|
||||
location_name: Human-readable location name (e.g., "Strip 1")
|
||||
feed_type: Feed type code (e.g., "layer")
|
||||
amount_kg: Amount of feed in kg
|
||||
notes: Optional notes
|
||||
"""
|
||||
self.goto_feed_page()
|
||||
|
||||
# The give form uses specific IDs
|
||||
self.page.select_option("#location_id", label=location_name)
|
||||
self.page.select_option("#feed_type_code", feed_type)
|
||||
self.page.fill("#amount_kg", str(amount_kg))
|
||||
|
||||
if notes:
|
||||
self.page.fill("#notes", notes)
|
||||
|
||||
# Submit the give form (first form on page)
|
||||
self.page.click('form[hx-post*="feed-given"] button[type="submit"]')
|
||||
|
||||
# Wait for HTMX response
|
||||
self.page.wait_for_load_state("networkidle")
|
||||
|
||||
def get_feed_inventory_balance(self, feed_type: str = "layer") -> dict:
|
||||
"""Get the current feed inventory from the page stats.
|
||||
|
||||
Returns dict with purchased_kg, given_kg, balance_kg if visible,
|
||||
or empty dict if stats not found.
|
||||
"""
|
||||
# This depends on how stats are displayed on the page
|
||||
# May need to parse text content from stats section
|
||||
# For now, return empty - can be enhanced based on actual UI
|
||||
return {}
|
||||
176
tests/e2e/pages/harvest.py
Normal file
176
tests/e2e/pages/harvest.py
Normal file
@@ -0,0 +1,176 @@
|
||||
# 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")
|
||||
134
tests/e2e/pages/move.py
Normal file
134
tests/e2e/pages/move.py
Normal file
@@ -0,0 +1,134 @@
|
||||
# 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()
|
||||
Reference in New Issue
Block a user