Working on LoginPage tests

This commit is contained in:
2025-10-26 22:45:34 +01:00
parent b98e52378e
commit 09d012d065
7 changed files with 900 additions and 320 deletions

View File

@@ -2,6 +2,7 @@ import dataclasses
import json
import uuid
from dataclasses import dataclass
from typing import Self
from bs4 import BeautifulSoup, Tag
from fastcore.xml import FT, to_xml
@@ -132,7 +133,10 @@ class TestableElement:
def _support_htmx(self):
"""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):
@@ -149,7 +153,7 @@ class TestableForm(TestableElement):
source: The source HTML string containing a form.
"""
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 = {} # field name; field value
self.select_fields = {} # list of possible options for 'select' input fields
@@ -236,7 +240,7 @@ class TestableForm(TestableElement):
elif options:
self.fields[name] = options[0]['value']
def fill_form(self, **kwargs):
def fill(self, **kwargs):
"""
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.
"""
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):
"""
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:
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):
"""
@@ -459,7 +502,7 @@ class MyTestClient:
# make sure that the commands are mounted
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.
@@ -483,7 +526,9 @@ class MyTestClient:
return self
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(
method,
url,
@@ -500,7 +545,7 @@ class MyTestClient:
self.set_content(res.text)
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.
@@ -517,6 +562,11 @@ class MyTestClient:
AssertionError: If the text is not found in the page content.
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:
raise ValueError(
"No page content available. Call open() before should_see()."
@@ -527,7 +577,7 @@ class MyTestClient:
if text not in visible_text:
# Provide a snippet of the actual content for debugging
snippet_length = 200
content_snippet = (
content_snippet = clean_text(
visible_text[:snippet_length] + "..."
if len(visible_text) > snippet_length
else visible_text
@@ -539,7 +589,7 @@ class MyTestClient:
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.
@@ -582,7 +632,7 @@ class MyTestClient:
return self
def find_element(self, selector: str):
def find_element(self, selector: str) -> TestableElement:
"""
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."
)
def find_form(self, fields: list = None):
def find_form(self, fields: list = None) -> TestableForm:
"""
Find a form element in the page content.
Can provide title of the fields to ease the search
@@ -630,22 +680,29 @@ class MyTestClient:
"""
if self._content is None:
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")
if len(results) == 0:
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:
return TestableForm(self, results[0])
if len(remaining) == 1:
return remaining[0]
else:
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:
@@ -657,7 +714,7 @@ class MyTestClient:
"""
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.
@@ -666,6 +723,7 @@ class MyTestClient:
"""
self._content = content
self._soup = BeautifulSoup(content, 'html.parser')
return self
@staticmethod
def _find_visible_text_element(soup, text: str):