From f768a3cf55064078692611ef8783377a239363dd Mon Sep 17 00:00:00 2001 From: Juro Oravec Date: Mon, 16 Dec 2024 14:36:18 +0100 Subject: [PATCH 001/326] docs: more work in progress comments --- README.md | 2 +- docs/overrides/main.html | 5 ----- mkdocs.yml | 3 ++- pyproject.toml | 5 +++++ 4 files changed, 8 insertions(+), 7 deletions(-) delete mode 100644 docs/overrides/main.html diff --git a/README.md b/README.md index 72de59df..7a13b78b 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![PyPI - Version](https://img.shields.io/pypi/v/django-components)](https://pypi.org/project/django-components/) [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/django-components)](https://pypi.org/project/django-components/) [![PyPI - License](https://img.shields.io/pypi/l/django-components)](https://github.com/EmilStenstrom/django-components/blob/master/LICENSE/) [![PyPI - Downloads](https://img.shields.io/pypi/dm/django-components)](https://pypistats.org/packages/django-components) [![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/EmilStenstrom/django-components/tests.yml)](https://github.com/EmilStenstrom/django-components/actions/workflows/tests.yml) -[**Docs (Work in progress)**](https://EmilStenstrom.github.io/django-components/latest/) +[**Documentation**](https://EmilStenstrom.github.io/django-components/latest/) Django-components is a package that introduces component-based architecture to Django's server-side rendering. It aims to combine Django's templating system with the modularity seen in modern frontend frameworks. diff --git a/docs/overrides/main.html b/docs/overrides/main.html deleted file mode 100644 index dc85d397..00000000 --- a/docs/overrides/main.html +++ /dev/null @@ -1,5 +0,0 @@ -{% extends "base.html" %} - -{% block announce %} - 🚨The documentation is still a work in progress. 🚨 -{% endblock %} \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index fe7a95f2..9c2250a2 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -27,7 +27,8 @@ validation: theme: name: "material" - custom_dir: docs/overrides + # Uncomment to extend / override files from the theme + # custom_dir: docs/overrides features: - content.action.edit - content.action.view diff --git a/pyproject.toml b/pyproject.toml index 34beb64f..a225c764 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,8 +33,13 @@ dependencies = [ ] license = {text = "MIT"} +# See https://docs.pypi.org/project_metadata/#icons [project.urls] Homepage = "https://github.com/EmilStenstrom/django-components/" +Documentation = "https://emilstenstrom.github.io/django-components/" +Changelog = "https://emilstenstrom.github.io/django-components/latest/release_notes/" +Issues = "https://github.com/EmilStenstrom/django-components/issues" +Donate = "https://github.com/sponsors/EmilStenstrom" [tool.setuptools.packages.find] From b2ce52dc532d46fa20faee9987109cc1ced91380 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Dec 2024 17:32:14 +0000 Subject: [PATCH 002/326] build(deps-dev): bump mypy from 1.13.0 to 1.14.0 Bumps [mypy](https://github.com/python/mypy) from 1.13.0 to 1.14.0. - [Changelog](https://github.com/python/mypy/blob/master/CHANGELOG.md) - [Commits](https://github.com/python/mypy/compare/v1.13.0...v1.14.0) --- updated-dependencies: - dependency-name: mypy dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index e7e4fca6..7bf66438 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -50,7 +50,7 @@ isort==5.13.2 # via -r requirements-dev.in mccabe==0.7.0 # via flake8 -mypy==1.13.0 +mypy==1.14.0 # via -r requirements-dev.in mypy-extensions==1.0.0 # via From 8f950cddaa47eb056f2eabe3ffaa26222f4a9c55 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Dec 2024 17:39:53 +0000 Subject: [PATCH 003/326] build(deps): bump urllib3 from 2.2.3 to 2.3.0 Bumps [urllib3](https://github.com/urllib3/urllib3) from 2.2.3 to 2.3.0. - [Release notes](https://github.com/urllib3/urllib3/releases) - [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst) - [Commits](https://github.com/urllib3/urllib3/compare/2.2.3...2.3.0) --- updated-dependencies: - dependency-name: urllib3 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-docs.txt b/requirements-docs.txt index 491f34f5..93fab1d5 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -223,7 +223,7 @@ tinycss2==1.4.0 # cssselect2 tzdata==2024.2 # via django -urllib3==2.2.3 +urllib3==2.3.0 # via requests verspec==0.1.0 # via mike From 081ef1b85fa4043728ac16dc0dd2b66a8a89c9d0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Dec 2024 17:46:55 +0000 Subject: [PATCH 004/326] build(deps): bump pymdown-extensions from 10.12 to 10.13 Bumps [pymdown-extensions](https://github.com/facelessuser/pymdown-extensions) from 10.12 to 10.13. - [Release notes](https://github.com/facelessuser/pymdown-extensions/releases) - [Commits](https://github.com/facelessuser/pymdown-extensions/compare/10.12...10.13) --- updated-dependencies: - dependency-name: pymdown-extensions dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-docs.txt b/requirements-docs.txt index 93fab1d5..811ae9dc 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -182,7 +182,7 @@ pycparser==2.22 # via cffi pygments==2.18.0 # via mkdocs-material -pymdown-extensions==10.12 +pymdown-extensions==10.13 # via # hatch.envs.docs # markdown-exec From 0985c8efc6a777be3479835366386d96cc834e79 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Dec 2024 17:53:12 +0000 Subject: [PATCH 005/326] build(deps): bump click from 8.1.7 to 8.1.8 Bumps [click](https://github.com/pallets/click) from 8.1.7 to 8.1.8. - [Release notes](https://github.com/pallets/click/releases) - [Changelog](https://github.com/pallets/click/blob/main/CHANGES.rst) - [Commits](https://github.com/pallets/click/compare/8.1.7...8.1.8) --- updated-dependencies: - dependency-name: click dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-docs.txt b/requirements-docs.txt index 811ae9dc..f5139235 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -45,7 +45,7 @@ cffi==1.17.1 # via cairocffi charset-normalizer==3.4.0 # via requests -click==8.1.7 +click==8.1.8 # via # black # mkdocs From ed26aec18d9ab96565589f2d82ccddac9bdba22a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Dec 2024 17:59:29 +0000 Subject: [PATCH 006/326] build(deps): bump jinja2 from 3.1.4 to 3.1.5 Bumps [jinja2](https://github.com/pallets/jinja) from 3.1.4 to 3.1.5. - [Release notes](https://github.com/pallets/jinja/releases) - [Changelog](https://github.com/pallets/jinja/blob/main/CHANGES.rst) - [Commits](https://github.com/pallets/jinja/compare/3.1.4...3.1.5) --- updated-dependencies: - dependency-name: jinja2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-docs.txt b/requirements-docs.txt index f5139235..baf8192a 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -80,7 +80,7 @@ importlib-metadata==8.5.0 # via mike importlib-resources==6.4.5 # via mike -jinja2==3.1.4 +jinja2==3.1.5 # via # mike # mkdocs From fe67d9054760bd6298f6b50257ec1e82099893f8 Mon Sep 17 00:00:00 2001 From: Juro Oravec Date: Sat, 28 Dec 2024 19:27:19 +0100 Subject: [PATCH 007/326] refactor: Backbone for passing JS and CSS variables (#861) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- CHANGELOG.md | 10 + src/django_components/component.py | 42 +- src/django_components/components/dynamic.py | 1 - src/django_components/dependencies.py | 359 +++++++++++++----- src/django_components/provide.py | 4 - .../django_components.min.js | 2 +- src/django_components/util/misc.py | 4 + src/django_components_js/README.md | 2 +- src/django_components_js/src/manager.ts | 2 +- tests/test_attributes.py | 20 +- tests/test_component.py | 46 +-- tests/test_component_as_view.py | 36 +- tests/test_component_media.py | 14 +- tests/test_context.py | 125 +++--- tests/test_dependencies.py | 85 +++-- tests/test_dependency_manager.py | 14 +- tests/test_dependency_rendering.py | 144 ++++++- tests/test_dependency_rendering_e2e.py | 106 ++++-- tests/test_expression.py | 67 +++- tests/test_registry.py | 4 +- tests/test_tag_formatter.py | 20 +- tests/test_templatetags.py | 12 +- tests/test_templatetags_component.py | 231 +++++++---- tests/test_templatetags_extends.py | 90 ++--- tests/test_templatetags_provide.py | 63 +-- tests/test_templatetags_slot_fill.py | 201 +++++----- tests/test_templatetags_templating.py | 65 ++-- tests/testutils.py | 6 + 28 files changed, 1181 insertions(+), 594 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f4610d2..1bc1f41a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Release notes +## v0.124 + +#### Refactor + +- The undocumented `Component.component_id` was removed. Instead, use `Component.id`. Changes: + + - While `component_id` was unique every time you instantiated `Component`, The new `id` is unique + every time you render the component (e.g. with `Component.render()`) + - The new `id` is available only during render, so e.g. from within `get_context_data()` + ## v0.123 #### Fix diff --git a/src/django_components/component.py b/src/django_components/component.py index 8675cc85..4f71643b 100644 --- a/src/django_components/component.py +++ b/src/django_components/component.py @@ -45,7 +45,14 @@ from django_components.context import ( get_injected_context_var, make_isolated_context_copy, ) -from django_components.dependencies import RenderType, cache_inlined_css, cache_inlined_js, postprocess_component_html +from django_components.dependencies import ( + RenderType, + cache_component_css, + cache_component_css_vars, + cache_component_js, + cache_component_js_vars, + postprocess_component_html, +) from django_components.expression import Expression, RuntimeKwargs, safe_resolve_list from django_components.node import BaseNode from django_components.slots import ( @@ -92,6 +99,7 @@ _type = type @dataclass(frozen=True) class RenderInput(Generic[ArgsType, KwargsType, SlotsType]): + id: str context: Context args: ArgsType kwargs: KwargsType @@ -297,7 +305,6 @@ class Component( def __init__( self, registered_name: Optional[str] = None, - component_id: Optional[str] = None, outer_context: Optional[Context] = None, registry: Optional[ComponentRegistry] = None, # noqa F811 ): @@ -318,7 +325,6 @@ class Component( self.registered_name: Optional[str] = registered_name self.outer_context: Context = outer_context or Context() - self.component_id = component_id or gen_id() self.registry = registry or registry_ self._render_stack: Deque[RenderStackItem[ArgsType, KwargsType, SlotsType]] = deque() # None == uninitialized, False == No types, Tuple == types @@ -331,6 +337,19 @@ class Component( def name(self) -> str: return self.registered_name or self.__class__.__name__ + @property + def id(self) -> str: + """ + Render ID - This ID is unique for every time a `Component.render()` (or equivalent) is called. + + This is useful for logging or debugging. + + Render IDs have the chance of collision 1 in 3.3M. + + Raises RuntimeError if called outside of rendering execution. + """ + return self.input.id + @property def input(self) -> RenderInput[ArgsType, KwargsType, SlotsType]: """ @@ -702,6 +721,7 @@ class Component( self._render_stack.append( RenderStackItem( input=RenderInput( + id=gen_id(), context=context, slots=slots, args=args, @@ -716,9 +736,14 @@ class Component( context_data = self.get_context_data(*args, **kwargs) self._validate_outputs(data=context_data) - # Process JS and CSS files - cache_inlined_js(self.__class__, self.js or "") - cache_inlined_css(self.__class__, self.css or "") + # Process Component's JS and CSS + js_data: Dict = {} # TODO + cache_component_js(self.__class__) + js_input_hash = cache_component_js_vars(self.__class__, js_data) if js_data else None + + css_data: Dict = {} # TODO + cache_component_css(self.__class__) + css_input_hash = cache_component_css_vars(self.__class__, css_data) if css_data else None with _prepare_template(self, context, context_data) as template: # For users, we expose boolean variables that they may check @@ -763,8 +788,10 @@ class Component( output = postprocess_component_html( component_cls=self.__class__, - component_id=self.component_id, + component_id=self.id, html_content=html_content, + css_input_hash=css_input_hash, + js_input_hash=js_input_hash, type=type, render_dependencies=render_dependencies, ) @@ -951,7 +978,6 @@ class ComponentNode(BaseNode): component: Component = component_cls( registered_name=self.name, outer_context=context, - component_id=self.node_id, registry=self.registry, ) diff --git a/src/django_components/components/dynamic.py b/src/django_components/components/dynamic.py index c8e2126c..511f5872 100644 --- a/src/django_components/components/dynamic.py +++ b/src/django_components/components/dynamic.py @@ -113,7 +113,6 @@ class DynamicComponent(Component): # NOTE: Slots are passed at component instantiation comp = comp_class( registered_name=self.registered_name, - component_id=self.component_id, outer_context=self.outer_context, registry=self.registry, ) diff --git a/src/django_components/dependencies.py b/src/django_components/dependencies.py index 4710c478..8956b52d 100644 --- a/src/django_components/dependencies.py +++ b/src/django_components/dependencies.py @@ -11,11 +11,10 @@ from typing import ( TYPE_CHECKING, Callable, Dict, - Iterable, List, Literal, - NamedTuple, Optional, + Sequence, Set, Tuple, Type, @@ -35,7 +34,7 @@ from django.utils.decorators import sync_and_async_middleware from django.utils.safestring import SafeString, mark_safe from django_components.util.html import SoupNode -from django_components.util.misc import get_import_path +from django_components.util.misc import get_import_path, is_nonempty_str if TYPE_CHECKING: from django_components.component import Component @@ -112,16 +111,21 @@ def _hash_comp_cls(comp_cls: Type["Component"]) -> str: def _gen_cache_key( comp_cls_hash: str, script_type: ScriptType, + input_hash: Optional[str], ) -> str: - return f"__components:{comp_cls_hash}:{script_type}" + if input_hash: + return f"__components:{comp_cls_hash}:{script_type}:{input_hash}" + else: + return f"__components:{comp_cls_hash}:{script_type}" def _is_script_in_cache( comp_cls: Type["Component"], script_type: ScriptType, + input_hash: Optional[str], ) -> bool: comp_cls_hash = _hash_comp_cls(comp_cls) - cache_key = _gen_cache_key(comp_cls_hash, script_type) + cache_key = _gen_cache_key(comp_cls_hash, script_type, input_hash) return comp_media_cache.has(cache_key) @@ -129,6 +133,7 @@ def _cache_script( comp_cls: Type["Component"], script: str, script_type: ScriptType, + input_hash: Optional[str], ) -> None: """ Given a component and it's inlined JS or CSS, store the JS/CSS in a cache, @@ -138,7 +143,7 @@ def _cache_script( # E.g. `__components:MyButton:js:df7c6d10` if script_type in ("js", "css"): - cache_key = _gen_cache_key(comp_cls_hash, script_type) + cache_key = _gen_cache_key(comp_cls_hash, script_type, input_hash) else: raise ValueError(f"Unexpected script_type '{script_type}'") @@ -147,33 +152,115 @@ def _cache_script( comp_media_cache.set(cache_key, script.strip()) -def cache_inlined_js(comp_cls: Type["Component"], content: str) -> None: - if not _is_nonempty_str(comp_cls.js): - return +def cache_component_js(comp_cls: Type["Component"]) -> None: + """ + Cache the content from `Component.js`. This is the common JS that's shared + among all instances of the same component. So even if the component is rendered multiple + times, this JS is loaded only once. + """ + if not comp_cls.js or not is_nonempty_str(comp_cls.js) or _is_script_in_cache(comp_cls, "js", None): + return None - # Prepare the script that's common to all instances of the same component - # E.g. `my_table.js` - if not _is_script_in_cache(comp_cls, "js"): + _cache_script( + comp_cls=comp_cls, + script=comp_cls.js, + script_type="js", + input_hash=None, + ) + + +# NOTE: In CSS, we link the CSS vars to the component via a stylesheet that defines +# the CSS vars under `[data-djc-css-a1b2c3]`. Because of this we define the variables +# separately from the rest of the CSS definition. +# +# We use conceptually similar approach for JS, except in JS we have to manually associate +# the JS variables ("stylesheet") with the target HTML element ("component"). +# +# It involves 3 steps: +# 1. Register the common logic (equivalent to registering common CSS). +# with `Components.manager.registerComponent`. +# 2. Register the unique set of JS variables (equivalent to defining CSS vars) +# with `Components.manager.registerComponentData`. +# 3. Actually run a component's JS instance with `Components.manager.callComponent`, +# specifying the components HTML elements with `component_id`, and JS vars with `input_hash`. +def cache_component_js_vars(comp_cls: Type["Component"], js_vars: Dict) -> Optional[str]: + if not is_nonempty_str(comp_cls.js): + return None + + # The hash for the file that holds the JS variables is derived from the variables themselves. + json_data = json.dumps(js_vars) + input_hash = md5(json_data.encode()).hexdigest()[0:6] + + # Generate and cache a JS script that contains the JS variables. + if not _is_script_in_cache(comp_cls, "js", input_hash): _cache_script( comp_cls=comp_cls, - script=content, + script="", # TODO script_type="js", + input_hash=input_hash, ) + return input_hash -def cache_inlined_css(comp_cls: Type["Component"], content: str) -> None: - if not _is_nonempty_str(comp_cls.js): - return - # Prepare the script that's common to all instances of the same component - if not _is_script_in_cache(comp_cls, "css"): - # E.g. `my_table.css` +def wrap_component_js(comp_cls: Type["Component"], content: str) -> SafeString: + if "' end tag. " + "This is not allowed, as it would break the HTML." + ) + return f"" + + +def cache_component_css(comp_cls: Type["Component"]) -> None: + """ + Cache the content from `Component.css`. This is the common CSS that's shared + among all instances of the same component. So even if the component is rendered multiple + times, this CSS is loaded only once. + """ + if not comp_cls.css or not is_nonempty_str(comp_cls.css) or _is_script_in_cache(comp_cls, "css", None): + return None + + _cache_script( + comp_cls=comp_cls, + script=comp_cls.css, + script_type="css", + input_hash=None, + ) + + +# NOTE: In CSS, we link the CSS vars to the component via a stylesheet that defines +# the CSS vars under the CSS selector `[data-djc-css-a1b2c3]`. We define the stylesheet +# with variables separately from `Component.css`, because different instances may return different +# data from `get_css_data()`, which will live in different stylesheets. +def cache_component_css_vars(comp_cls: Type["Component"], css_vars: Dict) -> Optional[str]: + if not is_nonempty_str(comp_cls.css): + return None + + # The hash for the file that holds the CSS variables is derived from the variables themselves. + json_data = json.dumps(css_vars) + input_hash = md5(json_data.encode()).hexdigest()[0:6] + + # Generate and cache a CSS stylesheet that contains the CSS variables. + if not _is_script_in_cache(comp_cls, "css", input_hash): _cache_script( comp_cls=comp_cls, - script=content, + script="", # TODO script_type="css", + input_hash=input_hash, ) + return input_hash + + +def wrap_component_css(comp_cls: Type["Component"], content: str) -> SafeString: + if "' end tag. " + "This is not allowed, as it would break the HTML." + ) + return f"" + ######################################################### # 2. Modify the HTML to use the same IDs defined in previous @@ -183,16 +270,43 @@ def cache_inlined_css(comp_cls: Type["Component"], content: str) -> None: ######################################################### -class Dependencies(NamedTuple): - # NOTE: We pass around the component CLASS, so the dependencies logic is not - # dependent on ComponentRegistries - component_cls: Type["Component"] - component_id: str +def _link_dependencies_with_component_html( + component_id: str, + html_content: str, + css_input_hash: Optional[str], +) -> str: + elems = SoupNode.from_fragment(html_content) + + # Insert component ID + for elem in elems: + # Ignore comments, text, doctype, etc. + if not elem.is_element(): + continue + + # Component ID is used for executing JS script, e.g. `data-djc-id-a1b2c3` + # + # NOTE: We use `data-djc-css-a1b2c3` and `data-djc-id-a1b2c3` instead of + # `data-djc-css="a1b2c3"` and `data-djc-id="a1b2c3"`, to allow + # multiple values to be associated with the same element, which may happen if + # One component renders another. + elem.set_attr(f"data-djc-id-{component_id}", True) + + # Attribute by which we bind the CSS variables to the component's CSS, + # e.g. `data-djc-css-a1b2c3` + if css_input_hash: + elem.set_attr(f"data-djc-css-{css_input_hash}", True) + + return SoupNode.to_html_multiroot(elems) def _insert_component_comment( content: str, - deps: Dependencies, + # NOTE: We pass around the component CLASS, so the dependencies logic is not + # dependent on ComponentRegistries + component_cls: Type["Component"], + component_id: str, + js_input_hash: Optional[str], + css_input_hash: Optional[str], ) -> str: """ Given some textual content, prepend it with a short string that @@ -200,14 +314,14 @@ def _insert_component_comment( declared JS / CSS scripts. """ # Add components to the cache - comp_cls_hash = _hash_comp_cls(deps.component_cls) - comp_hash_mapping[comp_cls_hash] = deps.component_cls + comp_cls_hash = _hash_comp_cls(component_cls) + comp_hash_mapping[comp_cls_hash] = component_cls - data = f"{comp_cls_hash},{deps.component_id}" + data = f"{comp_cls_hash},{component_id},{js_input_hash or ''},{css_input_hash or ''}" # NOTE: It's important that we put the comment BEFORE the content, so we can # use the order of comments to evaluate components' instance JS code in the correct order. - output = mark_safe(COMPONENT_DEPS_COMMENT.format(data=data)) + content + output = mark_safe(COMPONENT_DEPS_COMMENT.format(data=data) + content) return output @@ -217,9 +331,18 @@ def postprocess_component_html( component_cls: Type["Component"], component_id: str, html_content: str, + css_input_hash: Optional[str], + js_input_hash: Optional[str], type: RenderType, render_dependencies: bool, ) -> str: + # Make the HTML work with JS and CSS dependencies + html_content = _link_dependencies_with_component_html( + component_id=component_id, + html_content=html_content, + css_input_hash=css_input_hash, + ) + # NOTE: To better understand the next section, consider this: # # We define and cache the component's JS and CSS at the same time as @@ -242,10 +365,10 @@ def postprocess_component_html( # scripts are associated with it. output = _insert_component_comment( html_content, - Dependencies( - component_cls=component_cls, - component_id=component_id, - ), + component_cls=component_cls, + component_id=component_id, + js_input_hash=js_input_hash, + css_input_hash=css_input_hash, ) if render_dependencies: @@ -266,21 +389,34 @@ def postprocess_component_html( TContent = TypeVar("TContent", bound=Union[bytes, str]) -CSS_DEPENDENCY_PLACEHOLDER = '' -JS_DEPENDENCY_PLACEHOLDER = '' - -CSS_PLACEHOLDER_BYTES = bytes(CSS_DEPENDENCY_PLACEHOLDER, encoding="utf-8") -JS_PLACEHOLDER_BYTES = bytes(JS_DEPENDENCY_PLACEHOLDER, encoding="utf-8") +CSS_PLACEHOLDER_NAME = "CSS_PLACEHOLDER" +CSS_PLACEHOLDER_NAME_B = CSS_PLACEHOLDER_NAME.encode() +JS_PLACEHOLDER_NAME = "JS_PLACEHOLDER" +JS_PLACEHOLDER_NAME_B = JS_PLACEHOLDER_NAME.encode() +CSS_DEPENDENCY_PLACEHOLDER = f'' +JS_DEPENDENCY_PLACEHOLDER = f'' COMPONENT_DEPS_COMMENT = "" -# E.g. `` -COMPONENT_COMMENT_REGEX = re.compile(rb"") -# E.g. `table,123` -SCRIPT_NAME_REGEX = re.compile(rb"^(?P[\w\-\./]+?),(?P[\w]+?)$") + +# E.g. `` +COMPONENT_COMMENT_REGEX = re.compile(rb"") +# E.g. `table,123,a92ef298,bd002c3` +# - comp_cls_hash - Cache key of the component class that was rendered +# - id - Component render ID +# - js - Cache key for the JS data from `get_js_data()` +# - css - Cache key for the CSS data from `get_css_data()` +SCRIPT_NAME_REGEX = re.compile( + rb"^(?P[\w\-\./]+?),(?P[\w]+?),(?P[0-9a-f]*?),(?P[0-9a-f]*?)$" +) +# E.g. `data-djc-id-a1b2c3` +MAYBE_COMP_ID = r"(?: data-djc-id-\w{6})?" +# E.g. `data-djc-css-99914b` +MAYBE_COMP_CSS_ID = r"(?: data-djc-css-\w{6})?" + PLACEHOLDER_REGEX = re.compile( r"{css_placeholder}|{js_placeholder}".format( - css_placeholder=CSS_DEPENDENCY_PLACEHOLDER, - js_placeholder=JS_DEPENDENCY_PLACEHOLDER, + css_placeholder=f'', + js_placeholder=f'', ).encode() ) @@ -350,10 +486,10 @@ def render_dependencies(content: TContent, type: RenderType = "document") -> TCo nonlocal did_find_css_placeholder nonlocal did_find_js_placeholder - if match[0] == CSS_PLACEHOLDER_BYTES: + if CSS_PLACEHOLDER_NAME_B in match[0]: replacement = css_replacement did_find_css_placeholder = True - elif match[0] == JS_PLACEHOLDER_BYTES: + elif JS_PLACEHOLDER_NAME_B in match[0]: replacement = js_replacement did_find_js_placeholder = True else: @@ -418,11 +554,11 @@ def _process_dep_declarations(content: bytes, type: RenderType) -> Tuple[bytes, Process a textual content that may include metadata on rendered components. The metadata has format like this - `` + `` E.g. - `` + `` """ # Extract all matched instances of `` while also removing them from the text all_parts: List[bytes] = list() @@ -436,23 +572,49 @@ def _process_dep_declarations(content: bytes, type: RenderType) -> Tuple[bytes, # NOTE: Python's set does NOT preserve order seen_comp_hashes: Set[str] = set() comp_hashes: List[str] = [] + # Used for passing Python vars to JS/CSS + inputs_data: List[Tuple[str, ScriptType, Optional[str]]] = [] + comp_data: List[Tuple[str, ScriptType, Optional[str]]] = [] - # Process individual parts. Each part is like a CSV row of `name,id`. + # Process individual parts. Each part is like a CSV row of `name,id,js,css`. # E.g. something like this: - # `table_10bac31,1234` + # `table_10bac31,1234,a92ef298,a92ef298` for part in all_parts: part_match = SCRIPT_NAME_REGEX.match(part) if not part_match: raise RuntimeError("Malformed dependencies data") - comp_cls_hash = part_match.group("comp_cls_hash").decode("utf-8") + comp_cls_hash: str = part_match.group("comp_cls_hash").decode("utf-8") + js_input_hash: Optional[str] = part_match.group("js").decode("utf-8") or None + css_input_hash: Optional[str] = part_match.group("css").decode("utf-8") or None + if comp_cls_hash in seen_comp_hashes: continue comp_hashes.append(comp_cls_hash) seen_comp_hashes.add(comp_cls_hash) + # Schedule to load the `' end tag. " - "This is not allowed, as it would break the HTML." - ) - return f"" - + content = wrap_component_js(comp_cls, content) elif script_type == "css": - if "' end tag. " - "This is not allowed, as it would break the HTML." - ) + content = wrap_component_css(comp_cls, content) + else: + raise ValueError(f"Unexpected script_type '{script_type}'") - return f"" - - return script + return content def get_script_url( script_type: ScriptType, comp_cls: Type["Component"], + input_hash: Optional[str], ) -> str: comp_cls_hash = _hash_comp_cls(comp_cls) @@ -693,6 +855,7 @@ def get_script_url( kwargs={ "comp_cls_hash": comp_cls_hash, "script_type": script_type, + **({"input_hash": input_hash} if input_hash is not None else {}), }, ) @@ -703,10 +866,11 @@ def _gen_exec_script( loaded_js_urls: List[str], loaded_css_urls: List[str], ) -> Optional[str]: - if not to_load_js_tags and not to_load_css_tags and not loaded_css_urls and not loaded_js_urls: + # Return None if all lists are empty + if not any([to_load_js_tags, to_load_css_tags, loaded_css_urls, loaded_js_urls]): return None - def map_to_base64(lst: List[str]) -> List[str]: + def map_to_base64(lst: Sequence[str]) -> List[str]: return [base64.b64encode(tag.encode()).decode() for tag in lst] # Generate JSON that will tell the JS dependency manager which JS and CSS to load @@ -797,14 +961,16 @@ def cached_script_view( req: HttpRequest, comp_cls_hash: str, script_type: ScriptType, + input_hash: Optional[str] = None, ) -> HttpResponse: if req.method != "GET": return HttpResponseNotAllowed(["GET"]) - # Otherwise check if the file is among the dynamically generated files in the cache - cache_key = _gen_cache_key(comp_cls_hash, script_type) - script = comp_media_cache.get(cache_key) + comp_cls = comp_hash_mapping.get(comp_cls_hash) + if comp_cls is None: + return HttpResponseNotFound() + script = get_script_content(script_type, comp_cls, input_hash) if script is None: return HttpResponseNotFound() @@ -813,7 +979,8 @@ def cached_script_view( urlpatterns = [ - # E.g. `/components/cache/table.js` + # E.g. `/components/cache/table.js` or `/components/cache/table.0ab2c3.js` + path("cache/..", cached_script_view, name=CACHE_ENDPOINT_NAME), path("cache/.", cached_script_view, name=CACHE_ENDPOINT_NAME), ] diff --git a/src/django_components/provide.py b/src/django_components/provide.py index 2c168353..f34133fb 100644 --- a/src/django_components/provide.py +++ b/src/django_components/provide.py @@ -8,7 +8,6 @@ from django_components.context import set_provided_context_var from django_components.expression import RuntimeKwargs from django_components.node import BaseNode from django_components.util.logger import trace_msg -from django_components.util.misc import gen_id PROVIDE_NAME_KWARG = "name" @@ -28,10 +27,7 @@ class ProvideNode(BaseNode): ): super().__init__(nodelist=nodelist, args=None, kwargs=kwargs, node_id=node_id) - self.nodelist = nodelist - self.node_id = node_id or gen_id() self.trace_id = trace_id - self.kwargs = kwargs or RuntimeKwargs({}) def __repr__(self) -> str: return f"" diff --git a/src/django_components/static/django_components/django_components.min.js b/src/django_components/static/django_components/django_components.min.js index 2b944672..89b7f3cf 100644 --- a/src/django_components/static/django_components/django_components.min.js +++ b/src/django_components/static/django_components/django_components.min.js @@ -1 +1 @@ -(()=>{var x=o=>new DOMParser().parseFromString(o,"text/html").documentElement.textContent,E=Array.isArray,m=o=>typeof o=="function",H=o=>o!==null&&typeof o=="object",S=o=>(H(o)||m(o))&&m(o.then)&&m(o.catch);function N(o,i){try{return i?o.apply(null,i):o()}catch(s){L(s)}}function g(o,i){if(m(o)){let s=N(o,i);return s&&S(s)&&s.catch(c=>{L(c)}),[s]}if(E(o)){let s=[];for(let c=0;c{let i=new MutationObserver(s=>{for(let c of s)c.type==="childList"&&c.addedNodes.forEach(p=>{p.nodeName==="SCRIPT"&&p.hasAttribute("data-djc")&&o(p)})});return i.observe(document,{childList:!0,subtree:!0}),i};var y=()=>{let o=new Set,i=new Set,s={},c={},p=t=>{let e=new DOMParser().parseFromString(t,"text/html").querySelector("script");if(!e)throw Error("[Components] Failed to extract '), 1) self.assertEqual(rendered_raw.count(" - Variable: foo + Variable: foo @@ -415,7 +422,7 @@ class RenderDependenciesTests(BaseTestCase): 1 - Variable: hi + Variable: hi @@ -471,37 +478,59 @@ class MiddlewareTests(BaseTestCase): request = Mock() self.assertEqual(response, middleware(request=request)) - def test_middleware_response_with_components_with_slash_dash_and_underscore( - self, - ): + def test_middleware_response_with_components_with_slash_dash_and_underscore(self): registry.register("dynamic", DynamicComponent) + registry.register("test-component", component=SimpleComponent) + registry.register("test/component", component=SimpleComponent) + registry.register("test_component", component=SimpleComponent) - component_names = [ - "test-component", - "test/component", - "test_component", - ] - for component_name in component_names: - registry.register(name=component_name, component=SimpleComponent) - template_str: types.django_html = """ - {% load component_tags %} - {% component_css_dependencies %} - {% component_js_dependencies %} - {% component "dynamic" is=component_name variable='value' / %} - """ - template = Template(template_str) - rendered = create_and_process_template_response( - template, context=Context({"component_name": component_name}) - ) + template_str: types.django_html = """ + {% load component_tags %} + {% component_css_dependencies %} + {% component_js_dependencies %} + {% component "dynamic" is=component_name variable='value' / %} + """ + template = Template(template_str) + def assert_dependencies(content: str): # Dependency manager script (empty) - self.assertInHTML('', rendered, count=1) + self.assertInHTML('', content, count=1) # Inlined JS - self.assertInHTML('', rendered, count=1) + self.assertInHTML('', content, count=1) # Inlined CSS - self.assertInHTML("", rendered, count=1) + self.assertInHTML("", content, count=1) # Media.css - self.assertInHTML('', rendered, count=1) + self.assertInHTML('', content, count=1) - self.assertEqual(rendered.count("Variable: value"), 1) + rendered1 = create_and_process_template_response( + template, + context=Context({"component_name": "test-component"}), + ) + + assert_dependencies(rendered1) + self.assertEqual( + rendered1.count('Variable: value'), + 1, + ) + + rendered2 = create_and_process_template_response( + template, + context=Context({"component_name": "test-component"}), + ) + assert_dependencies(rendered2) + self.assertEqual( + rendered2.count('Variable: value'), + 1, + ) + + rendered3 = create_and_process_template_response( + template, + context=Context({"component_name": "test_component"}), + ) + + assert_dependencies(rendered3) + self.assertEqual( + rendered3.count('Variable: value'), + 1, + ) diff --git a/tests/test_dependency_manager.py b/tests/test_dependency_manager.py index 21980718..6eb4d86d 100644 --- a/tests/test_dependency_manager.py +++ b/tests/test_dependency_manager.py @@ -215,7 +215,7 @@ class CallComponentTests(_BaseDepManagerTestCase): const inputHash = 'input-abc'; // Pretend that this HTML belongs to our component - document.body.insertAdjacentHTML('beforeend', '
abc
'); + document.body.insertAdjacentHTML('beforeend', '
abc
'); let captured = null; manager.registerComponent(compName, (data, ctx) => { @@ -248,7 +248,7 @@ class CallComponentTests(_BaseDepManagerTestCase): "hello": "world", }, "ctx": { - "els": ['
abc
'], + "els": ['
abc
'], "id": "12345", "name": "my_comp", }, @@ -269,7 +269,7 @@ class CallComponentTests(_BaseDepManagerTestCase): const inputHash = 'input-abc'; // Pretend that this HTML belongs to our component - document.body.insertAdjacentHTML('beforeend', '
abc
'); + document.body.insertAdjacentHTML('beforeend', '
abc
'); manager.registerComponent(compName, (data, ctx) => { return Promise.resolve(123); @@ -309,7 +309,7 @@ class CallComponentTests(_BaseDepManagerTestCase): const inputHash = 'input-abc'; // Pretend that this HTML belongs to our component - document.body.insertAdjacentHTML('beforeend', '
abc
'); + document.body.insertAdjacentHTML('beforeend', '
abc
'); manager.registerComponent(compName, (data, ctx) => { throw Error('Oops!'); @@ -343,7 +343,7 @@ class CallComponentTests(_BaseDepManagerTestCase): const inputHash = 'input-abc'; // Pretend that this HTML belongs to our component - document.body.insertAdjacentHTML('beforeend', '
abc
'); + document.body.insertAdjacentHTML('beforeend', '
abc
'); manager.registerComponent(compName, async (data, ctx) => { throw Error('Oops!'); @@ -408,7 +408,7 @@ class CallComponentTests(_BaseDepManagerTestCase): const compId = '12345'; const inputHash = 'input-abc'; - document.body.insertAdjacentHTML('beforeend', '
abc
'); + document.body.insertAdjacentHTML('beforeend', '
abc
'); manager.registerComponent(compName, (data, ctx) => { return Promise.resolve(123); @@ -434,7 +434,7 @@ class CallComponentTests(_BaseDepManagerTestCase): const compId = '12345'; const inputHash = 'input-abc'; - document.body.insertAdjacentHTML('beforeend', '
abc
'); + document.body.insertAdjacentHTML('beforeend', '
abc
'); manager.registerComponentData(compName, inputHash, () => { return { hello: 'world' }; diff --git a/tests/test_dependency_rendering.py b/tests/test_dependency_rendering.py index 9875d8b1..7b8b5bc9 100644 --- a/tests/test_dependency_rendering.py +++ b/tests/test_dependency_rendering.py @@ -5,7 +5,7 @@ During actual rendering, the HTML is then picked up by the JS-side dependency ma import re -from django.template import Template +from django.template import Context, Template from django_components import Component, registry, types @@ -456,8 +456,8 @@ class DependencyRenderingTests(BaseTestCase): # `c3R5bGUyLmNzcw==` -> `style2.css` # `eHl6MS5jc3M=` -> `xyz1.css` # `L2NvbXBvbmVudHMvY2FjaGUvT3RoZXJDb21wb25lbnRfNjMyOWFlLmNzcw==` -> `/components/cache/OtherComponent_6329ae.css` - # `L2NvbXBvbmVudHMvY2FjaGUvU2ltcGxlQ29tcG9uZW50TmVzdGVkX2YwMmQzMi5jc3M=` -> `/components/cache/SimpleComponentNested_f02d32.css` # `L2NvbXBvbmVudHMvY2FjaGUvT3RoZXJDb21wb25lbnRfNjMyOWFlLmpz` -> `/components/cache/OtherComponent_6329ae.js` + # `L2NvbXBvbmVudHMvY2FjaGUvU2ltcGxlQ29tcG9uZW50TmVzdGVkX2YwMmQzMi5jc3M=` -> `/components/cache/SimpleComponentNested_f02d32.css` # `L2NvbXBvbmVudHMvY2FjaGUvU2ltcGxlQ29tcG9uZW50TmVzdGVkX2YwMmQzMi5qcw==` -> `/components/cache/SimpleComponentNested_f02d32.js` # `c2NyaXB0Lmpz` -> `script.js` # `c2NyaXB0Mi5qcw==` -> `script2.js` @@ -465,8 +465,16 @@ class DependencyRenderingTests(BaseTestCase): self.assertInHTML( """ @@ -491,3 +499,131 @@ class DependencyRenderingTests(BaseTestCase): template = Template(template_str) rendered = create_and_process_template_response(template) self.assertNotIn("_RENDERED", rendered) + + def test_adds_component_id_html_attr_single(self): + registry.register(name="test", component=SimpleComponent) + + template_str: types.django_html = """ + {% load component_tags %} + {% component 'test' variable='foo' / %} + """ + template = Template(template_str) + rendered = create_and_process_template_response(template) + + self.assertHTMLEqual(rendered, "Variable: foo") + + def test_adds_component_id_html_attr_single_multiroot(self): + class SimpleMultiroot(SimpleComponent): + template: types.django_html = """ + Variable: {{ variable }} + Variable2:
{{ variable }}
+ Variable3: {{ variable }} + """ + + registry.register(name="test", component=SimpleMultiroot) + + template_str: types.django_html = """ + {% load component_tags %} + {% component 'test' variable='foo' / %} + """ + template = Template(template_str) + rendered = create_and_process_template_response(template) + + self.assertHTMLEqual( + rendered, + """ + Variable: foo + Variable2:
foo
+ Variable3: foo + """, + ) + + # Test that, if multiple components share the same root HTML elements, + # then those elemens will have the `data-djc-id-` attribute added for each component. + def test_adds_component_id_html_attr_nested(self): + class SimpleMultiroot(SimpleComponent): + template: types.django_html = """ + Variable: {{ variable }} + Variable2:
{{ variable }}
+ Variable3: {{ variable }} + """ + + class SimpleOuter(SimpleComponent): + template: types.django_html = """ + {% load component_tags %} + {% component 'multiroot' variable='foo' / %} +
Another
+ """ + + registry.register(name="multiroot", component=SimpleMultiroot) + registry.register(name="outer", component=SimpleOuter) + + template_str: types.django_html = """ + {% load component_tags %} + {% component 'outer' variable='foo' / %} + """ + template = Template(template_str) + rendered = create_and_process_template_response(template) + + self.assertHTMLEqual( + rendered, + """ + Variable: foo + Variable2:
foo
+ Variable3: foo +
Another
+ """, + ) + + # `data-djc-id-` attribute should be added on each instance in the RESULTING HTML. + # So if in a loop, each iteration creates a new component, and each of those should + # have a unique `data-djc-id-` attribute. + def test_adds_component_id_html_attr_loops(self): + class SimpleMultiroot(SimpleComponent): + template: types.django_html = """ + Variable: {{ variable }} + Variable2:
{{ variable }}
+ Variable3: {{ variable }} + """ + + class SimpleOuter(SimpleComponent): + template: types.django_html = """ + {% load component_tags %} + {% component 'multiroot' variable='foo' / %} +
Another
+ """ + + registry.register(name="multiroot", component=SimpleMultiroot) + registry.register(name="outer", component=SimpleOuter) + + template_str: types.django_html = """ + {% load component_tags %} + {% for i in lst %} + {% component 'outer' variable='foo' / %} + {% endfor %} + """ + template = Template(template_str) + rendered = create_and_process_template_response( + template, + context=Context({"lst": range(3)}), + ) + + self.assertHTMLEqual( + rendered, + """ + Variable: foo + Variable2:
foo
+ Variable3: foo +
Another
+ + Variable: foo + Variable2:
foo
+ Variable3: foo +
Another
+ + Variable: foo + Variable2:
foo
+ Variable3: foo +
Another
+ """, + ) diff --git a/tests/test_dependency_rendering_e2e.py b/tests/test_dependency_rendering_e2e.py index d016a7c1..127407fe 100644 --- a/tests/test_dependency_rendering_e2e.py +++ b/tests/test_dependency_rendering_e2e.py @@ -3,6 +3,8 @@ Here we check that all parts of managing JS and CSS dependencies work together in an actual browser. """ +import re + from playwright.async_api import Page from django_components import types @@ -44,7 +46,10 @@ class E2eDependencyRenderingTests(BaseTestCase): data = await page.evaluate(test_js) # Check that the actual HTML content was loaded - self.assertIn('Variable: foo', data["bodyHTML"]) + self.assertRegex( + data["bodyHTML"], + re.compile(r'Variable: foo'), + ) self.assertInHTML('
123
', data["bodyHTML"], count=1) self.assertInHTML('
xyz
', data["bodyHTML"], count=1) @@ -106,17 +111,34 @@ class E2eDependencyRenderingTests(BaseTestCase): data = await page.evaluate(test_js) # Check that the actual HTML content was loaded - self.assertInHTML( - """ -
- Variable: variable - XYZ: variable_inner -
-
123
-
xyz
- """, + self.assertRegex( data["bodyHTML"], - count=1, + #
+ # Variable: + # + # variable + # + # XYZ: + # + # variable_inner + # + #
+ #
123
+ #
xyz
+ re.compile( + r'
\s*' + r"Variable:\s*" + r'\s*' + r"variable\s*" + r"<\/strong>\s*" + r"XYZ:\s*" + r'\s*' + r"variable_inner\s*" + r"<\/strong>\s*" + r"<\/div>\s*" + r'
123<\/div>\s*' + r'
xyz<\/div>\s*' + ), ) # Check components' inlined JS got loaded @@ -183,17 +205,34 @@ class E2eDependencyRenderingTests(BaseTestCase): data = await page.evaluate(test_js) # Check that the actual HTML content was loaded - self.assertInHTML( - """ -
- Variable: variable - XYZ: variable_inner -
-
123
-
xyz
- """, + self.assertRegex( data["bodyHTML"], - count=1, + #
+ # Variable: + # + # variable + # + # XYZ: + # + # variable_inner + # + #
+ #
123
+ #
xyz
+ re.compile( + r'
\s*' + r"Variable:\s*" + r'\s*' + r"variable\s*" + r"<\/strong>\s*" + r"XYZ:\s*" + r'\s*' + r"variable_inner\s*" + r"<\/strong>\s*" + r"<\/div>\s*" + r'
123<\/div>\s*' + r'
xyz<\/div>\s*' + ), ) # Check components' inlined JS did NOT get loaded @@ -342,7 +381,12 @@ class E2eDependencyRenderingTests(BaseTestCase): data = await page.evaluate(test_js) self.assertEqual(data["targetHtml"], None) - self.assertHTMLEqual('
123 xxx
', data["fragHtml"]) + self.assertRegex( + data["fragHtml"], + re.compile( + r'
\s*' r"123\s*" r'xxx\s*' r"
" + ), + ) self.assertIn("rgb(0, 0, 255)", data["fragBg"]) # AKA 'background: blue' await page.close() @@ -390,7 +434,12 @@ class E2eDependencyRenderingTests(BaseTestCase): data = await page.evaluate(test_js) self.assertEqual(data["targetHtml"], None) - self.assertHTMLEqual('
123 xxx
', data["fragHtml"]) + self.assertRegex( + data["fragHtml"], + re.compile( + r'
\s*' r"123\s*" r'xxx\s*' r"
" + ), + ) self.assertIn("rgb(0, 0, 255)", data["fragBg"]) # AKA 'background: blue' await page.close() @@ -441,11 +490,12 @@ class E2eDependencyRenderingTests(BaseTestCase): # NOTE: Unlike the vanilla JS tests, for the Alpine test we don't remove the targetHtml, # but only change its contents. - self.assertInHTML( - '
123 xxx
', + self.assertRegex( data["targetHtml"], + re.compile( + r'
\s*' r"123\s*" r'xxx\s*' r"
" + ), ) - self.assertHTMLEqual(data["fragHtml"], '
123 xxx
') self.assertIn("rgb(0, 0, 255)", data["fragBg"]) # AKA 'background: blue' await page.close() @@ -497,9 +547,9 @@ class E2eDependencyRenderingTests(BaseTestCase): self.assertEqual(data["targetHtml"], None) # NOTE: We test only the inner HTML, because the element itself may or may not have # extra CSS classes added by HTMX, which results in flaky tests. - self.assertHTMLEqual( + self.assertRegex( data["fragInnerHtml"], - '123 xxx', + re.compile(r'123\s*xxx'), ) self.assertIn("rgb(0, 0, 255)", data["fragBg"]) # AKA 'background: blue' diff --git a/tests/test_expression.py b/tests/test_expression.py index 065b895d..f3ac9299 100644 --- a/tests/test_expression.py +++ b/tests/test_expression.py @@ -150,7 +150,7 @@ class DynamicExprTests(BaseTestCase): self.assertEqual( rendered.strip(), - "\n
lorem
\n
True
\n
[{'a': 1}, {'a': 2}]
", # noqa: E501 + "\n
lorem
\n
True
\n
[{'a': 1}, {'a': 2}]
", # noqa: E501 ) @parametrize_context_behavior(["django", "isolated"]) @@ -220,7 +220,13 @@ class DynamicExprTests(BaseTestCase): self.assertEqual( rendered.strip(), - "\n
lorem ipsum dolor
\n
True
\n
[{'a': 1}, {'a': 2}]
\n
{'a': 3}
", # noqa E501 + ( + "\n" + "
lorem ipsum dolor
\n" + "
True
\n" + "
[{'a': 1}, {'a': 2}]
\n" + "
{'a': 3}
" + ), ) @parametrize_context_behavior(["django", "isolated"]) @@ -290,7 +296,13 @@ class DynamicExprTests(BaseTestCase): self.assertEqual( rendered.strip(), - "\n
\n
abc
\n
\n
", # noqa E501 + ( + "\n" + "
\n" + "
abc
\n" + "
\n" + "
" + ), ) @parametrize_context_behavior(["django", "isolated"]) @@ -364,7 +376,14 @@ class DynamicExprTests(BaseTestCase): self.assertEqual( rendered.strip(), - "\n
lorem ipsum dolor
\n
lorem ipsum dolor [{'a': 1}]
\n
True
\n
[{'a': 1}, {'a': 2}]
\n
{'a': 3}
", # noqa E501 + ( + "\n" + "
lorem ipsum dolor
\n" + "
lorem ipsum dolor [{'a': 1}]
\n" + "
True
\n" + "
[{'a': 1}, {'a': 2}]
\n" + "
{'a': 3}
" + ), ) @parametrize_context_behavior(["django", "isolated"]) @@ -408,7 +427,12 @@ class DynamicExprTests(BaseTestCase): self.assertEqual( rendered.strip(), - '\n
"
\n
{%}
\n
True
', # noqa: E501 + ( + "\n" + '
"
\n' + "
{%}
\n" + "
True
" + ), ) @parametrize_context_behavior(["django", "isolated"]) @@ -457,7 +481,14 @@ class DynamicExprTests(BaseTestCase): self.assertEqual( rendered.strip(), - "\n
\n
3
\n
True
\n
\n
True
", # noqa E501 + ( + "\n" + "
\n" + '
3
\n' + '
True
\n' + "
\n" + "
True
" + ), ) @@ -528,11 +559,11 @@ class SpreadOperatorTests(BaseTestCase): self.assertHTMLEqual( rendered, """ -
LoREM
-
{'@click': '() => {}', 'style': 'height: 20px'}
-
[1, 2, 3]
-
1
-
123
+
LoREM
+
{'@click': '() => {}', 'style': 'height: 20px'}
+
[1, 2, 3]
+
1
+
123
""", ) @@ -665,9 +696,9 @@ class SpreadOperatorTests(BaseTestCase): self.assertHTMLEqual( rendered, """ -
{'@click': '() => {}', 'style': 'height: 20px'}
-
[1, 2, 3]
-
1
+
{'@click': '() => {}', 'style': 'height: 20px'}
+
[1, 2, 3]
+
1
""", ) @@ -748,10 +779,10 @@ class SpreadOperatorTests(BaseTestCase): self.assertHTMLEqual( rendered, """ -
{'@click': '() => {}', 'style': 'OVERWRITTEN'}
-
[1, 2, 3]
-
1
-
OVERWRITTEN_X
+
{'@click': '() => {}', 'style': 'OVERWRITTEN'}
+
[1, 2, 3]
+
1
+
OVERWRITTEN_X
""", ) diff --git a/tests/test_registry.py b/tests/test_registry.py index 1f8e8511..bbb0a37f 100644 --- a/tests/test_registry.py +++ b/tests/test_registry.py @@ -203,11 +203,11 @@ class MultipleComponentRegistriesTest(BaseTestCase): self.assertHTMLEqual( rendered, """ - Variable: 123 + Variable: 123 Slot: SLOT 123 - Variable: 123 + Variable: 123 Slot: SLOT ABC """, diff --git a/tests/test_tag_formatter.py b/tests/test_tag_formatter.py index 0d6c769e..2b59298d 100644 --- a/tests/test_tag_formatter.py +++ b/tests/test_tag_formatter.py @@ -68,7 +68,7 @@ class ComponentTagTests(BaseTestCase): rendered, """ hello1 -
+
SLOT_DEFAULT
hello2 @@ -101,7 +101,7 @@ class ComponentTagTests(BaseTestCase): rendered, """ hello1 -
+
OVERRIDEN!
hello2 @@ -139,7 +139,7 @@ class ComponentTagTests(BaseTestCase): rendered, """ hello1 -
+
SLOT_DEFAULT
hello2 @@ -179,7 +179,7 @@ class ComponentTagTests(BaseTestCase): rendered, """ hello1 -
+
OVERRIDEN!
hello2 @@ -217,7 +217,7 @@ class ComponentTagTests(BaseTestCase): rendered, """ hello1 -
+
SLOT_DEFAULT
hello2 @@ -257,7 +257,7 @@ class ComponentTagTests(BaseTestCase): rendered, """ hello1 -
+
OVERRIDEN!
hello2 @@ -297,7 +297,7 @@ class ComponentTagTests(BaseTestCase): rendered, """ hello1 -
+
OVERRIDEN!
hello2 @@ -334,7 +334,7 @@ class ComponentTagTests(BaseTestCase): self.assertHTMLEqual( rendered, """ -
+
OVERRIDEN!
""", @@ -419,7 +419,7 @@ class ComponentTagTests(BaseTestCase): rendered, """ hello1 -
+
SLOT_DEFAULT
hello2 @@ -439,7 +439,7 @@ class ComponentTagTests(BaseTestCase): rendered, """ hello1 -
+
OVERRIDEN!
hello2 diff --git a/tests/test_templatetags.py b/tests/test_templatetags.py index 65bbfbf7..dc9f5b71 100644 --- a/tests/test_templatetags.py +++ b/tests/test_templatetags.py @@ -120,7 +120,7 @@ class MultilineTagsTests(BaseTestCase): """ rendered = Template(template).render(Context()) expected = """ - Variable: 123 + Variable: 123 """ self.assertHTMLEqual(rendered, expected) @@ -147,7 +147,7 @@ class NestedTagsTests(BaseTestCase): """ rendered = Template(template).render(Context()) expected = """ - Variable: lorem + Variable: lorem """ self.assertHTMLEqual(rendered, expected) @@ -161,7 +161,7 @@ class NestedTagsTests(BaseTestCase): """ rendered = Template(template).render(Context()) expected = """ - Variable: organisation's + Variable: organisation's """ self.assertHTMLEqual(rendered, expected) @@ -175,7 +175,7 @@ class NestedTagsTests(BaseTestCase): """ rendered = Template(template).render(Context()) expected = """ - Variable: organisation's + Variable: organisation's """ self.assertHTMLEqual(rendered, expected) @@ -189,7 +189,7 @@ class NestedTagsTests(BaseTestCase): """ rendered = Template(template).render(Context()) expected = """ - Variable: organisation"s + Variable: organisation"s """ self.assertHTMLEqual(rendered, expected) @@ -203,6 +203,6 @@ class NestedTagsTests(BaseTestCase): """ rendered = Template(template).render(Context()) expected = """ - Variable: organisation"s + Variable: organisation"s """ self.assertHTMLEqual(rendered, expected) diff --git a/tests/test_templatetags_component.py b/tests/test_templatetags_component.py index 2a6132ce..9260d37d 100644 --- a/tests/test_templatetags_component.py +++ b/tests/test_templatetags_component.py @@ -1,5 +1,3 @@ -import textwrap - from django.template import Context, Template, TemplateSyntaxError from django_components import AlreadyRegistered, Component, NotRegistered, register, registry, types @@ -60,7 +58,7 @@ class ComponentTemplateTagTest(BaseTestCase): template = Template(simple_tag_template) rendered = template.render(Context({})) - self.assertHTMLEqual(rendered, "Variable: variable\n") + self.assertHTMLEqual(rendered, "Variable: variable\n") @parametrize_context_behavior(["django", "isolated"]) def test_single_component_self_closing(self): @@ -73,7 +71,7 @@ class ComponentTemplateTagTest(BaseTestCase): template = Template(simple_tag_template) rendered = template.render(Context({})) - self.assertHTMLEqual(rendered, "Variable: variable\n") + self.assertHTMLEqual(rendered, "Variable: variable\n") @parametrize_context_behavior(["django", "isolated"]) def test_call_with_invalid_name(self): @@ -99,7 +97,7 @@ class ComponentTemplateTagTest(BaseTestCase): template = Template(simple_tag_template) rendered = template.render(Context({})) - self.assertHTMLEqual(rendered, "Variable: variable\n") + self.assertHTMLEqual(rendered, "Variable: variable\n") @parametrize_context_behavior(["django", "isolated"]) def test_call_component_with_two_variables(self): @@ -129,8 +127,13 @@ class ComponentTemplateTagTest(BaseTestCase): template = Template(simple_tag_template) rendered = template.render(Context({})) - expected_outcome = """Variable: variable\n""" """Variable2: hej""" - self.assertHTMLEqual(rendered, textwrap.dedent(expected_outcome)) + self.assertHTMLEqual( + rendered, + """ + Variable: variable + Variable2: hej + """, + ) @parametrize_context_behavior(["django", "isolated"]) def test_component_called_with_singlequoted_name(self): @@ -143,7 +146,7 @@ class ComponentTemplateTagTest(BaseTestCase): template = Template(simple_tag_template) rendered = template.render(Context({})) - self.assertHTMLEqual(rendered, "Variable: variable\n") + self.assertHTMLEqual(rendered, "Variable: variable\n") @parametrize_context_behavior(["django", "isolated"]) def test_raises_on_component_called_with_variable_as_name(self): @@ -183,7 +186,10 @@ class ComponentTemplateTagTest(BaseTestCase): rendered = template.render(Context({})) self.assertHTMLEqual( rendered, - "Provided variable: provided value\nDefault:

default text

", + """ + Provided variable: provided value + Default:

default text

+ """, ) @@ -222,7 +228,10 @@ class DynamicComponentTemplateTagTest(BaseTestCase): template = Template(simple_tag_template) rendered = template.render(Context({})) - self.assertHTMLEqual(rendered, "Variable: variable\n") + self.assertHTMLEqual( + rendered, + "Variable: variable", + ) @parametrize_context_behavior(["django", "isolated"]) def test_call_with_invalid_name(self): @@ -250,7 +259,10 @@ class DynamicComponentTemplateTagTest(BaseTestCase): template = Template(simple_tag_template) rendered = template.render(Context({})) - self.assertHTMLEqual(rendered, "Variable: variable\n") + self.assertHTMLEqual( + rendered, + "Variable: variable", + ) @parametrize_context_behavior(["django", "isolated"]) def test_component_called_with_variable_as_spread(self): @@ -272,7 +284,10 @@ class DynamicComponentTemplateTagTest(BaseTestCase): } ) ) - self.assertHTMLEqual(rendered, "Variable: variable\n") + self.assertHTMLEqual( + rendered, + "Variable: variable", + ) @parametrize_context_behavior(["django", "isolated"]) def test_component_as_class(self): @@ -291,7 +306,10 @@ class DynamicComponentTemplateTagTest(BaseTestCase): } ) ) - self.assertHTMLEqual(rendered, "Variable: variable\n") + self.assertHTMLEqual( + rendered, + "Variable: variable", + ) @parametrize_context_behavior( ["django", "isolated"], @@ -316,7 +334,7 @@ class DynamicComponentTemplateTagTest(BaseTestCase): template = Template(simple_tag_template) rendered = template.render(Context({})) - self.assertHTMLEqual(rendered, "Variable: variable\n") + self.assertHTMLEqual(rendered, "Variable: variable\n") @parametrize_context_behavior( ["django", "isolated"], @@ -342,7 +360,10 @@ class DynamicComponentTemplateTagTest(BaseTestCase): template = Template(simple_tag_template) rendered = template.render(Context({})) - self.assertHTMLEqual(rendered, "Variable: variable\n") + self.assertHTMLEqual( + rendered, + "Variable: variable", + ) @parametrize_context_behavior(["django", "isolated"]) def test_raises_already_registered_on_name_conflict(self): @@ -380,7 +401,7 @@ class DynamicComponentTemplateTagTest(BaseTestCase): self.assertHTMLEqual( rendered, """ - Variable: variable + Variable: variable Slot: HELLO_FROM_SLOT """, ) @@ -422,7 +443,7 @@ class DynamicComponentTemplateTagTest(BaseTestCase): self.assertHTMLEqual( rendered, """ - Variable: variable + Variable: variable Slot 1: HELLO_FROM_SLOT_1 Slot 2: HELLO_FROM_SLOT_2 """, @@ -465,7 +486,7 @@ class DynamicComponentTemplateTagTest(BaseTestCase): self.assertHTMLEqual( rendered, """ - Variable: variable + Variable: variable Slot 1: HELLO_FROM_SLOT_1 Slot 2: """, @@ -488,75 +509,149 @@ class DynamicComponentTemplateTagTest(BaseTestCase): class MultiComponentTests(BaseTestCase): - def register_components(self): + @parametrize_context_behavior(["django", "isolated"]) + def test_both_components_render_correctly_with_no_slots(self): registry.register("first_component", SlottedComponent) registry.register("second_component", SlottedComponentWithContext) - def make_template(self, first_slot: str = "", second_slot: str = "") -> Template: - template_str: types.django_html = f""" - {{% load component_tags %}} - {{% component 'first_component' %}} - {first_slot} - {{% endcomponent %}} - {{% component 'second_component' variable='xyz' %}} - {second_slot} - {{% endcomponent %}} + template_str: types.django_html = """ + {% load component_tags %} + {% component 'first_component' %} + {% endcomponent %} + {% component 'second_component' variable='xyz' %} + {% endcomponent %} """ - return Template(template_str) + template = Template(template_str) + rendered = template.render(Context()) - def expected_result(self, first_slot: str = "", second_slot: str = "") -> str: - first_slot = first_slot or "Default header" - second_slot = second_slot or "Default header" - return f""" - -
{first_slot}
+ self.assertHTMLEqual( + rendered, + """ + +
+ Default header +
Default main
Default footer
- -
{second_slot}
+ +
+ Default header +
Default main
Default footer
- """ - - def wrap_with_slot_tags(self, s): - return '{% fill "header" %}' + s + "{% endfill %}" - - @parametrize_context_behavior(["django", "isolated"]) - def test_both_components_render_correctly_with_no_slots(self): - self.register_components() - rendered = self.make_template().render(Context({})) - self.assertHTMLEqual(rendered, self.expected_result()) + """, + ) @parametrize_context_behavior(["django", "isolated"]) def test_both_components_render_correctly_with_slots(self): - self.register_components() - first_slot_content = "

Slot #1

" - second_slot_content = "
Slot #2
" - first_slot = self.wrap_with_slot_tags(first_slot_content) - second_slot = self.wrap_with_slot_tags(second_slot_content) - rendered = self.make_template(first_slot, second_slot).render(Context({})) + registry.register("first_component", SlottedComponent) + registry.register("second_component", SlottedComponentWithContext) + + template_str: types.django_html = """ + {% load component_tags %} + {% component 'first_component' %} + {% fill "header" %}

Slot #1

{% endfill %} + {% endcomponent %} + {% component 'second_component' variable='xyz' %} + {% fill "header" %}
Slot #2
{% endfill %} + {% endcomponent %} + """ + template = Template(template_str) + rendered = template.render(Context()) + self.assertHTMLEqual( rendered, - self.expected_result(first_slot_content, second_slot_content), + """ + +
+

Slot #1

+
+
Default main
+
Default footer
+
+ +
+
Slot #2
+
+
Default main
+
Default footer
+
+ """, ) @parametrize_context_behavior(["django", "isolated"]) def test_both_components_render_correctly_when_only_first_has_slots(self): - self.register_components() - first_slot_content = "

Slot #1

" - first_slot = self.wrap_with_slot_tags(first_slot_content) - rendered = self.make_template(first_slot).render(Context({})) - self.assertHTMLEqual(rendered, self.expected_result(first_slot_content)) + registry.register("first_component", SlottedComponent) + registry.register("second_component", SlottedComponentWithContext) + + template_str: types.django_html = """ + {% load component_tags %} + {% component 'first_component' %} + {% fill "header" %}

Slot #1

{% endfill %} + {% endcomponent %} + {% component 'second_component' variable='xyz' %} + {% endcomponent %} + """ + template = Template(template_str) + rendered = template.render(Context({})) + + self.assertHTMLEqual( + rendered, + """ + +
+

Slot #1

+
+
Default main
+
Default footer
+
+ +
+ Default header +
+
Default main
+
Default footer
+
+ """, + ) @parametrize_context_behavior(["django", "isolated"]) def test_both_components_render_correctly_when_only_second_has_slots(self): - self.register_components() - second_slot_content = "
Slot #2
" - second_slot = self.wrap_with_slot_tags(second_slot_content) - rendered = self.make_template("", second_slot).render(Context({})) - self.assertHTMLEqual(rendered, self.expected_result("", second_slot_content)) + registry.register("first_component", SlottedComponent) + registry.register("second_component", SlottedComponentWithContext) + + template_str: types.django_html = """ + {% load component_tags %} + {% component 'first_component' %} + {% endcomponent %} + {% component 'second_component' variable='xyz' %} + {% fill "header" %}
Slot #2
{% endfill %} + {% endcomponent %} + """ + template = Template(template_str) + rendered = template.render(Context({})) + + self.assertHTMLEqual( + rendered, + """ + +
+ Default header +
+
Default main
+
Default footer
+
+ +
+
Slot #2
+
+
Default main
+
Default footer
+
+ """, + ) class ComponentIsolationTests(BaseTestCase): @@ -596,17 +691,17 @@ class ComponentIsolationTests(BaseTestCase): self.assertHTMLEqual( rendered, """ - +
Override header
Default main
Default footer
- +
Default header
Override main
Default footer
- +
Default header
Default main
Override footer
@@ -641,7 +736,7 @@ class AggregateInputTests(BaseTestCase): self.assertHTMLEqual( rendered, """ -
+
attrs: {'@click.stop': "dispatch('click_event')", 'x-data': "{hello: 'world'}", 'class': 'padding-top-8'} my_dict: {'one': 2}
diff --git a/tests/test_templatetags_extends.py b/tests/test_templatetags_extends.py index 8c6eb009..1479cf08 100644 --- a/tests/test_templatetags_extends.py +++ b/tests/test_templatetags_extends.py @@ -49,14 +49,15 @@ class ExtendsCompatTests(BaseTestCase): {% endblock %} """ rendered = Template(template).render(Context()) + expected = """
-
BLOCK OVERRIDEN
- +
BLOCK OVERRIDEN
+
SLOT OVERRIDEN
Default main
Default footer
@@ -105,14 +106,14 @@ class ExtendsCompatTests(BaseTestCase):
-
BLOCK OVERRIDEN
- +
BLOCK OVERRIDEN
+
SLOT OVERRIDEN
Default main
Default footer
-
BLOCK OVERRIDEN
- +
BLOCK OVERRIDEN
+
SLOT OVERRIDEN 2
Default main
Default footer
@@ -171,14 +172,14 @@ class ExtendsCompatTests(BaseTestCase):
-
BLOCK OVERRIDEN
- +
BLOCK OVERRIDEN
+
SLOT OVERRIDEN
Default main
Default footer
-
BLOCK OVERRIDEN
- +
BLOCK OVERRIDEN
+
SLOT OVERRIDEN 2
Default main
Default footer
@@ -230,20 +231,21 @@ class ExtendsCompatTests(BaseTestCase): {% endblock %} """ rendered = Template(template).render(Context()) + expected = """
-
BLOCK OVERRIDEN
- +
BLOCK OVERRIDEN
+
SLOT OVERRIDEN
Default main
Default footer
-
BLOCK OVERRIDEN
- +
BLOCK OVERRIDEN
+
SLOT OVERRIDEN 2
Default main
Default footer
@@ -287,8 +289,8 @@ class ExtendsCompatTests(BaseTestCase): -
BLOCK OVERRIDEN
- +
BLOCK OVERRIDEN
+
SLOT OVERRIDEN
Default main
Default footer
@@ -335,14 +337,14 @@ class ExtendsCompatTests(BaseTestCase): -
BLOCK OVERRIDEN
- +
BLOCK OVERRIDEN
+
SLOT OVERRIDEN
Default main
Default footer
-
BLOCK OVERRIDEN
- +
BLOCK OVERRIDEN
+
SLOT OVERRIDEN 2
Default main
Default footer
@@ -389,11 +391,11 @@ class ExtendsCompatTests(BaseTestCase):
- +
Default header
-
BLOCK OVERRIDEN
- +
BLOCK OVERRIDEN
+
SLOT OVERRIDEN
Default main
Default footer
@@ -435,7 +437,7 @@ class ExtendsCompatTests(BaseTestCase):
Variable: - Variable: + Variable:
@@ -469,9 +471,9 @@ class ExtendsCompatTests(BaseTestCase): rendered = Template(template).render(Context()) expected = """ - + - +
BODY_FROM_FILL
Default footer
@@ -502,9 +504,9 @@ class ExtendsCompatTests(BaseTestCase): rendered = Template(template).render(Context()) expected = """ - + - +
BODY_FROM_FILL
Default footer
@@ -537,7 +539,7 @@ class ExtendsCompatTests(BaseTestCase):
- +
TEST
@@ -567,7 +569,7 @@ class ExtendsCompatTests(BaseTestCase): - +
58 giraffes and 2 pantaloons
@@ -594,9 +596,9 @@ class ExtendsCompatTests(BaseTestCase): rendered = Template(template).render(Context()) expected = """ - + - +
58 giraffes and 2 pantaloons
@@ -634,9 +636,9 @@ class ExtendsCompatTests(BaseTestCase): rendered = Template(template).render(Context()) expected = """ - + - +
BODY_FROM_FILL
Default footer
@@ -664,9 +666,9 @@ class ExtendsCompatTests(BaseTestCase): rendered = Template(template).render(Context()) expected = """ - + - +
Helloodiddoo @@ -700,9 +702,9 @@ class ExtendsCompatTests(BaseTestCase): rendered = Template(template).render(Context()) expected = """ - + - +
Helloodiddoo @@ -736,9 +738,9 @@ class ExtendsCompatTests(BaseTestCase): rendered = Template(template).render(Context()) expected = """ - + - +
Helloodiddoo @@ -782,9 +784,9 @@ class ExtendsCompatTests(BaseTestCase): rendered = Template(template).render(Context()) expected = """ - + - +
Helloodiddoo @@ -824,10 +826,10 @@ class ExtendsCompatTests(BaseTestCase): - +
-
injected: DepInject(hello='from_block')
+
injected: DepInject(hello='from_block')
Default footer
diff --git a/tests/test_templatetags_provide.py b/tests/test_templatetags_provide.py index 957b03e9..be0b00ad 100644 --- a/tests/test_templatetags_provide.py +++ b/tests/test_templatetags_provide.py @@ -36,7 +36,7 @@ class ProvideTemplateTagTest(BaseTestCase): self.assertHTMLEqual( rendered, """ -
injected: DepInject(key='hi', another=123)
+
injected: DepInject(key='hi', another=123)
""", ) @@ -87,8 +87,8 @@ class ProvideTemplateTagTest(BaseTestCase): self.assertHTMLEqual( rendered, """ -
key: hi
-
another: 123
+
key: hi
+
another: 123
""", ) @@ -120,8 +120,8 @@ class ProvideTemplateTagTest(BaseTestCase): self.assertHTMLEqual( rendered, """ -
key: hi
-
another: 123
+
key: hi
+
another: 123
""", ) @@ -150,7 +150,7 @@ class ProvideTemplateTagTest(BaseTestCase): self.assertHTMLEqual( rendered, """ -
injected: default
+
injected: default
""", ) @@ -183,8 +183,8 @@ class ProvideTemplateTagTest(BaseTestCase): self.assertHTMLEqual( rendered, """ -
injected: DepInject()
-
injected: default
+
injected: DepInject()
+
injected: default
""", ) @@ -216,8 +216,8 @@ class ProvideTemplateTagTest(BaseTestCase): self.assertHTMLEqual( rendered, """ -
-
+
+
""", ) @@ -248,8 +248,8 @@ class ProvideTemplateTagTest(BaseTestCase): self.assertHTMLEqual( rendered, """ -
injected: DepInject(key='hi', another=123)
-
injected: default
+
injected: DepInject(key='hi', another=123)
+
injected: default
""", ) @@ -286,8 +286,8 @@ class ProvideTemplateTagTest(BaseTestCase): self.assertHTMLEqual( rendered, """ -
injected: DepInject(key='hi', another=123)
-
injected: default
+
injected: DepInject(key='hi', another=123)
+
injected: default
""", ) @@ -328,8 +328,8 @@ class ProvideTemplateTagTest(BaseTestCase): self.assertHTMLEqual( rendered, """ -
injected: DepInject(key='hi', another=123)
-
injected: default
+
injected: DepInject(key='hi', another=123)
+
injected: default
""", ) @@ -431,7 +431,7 @@ class ProvideTemplateTagTest(BaseTestCase): self.assertHTMLEqual( rendered, """ -
injected: DepInject(var1={'key': 'hi', 'another': 123}, var2={'x': 'y'})
+
injected: DepInject(var1={'key': 'hi', 'another': 123}, var2={'x': 'y'})
""", ) @@ -505,9 +505,9 @@ class ProvideTemplateTagTest(BaseTestCase): self.assertHTMLEqual( rendered, """ -
injected: DepInject(key='hi1', another=1231, new=3)
-
injected: DepInject(key='hi', another=123, lost=0)
-
injected: default
+
injected: DepInject(key='hi1', another=1231, new=3)
+
injected: DepInject(key='hi', another=123, lost=0)
+
injected: default
""", ) @@ -545,8 +545,8 @@ class ProvideTemplateTagTest(BaseTestCase): self.assertHTMLEqual( rendered, """ -
first_provide: DepInject(key='hi', another=123, lost=0)
-
second_provide: DepInject(key='hi1', another=1231, new=3)
+
first_provide: DepInject(key='hi', another=123, lost=0)
+
second_provide: DepInject(key='hi1', another=1231, new=3)
""", ) @@ -575,7 +575,7 @@ class ProvideTemplateTagTest(BaseTestCase): rendered, """
-
injected: DepInject(key='hi', another=123)
+
injected: DepInject(key='hi', another=123)
""", ) @@ -613,7 +613,7 @@ class ProvideTemplateTagTest(BaseTestCase): self.assertHTMLEqual( rendered, """ -
+
injected: DepInject(key='hi', another=123)
""", @@ -646,7 +646,7 @@ class InjectTest(BaseTestCase): self.assertHTMLEqual( rendered, """ -
injected: DepInject(key='hi', another=123)
+
injected: DepInject(key='hi', another=123)
""", ) @@ -694,7 +694,7 @@ class InjectTest(BaseTestCase): self.assertHTMLEqual( rendered, """ -
injected: default
+
injected: default
""", ) @@ -799,10 +799,12 @@ class InjectTest(BaseTestCase): self.assertHTMLEqual( rendered, """ -
+
injected: DepInject(key='hi', data=123)
-
456
+
+ 456 +
""", ) @@ -861,9 +863,10 @@ class InjectTest(BaseTestCase): self.assertHTMLEqual( rendered, """ -
+
injected: DepInject(key='hi', data=123)
-
+
+
""", ) diff --git a/tests/test_templatetags_slot_fill.py b/tests/test_templatetags_slot_fill.py index 1a49c3ea..dba20ce2 100644 --- a/tests/test_templatetags_slot_fill.py +++ b/tests/test_templatetags_slot_fill.py @@ -67,9 +67,11 @@ class ComponentSlotTests(BaseTestCase): self.assertHTMLEqual( rendered, """ - +
Custom header
-
Variable: variable
+
+ Variable: variable +
Default footer
""", @@ -117,8 +119,10 @@ class ComponentSlotTests(BaseTestCase): self.assertHTMLEqual( rendered, """ - -
Variable: variable
+ +
+ Variable: variable +
@@ -149,7 +153,7 @@ class ComponentSlotTests(BaseTestCase): self.assertHTMLEqual( rendered, f""" - +
Default header
test123 - {context_behavior_data}
test321
@@ -171,7 +175,7 @@ class ComponentSlotTests(BaseTestCase): self.assertHTMLEqual( rendered, """ - +
Default header
Default main
Default footer
@@ -194,7 +198,7 @@ class ComponentSlotTests(BaseTestCase): template = Template(template_str) rendered = template.render(Context({})) - self.assertHTMLEqual(rendered, "") + self.assertHTMLEqual(rendered, "") @parametrize_context_behavior(["django", "isolated"]) def test_slotted_template_without_slots_and_single_quotes(self): @@ -211,7 +215,7 @@ class ComponentSlotTests(BaseTestCase): template = Template(template_str) rendered = template.render(Context({})) - self.assertHTMLEqual(rendered, "") + self.assertHTMLEqual(rendered, "") @parametrize_context_behavior(["django", "isolated"]) def test_variable_fill_name(self): @@ -227,7 +231,7 @@ class ComponentSlotTests(BaseTestCase): template = Template(template_str) rendered = template.render(Context({})) expected = """ - +
Hi there!
Default main
Default footer
@@ -296,7 +300,7 @@ class ComponentSlotTests(BaseTestCase): rendered, """ -
+
ABC: carl var
@@ -327,7 +331,7 @@ class ComponentSlotTests(BaseTestCase): self.assertHTMLEqual( rendered, """ -
+

Custom title

Default subtitle

@@ -451,8 +455,8 @@ class ComponentSlotTests(BaseTestCase): render_dependencies=False, ) - self.assertInHTML(rendered1, "
MAIN
") - self.assertInHTML(rendered2, "
MAIN
") + self.assertHTMLEqual(rendered1, "
MAIN
") + self.assertHTMLEqual(rendered2, "
MAIN
") # 3. Specify the required slot by its name rendered3 = TestComp.render( @@ -462,7 +466,7 @@ class ComponentSlotTests(BaseTestCase): }, render_dependencies=False, ) - self.assertInHTML(rendered3, "
MAIN
MAIN
") + self.assertHTMLEqual(rendered3, "
MAIN
MAIN
") # 4. RAISES: Specify the required slot by the "default" name # This raises because the slot that is marked as 'required' is NOT marked as 'default'. @@ -499,7 +503,7 @@ class ComponentSlotTests(BaseTestCase): rendered = Template(template_str).render(Context({})) expected = """ - +
Custom header
Custom main
Custom footer
@@ -553,8 +557,10 @@ class ComponentSlotDefaultTests(BaseTestCase): template = Template(template_str) expected = """ -
-

This fills the 'main' slot.

+
+
+

This fills the 'main' slot.

+
""" rendered = template.render(Context({})) @@ -579,8 +585,10 @@ class ComponentSlotDefaultTests(BaseTestCase): """ template = Template(template_str) expected = """ -
-

This fills the 'main' slot.

+
+
+

This fills the 'main' slot.

+
""" rendered = template.render(Context({})) @@ -606,9 +614,9 @@ class ComponentSlotDefaultTests(BaseTestCase): """ template = Template(template_str) expected = """ -
-

This fills the 'main' slot.

-

This fills the 'main' slot.

+
+

This fills the 'main' slot.

+

This fills the 'main' slot.

""" rendered = template.render(Context({})) @@ -686,14 +694,14 @@ class ComponentSlotDefaultTests(BaseTestCase): """ template = Template(template_str) expected = """ -
-
- -
This Is Allowed
-
-
-
-
+
+
+ +
This Is Allowed
+
+
+
+
""" rendered = template.render(Context({})) @@ -761,7 +769,7 @@ class ComponentSlotDefaultTests(BaseTestCase): self.assertHTMLEqual( rendered, """ - +
Default header
Default main
Default footer
@@ -799,7 +807,7 @@ class ComponentSlotDefaultTests(BaseTestCase): self.assertHTMLEqual( rendered_truthy, """ - +
123
Default main
Default footer
@@ -811,7 +819,7 @@ class ComponentSlotDefaultTests(BaseTestCase): self.assertHTMLEqual( rendered_falsy, """ - +
Default main
Default footer
@@ -862,7 +870,7 @@ class PassthroughSlotsTest(BaseTestCase): self.assertHTMLEqual( rendered, """ - +
OVERRIDEN_SLOT "header" - INDEX 0 - ORIGINAL "Default header"
@@ -910,7 +918,7 @@ class PassthroughSlotsTest(BaseTestCase): self.assertHTMLEqual( rendered, """ - +
OVERRIDEN_SLOT "header" - ORIGINAL "Default header"
@@ -997,9 +1005,9 @@ class PassthroughSlotsTest(BaseTestCase): rendered = template.render(Context()) expected = """ -
CUSTOM HEADER
-
CUSTOM MAIN
-
footer
+
CUSTOM HEADER
+
CUSTOM MAIN
+
footer
""" self.assertHTMLEqual(rendered, expected) @@ -1042,11 +1050,11 @@ class PassthroughSlotsTest(BaseTestCase): rendered = template.render(Context()) expected = """ -
- -
CUSTOM HEADER
-
CUSTOM MAIN
-
Default footer
+
+ +
CUSTOM HEADER
+
CUSTOM MAIN
+
Default footer
""" @@ -1093,11 +1101,11 @@ class PassthroughSlotsTest(BaseTestCase): rendered = template.render(Context()) expected = """ -
- -
Default header
-
CUSTOM MAIN
-
Default footer
+
+ +
Default header
+
CUSTOM MAIN
+
Default footer
""" @@ -1145,7 +1153,7 @@ class NestedSlotsTests(BaseTestCase): rendered = Template(template_str).render(Context()) expected = """ -
+
Wrapper Default
Parent1 Default @@ -1175,7 +1183,7 @@ class NestedSlotsTests(BaseTestCase): rendered = Template(template_str).render(Context()) expected = """ -
+
Entire Wrapper Replaced
""" @@ -1196,7 +1204,7 @@ class NestedSlotsTests(BaseTestCase): rendered = Template(template_str).render(Context()) expected = """ -
+
Wrapper Default
Parent1 Replaced @@ -1223,7 +1231,7 @@ class NestedSlotsTests(BaseTestCase): rendered = Template(template_str).render(Context()) expected = """ -
+
Wrapper Default
Parent1 Default @@ -1263,7 +1271,7 @@ class NestedSlotsTests(BaseTestCase): rendered = Template(template_str).render(Context()) expected = """ -
+
Entire Wrapper Replaced
""" @@ -1295,7 +1303,7 @@ class SlottedTemplateRegressionTests(BaseTestCase): self.assertHTMLEqual( rendered, """ - +
Default header
Default main
Default footer
@@ -1325,7 +1333,7 @@ class SlotDefaultTests(BaseTestCase): self.assertHTMLEqual( rendered, """ - +
Before: Default header
Default main
Default footer, after
@@ -1350,7 +1358,7 @@ class SlotDefaultTests(BaseTestCase): self.assertHTMLEqual( rendered, """ - +
First: Default header; Second: Default header
Default main
Default footer
@@ -1380,7 +1388,7 @@ class SlotDefaultTests(BaseTestCase): self.assertHTMLEqual( rendered, """ - +
First Default header Later Default header Later Default header
Default main
Default footer
@@ -1414,10 +1422,10 @@ class SlotDefaultTests(BaseTestCase): self.assertHTMLEqual( rendered, """ - +
header1_in_header1: Default header - +
header1_in_header2: Default header header2_in_header2: Default header @@ -1465,7 +1473,7 @@ class ScopedSlotTest(BaseTestCase): """ rendered = Template(template).render(Context()) expected = """ -
+
def 456
@@ -1500,7 +1508,7 @@ class ScopedSlotTest(BaseTestCase): """ rendered = Template(template).render(Context()) expected = """ -
+
def 456
@@ -1536,7 +1544,7 @@ class ScopedSlotTest(BaseTestCase): """ rendered = Template(template).render(Context()) expected = """ -
+
Default text def 456 @@ -1573,7 +1581,7 @@ class ScopedSlotTest(BaseTestCase): """ rendered = Template(template).render(Context()) expected = """ -
+
def 456
@@ -1611,7 +1619,7 @@ class ScopedSlotTest(BaseTestCase): """ rendered = Template(template).render(Context()) expected = """ -
+
def 456
@@ -1648,7 +1656,7 @@ class ScopedSlotTest(BaseTestCase): """ rendered = Template(template).render(Context()) expected = """ -
+
Default text A xyz Default text B 456
@@ -1712,7 +1720,7 @@ class ScopedSlotTest(BaseTestCase): {% endcomponent %} """ rendered = Template(template).render(Context()) - expected = "
overriden
" + expected = "
overriden
" self.assertHTMLEqual(rendered, expected) @parametrize_context_behavior(["django", "isolated"]) @@ -1735,7 +1743,7 @@ class ScopedSlotTest(BaseTestCase): {% endcomponent %} """ rendered = Template(template).render(Context()) - expected = "
{}
" + expected = "
{}
" self.assertHTMLEqual(rendered, expected) @parametrize_context_behavior(["django", "isolated"]) @@ -1761,7 +1769,7 @@ class ScopedSlotTest(BaseTestCase): {% endcomponent %} """ rendered = Template(template).render(Context()) - expected = "
Default text
" + expected = "
Default text
" self.assertHTMLEqual(rendered, expected) @parametrize_context_behavior(["django", "isolated"]) @@ -1800,7 +1808,7 @@ class ScopedSlotTest(BaseTestCase): ) expected = """ -
+
def 456
@@ -1845,7 +1853,7 @@ class ScopedSlotTest(BaseTestCase): ) expected = """ -
+
def 456
@@ -1889,9 +1897,9 @@ class ScopedSlotTest(BaseTestCase): self.assertHTMLEqual( rendered, """ -
+
data1_in_slot1: {'abc': 'def', 'input': 1} -
+
data1_in_slot2: {'abc': 'def', 'input': 1} data2_in_slot2: {'abc': 'def', 'input': 2}
@@ -1990,9 +1998,9 @@ class DuplicateSlotTest(BaseTestCase): self.assertHTMLEqual( rendered, """ -
Name: Jannete
-
Name: Jannete
-
Hello
+
Name: Jannete
+
Name: Jannete
+
Hello
""", ) @@ -2010,9 +2018,9 @@ class DuplicateSlotTest(BaseTestCase): self.assertHTMLEqual( rendered, """ -
Default header
-
Default main header
-
Default footer
+
Default header
+
Default main header
+
Default footer
""", ) @@ -2034,8 +2042,8 @@ class DuplicateSlotTest(BaseTestCase): rendered, """ OVERRIDDEN! -
-
+
+

OVERRIDDEN!

@@ -2071,8 +2079,8 @@ class DuplicateSlotTest(BaseTestCase): rendered, """ START -
-
+
+

NESTED

@@ -2198,11 +2206,11 @@ class SlotBehaviorTests(BaseTestCase): self.assertHTMLEqual( rendered, """ - +
Name: Igor
Day: Monday