import os import re import sys from pathlib import Path from textwrap import dedent from typing import Optional import pytest from django.core.exceptions import ImproperlyConfigured from django.forms.widgets import Media from django.template import Context, Template from django.templatetags.static import static from django.utils.html import format_html, html_safe from django.utils.safestring import mark_safe from pytest_django.asserts import assertHTMLEqual, assertInHTML from django_components import Component, autodiscover, registry, render_dependencies, types from django_components.testing import djc_test from .testutils import setup_test_config setup_test_config({"autodiscover": False}) # "Main media" refer to the HTML, JS, and CSS set on the Component class itself # (as opposed via the `Media` class). These have special handling in the Component. @djc_test class TestMainMedia: def test_html_js_css_inlined(self): class TestComponent(Component): template = dedent( """ {% load component_tags %} {% component_js_dependencies %} {% component_css_dependencies %}
Content
""" ) css = ".html-css-only { color: blue; }" js = "console.log('HTML and JS only');" assert TestComponent.css == ".html-css-only { color: blue; }" assert TestComponent.js == "console.log('HTML and JS only');" rendered = TestComponent.render() assertInHTML( '
Content
', rendered, ) assertInHTML( "", rendered, ) assertInHTML( "", rendered, ) # Check that the HTML / JS / CSS can be accessed on the component class assert TestComponent.template == dedent( """ {% load component_tags %} {% component_js_dependencies %} {% component_css_dependencies %}
Content
""" ) assert TestComponent.css == ".html-css-only { color: blue; }" assert TestComponent.js == "console.log('HTML and JS only');" assert isinstance(TestComponent._template, Template) assert TestComponent._template.origin.component_cls is TestComponent @djc_test( django_settings={ "STATICFILES_DIRS": [ os.path.join(Path(__file__).resolve().parent, "static_root"), ], } ) def test_html_js_css_filepath_rel_to_component(self): from tests.test_app.components.app_lvl_comp.app_lvl_comp import AppLvlCompComponent class TestComponent(AppLvlCompComponent): pass registry.register("test", TestComponent) assert ".html-css-only {\n color: blue;\n}" in TestComponent.css # type: ignore[operator] assert 'console.log("JS file");' in TestComponent.js # type: ignore[operator] rendered = Template( """ {% load component_tags %} {% component_js_dependencies %} {% component_css_dependencies %} {% component "test" variable="test" / %} """ ).render(Context()) assertInHTML( """
""", rendered, ) assertInHTML( "", rendered, ) assertInHTML( '', rendered, ) # Check that the HTML / JS / CSS can be accessed on the component class assert TestComponent.template == ( '
\n' " {% csrf_token %}\n" ' \n' ' \n' "
\n" ) assert TestComponent.css == ".html-css-only {\n color: blue;\n}\n" assert TestComponent.js == 'console.log("JS file");\n' assert isinstance(TestComponent._template, Template) assert TestComponent._template.origin.component_cls is TestComponent @djc_test( django_settings={ "STATICFILES_DIRS": [ os.path.join(Path(__file__).resolve().parent, "static_root"), ], } ) def test_html_js_css_filepath_from_static(self): class TestComponent(Component): template_file = "test_app_simple_template.html" css_file = "style.css" js_file = "script.js" def get_template_data(self, args, kwargs, slots, context): return { "variable": kwargs["variable"], } registry.register("test", TestComponent) assert "Variable: {{ variable }}" in TestComponent.template # type: ignore[operator] assert ".html-css-only {\n color: blue;\n}" in TestComponent.css # type: ignore[operator] assert 'console.log("HTML and JS only");' in TestComponent.js # type: ignore[operator] assert isinstance(TestComponent._template, Template) assert TestComponent._template.origin.component_cls is TestComponent rendered = Template( """ {% load component_tags %} {% component_js_dependencies %} {% component_css_dependencies %} {% component "test" variable="test" / %} """ ).render(Context()) assert 'Variable: test' in rendered assertInHTML( "", rendered, ) assertInHTML( '', rendered, ) # Check that the HTML / JS / CSS can be accessed on the component class assert TestComponent.template == "Variable: {{ variable }}\n" assert TestComponent.css == ( "/* Used in `MainMediaTest` tests in `test_component_media.py` */\n" ".html-css-only {\n" " color: blue;\n" "}" ) assert TestComponent.js == ( '/* Used in `MainMediaTest` tests in `test_component_media.py` */\nconsole.log("HTML and JS only");\n' ) @djc_test( django_settings={ "STATICFILES_DIRS": [ os.path.join(Path(__file__).resolve().parent, "static_root"), ], } ) def test_html_js_css_filepath_lazy_loaded(self): from tests.test_app.components.app_lvl_comp.app_lvl_comp import AppLvlCompComponent class TestComponent(AppLvlCompComponent): pass # NOTE: Currently the components' JS/CSS are loaded eagerly, to make the JS/CSS # files available via endpoints. If that is no longer true, uncomment the # following lines to test the lazy loading of the CSS. # # # Since this is a subclass, actual CSS is defined on the parent class, and thus # # the corresponding ComponentMedia instance is also on the parent class. # assert AppLvlCompComponent._component_media.css is UNSET # type: ignore[attr-defined] # assert AppLvlCompComponent._component_media.css_file == "app_lvl_comp.css" # type: ignore[attr-defined] # assert AppLvlCompComponent._component_media._template is UNSET # type: ignore[attr-defined] # # # Access the property to load the CSS # _ = TestComponent.css assert AppLvlCompComponent._component_media.css == (".html-css-only {\n" " color: blue;\n" "}\n") # type: ignore[attr-defined] assert AppLvlCompComponent._component_media.css_file == "app_lvl_comp/app_lvl_comp.css" # type: ignore[attr-defined] # Also check JS and HTML while we're at it assert AppLvlCompComponent._component_media.template == ( # type: ignore[attr-defined] '
\n' " {% csrf_token %}\n" ' \n' ' \n' "
\n" ) assert AppLvlCompComponent._component_media.template_file == "app_lvl_comp/app_lvl_comp.html" # type: ignore[attr-defined] assert AppLvlCompComponent._component_media.js == 'console.log("JS file");\n' # type: ignore[attr-defined] assert AppLvlCompComponent._component_media.js_file == "app_lvl_comp/app_lvl_comp.js" # type: ignore[attr-defined] assert isinstance(AppLvlCompComponent._component_media._template, Template) # type: ignore[attr-defined] assert AppLvlCompComponent._component_media._template.origin.component_cls is AppLvlCompComponent # type: ignore[attr-defined] def test_html_variable_filtered(self): class FilteredComponent(Component): template: types.django_html = """ Var1: {{ var1 }} Var2 (uppercased): {{ var2|upper }} """ def get_template_data(self, args, kwargs, slots, context): return { "var1": kwargs["var1"], "var2": kwargs["var2"], } rendered = FilteredComponent.render(kwargs={"var1": "test1", "var2": "test2"}) assertHTMLEqual( rendered, """ Var1: test1 Var2 (uppercased): TEST2 """, ) @djc_test class TestComponentMedia: def test_empty_media(self): class SimpleComponent(Component): template: types.django_html = """ {% load component_tags %} {% component_js_dependencies %} {% component_css_dependencies %} Variable: {{ variable }} """ class Media: pass rendered = SimpleComponent.render() assert rendered.count("', rendered) assertInHTML('', rendered) assertInHTML('', rendered) def test_css_js_as_string(self): class SimpleComponent(Component): template = """ {% load component_tags %} {% component_js_dependencies %} {% component_css_dependencies %} """ class Media: css = "path/to/style.css" js = "path/to/script.js" rendered = SimpleComponent.render() assertInHTML('', rendered) assertInHTML('', rendered) def test_css_as_dict(self): class SimpleComponent(Component): template = """ {% load component_tags %} {% component_js_dependencies %} {% component_css_dependencies %} """ class Media: css = { "all": "path/to/style.css", "print": ["path/to/style2.css"], "screen": "path/to/style3.css", } js = ["path/to/script.js"] rendered = SimpleComponent.render() assertInHTML('', rendered) assertInHTML('', rendered) assertInHTML('', rendered) assertInHTML('', rendered) def test_media_custom_render_js(self): class MyMedia(Media): def render_js(self): tags: list[str] = [] for path in self._js: # type: ignore[attr-defined] abs_path = self.absolute_path(path) # type: ignore[attr-defined] tags.append(f'') return tags class SimpleComponent(Component): template = """ {% load component_tags %} {% component_js_dependencies %} {% component_css_dependencies %} """ media_class = MyMedia class Media: js = ["path/to/script.js", "path/to/script2.js"] rendered = SimpleComponent.render() assert '' in rendered assert '' in rendered def test_media_custom_render_css(self): class MyMedia(Media): def render_css(self): tags: list[str] = [] media = sorted(self._css) # type: ignore[attr-defined] for medium in media: for path in self._css[medium]: # type: ignore[attr-defined] tags.append(f'') return tags class SimpleComponent(Component): template = """ {% load component_tags %} {% component_js_dependencies %} {% component_css_dependencies %} """ media_class = MyMedia class Media: css = { "all": "path/to/style.css", "print": ["path/to/style2.css"], "screen": "path/to/style3.css", } rendered = SimpleComponent.render() assertInHTML('', rendered) assertInHTML('', rendered) assertInHTML('', rendered) @djc_test( django_settings={ "INSTALLED_APPS": ("django_components", "tests"), } ) def test_glob_pattern_relative_to_component(self): from tests.components.glob.glob import GlobComponent rendered = GlobComponent.render() assertInHTML('', rendered) assertInHTML('', rendered) assertInHTML('', rendered) assertInHTML('', rendered) @djc_test( django_settings={ "INSTALLED_APPS": ("django_components", "tests"), } ) def test_glob_pattern_relative_to_root_dir(self): from tests.components.glob.glob import GlobComponentRootDir rendered = GlobComponentRootDir.render() assertInHTML('', rendered) assertInHTML('', rendered) assertInHTML('', rendered) assertInHTML('', rendered) @djc_test( django_settings={ "INSTALLED_APPS": ("django_components", "tests"), } ) def test_non_globs_not_modified(self): from tests.components.glob.glob import NonGlobComponentRootDir rendered = NonGlobComponentRootDir.render() assertInHTML('', rendered) assertInHTML('', rendered) @djc_test( django_settings={ "INSTALLED_APPS": ("django_components", "tests"), } ) def test_non_globs_not_modified_nonexist(self): from tests.components.glob.glob import NonGlobNonexistComponentRootDir rendered = NonGlobNonexistComponentRootDir.render() assertInHTML('', rendered) assertInHTML('', rendered) def test_glob_pattern_does_not_break_urls(self): from tests.components.glob.glob import UrlComponent rendered = UrlComponent.render() assertInHTML('', rendered) assertInHTML('', rendered) # `://` is escaped because Django's `Media.absolute_path()` doesn't consider `://` a valid URL assertInHTML('', rendered) assertInHTML('', rendered) assertInHTML( '', rendered ) assertInHTML( '', rendered ) # `://` is escaped because Django's `Media.absolute_path()` doesn't consider `://` a valid URL assertInHTML( '', rendered ) assertInHTML('', rendered) @djc_test class TestMediaPathAsObject: def test_safestring(self): """ Test that media work with paths defined as instances of classes that define the `__html__` method. See https://docs.djangoproject.com/en/5.2/topics/forms/media/#paths-as-objects """ # NOTE: @html_safe adds __html__ method from __str__ @html_safe class JSTag: def __init__(self, path: str) -> None: self.path = path def __str__(self): return f'' @html_safe class CSSTag: def __init__(self, path: str) -> None: self.path = path def __str__(self): return f'' # Format as mentioned in https://github.com/django-components/django-components/issues/522#issuecomment-2173577094 @html_safe class PathObj: def __init__(self, static_path: str) -> None: self.static_path = static_path def __str__(self): return format_html('', static(self.static_path)) class SimpleComponent(Component): template = """ {% load component_tags %} {% component_js_dependencies %} {% component_css_dependencies %} """ class Media: css = { "all": [ CSSTag("path/to/style.css"), # Formatted by CSSTag mark_safe(''), # Literal ], "print": [ CSSTag("path/to/style3.css"), # Formatted by CSSTag ], "screen": "path/to/style4.css", # Formatted by Media.render_css } js = [ JSTag("path/to/script.js"), # Formatted by JSTag mark_safe(''), # Literal PathObj("path/to/script3.js"), # Literal "path/to/script4.js", # Formatted by Media.render_js ] rendered = SimpleComponent.render() assertInHTML('', rendered) assertInHTML('', rendered) assertInHTML('', rendered) assertInHTML('', rendered) assertInHTML('', rendered) assertInHTML('', rendered) assertInHTML('', rendered) assertInHTML('', rendered) def test_pathlike(self): """ Test that media work with paths defined as instances of classes that define the `__fspath__` method. """ class MyPath(os.PathLike): def __init__(self, path: str) -> None: self.path = path def __fspath__(self): return self.path class SimpleComponent(Component): template = """ {% load component_tags %} {% component_js_dependencies %} {% component_css_dependencies %} """ class Media: css = { "all": [ MyPath("path/to/style.css"), Path("path/to/style2.css"), ], "print": [ MyPath("path/to/style3.css"), ], "screen": "path/to/style4.css", } js = [ MyPath("path/to/script.js"), Path("path/to/script2.js"), "path/to/script3.js", ] rendered = SimpleComponent.render() assertInHTML('', rendered) assertInHTML('', rendered) assertInHTML('', rendered) assertInHTML('', rendered) assertInHTML('', rendered) assertInHTML('', rendered) assertInHTML('', rendered) def test_str(self): """ Test that media work with paths defined as instances of classes that subclass 'str'. """ class MyStr(str): pass class SimpleComponent(Component): template = """ {% load component_tags %} {% component_js_dependencies %} {% component_css_dependencies %} """ class Media: css = { "all": [ MyStr("path/to/style.css"), "path/to/style2.css", ], "print": [ MyStr("path/to/style3.css"), ], "screen": "path/to/style4.css", } js = [ MyStr("path/to/script.js"), "path/to/script2.js", ] rendered = SimpleComponent.render() assertInHTML('', rendered) assertInHTML('', rendered) assertInHTML('', rendered) assertInHTML('', rendered) assertInHTML('', rendered) assertInHTML('', rendered) def test_bytes(self): """ Test that media work with paths defined as instances of classes that subclass 'bytes'. """ class MyBytes(bytes): pass class SimpleComponent(Component): template = """ {% load component_tags %} {% component_js_dependencies %} {% component_css_dependencies %} """ class Media: css = { "all": [ MyBytes(b"path/to/style.css"), b"path/to/style2.css", ], "print": [ MyBytes(b"path/to/style3.css"), ], "screen": b"path/to/style4.css", } js = [ MyBytes(b"path/to/script.js"), "path/to/script2.js", ] rendered = SimpleComponent.render() assertInHTML('', rendered) assertInHTML('', rendered) assertInHTML('', rendered) assertInHTML('', rendered) assertInHTML('', rendered) assertInHTML('', rendered) def test_function(self): class SimpleComponent(Component): template = """ {% load component_tags %} {% component_js_dependencies %} {% component_css_dependencies %} """ class Media: css = [ lambda: mark_safe(''), # Literal lambda: Path("calendar/style1.css"), lambda: "calendar/style2.css", lambda: b"calendar/style3.css", ] js = [ lambda: mark_safe(''), # Literal lambda: Path("calendar/script1.js"), lambda: "calendar/script2.js", lambda: b"calendar/script3.js", ] rendered = SimpleComponent.render() assertInHTML('', rendered) assertInHTML('', rendered) assertInHTML('', rendered) assertInHTML('', rendered) assertInHTML('', rendered) assertInHTML('', rendered) assertInHTML('', rendered) assertInHTML('', rendered) @djc_test( django_settings={ "STATIC_URL": "static/", } ) def test_works_with_static(self): """Test that all the different ways of defining media files works with Django's staticfiles""" class SimpleComponent(Component): template = """ {% load component_tags %} {% component_js_dependencies %} {% component_css_dependencies %} """ class Media: css = [ mark_safe(f''), # Literal Path("calendar/style1.css"), "calendar/style2.css", b"calendar/style3.css", lambda: "calendar/style4.css", ] js = [ mark_safe(f''), # Literal Path("calendar/script1.js"), "calendar/script2.js", b"calendar/script3.js", lambda: "calendar/script4.js", ] rendered = SimpleComponent.render() assertInHTML('', rendered) assertInHTML('', rendered) assertInHTML('', rendered) assertInHTML('', rendered) assertInHTML('', rendered) assertInHTML('', rendered) assertInHTML('', rendered) assertInHTML('', rendered) assertInHTML('', rendered) @djc_test class TestMediaStaticfiles: # For context see https://github.com/django-components/django-components/issues/522 @djc_test( django_settings={ # Configure static files. The dummy files are set up in the `./static_root` dir. # The URL should have path prefix /static/. # NOTE: We don't need STATICFILES_DIRS, because we don't run collectstatic # See https://docs.djangoproject.com/en/5.2/ref/settings/#std-setting-STATICFILES_DIRS "STATIC_URL": "static/", "STATIC_ROOT": os.path.join(Path(__file__).resolve().parent, "static_root"), # `django.contrib.staticfiles` MUST be installed for staticfiles resolution to work. "INSTALLED_APPS": [ "django.contrib.staticfiles", "django_components", ], } ) def test_default_static_files_storage(self): """Test integration with Django's staticfiles app""" class MyMedia(Media): def render_js(self): tags: list[str] = [] for path in self._js: # type: ignore[attr-defined] abs_path = self.absolute_path(path) # type: ignore[attr-defined] tags.append(f'') return tags class SimpleComponent(Component): template = """ {% load component_tags %} {% component_js_dependencies %} {% component_css_dependencies %} """ media_class = MyMedia class Media: css = "calendar/style.css" js = "calendar/script.js" rendered = SimpleComponent.render() # NOTE: Since we're using the default storage class for staticfiles, the files should # be searched as specified above (e.g. `calendar/script.js`) inside `static_root` dir. assertInHTML('', rendered) assertInHTML('', rendered) # For context see https://github.com/django-components/django-components/issues/522 @djc_test( django_settings={ # Configure static files. The dummy files are set up in the `./static_root` dir. # The URL should have path prefix /static/. # NOTE: We don't need STATICFILES_DIRS, because we don't run collectstatic # See https://docs.djangoproject.com/en/5.2/ref/settings/#std-setting-STATICFILES_DIRS "STATIC_URL": "static/", "STATIC_ROOT": os.path.join(Path(__file__).resolve().parent, "static_root"), # NOTE: STATICFILES_STORAGE is deprecated since 5.1, use STORAGES instead # See https://docs.djangoproject.com/en/5.2/ref/settings/#storages "STORAGES": { # This was NOT changed "default": { "BACKEND": "django.core.files.storage.FileSystemStorage", }, # This WAS changed so that static files are looked up by the `staticfiles.json` "staticfiles": { "BACKEND": "django.contrib.staticfiles.storage.ManifestStaticFilesStorage", }, }, # `django.contrib.staticfiles` MUST be installed for staticfiles resolution to work. "INSTALLED_APPS": [ "django.contrib.staticfiles", "django_components", ], } ) def test_manifest_static_files_storage(self): """Test integration with Django's staticfiles app and ManifestStaticFilesStorage""" class MyMedia(Media): def render_js(self): tags: list[str] = [] for path in self._js: # type: ignore[attr-defined] abs_path = self.absolute_path(path) # type: ignore[attr-defined] tags.append(f'') return tags class SimpleComponent(Component): template = """ {% load component_tags %} {% component_js_dependencies %} {% component_css_dependencies %} """ media_class = MyMedia class Media: css = "calendar/style.css" js = "calendar/script.js" rendered = SimpleComponent.render() # NOTE: Since we're using ManifestStaticFilesStorage, we expect the rendered media to link # to the files as defined in staticfiles.json assertInHTML('', rendered) assertInHTML('', rendered) @djc_test class TestMediaRelativePath: def _gen_parent_component(self): class ParentComponent(Component): template: types.django_html = """ {% load component_tags %}

Parent content

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

Slot content

{% component "variable_display" shadowing_variable='slot_default_override' new_variable='slot_default_unique' %} {% endcomponent %} {% endslot %}
""" # noqa def get_template_data(self, args, kwargs, slots, context): return {"shadowing_variable": "NOT SHADOWED"} return ParentComponent def _gen_variable_display_component(self): class VariableDisplay(Component): template: types.django_html = """ {% load component_tags %}

Shadowing variable = {{ shadowing_variable }}

Uniquely named variable = {{ unique_variable }}

""" def get_template_data(self, args, kwargs, slots, context): context = {} if kwargs["shadowing_variable"] is not None: context["shadowing_variable"] = kwargs["shadowing_variable"] if kwargs["new_variable"] is not None: context["unique_variable"] = kwargs["new_variable"] return context return VariableDisplay # Settings required for autodiscover to work @djc_test( django_settings={ "BASE_DIR": Path(__file__).resolve().parent, "STATICFILES_DIRS": [ Path(__file__).resolve().parent / "components", ], } ) def test_component_with_relative_media_paths(self): registry.register(name="parent_component", component=self._gen_parent_component()) registry.register(name="variable_display", component=self._gen_variable_display_component()) # 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 autodiscover(map_module=lambda p: f"tests.{p}" if p.startswith("components") else p) # Make sure that only relevant components are registered: comps_to_remove = [ comp_name for comp_name in registry.all() if comp_name not in ["relative_file_component", "parent_component", "variable_display"] ] for comp_name in comps_to_remove: registry.unregister(comp_name) template_str: types.django_html = """ {% load component_tags %} {% component_js_dependencies %} {% component_css_dependencies %} {% component 'relative_file_component' variable=variable / %} """ template = Template(template_str) rendered = render_dependencies(template.render(Context({"variable": "test"}))) assertInHTML('', rendered) assertInHTML( """
""", rendered, ) assertInHTML('', rendered) # Settings required for autodiscover to work @djc_test( django_settings={ "BASE_DIR": Path(__file__).resolve().parent, "STATICFILES_DIRS": [ Path(__file__).resolve().parent / "components", ], } ) def test_component_with_relative_media_paths_as_subcomponent(self): registry.register(name="parent_component", component=self._gen_parent_component()) registry.register(name="variable_display", component=self._gen_variable_display_component()) # 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 autodiscover(map_module=lambda p: f"tests.{p}" if p.startswith("components") else p) registry.unregister("relative_file_pathobj_component") template_str: types.django_html = """ {% load component_tags %} {% component_js_dependencies %} {% component_css_dependencies %} {% component 'parent_component' %} {% fill 'content' %} {% component 'relative_file_component' variable='hello' %} {% endcomponent %} {% endfill %} {% endcomponent %} """ template = Template(template_str) rendered = template.render(Context({})) assertInHTML('', rendered) # Settings required for autodiscover to work @djc_test( django_settings={ "BASE_DIR": Path(__file__).resolve().parent, "STATICFILES_DIRS": [ Path(__file__).resolve().parent / "components", ], } ) def test_component_with_relative_media_does_not_trigger_safestring_path_at__new__(self): """ Test that, for the __html__ objects are not coerced into string throughout the class creation. This is important to allow to call `collectstatic` command. Because some users use `static` inside the `__html__` or `__str__` methods. So if we "render" the safestring using str() during component class creation (__new__), then we force to call `static`. And if this happens during `collectstatic` run, then this triggers an error, because `static` is called before the static files exist. https://github.com/django-components/django-components/issues/522#issuecomment-2173577094 """ registry.register(name="parent_component", component=self._gen_parent_component()) registry.register(name="variable_display", component=self._gen_variable_display_component()) # Ensure that the module is executed again after import in autodiscovery if "tests.components.relative_file_pathobj.relative_file_pathobj" in sys.modules: del sys.modules["tests.components.relative_file_pathobj.relative_file_pathobj"] # Fix the paths, since the "components" dir is nested autodiscover(map_module=lambda p: f"tests.{p}" if p.startswith("components") else p) # Mark the PathObj instances of 'relative_file_pathobj_component' so they won't raise # error if PathObj.__str__ is triggered. CompCls = registry.get("relative_file_pathobj_component") CompCls.Media.js[0].throw_on_calling_str = False # type: ignore CompCls.Media.css["all"][0].throw_on_calling_str = False # type: ignore rendered = CompCls.render(kwargs={"variable": "abc"}) assertInHTML('', rendered) assertInHTML('', rendered) assertInHTML('', rendered) @djc_test class TestSubclassingAttributes: def test_both_js_and_js_file_none(self): class TestComp(Component): js = None js_file = None template = None template_file = None assert TestComp.js is None assert TestComp.js_file is None assert TestComp.template is None assert TestComp.template_file is None def test_mixing_none_and_non_none_raises(self): with pytest.raises( ImproperlyConfigured, match=re.escape("Received non-empty value from both 'template' and 'template_file' in Component TestComp"), ): class TestComp(Component): js = "console.log('hi')" js_file = None template = "

hi

" template_file = None def test_both_non_none_raises(self): with pytest.raises( ImproperlyConfigured, match=re.escape("Received non-empty value from both 'template' and 'template_file' in Component TestComp"), ): class TestComp(Component): js = "console.log('hi')" js_file = "file.js" template = "

hi

" template_file = "file.html" def test_parent_non_null_child_non_null(self): class ParentComp(Component): js = "console.log('parent')" template = "

parent

" class TestComp(ParentComp): js = "console.log('child')" template = "

child

" assert TestComp.js == "console.log('child')" assert TestComp.js_file is None assert TestComp.template == "

child

" assert TestComp.template_file is None assert isinstance(ParentComp._template, Template) assert ParentComp._template.source == "

parent

" assert ParentComp._template.origin.component_cls == ParentComp assert isinstance(TestComp._template, Template) assert TestComp._template.source == "

child

" assert TestComp._template.origin.component_cls == TestComp def test_parent_null_child_non_null(self): class ParentComp(Component): js = None template = None class TestComp(ParentComp): js = "console.log('child')" template = "

child

" assert TestComp.js == "console.log('child')" assert TestComp.js_file is None assert TestComp.template == "

child

" assert TestComp.template_file is None assert ParentComp._template is None assert isinstance(TestComp._template, Template) assert TestComp._template.source == "

child

" assert TestComp._template.origin.component_cls == TestComp def test_parent_non_null_child_null(self): class ParentComp(Component): js: Optional[str] = "console.log('parent')" template: Optional[str] = "

parent

" class TestComp(ParentComp): js = None template = None assert TestComp.js is None assert TestComp.js_file is None assert TestComp.template is None assert TestComp.template_file is None assert TestComp._template is None assert isinstance(ParentComp._template, Template) assert ParentComp._template.source == "

parent

" assert ParentComp._template.origin.component_cls == ParentComp def test_parent_null_child_null(self): class ParentComp(Component): js = None template = None class TestComp(ParentComp): js = None template = None assert TestComp.js is None assert TestComp.js_file is None assert TestComp.template is None assert TestComp.template_file is None assert TestComp._template is None assert ParentComp._template is None def test_grandparent_non_null_parent_pass_child_pass(self): class GrandParentComp(Component): js = "console.log('grandparent')" template = "

grandparent

" class ParentComp(GrandParentComp): pass class TestComp(ParentComp): pass assert TestComp.js == "console.log('grandparent')" assert TestComp.js_file is None assert TestComp.template == "

grandparent

" assert TestComp.template_file is None assert isinstance(GrandParentComp._template, Template) assert GrandParentComp._template.source == "

grandparent

" assert GrandParentComp._template.origin.component_cls == GrandParentComp assert isinstance(ParentComp._template, Template) assert ParentComp._template.source == "

grandparent

" assert ParentComp._template.origin.component_cls == ParentComp assert isinstance(TestComp._template, Template) assert TestComp._template.source == "

grandparent

" assert TestComp._template.origin.component_cls == TestComp def test_grandparent_non_null_parent_null_child_pass(self): class GrandParentComp(Component): js: Optional[str] = "console.log('grandparent')" template: Optional[str] = "

grandparent

" class ParentComp(GrandParentComp): js = None template = None class TestComp(ParentComp): pass assert TestComp.js is None assert TestComp.js_file is None assert TestComp.template is None assert TestComp.template_file is None assert isinstance(GrandParentComp._template, Template) assert GrandParentComp._template.source == "

grandparent

" assert GrandParentComp._template.origin.component_cls == GrandParentComp assert ParentComp._template is None assert TestComp._template is None def test_grandparent_non_null_parent_pass_child_non_null(self): class GrandParentComp(Component): js = "console.log('grandparent')" template = "

grandparent

" class ParentComp(GrandParentComp): pass class TestComp(ParentComp): js = "console.log('child')" template = "

child

" assert TestComp.js == "console.log('child')" assert TestComp.js_file is None assert TestComp.template == "

child

" assert TestComp.template_file is None assert isinstance(GrandParentComp._template, Template) assert GrandParentComp._template.source == "

grandparent

" assert GrandParentComp._template.origin.component_cls == GrandParentComp assert isinstance(ParentComp._template, Template) assert ParentComp._template.source == "

grandparent

" assert ParentComp._template.origin.component_cls == ParentComp assert isinstance(TestComp._template, Template) assert TestComp._template.source == "

child

" assert TestComp._template.origin.component_cls == TestComp def test_grandparent_null_parent_pass_child_non_null(self): class GrandParentComp(Component): js = None template = None class ParentComp(GrandParentComp): pass class TestComp(ParentComp): js = "console.log('child')" template = "

child

" assert TestComp.js == "console.log('child')" assert TestComp.js_file is None assert TestComp.template == "

child

" assert TestComp.template_file is None assert GrandParentComp._template is None assert ParentComp._template is None assert isinstance(TestComp._template, Template) assert TestComp._template.source == "

child

" assert TestComp._template.origin.component_cls == TestComp @djc_test class TestSubclassingMedia: def test_media_in_child_and_parent(self): class ParentComponent(Component): template: types.django_html = """ {% load component_tags %} {% component_js_dependencies %} {% component_css_dependencies %} """ class Media: css = "parent.css" js = "parent.js" class ChildComponent(ParentComponent): class Media: css = "child.css" js = "child.js" rendered = ChildComponent.render() assertInHTML('', rendered) assertInHTML('', rendered) assertInHTML('', rendered) assertInHTML('', rendered) assert str(ChildComponent.media) == ( '\n' '\n' '\n' '' ) def test_media_in_child_and_grandparent(self): class GrandParentComponent(Component): template: types.django_html = """ {% load component_tags %} {% component_js_dependencies %} {% component_css_dependencies %} """ class Media: css = "grandparent.css" js = "grandparent.js" # `pass` means that we inherit `Media` from `GrandParentComponent` class ParentComponent(GrandParentComponent): pass class ChildComponent(ParentComponent): class Media: css = "child.css" js = "child.js" rendered = ChildComponent.render() assertInHTML('', rendered) assertInHTML('', rendered) assertInHTML('', rendered) assertInHTML('', rendered) assert str(ChildComponent.media) == ( '\n' '\n' '\n' '' ) # Check that setting `Media = None` on a child class means that we will NOT inherit `Media` from the parent class def test_media_in_child_and_grandparent__inheritance_off(self): class GrandParentComponent(Component): template: types.django_html = """ {% load component_tags %} {% component_js_dependencies %} {% component_css_dependencies %} """ class Media: css = "grandparent.css" js = "grandparent.js" # `None` means that we will NOT inherit `Media` from `GrandParentComponent` class ParentComponent(GrandParentComponent): Media = None # type: ignore[assignment] class ChildComponent(ParentComponent): class Media: css = "child.css" js = "child.js" rendered = ChildComponent.render() assertInHTML('', rendered) assertInHTML('', rendered) assert "grandparent.css" not in rendered assert "grandparent.js" not in rendered assert str(ChildComponent.media) == ( '\n' ) def test_media_in_parent_and_grandparent(self): class GrandParentComponent(Component): template: types.django_html = """ {% load component_tags %} {% component_js_dependencies %} {% component_css_dependencies %} """ class Media: css = "grandparent.css" js = "grandparent.js" class ParentComponent(GrandParentComponent): class Media: css = "parent.css" js = "parent.js" class ChildComponent(ParentComponent): pass rendered = ChildComponent.render() assertInHTML('', rendered) assertInHTML('', rendered) assertInHTML('', rendered) assertInHTML('', rendered) assert str(ChildComponent.media) == ( '\n' '\n' '\n' '' ) def test_media_in_multiple_bases(self): class GrandParent1Component(Component): class Media: css = "grandparent1.css" js = "grandparent1.js" class GrandParent2Component(Component): pass # NOTE: The bases don't even have to be Component classes, # as long as they have the nested `Media` class. class GrandParent3Component: # NOTE: When we don't subclass `Component`, we have to correctly format the `Media` class class Media: css = {"all": ["grandparent3.css"]} js = ["grandparent3.js"] class GrandParent4Component: pass class Parent1Component(GrandParent1Component, GrandParent2Component): class Media: css = "parent1.css" js = "parent1.js" # `pass` means that we inherit `Media` from `GrandParent3Component` and `GrandParent4Component` class Parent2Component(GrandParent3Component, GrandParent4Component): pass class ChildComponent(Parent1Component, Parent2Component): template: types.django_html = """ {% load component_tags %} {% component_js_dependencies %} {% component_css_dependencies %} """ class Media: css = "child.css" js = "child.js" rendered = ChildComponent.render() assertInHTML('', rendered) assertInHTML('', rendered) assertInHTML('', rendered) assertInHTML('', rendered) assertInHTML('', rendered) assertInHTML('', rendered) assertInHTML('', rendered) assertInHTML('', rendered) assert str(ChildComponent.media) == ( '\n' '\n' '\n' '\n' '\n' '\n' '\n' '' ) # Check that setting `Media = None` on a child class means that we will NOT inherit `Media` from the parent class def test_media_in_multiple_bases__inheritance_off(self): class GrandParent1Component(Component): class Media: css = "grandparent1.css" js = "grandparent1.js" class GrandParent2Component(Component): pass # NOTE: The bases don't even have to be Component classes, # as long as they have the nested `Media` class. class GrandParent3Component: # NOTE: When we don't subclass `Component`, we have to correctly format the `Media` class class Media: css = {"all": ["grandparent3.css"]} js = ["grandparent3.js"] class GrandParent4Component: pass class Parent1Component(GrandParent1Component, GrandParent2Component): class Media: css = "parent1.css" js = "parent1.js" # `None` means that we will NOT inherit `Media` from `GrandParent3Component` and `GrandParent4Component` class Parent2Component(GrandParent3Component, GrandParent4Component): Media = None # type: ignore[assignment] class ChildComponent(Parent1Component, Parent2Component): template: types.django_html = """ {% load component_tags %} {% component_js_dependencies %} {% component_css_dependencies %} """ class Media: css = "child.css" js = "child.js" rendered = ChildComponent.render() assertInHTML('', rendered) assertInHTML('', rendered) assertInHTML('', rendered) assertInHTML('', rendered) assertInHTML('', rendered) assertInHTML('', rendered) assert "grandparent3.css" not in rendered assert "grandparent3.js" not in rendered assert str(ChildComponent.media) == ( '\n' '\n' '\n' '\n' '\n' '' ) def test_extend_false_in_child(self): class Parent1Component(Component): template: types.django_html = """ {% load component_tags %} {% component_js_dependencies %} {% component_css_dependencies %} """ class Media: css = "parent1.css" js = "parent1.js" class Parent2Component(Component): class Media: css = "parent2.css" js = "parent2.js" class ChildComponent(Parent1Component, Parent2Component): class Media: extend = False css = "child.css" js = "child.js" rendered = ChildComponent.render() assert "parent1.css" not in rendered assert "parent2.css" not in rendered assertInHTML('', rendered) assert "parent1.js" not in rendered assert "parent2.js" not in rendered assertInHTML('', rendered) assert str(ChildComponent.media) == ( '\n' ) def test_extend_false_in_parent(self): class GrandParentComponent(Component): class Media: css = "grandparent.css" js = "grandparent.js" class Parent1Component(Component): template: types.django_html = """ {% load component_tags %} {% component_js_dependencies %} {% component_css_dependencies %} """ class Media: css = "parent1.css" js = "parent1.js" class Parent2Component(GrandParentComponent): class Media: extend = False css = "parent2.css" js = "parent2.js" class ChildComponent(Parent1Component, Parent2Component): class Media: css = "child.css" js = "child.js" rendered = ChildComponent.render() assert "grandparent.css" not in rendered assertInHTML('', rendered) assertInHTML('', rendered) assertInHTML('', rendered) assert "grandparent.js" not in rendered assertInHTML('', rendered) assertInHTML('', rendered) assertInHTML('', rendered) assert str(ChildComponent.media) == ( '\n' '\n' '\n' '\n' '\n' '' ) def test_extend_list_in_child(self): class Parent1Component(Component): template: types.django_html = """ {% load component_tags %} {% component_js_dependencies %} {% component_css_dependencies %} """ class Media: css = "parent1.css" js = "parent1.js" class Parent2Component(Component): class Media: css = "parent2.css" js = "parent2.js" class Other1Component(Component): class Media: css = "other1.css" js = "other1.js" class Other2Component: class Media: css = {"all": ["other2.css"]} js = ["other2.js"] class ChildComponent(Parent1Component, Parent2Component): class Media: extend = [Other1Component, Other2Component] css = "child.css" js = "child.js" rendered = ChildComponent.render() assert "parent1.css" not in rendered assert "parent2.css" not in rendered assertInHTML('', rendered) assertInHTML('', rendered) assertInHTML('', rendered) assert "parent1.js" not in rendered assert "parent2.js" not in rendered assertInHTML('', rendered) assertInHTML('', rendered) assertInHTML('', rendered) assert str(ChildComponent.media) == ( '\n' '\n' '\n' '\n' '\n' '' ) def test_extend_list_in_parent(self): class Other1Component(Component): class Media: css = "other1.css" js = "other1.js" class Other2Component: class Media: css = {"all": ["other2.css"]} js = ["other2.js"] class GrandParentComponent(Component): class Media: css = "grandparent.css" js = "grandparent.js" class Parent1Component(Component): template: types.django_html = """ {% load component_tags %} {% component_js_dependencies %} {% component_css_dependencies %} """ class Media: css = "parent1.css" js = "parent1.js" class Parent2Component(GrandParentComponent): class Media: extend = [Other1Component, Other2Component] css = "parent2.css" js = "parent2.js" class ChildComponent(Parent1Component, Parent2Component): class Media: css = "child.css" js = "child.js" rendered = ChildComponent.render() assert "grandparent.css" not in rendered assertInHTML('', rendered) assertInHTML('', rendered) assertInHTML('', rendered) assertInHTML('', rendered) assertInHTML('', rendered) assert "grandparent.js" not in rendered assertInHTML('', rendered) assertInHTML('', rendered) assertInHTML('', rendered) assertInHTML('', rendered) assertInHTML('', rendered) assert str(ChildComponent.media) == ( '\n' '\n' '\n' '\n' '\n' '\n' '\n' '\n' '\n' '' )