django-components/tests/test_component.py
Juro Oravec 8677ee7941
refactor: deprecate template caching, get_template_name, get_template, assoc template with Comp cls (#1222)
* refactor: deprecate template caching, get_template_name, get_template, assoc template with Comp cls

* refactor: change implementation

* refactor: handle cached template loader

* refactor: fix tests

* refactor: fix test on windows

* refactor: try to  fix type errors

* refactor: Re-cast `context` to fix type errors

* refactor: fix linter error

* refactor: fix typing

* refactor: more linter fixes

* refactor: more linter errors

* refactor: revert extra node metadata
2025-06-01 19:20:22 +02:00

1604 lines
54 KiB
Python

"""
Tests focusing on the Component class.
For tests focusing on the `component` tag, see `test_templatetags_component.py`
"""
import re
from typing import Any, NamedTuple
import pytest
from django.conf import settings
from django.http import HttpRequest, HttpResponse
from django.template import Context, RequestContext, Template
from django.template.base import TextNode
from django.test import Client
from django.urls import path
from pytest_django.asserts import assertHTMLEqual, assertInHTML
from django_components import (
Component,
ComponentView,
Slot,
SlotInput,
all_components,
get_component_by_class_id,
register,
types,
)
from django_components.template import _get_component_template
from django_components.urls import urlpatterns as dc_urlpatterns
from django_components.testing import djc_test
from .testutils import PARAMETRIZE_CONTEXT_BEHAVIOR, setup_test_config
setup_test_config({"autodiscover": False})
# Client for testing endpoints via requests
class CustomClient(Client):
def __init__(self, urlpatterns=None, *args, **kwargs):
import types
if urlpatterns:
urls_module = types.ModuleType("urls")
urls_module.urlpatterns = urlpatterns + dc_urlpatterns # type: ignore
settings.ROOT_URLCONF = urls_module
else:
settings.ROOT_URLCONF = __name__
settings.SECRET_KEY = "secret" # noqa
super().__init__(*args, **kwargs)
@djc_test
class TestComponentLegacyApi:
# TODO_REMOVE_IN_V1 - Superseded by `self.get_template` in v1
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_get_template_string(self, components_settings):
class SimpleComponent(Component):
def get_template_string(self, context):
content: types.django_html = """
Variable: <strong>{{ variable }}</strong>
"""
return content
def get_template_data(self, args, kwargs, slots, context):
return {
"variable": kwargs.get("variable", None),
}
class Media:
css = "style.css"
js = "script.js"
rendered = SimpleComponent.render(kwargs={"variable": "test"})
assertHTMLEqual(
rendered,
"""
Variable: <strong data-djc-id-ca1bc3e>test</strong>
""",
)
# TODO_REMOVE_IN_V2 - `get_context_data()` was superseded by `self.get_template_data`
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_get_context_data(self, components_settings):
class SimpleComponent(Component):
template = """
Variable: <strong>{{ variable }}</strong>
"""
def get_context_data(self, variable=None):
return {
"variable": variable,
}
class Media:
css = "style.css"
js = "script.js"
rendered = SimpleComponent.render(kwargs={"variable": "test"})
assertHTMLEqual(
rendered,
"""
Variable: <strong data-djc-id-ca1bc3e>test</strong>
""",
)
# TODO_REMOVE_IN_V1 - Registry and registered name should be passed to `Component.render()`,
# not to the constructor.
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_component_instantiation(self, components_settings):
class SimpleComponent(Component):
template = """
<div>
Name: {{ name }}
</div>
"""
def get_template_data(self, args, kwargs, slots, context):
return {
"name": self.name,
}
# Old syntax
rendered = SimpleComponent("simple").render()
assertHTMLEqual(
rendered,
"""
<div data-djc-id-ca1bc3f>
Name: simple
</div>
""",
)
# New syntax
rendered = SimpleComponent.render(registered_name="simple")
assertHTMLEqual(
rendered,
"""
<div data-djc-id-ca1bc40>
Name: simple
</div>
""",
)
# Sanity check
rendered = SimpleComponent.render()
assertHTMLEqual(
rendered,
"""
<div data-djc-id-ca1bc41>
Name: SimpleComponent
</div>
""",
)
# TODO_v1 - Remove
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_get_template_name(self, components_settings):
class SvgComponent(Component):
def get_template_data(self, args, kwargs, slots, context):
return {
"name": kwargs.pop("name", None),
"css_class": kwargs.pop("css_class", None),
"title": kwargs.pop("title", None),
**kwargs,
}
def get_template_name(self, context):
return f"dynamic_{context['name']}.svg"
assertHTMLEqual(
SvgComponent.render(kwargs={"name": "svg1"}),
"""
<svg data-djc-id-ca1bc3e>Dynamic1</svg>
""",
)
assertHTMLEqual(
SvgComponent.render(kwargs={"name": "svg2"}),
"""
<svg data-djc-id-ca1bc3f>Dynamic2</svg>
""",
)
# TODO_v1 - Remove
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_get_template__string(self, components_settings):
class SimpleComponent(Component):
def get_template(self, context):
content: types.django_html = """
Variable: <strong>{{ variable }}</strong>
"""
return content
def get_template_data(self, args, kwargs, slots, context):
return {
"variable": kwargs.get("variable", None),
}
class Media:
css = "style.css"
js = "script.js"
rendered = SimpleComponent.render(kwargs={"variable": "test"})
assertHTMLEqual(
rendered,
"""
Variable: <strong data-djc-id-ca1bc3e>test</strong>
""",
)
# TODO_v1 - Remove
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_get_template__template(self, components_settings):
class TestComponent(Component):
def get_template_data(self, args, kwargs, slots, context):
return {
"variable": kwargs.pop("variable", None),
}
def get_template(self, context):
template_str = "Variable: <strong>{{ variable }}</strong>"
return Template(template_str)
rendered = TestComponent.render(kwargs={"variable": "test"})
assertHTMLEqual(
rendered,
"""
Variable: <strong data-djc-id-ca1bc3e>test</strong>
""",
)
# TODO_v1 - Remove
def test_get_template_is_cached(self):
class SimpleComponent(Component):
def get_template(self, context):
content: types.django_html = """
Variable: <strong>{{ variable }}</strong>
"""
return content
def get_template_data(self, args, kwargs, slots, context):
return {
"variable": kwargs.get("variable", None),
}
comp = SimpleComponent()
template_1 = _get_component_template(comp)
template_1._test_id = "123" # type: ignore[union-attr]
template_2 = _get_component_template(comp)
assert template_2._test_id == "123" # type: ignore[union-attr]
@djc_test
class TestComponent:
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_empty_component(self, components_settings):
class EmptyComponent(Component):
pass
EmptyComponent.render(args=["123"])
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_template_string_static_inlined(self, components_settings):
class SimpleComponent(Component):
template: types.django_html = """
Variable: <strong>{{ variable }}</strong>
"""
def get_template_data(self, args, kwargs, slots, context):
return {
"variable": kwargs.get("variable", None),
}
class Media:
css = "style.css"
js = "script.js"
rendered = SimpleComponent.render(kwargs={"variable": "test"})
assertHTMLEqual(
rendered,
"""
Variable: <strong data-djc-id-ca1bc3e>test</strong>
""",
)
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_template_file_static(self, components_settings):
class SimpleComponent(Component):
template_file = "simple_template.html"
def get_template_data(self, args, kwargs, slots, context):
return {
"variable": kwargs.get("variable", None),
}
class Media:
css = "style.css"
js = "script.js"
rendered = SimpleComponent.render(kwargs={"variable": "test"})
assertHTMLEqual(
rendered,
"""
Variable: <strong data-djc-id-ca1bc3e>test</strong>
""",
)
# Test that even with cached template loaders, each Component has its own `Template`
# even when multiple components point to the same template file.
@djc_test(
parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR,
django_settings={
"TEMPLATES": [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [
"tests/templates/",
"tests/components/",
],
"OPTIONS": {
"builtins": [
"django_components.templatetags.component_tags",
],
'loaders': [
('django.template.loaders.cached.Loader', [
# Default Django loader
'django.template.loaders.filesystem.Loader',
# Including this is the same as APP_DIRS=True
'django.template.loaders.app_directories.Loader',
# Components loader
'django_components.template_loader.Loader',
]),
],
},
}
],
},
)
def test_template_file_static__cached(self, components_settings):
class SimpleComponent1(Component):
template_file = "simple_template.html"
def get_template_data(self, args, kwargs, slots, context):
return {
"variable": kwargs.get("variable", None),
}
class SimpleComponent2(Component):
template_file = "simple_template.html"
def get_template_data(self, args, kwargs, slots, context):
return {
"variable": kwargs.get("variable", None),
}
SimpleComponent1.template # Triggers template loading
SimpleComponent2.template # Triggers template loading
# Both components have their own Template instance, but they point to the same template file.
assert isinstance(SimpleComponent1._template, Template)
assert isinstance(SimpleComponent2._template, Template)
assert SimpleComponent1._template is not SimpleComponent2._template
assert SimpleComponent1._template.source == SimpleComponent2._template.source
# The Template instances have different origins, but they point to the same template file.
assert SimpleComponent1._template.origin is not SimpleComponent2._template.origin
assert SimpleComponent1._template.origin.template_name == SimpleComponent2._template.origin.template_name
assert SimpleComponent1._template.origin.name == SimpleComponent2._template.origin.name
assert SimpleComponent1._template.origin.loader == SimpleComponent2._template.origin.loader
# The origins point to their respective Component classes.
assert SimpleComponent1._template.origin.component_cls == SimpleComponent1
assert SimpleComponent2._template.origin.component_cls == SimpleComponent2
rendered = SimpleComponent1.render(kwargs={"variable": "test"})
assertHTMLEqual(
rendered,
"""
Variable: <strong data-djc-id-ca1bc3e>test</strong>
""",
)
rendered = SimpleComponent2.render(kwargs={"variable": "test"})
assertHTMLEqual(
rendered,
"""
Variable: <strong data-djc-id-ca1bc3f>test</strong>
""",
)
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_template_file_static__compat(self, components_settings):
class SimpleComponent(Component):
template_name = "simple_template.html"
def get_template_data(self, args, kwargs, slots, context):
return {
"variable": kwargs.get("variable", None),
}
class Media:
css = "style.css"
js = "script.js"
# Access fields on Component class
assert SimpleComponent.template_name == "simple_template.html"
assert SimpleComponent.template_file == "simple_template.html"
SimpleComponent.template_name = "other_template.html"
assert SimpleComponent.template_name == "other_template.html"
assert SimpleComponent.template_file == "other_template.html"
SimpleComponent.template_name = "simple_template.html"
rendered = SimpleComponent.render(kwargs={"variable": "test"})
assertHTMLEqual(
rendered,
"""
Variable: <strong data-djc-id-ca1bc3e>test</strong>
""",
)
# Access fields on Component instance
comp = SimpleComponent()
assert comp.template_name == "simple_template.html"
assert comp.template_file == "simple_template.html"
# NOTE: Setting `template_file` on INSTANCE is not supported, as users should work
# with classes and not instances. This is tested for completeness.
comp.template_name = "other_template_2.html" # type: ignore[misc]
assert comp.template_name == "other_template_2.html"
assert comp.template_file == "other_template_2.html"
assert SimpleComponent.template_name == "other_template_2.html"
assert SimpleComponent.template_file == "other_template_2.html"
SimpleComponent.template_name = "simple_template.html"
rendered = comp.render(kwargs={"variable": "test"})
assertHTMLEqual(
rendered,
"""
Variable: <strong data-djc-id-ca1bc40>test</strong>
""",
)
def test_get_component_by_id(self):
class SimpleComponent(Component):
pass
assert get_component_by_class_id(SimpleComponent.class_id) == SimpleComponent
def test_get_component_by_id_raises_on_missing_component(self):
with pytest.raises(KeyError):
get_component_by_class_id("nonexistent")
def test_get_context_data_returns_none(self):
class SimpleComponent(Component):
template = "Hello"
def get_template_data(self, args, kwargs, slots, context):
return None
assert SimpleComponent.render() == "Hello"
@djc_test
class TestComponentRenderAPI:
def test_component_render_id(self):
class SimpleComponent(Component):
template = "render_id: {{ render_id }}"
def get_template_data(self, args, kwargs, slots, context):
return {"render_id": self.id}
rendered = SimpleComponent.render()
assert rendered == "render_id: ca1bc3e"
def test_input(self):
class TestComponent(Component):
template: types.django_html = """
{% load component_tags %}
Variable: <strong>{{ variable }}</strong>
{% slot 'my_slot' / %}
"""
def get_template_data(self, args, kwargs, slots, context):
assert self.input.args == [123, "str"]
assert self.input.kwargs == {"variable": "test", "another": 1}
assert isinstance(self.input.context, Context)
assert list(self.input.slots.keys()) == ["my_slot"]
my_slot = self.input.slots["my_slot"]
assert my_slot() == "MY_SLOT"
return {
"variable": kwargs["variable"],
}
def on_render_before(self, context, template):
assert self.input.args == [123, "str"]
assert self.input.kwargs == {"variable": "test", "another": 1}
assert isinstance(self.input.context, Context)
assert list(self.input.slots.keys()) == ["my_slot"]
my_slot = self.input.slots["my_slot"]
assert my_slot() == "MY_SLOT"
rendered = TestComponent.render(
kwargs={"variable": "test", "another": 1},
args=(123, "str"),
slots={"my_slot": "MY_SLOT"},
)
assertHTMLEqual(
rendered,
"""
Variable: <strong data-djc-id-ca1bc3e>test</strong> MY_SLOT
""",
)
def test_args_kwargs_slots__simple(self):
called = False
class TestComponent(Component):
template = ""
def get_template_data(self, args, kwargs, slots, context):
nonlocal called
called = True
assert self.args == [123, "str"]
assert self.kwargs == {"variable": "test", "another": 1}
assert list(self.slots.keys()) == ["my_slot"]
my_slot = self.slots["my_slot"]
assert my_slot() == "MY_SLOT"
TestComponent.render(
kwargs={"variable": "test", "another": 1},
args=(123, "str"),
slots={"my_slot": "MY_SLOT"},
)
assert called
def test_args_kwargs_slots__typed(self):
called = False
class TestComponent(Component):
template = ""
class Args(NamedTuple):
variable: int
another: str
class Kwargs(NamedTuple):
variable: str
another: int
class Slots(NamedTuple):
my_slot: SlotInput
def get_template_data(self, args, kwargs, slots, context):
nonlocal called
called = True
assert self.args == TestComponent.Args(123, "str")
assert self.kwargs == TestComponent.Kwargs(variable="test", another=1)
assert isinstance(self.slots, TestComponent.Slots)
assert isinstance(self.slots.my_slot, Slot)
assert self.slots.my_slot() == "MY_SLOT"
# Check that the instances are reused across multiple uses
assert self.args is self.args
assert self.kwargs is self.kwargs
assert self.slots is self.slots
TestComponent.render(
kwargs={"variable": "test", "another": 1},
args=(123, "str"),
slots={"my_slot": "MY_SLOT"},
)
assert called
def test_args_kwargs_slots__available_outside_render(self):
comp: Any = None
class TestComponent(Component):
template = ""
def get_template_data(self, args, kwargs, slots, context):
nonlocal comp
comp = self
assert comp is None
TestComponent.render()
assert comp.args == [] # type: ignore[attr-defined]
assert comp.kwargs == {} # type: ignore[attr-defined]
assert comp.slots == {} # type: ignore[attr-defined]
assert comp.context == Context() # type: ignore[attr-defined]
@djc_test
class TestComponentTemplateVars:
def test_args_kwargs_slots__simple_untyped(self):
class TestComponent(Component):
template: types.django_html = """
{% load component_tags %}
<div class="test-component">
{# Test whole objects #}
args: {{ component_vars.args|safe }}
kwargs: {{ component_vars.kwargs|safe }}
slots: {{ component_vars.slots|safe }}
{# Test individual values #}
arg: {{ component_vars.args.0|safe }}
kwarg: {{ component_vars.kwargs.variable|safe }}
slot: {{ component_vars.slots.my_slot|safe }}
</div>
"""
html = TestComponent.render(
args=[123, "str"],
kwargs={"variable": "test", "another": 1},
slots={"my_slot": "MY_SLOT"},
)
assertHTMLEqual(
html,
"""
<div class="test-component" data-djc-id-ca1bc3e="">
args: [123, 'str']
kwargs: {'variable': 'test', 'another': 1}
slots: {'my_slot': <Slot component_name='TestComponent' slot_name='my_slot'>}
arg: 123
kwarg: test
slot: <Slot component_name='TestComponent' slot_name='my_slot'>
</div>
""",
)
def test_args_kwargs_slots__simple_typed(self):
class TestComponent(Component):
class Args(NamedTuple):
variable: int
another: str
class Kwargs(NamedTuple):
variable: str
another: int
class Slots(NamedTuple):
my_slot: SlotInput
template: types.django_html = """
{% load component_tags %}
<div class="test-component">
{# Test whole objects #}
args: {{ component_vars.args|safe }}
kwargs: {{ component_vars.kwargs|safe }}
slots: {{ component_vars.slots|safe }}
{# Test individual values #}
arg: {{ component_vars.args.variable|safe }}
kwarg: {{ component_vars.kwargs.variable|safe }}
slot: {{ component_vars.slots.my_slot|safe }}
</div>
"""
html = TestComponent.render(
args=[123, "str"],
kwargs={"variable": "test", "another": 1},
slots={"my_slot": "MY_SLOT"},
)
assertHTMLEqual(
html,
"""
<div class="test-component" data-djc-id-ca1bc3e="">
args: Args(variable=123, another='str')
kwargs: Kwargs(variable='test', another=1)
slots: Slots(my_slot=<Slot component_name='TestComponent' slot_name='my_slot'>)
arg: 123
kwarg: test
slot: <Slot component_name='TestComponent' slot_name='my_slot'>
</div>
""",
)
def test_args_kwargs_slots__nested_untyped(self):
@register("wrapper")
class Wrapper(Component):
template: types.django_html = """
{% load component_tags %}
<div class="wrapper">
{% slot "content" default %}
<div class="test">DEFAULT</div>
{% endslot %}
</div>
"""
class TestComponent(Component):
template: types.django_html = """
{% load component_tags %}
<div class="test-component">
{% component "wrapper" %}
{# Test whole objects #}
args: {{ component_vars.args|safe }}
kwargs: {{ component_vars.kwargs|safe }}
slots: {{ component_vars.slots|safe }}
{# Test individual values #}
arg: {{ component_vars.args.0|safe }}
kwarg: {{ component_vars.kwargs.variable|safe }}
slot: {{ component_vars.slots.my_slot|safe }}
{% endcomponent %}
</div>
"""
html = TestComponent.render(
args=[123, "str"],
kwargs={"variable": "test", "another": 1},
slots={"my_slot": "MY_SLOT"},
)
assertHTMLEqual(
html,
"""
<div class="test-component" data-djc-id-ca1bc3e="">
<div class="wrapper" data-djc-id-ca1bc40="">
args: [123, 'str']
kwargs: {'variable': 'test', 'another': 1}
slots: {'my_slot': <Slot component_name='TestComponent' slot_name='my_slot'>}
arg: 123
kwarg: test
slot: <Slot component_name='TestComponent' slot_name='my_slot'>
</div>
</div>
""",
)
def test_args_kwargs_slots__nested_typed(self):
@register("wrapper")
class Wrapper(Component):
template: types.django_html = """
{% load component_tags %}
<div class="wrapper">
{% slot "content" default %}
<div class="test">DEFAULT</div>
{% endslot %}
</div>
"""
class TestComponent(Component):
class Args(NamedTuple):
variable: int
another: str
class Kwargs(NamedTuple):
variable: str
another: int
class Slots(NamedTuple):
my_slot: SlotInput
template: types.django_html = """
{% load component_tags %}
<div class="test-component">
{% component "wrapper" %}
{# Test whole objects #}
args: {{ component_vars.args|safe }}
kwargs: {{ component_vars.kwargs|safe }}
slots: {{ component_vars.slots|safe }}
{# Test individual values #}
arg: {{ component_vars.args.variable|safe }}
kwarg: {{ component_vars.kwargs.variable|safe }}
slot: {{ component_vars.slots.my_slot|safe }}
{% endcomponent %}
</div>
"""
html = TestComponent.render(
args=[123, "str"],
kwargs={"variable": "test", "another": 1},
slots={"my_slot": "MY_SLOT"},
)
assertHTMLEqual(
html,
"""
<div class="test-component" data-djc-id-ca1bc3e="">
<div class="wrapper" data-djc-id-ca1bc40="">
args: Args(variable=123, another='str')
kwargs: Kwargs(variable='test', another=1)
slots: Slots(my_slot=<Slot component_name='TestComponent' slot_name='my_slot'>)
arg: 123
kwarg: test
slot: <Slot component_name='TestComponent' slot_name='my_slot'>
</div>
</div>
""",
)
def test_args_kwargs_slots__nested_conditional_slots(self):
@register("wrapper")
class Wrapper(Component):
template: types.django_html = """
{% load component_tags %}
<div class="wrapper">
{% slot "content" default %}
<div class="test">DEFAULT</div>
{% endslot %}
</div>
"""
class TestComponent(Component):
template: types.django_html = """
{% load component_tags %}
<div class="test-component">
{% component "wrapper" %}
{% if component_vars.slots.subtitle %}
<div class="subtitle">
{% slot "subtitle" %}
Optional subtitle
{% endslot %}
</div>
{% endif %}
{% endcomponent %}
</div>
"""
html = TestComponent.render(
slots={"subtitle": "SUBTITLE_FILLED"},
)
assertHTMLEqual(
html,
"""
<div class="test-component" data-djc-id-ca1bc3e="">
<div class="wrapper" data-djc-id-ca1bc41="">
<div class="subtitle">SUBTITLE_FILLED</div>
</div>
</div>
""",
)
@djc_test
class TestComponentRender:
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_render_minimal(self, components_settings):
class SimpleComponent(Component):
template: types.django_html = """
{% load component_tags %}
the_arg2: {{ the_arg2 }}
args: {{ args|safe }}
the_kwarg: {{ the_kwarg }}
kwargs: {{ kwargs|safe }}
---
from_context: {{ from_context }}
---
slot_second: {% slot "second" default %}
SLOT_SECOND_DEFAULT
{% endslot %}
"""
def get_template_data(self, args, kwargs, slots, context):
return {
"the_arg2": args[0] if args else None,
"the_kwarg": kwargs.pop("the_kwarg", None),
"args": args[1:],
"kwargs": kwargs,
}
rendered = SimpleComponent.render()
assertHTMLEqual(
rendered,
"""
the_arg2: None
args: []
the_kwarg: None
kwargs: {}
---
from_context:
---
slot_second: SLOT_SECOND_DEFAULT
""",
)
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_render_full(self, components_settings):
class SimpleComponent(Component):
template: types.django_html = """
{% load component_tags %}
the_arg: {{ the_arg }}
the_arg2: {{ the_arg2 }}
args: {{ args|safe }}
the_kwarg: {{ the_kwarg }}
kwargs: {{ kwargs|safe }}
---
from_context: {{ from_context }}
---
slot_first: {% slot "first" required %}
{% endslot %}
---
slot_second: {% slot "second" default %}
SLOT_SECOND_DEFAULT
{% endslot %}
"""
def get_template_data(self, args, kwargs, slots, context):
return {
"the_arg": args[0],
"the_arg2": args[1],
"the_kwarg": kwargs.pop("the_kwarg", None),
"args": args[2:],
"kwargs": kwargs,
}
rendered = SimpleComponent.render(
context={"from_context": 98},
args=["one", "two", "three"],
kwargs={"the_kwarg": "test", "kw2": "ooo"},
slots={"first": "FIRST_SLOT"},
)
assertHTMLEqual(
rendered,
"""
the_arg: one
the_arg2: two
args: ['three']
the_kwarg: test
kwargs: {'kw2': 'ooo'}
---
from_context: 98
---
slot_first: FIRST_SLOT
---
slot_second: SLOT_SECOND_DEFAULT
""",
)
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_render_to_response_full(self, components_settings):
class SimpleComponent(Component):
template: types.django_html = """
{% load component_tags %}
the_arg: {{ the_arg }}
the_arg2: {{ the_arg2 }}
args: {{ args|safe }}
the_kwarg: {{ the_kwarg }}
kwargs: {{ kwargs|safe }}
---
from_context: {{ from_context }}
---
slot_first: {% slot "first" required %}
{% endslot %}
---
slot_second: {% slot "second" default %}
SLOT_SECOND_DEFAULT
{% endslot %}
"""
def get_template_data(self, args, kwargs, slots, context):
return {
"the_arg": args[0],
"the_arg2": args[1],
"the_kwarg": kwargs.pop("the_kwarg"),
"args": args[2:],
"kwargs": kwargs,
}
rendered = SimpleComponent.render_to_response(
context={"from_context": 98},
args=["one", "two", "three"],
kwargs={"the_kwarg": "test", "kw2": "ooo"},
slots={"first": "FIRST_SLOT"},
)
assert isinstance(rendered, HttpResponse)
assertHTMLEqual(
rendered.content.decode(),
"""
the_arg: one
the_arg2: two
args: ['three']
the_kwarg: test
kwargs: {'kw2': 'ooo'}
---
from_context: 98
---
slot_first: FIRST_SLOT
---
slot_second: SLOT_SECOND_DEFAULT
""",
)
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_render_to_response_change_response_class(self, components_settings):
class MyResponse:
def __init__(self, content: str) -> None:
self.content = bytes(content, "utf-8")
class SimpleComponent(Component):
response_class = MyResponse
template: types.django_html = "HELLO"
rendered = SimpleComponent.render_to_response()
assert isinstance(rendered, MyResponse)
assertHTMLEqual(
rendered.content.decode(),
"HELLO",
)
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_render_with_include(self, components_settings):
class SimpleComponent(Component):
template: types.django_html = """
{% load component_tags %}
{% include 'slotted_template.html' %}
"""
rendered = SimpleComponent.render()
assertHTMLEqual(
rendered,
"""
<custom-template data-djc-id-ca1bc3e>
<header>Default header</header>
<main>Default main</main>
<footer>Default footer</footer>
</custom-template>
""",
)
# See https://github.com/django-components/django-components/issues/580
# And https://github.com/django-components/django-components/commit/fee26ec1d8b46b5ee065ca1ce6143889b0f96764
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_render_with_include_and_context(self, components_settings):
class SimpleComponent(Component):
template: types.django_html = """
{% load component_tags %}
{% include 'slotted_template.html' %}
"""
rendered = SimpleComponent.render(context=Context())
assertHTMLEqual(
rendered,
"""
<custom-template data-djc-id-ca1bc3e>
<header>Default header</header>
<main>Default main</main>
<footer>Default footer</footer>
</custom-template>
""",
)
# See https://github.com/django-components/django-components/issues/580
# And https://github.com/django-components/django-components/issues/634
# And https://github.com/django-components/django-components/commit/fee26ec1d8b46b5ee065ca1ce6143889b0f96764
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_render_with_include_and_request_context(self, components_settings):
class SimpleComponent(Component):
template: types.django_html = """
{% load component_tags %}
{% include 'slotted_template.html' %}
"""
rendered = SimpleComponent.render(context=RequestContext(HttpRequest()))
assertHTMLEqual(
rendered,
"""
<custom-template data-djc-id-ca1bc3e>
<header>Default header</header>
<main>Default main</main>
<footer>Default footer</footer>
</custom-template>
""",
)
# See https://github.com/django-components/django-components/issues/580
# And https://github.com/django-components/django-components/issues/634
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_request_context_is_populated_from_context_processors(self, components_settings):
@register("thing")
class Thing(Component):
template: types.django_html = """
<kbd>Rendered {{ how }}</kbd>
<div>
CSRF token: {{ csrf_token|default:"<em>No CSRF token</em>" }}
</div>
"""
def get_template_data(self, args, kwargs, slots, context):
return {"how": kwargs.pop("how")}
class View(ComponentView):
def get(self, request):
how = "via GET request"
return self.component.render_to_response(
context=RequestContext(self.request),
kwargs={"how": how},
)
client = CustomClient(urlpatterns=[path("test_thing/", Thing.as_view())])
response = client.get("/test_thing/")
assert response.status_code == 200
# Full response:
# """
# <kbd>
# Rendered via GET request
# </kbd>
# <div>
# CSRF token:
# <div>
# test_csrf_token
# </div>
# </div>
# """
assertInHTML(
"""
<kbd data-djc-id-ca1bc3f>
Rendered via GET request
</kbd>
""",
response.content.decode(),
)
token_re = re.compile(rb"CSRF token:\s+predictabletoken")
token = token_re.findall(response.content)[0]
assert token == b"CSRF token: predictabletoken"
def test_request_context_created_when_no_context(self):
@register("thing")
class Thing(Component):
template: types.django_html = """
CSRF token: {{ csrf_token|default:"<em>No CSRF token</em>" }}
"""
class View:
def get(self, request):
return Thing.render_to_response(request=request)
client = CustomClient(urlpatterns=[path("test_thing/", Thing.as_view())])
response = client.get("/test_thing/")
assert response.status_code == 200
token_re = re.compile(rb"CSRF token:\s+predictabletoken")
token = token_re.findall(response.content)[0]
assert token == b"CSRF token: predictabletoken"
def test_request_context_created_when_already_a_context_dict(self):
@register("thing")
class Thing(Component):
template: types.django_html = """
<p>CSRF token: {{ csrf_token|default:"<em>No CSRF token</em>" }}</p>
<p>Existing context: {{ existing_context|default:"<em>No existing context</em>" }}</p>
"""
class View:
def get(self, request):
return Thing.render_to_response(request=request, context={"existing_context": "foo"})
client = CustomClient(urlpatterns=[path("test_thing/", Thing.as_view())])
response = client.get("/test_thing/")
assert response.status_code == 200
token_re = re.compile(rb"CSRF token:\s+predictabletoken")
token = token_re.findall(response.content)[0]
assert token == b"CSRF token: predictabletoken"
assert "Existing context: foo" in response.content.decode()
def request_context_ignores_context_when_already_a_context(self):
@register("thing")
class Thing(Component):
template: types.django_html = """
<p>CSRF token: {{ csrf_token|default:"<em>No CSRF token</em>" }}</p>
<p>Existing context: {{ existing_context|default:"<em>No existing context</em>" }}</p>
"""
class View:
def get(self, request):
return Thing.render_to_response(
request=request,
context=Context({"existing_context": "foo"}),
)
client = CustomClient(urlpatterns=[path("test_thing/", Thing.as_view())])
response = client.get("/test_thing/")
assert response.status_code == 200
token_re = re.compile(rb"CSRF token:\s+(?P<token>[0-9a-zA-Z]{64})")
assert not token_re.findall(response.content)
assert "Existing context: foo" in response.content.decode()
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_render_with_extends(self, components_settings):
class SimpleComponent(Component):
template: types.django_html = """
{% extends 'block.html' %}
{% block body %}
OVERRIDEN
{% endblock %}
"""
rendered = SimpleComponent.render(deps_strategy="ignore")
assertHTMLEqual(
rendered,
"""
<!DOCTYPE html>
<html data-djc-id-ca1bc3e lang="en">
<body>
<main role="main">
<div class='container main-container'>
OVERRIDEN
</div>
</main>
</body>
</html>
""",
)
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_render_can_access_instance(self, components_settings):
class TestComponent(Component):
template = "Variable: <strong>{{ id }}</strong>"
def get_template_data(self, args, kwargs, slots, context):
return {
"id": self.id,
}
rendered = TestComponent.render()
assertHTMLEqual(
rendered,
"Variable: <strong data-djc-id-ca1bc3e>ca1bc3e</strong>",
)
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_render_to_response_can_access_instance(self, components_settings):
class TestComponent(Component):
template = "Variable: <strong>{{ id }}</strong>"
def get_template_data(self, args, kwargs, slots, context):
return {
"id": self.id,
}
rendered_resp = TestComponent.render_to_response()
assertHTMLEqual(
rendered_resp.content.decode("utf-8"),
"Variable: <strong data-djc-id-ca1bc3e>ca1bc3e</strong>",
)
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_prepends_exceptions_with_component_path(self, components_settings):
@register("broken")
class Broken(Component):
template: types.django_html = """
{% load component_tags %}
<div> injected: {{ data|safe }} </div>
<main>
{% slot "content" default / %}
</main>
"""
def get_template_data(self, args, kwargs, slots, context):
data = self.inject("my_provide")
data["data1"] # This should raise TypeError
return {"data": data}
@register("provider")
class Provider(Component):
def get_template_data(self, args, kwargs, slots, context):
return {"data": kwargs["data"]}
template: types.django_html = """
{% load component_tags %}
{% provide "my_provide" key="hi" data=data %}
{% slot "content" default / %}
{% endprovide %}
"""
@register("parent")
class Parent(Component):
def get_template_data(self, args, kwargs, slots, context):
return {"data": kwargs["data"]}
template: types.django_html = """
{% load component_tags %}
{% component "provider" data=data %}
{% component "broken" %}
{% slot "content" default / %}
{% endcomponent %}
{% endcomponent %}
"""
@register("root")
class Root(Component):
template: types.django_html = """
{% load component_tags %}
{% component "parent" data=123 %}
{% fill "content" %}
456
{% endfill %}
{% endcomponent %}
"""
with pytest.raises(
TypeError,
match=re.escape(
"An error occured while rendering components Root > parent > provider > provider(slot:content) > broken:\n" # noqa: E501
"tuple indices must be integers or slices, not str"
),
):
Root.render()
@djc_test
class TestComponentHook:
def test_on_render_before(self):
@register("nested")
class NestedComponent(Component):
template: types.django_html = """
{% load component_tags %}
Hello from nested
<div>
{% slot "content" default / %}
</div>
"""
class SimpleComponent(Component):
template: types.django_html = """
{% load component_tags %}
args: {{ args|safe }}
kwargs: {{ kwargs|safe }}
---
from_on_before: {{ from_on_before }}
---
{% component "nested" %}
Hello from simple
{% endcomponent %}
"""
def get_template_data(self, args, kwargs, slots, context):
return {
"args": args,
"kwargs": kwargs,
}
def on_render_before(self, context: Context, template: Template) -> None:
# Insert value into the Context
context["from_on_before"] = ":)"
# Insert text into the Template
#
# NOTE: Users should NOT do this, because this will insert the text every time
# the component is rendered.
template.nodelist.append(TextNode("\n---\nFROM_ON_BEFORE"))
rendered = SimpleComponent.render()
assertHTMLEqual(
rendered,
"""
args: []
kwargs: {}
---
from_on_before: :)
---
Hello from nested
<div data-djc-id-ca1bc3e data-djc-id-ca1bc40>
Hello from simple
</div>
---
FROM_ON_BEFORE
""",
)
# Check that modifying the context or template does nothing
def test_on_render_after(self):
captured_content = None
@register("nested")
class NestedComponent(Component):
template: types.django_html = """
{% load component_tags %}
Hello from nested
<div>
{% slot "content" default / %}
</div>
"""
class SimpleComponent(Component):
template: types.django_html = """
{% load component_tags %}
args: {{ args|safe }}
kwargs: {{ kwargs|safe }}
---
from_on_after: {{ from_on_after }}
---
{% component "nested" %}
Hello from simple
{% endcomponent %}
"""
def get_template_data(self, args, kwargs, slots, context):
return {
"args": args,
"kwargs": kwargs,
}
# Check that modifying the context or template does nothing
def on_render_after(self, context: Context, template: Template, content: str) -> None:
# Insert value into the Context
context["from_on_after"] = ":)"
# Insert text into the Template
template.nodelist.append(TextNode("\n---\nFROM_ON_AFTER"))
nonlocal captured_content
captured_content = content
rendered = SimpleComponent.render()
assertHTMLEqual(
captured_content,
"""
args: []
kwargs: {}
---
from_on_after:
---
Hello from nested
<div data-djc-id-ca1bc3e data-djc-id-ca1bc40>
Hello from simple
</div>
""",
)
assertHTMLEqual(
rendered,
"""
args: []
kwargs: {}
---
from_on_after:
---
Hello from nested
<div data-djc-id-ca1bc3e data-djc-id-ca1bc40>
Hello from simple
</div>
""",
)
# Check that modifying the context or template does nothing
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_on_render_after_override_output(self, components_settings):
captured_content = None
@register("nested")
class NestedComponent(Component):
template: types.django_html = """
{% load component_tags %}
Hello from nested
<div>
{% slot "content" default / %}
</div>
"""
class SimpleComponent(Component):
template: types.django_html = """
{% load component_tags %}
args: {{ args|safe }}
kwargs: {{ kwargs|safe }}
---
from_on_before: {{ from_on_before }}
---
{% component "nested" %}
Hello from simple
{% endcomponent %}
"""
def get_template_data(self, args, kwargs, slots, context):
return {
"args": args,
"kwargs": kwargs,
}
def on_render_after(self, context: Context, template: Template, content: str) -> str:
nonlocal captured_content
captured_content = content
return "Chocolate cookie recipe: " + content
rendered = SimpleComponent.render()
assertHTMLEqual(
captured_content,
"""
args: []
kwargs: {}
---
from_on_before:
---
Hello from nested
<div data-djc-id-ca1bc3e data-djc-id-ca1bc40>
Hello from simple
</div>
""",
)
assertHTMLEqual(
rendered,
"""
Chocolate cookie recipe:
args: []
kwargs: {}
---
from_on_before:
---
Hello from nested
<div data-djc-id-ca1bc3e data-djc-id-ca1bc40>
Hello from simple
</div>
""",
)
def test_on_render_before_after_same_context(self):
context_in_before = None
context_in_after = None
@register("nested")
class NestedComponent(Component):
template: types.django_html = """
{% load component_tags %}
Hello from nested
<div>
{% slot "content" default / %}
</div>
"""
class SimpleComponent(Component):
template: types.django_html = """
{% load component_tags %}
args: {{ args|safe }}
kwargs: {{ kwargs|safe }}
---
from_on_after: {{ from_on_after }}
---
{% component "nested" %}
Hello from simple
{% endcomponent %}
"""
def get_template_data(self, args, kwargs, slots, context):
return {
"args": args,
"kwargs": kwargs,
}
def on_render_before(self, context: Context, template: Template) -> None:
context["from_on_before"] = ":)"
nonlocal context_in_before
context_in_before = context
# Check that modifying the context or template does nothing
def on_render_after(self, context: Context, template: Template, html: str) -> None:
context["from_on_after"] = ":)"
nonlocal context_in_after
context_in_after = context
SimpleComponent.render()
assert context_in_before == context_in_after
assert "from_on_before" in context_in_before # type: ignore[operator]
assert "from_on_after" in context_in_after # type: ignore[operator]
@djc_test
class TestComponentHelpers:
def test_all_components(self):
# NOTE: When running all tests, this list may already have some components
# as some components in test files are defined on module level, outside of
# `djc_test` decorator.
all_comps_before = len(all_components())
# Components don't have to be registered to be included in the list
class TestComponent(Component):
template: types.django_html = """
Hello from test
"""
assert len(all_components()) == all_comps_before + 1
@register("test2")
class Test2Component(Component):
template: types.django_html = """
Hello from test2
"""
assert len(all_components()) == all_comps_before + 2