312 lines
11 KiB
Python
312 lines
11 KiB
Python
|
|
import os
|
||
|
|
import random
|
||
|
|
|
||
|
|
from pathlib import Path
|
||
|
|
from typing import TYPE_CHECKING
|
||
|
|
|
||
|
|
from django.conf import settings
|
||
|
|
from django.forms import widgets
|
||
|
|
from django.template import Context
|
||
|
|
from django.test import SimpleTestCase
|
||
|
|
from django.utils.safestring import SafeString
|
||
|
|
|
||
|
|
from laces.components import Component, MediaContainer
|
||
|
|
from laces.tests.utils import MediaAssertionMixin
|
||
|
|
|
||
|
|
|
||
|
|
if TYPE_CHECKING:
|
||
|
|
from typing import Optional
|
||
|
|
|
||
|
|
from laces.typing import RenderContext
|
||
|
|
|
||
|
|
|
||
|
|
class TestComponent(MediaAssertionMixin, SimpleTestCase):
|
||
|
|
"""Directly test the Component class."""
|
||
|
|
|
||
|
|
def setUp(self) -> None:
|
||
|
|
self.component = Component()
|
||
|
|
|
||
|
|
def test_render_html(self) -> None:
|
||
|
|
"""Test the `render_html` method."""
|
||
|
|
# The default Component does not specify a `template_name` attribute which is
|
||
|
|
# required for `render_html`. So calling the method on the Component class
|
||
|
|
# will raise an error.
|
||
|
|
with self.assertRaises(AttributeError):
|
||
|
|
self.component.render_html()
|
||
|
|
|
||
|
|
def test_get_context_data_with_parent_context_empty_context(self) -> None:
|
||
|
|
"""
|
||
|
|
Test the default `get_context_data`.
|
||
|
|
|
||
|
|
The parent context should not matter, but we use it as it is used in
|
||
|
|
`render_html` (which passes a `Context` object).
|
||
|
|
"""
|
||
|
|
result = self.component.get_context_data(parent_context=Context())
|
||
|
|
|
||
|
|
self.assertIsInstance(result, dict)
|
||
|
|
self.assertEqual(result, {})
|
||
|
|
|
||
|
|
def test_get_context_data_with_parent_context_none(self) -> None:
|
||
|
|
"""Test the default `get_context_data` when received `parent_context=None`."""
|
||
|
|
result = self.component.get_context_data(parent_context=None)
|
||
|
|
|
||
|
|
self.assertIsInstance(result, dict)
|
||
|
|
self.assertEqual(result, {})
|
||
|
|
|
||
|
|
def test_get_context_data_without_parent_context_argument(self) -> None:
|
||
|
|
"""Test the default `get_context_data` when not passing `parent_context`."""
|
||
|
|
result = self.component.get_context_data()
|
||
|
|
|
||
|
|
self.assertIsInstance(result, dict)
|
||
|
|
self.assertEqual(result, {})
|
||
|
|
|
||
|
|
def test_media(self) -> None:
|
||
|
|
"""
|
||
|
|
Test the `media` property.
|
||
|
|
|
||
|
|
The `media` property is added through the `metaclass=MediaDefiningClass`
|
||
|
|
definition.
|
||
|
|
|
||
|
|
"""
|
||
|
|
empty_media = widgets.Media()
|
||
|
|
self.assertIsInstance(self.component.media, widgets.Media)
|
||
|
|
self.assertMediaEqual(self.component.media, empty_media)
|
||
|
|
|
||
|
|
|
||
|
|
class TestComponentSubclasses(MediaAssertionMixin, SimpleTestCase):
|
||
|
|
"""
|
||
|
|
Test the Component class through subclasses.
|
||
|
|
|
||
|
|
Most functionality of the Component class is only unlocked through subclassing and
|
||
|
|
definition of certain attributes (like `template_name`) or overriding of the
|
||
|
|
existing methods. This test class tests the functionality that is unlocked through
|
||
|
|
subclassing.
|
||
|
|
"""
|
||
|
|
|
||
|
|
@classmethod
|
||
|
|
def make_example_template_name(cls) -> str:
|
||
|
|
return f"example-{random.randint(1000, 10000)}.html"
|
||
|
|
|
||
|
|
@classmethod
|
||
|
|
def get_example_template_name(cls) -> str:
|
||
|
|
example_template_name = cls.make_example_template_name()
|
||
|
|
while os.path.exists(example_template_name):
|
||
|
|
example_template_name = cls.make_example_template_name()
|
||
|
|
return example_template_name
|
||
|
|
|
||
|
|
def setUp(self) -> None:
|
||
|
|
self.example_template_name = self.get_example_template_name()
|
||
|
|
self.example_template = (
|
||
|
|
Path(settings.PROJECT_DIR) / "templates" / self.example_template_name
|
||
|
|
)
|
||
|
|
# Write content to the template file to ensure it exists.
|
||
|
|
self.set_example_template_content("")
|
||
|
|
|
||
|
|
def set_example_template_content(self, content: str) -> None:
|
||
|
|
with open(self.example_template, "w") as f:
|
||
|
|
f.write(content)
|
||
|
|
|
||
|
|
def test_render_html_with_template_name_set(self) -> None:
|
||
|
|
"""
|
||
|
|
Test `render_html` method with a set `template_name` attribute.
|
||
|
|
"""
|
||
|
|
|
||
|
|
# -----------------------------------------------------------------------------
|
||
|
|
class ExampleComponent(Component):
|
||
|
|
template_name = self.example_template_name
|
||
|
|
|
||
|
|
# -----------------------------------------------------------------------------
|
||
|
|
|
||
|
|
self.set_example_template_content("Test")
|
||
|
|
|
||
|
|
result = ExampleComponent().render_html()
|
||
|
|
|
||
|
|
self.assertIsInstance(result, str)
|
||
|
|
self.assertIsInstance(result, SafeString)
|
||
|
|
self.assertEqual(result, "Test")
|
||
|
|
|
||
|
|
def test_render_html_with_template_name_set_and_data_from_get_context_data(
|
||
|
|
self,
|
||
|
|
) -> None:
|
||
|
|
"""
|
||
|
|
Test `render_html` method with `get_context_data` providing data for the
|
||
|
|
context.
|
||
|
|
"""
|
||
|
|
|
||
|
|
# -----------------------------------------------------------------------------
|
||
|
|
class ExampleComponent(Component):
|
||
|
|
template_name = self.example_template_name
|
||
|
|
|
||
|
|
def get_context_data(
|
||
|
|
self,
|
||
|
|
parent_context: "Optional[RenderContext]" = None,
|
||
|
|
) -> "RenderContext":
|
||
|
|
"""Return a context dict with fixed content."""
|
||
|
|
return {"name": "World"}
|
||
|
|
|
||
|
|
# -----------------------------------------------------------------------------
|
||
|
|
|
||
|
|
self.set_example_template_content("Hello {{ name }}")
|
||
|
|
|
||
|
|
result = ExampleComponent().render_html()
|
||
|
|
|
||
|
|
self.assertEqual(result, "Hello World")
|
||
|
|
|
||
|
|
def test_render_html_when_get_context_data_returns_none(self) -> None:
|
||
|
|
"""
|
||
|
|
Test `render_html` method when `get_context_data` returns `None`.
|
||
|
|
|
||
|
|
Originally, the `render_html` method explicitly raised a `TypeError` when
|
||
|
|
`None` was returned from `get_context_method`.
|
||
|
|
|
||
|
|
I was not able to find out why this check was put in place. The usage of
|
||
|
|
components in Wagtail does not reveal any issues when the raising of the
|
||
|
|
exception is removed. Also, the following template rendering (with
|
||
|
|
`django.template.base.Template.render`) works just fine with the context being
|
||
|
|
`None`.
|
||
|
|
|
||
|
|
It seems therefore safe to assume that this was a left-over without much current
|
||
|
|
need.
|
||
|
|
|
||
|
|
This test is in place to prove that a component can behave as expected when the
|
||
|
|
`get_context_data` method returns `None`.
|
||
|
|
"""
|
||
|
|
|
||
|
|
# -----------------------------------------------------------------------------
|
||
|
|
class ExampleComponent(Component):
|
||
|
|
template_name = self.example_template_name
|
||
|
|
|
||
|
|
def get_context_data(
|
||
|
|
self,
|
||
|
|
parent_context: "Optional[RenderContext]" = None,
|
||
|
|
) -> None:
|
||
|
|
"""Return `None` as the context data."""
|
||
|
|
return None
|
||
|
|
|
||
|
|
# -----------------------------------------------------------------------------
|
||
|
|
self.set_example_template_content("Hello")
|
||
|
|
|
||
|
|
result = ExampleComponent().render_html()
|
||
|
|
|
||
|
|
self.assertEqual(result, "Hello")
|
||
|
|
|
||
|
|
def test_media_defined_through_nested_class(self) -> None:
|
||
|
|
"""
|
||
|
|
Test the `media` property when defined through a nested class.
|
||
|
|
|
||
|
|
The `media` property is added through the `metaclass=MediaDefiningClass`
|
||
|
|
definition. This test ensures that the `media` property is available when
|
||
|
|
configured through a nested class.
|
||
|
|
"""
|
||
|
|
|
||
|
|
# -----------------------------------------------------------------------------
|
||
|
|
class ExampleComponent(Component):
|
||
|
|
class Media:
|
||
|
|
css = {"all": ["example.css"]}
|
||
|
|
js = ["example.js"]
|
||
|
|
|
||
|
|
# -----------------------------------------------------------------------------
|
||
|
|
|
||
|
|
result = ExampleComponent().media
|
||
|
|
|
||
|
|
self.assertIsInstance(result, widgets.Media)
|
||
|
|
self.assertMediaEqual(
|
||
|
|
result,
|
||
|
|
widgets.Media(css={"all": ["example.css"]}, js=["example.js"]),
|
||
|
|
)
|
||
|
|
|
||
|
|
def tearDown(self) -> None:
|
||
|
|
os.remove(path=self.example_template)
|
||
|
|
|
||
|
|
|
||
|
|
class TestMediaContainer(MediaAssertionMixin, SimpleTestCase):
|
||
|
|
"""
|
||
|
|
Test the MediaContainer class.
|
||
|
|
|
||
|
|
The `MediaContainer` functionality depends on the `django.forms.widgets.Media`
|
||
|
|
class. The `Media` class provides the logic to combine the media definitions of
|
||
|
|
multiple objects through its `__add__` method. The `MediaContainer` relies on this
|
||
|
|
functionality to provide a `media` property that combines the media definitions of
|
||
|
|
its members.
|
||
|
|
|
||
|
|
See also:
|
||
|
|
https://docs.djangoproject.com/en/4.2/topics/forms/media
|
||
|
|
"""
|
||
|
|
|
||
|
|
def setUp(self) -> None:
|
||
|
|
self.media_container = MediaContainer()
|
||
|
|
|
||
|
|
def test_empty(self) -> None:
|
||
|
|
result = self.media_container.media
|
||
|
|
|
||
|
|
self.assertIsInstance(result, widgets.Media)
|
||
|
|
self.assertMediaEqual(result, widgets.Media())
|
||
|
|
|
||
|
|
def test_single_member(self) -> None:
|
||
|
|
# -----------------------------------------------------------------------------
|
||
|
|
class ExampleClass:
|
||
|
|
media = widgets.Media(css={"all": ["example.css"]})
|
||
|
|
|
||
|
|
# -----------------------------------------------------------------------------
|
||
|
|
example = ExampleClass()
|
||
|
|
self.media_container.append(example)
|
||
|
|
|
||
|
|
result = self.media_container.media
|
||
|
|
|
||
|
|
self.assertIsInstance(result, widgets.Media)
|
||
|
|
self.assertMediaEqual(result, example.media)
|
||
|
|
self.assertMediaEqual(result, widgets.Media(css={"all": ["example.css"]}))
|
||
|
|
|
||
|
|
def test_two_members_of_same_class(self) -> None:
|
||
|
|
# -----------------------------------------------------------------------------
|
||
|
|
class ExampleClass:
|
||
|
|
media = widgets.Media(css={"all": ["example.css"]}, js=["example.js"])
|
||
|
|
|
||
|
|
# -----------------------------------------------------------------------------
|
||
|
|
example_1 = ExampleClass()
|
||
|
|
example_2 = ExampleClass()
|
||
|
|
self.media_container.append(example_1)
|
||
|
|
self.media_container.append(example_2)
|
||
|
|
|
||
|
|
result = self.media_container.media
|
||
|
|
|
||
|
|
self.assertIsInstance(result, widgets.Media)
|
||
|
|
self.assertMediaEqual(
|
||
|
|
result,
|
||
|
|
widgets.Media(css={"all": ["example.css"]}, js=["example.js"]),
|
||
|
|
)
|
||
|
|
|
||
|
|
def test_two_members_of_different_classes(self) -> None:
|
||
|
|
# -----------------------------------------------------------------------------
|
||
|
|
class ExampleClass:
|
||
|
|
media = widgets.Media(css={"all": ["shared.css"]}, js=["example.js"])
|
||
|
|
|
||
|
|
class OtherExampleClass:
|
||
|
|
media = widgets.Media(
|
||
|
|
css={
|
||
|
|
"all": ["other.css", "shared.css"],
|
||
|
|
"print": ["print.css"],
|
||
|
|
},
|
||
|
|
js=["other.js"],
|
||
|
|
)
|
||
|
|
|
||
|
|
# -----------------------------------------------------------------------------
|
||
|
|
example = ExampleClass()
|
||
|
|
self.media_container.append(example)
|
||
|
|
other = OtherExampleClass()
|
||
|
|
self.media_container.append(other)
|
||
|
|
|
||
|
|
result = self.media_container.media
|
||
|
|
|
||
|
|
self.assertIsInstance(result, widgets.Media)
|
||
|
|
self.assertMediaEqual(
|
||
|
|
result,
|
||
|
|
widgets.Media(
|
||
|
|
css={
|
||
|
|
"all": ["other.css", "shared.css"],
|
||
|
|
"print": ["print.css"],
|
||
|
|
},
|
||
|
|
js=["example.js", "other.js"],
|
||
|
|
),
|
||
|
|
)
|