import pytest from fasthtml.fastapp import fast_app from myfasthtml.test.testclient import TestableForm, MyTestClient @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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = TestableForm(mock_client, html) assert form.fields == {"size": None}, 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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'])}" class TestMyTestClientFill: def test_i_can_fill_form_using_input_name(self, mock_client): """ I can fill using the input name """ html = '' form = TestableForm(mock_client, html) form.fill(username="john_doe") assert form.fields == {"username": "john_doe"} def test_i_can_fill_form_using_label(self, mock_client): """ I can fill using the label associated with the input """ html = '' form = TestableForm(mock_client, html) form.fill(Username="john_doe") assert form.fields == {"username": "john_doe"} def test_i_cannot_fill_form_with_invalid_field_name(self, mock_client): """ I cannot fill form with invalid field name """ html = '' form = TestableForm(mock_client, html) with pytest.raises(ValueError) as excinfo: form.fill(invalid_field="john_doe") assert str(excinfo.value) == "Invalid field name 'invalid_field'." class TestableFormSubmit: """ Test suite for the submit() method of TestableForm class. This module tests form submission for both HTMX and classic forms. """ def test_i_can_submit_classic_form_with_post_method(self): """ Test that a classic form with POST method is submitted correctly. This ensures the form uses the action and method attributes properly. """ # HTML form with classic submission html = ''' ''' # Create the form test_app, rt = fast_app(default_hdrs=False) client = MyTestClient(test_app) form = TestableForm(client, html) @rt('/submit') def post(username: str, password: str): return f"Form received {username=}, {password=}" form.submit() assert client.get_content() == "Form received username='john_doe', password='secret123'" def test_i_can_submit_classic_form_with_get_method(self): """ Test that a classic form with GET method is submitted correctly. This ensures GET requests are properly handled. """ html = ''' ''' test_app, rt = fast_app(default_hdrs=False) client = MyTestClient(test_app) form = TestableForm(client, html) @rt('/search') def get(q: str, page: int): return f"Search results for {q=}, {page=}" form.submit() assert client.get_content() == "Search results for q='python', page=1" def test_i_can_submit_classic_form_without_method_defaults_to_post(self): """ Test that POST is used by default when method attribute is absent. This ensures proper default behavior matching HTML standards. """ html = ''' ''' test_app, rt = fast_app(default_hdrs=False) client = MyTestClient(test_app) form = TestableForm(client, html) @rt('/submit') def post(data: str): return f"Received {data=}" form.submit() assert client.get_content() == "Received data='test'" def test_i_can_submit_form_with_htmx_post(self): """ Test that a form with hx_post uses HTMX submission. This ensures HTMX-enabled forms are handled correctly. """ html = ''' ''' test_app, rt = fast_app(default_hdrs=False) client = MyTestClient(test_app) form = TestableForm(client, html) @rt('/htmx-submit') def post(username: str): return f"HTMX received {username=}" form.submit() assert client.get_content() == "HTMX received username='alice'" def test_i_can_submit_form_with_htmx_get(self): """ Test that a form with hx_get uses HTMX submission. This ensures HTMX GET requests work properly. """ html = ''' ''' test_app, rt = fast_app(default_hdrs=False) client = MyTestClient(test_app) form = TestableForm(client, html) @rt('/htmx-search') def get(query: str): return f"HTMX search for {query=}" form.submit() assert client.get_content() == "HTMX search for query='fasthtml'" def test_i_can_submit_form_with_filled_fields(self): """ Test that fields filled via fill_form() are submitted correctly. This ensures dynamic form filling works as expected. """ html = ''' ''' test_app, rt = fast_app(default_hdrs=False) client = MyTestClient(test_app) form = TestableForm(client, html) # Fill the form dynamically form.fill(username="bob", password="secure456") @rt('/login') def post(username: str, password: str): return f"Login {username=}, {password=}" form.submit() assert client.get_content() == "Login username='bob', password='secure456'" def test_i_can_submit_form_with_mixed_field_types(self): """ Test that all field types are submitted with correct values. This ensures type conversion and submission work together. """ html = ''' ''' test_app, rt = fast_app(default_hdrs=False) client = MyTestClient(test_app) form = TestableForm(client, html) @rt('/register') def post(username: str, age: int, newsletter: bool, country: str): return f"Registration {username=}, {age=}, {newsletter=}, {country=}" result = form.submit() # Note: the types are converted in self.fields expected = "Registration username='charlie', age=30, newsletter=True, country='US'" assert client.get_content() == expected def test_i_cannot_submit_classic_form_without_action(self): """ Test that an exception is raised when action attribute is missing. This ensures proper error handling for malformed forms. """ html = ''' ''' test_app, rt = fast_app(default_hdrs=False) client = MyTestClient(test_app) form = TestableForm(client, html) # Should raise ValueError try: form.submit() assert False, "Expected ValueError to be raised" except ValueError as e: assert "no 'action' attribute" in str(e).lower() def test_i_cannot_submit_classic_form_with_empty_action(self): """ Test that an exception is raised when action attribute is empty. This ensures validation of the action attribute. """ html = ''' ''' test_app, rt = fast_app(default_hdrs=False) client = MyTestClient(test_app) form = TestableForm(client, html) # Should raise ValueError try: form.submit() assert False, "Expected ValueError to be raised" except ValueError as e: assert "no 'action' attribute" in str(e).lower() def test_i_can_submit_form_with_case_insensitive_method(self): """ Test that HTTP method is properly normalized to uppercase. This ensures method attribute is case-insensitive. """ html = ''' ''' test_app, rt = fast_app(default_hdrs=False) client = MyTestClient(test_app) form = TestableForm(client, html) @rt('/submit') def post(data: str): return f"Received {data=}" form.submit() assert client.get_content() == "Received data='test'" def test_i_can_prioritize_htmx_over_classic_submission(self): """ Test that HTMX is prioritized even when action/method are present. This ensures correct priority between HTMX and classic submission. """ html = ''' ''' test_app, rt = fast_app(default_hdrs=False) client = MyTestClient(test_app) form = TestableForm(client, html) @rt('/classic') def post_classic(data: str): return "Classic submission" @rt('/htmx') def post_htmx(data: str): return "HTMX submission" form.submit() assert client.get_content() == "HTMX submission" def test_i_can_submit_empty_form(self): """ Test that a form without fields can be submitted. This ensures robustness for minimal forms. """ html = '' test_app, rt = fast_app(default_hdrs=False) client = MyTestClient(test_app) form = TestableForm(client, html) @rt('/empty') def post(): return "Empty form received" form.submit() assert client.get_content() == "Empty form received"