summaryrefslogtreecommitdiffstats
path: root/tests
diff options
context:
space:
mode:
Diffstat (limited to 'tests')
-rw-r--r--tests/__init__.py0
-rw-r--r--tests/conftest.py24
-rw-r--r--tests/test_catalog.py88
-rw-r--r--tests/test_component.py315
-rw-r--r--tests/test_html_attrs.py281
-rw-r--r--tests/test_middleware.py152
-rw-r--r--tests/test_render.py992
7 files changed, 1852 insertions, 0 deletions
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/__init__.py
diff --git a/tests/conftest.py b/tests/conftest.py
new file mode 100644
index 0000000..6256373
--- /dev/null
+++ b/tests/conftest.py
@@ -0,0 +1,24 @@
+import pytest
+
+import jinjax
+
+
+@pytest.fixture()
+def folder(tmp_path):
+ d = tmp_path / "components"
+ d.mkdir()
+ return d
+
+
+@pytest.fixture()
+def folder_t(tmp_path):
+ d = tmp_path / "templates"
+ d.mkdir()
+ return d
+
+
+@pytest.fixture()
+def catalog(folder):
+ catalog = jinjax.Catalog(auto_reload=False)
+ catalog.add_folder(folder)
+ return catalog
diff --git a/tests/test_catalog.py b/tests/test_catalog.py
new file mode 100644
index 0000000..d3c4bc0
--- /dev/null
+++ b/tests/test_catalog.py
@@ -0,0 +1,88 @@
+import pytest
+
+import jinjax
+
+
+def test_add_folder_with_default_prefix():
+ catalog = jinjax.Catalog()
+ catalog.add_folder("file_path")
+
+ assert "file_path" in catalog.prefixes[""].searchpath
+
+
+def test_add_folder_with_custom_prefix():
+ catalog = jinjax.Catalog()
+ catalog.add_folder("file_path", prefix="custom")
+
+ assert "file_path" in catalog.prefixes["custom"].searchpath
+
+
+def test_add_folder_with_dirty_prefix():
+ catalog = jinjax.Catalog()
+ catalog.add_folder("file_path", prefix="/custom.")
+
+ assert "/custom." not in catalog.prefixes
+ assert "file_path" in catalog.prefixes["custom"].searchpath
+
+
+def test_add_folders_with_same_prefix():
+ catalog = jinjax.Catalog()
+ catalog.add_folder("file_path1", prefix="custom")
+ catalog.add_folder("file_path2", prefix="custom")
+
+ assert "file_path1" in catalog.prefixes["custom"].searchpath
+ assert "file_path2" in catalog.prefixes["custom"].searchpath
+
+
+def test_add_same_folder_in_same_prefix_does_nothing():
+ catalog = jinjax.Catalog()
+ catalog.add_folder("file_path", prefix="custom")
+ catalog.add_folder("file_path", prefix="custom")
+
+ assert catalog.prefixes["custom"].searchpath.count("file_path") == 1
+
+
+def test_add_module_legacy():
+ class Module:
+ components_path = "legacy_path"
+ prefix = "legacy"
+
+ catalog = jinjax.Catalog()
+ module = Module()
+ catalog.add_module(module)
+
+ assert "legacy_path" in catalog.prefixes["legacy"].searchpath
+
+
+def test_add_module_legacy_with_default_prefix():
+ class Module:
+ components_path = "legacy_path"
+
+ catalog = jinjax.Catalog()
+ module = Module()
+ catalog.add_module(module)
+
+ assert "legacy_path" in catalog.prefixes[""].searchpath
+
+
+def test_add_module_legacy_with_custom_prefix():
+ class Module:
+ components_path = "legacy_path"
+ prefix = "legacy"
+
+ catalog = jinjax.Catalog()
+ module = Module()
+ catalog.add_module(module, prefix="custom")
+
+ assert "legacy" not in catalog.prefixes
+ assert "legacy_path" in catalog.prefixes["custom"].searchpath
+
+
+def test_add_module_fails_with_other_modules():
+ class Module:
+ pass
+
+ catalog = jinjax.Catalog()
+ module = Module()
+ with pytest.raises(AttributeError):
+ catalog.add_module(module)
diff --git a/tests/test_component.py b/tests/test_component.py
new file mode 100644
index 0000000..9961b21
--- /dev/null
+++ b/tests/test_component.py
@@ -0,0 +1,315 @@
+import pytest
+
+from jinjax import Component, DuplicateDefDeclaration, InvalidArgument
+
+
+def test_load_args():
+ com = Component(
+ name="Test.jinja",
+ source='{#def message, lorem=4, ipsum="bar" -#}\n',
+ )
+ assert com.required == ["message"]
+ assert com.optional == {
+ "lorem": 4,
+ "ipsum": "bar",
+ }
+
+
+def test_expression_args():
+ com = Component(
+ name="Test.jinja",
+ source="{#def expr=1 + 2 + 3, a=1 -#}\n",
+ )
+ assert com.required == []
+ assert com.optional == {
+ "expr": 6,
+ "a": 1,
+ }
+
+
+def test_dict_args():
+ com = Component(
+ name="Test.jinja",
+ source="{#def expr={'a': 'b', 'c': 'd'} -#}\n",
+ )
+ assert com.optional == {
+ "expr": {"a": "b", "c": "d"},
+ }
+
+ com = Component(
+ name="Test.jinja",
+ source='{#def a=1, expr={"a": "b", "c": "d"} -#}\n',
+ )
+ assert com.optional == {
+ "a": 1,
+ "expr": {"a": "b", "c": "d"},
+ }
+
+
+def test_lowercase_booleans():
+ com = Component(
+ name="Test.jinja",
+ source="{#def a=false, b=true -#}\n",
+ )
+ assert com.optional == {
+ "a": False,
+ "b": True,
+ }
+
+
+def test_no_args():
+ com = Component(
+ name="Test.jinja",
+ source="\n",
+ )
+ assert com.required == []
+ assert com.optional == {}
+
+
+def test_fails_when_invalid_name():
+ with pytest.raises(InvalidArgument):
+ source = "{#def 000abc -#}\n"
+ co = Component(name="", source=source)
+ print(co.required, co.optional)
+
+
+def test_fails_when_missing_comma_between_args():
+ with pytest.raises(InvalidArgument):
+ source = "{#def lorem ipsum -#}\n"
+ co = Component(name="", source=source)
+ print(co.required, co.optional)
+
+
+def test_fails_when_missing_quotes_arround_default_value():
+ with pytest.raises(InvalidArgument):
+ source = "{#def lorem=ipsum -#}\n"
+ co = Component(name="", source=source)
+ print(co.required, co.optional)
+
+
+def test_fails_when_prop_is_expression():
+ with pytest.raises(InvalidArgument):
+ source = "{#def a-b -#}\n"
+ co = Component(name="", source=source)
+ print(co.required, co.optional)
+
+
+def test_fails_when_extra_comma_between_args():
+ with pytest.raises(InvalidArgument):
+ source = "{#def a, , b -#}\n"
+ co = Component(name="", source=source)
+ print(co.required, co.optional)
+
+
+def test_comma_in_default_value():
+ com = Component(
+ name="Test.jinja",
+ source="{#def a='lorem, ipsum' -#}\n",
+ )
+ assert com.optional == {"a": "lorem, ipsum"}
+
+
+def test_load_assets():
+ com = Component(
+ name="Test.jinja",
+ url_prefix="/static/",
+ source="""
+ {#css a.css, "b.css", c.css -#}
+ {#js a.js, b.js, c.js -#}
+ """,
+ )
+ assert com.css == ["/static/a.css", "/static/b.css", "/static/c.css"]
+ assert com.js == ["/static/a.js", "/static/b.js", "/static/c.js"]
+
+
+def test_no_comma_in_assets_list_is_your_problem():
+ com = Component(
+ name="Test.jinja",
+ source="{#js a.js b.js c.js -#}\n",
+ url_prefix="/static/"
+ )
+ assert com.js == ["/static/a.js b.js c.js"]
+
+
+def test_load_metadata_in_any_order():
+ com = Component(
+ name="Test.jinja",
+ source="""
+ {#css a.css #}
+ {#def lorem, ipsum=4 #}
+ {#js a.js #}
+ """,
+ )
+ assert com.required == ["lorem"]
+ assert com.optional == {"ipsum": 4}
+ assert com.css == ["a.css"]
+ assert com.js == ["a.js"]
+
+
+def test_ignore_metadata_if_not_first():
+ com = Component(
+ name="Test.jinja",
+ source="""
+ I am content
+ {#css a.css #}
+ {#def lorem, ipsum=4 #}
+ {#js a.js #}
+ """,
+ )
+ assert com.required == []
+ assert com.optional == {}
+ assert com.css == []
+ assert com.js == []
+
+
+def test_fail_with_more_than_one_args_declaration():
+ with pytest.raises(DuplicateDefDeclaration):
+ Component(
+ name="Test.jinja",
+ source="""
+ {#def lorem, ipsum=4 #}
+ {#def a, b, c, ipsum="nope" #}
+ """,
+ )
+
+
+def test_merge_repeated_css_or_js_declarations():
+ com = Component(
+ name="Test.jinja",
+ source="""
+ {#css a.css #}
+ {#def lorem, ipsum=4 #}
+ {#css b.css #}
+ {#js a.js #}
+ {#js b.js #}
+ """,
+ )
+ assert com.required == ["lorem"]
+ assert com.optional == {"ipsum": 4}
+ assert com.css == ["a.css", "b.css"]
+ assert com.js == ["a.js", "b.js"]
+
+
+def test_linejump_in_args_decl():
+ com = Component(
+ name="Test.jinja",
+ source='{#def\n message,\n lorem=4,\n ipsum="bar"\n#}\n',
+ )
+ assert com.required == ["message"]
+ assert com.optional == {
+ "lorem": 4,
+ "ipsum": "bar",
+ }
+
+
+def test_global_assets():
+ com = Component(
+ name="Test.jinja",
+ source="""
+ {#css a.css, /static/shared/b.css, http://example.com/cdn.css #}
+ {#js "http://example.com/cdn.js", a.js, /static/shared/b.js #}
+ """,
+ )
+ assert com.css == ["a.css", "/static/shared/b.css", "http://example.com/cdn.css"]
+ assert com.js == ["http://example.com/cdn.js", "a.js", "/static/shared/b.js"]
+
+
+def test_types_in_args_decl():
+ com = Component(
+ name="Test.jinja",
+ source="""{# def
+ ring_class: str = "ring-1 ring-black",
+ rounded_class: str = "rounded-2xl md:rounded-3xl",
+
+ image: str | None = None,
+
+ title: str = "",
+ p_class: str = "px-5 md:px-6 py-5 md:py-6",
+ gap_class: str = "gap-4",
+ content_class: str = "",
+
+ layer_class: str | None = None,
+ layer_height: int = 4,
+#}"""
+ )
+ assert com.required == []
+ print(com.optional)
+ assert com.optional == {
+ "ring_class": "ring-1 ring-black",
+ "rounded_class": "rounded-2xl md:rounded-3xl",
+ "image": None,
+ "title": "",
+ "p_class": "px-5 md:px-6 py-5 md:py-6",
+ "gap_class": "gap-4",
+ "content_class": "",
+ "layer_class": None,
+ "layer_height": 4,
+ }
+
+
+def test_comments_in_args_decl():
+ com = Component(
+ name="Test.jinja",
+ source="""{# def
+ #
+ # Card style
+ ring_class: str = "ring-1 ring-black",
+ rounded_class: str = "rounded-2xl md:rounded-3xl",
+ #
+ # Image
+ image: str | None = None,
+ #
+ # Content
+ title: str = "",
+ p_class: str = "px-5 md:px-6 py-5 md:py-6",
+ gap_class: str = "gap-4",
+ content_class: str = "",
+ #
+ # Decorative layer
+ layer_class: str | None = None,
+ layer_height: int = 4,
+#}"""
+ )
+ assert com.required == []
+ print(com.optional)
+ assert com.optional == {
+ "ring_class": "ring-1 ring-black",
+ "rounded_class": "rounded-2xl md:rounded-3xl",
+ "image": None,
+ "title": "",
+ "p_class": "px-5 md:px-6 py-5 md:py-6",
+ "gap_class": "gap-4",
+ "content_class": "",
+ "layer_class": None,
+ "layer_height": 4,
+ }
+
+
+def test_comment_after_args_decl():
+ com = Component(
+ name="Test.jinja",
+ source="""
+{# def
+ arg,
+#}
+
+{#
+ Some comment.
+#}
+Hi
+""".strip())
+ assert com.required == ["arg"]
+ assert com.optional == {}
+
+
+def test_fake_decl():
+ com = Component(
+ name="Test.jinja",
+ source="""
+{# definitely not an args decl! #}
+{# def arg #}
+{# jsadfghkl are letters #}
+{# csssssss #}
+""".strip())
+ assert com.required == ["arg"]
+ assert com.optional == {}
diff --git a/tests/test_html_attrs.py b/tests/test_html_attrs.py
new file mode 100644
index 0000000..bc1a68f
--- /dev/null
+++ b/tests/test_html_attrs.py
@@ -0,0 +1,281 @@
+import pytest
+
+from jinjax.html_attrs import HTMLAttrs
+
+
+def test_parse_initial_attrs():
+ attrs = HTMLAttrs(
+ {
+ "title": "hi",
+ "data-position": "top",
+ "class": "z4 c3 a1 z4 b2",
+ "open": True,
+ "disabled": False,
+ "value": 0,
+ "foobar": None,
+ }
+ )
+ assert attrs.classes == "a1 b2 c3 z4"
+ assert attrs.get("class") == "a1 b2 c3 z4"
+ assert attrs.get("data-position") == "top"
+ assert attrs.get("data_position") == "top"
+ assert attrs.get("title") == "hi"
+ assert attrs.get("open") is True
+ assert attrs.get("disabled", "meh") == "meh"
+ assert attrs.get("value") == "0"
+
+ assert attrs.get("disabled") is None
+ assert attrs.get("foobar") is None
+
+ attrs.set(data_value=0)
+ attrs.set(data_position=False)
+ assert attrs.get("data-value") == 0
+ assert attrs.get("data-position") is None
+ assert attrs.get("data_position") is None
+
+def test_getattr():
+ attrs = HTMLAttrs(
+ {
+ "title": "hi",
+ "class": "z4 c3 a1 z4 b2",
+ "open": True,
+ }
+ )
+ assert attrs["class"] == "a1 b2 c3 z4"
+ assert attrs["title"] == "hi"
+ assert attrs["open"] is True
+ assert attrs["lorem"] is None
+
+
+def test_deltattr():
+ attrs = HTMLAttrs(
+ {
+ "title": "hi",
+ "class": "z4 c3 a1 z4 b2",
+ "open": True,
+ }
+ )
+ assert attrs["class"] == "a1 b2 c3 z4"
+ del attrs["title"]
+ assert attrs["title"] is None
+
+
+def test_render():
+ attrs = HTMLAttrs(
+ {
+ "title": "hi",
+ "data-position": "top",
+ "class": "z4 c3 a1 z4 b2",
+ "open": True,
+ "disabled": False,
+ }
+ )
+ assert 'class="a1 b2 c3 z4" data-position="top" title="hi" open' == attrs.render()
+
+
+def test_set():
+ attrs = HTMLAttrs({})
+ attrs.set(title="hi", data_position="top")
+ attrs.set(open=True)
+ assert 'data-position="top" title="hi" open' == attrs.render()
+
+ attrs.set(title=False, open=False)
+ assert 'data-position="top"' == attrs.render()
+
+
+def test_class_management():
+ attrs = HTMLAttrs(
+ {
+ "class": "z4 c3 a1 z4 b2",
+ }
+ )
+ attrs.set(classes="lorem bipsum lorem a1")
+
+ assert attrs.classes == "a1 b2 bipsum c3 lorem z4"
+
+ attrs.remove_class("bipsum")
+ assert attrs.classes == "a1 b2 c3 lorem z4"
+
+ attrs.set(classes=None)
+ attrs.set(classes="meh")
+ assert attrs.classes == "meh"
+
+
+def test_setdefault():
+ attrs = HTMLAttrs(
+ {
+ "title": "hi",
+ }
+ )
+ attrs.setdefault(
+ title="default title",
+ data_lorem="ipsum",
+ open=True,
+ disabled=False,
+ )
+ assert 'data-lorem="ipsum" title="hi"' == attrs.render()
+
+
+def test_as_dict():
+ attrs = HTMLAttrs(
+ {
+ "title": "hi",
+ "data-position": "top",
+ "class": "z4 c3 a1 z4 b2",
+ "open": True,
+ "disabled": False,
+ }
+ )
+ assert attrs.as_dict == {
+ "class": "a1 b2 c3 z4",
+ "data-position": "top",
+ "title": "hi",
+ "open": True,
+ }
+
+
+def test_as_dict_no_classes():
+ attrs = HTMLAttrs(
+ {
+ "title": "hi",
+ "data-position": "top",
+ "open": True,
+ }
+ )
+ assert attrs.as_dict == {
+ "data-position": "top",
+ "title": "hi",
+ "open": True,
+ }
+
+
+def test_render_attrs_lik_set():
+ attrs = HTMLAttrs({"class": "lorem"})
+ expected = 'class="ipsum lorem" data-position="top" title="hi" open'
+ result = attrs.render(
+ title="hi",
+ data_position="top",
+ classes="ipsum",
+ open=True,
+ )
+ print(result)
+ assert expected == result
+
+
+def test_do_not_escape_tailwind_syntax():
+ attrs = HTMLAttrs({"class": "lorem [&_a]:flex"})
+ expected = 'class="[&_a]:flex ipsum lorem" title="Hi&Stuff"'
+ result = attrs.render(
+ **{
+ "title": "Hi&Stuff",
+ "class": "ipsum",
+ }
+ )
+ print(result)
+ assert expected == result
+
+
+def test_do_escape_quotes_inside_attrs():
+ attrs = HTMLAttrs(
+ {
+ "class": "lorem text-['red']",
+ "title": 'I say "hey"',
+ "open": True,
+ }
+ )
+ expected = """class="lorem text-['red']" title='I say "hey"' open"""
+ result = attrs.render()
+ print(result)
+ assert expected == result
+
+
+def test_additional_attributes_are_lazily_evaluated_to_strings():
+ class TestObject:
+ def __str__(self):
+ raise RuntimeError("Should not be called unless rendered.")
+
+ attrs = HTMLAttrs(
+ {
+ "some_object": TestObject(),
+ }
+ )
+
+ with pytest.raises(RuntimeError):
+ attrs.render()
+
+
+def test_additional_attributes_lazily_evaluated_has_string_methods():
+ class TestObject:
+ def __str__(self):
+ return "test"
+
+ attrs = HTMLAttrs({"some_object": TestObject()})
+
+ assert attrs["some_object"].__str__
+ assert attrs["some_object"].__repr__
+ assert attrs["some_object"].__int__
+ assert attrs["some_object"].__float__
+ assert attrs["some_object"].__complex__
+ assert attrs["some_object"].__hash__
+ assert attrs["some_object"].__eq__
+ assert attrs["some_object"].__lt__
+ assert attrs["some_object"].__le__
+ assert attrs["some_object"].__gt__
+ assert attrs["some_object"].__ge__
+ assert attrs["some_object"].__contains__
+ assert attrs["some_object"].__len__
+ assert attrs["some_object"].__getitem__
+ assert attrs["some_object"].__add__
+ assert attrs["some_object"].__radd__
+ assert attrs["some_object"].__mul__
+ assert attrs["some_object"].__mod__
+ assert attrs["some_object"].__rmod__
+ assert attrs["some_object"].capitalize
+ assert attrs["some_object"].casefold
+ assert attrs["some_object"].center
+ assert attrs["some_object"].count
+ assert attrs["some_object"].removeprefix
+ assert attrs["some_object"].removesuffix
+ assert attrs["some_object"].encode
+ assert attrs["some_object"].endswith
+ assert attrs["some_object"].expandtabs
+ assert attrs["some_object"].find
+ assert attrs["some_object"].format
+ assert attrs["some_object"].format_map
+ assert attrs["some_object"].index
+ assert attrs["some_object"].isalpha
+ assert attrs["some_object"].isalnum
+ assert attrs["some_object"].isascii
+ assert attrs["some_object"].isdecimal
+ assert attrs["some_object"].isdigit
+ assert attrs["some_object"].isidentifier
+ assert attrs["some_object"].islower
+ assert attrs["some_object"].isnumeric
+ assert attrs["some_object"].isprintable
+ assert attrs["some_object"].isspace
+ assert attrs["some_object"].istitle
+ assert attrs["some_object"].isupper
+ assert attrs["some_object"].join
+ assert attrs["some_object"].ljust
+ assert attrs["some_object"].lower
+ assert attrs["some_object"].lstrip
+ assert attrs["some_object"].partition
+ assert attrs["some_object"].replace
+ assert attrs["some_object"].rfind
+ assert attrs["some_object"].rindex
+ assert attrs["some_object"].rjust
+ assert attrs["some_object"].rpartition
+ assert attrs["some_object"].rstrip
+ assert attrs["some_object"].split
+ assert attrs["some_object"].rsplit
+ assert attrs["some_object"].splitlines
+ assert attrs["some_object"].startswith
+ assert attrs["some_object"].strip
+ assert attrs["some_object"].swapcase
+ assert attrs["some_object"].title
+ assert attrs["some_object"].translate
+ assert attrs["some_object"].upper
+ assert attrs["some_object"].zfill
+
+ assert attrs["some_object"].upper() == "TEST"
+ assert attrs["some_object"].title() == "Test"
diff --git a/tests/test_middleware.py b/tests/test_middleware.py
new file mode 100644
index 0000000..b1fa9a9
--- /dev/null
+++ b/tests/test_middleware.py
@@ -0,0 +1,152 @@
+import typing as t
+from pathlib import Path
+
+import jinjax
+
+
+def application(environ, start_response) -> list[bytes]:
+ status = "200 OK"
+ headers = [("Content-type", "text/plain")]
+ start_response(status, headers)
+ return [b"NOPE"]
+
+
+def make_environ(**kw) -> dict[str, t.Any]:
+ kw.setdefault("PATH_INFO", "/")
+ kw.setdefault("REQUEST_METHOD", "GET")
+ return kw
+
+
+def mock_start_response(status: str, headers: dict[str, t.Any]):
+ pass
+
+
+def get_catalog(folder: "str | Path", **kw) -> jinjax.Catalog:
+ catalog = jinjax.Catalog(**kw)
+ catalog.add_folder(folder)
+ return catalog
+
+
+TMiddleware = t.Callable[
+ [
+ dict[str, t.Any],
+ t.Callable[[str, dict[str, t.Any]], None],
+ ],
+ t.Any
+]
+
+def run_middleware(middleware: TMiddleware, url: str):
+ return middleware(make_environ(PATH_INFO=url), mock_start_response)
+
+
+# Tests
+
+
+def test_css_is_returned(folder):
+ (folder / "page.css").write_text("/* Page.css */")
+ catalog = get_catalog(folder)
+ middleware = catalog.get_middleware(application)
+
+ resp = run_middleware(middleware, "/static/components/page.css")
+ assert resp and not isinstance(resp, list)
+ text = resp.filelike.read().strip()
+ assert text == b"/* Page.css */"
+
+
+def test_js_is_returned(folder):
+ (folder / "page.js").write_text("/* Page.js */")
+ catalog = get_catalog(folder)
+ middleware = catalog.get_middleware(application)
+
+ resp = run_middleware(middleware, "/static/components/page.js")
+ assert resp and not isinstance(resp, list)
+ text = resp.filelike.read().strip()
+ assert text == b"/* Page.js */"
+
+
+def test_other_file_extensions_ignored(folder):
+ (folder / "Page.jinja").write_text("???")
+ catalog = get_catalog(folder)
+ middleware = catalog.get_middleware(application)
+ resp = run_middleware(middleware, "/static/components/Page.jinja")
+ assert resp == [b"NOPE"]
+
+
+def test_add_custom_extensions(folder):
+ (folder / "Page.jinja").write_text("???")
+ catalog = get_catalog(folder)
+ middleware = catalog.get_middleware(application, allowed_ext=[".jinja"])
+
+ resp = run_middleware(middleware, "/static/components/Page.jinja")
+ assert resp and not isinstance(resp, list)
+ text = resp.filelike.read().strip()
+ assert text == b"???"
+
+
+def test_custom_root_url(folder):
+ (folder / "page.css").write_text("/* Page.css */")
+ catalog = get_catalog(folder, root_url="/static/co/")
+ middleware = catalog.get_middleware(application)
+
+ resp = run_middleware(middleware, "/static/co/page.css")
+ assert resp and not isinstance(resp, list)
+ text = resp.filelike.read().strip()
+ assert text == b"/* Page.css */"
+
+
+def test_autorefresh_load(folder):
+ (folder / "page.css").write_text("/* Page.css */")
+ catalog = get_catalog(folder)
+ middleware = catalog.get_middleware(application, autorefresh=True)
+
+ resp = run_middleware(middleware, "/static/components/page.css")
+ assert resp and not isinstance(resp, list)
+ text = resp.filelike.read().strip()
+ assert text == b"/* Page.css */"
+
+
+def test_autorefresh_block(folder):
+ (folder / "Page.jinja").write_text("???")
+ catalog = get_catalog(folder)
+ middleware = catalog.get_middleware(application, autorefresh=True)
+
+ resp = run_middleware(middleware, "/static/components/Page.jinja")
+ assert resp == [b"NOPE"]
+
+
+def test_multiple_folders(tmp_path):
+ folder1 = tmp_path / "folder1"
+ folder1.mkdir()
+ (folder1 / "folder1.css").write_text("folder1")
+
+ folder2 = tmp_path / "folder2"
+ folder2.mkdir()
+ (folder2 / "folder2.css").write_text("folder2")
+
+ catalog = jinjax.Catalog()
+ catalog.add_folder(folder1)
+ catalog.add_folder(folder2)
+ middleware = catalog.get_middleware(application)
+
+ resp = run_middleware(middleware, "/static/components/folder1.css")
+ assert resp.filelike.read() == b"folder1"
+ resp = run_middleware(middleware, "/static/components/folder2.css")
+ assert resp.filelike.read() == b"folder2"
+
+
+def test_multiple_folders_precedence(tmp_path):
+ folder1 = tmp_path / "folder1"
+ folder1.mkdir()
+ (folder1 / "name.css").write_text("folder1")
+
+ folder2 = tmp_path / "folder2"
+ folder2.mkdir()
+ (folder2 / "name.css").write_text("folder2")
+
+ catalog = jinjax.Catalog()
+ catalog.add_folder(folder1)
+ catalog.add_folder(folder2)
+ middleware = catalog.get_middleware(application)
+
+ resp = run_middleware(middleware, "/static/components/name.css")
+ assert resp.filelike.read() == b"folder1"
diff --git a/tests/test_render.py b/tests/test_render.py
new file mode 100644
index 0000000..1e52cda
--- /dev/null
+++ b/tests/test_render.py
@@ -0,0 +1,992 @@
+import time
+from pathlib import Path
+from textwrap import dedent
+
+import jinja2
+import pytest
+from jinja2.exceptions import TemplateSyntaxError
+from markupsafe import Markup
+
+import jinjax
+
+
+@pytest.mark.parametrize("autoescape", [True, False])
+def test_render_simple(catalog, folder, autoescape):
+ catalog.jinja_env.autoescape = autoescape
+
+ (folder / "Greeting.jinja").write_text(
+ """
+{#def message #}
+<div class="greeting [&_a]:flex">{{ message }}</div>
+ """
+ )
+ html = catalog.render("Greeting", message="Hello world!")
+ assert html == Markup('<div class="greeting [&_a]:flex">Hello world!</div>')
+
+
+@pytest.mark.parametrize("autoescape", [True, False])
+def test_render_source(catalog, autoescape):
+ catalog.jinja_env.autoescape = autoescape
+
+ source = '{#def message #}\n<div class="greeting [&_a]:flex">{{ message }}</div>'
+ expected = Markup('<div class="greeting [&_a]:flex">Hello world!</div>')
+
+ html = catalog.render("Greeting", message="Hello world!", _source=source)
+ assert expected == html
+
+ # Legacy
+ html = catalog.render("Greeting", message="Hello world!", __source=source)
+ assert expected == html
+
+
+@pytest.mark.parametrize("autoescape", [True, False])
+def test_render_content(catalog, folder, autoescape):
+ catalog.jinja_env.autoescape = autoescape
+
+ (folder / "Card.jinja").write_text("""
+<section class="card">
+{{ content }}
+</section>
+ """)
+
+ content = '<button type="button">Close</button>'
+ expected = Markup(f'<section class="card">\n{content}\n</section>')
+
+ html = catalog.render("Card", _content=content)
+ print(html)
+ assert expected == html
+
+ # Legacy
+ html = catalog.render("Card", __content=content)
+ assert expected == html
+
+
+@pytest.mark.parametrize("autoescape", [True, False])
+@pytest.mark.parametrize(
+ "source, expected",
+ [
+ ("<Title>Hi</Title><Title>Hi</Title>", "<h1>Hi</h1><h1>Hi</h1>"),
+ ("<Icon /><Icon />", '<i class="icon"></i><i class="icon"></i>'),
+ ("<Title>Hi</Title><Icon />", '<h1>Hi</h1><i class="icon"></i>'),
+ ("<Icon /><Title>Hi</Title>", '<i class="icon"></i><h1>Hi</h1>'),
+ ],
+)
+def test_render_mix_of_contentful_and_contentless_components(
+ catalog,
+ folder,
+ source,
+ expected,
+ autoescape,
+):
+ catalog.jinja_env.autoescape = autoescape
+
+ (folder / "Icon.jinja").write_text('<i class="icon"></i>')
+ (folder / "Title.jinja").write_text("<h1>{{ content }}</h1>")
+ (folder / "Page.jinja").write_text(source)
+
+ html = catalog.render("Page")
+ assert html == Markup(expected)
+
+
+@pytest.mark.parametrize("autoescape", [True, False])
+def test_composition(catalog, folder, autoescape):
+ catalog.jinja_env.autoescape = autoescape
+
+ (folder / "Greeting.jinja").write_text(
+ """
+{#def message #}
+<div class="greeting [&_a]:flex">{{ message }}</div>
+"""
+ )
+
+ (folder / "CloseBtn.jinja").write_text(
+ """
+{#def disabled=False -#}
+<button type="button"{{ " disabled" if disabled else "" }}>&times;</button>
+"""
+ )
+
+ (folder / "Card.jinja").write_text(
+ """
+<section class="card">
+{{ content }}
+<CloseBtn disabled />
+</section>
+"""
+ )
+
+ (folder / "Page.jinja").write_text(
+ """
+{#def message #}
+<Card>
+<Greeting :message="message" />
+<button type="button">Close</button>
+</Card>
+"""
+ )
+
+ html = catalog.render("Page", message="Hello")
+ print(html)
+ assert (
+ """
+<section class="card">
+<div class="greeting [&_a]:flex">Hello</div>
+<button type="button">Close</button>
+<button type="button" disabled>&times;</button>
+</section>
+""".strip()
+ in html
+ )
+
+
+@pytest.mark.parametrize("autoescape", [True, False])
+def test_just_properties(catalog, folder, autoescape):
+ catalog.jinja_env.autoescape = autoescape
+
+ (folder / "Lorem.jinja").write_text(
+ """
+{#def ipsum=False #}
+<p>lorem {{ "ipsum" if ipsum else "lorem" }}</p>
+"""
+ )
+
+ (folder / "Layout.jinja").write_text(
+ """
+<main>
+{{ content }}
+</main>
+"""
+ )
+
+ (folder / "Page.jinja").write_text(
+ """
+<Layout>
+<Lorem ipsum />
+<p>meh</p>
+<Lorem />
+</Layout>
+"""
+ )
+
+ html = catalog.render("Page")
+ print(html)
+ assert (
+ """
+<main>
+<p>lorem ipsum</p>
+<p>meh</p>
+<p>lorem lorem</p>
+</main>
+""".strip()
+ in html
+ )
+
+
+@pytest.mark.parametrize("autoescape", [True, False])
+def test_render_assets(catalog, folder, autoescape):
+ catalog.jinja_env.autoescape = autoescape
+
+ (folder / "Greeting.jinja").write_text(
+ """
+{#def message #}
+{#css greeting.css, http://example.com/super.css #}
+{#js greeting.js #}
+<div class="greeting [&_a]:flex">{{ message }}</div>
+"""
+ )
+
+ (folder / "Card.jinja").write_text(
+ """
+{#css https://somewhere.com/style.css, card.css #}
+{#js card.js, shared.js #}
+<section class="card">
+{{ content }}
+</section>
+"""
+ )
+
+ (folder / "Layout.jinja").write_text(
+ """
+<html>
+{{ catalog.render_assets() }}
+{{ content }}
+</html>
+"""
+ )
+
+ (folder / "Page.jinja").write_text(
+ """
+{#def message #}
+{#js https://somewhere.com/blabla.js, shared.js #}
+<Layout>
+<Card>
+<Greeting :message="message" />
+<button type="button">Close</button>
+</Card>
+</Layout>
+"""
+ )
+
+ html = catalog.render("Page", message="Hello")
+ print(html)
+ assert (
+ """
+<html>
+<link rel="stylesheet" href="https://somewhere.com/style.css">
+<link rel="stylesheet" href="/static/components/card.css">
+<link rel="stylesheet" href="/static/components/greeting.css">
+<link rel="stylesheet" href="http://example.com/super.css">
+<script type="module" src="https://somewhere.com/blabla.js"></script>
+<script type="module" src="/static/components/shared.js"></script>
+<script type="module" src="/static/components/card.js"></script>
+<script type="module" src="/static/components/greeting.js"></script>
+<section class="card">
+<div class="greeting [&_a]:flex">Hello</div>
+<button type="button">Close</button>
+</section>
+</html>
+""".strip()
+ in html
+ )
+
+
+@pytest.mark.parametrize("autoescape", [True, False])
+def test_global_values(catalog, folder, autoescape):
+ catalog.jinja_env.autoescape = autoescape
+
+ (folder / "Global.jinja").write_text("""{{ globalvar }}""")
+ message = "Hello world!"
+ catalog.jinja_env.globals["globalvar"] = message
+ html = catalog.render("Global")
+ print(html)
+ assert message in html
+
+
+@pytest.mark.parametrize("autoescape", [True, False])
+def test_required_attr_are_required(catalog, folder, autoescape):
+ catalog.jinja_env.autoescape = autoescape
+
+ (folder / "Greeting.jinja").write_text(
+ """
+{#def message #}
+<div class="greeting">{{ message }}</div>
+"""
+ )
+
+ with pytest.raises(jinjax.MissingRequiredArgument):
+ catalog.render("Greeting")
+
+
+@pytest.mark.parametrize("autoescape", [True, False])
+def test_subfolder(catalog, folder, autoescape):
+ catalog.jinja_env.autoescape = autoescape
+
+ sub = folder / "UI"
+ sub.mkdir()
+ (folder / "Meh.jinja").write_text("<UI.Tab>Meh</UI.Tab>")
+ (sub / "Tab.jinja").write_text('<div class="tab">{{ content }}</div>')
+
+ html = catalog.render("Meh")
+ assert html == Markup('<div class="tab">Meh</div>')
+
+
+@pytest.mark.parametrize("autoescape", [True, False])
+def test_default_attr(catalog, folder, autoescape):
+ catalog.jinja_env.autoescape = autoescape
+
+ (folder / "Greeting.jinja").write_text(
+ """
+{#def message="Hello", world=False #}
+<div>{{ message }}{% if world %} World{% endif %}</div>
+"""
+ )
+
+ (folder / "Page.jinja").write_text(
+ """
+<Greeting />
+<Greeting message="Hi" />
+<Greeting :world="False" />
+<Greeting :world="True" />
+<Greeting world />
+"""
+ )
+
+ html = catalog.render("Page", message="Hello")
+ print(html)
+ assert (
+ """
+<div>Hello</div>
+<div>Hi</div>
+<div>Hello</div>
+<div>Hello World</div>
+<div>Hello World</div>
+""".strip()
+ in html
+ )
+
+
+@pytest.mark.parametrize("autoescape", [True, False])
+def test_raw_content(catalog, folder, autoescape):
+ catalog.jinja_env.autoescape = autoescape
+
+ (folder / "Code.jinja").write_text("""
+<pre class="code">
+{{ content|e }}
+</pre>
+""")
+
+ (folder / "Page.jinja").write_text("""
+<Code>
+{% raw -%}
+{#def message="Hello", world=False #}
+<Header />
+<div>{{ message }}{% if world %} World{% endif %}</div>
+{%- endraw %}
+</Code>
+""")
+
+ html = catalog.render("Page")
+ print(html)
+ assert (
+ """
+<pre class="code">
+{#def message=&#34;Hello&#34;, world=False #}
+&lt;Header /&gt;
+&lt;div&gt;{{ message }}{% if world %} World{% endif %}&lt;/div&gt;
+</pre>
+""".strip()
+ in html
+ )
+
+
+@pytest.mark.parametrize("autoescape", [True, False])
+def test_multiple_raw(catalog, folder, autoescape):
+ catalog.jinja_env.autoescape = autoescape
+
+ (folder / "C.jinja").write_text("""
+<div {{ attrs.render() }}></div>
+""")
+
+ (folder / "Page.jinja").write_text("""
+<C id="1" />
+{% raw -%}
+<C id="2" />
+{%- endraw %}
+<C id="3" />
+{% raw %}<C id="4" />{% endraw %}
+<C id="5" />
+""")
+
+ html = catalog.render("Page", message="Hello")
+ print(html)
+ assert (
+ """
+<div id="1"></div>
+&lt;C id=&#34;2&#34; /&gt;
+<div id="3"></div>
+&lt;C id=&#34;4&#34; /&gt;
+<div id="5"></div>
+""".strip()
+ in html
+ )
+
+
+@pytest.mark.parametrize("autoescape", [True, False])
+def test_check_for_unclosed(catalog, folder, autoescape):
+ catalog.jinja_env.autoescape = autoescape
+
+ (folder / "Lorem.jinja").write_text("""
+{#def ipsum=False #}
+<p>lorem {{ "ipsum" if ipsum else "lorem" }}</p>
+""")
+
+ (folder / "Page.jinja").write_text("""
+<main>
+<Lorem ipsum>
+</main>
+""")
+
+ with pytest.raises(TemplateSyntaxError):
+ try:
+ catalog.render("Page")
+ except TemplateSyntaxError as err:
+ print(err)
+ raise
+
+
+@pytest.mark.parametrize("autoescape", [True, False])
+def test_dict_as_attr(catalog, folder, autoescape):
+ catalog.jinja_env.autoescape = autoescape
+
+ (folder / "CitiesList.jinja").write_text("""
+{#def cities #}
+{% for city, country in cities.items() -%}
+<p>{{ city }}, {{ country }}</p>
+{%- endfor %}
+""")
+
+ (folder / "Page.jinja").write_text("""
+<CitiesList :cities="{
+ 'Lima': 'Peru',
+ 'New York': 'USA',
+}" />
+""")
+
+ html = catalog.render("Page")
+ assert html == Markup("<p>Lima, Peru</p><p>New York, USA</p>")
+
+
+@pytest.mark.parametrize("autoescape", [True, False])
+def test_cleanup_assets(catalog, folder, autoescape):
+ catalog.jinja_env.autoescape = autoescape
+
+ (folder / "Layout.jinja").write_text("""
+<html>
+{{ catalog.render_assets() }}
+{{ content }}
+</html>
+""")
+
+ (folder / "Foo.jinja").write_text("""
+{#js foo.js #}
+<Layout>
+<p>Foo</p>
+</Layout>
+""")
+
+ (folder / "Bar.jinja").write_text("""
+{#js bar.js #}
+<Layout>
+<p>Bar</p>
+</Layout>
+""")
+
+ html = catalog.render("Foo")
+ print(html, "\n")
+ assert (
+ """
+<html>
+<script type="module" src="/static/components/foo.js"></script>
+<p>Foo</p>
+</html>
+""".strip()
+ in html
+ )
+
+ html = catalog.render("Bar")
+ print(html)
+ assert (
+ """
+<html>
+<script type="module" src="/static/components/bar.js"></script>
+<p>Bar</p>
+</html>
+""".strip()
+ in html
+ )
+
+
+@pytest.mark.parametrize("autoescape", [True, False])
+def test_do_not_mess_with_external_jinja_env(folder_t, folder, autoescape):
+ """https://github.com/jpsca/jinjax/issues/19"""
+ (folder_t / "greeting.html").write_text("Jinja still works")
+ (folder / "Greeting.jinja").write_text("JinjaX works")
+
+ jinja_env = jinja2.Environment(
+ loader=jinja2.FileSystemLoader(folder_t),
+ extensions=["jinja2.ext.i18n"],
+ )
+ jinja_env.globals = {"glo": "bar"}
+ jinja_env.filters = {"fil": lambda x: x}
+ jinja_env.tests = {"tes": lambda x: x}
+ jinja_env.autoescape = autoescape
+
+ catalog = jinjax.Catalog(
+ jinja_env=jinja_env,
+ extensions=["jinja2.ext.debug"],
+ globals={"xglo": "foo"},
+ filters={"xfil": lambda x: x},
+ tests={"xtes": lambda x: x},
+ )
+ catalog.add_folder(folder)
+
+ html = catalog.render("Greeting")
+ assert html == Markup("JinjaX works")
+
+ assert catalog.jinja_env.globals["catalog"] == catalog
+ assert catalog.jinja_env.globals["glo"] == "bar"
+ assert catalog.jinja_env.globals["xglo"] == "foo"
+ assert catalog.jinja_env.filters["fil"]
+ assert catalog.jinja_env.filters["xfil"]
+ assert catalog.jinja_env.tests["tes"]
+ assert catalog.jinja_env.tests["xtes"]
+ assert "jinja2.ext.InternationalizationExtension" in catalog.jinja_env.extensions
+ assert "jinja2.ext.DebugExtension" in catalog.jinja_env.extensions
+ assert "jinja2.ext.ExprStmtExtension" in catalog.jinja_env.extensions
+
+ tmpl = jinja_env.get_template("greeting.html")
+ assert tmpl.render() == "Jinja still works"
+
+ assert jinja_env.globals["catalog"] == catalog
+ assert jinja_env.globals["glo"] == "bar"
+ assert "xglo" not in jinja_env.globals
+ assert jinja_env.filters["fil"]
+ assert "xfil" not in jinja_env.filters
+ assert jinja_env.tests["tes"]
+ assert "xtes" not in jinja_env.tests
+ assert "jinja2.ext.InternationalizationExtension" in jinja_env.extensions
+ assert "jinja2.ext.DebugExtension" not in jinja_env.extensions
+
+
+@pytest.mark.parametrize("autoescape", [True, False])
+def test_auto_reload(catalog, folder, autoescape):
+ catalog.jinja_env.autoescape = autoescape
+
+ (folder / "Layout.jinja").write_text("""
+<html>
+{{ content }}
+</html>
+""")
+
+ (folder / "Foo.jinja").write_text("""
+<Layout>
+<p>Foo</p>
+<Bar></Bar>
+</Layout>
+""")
+
+ bar_file = folder / "Bar.jinja"
+ bar_file.write_text("<p>Bar</p>")
+
+ html1 = catalog.render("Foo")
+ print(bar_file.stat().st_mtime)
+ print(html1, "\n")
+ assert (
+ """
+<html>
+<p>Foo</p>
+<p>Bar</p>
+</html>
+""".strip()
+ in html1
+ )
+
+ # Give it some time so the st_mtime are different
+ time.sleep(0.1)
+
+ catalog.auto_reload = False
+ bar_file.write_text("<p>Ignored</p>")
+ print(bar_file.stat().st_mtime)
+ html2 = catalog.render("Foo")
+ print(html2, "\n")
+
+ catalog.auto_reload = True
+ bar_file.write_text("<p>Updated</p>")
+ print(bar_file.stat().st_mtime)
+ html3 = catalog.render("Foo")
+ print(html3, "\n")
+
+ assert html1 == html2
+ assert (
+ """
+<html>
+<p>Foo</p>
+<p>Updated</p>
+</html>
+""".strip()
+ in html3
+ )
+
+
+@pytest.mark.parametrize("autoescape", [True, False])
+def test_subcomponents(catalog, folder, autoescape):
+ catalog.jinja_env.autoescape = autoescape
+
+ """Issue https://github.com/jpsca/jinjax/issues/32"""
+ (folder / "Page.jinja").write_text("""
+{#def message #}
+<html>
+<p>lorem ipsum</p>
+<Subcomponent />
+{{ message }}
+</html>
+""")
+
+ (folder / "Subcomponent.jinja").write_text("""
+<p>foo bar</p>
+""")
+
+ html = catalog.render("Page", message="<3")
+
+ if autoescape:
+ expected = """
+<html>
+<p>lorem ipsum</p>
+<p>foo bar</p>
+&lt;3
+</html>"""
+ else:
+ expected = """
+<html>
+<p>lorem ipsum</p>
+<p>foo bar</p>
+<3
+</html>"""
+
+ assert html == Markup(expected.strip())
+
+
+@pytest.mark.parametrize("autoescape", [True, False])
+def test_fingerprint_assets(catalog, folder: Path, autoescape):
+ catalog.jinja_env.autoescape = autoescape
+
+ (folder / "Layout.jinja").write_text("""
+<html>
+{{ catalog.render_assets() }}
+{{ content }}
+</html>
+""")
+
+ (folder / "Page.jinja").write_text("""
+{#css app.css, http://example.com/super.css #}
+{#js app.js #}
+<Layout>Hi</Layout>
+""")
+
+ (folder / "app.css").write_text("...")
+
+ catalog.fingerprint = True
+ html = catalog.render("Page", message="Hello")
+ print(html)
+
+ assert 'src="/static/components/app.js"' in html
+ assert 'href="/static/components/app-' in html
+ assert 'href="http://example.com/super.css' in html
+
+
+@pytest.mark.parametrize("autoescape", [True, False])
+def test_colon_in_attrs(catalog, folder, autoescape):
+ catalog.jinja_env.autoescape = autoescape
+
+ (folder / "C.jinja").write_text("""
+<div {{ attrs.render() }}></div>
+""")
+
+ (folder / "Page.jinja").write_text("""
+<C hx-on:click="show = !show" />
+""")
+
+ html = catalog.render("Page", message="Hello")
+ print(html)
+ assert """<div hx-on:click="show = !show"></div>""" in html
+
+
+@pytest.mark.parametrize("autoescape", [True, False])
+def test_template_globals(catalog, folder, autoescape):
+ catalog.jinja_env.autoescape = autoescape
+
+ (folder / "Input.jinja").write_text("""
+{# def name, value #}<input type="text" name="{{name}}" value="{{value}}">
+""")
+
+ (folder / "CsrfToken.jinja").write_text("""
+<input type="hidden" name="csrft" value="{{csrf_token}}">
+""")
+
+ (folder / "Form.jinja").write_text("""
+<form><CsrfToken/>{{content}}</form>
+""")
+
+ (folder / "Page.jinja").write_text("""
+{# def value #}
+<Form><Input name="foo" :value="value"/></Form>
+""")
+
+ html = catalog.render("Page", value="bar", __globals={"csrf_token": "abc"})
+ print(html)
+ assert """<input type="hidden" name="csrft" value="abc">""" in html
+
+
+@pytest.mark.parametrize("autoescape", [True, False])
+def test_template_globals_update_cache(catalog, folder, autoescape):
+ catalog.jinja_env.autoescape = autoescape
+
+ (folder / "CsrfToken.jinja").write_text(
+ """<input type="hidden" name="csrft" value="{{csrf_token}}">"""
+ )
+ (folder / "Page.jinja").write_text("""<CsrfToken/>""")
+
+ html = catalog.render("Page", __globals={"csrf_token": "abc"})
+ print(html)
+ assert """<input type="hidden" name="csrft" value="abc">""" in html
+
+ html = catalog.render("Page", __globals={"csrf_token": "xyz"})
+ print(html)
+ assert """<input type="hidden" name="csrft" value="xyz">""" in html
+
+
+@pytest.mark.parametrize("autoescape", [True, False])
+def test_alpine_sintax(catalog, folder, autoescape):
+ catalog.jinja_env.autoescape = autoescape
+
+ (folder / "Greeting.jinja").write_text("""
+{#def message #}
+<button @click="alert('{{ message }}')">Say Hi</button>""")
+
+ html = catalog.render("Greeting", message="Hello world!")
+ print(html)
+ expected = """<button @click="alert('Hello world!')">Say Hi</button>"""
+ assert html == Markup(expected)
+
+
+@pytest.mark.parametrize("autoescape", [True, False])
+def test_alpine_sintax_in_component(catalog, folder, autoescape):
+ catalog.jinja_env.autoescape = autoescape
+
+ (folder / "Button.jinja").write_text(
+ """<button {{ attrs.render() }}>{{ content }}</button>"""
+ )
+
+ (folder / "Greeting.jinja").write_text(
+ """<Button @click="alert('Hello world!')">Say Hi</Button>"""
+ )
+
+ html = catalog.render("Greeting")
+ print(html)
+ expected = """<button @click="alert('Hello world!')">Say Hi</button>"""
+ assert html == Markup(expected)
+
+
+@pytest.mark.parametrize("autoescape", [True, False])
+def test_autoescaped_attrs(catalog, folder, autoescape):
+ catalog.jinja_env.autoescape = autoescape
+
+ (folder / "CheckboxItem.jinja").write_text(
+ """<div {{ attrs.render(class="relative") }}></div>"""
+ )
+
+ (folder / "Page.jinja").write_text(
+ """<CheckboxItem class="border border-red-500" />"""
+ )
+
+ html = catalog.render("Page")
+ print(html)
+ expected = """<div class="border border-red-500 relative"></div>"""
+ assert html == Markup(expected)
+
+
+@pytest.mark.parametrize(
+ "template",
+ [
+ pytest.param(
+ dedent(
+ """
+ {# def
+ href,
+ hx_target="#maincontent",
+ hx_swap="innerHTML show:body:top",
+ hx_push_url=true,
+ #}
+ <a href="{{href}}" hx-get="{{href}}" hx-target="{{hx_target}}"
+ hx-swap="{{hx_swap}}"
+ {% if hx_push_url %}hx-push-url="true"{% endif %}>
+ {{- content -}}
+ </a>
+ """
+ ),
+ id="no comment",
+ ),
+ pytest.param(
+ dedent(
+ """
+ {# def
+ href,
+ hx_target="#maincontent", # css selector
+ hx_swap="innerHTML show:body:top",
+ hx_push_url=true,
+ #}
+ <a href="{{href}}" hx-get="{{href}}" hx-target="{{hx_target}}"
+ hx-swap="{{hx_swap}}"
+ {% if hx_push_url %}hx-push-url="true"{% endif %}>
+ {{- content -}}
+ </a>
+ """
+ ),
+ id="comment with # on line",
+ ),
+ pytest.param(
+ dedent(
+ """
+ {# def
+ href, # url of the target page
+ hx_target="#maincontent", # css selector
+ hx_swap="innerHTML show:body:top", # browse on top of the page
+ hx_push_url=true, # replace the url of the browser
+ #}
+ <a href="{{href}}" hx-get="{{href}}" hx-target="{{hx_target}}"
+ hx-swap="{{hx_swap}}"
+ {% if hx_push_url %}hx-push-url="true"{% endif %}>
+ {{- content -}}
+ </a>
+ """
+ ),
+ id="many comments",
+ ),
+ pytest.param(
+ dedent(
+ """
+ {# def
+ href: str, # url of the target page
+ hx_target: str = "#maincontent", # css selector
+ hx_swap: str = "innerHTML show:body:top", # browse on top of the page
+ hx_push_url: bool = true, # replace the url
+ #}
+ <a href="{{href}}" hx-get="{{href}}" hx-target="{{hx_target}}"
+ hx-swap="{{hx_swap}}"
+ {% if hx_push_url %}hx-push-url="true"{% endif %}>
+ {{- content -}}
+ </a>
+ """
+ ),
+ id="many comments and typing",
+ ),
+ ],
+)
+@pytest.mark.parametrize("autoescape", [True, False])
+def test_strip_comment(catalog, folder, autoescape, template):
+ catalog.jinja_env.autoescape = autoescape
+
+ (folder / "A.jinja").write_text(template)
+
+ (folder / "Page.jinja").write_text("""<A href="/yolo">Yolo</A>""")
+
+ html = catalog.render("Page")
+ print(html)
+ expected = """
+<a href="/yolo" hx-get="/yolo" hx-target="#maincontent"
+hx-swap="innerHTML show:body:top"
+hx-push-url="true">Yolo</a>""".strip()
+ assert html == Markup(expected)
+
+
+@pytest.mark.parametrize("autoescape", [True, False])
+def test_auto_load_assets_with_same_name(catalog, folder, autoescape):
+ catalog.jinja_env.autoescape = autoescape
+
+ (folder / "Layout.jinja").write_text(
+ """{{ catalog.render_assets() }}\n{{ content }}"""
+ )
+
+ (folder / "FooBar.css").touch()
+
+ (folder / "common").mkdir()
+ (folder / "common" / "Form.jinja").write_text(
+ """
+{#js "shared.js" #}
+<form></form>"""
+ )
+
+ (folder / "common" / "Form.css").touch()
+ (folder / "common" / "Form.js").touch()
+
+ (folder / "Page.jinja").write_text(
+ """
+{#css "Page.css" #}
+<Layout><common.Form></common.Form></Layout>"""
+ )
+
+ (folder / "Page.css").touch()
+ (folder / "Page.js").touch()
+
+ html = catalog.render("Page")
+ print(html)
+
+ expected = """
+<link rel="stylesheet" href="/static/components/Page.css">
+<link rel="stylesheet" href="/static/components/common/Form.css">
+<script type="module" src="/static/components/Page.js"></script>
+<script type="module" src="/static/components/shared.js"></script>
+<script type="module" src="/static/components/common/Form.js"></script>
+<form></form>
+""".strip()
+
+ assert html == Markup(expected)
+
+
+def test_vue_like_syntax(catalog, folder):
+ (folder / "Test.jinja").write_text("""
+ {#def a, b, c, d #}
+ {{ a }} {{ b }} {{ c }} {{ d }}
+ """)
+ (folder / "Caller.jinja").write_text(
+ """<Test :a="2+2" b="2+2" :c="{'lorem': 'ipsum'}" :d="false" />"""
+ )
+ html = catalog.render("Caller")
+ print(html)
+ expected = """4 2+2 {'lorem': 'ipsum'} False""".strip()
+ assert html == Markup(expected)
+
+
+def test_jinja_like_syntax(catalog, folder):
+ (folder / "Test.jinja").write_text("""
+ {#def a, b, c, d #}
+ {{ a }} {{ b }} {{ c }} {{ d }}
+ """)
+ (folder / "Caller.jinja").write_text(
+ """<Test a={{ 2+2 }} b="2+2" c={{ {'lorem': 'ipsum'} }} d={{ false }} />"""
+ )
+ html = catalog.render("Caller")
+ print(html)
+ expected = """4 2+2 {'lorem': 'ipsum'} False""".strip()
+ assert html == Markup(expected)
+
+
+def test_mixed_syntax(catalog, folder):
+ (folder / "Test.jinja").write_text("""
+ {#def a, b, c, d #}
+ {{ a }} {{ b }} {{ c }} {{ d }}
+ """)
+ (folder / "Caller.jinja").write_text(
+ """<Test :a={{ 2+2 }} b="{{2+2}}" :c={{ {'lorem': 'ipsum'} }} :d={{ false }} />"""
+ )
+ html = catalog.render("Caller")
+ print(html)
+ expected = """4 {{2+2}} {'lorem': 'ipsum'} False""".strip()
+ assert html == Markup(expected)
+
+
+@pytest.mark.parametrize("autoescape", [True, False])
+def test_slots(catalog, folder, autoescape):
+ catalog.jinja_env.autoescape = autoescape
+
+ (folder / "Component.jinja").write_text(
+ """
+<p>{{ content }}</p>
+<p>{{ content("first") }}</p>
+<p>{{ content("second") }}</p>
+<p>{{ content("antoher") }}</p>
+<p>{{ content() }}</p>
+""".strip()
+ )
+
+ (folder / "Messages.jinja").write_text(
+ """
+<Component>
+{% if _slot == "first" %}Hello World
+{%- elif _slot == "second" %}Lorem Ipsum
+{%- elif _slot == "meh" %}QWERTYUIOP
+{%- else %}Default{% endif %}
+</Component>
+""".strip()
+ )
+
+ html = catalog.render("Messages")
+ print(html)
+ expected = """
+<p>Default</p>
+<p>Hello World</p>
+<p>Lorem Ipsum</p>
+<p>Default</p>
+<p>Default</p>
+""".strip()
+ assert html == Markup(expected)