feat: add decorator for writing component tests (#1008)

* feat: add decorator for writing component tests

* refactor: udpate changelog + update deps pins

* refactor: fix deps

* refactor: make cached_ref into generic and fix linter errors

* refactor: fix coverage testing

* refactor: use global var instead of env var for is_testing state
This commit is contained in:
Juro Oravec 2025-03-02 19:46:12 +01:00 committed by GitHub
parent 81ac59f7fb
commit 7dfcb447c4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
62 changed files with 4428 additions and 3661 deletions

View file

@ -1,5 +1,23 @@
# Release notes # Release notes
## v0.131
#### Feat
- `@djc_test` decorator for writing tests that involve Components.
- The decorator manages global state, ensuring that tests don't leak.
- If using `pytest`, the decorator allows you to parametrize Django or Components settings.
- The decorator also serves as a stand-in for Django's `@override_settings`.
See the API reference for [`@djc_test`](https://django-components.github.io/django-components/0.131/reference/testing_api/#djc_test) for more details.
#### Internal
- Settings are now loaded only once, and thus are considered immutable once loaded. Previously,
django-components would load settings from `settings.COMPONENTS` on each access. The new behavior
aligns with Django's settings.
## v0.130 ## v0.130
#### Feat #### Feat

View file

@ -8,4 +8,5 @@ nav:
- Typing and validation: typing_and_validation.md - Typing and validation: typing_and_validation.md
- Custom template tags: template_tags.md - Custom template tags: template_tags.md
- Tag formatters: tag_formatter.md - Tag formatters: tag_formatter.md
- Testing: testing.md
- Authoring component libraries: authoring_component_libraries.md - Authoring component libraries: authoring_component_libraries.md

View file

@ -0,0 +1,111 @@
_New in version 0.131_
The [`@djc_test`](../../../reference/testing_api#djc_test) decorator is a powerful tool for testing components created with `django-components`. It ensures that each test is properly isolated, preventing components registered in one test from affecting others.
## Usage
The [`@djc_test`](../../../reference/testing_api#djc_test) decorator can be applied to functions, methods, or classes. When applied to a class, it recursively decorates all methods starting with `test_`, including those in nested classes. This allows for comprehensive testing of component behavior.
### Applying to a Function
To apply [`djc_test`](../../../reference/testing_api#djc_test) to a function,
simply decorate the function as shown below:
```python
import django
from django_components.testing import djc_test
django.setup()
@djc_test
def test_my_component():
@register("my_component")
class MyComponent(Component):
template = "..."
...
```
### Applying to a Class
When applied to a class, `djc_test` decorates each `test_` method individually:
```python
import django
from django_components.testing import djc_test
django.setup()
@djc_test
class TestMyComponent:
def test_something(self):
...
class Nested:
def test_something_else(self):
...
```
This is equivalent to applying the decorator to each method individually:
```python
import django
from django_components.testing import djc_test
django.setup()
class TestMyComponent:
@djc_test
def test_something(self):
...
class Nested:
@djc_test
def test_something_else(self):
...
```
### Arguments
See the API reference for [`@djc_test`](../../../reference/testing_api#djc_test) for more details.
### Setting Up Django
Before using [`djc_test`](../../../reference/testing_api#djc_test), ensure Django is set up:
```python
import django
from django_components.testing import djc_test
django.setup()
@djc_test
def test_my_component():
...
```
## Example: Parametrizing Context Behavior
You can parametrize the [context behavior](../../../reference/settings#django_components.app_settings.ComponentsSettings.context_behavior) using [`djc_test`](../../../reference/testing_api#djc_test):
```python
from django_components.testing import djc_test
@djc_test(
# Settings applied to all cases
components_settings={
"app_dirs": ["custom_dir"],
},
# Parametrized settings
parametrize=(
["components_settings"],
[
[{"context_behavior": "django"}],
[{"context_behavior": "isolated"}],
],
["django", "isolated"],
)
)
def test_context_behavior(components_settings):
rendered = MyComponent().render()
...
```

View file

@ -11,3 +11,4 @@ nav:
- Template tags: template_tags.md - Template tags: template_tags.md
- Template vars: template_vars.md - Template vars: template_vars.md
- URLs: urls.md - URLs: urls.md
- Testing API: testing_api.md

View file

@ -0,0 +1,9 @@
<!-- Autogenerated by reference.py -->
# Testing API
::: django_components.testing.djc_test
options:
show_if_no_docstring: true

View file

@ -108,6 +108,40 @@ def gen_reference_api():
f.write("\n") f.write("\n")
def gen_reference_testing_api():
"""
Generate documentation for the Python API of `django_components.testing`.
This takes all public symbols exported from `django_components.testing`.
"""
module = import_module("django_components.testing")
preface = "<!-- Autogenerated by reference.py -->\n\n"
preface += (root / "docs/templates/reference_testing_api.md").read_text()
out_file = root / "docs/reference/testing_api.md"
out_file.parent.mkdir(parents=True, exist_ok=True)
with out_file.open("w", encoding="utf-8") as f:
f.write(preface + "\n\n")
for name, obj in inspect.getmembers(module):
if (
name.startswith("_")
or inspect.ismodule(obj)
):
continue
# For each entry, generate a mkdocstrings entry, e.g.
# ```
# ::: django_components.testing.djc_test
# options:
# show_if_no_docstring: true
# ```
f.write(f"::: {module.__name__}.{name}\n" f" options:\n" f" show_if_no_docstring: true\n")
f.write("\n")
def gen_reference_exceptions(): def gen_reference_exceptions():
""" """
Generate documentation for the Exception classes included in the Python API of `django_components`. Generate documentation for the Exception classes included in the Python API of `django_components`.
@ -739,6 +773,7 @@ def gen_reference():
gen_reference_templatetags() gen_reference_templatetags()
gen_reference_templatevars() gen_reference_templatevars()
gen_reference_signals() gen_reference_signals()
gen_reference_testing_api()
# This is run when `gen-files` plugin is run in mkdocs.yml # This is run when `gen-files` plugin is run in mkdocs.yml

View file

@ -0,0 +1 @@
# Testing API

View file

@ -109,6 +109,7 @@ disallow_untyped_defs = true
testpaths = [ testpaths = [
"tests" "tests"
] ]
asyncio_mode = "auto"
[tool.hatch.env] [tool.hatch.env]
requires = [ requires = [

View file

@ -5,3 +5,5 @@ requests
types-requests types-requests
whitenoise whitenoise
asv asv
pytest-asyncio
pytest-django

View file

@ -4,17 +4,23 @@
# #
# pip-compile requirements-ci.in # pip-compile requirements-ci.in
# #
cachetools==5.5.0 asv==0.6.4
# via -r requirements-ci.in
asv-runner==0.2.1
# via asv
build==1.2.2.post1
# via asv
cachetools==5.5.2
# via tox # via tox
certifi==2024.8.30 certifi==2025.1.31
# via requests # via requests
chardet==5.2.0 chardet==5.2.0
# via tox # via tox
charset-normalizer==3.3.2 charset-normalizer==3.4.1
# via requests # via requests
colorama==0.4.6 colorama==0.4.6
# via tox # via tox
distlib==0.3.8 distlib==0.3.9
# via virtualenv # via virtualenv
filelock==3.16.1 filelock==3.16.1
# via # via
@ -24,9 +30,17 @@ greenlet==3.1.1
# via playwright # via playwright
idna==3.10 idna==3.10
# via requests # via requests
importlib-metadata==8.5.0
# via asv-runner
iniconfig==2.0.0
# via pytest
json5==0.10.0
# via asv
packaging==24.2 packaging==24.2
# via # via
# build
# pyproject-api # pyproject-api
# pytest
# tox # tox
platformdirs==4.3.6 platformdirs==4.3.6
# via # via
@ -35,18 +49,36 @@ platformdirs==4.3.6
playwright==1.48.0 playwright==1.48.0
# via -r requirements-ci.in # via -r requirements-ci.in
pluggy==1.5.0 pluggy==1.5.0
# via tox # via
# pytest
# tox
pyee==12.0.0 pyee==12.0.0
# via playwright # via playwright
pympler==1.1
# via asv
pyproject-api==1.8.0 pyproject-api==1.8.0
# via tox # via tox
pyproject-hooks==1.2.0
# via build
pytest==8.3.4
# via
# pytest-asyncio
# pytest-django
pytest-asyncio==0.24.0
# via -r requirements-ci.in
pytest-django==4.10.0
# via -r requirements-ci.in
pyyaml==6.0.2
# via asv
requests==2.32.3 requests==2.32.3
# via -r requirements-ci.in # via -r requirements-ci.in
tabulate==0.9.0
# via asv
tox==4.24.1 tox==4.24.1
# via # via
# -r requirements-ci.in # -r requirements-ci.in
# tox-gh-actions # tox-gh-actions
tox-gh-actions==3.2.0 tox-gh-actions==3.3.0
# via -r requirements-ci.in # via -r requirements-ci.in
types-requests==2.32.0.20241016 types-requests==2.32.0.20241016
# via -r requirements-ci.in # via -r requirements-ci.in
@ -56,7 +88,11 @@ urllib3==2.2.3
# via # via
# requests # requests
# types-requests # types-requests
virtualenv==20.29.1 virtualenv==20.29.2
# via tox # via
# asv
# tox
whitenoise==6.7.0 whitenoise==6.7.0
# via -r requirements-ci.in # via -r requirements-ci.in
zipp==3.20.2
# via importlib-metadata

View file

@ -2,6 +2,8 @@ django
djc-core-html-parser djc-core-html-parser
tox tox
pytest pytest
pytest-asyncio
pytest-django
syrupy syrupy
flake8 flake8
flake8-pyproject flake8-pyproject

View file

@ -6,11 +6,17 @@
# #
asgiref==3.8.1 asgiref==3.8.1
# via django # via django
black==24.10.0 asv==0.6.4
# via -r requirements-dev.in # via -r requirements-dev.in
cachetools==5.5.1 asv-runner==0.2.1
# via asv
black==25.1.0
# via -r requirements-dev.in
build==1.2.2.post1
# via asv
cachetools==5.5.2
# via tox # via tox
certifi==2024.8.30 certifi==2025.1.31
# via requests # via requests
cfgv==3.4.0 cfgv==3.4.0
# via pre-commit # via pre-commit
@ -40,14 +46,18 @@ flake8-pyproject==1.2.3
# via -r requirements-dev.in # via -r requirements-dev.in
greenlet==3.1.1 greenlet==3.1.1
# via playwright # via playwright
identify==2.6.7 identify==2.6.8
# via pre-commit # via pre-commit
idna==3.10 idna==3.10
# via requests # via requests
importlib-metadata==8.5.0
# via asv-runner
iniconfig==2.0.0 iniconfig==2.0.0
# via pytest # via pytest
isort==6.0.0 isort==6.0.1
# via -r requirements-dev.in # via -r requirements-dev.in
json5==0.10.0
# via asv
mccabe==0.7.0 mccabe==0.7.0
# via flake8 # via flake8
mypy==1.15.0 mypy==1.15.0
@ -61,6 +71,7 @@ nodeenv==1.9.1
packaging==24.2 packaging==24.2
# via # via
# black # black
# build
# pyproject-api # pyproject-api
# pytest # pytest
# tox # tox
@ -71,7 +82,7 @@ platformdirs==4.3.6
# black # black
# tox # tox
# virtualenv # virtualenv
playwright==1.49.0 playwright==1.48.0
# via -r requirements-dev.in # via -r requirements-dev.in
pluggy==1.5.0 pluggy==1.5.0
# via # via
@ -86,19 +97,39 @@ pyee==12.0.0
pyflakes==3.2.0 pyflakes==3.2.0
# via flake8 # via flake8
pygments==2.19.1 pygments==2.19.1
# via pygments-djc # via
# -r requirements-dev.in
# pygments-djc
pygments-djc==1.0.1 pygments-djc==1.0.1
# via -r requirements-dev.in # via -r requirements-dev.in
pympler==1.1
# via asv
pyproject-api==1.8.0 pyproject-api==1.8.0
# via tox # via tox
pyproject-hooks==1.2.0
# via build
pytest==8.3.4 pytest==8.3.4
# via
# -r requirements-dev.in
# pytest-asyncio
# pytest-django
# syrupy
pytest-asyncio==0.24.0
# via -r requirements-dev.in
pytest-django==4.10.0
# via -r requirements-dev.in # via -r requirements-dev.in
pyyaml==6.0.2 pyyaml==6.0.2
# via pre-commit # via
# asv
# pre-commit
requests==2.32.3 requests==2.32.3
# via -r requirements-dev.in # via -r requirements-dev.in
sqlparse==0.5.3 sqlparse==0.5.3
# via django # via django
syrupy==4.8.2
# via -r requirements-dev.in
tabulate==0.9.0
# via asv
tox==4.24.1 tox==4.24.1
# via -r requirements-dev.in # via -r requirements-dev.in
types-requests==2.32.0.20241016 types-requests==2.32.0.20241016
@ -111,9 +142,12 @@ urllib3==2.2.3
# via # via
# requests # requests
# types-requests # types-requests
virtualenv==20.28.0 virtualenv==20.29.2
# via # via
# asv
# pre-commit # pre-commit
# tox # tox
whitenoise==6.8.2 whitenoise==6.7.0
# via -r requirements-dev.in # via -r requirements-dev.in
zipp==3.20.2
# via importlib-metadata

View file

@ -5,7 +5,9 @@ from os import PathLike
from pathlib import Path from pathlib import Path
from typing import ( from typing import (
TYPE_CHECKING, TYPE_CHECKING,
Any,
Callable, Callable,
Dict,
Generic, Generic,
List, List,
Literal, Literal,
@ -671,94 +673,140 @@ defaults = ComponentsSettings(
# fmt: on # fmt: on
# Interface through which we access the settings.
#
# This is the only place where we actually access the settings.
# The settings are merged with defaults, and then validated.
#
# The settings are then available through the `app_settings` object.
#
# Settings are loaded from Django settings only once, at `apps.py` in `ready()`.
class InternalSettings: class InternalSettings:
@property def __init__(self, settings: Optional[Dict[str, Any]] = None):
def _settings(self) -> ComponentsSettings: self._settings = ComponentsSettings(**settings) if settings else defaults
def _load_settings(self) -> None:
data = getattr(settings, "COMPONENTS", {}) data = getattr(settings, "COMPONENTS", {})
return ComponentsSettings(**data) if not isinstance(data, ComponentsSettings) else data components_settings = ComponentsSettings(**data) if not isinstance(data, ComponentsSettings) else data
@property # Merge we defaults and otherwise initialize if necessary
def AUTODISCOVER(self) -> bool:
return default(self._settings.autodiscover, cast(bool, defaults.autodiscover))
@property # For DIRS setting, we use a getter for the default value, because the default value
def CACHE(self) -> Optional[str]: # uses Django settings, which may not yet be initialized at the time these settings are generated.
return default(self._settings.cache, defaults.cache) dirs_default_fn = cast(Dynamic[Sequence[Union[str, Tuple[str, str]]]], defaults.dirs)
dirs_default = dirs_default_fn.getter()
@property self._settings = ComponentsSettings(
def DIRS(self) -> Sequence[Union[str, PathLike, Tuple[str, str], Tuple[str, PathLike]]]: autodiscover=default(components_settings.autodiscover, defaults.autodiscover),
# For DIRS we use a getter, because default values uses Django settings, cache=default(components_settings.cache, defaults.cache),
# which may not yet be initialized at the time these settings are generated. dirs=default(components_settings.dirs, dirs_default),
default_fn = cast(Dynamic[Sequence[Union[str, Tuple[str, str]]]], defaults.dirs) app_dirs=default(components_settings.app_dirs, defaults.app_dirs),
default_dirs = default_fn.getter() debug_highlight_components=default(
return default(self._settings.dirs, default_dirs) components_settings.debug_highlight_components, defaults.debug_highlight_components
),
debug_highlight_slots=default(components_settings.debug_highlight_slots, defaults.debug_highlight_slots),
dynamic_component_name=default(
components_settings.dynamic_component_name, defaults.dynamic_component_name
),
libraries=default(components_settings.libraries, defaults.libraries),
multiline_tags=default(components_settings.multiline_tags, defaults.multiline_tags),
reload_on_file_change=self._prepare_reload_on_file_change(components_settings),
template_cache_size=default(components_settings.template_cache_size, defaults.template_cache_size),
static_files_allowed=default(components_settings.static_files_allowed, defaults.static_files_allowed),
static_files_forbidden=self._prepare_static_files_forbidden(components_settings),
context_behavior=self._prepare_context_behavior(components_settings),
tag_formatter=default(components_settings.tag_formatter, defaults.tag_formatter), # type: ignore[arg-type]
)
@property def _prepare_reload_on_file_change(self, new_settings: ComponentsSettings) -> bool:
def APP_DIRS(self) -> Sequence[str]: val = new_settings.reload_on_file_change
return default(self._settings.app_dirs, cast(List[str], defaults.app_dirs))
@property
def DEBUG_HIGHLIGHT_COMPONENTS(self) -> bool:
return default(self._settings.debug_highlight_components, cast(bool, defaults.debug_highlight_components))
@property
def DEBUG_HIGHLIGHT_SLOTS(self) -> bool:
return default(self._settings.debug_highlight_slots, cast(bool, defaults.debug_highlight_slots))
@property
def DYNAMIC_COMPONENT_NAME(self) -> str:
return default(self._settings.dynamic_component_name, cast(str, defaults.dynamic_component_name))
@property
def LIBRARIES(self) -> List[str]:
return default(self._settings.libraries, cast(List[str], defaults.libraries))
@property
def MULTILINE_TAGS(self) -> bool:
return default(self._settings.multiline_tags, cast(bool, defaults.multiline_tags))
@property
def RELOAD_ON_FILE_CHANGE(self) -> bool:
val = self._settings.reload_on_file_change
# TODO_REMOVE_IN_V1 # TODO_REMOVE_IN_V1
if val is None: if val is None:
val = self._settings.reload_on_template_change val = new_settings.reload_on_template_change
return default(val, cast(bool, defaults.reload_on_file_change)) return default(val, cast(bool, defaults.reload_on_file_change))
@property def _prepare_static_files_forbidden(self, new_settings: ComponentsSettings) -> List[Union[str, re.Pattern]]:
def TEMPLATE_CACHE_SIZE(self) -> int: val = new_settings.static_files_forbidden
return default(self._settings.template_cache_size, cast(int, defaults.template_cache_size))
@property
def STATIC_FILES_ALLOWED(self) -> Sequence[Union[str, re.Pattern]]:
return default(self._settings.static_files_allowed, cast(List[str], defaults.static_files_allowed))
@property
def STATIC_FILES_FORBIDDEN(self) -> Sequence[Union[str, re.Pattern]]:
val = self._settings.static_files_forbidden
# TODO_REMOVE_IN_V1 # TODO_REMOVE_IN_V1
if val is None: if val is None:
val = self._settings.forbidden_static_files val = new_settings.forbidden_static_files
return default(val, cast(List[str], defaults.static_files_forbidden)) return default(val, cast(List[Union[str, re.Pattern]], defaults.static_files_forbidden))
@property def _prepare_context_behavior(self, new_settings: ComponentsSettings) -> Literal["django", "isolated"]:
def CONTEXT_BEHAVIOR(self) -> ContextBehavior: raw_value = cast(
raw_value = cast(str, default(self._settings.context_behavior, defaults.context_behavior)) Literal["django", "isolated"],
return self._validate_context_behavior(raw_value) default(new_settings.context_behavior, defaults.context_behavior),
)
def _validate_context_behavior(self, raw_value: Union[ContextBehavior, str]) -> ContextBehavior:
try: try:
return ContextBehavior(raw_value) ContextBehavior(raw_value)
except ValueError: except ValueError:
valid_values = [behavior.value for behavior in ContextBehavior] valid_values = [behavior.value for behavior in ContextBehavior]
raise ValueError(f"Invalid context behavior: {raw_value}. Valid options are {valid_values}") raise ValueError(f"Invalid context behavior: {raw_value}. Valid options are {valid_values}")
return raw_value
# TODO REMOVE THE PROPERTIES BELOW? THEY NO LONGER SERVE ANY PURPOSE
@property
def AUTODISCOVER(self) -> bool:
return self._settings.autodiscover # type: ignore[return-value]
@property
def CACHE(self) -> Optional[str]:
return self._settings.cache
@property
def DIRS(self) -> Sequence[Union[str, PathLike, Tuple[str, str], Tuple[str, PathLike]]]:
return self._settings.dirs # type: ignore[return-value]
@property
def APP_DIRS(self) -> Sequence[str]:
return self._settings.app_dirs # type: ignore[return-value]
@property
def DEBUG_HIGHLIGHT_COMPONENTS(self) -> bool:
return self._settings.debug_highlight_components # type: ignore[return-value]
@property
def DEBUG_HIGHLIGHT_SLOTS(self) -> bool:
return self._settings.debug_highlight_slots # type: ignore[return-value]
@property
def DYNAMIC_COMPONENT_NAME(self) -> str:
return self._settings.dynamic_component_name # type: ignore[return-value]
@property
def LIBRARIES(self) -> List[str]:
return self._settings.libraries # type: ignore[return-value]
@property
def MULTILINE_TAGS(self) -> bool:
return self._settings.multiline_tags # type: ignore[return-value]
@property
def RELOAD_ON_FILE_CHANGE(self) -> bool:
return self._settings.reload_on_file_change # type: ignore[return-value]
@property
def TEMPLATE_CACHE_SIZE(self) -> int:
return self._settings.template_cache_size # type: ignore[return-value]
@property
def STATIC_FILES_ALLOWED(self) -> Sequence[Union[str, re.Pattern]]:
return self._settings.static_files_allowed # type: ignore[return-value]
@property
def STATIC_FILES_FORBIDDEN(self) -> Sequence[Union[str, re.Pattern]]:
return self._settings.static_files_forbidden # type: ignore[return-value]
@property
def CONTEXT_BEHAVIOR(self) -> ContextBehavior:
return ContextBehavior(self._settings.context_behavior)
@property @property
def TAG_FORMATTER(self) -> Union["TagFormatterABC", str]: def TAG_FORMATTER(self) -> Union["TagFormatterABC", str]:
tag_formatter = default(self._settings.tag_formatter, cast(str, defaults.tag_formatter)) return self._settings.tag_formatter # type: ignore[return-value]
return cast(Union["TagFormatterABC", str], tag_formatter)
app_settings = InternalSettings() app_settings = InternalSettings()

View file

@ -19,6 +19,8 @@ class ComponentsConfig(AppConfig):
from django_components.components.dynamic import DynamicComponent from django_components.components.dynamic import DynamicComponent
from django_components.util.django_monkeypatch import monkeypatch_template_cls from django_components.util.django_monkeypatch import monkeypatch_template_cls
app_settings._load_settings()
# NOTE: This monkeypatch is applied here, before Django processes any requests. # NOTE: This monkeypatch is applied here, before Django processes any requests.
# To make django-components work with django-debug-toolbar-template-profiler # To make django-components work with django-debug-toolbar-template-profiler
# See https://github.com/django-components/django-components/discussions/819 # See https://github.com/django-components/django-components/discussions/819

View file

@ -3,6 +3,12 @@ from typing import Callable, List, Optional
from django_components.util.loader import get_component_files from django_components.util.loader import get_component_files
from django_components.util.logger import logger from django_components.util.logger import logger
from django_components.util.testing import is_testing
# In tests, we want to capture which modules have been loaded, so we can
# clean them up between tests. But there's no need to track this in
# production.
LOADED_MODULES: List[str] = []
def autodiscover( def autodiscover(
@ -94,4 +100,10 @@ def _import_modules(
logger.debug(f'Importing module "{module_name}"') logger.debug(f'Importing module "{module_name}"')
importlib.import_module(module_name) importlib.import_module(module_name)
imported_modules.append(module_name) imported_modules.append(module_name)
# In tests tagged with `@djc_test`, we want to capture the modules that
# are imported so we can clean them up between tests.
if is_testing():
LOADED_MODULES.append(module_name)
return imported_modules return imported_modules

View file

@ -1,3 +1,4 @@
import sys
import types import types
from collections import deque from collections import deque
from contextlib import contextmanager from contextlib import contextmanager
@ -22,6 +23,7 @@ from typing import (
Union, Union,
cast, cast,
) )
from weakref import ReferenceType
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
from django.forms.widgets import Media as MediaCls from django.forms.widgets import Media as MediaCls
@ -77,6 +79,7 @@ from django_components.util.logger import trace_component_msg
from django_components.util.misc import gen_id, get_import_path, hash_comp_cls from django_components.util.misc import gen_id, get_import_path, hash_comp_cls
from django_components.util.template_tag import TagAttr from django_components.util.template_tag import TagAttr
from django_components.util.validation import validate_typed_dict, validate_typed_tuple from django_components.util.validation import validate_typed_dict, validate_typed_tuple
from django_components.util.weakref import cached_ref
# TODO_REMOVE_IN_V1 - Users should use top-level import instead # TODO_REMOVE_IN_V1 - Users should use top-level import instead
# isort: off # isort: off
@ -99,6 +102,17 @@ JsDataType = TypeVar("JsDataType", bound=Mapping[str, Any])
CssDataType = TypeVar("CssDataType", bound=Mapping[str, Any]) CssDataType = TypeVar("CssDataType", bound=Mapping[str, Any])
# NOTE: `ReferenceType` is NOT a generic pre-3.9
if sys.version_info >= (3, 9):
AllComponents = List[ReferenceType["ComponentRegistry"]]
else:
AllComponents = List[ReferenceType]
# Keep track of all the Component classes created, so we can clean up after tests
ALL_COMPONENTS: AllComponents = []
@dataclass(frozen=True) @dataclass(frozen=True)
class RenderInput(Generic[ArgsType, KwargsType, SlotsType]): class RenderInput(Generic[ArgsType, KwargsType, SlotsType]):
context: Context context: Context
@ -613,6 +627,8 @@ class Component(
cls._class_hash = hash_comp_cls(cls) cls._class_hash = hash_comp_cls(cls)
comp_hash_mapping[cls._class_hash] = cls comp_hash_mapping[cls._class_hash] = cls
ALL_COMPONENTS.append(cached_ref(cls)) # type: ignore[arg-type]
@contextmanager @contextmanager
def _with_metadata(self, item: MetadataItem) -> Generator[None, None, None]: def _with_metadata(self, item: MetadataItem) -> Generator[None, None, None]:
self._metadata_stack.append(item) self._metadata_stack.append(item)

View file

@ -1,6 +1,7 @@
import os import os
import sys import sys
from collections import deque from collections import deque
from copy import copy
from dataclasses import dataclass from dataclasses import dataclass
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING, Any, Callable, Dict, List, Literal, Optional, Protocol, Tuple, Type, Union, cast from typing import TYPE_CHECKING, Any, Callable, Dict, List, Literal, Optional, Protocol, Tuple, Type, Union, cast
@ -238,6 +239,13 @@ class ComponentMedia:
f"Received non-null value from both '{inlined_attr}' and '{file_attr}' in" f"Received non-null value from both '{inlined_attr}' and '{file_attr}' in"
f" Component {self.comp_cls.__name__}. Only one of the two must be set." f" Component {self.comp_cls.__name__}. Only one of the two must be set."
) )
# Make a copy of the original state, so we can reset it in tests
self._original = copy(self)
# Return ComponentMedia to its original state before the media was resolved
def reset(self) -> None:
self.__dict__.update(self._original.__dict__)
self.resolved = False
# This metaclass is all about one thing - lazily resolving the media files. # This metaclass is all about one thing - lazily resolving the media files.

View file

@ -1,4 +1,6 @@
import sys
from typing import TYPE_CHECKING, Callable, Dict, List, NamedTuple, Optional, Set, Type, Union from typing import TYPE_CHECKING, Callable, Dict, List, NamedTuple, Optional, Set, Type, Union
from weakref import ReferenceType, finalize
from django.template import Library from django.template import Library
from django.template.base import Parser, Token from django.template.base import Parser, Token
@ -6,6 +8,7 @@ from django.template.base import Parser, Token
from django_components.app_settings import ContextBehaviorType, app_settings from django_components.app_settings import ContextBehaviorType, app_settings
from django_components.library import is_tag_protected, mark_protected_tags, register_tag from django_components.library import is_tag_protected, mark_protected_tags, register_tag
from django_components.tag_formatter import TagFormatterABC, get_tag_formatter from django_components.tag_formatter import TagFormatterABC, get_tag_formatter
from django_components.util.weakref import cached_ref
if TYPE_CHECKING: if TYPE_CHECKING:
from django_components.component import ( from django_components.component import (
@ -19,6 +22,13 @@ if TYPE_CHECKING:
) )
# NOTE: `ReferenceType` is NOT a generic pre-3.9
if sys.version_info >= (3, 9):
AllRegistries = List[ReferenceType["ComponentRegistry"]]
else:
AllRegistries = List[ReferenceType]
class AlreadyRegistered(Exception): class AlreadyRegistered(Exception):
""" """
Raised when you try to register a [Component](../api#django_components#Component), Raised when you try to register a [Component](../api#django_components#Component),
@ -130,7 +140,7 @@ class InternalRegistrySettings(NamedTuple):
# We keep track of all registries that exist so that, when users want to # We keep track of all registries that exist so that, when users want to
# dynamically resolve component name to component class, they would be able # dynamically resolve component name to component class, they would be able
# to search across all registries. # to search across all registries.
all_registries: List["ComponentRegistry"] = [] ALL_REGISTRIES: AllRegistries = []
class ComponentRegistry: class ComponentRegistry:
@ -223,10 +233,19 @@ class ComponentRegistry:
self._registry: Dict[str, ComponentRegistryEntry] = {} # component name -> component_entry mapping self._registry: Dict[str, ComponentRegistryEntry] = {} # component name -> component_entry mapping
self._tags: Dict[str, Set[str]] = {} # tag -> list[component names] self._tags: Dict[str, Set[str]] = {} # tag -> list[component names]
self._library = library self._library = library
self._settings_input = settings self._settings = settings
self._settings: Optional[Callable[[], InternalRegistrySettings]] = None
all_registries.append(self) ALL_REGISTRIES.append(cached_ref(self))
def __del__(self) -> None:
# Unregister all components when the registry is deleted
self.clear()
def __copy__(self) -> "ComponentRegistry":
new_registry = ComponentRegistry(self.library, self._settings)
new_registry._registry = self._registry.copy()
new_registry._tags = self._tags.copy()
return new_registry
@property @property
def library(self) -> Library: def library(self) -> Library:
@ -254,23 +273,12 @@ class ComponentRegistry:
""" """
[Registry settings](../api#django_components.RegistrySettings) configured for this registry. [Registry settings](../api#django_components.RegistrySettings) configured for this registry.
""" """
# This is run on subsequent calls
if self._settings is not None:
# NOTE: Registry's settings can be a function, so we always take
# the latest value from Django's settings.
settings = self._settings()
# First-time initialization
# NOTE: We allow the settings to be given as a getter function # NOTE: We allow the settings to be given as a getter function
# so the settings can respond to changes. # so the settings can respond to changes.
# So we wrapp that in our getter, which assigns default values from the settings. if callable(self._settings):
settings_input: Optional[RegistrySettings] = self._settings(self)
else: else:
settings_input = self._settings
def get_settings() -> InternalRegistrySettings:
if callable(self._settings_input):
settings_input: Optional[RegistrySettings] = self._settings_input(self)
else:
settings_input = self._settings_input
if settings_input: if settings_input:
context_behavior = settings_input.context_behavior or settings_input.CONTEXT_BEHAVIOR context_behavior = settings_input.context_behavior or settings_input.CONTEXT_BEHAVIOR
@ -284,11 +292,6 @@ class ComponentRegistry:
tag_formatter=tag_formatter or app_settings.TAG_FORMATTER, tag_formatter=tag_formatter or app_settings.TAG_FORMATTER,
) )
self._settings = get_settings
settings = self._settings()
return settings
def register(self, name: str, component: Type["Component"]) -> None: def register(self, name: str, component: Type["Component"]) -> None:
""" """
Register a [`Component`](../api#django_components.Component) class Register a [`Component`](../api#django_components.Component) class
@ -330,6 +333,9 @@ class ComponentRegistry:
self._registry[name] = entry self._registry[name] = entry
# If the component class is deleted, unregister it from this registry.
finalize(entry.cls, lambda: self.unregister(name) if name in self._registry else None)
def unregister(self, name: str) -> None: def unregister(self, name: str) -> None:
""" """
Unregister the [`Component`](../api#django_components.Component) class Unregister the [`Component`](../api#django_components.Component) class
@ -360,22 +366,27 @@ class ComponentRegistry:
entry = self._registry[name] entry = self._registry[name]
tag = entry.tag tag = entry.tag
# Unregister the tag from library if this was the last component using this tag # Unregister the tag from library.
# Unlink component from tag # If this was the last component using this tag, unlink component from tag.
if tag in self._tags:
if name in self._tags[tag]:
self._tags[tag].remove(name) self._tags[tag].remove(name)
# Cleanup # Cleanup
is_tag_empty = not len(self._tags[tag]) is_tag_empty = not len(self._tags[tag])
if is_tag_empty: if is_tag_empty:
del self._tags[tag] self._tags.pop(tag, None)
else:
is_tag_empty = True
# Only unregister a tag if it's NOT protected # Only unregister a tag if it's NOT protected
is_protected = is_tag_protected(self.library, tag) is_protected = is_tag_protected(self.library, tag)
if not is_protected: if not is_protected:
# Unregister the tag from library if this was the last component using this tag # Unregister the tag from library if this was the last component using this tag
if is_tag_empty and tag in self.library.tags: if is_tag_empty and tag in self.library.tags:
del self.library.tags[tag] self.library.tags.pop(tag, None)
entry = self._registry[name]
del self._registry[name] del self._registry[name]
def get(self, name: str) -> Type["Component"]: def get(self, name: str) -> Type["Component"]:

View file

@ -4,7 +4,7 @@ from typing import Any, Dict, Optional, Type, Union, cast
from django.template import Context, Template from django.template import Context, Template
from django_components import Component, ComponentRegistry, NotRegistered, types from django_components import Component, ComponentRegistry, NotRegistered, types
from django_components.component_registry import all_registries from django_components.component_registry import ALL_REGISTRIES
class DynamicComponent(Component): class DynamicComponent(Component):
@ -170,7 +170,11 @@ class DynamicComponent(Component):
component_cls = registry.get(comp_name_or_class) component_cls = registry.get(comp_name_or_class)
else: else:
# Search all registries for the first match # Search all registries for the first match
for reg in all_registries: for reg_ref in ALL_REGISTRIES:
reg = reg_ref()
if not reg:
continue
try: try:
component_cls = reg.get(comp_name_or_class) component_cls = reg.get(comp_name_or_class)
break break

View file

@ -106,9 +106,16 @@ class StringifiedNode(Node):
def is_aggregate_key(key: str) -> bool: def is_aggregate_key(key: str) -> bool:
key = key.strip()
# NOTE: If we get a key that starts with `:`, like `:class`, we do not split it. # NOTE: If we get a key that starts with `:`, like `:class`, we do not split it.
# This syntax is used by Vue and AlpineJS. # This syntax is used by Vue and AlpineJS.
return ":" in key and not key.startswith(":") return (
":" in key
# `:` or `:class` is NOT ok
and not key.startswith(":")
# `attrs:class` is OK, but `attrs:` is NOT ok
and bool(key.split(":", maxsplit=1)[1])
)
# A string that must start and end with quotes, and somewhere inside includes # A string that must start and end with quotes, and somewhere inside includes

View file

@ -0,0 +1,9 @@
# Public API for test-related functionality
# isort: off
from django_components.util.testing import djc_test
# isort: on
__all__ = [
"djc_test",
]

View file

@ -0,0 +1,525 @@
import inspect
import sys
from functools import wraps
from typing import Any, Callable, Dict, Iterable, List, Optional, Sequence, Set, Tuple, Type, Union
from unittest.mock import patch
from weakref import ReferenceType
from django.conf import settings as _django_settings
from django.template import engines
from django.test import override_settings
from django_components.component import ALL_COMPONENTS, Component, component_node_subclasses_by_name
from django_components.component_media import ComponentMedia
from django_components.component_registry import ALL_REGISTRIES, ComponentRegistry
# NOTE: `ReferenceType` is NOT a generic pre-3.9
if sys.version_info >= (3, 9):
RegistryRef = ReferenceType[ComponentRegistry]
RegistriesCopies = List[Tuple[ReferenceType[ComponentRegistry], List[str]]]
InitialComponents = List[ReferenceType[Type[Component]]]
else:
RegistriesCopies = List[Tuple[ReferenceType, List[str]]]
InitialComponents = List[ReferenceType]
RegistryRef = ReferenceType
# Whether we're inside a test that was wrapped with `djc_test`.
# This is used so that we capture which modules we imported only if inside a test.
IS_TESTING = False
def is_testing() -> bool:
return IS_TESTING
class GenIdPatcher:
def __init__(self) -> None:
self._gen_id_count = 10599485
self._gen_id_patch: Any = None
# Mock the `generate` function used inside `gen_id` so it returns deterministic IDs
def start(self) -> None:
# Random number so that the generated IDs are "hex-looking", e.g. a1bc3d
self._gen_id_count = 10599485
def mock_gen_id(*args: Any, **kwargs: Any) -> str:
self._gen_id_count += 1
return hex(self._gen_id_count)[2:]
self._gen_id_patch = patch("django_components.util.misc.generate", side_effect=mock_gen_id)
self._gen_id_patch.start()
def stop(self) -> None:
if not self._gen_id_patch:
return
self._gen_id_patch.stop()
self._gen_id_patch = None
class CsrfTokenPatcher:
def __init__(self) -> None:
self._csrf_token = "predictabletoken"
self._csrf_token_patch: Any = None
def start(self) -> None:
self._csrf_token_patch = patch("django.middleware.csrf.get_token", return_value=self._csrf_token)
self._csrf_token_patch.start()
def stop(self) -> None:
if self._csrf_token_patch:
self._csrf_token_patch.stop()
self._csrf_token_patch = None
def djc_test(
django_settings: Union[Optional[Dict], Callable, Type] = None,
components_settings: Optional[Dict] = None,
# Input to `@pytest.mark.parametrize`
parametrize: Optional[
Union[
Tuple[
# names
Sequence[str],
# values
Sequence[Sequence[Any]],
],
Tuple[
# names
Sequence[str],
# values
Sequence[Sequence[Any]],
# ids
Optional[
Union[
Iterable[Union[None, str, float, int, bool]],
Callable[[Any], Optional[object]],
]
],
],
]
] = None,
) -> Callable:
"""
Decorator for testing components from django-components.
`@djc_test` manages the global state of django-components, ensuring that each test is properly
isolated and that components registered in one test do not affect other tests.
This decorator can be applied to a function, method, or a class. If applied to a class,
it will search for all methods that start with `test_`, and apply the decorator to them.
This is applied recursively to nested classes as well.
Examples:
Applying to a function:
```python
from django_components.testing import djc_test
@djc_test
def test_my_component():
@register("my_component")
class MyComponent(Component):
template = "..."
...
```
Applying to a class:
```python
from django_components.testing import djc_test
@djc_test
class TestMyComponent:
def test_something(self):
...
class Nested:
def test_something_else(self):
...
```
Applying to a class is the same as applying the decorator to each `test_` method individually:
```python
from django_components.testing import djc_test
class TestMyComponent:
@djc_test
def test_something(self):
...
class Nested:
@djc_test
def test_something_else(self):
...
```
To use `@djc_test`, Django must be set up first:
```python
import django
from django_components.testing import djc_test
django.setup()
@djc_test
def test_my_component():
...
```
**Arguments:**
- `django_settings`: Django settings, a dictionary passed to Django's
[`@override_settings`](https://docs.djangoproject.com/en/5.1/topics/testing/tools/#django.test.override_settings).
The test runs within the context of these overridden settings.
If `django_settings` contains django-components settings (`COMPONENTS` field), these are merged.
Other Django settings are simply overridden.
- `components_settings`: Instead of defining django-components settings under `django_settings["COMPONENTS"]`,
you can simply set the Components settings here.
These settings are merged with the django-components settings from `django_settings["COMPONENTS"]`.
Fields in `components_settings` override fields in `django_settings["COMPONENTS"]`.
- `parametrize`: Parametrize the test function with
[`pytest.mark.parametrize`](https://docs.pytest.org/en/stable/how-to/parametrize.html#pytest-mark-parametrize).
This requires [pytest](https://docs.pytest.org/) to be installed.
The input is a tuple of:
- `(param_names, param_values)` or
- `(param_names, param_values, ids)`
Example:
```py
from django_components.testing import djc_test
@djc_test(
parametrize=(
["input", "expected"],
[[1, "<div>1</div>"], [2, "<div>2</div>"]],
ids=["1", "2"]
)
)
def test_component(input, expected):
rendered = MyComponent(input=input).render()
assert rendered == expected
```
You can parametrize the Django or Components settings by setting up parameters called
`django_settings` and `components_settings`. These will be merged with the respetive settings
from the decorator.
Example of parametrizing context_behavior:
```python
from django_components.testing import djc_test
@djc_test(
components_settings={
# Settings shared by all tests
"app_dirs": ["custom_dir"],
},
parametrize=(
# Parametrized settings
["components_settings"],
[
[{"context_behavior": "django"}],
[{"context_behavior": "isolated"}],
],
["django", "isolated"],
)
)
def test_context_behavior(components_settings):
rendered = MyComponent().render()
...
```
**Settings resolution:**
`@djc_test` accepts settings from different sources. The settings are resolved in the following order:
- Django settings:
1. The defaults are the Django settings that Django was set up with.
2. Those are then overriden with fields in the `django_settings` kwarg.
3. The parametrized `django_settings` override the fields on the `django_settings` kwarg.
Priority: `django_settings` (parametrized) > `django_settings` > `django.conf.settings`
- Components settings:
1. Same as above, except that the `django_settings["COMPONENTS"]` field is merged instead of overridden.
2. The `components_settings` kwarg is then merged with the `django_settings["COMPONENTS"]` field.
3. The parametrized `components_settings` override the fields on the `components_settings` kwarg.
Priority: `components_settings` (parametrized) > `components_settings` > `django_settings["COMPONENTS"]` > `django.conf.settings.COMPONENTS`
""" # noqa: E501
def decorator(func: Callable) -> Callable:
if isinstance(func, type):
# If `djc_test` is applied to a class, we need to apply it to each test method
# individually.
# The rest of this function addresses `func` being a function
decorator = djc_test(
# Check for callable in case `@djc_test` was applied without calling it as `djc_test(settings)`.
django_settings=django_settings if not callable(django_settings) else None,
components_settings=components_settings,
parametrize=parametrize,
)
for name, attr in func.__dict__.items():
if isinstance(attr, type):
# If the attribute is a class, apply the decorator to its methods:
djc_test(
django_settings=django_settings if not callable(django_settings) else None,
components_settings=components_settings,
parametrize=parametrize,
)(attr)
if callable(attr) and name.startswith("test_"):
method = decorator(attr)
setattr(func, name, method)
return func
if getattr(func, "_djc_test_wrapped", False):
return func
gen_id_patcher = GenIdPatcher()
csrf_token_patcher = CsrfTokenPatcher()
# Contents of this function will run as the test
def _wrapper_impl(*args: Any, **kwargs: Any) -> Any:
# Merge the settings
current_django_settings = django_settings if not callable(django_settings) else None
current_django_settings = current_django_settings.copy() if current_django_settings else {}
if parametrize and "django_settings" in kwargs:
current_django_settings.update(kwargs["django_settings"])
current_component_settings = components_settings.copy() if components_settings else {}
if parametrize and "components_settings" in kwargs:
# We've received a parametrized test function, so we need to
# apply the parametrized settings to the test function.
current_component_settings.update(kwargs["components_settings"])
merged_settings = _merge_django_settings(
current_django_settings,
current_component_settings,
)
with override_settings(**merged_settings):
# Make a copy of `ALL_COMPONENTS` and `ALL_REGISTRIES` as they were before the test.
# Since the tests require Django to be configured, this should contain any
# components that were registered with autodiscovery / at `AppConfig.ready()`.
_ALL_COMPONENTS = ALL_COMPONENTS.copy()
_ALL_REGISTRIES_COPIES: RegistriesCopies = []
for reg_ref in ALL_REGISTRIES:
reg = reg_ref()
if not reg:
continue
_ALL_REGISTRIES_COPIES.append((reg_ref, list(reg._registry.keys())))
# Prepare global state
_setup_djc_global_state(gen_id_patcher, csrf_token_patcher)
def cleanup() -> None:
_clear_djc_global_state(
gen_id_patcher,
csrf_token_patcher,
_ALL_COMPONENTS, # type: ignore[arg-type]
_ALL_REGISTRIES_COPIES,
)
try:
# Execute
result = func(*args, **kwargs)
except Exception as err:
# On failure
cleanup()
raise err from None
# On success
cleanup()
return result
# Handle async test functions
if inspect.iscoroutinefunction(func):
async def wrapper_outer(*args: Any, **kwargs: Any) -> Any:
return await _wrapper_impl(*args, **kwargs)
else:
def wrapper_outer(*args: Any, **kwargs: Any) -> Any: # type: ignore[misc]
return _wrapper_impl(*args, **kwargs)
wrapper = wraps(func)(wrapper_outer)
# Allow to parametrize tests with pytest. This will effectively run the test multiple times,
# with the different parameter values, and will display these as separte tests in the report.
if parametrize:
# We optionally allow to pass in the `ids` kwarg. Since `ids` is kwarg-only,
# we can't just spread all tuple elements into the `parametrize` call, but we have
# to manually apply it.
if len(parametrize) == 3:
# @pytest.mark.parametrize("a,b,expected", testdata, ids=["forward", "backward"])
param_names, values, ids = parametrize
else:
# @pytest.mark.parametrize("a,b,expected", testdata)
param_names, values = parametrize
ids = None
for value in values:
# Validate that the first user-provided element in each value tuple is a dictionary,
# since it's meant to be used for overriding django-components settings
value_overrides = value[0]
if not isinstance(value_overrides, dict):
raise ValueError(
"The first element in each value tuple in `parametrize`"
f"must be a dictionary, but got {value_overrides}"
)
# NOTE: Lazily import pytest, so user can still run tests with plain `unittest`
# if they choose not to use parametrization.
import pytest
wrapper = pytest.mark.parametrize(param_names, values, ids=ids)(wrapper)
wrapper._djc_test_wrapped = True # type: ignore[attr-defined]
return wrapper
# Handle `@djc_test` (no arguments, func passed directly)
if callable(django_settings):
return decorator(django_settings)
# Handle `@djc_test(settings)`
return decorator
# Merge settings such that the fields in the `COMPONENTS` setting are merged.
def _merge_django_settings(
django_settings: Optional[Dict] = None,
components_settings: Optional[Dict] = None,
) -> Dict:
merged_settings = {} if not django_settings else django_settings.copy()
merged_settings["COMPONENTS"] = {
# Use the Django settings as they were before the `override_settings`
# as the defaults.
**(_django_settings.COMPONENTS if _django_settings.configured else {}),
**merged_settings.get("COMPONENTS", {}),
**(components_settings or {}),
}
return merged_settings
def _setup_djc_global_state(
gen_id_patcher: GenIdPatcher,
csrf_token_patcher: CsrfTokenPatcher,
) -> None:
# Declare that the code is running in test mode - this is used
# by the import / autodiscover mechanism to clean up loaded modules
# between tests.
global IS_TESTING
IS_TESTING = True
gen_id_patcher.start()
csrf_token_patcher.start()
# Re-load the settings, so that the test-specific settings overrides are applied
from django_components.app_settings import app_settings
app_settings._load_settings()
def _clear_djc_global_state(
gen_id_patcher: GenIdPatcher,
csrf_token_patcher: CsrfTokenPatcher,
initial_components: InitialComponents,
initial_registries_copies: RegistriesCopies,
) -> None:
gen_id_patcher.stop()
csrf_token_patcher.stop()
# Clear loader cache - That is, templates that were loaded with Django's `get_template()`.
# This applies to components that had the `template_name` / `template_field` field set.
# See https://stackoverflow.com/a/77531127/9788634
#
# If we don't do this, then, since the templates are cached, the next test might fail
# beause the IDs count will reset to 0, but we won't generate IDs for the Nodes of the cached
# templates. Thus, the IDs will be out of sync between the tests.
for engine in engines.all():
engine.engine.template_loaders[0].reset()
# NOTE: There are 1-2 tests which check Templates, so we need to clear the cache
from django_components.cache import component_media_cache, template_cache
if template_cache:
template_cache.clear()
if component_media_cache:
component_media_cache.clear()
# Remove cached Node subclasses
component_node_subclasses_by_name.clear()
# Clean up any loaded media (HTML, JS, CSS)
for comp_cls_ref in ALL_COMPONENTS:
comp_cls = comp_cls_ref()
if comp_cls is None:
continue
for file_attr, value_attr in [("template_file", "template"), ("js_file", "js"), ("css_file", "css")]:
# If both fields are set, then the value was set from the file field.
# Since we have some tests that check for these, we need to reset the state.
comp_media: ComponentMedia = comp_cls._component_media # type: ignore[attr-defined]
if getattr(comp_media, file_attr, None) and getattr(comp_media, value_attr, None):
# Remove the value field, so it's not used in the next test
setattr(comp_media, value_attr, None)
comp_media.reset()
# Remove components that were created during the test
initial_components_set = set(initial_components)
all_comps_len = len(ALL_COMPONENTS)
for index in range(all_comps_len):
reverse_index = all_comps_len - index - 1
comp_cls_ref = ALL_COMPONENTS[reverse_index]
if comp_cls_ref not in initial_components_set:
del ALL_COMPONENTS[reverse_index]
# Remove registries that were created during the test
initial_registries_set: Set[RegistryRef] = set([reg_ref for reg_ref, init_keys in initial_registries_copies])
for index in range(len(ALL_REGISTRIES)):
registry_ref = ALL_REGISTRIES[len(ALL_REGISTRIES) - index - 1]
if registry_ref not in initial_registries_set:
del ALL_REGISTRIES[len(ALL_REGISTRIES) - index - 1]
# For the remaining registries, unregistr components that were registered
# during tests.
# NOTE: The approach below does NOT take into account:
# - If a component was UNregistered during the test
# - If a previously-registered component was overwritten with different registration.
for reg_ref, init_keys in initial_registries_copies:
registry_original = reg_ref()
if not registry_original:
continue
# Get the keys that were registered during the test
initial_registered_keys = set(init_keys)
after_test_registered_keys = set(registry_original._registry.keys())
keys_registered_during_test = after_test_registered_keys - initial_registered_keys
# Remove them
for key in keys_registered_during_test:
registry_original.unregister(key)
# Delete autoimported modules from memory, so the module
# is executed also the next time one of the tests calls `autodiscover`.
from django_components.autodiscovery import LOADED_MODULES
for mod in LOADED_MODULES:
sys.modules.pop(mod, None)
LOADED_MODULES.clear()
global IS_TESTING
IS_TESTING = False

View file

@ -0,0 +1,29 @@
import sys
from typing import Any, Dict, TypeVar, overload
from weakref import ReferenceType, finalize, ref
GLOBAL_REFS: Dict[Any, ReferenceType] = {}
T = TypeVar("T")
# NOTE: `ReferenceType` is NOT a generic pre-3.9
if sys.version_info >= (3, 9):
@overload # type: ignore[misc]
def cached_ref(obj: T) -> ReferenceType[T]: ... # noqa: E704
def cached_ref(obj: Any) -> ReferenceType:
"""
Same as `weakref.ref()`, creating a weak reference to a given objet.
But unlike `weakref.ref()`, this function also caches the result,
so it returns the same reference for the same object.
"""
if obj not in GLOBAL_REFS:
GLOBAL_REFS[obj] = ref(obj)
# Remove this entry from GLOBAL_REFS when the object is deleted.
finalize(obj, lambda: GLOBAL_REFS.pop(obj))
return GLOBAL_REFS[obj]

View file

@ -1,54 +0,0 @@
from pathlib import Path
from typing import Dict, Optional
import django
from django.conf import settings
def setup_test_config(
components: Optional[Dict] = None,
extra_settings: Optional[Dict] = None,
):
if settings.configured:
return
default_settings = {
"BASE_DIR": Path(__file__).resolve().parent,
"INSTALLED_APPS": ("django_components", "tests.test_app"),
"TEMPLATES": [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [
"tests/templates/",
"tests/components/", # Required for template relative imports in tests
],
"OPTIONS": {
"builtins": [
"django_components.templatetags.component_tags",
]
},
}
],
"COMPONENTS": {
"template_cache_size": 128,
**(components or {}),
},
"MIDDLEWARE": ["django_components.middleware.ComponentDependencyMiddleware"],
"DATABASES": {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": ":memory:",
}
},
"SECRET_KEY": "secret",
"ROOT_URLCONF": "django_components.urls",
}
settings.configure(
**{
**default_settings,
**(extra_settings or {}),
}
)
django.setup()

View file

@ -1,86 +1,66 @@
import re
import pytest
from django.template import Context, Template, TemplateSyntaxError from django.template import Context, Template, TemplateSyntaxError
from django.utils.safestring import SafeString, mark_safe from django.utils.safestring import SafeString, mark_safe
from pytest_django.asserts import assertHTMLEqual
from django_components import Component, register, types from django_components import Component, register, types
from django_components.attributes import append_attributes, attributes_to_string from django_components.attributes import append_attributes, attributes_to_string
from django_components.testing import djc_test
from .django_test_setup import setup_test_config from .testutils import PARAMETRIZE_CONTEXT_BEHAVIOR, setup_test_config
from .testutils import BaseTestCase, parametrize_context_behavior
setup_test_config({"autodiscover": False}) setup_test_config({"autodiscover": False})
class AttributesToStringTest(BaseTestCase): @djc_test
class TestAttributesToString:
def test_simple_attribute(self): def test_simple_attribute(self):
self.assertEqual( assert attributes_to_string({"foo": "bar"}) == 'foo="bar"'
attributes_to_string({"foo": "bar"}),
'foo="bar"',
)
def test_multiple_attributes(self): def test_multiple_attributes(self):
self.assertEqual( assert attributes_to_string({"class": "foo", "style": "color: red;"}) == 'class="foo" style="color: red;"'
attributes_to_string({"class": "foo", "style": "color: red;"}),
'class="foo" style="color: red;"',
)
def test_escapes_special_characters(self): def test_escapes_special_characters(self):
self.assertEqual( assert attributes_to_string({"x-on:click": "bar", "@click": "'baz'"}) == 'x-on:click="bar" @click="&#x27;baz&#x27;"' # noqa: E501
attributes_to_string({"x-on:click": "bar", "@click": "'baz'"}),
'x-on:click="bar" @click="&#x27;baz&#x27;"',
)
def test_does_not_escape_special_characters_if_safe_string(self): def test_does_not_escape_special_characters_if_safe_string(self):
self.assertEqual( assert attributes_to_string({"foo": mark_safe("'bar'")}) == "foo=\"'bar'\""
attributes_to_string({"foo": mark_safe("'bar'")}),
"foo=\"'bar'\"",
)
def test_result_is_safe_string(self): def test_result_is_safe_string(self):
result = attributes_to_string({"foo": mark_safe("'bar'")}) result = attributes_to_string({"foo": mark_safe("'bar'")})
self.assertTrue(isinstance(result, SafeString)) assert isinstance(result, SafeString)
def test_attribute_with_no_value(self): def test_attribute_with_no_value(self):
self.assertEqual( assert attributes_to_string({"required": None}) == ""
attributes_to_string({"required": None}),
"",
)
def test_attribute_with_false_value(self): def test_attribute_with_false_value(self):
self.assertEqual( assert attributes_to_string({"required": False}) == ""
attributes_to_string({"required": False}),
"",
)
def test_attribute_with_true_value(self): def test_attribute_with_true_value(self):
self.assertEqual( assert attributes_to_string({"required": True}) == "required"
attributes_to_string({"required": True}),
"required",
)
class AppendAttributesTest(BaseTestCase): @djc_test
class TestAppendAttributes:
def test_single_dict(self): def test_single_dict(self):
self.assertEqual( assert append_attributes(("foo", "bar")) == {"foo": "bar"}
append_attributes(("foo", "bar")),
{"foo": "bar"},
)
def test_appends_dicts(self): def test_appends_dicts(self):
self.assertEqual( assert append_attributes(("class", "foo"), ("id", "bar"), ("class", "baz")) == {"class": "foo baz", "id": "bar"} # noqa: E501
append_attributes(("class", "foo"), ("id", "bar"), ("class", "baz")),
{"class": "foo baz", "id": "bar"},
)
class HtmlAttrsTests(BaseTestCase): @djc_test
class TestHtmlAttrs:
template_str: types.django_html = """ template_str: types.django_html = """
{% load component_tags %} {% load component_tags %}
{% component "test" attrs:@click.stop="dispatch('click_event')" attrs:x-data="{hello: 'world'}" attrs:class=class_var %} {% component "test" attrs:@click.stop="dispatch('click_event')" attrs:x-data="{hello: 'world'}" attrs:class=class_var %}
{% endcomponent %} {% endcomponent %}
""" # noqa: E501 """ # noqa: E501
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_tag_positional_args(self): def test_tag_positional_args(self, components_settings):
@register("test") @register("test")
class AttrsComponent(Component): class AttrsComponent(Component):
template: types.django_html = """ template: types.django_html = """
@ -98,7 +78,7 @@ class HtmlAttrsTests(BaseTestCase):
template = Template(self.template_str) template = Template(self.template_str)
rendered = template.render(Context({"class_var": "padding-top-8"})) rendered = template.render(Context({"class_var": "padding-top-8"}))
self.assertHTMLEqual( assertHTMLEqual(
rendered, rendered,
""" """
<div @click.stop="dispatch('click_event')" x-data="{hello: 'world'}" class="padding-top-8 added_class another-class" data-djc-id-a1bc3f data-id=123> <div @click.stop="dispatch('click_event')" x-data="{hello: 'world'}" class="padding-top-8 added_class another-class" data-djc-id-a1bc3f data-id=123>
@ -106,7 +86,7 @@ class HtmlAttrsTests(BaseTestCase):
</div> </div>
""", # noqa: E501 """, # noqa: E501
) )
self.assertNotIn("override-me", rendered) assert "override-me" not in rendered
def test_tag_raises_on_extra_positional_args(self): def test_tag_raises_on_extra_positional_args(self):
@register("test") @register("test")
@ -127,8 +107,9 @@ class HtmlAttrsTests(BaseTestCase):
template = Template(self.template_str) template = Template(self.template_str)
with self.assertRaisesMessage( with pytest.raises(
TypeError, "Invalid parameters for tag 'html_attrs': takes 2 positional argument(s) but more were given" TypeError,
match=re.escape("Invalid parameters for tag 'html_attrs': takes 2 positional argument(s) but more were given"), # noqa: E501
): ):
template.render(Context({"class_var": "padding-top-8"})) template.render(Context({"class_var": "padding-top-8"}))
@ -150,7 +131,7 @@ class HtmlAttrsTests(BaseTestCase):
template = Template(self.template_str) template = Template(self.template_str)
rendered = template.render(Context({"class_var": "padding-top-8"})) rendered = template.render(Context({"class_var": "padding-top-8"}))
self.assertHTMLEqual( assertHTMLEqual(
rendered, rendered,
""" """
<div @click.stop="dispatch('click_event')" class="added_class another-class padding-top-8" data-djc-id-a1bc3f data-id="123" x-data="{hello: 'world'}"> <div @click.stop="dispatch('click_event')" class="added_class another-class padding-top-8" data-djc-id-a1bc3f data-id="123" x-data="{hello: 'world'}">
@ -158,7 +139,7 @@ class HtmlAttrsTests(BaseTestCase):
</div> </div>
""", # noqa: E501 """, # noqa: E501
) )
self.assertNotIn("override-me", rendered) assert "override-me" not in rendered
def test_tag_kwargs_2(self): def test_tag_kwargs_2(self):
@register("test") @register("test")
@ -178,7 +159,7 @@ class HtmlAttrsTests(BaseTestCase):
template = Template(self.template_str) template = Template(self.template_str)
rendered = template.render(Context({"class_var": "padding-top-8"})) rendered = template.render(Context({"class_var": "padding-top-8"}))
self.assertHTMLEqual( assertHTMLEqual(
rendered, rendered,
""" """
<div @click.stop="dispatch('click_event')" x-data="{hello: 'world'}" class="padding-top-8 added_class another-class" data-djc-id-a1bc3f data-id=123> <div @click.stop="dispatch('click_event')" x-data="{hello: 'world'}" class="padding-top-8 added_class another-class" data-djc-id-a1bc3f data-id=123>
@ -186,7 +167,7 @@ class HtmlAttrsTests(BaseTestCase):
</div> </div>
""", # noqa: E501 """, # noqa: E501
) )
self.assertNotIn("override-me", rendered) assert "override-me" not in rendered
def test_tag_spread(self): def test_tag_spread(self):
@register("test") @register("test")
@ -210,7 +191,7 @@ class HtmlAttrsTests(BaseTestCase):
template = Template(self.template_str) template = Template(self.template_str)
rendered = template.render(Context({"class_var": "padding-top-8"})) rendered = template.render(Context({"class_var": "padding-top-8"}))
self.assertHTMLEqual( assertHTMLEqual(
rendered, rendered,
""" """
<div @click.stop="dispatch('click_event')" class="added_class another-class padding-top-8" data-djc-id-a1bc3f data-id="123" x-data="{hello: 'world'}"> <div @click.stop="dispatch('click_event')" class="added_class another-class padding-top-8" data-djc-id-a1bc3f data-id="123" x-data="{hello: 'world'}">
@ -218,7 +199,7 @@ class HtmlAttrsTests(BaseTestCase):
</div> </div>
""", # noqa: E501 """, # noqa: E501
) )
self.assertNotIn("override-me", rendered) assert "override-me" not in rendered
def test_tag_aggregate_args(self): def test_tag_aggregate_args(self):
@register("test") @register("test")
@ -237,7 +218,7 @@ class HtmlAttrsTests(BaseTestCase):
rendered = template.render(Context({"class_var": "padding-top-8"})) rendered = template.render(Context({"class_var": "padding-top-8"}))
# NOTE: The attrs from self.template_str should be ignored because they are not used. # NOTE: The attrs from self.template_str should be ignored because they are not used.
self.assertHTMLEqual( assertHTMLEqual(
rendered, rendered,
""" """
<div class="added_class another-class from_agg_key" data-djc-id-a1bc3f data-id="123" type="submit"> <div class="added_class another-class from_agg_key" data-djc-id-a1bc3f data-id="123" type="submit">
@ -245,7 +226,7 @@ class HtmlAttrsTests(BaseTestCase):
</div> </div>
""", # noqa: E501 """, # noqa: E501
) )
self.assertNotIn("override-me", rendered) assert "override-me" not in rendered
# Note: Because there's both `attrs:class` and `defaults:class`, the `attrs`, # Note: Because there's both `attrs:class` and `defaults:class`, the `attrs`,
# it's as if the template tag call was (ignoring the `class` and `data-id` attrs): # it's as if the template tag call was (ignoring the `class` and `data-id` attrs):
@ -268,8 +249,9 @@ class HtmlAttrsTests(BaseTestCase):
template = Template(self.template_str) template = Template(self.template_str)
with self.assertRaisesMessage( with pytest.raises(
TypeError, "Invalid parameters for tag 'html_attrs': got multiple values for argument 'attrs'" TypeError,
match=re.escape("Invalid parameters for tag 'html_attrs': got multiple values for argument 'attrs'"),
): ):
template.render(Context({"class_var": "padding-top-8"})) template.render(Context({"class_var": "padding-top-8"}))
@ -295,9 +277,9 @@ class HtmlAttrsTests(BaseTestCase):
template = Template(self.template_str) template = Template(self.template_str)
with self.assertRaisesMessage( with pytest.raises(
TemplateSyntaxError, TemplateSyntaxError,
"Received argument 'defaults' both as a regular input", match=re.escape("Received argument 'defaults' both as a regular input"),
): ):
template.render(Context({"class_var": "padding-top-8"})) template.render(Context({"class_var": "padding-top-8"}))
@ -316,7 +298,7 @@ class HtmlAttrsTests(BaseTestCase):
template = Template(self.template_str) template = Template(self.template_str)
rendered = template.render(Context({"class_var": "padding-top-8"})) rendered = template.render(Context({"class_var": "padding-top-8"}))
self.assertHTMLEqual( assertHTMLEqual(
rendered, rendered,
""" """
<div class="added_class another-class override-me" data-djc-id-a1bc3f data-id=123> <div class="added_class another-class override-me" data-djc-id-a1bc3f data-id=123>
@ -345,7 +327,7 @@ class HtmlAttrsTests(BaseTestCase):
""" # noqa: E501 """ # noqa: E501
template = Template(template_str) template = Template(template_str)
rendered = template.render(Context({"class_var": "padding-top-8"})) rendered = template.render(Context({"class_var": "padding-top-8"}))
self.assertHTMLEqual( assertHTMLEqual(
rendered, rendered,
""" """
<div @click.stop="dispatch('click_event')" x-data="{hello: 'world'}" class="padding-top-8 added_class another-class" data-djc-id-a1bc3f data-id=123> <div @click.stop="dispatch('click_event')" x-data="{hello: 'world'}" class="padding-top-8 added_class another-class" data-djc-id-a1bc3f data-id=123>
@ -353,7 +335,7 @@ class HtmlAttrsTests(BaseTestCase):
</div> </div>
""", # noqa: E501 """, # noqa: E501
) )
self.assertNotIn("override-me", rendered) assert "override-me" not in rendered
def test_tag_no_attrs_no_defaults(self): def test_tag_no_attrs_no_defaults(self):
@register("test") @register("test")
@ -370,7 +352,7 @@ class HtmlAttrsTests(BaseTestCase):
template = Template(self.template_str) template = Template(self.template_str)
rendered = template.render(Context({"class_var": "padding-top-8"})) rendered = template.render(Context({"class_var": "padding-top-8"}))
self.assertHTMLEqual( assertHTMLEqual(
rendered, rendered,
""" """
<div class="added_class another-class" data-djc-id-a1bc3f data-id="123"> <div class="added_class another-class" data-djc-id-a1bc3f data-id="123">
@ -378,7 +360,7 @@ class HtmlAttrsTests(BaseTestCase):
</div> </div>
""", """,
) )
self.assertNotIn("override-me", rendered) assert "override-me" not in rendered
def test_tag_empty(self): def test_tag_empty(self):
@register("test") @register("test")
@ -398,7 +380,7 @@ class HtmlAttrsTests(BaseTestCase):
template = Template(self.template_str) template = Template(self.template_str)
rendered = template.render(Context({"class_var": "padding-top-8"})) rendered = template.render(Context({"class_var": "padding-top-8"}))
self.assertHTMLEqual( assertHTMLEqual(
rendered, rendered,
""" """
<div data-djc-id-a1bc3f> <div data-djc-id-a1bc3f>
@ -406,7 +388,7 @@ class HtmlAttrsTests(BaseTestCase):
</div> </div>
""", """,
) )
self.assertNotIn("override-me", rendered) assert "override-me" not in rendered
def test_tag_null_attrs_and_defaults(self): def test_tag_null_attrs_and_defaults(self):
@register("test") @register("test")
@ -426,7 +408,7 @@ class HtmlAttrsTests(BaseTestCase):
template = Template(self.template_str) template = Template(self.template_str)
rendered = template.render(Context({"class_var": "padding-top-8"})) rendered = template.render(Context({"class_var": "padding-top-8"}))
self.assertHTMLEqual( assertHTMLEqual(
rendered, rendered,
""" """
<div data-djc-id-a1bc3f> <div data-djc-id-a1bc3f>
@ -434,4 +416,4 @@ class HtmlAttrsTests(BaseTestCase):
</div> </div>
""", """,
) )
self.assertNotIn("override-me", rendered) assert "override-me" not in rendered

View file

@ -1,65 +1,60 @@
import sys import sys
from unittest import TestCase
from django.conf import settings import pytest
from django_components import AlreadyRegistered, registry from django_components import AlreadyRegistered, registry
from django_components.autodiscovery import autodiscover, import_libraries from django_components.autodiscovery import autodiscover, import_libraries
from django_components.testing import djc_test
from .django_test_setup import setup_test_config from .testutils import setup_test_config
# NOTE: This is different from BaseTestCase in testutils.py, because here we need
# TestCase instead of SimpleTestCase.
class _TestCase(TestCase):
def tearDown(self) -> None:
super().tearDown()
registry.clear()
class TestAutodiscover(_TestCase):
def test_autodiscover(self):
setup_test_config({"autodiscover": False}) setup_test_config({"autodiscover": False})
@djc_test
class TestAutodiscover:
def test_autodiscover(self):
all_components = registry.all().copy() all_components = registry.all().copy()
self.assertNotIn("single_file_component", all_components) assert "single_file_component" not in all_components
self.assertNotIn("multi_file_component", all_components) assert "multi_file_component" not in all_components
self.assertNotIn("relative_file_component", all_components) assert "relative_file_component" not in all_components
self.assertNotIn("relative_file_pathobj_component", all_components) assert "relative_file_pathobj_component" not in all_components
try: try:
modules = autodiscover(map_module=lambda p: "tests." + p if p.startswith("components") else p) modules = autodiscover(map_module=lambda p: "tests." + p if p.startswith("components") else p)
except AlreadyRegistered: except AlreadyRegistered:
self.fail("Autodiscover should not raise AlreadyRegistered exception") pytest.fail("Autodiscover should not raise AlreadyRegistered exception")
self.assertIn("tests.components", modules) assert "tests.components" in modules
self.assertIn("tests.components.single_file", modules) assert "tests.components.single_file" in modules
self.assertIn("tests.components.staticfiles.staticfiles", modules) assert "tests.components.staticfiles.staticfiles" in modules
self.assertIn("tests.components.multi_file.multi_file", modules) assert "tests.components.multi_file.multi_file" in modules
self.assertIn("tests.components.relative_file_pathobj.relative_file_pathobj", modules) assert "tests.components.relative_file_pathobj.relative_file_pathobj" in modules
self.assertIn("tests.components.relative_file.relative_file", modules) assert "tests.components.relative_file.relative_file" in modules
self.assertIn("tests.test_app.components.app_lvl_comp.app_lvl_comp", modules) assert "tests.test_app.components.app_lvl_comp.app_lvl_comp" in modules
self.assertIn("django_components.components", modules) assert "django_components.components" in modules
self.assertIn("django_components.components.dynamic", modules) assert "django_components.components.dynamic" in modules
all_components = registry.all().copy() all_components = registry.all().copy()
self.assertIn("single_file_component", all_components) assert "single_file_component" in all_components
self.assertIn("multi_file_component", all_components) assert "multi_file_component" in all_components
self.assertIn("relative_file_component", all_components) assert "relative_file_component" in all_components
self.assertIn("relative_file_pathobj_component", all_components) assert "relative_file_pathobj_component" in all_components
class TestImportLibraries(_TestCase): @djc_test
class TestImportLibraries:
@djc_test(
components_settings={
"libraries": ["tests.components.single_file", "tests.components.multi_file.multi_file"]
}
)
def test_import_libraries(self): def test_import_libraries(self):
# Prepare settings
setup_test_config({"autodiscover": False})
settings.COMPONENTS["libraries"] = ["tests.components.single_file", "tests.components.multi_file.multi_file"]
# Ensure we start with a clean state # Ensure we start with a clean state
registry.clear() registry.clear()
all_components = registry.all().copy() all_components = registry.all().copy()
self.assertNotIn("single_file_component", all_components) assert "single_file_component" not in all_components
self.assertNotIn("multi_file_component", all_components) assert "multi_file_component" not in all_components
# Ensure that the modules are executed again after import # Ensure that the modules are executed again after import
if "tests.components.single_file" in sys.modules: if "tests.components.single_file" in sys.modules:
@ -70,31 +65,26 @@ class TestImportLibraries(_TestCase):
try: try:
modules = import_libraries() modules = import_libraries()
except AlreadyRegistered: except AlreadyRegistered:
self.fail("Autodiscover should not raise AlreadyRegistered exception") pytest.fail("Autodiscover should not raise AlreadyRegistered exception")
self.assertIn("tests.components.single_file", modules) assert "tests.components.single_file" in modules
self.assertIn("tests.components.multi_file.multi_file", modules) assert "tests.components.multi_file.multi_file" in modules
all_components = registry.all().copy() all_components = registry.all().copy()
self.assertIn("single_file_component", all_components) assert "single_file_component" in all_components
self.assertIn("multi_file_component", all_components) assert "multi_file_component" in all_components
settings.COMPONENTS["libraries"] = [] @djc_test(
components_settings={
def test_import_libraries_map_modules(self): "libraries": ["components.single_file", "components.multi_file.multi_file"]
# Prepare settings
setup_test_config(
{
"autodiscover": False,
} }
) )
settings.COMPONENTS["libraries"] = ["components.single_file", "components.multi_file.multi_file"] def test_import_libraries_map_modules(self):
# Ensure we start with a clean state # Ensure we start with a clean state
registry.clear() registry.clear()
all_components = registry.all().copy() all_components = registry.all().copy()
self.assertNotIn("single_file_component", all_components) assert "single_file_component" not in all_components
self.assertNotIn("multi_file_component", all_components) assert "multi_file_component" not in all_components
# Ensure that the modules are executed again after import # Ensure that the modules are executed again after import
if "tests.components.single_file" in sys.modules: if "tests.components.single_file" in sys.modules:
@ -105,13 +95,11 @@ class TestImportLibraries(_TestCase):
try: try:
modules = import_libraries(map_module=lambda p: "tests." + p if p.startswith("components") else p) modules = import_libraries(map_module=lambda p: "tests." + p if p.startswith("components") else p)
except AlreadyRegistered: except AlreadyRegistered:
self.fail("Autodiscover should not raise AlreadyRegistered exception") pytest.fail("Autodiscover should not raise AlreadyRegistered exception")
self.assertIn("tests.components.single_file", modules) assert "tests.components.single_file" in modules
self.assertIn("tests.components.multi_file.multi_file", modules) assert "tests.components.multi_file.multi_file" in modules
all_components = registry.all().copy() all_components = registry.all().copy()
self.assertIn("single_file_component", all_components) assert "single_file_component" in all_components
self.assertIn("multi_file_component", all_components) assert "multi_file_component" in all_components
settings.COMPONENTS["libraries"] = []

View file

@ -6431,17 +6431,10 @@ def project_output_form(context: Context, data: ProjectOutputFormData):
# The code above is used also used when benchmarking. # The code above is used also used when benchmarking.
# The section below is NOT included. # The section below is NOT included.
from .testutils import CsrfTokenPatcher, GenIdPatcher # noqa: E402 from django_components.testing import djc_test # noqa: E402
@djc_test
def test_render(snapshot): def test_render(snapshot):
id_patcher = GenIdPatcher()
id_patcher.start()
csrf_token_patcher = CsrfTokenPatcher()
csrf_token_patcher.start()
data = gen_render_data() data = gen_render_data()
rendered = render(data) rendered = render(data)
assert rendered == snapshot assert rendered == snapshot
id_patcher.stop()
csrf_token_patcher.stop()

View file

@ -332,17 +332,10 @@ def button(context: Context, data: ButtonData):
# The code above is used also used when benchmarking. # The code above is used also used when benchmarking.
# The section below is NOT included. # The section below is NOT included.
from .testutils import CsrfTokenPatcher, GenIdPatcher # noqa: E402 from django_components.testing import djc_test # noqa: E402
@djc_test
def test_render(snapshot): def test_render(snapshot):
id_patcher = GenIdPatcher()
id_patcher.start()
csrf_token_patcher = CsrfTokenPatcher()
csrf_token_patcher.start()
data = gen_render_data() data = gen_render_data()
rendered = render(data) rendered = render(data)
assert rendered == snapshot assert rendered == snapshot
id_patcher.stop()
csrf_token_patcher.stop()

View file

@ -6007,14 +6007,10 @@ class ProjectOutputForm(Component):
# The code above is used also used when benchmarking. # The code above is used also used when benchmarking.
# The section below is NOT included. # The section below is NOT included.
from .testutils import CsrfTokenPatcher, GenIdPatcher # noqa: E402 from django_components.testing import djc_test # noqa: E402
@djc_test
def test_render(snapshot): def test_render(snapshot):
id_patcher = GenIdPatcher()
id_patcher.start()
csrf_token_patcher = CsrfTokenPatcher()
csrf_token_patcher.start()
registry.register("Button", Button) registry.register("Button", Button)
registry.register("Menu", Menu) registry.register("Menu", Menu)
registry.register("MenuList", MenuList) registry.register("MenuList", MenuList)
@ -6055,6 +6051,3 @@ def test_render(snapshot):
data = gen_render_data() data = gen_render_data()
rendered = render(data) rendered = render(data)
assert rendered == snapshot assert rendered == snapshot
id_patcher.stop()
csrf_token_patcher.stop()

View file

@ -335,17 +335,10 @@ class Button(Component):
# The code above is used also used when benchmarking. # The code above is used also used when benchmarking.
# The section below is NOT included. # The section below is NOT included.
from .testutils import CsrfTokenPatcher, GenIdPatcher # noqa: E402 from django_components.testing import djc_test # noqa: E402
@djc_test
def test_render(snapshot): def test_render(snapshot):
id_patcher = GenIdPatcher()
id_patcher.start()
csrf_token_patcher = CsrfTokenPatcher()
csrf_token_patcher.start()
data = gen_render_data() data = gen_render_data()
rendered = render(data) rendered = render(data)
assert rendered == snapshot assert rendered == snapshot
id_patcher.stop()
csrf_token_patcher.stop()

View file

@ -1,15 +1,16 @@
from django.test import TestCase, override_settings
from django.core.cache.backends.locmem import LocMemCache from django.core.cache.backends.locmem import LocMemCache
from django_components.util.cache import LRUCache
from django_components import Component, register from django_components import Component, register
from django_components.testing import djc_test
from django_components.util.cache import LRUCache
from .django_test_setup import setup_test_config from .testutils import setup_test_config
setup_test_config({"autodiscover": False}) setup_test_config({"autodiscover": False})
class CacheTests(TestCase): @djc_test
class TestCache:
def test_cache(self): def test_cache(self):
cache = LRUCache[int](maxsize=3) cache = LRUCache[int](maxsize=3)
@ -17,58 +18,59 @@ class CacheTests(TestCase):
cache.set("b", 2) cache.set("b", 2)
cache.set("c", 3) cache.set("c", 3)
self.assertEqual(cache.get("a"), 1) assert cache.get("a") == 1
self.assertEqual(cache.get("b"), 2) assert cache.get("b") == 2
self.assertEqual(cache.get("c"), 3) assert cache.get("c") == 3
cache.set("d", 4) cache.set("d", 4)
self.assertEqual(cache.get("a"), None) assert cache.get("a") is None
self.assertEqual(cache.get("b"), 2) assert cache.get("b") == 2
self.assertEqual(cache.get("c"), 3) assert cache.get("c") == 3
self.assertEqual(cache.get("d"), 4) assert cache.get("d") == 4
cache.set("e", 5) cache.set("e", 5)
cache.set("f", 6) cache.set("f", 6)
self.assertEqual(cache.get("b"), None) assert cache.get("b") is None
self.assertEqual(cache.get("c"), None) assert cache.get("c") is None
self.assertEqual(cache.get("d"), 4) assert cache.get("d") == 4
self.assertEqual(cache.get("e"), 5) assert cache.get("e") == 5
self.assertEqual(cache.get("f"), 6) assert cache.get("f") == 6
cache.clear() cache.clear()
self.assertEqual(cache.get("d"), None) assert cache.get("d") is None
self.assertEqual(cache.get("e"), None) assert cache.get("e") is None
self.assertEqual(cache.get("f"), None) assert cache.get("f") is None
def test_cache_maxsize_zero(self): def test_cache_maxsize_zero(self):
cache = LRUCache[int](maxsize=0) cache = LRUCache[int](maxsize=0)
cache.set("a", 1) cache.set("a", 1)
self.assertEqual(cache.get("a"), None) assert cache.get("a") is None
cache.set("b", 2) cache.set("b", 2)
cache.set("c", 3) cache.set("c", 3)
self.assertEqual(cache.get("b"), None) assert cache.get("b") is None
self.assertEqual(cache.get("c"), None) assert cache.get("c") is None
# Same with negative numbers # Same with negative numbers
cache = LRUCache[int](maxsize=-1) cache = LRUCache[int](maxsize=-1)
cache.set("a", 1) cache.set("a", 1)
self.assertEqual(cache.get("a"), None) assert cache.get("a") is None
cache.set("b", 2) cache.set("b", 2)
cache.set("c", 3) cache.set("c", 3)
self.assertEqual(cache.get("b"), None) assert cache.get("b") is None
self.assertEqual(cache.get("c"), None) assert cache.get("c") is None
class ComponentMediaCacheTests(TestCase): @djc_test
def setUp(self): class TestComponentMediaCache:
# Create a custom locmem cache for testing @djc_test(components_settings={"cache": "test-cache"})
self.test_cache = LocMemCache( def test_component_media_caching(self):
test_cache = LocMemCache(
"test-cache", "test-cache",
{ {
"TIMEOUT": None, # No timeout "TIMEOUT": None, # No timeout
@ -77,8 +79,6 @@ class ComponentMediaCacheTests(TestCase):
}, },
) )
@override_settings(COMPONENTS={"cache": "test-cache"})
def test_component_media_caching(self):
@register("test_simple") @register("test_simple")
class TestSimpleComponent(Component): class TestSimpleComponent(Component):
template = """ template = """
@ -123,28 +123,22 @@ class ComponentMediaCacheTests(TestCase):
# Register our test cache # Register our test cache
from django.core.cache import caches from django.core.cache import caches
caches["test-cache"] = self.test_cache caches["test-cache"] = test_cache
# Render the components to trigger caching # Render the components to trigger caching
TestMediaAndVarsComponent.render() TestMediaAndVarsComponent.render()
# Check that JS/CSS is cached for components that have them # Check that JS/CSS is cached for components that have them
self.assertTrue(self.test_cache.has_key(f"__components:{TestMediaAndVarsComponent._class_hash}:js")) assert test_cache.has_key(f"__components:{TestMediaAndVarsComponent._class_hash}:js")
self.assertTrue(self.test_cache.has_key(f"__components:{TestMediaAndVarsComponent._class_hash}:css")) assert test_cache.has_key(f"__components:{TestMediaAndVarsComponent._class_hash}:css")
self.assertTrue(self.test_cache.has_key(f"__components:{TestMediaNoVarsComponent._class_hash}:js")) assert test_cache.has_key(f"__components:{TestMediaNoVarsComponent._class_hash}:js")
self.assertTrue(self.test_cache.has_key(f"__components:{TestMediaNoVarsComponent._class_hash}:css")) assert test_cache.has_key(f"__components:{TestMediaNoVarsComponent._class_hash}:css")
self.assertFalse(self.test_cache.has_key(f"__components:{TestSimpleComponent._class_hash}:js")) assert not test_cache.has_key(f"__components:{TestSimpleComponent._class_hash}:js")
self.assertFalse(self.test_cache.has_key(f"__components:{TestSimpleComponent._class_hash}:css")) assert not test_cache.has_key(f"__components:{TestSimpleComponent._class_hash}:css")
# Check that we cache `Component.js` / `Component.css` # Check that we cache `Component.js` / `Component.css`
self.assertEqual( assert test_cache.get(f"__components:{TestMediaNoVarsComponent._class_hash}:js").strip() == "console.log('Hello from JS');" # noqa: E501
self.test_cache.get(f"__components:{TestMediaNoVarsComponent._class_hash}:js").strip(), assert test_cache.get(f"__components:{TestMediaNoVarsComponent._class_hash}:css").strip() == ".novars-component { color: blue; }" # noqa: E501
"console.log('Hello from JS');",
)
self.assertEqual(
self.test_cache.get(f"__components:{TestMediaNoVarsComponent._class_hash}:css").strip(),
".novars-component { color: blue; }",
)
# Check that we cache JS / CSS scripts generated from `get_js_data` / `get_css_data` # Check that we cache JS / CSS scripts generated from `get_js_data` / `get_css_data`
# NOTE: The hashes is generated from the data. # NOTE: The hashes is generated from the data.
@ -152,11 +146,5 @@ class ComponentMediaCacheTests(TestCase):
css_vars_hash = "d039a3" css_vars_hash = "d039a3"
# TODO - Update once JS and CSS vars are enabled # TODO - Update once JS and CSS vars are enabled
self.assertEqual( assert test_cache.get(f"__components:{TestMediaAndVarsComponent._class_hash}:js:{js_vars_hash}").strip() == ""
self.test_cache.get(f"__components:{TestMediaAndVarsComponent._class_hash}:js:{js_vars_hash}").strip(), assert test_cache.get(f"__components:{TestMediaAndVarsComponent._class_hash}:css:{css_vars_hash}").strip() == "" # noqa: E501
"",
)
self.assertEqual(
self.test_cache.get(f"__components:{TestMediaAndVarsComponent._class_hash}:css:{css_vars_hash}").strip(),
"",
)

View file

@ -3,43 +3,43 @@ import tempfile
from io import StringIO from io import StringIO
from shutil import rmtree from shutil import rmtree
import pytest
from django.core.management import call_command from django.core.management import call_command
from django.core.management.base import CommandError from django.core.management.base import CommandError
from django.test import TestCase
from .django_test_setup import setup_test_config from django_components.testing import djc_test
from .testutils import setup_test_config
setup_test_config() setup_test_config()
class CreateComponentCommandTest(TestCase): @djc_test
def setUp(self): class TestCreateComponentCommand:
super().setUp()
self.temp_dir = tempfile.mkdtemp()
def tearDown(self):
super().tearDown()
rmtree(self.temp_dir)
def test_default_file_names(self): def test_default_file_names(self):
temp_dir = tempfile.mkdtemp()
component_name = "defaultcomponent" component_name = "defaultcomponent"
call_command("startcomponent", component_name, "--path", self.temp_dir) call_command("startcomponent", component_name, "--path", temp_dir)
expected_files = [ expected_files = [
os.path.join(self.temp_dir, component_name, "script.js"), os.path.join(temp_dir, component_name, "script.js"),
os.path.join(self.temp_dir, component_name, "style.css"), os.path.join(temp_dir, component_name, "style.css"),
os.path.join(self.temp_dir, component_name, "template.html"), os.path.join(temp_dir, component_name, "template.html"),
] ]
for file_path in expected_files: for file_path in expected_files:
self.assertTrue(os.path.exists(file_path)) assert os.path.exists(file_path)
rmtree(temp_dir)
def test_nondefault_creation(self): def test_nondefault_creation(self):
temp_dir = tempfile.mkdtemp()
component_name = "testcomponent" component_name = "testcomponent"
call_command( call_command(
"startcomponent", "startcomponent",
component_name, component_name,
"--path", "--path",
self.temp_dir, temp_dir,
"--js", "--js",
"test.js", "test.js",
"--css", "--css",
@ -49,31 +49,39 @@ class CreateComponentCommandTest(TestCase):
) )
expected_files = [ expected_files = [
os.path.join(self.temp_dir, component_name, "test.js"), os.path.join(temp_dir, component_name, "test.js"),
os.path.join(self.temp_dir, component_name, "test.css"), os.path.join(temp_dir, component_name, "test.css"),
os.path.join(self.temp_dir, component_name, "test.html"), os.path.join(temp_dir, component_name, "test.html"),
os.path.join(self.temp_dir, component_name, f"{component_name}.py"), os.path.join(temp_dir, component_name, f"{component_name}.py"),
] ]
for file_path in expected_files: for file_path in expected_files:
self.assertTrue(os.path.exists(file_path), f"File {file_path} was not created") assert os.path.exists(file_path), f"File {file_path} was not created"
rmtree(temp_dir)
def test_dry_run(self): def test_dry_run(self):
temp_dir = tempfile.mkdtemp()
component_name = "dryruncomponent" component_name = "dryruncomponent"
call_command( call_command(
"startcomponent", "startcomponent",
component_name, component_name,
"--path", "--path",
self.temp_dir, temp_dir,
"--dry-run", "--dry-run",
) )
component_path = os.path.join(self.temp_dir, component_name) component_path = os.path.join(temp_dir, component_name)
self.assertFalse(os.path.exists(component_path)) assert not os.path.exists(component_path)
rmtree(temp_dir)
def test_force_overwrite(self): def test_force_overwrite(self):
temp_dir = tempfile.mkdtemp()
component_name = "existingcomponent" component_name = "existingcomponent"
component_path = os.path.join(self.temp_dir, component_name) component_path = os.path.join(temp_dir, component_name)
os.makedirs(component_path) os.makedirs(component_path)
with open(os.path.join(component_path, f"{component_name}.py"), "w") as f: with open(os.path.join(component_path, f"{component_name}.py"), "w") as f:
@ -83,31 +91,41 @@ class CreateComponentCommandTest(TestCase):
"startcomponent", "startcomponent",
component_name, component_name,
"--path", "--path",
self.temp_dir, temp_dir,
"--force", "--force",
) )
with open(os.path.join(component_path, f"{component_name}.py"), "r") as f: with open(os.path.join(component_path, f"{component_name}.py"), "r") as f:
self.assertNotIn("hello world", f.read()) assert "hello world" not in f.read()
rmtree(temp_dir)
def test_error_existing_component_no_force(self): def test_error_existing_component_no_force(self):
temp_dir = tempfile.mkdtemp()
component_name = "existingcomponent_2" component_name = "existingcomponent_2"
component_path = os.path.join(self.temp_dir, component_name) component_path = os.path.join(temp_dir, component_name)
os.makedirs(component_path) os.makedirs(component_path)
with self.assertRaises(CommandError): with pytest.raises(CommandError):
call_command("startcomponent", component_name, "--path", self.temp_dir) call_command("startcomponent", component_name, "--path", temp_dir)
rmtree(temp_dir)
def test_verbose_output(self): def test_verbose_output(self):
temp_dir = tempfile.mkdtemp()
component_name = "verbosecomponent" component_name = "verbosecomponent"
out = StringIO() out = StringIO()
call_command( call_command(
"startcomponent", "startcomponent",
component_name, component_name,
"--path", "--path",
self.temp_dir, temp_dir,
"--verbose", "--verbose",
stdout=out, stdout=out,
) )
output = out.getvalue() output = out.getvalue()
self.assertIn("component at", output) assert "component at" in output
rmtree(temp_dir)

View file

@ -15,6 +15,7 @@ else:
from unittest import skipIf from unittest import skipIf
import pytest
from django.conf import settings from django.conf import settings
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
from django.http import HttpRequest, HttpResponse from django.http import HttpRequest, HttpResponse
@ -23,13 +24,14 @@ from django.template.base import TextNode
from django.test import Client from django.test import Client
from django.urls import path from django.urls import path
from django.utils.safestring import SafeString from django.utils.safestring import SafeString
from pytest_django.asserts import assertHTMLEqual, assertInHTML
from django_components import Component, ComponentView, Slot, SlotFunc, register, registry, types from django_components import Component, ComponentView, Slot, SlotFunc, register, types
from django_components.slots import SlotRef from django_components.slots import SlotRef
from django_components.urls import urlpatterns as dc_urlpatterns from django_components.urls import urlpatterns as dc_urlpatterns
from .django_test_setup import setup_test_config from django_components.testing import djc_test
from .testutils import BaseTestCase, parametrize_context_behavior from .testutils import PARAMETRIZE_CONTEXT_BEHAVIOR, setup_test_config
setup_test_config({"autodiscover": False}) setup_test_config({"autodiscover": False})
@ -78,9 +80,10 @@ else:
# TODO_REMOVE_IN_V1 - Superseded by `self.get_template` in v1 # TODO_REMOVE_IN_V1 - Superseded by `self.get_template` in v1
class ComponentOldTemplateApiTest(BaseTestCase): @djc_test
@parametrize_context_behavior(["django", "isolated"]) class TestComponentOldTemplateApi:
def test_get_template_string(self): @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_get_template_string(self, components_settings):
class SimpleComponent(Component): class SimpleComponent(Component):
def get_template_string(self, context): def get_template_string(self, context):
content: types.django_html = """ content: types.django_html = """
@ -98,7 +101,7 @@ class ComponentOldTemplateApiTest(BaseTestCase):
js = "script.js" js = "script.js"
rendered = SimpleComponent.render(kwargs={"variable": "test"}) rendered = SimpleComponent.render(kwargs={"variable": "test"})
self.assertHTMLEqual( assertHTMLEqual(
rendered, rendered,
""" """
Variable: <strong data-djc-id-a1bc3e>test</strong> Variable: <strong data-djc-id-a1bc3e>test</strong>
@ -106,57 +109,18 @@ class ComponentOldTemplateApiTest(BaseTestCase):
) )
class ComponentTest(BaseTestCase): @djc_test
class ParentComponent(Component): class TestComponent:
template: types.django_html = """ @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
{% load component_tags %} def test_empty_component(self, components_settings):
<div>
<h1>Parent content</h1>
{% component name="variable_display" shadowing_variable='override' new_variable='unique_val' %}
{% endcomponent %}
</div>
<div>
{% slot 'content' %}
<h2>Slot content</h2>
{% component name="variable_display" shadowing_variable='slot_default_override' new_variable='slot_default_unique' %}
{% endcomponent %}
{% endslot %}
</div>
""" # noqa
def get_context_data(self):
return {"shadowing_variable": "NOT SHADOWED"}
class VariableDisplay(Component):
template: types.django_html = """
{% load component_tags %}
<h1>Shadowing variable = {{ shadowing_variable }}</h1>
<h1>Uniquely named variable = {{ unique_variable }}</h1>
"""
def get_context_data(self, shadowing_variable=None, new_variable=None):
context = {}
if shadowing_variable is not None:
context["shadowing_variable"] = shadowing_variable
if new_variable is not None:
context["unique_variable"] = new_variable
return context
def setUp(self):
super().setUp()
registry.register(name="parent_component", component=self.ParentComponent)
registry.register(name="variable_display", component=self.VariableDisplay)
@parametrize_context_behavior(["django", "isolated"])
def test_empty_component(self):
class EmptyComponent(Component): class EmptyComponent(Component):
pass pass
with self.assertRaises(ImproperlyConfigured): with pytest.raises(ImproperlyConfigured):
EmptyComponent("empty_component")._get_template(Context({}), "123") EmptyComponent("empty_component")._get_template(Context({}), "123")
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_template_string_static_inlined(self): def test_template_string_static_inlined(self, components_settings):
class SimpleComponent(Component): class SimpleComponent(Component):
template: types.django_html = """ template: types.django_html = """
Variable: <strong>{{ variable }}</strong> Variable: <strong>{{ variable }}</strong>
@ -172,15 +136,15 @@ class ComponentTest(BaseTestCase):
js = "script.js" js = "script.js"
rendered = SimpleComponent.render(kwargs={"variable": "test"}) rendered = SimpleComponent.render(kwargs={"variable": "test"})
self.assertHTMLEqual( assertHTMLEqual(
rendered, rendered,
""" """
Variable: <strong data-djc-id-a1bc3e>test</strong> Variable: <strong data-djc-id-a1bc3e>test</strong>
""", """,
) )
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_template_string_dynamic(self): def test_template_string_dynamic(self, components_settings):
class SimpleComponent(Component): class SimpleComponent(Component):
def get_template(self, context): def get_template(self, context):
content: types.django_html = """ content: types.django_html = """
@ -198,15 +162,15 @@ class ComponentTest(BaseTestCase):
js = "script.js" js = "script.js"
rendered = SimpleComponent.render(kwargs={"variable": "test"}) rendered = SimpleComponent.render(kwargs={"variable": "test"})
self.assertHTMLEqual( assertHTMLEqual(
rendered, rendered,
""" """
Variable: <strong data-djc-id-a1bc3e>test</strong> Variable: <strong data-djc-id-a1bc3e>test</strong>
""", """,
) )
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_template_file_static(self): def test_template_file_static(self, components_settings):
class SimpleComponent(Component): class SimpleComponent(Component):
template_file = "simple_template.html" template_file = "simple_template.html"
@ -220,15 +184,15 @@ class ComponentTest(BaseTestCase):
js = "script.js" js = "script.js"
rendered = SimpleComponent.render(kwargs={"variable": "test"}) rendered = SimpleComponent.render(kwargs={"variable": "test"})
self.assertHTMLEqual( assertHTMLEqual(
rendered, rendered,
""" """
Variable: <strong data-djc-id-a1bc3e>test</strong> Variable: <strong data-djc-id-a1bc3e>test</strong>
""", """,
) )
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_template_file_static__compat(self): def test_template_file_static__compat(self, components_settings):
class SimpleComponent(Component): class SimpleComponent(Component):
template_name = "simple_template.html" template_name = "simple_template.html"
@ -241,16 +205,16 @@ class ComponentTest(BaseTestCase):
css = "style.css" css = "style.css"
js = "script.js" js = "script.js"
self.assertEqual(SimpleComponent.template_name, "simple_template.html") assert SimpleComponent.template_name == "simple_template.html"
self.assertEqual(SimpleComponent.template_file, "simple_template.html") assert SimpleComponent.template_file == "simple_template.html"
SimpleComponent.template_name = "other_template.html" SimpleComponent.template_name = "other_template.html"
self.assertEqual(SimpleComponent.template_name, "other_template.html") assert SimpleComponent.template_name == "other_template.html"
self.assertEqual(SimpleComponent.template_file, "other_template.html") assert SimpleComponent.template_file == "other_template.html"
SimpleComponent.template_name = "simple_template.html" SimpleComponent.template_name = "simple_template.html"
rendered = SimpleComponent.render(kwargs={"variable": "test"}) rendered = SimpleComponent.render(kwargs={"variable": "test"})
self.assertHTMLEqual( assertHTMLEqual(
rendered, rendered,
""" """
Variable: <strong data-djc-id-a1bc3e>test</strong> Variable: <strong data-djc-id-a1bc3e>test</strong>
@ -258,28 +222,28 @@ class ComponentTest(BaseTestCase):
) )
comp = SimpleComponent() comp = SimpleComponent()
self.assertEqual(comp.template_name, "simple_template.html") assert comp.template_name == "simple_template.html"
self.assertEqual(comp.template_file, "simple_template.html") assert comp.template_file == "simple_template.html"
# NOTE: Setting `template_file` on INSTANCE is not supported, as users should work # NOTE: Setting `template_file` on INSTANCE is not supported, as users should work
# with classes and not instances. This is tested for completeness. # with classes and not instances. This is tested for completeness.
comp.template_name = "other_template_2.html" comp.template_name = "other_template_2.html"
self.assertEqual(comp.template_name, "other_template_2.html") assert comp.template_name == "other_template_2.html"
self.assertEqual(comp.template_file, "other_template_2.html") assert comp.template_file == "other_template_2.html"
self.assertEqual(SimpleComponent.template_name, "other_template_2.html") assert SimpleComponent.template_name == "other_template_2.html"
self.assertEqual(SimpleComponent.template_file, "other_template_2.html") assert SimpleComponent.template_file == "other_template_2.html"
SimpleComponent.template_name = "simple_template.html" SimpleComponent.template_name = "simple_template.html"
rendered = comp.render(kwargs={"variable": "test"}) rendered = comp.render(kwargs={"variable": "test"})
self.assertHTMLEqual( assertHTMLEqual(
rendered, rendered,
""" """
Variable: <strong data-djc-id-a1bc3f>test</strong> Variable: <strong data-djc-id-a1bc3f>test</strong>
""", """,
) )
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_template_file_dynamic(self): def test_template_file_dynamic(self, components_settings):
class SvgComponent(Component): class SvgComponent(Component):
def get_context_data(self, name, css_class="", title="", **attrs): def get_context_data(self, name, css_class="", title="", **attrs):
return { return {
@ -292,21 +256,21 @@ class ComponentTest(BaseTestCase):
def get_template_name(self, context): def get_template_name(self, context):
return f"dynamic_{context['name']}.svg" return f"dynamic_{context['name']}.svg"
self.assertHTMLEqual( assertHTMLEqual(
SvgComponent.render(kwargs={"name": "svg1"}), SvgComponent.render(kwargs={"name": "svg1"}),
""" """
<svg data-djc-id-a1bc3e>Dynamic1</svg> <svg data-djc-id-a1bc3e>Dynamic1</svg>
""", """,
) )
self.assertHTMLEqual( assertHTMLEqual(
SvgComponent.render(kwargs={"name": "svg2"}), SvgComponent.render(kwargs={"name": "svg2"}),
""" """
<svg data-djc-id-a1bc3f>Dynamic2</svg> <svg data-djc-id-a1bc3f>Dynamic2</svg>
""", """,
) )
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_allows_to_return_template(self): def test_allows_to_return_template(self, components_settings):
class TestComponent(Component): class TestComponent(Component):
def get_context_data(self, variable, **attrs): def get_context_data(self, variable, **attrs):
return { return {
@ -318,7 +282,7 @@ class ComponentTest(BaseTestCase):
return Template(template_str) return Template(template_str)
rendered = TestComponent.render(kwargs={"variable": "test"}) rendered = TestComponent.render(kwargs={"variable": "test"})
self.assertHTMLEqual( assertHTMLEqual(
rendered, rendered,
""" """
Variable: <strong data-djc-id-a1bc3e>test</strong> Variable: <strong data-djc-id-a1bc3e>test</strong>
@ -326,16 +290,14 @@ class ComponentTest(BaseTestCase):
) )
def test_input(self): def test_input(self):
tester = self
class TestComponent(Component): class TestComponent(Component):
@no_type_check @no_type_check
def get_context_data(self, var1, var2, variable, another, **attrs): def get_context_data(self, var1, var2, variable, another, **attrs):
tester.assertEqual(self.input.args, (123, "str")) assert self.input.args == (123, "str")
tester.assertEqual(self.input.kwargs, {"variable": "test", "another": 1}) assert self.input.kwargs == {"variable": "test", "another": 1}
tester.assertIsInstance(self.input.context, Context) assert isinstance(self.input.context, Context)
tester.assertEqual(list(self.input.slots.keys()), ["my_slot"]) assert list(self.input.slots.keys()) == ["my_slot"]
tester.assertEqual(self.input.slots["my_slot"](Context(), None, None), "MY_SLOT") assert self.input.slots["my_slot"](Context(), None, None) == "MY_SLOT"
return { return {
"variable": variable, "variable": variable,
@ -343,11 +305,11 @@ class ComponentTest(BaseTestCase):
@no_type_check @no_type_check
def get_template(self, context): def get_template(self, context):
tester.assertEqual(self.input.args, (123, "str")) assert self.input.args == (123, "str")
tester.assertEqual(self.input.kwargs, {"variable": "test", "another": 1}) assert self.input.kwargs == {"variable": "test", "another": 1}
tester.assertIsInstance(self.input.context, Context) assert isinstance(self.input.context, Context)
tester.assertEqual(list(self.input.slots.keys()), ["my_slot"]) assert list(self.input.slots.keys()) == ["my_slot"]
tester.assertEqual(self.input.slots["my_slot"](Context(), None, None), "MY_SLOT") assert self.input.slots["my_slot"](Context(), None, None) == "MY_SLOT"
template_str: types.django_html = """ template_str: types.django_html = """
{% load component_tags %} {% load component_tags %}
@ -362,15 +324,15 @@ class ComponentTest(BaseTestCase):
slots={"my_slot": "MY_SLOT"}, slots={"my_slot": "MY_SLOT"},
) )
self.assertHTMLEqual( assertHTMLEqual(
rendered, rendered,
""" """
Variable: <strong data-djc-id-a1bc3e>test</strong> MY_SLOT Variable: <strong data-djc-id-a1bc3e>test</strong> MY_SLOT
""", """,
) )
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_prepends_exceptions_with_component_path(self): def test_prepends_exceptions_with_component_path(self, components_settings):
@register("broken") @register("broken")
class Broken(Component): class Broken(Component):
template: types.django_html = """ template: types.django_html = """
@ -423,15 +385,18 @@ class ComponentTest(BaseTestCase):
{% endcomponent %} {% endcomponent %}
""" """
with self.assertRaisesMessage( with pytest.raises(
TypeError, TypeError,
"An error occured while rendering components Root > parent > provider > provider(slot:content) > broken:\n" match=re.escape(
"tuple indices must be integers or slices, not str", "An error occured while rendering components Root > parent > provider > provider(slot:content) > broken:\n" # noqa: E501
"tuple indices must be integers or slices, not str"
),
): ):
Root.render() Root.render()
class ComponentValidationTest(BaseTestCase): @djc_test
class TestComponentValidation:
def test_validate_input_passes(self): def test_validate_input_passes(self):
class TestComponent(Component[CompArgs, CompKwargs, CompSlots, CompData, Any, Any]): class TestComponent(Component[CompArgs, CompKwargs, CompSlots, CompData, Any, Any]):
def get_context_data(self, var1, var2, variable, another, **attrs): def get_context_data(self, var1, var2, variable, another, **attrs):
@ -455,7 +420,7 @@ class ComponentValidationTest(BaseTestCase):
}, },
) )
self.assertHTMLEqual( assertHTMLEqual(
rendered, rendered,
""" """
Variable: <strong data-djc-id-a1bc3e>test</strong> Variable: <strong data-djc-id-a1bc3e>test</strong>
@ -479,7 +444,10 @@ class ComponentValidationTest(BaseTestCase):
Slot 2: {% slot "my_slot2" / %} Slot 2: {% slot "my_slot2" / %}
""" """
with self.assertRaisesMessage(TypeError, "Component 'TestComponent' expected 2 positional arguments, got 1"): with pytest.raises(
TypeError,
match=re.escape("Component 'TestComponent' expected 2 positional arguments, got 1"),
):
TestComponent.render( TestComponent.render(
kwargs={"variable": 1, "another": "test"}, # type: ignore kwargs={"variable": 1, "another": "test"}, # type: ignore
args=(123,), # type: ignore args=(123,), # type: ignore
@ -489,7 +457,10 @@ class ComponentValidationTest(BaseTestCase):
}, },
) )
with self.assertRaisesMessage(TypeError, "Component 'TestComponent' expected 2 positional arguments, got 0"): with pytest.raises(
TypeError,
match=re.escape("Component 'TestComponent' expected 2 positional arguments, got 0"),
):
TestComponent.render( TestComponent.render(
kwargs={"variable": 1, "another": "test"}, # type: ignore kwargs={"variable": 1, "another": "test"}, # type: ignore
slots={ slots={
@ -498,9 +469,11 @@ class ComponentValidationTest(BaseTestCase):
}, },
) )
with self.assertRaisesMessage( with pytest.raises(
TypeError, TypeError,
"Component 'TestComponent' expected keyword argument 'variable' to be <class 'str'>, got 1 of type <class 'int'>", # noqa: E501 match=re.escape(
"Component 'TestComponent' expected keyword argument 'variable' to be <class 'str'>, got 1 of type <class 'int'>" # noqa: E501
),
): ):
TestComponent.render( TestComponent.render(
kwargs={"variable": 1, "another": "test"}, # type: ignore kwargs={"variable": 1, "another": "test"}, # type: ignore
@ -511,12 +484,17 @@ class ComponentValidationTest(BaseTestCase):
}, },
) )
with self.assertRaisesMessage(TypeError, "Component 'TestComponent' expected 2 positional arguments, got 0"): with pytest.raises(
TypeError,
match=re.escape("Component 'TestComponent' expected 2 positional arguments, got 0"),
):
TestComponent.render() TestComponent.render()
with self.assertRaisesMessage( with pytest.raises(
TypeError, TypeError,
"Component 'TestComponent' expected keyword argument 'variable' to be <class 'str'>, got 1 of type <class 'int'>", # noqa: E501 match=re.escape(
"Component 'TestComponent' expected keyword argument 'variable' to be <class 'str'>, got 1 of type <class 'int'>" # noqa: E501
),
): ):
TestComponent.render( TestComponent.render(
kwargs={"variable": 1, "another": "test"}, # type: ignore kwargs={"variable": 1, "another": "test"}, # type: ignore
@ -527,8 +505,9 @@ class ComponentValidationTest(BaseTestCase):
}, },
) )
with self.assertRaisesMessage( with pytest.raises(
TypeError, "Component 'TestComponent' is missing a required keyword argument 'another'" TypeError,
match=re.escape("Component 'TestComponent' is missing a required keyword argument 'another'"),
): ):
TestComponent.render( TestComponent.render(
kwargs={"variable": "abc"}, # type: ignore kwargs={"variable": "abc"}, # type: ignore
@ -539,9 +518,11 @@ class ComponentValidationTest(BaseTestCase):
}, },
) )
with self.assertRaisesMessage( with pytest.raises(
TypeError, TypeError,
"Component 'TestComponent' expected slot 'my_slot' to be typing.Union[str, int, django_components.slots.Slot], got 123.5 of type <class 'float'>", # noqa: E501 match=re.escape(
"Component 'TestComponent' expected slot 'my_slot' to be typing.Union[str, int, django_components.slots.Slot], got 123.5 of type <class 'float'>" # noqa: E501
),
): ):
TestComponent.render( TestComponent.render(
kwargs={"variable": "abc", "another": 1}, kwargs={"variable": "abc", "another": 1},
@ -552,7 +533,10 @@ class ComponentValidationTest(BaseTestCase):
}, },
) )
with self.assertRaisesMessage(TypeError, "Component 'TestComponent' is missing a required slot 'my_slot2'"): with pytest.raises(
TypeError,
match=re.escape("Component 'TestComponent' is missing a required slot 'my_slot2'"),
):
TestComponent.render( TestComponent.render(
kwargs={"variable": "abc", "another": 1}, kwargs={"variable": "abc", "another": 1},
args=(123, "str"), args=(123, "str"),
@ -584,7 +568,7 @@ class ComponentValidationTest(BaseTestCase):
}, },
) )
self.assertHTMLEqual( assertHTMLEqual(
rendered, rendered,
""" """
Variable: <strong data-djc-id-a1bc3e>test</strong> Variable: <strong data-djc-id-a1bc3e>test</strong>
@ -616,7 +600,7 @@ class ComponentValidationTest(BaseTestCase):
}, },
) )
self.assertHTMLEqual( assertHTMLEqual(
rendered, rendered,
""" """
Variable: <strong data-djc-id-a1bc3e>test</strong> Variable: <strong data-djc-id-a1bc3e>test</strong>
@ -640,7 +624,10 @@ class ComponentValidationTest(BaseTestCase):
Slot 2: {% slot "my_slot2" / %} Slot 2: {% slot "my_slot2" / %}
""" """
with self.assertRaisesMessage(TypeError, "Component 'TestComponent' got unexpected data keys 'invalid_key'"): with pytest.raises(
TypeError,
match=re.escape("Component 'TestComponent' got unexpected data keys 'invalid_key'"),
):
TestComponent.render( TestComponent.render(
kwargs={"variable": "test", "another": 1}, kwargs={"variable": "test", "another": 1},
args=(123, "str"), args=(123, "str"),
@ -703,7 +690,7 @@ class ComponentValidationTest(BaseTestCase):
rendered = TestComponent.render(args=(Inner(),), kwargs={"inner": Inner()}) rendered = TestComponent.render(args=(Inner(),), kwargs={"inner": Inner()})
self.assertHTMLEqual( assertHTMLEqual(
rendered, rendered,
""" """
Name: <strong data-djc-id-a1bc3e>TestComponent</strong> Name: <strong data-djc-id-a1bc3e>TestComponent</strong>
@ -756,9 +743,10 @@ class ComponentValidationTest(BaseTestCase):
) )
class ComponentRenderTest(BaseTestCase): @djc_test
@parametrize_context_behavior(["django", "isolated"]) class TestComponentRender:
def test_render_minimal(self): @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_render_minimal(self, components_settings):
class SimpleComponent(Component): class SimpleComponent(Component):
template: types.django_html = """ template: types.django_html = """
{% load component_tags %} {% load component_tags %}
@ -783,7 +771,7 @@ class ComponentRenderTest(BaseTestCase):
} }
rendered = SimpleComponent.render() rendered = SimpleComponent.render()
self.assertHTMLEqual( assertHTMLEqual(
rendered, rendered,
""" """
the_arg2: None the_arg2: None
@ -797,8 +785,8 @@ class ComponentRenderTest(BaseTestCase):
""", """,
) )
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_render_full(self): def test_render_full(self, components_settings):
class SimpleComponent(Component): class SimpleComponent(Component):
template: types.django_html = """ template: types.django_html = """
{% load component_tags %} {% load component_tags %}
@ -833,7 +821,7 @@ class ComponentRenderTest(BaseTestCase):
kwargs={"the_kwarg": "test", "kw2": "ooo"}, kwargs={"the_kwarg": "test", "kw2": "ooo"},
slots={"first": "FIRST_SLOT"}, slots={"first": "FIRST_SLOT"},
) )
self.assertHTMLEqual( assertHTMLEqual(
rendered, rendered,
""" """
the_arg: one the_arg: one
@ -850,8 +838,8 @@ class ComponentRenderTest(BaseTestCase):
""", """,
) )
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_render_to_response_full(self): def test_render_to_response_full(self, components_settings):
class SimpleComponent(Component): class SimpleComponent(Component):
template: types.django_html = """ template: types.django_html = """
{% load component_tags %} {% load component_tags %}
@ -886,9 +874,9 @@ class ComponentRenderTest(BaseTestCase):
kwargs={"the_kwarg": "test", "kw2": "ooo"}, kwargs={"the_kwarg": "test", "kw2": "ooo"},
slots={"first": "FIRST_SLOT"}, slots={"first": "FIRST_SLOT"},
) )
self.assertIsInstance(rendered, HttpResponse) assert isinstance(rendered, HttpResponse)
self.assertHTMLEqual( assertHTMLEqual(
rendered.content.decode(), rendered.content.decode(),
""" """
the_arg: one the_arg: one
@ -905,8 +893,8 @@ class ComponentRenderTest(BaseTestCase):
""", """,
) )
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_render_to_response_change_response_class(self): def test_render_to_response_change_response_class(self, components_settings):
class MyResponse: class MyResponse:
def __init__(self, content: str) -> None: def __init__(self, content: str) -> None:
self.content = bytes(content, "utf-8") self.content = bytes(content, "utf-8")
@ -916,17 +904,24 @@ class ComponentRenderTest(BaseTestCase):
template: types.django_html = "HELLO" template: types.django_html = "HELLO"
rendered = SimpleComponent.render_to_response() rendered = SimpleComponent.render_to_response()
self.assertIsInstance(rendered, MyResponse) assert isinstance(rendered, MyResponse)
self.assertHTMLEqual( assertHTMLEqual(
rendered.content.decode(), rendered.content.decode(),
"HELLO", "HELLO",
) )
@parametrize_context_behavior([("django", False), ("isolated", True)]) @djc_test(
def test_render_slot_as_func(self, context_behavior_data): parametrize=(
is_isolated = context_behavior_data ["components_settings", "is_isolated"],
[
[{"context_behavior": "django"}, False],
[{"context_behavior": "isolated"}, True],
],
["django", "isolated"],
)
)
def test_render_slot_as_func(self, components_settings, is_isolated):
class SimpleComponent(Component): class SimpleComponent(Component):
template: types.django_html = """ template: types.django_html = """
{% load component_tags %} {% load component_tags %}
@ -943,30 +938,30 @@ class ComponentRenderTest(BaseTestCase):
} }
def first_slot(ctx: Context, slot_data: Dict, slot_ref: SlotRef): def first_slot(ctx: Context, slot_data: Dict, slot_ref: SlotRef):
self.assertIsInstance(ctx, Context) assert isinstance(ctx, Context)
# NOTE: Since the slot has access to the Context object, it should behave # NOTE: Since the slot has access to the Context object, it should behave
# the same way as it does in templates - when in "isolated" mode, then the # the same way as it does in templates - when in "isolated" mode, then the
# slot fill has access only to the "root" context, but not to the data of # slot fill has access only to the "root" context, but not to the data of
# get_context_data() of SimpleComponent. # get_context_data() of SimpleComponent.
if is_isolated: if is_isolated:
self.assertEqual(ctx.get("the_arg"), None) assert ctx.get("the_arg") is None
self.assertEqual(ctx.get("the_kwarg"), None) assert ctx.get("the_kwarg") is None
self.assertEqual(ctx.get("kwargs"), None) assert ctx.get("kwargs") is None
self.assertEqual(ctx.get("abc"), None) assert ctx.get("abc") is None
else: else:
self.assertEqual(ctx["the_arg"], "1") assert ctx["the_arg"] == "1"
self.assertEqual(ctx["the_kwarg"], 3) assert ctx["the_kwarg"] == 3
self.assertEqual(ctx["kwargs"], {}) assert ctx["kwargs"] == {}
self.assertEqual(ctx["abc"], "def") assert ctx["abc"] == "def"
slot_data_expected = { slot_data_expected = {
"data1": "abc", "data1": "abc",
"data2": {"hello": "world", "one": 123}, "data2": {"hello": "world", "one": 123},
} }
self.assertDictEqual(slot_data_expected, slot_data) assert slot_data_expected == slot_data
self.assertIsInstance(slot_ref, SlotRef) assert isinstance(slot_ref, SlotRef)
self.assertEqual("SLOT_DEFAULT", str(slot_ref).strip()) assert "SLOT_DEFAULT" == str(slot_ref).strip()
return f"FROM_INSIDE_FIRST_SLOT | {slot_ref}" return f"FROM_INSIDE_FIRST_SLOT | {slot_ref}"
@ -976,13 +971,13 @@ class ComponentRenderTest(BaseTestCase):
kwargs={"the_kwarg": 3}, kwargs={"the_kwarg": 3},
slots={"first": first_slot}, slots={"first": first_slot},
) )
self.assertHTMLEqual( assertHTMLEqual(
rendered, rendered,
"FROM_INSIDE_FIRST_SLOT | SLOT_DEFAULT", "FROM_INSIDE_FIRST_SLOT | SLOT_DEFAULT",
) )
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_render_raises_on_missing_slot(self): def test_render_raises_on_missing_slot(self, components_settings):
class SimpleComponent(Component): class SimpleComponent(Component):
template: types.django_html = """ template: types.django_html = """
{% load component_tags %} {% load component_tags %}
@ -990,8 +985,11 @@ class ComponentRenderTest(BaseTestCase):
{% endslot %} {% endslot %}
""" """
with self.assertRaisesMessage( with pytest.raises(
TemplateSyntaxError, "Slot 'first' is marked as 'required' (i.e. non-optional), yet no fill is provided." TemplateSyntaxError,
match=re.escape(
"Slot 'first' is marked as 'required' (i.e. non-optional), yet no fill is provided."
),
): ):
SimpleComponent.render() SimpleComponent.render()
@ -999,8 +997,8 @@ class ComponentRenderTest(BaseTestCase):
slots={"first": "FIRST_SLOT"}, slots={"first": "FIRST_SLOT"},
) )
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_render_with_include(self): def test_render_with_include(self, components_settings):
class SimpleComponent(Component): class SimpleComponent(Component):
template: types.django_html = """ template: types.django_html = """
{% load component_tags %} {% load component_tags %}
@ -1008,7 +1006,7 @@ class ComponentRenderTest(BaseTestCase):
""" """
rendered = SimpleComponent.render() rendered = SimpleComponent.render()
self.assertHTMLEqual( assertHTMLEqual(
rendered, rendered,
""" """
<custom-template data-djc-id-a1bc3e> <custom-template data-djc-id-a1bc3e>
@ -1021,8 +1019,8 @@ class ComponentRenderTest(BaseTestCase):
# See https://github.com/django-components/django-components/issues/580 # See https://github.com/django-components/django-components/issues/580
# And https://github.com/django-components/django-components/commit/fee26ec1d8b46b5ee065ca1ce6143889b0f96764 # And https://github.com/django-components/django-components/commit/fee26ec1d8b46b5ee065ca1ce6143889b0f96764
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_render_with_include_and_context(self): def test_render_with_include_and_context(self, components_settings):
class SimpleComponent(Component): class SimpleComponent(Component):
template: types.django_html = """ template: types.django_html = """
{% load component_tags %} {% load component_tags %}
@ -1030,7 +1028,7 @@ class ComponentRenderTest(BaseTestCase):
""" """
rendered = SimpleComponent.render(context=Context()) rendered = SimpleComponent.render(context=Context())
self.assertHTMLEqual( assertHTMLEqual(
rendered, rendered,
""" """
<custom-template data-djc-id-a1bc3e> <custom-template data-djc-id-a1bc3e>
@ -1044,8 +1042,8 @@ class ComponentRenderTest(BaseTestCase):
# See https://github.com/django-components/django-components/issues/580 # See https://github.com/django-components/django-components/issues/580
# And https://github.com/django-components/django-components/issues/634 # And https://github.com/django-components/django-components/issues/634
# And https://github.com/django-components/django-components/commit/fee26ec1d8b46b5ee065ca1ce6143889b0f96764 # And https://github.com/django-components/django-components/commit/fee26ec1d8b46b5ee065ca1ce6143889b0f96764
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_render_with_include_and_request_context(self): def test_render_with_include_and_request_context(self, components_settings):
class SimpleComponent(Component): class SimpleComponent(Component):
template: types.django_html = """ template: types.django_html = """
{% load component_tags %} {% load component_tags %}
@ -1053,7 +1051,7 @@ class ComponentRenderTest(BaseTestCase):
""" """
rendered = SimpleComponent.render(context=RequestContext(HttpRequest())) rendered = SimpleComponent.render(context=RequestContext(HttpRequest()))
self.assertHTMLEqual( assertHTMLEqual(
rendered, rendered,
""" """
<custom-template data-djc-id-a1bc3e> <custom-template data-djc-id-a1bc3e>
@ -1066,8 +1064,8 @@ class ComponentRenderTest(BaseTestCase):
# See https://github.com/django-components/django-components/issues/580 # See https://github.com/django-components/django-components/issues/580
# And https://github.com/django-components/django-components/issues/634 # And https://github.com/django-components/django-components/issues/634
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_request_context_is_populated_from_context_processors(self): def test_request_context_is_populated_from_context_processors(self, components_settings):
@register("thing") @register("thing")
class Thing(Component): class Thing(Component):
template: types.django_html = """ template: types.django_html = """
@ -1092,7 +1090,7 @@ class ComponentRenderTest(BaseTestCase):
client = CustomClient(urlpatterns=[path("test_thing/", Thing.as_view())]) client = CustomClient(urlpatterns=[path("test_thing/", Thing.as_view())])
response = client.get("/test_thing/") response = client.get("/test_thing/")
self.assertEqual(response.status_code, 200) assert response.status_code == 200
# Full response: # Full response:
# """ # """
@ -1106,7 +1104,7 @@ class ComponentRenderTest(BaseTestCase):
# </div> # </div>
# </div> # </div>
# """ # """
self.assertInHTML( assertInHTML(
""" """
<kbd data-djc-id-a1bc3e> <kbd data-djc-id-a1bc3e>
Rendered via GET request Rendered via GET request
@ -1118,7 +1116,7 @@ class ComponentRenderTest(BaseTestCase):
token_re = re.compile(rb"CSRF token:\s+predictabletoken") token_re = re.compile(rb"CSRF token:\s+predictabletoken")
token = token_re.findall(response.content)[0] token = token_re.findall(response.content)[0]
self.assertEqual(token, b'CSRF token: predictabletoken') assert token == b"CSRF token: predictabletoken"
def test_request_context_created_when_no_context(self): def test_request_context_created_when_no_context(self):
@register("thing") @register("thing")
@ -1133,12 +1131,12 @@ class ComponentRenderTest(BaseTestCase):
client = CustomClient(urlpatterns=[path("test_thing/", Thing.as_view())]) client = CustomClient(urlpatterns=[path("test_thing/", Thing.as_view())])
response = client.get("/test_thing/") response = client.get("/test_thing/")
self.assertEqual(response.status_code, 200) assert response.status_code == 200
token_re = re.compile(rb"CSRF token:\s+predictabletoken") token_re = re.compile(rb"CSRF token:\s+predictabletoken")
token = token_re.findall(response.content)[0] token = token_re.findall(response.content)[0]
self.assertEqual(token, b'CSRF token: predictabletoken') assert token == b"CSRF token: predictabletoken"
def test_request_context_created_when_already_a_context_dict(self): def test_request_context_created_when_already_a_context_dict(self):
@register("thing") @register("thing")
@ -1154,13 +1152,13 @@ class ComponentRenderTest(BaseTestCase):
client = CustomClient(urlpatterns=[path("test_thing/", Thing.as_view())]) client = CustomClient(urlpatterns=[path("test_thing/", Thing.as_view())])
response = client.get("/test_thing/") response = client.get("/test_thing/")
self.assertEqual(response.status_code, 200) assert response.status_code == 200
token_re = re.compile(rb"CSRF token:\s+predictabletoken") token_re = re.compile(rb"CSRF token:\s+predictabletoken")
token = token_re.findall(response.content)[0] token = token_re.findall(response.content)[0]
self.assertEqual(token, b'CSRF token: predictabletoken') assert token == b"CSRF token: predictabletoken"
self.assertInHTML("Existing context: foo", response.content.decode()) assert "Existing context: foo" in response.content.decode()
def request_context_ignores_context_when_already_a_context(self): def request_context_ignores_context_when_already_a_context(self):
@register("thing") @register("thing")
@ -1176,15 +1174,15 @@ class ComponentRenderTest(BaseTestCase):
client = CustomClient(urlpatterns=[path("test_thing/", Thing.as_view())]) client = CustomClient(urlpatterns=[path("test_thing/", Thing.as_view())])
response = client.get("/test_thing/") response = client.get("/test_thing/")
self.assertEqual(response.status_code, 200) assert response.status_code == 200
token_re = re.compile(rb"CSRF token:\s+(?P<token>[0-9a-zA-Z]{64})") token_re = re.compile(rb"CSRF token:\s+(?P<token>[0-9a-zA-Z]{64})")
self.assertFalse(token_re.findall(response.content)) assert not token_re.findall(response.content)
self.assertInHTML("Existing context: foo", response.content.decode()) assert "Existing context: foo" in response.content.decode()
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_render_with_extends(self): def test_render_with_extends(self, components_settings):
class SimpleComponent(Component): class SimpleComponent(Component):
template: types.django_html = """ template: types.django_html = """
{% extends 'block.html' %} {% extends 'block.html' %}
@ -1194,7 +1192,7 @@ class ComponentRenderTest(BaseTestCase):
""" """
rendered = SimpleComponent.render(render_dependencies=False) rendered = SimpleComponent.render(render_dependencies=False)
self.assertHTMLEqual( assertHTMLEqual(
rendered, rendered,
""" """
<!DOCTYPE html> <!DOCTYPE html>
@ -1210,8 +1208,8 @@ class ComponentRenderTest(BaseTestCase):
""", """,
) )
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_render_can_access_instance(self): def test_render_can_access_instance(self, components_settings):
class TestComponent(Component): class TestComponent(Component):
template = "Variable: <strong>{{ id }}</strong>" template = "Variable: <strong>{{ id }}</strong>"
@ -1221,13 +1219,13 @@ class ComponentRenderTest(BaseTestCase):
} }
rendered = TestComponent.render() rendered = TestComponent.render()
self.assertHTMLEqual( assertHTMLEqual(
rendered, rendered,
"Variable: <strong data-djc-id-a1bc3e>a1bc3e</strong>", "Variable: <strong data-djc-id-a1bc3e>a1bc3e</strong>",
) )
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_render_to_response_can_access_instance(self): def test_render_to_response_can_access_instance(self, components_settings):
class TestComponent(Component): class TestComponent(Component):
template = "Variable: <strong>{{ id }}</strong>" template = "Variable: <strong>{{ id }}</strong>"
@ -1237,13 +1235,14 @@ class ComponentRenderTest(BaseTestCase):
} }
rendered_resp = TestComponent.render_to_response() rendered_resp = TestComponent.render_to_response()
self.assertHTMLEqual( assertHTMLEqual(
rendered_resp.content.decode("utf-8"), rendered_resp.content.decode("utf-8"),
"Variable: <strong data-djc-id-a1bc3e>a1bc3e</strong>", "Variable: <strong data-djc-id-a1bc3e>a1bc3e</strong>",
) )
class ComponentHookTest(BaseTestCase): @djc_test
class TestComponentHook:
def test_on_render_before(self): def test_on_render_before(self):
@register("nested") @register("nested")
class NestedComponent(Component): class NestedComponent(Component):
@ -1285,7 +1284,7 @@ class ComponentHookTest(BaseTestCase):
template.nodelist.append(TextNode("\n---\nFROM_ON_BEFORE")) template.nodelist.append(TextNode("\n---\nFROM_ON_BEFORE"))
rendered = SimpleComponent.render() rendered = SimpleComponent.render()
self.assertHTMLEqual( assertHTMLEqual(
rendered, rendered,
""" """
args: () args: ()
@ -1348,7 +1347,7 @@ class ComponentHookTest(BaseTestCase):
rendered = SimpleComponent.render() rendered = SimpleComponent.render()
self.assertHTMLEqual( assertHTMLEqual(
captured_content, captured_content,
""" """
args: () args: ()
@ -1362,7 +1361,7 @@ class ComponentHookTest(BaseTestCase):
</div> </div>
""", """,
) )
self.assertHTMLEqual( assertHTMLEqual(
rendered, rendered,
""" """
args: () args: ()
@ -1378,8 +1377,8 @@ class ComponentHookTest(BaseTestCase):
) )
# Check that modifying the context or template does nothing # Check that modifying the context or template does nothing
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_on_render_after_override_output(self): def test_on_render_after_override_output(self, components_settings):
captured_content = None captured_content = None
@register("nested") @register("nested")
@ -1419,7 +1418,7 @@ class ComponentHookTest(BaseTestCase):
rendered = SimpleComponent.render() rendered = SimpleComponent.render()
self.assertHTMLEqual( assertHTMLEqual(
captured_content, captured_content,
""" """
args: () args: ()
@ -1433,7 +1432,7 @@ class ComponentHookTest(BaseTestCase):
</div> </div>
""", """,
) )
self.assertHTMLEqual( assertHTMLEqual(
rendered, rendered,
""" """
Chocolate cookie recipe: Chocolate cookie recipe:
@ -1495,9 +1494,6 @@ class ComponentHookTest(BaseTestCase):
SimpleComponent.render() SimpleComponent.render()
self.assertEqual( assert context_in_before == context_in_after
context_in_before, assert "from_on_before" in context_in_before # type: ignore[operator]
context_in_after, assert "from_on_after" in context_in_after # type: ignore[operator]
)
self.assertIn("from_on_before", context_in_before)
self.assertIn("from_on_after", context_in_after)

View file

@ -1,12 +1,13 @@
from django_components.util.component_highlight import apply_component_highlight, COLORS from django_components.util.component_highlight import apply_component_highlight, COLORS
from .django_test_setup import setup_test_config from django_components.testing import djc_test
from .testutils import BaseTestCase from .testutils import setup_test_config
setup_test_config({"autodiscover": False}) setup_test_config({"autodiscover": False})
class ComponentHighlightTests(BaseTestCase): @djc_test
class TestComponentHighlight:
def test_component_highlight(self): def test_component_highlight(self):
# Test component highlighting # Test component highlighting
test_html = "<div>Test content</div>" test_html = "<div>Test content</div>"
@ -14,12 +15,12 @@ class ComponentHighlightTests(BaseTestCase):
result = apply_component_highlight("component", test_html, component_name) result = apply_component_highlight("component", test_html, component_name)
# Check that the output contains the component name # Check that the output contains the component name
self.assertIn(component_name, result) assert component_name in result
# Check that the output contains the original HTML # Check that the output contains the original HTML
self.assertIn(test_html, result) assert test_html in result
# Check that the component colors are used # Check that the component colors are used
self.assertIn(COLORS["component"].text_color, result) assert COLORS["component"].text_color in result
self.assertIn(COLORS["component"].border_color, result) assert COLORS["component"].border_color in result
def test_slot_highlight(self): def test_slot_highlight(self):
# Test slot highlighting # Test slot highlighting
@ -28,9 +29,9 @@ class ComponentHighlightTests(BaseTestCase):
result = apply_component_highlight("slot", test_html, slot_name) result = apply_component_highlight("slot", test_html, slot_name)
# Check that the output contains the slot name # Check that the output contains the slot name
self.assertIn(slot_name, result) assert slot_name in result
# Check that the output contains the original HTML # Check that the output contains the original HTML
self.assertIn(test_html, result) assert test_html in result
# Check that the slot colors are used # Check that the slot colors are used
self.assertIn(COLORS["slot"].text_color, result) assert COLORS["slot"].text_color in result
self.assertIn(COLORS["slot"].border_color, result) assert COLORS["slot"].border_color in result

View file

@ -1,83 +1,72 @@
import os import os
import sys import sys
from pathlib import Path from pathlib import Path
from textwrap import dedent
from django.forms.widgets import Media from django.forms.widgets import Media
from django.template import Context, Template from django.template import Context, Template
from django.templatetags.static import static from django.templatetags.static import static
from django.test import override_settings
from django.utils.html import format_html, html_safe from django.utils.html import format_html, html_safe
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from pytest_django.asserts import assertHTMLEqual, assertInHTML
from django_components import Component, registry, render_dependencies, types from django_components import Component, autodiscover, registry, render_dependencies, types
from .django_test_setup import setup_test_config from django_components.testing import djc_test
from .testutils import BaseTestCase, autodiscover_with_cleanup from .testutils import setup_test_config
setup_test_config({"autodiscover": False}) setup_test_config({"autodiscover": False})
# "Main media" refer to the HTML, JS, and CSS set on the Component class itself # "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. # (as opposed via the `Media` class). These have special handling in the Component.
class MainMediaTest(BaseTestCase): @djc_test
class TestMainMedia:
def test_html_js_css_inlined(self): def test_html_js_css_inlined(self):
class TestComponent(Component): class TestComponent(Component):
template = """ template = dedent("""
{% load component_tags %} {% load component_tags %}
{% component_js_dependencies %} {% component_js_dependencies %}
{% component_css_dependencies %} {% component_css_dependencies %}
<div class='html-css-only'>Content</div> <div class='html-css-only'>Content</div>
""" """)
css = ".html-css-only { color: blue; }" css = ".html-css-only { color: blue; }"
js = "console.log('HTML and JS only');" js = "console.log('HTML and JS only');"
self.assertEqual( assert TestComponent.css == ".html-css-only { color: blue; }"
TestComponent.css, assert TestComponent.js == "console.log('HTML and JS only');"
".html-css-only { color: blue; }",
)
self.assertEqual(
TestComponent.js,
"console.log('HTML and JS only');",
)
rendered = TestComponent.render() rendered = TestComponent.render()
self.assertInHTML( assertInHTML(
'<div class="html-css-only" data-djc-id-a1bc3e>Content</div>', '<div class="html-css-only" data-djc-id-a1bc3e>Content</div>',
rendered, rendered,
) )
self.assertInHTML( assertInHTML(
"<style>.html-css-only { color: blue; }</style>", "<style>.html-css-only { color: blue; }</style>",
rendered, rendered,
) )
self.assertInHTML( assertInHTML(
"<script>console.log('HTML and JS only');</script>", "<script>console.log('HTML and JS only');</script>",
rendered, rendered,
) )
# Check that the HTML / JS / CSS can be accessed on the component class # Check that the HTML / JS / CSS can be accessed on the component class
self.assertEqual( assert TestComponent.template == dedent("""
TestComponent.template,
"""
{% load component_tags %} {% load component_tags %}
{% component_js_dependencies %} {% component_js_dependencies %}
{% component_css_dependencies %} {% component_css_dependencies %}
<div class='html-css-only'>Content</div> <div class='html-css-only'>Content</div>
""", """)
) assert TestComponent.css == ".html-css-only { color: blue; }"
self.assertEqual( assert TestComponent.js == "console.log('HTML and JS only');"
TestComponent.css,
".html-css-only { color: blue; }",
)
self.assertEqual(
TestComponent.js,
"console.log('HTML and JS only');",
)
@override_settings( @djc_test(
STATICFILES_DIRS=[ django_settings={
"STATICFILES_DIRS": [
os.path.join(Path(__file__).resolve().parent, "static_root"), os.path.join(Path(__file__).resolve().parent, "static_root"),
], ],
}
) )
def test_html_js_css_filepath_rel_to_component(self): def test_html_js_css_filepath_rel_to_component(self):
from tests.test_app.components.app_lvl_comp.app_lvl_comp import AppLvlCompComponent from tests.test_app.components.app_lvl_comp.app_lvl_comp import AppLvlCompComponent
@ -87,14 +76,8 @@ class MainMediaTest(BaseTestCase):
registry.register("test", TestComponent) registry.register("test", TestComponent)
self.assertIn( assert ".html-css-only {\n color: blue;\n}" in TestComponent.css # type: ignore[operator]
".html-css-only {\n color: blue;\n}", assert 'console.log("JS file");' in TestComponent.js # type: ignore[operator]
TestComponent.css,
)
self.assertIn(
'console.log("JS file");',
TestComponent.js,
)
rendered_raw = Template( rendered_raw = Template(
""" """
@ -106,7 +89,7 @@ class MainMediaTest(BaseTestCase):
).render(Context()) ).render(Context())
rendered = render_dependencies(rendered_raw) rendered = render_dependencies(rendered_raw)
self.assertInHTML( assertInHTML(
""" """
<form data-djc-id-a1bc41 method="post"> <form data-djc-id-a1bc41 method="post">
<input name="variable" type="text" value="test"/> <input name="variable" type="text" value="test"/>
@ -115,37 +98,33 @@ class MainMediaTest(BaseTestCase):
""", """,
rendered, rendered,
) )
self.assertInHTML( assertInHTML(
"<style>.html-css-only { color: blue; }</style>", "<style>.html-css-only { color: blue; }</style>",
rendered, rendered,
) )
self.assertInHTML( assertInHTML(
'<script>console.log("JS file");</script>', '<script>console.log("JS file");</script>',
rendered, rendered,
) )
# Check that the HTML / JS / CSS can be accessed on the component class # Check that the HTML / JS / CSS can be accessed on the component class
self.assertEqual( assert TestComponent.template == (
TestComponent.template,
(
'<form method="post">\n' '<form method="post">\n'
" {% csrf_token %}\n" " {% csrf_token %}\n"
' <input type="text" name="variable" value="{{ variable }}">\n' ' <input type="text" name="variable" value="{{ variable }}">\n'
' <input type="submit">\n' ' <input type="submit">\n'
"</form>\n" "</form>\n"
),
) )
self.assertEqual(TestComponent.css, ".html-css-only {\n" " color: blue;\n" "}\n") assert TestComponent.css == ".html-css-only {\n color: blue;\n}\n"
self.assertEqual( assert TestComponent.js == 'console.log("JS file");\n'
TestComponent.js,
'console.log("JS file");\n',
)
@override_settings( @djc_test(
STATICFILES_DIRS=[ django_settings={
"STATICFILES_DIRS": [
os.path.join(Path(__file__).resolve().parent, "static_root"), os.path.join(Path(__file__).resolve().parent, "static_root"),
], ],
}
) )
def test_html_js_css_filepath_from_static(self): def test_html_js_css_filepath_from_static(self):
class TestComponent(Component): class TestComponent(Component):
@ -160,18 +139,9 @@ class MainMediaTest(BaseTestCase):
registry.register("test", TestComponent) registry.register("test", TestComponent)
self.assertIn( assert "Variable: <strong>{{ variable }}</strong>" in TestComponent.template # type: ignore[operator]
"Variable: <strong>{{ variable }}</strong>", assert ".html-css-only {\n color: blue;\n}" in TestComponent.css # type: ignore[operator]
TestComponent.template, assert 'console.log("HTML and JS only");' in TestComponent.js # type: ignore[operator]
)
self.assertIn(
".html-css-only {\n color: blue;\n}",
TestComponent.css,
)
self.assertIn(
'console.log("HTML and JS only");',
TestComponent.js,
)
rendered_raw = Template( rendered_raw = Template(
""" """
@ -183,45 +153,35 @@ class MainMediaTest(BaseTestCase):
).render(Context()) ).render(Context())
rendered = render_dependencies(rendered_raw) rendered = render_dependencies(rendered_raw)
self.assertIn( assert 'Variable: <strong data-djc-id-a1bc41="">test</strong>' in rendered
'Variable: <strong data-djc-id-a1bc41="">test</strong>', assertInHTML(
rendered,
)
self.assertInHTML(
"<style>/* Used in `MainMediaTest` tests in `test_component_media.py` */\n.html-css-only {\n color: blue;\n}</style>", "<style>/* Used in `MainMediaTest` tests in `test_component_media.py` */\n.html-css-only {\n color: blue;\n}</style>",
rendered, rendered,
) )
self.assertInHTML( assertInHTML(
'<script>/* Used in `MainMediaTest` tests in `test_component_media.py` */\nconsole.log("HTML and JS only");</script>', '<script>/* Used in `MainMediaTest` tests in `test_component_media.py` */\nconsole.log("HTML and JS only");</script>',
rendered, rendered,
) )
# Check that the HTML / JS / CSS can be accessed on the component class # Check that the HTML / JS / CSS can be accessed on the component class
self.assertEqual( assert TestComponent.template == "Variable: <strong>{{ variable }}</strong>\n"
TestComponent.template, assert TestComponent.css == (
"Variable: <strong>{{ variable }}</strong>\n",
)
self.assertEqual(
TestComponent.css,
(
"/* Used in `MainMediaTest` tests in `test_component_media.py` */\n" "/* Used in `MainMediaTest` tests in `test_component_media.py` */\n"
".html-css-only {\n" ".html-css-only {\n"
" color: blue;\n" " color: blue;\n"
"}" "}"
),
) )
self.assertEqual( assert TestComponent.js == (
TestComponent.js,
(
"/* Used in `MainMediaTest` tests in `test_component_media.py` */\n" "/* Used in `MainMediaTest` tests in `test_component_media.py` */\n"
'console.log("HTML and JS only");\n' 'console.log("HTML and JS only");\n'
),
) )
@override_settings( @djc_test(
STATICFILES_DIRS=[ django_settings={
"STATICFILES_DIRS": [
os.path.join(Path(__file__).resolve().parent, "static_root"), os.path.join(Path(__file__).resolve().parent, "static_root"),
], ],
}
) )
def test_html_js_css_filepath_lazy_loaded(self): def test_html_js_css_filepath_lazy_loaded(self):
from tests.test_app.components.app_lvl_comp.app_lvl_comp import AppLvlCompComponent from tests.test_app.components.app_lvl_comp.app_lvl_comp import AppLvlCompComponent
@ -231,51 +191,26 @@ class MainMediaTest(BaseTestCase):
# NOTE: Since this is a subclass, actual CSS is defined on the parent class, and thus # NOTE: 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. # the corresponding ComponentMedia instance is also on the parent class.
self.assertEqual( assert AppLvlCompComponent._component_media.css is None # type: ignore[attr-defined]
AppLvlCompComponent._component_media.css, # type: ignore[attr-defined] assert AppLvlCompComponent._component_media.css_file == "app_lvl_comp.css" # type: ignore[attr-defined]
None,
)
self.assertEqual(
AppLvlCompComponent._component_media.css_file, # type: ignore[attr-defined]
"app_lvl_comp.css",
)
# Access the property to load the CSS # Access the property to load the CSS
_ = TestComponent.css _ = TestComponent.css
self.assertEqual( assert AppLvlCompComponent._component_media.css == (".html-css-only {\n" " color: blue;\n" "}\n") # type: ignore[attr-defined]
AppLvlCompComponent._component_media.css, # type: ignore[attr-defined] assert AppLvlCompComponent._component_media.css_file == "app_lvl_comp/app_lvl_comp.css" # type: ignore[attr-defined]
(".html-css-only {\n" " color: blue;\n" "}\n"),
)
self.assertEqual(
AppLvlCompComponent._component_media.css_file, # type: ignore[attr-defined]
"app_lvl_comp/app_lvl_comp.css",
)
# Also check JS and HTML while we're at it # Also check JS and HTML while we're at it
self.assertEqual( assert AppLvlCompComponent._component_media.template == ( # type: ignore[attr-defined]
AppLvlCompComponent._component_media.template, # type: ignore[attr-defined]
(
'<form method="post">\n' '<form method="post">\n'
" {% csrf_token %}\n" " {% csrf_token %}\n"
' <input type="text" name="variable" value="{{ variable }}">\n' ' <input type="text" name="variable" value="{{ variable }}">\n'
' <input type="submit">\n' ' <input type="submit">\n'
"</form>\n" "</form>\n"
),
)
self.assertEqual(
AppLvlCompComponent._component_media.template_file, # type: ignore[attr-defined]
"app_lvl_comp/app_lvl_comp.html",
)
self.assertEqual(
AppLvlCompComponent._component_media.js, # type: ignore[attr-defined]
'console.log("JS file");\n',
)
self.assertEqual(
AppLvlCompComponent._component_media.js_file, # type: ignore[attr-defined]
"app_lvl_comp/app_lvl_comp.js",
) )
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]
def test_html_variable(self): def test_html_variable(self):
class VariableHTMLComponent(Component): class VariableHTMLComponent(Component):
@ -284,7 +219,7 @@ class MainMediaTest(BaseTestCase):
comp = VariableHTMLComponent("variable_html_component") comp = VariableHTMLComponent("variable_html_component")
context = Context({"variable": "Dynamic Content"}) context = Context({"variable": "Dynamic Content"})
self.assertHTMLEqual( assertHTMLEqual(
comp.render(context), comp.render(context),
'<div class="variable-html" data-djc-id-a1bc3e>Dynamic Content</div>', '<div class="variable-html" data-djc-id-a1bc3e>Dynamic Content</div>',
) )
@ -303,7 +238,7 @@ class MainMediaTest(BaseTestCase):
} }
rendered = FilteredComponent.render(kwargs={"var1": "test1", "var2": "test2"}) rendered = FilteredComponent.render(kwargs={"var1": "test1", "var2": "test2"})
self.assertHTMLEqual( assertHTMLEqual(
rendered, rendered,
""" """
Var1: <strong data-djc-id-a1bc3e>test1</strong> Var1: <strong data-djc-id-a1bc3e>test1</strong>
@ -312,7 +247,8 @@ class MainMediaTest(BaseTestCase):
) )
class ComponentMediaTests(BaseTestCase): @djc_test
class TestComponentMedia:
def test_empty_media(self): def test_empty_media(self):
class SimpleComponent(Component): class SimpleComponent(Component):
template: types.django_html = """ template: types.django_html = """
@ -327,10 +263,10 @@ class ComponentMediaTests(BaseTestCase):
rendered = SimpleComponent.render() rendered = SimpleComponent.render()
self.assertEqual(rendered.count("<style"), 0) assert rendered.count("<style") == 0
self.assertEqual(rendered.count("<link"), 0) assert rendered.count("<link") == 0
self.assertEqual(rendered.count("<script"), 1) # 1 Boilerplate script assert rendered.count("<script") == 1 # 1 Boilerplate script
def test_css_js_as_lists(self): def test_css_js_as_lists(self):
class SimpleComponent(Component): class SimpleComponent(Component):
@ -346,10 +282,10 @@ class ComponentMediaTests(BaseTestCase):
rendered = SimpleComponent.render() rendered = SimpleComponent.render()
self.assertInHTML('<link href="path/to/style.css" media="all" rel="stylesheet">', rendered) assertInHTML('<link href="path/to/style.css" media="all" rel="stylesheet">', rendered)
self.assertInHTML('<link href="path/to/style2.css" media="all" rel="stylesheet">', rendered) assertInHTML('<link href="path/to/style2.css" media="all" rel="stylesheet">', rendered)
self.assertInHTML('<script src="path/to/script.js"></script>', rendered) assertInHTML('<script src="path/to/script.js"></script>', rendered)
def test_css_js_as_string(self): def test_css_js_as_string(self):
class SimpleComponent(Component): class SimpleComponent(Component):
@ -365,8 +301,8 @@ class ComponentMediaTests(BaseTestCase):
rendered = SimpleComponent.render() rendered = SimpleComponent.render()
self.assertInHTML('<link href="path/to/style.css" media="all" rel="stylesheet">', rendered) assertInHTML('<link href="path/to/style.css" media="all" rel="stylesheet">', rendered)
self.assertInHTML('<script src="path/to/script.js"></script>', rendered) assertInHTML('<script src="path/to/script.js"></script>', rendered)
def test_css_as_dict(self): def test_css_as_dict(self):
class SimpleComponent(Component): class SimpleComponent(Component):
@ -386,11 +322,11 @@ class ComponentMediaTests(BaseTestCase):
rendered = SimpleComponent.render() rendered = SimpleComponent.render()
self.assertInHTML('<link href="path/to/style.css" media="all" rel="stylesheet">', rendered) assertInHTML('<link href="path/to/style.css" media="all" rel="stylesheet">', rendered)
self.assertInHTML('<link href="path/to/style2.css" media="print" rel="stylesheet">', rendered) assertInHTML('<link href="path/to/style2.css" media="print" rel="stylesheet">', rendered)
self.assertInHTML('<link href="path/to/style3.css" media="screen" rel="stylesheet">', rendered) assertInHTML('<link href="path/to/style3.css" media="screen" rel="stylesheet">', rendered)
self.assertInHTML('<script src="path/to/script.js"></script>', rendered) assertInHTML('<script src="path/to/script.js"></script>', rendered)
def test_media_custom_render_js(self): def test_media_custom_render_js(self):
class MyMedia(Media): class MyMedia(Media):
@ -415,8 +351,8 @@ class ComponentMediaTests(BaseTestCase):
rendered = SimpleComponent.render() rendered = SimpleComponent.render()
self.assertIn('<script defer src="path/to/script.js"></script>', rendered) assert '<script defer src="path/to/script.js"></script>' in rendered
self.assertIn('<script defer src="path/to/script2.js"></script>', rendered) assert '<script defer src="path/to/script2.js"></script>' in rendered
def test_media_custom_render_css(self): def test_media_custom_render_css(self):
class MyMedia(Media): class MyMedia(Media):
@ -446,12 +382,13 @@ class ComponentMediaTests(BaseTestCase):
rendered = SimpleComponent.render() rendered = SimpleComponent.render()
self.assertInHTML('<link abc href="path/to/style.css" media="all" rel="stylesheet">', rendered) assertInHTML('<link abc href="path/to/style.css" media="all" rel="stylesheet">', rendered)
self.assertInHTML('<link abc href="path/to/style2.css" media="print" rel="stylesheet">', rendered) assertInHTML('<link abc href="path/to/style2.css" media="print" rel="stylesheet">', rendered)
self.assertInHTML('<link abc href="path/to/style3.css" media="screen" rel="stylesheet">', rendered) assertInHTML('<link abc href="path/to/style3.css" media="screen" rel="stylesheet">', rendered)
class MediaPathAsObjectTests(BaseTestCase): @djc_test
class TestMediaPathAsObject:
def test_safestring(self): def test_safestring(self):
""" """
Test that media work with paths defined as instances of classes that define Test that media work with paths defined as instances of classes that define
@ -513,15 +450,15 @@ class MediaPathAsObjectTests(BaseTestCase):
rendered = SimpleComponent.render() rendered = SimpleComponent.render()
self.assertInHTML('<link css_tag href="path/to/style.css" rel="stylesheet" />', rendered) assertInHTML('<link css_tag href="path/to/style.css" rel="stylesheet" />', rendered)
self.assertInHTML('<link hi href="path/to/style2.css" rel="stylesheet" />', rendered) assertInHTML('<link hi href="path/to/style2.css" rel="stylesheet" />', rendered)
self.assertInHTML('<link css_tag href="path/to/style3.css" rel="stylesheet" />', rendered) assertInHTML('<link css_tag href="path/to/style3.css" rel="stylesheet" />', rendered)
self.assertInHTML('<link href="path/to/style4.css" media="screen" rel="stylesheet">', rendered) assertInHTML('<link href="path/to/style4.css" media="screen" rel="stylesheet">', rendered)
self.assertInHTML('<script js_tag src="path/to/script.js" type="module"></script>', rendered) assertInHTML('<script js_tag src="path/to/script.js" type="module"></script>', rendered)
self.assertInHTML('<script hi src="path/to/script2.js"></script>', rendered) assertInHTML('<script hi src="path/to/script2.js"></script>', rendered)
self.assertInHTML('<script type="module" src="path/to/script3.js"></script>', rendered) assertInHTML('<script type="module" src="path/to/script3.js"></script>', rendered)
self.assertInHTML('<script src="path/to/script4.js"></script>', rendered) assertInHTML('<script src="path/to/script4.js"></script>', rendered)
def test_pathlike(self): def test_pathlike(self):
""" """
@ -562,14 +499,14 @@ class MediaPathAsObjectTests(BaseTestCase):
rendered = SimpleComponent.render() rendered = SimpleComponent.render()
self.assertInHTML('<link href="path/to/style.css" media="all" rel="stylesheet">', rendered) assertInHTML('<link href="path/to/style.css" media="all" rel="stylesheet">', rendered)
self.assertInHTML('<link href="path/to/style2.css" media="all" rel="stylesheet">', rendered) assertInHTML('<link href="path/to/style2.css" media="all" rel="stylesheet">', rendered)
self.assertInHTML('<link href="path/to/style3.css" media="print" rel="stylesheet">', rendered) assertInHTML('<link href="path/to/style3.css" media="print" rel="stylesheet">', rendered)
self.assertInHTML('<link href="path/to/style4.css" media="screen" rel="stylesheet">', rendered) assertInHTML('<link href="path/to/style4.css" media="screen" rel="stylesheet">', rendered)
self.assertInHTML('<script src="path/to/script.js"></script>', rendered) assertInHTML('<script src="path/to/script.js"></script>', rendered)
self.assertInHTML('<script src="path/to/script2.js"></script>', rendered) assertInHTML('<script src="path/to/script2.js"></script>', rendered)
self.assertInHTML('<script src="path/to/script3.js"></script>', rendered) assertInHTML('<script src="path/to/script3.js"></script>', rendered)
def test_str(self): def test_str(self):
""" """
@ -605,13 +542,13 @@ class MediaPathAsObjectTests(BaseTestCase):
rendered = SimpleComponent.render() rendered = SimpleComponent.render()
self.assertInHTML('<link href="path/to/style.css" media="all" rel="stylesheet">', rendered) assertInHTML('<link href="path/to/style.css" media="all" rel="stylesheet">', rendered)
self.assertInHTML('<link href="path/to/style2.css" media="all" rel="stylesheet">', rendered) assertInHTML('<link href="path/to/style2.css" media="all" rel="stylesheet">', rendered)
self.assertInHTML('<link href="path/to/style3.css" media="print" rel="stylesheet">', rendered) assertInHTML('<link href="path/to/style3.css" media="print" rel="stylesheet">', rendered)
self.assertInHTML('<link href="path/to/style4.css" media="screen" rel="stylesheet">', rendered) assertInHTML('<link href="path/to/style4.css" media="screen" rel="stylesheet">', rendered)
self.assertInHTML('<script src="path/to/script.js"></script>', rendered) assertInHTML('<script src="path/to/script.js"></script>', rendered)
self.assertInHTML('<script src="path/to/script2.js"></script>', rendered) assertInHTML('<script src="path/to/script2.js"></script>', rendered)
def test_bytes(self): def test_bytes(self):
""" """
@ -647,13 +584,13 @@ class MediaPathAsObjectTests(BaseTestCase):
rendered = SimpleComponent.render() rendered = SimpleComponent.render()
self.assertInHTML('<link href="path/to/style.css" media="all" rel="stylesheet">', rendered) assertInHTML('<link href="path/to/style.css" media="all" rel="stylesheet">', rendered)
self.assertInHTML('<link href="path/to/style2.css" media="all" rel="stylesheet">', rendered) assertInHTML('<link href="path/to/style2.css" media="all" rel="stylesheet">', rendered)
self.assertInHTML('<link href="path/to/style3.css" media="print" rel="stylesheet">', rendered) assertInHTML('<link href="path/to/style3.css" media="print" rel="stylesheet">', rendered)
self.assertInHTML('<link href="path/to/style4.css" media="screen" rel="stylesheet">', rendered) assertInHTML('<link href="path/to/style4.css" media="screen" rel="stylesheet">', rendered)
self.assertInHTML('<script src="path/to/script.js"></script>', rendered) assertInHTML('<script src="path/to/script.js"></script>', rendered)
self.assertInHTML('<script src="path/to/script2.js"></script>', rendered) assertInHTML('<script src="path/to/script2.js"></script>', rendered)
def test_function(self): def test_function(self):
class SimpleComponent(Component): class SimpleComponent(Component):
@ -679,17 +616,21 @@ class MediaPathAsObjectTests(BaseTestCase):
rendered = SimpleComponent.render() rendered = SimpleComponent.render()
self.assertInHTML('<link hi href="calendar/style.css" rel="stylesheet" />', rendered) assertInHTML('<link hi href="calendar/style.css" rel="stylesheet" />', rendered)
self.assertInHTML('<link href="calendar/style1.css" media="all" rel="stylesheet">', rendered) assertInHTML('<link href="calendar/style1.css" media="all" rel="stylesheet">', rendered)
self.assertInHTML('<link href="calendar/style2.css" media="all" rel="stylesheet">', rendered) assertInHTML('<link href="calendar/style2.css" media="all" rel="stylesheet">', rendered)
self.assertInHTML('<link href="calendar/style3.css" media="all" rel="stylesheet">', rendered) assertInHTML('<link href="calendar/style3.css" media="all" rel="stylesheet">', rendered)
self.assertInHTML('<script hi src="calendar/script.js"></script>', rendered) assertInHTML('<script hi src="calendar/script.js"></script>', rendered)
self.assertInHTML('<script src="calendar/script1.js"></script>', rendered) assertInHTML('<script src="calendar/script1.js"></script>', rendered)
self.assertInHTML('<script src="calendar/script2.js"></script>', rendered) assertInHTML('<script src="calendar/script2.js"></script>', rendered)
self.assertInHTML('<script src="calendar/script3.js"></script>', rendered) assertInHTML('<script src="calendar/script3.js"></script>', rendered)
@override_settings(STATIC_URL="static/") @djc_test(
django_settings={
"STATIC_URL": "static/",
}
)
def test_works_with_static(self): def test_works_with_static(self):
"""Test that all the different ways of defining media files works with Django's staticfiles""" """Test that all the different ways of defining media files works with Django's staticfiles"""
@ -718,32 +659,35 @@ class MediaPathAsObjectTests(BaseTestCase):
rendered = SimpleComponent.render() rendered = SimpleComponent.render()
self.assertInHTML('<link hi href="/static/calendar/style.css" rel="stylesheet" />', rendered) assertInHTML('<link hi href="/static/calendar/style.css" rel="stylesheet" />', rendered)
self.assertInHTML('<link href="/static/calendar/style1.css" media="all" rel="stylesheet" />', rendered) assertInHTML('<link href="/static/calendar/style1.css" media="all" rel="stylesheet" />', rendered)
self.assertInHTML('<link href="/static/calendar/style1.css" media="all" rel="stylesheet">', rendered) assertInHTML('<link href="/static/calendar/style1.css" media="all" rel="stylesheet">', rendered)
self.assertInHTML('<link href="/static/calendar/style2.css" media="all" rel="stylesheet">', rendered) assertInHTML('<link href="/static/calendar/style2.css" media="all" rel="stylesheet">', rendered)
self.assertInHTML('<link href="/static/calendar/style3.css" media="all" rel="stylesheet">', rendered) assertInHTML('<link href="/static/calendar/style3.css" media="all" rel="stylesheet">', rendered)
self.assertInHTML('<script hi src="/static/calendar/script.js"></script>', rendered) assertInHTML('<script hi src="/static/calendar/script.js"></script>', rendered)
self.assertInHTML('<script src="/static/calendar/script1.js"></script>', rendered) assertInHTML('<script src="/static/calendar/script1.js"></script>', rendered)
self.assertInHTML('<script src="/static/calendar/script2.js"></script>', rendered) assertInHTML('<script src="/static/calendar/script2.js"></script>', rendered)
self.assertInHTML('<script src="/static/calendar/script3.js"></script>', rendered) assertInHTML('<script src="/static/calendar/script3.js"></script>', rendered)
class MediaStaticfilesTests(BaseTestCase): @djc_test
class TestMediaStaticfiles:
# For context see https://github.com/django-components/django-components/issues/522 # For context see https://github.com/django-components/django-components/issues/522
@override_settings( @djc_test(
django_settings={
# Configure static files. The dummy files are set up in the `./static_root` dir. # Configure static files. The dummy files are set up in the `./static_root` dir.
# The URL should have path prefix /static/. # The URL should have path prefix /static/.
# NOTE: We don't need STATICFILES_DIRS, because we don't run collectstatic # NOTE: We don't need STATICFILES_DIRS, because we don't run collectstatic
# See https://docs.djangoproject.com/en/5.0/ref/settings/#std-setting-STATICFILES_DIRS # See https://docs.djangoproject.com/en/5.0/ref/settings/#std-setting-STATICFILES_DIRS
STATIC_URL="static/", "STATIC_URL": "static/",
STATIC_ROOT=os.path.join(Path(__file__).resolve().parent, "static_root"), "STATIC_ROOT": os.path.join(Path(__file__).resolve().parent, "static_root"),
# `django.contrib.staticfiles` MUST be installed for staticfiles resolution to work. # `django.contrib.staticfiles` MUST be installed for staticfiles resolution to work.
INSTALLED_APPS=[ "INSTALLED_APPS": [
"django.contrib.staticfiles", "django.contrib.staticfiles",
"django_components", "django_components",
], ],
}
) )
def test_default_static_files_storage(self): def test_default_static_files_storage(self):
"""Test integration with Django's staticfiles app""" """Test integration with Django's staticfiles app"""
@ -773,21 +717,22 @@ class MediaStaticfilesTests(BaseTestCase):
# NOTE: Since we're using the default storage class for staticfiles, the files should # 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. # be searched as specified above (e.g. `calendar/script.js`) inside `static_root` dir.
self.assertInHTML('<link href="/static/calendar/style.css" media="all" rel="stylesheet">', rendered) assertInHTML('<link href="/static/calendar/style.css" media="all" rel="stylesheet">', rendered)
self.assertInHTML('<script defer src="/static/calendar/script.js"></script>', rendered) assertInHTML('<script defer src="/static/calendar/script.js"></script>', rendered)
# For context see https://github.com/django-components/django-components/issues/522 # For context see https://github.com/django-components/django-components/issues/522
@override_settings( @djc_test(
django_settings={
# Configure static files. The dummy files are set up in the `./static_root` dir. # Configure static files. The dummy files are set up in the `./static_root` dir.
# The URL should have path prefix /static/. # The URL should have path prefix /static/.
# NOTE: We don't need STATICFILES_DIRS, because we don't run collectstatic # NOTE: We don't need STATICFILES_DIRS, because we don't run collectstatic
# See https://docs.djangoproject.com/en/5.0/ref/settings/#std-setting-STATICFILES_DIRS # See https://docs.djangoproject.com/en/5.0/ref/settings/#std-setting-STATICFILES_DIRS
STATIC_URL="static/", "STATIC_URL": "static/",
STATIC_ROOT=os.path.join(Path(__file__).resolve().parent, "static_root"), "STATIC_ROOT": os.path.join(Path(__file__).resolve().parent, "static_root"),
# NOTE: STATICFILES_STORAGE is deprecated since 5.1, use STORAGES instead # NOTE: STATICFILES_STORAGE is deprecated since 5.1, use STORAGES instead
# See https://docs.djangoproject.com/en/5.0/ref/settings/#staticfiles-storage # See https://docs.djangoproject.com/en/5.0/ref/settings/#staticfiles-storage
STORAGES={ "STORAGES": {
# This was NOT changed # This was NOT changed
"default": { "default": {
"BACKEND": "django.core.files.storage.FileSystemStorage", "BACKEND": "django.core.files.storage.FileSystemStorage",
@ -798,10 +743,11 @@ class MediaStaticfilesTests(BaseTestCase):
}, },
}, },
# `django.contrib.staticfiles` MUST be installed for staticfiles resolution to work. # `django.contrib.staticfiles` MUST be installed for staticfiles resolution to work.
INSTALLED_APPS=[ "INSTALLED_APPS": [
"django.contrib.staticfiles", "django.contrib.staticfiles",
"django_components", "django_components",
], ],
}
) )
def test_manifest_static_files_storage(self): def test_manifest_static_files_storage(self):
"""Test integration with Django's staticfiles app and ManifestStaticFilesStorage""" """Test integration with Django's staticfiles app and ManifestStaticFilesStorage"""
@ -831,14 +777,15 @@ class MediaStaticfilesTests(BaseTestCase):
# NOTE: Since we're using ManifestStaticFilesStorage, we expect the rendered media to link # NOTE: Since we're using ManifestStaticFilesStorage, we expect the rendered media to link
# to the files as defined in staticfiles.json # to the files as defined in staticfiles.json
self.assertInHTML( assertInHTML(
'<link href="/static/calendar/style.0eeb72042b59.css" media="all" rel="stylesheet">', rendered '<link href="/static/calendar/style.0eeb72042b59.css" media="all" rel="stylesheet">', rendered
) )
self.assertInHTML('<script defer src="/static/calendar/script.e1815e23e0ec.js"></script>', rendered) assertInHTML('<script defer src="/static/calendar/script.e1815e23e0ec.js"></script>', rendered)
class MediaRelativePathTests(BaseTestCase): @djc_test
class TestMediaRelativePath:
class ParentComponent(Component): class ParentComponent(Component):
template: types.django_html = """ template: types.django_html = """
{% load component_tags %} {% load component_tags %}
@ -874,25 +821,26 @@ class MediaRelativePathTests(BaseTestCase):
context["unique_variable"] = new_variable context["unique_variable"] = new_variable
return context return context
def setUp(self): # Settings required for autodiscover to work
super().setUp() @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.ParentComponent) registry.register(name="parent_component", component=self.ParentComponent)
registry.register(name="variable_display", component=self.VariableDisplay) registry.register(name="variable_display", component=self.VariableDisplay)
# Settings required for autodiscover to work
@override_settings(
BASE_DIR=Path(__file__).resolve().parent,
STATICFILES_DIRS=[
Path(__file__).resolve().parent / "components",
],
)
def test_component_with_relative_media_paths(self):
# Ensure that the module is executed again after import in autodiscovery # Ensure that the module is executed again after import in autodiscovery
if "tests.components.relative_file.relative_file" in sys.modules: if "tests.components.relative_file.relative_file" in sys.modules:
del sys.modules["tests.components.relative_file.relative_file"] del sys.modules["tests.components.relative_file.relative_file"]
# Fix the paths, since the "components" dir is nested # Fix the paths, since the "components" dir is nested
with autodiscover_with_cleanup(map_module=lambda p: f"tests.{p}" if p.startswith("components") else p): autodiscover(map_module=lambda p: f"tests.{p}" if p.startswith("components") else p)
# Make sure that only relevant components are registered: # Make sure that only relevant components are registered:
comps_to_remove = [ comps_to_remove = [
comp_name comp_name
@ -911,9 +859,9 @@ class MediaRelativePathTests(BaseTestCase):
template = Template(template_str) template = Template(template_str)
rendered = render_dependencies(template.render(Context({"variable": "test"}))) rendered = render_dependencies(template.render(Context({"variable": "test"})))
self.assertInHTML('<link href="relative_file/relative_file.css" media="all" rel="stylesheet">', rendered) assertInHTML('<link href="relative_file/relative_file.css" media="all" rel="stylesheet">', rendered)
self.assertInHTML( assertInHTML(
""" """
<form data-djc-id-a1bc41 method="post"> <form data-djc-id-a1bc41 method="post">
<input type="text" name="variable" value="test"> <input type="text" name="variable" value="test">
@ -923,22 +871,28 @@ class MediaRelativePathTests(BaseTestCase):
rendered, rendered,
) )
self.assertInHTML('<link href="relative_file/relative_file.css" media="all" rel="stylesheet">', rendered) assertInHTML('<link href="relative_file/relative_file.css" media="all" rel="stylesheet">', rendered)
# Settings required for autodiscover to work # Settings required for autodiscover to work
@override_settings( @djc_test(
BASE_DIR=Path(__file__).resolve().parent, django_settings={
STATICFILES_DIRS=[ "BASE_DIR": Path(__file__).resolve().parent,
"STATICFILES_DIRS": [
Path(__file__).resolve().parent / "components", Path(__file__).resolve().parent / "components",
], ],
}
) )
def test_component_with_relative_media_paths_as_subcomponent(self): def test_component_with_relative_media_paths_as_subcomponent(self):
registry.register(name="parent_component", component=self.ParentComponent)
registry.register(name="variable_display", component=self.VariableDisplay)
# Ensure that the module is executed again after import in autodiscovery # Ensure that the module is executed again after import in autodiscovery
if "tests.components.relative_file.relative_file" in sys.modules: if "tests.components.relative_file.relative_file" in sys.modules:
del sys.modules["tests.components.relative_file.relative_file"] del sys.modules["tests.components.relative_file.relative_file"]
# Fix the paths, since the "components" dir is nested # Fix the paths, since the "components" dir is nested
with autodiscover_with_cleanup(map_module=lambda p: f"tests.{p}" if p.startswith("components") else p): autodiscover(map_module=lambda p: f"tests.{p}" if p.startswith("components") else p)
registry.unregister("relative_file_pathobj_component") registry.unregister("relative_file_pathobj_component")
template_str: types.django_html = """ template_str: types.django_html = """
@ -954,14 +908,16 @@ class MediaRelativePathTests(BaseTestCase):
""" """
template = Template(template_str) template = Template(template_str)
rendered = template.render(Context({})) rendered = template.render(Context({}))
self.assertInHTML('<input type="text" name="variable" value="hello">', rendered) assertInHTML('<input type="text" name="variable" value="hello">', rendered)
# Settings required for autodiscover to work # Settings required for autodiscover to work
@override_settings( @djc_test(
BASE_DIR=Path(__file__).resolve().parent, django_settings={
STATICFILES_DIRS=[ "BASE_DIR": Path(__file__).resolve().parent,
"STATICFILES_DIRS": [
Path(__file__).resolve().parent / "components", Path(__file__).resolve().parent / "components",
], ],
}
) )
def test_component_with_relative_media_does_not_trigger_safestring_path_at__new__(self): def test_component_with_relative_media_does_not_trigger_safestring_path_at__new__(self):
""" """
@ -974,13 +930,16 @@ class MediaRelativePathTests(BaseTestCase):
https://github.com/django-components/django-components/issues/522#issuecomment-2173577094 https://github.com/django-components/django-components/issues/522#issuecomment-2173577094
""" """
registry.register(name="parent_component", component=self.ParentComponent)
registry.register(name="variable_display", component=self.VariableDisplay)
# Ensure that the module is executed again after import in autodiscovery # Ensure that the module is executed again after import in autodiscovery
if "tests.components.relative_file_pathobj.relative_file_pathobj" in sys.modules: if "tests.components.relative_file_pathobj.relative_file_pathobj" in sys.modules:
del sys.modules["tests.components.relative_file_pathobj.relative_file_pathobj"] del sys.modules["tests.components.relative_file_pathobj.relative_file_pathobj"]
# Fix the paths, since the "components" dir is nested # Fix the paths, since the "components" dir is nested
with autodiscover_with_cleanup(map_module=lambda p: f"tests.{p}" if p.startswith("components") else p): 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 # Mark the PathObj instances of 'relative_file_pathobj_component' so they won't raise
# error if PathObj.__str__ is triggered. # error if PathObj.__str__ is triggered.
CompCls = registry.get("relative_file_pathobj_component") CompCls = registry.get("relative_file_pathobj_component")
@ -989,13 +948,14 @@ class MediaRelativePathTests(BaseTestCase):
rendered = CompCls.render(kwargs={"variable": "abc"}) rendered = CompCls.render(kwargs={"variable": "abc"})
self.assertInHTML('<input type="text" name="variable" value="abc">', rendered) assertInHTML('<input type="text" name="variable" value="abc">', rendered)
self.assertInHTML('<link href="relative_file_pathobj.css" rel="stylesheet">', rendered) assertInHTML('<link href="relative_file_pathobj.css" rel="stylesheet">', rendered)
self.assertInHTML('<script type="module" src="relative_file_pathobj.js"></script>', rendered) assertInHTML('<script type="module" src="relative_file_pathobj.js"></script>', rendered)
class SubclassingMediaTests(BaseTestCase): @djc_test
class TestSubclassingMedia:
def test_media_in_child_and_parent(self): def test_media_in_child_and_parent(self):
class ParentComponent(Component): class ParentComponent(Component):
template: types.django_html = """ template: types.django_html = """
@ -1015,11 +975,11 @@ class SubclassingMediaTests(BaseTestCase):
rendered = ChildComponent.render() rendered = ChildComponent.render()
self.assertInHTML('<link href="child.css" media="all" rel="stylesheet">', rendered) assertInHTML('<link href="child.css" media="all" rel="stylesheet">', rendered)
self.assertInHTML('<link href="parent.css" media="all" rel="stylesheet">', rendered) assertInHTML('<link href="parent.css" media="all" rel="stylesheet">', rendered)
self.assertInHTML('<script src="child.js"></script>', rendered) assertInHTML('<script src="child.js"></script>', rendered)
self.assertInHTML('<script src="parent.js"></script>', rendered) assertInHTML('<script src="parent.js"></script>', rendered)
def test_media_in_child_and_grandparent(self): def test_media_in_child_and_grandparent(self):
class GrandParentComponent(Component): class GrandParentComponent(Component):
@ -1043,11 +1003,11 @@ class SubclassingMediaTests(BaseTestCase):
rendered = ChildComponent.render() rendered = ChildComponent.render()
self.assertInHTML('<link href="child.css" media="all" rel="stylesheet">', rendered) assertInHTML('<link href="child.css" media="all" rel="stylesheet">', rendered)
self.assertInHTML('<link href="grandparent.css" media="all" rel="stylesheet">', rendered) assertInHTML('<link href="grandparent.css" media="all" rel="stylesheet">', rendered)
self.assertInHTML('<script src="child.js"></script>', rendered) assertInHTML('<script src="child.js"></script>', rendered)
self.assertInHTML('<script src="grandparent.js"></script>', rendered) assertInHTML('<script src="grandparent.js"></script>', rendered)
def test_media_in_parent_and_grandparent(self): def test_media_in_parent_and_grandparent(self):
class GrandParentComponent(Component): class GrandParentComponent(Component):
@ -1071,11 +1031,11 @@ class SubclassingMediaTests(BaseTestCase):
rendered = ChildComponent.render() rendered = ChildComponent.render()
self.assertInHTML('<link href="parent.css" media="all" rel="stylesheet">', rendered) assertInHTML('<link href="parent.css" media="all" rel="stylesheet">', rendered)
self.assertInHTML('<link href="grandparent.css" media="all" rel="stylesheet">', rendered) assertInHTML('<link href="grandparent.css" media="all" rel="stylesheet">', rendered)
self.assertInHTML('<script src="parent.js"></script>', rendered) assertInHTML('<script src="parent.js"></script>', rendered)
self.assertInHTML('<script src="grandparent.js"></script>', rendered) assertInHTML('<script src="grandparent.js"></script>', rendered)
def test_media_in_multiple_bases(self): def test_media_in_multiple_bases(self):
class GrandParent1Component(Component): class GrandParent1Component(Component):
@ -1118,15 +1078,15 @@ class SubclassingMediaTests(BaseTestCase):
rendered = ChildComponent.render() rendered = ChildComponent.render()
self.assertInHTML('<link href="child.css" media="all" rel="stylesheet">', rendered) assertInHTML('<link href="child.css" media="all" rel="stylesheet">', rendered)
self.assertInHTML('<link href="parent1.css" media="all" rel="stylesheet">', rendered) assertInHTML('<link href="parent1.css" media="all" rel="stylesheet">', rendered)
self.assertInHTML('<link href="grandparent1.css" media="all" rel="stylesheet">', rendered) assertInHTML('<link href="grandparent1.css" media="all" rel="stylesheet">', rendered)
self.assertInHTML('<link href="grandparent3.css" media="all" rel="stylesheet">', rendered) assertInHTML('<link href="grandparent3.css" media="all" rel="stylesheet">', rendered)
self.assertInHTML('<script src="child.js"></script>', rendered) assertInHTML('<script src="child.js"></script>', rendered)
self.assertInHTML('<script src="parent1.js"></script>', rendered) assertInHTML('<script src="parent1.js"></script>', rendered)
self.assertInHTML('<script src="grandparent1.js"></script>', rendered) assertInHTML('<script src="grandparent1.js"></script>', rendered)
self.assertInHTML('<script src="grandparent3.js"></script>', rendered) assertInHTML('<script src="grandparent3.js"></script>', rendered)
def test_extend_false_in_child(self): def test_extend_false_in_child(self):
class Parent1Component(Component): class Parent1Component(Component):
@ -1153,13 +1113,13 @@ class SubclassingMediaTests(BaseTestCase):
rendered = ChildComponent.render() rendered = ChildComponent.render()
self.assertNotIn("parent1.css", rendered) assert "parent1.css" not in rendered
self.assertNotIn("parent2.css", rendered) assert "parent2.css" not in rendered
self.assertInHTML('<link href="child.css" media="all" rel="stylesheet">', rendered) assertInHTML('<link href="child.css" media="all" rel="stylesheet">', rendered)
self.assertNotIn("parent1.js", rendered) assert "parent1.js" not in rendered
self.assertNotIn("parent2.js", rendered) assert "parent2.js" not in rendered
self.assertInHTML('<script src="child.js"></script>', rendered) assertInHTML('<script src="child.js"></script>', rendered)
def test_extend_false_in_parent(self): def test_extend_false_in_parent(self):
class GrandParentComponent(Component): class GrandParentComponent(Component):
@ -1191,15 +1151,15 @@ class SubclassingMediaTests(BaseTestCase):
rendered = ChildComponent.render() rendered = ChildComponent.render()
self.assertNotIn("grandparent.css", rendered) assert "grandparent.css" not in rendered
self.assertInHTML('<link href="parent1.css" media="all" rel="stylesheet">', rendered) assertInHTML('<link href="parent1.css" media="all" rel="stylesheet">', rendered)
self.assertInHTML('<link href="parent2.css" media="all" rel="stylesheet">', rendered) assertInHTML('<link href="parent2.css" media="all" rel="stylesheet">', rendered)
self.assertInHTML('<link href="child.css" media="all" rel="stylesheet">', rendered) assertInHTML('<link href="child.css" media="all" rel="stylesheet">', rendered)
self.assertNotIn("grandparent.js", rendered) assert "grandparent.js" not in rendered
self.assertInHTML('<script src="parent1.js"></script>', rendered) assertInHTML('<script src="parent1.js"></script>', rendered)
self.assertInHTML('<script src="parent2.js"></script>', rendered) assertInHTML('<script src="parent2.js"></script>', rendered)
self.assertInHTML('<script src="child.js"></script>', rendered) assertInHTML('<script src="child.js"></script>', rendered)
def test_extend_list_in_child(self): def test_extend_list_in_child(self):
class Parent1Component(Component): class Parent1Component(Component):
@ -1236,17 +1196,17 @@ class SubclassingMediaTests(BaseTestCase):
rendered = ChildComponent.render() rendered = ChildComponent.render()
self.assertNotIn("parent1.css", rendered) assert "parent1.css" not in rendered
self.assertNotIn("parent2.css", rendered) assert "parent2.css" not in rendered
self.assertInHTML('<link href="other1.css" media="all" rel="stylesheet">', rendered) assertInHTML('<link href="other1.css" media="all" rel="stylesheet">', rendered)
self.assertInHTML('<link href="other2.css" media="all" rel="stylesheet">', rendered) assertInHTML('<link href="other2.css" media="all" rel="stylesheet">', rendered)
self.assertInHTML('<link href="child.css" media="all" rel="stylesheet">', rendered) assertInHTML('<link href="child.css" media="all" rel="stylesheet">', rendered)
self.assertNotIn("parent1.js", rendered) assert "parent1.js" not in rendered
self.assertNotIn("parent2.js", rendered) assert "parent2.js" not in rendered
self.assertInHTML('<script src="other1.js"></script>', rendered) assertInHTML('<script src="other1.js"></script>', rendered)
self.assertInHTML('<script src="other2.js"></script>', rendered) assertInHTML('<script src="other2.js"></script>', rendered)
self.assertInHTML('<script src="child.js"></script>', rendered) assertInHTML('<script src="child.js"></script>', rendered)
def test_extend_list_in_parent(self): def test_extend_list_in_parent(self):
class Other1Component(Component): class Other1Component(Component):
@ -1288,16 +1248,16 @@ class SubclassingMediaTests(BaseTestCase):
rendered = ChildComponent.render() rendered = ChildComponent.render()
self.assertNotIn("grandparent.css", rendered) assert "grandparent.css" not in rendered
self.assertInHTML('<link href="other1.css" media="all" rel="stylesheet">', rendered) assertInHTML('<link href="other1.css" media="all" rel="stylesheet">', rendered)
self.assertInHTML('<link href="other2.css" media="all" rel="stylesheet">', rendered) assertInHTML('<link href="other2.css" media="all" rel="stylesheet">', rendered)
self.assertInHTML('<link href="parent1.css" media="all" rel="stylesheet">', rendered) assertInHTML('<link href="parent1.css" media="all" rel="stylesheet">', rendered)
self.assertInHTML('<link href="parent2.css" media="all" rel="stylesheet">', rendered) assertInHTML('<link href="parent2.css" media="all" rel="stylesheet">', rendered)
self.assertInHTML('<link href="child.css" media="all" rel="stylesheet">', rendered) assertInHTML('<link href="child.css" media="all" rel="stylesheet">', rendered)
self.assertNotIn("grandparent.js", rendered) assert "grandparent.js" not in rendered
self.assertInHTML('<script src="other1.js"></script>', rendered) assertInHTML('<script src="other1.js"></script>', rendered)
self.assertInHTML('<script src="other2.js"></script>', rendered) assertInHTML('<script src="other2.js"></script>', rendered)
self.assertInHTML('<script src="parent1.js"></script>', rendered) assertInHTML('<script src="parent1.js"></script>', rendered)
self.assertInHTML('<script src="parent2.js"></script>', rendered) assertInHTML('<script src="parent2.js"></script>', rendered)
self.assertInHTML('<script src="child.js"></script>', rendered) assertInHTML('<script src="child.js"></script>', rendered)

View file

@ -1,12 +1,15 @@
import re
from typing import Dict, Optional from typing import Dict, Optional
import pytest
from django.http import HttpRequest from django.http import HttpRequest
from django.template import Context, RequestContext, Template from django.template import Context, RequestContext, Template
from pytest_django.asserts import assertHTMLEqual, assertInHTML
from django_components import Component, register, registry, types from django_components import Component, register, registry, types
from .django_test_setup import setup_test_config from django_components.testing import djc_test
from .testutils import BaseTestCase, parametrize_context_behavior from .testutils import PARAMETRIZE_CONTEXT_BEHAVIOR, setup_test_config
setup_test_config({"autodiscover": False}) setup_test_config({"autodiscover": False})
@ -66,7 +69,8 @@ class IncrementerComponent(Component):
######################### #########################
class ContextTests(BaseTestCase): @djc_test
class TestContext:
class ParentComponent(Component): class ParentComponent(Component):
template: types.django_html = """ template: types.django_html = """
{% load component_tags %} {% load component_tags %}
@ -87,15 +91,13 @@ class ContextTests(BaseTestCase):
def get_context_data(self): def get_context_data(self):
return {"shadowing_variable": "NOT SHADOWED"} return {"shadowing_variable": "NOT SHADOWED"}
def setUp(self): @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
super().setUp() def test_nested_component_context_shadows_parent_with_unfilled_slots_and_component_tag(
self, components_settings,
):
registry.register(name="variable_display", component=VariableDisplay) registry.register(name="variable_display", component=VariableDisplay)
registry.register(name="parent_component", component=self.ParentComponent) registry.register(name="parent_component", component=self.ParentComponent)
@parametrize_context_behavior(["django", "isolated"])
def test_nested_component_context_shadows_parent_with_unfilled_slots_and_component_tag(
self,
):
template_str: types.django_html = """ template_str: types.django_html = """
{% load component_tags %} {% load component_tags %}
{% component 'parent_component' %}{% endcomponent %} {% component 'parent_component' %}{% endcomponent %}
@ -103,14 +105,17 @@ class ContextTests(BaseTestCase):
template = Template(template_str) template = Template(template_str)
rendered = template.render(Context()) rendered = template.render(Context())
self.assertInHTML("<h1 data-djc-id-a1bc43>Shadowing variable = override</h1>", rendered) assertInHTML("<h1 data-djc-id-a1bc43>Shadowing variable = override</h1>", rendered)
self.assertInHTML("<h1 data-djc-id-a1bc44>Shadowing variable = slot_default_override</h1>", rendered) assertInHTML("<h1 data-djc-id-a1bc44>Shadowing variable = slot_default_override</h1>", rendered)
self.assertNotIn("Shadowing variable = NOT SHADOWED", rendered) assert "Shadowing variable = NOT SHADOWED" not in rendered
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_nested_component_instances_have_unique_context_with_unfilled_slots_and_component_tag( def test_nested_component_instances_have_unique_context_with_unfilled_slots_and_component_tag(
self, self, components_settings,
): ):
registry.register(name="variable_display", component=VariableDisplay)
registry.register(name="parent_component", component=self.ParentComponent)
template_str: types.django_html = """ template_str: types.django_html = """
{% load component_tags %} {% load component_tags %}
{% component name='parent_component' %}{% endcomponent %} {% component name='parent_component' %}{% endcomponent %}
@ -118,14 +123,17 @@ class ContextTests(BaseTestCase):
template = Template(template_str) template = Template(template_str)
rendered = template.render(Context()) rendered = template.render(Context())
self.assertInHTML("<h1 data-djc-id-a1bc43>Uniquely named variable = unique_val</h1>", rendered) assertInHTML("<h1 data-djc-id-a1bc43>Uniquely named variable = unique_val</h1>", rendered)
self.assertInHTML( assertInHTML(
"<h1 data-djc-id-a1bc44>Uniquely named variable = slot_default_unique</h1>", "<h1 data-djc-id-a1bc44>Uniquely named variable = slot_default_unique</h1>",
rendered, rendered,
) )
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_nested_component_context_shadows_parent_with_filled_slots(self): def test_nested_component_context_shadows_parent_with_filled_slots(self, components_settings):
registry.register(name="variable_display", component=VariableDisplay)
registry.register(name="parent_component", component=self.ParentComponent)
template_str: types.django_html = """ template_str: types.django_html = """
{% load component_tags %} {% load component_tags %}
{% component 'parent_component' %} {% component 'parent_component' %}
@ -138,12 +146,15 @@ class ContextTests(BaseTestCase):
template = Template(template_str) template = Template(template_str)
rendered = template.render(Context()) rendered = template.render(Context())
self.assertInHTML("<h1 data-djc-id-a1bc45>Shadowing variable = override</h1>", rendered) assertInHTML("<h1 data-djc-id-a1bc45>Shadowing variable = override</h1>", rendered)
self.assertInHTML("<h1 data-djc-id-a1bc46>Shadowing variable = shadow_from_slot</h1>", rendered) assertInHTML("<h1 data-djc-id-a1bc46>Shadowing variable = shadow_from_slot</h1>", rendered)
self.assertNotIn("Shadowing variable = NOT SHADOWED", rendered) assert "Shadowing variable = NOT SHADOWED" not in rendered
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_nested_component_instances_have_unique_context_with_filled_slots(self, components_settings):
registry.register(name="variable_display", component=VariableDisplay)
registry.register(name="parent_component", component=self.ParentComponent)
@parametrize_context_behavior(["django", "isolated"])
def test_nested_component_instances_have_unique_context_with_filled_slots(self):
template_str: types.django_html = """ template_str: types.django_html = """
{% load component_tags %} {% load component_tags %}
{% component 'parent_component' %} {% component 'parent_component' %}
@ -156,13 +167,16 @@ class ContextTests(BaseTestCase):
template = Template(template_str) template = Template(template_str)
rendered = template.render(Context()) rendered = template.render(Context())
self.assertInHTML("<h1 data-djc-id-a1bc45>Uniquely named variable = unique_val</h1>", rendered) assertInHTML("<h1 data-djc-id-a1bc45>Uniquely named variable = unique_val</h1>", rendered)
self.assertInHTML("<h1 data-djc-id-a1bc46>Uniquely named variable = unique_from_slot</h1>", rendered) assertInHTML("<h1 data-djc-id-a1bc46>Uniquely named variable = unique_from_slot</h1>", rendered)
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_nested_component_context_shadows_outer_context_with_unfilled_slots_and_component_tag( def test_nested_component_context_shadows_outer_context_with_unfilled_slots_and_component_tag(
self, self, components_settings,
): ):
registry.register(name="variable_display", component=VariableDisplay)
registry.register(name="parent_component", component=self.ParentComponent)
template_str: types.django_html = """ template_str: types.django_html = """
{% load component_tags %} {% load component_tags %}
{% component name='parent_component' %}{% endcomponent %} {% component name='parent_component' %}{% endcomponent %}
@ -170,14 +184,17 @@ class ContextTests(BaseTestCase):
template = Template(template_str) template = Template(template_str)
rendered = template.render(Context({"shadowing_variable": "NOT SHADOWED"})) rendered = template.render(Context({"shadowing_variable": "NOT SHADOWED"}))
self.assertInHTML("<h1 data-djc-id-a1bc43>Shadowing variable = override</h1>", rendered) assertInHTML("<h1 data-djc-id-a1bc43>Shadowing variable = override</h1>", rendered)
self.assertInHTML("<h1 data-djc-id-a1bc44>Shadowing variable = slot_default_override</h1>", rendered) assertInHTML("<h1 data-djc-id-a1bc44>Shadowing variable = slot_default_override</h1>", rendered)
self.assertNotIn("Shadowing variable = NOT SHADOWED", rendered) assert "Shadowing variable = NOT SHADOWED" not in rendered
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_nested_component_context_shadows_outer_context_with_filled_slots( def test_nested_component_context_shadows_outer_context_with_filled_slots(
self, self, components_settings,
): ):
registry.register(name="variable_display", component=VariableDisplay)
registry.register(name="parent_component", component=self.ParentComponent)
template_str: types.django_html = """ template_str: types.django_html = """
{% load component_tags %} {% load component_tags %}
{% component 'parent_component' %} {% component 'parent_component' %}
@ -190,12 +207,13 @@ class ContextTests(BaseTestCase):
template = Template(template_str) template = Template(template_str)
rendered = template.render(Context({"shadowing_variable": "NOT SHADOWED"})) rendered = template.render(Context({"shadowing_variable": "NOT SHADOWED"}))
self.assertInHTML("<h1 data-djc-id-a1bc45>Shadowing variable = override</h1>", rendered) assertInHTML("<h1 data-djc-id-a1bc45>Shadowing variable = override</h1>", rendered)
self.assertInHTML("<h1 data-djc-id-a1bc46>Shadowing variable = shadow_from_slot</h1>", rendered) assertInHTML("<h1 data-djc-id-a1bc46>Shadowing variable = shadow_from_slot</h1>", rendered)
self.assertNotIn("Shadowing variable = NOT SHADOWED", rendered) assert "Shadowing variable = NOT SHADOWED" not in rendered
class ParentArgsTests(BaseTestCase): @djc_test
class TestParentArgs:
class ParentComponentWithArgs(Component): class ParentComponentWithArgs(Component):
template: types.django_html = """ template: types.django_html = """
{% load component_tags %} {% load component_tags %}
@ -216,14 +234,12 @@ class ParentArgsTests(BaseTestCase):
def get_context_data(self, parent_value): def get_context_data(self, parent_value):
return {"inner_parent_value": parent_value} return {"inner_parent_value": parent_value}
def setUp(self): @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
super().setUp() def test_parent_args_can_be_drawn_from_context(self, components_settings):
registry.register(name="incrementer", component=IncrementerComponent) registry.register(name="incrementer", component=IncrementerComponent)
registry.register(name="parent_with_args", component=self.ParentComponentWithArgs) registry.register(name="parent_with_args", component=self.ParentComponentWithArgs)
registry.register(name="variable_display", component=VariableDisplay) registry.register(name="variable_display", component=VariableDisplay)
@parametrize_context_behavior(["django", "isolated"])
def test_parent_args_can_be_drawn_from_context(self):
template_str: types.django_html = """ template_str: types.django_html = """
{% load component_tags %} {% load component_tags %}
{% component 'parent_with_args' parent_value=parent_value %} {% component 'parent_with_args' parent_value=parent_value %}
@ -232,7 +248,7 @@ class ParentArgsTests(BaseTestCase):
template = Template(template_str) template = Template(template_str)
rendered = template.render(Context({"parent_value": "passed_in"})) rendered = template.render(Context({"parent_value": "passed_in"}))
self.assertHTMLEqual( assertHTMLEqual(
rendered, rendered,
""" """
<div data-djc-id-a1bc3f> <div data-djc-id-a1bc3f>
@ -248,8 +264,12 @@ class ParentArgsTests(BaseTestCase):
""", """,
) )
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_parent_args_available_outside_slots(self): def test_parent_args_available_outside_slots(self, components_settings):
registry.register(name="incrementer", component=IncrementerComponent)
registry.register(name="parent_with_args", component=self.ParentComponentWithArgs)
registry.register(name="variable_display", component=VariableDisplay)
template_str: types.django_html = """ template_str: types.django_html = """
{% load component_tags %} {% load component_tags %}
{% component 'parent_with_args' parent_value='passed_in' %}{%endcomponent %} {% component 'parent_with_args' parent_value='passed_in' %}{%endcomponent %}
@ -257,19 +277,24 @@ class ParentArgsTests(BaseTestCase):
template = Template(template_str) template = Template(template_str)
rendered = template.render(Context()) rendered = template.render(Context())
self.assertInHTML("<h1 data-djc-id-a1bc43>Shadowing variable = passed_in</h1>", rendered) assertInHTML("<h1 data-djc-id-a1bc43>Shadowing variable = passed_in</h1>", rendered)
self.assertInHTML("<h1 data-djc-id-a1bc44>Uniquely named variable = passed_in</h1>", rendered) assertInHTML("<h1 data-djc-id-a1bc44>Uniquely named variable = passed_in</h1>", rendered)
self.assertNotIn("Shadowing variable = NOT SHADOWED", rendered) assert "Shadowing variable = NOT SHADOWED" not in rendered
# NOTE: Second arg in tuple are expected values passed through components. @djc_test(
@parametrize_context_behavior( parametrize=(
["components_settings", "first_val", "second_val"],
[ [
("django", ("passed_in", "passed_in")), [{"context_behavior": "django"}, "passed_in", "passed_in"],
("isolated", ("passed_in", "")), [{"context_behavior": "isolated"}, "passed_in", ""],
] ],
["django", "isolated"],
) )
def test_parent_args_available_in_slots(self, context_behavior_data): )
first_val, second_val = context_behavior_data def test_parent_args_available_in_slots(self, components_settings, first_val, second_val):
registry.register(name="incrementer", component=IncrementerComponent)
registry.register(name="parent_with_args", component=self.ParentComponentWithArgs)
registry.register(name="variable_display", component=VariableDisplay)
template_str: types.django_html = """ template_str: types.django_html = """
{% load component_tags %} {% load component_tags %}
@ -283,7 +308,7 @@ class ParentArgsTests(BaseTestCase):
template = Template(template_str) template = Template(template_str)
rendered = template.render(Context()) rendered = template.render(Context())
self.assertHTMLEqual( assertHTMLEqual(
rendered, rendered,
f""" f"""
<div data-djc-id-a1bc41> <div data-djc-id-a1bc41>
@ -299,26 +324,25 @@ class ParentArgsTests(BaseTestCase):
) )
class ContextCalledOnceTests(BaseTestCase): @djc_test
def setUp(self): class TestContextCalledOnce:
super().setUp() @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_one_context_call_with_simple_component(self, components_settings):
registry.register(name="incrementer", component=IncrementerComponent) registry.register(name="incrementer", component=IncrementerComponent)
@parametrize_context_behavior(["django", "isolated"])
def test_one_context_call_with_simple_component(self):
template_str: types.django_html = """ template_str: types.django_html = """
{% load component_tags %} {% load component_tags %}
{% component name='incrementer' %}{% endcomponent %} {% component name='incrementer' %}{% endcomponent %}
""" """
template = Template(template_str) template = Template(template_str)
rendered = template.render(Context()).strip().replace("\n", "") rendered = template.render(Context()).strip().replace("\n", "")
self.assertHTMLEqual( assertHTMLEqual(
rendered, rendered,
'<p class="incrementer" data-djc-id-a1bc3f>value=1;calls=1</p>', '<p class="incrementer" data-djc-id-a1bc3f>value=1;calls=1</p>',
) )
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_one_context_call_with_simple_component_and_arg(self): def test_one_context_call_with_simple_component_and_arg(self, components_settings):
registry.register(name="incrementer", component=IncrementerComponent)
template_str: types.django_html = """ template_str: types.django_html = """
{% load component_tags %} {% load component_tags %}
{% component name='incrementer' value='2' %}{% endcomponent %} {% component name='incrementer' value='2' %}{% endcomponent %}
@ -326,15 +350,16 @@ class ContextCalledOnceTests(BaseTestCase):
template = Template(template_str) template = Template(template_str)
rendered = template.render(Context()).strip() rendered = template.render(Context()).strip()
self.assertHTMLEqual( assertHTMLEqual(
rendered, rendered,
""" """
<p class="incrementer" data-djc-id-a1bc3f>value=3;calls=1</p> <p class="incrementer" data-djc-id-a1bc3f>value=3;calls=1</p>
""", """,
) )
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_one_context_call_with_component(self): def test_one_context_call_with_component(self, components_settings):
registry.register(name="incrementer", component=IncrementerComponent)
template_str: types.django_html = """ template_str: types.django_html = """
{% load component_tags %} {% load component_tags %}
{% component 'incrementer' %}{% endcomponent %} {% component 'incrementer' %}{% endcomponent %}
@ -342,10 +367,11 @@ class ContextCalledOnceTests(BaseTestCase):
template = Template(template_str) template = Template(template_str)
rendered = template.render(Context()).strip() rendered = template.render(Context()).strip()
self.assertHTMLEqual(rendered, '<p class="incrementer" data-djc-id-a1bc3f>value=1;calls=1</p>') assertHTMLEqual(rendered, '<p class="incrementer" data-djc-id-a1bc3f>value=1;calls=1</p>')
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_one_context_call_with_component_and_arg(self): def test_one_context_call_with_component_and_arg(self, components_settings):
registry.register(name="incrementer", component=IncrementerComponent)
template_str: types.django_html = """ template_str: types.django_html = """
{% load component_tags %} {% load component_tags %}
{% component 'incrementer' value='3' %}{% endcomponent %} {% component 'incrementer' value='3' %}{% endcomponent %}
@ -353,10 +379,11 @@ class ContextCalledOnceTests(BaseTestCase):
template = Template(template_str) template = Template(template_str)
rendered = template.render(Context()).strip() rendered = template.render(Context()).strip()
self.assertHTMLEqual(rendered, '<p class="incrementer" data-djc-id-a1bc3f>value=4;calls=1</p>') assertHTMLEqual(rendered, '<p class="incrementer" data-djc-id-a1bc3f>value=4;calls=1</p>')
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_one_context_call_with_slot(self): def test_one_context_call_with_slot(self, components_settings):
registry.register(name="incrementer", component=IncrementerComponent)
template_str: types.django_html = """ template_str: types.django_html = """
{% load component_tags %} {% load component_tags %}
{% component 'incrementer' %} {% component 'incrementer' %}
@ -368,7 +395,7 @@ class ContextCalledOnceTests(BaseTestCase):
template = Template(template_str) template = Template(template_str)
rendered = template.render(Context()).strip() rendered = template.render(Context()).strip()
self.assertHTMLEqual( assertHTMLEqual(
rendered, rendered,
""" """
<p class="incrementer" data-djc-id-a1bc40>value=1;calls=1</p> <p class="incrementer" data-djc-id-a1bc40>value=1;calls=1</p>
@ -377,8 +404,9 @@ class ContextCalledOnceTests(BaseTestCase):
rendered, rendered,
) )
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_one_context_call_with_slot_and_arg(self): def test_one_context_call_with_slot_and_arg(self, components_settings):
registry.register(name="incrementer", component=IncrementerComponent)
template_str: types.django_html = """ template_str: types.django_html = """
{% load component_tags %} {% load component_tags %}
{% component 'incrementer' value='3' %} {% component 'incrementer' value='3' %}
@ -390,7 +418,7 @@ class ContextCalledOnceTests(BaseTestCase):
template = Template(template_str) template = Template(template_str)
rendered = template.render(Context()).strip() rendered = template.render(Context()).strip()
self.assertHTMLEqual( assertHTMLEqual(
rendered, rendered,
""" """
<p class="incrementer" data-djc-id-a1bc40>value=4;calls=1</p> <p class="incrementer" data-djc-id-a1bc40>value=4;calls=1</p>
@ -400,92 +428,92 @@ class ContextCalledOnceTests(BaseTestCase):
) )
class ComponentsCanAccessOuterContext(BaseTestCase): @djc_test
def setUp(self): class TestComponentsCanAccessOuterContext:
super().setUp() @djc_test(
registry.register(name="simple_component", component=SimpleComponent) parametrize=(
["components_settings", "expected_value"],
# NOTE: Second arg in tuple is expected value.
@parametrize_context_behavior(
[ [
("django", "outer_value"), [{"context_behavior": "django"}, "outer_value"],
("isolated", ""), [{"context_behavior": "isolated"}, ""],
] ],
["django", "isolated"],
) )
def test_simple_component_can_use_outer_context(self, context_behavior_data): )
def test_simple_component_can_use_outer_context(self, components_settings, expected_value):
registry.register(name="simple_component", component=SimpleComponent)
template_str: types.django_html = """ template_str: types.django_html = """
{% load component_tags %} {% load component_tags %}
{% component 'simple_component' %}{% endcomponent %} {% component 'simple_component' %}{% endcomponent %}
""" """
template = Template(template_str) template = Template(template_str)
rendered = template.render(Context({"variable": "outer_value"})) rendered = template.render(Context({"variable": "outer_value"}))
self.assertHTMLEqual( assertHTMLEqual(
rendered, rendered,
f""" f"""
Variable: <strong data-djc-id-a1bc3f> {context_behavior_data} </strong> Variable: <strong data-djc-id-a1bc3f> {expected_value} </strong>
""", """,
) )
class IsolatedContextTests(BaseTestCase): @djc_test
def setUp(self): class TestIsolatedContext:
super().setUp() @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_simple_component_can_pass_outer_context_in_args(self, components_settings):
registry.register(name="simple_component", component=SimpleComponent) registry.register(name="simple_component", component=SimpleComponent)
@parametrize_context_behavior(["django", "isolated"])
def test_simple_component_can_pass_outer_context_in_args(self):
template_str: types.django_html = """ template_str: types.django_html = """
{% load component_tags %} {% load component_tags %}
{% component 'simple_component' variable only %}{% endcomponent %} {% component 'simple_component' variable only %}{% endcomponent %}
""" """
template = Template(template_str) template = Template(template_str)
rendered = template.render(Context({"variable": "outer_value"})).strip() rendered = template.render(Context({"variable": "outer_value"})).strip()
self.assertIn("outer_value", rendered) assert "outer_value" in rendered
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_simple_component_cannot_use_outer_context(self): def test_simple_component_cannot_use_outer_context(self, components_settings):
registry.register(name="simple_component", component=SimpleComponent)
template_str: types.django_html = """ template_str: types.django_html = """
{% load component_tags %} {% load component_tags %}
{% component 'simple_component' only %}{% endcomponent %} {% component 'simple_component' only %}{% endcomponent %}
""" """
template = Template(template_str) template = Template(template_str)
rendered = template.render(Context({"variable": "outer_value"})).strip() rendered = template.render(Context({"variable": "outer_value"})).strip()
self.assertNotIn("outer_value", rendered) assert "outer_value" not in rendered
class IsolatedContextSettingTests(BaseTestCase): @djc_test
def setUp(self): class TestIsolatedContextSetting:
super().setUp() @djc_test(components_settings={"context_behavior": "isolated"})
registry.register(name="simple_component", component=SimpleComponent)
@parametrize_context_behavior(["isolated"])
def test_component_tag_includes_variable_with_isolated_context_from_settings( def test_component_tag_includes_variable_with_isolated_context_from_settings(
self, self,
): ):
registry.register(name="simple_component", component=SimpleComponent)
template_str: types.django_html = """ template_str: types.django_html = """
{% load component_tags %} {% load component_tags %}
{% component 'simple_component' variable %}{% endcomponent %} {% component 'simple_component' variable %}{% endcomponent %}
""" """
template = Template(template_str) template = Template(template_str)
rendered = template.render(Context({"variable": "outer_value"})) rendered = template.render(Context({"variable": "outer_value"}))
self.assertIn("outer_value", rendered) assert "outer_value" in rendered
@parametrize_context_behavior(["isolated"]) @djc_test(components_settings={"context_behavior": "isolated"})
def test_component_tag_excludes_variable_with_isolated_context_from_settings( def test_component_tag_excludes_variable_with_isolated_context_from_settings(
self, self,
): ):
registry.register(name="simple_component", component=SimpleComponent)
template_str: types.django_html = """ template_str: types.django_html = """
{% load component_tags %} {% load component_tags %}
{% component 'simple_component' %}{% endcomponent %} {% component 'simple_component' %}{% endcomponent %}
""" """
template = Template(template_str) template = Template(template_str)
rendered = template.render(Context({"variable": "outer_value"})) rendered = template.render(Context({"variable": "outer_value"}))
self.assertNotIn("outer_value", rendered) assert "outer_value" not in rendered
@parametrize_context_behavior(["isolated"]) @djc_test(components_settings={"context_behavior": "isolated"})
def test_component_includes_variable_with_isolated_context_from_settings( def test_component_includes_variable_with_isolated_context_from_settings(
self, self,
): ):
registry.register(name="simple_component", component=SimpleComponent)
template_str: types.django_html = """ template_str: types.django_html = """
{% load component_tags %} {% load component_tags %}
{% component 'simple_component' variable %} {% component 'simple_component' variable %}
@ -493,12 +521,13 @@ class IsolatedContextSettingTests(BaseTestCase):
""" """
template = Template(template_str) template = Template(template_str)
rendered = template.render(Context({"variable": "outer_value"})) rendered = template.render(Context({"variable": "outer_value"}))
self.assertIn("outer_value", rendered) assert "outer_value" in rendered
@parametrize_context_behavior(["isolated"]) @djc_test(components_settings={"context_behavior": "isolated"})
def test_component_excludes_variable_with_isolated_context_from_settings( def test_component_excludes_variable_with_isolated_context_from_settings(
self, self,
): ):
registry.register(name="simple_component", component=SimpleComponent)
template_str: types.django_html = """ template_str: types.django_html = """
{% load component_tags %} {% load component_tags %}
{% component 'simple_component' %} {% component 'simple_component' %}
@ -506,12 +535,13 @@ class IsolatedContextSettingTests(BaseTestCase):
""" """
template = Template(template_str) template = Template(template_str)
rendered = template.render(Context({"variable": "outer_value"})) rendered = template.render(Context({"variable": "outer_value"}))
self.assertNotIn("outer_value", rendered) assert "outer_value" not in rendered
class ContextProcessorsTests(BaseTestCase): @djc_test
@parametrize_context_behavior(["django", "isolated"]) class TestContextProcessors:
def test_request_context_in_template(self): @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_request_context_in_template(self, components_settings):
context_processors_data: Optional[Dict] = None context_processors_data: Optional[Dict] = None
inner_request: Optional[HttpRequest] = None inner_request: Optional[HttpRequest] = None
@ -537,12 +567,12 @@ class ContextProcessorsTests(BaseTestCase):
template = Template(template_str) template = Template(template_str)
rendered = template.render(request_context) rendered = template.render(request_context)
self.assertIn("csrfmiddlewaretoken", rendered) assert "csrfmiddlewaretoken" in rendered
self.assertEqual(list(context_processors_data.keys()), ["csrf_token"]) # type: ignore[union-attr] assert list(context_processors_data.keys()) == ["csrf_token"] # type: ignore[union-attr]
self.assertEqual(inner_request, request) assert inner_request == request
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_request_context_in_template_nested(self): def test_request_context_in_template_nested(self, components_settings):
context_processors_data = None context_processors_data = None
context_processors_data_child = None context_processors_data_child = None
parent_request: Optional[HttpRequest] = None parent_request: Optional[HttpRequest] = None
@ -583,14 +613,14 @@ class ContextProcessorsTests(BaseTestCase):
template = Template(template_str) template = Template(template_str)
rendered = template.render(request_context) rendered = template.render(request_context)
self.assertIn("csrfmiddlewaretoken", rendered) assert "csrfmiddlewaretoken" in rendered
self.assertEqual(list(context_processors_data.keys()), ["csrf_token"]) # type: ignore[union-attr] assert list(context_processors_data.keys()) == ["csrf_token"] # type: ignore[union-attr]
self.assertEqual(list(context_processors_data_child.keys()), ["csrf_token"]) # type: ignore[union-attr] assert list(context_processors_data_child.keys()) == ["csrf_token"] # type: ignore[union-attr]
self.assertEqual(parent_request, request) assert parent_request == request
self.assertEqual(child_request, request) assert child_request == request
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_request_context_in_template_slot(self): def test_request_context_in_template_slot(self, components_settings):
context_processors_data = None context_processors_data = None
context_processors_data_child = None context_processors_data_child = None
parent_request: Optional[HttpRequest] = None parent_request: Optional[HttpRequest] = None
@ -635,14 +665,14 @@ class ContextProcessorsTests(BaseTestCase):
template = Template(template_str) template = Template(template_str)
rendered = template.render(request_context) rendered = template.render(request_context)
self.assertIn("csrfmiddlewaretoken", rendered) assert "csrfmiddlewaretoken" in rendered
self.assertEqual(list(context_processors_data.keys()), ["csrf_token"]) # type: ignore[union-attr] assert list(context_processors_data.keys()) == ["csrf_token"] # type: ignore[union-attr]
self.assertEqual(list(context_processors_data_child.keys()), ["csrf_token"]) # type: ignore[union-attr] assert list(context_processors_data_child.keys()) == ["csrf_token"] # type: ignore[union-attr]
self.assertEqual(parent_request, request) assert parent_request == request
self.assertEqual(child_request, request) assert child_request == request
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_request_context_in_python(self): def test_request_context_in_python(self, components_settings):
context_processors_data = None context_processors_data = None
inner_request: Optional[HttpRequest] = None inner_request: Optional[HttpRequest] = None
@ -661,12 +691,12 @@ class ContextProcessorsTests(BaseTestCase):
request_context = RequestContext(request) request_context = RequestContext(request)
rendered = TestComponent.render(request_context) rendered = TestComponent.render(request_context)
self.assertIn("csrfmiddlewaretoken", rendered) assert "csrfmiddlewaretoken" in rendered
self.assertEqual(list(context_processors_data.keys()), ["csrf_token"]) # type: ignore[union-attr] assert list(context_processors_data.keys()) == ["csrf_token"] # type: ignore[union-attr]
self.assertEqual(inner_request, request) assert inner_request == request
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_request_context_in_python_nested(self): def test_request_context_in_python_nested(self, components_settings):
context_processors_data: Optional[Dict] = None context_processors_data: Optional[Dict] = None
context_processors_data_child: Optional[Dict] = None context_processors_data_child: Optional[Dict] = None
parent_request: Optional[HttpRequest] = None parent_request: Optional[HttpRequest] = None
@ -701,14 +731,14 @@ class ContextProcessorsTests(BaseTestCase):
request_context = RequestContext(request) request_context = RequestContext(request)
rendered = TestParentComponent.render(request_context) rendered = TestParentComponent.render(request_context)
self.assertIn("csrfmiddlewaretoken", rendered) assert "csrfmiddlewaretoken" in rendered
self.assertEqual(list(context_processors_data.keys()), ["csrf_token"]) # type: ignore[union-attr] assert list(context_processors_data.keys()) == ["csrf_token"] # type: ignore[union-attr]
self.assertEqual(list(context_processors_data_child.keys()), ["csrf_token"]) # type: ignore[union-attr] assert list(context_processors_data_child.keys()) == ["csrf_token"] # type: ignore[union-attr]
self.assertEqual(parent_request, request) assert parent_request == request
self.assertEqual(child_request, request) assert child_request == request
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_request_in_python(self): def test_request_in_python(self, components_settings):
context_processors_data: Optional[Dict] = None context_processors_data: Optional[Dict] = None
inner_request: Optional[HttpRequest] = None inner_request: Optional[HttpRequest] = None
@ -726,12 +756,12 @@ class ContextProcessorsTests(BaseTestCase):
request = HttpRequest() request = HttpRequest()
rendered = TestComponent.render(request=request) rendered = TestComponent.render(request=request)
self.assertIn("csrfmiddlewaretoken", rendered) assert "csrfmiddlewaretoken" in rendered
self.assertEqual(list(context_processors_data.keys()), ["csrf_token"]) # type: ignore[union-attr] assert list(context_processors_data.keys()) == ["csrf_token"] # type: ignore[union-attr]
self.assertEqual(inner_request, request) assert inner_request == request
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_request_in_python_nested(self): def test_request_in_python_nested(self, components_settings):
context_processors_data: Optional[Dict] = None context_processors_data: Optional[Dict] = None
context_processors_data_child: Optional[Dict] = None context_processors_data_child: Optional[Dict] = None
parent_request: Optional[HttpRequest] = None parent_request: Optional[HttpRequest] = None
@ -765,15 +795,15 @@ class ContextProcessorsTests(BaseTestCase):
request = HttpRequest() request = HttpRequest()
rendered = TestParentComponent.render(request=request) rendered = TestParentComponent.render(request=request)
self.assertIn("csrfmiddlewaretoken", rendered) assert "csrfmiddlewaretoken" in rendered
self.assertEqual(list(context_processors_data.keys()), ["csrf_token"]) # type: ignore[union-attr] assert list(context_processors_data.keys()) == ["csrf_token"] # type: ignore[union-attr]
self.assertEqual(list(context_processors_data_child.keys()), ["csrf_token"]) # type: ignore[union-attr] assert list(context_processors_data_child.keys()) == ["csrf_token"] # type: ignore[union-attr]
self.assertEqual(parent_request, request) assert parent_request == request
self.assertEqual(child_request, request) assert child_request == request
# No request, regular Context # No request, regular Context
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_no_context_processor_when_non_request_context_in_python(self): def test_no_context_processor_when_non_request_context_in_python(self, components_settings):
context_processors_data: Optional[Dict] = None context_processors_data: Optional[Dict] = None
inner_request: Optional[HttpRequest] = None inner_request: Optional[HttpRequest] = None
@ -790,13 +820,13 @@ class ContextProcessorsTests(BaseTestCase):
rendered = TestComponent.render(context=Context()) rendered = TestComponent.render(context=Context())
self.assertNotIn("csrfmiddlewaretoken", rendered) assert "csrfmiddlewaretoken" not in rendered
self.assertEqual(list(context_processors_data.keys()), []) # type: ignore[union-attr] assert list(context_processors_data.keys()) == [] # type: ignore[union-attr]
self.assertEqual(inner_request, None) assert inner_request is None
# No request, no Context # No request, no Context
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_no_context_processor_when_non_request_context_in_python_2(self): def test_no_context_processor_when_non_request_context_in_python_2(self, components_settings):
context_processors_data: Optional[Dict] = None context_processors_data: Optional[Dict] = None
inner_request: Optional[HttpRequest] = None inner_request: Optional[HttpRequest] = None
@ -813,13 +843,13 @@ class ContextProcessorsTests(BaseTestCase):
rendered = TestComponent.render() rendered = TestComponent.render()
self.assertNotIn("csrfmiddlewaretoken", rendered) assert "csrfmiddlewaretoken" not in rendered
self.assertEqual(list(context_processors_data.keys()), []) # type: ignore[union-attr] assert list(context_processors_data.keys()) == [] # type: ignore[union-attr]
self.assertEqual(inner_request, None) assert inner_request is None
# Yes request, regular Context # Yes request, regular Context
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_context_processor_when_regular_context_and_request_in_python(self): def test_context_processor_when_regular_context_and_request_in_python(self, components_settings):
context_processors_data: Optional[Dict] = None context_processors_data: Optional[Dict] = None
inner_request: Optional[HttpRequest] = None inner_request: Optional[HttpRequest] = None
@ -837,17 +867,17 @@ class ContextProcessorsTests(BaseTestCase):
request = HttpRequest() request = HttpRequest()
rendered = TestComponent.render(Context(), request=request) rendered = TestComponent.render(Context(), request=request)
self.assertIn("csrfmiddlewaretoken", rendered) assert "csrfmiddlewaretoken" in rendered
self.assertEqual(list(context_processors_data.keys()), ["csrf_token"]) # type: ignore[union-attr] assert list(context_processors_data.keys()) == ["csrf_token"] # type: ignore[union-attr]
self.assertEqual(inner_request, request) assert inner_request == request
def test_raises_on_accessing_context_processors_data_outside_of_rendering(self): def test_raises_on_accessing_context_processors_data_outside_of_rendering(self):
class TestComponent(Component): class TestComponent(Component):
template: types.django_html = """{% csrf_token %}""" template: types.django_html = """{% csrf_token %}"""
with self.assertRaisesMessage( with pytest.raises(
RuntimeError, RuntimeError,
"Tried to access Component's `context_processors_data` attribute while outside of rendering execution", match=re.escape("Tried to access Component's `context_processors_data` attribute while outside of rendering execution"), # noqa: E501
): ):
TestComponent().context_processors_data TestComponent().context_processors_data
@ -855,14 +885,18 @@ class ContextProcessorsTests(BaseTestCase):
class TestComponent(Component): class TestComponent(Component):
template: types.django_html = """{% csrf_token %}""" template: types.django_html = """{% csrf_token %}"""
with self.assertRaisesMessage( with pytest.raises(
RuntimeError, RuntimeError,
"Tried to access Component's `request` attribute while outside of rendering execution", match=re.escape("Tried to access Component's `request` attribute while outside of rendering execution"),
): ):
TestComponent().request TestComponent().request
class OuterContextPropertyTests(BaseTestCase): @djc_test
class TestOuterContextProperty:
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_outer_context_property_with_component(self, components_settings):
@register("outer_context_component")
class OuterContextComponent(Component): class OuterContextComponent(Component):
template: types.django_html = """ template: types.django_html = """
Variable: <strong>{{ variable }}</strong> Variable: <strong>{{ variable }}</strong>
@ -871,22 +905,17 @@ class OuterContextPropertyTests(BaseTestCase):
def get_context_data(self): def get_context_data(self):
return self.outer_context.flatten() # type: ignore[union-attr] return self.outer_context.flatten() # type: ignore[union-attr]
def setUp(self):
super().setUp()
registry.register(name="outer_context_component", component=self.OuterContextComponent)
@parametrize_context_behavior(["django", "isolated"])
def test_outer_context_property_with_component(self):
template_str: types.django_html = """ template_str: types.django_html = """
{% load component_tags %} {% load component_tags %}
{% component 'outer_context_component' only %}{% endcomponent %} {% component 'outer_context_component' only %}{% endcomponent %}
""" """
template = Template(template_str) template = Template(template_str)
rendered = template.render(Context({"variable": "outer_value"})).strip() rendered = template.render(Context({"variable": "outer_value"})).strip()
self.assertIn("outer_value", rendered) assert "outer_value" in rendered
class ContextVarsIsFilledTests(BaseTestCase): @djc_test
class TestContextVarsIsFilled:
class IsFilledVarsComponent(Component): class IsFilledVarsComponent(Component):
template: types.django_html = """ template: types.django_html = """
{% load component_tags %} {% load component_tags %}
@ -936,16 +965,8 @@ class ContextVarsIsFilledTests(BaseTestCase):
</div> </div>
""" """
def setUp(self) -> None: @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
super().setUp() def test_is_filled_vars(self, components_settings):
registry.register("conditional_slots", self.ComponentWithConditionalSlots)
registry.register(
"complex_conditional_slots",
self.ComponentWithComplexConditionalSlots,
)
@parametrize_context_behavior(["django", "isolated"])
def test_is_filled_vars(self):
registry.register("is_filled_vars", self.IsFilledVarsComponent) registry.register("is_filled_vars", self.IsFilledVarsComponent)
template: types.django_html = """ template: types.django_html = """
@ -968,10 +989,10 @@ class ContextVarsIsFilledTests(BaseTestCase):
escape_this_________: True escape_this_________: True
</div> </div>
""" """
self.assertHTMLEqual(rendered, expected) assertHTMLEqual(rendered, expected)
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_is_filled_vars_default(self): def test_is_filled_vars_default(self, components_settings):
registry.register("is_filled_vars", self.IsFilledVarsComponent) registry.register("is_filled_vars", self.IsFilledVarsComponent)
template: types.django_html = """ template: types.django_html = """
@ -991,10 +1012,12 @@ class ContextVarsIsFilledTests(BaseTestCase):
escape_this_________: False escape_this_________: False
</div> </div>
""" """
self.assertHTMLEqual(rendered, expected) assertHTMLEqual(rendered, expected)
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_simple_component_with_conditional_slot(self, components_settings):
registry.register("conditional_slots", self.ComponentWithConditionalSlots)
@parametrize_context_behavior(["django", "isolated"])
def test_simple_component_with_conditional_slot(self):
template: types.django_html = """ template: types.django_html = """
{% load component_tags %} {% load component_tags %}
{% component "conditional_slots" %}{% endcomponent %} {% component "conditional_slots" %}{% endcomponent %}
@ -1007,10 +1030,12 @@ class ContextVarsIsFilledTests(BaseTestCase):
</div> </div>
""" """
rendered = Template(template).render(Context({})) rendered = Template(template).render(Context({}))
self.assertHTMLEqual(rendered, expected) assertHTMLEqual(rendered, expected)
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_component_with_filled_conditional_slot(self, components_settings):
registry.register("conditional_slots", self.ComponentWithConditionalSlots)
@parametrize_context_behavior(["django", "isolated"])
def test_component_with_filled_conditional_slot(self):
template: types.django_html = """ template: types.django_html = """
{% load component_tags %} {% load component_tags %}
{% component "conditional_slots" %} {% component "conditional_slots" %}
@ -1028,10 +1053,15 @@ class ContextVarsIsFilledTests(BaseTestCase):
</div> </div>
""" """
rendered = Template(template).render(Context({})) rendered = Template(template).render(Context({}))
self.assertHTMLEqual(rendered, expected) assertHTMLEqual(rendered, expected)
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_elif_of_complex_conditional_slots(self, components_settings):
registry.register(
"complex_conditional_slots",
self.ComponentWithComplexConditionalSlots,
)
@parametrize_context_behavior(["django", "isolated"])
def test_elif_of_complex_conditional_slots(self):
template: types.django_html = """ template: types.django_html = """
{% load component_tags %} {% load component_tags %}
{% component "complex_conditional_slots" %} {% component "complex_conditional_slots" %}
@ -1049,10 +1079,15 @@ class ContextVarsIsFilledTests(BaseTestCase):
</div> </div>
""" """
rendered = Template(template).render(Context({})) rendered = Template(template).render(Context({}))
self.assertHTMLEqual(rendered, expected) assertHTMLEqual(rendered, expected)
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_else_of_complex_conditional_slots(self, components_settings):
registry.register(
"complex_conditional_slots",
self.ComponentWithComplexConditionalSlots,
)
@parametrize_context_behavior(["django", "isolated"])
def test_else_of_complex_conditional_slots(self):
template: types.django_html = """ template: types.django_html = """
{% load component_tags %} {% load component_tags %}
{% component "complex_conditional_slots" %} {% component "complex_conditional_slots" %}
@ -1067,10 +1102,10 @@ class ContextVarsIsFilledTests(BaseTestCase):
</div> </div>
""" """
rendered = Template(template).render(Context({})) rendered = Template(template).render(Context({}))
self.assertHTMLEqual(rendered, expected) assertHTMLEqual(rendered, expected)
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_component_with_negated_conditional_slot(self): def test_component_with_negated_conditional_slot(self, components_settings):
@register("negated_conditional_slot") @register("negated_conditional_slot")
class ComponentWithNegatedConditionalSlot(Component): class ComponentWithNegatedConditionalSlot(Component):
template: types.django_html = """ template: types.django_html = """
@ -1101,10 +1136,10 @@ class ContextVarsIsFilledTests(BaseTestCase):
</div> </div>
""" """
rendered = Template(template).render(Context({})) rendered = Template(template).render(Context({}))
self.assertHTMLEqual(rendered, expected) assertHTMLEqual(rendered, expected)
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_is_filled_vars_in_hooks(self): def test_is_filled_vars_in_hooks(self, components_settings):
captured_before = None captured_before = None
captured_after = None captured_after = None
@ -1127,5 +1162,5 @@ class ContextVarsIsFilledTests(BaseTestCase):
Template(template).render(Context()) Template(template).render(Context())
expected = {"default": True} expected = {"default": True}
self.assertEqual(captured_before, expected) assert captured_before == expected
self.assertEqual(captured_after, expected) assert captured_after == expected

View file

@ -8,15 +8,17 @@ For checking the OUTPUT of the dependencies, see `test_dependency_rendering.py`.
import re import re
from unittest.mock import Mock from unittest.mock import Mock
import pytest
from django.http import HttpResponseNotModified from django.http import HttpResponseNotModified
from django.template import Context, Template from django.template import Context, Template
from pytest_django.asserts import assertHTMLEqual, assertInHTML
from django_components import Component, registry, render_dependencies, types from django_components import Component, registry, render_dependencies, types
from django_components.components.dynamic import DynamicComponent from django_components.components.dynamic import DynamicComponent
from django_components.middleware import ComponentDependencyMiddleware from django_components.middleware import ComponentDependencyMiddleware
from .django_test_setup import setup_test_config from django_components.testing import djc_test
from .testutils import BaseTestCase, create_and_process_template_response from .testutils import create_and_process_template_response, setup_test_config
setup_test_config({"autodiscover": False}) setup_test_config({"autodiscover": False})
@ -47,7 +49,8 @@ class SimpleComponent(Component):
js = "script.js" js = "script.js"
class RenderDependenciesTests(BaseTestCase): @djc_test
class TestRenderDependencies:
def test_standalone_render_dependencies(self): def test_standalone_render_dependencies(self):
registry.register(name="test", component=SimpleComponent) registry.register(name="test", component=SimpleComponent)
@ -61,23 +64,23 @@ class RenderDependenciesTests(BaseTestCase):
rendered_raw = template.render(Context({})) rendered_raw = template.render(Context({}))
# Placeholders # Placeholders
self.assertEqual(rendered_raw.count('<link name="CSS_PLACEHOLDER">'), 1) assert rendered_raw.count('<link name="CSS_PLACEHOLDER">') == 1
self.assertEqual(rendered_raw.count('<script name="JS_PLACEHOLDER"></script>'), 1) assert rendered_raw.count('<script name="JS_PLACEHOLDER"></script>') == 1
self.assertEqual(rendered_raw.count("<script"), 1) assert rendered_raw.count("<script") == 1
self.assertEqual(rendered_raw.count("<style"), 0) assert rendered_raw.count("<style") == 0
self.assertEqual(rendered_raw.count("<link"), 1) assert rendered_raw.count("<link") == 1
self.assertEqual(rendered_raw.count("_RENDERED"), 1) assert rendered_raw.count("_RENDERED") == 1
rendered = render_dependencies(rendered_raw) rendered = render_dependencies(rendered_raw)
# Dependency manager script # Dependency manager script
self.assertInHTML('<script src="django_components/django_components.min.js"></script>', rendered, count=1) assertInHTML('<script src="django_components/django_components.min.js"></script>', rendered, count=1)
self.assertInHTML("<style>.xyz { color: red; }</style>", rendered, count=1) # Inlined CSS assertInHTML("<style>.xyz { color: red; }</style>", rendered, count=1) # Inlined CSS
self.assertInHTML('<script>console.log("xyz");</script>', rendered, count=1) # Inlined JS assertInHTML('<script>console.log("xyz");</script>', rendered, count=1) # Inlined JS
self.assertInHTML('<link href="style.css" media="all" rel="stylesheet">', rendered, count=1) # Media.css assertInHTML('<link href="style.css" media="all" rel="stylesheet">', rendered, count=1) # Media.css
def test_middleware_renders_dependencies(self): def test_middleware_renders_dependencies(self):
registry.register(name="test", component=SimpleComponent) registry.register(name="test", component=SimpleComponent)
@ -92,14 +95,14 @@ class RenderDependenciesTests(BaseTestCase):
rendered = create_and_process_template_response(template, use_middleware=True) rendered = create_and_process_template_response(template, use_middleware=True)
# Dependency manager script # Dependency manager script
self.assertInHTML('<script src="django_components/django_components.min.js"></script>', rendered, count=1) assertInHTML('<script src="django_components/django_components.min.js"></script>', rendered, count=1)
self.assertInHTML("<style>.xyz { color: red; }</style>", rendered, count=1) # Inlined CSS assertInHTML("<style>.xyz { color: red; }</style>", rendered, count=1) # Inlined CSS
self.assertInHTML('<script>console.log("xyz");</script>', rendered, count=1) # Inlined JS assertInHTML('<script>console.log("xyz");</script>', rendered, count=1) # Inlined JS
self.assertInHTML('<link href="style.css" media="all" rel="stylesheet">', rendered, count=1) # Media.css assertInHTML('<link href="style.css" media="all" rel="stylesheet">', rendered, count=1) # Media.css
self.assertEqual(rendered.count("<link"), 1) assert rendered.count("<link") == 1
self.assertEqual(rendered.count("<style"), 1) assert rendered.count("<style") == 1
def test_component_render_renders_dependencies(self): def test_component_render_renders_dependencies(self):
class SimpleComponentWithDeps(SimpleComponent): class SimpleComponentWithDeps(SimpleComponent):
@ -119,14 +122,14 @@ class RenderDependenciesTests(BaseTestCase):
) )
# Dependency manager script # Dependency manager script
self.assertInHTML('<script src="django_components/django_components.min.js"></script>', rendered, count=1) assertInHTML('<script src="django_components/django_components.min.js"></script>', rendered, count=1)
self.assertInHTML("<style>.xyz { color: red; }</style>", rendered, count=1) # Inlined CSS assertInHTML("<style>.xyz { color: red; }</style>", rendered, count=1) # Inlined CSS
self.assertInHTML('<script>console.log("xyz");</script>', rendered, count=1) # Inlined JS assertInHTML('<script>console.log("xyz");</script>', rendered, count=1) # Inlined JS
self.assertInHTML('<link href="style.css" media="all" rel="stylesheet">', rendered, count=1) # Media.css assertInHTML('<link href="style.css" media="all" rel="stylesheet">', rendered, count=1) # Media.css
self.assertEqual(rendered.count("<link"), 1) assert rendered.count("<link") == 1
self.assertEqual(rendered.count("<style"), 1) assert rendered.count("<style") == 1
def test_component_render_renders_dependencies_opt_out(self): def test_component_render_renders_dependencies_opt_out(self):
class SimpleComponentWithDeps(SimpleComponent): class SimpleComponentWithDeps(SimpleComponent):
@ -146,18 +149,18 @@ class RenderDependenciesTests(BaseTestCase):
render_dependencies=False, render_dependencies=False,
) )
self.assertEqual(rendered_raw.count("<script"), 1) assert rendered_raw.count("<script") == 1
self.assertEqual(rendered_raw.count("<style"), 0) assert rendered_raw.count("<style") == 0
self.assertEqual(rendered_raw.count("<link"), 1) assert rendered_raw.count("<link") == 1
self.assertEqual(rendered_raw.count("_RENDERED"), 1) assert rendered_raw.count("_RENDERED") == 1
# Dependency manager script # Dependency manager script
self.assertInHTML('<script src="django_components/django_components.min.js"></script>', rendered_raw, count=0) assertInHTML('<script src="django_components/django_components.min.js"></script>', rendered_raw, count=0)
self.assertInHTML("<style>.xyz { color: red; }</style>", rendered_raw, count=0) # Inlined CSS assertInHTML("<style>.xyz { color: red; }</style>", rendered_raw, count=0) # Inlined CSS
self.assertInHTML('<link href="style.css" media="all" rel="stylesheet">', rendered_raw, count=0) # Media.css assertInHTML('<link href="style.css" media="all" rel="stylesheet">', rendered_raw, count=0) # Media.css
self.assertInHTML( assertInHTML(
'<script>console.log("xyz");</script>', '<script>console.log("xyz");</script>',
rendered_raw, rendered_raw,
count=0, count=0,
@ -182,14 +185,14 @@ class RenderDependenciesTests(BaseTestCase):
rendered = response.content.decode() rendered = response.content.decode()
# Dependency manager script # Dependency manager script
self.assertInHTML('<script src="django_components/django_components.min.js"></script>', rendered, count=1) assertInHTML('<script src="django_components/django_components.min.js"></script>', rendered, count=1)
self.assertInHTML("<style>.xyz { color: red; }</style>", rendered, count=1) # Inlined CSS assertInHTML("<style>.xyz { color: red; }</style>", rendered, count=1) # Inlined CSS
self.assertInHTML('<script>console.log("xyz");</script>', rendered, count=1) # Inlined JS assertInHTML('<script>console.log("xyz");</script>', rendered, count=1) # Inlined JS
self.assertEqual(rendered.count('<link href="style.css" media="all" rel="stylesheet">'), 1) # Media.css assert rendered.count('<link href="style.css" media="all" rel="stylesheet">') == 1 # Media.css
self.assertEqual(rendered.count("<link"), 1) assert rendered.count("<link") == 1
self.assertEqual(rendered.count("<style"), 1) assert rendered.count("<style") == 1
def test_inserts_styles_and_script_to_default_places_if_not_overriden(self): def test_inserts_styles_and_script_to_default_places_if_not_overriden(self):
registry.register(name="test", component=SimpleComponent) registry.register(name="test", component=SimpleComponent)
@ -207,12 +210,12 @@ class RenderDependenciesTests(BaseTestCase):
rendered_raw = Template(template_str).render(Context({})) rendered_raw = Template(template_str).render(Context({}))
rendered = render_dependencies(rendered_raw) rendered = render_dependencies(rendered_raw)
self.assertEqual(rendered.count("<script"), 4) assert rendered.count("<script") == 4
self.assertEqual(rendered.count("<style"), 1) assert rendered.count("<style") == 1
self.assertEqual(rendered.count("<link"), 1) assert rendered.count("<link") == 1
self.assertEqual(rendered.count("_RENDERED"), 0) assert rendered.count("_RENDERED") == 0
self.assertInHTML( assertInHTML(
""" """
<head> <head>
<style>.xyz { color: red; }</style> <style>.xyz { color: red; }</style>
@ -226,12 +229,12 @@ class RenderDependenciesTests(BaseTestCase):
body_re = re.compile(r"<body>(.*?)</body>", re.DOTALL) body_re = re.compile(r"<body>(.*?)</body>", re.DOTALL)
rendered_body = body_re.search(rendered).group(1) # type: ignore[union-attr] rendered_body = body_re.search(rendered).group(1) # type: ignore[union-attr]
self.assertInHTML( assertInHTML(
"""<script src="django_components/django_components.min.js">""", """<script src="django_components/django_components.min.js">""",
rendered_body, rendered_body,
count=1, count=1,
) )
self.assertInHTML( assertInHTML(
'<script>console.log("xyz");</script>', '<script>console.log("xyz");</script>',
rendered_body, rendered_body,
count=1, count=1,
@ -256,12 +259,12 @@ class RenderDependenciesTests(BaseTestCase):
rendered_raw = Template(template_str).render(Context({})) rendered_raw = Template(template_str).render(Context({}))
rendered = render_dependencies(rendered_raw) rendered = render_dependencies(rendered_raw)
self.assertEqual(rendered.count("<script"), 4) assert rendered.count("<script") == 4
self.assertEqual(rendered.count("<style"), 1) assert rendered.count("<style") == 1
self.assertEqual(rendered.count("<link"), 1) assert rendered.count("<link") == 1
self.assertEqual(rendered.count("_RENDERED"), 0) assert rendered.count("_RENDERED") == 0
self.assertInHTML( assertInHTML(
""" """
<body> <body>
Variable: <strong data-djc-id-a1bc41>foo</strong> Variable: <strong data-djc-id-a1bc41>foo</strong>
@ -277,12 +280,12 @@ class RenderDependenciesTests(BaseTestCase):
head_re = re.compile(r"<head>(.*?)</head>", re.DOTALL) head_re = re.compile(r"<head>(.*?)</head>", re.DOTALL)
rendered_head = head_re.search(rendered).group(1) # type: ignore[union-attr] rendered_head = head_re.search(rendered).group(1) # type: ignore[union-attr]
self.assertInHTML( assertInHTML(
"""<script src="django_components/django_components.min.js">""", """<script src="django_components/django_components.min.js">""",
rendered_head, rendered_head,
count=1, count=1,
) )
self.assertInHTML( assertInHTML(
'<script>console.log("xyz");</script>', '<script>console.log("xyz");</script>',
rendered_head, rendered_head,
count=1, count=1,
@ -300,7 +303,7 @@ class RenderDependenciesTests(BaseTestCase):
rendered_raw = Template(template_str).render(Context({"formset": [1]})) rendered_raw = Template(template_str).render(Context({"formset": [1]}))
rendered = render_dependencies(rendered_raw, type="fragment") rendered = render_dependencies(rendered_raw, type="fragment")
self.assertHTMLEqual(rendered, "<thead>") assertHTMLEqual(rendered, "<thead>")
def test_does_not_modify_html_when_no_component_used(self): def test_does_not_modify_html_when_no_component_used(self):
registry.register(name="test", component=SimpleComponent) registry.register(name="test", component=SimpleComponent)
@ -358,7 +361,7 @@ class RenderDependenciesTests(BaseTestCase):
</table> </table>
""" """
self.assertHTMLEqual(expected, rendered) assertHTMLEqual(expected, rendered)
# Explanation: The component is used in the template, but the template doesn't use # Explanation: The component is used in the template, but the template doesn't use
# {% component_js_dependencies %} or {% component_css_dependencies %} tags, # {% component_js_dependencies %} or {% component_css_dependencies %} tags,
@ -435,7 +438,7 @@ class RenderDependenciesTests(BaseTestCase):
</script> </script>
""" # noqa: E501 """ # noqa: E501
self.assertHTMLEqual(expected, rendered) assertHTMLEqual(expected, rendered)
def test_raises_if_script_end_tag_inside_component_js(self): def test_raises_if_script_end_tag_inside_component_js(self):
class ComponentWithScript(SimpleComponent): class ComponentWithScript(SimpleComponent):
@ -445,9 +448,9 @@ class RenderDependenciesTests(BaseTestCase):
registry.register(name="test", component=ComponentWithScript) registry.register(name="test", component=ComponentWithScript)
with self.assertRaisesMessage( with pytest.raises(
RuntimeError, RuntimeError,
"Content of `Component.js` for component 'ComponentWithScript' contains '</script>' end tag.", match=re.escape("Content of `Component.js` for component 'ComponentWithScript' contains '</script>' end tag."), # noqa: E501
): ):
ComponentWithScript.render(kwargs={"variable": "foo"}) ComponentWithScript.render(kwargs={"variable": "foo"})
@ -462,19 +465,20 @@ class RenderDependenciesTests(BaseTestCase):
registry.register(name="test", component=ComponentWithScript) registry.register(name="test", component=ComponentWithScript)
with self.assertRaisesMessage( with pytest.raises(
RuntimeError, RuntimeError,
"Content of `Component.css` for component 'ComponentWithScript' contains '</style>' end tag.", match=re.escape("Content of `Component.css` for component 'ComponentWithScript' contains '</style>' end tag."), # noqa: E501
): ):
ComponentWithScript.render(kwargs={"variable": "foo"}) ComponentWithScript.render(kwargs={"variable": "foo"})
class MiddlewareTests(BaseTestCase): @djc_test
class TestMiddleware:
def test_middleware_response_without_content_type(self): def test_middleware_response_without_content_type(self):
response = HttpResponseNotModified() response = HttpResponseNotModified()
middleware = ComponentDependencyMiddleware(get_response=lambda _: response) middleware = ComponentDependencyMiddleware(get_response=lambda _: response)
request = Mock() request = Mock()
self.assertEqual(response, middleware(request=request)) assert 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("dynamic", DynamicComponent)
@ -492,14 +496,14 @@ class MiddlewareTests(BaseTestCase):
def assert_dependencies(content: str): def assert_dependencies(content: str):
# Dependency manager script (empty) # Dependency manager script (empty)
self.assertInHTML('<script src="django_components/django_components.min.js"></script>', content, count=1) assertInHTML('<script src="django_components/django_components.min.js"></script>', content, count=1)
# Inlined JS # Inlined JS
self.assertInHTML('<script>console.log("xyz");</script>', content, count=1) assertInHTML('<script>console.log("xyz");</script>', content, count=1)
# Inlined CSS # Inlined CSS
self.assertInHTML("<style>.xyz { color: red; }</style>", content, count=1) assertInHTML("<style>.xyz { color: red; }</style>", content, count=1)
# Media.css # Media.css
self.assertInHTML('<link href="style.css" media="all" rel="stylesheet">', content, count=1) assertInHTML('<link href="style.css" media="all" rel="stylesheet">', content, count=1)
rendered1 = create_and_process_template_response( rendered1 = create_and_process_template_response(
template, template,
@ -507,10 +511,7 @@ class MiddlewareTests(BaseTestCase):
) )
assert_dependencies(rendered1) assert_dependencies(rendered1)
self.assertEqual( assert rendered1.count('Variable: <strong data-djc-id-a1bc42="" data-djc-id-a1bc41="">value</strong>') == 1
rendered1.count('Variable: <strong data-djc-id-a1bc42="" data-djc-id-a1bc41="">value</strong>'),
1,
)
rendered2 = create_and_process_template_response( rendered2 = create_and_process_template_response(
template, template,
@ -518,10 +519,7 @@ class MiddlewareTests(BaseTestCase):
) )
assert_dependencies(rendered2) assert_dependencies(rendered2)
self.assertEqual( assert rendered2.count('Variable: <strong data-djc-id-a1bc44="" data-djc-id-a1bc43="">value</strong>') == 1
rendered2.count('Variable: <strong data-djc-id-a1bc44="" data-djc-id-a1bc43="">value</strong>'),
1,
)
rendered3 = create_and_process_template_response( rendered3 = create_and_process_template_response(
template, template,
@ -529,7 +527,4 @@ class MiddlewareTests(BaseTestCase):
) )
assert_dependencies(rendered3) assert_dependencies(rendered3)
self.assertEqual( assert rendered3.count('Variable: <strong data-djc-id-a1bc46="" data-djc-id-a1bc45="">value</strong>') == 1
rendered3.count('Variable: <strong data-djc-id-a1bc46="" data-djc-id-a1bc45="">value</strong>'),
1,
)

View file

@ -1,12 +1,13 @@
import re
from typing import List from typing import List
from django.test import override_settings import pytest
from playwright.async_api import Error, Page from playwright.async_api import Browser, Error, Page
from django_components import types from django_components import types
from tests.django_test_setup import setup_test_config from django_components.testing import djc_test
from tests.testutils import setup_test_config
from tests.e2e.utils import TEST_SERVER_URL, with_playwright from tests.e2e.utils import TEST_SERVER_URL, with_playwright
from tests.testutils import BaseTestCase
setup_test_config( setup_test_config(
components={"autodiscover": False}, components={"autodiscover": False},
@ -18,9 +19,8 @@ setup_test_config(
urlpatterns: List = [] urlpatterns: List = []
class _BaseDepManagerTestCase(BaseTestCase): async def _create_page_with_dep_manager(browser: Browser) -> Page:
async def _create_page_with_dep_manager(self) -> Page: page = await browser.new_page()
page = await self.browser.new_page()
# Load the JS library by opening a page with the script, and then running the script code # Load the JS library by opening a page with the script, and then running the script code
# E.g. `http://localhost:54017/static/django_components/django_components.min.js` # E.g. `http://localhost:54017/static/django_components/django_components.min.js`
@ -49,38 +49,43 @@ class _BaseDepManagerTestCase(BaseTestCase):
return page return page
@override_settings(STATIC_URL="static/") @djc_test(
class DependencyManagerTests(_BaseDepManagerTestCase): django_settings={
"STATIC_URL": "static/",
}
)
class TestDependencyManager:
@with_playwright @with_playwright
async def test_script_loads(self): async def test_script_loads(self):
page = await self._create_page_with_dep_manager() page = await _create_page_with_dep_manager(self.browser) # type: ignore[attr-defined]
# Check the exposed API # Check the exposed API
keys = sorted(await page.evaluate("Object.keys(Components)")) keys = sorted(await page.evaluate("Object.keys(Components)"))
self.assertEqual(keys, ["createComponentsManager", "manager", "unescapeJs"]) assert keys == ["createComponentsManager", "manager", "unescapeJs"]
keys = await page.evaluate("Object.keys(Components.manager)") keys = await page.evaluate("Object.keys(Components.manager)")
self.assertEqual( assert keys == [
keys,
[
"callComponent", "callComponent",
"registerComponent", "registerComponent",
"registerComponentData", "registerComponentData",
"loadJs", "loadJs",
"loadCss", "loadCss",
"markScriptLoaded", "markScriptLoaded",
], ]
)
await page.close() await page.close()
# Tests for `manager.loadJs()` / `manager.loadCss()` / `manager.markAsLoaded()` # Tests for `manager.loadJs()` / `manager.loadCss()` / `manager.markAsLoaded()`
@override_settings(STATIC_URL="static/") @djc_test(
class LoadScriptTests(_BaseDepManagerTestCase): django_settings={
"STATIC_URL": "static/",
}
)
class TestLoadScript:
@with_playwright @with_playwright
async def test_load_js_scripts(self): async def test_load_js_scripts(self):
page = await self._create_page_with_dep_manager() page = await _create_page_with_dep_manager(self.browser) # type: ignore[attr-defined]
# JS code that loads a few dependencies, capturing the HTML after each action # JS code that loads a few dependencies, capturing the HTML after each action
test_js: types.js = """() => { test_js: types.js = """() => {
@ -113,20 +118,18 @@ class LoadScriptTests(_BaseDepManagerTestCase):
data = await page.evaluate(test_js) data = await page.evaluate(test_js)
self.assertEqual(data["bodyAfterFirstLoad"], '<script src="/one/two"></script>') assert data["bodyAfterFirstLoad"] == '<script src="/one/two"></script>'
self.assertEqual(data["bodyAfterSecondLoad"], '<script src="/one/two"></script>') assert data["bodyAfterSecondLoad"] == '<script src="/one/two"></script>'
self.assertEqual( assert data["bodyAfterThirdLoad"] == '<script src="/one/two"></script><script src="/four/three"></script>'
data["bodyAfterThirdLoad"], '<script src="/one/two"></script><script src="/four/three"></script>'
)
self.assertEqual(data["headBeforeFirstLoad"], data["headAfterThirdLoad"]) assert data["headBeforeFirstLoad"] == data["headAfterThirdLoad"]
self.assertEqual(data["headBeforeFirstLoad"], "") assert data["headBeforeFirstLoad"] == ""
await page.close() await page.close()
@with_playwright @with_playwright
async def test_load_css_scripts(self): async def test_load_css_scripts(self):
page = await self._create_page_with_dep_manager() page = await _create_page_with_dep_manager(self.browser) # type: ignore[attr-defined]
# JS code that loads a few dependencies, capturing the HTML after each action # JS code that loads a few dependencies, capturing the HTML after each action
test_js: types.js = """() => { test_js: types.js = """() => {
@ -159,18 +162,18 @@ class LoadScriptTests(_BaseDepManagerTestCase):
data = await page.evaluate(test_js) data = await page.evaluate(test_js)
self.assertEqual(data["headAfterFirstLoad"], '<link href="/one/two">') assert data["headAfterFirstLoad"] == '<link href="/one/two">'
self.assertEqual(data["headAfterSecondLoad"], '<link href="/one/two">') assert data["headAfterSecondLoad"] == '<link href="/one/two">'
self.assertEqual(data["headAfterThirdLoad"], '<link href="/one/two"><link href="/four/three">') assert data["headAfterThirdLoad"] == '<link href="/one/two"><link href="/four/three">'
self.assertEqual(data["bodyBeforeFirstLoad"], data["bodyAfterThirdLoad"]) assert data["bodyBeforeFirstLoad"] == data["bodyAfterThirdLoad"]
self.assertEqual(data["bodyBeforeFirstLoad"], "") assert data["bodyBeforeFirstLoad"] == ""
await page.close() await page.close()
@with_playwright @with_playwright
async def test_does_not_load_script_if_marked_as_loaded(self): async def test_does_not_load_script_if_marked_as_loaded(self):
page = await self._create_page_with_dep_manager() page = await _create_page_with_dep_manager(self.browser) # type: ignore[attr-defined]
# JS code that loads a few dependencies, capturing the HTML after each action # JS code that loads a few dependencies, capturing the HTML after each action
test_js: types.js = """() => { test_js: types.js = """() => {
@ -194,18 +197,22 @@ class LoadScriptTests(_BaseDepManagerTestCase):
data = await page.evaluate(test_js) data = await page.evaluate(test_js)
self.assertEqual(data["headAfterFirstLoad"], "") assert data["headAfterFirstLoad"] == ""
self.assertEqual(data["bodyAfterSecondLoad"], "") assert data["bodyAfterSecondLoad"] == ""
await page.close() await page.close()
# Tests for `manager.registerComponent()` / `registerComponentData()` / `callComponent()` # Tests for `manager.registerComponent()` / `registerComponentData()` / `callComponent()`
@override_settings(STATIC_URL="static/") @djc_test(
class CallComponentTests(_BaseDepManagerTestCase): django_settings={
"STATIC_URL": "static/",
}
)
class TestCallComponent:
@with_playwright @with_playwright
async def test_calls_component_successfully(self): async def test_calls_component_successfully(self):
page = await self._create_page_with_dep_manager() page = await _create_page_with_dep_manager(self.browser) # type: ignore[attr-defined]
test_js: types.js = """() => { test_js: types.js = """() => {
const manager = Components.createComponentsManager(); const manager = Components.createComponentsManager();
@ -240,10 +247,8 @@ class CallComponentTests(_BaseDepManagerTestCase):
data = await page.evaluate(test_js) data = await page.evaluate(test_js)
self.assertEqual(data["result"], 123) assert data["result"] == 123
self.assertEqual( assert data["captured"] == {
data["captured"],
{
"data": { "data": {
"hello": "world", "hello": "world",
}, },
@ -252,14 +257,13 @@ class CallComponentTests(_BaseDepManagerTestCase):
"id": "12345", "id": "12345",
"name": "my_comp", "name": "my_comp",
}, },
}, }
)
await page.close() await page.close()
@with_playwright @with_playwright
async def test_calls_component_successfully_async(self): async def test_calls_component_successfully_async(self):
page = await self._create_page_with_dep_manager() page = await _create_page_with_dep_manager(self.browser) # type: ignore[attr-defined]
test_js: types.js = """() => { test_js: types.js = """() => {
const manager = Components.createComponentsManager(); const manager = Components.createComponentsManager();
@ -292,14 +296,14 @@ class CallComponentTests(_BaseDepManagerTestCase):
data = await page.evaluate(test_js) data = await page.evaluate(test_js)
self.assertEqual(data["result"], 123) assert data["result"] == 123
self.assertEqual(data["isPromise"], True) assert data["isPromise"] is True
await page.close() await page.close()
@with_playwright @with_playwright
async def test_error_in_component_call_do_not_propagate_sync(self): async def test_error_in_component_call_do_not_propagate_sync(self):
page = await self._create_page_with_dep_manager() page = await _create_page_with_dep_manager(self.browser) # type: ignore[attr-defined]
test_js: types.js = """() => { test_js: types.js = """() => {
const manager = Components.createComponentsManager(); const manager = Components.createComponentsManager();
@ -327,13 +331,13 @@ class CallComponentTests(_BaseDepManagerTestCase):
data = await page.evaluate(test_js) data = await page.evaluate(test_js)
self.assertEqual(data, None) assert data is None
await page.close() await page.close()
@with_playwright @with_playwright
async def test_error_in_component_call_do_not_propagate_async(self): async def test_error_in_component_call_do_not_propagate_async(self):
page = await self._create_page_with_dep_manager() page = await _create_page_with_dep_manager(self.browser) # type: ignore[attr-defined]
test_js: types.js = """() => { test_js: types.js = """() => {
const manager = Components.createComponentsManager(); const manager = Components.createComponentsManager();
@ -360,16 +364,16 @@ class CallComponentTests(_BaseDepManagerTestCase):
data = await page.evaluate(test_js) data = await page.evaluate(test_js)
self.assertEqual(len(data), 1) assert len(data) == 1
self.assertEqual(data[0]["status"], "rejected") assert data[0]["status"] == "rejected"
self.assertIsInstance(data[0]["reason"], Error) assert isinstance(data[0]["reason"], Error)
self.assertEqual(data[0]["reason"].message, "Oops!") assert data[0]["reason"].message == "Oops!"
await page.close() await page.close()
@with_playwright @with_playwright
async def test_raises_if_component_element_not_in_dom(self): async def test_raises_if_component_element_not_in_dom(self):
page = await self._create_page_with_dep_manager() page = await _create_page_with_dep_manager(self.browser) # type: ignore[attr-defined]
test_js: types.js = """() => { test_js: types.js = """() => {
const manager = Components.createComponentsManager(); const manager = Components.createComponentsManager();
@ -390,8 +394,9 @@ class CallComponentTests(_BaseDepManagerTestCase):
manager.callComponent(compName, compId, inputHash); manager.callComponent(compName, compId, inputHash);
}""" }"""
with self.assertRaisesMessage( with pytest.raises(
Error, "Error: [Components] 'my_comp': No elements with component ID '12345' found" Error,
match=re.escape("Error: [Components] 'my_comp': No elements with component ID '12345' found"),
): ):
await page.evaluate(test_js) await page.evaluate(test_js)
@ -399,7 +404,7 @@ class CallComponentTests(_BaseDepManagerTestCase):
@with_playwright @with_playwright
async def test_raises_if_input_hash_not_registered(self): async def test_raises_if_input_hash_not_registered(self):
page = await self._create_page_with_dep_manager() page = await _create_page_with_dep_manager(self.browser) # type: ignore[attr-defined]
test_js: types.js = """() => { test_js: types.js = """() => {
const manager = Components.createComponentsManager(); const manager = Components.createComponentsManager();
@ -418,14 +423,17 @@ class CallComponentTests(_BaseDepManagerTestCase):
manager.callComponent(compName, compId, inputHash); manager.callComponent(compName, compId, inputHash);
}""" }"""
with self.assertRaisesMessage(Error, "Error: [Components] 'my_comp': Cannot find input for hash 'input-abc'"): with pytest.raises(
Error,
match=re.escape("Error: [Components] 'my_comp': Cannot find input for hash 'input-abc'"),
):
await page.evaluate(test_js) await page.evaluate(test_js)
await page.close() await page.close()
@with_playwright @with_playwright
async def test_raises_if_component_not_registered(self): async def test_raises_if_component_not_registered(self):
page = await self._create_page_with_dep_manager() page = await _create_page_with_dep_manager(self.browser) # type: ignore[attr-defined]
test_js: types.js = """() => { test_js: types.js = """() => {
const manager = Components.createComponentsManager(); const manager = Components.createComponentsManager();
@ -444,7 +452,10 @@ class CallComponentTests(_BaseDepManagerTestCase):
manager.callComponent(compName, compId, inputHash); manager.callComponent(compName, compId, inputHash);
}""" }"""
with self.assertRaisesMessage(Error, "Error: [Components] 'my_comp': No component registered for that name"): with pytest.raises(
Error,
match=re.escape("Error: [Components] 'my_comp': No component registered for that name"),
):
await page.evaluate(test_js) await page.evaluate(test_js)
await page.close() await page.close()

View file

@ -6,11 +6,12 @@ During actual rendering, the HTML is then picked up by the JS-side dependency ma
import re import re
from django.template import Context, Template from django.template import Context, Template
from pytest_django.asserts import assertHTMLEqual, assertInHTML
from django_components import Component, registry, types from django_components import Component, registry, types
from django_components.testing import djc_test
from .django_test_setup import setup_test_config from .testutils import create_and_process_template_response, setup_test_config
from .testutils import BaseTestCase, create_and_process_template_response
setup_test_config({"autodiscover": False}) setup_test_config({"autodiscover": False})
@ -108,7 +109,8 @@ class MultistyleComponent(Component):
js = ["script.js", "script2.js"] js = ["script.js", "script2.js"]
class DependencyRenderingTests(BaseTestCase): @djc_test
class TestDependencyRendering:
def test_no_dependencies_when_no_components_used(self): def test_no_dependencies_when_no_components_used(self):
registry.register(name="test", component=SimpleComponent) registry.register(name="test", component=SimpleComponent)
@ -121,16 +123,16 @@ class DependencyRenderingTests(BaseTestCase):
rendered = create_and_process_template_response(template) rendered = create_and_process_template_response(template)
# Dependency manager script # Dependency manager script
self.assertInHTML('<script src="django_components/django_components.min.js"></script>', rendered, count=1) assertInHTML('<script src="django_components/django_components.min.js"></script>', rendered, count=1)
self.assertEqual(rendered.count("<script"), 1) # 1 boilerplate script assert rendered.count("<script") == 1 # 1 boilerplate script
self.assertEqual(rendered.count("<link"), 0) # No CSS assert rendered.count("<link") == 0 # No CSS
self.assertEqual(rendered.count("<style"), 0) assert rendered.count("<style") == 0
self.assertNotIn("loadedJsUrls", rendered) assert "loadedJsUrls" not in rendered
self.assertNotIn("loadedCssUrls", rendered) assert "loadedCssUrls" not in rendered
self.assertNotIn("toLoadJsTags", rendered) assert "toLoadJsTags" not in rendered
self.assertNotIn("toLoadCssTags", rendered) assert "toLoadCssTags" not in rendered
def test_no_js_dependencies_when_no_components_used(self): def test_no_js_dependencies_when_no_components_used(self):
registry.register(name="test", component=SimpleComponent) registry.register(name="test", component=SimpleComponent)
@ -142,16 +144,16 @@ class DependencyRenderingTests(BaseTestCase):
rendered = create_and_process_template_response(template) rendered = create_and_process_template_response(template)
# Dependency manager script # Dependency manager script
self.assertInHTML('<script src="django_components/django_components.min.js"></script>', rendered, count=1) assertInHTML('<script src="django_components/django_components.min.js"></script>', rendered, count=1)
self.assertEqual(rendered.count("<script"), 1) # 1 boilerplate script assert rendered.count("<script") == 1 # 1 boilerplate script
self.assertEqual(rendered.count("<link"), 0) # No CSS assert rendered.count("<link") == 0 # No CSS
self.assertEqual(rendered.count("<style"), 0) assert rendered.count("<style") == 0
self.assertNotIn("loadedJsUrls", rendered) assert "loadedJsUrls" not in rendered
self.assertNotIn("loadedCssUrls", rendered) assert "loadedCssUrls" not in rendered
self.assertNotIn("toLoadJsTags", rendered) assert "toLoadJsTags" not in rendered
self.assertNotIn("toLoadCssTags", rendered) assert "toLoadCssTags" not in rendered
def test_no_css_dependencies_when_no_components_used(self): def test_no_css_dependencies_when_no_components_used(self):
registry.register(name="test", component=SimpleComponent) registry.register(name="test", component=SimpleComponent)
@ -162,9 +164,9 @@ class DependencyRenderingTests(BaseTestCase):
template = Template(template_str) template = Template(template_str)
rendered = create_and_process_template_response(template) rendered = create_and_process_template_response(template)
self.assertEqual(rendered.count("<script"), 0) # No JS assert rendered.count("<script") == 0 # No JS
self.assertEqual(rendered.count("<link"), 0) # No CSS assert rendered.count("<link") == 0 # No CSS
self.assertEqual(rendered.count("<style"), 0) assert rendered.count("<style") == 0
def test_single_component_dependencies(self): def test_single_component_dependencies(self):
registry.register(name="test", component=SimpleComponent) registry.register(name="test", component=SimpleComponent)
@ -179,16 +181,16 @@ class DependencyRenderingTests(BaseTestCase):
rendered = create_and_process_template_response(template) rendered = create_and_process_template_response(template)
# Dependency manager script # Dependency manager script
self.assertInHTML('<script src="django_components/django_components.min.js"></script>', rendered, count=1) assertInHTML('<script src="django_components/django_components.min.js"></script>', rendered, count=1)
self.assertEqual(rendered.count('<link href="style.css" media="all" rel="stylesheet">'), 1) # Media.css assert rendered.count('<link href="style.css" media="all" rel="stylesheet">') == 1 # Media.css
self.assertEqual(rendered.count("<link"), 1) assert rendered.count("<link") == 1
self.assertEqual(rendered.count("<style"), 0) assert rendered.count("<style") == 0
self.assertEqual(rendered.count("<script"), 3) assert rendered.count("<script") == 3
# `c3R5bGUuY3Nz` is base64 encoded `style.css` # `c3R5bGUuY3Nz` is base64 encoded `style.css`
# `c2NyaXB0Lmpz` is base64 encoded `style.js` # `c2NyaXB0Lmpz` is base64 encoded `style.js`
self.assertInHTML( assertInHTML(
""" """
<script type="application/json" data-djc> <script type="application/json" data-djc>
{"loadedCssUrls": ["c3R5bGUuY3Nz"], {"loadedCssUrls": ["c3R5bGUuY3Nz"],
@ -214,16 +216,16 @@ class DependencyRenderingTests(BaseTestCase):
rendered = create_and_process_template_response(template) rendered = create_and_process_template_response(template)
# Dependency manager script # Dependency manager script
self.assertInHTML('<script src="django_components/django_components.min.js"></script>', rendered, count=1) assertInHTML('<script src="django_components/django_components.min.js"></script>', rendered, count=1)
self.assertEqual(rendered.count('<link href="style.css" media="all" rel="stylesheet">'), 1) # Media.css assert rendered.count('<link href="style.css" media="all" rel="stylesheet">') == 1 # Media.css
self.assertEqual(rendered.count("<link"), 1) assert rendered.count("<link") == 1
self.assertEqual(rendered.count("<style"), 0) assert rendered.count("<style") == 0
self.assertEqual(rendered.count("<script"), 3) assert rendered.count("<script") == 3
# `c3R5bGUuY3Nz` is base64 encoded `style.css` # `c3R5bGUuY3Nz` is base64 encoded `style.css`
# `c2NyaXB0Lmpz` is base64 encoded `style.js` # `c2NyaXB0Lmpz` is base64 encoded `style.js`
self.assertInHTML( assertInHTML(
""" """
<script type="application/json" data-djc> <script type="application/json" data-djc>
{"loadedCssUrls": ["c3R5bGUuY3Nz"], {"loadedCssUrls": ["c3R5bGUuY3Nz"],
@ -248,7 +250,7 @@ class DependencyRenderingTests(BaseTestCase):
template = Template(template_str) template = Template(template_str)
rendered = create_and_process_template_response(template) rendered = create_and_process_template_response(template)
self.assertNotIn("_RENDERED", rendered) assert "_RENDERED" not in rendered
def test_single_component_css_dependencies(self): def test_single_component_css_dependencies(self):
registry.register(name="test", component=SimpleComponent) registry.register(name="test", component=SimpleComponent)
@ -261,13 +263,13 @@ class DependencyRenderingTests(BaseTestCase):
rendered = create_and_process_template_response(template) rendered = create_and_process_template_response(template)
# Dependency manager script - NOT present # Dependency manager script - NOT present
self.assertInHTML('<script src="django_components/django_components.min.js"></script>', rendered, count=0) assertInHTML('<script src="django_components/django_components.min.js"></script>', rendered, count=0)
self.assertEqual(rendered.count("<link"), 1) assert rendered.count("<link") == 1
self.assertEqual(rendered.count("<style"), 0) assert rendered.count("<style") == 0
self.assertEqual(rendered.count("<script"), 0) # No JS scripts assert rendered.count("<script") == 0 # No JS scripts
self.assertEqual(rendered.count('<link href="style.css" media="all" rel="stylesheet">'), 1) # Media.css assert rendered.count('<link href="style.css" media="all" rel="stylesheet">') == 1 # Media.css
def test_single_component_js_dependencies(self): def test_single_component_js_dependencies(self):
registry.register(name="test", component=SimpleComponent) registry.register(name="test", component=SimpleComponent)
@ -280,16 +282,16 @@ class DependencyRenderingTests(BaseTestCase):
rendered = create_and_process_template_response(template) rendered = create_and_process_template_response(template)
# Dependency manager script # Dependency manager script
self.assertInHTML('<script src="django_components/django_components.min.js"></script>', rendered, count=1) assertInHTML('<script src="django_components/django_components.min.js"></script>', rendered, count=1)
# CSS NOT included # CSS NOT included
self.assertEqual(rendered.count("<link"), 0) assert rendered.count("<link") == 0
self.assertEqual(rendered.count("<style"), 0) assert rendered.count("<style") == 0
self.assertEqual(rendered.count("<script"), 3) assert rendered.count("<script") == 3
# `c3R5bGUuY3Nz` is base64 encoded `style.css` # `c3R5bGUuY3Nz` is base64 encoded `style.css`
# `c2NyaXB0Lmpz` is base64 encoded `style.js` # `c2NyaXB0Lmpz` is base64 encoded `style.js`
self.assertInHTML( assertInHTML(
""" """
<script type="application/json" data-djc> <script type="application/json" data-djc>
{"loadedCssUrls": ["c3R5bGUuY3Nz"], {"loadedCssUrls": ["c3R5bGUuY3Nz"],
@ -316,14 +318,14 @@ class DependencyRenderingTests(BaseTestCase):
rendered = create_and_process_template_response(template) rendered = create_and_process_template_response(template)
# Dependency manager script # Dependency manager script
self.assertInHTML('<script src="django_components/django_components.min.js"></script>', rendered, count=1) assertInHTML('<script src="django_components/django_components.min.js"></script>', rendered, count=1)
self.assertEqual(rendered.count("<link"), 2) assert rendered.count("<link") == 2
self.assertEqual(rendered.count("<style"), 0) assert rendered.count("<style") == 0
self.assertEqual(rendered.count("<script"), 4) # 2 scripts belong to the boilerplate assert rendered.count("<script") == 4 # 2 scripts belong to the boilerplate
# Media.css # Media.css
self.assertInHTML( assertInHTML(
""" """
<link href="style.css" media="all" rel="stylesheet"> <link href="style.css" media="all" rel="stylesheet">
<link href="style2.css" media="all" rel="stylesheet"> <link href="style2.css" media="all" rel="stylesheet">
@ -333,7 +335,7 @@ class DependencyRenderingTests(BaseTestCase):
) )
# Media.js # Media.js
self.assertInHTML( assertInHTML(
""" """
<script src="script.js"></script> <script src="script.js"></script>
<script src="script2.js"></script> <script src="script2.js"></script>
@ -347,7 +349,7 @@ class DependencyRenderingTests(BaseTestCase):
# `c3R5bGUyLmNzcw==` -> `style2.css` # `c3R5bGUyLmNzcw==` -> `style2.css`
# `c2NyaXB0Lmpz` -> `script.js` # `c2NyaXB0Lmpz` -> `script.js`
# `c2NyaXB0Mi5qcw==` -> `script2.js` # `c2NyaXB0Mi5qcw==` -> `script2.js`
self.assertInHTML( assertInHTML(
""" """
<script type="application/json" data-djc> <script type="application/json" data-djc>
{"loadedCssUrls": ["c3R5bGUuY3Nz", "c3R5bGUyLmNzcw=="], {"loadedCssUrls": ["c3R5bGUuY3Nz", "c3R5bGUyLmNzcw=="],
@ -373,16 +375,16 @@ class DependencyRenderingTests(BaseTestCase):
rendered = create_and_process_template_response(template) rendered = create_and_process_template_response(template)
# Dependency manager script # Dependency manager script
self.assertInHTML('<script src="django_components/django_components.min.js"></script>', rendered, count=1) assertInHTML('<script src="django_components/django_components.min.js"></script>', rendered, count=1)
self.assertEqual(rendered.count("<script"), 1) # 1 boilerplate script assert rendered.count("<script") == 1 # 1 boilerplate script
self.assertEqual(rendered.count("<link"), 0) # No CSS assert rendered.count("<link") == 0 # No CSS
self.assertEqual(rendered.count("<style"), 0) assert rendered.count("<style") == 0
self.assertNotIn("loadedJsUrls", rendered) assert "loadedJsUrls" not in rendered
self.assertNotIn("loadedCssUrls", rendered) assert "loadedCssUrls" not in rendered
self.assertNotIn("toLoadJsTags", rendered) assert "toLoadJsTags" not in rendered
self.assertNotIn("toLoadCssTags", rendered) assert "toLoadCssTags" not in rendered
def test_multiple_components_dependencies(self): def test_multiple_components_dependencies(self):
registry.register(name="inner", component=SimpleComponent) registry.register(name="inner", component=SimpleComponent)
@ -402,15 +404,15 @@ class DependencyRenderingTests(BaseTestCase):
# Dependency manager script # Dependency manager script
# NOTE: Should be present only ONCE! # NOTE: Should be present only ONCE!
self.assertInHTML('<script src="django_components/django_components.min.js"></script>', rendered, count=1) assertInHTML('<script src="django_components/django_components.min.js"></script>', rendered, count=1)
self.assertEqual(rendered.count("<script"), 7) # 2 scripts belong to the boilerplate assert rendered.count("<script") == 7 # 2 scripts belong to the boilerplate
self.assertEqual(rendered.count("<link"), 3) assert rendered.count("<link") == 3
self.assertEqual(rendered.count("<style"), 2) assert rendered.count("<style") == 2
# Components' inlined CSS # Components' inlined CSS
# NOTE: Each of these should be present only ONCE! # NOTE: Each of these should be present only ONCE!
self.assertInHTML( assertInHTML(
""" """
<style>.my-class { color: red; }</style> <style>.my-class { color: red; }</style>
<style>.xyz { color: red; }</style> <style>.xyz { color: red; }</style>
@ -424,7 +426,7 @@ class DependencyRenderingTests(BaseTestCase):
# - "style.css", "style2.css" (from SimpleComponentNested) # - "style.css", "style2.css" (from SimpleComponentNested)
# - "style.css" (from SimpleComponent inside SimpleComponentNested) # - "style.css" (from SimpleComponent inside SimpleComponentNested)
# - "xyz1.css" (from OtherComponent inserted into SimpleComponentNested) # - "xyz1.css" (from OtherComponent inserted into SimpleComponentNested)
self.assertInHTML( assertInHTML(
""" """
<link href="style.css" media="all" rel="stylesheet"> <link href="style.css" media="all" rel="stylesheet">
<link href="style2.css" media="all" rel="stylesheet"> <link href="style2.css" media="all" rel="stylesheet">
@ -439,7 +441,7 @@ class DependencyRenderingTests(BaseTestCase):
# - "script2.js" (from SimpleComponentNested) # - "script2.js" (from SimpleComponentNested)
# - "script.js" (from SimpleComponent inside SimpleComponentNested) # - "script.js" (from SimpleComponent inside SimpleComponentNested)
# - "xyz1.js" (from OtherComponent inserted into SimpleComponentNested) # - "xyz1.js" (from OtherComponent inserted into SimpleComponentNested)
self.assertInHTML( assertInHTML(
""" """
<script src="script2.js"></script> <script src="script2.js"></script>
<script src="script.js"></script> <script src="script.js"></script>
@ -462,7 +464,7 @@ class DependencyRenderingTests(BaseTestCase):
# `c2NyaXB0Lmpz` -> `script.js` # `c2NyaXB0Lmpz` -> `script.js`
# `c2NyaXB0Mi5qcw==` -> `script2.js` # `c2NyaXB0Mi5qcw==` -> `script2.js`
# `eHl6MS5qcw==` -> `xyz1.js` # `eHl6MS5qcw==` -> `xyz1.js`
self.assertInHTML( assertInHTML(
""" """
<script type="application/json" data-djc> <script type="application/json" data-djc>
{"loadedCssUrls": ["L2NvbXBvbmVudHMvY2FjaGUvT3RoZXJDb21wb25lbnRfNjMyOWFlLmNzcw==", {"loadedCssUrls": ["L2NvbXBvbmVudHMvY2FjaGUvT3RoZXJDb21wb25lbnRfNjMyOWFlLmNzcw==",
@ -498,7 +500,7 @@ class DependencyRenderingTests(BaseTestCase):
""" """
template = Template(template_str) template = Template(template_str)
rendered = create_and_process_template_response(template) rendered = create_and_process_template_response(template)
self.assertNotIn("_RENDERED", rendered) assert "_RENDERED" not in rendered
def test_adds_component_id_html_attr_single(self): def test_adds_component_id_html_attr_single(self):
registry.register(name="test", component=SimpleComponent) registry.register(name="test", component=SimpleComponent)
@ -510,7 +512,7 @@ class DependencyRenderingTests(BaseTestCase):
template = Template(template_str) template = Template(template_str)
rendered = create_and_process_template_response(template) rendered = create_and_process_template_response(template)
self.assertHTMLEqual(rendered, "Variable: <strong data-djc-id-a1bc3f>foo</strong>") assertHTMLEqual(rendered, "Variable: <strong data-djc-id-a1bc3f>foo</strong>")
def test_adds_component_id_html_attr_single_multiroot(self): def test_adds_component_id_html_attr_single_multiroot(self):
class SimpleMultiroot(SimpleComponent): class SimpleMultiroot(SimpleComponent):
@ -529,7 +531,7 @@ class DependencyRenderingTests(BaseTestCase):
template = Template(template_str) template = Template(template_str)
rendered = create_and_process_template_response(template) rendered = create_and_process_template_response(template)
self.assertHTMLEqual( assertHTMLEqual(
rendered, rendered,
""" """
Variable: <strong data-djc-id-a1bc3f>foo</strong> Variable: <strong data-djc-id-a1bc3f>foo</strong>
@ -565,7 +567,7 @@ class DependencyRenderingTests(BaseTestCase):
template = Template(template_str) template = Template(template_str)
rendered = create_and_process_template_response(template) rendered = create_and_process_template_response(template)
self.assertHTMLEqual( assertHTMLEqual(
rendered, rendered,
""" """
Variable: <strong data-djc-id-a1bc3f data-djc-id-a1bc41>foo</strong> Variable: <strong data-djc-id-a1bc3f data-djc-id-a1bc41>foo</strong>
@ -608,7 +610,7 @@ class DependencyRenderingTests(BaseTestCase):
context=Context({"lst": range(3)}), context=Context({"lst": range(3)}),
) )
self.assertHTMLEqual( assertHTMLEqual(
rendered, rendered,
""" """
Variable: <strong data-djc-id-a1bc3f data-djc-id-a1bc41>foo</strong> Variable: <strong data-djc-id-a1bc3f data-djc-id-a1bc41>foo</strong>

View file

@ -6,23 +6,25 @@ in an actual browser.
import re import re
from playwright.async_api import Page from playwright.async_api import Page
from pytest_django.asserts import assertHTMLEqual, assertInHTML
from django_components import types from django_components import types
from tests.django_test_setup import setup_test_config from django_components.testing import djc_test
from tests.testutils import setup_test_config
from tests.e2e.utils import TEST_SERVER_URL, with_playwright from tests.e2e.utils import TEST_SERVER_URL, with_playwright
from tests.testutils import BaseTestCase
setup_test_config({"autodiscover": False}) setup_test_config({"autodiscover": False})
# NOTE: All views, components, and associated JS and CSS are defined in # NOTE: All views, components, and associated JS and CSS are defined in
# `tests/e2e/testserver/testserver` # `tests/e2e/testserver/testserver`
class E2eDependencyRenderingTests(BaseTestCase): @djc_test
class TestE2eDependencyRendering:
@with_playwright @with_playwright
async def test_single_component_dependencies(self): async def test_single_component_dependencies(self):
single_comp_url = TEST_SERVER_URL + "/single" single_comp_url = TEST_SERVER_URL + "/single"
page: Page = await self.browser.new_page() page: Page = await self.browser.new_page() # type: ignore[attr-defined]
await page.goto(single_comp_url) await page.goto(single_comp_url)
test_js: types.js = """() => { test_js: types.js = """() => {
@ -46,24 +48,23 @@ class E2eDependencyRenderingTests(BaseTestCase):
data = await page.evaluate(test_js) data = await page.evaluate(test_js)
# Check that the actual HTML content was loaded # Check that the actual HTML content was loaded
self.assertRegex( assert re.compile(
data["bodyHTML"], r'Variable: <strong class="inner" data-djc-id-\w{6}="">foo</strong>'
re.compile(r'Variable: <strong class="inner" data-djc-id-\w{6}="">foo</strong>'), ).search(data["bodyHTML"]) is not None
) assertInHTML('<div class="my-style"> 123 </div>', data["bodyHTML"], count=1)
self.assertInHTML('<div class="my-style"> 123 </div>', data["bodyHTML"], count=1) assertInHTML('<div class="my-style2"> xyz </div>', data["bodyHTML"], count=1)
self.assertInHTML('<div class="my-style2"> xyz </div>', data["bodyHTML"], count=1)
# Check components' inlined JS got loaded # Check components' inlined JS got loaded
self.assertEqual(data["componentJsMsg"], "kapowww!") assert data["componentJsMsg"] == "kapowww!"
# Check JS from Media.js got loaded # Check JS from Media.js got loaded
self.assertEqual(data["scriptJsMsg"], {"hello": "world"}) assert data["scriptJsMsg"] == {"hello": "world"}
# Check components' inlined CSS got loaded # Check components' inlined CSS got loaded
self.assertEqual(data["innerFontSize"], "4px") assert data["innerFontSize"] == "4px"
# Check CSS from Media.css got loaded # Check CSS from Media.css got loaded
self.assertIn("rgb(0, 0, 255)", data["myStyleBg"]) # AKA 'background: blue' assert "rgb(0, 0, 255)" in data["myStyleBg"] # AKA 'background: blue'
await page.close() await page.close()
@ -71,7 +72,7 @@ class E2eDependencyRenderingTests(BaseTestCase):
async def test_multiple_component_dependencies(self): async def test_multiple_component_dependencies(self):
single_comp_url = TEST_SERVER_URL + "/multi" single_comp_url = TEST_SERVER_URL + "/multi"
page: Page = await self.browser.new_page() page: Page = await self.browser.new_page() # type: ignore[attr-defined]
await page.goto(single_comp_url) await page.goto(single_comp_url)
test_js: types.js = """() => { test_js: types.js = """() => {
@ -111,8 +112,7 @@ class E2eDependencyRenderingTests(BaseTestCase):
data = await page.evaluate(test_js) data = await page.evaluate(test_js)
# Check that the actual HTML content was loaded # Check that the actual HTML content was loaded
self.assertRegex( assert re.compile(
data["bodyHTML"],
# <div class="outer" data-djc-id-10uLMD> # <div class="outer" data-djc-id-10uLMD>
# Variable: # Variable:
# <strong class="inner" data-djc-id-DZEnUC> # <strong class="inner" data-djc-id-DZEnUC>
@ -125,7 +125,6 @@ class E2eDependencyRenderingTests(BaseTestCase):
# </div> # </div>
# <div class="my-style">123</div> # <div class="my-style">123</div>
# <div class="my-style2">xyz</div> # <div class="my-style2">xyz</div>
re.compile(
r'<div class="outer" data-djc-id-\w{6}="">\s*' r'<div class="outer" data-djc-id-\w{6}="">\s*'
r"Variable:\s*" r"Variable:\s*"
r'<strong class="inner" data-djc-id-\w{6}="">\s*' r'<strong class="inner" data-djc-id-\w{6}="">\s*'
@ -138,26 +137,25 @@ class E2eDependencyRenderingTests(BaseTestCase):
r"<\/div>\s*" r"<\/div>\s*"
r'<div class="my-style">123<\/div>\s*' r'<div class="my-style">123<\/div>\s*'
r'<div class="my-style2">xyz<\/div>\s*' r'<div class="my-style2">xyz<\/div>\s*'
), ).search(data["bodyHTML"]) is not None
)
# Check components' inlined JS got loaded # Check components' inlined JS got loaded
self.assertEqual(data["component1JsMsg"], "kapowww!") assert data["component1JsMsg"] == "kapowww!"
self.assertEqual(data["component2JsMsg"], "bongo!") assert data["component2JsMsg"] == "bongo!"
self.assertEqual(data["component3JsMsg"], "wowzee!") assert data["component3JsMsg"] == "wowzee!"
# Check JS from Media.js got loaded # Check JS from Media.js got loaded
self.assertEqual(data["scriptJs1Msg"], {"hello": "world"}) assert data["scriptJs1Msg"] == {"hello": "world"}
self.assertEqual(data["scriptJs2Msg"], {"hello2": "world2"}) assert data["scriptJs2Msg"] == {"hello2": "world2"}
# Check components' inlined CSS got loaded # Check components' inlined CSS got loaded
self.assertEqual(data["innerFontSize"], "4px") assert data["innerFontSize"] == "4px"
self.assertEqual(data["outerFontSize"], "40px") assert data["outerFontSize"] == "40px"
self.assertEqual(data["otherDisplay"], "flex") assert data["otherDisplay"] == "flex"
# Check CSS from Media.css got loaded # Check CSS from Media.css got loaded
self.assertIn("rgb(0, 0, 255)", data["myStyleBg"]) # AKA 'background: blue' assert "rgb(0, 0, 255)" in data["myStyleBg"] # AKA 'background: blue'
self.assertEqual("rgb(255, 0, 0)", data["myStyle2Color"]) # AKA 'color: red' assert data["myStyle2Color"] == "rgb(255, 0, 0)" # AKA 'color: red'
await page.close() await page.close()
@ -165,7 +163,7 @@ class E2eDependencyRenderingTests(BaseTestCase):
async def test_renders_css_nojs_env(self): async def test_renders_css_nojs_env(self):
single_comp_url = TEST_SERVER_URL + "/multi" single_comp_url = TEST_SERVER_URL + "/multi"
page: Page = await self.browser.new_page(java_script_enabled=False) page: Page = await self.browser.new_page(java_script_enabled=False) # type: ignore[attr-defined]
await page.goto(single_comp_url) await page.goto(single_comp_url)
test_js: types.js = """() => { test_js: types.js = """() => {
@ -205,8 +203,7 @@ class E2eDependencyRenderingTests(BaseTestCase):
data = await page.evaluate(test_js) data = await page.evaluate(test_js)
# Check that the actual HTML content was loaded # Check that the actual HTML content was loaded
self.assertRegex( #
data["bodyHTML"],
# <div class="outer" data-djc-id-10uLMD> # <div class="outer" data-djc-id-10uLMD>
# Variable: # Variable:
# <strong class="inner" data-djc-id-DZEnUC> # <strong class="inner" data-djc-id-DZEnUC>
@ -219,7 +216,7 @@ class E2eDependencyRenderingTests(BaseTestCase):
# </div> # </div>
# <div class="my-style">123</div> # <div class="my-style">123</div>
# <div class="my-style2">xyz</div> # <div class="my-style2">xyz</div>
re.compile( assert re.compile(
r'<div class="outer" data-djc-id-\w{6}="">\s*' r'<div class="outer" data-djc-id-\w{6}="">\s*'
r"Variable:\s*" r"Variable:\s*"
r'<strong class="inner" data-djc-id-\w{6}="">\s*' r'<strong class="inner" data-djc-id-\w{6}="">\s*'
@ -232,26 +229,25 @@ class E2eDependencyRenderingTests(BaseTestCase):
r"<\/div>\s*" r"<\/div>\s*"
r'<div class="my-style">123<\/div>\s*' r'<div class="my-style">123<\/div>\s*'
r'<div class="my-style2">xyz<\/div>\s*' r'<div class="my-style2">xyz<\/div>\s*'
), ).search(data["bodyHTML"]) is not None
)
# Check components' inlined JS did NOT get loaded # Check components' inlined JS did NOT get loaded
self.assertEqual(data["component1JsMsg"], None) assert data["component1JsMsg"] is None
self.assertEqual(data["component2JsMsg"], None) assert data["component2JsMsg"] is None
self.assertEqual(data["component3JsMsg"], None) assert data["component3JsMsg"] is None
# Check JS from Media.js did NOT get loaded # Check JS from Media.js did NOT get loaded
self.assertEqual(data["scriptJs1Msg"], None) assert data["scriptJs1Msg"] is None
self.assertEqual(data["scriptJs2Msg"], None) assert data["scriptJs2Msg"] is None
# Check components' inlined CSS got loaded # Check components' inlined CSS got loaded
self.assertEqual(data["innerFontSize"], "4px") assert data["innerFontSize"] == "4px"
self.assertEqual(data["outerFontSize"], "40px") assert data["outerFontSize"] == "40px"
self.assertEqual(data["otherDisplay"], "flex") assert data["otherDisplay"] == "flex"
# Check CSS from Media.css got loaded # Check CSS from Media.css got loaded
self.assertIn("rgb(0, 0, 255)", data["myStyleBg"]) # AKA 'background: blue' assert "rgb(0, 0, 255)" in data["myStyleBg"] # AKA 'background: blue'
self.assertEqual("rgb(255, 0, 0)", data["myStyle2Color"]) # AKA 'color: red' assert "rgb(255, 0, 0)" in data["myStyle2Color"] # AKA 'color: red'
await page.close() await page.close()
@ -259,7 +255,7 @@ class E2eDependencyRenderingTests(BaseTestCase):
async def test_js_executed_in_order__js(self): async def test_js_executed_in_order__js(self):
single_comp_url = TEST_SERVER_URL + "/js-order/js" single_comp_url = TEST_SERVER_URL + "/js-order/js"
page: Page = await self.browser.new_page() page: Page = await self.browser.new_page() # type: ignore[attr-defined]
await page.goto(single_comp_url) await page.goto(single_comp_url)
test_js: types.js = """() => { test_js: types.js = """() => {
@ -271,13 +267,13 @@ class E2eDependencyRenderingTests(BaseTestCase):
data = await page.evaluate(test_js) data = await page.evaluate(test_js)
# Check components' inlined JS got loaded # Check components' inlined JS got loaded
self.assertEqual(data["testSimpleComponent"], "kapowww!") assert data["testSimpleComponent"] == "kapowww!"
self.assertEqual(data["testSimpleComponentNested"], "bongo!") assert data["testSimpleComponentNested"] == "bongo!"
self.assertEqual(data["testOtherComponent"], "wowzee!") assert data["testOtherComponent"] == "wowzee!"
# Check JS from Media.js got loaded # Check JS from Media.js got loaded
self.assertEqual(data["testMsg"], {"hello": "world"}) assert data["testMsg"] == {"hello": "world"}
self.assertEqual(data["testMsg2"], {"hello2": "world2"}) assert data["testMsg2"] == {"hello2": "world2"}
await page.close() await page.close()
@ -285,7 +281,7 @@ class E2eDependencyRenderingTests(BaseTestCase):
async def test_js_executed_in_order__media(self): async def test_js_executed_in_order__media(self):
single_comp_url = TEST_SERVER_URL + "/js-order/media" single_comp_url = TEST_SERVER_URL + "/js-order/media"
page: Page = await self.browser.new_page() page: Page = await self.browser.new_page() # type: ignore[attr-defined]
await page.goto(single_comp_url) await page.goto(single_comp_url)
test_js: types.js = """() => { test_js: types.js = """() => {
@ -298,13 +294,13 @@ class E2eDependencyRenderingTests(BaseTestCase):
# Check components' inlined JS got loaded # Check components' inlined JS got loaded
# NOTE: The Media JS are loaded BEFORE the components' JS, so they should be empty # NOTE: The Media JS are loaded BEFORE the components' JS, so they should be empty
self.assertEqual(data["testSimpleComponent"], None) assert data["testSimpleComponent"] is None
self.assertEqual(data["testSimpleComponentNested"], None) assert data["testSimpleComponentNested"] is None
self.assertEqual(data["testOtherComponent"], None) assert data["testOtherComponent"] is None
# Check JS from Media.js # Check JS from Media.js
self.assertEqual(data["testMsg"], {"hello": "world"}) assert data["testMsg"] == {"hello": "world"}
self.assertEqual(data["testMsg2"], {"hello2": "world2"}) assert data["testMsg2"] == {"hello2": "world2"}
await page.close() await page.close()
@ -315,7 +311,7 @@ class E2eDependencyRenderingTests(BaseTestCase):
async def test_js_executed_in_order__invalid(self): async def test_js_executed_in_order__invalid(self):
single_comp_url = TEST_SERVER_URL + "/js-order/invalid" single_comp_url = TEST_SERVER_URL + "/js-order/invalid"
page: Page = await self.browser.new_page() page: Page = await self.browser.new_page() # type: ignore[attr-defined]
await page.goto(single_comp_url) await page.goto(single_comp_url)
test_js: types.js = """() => { test_js: types.js = """() => {
@ -326,20 +322,20 @@ class E2eDependencyRenderingTests(BaseTestCase):
data = await page.evaluate(test_js) data = await page.evaluate(test_js)
# Check components' inlined JS got loaded # Check components' inlined JS got loaded
self.assertEqual(data["testSimpleComponent"], None) assert data["testSimpleComponent"] is None
self.assertEqual(data["testSimpleComponentNested"], None) assert data["testSimpleComponentNested"] is None
self.assertEqual(data["testOtherComponent"], None) assert data["testOtherComponent"] is None
# Check JS from Media.js got loaded # Check JS from Media.js got loaded
self.assertEqual(data["testMsg"], None) assert data["testMsg"] is None
self.assertEqual(data["testMsg2"], None) assert data["testMsg2"] is None
await page.close() await page.close()
# Fragment where JS and CSS is defined on Component class # Fragment where JS and CSS is defined on Component class
@with_playwright @with_playwright
async def test_fragment_comp(self): async def test_fragment_comp(self):
page: Page = await self.browser.new_page() page: Page = await self.browser.new_page() # type: ignore[attr-defined]
await page.goto(f"{TEST_SERVER_URL}/fragment/base/js?frag=comp") await page.goto(f"{TEST_SERVER_URL}/fragment/base/js?frag=comp")
test_before_js: types.js = """() => { test_before_js: types.js = """() => {
@ -353,8 +349,8 @@ class E2eDependencyRenderingTests(BaseTestCase):
data_before = await page.evaluate(test_before_js) data_before = await page.evaluate(test_before_js)
self.assertEqual(data_before["targetHtml"], '<div id="target">OLD</div>') assert data_before["targetHtml"] == '<div id="target">OLD</div>'
self.assertEqual(data_before["fragHtml"], None) assert data_before["fragHtml"] is None
# Clicking button should load and insert the fragment # Clicking button should load and insert the fragment
await page.locator("button").click() await page.locator("button").click()
@ -380,21 +376,18 @@ class E2eDependencyRenderingTests(BaseTestCase):
data = await page.evaluate(test_js) data = await page.evaluate(test_js)
self.assertEqual(data["targetHtml"], None) assert data["targetHtml"] is None
self.assertRegex( assert re.compile(
data["fragHtml"],
re.compile(
r'<div class="frag" data-djc-id-\w{6}="">\s*' r"123\s*" r'<span id="frag-text">xxx</span>\s*' r"</div>" r'<div class="frag" data-djc-id-\w{6}="">\s*' r"123\s*" r'<span id="frag-text">xxx</span>\s*' r"</div>"
), ).search(data["fragHtml"]) is not None
) assert "rgb(0, 0, 255)" in data["fragBg"] # AKA 'background: blue'
self.assertIn("rgb(0, 0, 255)", data["fragBg"]) # AKA 'background: blue'
await page.close() await page.close()
# Fragment where JS and CSS is defined on Media class # Fragment where JS and CSS is defined on Media class
@with_playwright @with_playwright
async def test_fragment_media(self): async def test_fragment_media(self):
page: Page = await self.browser.new_page() page: Page = await self.browser.new_page() # type: ignore[attr-defined]
await page.goto(f"{TEST_SERVER_URL}/fragment/base/js?frag=media") await page.goto(f"{TEST_SERVER_URL}/fragment/base/js?frag=media")
test_before_js: types.js = """() => { test_before_js: types.js = """() => {
@ -408,8 +401,8 @@ class E2eDependencyRenderingTests(BaseTestCase):
data_before = await page.evaluate(test_before_js) data_before = await page.evaluate(test_before_js)
self.assertEqual(data_before["targetHtml"], '<div id="target">OLD</div>') assert data_before["targetHtml"] == '<div id="target">OLD</div>'
self.assertEqual(data_before["fragHtml"], None) assert data_before["fragHtml"] is None
# Clicking button should load and insert the fragment # Clicking button should load and insert the fragment
await page.locator("button").click() await page.locator("button").click()
@ -433,21 +426,18 @@ class E2eDependencyRenderingTests(BaseTestCase):
data = await page.evaluate(test_js) data = await page.evaluate(test_js)
self.assertEqual(data["targetHtml"], None) assert data["targetHtml"] is None
self.assertRegex( assert re.compile(
data["fragHtml"],
re.compile(
r'<div class="frag" data-djc-id-\w{6}="">\s*' r"123\s*" r'<span id="frag-text">xxx</span>\s*' r"</div>" r'<div class="frag" data-djc-id-\w{6}="">\s*' r"123\s*" r'<span id="frag-text">xxx</span>\s*' r"</div>"
), ).search(data["fragHtml"]) is not None
) assert "rgb(0, 0, 255)" in data["fragBg"] # AKA 'background: blue'
self.assertIn("rgb(0, 0, 255)", data["fragBg"]) # AKA 'background: blue'
await page.close() await page.close()
# Fragment loaded by AlpineJS # Fragment loaded by AlpineJS
@with_playwright @with_playwright
async def test_fragment_alpine(self): async def test_fragment_alpine(self):
page: Page = await self.browser.new_page() page: Page = await self.browser.new_page() # type: ignore[attr-defined]
await page.goto(f"{TEST_SERVER_URL}/fragment/base/alpine?frag=comp") await page.goto(f"{TEST_SERVER_URL}/fragment/base/alpine?frag=comp")
test_before_js: types.js = """() => { test_before_js: types.js = """() => {
@ -461,8 +451,8 @@ class E2eDependencyRenderingTests(BaseTestCase):
data_before = await page.evaluate(test_before_js) data_before = await page.evaluate(test_before_js)
self.assertEqual(data_before["targetHtml"], '<div id="target" x-html="htmlVar">OLD</div>') assert data_before["targetHtml"] == '<div id="target" x-html="htmlVar">OLD</div>'
self.assertEqual(data_before["fragHtml"], None) assert data_before["fragHtml"] is None
# Clicking button should load and insert the fragment # Clicking button should load and insert the fragment
await page.locator("button").click() await page.locator("button").click()
@ -490,20 +480,17 @@ class E2eDependencyRenderingTests(BaseTestCase):
# NOTE: Unlike the vanilla JS tests, for the Alpine test we don't remove the targetHtml, # NOTE: Unlike the vanilla JS tests, for the Alpine test we don't remove the targetHtml,
# but only change its contents. # but only change its contents.
self.assertRegex( assert re.compile(
data["targetHtml"],
re.compile(
r'<div class="frag" data-djc-id-\w{6}="">\s*' r"123\s*" r'<span id="frag-text">xxx</span>\s*' r"</div>" r'<div class="frag" data-djc-id-\w{6}="">\s*' r"123\s*" r'<span id="frag-text">xxx</span>\s*' r"</div>"
), ).search(data["targetHtml"]) is not None
) assert "rgb(0, 0, 255)" in data["fragBg"] # AKA 'background: blue'
self.assertIn("rgb(0, 0, 255)", data["fragBg"]) # AKA 'background: blue'
await page.close() await page.close()
# Fragment loaded by HTMX # Fragment loaded by HTMX
@with_playwright @with_playwright
async def test_fragment_htmx(self): async def test_fragment_htmx(self):
page: Page = await self.browser.new_page() page: Page = await self.browser.new_page() # type: ignore[attr-defined]
await page.goto(f"{TEST_SERVER_URL}/fragment/base/htmx?frag=comp") await page.goto(f"{TEST_SERVER_URL}/fragment/base/htmx?frag=comp")
test_before_js: types.js = """() => { test_before_js: types.js = """() => {
@ -517,8 +504,8 @@ class E2eDependencyRenderingTests(BaseTestCase):
data_before = await page.evaluate(test_before_js) data_before = await page.evaluate(test_before_js)
self.assertEqual(data_before["targetHtml"], '<div id="target">OLD</div>') assert data_before["targetHtml"] == '<div id="target">OLD</div>'
self.assertEqual(data_before["fragHtml"], None) assert data_before["fragHtml"] is None
# Clicking button should load and insert the fragment # Clicking button should load and insert the fragment
await page.locator("button").click() await page.locator("button").click()
@ -544,14 +531,11 @@ class E2eDependencyRenderingTests(BaseTestCase):
data = await page.evaluate(test_js) data = await page.evaluate(test_js)
self.assertEqual(data["targetHtml"], None) assert data["targetHtml"] is None
# NOTE: We test only the inner HTML, because the element itself may or may not have # 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. # extra CSS classes added by HTMX, which results in flaky tests.
self.assertRegex( assert re.compile(r'123\s*<span id="frag-text">xxx</span>').search(data["fragInnerHtml"]) is not None
data["fragInnerHtml"], assert "rgb(0, 0, 255)" in data["fragBg"] # AKA 'background: blue'
re.compile(r'123\s*<span id="frag-text">xxx</span>'),
)
self.assertIn("rgb(0, 0, 255)", data["fragBg"]) # AKA 'background: blue'
await page.close() await page.close()
@ -559,11 +543,11 @@ class E2eDependencyRenderingTests(BaseTestCase):
async def test_alpine__head(self): async def test_alpine__head(self):
single_comp_url = TEST_SERVER_URL + "/alpine/head" single_comp_url = TEST_SERVER_URL + "/alpine/head"
page: Page = await self.browser.new_page() page: Page = await self.browser.new_page() # type: ignore[attr-defined]
await page.goto(single_comp_url) await page.goto(single_comp_url)
component_text = await page.locator('[x-data="alpine_test"]').text_content() component_text = await page.locator('[x-data="alpine_test"]').text_content()
self.assertHTMLEqual(component_text.strip(), "ALPINE_TEST: 123") assertHTMLEqual(component_text.strip(), "ALPINE_TEST: 123")
await page.close() await page.close()
@ -571,11 +555,11 @@ class E2eDependencyRenderingTests(BaseTestCase):
async def test_alpine__body(self): async def test_alpine__body(self):
single_comp_url = TEST_SERVER_URL + "/alpine/body" single_comp_url = TEST_SERVER_URL + "/alpine/body"
page: Page = await self.browser.new_page() page: Page = await self.browser.new_page() # type: ignore[attr-defined]
await page.goto(single_comp_url) await page.goto(single_comp_url)
component_text = await page.locator('[x-data="alpine_test"]').text_content() component_text = await page.locator('[x-data="alpine_test"]').text_content()
self.assertHTMLEqual(component_text.strip(), "ALPINE_TEST: 123") assertHTMLEqual(component_text.strip(), "ALPINE_TEST: 123")
await page.close() await page.close()
@ -583,11 +567,11 @@ class E2eDependencyRenderingTests(BaseTestCase):
async def test_alpine__body2(self): async def test_alpine__body2(self):
single_comp_url = TEST_SERVER_URL + "/alpine/body2" single_comp_url = TEST_SERVER_URL + "/alpine/body2"
page: Page = await self.browser.new_page() page: Page = await self.browser.new_page() # type: ignore[attr-defined]
await page.goto(single_comp_url) await page.goto(single_comp_url)
component_text = await page.locator('[x-data="alpine_test"]').text_content() component_text = await page.locator('[x-data="alpine_test"]').text_content()
self.assertHTMLEqual(component_text.strip(), "ALPINE_TEST: 123") assertHTMLEqual(component_text.strip(), "ALPINE_TEST: 123")
await page.close() await page.close()
@ -595,10 +579,10 @@ class E2eDependencyRenderingTests(BaseTestCase):
async def test_alpine__invalid(self): async def test_alpine__invalid(self):
single_comp_url = TEST_SERVER_URL + "/alpine/invalid" single_comp_url = TEST_SERVER_URL + "/alpine/invalid"
page: Page = await self.browser.new_page() page: Page = await self.browser.new_page() # type: ignore[attr-defined]
await page.goto(single_comp_url) await page.goto(single_comp_url)
component_text = await page.locator('[x-data="alpine_test"]').text_content() component_text = await page.locator('[x-data="alpine_test"]').text_content()
self.assertHTMLEqual(component_text.strip(), "ALPINE_TEST:") assertHTMLEqual(component_text.strip(), "ALPINE_TEST:")
await page.close() await page.close()

View file

@ -1,15 +1,18 @@
"""Catch-all for tests that use template tags and don't fit other files""" """Catch-all for tests that use template tags and don't fit other files"""
import re
from typing import Any, Dict from typing import Any, Dict
import pytest
from django.template import Context, Template, TemplateSyntaxError from django.template import Context, Template, TemplateSyntaxError
from django.template.base import FilterExpression, Node, Parser, Token from django.template.base import FilterExpression, Node, Parser, Token
from pytest_django.asserts import assertHTMLEqual
from django_components import Component, register, registry, types from django_components import Component, register, registry, types
from django_components.expression import DynamicFilterExpression, is_aggregate_key from django_components.expression import DynamicFilterExpression, is_aggregate_key
from .django_test_setup import setup_test_config from django_components.testing import djc_test
from .testutils import BaseTestCase, parametrize_context_behavior from .testutils import PARAMETRIZE_CONTEXT_BEHAVIOR, setup_test_config
setup_test_config({"autodiscover": False}) setup_test_config({"autodiscover": False})
@ -47,19 +50,20 @@ def make_context(d: Dict):
# NOTE: Django calls the `{{ }}` syntax "variables" and `{% %}` "blocks" # NOTE: Django calls the `{{ }}` syntax "variables" and `{% %}` "blocks"
class DynamicExprTests(BaseTestCase): @djc_test
class TestDynamicExpr:
def test_variable_resolve_dynamic_expr(self): def test_variable_resolve_dynamic_expr(self):
expr = DynamicFilterExpression(default_parser, '"{{ var_a|lower }}"') expr = DynamicFilterExpression(default_parser, '"{{ var_a|lower }}"')
ctx = make_context({"var_a": "LoREM"}) ctx = make_context({"var_a": "LoREM"})
self.assertEqual(expr.resolve(ctx), "lorem") assert expr.resolve(ctx) == "lorem"
def test_variable_raises_on_dynamic_expr_with_quotes_mismatch(self): def test_variable_raises_on_dynamic_expr_with_quotes_mismatch(self):
with self.assertRaises(TemplateSyntaxError): with pytest.raises(TemplateSyntaxError):
DynamicFilterExpression(default_parser, "'{{ var_a|lower }}\"") DynamicFilterExpression(default_parser, "'{{ var_a|lower }}\"")
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_variable_in_template(self): def test_variable_in_template(self, components_settings):
captured = {} captured = {}
@register("test") @register("test")
@ -110,11 +114,11 @@ class DynamicExprTests(BaseTestCase):
) )
# Check that variables passed to the component are of correct type # Check that variables passed to the component are of correct type
self.assertEqual(captured["pos_var1"], "lorem") assert captured["pos_var1"] == "lorem"
self.assertEqual(captured["bool_var"], True) assert captured["bool_var"] is True
self.assertEqual(captured["list_var"], [{"a": 1}, {"a": 2}]) assert captured["list_var"] == [{"a": 1}, {"a": 2}]
self.assertHTMLEqual( assertHTMLEqual(
rendered, rendered,
""" """
<!-- _RENDERED SimpleComponent_5b8d97,a1bc3f,, --> <!-- _RENDERED SimpleComponent_5b8d97,a1bc3f,, -->
@ -124,8 +128,8 @@ class DynamicExprTests(BaseTestCase):
""", """,
) )
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_block_in_template(self): def test_block_in_template(self, components_settings):
registry.library.tag(noop) registry.library.tag(noop)
captured = {} captured = {}
@ -183,11 +187,11 @@ class DynamicExprTests(BaseTestCase):
) )
# Check that variables passed to the component are of correct type # Check that variables passed to the component are of correct type
self.assertEqual(captured["bool_var"], True) assert captured["bool_var"] is True
self.assertEqual(captured["dict_var"], {"a": 3}) assert captured["dict_var"] == {"a": 3}
self.assertEqual(captured["list_var"], [{"a": 1}, {"a": 2}]) assert captured["list_var"] == [{"a": 1}, {"a": 2}]
self.assertHTMLEqual( assertHTMLEqual(
rendered, rendered,
""" """
<!-- _RENDERED SimpleComponent_743413,a1bc3f,, --> <!-- _RENDERED SimpleComponent_743413,a1bc3f,, -->
@ -198,8 +202,8 @@ class DynamicExprTests(BaseTestCase):
""", """,
) )
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_comment_in_template(self): def test_comment_in_template(self, components_settings):
registry.library.tag(noop) registry.library.tag(noop)
captured = {} captured = {}
@ -256,25 +260,22 @@ class DynamicExprTests(BaseTestCase):
) )
# Check that variables passed to the component are of correct type # Check that variables passed to the component are of correct type
self.assertEqual(captured["pos_var1"], "") assert captured["pos_var1"] == ""
self.assertEqual(captured["pos_var2"], " abc") assert captured["pos_var2"] == " abc"
self.assertEqual(captured["bool_var"], "") assert captured["bool_var"] == ""
self.assertEqual(captured["list_var"], " ") assert captured["list_var"] == " "
# NOTE: This is whitespace-sensitive test, so we check exact output # NOTE: This is whitespace-sensitive test, so we check exact output
self.assertEqual( assert rendered.strip() == (
rendered.strip(), "<!-- _RENDERED SimpleComponent_6f07b3,a1bc3f,, -->\n"
(
"<!-- _RENDERED SimpleComponent_e258c0,a1bc3f,, -->\n"
' <div data-djc-id-a1bc3f=""></div>\n' ' <div data-djc-id-a1bc3f=""></div>\n'
' <div data-djc-id-a1bc3f=""> abc</div>\n' ' <div data-djc-id-a1bc3f=""> abc</div>\n'
' <div data-djc-id-a1bc3f=""></div>\n' ' <div data-djc-id-a1bc3f=""></div>\n'
' <div data-djc-id-a1bc3f=""> </div>' ' <div data-djc-id-a1bc3f=""> </div>'
),
) )
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_mixed_in_template(self): def test_mixed_in_template(self, components_settings):
registry.library.tag(noop) registry.library.tag(noop)
captured = {} captured = {}
@ -336,25 +337,22 @@ class DynamicExprTests(BaseTestCase):
) )
# Check that variables passed to the component are of correct type # Check that variables passed to the component are of correct type
self.assertEqual(captured["bool_var"], " True ") assert captured["bool_var"] == " True "
self.assertEqual(captured["dict_var"], " {'a': 3} ") assert captured["dict_var"] == " {'a': 3} "
self.assertEqual(captured["list_var"], " [{'a': 1}, {'a': 2}] ") assert captured["list_var"] == " [{'a': 1}, {'a': 2}] "
# NOTE: This is whitespace-sensitive test, so we check exact output # NOTE: This is whitespace-sensitive test, so we check exact output
self.assertEqual( assert rendered.strip() == (
rendered.strip(), "<!-- _RENDERED SimpleComponent_85c7eb,a1bc3f,, -->\n"
(
"<!-- _RENDERED SimpleComponent_6c8e94,a1bc3f,, -->\n"
' <div data-djc-id-a1bc3f=""> lorem ipsum dolor </div>\n' ' <div data-djc-id-a1bc3f=""> lorem ipsum dolor </div>\n'
' <div data-djc-id-a1bc3f=""> lorem ipsum dolor [{\'a\': 1}] </div>\n' ' <div data-djc-id-a1bc3f=""> lorem ipsum dolor [{\'a\': 1}] </div>\n'
' <div data-djc-id-a1bc3f=""> True </div>\n' ' <div data-djc-id-a1bc3f=""> True </div>\n'
' <div data-djc-id-a1bc3f=""> [{\'a\': 1}, {\'a\': 2}] </div>\n' ' <div data-djc-id-a1bc3f=""> [{\'a\': 1}, {\'a\': 2}] </div>\n'
' <div data-djc-id-a1bc3f=""> {\'a\': 3} </div>' ' <div data-djc-id-a1bc3f=""> {\'a\': 3} </div>'
),
) )
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_ignores_invalid_tag(self): def test_ignores_invalid_tag(self, components_settings):
registry.library.tag(noop) registry.library.tag(noop)
@register("test") @register("test")
@ -390,7 +388,7 @@ class DynamicExprTests(BaseTestCase):
Context({"is_active": True}), Context({"is_active": True}),
) )
self.assertHTMLEqual( assertHTMLEqual(
rendered, rendered,
""" """
<!-- _RENDERED SimpleComponent_c7a5c3,a1bc3f,, --> <!-- _RENDERED SimpleComponent_c7a5c3,a1bc3f,, -->
@ -400,8 +398,8 @@ class DynamicExprTests(BaseTestCase):
""", """,
) )
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_nested_in_template(self): def test_nested_in_template(self, components_settings):
registry.library.tag(noop) registry.library.tag(noop)
@register("test") @register("test")
@ -442,7 +440,7 @@ class DynamicExprTests(BaseTestCase):
), ),
) )
self.assertHTMLEqual( assertHTMLEqual(
rendered, rendered,
""" """
<!-- _RENDERED SimpleComponent_5c8766,a1bc41,, --> <!-- _RENDERED SimpleComponent_5c8766,a1bc41,, -->
@ -456,9 +454,9 @@ class DynamicExprTests(BaseTestCase):
) )
class SpreadOperatorTests(BaseTestCase): class TestSpreadOperator:
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_component(self): def test_component(self, components_settings):
captured = {} captured = {}
@register("test") @register("test")
@ -513,12 +511,12 @@ class SpreadOperatorTests(BaseTestCase):
) )
# Check that variables passed to the component are of correct type # Check that variables passed to the component are of correct type
self.assertEqual(captured["attrs"], {"@click": "() => {}", "style": "height: 20px"}) assert captured["attrs"] == {"@click": "() => {}", "style": "height: 20px"}
self.assertEqual(captured["items"], [1, 2, 3]) assert captured["items"] == [1, 2, 3]
self.assertEqual(captured["a"], 1) assert captured["a"] == 1
self.assertEqual(captured["x"], 123) assert captured["x"] == 123
self.assertHTMLEqual( assertHTMLEqual(
rendered, rendered,
""" """
<div data-djc-id-a1bc3f>LoREM</div> <div data-djc-id-a1bc3f>LoREM</div>
@ -529,8 +527,8 @@ class SpreadOperatorTests(BaseTestCase):
""", """,
) )
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_slot(self): def test_slot(self, components_settings):
@register("test") @register("test")
class SimpleComponent(Component): class SimpleComponent(Component):
def get_context_data(self): def get_context_data(self):
@ -559,15 +557,15 @@ class SpreadOperatorTests(BaseTestCase):
template = Template(template_str) template = Template(template_str)
rendered = template.render(Context({})) rendered = template.render(Context({}))
self.assertHTMLEqual( assertHTMLEqual(
rendered, rendered,
""" """
{'items': [1, 2, 3], 'a': 1, 'x': 123, 'attrs': {'@click': '() =&gt; {}', 'style': 'height: 20px'}} {'items': [1, 2, 3], 'a': 1, 'x': 123, 'attrs': {'@click': '() =&gt; {}', 'style': 'height: 20px'}}
""", """,
) )
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_fill(self): def test_fill(self, components_settings):
@register("test") @register("test")
class SimpleComponent(Component): class SimpleComponent(Component):
def get_context_data(self): def get_context_data(self):
@ -608,7 +606,7 @@ class SpreadOperatorTests(BaseTestCase):
), ),
) )
self.assertHTMLEqual( assertHTMLEqual(
rendered, rendered,
""" """
{'items': [1, 2, 3], 'a': 1, 'x': 123, 'attrs': {'@click': '() =&gt; {}', 'style': 'height: 20px'}} {'items': [1, 2, 3], 'a': 1, 'x': 123, 'attrs': {'@click': '() =&gt; {}', 'style': 'height: 20px'}}
@ -616,8 +614,8 @@ class SpreadOperatorTests(BaseTestCase):
""", """,
) )
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_provide(self): def test_provide(self, components_settings):
@register("test") @register("test")
class SimpleComponent(Component): class SimpleComponent(Component):
def get_context_data(self): def get_context_data(self):
@ -655,7 +653,7 @@ class SpreadOperatorTests(BaseTestCase):
), ),
) )
self.assertHTMLEqual( assertHTMLEqual(
rendered, rendered,
""" """
<div data-djc-id-a1bc41>{'@click': '() =&gt; {}', 'style': 'height: 20px'}</div> <div data-djc-id-a1bc41>{'@click': '() =&gt; {}', 'style': 'height: 20px'}</div>
@ -664,8 +662,8 @@ class SpreadOperatorTests(BaseTestCase):
""", """,
) )
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_html_attrs(self): def test_html_attrs(self, components_settings):
template_str: types.django_html = """ template_str: types.django_html = """
{% load component_tags %} {% load component_tags %}
<div {% html_attrs defaults:test="hi" ...my_dict attrs:lol="123" %}> <div {% html_attrs defaults:test="hi" ...my_dict attrs:lol="123" %}>
@ -683,15 +681,15 @@ class SpreadOperatorTests(BaseTestCase):
} }
), ),
) )
self.assertHTMLEqual( assertHTMLEqual(
rendered, rendered,
""" """
<div test="hi" class="my-class button" style="height: 20px" lol="123"> <div test="hi" class="my-class button" style="height: 20px" lol="123">
""", """,
) )
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_later_spreads_do_not_overwrite_earlier(self): def test_later_spreads_do_not_overwrite_earlier(self, components_settings):
@register("test") @register("test")
class SimpleComponent(Component): class SimpleComponent(Component):
def get_context_data( def get_context_data(
@ -737,7 +735,7 @@ class SpreadOperatorTests(BaseTestCase):
template1 = Template(template_str1) template1 = Template(template_str1)
with self.assertRaisesMessage(TypeError, "got multiple values for argument 'x'"): with pytest.raises(TypeError, match=re.escape("got multiple values for argument 'x'")):
template1.render(context) template1.render(context)
# But, similarly to python, we can merge multiple **kwargs by instead # But, similarly to python, we can merge multiple **kwargs by instead
@ -759,7 +757,7 @@ class SpreadOperatorTests(BaseTestCase):
template2 = Template(template_str2) template2 = Template(template_str2)
rendered2 = template2.render(context) rendered2 = template2.render(context)
self.assertHTMLEqual( assertHTMLEqual(
rendered2, rendered2,
""" """
<div data-djc-id-a1bc40>{'@click': '() =&gt; {}', 'style': 'OVERWRITTEN'}</div> <div data-djc-id-a1bc40>{'@click': '() =&gt; {}', 'style': 'OVERWRITTEN'}</div>
@ -769,8 +767,8 @@ class SpreadOperatorTests(BaseTestCase):
""", """,
) )
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_raises_on_missing_value(self): def test_raises_on_missing_value(self, components_settings):
@register("test") @register("test")
class SimpleComponent(Component): class SimpleComponent(Component):
pass pass
@ -785,11 +783,11 @@ class SpreadOperatorTests(BaseTestCase):
""" """
) )
with self.assertRaisesMessage(TemplateSyntaxError, "Spread syntax '...' is missing a value"): with pytest.raises(TemplateSyntaxError, match=re.escape("Spread syntax '...' is missing a value")):
Template(template_str) Template(template_str)
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_spread_list_and_iterables(self): def test_spread_list_and_iterables(self, components_settings):
captured = None captured = None
@register("test") @register("test")
@ -821,16 +819,13 @@ class SpreadOperatorTests(BaseTestCase):
template.render(context) template.render(context)
self.assertEqual( assert captured == (
captured,
(
("a", "b", "c", 1, 2, 3), ("a", "b", "c", 1, 2, 3),
{}, {},
),
) )
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_raises_on_non_dict(self): def test_raises_on_non_dict(self, components_settings):
@register("test") @register("test")
class SimpleComponent(Component): class SimpleComponent(Component):
pass pass
@ -847,11 +842,15 @@ class SpreadOperatorTests(BaseTestCase):
template = Template(template_str) template = Template(template_str)
# List # List
with self.assertRaisesMessage(ValueError, "Cannot spread non-iterable value: '...var_b' resolved to 123"): with pytest.raises(
ValueError,
match=re.escape("Cannot spread non-iterable value: '...var_b' resolved to 123")
):
template.render(Context({"var_b": 123})) template.render(Context({"var_b": 123}))
class AggregateKwargsTest(BaseTestCase): @djc_test
class TestAggregateKwargs:
def test_aggregate_kwargs(self): def test_aggregate_kwargs(self):
captured = None captured = None
@ -879,9 +878,7 @@ class AggregateKwargsTest(BaseTestCase):
template = Template(template_str) template = Template(template_str)
template.render(Context({"class_var": "padding-top-8", "four": 4})) template.render(Context({"class_var": "padding-top-8", "four": 4}))
self.assertEqual( assert captured == (
captured,
(
(), (),
{ {
"attrs": { "attrs": {
@ -893,17 +890,16 @@ class AggregateKwargsTest(BaseTestCase):
"my_dict": {"one": 2}, "my_dict": {"one": 2},
"three": 4, "three": 4,
}, },
),
) )
def is_aggregate_key(self): def test_is_aggregate_key(self):
self.assertEqual(is_aggregate_key(""), False) assert not is_aggregate_key("")
self.assertEqual(is_aggregate_key(" "), False) assert not is_aggregate_key(" ")
self.assertEqual(is_aggregate_key(" : "), False) assert not is_aggregate_key(" : ")
self.assertEqual(is_aggregate_key("attrs"), False) assert not is_aggregate_key("attrs")
self.assertEqual(is_aggregate_key(":attrs"), False) assert not is_aggregate_key(":attrs")
self.assertEqual(is_aggregate_key(" :attrs "), False) assert not is_aggregate_key(" :attrs ")
self.assertEqual(is_aggregate_key("attrs:"), False) assert not is_aggregate_key("attrs:")
self.assertEqual(is_aggregate_key(":attrs:"), False) assert not is_aggregate_key(":attrs:")
self.assertEqual(is_aggregate_key("at:trs"), True) assert is_aggregate_key("at:trs")
self.assertEqual(is_aggregate_key(":at:trs"), False) assert not is_aggregate_key(":at:trs")

View file

@ -3,14 +3,14 @@ from typing import Any, Dict
from django.conf import settings from django.conf import settings
from django.http import HttpResponse from django.http import HttpResponse
from django.template import Context, Template from django.template import Context, Template
from django.test import Client from django.test import Client, SimpleTestCase
from django.urls import path from django.urls import path
from django_components import Component, ComponentView, register, types from django_components import Component, ComponentView, register, types
from django_components.urls import urlpatterns as dc_urlpatterns from django_components.urls import urlpatterns as dc_urlpatterns
from .django_test_setup import setup_test_config from django_components.testing import djc_test
from .testutils import BaseTestCase, parametrize_context_behavior from .testutils import setup_test_config
setup_test_config({"autodiscover": False}) setup_test_config({"autodiscover": False})
@ -29,7 +29,8 @@ class CustomClient(Client):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
class TestComponentAsView(BaseTestCase): @djc_test
class TestComponentAsView(SimpleTestCase):
def test_render_component_from_template(self): def test_render_component_from_template(self):
@register("testcomponent") @register("testcomponent")
class MockComponentRequest(Component): class MockComponentRequest(Component):
@ -183,7 +184,6 @@ class TestComponentAsView(BaseTestCase):
response.content.decode(), response.content.decode(),
) )
@parametrize_context_behavior(["django", "isolated"])
def test_replace_slot_in_view(self): def test_replace_slot_in_view(self):
class MockComponentSlot(Component): class MockComponentSlot(Component):
template = """ template = """
@ -212,7 +212,6 @@ class TestComponentAsView(BaseTestCase):
response.content, response.content,
) )
@parametrize_context_behavior(["django", "isolated"])
def test_replace_slot_in_view_with_insecure_content(self): def test_replace_slot_in_view_with_insecure_content(self):
class MockInsecureComponentSlot(Component): class MockInsecureComponentSlot(Component):
template = """ template = """
@ -234,7 +233,6 @@ class TestComponentAsView(BaseTestCase):
response.content, response.content,
) )
@parametrize_context_behavior(["django", "isolated"])
def test_replace_context_in_view(self): def test_replace_context_in_view(self):
class TestComponent(Component): class TestComponent(Component):
template = """ template = """
@ -255,7 +253,6 @@ class TestComponentAsView(BaseTestCase):
response.content, response.content,
) )
@parametrize_context_behavior(["django", "isolated"])
def test_replace_context_in_view_with_insecure_content(self): def test_replace_context_in_view_with_insecure_content(self):
class MockInsecureComponentContext(Component): class MockInsecureComponentContext(Component):
template = """ template = """

View file

@ -2,9 +2,10 @@ import re
from pathlib import Path from pathlib import Path
from django.contrib.staticfiles.management.commands.collectstatic import Command from django.contrib.staticfiles.management.commands.collectstatic import Command
from django.test import SimpleTestCase, override_settings from django.test import SimpleTestCase
from .django_test_setup import setup_test_config from django_components.testing import djc_test
from .testutils import setup_test_config
setup_test_config({"autodiscover": False}) setup_test_config({"autodiscover": False})
@ -76,14 +77,16 @@ COMPONENTS = {
class StaticFilesFinderTests(SimpleTestCase): class StaticFilesFinderTests(SimpleTestCase):
@override_settings( @djc_test(
django_settings={
**common_settings, **common_settings,
COMPONENTS=COMPONENTS, "STATICFILES_FINDERS": [
STATICFILES_FINDERS=[
# Default finders # Default finders
"django.contrib.staticfiles.finders.FileSystemFinder", "django.contrib.staticfiles.finders.FileSystemFinder",
"django.contrib.staticfiles.finders.AppDirectoriesFinder", "django.contrib.staticfiles.finders.AppDirectoriesFinder",
], ],
},
components_settings=COMPONENTS,
) )
def test_python_and_html_included(self): def test_python_and_html_included(self):
collected = do_collect() collected = do_collect()
@ -97,16 +100,18 @@ class StaticFilesFinderTests(SimpleTestCase):
self.assertListEqual(collected["unmodified"], []) self.assertListEqual(collected["unmodified"], [])
self.assertListEqual(collected["post_processed"], []) self.assertListEqual(collected["post_processed"], [])
@override_settings( @djc_test(
django_settings={
**common_settings, **common_settings,
COMPONENTS=COMPONENTS, "STATICFILES_FINDERS": [
STATICFILES_FINDERS=[
# Default finders # Default finders
"django.contrib.staticfiles.finders.FileSystemFinder", "django.contrib.staticfiles.finders.FileSystemFinder",
"django.contrib.staticfiles.finders.AppDirectoriesFinder", "django.contrib.staticfiles.finders.AppDirectoriesFinder",
# Django components # Django components
"django_components.finders.ComponentsFileSystemFinder", "django_components.finders.ComponentsFileSystemFinder",
], ],
},
components_settings=COMPONENTS,
) )
def test_python_and_html_omitted(self): def test_python_and_html_omitted(self):
collected = do_collect() collected = do_collect()
@ -120,22 +125,24 @@ class StaticFilesFinderTests(SimpleTestCase):
self.assertListEqual(collected["unmodified"], []) self.assertListEqual(collected["unmodified"], [])
self.assertListEqual(collected["post_processed"], []) self.assertListEqual(collected["post_processed"], [])
@override_settings( @djc_test(
django_settings={
**common_settings, **common_settings,
COMPONENTS={ "STATICFILES_FINDERS": [
**COMPONENTS,
"static_files_allowed": [
".js",
],
"static_files_forbidden": [],
},
STATICFILES_FINDERS=[
# Default finders # Default finders
"django.contrib.staticfiles.finders.FileSystemFinder", "django.contrib.staticfiles.finders.FileSystemFinder",
"django.contrib.staticfiles.finders.AppDirectoriesFinder", "django.contrib.staticfiles.finders.AppDirectoriesFinder",
# Django components # Django components
"django_components.finders.ComponentsFileSystemFinder", "django_components.finders.ComponentsFileSystemFinder",
], ],
},
components_settings={
**COMPONENTS,
"static_files_allowed": [
".js",
],
"static_files_forbidden": [],
},
) )
def test_set_static_files_allowed(self): def test_set_static_files_allowed(self):
collected = do_collect() collected = do_collect()
@ -149,9 +156,18 @@ class StaticFilesFinderTests(SimpleTestCase):
self.assertListEqual(collected["unmodified"], []) self.assertListEqual(collected["unmodified"], [])
self.assertListEqual(collected["post_processed"], []) self.assertListEqual(collected["post_processed"], [])
@override_settings( @djc_test(
django_settings={
**common_settings, **common_settings,
COMPONENTS={ "STATICFILES_FINDERS": [
# Default finders
"django.contrib.staticfiles.finders.FileSystemFinder",
"django.contrib.staticfiles.finders.AppDirectoriesFinder",
# Django components
"django_components.finders.ComponentsFileSystemFinder",
],
},
components_settings={
**COMPONENTS, **COMPONENTS,
"static_files_allowed": [ "static_files_allowed": [
re.compile(r".*"), re.compile(r".*"),
@ -160,13 +176,6 @@ class StaticFilesFinderTests(SimpleTestCase):
re.compile(r"\.(?:js)$"), re.compile(r"\.(?:js)$"),
], ],
}, },
STATICFILES_FINDERS=[
# Default finders
"django.contrib.staticfiles.finders.FileSystemFinder",
"django.contrib.staticfiles.finders.AppDirectoriesFinder",
# Django components
"django_components.finders.ComponentsFileSystemFinder",
],
) )
def test_set_forbidden_files(self): def test_set_forbidden_files(self):
collected = do_collect() collected = do_collect()
@ -180,9 +189,18 @@ class StaticFilesFinderTests(SimpleTestCase):
self.assertListEqual(collected["unmodified"], []) self.assertListEqual(collected["unmodified"], [])
self.assertListEqual(collected["post_processed"], []) self.assertListEqual(collected["post_processed"], [])
@override_settings( @djc_test(
django_settings={
**common_settings, **common_settings,
COMPONENTS={ "STATICFILES_FINDERS": [
# Default finders
"django.contrib.staticfiles.finders.FileSystemFinder",
"django.contrib.staticfiles.finders.AppDirectoriesFinder",
# Django components
"django_components.finders.ComponentsFileSystemFinder",
],
},
components_settings={
**COMPONENTS, **COMPONENTS,
"static_files_allowed": [ "static_files_allowed": [
".js", ".js",
@ -192,13 +210,6 @@ class StaticFilesFinderTests(SimpleTestCase):
".js", ".js",
], ],
}, },
STATICFILES_FINDERS=[
# Default finders
"django.contrib.staticfiles.finders.FileSystemFinder",
"django.contrib.staticfiles.finders.AppDirectoriesFinder",
# Django components
"django_components.finders.ComponentsFileSystemFinder",
],
) )
def test_set_both_allowed_and_forbidden_files(self): def test_set_both_allowed_and_forbidden_files(self):
collected = do_collect() collected = do_collect()

View file

@ -1,20 +1,22 @@
from django.test import TestCase from pytest_django.asserts import assertHTMLEqual
from djc_core_html_parser import set_html_attributes from djc_core_html_parser import set_html_attributes
from .django_test_setup import setup_test_config from django_components.testing import djc_test
from .testutils import setup_test_config
setup_test_config({"autodiscover": False}) setup_test_config({"autodiscover": False})
# This same set of tests is also found in djc_html_parser, to ensure that # This same set of tests is also found in djc_html_parser, to ensure that
# this implementation can be replaced with the djc_html_parser's Rust-based implementation # this implementation can be replaced with the djc_html_parser's Rust-based implementation
class TestHTMLParser(TestCase): @djc_test
class TestHTMLParser:
def test_basic_transformation(self): def test_basic_transformation(self):
html = "<div><p>Hello</p></div>" html = "<div><p>Hello</p></div>"
result, _ = set_html_attributes(html, root_attributes=["data-root"], all_attributes=["data-all"]) result, _ = set_html_attributes(html, root_attributes=["data-root"], all_attributes=["data-all"])
self.assertHTMLEqual( assertHTMLEqual(
result, result,
""" """
<div data-root data-all> <div data-root data-all>
@ -27,7 +29,7 @@ class TestHTMLParser(TestCase):
html = "<div>First</div><span>Second</span>" html = "<div>First</div><span>Second</span>"
result, _ = set_html_attributes(html, root_attributes=["data-root"], all_attributes=["data-all"]) result, _ = set_html_attributes(html, root_attributes=["data-root"], all_attributes=["data-all"])
self.assertHTMLEqual( assertHTMLEqual(
result, result,
""" """
<div data-root data-all>First</div> <div data-root data-all>First</div>
@ -81,7 +83,7 @@ class TestHTMLParser(TestCase):
<p data-all data-v-123>&copy; 2024</p> <p data-all data-v-123>&copy; 2024</p>
</footer> </footer>
""" # noqa: E501 """ # noqa: E501
self.assertHTMLEqual(result, expected) assertHTMLEqual(result, expected)
def test_void_elements(self): def test_void_elements(self):
test_cases = [ test_cases = [
@ -93,7 +95,7 @@ class TestHTMLParser(TestCase):
for input_html, expected in test_cases: for input_html, expected in test_cases:
result, _ = set_html_attributes(input_html, ["data-root"], ["data-v-123"]) result, _ = set_html_attributes(input_html, ["data-root"], ["data-v-123"])
self.assertHTMLEqual(result, expected) assertHTMLEqual(result, expected)
def test_html_head_with_meta(self): def test_html_head_with_meta(self):
html = """ html = """
@ -106,7 +108,7 @@ class TestHTMLParser(TestCase):
result, _ = set_html_attributes(html, ["data-root"], ["data-v-123"]) result, _ = set_html_attributes(html, ["data-root"], ["data-v-123"])
self.assertHTMLEqual( assertHTMLEqual(
result, result,
""" """
<head data-root data-v-123> <head data-root data-v-123>
@ -128,7 +130,7 @@ class TestHTMLParser(TestCase):
result, captured = set_html_attributes(html, ["data-root"], ["data-v-123"], watch_on_attribute="data-id") result, captured = set_html_attributes(html, ["data-root"], ["data-v-123"], watch_on_attribute="data-id")
self.assertHTMLEqual( assertHTMLEqual(
result, result,
""" """
<div data-id="123" data-root data-v-123> <div data-id="123" data-root data-v-123>
@ -140,14 +142,14 @@ class TestHTMLParser(TestCase):
) )
# Verify attribute capturing # Verify attribute capturing
self.assertEqual(len(captured), 3) assert len(captured) == 3
# Root element should have both root and all attributes # Root element should have both root and all attributes
self.assertEqual(captured["123"], ["data-root", "data-v-123"]) assert captured["123"] == ["data-root", "data-v-123"]
# Non-root elements should only have all attributes # Non-root elements should only have all attributes
self.assertEqual(captured["456"], ["data-v-123"]) assert captured["456"] == ["data-v-123"]
self.assertEqual(captured["789"], ["data-v-123"]) assert captured["789"] == ["data-v-123"]
def test_whitespace_preservation(self): def test_whitespace_preservation(self):
html = """<div> html = """<div>
@ -161,4 +163,4 @@ class TestHTMLParser(TestCase):
<span data-all=""> Text with spaces </span> <span data-all=""> Text with spaces </span>
</div>""" </div>"""
self.assertEqual(result, expected) assert result == expected

View file

@ -1,21 +1,25 @@
import re
import os import os
from pathlib import Path from pathlib import Path
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch
import pytest
from django.conf import settings from django.conf import settings
from django.test import override_settings
from django_components.util.loader import _filepath_to_python_module, get_component_dirs, get_component_files from django_components.util.loader import _filepath_to_python_module, get_component_dirs, get_component_files
from .django_test_setup import setup_test_config from django_components.testing import djc_test
from .testutils import BaseTestCase from .testutils import setup_test_config
setup_test_config({"autodiscover": False}) setup_test_config({"autodiscover": False})
class ComponentDirsTest(BaseTestCase): @djc_test
@override_settings( class TestComponentDirs:
BASE_DIR=Path(__file__).parent.resolve(), @djc_test(
django_settings={
"BASE_DIR": Path(__file__).parent.resolve(),
},
) )
def test_get_dirs__base_dir(self): def test_get_dirs__base_dir(self):
dirs = sorted(get_component_dirs()) dirs = sorted(get_component_dirs())
@ -23,24 +27,23 @@ class ComponentDirsTest(BaseTestCase):
apps_dirs = [dirs[0], dirs[2]] apps_dirs = [dirs[0], dirs[2]]
own_dirs = [dirs[1], *dirs[3:]] own_dirs = [dirs[1], *dirs[3:]]
self.assertEqual( assert own_dirs == [
own_dirs,
[
# Top-level /components dir # Top-level /components dir
Path(__file__).parent.resolve() Path(__file__).parent.resolve()
/ "components", / "components",
], ]
)
# Apps with a `components` dir # Apps with a `components` dir
self.assertEqual(len(apps_dirs), 2) assert len(apps_dirs) == 2
# NOTE: Compare parts so that the test works on Windows too # NOTE: Compare parts so that the test works on Windows too
self.assertTupleEqual(apps_dirs[0].parts[-2:], ("django_components", "components")) assert apps_dirs[0].parts[-2:] == ("django_components", "components")
self.assertTupleEqual(apps_dirs[1].parts[-3:], ("tests", "test_app", "components")) assert apps_dirs[1].parts[-3:] == ("tests", "test_app", "components")
@override_settings( @djc_test(
BASE_DIR=Path(__file__).parent.resolve() / "test_structures" / "test_structure_1", # noqa django_settings={
"BASE_DIR": Path(__file__).parent.resolve() / "test_structures" / "test_structure_1", # noqa
},
) )
def test_get_dirs__base_dir__complex(self): def test_get_dirs__base_dir__complex(self):
dirs = sorted(get_component_dirs()) dirs = sorted(get_component_dirs())
@ -49,25 +52,27 @@ class ComponentDirsTest(BaseTestCase):
own_dirs = dirs[2:] own_dirs = dirs[2:]
# Apps with a `components` dir # Apps with a `components` dir
self.assertEqual(len(apps_dirs), 2) assert len(apps_dirs) == 2
# NOTE: Compare parts so that the test works on Windows too # NOTE: Compare parts so that the test works on Windows too
self.assertTupleEqual(apps_dirs[0].parts[-2:], ("django_components", "components")) assert apps_dirs[0].parts[-2:] == ("django_components", "components")
self.assertTupleEqual(apps_dirs[1].parts[-3:], ("tests", "test_app", "components")) assert apps_dirs[1].parts[-3:] == ("tests", "test_app", "components")
expected = [ expected = [
Path(__file__).parent.resolve() / "test_structures" / "test_structure_1" / "components", Path(__file__).parent.resolve() / "test_structures" / "test_structure_1" / "components",
] ]
self.assertEqual(own_dirs, expected) assert own_dirs == expected
@override_settings( @djc_test(
BASE_DIR=Path(__file__).parent.resolve(), django_settings={
STATICFILES_DIRS=[ "BASE_DIR": Path(__file__).parent.resolve(),
"STATICFILES_DIRS": [
Path(__file__).parent.resolve() / "components", Path(__file__).parent.resolve() / "components",
("with_alias", Path(__file__).parent.resolve() / "components"), ("with_alias", Path(__file__).parent.resolve() / "components"),
("too_many", Path(__file__).parent.resolve() / "components", Path(__file__).parent.resolve()), ("too_many", Path(__file__).parent.resolve() / "components", Path(__file__).parent.resolve()),
("with_not_str_alias", 3), ("with_not_str_alias", 3),
], # noqa ], # noqa
},
) )
@patch("django_components.util.loader.logger.warning") @patch("django_components.util.loader.logger.warning")
def test_get_dirs__components_dirs(self, mock_warning: MagicMock): def test_get_dirs__components_dirs(self, mock_warning: MagicMock):
@ -78,27 +83,26 @@ class ComponentDirsTest(BaseTestCase):
own_dirs = [dirs[1], *dirs[3:]] own_dirs = [dirs[1], *dirs[3:]]
# Apps with a `components` dir # Apps with a `components` dir
self.assertEqual(len(apps_dirs), 2) assert len(apps_dirs) == 2
# NOTE: Compare parts so that the test works on Windows too # NOTE: Compare parts so that the test works on Windows too
self.assertTupleEqual(apps_dirs[0].parts[-2:], ("django_components", "components")) assert apps_dirs[0].parts[-2:] == ("django_components", "components")
self.assertTupleEqual(apps_dirs[1].parts[-3:], ("tests", "test_app", "components")) assert apps_dirs[1].parts[-3:] == ("tests", "test_app", "components")
self.assertEqual( assert own_dirs == [
own_dirs,
[
# Top-level /components dir # Top-level /components dir
Path(__file__).parent.resolve() Path(__file__).parent.resolve()
/ "components", / "components",
], ]
)
warn_inputs = [warn.args[0] for warn in mock_warning.call_args_list] warn_inputs = [warn.args[0] for warn in mock_warning.call_args_list]
assert "Got <class 'int'> : 3" in warn_inputs[0] assert "Got <class 'int'> : 3" in warn_inputs[0]
@override_settings( @djc_test(
BASE_DIR=Path(__file__).parent.resolve(), django_settings={
COMPONENTS={ "BASE_DIR": Path(__file__).parent.resolve(),
},
components_settings={
"dirs": [], "dirs": [],
}, },
) )
@ -108,35 +112,41 @@ class ComponentDirsTest(BaseTestCase):
apps_dirs = dirs apps_dirs = dirs
# Apps with a `components` dir # Apps with a `components` dir
self.assertEqual(len(apps_dirs), 2) assert len(apps_dirs) == 2
# NOTE: Compare parts so that the test works on Windows too # NOTE: Compare parts so that the test works on Windows too
self.assertTupleEqual(apps_dirs[0].parts[-2:], ("django_components", "components")) assert apps_dirs[0].parts[-2:] == ("django_components", "components")
self.assertTupleEqual(apps_dirs[1].parts[-3:], ("tests", "test_app", "components")) assert apps_dirs[1].parts[-3:] == ("tests", "test_app", "components")
@override_settings( @djc_test(
BASE_DIR=Path(__file__).parent.resolve(), django_settings={
COMPONENTS={ "BASE_DIR": Path(__file__).parent.resolve(),
},
components_settings={
"dirs": ["components"], "dirs": ["components"],
}, },
) )
def test_get_dirs__componenents_dirs__raises_on_relative_path_1(self): def test_get_dirs__componenents_dirs__raises_on_relative_path_1(self):
with self.assertRaisesMessage(ValueError, "COMPONENTS.dirs must contain absolute paths"): with pytest.raises(ValueError, match=re.escape("COMPONENTS.dirs must contain absolute paths")):
get_component_dirs() get_component_dirs()
@override_settings( @djc_test(
BASE_DIR=Path(__file__).parent.resolve(), django_settings={
COMPONENTS={ "BASE_DIR": Path(__file__).parent.resolve(),
},
components_settings={
"dirs": [("with_alias", "components")], "dirs": [("with_alias", "components")],
}, },
) )
def test_get_dirs__component_dirs__raises_on_relative_path_2(self): def test_get_dirs__component_dirs__raises_on_relative_path_2(self):
with self.assertRaisesMessage(ValueError, "COMPONENTS.dirs must contain absolute paths"): with pytest.raises(ValueError, match=re.escape("COMPONENTS.dirs must contain absolute paths")):
get_component_dirs() get_component_dirs()
@override_settings( @djc_test(
BASE_DIR=Path(__file__).parent.resolve(), django_settings={
COMPONENTS={ "BASE_DIR": Path(__file__).parent.resolve(),
},
components_settings={
"app_dirs": ["custom_comps_dir"], "app_dirs": ["custom_comps_dir"],
}, },
) )
@ -147,23 +157,22 @@ class ComponentDirsTest(BaseTestCase):
own_dirs = dirs[:1] own_dirs = dirs[:1]
# Apps with a `components` dir # Apps with a `components` dir
self.assertEqual(len(apps_dirs), 1) assert len(apps_dirs) == 1
# NOTE: Compare parts so that the test works on Windows too # NOTE: Compare parts so that the test works on Windows too
self.assertTupleEqual(apps_dirs[0].parts[-3:], ("tests", "test_app", "custom_comps_dir")) assert apps_dirs[0].parts[-3:] == ("tests", "test_app", "custom_comps_dir")
self.assertEqual( assert own_dirs == [
own_dirs,
[
# Top-level /components dir # Top-level /components dir
Path(__file__).parent.resolve() Path(__file__).parent.resolve()
/ "components", / "components",
], ]
)
@override_settings( @djc_test(
BASE_DIR=Path(__file__).parent.resolve(), django_settings={
COMPONENTS={ "BASE_DIR": Path(__file__).parent.resolve(),
},
components_settings={
"app_dirs": [], "app_dirs": [],
}, },
) )
@ -172,18 +181,17 @@ class ComponentDirsTest(BaseTestCase):
own_dirs = dirs own_dirs = dirs
self.assertEqual( assert own_dirs == [
own_dirs,
[
# Top-level /components dir # Top-level /components dir
Path(__file__).parent.resolve() Path(__file__).parent.resolve()
/ "components", / "components",
], ]
)
@override_settings( @djc_test(
BASE_DIR=Path(__file__).parent.resolve(), django_settings={
COMPONENTS={ "BASE_DIR": Path(__file__).parent.resolve(),
},
components_settings={
"app_dirs": ["this_dir_does_not_exist"], "app_dirs": ["this_dir_does_not_exist"],
}, },
) )
@ -192,18 +200,17 @@ class ComponentDirsTest(BaseTestCase):
own_dirs = dirs own_dirs = dirs
self.assertEqual( assert own_dirs == [
own_dirs,
[
# Top-level /components dir # Top-level /components dir
Path(__file__).parent.resolve() Path(__file__).parent.resolve()
/ "components", / "components",
], ]
)
@override_settings( @djc_test(
BASE_DIR=Path(__file__).parent.resolve(), django_settings={
INSTALLED_APPS=("django_components", "tests.test_app_nested.app"), "BASE_DIR": Path(__file__).parent.resolve(),
"INSTALLED_APPS": ("django_components", "tests.test_app_nested.app"),
},
) )
def test_get_dirs__nested_apps(self): def test_get_dirs__nested_apps(self):
dirs = sorted(get_component_dirs()) dirs = sorted(get_component_dirs())
@ -212,25 +219,25 @@ class ComponentDirsTest(BaseTestCase):
own_dirs = [dirs[1]] own_dirs = [dirs[1]]
# Apps with a `components` dir # Apps with a `components` dir
self.assertEqual(len(apps_dirs), 2) assert len(apps_dirs) == 2
# NOTE: Compare parts so that the test works on Windows too # NOTE: Compare parts so that the test works on Windows too
self.assertTupleEqual(apps_dirs[0].parts[-2:], ("django_components", "components")) assert apps_dirs[0].parts[-2:] == ("django_components", "components")
self.assertTupleEqual(apps_dirs[1].parts[-4:], ("tests", "test_app_nested", "app", "components")) assert apps_dirs[1].parts[-4:] == ("tests", "test_app_nested", "app", "components")
self.assertEqual( assert own_dirs == [
own_dirs,
[
# Top-level /components dir # Top-level /components dir
Path(__file__).parent.resolve() Path(__file__).parent.resolve()
/ "components", / "components",
], ]
)
class ComponentFilesTest(BaseTestCase): @djc_test
@override_settings( class TestComponentFiles:
BASE_DIR=Path(__file__).parent.resolve(), @djc_test(
django_settings={
"BASE_DIR": Path(__file__).parent.resolve(),
},
) )
def test_get_files__py(self): def test_get_files__py(self):
files = sorted(get_component_files(".py")) files = sorted(get_component_files(".py"))
@ -238,9 +245,7 @@ class ComponentFilesTest(BaseTestCase):
dot_paths = [f.dot_path for f in files] dot_paths = [f.dot_path for f in files]
file_paths = [f.filepath for f in files] file_paths = [f.filepath for f in files]
self.assertEqual( assert dot_paths == [
dot_paths,
[
"components", "components",
"components.multi_file.multi_file", "components.multi_file.multi_file",
"components.relative_file.relative_file", "components.relative_file.relative_file",
@ -251,27 +256,24 @@ class ComponentFilesTest(BaseTestCase):
"django_components.components", "django_components.components",
"django_components.components.dynamic", "django_components.components.dynamic",
"tests.test_app.components.app_lvl_comp.app_lvl_comp", "tests.test_app.components.app_lvl_comp.app_lvl_comp",
], ]
)
# NOTE: Compare parts so that the test works on Windows too # NOTE: Compare parts so that the test works on Windows too
self.assertTupleEqual(file_paths[0].parts[-3:], ("tests", "components", "__init__.py")) assert file_paths[0].parts[-3:] == ("tests", "components", "__init__.py")
self.assertTupleEqual(file_paths[1].parts[-4:], ("tests", "components", "multi_file", "multi_file.py")) assert file_paths[1].parts[-4:] == ("tests", "components", "multi_file", "multi_file.py")
self.assertTupleEqual(file_paths[2].parts[-4:], ("tests", "components", "relative_file", "relative_file.py")) assert file_paths[2].parts[-4:] == ("tests", "components", "relative_file", "relative_file.py")
self.assertTupleEqual( assert file_paths[3].parts[-4:] == ("tests", "components", "relative_file_pathobj", "relative_file_pathobj.py")
file_paths[3].parts[-4:], ("tests", "components", "relative_file_pathobj", "relative_file_pathobj.py") assert file_paths[4].parts[-3:] == ("tests", "components", "single_file.py")
) assert file_paths[5].parts[-4:] == ("tests", "components", "staticfiles", "staticfiles.py")
self.assertTupleEqual(file_paths[4].parts[-3:], ("tests", "components", "single_file.py")) assert file_paths[6].parts[-3:] == ("tests", "components", "urls.py")
self.assertTupleEqual(file_paths[5].parts[-4:], ("tests", "components", "staticfiles", "staticfiles.py")) assert file_paths[7].parts[-3:] == ("django_components", "components", "__init__.py")
self.assertTupleEqual(file_paths[6].parts[-3:], ("tests", "components", "urls.py")) assert file_paths[8].parts[-3:] == ("django_components", "components", "dynamic.py")
self.assertTupleEqual(file_paths[7].parts[-3:], ("django_components", "components", "__init__.py")) assert file_paths[9].parts[-5:] == ("tests", "test_app", "components", "app_lvl_comp", "app_lvl_comp.py")
self.assertTupleEqual(file_paths[8].parts[-3:], ("django_components", "components", "dynamic.py"))
self.assertTupleEqual(
file_paths[9].parts[-5:], ("tests", "test_app", "components", "app_lvl_comp", "app_lvl_comp.py")
)
@override_settings( @djc_test(
BASE_DIR=Path(__file__).parent.resolve(), django_settings={
"BASE_DIR": Path(__file__).parent.resolve(),
},
) )
def test_get_files__js(self): def test_get_files__js(self):
files = sorted(get_component_files(".js")) files = sorted(get_component_files(".js"))
@ -279,82 +281,52 @@ class ComponentFilesTest(BaseTestCase):
dot_paths = [f.dot_path for f in files] dot_paths = [f.dot_path for f in files]
file_paths = [f.filepath for f in files] file_paths = [f.filepath for f in files]
self.assertEqual( assert dot_paths == [
dot_paths,
[
"components.relative_file.relative_file", "components.relative_file.relative_file",
"components.relative_file_pathobj.relative_file_pathobj", "components.relative_file_pathobj.relative_file_pathobj",
"components.staticfiles.staticfiles", "components.staticfiles.staticfiles",
"tests.test_app.components.app_lvl_comp.app_lvl_comp", "tests.test_app.components.app_lvl_comp.app_lvl_comp",
], ]
)
# NOTE: Compare parts so that the test works on Windows too # NOTE: Compare parts so that the test works on Windows too
self.assertTupleEqual(file_paths[0].parts[-4:], ("tests", "components", "relative_file", "relative_file.js")) assert file_paths[0].parts[-4:] == ("tests", "components", "relative_file", "relative_file.js")
self.assertTupleEqual( assert file_paths[1].parts[-4:] == ("tests", "components", "relative_file_pathobj", "relative_file_pathobj.js")
file_paths[1].parts[-4:], ("tests", "components", "relative_file_pathobj", "relative_file_pathobj.js") assert file_paths[2].parts[-4:] == ("tests", "components", "staticfiles", "staticfiles.js")
) assert file_paths[3].parts[-5:] == ("tests", "test_app", "components", "app_lvl_comp", "app_lvl_comp.js")
self.assertTupleEqual(file_paths[2].parts[-4:], ("tests", "components", "staticfiles", "staticfiles.js"))
self.assertTupleEqual(
file_paths[3].parts[-5:], ("tests", "test_app", "components", "app_lvl_comp", "app_lvl_comp.js")
)
class TestFilepathToPythonModule(BaseTestCase): @djc_test
class TestFilepathToPythonModule:
def test_prepares_path(self): def test_prepares_path(self):
base_path = str(settings.BASE_DIR) base_path = str(settings.BASE_DIR)
the_path = os.path.join(base_path, "tests.py") the_path = os.path.join(base_path, "tests.py")
self.assertEqual( assert _filepath_to_python_module(the_path, base_path, None) == "tests"
_filepath_to_python_module(the_path, base_path, None),
"tests",
)
the_path = os.path.join(base_path, "tests/components/relative_file/relative_file.py") the_path = os.path.join(base_path, "tests/components/relative_file/relative_file.py")
self.assertEqual( assert _filepath_to_python_module(the_path, base_path, None) == "tests.components.relative_file.relative_file"
_filepath_to_python_module(the_path, base_path, None),
"tests.components.relative_file.relative_file",
)
def test_handles_separators_based_on_os_name(self): def test_handles_separators_based_on_os_name(self):
base_path = str(settings.BASE_DIR) base_path = str(settings.BASE_DIR)
with patch("os.name", new="posix"): with patch("os.name", new="posix"):
the_path = base_path + "/" + "tests.py" the_path = base_path + "/" + "tests.py"
self.assertEqual( assert _filepath_to_python_module(the_path, base_path, None) == "tests"
_filepath_to_python_module(the_path, base_path, None),
"tests",
)
the_path = base_path + "/" + "tests/components/relative_file/relative_file.py" the_path = base_path + "/" + "tests/components/relative_file/relative_file.py"
self.assertEqual( assert _filepath_to_python_module(the_path, base_path, None) == "tests.components.relative_file.relative_file" # noqa: E501
_filepath_to_python_module(the_path, base_path, None),
"tests.components.relative_file.relative_file",
)
base_path = str(settings.BASE_DIR).replace("/", "\\") base_path = str(settings.BASE_DIR).replace("/", "\\")
with patch("os.name", new="nt"): with patch("os.name", new="nt"):
the_path = base_path + "\\" + "tests.py" the_path = base_path + "\\" + "tests.py"
self.assertEqual( assert _filepath_to_python_module(the_path, base_path, None) == "tests"
_filepath_to_python_module(the_path, base_path, None),
"tests",
)
the_path = base_path + "\\" + "tests\\components\\relative_file\\relative_file.py" the_path = base_path + "\\" + "tests\\components\\relative_file\\relative_file.py"
self.assertEqual( assert _filepath_to_python_module(the_path, base_path, None) == "tests.components.relative_file.relative_file" # noqa: E501
_filepath_to_python_module(the_path, base_path, None),
"tests.components.relative_file.relative_file",
)
# NOTE: Windows should handle also POSIX separator # NOTE: Windows should handle also POSIX separator
the_path = base_path + "/" + "tests.py" the_path = base_path + "/" + "tests.py"
self.assertEqual( assert _filepath_to_python_module(the_path, base_path, None) == "tests"
_filepath_to_python_module(the_path, base_path, None),
"tests",
)
the_path = base_path + "/" + "tests/components/relative_file/relative_file.py" the_path = base_path + "/" + "tests/components/relative_file/relative_file.py"
self.assertEqual( assert _filepath_to_python_module(the_path, base_path, None) == "tests.components.relative_file.relative_file" # noqa: E501
_filepath_to_python_module(the_path, base_path, None),
"tests.components.relative_file.relative_file",
)

View file

@ -1,5 +1,6 @@
import inspect import inspect
import re
import pytest
from django.template import Context, Template from django.template import Context, Template
from django.template.exceptions import TemplateSyntaxError from django.template.exceptions import TemplateSyntaxError
@ -7,15 +8,16 @@ from django_components import types
from django_components.node import BaseNode, template_tag from django_components.node import BaseNode, template_tag
from django_components.templatetags import component_tags from django_components.templatetags import component_tags
from .django_test_setup import setup_test_config from django_components.testing import djc_test
from .testutils import BaseTestCase from .testutils import setup_test_config
setup_test_config({"autodiscover": False}) setup_test_config({"autodiscover": False})
class NodeTests(BaseTestCase): @djc_test
class TestNode:
def test_node_class_requires_tag(self): def test_node_class_requires_tag(self):
with self.assertRaises(ValueError): with pytest.raises(ValueError):
class CaptureNode(BaseNode): class CaptureNode(BaseNode):
pass pass
@ -41,14 +43,14 @@ class NodeTests(BaseTestCase):
template = Template(template_str) template = Template(template_str)
rendered = template.render(Context({})) rendered = template.render(Context({}))
self.assertEqual(rendered.strip(), "Hello, John!\n Shorthand: Hello, Mary!") assert rendered.strip() == "Hello, John!\n Shorthand: Hello, Mary!"
# But raises if missing end tag # But raises if missing end tag
template_str2: types.django_html = """ template_str2: types.django_html = """
{% load component_tags %} {% load component_tags %}
{% mytag 'John' %} {% mytag 'John' %}
""" """
with self.assertRaisesMessage(TemplateSyntaxError, "Unclosed tag on line 3: 'mytag'"): with pytest.raises(TemplateSyntaxError, match=re.escape("Unclosed tag on line 3: 'mytag'")):
Template(template_str2) Template(template_str2)
TestNode.unregister(component_tags.register) TestNode.unregister(component_tags.register)
@ -69,7 +71,7 @@ class NodeTests(BaseTestCase):
{% endmytag %} {% endmytag %}
Shorthand: {% mytag 'Mary' / %} Shorthand: {% mytag 'Mary' / %}
""" """
with self.assertRaisesMessage(TemplateSyntaxError, "Invalid block tag on line 4: 'endmytag'"): with pytest.raises(TemplateSyntaxError, match=re.escape("Invalid block tag on line 4: 'endmytag'")):
Template(template_str) Template(template_str)
# Works when missing end tag # Works when missing end tag
@ -79,7 +81,7 @@ class NodeTests(BaseTestCase):
""" """
template2 = Template(template_str2) template2 = Template(template_str2)
rendered2 = template2.render(Context({})) rendered2 = template2.render(Context({}))
self.assertEqual(rendered2.strip(), "Hello, John!") assert rendered2.strip() == "Hello, John!"
TestNode.unregister(component_tags.register) TestNode.unregister(component_tags.register)
@ -107,9 +109,9 @@ class NodeTests(BaseTestCase):
template.render(Context({})) template.render(Context({}))
allowed_flags, flags, active_flags = captured # type: ignore allowed_flags, flags, active_flags = captured # type: ignore
self.assertEqual(allowed_flags, ["required", "default"]) assert allowed_flags == ["required", "default"]
self.assertEqual(flags, {"required": True, "default": False}) assert flags == {"required": True, "default": False}
self.assertEqual(active_flags, ["required"]) assert active_flags == ["required"]
TestNode.unregister(component_tags.register) TestNode.unregister(component_tags.register)
@ -135,13 +137,16 @@ class NodeTests(BaseTestCase):
template = Template(template_str) template = Template(template_str)
rendered = template.render(Context({"name": "John"})) rendered = template.render(Context({"name": "John"}))
self.assertEqual(captured, {"False": False, "None": None, "True": True, "name": "John"}) assert captured == {"False": False, "None": None, "True": True, "name": "John"}
self.assertEqual(rendered.strip(), "Hello, John!") assert rendered.strip() == "Hello, John!"
TestNode.unregister(component_tags.register) TestNode.unregister(component_tags.register)
def test_node_render_raises_if_no_context_arg(self): def test_node_render_raises_if_no_context_arg(self):
with self.assertRaisesMessage(TypeError, "`render()` method of TestNode must have at least two parameters"): with pytest.raises(
TypeError,
match=re.escape("`render()` method of TestNode must have at least two parameters"),
):
class TestNode(BaseNode): class TestNode(BaseNode):
tag = "mytag" tag = "mytag"
@ -171,7 +176,7 @@ class NodeTests(BaseTestCase):
""" """
) )
template1.render(Context({})) template1.render(Context({}))
self.assertEqual(captured, ("John", 1, "Hello", "default")) assert captured == ("John", 1, "Hello", "default")
# Set all params # Set all params
template2 = Template( template2 = Template(
@ -181,7 +186,7 @@ class NodeTests(BaseTestCase):
""" """
) )
template2.render(Context({})) template2.render(Context({}))
self.assertEqual(captured, ("John2", 2, "Hello", "custom")) assert captured == ("John2", 2, "Hello", "custom")
# Set no params # Set no params
template3 = Template( template3 = Template(
@ -190,8 +195,9 @@ class NodeTests(BaseTestCase):
{% mytag %} {% mytag %}
""" """
) )
with self.assertRaisesMessage( with pytest.raises(
TypeError, "Invalid parameters for tag 'mytag': missing a required argument: 'name'" TypeError,
match=re.escape("Invalid parameters for tag 'mytag': missing a required argument: 'name'"),
): ):
template3.render(Context({})) template3.render(Context({}))
@ -202,8 +208,9 @@ class NodeTests(BaseTestCase):
{% mytag msg='Hello' %} {% mytag msg='Hello' %}
""" """
) )
with self.assertRaisesMessage( with pytest.raises(
TypeError, "Invalid parameters for tag 'mytag': missing a required argument: 'name'" TypeError,
match=re.escape("Invalid parameters for tag 'mytag': missing a required argument: 'name'"),
): ):
template4.render(Context({})) template4.render(Context({}))
@ -214,8 +221,9 @@ class NodeTests(BaseTestCase):
{% mytag name='John' %} {% mytag name='John' %}
""" """
) )
with self.assertRaisesMessage( with pytest.raises(
TypeError, "Invalid parameters for tag 'mytag': missing a required argument: 'msg'" TypeError,
match=re.escape("Invalid parameters for tag 'mytag': missing a required argument: 'msg'"),
): ):
template5.render(Context({})) template5.render(Context({}))
@ -226,8 +234,9 @@ class NodeTests(BaseTestCase):
{% mytag 123 count=1 name='John' %} {% mytag 123 count=1 name='John' %}
""" """
) )
with self.assertRaisesMessage( with pytest.raises(
TypeError, "Invalid parameters for tag 'mytag': got multiple values for argument 'name'" TypeError,
match=re.escape("Invalid parameters for tag 'mytag': got multiple values for argument 'name'"),
): ):
template6.render(Context({})) template6.render(Context({}))
@ -238,7 +247,10 @@ class NodeTests(BaseTestCase):
{% mytag count=1 name='John' 123 %} {% mytag count=1 name='John' 123 %}
""" """
) )
with self.assertRaisesMessage(TypeError, "positional argument follows keyword argument"): with pytest.raises(
TypeError,
match=re.escape("positional argument follows keyword argument"),
):
template6.render(Context({})) template6.render(Context({}))
# Extra kwargs # Extra kwargs
@ -248,8 +260,9 @@ class NodeTests(BaseTestCase):
{% mytag 'John' msg='Hello' mode='custom' var=123 %} {% mytag 'John' msg='Hello' mode='custom' var=123 %}
""" """
) )
with self.assertRaisesMessage( with pytest.raises(
TypeError, "Invalid parameters for tag 'mytag': got an unexpected keyword argument 'var'" TypeError,
match=re.escape("Invalid parameters for tag 'mytag': got an unexpected keyword argument 'var'"),
): ):
template7.render(Context({})) template7.render(Context({}))
@ -260,8 +273,9 @@ class NodeTests(BaseTestCase):
{% mytag 'John' msg='Hello' mode='custom' data-id=123 class="pa-4" @click.once="myVar" %} {% mytag 'John' msg='Hello' mode='custom' data-id=123 class="pa-4" @click.once="myVar" %}
""" """
) )
with self.assertRaisesMessage( with pytest.raises(
TypeError, "Invalid parameters for tag 'mytag': got an unexpected keyword argument 'data-id'" TypeError,
match=re.escape("Invalid parameters for tag 'mytag': got an unexpected keyword argument 'data-id'"),
): ):
template8.render(Context({})) template8.render(Context({}))
@ -272,7 +286,10 @@ class NodeTests(BaseTestCase):
{% mytag data-id=123 'John' msg='Hello' %} {% mytag data-id=123 'John' msg='Hello' %}
""" """
) )
with self.assertRaisesMessage(SyntaxError, "positional argument follows keyword argument"): with pytest.raises(
SyntaxError,
match=re.escape("positional argument follows keyword argument"),
):
template9.render(Context({})) template9.render(Context({}))
TestNode1.unregister(component_tags.register) TestNode1.unregister(component_tags.register)
@ -301,14 +318,11 @@ class NodeTests(BaseTestCase):
""" """
) )
template1.render(Context({})) template1.render(Context({}))
self.assertEqual( assert captured == (
captured,
(
"John", "John",
(123, 456, 789), (123, 456, 789),
"Hello", "Hello",
{"a": 1, "b": 2, "c": 3, "data-id": 123, "class": "pa-4", "@click.once": "myVar"}, {"a": 1, "b": 2, "c": 3, "data-id": 123, "class": "pa-4", "@click.once": "myVar"},
),
) )
TestNode1.unregister(component_tags.register) TestNode1.unregister(component_tags.register)
@ -343,17 +357,14 @@ class NodeTests(BaseTestCase):
template.render(Context({})) template.render(Context({}))
# All kwargs should be accepted since the function accepts **kwargs # All kwargs should be accepted since the function accepts **kwargs
self.assertEqual( assert captured == {
captured,
{
"name": "John", "name": "John",
"age": 25, "age": 25,
"data-id": 123, "data-id": 123,
"class": "header", "class": "header",
"@click": "handleClick", "@click": "handleClick",
"v-if": "isVisible", "v-if": "isVisible",
}, }
)
# Test with positional args (should fail since function only accepts kwargs) # Test with positional args (should fail since function only accepts kwargs)
template2 = Template( template2 = Template(
@ -362,17 +373,22 @@ class NodeTests(BaseTestCase):
{% mytag "John" name="Mary" %} {% mytag "John" name="Mary" %}
""" """
) )
with self.assertRaisesMessage( with pytest.raises(
TypeError, "Invalid parameters for tag 'mytag': takes 0 positional arguments but 1 was given" TypeError,
match=re.escape("Invalid parameters for tag 'mytag': takes 0 positional arguments but 1 was given"),
): ):
template2.render(Context({})) template2.render(Context({}))
TestNode.unregister(component_tags.register) TestNode.unregister(component_tags.register)
class DecoratorTests(BaseTestCase): @djc_test
class TestDecorator:
def test_decorator_requires_tag(self): def test_decorator_requires_tag(self):
with self.assertRaisesMessage(TypeError, "template_tag() missing 1 required positional argument: 'tag'"): with pytest.raises(
TypeError,
match=re.escape("template_tag() missing 1 required positional argument: 'tag'"),
):
@template_tag(component_tags.register) # type: ignore @template_tag(component_tags.register) # type: ignore
def mytag(node: BaseNode, context: Context) -> str: def mytag(node: BaseNode, context: Context) -> str:
@ -394,14 +410,17 @@ class DecoratorTests(BaseTestCase):
template = Template(template_str) template = Template(template_str)
rendered = template.render(Context({})) rendered = template.render(Context({}))
self.assertEqual(rendered.strip(), "Hello, John!\n Shorthand: Hello, Mary!") assert rendered.strip() == "Hello, John!\n Shorthand: Hello, Mary!"
# But raises if missing end tag # But raises if missing end tag
template_str2: types.django_html = """ template_str2: types.django_html = """
{% load component_tags %} {% load component_tags %}
{% mytag 'John' %} {% mytag 'John' %}
""" """
with self.assertRaisesMessage(TemplateSyntaxError, "Unclosed tag on line 3: 'mytag'"): with pytest.raises(
TemplateSyntaxError,
match=re.escape("Unclosed tag on line 3: 'mytag'"),
):
Template(template_str2) Template(template_str2)
render._node.unregister(component_tags.register) # type: ignore[attr-defined] render._node.unregister(component_tags.register) # type: ignore[attr-defined]
@ -418,7 +437,10 @@ class DecoratorTests(BaseTestCase):
{% endmytag %} {% endmytag %}
Shorthand: {% mytag 'Mary' / %} Shorthand: {% mytag 'Mary' / %}
""" """
with self.assertRaisesMessage(TemplateSyntaxError, "Invalid block tag on line 4: 'endmytag'"): with pytest.raises(
TemplateSyntaxError,
match=re.escape("Invalid block tag on line 4: 'endmytag'"),
):
Template(template_str) Template(template_str)
# Works when missing end tag # Works when missing end tag
@ -428,7 +450,7 @@ class DecoratorTests(BaseTestCase):
""" """
template2 = Template(template_str2) template2 = Template(template_str2)
rendered2 = template2.render(Context({})) rendered2 = template2.render(Context({}))
self.assertEqual(rendered2.strip(), "Hello, John!") assert rendered2.strip() == "Hello, John!"
render._node.unregister(component_tags.register) # type: ignore[attr-defined] render._node.unregister(component_tags.register) # type: ignore[attr-defined]
@ -456,15 +478,15 @@ class DecoratorTests(BaseTestCase):
template = Template(template_str) template = Template(template_str)
rendered = template.render(Context({"name": "John"})) rendered = template.render(Context({"name": "John"}))
self.assertEqual(captured, {"False": False, "None": None, "True": True, "name": "John"}) assert captured == {"False": False, "None": None, "True": True, "name": "John"}
self.assertEqual(rendered.strip(), "Hello, John!") assert rendered.strip() == "Hello, John!"
render._node.unregister(component_tags.register) # type: ignore[attr-defined] render._node.unregister(component_tags.register) # type: ignore[attr-defined]
def test_decorator_render_raises_if_no_context_arg(self): def test_decorator_render_raises_if_no_context_arg(self):
with self.assertRaisesMessage( with pytest.raises(
TypeError, TypeError,
"Failed to create node class in 'template_tag()' for 'render'", match=re.escape("Failed to create node class in 'template_tag()' for 'render'"),
): ):
@template_tag(component_tags.register, tag="mytag") # type: ignore @template_tag(component_tags.register, tag="mytag") # type: ignore
@ -490,7 +512,7 @@ class DecoratorTests(BaseTestCase):
""" """
) )
template1.render(Context({})) template1.render(Context({}))
self.assertEqual(captured, ("John", 1, "Hello", "default")) assert captured == ("John", 1, "Hello", "default")
# Set all params # Set all params
template2 = Template( template2 = Template(
@ -500,7 +522,7 @@ class DecoratorTests(BaseTestCase):
""" """
) )
template2.render(Context({})) template2.render(Context({}))
self.assertEqual(captured, ("John2", 2, "Hello", "custom")) assert captured == ("John2", 2, "Hello", "custom")
# Set no params # Set no params
template3 = Template( template3 = Template(
@ -509,8 +531,9 @@ class DecoratorTests(BaseTestCase):
{% mytag %} {% mytag %}
""" """
) )
with self.assertRaisesMessage( with pytest.raises(
TypeError, "Invalid parameters for tag 'mytag': missing a required argument: 'name'" TypeError,
match=re.escape("Invalid parameters for tag 'mytag': missing a required argument: 'name'"),
): ):
template3.render(Context({})) template3.render(Context({}))
@ -521,8 +544,9 @@ class DecoratorTests(BaseTestCase):
{% mytag msg='Hello' %} {% mytag msg='Hello' %}
""" """
) )
with self.assertRaisesMessage( with pytest.raises(
TypeError, "Invalid parameters for tag 'mytag': missing a required argument: 'name'" TypeError,
match=re.escape("Invalid parameters for tag 'mytag': missing a required argument: 'name'"),
): ):
template4.render(Context({})) template4.render(Context({}))
@ -533,8 +557,9 @@ class DecoratorTests(BaseTestCase):
{% mytag name='John' %} {% mytag name='John' %}
""" """
) )
with self.assertRaisesMessage( with pytest.raises(
TypeError, "Invalid parameters for tag 'mytag': missing a required argument: 'msg'" TypeError,
match=re.escape("Invalid parameters for tag 'mytag': missing a required argument: 'msg'"),
): ):
template5.render(Context({})) template5.render(Context({}))
@ -545,8 +570,9 @@ class DecoratorTests(BaseTestCase):
{% mytag 123 count=1 name='John' %} {% mytag 123 count=1 name='John' %}
""" """
) )
with self.assertRaisesMessage( with pytest.raises(
TypeError, "Invalid parameters for tag 'mytag': got multiple values for argument 'name'" TypeError,
match=re.escape("Invalid parameters for tag 'mytag': got multiple values for argument 'name'"),
): ):
template6.render(Context({})) template6.render(Context({}))
@ -557,7 +583,10 @@ class DecoratorTests(BaseTestCase):
{% mytag count=1 name='John' 123 %} {% mytag count=1 name='John' 123 %}
""" """
) )
with self.assertRaisesMessage(TypeError, "positional argument follows keyword argument"): with pytest.raises(
TypeError,
match=re.escape("positional argument follows keyword argument"),
):
template6.render(Context({})) template6.render(Context({}))
# Extra kwargs # Extra kwargs
@ -567,8 +596,9 @@ class DecoratorTests(BaseTestCase):
{% mytag 'John' msg='Hello' mode='custom' var=123 %} {% mytag 'John' msg='Hello' mode='custom' var=123 %}
""" """
) )
with self.assertRaisesMessage( with pytest.raises(
TypeError, "Invalid parameters for tag 'mytag': got an unexpected keyword argument 'var'" TypeError,
match=re.escape("Invalid parameters for tag 'mytag': got an unexpected keyword argument 'var'"),
): ):
template7.render(Context({})) template7.render(Context({}))
@ -579,8 +609,9 @@ class DecoratorTests(BaseTestCase):
{% mytag 'John' msg='Hello' mode='custom' data-id=123 class="pa-4" @click.once="myVar" %} {% mytag 'John' msg='Hello' mode='custom' data-id=123 class="pa-4" @click.once="myVar" %}
""" """
) )
with self.assertRaisesMessage( with pytest.raises(
TypeError, "Invalid parameters for tag 'mytag': got an unexpected keyword argument 'data-id'" TypeError,
match=re.escape("Invalid parameters for tag 'mytag': got an unexpected keyword argument 'data-id'"),
): ):
template8.render(Context({})) template8.render(Context({}))
@ -591,7 +622,10 @@ class DecoratorTests(BaseTestCase):
{% mytag data-id=123 'John' msg='Hello' %} {% mytag data-id=123 'John' msg='Hello' %}
""" """
) )
with self.assertRaisesMessage(SyntaxError, "positional argument follows keyword argument"): with pytest.raises(
SyntaxError,
match=re.escape("positional argument follows keyword argument"),
):
template9.render(Context({})) template9.render(Context({}))
render._node.unregister(component_tags.register) # type: ignore[attr-defined] render._node.unregister(component_tags.register) # type: ignore[attr-defined]
@ -615,14 +649,11 @@ class DecoratorTests(BaseTestCase):
""" """
) )
template1.render(Context({})) template1.render(Context({}))
self.assertEqual( assert captured == (
captured,
(
"John", "John",
(123, 456, 789), (123, 456, 789),
"Hello", "Hello",
{"a": 1, "b": 2, "c": 3, "data-id": 123, "class": "pa-4", "@click.once": "myVar"}, {"a": 1, "b": 2, "c": 3, "data-id": 123, "class": "pa-4", "@click.once": "myVar"},
),
) )
render._node.unregister(component_tags.register) # type: ignore[attr-defined] render._node.unregister(component_tags.register) # type: ignore[attr-defined]
@ -653,17 +684,14 @@ class DecoratorTests(BaseTestCase):
template.render(Context({})) template.render(Context({}))
# All kwargs should be accepted since the function accepts **kwargs # All kwargs should be accepted since the function accepts **kwargs
self.assertEqual( assert captured == {
captured,
{
"name": "John", "name": "John",
"age": 25, "age": 25,
"data-id": 123, "data-id": 123,
"class": "header", "class": "header",
"@click": "handleClick", "@click": "handleClick",
"v-if": "isVisible", "v-if": "isVisible",
}, }
)
# Test with positional args (should fail since function only accepts kwargs) # Test with positional args (should fail since function only accepts kwargs)
template2 = Template( template2 = Template(
@ -672,8 +700,9 @@ class DecoratorTests(BaseTestCase):
{% mytag "John" name="Mary" %} {% mytag "John" name="Mary" %}
""" """
) )
with self.assertRaisesMessage( with pytest.raises(
TypeError, "Invalid parameters for tag 'mytag': takes 0 positional arguments but 1 was given" TypeError,
match=re.escape("Invalid parameters for tag 'mytag': takes 0 positional arguments but 1 was given"),
): ):
template2.render(Context({})) template2.render(Context({}))
@ -704,7 +733,8 @@ def force_signature_validation(fn):
return SignatureOnlyFunction(fn) return SignatureOnlyFunction(fn)
class SignatureBasedValidationTests(BaseTestCase): @djc_test
class TestSignatureBasedValidation:
# Test that the template tag can be used within the template under the registered tag # Test that the template tag can be used within the template under the registered tag
def test_node_class_tags(self): def test_node_class_tags(self):
class TestNode(BaseNode): class TestNode(BaseNode):
@ -727,14 +757,17 @@ class SignatureBasedValidationTests(BaseTestCase):
template = Template(template_str) template = Template(template_str)
rendered = template.render(Context({})) rendered = template.render(Context({}))
self.assertEqual(rendered.strip(), "Hello, John!\n Shorthand: Hello, Mary!") assert rendered.strip() == "Hello, John!\n Shorthand: Hello, Mary!"
# But raises if missing end tag # But raises if missing end tag
template_str2: types.django_html = """ template_str2: types.django_html = """
{% load component_tags %} {% load component_tags %}
{% mytag 'John' %} {% mytag 'John' %}
""" """
with self.assertRaisesMessage(TemplateSyntaxError, "Unclosed tag on line 3: 'mytag'"): with pytest.raises(
TemplateSyntaxError,
match=re.escape("Unclosed tag on line 3: 'mytag'"),
):
Template(template_str2) Template(template_str2)
TestNode.unregister(component_tags.register) TestNode.unregister(component_tags.register)
@ -756,7 +789,10 @@ class SignatureBasedValidationTests(BaseTestCase):
{% endmytag %} {% endmytag %}
Shorthand: {% mytag 'Mary' / %} Shorthand: {% mytag 'Mary' / %}
""" """
with self.assertRaisesMessage(TemplateSyntaxError, "Invalid block tag on line 4: 'endmytag'"): with pytest.raises(
TemplateSyntaxError,
match=re.escape("Invalid block tag on line 4: 'endmytag'"),
):
Template(template_str) Template(template_str)
# Works when missing end tag # Works when missing end tag
@ -766,7 +802,7 @@ class SignatureBasedValidationTests(BaseTestCase):
""" """
template2 = Template(template_str2) template2 = Template(template_str2)
rendered2 = template2.render(Context({})) rendered2 = template2.render(Context({}))
self.assertEqual(rendered2.strip(), "Hello, John!") assert rendered2.strip() == "Hello, John!"
TestNode.unregister(component_tags.register) TestNode.unregister(component_tags.register)
@ -794,9 +830,9 @@ class SignatureBasedValidationTests(BaseTestCase):
template.render(Context({})) template.render(Context({}))
allowed_flags, flags, active_flags = captured # type: ignore allowed_flags, flags, active_flags = captured # type: ignore
self.assertEqual(allowed_flags, ["required", "default"]) assert allowed_flags == ["required", "default"]
self.assertEqual(flags, {"required": True, "default": False}) assert flags == {"required": True, "default": False}
self.assertEqual(active_flags, ["required"]) assert active_flags == ["required"]
TestNode.unregister(component_tags.register) TestNode.unregister(component_tags.register)
@ -822,13 +858,16 @@ class SignatureBasedValidationTests(BaseTestCase):
template = Template(template_str) template = Template(template_str)
rendered = template.render(Context({"name": "John"})) rendered = template.render(Context({"name": "John"}))
self.assertEqual(captured, {"False": False, "None": None, "True": True, "name": "John"}) assert captured == {"False": False, "None": None, "True": True, "name": "John"}
self.assertEqual(rendered.strip(), "Hello, John!") assert rendered.strip() == "Hello, John!"
TestNode.unregister(component_tags.register) TestNode.unregister(component_tags.register)
def test_node_render_raises_if_no_context_arg(self): def test_node_render_raises_if_no_context_arg(self):
with self.assertRaisesMessage(TypeError, "`render()` method of TestNode must have at least two parameters"): with pytest.raises(
TypeError,
match=re.escape("`render()` method of TestNode must have at least two parameters"),
):
class TestNode(BaseNode): class TestNode(BaseNode):
tag = "mytag" tag = "mytag"
@ -859,7 +898,7 @@ class SignatureBasedValidationTests(BaseTestCase):
""" """
) )
template1.render(Context({})) template1.render(Context({}))
self.assertEqual(captured, ("John", 1, "Hello", "default")) assert captured == ("John", 1, "Hello", "default")
# Set all params # Set all params
template2 = Template( template2 = Template(
@ -869,7 +908,7 @@ class SignatureBasedValidationTests(BaseTestCase):
""" """
) )
template2.render(Context({})) template2.render(Context({}))
self.assertEqual(captured, ("John2", 2, "Hello", "custom")) assert captured == ("John2", 2, "Hello", "custom")
# Set no params # Set no params
template3 = Template( template3 = Template(
@ -878,8 +917,9 @@ class SignatureBasedValidationTests(BaseTestCase):
{% mytag %} {% mytag %}
""" """
) )
with self.assertRaisesMessage( with pytest.raises(
TypeError, "Invalid parameters for tag 'mytag': missing a required argument: 'name'" TypeError,
match=re.escape("Invalid parameters for tag 'mytag': missing a required argument: 'name'"),
): ):
template3.render(Context({})) template3.render(Context({}))
@ -890,8 +930,9 @@ class SignatureBasedValidationTests(BaseTestCase):
{% mytag msg='Hello' %} {% mytag msg='Hello' %}
""" """
) )
with self.assertRaisesMessage( with pytest.raises(
TypeError, "Invalid parameters for tag 'mytag': missing a required argument: 'name'" TypeError,
match=re.escape("Invalid parameters for tag 'mytag': missing a required argument: 'name'"),
): ):
template4.render(Context({})) template4.render(Context({}))
@ -902,8 +943,9 @@ class SignatureBasedValidationTests(BaseTestCase):
{% mytag name='John' %} {% mytag name='John' %}
""" """
) )
with self.assertRaisesMessage( with pytest.raises(
TypeError, "Invalid parameters for tag 'mytag': missing a required argument: 'msg'" TypeError,
match=re.escape("Invalid parameters for tag 'mytag': missing a required argument: 'msg'"),
): ):
template5.render(Context({})) template5.render(Context({}))
@ -914,8 +956,9 @@ class SignatureBasedValidationTests(BaseTestCase):
{% mytag 123 count=1 name='John' %} {% mytag 123 count=1 name='John' %}
""" """
) )
with self.assertRaisesMessage( with pytest.raises(
TypeError, "Invalid parameters for tag 'mytag': got multiple values for argument 'name'" TypeError,
match=re.escape("Invalid parameters for tag 'mytag': got multiple values for argument 'name'"),
): ):
template6.render(Context({})) template6.render(Context({}))
@ -926,7 +969,10 @@ class SignatureBasedValidationTests(BaseTestCase):
{% mytag count=1 name='John' 123 %} {% mytag count=1 name='John' 123 %}
""" """
) )
with self.assertRaisesMessage(TypeError, "positional argument follows keyword argument"): with pytest.raises(
TypeError,
match=re.escape("positional argument follows keyword argument"),
):
template6.render(Context({})) template6.render(Context({}))
# Extra kwargs # Extra kwargs
@ -936,8 +982,9 @@ class SignatureBasedValidationTests(BaseTestCase):
{% mytag 'John' msg='Hello' mode='custom' var=123 %} {% mytag 'John' msg='Hello' mode='custom' var=123 %}
""" """
) )
with self.assertRaisesMessage( with pytest.raises(
TypeError, "Invalid parameters for tag 'mytag': got an unexpected keyword argument 'var'" TypeError,
match=re.escape("Invalid parameters for tag 'mytag': got an unexpected keyword argument 'var'"),
): ):
template7.render(Context({})) template7.render(Context({}))
@ -948,8 +995,9 @@ class SignatureBasedValidationTests(BaseTestCase):
{% mytag 'John' msg='Hello' mode='custom' data-id=123 class="pa-4" @click.once="myVar" %} {% mytag 'John' msg='Hello' mode='custom' data-id=123 class="pa-4" @click.once="myVar" %}
""" """
) )
with self.assertRaisesMessage( with pytest.raises(
TypeError, "Invalid parameters for tag 'mytag': got an unexpected keyword argument 'data-id'" TypeError,
match=re.escape("Invalid parameters for tag 'mytag': got an unexpected keyword argument 'data-id'"),
): ):
template8.render(Context({})) template8.render(Context({}))
@ -960,7 +1008,10 @@ class SignatureBasedValidationTests(BaseTestCase):
{% mytag data-id=123 'John' msg='Hello' %} {% mytag data-id=123 'John' msg='Hello' %}
""" """
) )
with self.assertRaisesMessage(SyntaxError, "positional argument follows keyword argument"): with pytest.raises(
SyntaxError,
match=re.escape("positional argument follows keyword argument"),
):
template9.render(Context({})) template9.render(Context({}))
TestNode1.unregister(component_tags.register) TestNode1.unregister(component_tags.register)
@ -990,14 +1041,11 @@ class SignatureBasedValidationTests(BaseTestCase):
""" """
) )
template1.render(Context({})) template1.render(Context({}))
self.assertEqual( assert captured == (
captured,
(
"John", "John",
(123, 456, 789), (123, 456, 789),
"Hello", "Hello",
{"a": 1, "b": 2, "c": 3, "data-id": 123, "class": "pa-4", "@click.once": "myVar"}, {"a": 1, "b": 2, "c": 3, "data-id": 123, "class": "pa-4", "@click.once": "myVar"},
),
) )
TestNode1.unregister(component_tags.register) TestNode1.unregister(component_tags.register)
@ -1033,17 +1081,14 @@ class SignatureBasedValidationTests(BaseTestCase):
template.render(Context({})) template.render(Context({}))
# All kwargs should be accepted since the function accepts **kwargs # All kwargs should be accepted since the function accepts **kwargs
self.assertEqual( assert captured == {
captured,
{
"name": "John", "name": "John",
"age": 25, "age": 25,
"data-id": 123, "data-id": 123,
"class": "header", "class": "header",
"@click": "handleClick", "@click": "handleClick",
"v-if": "isVisible", "v-if": "isVisible",
}, }
)
# Test with positional args (should fail since function only accepts kwargs) # Test with positional args (should fail since function only accepts kwargs)
template2 = Template( template2 = Template(
@ -1052,8 +1097,9 @@ class SignatureBasedValidationTests(BaseTestCase):
{% mytag "John" name="Mary" %} {% mytag "John" name="Mary" %}
""" """
) )
with self.assertRaisesMessage( with pytest.raises(
TypeError, "Invalid parameters for tag 'mytag': takes 0 positional arguments but 1 was given" TypeError,
match=re.escape("Invalid parameters for tag 'mytag': takes 0 positional arguments but 1 was given"),
): ):
template2.render(Context({})) template2.render(Context({}))

View file

@ -1,7 +1,5 @@
import unittest import pytest
from django.template import Context, Engine, Library, Template from django.template import Context, Engine, Library, Template
from django.test import override_settings
from django_components import ( from django_components import (
AlreadyRegistered, AlreadyRegistered,
@ -17,9 +15,10 @@ from django_components import (
registry, registry,
types, types,
) )
from pytest_django.asserts import assertHTMLEqual
from .django_test_setup import setup_test_config from django_components.testing import djc_test
from .testutils import BaseTestCase, parametrize_context_behavior from .testutils import PARAMETRIZE_CONTEXT_BEHAVIOR, setup_test_config
setup_test_config({"autodiscover": False}) setup_test_config({"autodiscover": False})
@ -37,17 +36,14 @@ class MockComponentView(Component):
pass pass
class ComponentRegistryTest(unittest.TestCase): @djc_test
def setUp(self): class TestComponentRegistry:
super().setUp()
self.registry = ComponentRegistry()
def test_register_class_decorator(self): def test_register_class_decorator(self):
@register("decorated_component") @register("decorated_component")
class TestComponent(Component): class TestComponent(Component):
pass pass
self.assertEqual(registry.get("decorated_component"), TestComponent) assert registry.get("decorated_component") == TestComponent
# Cleanup # Cleanup
registry.unregister("decorated_component") registry.unregister("decorated_component")
@ -58,94 +54,95 @@ class ComponentRegistryTest(unittest.TestCase):
default_registry_comps_before = len(registry.all()) default_registry_comps_before = len(registry.all())
self.assertDictEqual(my_reg.all(), {}) assert my_reg.all() == {}
@register("decorated_component", registry=my_reg) @register("decorated_component", registry=my_reg)
class TestComponent(Component): class TestComponent(Component):
pass pass
self.assertDictEqual(my_reg.all(), {"decorated_component": TestComponent}) assert my_reg.all() == {"decorated_component": TestComponent}
# Check that the component was NOT added to the default registry # Check that the component was NOT added to the default registry
default_registry_comps_after = len(registry.all()) default_registry_comps_after = len(registry.all())
self.assertEqual(default_registry_comps_before, default_registry_comps_after) assert default_registry_comps_before == default_registry_comps_after
def test_simple_register(self): def test_simple_register(self):
self.registry.register(name="testcomponent", component=MockComponent) custom_registry = ComponentRegistry()
self.assertEqual(self.registry.all(), {"testcomponent": MockComponent}) custom_registry.register(name="testcomponent", component=MockComponent)
assert custom_registry.all() == {"testcomponent": MockComponent}
def test_register_two_components(self): def test_register_two_components(self):
self.registry.register(name="testcomponent", component=MockComponent) custom_registry = ComponentRegistry()
self.registry.register(name="testcomponent2", component=MockComponent) custom_registry.register(name="testcomponent", component=MockComponent)
self.assertEqual( custom_registry.register(name="testcomponent2", component=MockComponent)
self.registry.all(), assert custom_registry.all() == {
{
"testcomponent": MockComponent, "testcomponent": MockComponent,
"testcomponent2": MockComponent, "testcomponent2": MockComponent,
}, }
)
def test_unregisters_only_unused_tags(self): def test_unregisters_only_unused_tags(self):
self.assertDictEqual(self.registry._tags, {}) custom_library = Library()
custom_registry = ComponentRegistry(library=custom_library)
assert custom_registry._tags == {}
# NOTE: We preserve the default component tags # NOTE: We preserve the default component tags
self.assertNotIn("component", self.registry.library.tags) assert "component" not in custom_registry.library.tags
# Register two components that use the same tag # Register two components that use the same tag
self.registry.register(name="testcomponent", component=MockComponent) custom_registry.register(name="testcomponent", component=MockComponent)
self.registry.register(name="testcomponent2", component=MockComponent) custom_registry.register(name="testcomponent2", component=MockComponent)
self.assertDictEqual( assert custom_registry._tags == {
self.registry._tags,
{
"component": {"testcomponent", "testcomponent2"}, "component": {"testcomponent", "testcomponent2"},
}, }
)
self.assertIn("component", self.registry.library.tags) assert "component" in custom_registry.library.tags
# Unregister only one of the components. The tags should remain # Unregister only one of the components. The tags should remain
self.registry.unregister(name="testcomponent") custom_registry.unregister(name="testcomponent")
self.assertDictEqual( assert custom_registry._tags == {
self.registry._tags,
{
"component": {"testcomponent2"}, "component": {"testcomponent2"},
}, }
)
self.assertIn("component", self.registry.library.tags) assert "component" in custom_registry.library.tags
# Unregister the second components. The tags should be removed # Unregister the second components. The tags should be removed
self.registry.unregister(name="testcomponent2") custom_registry.unregister(name="testcomponent2")
self.assertDictEqual(self.registry._tags, {}) assert custom_registry._tags == {}
self.assertNotIn("component", self.registry.library.tags) assert "component" not in custom_registry.library.tags
def test_prevent_registering_different_components_with_the_same_name(self): def test_prevent_registering_different_components_with_the_same_name(self):
self.registry.register(name="testcomponent", component=MockComponent) custom_registry = ComponentRegistry()
with self.assertRaises(AlreadyRegistered): custom_registry.register(name="testcomponent", component=MockComponent)
self.registry.register(name="testcomponent", component=MockComponent2) with pytest.raises(AlreadyRegistered):
custom_registry.register(name="testcomponent", component=MockComponent2)
def test_allow_duplicated_registration_of_the_same_component(self): def test_allow_duplicated_registration_of_the_same_component(self):
custom_registry = ComponentRegistry()
try: try:
self.registry.register(name="testcomponent", component=MockComponentView) custom_registry.register(name="testcomponent", component=MockComponentView)
self.registry.register(name="testcomponent", component=MockComponentView) custom_registry.register(name="testcomponent", component=MockComponentView)
except AlreadyRegistered: except AlreadyRegistered:
self.fail("Should not raise AlreadyRegistered") pytest.fail("Should not raise AlreadyRegistered")
def test_simple_unregister(self): def test_simple_unregister(self):
self.registry.register(name="testcomponent", component=MockComponent) custom_registry = ComponentRegistry()
self.registry.unregister(name="testcomponent") custom_registry.register(name="testcomponent", component=MockComponent)
self.assertEqual(self.registry.all(), {}) custom_registry.unregister(name="testcomponent")
assert custom_registry.all() == {}
def test_raises_on_failed_unregister(self): def test_raises_on_failed_unregister(self):
with self.assertRaises(NotRegistered): custom_registry = ComponentRegistry()
self.registry.unregister(name="testcomponent") with pytest.raises(NotRegistered):
custom_registry.unregister(name="testcomponent")
class MultipleComponentRegistriesTest(BaseTestCase): @djc_test
@parametrize_context_behavior(["django", "isolated"]) class TestMultipleComponentRegistries:
def test_different_registries_have_different_settings(self): @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_different_registries_have_different_settings(self, components_settings):
library_a = Library() library_a = Library()
registry_a = ComponentRegistry( registry_a = ComponentRegistry(
library=library_a, library=library_a,
@ -200,7 +197,7 @@ class MultipleComponentRegistriesTest(BaseTestCase):
rendered = template.render(Context({})) rendered = template.render(Context({}))
self.assertHTMLEqual( assertHTMLEqual(
rendered, rendered,
""" """
Variable: <strong data-djc-id-a1bc40>123</strong> Variable: <strong data-djc-id-a1bc40>123</strong>
@ -218,14 +215,15 @@ class MultipleComponentRegistriesTest(BaseTestCase):
engine.template_builtins.remove(library_b) engine.template_builtins.remove(library_b)
class ProtectedTagsTest(unittest.TestCase): @djc_test
def setUp(self): class TestProtectedTags:
super().setUp()
self.registry = ComponentRegistry()
# NOTE: Use the `component_shorthand_formatter` formatter, so the components # NOTE: Use the `component_shorthand_formatter` formatter, so the components
# are registered under that tag # are registered under that tag
@override_settings(COMPONENTS={"tag_formatter": "django_components.component_shorthand_formatter"}) @djc_test(
components_settings={
"tag_formatter": "django_components.component_shorthand_formatter",
},
)
def test_raises_on_overriding_our_tags(self): def test_raises_on_overriding_our_tags(self):
for tag in [ for tag in [
"component_css_dependencies", "component_css_dependencies",
@ -235,7 +233,7 @@ class ProtectedTagsTest(unittest.TestCase):
"provide", "provide",
"slot", "slot",
]: ]:
with self.assertRaises(TagProtectedError): with pytest.raises(TagProtectedError):
@register(tag) @register(tag)
class TestComponent(Component): class TestComponent(Component):

View file

@ -1,38 +1,66 @@
import re
from pathlib import Path from pathlib import Path
import pytest
from django.test import override_settings from django.test import override_settings
from django_components import ComponentsSettings from django_components.app_settings import ComponentsSettings, app_settings
from django_components.app_settings import app_settings
from .django_test_setup import setup_test_config from django_components.testing import djc_test
from .testutils import BaseTestCase from .testutils import setup_test_config
setup_test_config(components={"autodiscover": False}) setup_test_config(components={"autodiscover": False})
class SettingsTestCase(BaseTestCase): @djc_test
@override_settings(COMPONENTS={"context_behavior": "isolated"}) class TestSettings:
@djc_test(
components_settings={
"context_behavior": "isolated",
},
)
def test_valid_context_behavior(self): def test_valid_context_behavior(self):
self.assertEqual(app_settings.CONTEXT_BEHAVIOR, "isolated") assert app_settings.CONTEXT_BEHAVIOR == "isolated"
@override_settings(COMPONENTS={"context_behavior": "invalid_value"}) # NOTE: Since the part that we want to test here is otherwise part of the test setup
# this test places the `override_settings` and `_load_settings` (which is called by `djc_test`)
# inside the test.
def test_raises_on_invalid_context_behavior(self): def test_raises_on_invalid_context_behavior(self):
with self.assertRaises(ValueError): with override_settings(COMPONENTS={"context_behavior": "invalid_value"}):
app_settings.CONTEXT_BEHAVIOR with pytest.raises(
ValueError,
match=re.escape("Invalid context behavior: invalid_value. Valid options are ['django', 'isolated']"),
):
app_settings._load_settings()
@override_settings(BASE_DIR="base_dir") @djc_test(
django_settings={
"BASE_DIR": "base_dir",
},
)
def test_works_when_base_dir_is_string(self): def test_works_when_base_dir_is_string(self):
self.assertEqual(app_settings.DIRS, [Path("base_dir/components")]) assert app_settings.DIRS == [Path("base_dir/components")]
@override_settings(BASE_DIR=Path("base_dir")) @djc_test(
django_settings={
"BASE_DIR": Path("base_dir"),
},
)
def test_works_when_base_dir_is_path(self): def test_works_when_base_dir_is_path(self):
self.assertEqual(app_settings.DIRS, [Path("base_dir/components")]) assert app_settings.DIRS == [Path("base_dir/components")]
@override_settings(COMPONENTS={"context_behavior": "isolated"}) @djc_test(
components_settings={
"context_behavior": "isolated",
},
)
def test_settings_as_dict(self): def test_settings_as_dict(self):
self.assertEqual(app_settings.CONTEXT_BEHAVIOR, "isolated") assert app_settings.CONTEXT_BEHAVIOR == "isolated"
@override_settings(COMPONENTS=ComponentsSettings(context_behavior="isolated")) # NOTE: Since the part that we want to test here is otherwise part of the test setup
# this test places the `override_settings` and `_load_settings` (which is called by `djc_test`)
# inside the test.
def test_settings_as_instance(self): def test_settings_as_instance(self):
self.assertEqual(app_settings.CONTEXT_BEHAVIOR, "isolated") with override_settings(COMPONENTS=ComponentsSettings(context_behavior="isolated")):
app_settings._load_settings()
assert app_settings.CONTEXT_BEHAVIOR == "isolated"

View file

@ -1,11 +1,11 @@
from typing import Callable from functools import wraps
from django.template import Context, Template from django.template import Context, Template
from django_components import Component, register, registry, types from django_components import Component, registry, types
from .django_test_setup import setup_test_config from django_components.testing import djc_test
from .testutils import BaseTestCase, parametrize_context_behavior from .testutils import PARAMETRIZE_CONTEXT_BEHAVIOR, setup_test_config
setup_test_config({"autodiscover": False}) setup_test_config({"autodiscover": False})
@ -29,27 +29,26 @@ def _get_templates_used_to_render(subject_template, render_context=None):
return templates_used return templates_used
class TemplateSignalTest(BaseTestCase): def with_template_signal(func):
saved_render_method: Callable # Assigned during setup. @wraps(func)
def wrapper(*args, **kwargs):
def tearDown(self): # Emulate Django test instrumentation for TestCase (see setup_test_environment)
super().tearDown()
Template._render = self.saved_render_method
def setUp(self):
"""Emulate Django test instrumentation for TestCase (see setup_test_environment)"""
super().setUp()
from django.test.utils import instrumented_test_render from django.test.utils import instrumented_test_render
from django.template import Template
self.saved_render_method = Template._render original_template_render = Template._render
Template._render = instrumented_test_render Template._render = instrumented_test_render
registry.clear() func(*args, **kwargs)
registry.register("test_component", SlottedComponent)
@register("inner_component") Template._render = original_template_render
class SimpleComponent(Component):
return wrapper
@djc_test
class TestTemplateSignal:
class InnerComponent(Component):
template_file = "simple_template.html" template_file = "simple_template.html"
def get_context_data(self, variable, variable2="default"): def get_context_data(self, variable, variable2="default"):
@ -62,18 +61,24 @@ class TemplateSignalTest(BaseTestCase):
css = "style.css" css = "style.css"
js = "script.js" js = "script.js"
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_template_rendered(self): @with_template_signal
def test_template_rendered(self, components_settings):
registry.register("test_component", SlottedComponent)
registry.register("inner_component", self.InnerComponent)
template_str: types.django_html = """ template_str: types.django_html = """
{% load component_tags %} {% load component_tags %}
{% component 'test_component' %}{% endcomponent %} {% component 'test_component' %}{% endcomponent %}
""" """
template = Template(template_str, name="root") template = Template(template_str, name="root")
templates_used = _get_templates_used_to_render(template) templates_used = _get_templates_used_to_render(template)
self.assertIn("slotted_template.html", templates_used) assert "slotted_template.html" in templates_used
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_template_rendered_nested_components(self): @with_template_signal
def test_template_rendered_nested_components(self, components_settings):
registry.register("test_component", SlottedComponent)
registry.register("inner_component", self.InnerComponent)
template_str: types.django_html = """ template_str: types.django_html = """
{% load component_tags %} {% load component_tags %}
{% component 'test_component' %} {% component 'test_component' %}
@ -84,5 +89,5 @@ class TemplateSignalTest(BaseTestCase):
""" """
template = Template(template_str, name="root") template = Template(template_str, name="root")
templates_used = _get_templates_used_to_render(template) templates_used = _get_templates_used_to_render(template)
self.assertIn("slotted_template.html", templates_used) assert "slotted_template.html" in templates_used
self.assertIn("simple_template.html", templates_used) assert "simple_template.html" in templates_used

View file

@ -1,10 +1,14 @@
import re
import pytest
from django.template import Context, Template from django.template import Context, Template
from pytest_django.asserts import assertHTMLEqual
from django_components import Component, register, types from django_components import Component, register, types
from django_components.tag_formatter import ShorthandComponentFormatter from django_components.tag_formatter import ShorthandComponentFormatter
from .django_test_setup import setup_test_config from django_components.testing import djc_test
from .testutils import BaseTestCase, parametrize_context_behavior from .testutils import PARAMETRIZE_CONTEXT_BEHAVIOR, setup_test_config
setup_test_config({"autodiscover": False}) setup_test_config({"autodiscover": False})
@ -43,9 +47,10 @@ def create_validator_tag_formatter(tag_name: str):
return ValidatorTagFormatter() return ValidatorTagFormatter()
class ComponentTagTests(BaseTestCase): @djc_test
@parametrize_context_behavior(["django", "isolated"]) class TestComponentTag:
def test_formatter_default_inline(self): @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_formatter_default_inline(self, components_settings):
@register("simple") @register("simple")
class SimpleComponent(Component): class SimpleComponent(Component):
template: types.django_html = """ template: types.django_html = """
@ -64,7 +69,7 @@ class ComponentTagTests(BaseTestCase):
""" """
) )
rendered = template.render(Context()) rendered = template.render(Context())
self.assertHTMLEqual( assertHTMLEqual(
rendered, rendered,
""" """
hello1 hello1
@ -75,8 +80,8 @@ class ComponentTagTests(BaseTestCase):
""", """,
) )
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_formatter_default_block(self): def test_formatter_default_block(self, components_settings):
@register("simple") @register("simple")
class SimpleComponent(Component): class SimpleComponent(Component):
template: types.django_html = """ template: types.django_html = """
@ -97,7 +102,7 @@ class ComponentTagTests(BaseTestCase):
""" """
) )
rendered = template.render(Context()) rendered = template.render(Context())
self.assertHTMLEqual( assertHTMLEqual(
rendered, rendered,
""" """
hello1 hello1
@ -108,15 +113,13 @@ class ComponentTagTests(BaseTestCase):
""", """,
) )
@parametrize_context_behavior( @djc_test(
cases=["django", "isolated"], parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR,
settings={ components_settings={
"COMPONENTS": {
"tag_formatter": "django_components.component_formatter", "tag_formatter": "django_components.component_formatter",
}, },
},
) )
def test_formatter_component_inline(self): def test_formatter_component_inline(self, components_settings):
@register("simple") @register("simple")
class SimpleComponent(Component): class SimpleComponent(Component):
template: types.django_html = """ template: types.django_html = """
@ -135,7 +138,7 @@ class ComponentTagTests(BaseTestCase):
""" """
) )
rendered = template.render(Context()) rendered = template.render(Context())
self.assertHTMLEqual( assertHTMLEqual(
rendered, rendered,
""" """
hello1 hello1
@ -146,15 +149,13 @@ class ComponentTagTests(BaseTestCase):
""", """,
) )
@parametrize_context_behavior( @djc_test(
cases=["django", "isolated"], parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR,
settings={ components_settings={
"COMPONENTS": {
"tag_formatter": "django_components.component_formatter", "tag_formatter": "django_components.component_formatter",
}, },
},
) )
def test_formatter_component_block(self): def test_formatter_component_block(self, components_settings):
@register("simple") @register("simple")
class SimpleComponent(Component): class SimpleComponent(Component):
template: types.django_html = """ template: types.django_html = """
@ -175,7 +176,7 @@ class ComponentTagTests(BaseTestCase):
""" """
) )
rendered = template.render(Context()) rendered = template.render(Context())
self.assertHTMLEqual( assertHTMLEqual(
rendered, rendered,
""" """
hello1 hello1
@ -186,15 +187,13 @@ class ComponentTagTests(BaseTestCase):
""", """,
) )
@parametrize_context_behavior( @djc_test(
cases=["django", "isolated"], parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR,
settings={ components_settings={
"COMPONENTS": {
"tag_formatter": "django_components.component_shorthand_formatter", "tag_formatter": "django_components.component_shorthand_formatter",
}, },
},
) )
def test_formatter_shorthand_inline(self): def test_formatter_shorthand_inline(self, components_settings):
@register("simple") @register("simple")
class SimpleComponent(Component): class SimpleComponent(Component):
template: types.django_html = """ template: types.django_html = """
@ -213,7 +212,7 @@ class ComponentTagTests(BaseTestCase):
""" """
) )
rendered = template.render(Context()) rendered = template.render(Context())
self.assertHTMLEqual( assertHTMLEqual(
rendered, rendered,
""" """
hello1 hello1
@ -224,15 +223,13 @@ class ComponentTagTests(BaseTestCase):
""", """,
) )
@parametrize_context_behavior( @djc_test(
cases=["django", "isolated"], parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR,
settings={ components_settings={
"COMPONENTS": {
"tag_formatter": "django_components.component_shorthand_formatter", "tag_formatter": "django_components.component_shorthand_formatter",
}, },
},
) )
def test_formatter_shorthand_block(self): def test_formatter_shorthand_block(self, components_settings):
@register("simple") @register("simple")
class SimpleComponent(Component): class SimpleComponent(Component):
template: types.django_html = """ template: types.django_html = """
@ -253,7 +250,7 @@ class ComponentTagTests(BaseTestCase):
""" """
) )
rendered = template.render(Context()) rendered = template.render(Context())
self.assertHTMLEqual( assertHTMLEqual(
rendered, rendered,
""" """
hello1 hello1
@ -264,15 +261,13 @@ class ComponentTagTests(BaseTestCase):
""", """,
) )
@parametrize_context_behavior( @djc_test(
cases=["django", "isolated"], parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR,
settings={ components_settings={
"COMPONENTS": {
"tag_formatter": SlashEndTagFormatter(), "tag_formatter": SlashEndTagFormatter(),
}, },
},
) )
def test_forward_slash_in_end_tag(self): def test_forward_slash_in_end_tag(self, components_settings):
@register("simple") @register("simple")
class SimpleComponent(Component): class SimpleComponent(Component):
template: types.django_html = """ template: types.django_html = """
@ -293,7 +288,7 @@ class ComponentTagTests(BaseTestCase):
""" """
) )
rendered = template.render(Context()) rendered = template.render(Context())
self.assertHTMLEqual( assertHTMLEqual(
rendered, rendered,
""" """
hello1 hello1
@ -304,15 +299,13 @@ class ComponentTagTests(BaseTestCase):
""", """,
) )
@parametrize_context_behavior( @djc_test(
cases=["django", "isolated"], parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR,
settings={ components_settings={
"COMPONENTS": {
"tag_formatter": ShorthandComponentFormatter(), "tag_formatter": ShorthandComponentFormatter(),
}, },
},
) )
def test_import_formatter_by_value(self): def test_import_formatter_by_value(self, components_settings):
@register("simple") @register("simple")
class SimpleComponent(Component): class SimpleComponent(Component):
template: types.django_html = """ template: types.django_html = """
@ -331,7 +324,7 @@ class ComponentTagTests(BaseTestCase):
""" """
) )
rendered = template.render(Context()) rendered = template.render(Context())
self.assertHTMLEqual( assertHTMLEqual(
rendered, rendered,
""" """
<div data-djc-id-a1bc3f> <div data-djc-id-a1bc3f>
@ -340,34 +333,32 @@ class ComponentTagTests(BaseTestCase):
""", """,
) )
@parametrize_context_behavior( @djc_test(
cases=["django", "isolated"], parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR,
settings={ components_settings={
"COMPONENTS": {
"tag_formatter": MultiwordStartTagFormatter(), "tag_formatter": MultiwordStartTagFormatter(),
}, },
},
) )
def test_raises_on_invalid_start_tag(self): def test_raises_on_invalid_start_tag(self, components_settings):
with self.assertRaisesMessage( with pytest.raises(
ValueError, "MultiwordStartTagFormatter returned an invalid tag for start_tag: 'simple comp'" ValueError,
match=re.escape("MultiwordStartTagFormatter returned an invalid tag for start_tag: 'simple comp'"),
): ):
@register("simple") @register("simple")
class SimpleComponent(Component): class SimpleComponent(Component):
template = """{% load component_tags %}""" template = """{% load component_tags %}"""
@parametrize_context_behavior( @djc_test(
cases=["django", "isolated"], parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR,
settings={ components_settings={
"COMPONENTS": {
"tag_formatter": MultiwordBlockEndTagFormatter(), "tag_formatter": MultiwordBlockEndTagFormatter(),
}, },
},
) )
def test_raises_on_invalid_block_end_tag(self): def test_raises_on_invalid_block_end_tag(self, components_settings):
with self.assertRaisesMessage( with pytest.raises(
ValueError, "MultiwordBlockEndTagFormatter returned an invalid tag for end_tag: 'end simple'" ValueError,
match=re.escape("MultiwordBlockEndTagFormatter returned an invalid tag for end_tag: 'end simple'"),
): ):
@register("simple") @register("simple")
@ -388,15 +379,13 @@ class ComponentTagTests(BaseTestCase):
""" """
) )
@parametrize_context_behavior( @djc_test(
cases=["django", "isolated"], parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR,
settings={ components_settings={
"COMPONENTS": {
"tag_formatter": create_validator_tag_formatter("simple"), "tag_formatter": create_validator_tag_formatter("simple"),
}, },
},
) )
def test_method_args(self): def test_method_args(self, components_settings):
@register("simple") @register("simple")
class SimpleComponent(Component): class SimpleComponent(Component):
template: types.django_html = """ template: types.django_html = """
@ -415,7 +404,7 @@ class ComponentTagTests(BaseTestCase):
""" """
) )
rendered = template.render(Context()) rendered = template.render(Context())
self.assertHTMLEqual( assertHTMLEqual(
rendered, rendered,
""" """
hello1 hello1
@ -435,7 +424,7 @@ class ComponentTagTests(BaseTestCase):
""" """
) )
rendered = template.render(Context()) rendered = template.render(Context())
self.assertHTMLEqual( assertHTMLEqual(
rendered, rendered,
""" """
hello1 hello1

View file

@ -1,5 +1,7 @@
import re
from unittest import skip from unittest import skip
import pytest
from django.template import Context, Template, TemplateSyntaxError from django.template import Context, Template, TemplateSyntaxError
from django.template.base import Parser from django.template.base import Parser
from django.template.engine import Engine from django.template.engine import Engine
@ -7,8 +9,8 @@ from django.template.engine import Engine
from django_components import Component, register, types from django_components import Component, register, types
from django_components.util.tag_parser import TagAttr, TagValue, TagValuePart, TagValueStruct, parse_tag from django_components.util.tag_parser import TagAttr, TagValue, TagValuePart, TagValueStruct, parse_tag
from .django_test_setup import setup_test_config from django_components.testing import djc_test
from .testutils import BaseTestCase from .testutils import setup_test_config
setup_test_config({"autodiscover": False}) setup_test_config({"autodiscover": False})
@ -24,7 +26,8 @@ def _get_parser() -> Parser:
) )
class TagParserTests(BaseTestCase): @djc_test
class TestTagParser:
def test_args_kwargs(self): def test_args_kwargs(self):
_, attrs = parse_tag("component 'my_comp' key=val key2='val2 two' ", None) _, attrs = parse_tag("component 'my_comp' key=val key2='val2 two' ", None)
@ -99,16 +102,13 @@ class TagParserTests(BaseTestCase):
), ),
] ]
self.assertEqual(attrs, expected_attrs) assert attrs == expected_attrs
self.assertEqual( assert [a.serialize() for a in attrs] == [
[a.serialize() for a in attrs],
[
"component", "component",
"'my_comp'", "'my_comp'",
"key=val", "key=val",
"key2='val2 two'", "key2='val2 two'",
], ]
)
def test_nested_quotes(self): def test_nested_quotes(self):
_, attrs = parse_tag("component 'my_comp' key=val key2='val2 \"two\"' text=\"organisation's\" ", None) _, attrs = parse_tag("component 'my_comp' key=val key2='val2 \"two\"' text=\"organisation's\" ", None)
@ -205,17 +205,14 @@ class TagParserTests(BaseTestCase):
), ),
] ]
self.assertEqual(attrs, expected_attrs) assert attrs == expected_attrs
self.assertEqual( assert [a.serialize() for a in attrs] == [
[a.serialize() for a in attrs],
[
"component", "component",
"'my_comp'", "'my_comp'",
"key=val", "key=val",
"key2='val2 \"two\"'", "key2='val2 \"two\"'",
'text="organisation\'s"', 'text="organisation\'s"',
], ]
)
def test_trailing_quote_single(self): def test_trailing_quote_single(self):
_, attrs = parse_tag("component 'my_comp' key=val key2='val2 \"two\"' text=\"organisation's\" 'abc", None) _, attrs = parse_tag("component 'my_comp' key=val key2='val2 \"two\"' text=\"organisation's\" 'abc", None)
@ -329,18 +326,15 @@ class TagParserTests(BaseTestCase):
), ),
] ]
self.assertEqual(attrs, expected_attrs) assert attrs == expected_attrs
self.assertEqual( assert [a.serialize() for a in attrs] == [
[a.serialize() for a in attrs],
[
"component", "component",
"'my_comp'", "'my_comp'",
"key=val", "key=val",
"key2='val2 \"two\"'", "key2='val2 \"two\"'",
'text="organisation\'s"', 'text="organisation\'s"',
"'abc", "'abc",
], ]
)
def test_trailing_quote_double(self): def test_trailing_quote_double(self):
_, attrs = parse_tag('component "my_comp" key=val key2="val2 \'two\'" text=\'organisation"s\' "abc', None) _, attrs = parse_tag('component "my_comp" key=val key2="val2 \'two\'" text=\'organisation"s\' "abc', None)
@ -454,18 +448,15 @@ class TagParserTests(BaseTestCase):
), ),
] ]
self.assertEqual(attrs, expected_attrs) assert attrs == expected_attrs
self.assertEqual( assert [a.serialize() for a in attrs] == [
[a.serialize() for a in attrs],
[
"component", "component",
'"my_comp"', '"my_comp"',
"key=val", "key=val",
"key2=\"val2 'two'\"", "key2=\"val2 'two'\"",
"text='organisation\"s'", "text='organisation\"s'",
'"abc', '"abc',
], ]
)
def test_trailing_quote_as_value_single(self): def test_trailing_quote_as_value_single(self):
_, attrs = parse_tag( _, attrs = parse_tag(
@ -582,18 +573,15 @@ class TagParserTests(BaseTestCase):
), ),
] ]
self.assertEqual(attrs, expected_attrs) assert attrs == expected_attrs
self.assertEqual( assert [a.serialize() for a in attrs] == [
[a.serialize() for a in attrs],
[
"component", "component",
"'my_comp'", "'my_comp'",
"key=val", "key=val",
"key2='val2 \"two\"'", "key2='val2 \"two\"'",
'text="organisation\'s"', 'text="organisation\'s"',
"value='abc", "value='abc",
], ]
)
def test_trailing_quote_as_value_double(self): def test_trailing_quote_as_value_double(self):
_, attrs = parse_tag( _, attrs = parse_tag(
@ -710,18 +698,15 @@ class TagParserTests(BaseTestCase):
), ),
] ]
self.assertEqual(attrs, expected_attrs) assert attrs == expected_attrs
self.assertEqual( assert [a.serialize() for a in attrs] == [
[a.serialize() for a in attrs],
[
"component", "component",
'"my_comp"', '"my_comp"',
"key=val", "key=val",
"key2=\"val2 'two'\"", "key2=\"val2 'two'\"",
"text='organisation\"s'", "text='organisation\"s'",
'value="abc', 'value="abc',
], ]
)
def test_translation(self): def test_translation(self):
_, attrs = parse_tag('component "my_comp" _("one") key=_("two")', None) _, attrs = parse_tag('component "my_comp" _("one") key=_("two")', None)
@ -795,16 +780,13 @@ class TagParserTests(BaseTestCase):
), ),
] ]
self.assertEqual(attrs, expected_attrs) assert attrs == expected_attrs
self.assertEqual( assert [a.serialize() for a in attrs] == [
[a.serialize() for a in attrs],
[
"component", "component",
'"my_comp"', '"my_comp"',
'_("one")', '_("one")',
'key=_("two")', 'key=_("two")',
], ]
)
def test_tag_parser_filters(self): def test_tag_parser_filters(self):
_, attrs = parse_tag( _, attrs = parse_tag(
@ -908,17 +890,14 @@ class TagParserTests(BaseTestCase):
), ),
] ]
self.assertEqual(attrs, expected_attrs) assert attrs == expected_attrs
self.assertEqual( assert [a.serialize() for a in attrs] == [
[a.serialize() for a in attrs],
[
"component", "component",
'"my_comp"', '"my_comp"',
"value|lower", "value|lower",
'key=val|yesno:"yes,no"', 'key=val|yesno:"yes,no"',
'key2=val2|default:"N/A"|upper', 'key2=val2|default:"N/A"|upper',
], ]
)
def test_translation_whitespace(self): def test_translation_whitespace(self):
_, attrs = parse_tag('component value=_( "test" )', None) _, attrs = parse_tag('component value=_( "test" )', None)
@ -961,7 +940,7 @@ class TagParserTests(BaseTestCase):
start_index=10, start_index=10,
), ),
] ]
self.assertEqual(attrs, expected_attrs) assert attrs == expected_attrs
def test_filter_whitespace(self): def test_filter_whitespace(self):
_, attrs = parse_tag("component value | lower key=val | upper key2=val2", None) _, attrs = parse_tag("component value | lower key=val | upper key2=val2", None)
@ -1041,12 +1020,12 @@ class TagParserTests(BaseTestCase):
), ),
] ]
self.assertEqual(attrs, expected_attrs) assert attrs == expected_attrs
def test_filter_argument_must_follow_filter(self): def test_filter_argument_must_follow_filter(self):
with self.assertRaisesMessage( with pytest.raises(
TemplateSyntaxError, TemplateSyntaxError,
"Filter argument (':arg') must follow a filter ('|filter')", match=re.escape("Filter argument (':arg') must follow a filter ('|filter')"),
): ):
parse_tag('component value=val|yesno:"yes,no":arg', None) parse_tag('component value=val|yesno:"yes,no":arg', None)
@ -1097,7 +1076,7 @@ class TagParserTests(BaseTestCase):
), ),
] ]
self.assertEqual(attrs, expected_attrs) assert attrs == expected_attrs
def test_dict_trailing_comma(self): def test_dict_trailing_comma(self):
_, attrs = parse_tag('component data={ "key": "val", }', None) _, attrs = parse_tag('component data={ "key": "val", }', None)
@ -1146,18 +1125,27 @@ class TagParserTests(BaseTestCase):
), ),
] ]
self.assertEqual(attrs, expected_attrs) assert attrs == expected_attrs
def test_dict_missing_colon(self): def test_dict_missing_colon(self):
with self.assertRaisesMessage(TemplateSyntaxError, "Dictionary key is missing a value"): with pytest.raises(
TemplateSyntaxError,
match=re.escape("Dictionary key is missing a value"),
):
parse_tag('component data={ "key" }', None) parse_tag('component data={ "key" }', None)
def test_dict_missing_colon_2(self): def test_dict_missing_colon_2(self):
with self.assertRaisesMessage(TemplateSyntaxError, "Dictionary key is missing a value"): with pytest.raises(
TemplateSyntaxError,
match=re.escape("Dictionary key is missing a value"),
):
parse_tag('component data={ "key", "val" }', None) parse_tag('component data={ "key", "val" }', None)
def test_dict_extra_colon(self): def test_dict_extra_colon(self):
with self.assertRaisesMessage(TemplateSyntaxError, "Unexpected colon"): with pytest.raises(
TemplateSyntaxError,
match=re.escape("Unexpected colon"),
):
_, attrs = parse_tag("component data={ key:: key }", None) _, attrs = parse_tag("component data={ key:: key }", None)
def test_dict_spread(self): def test_dict_spread(self):
@ -1202,7 +1190,7 @@ class TagParserTests(BaseTestCase):
), ),
] ]
self.assertEqual(attrs, expected_attrs) assert attrs == expected_attrs
def test_dict_spread_between_key_value_pairs(self): def test_dict_spread_between_key_value_pairs(self):
_, attrs = parse_tag('component data={ "key": val, **spread, "key2": val2 }', None) _, attrs = parse_tag('component data={ "key": val, **spread, "key2": val2 }', None)
@ -1266,14 +1254,15 @@ class TagParserTests(BaseTestCase):
), ),
] ]
self.assertEqual(attrs, expected_attrs) assert attrs == expected_attrs
# Test that dictionary keys cannot have filter arguments - The `:` is parsed as dictionary key separator # Test that dictionary keys cannot have filter arguments - The `:` is parsed as dictionary key separator
# So instead, the content below will be parsed as key `"key"|filter`, and value `"arg":"value"' # So instead, the content below will be parsed as key `"key"|filter`, and value `"arg":"value"'
# And the latter is invalid because it's missing the `|` separator. # And the latter is invalid because it's missing the `|` separator.
def test_colon_in_dictionary_keys(self): def test_colon_in_dictionary_keys(self):
with self.assertRaisesMessage( with pytest.raises(
TemplateSyntaxError, "Filter argument (':arg') must follow a filter ('|filter')" TemplateSyntaxError,
match=re.escape("Filter argument (':arg') must follow a filter ('|filter')"),
): ):
_, attrs = parse_tag('component data={"key"|filter:"arg": "value"}', None) _, attrs = parse_tag('component data={"key"|filter:"arg": "value"}', None)
@ -1329,7 +1318,7 @@ class TagParserTests(BaseTestCase):
), ),
] ]
self.assertEqual(attrs, expected_attrs) assert attrs == expected_attrs
def test_list_trailing_comma(self): def test_list_trailing_comma(self):
_, attrs = parse_tag("component data=[1, 2, 3, ]", None) _, attrs = parse_tag("component data=[1, 2, 3, ]", None)
@ -1383,7 +1372,7 @@ class TagParserTests(BaseTestCase):
), ),
] ]
self.assertEqual(attrs, expected_attrs) assert attrs == expected_attrs
def test_lists_complex(self): def test_lists_complex(self):
_, attrs = parse_tag( _, attrs = parse_tag(
@ -1552,16 +1541,13 @@ class TagParserTests(BaseTestCase):
), ),
] ]
self.assertEqual(attrs, expected_attrs) assert attrs == expected_attrs
self.assertEqual( assert [a.serialize() for a in attrs] == [
[a.serialize() for a in attrs],
[
"component", "component",
"nums=[1, 2|add:3, *spread]", "nums=[1, 2|add:3, *spread]",
'items=["a"|upper, \'b\'|lower, c|default:"d"]', 'items=["a"|upper, \'b\'|lower, c|default:"d"]',
'mixed=[1, [*nested], {"key": "val"}]', 'mixed=[1, [*nested], {"key": "val"}]',
], ]
)
def test_dicts_complex(self): def test_dicts_complex(self):
_, attrs = parse_tag( _, attrs = parse_tag(
@ -1728,16 +1714,13 @@ class TagParserTests(BaseTestCase):
), ),
] ]
self.assertEqual(attrs, expected_attrs) assert attrs == expected_attrs
self.assertEqual( assert [a.serialize() for a in attrs] == [
[a.serialize() for a in attrs],
[
"component", "component",
'simple={"a": 1|add:2}', 'simple={"a": 1|add:2}',
'nested={"key"|upper: val|lower, **spread, "obj": {"x": 1|add:2}}', 'nested={"key"|upper: val|lower, **spread, "obj": {"x": 1|add:2}}',
'filters={"a"|lower: "b"|upper, c|default: "e"|yesno:"yes,no"}', 'filters={"a"|lower: "b"|upper, c|default: "e"|yesno:"yes,no"}',
], ]
)
def test_mixed_complex(self): def test_mixed_complex(self):
_, attrs = parse_tag( _, attrs = parse_tag(
@ -2053,42 +2036,39 @@ class TagParserTests(BaseTestCase):
), ),
] ]
self.assertEqual(attrs, expected_attrs) assert attrs == expected_attrs
self.assertEqual( assert [a.serialize() for a in attrs] == [
[a.serialize() for a in attrs],
[
"component", "component",
'data={"items": [1|add:2, {"x"|upper: 2|add:3}, *spread_items|default:""], "nested": {"a": [1|add:2, *nums|default:""], "b": {"x": [*more|default:""]}}, **rest|default, "key": _(\'value\')|upper}', # noqa: E501 'data={"items": [1|add:2, {"x"|upper: 2|add:3}, *spread_items|default:""], "nested": {"a": [1|add:2, *nums|default:""], "b": {"x": [*more|default:""]}}, **rest|default, "key": _(\'value\')|upper}', # noqa: E501
], ]
)
# Test that spread operator cannot be used as dictionary value # Test that spread operator cannot be used as dictionary value
def test_spread_as_dictionary_value(self): def test_spread_as_dictionary_value(self):
with self.assertRaisesMessage( with pytest.raises(
TemplateSyntaxError, TemplateSyntaxError,
"Spread syntax cannot be used in place of a dictionary value", match=re.escape("Spread syntax cannot be used in place of a dictionary value"),
): ):
parse_tag('component data={"key": **spread}', None) parse_tag('component data={"key": **spread}', None)
def test_spread_with_colon_interpreted_as_key(self): def test_spread_with_colon_interpreted_as_key(self):
with self.assertRaisesMessage( with pytest.raises(
TemplateSyntaxError, TemplateSyntaxError,
"Spread syntax cannot be used in place of a dictionary key", match=re.escape("Spread syntax cannot be used in place of a dictionary key"),
): ):
_, attrs = parse_tag("component data={**spread|abc: 123 }", None) _, attrs = parse_tag("component data={**spread|abc: 123 }", None)
def test_spread_in_filter_position(self): def test_spread_in_filter_position(self):
with self.assertRaisesMessage( with pytest.raises(
TemplateSyntaxError, TemplateSyntaxError,
"Spread syntax cannot be used inside of a filter", match=re.escape("Spread syntax cannot be used inside of a filter"),
): ):
_, attrs = parse_tag("component data=val|...spread|abc }", None) _, attrs = parse_tag("component data=val|...spread|abc }", None)
def test_spread_whitespace(self): def test_spread_whitespace(self):
# NOTE: Separating `...` from its variable is NOT valid, and will result in error. # NOTE: Separating `...` from its variable is NOT valid, and will result in error.
with self.assertRaisesMessage( with pytest.raises(
TemplateSyntaxError, TemplateSyntaxError,
"Spread syntax '...' is missing a value", match=re.escape("Spread syntax '...' is missing a value"),
): ):
_, attrs = parse_tag("component ... attrs", None) _, attrs = parse_tag("component ... attrs", None)
@ -2166,75 +2146,75 @@ class TagParserTests(BaseTestCase):
start_index=38, start_index=38,
), ),
] ]
self.assertEqual(attrs, expected_attrs) assert attrs == expected_attrs
# Test that one cannot use e.g. `...`, `**`, `*` in wrong places # Test that one cannot use e.g. `...`, `**`, `*` in wrong places
def test_spread_incorrect_syntax(self): def test_spread_incorrect_syntax(self):
with self.assertRaisesMessage( with pytest.raises(
TemplateSyntaxError, TemplateSyntaxError,
"Spread syntax '*' found outside of a list", match=re.escape("Spread syntax '*' found outside of a list"),
): ):
_, attrs = parse_tag('component dict={"a": "b", *my_attr}', None) _, attrs = parse_tag('component dict={"a": "b", *my_attr}', None)
with self.assertRaisesMessage( with pytest.raises(
TemplateSyntaxError, TemplateSyntaxError,
"Spread syntax '...' found in dict. It must be used on tag attributes only", match=re.escape("Spread syntax '...' found in dict. It must be used on tag attributes only"),
): ):
_, attrs = parse_tag('component dict={"a": "b", ...my_attr}', None) _, attrs = parse_tag('component dict={"a": "b", ...my_attr}', None)
with self.assertRaisesMessage( with pytest.raises(
TemplateSyntaxError, TemplateSyntaxError,
"Spread syntax '**' found outside of a dictionary", match=re.escape("Spread syntax '**' found outside of a dictionary"),
): ):
_, attrs = parse_tag('component list=["a", "b", **my_list]', None) _, attrs = parse_tag('component list=["a", "b", **my_list]', None)
with self.assertRaisesMessage( with pytest.raises(
TemplateSyntaxError, TemplateSyntaxError,
"Spread syntax '...' found in list. It must be used on tag attributes only", match=re.escape("Spread syntax '...' found in list. It must be used on tag attributes only"),
): ):
_, attrs = parse_tag('component list=["a", "b", ...my_list]', None) _, attrs = parse_tag('component list=["a", "b", ...my_list]', None)
with self.assertRaisesMessage( with pytest.raises(
TemplateSyntaxError, TemplateSyntaxError,
"Spread syntax '*' found outside of a list", match=re.escape("Spread syntax '*' found outside of a list"),
): ):
_, attrs = parse_tag("component *attrs", None) _, attrs = parse_tag("component *attrs", None)
with self.assertRaisesMessage( with pytest.raises(
TemplateSyntaxError, TemplateSyntaxError,
"Spread syntax '**' found outside of a dictionary", match=re.escape("Spread syntax '**' found outside of a dictionary"),
): ):
_, attrs = parse_tag("component **attrs", None) _, attrs = parse_tag("component **attrs", None)
with self.assertRaisesMessage( with pytest.raises(
TemplateSyntaxError, TemplateSyntaxError,
"Spread syntax '*' found outside of a list", match=re.escape("Spread syntax '*' found outside of a list"),
): ):
_, attrs = parse_tag("component key=*attrs", None) _, attrs = parse_tag("component key=*attrs", None)
with self.assertRaisesMessage( with pytest.raises(
TemplateSyntaxError, TemplateSyntaxError,
"Spread syntax '**' found outside of a dictionary", match=re.escape("Spread syntax '**' found outside of a dictionary"),
): ):
_, attrs = parse_tag("component key=**attrs", None) _, attrs = parse_tag("component key=**attrs", None)
# Test that one cannot do `key=...{"a": "b"}` # Test that one cannot do `key=...{"a": "b"}`
def test_spread_onto_key(self): def test_spread_onto_key(self):
with self.assertRaisesMessage( with pytest.raises(
TemplateSyntaxError, TemplateSyntaxError,
"Spread syntax '...' cannot follow a key ('key=...attrs')", match=re.escape("Spread syntax '...' cannot follow a key ('key=...attrs')"),
): ):
_, attrs = parse_tag('component key=...{"a": "b"}', None) _, attrs = parse_tag('component key=...{"a": "b"}', None)
with self.assertRaisesMessage( with pytest.raises(
TemplateSyntaxError, TemplateSyntaxError,
"Spread syntax '...' cannot follow a key ('key=...attrs')", match=re.escape("Spread syntax '...' cannot follow a key ('key=...attrs')"),
): ):
_, attrs = parse_tag('component key=...["a", "b"]', None) _, attrs = parse_tag('component key=...["a", "b"]', None)
with self.assertRaisesMessage( with pytest.raises(
TemplateSyntaxError, TemplateSyntaxError,
"Spread syntax '...' cannot follow a key ('key=...attrs')", match=re.escape("Spread syntax '...' cannot follow a key ('key=...attrs')"),
): ):
_, attrs = parse_tag("component key=...attrs", None) _, attrs = parse_tag("component key=...attrs", None)
@ -2312,15 +2292,12 @@ class TagParserTests(BaseTestCase):
start_index=10, start_index=10,
), ),
] ]
self.assertEqual(attrs, expected_attrs) assert attrs == expected_attrs
self.assertEqual( assert [a.serialize() for a in attrs] == [
[a.serialize() for a in attrs],
[
"component", "component",
'{**{"key": val2}, "key": val1}', '{**{"key": val2}, "key": val1}',
], ]
)
def test_spread_dict_literal_as_attribute(self): def test_spread_dict_literal_as_attribute(self):
_, attrs = parse_tag('component ...{"key": val2}', None) _, attrs = parse_tag('component ...{"key": val2}', None)
@ -2366,15 +2343,12 @@ class TagParserTests(BaseTestCase):
start_index=10, start_index=10,
), ),
] ]
self.assertEqual(attrs, expected_attrs) assert attrs == expected_attrs
self.assertEqual( assert [a.serialize() for a in attrs] == [
[a.serialize() for a in attrs],
[
"component", "component",
'...{"key": val2}', '...{"key": val2}',
], ]
)
def test_spread_list_literal_nested(self): def test_spread_list_literal_nested(self):
_, attrs = parse_tag("component [ *[val1], val2 ]", None) _, attrs = parse_tag("component [ *[val1], val2 ]", None)
@ -2436,15 +2410,12 @@ class TagParserTests(BaseTestCase):
start_index=10, start_index=10,
), ),
] ]
self.assertEqual(attrs, expected_attrs) assert attrs == expected_attrs
self.assertEqual( assert [a.serialize() for a in attrs] == [
[a.serialize() for a in attrs],
[
"component", "component",
"[*[val1], val2]", "[*[val1], val2]",
], ]
)
def test_spread_list_literal_as_attribute(self): def test_spread_list_literal_as_attribute(self):
_, attrs = parse_tag("component ...[val1]", None) _, attrs = parse_tag("component ...[val1]", None)
@ -2487,15 +2458,12 @@ class TagParserTests(BaseTestCase):
start_index=10, start_index=10,
), ),
] ]
self.assertEqual(attrs, expected_attrs) assert attrs == expected_attrs
self.assertEqual( assert [a.serialize() for a in attrs] == [
[a.serialize() for a in attrs],
[
"component", "component",
"...[val1]", "...[val1]",
], ]
)
def test_dynamic_expressions(self): def test_dynamic_expressions(self):
_, attrs = parse_tag("component '{% lorem w 4 %}'", None) _, attrs = parse_tag("component '{% lorem w 4 %}'", None)
@ -2540,7 +2508,7 @@ class TagParserTests(BaseTestCase):
start_index=10, start_index=10,
), ),
] ]
self.assertEqual(attrs, expected_attrs) assert attrs == expected_attrs
def test_dynamic_expressions_in_dict(self): def test_dynamic_expressions_in_dict(self):
_, attrs = parse_tag('component { "key": "{% lorem w 4 %}" }', None) _, attrs = parse_tag('component { "key": "{% lorem w 4 %}" }', None)
@ -2588,7 +2556,7 @@ class TagParserTests(BaseTestCase):
start_index=10, start_index=10,
), ),
] ]
self.assertEqual(attrs, expected_attrs) assert attrs == expected_attrs
def test_dynamic_expressions_in_list(self): def test_dynamic_expressions_in_list(self):
_, attrs = parse_tag("component [ '{% lorem w 4 %}' ]", None) _, attrs = parse_tag("component [ '{% lorem w 4 %}' ]", None)
@ -2633,49 +2601,50 @@ class TagParserTests(BaseTestCase):
start_index=10, start_index=10,
), ),
] ]
self.assertEqual(attrs, expected_attrs) assert attrs == expected_attrs
class ResolverTests(BaseTestCase): @djc_test
class TestResolver:
def test_resolve_simple(self): def test_resolve_simple(self):
_, attrs = parse_tag("123", None) _, attrs = parse_tag("123", None)
resolved = attrs[0].value.resolve(Context()) resolved = attrs[0].value.resolve(Context())
self.assertEqual(resolved, 123) assert resolved == 123
_, attrs = parse_tag("'123'", None) _, attrs = parse_tag("'123'", None)
resolved = attrs[0].value.resolve(Context()) resolved = attrs[0].value.resolve(Context())
self.assertEqual(resolved, "123") assert resolved == "123"
_, attrs = parse_tag("abc", None) _, attrs = parse_tag("abc", None)
resolved = attrs[0].value.resolve(Context({"abc": "foo"})) resolved = attrs[0].value.resolve(Context({"abc": "foo"}))
self.assertEqual(resolved, "foo") assert resolved == "foo"
def test_resolve_list(self): def test_resolve_list(self):
_, attrs = parse_tag("[1, 2, 3,]", None) _, attrs = parse_tag("[1, 2, 3,]", None)
resolved = attrs[0].value.resolve(Context()) resolved = attrs[0].value.resolve(Context())
self.assertEqual(resolved, [1, 2, 3]) assert resolved == [1, 2, 3]
def test_resolve_list_with_spread(self): def test_resolve_list_with_spread(self):
_, attrs = parse_tag("[ 1, 2, *[3, val1, *val2, 5], val3, 6 ]", None) _, attrs = parse_tag("[ 1, 2, *[3, val1, *val2, 5], val3, 6 ]", None)
resolved = attrs[0].value.resolve(Context({"val1": "foo", "val2": ["bar", "baz"], "val3": "qux"})) resolved = attrs[0].value.resolve(Context({"val1": "foo", "val2": ["bar", "baz"], "val3": "qux"}))
self.assertEqual(resolved, [1, 2, 3, "foo", "bar", "baz", 5, "qux", 6]) assert resolved == [1, 2, 3, "foo", "bar", "baz", 5, "qux", 6]
def test_resolve_dict(self): def test_resolve_dict(self):
_, attrs = parse_tag('{"a": 1, "b": 2,}', None) _, attrs = parse_tag('{"a": 1, "b": 2,}', None)
resolved = attrs[0].value.resolve(Context()) resolved = attrs[0].value.resolve(Context())
self.assertEqual(resolved, {"a": 1, "b": 2}) assert resolved == {"a": 1, "b": 2}
def test_resolve_dict_with_spread(self): def test_resolve_dict_with_spread(self):
_, attrs = parse_tag('{ **{"key": val2, **{ val3: val4 }, "key3": val4 } }', None) _, attrs = parse_tag('{ **{"key": val2, **{ val3: val4 }, "key3": val4 } }', None)
context = Context({"val2": "foo", "val3": "bar", "val4": "baz"}) context = Context({"val2": "foo", "val3": "bar", "val4": "baz"})
resolved = attrs[0].value.resolve(context) resolved = attrs[0].value.resolve(context)
self.assertEqual(resolved, {"key": "foo", "bar": "baz", "key3": "baz"}) assert resolved == {"key": "foo", "bar": "baz", "key3": "baz"}
def test_resolve_dynamic_expr(self): def test_resolve_dynamic_expr(self):
parser = _get_parser() parser = _get_parser()
_, attrs = parse_tag("'{% lorem 4 w %}'", parser) _, attrs = parse_tag("'{% lorem 4 w %}'", parser)
resolved = attrs[0].value.resolve(Context()) resolved = attrs[0].value.resolve(Context())
self.assertEqual(resolved, "lorem ipsum dolor sit") assert resolved == "lorem ipsum dolor sit"
def test_resolve_complex(self): def test_resolve_complex(self):
parser = _get_parser() parser = _get_parser()
@ -2718,9 +2687,7 @@ class ResolverTests(BaseTestCase):
) )
resolved = attrs[0].value.resolve(context) resolved = attrs[0].value.resolve(context)
self.assertEqual( assert resolved == {
resolved,
{
"items": [3, {"X": 5}, "foo", "bar"], "items": [3, {"X": 5}, "foo", "bar"],
"nested": { "nested": {
"a": [3, 1, 2, 3, "l", "o", "r", "e", "m"], "a": [3, 1, 2, 3, "l", "o", "r", "e", "m"],
@ -2731,8 +2698,7 @@ class ResolverTests(BaseTestCase):
}, },
"a": "b", "a": "b",
"key": "VALUE", "key": "VALUE",
}, }
)
@skip("TODO: Enable once template parsing is fixed by us") @skip("TODO: Enable once template parsing is fixed by us")
def test_resolve_complex_as_component(self): def test_resolve_complex_as_component(self):
@ -2785,9 +2751,7 @@ class ResolverTests(BaseTestCase):
) )
) )
self.assertEqual( assert captured == {
captured,
{
"items": [3, {"X": 5}, "foo", "bar"], "items": [3, {"X": 5}, "foo", "bar"],
"nested": { "nested": {
"a": [3, 1, 2, 3, "l", "o", "r", "e", "m"], "a": [3, 1, 2, 3, "l", "o", "r", "e", "m"],
@ -2798,8 +2762,7 @@ class ResolverTests(BaseTestCase):
}, },
"a": "b", "a": "b",
"key": "VALUE", "key": "VALUE",
}, }
)
def test_component_args_kwargs(self): def test_component_args_kwargs(self):
captured = None captured = None
@ -2820,7 +2783,7 @@ class ResolverTests(BaseTestCase):
""" """
Template(template_str).render(Context({"myvar": "myval", "val2": [1, 2, 3]})) Template(template_str).render(Context({"myvar": "myval", "val2": [1, 2, 3]}))
self.assertEqual(captured, ((42, "myval"), {"key": "val", "key2": [1, 2, 3]})) assert captured == ((42, "myval"), {"key": "val", "key2": [1, 2, 3]})
def test_component_special_kwargs(self): def test_component_special_kwargs(self):
captured = None captured = None
@ -2841,9 +2804,7 @@ class ResolverTests(BaseTestCase):
""" """
Template(template_str).render(Context({"date": 2024, "bzz": "fzz"})) Template(template_str).render(Context({"date": 2024, "bzz": "fzz"}))
self.assertEqual( assert captured == (
captured,
(
tuple([]), tuple([]),
{ {
"date": 2024, "date": 2024,
@ -2852,5 +2813,4 @@ class ResolverTests(BaseTestCase):
"@event": {"na-me.mod": "fzz"}, "@event": {"na-me.mod": "fzz"},
"#my-id": True, "#my-id": True,
}, },
),
) )

View file

@ -2,27 +2,28 @@ from django.template import Context, Template
from django_components import Component, cached_template, types from django_components import Component, cached_template, types
from .django_test_setup import setup_test_config from django_components.testing import djc_test
from .testutils import BaseTestCase from .testutils import setup_test_config
setup_test_config({"autodiscover": False}) setup_test_config({"autodiscover": False})
class TemplateCacheTest(BaseTestCase): @djc_test
class TestTemplateCache:
def test_cached_template(self): def test_cached_template(self):
template_1 = cached_template("Variable: <strong>{{ variable }}</strong>") template_1 = cached_template("Variable: <strong>{{ variable }}</strong>")
template_1._test_id = "123" template_1._test_id = "123"
template_2 = cached_template("Variable: <strong>{{ variable }}</strong>") template_2 = cached_template("Variable: <strong>{{ variable }}</strong>")
self.assertEqual(template_2._test_id, "123") assert template_2._test_id == "123"
def test_cached_template_accepts_class(self): def test_cached_template_accepts_class(self):
class MyTemplate(Template): class MyTemplate(Template):
pass pass
template = cached_template("Variable: <strong>{{ variable }}</strong>", MyTemplate) template = cached_template("Variable: <strong>{{ variable }}</strong>", MyTemplate)
self.assertIsInstance(template, MyTemplate) assert isinstance(template, MyTemplate)
def test_component_template_is_cached(self): def test_component_template_is_cached(self):
class SimpleComponent(Component): class SimpleComponent(Component):
@ -42,4 +43,4 @@ class TemplateCacheTest(BaseTestCase):
template_1._test_id = "123" template_1._test_id = "123"
template_2 = comp._get_template(Context({}), component_id="123") template_2 = comp._get_template(Context({}), component_id="123")
self.assertEqual(template_2._test_id, "123") assert template_2._test_id == "123"

View file

@ -1,11 +1,12 @@
from django.template import Context from django.template import Context
from django.template.base import Template, Token, TokenType from django.template.base import Template, Token, TokenType
from pytest_django.asserts import assertHTMLEqual
from django_components import Component, register, types from django_components import Component, register, types
from django_components.util.template_parser import parse_template from django_components.util.template_parser import parse_template
from .django_test_setup import setup_test_config from django_components.testing import djc_test
from .testutils import BaseTestCase from .testutils import setup_test_config
setup_test_config({"autodiscover": False}) setup_test_config({"autodiscover": False})
@ -19,7 +20,8 @@ def token2tuple(token: Token):
) )
class TemplateParserTests(BaseTestCase): @djc_test
class TestTemplateParser:
def test_template_text(self): def test_template_text(self):
tokens = parse_template("Hello world") tokens = parse_template("Hello world")
@ -28,7 +30,7 @@ class TemplateParserTests(BaseTestCase):
(TokenType.TEXT, "Hello world", (0, 11), 1), (TokenType.TEXT, "Hello world", (0, 11), 1),
] ]
self.assertEqual(token_tuples, expected_tokens) assert token_tuples == expected_tokens
def test_template_variable(self): def test_template_variable(self):
tokens = parse_template("Hello {{ name }}") tokens = parse_template("Hello {{ name }}")
@ -39,7 +41,7 @@ class TemplateParserTests(BaseTestCase):
(TokenType.VAR, "name", (6, 16), 1), (TokenType.VAR, "name", (6, 16), 1),
] ]
self.assertEqual(token_tuples, expected_tokens) assert token_tuples == expected_tokens
# NOTE(Juro): IMO this should be a TemplateSyntaxError, but Django doesn't raise it # NOTE(Juro): IMO this should be a TemplateSyntaxError, but Django doesn't raise it
def test_template_variable_unterminated(self): def test_template_variable_unterminated(self):
@ -50,7 +52,7 @@ class TemplateParserTests(BaseTestCase):
(TokenType.TEXT, "Hello {{ name", (0, 13), 1), (TokenType.TEXT, "Hello {{ name", (0, 13), 1),
] ]
self.assertEqual(token_tuples, expected_tokens) assert token_tuples == expected_tokens
def test_template_tag(self): def test_template_tag(self):
tokens = parse_template("{% component 'my_comp' key=val %}") tokens = parse_template("{% component 'my_comp' key=val %}")
@ -60,7 +62,7 @@ class TemplateParserTests(BaseTestCase):
(TokenType.BLOCK, "component 'my_comp' key=val", (0, 33), 1), (TokenType.BLOCK, "component 'my_comp' key=val", (0, 33), 1),
] ]
self.assertEqual(token_tuples, expected_tokens) assert token_tuples == expected_tokens
# NOTE(Juro): IMO this should be a TemplateSyntaxError, but Django doesn't raise it # NOTE(Juro): IMO this should be a TemplateSyntaxError, but Django doesn't raise it
def test_template_tag_unterminated(self): def test_template_tag_unterminated(self):
@ -71,7 +73,7 @@ class TemplateParserTests(BaseTestCase):
(TokenType.TEXT, "{% if true", (0, 10), 1), (TokenType.TEXT, "{% if true", (0, 10), 1),
] ]
self.assertEqual(token_tuples, expected_tokens) assert token_tuples == expected_tokens
def test_template_comment(self): def test_template_comment(self):
tokens = parse_template("Hello{# this is a comment #}World") tokens = parse_template("Hello{# this is a comment #}World")
@ -83,7 +85,7 @@ class TemplateParserTests(BaseTestCase):
(TokenType.TEXT, "World", (28, 33), 1), (TokenType.TEXT, "World", (28, 33), 1),
] ]
self.assertEqual(token_tuples, expected_tokens) assert token_tuples == expected_tokens
# NOTE(Juro): IMO this should be a TemplateSyntaxError, but Django doesn't raise it # NOTE(Juro): IMO this should be a TemplateSyntaxError, but Django doesn't raise it
def test_template_comment_unterminated(self): def test_template_comment_unterminated(self):
@ -94,7 +96,7 @@ class TemplateParserTests(BaseTestCase):
(TokenType.TEXT, "{# comment", (0, 10), 1), (TokenType.TEXT, "{# comment", (0, 10), 1),
] ]
self.assertEqual(token_tuples, expected_tokens) assert token_tuples == expected_tokens
def test_template_verbatim(self): def test_template_verbatim(self):
tokens = parse_template( tokens = parse_template(
@ -115,7 +117,7 @@ class TemplateParserTests(BaseTestCase):
(TokenType.BLOCK, "endverbatim", (107, 124), 4), (TokenType.BLOCK, "endverbatim", (107, 124), 4),
] ]
self.assertEqual(token_tuples, expected_tokens) assert token_tuples == expected_tokens
def test_template_verbatim_with_name(self): def test_template_verbatim_with_name(self):
tokens = parse_template( tokens = parse_template(
@ -142,7 +144,7 @@ class TemplateParserTests(BaseTestCase):
(TokenType.BLOCK, "endverbatim myblock", (184, 209), 6), (TokenType.BLOCK, "endverbatim myblock", (184, 209), 6),
] ]
self.assertEqual(token_tuples, expected_tokens) assert token_tuples == expected_tokens
def test_template_nested_tags(self): def test_template_nested_tags(self):
tokens = parse_template("""{% component 'test' "{% lorem var_a w %}" %}""") tokens = parse_template("""{% component 'test' "{% lorem var_a w %}" %}""")
@ -152,7 +154,7 @@ class TemplateParserTests(BaseTestCase):
(TokenType.BLOCK, "component 'test' \"{% lorem var_a w %}\"", (0, 44), 1), (TokenType.BLOCK, "component 'test' \"{% lorem var_a w %}\"", (0, 44), 1),
] ]
self.assertEqual(token_tuples, expected_tokens) assert token_tuples == expected_tokens
def test_brackets_and_percent_in_text(self): def test_brackets_and_percent_in_text(self):
tokens = parse_template('{% component \'test\' \'"\' "{%}" bool_var="{% noop is_active %}" / %}') tokens = parse_template('{% component \'test\' \'"\' "{%}" bool_var="{% noop is_active %}" / %}')
@ -163,7 +165,7 @@ class TemplateParserTests(BaseTestCase):
(TokenType.BLOCK, 'component \'test\' \'"\' "{%}" bool_var="{% noop is_active %}" /', (0, 66), 1), (TokenType.BLOCK, 'component \'test\' \'"\' "{%}" bool_var="{% noop is_active %}" /', (0, 66), 1),
] ]
self.assertEqual(token_tuples, expected_tokens) assert token_tuples == expected_tokens
def test_template_mixed(self): def test_template_mixed(self):
tokens = parse_template( tokens = parse_template(
@ -201,7 +203,7 @@ class TemplateParserTests(BaseTestCase):
(TokenType.BLOCK, "endif", (341, 352), 10), (TokenType.BLOCK, "endif", (341, 352), 10),
] ]
self.assertEqual(token_tuples, expected_tokens) assert token_tuples == expected_tokens
# Check that a template that contains `{% %}` inside of a component tag is parsed correctly # Check that a template that contains `{% %}` inside of a component tag is parsed correctly
def test_component_mixed(self): def test_component_mixed(self):
@ -234,7 +236,7 @@ class TemplateParserTests(BaseTestCase):
template = Template(template_str) template = Template(template_str)
rendered = template.render(Context({"name": "John", "show_greeting": True, "var_a": 2})) rendered = template.render(Context({"name": "John", "show_greeting": True, "var_a": 2}))
self.assertHTMLEqual( assertHTMLEqual(
rendered, rendered,
""" """
<div> <div>

View file

@ -1,11 +1,11 @@
"""Catch-all for tests that use template tags and don't fit other files""" """Catch-all for tests that use template tags and don't fit other files"""
from django.template import Context, Template from django.template import Context, Template
from pytest_django.asserts import assertHTMLEqual
from django_components import Component, register, registry, types from django_components import Component, register, registry, types
from .django_test_setup import setup_test_config from django_components.testing import djc_test
from .testutils import BaseTestCase, parametrize_context_behavior from .testutils import PARAMETRIZE_CONTEXT_BEHAVIOR, setup_test_config
setup_test_config({"autodiscover": False}) setup_test_config({"autodiscover": False})
@ -19,9 +19,10 @@ class SlottedComponent(Component):
####################### #######################
class MultilineTagsTests(BaseTestCase): @djc_test
@parametrize_context_behavior(["django", "isolated"]) class TestMultilineTags:
def test_multiline_tags(self): @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_multiline_tags(self, components_settings):
@register("test_component") @register("test_component")
class SimpleComponent(Component): class SimpleComponent(Component):
template: types.django_html = """ template: types.django_html = """
@ -47,10 +48,11 @@ class MultilineTagsTests(BaseTestCase):
expected = """ expected = """
Variable: <strong data-djc-id-a1bc3f>123</strong> Variable: <strong data-djc-id-a1bc3f>123</strong>
""" """
self.assertHTMLEqual(rendered, expected) assertHTMLEqual(rendered, expected)
class NestedTagsTests(BaseTestCase): @djc_test
class TestNestedTags:
class SimpleComponent(Component): class SimpleComponent(Component):
template: types.django_html = """ template: types.django_html = """
Variable: <strong>{{ var }}</strong> Variable: <strong>{{ var }}</strong>
@ -62,8 +64,8 @@ class NestedTagsTests(BaseTestCase):
} }
# See https://github.com/django-components/django-components/discussions/671 # See https://github.com/django-components/django-components/discussions/671
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_nested_tags(self): def test_nested_tags(self, components_settings):
registry.register("test", self.SimpleComponent) registry.register("test", self.SimpleComponent)
template: types.django_html = """ template: types.django_html = """
@ -74,10 +76,10 @@ class NestedTagsTests(BaseTestCase):
expected = """ expected = """
Variable: <strong data-djc-id-a1bc3f>lorem</strong> Variable: <strong data-djc-id-a1bc3f>lorem</strong>
""" """
self.assertHTMLEqual(rendered, expected) assertHTMLEqual(rendered, expected)
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_nested_quote_single(self): def test_nested_quote_single(self, components_settings):
registry.register("test", self.SimpleComponent) registry.register("test", self.SimpleComponent)
template: types.django_html = """ template: types.django_html = """
@ -88,10 +90,10 @@ class NestedTagsTests(BaseTestCase):
expected = """ expected = """
Variable: <strong data-djc-id-a1bc3f>organisation&#x27;s</strong> Variable: <strong data-djc-id-a1bc3f>organisation&#x27;s</strong>
""" """
self.assertHTMLEqual(rendered, expected) assertHTMLEqual(rendered, expected)
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_nested_quote_single_self_closing(self): def test_nested_quote_single_self_closing(self, components_settings):
registry.register("test", self.SimpleComponent) registry.register("test", self.SimpleComponent)
template: types.django_html = """ template: types.django_html = """
@ -102,10 +104,10 @@ class NestedTagsTests(BaseTestCase):
expected = """ expected = """
Variable: <strong data-djc-id-a1bc3f>organisation&#x27;s</strong> Variable: <strong data-djc-id-a1bc3f>organisation&#x27;s</strong>
""" """
self.assertHTMLEqual(rendered, expected) assertHTMLEqual(rendered, expected)
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_nested_quote_double(self): def test_nested_quote_double(self, components_settings):
registry.register("test", self.SimpleComponent) registry.register("test", self.SimpleComponent)
template: types.django_html = """ template: types.django_html = """
@ -116,10 +118,10 @@ class NestedTagsTests(BaseTestCase):
expected = """ expected = """
Variable: <strong data-djc-id-a1bc3f>organisation"s</strong> Variable: <strong data-djc-id-a1bc3f>organisation"s</strong>
""" """
self.assertHTMLEqual(rendered, expected) assertHTMLEqual(rendered, expected)
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_nested_quote_double_self_closing(self): def test_nested_quote_double_self_closing(self, components_settings):
registry.register("test", self.SimpleComponent) registry.register("test", self.SimpleComponent)
template: types.django_html = """ template: types.django_html = """
@ -130,4 +132,4 @@ class NestedTagsTests(BaseTestCase):
expected = """ expected = """
Variable: <strong data-djc-id-a1bc3f>organisation"s</strong> Variable: <strong data-djc-id-a1bc3f>organisation"s</strong>
""" """
self.assertHTMLEqual(rendered, expected) assertHTMLEqual(rendered, expected)

View file

@ -1,9 +1,12 @@
from django.template import Context, Template, TemplateSyntaxError import re
import pytest
from django.template import Context, Template, TemplateSyntaxError
from pytest_django.asserts import assertHTMLEqual
from django_components import AlreadyRegistered, Component, NotRegistered, register, registry, types from django_components import AlreadyRegistered, Component, NotRegistered, register, registry, types
from .django_test_setup import setup_test_config from django_components.testing import djc_test
from .testutils import BaseTestCase, parametrize_context_behavior from .testutils import PARAMETRIZE_CONTEXT_BEHAVIOR, setup_test_config
setup_test_config({"autodiscover": False}) setup_test_config({"autodiscover": False})
@ -31,7 +34,8 @@ class SlottedComponentWithContext(Component):
####################### #######################
class ComponentTemplateTagTest(BaseTestCase): @djc_test
class TestComponentTemplateTag:
class SimpleComponent(Component): class SimpleComponent(Component):
template: types.django_html = """ template: types.django_html = """
Variable: <strong>{{ variable }}</strong> Variable: <strong>{{ variable }}</strong>
@ -47,8 +51,8 @@ class ComponentTemplateTagTest(BaseTestCase):
css = "style.css" css = "style.css"
js = "script.js" js = "script.js"
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_single_component(self): def test_single_component(self, components_settings):
registry.register(name="test", component=self.SimpleComponent) registry.register(name="test", component=self.SimpleComponent)
simple_tag_template: types.django_html = """ simple_tag_template: types.django_html = """
@ -58,10 +62,10 @@ class ComponentTemplateTagTest(BaseTestCase):
template = Template(simple_tag_template) template = Template(simple_tag_template)
rendered = template.render(Context({})) rendered = template.render(Context({}))
self.assertHTMLEqual(rendered, "Variable: <strong data-djc-id-a1bc3f>variable</strong>\n") assertHTMLEqual(rendered, "Variable: <strong data-djc-id-a1bc3f>variable</strong>\n")
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_single_component_self_closing(self): def test_single_component_self_closing(self, components_settings):
registry.register(name="test", component=self.SimpleComponent) registry.register(name="test", component=self.SimpleComponent)
simple_tag_template: types.django_html = """ simple_tag_template: types.django_html = """
@ -71,10 +75,10 @@ class ComponentTemplateTagTest(BaseTestCase):
template = Template(simple_tag_template) template = Template(simple_tag_template)
rendered = template.render(Context({})) rendered = template.render(Context({}))
self.assertHTMLEqual(rendered, "Variable: <strong data-djc-id-a1bc3f>variable</strong>\n") assertHTMLEqual(rendered, "Variable: <strong data-djc-id-a1bc3f>variable</strong>\n")
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_call_with_invalid_name(self): def test_call_with_invalid_name(self, components_settings):
registry.register(name="test_one", component=self.SimpleComponent) registry.register(name="test_one", component=self.SimpleComponent)
simple_tag_template: types.django_html = """ simple_tag_template: types.django_html = """
@ -83,11 +87,11 @@ class ComponentTemplateTagTest(BaseTestCase):
""" """
template = Template(simple_tag_template) template = Template(simple_tag_template)
with self.assertRaises(NotRegistered): with pytest.raises(NotRegistered):
template.render(Context({})) template.render(Context({}))
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_component_called_with_positional_name(self): def test_component_called_with_positional_name(self, components_settings):
registry.register(name="test", component=self.SimpleComponent) registry.register(name="test", component=self.SimpleComponent)
simple_tag_template: types.django_html = """ simple_tag_template: types.django_html = """
@ -97,10 +101,10 @@ class ComponentTemplateTagTest(BaseTestCase):
template = Template(simple_tag_template) template = Template(simple_tag_template)
rendered = template.render(Context({})) rendered = template.render(Context({}))
self.assertHTMLEqual(rendered, "Variable: <strong data-djc-id-a1bc3f>variable</strong>\n") assertHTMLEqual(rendered, "Variable: <strong data-djc-id-a1bc3f>variable</strong>\n")
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_call_component_with_two_variables(self): def test_call_component_with_two_variables(self, components_settings):
@register("test") @register("test")
class IffedComponent(Component): class IffedComponent(Component):
template: types.django_html = """ template: types.django_html = """
@ -127,7 +131,7 @@ class ComponentTemplateTagTest(BaseTestCase):
template = Template(simple_tag_template) template = Template(simple_tag_template)
rendered = template.render(Context({})) rendered = template.render(Context({}))
self.assertHTMLEqual( assertHTMLEqual(
rendered, rendered,
""" """
Variable: <strong data-djc-id-a1bc3f>variable</strong> Variable: <strong data-djc-id-a1bc3f>variable</strong>
@ -135,8 +139,8 @@ class ComponentTemplateTagTest(BaseTestCase):
""", """,
) )
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_component_called_with_singlequoted_name(self): def test_component_called_with_singlequoted_name(self, components_settings):
registry.register(name="test", component=self.SimpleComponent) registry.register(name="test", component=self.SimpleComponent)
simple_tag_template: types.django_html = """ simple_tag_template: types.django_html = """
@ -146,10 +150,10 @@ class ComponentTemplateTagTest(BaseTestCase):
template = Template(simple_tag_template) template = Template(simple_tag_template)
rendered = template.render(Context({})) rendered = template.render(Context({}))
self.assertHTMLEqual(rendered, "Variable: <strong data-djc-id-a1bc3f>variable</strong>\n") assertHTMLEqual(rendered, "Variable: <strong data-djc-id-a1bc3f>variable</strong>\n")
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_raises_on_component_called_with_variable_as_name(self): def test_raises_on_component_called_with_variable_as_name(self, components_settings):
registry.register(name="test", component=self.SimpleComponent) registry.register(name="test", component=self.SimpleComponent)
simple_tag_template: types.django_html = """ simple_tag_template: types.django_html = """
@ -159,14 +163,14 @@ class ComponentTemplateTagTest(BaseTestCase):
{% endwith %} {% endwith %}
""" """
with self.assertRaisesMessage( with pytest.raises(
TemplateSyntaxError, TemplateSyntaxError,
"Component name must be a string 'literal', got: component_name", match=re.escape("Component name must be a string 'literal', got: component_name"),
): ):
Template(simple_tag_template) Template(simple_tag_template)
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_component_accepts_provided_and_default_parameters(self): def test_component_accepts_provided_and_default_parameters(self, components_settings):
@register("test") @register("test")
class ComponentWithProvidedAndDefaultParameters(Component): class ComponentWithProvidedAndDefaultParameters(Component):
template: types.django_html = """ template: types.django_html = """
@ -184,7 +188,7 @@ class ComponentTemplateTagTest(BaseTestCase):
""" """
template = Template(template_str) template = Template(template_str)
rendered = template.render(Context({})) rendered = template.render(Context({}))
self.assertHTMLEqual( assertHTMLEqual(
rendered, rendered,
""" """
Provided variable: <strong data-djc-id-a1bc3f>provided value</strong> Provided variable: <strong data-djc-id-a1bc3f>provided value</strong>
@ -193,7 +197,8 @@ class ComponentTemplateTagTest(BaseTestCase):
) )
class DynamicComponentTemplateTagTest(BaseTestCase): @djc_test
class TestDynamicComponentTemplateTag:
class SimpleComponent(Component): class SimpleComponent(Component):
template: types.django_html = """ template: types.django_html = """
Variable: <strong>{{ variable }}</strong> Variable: <strong>{{ variable }}</strong>
@ -209,16 +214,8 @@ class DynamicComponentTemplateTagTest(BaseTestCase):
css = "style.css" css = "style.css"
js = "script.js" js = "script.js"
def setUp(self): @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
super().setUp() def test_basic(self, components_settings):
# Run app installation so the `dynamic` component is defined
from django_components.apps import ComponentsConfig
ComponentsConfig.ready(None) # type: ignore[arg-type]
@parametrize_context_behavior(["django", "isolated"])
def test_basic(self):
registry.register(name="test", component=self.SimpleComponent) registry.register(name="test", component=self.SimpleComponent)
simple_tag_template: types.django_html = """ simple_tag_template: types.django_html = """
@ -228,13 +225,13 @@ class DynamicComponentTemplateTagTest(BaseTestCase):
template = Template(simple_tag_template) template = Template(simple_tag_template)
rendered = template.render(Context({})) rendered = template.render(Context({}))
self.assertHTMLEqual( assertHTMLEqual(
rendered, rendered,
"Variable: <strong data-djc-id-a1bc3f data-djc-id-a1bc40>variable</strong>", "Variable: <strong data-djc-id-a1bc3f data-djc-id-a1bc40>variable</strong>",
) )
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_call_with_invalid_name(self): def test_call_with_invalid_name(self, components_settings):
registry.register(name="test", component=self.SimpleComponent) registry.register(name="test", component=self.SimpleComponent)
simple_tag_template: types.django_html = """ simple_tag_template: types.django_html = """
@ -243,11 +240,11 @@ class DynamicComponentTemplateTagTest(BaseTestCase):
""" """
template = Template(simple_tag_template) template = Template(simple_tag_template)
with self.assertRaisesMessage(NotRegistered, "The component 'haber_der_baber' was not found"): with pytest.raises(NotRegistered, match=re.escape("The component 'haber_der_baber' was not found")):
template.render(Context({})) template.render(Context({}))
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_component_called_with_variable_as_name(self): def test_component_called_with_variable_as_name(self, components_settings):
registry.register(name="test", component=self.SimpleComponent) registry.register(name="test", component=self.SimpleComponent)
simple_tag_template: types.django_html = """ simple_tag_template: types.django_html = """
@ -259,13 +256,13 @@ class DynamicComponentTemplateTagTest(BaseTestCase):
template = Template(simple_tag_template) template = Template(simple_tag_template)
rendered = template.render(Context({})) rendered = template.render(Context({}))
self.assertHTMLEqual( assertHTMLEqual(
rendered, rendered,
"Variable: <strong data-djc-id-a1bc3f data-djc-id-a1bc40>variable</strong>", "Variable: <strong data-djc-id-a1bc3f data-djc-id-a1bc40>variable</strong>",
) )
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_component_called_with_variable_as_spread(self): def test_component_called_with_variable_as_spread(self, components_settings):
registry.register(name="test", component=self.SimpleComponent) registry.register(name="test", component=self.SimpleComponent)
simple_tag_template: types.django_html = """ simple_tag_template: types.django_html = """
@ -284,13 +281,13 @@ class DynamicComponentTemplateTagTest(BaseTestCase):
} }
) )
) )
self.assertHTMLEqual( assertHTMLEqual(
rendered, rendered,
"Variable: <strong data-djc-id-a1bc3f data-djc-id-a1bc40>variable</strong>", "Variable: <strong data-djc-id-a1bc3f data-djc-id-a1bc40>variable</strong>",
) )
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_component_as_class(self): def test_component_as_class(self, components_settings):
registry.register(name="test", component=self.SimpleComponent) registry.register(name="test", component=self.SimpleComponent)
simple_tag_template: types.django_html = """ simple_tag_template: types.django_html = """
@ -306,21 +303,19 @@ class DynamicComponentTemplateTagTest(BaseTestCase):
} }
) )
) )
self.assertHTMLEqual( assertHTMLEqual(
rendered, rendered,
"Variable: <strong data-djc-id-a1bc3f data-djc-id-a1bc40>variable</strong>", "Variable: <strong data-djc-id-a1bc3f data-djc-id-a1bc40>variable</strong>",
) )
@parametrize_context_behavior( @djc_test(
["django", "isolated"], parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR,
settings={ components_settings={
"COMPONENTS": {
"tag_formatter": "django_components.component_shorthand_formatter", "tag_formatter": "django_components.component_shorthand_formatter",
"autodiscover": False, "autodiscover": False,
}, },
},
) )
def test_shorthand_formatter(self): def test_shorthand_formatter(self, components_settings):
from django_components.apps import ComponentsConfig from django_components.apps import ComponentsConfig
ComponentsConfig.ready(None) # type: ignore[arg-type] ComponentsConfig.ready(None) # type: ignore[arg-type]
@ -334,19 +329,17 @@ class DynamicComponentTemplateTagTest(BaseTestCase):
template = Template(simple_tag_template) template = Template(simple_tag_template)
rendered = template.render(Context({})) rendered = template.render(Context({}))
self.assertHTMLEqual(rendered, "Variable: <strong data-djc-id-a1bc3f data-djc-id-a1bc40>variable</strong>\n") assertHTMLEqual(rendered, "Variable: <strong data-djc-id-a1bc3f data-djc-id-a1bc40>variable</strong>\n")
@parametrize_context_behavior( @djc_test(
["django", "isolated"], parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR,
settings={ components_settings={
"COMPONENTS": {
"dynamic_component_name": "uno_reverse", "dynamic_component_name": "uno_reverse",
"tag_formatter": "django_components.component_shorthand_formatter", "tag_formatter": "django_components.component_shorthand_formatter",
"autodiscover": False, "autodiscover": False,
}, },
},
) )
def test_component_name_is_configurable(self): def test_component_name_is_configurable(self, components_settings):
from django_components.apps import ComponentsConfig from django_components.apps import ComponentsConfig
ComponentsConfig.ready(None) # type: ignore[arg-type] ComponentsConfig.ready(None) # type: ignore[arg-type]
@ -360,18 +353,21 @@ class DynamicComponentTemplateTagTest(BaseTestCase):
template = Template(simple_tag_template) template = Template(simple_tag_template)
rendered = template.render(Context({})) rendered = template.render(Context({}))
self.assertHTMLEqual( assertHTMLEqual(
rendered, rendered,
"Variable: <strong data-djc-id-a1bc3f data-djc-id-a1bc40>variable</strong>", "Variable: <strong data-djc-id-a1bc3f data-djc-id-a1bc40>variable</strong>",
) )
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_raises_already_registered_on_name_conflict(self): def test_raises_already_registered_on_name_conflict(self, components_settings):
with self.assertRaisesMessage(AlreadyRegistered, 'The component "dynamic" has already been registered'): with pytest.raises(
AlreadyRegistered,
match=re.escape('The component "dynamic" has already been registered'),
):
registry.register(name="dynamic", component=self.SimpleComponent) registry.register(name="dynamic", component=self.SimpleComponent)
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_component_called_with_default_slot(self): def test_component_called_with_default_slot(self, components_settings):
class SimpleSlottedComponent(Component): class SimpleSlottedComponent(Component):
template: types.django_html = """ template: types.django_html = """
{% load component_tags %} {% load component_tags %}
@ -398,7 +394,7 @@ class DynamicComponentTemplateTagTest(BaseTestCase):
template = Template(simple_tag_template) template = Template(simple_tag_template)
rendered = template.render(Context({})) rendered = template.render(Context({}))
self.assertHTMLEqual( assertHTMLEqual(
rendered, rendered,
""" """
Variable: <strong data-djc-id-a1bc3f data-djc-id-a1bc40>variable</strong> Variable: <strong data-djc-id-a1bc3f data-djc-id-a1bc40>variable</strong>
@ -406,8 +402,8 @@ class DynamicComponentTemplateTagTest(BaseTestCase):
""", """,
) )
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_component_called_with_named_slots(self): def test_component_called_with_named_slots(self, components_settings):
class SimpleSlottedComponent(Component): class SimpleSlottedComponent(Component):
template: types.django_html = """ template: types.django_html = """
{% load component_tags %} {% load component_tags %}
@ -440,7 +436,7 @@ class DynamicComponentTemplateTagTest(BaseTestCase):
template = Template(simple_tag_template) template = Template(simple_tag_template)
rendered = template.render(Context({})) rendered = template.render(Context({}))
self.assertHTMLEqual( assertHTMLEqual(
rendered, rendered,
""" """
Variable: <strong data-djc-id-a1bc41 data-djc-id-a1bc42>variable</strong> Variable: <strong data-djc-id-a1bc41 data-djc-id-a1bc42>variable</strong>
@ -449,8 +445,8 @@ class DynamicComponentTemplateTagTest(BaseTestCase):
""", """,
) )
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_ignores_invalid_slots(self): def test_ignores_invalid_slots(self, components_settings):
class SimpleSlottedComponent(Component): class SimpleSlottedComponent(Component):
template: types.django_html = """ template: types.django_html = """
{% load component_tags %} {% load component_tags %}
@ -483,7 +479,7 @@ class DynamicComponentTemplateTagTest(BaseTestCase):
template = Template(simple_tag_template) template = Template(simple_tag_template)
rendered = template.render(Context({})) rendered = template.render(Context({}))
self.assertHTMLEqual( assertHTMLEqual(
rendered, rendered,
""" """
Variable: <strong data-djc-id-a1bc41 data-djc-id-a1bc42>variable</strong> Variable: <strong data-djc-id-a1bc41 data-djc-id-a1bc42>variable</strong>
@ -492,8 +488,8 @@ class DynamicComponentTemplateTagTest(BaseTestCase):
""", """,
) )
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_raises_on_invalid_args(self): def test_raises_on_invalid_args(self, components_settings):
registry.register(name="test", component=self.SimpleComponent) registry.register(name="test", component=self.SimpleComponent)
simple_tag_template: types.django_html = """ simple_tag_template: types.django_html = """
@ -504,13 +500,17 @@ class DynamicComponentTemplateTagTest(BaseTestCase):
""" """
template = Template(simple_tag_template) template = Template(simple_tag_template)
with self.assertRaisesMessage(TypeError, "got an unexpected keyword argument 'invalid_variable'"): with pytest.raises(
TypeError,
match=re.escape("got an unexpected keyword argument 'invalid_variable'"),
):
template.render(Context({})) template.render(Context({}))
class MultiComponentTests(BaseTestCase): @djc_test
@parametrize_context_behavior(["django", "isolated"]) class TestMultiComponent:
def test_both_components_render_correctly_with_no_slots(self): @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_both_components_render_correctly_with_no_slots(self, components_settings):
registry.register("first_component", SlottedComponent) registry.register("first_component", SlottedComponent)
registry.register("second_component", SlottedComponentWithContext) registry.register("second_component", SlottedComponentWithContext)
@ -524,7 +524,7 @@ class MultiComponentTests(BaseTestCase):
template = Template(template_str) template = Template(template_str)
rendered = template.render(Context()) rendered = template.render(Context())
self.assertHTMLEqual( assertHTMLEqual(
rendered, rendered,
""" """
<custom-template data-djc-id-a1bc40> <custom-template data-djc-id-a1bc40>
@ -534,7 +534,7 @@ class MultiComponentTests(BaseTestCase):
<main>Default main</main> <main>Default main</main>
<footer>Default footer</footer> <footer>Default footer</footer>
</custom-template> </custom-template>
<custom-template data-djc-id-a1bc44> <custom-template data-djc-id-a1bc47>
<header> <header>
Default header Default header
</header> </header>
@ -544,8 +544,8 @@ class MultiComponentTests(BaseTestCase):
""", """,
) )
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_both_components_render_correctly_with_slots(self): def test_both_components_render_correctly_with_slots(self, components_settings):
registry.register("first_component", SlottedComponent) registry.register("first_component", SlottedComponent)
registry.register("second_component", SlottedComponentWithContext) registry.register("second_component", SlottedComponentWithContext)
@ -561,7 +561,7 @@ class MultiComponentTests(BaseTestCase):
template = Template(template_str) template = Template(template_str)
rendered = template.render(Context()) rendered = template.render(Context())
self.assertHTMLEqual( assertHTMLEqual(
rendered, rendered,
""" """
<custom-template data-djc-id-a1bc42> <custom-template data-djc-id-a1bc42>
@ -571,7 +571,7 @@ class MultiComponentTests(BaseTestCase):
<main>Default main</main> <main>Default main</main>
<footer>Default footer</footer> <footer>Default footer</footer>
</custom-template> </custom-template>
<custom-template data-djc-id-a1bc46> <custom-template data-djc-id-a1bc49>
<header> <header>
<div>Slot #2</div> <div>Slot #2</div>
</header> </header>
@ -581,16 +581,8 @@ class MultiComponentTests(BaseTestCase):
""", """,
) )
@parametrize_context_behavior( @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
# TODO: Why is this the only place where this needs to be parametrized? def test_both_components_render_correctly_when_only_first_has_slots(self, components_settings):
cases=[
("django", "data-djc-id-a1bc48"),
("isolated", "data-djc-id-a1bc45"),
]
)
def test_both_components_render_correctly_when_only_first_has_slots(self, context_behavior_data):
second_id = context_behavior_data
registry.register("first_component", SlottedComponent) registry.register("first_component", SlottedComponent)
registry.register("second_component", SlottedComponentWithContext) registry.register("second_component", SlottedComponentWithContext)
@ -605,9 +597,9 @@ class MultiComponentTests(BaseTestCase):
template = Template(template_str) template = Template(template_str)
rendered = template.render(Context({})) rendered = template.render(Context({}))
self.assertHTMLEqual( assertHTMLEqual(
rendered, rendered,
f""" """
<custom-template data-djc-id-a1bc41> <custom-template data-djc-id-a1bc41>
<header> <header>
<p>Slot #1</p> <p>Slot #1</p>
@ -615,7 +607,7 @@ class MultiComponentTests(BaseTestCase):
<main>Default main</main> <main>Default main</main>
<footer>Default footer</footer> <footer>Default footer</footer>
</custom-template> </custom-template>
<custom-template {second_id}> <custom-template data-djc-id-a1bc48>
<header> <header>
Default header Default header
</header> </header>
@ -625,8 +617,8 @@ class MultiComponentTests(BaseTestCase):
""", """,
) )
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_both_components_render_correctly_when_only_second_has_slots(self): def test_both_components_render_correctly_when_only_second_has_slots(self, components_settings):
registry.register("first_component", SlottedComponent) registry.register("first_component", SlottedComponent)
registry.register("second_component", SlottedComponentWithContext) registry.register("second_component", SlottedComponentWithContext)
@ -641,7 +633,7 @@ class MultiComponentTests(BaseTestCase):
template = Template(template_str) template = Template(template_str)
rendered = template.render(Context({})) rendered = template.render(Context({}))
self.assertHTMLEqual( assertHTMLEqual(
rendered, rendered,
""" """
<custom-template data-djc-id-a1bc41> <custom-template data-djc-id-a1bc41>
@ -651,7 +643,7 @@ class MultiComponentTests(BaseTestCase):
<main>Default main</main> <main>Default main</main>
<footer>Default footer</footer> <footer>Default footer</footer>
</custom-template> </custom-template>
<custom-template data-djc-id-a1bc45> <custom-template data-djc-id-a1bc48>
<header> <header>
<div>Slot #2</div> <div>Slot #2</div>
</header> </header>
@ -662,7 +654,8 @@ class MultiComponentTests(BaseTestCase):
) )
class ComponentIsolationTests(BaseTestCase): @djc_test
class TestComponentIsolation:
class SlottedComponent(Component): class SlottedComponent(Component):
template: types.django_html = """ template: types.django_html = """
{% load component_tags %} {% load component_tags %}
@ -673,12 +666,9 @@ class ComponentIsolationTests(BaseTestCase):
</custom-template> </custom-template>
""" """
def setUp(self): @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
super().setUp() def test_instances_of_component_do_not_share_slots(self, components_settings):
registry.register("test", self.SlottedComponent) registry.register("test", self.SlottedComponent)
@parametrize_context_behavior(["django", "isolated"])
def test_instances_of_component_do_not_share_slots(self):
template_str: types.django_html = """ template_str: types.django_html = """
{% load component_tags %} {% load component_tags %}
{% component "test" %} {% component "test" %}
@ -696,7 +686,7 @@ class ComponentIsolationTests(BaseTestCase):
template.render(Context({})) template.render(Context({}))
rendered = template.render(Context({})) rendered = template.render(Context({}))
self.assertHTMLEqual( assertHTMLEqual(
rendered, rendered,
""" """
<custom-template data-djc-id-a1bc4a> <custom-template data-djc-id-a1bc4a>
@ -718,9 +708,10 @@ class ComponentIsolationTests(BaseTestCase):
) )
class AggregateInputTests(BaseTestCase): @djc_test
@parametrize_context_behavior(["django", "isolated"]) class TestAggregateInput:
def test_agg_input_accessible_in_get_context_data(self): @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_agg_input_accessible_in_get_context_data(self, components_settings):
@register("test") @register("test")
class AttrsComponent(Component): class AttrsComponent(Component):
template: types.django_html = """ template: types.django_html = """
@ -741,7 +732,7 @@ class AggregateInputTests(BaseTestCase):
""" # noqa: E501 """ # noqa: E501
template = Template(template_str) template = Template(template_str)
rendered = template.render(Context({"class_var": "padding-top-8"})) rendered = template.render(Context({"class_var": "padding-top-8"}))
self.assertHTMLEqual( assertHTMLEqual(
rendered, rendered,
""" """
<div data-djc-id-a1bc3f> <div data-djc-id-a1bc3f>
@ -752,9 +743,10 @@ class AggregateInputTests(BaseTestCase):
) )
class RecursiveComponentTests(BaseTestCase): @djc_test
@parametrize_context_behavior(["django", "isolated"]) class TestRecursiveComponent:
def test_recursive_component(self): @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_recursive_component(self, components_settings):
DEPTH = 100 DEPTH = 100
@register("recursive") @register("recursive")
@ -775,16 +767,14 @@ class RecursiveComponentTests(BaseTestCase):
result = Recursive.render() result = Recursive.render()
for i in range(DEPTH): for i in range(DEPTH):
self.assertIn(f"<span> depth: {i + 1} </span>", result) assert f"<span> depth: {i + 1} </span>" in result
class ComponentTemplateSyntaxErrorTests(BaseTestCase): @djc_test
def setUp(self): class TestComponentTemplateSyntaxError:
super().setUp() @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_variable_outside_fill_tag_compiles_w_out_error(self, components_settings):
registry.register("test", SlottedComponent) registry.register("test", SlottedComponent)
@parametrize_context_behavior(["django", "isolated"])
def test_variable_outside_fill_tag_compiles_w_out_error(self):
# As of v0.28 this is valid, provided the component registered under "test" # As of v0.28 this is valid, provided the component registered under "test"
# contains a slot tag marked as 'default'. This is verified outside # contains a slot tag marked as 'default'. This is verified outside
# template compilation time. # template compilation time.
@ -796,8 +786,9 @@ class ComponentTemplateSyntaxErrorTests(BaseTestCase):
""" """
Template(template_str) Template(template_str)
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_text_outside_fill_tag_is_not_error_when_no_fill_tags(self): def test_text_outside_fill_tag_is_not_error_when_no_fill_tags(self, components_settings):
registry.register("test", SlottedComponent)
# As of v0.28 this is valid, provided the component registered under "test" # As of v0.28 this is valid, provided the component registered under "test"
# contains a slot tag marked as 'default'. This is verified outside # contains a slot tag marked as 'default'. This is verified outside
# template compilation time. # template compilation time.
@ -809,8 +800,9 @@ class ComponentTemplateSyntaxErrorTests(BaseTestCase):
""" """
Template(template_str) Template(template_str)
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_text_outside_fill_tag_is_error_when_fill_tags(self): def test_text_outside_fill_tag_is_error_when_fill_tags(self, components_settings):
registry.register("test", SlottedComponent)
template_str: types.django_html = """ template_str: types.django_html = """
{% load component_tags %} {% load component_tags %}
{% component "test" %} {% component "test" %}
@ -820,17 +812,18 @@ class ComponentTemplateSyntaxErrorTests(BaseTestCase):
""" """
template = Template(template_str) template = Template(template_str)
with self.assertRaisesMessage( with pytest.raises(
TemplateSyntaxError, TemplateSyntaxError,
"Illegal content passed to component 'test'. Explicit 'fill' tags cannot occur alongside other text", match=re.escape("Illegal content passed to component 'test'. Explicit 'fill' tags cannot occur alongside other text"), # noqa: E501
): ):
template.render(Context()) template.render(Context())
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_unclosed_component_is_error(self): def test_unclosed_component_is_error(self, components_settings):
with self.assertRaisesMessage( registry.register("test", SlottedComponent)
with pytest.raises(
TemplateSyntaxError, TemplateSyntaxError,
"Unclosed tag on line 3: 'component'", match=re.escape("Unclosed tag on line 3: 'component'"),
): ):
template_str: types.django_html = """ template_str: types.django_html = """
{% load component_tags %} {% load component_tags %}

View file

@ -1,11 +1,12 @@
"""Catch-all for tests that use template tags and don't fit other files""" """Catch-all for tests that use template tags and don't fit other files"""
from django.template import Context, Template from django.template import Context, Template
from pytest_django.asserts import assertHTMLEqual
from django_components import Component, register, registry, types from django_components import Component, register, registry, types
from .django_test_setup import setup_test_config from django_components.testing import djc_test
from .testutils import BaseTestCase, parametrize_context_behavior from .testutils import PARAMETRIZE_CONTEXT_BEHAVIOR, setup_test_config
setup_test_config({"autodiscover": False}) setup_test_config({"autodiscover": False})
@ -32,9 +33,10 @@ class RelativeFileComponentUsingGetTemplateName(Component):
####################### #######################
class ExtendsCompatTests(BaseTestCase): @djc_test
@parametrize_context_behavior(["isolated", "django"]) class TestExtendsCompat:
def test_double_extends_on_main_template_and_component_one_component(self): @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_double_extends_on_main_template_and_component_one_component(self, components_settings):
registry.register("blocked_and_slotted_component", BlockedAndSlottedComponent) registry.register("blocked_and_slotted_component", BlockedAndSlottedComponent)
@register("extended_component") @register("extended_component")
@ -76,10 +78,10 @@ class ExtendsCompatTests(BaseTestCase):
</body> </body>
</html> </html>
""" """
self.assertHTMLEqual(rendered, expected) assertHTMLEqual(rendered, expected)
@parametrize_context_behavior(["isolated", "django"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_double_extends_on_main_template_and_component_two_identical_components(self): def test_double_extends_on_main_template_and_component_two_identical_components(self, components_settings):
registry.register("blocked_and_slotted_component", BlockedAndSlottedComponent) registry.register("blocked_and_slotted_component", BlockedAndSlottedComponent)
@register("extended_component") @register("extended_component")
@ -132,10 +134,11 @@ class ExtendsCompatTests(BaseTestCase):
</body> </body>
</html> </html>
""" """
self.assertHTMLEqual(rendered, expected)
@parametrize_context_behavior(["isolated", "django"]) assertHTMLEqual(rendered, expected)
def test_double_extends_on_main_template_and_component_two_different_components_same_parent(self):
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_double_extends_on_main_template_and_component_two_different_components_same_parent(self, components_settings): # noqa: E501
registry.register("blocked_and_slotted_component", BlockedAndSlottedComponent) registry.register("blocked_and_slotted_component", BlockedAndSlottedComponent)
@register("extended_component") @register("extended_component")
@ -199,10 +202,10 @@ class ExtendsCompatTests(BaseTestCase):
</html> </html>
""" """
self.assertHTMLEqual(rendered, expected) assertHTMLEqual(rendered, expected)
@parametrize_context_behavior(["isolated", "django"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_double_extends_on_main_template_and_component_two_different_components_different_parent(self): def test_double_extends_on_main_template_and_component_two_different_components_different_parent(self, components_settings): # noqa: E501
registry.register("blocked_and_slotted_component", BlockedAndSlottedComponent) registry.register("blocked_and_slotted_component", BlockedAndSlottedComponent)
@register("extended_component") @register("extended_component")
@ -264,10 +267,10 @@ class ExtendsCompatTests(BaseTestCase):
</body> </body>
</html> </html>
""" """
self.assertHTMLEqual(rendered, expected) assertHTMLEqual(rendered, expected)
@parametrize_context_behavior(["isolated", "django"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_extends_on_component_one_component(self): def test_extends_on_component_one_component(self, components_settings):
registry.register("blocked_and_slotted_component", BlockedAndSlottedComponent) registry.register("blocked_and_slotted_component", BlockedAndSlottedComponent)
@register("extended_component") @register("extended_component")
@ -307,10 +310,10 @@ class ExtendsCompatTests(BaseTestCase):
</body> </body>
</html> </html>
""" """
self.assertHTMLEqual(rendered, expected) assertHTMLEqual(rendered, expected)
@parametrize_context_behavior(["isolated", "django"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_extends_on_component_two_component(self): def test_extends_on_component_two_component(self, components_settings):
registry.register("blocked_and_slotted_component", BlockedAndSlottedComponent) registry.register("blocked_and_slotted_component", BlockedAndSlottedComponent)
@register("extended_component") @register("extended_component")
@ -361,10 +364,10 @@ class ExtendsCompatTests(BaseTestCase):
</body> </body>
</html> </html>
""" """
self.assertHTMLEqual(rendered, expected) assertHTMLEqual(rendered, expected)
@parametrize_context_behavior(["isolated", "django"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_double_extends_on_main_template_and_nested_component(self): def test_double_extends_on_main_template_and_nested_component(self, components_settings):
registry.register("slotted_component", SlottedComponent) registry.register("slotted_component", SlottedComponent)
registry.register("blocked_and_slotted_component", BlockedAndSlottedComponent) registry.register("blocked_and_slotted_component", BlockedAndSlottedComponent)
@ -403,8 +406,8 @@ class ExtendsCompatTests(BaseTestCase):
<custom-template data-djc-id-a1bc42> <custom-template data-djc-id-a1bc42>
<header>Default header</header> <header>Default header</header>
<main> <main>
<div data-djc-id-a1bc46>BLOCK OVERRIDEN</div> <div data-djc-id-a1bc49>BLOCK OVERRIDEN</div>
<custom-template data-djc-id-a1bc46> <custom-template data-djc-id-a1bc49>
<header>SLOT OVERRIDEN</header> <header>SLOT OVERRIDEN</header>
<main>Default main</main> <main>Default main</main>
<footer>Default footer</footer> <footer>Default footer</footer>
@ -418,10 +421,10 @@ class ExtendsCompatTests(BaseTestCase):
</html> </html>
""" """
self.assertHTMLEqual(rendered, expected) assertHTMLEqual(rendered, expected)
@parametrize_context_behavior(["isolated", "django"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_double_extends_on_main_template_and_nested_component_and_include(self): def test_double_extends_on_main_template_and_nested_component_and_include(self, components_settings):
registry.register("slotted_component", SlottedComponent) registry.register("slotted_component", SlottedComponent)
registry.register("blocked_and_slotted_component", BlockedAndSlottedComponent) registry.register("blocked_and_slotted_component", BlockedAndSlottedComponent)
@ -452,15 +455,15 @@ class ExtendsCompatTests(BaseTestCase):
</body> </body>
</html> </html>
""" """
self.assertHTMLEqual(rendered, expected) assertHTMLEqual(rendered, expected)
# second rendering after cache built # second rendering after cache built
rendered_2 = Template(template).render(Context()) rendered_2 = Template(template).render(Context())
expected_2 = expected.replace("data-djc-id-a1bc3f", "data-djc-id-a1bc41") expected_2 = expected.replace("data-djc-id-a1bc3f", "data-djc-id-a1bc41")
self.assertHTMLEqual(rendered_2, expected_2) assertHTMLEqual(rendered_2, expected_2)
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_slots_inside_extends(self): def test_slots_inside_extends(self, components_settings):
registry.register("slotted_component", SlottedComponent) registry.register("slotted_component", SlottedComponent)
@register("slot_inside_extends") @register("slot_inside_extends")
@ -490,10 +493,10 @@ class ExtendsCompatTests(BaseTestCase):
</body> </body>
</html> </html>
""" """
self.assertHTMLEqual(rendered, expected) assertHTMLEqual(rendered, expected)
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_slots_inside_include(self): def test_slots_inside_include(self, components_settings):
registry.register("slotted_component", SlottedComponent) registry.register("slotted_component", SlottedComponent)
@register("slot_inside_include") @register("slot_inside_include")
@ -523,10 +526,10 @@ class ExtendsCompatTests(BaseTestCase):
</body> </body>
</html> </html>
""" """
self.assertHTMLEqual(rendered, expected) assertHTMLEqual(rendered, expected)
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_component_inside_block(self): def test_component_inside_block(self, components_settings):
registry.register("slotted_component", SlottedComponent) registry.register("slotted_component", SlottedComponent)
template: types.django_html = """ template: types.django_html = """
{% extends "block.html" %} {% extends "block.html" %}
@ -558,10 +561,10 @@ class ExtendsCompatTests(BaseTestCase):
</body> </body>
</html> </html>
""" """
self.assertHTMLEqual(rendered, expected) assertHTMLEqual(rendered, expected)
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_block_inside_component(self): def test_block_inside_component(self, components_settings):
registry.register("slotted_component", SlottedComponent) registry.register("slotted_component", SlottedComponent)
template: types.django_html = """ template: types.django_html = """
@ -587,10 +590,10 @@ class ExtendsCompatTests(BaseTestCase):
</body> </body>
</html> </html>
""" """
self.assertHTMLEqual(rendered, expected) assertHTMLEqual(rendered, expected)
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_block_inside_component_parent(self): def test_block_inside_component_parent(self, components_settings):
registry.register("slotted_component", SlottedComponent) registry.register("slotted_component", SlottedComponent)
@register("block_in_component_parent") @register("block_in_component_parent")
@ -616,10 +619,10 @@ class ExtendsCompatTests(BaseTestCase):
</body> </body>
</html> </html>
""" """
self.assertHTMLEqual(rendered, expected) assertHTMLEqual(rendered, expected)
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_block_does_not_affect_inside_component(self): def test_block_does_not_affect_inside_component(self, components_settings):
""" """
Assert that when we call a component with `{% component %}`, that Assert that when we call a component with `{% component %}`, that
the `{% block %}` will NOT affect the inner component. the `{% block %}` will NOT affect the inner component.
@ -655,10 +658,10 @@ class ExtendsCompatTests(BaseTestCase):
</html> </html>
wow wow
""" """
self.assertHTMLEqual(rendered, expected) assertHTMLEqual(rendered, expected)
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_slot_inside_block__slot_default_block_default(self): def test_slot_inside_block__slot_default_block_default(self, components_settings):
registry.register("slotted_component", SlottedComponent) registry.register("slotted_component", SlottedComponent)
@register("slot_inside_block") @register("slot_inside_block")
@ -687,10 +690,10 @@ class ExtendsCompatTests(BaseTestCase):
</body> </body>
</html> </html>
""" """
self.assertHTMLEqual(rendered, expected) assertHTMLEqual(rendered, expected)
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_slot_inside_block__slot_default_block_override(self): def test_slot_inside_block__slot_default_block_override(self, components_settings):
registry.clear() registry.clear()
registry.register("slotted_component", SlottedComponent) registry.register("slotted_component", SlottedComponent)
@ -723,10 +726,10 @@ class ExtendsCompatTests(BaseTestCase):
</body> </body>
</html> </html>
""" """
self.assertHTMLEqual(rendered, expected) assertHTMLEqual(rendered, expected)
@parametrize_context_behavior(["isolated", "django"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_slot_inside_block__slot_overriden_block_default(self): def test_slot_inside_block__slot_overriden_block_default(self, components_settings):
registry.register("slotted_component", SlottedComponent) registry.register("slotted_component", SlottedComponent)
@register("slot_inside_block") @register("slot_inside_block")
@ -759,10 +762,10 @@ class ExtendsCompatTests(BaseTestCase):
</body> </body>
</html> </html>
""" """
self.assertHTMLEqual(rendered, expected) assertHTMLEqual(rendered, expected)
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_slot_inside_block__slot_overriden_block_overriden(self): def test_slot_inside_block__slot_overriden_block_overriden(self, components_settings):
registry.register("slotted_component", SlottedComponent) registry.register("slotted_component", SlottedComponent)
@register("slot_inside_block") @register("slot_inside_block")
@ -805,10 +808,10 @@ class ExtendsCompatTests(BaseTestCase):
</body> </body>
</html> </html>
""" """
self.assertHTMLEqual(rendered, expected) assertHTMLEqual(rendered, expected)
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_inject_inside_block(self): def test_inject_inside_block(self, components_settings):
registry.register("slotted_component", SlottedComponent) registry.register("slotted_component", SlottedComponent)
@register("injectee") @register("injectee")
@ -837,17 +840,17 @@ class ExtendsCompatTests(BaseTestCase):
<custom-template data-djc-id-a1bc44> <custom-template data-djc-id-a1bc44>
<header></header> <header></header>
<main> <main>
<div data-djc-id-a1bc48> injected: DepInject(hello='from_block') </div> <div data-djc-id-a1bc4b> injected: DepInject(hello='from_block') </div>
</main> </main>
<footer>Default footer</footer> <footer>Default footer</footer>
</custom-template> </custom-template>
</body> </body>
</html> </html>
""" """
self.assertHTMLEqual(rendered, expected) assertHTMLEqual(rendered, expected)
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_component_using_template_file_extends_relative_file(self): def test_component_using_template_file_extends_relative_file(self, components_settings):
registry.register("relative_file_component_using_template_file", RelativeFileComponentUsingTemplateFile) registry.register("relative_file_component_using_template_file", RelativeFileComponentUsingTemplateFile)
template: types.django_html = """ template: types.django_html = """
@ -867,10 +870,10 @@ class ExtendsCompatTests(BaseTestCase):
</body> </body>
</html> </html>
""" """
self.assertHTMLEqual(rendered, expected) assertHTMLEqual(rendered, expected)
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_component_using_get_template_name_extends_relative_file(self): def test_component_using_get_template_name_extends_relative_file(self, components_settings):
registry.register("relative_file_component_using_get_template_name", RelativeFileComponentUsingGetTemplateName) registry.register("relative_file_component_using_get_template_name", RelativeFileComponentUsingGetTemplateName)
template: types.django_html = """ template: types.django_html = """
@ -890,4 +893,4 @@ class ExtendsCompatTests(BaseTestCase):
</body> </body>
</html> </html>
""" """
self.assertHTMLEqual(rendered, expected) assertHTMLEqual(rendered, expected)

View file

@ -1,24 +1,28 @@
import re
from typing import Any from typing import Any
import pytest
from django.template import Context, Template, TemplateSyntaxError from django.template import Context, Template, TemplateSyntaxError
from pytest_django.asserts import assertHTMLEqual
from django_components import Component, register, types from django_components import Component, register, types
from django_components.perfutil.provide import provide_cache, provide_references, all_reference_ids from django_components.perfutil.provide import provide_cache, provide_references, all_reference_ids
from .django_test_setup import setup_test_config from django_components.testing import djc_test
from .testutils import BaseTestCase, parametrize_context_behavior from .testutils import PARAMETRIZE_CONTEXT_BEHAVIOR, setup_test_config
setup_test_config({"autodiscover": False}) setup_test_config({"autodiscover": False})
class ProvideTemplateTagTest(BaseTestCase): @djc_test
class TestProvideTemplateTag:
def _assert_clear_cache(self): def _assert_clear_cache(self):
self.assertEqual(provide_cache, {}) assert provide_cache == {}
self.assertEqual(provide_references, {}) assert provide_references == {}
self.assertEqual(all_reference_ids, set()) assert all_reference_ids == set()
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_provide_basic(self): def test_provide_basic(self, components_settings):
@register("injectee") @register("injectee")
class InjectComponent(Component): class InjectComponent(Component):
template: types.django_html = """ template: types.django_html = """
@ -39,7 +43,7 @@ class ProvideTemplateTagTest(BaseTestCase):
template = Template(template_str) template = Template(template_str)
rendered = template.render(Context({})) rendered = template.render(Context({}))
self.assertHTMLEqual( assertHTMLEqual(
rendered, rendered,
""" """
<div data-djc-id-a1bc41> injected: DepInject(key='hi', another=1) </div> <div data-djc-id-a1bc41> injected: DepInject(key='hi', another=1) </div>
@ -47,8 +51,8 @@ class ProvideTemplateTagTest(BaseTestCase):
) )
self._assert_clear_cache() self._assert_clear_cache()
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_provide_basic_self_closing(self): def test_provide_basic_self_closing(self, components_settings):
template_str: types.django_html = """ template_str: types.django_html = """
{% load component_tags %} {% load component_tags %}
<div> <div>
@ -58,7 +62,7 @@ class ProvideTemplateTagTest(BaseTestCase):
template = Template(template_str) template = Template(template_str)
rendered = template.render(Context({})) rendered = template.render(Context({}))
self.assertHTMLEqual( assertHTMLEqual(
rendered, rendered,
""" """
<div></div> <div></div>
@ -66,8 +70,8 @@ class ProvideTemplateTagTest(BaseTestCase):
) )
self._assert_clear_cache() self._assert_clear_cache()
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_provide_access_keys_in_python(self): def test_provide_access_keys_in_python(self, components_settings):
@register("injectee") @register("injectee")
class InjectComponent(Component): class InjectComponent(Component):
template: types.django_html = """ template: types.django_html = """
@ -92,7 +96,7 @@ class ProvideTemplateTagTest(BaseTestCase):
template = Template(template_str) template = Template(template_str)
rendered = template.render(Context({})) rendered = template.render(Context({}))
self.assertHTMLEqual( assertHTMLEqual(
rendered, rendered,
""" """
<div data-djc-id-a1bc41> key: hi </div> <div data-djc-id-a1bc41> key: hi </div>
@ -101,8 +105,8 @@ class ProvideTemplateTagTest(BaseTestCase):
) )
self._assert_clear_cache() self._assert_clear_cache()
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_provide_access_keys_in_django(self): def test_provide_access_keys_in_django(self, components_settings):
@register("injectee") @register("injectee")
class InjectComponent(Component): class InjectComponent(Component):
template: types.django_html = """ template: types.django_html = """
@ -126,7 +130,7 @@ class ProvideTemplateTagTest(BaseTestCase):
template = Template(template_str) template = Template(template_str)
rendered = template.render(Context({})) rendered = template.render(Context({}))
self.assertHTMLEqual( assertHTMLEqual(
rendered, rendered,
""" """
<div data-djc-id-a1bc41> key: hi </div> <div data-djc-id-a1bc41> key: hi </div>
@ -135,8 +139,8 @@ class ProvideTemplateTagTest(BaseTestCase):
) )
self._assert_clear_cache() self._assert_clear_cache()
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_provide_does_not_leak(self): def test_provide_does_not_leak(self, components_settings):
@register("injectee") @register("injectee")
class InjectComponent(Component): class InjectComponent(Component):
template: types.django_html = """ template: types.django_html = """
@ -157,7 +161,7 @@ class ProvideTemplateTagTest(BaseTestCase):
template = Template(template_str) template = Template(template_str)
rendered = template.render(Context({})) rendered = template.render(Context({}))
self.assertHTMLEqual( assertHTMLEqual(
rendered, rendered,
""" """
<div data-djc-id-a1bc41> injected: default </div> <div data-djc-id-a1bc41> injected: default </div>
@ -165,8 +169,8 @@ class ProvideTemplateTagTest(BaseTestCase):
) )
self._assert_clear_cache() self._assert_clear_cache()
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_provide_empty(self): def test_provide_empty(self, components_settings):
"""Check provide tag with no kwargs""" """Check provide tag with no kwargs"""
@register("injectee") @register("injectee")
@ -191,7 +195,7 @@ class ProvideTemplateTagTest(BaseTestCase):
template = Template(template_str) template = Template(template_str)
rendered = template.render(Context({})) rendered = template.render(Context({}))
self.assertHTMLEqual( assertHTMLEqual(
rendered, rendered,
""" """
<div data-djc-id-a1bc42> injected: DepInject() </div> <div data-djc-id-a1bc42> injected: DepInject() </div>
@ -200,7 +204,7 @@ class ProvideTemplateTagTest(BaseTestCase):
) )
self._assert_clear_cache() self._assert_clear_cache()
@parametrize_context_behavior(["django"]) @djc_test(components_settings={"context_behavior": "django"})
def test_provide_no_inject(self): def test_provide_no_inject(self):
"""Check that nothing breaks if we do NOT inject even if some data is provided""" """Check that nothing breaks if we do NOT inject even if some data is provided"""
@ -225,7 +229,7 @@ class ProvideTemplateTagTest(BaseTestCase):
template = Template(template_str) template = Template(template_str)
rendered = template.render(Context({})) rendered = template.render(Context({}))
self.assertHTMLEqual( assertHTMLEqual(
rendered, rendered,
""" """
<div data-djc-id-a1bc42></div> <div data-djc-id-a1bc42></div>
@ -234,8 +238,8 @@ class ProvideTemplateTagTest(BaseTestCase):
) )
self._assert_clear_cache() self._assert_clear_cache()
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_provide_name_single_quotes(self): def test_provide_name_single_quotes(self, components_settings):
@register("injectee") @register("injectee")
class InjectComponent(Component): class InjectComponent(Component):
template: types.django_html = """ template: types.django_html = """
@ -258,7 +262,7 @@ class ProvideTemplateTagTest(BaseTestCase):
template = Template(template_str) template = Template(template_str)
rendered = template.render(Context({})) rendered = template.render(Context({}))
self.assertHTMLEqual( assertHTMLEqual(
rendered, rendered,
""" """
<div data-djc-id-a1bc42> injected: DepInject(key='hi', another=7) </div> <div data-djc-id-a1bc42> injected: DepInject(key='hi', another=7) </div>
@ -267,8 +271,8 @@ class ProvideTemplateTagTest(BaseTestCase):
) )
self._assert_clear_cache() self._assert_clear_cache()
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_provide_name_as_var(self): def test_provide_name_as_var(self, components_settings):
@register("injectee") @register("injectee")
class InjectComponent(Component): class InjectComponent(Component):
template: types.django_html = """ template: types.django_html = """
@ -297,7 +301,7 @@ class ProvideTemplateTagTest(BaseTestCase):
) )
) )
self.assertHTMLEqual( assertHTMLEqual(
rendered, rendered,
""" """
<div data-djc-id-a1bc42> injected: DepInject(key='hi', another=8) </div> <div data-djc-id-a1bc42> injected: DepInject(key='hi', another=8) </div>
@ -306,8 +310,8 @@ class ProvideTemplateTagTest(BaseTestCase):
) )
self._assert_clear_cache() self._assert_clear_cache()
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_provide_name_as_spread(self): def test_provide_name_as_spread(self, components_settings):
@register("injectee") @register("injectee")
class InjectComponent(Component): class InjectComponent(Component):
template: types.django_html = """ template: types.django_html = """
@ -340,7 +344,7 @@ class ProvideTemplateTagTest(BaseTestCase):
) )
) )
self.assertHTMLEqual( assertHTMLEqual(
rendered, rendered,
""" """
<div data-djc-id-a1bc42> injected: DepInject(key='hi', another=9) </div> <div data-djc-id-a1bc42> injected: DepInject(key='hi', another=9) </div>
@ -349,8 +353,8 @@ class ProvideTemplateTagTest(BaseTestCase):
) )
self._assert_clear_cache() self._assert_clear_cache()
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_provide_no_name_raises(self): def test_provide_no_name_raises(self, components_settings):
@register("injectee") @register("injectee")
class InjectComponent(Component): class InjectComponent(Component):
template: types.django_html = """ template: types.django_html = """
@ -370,16 +374,16 @@ class ProvideTemplateTagTest(BaseTestCase):
{% component "injectee" %} {% component "injectee" %}
{% endcomponent %} {% endcomponent %}
""" """
with self.assertRaisesMessage( with pytest.raises(
TypeError, TypeError,
"Invalid parameters for tag 'provide': missing a required argument: 'name'", match=re.escape("Invalid parameters for tag 'provide': missing a required argument: 'name'"),
): ):
Template(template_str).render(Context({})) Template(template_str).render(Context({}))
self._assert_clear_cache() self._assert_clear_cache()
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_provide_name_must_be_string_literal(self): def test_provide_name_must_be_string_literal(self, components_settings):
@register("injectee") @register("injectee")
class InjectComponent(Component): class InjectComponent(Component):
template: types.django_html = """ template: types.django_html = """
@ -399,16 +403,16 @@ class ProvideTemplateTagTest(BaseTestCase):
{% component "injectee" %} {% component "injectee" %}
{% endcomponent %} {% endcomponent %}
""" """
with self.assertRaisesMessage( with pytest.raises(
TemplateSyntaxError, TemplateSyntaxError,
"Provide tag received an empty string. Key must be non-empty and a valid identifier", match=re.escape("Provide tag received an empty string. Key must be non-empty and a valid identifier"),
): ):
Template(template_str).render(Context({})) Template(template_str).render(Context({}))
self._assert_clear_cache() self._assert_clear_cache()
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_provide_name_must_be_identifier(self): def test_provide_name_must_be_identifier(self, components_settings):
@register("injectee") @register("injectee")
class InjectComponent(Component): class InjectComponent(Component):
template: types.django_html = """ template: types.django_html = """
@ -430,12 +434,12 @@ class ProvideTemplateTagTest(BaseTestCase):
""" """
template = Template(template_str) template = Template(template_str)
with self.assertRaises(TemplateSyntaxError): with pytest.raises(TemplateSyntaxError):
template.render(Context({})) template.render(Context({}))
self._assert_clear_cache() self._assert_clear_cache()
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_provide_aggregate_dics(self): def test_provide_aggregate_dics(self, components_settings):
@register("injectee") @register("injectee")
class InjectComponent(Component): class InjectComponent(Component):
template: types.django_html = """ template: types.django_html = """
@ -456,7 +460,7 @@ class ProvideTemplateTagTest(BaseTestCase):
template = Template(template_str) template = Template(template_str)
rendered = template.render(Context({})) rendered = template.render(Context({}))
self.assertHTMLEqual( assertHTMLEqual(
rendered, rendered,
""" """
<div data-djc-id-a1bc41> injected: DepInject(var1={'key': 'hi', 'another': 13}, var2={'x': 'y'}) </div> <div data-djc-id-a1bc41> injected: DepInject(var1={'key': 'hi', 'another': 13}, var2={'x': 'y'}) </div>
@ -464,8 +468,8 @@ class ProvideTemplateTagTest(BaseTestCase):
) )
self._assert_clear_cache() self._assert_clear_cache()
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_provide_does_not_expose_kwargs_to_context(self): def test_provide_does_not_expose_kwargs_to_context(self, components_settings):
"""Check that `provide` tag doesn't assign the keys to the context like `with` tag does""" """Check that `provide` tag doesn't assign the keys to the context like `with` tag does"""
@register("injectee") @register("injectee")
@ -490,7 +494,7 @@ class ProvideTemplateTagTest(BaseTestCase):
template = Template(template_str) template = Template(template_str)
rendered = template.render(Context({"var": "123"})) rendered = template.render(Context({"var": "123"}))
self.assertHTMLEqual( assertHTMLEqual(
rendered, rendered,
""" """
var_out: 123 var_out: 123
@ -501,8 +505,8 @@ class ProvideTemplateTagTest(BaseTestCase):
) )
self._assert_clear_cache() self._assert_clear_cache()
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_provide_nested_in_provide_same_key(self): def test_provide_nested_in_provide_same_key(self, components_settings):
"""Check that inner `provide` with same key overshadows outer `provide`""" """Check that inner `provide` with same key overshadows outer `provide`"""
@register("injectee") @register("injectee")
@ -532,7 +536,7 @@ class ProvideTemplateTagTest(BaseTestCase):
template = Template(template_str) template = Template(template_str)
rendered = template.render(Context({})) rendered = template.render(Context({}))
self.assertHTMLEqual( assertHTMLEqual(
rendered, rendered,
""" """
<div data-djc-id-a1bc45> injected: DepInject(key='hi1', another=16, new=3) </div> <div data-djc-id-a1bc45> injected: DepInject(key='hi1', another=16, new=3) </div>
@ -543,8 +547,8 @@ class ProvideTemplateTagTest(BaseTestCase):
self._assert_clear_cache() self._assert_clear_cache()
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_provide_nested_in_provide_different_key(self): def test_provide_nested_in_provide_different_key(self, components_settings):
"""Check that `provide` tag with different keys don't affect each other""" """Check that `provide` tag with different keys don't affect each other"""
@register("injectee") @register("injectee")
@ -574,7 +578,7 @@ class ProvideTemplateTagTest(BaseTestCase):
template = Template(template_str) template = Template(template_str)
rendered = template.render(Context({})) rendered = template.render(Context({}))
self.assertHTMLEqual( assertHTMLEqual(
rendered, rendered,
""" """
<div data-djc-id-a1bc43> first_provide: DepInject(key='hi', another=17, lost=0) </div> <div data-djc-id-a1bc43> first_provide: DepInject(key='hi', another=17, lost=0) </div>
@ -583,8 +587,8 @@ class ProvideTemplateTagTest(BaseTestCase):
) )
self._assert_clear_cache() self._assert_clear_cache()
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_provide_in_include(self): def test_provide_in_include(self, components_settings):
@register("injectee") @register("injectee")
class InjectComponent(Component): class InjectComponent(Component):
template: types.django_html = """ template: types.django_html = """
@ -604,7 +608,7 @@ class ProvideTemplateTagTest(BaseTestCase):
template = Template(template_str) template = Template(template_str)
rendered = template.render(Context({})) rendered = template.render(Context({}))
self.assertHTMLEqual( assertHTMLEqual(
rendered, rendered,
""" """
<div> <div>
@ -614,8 +618,8 @@ class ProvideTemplateTagTest(BaseTestCase):
) )
self._assert_clear_cache() self._assert_clear_cache()
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_slot_in_provide(self): def test_slot_in_provide(self, components_settings):
@register("injectee") @register("injectee")
class InjectComponent(Component): class InjectComponent(Component):
template: types.django_html = """ template: types.django_html = """
@ -644,7 +648,7 @@ class ProvideTemplateTagTest(BaseTestCase):
template = Template(template_str) template = Template(template_str)
rendered = template.render(Context({})) rendered = template.render(Context({}))
self.assertHTMLEqual( assertHTMLEqual(
rendered, rendered,
""" """
<div data-djc-id-a1bc40 data-djc-id-a1bc44> <div data-djc-id-a1bc40 data-djc-id-a1bc44>
@ -655,14 +659,15 @@ class ProvideTemplateTagTest(BaseTestCase):
self._assert_clear_cache() self._assert_clear_cache()
class InjectTest(BaseTestCase): @djc_test
class TestInject:
def _assert_clear_cache(self): def _assert_clear_cache(self):
self.assertEqual(provide_cache, {}) assert provide_cache == {}
self.assertEqual(provide_references, {}) assert provide_references == {}
self.assertEqual(all_reference_ids, set()) assert all_reference_ids == set()
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_inject_basic(self): def test_inject_basic(self, components_settings):
@register("injectee") @register("injectee")
class InjectComponent(Component): class InjectComponent(Component):
template: types.django_html = """ template: types.django_html = """
@ -683,7 +688,7 @@ class InjectTest(BaseTestCase):
template = Template(template_str) template = Template(template_str)
rendered = template.render(Context({})) rendered = template.render(Context({}))
self.assertHTMLEqual( assertHTMLEqual(
rendered, rendered,
""" """
<div data-djc-id-a1bc41> injected: DepInject(key='hi', another=21) </div> <div data-djc-id-a1bc41> injected: DepInject(key='hi', another=21) </div>
@ -691,8 +696,8 @@ class InjectTest(BaseTestCase):
) )
self._assert_clear_cache() self._assert_clear_cache()
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_inject_missing_key_raises_without_default(self): def test_inject_missing_key_raises_without_default(self, components_settings):
@register("injectee") @register("injectee")
class InjectComponent(Component): class InjectComponent(Component):
template: types.django_html = """ template: types.django_html = """
@ -710,13 +715,13 @@ class InjectTest(BaseTestCase):
""" """
template = Template(template_str) template = Template(template_str)
with self.assertRaises(KeyError): with pytest.raises(KeyError):
template.render(Context({})) template.render(Context({}))
self._assert_clear_cache() self._assert_clear_cache()
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_inject_missing_key_ok_with_default(self): def test_inject_missing_key_ok_with_default(self, components_settings):
@register("injectee") @register("injectee")
class InjectComponent(Component): class InjectComponent(Component):
template: types.django_html = """ template: types.django_html = """
@ -734,7 +739,7 @@ class InjectTest(BaseTestCase):
""" """
template = Template(template_str) template = Template(template_str)
rendered = template.render(Context({})) rendered = template.render(Context({}))
self.assertHTMLEqual( assertHTMLEqual(
rendered, rendered,
""" """
<div data-djc-id-a1bc3f> injected: default </div> <div data-djc-id-a1bc3f> injected: default </div>
@ -742,8 +747,8 @@ class InjectTest(BaseTestCase):
) )
self._assert_clear_cache() self._assert_clear_cache()
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_inject_empty_string(self): def test_inject_empty_string(self, components_settings):
@register("injectee") @register("injectee")
class InjectComponent(Component): class InjectComponent(Component):
template: types.django_html = """ template: types.django_html = """
@ -765,13 +770,13 @@ class InjectTest(BaseTestCase):
""" """
template = Template(template_str) template = Template(template_str)
with self.assertRaises(KeyError): with pytest.raises(KeyError):
template.render(Context({})) template.render(Context({}))
self._assert_clear_cache() self._assert_clear_cache()
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_inject_raises_on_called_outside_get_context_data(self): def test_inject_raises_on_called_outside_get_context_data(self, components_settings):
@register("injectee") @register("injectee")
class InjectComponent(Component): class InjectComponent(Component):
template: types.django_html = """ template: types.django_html = """
@ -783,14 +788,14 @@ class InjectTest(BaseTestCase):
return {"var": var} return {"var": var}
comp = InjectComponent("") comp = InjectComponent("")
with self.assertRaises(RuntimeError): with pytest.raises(RuntimeError):
comp.inject("abc", "def") comp.inject("abc", "def")
self._assert_clear_cache() self._assert_clear_cache()
# See https://github.com/django-components/django-components/pull/778 # See https://github.com/django-components/django-components/pull/778
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_inject_in_fill(self): def test_inject_in_fill(self, components_settings):
@register("injectee") @register("injectee")
class Injectee(Component): class Injectee(Component):
template: types.django_html = """ template: types.django_html = """
@ -844,7 +849,7 @@ class InjectTest(BaseTestCase):
rendered = Root.render() rendered = Root.render()
self.assertHTMLEqual( assertHTMLEqual(
rendered, rendered,
""" """
<div data-djc-id-a1bc3e data-djc-id-a1bc41 data-djc-id-a1bc45 data-djc-id-a1bc49> <div data-djc-id-a1bc3e data-djc-id-a1bc41 data-djc-id-a1bc45 data-djc-id-a1bc49>
@ -858,8 +863,8 @@ class InjectTest(BaseTestCase):
self._assert_clear_cache() self._assert_clear_cache()
# See https://github.com/django-components/django-components/pull/786 # See https://github.com/django-components/django-components/pull/786
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_inject_in_slot_in_fill(self): def test_inject_in_slot_in_fill(self, components_settings):
@register("injectee") @register("injectee")
class Injectee(Component): class Injectee(Component):
template: types.django_html = """ template: types.django_html = """
@ -909,7 +914,7 @@ class InjectTest(BaseTestCase):
rendered = Root.render() rendered = Root.render()
self.assertHTMLEqual( assertHTMLEqual(
rendered, rendered,
""" """
<div data-djc-id-a1bc3e data-djc-id-a1bc41 data-djc-id-a1bc44 data-djc-id-a1bc48> <div data-djc-id-a1bc3e data-djc-id-a1bc41 data-djc-id-a1bc44 data-djc-id-a1bc48>
@ -928,15 +933,14 @@ class InjectTest(BaseTestCase):
# #
# Instead, we manage the state ourselves, and remove the cache entry # Instead, we manage the state ourselves, and remove the cache entry
# when the component rendered is done. # when the component rendered is done.
class ProvideCacheTest(BaseTestCase): @djc_test
class TestProvideCache:
def _assert_clear_cache(self): def _assert_clear_cache(self):
self.assertEqual(provide_cache, {}) assert provide_cache == {}
self.assertEqual(provide_references, {}) assert provide_references == {}
self.assertEqual(all_reference_ids, set()) assert all_reference_ids == set()
def test_provide_outside_component(self): def test_provide_outside_component(self):
tester = self
@register("injectee") @register("injectee")
class Injectee(Component): class Injectee(Component):
template: types.django_html = """ template: types.django_html = """
@ -946,7 +950,7 @@ class ProvideCacheTest(BaseTestCase):
""" """
def get_context_data(self): def get_context_data(self):
tester.assertEqual(len(provide_cache), 1) assert len(provide_cache) == 1
data = self.inject("my_provide") data = self.inject("my_provide")
return {"data": data, "ran": True} return {"data": data, "ran": True}
@ -965,7 +969,7 @@ class ProvideCacheTest(BaseTestCase):
rendered = template.render(Context({})) rendered = template.render(Context({}))
self.assertHTMLEqual( assertHTMLEqual(
rendered, rendered,
""" """
<div data-djc-id-a1bc41> <div data-djc-id-a1bc41>
@ -980,14 +984,13 @@ class ProvideCacheTest(BaseTestCase):
# Cache should be cleared even if there is an error. # Cache should be cleared even if there is an error.
def test_provide_outside_component_with_error(self): def test_provide_outside_component_with_error(self):
tester = self
@register("injectee") @register("injectee")
class Injectee(Component): class Injectee(Component):
template = "" template = ""
def get_context_data(self): def get_context_data(self):
tester.assertEqual(len(provide_cache), 1) assert len(provide_cache) == 1
data = self.inject("my_provide") data = self.inject("my_provide")
raise ValueError("Oops") raise ValueError("Oops")
@ -1005,14 +1008,12 @@ class ProvideCacheTest(BaseTestCase):
template = Template(template_str) template = Template(template_str)
self._assert_clear_cache() self._assert_clear_cache()
with self.assertRaisesMessage(ValueError, "Oops"): with pytest.raises(ValueError, match=re.escape("Oops")):
template.render(Context({})) template.render(Context({}))
self._assert_clear_cache() self._assert_clear_cache()
def test_provide_inside_component(self): def test_provide_inside_component(self):
tester = self
@register("injectee") @register("injectee")
class Injectee(Component): class Injectee(Component):
template: types.django_html = """ template: types.django_html = """
@ -1022,7 +1023,7 @@ class ProvideCacheTest(BaseTestCase):
""" """
def get_context_data(self): def get_context_data(self):
tester.assertEqual(len(provide_cache), 1) assert len(provide_cache) == 1
data = self.inject("my_provide") data = self.inject("my_provide")
return {"data": data, "ran": True} return {"data": data, "ran": True}
@ -1040,7 +1041,7 @@ class ProvideCacheTest(BaseTestCase):
rendered = Root.render() rendered = Root.render()
self.assertHTMLEqual( assertHTMLEqual(
rendered, rendered,
""" """
<div data-djc-id-a1bc3e data-djc-id-a1bc42> <div data-djc-id-a1bc3e data-djc-id-a1bc42>
@ -1054,14 +1055,12 @@ class ProvideCacheTest(BaseTestCase):
self._assert_clear_cache() self._assert_clear_cache()
def test_provide_inside_component_with_error(self): def test_provide_inside_component_with_error(self):
tester = self
@register("injectee") @register("injectee")
class Injectee(Component): class Injectee(Component):
template = "" template = ""
def get_context_data(self): def get_context_data(self):
tester.assertEqual(len(provide_cache), 1) assert len(provide_cache) == 1
data = self.inject("my_provide") data = self.inject("my_provide")
raise ValueError("Oops") raise ValueError("Oops")
@ -1078,7 +1077,7 @@ class ProvideCacheTest(BaseTestCase):
self._assert_clear_cache() self._assert_clear_cache()
with self.assertRaisesMessage(ValueError, "Oops"): with pytest.raises(ValueError, match=re.escape("Oops")):
Root.render() Root.render()
self._assert_clear_cache() self._assert_clear_cache()

File diff suppressed because it is too large Load diff

View file

@ -3,11 +3,12 @@
from typing import Any, Dict, Optional from typing import Any, Dict, Optional
from django.template import Context, Template from django.template import Context, Template
from pytest_django.asserts import assertHTMLEqual
from django_components import Component, register, registry, types from django_components import Component, register, registry, types
from .django_test_setup import setup_test_config from django_components.testing import djc_test
from .testutils import BaseTestCase, parametrize_context_behavior from .testutils import PARAMETRIZE_CONTEXT_BEHAVIOR, setup_test_config
setup_test_config({"autodiscover": False}) setup_test_config({"autodiscover": False})
@ -33,7 +34,8 @@ class SlottedComponentWithContext(SlottedComponent):
####################### #######################
class NestedSlotTests(BaseTestCase): @djc_test
class TestNestedSlot:
class NestedComponent(Component): class NestedComponent(Component):
template: types.django_html = """ template: types.django_html = """
{% load component_tags %} {% load component_tags %}
@ -42,8 +44,8 @@ class NestedSlotTests(BaseTestCase):
{% endslot %} {% endslot %}
""" """
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_default_slot_contents_render_correctly(self): def test_default_slot_contents_render_correctly(self, components_settings):
registry.clear() registry.clear()
registry.register("test", self.NestedComponent) registry.register("test", self.NestedComponent)
template_str: types.django_html = """ template_str: types.django_html = """
@ -52,10 +54,10 @@ class NestedSlotTests(BaseTestCase):
""" """
template = Template(template_str) template = Template(template_str)
rendered = template.render(Context({})) rendered = template.render(Context({}))
self.assertHTMLEqual(rendered, '<div id="outer" data-djc-id-a1bc3f>Default</div>') assertHTMLEqual(rendered, '<div id="outer" data-djc-id-a1bc3f>Default</div>')
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_inner_slot_overriden(self): def test_inner_slot_overriden(self, components_settings):
registry.clear() registry.clear()
registry.register("test", self.NestedComponent) registry.register("test", self.NestedComponent)
template_str: types.django_html = """ template_str: types.django_html = """
@ -66,10 +68,10 @@ class NestedSlotTests(BaseTestCase):
""" """
template = Template(template_str) template = Template(template_str)
rendered = template.render(Context({})) rendered = template.render(Context({}))
self.assertHTMLEqual(rendered, '<div id="outer" data-djc-id-a1bc40>Override</div>') assertHTMLEqual(rendered, '<div id="outer" data-djc-id-a1bc40>Override</div>')
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_outer_slot_overriden(self): def test_outer_slot_overriden(self, components_settings):
registry.clear() registry.clear()
registry.register("test", self.NestedComponent) registry.register("test", self.NestedComponent)
template_str: types.django_html = """ template_str: types.django_html = """
@ -78,10 +80,10 @@ class NestedSlotTests(BaseTestCase):
""" """
template = Template(template_str) template = Template(template_str)
rendered = template.render(Context({})) rendered = template.render(Context({}))
self.assertHTMLEqual(rendered, "<p data-djc-id-a1bc40>Override</p>") assertHTMLEqual(rendered, "<p data-djc-id-a1bc40>Override</p>")
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_both_overriden_and_inner_removed(self): def test_both_overriden_and_inner_removed(self, components_settings):
registry.clear() registry.clear()
registry.register("test", self.NestedComponent) registry.register("test", self.NestedComponent)
template_str: types.django_html = """ template_str: types.django_html = """
@ -93,13 +95,22 @@ class NestedSlotTests(BaseTestCase):
""" """
template = Template(template_str) template = Template(template_str)
rendered = template.render(Context({})) rendered = template.render(Context({}))
self.assertHTMLEqual(rendered, "<p data-djc-id-a1bc41>Override</p>") assertHTMLEqual(rendered, "<p data-djc-id-a1bc41>Override</p>")
# NOTE: Second arg in tuple is expected name in nested fill. In "django" mode, # NOTE: Second arg in tuple is expected name in nested fill. In "django" mode,
# the value should be overridden by the component, while in "isolated" it should # the value should be overridden by the component, while in "isolated" it should
# remain top-level context. # remain top-level context.
@parametrize_context_behavior([("django", "Joe2"), ("isolated", "Jannete")]) @djc_test(
def test_fill_inside_fill_with_same_name(self, context_behavior_data): parametrize=(
["components_settings", "expected"],
[
[{"context_behavior": "django"}, "Joe2"],
[{"context_behavior": "isolated"}, "Jannete"],
],
["django", "isolated"],
)
)
def test_fill_inside_fill_with_same_name(self, components_settings, expected):
class SlottedComponent(Component): class SlottedComponent(Component):
template: types.django_html = """ template: types.django_html = """
{% load component_tags %} {% load component_tags %}
@ -142,13 +153,13 @@ class NestedSlotTests(BaseTestCase):
self.template = Template(template_str) self.template = Template(template_str)
rendered = self.template.render(Context({"day": "Monday", "name": "Jannete"})) rendered = self.template.render(Context({"day": "Monday", "name": "Jannete"}))
self.assertHTMLEqual( assertHTMLEqual(
rendered, rendered,
f""" f"""
<custom-template data-djc-id-a1bc45> <custom-template data-djc-id-a1bc45>
<header> <header>
<custom-template data-djc-id-a1bc49> <custom-template data-djc-id-a1bc49>
<header>Name2: {context_behavior_data}</header> <header>Name2: {expected}</header>
<main>Day2: Monday</main> <main>Day2: Monday</main>
<footer>XYZ</footer> <footer>XYZ</footer>
</custom-template> </custom-template>
@ -162,7 +173,8 @@ class NestedSlotTests(BaseTestCase):
# NOTE: This test group are kept for backward compatibility, as the same logic # NOTE: This test group are kept for backward compatibility, as the same logic
# as provided by {% if %} tags was previously provided by this library. # as provided by {% if %} tags was previously provided by this library.
class ConditionalSlotTests(BaseTestCase): @djc_test
class TestConditionalSlot:
class ConditionalComponent(Component): class ConditionalComponent(Component):
template: types.django_html = """ template: types.django_html = """
{% load component_tags %} {% load component_tags %}
@ -176,12 +188,9 @@ class ConditionalSlotTests(BaseTestCase):
def get_context_data(self, branch=None): def get_context_data(self, branch=None):
return {"branch": branch} return {"branch": branch}
def setUp(self): @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
super().setUp() def test_no_content_if_branches_are_false(self, components_settings):
registry.register("test", self.ConditionalComponent) registry.register("test", self.ConditionalComponent)
@parametrize_context_behavior(["django", "isolated"])
def test_no_content_if_branches_are_false(self):
template_str: types.django_html = """ template_str: types.django_html = """
{% load component_tags %} {% load component_tags %}
{% component 'test' %} {% component 'test' %}
@ -191,10 +200,11 @@ class ConditionalSlotTests(BaseTestCase):
""" """
template = Template(template_str) template = Template(template_str)
rendered = template.render(Context({})) rendered = template.render(Context({}))
self.assertHTMLEqual(rendered, "") assertHTMLEqual(rendered, "")
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_default_content_if_no_slots(self): def test_default_content_if_no_slots(self, components_settings):
registry.register("test", self.ConditionalComponent)
template_str: types.django_html = """ template_str: types.django_html = """
{% load component_tags %} {% load component_tags %}
{% component 'test' branch='a' %}{% endcomponent %} {% component 'test' branch='a' %}{% endcomponent %}
@ -202,7 +212,7 @@ class ConditionalSlotTests(BaseTestCase):
""" """
template = Template(template_str) template = Template(template_str)
rendered = template.render(Context({})) rendered = template.render(Context({}))
self.assertHTMLEqual( assertHTMLEqual(
rendered, rendered,
""" """
<p id="a" data-djc-id-a1bc40>Default A</p> <p id="a" data-djc-id-a1bc40>Default A</p>
@ -210,8 +220,9 @@ class ConditionalSlotTests(BaseTestCase):
""", """,
) )
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_one_slot_overridden(self): def test_one_slot_overridden(self, components_settings):
registry.register("test", self.ConditionalComponent)
template_str: types.django_html = """ template_str: types.django_html = """
{% load component_tags %} {% load component_tags %}
{% component 'test' branch='a' %} {% component 'test' branch='a' %}
@ -223,7 +234,7 @@ class ConditionalSlotTests(BaseTestCase):
""" """
template = Template(template_str) template = Template(template_str)
rendered = template.render(Context({})) rendered = template.render(Context({}))
self.assertHTMLEqual( assertHTMLEqual(
rendered, rendered,
""" """
<p id="a" data-djc-id-a1bc42>Default A</p> <p id="a" data-djc-id-a1bc42>Default A</p>
@ -231,8 +242,9 @@ class ConditionalSlotTests(BaseTestCase):
""", """,
) )
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_both_slots_overridden(self): def test_both_slots_overridden(self, components_settings):
registry.register("test", self.ConditionalComponent)
template_str: types.django_html = """ template_str: types.django_html = """
{% load component_tags %} {% load component_tags %}
{% component 'test' branch='a' %} {% component 'test' branch='a' %}
@ -246,7 +258,7 @@ class ConditionalSlotTests(BaseTestCase):
""" """
template = Template(template_str) template = Template(template_str)
rendered = template.render(Context({})) rendered = template.render(Context({}))
self.assertHTMLEqual( assertHTMLEqual(
rendered, rendered,
""" """
<p id="a" data-djc-id-a1bc44>Override A</p> <p id="a" data-djc-id-a1bc44>Override A</p>
@ -255,7 +267,8 @@ class ConditionalSlotTests(BaseTestCase):
) )
class SlotIterationTest(BaseTestCase): @djc_test
class TestSlotIteration:
"""Tests a behaviour of {% fill .. %} tag which is inside a template {% for .. %} loop.""" """Tests a behaviour of {% fill .. %} tag which is inside a template {% for .. %} loop."""
class ComponentSimpleSlotInALoop(Component): class ComponentSimpleSlotInALoop(Component):
@ -274,13 +287,17 @@ class SlotIterationTest(BaseTestCase):
} }
# NOTE: Second arg in tuple is expected result. In isolated mode, loops should NOT leak. # NOTE: Second arg in tuple is expected result. In isolated mode, loops should NOT leak.
@parametrize_context_behavior( @djc_test(
parametrize=(
["components_settings", "expected"],
[ [
("django", "OBJECT1 OBJECT2"), [{"context_behavior": "django"}, "OBJECT1 OBJECT2"],
("isolated", ""), [{"context_behavior": "isolated"}, ""],
] ],
["django", "isolated"],
) )
def test_inner_slot_iteration_basic(self, context_behavior_data): )
def test_inner_slot_iteration_basic(self, components_settings, expected):
registry.register("slot_in_a_loop", self.ComponentSimpleSlotInALoop) registry.register("slot_in_a_loop", self.ComponentSimpleSlotInALoop)
template_str: types.django_html = """ template_str: types.django_html = """
@ -295,17 +312,21 @@ class SlotIterationTest(BaseTestCase):
objects = ["OBJECT1", "OBJECT2"] objects = ["OBJECT1", "OBJECT2"]
rendered = template.render(Context({"objects": objects})) rendered = template.render(Context({"objects": objects}))
self.assertHTMLEqual(rendered, context_behavior_data) assertHTMLEqual(rendered, expected)
# NOTE: Second arg in tuple is expected result. In isolated mode, while loops should NOT leak, # NOTE: Second arg in tuple is expected result. In isolated mode, while loops should NOT leak,
# we should still have access to root context (returned from get_context_data) # we should still have access to root context (returned from get_context_data)
@parametrize_context_behavior( @djc_test(
parametrize=(
["components_settings", "expected"],
[ [
("django", "OUTER_SCOPE_VARIABLE OBJECT1 OUTER_SCOPE_VARIABLE OBJECT2"), [{"context_behavior": "django"}, "OUTER_SCOPE_VARIABLE OBJECT1 OUTER_SCOPE_VARIABLE OBJECT2"],
("isolated", "OUTER_SCOPE_VARIABLE OUTER_SCOPE_VARIABLE"), [{"context_behavior": "isolated"}, "OUTER_SCOPE_VARIABLE OUTER_SCOPE_VARIABLE"],
] ],
["django", "isolated"],
) )
def test_inner_slot_iteration_with_variable_from_outer_scope(self, context_behavior_data): )
def test_inner_slot_iteration_with_variable_from_outer_scope(self, components_settings, expected):
registry.register("slot_in_a_loop", self.ComponentSimpleSlotInALoop) registry.register("slot_in_a_loop", self.ComponentSimpleSlotInALoop)
template_str: types.django_html = """ template_str: types.django_html = """
@ -328,16 +349,20 @@ class SlotIterationTest(BaseTestCase):
) )
) )
self.assertHTMLEqual(rendered, context_behavior_data) assertHTMLEqual(rendered, expected)
# NOTE: Second arg in tuple is expected result. In isolated mode, loops should NOT leak. # NOTE: Second arg in tuple is expected result. In isolated mode, loops should NOT leak.
@parametrize_context_behavior( @djc_test(
parametrize=(
["components_settings", "expected"],
[ [
("django", "ITER1_OBJ1 ITER1_OBJ2 ITER2_OBJ1 ITER2_OBJ2"), [{"context_behavior": "django"}, "ITER1_OBJ1 ITER1_OBJ2 ITER2_OBJ1 ITER2_OBJ2"],
("isolated", ""), [{"context_behavior": "isolated"}, ""],
] ],
["django", "isolated"],
) )
def test_inner_slot_iteration_nested(self, context_behavior_data): )
def test_inner_slot_iteration_nested(self, components_settings, expected):
registry.register("slot_in_a_loop", self.ComponentSimpleSlotInALoop) registry.register("slot_in_a_loop", self.ComponentSimpleSlotInALoop)
objects = [ objects = [
@ -360,14 +385,16 @@ class SlotIterationTest(BaseTestCase):
template = Template(template_str) template = Template(template_str)
rendered = template.render(Context({"objects": objects})) rendered = template.render(Context({"objects": objects}))
self.assertHTMLEqual(rendered, context_behavior_data) assertHTMLEqual(rendered, expected)
# NOTE: Second arg in tuple is expected result. In isolated mode, while loops should NOT leak, # NOTE: Second arg in tuple is expected result. In isolated mode, while loops should NOT leak,
# we should still have access to root context (returned from get_context_data) # we should still have access to root context (returned from get_context_data)
@parametrize_context_behavior( @djc_test(
parametrize=(
["components_settings", "expected"],
[ [
( [
"django", {"context_behavior": "django"},
""" """
OUTER_SCOPE_VARIABLE1 OUTER_SCOPE_VARIABLE1
OUTER_SCOPE_VARIABLE2 OUTER_SCOPE_VARIABLE2
@ -380,11 +407,13 @@ class SlotIterationTest(BaseTestCase):
OUTER_SCOPE_VARIABLE2 OUTER_SCOPE_VARIABLE2
ITER2_OBJ2 ITER2_OBJ2
""", """,
), ],
("isolated", "OUTER_SCOPE_VARIABLE1 OUTER_SCOPE_VARIABLE1"), [{"context_behavior": "isolated"}, "OUTER_SCOPE_VARIABLE1 OUTER_SCOPE_VARIABLE1"],
] ],
["django", "isolated"],
) )
def test_inner_slot_iteration_nested_with_outer_scope_variable(self, context_behavior_data): )
def test_inner_slot_iteration_nested_with_outer_scope_variable(self, components_settings, expected):
registry.register("slot_in_a_loop", self.ComponentSimpleSlotInALoop) registry.register("slot_in_a_loop", self.ComponentSimpleSlotInALoop)
objects = [ objects = [
@ -417,16 +446,20 @@ class SlotIterationTest(BaseTestCase):
) )
) )
self.assertHTMLEqual(rendered, context_behavior_data) assertHTMLEqual(rendered, expected)
# NOTE: Second arg in tuple is expected result. In isolated mode, loops should NOT leak. # NOTE: Second arg in tuple is expected result. In isolated mode, loops should NOT leak.
@parametrize_context_behavior( @djc_test(
parametrize=(
["components_settings", "expected"],
[ [
("django", "ITER1_OBJ1 default ITER1_OBJ2 default ITER2_OBJ1 default ITER2_OBJ2 default"), [{"context_behavior": "django"}, "ITER1_OBJ1 default ITER1_OBJ2 default ITER2_OBJ1 default ITER2_OBJ2 default"], # noqa: E501
("isolated", ""), [{"context_behavior": "isolated"}, ""],
] ],
["django", "isolated"],
) )
def test_inner_slot_iteration_nested_with_slot_default(self, context_behavior_data): )
def test_inner_slot_iteration_nested_with_slot_default(self, components_settings, expected):
registry.register("slot_in_a_loop", self.ComponentSimpleSlotInALoop) registry.register("slot_in_a_loop", self.ComponentSimpleSlotInALoop)
objects = [ objects = [
@ -449,13 +482,15 @@ class SlotIterationTest(BaseTestCase):
template = Template(template_str) template = Template(template_str)
rendered = template.render(Context({"objects": objects})) rendered = template.render(Context({"objects": objects}))
self.assertHTMLEqual(rendered, context_behavior_data) assertHTMLEqual(rendered, expected)
# NOTE: Second arg in tuple is expected result. In isolated mode, loops should NOT leak. # NOTE: Second arg in tuple is expected result. In isolated mode, loops should NOT leak.
@parametrize_context_behavior( @djc_test(
parametrize=(
["components_settings", "expected"],
[ [
( [
"django", {"context_behavior": "django"},
""" """
OUTER_SCOPE_VARIABLE1 OUTER_SCOPE_VARIABLE1
OUTER_SCOPE_VARIABLE2 OUTER_SCOPE_VARIABLE2
@ -468,16 +503,19 @@ class SlotIterationTest(BaseTestCase):
OUTER_SCOPE_VARIABLE2 OUTER_SCOPE_VARIABLE2
ITER2_OBJ2 default ITER2_OBJ2 default
""", """,
), ],
# NOTE: In this case the `object.inner` in the inner "slot_in_a_loop" # NOTE: In this case the `object.inner` in the inner "slot_in_a_loop"
# should be undefined, so the loop inside the inner `slot_in_a_loop` # should be undefined, so the loop inside the inner `slot_in_a_loop`
# shouldn't run. Hence even the inner `slot_inner` fill should NOT run. # shouldn't run. Hence even the inner `slot_inner` fill should NOT run.
("isolated", "OUTER_SCOPE_VARIABLE1 OUTER_SCOPE_VARIABLE1"), [{"context_behavior": "isolated"}, "OUTER_SCOPE_VARIABLE1 OUTER_SCOPE_VARIABLE1"],
] ],
["django", "isolated"],
)
) )
def test_inner_slot_iteration_nested_with_slot_default_and_outer_scope_variable( def test_inner_slot_iteration_nested_with_slot_default_and_outer_scope_variable(
self, self,
context_behavior_data, components_settings,
expected,
): ):
registry.register("slot_in_a_loop", self.ComponentSimpleSlotInALoop) registry.register("slot_in_a_loop", self.ComponentSimpleSlotInALoop)
@ -510,9 +548,9 @@ class SlotIterationTest(BaseTestCase):
} }
) )
) )
self.assertHTMLEqual(rendered, context_behavior_data) assertHTMLEqual(rendered, expected)
@parametrize_context_behavior(["isolated"]) @djc_test(components_settings={"context_behavior": "isolated"})
def test_inner_slot_iteration_nested_with_slot_default_and_outer_scope_variable__isolated_2( def test_inner_slot_iteration_nested_with_slot_default_and_outer_scope_variable__isolated_2(
self, self,
): ):
@ -551,7 +589,7 @@ class SlotIterationTest(BaseTestCase):
) )
) )
self.assertHTMLEqual( assertHTMLEqual(
rendered, rendered,
""" """
OUTER_SCOPE_VARIABLE1 OUTER_SCOPE_VARIABLE1
@ -568,7 +606,8 @@ class SlotIterationTest(BaseTestCase):
) )
class ComponentNestingTests(BaseTestCase): @djc_test
class TestComponentNesting:
class CalendarComponent(Component): class CalendarComponent(Component):
"""Nested in ComponentWithNestedComponent""" """Nested in ComponentWithNestedComponent"""
@ -604,17 +643,22 @@ class ComponentNestingTests(BaseTestCase):
</div> </div>
""" """
def setUp(self) -> None:
super().setUp()
registry.register("dashboard", self.DashboardComponent)
registry.register("calendar", self.CalendarComponent)
# NOTE: Second arg in tuple are expected names in nested fills. In "django" mode, # NOTE: Second arg in tuple are expected names in nested fills. In "django" mode,
# the value should be overridden by the component, while in "isolated" it should # the value should be overridden by the component, while in "isolated" it should
# remain top-level context. # remain top-level context.
@parametrize_context_behavior([("django", ("Igor", "Joe2")), ("isolated", ("Jannete", "Jannete"))]) @djc_test(
def test_component_inside_slot(self, context_behavior_data): parametrize=(
first_name, second_name = context_behavior_data ["components_settings", "first_name", "second_name"],
[
[{"context_behavior": "django"}, "Igor", "Joe2"],
[{"context_behavior": "isolated"}, "Jannete", "Jannete"],
],
["django", "isolated"],
)
)
def test_component_inside_slot(self, components_settings, first_name, second_name):
registry.register("dashboard", self.DashboardComponent)
registry.register("calendar", self.CalendarComponent)
class SlottedComponent(Component): class SlottedComponent(Component):
template: types.django_html = """ template: types.django_html = """
@ -657,7 +701,7 @@ class ComponentNestingTests(BaseTestCase):
self.template = Template(template_str) self.template = Template(template_str)
rendered = self.template.render(Context({"day": "Monday", "name": "Jannete"})) rendered = self.template.render(Context({"day": "Monday", "name": "Jannete"}))
self.assertHTMLEqual( assertHTMLEqual(
rendered, rendered,
f""" f"""
<custom-template data-djc-id-a1bc45> <custom-template data-djc-id-a1bc45>
@ -675,13 +719,20 @@ class ComponentNestingTests(BaseTestCase):
) )
# NOTE: Second arg in tuple is expected list content. In isolated mode, loops should NOT leak. # NOTE: Second arg in tuple is expected list content. In isolated mode, loops should NOT leak.
@parametrize_context_behavior( @djc_test(
parametrize=(
["components_settings", "expected"],
[ [
("django", "<li>1</li> <li>2</li> <li>3</li>"), [{"context_behavior": "django"}, "<li>1</li> <li>2</li> <li>3</li>"],
("isolated", ""), [{"context_behavior": "isolated"}, ""],
] ],
["django", "isolated"],
) )
def test_component_nesting_component_without_fill(self, context_behavior_data): )
def test_component_nesting_component_without_fill(self, components_settings, expected):
registry.register("dashboard", self.DashboardComponent)
registry.register("calendar", self.CalendarComponent)
template_str: types.django_html = """ template_str: types.django_html = """
{% load component_tags %} {% load component_tags %}
{% component "dashboard" %}{% endcomponent %} {% component "dashboard" %}{% endcomponent %}
@ -699,20 +750,27 @@ class ComponentNestingTests(BaseTestCase):
</main> </main>
</div> </div>
<ol> <ol>
{context_behavior_data} {expected}
</ol> </ol>
</div> </div>
""" """
self.assertHTMLEqual(rendered, expected) assertHTMLEqual(rendered, expected)
# NOTE: Second arg in tuple is expected list content. In isolated mode, loops should NOT leak. # NOTE: Second arg in tuple is expected list content. In isolated mode, loops should NOT leak.
@parametrize_context_behavior( @djc_test(
parametrize=(
["components_settings", "expected"],
[ [
("django", "<li>1</li> <li>2</li> <li>3</li>"), [{"context_behavior": "django"}, "<li>1</li> <li>2</li> <li>3</li>"],
("isolated", ""), [{"context_behavior": "isolated"}, ""],
] ],
["django", "isolated"],
) )
def test_component_nesting_slot_inside_component_fill(self, context_behavior_data): )
def test_component_nesting_slot_inside_component_fill(self, components_settings, expected):
registry.register("dashboard", self.DashboardComponent)
registry.register("calendar", self.CalendarComponent)
template_str: types.django_html = """ template_str: types.django_html = """
{% load component_tags %} {% load component_tags %}
{% component "dashboard" %} {% component "dashboard" %}
@ -734,14 +792,14 @@ class ComponentNestingTests(BaseTestCase):
</main> </main>
</div> </div>
<ol> <ol>
{context_behavior_data} {expected}
</ol> </ol>
</div> </div>
""" """
self.assertHTMLEqual(rendered, expected) assertHTMLEqual(rendered, expected)
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_component_nesting_deep_slot_inside_component_fill(self): def test_component_nesting_deep_slot_inside_component_fill(self, components_settings):
@register("complex_child") @register("complex_child")
class ComplexChildComponent(Component): class ComplexChildComponent(Component):
template: types.django_html = """ template: types.django_html = """
@ -790,13 +848,13 @@ class ComponentNestingTests(BaseTestCase):
<div data-djc-id-a1bc44> 3 </div> <div data-djc-id-a1bc44> 3 </div>
</li> </li>
""" """
self.assertHTMLEqual(rendered, expected) assertHTMLEqual(rendered, expected)
# This test is based on real-life example. # This test is based on real-life example.
# It ensures that deeply nested slots in fills with same names are resolved correctly. # It ensures that deeply nested slots in fills with same names are resolved correctly.
# It also ensures that the component_vars.is_filled context is correctly populated. # It also ensures that the component_vars.is_filled context is correctly populated.
@parametrize_context_behavior(["django", "isolated"]) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_component_nesting_deep_slot_inside_component_fill_2(self): def test_component_nesting_deep_slot_inside_component_fill_2(self, components_settings):
@register("TestPage") @register("TestPage")
class TestPage(Component): class TestPage(Component):
template: types.django_html = """ template: types.django_html = """
@ -898,16 +956,23 @@ class ComponentNestingTests(BaseTestCase):
</body> </body>
</html> </html>
""" """
self.assertHTMLEqual(rendered, expected) assertHTMLEqual(rendered, expected)
# NOTE: Second arg in tuple is expected list content. In isolated mode, loops should NOT leak. # NOTE: Second arg in tuple is expected list content. In isolated mode, loops should NOT leak.
@parametrize_context_behavior( @djc_test(
parametrize=(
["components_settings", "expected"],
[ [
("django", "<li>1</li> <li>2</li>"), [{"context_behavior": "django"}, "<li>1</li> <li>2</li>"],
("isolated", ""), [{"context_behavior": "isolated"}, ""],
] ],
["django", "isolated"],
) )
def test_component_nesting_component_with_slot_default(self, context_behavior_data): )
def test_component_nesting_component_with_slot_default(self, components_settings, expected):
registry.register("dashboard", self.DashboardComponent)
registry.register("calendar", self.CalendarComponent)
template_str: types.django_html = """ template_str: types.django_html = """
{% load component_tags %} {% load component_tags %}
{% component "dashboard" %} {% component "dashboard" %}
@ -927,8 +992,8 @@ class ComponentNestingTests(BaseTestCase):
</main> </main>
</div> </div>
<ol> <ol>
{context_behavior_data} {expected}
</ol> </ol>
</div> </div>
""" """
self.assertHTMLEqual(rendered, expected) assertHTMLEqual(rendered, expected)

View file

@ -1,20 +1,14 @@
from django_components.util.misc import is_str_wrapped_in_quotes from django_components.util.misc import is_str_wrapped_in_quotes
from .django_test_setup import setup_test_config class TestUtils:
from .testutils import BaseTestCase
setup_test_config({"autodiscover": False})
class UtilsTest(BaseTestCase):
def test_is_str_wrapped_in_quotes(self): def test_is_str_wrapped_in_quotes(self):
self.assertEqual(is_str_wrapped_in_quotes("word"), False) assert is_str_wrapped_in_quotes("word") is False
self.assertEqual(is_str_wrapped_in_quotes('word"'), False) assert is_str_wrapped_in_quotes('word"') is False
self.assertEqual(is_str_wrapped_in_quotes('"word'), False) assert is_str_wrapped_in_quotes('"word') is False
self.assertEqual(is_str_wrapped_in_quotes('"word"'), True) assert is_str_wrapped_in_quotes('"word"') is True
self.assertEqual(is_str_wrapped_in_quotes("\"word'"), False) assert is_str_wrapped_in_quotes("\"word'") is False
self.assertEqual(is_str_wrapped_in_quotes('"word" '), False) assert is_str_wrapped_in_quotes('"word" ') is False
self.assertEqual(is_str_wrapped_in_quotes('"'), False) assert is_str_wrapped_in_quotes('"') is False
self.assertEqual(is_str_wrapped_in_quotes(""), False) assert is_str_wrapped_in_quotes("") is False
self.assertEqual(is_str_wrapped_in_quotes('""'), True) assert is_str_wrapped_in_quotes('""') is True
self.assertEqual(is_str_wrapped_in_quotes("\"'"), False) assert is_str_wrapped_in_quotes("\"'") is False

View file

@ -1,84 +1,31 @@
import contextlib from pathlib import Path
import functools from typing import Dict, Optional
import sys from unittest.mock import Mock
from typing import Any, Dict, List, Optional, Tuple, Union
from unittest.mock import Mock, patch
from django.template import Context, Node import django
from django.template.loader import engines from django.conf import settings
from django.template import Context
from django.template.response import TemplateResponse from django.template.response import TemplateResponse
from django.test import SimpleTestCase, override_settings
from django_components.app_settings import ContextBehavior
from django_components.autodiscovery import autodiscover
from django_components.component_registry import registry
from django_components.middleware import ComponentDependencyMiddleware from django_components.middleware import ComponentDependencyMiddleware
# Common use case in our tests is to check that the component works in both
# "django" and "isolated" context behaviors. If you need only that, pass this
# tuple to `djc_test` as the `parametrize` argument.
PARAMETRIZE_CONTEXT_BEHAVIOR = (
["components_settings"],
[
[{"context_behavior": "django"}],
[{"context_behavior": "isolated"}],
],
["django", "isolated"],
)
# Create middleware instance # Create middleware instance
response_stash = None response_stash = None
middleware = ComponentDependencyMiddleware(get_response=lambda _: response_stash) middleware = ComponentDependencyMiddleware(get_response=lambda _: response_stash)
class GenIdPatcher:
def __init__(self):
self._gen_id_count = 10599485
# Mock the `generate` function used inside `gen_id` so it returns deterministic IDs
def start(self):
# Random number so that the generated IDs are "hex-looking", e.g. a1bc3d
self._gen_id_count = 10599485
def mock_gen_id(*args, **kwargs):
self._gen_id_count += 1
return hex(self._gen_id_count)[2:]
self._gen_id_patch = patch("django_components.util.misc.generate", side_effect=mock_gen_id)
self._gen_id_patch.start()
def stop(self):
self._gen_id_patch.stop()
self._gen_id_count = 10599485
class CsrfTokenPatcher:
def __init__(self):
self._csrf_token = "predictabletoken"
def start(self):
self._csrf_token_patch = patch("django.middleware.csrf.get_token", return_value=self._csrf_token)
self._csrf_token_patch.start()
def stop(self):
self._csrf_token_patch.stop()
class BaseTestCase(SimpleTestCase):
def setUp(self):
super().setUp()
self.gen_id_patcher = GenIdPatcher()
self.gen_id_patcher.start()
self.csrf_token_patcher = CsrfTokenPatcher()
self.csrf_token_patcher.start()
def tearDown(self):
self.gen_id_patcher.stop()
self.csrf_token_patcher.stop()
super().tearDown()
registry.clear()
from django_components.cache import component_media_cache, template_cache
# NOTE: There are 1-2 tests which check Templates, so we need to clear the cache
if template_cache:
template_cache.clear()
if component_media_cache:
component_media_cache.clear()
from django_components.component import component_node_subclasses_by_name
component_node_subclasses_by_name.clear()
request = Mock() request = Mock()
mock_template = Mock() mock_template = Mock()
@ -97,154 +44,50 @@ def create_and_process_template_response(template, context=None, use_middleware=
return response.content.decode("utf-8") return response.content.decode("utf-8")
def print_nodes(nodes: List[Node], indent=0) -> None: def setup_test_config(
""" components: Optional[Dict] = None,
Render a Nodelist, inlining child nodes with extra on separate lines and with extra_settings: Optional[Dict] = None,
extra indentation. ):
""" if settings.configured:
for node in nodes: return
child_nodes: List[Node] = []
for attr in node.child_nodelists:
attr_child_nodes = getattr(node, attr, None) or []
if attr_child_nodes:
child_nodes.extend(attr_child_nodes)
repr = str(node) default_settings = {
repr = "\n".join([(" " * 4 * indent) + line for line in repr.split("\n")]) "BASE_DIR": Path(__file__).resolve().parent,
print(repr) "INSTALLED_APPS": ("django_components", "tests.test_app"),
if child_nodes: "TEMPLATES": [
print_nodes(child_nodes, indent=indent + 1) {
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [
"tests/templates/",
"tests/components/", # Required for template relative imports in tests
],
"OPTIONS": {
"builtins": [
"django_components.templatetags.component_tags",
]
},
}
],
"COMPONENTS": {
"template_cache_size": 128,
**(components or {}),
},
"MIDDLEWARE": ["django_components.middleware.ComponentDependencyMiddleware"],
"DATABASES": {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": ":memory:",
}
},
"SECRET_KEY": "secret",
"ROOT_URLCONF": "django_components.urls",
}
settings.configure(
**{
**default_settings,
**(extra_settings or {}),
}
)
# TODO: Make sure that this is done before/after each test automatically? django.setup()
@contextlib.contextmanager
def autodiscover_with_cleanup(*args, **kwargs):
"""
Use this in place of regular `autodiscover` in test files to ensure that
the autoimport does not pollute the global state.
"""
imported_modules = autodiscover(*args, **kwargs)
try:
yield imported_modules
finally:
# Teardown - delete autoimported modules, so the module is executed also the
# next time one of the tests calls `autodiscover`.
for mod in imported_modules:
del sys.modules[mod]
ContextBehStr = Union[ContextBehavior, str]
ContextBehParam = Union[ContextBehStr, Tuple[ContextBehStr, Any]]
def parametrize_context_behavior(cases: List[ContextBehParam], settings: Optional[Dict] = None):
"""
Use this decorator to run a test function with django_component's
context_behavior settings set to given values.
You can set only a single mode:
```py
@parametrize_context_behavior(["isolated"])
def test_bla_bla(self):
# do something with app_settings.CONTEXT_BEHAVIOR set
# to "isolated"
...
```
Or you can set a test to run in both modes:
```py
@parametrize_context_behavior(["django", "isolated"])
def test_bla_bla(self):
# Runs this test function twice. Once with
# app_settings.CONTEXT_BEHAVIOR set to "django",
# the other time set to "isolated"
...
```
If you need to pass parametrized data to the tests,
pass a tuple of (mode, data) instead of plain string.
To access the data as a fixture, add `context_behavior_data`
as a function argument:
```py
@parametrize_context_behavior([
("django", "result for django"),
("isolated", "result for isolated"),
])
def test_bla_bla(self, context_behavior_data):
# Runs this test function twice. Once with
# app_settings.CONTEXT_BEHAVIOR set to "django",
# the other time set to "isolated".
#
# `context_behavior_data` will first have a value
# of "result for django", then of "result for isolated"
print(context_behavior_data)
...
```
NOTE: Use only on functions and methods. This decorator was NOT tested on classes
"""
def decorator(test_func):
# NOTE: Ideally this decorator would parametrize the test function
# with `pytest.mark.parametrize`, so all test cases would be treated as separate
# tests and thus isolated. But I wasn't able to get it to work. Hence,
# as a workaround, we run multiple test cases within the same test run.
# Because of this, we need to clear the loader cache, and, on error, we need to
# propagate the info on which test case failed.
@functools.wraps(test_func)
def wrapper(self: BaseTestCase, *args, **kwargs):
for case in cases:
# Clear loader cache, see https://stackoverflow.com/a/77531127/9788634
for engine in engines.all():
engine.engine.template_loaders[0].reset()
# Reset gen_id
self.gen_id_patcher.stop()
self.gen_id_patcher.start()
# Reset template cache
from django_components.cache import component_media_cache, template_cache
if template_cache: # May be None if the cache was not initialized
template_cache.clear()
if component_media_cache:
component_media_cache.clear()
from django_components.component import component_node_subclasses_by_name
component_node_subclasses_by_name.clear()
case_has_data = not isinstance(case, str)
if isinstance(case, str):
context_beh, fixture = case, None
else:
context_beh, fixture = case
# Set `COMPONENTS={"context_behavior": context_beh}`, but do so carefully,
# so we override only that single setting, and so that we operate on copies
# to avoid spilling settings across the test cases
merged_settings = {} if not settings else settings.copy()
if "COMPONENTS" in merged_settings:
merged_settings["COMPONENTS"] = merged_settings["COMPONENTS"].copy()
else:
merged_settings["COMPONENTS"] = {}
merged_settings["COMPONENTS"]["context_behavior"] = context_beh
with override_settings(**merged_settings):
# Call the test function with the fixture as an argument
try:
if case_has_data:
test_func(self, *args, context_behavior_data=fixture, **kwargs)
else:
test_func(self, *args, **kwargs)
except Exception as err:
# Give a hint on which iteration the test failed
raise RuntimeError(
f"An error occured in test function '{test_func.__name__}' with"
f" context_behavior='{context_beh}'. See the original error above."
) from err
return wrapper
return decorator

View file

@ -34,7 +34,9 @@ deps =
djc-core-html-parser djc-core-html-parser
pytest pytest
pytest-xdist pytest-xdist
syrupy # snapshot testing pytest-django
pytest-asyncio
syrupy # pytest snapshot testing
# NOTE: Keep playwright is sync with the version in requirements-ci.txt # NOTE: Keep playwright is sync with the version in requirements-ci.txt
# Othrwise we get error: # Othrwise we get error:
# playwright._impl._errors.Error: BrowserType.launch: Executable doesn't exist at /home/runner/.cache/ms-playwright/chromium-1140/chrome-linux/chrome # playwright._impl._errors.Error: BrowserType.launch: Executable doesn't exist at /home/runner/.cache/ms-playwright/chromium-1140/chrome-linux/chrome
@ -56,6 +58,8 @@ commands = isort --check-only --diff src/django_components
[testenv:coverage] [testenv:coverage]
deps = deps =
pytest-cov pytest-cov
pytest-django
pytest-asyncio
syrupy # snapshot testing syrupy # snapshot testing
# NOTE: Keep playwright in sync with the version in requirements-ci.txt # NOTE: Keep playwright in sync with the version in requirements-ci.txt
playwright==1.48.0 playwright==1.48.0