""" Tests focusing on the Component class. For tests focusing on the `component` tag, see `test_templatetags_component.py` """ import os import re from typing import Any, List, Literal, NamedTuple, Optional 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, ComponentRegistry, ComponentView, Slot, SlotInput, all_components, get_component_by_class_id, register, registry, types, ) from django_components.template import _get_component_template from django_components.testing import djc_test from django_components.urls import urlpatterns as dc_urlpatterns from .testutils import PARAMETRIZE_CONTEXT_BEHAVIOR, setup_test_config setup_test_config() # 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[attr-defined] settings.ROOT_URLCONF = urls_module else: settings.ROOT_URLCONF = __name__ settings.SECRET_KEY = "secret" # noqa: S105 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: {{ variable }} """ 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: test """, ) # 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: {{ variable }} """ 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: test """, ) # 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 = """
CSRF token: {{ csrf_token|default:"No CSRF token" }}
Existing context: {{ existing_context|default:"No existing context" }}
""" 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 = """CSRF token: {{ csrf_token|default:"No CSRF token" }}
Existing context: {{ existing_context|default:"No existing context" }}
""" 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+(?PHello
" rendered = SimpleComponent.render() assertHTMLEqual( rendered, "textHello
", ) # Works without lambda class SimpleComponent2(SimpleComponent): def on_render(self, context: Context, template: Template): html, _error = yield template.render(context) return html + "Hello
" rendered2 = SimpleComponent2.render() assertHTMLEqual( rendered2, "textHello
", ) def test_lambda_yield_error(self): def broken_template(): raise ValueError("BROKEN") class SimpleComponent(Component): def on_render(self, context: Context, template: Template): _html, error = yield lambda: broken_template() error.args = ("ERROR MODIFIED",) with pytest.raises( ValueError, match=re.escape("An error occured while rendering components SimpleComponent:\nERROR MODIFIED") ): SimpleComponent.render() # Does NOT work without lambda class SimpleComponent2(SimpleComponent): def on_render(self, context: Context, template: Template): # This raises an error instead of capturing it, # so we never get to modifying the error. _html, error = yield broken_template() error.args = ("ERROR MODIFIED",) with pytest.raises( ValueError, match=re.escape("An error occured while rendering components SimpleComponent2:\nBROKEN") ): SimpleComponent2.render() def test_on_render_no_yield(self): class SimpleComponent(Component): template: types.django_html = """ text """ def on_render(self, context: Context, template: Template): return "OVERRIDDEN" rendered = SimpleComponent.render() assert rendered == "OVERRIDDEN" def test_on_render_reraise_error(self): registry.register("broken", self._gen_broken_component()) class SimpleComponent(Component): template: types.django_html = """ {% component "broken" / %} """ def on_render(self, context: Context, template: Template): _html, error = yield lambda: template.render(context) raise error from None # Re-raise original error with pytest.raises(ValueError, match=re.escape("BROKEN")): SimpleComponent.render() def test_on_render_multiple_yields(self): registry.register("broken", self._gen_broken_component()) results = [] class SimpleComponent(Component): template: types.django_html = """ {% if case == 1 %} {% component "broken" / %} {% elif case == 2 %}