mirror of
https://github.com/django-components/django-components.git
synced 2025-07-08 01:45:00 +00:00

* refactor: deprecate Component.input and add raw_args, raw_kwargs, raw_slots * docs: update changelog
743 lines
26 KiB
Python
743 lines
26 KiB
Python
"""
|
|
Tests focusing on the Python part of slots.
|
|
For tests focusing on the `{% slot %}` tag, see `test_templatetags_slot_fill.py`
|
|
"""
|
|
|
|
import re
|
|
|
|
import pytest
|
|
from django.template import Context, Template, TemplateSyntaxError
|
|
from django.utils.safestring import mark_safe
|
|
from django.template.base import NodeList, TextNode
|
|
from pytest_django.asserts import assertHTMLEqual
|
|
|
|
from django_components import Component, register, types
|
|
from django_components.component import ComponentNode
|
|
from django_components.slots import FillNode, Slot, SlotContext, SlotFallback
|
|
|
|
from django_components.testing import djc_test
|
|
from .testutils import PARAMETRIZE_CONTEXT_BEHAVIOR, setup_test_config
|
|
|
|
setup_test_config({"autodiscover": False})
|
|
|
|
|
|
# Test interaction of the `Slot` instances with Component rendering
|
|
@djc_test
|
|
class TestSlot:
|
|
@djc_test(
|
|
parametrize=(
|
|
["components_settings", "is_isolated"],
|
|
[
|
|
[{"context_behavior": "django"}, False],
|
|
[{"context_behavior": "isolated"}, True],
|
|
],
|
|
["django", "isolated"],
|
|
)
|
|
)
|
|
def test_render_slot_as_func(self, components_settings, is_isolated):
|
|
class SimpleComponent(Component):
|
|
template: types.django_html = """
|
|
{% load component_tags %}
|
|
{% slot "first" required data1="abc" data2:hello="world" data2:one=123 %}
|
|
SLOT_DEFAULT
|
|
{% endslot %}
|
|
"""
|
|
|
|
def get_template_data(self, args, kwargs, slots, context):
|
|
return {
|
|
"the_arg": args[0],
|
|
"the_kwarg": kwargs.pop("the_kwarg", None),
|
|
"kwargs": kwargs,
|
|
}
|
|
|
|
def slot_fn(ctx: SlotContext):
|
|
context = ctx.context
|
|
assert isinstance(context, Context)
|
|
# NOTE: Since the slot has access to the Context object, it should behave
|
|
# the same way as it does in templates - when in "isolated" mode, then the
|
|
# slot fill has access only to the "root" context, but not to the data of
|
|
# get_template_data() of SimpleComponent.
|
|
if is_isolated:
|
|
assert context.get("the_arg") is None
|
|
assert context.get("the_kwarg") is None
|
|
assert context.get("kwargs") is None
|
|
assert context.get("abc") is None
|
|
else:
|
|
assert context["the_arg"] == "1"
|
|
assert context["the_kwarg"] == 3
|
|
assert context["kwargs"] == {}
|
|
assert context["abc"] == "def"
|
|
|
|
slot_data_expected = {
|
|
"data1": "abc",
|
|
"data2": {"hello": "world", "one": 123},
|
|
}
|
|
assert slot_data_expected == ctx.data
|
|
|
|
assert isinstance(ctx.fallback, SlotFallback)
|
|
assert "SLOT_DEFAULT" == str(ctx.fallback).strip()
|
|
|
|
return f"FROM_INSIDE_SLOT_FN | {ctx.fallback}"
|
|
|
|
rendered = SimpleComponent.render(
|
|
context={"abc": "def"},
|
|
args=["1"],
|
|
kwargs={"the_kwarg": 3},
|
|
slots={"first": slot_fn},
|
|
)
|
|
assertHTMLEqual(
|
|
rendered,
|
|
"FROM_INSIDE_SLOT_FN | SLOT_DEFAULT",
|
|
)
|
|
|
|
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
|
def test_render_raises_on_missing_slot(self, components_settings):
|
|
class SimpleComponent(Component):
|
|
template: types.django_html = """
|
|
{% load component_tags %}
|
|
{% slot "first" required %}
|
|
{% endslot %}
|
|
"""
|
|
|
|
with pytest.raises(
|
|
TemplateSyntaxError,
|
|
match=re.escape("Slot 'first' is marked as 'required' (i.e. non-optional), yet no fill is provided."),
|
|
):
|
|
SimpleComponent.render()
|
|
|
|
with pytest.raises(
|
|
TemplateSyntaxError,
|
|
match=re.escape("Slot 'first' is marked as 'required' (i.e. non-optional), yet no fill is provided."),
|
|
):
|
|
SimpleComponent.render(
|
|
slots={"first": None},
|
|
)
|
|
|
|
SimpleComponent.render(
|
|
slots={"first": "SLOT_FN"},
|
|
)
|
|
|
|
def test_render_raises_on_slot_instance_in_slot_constructor(self):
|
|
slot: Slot = Slot(lambda ctx: "SLOT_FN")
|
|
|
|
with pytest.raises(
|
|
ValueError,
|
|
match=re.escape("Slot received another Slot instance as `contents`"),
|
|
):
|
|
Slot(slot)
|
|
|
|
def test_render_slot_in_python__minimal(self):
|
|
def slot_fn(ctx: SlotContext):
|
|
assert ctx.context is None
|
|
assert ctx.data == {}
|
|
assert ctx.fallback is None
|
|
|
|
return "FROM_INSIDE_SLOT_FN"
|
|
|
|
slot: Slot = Slot(slot_fn)
|
|
rendered = slot()
|
|
assertHTMLEqual(
|
|
rendered,
|
|
"FROM_INSIDE_SLOT_FN",
|
|
)
|
|
|
|
def test_render_slot_in_python__with_data(self):
|
|
def slot_fn(ctx: SlotContext):
|
|
assert ctx.context is not None
|
|
assert ctx.context["the_arg"] == "1"
|
|
assert ctx.context["the_kwarg"] == 3
|
|
assert ctx.context["kwargs"] == {}
|
|
assert ctx.context["abc"] == "def"
|
|
|
|
slot_data_expected = {
|
|
"data1": "abc",
|
|
"data2": {"hello": "world", "one": 123},
|
|
}
|
|
assert slot_data_expected == ctx.data
|
|
|
|
assert isinstance(ctx.fallback, str)
|
|
assert "SLOT_DEFAULT" == ctx.fallback
|
|
|
|
return f"FROM_INSIDE_SLOT_FN | {ctx.fallback}"
|
|
|
|
slot: Slot = Slot(slot_fn)
|
|
context = Context({"the_arg": "1", "the_kwarg": 3, "kwargs": {}, "abc": "def"})
|
|
|
|
# Test positional arguments
|
|
rendered = slot(
|
|
{"data1": "abc", "data2": {"hello": "world", "one": 123}},
|
|
"SLOT_DEFAULT",
|
|
context,
|
|
)
|
|
assertHTMLEqual(
|
|
rendered,
|
|
"FROM_INSIDE_SLOT_FN | SLOT_DEFAULT",
|
|
)
|
|
|
|
# Test keyword arguments
|
|
rendered2 = slot(
|
|
data={"data1": "abc", "data2": {"hello": "world", "one": 123}},
|
|
fallback="SLOT_DEFAULT",
|
|
context=context,
|
|
)
|
|
assertHTMLEqual(
|
|
rendered2,
|
|
"FROM_INSIDE_SLOT_FN | SLOT_DEFAULT",
|
|
)
|
|
|
|
def test_render_slot_unsafe_content__func(self):
|
|
def slot_fn1(ctx: SlotContext):
|
|
return mark_safe("<script>alert('XSS')</script>")
|
|
|
|
def slot_fn2(ctx: SlotContext):
|
|
return "<script>alert('XSS')</script>"
|
|
|
|
slot1: Slot = Slot(slot_fn1)
|
|
slot2: Slot = Slot(slot_fn2)
|
|
|
|
rendered1 = slot1()
|
|
rendered2 = slot2()
|
|
assert rendered1 == "<script>alert('XSS')</script>"
|
|
assert rendered2 == "<script>alert('XSS')</script>"
|
|
|
|
def test_render_slot_unsafe_content__string(self):
|
|
slot1: Slot = Slot(mark_safe("<script>alert('XSS')</script>"))
|
|
slot2: Slot = Slot("<script>alert('XSS')</script>")
|
|
|
|
rendered1 = slot1()
|
|
rendered2 = slot2()
|
|
assert rendered1 == "<script>alert('XSS')</script>"
|
|
assert rendered2 == "<script>alert('XSS')</script>"
|
|
|
|
# Part of the slot caching feature - test that static content slots reuse the slot function.
|
|
# See https://github.com/django-components/django-components/issues/1164#issuecomment-2854682354
|
|
def test_slots_same_contents__string(self):
|
|
captured_slots = {}
|
|
|
|
class SimpleComponent(Component):
|
|
template: types.django_html = """
|
|
{% load component_tags %}
|
|
{% slot "first" required %}
|
|
{% endslot %}
|
|
"""
|
|
|
|
def get_template_data(self, args, kwargs, slots, context):
|
|
nonlocal captured_slots
|
|
captured_slots = slots
|
|
|
|
SimpleComponent.render(
|
|
slots={"first": "FIRST_SLOT"},
|
|
)
|
|
|
|
first_slot_func = captured_slots["first"]
|
|
assert isinstance(first_slot_func, Slot)
|
|
assert first_slot_func.content_func is not None
|
|
assert first_slot_func.contents == "FIRST_SLOT"
|
|
|
|
captured_slots = {}
|
|
SimpleComponent.render(
|
|
slots={"first": "FIRST_SLOT"},
|
|
)
|
|
|
|
second_slot_func = captured_slots["first"]
|
|
assert isinstance(second_slot_func, Slot)
|
|
assert second_slot_func.content_func is not None
|
|
assert second_slot_func.contents == "FIRST_SLOT"
|
|
|
|
assert first_slot_func.contents == second_slot_func.contents
|
|
|
|
# Part of the slot caching feature - test that consistent functions passed as slots
|
|
# reuse the slot function.
|
|
def test_slots_same_contents__func(self):
|
|
captured_slots = {}
|
|
|
|
class SimpleComponent(Component):
|
|
template: types.django_html = """
|
|
{% load component_tags %}
|
|
{% slot "first" required %}
|
|
{% endslot %}
|
|
"""
|
|
|
|
def get_template_data(self, args, kwargs, slots, context):
|
|
nonlocal captured_slots
|
|
captured_slots = slots
|
|
|
|
slot_func = lambda ctx: "FROM_INSIDE_SLOT" # noqa: E731
|
|
|
|
SimpleComponent.render(
|
|
slots={"first": slot_func},
|
|
)
|
|
|
|
first_slot_func = captured_slots["first"]
|
|
assert isinstance(first_slot_func, Slot)
|
|
assert callable(first_slot_func.content_func)
|
|
assert callable(first_slot_func.contents)
|
|
|
|
captured_slots = {}
|
|
SimpleComponent.render(
|
|
slots={"first": slot_func},
|
|
)
|
|
|
|
second_slot_func = captured_slots["first"]
|
|
assert isinstance(second_slot_func, Slot)
|
|
assert callable(second_slot_func.content_func)
|
|
assert callable(second_slot_func.contents)
|
|
|
|
assert first_slot_func.contents is second_slot_func.contents
|
|
|
|
# Part of the slot caching feature - test that `Slot` instances with identical function
|
|
# passed as slots reuse the slot function.
|
|
def test_slots_same_contents__slot(self):
|
|
captured_slots = {}
|
|
|
|
class SimpleComponent(Component):
|
|
template: types.django_html = """
|
|
{% load component_tags %}
|
|
{% slot "first" required %}
|
|
{% endslot %}
|
|
"""
|
|
|
|
def get_template_data(self, args, kwargs, slots, context):
|
|
nonlocal captured_slots
|
|
captured_slots = slots
|
|
|
|
slot_func = lambda ctx: "FROM_INSIDE_SLOT" # noqa: E731
|
|
|
|
SimpleComponent.render(
|
|
slots={"first": Slot(slot_func)},
|
|
)
|
|
|
|
first_slot_func = captured_slots["first"]
|
|
assert isinstance(first_slot_func, Slot)
|
|
assert callable(first_slot_func.content_func)
|
|
assert callable(first_slot_func.contents)
|
|
|
|
captured_slots = {}
|
|
SimpleComponent.render(
|
|
slots={"first": Slot(slot_func)},
|
|
)
|
|
|
|
second_slot_func = captured_slots["first"]
|
|
assert isinstance(second_slot_func, Slot)
|
|
assert callable(second_slot_func.content_func)
|
|
assert callable(second_slot_func.contents)
|
|
|
|
assert first_slot_func.contents == second_slot_func.contents
|
|
|
|
# Part of the slot caching feature - test that identical slot fill content
|
|
# slots reuse the slot function.
|
|
def test_slots_same_contents__fill_tag_default(self):
|
|
captured_slots = {}
|
|
|
|
@register("test")
|
|
class SimpleComponent(Component):
|
|
template: types.django_html = """
|
|
{% load component_tags %}
|
|
{% slot "first" default %}
|
|
{% endslot %}
|
|
"""
|
|
|
|
def get_template_data(self, args, kwargs, slots, context):
|
|
nonlocal captured_slots
|
|
captured_slots = slots
|
|
|
|
template_str: types.django_html = """
|
|
{% load component_tags %}
|
|
{% component "test" %}
|
|
FROM_INSIDE_DEFAULT_SLOT
|
|
{% endcomponent %}
|
|
"""
|
|
template = Template(template_str)
|
|
|
|
template.render(Context())
|
|
|
|
first_slot_func = captured_slots["default"]
|
|
assert isinstance(first_slot_func, Slot)
|
|
assert callable(first_slot_func.content_func)
|
|
assert first_slot_func.contents == "\n FROM_INSIDE_DEFAULT_SLOT\n "
|
|
|
|
captured_slots = {}
|
|
template.render(Context())
|
|
|
|
second_slot_func = captured_slots["default"]
|
|
assert isinstance(second_slot_func, Slot)
|
|
assert callable(second_slot_func.content_func)
|
|
assert second_slot_func.contents == "\n FROM_INSIDE_DEFAULT_SLOT\n "
|
|
|
|
assert first_slot_func.contents == second_slot_func.contents
|
|
|
|
# Part of the slot caching feature - test that identical slot fill content
|
|
# slots reuse the slot function.
|
|
def test_slots_same_contents__fill_tag_named(self):
|
|
captured_slots = {}
|
|
|
|
@register("test")
|
|
class SimpleComponent(Component):
|
|
template: types.django_html = """
|
|
{% load component_tags %}
|
|
{% slot "first" default %}
|
|
{% endslot %}
|
|
"""
|
|
|
|
def get_template_data(self, args, kwargs, slots, context):
|
|
nonlocal captured_slots
|
|
captured_slots = slots
|
|
|
|
template_str: types.django_html = """
|
|
{% load component_tags %}
|
|
{% component "test" %}
|
|
{% fill "first" %}
|
|
FROM_INSIDE_NAMED_SLOT
|
|
{% endfill %}
|
|
{% endcomponent %}
|
|
"""
|
|
template = Template(template_str)
|
|
|
|
template.render(Context())
|
|
|
|
first_slot_func = captured_slots["first"]
|
|
assert isinstance(first_slot_func, Slot)
|
|
assert callable(first_slot_func.content_func)
|
|
assert first_slot_func.contents == "\n FROM_INSIDE_NAMED_SLOT\n "
|
|
|
|
captured_slots = {}
|
|
template.render(Context())
|
|
|
|
second_slot_func = captured_slots["first"]
|
|
assert isinstance(second_slot_func, Slot)
|
|
assert callable(second_slot_func.content_func)
|
|
assert second_slot_func.contents == "\n FROM_INSIDE_NAMED_SLOT\n "
|
|
|
|
assert first_slot_func.contents == second_slot_func.contents
|
|
|
|
def test_slot_metadata__string(self):
|
|
captured_slots = {}
|
|
|
|
class SimpleComponent(Component):
|
|
template: types.django_html = """
|
|
{% load component_tags %}
|
|
{% slot "first" required %}
|
|
{% endslot %}
|
|
"""
|
|
|
|
def get_template_data(self, args, kwargs, slots, context):
|
|
nonlocal captured_slots
|
|
captured_slots = slots
|
|
|
|
SimpleComponent.render(
|
|
slots={"first": "FIRST_SLOT"},
|
|
)
|
|
|
|
first_slot_func = captured_slots["first"]
|
|
assert isinstance(first_slot_func, Slot)
|
|
assert first_slot_func.component_name == "SimpleComponent"
|
|
assert first_slot_func.slot_name == "first"
|
|
assert first_slot_func.fill_node is None
|
|
assert first_slot_func.extra == {}
|
|
|
|
first_nodelist: NodeList = first_slot_func.nodelist
|
|
assert len(first_nodelist) == 1
|
|
assert isinstance(first_nodelist[0], TextNode)
|
|
assert first_nodelist[0].s == "FIRST_SLOT"
|
|
|
|
# Part of the slot caching feature - test that consistent functions passed as slots
|
|
# reuse the slot function.
|
|
def test_slot_metadata__func(self):
|
|
captured_slots = {}
|
|
|
|
class SimpleComponent(Component):
|
|
template: types.django_html = """
|
|
{% load component_tags %}
|
|
{% slot "first" required %}
|
|
{% endslot %}
|
|
"""
|
|
|
|
def get_template_data(self, args, kwargs, slots, context):
|
|
nonlocal captured_slots
|
|
captured_slots = slots
|
|
|
|
slot_func = lambda ctx: "FROM_INSIDE_SLOT" # noqa: E731
|
|
|
|
SimpleComponent.render(
|
|
slots={"first": slot_func},
|
|
)
|
|
|
|
first_slot_func = captured_slots["first"]
|
|
assert isinstance(first_slot_func, Slot)
|
|
assert first_slot_func.component_name == "SimpleComponent"
|
|
assert first_slot_func.slot_name == "first"
|
|
assert first_slot_func.fill_node is None
|
|
assert first_slot_func.extra == {}
|
|
assert first_slot_func.nodelist is None
|
|
|
|
# Part of the slot caching feature - test that `Slot` instances with identical function
|
|
# passed as slots reuse the slot function.
|
|
def test_slot_metadata__slot(self):
|
|
captured_slots = {}
|
|
|
|
class SimpleComponent(Component):
|
|
template: types.django_html = """
|
|
{% load component_tags %}
|
|
{% slot "first" required %}
|
|
{% endslot %}
|
|
"""
|
|
|
|
def get_template_data(self, args, kwargs, slots, context):
|
|
nonlocal captured_slots
|
|
captured_slots = slots
|
|
|
|
slot_func = lambda ctx: "FROM_INSIDE_SLOT" # noqa: E731
|
|
|
|
SimpleComponent.render(
|
|
slots={"first": Slot(slot_func, extra={"foo": "bar"}, slot_name="whoop")},
|
|
)
|
|
|
|
first_slot_func = captured_slots["first"]
|
|
assert isinstance(first_slot_func, Slot)
|
|
assert first_slot_func.component_name == "SimpleComponent"
|
|
assert first_slot_func.slot_name == "whoop"
|
|
assert first_slot_func.fill_node is None
|
|
assert first_slot_func.extra == {"foo": "bar"}
|
|
assert first_slot_func.nodelist is None
|
|
|
|
# Part of the slot caching feature - test that identical slot fill content
|
|
# slots reuse the slot function.
|
|
def test_slot_metadata__fill_tag_default(self):
|
|
captured_slots = {}
|
|
|
|
@register("test")
|
|
class SimpleComponent(Component):
|
|
template: types.django_html = """
|
|
{% load component_tags %}
|
|
{% slot "first" default %}
|
|
{% endslot %}
|
|
"""
|
|
|
|
def get_template_data(self, args, kwargs, slots, context):
|
|
nonlocal captured_slots
|
|
captured_slots = slots
|
|
|
|
template_str: types.django_html = """
|
|
{% load component_tags %}
|
|
{% component "test" %}
|
|
FROM_INSIDE_DEFAULT_SLOT
|
|
{% endcomponent %}
|
|
"""
|
|
template = Template(template_str)
|
|
|
|
template.render(Context())
|
|
|
|
first_slot_func = captured_slots["default"]
|
|
assert isinstance(first_slot_func, Slot)
|
|
assert first_slot_func.component_name == "test"
|
|
assert first_slot_func.slot_name == "default"
|
|
assert isinstance(first_slot_func.fill_node, ComponentNode)
|
|
assert first_slot_func.extra == {}
|
|
|
|
first_nodelist: NodeList = first_slot_func.nodelist
|
|
assert len(first_nodelist) == 1
|
|
assert isinstance(first_nodelist[0], TextNode)
|
|
assert first_nodelist[0].s == "\n FROM_INSIDE_DEFAULT_SLOT\n "
|
|
|
|
# Part of the slot caching feature - test that identical slot fill content
|
|
# slots reuse the slot function.
|
|
def test_slot_metadata__fill_tag_named(self):
|
|
captured_slots = {}
|
|
|
|
@register("test")
|
|
class SimpleComponent(Component):
|
|
template: types.django_html = """
|
|
{% load component_tags %}
|
|
{% slot "first" default %}
|
|
{% endslot %}
|
|
"""
|
|
|
|
def get_template_data(self, args, kwargs, slots, context):
|
|
nonlocal captured_slots
|
|
captured_slots = slots
|
|
|
|
template_str: types.django_html = """
|
|
{% load component_tags %}
|
|
{% component "test" %}
|
|
{% fill "first" %}
|
|
FROM_INSIDE_NAMED_SLOT
|
|
{% endfill %}
|
|
{% endcomponent %}
|
|
"""
|
|
template = Template(template_str)
|
|
|
|
template.render(Context())
|
|
|
|
first_slot_func = captured_slots["first"]
|
|
assert isinstance(first_slot_func, Slot)
|
|
assert first_slot_func.component_name == "test"
|
|
assert first_slot_func.slot_name == "first"
|
|
assert isinstance(first_slot_func.fill_node, FillNode)
|
|
assert first_slot_func.extra == {}
|
|
|
|
first_nodelist: NodeList = first_slot_func.nodelist
|
|
assert len(first_nodelist) == 1
|
|
assert isinstance(first_nodelist[0], TextNode)
|
|
assert first_nodelist[0].s == "\n FROM_INSIDE_NAMED_SLOT\n "
|
|
|
|
# Part of the slot caching feature - test that identical slot fill content
|
|
# slots reuse the slot function.
|
|
def test_slot_metadata__fill_tag_body(self):
|
|
captured_slots = {}
|
|
|
|
@register("test")
|
|
class SimpleComponent(Component):
|
|
template: types.django_html = """
|
|
{% load component_tags %}
|
|
{% slot "first" default %}
|
|
{% endslot %}
|
|
"""
|
|
|
|
def get_template_data(self, args, kwargs, slots, context):
|
|
nonlocal captured_slots
|
|
captured_slots = slots
|
|
|
|
template_str: types.django_html = """
|
|
{% load component_tags %}
|
|
{% component "test" %}
|
|
{% fill "first" body=my_slot / %}
|
|
{% endcomponent %}
|
|
"""
|
|
template = Template(template_str)
|
|
|
|
template.render(
|
|
Context(
|
|
{
|
|
"my_slot": Slot(lambda ctx: "FROM_INSIDE_NAMED_SLOT", extra={"foo": "bar"}, slot_name="whoop"),
|
|
}
|
|
)
|
|
)
|
|
|
|
first_slot_func = captured_slots["first"]
|
|
assert isinstance(first_slot_func, Slot)
|
|
assert first_slot_func.component_name == "test"
|
|
assert first_slot_func.slot_name == "whoop"
|
|
assert isinstance(first_slot_func.fill_node, FillNode)
|
|
assert first_slot_func.extra == {"foo": "bar"}
|
|
assert first_slot_func.nodelist is None
|
|
|
|
def test_pass_body_to_fill__slot(self):
|
|
@register("test")
|
|
class SimpleComponent(Component):
|
|
template: types.django_html = """
|
|
{% load component_tags %}
|
|
{% slot "first" default %}
|
|
{% endslot %}
|
|
"""
|
|
|
|
template_str: types.django_html = """
|
|
{% load component_tags %}
|
|
{% component "test" %}
|
|
{% fill "first" body=my_slot / %}
|
|
{% endcomponent %}
|
|
"""
|
|
template = Template(template_str)
|
|
|
|
my_slot: Slot = Slot(lambda ctx: "FROM_INSIDE_NAMED_SLOT")
|
|
rendered: str = template.render(Context({"my_slot": my_slot}))
|
|
|
|
assert rendered.strip() == "FROM_INSIDE_NAMED_SLOT"
|
|
|
|
def test_pass_body_to_fill__string(self):
|
|
@register("test")
|
|
class SimpleComponent(Component):
|
|
template: types.django_html = """
|
|
{% load component_tags %}
|
|
{% slot "first" default %}
|
|
{% endslot %}
|
|
"""
|
|
|
|
template_str: types.django_html = """
|
|
{% load component_tags %}
|
|
{% component "test" %}
|
|
{% fill "first" body=my_slot / %}
|
|
{% endcomponent %}
|
|
"""
|
|
template = Template(template_str)
|
|
|
|
rendered: str = template.render(Context({"my_slot": "FROM_INSIDE_NAMED_SLOT"}))
|
|
|
|
assert rendered.strip() == "FROM_INSIDE_NAMED_SLOT"
|
|
|
|
def test_pass_body_to_fill_raises_on_body(self):
|
|
@register("test")
|
|
class SimpleComponent(Component):
|
|
template: types.django_html = """
|
|
{% load component_tags %}
|
|
{% slot "first" default %}
|
|
{% endslot %}
|
|
"""
|
|
|
|
template_str: types.django_html = """
|
|
{% load component_tags %}
|
|
{% component "test" %}
|
|
{% fill "first" body=my_slot %}
|
|
FROM_INSIDE_NAMED_SLOT
|
|
{% endfill %}
|
|
{% endcomponent %}
|
|
"""
|
|
template = Template(template_str)
|
|
|
|
my_slot: Slot = Slot(lambda ctx: "FROM_INSIDE_NAMED_SLOT")
|
|
|
|
with pytest.raises(
|
|
TemplateSyntaxError,
|
|
match=re.escape("Fill 'first' received content both through 'body' kwarg and '{% fill %}' body."),
|
|
):
|
|
template.render(Context({"my_slot": my_slot}))
|
|
|
|
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
|
def test_slot_call_outside_render_context(self, components_settings):
|
|
from django_components import register, Component
|
|
|
|
seen_slots = []
|
|
|
|
@register("MyTopLevelComponent")
|
|
class MyTopLevelComponent(Component):
|
|
template = """
|
|
{% for thing in words %}
|
|
{% component "MyComponentBeingLooped" / %}
|
|
{% endfor %}
|
|
"""
|
|
|
|
def get_template_data(self, args, kwargs, slots, context):
|
|
return {
|
|
"words": ["apple", "car", "russia"],
|
|
}
|
|
|
|
@register("MyComponentBeingLooped")
|
|
class MyComponentBeingLooped(Component):
|
|
template = """
|
|
{% component "MyComponentWithASlot" %}
|
|
{% fill "my_slot" %}
|
|
{% component "MyInnerComponent" / %}
|
|
{% endfill %}
|
|
{% endcomponent %}
|
|
"""
|
|
|
|
@register("MyInnerComponent")
|
|
class MyInnerComponent(Component):
|
|
template = "Hello!"
|
|
|
|
@register("MyComponentWithASlot")
|
|
class MyComponentWithASlot(Component):
|
|
template = "CAPTURER"
|
|
|
|
def get_template_data(self, args, kwargs, slots, context):
|
|
seen_slots.append(self.slots["my_slot"])
|
|
|
|
MyTopLevelComponent.render()
|
|
|
|
assert len(seen_slots) == 3
|
|
|
|
results = [slot().strip() for slot in seen_slots]
|
|
assert results == [
|
|
"<!-- _RENDERED MyInnerComponent_fb676b,ca1bc49,, -->Hello!",
|
|
"<!-- _RENDERED MyInnerComponent_fb676b,ca1bc4a,, -->Hello!",
|
|
"<!-- _RENDERED MyInnerComponent_fb676b,ca1bc4b,, -->Hello!",
|
|
]
|