Working on LoginPage tests
This commit is contained in:
6
Makefile
6
Makefile
@@ -14,5 +14,9 @@ clean-build: clean-package
|
|||||||
find . -name "*.pyc" -exec rm -f {} +
|
find . -name "*.pyc" -exec rm -f {} +
|
||||||
find . -name "*.pyo" -exec rm -f {} +
|
find . -name "*.pyo" -exec rm -f {} +
|
||||||
|
|
||||||
|
clean-tests:
|
||||||
|
rm -rf tests/.sesskey
|
||||||
|
rm -rf tests/Users.db
|
||||||
|
|
||||||
# Alias to clean everything
|
# Alias to clean everything
|
||||||
clean: clean-build
|
clean: clean-build clean-tests
|
||||||
@@ -18,7 +18,7 @@ from ..auth.utils import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def setup_auth_routes(app, rt, mount_auth_app=True):
|
def setup_auth_routes(app, rt, mount_auth_app=True, sqlite_db_path="Users.db"):
|
||||||
"""
|
"""
|
||||||
Setup all authentication and protected routes.
|
Setup all authentication and protected routes.
|
||||||
|
|
||||||
@@ -26,6 +26,7 @@ def setup_auth_routes(app, rt, mount_auth_app=True):
|
|||||||
app: FastHTML application instance
|
app: FastHTML application instance
|
||||||
rt: Route decorator from FastHTML
|
rt: Route decorator from FastHTML
|
||||||
mount_auth_app: Whether to mount the auth FastApi API routes
|
mount_auth_app: Whether to mount the auth FastApi API routes
|
||||||
|
sqlite_db_path: by default, create a new SQLite database at this path
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
@@ -187,7 +188,7 @@ def setup_auth_routes(app, rt, mount_auth_app=True):
|
|||||||
|
|
||||||
def mount_auth_fastapi_api():
|
def mount_auth_fastapi_api():
|
||||||
# Mount FastAPI auth backend
|
# Mount FastAPI auth backend
|
||||||
auth_api = create_auth_app_for_sqlite("Users.db", "jwt-secret-to-change")
|
auth_api = create_auth_app_for_sqlite(sqlite_db_path, "jwt-secret-to-change")
|
||||||
app.mount("/auth", auth_api)
|
app.mount("/auth", auth_api)
|
||||||
|
|
||||||
if mount_auth_app:
|
if mount_auth_app:
|
||||||
|
|||||||
@@ -27,8 +27,10 @@ DEFAULT_SKIP_PATTERNS = [
|
|||||||
r'.*\.css',
|
r'.*\.css',
|
||||||
r'.*\.js',
|
r'.*\.js',
|
||||||
'/login',
|
'/login',
|
||||||
'/login2',
|
'/login-p',
|
||||||
'/register',
|
'/register',
|
||||||
|
'/register-p',
|
||||||
|
'/logout',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import dataclasses
|
|||||||
import json
|
import json
|
||||||
import uuid
|
import uuid
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
from typing import Self
|
||||||
|
|
||||||
from bs4 import BeautifulSoup, Tag
|
from bs4 import BeautifulSoup, Tag
|
||||||
from fastcore.xml import FT, to_xml
|
from fastcore.xml import FT, to_xml
|
||||||
@@ -132,7 +133,10 @@ class TestableElement:
|
|||||||
|
|
||||||
def _support_htmx(self):
|
def _support_htmx(self):
|
||||||
"""Check if the element supports HTMX."""
|
"""Check if the element supports HTMX."""
|
||||||
return self.ft.has_attr('hx_get') or self.ft.has_attr('hx_post')
|
return ('hx_get' in self.ft.attrs or
|
||||||
|
'hx-get' in self.ft.attrs or
|
||||||
|
'hx_post' in self.ft.attrs or
|
||||||
|
'hx-post' in self.ft.attrs)
|
||||||
|
|
||||||
|
|
||||||
class TestableForm(TestableElement):
|
class TestableForm(TestableElement):
|
||||||
@@ -149,7 +153,7 @@ class TestableForm(TestableElement):
|
|||||||
source: The source HTML string containing a form.
|
source: The source HTML string containing a form.
|
||||||
"""
|
"""
|
||||||
super().__init__(client, source)
|
super().__init__(client, source)
|
||||||
self.form = BeautifulSoup(source, 'html.parser').find('form')
|
self.form = BeautifulSoup(self.html_fragment, 'html.parser').find('form')
|
||||||
self.fields_mapping = {} # link between the input label and the input name
|
self.fields_mapping = {} # link between the input label and the input name
|
||||||
self.fields = {} # field name; field value
|
self.fields = {} # field name; field value
|
||||||
self.select_fields = {} # list of possible options for 'select' input fields
|
self.select_fields = {} # list of possible options for 'select' input fields
|
||||||
@@ -236,7 +240,7 @@ class TestableForm(TestableElement):
|
|||||||
elif options:
|
elif options:
|
||||||
self.fields[name] = options[0]['value']
|
self.fields[name] = options[0]['value']
|
||||||
|
|
||||||
def fill_form(self, **kwargs):
|
def fill(self, **kwargs):
|
||||||
"""
|
"""
|
||||||
Fill the form with the given data.
|
Fill the form with the given data.
|
||||||
|
|
||||||
@@ -244,16 +248,55 @@ class TestableForm(TestableElement):
|
|||||||
**kwargs: Field names and their values to fill in the form.
|
**kwargs: Field names and their values to fill in the form.
|
||||||
"""
|
"""
|
||||||
for name, value in kwargs.items():
|
for name, value in kwargs.items():
|
||||||
self.fields[name] = value
|
field_name = self.translate(name)
|
||||||
|
if field_name not in self.fields:
|
||||||
|
raise ValueError(f"Invalid field name '{name}'.")
|
||||||
|
self.fields[self.translate(name)] = value
|
||||||
|
|
||||||
def submit(self):
|
def submit(self):
|
||||||
"""
|
"""
|
||||||
Submit the form.
|
Submit the form.
|
||||||
|
|
||||||
|
This method handles both HTMX-enabled forms and classic HTML form submissions:
|
||||||
|
- If the form supports HTMX (has hx_post, hx_get, etc.), uses HTMX request
|
||||||
|
- Otherwise, simulates a classic browser form submission using the form's
|
||||||
|
action and method attributes
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The response from the form submission.
|
The response from the form submission.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If the form has no action attribute for classic submission.
|
||||||
"""
|
"""
|
||||||
return self._send_htmx_request(data=self.fields)
|
# Check if the form supports HTMX
|
||||||
|
if self._support_htmx():
|
||||||
|
return self._send_htmx_request(data=self.fields)
|
||||||
|
|
||||||
|
# Classic form submission
|
||||||
|
action = self.form.get('action')
|
||||||
|
if not action or action.strip() == '':
|
||||||
|
raise ValueError(
|
||||||
|
"The form has no 'action' attribute. "
|
||||||
|
"Cannot submit a classic form without a target URL."
|
||||||
|
)
|
||||||
|
|
||||||
|
method = self.form.get('method', 'post').upper()
|
||||||
|
|
||||||
|
# Prepare headers for classic form submission
|
||||||
|
headers = {
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Send the request via the client
|
||||||
|
return self.client.send_request(
|
||||||
|
method=method,
|
||||||
|
url=action,
|
||||||
|
headers=headers,
|
||||||
|
data=self.fields
|
||||||
|
)
|
||||||
|
|
||||||
|
def translate(self, field):
|
||||||
|
return self.fields_mapping.get(field, field)
|
||||||
|
|
||||||
def _update_fields_mapping(self):
|
def _update_fields_mapping(self):
|
||||||
"""
|
"""
|
||||||
@@ -459,7 +502,7 @@ class MyTestClient:
|
|||||||
# make sure that the commands are mounted
|
# make sure that the commands are mounted
|
||||||
mount_commands(self.app)
|
mount_commands(self.app)
|
||||||
|
|
||||||
def open(self, path: str):
|
def open(self, path: str) -> Self:
|
||||||
"""
|
"""
|
||||||
Open a page and store its content for subsequent assertions.
|
Open a page and store its content for subsequent assertions.
|
||||||
|
|
||||||
@@ -483,7 +526,9 @@ class MyTestClient:
|
|||||||
return self
|
return self
|
||||||
|
|
||||||
def send_request(self, method: str, url: str, headers: dict = None, data=None, json_data=None):
|
def send_request(self, method: str, url: str, headers: dict = None, data=None, json_data=None):
|
||||||
json_data['session'] = self._session
|
if json_data is not None:
|
||||||
|
json_data['session'] = self._session
|
||||||
|
|
||||||
res = self.client.request(
|
res = self.client.request(
|
||||||
method,
|
method,
|
||||||
url,
|
url,
|
||||||
@@ -500,7 +545,7 @@ class MyTestClient:
|
|||||||
self.set_content(res.text)
|
self.set_content(res.text)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def should_see(self, text: str):
|
def should_see(self, text: str) -> Self:
|
||||||
"""
|
"""
|
||||||
Assert that the given text is present in the visible page content.
|
Assert that the given text is present in the visible page content.
|
||||||
|
|
||||||
@@ -517,6 +562,11 @@ class MyTestClient:
|
|||||||
AssertionError: If the text is not found in the page content.
|
AssertionError: If the text is not found in the page content.
|
||||||
ValueError: If no page has been opened yet.
|
ValueError: If no page has been opened yet.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
def clean_text(txt):
|
||||||
|
return "\n".join(line for line in txt.splitlines() if line.strip())
|
||||||
|
|
||||||
|
|
||||||
if self._content is None:
|
if self._content is None:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
"No page content available. Call open() before should_see()."
|
"No page content available. Call open() before should_see()."
|
||||||
@@ -527,7 +577,7 @@ class MyTestClient:
|
|||||||
if text not in visible_text:
|
if text not in visible_text:
|
||||||
# Provide a snippet of the actual content for debugging
|
# Provide a snippet of the actual content for debugging
|
||||||
snippet_length = 200
|
snippet_length = 200
|
||||||
content_snippet = (
|
content_snippet = clean_text(
|
||||||
visible_text[:snippet_length] + "..."
|
visible_text[:snippet_length] + "..."
|
||||||
if len(visible_text) > snippet_length
|
if len(visible_text) > snippet_length
|
||||||
else visible_text
|
else visible_text
|
||||||
@@ -539,7 +589,7 @@ class MyTestClient:
|
|||||||
|
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def should_not_see(self, text: str):
|
def should_not_see(self, text: str) -> Self:
|
||||||
"""
|
"""
|
||||||
Assert that the given text is NOT present in the visible page content.
|
Assert that the given text is NOT present in the visible page content.
|
||||||
|
|
||||||
@@ -582,7 +632,7 @@ class MyTestClient:
|
|||||||
|
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def find_element(self, selector: str):
|
def find_element(self, selector: str) -> TestableElement:
|
||||||
"""
|
"""
|
||||||
Find a single HTML element using a CSS selector.
|
Find a single HTML element using a CSS selector.
|
||||||
|
|
||||||
@@ -621,7 +671,7 @@ 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):
|
def find_form(self, fields: list = None) -> TestableForm:
|
||||||
"""
|
"""
|
||||||
Find a form element in the page content.
|
Find a form element in the page content.
|
||||||
Can provide title of the fields to ease the search
|
Can provide title of the fields to ease the search
|
||||||
@@ -630,22 +680,29 @@ class MyTestClient:
|
|||||||
"""
|
"""
|
||||||
if self._content is None:
|
if self._content is None:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
"No page content available. Call open() before find_element()."
|
"No page content available. Call open() before find_form()."
|
||||||
)
|
)
|
||||||
|
|
||||||
results = self._soup.select("form")
|
results = self._soup.select("form")
|
||||||
if len(results) == 0:
|
if len(results) == 0:
|
||||||
raise AssertionError(
|
raise AssertionError(
|
||||||
f"No element form found."
|
f"No form found."
|
||||||
)
|
)
|
||||||
|
|
||||||
# result = _filter(results, fields)
|
if fields is None:
|
||||||
|
remaining = [TestableForm(self, form) for form in results]
|
||||||
|
else:
|
||||||
|
remaining = []
|
||||||
|
for form in results:
|
||||||
|
testable_form = TestableForm(self, form)
|
||||||
|
if all(testable_form.translate(field) in testable_form.fields for field in fields):
|
||||||
|
remaining.append(testable_form)
|
||||||
|
|
||||||
if len(results) == 1:
|
if len(remaining) == 1:
|
||||||
return TestableForm(self, results[0])
|
return remaining[0]
|
||||||
else:
|
else:
|
||||||
raise AssertionError(
|
raise AssertionError(
|
||||||
f"Found {len(results)} forms (with the specified fields). Expected exactly 1."
|
f"Found {len(remaining)} forms (with the specified fields). Expected exactly 1."
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_content(self) -> str:
|
def get_content(self) -> str:
|
||||||
@@ -657,7 +714,7 @@ class MyTestClient:
|
|||||||
"""
|
"""
|
||||||
return self._content
|
return self._content
|
||||||
|
|
||||||
def set_content(self, content: str):
|
def set_content(self, content: str) -> Self:
|
||||||
"""
|
"""
|
||||||
Set the HTML content and parse it with BeautifulSoup.
|
Set the HTML content and parse it with BeautifulSoup.
|
||||||
|
|
||||||
@@ -666,6 +723,7 @@ class MyTestClient:
|
|||||||
"""
|
"""
|
||||||
self._content = content
|
self._content = content
|
||||||
self._soup = BeautifulSoup(content, 'html.parser')
|
self._soup = BeautifulSoup(content, 'html.parser')
|
||||||
|
return self
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _find_visible_text_element(soup, text: str):
|
def _find_visible_text_element(soup, text: str):
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from fasthtml.fastapp import fast_app
|
from fasthtml.fastapp import fast_app
|
||||||
|
|
||||||
from myfasthtml.auth.routes import setup_auth_routes
|
from myfasthtml.auth.routes import setup_auth_routes
|
||||||
from myfasthtml.auth.utils import create_auth_beforeware
|
from myfasthtml.auth.utils import create_auth_beforeware, register_user
|
||||||
from myfasthtml.core.testclient import MyTestClient
|
from myfasthtml.core.testclient import MyTestClient
|
||||||
|
|
||||||
|
|
||||||
@@ -10,7 +12,7 @@ from myfasthtml.core.testclient import MyTestClient
|
|||||||
def app():
|
def app():
|
||||||
beforeware = create_auth_beforeware()
|
beforeware = create_auth_beforeware()
|
||||||
test_app, test_rt = fast_app(before=beforeware)
|
test_app, test_rt = fast_app(before=beforeware)
|
||||||
setup_auth_routes(test_app, test_rt)
|
setup_auth_routes(test_app, test_rt, mount_auth_app=True, sqlite_db_path="TestUsers.db")
|
||||||
return test_app
|
return test_app
|
||||||
|
|
||||||
|
|
||||||
@@ -24,16 +26,34 @@ def user(app):
|
|||||||
user = MyTestClient(app)
|
user = MyTestClient(app)
|
||||||
return user
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def cleanup():
|
||||||
|
if os.path.exists("TestUsers.db"):
|
||||||
|
os.remove("TestUsers.db")
|
||||||
|
|
||||||
|
|
||||||
def test_i_can_see_login_page(user):
|
def test_i_can_see_login_page(user):
|
||||||
user.open("/login")
|
user.open("/login")
|
||||||
user.should_see("Sign In")
|
user.should_see("Sign In")
|
||||||
user.should_see("Register here")
|
user.should_see("Register here")
|
||||||
|
user.find_form(fields=["Email", "Password"])
|
||||||
|
|
||||||
|
|
||||||
def test_i_cannot_login_with_wrong_credentials(user):
|
def test_i_cannot_login_with_wrong_credentials(user):
|
||||||
user.open("/login")
|
user.open("/login")
|
||||||
user.fill_form({
|
form = user.find_form(fields=["Email", "Password"])
|
||||||
"username": "wrong",
|
form.fill(Email="user@email.com", Password="#Passw0rd")
|
||||||
"password": ""
|
form.submit()
|
||||||
})
|
user.should_see("Invalid email or password. Please try again.")
|
||||||
user.click("button")
|
|
||||||
user.should_see("Invalid credentials")
|
|
||||||
|
def test_i_can_login_with_correct_credentials(user):
|
||||||
|
# create user
|
||||||
|
register_user("user@email.com", "user", "#Passw0rd")
|
||||||
|
|
||||||
|
user.open("/login")
|
||||||
|
form = user.find_form(fields=["Email", "Password"])
|
||||||
|
form.fill(Email="user@email.com", Password="#Passw0rd")
|
||||||
|
form.submit()
|
||||||
|
user.should_see("You are now logged in")
|
||||||
|
|||||||
@@ -2,43 +2,45 @@ import pytest
|
|||||||
from fasthtml.components import Div
|
from fasthtml.components import Div
|
||||||
from fasthtml.fastapp import fast_app
|
from fasthtml.fastapp import fast_app
|
||||||
|
|
||||||
from myfasthtml.core.testclient import MyTestClient, TestableElement
|
from myfasthtml.core.testclient import MyTestClient, TestableElement, TestableForm
|
||||||
|
|
||||||
|
|
||||||
def test_i_can_open_a_page():
|
class TestMyTestClientOpen:
|
||||||
test_app, rt = fast_app(default_hdrs=False)
|
|
||||||
client = MyTestClient(test_app)
|
|
||||||
|
|
||||||
@rt('/')
|
def test_i_can_open_a_page(self):
|
||||||
def get(): return "hello world"
|
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"
|
||||||
|
|
||||||
client.open("/")
|
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'
|
||||||
|
|
||||||
assert client.get_content() == "hello world"
|
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'"
|
||||||
|
|
||||||
|
|
||||||
def test_i_can_open_a_page_when_html():
|
class TestMyTestClientShouldSee:
|
||||||
test_app, rt = fast_app(default_hdrs=False)
|
def test_i_can_see_text_in_plain_response(self):
|
||||||
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 that should_see() works with plain text responses."""
|
||||||
test_app, rt = fast_app(default_hdrs=False)
|
test_app, rt = fast_app(default_hdrs=False)
|
||||||
client = MyTestClient(test_app)
|
client = MyTestClient(test_app)
|
||||||
@@ -48,279 +50,435 @@ def test_i_can_see_text_in_plain_response():
|
|||||||
return "hello world"
|
return "hello world"
|
||||||
|
|
||||||
client.open("/").should_see("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 test_i_can_see_text_in_html_response(self):
|
||||||
def get():
|
"""Test that should_see() extracts visible text from HTML responses."""
|
||||||
return "<html><body><h1>Welcome</h1><p>This is a test</p></body></html>"
|
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")
|
||||||
|
|
||||||
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)
|
||||||
def test_i_can_see_text_ignoring_html_tags():
|
client = MyTestClient(test_app)
|
||||||
"""Test that should_see() searches in visible text only, not in HTML tags."""
|
|
||||||
test_app, rt = fast_app(default_hdrs=False)
|
@rt('/')
|
||||||
client = MyTestClient(test_app)
|
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)
|
||||||
|
|
||||||
@rt('/')
|
def test_i_cannot_see_text_that_is_not_present(self):
|
||||||
def get():
|
"""Test that should_see() raises AssertionError when text is not found."""
|
||||||
return '<div class="container">Content</div>'
|
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)
|
||||||
|
|
||||||
# Should find the visible text
|
def test_i_cannot_call_should_see_without_opening_page(self):
|
||||||
client.open("/").should_see("Content")
|
"""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()."
|
||||||
|
|
||||||
# Should NOT find text that's only in attributes/tags
|
def test_i_can_verify_text_is_not_present(self):
|
||||||
with pytest.raises(AssertionError) as exc_info:
|
"""Test that should_not_see() works when text is absent."""
|
||||||
client.should_see("container")
|
test_app, rt = fast_app(default_hdrs=False)
|
||||||
|
client = MyTestClient(test_app)
|
||||||
|
|
||||||
|
@rt('/')
|
||||||
|
def get():
|
||||||
|
return "hello world"
|
||||||
|
|
||||||
|
client.open("/").should_not_see("goodbye")
|
||||||
|
|
||||||
assert "Expected to see 'container' in page content but it was not found" in str(exc_info.value)
|
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)
|
||||||
def test_i_cannot_see_text_that_is_not_present():
|
client = MyTestClient(test_app)
|
||||||
"""Test that should_see() raises AssertionError when text is not found."""
|
|
||||||
test_app, rt = fast_app(default_hdrs=False)
|
@rt('/')
|
||||||
client = MyTestClient(test_app)
|
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
|
||||||
|
|
||||||
@rt('/')
|
def test_i_cannot_call_should_not_see_without_opening_page(self):
|
||||||
def get():
|
"""Test that should_not_see() raises ValueError if no page has been opened."""
|
||||||
return "hello world"
|
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()."
|
||||||
|
|
||||||
with pytest.raises(AssertionError) as exc_info:
|
def test_i_can_chain_multiple_assertions(self):
|
||||||
client.open("/").should_see("goodbye")
|
"""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")
|
||||||
|
|
||||||
assert "Expected to see 'goodbye' in page content but it was not found" in str(exc_info.value)
|
def test_i_can_see_element_context_when_text_should_not_be_seen(self):
|
||||||
assert "hello world" in str(exc_info.value)
|
"""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)
|
||||||
def test_i_cannot_call_should_see_without_opening_page():
|
|
||||||
"""Test that should_see() raises ValueError if no page has been opened."""
|
@rt('/')
|
||||||
test_app, rt = fast_app(default_hdrs=False)
|
def get():
|
||||||
client = MyTestClient(test_app)
|
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
|
||||||
|
|
||||||
with pytest.raises(ValueError) as exc_info:
|
def test_i_can_configure_parent_levels_in_constructor(self):
|
||||||
client.should_see("anything")
|
"""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
|
||||||
|
|
||||||
assert str(exc_info.value) == "No page content available. Call open() before should_see()."
|
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)
|
||||||
def test_i_can_verify_text_is_not_present():
|
client = MyTestClient(test_app)
|
||||||
"""Test that should_not_see() works when text is absent."""
|
|
||||||
test_app, rt = fast_app(default_hdrs=False)
|
@rt('/')
|
||||||
client = MyTestClient(test_app)
|
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
|
||||||
|
|
||||||
@rt('/')
|
def test_i_can_find_fragmented_text_across_tags(self):
|
||||||
def get():
|
"""Test that text fragmented across multiple tags is correctly found."""
|
||||||
return "hello world"
|
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
|
||||||
|
|
||||||
client.open("/").should_not_see("goodbye")
|
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)
|
||||||
def test_i_cannot_use_should_not_see_when_text_is_present():
|
client = MyTestClient(test_app)
|
||||||
"""Test that should_not_see() raises AssertionError when text is found."""
|
|
||||||
test_app, rt = fast_app(default_hdrs=False)
|
@rt('/')
|
||||||
client = MyTestClient(test_app)
|
def get():
|
||||||
|
return '<div class="error message" title="error info">Success</div>'
|
||||||
@rt('/')
|
|
||||||
def get():
|
# "error" is in attributes but not in visible text
|
||||||
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")
|
client.open("/").should_not_see("error")
|
||||||
|
|
||||||
|
# "Success" is in visible text
|
||||||
|
with pytest.raises(AssertionError):
|
||||||
|
client.should_not_see("Success")
|
||||||
|
|
||||||
|
|
||||||
|
class TestMyTestClientFindElement:
|
||||||
|
|
||||||
error_message = str(exc_info.value)
|
@pytest.mark.parametrize("selector,expected_tag", [
|
||||||
assert '<p>error</p>' in error_message
|
("#unique-id", '<div class="main wrapper"'),
|
||||||
assert '<div class="container">' in error_message
|
(".home-link", '<a class="link home-link"'),
|
||||||
assert '<div class="wrapper">' in error_message
|
("div > div", '<div class="content"'),
|
||||||
|
("div span", "<span"),
|
||||||
|
("[data-type]", '<a class="link home-link"'),
|
||||||
def test_i_can_find_text_in_nested_elements():
|
('[data-type="navigation"]', '<a class="link home-link"'),
|
||||||
"""Test that the smallest element containing the text is found in nested structures."""
|
('[class~="link"]', '<a class="link home-link"'),
|
||||||
test_app, rt = fast_app(default_hdrs=False)
|
('[href^="/home"]', '<a class="link home-link"'),
|
||||||
client = MyTestClient(test_app)
|
('[href$="about"]', '<a href="/about">'),
|
||||||
|
('[data-author*="john"]', '<a class="link home-link"'),
|
||||||
@rt('/')
|
])
|
||||||
def get():
|
def test_i_can_find_element(self, selector, expected_tag):
|
||||||
return '<div><section><article><p class="target">nested text</p></article></section></div>'
|
"""Test that find_element works with various CSS selectors."""
|
||||||
|
test_app, rt = fast_app(default_hdrs=False)
|
||||||
with pytest.raises(AssertionError) as exc_info:
|
client = MyTestClient(test_app)
|
||||||
client.open("/").should_not_see("nested text")
|
|
||||||
|
@rt('/')
|
||||||
error_message = str(exc_info.value)
|
def get():
|
||||||
# Should find the <p> element, not the outer <div>
|
return '''
|
||||||
assert '<p class="target">nested text</p>' in error_message
|
<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>
|
||||||
def test_i_can_find_fragmented_text_across_tags():
|
<div class="content">
|
||||||
"""Test that text fragmented across multiple tags is correctly found."""
|
<span class="text">Content</span>
|
||||||
test_app, rt = fast_app(default_hdrs=False)
|
</div>
|
||||||
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>
|
||||||
</div>
|
'''
|
||||||
'''
|
|
||||||
|
element = client.open("/").find_element(selector)
|
||||||
element = client.open("/").find_element(selector)
|
|
||||||
|
assert element is not None
|
||||||
assert element is not None
|
assert isinstance(element, TestableElement)
|
||||||
assert isinstance(element, TestableElement)
|
assert expected_tag in element.html_fragment
|
||||||
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()."
|
||||||
|
|
||||||
|
|
||||||
def test_i_cannot_find_element_when_none_exists():
|
class TestMyTestClientFindForm:
|
||||||
"""Test that find_element raises AssertionError when no element matches."""
|
def test_i_can_find_form(self):
|
||||||
test_app, rt = fast_app(default_hdrs=False)
|
"""Test that find_form works fo simple form."""
|
||||||
client = MyTestClient(test_app)
|
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
|
||||||
|
|
||||||
@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()."
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import pytest
|
import pytest
|
||||||
|
from fasthtml.fastapp import fast_app
|
||||||
|
|
||||||
from myfasthtml.core.testclient import TestableForm
|
from myfasthtml.core.testclient import TestableForm, MyTestClient
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
@@ -583,3 +584,339 @@ class TestableFormUpdateFieldValues:
|
|||||||
f"Expected {{'flag': True}}, got {form.fields}"
|
f"Expected {{'flag': True}}, got {form.fields}"
|
||||||
assert isinstance(form.fields["flag"], bool), \
|
assert isinstance(form.fields["flag"], bool), \
|
||||||
f"Expected bool type, got {type(form.fields['flag'])}"
|
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