mirror of
https://github.com/django-components/django-components.git
synced 2025-07-07 17:34:59 +00:00

* refactor: Cache components' JS and CSS scripts at class creation time * refactor: add test for no template_rendered signal for component with no template
711 lines
27 KiB
Python
711 lines
27 KiB
Python
import gc
|
|
from typing import Any, Callable, Dict, List, cast
|
|
|
|
import pytest
|
|
from django.http import HttpRequest, HttpResponse
|
|
from django.template import Context, Origin, Template
|
|
from django.test import Client
|
|
|
|
from django_components import Component, Slot, SlotNode, register, registry
|
|
from django_components.app_settings import app_settings
|
|
from django_components.component_registry import ComponentRegistry
|
|
from django_components.extension import (
|
|
URLRoute,
|
|
ComponentExtension,
|
|
ExtensionComponentConfig,
|
|
OnComponentClassCreatedContext,
|
|
OnComponentClassDeletedContext,
|
|
OnRegistryCreatedContext,
|
|
OnRegistryDeletedContext,
|
|
OnComponentRegisteredContext,
|
|
OnComponentUnregisteredContext,
|
|
OnComponentInputContext,
|
|
OnComponentDataContext,
|
|
OnComponentRenderedContext,
|
|
OnSlotRenderedContext,
|
|
OnTemplateLoadedContext,
|
|
OnTemplateCompiledContext,
|
|
OnJsLoadedContext,
|
|
OnCssLoadedContext,
|
|
)
|
|
from django_components.extensions.cache import CacheExtension
|
|
from django_components.extensions.debug_highlight import DebugHighlightExtension
|
|
from django_components.extensions.defaults import DefaultsExtension
|
|
from django_components.extensions.dependencies import DependenciesExtension
|
|
from django_components.extensions.view import ViewExtension
|
|
|
|
from django_components.testing import djc_test
|
|
from .testutils import setup_test_config
|
|
|
|
setup_test_config({"autodiscover": False})
|
|
|
|
|
|
def dummy_view(request: HttpRequest):
|
|
# Test that the request object is passed to the view
|
|
assert isinstance(request, HttpRequest)
|
|
return HttpResponse("Hello, world!")
|
|
|
|
|
|
def dummy_view_2(request: HttpRequest, id: int, name: str):
|
|
return HttpResponse(f"Hello, world! {id} {name}")
|
|
|
|
|
|
# TODO_V1 - Remove
|
|
class LegacyExtension(ComponentExtension):
|
|
name = "legacy"
|
|
|
|
class ExtensionClass(ExtensionComponentConfig):
|
|
foo = "1"
|
|
bar = "2"
|
|
|
|
@classmethod
|
|
def baz(cls):
|
|
return "3"
|
|
|
|
|
|
class DummyExtension(ComponentExtension):
|
|
"""
|
|
Test extension that tracks all hook calls and their arguments.
|
|
"""
|
|
|
|
name = "test_extension"
|
|
|
|
class ComponentConfig(ExtensionComponentConfig):
|
|
foo = "1"
|
|
bar = "2"
|
|
|
|
@classmethod
|
|
def baz(cls):
|
|
return "3"
|
|
|
|
def __init__(self) -> None:
|
|
self.calls: Dict[str, List[Any]] = {
|
|
"on_component_class_created": [],
|
|
"on_component_class_deleted": [],
|
|
"on_registry_created": [],
|
|
"on_registry_deleted": [],
|
|
"on_component_registered": [],
|
|
"on_component_unregistered": [],
|
|
"on_component_input": [],
|
|
"on_component_data": [],
|
|
"on_component_rendered": [],
|
|
"on_slot_rendered": [],
|
|
"on_template_loaded": [],
|
|
"on_template_compiled": [],
|
|
"on_js_loaded": [],
|
|
"on_css_loaded": [],
|
|
}
|
|
|
|
urls = [
|
|
URLRoute(path="dummy-view/", handler=dummy_view, name="dummy"),
|
|
URLRoute(path="dummy-view-2/<int:id>/<str:name>/", handler=dummy_view_2, name="dummy-2"),
|
|
]
|
|
|
|
def on_component_class_created(self, ctx: OnComponentClassCreatedContext) -> None:
|
|
# NOTE: Store only component name to avoid strong references
|
|
self.calls["on_component_class_created"].append(ctx.component_cls.__name__)
|
|
|
|
def on_component_class_deleted(self, ctx: OnComponentClassDeletedContext) -> None:
|
|
# NOTE: Store only component name to avoid strong references
|
|
self.calls["on_component_class_deleted"].append(ctx.component_cls.__name__)
|
|
|
|
def on_registry_created(self, ctx: OnRegistryCreatedContext) -> None:
|
|
# NOTE: Store only registry object ID to avoid strong references
|
|
self.calls["on_registry_created"].append(id(ctx.registry))
|
|
|
|
def on_registry_deleted(self, ctx: OnRegistryDeletedContext) -> None:
|
|
# NOTE: Store only registry object ID to avoid strong references
|
|
self.calls["on_registry_deleted"].append(id(ctx.registry))
|
|
|
|
def on_component_registered(self, ctx: OnComponentRegisteredContext) -> None:
|
|
self.calls["on_component_registered"].append(ctx)
|
|
|
|
def on_component_unregistered(self, ctx: OnComponentUnregisteredContext) -> None:
|
|
self.calls["on_component_unregistered"].append(ctx)
|
|
|
|
def on_component_input(self, ctx: OnComponentInputContext) -> None:
|
|
self.calls["on_component_input"].append(ctx)
|
|
|
|
def on_component_data(self, ctx: OnComponentDataContext) -> None:
|
|
self.calls["on_component_data"].append(ctx)
|
|
|
|
def on_component_rendered(self, ctx: OnComponentRenderedContext) -> None:
|
|
self.calls["on_component_rendered"].append(ctx)
|
|
|
|
def on_slot_rendered(self, ctx: OnSlotRenderedContext) -> None:
|
|
self.calls["on_slot_rendered"].append(ctx)
|
|
|
|
def on_template_loaded(self, ctx):
|
|
self.calls["on_template_loaded"].append(ctx)
|
|
|
|
def on_template_compiled(self, ctx):
|
|
self.calls["on_template_compiled"].append(ctx)
|
|
|
|
def on_js_loaded(self, ctx):
|
|
self.calls["on_js_loaded"].append(ctx)
|
|
|
|
def on_css_loaded(self, ctx):
|
|
self.calls["on_css_loaded"].append(ctx)
|
|
|
|
|
|
class DummyNestedExtension(ComponentExtension):
|
|
name = "test_nested_extension"
|
|
|
|
urls = [
|
|
URLRoute(
|
|
path="nested-view/",
|
|
children=[
|
|
URLRoute(path="<int:id>/<str:name>/", handler=dummy_view_2, name="dummy-2"),
|
|
],
|
|
name="dummy",
|
|
),
|
|
]
|
|
|
|
|
|
class RenderExtension(ComponentExtension):
|
|
name = "render"
|
|
|
|
|
|
class SlotOverrideExtension(ComponentExtension):
|
|
name = "slot_override"
|
|
|
|
def on_slot_rendered(self, ctx: OnSlotRenderedContext):
|
|
return "OVERRIDEN BY EXTENSION"
|
|
|
|
|
|
class ErrorOnComponentRenderedExtension(ComponentExtension):
|
|
name = "error_on_component_rendered"
|
|
|
|
def on_component_rendered(self, ctx: OnComponentRenderedContext):
|
|
raise RuntimeError("Custom error from extension")
|
|
|
|
|
|
class ReturnHtmlOnComponentRenderedExtension(ComponentExtension):
|
|
name = "return_html_on_component_rendered"
|
|
|
|
def on_component_rendered(self, ctx: OnComponentRenderedContext):
|
|
return f"<div>OVERRIDDEN: {ctx.result}</div>"
|
|
|
|
|
|
def with_component_cls(on_created: Callable):
|
|
class TempComponent(Component):
|
|
template = "Hello {{ name }}!"
|
|
|
|
def get_template_data(self, args, kwargs, slots, context):
|
|
return {"name": kwargs.get("name", "World")}
|
|
|
|
on_created()
|
|
|
|
|
|
def with_registry(on_created: Callable):
|
|
registry = ComponentRegistry()
|
|
|
|
on_created(registry)
|
|
|
|
|
|
class OverrideAssetExtension(ComponentExtension):
|
|
name = "override_asset_extension"
|
|
|
|
def on_template_loaded(self, ctx):
|
|
return "OVERRIDDEN TEMPLATE"
|
|
|
|
def on_js_loaded(self, ctx):
|
|
return "OVERRIDDEN JS"
|
|
|
|
def on_css_loaded(self, ctx):
|
|
return "OVERRIDDEN CSS"
|
|
|
|
|
|
@djc_test
|
|
class TestExtension:
|
|
@djc_test(components_settings={"extensions": [DummyExtension]})
|
|
def test_extensions_setting(self):
|
|
assert len(app_settings.EXTENSIONS) == 6
|
|
assert isinstance(app_settings.EXTENSIONS[0], CacheExtension)
|
|
assert isinstance(app_settings.EXTENSIONS[1], DefaultsExtension)
|
|
assert isinstance(app_settings.EXTENSIONS[2], DependenciesExtension)
|
|
assert isinstance(app_settings.EXTENSIONS[3], ViewExtension)
|
|
assert isinstance(app_settings.EXTENSIONS[4], DebugHighlightExtension)
|
|
assert isinstance(app_settings.EXTENSIONS[5], DummyExtension)
|
|
|
|
@djc_test(components_settings={"extensions": [DummyExtension]})
|
|
def test_access_component_from_extension(self):
|
|
class TestAccessComp(Component):
|
|
template = "Hello {{ name }}!"
|
|
|
|
def get_template_data(self, args, kwargs, slots, context):
|
|
return {"name": kwargs.get("name", "World")}
|
|
|
|
ext_class = TestAccessComp.TestExtension # type: ignore[attr-defined]
|
|
assert issubclass(ext_class, ComponentExtension.ComponentConfig)
|
|
assert ext_class.component_class is TestAccessComp
|
|
|
|
# NOTE: Required for test_component_class_lifecycle_hooks to work
|
|
del TestAccessComp
|
|
gc.collect()
|
|
|
|
def test_raises_on_extension_name_conflict(self):
|
|
@djc_test(components_settings={"extensions": [RenderExtension]})
|
|
def inner():
|
|
pass
|
|
|
|
with pytest.raises(ValueError, match="Extension name 'render' conflicts with existing Component class API"):
|
|
inner()
|
|
|
|
def test_raises_on_multiple_extensions_with_same_name(self):
|
|
@djc_test(components_settings={"extensions": [DummyExtension, DummyExtension]})
|
|
def inner():
|
|
pass
|
|
|
|
with pytest.raises(ValueError, match="Multiple extensions cannot have the same name 'test_extension'"):
|
|
inner()
|
|
|
|
|
|
@djc_test
|
|
class TestExtensionHooks:
|
|
@djc_test(components_settings={"extensions": [DummyExtension]})
|
|
def test_component_class_lifecycle_hooks(self):
|
|
extension = cast(DummyExtension, app_settings.EXTENSIONS[5])
|
|
|
|
assert len(extension.calls["on_component_class_created"]) == 0
|
|
assert len(extension.calls["on_component_class_deleted"]) == 0
|
|
|
|
did_call_on_comp_cls_created = False
|
|
|
|
def on_comp_cls_created():
|
|
nonlocal did_call_on_comp_cls_created
|
|
did_call_on_comp_cls_created = True
|
|
|
|
# Verify on_component_class_created was called
|
|
assert len(extension.calls["on_component_class_created"]) == 1
|
|
assert extension.calls["on_component_class_created"][0] == "TempComponent"
|
|
|
|
# Create a component class in a separate scope, to avoid any references from within
|
|
# this test function, so we can garbage collect it after the function returns
|
|
with_component_cls(on_comp_cls_created)
|
|
assert did_call_on_comp_cls_created
|
|
|
|
# This should trigger the garbage collection of the component class
|
|
gc.collect()
|
|
|
|
# Verify on_component_class_deleted was called
|
|
# NOTE: The previous test, `test_access_component_from_extension`, is sometimes
|
|
# garbage-collected too late, in which case it's included in `on_component_class_deleted`.
|
|
# So in the test we check only for the last call.
|
|
assert len(extension.calls["on_component_class_deleted"]) >= 1
|
|
assert extension.calls["on_component_class_deleted"][-1] == "TempComponent"
|
|
|
|
@djc_test(components_settings={"extensions": [DummyExtension]})
|
|
def test_registry_lifecycle_hooks(self):
|
|
extension = cast(DummyExtension, app_settings.EXTENSIONS[5])
|
|
|
|
assert len(extension.calls["on_registry_created"]) == 0
|
|
assert len(extension.calls["on_registry_deleted"]) == 0
|
|
|
|
did_call_on_registry_created = False
|
|
reg_id = None
|
|
|
|
def on_registry_created(reg):
|
|
nonlocal did_call_on_registry_created
|
|
nonlocal reg_id
|
|
did_call_on_registry_created = True
|
|
reg_id = id(reg)
|
|
|
|
# Verify on_registry_created was called
|
|
assert len(extension.calls["on_registry_created"]) == 1
|
|
assert extension.calls["on_registry_created"][0] == reg_id
|
|
|
|
with_registry(on_registry_created)
|
|
assert did_call_on_registry_created
|
|
assert reg_id is not None
|
|
|
|
gc.collect()
|
|
|
|
# Verify on_registry_deleted was called
|
|
assert len(extension.calls["on_registry_deleted"]) == 1
|
|
assert extension.calls["on_registry_deleted"][0] == reg_id
|
|
|
|
@djc_test(components_settings={"extensions": [DummyExtension]})
|
|
def test_component_registration_hooks(self):
|
|
class TestComponent(Component):
|
|
template = "Hello {{ name }}!"
|
|
|
|
def get_template_data(self, args, kwargs, slots, context):
|
|
return {"name": kwargs.get("name", "World")}
|
|
|
|
registry.register("test_comp", TestComponent)
|
|
extension = cast(DummyExtension, app_settings.EXTENSIONS[5])
|
|
|
|
# Verify on_component_registered was called
|
|
assert len(extension.calls["on_component_registered"]) == 1
|
|
reg_call: OnComponentRegisteredContext = extension.calls["on_component_registered"][0]
|
|
assert reg_call.registry == registry
|
|
assert reg_call.name == "test_comp"
|
|
assert reg_call.component_cls == TestComponent
|
|
|
|
registry.unregister("test_comp")
|
|
|
|
# Verify on_component_unregistered was called
|
|
assert len(extension.calls["on_component_unregistered"]) == 1
|
|
unreg_call: OnComponentUnregisteredContext = extension.calls["on_component_unregistered"][0]
|
|
assert unreg_call.registry == registry
|
|
assert unreg_call.name == "test_comp"
|
|
assert unreg_call.component_cls == TestComponent
|
|
|
|
@djc_test(components_settings={"extensions": [DummyExtension]})
|
|
def test_component_render_hooks(self):
|
|
@register("test_comp")
|
|
class TestComponent(Component):
|
|
template = "Hello {{ name }}!"
|
|
|
|
def get_template_data(self, args, kwargs, slots, context):
|
|
return {"name": kwargs.get("name", "World")}
|
|
|
|
def get_js_data(self, args, kwargs, slots, context):
|
|
return {"script": "console.log('Hello!')"}
|
|
|
|
def get_css_data(self, args, kwargs, slots, context):
|
|
return {"style": "body { color: blue; }"}
|
|
|
|
# Render the component with some args and kwargs
|
|
test_context = Context({"foo": "bar"})
|
|
test_slots = {"content": "Some content"}
|
|
TestComponent.render(context=test_context, args=("arg1", "arg2"), kwargs={"name": "Test"}, slots=test_slots)
|
|
|
|
extension = cast(DummyExtension, app_settings.EXTENSIONS[5])
|
|
|
|
# Verify on_component_input was called with correct args
|
|
assert len(extension.calls["on_component_input"]) == 1
|
|
input_call: OnComponentInputContext = extension.calls["on_component_input"][0]
|
|
assert input_call.component_cls == TestComponent
|
|
assert isinstance(input_call.component_id, str)
|
|
assert input_call.args == ["arg1", "arg2"]
|
|
assert input_call.kwargs == {"name": "Test"}
|
|
assert len(input_call.slots) == 1
|
|
assert isinstance(input_call.slots["content"], Slot)
|
|
assert input_call.context == test_context
|
|
|
|
# Verify on_component_data was called with correct args
|
|
assert len(extension.calls["on_component_data"]) == 1
|
|
data_call: OnComponentDataContext = extension.calls["on_component_data"][0]
|
|
assert data_call.component_cls == TestComponent
|
|
assert isinstance(data_call.component_id, str)
|
|
assert data_call.template_data == {"name": "Test"}
|
|
assert data_call.js_data == {"script": "console.log('Hello!')"}
|
|
assert data_call.css_data == {"style": "body { color: blue; }"}
|
|
|
|
# Verify on_component_rendered was called with correct args
|
|
assert len(extension.calls["on_component_rendered"]) == 1
|
|
rendered_call: OnComponentRenderedContext = extension.calls["on_component_rendered"][0]
|
|
assert rendered_call.component_cls == TestComponent
|
|
assert isinstance(rendered_call.component, TestComponent)
|
|
assert isinstance(rendered_call.component_id, str)
|
|
assert rendered_call.result == "<!-- _RENDERED TestComponent_f4a4f0,ca1bc3e,, -->Hello Test!"
|
|
assert rendered_call.error is None
|
|
|
|
@djc_test(components_settings={"extensions": [DummyExtension]})
|
|
def test_component_render_hooks__error(self):
|
|
@register("test_comp")
|
|
class TestComponent(Component):
|
|
template = "Hello {{ name }}!"
|
|
|
|
def on_render_after(self, context, template, result, error):
|
|
raise Exception("Oopsie woopsie")
|
|
|
|
with pytest.raises(Exception, match="Oopsie woopsie"):
|
|
# Render the component with some args and kwargs
|
|
TestComponent.render(
|
|
context=Context({"foo": "bar"}),
|
|
args=("arg1", "arg2"),
|
|
kwargs={"name": "Test"},
|
|
slots={"content": "Some content"},
|
|
)
|
|
|
|
extension = cast(DummyExtension, app_settings.EXTENSIONS[5])
|
|
|
|
# Verify on_component_rendered was called with correct args
|
|
assert len(extension.calls["on_component_rendered"]) == 1
|
|
rendered_call: OnComponentRenderedContext = extension.calls["on_component_rendered"][0]
|
|
assert rendered_call.component_cls == TestComponent
|
|
assert isinstance(rendered_call.component, TestComponent)
|
|
assert isinstance(rendered_call.component_id, str)
|
|
assert rendered_call.result is None
|
|
assert isinstance(rendered_call.error, Exception)
|
|
assert str(rendered_call.error) == "An error occured while rendering components TestComponent:\nOopsie woopsie"
|
|
|
|
@djc_test(components_settings={"extensions": [DummyExtension]})
|
|
def test_on_slot_rendered(self):
|
|
@register("test_comp")
|
|
class TestComponent(Component):
|
|
template = "Hello {% slot 'content' required default / %}!"
|
|
|
|
# Render the component with some args and kwargs
|
|
test_context = Context({"foo": "bar"})
|
|
rendered = TestComponent.render(
|
|
context=test_context,
|
|
args=("arg1", "arg2"),
|
|
kwargs={"name": "Test"},
|
|
slots={"content": "Some content"},
|
|
)
|
|
|
|
assert rendered == "Hello Some content!"
|
|
|
|
extension = cast(DummyExtension, app_settings.EXTENSIONS[5])
|
|
|
|
# Verify on_slot_rendered was called with correct args
|
|
assert len(extension.calls["on_slot_rendered"]) == 1
|
|
slot_call: OnSlotRenderedContext = extension.calls["on_slot_rendered"][0]
|
|
assert isinstance(slot_call.component, TestComponent)
|
|
assert slot_call.component_cls == TestComponent
|
|
|
|
assert slot_call.component_id == "ca1bc3e"
|
|
assert isinstance(slot_call.slot, Slot)
|
|
assert slot_call.slot_name == "content"
|
|
assert isinstance(slot_call.slot_node, SlotNode)
|
|
assert slot_call.slot_node.template_name.endswith("test_extension.py::TestComponent") # type: ignore
|
|
assert slot_call.slot_node.template_component == TestComponent
|
|
assert slot_call.slot_is_required is True
|
|
assert slot_call.slot_is_default is True
|
|
assert slot_call.result == "Some content"
|
|
|
|
@djc_test(components_settings={"extensions": [SlotOverrideExtension]})
|
|
def test_on_slot_rendered__override(self):
|
|
@register("test_comp")
|
|
class TestComponent(Component):
|
|
template = "Hello {% slot 'content' required default / %}!"
|
|
|
|
rendered = TestComponent.render(
|
|
slots={"content": "Some content"},
|
|
)
|
|
|
|
assert rendered == "Hello OVERRIDEN BY EXTENSION!"
|
|
|
|
@djc_test(components_settings={"extensions": [ErrorOnComponentRenderedExtension]})
|
|
def test_on_component_rendered__error_from_extension(self):
|
|
@register("test_comp_error_ext")
|
|
class TestComponent(Component):
|
|
template = "Hello {{ name }}!"
|
|
|
|
def get_template_data(self, args, kwargs, slots, context):
|
|
return {"name": kwargs.get("name", "World")}
|
|
|
|
with pytest.raises(RuntimeError, match="Custom error from extension"):
|
|
TestComponent.render(args=(), kwargs={"name": "Test"})
|
|
|
|
@djc_test(components_settings={"extensions": [ReturnHtmlOnComponentRenderedExtension]})
|
|
def test_on_component_rendered__return_html_from_extension(self):
|
|
@register("test_comp_html_ext")
|
|
class TestComponent(Component):
|
|
template = "Hello {{ name }}!"
|
|
|
|
def get_template_data(self, args, kwargs, slots, context):
|
|
return {"name": kwargs.get("name", "World")}
|
|
|
|
rendered = TestComponent.render(args=(), kwargs={"name": "Test"})
|
|
assert rendered == "<div>OVERRIDDEN: Hello Test!</div>"
|
|
|
|
@djc_test(components_settings={"extensions": [DummyExtension]})
|
|
def test_asset_hooks__inlined(self):
|
|
@register("test_comp_hooks")
|
|
class TestComponent(Component):
|
|
template = "Hello {{ name }}!"
|
|
js = "console.log('hi');"
|
|
css = "body { color: red; }"
|
|
|
|
def get_template_data(self, args, kwargs, slots, context):
|
|
return {"name": kwargs.get("name", "World")}
|
|
|
|
# Render the component to trigger all hooks
|
|
TestComponent.render(args=(), kwargs={"name": "Test"})
|
|
|
|
extension = cast(DummyExtension, app_settings.EXTENSIONS[5])
|
|
|
|
# on_template_loaded
|
|
assert len(extension.calls["on_template_loaded"]) == 1
|
|
ctx1: OnTemplateLoadedContext = extension.calls["on_template_loaded"][0]
|
|
assert ctx1.component_cls == TestComponent
|
|
assert ctx1.content == "Hello {{ name }}!"
|
|
assert isinstance(ctx1.origin, Origin)
|
|
assert ctx1.origin.name.endswith("test_extension.py::TestComponent")
|
|
assert ctx1.name is None
|
|
|
|
# on_template_compiled
|
|
assert len(extension.calls["on_template_compiled"]) == 1
|
|
ctx2: OnTemplateCompiledContext = extension.calls["on_template_compiled"][0]
|
|
assert ctx2.component_cls == TestComponent
|
|
assert isinstance(ctx2.template, Template)
|
|
|
|
# on_js_loaded
|
|
assert len(extension.calls["on_js_loaded"]) == 1
|
|
ctx3: OnJsLoadedContext = extension.calls["on_js_loaded"][0]
|
|
assert ctx3.component_cls == TestComponent
|
|
assert ctx3.content == "console.log('hi');"
|
|
|
|
# on_css_loaded
|
|
assert len(extension.calls["on_css_loaded"]) == 1
|
|
ctx4: OnCssLoadedContext = extension.calls["on_css_loaded"][0]
|
|
assert ctx4.component_cls == TestComponent
|
|
assert ctx4.content == "body { color: red; }"
|
|
|
|
@djc_test(components_settings={"extensions": [DummyExtension]})
|
|
def test_asset_hooks__file(self):
|
|
@register("test_comp_hooks")
|
|
class TestComponent(Component):
|
|
template_file = "relative_file/relative_file.html"
|
|
js_file = "relative_file/relative_file.js"
|
|
css_file = "relative_file/relative_file.css"
|
|
|
|
def get_template_data(self, args, kwargs, slots, context):
|
|
return {"name": kwargs.get("name", "World")}
|
|
|
|
# Render the component to trigger all hooks
|
|
TestComponent.render(args=(), kwargs={"name": "Test"})
|
|
|
|
extension = cast(DummyExtension, app_settings.EXTENSIONS[5])
|
|
|
|
# on_template_loaded
|
|
# NOTE: The template file gets picked up by 'django.template.loaders.filesystem.Loader',
|
|
# as well as our own loader, so we get two calls here.
|
|
assert len(extension.calls["on_template_loaded"]) == 2
|
|
ctx1: OnTemplateLoadedContext = extension.calls["on_template_loaded"][0]
|
|
assert ctx1.component_cls == TestComponent
|
|
assert ctx1.content == (
|
|
'<form method="post">\n'
|
|
' {% csrf_token %}\n'
|
|
' <input type="text" name="variable" value="{{ variable }}">\n'
|
|
' <input type="submit">\n'
|
|
'</form>\n'
|
|
)
|
|
assert isinstance(ctx1.origin, Origin)
|
|
assert ctx1.origin.name.endswith("relative_file.html")
|
|
assert ctx1.name == "relative_file/relative_file.html"
|
|
|
|
# on_template_compiled
|
|
assert len(extension.calls["on_template_compiled"]) == 2
|
|
ctx2: OnTemplateCompiledContext = extension.calls["on_template_compiled"][0]
|
|
assert ctx2.component_cls == TestComponent
|
|
assert isinstance(ctx2.template, Template)
|
|
|
|
# on_js_loaded
|
|
assert len(extension.calls["on_js_loaded"]) == 1
|
|
ctx3: OnJsLoadedContext = extension.calls["on_js_loaded"][0]
|
|
assert ctx3.component_cls == TestComponent
|
|
assert ctx3.content == 'console.log("JS file");\n'
|
|
|
|
# on_css_loaded
|
|
assert len(extension.calls["on_css_loaded"]) == 1
|
|
ctx4: OnCssLoadedContext = extension.calls["on_css_loaded"][0]
|
|
assert ctx4.component_cls == TestComponent
|
|
assert ctx4.content == (
|
|
'.html-css-only {\n'
|
|
' color: blue;\n'
|
|
'}\n'
|
|
)
|
|
|
|
@djc_test(components_settings={"extensions": [OverrideAssetExtension]})
|
|
def test_asset_hooks_override(self):
|
|
@register("test_comp_override")
|
|
class TestComponent(Component):
|
|
template = "Hello {{ name }}!"
|
|
js = "console.log('hi');"
|
|
css = "body { color: red; }"
|
|
|
|
def get_template_data(self, args, kwargs, slots, context):
|
|
return {"name": kwargs.get("name", "World")}
|
|
|
|
# No need to render, accessing the attributes should trigger the hooks
|
|
assert TestComponent.template == "OVERRIDDEN TEMPLATE"
|
|
assert TestComponent.js == "OVERRIDDEN JS"
|
|
assert TestComponent.css == "OVERRIDDEN CSS"
|
|
|
|
|
|
@djc_test
|
|
class TestExtensionViews:
|
|
@djc_test(components_settings={"extensions": [DummyExtension]})
|
|
def test_views(self):
|
|
client = Client()
|
|
|
|
# Check basic view
|
|
response = client.get("/components/ext/test_extension/dummy-view/")
|
|
assert response.status_code == 200
|
|
assert response.content == b"Hello, world!"
|
|
|
|
# Check that URL parameters are passed to the view
|
|
response2 = client.get("/components/ext/test_extension/dummy-view-2/123/John/")
|
|
assert response2.status_code == 200
|
|
assert response2.content == b"Hello, world! 123 John"
|
|
|
|
@djc_test(components_settings={"extensions": [DummyNestedExtension]})
|
|
def test_nested_views(self):
|
|
client = Client()
|
|
|
|
# Check basic view
|
|
# NOTE: Since the parent route contains child routes, the parent route should not be matched
|
|
response = client.get("/components/ext/test_nested_extension/nested-view/")
|
|
assert response.status_code == 404
|
|
|
|
# Check that URL parameters are passed to the view
|
|
response2 = client.get("/components/ext/test_nested_extension/nested-view/123/John/")
|
|
assert response2.status_code == 200
|
|
assert response2.content == b"Hello, world! 123 John"
|
|
|
|
|
|
@djc_test
|
|
class TestExtensionDefaults:
|
|
@djc_test(
|
|
components_settings={
|
|
"extensions": [DummyExtension],
|
|
"extensions_defaults": {
|
|
"test_extension": {},
|
|
},
|
|
}
|
|
)
|
|
def test_no_defaults(self):
|
|
class TestComponent(Component):
|
|
template = "Hello"
|
|
|
|
dummy_ext_cls: DummyExtension.ComponentConfig = TestComponent.TestExtension # type: ignore[attr-defined]
|
|
assert dummy_ext_cls.foo == "1"
|
|
assert dummy_ext_cls.bar == "2"
|
|
assert dummy_ext_cls.baz() == "3"
|
|
|
|
@djc_test(
|
|
components_settings={
|
|
"extensions": [DummyExtension],
|
|
"extensions_defaults": {
|
|
"test_extension": {
|
|
"foo": "NEW_FOO",
|
|
"baz": classmethod(lambda self: "OVERRIDEN"),
|
|
},
|
|
"nonexistent": {
|
|
"1": "2",
|
|
},
|
|
},
|
|
}
|
|
)
|
|
def test_defaults(self):
|
|
class TestComponent(Component):
|
|
template = "Hello"
|
|
|
|
dummy_ext_cls: DummyExtension.ComponentConfig = TestComponent.TestExtension # type: ignore[attr-defined]
|
|
assert dummy_ext_cls.foo == "NEW_FOO"
|
|
assert dummy_ext_cls.bar == "2"
|
|
assert dummy_ext_cls.baz() == "OVERRIDEN"
|
|
|
|
|
|
@djc_test
|
|
class TestLegacyApi:
|
|
# TODO_V1 - Remove
|
|
@djc_test(
|
|
components_settings={
|
|
"extensions": [LegacyExtension],
|
|
}
|
|
)
|
|
def test_extension_class(self):
|
|
class TestComponent(Component):
|
|
template = "Hello"
|
|
|
|
dummy_ext_cls: LegacyExtension.ExtensionClass = TestComponent.Legacy # type: ignore[attr-defined]
|
|
assert dummy_ext_cls.foo == "1"
|
|
assert dummy_ext_cls.bar == "2"
|
|
assert dummy_ext_cls.baz() == "3"
|