django-components/tests/test_component.py
Juro Oravec bae0f28813
refactor: Instantiate component when rendering, and remove metadata stack (#1212)
* refactor: Instantiate component when rendering, and remove metadata stack

* refactor: update test

* refactor: fix linter errors

* docs: remove example from changelog
2025-05-25 23:33:38 +02:00

1498 lines
50 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.core.exceptions import ImproperlyConfigured
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.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>
""",
)
@djc_test
class TestComponent:
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_empty_component(self, components_settings):
class EmptyComponent(Component):
pass
with pytest.raises(ImproperlyConfigured):
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_string_dynamic(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>
""",
)
@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>
""",
)
@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>
""",
)
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_template_file_dynamic(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>
""",
)
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_allows_to_return_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>
""",
)
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):
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 get_template(self, 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"
template_str: types.django_html = """
{% load component_tags %}
Variable: <strong>{{ variable }}</strong>
{% slot 'my_slot' / %}
"""
return Template(template_str)
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