diff options
Diffstat (limited to 'tests')
-rw-r--r-- | tests/__init__.py | 0 | ||||
-rw-r--r-- | tests/conftest.py | 24 | ||||
-rw-r--r-- | tests/test_catalog.py | 88 | ||||
-rw-r--r-- | tests/test_component.py | 315 | ||||
-rw-r--r-- | tests/test_html_attrs.py | 281 | ||||
-rw-r--r-- | tests/test_middleware.py | 152 | ||||
-rw-r--r-- | tests/test_render.py | 992 |
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 "" }}>×</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>×</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="Hello", world=False #} +<Header /> +<div>{{ message }}{% if world %} World{% endif %}</div> +</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> +<C id="2" /> +<div id="3"></div> +<C id="4" /> +<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> +<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) |