Added authentication forms and routes
This commit is contained in:
0
tests/auth/__init__.py
Normal file
0
tests/auth/__init__.py
Normal file
115
tests/auth/test_login.py
Normal file
115
tests/auth/test_login.py
Normal file
@@ -0,0 +1,115 @@
|
||||
import os
|
||||
from dataclasses import dataclass
|
||||
|
||||
import pytest
|
||||
from fasthtml.fastapp import fast_app
|
||||
|
||||
import myfasthtml.auth.utils
|
||||
from myfasthtml.auth.routes import setup_auth_routes
|
||||
from myfasthtml.auth.utils import create_auth_beforeware, register_user
|
||||
from myfasthtml.core.testclient import MyTestClient
|
||||
|
||||
|
||||
@dataclass
|
||||
class DummyUser:
|
||||
email: str
|
||||
username: str
|
||||
password: str
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def registered_user():
|
||||
user = DummyUser("user@email.com", "user", "#Passw0rd")
|
||||
register_user(user.email, user.username, user.password)
|
||||
return user
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def app():
|
||||
beforeware = create_auth_beforeware()
|
||||
test_app, test_rt = fast_app(before=beforeware)
|
||||
setup_auth_routes(test_app, test_rt, mount_auth_app=True, sqlite_db_path="TestUsers.db")
|
||||
|
||||
@test_rt('/')
|
||||
def index(): return "You are now logged in !"
|
||||
|
||||
return test_app
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def rt(app):
|
||||
return app.route
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def user(app):
|
||||
user = MyTestClient(app)
|
||||
previous = myfasthtml.auth.utils.http_client
|
||||
myfasthtml.auth.utils.http_client = user.client
|
||||
|
||||
yield user
|
||||
|
||||
myfasthtml.auth.utils.http_client = previous
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def cleanup():
|
||||
if os.path.exists("TestUsers.db"):
|
||||
os.remove("TestUsers.db")
|
||||
|
||||
|
||||
def test_i_can_see_login_page(user):
|
||||
user.open("/login")
|
||||
user.should_see("Sign In")
|
||||
user.should_see("Register here")
|
||||
user.find_form(fields=["Email", "Password"])
|
||||
|
||||
|
||||
def test_i_cannot_login_with_wrong_credentials(user):
|
||||
user.open("/login")
|
||||
form = user.find_form(fields=["Email", "Password"])
|
||||
form.fill(Email="user@email.com", Password="#Passw0rd")
|
||||
form.submit()
|
||||
user.should_see("Invalid email or password. Please try again.")
|
||||
|
||||
|
||||
def test_i_can_login_with_correct_credentials(user, registered_user):
|
||||
user.open("/login")
|
||||
form = user.find_form(fields=["Email", "Password"])
|
||||
form.fill(Email=registered_user.email, Password=registered_user.password)
|
||||
form.submit()
|
||||
user.should_see("You are now logged in !")
|
||||
|
||||
|
||||
def test_i_can_can_navigate_once_logged_in(user, registered_user):
|
||||
user.open("/welcome") # not logged in, redirects to login
|
||||
user.should_see("Sign In")
|
||||
|
||||
form = user.find_form(fields=["Email", "Password"])
|
||||
form.fill(Email=registered_user.email, Password=registered_user.password)
|
||||
form.submit()
|
||||
|
||||
user.open("/welcome") # once logged in, welcome page is accessible
|
||||
user.should_see("Welcome back, user@email.com!") # welcome page is predefined
|
||||
|
||||
|
||||
def test_i_can_register(user):
|
||||
user.open("/register")
|
||||
form = user.find_form(fields=["Email", "Username", "Password"])
|
||||
form.fill(Email="user@email.com", Username="username", Password="#Passw0rd", confirm_password="#Passw0rd")
|
||||
form.submit()
|
||||
|
||||
user.should_see("You are now logged in !")
|
||||
|
||||
|
||||
def test_i_can_logout(user, registered_user):
|
||||
user.open("/login")
|
||||
form = user.find_form(fields=["Email", "Password"])
|
||||
form.fill(Email=registered_user.email, Password=registered_user.password)
|
||||
form.submit()
|
||||
|
||||
user.open("/logout")
|
||||
user.should_see("Sign In")
|
||||
|
||||
user.open("/welcome")
|
||||
user.should_see("Sign In")
|
||||
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")
|
||||
@@ -1,326 +0,0 @@
|
||||
import pytest
|
||||
from fasthtml.components import Div
|
||||
from fasthtml.fastapp import fast_app
|
||||
|
||||
from myfasthtml.core.testclient import MyTestClient, TestableElement
|
||||
|
||||
|
||||
def test_i_can_open_a_page():
|
||||
test_app, rt = fast_app(default_hdrs=False)
|
||||
client = MyTestClient(test_app)
|
||||
|
||||
@rt('/')
|
||||
def get(): return "hello world"
|
||||
|
||||
client.open("/")
|
||||
|
||||
assert client.get_content() == "hello world"
|
||||
|
||||
|
||||
def test_i_can_open_a_page_when_html():
|
||||
test_app, rt = fast_app(default_hdrs=False)
|
||||
client = MyTestClient(test_app)
|
||||
|
||||
@rt('/')
|
||||
def get(): return Div("hello world")
|
||||
|
||||
client.open("/")
|
||||
|
||||
assert client.get_content() == ' <!doctype html>\n <html>\n <head>\n <title>FastHTML page</title>\n <link rel="canonical" href="http://testserver/">\n </head>\n <body>\n <div>hello world</div>\n </body>\n </html>\n'
|
||||
|
||||
|
||||
def test_i_cannot_open_a_page_not_defined():
|
||||
test_app, rt = fast_app(default_hdrs=False)
|
||||
client = MyTestClient(test_app)
|
||||
|
||||
with pytest.raises(AssertionError) as exc_info:
|
||||
client.open("/not_found")
|
||||
|
||||
assert str(exc_info.value) == "Failed to open '/not_found'. status code=404 : reason='404 Not Found'"
|
||||
|
||||
def test_i_can_see_text_in_plain_response():
|
||||
"""Test that should_see() works with plain text responses."""
|
||||
test_app, rt = fast_app(default_hdrs=False)
|
||||
client = MyTestClient(test_app)
|
||||
|
||||
@rt('/')
|
||||
def get():
|
||||
return "hello world"
|
||||
|
||||
client.open("/").should_see("hello world")
|
||||
|
||||
|
||||
def test_i_can_see_text_in_html_response():
|
||||
"""Test that should_see() extracts visible text from HTML responses."""
|
||||
test_app, rt = fast_app(default_hdrs=False)
|
||||
client = MyTestClient(test_app)
|
||||
|
||||
@rt('/')
|
||||
def get():
|
||||
return "<html><body><h1>Welcome</h1><p>This is a test</p></body></html>"
|
||||
|
||||
client.open("/").should_see("Welcome").should_see("This is a test")
|
||||
|
||||
|
||||
def test_i_can_see_text_ignoring_html_tags():
|
||||
"""Test that should_see() searches in visible text only, not in HTML tags."""
|
||||
test_app, rt = fast_app(default_hdrs=False)
|
||||
client = MyTestClient(test_app)
|
||||
|
||||
@rt('/')
|
||||
def get():
|
||||
return '<div class="container">Content</div>'
|
||||
|
||||
# Should find the visible text
|
||||
client.open("/").should_see("Content")
|
||||
|
||||
# Should NOT find text that's only in attributes/tags
|
||||
with pytest.raises(AssertionError) as exc_info:
|
||||
client.should_see("container")
|
||||
|
||||
assert "Expected to see 'container' in page content but it was not found" in str(exc_info.value)
|
||||
|
||||
|
||||
def test_i_cannot_see_text_that_is_not_present():
|
||||
"""Test that should_see() raises AssertionError when text is not found."""
|
||||
test_app, rt = fast_app(default_hdrs=False)
|
||||
client = MyTestClient(test_app)
|
||||
|
||||
@rt('/')
|
||||
def get():
|
||||
return "hello world"
|
||||
|
||||
with pytest.raises(AssertionError) as exc_info:
|
||||
client.open("/").should_see("goodbye")
|
||||
|
||||
assert "Expected to see 'goodbye' in page content but it was not found" in str(exc_info.value)
|
||||
assert "hello world" in str(exc_info.value)
|
||||
|
||||
|
||||
def test_i_cannot_call_should_see_without_opening_page():
|
||||
"""Test that should_see() raises ValueError if no page has been opened."""
|
||||
test_app, rt = fast_app(default_hdrs=False)
|
||||
client = MyTestClient(test_app)
|
||||
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
client.should_see("anything")
|
||||
|
||||
assert str(exc_info.value) == "No page content available. Call open() before should_see()."
|
||||
|
||||
|
||||
def test_i_can_verify_text_is_not_present():
|
||||
"""Test that should_not_see() works when text is absent."""
|
||||
test_app, rt = fast_app(default_hdrs=False)
|
||||
client = MyTestClient(test_app)
|
||||
|
||||
@rt('/')
|
||||
def get():
|
||||
return "hello world"
|
||||
|
||||
client.open("/").should_not_see("goodbye")
|
||||
|
||||
|
||||
def test_i_cannot_use_should_not_see_when_text_is_present():
|
||||
"""Test that should_not_see() raises AssertionError when text is found."""
|
||||
test_app, rt = fast_app(default_hdrs=False)
|
||||
client = MyTestClient(test_app)
|
||||
|
||||
@rt('/')
|
||||
def get():
|
||||
return "hello world"
|
||||
|
||||
with pytest.raises(AssertionError) as exc_info:
|
||||
client.open("/").should_not_see("hello")
|
||||
|
||||
error_message = str(exc_info.value)
|
||||
assert "Expected NOT to see 'hello' in page content but it was found" in error_message
|
||||
|
||||
|
||||
def test_i_cannot_call_should_not_see_without_opening_page():
|
||||
"""Test that should_not_see() raises ValueError if no page has been opened."""
|
||||
test_app, rt = fast_app(default_hdrs=False)
|
||||
client = MyTestClient(test_app)
|
||||
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
client.should_not_see("anything")
|
||||
|
||||
assert str(exc_info.value) == "No page content available. Call open() before should_not_see()."
|
||||
|
||||
|
||||
def test_i_can_chain_multiple_assertions():
|
||||
"""Test that assertions can be chained together."""
|
||||
test_app, rt = fast_app(default_hdrs=False)
|
||||
client = MyTestClient(test_app)
|
||||
|
||||
@rt('/')
|
||||
def get():
|
||||
return "<html><body><h1>Welcome</h1><p>Content here</p></body></html>"
|
||||
|
||||
# Chain multiple assertions
|
||||
client.open("/").should_see("Welcome").should_see("Content").should_not_see("Error")
|
||||
|
||||
|
||||
def test_i_can_see_element_context_when_text_should_not_be_seen():
|
||||
"""Test that the HTML element containing the text is displayed with parent context."""
|
||||
test_app, rt = fast_app(default_hdrs=False)
|
||||
client = MyTestClient(test_app)
|
||||
|
||||
@rt('/')
|
||||
def get():
|
||||
return '<div class="container"><p class="content">forbidden text</p></div>'
|
||||
|
||||
with pytest.raises(AssertionError) as exc_info:
|
||||
client.open("/").should_not_see("forbidden text")
|
||||
|
||||
error_message = str(exc_info.value)
|
||||
assert "Found in:" in error_message
|
||||
assert '<p class="content">forbidden text</p>' in error_message
|
||||
assert '<div class="container">' in error_message
|
||||
|
||||
|
||||
def test_i_can_configure_parent_levels_in_constructor():
|
||||
"""Test that parent_levels parameter controls the number of parent levels shown."""
|
||||
test_app, rt = fast_app(default_hdrs=False)
|
||||
client = MyTestClient(test_app, parent_levels=2)
|
||||
|
||||
@rt('/')
|
||||
def get():
|
||||
return '<body><div class="wrapper"><div class="container"><p>error</p></div></div></body>'
|
||||
|
||||
with pytest.raises(AssertionError) as exc_info:
|
||||
client.open("/").should_not_see("error")
|
||||
|
||||
error_message = str(exc_info.value)
|
||||
assert '<p>error</p>' in error_message
|
||||
assert '<div class="container">' in error_message
|
||||
assert '<div class="wrapper">' in error_message
|
||||
|
||||
|
||||
def test_i_can_find_text_in_nested_elements():
|
||||
"""Test that the smallest element containing the text is found in nested structures."""
|
||||
test_app, rt = fast_app(default_hdrs=False)
|
||||
client = MyTestClient(test_app)
|
||||
|
||||
@rt('/')
|
||||
def get():
|
||||
return '<div><section><article><p class="target">nested text</p></article></section></div>'
|
||||
|
||||
with pytest.raises(AssertionError) as exc_info:
|
||||
client.open("/").should_not_see("nested text")
|
||||
|
||||
error_message = str(exc_info.value)
|
||||
# Should find the <p> element, not the outer <div>
|
||||
assert '<p class="target">nested text</p>' in error_message
|
||||
|
||||
|
||||
def test_i_can_find_fragmented_text_across_tags():
|
||||
"""Test that text fragmented across multiple tags is correctly found."""
|
||||
test_app, rt = fast_app(default_hdrs=False)
|
||||
client = MyTestClient(test_app)
|
||||
|
||||
@rt('/')
|
||||
def get():
|
||||
return '<p class="message">hel<span>lo</span> world</p>'
|
||||
|
||||
with pytest.raises(AssertionError) as exc_info:
|
||||
client.open("/").should_not_see("hello world")
|
||||
|
||||
error_message = str(exc_info.value)
|
||||
# Should find the parent <p> element that contains the full text
|
||||
assert '<p class="message">' in error_message
|
||||
|
||||
|
||||
def test_i_do_not_find_text_in_html_attributes():
|
||||
"""Test that text in HTML attributes is not considered as visible text."""
|
||||
test_app, rt = fast_app(default_hdrs=False)
|
||||
client = MyTestClient(test_app)
|
||||
|
||||
@rt('/')
|
||||
def get():
|
||||
return '<div class="error message" title="error info">Success</div>'
|
||||
|
||||
# "error" is in attributes but not in visible text
|
||||
client.open("/").should_not_see("error")
|
||||
|
||||
# "Success" is in visible text
|
||||
with pytest.raises(AssertionError):
|
||||
client.should_not_see("Success")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("selector,expected_tag", [
|
||||
("#unique-id", '<div class="main wrapper"'),
|
||||
(".home-link", '<a class="link home-link"'),
|
||||
("div > div", '<div class="content"'),
|
||||
("div span", "<span"),
|
||||
("[data-type]", '<a class="link home-link"'),
|
||||
('[data-type="navigation"]', '<a class="link home-link"'),
|
||||
('[class~="link"]', '<a class="link home-link"'),
|
||||
('[href^="/home"]', '<a class="link home-link"'),
|
||||
('[href$="about"]', '<a href="/about">'),
|
||||
('[data-author*="john"]', '<a class="link home-link"'),
|
||||
])
|
||||
def test_i_can_find_element(selector, expected_tag):
|
||||
"""Test that find_element works with various CSS selectors."""
|
||||
test_app, rt = fast_app(default_hdrs=False)
|
||||
client = MyTestClient(test_app)
|
||||
|
||||
@rt('/')
|
||||
def get():
|
||||
return '''
|
||||
<div id="unique-id" class="main wrapper">
|
||||
<a href="/home" class="link home-link" data-type="navigation" data-author="john-doe">Home</a>
|
||||
<a href="/about">About</a>
|
||||
<div class="content">
|
||||
<span class="text">Content</span>
|
||||
</div>
|
||||
</div>
|
||||
'''
|
||||
|
||||
element = client.open("/").find_element(selector)
|
||||
|
||||
assert element is not None
|
||||
assert isinstance(element, TestableElement)
|
||||
assert expected_tag in element.html_fragment
|
||||
|
||||
|
||||
def test_i_cannot_find_element_when_none_exists():
|
||||
"""Test that find_element raises AssertionError when no element matches."""
|
||||
test_app, rt = fast_app(default_hdrs=False)
|
||||
client = MyTestClient(test_app)
|
||||
|
||||
@rt('/')
|
||||
def get():
|
||||
return '<div class="container"><p>Content</p></div>'
|
||||
|
||||
with pytest.raises(AssertionError) as exc_info:
|
||||
client.open("/").find_element("#non-existent")
|
||||
|
||||
assert "No element found matching selector '#non-existent'" in str(exc_info.value)
|
||||
|
||||
|
||||
def test_i_cannot_find_element_when_multiple_exist():
|
||||
"""Test that find_element raises AssertionError when multiple elements match."""
|
||||
test_app, rt = fast_app(default_hdrs=False)
|
||||
client = MyTestClient(test_app)
|
||||
|
||||
@rt('/')
|
||||
def get():
|
||||
return '<div><p class="text">First</p><p class="text">Second</p><p class="text">Third</p></div>'
|
||||
|
||||
with pytest.raises(AssertionError) as exc_info:
|
||||
client.open("/").find_element(".text")
|
||||
|
||||
error_message = str(exc_info.value)
|
||||
assert "Found 3 elements matching selector '.text'" in error_message
|
||||
assert "Expected exactly 1" in error_message
|
||||
|
||||
|
||||
def test_i_cannot_call_find_element_without_opening_page():
|
||||
"""Test that find_element raises ValueError if no page has been opened."""
|
||||
test_app, rt = fast_app(default_hdrs=False)
|
||||
client = MyTestClient(test_app)
|
||||
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
client.find_element("#any-selector")
|
||||
|
||||
assert str(exc_info.value) == "No page content available. Call open() before find_element()."
|
||||
0
tests/testclient/__init__.py
Normal file
0
tests/testclient/__init__.py
Normal file
484
tests/testclient/test_mytestclient.py
Normal file
484
tests/testclient/test_mytestclient.py
Normal file
@@ -0,0 +1,484 @@
|
||||
import pytest
|
||||
from fasthtml.components import Div
|
||||
from fasthtml.fastapp import fast_app
|
||||
|
||||
from myfasthtml.core.testclient import MyTestClient, TestableElement, TestableForm
|
||||
|
||||
|
||||
class TestMyTestClientOpen:
|
||||
|
||||
def test_i_can_open_a_page(self):
|
||||
test_app, rt = fast_app(default_hdrs=False)
|
||||
client = MyTestClient(test_app)
|
||||
|
||||
@rt('/')
|
||||
def get(): return "hello world"
|
||||
|
||||
client.open("/")
|
||||
|
||||
assert client.get_content() == "hello world"
|
||||
|
||||
def test_i_can_open_a_page_when_html(self):
|
||||
test_app, rt = fast_app(default_hdrs=False)
|
||||
client = MyTestClient(test_app)
|
||||
|
||||
@rt('/')
|
||||
def get(): return Div("hello world")
|
||||
|
||||
client.open("/")
|
||||
|
||||
assert client.get_content() == ' <!doctype html>\n <html>\n <head>\n <title>FastHTML page</title>\n <link rel="canonical" href="http://testserver/">\n </head>\n <body>\n <div>hello world</div>\n </body>\n </html>\n'
|
||||
|
||||
def test_i_cannot_open_a_page_not_defined(self):
|
||||
test_app, rt = fast_app(default_hdrs=False)
|
||||
client = MyTestClient(test_app)
|
||||
|
||||
with pytest.raises(AssertionError) as exc_info:
|
||||
client.open("/not_found")
|
||||
|
||||
assert str(exc_info.value) == "Failed to open '/not_found'. status code=404 : reason='404 Not Found'"
|
||||
|
||||
|
||||
class TestMyTestClientShouldSee:
|
||||
def test_i_can_see_text_in_plain_response(self):
|
||||
"""Test that should_see() works with plain text responses."""
|
||||
test_app, rt = fast_app(default_hdrs=False)
|
||||
client = MyTestClient(test_app)
|
||||
|
||||
@rt('/')
|
||||
def get():
|
||||
return "hello world"
|
||||
|
||||
client.open("/").should_see("hello world")
|
||||
|
||||
def test_i_can_see_text_in_html_response(self):
|
||||
"""Test that should_see() extracts visible text from HTML responses."""
|
||||
test_app, rt = fast_app(default_hdrs=False)
|
||||
client = MyTestClient(test_app)
|
||||
|
||||
@rt('/')
|
||||
def get():
|
||||
return "<html><body><h1>Welcome</h1><p>This is a test</p></body></html>"
|
||||
|
||||
client.open("/").should_see("Welcome").should_see("This is a test")
|
||||
|
||||
def test_i_can_see_text_ignoring_html_tags(self):
|
||||
"""Test that should_see() searches in visible text only, not in HTML tags."""
|
||||
test_app, rt = fast_app(default_hdrs=False)
|
||||
client = MyTestClient(test_app)
|
||||
|
||||
@rt('/')
|
||||
def get():
|
||||
return '<div class="container">Content</div>'
|
||||
|
||||
# Should find the visible text
|
||||
client.open("/").should_see("Content")
|
||||
|
||||
# Should NOT find text that's only in attributes/tags
|
||||
with pytest.raises(AssertionError) as exc_info:
|
||||
client.should_see("container")
|
||||
|
||||
assert "Expected to see 'container' in page content but it was not found" in str(exc_info.value)
|
||||
|
||||
def test_i_cannot_see_text_that_is_not_present(self):
|
||||
"""Test that should_see() raises AssertionError when text is not found."""
|
||||
test_app, rt = fast_app(default_hdrs=False)
|
||||
client = MyTestClient(test_app)
|
||||
|
||||
@rt('/')
|
||||
def get():
|
||||
return "hello world"
|
||||
|
||||
with pytest.raises(AssertionError) as exc_info:
|
||||
client.open("/").should_see("goodbye")
|
||||
|
||||
assert "Expected to see 'goodbye' in page content but it was not found" in str(exc_info.value)
|
||||
assert "hello world" in str(exc_info.value)
|
||||
|
||||
def test_i_cannot_call_should_see_without_opening_page(self):
|
||||
"""Test that should_see() raises ValueError if no page has been opened."""
|
||||
test_app, rt = fast_app(default_hdrs=False)
|
||||
client = MyTestClient(test_app)
|
||||
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
client.should_see("anything")
|
||||
|
||||
assert str(exc_info.value) == "No page content available. Call open() before should_see()."
|
||||
|
||||
def test_i_can_verify_text_is_not_present(self):
|
||||
"""Test that should_not_see() works when text is absent."""
|
||||
test_app, rt = fast_app(default_hdrs=False)
|
||||
client = MyTestClient(test_app)
|
||||
|
||||
@rt('/')
|
||||
def get():
|
||||
return "hello world"
|
||||
|
||||
client.open("/").should_not_see("goodbye")
|
||||
|
||||
def test_i_cannot_use_should_not_see_when_text_is_present(self):
|
||||
"""Test that should_not_see() raises AssertionError when text is found."""
|
||||
test_app, rt = fast_app(default_hdrs=False)
|
||||
client = MyTestClient(test_app)
|
||||
|
||||
@rt('/')
|
||||
def get():
|
||||
return "hello world"
|
||||
|
||||
with pytest.raises(AssertionError) as exc_info:
|
||||
client.open("/").should_not_see("hello")
|
||||
|
||||
error_message = str(exc_info.value)
|
||||
assert "Expected NOT to see 'hello' in page content but it was found" in error_message
|
||||
|
||||
def test_i_cannot_call_should_not_see_without_opening_page(self):
|
||||
"""Test that should_not_see() raises ValueError if no page has been opened."""
|
||||
test_app, rt = fast_app(default_hdrs=False)
|
||||
client = MyTestClient(test_app)
|
||||
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
client.should_not_see("anything")
|
||||
|
||||
assert str(exc_info.value) == "No page content available. Call open() before should_not_see()."
|
||||
|
||||
def test_i_can_chain_multiple_assertions(self):
|
||||
"""Test that assertions can be chained together."""
|
||||
test_app, rt = fast_app(default_hdrs=False)
|
||||
client = MyTestClient(test_app)
|
||||
|
||||
@rt('/')
|
||||
def get():
|
||||
return "<html><body><h1>Welcome</h1><p>Content here</p></body></html>"
|
||||
|
||||
# Chain multiple assertions
|
||||
client.open("/").should_see("Welcome").should_see("Content").should_not_see("Error")
|
||||
|
||||
def test_i_can_see_element_context_when_text_should_not_be_seen(self):
|
||||
"""Test that the HTML element containing the text is displayed with parent context."""
|
||||
test_app, rt = fast_app(default_hdrs=False)
|
||||
client = MyTestClient(test_app)
|
||||
|
||||
@rt('/')
|
||||
def get():
|
||||
return '<div class="container"><p class="content">forbidden text</p></div>'
|
||||
|
||||
with pytest.raises(AssertionError) as exc_info:
|
||||
client.open("/").should_not_see("forbidden text")
|
||||
|
||||
error_message = str(exc_info.value)
|
||||
assert "Found in:" in error_message
|
||||
assert '<p class="content">forbidden text</p>' in error_message
|
||||
assert '<div class="container">' in error_message
|
||||
|
||||
def test_i_can_configure_parent_levels_in_constructor(self):
|
||||
"""Test that parent_levels parameter controls the number of parent levels shown."""
|
||||
test_app, rt = fast_app(default_hdrs=False)
|
||||
client = MyTestClient(test_app, parent_levels=2)
|
||||
|
||||
@rt('/')
|
||||
def get():
|
||||
return '<body><div class="wrapper"><div class="container"><p>error</p></div></div></body>'
|
||||
|
||||
with pytest.raises(AssertionError) as exc_info:
|
||||
client.open("/").should_not_see("error")
|
||||
|
||||
error_message = str(exc_info.value)
|
||||
assert '<p>error</p>' in error_message
|
||||
assert '<div class="container">' in error_message
|
||||
assert '<div class="wrapper">' in error_message
|
||||
|
||||
def test_i_can_find_text_in_nested_elements(self):
|
||||
"""Test that the smallest element containing the text is found in nested structures."""
|
||||
test_app, rt = fast_app(default_hdrs=False)
|
||||
client = MyTestClient(test_app)
|
||||
|
||||
@rt('/')
|
||||
def get():
|
||||
return '<div><section><article><p class="target">nested text</p></article></section></div>'
|
||||
|
||||
with pytest.raises(AssertionError) as exc_info:
|
||||
client.open("/").should_not_see("nested text")
|
||||
|
||||
error_message = str(exc_info.value)
|
||||
# Should find the <p> element, not the outer <div>
|
||||
assert '<p class="target">nested text</p>' in error_message
|
||||
|
||||
def test_i_can_find_fragmented_text_across_tags(self):
|
||||
"""Test that text fragmented across multiple tags is correctly found."""
|
||||
test_app, rt = fast_app(default_hdrs=False)
|
||||
client = MyTestClient(test_app)
|
||||
|
||||
@rt('/')
|
||||
def get():
|
||||
return '<p class="message">hel<span>lo</span> world</p>'
|
||||
|
||||
with pytest.raises(AssertionError) as exc_info:
|
||||
client.open("/").should_not_see("hello world")
|
||||
|
||||
error_message = str(exc_info.value)
|
||||
# Should find the parent <p> element that contains the full text
|
||||
assert '<p class="message">' in error_message
|
||||
|
||||
def test_i_do_not_find_text_in_html_attributes(self):
|
||||
"""Test that text in HTML attributes is not considered as visible text."""
|
||||
test_app, rt = fast_app(default_hdrs=False)
|
||||
client = MyTestClient(test_app)
|
||||
|
||||
@rt('/')
|
||||
def get():
|
||||
return '<div class="error message" title="error info">Success</div>'
|
||||
|
||||
# "error" is in attributes but not in visible text
|
||||
client.open("/").should_not_see("error")
|
||||
|
||||
# "Success" is in visible text
|
||||
with pytest.raises(AssertionError):
|
||||
client.should_not_see("Success")
|
||||
|
||||
|
||||
class TestMyTestClientFindElement:
|
||||
|
||||
@pytest.mark.parametrize("selector,expected_tag", [
|
||||
("#unique-id", '<div class="main wrapper"'),
|
||||
(".home-link", '<a class="link home-link"'),
|
||||
("div > div", '<div class="content"'),
|
||||
("div span", "<span"),
|
||||
("[data-type]", '<a class="link home-link"'),
|
||||
('[data-type="navigation"]', '<a class="link home-link"'),
|
||||
('[class~="link"]', '<a class="link home-link"'),
|
||||
('[href^="/home"]', '<a class="link home-link"'),
|
||||
('[href$="about"]', '<a href="/about">'),
|
||||
('[data-author*="john"]', '<a class="link home-link"'),
|
||||
])
|
||||
def test_i_can_find_element(self, selector, expected_tag):
|
||||
"""Test that find_element works with various CSS selectors."""
|
||||
test_app, rt = fast_app(default_hdrs=False)
|
||||
client = MyTestClient(test_app)
|
||||
|
||||
@rt('/')
|
||||
def get():
|
||||
return '''
|
||||
<div id="unique-id" class="main wrapper">
|
||||
<a href="/home" class="link home-link" data-type="navigation" data-author="john-doe">Home</a>
|
||||
<a href="/about">About</a>
|
||||
<div class="content">
|
||||
<span class="text">Content</span>
|
||||
</div>
|
||||
</div>
|
||||
'''
|
||||
|
||||
element = client.open("/").find_element(selector)
|
||||
|
||||
assert element is not None
|
||||
assert isinstance(element, TestableElement)
|
||||
assert expected_tag in element.html_fragment
|
||||
|
||||
def test_i_cannot_find_element_when_none_exists(self):
|
||||
"""Test that find_element raises AssertionError when no element matches."""
|
||||
test_app, rt = fast_app(default_hdrs=False)
|
||||
client = MyTestClient(test_app)
|
||||
|
||||
@rt('/')
|
||||
def get():
|
||||
return '<div class="container"><p>Content</p></div>'
|
||||
|
||||
with pytest.raises(AssertionError) as exc_info:
|
||||
client.open("/").find_element("#non-existent")
|
||||
|
||||
assert "No element found matching selector '#non-existent'" in str(exc_info.value)
|
||||
|
||||
def test_i_cannot_find_element_when_multiple_exist(self):
|
||||
"""Test that find_element raises AssertionError when multiple elements match."""
|
||||
test_app, rt = fast_app(default_hdrs=False)
|
||||
client = MyTestClient(test_app)
|
||||
|
||||
@rt('/')
|
||||
def get():
|
||||
return '<div><p class="text">First</p><p class="text">Second</p><p class="text">Third</p></div>'
|
||||
|
||||
with pytest.raises(AssertionError) as exc_info:
|
||||
client.open("/").find_element(".text")
|
||||
|
||||
error_message = str(exc_info.value)
|
||||
assert "Found 3 elements matching selector '.text'" in error_message
|
||||
assert "Expected exactly 1" in error_message
|
||||
|
||||
def test_i_cannot_call_find_element_without_opening_page(self):
|
||||
"""Test that find_element raises ValueError if no page has been opened."""
|
||||
test_app, rt = fast_app(default_hdrs=False)
|
||||
client = MyTestClient(test_app)
|
||||
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
client.find_element("#any-selector")
|
||||
|
||||
assert str(exc_info.value) == "No page content available. Call open() before find_element()."
|
||||
|
||||
|
||||
class TestMyTestClientFindForm:
|
||||
def test_i_can_find_form(self):
|
||||
"""Test that find_form works fo simple form."""
|
||||
test_app, rt = fast_app(default_hdrs=False)
|
||||
client = MyTestClient(test_app)
|
||||
|
||||
@rt('/')
|
||||
def get():
|
||||
return '''<form></form>'''
|
||||
|
||||
form = client.open("/").find_form()
|
||||
|
||||
assert form is not None
|
||||
assert isinstance(form, TestableForm)
|
||||
|
||||
def test_i_can_find_form_in_nested_elements(self):
|
||||
"""Test that find_form works when form is nested in other elements."""
|
||||
test_app, rt = fast_app(default_hdrs=False)
|
||||
client = MyTestClient(test_app)
|
||||
|
||||
@rt('/')
|
||||
def get():
|
||||
return '''
|
||||
<div class="container">
|
||||
<div class="wrapper">
|
||||
<form></form>
|
||||
</div>
|
||||
</div>
|
||||
'''
|
||||
|
||||
form = client.open("/").find_form()
|
||||
|
||||
assert form is not None
|
||||
assert isinstance(form, TestableForm)
|
||||
|
||||
def test_i_can_find_form_with_all_fields(self):
|
||||
"""Test that find_form works when form has fields."""
|
||||
test_app, rt = fast_app(default_hdrs=False)
|
||||
client = MyTestClient(test_app)
|
||||
|
||||
@rt('/')
|
||||
def get():
|
||||
return '''
|
||||
<form>
|
||||
<input type="text" name="username" placeholder="Username">
|
||||
<input type="password" name="password" placeholder="Password">
|
||||
<button type="submit">Sign In</button>
|
||||
</form>
|
||||
'''
|
||||
|
||||
form = client.open("/").find_form(fields=["username", "password"])
|
||||
|
||||
assert form is not None
|
||||
assert isinstance(form, TestableForm)
|
||||
|
||||
def test_i_can_find_form_with_label(self):
|
||||
"""Test that find_form works when form has fields."""
|
||||
test_app, rt = fast_app(default_hdrs=False)
|
||||
client = MyTestClient(test_app)
|
||||
|
||||
@rt('/')
|
||||
def get():
|
||||
return '''
|
||||
<form>
|
||||
<label for="username">Username</label>
|
||||
<input type="text" name="username" placeholder="Username">
|
||||
<input type="password" name="password" placeholder="Password">
|
||||
<button type="submit">Sign In</button>
|
||||
</form>
|
||||
'''
|
||||
|
||||
form = client.open("/").find_form(fields=["Username", "password"])
|
||||
|
||||
assert form is not None
|
||||
assert isinstance(form, TestableForm)
|
||||
|
||||
def test_i_can_find_form_with_one_field(self):
|
||||
"""Test that find_form works when form has at least one field."""
|
||||
test_app, rt = fast_app(default_hdrs=False)
|
||||
client = MyTestClient(test_app)
|
||||
|
||||
@rt('/')
|
||||
def get():
|
||||
return '''
|
||||
<form>
|
||||
<input type="text" name="username" placeholder="Username">
|
||||
<input type="password" name="password" placeholder="Password">
|
||||
<button type="submit">Sign In</button>
|
||||
</form>
|
||||
'''
|
||||
|
||||
form = client.open("/").find_form(fields=["username"])
|
||||
|
||||
assert form is not None
|
||||
assert isinstance(form, TestableForm)
|
||||
|
||||
def test_i_cannot_find_element_when_none_exists(self):
|
||||
"""Test that find_form raises AssertionError when no form found."""
|
||||
test_app, rt = fast_app(default_hdrs=False)
|
||||
client = MyTestClient(test_app)
|
||||
|
||||
@rt('/')
|
||||
def get():
|
||||
return '<div class="container"><p>Content</p></div>'
|
||||
|
||||
with pytest.raises(AssertionError) as exc_info:
|
||||
client.open("/").find_form()
|
||||
|
||||
assert "No form found" in str(exc_info.value)
|
||||
|
||||
def test_i_cannot_call_find_form_without_opening_page(self):
|
||||
"""Test that find_form raises ValueError if no page has been opened."""
|
||||
test_app, rt = fast_app(default_hdrs=False)
|
||||
client = MyTestClient(test_app)
|
||||
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
client.find_form()
|
||||
|
||||
assert str(exc_info.value) == "No page content available. Call open() before find_form()."
|
||||
|
||||
def test_cannot_find_form_when_multiple_exist(self):
|
||||
"""Test that find_form raises AssertionError when multiple forms match."""
|
||||
test_app, rt = fast_app(default_hdrs=False)
|
||||
client = MyTestClient(test_app)
|
||||
|
||||
@rt('/')
|
||||
def get():
|
||||
return '''
|
||||
<div class="container">
|
||||
<form></form>
|
||||
<form></form>
|
||||
</div>
|
||||
'''
|
||||
|
||||
with pytest.raises(AssertionError) as exc_info:
|
||||
client.open("/").find_form()
|
||||
|
||||
error_message = str(exc_info.value)
|
||||
assert "Found 2 forms (with the specified fields). Expected exactly 1." in error_message
|
||||
|
||||
def test_cannot_find_form_with_fields_when_multiple_exist(self):
|
||||
"""Test that find_form raises AssertionError when multiple forms match."""
|
||||
test_app, rt = fast_app(default_hdrs=False)
|
||||
client = MyTestClient(test_app)
|
||||
|
||||
@rt('/')
|
||||
def get():
|
||||
return '''
|
||||
<div class="container">
|
||||
<form>
|
||||
<input type="text" name="username" placeholder="Username">
|
||||
<input type="password" name="password" placeholder="Password">
|
||||
<button type="submit">Sign In</button>
|
||||
</form>
|
||||
<form>
|
||||
<input type="text" name="username" placeholder="Username">
|
||||
<input type="password" name="password" placeholder="Password">
|
||||
<button type="submit">Sign In</button>
|
||||
</form>
|
||||
</div>
|
||||
'''
|
||||
|
||||
with pytest.raises(AssertionError) as exc_info:
|
||||
client.open("/").find_form(fields=["username", "password"])
|
||||
|
||||
error_message = str(exc_info.value)
|
||||
assert "Found 2 forms (with the specified fields). Expected exactly 1." in error_message
|
||||
|
||||
922
tests/testclient/test_testable_form.py
Normal file
922
tests/testclient/test_testable_form.py
Normal file
@@ -0,0 +1,922 @@
|
||||
import pytest
|
||||
from fasthtml.fastapp import fast_app
|
||||
|
||||
from myfasthtml.core.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><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'])}"
|
||||
|
||||
|
||||
class TestMyTestClientFill:
|
||||
def test_i_can_fill_form_using_input_name(self, mock_client):
|
||||
"""
|
||||
I can fill using the input name
|
||||
"""
|
||||
html = '<form><label for="uid">Username</label><input id="uid" name="username" /></form>'
|
||||
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><label for="uid">Username</label><input id="uid" name="username" /></form>'
|
||||
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><label for="uid">Username</label><input id="uid" name="username" /></form>'
|
||||
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 = '''
|
||||
<form action="/submit" method="post">
|
||||
<input type="text" name="username" value="john_doe" />
|
||||
<input type="password" name="password" value="secret123" />
|
||||
</form>
|
||||
'''
|
||||
|
||||
# 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 = '''
|
||||
<form action="/search" method="get">
|
||||
<input type="text" name="q" value="python" />
|
||||
<input type="text" name="page" value="1" />
|
||||
</form>
|
||||
'''
|
||||
|
||||
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 = '''
|
||||
<form action="/submit">
|
||||
<input type="text" name="data" value="test" />
|
||||
</form>
|
||||
'''
|
||||
|
||||
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 = '''
|
||||
<form hx-post="/htmx-submit">
|
||||
<input type="text" name="username" value="alice" />
|
||||
</form>
|
||||
'''
|
||||
|
||||
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 = '''
|
||||
<form hx-get="/htmx-search">
|
||||
<input type="text" name="query" value="fasthtml" />
|
||||
</form>
|
||||
'''
|
||||
|
||||
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 = '''
|
||||
<form action="/login" method="post">
|
||||
<input type="text" name="username" value="" />
|
||||
<input type="password" name="password" value="" />
|
||||
</form>
|
||||
'''
|
||||
|
||||
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 = '''
|
||||
<form action="/register" method="post">
|
||||
<input type="text" name="username" value="charlie" />
|
||||
<input type="number" name="age" value="30" />
|
||||
<input type="checkbox" name="newsletter" checked />
|
||||
<select name="country">
|
||||
<option value="US" selected>USA</option>
|
||||
<option value="FR">France</option>
|
||||
</select>
|
||||
</form>
|
||||
'''
|
||||
|
||||
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 = '''
|
||||
<form method="post">
|
||||
<input type="text" name="data" value="test" />
|
||||
</form>
|
||||
'''
|
||||
|
||||
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 = '''
|
||||
<form action="" method="post">
|
||||
<input type="text" name="data" value="test" />
|
||||
</form>
|
||||
'''
|
||||
|
||||
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 = '''
|
||||
<form action="/submit" method="PoSt">
|
||||
<input type="text" name="data" value="test" />
|
||||
</form>
|
||||
'''
|
||||
|
||||
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 = '''
|
||||
<form action="/classic" method="post" hx-post="/htmx">
|
||||
<input type="text" name="data" value="test" />
|
||||
</form>
|
||||
'''
|
||||
|
||||
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 = '<form action="/empty" method="post"></form>'
|
||||
|
||||
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"
|
||||
Reference in New Issue
Block a user