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
## 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
#### Feat

View file

@ -8,4 +8,5 @@ nav:
- Typing and validation: typing_and_validation.md
- Custom template tags: template_tags.md
- Tag formatters: tag_formatter.md
- Testing: testing.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 vars: template_vars.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")
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():
"""
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_templatevars()
gen_reference_signals()
gen_reference_testing_api()
# 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 = [
"tests"
]
asyncio_mode = "auto"
[tool.hatch.env]
requires = [

View file

@ -4,4 +4,6 @@ playwright
requests
types-requests
whitenoise
asv
asv
pytest-asyncio
pytest-django

View file

@ -4,17 +4,23 @@
#
# 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
certifi==2024.8.30
certifi==2025.1.31
# via requests
chardet==5.2.0
# via tox
charset-normalizer==3.3.2
charset-normalizer==3.4.1
# via requests
colorama==0.4.6
# via tox
distlib==0.3.8
distlib==0.3.9
# via virtualenv
filelock==3.16.1
# via
@ -24,9 +30,17 @@ greenlet==3.1.1
# via playwright
idna==3.10
# 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
# via
# build
# pyproject-api
# pytest
# tox
platformdirs==4.3.6
# via
@ -35,18 +49,36 @@ platformdirs==4.3.6
playwright==1.48.0
# via -r requirements-ci.in
pluggy==1.5.0
# via tox
# via
# pytest
# tox
pyee==12.0.0
# via playwright
pympler==1.1
# via asv
pyproject-api==1.8.0
# 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
# via -r requirements-ci.in
tabulate==0.9.0
# via asv
tox==4.24.1
# via
# -r requirements-ci.in
# tox-gh-actions
tox-gh-actions==3.2.0
tox-gh-actions==3.3.0
# via -r requirements-ci.in
types-requests==2.32.0.20241016
# via -r requirements-ci.in
@ -56,7 +88,11 @@ urllib3==2.2.3
# via
# requests
# types-requests
virtualenv==20.29.1
# via tox
virtualenv==20.29.2
# via
# asv
# tox
whitenoise==6.7.0
# via -r requirements-ci.in
zipp==3.20.2
# via importlib-metadata

View file

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

View file

@ -6,11 +6,17 @@
#
asgiref==3.8.1
# via django
black==24.10.0
asv==0.6.4
# 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
certifi==2024.8.30
certifi==2025.1.31
# via requests
cfgv==3.4.0
# via pre-commit
@ -40,14 +46,18 @@ flake8-pyproject==1.2.3
# via -r requirements-dev.in
greenlet==3.1.1
# via playwright
identify==2.6.7
identify==2.6.8
# via pre-commit
idna==3.10
# via requests
importlib-metadata==8.5.0
# via asv-runner
iniconfig==2.0.0
# via pytest
isort==6.0.0
isort==6.0.1
# via -r requirements-dev.in
json5==0.10.0
# via asv
mccabe==0.7.0
# via flake8
mypy==1.15.0
@ -61,6 +71,7 @@ nodeenv==1.9.1
packaging==24.2
# via
# black
# build
# pyproject-api
# pytest
# tox
@ -71,7 +82,7 @@ platformdirs==4.3.6
# black
# tox
# virtualenv
playwright==1.49.0
playwright==1.48.0
# via -r requirements-dev.in
pluggy==1.5.0
# via
@ -86,19 +97,39 @@ pyee==12.0.0
pyflakes==3.2.0
# via flake8
pygments==2.19.1
# via pygments-djc
# via
# -r requirements-dev.in
# pygments-djc
pygments-djc==1.0.1
# via -r requirements-dev.in
pympler==1.1
# via asv
pyproject-api==1.8.0
# via tox
pyproject-hooks==1.2.0
# via build
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
pyyaml==6.0.2
# via pre-commit
# via
# asv
# pre-commit
requests==2.32.3
# via -r requirements-dev.in
sqlparse==0.5.3
# via django
syrupy==4.8.2
# via -r requirements-dev.in
tabulate==0.9.0
# via asv
tox==4.24.1
# via -r requirements-dev.in
types-requests==2.32.0.20241016
@ -111,9 +142,12 @@ urllib3==2.2.3
# via
# requests
# types-requests
virtualenv==20.28.0
virtualenv==20.29.2
# via
# asv
# pre-commit
# tox
whitenoise==6.8.2
whitenoise==6.7.0
# 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 typing import (
TYPE_CHECKING,
Any,
Callable,
Dict,
Generic,
List,
Literal,
@ -671,94 +673,140 @@ defaults = ComponentsSettings(
# 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:
@property
def _settings(self) -> ComponentsSettings:
def __init__(self, settings: Optional[Dict[str, Any]] = None):
self._settings = ComponentsSettings(**settings) if settings else defaults
def _load_settings(self) -> None:
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
def AUTODISCOVER(self) -> bool:
return default(self._settings.autodiscover, cast(bool, defaults.autodiscover))
# Merge we defaults and otherwise initialize if necessary
@property
def CACHE(self) -> Optional[str]:
return default(self._settings.cache, defaults.cache)
# For DIRS setting, we use a getter for the default value, because the default value
# uses Django settings, which may not yet be initialized at the time these settings are generated.
dirs_default_fn = cast(Dynamic[Sequence[Union[str, Tuple[str, str]]]], defaults.dirs)
dirs_default = dirs_default_fn.getter()
@property
def DIRS(self) -> Sequence[Union[str, PathLike, Tuple[str, str], Tuple[str, PathLike]]]:
# For DIRS we use a getter, because default values uses Django settings,
# which may not yet be initialized at the time these settings are generated.
default_fn = cast(Dynamic[Sequence[Union[str, Tuple[str, str]]]], defaults.dirs)
default_dirs = default_fn.getter()
return default(self._settings.dirs, default_dirs)
self._settings = ComponentsSettings(
autodiscover=default(components_settings.autodiscover, defaults.autodiscover),
cache=default(components_settings.cache, defaults.cache),
dirs=default(components_settings.dirs, dirs_default),
app_dirs=default(components_settings.app_dirs, defaults.app_dirs),
debug_highlight_components=default(
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 APP_DIRS(self) -> Sequence[str]:
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
def _prepare_reload_on_file_change(self, new_settings: ComponentsSettings) -> bool:
val = new_settings.reload_on_file_change
# TODO_REMOVE_IN_V1
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))
@property
def TEMPLATE_CACHE_SIZE(self) -> int:
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
def _prepare_static_files_forbidden(self, new_settings: ComponentsSettings) -> List[Union[str, re.Pattern]]:
val = new_settings.static_files_forbidden
# TODO_REMOVE_IN_V1
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 CONTEXT_BEHAVIOR(self) -> ContextBehavior:
raw_value = cast(str, default(self._settings.context_behavior, defaults.context_behavior))
return self._validate_context_behavior(raw_value)
def _validate_context_behavior(self, raw_value: Union[ContextBehavior, str]) -> ContextBehavior:
def _prepare_context_behavior(self, new_settings: ComponentsSettings) -> Literal["django", "isolated"]:
raw_value = cast(
Literal["django", "isolated"],
default(new_settings.context_behavior, defaults.context_behavior),
)
try:
return ContextBehavior(raw_value)
ContextBehavior(raw_value)
except ValueError:
valid_values = [behavior.value for behavior in ContextBehavior]
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
def TAG_FORMATTER(self) -> Union["TagFormatterABC", str]:
tag_formatter = default(self._settings.tag_formatter, cast(str, defaults.tag_formatter))
return cast(Union["TagFormatterABC", str], tag_formatter)
return self._settings.tag_formatter # type: ignore[return-value]
app_settings = InternalSettings()

View file

@ -19,6 +19,8 @@ class ComponentsConfig(AppConfig):
from django_components.components.dynamic import DynamicComponent
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.
# To make django-components work with django-debug-toolbar-template-profiler
# 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.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(
@ -94,4 +100,10 @@ def _import_modules(
logger.debug(f'Importing module "{module_name}"')
importlib.import_module(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

View file

@ -1,3 +1,4 @@
import sys
import types
from collections import deque
from contextlib import contextmanager
@ -22,6 +23,7 @@ from typing import (
Union,
cast,
)
from weakref import ReferenceType
from django.core.exceptions import ImproperlyConfigured
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.template_tag import TagAttr
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
# isort: off
@ -99,6 +102,17 @@ JsDataType = TypeVar("JsDataType", 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)
class RenderInput(Generic[ArgsType, KwargsType, SlotsType]):
context: Context
@ -613,6 +627,8 @@ class Component(
cls._class_hash = hash_comp_cls(cls)
comp_hash_mapping[cls._class_hash] = cls
ALL_COMPONENTS.append(cached_ref(cls)) # type: ignore[arg-type]
@contextmanager
def _with_metadata(self, item: MetadataItem) -> Generator[None, None, None]:
self._metadata_stack.append(item)

View file

@ -1,6 +1,7 @@
import os
import sys
from collections import deque
from copy import copy
from dataclasses import dataclass
from pathlib import Path
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" 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.

View file

@ -1,4 +1,6 @@
import sys
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.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.library import is_tag_protected, mark_protected_tags, register_tag
from django_components.tag_formatter import TagFormatterABC, get_tag_formatter
from django_components.util.weakref import cached_ref
if TYPE_CHECKING:
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):
"""
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
# dynamically resolve component name to component class, they would be able
# to search across all registries.
all_registries: List["ComponentRegistry"] = []
ALL_REGISTRIES: AllRegistries = []
class ComponentRegistry:
@ -223,10 +233,19 @@ class ComponentRegistry:
self._registry: Dict[str, ComponentRegistryEntry] = {} # component name -> component_entry mapping
self._tags: Dict[str, Set[str]] = {} # tag -> list[component names]
self._library = library
self._settings_input = settings
self._settings: Optional[Callable[[], InternalRegistrySettings]] = None
self._settings = settings
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
def library(self) -> Library:
@ -254,40 +273,24 @@ class ComponentRegistry:
"""
[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
# 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:
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:
context_behavior = settings_input.context_behavior or settings_input.CONTEXT_BEHAVIOR
tag_formatter = settings_input.tag_formatter or settings_input.TAG_FORMATTER
else:
context_behavior = None
tag_formatter = None
if settings_input:
context_behavior = settings_input.context_behavior or settings_input.CONTEXT_BEHAVIOR
tag_formatter = settings_input.tag_formatter or settings_input.TAG_FORMATTER
else:
context_behavior = None
tag_formatter = None
return InternalRegistrySettings(
context_behavior=context_behavior or app_settings.CONTEXT_BEHAVIOR.value,
tag_formatter=tag_formatter or app_settings.TAG_FORMATTER,
)
self._settings = get_settings
settings = self._settings()
return settings
return InternalRegistrySettings(
context_behavior=context_behavior or app_settings.CONTEXT_BEHAVIOR.value,
tag_formatter=tag_formatter or app_settings.TAG_FORMATTER,
)
def register(self, name: str, component: Type["Component"]) -> None:
"""
@ -330,6 +333,9 @@ class ComponentRegistry:
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:
"""
Unregister the [`Component`](../api#django_components.Component) class
@ -360,22 +366,27 @@ class ComponentRegistry:
entry = self._registry[name]
tag = entry.tag
# Unregister the tag from library if this was the last component using this tag
# Unlink component from tag
self._tags[tag].remove(name)
# Unregister the tag from library.
# 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)
# Cleanup
is_tag_empty = not len(self._tags[tag])
if is_tag_empty:
del self._tags[tag]
# Cleanup
is_tag_empty = not len(self._tags[tag])
if is_tag_empty:
self._tags.pop(tag, None)
else:
is_tag_empty = True
# Only unregister a tag if it's NOT protected
is_protected = is_tag_protected(self.library, tag)
if not is_protected:
# Unregister the tag from library if this was the last component using this tag
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]
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_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):
@ -170,7 +170,11 @@ class DynamicComponent(Component):
component_cls = registry.get(comp_name_or_class)
else:
# 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:
component_cls = reg.get(comp_name_or_class)
break

View file

@ -106,9 +106,16 @@ class StringifiedNode(Node):
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.
# 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

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

View file

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

View file

@ -6431,17 +6431,10 @@ def project_output_form(context: Context, data: ProjectOutputFormData):
# The code above is used also used when benchmarking.
# 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):
id_patcher = GenIdPatcher()
id_patcher.start()
csrf_token_patcher = CsrfTokenPatcher()
csrf_token_patcher.start()
data = gen_render_data()
rendered = render(data)
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 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):
id_patcher = GenIdPatcher()
id_patcher.start()
csrf_token_patcher = CsrfTokenPatcher()
csrf_token_patcher.start()
data = gen_render_data()
rendered = render(data)
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 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):
id_patcher = GenIdPatcher()
id_patcher.start()
csrf_token_patcher = CsrfTokenPatcher()
csrf_token_patcher.start()
registry.register("Button", Button)
registry.register("Menu", Menu)
registry.register("MenuList", MenuList)
@ -6055,6 +6051,3 @@ def test_render(snapshot):
data = gen_render_data()
rendered = render(data)
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 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):
id_patcher = GenIdPatcher()
id_patcher.start()
csrf_token_patcher = CsrfTokenPatcher()
csrf_token_patcher.start()
data = gen_render_data()
rendered = render(data)
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_components.util.cache import LRUCache
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})
class CacheTests(TestCase):
@djc_test
class TestCache:
def test_cache(self):
cache = LRUCache[int](maxsize=3)
@ -17,58 +18,59 @@ class CacheTests(TestCase):
cache.set("b", 2)
cache.set("c", 3)
self.assertEqual(cache.get("a"), 1)
self.assertEqual(cache.get("b"), 2)
self.assertEqual(cache.get("c"), 3)
assert cache.get("a") == 1
assert cache.get("b") == 2
assert cache.get("c") == 3
cache.set("d", 4)
self.assertEqual(cache.get("a"), None)
self.assertEqual(cache.get("b"), 2)
self.assertEqual(cache.get("c"), 3)
self.assertEqual(cache.get("d"), 4)
assert cache.get("a") is None
assert cache.get("b") == 2
assert cache.get("c") == 3
assert cache.get("d") == 4
cache.set("e", 5)
cache.set("f", 6)
self.assertEqual(cache.get("b"), None)
self.assertEqual(cache.get("c"), None)
self.assertEqual(cache.get("d"), 4)
self.assertEqual(cache.get("e"), 5)
self.assertEqual(cache.get("f"), 6)
assert cache.get("b") is None
assert cache.get("c") is None
assert cache.get("d") == 4
assert cache.get("e") == 5
assert cache.get("f") == 6
cache.clear()
self.assertEqual(cache.get("d"), None)
self.assertEqual(cache.get("e"), None)
self.assertEqual(cache.get("f"), None)
assert cache.get("d") is None
assert cache.get("e") is None
assert cache.get("f") is None
def test_cache_maxsize_zero(self):
cache = LRUCache[int](maxsize=0)
cache.set("a", 1)
self.assertEqual(cache.get("a"), None)
assert cache.get("a") is None
cache.set("b", 2)
cache.set("c", 3)
self.assertEqual(cache.get("b"), None)
self.assertEqual(cache.get("c"), None)
assert cache.get("b") is None
assert cache.get("c") is None
# Same with negative numbers
cache = LRUCache[int](maxsize=-1)
cache.set("a", 1)
self.assertEqual(cache.get("a"), None)
assert cache.get("a") is None
cache.set("b", 2)
cache.set("c", 3)
self.assertEqual(cache.get("b"), None)
self.assertEqual(cache.get("c"), None)
assert cache.get("b") is None
assert cache.get("c") is None
class ComponentMediaCacheTests(TestCase):
def setUp(self):
# Create a custom locmem cache for testing
self.test_cache = LocMemCache(
@djc_test
class TestComponentMediaCache:
@djc_test(components_settings={"cache": "test-cache"})
def test_component_media_caching(self):
test_cache = LocMemCache(
"test-cache",
{
"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")
class TestSimpleComponent(Component):
template = """
@ -123,28 +123,22 @@ class ComponentMediaCacheTests(TestCase):
# Register our test cache
from django.core.cache import caches
caches["test-cache"] = self.test_cache
caches["test-cache"] = test_cache
# Render the components to trigger caching
TestMediaAndVarsComponent.render()
# Check that JS/CSS is cached for components that have them
self.assertTrue(self.test_cache.has_key(f"__components:{TestMediaAndVarsComponent._class_hash}:js"))
self.assertTrue(self.test_cache.has_key(f"__components:{TestMediaAndVarsComponent._class_hash}:css"))
self.assertTrue(self.test_cache.has_key(f"__components:{TestMediaNoVarsComponent._class_hash}:js"))
self.assertTrue(self.test_cache.has_key(f"__components:{TestMediaNoVarsComponent._class_hash}:css"))
self.assertFalse(self.test_cache.has_key(f"__components:{TestSimpleComponent._class_hash}:js"))
self.assertFalse(self.test_cache.has_key(f"__components:{TestSimpleComponent._class_hash}:css"))
assert test_cache.has_key(f"__components:{TestMediaAndVarsComponent._class_hash}:js")
assert test_cache.has_key(f"__components:{TestMediaAndVarsComponent._class_hash}:css")
assert test_cache.has_key(f"__components:{TestMediaNoVarsComponent._class_hash}:js")
assert test_cache.has_key(f"__components:{TestMediaNoVarsComponent._class_hash}:css")
assert not test_cache.has_key(f"__components:{TestSimpleComponent._class_hash}:js")
assert not test_cache.has_key(f"__components:{TestSimpleComponent._class_hash}:css")
# Check that we cache `Component.js` / `Component.css`
self.assertEqual(
self.test_cache.get(f"__components:{TestMediaNoVarsComponent._class_hash}:js").strip(),
"console.log('Hello from JS');",
)
self.assertEqual(
self.test_cache.get(f"__components:{TestMediaNoVarsComponent._class_hash}:css").strip(),
".novars-component { color: blue; }",
)
assert test_cache.get(f"__components:{TestMediaNoVarsComponent._class_hash}:js").strip() == "console.log('Hello from JS');" # noqa: E501
assert test_cache.get(f"__components:{TestMediaNoVarsComponent._class_hash}:css").strip() == ".novars-component { color: blue; }" # noqa: E501
# Check that we cache JS / CSS scripts generated from `get_js_data` / `get_css_data`
# NOTE: The hashes is generated from the data.
@ -152,11 +146,5 @@ class ComponentMediaCacheTests(TestCase):
css_vars_hash = "d039a3"
# TODO - Update once JS and CSS vars are enabled
self.assertEqual(
self.test_cache.get(f"__components:{TestMediaAndVarsComponent._class_hash}:js:{js_vars_hash}").strip(),
"",
)
self.assertEqual(
self.test_cache.get(f"__components:{TestMediaAndVarsComponent._class_hash}:css:{css_vars_hash}").strip(),
"",
)
assert 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

View file

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

View file

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

File diff suppressed because it is too large Load diff

View file

@ -1,12 +1,15 @@
import re
from typing import Dict, Optional
import pytest
from django.http import HttpRequest
from django.template import Context, RequestContext, Template
from pytest_django.asserts import assertHTMLEqual, assertInHTML
from django_components import Component, register, registry, types
from .django_test_setup import setup_test_config
from .testutils import BaseTestCase, parametrize_context_behavior
from django_components.testing import djc_test
from .testutils import PARAMETRIZE_CONTEXT_BEHAVIOR, setup_test_config
setup_test_config({"autodiscover": False})
@ -66,7 +69,8 @@ class IncrementerComponent(Component):
#########################
class ContextTests(BaseTestCase):
@djc_test
class TestContext:
class ParentComponent(Component):
template: types.django_html = """
{% load component_tags %}
@ -87,15 +91,13 @@ class ContextTests(BaseTestCase):
def get_context_data(self):
return {"shadowing_variable": "NOT SHADOWED"}
def setUp(self):
super().setUp()
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
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="parent_component", component=self.ParentComponent)
@parametrize_context_behavior(["django", "isolated"])
def test_nested_component_context_shadows_parent_with_unfilled_slots_and_component_tag(
self,
):
template_str: types.django_html = """
{% load component_tags %}
{% component 'parent_component' %}{% endcomponent %}
@ -103,14 +105,17 @@ class ContextTests(BaseTestCase):
template = Template(template_str)
rendered = template.render(Context())
self.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)
self.assertNotIn("Shadowing variable = NOT SHADOWED", rendered)
assertInHTML("<h1 data-djc-id-a1bc43>Shadowing variable = override</h1>", rendered)
assertInHTML("<h1 data-djc-id-a1bc44>Shadowing variable = slot_default_override</h1>", 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(
self,
self, components_settings,
):
registry.register(name="variable_display", component=VariableDisplay)
registry.register(name="parent_component", component=self.ParentComponent)
template_str: types.django_html = """
{% load component_tags %}
{% component name='parent_component' %}{% endcomponent %}
@ -118,14 +123,17 @@ class ContextTests(BaseTestCase):
template = Template(template_str)
rendered = template.render(Context())
self.assertInHTML("<h1 data-djc-id-a1bc43>Uniquely named variable = unique_val</h1>", rendered)
self.assertInHTML(
assertInHTML("<h1 data-djc-id-a1bc43>Uniquely named variable = unique_val</h1>", rendered)
assertInHTML(
"<h1 data-djc-id-a1bc44>Uniquely named variable = slot_default_unique</h1>",
rendered,
)
@parametrize_context_behavior(["django", "isolated"])
def test_nested_component_context_shadows_parent_with_filled_slots(self):
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
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 = """
{% load component_tags %}
{% component 'parent_component' %}
@ -138,12 +146,15 @@ class ContextTests(BaseTestCase):
template = Template(template_str)
rendered = template.render(Context())
self.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)
self.assertNotIn("Shadowing variable = NOT SHADOWED", rendered)
assertInHTML("<h1 data-djc-id-a1bc45>Shadowing variable = override</h1>", rendered)
assertInHTML("<h1 data-djc-id-a1bc46>Shadowing variable = shadow_from_slot</h1>", 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 = """
{% load component_tags %}
{% component 'parent_component' %}
@ -156,13 +167,16 @@ class ContextTests(BaseTestCase):
template = Template(template_str)
rendered = template.render(Context())
self.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-a1bc45>Uniquely named variable = unique_val</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(
self,
self, components_settings,
):
registry.register(name="variable_display", component=VariableDisplay)
registry.register(name="parent_component", component=self.ParentComponent)
template_str: types.django_html = """
{% load component_tags %}
{% component name='parent_component' %}{% endcomponent %}
@ -170,14 +184,17 @@ class ContextTests(BaseTestCase):
template = Template(template_str)
rendered = template.render(Context({"shadowing_variable": "NOT SHADOWED"}))
self.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)
self.assertNotIn("Shadowing variable = NOT SHADOWED", rendered)
assertInHTML("<h1 data-djc-id-a1bc43>Shadowing variable = override</h1>", rendered)
assertInHTML("<h1 data-djc-id-a1bc44>Shadowing variable = slot_default_override</h1>", 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(
self,
self, components_settings,
):
registry.register(name="variable_display", component=VariableDisplay)
registry.register(name="parent_component", component=self.ParentComponent)
template_str: types.django_html = """
{% load component_tags %}
{% component 'parent_component' %}
@ -190,12 +207,13 @@ class ContextTests(BaseTestCase):
template = Template(template_str)
rendered = template.render(Context({"shadowing_variable": "NOT SHADOWED"}))
self.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)
self.assertNotIn("Shadowing variable = NOT SHADOWED", rendered)
assertInHTML("<h1 data-djc-id-a1bc45>Shadowing variable = override</h1>", rendered)
assertInHTML("<h1 data-djc-id-a1bc46>Shadowing variable = shadow_from_slot</h1>", rendered)
assert "Shadowing variable = NOT SHADOWED" not in rendered
class ParentArgsTests(BaseTestCase):
@djc_test
class TestParentArgs:
class ParentComponentWithArgs(Component):
template: types.django_html = """
{% load component_tags %}
@ -216,14 +234,12 @@ class ParentArgsTests(BaseTestCase):
def get_context_data(self, parent_value):
return {"inner_parent_value": parent_value}
def setUp(self):
super().setUp()
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_parent_args_can_be_drawn_from_context(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)
@parametrize_context_behavior(["django", "isolated"])
def test_parent_args_can_be_drawn_from_context(self):
template_str: types.django_html = """
{% load component_tags %}
{% component 'parent_with_args' parent_value=parent_value %}
@ -232,7 +248,7 @@ class ParentArgsTests(BaseTestCase):
template = Template(template_str)
rendered = template.render(Context({"parent_value": "passed_in"}))
self.assertHTMLEqual(
assertHTMLEqual(
rendered,
"""
<div data-djc-id-a1bc3f>
@ -248,8 +264,12 @@ class ParentArgsTests(BaseTestCase):
""",
)
@parametrize_context_behavior(["django", "isolated"])
def test_parent_args_available_outside_slots(self):
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
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 = """
{% load component_tags %}
{% component 'parent_with_args' parent_value='passed_in' %}{%endcomponent %}
@ -257,19 +277,24 @@ class ParentArgsTests(BaseTestCase):
template = Template(template_str)
rendered = template.render(Context())
self.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)
self.assertNotIn("Shadowing variable = NOT SHADOWED", rendered)
assertInHTML("<h1 data-djc-id-a1bc43>Shadowing variable = passed_in</h1>", rendered)
assertInHTML("<h1 data-djc-id-a1bc44>Uniquely named variable = passed_in</h1>", rendered)
assert "Shadowing variable = NOT SHADOWED" not in rendered
# NOTE: Second arg in tuple are expected values passed through components.
@parametrize_context_behavior(
[
("django", ("passed_in", "passed_in")),
("isolated", ("passed_in", "")),
]
@djc_test(
parametrize=(
["components_settings", "first_val", "second_val"],
[
[{"context_behavior": "django"}, "passed_in", "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 = """
{% load component_tags %}
@ -283,7 +308,7 @@ class ParentArgsTests(BaseTestCase):
template = Template(template_str)
rendered = template.render(Context())
self.assertHTMLEqual(
assertHTMLEqual(
rendered,
f"""
<div data-djc-id-a1bc41>
@ -299,26 +324,25 @@ class ParentArgsTests(BaseTestCase):
)
class ContextCalledOnceTests(BaseTestCase):
def setUp(self):
super().setUp()
@djc_test
class TestContextCalledOnce:
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_one_context_call_with_simple_component(self, components_settings):
registry.register(name="incrementer", component=IncrementerComponent)
@parametrize_context_behavior(["django", "isolated"])
def test_one_context_call_with_simple_component(self):
template_str: types.django_html = """
{% load component_tags %}
{% component name='incrementer' %}{% endcomponent %}
"""
template = Template(template_str)
rendered = template.render(Context()).strip().replace("\n", "")
self.assertHTMLEqual(
assertHTMLEqual(
rendered,
'<p class="incrementer" data-djc-id-a1bc3f>value=1;calls=1</p>',
)
@parametrize_context_behavior(["django", "isolated"])
def test_one_context_call_with_simple_component_and_arg(self):
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_one_context_call_with_simple_component_and_arg(self, components_settings):
registry.register(name="incrementer", component=IncrementerComponent)
template_str: types.django_html = """
{% load component_tags %}
{% component name='incrementer' value='2' %}{% endcomponent %}
@ -326,15 +350,16 @@ class ContextCalledOnceTests(BaseTestCase):
template = Template(template_str)
rendered = template.render(Context()).strip()
self.assertHTMLEqual(
assertHTMLEqual(
rendered,
"""
<p class="incrementer" data-djc-id-a1bc3f>value=3;calls=1</p>
""",
)
@parametrize_context_behavior(["django", "isolated"])
def test_one_context_call_with_component(self):
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_one_context_call_with_component(self, components_settings):
registry.register(name="incrementer", component=IncrementerComponent)
template_str: types.django_html = """
{% load component_tags %}
{% component 'incrementer' %}{% endcomponent %}
@ -342,10 +367,11 @@ class ContextCalledOnceTests(BaseTestCase):
template = Template(template_str)
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"])
def test_one_context_call_with_component_and_arg(self):
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_one_context_call_with_component_and_arg(self, components_settings):
registry.register(name="incrementer", component=IncrementerComponent)
template_str: types.django_html = """
{% load component_tags %}
{% component 'incrementer' value='3' %}{% endcomponent %}
@ -353,10 +379,11 @@ class ContextCalledOnceTests(BaseTestCase):
template = Template(template_str)
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"])
def test_one_context_call_with_slot(self):
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_one_context_call_with_slot(self, components_settings):
registry.register(name="incrementer", component=IncrementerComponent)
template_str: types.django_html = """
{% load component_tags %}
{% component 'incrementer' %}
@ -368,7 +395,7 @@ class ContextCalledOnceTests(BaseTestCase):
template = Template(template_str)
rendered = template.render(Context()).strip()
self.assertHTMLEqual(
assertHTMLEqual(
rendered,
"""
<p class="incrementer" data-djc-id-a1bc40>value=1;calls=1</p>
@ -377,8 +404,9 @@ class ContextCalledOnceTests(BaseTestCase):
rendered,
)
@parametrize_context_behavior(["django", "isolated"])
def test_one_context_call_with_slot_and_arg(self):
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_one_context_call_with_slot_and_arg(self, components_settings):
registry.register(name="incrementer", component=IncrementerComponent)
template_str: types.django_html = """
{% load component_tags %}
{% component 'incrementer' value='3' %}
@ -390,7 +418,7 @@ class ContextCalledOnceTests(BaseTestCase):
template = Template(template_str)
rendered = template.render(Context()).strip()
self.assertHTMLEqual(
assertHTMLEqual(
rendered,
"""
<p class="incrementer" data-djc-id-a1bc40>value=4;calls=1</p>
@ -400,92 +428,92 @@ class ContextCalledOnceTests(BaseTestCase):
)
class ComponentsCanAccessOuterContext(BaseTestCase):
def setUp(self):
super().setUp()
registry.register(name="simple_component", component=SimpleComponent)
# NOTE: Second arg in tuple is expected value.
@parametrize_context_behavior(
[
("django", "outer_value"),
("isolated", ""),
]
@djc_test
class TestComponentsCanAccessOuterContext:
@djc_test(
parametrize=(
["components_settings", "expected_value"],
[
[{"context_behavior": "django"}, "outer_value"],
[{"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 = """
{% load component_tags %}
{% component 'simple_component' %}{% endcomponent %}
"""
template = Template(template_str)
rendered = template.render(Context({"variable": "outer_value"}))
self.assertHTMLEqual(
assertHTMLEqual(
rendered,
f"""
Variable: <strong data-djc-id-a1bc3f> {context_behavior_data} </strong>
Variable: <strong data-djc-id-a1bc3f> {expected_value} </strong>
""",
)
class IsolatedContextTests(BaseTestCase):
def setUp(self):
super().setUp()
@djc_test
class TestIsolatedContext:
@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)
@parametrize_context_behavior(["django", "isolated"])
def test_simple_component_can_pass_outer_context_in_args(self):
template_str: types.django_html = """
{% load component_tags %}
{% component 'simple_component' variable only %}{% endcomponent %}
"""
template = Template(template_str)
rendered = template.render(Context({"variable": "outer_value"})).strip()
self.assertIn("outer_value", rendered)
assert "outer_value" in rendered
@parametrize_context_behavior(["django", "isolated"])
def test_simple_component_cannot_use_outer_context(self):
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_simple_component_cannot_use_outer_context(self, components_settings):
registry.register(name="simple_component", component=SimpleComponent)
template_str: types.django_html = """
{% load component_tags %}
{% component 'simple_component' only %}{% endcomponent %}
"""
template = Template(template_str)
rendered = template.render(Context({"variable": "outer_value"})).strip()
self.assertNotIn("outer_value", rendered)
assert "outer_value" not in rendered
class IsolatedContextSettingTests(BaseTestCase):
def setUp(self):
super().setUp()
registry.register(name="simple_component", component=SimpleComponent)
@parametrize_context_behavior(["isolated"])
@djc_test
class TestIsolatedContextSetting:
@djc_test(components_settings={"context_behavior": "isolated"})
def test_component_tag_includes_variable_with_isolated_context_from_settings(
self,
):
registry.register(name="simple_component", component=SimpleComponent)
template_str: types.django_html = """
{% load component_tags %}
{% component 'simple_component' variable %}{% endcomponent %}
"""
template = Template(template_str)
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(
self,
):
registry.register(name="simple_component", component=SimpleComponent)
template_str: types.django_html = """
{% load component_tags %}
{% component 'simple_component' %}{% endcomponent %}
"""
template = Template(template_str)
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(
self,
):
registry.register(name="simple_component", component=SimpleComponent)
template_str: types.django_html = """
{% load component_tags %}
{% component 'simple_component' variable %}
@ -493,12 +521,13 @@ class IsolatedContextSettingTests(BaseTestCase):
"""
template = Template(template_str)
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(
self,
):
registry.register(name="simple_component", component=SimpleComponent)
template_str: types.django_html = """
{% load component_tags %}
{% component 'simple_component' %}
@ -506,12 +535,13 @@ class IsolatedContextSettingTests(BaseTestCase):
"""
template = Template(template_str)
rendered = template.render(Context({"variable": "outer_value"}))
self.assertNotIn("outer_value", rendered)
assert "outer_value" not in rendered
class ContextProcessorsTests(BaseTestCase):
@parametrize_context_behavior(["django", "isolated"])
def test_request_context_in_template(self):
@djc_test
class TestContextProcessors:
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_request_context_in_template(self, components_settings):
context_processors_data: Optional[Dict] = None
inner_request: Optional[HttpRequest] = None
@ -537,12 +567,12 @@ class ContextProcessorsTests(BaseTestCase):
template = Template(template_str)
rendered = template.render(request_context)
self.assertIn("csrfmiddlewaretoken", rendered)
self.assertEqual(list(context_processors_data.keys()), ["csrf_token"]) # type: ignore[union-attr]
self.assertEqual(inner_request, request)
assert "csrfmiddlewaretoken" in rendered
assert list(context_processors_data.keys()) == ["csrf_token"] # type: ignore[union-attr]
assert inner_request == request
@parametrize_context_behavior(["django", "isolated"])
def test_request_context_in_template_nested(self):
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_request_context_in_template_nested(self, components_settings):
context_processors_data = None
context_processors_data_child = None
parent_request: Optional[HttpRequest] = None
@ -583,14 +613,14 @@ class ContextProcessorsTests(BaseTestCase):
template = Template(template_str)
rendered = template.render(request_context)
self.assertIn("csrfmiddlewaretoken", rendered)
self.assertEqual(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]
self.assertEqual(parent_request, request)
self.assertEqual(child_request, request)
assert "csrfmiddlewaretoken" in rendered
assert list(context_processors_data.keys()) == ["csrf_token"] # type: ignore[union-attr]
assert list(context_processors_data_child.keys()) == ["csrf_token"] # type: ignore[union-attr]
assert parent_request == request
assert child_request == request
@parametrize_context_behavior(["django", "isolated"])
def test_request_context_in_template_slot(self):
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_request_context_in_template_slot(self, components_settings):
context_processors_data = None
context_processors_data_child = None
parent_request: Optional[HttpRequest] = None
@ -635,14 +665,14 @@ class ContextProcessorsTests(BaseTestCase):
template = Template(template_str)
rendered = template.render(request_context)
self.assertIn("csrfmiddlewaretoken", rendered)
self.assertEqual(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]
self.assertEqual(parent_request, request)
self.assertEqual(child_request, request)
assert "csrfmiddlewaretoken" in rendered
assert list(context_processors_data.keys()) == ["csrf_token"] # type: ignore[union-attr]
assert list(context_processors_data_child.keys()) == ["csrf_token"] # type: ignore[union-attr]
assert parent_request == request
assert child_request == request
@parametrize_context_behavior(["django", "isolated"])
def test_request_context_in_python(self):
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_request_context_in_python(self, components_settings):
context_processors_data = None
inner_request: Optional[HttpRequest] = None
@ -661,12 +691,12 @@ class ContextProcessorsTests(BaseTestCase):
request_context = RequestContext(request)
rendered = TestComponent.render(request_context)
self.assertIn("csrfmiddlewaretoken", rendered)
self.assertEqual(list(context_processors_data.keys()), ["csrf_token"]) # type: ignore[union-attr]
self.assertEqual(inner_request, request)
assert "csrfmiddlewaretoken" in rendered
assert list(context_processors_data.keys()) == ["csrf_token"] # type: ignore[union-attr]
assert inner_request == request
@parametrize_context_behavior(["django", "isolated"])
def test_request_context_in_python_nested(self):
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_request_context_in_python_nested(self, components_settings):
context_processors_data: Optional[Dict] = None
context_processors_data_child: Optional[Dict] = None
parent_request: Optional[HttpRequest] = None
@ -701,14 +731,14 @@ class ContextProcessorsTests(BaseTestCase):
request_context = RequestContext(request)
rendered = TestParentComponent.render(request_context)
self.assertIn("csrfmiddlewaretoken", rendered)
self.assertEqual(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]
self.assertEqual(parent_request, request)
self.assertEqual(child_request, request)
assert "csrfmiddlewaretoken" in rendered
assert list(context_processors_data.keys()) == ["csrf_token"] # type: ignore[union-attr]
assert list(context_processors_data_child.keys()) == ["csrf_token"] # type: ignore[union-attr]
assert parent_request == request
assert child_request == request
@parametrize_context_behavior(["django", "isolated"])
def test_request_in_python(self):
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_request_in_python(self, components_settings):
context_processors_data: Optional[Dict] = None
inner_request: Optional[HttpRequest] = None
@ -726,12 +756,12 @@ class ContextProcessorsTests(BaseTestCase):
request = HttpRequest()
rendered = TestComponent.render(request=request)
self.assertIn("csrfmiddlewaretoken", rendered)
self.assertEqual(list(context_processors_data.keys()), ["csrf_token"]) # type: ignore[union-attr]
self.assertEqual(inner_request, request)
assert "csrfmiddlewaretoken" in rendered
assert list(context_processors_data.keys()) == ["csrf_token"] # type: ignore[union-attr]
assert inner_request == request
@parametrize_context_behavior(["django", "isolated"])
def test_request_in_python_nested(self):
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_request_in_python_nested(self, components_settings):
context_processors_data: Optional[Dict] = None
context_processors_data_child: Optional[Dict] = None
parent_request: Optional[HttpRequest] = None
@ -765,15 +795,15 @@ class ContextProcessorsTests(BaseTestCase):
request = HttpRequest()
rendered = TestParentComponent.render(request=request)
self.assertIn("csrfmiddlewaretoken", rendered)
self.assertEqual(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]
self.assertEqual(parent_request, request)
self.assertEqual(child_request, request)
assert "csrfmiddlewaretoken" in rendered
assert list(context_processors_data.keys()) == ["csrf_token"] # type: ignore[union-attr]
assert list(context_processors_data_child.keys()) == ["csrf_token"] # type: ignore[union-attr]
assert parent_request == request
assert child_request == request
# No request, regular Context
@parametrize_context_behavior(["django", "isolated"])
def test_no_context_processor_when_non_request_context_in_python(self):
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_no_context_processor_when_non_request_context_in_python(self, components_settings):
context_processors_data: Optional[Dict] = None
inner_request: Optional[HttpRequest] = None
@ -790,13 +820,13 @@ class ContextProcessorsTests(BaseTestCase):
rendered = TestComponent.render(context=Context())
self.assertNotIn("csrfmiddlewaretoken", rendered)
self.assertEqual(list(context_processors_data.keys()), []) # type: ignore[union-attr]
self.assertEqual(inner_request, None)
assert "csrfmiddlewaretoken" not in rendered
assert list(context_processors_data.keys()) == [] # type: ignore[union-attr]
assert inner_request is None
# No request, no Context
@parametrize_context_behavior(["django", "isolated"])
def test_no_context_processor_when_non_request_context_in_python_2(self):
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_no_context_processor_when_non_request_context_in_python_2(self, components_settings):
context_processors_data: Optional[Dict] = None
inner_request: Optional[HttpRequest] = None
@ -813,13 +843,13 @@ class ContextProcessorsTests(BaseTestCase):
rendered = TestComponent.render()
self.assertNotIn("csrfmiddlewaretoken", rendered)
self.assertEqual(list(context_processors_data.keys()), []) # type: ignore[union-attr]
self.assertEqual(inner_request, None)
assert "csrfmiddlewaretoken" not in rendered
assert list(context_processors_data.keys()) == [] # type: ignore[union-attr]
assert inner_request is None
# Yes request, regular Context
@parametrize_context_behavior(["django", "isolated"])
def test_context_processor_when_regular_context_and_request_in_python(self):
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_context_processor_when_regular_context_and_request_in_python(self, components_settings):
context_processors_data: Optional[Dict] = None
inner_request: Optional[HttpRequest] = None
@ -837,17 +867,17 @@ class ContextProcessorsTests(BaseTestCase):
request = HttpRequest()
rendered = TestComponent.render(Context(), request=request)
self.assertIn("csrfmiddlewaretoken", rendered)
self.assertEqual(list(context_processors_data.keys()), ["csrf_token"]) # type: ignore[union-attr]
self.assertEqual(inner_request, request)
assert "csrfmiddlewaretoken" in rendered
assert list(context_processors_data.keys()) == ["csrf_token"] # type: ignore[union-attr]
assert inner_request == request
def test_raises_on_accessing_context_processors_data_outside_of_rendering(self):
class TestComponent(Component):
template: types.django_html = """{% csrf_token %}"""
with self.assertRaisesMessage(
with pytest.raises(
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
@ -855,38 +885,37 @@ class ContextProcessorsTests(BaseTestCase):
class TestComponent(Component):
template: types.django_html = """{% csrf_token %}"""
with self.assertRaisesMessage(
with pytest.raises(
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
class OuterContextPropertyTests(BaseTestCase):
class OuterContextComponent(Component):
template: types.django_html = """
Variable: <strong>{{ variable }}</strong>
"""
@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):
template: types.django_html = """
Variable: <strong>{{ variable }}</strong>
"""
def get_context_data(self):
return self.outer_context.flatten() # type: ignore[union-attr]
def get_context_data(self):
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 = """
{% load component_tags %}
{% component 'outer_context_component' only %}{% endcomponent %}
"""
template = Template(template_str)
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):
template: types.django_html = """
{% load component_tags %}
@ -936,16 +965,8 @@ class ContextVarsIsFilledTests(BaseTestCase):
</div>
"""
def setUp(self) -> None:
super().setUp()
registry.register("conditional_slots", self.ComponentWithConditionalSlots)
registry.register(
"complex_conditional_slots",
self.ComponentWithComplexConditionalSlots,
)
@parametrize_context_behavior(["django", "isolated"])
def test_is_filled_vars(self):
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_is_filled_vars(self, components_settings):
registry.register("is_filled_vars", self.IsFilledVarsComponent)
template: types.django_html = """
@ -968,10 +989,10 @@ class ContextVarsIsFilledTests(BaseTestCase):
escape_this_________: True
</div>
"""
self.assertHTMLEqual(rendered, expected)
assertHTMLEqual(rendered, expected)
@parametrize_context_behavior(["django", "isolated"])
def test_is_filled_vars_default(self):
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_is_filled_vars_default(self, components_settings):
registry.register("is_filled_vars", self.IsFilledVarsComponent)
template: types.django_html = """
@ -991,10 +1012,12 @@ class ContextVarsIsFilledTests(BaseTestCase):
escape_this_________: False
</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 = """
{% load component_tags %}
{% component "conditional_slots" %}{% endcomponent %}
@ -1007,10 +1030,12 @@ class ContextVarsIsFilledTests(BaseTestCase):
</div>
"""
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 = """
{% load component_tags %}
{% component "conditional_slots" %}
@ -1028,10 +1053,15 @@ class ContextVarsIsFilledTests(BaseTestCase):
</div>
"""
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 = """
{% load component_tags %}
{% component "complex_conditional_slots" %}
@ -1049,10 +1079,15 @@ class ContextVarsIsFilledTests(BaseTestCase):
</div>
"""
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 = """
{% load component_tags %}
{% component "complex_conditional_slots" %}
@ -1067,10 +1102,10 @@ class ContextVarsIsFilledTests(BaseTestCase):
</div>
"""
rendered = Template(template).render(Context({}))
self.assertHTMLEqual(rendered, expected)
assertHTMLEqual(rendered, expected)
@parametrize_context_behavior(["django", "isolated"])
def test_component_with_negated_conditional_slot(self):
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_component_with_negated_conditional_slot(self, components_settings):
@register("negated_conditional_slot")
class ComponentWithNegatedConditionalSlot(Component):
template: types.django_html = """
@ -1101,10 +1136,10 @@ class ContextVarsIsFilledTests(BaseTestCase):
</div>
"""
rendered = Template(template).render(Context({}))
self.assertHTMLEqual(rendered, expected)
assertHTMLEqual(rendered, expected)
@parametrize_context_behavior(["django", "isolated"])
def test_is_filled_vars_in_hooks(self):
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_is_filled_vars_in_hooks(self, components_settings):
captured_before = None
captured_after = None
@ -1127,5 +1162,5 @@ class ContextVarsIsFilledTests(BaseTestCase):
Template(template).render(Context())
expected = {"default": True}
self.assertEqual(captured_before, expected)
self.assertEqual(captured_after, expected)
assert captured_before == 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
from unittest.mock import Mock
import pytest
from django.http import HttpResponseNotModified
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.components.dynamic import DynamicComponent
from django_components.middleware import ComponentDependencyMiddleware
from .django_test_setup import setup_test_config
from .testutils import BaseTestCase, create_and_process_template_response
from django_components.testing import djc_test
from .testutils import create_and_process_template_response, setup_test_config
setup_test_config({"autodiscover": False})
@ -47,7 +49,8 @@ class SimpleComponent(Component):
js = "script.js"
class RenderDependenciesTests(BaseTestCase):
@djc_test
class TestRenderDependencies:
def test_standalone_render_dependencies(self):
registry.register(name="test", component=SimpleComponent)
@ -61,23 +64,23 @@ class RenderDependenciesTests(BaseTestCase):
rendered_raw = template.render(Context({}))
# Placeholders
self.assertEqual(rendered_raw.count('<link name="CSS_PLACEHOLDER">'), 1)
self.assertEqual(rendered_raw.count('<script name="JS_PLACEHOLDER"></script>'), 1)
assert rendered_raw.count('<link name="CSS_PLACEHOLDER">') == 1
assert rendered_raw.count('<script name="JS_PLACEHOLDER"></script>') == 1
self.assertEqual(rendered_raw.count("<script"), 1)
self.assertEqual(rendered_raw.count("<style"), 0)
self.assertEqual(rendered_raw.count("<link"), 1)
self.assertEqual(rendered_raw.count("_RENDERED"), 1)
assert rendered_raw.count("<script") == 1
assert rendered_raw.count("<style") == 0
assert rendered_raw.count("<link") == 1
assert rendered_raw.count("_RENDERED") == 1
rendered = render_dependencies(rendered_raw)
# 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
self.assertInHTML('<script>console.log("xyz");</script>', rendered, count=1) # Inlined JS
assertInHTML("<style>.xyz { color: red; }</style>", rendered, count=1) # Inlined CSS
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):
registry.register(name="test", component=SimpleComponent)
@ -92,14 +95,14 @@ class RenderDependenciesTests(BaseTestCase):
rendered = create_and_process_template_response(template, use_middleware=True)
# 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
self.assertInHTML('<script>console.log("xyz");</script>', rendered, count=1) # Inlined JS
assertInHTML("<style>.xyz { color: red; }</style>", rendered, count=1) # Inlined CSS
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
self.assertEqual(rendered.count("<link"), 1)
self.assertEqual(rendered.count("<style"), 1)
assertInHTML('<link href="style.css" media="all" rel="stylesheet">', rendered, count=1) # Media.css
assert rendered.count("<link") == 1
assert rendered.count("<style") == 1
def test_component_render_renders_dependencies(self):
class SimpleComponentWithDeps(SimpleComponent):
@ -119,14 +122,14 @@ class RenderDependenciesTests(BaseTestCase):
)
# 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
self.assertInHTML('<script>console.log("xyz");</script>', rendered, count=1) # Inlined JS
assertInHTML("<style>.xyz { color: red; }</style>", rendered, count=1) # Inlined CSS
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
self.assertEqual(rendered.count("<link"), 1)
self.assertEqual(rendered.count("<style"), 1)
assertInHTML('<link href="style.css" media="all" rel="stylesheet">', rendered, count=1) # Media.css
assert rendered.count("<link") == 1
assert rendered.count("<style") == 1
def test_component_render_renders_dependencies_opt_out(self):
class SimpleComponentWithDeps(SimpleComponent):
@ -146,18 +149,18 @@ class RenderDependenciesTests(BaseTestCase):
render_dependencies=False,
)
self.assertEqual(rendered_raw.count("<script"), 1)
self.assertEqual(rendered_raw.count("<style"), 0)
self.assertEqual(rendered_raw.count("<link"), 1)
self.assertEqual(rendered_raw.count("_RENDERED"), 1)
assert rendered_raw.count("<script") == 1
assert rendered_raw.count("<style") == 0
assert rendered_raw.count("<link") == 1
assert rendered_raw.count("_RENDERED") == 1
# 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
self.assertInHTML('<link href="style.css" media="all" rel="stylesheet">', rendered_raw, count=0) # Media.css
assertInHTML("<style>.xyz { color: red; }</style>", rendered_raw, count=0) # Inlined CSS
assertInHTML('<link href="style.css" media="all" rel="stylesheet">', rendered_raw, count=0) # Media.css
self.assertInHTML(
assertInHTML(
'<script>console.log("xyz");</script>',
rendered_raw,
count=0,
@ -182,14 +185,14 @@ class RenderDependenciesTests(BaseTestCase):
rendered = response.content.decode()
# 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
self.assertInHTML('<script>console.log("xyz");</script>', rendered, count=1) # Inlined JS
assertInHTML("<style>.xyz { color: red; }</style>", rendered, count=1) # Inlined CSS
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
self.assertEqual(rendered.count("<link"), 1)
self.assertEqual(rendered.count("<style"), 1)
assert rendered.count('<link href="style.css" media="all" rel="stylesheet">') == 1 # Media.css
assert rendered.count("<link") == 1
assert rendered.count("<style") == 1
def test_inserts_styles_and_script_to_default_places_if_not_overriden(self):
registry.register(name="test", component=SimpleComponent)
@ -207,12 +210,12 @@ class RenderDependenciesTests(BaseTestCase):
rendered_raw = Template(template_str).render(Context({}))
rendered = render_dependencies(rendered_raw)
self.assertEqual(rendered.count("<script"), 4)
self.assertEqual(rendered.count("<style"), 1)
self.assertEqual(rendered.count("<link"), 1)
self.assertEqual(rendered.count("_RENDERED"), 0)
assert rendered.count("<script") == 4
assert rendered.count("<style") == 1
assert rendered.count("<link") == 1
assert rendered.count("_RENDERED") == 0
self.assertInHTML(
assertInHTML(
"""
<head>
<style>.xyz { color: red; }</style>
@ -226,12 +229,12 @@ class RenderDependenciesTests(BaseTestCase):
body_re = re.compile(r"<body>(.*?)</body>", re.DOTALL)
rendered_body = body_re.search(rendered).group(1) # type: ignore[union-attr]
self.assertInHTML(
assertInHTML(
"""<script src="django_components/django_components.min.js">""",
rendered_body,
count=1,
)
self.assertInHTML(
assertInHTML(
'<script>console.log("xyz");</script>',
rendered_body,
count=1,
@ -256,12 +259,12 @@ class RenderDependenciesTests(BaseTestCase):
rendered_raw = Template(template_str).render(Context({}))
rendered = render_dependencies(rendered_raw)
self.assertEqual(rendered.count("<script"), 4)
self.assertEqual(rendered.count("<style"), 1)
self.assertEqual(rendered.count("<link"), 1)
self.assertEqual(rendered.count("_RENDERED"), 0)
assert rendered.count("<script") == 4
assert rendered.count("<style") == 1
assert rendered.count("<link") == 1
assert rendered.count("_RENDERED") == 0
self.assertInHTML(
assertInHTML(
"""
<body>
Variable: <strong data-djc-id-a1bc41>foo</strong>
@ -277,12 +280,12 @@ class RenderDependenciesTests(BaseTestCase):
head_re = re.compile(r"<head>(.*?)</head>", re.DOTALL)
rendered_head = head_re.search(rendered).group(1) # type: ignore[union-attr]
self.assertInHTML(
assertInHTML(
"""<script src="django_components/django_components.min.js">""",
rendered_head,
count=1,
)
self.assertInHTML(
assertInHTML(
'<script>console.log("xyz");</script>',
rendered_head,
count=1,
@ -300,7 +303,7 @@ class RenderDependenciesTests(BaseTestCase):
rendered_raw = Template(template_str).render(Context({"formset": [1]}))
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):
registry.register(name="test", component=SimpleComponent)
@ -358,7 +361,7 @@ class RenderDependenciesTests(BaseTestCase):
</table>
"""
self.assertHTMLEqual(expected, rendered)
assertHTMLEqual(expected, rendered)
# Explanation: The component is used in the template, but the template doesn't use
# {% component_js_dependencies %} or {% component_css_dependencies %} tags,
@ -435,7 +438,7 @@ class RenderDependenciesTests(BaseTestCase):
</script>
""" # noqa: E501
self.assertHTMLEqual(expected, rendered)
assertHTMLEqual(expected, rendered)
def test_raises_if_script_end_tag_inside_component_js(self):
class ComponentWithScript(SimpleComponent):
@ -445,9 +448,9 @@ class RenderDependenciesTests(BaseTestCase):
registry.register(name="test", component=ComponentWithScript)
with self.assertRaisesMessage(
with pytest.raises(
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"})
@ -462,19 +465,20 @@ class RenderDependenciesTests(BaseTestCase):
registry.register(name="test", component=ComponentWithScript)
with self.assertRaisesMessage(
with pytest.raises(
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"})
class MiddlewareTests(BaseTestCase):
@djc_test
class TestMiddleware:
def test_middleware_response_without_content_type(self):
response = HttpResponseNotModified()
middleware = ComponentDependencyMiddleware(get_response=lambda _: response)
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):
registry.register("dynamic", DynamicComponent)
@ -492,14 +496,14 @@ class MiddlewareTests(BaseTestCase):
def assert_dependencies(content: str):
# 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
self.assertInHTML('<script>console.log("xyz");</script>', content, count=1)
assertInHTML('<script>console.log("xyz");</script>', content, count=1)
# Inlined CSS
self.assertInHTML("<style>.xyz { color: red; }</style>", content, count=1)
assertInHTML("<style>.xyz { color: red; }</style>", content, count=1)
# 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(
template,
@ -507,10 +511,7 @@ class MiddlewareTests(BaseTestCase):
)
assert_dependencies(rendered1)
self.assertEqual(
rendered1.count('Variable: <strong data-djc-id-a1bc42="" data-djc-id-a1bc41="">value</strong>'),
1,
)
assert rendered1.count('Variable: <strong data-djc-id-a1bc42="" data-djc-id-a1bc41="">value</strong>') == 1
rendered2 = create_and_process_template_response(
template,
@ -518,10 +519,7 @@ class MiddlewareTests(BaseTestCase):
)
assert_dependencies(rendered2)
self.assertEqual(
rendered2.count('Variable: <strong data-djc-id-a1bc44="" data-djc-id-a1bc43="">value</strong>'),
1,
)
assert rendered2.count('Variable: <strong data-djc-id-a1bc44="" data-djc-id-a1bc43="">value</strong>') == 1
rendered3 = create_and_process_template_response(
template,
@ -529,7 +527,4 @@ class MiddlewareTests(BaseTestCase):
)
assert_dependencies(rendered3)
self.assertEqual(
rendered3.count('Variable: <strong data-djc-id-a1bc46="" data-djc-id-a1bc45="">value</strong>'),
1,
)
assert 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 django.test import override_settings
from playwright.async_api import Error, Page
import pytest
from playwright.async_api import Browser, Error, Page
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.testutils import BaseTestCase
setup_test_config(
components={"autodiscover": False},
@ -18,69 +19,73 @@ setup_test_config(
urlpatterns: List = []
class _BaseDepManagerTestCase(BaseTestCase):
async def _create_page_with_dep_manager(self) -> Page:
page = await self.browser.new_page()
async def _create_page_with_dep_manager(browser: Browser) -> Page:
page = await browser.new_page()
# 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`
script_url = TEST_SERVER_URL + "/static/django_components/django_components.min.js"
await page.goto(script_url)
# 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`
script_url = TEST_SERVER_URL + "/static/django_components/django_components.min.js"
await page.goto(script_url)
# The page's body is the script code. We load it by executing the code
await page.evaluate(
"""
() => {
eval(document.body.textContent);
}
"""
)
# The page's body is the script code. We load it by executing the code
await page.evaluate(
"""
() => {
eval(document.body.textContent);
}
"""
)
# Ensure the body is clear
await page.evaluate(
"""
() => {
document.body.innerHTML = '';
document.head.innerHTML = '';
}
"""
)
# Ensure the body is clear
await page.evaluate(
"""
() => {
document.body.innerHTML = '';
document.head.innerHTML = '';
}
"""
)
return page
return page
@override_settings(STATIC_URL="static/")
class DependencyManagerTests(_BaseDepManagerTestCase):
@djc_test(
django_settings={
"STATIC_URL": "static/",
}
)
class TestDependencyManager:
@with_playwright
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
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)")
self.assertEqual(
keys,
[
"callComponent",
"registerComponent",
"registerComponentData",
"loadJs",
"loadCss",
"markScriptLoaded",
],
)
assert keys == [
"callComponent",
"registerComponent",
"registerComponentData",
"loadJs",
"loadCss",
"markScriptLoaded",
]
await page.close()
# Tests for `manager.loadJs()` / `manager.loadCss()` / `manager.markAsLoaded()`
@override_settings(STATIC_URL="static/")
class LoadScriptTests(_BaseDepManagerTestCase):
@djc_test(
django_settings={
"STATIC_URL": "static/",
}
)
class TestLoadScript:
@with_playwright
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
test_js: types.js = """() => {
@ -113,20 +118,18 @@ class LoadScriptTests(_BaseDepManagerTestCase):
data = await page.evaluate(test_js)
self.assertEqual(data["bodyAfterFirstLoad"], '<script src="/one/two"></script>')
self.assertEqual(data["bodyAfterSecondLoad"], '<script src="/one/two"></script>')
self.assertEqual(
data["bodyAfterThirdLoad"], '<script src="/one/two"></script><script src="/four/three"></script>'
)
assert data["bodyAfterFirstLoad"] == '<script src="/one/two"></script>'
assert data["bodyAfterSecondLoad"] == '<script src="/one/two"></script>'
assert data["bodyAfterThirdLoad"] == '<script src="/one/two"></script><script src="/four/three"></script>'
self.assertEqual(data["headBeforeFirstLoad"], data["headAfterThirdLoad"])
self.assertEqual(data["headBeforeFirstLoad"], "")
assert data["headBeforeFirstLoad"] == data["headAfterThirdLoad"]
assert data["headBeforeFirstLoad"] == ""
await page.close()
@with_playwright
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
test_js: types.js = """() => {
@ -159,18 +162,18 @@ class LoadScriptTests(_BaseDepManagerTestCase):
data = await page.evaluate(test_js)
self.assertEqual(data["headAfterFirstLoad"], '<link href="/one/two">')
self.assertEqual(data["headAfterSecondLoad"], '<link href="/one/two">')
self.assertEqual(data["headAfterThirdLoad"], '<link href="/one/two"><link href="/four/three">')
assert data["headAfterFirstLoad"] == '<link href="/one/two">'
assert data["headAfterSecondLoad"] == '<link href="/one/two">'
assert data["headAfterThirdLoad"] == '<link href="/one/two"><link href="/four/three">'
self.assertEqual(data["bodyBeforeFirstLoad"], data["bodyAfterThirdLoad"])
self.assertEqual(data["bodyBeforeFirstLoad"], "")
assert data["bodyBeforeFirstLoad"] == data["bodyAfterThirdLoad"]
assert data["bodyBeforeFirstLoad"] == ""
await page.close()
@with_playwright
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
test_js: types.js = """() => {
@ -194,18 +197,22 @@ class LoadScriptTests(_BaseDepManagerTestCase):
data = await page.evaluate(test_js)
self.assertEqual(data["headAfterFirstLoad"], "")
self.assertEqual(data["bodyAfterSecondLoad"], "")
assert data["headAfterFirstLoad"] == ""
assert data["bodyAfterSecondLoad"] == ""
await page.close()
# Tests for `manager.registerComponent()` / `registerComponentData()` / `callComponent()`
@override_settings(STATIC_URL="static/")
class CallComponentTests(_BaseDepManagerTestCase):
@djc_test(
django_settings={
"STATIC_URL": "static/",
}
)
class TestCallComponent:
@with_playwright
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 = """() => {
const manager = Components.createComponentsManager();
@ -240,26 +247,23 @@ class CallComponentTests(_BaseDepManagerTestCase):
data = await page.evaluate(test_js)
self.assertEqual(data["result"], 123)
self.assertEqual(
data["captured"],
{
"data": {
"hello": "world",
},
"ctx": {
"els": ['<div data-djc-id-12345=""> abc </div>'],
"id": "12345",
"name": "my_comp",
},
assert data["result"] == 123
assert data["captured"] == {
"data": {
"hello": "world",
},
)
"ctx": {
"els": ['<div data-djc-id-12345=""> abc </div>'],
"id": "12345",
"name": "my_comp",
},
}
await page.close()
@with_playwright
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 = """() => {
const manager = Components.createComponentsManager();
@ -292,14 +296,14 @@ class CallComponentTests(_BaseDepManagerTestCase):
data = await page.evaluate(test_js)
self.assertEqual(data["result"], 123)
self.assertEqual(data["isPromise"], True)
assert data["result"] == 123
assert data["isPromise"] is True
await page.close()
@with_playwright
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 = """() => {
const manager = Components.createComponentsManager();
@ -327,13 +331,13 @@ class CallComponentTests(_BaseDepManagerTestCase):
data = await page.evaluate(test_js)
self.assertEqual(data, None)
assert data is None
await page.close()
@with_playwright
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 = """() => {
const manager = Components.createComponentsManager();
@ -360,16 +364,16 @@ class CallComponentTests(_BaseDepManagerTestCase):
data = await page.evaluate(test_js)
self.assertEqual(len(data), 1)
self.assertEqual(data[0]["status"], "rejected")
self.assertIsInstance(data[0]["reason"], Error)
self.assertEqual(data[0]["reason"].message, "Oops!")
assert len(data) == 1
assert data[0]["status"] == "rejected"
assert isinstance(data[0]["reason"], Error)
assert data[0]["reason"].message == "Oops!"
await page.close()
@with_playwright
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 = """() => {
const manager = Components.createComponentsManager();
@ -390,8 +394,9 @@ class CallComponentTests(_BaseDepManagerTestCase):
manager.callComponent(compName, compId, inputHash);
}"""
with self.assertRaisesMessage(
Error, "Error: [Components] 'my_comp': No elements with component ID '12345' found"
with pytest.raises(
Error,
match=re.escape("Error: [Components] 'my_comp': No elements with component ID '12345' found"),
):
await page.evaluate(test_js)
@ -399,7 +404,7 @@ class CallComponentTests(_BaseDepManagerTestCase):
@with_playwright
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 = """() => {
const manager = Components.createComponentsManager();
@ -418,14 +423,17 @@ class CallComponentTests(_BaseDepManagerTestCase):
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.close()
@with_playwright
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 = """() => {
const manager = Components.createComponentsManager();
@ -444,7 +452,10 @@ class CallComponentTests(_BaseDepManagerTestCase):
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.close()

View file

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

View file

@ -6,23 +6,25 @@ in an actual browser.
import re
from playwright.async_api import Page
from pytest_django.asserts import assertHTMLEqual, assertInHTML
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.testutils import BaseTestCase
setup_test_config({"autodiscover": False})
# NOTE: All views, components, and associated JS and CSS are defined in
# `tests/e2e/testserver/testserver`
class E2eDependencyRenderingTests(BaseTestCase):
@djc_test
class TestE2eDependencyRendering:
@with_playwright
async def test_single_component_dependencies(self):
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)
test_js: types.js = """() => {
@ -46,24 +48,23 @@ class E2eDependencyRenderingTests(BaseTestCase):
data = await page.evaluate(test_js)
# Check that the actual HTML content was loaded
self.assertRegex(
data["bodyHTML"],
re.compile(r'Variable: <strong class="inner" data-djc-id-\w{6}="">foo</strong>'),
)
self.assertInHTML('<div class="my-style"> 123 </div>', data["bodyHTML"], count=1)
self.assertInHTML('<div class="my-style2"> xyz </div>', data["bodyHTML"], count=1)
assert 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)
assertInHTML('<div class="my-style2"> xyz </div>', data["bodyHTML"], count=1)
# Check components' inlined JS got loaded
self.assertEqual(data["componentJsMsg"], "kapowww!")
assert data["componentJsMsg"] == "kapowww!"
# Check JS from Media.js got loaded
self.assertEqual(data["scriptJsMsg"], {"hello": "world"})
assert data["scriptJsMsg"] == {"hello": "world"}
# Check components' inlined CSS got loaded
self.assertEqual(data["innerFontSize"], "4px")
assert data["innerFontSize"] == "4px"
# 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()
@ -71,7 +72,7 @@ class E2eDependencyRenderingTests(BaseTestCase):
async def test_multiple_component_dependencies(self):
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)
test_js: types.js = """() => {
@ -111,8 +112,7 @@ class E2eDependencyRenderingTests(BaseTestCase):
data = await page.evaluate(test_js)
# Check that the actual HTML content was loaded
self.assertRegex(
data["bodyHTML"],
assert re.compile(
# <div class="outer" data-djc-id-10uLMD>
# Variable:
# <strong class="inner" data-djc-id-DZEnUC>
@ -125,39 +125,37 @@ class E2eDependencyRenderingTests(BaseTestCase):
# </div>
# <div class="my-style">123</div>
# <div class="my-style2">xyz</div>
re.compile(
r'<div class="outer" data-djc-id-\w{6}="">\s*'
r"Variable:\s*"
r'<strong class="inner" data-djc-id-\w{6}="">\s*'
r"variable\s*"
r"<\/strong>\s*"
r"XYZ:\s*"
r'<strong class="other" data-djc-id-\w{6}="">\s*'
r"variable_inner\s*"
r"<\/strong>\s*"
r"<\/div>\s*"
r'<div class="my-style">123<\/div>\s*'
r'<div class="my-style2">xyz<\/div>\s*'
),
)
r'<div class="outer" data-djc-id-\w{6}="">\s*'
r"Variable:\s*"
r'<strong class="inner" data-djc-id-\w{6}="">\s*'
r"variable\s*"
r"<\/strong>\s*"
r"XYZ:\s*"
r'<strong class="other" data-djc-id-\w{6}="">\s*'
r"variable_inner\s*"
r"<\/strong>\s*"
r"<\/div>\s*"
r'<div class="my-style">123<\/div>\s*'
r'<div class="my-style2">xyz<\/div>\s*'
).search(data["bodyHTML"]) is not None
# Check components' inlined JS got loaded
self.assertEqual(data["component1JsMsg"], "kapowww!")
self.assertEqual(data["component2JsMsg"], "bongo!")
self.assertEqual(data["component3JsMsg"], "wowzee!")
assert data["component1JsMsg"] == "kapowww!"
assert data["component2JsMsg"] == "bongo!"
assert data["component3JsMsg"] == "wowzee!"
# Check JS from Media.js got loaded
self.assertEqual(data["scriptJs1Msg"], {"hello": "world"})
self.assertEqual(data["scriptJs2Msg"], {"hello2": "world2"})
assert data["scriptJs1Msg"] == {"hello": "world"}
assert data["scriptJs2Msg"] == {"hello2": "world2"}
# Check components' inlined CSS got loaded
self.assertEqual(data["innerFontSize"], "4px")
self.assertEqual(data["outerFontSize"], "40px")
self.assertEqual(data["otherDisplay"], "flex")
assert data["innerFontSize"] == "4px"
assert data["outerFontSize"] == "40px"
assert data["otherDisplay"] == "flex"
# Check CSS from Media.css got loaded
self.assertIn("rgb(0, 0, 255)", data["myStyleBg"]) # AKA 'background: blue'
self.assertEqual("rgb(255, 0, 0)", data["myStyle2Color"]) # AKA 'color: red'
assert "rgb(0, 0, 255)" in data["myStyleBg"] # AKA 'background: blue'
assert data["myStyle2Color"] == "rgb(255, 0, 0)" # AKA 'color: red'
await page.close()
@ -165,7 +163,7 @@ class E2eDependencyRenderingTests(BaseTestCase):
async def test_renders_css_nojs_env(self):
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)
test_js: types.js = """() => {
@ -205,53 +203,51 @@ class E2eDependencyRenderingTests(BaseTestCase):
data = await page.evaluate(test_js)
# Check that the actual HTML content was loaded
self.assertRegex(
data["bodyHTML"],
# <div class="outer" data-djc-id-10uLMD>
# Variable:
# <strong class="inner" data-djc-id-DZEnUC>
# variable
# </strong>
# XYZ:
# <strong data-djc-id-IYirHK class="other">
# variable_inner
# </strong>
# </div>
# <div class="my-style">123</div>
# <div class="my-style2">xyz</div>
re.compile(
r'<div class="outer" data-djc-id-\w{6}="">\s*'
r"Variable:\s*"
r'<strong class="inner" data-djc-id-\w{6}="">\s*'
r"variable\s*"
r"<\/strong>\s*"
r"XYZ:\s*"
r'<strong class="other" data-djc-id-\w{6}="">\s*'
r"variable_inner\s*"
r"<\/strong>\s*"
r"<\/div>\s*"
r'<div class="my-style">123<\/div>\s*'
r'<div class="my-style2">xyz<\/div>\s*'
),
)
#
# <div class="outer" data-djc-id-10uLMD>
# Variable:
# <strong class="inner" data-djc-id-DZEnUC>
# variable
# </strong>
# XYZ:
# <strong data-djc-id-IYirHK class="other">
# variable_inner
# </strong>
# </div>
# <div class="my-style">123</div>
# <div class="my-style2">xyz</div>
assert re.compile(
r'<div class="outer" data-djc-id-\w{6}="">\s*'
r"Variable:\s*"
r'<strong class="inner" data-djc-id-\w{6}="">\s*'
r"variable\s*"
r"<\/strong>\s*"
r"XYZ:\s*"
r'<strong class="other" data-djc-id-\w{6}="">\s*'
r"variable_inner\s*"
r"<\/strong>\s*"
r"<\/div>\s*"
r'<div class="my-style">123<\/div>\s*'
r'<div class="my-style2">xyz<\/div>\s*'
).search(data["bodyHTML"]) is not None
# Check components' inlined JS did NOT get loaded
self.assertEqual(data["component1JsMsg"], None)
self.assertEqual(data["component2JsMsg"], None)
self.assertEqual(data["component3JsMsg"], None)
assert data["component1JsMsg"] is None
assert data["component2JsMsg"] is None
assert data["component3JsMsg"] is None
# Check JS from Media.js did NOT get loaded
self.assertEqual(data["scriptJs1Msg"], None)
self.assertEqual(data["scriptJs2Msg"], None)
assert data["scriptJs1Msg"] is None
assert data["scriptJs2Msg"] is None
# Check components' inlined CSS got loaded
self.assertEqual(data["innerFontSize"], "4px")
self.assertEqual(data["outerFontSize"], "40px")
self.assertEqual(data["otherDisplay"], "flex")
assert data["innerFontSize"] == "4px"
assert data["outerFontSize"] == "40px"
assert data["otherDisplay"] == "flex"
# Check CSS from Media.css got loaded
self.assertIn("rgb(0, 0, 255)", data["myStyleBg"]) # AKA 'background: blue'
self.assertEqual("rgb(255, 0, 0)", data["myStyle2Color"]) # AKA 'color: red'
assert "rgb(0, 0, 255)" in data["myStyleBg"] # AKA 'background: blue'
assert "rgb(255, 0, 0)" in data["myStyle2Color"] # AKA 'color: red'
await page.close()
@ -259,7 +255,7 @@ class E2eDependencyRenderingTests(BaseTestCase):
async def test_js_executed_in_order__js(self):
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)
test_js: types.js = """() => {
@ -271,13 +267,13 @@ class E2eDependencyRenderingTests(BaseTestCase):
data = await page.evaluate(test_js)
# Check components' inlined JS got loaded
self.assertEqual(data["testSimpleComponent"], "kapowww!")
self.assertEqual(data["testSimpleComponentNested"], "bongo!")
self.assertEqual(data["testOtherComponent"], "wowzee!")
assert data["testSimpleComponent"] == "kapowww!"
assert data["testSimpleComponentNested"] == "bongo!"
assert data["testOtherComponent"] == "wowzee!"
# Check JS from Media.js got loaded
self.assertEqual(data["testMsg"], {"hello": "world"})
self.assertEqual(data["testMsg2"], {"hello2": "world2"})
assert data["testMsg"] == {"hello": "world"}
assert data["testMsg2"] == {"hello2": "world2"}
await page.close()
@ -285,7 +281,7 @@ class E2eDependencyRenderingTests(BaseTestCase):
async def test_js_executed_in_order__media(self):
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)
test_js: types.js = """() => {
@ -298,13 +294,13 @@ class E2eDependencyRenderingTests(BaseTestCase):
# Check components' inlined JS got loaded
# NOTE: The Media JS are loaded BEFORE the components' JS, so they should be empty
self.assertEqual(data["testSimpleComponent"], None)
self.assertEqual(data["testSimpleComponentNested"], None)
self.assertEqual(data["testOtherComponent"], None)
assert data["testSimpleComponent"] is None
assert data["testSimpleComponentNested"] is None
assert data["testOtherComponent"] is None
# Check JS from Media.js
self.assertEqual(data["testMsg"], {"hello": "world"})
self.assertEqual(data["testMsg2"], {"hello2": "world2"})
assert data["testMsg"] == {"hello": "world"}
assert data["testMsg2"] == {"hello2": "world2"}
await page.close()
@ -315,7 +311,7 @@ class E2eDependencyRenderingTests(BaseTestCase):
async def test_js_executed_in_order__invalid(self):
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)
test_js: types.js = """() => {
@ -326,20 +322,20 @@ class E2eDependencyRenderingTests(BaseTestCase):
data = await page.evaluate(test_js)
# Check components' inlined JS got loaded
self.assertEqual(data["testSimpleComponent"], None)
self.assertEqual(data["testSimpleComponentNested"], None)
self.assertEqual(data["testOtherComponent"], None)
assert data["testSimpleComponent"] is None
assert data["testSimpleComponentNested"] is None
assert data["testOtherComponent"] is None
# Check JS from Media.js got loaded
self.assertEqual(data["testMsg"], None)
self.assertEqual(data["testMsg2"], None)
assert data["testMsg"] is None
assert data["testMsg2"] is None
await page.close()
# Fragment where JS and CSS is defined on Component class
@with_playwright
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")
test_before_js: types.js = """() => {
@ -353,8 +349,8 @@ class E2eDependencyRenderingTests(BaseTestCase):
data_before = await page.evaluate(test_before_js)
self.assertEqual(data_before["targetHtml"], '<div id="target">OLD</div>')
self.assertEqual(data_before["fragHtml"], None)
assert data_before["targetHtml"] == '<div id="target">OLD</div>'
assert data_before["fragHtml"] is None
# Clicking button should load and insert the fragment
await page.locator("button").click()
@ -380,21 +376,18 @@ class E2eDependencyRenderingTests(BaseTestCase):
data = await page.evaluate(test_js)
self.assertEqual(data["targetHtml"], None)
self.assertRegex(
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>"
),
)
self.assertIn("rgb(0, 0, 255)", data["fragBg"]) # AKA 'background: blue'
assert data["targetHtml"] is None
assert 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>"
).search(data["fragHtml"]) is not None
assert "rgb(0, 0, 255)" in data["fragBg"] # AKA 'background: blue'
await page.close()
# Fragment where JS and CSS is defined on Media class
@with_playwright
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")
test_before_js: types.js = """() => {
@ -408,8 +401,8 @@ class E2eDependencyRenderingTests(BaseTestCase):
data_before = await page.evaluate(test_before_js)
self.assertEqual(data_before["targetHtml"], '<div id="target">OLD</div>')
self.assertEqual(data_before["fragHtml"], None)
assert data_before["targetHtml"] == '<div id="target">OLD</div>'
assert data_before["fragHtml"] is None
# Clicking button should load and insert the fragment
await page.locator("button").click()
@ -433,21 +426,18 @@ class E2eDependencyRenderingTests(BaseTestCase):
data = await page.evaluate(test_js)
self.assertEqual(data["targetHtml"], None)
self.assertRegex(
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>"
),
)
self.assertIn("rgb(0, 0, 255)", data["fragBg"]) # AKA 'background: blue'
assert data["targetHtml"] is None
assert 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>"
).search(data["fragHtml"]) is not None
assert "rgb(0, 0, 255)" in data["fragBg"] # AKA 'background: blue'
await page.close()
# Fragment loaded by AlpineJS
@with_playwright
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")
test_before_js: types.js = """() => {
@ -461,8 +451,8 @@ class E2eDependencyRenderingTests(BaseTestCase):
data_before = await page.evaluate(test_before_js)
self.assertEqual(data_before["targetHtml"], '<div id="target" x-html="htmlVar">OLD</div>')
self.assertEqual(data_before["fragHtml"], None)
assert data_before["targetHtml"] == '<div id="target" x-html="htmlVar">OLD</div>'
assert data_before["fragHtml"] is None
# Clicking button should load and insert the fragment
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,
# but only change its contents.
self.assertRegex(
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>"
),
)
self.assertIn("rgb(0, 0, 255)", data["fragBg"]) # AKA 'background: blue'
assert 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>"
).search(data["targetHtml"]) is not None
assert "rgb(0, 0, 255)" in data["fragBg"] # AKA 'background: blue'
await page.close()
# Fragment loaded by HTMX
@with_playwright
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")
test_before_js: types.js = """() => {
@ -517,8 +504,8 @@ class E2eDependencyRenderingTests(BaseTestCase):
data_before = await page.evaluate(test_before_js)
self.assertEqual(data_before["targetHtml"], '<div id="target">OLD</div>')
self.assertEqual(data_before["fragHtml"], None)
assert data_before["targetHtml"] == '<div id="target">OLD</div>'
assert data_before["fragHtml"] is None
# Clicking button should load and insert the fragment
await page.locator("button").click()
@ -544,14 +531,11 @@ class E2eDependencyRenderingTests(BaseTestCase):
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
# extra CSS classes added by HTMX, which results in flaky tests.
self.assertRegex(
data["fragInnerHtml"],
re.compile(r'123\s*<span id="frag-text">xxx</span>'),
)
self.assertIn("rgb(0, 0, 255)", data["fragBg"]) # AKA 'background: blue'
assert re.compile(r'123\s*<span id="frag-text">xxx</span>').search(data["fragInnerHtml"]) is not None
assert "rgb(0, 0, 255)" in data["fragBg"] # AKA 'background: blue'
await page.close()
@ -559,11 +543,11 @@ class E2eDependencyRenderingTests(BaseTestCase):
async def test_alpine__head(self):
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)
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()
@ -571,11 +555,11 @@ class E2eDependencyRenderingTests(BaseTestCase):
async def test_alpine__body(self):
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)
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()
@ -583,11 +567,11 @@ class E2eDependencyRenderingTests(BaseTestCase):
async def test_alpine__body2(self):
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)
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()
@ -595,10 +579,10 @@ class E2eDependencyRenderingTests(BaseTestCase):
async def test_alpine__invalid(self):
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)
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()

View file

@ -1,15 +1,18 @@
"""Catch-all for tests that use template tags and don't fit other files"""
import re
from typing import Any, Dict
import pytest
from django.template import Context, Template, TemplateSyntaxError
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.expression import DynamicFilterExpression, is_aggregate_key
from .django_test_setup import setup_test_config
from .testutils import BaseTestCase, parametrize_context_behavior
from django_components.testing import djc_test
from .testutils import PARAMETRIZE_CONTEXT_BEHAVIOR, setup_test_config
setup_test_config({"autodiscover": False})
@ -47,19 +50,20 @@ def make_context(d: Dict):
# NOTE: Django calls the `{{ }}` syntax "variables" and `{% %}` "blocks"
class DynamicExprTests(BaseTestCase):
@djc_test
class TestDynamicExpr:
def test_variable_resolve_dynamic_expr(self):
expr = DynamicFilterExpression(default_parser, '"{{ var_a|lower }}"')
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):
with self.assertRaises(TemplateSyntaxError):
with pytest.raises(TemplateSyntaxError):
DynamicFilterExpression(default_parser, "'{{ var_a|lower }}\"")
@parametrize_context_behavior(["django", "isolated"])
def test_variable_in_template(self):
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_variable_in_template(self, components_settings):
captured = {}
@register("test")
@ -110,11 +114,11 @@ class DynamicExprTests(BaseTestCase):
)
# Check that variables passed to the component are of correct type
self.assertEqual(captured["pos_var1"], "lorem")
self.assertEqual(captured["bool_var"], True)
self.assertEqual(captured["list_var"], [{"a": 1}, {"a": 2}])
assert captured["pos_var1"] == "lorem"
assert captured["bool_var"] is True
assert captured["list_var"] == [{"a": 1}, {"a": 2}]
self.assertHTMLEqual(
assertHTMLEqual(
rendered,
"""
<!-- _RENDERED SimpleComponent_5b8d97,a1bc3f,, -->
@ -124,8 +128,8 @@ class DynamicExprTests(BaseTestCase):
""",
)
@parametrize_context_behavior(["django", "isolated"])
def test_block_in_template(self):
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_block_in_template(self, components_settings):
registry.library.tag(noop)
captured = {}
@ -183,11 +187,11 @@ class DynamicExprTests(BaseTestCase):
)
# Check that variables passed to the component are of correct type
self.assertEqual(captured["bool_var"], True)
self.assertEqual(captured["dict_var"], {"a": 3})
self.assertEqual(captured["list_var"], [{"a": 1}, {"a": 2}])
assert captured["bool_var"] is True
assert captured["dict_var"] == {"a": 3}
assert captured["list_var"] == [{"a": 1}, {"a": 2}]
self.assertHTMLEqual(
assertHTMLEqual(
rendered,
"""
<!-- _RENDERED SimpleComponent_743413,a1bc3f,, -->
@ -198,8 +202,8 @@ class DynamicExprTests(BaseTestCase):
""",
)
@parametrize_context_behavior(["django", "isolated"])
def test_comment_in_template(self):
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_comment_in_template(self, components_settings):
registry.library.tag(noop)
captured = {}
@ -256,25 +260,22 @@ class DynamicExprTests(BaseTestCase):
)
# Check that variables passed to the component are of correct type
self.assertEqual(captured["pos_var1"], "")
self.assertEqual(captured["pos_var2"], " abc")
self.assertEqual(captured["bool_var"], "")
self.assertEqual(captured["list_var"], " ")
assert captured["pos_var1"] == ""
assert captured["pos_var2"] == " abc"
assert captured["bool_var"] == ""
assert captured["list_var"] == " "
# NOTE: This is whitespace-sensitive test, so we check exact output
self.assertEqual(
rendered.strip(),
(
"<!-- _RENDERED SimpleComponent_e258c0,a1bc3f,, -->\n"
' <div data-djc-id-a1bc3f=""></div>\n'
' <div data-djc-id-a1bc3f=""> abc</div>\n'
' <div data-djc-id-a1bc3f=""></div>\n'
' <div data-djc-id-a1bc3f=""> </div>'
),
assert rendered.strip() == (
"<!-- _RENDERED SimpleComponent_6f07b3,a1bc3f,, -->\n"
' <div data-djc-id-a1bc3f=""></div>\n'
' <div data-djc-id-a1bc3f=""> abc</div>\n'
' <div data-djc-id-a1bc3f=""></div>\n'
' <div data-djc-id-a1bc3f=""> </div>'
)
@parametrize_context_behavior(["django", "isolated"])
def test_mixed_in_template(self):
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_mixed_in_template(self, components_settings):
registry.library.tag(noop)
captured = {}
@ -336,25 +337,22 @@ class DynamicExprTests(BaseTestCase):
)
# Check that variables passed to the component are of correct type
self.assertEqual(captured["bool_var"], " True ")
self.assertEqual(captured["dict_var"], " {'a': 3} ")
self.assertEqual(captured["list_var"], " [{'a': 1}, {'a': 2}] ")
assert captured["bool_var"] == " True "
assert captured["dict_var"] == " {'a': 3} "
assert captured["list_var"] == " [{'a': 1}, {'a': 2}] "
# NOTE: This is whitespace-sensitive test, so we check exact output
self.assertEqual(
rendered.strip(),
(
"<!-- _RENDERED SimpleComponent_6c8e94,a1bc3f,, -->\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=""> True </div>\n'
' <div data-djc-id-a1bc3f=""> [{\'a\': 1}, {\'a\': 2}] </div>\n'
' <div data-djc-id-a1bc3f=""> {\'a\': 3} </div>'
),
assert rendered.strip() == (
"<!-- _RENDERED SimpleComponent_85c7eb,a1bc3f,, -->\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=""> True </div>\n'
' <div data-djc-id-a1bc3f=""> [{\'a\': 1}, {\'a\': 2}] </div>\n'
' <div data-djc-id-a1bc3f=""> {\'a\': 3} </div>'
)
@parametrize_context_behavior(["django", "isolated"])
def test_ignores_invalid_tag(self):
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_ignores_invalid_tag(self, components_settings):
registry.library.tag(noop)
@register("test")
@ -390,7 +388,7 @@ class DynamicExprTests(BaseTestCase):
Context({"is_active": True}),
)
self.assertHTMLEqual(
assertHTMLEqual(
rendered,
"""
<!-- _RENDERED SimpleComponent_c7a5c3,a1bc3f,, -->
@ -400,8 +398,8 @@ class DynamicExprTests(BaseTestCase):
""",
)
@parametrize_context_behavior(["django", "isolated"])
def test_nested_in_template(self):
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_nested_in_template(self, components_settings):
registry.library.tag(noop)
@register("test")
@ -442,7 +440,7 @@ class DynamicExprTests(BaseTestCase):
),
)
self.assertHTMLEqual(
assertHTMLEqual(
rendered,
"""
<!-- _RENDERED SimpleComponent_5c8766,a1bc41,, -->
@ -456,9 +454,9 @@ class DynamicExprTests(BaseTestCase):
)
class SpreadOperatorTests(BaseTestCase):
@parametrize_context_behavior(["django", "isolated"])
def test_component(self):
class TestSpreadOperator:
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_component(self, components_settings):
captured = {}
@register("test")
@ -513,12 +511,12 @@ class SpreadOperatorTests(BaseTestCase):
)
# Check that variables passed to the component are of correct type
self.assertEqual(captured["attrs"], {"@click": "() => {}", "style": "height: 20px"})
self.assertEqual(captured["items"], [1, 2, 3])
self.assertEqual(captured["a"], 1)
self.assertEqual(captured["x"], 123)
assert captured["attrs"] == {"@click": "() => {}", "style": "height: 20px"}
assert captured["items"] == [1, 2, 3]
assert captured["a"] == 1
assert captured["x"] == 123
self.assertHTMLEqual(
assertHTMLEqual(
rendered,
"""
<div data-djc-id-a1bc3f>LoREM</div>
@ -529,8 +527,8 @@ class SpreadOperatorTests(BaseTestCase):
""",
)
@parametrize_context_behavior(["django", "isolated"])
def test_slot(self):
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_slot(self, components_settings):
@register("test")
class SimpleComponent(Component):
def get_context_data(self):
@ -559,15 +557,15 @@ class SpreadOperatorTests(BaseTestCase):
template = Template(template_str)
rendered = template.render(Context({}))
self.assertHTMLEqual(
assertHTMLEqual(
rendered,
"""
{'items': [1, 2, 3], 'a': 1, 'x': 123, 'attrs': {'@click': '() =&gt; {}', 'style': 'height: 20px'}}
""",
)
@parametrize_context_behavior(["django", "isolated"])
def test_fill(self):
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_fill(self, components_settings):
@register("test")
class SimpleComponent(Component):
def get_context_data(self):
@ -608,7 +606,7 @@ class SpreadOperatorTests(BaseTestCase):
),
)
self.assertHTMLEqual(
assertHTMLEqual(
rendered,
"""
{'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"])
def test_provide(self):
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_provide(self, components_settings):
@register("test")
class SimpleComponent(Component):
def get_context_data(self):
@ -655,7 +653,7 @@ class SpreadOperatorTests(BaseTestCase):
),
)
self.assertHTMLEqual(
assertHTMLEqual(
rendered,
"""
<div data-djc-id-a1bc41>{'@click': '() =&gt; {}', 'style': 'height: 20px'}</div>
@ -664,8 +662,8 @@ class SpreadOperatorTests(BaseTestCase):
""",
)
@parametrize_context_behavior(["django", "isolated"])
def test_html_attrs(self):
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_html_attrs(self, components_settings):
template_str: types.django_html = """
{% load component_tags %}
<div {% html_attrs defaults:test="hi" ...my_dict attrs:lol="123" %}>
@ -683,15 +681,15 @@ class SpreadOperatorTests(BaseTestCase):
}
),
)
self.assertHTMLEqual(
assertHTMLEqual(
rendered,
"""
<div test="hi" class="my-class button" style="height: 20px" lol="123">
""",
)
@parametrize_context_behavior(["django", "isolated"])
def test_later_spreads_do_not_overwrite_earlier(self):
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_later_spreads_do_not_overwrite_earlier(self, components_settings):
@register("test")
class SimpleComponent(Component):
def get_context_data(
@ -737,7 +735,7 @@ class SpreadOperatorTests(BaseTestCase):
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)
# But, similarly to python, we can merge multiple **kwargs by instead
@ -759,7 +757,7 @@ class SpreadOperatorTests(BaseTestCase):
template2 = Template(template_str2)
rendered2 = template2.render(context)
self.assertHTMLEqual(
assertHTMLEqual(
rendered2,
"""
<div data-djc-id-a1bc40>{'@click': '() =&gt; {}', 'style': 'OVERWRITTEN'}</div>
@ -769,8 +767,8 @@ class SpreadOperatorTests(BaseTestCase):
""",
)
@parametrize_context_behavior(["django", "isolated"])
def test_raises_on_missing_value(self):
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_raises_on_missing_value(self, components_settings):
@register("test")
class SimpleComponent(Component):
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)
@parametrize_context_behavior(["django", "isolated"])
def test_spread_list_and_iterables(self):
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_spread_list_and_iterables(self, components_settings):
captured = None
@register("test")
@ -821,16 +819,13 @@ class SpreadOperatorTests(BaseTestCase):
template.render(context)
self.assertEqual(
captured,
(
("a", "b", "c", 1, 2, 3),
{},
),
assert captured == (
("a", "b", "c", 1, 2, 3),
{},
)
@parametrize_context_behavior(["django", "isolated"])
def test_raises_on_non_dict(self):
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_raises_on_non_dict(self, components_settings):
@register("test")
class SimpleComponent(Component):
pass
@ -847,11 +842,15 @@ class SpreadOperatorTests(BaseTestCase):
template = Template(template_str)
# 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}))
class AggregateKwargsTest(BaseTestCase):
@djc_test
class TestAggregateKwargs:
def test_aggregate_kwargs(self):
captured = None
@ -879,31 +878,28 @@ class AggregateKwargsTest(BaseTestCase):
template = Template(template_str)
template.render(Context({"class_var": "padding-top-8", "four": 4}))
self.assertEqual(
captured,
(
(),
{
"attrs": {
"@click.stop": "dispatch('click_event')",
"x-data": "{hello: 'world'}",
"class": "padding-top-8",
":placeholder": "No text",
},
"my_dict": {"one": 2},
"three": 4,
assert captured == (
(),
{
"attrs": {
"@click.stop": "dispatch('click_event')",
"x-data": "{hello: 'world'}",
"class": "padding-top-8",
":placeholder": "No text",
},
),
"my_dict": {"one": 2},
"three": 4,
},
)
def is_aggregate_key(self):
self.assertEqual(is_aggregate_key(""), False)
self.assertEqual(is_aggregate_key(" "), False)
self.assertEqual(is_aggregate_key(" : "), False)
self.assertEqual(is_aggregate_key("attrs"), False)
self.assertEqual(is_aggregate_key(":attrs"), False)
self.assertEqual(is_aggregate_key(" :attrs "), False)
self.assertEqual(is_aggregate_key("attrs:"), False)
self.assertEqual(is_aggregate_key(":attrs:"), False)
self.assertEqual(is_aggregate_key("at:trs"), True)
self.assertEqual(is_aggregate_key(":at:trs"), False)
def test_is_aggregate_key(self):
assert not is_aggregate_key("")
assert not is_aggregate_key(" ")
assert not is_aggregate_key(" : ")
assert not is_aggregate_key("attrs")
assert not is_aggregate_key(":attrs")
assert not is_aggregate_key(" :attrs ")
assert not is_aggregate_key("attrs:")
assert not is_aggregate_key(":attrs:")
assert is_aggregate_key("at:trs")
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.http import HttpResponse
from django.template import Context, Template
from django.test import Client
from django.test import Client, SimpleTestCase
from django.urls import path
from django_components import Component, ComponentView, register, types
from django_components.urls import urlpatterns as dc_urlpatterns
from .django_test_setup import setup_test_config
from .testutils import BaseTestCase, parametrize_context_behavior
from django_components.testing import djc_test
from .testutils import setup_test_config
setup_test_config({"autodiscover": False})
@ -29,7 +29,8 @@ class CustomClient(Client):
super().__init__(*args, **kwargs)
class TestComponentAsView(BaseTestCase):
@djc_test
class TestComponentAsView(SimpleTestCase):
def test_render_component_from_template(self):
@register("testcomponent")
class MockComponentRequest(Component):
@ -183,7 +184,6 @@ class TestComponentAsView(BaseTestCase):
response.content.decode(),
)
@parametrize_context_behavior(["django", "isolated"])
def test_replace_slot_in_view(self):
class MockComponentSlot(Component):
template = """
@ -212,7 +212,6 @@ class TestComponentAsView(BaseTestCase):
response.content,
)
@parametrize_context_behavior(["django", "isolated"])
def test_replace_slot_in_view_with_insecure_content(self):
class MockInsecureComponentSlot(Component):
template = """
@ -234,7 +233,6 @@ class TestComponentAsView(BaseTestCase):
response.content,
)
@parametrize_context_behavior(["django", "isolated"])
def test_replace_context_in_view(self):
class TestComponent(Component):
template = """
@ -255,7 +253,6 @@ class TestComponentAsView(BaseTestCase):
response.content,
)
@parametrize_context_behavior(["django", "isolated"])
def test_replace_context_in_view_with_insecure_content(self):
class MockInsecureComponentContext(Component):
template = """

View file

@ -2,9 +2,10 @@ import re
from pathlib import Path
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})
@ -76,14 +77,16 @@ COMPONENTS = {
class StaticFilesFinderTests(SimpleTestCase):
@override_settings(
**common_settings,
COMPONENTS=COMPONENTS,
STATICFILES_FINDERS=[
# Default finders
"django.contrib.staticfiles.finders.FileSystemFinder",
"django.contrib.staticfiles.finders.AppDirectoriesFinder",
],
@djc_test(
django_settings={
**common_settings,
"STATICFILES_FINDERS": [
# Default finders
"django.contrib.staticfiles.finders.FileSystemFinder",
"django.contrib.staticfiles.finders.AppDirectoriesFinder",
],
},
components_settings=COMPONENTS,
)
def test_python_and_html_included(self):
collected = do_collect()
@ -97,16 +100,18 @@ class StaticFilesFinderTests(SimpleTestCase):
self.assertListEqual(collected["unmodified"], [])
self.assertListEqual(collected["post_processed"], [])
@override_settings(
**common_settings,
COMPONENTS=COMPONENTS,
STATICFILES_FINDERS=[
# Default finders
"django.contrib.staticfiles.finders.FileSystemFinder",
"django.contrib.staticfiles.finders.AppDirectoriesFinder",
# Django components
"django_components.finders.ComponentsFileSystemFinder",
],
@djc_test(
django_settings={
**common_settings,
"STATICFILES_FINDERS": [
# Default finders
"django.contrib.staticfiles.finders.FileSystemFinder",
"django.contrib.staticfiles.finders.AppDirectoriesFinder",
# Django components
"django_components.finders.ComponentsFileSystemFinder",
],
},
components_settings=COMPONENTS,
)
def test_python_and_html_omitted(self):
collected = do_collect()
@ -120,22 +125,24 @@ class StaticFilesFinderTests(SimpleTestCase):
self.assertListEqual(collected["unmodified"], [])
self.assertListEqual(collected["post_processed"], [])
@override_settings(
**common_settings,
COMPONENTS={
@djc_test(
django_settings={
**common_settings,
"STATICFILES_FINDERS": [
# Default finders
"django.contrib.staticfiles.finders.FileSystemFinder",
"django.contrib.staticfiles.finders.AppDirectoriesFinder",
# Django components
"django_components.finders.ComponentsFileSystemFinder",
],
},
components_settings={
**COMPONENTS,
"static_files_allowed": [
".js",
],
"static_files_forbidden": [],
},
STATICFILES_FINDERS=[
# Default finders
"django.contrib.staticfiles.finders.FileSystemFinder",
"django.contrib.staticfiles.finders.AppDirectoriesFinder",
# Django components
"django_components.finders.ComponentsFileSystemFinder",
],
)
def test_set_static_files_allowed(self):
collected = do_collect()
@ -149,9 +156,18 @@ class StaticFilesFinderTests(SimpleTestCase):
self.assertListEqual(collected["unmodified"], [])
self.assertListEqual(collected["post_processed"], [])
@override_settings(
**common_settings,
COMPONENTS={
@djc_test(
django_settings={
**common_settings,
"STATICFILES_FINDERS": [
# Default finders
"django.contrib.staticfiles.finders.FileSystemFinder",
"django.contrib.staticfiles.finders.AppDirectoriesFinder",
# Django components
"django_components.finders.ComponentsFileSystemFinder",
],
},
components_settings={
**COMPONENTS,
"static_files_allowed": [
re.compile(r".*"),
@ -160,13 +176,6 @@ class StaticFilesFinderTests(SimpleTestCase):
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):
collected = do_collect()
@ -180,9 +189,18 @@ class StaticFilesFinderTests(SimpleTestCase):
self.assertListEqual(collected["unmodified"], [])
self.assertListEqual(collected["post_processed"], [])
@override_settings(
**common_settings,
COMPONENTS={
@djc_test(
django_settings={
**common_settings,
"STATICFILES_FINDERS": [
# Default finders
"django.contrib.staticfiles.finders.FileSystemFinder",
"django.contrib.staticfiles.finders.AppDirectoriesFinder",
# Django components
"django_components.finders.ComponentsFileSystemFinder",
],
},
components_settings={
**COMPONENTS,
"static_files_allowed": [
".js",
@ -192,13 +210,6 @@ class StaticFilesFinderTests(SimpleTestCase):
".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):
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 .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})
# 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
class TestHTMLParser(TestCase):
@djc_test
class TestHTMLParser:
def test_basic_transformation(self):
html = "<div><p>Hello</p></div>"
result, _ = set_html_attributes(html, root_attributes=["data-root"], all_attributes=["data-all"])
self.assertHTMLEqual(
assertHTMLEqual(
result,
"""
<div data-root data-all>
@ -27,7 +29,7 @@ class TestHTMLParser(TestCase):
html = "<div>First</div><span>Second</span>"
result, _ = set_html_attributes(html, root_attributes=["data-root"], all_attributes=["data-all"])
self.assertHTMLEqual(
assertHTMLEqual(
result,
"""
<div data-root data-all>First</div>
@ -81,7 +83,7 @@ class TestHTMLParser(TestCase):
<p data-all data-v-123>&copy; 2024</p>
</footer>
""" # noqa: E501
self.assertHTMLEqual(result, expected)
assertHTMLEqual(result, expected)
def test_void_elements(self):
test_cases = [
@ -93,7 +95,7 @@ class TestHTMLParser(TestCase):
for input_html, expected in test_cases:
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):
html = """
@ -106,7 +108,7 @@ class TestHTMLParser(TestCase):
result, _ = set_html_attributes(html, ["data-root"], ["data-v-123"])
self.assertHTMLEqual(
assertHTMLEqual(
result,
"""
<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")
self.assertHTMLEqual(
assertHTMLEqual(
result,
"""
<div data-id="123" data-root data-v-123>
@ -140,14 +142,14 @@ class TestHTMLParser(TestCase):
)
# Verify attribute capturing
self.assertEqual(len(captured), 3)
assert len(captured) == 3
# 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
self.assertEqual(captured["456"], ["data-v-123"])
self.assertEqual(captured["789"], ["data-v-123"])
assert captured["456"] == ["data-v-123"]
assert captured["789"] == ["data-v-123"]
def test_whitespace_preservation(self):
html = """<div>
@ -161,4 +163,4 @@ class TestHTMLParser(TestCase):
<span data-all=""> Text with spaces </span>
</div>"""
self.assertEqual(result, expected)
assert result == expected

View file

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

View file

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

View file

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

View file

@ -1,38 +1,66 @@
import re
from pathlib import Path
import pytest
from django.test import override_settings
from django_components import ComponentsSettings
from django_components.app_settings import app_settings
from django_components.app_settings import ComponentsSettings, app_settings
from .django_test_setup import setup_test_config
from .testutils import BaseTestCase
from django_components.testing import djc_test
from .testutils import setup_test_config
setup_test_config(components={"autodiscover": False})
class SettingsTestCase(BaseTestCase):
@override_settings(COMPONENTS={"context_behavior": "isolated"})
@djc_test
class TestSettings:
@djc_test(
components_settings={
"context_behavior": "isolated",
},
)
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):
with self.assertRaises(ValueError):
app_settings.CONTEXT_BEHAVIOR
with override_settings(COMPONENTS={"context_behavior": "invalid_value"}):
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):
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):
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):
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):
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_components import Component, register, registry, types
from django_components import Component, registry, types
from .django_test_setup import setup_test_config
from .testutils import BaseTestCase, parametrize_context_behavior
from django_components.testing import djc_test
from .testutils import PARAMETRIZE_CONTEXT_BEHAVIOR, setup_test_config
setup_test_config({"autodiscover": False})
@ -29,51 +29,56 @@ def _get_templates_used_to_render(subject_template, render_context=None):
return templates_used
class TemplateSignalTest(BaseTestCase):
saved_render_method: Callable # Assigned during setup.
def tearDown(self):
super().tearDown()
Template._render = self.saved_render_method
def setUp(self):
"""Emulate Django test instrumentation for TestCase (see setup_test_environment)"""
super().setUp()
def with_template_signal(func):
@wraps(func)
def wrapper(*args, **kwargs):
# Emulate Django test instrumentation for TestCase (see setup_test_environment)
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
registry.clear()
func(*args, **kwargs)
Template._render = original_template_render
return wrapper
@djc_test
class TestTemplateSignal:
class InnerComponent(Component):
template_file = "simple_template.html"
def get_context_data(self, variable, variable2="default"):
return {
"variable": variable,
"variable2": variable2,
}
class Media:
css = "style.css"
js = "script.js"
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
@with_template_signal
def test_template_rendered(self, components_settings):
registry.register("test_component", SlottedComponent)
@register("inner_component")
class SimpleComponent(Component):
template_file = "simple_template.html"
def get_context_data(self, variable, variable2="default"):
return {
"variable": variable,
"variable2": variable2,
}
class Media:
css = "style.css"
js = "script.js"
@parametrize_context_behavior(["django", "isolated"])
def test_template_rendered(self):
registry.register("inner_component", self.InnerComponent)
template_str: types.django_html = """
{% load component_tags %}
{% component 'test_component' %}{% endcomponent %}
"""
template = Template(template_str, name="root")
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"])
def test_template_rendered_nested_components(self):
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
@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 = """
{% load component_tags %}
{% component 'test_component' %}
@ -84,5 +89,5 @@ class TemplateSignalTest(BaseTestCase):
"""
template = Template(template_str, name="root")
templates_used = _get_templates_used_to_render(template)
self.assertIn("slotted_template.html", templates_used)
self.assertIn("simple_template.html", templates_used)
assert "slotted_template.html" in 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 pytest_django.asserts import assertHTMLEqual
from django_components import Component, register, types
from django_components.tag_formatter import ShorthandComponentFormatter
from .django_test_setup import setup_test_config
from .testutils import BaseTestCase, parametrize_context_behavior
from django_components.testing import djc_test
from .testutils import PARAMETRIZE_CONTEXT_BEHAVIOR, setup_test_config
setup_test_config({"autodiscover": False})
@ -43,9 +47,10 @@ def create_validator_tag_formatter(tag_name: str):
return ValidatorTagFormatter()
class ComponentTagTests(BaseTestCase):
@parametrize_context_behavior(["django", "isolated"])
def test_formatter_default_inline(self):
@djc_test
class TestComponentTag:
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_formatter_default_inline(self, components_settings):
@register("simple")
class SimpleComponent(Component):
template: types.django_html = """
@ -64,7 +69,7 @@ class ComponentTagTests(BaseTestCase):
"""
)
rendered = template.render(Context())
self.assertHTMLEqual(
assertHTMLEqual(
rendered,
"""
hello1
@ -75,8 +80,8 @@ class ComponentTagTests(BaseTestCase):
""",
)
@parametrize_context_behavior(["django", "isolated"])
def test_formatter_default_block(self):
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_formatter_default_block(self, components_settings):
@register("simple")
class SimpleComponent(Component):
template: types.django_html = """
@ -97,7 +102,7 @@ class ComponentTagTests(BaseTestCase):
"""
)
rendered = template.render(Context())
self.assertHTMLEqual(
assertHTMLEqual(
rendered,
"""
hello1
@ -108,15 +113,13 @@ class ComponentTagTests(BaseTestCase):
""",
)
@parametrize_context_behavior(
cases=["django", "isolated"],
settings={
"COMPONENTS": {
"tag_formatter": "django_components.component_formatter",
},
@djc_test(
parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR,
components_settings={
"tag_formatter": "django_components.component_formatter",
},
)
def test_formatter_component_inline(self):
def test_formatter_component_inline(self, components_settings):
@register("simple")
class SimpleComponent(Component):
template: types.django_html = """
@ -135,7 +138,7 @@ class ComponentTagTests(BaseTestCase):
"""
)
rendered = template.render(Context())
self.assertHTMLEqual(
assertHTMLEqual(
rendered,
"""
hello1
@ -146,15 +149,13 @@ class ComponentTagTests(BaseTestCase):
""",
)
@parametrize_context_behavior(
cases=["django", "isolated"],
settings={
"COMPONENTS": {
"tag_formatter": "django_components.component_formatter",
},
@djc_test(
parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR,
components_settings={
"tag_formatter": "django_components.component_formatter",
},
)
def test_formatter_component_block(self):
def test_formatter_component_block(self, components_settings):
@register("simple")
class SimpleComponent(Component):
template: types.django_html = """
@ -175,7 +176,7 @@ class ComponentTagTests(BaseTestCase):
"""
)
rendered = template.render(Context())
self.assertHTMLEqual(
assertHTMLEqual(
rendered,
"""
hello1
@ -186,15 +187,13 @@ class ComponentTagTests(BaseTestCase):
""",
)
@parametrize_context_behavior(
cases=["django", "isolated"],
settings={
"COMPONENTS": {
"tag_formatter": "django_components.component_shorthand_formatter",
},
@djc_test(
parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR,
components_settings={
"tag_formatter": "django_components.component_shorthand_formatter",
},
)
def test_formatter_shorthand_inline(self):
def test_formatter_shorthand_inline(self, components_settings):
@register("simple")
class SimpleComponent(Component):
template: types.django_html = """
@ -213,7 +212,7 @@ class ComponentTagTests(BaseTestCase):
"""
)
rendered = template.render(Context())
self.assertHTMLEqual(
assertHTMLEqual(
rendered,
"""
hello1
@ -224,15 +223,13 @@ class ComponentTagTests(BaseTestCase):
""",
)
@parametrize_context_behavior(
cases=["django", "isolated"],
settings={
"COMPONENTS": {
"tag_formatter": "django_components.component_shorthand_formatter",
},
@djc_test(
parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR,
components_settings={
"tag_formatter": "django_components.component_shorthand_formatter",
},
)
def test_formatter_shorthand_block(self):
def test_formatter_shorthand_block(self, components_settings):
@register("simple")
class SimpleComponent(Component):
template: types.django_html = """
@ -253,7 +250,7 @@ class ComponentTagTests(BaseTestCase):
"""
)
rendered = template.render(Context())
self.assertHTMLEqual(
assertHTMLEqual(
rendered,
"""
hello1
@ -264,15 +261,13 @@ class ComponentTagTests(BaseTestCase):
""",
)
@parametrize_context_behavior(
cases=["django", "isolated"],
settings={
"COMPONENTS": {
"tag_formatter": SlashEndTagFormatter(),
},
@djc_test(
parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR,
components_settings={
"tag_formatter": SlashEndTagFormatter(),
},
)
def test_forward_slash_in_end_tag(self):
def test_forward_slash_in_end_tag(self, components_settings):
@register("simple")
class SimpleComponent(Component):
template: types.django_html = """
@ -293,7 +288,7 @@ class ComponentTagTests(BaseTestCase):
"""
)
rendered = template.render(Context())
self.assertHTMLEqual(
assertHTMLEqual(
rendered,
"""
hello1
@ -304,15 +299,13 @@ class ComponentTagTests(BaseTestCase):
""",
)
@parametrize_context_behavior(
cases=["django", "isolated"],
settings={
"COMPONENTS": {
"tag_formatter": ShorthandComponentFormatter(),
},
@djc_test(
parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR,
components_settings={
"tag_formatter": ShorthandComponentFormatter(),
},
)
def test_import_formatter_by_value(self):
def test_import_formatter_by_value(self, components_settings):
@register("simple")
class SimpleComponent(Component):
template: types.django_html = """
@ -331,7 +324,7 @@ class ComponentTagTests(BaseTestCase):
"""
)
rendered = template.render(Context())
self.assertHTMLEqual(
assertHTMLEqual(
rendered,
"""
<div data-djc-id-a1bc3f>
@ -340,34 +333,32 @@ class ComponentTagTests(BaseTestCase):
""",
)
@parametrize_context_behavior(
cases=["django", "isolated"],
settings={
"COMPONENTS": {
"tag_formatter": MultiwordStartTagFormatter(),
},
@djc_test(
parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR,
components_settings={
"tag_formatter": MultiwordStartTagFormatter(),
},
)
def test_raises_on_invalid_start_tag(self):
with self.assertRaisesMessage(
ValueError, "MultiwordStartTagFormatter returned an invalid tag for start_tag: 'simple comp'"
def test_raises_on_invalid_start_tag(self, components_settings):
with pytest.raises(
ValueError,
match=re.escape("MultiwordStartTagFormatter returned an invalid tag for start_tag: 'simple comp'"),
):
@register("simple")
class SimpleComponent(Component):
template = """{% load component_tags %}"""
@parametrize_context_behavior(
cases=["django", "isolated"],
settings={
"COMPONENTS": {
"tag_formatter": MultiwordBlockEndTagFormatter(),
},
@djc_test(
parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR,
components_settings={
"tag_formatter": MultiwordBlockEndTagFormatter(),
},
)
def test_raises_on_invalid_block_end_tag(self):
with self.assertRaisesMessage(
ValueError, "MultiwordBlockEndTagFormatter returned an invalid tag for end_tag: 'end simple'"
def test_raises_on_invalid_block_end_tag(self, components_settings):
with pytest.raises(
ValueError,
match=re.escape("MultiwordBlockEndTagFormatter returned an invalid tag for end_tag: 'end simple'"),
):
@register("simple")
@ -388,15 +379,13 @@ class ComponentTagTests(BaseTestCase):
"""
)
@parametrize_context_behavior(
cases=["django", "isolated"],
settings={
"COMPONENTS": {
"tag_formatter": create_validator_tag_formatter("simple"),
},
@djc_test(
parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR,
components_settings={
"tag_formatter": create_validator_tag_formatter("simple"),
},
)
def test_method_args(self):
def test_method_args(self, components_settings):
@register("simple")
class SimpleComponent(Component):
template: types.django_html = """
@ -415,7 +404,7 @@ class ComponentTagTests(BaseTestCase):
"""
)
rendered = template.render(Context())
self.assertHTMLEqual(
assertHTMLEqual(
rendered,
"""
hello1
@ -435,7 +424,7 @@ class ComponentTagTests(BaseTestCase):
"""
)
rendered = template.render(Context())
self.assertHTMLEqual(
assertHTMLEqual(
rendered,
"""
hello1

View file

@ -1,5 +1,7 @@
import re
from unittest import skip
import pytest
from django.template import Context, Template, TemplateSyntaxError
from django.template.base import Parser
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.util.tag_parser import TagAttr, TagValue, TagValuePart, TagValueStruct, parse_tag
from .django_test_setup import setup_test_config
from .testutils import BaseTestCase
from django_components.testing import djc_test
from .testutils import setup_test_config
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):
_, attrs = parse_tag("component 'my_comp' key=val key2='val2 two' ", None)
@ -99,16 +102,13 @@ class TagParserTests(BaseTestCase):
),
]
self.assertEqual(attrs, expected_attrs)
self.assertEqual(
[a.serialize() for a in attrs],
[
"component",
"'my_comp'",
"key=val",
"key2='val2 two'",
],
)
assert attrs == expected_attrs
assert [a.serialize() for a in attrs] == [
"component",
"'my_comp'",
"key=val",
"key2='val2 two'",
]
def test_nested_quotes(self):
_, 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)
self.assertEqual(
[a.serialize() for a in attrs],
[
"component",
"'my_comp'",
"key=val",
"key2='val2 \"two\"'",
'text="organisation\'s"',
],
)
assert attrs == expected_attrs
assert [a.serialize() for a in attrs] == [
"component",
"'my_comp'",
"key=val",
"key2='val2 \"two\"'",
'text="organisation\'s"',
]
def test_trailing_quote_single(self):
_, 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)
self.assertEqual(
[a.serialize() for a in attrs],
[
"component",
"'my_comp'",
"key=val",
"key2='val2 \"two\"'",
'text="organisation\'s"',
"'abc",
],
)
assert attrs == expected_attrs
assert [a.serialize() for a in attrs] == [
"component",
"'my_comp'",
"key=val",
"key2='val2 \"two\"'",
'text="organisation\'s"',
"'abc",
]
def test_trailing_quote_double(self):
_, 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)
self.assertEqual(
[a.serialize() for a in attrs],
[
"component",
'"my_comp"',
"key=val",
"key2=\"val2 'two'\"",
"text='organisation\"s'",
'"abc',
],
)
assert attrs == expected_attrs
assert [a.serialize() for a in attrs] == [
"component",
'"my_comp"',
"key=val",
"key2=\"val2 'two'\"",
"text='organisation\"s'",
'"abc',
]
def test_trailing_quote_as_value_single(self):
_, attrs = parse_tag(
@ -582,18 +573,15 @@ class TagParserTests(BaseTestCase):
),
]
self.assertEqual(attrs, expected_attrs)
self.assertEqual(
[a.serialize() for a in attrs],
[
"component",
"'my_comp'",
"key=val",
"key2='val2 \"two\"'",
'text="organisation\'s"',
"value='abc",
],
)
assert attrs == expected_attrs
assert [a.serialize() for a in attrs] == [
"component",
"'my_comp'",
"key=val",
"key2='val2 \"two\"'",
'text="organisation\'s"',
"value='abc",
]
def test_trailing_quote_as_value_double(self):
_, attrs = parse_tag(
@ -710,18 +698,15 @@ class TagParserTests(BaseTestCase):
),
]
self.assertEqual(attrs, expected_attrs)
self.assertEqual(
[a.serialize() for a in attrs],
[
"component",
'"my_comp"',
"key=val",
"key2=\"val2 'two'\"",
"text='organisation\"s'",
'value="abc',
],
)
assert attrs == expected_attrs
assert [a.serialize() for a in attrs] == [
"component",
'"my_comp"',
"key=val",
"key2=\"val2 'two'\"",
"text='organisation\"s'",
'value="abc',
]
def test_translation(self):
_, attrs = parse_tag('component "my_comp" _("one") key=_("two")', None)
@ -795,16 +780,13 @@ class TagParserTests(BaseTestCase):
),
]
self.assertEqual(attrs, expected_attrs)
self.assertEqual(
[a.serialize() for a in attrs],
[
"component",
'"my_comp"',
'_("one")',
'key=_("two")',
],
)
assert attrs == expected_attrs
assert [a.serialize() for a in attrs] == [
"component",
'"my_comp"',
'_("one")',
'key=_("two")',
]
def test_tag_parser_filters(self):
_, attrs = parse_tag(
@ -908,17 +890,14 @@ class TagParserTests(BaseTestCase):
),
]
self.assertEqual(attrs, expected_attrs)
self.assertEqual(
[a.serialize() for a in attrs],
[
"component",
'"my_comp"',
"value|lower",
'key=val|yesno:"yes,no"',
'key2=val2|default:"N/A"|upper',
],
)
assert attrs == expected_attrs
assert [a.serialize() for a in attrs] == [
"component",
'"my_comp"',
"value|lower",
'key=val|yesno:"yes,no"',
'key2=val2|default:"N/A"|upper',
]
def test_translation_whitespace(self):
_, attrs = parse_tag('component value=_( "test" )', None)
@ -961,7 +940,7 @@ class TagParserTests(BaseTestCase):
start_index=10,
),
]
self.assertEqual(attrs, expected_attrs)
assert attrs == expected_attrs
def test_filter_whitespace(self):
_, 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):
with self.assertRaisesMessage(
with pytest.raises(
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)
@ -1097,7 +1076,7 @@ class TagParserTests(BaseTestCase):
),
]
self.assertEqual(attrs, expected_attrs)
assert attrs == expected_attrs
def test_dict_trailing_comma(self):
_, 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):
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)
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)
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)
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):
_, 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
# 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.
def test_colon_in_dictionary_keys(self):
with self.assertRaisesMessage(
TemplateSyntaxError, "Filter argument (':arg') must follow a filter ('|filter')"
with pytest.raises(
TemplateSyntaxError,
match=re.escape("Filter argument (':arg') must follow a filter ('|filter')"),
):
_, 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):
_, 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):
_, attrs = parse_tag(
@ -1552,16 +1541,13 @@ class TagParserTests(BaseTestCase):
),
]
self.assertEqual(attrs, expected_attrs)
self.assertEqual(
[a.serialize() for a in attrs],
[
"component",
"nums=[1, 2|add:3, *spread]",
'items=["a"|upper, \'b\'|lower, c|default:"d"]',
'mixed=[1, [*nested], {"key": "val"}]',
],
)
assert attrs == expected_attrs
assert [a.serialize() for a in attrs] == [
"component",
"nums=[1, 2|add:3, *spread]",
'items=["a"|upper, \'b\'|lower, c|default:"d"]',
'mixed=[1, [*nested], {"key": "val"}]',
]
def test_dicts_complex(self):
_, attrs = parse_tag(
@ -1728,16 +1714,13 @@ class TagParserTests(BaseTestCase):
),
]
self.assertEqual(attrs, expected_attrs)
self.assertEqual(
[a.serialize() for a in attrs],
[
"component",
'simple={"a": 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"}',
],
)
assert attrs == expected_attrs
assert [a.serialize() for a in attrs] == [
"component",
'simple={"a": 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"}',
]
def test_mixed_complex(self):
_, attrs = parse_tag(
@ -2053,42 +2036,39 @@ class TagParserTests(BaseTestCase):
),
]
self.assertEqual(attrs, expected_attrs)
self.assertEqual(
[a.serialize() for a in attrs],
[
"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
],
)
assert attrs == expected_attrs
assert [a.serialize() for a in attrs] == [
"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
]
# Test that spread operator cannot be used as dictionary value
def test_spread_as_dictionary_value(self):
with self.assertRaisesMessage(
with pytest.raises(
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)
def test_spread_with_colon_interpreted_as_key(self):
with self.assertRaisesMessage(
with pytest.raises(
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)
def test_spread_in_filter_position(self):
with self.assertRaisesMessage(
with pytest.raises(
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)
def test_spread_whitespace(self):
# NOTE: Separating `...` from its variable is NOT valid, and will result in error.
with self.assertRaisesMessage(
with pytest.raises(
TemplateSyntaxError,
"Spread syntax '...' is missing a value",
match=re.escape("Spread syntax '...' is missing a value"),
):
_, attrs = parse_tag("component ... attrs", None)
@ -2166,75 +2146,75 @@ class TagParserTests(BaseTestCase):
start_index=38,
),
]
self.assertEqual(attrs, expected_attrs)
assert attrs == expected_attrs
# Test that one cannot use e.g. `...`, `**`, `*` in wrong places
def test_spread_incorrect_syntax(self):
with self.assertRaisesMessage(
with pytest.raises(
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)
with self.assertRaisesMessage(
with pytest.raises(
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)
with self.assertRaisesMessage(
with pytest.raises(
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)
with self.assertRaisesMessage(
with pytest.raises(
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)
with self.assertRaisesMessage(
with pytest.raises(
TemplateSyntaxError,
"Spread syntax '*' found outside of a list",
match=re.escape("Spread syntax '*' found outside of a list"),
):
_, attrs = parse_tag("component *attrs", None)
with self.assertRaisesMessage(
with pytest.raises(
TemplateSyntaxError,
"Spread syntax '**' found outside of a dictionary",
match=re.escape("Spread syntax '**' found outside of a dictionary"),
):
_, attrs = parse_tag("component **attrs", None)
with self.assertRaisesMessage(
with pytest.raises(
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)
with self.assertRaisesMessage(
with pytest.raises(
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)
# Test that one cannot do `key=...{"a": "b"}`
def test_spread_onto_key(self):
with self.assertRaisesMessage(
with pytest.raises(
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)
with self.assertRaisesMessage(
with pytest.raises(
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)
with self.assertRaisesMessage(
with pytest.raises(
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)
@ -2312,15 +2292,12 @@ class TagParserTests(BaseTestCase):
start_index=10,
),
]
self.assertEqual(attrs, expected_attrs)
assert attrs == expected_attrs
self.assertEqual(
[a.serialize() for a in attrs],
[
"component",
'{**{"key": val2}, "key": val1}',
],
)
assert [a.serialize() for a in attrs] == [
"component",
'{**{"key": val2}, "key": val1}',
]
def test_spread_dict_literal_as_attribute(self):
_, attrs = parse_tag('component ...{"key": val2}', None)
@ -2366,15 +2343,12 @@ class TagParserTests(BaseTestCase):
start_index=10,
),
]
self.assertEqual(attrs, expected_attrs)
assert attrs == expected_attrs
self.assertEqual(
[a.serialize() for a in attrs],
[
"component",
'...{"key": val2}',
],
)
assert [a.serialize() for a in attrs] == [
"component",
'...{"key": val2}',
]
def test_spread_list_literal_nested(self):
_, attrs = parse_tag("component [ *[val1], val2 ]", None)
@ -2436,15 +2410,12 @@ class TagParserTests(BaseTestCase):
start_index=10,
),
]
self.assertEqual(attrs, expected_attrs)
assert attrs == expected_attrs
self.assertEqual(
[a.serialize() for a in attrs],
[
"component",
"[*[val1], val2]",
],
)
assert [a.serialize() for a in attrs] == [
"component",
"[*[val1], val2]",
]
def test_spread_list_literal_as_attribute(self):
_, attrs = parse_tag("component ...[val1]", None)
@ -2487,15 +2458,12 @@ class TagParserTests(BaseTestCase):
start_index=10,
),
]
self.assertEqual(attrs, expected_attrs)
assert attrs == expected_attrs
self.assertEqual(
[a.serialize() for a in attrs],
[
"component",
"...[val1]",
],
)
assert [a.serialize() for a in attrs] == [
"component",
"...[val1]",
]
def test_dynamic_expressions(self):
_, attrs = parse_tag("component '{% lorem w 4 %}'", None)
@ -2540,7 +2508,7 @@ class TagParserTests(BaseTestCase):
start_index=10,
),
]
self.assertEqual(attrs, expected_attrs)
assert attrs == expected_attrs
def test_dynamic_expressions_in_dict(self):
_, attrs = parse_tag('component { "key": "{% lorem w 4 %}" }', None)
@ -2588,7 +2556,7 @@ class TagParserTests(BaseTestCase):
start_index=10,
),
]
self.assertEqual(attrs, expected_attrs)
assert attrs == expected_attrs
def test_dynamic_expressions_in_list(self):
_, attrs = parse_tag("component [ '{% lorem w 4 %}' ]", None)
@ -2633,49 +2601,50 @@ class TagParserTests(BaseTestCase):
start_index=10,
),
]
self.assertEqual(attrs, expected_attrs)
assert attrs == expected_attrs
class ResolverTests(BaseTestCase):
@djc_test
class TestResolver:
def test_resolve_simple(self):
_, attrs = parse_tag("123", None)
resolved = attrs[0].value.resolve(Context())
self.assertEqual(resolved, 123)
assert resolved == 123
_, attrs = parse_tag("'123'", None)
resolved = attrs[0].value.resolve(Context())
self.assertEqual(resolved, "123")
assert resolved == "123"
_, attrs = parse_tag("abc", None)
resolved = attrs[0].value.resolve(Context({"abc": "foo"}))
self.assertEqual(resolved, "foo")
assert resolved == "foo"
def test_resolve_list(self):
_, attrs = parse_tag("[1, 2, 3,]", None)
resolved = attrs[0].value.resolve(Context())
self.assertEqual(resolved, [1, 2, 3])
assert resolved == [1, 2, 3]
def test_resolve_list_with_spread(self):
_, 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"}))
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):
_, attrs = parse_tag('{"a": 1, "b": 2,}', None)
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):
_, attrs = parse_tag('{ **{"key": val2, **{ val3: val4 }, "key3": val4 } }', None)
context = Context({"val2": "foo", "val3": "bar", "val4": "baz"})
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):
parser = _get_parser()
_, attrs = parse_tag("'{% lorem 4 w %}'", parser)
resolved = attrs[0].value.resolve(Context())
self.assertEqual(resolved, "lorem ipsum dolor sit")
assert resolved == "lorem ipsum dolor sit"
def test_resolve_complex(self):
parser = _get_parser()
@ -2718,21 +2687,18 @@ class ResolverTests(BaseTestCase):
)
resolved = attrs[0].value.resolve(context)
self.assertEqual(
resolved,
{
"items": [3, {"X": 5}, "foo", "bar"],
"nested": {
"a": [3, 1, 2, 3, "l", "o", "r", "e", "m"],
"b": {
"x": ["b", "a", "z"],
"lorem ipsum": "lorem ipsum dolor",
},
assert resolved == {
"items": [3, {"X": 5}, "foo", "bar"],
"nested": {
"a": [3, 1, 2, 3, "l", "o", "r", "e", "m"],
"b": {
"x": ["b", "a", "z"],
"lorem ipsum": "lorem ipsum dolor",
},
"a": "b",
"key": "VALUE",
},
)
"a": "b",
"key": "VALUE",
}
@skip("TODO: Enable once template parsing is fixed by us")
def test_resolve_complex_as_component(self):
@ -2785,21 +2751,18 @@ class ResolverTests(BaseTestCase):
)
)
self.assertEqual(
captured,
{
"items": [3, {"X": 5}, "foo", "bar"],
"nested": {
"a": [3, 1, 2, 3, "l", "o", "r", "e", "m"],
"b": {
"x": ["b", "a", "z"],
"lorem ipsum": "lorem ipsum dolor",
},
assert captured == {
"items": [3, {"X": 5}, "foo", "bar"],
"nested": {
"a": [3, 1, 2, 3, "l", "o", "r", "e", "m"],
"b": {
"x": ["b", "a", "z"],
"lorem ipsum": "lorem ipsum dolor",
},
"a": "b",
"key": "VALUE",
},
)
"a": "b",
"key": "VALUE",
}
def test_component_args_kwargs(self):
captured = None
@ -2820,7 +2783,7 @@ class ResolverTests(BaseTestCase):
"""
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):
captured = None
@ -2841,16 +2804,13 @@ class ResolverTests(BaseTestCase):
"""
Template(template_str).render(Context({"date": 2024, "bzz": "fzz"}))
self.assertEqual(
captured,
(
tuple([]),
{
"date": 2024,
"@lol": 2,
"na-me": "fzz",
"@event": {"na-me.mod": "fzz"},
"#my-id": True,
},
),
assert captured == (
tuple([]),
{
"date": 2024,
"@lol": 2,
"na-me": "fzz",
"@event": {"na-me.mod": "fzz"},
"#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_test_setup import setup_test_config
from .testutils import BaseTestCase
from django_components.testing import djc_test
from .testutils import setup_test_config
setup_test_config({"autodiscover": False})
class TemplateCacheTest(BaseTestCase):
@djc_test
class TestTemplateCache:
def test_cached_template(self):
template_1 = cached_template("Variable: <strong>{{ variable }}</strong>")
template_1._test_id = "123"
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):
class MyTemplate(Template):
pass
template = cached_template("Variable: <strong>{{ variable }}</strong>", MyTemplate)
self.assertIsInstance(template, MyTemplate)
assert isinstance(template, MyTemplate)
def test_component_template_is_cached(self):
class SimpleComponent(Component):
@ -42,4 +43,4 @@ class TemplateCacheTest(BaseTestCase):
template_1._test_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.base import Template, Token, TokenType
from pytest_django.asserts import assertHTMLEqual
from django_components import Component, register, types
from django_components.util.template_parser import parse_template
from .django_test_setup import setup_test_config
from .testutils import BaseTestCase
from django_components.testing import djc_test
from .testutils import setup_test_config
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):
tokens = parse_template("Hello world")
@ -28,7 +30,7 @@ class TemplateParserTests(BaseTestCase):
(TokenType.TEXT, "Hello world", (0, 11), 1),
]
self.assertEqual(token_tuples, expected_tokens)
assert token_tuples == expected_tokens
def test_template_variable(self):
tokens = parse_template("Hello {{ name }}")
@ -39,7 +41,7 @@ class TemplateParserTests(BaseTestCase):
(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
def test_template_variable_unterminated(self):
@ -50,7 +52,7 @@ class TemplateParserTests(BaseTestCase):
(TokenType.TEXT, "Hello {{ name", (0, 13), 1),
]
self.assertEqual(token_tuples, expected_tokens)
assert token_tuples == expected_tokens
def test_template_tag(self):
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),
]
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
def test_template_tag_unterminated(self):
@ -71,7 +73,7 @@ class TemplateParserTests(BaseTestCase):
(TokenType.TEXT, "{% if true", (0, 10), 1),
]
self.assertEqual(token_tuples, expected_tokens)
assert token_tuples == expected_tokens
def test_template_comment(self):
tokens = parse_template("Hello{# this is a comment #}World")
@ -83,7 +85,7 @@ class TemplateParserTests(BaseTestCase):
(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
def test_template_comment_unterminated(self):
@ -94,7 +96,7 @@ class TemplateParserTests(BaseTestCase):
(TokenType.TEXT, "{# comment", (0, 10), 1),
]
self.assertEqual(token_tuples, expected_tokens)
assert token_tuples == expected_tokens
def test_template_verbatim(self):
tokens = parse_template(
@ -115,7 +117,7 @@ class TemplateParserTests(BaseTestCase):
(TokenType.BLOCK, "endverbatim", (107, 124), 4),
]
self.assertEqual(token_tuples, expected_tokens)
assert token_tuples == expected_tokens
def test_template_verbatim_with_name(self):
tokens = parse_template(
@ -142,7 +144,7 @@ class TemplateParserTests(BaseTestCase):
(TokenType.BLOCK, "endverbatim myblock", (184, 209), 6),
]
self.assertEqual(token_tuples, expected_tokens)
assert token_tuples == expected_tokens
def test_template_nested_tags(self):
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),
]
self.assertEqual(token_tuples, expected_tokens)
assert token_tuples == expected_tokens
def test_brackets_and_percent_in_text(self):
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),
]
self.assertEqual(token_tuples, expected_tokens)
assert token_tuples == expected_tokens
def test_template_mixed(self):
tokens = parse_template(
@ -201,7 +203,7 @@ class TemplateParserTests(BaseTestCase):
(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
def test_component_mixed(self):
@ -234,7 +236,7 @@ class TemplateParserTests(BaseTestCase):
template = Template(template_str)
rendered = template.render(Context({"name": "John", "show_greeting": True, "var_a": 2}))
self.assertHTMLEqual(
assertHTMLEqual(
rendered,
"""
<div>

View file

@ -1,11 +1,11 @@
"""Catch-all for tests that use template tags and don't fit other files"""
from django.template import Context, Template
from pytest_django.asserts import assertHTMLEqual
from django_components import Component, register, registry, types
from .django_test_setup import setup_test_config
from .testutils import BaseTestCase, parametrize_context_behavior
from django_components.testing import djc_test
from .testutils import PARAMETRIZE_CONTEXT_BEHAVIOR, setup_test_config
setup_test_config({"autodiscover": False})
@ -19,9 +19,10 @@ class SlottedComponent(Component):
#######################
class MultilineTagsTests(BaseTestCase):
@parametrize_context_behavior(["django", "isolated"])
def test_multiline_tags(self):
@djc_test
class TestMultilineTags:
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_multiline_tags(self, components_settings):
@register("test_component")
class SimpleComponent(Component):
template: types.django_html = """
@ -47,10 +48,11 @@ class MultilineTagsTests(BaseTestCase):
expected = """
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):
template: types.django_html = """
Variable: <strong>{{ var }}</strong>
@ -62,8 +64,8 @@ class NestedTagsTests(BaseTestCase):
}
# See https://github.com/django-components/django-components/discussions/671
@parametrize_context_behavior(["django", "isolated"])
def test_nested_tags(self):
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_nested_tags(self, components_settings):
registry.register("test", self.SimpleComponent)
template: types.django_html = """
@ -74,10 +76,10 @@ class NestedTagsTests(BaseTestCase):
expected = """
Variable: <strong data-djc-id-a1bc3f>lorem</strong>
"""
self.assertHTMLEqual(rendered, expected)
assertHTMLEqual(rendered, expected)
@parametrize_context_behavior(["django", "isolated"])
def test_nested_quote_single(self):
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_nested_quote_single(self, components_settings):
registry.register("test", self.SimpleComponent)
template: types.django_html = """
@ -88,10 +90,10 @@ class NestedTagsTests(BaseTestCase):
expected = """
Variable: <strong data-djc-id-a1bc3f>organisation&#x27;s</strong>
"""
self.assertHTMLEqual(rendered, expected)
assertHTMLEqual(rendered, expected)
@parametrize_context_behavior(["django", "isolated"])
def test_nested_quote_single_self_closing(self):
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_nested_quote_single_self_closing(self, components_settings):
registry.register("test", self.SimpleComponent)
template: types.django_html = """
@ -102,10 +104,10 @@ class NestedTagsTests(BaseTestCase):
expected = """
Variable: <strong data-djc-id-a1bc3f>organisation&#x27;s</strong>
"""
self.assertHTMLEqual(rendered, expected)
assertHTMLEqual(rendered, expected)
@parametrize_context_behavior(["django", "isolated"])
def test_nested_quote_double(self):
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_nested_quote_double(self, components_settings):
registry.register("test", self.SimpleComponent)
template: types.django_html = """
@ -116,10 +118,10 @@ class NestedTagsTests(BaseTestCase):
expected = """
Variable: <strong data-djc-id-a1bc3f>organisation"s</strong>
"""
self.assertHTMLEqual(rendered, expected)
assertHTMLEqual(rendered, expected)
@parametrize_context_behavior(["django", "isolated"])
def test_nested_quote_double_self_closing(self):
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_nested_quote_double_self_closing(self, components_settings):
registry.register("test", self.SimpleComponent)
template: types.django_html = """
@ -130,4 +132,4 @@ class NestedTagsTests(BaseTestCase):
expected = """
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_test_setup import setup_test_config
from .testutils import BaseTestCase, parametrize_context_behavior
from django_components.testing import djc_test
from .testutils import PARAMETRIZE_CONTEXT_BEHAVIOR, setup_test_config
setup_test_config({"autodiscover": False})
@ -31,7 +34,8 @@ class SlottedComponentWithContext(Component):
#######################
class ComponentTemplateTagTest(BaseTestCase):
@djc_test
class TestComponentTemplateTag:
class SimpleComponent(Component):
template: types.django_html = """
Variable: <strong>{{ variable }}</strong>
@ -47,8 +51,8 @@ class ComponentTemplateTagTest(BaseTestCase):
css = "style.css"
js = "script.js"
@parametrize_context_behavior(["django", "isolated"])
def test_single_component(self):
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_single_component(self, components_settings):
registry.register(name="test", component=self.SimpleComponent)
simple_tag_template: types.django_html = """
@ -58,10 +62,10 @@ class ComponentTemplateTagTest(BaseTestCase):
template = Template(simple_tag_template)
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"])
def test_single_component_self_closing(self):
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_single_component_self_closing(self, components_settings):
registry.register(name="test", component=self.SimpleComponent)
simple_tag_template: types.django_html = """
@ -71,10 +75,10 @@ class ComponentTemplateTagTest(BaseTestCase):
template = Template(simple_tag_template)
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"])
def test_call_with_invalid_name(self):
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_call_with_invalid_name(self, components_settings):
registry.register(name="test_one", component=self.SimpleComponent)
simple_tag_template: types.django_html = """
@ -83,11 +87,11 @@ class ComponentTemplateTagTest(BaseTestCase):
"""
template = Template(simple_tag_template)
with self.assertRaises(NotRegistered):
with pytest.raises(NotRegistered):
template.render(Context({}))
@parametrize_context_behavior(["django", "isolated"])
def test_component_called_with_positional_name(self):
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_component_called_with_positional_name(self, components_settings):
registry.register(name="test", component=self.SimpleComponent)
simple_tag_template: types.django_html = """
@ -97,10 +101,10 @@ class ComponentTemplateTagTest(BaseTestCase):
template = Template(simple_tag_template)
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"])
def test_call_component_with_two_variables(self):
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_call_component_with_two_variables(self, components_settings):
@register("test")
class IffedComponent(Component):
template: types.django_html = """
@ -127,7 +131,7 @@ class ComponentTemplateTagTest(BaseTestCase):
template = Template(simple_tag_template)
rendered = template.render(Context({}))
self.assertHTMLEqual(
assertHTMLEqual(
rendered,
"""
Variable: <strong data-djc-id-a1bc3f>variable</strong>
@ -135,8 +139,8 @@ class ComponentTemplateTagTest(BaseTestCase):
""",
)
@parametrize_context_behavior(["django", "isolated"])
def test_component_called_with_singlequoted_name(self):
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_component_called_with_singlequoted_name(self, components_settings):
registry.register(name="test", component=self.SimpleComponent)
simple_tag_template: types.django_html = """
@ -146,10 +150,10 @@ class ComponentTemplateTagTest(BaseTestCase):
template = Template(simple_tag_template)
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"])
def test_raises_on_component_called_with_variable_as_name(self):
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_raises_on_component_called_with_variable_as_name(self, components_settings):
registry.register(name="test", component=self.SimpleComponent)
simple_tag_template: types.django_html = """
@ -159,14 +163,14 @@ class ComponentTemplateTagTest(BaseTestCase):
{% endwith %}
"""
with self.assertRaisesMessage(
with pytest.raises(
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)
@parametrize_context_behavior(["django", "isolated"])
def test_component_accepts_provided_and_default_parameters(self):
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_component_accepts_provided_and_default_parameters(self, components_settings):
@register("test")
class ComponentWithProvidedAndDefaultParameters(Component):
template: types.django_html = """
@ -184,7 +188,7 @@ class ComponentTemplateTagTest(BaseTestCase):
"""
template = Template(template_str)
rendered = template.render(Context({}))
self.assertHTMLEqual(
assertHTMLEqual(
rendered,
"""
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):
template: types.django_html = """
Variable: <strong>{{ variable }}</strong>
@ -209,16 +214,8 @@ class DynamicComponentTemplateTagTest(BaseTestCase):
css = "style.css"
js = "script.js"
def setUp(self):
super().setUp()
# 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):
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_basic(self, components_settings):
registry.register(name="test", component=self.SimpleComponent)
simple_tag_template: types.django_html = """
@ -228,13 +225,13 @@ class DynamicComponentTemplateTagTest(BaseTestCase):
template = Template(simple_tag_template)
rendered = template.render(Context({}))
self.assertHTMLEqual(
assertHTMLEqual(
rendered,
"Variable: <strong data-djc-id-a1bc3f data-djc-id-a1bc40>variable</strong>",
)
@parametrize_context_behavior(["django", "isolated"])
def test_call_with_invalid_name(self):
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_call_with_invalid_name(self, components_settings):
registry.register(name="test", component=self.SimpleComponent)
simple_tag_template: types.django_html = """
@ -243,11 +240,11 @@ class DynamicComponentTemplateTagTest(BaseTestCase):
"""
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({}))
@parametrize_context_behavior(["django", "isolated"])
def test_component_called_with_variable_as_name(self):
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_component_called_with_variable_as_name(self, components_settings):
registry.register(name="test", component=self.SimpleComponent)
simple_tag_template: types.django_html = """
@ -259,13 +256,13 @@ class DynamicComponentTemplateTagTest(BaseTestCase):
template = Template(simple_tag_template)
rendered = template.render(Context({}))
self.assertHTMLEqual(
assertHTMLEqual(
rendered,
"Variable: <strong data-djc-id-a1bc3f data-djc-id-a1bc40>variable</strong>",
)
@parametrize_context_behavior(["django", "isolated"])
def test_component_called_with_variable_as_spread(self):
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_component_called_with_variable_as_spread(self, components_settings):
registry.register(name="test", component=self.SimpleComponent)
simple_tag_template: types.django_html = """
@ -284,13 +281,13 @@ class DynamicComponentTemplateTagTest(BaseTestCase):
}
)
)
self.assertHTMLEqual(
assertHTMLEqual(
rendered,
"Variable: <strong data-djc-id-a1bc3f data-djc-id-a1bc40>variable</strong>",
)
@parametrize_context_behavior(["django", "isolated"])
def test_component_as_class(self):
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_component_as_class(self, components_settings):
registry.register(name="test", component=self.SimpleComponent)
simple_tag_template: types.django_html = """
@ -306,21 +303,19 @@ class DynamicComponentTemplateTagTest(BaseTestCase):
}
)
)
self.assertHTMLEqual(
assertHTMLEqual(
rendered,
"Variable: <strong data-djc-id-a1bc3f data-djc-id-a1bc40>variable</strong>",
)
@parametrize_context_behavior(
["django", "isolated"],
settings={
"COMPONENTS": {
"tag_formatter": "django_components.component_shorthand_formatter",
"autodiscover": False,
},
@djc_test(
parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR,
components_settings={
"tag_formatter": "django_components.component_shorthand_formatter",
"autodiscover": False,
},
)
def test_shorthand_formatter(self):
def test_shorthand_formatter(self, components_settings):
from django_components.apps import ComponentsConfig
ComponentsConfig.ready(None) # type: ignore[arg-type]
@ -334,19 +329,17 @@ class DynamicComponentTemplateTagTest(BaseTestCase):
template = Template(simple_tag_template)
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(
["django", "isolated"],
settings={
"COMPONENTS": {
"dynamic_component_name": "uno_reverse",
"tag_formatter": "django_components.component_shorthand_formatter",
"autodiscover": False,
},
@djc_test(
parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR,
components_settings={
"dynamic_component_name": "uno_reverse",
"tag_formatter": "django_components.component_shorthand_formatter",
"autodiscover": False,
},
)
def test_component_name_is_configurable(self):
def test_component_name_is_configurable(self, components_settings):
from django_components.apps import ComponentsConfig
ComponentsConfig.ready(None) # type: ignore[arg-type]
@ -360,18 +353,21 @@ class DynamicComponentTemplateTagTest(BaseTestCase):
template = Template(simple_tag_template)
rendered = template.render(Context({}))
self.assertHTMLEqual(
assertHTMLEqual(
rendered,
"Variable: <strong data-djc-id-a1bc3f data-djc-id-a1bc40>variable</strong>",
)
@parametrize_context_behavior(["django", "isolated"])
def test_raises_already_registered_on_name_conflict(self):
with self.assertRaisesMessage(AlreadyRegistered, 'The component "dynamic" has already been registered'):
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_raises_already_registered_on_name_conflict(self, components_settings):
with pytest.raises(
AlreadyRegistered,
match=re.escape('The component "dynamic" has already been registered'),
):
registry.register(name="dynamic", component=self.SimpleComponent)
@parametrize_context_behavior(["django", "isolated"])
def test_component_called_with_default_slot(self):
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_component_called_with_default_slot(self, components_settings):
class SimpleSlottedComponent(Component):
template: types.django_html = """
{% load component_tags %}
@ -398,7 +394,7 @@ class DynamicComponentTemplateTagTest(BaseTestCase):
template = Template(simple_tag_template)
rendered = template.render(Context({}))
self.assertHTMLEqual(
assertHTMLEqual(
rendered,
"""
Variable: <strong data-djc-id-a1bc3f data-djc-id-a1bc40>variable</strong>
@ -406,8 +402,8 @@ class DynamicComponentTemplateTagTest(BaseTestCase):
""",
)
@parametrize_context_behavior(["django", "isolated"])
def test_component_called_with_named_slots(self):
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_component_called_with_named_slots(self, components_settings):
class SimpleSlottedComponent(Component):
template: types.django_html = """
{% load component_tags %}
@ -440,7 +436,7 @@ class DynamicComponentTemplateTagTest(BaseTestCase):
template = Template(simple_tag_template)
rendered = template.render(Context({}))
self.assertHTMLEqual(
assertHTMLEqual(
rendered,
"""
Variable: <strong data-djc-id-a1bc41 data-djc-id-a1bc42>variable</strong>
@ -449,8 +445,8 @@ class DynamicComponentTemplateTagTest(BaseTestCase):
""",
)
@parametrize_context_behavior(["django", "isolated"])
def test_ignores_invalid_slots(self):
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_ignores_invalid_slots(self, components_settings):
class SimpleSlottedComponent(Component):
template: types.django_html = """
{% load component_tags %}
@ -483,7 +479,7 @@ class DynamicComponentTemplateTagTest(BaseTestCase):
template = Template(simple_tag_template)
rendered = template.render(Context({}))
self.assertHTMLEqual(
assertHTMLEqual(
rendered,
"""
Variable: <strong data-djc-id-a1bc41 data-djc-id-a1bc42>variable</strong>
@ -492,8 +488,8 @@ class DynamicComponentTemplateTagTest(BaseTestCase):
""",
)
@parametrize_context_behavior(["django", "isolated"])
def test_raises_on_invalid_args(self):
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_raises_on_invalid_args(self, components_settings):
registry.register(name="test", component=self.SimpleComponent)
simple_tag_template: types.django_html = """
@ -504,13 +500,17 @@ class DynamicComponentTemplateTagTest(BaseTestCase):
"""
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({}))
class MultiComponentTests(BaseTestCase):
@parametrize_context_behavior(["django", "isolated"])
def test_both_components_render_correctly_with_no_slots(self):
@djc_test
class TestMultiComponent:
@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("second_component", SlottedComponentWithContext)
@ -524,7 +524,7 @@ class MultiComponentTests(BaseTestCase):
template = Template(template_str)
rendered = template.render(Context())
self.assertHTMLEqual(
assertHTMLEqual(
rendered,
"""
<custom-template data-djc-id-a1bc40>
@ -534,7 +534,7 @@ class MultiComponentTests(BaseTestCase):
<main>Default main</main>
<footer>Default footer</footer>
</custom-template>
<custom-template data-djc-id-a1bc44>
<custom-template data-djc-id-a1bc47>
<header>
Default header
</header>
@ -544,8 +544,8 @@ class MultiComponentTests(BaseTestCase):
""",
)
@parametrize_context_behavior(["django", "isolated"])
def test_both_components_render_correctly_with_slots(self):
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_both_components_render_correctly_with_slots(self, components_settings):
registry.register("first_component", SlottedComponent)
registry.register("second_component", SlottedComponentWithContext)
@ -561,7 +561,7 @@ class MultiComponentTests(BaseTestCase):
template = Template(template_str)
rendered = template.render(Context())
self.assertHTMLEqual(
assertHTMLEqual(
rendered,
"""
<custom-template data-djc-id-a1bc42>
@ -571,7 +571,7 @@ class MultiComponentTests(BaseTestCase):
<main>Default main</main>
<footer>Default footer</footer>
</custom-template>
<custom-template data-djc-id-a1bc46>
<custom-template data-djc-id-a1bc49>
<header>
<div>Slot #2</div>
</header>
@ -581,16 +581,8 @@ class MultiComponentTests(BaseTestCase):
""",
)
@parametrize_context_behavior(
# TODO: Why is this the only place where this needs to be parametrized?
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
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_both_components_render_correctly_when_only_first_has_slots(self, components_settings):
registry.register("first_component", SlottedComponent)
registry.register("second_component", SlottedComponentWithContext)
@ -605,9 +597,9 @@ class MultiComponentTests(BaseTestCase):
template = Template(template_str)
rendered = template.render(Context({}))
self.assertHTMLEqual(
assertHTMLEqual(
rendered,
f"""
"""
<custom-template data-djc-id-a1bc41>
<header>
<p>Slot #1</p>
@ -615,7 +607,7 @@ class MultiComponentTests(BaseTestCase):
<main>Default main</main>
<footer>Default footer</footer>
</custom-template>
<custom-template {second_id}>
<custom-template data-djc-id-a1bc48>
<header>
Default header
</header>
@ -625,8 +617,8 @@ class MultiComponentTests(BaseTestCase):
""",
)
@parametrize_context_behavior(["django", "isolated"])
def test_both_components_render_correctly_when_only_second_has_slots(self):
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_both_components_render_correctly_when_only_second_has_slots(self, components_settings):
registry.register("first_component", SlottedComponent)
registry.register("second_component", SlottedComponentWithContext)
@ -641,7 +633,7 @@ class MultiComponentTests(BaseTestCase):
template = Template(template_str)
rendered = template.render(Context({}))
self.assertHTMLEqual(
assertHTMLEqual(
rendered,
"""
<custom-template data-djc-id-a1bc41>
@ -651,7 +643,7 @@ class MultiComponentTests(BaseTestCase):
<main>Default main</main>
<footer>Default footer</footer>
</custom-template>
<custom-template data-djc-id-a1bc45>
<custom-template data-djc-id-a1bc48>
<header>
<div>Slot #2</div>
</header>
@ -662,7 +654,8 @@ class MultiComponentTests(BaseTestCase):
)
class ComponentIsolationTests(BaseTestCase):
@djc_test
class TestComponentIsolation:
class SlottedComponent(Component):
template: types.django_html = """
{% load component_tags %}
@ -673,12 +666,9 @@ class ComponentIsolationTests(BaseTestCase):
</custom-template>
"""
def setUp(self):
super().setUp()
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_instances_of_component_do_not_share_slots(self, components_settings):
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 = """
{% load component_tags %}
{% component "test" %}
@ -696,7 +686,7 @@ class ComponentIsolationTests(BaseTestCase):
template.render(Context({}))
rendered = template.render(Context({}))
self.assertHTMLEqual(
assertHTMLEqual(
rendered,
"""
<custom-template data-djc-id-a1bc4a>
@ -718,9 +708,10 @@ class ComponentIsolationTests(BaseTestCase):
)
class AggregateInputTests(BaseTestCase):
@parametrize_context_behavior(["django", "isolated"])
def test_agg_input_accessible_in_get_context_data(self):
@djc_test
class TestAggregateInput:
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_agg_input_accessible_in_get_context_data(self, components_settings):
@register("test")
class AttrsComponent(Component):
template: types.django_html = """
@ -741,7 +732,7 @@ class AggregateInputTests(BaseTestCase):
""" # noqa: E501
template = Template(template_str)
rendered = template.render(Context({"class_var": "padding-top-8"}))
self.assertHTMLEqual(
assertHTMLEqual(
rendered,
"""
<div data-djc-id-a1bc3f>
@ -752,9 +743,10 @@ class AggregateInputTests(BaseTestCase):
)
class RecursiveComponentTests(BaseTestCase):
@parametrize_context_behavior(["django", "isolated"])
def test_recursive_component(self):
@djc_test
class TestRecursiveComponent:
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_recursive_component(self, components_settings):
DEPTH = 100
@register("recursive")
@ -775,16 +767,14 @@ class RecursiveComponentTests(BaseTestCase):
result = Recursive.render()
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):
def setUp(self):
super().setUp()
@djc_test
class TestComponentTemplateSyntaxError:
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_variable_outside_fill_tag_compiles_w_out_error(self, components_settings):
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"
# contains a slot tag marked as 'default'. This is verified outside
# template compilation time.
@ -796,8 +786,9 @@ class ComponentTemplateSyntaxErrorTests(BaseTestCase):
"""
Template(template_str)
@parametrize_context_behavior(["django", "isolated"])
def test_text_outside_fill_tag_is_not_error_when_no_fill_tags(self):
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
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"
# contains a slot tag marked as 'default'. This is verified outside
# template compilation time.
@ -809,8 +800,9 @@ class ComponentTemplateSyntaxErrorTests(BaseTestCase):
"""
Template(template_str)
@parametrize_context_behavior(["django", "isolated"])
def test_text_outside_fill_tag_is_error_when_fill_tags(self):
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_text_outside_fill_tag_is_error_when_fill_tags(self, components_settings):
registry.register("test", SlottedComponent)
template_str: types.django_html = """
{% load component_tags %}
{% component "test" %}
@ -820,17 +812,18 @@ class ComponentTemplateSyntaxErrorTests(BaseTestCase):
"""
template = Template(template_str)
with self.assertRaisesMessage(
with pytest.raises(
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())
@parametrize_context_behavior(["django", "isolated"])
def test_unclosed_component_is_error(self):
with self.assertRaisesMessage(
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_unclosed_component_is_error(self, components_settings):
registry.register("test", SlottedComponent)
with pytest.raises(
TemplateSyntaxError,
"Unclosed tag on line 3: 'component'",
match=re.escape("Unclosed tag on line 3: 'component'"),
):
template_str: types.django_html = """
{% load component_tags %}

View file

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

View file

@ -1,24 +1,28 @@
import re
from typing import Any
import pytest
from django.template import Context, Template, TemplateSyntaxError
from pytest_django.asserts import assertHTMLEqual
from django_components import Component, register, types
from django_components.perfutil.provide import provide_cache, provide_references, all_reference_ids
from .django_test_setup import setup_test_config
from .testutils import BaseTestCase, parametrize_context_behavior
from django_components.testing import djc_test
from .testutils import PARAMETRIZE_CONTEXT_BEHAVIOR, setup_test_config
setup_test_config({"autodiscover": False})
class ProvideTemplateTagTest(BaseTestCase):
@djc_test
class TestProvideTemplateTag:
def _assert_clear_cache(self):
self.assertEqual(provide_cache, {})
self.assertEqual(provide_references, {})
self.assertEqual(all_reference_ids, set())
assert provide_cache == {}
assert provide_references == {}
assert all_reference_ids == set()
@parametrize_context_behavior(["django", "isolated"])
def test_provide_basic(self):
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_provide_basic(self, components_settings):
@register("injectee")
class InjectComponent(Component):
template: types.django_html = """
@ -39,7 +43,7 @@ class ProvideTemplateTagTest(BaseTestCase):
template = Template(template_str)
rendered = template.render(Context({}))
self.assertHTMLEqual(
assertHTMLEqual(
rendered,
"""
<div data-djc-id-a1bc41> injected: DepInject(key='hi', another=1) </div>
@ -47,8 +51,8 @@ class ProvideTemplateTagTest(BaseTestCase):
)
self._assert_clear_cache()
@parametrize_context_behavior(["django", "isolated"])
def test_provide_basic_self_closing(self):
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_provide_basic_self_closing(self, components_settings):
template_str: types.django_html = """
{% load component_tags %}
<div>
@ -58,7 +62,7 @@ class ProvideTemplateTagTest(BaseTestCase):
template = Template(template_str)
rendered = template.render(Context({}))
self.assertHTMLEqual(
assertHTMLEqual(
rendered,
"""
<div></div>
@ -66,8 +70,8 @@ class ProvideTemplateTagTest(BaseTestCase):
)
self._assert_clear_cache()
@parametrize_context_behavior(["django", "isolated"])
def test_provide_access_keys_in_python(self):
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_provide_access_keys_in_python(self, components_settings):
@register("injectee")
class InjectComponent(Component):
template: types.django_html = """
@ -92,7 +96,7 @@ class ProvideTemplateTagTest(BaseTestCase):
template = Template(template_str)
rendered = template.render(Context({}))
self.assertHTMLEqual(
assertHTMLEqual(
rendered,
"""
<div data-djc-id-a1bc41> key: hi </div>
@ -101,8 +105,8 @@ class ProvideTemplateTagTest(BaseTestCase):
)
self._assert_clear_cache()
@parametrize_context_behavior(["django", "isolated"])
def test_provide_access_keys_in_django(self):
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_provide_access_keys_in_django(self, components_settings):
@register("injectee")
class InjectComponent(Component):
template: types.django_html = """
@ -126,7 +130,7 @@ class ProvideTemplateTagTest(BaseTestCase):
template = Template(template_str)
rendered = template.render(Context({}))
self.assertHTMLEqual(
assertHTMLEqual(
rendered,
"""
<div data-djc-id-a1bc41> key: hi </div>
@ -135,8 +139,8 @@ class ProvideTemplateTagTest(BaseTestCase):
)
self._assert_clear_cache()
@parametrize_context_behavior(["django", "isolated"])
def test_provide_does_not_leak(self):
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_provide_does_not_leak(self, components_settings):
@register("injectee")
class InjectComponent(Component):
template: types.django_html = """
@ -157,7 +161,7 @@ class ProvideTemplateTagTest(BaseTestCase):
template = Template(template_str)
rendered = template.render(Context({}))
self.assertHTMLEqual(
assertHTMLEqual(
rendered,
"""
<div data-djc-id-a1bc41> injected: default </div>
@ -165,8 +169,8 @@ class ProvideTemplateTagTest(BaseTestCase):
)
self._assert_clear_cache()
@parametrize_context_behavior(["django", "isolated"])
def test_provide_empty(self):
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_provide_empty(self, components_settings):
"""Check provide tag with no kwargs"""
@register("injectee")
@ -191,7 +195,7 @@ class ProvideTemplateTagTest(BaseTestCase):
template = Template(template_str)
rendered = template.render(Context({}))
self.assertHTMLEqual(
assertHTMLEqual(
rendered,
"""
<div data-djc-id-a1bc42> injected: DepInject() </div>
@ -200,7 +204,7 @@ class ProvideTemplateTagTest(BaseTestCase):
)
self._assert_clear_cache()
@parametrize_context_behavior(["django"])
@djc_test(components_settings={"context_behavior": "django"})
def test_provide_no_inject(self):
"""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)
rendered = template.render(Context({}))
self.assertHTMLEqual(
assertHTMLEqual(
rendered,
"""
<div data-djc-id-a1bc42></div>
@ -234,8 +238,8 @@ class ProvideTemplateTagTest(BaseTestCase):
)
self._assert_clear_cache()
@parametrize_context_behavior(["django", "isolated"])
def test_provide_name_single_quotes(self):
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_provide_name_single_quotes(self, components_settings):
@register("injectee")
class InjectComponent(Component):
template: types.django_html = """
@ -258,7 +262,7 @@ class ProvideTemplateTagTest(BaseTestCase):
template = Template(template_str)
rendered = template.render(Context({}))
self.assertHTMLEqual(
assertHTMLEqual(
rendered,
"""
<div data-djc-id-a1bc42> injected: DepInject(key='hi', another=7) </div>
@ -267,8 +271,8 @@ class ProvideTemplateTagTest(BaseTestCase):
)
self._assert_clear_cache()
@parametrize_context_behavior(["django", "isolated"])
def test_provide_name_as_var(self):
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_provide_name_as_var(self, components_settings):
@register("injectee")
class InjectComponent(Component):
template: types.django_html = """
@ -297,7 +301,7 @@ class ProvideTemplateTagTest(BaseTestCase):
)
)
self.assertHTMLEqual(
assertHTMLEqual(
rendered,
"""
<div data-djc-id-a1bc42> injected: DepInject(key='hi', another=8) </div>
@ -306,8 +310,8 @@ class ProvideTemplateTagTest(BaseTestCase):
)
self._assert_clear_cache()
@parametrize_context_behavior(["django", "isolated"])
def test_provide_name_as_spread(self):
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_provide_name_as_spread(self, components_settings):
@register("injectee")
class InjectComponent(Component):
template: types.django_html = """
@ -340,7 +344,7 @@ class ProvideTemplateTagTest(BaseTestCase):
)
)
self.assertHTMLEqual(
assertHTMLEqual(
rendered,
"""
<div data-djc-id-a1bc42> injected: DepInject(key='hi', another=9) </div>
@ -349,8 +353,8 @@ class ProvideTemplateTagTest(BaseTestCase):
)
self._assert_clear_cache()
@parametrize_context_behavior(["django", "isolated"])
def test_provide_no_name_raises(self):
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_provide_no_name_raises(self, components_settings):
@register("injectee")
class InjectComponent(Component):
template: types.django_html = """
@ -370,16 +374,16 @@ class ProvideTemplateTagTest(BaseTestCase):
{% component "injectee" %}
{% endcomponent %}
"""
with self.assertRaisesMessage(
with pytest.raises(
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({}))
self._assert_clear_cache()
@parametrize_context_behavior(["django", "isolated"])
def test_provide_name_must_be_string_literal(self):
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_provide_name_must_be_string_literal(self, components_settings):
@register("injectee")
class InjectComponent(Component):
template: types.django_html = """
@ -399,16 +403,16 @@ class ProvideTemplateTagTest(BaseTestCase):
{% component "injectee" %}
{% endcomponent %}
"""
with self.assertRaisesMessage(
with pytest.raises(
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({}))
self._assert_clear_cache()
@parametrize_context_behavior(["django", "isolated"])
def test_provide_name_must_be_identifier(self):
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_provide_name_must_be_identifier(self, components_settings):
@register("injectee")
class InjectComponent(Component):
template: types.django_html = """
@ -430,12 +434,12 @@ class ProvideTemplateTagTest(BaseTestCase):
"""
template = Template(template_str)
with self.assertRaises(TemplateSyntaxError):
with pytest.raises(TemplateSyntaxError):
template.render(Context({}))
self._assert_clear_cache()
@parametrize_context_behavior(["django", "isolated"])
def test_provide_aggregate_dics(self):
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_provide_aggregate_dics(self, components_settings):
@register("injectee")
class InjectComponent(Component):
template: types.django_html = """
@ -456,7 +460,7 @@ class ProvideTemplateTagTest(BaseTestCase):
template = Template(template_str)
rendered = template.render(Context({}))
self.assertHTMLEqual(
assertHTMLEqual(
rendered,
"""
<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()
@parametrize_context_behavior(["django", "isolated"])
def test_provide_does_not_expose_kwargs_to_context(self):
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
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"""
@register("injectee")
@ -490,7 +494,7 @@ class ProvideTemplateTagTest(BaseTestCase):
template = Template(template_str)
rendered = template.render(Context({"var": "123"}))
self.assertHTMLEqual(
assertHTMLEqual(
rendered,
"""
var_out: 123
@ -501,8 +505,8 @@ class ProvideTemplateTagTest(BaseTestCase):
)
self._assert_clear_cache()
@parametrize_context_behavior(["django", "isolated"])
def test_provide_nested_in_provide_same_key(self):
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_provide_nested_in_provide_same_key(self, components_settings):
"""Check that inner `provide` with same key overshadows outer `provide`"""
@register("injectee")
@ -532,7 +536,7 @@ class ProvideTemplateTagTest(BaseTestCase):
template = Template(template_str)
rendered = template.render(Context({}))
self.assertHTMLEqual(
assertHTMLEqual(
rendered,
"""
<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()
@parametrize_context_behavior(["django", "isolated"])
def test_provide_nested_in_provide_different_key(self):
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_provide_nested_in_provide_different_key(self, components_settings):
"""Check that `provide` tag with different keys don't affect each other"""
@register("injectee")
@ -574,7 +578,7 @@ class ProvideTemplateTagTest(BaseTestCase):
template = Template(template_str)
rendered = template.render(Context({}))
self.assertHTMLEqual(
assertHTMLEqual(
rendered,
"""
<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()
@parametrize_context_behavior(["django", "isolated"])
def test_provide_in_include(self):
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_provide_in_include(self, components_settings):
@register("injectee")
class InjectComponent(Component):
template: types.django_html = """
@ -604,7 +608,7 @@ class ProvideTemplateTagTest(BaseTestCase):
template = Template(template_str)
rendered = template.render(Context({}))
self.assertHTMLEqual(
assertHTMLEqual(
rendered,
"""
<div>
@ -614,8 +618,8 @@ class ProvideTemplateTagTest(BaseTestCase):
)
self._assert_clear_cache()
@parametrize_context_behavior(["django", "isolated"])
def test_slot_in_provide(self):
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_slot_in_provide(self, components_settings):
@register("injectee")
class InjectComponent(Component):
template: types.django_html = """
@ -644,7 +648,7 @@ class ProvideTemplateTagTest(BaseTestCase):
template = Template(template_str)
rendered = template.render(Context({}))
self.assertHTMLEqual(
assertHTMLEqual(
rendered,
"""
<div data-djc-id-a1bc40 data-djc-id-a1bc44>
@ -655,14 +659,15 @@ class ProvideTemplateTagTest(BaseTestCase):
self._assert_clear_cache()
class InjectTest(BaseTestCase):
@djc_test
class TestInject:
def _assert_clear_cache(self):
self.assertEqual(provide_cache, {})
self.assertEqual(provide_references, {})
self.assertEqual(all_reference_ids, set())
assert provide_cache == {}
assert provide_references == {}
assert all_reference_ids == set()
@parametrize_context_behavior(["django", "isolated"])
def test_inject_basic(self):
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_inject_basic(self, components_settings):
@register("injectee")
class InjectComponent(Component):
template: types.django_html = """
@ -683,7 +688,7 @@ class InjectTest(BaseTestCase):
template = Template(template_str)
rendered = template.render(Context({}))
self.assertHTMLEqual(
assertHTMLEqual(
rendered,
"""
<div data-djc-id-a1bc41> injected: DepInject(key='hi', another=21) </div>
@ -691,8 +696,8 @@ class InjectTest(BaseTestCase):
)
self._assert_clear_cache()
@parametrize_context_behavior(["django", "isolated"])
def test_inject_missing_key_raises_without_default(self):
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_inject_missing_key_raises_without_default(self, components_settings):
@register("injectee")
class InjectComponent(Component):
template: types.django_html = """
@ -710,13 +715,13 @@ class InjectTest(BaseTestCase):
"""
template = Template(template_str)
with self.assertRaises(KeyError):
with pytest.raises(KeyError):
template.render(Context({}))
self._assert_clear_cache()
@parametrize_context_behavior(["django", "isolated"])
def test_inject_missing_key_ok_with_default(self):
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_inject_missing_key_ok_with_default(self, components_settings):
@register("injectee")
class InjectComponent(Component):
template: types.django_html = """
@ -734,7 +739,7 @@ class InjectTest(BaseTestCase):
"""
template = Template(template_str)
rendered = template.render(Context({}))
self.assertHTMLEqual(
assertHTMLEqual(
rendered,
"""
<div data-djc-id-a1bc3f> injected: default </div>
@ -742,8 +747,8 @@ class InjectTest(BaseTestCase):
)
self._assert_clear_cache()
@parametrize_context_behavior(["django", "isolated"])
def test_inject_empty_string(self):
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_inject_empty_string(self, components_settings):
@register("injectee")
class InjectComponent(Component):
template: types.django_html = """
@ -765,13 +770,13 @@ class InjectTest(BaseTestCase):
"""
template = Template(template_str)
with self.assertRaises(KeyError):
with pytest.raises(KeyError):
template.render(Context({}))
self._assert_clear_cache()
@parametrize_context_behavior(["django", "isolated"])
def test_inject_raises_on_called_outside_get_context_data(self):
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_inject_raises_on_called_outside_get_context_data(self, components_settings):
@register("injectee")
class InjectComponent(Component):
template: types.django_html = """
@ -783,14 +788,14 @@ class InjectTest(BaseTestCase):
return {"var": var}
comp = InjectComponent("")
with self.assertRaises(RuntimeError):
with pytest.raises(RuntimeError):
comp.inject("abc", "def")
self._assert_clear_cache()
# See https://github.com/django-components/django-components/pull/778
@parametrize_context_behavior(["django", "isolated"])
def test_inject_in_fill(self):
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_inject_in_fill(self, components_settings):
@register("injectee")
class Injectee(Component):
template: types.django_html = """
@ -844,7 +849,7 @@ class InjectTest(BaseTestCase):
rendered = Root.render()
self.assertHTMLEqual(
assertHTMLEqual(
rendered,
"""
<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()
# See https://github.com/django-components/django-components/pull/786
@parametrize_context_behavior(["django", "isolated"])
def test_inject_in_slot_in_fill(self):
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_inject_in_slot_in_fill(self, components_settings):
@register("injectee")
class Injectee(Component):
template: types.django_html = """
@ -909,7 +914,7 @@ class InjectTest(BaseTestCase):
rendered = Root.render()
self.assertHTMLEqual(
assertHTMLEqual(
rendered,
"""
<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
# when the component rendered is done.
class ProvideCacheTest(BaseTestCase):
@djc_test
class TestProvideCache:
def _assert_clear_cache(self):
self.assertEqual(provide_cache, {})
self.assertEqual(provide_references, {})
self.assertEqual(all_reference_ids, set())
assert provide_cache == {}
assert provide_references == {}
assert all_reference_ids == set()
def test_provide_outside_component(self):
tester = self
@register("injectee")
class Injectee(Component):
template: types.django_html = """
@ -946,7 +950,7 @@ class ProvideCacheTest(BaseTestCase):
"""
def get_context_data(self):
tester.assertEqual(len(provide_cache), 1)
assert len(provide_cache) == 1
data = self.inject("my_provide")
return {"data": data, "ran": True}
@ -965,7 +969,7 @@ class ProvideCacheTest(BaseTestCase):
rendered = template.render(Context({}))
self.assertHTMLEqual(
assertHTMLEqual(
rendered,
"""
<div data-djc-id-a1bc41>
@ -980,14 +984,13 @@ class ProvideCacheTest(BaseTestCase):
# Cache should be cleared even if there is an error.
def test_provide_outside_component_with_error(self):
tester = self
@register("injectee")
class Injectee(Component):
template = ""
def get_context_data(self):
tester.assertEqual(len(provide_cache), 1)
assert len(provide_cache) == 1
data = self.inject("my_provide")
raise ValueError("Oops")
@ -1005,14 +1008,12 @@ class ProvideCacheTest(BaseTestCase):
template = Template(template_str)
self._assert_clear_cache()
with self.assertRaisesMessage(ValueError, "Oops"):
with pytest.raises(ValueError, match=re.escape("Oops")):
template.render(Context({}))
self._assert_clear_cache()
def test_provide_inside_component(self):
tester = self
@register("injectee")
class Injectee(Component):
template: types.django_html = """
@ -1022,7 +1023,7 @@ class ProvideCacheTest(BaseTestCase):
"""
def get_context_data(self):
tester.assertEqual(len(provide_cache), 1)
assert len(provide_cache) == 1
data = self.inject("my_provide")
return {"data": data, "ran": True}
@ -1040,7 +1041,7 @@ class ProvideCacheTest(BaseTestCase):
rendered = Root.render()
self.assertHTMLEqual(
assertHTMLEqual(
rendered,
"""
<div data-djc-id-a1bc3e data-djc-id-a1bc42>
@ -1054,14 +1055,12 @@ class ProvideCacheTest(BaseTestCase):
self._assert_clear_cache()
def test_provide_inside_component_with_error(self):
tester = self
@register("injectee")
class Injectee(Component):
template = ""
def get_context_data(self):
tester.assertEqual(len(provide_cache), 1)
assert len(provide_cache) == 1
data = self.inject("my_provide")
raise ValueError("Oops")
@ -1078,7 +1077,7 @@ class ProvideCacheTest(BaseTestCase):
self._assert_clear_cache()
with self.assertRaisesMessage(ValueError, "Oops"):
with pytest.raises(ValueError, match=re.escape("Oops")):
Root.render()
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 django.template import Context, Template
from pytest_django.asserts import assertHTMLEqual
from django_components import Component, register, registry, types
from .django_test_setup import setup_test_config
from .testutils import BaseTestCase, parametrize_context_behavior
from django_components.testing import djc_test
from .testutils import PARAMETRIZE_CONTEXT_BEHAVIOR, setup_test_config
setup_test_config({"autodiscover": False})
@ -33,7 +34,8 @@ class SlottedComponentWithContext(SlottedComponent):
#######################
class NestedSlotTests(BaseTestCase):
@djc_test
class TestNestedSlot:
class NestedComponent(Component):
template: types.django_html = """
{% load component_tags %}
@ -42,8 +44,8 @@ class NestedSlotTests(BaseTestCase):
{% endslot %}
"""
@parametrize_context_behavior(["django", "isolated"])
def test_default_slot_contents_render_correctly(self):
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_default_slot_contents_render_correctly(self, components_settings):
registry.clear()
registry.register("test", self.NestedComponent)
template_str: types.django_html = """
@ -52,10 +54,10 @@ class NestedSlotTests(BaseTestCase):
"""
template = Template(template_str)
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"])
def test_inner_slot_overriden(self):
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_inner_slot_overriden(self, components_settings):
registry.clear()
registry.register("test", self.NestedComponent)
template_str: types.django_html = """
@ -66,10 +68,10 @@ class NestedSlotTests(BaseTestCase):
"""
template = Template(template_str)
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"])
def test_outer_slot_overriden(self):
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_outer_slot_overriden(self, components_settings):
registry.clear()
registry.register("test", self.NestedComponent)
template_str: types.django_html = """
@ -78,10 +80,10 @@ class NestedSlotTests(BaseTestCase):
"""
template = Template(template_str)
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"])
def test_both_overriden_and_inner_removed(self):
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_both_overriden_and_inner_removed(self, components_settings):
registry.clear()
registry.register("test", self.NestedComponent)
template_str: types.django_html = """
@ -93,13 +95,22 @@ class NestedSlotTests(BaseTestCase):
"""
template = Template(template_str)
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,
# the value should be overridden by the component, while in "isolated" it should
# remain top-level context.
@parametrize_context_behavior([("django", "Joe2"), ("isolated", "Jannete")])
def test_fill_inside_fill_with_same_name(self, context_behavior_data):
@djc_test(
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):
template: types.django_html = """
{% load component_tags %}
@ -142,13 +153,13 @@ class NestedSlotTests(BaseTestCase):
self.template = Template(template_str)
rendered = self.template.render(Context({"day": "Monday", "name": "Jannete"}))
self.assertHTMLEqual(
assertHTMLEqual(
rendered,
f"""
<custom-template data-djc-id-a1bc45>
<header>
<custom-template data-djc-id-a1bc49>
<header>Name2: {context_behavior_data}</header>
<header>Name2: {expected}</header>
<main>Day2: Monday</main>
<footer>XYZ</footer>
</custom-template>
@ -162,7 +173,8 @@ class NestedSlotTests(BaseTestCase):
# NOTE: This test group are kept for backward compatibility, as the same logic
# as provided by {% if %} tags was previously provided by this library.
class ConditionalSlotTests(BaseTestCase):
@djc_test
class TestConditionalSlot:
class ConditionalComponent(Component):
template: types.django_html = """
{% load component_tags %}
@ -176,12 +188,9 @@ class ConditionalSlotTests(BaseTestCase):
def get_context_data(self, branch=None):
return {"branch": branch}
def setUp(self):
super().setUp()
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_no_content_if_branches_are_false(self, components_settings):
registry.register("test", self.ConditionalComponent)
@parametrize_context_behavior(["django", "isolated"])
def test_no_content_if_branches_are_false(self):
template_str: types.django_html = """
{% load component_tags %}
{% component 'test' %}
@ -191,10 +200,11 @@ class ConditionalSlotTests(BaseTestCase):
"""
template = Template(template_str)
rendered = template.render(Context({}))
self.assertHTMLEqual(rendered, "")
assertHTMLEqual(rendered, "")
@parametrize_context_behavior(["django", "isolated"])
def test_default_content_if_no_slots(self):
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_default_content_if_no_slots(self, components_settings):
registry.register("test", self.ConditionalComponent)
template_str: types.django_html = """
{% load component_tags %}
{% component 'test' branch='a' %}{% endcomponent %}
@ -202,7 +212,7 @@ class ConditionalSlotTests(BaseTestCase):
"""
template = Template(template_str)
rendered = template.render(Context({}))
self.assertHTMLEqual(
assertHTMLEqual(
rendered,
"""
<p id="a" data-djc-id-a1bc40>Default A</p>
@ -210,8 +220,9 @@ class ConditionalSlotTests(BaseTestCase):
""",
)
@parametrize_context_behavior(["django", "isolated"])
def test_one_slot_overridden(self):
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_one_slot_overridden(self, components_settings):
registry.register("test", self.ConditionalComponent)
template_str: types.django_html = """
{% load component_tags %}
{% component 'test' branch='a' %}
@ -223,7 +234,7 @@ class ConditionalSlotTests(BaseTestCase):
"""
template = Template(template_str)
rendered = template.render(Context({}))
self.assertHTMLEqual(
assertHTMLEqual(
rendered,
"""
<p id="a" data-djc-id-a1bc42>Default A</p>
@ -231,8 +242,9 @@ class ConditionalSlotTests(BaseTestCase):
""",
)
@parametrize_context_behavior(["django", "isolated"])
def test_both_slots_overridden(self):
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_both_slots_overridden(self, components_settings):
registry.register("test", self.ConditionalComponent)
template_str: types.django_html = """
{% load component_tags %}
{% component 'test' branch='a' %}
@ -246,7 +258,7 @@ class ConditionalSlotTests(BaseTestCase):
"""
template = Template(template_str)
rendered = template.render(Context({}))
self.assertHTMLEqual(
assertHTMLEqual(
rendered,
"""
<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."""
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.
@parametrize_context_behavior(
[
("django", "OBJECT1 OBJECT2"),
("isolated", ""),
]
@djc_test(
parametrize=(
["components_settings", "expected"],
[
[{"context_behavior": "django"}, "OBJECT1 OBJECT2"],
[{"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)
template_str: types.django_html = """
@ -295,17 +312,21 @@ class SlotIterationTest(BaseTestCase):
objects = ["OBJECT1", "OBJECT2"]
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,
# we should still have access to root context (returned from get_context_data)
@parametrize_context_behavior(
[
("django", "OUTER_SCOPE_VARIABLE OBJECT1 OUTER_SCOPE_VARIABLE OBJECT2"),
("isolated", "OUTER_SCOPE_VARIABLE OUTER_SCOPE_VARIABLE"),
]
@djc_test(
parametrize=(
["components_settings", "expected"],
[
[{"context_behavior": "django"}, "OUTER_SCOPE_VARIABLE OBJECT1 OUTER_SCOPE_VARIABLE OBJECT2"],
[{"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)
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.
@parametrize_context_behavior(
[
("django", "ITER1_OBJ1 ITER1_OBJ2 ITER2_OBJ1 ITER2_OBJ2"),
("isolated", ""),
]
@djc_test(
parametrize=(
["components_settings", "expected"],
[
[{"context_behavior": "django"}, "ITER1_OBJ1 ITER1_OBJ2 ITER2_OBJ1 ITER2_OBJ2"],
[{"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)
objects = [
@ -360,31 +385,35 @@ class SlotIterationTest(BaseTestCase):
template = Template(template_str)
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,
# we should still have access to root context (returned from get_context_data)
@parametrize_context_behavior(
[
(
"django",
"""
OUTER_SCOPE_VARIABLE1
OUTER_SCOPE_VARIABLE2
ITER1_OBJ1
OUTER_SCOPE_VARIABLE2
ITER1_OBJ2
OUTER_SCOPE_VARIABLE1
OUTER_SCOPE_VARIABLE2
ITER2_OBJ1
OUTER_SCOPE_VARIABLE2
ITER2_OBJ2
""",
),
("isolated", "OUTER_SCOPE_VARIABLE1 OUTER_SCOPE_VARIABLE1"),
]
@djc_test(
parametrize=(
["components_settings", "expected"],
[
[
{"context_behavior": "django"},
"""
OUTER_SCOPE_VARIABLE1
OUTER_SCOPE_VARIABLE2
ITER1_OBJ1
OUTER_SCOPE_VARIABLE2
ITER1_OBJ2
OUTER_SCOPE_VARIABLE1
OUTER_SCOPE_VARIABLE2
ITER2_OBJ1
OUTER_SCOPE_VARIABLE2
ITER2_OBJ2
""",
],
[{"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)
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.
@parametrize_context_behavior(
[
("django", "ITER1_OBJ1 default ITER1_OBJ2 default ITER2_OBJ1 default ITER2_OBJ2 default"),
("isolated", ""),
]
@djc_test(
parametrize=(
["components_settings", "expected"],
[
[{"context_behavior": "django"}, "ITER1_OBJ1 default ITER1_OBJ2 default ITER2_OBJ1 default ITER2_OBJ2 default"], # noqa: E501
[{"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)
objects = [
@ -449,35 +482,40 @@ class SlotIterationTest(BaseTestCase):
template = Template(template_str)
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.
@parametrize_context_behavior(
[
(
"django",
"""
OUTER_SCOPE_VARIABLE1
OUTER_SCOPE_VARIABLE2
ITER1_OBJ1 default
OUTER_SCOPE_VARIABLE2
ITER1_OBJ2 default
OUTER_SCOPE_VARIABLE1
OUTER_SCOPE_VARIABLE2
ITER2_OBJ1 default
OUTER_SCOPE_VARIABLE2
ITER2_OBJ2 default
""",
),
# 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`
# shouldn't run. Hence even the inner `slot_inner` fill should NOT run.
("isolated", "OUTER_SCOPE_VARIABLE1 OUTER_SCOPE_VARIABLE1"),
]
@djc_test(
parametrize=(
["components_settings", "expected"],
[
[
{"context_behavior": "django"},
"""
OUTER_SCOPE_VARIABLE1
OUTER_SCOPE_VARIABLE2
ITER1_OBJ1 default
OUTER_SCOPE_VARIABLE2
ITER1_OBJ2 default
OUTER_SCOPE_VARIABLE1
OUTER_SCOPE_VARIABLE2
ITER2_OBJ1 default
OUTER_SCOPE_VARIABLE2
ITER2_OBJ2 default
""",
],
# 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`
# shouldn't run. Hence even the inner `slot_inner` fill should NOT run.
[{"context_behavior": "isolated"}, "OUTER_SCOPE_VARIABLE1 OUTER_SCOPE_VARIABLE1"],
],
["django", "isolated"],
)
)
def test_inner_slot_iteration_nested_with_slot_default_and_outer_scope_variable(
self,
context_behavior_data,
components_settings,
expected,
):
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(
self,
):
@ -551,7 +589,7 @@ class SlotIterationTest(BaseTestCase):
)
)
self.assertHTMLEqual(
assertHTMLEqual(
rendered,
"""
OUTER_SCOPE_VARIABLE1
@ -568,7 +606,8 @@ class SlotIterationTest(BaseTestCase):
)
class ComponentNestingTests(BaseTestCase):
@djc_test
class TestComponentNesting:
class CalendarComponent(Component):
"""Nested in ComponentWithNestedComponent"""
@ -604,17 +643,22 @@ class ComponentNestingTests(BaseTestCase):
</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,
# the value should be overridden by the component, while in "isolated" it should
# remain top-level context.
@parametrize_context_behavior([("django", ("Igor", "Joe2")), ("isolated", ("Jannete", "Jannete"))])
def test_component_inside_slot(self, context_behavior_data):
first_name, second_name = context_behavior_data
@djc_test(
parametrize=(
["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):
template: types.django_html = """
@ -657,7 +701,7 @@ class ComponentNestingTests(BaseTestCase):
self.template = Template(template_str)
rendered = self.template.render(Context({"day": "Monday", "name": "Jannete"}))
self.assertHTMLEqual(
assertHTMLEqual(
rendered,
f"""
<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.
@parametrize_context_behavior(
[
("django", "<li>1</li> <li>2</li> <li>3</li>"),
("isolated", ""),
]
@djc_test(
parametrize=(
["components_settings", "expected"],
[
[{"context_behavior": "django"}, "<li>1</li> <li>2</li> <li>3</li>"],
[{"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 = """
{% load component_tags %}
{% component "dashboard" %}{% endcomponent %}
@ -699,20 +750,27 @@ class ComponentNestingTests(BaseTestCase):
</main>
</div>
<ol>
{context_behavior_data}
{expected}
</ol>
</div>
"""
self.assertHTMLEqual(rendered, expected)
assertHTMLEqual(rendered, expected)
# NOTE: Second arg in tuple is expected list content. In isolated mode, loops should NOT leak.
@parametrize_context_behavior(
[
("django", "<li>1</li> <li>2</li> <li>3</li>"),
("isolated", ""),
]
@djc_test(
parametrize=(
["components_settings", "expected"],
[
[{"context_behavior": "django"}, "<li>1</li> <li>2</li> <li>3</li>"],
[{"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 = """
{% load component_tags %}
{% component "dashboard" %}
@ -734,14 +792,14 @@ class ComponentNestingTests(BaseTestCase):
</main>
</div>
<ol>
{context_behavior_data}
{expected}
</ol>
</div>
"""
self.assertHTMLEqual(rendered, expected)
assertHTMLEqual(rendered, expected)
@parametrize_context_behavior(["django", "isolated"])
def test_component_nesting_deep_slot_inside_component_fill(self):
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_component_nesting_deep_slot_inside_component_fill(self, components_settings):
@register("complex_child")
class ComplexChildComponent(Component):
template: types.django_html = """
@ -790,13 +848,13 @@ class ComponentNestingTests(BaseTestCase):
<div data-djc-id-a1bc44> 3 </div>
</li>
"""
self.assertHTMLEqual(rendered, expected)
assertHTMLEqual(rendered, expected)
# This test is based on real-life example.
# 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.
@parametrize_context_behavior(["django", "isolated"])
def test_component_nesting_deep_slot_inside_component_fill_2(self):
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_component_nesting_deep_slot_inside_component_fill_2(self, components_settings):
@register("TestPage")
class TestPage(Component):
template: types.django_html = """
@ -898,16 +956,23 @@ class ComponentNestingTests(BaseTestCase):
</body>
</html>
"""
self.assertHTMLEqual(rendered, expected)
assertHTMLEqual(rendered, expected)
# NOTE: Second arg in tuple is expected list content. In isolated mode, loops should NOT leak.
@parametrize_context_behavior(
[
("django", "<li>1</li> <li>2</li>"),
("isolated", ""),
]
@djc_test(
parametrize=(
["components_settings", "expected"],
[
[{"context_behavior": "django"}, "<li>1</li> <li>2</li>"],
[{"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 = """
{% load component_tags %}
{% component "dashboard" %}
@ -927,8 +992,8 @@ class ComponentNestingTests(BaseTestCase):
</main>
</div>
<ol>
{context_behavior_data}
{expected}
</ol>
</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_test_setup import setup_test_config
from .testutils import BaseTestCase
setup_test_config({"autodiscover": False})
class UtilsTest(BaseTestCase):
class TestUtils:
def test_is_str_wrapped_in_quotes(self):
self.assertEqual(is_str_wrapped_in_quotes("word"), False)
self.assertEqual(is_str_wrapped_in_quotes('word"'), False)
self.assertEqual(is_str_wrapped_in_quotes('"word'), False)
self.assertEqual(is_str_wrapped_in_quotes('"word"'), True)
self.assertEqual(is_str_wrapped_in_quotes("\"word'"), False)
self.assertEqual(is_str_wrapped_in_quotes('"word" '), False)
self.assertEqual(is_str_wrapped_in_quotes('"'), False)
self.assertEqual(is_str_wrapped_in_quotes(""), False)
self.assertEqual(is_str_wrapped_in_quotes('""'), True)
self.assertEqual(is_str_wrapped_in_quotes("\"'"), False)
assert is_str_wrapped_in_quotes("word") is False
assert is_str_wrapped_in_quotes('word"') is False
assert is_str_wrapped_in_quotes('"word') is False
assert is_str_wrapped_in_quotes('"word"') is True
assert is_str_wrapped_in_quotes("\"word'") is False
assert is_str_wrapped_in_quotes('"word" ') is False
assert is_str_wrapped_in_quotes('"') is False
assert is_str_wrapped_in_quotes("") is False
assert is_str_wrapped_in_quotes('""') is True
assert is_str_wrapped_in_quotes("\"'") is False

View file

@ -1,84 +1,31 @@
import contextlib
import functools
import sys
from typing import Any, Dict, List, Optional, Tuple, Union
from unittest.mock import Mock, patch
from pathlib import Path
from typing import Dict, Optional
from unittest.mock import Mock
from django.template import Context, Node
from django.template.loader import engines
import django
from django.conf import settings
from django.template import Context
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
# 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
response_stash = None
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()
mock_template = Mock()
@ -97,154 +44,50 @@ def create_and_process_template_response(template, context=None, use_middleware=
return response.content.decode("utf-8")
def print_nodes(nodes: List[Node], indent=0) -> None:
"""
Render a Nodelist, inlining child nodes with extra on separate lines and with
extra indentation.
"""
for node in nodes:
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)
def setup_test_config(
components: Optional[Dict] = None,
extra_settings: Optional[Dict] = None,
):
if settings.configured:
return
repr = str(node)
repr = "\n".join([(" " * 4 * indent) + line for line in repr.split("\n")])
print(repr)
if child_nodes:
print_nodes(child_nodes, indent=indent + 1)
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 {}),
}
)
# TODO: Make sure that this is done before/after each test automatically?
@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
django.setup()

View file

@ -34,7 +34,9 @@ deps =
djc-core-html-parser
pytest
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
# 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
@ -56,6 +58,8 @@ commands = isort --check-only --diff src/django_components
[testenv:coverage]
deps =
pytest-cov
pytest-django
pytest-asyncio
syrupy # snapshot testing
# NOTE: Keep playwright in sync with the version in requirements-ci.txt
playwright==1.48.0