Updated README.md.
Added other TestableControls
This commit is contained in:
@@ -836,6 +836,452 @@ class TestableCheckbox(TestableControl):
|
||||
return self._send_value()
|
||||
|
||||
|
||||
class TestableTextarea(TestableControl):
|
||||
"""
|
||||
Represents a textarea element that can be interacted with in tests.
|
||||
|
||||
Textareas are similar to text inputs but support multi-line text.
|
||||
"""
|
||||
|
||||
def __init__(self, client, source):
|
||||
"""
|
||||
Initialize a testable textarea.
|
||||
|
||||
Args:
|
||||
client: The MyTestClient instance.
|
||||
source: The source HTML or BeautifulSoup Tag.
|
||||
"""
|
||||
# Parse as textarea element
|
||||
super().__init__(client, source, "textarea")
|
||||
|
||||
def send(self, value):
|
||||
"""
|
||||
Set the textarea value and trigger HTMX update if configured.
|
||||
|
||||
Args:
|
||||
value: The text value to set (can be multi-line string).
|
||||
|
||||
Returns:
|
||||
Response from HTMX request if applicable, None otherwise.
|
||||
"""
|
||||
self.fields[self.name] = value
|
||||
return self._send_value()
|
||||
|
||||
def append(self, text):
|
||||
"""
|
||||
Append text to the current textarea value.
|
||||
|
||||
Args:
|
||||
text: Text to append.
|
||||
|
||||
Returns:
|
||||
Response from HTMX request if applicable, None otherwise.
|
||||
"""
|
||||
current_value = self.fields.get(self.name, '')
|
||||
self.fields[self.name] = current_value + text
|
||||
return self._send_value()
|
||||
|
||||
def clear(self):
|
||||
"""
|
||||
Clear the textarea content.
|
||||
|
||||
Returns:
|
||||
Response from HTMX request if applicable, None otherwise.
|
||||
"""
|
||||
self.fields[self.name] = ''
|
||||
return self._send_value()
|
||||
|
||||
|
||||
class TestableSelect(TestableControl):
|
||||
"""
|
||||
Represents a select dropdown element that can be interacted with in tests.
|
||||
|
||||
Supports both single and multiple selection modes.
|
||||
"""
|
||||
|
||||
def __init__(self, client, source):
|
||||
"""
|
||||
Initialize a testable select element.
|
||||
|
||||
Args:
|
||||
client: The MyTestClient instance.
|
||||
source: The source HTML or BeautifulSoup Tag.
|
||||
"""
|
||||
# Parse as select element
|
||||
super().__init__(client, source, "select")
|
||||
self._is_multiple = self.my_ft.attrs.get('multiple') is not None
|
||||
|
||||
@property
|
||||
def is_multiple(self):
|
||||
"""Check if this is a multiple selection dropdown."""
|
||||
return self._is_multiple
|
||||
|
||||
@property
|
||||
def options(self):
|
||||
"""
|
||||
Get all available options for this select.
|
||||
|
||||
Returns:
|
||||
List of dicts with 'value' and 'text' keys.
|
||||
"""
|
||||
return self.select_fields.get(self.name, [])
|
||||
|
||||
def select(self, value):
|
||||
"""
|
||||
Select an option by value.
|
||||
|
||||
Args:
|
||||
value: The value of the option to select (not the text).
|
||||
|
||||
Returns:
|
||||
Response from HTMX request if applicable, None otherwise.
|
||||
|
||||
Raises:
|
||||
ValueError: If the value is not in the available options.
|
||||
"""
|
||||
# Validate the value exists in options
|
||||
available_values = [opt['value'] for opt in self.options]
|
||||
if value not in available_values:
|
||||
raise ValueError(
|
||||
f"Value '{value}' not found in select options. "
|
||||
f"Available values: {available_values}"
|
||||
)
|
||||
|
||||
if self.is_multiple:
|
||||
# For multiple select, value should be a list
|
||||
current = self.fields.get(self.name, [])
|
||||
if not isinstance(current, list):
|
||||
current = [current] if current else []
|
||||
if value not in current:
|
||||
current.append(value)
|
||||
self.fields[self.name] = current
|
||||
else:
|
||||
# For single select, just set the value
|
||||
self.fields[self.name] = value
|
||||
|
||||
return self._send_value()
|
||||
|
||||
def select_by_text(self, text):
|
||||
"""
|
||||
Select an option by its visible text.
|
||||
|
||||
Args:
|
||||
text: The visible text of the option to select.
|
||||
|
||||
Returns:
|
||||
Response from HTMX request if applicable, None otherwise.
|
||||
|
||||
Raises:
|
||||
ValueError: If the text is not found in options.
|
||||
"""
|
||||
# Find the value corresponding to this text
|
||||
for option in self.options:
|
||||
if option['text'] == text:
|
||||
return self.select(option['value'])
|
||||
|
||||
raise ValueError(
|
||||
f"Option with text '{text}' not found. "
|
||||
f"Available texts: {[opt['text'] for opt in self.options]}"
|
||||
)
|
||||
|
||||
def deselect(self, value):
|
||||
"""
|
||||
Deselect an option (only for multiple selects).
|
||||
|
||||
Args:
|
||||
value: The value of the option to deselect.
|
||||
|
||||
Returns:
|
||||
Response from HTMX request if applicable, None otherwise.
|
||||
|
||||
Raises:
|
||||
ValueError: If called on a non-multiple select.
|
||||
"""
|
||||
if not self.is_multiple:
|
||||
raise ValueError("Cannot deselect on a single-select dropdown")
|
||||
|
||||
current = self.fields.get(self.name, [])
|
||||
if not isinstance(current, list):
|
||||
current = [current] if current else []
|
||||
|
||||
if value in current:
|
||||
current.remove(value)
|
||||
self.fields[self.name] = current
|
||||
return self._send_value()
|
||||
|
||||
return None
|
||||
|
||||
|
||||
class TestableRange(TestableControl):
|
||||
"""
|
||||
Represents a range input (slider) that can be interacted with in tests.
|
||||
"""
|
||||
|
||||
def __init__(self, client, source):
|
||||
"""
|
||||
Initialize a testable range input.
|
||||
|
||||
Args:
|
||||
client: The MyTestClient instance.
|
||||
source: The source HTML or BeautifulSoup Tag.
|
||||
"""
|
||||
super().__init__(client, source, "input")
|
||||
|
||||
# Extract min, max, step from attributes
|
||||
self._min = float(self.my_ft.attrs.get('min', 0))
|
||||
self._max = float(self.my_ft.attrs.get('max', 100))
|
||||
self._step = float(self.my_ft.attrs.get('step', 1))
|
||||
|
||||
@property
|
||||
def min_value(self):
|
||||
"""Get the minimum value of the range."""
|
||||
return self._min
|
||||
|
||||
@property
|
||||
def max_value(self):
|
||||
"""Get the maximum value of the range."""
|
||||
return self._max
|
||||
|
||||
@property
|
||||
def step(self):
|
||||
"""Get the step increment of the range."""
|
||||
return self._step
|
||||
|
||||
def set(self, value):
|
||||
"""
|
||||
Set the range value.
|
||||
|
||||
Args:
|
||||
value: Numeric value to set (will be clamped to min/max).
|
||||
|
||||
Returns:
|
||||
Response from HTMX request if applicable, None otherwise.
|
||||
"""
|
||||
# Clamp value to valid range
|
||||
value = max(self._min, min(self._max, float(value)))
|
||||
|
||||
# Round to nearest step
|
||||
value = round((value - self._min) / self._step) * self._step + self._min
|
||||
|
||||
self.fields[self.name] = value
|
||||
return self._send_value()
|
||||
|
||||
def increase(self):
|
||||
"""
|
||||
Increase the range value by one step.
|
||||
|
||||
Returns:
|
||||
Response from HTMX request if applicable, None otherwise.
|
||||
"""
|
||||
current = float(self.fields.get(self.name, self._min))
|
||||
return self.set(current + self._step)
|
||||
|
||||
def decrease(self):
|
||||
"""
|
||||
Decrease the range value by one step.
|
||||
|
||||
Returns:
|
||||
Response from HTMX request if applicable, None otherwise.
|
||||
"""
|
||||
current = float(self.fields.get(self.name, self._min))
|
||||
return self.set(current - self._step)
|
||||
|
||||
|
||||
class TestableRadio(TestableControl):
|
||||
"""
|
||||
Represents a radio button input that can be interacted with in tests.
|
||||
|
||||
Note: Radio buttons with the same name form a group where only one
|
||||
can be selected at a time.
|
||||
"""
|
||||
|
||||
def __init__(self, client, source):
|
||||
"""
|
||||
Initialize a testable radio button.
|
||||
|
||||
Args:
|
||||
client: The MyTestClient instance.
|
||||
source: The source HTML or BeautifulSoup Tag.
|
||||
"""
|
||||
super().__init__(client, source, "input")
|
||||
self._radio_value = self.my_ft.attrs.get('value', '')
|
||||
|
||||
@property
|
||||
def radio_value(self):
|
||||
"""Get the value attribute of this radio button."""
|
||||
return self._radio_value
|
||||
|
||||
@property
|
||||
def is_checked(self):
|
||||
"""Check if this radio button is currently selected."""
|
||||
return self.fields.get(self.name) == self._radio_value
|
||||
|
||||
def select(self):
|
||||
"""
|
||||
Select this radio button.
|
||||
|
||||
Returns:
|
||||
Response from HTMX request if applicable, None otherwise.
|
||||
"""
|
||||
self.fields[self.name] = self._radio_value
|
||||
return self._send_value()
|
||||
|
||||
|
||||
class TestableButton(TestableElement):
|
||||
"""
|
||||
Represents a button element that can be clicked in tests.
|
||||
|
||||
Buttons can trigger HTMX requests or form submissions.
|
||||
"""
|
||||
|
||||
def __init__(self, client, source):
|
||||
"""
|
||||
Initialize a testable button.
|
||||
|
||||
Args:
|
||||
client: The MyTestClient instance.
|
||||
source: The source HTML or BeautifulSoup Tag.
|
||||
"""
|
||||
super().__init__(client, source, "button")
|
||||
|
||||
@property
|
||||
def text(self):
|
||||
"""Get the visible text of the button."""
|
||||
return self.element.get_text(strip=True)
|
||||
|
||||
def click(self):
|
||||
"""
|
||||
Click the button and trigger any associated HTMX request.
|
||||
|
||||
Returns:
|
||||
Response from HTMX request if applicable, None otherwise.
|
||||
"""
|
||||
if self._support_htmx():
|
||||
return self._send_htmx_request()
|
||||
return None
|
||||
|
||||
|
||||
class TestableDatalist(TestableControl):
|
||||
"""
|
||||
Represents an input with datalist (autocomplete/combobox) that can be
|
||||
interacted with in tests.
|
||||
|
||||
This is essentially an input that can show suggestions from a datalist.
|
||||
"""
|
||||
|
||||
def __init__(self, client, source):
|
||||
"""
|
||||
Initialize a testable input with datalist.
|
||||
|
||||
Args:
|
||||
client: The MyTestClient instance.
|
||||
source: The source HTML or BeautifulSoup Tag.
|
||||
"""
|
||||
super().__init__(client, source, "input")
|
||||
|
||||
# Find associated datalist
|
||||
list_id = self.my_ft.attrs.get('list')
|
||||
self._datalist_options = []
|
||||
|
||||
if list_id:
|
||||
# Parse the full HTML to find the datalist
|
||||
soup = BeautifulSoup(self.html_fragment, 'html.parser')
|
||||
datalist = soup.find('datalist', id=list_id)
|
||||
|
||||
if datalist:
|
||||
for option in datalist.find_all('option'):
|
||||
option_value = option.get('value', option.get_text(strip=True))
|
||||
self._datalist_options.append(option_value)
|
||||
|
||||
@property
|
||||
def suggestions(self):
|
||||
"""
|
||||
Get all available suggestions from the datalist.
|
||||
|
||||
Returns:
|
||||
List of suggestion values.
|
||||
"""
|
||||
return self._datalist_options
|
||||
|
||||
def send(self, value):
|
||||
"""
|
||||
Set the input value (can be any value, not restricted to suggestions).
|
||||
|
||||
Args:
|
||||
value: The value to set.
|
||||
|
||||
Returns:
|
||||
Response from HTMX request if applicable, None otherwise.
|
||||
"""
|
||||
self.fields[self.name] = value
|
||||
return self._send_value()
|
||||
|
||||
def select_suggestion(self, value):
|
||||
"""
|
||||
Select a value from the datalist suggestions.
|
||||
|
||||
Args:
|
||||
value: The suggestion value to select.
|
||||
|
||||
Returns:
|
||||
Response from HTMX request if applicable, None otherwise.
|
||||
|
||||
Raises:
|
||||
ValueError: If the value is not in the suggestions.
|
||||
"""
|
||||
if value not in self._datalist_options:
|
||||
raise ValueError(
|
||||
f"Value '{value}' not found in datalist suggestions. "
|
||||
f"Available: {self._datalist_options}"
|
||||
)
|
||||
|
||||
return self.send(value)
|
||||
|
||||
|
||||
# Update the TestableElement factory method
|
||||
# This should be added to the MyTestClient._testable_element_factory method
|
||||
|
||||
def _testable_element_factory_extended(client, elt):
|
||||
"""
|
||||
Extended factory method for creating appropriate Testable* instances.
|
||||
|
||||
This should replace or extend the existing _testable_element_factory method
|
||||
in MyTestClient.
|
||||
|
||||
Args:
|
||||
client: The MyTestClient instance.
|
||||
elt: BeautifulSoup Tag element.
|
||||
|
||||
Returns:
|
||||
Appropriate Testable* instance based on element type.
|
||||
"""
|
||||
if elt.name == "input":
|
||||
input_type = elt.get("type", "text").lower()
|
||||
|
||||
if input_type == "checkbox":
|
||||
return TestableCheckbox(client, elt)
|
||||
elif input_type == "radio":
|
||||
return TestableRadio(client, elt)
|
||||
elif input_type == "range":
|
||||
return TestableRange(client, elt)
|
||||
elif elt.get("list"): # Input with datalist
|
||||
return TestableDatalist(client, elt)
|
||||
else:
|
||||
return TestableInput(client, elt)
|
||||
|
||||
elif elt.name == "textarea":
|
||||
return TestableTextarea(client, elt)
|
||||
|
||||
elif elt.name == "select":
|
||||
return TestableSelect(client, elt)
|
||||
|
||||
elif elt.name == "button":
|
||||
return TestableButton(client, elt)
|
||||
|
||||
else:
|
||||
return TestableElement(client, elt, elt.name)
|
||||
|
||||
|
||||
# def get_value(tag):
|
||||
# """Return the current user-facing value of an HTML input-like element."""
|
||||
# if tag.name == 'input':
|
||||
@@ -1148,10 +1594,38 @@ class MyTestClient:
|
||||
return self
|
||||
|
||||
def _testable_element_factory(self, elt):
|
||||
"""
|
||||
Factory method for creating appropriate Testable* instances.
|
||||
|
||||
Args:
|
||||
elt: BeautifulSoup Tag element.
|
||||
|
||||
Returns:
|
||||
Appropriate Testable* instance based on element type.
|
||||
"""
|
||||
if elt.name == "input":
|
||||
if elt.get("type") == "checkbox":
|
||||
input_type = elt.get("type", "text").lower()
|
||||
|
||||
if input_type == "checkbox":
|
||||
return TestableCheckbox(self, elt)
|
||||
return TestableInput(self, elt)
|
||||
elif input_type == "radio":
|
||||
return TestableRadio(self, elt)
|
||||
elif input_type == "range":
|
||||
return TestableRange(self, elt)
|
||||
elif elt.get("list"): # Input with datalist
|
||||
return TestableDatalist(self, elt)
|
||||
else:
|
||||
return TestableInput(self, elt)
|
||||
|
||||
elif elt.name == "textarea":
|
||||
return TestableTextarea(self, elt)
|
||||
|
||||
elif elt.name == "select":
|
||||
return TestableSelect(self, elt)
|
||||
|
||||
elif elt.name == "button":
|
||||
return TestableButton(self, elt)
|
||||
|
||||
else:
|
||||
return TestableElement(self, elt, elt.name)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user