Adding test for LoginPage
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -22,6 +22,7 @@ tools.db
|
|||||||
.idea/sqldialects.xml
|
.idea/sqldialects.xml
|
||||||
.idea_bak
|
.idea_bak
|
||||||
**/*.prof
|
**/*.prof
|
||||||
|
**/*.db
|
||||||
|
|
||||||
# Created by .ignore support plugin (hsz.mobi)
|
# Created by .ignore support plugin (hsz.mobi)
|
||||||
### Python template
|
### Python template
|
||||||
|
|||||||
BIN
src/Users.db
BIN
src/Users.db
Binary file not shown.
@@ -20,7 +20,7 @@ daisyui_online_links = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
app, rt = fast_app(
|
app, rt = fast_app(
|
||||||
# before=beforeware,
|
before=beforeware,
|
||||||
hdrs=tuple(daisyui_offline_links)
|
hdrs=tuple(daisyui_offline_links)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -106,6 +106,15 @@ class RegisterPage:
|
|||||||
cls="btn w-full font-bold py-2 px-4 rounded"
|
cls="btn w-full font-bold py-2 px-4 rounded"
|
||||||
),
|
),
|
||||||
|
|
||||||
|
# Registration link
|
||||||
|
Div(
|
||||||
|
P(
|
||||||
|
"Already have an account? ",
|
||||||
|
A("Sign in here", href="/login", cls="text-blue-600 hover:underline"),
|
||||||
|
cls="text-sm text-gray-600 text-center"
|
||||||
|
)
|
||||||
|
),
|
||||||
|
|
||||||
action="register-p",
|
action="register-p",
|
||||||
method="post",
|
method="post",
|
||||||
cls="mb-6"
|
cls="mb-6"
|
||||||
|
|||||||
@@ -45,7 +45,6 @@ def setup_auth_routes(app, rt, mount_auth_app=True):
|
|||||||
"""
|
"""
|
||||||
return LoginPage(error_message=error)
|
return LoginPage(error_message=error)
|
||||||
|
|
||||||
|
|
||||||
@rt("/login-p")
|
@rt("/login-p")
|
||||||
def post(email: str, password: str, session, redirect_url: str = "/"):
|
def post(email: str, password: str, session, redirect_url: str = "/"):
|
||||||
"""
|
"""
|
||||||
@@ -99,6 +98,7 @@ def setup_auth_routes(app, rt, mount_auth_app=True):
|
|||||||
|
|
||||||
Args:
|
Args:
|
||||||
email: User email from form
|
email: User email from form
|
||||||
|
username: User name of the
|
||||||
password: User password from form
|
password: User password from form
|
||||||
confirm_password: Password confirmation from form
|
confirm_password: Password confirmation from form
|
||||||
session: FastHTML session object
|
session: FastHTML session object
|
||||||
@@ -107,12 +107,12 @@ def setup_auth_routes(app, rt, mount_auth_app=True):
|
|||||||
RegisterPage with success/error message via HTMX
|
RegisterPage with success/error message via HTMX
|
||||||
"""
|
"""
|
||||||
# Validate password confirmation
|
# Validate password confirmation
|
||||||
# if password != confirm_password:
|
if password != confirm_password:
|
||||||
# return RegisterPage(error_message="Passwords do not match. Please try again.")
|
return RegisterPage(error_message="Passwords do not match. Please try again.")
|
||||||
#
|
|
||||||
# # Validate password length
|
# Validate password length
|
||||||
# if len(password) < 8:
|
if len(password) < 8:
|
||||||
# return RegisterPage(error_message="Password must be at least 8 characters long.")
|
return RegisterPage(error_message="Password must be at least 8 characters long.")
|
||||||
|
|
||||||
# Attempt registration
|
# Attempt registration
|
||||||
result = register_user(email, username, password)
|
result = register_user(email, username, password)
|
||||||
|
|||||||
@@ -129,6 +129,307 @@ class TestableElement:
|
|||||||
|
|
||||||
# Send the request
|
# Send the request
|
||||||
return self.client.send_request(method, url, headers=headers, data=data, json_data=json_data)
|
return self.client.send_request(method, url, headers=headers, data=data, json_data=json_data)
|
||||||
|
|
||||||
|
def _support_htmx(self):
|
||||||
|
"""Check if the element supports HTMX."""
|
||||||
|
return self.ft.has_attr('hx_get') or self.ft.has_attr('hx_post')
|
||||||
|
|
||||||
|
|
||||||
|
class TestableForm(TestableElement):
|
||||||
|
"""
|
||||||
|
Represents an HTML form that can be filled and submitted in tests.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, client, source):
|
||||||
|
"""
|
||||||
|
Initialize a testable form.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
client: The MyTestClient instance.
|
||||||
|
source: The source HTML string containing a form.
|
||||||
|
"""
|
||||||
|
super().__init__(client, source)
|
||||||
|
self.form = BeautifulSoup(source, 'html.parser').find('form')
|
||||||
|
self.fields_mapping = {} # link between the input label and the input name
|
||||||
|
self.fields = {} # field name; field value
|
||||||
|
self.select_fields = {} # list of possible options for 'select' input fields
|
||||||
|
|
||||||
|
self._update_fields_mapping()
|
||||||
|
self.update_fields()
|
||||||
|
|
||||||
|
def update_fields(self):
|
||||||
|
"""
|
||||||
|
Update the fields dictionary with current form values and their proper types.
|
||||||
|
|
||||||
|
This method processes all input and select elements in the form:
|
||||||
|
- Determines the appropriate Python type (str, int, float, bool) based on
|
||||||
|
the HTML input type attribute and/or the value itself
|
||||||
|
- For select elements, populates self.select_fields with available options
|
||||||
|
- Stores the final typed values in self.fields
|
||||||
|
|
||||||
|
Type conversion priority:
|
||||||
|
1. HTML type attribute (checkbox → bool, number → int/float, etc.)
|
||||||
|
2. Value analysis fallback for ambiguous types (text/hidden/absent type)
|
||||||
|
"""
|
||||||
|
self.fields = {}
|
||||||
|
self.select_fields = {}
|
||||||
|
|
||||||
|
# Process input fields
|
||||||
|
for input_field in self.form.find_all('input'):
|
||||||
|
name = input_field.get('name')
|
||||||
|
if not name:
|
||||||
|
continue
|
||||||
|
|
||||||
|
input_type = input_field.get('type', 'text').lower()
|
||||||
|
raw_value = input_field.get('value', '')
|
||||||
|
|
||||||
|
# Type conversion based on input type
|
||||||
|
if input_type == 'checkbox':
|
||||||
|
# Checkbox: bool based on 'checked' attribute
|
||||||
|
self.fields[name] = input_field.has_attr('checked')
|
||||||
|
|
||||||
|
elif input_type == 'radio':
|
||||||
|
# Radio: str value (only if checked)
|
||||||
|
if input_field.has_attr('checked'):
|
||||||
|
self.fields[name] = raw_value
|
||||||
|
elif name not in self.fields:
|
||||||
|
# If no radio is checked yet, don't set a default
|
||||||
|
pass
|
||||||
|
|
||||||
|
elif input_type == 'number':
|
||||||
|
# Number: int or float based on value
|
||||||
|
self.fields[name] = self._convert_number(raw_value)
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Other types (text, hidden, email, password, etc.): analyze value
|
||||||
|
self.fields[name] = self._convert_value(raw_value)
|
||||||
|
|
||||||
|
# Process select fields
|
||||||
|
for select_field in self.form.find_all('select'):
|
||||||
|
name = select_field.get('name')
|
||||||
|
if not name:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Extract all options
|
||||||
|
options = []
|
||||||
|
selected_value = None
|
||||||
|
|
||||||
|
for option in select_field.find_all('option'):
|
||||||
|
option_value = option.get('value', option.get_text(strip=True))
|
||||||
|
option_text = option.get_text(strip=True)
|
||||||
|
|
||||||
|
options.append({
|
||||||
|
'value': option_value,
|
||||||
|
'text': option_text
|
||||||
|
})
|
||||||
|
|
||||||
|
# Track selected option
|
||||||
|
if option.has_attr('selected'):
|
||||||
|
selected_value = option_value
|
||||||
|
|
||||||
|
# Store options list
|
||||||
|
self.select_fields[name] = options
|
||||||
|
|
||||||
|
# Store selected value (or first option if none selected)
|
||||||
|
if selected_value is not None:
|
||||||
|
self.fields[name] = selected_value
|
||||||
|
elif options:
|
||||||
|
self.fields[name] = options[0]['value']
|
||||||
|
|
||||||
|
def fill_form(self, **kwargs):
|
||||||
|
"""
|
||||||
|
Fill the form with the given data.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
**kwargs: Field names and their values to fill in the form.
|
||||||
|
"""
|
||||||
|
for name, value in kwargs.items():
|
||||||
|
self.fields[name] = value
|
||||||
|
|
||||||
|
def submit(self):
|
||||||
|
"""
|
||||||
|
Submit the form.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The response from the form submission.
|
||||||
|
"""
|
||||||
|
return self._send_htmx_request(data=self.fields)
|
||||||
|
|
||||||
|
def _update_fields_mapping(self):
|
||||||
|
"""
|
||||||
|
Build a mapping between label text and input field names.
|
||||||
|
|
||||||
|
This method finds all labels in the form and associates them with their
|
||||||
|
corresponding input fields using the following priority order:
|
||||||
|
1. Explicit association via 'for' attribute matching input 'id'
|
||||||
|
2. Implicit association (label contains the input)
|
||||||
|
3. Parent-level association with 'for'/'id'
|
||||||
|
4. Proximity association (siblings in same parent)
|
||||||
|
5. No label (use input name as key)
|
||||||
|
|
||||||
|
The mapping is stored in self.fields_mapping as {label_text: input_name}.
|
||||||
|
For inputs without a name, the id is used. If neither exists, a generic
|
||||||
|
key like "unnamed_0" is generated.
|
||||||
|
"""
|
||||||
|
self.fields_mapping = {}
|
||||||
|
processed_inputs = set()
|
||||||
|
unnamed_counter = 0
|
||||||
|
|
||||||
|
# Get all inputs in the form
|
||||||
|
all_inputs = self.form.find_all('input')
|
||||||
|
|
||||||
|
# Priority 1 & 2: Explicit association (for/id) and implicit (nested)
|
||||||
|
for label in self.form.find_all('label'):
|
||||||
|
label_text = label.get_text(strip=True)
|
||||||
|
|
||||||
|
# Check for explicit association via 'for' attribute
|
||||||
|
label_for = label.get('for')
|
||||||
|
if label_for:
|
||||||
|
input_field = self.form.find('input', id=label_for)
|
||||||
|
if input_field:
|
||||||
|
input_name = self._get_input_identifier(input_field, unnamed_counter)
|
||||||
|
if input_name.startswith('unnamed_'):
|
||||||
|
unnamed_counter += 1
|
||||||
|
self.fields_mapping[label_text] = input_name
|
||||||
|
processed_inputs.add(id(input_field))
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Check for implicit association (label contains input)
|
||||||
|
input_field = label.find('input')
|
||||||
|
if input_field:
|
||||||
|
input_name = self._get_input_identifier(input_field, unnamed_counter)
|
||||||
|
if input_name.startswith('unnamed_'):
|
||||||
|
unnamed_counter += 1
|
||||||
|
self.fields_mapping[label_text] = input_name
|
||||||
|
processed_inputs.add(id(input_field))
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Priority 3 & 4: Parent-level associations
|
||||||
|
for label in self.form.find_all('label'):
|
||||||
|
label_text = label.get_text(strip=True)
|
||||||
|
|
||||||
|
# Skip if this label was already processed
|
||||||
|
if label_text in self.fields_mapping:
|
||||||
|
continue
|
||||||
|
|
||||||
|
parent = label.parent
|
||||||
|
if parent:
|
||||||
|
input_found = False
|
||||||
|
|
||||||
|
# Priority 3: Look for sibling input with matching for/id
|
||||||
|
label_for = label.get('for')
|
||||||
|
if label_for:
|
||||||
|
for sibling in parent.find_all('input'):
|
||||||
|
if sibling.get('id') == label_for and id(sibling) not in processed_inputs:
|
||||||
|
input_name = self._get_input_identifier(sibling, unnamed_counter)
|
||||||
|
if input_name.startswith('unnamed_'):
|
||||||
|
unnamed_counter += 1
|
||||||
|
self.fields_mapping[label_text] = input_name
|
||||||
|
processed_inputs.add(id(sibling))
|
||||||
|
input_found = True
|
||||||
|
break
|
||||||
|
|
||||||
|
# Priority 4: Fallback to proximity if no input found yet
|
||||||
|
if not input_found:
|
||||||
|
for sibling in parent.find_all('input'):
|
||||||
|
if id(sibling) not in processed_inputs:
|
||||||
|
input_name = self._get_input_identifier(sibling, unnamed_counter)
|
||||||
|
if input_name.startswith('unnamed_'):
|
||||||
|
unnamed_counter += 1
|
||||||
|
self.fields_mapping[label_text] = input_name
|
||||||
|
processed_inputs.add(id(sibling))
|
||||||
|
break
|
||||||
|
|
||||||
|
# Priority 5: Inputs without labels
|
||||||
|
for input_field in all_inputs:
|
||||||
|
if id(input_field) not in processed_inputs:
|
||||||
|
input_name = self._get_input_identifier(input_field, unnamed_counter)
|
||||||
|
if input_name.startswith('unnamed_'):
|
||||||
|
unnamed_counter += 1
|
||||||
|
self.fields_mapping[input_name] = input_name
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _get_input_identifier(input_field, counter):
|
||||||
|
"""
|
||||||
|
Get the identifier for an input field.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
input_field: The BeautifulSoup Tag object representing the input.
|
||||||
|
counter: Current counter for unnamed inputs.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The input name, id, or a generated "unnamed_X" identifier.
|
||||||
|
"""
|
||||||
|
if input_field.get('name'):
|
||||||
|
return input_field['name']
|
||||||
|
elif input_field.get('id'):
|
||||||
|
return input_field['id']
|
||||||
|
else:
|
||||||
|
return f"unnamed_{counter}"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _convert_number(value):
|
||||||
|
"""
|
||||||
|
Convert a string value to int or float.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
value: String value to convert.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
int, float, or empty string if conversion fails.
|
||||||
|
"""
|
||||||
|
if not value or value.strip() == '':
|
||||||
|
return ''
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Try float first to detect decimal numbers
|
||||||
|
if '.' in value or 'e' in value.lower():
|
||||||
|
return float(value)
|
||||||
|
else:
|
||||||
|
return int(value)
|
||||||
|
except ValueError:
|
||||||
|
return value
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _convert_value(value):
|
||||||
|
"""
|
||||||
|
Analyze and convert a value to its appropriate type.
|
||||||
|
|
||||||
|
Conversion priority:
|
||||||
|
1. Boolean keywords (true/false)
|
||||||
|
2. Float (contains decimal point)
|
||||||
|
3. Int (numeric)
|
||||||
|
4. Empty string
|
||||||
|
5. String (default)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
value: String value to convert.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Converted value with appropriate type (bool, float, int, or str).
|
||||||
|
"""
|
||||||
|
if not value or value.strip() == '':
|
||||||
|
return ''
|
||||||
|
|
||||||
|
value_lower = value.lower().strip()
|
||||||
|
|
||||||
|
# Check for boolean
|
||||||
|
if value_lower in ('true', 'false'):
|
||||||
|
return value_lower == 'true'
|
||||||
|
|
||||||
|
# Check for numeric values
|
||||||
|
try:
|
||||||
|
# Check for float (has decimal point or scientific notation)
|
||||||
|
if '.' in value or 'e' in value_lower:
|
||||||
|
return float(value)
|
||||||
|
# Try int
|
||||||
|
else:
|
||||||
|
return int(value)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Default to string
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
class MyTestClient:
|
class MyTestClient:
|
||||||
@@ -320,6 +621,33 @@ class MyTestClient:
|
|||||||
f"Found {len(results)} elements matching selector '{selector}'. Expected exactly 1."
|
f"Found {len(results)} elements matching selector '{selector}'. Expected exactly 1."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def find_form(self, fields: list = None):
|
||||||
|
"""
|
||||||
|
Find a form element in the page content.
|
||||||
|
Can provide title of the fields to ease the search
|
||||||
|
:param fields:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
if self._content is None:
|
||||||
|
raise ValueError(
|
||||||
|
"No page content available. Call open() before find_element()."
|
||||||
|
)
|
||||||
|
|
||||||
|
results = self._soup.select("form")
|
||||||
|
if len(results) == 0:
|
||||||
|
raise AssertionError(
|
||||||
|
f"No element form found."
|
||||||
|
)
|
||||||
|
|
||||||
|
# result = _filter(results, fields)
|
||||||
|
|
||||||
|
if len(results) == 1:
|
||||||
|
return TestableForm(self, results[0])
|
||||||
|
else:
|
||||||
|
raise AssertionError(
|
||||||
|
f"Found {len(results)} forms (with the specified fields). Expected exactly 1."
|
||||||
|
)
|
||||||
|
|
||||||
def get_content(self) -> str:
|
def get_content(self) -> str:
|
||||||
"""
|
"""
|
||||||
Get the raw HTML content of the last opened page.
|
Get the raw HTML content of the last opened page.
|
||||||
|
|||||||
0
tests/auth/__init__.py
Normal file
0
tests/auth/__init__.py
Normal file
39
tests/auth/test_login.py
Normal file
39
tests/auth/test_login.py
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import pytest
|
||||||
|
from fasthtml.fastapp import fast_app
|
||||||
|
|
||||||
|
from myfasthtml.auth.routes import setup_auth_routes
|
||||||
|
from myfasthtml.auth.utils import create_auth_beforeware
|
||||||
|
from myfasthtml.core.testclient import MyTestClient
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def app():
|
||||||
|
beforeware = create_auth_beforeware()
|
||||||
|
test_app, test_rt = fast_app(before=beforeware)
|
||||||
|
setup_auth_routes(test_app, test_rt)
|
||||||
|
return test_app
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def rt(app):
|
||||||
|
return app.route
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def user(app):
|
||||||
|
user = MyTestClient(app)
|
||||||
|
return user
|
||||||
|
|
||||||
|
def test_i_can_see_login_page(user):
|
||||||
|
user.open("/login")
|
||||||
|
user.should_see("Sign In")
|
||||||
|
user.should_see("Register here")
|
||||||
|
|
||||||
|
def test_i_cannot_login_with_wrong_credentials(user):
|
||||||
|
user.open("/login")
|
||||||
|
user.fill_form({
|
||||||
|
"username": "wrong",
|
||||||
|
"password": ""
|
||||||
|
})
|
||||||
|
user.click("button")
|
||||||
|
user.should_see("Invalid credentials")
|
||||||
0
tests/auth/test_register.py
Normal file
0
tests/auth/test_register.py
Normal file
33
tests/auth/test_utils.py
Normal file
33
tests/auth/test_utils.py
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import pytest
|
||||||
|
from fasthtml.fastapp import fast_app
|
||||||
|
|
||||||
|
from myfasthtml.auth.utils import create_auth_beforeware
|
||||||
|
from myfasthtml.core.testclient import MyTestClient
|
||||||
|
|
||||||
|
def test_non_protected_route():
|
||||||
|
app, rt = fast_app()
|
||||||
|
user = MyTestClient(app)
|
||||||
|
|
||||||
|
@rt('/')
|
||||||
|
def index(): return "Welcome"
|
||||||
|
|
||||||
|
@rt('/login')
|
||||||
|
def index(): return "Sign In"
|
||||||
|
|
||||||
|
user.open("/")
|
||||||
|
user.should_see("Welcome")
|
||||||
|
|
||||||
|
|
||||||
|
def test_all_routes_are_protected():
|
||||||
|
beforeware = create_auth_beforeware()
|
||||||
|
app, rt = fast_app(before=beforeware)
|
||||||
|
user = MyTestClient(app)
|
||||||
|
|
||||||
|
@rt('/')
|
||||||
|
def index(): return "Welcome"
|
||||||
|
|
||||||
|
@rt('/login')
|
||||||
|
def index(): return "Sign In"
|
||||||
|
|
||||||
|
user.open("/")
|
||||||
|
user.should_see("Sign In")
|
||||||
0
tests/testclient/__init__.py
Normal file
0
tests/testclient/__init__.py
Normal file
585
tests/testclient/test_testable_form.py
Normal file
585
tests/testclient/test_testable_form.py
Normal file
@@ -0,0 +1,585 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
from myfasthtml.core.testclient import TestableForm
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_client():
|
||||||
|
"""Mock client for testing purposes."""
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class TestableFormUpdateFieldMapping:
|
||||||
|
def test_i_can_map_label_with_explicit_for_attribute(self, mock_client):
|
||||||
|
"""
|
||||||
|
Test that labels with explicit 'for' attribute are correctly mapped.
|
||||||
|
|
||||||
|
This is the most reliable association method (priority 1).
|
||||||
|
"""
|
||||||
|
html = '<form><label for="uid">Username</label><input id="uid" name="username" /></form>'
|
||||||
|
form = TestableForm(mock_client, html)
|
||||||
|
|
||||||
|
assert form.fields_mapping == {"Username": "username"}
|
||||||
|
|
||||||
|
def test_i_can_map_label_containing_input(self, mock_client):
|
||||||
|
"""
|
||||||
|
Test that labels containing inputs are correctly mapped.
|
||||||
|
|
||||||
|
This tests implicit association by nesting (priority 2).
|
||||||
|
"""
|
||||||
|
html = '<form><label>Username<input name="username" /></label></form>'
|
||||||
|
form = TestableForm(mock_client, html)
|
||||||
|
|
||||||
|
assert form.fields_mapping == {"Username": "username"}
|
||||||
|
|
||||||
|
def test_i_can_map_label_and_input_as_siblings_with_for_id(self, mock_client):
|
||||||
|
"""
|
||||||
|
Test that sibling labels and inputs with for/id are correctly mapped.
|
||||||
|
|
||||||
|
This tests parent-level association with explicit for/id (priority 3).
|
||||||
|
"""
|
||||||
|
html = '<form><div><label for="uid">Username</label><input id="uid" name="username" /></div></form>'
|
||||||
|
form = TestableForm(mock_client, html)
|
||||||
|
|
||||||
|
assert form.fields_mapping == {"Username": "username"}
|
||||||
|
|
||||||
|
def test_i_can_map_label_and_input_as_siblings_by_proximity(self, mock_client):
|
||||||
|
"""
|
||||||
|
Test that sibling labels and inputs are mapped by proximity.
|
||||||
|
|
||||||
|
This tests association by proximity without for/id (priority 4).
|
||||||
|
"""
|
||||||
|
html = '<form><div><label>Username</label><input name="username" /></div></form>'
|
||||||
|
form = TestableForm(mock_client, html)
|
||||||
|
|
||||||
|
assert form.fields_mapping == {"Username": "username"}
|
||||||
|
|
||||||
|
def test_i_can_map_input_without_label_using_name(self, mock_client):
|
||||||
|
"""
|
||||||
|
Test that inputs without labels use their name attribute as key.
|
||||||
|
|
||||||
|
This tests the fallback mechanism (priority 5).
|
||||||
|
"""
|
||||||
|
html = '<form><input name="csrf_token" /></form>'
|
||||||
|
form = TestableForm(mock_client, html)
|
||||||
|
|
||||||
|
assert form.fields_mapping == {"csrf_token": "csrf_token"}
|
||||||
|
|
||||||
|
def test_i_can_map_input_without_name_using_id(self, mock_client):
|
||||||
|
"""
|
||||||
|
Test that inputs without name attribute fallback to id attribute.
|
||||||
|
|
||||||
|
This ensures inputs without name can still be identified.
|
||||||
|
"""
|
||||||
|
html = '<form><input id="submit_btn" /></form>'
|
||||||
|
form = TestableForm(mock_client, html)
|
||||||
|
|
||||||
|
assert form.fields_mapping == {"submit_btn": "submit_btn"}
|
||||||
|
|
||||||
|
def test_i_can_map_input_without_name_and_id_using_unnamed(self, mock_client):
|
||||||
|
"""
|
||||||
|
Test that inputs without name or id get a generated unnamed key.
|
||||||
|
|
||||||
|
This ensures all inputs are tracked even without identifiers.
|
||||||
|
"""
|
||||||
|
html = '<form><input type="submit" /></form>'
|
||||||
|
form = TestableForm(mock_client, html)
|
||||||
|
|
||||||
|
assert form.fields_mapping == {"unnamed_0": "unnamed_0"}
|
||||||
|
|
||||||
|
def test_i_can_handle_multiple_unnamed_inputs(self, mock_client):
|
||||||
|
"""
|
||||||
|
Test that multiple unnamed inputs get incrementing counters.
|
||||||
|
|
||||||
|
This ensures each unnamed input has a unique identifier.
|
||||||
|
"""
|
||||||
|
html = '<form><input type="submit" /><input type="button" /></form>'
|
||||||
|
form = TestableForm(mock_client, html)
|
||||||
|
|
||||||
|
assert form.fields_mapping == {"unnamed_0": "unnamed_0", "unnamed_1": "unnamed_1"}
|
||||||
|
|
||||||
|
def test_i_can_strip_whitespace_from_label_text(self, mock_client):
|
||||||
|
"""
|
||||||
|
Test that whitespace and newlines are stripped from label text.
|
||||||
|
|
||||||
|
This ensures clean, consistent label keys in the mapping.
|
||||||
|
"""
|
||||||
|
html = '<form><label for="uid"> Username \n</label><input id="uid" name="username" /></form>'
|
||||||
|
form = TestableForm(mock_client, html)
|
||||||
|
|
||||||
|
assert form.fields_mapping == {"Username": "username"}
|
||||||
|
|
||||||
|
def test_i_can_extract_text_from_complex_labels(self, mock_client):
|
||||||
|
"""
|
||||||
|
Test that text from nested elements in labels is extracted.
|
||||||
|
|
||||||
|
This ensures labels with spans, emphasis, etc. are handled correctly.
|
||||||
|
"""
|
||||||
|
html = '<form><label for="uid">Username <span class="required">*</span></label><input id="uid" name="username" /></form>'
|
||||||
|
form = TestableForm(mock_client, html)
|
||||||
|
|
||||||
|
assert form.fields_mapping == {"Username*": "username"}
|
||||||
|
|
||||||
|
def test_i_can_handle_mixed_scenarios_in_same_form(self, mock_client):
|
||||||
|
"""
|
||||||
|
Test that all association priorities work together in one form.
|
||||||
|
|
||||||
|
This is a comprehensive test ensuring the priority system works correctly.
|
||||||
|
"""
|
||||||
|
html = '''
|
||||||
|
<form>
|
||||||
|
<label for="email">Email</label>
|
||||||
|
<input id="email" name="email" />
|
||||||
|
|
||||||
|
<label>Password<input name="password" /></label>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="phone">Phone</label>
|
||||||
|
<input id="phone" name="phone" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label>Address</label>
|
||||||
|
<input name="address" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input name="csrf_token" />
|
||||||
|
<input id="submit_btn" />
|
||||||
|
<input type="hidden" />
|
||||||
|
</form>
|
||||||
|
'''
|
||||||
|
form = TestableForm(mock_client, html)
|
||||||
|
|
||||||
|
expected = {
|
||||||
|
"Email": "email",
|
||||||
|
"Password": "password",
|
||||||
|
"Phone": "phone",
|
||||||
|
"Address": "address",
|
||||||
|
"csrf_token": "csrf_token",
|
||||||
|
"submit_btn": "submit_btn",
|
||||||
|
"unnamed_0": "unnamed_0"
|
||||||
|
}
|
||||||
|
|
||||||
|
assert form.fields_mapping == expected
|
||||||
|
|
||||||
|
def test_i_can_handle_empty_form(self, mock_client):
|
||||||
|
"""
|
||||||
|
Test that an empty form doesn't cause errors.
|
||||||
|
|
||||||
|
This ensures robustness when dealing with minimal forms.
|
||||||
|
"""
|
||||||
|
html = '<form></form>'
|
||||||
|
form = TestableForm(mock_client, html)
|
||||||
|
|
||||||
|
assert form.fields_mapping == {}
|
||||||
|
|
||||||
|
def test_i_can_handle_form_with_only_labels(self, mock_client):
|
||||||
|
"""
|
||||||
|
Test that labels without associated inputs don't cause errors.
|
||||||
|
|
||||||
|
This ensures the code handles malformed or incomplete forms gracefully.
|
||||||
|
"""
|
||||||
|
html = '<form><label>Test</label></form>'
|
||||||
|
form = TestableForm(mock_client, html)
|
||||||
|
|
||||||
|
assert form.fields_mapping == {}
|
||||||
|
|
||||||
|
def test_i_can_handle_label_with_invalid_for_attribute(self, mock_client):
|
||||||
|
"""
|
||||||
|
Test that labels with invalid 'for' attributes fallback correctly.
|
||||||
|
|
||||||
|
This ensures the priority system cascades properly when higher
|
||||||
|
priorities fail to find a match.
|
||||||
|
"""
|
||||||
|
html = '<form><div><label for="nonexistent">Test</label><input name="field" /></div></form>'
|
||||||
|
form = TestableForm(mock_client, html)
|
||||||
|
|
||||||
|
assert form.fields_mapping == {"Test": "field"}
|
||||||
|
|
||||||
|
|
||||||
|
class TestableFormUpdateFieldValues:
|
||||||
|
def test_i_can_handle_checkbox_checked(self, mock_client):
|
||||||
|
"""
|
||||||
|
Test that a checked checkbox is converted to True.
|
||||||
|
|
||||||
|
This ensures proper boolean handling for checked checkboxes.
|
||||||
|
"""
|
||||||
|
html = '<form><input type="checkbox" name="agree" checked /></form>'
|
||||||
|
form = TestableForm(mock_client, html)
|
||||||
|
|
||||||
|
assert form.fields == {"agree": True}, \
|
||||||
|
f"Expected {{'agree': True}}, got {form.fields}"
|
||||||
|
|
||||||
|
def test_i_can_handle_checkbox_unchecked(self, mock_client):
|
||||||
|
"""
|
||||||
|
Test that an unchecked checkbox is converted to False.
|
||||||
|
|
||||||
|
This ensures proper boolean handling for unchecked checkboxes.
|
||||||
|
"""
|
||||||
|
html = '<form><input type="checkbox" name="agree" /></form>'
|
||||||
|
form = TestableForm(mock_client, html)
|
||||||
|
|
||||||
|
assert form.fields == {"agree": False}, \
|
||||||
|
f"Expected {{'agree': False}}, got {form.fields}"
|
||||||
|
|
||||||
|
def test_i_can_handle_radio_button_checked(self, mock_client):
|
||||||
|
"""
|
||||||
|
Test that a checked radio button returns its value as string.
|
||||||
|
|
||||||
|
This ensures radio buttons store their value attribute.
|
||||||
|
"""
|
||||||
|
html = '<form><input type="radio" name="size" value="large" checked /></form>'
|
||||||
|
form = TestableForm(mock_client, html)
|
||||||
|
|
||||||
|
assert form.fields == {"size": "large"}, \
|
||||||
|
f"Expected {{'size': 'large'}}, got {form.fields}"
|
||||||
|
|
||||||
|
def test_i_can_handle_multiple_radio_buttons_with_one_checked(self, mock_client):
|
||||||
|
"""
|
||||||
|
Test that only the checked radio button value is returned.
|
||||||
|
|
||||||
|
This ensures correct handling of radio button groups.
|
||||||
|
"""
|
||||||
|
html = '''
|
||||||
|
<form>
|
||||||
|
<input type="radio" name="size" value="small" />
|
||||||
|
<input type="radio" name="size" value="medium" checked />
|
||||||
|
<input type="radio" name="size" value="large" />
|
||||||
|
</form>
|
||||||
|
'''
|
||||||
|
form = TestableForm(mock_client, html)
|
||||||
|
|
||||||
|
assert form.fields == {"size": "medium"}, \
|
||||||
|
f"Expected {{'size': 'medium'}}, got {form.fields}"
|
||||||
|
|
||||||
|
def test_i_can_handle_radio_buttons_with_none_checked(self, mock_client):
|
||||||
|
"""
|
||||||
|
Test that no value is set when no radio button is checked.
|
||||||
|
|
||||||
|
This ensures proper handling of unchecked radio button groups.
|
||||||
|
"""
|
||||||
|
html = '''
|
||||||
|
<form>
|
||||||
|
<input type="radio" name="size" value="small" />
|
||||||
|
<input type="radio" name="size" value="medium" />
|
||||||
|
<input type="radio" name="size" value="large" />
|
||||||
|
</form>
|
||||||
|
'''
|
||||||
|
form = TestableForm(mock_client, html)
|
||||||
|
|
||||||
|
assert "size" not in form.fields, \
|
||||||
|
f"Expected 'size' not in fields, got {form.fields}"
|
||||||
|
|
||||||
|
def test_i_can_handle_number_input_with_integer(self, mock_client):
|
||||||
|
"""
|
||||||
|
Test that a number input with integer value becomes int.
|
||||||
|
|
||||||
|
This ensures proper type conversion for integer numbers.
|
||||||
|
"""
|
||||||
|
html = '<form><input type="number" name="age" value="25" /></form>'
|
||||||
|
form = TestableForm(mock_client, html)
|
||||||
|
|
||||||
|
assert form.fields == {"age": 25}, \
|
||||||
|
f"Expected {{'age': 25}}, got {form.fields}"
|
||||||
|
assert isinstance(form.fields["age"], int), \
|
||||||
|
f"Expected int type, got {type(form.fields['age'])}"
|
||||||
|
|
||||||
|
def test_i_can_handle_number_input_with_float(self, mock_client):
|
||||||
|
"""
|
||||||
|
Test that a number input with decimal value becomes float.
|
||||||
|
|
||||||
|
This ensures proper type conversion for floating point numbers.
|
||||||
|
"""
|
||||||
|
html = '<form><input type="number" name="price" value="19.99" /></form>'
|
||||||
|
form = TestableForm(mock_client, html)
|
||||||
|
|
||||||
|
assert form.fields == {"price": 19.99}, \
|
||||||
|
f"Expected {{'price': 19.99}}, got {form.fields}"
|
||||||
|
assert isinstance(form.fields["price"], float), \
|
||||||
|
f"Expected float type, got {type(form.fields['price'])}"
|
||||||
|
|
||||||
|
def test_i_can_handle_text_input_with_string_value(self, mock_client):
|
||||||
|
"""
|
||||||
|
Test that a text input with string value remains str.
|
||||||
|
|
||||||
|
This ensures text values are not converted unnecessarily.
|
||||||
|
"""
|
||||||
|
html = '<form><input type="text" name="username" value="john_doe" /></form>'
|
||||||
|
form = TestableForm(mock_client, html)
|
||||||
|
|
||||||
|
assert form.fields == {"username": "john_doe"}, \
|
||||||
|
f"Expected {{'username': 'john_doe'}}, got {form.fields}"
|
||||||
|
assert isinstance(form.fields["username"], str), \
|
||||||
|
f"Expected str type, got {type(form.fields['username'])}"
|
||||||
|
|
||||||
|
def test_i_can_handle_text_input_with_integer_value(self, mock_client):
|
||||||
|
"""
|
||||||
|
Test that a text input with numeric value is converted to int.
|
||||||
|
|
||||||
|
This ensures automatic type detection for text inputs.
|
||||||
|
"""
|
||||||
|
html = '<form><input type="text" name="code" value="123" /></form>'
|
||||||
|
form = TestableForm(mock_client, html)
|
||||||
|
|
||||||
|
assert form.fields == {"code": 123}, \
|
||||||
|
f"Expected {{'code': 123}}, got {form.fields}"
|
||||||
|
assert isinstance(form.fields["code"], int), \
|
||||||
|
f"Expected int type, got {type(form.fields['code'])}"
|
||||||
|
|
||||||
|
def test_i_can_handle_text_input_with_float_value(self, mock_client):
|
||||||
|
"""
|
||||||
|
Test that a text input with decimal value is converted to float.
|
||||||
|
|
||||||
|
This ensures automatic float detection for text inputs.
|
||||||
|
"""
|
||||||
|
html = '<form><input type="text" name="rate" value="3.14" /></form>'
|
||||||
|
form = TestableForm(mock_client, html)
|
||||||
|
|
||||||
|
assert form.fields == {"rate": 3.14}, \
|
||||||
|
f"Expected {{'rate': 3.14}}, got {form.fields}"
|
||||||
|
assert isinstance(form.fields["rate"], float), \
|
||||||
|
f"Expected float type, got {type(form.fields['rate'])}"
|
||||||
|
|
||||||
|
def test_i_can_handle_text_input_with_boolean_true(self, mock_client):
|
||||||
|
"""
|
||||||
|
Test that a text input with 'true' is converted to bool.
|
||||||
|
|
||||||
|
This ensures boolean keyword detection for text inputs.
|
||||||
|
"""
|
||||||
|
html = '<form><input type="text" name="flag" value="true" /></form>'
|
||||||
|
form = TestableForm(mock_client, html)
|
||||||
|
|
||||||
|
assert form.fields == {"flag": True}, \
|
||||||
|
f"Expected {{'flag': True}}, got {form.fields}"
|
||||||
|
assert isinstance(form.fields["flag"], bool), \
|
||||||
|
f"Expected bool type, got {type(form.fields['flag'])}"
|
||||||
|
|
||||||
|
def test_i_can_handle_text_input_with_boolean_false(self, mock_client):
|
||||||
|
"""
|
||||||
|
Test that a text input with 'false' is converted to bool.
|
||||||
|
|
||||||
|
This ensures boolean keyword detection for false values.
|
||||||
|
"""
|
||||||
|
html = '<form><input type="text" name="flag" value="false" /></form>'
|
||||||
|
form = TestableForm(mock_client, html)
|
||||||
|
|
||||||
|
assert form.fields == {"flag": False}, \
|
||||||
|
f"Expected {{'flag': False}}, got {form.fields}"
|
||||||
|
assert isinstance(form.fields["flag"], bool), \
|
||||||
|
f"Expected bool type, got {type(form.fields['flag'])}"
|
||||||
|
|
||||||
|
def test_i_can_handle_hidden_input_with_auto_conversion(self, mock_client):
|
||||||
|
"""
|
||||||
|
Test that hidden inputs benefit from automatic type conversion.
|
||||||
|
|
||||||
|
This ensures hidden fields are processed like text fields.
|
||||||
|
"""
|
||||||
|
html = '<form><input type="hidden" name="id" value="42" /></form>'
|
||||||
|
form = TestableForm(mock_client, html)
|
||||||
|
|
||||||
|
assert form.fields == {"id": 42}, \
|
||||||
|
f"Expected {{'id': 42}}, got {form.fields}"
|
||||||
|
assert isinstance(form.fields["id"], int), \
|
||||||
|
f"Expected int type, got {type(form.fields['id'])}"
|
||||||
|
|
||||||
|
def test_i_can_handle_empty_input_value(self, mock_client):
|
||||||
|
"""
|
||||||
|
Test that an empty input value remains an empty string.
|
||||||
|
|
||||||
|
This ensures empty values are not converted to None or other types.
|
||||||
|
"""
|
||||||
|
html = '<form><input type="text" name="optional" value="" /></form>'
|
||||||
|
form = TestableForm(mock_client, html)
|
||||||
|
|
||||||
|
assert form.fields == {"optional": ""}, \
|
||||||
|
f"Expected {{'optional': ''}}, got {form.fields}"
|
||||||
|
assert isinstance(form.fields["optional"], str), \
|
||||||
|
f"Expected str type, got {type(form.fields['optional'])}"
|
||||||
|
|
||||||
|
def test_i_can_extract_select_options(self, mock_client):
|
||||||
|
"""
|
||||||
|
Test that select options are correctly extracted.
|
||||||
|
|
||||||
|
This ensures proper population of select_fields dictionary.
|
||||||
|
"""
|
||||||
|
html = '''
|
||||||
|
<form>
|
||||||
|
<select name="country">
|
||||||
|
<option value="FR">France</option>
|
||||||
|
<option value="US">USA</option>
|
||||||
|
</select>
|
||||||
|
</form>
|
||||||
|
'''
|
||||||
|
form = TestableForm(mock_client, html)
|
||||||
|
|
||||||
|
expected_options = [
|
||||||
|
{"value": "FR", "text": "France"},
|
||||||
|
{"value": "US", "text": "USA"}
|
||||||
|
]
|
||||||
|
|
||||||
|
assert form.select_fields == {"country": expected_options}, \
|
||||||
|
f"Expected {{'country': {expected_options}}}, got {form.select_fields}"
|
||||||
|
|
||||||
|
def test_i_can_handle_select_with_selected_option(self, mock_client):
|
||||||
|
"""
|
||||||
|
Test that the selected option is stored in fields.
|
||||||
|
|
||||||
|
This ensures proper detection of selected options.
|
||||||
|
"""
|
||||||
|
html = '''
|
||||||
|
<form>
|
||||||
|
<select name="country">
|
||||||
|
<option value="FR">France</option>
|
||||||
|
<option value="US" selected>USA</option>
|
||||||
|
</select>
|
||||||
|
</form>
|
||||||
|
'''
|
||||||
|
form = TestableForm(mock_client, html)
|
||||||
|
|
||||||
|
assert form.fields == {"country": "US"}, \
|
||||||
|
f"Expected {{'country': 'US'}}, got {form.fields}"
|
||||||
|
|
||||||
|
def test_i_can_handle_select_without_selected_option(self, mock_client):
|
||||||
|
"""
|
||||||
|
Test that the first option is used by default.
|
||||||
|
|
||||||
|
This ensures proper default value handling for select fields.
|
||||||
|
"""
|
||||||
|
html = '''
|
||||||
|
<form>
|
||||||
|
<select name="country">
|
||||||
|
<option value="FR">France</option>
|
||||||
|
<option value="US">USA</option>
|
||||||
|
</select>
|
||||||
|
</form>
|
||||||
|
'''
|
||||||
|
form = TestableForm(mock_client, html)
|
||||||
|
|
||||||
|
assert form.fields == {"country": "FR"}, \
|
||||||
|
f"Expected {{'country': 'FR'}}, got {form.fields}"
|
||||||
|
|
||||||
|
def test_i_can_handle_select_option_without_value_attribute(self, mock_client):
|
||||||
|
"""
|
||||||
|
Test that option text is used when value attribute is missing.
|
||||||
|
|
||||||
|
This ensures fallback to text content for options without value.
|
||||||
|
"""
|
||||||
|
html = '''
|
||||||
|
<form>
|
||||||
|
<select name="country">
|
||||||
|
<option>France</option>
|
||||||
|
<option>USA</option>
|
||||||
|
</select>
|
||||||
|
</form>
|
||||||
|
'''
|
||||||
|
form = TestableForm(mock_client, html)
|
||||||
|
|
||||||
|
expected_options = [
|
||||||
|
{"value": "France", "text": "France"},
|
||||||
|
{"value": "USA", "text": "USA"}
|
||||||
|
]
|
||||||
|
|
||||||
|
assert form.select_fields == {"country": expected_options}, \
|
||||||
|
f"Expected {{'country': {expected_options}}}, got {form.select_fields}"
|
||||||
|
|
||||||
|
def test_i_can_handle_mixed_input_types_in_same_form(self, mock_client):
|
||||||
|
"""
|
||||||
|
Test that all input types work together correctly.
|
||||||
|
|
||||||
|
This is a comprehensive test ensuring type handling is consistent.
|
||||||
|
"""
|
||||||
|
html = '''
|
||||||
|
<form>
|
||||||
|
<input type="text" name="username" value="john" />
|
||||||
|
<input type="number" name="age" value="30" />
|
||||||
|
<input type="checkbox" name="subscribe" checked />
|
||||||
|
<input type="radio" name="gender" value="male" checked />
|
||||||
|
<input type="hidden" name="token" value="123" />
|
||||||
|
<select name="country">
|
||||||
|
<option value="FR" selected>France</option>
|
||||||
|
<option value="US">USA</option>
|
||||||
|
</select>
|
||||||
|
</form>
|
||||||
|
'''
|
||||||
|
form = TestableForm(mock_client, html)
|
||||||
|
|
||||||
|
expected_fields = {
|
||||||
|
"username": "john",
|
||||||
|
"age": 30,
|
||||||
|
"subscribe": True,
|
||||||
|
"gender": "male",
|
||||||
|
"token": 123,
|
||||||
|
"country": "FR"
|
||||||
|
}
|
||||||
|
|
||||||
|
assert form.fields == expected_fields, \
|
||||||
|
f"Expected {expected_fields}, got {form.fields}"
|
||||||
|
|
||||||
|
assert "country" in form.select_fields, \
|
||||||
|
f"Expected 'country' in select_fields, got {form.select_fields}"
|
||||||
|
|
||||||
|
def test_i_can_handle_input_without_name_attribute(self, mock_client):
|
||||||
|
"""
|
||||||
|
Test that inputs without name attribute are ignored.
|
||||||
|
|
||||||
|
This ensures proper handling of unnamed inputs.
|
||||||
|
"""
|
||||||
|
html = '<form><input type="text" value="test" /></form>'
|
||||||
|
form = TestableForm(mock_client, html)
|
||||||
|
|
||||||
|
assert form.fields == {}, \
|
||||||
|
f"Expected empty dict, got {form.fields}"
|
||||||
|
|
||||||
|
def test_i_can_handle_select_without_name_attribute(self, mock_client):
|
||||||
|
"""
|
||||||
|
Test that select elements without name attribute are ignored.
|
||||||
|
|
||||||
|
This ensures proper handling of unnamed select fields.
|
||||||
|
"""
|
||||||
|
html = '<form><select><option>Test</option></select></form>'
|
||||||
|
form = TestableForm(mock_client, html)
|
||||||
|
|
||||||
|
assert form.select_fields == {}, \
|
||||||
|
f"Expected empty dict, got {form.select_fields}"
|
||||||
|
assert form.fields == {}, \
|
||||||
|
f"Expected empty dict, got {form.fields}"
|
||||||
|
|
||||||
|
def test_i_can_handle_number_input_with_empty_value(self, mock_client):
|
||||||
|
"""
|
||||||
|
Test that a number input with empty value remains empty string.
|
||||||
|
|
||||||
|
This ensures empty values are not converted to 0 or None.
|
||||||
|
"""
|
||||||
|
html = '<form><input type="number" name="count" value="" /></form>'
|
||||||
|
form = TestableForm(mock_client, html)
|
||||||
|
|
||||||
|
assert form.fields == {"count": ""}, \
|
||||||
|
f"Expected {{'count': ''}}, got {form.fields}"
|
||||||
|
|
||||||
|
def test_i_can_handle_select_with_empty_options(self, mock_client):
|
||||||
|
"""
|
||||||
|
Test behavior with a select element without options.
|
||||||
|
|
||||||
|
This ensures robustness when dealing with empty selects.
|
||||||
|
"""
|
||||||
|
html = '<form><select name="empty"></select></form>'
|
||||||
|
form = TestableForm(mock_client, html)
|
||||||
|
|
||||||
|
assert form.select_fields == {"empty": []}, \
|
||||||
|
f"Expected {{'empty': []}}, got {form.select_fields}"
|
||||||
|
assert "empty" not in form.fields, \
|
||||||
|
f"Expected 'empty' not in fields, got {form.fields}"
|
||||||
|
|
||||||
|
def test_i_can_handle_case_insensitive_boolean_values(self, mock_client):
|
||||||
|
"""
|
||||||
|
Test that boolean values are case-insensitive.
|
||||||
|
|
||||||
|
This ensures 'TRUE', 'True', 'FALSE', 'False' are all converted properly.
|
||||||
|
"""
|
||||||
|
html = '<form><input type="text" name="flag" value="TRUE" /></form>'
|
||||||
|
form = TestableForm(mock_client, html)
|
||||||
|
|
||||||
|
assert form.fields == {"flag": True}, \
|
||||||
|
f"Expected {{'flag': True}}, got {form.fields}"
|
||||||
|
assert isinstance(form.fields["flag"], bool), \
|
||||||
|
f"Expected bool type, got {type(form.fields['flag'])}"
|
||||||
Reference in New Issue
Block a user