tests: Split test files and run template tests under both context behavior modes (#509)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
Juro Oravec 2024-05-28 08:23:32 +02:00 committed by GitHub
parent 8bbe81d717
commit 95f6554f4c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 3392 additions and 3339 deletions

View file

@ -6,7 +6,7 @@ from django_components.attributes import append_attributes, attributes_to_string
# isort: off # isort: off
from .django_test_setup import * # NOQA from .django_test_setup import * # NOQA
from .testutils import BaseTestCase from .testutils import BaseTestCase, parametrize_context_behavior
# isort: on # isort: on
@ -83,6 +83,7 @@ class HtmlAttrsTests(BaseTestCase):
{% endcomponent %} {% endcomponent %}
""" # noqa: E501 """ # noqa: E501
@parametrize_context_behavior(["django", "isolated"])
def test_tag_positional_args(self): def test_tag_positional_args(self):
@component.register("test") @component.register("test")
class AttrsComponent(component.Component): class AttrsComponent(component.Component):

View file

@ -1,14 +1,14 @@
import sys """
from pathlib import Path Tests focusing on the Component class.
from typing import Any, Dict, List, Optional For tests focusing on the `component` tag, see `test_templatetags_component.py`
"""
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
from django.template import Context, Template from django.template import Context
from django.test import override_settings
# isort: off # isort: off
from .django_test_setup import * # NOQA from .django_test_setup import * # NOQA
from .testutils import BaseTestCase, autodiscover_with_cleanup from .testutils import BaseTestCase, parametrize_context_behavior
# isort: on # isort: on
@ -57,6 +57,7 @@ class ComponentTest(BaseTestCase):
component.registry.register(name="parent_component", component=cls.ParentComponent) component.registry.register(name="parent_component", component=cls.ParentComponent)
component.registry.register(name="variable_display", component=cls.VariableDisplay) component.registry.register(name="variable_display", component=cls.VariableDisplay)
@parametrize_context_behavior(["django", "isolated"])
def test_empty_component(self): def test_empty_component(self):
class EmptyComponent(component.Component): class EmptyComponent(component.Component):
pass pass
@ -64,6 +65,7 @@ class ComponentTest(BaseTestCase):
with self.assertRaises(ImproperlyConfigured): with self.assertRaises(ImproperlyConfigured):
EmptyComponent("empty_component").get_template(Context({})) EmptyComponent("empty_component").get_template(Context({}))
@parametrize_context_behavior(["django", "isolated"])
def test_simple_component(self): def test_simple_component(self):
class SimpleComponent(component.Component): class SimpleComponent(component.Component):
template: types.django_html = """ template: types.django_html = """
@ -97,107 +99,7 @@ class ComponentTest(BaseTestCase):
""", """,
) )
def test_css_only_component(self): @parametrize_context_behavior(["django", "isolated"])
class SimpleComponent(component.Component):
template: types.django_html = """
Variable: <strong>{{ variable }}</strong>
"""
class Media:
css = "style.css"
comp = SimpleComponent("simple_component")
self.assertHTMLEqual(
comp.render_dependencies(),
"""
<link href="style.css" media="all" rel="stylesheet">
""",
)
def test_js_only_component(self):
class SimpleComponent(component.Component):
template: types.django_html = """
Variable: <strong>{{ variable }}</strong>
"""
class Media:
js = "script.js"
comp = SimpleComponent("simple_component")
self.assertHTMLEqual(
comp.render_dependencies(),
"""
<script src="script.js"></script>
""",
)
def test_empty_media_component(self):
class SimpleComponent(component.Component):
template: types.django_html = """
Variable: <strong>{{ variable }}</strong>
"""
class Media:
pass
comp = SimpleComponent("simple_component")
self.assertHTMLEqual(comp.render_dependencies(), "")
def test_missing_media_component(self):
class SimpleComponent(component.Component):
template: types.django_html = """
Variable: <strong>{{ variable }}</strong>
"""
comp = SimpleComponent("simple_component")
self.assertHTMLEqual(comp.render_dependencies(), "")
def test_component_with_list_of_styles(self):
class MultistyleComponent(component.Component):
class Media:
css = ["style.css", "style2.css"]
js = ["script.js", "script2.js"]
comp = MultistyleComponent("multistyle_component")
self.assertHTMLEqual(
comp.render_dependencies(),
"""
<link href="style.css" media="all" rel="stylesheet">
<link href="style2.css" media="all" rel="stylesheet">
<script src="script.js"></script>
<script src="script2.js"></script>
""",
)
def test_component_with_filtered_template(self):
class FilteredComponent(component.Component):
template: types.django_html = """
Var1: <strong>{{ var1 }}</strong>
Var2 (uppercased): <strong>{{ var2|upper }}</strong>
"""
def get_context_data(self, var1=None, var2=None):
return {
"var1": var1,
"var2": var2,
}
comp = FilteredComponent("filtered_component")
context = Context(comp.get_context_data(var1="test1", var2="test2"))
self.assertHTMLEqual(
comp.render(context),
"""
Var1: <strong>test1</strong>
Var2 (uppercased): <strong>TEST2</strong>
""",
)
def test_component_with_dynamic_template(self): def test_component_with_dynamic_template(self):
class SvgComponent(component.Component): class SvgComponent(component.Component):
def get_context_data(self, name, css_class="", title="", **attrs): def get_context_data(self, name, css_class="", title="", **attrs):
@ -224,798 +126,3 @@ class ComponentTest(BaseTestCase):
<svg>Dynamic2</svg> <svg>Dynamic2</svg>
""", """,
) )
# Settings required for autodiscover to work
@override_settings(
BASE_DIR=Path(__file__).resolve().parent,
STATICFILES_DIRS=[
Path(__file__).resolve().parent / "components",
],
)
def test_component_with_relative_paths_as_subcomponent(self):
# Ensure that the module is executed again after import in autodiscovery
if "tests.components.relative_file.relative_file" in sys.modules:
del sys.modules["tests.components.relative_file.relative_file"]
# Fix the paths, since the "components" dir is nested
with autodiscover_with_cleanup(map_import_paths=lambda p: f"tests.{p}"):
template_str: types.django_html = """
{% load component_tags %}{% component_dependencies %}
{% component 'parent_component' %}
{% fill 'content' %}
{% component name='relative_file_component' variable='hello' %}
{% endcomponent %}
{% endfill %}
{% endcomponent %}
"""
template = Template(template_str)
rendered = template.render(Context({}))
self.assertIn('<input type="text" name="variable" value="hello">', rendered, rendered)
def test_component_inside_slot(self):
class SlottedComponent(component.Component):
template: types.django_html = """
{% load component_tags %}
<custom-template>
<header>{% slot "header" %}Default header{% endslot %}</header>
<main>{% slot "main" %}Default main{% endslot %}</main>
<footer>{% slot "footer" %}Default footer{% endslot %}</footer>
</custom-template>
"""
def get_context_data(self, name: Optional[str] = None) -> Dict[str, Any]:
return {
"name": name,
}
component.registry.register("test", SlottedComponent)
template_str: types.django_html = """
{% load component_tags %}
{% component "test" name='Igor' %}
{% fill "header" %}
Name: {{ name }}
{% endfill %}
{% fill "main" %}
Day: {{ day }}
{% endfill %}
{% fill "footer" %}
{% component "test" name='Joe2' %}
{% fill "header" %}
Name2: {{ name }}
{% endfill %}
{% fill "main" %}
Day2: {{ day }}
{% endfill %}
{% endcomponent %}
{% endfill %}
{% endcomponent %}
"""
self.template = Template(template_str)
# {{ name }} should be "Jannete" everywhere
rendered = self.template.render(Context({"day": "Monday", "name": "Jannete"}))
self.assertHTMLEqual(
rendered,
"""
<custom-template>
<header>Name: Jannete</header>
<main>Day: Monday</main>
<footer>
<custom-template>
<header>Name2: Jannete</header>
<main>Day2: Monday</main>
<footer>Default footer</footer>
</custom-template>
</footer>
</custom-template>
""",
)
def test_fill_inside_fill_with_same_name(self):
class SlottedComponent(component.Component):
template: types.django_html = """
{% load component_tags %}
<custom-template>
<header>{% slot "header" %}Default header{% endslot %}</header>
<main>{% slot "main" %}Default main{% endslot %}</main>
<footer>{% slot "footer" %}Default footer{% endslot %}</footer>
</custom-template>
"""
def get_context_data(self, name: Optional[str] = None) -> Dict[str, Any]:
return {
"name": name,
}
component.registry.register("test", SlottedComponent)
template_str: types.django_html = """
{% load component_tags %}
{% component "test" name='Igor' %}
{% fill "header" %}
{% component "test" name='Joe2' %}
{% fill "header" %}
Name2: {{ name }}
{% endfill %}
{% fill "main" %}
Day2: {{ day }}
{% endfill %}
{% fill "footer" %}
XYZ
{% endfill %}
{% endcomponent %}
{% endfill %}
{% fill "footer" %}
WWW
{% endfill %}
{% endcomponent %}
"""
self.template = Template(template_str)
# {{ name }} should be "Jannete" everywhere
rendered = self.template.render(Context({"day": "Monday", "name": "Jannete"}))
self.assertHTMLEqual(
rendered,
"""
<custom-template>
<header>
<custom-template>
<header>Name2: Jannete</header>
<main>Day2: Monday</main>
<footer>XYZ</footer>
</custom-template>
</header>
<main>Default main</main>
<footer>WWW</footer>
</custom-template>
""",
)
@override_settings(
COMPONENTS={
"context_behavior": "isolated",
},
)
def test_slots_of_top_level_comps_can_access_full_outer_ctx(self):
class SlottedComponent(component.Component):
template: types.django_html = """
{% load component_tags %}
<div>
<main>{% slot "main" default %}Easy to override{% endslot %}</main>
</div>
"""
def get_context_data(self, name: Optional[str] = None) -> Dict[str, Any]:
return {
"name": name,
}
component.registry.register("test", SlottedComponent)
template_str: types.django_html = """
{% load component_tags %}
<body>
{% component "test" %}
ABC: {{ name }} {{ some }}
{% endcomponent %}
</body>
"""
self.template = Template(template_str)
nested_ctx = Context()
# Check that the component can access vars across different context layers
nested_ctx.push({"some": "var"})
nested_ctx.push({"name": "carl"})
rendered = self.template.render(nested_ctx)
self.assertHTMLEqual(
rendered,
"""
<body>
<div>
<main> ABC: carl var </main>
</div>
</body>
""",
)
class DuplicateSlotTest(BaseTestCase):
class DuplicateSlotComponent(component.Component):
template: types.django_html = """
{% load component_tags %}
<header>{% slot "header" %}Default header{% endslot %}</header>
{# Slot name 'header' used twice. #}
<main>{% slot "header" %}Default main header{% endslot %}</main>
<footer>{% slot "footer" %}Default footer{% endslot %}</footer>
"""
def get_context_data(self, name: Optional[str] = None) -> Dict[str, Any]:
return {
"name": name,
}
class DuplicateSlotNestedComponent(component.Component):
template: types.django_html = """
{% load component_tags %}
{% slot "header" %}START{% endslot %}
<div class="dashboard-component">
{% component "calendar" date="2020-06-06" %}
{% fill "header" %} {# fills and slots with same name relate to diff. things. #}
{% slot "header" %}NESTED{% endslot %}
{% endfill %}
{% fill "body" %}Here are your to-do items for today:{% endfill %}
{% endcomponent %}
<ol>
{% for item in items %}
<li>{{ item }}</li>
{% slot "header" %}LOOP {{ item }} {% endslot %}
{% endfor %}
</ol>
</div>
"""
def get_context_data(self, items: List) -> Dict[str, Any]:
return {
"items": items,
}
class CalendarComponent(component.Component):
"""Nested in ComponentWithNestedComponent"""
template: types.django_html = """
{% load component_tags %}
<div class="calendar-component">
<h1>
{% slot "header" %}Today's date is <span>{{ date }}</span>{% endslot %}
</h1>
<main>
{% slot "body" %}
You have no events today.
{% endslot %}
</main>
</div>
"""
@classmethod
def setUpClass(cls):
super().setUpClass()
component.registry.register(name="duplicate_slot", component=cls.DuplicateSlotComponent)
component.registry.register(name="duplicate_slot_nested", component=cls.DuplicateSlotNestedComponent)
component.registry.register(name="calendar", component=cls.CalendarComponent)
def test_duplicate_slots(self):
template_str: types.django_html = """
{% load component_tags %}
{% component "duplicate_slot" %}
{% fill "header" %}
Name: {{ name }}
{% endfill %}
{% fill "footer" %}
Hello
{% endfill %}
{% endcomponent %}
"""
self.template = Template(template_str)
rendered = self.template.render(Context({"name": "Jannete"}))
self.assertHTMLEqual(
rendered,
"""
<header>Name: Jannete</header>
<main>Name: Jannete</main>
<footer>Hello</footer>
""",
)
def test_duplicate_slots_fallback(self):
template_str: types.django_html = """
{% load component_tags %}
{% component "duplicate_slot" %}
{% endcomponent %}
"""
self.template = Template(template_str)
rendered = self.template.render(Context({}))
# NOTE: Slots should have different fallbacks even though they use the same name
self.assertHTMLEqual(
rendered,
"""
<header>Default header</header>
<main>Default main header</main>
<footer>Default footer</footer>
""",
)
def test_duplicate_slots_nested(self):
template_str: types.django_html = """
{% load component_tags %}
{% component "duplicate_slot_nested" items=items %}
{% fill "header" %}
OVERRIDDEN!
{% endfill %}
{% endcomponent %}
"""
self.template = Template(template_str)
rendered = self.template.render(Context({"items": [1, 2, 3]}))
# NOTE: Slots should have different fallbacks even though they use the same name
self.assertHTMLEqual(
rendered,
"""
OVERRIDDEN!
<div class="dashboard-component">
<div class="calendar-component">
<h1>
OVERRIDDEN!
</h1>
<main>
Here are your to-do items for today:
</main>
</div>
<ol>
<li>1</li>
OVERRIDDEN!
<li>2</li>
OVERRIDDEN!
<li>3</li>
OVERRIDDEN!
</ol>
</div>
""",
)
def test_duplicate_slots_nested_fallback(self):
template_str: types.django_html = """
{% load component_tags %}
{% component "duplicate_slot_nested" items=items %}
{% endcomponent %}
"""
self.template = Template(template_str)
rendered = self.template.render(Context({"items": [1, 2, 3]}))
# NOTE: Slots should have different fallbacks even though they use the same name
self.assertHTMLEqual(
rendered,
"""
START
<div class="dashboard-component">
<div class="calendar-component">
<h1>
NESTED
</h1>
<main>
Here are your to-do items for today:
</main>
</div>
<ol>
<li>1</li>
LOOP 1
<li>2</li>
LOOP 2
<li>3</li>
LOOP 3
</ol>
</div>
""",
)
class InlineComponentTest(BaseTestCase):
def test_inline_html_component(self):
class InlineHTMLComponent(component.Component):
template = "<div class='inline'>Hello Inline</div>"
comp = InlineHTMLComponent("inline_html_component")
self.assertHTMLEqual(
comp.render(Context({})),
"<div class='inline'>Hello Inline</div>",
)
def test_html_and_css_only(self):
class HTMLCSSComponent(component.Component):
template = "<div class='html-css-only'>Content</div>"
css = ".html-css-only { color: blue; }"
comp = HTMLCSSComponent("html_css_component")
self.assertHTMLEqual(
comp.render(Context({})),
"<div class='html-css-only'>Content</div>",
)
self.assertHTMLEqual(
comp.render_css_dependencies(),
"<style>.html-css-only { color: blue; }</style>",
)
def test_html_and_js_only(self):
class HTMLJSComponent(component.Component):
template = "<div class='html-js-only'>Content</div>"
js = "console.log('HTML and JS only');"
comp = HTMLJSComponent("html_js_component")
self.assertHTMLEqual(
comp.render(Context({})),
"<div class='html-js-only'>Content</div>",
)
self.assertHTMLEqual(
comp.render_js_dependencies(),
"<script>console.log('HTML and JS only');</script>",
)
def test_html_string_with_css_js_files(self):
class HTMLStringFileCSSJSComponent(component.Component):
template = "<div class='html-string-file'>Content</div>"
class Media:
css = "path/to/style.css"
js = "path/to/script.js"
comp = HTMLStringFileCSSJSComponent("html_string_file_css_js_component")
self.assertHTMLEqual(
comp.render(Context({})),
"<div class='html-string-file'>Content</div>",
)
self.assertHTMLEqual(
comp.render_dependencies(),
"""
<link href="path/to/style.css" media="all" rel="stylesheet">
<script src="path/to/script.js"></script>
""",
)
def test_html_js_string_with_css_file(self):
class HTMLStringFileCSSJSComponent(component.Component):
template = "<div class='html-string-file'>Content</div>"
js = "console.log('HTML and JS only');"
class Media:
css = "path/to/style.css"
comp = HTMLStringFileCSSJSComponent("html_string_file_css_js_component")
self.assertHTMLEqual(
comp.render(Context({})),
"<div class='html-string-file'>Content</div>",
)
self.assertHTMLEqual(
comp.render_dependencies(),
"""
<link href="path/to/style.css" media="all" rel="stylesheet">
<script>console.log('HTML and JS only');</script>
""",
)
def test_html_css_string_with_js_file(self):
class HTMLStringFileCSSJSComponent(component.Component):
template = "<div class='html-string-file'>Content</div>"
css = ".html-string-file { color: blue; }"
class Media:
js = "path/to/script.js"
comp = HTMLStringFileCSSJSComponent("html_string_file_css_js_component")
self.assertHTMLEqual(
comp.render(Context({})),
"<div class='html-string-file'>Content</div>",
)
self.assertHTMLEqual(
comp.render_dependencies(),
"""
<style>.html-string-file { color: blue; }</style><script src="path/to/script.js"></script>
""",
)
def test_component_with_variable_in_html(self):
class VariableHTMLComponent(component.Component):
def get_template(self, context):
return Template("<div class='variable-html'>{{ variable }}</div>")
comp = VariableHTMLComponent("variable_html_component")
context = Context({"variable": "Dynamic Content"})
self.assertHTMLEqual(
comp.render(context),
"<div class='variable-html'>Dynamic Content</div>",
)
class ComponentMediaTests(BaseTestCase):
def test_component_media_with_strings(self):
class SimpleComponent(component.Component):
class Media:
css = "path/to/style.css"
js = "path/to/script.js"
comp = SimpleComponent("")
self.assertHTMLEqual(
comp.render_dependencies(),
"""
<link href="path/to/style.css" media="all" rel="stylesheet">
<script src="path/to/script.js"></script>
""",
)
def test_component_media_with_lists(self):
class SimpleComponent(component.Component):
class Media:
css = ["path/to/style.css", "path/to/style2.css"]
js = ["path/to/script.js"]
comp = SimpleComponent("")
self.assertHTMLEqual(
comp.render_dependencies(),
"""
<link href="path/to/style.css" media="all" rel="stylesheet">
<link href="path/to/style2.css" media="all" rel="stylesheet">
<script src="path/to/script.js"></script>
""",
)
def test_component_media_with_dict_and_list(self):
class SimpleComponent(component.Component):
class Media:
css = {
"all": "path/to/style.css",
"print": ["path/to/style2.css"],
"screen": "path/to/style3.css",
}
js = ["path/to/script.js"]
comp = SimpleComponent("")
self.assertHTMLEqual(
comp.render_dependencies(),
"""
<link href="path/to/style.css" media="all" rel="stylesheet">
<link href="path/to/style2.css" media="print" rel="stylesheet">
<link href="path/to/style3.css" media="screen" rel="stylesheet">
<script src="path/to/script.js"></script>
""",
)
def test_component_media_with_dict_with_list_and_list(self):
class SimpleComponent(component.Component):
class Media:
css = {"all": ["path/to/style.css"]}
js = ["path/to/script.js"]
comp = SimpleComponent("")
self.assertHTMLEqual(
comp.render_dependencies(),
"""
<link href="path/to/style.css" media="all" rel="stylesheet">
<script src="path/to/script.js"></script>
""",
)
# Settings required for autodiscover to work
@override_settings(
BASE_DIR=Path(__file__).resolve().parent,
STATICFILES_DIRS=[
Path(__file__).resolve().parent / "components",
],
)
def test_component_media_with_dict_with_relative_paths(self):
# Fix the paths, since the "components" dir is nested
with autodiscover_with_cleanup(map_import_paths=lambda p: f"tests.{p}"):
template_str: types.django_html = """
{% load component_tags %}{% component_dependencies %}
{% component name='relative_file_component' variable=variable %}
{% endcomponent %}
"""
template = Template(template_str)
rendered = template.render(Context({"variable": "test"}))
self.assertHTMLEqual(
rendered,
"""
<link href="relative_file/relative_file.css" media="all" rel="stylesheet">
<script src="relative_file/relative_file.js"></script>
<form method="post">
<input type="text" name="variable" value="test">
<input type="submit">
</form>
""",
)
class ComponentIsolationTests(BaseTestCase):
def setUp(self):
class SlottedComponent(component.Component):
template: types.django_html = """
{% load component_tags %}
<custom-template>
<header>{% slot "header" %}Default header{% endslot %}</header>
<main>{% slot "main" %}Default main{% endslot %}</main>
<footer>{% slot "footer" %}Default footer{% endslot %}</footer>
</custom-template>
"""
component.registry.register("test", SlottedComponent)
def test_instances_of_component_do_not_share_slots(self):
template_str: types.django_html = """
{% load component_tags %}
{% component "test" %}
{% fill "header" %}Override header{% endfill %}
{% endcomponent %}
{% component "test" %}
{% fill "main" %}Override main{% endfill %}
{% endcomponent %}
{% component "test" %}
{% fill "footer" %}Override footer{% endfill %}
{% endcomponent %}
"""
template = Template(template_str)
template.render(Context({}))
rendered = template.render(Context({}))
self.assertHTMLEqual(
rendered,
"""
<custom-template>
<header>Override header</header>
<main>Default main</main>
<footer>Default footer</footer>
</custom-template>
<custom-template>
<header>Default header</header>
<main>Override main</main>
<footer>Default footer</footer>
</custom-template>
<custom-template>
<header>Default header</header>
<main>Default main</main>
<footer>Override footer</footer>
</custom-template>
""",
)
class SlotBehaviorTests(BaseTestCase):
# NOTE: This is standalone function instead of setUp, so we can configure
# Django settings per test with `@override_settings`
def make_template(self) -> Template:
class SlottedComponent(component.Component):
template: types.django_html = """
{% load component_tags %}
<custom-template>
<header>{% slot "header" %}Default header{% endslot %}</header>
<main>{% slot "main" %}Default main{% endslot %}</main>
<footer>{% slot "footer" %}Default footer{% endslot %}</footer>
</custom-template>
"""
def get_context_data(self, name: Optional[str] = None) -> Dict[str, Any]:
return {
"name": name,
}
component.registry.register("test", SlottedComponent)
template_str: types.django_html = """
{% load component_tags %}
{% component "test" name='Igor' %}
{% fill "header" %}
Name: {{ name }}
{% endfill %}
{% fill "main" %}
Day: {{ day }}
{% endfill %}
{% fill "footer" %}
{% component "test" name='Joe2' %}
{% fill "header" %}
Name2: {{ name }}
{% endfill %}
{% fill "main" %}
Day2: {{ day }}
{% endfill %}
{% endcomponent %}
{% endfill %}
{% endcomponent %}
"""
return Template(template_str)
@override_settings(
COMPONENTS={"context_behavior": "django"},
)
def test_slot_context__django(self):
template = self.make_template()
# {{ name }} should be neither Jannete not empty, because overriden everywhere
rendered = template.render(Context({"day": "Monday", "name": "Jannete"}))
self.assertHTMLEqual(
rendered,
"""
<custom-template>
<header>Name: Igor</header>
<main>Day: Monday</main>
<footer>
<custom-template>
<header>Name2: Joe2</header>
<main>Day2: Monday</main>
<footer>Default footer</footer>
</custom-template>
</footer>
</custom-template>
""",
)
# {{ name }} should be effectively the same as before, because overriden everywhere
rendered2 = template.render(Context({"day": "Monday"}))
self.assertHTMLEqual(rendered2, rendered)
@override_settings(
COMPONENTS={"context_behavior": "isolated"},
)
def test_slot_context__isolated(self):
template = self.make_template()
# {{ name }} should be "Jannete" everywhere
rendered = template.render(Context({"day": "Monday", "name": "Jannete"}))
self.assertHTMLEqual(
rendered,
"""
<custom-template>
<header>Name: Jannete</header>
<main>Day: Monday</main>
<footer>
<custom-template>
<header>Name2: Jannete</header>
<main>Day2: Monday</main>
<footer>Default footer</footer>
</custom-template>
</footer>
</custom-template>
""",
)
# {{ name }} should be empty everywhere
rendered2 = template.render(Context({"day": "Monday"}))
self.assertHTMLEqual(
rendered2,
"""
<custom-template>
<header>Name: </header>
<main>Day: Monday</main>
<footer>
<custom-template>
<header>Name2: </header>
<main>Day2: Monday</main>
<footer>Default footer</footer>
</custom-template>
</footer>
</custom-template>
""",
)
class AggregateInputTests(BaseTestCase):
def test_agg_input_accessible_in_get_context_data(self):
@component.register("test")
class AttrsComponent(component.Component):
template: types.django_html = """
{% load component_tags %}
<div>
attrs: {{ attrs|safe }}
my_dict: {{ my_dict|safe }}
</div>
"""
def get_context_data(self, *args, attrs, my_dict):
return {"attrs": attrs, "my_dict": my_dict}
template_str: types.django_html = """
{% load component_tags %}
{% component "test" attrs:@click.stop="dispatch('click_event')" attrs:x-data="{hello: 'world'}" attrs:class=class_var my_dict:one=2 %}
{% endcomponent %}
""" # noqa: E501
template = Template(template_str)
rendered = template.render(Context({"class_var": "padding-top-8"}))
self.assertHTMLEqual(
rendered,
"""
<div>
attrs: {'@click.stop': "dispatch('click_event')", 'x-data': "{hello: 'world'}", 'class': 'padding-top-8'}
my_dict: {'one': 2}
</div>
""", # noqa: E501
)

View file

@ -8,7 +8,7 @@ from django.urls import path
# isort: off # isort: off
from .django_test_setup import * # noqa from .django_test_setup import * # noqa
from .testutils import BaseTestCase from .testutils import BaseTestCase, parametrize_context_behavior
# isort: on # isort: on
@ -113,6 +113,7 @@ class TestComponentAsView(BaseTestCase):
response.content, response.content,
) )
@parametrize_context_behavior(["django", "isolated"])
def test_replace_slot_in_view(self): def test_replace_slot_in_view(self):
class MockComponentSlot(component.Component): class MockComponentSlot(component.Component):
template = """ template = """
@ -141,6 +142,7 @@ class TestComponentAsView(BaseTestCase):
response.content, response.content,
) )
@parametrize_context_behavior(["django", "isolated"])
def test_replace_slot_in_view_with_insecure_content(self): def test_replace_slot_in_view_with_insecure_content(self):
class MockInsecureComponentSlot(component.Component): class MockInsecureComponentSlot(component.Component):
template = """ template = """
@ -162,6 +164,28 @@ class TestComponentAsView(BaseTestCase):
response.content, response.content,
) )
@parametrize_context_behavior(["django", "isolated"])
def test_replace_context_in_view(self):
class TestComponent(component.Component):
template = """
{% load component_tags %}
<div>
Hey, I'm {{ name }}
</div>
"""
def get(self, request, *args, **kwargs) -> HttpResponse:
return self.render_to_response({"name": "Bob"})
client = CustomClient(urlpatterns=[path("test_context_django/", TestComponent.as_view())])
response = client.get("/test_context_django/")
self.assertEqual(response.status_code, 200)
self.assertIn(
b"Hey, I'm Bob",
response.content,
)
@parametrize_context_behavior(["django", "isolated"])
def test_replace_context_in_view_with_insecure_content(self): def test_replace_context_in_view_with_insecure_content(self):
class MockInsecureComponentContext(component.Component): class MockInsecureComponentContext(component.Component):
template = """ template = """

View file

@ -0,0 +1,388 @@
import sys
from pathlib import Path
from django.template import Context, Template
from django.test import override_settings
# isort: off
from .django_test_setup import * # NOQA
from .testutils import BaseTestCase, autodiscover_with_cleanup
# isort: on
from django_components import component, types
class InlineComponentTest(BaseTestCase):
def test_html(self):
class InlineHTMLComponent(component.Component):
template = "<div class='inline'>Hello Inline</div>"
comp = InlineHTMLComponent("inline_html_component")
self.assertHTMLEqual(
comp.render(Context({})),
"<div class='inline'>Hello Inline</div>",
)
def test_html_and_css(self):
class HTMLCSSComponent(component.Component):
template = "<div class='html-css-only'>Content</div>"
css = ".html-css-only { color: blue; }"
comp = HTMLCSSComponent("html_css_component")
self.assertHTMLEqual(
comp.render(Context({})),
"<div class='html-css-only'>Content</div>",
)
self.assertHTMLEqual(
comp.render_css_dependencies(),
"<style>.html-css-only { color: blue; }</style>",
)
def test_html_and_js(self):
class HTMLJSComponent(component.Component):
template = "<div class='html-js-only'>Content</div>"
js = "console.log('HTML and JS only');"
comp = HTMLJSComponent("html_js_component")
self.assertHTMLEqual(
comp.render(Context({})),
"<div class='html-js-only'>Content</div>",
)
self.assertHTMLEqual(
comp.render_js_dependencies(),
"<script>console.log('HTML and JS only');</script>",
)
def test_html_inline_and_css_js_files(self):
class HTMLStringFileCSSJSComponent(component.Component):
template = "<div class='html-string-file'>Content</div>"
class Media:
css = "path/to/style.css"
js = "path/to/script.js"
comp = HTMLStringFileCSSJSComponent("html_string_file_css_js_component")
self.assertHTMLEqual(
comp.render(Context({})),
"<div class='html-string-file'>Content</div>",
)
self.assertHTMLEqual(
comp.render_dependencies(),
"""
<link href="path/to/style.css" media="all" rel="stylesheet">
<script src="path/to/script.js"></script>
""",
)
def test_html_js_inline_and_css_file(self):
class HTMLStringFileCSSJSComponent(component.Component):
template = "<div class='html-string-file'>Content</div>"
js = "console.log('HTML and JS only');"
class Media:
css = "path/to/style.css"
comp = HTMLStringFileCSSJSComponent("html_string_file_css_js_component")
self.assertHTMLEqual(
comp.render(Context({})),
"<div class='html-string-file'>Content</div>",
)
self.assertHTMLEqual(
comp.render_dependencies(),
"""
<link href="path/to/style.css" media="all" rel="stylesheet">
<script>console.log('HTML and JS only');</script>
""",
)
def test_html_css_inline_and_js_file(self):
class HTMLStringFileCSSJSComponent(component.Component):
template = "<div class='html-string-file'>Content</div>"
css = ".html-string-file { color: blue; }"
class Media:
js = "path/to/script.js"
comp = HTMLStringFileCSSJSComponent("html_string_file_css_js_component")
self.assertHTMLEqual(
comp.render(Context({})),
"<div class='html-string-file'>Content</div>",
)
self.assertHTMLEqual(
comp.render_dependencies(),
"""
<style>.html-string-file { color: blue; }</style><script src="path/to/script.js"></script>
""",
)
def test_html_variable(self):
class VariableHTMLComponent(component.Component):
def get_template(self, context):
return Template("<div class='variable-html'>{{ variable }}</div>")
comp = VariableHTMLComponent("variable_html_component")
context = Context({"variable": "Dynamic Content"})
self.assertHTMLEqual(
comp.render(context),
"<div class='variable-html'>Dynamic Content</div>",
)
def test_html_variable_filtered(self):
class FilteredComponent(component.Component):
template: types.django_html = """
Var1: <strong>{{ var1 }}</strong>
Var2 (uppercased): <strong>{{ var2|upper }}</strong>
"""
def get_context_data(self, var1=None, var2=None):
return {
"var1": var1,
"var2": var2,
}
comp = FilteredComponent("filtered_component")
context = Context(comp.get_context_data(var1="test1", var2="test2"))
self.assertHTMLEqual(
comp.render(context),
"""
Var1: <strong>test1</strong>
Var2 (uppercased): <strong>TEST2</strong>
""",
)
class ComponentMediaTests(BaseTestCase):
def test_css_and_js(self):
class SimpleComponent(component.Component):
template: types.django_html = """
Variable: <strong>{{ variable }}</strong>
"""
class Media:
css = "style.css"
js = "script.js"
comp = SimpleComponent("simple_component")
self.assertHTMLEqual(
comp.render_dependencies(),
"""
<link href="style.css" media="all" rel="stylesheet">
<script src="script.js"></script>
""",
)
def test_css_only(self):
class SimpleComponent(component.Component):
template: types.django_html = """
Variable: <strong>{{ variable }}</strong>
"""
class Media:
css = "style.css"
comp = SimpleComponent("simple_component")
self.assertHTMLEqual(
comp.render_dependencies(),
"""
<link href="style.css" media="all" rel="stylesheet">
""",
)
def test_js_only(self):
class SimpleComponent(component.Component):
template: types.django_html = """
Variable: <strong>{{ variable }}</strong>
"""
class Media:
js = "script.js"
comp = SimpleComponent("simple_component")
self.assertHTMLEqual(
comp.render_dependencies(),
"""
<script src="script.js"></script>
""",
)
def test_empty_media(self):
class SimpleComponent(component.Component):
template: types.django_html = """
Variable: <strong>{{ variable }}</strong>
"""
class Media:
pass
comp = SimpleComponent("simple_component")
self.assertHTMLEqual(comp.render_dependencies(), "")
def test_missing_media(self):
class SimpleComponent(component.Component):
template: types.django_html = """
Variable: <strong>{{ variable }}</strong>
"""
comp = SimpleComponent("simple_component")
self.assertHTMLEqual(comp.render_dependencies(), "")
def test_css_js_as_lists(self):
class SimpleComponent(component.Component):
class Media:
css = ["path/to/style.css", "path/to/style2.css"]
js = ["path/to/script.js"]
comp = SimpleComponent("")
self.assertHTMLEqual(
comp.render_dependencies(),
"""
<link href="path/to/style.css" media="all" rel="stylesheet">
<link href="path/to/style2.css" media="all" rel="stylesheet">
<script src="path/to/script.js"></script>
""",
)
def test_css_js_as_string(self):
class SimpleComponent(component.Component):
class Media:
css = "path/to/style.css"
js = "path/to/script.js"
comp = SimpleComponent("")
self.assertHTMLEqual(
comp.render_dependencies(),
"""
<link href="path/to/style.css" media="all" rel="stylesheet">
<script src="path/to/script.js"></script>
""",
)
def test_css_js_as_dict_and_list(self):
class SimpleComponent(component.Component):
class Media:
css = {
"all": "path/to/style.css",
"print": ["path/to/style2.css"],
"screen": "path/to/style3.css",
}
js = ["path/to/script.js"]
comp = SimpleComponent("")
self.assertHTMLEqual(
comp.render_dependencies(),
"""
<link href="path/to/style.css" media="all" rel="stylesheet">
<link href="path/to/style2.css" media="print" rel="stylesheet">
<link href="path/to/style3.css" media="screen" rel="stylesheet">
<script src="path/to/script.js"></script>
""",
)
class MediaRelativePathTests(BaseTestCase):
class ParentComponent(component.Component):
template: types.django_html = """
{% load component_tags %}
<div>
<h1>Parent content</h1>
{% component name="variable_display" shadowing_variable='override' new_variable='unique_val' %}
{% endcomponent %}
</div>
<div>
{% slot 'content' %}
<h2>Slot content</h2>
{% component name="variable_display" shadowing_variable='slot_default_override' new_variable='slot_default_unique' %}
{% endcomponent %}
{% endslot %}
</div>
""" # noqa
def get_context_data(self):
return {"shadowing_variable": "NOT SHADOWED"}
class VariableDisplay(component.Component):
template: types.django_html = """
{% load component_tags %}
<h1>Shadowing variable = {{ shadowing_variable }}</h1>
<h1>Uniquely named variable = {{ unique_variable }}</h1>
"""
def get_context_data(self, shadowing_variable=None, new_variable=None):
context = {}
if shadowing_variable is not None:
context["shadowing_variable"] = shadowing_variable
if new_variable is not None:
context["unique_variable"] = new_variable
return context
@classmethod
def setUpClass(cls):
super().setUpClass()
component.registry.register(name="parent_component", component=cls.ParentComponent)
component.registry.register(name="variable_display", component=cls.VariableDisplay)
# Settings required for autodiscover to work
@override_settings(
BASE_DIR=Path(__file__).resolve().parent,
STATICFILES_DIRS=[
Path(__file__).resolve().parent / "components",
],
)
def test_component_with_relative_media_paths(self):
# Ensure that the module is executed again after import in autodiscovery
if "tests.components.relative_file.relative_file" in sys.modules:
del sys.modules["tests.components.relative_file.relative_file"]
# Fix the paths, since the "components" dir is nested
with autodiscover_with_cleanup(map_import_paths=lambda p: f"tests.{p}"):
template_str: types.django_html = """
{% load component_tags %}{% component_dependencies %}
{% component name='relative_file_component' variable=variable %}
{% endcomponent %}
"""
template = Template(template_str)
rendered = template.render(Context({"variable": "test"}))
self.assertHTMLEqual(
rendered,
"""
<link href="relative_file/relative_file.css" media="all" rel="stylesheet">
<script src="relative_file/relative_file.js"></script>
<form method="post">
<input type="text" name="variable" value="test">
<input type="submit">
</form>
""",
)
# Settings required for autodiscover to work
@override_settings(
BASE_DIR=Path(__file__).resolve().parent,
STATICFILES_DIRS=[
Path(__file__).resolve().parent / "components",
],
)
def test_component_with_relative_media_paths_as_subcomponent(self):
# Ensure that the module is executed again after import in autodiscovery
if "tests.components.relative_file.relative_file" in sys.modules:
del sys.modules["tests.components.relative_file.relative_file"]
# Fix the paths, since the "components" dir is nested
with autodiscover_with_cleanup(map_import_paths=lambda p: f"tests.{p}"):
template_str: types.django_html = """
{% load component_tags %}{% component_dependencies %}
{% component 'parent_component' %}
{% fill 'content' %}
{% component name='relative_file_component' variable='hello' %}
{% endcomponent %}
{% endfill %}
{% endcomponent %}
"""
template = Template(template_str)
rendered = template.render(Context({}))
self.assertIn('<input type="text" name="variable" value="hello">', rendered, rendered)

View file

@ -1,12 +1,9 @@
from unittest.mock import PropertyMock, patch
from django.template import Context, Template from django.template import Context, Template
from django.test import override_settings
from django_components import component, types from django_components import component, types
from .django_test_setup import * # NOQA from .django_test_setup import * # NOQA
from .testutils import BaseTestCase from .testutils import BaseTestCase, parametrize_context_behavior
######################### #########################
# COMPONENTS # COMPONENTS
@ -94,6 +91,7 @@ class ContextTests(BaseTestCase):
component.registry.register(name="variable_display", component=VariableDisplay) component.registry.register(name="variable_display", component=VariableDisplay)
component.registry.register(name="parent_component", component=cls.ParentComponent) component.registry.register(name="parent_component", component=cls.ParentComponent)
@parametrize_context_behavior(["django", "isolated"])
def test_nested_component_context_shadows_parent_with_unfilled_slots_and_component_tag( def test_nested_component_context_shadows_parent_with_unfilled_slots_and_component_tag(
self, self,
): ):
@ -112,6 +110,7 @@ class ContextTests(BaseTestCase):
) )
self.assertNotIn("<h1>Shadowing variable = NOT SHADOWED</h1>", rendered, rendered) self.assertNotIn("<h1>Shadowing variable = NOT SHADOWED</h1>", rendered, rendered)
@parametrize_context_behavior(["django", "isolated"])
def test_nested_component_instances_have_unique_context_with_unfilled_slots_and_component_tag( def test_nested_component_instances_have_unique_context_with_unfilled_slots_and_component_tag(
self, self,
): ):
@ -130,6 +129,7 @@ class ContextTests(BaseTestCase):
rendered, rendered,
) )
@parametrize_context_behavior(["django", "isolated"])
def test_nested_component_context_shadows_parent_with_filled_slots(self): def test_nested_component_context_shadows_parent_with_filled_slots(self):
template_str: types.django_html = """ template_str: types.django_html = """
{% load component_tags %}{% component_dependencies %} {% load component_tags %}{% component_dependencies %}
@ -151,6 +151,7 @@ class ContextTests(BaseTestCase):
) )
self.assertNotIn("<h1>Shadowing variable = NOT SHADOWED</h1>", rendered, rendered) self.assertNotIn("<h1>Shadowing variable = NOT SHADOWED</h1>", rendered, rendered)
@parametrize_context_behavior(["django", "isolated"])
def test_nested_component_instances_have_unique_context_with_filled_slots(self): def test_nested_component_instances_have_unique_context_with_filled_slots(self):
template_str: types.django_html = """ template_str: types.django_html = """
{% load component_tags %} {% load component_tags %}
@ -172,6 +173,7 @@ class ContextTests(BaseTestCase):
rendered, rendered,
) )
@parametrize_context_behavior(["django", "isolated"])
def test_nested_component_context_shadows_outer_context_with_unfilled_slots_and_component_tag( def test_nested_component_context_shadows_outer_context_with_unfilled_slots_and_component_tag(
self, self,
): ):
@ -191,6 +193,7 @@ class ContextTests(BaseTestCase):
) )
self.assertNotIn("<h1>Shadowing variable = NOT SHADOWED</h1>", rendered, rendered) self.assertNotIn("<h1>Shadowing variable = NOT SHADOWED</h1>", rendered, rendered)
@parametrize_context_behavior(["django", "isolated"])
def test_nested_component_context_shadows_outer_context_with_filled_slots( def test_nested_component_context_shadows_outer_context_with_filled_slots(
self, self,
): ):
@ -243,42 +246,8 @@ class ParentArgsTests(BaseTestCase):
component.registry.register(name="parent_with_args", component=cls.ParentComponentWithArgs) component.registry.register(name="parent_with_args", component=cls.ParentComponentWithArgs)
component.registry.register(name="variable_display", component=VariableDisplay) component.registry.register(name="variable_display", component=VariableDisplay)
@override_settings( @parametrize_context_behavior(["django", "isolated"])
COMPONENTS={ def test_parent_args_can_be_drawn_from_context(self):
"context_behavior": "django",
}
)
def test_parent_args_can_be_drawn_from_context__django(self):
template_str: types.django_html = """
{% load component_tags %}{% component_dependencies %}
{% component 'parent_with_args' parent_value=parent_value %}
{% endcomponent %}
"""
template = Template(template_str)
rendered = template.render(Context({"parent_value": "passed_in"}))
self.assertHTMLEqual(
rendered,
"""
<div>
<h1>Parent content</h1>
<h1>Shadowing variable = passed_in</h1>
<h1>Uniquely named variable = unique_val</h1>
</div>
<div>
<h2>Slot content</h2>
<h1>Shadowing variable = slot_default_override</h1>
<h1>Uniquely named variable = passed_in</h1>
</div>
""",
)
@override_settings(
COMPONENTS={
"context_behavior": "isolated",
}
)
def test_parent_args_can_be_drawn_from_context__isolated(self):
template_str: types.django_html = """ template_str: types.django_html = """
{% load component_tags %}{% component_dependencies %} {% load component_tags %}{% component_dependencies %}
{% component 'parent_with_args' parent_value=parent_value %} {% component 'parent_with_args' parent_value=parent_value %}
@ -303,6 +272,7 @@ class ParentArgsTests(BaseTestCase):
""", """,
) )
@parametrize_context_behavior(["django", "isolated"])
def test_parent_args_available_outside_slots(self): def test_parent_args_available_outside_slots(self):
template_str: types.django_html = """ template_str: types.django_html = """
{% load component_tags %}{% component_dependencies %} {% load component_tags %}{% component_dependencies %}
@ -315,12 +285,16 @@ class ParentArgsTests(BaseTestCase):
self.assertIn("<h1>Uniquely named variable = passed_in</h1>", rendered, rendered) self.assertIn("<h1>Uniquely named variable = passed_in</h1>", rendered, rendered)
self.assertNotIn("<h1>Shadowing variable = NOT SHADOWED</h1>", rendered, rendered) self.assertNotIn("<h1>Shadowing variable = NOT SHADOWED</h1>", rendered, rendered)
@override_settings( # NOTE: Second arg in tuple are expected values passed through components.
COMPONENTS={ @parametrize_context_behavior(
"context_behavior": "django", [
} ("django", ("passed_in", "passed_in")),
("isolated", ("passed_in", "")),
]
) )
def test_parent_args_available_in_slots__django(self): def test_parent_args_available_in_slots(self, context_behavior_data):
first_val, second_val = context_behavior_data
template_str: types.django_html = """ template_str: types.django_html = """
{% load component_tags %}{% component_dependencies %} {% load component_tags %}{% component_dependencies %}
{% component 'parent_with_args' parent_value='passed_in' %} {% component 'parent_with_args' parent_value='passed_in' %}
@ -334,47 +308,15 @@ class ParentArgsTests(BaseTestCase):
rendered = template.render(Context()) rendered = template.render(Context())
self.assertHTMLEqual( self.assertHTMLEqual(
rendered, rendered,
""" f"""
<div> <div>
<h1>Parent content</h1> <h1>Parent content</h1>
<h1>Shadowing variable = passed_in</h1> <h1>Shadowing variable = {first_val}</h1>
<h1>Uniquely named variable = unique_val</h1> <h1>Uniquely named variable = unique_val</h1>
</div> </div>
<div> <div>
<h1>Shadowing variable = value_from_slot</h1> <h1>Shadowing variable = value_from_slot</h1>
<h1>Uniquely named variable = passed_in</h1> <h1>Uniquely named variable = {second_val}</h1>
</div>
""",
)
@override_settings(
COMPONENTS={
"context_behavior": "isolated",
}
)
def test_parent_args_not_available_in_slots__isolated(self):
template_str: types.django_html = """
{% load component_tags %}{% component_dependencies %}
{% component 'parent_with_args' parent_value='passed_in' %}
{% fill 'content' %}
{% component name='variable_display' shadowing_variable='value_from_slot' new_variable=inner_parent_value %}
{% endcomponent %}
{% endfill %}
{% endcomponent %}
""" # noqa: E501
template = Template(template_str)
rendered = template.render(Context())
self.assertHTMLEqual(
rendered,
"""
<div>
<h1>Parent content</h1>
<h1>Shadowing variable = passed_in</h1>
<h1>Uniquely named variable = unique_val</h1>
</div>
<div>
<h1>Shadowing variable = value_from_slot</h1>
<h1>Uniquely named variable = </h1>
</div> </div>
""", """,
) )
@ -386,6 +328,7 @@ class ContextCalledOnceTests(BaseTestCase):
super().setUpClass() super().setUpClass()
component.registry.register(name="incrementer", component=IncrementerComponent) component.registry.register(name="incrementer", component=IncrementerComponent)
@parametrize_context_behavior(["django", "isolated"])
def test_one_context_call_with_simple_component(self): def test_one_context_call_with_simple_component(self):
template_str: types.django_html = """ template_str: types.django_html = """
{% load component_tags %}{% component_dependencies %} {% load component_tags %}{% component_dependencies %}
@ -398,6 +341,7 @@ class ContextCalledOnceTests(BaseTestCase):
'<p class="incrementer">value=1;calls=1</p>', '<p class="incrementer">value=1;calls=1</p>',
) )
@parametrize_context_behavior(["django", "isolated"])
def test_one_context_call_with_simple_component_and_arg(self): def test_one_context_call_with_simple_component_and_arg(self):
template_str: types.django_html = """ template_str: types.django_html = """
{% load component_tags %} {% load component_tags %}
@ -408,6 +352,7 @@ class ContextCalledOnceTests(BaseTestCase):
self.assertHTMLEqual(rendered, '<p class="incrementer">value=3;calls=1</p>', rendered) self.assertHTMLEqual(rendered, '<p class="incrementer">value=3;calls=1</p>', rendered)
@parametrize_context_behavior(["django", "isolated"])
def test_one_context_call_with_component(self): def test_one_context_call_with_component(self):
template_str: types.django_html = """ template_str: types.django_html = """
{% load component_tags %} {% load component_tags %}
@ -418,6 +363,7 @@ class ContextCalledOnceTests(BaseTestCase):
self.assertHTMLEqual(rendered, '<p class="incrementer">value=1;calls=1</p>', rendered) self.assertHTMLEqual(rendered, '<p class="incrementer">value=1;calls=1</p>', rendered)
@parametrize_context_behavior(["django", "isolated"])
def test_one_context_call_with_component_and_arg(self): def test_one_context_call_with_component_and_arg(self):
template_str: types.django_html = """ template_str: types.django_html = """
{% load component_tags %} {% load component_tags %}
@ -428,6 +374,7 @@ class ContextCalledOnceTests(BaseTestCase):
self.assertHTMLEqual(rendered, '<p class="incrementer">value=4;calls=1</p>', rendered) self.assertHTMLEqual(rendered, '<p class="incrementer">value=4;calls=1</p>', rendered)
@parametrize_context_behavior(["django", "isolated"])
def test_one_context_call_with_slot(self): def test_one_context_call_with_slot(self):
template_str: types.django_html = """ template_str: types.django_html = """
{% load component_tags %} {% load component_tags %}
@ -446,6 +393,7 @@ class ContextCalledOnceTests(BaseTestCase):
rendered, rendered,
) )
@parametrize_context_behavior(["django", "isolated"])
def test_one_context_call_with_slot_and_arg(self): def test_one_context_call_with_slot_and_arg(self):
template_str: types.django_html = """ template_str: types.django_html = """
{% load component_tags %} {% load component_tags %}
@ -471,10 +419,14 @@ class ComponentsCanAccessOuterContext(BaseTestCase):
super().setUpClass() super().setUpClass()
component.registry.register(name="simple_component", component=SimpleComponent) component.registry.register(name="simple_component", component=SimpleComponent)
@override_settings( # NOTE: Second arg in tuple is expected value.
COMPONENTS={"context_behavior": "django"}, @parametrize_context_behavior(
[
("django", "outer_value"),
("isolated", ""),
]
) )
def test_simple_component_can_use_outer_context__django(self): def test_simple_component_can_use_outer_context(self, context_behavior_data):
template_str: types.django_html = """ template_str: types.django_html = """
{% load component_tags %}{% component_dependencies %} {% load component_tags %}{% component_dependencies %}
{% component 'simple_component' %}{% endcomponent %} {% component 'simple_component' %}{% endcomponent %}
@ -483,25 +435,8 @@ class ComponentsCanAccessOuterContext(BaseTestCase):
rendered = template.render(Context({"variable": "outer_value"})) rendered = template.render(Context({"variable": "outer_value"}))
self.assertHTMLEqual( self.assertHTMLEqual(
rendered, rendered,
""" f"""
Variable: <strong> outer_value </strong> Variable: <strong> {context_behavior_data} </strong>
""",
)
@override_settings(
COMPONENTS={"context_behavior": "isolated"},
)
def test_simple_component_cannot_use_outer_context__isolated(self):
template_str: types.django_html = """
{% load component_tags %}{% component_dependencies %}
{% component 'simple_component' %}{% endcomponent %}
"""
template = Template(template_str)
rendered = template.render(Context({"variable": "outer_value"}))
self.assertHTMLEqual(
rendered,
"""
Variable: <strong> </strong>
""", """,
) )
@ -512,6 +447,7 @@ class IsolatedContextTests(BaseTestCase):
super().setUpClass() super().setUpClass()
component.registry.register(name="simple_component", component=SimpleComponent) component.registry.register(name="simple_component", component=SimpleComponent)
@parametrize_context_behavior(["django", "isolated"])
def test_simple_component_can_pass_outer_context_in_args(self): def test_simple_component_can_pass_outer_context_in_args(self):
template_str: types.django_html = """ template_str: types.django_html = """
{% load component_tags %}{% component_dependencies %} {% load component_tags %}{% component_dependencies %}
@ -521,6 +457,7 @@ class IsolatedContextTests(BaseTestCase):
rendered = template.render(Context({"variable": "outer_value"})).strip() rendered = template.render(Context({"variable": "outer_value"})).strip()
self.assertIn("outer_value", rendered, rendered) self.assertIn("outer_value", rendered, rendered)
@parametrize_context_behavior(["django", "isolated"])
def test_simple_component_cannot_use_outer_context(self): def test_simple_component_cannot_use_outer_context(self):
template_str: types.django_html = """ template_str: types.django_html = """
{% load component_tags %}{% component_dependencies %} {% load component_tags %}{% component_dependencies %}
@ -537,17 +474,7 @@ class IsolatedContextSettingTests(BaseTestCase):
super().setUpClass() super().setUpClass()
component.registry.register(name="simple_component", component=SimpleComponent) component.registry.register(name="simple_component", component=SimpleComponent)
def setUp(self): @parametrize_context_behavior(["isolated"])
self.patcher = patch(
"django_components.app_settings.AppSettings.CONTEXT_BEHAVIOR",
new_callable=PropertyMock,
)
self.mock_isolate_context = self.patcher.start()
self.mock_isolate_context.return_value = "isolated"
def tearDown(self):
self.patcher.stop()
def test_component_tag_includes_variable_with_isolated_context_from_settings( def test_component_tag_includes_variable_with_isolated_context_from_settings(
self, self,
): ):
@ -559,6 +486,7 @@ class IsolatedContextSettingTests(BaseTestCase):
rendered = template.render(Context({"variable": "outer_value"})) rendered = template.render(Context({"variable": "outer_value"}))
self.assertIn("outer_value", rendered, rendered) self.assertIn("outer_value", rendered, rendered)
@parametrize_context_behavior(["isolated"])
def test_component_tag_excludes_variable_with_isolated_context_from_settings( def test_component_tag_excludes_variable_with_isolated_context_from_settings(
self, self,
): ):
@ -570,6 +498,7 @@ class IsolatedContextSettingTests(BaseTestCase):
rendered = template.render(Context({"variable": "outer_value"})) rendered = template.render(Context({"variable": "outer_value"}))
self.assertNotIn("outer_value", rendered, rendered) self.assertNotIn("outer_value", rendered, rendered)
@parametrize_context_behavior(["isolated"])
def test_component_includes_variable_with_isolated_context_from_settings( def test_component_includes_variable_with_isolated_context_from_settings(
self, self,
): ):
@ -582,6 +511,7 @@ class IsolatedContextSettingTests(BaseTestCase):
rendered = template.render(Context({"variable": "outer_value"})) rendered = template.render(Context({"variable": "outer_value"}))
self.assertIn("outer_value", rendered, rendered) self.assertIn("outer_value", rendered, rendered)
@parametrize_context_behavior(["isolated"])
def test_component_excludes_variable_with_isolated_context_from_settings( def test_component_excludes_variable_with_isolated_context_from_settings(
self, self,
): ):
@ -609,10 +539,8 @@ class OuterContextPropertyTests(BaseTestCase):
super().setUpClass() super().setUpClass()
component.registry.register(name="outer_context_component", component=cls.OuterContextComponent) component.registry.register(name="outer_context_component", component=cls.OuterContextComponent)
@override_settings( @parametrize_context_behavior(["django", "isolated"])
COMPONENTS={"context_behavior": "django"}, def test_outer_context_property_with_component(self):
)
def test_outer_context_property_with_component__django(self):
template_str: types.django_html = """ template_str: types.django_html = """
{% load component_tags %}{% component_dependencies %} {% load component_tags %}{% component_dependencies %}
{% component 'outer_context_component' only %}{% endcomponent %} {% component 'outer_context_component' only %}{% endcomponent %}
@ -621,14 +549,216 @@ class OuterContextPropertyTests(BaseTestCase):
rendered = template.render(Context({"variable": "outer_value"})).strip() rendered = template.render(Context({"variable": "outer_value"})).strip()
self.assertIn("outer_value", rendered, rendered) self.assertIn("outer_value", rendered, rendered)
@override_settings(
COMPONENTS={"context_behavior": "isolated"}, class ContextVarsIsFilledTests(BaseTestCase):
) class IsFilledVarsComponent(component.Component):
def test_outer_context_property_with_component__isolated(self): template: types.django_html = """
template_str: types.django_html = """ {% load component_tags %}
{% load component_tags %}{% component_dependencies %} <div class="frontmatter-component">
{% component 'outer_context_component' only %}{% endcomponent %} {% slot "title" default %}{% endslot %}
{% slot "my_title" %}{% endslot %}
{% slot "my title 1" %}{% endslot %}
{% slot "my-title-2" %}{% endslot %}
{% slot "escape this: #$%^*()" %}{% endslot %}
{{ component_vars.is_filled|safe }}
</div>
""" """
template = Template(template_str)
rendered = template.render(Context({"variable": "outer_value"})).strip() class ComponentWithConditionalSlots(component.Component):
self.assertIn("outer_value", rendered, rendered) template: types.django_html = """
{# Example from django-components/issues/98 #}
{% load component_tags %}
<div class="frontmatter-component">
<div class="title">{% slot "title" %}Title{% endslot %}</div>
{% if component_vars.is_filled.subtitle %}
<div class="subtitle">
{% slot "subtitle" %}Optional subtitle
{% endslot %}
</div>
{% endif %}
</div>
"""
class ComponentWithComplexConditionalSlots(component.Component):
template: types.django_html = """
{# Example from django-components/issues/98 #}
{% load component_tags %}
<div class="frontmatter-component">
<div class="title">{% slot "title" %}Title{% endslot %}</div>
{% if component_vars.is_filled.subtitle %}
<div class="subtitle">{% slot "subtitle" %}Optional subtitle{% endslot %}</div>
{% elif component_vars.is_filled.alt_subtitle %}
<div class="subtitle">{% slot "alt_subtitle" %}Why would you want this?{% endslot %}</div>
{% else %}
<div class="warning">Nothing filled!</div>
{% endif %}
</div>
"""
class ComponentWithNegatedConditionalSlot(component.Component):
template: types.django_html = """
{# Example from django-components/issues/98 #}
{% load component_tags %}
<div class="frontmatter-component">
<div class="title">{% slot "title" %}Title{% endslot %}</div>
{% if not component_vars.is_filled.subtitle %}
<div class="warning">Subtitle not filled!</div>
{% else %}
<div class="subtitle">{% slot "alt_subtitle" %}Why would you want this?{% endslot %}</div>
{% endif %}
</div>
"""
@classmethod
def setUpClass(cls) -> None:
super().setUpClass()
component.registry.register("is_filled_vars", cls.IsFilledVarsComponent)
component.registry.register("conditional_slots", cls.ComponentWithConditionalSlots)
component.registry.register(
"complex_conditional_slots",
cls.ComponentWithComplexConditionalSlots,
)
component.registry.register("negated_conditional_slot", cls.ComponentWithNegatedConditionalSlot)
@classmethod
def tearDownClass(cls) -> None:
super().tearDownClass()
component.registry.clear()
@parametrize_context_behavior(["django", "isolated"])
def test_is_filled_vars(self):
template: types.django_html = """
{% load component_tags %}
{% component "is_filled_vars" %}
{% fill "title" %}{% endfill %}
{% fill "my-title-2" %}{% endfill %}
{% fill "escape this: #$%^*()" %}{% endfill %}
{% endcomponent %}
"""
rendered = Template(template).render(Context())
expected = """
<div class="frontmatter-component">
{'title': True,
'my_title': False,
'my_title_1': False,
'my_title_2': True,
'escape_this_________': True}
</div>
"""
self.assertHTMLEqual(rendered, expected)
@parametrize_context_behavior(["django", "isolated"])
def test_is_filled_vars_default(self):
template: types.django_html = """
{% load component_tags %}
{% component "is_filled_vars" %}
bla bla
{% endcomponent %}
"""
rendered = Template(template).render(Context())
expected = """
<div class="frontmatter-component">
bla bla
{'title': True,
'my_title': False,
'my_title_1': False,
'my_title_2': False,
'escape_this_________': False}
</div>
"""
self.assertHTMLEqual(rendered, expected)
@parametrize_context_behavior(["django", "isolated"])
def test_simple_component_with_conditional_slot(self):
template: types.django_html = """
{% load component_tags %}
{% component "conditional_slots" %}{% endcomponent %}
"""
expected = """
<div class="frontmatter-component">
<div class="title">
Title
</div>
</div>
"""
rendered = Template(template).render(Context({}))
self.assertHTMLEqual(rendered, expected)
@parametrize_context_behavior(["django", "isolated"])
def test_component_with_filled_conditional_slot(self):
template: types.django_html = """
{% load component_tags %}
{% component "conditional_slots" %}
{% fill "subtitle" %} My subtitle {% endfill %}
{% endcomponent %}
"""
expected = """
<div class="frontmatter-component">
<div class="title">
Title
</div>
<div class="subtitle">
My subtitle
</div>
</div>
"""
rendered = Template(template).render(Context({}))
self.assertHTMLEqual(rendered, expected)
@parametrize_context_behavior(["django", "isolated"])
def test_elif_of_complex_conditional_slots(self):
template: types.django_html = """
{% load component_tags %}
{% component "complex_conditional_slots" %}
{% fill "alt_subtitle" %} A different subtitle {% endfill %}
{% endcomponent %}
"""
expected = """
<div class="frontmatter-component">
<div class="title">
Title
</div>
<div class="subtitle">
A different subtitle
</div>
</div>
"""
rendered = Template(template).render(Context({}))
self.assertHTMLEqual(rendered, expected)
@parametrize_context_behavior(["django", "isolated"])
def test_else_of_complex_conditional_slots(self):
template: types.django_html = """
{% load component_tags %}
{% component "complex_conditional_slots" %}
{% endcomponent %}
"""
expected = """
<div class="frontmatter-component">
<div class="title">
Title
</div>
<div class="warning">Nothing filled!</div>
</div>
"""
rendered = Template(template).render(Context({}))
self.assertHTMLEqual(rendered, expected)
@parametrize_context_behavior(["django", "isolated"])
def test_component_with_negated_conditional_slot(self):
template: types.django_html = """
{% load component_tags %}
{% component "negated_conditional_slot" %}
{# Whoops! Forgot to fill a slot! #}
{% endcomponent %}
"""
expected = """
<div class="frontmatter-component">
<div class="title">
Title
</div>
<div class="warning">Subtitle not filled!</div>
</div>
"""
rendered = Template(template).render(Context({}))
self.assertHTMLEqual(rendered, expected)

View file

@ -12,7 +12,9 @@ from .testutils import BaseTestCase, create_and_process_template_response
class SimpleComponent(component.Component): class SimpleComponent(component.Component):
template_name = "simple_template.html" template: types.django_html = """
Variable: <strong>{{ variable }}</strong>
"""
def get_context_data(self, variable, variable2="default"): def get_context_data(self, variable, variable2="default"):
return { return {

View file

@ -3,7 +3,7 @@ from django.template.base import Parser
# isort: off # isort: off
from .django_test_setup import * # NOQA from .django_test_setup import * # NOQA
from .testutils import BaseTestCase from .testutils import BaseTestCase, parametrize_context_behavior
# isort: on # isort: on
@ -56,21 +56,23 @@ class ParserTest(BaseTestCase):
class ParserComponentTest(BaseTestCase): class ParserComponentTest(BaseTestCase):
def test_special_chars_accessible_via_kwargs(self): class SimpleComponent(component.Component):
@component.register(name="test") template: types.django_html = """
class SimpleComponent(component.Component): {{ date }}
template: types.django_html = """ {{ id }}
{{ date }} {{ on_click }}
{{ id }} """
{{ on_click }}
"""
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
return { return {
"date": kwargs["my-date"], "date": kwargs["my-date"],
"id": kwargs["#some_id"], "id": kwargs["#some_id"],
"on_click": kwargs["@click.native"], "on_click": kwargs["@click.native"],
} }
@parametrize_context_behavior(["django", "isolated"])
def test_special_chars_accessible_via_kwargs(self):
component.registry.register("test", self.SimpleComponent)
template_str: types.django_html = """ template_str: types.django_html = """
{% load component_tags %} {% load component_tags %}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,417 @@
import textwrap
from django.template import Context, Template, TemplateSyntaxError
# isort: off
from .django_test_setup import * # NOQA
from .testutils import BaseTestCase, parametrize_context_behavior
# isort: on
import django_components
import django_components.component_registry
from django_components import component, types
class SlottedComponent(component.Component):
template_name = "slotted_template.html"
class SlottedComponentWithContext(component.Component):
template: types.django_html = """
{% load component_tags %}
<custom-template>
<header>{% slot "header" %}Default header{% endslot %}</header>
<main>{% slot "main" %}Default main{% endslot %}</main>
<footer>{% slot "footer" %}Default footer{% endslot %}</footer>
</custom-template>
"""
def get_context_data(self, variable):
return {"variable": variable}
#######################
# TESTS
#######################
class ComponentTemplateTagTest(BaseTestCase):
class SimpleComponent(component.Component):
template_name = "simple_template.html"
def get_context_data(self, variable, variable2="default"):
return {
"variable": variable,
"variable2": variable2,
}
class Media:
css = "style.css"
js = "script.js"
def setUp(self):
# NOTE: component.registry is global, so need to clear before each test
component.registry.clear()
@parametrize_context_behavior(["django", "isolated"])
def test_single_component(self):
component.registry.register(name="test", component=self.SimpleComponent)
simple_tag_template: types.django_html = """
{% load component_tags %}
{% component name="test" variable="variable" %}{% endcomponent %}
"""
template = Template(simple_tag_template)
rendered = template.render(Context({}))
self.assertHTMLEqual(rendered, "Variable: <strong>variable</strong>\n")
@parametrize_context_behavior(["django", "isolated"])
def test_call_with_invalid_name(self):
# Note: No tag registered
simple_tag_template: types.django_html = """
{% load component_tags %}
{% component name="test" variable="variable" %}{% endcomponent %}
"""
template = Template(simple_tag_template)
with self.assertRaises(django_components.component_registry.NotRegistered):
template.render(Context({}))
@parametrize_context_behavior(["django", "isolated"])
def test_component_called_with_positional_name(self):
component.registry.register(name="test", component=self.SimpleComponent)
simple_tag_template: types.django_html = """
{% load component_tags %}
{% component "test" variable="variable" %}{% endcomponent %}
"""
template = Template(simple_tag_template)
rendered = template.render(Context({}))
self.assertHTMLEqual(rendered, "Variable: <strong>variable</strong>\n")
@parametrize_context_behavior(["django", "isolated"])
def test_call_component_with_two_variables(self):
@component.register("test")
class IffedComponent(component.Component):
template: types.django_html = """
Variable: <strong>{{ variable }}</strong>
{% if variable2 != "default" %}
Variable2: <strong>{{ variable2 }}</strong>
{% endif %}
"""
def get_context_data(self, variable, variable2="default"):
return {
"variable": variable,
"variable2": variable2,
}
class Media:
css = "style.css"
js = "script.js"
simple_tag_template: types.django_html = """
{% load component_tags %}
{% component name="test" variable="variable" variable2="hej" %}{% endcomponent %}
"""
template = Template(simple_tag_template)
rendered = template.render(Context({}))
expected_outcome = """Variable: <strong>variable</strong>\n""" """Variable2: <strong>hej</strong>"""
self.assertHTMLEqual(rendered, textwrap.dedent(expected_outcome))
@parametrize_context_behavior(["django", "isolated"])
def test_component_called_with_singlequoted_name(self):
component.registry.register(name="test", component=self.SimpleComponent)
simple_tag_template: types.django_html = """
{% load component_tags %}
{% component 'test' variable="variable" %}{% endcomponent %}
"""
template = Template(simple_tag_template)
rendered = template.render(Context({}))
self.assertHTMLEqual(rendered, "Variable: <strong>variable</strong>\n")
@parametrize_context_behavior(["django", "isolated"])
def test_component_called_with_variable_as_name(self):
component.registry.register(name="test", component=self.SimpleComponent)
simple_tag_template: types.django_html = """
{% load component_tags %}
{% with component_name="test" %}
{% component component_name variable="variable" %}{% endcomponent %}
{% endwith %}
"""
template = Template(simple_tag_template)
rendered = template.render(Context({}))
self.assertHTMLEqual(rendered, "Variable: <strong>variable</strong>\n")
@parametrize_context_behavior(["django", "isolated"])
def test_component_called_with_invalid_variable_as_name(self):
component.registry.register(name="test", component=self.SimpleComponent)
simple_tag_template: types.django_html = """
{% load component_tags %}
{% with component_name="BLAHONGA" %}
{% component component_name variable="variable" %}{% endcomponent %}
{% endwith %}
"""
template = Template(simple_tag_template)
with self.assertRaises(django_components.component_registry.NotRegistered):
template.render(Context({}))
@parametrize_context_behavior(["django", "isolated"])
def test_component_accepts_provided_and_default_parameters(self):
@component.register("test")
class ComponentWithProvidedAndDefaultParameters(component.Component):
template: types.django_html = """
Provided variable: <strong>{{ variable }}</strong>
Default: <p>{{ default_param }}</p>
"""
def get_context_data(self, variable, default_param="default text"):
return {"variable": variable, "default_param": default_param}
template_str: types.django_html = """
{% load component_tags %}
{% component "test" variable="provided value" %}
{% endcomponent %}
"""
template = Template(template_str)
rendered = template.render(Context({}))
self.assertHTMLEqual(
rendered,
"Provided variable: <strong>provided value</strong>\nDefault: <p>default text</p>",
)
class MultiComponentTests(BaseTestCase):
def setUp(self):
component.registry.clear()
def register_components(self):
component.registry.register("first_component", SlottedComponent)
component.registry.register("second_component", SlottedComponentWithContext)
def make_template(self, first_slot: str = "", second_slot: str = "") -> Template:
template_str: types.django_html = f"""
{{% load component_tags %}}
{{% component 'first_component' %}}
{first_slot}
{{% endcomponent %}}
{{% component 'second_component' variable='xyz' %}}
{second_slot}
{{% endcomponent %}}
"""
return Template(template_str)
def expected_result(self, first_slot: str = "", second_slot: str = "") -> str:
first_slot = first_slot or "Default header"
second_slot = second_slot or "Default header"
return f"""
<custom-template>
<header>{first_slot}</header>
<main>Default main</main>
<footer>Default footer</footer>
</custom-template>
<custom-template>
<header>{second_slot}</header>
<main>Default main</main>
<footer>Default footer</footer>
</custom-template>
"""
def wrap_with_slot_tags(self, s):
return '{% fill "header" %}' + s + "{% endfill %}"
@parametrize_context_behavior(["django", "isolated"])
def test_both_components_render_correctly_with_no_slots(self):
self.register_components()
rendered = self.make_template().render(Context({}))
self.assertHTMLEqual(rendered, self.expected_result())
@parametrize_context_behavior(["django", "isolated"])
def test_both_components_render_correctly_with_slots(self):
self.register_components()
first_slot_content = "<p>Slot #1</p>"
second_slot_content = "<div>Slot #2</div>"
first_slot = self.wrap_with_slot_tags(first_slot_content)
second_slot = self.wrap_with_slot_tags(second_slot_content)
rendered = self.make_template(first_slot, second_slot).render(Context({}))
self.assertHTMLEqual(
rendered,
self.expected_result(first_slot_content, second_slot_content),
)
@parametrize_context_behavior(["django", "isolated"])
def test_both_components_render_correctly_when_only_first_has_slots(self):
self.register_components()
first_slot_content = "<p>Slot #1</p>"
first_slot = self.wrap_with_slot_tags(first_slot_content)
rendered = self.make_template(first_slot).render(Context({}))
self.assertHTMLEqual(rendered, self.expected_result(first_slot_content))
@parametrize_context_behavior(["django", "isolated"])
def test_both_components_render_correctly_when_only_second_has_slots(self):
self.register_components()
second_slot_content = "<div>Slot #2</div>"
second_slot = self.wrap_with_slot_tags(second_slot_content)
rendered = self.make_template("", second_slot).render(Context({}))
self.assertHTMLEqual(rendered, self.expected_result("", second_slot_content))
class ComponentIsolationTests(BaseTestCase):
def setUp(self):
class SlottedComponent(component.Component):
template: types.django_html = """
{% load component_tags %}
<custom-template>
<header>{% slot "header" %}Default header{% endslot %}</header>
<main>{% slot "main" %}Default main{% endslot %}</main>
<footer>{% slot "footer" %}Default footer{% endslot %}</footer>
</custom-template>
"""
component.registry.register("test", SlottedComponent)
@parametrize_context_behavior(["django", "isolated"])
def test_instances_of_component_do_not_share_slots(self):
template_str: types.django_html = """
{% load component_tags %}
{% component "test" %}
{% fill "header" %}Override header{% endfill %}
{% endcomponent %}
{% component "test" %}
{% fill "main" %}Override main{% endfill %}
{% endcomponent %}
{% component "test" %}
{% fill "footer" %}Override footer{% endfill %}
{% endcomponent %}
"""
template = Template(template_str)
template.render(Context({}))
rendered = template.render(Context({}))
self.assertHTMLEqual(
rendered,
"""
<custom-template>
<header>Override header</header>
<main>Default main</main>
<footer>Default footer</footer>
</custom-template>
<custom-template>
<header>Default header</header>
<main>Override main</main>
<footer>Default footer</footer>
</custom-template>
<custom-template>
<header>Default header</header>
<main>Default main</main>
<footer>Override footer</footer>
</custom-template>
""",
)
class AggregateInputTests(BaseTestCase):
@parametrize_context_behavior(["django", "isolated"])
def test_agg_input_accessible_in_get_context_data(self):
@component.register("test")
class AttrsComponent(component.Component):
template: types.django_html = """
{% load component_tags %}
<div>
attrs: {{ attrs|safe }}
my_dict: {{ my_dict|safe }}
</div>
"""
def get_context_data(self, *args, attrs, my_dict):
return {"attrs": attrs, "my_dict": my_dict}
template_str: types.django_html = """
{% load component_tags %}
{% component "test" attrs:@click.stop="dispatch('click_event')" attrs:x-data="{hello: 'world'}" attrs:class=class_var my_dict:one=2 %}
{% endcomponent %}
""" # noqa: E501
template = Template(template_str)
rendered = template.render(Context({"class_var": "padding-top-8"}))
self.assertHTMLEqual(
rendered,
"""
<div>
attrs: {'@click.stop': "dispatch('click_event')", 'x-data': "{hello: 'world'}", 'class': 'padding-top-8'}
my_dict: {'one': 2}
</div>
""", # noqa: E501
)
class ComponentTemplateSyntaxErrorTests(BaseTestCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
component.registry.register("test", SlottedComponent)
@classmethod
def tearDownClass(cls) -> None:
super().tearDownClass()
component.registry.clear()
@parametrize_context_behavior(["django", "isolated"])
def test_variable_outside_fill_tag_compiles_w_out_error(self):
# As of v0.28 this is valid, provided the component registered under "test"
# contains a slot tag marked as 'default'. This is verified outside
# template compilation time.
template_str: types.django_html = """
{% load component_tags %}
{% component "test" %}
{{ anything }}
{% endcomponent %}
"""
Template(template_str)
@parametrize_context_behavior(["django", "isolated"])
def test_text_outside_fill_tag_is_not_error(self):
# As of v0.28 this is valid, provided the component registered under "test"
# contains a slot tag marked as 'default'. This is verified outside
# template compilation time.
template_str: types.django_html = """
{% load component_tags %}
{% component "test" %}
Text
{% endcomponent %}
"""
Template(template_str)
@parametrize_context_behavior(["django", "isolated"])
def test_nonfill_block_outside_fill_tag_is_error(self):
with self.assertRaises(TemplateSyntaxError):
template_str: types.django_html = """
{% load component_tags %}
{% component "test" %}
{% if True %}
{% fill "header" %}{% endfill %}
{% endif %}
{% endcomponent %}
"""
Template(template_str)
@parametrize_context_behavior(["django", "isolated"])
def test_unclosed_component_is_error(self):
with self.assertRaises(TemplateSyntaxError):
template_str: types.django_html = """
{% load component_tags %}
{% component "test" %}
{% fill "header" %}{% endfill %}
"""
Template(template_str)

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,826 @@
"""This file tests various ways how the individual tags can be combined inside the templates"""
from typing import Any, Dict, Optional
from django.template import Context, Template
# isort: off
from .django_test_setup import * # NOQA
from .testutils import BaseTestCase, parametrize_context_behavior
# isort: on
import django_components
import django_components.component_registry
from django_components import component, types
class SlottedComponent(component.Component):
template: types.django_html = """
{% load component_tags %}
<custom-template>
<header>{% slot "header" %}Default header{% endslot %}</header>
<main>{% slot "main" %}Default main{% endslot %}</main>
<footer>{% slot "footer" %}Default footer{% endslot %}</footer>
</custom-template>
"""
class SlottedComponentWithContext(SlottedComponent):
def get_context_data(self, variable):
return {"variable": variable}
#######################
# TESTS
#######################
class NestedSlotTests(BaseTestCase):
class NestedComponent(component.Component):
template: types.django_html = """
{% load component_tags %}
{% slot 'outer' %}
<div id="outer">{% slot 'inner' %}Default{% endslot %}</div>
{% endslot %}
"""
@parametrize_context_behavior(["django", "isolated"])
def test_default_slot_contents_render_correctly(self):
component.registry.clear()
component.registry.register("test", self.NestedComponent)
template_str: types.django_html = """
{% load component_tags %}
{% component 'test' %}{% endcomponent %}
"""
template = Template(template_str)
rendered = template.render(Context({}))
self.assertHTMLEqual(rendered, '<div id="outer">Default</div>')
@parametrize_context_behavior(["django", "isolated"])
def test_inner_slot_overriden(self):
component.registry.clear()
component.registry.register("test", self.NestedComponent)
template_str: types.django_html = """
{% load component_tags %}
{% component 'test' %}
{% fill 'inner' %}Override{% endfill %}
{% endcomponent %}
"""
template = Template(template_str)
rendered = template.render(Context({}))
self.assertHTMLEqual(rendered, '<div id="outer">Override</div>')
@parametrize_context_behavior(["django", "isolated"])
def test_outer_slot_overriden(self):
component.registry.clear()
component.registry.register("test", self.NestedComponent)
template_str: types.django_html = """
{% load component_tags %}
{% component 'test' %}{% fill 'outer' %}<p>Override</p>{% endfill %}{% endcomponent %}
"""
template = Template(template_str)
rendered = template.render(Context({}))
self.assertHTMLEqual(rendered, "<p>Override</p>")
@parametrize_context_behavior(["django", "isolated"])
def test_both_overriden_and_inner_removed(self):
component.registry.clear()
component.registry.register("test", self.NestedComponent)
template_str: types.django_html = """
{% load component_tags %}
{% component 'test' %}
{% fill 'outer' %}<p>Override</p>{% endfill %}
{% fill 'inner' %}<p>Will not appear</p>{% endfill %}
{% endcomponent %}
"""
template = Template(template_str)
rendered = template.render(Context({}))
self.assertHTMLEqual(rendered, "<p>Override</p>")
# NOTE: Second arg in tuple is expected name in nested fill. In "django" mode,
# the value should be overridden by the component, while in "isolated" it should
# remain top-level context.
@parametrize_context_behavior([("django", "Joe2"), ("isolated", "Jannete")])
def test_fill_inside_fill_with_same_name(self, context_behavior_data):
class SlottedComponent(component.Component):
template: types.django_html = """
{% load component_tags %}
<custom-template>
<header>{% slot "header" %}Default header{% endslot %}</header>
<main>{% slot "main" %}Default main{% endslot %}</main>
<footer>{% slot "footer" %}Default footer{% endslot %}</footer>
</custom-template>
"""
def get_context_data(self, name: Optional[str] = None) -> Dict[str, Any]:
return {
"name": name,
}
component.registry.clear()
component.registry.register("test", SlottedComponent)
template_str: types.django_html = """
{% load component_tags %}
{% component "test" name='Igor' %}
{% fill "header" %}
{% component "test" name='Joe2' %}
{% fill "header" %}
Name2: {{ name }}
{% endfill %}
{% fill "main" %}
Day2: {{ day }}
{% endfill %}
{% fill "footer" %}
XYZ
{% endfill %}
{% endcomponent %}
{% endfill %}
{% fill "footer" %}
WWW
{% endfill %}
{% endcomponent %}
"""
self.template = Template(template_str)
rendered = self.template.render(Context({"day": "Monday", "name": "Jannete"}))
self.assertHTMLEqual(
rendered,
f"""
<custom-template>
<header>
<custom-template>
<header>Name2: {context_behavior_data}</header>
<main>Day2: Monday</main>
<footer>XYZ</footer>
</custom-template>
</header>
<main>Default main</main>
<footer>WWW</footer>
</custom-template>
""",
)
# NOTE: This test group are kept for backward compatibility, as the same logic
# as provided by {% if %} tags was previously provided by this library.
class ConditionalSlotTests(BaseTestCase):
class ConditionalComponent(component.Component):
template: types.django_html = """
{% load component_tags %}
{% if branch == 'a' %}
<p id="a">{% slot 'a' %}Default A{% endslot %}</p>
{% elif branch == 'b' %}
<p id="b">{% slot 'b' %}Default B{% endslot %}</p>
{% endif %}
"""
def get_context_data(self, branch=None):
return {"branch": branch}
@classmethod
def setUpClass(cls):
super().setUpClass()
component.registry.clear()
component.registry.register("test", cls.ConditionalComponent)
@classmethod
def tearDownClass(cls):
super().tearDownClass()
component.registry.clear()
@parametrize_context_behavior(["django", "isolated"])
def test_no_content_if_branches_are_false(self):
template_str: types.django_html = """
{% load component_tags %}
{% component 'test' %}
{% fill 'a' %}Override A{% endfill %}
{% fill 'b' %}Override B{% endfill %}
{% endcomponent %}
"""
template = Template(template_str)
rendered = template.render(Context({}))
self.assertHTMLEqual(rendered, "")
@parametrize_context_behavior(["django", "isolated"])
def test_default_content_if_no_slots(self):
template_str: types.django_html = """
{% load component_tags %}
{% component 'test' branch='a' %}{% endcomponent %}
{% component 'test' branch='b' %}{% endcomponent %}
"""
template = Template(template_str)
rendered = template.render(Context({}))
self.assertHTMLEqual(rendered, '<p id="a">Default A</p><p id="b">Default B</p>')
@parametrize_context_behavior(["django", "isolated"])
def test_one_slot_overridden(self):
template_str: types.django_html = """
{% load component_tags %}
{% component 'test' branch='a' %}
{% fill 'b' %}Override B{% endfill %}
{% endcomponent %}
{% component 'test' branch='b' %}
{% fill 'b' %}Override B{% endfill %}
{% endcomponent %}
"""
template = Template(template_str)
rendered = template.render(Context({}))
self.assertHTMLEqual(rendered, '<p id="a">Default A</p><p id="b">Override B</p>')
@parametrize_context_behavior(["django", "isolated"])
def test_both_slots_overridden(self):
template_str: types.django_html = """
{% load component_tags %}
{% component 'test' branch='a' %}
{% fill 'a' %}Override A{% endfill %}
{% fill 'b' %}Override B{% endfill %}
{% endcomponent %}
{% component 'test' branch='b' %}
{% fill 'a' %}Override A{% endfill %}
{% fill 'b' %}Override B{% endfill %}
{% endcomponent %}
"""
template = Template(template_str)
rendered = template.render(Context({}))
self.assertHTMLEqual(rendered, '<p id="a">Override A</p><p id="b">Override B</p>')
class SlotIterationTest(BaseTestCase):
"""Tests a behaviour of {% fill .. %} tag which is inside a template {% for .. %} loop."""
class ComponentSimpleSlotInALoop(django_components.component.Component):
template: types.django_html = """
{% load component_tags %}
{% for object in objects %}
{% slot 'slot_inner' %}
{{ object }} default
{% endslot %}
{% endfor %}
"""
def get_context_data(self, objects, *args, **kwargs) -> dict:
return {
"objects": objects,
}
def setUp(self):
django_components.component.registry.clear()
# NOTE: Second arg in tuple is expected result. In isolated mode, loops should NOT leak.
@parametrize_context_behavior(
[
("django", "OBJECT1 OBJECT2"),
("isolated", ""),
]
)
def test_inner_slot_iteration_basic(self, context_behavior_data):
component.registry.register("slot_in_a_loop", self.ComponentSimpleSlotInALoop)
template_str: types.django_html = """
{% load component_tags %}
{% component "slot_in_a_loop" objects=objects %}
{% fill "slot_inner" %}
{{ object }}
{% endfill %}
{% endcomponent %}
"""
template = Template(template_str)
objects = ["OBJECT1", "OBJECT2"]
rendered = template.render(Context({"objects": objects}))
self.assertHTMLEqual(rendered, context_behavior_data)
# NOTE: Second arg in tuple is expected result. In isolated mode, while loops should NOT leak,
# we should still have access to root context (returned from get_context_data)
@parametrize_context_behavior(
[
("django", "OUTER_SCOPE_VARIABLE OBJECT1 OUTER_SCOPE_VARIABLE OBJECT2"),
("isolated", "OUTER_SCOPE_VARIABLE OUTER_SCOPE_VARIABLE"),
]
)
def test_inner_slot_iteration_with_variable_from_outer_scope(self, context_behavior_data):
component.registry.register("slot_in_a_loop", self.ComponentSimpleSlotInALoop)
template_str: types.django_html = """
{% load component_tags %}
{% component "slot_in_a_loop" objects=objects %}
{% fill "slot_inner" %}
{{ outer_scope_variable }}
{{ object }}
{% endfill %}
{% endcomponent %}
"""
template = Template(template_str)
objects = ["OBJECT1", "OBJECT2"]
rendered = template.render(
Context(
{
"objects": objects,
"outer_scope_variable": "OUTER_SCOPE_VARIABLE",
}
)
)
self.assertHTMLEqual(rendered, context_behavior_data)
# NOTE: Second arg in tuple is expected result. In isolated mode, loops should NOT leak.
@parametrize_context_behavior(
[
("django", "ITER1_OBJ1 ITER1_OBJ2 ITER2_OBJ1 ITER2_OBJ2"),
("isolated", ""),
]
)
def test_inner_slot_iteration_nested(self, context_behavior_data):
component.registry.register("slot_in_a_loop", self.ComponentSimpleSlotInALoop)
objects = [
{"inner": ["ITER1_OBJ1", "ITER1_OBJ2"]},
{"inner": ["ITER2_OBJ1", "ITER2_OBJ2"]},
]
template_str: types.django_html = """
{% load component_tags %}
{% component "slot_in_a_loop" objects=objects %}
{% fill "slot_inner" %}
{% component "slot_in_a_loop" objects=object.inner %}
{% fill "slot_inner" %}
{{ object }}
{% endfill %}
{% endcomponent %}
{% endfill %}
{% endcomponent %}
"""
template = Template(template_str)
rendered = template.render(Context({"objects": objects}))
self.assertHTMLEqual(rendered, context_behavior_data)
# NOTE: Second arg in tuple is expected result. In isolated mode, while loops should NOT leak,
# we should still have access to root context (returned from get_context_data)
@parametrize_context_behavior(
[
(
"django",
"""
OUTER_SCOPE_VARIABLE1
OUTER_SCOPE_VARIABLE2
ITER1_OBJ1
OUTER_SCOPE_VARIABLE2
ITER1_OBJ2
OUTER_SCOPE_VARIABLE1
OUTER_SCOPE_VARIABLE2
ITER2_OBJ1
OUTER_SCOPE_VARIABLE2
ITER2_OBJ2
""",
),
("isolated", "OUTER_SCOPE_VARIABLE1 OUTER_SCOPE_VARIABLE1"),
]
)
def test_inner_slot_iteration_nested_with_outer_scope_variable(self, context_behavior_data):
component.registry.register("slot_in_a_loop", self.ComponentSimpleSlotInALoop)
objects = [
{"inner": ["ITER1_OBJ1", "ITER1_OBJ2"]},
{"inner": ["ITER2_OBJ1", "ITER2_OBJ2"]},
]
template_str: types.django_html = """
{% load component_tags %}
{% component "slot_in_a_loop" objects=objects %}
{% fill "slot_inner" %}
{{ outer_scope_variable_1 }}
{% component "slot_in_a_loop" objects=object.inner %}
{% fill "slot_inner" %}
{{ outer_scope_variable_2 }}
{{ object }}
{% endfill %}
{% endcomponent %}
{% endfill %}
{% endcomponent %}
"""
template = Template(template_str)
rendered = template.render(
Context(
{
"objects": objects,
"outer_scope_variable_1": "OUTER_SCOPE_VARIABLE1",
"outer_scope_variable_2": "OUTER_SCOPE_VARIABLE2",
}
)
)
self.assertHTMLEqual(rendered, context_behavior_data)
# NOTE: Second arg in tuple is expected result. In isolated mode, loops should NOT leak.
@parametrize_context_behavior(
[
("django", "ITER1_OBJ1 default ITER1_OBJ2 default ITER2_OBJ1 default ITER2_OBJ2 default"),
("isolated", ""),
]
)
def test_inner_slot_iteration_nested_with_slot_default(self, context_behavior_data):
component.registry.register("slot_in_a_loop", self.ComponentSimpleSlotInALoop)
objects = [
{"inner": ["ITER1_OBJ1", "ITER1_OBJ2"]},
{"inner": ["ITER2_OBJ1", "ITER2_OBJ2"]},
]
template_str: types.django_html = """
{% load component_tags %}
{% component "slot_in_a_loop" objects=objects %}
{% fill "slot_inner" %}
{% component "slot_in_a_loop" objects=object.inner %}
{% fill "slot_inner" default="super_slot_inner" %}
{{ super_slot_inner }}
{% endfill %}
{% endcomponent %}
{% endfill %}
{% endcomponent %}
"""
template = Template(template_str)
rendered = template.render(Context({"objects": objects}))
self.assertHTMLEqual(rendered, context_behavior_data)
# NOTE: Second arg in tuple is expected result. In isolated mode, loops should NOT leak.
@parametrize_context_behavior(
[
(
"django",
"""
OUTER_SCOPE_VARIABLE1
OUTER_SCOPE_VARIABLE2
ITER1_OBJ1 default
OUTER_SCOPE_VARIABLE2
ITER1_OBJ2 default
OUTER_SCOPE_VARIABLE1
OUTER_SCOPE_VARIABLE2
ITER2_OBJ1 default
OUTER_SCOPE_VARIABLE2
ITER2_OBJ2 default
""",
),
# NOTE: In this case the `object.inner` in the inner "slot_in_a_loop"
# should be undefined, so the loop inside the inner `slot_in_a_loop`
# shouldn't run. Hence even the inner `slot_inner` fill should NOT run.
("isolated", "OUTER_SCOPE_VARIABLE1 OUTER_SCOPE_VARIABLE1"),
]
)
def test_inner_slot_iteration_nested_with_slot_default_and_outer_scope_variable(
self,
context_behavior_data,
):
component.registry.register("slot_in_a_loop", self.ComponentSimpleSlotInALoop)
objects = [
{"inner": ["ITER1_OBJ1", "ITER1_OBJ2"]},
{"inner": ["ITER2_OBJ1", "ITER2_OBJ2"]},
]
template_str: types.django_html = """
{% load component_tags %}
{% component "slot_in_a_loop" objects=objects %}
{% fill "slot_inner" %}
{{ outer_scope_variable_1 }}
{% component "slot_in_a_loop" objects=object.inner %}
{% fill "slot_inner" default="super_slot_inner" %}
{{ outer_scope_variable_2 }}
{{ super_slot_inner }}
{% endfill %}
{% endcomponent %}
{% endfill %}
{% endcomponent %}
"""
template = Template(template_str)
rendered = template.render(
Context(
{
"objects": objects,
"outer_scope_variable_1": "OUTER_SCOPE_VARIABLE1",
"outer_scope_variable_2": "OUTER_SCOPE_VARIABLE2",
}
)
)
self.assertHTMLEqual(rendered, context_behavior_data)
@parametrize_context_behavior(["isolated"])
def test_inner_slot_iteration_nested_with_slot_default_and_outer_scope_variable__isolated_2(
self,
):
component.registry.register("slot_in_a_loop", self.ComponentSimpleSlotInALoop)
objects = [
{"inner": ["ITER1_OBJ1", "ITER1_OBJ2"]},
{"inner": ["ITER2_OBJ1", "ITER2_OBJ2"]},
]
# NOTE: In this case we use `objects` in the inner "slot_in_a_loop", which
# is defined in the root context. So the loop inside the inner `slot_in_a_loop`
# should run.
template_str: types.django_html = """
{% load component_tags %}
{% component "slot_in_a_loop" objects=objects %}
{% fill "slot_inner" %}
{{ outer_scope_variable_1|safe }}
{% component "slot_in_a_loop" objects=objects %}
{% fill "slot_inner" default="super_slot_inner" %}
{{ outer_scope_variable_2|safe }}
{{ super_slot_inner }}
{% endfill %}
{% endcomponent %}
{% endfill %}
{% endcomponent %}
"""
template = Template(template_str)
rendered = template.render(
Context(
{
"objects": objects,
"outer_scope_variable_1": "OUTER_SCOPE_VARIABLE1",
"outer_scope_variable_2": "OUTER_SCOPE_VARIABLE2",
}
)
)
self.assertHTMLEqual(
rendered,
"""
OUTER_SCOPE_VARIABLE1
OUTER_SCOPE_VARIABLE2
{'inner': ['ITER1_OBJ1', 'ITER1_OBJ2']} default
OUTER_SCOPE_VARIABLE2
{'inner': ['ITER2_OBJ1', 'ITER2_OBJ2']} default
OUTER_SCOPE_VARIABLE1
OUTER_SCOPE_VARIABLE2
{'inner': ['ITER1_OBJ1', 'ITER1_OBJ2']} default
OUTER_SCOPE_VARIABLE2
{'inner': ['ITER2_OBJ1', 'ITER2_OBJ2']} default
""",
)
class ComponentNestingTests(BaseTestCase):
class CalendarComponent(component.Component):
"""Nested in ComponentWithNestedComponent"""
template: types.django_html = """
{% load component_tags %}
<div class="calendar-component">
<h1>
{% slot "header" %}Today's date is <span>{{ date }}</span>{% endslot %}
</h1>
<main>
{% slot "body" %}
You have no events today.
{% endslot %}
</main>
</div>
"""
class DashboardComponent(component.Component):
template: types.django_html = """
{% load component_tags %}
<div class="dashboard-component">
{% component "calendar" date="2020-06-06" %}
{% fill "header" %} {# fills and slots with same name relate to diff. things. #}
{% slot "header" %}Welcome to your dashboard!{% endslot %}
{% endfill %}
{% fill "body" %}Here are your to-do items for today:{% endfill %}
{% endcomponent %}
<ol>
{% for item in items %}
<li>{{ item }}</li>
{% endfor %}
</ol>
</div>
"""
class ComplexChildComponent(component.Component):
template: types.django_html = """
{% load component_tags %}
<div>
{% slot "content" default %}
No slot!
{% endslot %}
</div>
"""
class ComplexParentComponent(component.Component):
template: types.django_html = """
{% load component_tags %}
ITEMS: {{ items|safe }}
{% for item in items %}
<li>
{% component "complex_child" %}
{{ item.value }}
{% endcomponent %}
</li>
{% endfor %}
"""
def get_context_data(self, items, *args, **kwargs) -> Dict[str, Any]:
return {"items": items}
@classmethod
def setUpClass(cls) -> None:
super().setUpClass()
component.registry.register("dashboard", cls.DashboardComponent)
component.registry.register("calendar", cls.CalendarComponent)
component.registry.register("complex_child", cls.ComplexChildComponent)
component.registry.register("complex_parent", cls.ComplexParentComponent)
@classmethod
def tearDownClass(cls) -> None:
super().tearDownClass()
component.registry.clear()
# NOTE: Second arg in tuple are expected names in nested fills. In "django" mode,
# the value should be overridden by the component, while in "isolated" it should
# remain top-level context.
@parametrize_context_behavior([("django", ("Igor", "Joe2")), ("isolated", ("Jannete", "Jannete"))])
def test_component_inside_slot(self, context_behavior_data):
first_name, second_name = context_behavior_data
class SlottedComponent(component.Component):
template: types.django_html = """
{% load component_tags %}
<custom-template>
<header>{% slot "header" %}Default header{% endslot %}</header>
<main>{% slot "main" %}Default main{% endslot %}</main>
<footer>{% slot "footer" %}Default footer{% endslot %}</footer>
</custom-template>
"""
def get_context_data(self, name: Optional[str] = None) -> Dict[str, Any]:
return {
"name": name,
}
component.registry.register("test", SlottedComponent)
template_str: types.django_html = """
{% load component_tags %}
{% component "test" name='Igor' %}
{% fill "header" %}
Name: {{ name }}
{% endfill %}
{% fill "main" %}
Day: {{ day }}
{% endfill %}
{% fill "footer" %}
{% component "test" name='Joe2' %}
{% fill "header" %}
Name2: {{ name }}
{% endfill %}
{% fill "main" %}
Day2: {{ day }}
{% endfill %}
{% endcomponent %}
{% endfill %}
{% endcomponent %}
"""
self.template = Template(template_str)
rendered = self.template.render(Context({"day": "Monday", "name": "Jannete"}))
self.assertHTMLEqual(
rendered,
f"""
<custom-template>
<header>Name: {first_name}</header>
<main>Day: Monday</main>
<footer>
<custom-template>
<header>Name2: {second_name}</header>
<main>Day2: Monday</main>
<footer>Default footer</footer>
</custom-template>
</footer>
</custom-template>
""",
)
# NOTE: Second arg in tuple is expected list content. In isolated mode, loops should NOT leak.
@parametrize_context_behavior(
[
("django", "<li>1</li> <li>2</li> <li>3</li>"),
("isolated", ""),
]
)
def test_component_nesting_component_without_fill(self, context_behavior_data):
template_str: types.django_html = """
{% load component_tags %}
{% component "dashboard" %}{% endcomponent %}
"""
template = Template(template_str)
rendered = template.render(Context({"items": [1, 2, 3]}))
expected = f"""
<div class="dashboard-component">
<div class="calendar-component">
<h1>
Welcome to your dashboard!
</h1>
<main>
Here are your to-do items for today:
</main>
</div>
<ol>
{context_behavior_data}
</ol>
</div>
"""
self.assertHTMLEqual(rendered, expected)
# NOTE: Second arg in tuple is expected list content. In isolated mode, loops should NOT leak.
@parametrize_context_behavior(
[
("django", "<li>1</li> <li>2</li> <li>3</li>"),
("isolated", ""),
]
)
def test_component_nesting_slot_inside_component_fill(self, context_behavior_data):
template_str: types.django_html = """
{% load component_tags %}
{% component "dashboard" %}
{% fill "header" %}
Whoa!
{% endfill %}
{% endcomponent %}
"""
template = Template(template_str)
rendered = template.render(Context({"items": [1, 2, 3]}))
expected = f"""
<div class="dashboard-component">
<div class="calendar-component">
<h1>
Whoa!
</h1>
<main>
Here are your to-do items for today:
</main>
</div>
<ol>
{context_behavior_data}
</ol>
</div>
"""
self.assertHTMLEqual(rendered, expected)
@parametrize_context_behavior(["django", "isolated"])
def test_component_nesting_deep_slot_inside_component_fill(self):
template_str: types.django_html = """
{% load component_tags %}
{% component "complex_parent" items=items %}{% endcomponent %}
"""
template = Template(template_str)
items = [{"value": 1}, {"value": 2}, {"value": 3}]
rendered = template.render(Context({"items": items}))
expected = """
ITEMS: [{'value': 1}, {'value': 2}, {'value': 3}]
<li>
<div> 1 </div>
</li>
<li>
<div> 2 </div>
</li>
<li>
<div> 3 </div>
</li>
"""
self.assertHTMLEqual(rendered, expected)
# NOTE: Second arg in tuple is expected list content. In isolated mode, loops should NOT leak.
@parametrize_context_behavior(
[
("django", "<li>1</li> <li>2</li>"),
("isolated", ""),
]
)
def test_component_nesting_component_with_slot_default(self, context_behavior_data):
template_str: types.django_html = """
{% load component_tags %}
{% component "dashboard" %}
{% fill "header" default="h" %} Hello! {{ h }} {% endfill %}
{% endcomponent %}
"""
template = Template(template_str)
rendered = template.render(Context({"items": [1, 2]}))
expected = f"""
<div class="dashboard-component">
<div class="calendar-component">
<h1>
Hello! Welcome to your dashboard!
</h1>
<main>
Here are your to-do items for today:
</main>
</div>
<ol>
{context_behavior_data}
</ol>
</div>
"""
self.assertHTMLEqual(rendered, expected)

View file

@ -1,13 +1,16 @@
import contextlib import contextlib
import functools
import sys import sys
from typing import List from typing import Any, List, Tuple, Union
from unittest.mock import Mock from unittest.mock import Mock
from django.template import Context, Node from django.template import Context, Node
from django.template.loader import engines
from django.template.response import TemplateResponse from django.template.response import TemplateResponse
from django.test import SimpleTestCase from django.test import SimpleTestCase, override_settings
from django_components import autodiscover from django_components import autodiscover
from django_components.app_settings import ContextBehavior
from django_components.component_registry import registry from django_components.component_registry import registry
from django_components.middleware import ComponentDependencyMiddleware from django_components.middleware import ComponentDependencyMiddleware
@ -22,6 +25,11 @@ class BaseTestCase(SimpleTestCase):
registry.clear() registry.clear()
return super().setUpClass() return super().setUpClass()
@classmethod
def tearDownClass(cls) -> None:
super().tearDownClass()
registry.clear()
request = Mock() request = Mock()
mock_template = Mock() mock_template = Mock()
@ -75,3 +83,94 @@ def autodiscover_with_cleanup(*args, **kwargs):
# next time one of the tests calls `autodiscover`. # next time one of the tests calls `autodiscover`.
for mod in imported_modules: for mod in imported_modules:
del sys.modules[mod] del sys.modules[mod]
ContextBehStr = Union[ContextBehavior, str]
ContextBehParam = Union[ContextBehStr, Tuple[ContextBehStr, Any]]
def parametrize_context_behavior(cases: List[ContextBehParam]):
"""
Use this decorator to run a test function with django_component's
context_behavior settings set to given values.
You can set only a single mode:
```py
@parametrize_context_behavior(["isolated"])
def test_bla_bla(self):
# do something with app_settings.CONTEXT_BEHAVIOR set
# to "isolated"
...
```
Or you can set a test to run in both modes:
```py
@parametrize_context_behavior(["django", "isolated"])
def test_bla_bla(self):
# Runs this test function twice. Once with
# app_settings.CONTEXT_BEHAVIOR set to "django",
# the other time set to "isolated"
...
```
If you need to pass parametrized data to the tests,
pass a tuple of (mode, data) instead of plain string.
To access the data as a fixture, add `context_behavior_data`
as a function argument:
```py
@parametrize_context_behavior([
("django", "result for django"),
("isolated", "result for isolated"),
])
def test_bla_bla(self, context_behavior_data):
# Runs this test function twice. Once with
# app_settings.CONTEXT_BEHAVIOR set to "django",
# the other time set to "isolated".
#
# `context_behavior_data` will first have a value
# of "result for django", then of "result for isolated"
print(context_behavior_data)
...
```
NOTE: Use only on functions and methods. This decorator was NOT tested on classes
"""
def decorator(test_func):
# NOTE: Ideally this decorator would parametrize the test function
# with `pytest.mark.parametrize`, so all test cases would be treated as separate
# tests and thus isolated. But I wasn't able to get it to work. Hence,
# as a workaround, we run multiple test cases within the same test run.
# Because of this, we need to clear the loader cache, and, on error, we need to
# propagate the info on which test case failed.
@functools.wraps(test_func)
def wrapper(*args, **kwargs):
for case in cases:
# Clear loader cache, see https://stackoverflow.com/a/77531127/9788634
for engine in engines.all():
engine.engine.template_loaders[0].reset()
case_has_data = not isinstance(case, str)
if isinstance(case, str):
context_beh, fixture = case, None
else:
context_beh, fixture = case
with override_settings(COMPONENTS={"context_behavior": context_beh}):
# Call the test function with the fixture as an argument
try:
if case_has_data:
test_func(*args, context_behavior_data=fixture, **kwargs)
else:
test_func(*args, **kwargs)
except Exception as err:
# Give a hint on which iteration the test failed
raise RuntimeError(
f"An error occured in test function '{test_func.__name__}' with"
f" context_behavior='{context_beh}'. See the original error above."
) from err
return wrapper
return decorator