Added Commands Management
This commit is contained in:
25
tests/test_commands.py
Normal file
25
tests/test_commands.py
Normal file
@@ -0,0 +1,25 @@
|
||||
import pytest
|
||||
|
||||
from myfasthtml.core.commands import Command, CommandsManager
|
||||
|
||||
|
||||
def callback():
|
||||
return "Hello World"
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def test_reset_command_manager():
|
||||
CommandsManager.reset()
|
||||
|
||||
|
||||
def test_i_can_create_a_command_with_no_params():
|
||||
command = Command('test', 'Command description', callback)
|
||||
assert command.id is not None
|
||||
assert command.name == 'test'
|
||||
assert command.description == 'Command description'
|
||||
assert command.execute() == "Hello World"
|
||||
|
||||
|
||||
def test_command_are_registered():
|
||||
command = Command('test', 'Command description', callback)
|
||||
assert CommandsManager.commands.get(str(command.id)) is command
|
||||
53
tests/test_integration_commands.py
Normal file
53
tests/test_integration_commands.py
Normal file
@@ -0,0 +1,53 @@
|
||||
import pytest
|
||||
from fasthtml.fastapp import fast_app
|
||||
|
||||
from myfasthtml.controls.button import mk_button
|
||||
from myfasthtml.core.commands import Command, CommandsManager
|
||||
from myfasthtml.core.testclient import MyTestClient, TestableElement
|
||||
|
||||
|
||||
def new_value(value):
|
||||
return value
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def user():
|
||||
test_app, rt = fast_app(default_hdrs=False)
|
||||
user = MyTestClient(test_app)
|
||||
return user
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def rt(user):
|
||||
return user.app.route
|
||||
|
||||
|
||||
def test_i_can_trigger_a_command(user):
|
||||
command = Command('test', 'TestingCommand', new_value, "this is my new value")
|
||||
testable = TestableElement(user, mk_button('button', command))
|
||||
testable.click()
|
||||
assert user.get_content() == "this is my new value"
|
||||
|
||||
|
||||
def test_error_is_raised_when_command_is_not_found(user):
|
||||
command = Command('test', 'TestingCommand', new_value, "this is my new value")
|
||||
CommandsManager.reset()
|
||||
testable = TestableElement(user, mk_button('button', command))
|
||||
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
testable.click()
|
||||
|
||||
assert "not found." in str(exc_info.value)
|
||||
|
||||
|
||||
def test_i_can_play_a_complex_scenario(user, rt):
|
||||
command = Command('test', 'TestingCommand', new_value, "this is my new value")
|
||||
|
||||
@rt('/')
|
||||
def get(): return mk_button('button', command)
|
||||
|
||||
user.open("/")
|
||||
user.should_see("button")
|
||||
|
||||
user.find_element("button").click()
|
||||
user.should_see("this is my new value")
|
||||
326
tests/test_mytestclient.py
Normal file
326
tests/test_mytestclient.py
Normal file
@@ -0,0 +1,326 @@
|
||||
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()."
|
||||
69
tests/test_testable_element.py
Normal file
69
tests/test_testable_element.py
Normal file
@@ -0,0 +1,69 @@
|
||||
import pytest
|
||||
from fasthtml.components import Div
|
||||
from fasthtml.fastapp import fast_app
|
||||
|
||||
from myfasthtml.core.testclient import MyTestClient, TestableElement, MyFT
|
||||
|
||||
|
||||
def test_i_can_create_testable_element_from_ft():
|
||||
ft = Div("hello world", id="test")
|
||||
testable_element = TestableElement(None, ft)
|
||||
|
||||
assert testable_element.ft == ft
|
||||
assert testable_element.html_fragment == '<div id="test">hello world</div>'
|
||||
|
||||
|
||||
def test_i_can_create_testable_element_from_str():
|
||||
ft = '<div id="test">hello world</div>'
|
||||
testable_element = TestableElement(None, ft)
|
||||
|
||||
assert testable_element.ft == MyFT('div', {'id': 'test'})
|
||||
assert testable_element.html_fragment == '<div id="test">hello world</div>'
|
||||
|
||||
|
||||
def test_i_can_create_testable_element_from_beautifulsoup_element():
|
||||
ft = '<div id="test">hello world</div>'
|
||||
from bs4 import BeautifulSoup
|
||||
tag = BeautifulSoup(ft, 'html.parser').div
|
||||
testable_element = TestableElement(None, tag)
|
||||
|
||||
assert testable_element.ft == MyFT('div', {'id': 'test'})
|
||||
assert testable_element.html_fragment == '<div id="test">hello world</div>'
|
||||
|
||||
|
||||
def test_i_cannot_create_testable_element_from_other_type():
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
TestableElement(None, 123)
|
||||
|
||||
assert str(exc_info.value) == "Invalid source '123' for TestableElement."
|
||||
|
||||
|
||||
def test_i_can_click():
|
||||
test_app, rt = fast_app(default_hdrs=False)
|
||||
client = MyTestClient(test_app)
|
||||
ft = Div(
|
||||
hx_post="/search",
|
||||
hx_target="#results",
|
||||
hx_swap="innerHTML",
|
||||
hx_vals='{"attr": "attr_value"}'
|
||||
)
|
||||
testable_element = TestableElement(client, ft)
|
||||
|
||||
@rt('/search')
|
||||
def post(hx_target: str, hx_swap: str, attr: str): # hx_post is used to the Verb. It's not a parameter
|
||||
return f"received {hx_target=}, {hx_swap=}, {attr=}"
|
||||
|
||||
testable_element.click()
|
||||
assert client.get_content() == "received hx_target='#results', hx_swap='innerHTML', attr='attr_value'"
|
||||
|
||||
|
||||
def test_i_cannot_test_when_not_clickable():
|
||||
test_app, rt = fast_app(default_hdrs=False)
|
||||
client = MyTestClient(test_app)
|
||||
ft = Div("hello world")
|
||||
testable_element = TestableElement(client, ft)
|
||||
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
testable_element.click()
|
||||
|
||||
assert str(exc_info.value) == "The <div> element has no HTMX verb attribute (e.g., hx_get, hx_post) to define a URL."
|
||||
Reference in New Issue
Block a user