mirror of
https://github.com/django-components/django-components.git
synced 2025-11-25 00:19:39 +00:00
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:
parent
81ac59f7fb
commit
7dfcb447c4
62 changed files with 4428 additions and 3661 deletions
18
CHANGELOG.md
18
CHANGELOG.md
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
111
docs/concepts/advanced/testing.md
Normal file
111
docs/concepts/advanced/testing.md
Normal 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()
|
||||
...
|
||||
```
|
||||
|
|
@ -11,3 +11,4 @@ nav:
|
|||
- Template tags: template_tags.md
|
||||
- Template vars: template_vars.md
|
||||
- URLs: urls.md
|
||||
- Testing API: testing_api.md
|
||||
|
|
|
|||
9
docs/reference/testing_api.md
Normal file
9
docs/reference/testing_api.md
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<!-- Autogenerated by reference.py -->
|
||||
|
||||
# Testing API
|
||||
|
||||
|
||||
::: django_components.testing.djc_test
|
||||
options:
|
||||
show_if_no_docstring: true
|
||||
|
||||
|
|
@ -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
|
||||
|
|
|
|||
1
docs/templates/reference_testing_api.md
vendored
Normal file
1
docs/templates/reference_testing_api.md
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
# Testing API
|
||||
|
|
@ -109,6 +109,7 @@ disallow_untyped_defs = true
|
|||
testpaths = [
|
||||
"tests"
|
||||
]
|
||||
asyncio_mode = "auto"
|
||||
|
||||
[tool.hatch.env]
|
||||
requires = [
|
||||
|
|
|
|||
|
|
@ -4,4 +4,6 @@ playwright
|
|||
requests
|
||||
types-requests
|
||||
whitenoise
|
||||
asv
|
||||
asv
|
||||
pytest-asyncio
|
||||
pytest-django
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ django
|
|||
djc-core-html-parser
|
||||
tox
|
||||
pytest
|
||||
pytest-asyncio
|
||||
pytest-django
|
||||
syrupy
|
||||
flake8
|
||||
flake8-pyproject
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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"]:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
9
src/django_components/testing.py
Normal file
9
src/django_components/testing.py
Normal 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",
|
||||
]
|
||||
525
src/django_components/util/testing.py
Normal file
525
src/django_components/util/testing.py
Normal 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
|
||||
29
src/django_components/util/weakref.py
Normal file
29
src/django_components/util/weakref.py
Normal 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]
|
||||
|
|
@ -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()
|
||||
|
|
@ -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="'baz'"',
|
||||
)
|
||||
assert attributes_to_string({"x-on:click": "bar", "@click": "'baz'"}) == 'x-on:click="bar" @click="'baz'"' # 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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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': '() => {}', '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': '() => {}', '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': '() => {}', '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': '() => {}', '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")
|
||||
|
|
|
|||
|
|
@ -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 = """
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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>© 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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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({}))
|
||||
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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'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'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)
|
||||
|
|
|
|||
|
|
@ -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 %}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
6
tox.ini
6
tox.ini
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue