import sys from pathlib import Path from typing import Any, Dict, List, Optional from django.core.exceptions import ImproperlyConfigured from django.template import Context, Template from django.test import override_settings # isort: off from .django_test_setup import * # NOQA from .testutils import BaseTestCase, autodiscover_with_cleanup # isort: on from django_components import component ######################### # COMPONENTS ######################### class ParentComponent(component.Component): template_name = "parent_template.html" def get_context_data(self): return {"shadowing_variable": "NOT SHADOWED"} class VariableDisplay(component.Component): template_name = "variable_display.html" 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 DuplicateSlotComponent(component.Component): template_name = "template_with_nonunique_slots.html" def get_context_data(self, name: Optional[str] = None) -> Dict[str, Any]: return { "name": name, } class DuplicateSlotNestedComponent(component.Component): template_name = "template_with_nonunique_slots_nested.html" def get_context_data(self, items: List) -> Dict[str, Any]: return { "items": items, } class CalendarComponent(component.Component): """Nested in ComponentWithNestedComponent""" template_name = "slotted_component_nesting_template_pt1_calendar.html" ######################### # TESTS ######################### class ComponentTest(BaseTestCase): @classmethod def setUpClass(cls): super().setUpClass() component.registry.register(name="parent_component", component=ParentComponent) component.registry.register(name="variable_display", component=VariableDisplay) def test_empty_component(self): class EmptyComponent(component.Component): pass with self.assertRaises(ImproperlyConfigured): EmptyComponent("empty_component").get_template(Context({})) def test_simple_component(self): class SimpleComponent(component.Component): template_name = "simple_template.html" def get_context_data(self, variable=None): return { "variable": variable, } class Media: css = "style.css" js = "script.js" comp = SimpleComponent("simple_component") context = Context(comp.get_context_data(variable="test")) self.assertHTMLEqual( comp.render_dependencies(), """ """, ) self.assertHTMLEqual( comp.render(context), """ Variable: test """, ) def test_css_only_component(self): class SimpleComponent(component.Component): template_name = "simple_template.html" class Media: css = "style.css" comp = SimpleComponent("simple_component") self.assertHTMLEqual( comp.render_dependencies(), """ """, ) def test_js_only_component(self): class SimpleComponent(component.Component): template_name = "simple_template.html" class Media: js = "script.js" comp = SimpleComponent("simple_component") self.assertHTMLEqual( comp.render_dependencies(), """ """, ) def test_empty_media_component(self): class SimpleComponent(component.Component): template_name = "simple_template.html" class Media: pass comp = SimpleComponent("simple_component") self.assertHTMLEqual(comp.render_dependencies(), "") def test_missing_media_component(self): class SimpleComponent(component.Component): template_name = "simple_template.html" comp = SimpleComponent("simple_component") self.assertHTMLEqual(comp.render_dependencies(), "") def test_component_with_list_of_styles(self): class MultistyleComponent(component.Component): class Media: css = ["style.css", "style2.css"] js = ["script.js", "script2.js"] comp = MultistyleComponent("multistyle_component") self.assertHTMLEqual( comp.render_dependencies(), """ """, ) def test_component_with_filtered_template(self): class FilteredComponent(component.Component): template_name = "filtered_template.html" def get_context_data(self, var1=None, var2=None): return { "var1": var1, "var2": var2, } comp = FilteredComponent("filtered_component") context = Context(comp.get_context_data(var1="test1", var2="test2")) self.assertHTMLEqual( comp.render(context), """ Var1: test1 Var2 (uppercased): TEST2 """, ) def test_component_with_dynamic_template(self): class SvgComponent(component.Component): def get_context_data(self, name, css_class="", title="", **attrs): return { "name": name, "css_class": css_class, "title": title, **attrs, } def get_template_name(self, context): return f"svg_{context['name']}.svg" comp = SvgComponent("svg_component") self.assertHTMLEqual( comp.render(Context(comp.get_context_data(name="dynamic1"))), """ Dynamic1 """, ) self.assertHTMLEqual( comp.render(Context(comp.get_context_data(name="dynamic2"))), """ Dynamic2 """, ) # Settings required for autodiscover to work @override_settings( BASE_DIR=Path(__file__).resolve().parent, STATICFILES_DIRS=[ Path(__file__).resolve().parent / "components", ], ) def test_component_with_relative_paths_as_subcomponent(self): # Ensure that the module is executed again after import in autodiscovery if "tests.components.relative_file.relative_file" in sys.modules: del sys.modules["tests.components.relative_file.relative_file"] # Fix the paths, since the "components" dir is nested with autodiscover_with_cleanup(map_import_paths=lambda p: f"tests.{p}"): template = Template( """ {% load component_tags %}{% component_dependencies %} {% component 'parent_component' %} {% fill 'content' %} {% component name='relative_file_component' variable='hello' %} {% endcomponent %} {% endfill %} {% endcomponent %} """ # NOQA ) rendered = template.render(Context({})) self.assertIn('', rendered, rendered) def test_component_inside_slot(self): class SlottedComponent(component.Component): template_name = "slotted_template.html" def get_context_data(self, name: Optional[str] = None) -> Dict[str, Any]: return { "name": name, } component.registry.register("test", SlottedComponent) self.template = Template( """ {% load component_tags %} {% component "test" name='Igor' %} {% fill "header" %} Name: {{ name }} {% endfill %} {% fill "main" %} Day: {{ day }} {% endfill %} {% fill "footer" %} {% component "test" name='Joe2' %} {% fill "header" %} Name2: {{ name }} {% endfill %} {% fill "main" %} Day2: {{ day }} {% endfill %} {% endcomponent %} {% endfill %} {% endcomponent %} """ ) # {{ name }} should be "Jannete" everywhere rendered = self.template.render(Context({"day": "Monday", "name": "Jannete"})) self.assertHTMLEqual( rendered, """
Name: Jannete
Day: Monday
""", ) def test_fill_inside_fill_with_same_name(self): class SlottedComponent(component.Component): template_name = "slotted_template.html" def get_context_data(self, name: Optional[str] = None) -> Dict[str, Any]: return { "name": name, } component.registry.register("test", SlottedComponent) self.template = Template( """ {% load component_tags %} {% component "test" name='Igor' %} {% fill "header" %} {% component "test" name='Joe2' %} {% fill "header" %} Name2: {{ name }} {% endfill %} {% fill "main" %} Day2: {{ day }} {% endfill %} {% fill "footer" %} XYZ {% endfill %} {% endcomponent %} {% endfill %} {% fill "footer" %} WWW {% endfill %} {% endcomponent %} """ ) # {{ name }} should be "Jannete" everywhere rendered = self.template.render(Context({"day": "Monday", "name": "Jannete"})) self.assertHTMLEqual( rendered, """
Name2: Jannete
Day2: Monday
XYZ
Default main
""", ) @override_settings( COMPONENTS={ "context_behavior": "isolated", "slot_context_behavior": "isolated", }, ) def test_slots_of_top_level_comps_can_access_full_outer_ctx(self): class SlottedComponent(component.Component): template_name = "template_with_default_slot.html" def get_context_data(self, name: Optional[str] = None) -> Dict[str, Any]: return { "name": name, } component.registry.register("test", SlottedComponent) self.template = Template( """ {% load component_tags %} {% component "test" %} ABC: {{ name }} {% endcomponent %} """ ) nested_ctx = Context() nested_ctx.push({"some": "var"}) # <-- Nested comp's take data only from this layer nested_ctx.push({"name": "carl"}) # <-- But for top-level comp, it should access this layer too rendered = self.template.render(nested_ctx) self.assertHTMLEqual( rendered, """
ABC: carl
""", ) class DuplicateSlotTest(BaseTestCase): @classmethod def setUpClass(cls): super().setUpClass() component.registry.register(name="duplicate_slot", component=DuplicateSlotComponent) component.registry.register(name="duplicate_slot_nested", component=DuplicateSlotNestedComponent) component.registry.register(name="calendar", component=CalendarComponent) def test_duplicate_slots(self): self.template = Template( """ {% load component_tags %} {% component "duplicate_slot" %} {% fill "header" %} Name: {{ name }} {% endfill %} {% fill "footer" %} Hello {% endfill %} {% endcomponent %} """ ) rendered = self.template.render(Context({"name": "Jannete"})) self.assertHTMLEqual( rendered, """
Name: Jannete
Name: Jannete
""", ) def test_duplicate_slots_fallback(self): self.template = Template( """ {% load component_tags %} {% component "duplicate_slot" %} {% endcomponent %} """ ) rendered = self.template.render(Context({})) # NOTE: Slots should have different fallbacks even though they use the same name self.assertHTMLEqual( rendered, """
Default header
Default main header
""", ) def test_duplicate_slots_nested(self): self.template = Template( """ {% load component_tags %} {% component "duplicate_slot_nested" items=items %} {% fill "header" %} OVERRIDDEN! {% endfill %} {% endcomponent %} """ ) rendered = self.template.render(Context({"items": [1, 2, 3]})) # NOTE: Slots should have different fallbacks even though they use the same name self.assertHTMLEqual( rendered, """ OVERRIDDEN!

OVERRIDDEN!

Here are your to-do items for today:
  1. 1
  2. OVERRIDDEN!
  3. 2
  4. OVERRIDDEN!
  5. 3
  6. OVERRIDDEN!
""", ) def test_duplicate_slots_nested_fallback(self): self.template = Template( """ {% load component_tags %} {% component "duplicate_slot_nested" items=items %} {% endcomponent %} """ ) rendered = self.template.render(Context({"items": [1, 2, 3]})) # NOTE: Slots should have different fallbacks even though they use the same name self.assertHTMLEqual( rendered, """ START

NESTED

Here are your to-do items for today:
  1. 1
  2. LOOP 1
  3. 2
  4. LOOP 2
  5. 3
  6. LOOP 3
""", ) class InlineComponentTest(BaseTestCase): def test_inline_html_component(self): class InlineHTMLComponent(component.Component): template = "
Hello Inline
" comp = InlineHTMLComponent("inline_html_component") self.assertHTMLEqual( comp.render(Context({})), "
Hello Inline
", ) def test_html_and_css_only(self): class HTMLCSSComponent(component.Component): template = "
Content
" css = ".html-css-only { color: blue; }" comp = HTMLCSSComponent("html_css_component") self.assertHTMLEqual( comp.render(Context({})), "
Content
", ) self.assertHTMLEqual( comp.render_css_dependencies(), "", ) def test_html_and_js_only(self): class HTMLJSComponent(component.Component): template = "
Content
" js = "console.log('HTML and JS only');" comp = HTMLJSComponent("html_js_component") self.assertHTMLEqual( comp.render(Context({})), "
Content
", ) self.assertHTMLEqual( comp.render_js_dependencies(), "", ) def test_html_string_with_css_js_files(self): class HTMLStringFileCSSJSComponent(component.Component): template = "
Content
" class Media: css = "path/to/style.css" js = "path/to/script.js" comp = HTMLStringFileCSSJSComponent("html_string_file_css_js_component") self.assertHTMLEqual( comp.render(Context({})), "
Content
", ) self.assertHTMLEqual( comp.render_dependencies(), """ """, ) def test_html_js_string_with_css_file(self): class HTMLStringFileCSSJSComponent(component.Component): template = "
Content
" js = "console.log('HTML and JS only');" class Media: css = "path/to/style.css" comp = HTMLStringFileCSSJSComponent("html_string_file_css_js_component") self.assertHTMLEqual( comp.render(Context({})), "
Content
", ) self.assertHTMLEqual( comp.render_dependencies(), """ """, ) def test_html_css_string_with_js_file(self): class HTMLStringFileCSSJSComponent(component.Component): template = "
Content
" css = ".html-string-file { color: blue; }" class Media: js = "path/to/script.js" comp = HTMLStringFileCSSJSComponent("html_string_file_css_js_component") self.assertHTMLEqual( comp.render(Context({})), "
Content
", ) self.assertHTMLEqual( comp.render_dependencies(), """ """, ) def test_component_with_variable_in_html(self): class VariableHTMLComponent(component.Component): def get_template(self, context): return Template("
{{ variable }}
") comp = VariableHTMLComponent("variable_html_component") context = Context({"variable": "Dynamic Content"}) self.assertHTMLEqual( comp.render(context), "
Dynamic Content
", ) class ComponentMediaTests(BaseTestCase): def test_component_media_with_strings(self): class SimpleComponent(component.Component): class Media: css = "path/to/style.css" js = "path/to/script.js" comp = SimpleComponent("") self.assertHTMLEqual( comp.render_dependencies(), """ """, ) def test_component_media_with_lists(self): class SimpleComponent(component.Component): class Media: css = ["path/to/style.css", "path/to/style2.css"] js = ["path/to/script.js"] comp = SimpleComponent("") self.assertHTMLEqual( comp.render_dependencies(), """ """, ) def test_component_media_with_dict_and_list(self): class SimpleComponent(component.Component): class Media: css = { "all": "path/to/style.css", "print": ["path/to/style2.css"], "screen": "path/to/style3.css", } js = ["path/to/script.js"] comp = SimpleComponent("") self.assertHTMLEqual( comp.render_dependencies(), """ """, ) def test_component_media_with_dict_with_list_and_list(self): class SimpleComponent(component.Component): class Media: css = {"all": ["path/to/style.css"]} js = ["path/to/script.js"] comp = SimpleComponent("") self.assertHTMLEqual( comp.render_dependencies(), """ """, ) # Settings required for autodiscover to work @override_settings( BASE_DIR=Path(__file__).resolve().parent, STATICFILES_DIRS=[ Path(__file__).resolve().parent / "components", ], ) def test_component_media_with_dict_with_relative_paths(self): # Fix the paths, since the "components" dir is nested with autodiscover_with_cleanup(map_import_paths=lambda p: f"tests.{p}"): template = Template( """ {% load component_tags %}{% component_dependencies %} {% component name='relative_file_component' variable=variable %} {% endcomponent %} """ # NOQA ) rendered = template.render(Context({"variable": "test"})) self.assertHTMLEqual( rendered, """
""", ) class ComponentIsolationTests(BaseTestCase): def setUp(self): class SlottedComponent(component.Component): template_name = "slotted_template.html" component.registry.register("test", SlottedComponent) def test_instances_of_component_do_not_share_slots(self): template = Template( """ {% load component_tags %} {% component "test" %} {% fill "header" %}Override header{% endfill %} {% endcomponent %} {% component "test" %} {% fill "main" %}Override main{% endfill %} {% endcomponent %} {% component "test" %} {% fill "footer" %}Override footer{% endfill %} {% endcomponent %} """ ) template.render(Context({})) rendered = template.render(Context({})) self.assertHTMLEqual( rendered, """
Override header
Default main
Default header
Override main
Default header
Default main
""", ) class SlotBehaviorTests(BaseTestCase): def setUp(self): class SlottedComponent(component.Component): template_name = "slotted_template.html" def get_context_data(self, name: Optional[str] = None) -> Dict[str, Any]: return { "name": name, } component.registry.register("test", SlottedComponent) self.template = Template( """ {% load component_tags %} {% component "test" name='Igor' %} {% fill "header" %} Name: {{ name }} {% endfill %} {% fill "main" %} Day: {{ day }} {% endfill %} {% fill "footer" %} {% component "test" name='Joe2' %} {% fill "header" %} Name2: {{ name }} {% endfill %} {% fill "main" %} Day2: {{ day }} {% endfill %} {% endcomponent %} {% endfill %} {% endcomponent %} """ ) @override_settings( COMPONENTS={"slot_context_behavior": "allow_override"}, ) def test_slot_context_allow_override(self): # {{ name }} should be neither Jannete not empty, because overriden everywhere rendered = self.template.render(Context({"day": "Monday", "name": "Jannete"})) self.assertHTMLEqual( rendered, """
Name: Igor
Day: Monday
""", ) # {{ name }} should be effectively the same as before, because overriden everywhere rendered2 = self.template.render(Context({"day": "Monday"})) self.assertHTMLEqual(rendered2, rendered) @override_settings( COMPONENTS={"slot_context_behavior": "isolated"}, ) def test_slot_context_isolated(self): # {{ name }} should be "Jannete" everywhere rendered = self.template.render(Context({"day": "Monday", "name": "Jannete"})) self.assertHTMLEqual( rendered, """
Name: Jannete
Day: Monday
""", ) # {{ name }} should be empty everywhere rendered2 = self.template.render(Context({"day": "Monday"})) self.assertHTMLEqual( rendered2, """
Name:
Day: Monday
""", ) @override_settings( COMPONENTS={ "slot_context_behavior": "prefer_root", }, ) def test_slot_context_prefer_root(self): # {{ name }} should be "Jannete" everywhere rendered = self.template.render(Context({"day": "Monday", "name": "Jannete"})) self.assertHTMLEqual( rendered, """
Name: Jannete
Day: Monday
""", ) # {{ name }} should be neither "Jannete" nor empty anywhere rendered = self.template.render(Context({"day": "Monday"})) self.assertHTMLEqual( rendered, """
Name: Igor
Day: Monday
""", )