mirror of
https://github.com/django-components/django-components.git
synced 2025-11-13 04:14:13 +00:00
refactor: add ErrorFallback (#1433)
This commit is contained in:
parent
eee3910b54
commit
60651f30b2
10 changed files with 603 additions and 11 deletions
31
CHANGELOG.md
31
CHANGELOG.md
|
|
@ -4,6 +4,37 @@
|
|||
|
||||
#### Feat
|
||||
|
||||
- New built-in component [`ErrorFallback`](https://django-components.github.io/django-components/0.142.0/reference/components/)
|
||||
|
||||
Use `ErrorFallback` to catch errors and display a fallback content instead.
|
||||
|
||||
This is similar to React's [`ErrorBoundary`](https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary)
|
||||
component.
|
||||
|
||||
Either pass the fallback as a kwarg:
|
||||
|
||||
```django
|
||||
{% component "error_fallback" fallback="Oops, something went wrong" %}
|
||||
{% component "table" / %}
|
||||
{% endcomponent %}
|
||||
```
|
||||
|
||||
Or use the full `fallback` slot:
|
||||
|
||||
```django
|
||||
{% component "error_fallback" %}
|
||||
{% fill "content" %}
|
||||
{% component "table" / %}
|
||||
{% endfill %}
|
||||
{% fill "fallback" data="data" %}
|
||||
<p>Oops, something went wrong</p>
|
||||
{% button href="/report-error" %}
|
||||
Report error
|
||||
{% endbutton %}
|
||||
{% endfill %}
|
||||
{% endcomponent %}
|
||||
```
|
||||
|
||||
- Wrap the template rendering in `Component.on_render()` in a lambda function.
|
||||
|
||||
When you wrap the rendering call in a lambda function, and the rendering fails,
|
||||
|
|
|
|||
|
|
@ -54,7 +54,8 @@ python manage.py components ext run <extension> <command>
|
|||
## `components create`
|
||||
|
||||
```txt
|
||||
usage: python manage.py components create [-h] [--path PATH] [--js JS] [--css CSS] [--template TEMPLATE] [--force] [--verbose] [--dry-run]
|
||||
usage: python manage.py components create [-h] [--path PATH] [--js JS] [--css CSS] [--template TEMPLATE] [--force] [--verbose]
|
||||
[--dry-run]
|
||||
name
|
||||
|
||||
```
|
||||
|
|
@ -463,7 +464,8 @@ ProjectDashboardAction project.components.dashboard_action.ProjectDashboardAc
|
|||
|
||||
```txt
|
||||
usage: upgradecomponent [-h] [--path PATH] [--version] [-v {0,1,2,3}] [--settings SETTINGS]
|
||||
[--pythonpath PYTHONPATH] [--traceback] [--no-color] [--force-color] [--skip-checks]
|
||||
[--pythonpath PYTHONPATH] [--traceback] [--no-color] [--force-color]
|
||||
[--skip-checks]
|
||||
|
||||
```
|
||||
|
||||
|
|
@ -507,9 +509,10 @@ Deprecated. Use `components upgrade` instead.
|
|||
## `startcomponent`
|
||||
|
||||
```txt
|
||||
usage: startcomponent [-h] [--path PATH] [--js JS] [--css CSS] [--template TEMPLATE] [--force] [--verbose]
|
||||
[--dry-run] [--version] [-v {0,1,2,3}] [--settings SETTINGS] [--pythonpath PYTHONPATH]
|
||||
[--traceback] [--no-color] [--force-color] [--skip-checks]
|
||||
usage: startcomponent [-h] [--path PATH] [--js JS] [--css CSS] [--template TEMPLATE] [--force]
|
||||
[--verbose] [--dry-run] [--version] [-v {0,1,2,3}] [--settings SETTINGS]
|
||||
[--pythonpath PYTHONPATH] [--traceback] [--no-color] [--force-color]
|
||||
[--skip-checks]
|
||||
name
|
||||
|
||||
```
|
||||
|
|
|
|||
|
|
@ -13,3 +13,11 @@ These are the components provided by django_components.
|
|||
separate_signature: false
|
||||
members: false
|
||||
|
||||
::: django_components.components.error_fallback.ErrorFallback
|
||||
options:
|
||||
inherited_members: false
|
||||
show_root_heading: true
|
||||
show_signature: false
|
||||
separate_signature: false
|
||||
members: false
|
||||
|
||||
|
|
|
|||
|
|
@ -170,7 +170,7 @@ COMPONENTS = {
|
|||
|
||||
|
||||
|
||||
<a href="https://github.com/django-components/django-components/tree/master/src/django_components/templatetags/component_tags.py#L988" target="_blank">See source code</a>
|
||||
<a href="https://github.com/django-components/django-components/tree/master/src/django_components/templatetags/component_tags.py#L990" target="_blank">See source code</a>
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ from django_components.util.routing import URLRoute, URLRouteHandler
|
|||
from django_components.util.types import Empty
|
||||
|
||||
# NOTE: Import built-in components last to avoid circular imports
|
||||
from django_components.components import DynamicComponent
|
||||
from django_components.components import DynamicComponent, ErrorFallback
|
||||
|
||||
# isort: on
|
||||
|
||||
|
|
@ -120,6 +120,7 @@ __all__ = [
|
|||
"DependenciesStrategy",
|
||||
"DynamicComponent",
|
||||
"Empty",
|
||||
"ErrorFallback",
|
||||
"ExtensionComponentConfig",
|
||||
"FillNode",
|
||||
"NotRegistered",
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ class ComponentsConfig(AppConfig):
|
|||
from django_components.autodiscovery import autodiscover, import_libraries
|
||||
from django_components.component_registry import registry
|
||||
from django_components.components.dynamic import DynamicComponent
|
||||
from django_components.components.error_fallback import ErrorFallback
|
||||
from django_components.extension import extensions
|
||||
from django_components.util.django_monkeypatch import (
|
||||
monkeypatch_include_node,
|
||||
|
|
@ -66,6 +67,7 @@ class ComponentsConfig(AppConfig):
|
|||
|
||||
# Register the dynamic component under the name as given in settings
|
||||
registry.register(app_settings.DYNAMIC_COMPONENT_NAME, DynamicComponent)
|
||||
registry.register("error_fallback", ErrorFallback)
|
||||
|
||||
# Let extensions process any components which may have been created before the app was ready
|
||||
extensions._init_app()
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
# NOTE: Components exported here are documented in the API reference
|
||||
from django_components.components.dynamic import DynamicComponent
|
||||
from django_components.components.error_fallback import ErrorFallback
|
||||
|
||||
__all__ = ["DynamicComponent"]
|
||||
__all__ = ["DynamicComponent", "ErrorFallback"]
|
||||
|
|
|
|||
156
src/django_components/components/error_fallback.py
Normal file
156
src/django_components/components/error_fallback.py
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
from typing import NamedTuple, Optional, cast
|
||||
|
||||
from django.template import Context, Template
|
||||
from django.template.exceptions import TemplateSyntaxError
|
||||
|
||||
from django_components import Component, OnRenderGenerator, SlotInput, types
|
||||
|
||||
|
||||
class ErrorFallback(Component):
|
||||
"""
|
||||
Use `ErrorFallback` to catch errors and display a fallback content instead.
|
||||
|
||||
This is similar to React's
|
||||
[`ErrorBoundary`](https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary)
|
||||
component.
|
||||
|
||||
**Example:**
|
||||
|
||||
Given this template:
|
||||
|
||||
```django
|
||||
{% component "error_fallback" fallback="Oops, something went wrong" %}
|
||||
{% component "table" / %}
|
||||
{% endcomponent %}
|
||||
```
|
||||
|
||||
Then:
|
||||
|
||||
- If the `table` component does NOT raise an error, then the table is rendered as normal.
|
||||
- If the `table` component DOES raise an error, then `error_fallback` renders `Oops, something went wrong`.
|
||||
|
||||
To have more control over the fallback content, you can use the `fallback` slot
|
||||
instead of the `fallback` kwarg.
|
||||
|
||||
```django
|
||||
{% component "error_fallback" %}
|
||||
{% fill "content" %}
|
||||
{% component "table" / %}
|
||||
{% endfill %}
|
||||
{% fill "fallback" %}
|
||||
<p>Oops, something went wrong</p>
|
||||
{% button href="/report-error" %}
|
||||
Report error
|
||||
{% endbutton %}
|
||||
{% endfill %}
|
||||
{% endcomponent %}
|
||||
```
|
||||
|
||||
If you want to print the error, you can access the `error` variable
|
||||
as [slot data](../../concepts/fundamentals/slots/#slot-data).
|
||||
|
||||
```django
|
||||
{% component "error_fallback" %}
|
||||
{% fill "content" %}
|
||||
{% component "table" / %}
|
||||
{% endfill %}
|
||||
{% fill "fallback" data="data" %}
|
||||
Oops, something went wrong:
|
||||
<pre>{{ data.error }}</pre>
|
||||
{% endfill %}
|
||||
{% endcomponent %}
|
||||
```
|
||||
|
||||
**Python:**
|
||||
|
||||
With fallback kwarg:
|
||||
|
||||
```py
|
||||
from django_components import ErrorFallback
|
||||
|
||||
ErrorFallback.render(
|
||||
slots={
|
||||
# Main content
|
||||
"content": lambda ctx: TableComponent.render(
|
||||
deps_strategy="ignore",
|
||||
),
|
||||
},
|
||||
kwargs={
|
||||
# Fallback content
|
||||
"fallback": "Oops, something went wrong",
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
With fallback slot:
|
||||
|
||||
```py
|
||||
from django_components import ErrorFallback
|
||||
|
||||
ErrorFallback.render(
|
||||
slots={
|
||||
# Main content
|
||||
"content": lambda ctx: TableComponent.render(
|
||||
deps_strategy="ignore",
|
||||
),
|
||||
# Fallback content
|
||||
"fallback": lambda ctx: mark_safe("Oops, something went wrong: " + ctx.error),
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
!!! info
|
||||
|
||||
Remember to define the `content` slot as function, so it's evaluated from inside of `ErrorFallback`.
|
||||
"""
|
||||
|
||||
class Kwargs(NamedTuple):
|
||||
fallback: Optional[str] = None
|
||||
|
||||
class Slots(NamedTuple):
|
||||
default: Optional[SlotInput] = None
|
||||
content: Optional[SlotInput] = None
|
||||
fallback: Optional[SlotInput] = None
|
||||
|
||||
def on_render(
|
||||
self,
|
||||
context: Context,
|
||||
template: Optional[Template],
|
||||
) -> OnRenderGenerator:
|
||||
if template is None:
|
||||
raise TemplateSyntaxError("The 'error_fallback' component must have a template.")
|
||||
|
||||
fallback_kwarg = cast("ErrorFallback.Kwargs", self.kwargs).fallback
|
||||
fallback_slot = cast("ErrorFallback.Slots", self.slots).fallback
|
||||
|
||||
if fallback_kwarg is not None and fallback_slot is not None:
|
||||
raise TemplateSyntaxError(
|
||||
"The 'fallback' argument and slot cannot both be provided. Please provide only one.",
|
||||
)
|
||||
|
||||
result, error = yield lambda: template.render(context)
|
||||
|
||||
# No error, return the result
|
||||
if error is None:
|
||||
return result
|
||||
|
||||
# Error, return the fallback
|
||||
if fallback_kwarg is not None:
|
||||
return fallback_kwarg
|
||||
elif fallback_slot is not None:
|
||||
# Render the template second time, this time with the error
|
||||
# So that we render the fallback slot with proper access to the outer context and whatnot.
|
||||
with context.push({"error": error}):
|
||||
return template.render(context)
|
||||
else:
|
||||
return ""
|
||||
|
||||
# TODO - Once we don't have to pass Context to the slot, we can remove the template
|
||||
# and render the slots directly.
|
||||
template: types.django_html = """
|
||||
{% if not error %}
|
||||
{% slot "content" default / %}
|
||||
{% else %}
|
||||
{% slot "fallback" error=error / %}
|
||||
{% endif %}
|
||||
"""
|
||||
388
tests/test_component_error_fallback.py
Normal file
388
tests/test_component_error_fallback.py
Normal file
|
|
@ -0,0 +1,388 @@
|
|||
from typing import NamedTuple
|
||||
|
||||
import pytest
|
||||
from django.template import Context, Template
|
||||
from django.template.exceptions import TemplateSyntaxError
|
||||
from pytest_django.asserts import assertHTMLEqual
|
||||
|
||||
from django_components import Component, ErrorFallback, register, types
|
||||
from django_components.testing import djc_test
|
||||
|
||||
from .testutils import PARAMETRIZE_CONTEXT_BEHAVIOR, setup_test_config
|
||||
|
||||
setup_test_config({"autodiscover": False})
|
||||
|
||||
|
||||
@djc_test
|
||||
class TestErrorFallbackComponent:
|
||||
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
||||
def test_basic__python(self, components_settings):
|
||||
# 1. Content does not raise, fallback present
|
||||
rendered1 = ErrorFallback.render(
|
||||
slots={
|
||||
"content": lambda _data: "SAFE CONTENT",
|
||||
"fallback": lambda _data: "FALLBACK CONTENT",
|
||||
},
|
||||
)
|
||||
assert rendered1.strip() == "SAFE CONTENT"
|
||||
|
||||
# 2. Content raises, fallback present
|
||||
def error_content(_ctx):
|
||||
raise Exception("fail!")
|
||||
|
||||
rendered2 = ErrorFallback.render(
|
||||
slots={
|
||||
"content": error_content,
|
||||
"fallback": lambda _data: "FALLBACK CONTENT",
|
||||
},
|
||||
)
|
||||
assert rendered2.strip() == "FALLBACK CONTENT"
|
||||
|
||||
# 3. Content raises, fallback missing - valid
|
||||
rendered3 = ErrorFallback.render(
|
||||
slots={
|
||||
"content": error_content,
|
||||
},
|
||||
)
|
||||
assert rendered3.strip() == ""
|
||||
|
||||
# 4. Same as 3., but with default slot
|
||||
rendered4 = ErrorFallback.render(
|
||||
slots={
|
||||
"default": error_content,
|
||||
},
|
||||
)
|
||||
assert rendered4.strip() == ""
|
||||
|
||||
# 5. Content missing, fallback present - valid
|
||||
rendered5 = ErrorFallback.render(
|
||||
slots={
|
||||
"fallback": lambda _ctx: "FALLBACK CONTENT",
|
||||
},
|
||||
)
|
||||
assert rendered5.strip() == ""
|
||||
|
||||
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
||||
def test_basic__template(self, components_settings):
|
||||
@register("broken")
|
||||
class BrokenComponent(Component):
|
||||
def on_render(self, context: Context, template: Template):
|
||||
raise Exception("fail!")
|
||||
|
||||
# 1. Content does not raise, fallback present
|
||||
template_str1: types.django_html = """
|
||||
{% load component_tags %}
|
||||
{% component "error_fallback" %}
|
||||
{% fill "content" %}SAFE CONTENT{% endfill %}
|
||||
{% fill "fallback" %}FALLBACK CONTENT{% endfill %}
|
||||
{% endcomponent %}
|
||||
"""
|
||||
template1 = Template(template_str1)
|
||||
rendered1 = template1.render(Context({}))
|
||||
|
||||
assert "SAFE CONTENT" in rendered1
|
||||
assert "FALLBACK CONTENT" not in rendered1
|
||||
|
||||
# 2. Content raises, fallback present
|
||||
template_str2: types.django_html = """
|
||||
{% load component_tags %}
|
||||
{% component "error_fallback" %}
|
||||
{% fill "content" %}
|
||||
{% component "broken" / %}
|
||||
{% endfill %}
|
||||
{% fill "fallback" %}
|
||||
FALLBACK CONTENT
|
||||
{% endfill %}
|
||||
{% endcomponent %}
|
||||
"""
|
||||
template2 = Template(template_str2)
|
||||
rendered2 = template2.render(Context({}))
|
||||
assert "FALLBACK CONTENT" in rendered2
|
||||
|
||||
# 3. Content raises, fallback missing - valid
|
||||
template_str3: types.django_html = """
|
||||
{% load component_tags %}
|
||||
{% component "error_fallback" %}
|
||||
{% fill "content" %}
|
||||
{% component "broken" / %}
|
||||
{% endfill %}
|
||||
{% endcomponent %}
|
||||
"""
|
||||
template3 = Template(template_str3)
|
||||
rendered3 = template3.render(Context({}))
|
||||
assert rendered3.strip() == ""
|
||||
|
||||
# 4. Same as 3., but with default slot
|
||||
template_str4: types.django_html = """
|
||||
{% load component_tags %}
|
||||
{% component "error_fallback" %}
|
||||
{% component "broken" / %}
|
||||
{% endcomponent %}
|
||||
"""
|
||||
template4 = Template(template_str4)
|
||||
rendered4 = template4.render(Context({}))
|
||||
assert rendered4.strip() == ""
|
||||
|
||||
# 5. Content missing, fallback present - valid
|
||||
template_str5: types.django_html = """
|
||||
{% load component_tags %}
|
||||
{% component "error_fallback" %}
|
||||
{% fill "fallback" %}FALLBACK CONTENT{% endfill %}
|
||||
{% endcomponent %}
|
||||
"""
|
||||
template5 = Template(template_str5)
|
||||
rendered5 = template5.render(Context({}))
|
||||
assert rendered5.strip() == ""
|
||||
|
||||
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
||||
def test_component_called_with_default_slot(self, components_settings):
|
||||
@register("test")
|
||||
class SimpleSlottedComponent(Component):
|
||||
template: types.django_html = """
|
||||
{% load component_tags %}
|
||||
Variable: <strong>{{ variable }}</strong>
|
||||
Slot: {% slot "default" default / %}
|
||||
"""
|
||||
|
||||
def get_template_data(self, args, kwargs, slots, context):
|
||||
return {
|
||||
"variable": kwargs["variable"],
|
||||
"variable2": kwargs.get("variable2", "default"),
|
||||
}
|
||||
|
||||
simple_tag_template: types.django_html = """
|
||||
{% load component_tags %}
|
||||
{% with component_name="test" %}
|
||||
{% component "dynamic" is=component_name variable="variable" %}
|
||||
HELLO_FROM_SLOT
|
||||
{% endcomponent %}
|
||||
{% endwith %}
|
||||
"""
|
||||
|
||||
template = Template(simple_tag_template)
|
||||
rendered = template.render(Context({}))
|
||||
assertHTMLEqual(
|
||||
rendered,
|
||||
"""
|
||||
Variable: <strong data-djc-id-ca1bc3f data-djc-id-ca1bc40>variable</strong>
|
||||
Slot: HELLO_FROM_SLOT
|
||||
""",
|
||||
)
|
||||
|
||||
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
||||
def test_fallback_as_kwarg__python(self, components_settings):
|
||||
# Content does not raise, fallback kwarg present
|
||||
rendered1 = ErrorFallback.render(
|
||||
slots={
|
||||
"content": lambda _ctx: "SAFE CONTENT",
|
||||
},
|
||||
kwargs={
|
||||
"fallback": "FALLBACK CONTENT",
|
||||
},
|
||||
)
|
||||
assert rendered1.strip() == "SAFE CONTENT"
|
||||
|
||||
# Content raises, fallback kwarg present
|
||||
def error_content(_ctx):
|
||||
raise Exception("fail!")
|
||||
|
||||
rendered2 = ErrorFallback.render(
|
||||
slots={
|
||||
"content": error_content,
|
||||
},
|
||||
kwargs={
|
||||
"fallback": "FALLBACK CONTENT",
|
||||
},
|
||||
)
|
||||
assert rendered2.strip() == "FALLBACK CONTENT"
|
||||
|
||||
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
||||
def test_fallback_as_kwarg__template(self, components_settings):
|
||||
@register("broken")
|
||||
class BrokenComponent(Component):
|
||||
def on_render(self, context: Context, template: Template):
|
||||
raise Exception("fail!")
|
||||
|
||||
# Content does not raise, fallback kwarg present
|
||||
template_str1: types.django_html = """
|
||||
{% load component_tags %}
|
||||
{% component "error_fallback" fallback="FALLBACK CONTENT" %}
|
||||
SAFE CONTENT
|
||||
{% endcomponent %}
|
||||
"""
|
||||
template1 = Template(template_str1)
|
||||
rendered1 = template1.render(Context({}))
|
||||
assert "SAFE CONTENT" in rendered1
|
||||
assert "FALLBACK CONTENT" not in rendered1
|
||||
|
||||
# Content raises, fallback kwarg present
|
||||
template_str2: types.django_html = """
|
||||
{% load component_tags %}
|
||||
{% component "error_fallback" fallback="FALLBACK CONTENT" %}
|
||||
{% component "broken" / %}
|
||||
{% endcomponent %}
|
||||
"""
|
||||
template2 = Template(template_str2)
|
||||
rendered2 = template2.render(Context({}))
|
||||
assert "FALLBACK CONTENT" in rendered2
|
||||
|
||||
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
||||
def test_raises_on_fallback_as_both_slot_and_kwarg(self, components_settings):
|
||||
# Python API: fallback as both slot and kwarg
|
||||
with pytest.raises(
|
||||
TemplateSyntaxError,
|
||||
match=r"The 'fallback' argument and slot cannot both be provided. Please provide only one.",
|
||||
):
|
||||
ErrorFallback.render(
|
||||
slots={
|
||||
"content": lambda _ctx: "SAFE CONTENT",
|
||||
"fallback": lambda _ctx: "FALLBACK CONTENT",
|
||||
},
|
||||
kwargs={
|
||||
"fallback": "FALLBACK CONTENT",
|
||||
},
|
||||
)
|
||||
|
||||
# Template API: fallback as both slot and kwarg
|
||||
template_str: types.django_html = """
|
||||
{% load component_tags %}
|
||||
{% component "error_fallback" fallback="FALLBACK CONTENT" %}
|
||||
{% fill "fallback" %}FALLBACK CONTENT{% endfill %}
|
||||
{% endcomponent %}
|
||||
"""
|
||||
template = Template(template_str)
|
||||
with pytest.raises(
|
||||
TemplateSyntaxError,
|
||||
match=r"The 'fallback' argument and slot cannot both be provided. Please provide only one.",
|
||||
):
|
||||
template.render(Context({}))
|
||||
|
||||
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
||||
def test_error_fallback_inside_loop(self, components_settings):
|
||||
@register("sometimes_broken")
|
||||
class SometimesBrokenComponent(Component):
|
||||
template: types.django_html = """
|
||||
Item: {{ item_name }}
|
||||
"""
|
||||
|
||||
def get_template_data(self, args, kwargs, slots, context):
|
||||
return {
|
||||
"item_name": kwargs.get("item_name", "default"),
|
||||
}
|
||||
|
||||
def on_render(self, context: Context, template: Template):
|
||||
if self.kwargs.get("should_break", False):
|
||||
raise Exception("fail!")
|
||||
return super().on_render(context, template)
|
||||
|
||||
# Test error fallback inside a loop with some items failing
|
||||
template_str: types.django_html = """
|
||||
{% load component_tags %}
|
||||
{% for item in items %}
|
||||
{% component "error_fallback" %}
|
||||
{% fill "content" %}
|
||||
{% component "sometimes_broken" item_name=item.name should_break=item.should_break / %}
|
||||
{% endfill %}
|
||||
{% fill "fallback" %}
|
||||
ERROR: Failed to render {{ item.name }}
|
||||
{% endfill %}
|
||||
{% endcomponent %}
|
||||
{% endfor %}
|
||||
"""
|
||||
|
||||
template = Template(template_str)
|
||||
context_data = {
|
||||
"items": [
|
||||
{"name": "item1", "should_break": False},
|
||||
{"name": "item2", "should_break": True},
|
||||
{"name": "item3", "should_break": False},
|
||||
{"name": "item4", "should_break": True},
|
||||
]
|
||||
}
|
||||
rendered = template.render(Context(context_data))
|
||||
|
||||
expected = """
|
||||
Item: item1
|
||||
ERROR: Failed to render item2
|
||||
Item: item3
|
||||
ERROR: Failed to render item4
|
||||
"""
|
||||
assertHTMLEqual(rendered, expected)
|
||||
|
||||
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
||||
def test_error_fallback_nested_inside_another(self, components_settings):
|
||||
@register("broken")
|
||||
class BrokenComponent(Component):
|
||||
class Kwargs(NamedTuple):
|
||||
msg: str
|
||||
|
||||
def on_render(self, context: Context, template: Template):
|
||||
raise Exception(self.kwargs.msg)
|
||||
|
||||
# Test nested error fallback components
|
||||
template_str: types.django_html = """
|
||||
{% load component_tags %}
|
||||
{% component "error_fallback" %}
|
||||
{% fill "content" %}
|
||||
{% if should_outer_fail %}
|
||||
{% component "broken" msg="OUTER_FAIL" / %}
|
||||
{% endif %}
|
||||
OUTER_CONTENT_START
|
||||
{% component "error_fallback" %}
|
||||
{% fill "content" %}
|
||||
{% component "broken" msg="INNER_FAIL" / %}
|
||||
{% endfill %}
|
||||
{% fill "fallback" data="data" %}
|
||||
{% if should_inner_fallback_fail %}
|
||||
{% component "broken" msg="INNER_FALLBACK_FAIL" / %}
|
||||
{% else %}
|
||||
INNER_FALLBACK: {{ data.error }}
|
||||
{% endif %}
|
||||
{% endfill %}
|
||||
{% endcomponent %}
|
||||
OUTER_CONTENT_END
|
||||
{% endfill %}
|
||||
{% fill "fallback" data="data" %}
|
||||
OUTER_FALLBACK: {{ data.error }}
|
||||
{% endfill %}
|
||||
{% endcomponent %}
|
||||
"""
|
||||
template = Template(template_str)
|
||||
|
||||
rendered1 = template.render(Context({}))
|
||||
if components_settings["context_behavior"] == "django":
|
||||
expected1 = """
|
||||
OUTER_CONTENT_START
|
||||
INNER_FALLBACK: An error occured while rendering components error_fallback > broken: INNER_FAIL
|
||||
OUTER_CONTENT_END
|
||||
"""
|
||||
else:
|
||||
expected1 = """
|
||||
OUTER_CONTENT_START
|
||||
INNER_FALLBACK: An error occured while rendering components error_fallback(slot:content) > broken: INNER_FAIL
|
||||
OUTER_CONTENT_END
|
||||
""" # noqa: E501
|
||||
assertHTMLEqual(rendered1, expected1)
|
||||
|
||||
rendered2 = template.render(Context({"should_outer_fail": True}))
|
||||
if components_settings["context_behavior"] == "django":
|
||||
expected2 = """
|
||||
OUTER_FALLBACK: An error occured while rendering components broken: OUTER_FAIL
|
||||
"""
|
||||
else:
|
||||
expected2 = """
|
||||
OUTER_FALLBACK: An error occured while rendering components error_fallback(slot:content) > broken: OUTER_FAIL
|
||||
""" # noqa: E501
|
||||
assertHTMLEqual(rendered2, expected2)
|
||||
|
||||
# Test when inner fallback also fails
|
||||
rendered3 = template.render(Context({"should_inner_fallback_fail": True}))
|
||||
if components_settings["context_behavior"] == "django":
|
||||
expected3 = """
|
||||
OUTER_FALLBACK: An error occured while rendering components error_fallback > broken > broken: INNER_FALLBACK_FAIL
|
||||
""" # noqa: E501
|
||||
else:
|
||||
expected3 = """
|
||||
OUTER_FALLBACK: An error occured while rendering components error_fallback(slot:content) > error_fallback > error_fallback(slot:fallback) > broken: INNER_FALLBACK_FAIL
|
||||
""" # noqa: E501
|
||||
assertHTMLEqual(rendered3, expected3)
|
||||
|
|
@ -250,6 +250,7 @@ class TestComponentFiles:
|
|||
"components.urls",
|
||||
"django_components.components",
|
||||
"django_components.components.dynamic",
|
||||
"django_components.components.error_fallback",
|
||||
"tests.test_app.components.app_lvl_comp.app_lvl_comp",
|
||||
]
|
||||
|
||||
|
|
@ -264,7 +265,8 @@ class TestComponentFiles:
|
|||
assert file_paths[7].parts[-3:] == ("tests", "components", "urls.py")
|
||||
assert file_paths[8].parts[-3:] == ("django_components", "components", "__init__.py")
|
||||
assert file_paths[9].parts[-3:] == ("django_components", "components", "dynamic.py")
|
||||
assert file_paths[10].parts[-5:] == ("tests", "test_app", "components", "app_lvl_comp", "app_lvl_comp.py")
|
||||
assert file_paths[10].parts[-3:] == ("django_components", "components", "error_fallback.py")
|
||||
assert file_paths[11].parts[-5:] == ("tests", "test_app", "components", "app_lvl_comp", "app_lvl_comp.py")
|
||||
|
||||
@djc_test(
|
||||
django_settings={
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue