mirror of
https://github.com/django-components/django-components.git
synced 2025-09-27 07:59:08 +00:00
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:
parent
8bbe81d717
commit
95f6554f4c
13 changed files with 3392 additions and 3339 deletions
|
@ -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):
|
||||||
|
|
|
@ -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
|
|
||||||
)
|
|
||||||
|
|
|
@ -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 = """
|
||||||
|
|
388
tests/test_component_media.py
Normal file
388
tests/test_component_media.py
Normal 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)
|
|
@ -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,44 +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):
|
]
|
||||||
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 = passed_in</h1>
|
|
||||||
</div>
|
|
||||||
""",
|
|
||||||
)
|
)
|
||||||
|
def test_parent_args_available_in_slots(self, context_behavior_data):
|
||||||
|
first_val, second_val = context_behavior_data
|
||||||
|
|
||||||
@override_settings(
|
|
||||||
COMPONENTS={
|
|
||||||
"context_behavior": "isolated",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
def test_parent_args_not_available_in_slots__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='passed_in' %}
|
{% component 'parent_with_args' parent_value='passed_in' %}
|
||||||
|
@ -366,15 +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 = </h1>
|
<h1>Uniquely named variable = {second_val}</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)
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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,8 +56,6 @@ class ParserTest(BaseTestCase):
|
||||||
|
|
||||||
|
|
||||||
class ParserComponentTest(BaseTestCase):
|
class ParserComponentTest(BaseTestCase):
|
||||||
def test_special_chars_accessible_via_kwargs(self):
|
|
||||||
@component.register(name="test")
|
|
||||||
class SimpleComponent(component.Component):
|
class SimpleComponent(component.Component):
|
||||||
template: types.django_html = """
|
template: types.django_html = """
|
||||||
{{ date }}
|
{{ date }}
|
||||||
|
@ -72,6 +70,10 @@ class ParserComponentTest(BaseTestCase):
|
||||||
"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 %}
|
||||||
{% component "test" my-date="2015-06-19" @click.native=do_something #some_id=True %}
|
{% component "test" my-date="2015-06-19" @click.native=do_something #some_id=True %}
|
||||||
|
|
File diff suppressed because it is too large
Load diff
417
tests/test_templatetags_component.py
Normal file
417
tests/test_templatetags_component.py
Normal 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)
|
1294
tests/test_templatetags_slot_fill.py
Normal file
1294
tests/test_templatetags_slot_fill.py
Normal file
File diff suppressed because it is too large
Load diff
826
tests/test_templatetags_templating.py
Normal file
826
tests/test_templatetags_templating.py
Normal 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)
|
|
@ -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
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue