from django.template import Context, Template from django_components import Component, registry, types from .django_test_setup import setup_test_config from .testutils import BaseTestCase, parametrize_context_behavior setup_test_config({"autodiscover": False}) ######################### # COMPONENTS ######################### class SimpleComponent(Component): template: types.django_html = """ Variable: {{ variable }} """ def get_context_data(self, variable=None): return {"variable": variable} if variable is not None else {} @staticmethod def expected_output(variable_value): return "Variable: < strong > {} < / strong >".format(variable_value) class VariableDisplay(Component): template: types.django_html = """ {% load component_tags %}

Shadowing variable = {{ shadowing_variable }}

Uniquely named variable = {{ unique_variable }}

""" def get_context_data(self, shadowing_variable=None, new_variable=None): context = {} if shadowing_variable is not None: context["shadowing_variable"] = shadowing_variable if new_variable is not None: context["unique_variable"] = new_variable return context class IncrementerComponent(Component): template: types.django_html = """ {% load component_tags %}

value={{ value }};calls={{ calls }}

{% slot 'content' %}{% endslot %} """ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.call_count = 0 def get_context_data(self, value=0): value = int(value) if hasattr(self, "call_count"): self.call_count += 1 else: self.call_count = 1 return {"value": value + 1, "calls": self.call_count} ######################### # TESTS ######################### class ContextTests(BaseTestCase): class ParentComponent(Component): template: types.django_html = """ {% load component_tags %}

Parent content

{% component name="variable_display" shadowing_variable='override' new_variable='unique_val' %} {% endcomponent %}
{% slot 'content' %}

Slot content

{% component name="variable_display" shadowing_variable='slot_default_override' new_variable='slot_default_unique' %} {% endcomponent %} {% endslot %}
""" # noqa def get_context_data(self): return {"shadowing_variable": "NOT SHADOWED"} def setUp(self): super().setUp() registry.register(name="variable_display", component=VariableDisplay) registry.register(name="parent_component", component=self.ParentComponent) @parametrize_context_behavior(["django", "isolated"]) def test_nested_component_context_shadows_parent_with_unfilled_slots_and_component_tag( self, ): template_str: types.django_html = """ {% load component_tags %}{% component_dependencies %} {% component 'parent_component' %}{% endcomponent %} """ template = Template(template_str) rendered = template.render(Context()) self.assertIn("

Shadowing variable = override

", rendered, rendered) self.assertIn( "

Shadowing variable = slot_default_override

", rendered, rendered, ) self.assertNotIn("

Shadowing variable = NOT SHADOWED

", rendered, rendered) @parametrize_context_behavior(["django", "isolated"]) def test_nested_component_instances_have_unique_context_with_unfilled_slots_and_component_tag( self, ): template_str: types.django_html = """ {% load component_tags %} {% component_dependencies %} {% component name='parent_component' %}{% endcomponent %} """ template = Template(template_str) rendered = template.render(Context()) self.assertIn("

Uniquely named variable = unique_val

", rendered, rendered) self.assertIn( "

Uniquely named variable = slot_default_unique

", rendered, rendered, ) @parametrize_context_behavior(["django", "isolated"]) def test_nested_component_context_shadows_parent_with_filled_slots(self): template_str: types.django_html = """ {% load component_tags %}{% component_dependencies %} {% component 'parent_component' %} {% fill 'content' %} {% component name='variable_display' shadowing_variable='shadow_from_slot' new_variable='unique_from_slot' %} {% endcomponent %} {% endfill %} {% endcomponent %} """ # NOQA template = Template(template_str) rendered = template.render(Context()) self.assertIn("

Shadowing variable = override

", rendered, rendered) self.assertIn( "

Shadowing variable = shadow_from_slot

", rendered, rendered, ) self.assertNotIn("

Shadowing variable = NOT SHADOWED

", rendered, rendered) @parametrize_context_behavior(["django", "isolated"]) def test_nested_component_instances_have_unique_context_with_filled_slots(self): template_str: types.django_html = """ {% load component_tags %} {% component_dependencies %} {% component 'parent_component' %} {% fill 'content' %} {% component name='variable_display' shadowing_variable='shadow_from_slot' new_variable='unique_from_slot' %} {% endcomponent %} {% endfill %} {% endcomponent %} """ # NOQA template = Template(template_str) rendered = template.render(Context()) self.assertIn("

Uniquely named variable = unique_val

", rendered, rendered) self.assertIn( "

Uniquely named variable = unique_from_slot

", rendered, rendered, ) @parametrize_context_behavior(["django", "isolated"]) def test_nested_component_context_shadows_outer_context_with_unfilled_slots_and_component_tag( self, ): template_str: types.django_html = """ {% load component_tags %} {% component_dependencies %} {% component name='parent_component' %}{% endcomponent %} """ template = Template(template_str) rendered = template.render(Context({"shadowing_variable": "NOT SHADOWED"})) self.assertIn("

Shadowing variable = override

", rendered, rendered) self.assertIn( "

Shadowing variable = slot_default_override

", rendered, rendered, ) self.assertNotIn("

Shadowing variable = NOT SHADOWED

", rendered, rendered) @parametrize_context_behavior(["django", "isolated"]) def test_nested_component_context_shadows_outer_context_with_filled_slots( self, ): template_str: types.django_html = """ {% load component_tags %}{% component_dependencies %} {% component 'parent_component' %} {% fill 'content' %} {% component name='variable_display' shadowing_variable='shadow_from_slot' new_variable='unique_from_slot' %} {% endcomponent %} {% endfill %} {% endcomponent %} """ # NOQA template = Template(template_str) rendered = template.render(Context({"shadowing_variable": "NOT SHADOWED"})) self.assertIn("

Shadowing variable = override

", rendered, rendered) self.assertIn( "

Shadowing variable = shadow_from_slot

", rendered, rendered, ) self.assertNotIn("

Shadowing variable = NOT SHADOWED

", rendered, rendered) class ParentArgsTests(BaseTestCase): class ParentComponentWithArgs(Component): template: types.django_html = """ {% load component_tags %}

Parent content

{% component name="variable_display" shadowing_variable=inner_parent_value new_variable='unique_val' %} {% endcomponent %}
{% slot 'content' %}

Slot content

{% component name="variable_display" shadowing_variable='slot_default_override' new_variable=inner_parent_value %} {% endcomponent %} {% endslot %}
""" # noqa def get_context_data(self, parent_value): return {"inner_parent_value": parent_value} def setUp(self): super().setUp() registry.register(name="incrementer", component=IncrementerComponent) registry.register(name="parent_with_args", component=self.ParentComponentWithArgs) registry.register(name="variable_display", component=VariableDisplay) @parametrize_context_behavior(["django", "isolated"]) def test_parent_args_can_be_drawn_from_context(self): template_str: types.django_html = """ {% load component_tags %}{% component_dependencies %} {% component 'parent_with_args' parent_value=parent_value %} {% endcomponent %} """ template = Template(template_str) rendered = template.render(Context({"parent_value": "passed_in"})) self.assertHTMLEqual( rendered, """

Parent content

Shadowing variable = passed_in

Uniquely named variable = unique_val

Slot content

Shadowing variable = slot_default_override

Uniquely named variable = passed_in

""", ) @parametrize_context_behavior(["django", "isolated"]) def test_parent_args_available_outside_slots(self): template_str: types.django_html = """ {% load component_tags %}{% component_dependencies %} {% component 'parent_with_args' parent_value='passed_in' %}{%endcomponent %} """ template = Template(template_str) rendered = template.render(Context()) self.assertIn("

Shadowing variable = passed_in

", rendered, rendered) self.assertIn("

Uniquely named variable = passed_in

", rendered, rendered) self.assertNotIn("

Shadowing variable = NOT SHADOWED

", rendered, rendered) # NOTE: Second arg in tuple are expected values passed through components. @parametrize_context_behavior( [ ("django", ("passed_in", "passed_in")), ("isolated", ("passed_in", "")), ] ) def test_parent_args_available_in_slots(self, context_behavior_data): first_val, second_val = context_behavior_data template_str: types.django_html = """ {% load component_tags %}{% component_dependencies %} {% component 'parent_with_args' parent_value='passed_in' %} {% fill 'content' %} {% component name='variable_display' shadowing_variable='value_from_slot' new_variable=inner_parent_value %} {% endcomponent %} {% endfill %} {% endcomponent %} """ # noqa: E501 template = Template(template_str) rendered = template.render(Context()) self.assertHTMLEqual( rendered, f"""

Parent content

Shadowing variable = {first_val}

Uniquely named variable = unique_val

Shadowing variable = value_from_slot

Uniquely named variable = {second_val}

""", ) class ContextCalledOnceTests(BaseTestCase): def setUp(self): super().setUp() registry.register(name="incrementer", component=IncrementerComponent) @parametrize_context_behavior(["django", "isolated"]) def test_one_context_call_with_simple_component(self): template_str: types.django_html = """ {% load component_tags %}{% component_dependencies %} {% component name='incrementer' %}{% endcomponent %} """ template = Template(template_str) rendered = template.render(Context()).strip().replace("\n", "") self.assertHTMLEqual( rendered, '

value=1;calls=1

', ) @parametrize_context_behavior(["django", "isolated"]) def test_one_context_call_with_simple_component_and_arg(self): template_str: types.django_html = """ {% load component_tags %} {% component name='incrementer' value='2' %}{% endcomponent %} """ template = Template(template_str) rendered = template.render(Context()).strip() self.assertHTMLEqual(rendered, '

value=3;calls=1

', rendered) @parametrize_context_behavior(["django", "isolated"]) def test_one_context_call_with_component(self): template_str: types.django_html = """ {% load component_tags %} {% component 'incrementer' %}{% endcomponent %} """ template = Template(template_str) rendered = template.render(Context()).strip() self.assertHTMLEqual(rendered, '

value=1;calls=1

', rendered) @parametrize_context_behavior(["django", "isolated"]) def test_one_context_call_with_component_and_arg(self): template_str: types.django_html = """ {% load component_tags %} {% component 'incrementer' value='3' %}{% endcomponent %} """ template = Template(template_str) rendered = template.render(Context()).strip() self.assertHTMLEqual(rendered, '

value=4;calls=1

', rendered) @parametrize_context_behavior(["django", "isolated"]) def test_one_context_call_with_slot(self): template_str: types.django_html = """ {% load component_tags %} {% component 'incrementer' %} {% fill 'content' %}

slot

{% endfill %} {% endcomponent %} """ template = Template(template_str) rendered = template.render(Context()).strip() self.assertHTMLEqual( rendered, '

value=1;calls=1

\n

slot

', rendered, ) @parametrize_context_behavior(["django", "isolated"]) def test_one_context_call_with_slot_and_arg(self): template_str: types.django_html = """ {% load component_tags %} {% component 'incrementer' value='3' %} {% fill 'content' %}

slot

{% endfill %} {% endcomponent %} """ template = Template(template_str) rendered = template.render(Context()).strip() self.assertHTMLEqual( rendered, '

value=4;calls=1

\n

slot

', rendered, ) class ComponentsCanAccessOuterContext(BaseTestCase): def setUp(self): super().setUp() registry.register(name="simple_component", component=SimpleComponent) # NOTE: Second arg in tuple is expected value. @parametrize_context_behavior( [ ("django", "outer_value"), ("isolated", ""), ] ) def test_simple_component_can_use_outer_context(self, context_behavior_data): template_str: types.django_html = """ {% load component_tags %}{% component_dependencies %} {% component 'simple_component' %}{% endcomponent %} """ template = Template(template_str) rendered = template.render(Context({"variable": "outer_value"})) self.assertHTMLEqual( rendered, f""" Variable: {context_behavior_data} """, ) class IsolatedContextTests(BaseTestCase): def setUp(self): super().setUp() registry.register(name="simple_component", component=SimpleComponent) @parametrize_context_behavior(["django", "isolated"]) def test_simple_component_can_pass_outer_context_in_args(self): template_str: types.django_html = """ {% load component_tags %}{% component_dependencies %} {% component 'simple_component' variable only %}{% endcomponent %} """ template = Template(template_str) rendered = template.render(Context({"variable": "outer_value"})).strip() self.assertIn("outer_value", rendered, rendered) @parametrize_context_behavior(["django", "isolated"]) def test_simple_component_cannot_use_outer_context(self): template_str: types.django_html = """ {% load component_tags %}{% component_dependencies %} {% component 'simple_component' only %}{% endcomponent %} """ template = Template(template_str) rendered = template.render(Context({"variable": "outer_value"})).strip() self.assertNotIn("outer_value", rendered, rendered) class IsolatedContextSettingTests(BaseTestCase): def setUp(self): super().setUp() registry.register(name="simple_component", component=SimpleComponent) @parametrize_context_behavior(["isolated"]) def test_component_tag_includes_variable_with_isolated_context_from_settings( self, ): template_str: types.django_html = """ {% load component_tags %}{% component_dependencies %} {% component 'simple_component' variable %}{% endcomponent %} """ template = Template(template_str) rendered = template.render(Context({"variable": "outer_value"})) self.assertIn("outer_value", rendered, rendered) @parametrize_context_behavior(["isolated"]) def test_component_tag_excludes_variable_with_isolated_context_from_settings( self, ): template_str: types.django_html = """ {% load component_tags %}{% component_dependencies %} {% component 'simple_component' %}{% endcomponent %} """ template = Template(template_str) rendered = template.render(Context({"variable": "outer_value"})) self.assertNotIn("outer_value", rendered, rendered) @parametrize_context_behavior(["isolated"]) def test_component_includes_variable_with_isolated_context_from_settings( self, ): template_str: types.django_html = """ {% load component_tags %}{% component_dependencies %} {% component 'simple_component' variable %} {% endcomponent %} """ template = Template(template_str) rendered = template.render(Context({"variable": "outer_value"})) self.assertIn("outer_value", rendered, rendered) @parametrize_context_behavior(["isolated"]) def test_component_excludes_variable_with_isolated_context_from_settings( self, ): template_str: types.django_html = """ {% load component_tags %}{% component_dependencies %} {% component 'simple_component' %} {% endcomponent %} """ template = Template(template_str) rendered = template.render(Context({"variable": "outer_value"})) self.assertNotIn("outer_value", rendered, rendered) class OuterContextPropertyTests(BaseTestCase): class OuterContextComponent(Component): template: types.django_html = """ Variable: {{ variable }} """ def get_context_data(self): return self.outer_context.flatten() def setUp(self): super().setUp() registry.register(name="outer_context_component", component=self.OuterContextComponent) @parametrize_context_behavior(["django", "isolated"]) def test_outer_context_property_with_component(self): template_str: types.django_html = """ {% load component_tags %}{% component_dependencies %} {% component 'outer_context_component' only %}{% endcomponent %} """ template = Template(template_str) rendered = template.render(Context({"variable": "outer_value"})).strip() self.assertIn("outer_value", rendered, rendered) class ContextVarsIsFilledTests(BaseTestCase): class IsFilledVarsComponent(Component): template: types.django_html = """ {% load component_tags %}
{% slot "title" default %}{% endslot %} {% slot "my_title" %}{% endslot %} {% slot "my title 1" %}{% endslot %} {% slot "my-title-2" %}{% endslot %} {% slot "escape this: #$%^*()" %}{% endslot %} {{ component_vars.is_filled|safe }}
""" class ComponentWithConditionalSlots(Component): template: types.django_html = """ {# Example from django-components/issues/98 #} {% load component_tags %}
{% slot "title" %}Title{% endslot %}
{% if component_vars.is_filled.subtitle %}
{% slot "subtitle" %}Optional subtitle {% endslot %}
{% endif %}
""" class ComponentWithComplexConditionalSlots(Component): template: types.django_html = """ {# Example from django-components/issues/98 #} {% load component_tags %}
{% slot "title" %}Title{% endslot %}
{% if component_vars.is_filled.subtitle %}
{% slot "subtitle" %}Optional subtitle{% endslot %}
{% elif component_vars.is_filled.alt_subtitle %}
{% slot "alt_subtitle" %}Why would you want this?{% endslot %}
{% else %}
Nothing filled!
{% endif %}
""" class ComponentWithNegatedConditionalSlot(Component): template: types.django_html = """ {# Example from django-components/issues/98 #} {% load component_tags %}
{% slot "title" %}Title{% endslot %}
{% if not component_vars.is_filled.subtitle %}
Subtitle not filled!
{% else %}
{% slot "alt_subtitle" %}Why would you want this?{% endslot %}
{% endif %}
""" def setUp(self) -> None: super().setUp() registry.register("is_filled_vars", self.IsFilledVarsComponent) registry.register("conditional_slots", self.ComponentWithConditionalSlots) registry.register( "complex_conditional_slots", self.ComponentWithComplexConditionalSlots, ) registry.register("negated_conditional_slot", self.ComponentWithNegatedConditionalSlot) @parametrize_context_behavior(["django", "isolated"]) def test_is_filled_vars(self): template: types.django_html = """ {% load component_tags %} {% component "is_filled_vars" %} {% fill "title" %}{% endfill %} {% fill "my-title-2" %}{% endfill %} {% fill "escape this: #$%^*()" %}{% endfill %} {% endcomponent %} """ rendered = Template(template).render(Context()) expected = """
{'title': True, 'my_title': False, 'my_title_1': False, 'my_title_2': True, 'escape_this_________': True}
""" self.assertHTMLEqual(rendered, expected) @parametrize_context_behavior(["django", "isolated"]) def test_is_filled_vars_default(self): template: types.django_html = """ {% load component_tags %} {% component "is_filled_vars" %} bla bla {% endcomponent %} """ rendered = Template(template).render(Context()) expected = """
bla bla {'title': True, 'my_title': False, 'my_title_1': False, 'my_title_2': False, 'escape_this_________': False}
""" self.assertHTMLEqual(rendered, expected) @parametrize_context_behavior(["django", "isolated"]) def test_simple_component_with_conditional_slot(self): template: types.django_html = """ {% load component_tags %} {% component "conditional_slots" %}{% endcomponent %} """ expected = """
Title
""" rendered = Template(template).render(Context({})) self.assertHTMLEqual(rendered, expected) @parametrize_context_behavior(["django", "isolated"]) def test_component_with_filled_conditional_slot(self): template: types.django_html = """ {% load component_tags %} {% component "conditional_slots" %} {% fill "subtitle" %} My subtitle {% endfill %} {% endcomponent %} """ expected = """
Title
My subtitle
""" rendered = Template(template).render(Context({})) self.assertHTMLEqual(rendered, expected) @parametrize_context_behavior(["django", "isolated"]) def test_elif_of_complex_conditional_slots(self): template: types.django_html = """ {% load component_tags %} {% component "complex_conditional_slots" %} {% fill "alt_subtitle" %} A different subtitle {% endfill %} {% endcomponent %} """ expected = """
Title
A different subtitle
""" rendered = Template(template).render(Context({})) self.assertHTMLEqual(rendered, expected) @parametrize_context_behavior(["django", "isolated"]) def test_else_of_complex_conditional_slots(self): template: types.django_html = """ {% load component_tags %} {% component "complex_conditional_slots" %} {% endcomponent %} """ expected = """
Title
Nothing filled!
""" rendered = Template(template).render(Context({})) self.assertHTMLEqual(rendered, expected) @parametrize_context_behavior(["django", "isolated"]) def test_component_with_negated_conditional_slot(self): template: types.django_html = """ {% load component_tags %} {% component "negated_conditional_slot" %} {# Whoops! Forgot to fill a slot! #} {% endcomponent %} """ expected = """
Title
Subtitle not filled!
""" rendered = Template(template).render(Context({})) self.assertHTMLEqual(rendered, expected)