Only render dependencies that are used of a specific page (#52)

Co-authored-by: rbeard0330 <@dul2k3BKW6m>
This commit is contained in:
Emil Stenström 2021-03-24 22:47:48 +01:00 committed by GitHub
parent cc8db8056e
commit 57a5aa0f4b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 2576 additions and 99 deletions

View file

@ -1,10 +1,12 @@
from time import perf_counter from time import perf_counter
from django.template import Context, Template from django.template import Context, Template
from django.test import override_settings
from django_components import component from django_components.middleware import CSS_DEPENDENCY_PLACEHOLDER, JS_DEPENDENCY_PLACEHOLDER
from tests.django_test_setup import * # NOQA from tests.django_test_setup import * # NOQA
from tests.testutils import Django30CompatibleSimpleTestCase as SimpleTestCase from django_components import component
from tests.testutils import Django30CompatibleSimpleTestCase as SimpleTestCase, create_and_process_template_response
class SlottedComponent(component.Component): class SlottedComponent(component.Component):
@ -22,20 +24,112 @@ class SimpleComponent(component.Component):
def template(self, context): def template(self, context):
return "simple_template.html" return "simple_template.html"
class Media:
css = {"all": ["style.css"]}
js = ["script.js"]
class BreadcrumbComponent(component.Component):
LINKS = [
('https://developer.mozilla.org/en-US/docs/Learn',
'Learn web development'),
('https://developer.mozilla.org/en-US/docs/Learn/HTML',
'Structuring the web with HTML'),
('https://developer.mozilla.org/en-US/docs/Learn/HTML/Introduction_to_HTML',
'Introduction to HTML'),
('https://developer.mozilla.org/en-US/docs/Learn/HTML/Introduction_to_HTML/Document_and_website_structure',
'Document and website structure')
]
def context(self, items):
if items > 4:
items = 4
elif items < 0:
items = 0
return {'links': self.LINKS[:items - 1]}
def template(self, context):
return "mdn_component_template.html"
class Media:
css = {"all": ["test.css"]}
js = ["test.js"]
EXPECTED_CSS = """<link href="test.css" media="all" rel="stylesheet">"""
EXPECTED_JS = """<script src="test.js"></script>"""
@override_settings(COMPONENTS={'RENDER_DEPENDENCIES': True})
class RenderBenchmarks(SimpleTestCase): class RenderBenchmarks(SimpleTestCase):
def setUp(self): def setUp(self):
component.registry.clear() component.registry.clear()
component.registry.register('test_component', SlottedComponent) component.registry.register('test_component', SlottedComponent)
component.registry.register('inner_component', SimpleComponent) component.registry.register('inner_component', SimpleComponent)
component.registry.register('breadcrumb_component', BreadcrumbComponent)
def test_render_time(self): @staticmethod
def timed_loop(func, iterations=1000):
"""Run func iterations times, and return the time in ms per iteration."""
start_time = perf_counter()
for _ in range(iterations):
func()
end_time = perf_counter()
total_elapsed = end_time - start_time # NOQA
return total_elapsed * 1000 / iterations
def test_render_time_for_small_component(self):
template = Template("{% load component_tags %}{% component_block 'test_component' %}" template = Template("{% load component_tags %}{% component_block 'test_component' %}"
"{% slot \"header\" %}{% component 'inner_component' variable='foo' %}{% endslot %}" "{% slot \"header\" %}{% component 'inner_component' variable='foo' %}{% endslot %}"
"{% endcomponent_block %}", name='root') "{% endcomponent_block %}", name='root')
start_time = perf_counter()
for _ in range(1000): print(f'{self.timed_loop(lambda: template.render(Context({})))} ms per iteration')
template.render(Context({}))
end_time = perf_counter() def test_middleware_time_with_dependency_for_small_page(self):
total_elapsed = end_time - start_time # NOQA template = Template("{% load component_tags %}{% component_dependencies %}"
print(f'{total_elapsed } ms per template') "{% component_block 'test_component' %}{% slot \"header\" %}"
"{% component 'inner_component' variable='foo' %}{% endslot %}{% endcomponent_block %}",
name='root')
# Sanity tests
response = create_and_process_template_response(template)
response_content = response.content.decode('utf-8')
self.assertNotIn(CSS_DEPENDENCY_PLACEHOLDER, response_content)
self.assertNotIn(JS_DEPENDENCY_PLACEHOLDER, response_content)
self.assertIn('style.css', response_content)
self.assertIn('script.js', response_content)
without_middleware = self.timed_loop(lambda: create_and_process_template_response(template,
use_middleware=False))
with_middleware = self.timed_loop(lambda: create_and_process_template_response(template, use_middleware=True))
print('Small page middleware test')
self.report_results(with_middleware, without_middleware)
def test_render_time_with_dependency_for_large_page(self):
from django.template.loader import get_template
template = get_template('mdn_complete_page.html')
response = create_and_process_template_response(template, {})
response_content = response.content.decode('utf-8')
self.assertNotIn(CSS_DEPENDENCY_PLACEHOLDER, response_content)
self.assertNotIn(JS_DEPENDENCY_PLACEHOLDER, response_content)
self.assertIn('test.css', response_content)
self.assertIn('test.js', response_content)
without_middleware = self.timed_loop(
lambda: create_and_process_template_response(template, {}, use_middleware=False))
with_middleware = self.timed_loop(
lambda: create_and_process_template_response(template, {}, use_middleware=True))
print('Large page middleware test')
self.report_results(with_middleware, without_middleware)
@staticmethod
def report_results(with_middleware, without_middleware):
print(f'Middleware active\t\t{with_middleware:.3f} ms per iteration')
print(f'Middleware inactive\t{without_middleware:.3f} ms per iteration')
time_difference = with_middleware - without_middleware
if without_middleware > with_middleware:
print(f'Decrease of {-100 * time_difference / with_middleware:.2f}%')
else:
print(f'Increase of {100 * time_difference / without_middleware:.2f}%')

View file

@ -42,7 +42,7 @@ class SimplifiedInterfaceMediaDefiningClass(MediaDefiningClass):
class Component(metaclass=SimplifiedInterfaceMediaDefiningClass): class Component(metaclass=SimplifiedInterfaceMediaDefiningClass):
def __init__(self, component_name): def __init__(self, component_name):
self.__component_name = component_name self._component_name = component_name
self.instance_template = None self.instance_template = None
self.slots = {} self.slots = {}
@ -90,7 +90,7 @@ class Component(metaclass=SimplifiedInterfaceMediaDefiningClass):
if settings.DEBUG: if settings.DEBUG:
warnings.warn( warnings.warn(
"Component {} was provided with unexpected slots: {}".format( "Component {} was provided with unexpected slots: {}".format(
self.__component_name, unexpected_slots self._component_name, unexpected_slots
) )
) )
for unexpected_slot in unexpected_slots: for unexpected_slot in unexpected_slots:

View file

@ -0,0 +1,74 @@
import re
from django.conf import settings
from django.forms import Media
from django.http import StreamingHttpResponse
RENDERED_COMPONENTS_CONTEXT_KEY = "_COMPONENT_DEPENDENCIES"
CSS_DEPENDENCY_PLACEHOLDER = '<link name="CSS_PLACEHOLDER">'
JS_DEPENDENCY_PLACEHOLDER = '<script name="JS_PLACEHOLDER">'
SCRIPT_TAG_REGEX = re.compile('<script')
COMPONENT_COMMENT_REGEX = re.compile(rb'<!-- _RENDERED (?P<name>\w+?) -->')
PLACEHOLDER_REGEX = re.compile(rb'<!-- _RENDERED (?P<name>\w+?) -->'
rb'|<link name="CSS_PLACEHOLDER">'
rb'|<script name="JS_PLACEHOLDER">')
class ComponentDependencyMiddleware:
"""Middleware that inserts CSS/JS dependencies for all rendered components at points marked with template tags."""
dependency_regex = COMPONENT_COMMENT_REGEX
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
response = self.get_response(request)
if getattr(settings, "COMPONENTS", {}).get('RENDER_DEPENDENCIES', False)\
and not isinstance(response, StreamingHttpResponse)\
and response['Content-Type'].startswith('text/html'):
response.content = process_response_content(response.content)
return response
def process_response_content(content):
from django_components.component import registry
component_names_seen = {match.group('name') for match in COMPONENT_COMMENT_REGEX.finditer(content)}
all_components = [registry.get(name.decode('utf-8'))('') for name in component_names_seen]
all_media = join_media(all_components)
js_dependencies = b''.join(media.encode('utf-8') for media in all_media.render_js())
css_dependencies = b''.join(media.encode('utf-8') for media in all_media.render_css())
return PLACEHOLDER_REGEX.sub(DependencyReplacer(css_dependencies, js_dependencies), content)
def add_module_attribute_to_scripts(scripts):
return re.sub(SCRIPT_TAG_REGEX, '<script type="module"', scripts)
class DependencyReplacer:
"""Replacer for use in re.sub that replaces the first placeholder CSS and JS
tags it encounters and removes any subsequent ones."""
CSS_PLACEHOLDER = bytes(CSS_DEPENDENCY_PLACEHOLDER, encoding='utf-8')
JS_PLACEHOLDER = bytes(JS_DEPENDENCY_PLACEHOLDER, encoding='utf-8')
def __init__(self, css_string, js_string):
self.js_string = js_string
self.css_string = css_string
def __call__(self, match):
if match[0] == self.CSS_PLACEHOLDER:
replacement, self.css_string = self.css_string, b""
elif match[0] == self.JS_PLACEHOLDER:
replacement, self.js_string = self.js_string, b""
else:
replacement = b''
return replacement
def join_media(components):
"""Return combined media object for iterable of components."""
return sum([component.media for component in components], Media())

View file

@ -1,11 +1,13 @@
from collections import defaultdict from collections import defaultdict
from django import template from django import template
from django.conf import settings
from django.template.base import Node, NodeList, TemplateSyntaxError, TokenType from django.template.base import Node, NodeList, TemplateSyntaxError, TokenType
from django.template.library import parse_bits from django.template.library import parse_bits
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from django_components.component import registry from django_components.component import registry
from django_components.middleware import CSS_DEPENDENCY_PLACEHOLDER, JS_DEPENDENCY_PLACEHOLDER
register = template.Library() register = template.Library()
@ -26,35 +28,44 @@ def get_components_from_registry(registry):
@register.simple_tag(name="component_dependencies") @register.simple_tag(name="component_dependencies")
def component_dependencies_tag(): def component_dependencies_tag():
"""Render both the CSS and JS dependency tags.""" """Marks location where CSS link and JS script tags should be rendered."""
rendered_dependencies = [] if is_dependency_middleware_active():
for component in get_components_from_registry(registry): return mark_safe(CSS_DEPENDENCY_PLACEHOLDER + JS_DEPENDENCY_PLACEHOLDER)
rendered_dependencies.append(component.render_dependencies()) else:
rendered_dependencies = []
for component in get_components_from_registry(registry):
rendered_dependencies.append(component.render_dependencies())
return mark_safe("\n".join(rendered_dependencies)) return mark_safe("\n".join(rendered_dependencies))
@register.simple_tag(name="component_css_dependencies") @register.simple_tag(name="component_css_dependencies")
def component_css_dependencies_tag(): def component_css_dependencies_tag():
"""Render the CSS tags.""" """Marks location where CSS link tags should be rendered."""
rendered_dependencies = [] if is_dependency_middleware_active():
for component in get_components_from_registry(registry): return mark_safe(CSS_DEPENDENCY_PLACEHOLDER)
rendered_dependencies.append(component.render_css_dependencies()) else:
rendered_dependencies = []
for component in get_components_from_registry(registry):
rendered_dependencies.append(component.render_css_dependencies())
return mark_safe("\n".join(rendered_dependencies)) return mark_safe("\n".join(rendered_dependencies))
@register.simple_tag(name="component_js_dependencies") @register.simple_tag(name="component_js_dependencies")
def component_js_dependencies_tag(): def component_js_dependencies_tag():
"""Render the JS tags.""" """Marks location where JS script tags should be rendered."""
rendered_dependencies = [] if is_dependency_middleware_active():
for component in get_components_from_registry(registry): return mark_safe(JS_DEPENDENCY_PLACEHOLDER)
rendered_dependencies.append(component.render_js_dependencies()) else:
rendered_dependencies = []
for component in get_components_from_registry(registry):
rendered_dependencies.append(component.render_js_dependencies())
return mark_safe("\n".join(rendered_dependencies)) return mark_safe("\n".join(rendered_dependencies))
@register.tag(name='component') @register.tag(name='component')
@ -101,6 +112,7 @@ class ComponentNode(Node):
for slot in slots: for slot in slots:
slot_dict[slot.name].extend(slot.nodelist) slot_dict[slot.name].extend(slot.nodelist)
self.component.slots = slot_dict self.component.slots = slot_dict
self.should_render_dependencies = is_dependency_middleware_active()
def __repr__(self): def __repr__(self):
return "<Component Node: %s. Contents: %r>" % (self.component, self.component.instance_template.nodelist) return "<Component Node: %s. Contents: %r>" % (self.component, self.component.instance_template.nodelist)
@ -121,7 +133,11 @@ class ComponentNode(Node):
context = context.new() context = context.new()
with context.update(component_context): with context.update(component_context):
return self.component.render(context) rendered_component = self.component.render(context)
if self.should_render_dependencies:
return f'<!-- _RENDERED {self.component._component_name} -->' + rendered_component
else:
return rendered_component
@register.tag("component_block") @register.tag("component_block")
@ -227,3 +243,7 @@ def safe_resolve(context_item, context):
def is_wrapped_in_quotes(s): def is_wrapped_in_quotes(s):
return s.startswith(('"', "'")) and s[0] == s[-1] return s.startswith(('"', "'")) and s[0] == s[-1]
def is_dependency_middleware_active():
return getattr(settings, "COMPONENTS", {}).get('RENDER_DEPENDENCIES', False)

View file

@ -11,6 +11,8 @@ if not settings.configured:
COMPONENTS={ COMPONENTS={
'TEMPLATE_CACHE_SIZE': 128 'TEMPLATE_CACHE_SIZE': 128
}, },
MIDDLEWARE=['django_components.middleware.ComponentDependencyMiddleware'],
DATABASES={},
) )
django.setup() django.setup()

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,14 @@
<div class="breadcrumb-container">
<nav class="breadcrumbs">
<ol typeof="BreadcrumbList" vocab="https://schema.org/" aria-label="breadcrumbs">
{% for label, url in links %}
<li property="itemListElement" typeof="ListItem">
<a class="breadcrumb-current-page" property="item" typeof="WebPage" href="{{ url }}">
<span property="name">{{ label }}</span>
</a>
<meta property="position" content="4">
</li>
{% endfor %}
</ol>
</nav>
</div>

View file

@ -215,28 +215,27 @@ class ContextCalledOnceTests(SimpleTestCase):
self.assertEqual(rendered, '<p class="incrementer">value=1;calls=1</p>', rendered) self.assertEqual(rendered, '<p class="incrementer">value=1;calls=1</p>', rendered)
def test_one_context_call_with_simple_component_and_arg(self): def test_one_context_call_with_simple_component_and_arg(self):
template = Template("{% load component_tags %}{% component_dependencies %}" template = Template("{% load component_tags %}{% component name='incrementer' value='2' %}")
"{% component name='incrementer' value='2' %}")
rendered = template.render(Context()).strip() rendered = template.render(Context()).strip()
self.assertEqual(rendered, '<p class="incrementer">value=3;calls=1</p>', rendered) self.assertEqual(rendered, '<p class="incrementer">value=3;calls=1</p>', rendered)
def test_one_context_call_with_component_block(self): def test_one_context_call_with_component_block(self):
template = Template("{% load component_tags %}{% component_dependencies %}" template = Template("{% load component_tags %}"
"{% component_block 'incrementer' %}{% endcomponent_block %}") "{% component_block 'incrementer' %}{% endcomponent_block %}")
rendered = template.render(Context()).strip() rendered = template.render(Context()).strip()
self.assertEqual(rendered, '<p class="incrementer">value=1;calls=1</p>', rendered) self.assertEqual(rendered, '<p class="incrementer">value=1;calls=1</p>', rendered)
def test_one_context_call_with_component_block_and_arg(self): def test_one_context_call_with_component_block_and_arg(self):
template = Template("{% load component_tags %}{% component_dependencies %}" template = Template("{% load component_tags %}"
"{% component_block 'incrementer' value='3' %}{% endcomponent_block %}") "{% component_block 'incrementer' value='3' %}{% endcomponent_block %}")
rendered = template.render(Context()).strip() rendered = template.render(Context()).strip()
self.assertEqual(rendered, '<p class="incrementer">value=4;calls=1</p>', rendered) self.assertEqual(rendered, '<p class="incrementer">value=4;calls=1</p>', rendered)
def test_one_context_call_with_slot(self): def test_one_context_call_with_slot(self):
template = Template("{% load component_tags %}{% component_dependencies %}" template = Template("{% load component_tags %}"
"{% component_block 'incrementer' %}{% slot 'content' %}" "{% component_block 'incrementer' %}{% slot 'content' %}"
"<p>slot</p>{% endslot %}{% endcomponent_block %}") "<p>slot</p>{% endslot %}{% endcomponent_block %}")
rendered = template.render(Context()).strip() rendered = template.render(Context()).strip()
@ -244,7 +243,7 @@ class ContextCalledOnceTests(SimpleTestCase):
self.assertEqual(rendered, '<p class="incrementer">value=1;calls=1</p>\n<p>slot</p>', rendered) self.assertEqual(rendered, '<p class="incrementer">value=1;calls=1</p>\n<p>slot</p>', rendered)
def test_one_context_call_with_slot_and_arg(self): def test_one_context_call_with_slot_and_arg(self):
template = Template("{% load component_tags %}{% component_dependencies %}" template = Template("{% load component_tags %}"
"{% component_block 'incrementer' value='3' %}{% slot 'content' %}" "{% component_block 'incrementer' value='3' %}{% slot 'content' %}"
"<p>slot</p>{% endslot %}{% endcomponent_block %}") "<p>slot</p>{% endslot %}{% endcomponent_block %}")
rendered = template.render(Context()).strip() rendered = template.render(Context()).strip()

View file

@ -0,0 +1,202 @@
from django.template import Template
from django.test import override_settings
from .django_test_setup import * # NOQA
from django_components import component
from .test_templatetags import SimpleComponent
from .testutils import create_and_process_template_response, Django30CompatibleSimpleTestCase as SimpleTestCase
class SimpleComponentAlternate(component.Component):
def context(self, variable):
return {}
def template(self, context):
return "simple_template.html"
class Media:
css = "style2.css"
js = "script2.js"
class SimpleComponentWithSharedDependency(component.Component):
def context(self, variable, variable2="default"):
return {
"variable": variable,
"variable2": variable2,
}
def template(self, context):
return "simple_template.html"
class Media:
css = ["style.css", "style2.css"]
js = ["script.js", "script2.js"]
class MultistyleComponent(component.Component):
def template(self, context):
return "simple_template.html"
class Media:
css = ["style.css", "style2.css"]
js = ["script.js", "script2.js"]
@override_settings(COMPONENTS={'RENDER_DEPENDENCIES': True})
class ComponentMediaRenderingTests(SimpleTestCase):
def setUp(self):
# NOTE: component.registry is global, so need to clear before each test
component.registry.clear()
def test_no_dependencies_when_no_components_used(self):
component.registry.register(name="test", component=SimpleComponent)
template = Template("{% load component_tags %}{% component_dependencies %}")
rendered = create_and_process_template_response(template)
self.assertInHTML('<script src="script.js>"', rendered, count=0)
self.assertInHTML('<link href="style.css" type="text/css" media="all" rel="stylesheet"/>', rendered, count=0)
self.assertInHTML('<link href="style.css" type="text/css" media="all" rel="stylesheet"/>', rendered, count=0)
def test_no_js_dependencies_when_no_components_used(self):
component.registry.register(name="test", component=SimpleComponent)
template = Template("{% load component_tags %}{% component_js_dependencies %}")
rendered = create_and_process_template_response(template)
self.assertInHTML('<script src="script.js>"', rendered, count=0)
def test_no_css_dependencies_when_no_components_used(self):
component.registry.register(name="test", component=SimpleComponent)
template = Template("{% load component_tags %}{% component_css_dependencies %}")
rendered = create_and_process_template_response(template)
self.assertInHTML('<link href="style.css" type="text/css" media="all" rel="stylesheet"/>', rendered, count=0)
def test_single_component_dependencies_render_when_used(self):
component.registry.register(name="test", component=SimpleComponent)
template = Template("{% load component_tags %}{% component_dependencies %}"
"{% component 'test' variable='foo' %}")
rendered = create_and_process_template_response(template)
self.assertInHTML('<link href="style.css" type="text/css" media="all" rel="stylesheet"/>', rendered, count=1)
self.assertInHTML('<script src="script.js">', rendered, count=1)
def test_placeholder_removed_when_single_component_rendered(self):
component.registry.register(name="test", component=SimpleComponent)
template = Template("{% load component_tags %}{% component_dependencies %}"
"{% component 'test' variable='foo' %}")
rendered = create_and_process_template_response(template)
self.assertNotIn('_RENDERED', rendered)
def test_single_component_css_dependencies(self):
component.registry.register(name="test", component=SimpleComponent)
template = Template("{% load component_tags %}{% component_css_dependencies %}"
"{% component 'test' variable='foo' %}")
rendered = create_and_process_template_response(template)
self.assertInHTML('<link href="style.css" type="text/css" media="all" rel="stylesheet"/>', rendered, count=1)
def test_single_component_js_dependencies(self):
component.registry.register(name="test", component=SimpleComponent)
template = Template("{% load component_tags %}{% component_js_dependencies %}"
"{% component 'test' variable='foo' %}")
rendered = create_and_process_template_response(template)
self.assertInHTML('<script src="script.js">', rendered, count=1)
def test_all_dependencies_are_rendered_for_component_with_multiple_dependencies(self):
component.registry.register(name='test', component=MultistyleComponent)
template = Template("{% load component_tags %}{% component_dependencies %}{% component 'test' %}")
rendered = create_and_process_template_response(template)
self.assertInHTML('<script src="script.js">', rendered, count=1)
self.assertInHTML('<script src="script2.js">', rendered, count=1)
self.assertInHTML('<link href="style.css" type="text/css" media="all" rel="stylesheet"/>', rendered, count=1)
self.assertInHTML('<link href="style2.css" type="text/css" media="all" rel="stylesheet"/>', rendered, count=1)
def test_all_js_dependencies_are_rendered_for_component_with_multiple_dependencies(self):
component.registry.register(name='test', component=MultistyleComponent)
template = Template("{% load component_tags %}{% component_js_dependencies %}{% component 'test' %}")
rendered = create_and_process_template_response(template)
self.assertInHTML('<script src="script.js">', rendered, count=1)
self.assertInHTML('<script src="script2.js">', rendered, count=1)
self.assertInHTML('<link href="style.css" type="text/css" media="all" rel="stylesheet"/>', rendered, count=0)
self.assertInHTML('<link href="style2.css" type="text/css" media="all" rel="stylesheet"/>', rendered, count=0)
def test_all_css_dependencies_are_rendered_for_component_with_multiple_dependencies(self):
component.registry.register(name='test', component=MultistyleComponent)
template = Template("{% load component_tags %}{% component_css_dependencies %}{% component 'test' %}")
rendered = create_and_process_template_response(template)
self.assertInHTML('<script src="script.js">', rendered, count=0)
self.assertInHTML('<script src="script2.js">', rendered, count=0)
self.assertInHTML('<link href="style.css" type="text/css" media="all" rel="stylesheet"/>', rendered, count=1)
self.assertInHTML('<link href="style2.css" type="text/css" media="all" rel="stylesheet"/>', rendered, count=1)
def test_no_dependencies_with_multiple_unused_components(self):
component.registry.register(name="test1", component=SimpleComponent)
component.registry.register(name="test2", component=SimpleComponentAlternate)
template = Template("{% load component_tags %}{% component_dependencies %}")
rendered = create_and_process_template_response(template)
self.assertInHTML('<script src="script.js">', rendered, count=0)
self.assertInHTML('<script src="script2.js">', rendered, count=0)
self.assertInHTML('<link href="style.css" type="text/css" media="all" rel="stylesheet"/>', rendered, count=0)
self.assertInHTML('<link href="style2.css" type="text/css" media="all" rel="stylesheet"/>', rendered, count=0)
def test_correct_css_dependencies_with_multiple_components(self):
component.registry.register(name="test1", component=SimpleComponent)
component.registry.register(name="test2", component=SimpleComponentAlternate)
template = Template("{% load component_tags %}{% component_css_dependencies %}"
"{% component 'test1' 'variable' %}")
rendered = create_and_process_template_response(template)
self.assertInHTML('<link href="style.css" type="text/css" media="all" rel="stylesheet"/>', rendered, count=1)
self.assertInHTML('<link href="style2.css" type="text/css" media="all" rel="stylesheet"/>', rendered, count=0)
def test_correct_js_dependencies_with_multiple_components(self):
component.registry.register(name="test1", component=SimpleComponent)
component.registry.register(name="test2", component=SimpleComponentAlternate)
template = Template("{% load component_tags %}{% component_js_dependencies %}"
"{% component 'test1' 'variable' %}")
rendered = create_and_process_template_response(template)
self.assertInHTML('<script src="script.js">', rendered, count=1)
self.assertInHTML('<script src="script2.js">', rendered, count=0)
def test_correct_dependencies_with_multiple_components(self):
component.registry.register(name="test1", component=SimpleComponent)
component.registry.register(name="test2", component=SimpleComponentAlternate)
template = Template("{% load component_tags %}{% component_dependencies %}"
"{% component 'test2' variable='variable' %}")
rendered = create_and_process_template_response(template)
self.assertInHTML('<script src="script.js">', rendered, count=0)
self.assertInHTML('<script src="script2.js">', rendered, count=1)
self.assertInHTML('<link href="style.css" type="text/css" media="all" rel="stylesheet"/>', rendered, count=0)
self.assertInHTML('<link href="style2.css" type="text/css" media="all" rel="stylesheet"/>', rendered, count=1)
def test_shared_dependencies_rendered_once(self):
component.registry.register(name="test1", component=SimpleComponent)
component.registry.register(name="test2", component=SimpleComponentAlternate)
component.registry.register(name="test3", component=SimpleComponentWithSharedDependency)
template = Template("{% load component_tags %}{% component_dependencies %}"
"{% component 'test1' variable='variable' %}{% component 'test2' variable='variable' %}"
"{% component 'test1' variable='variable' %}")
rendered = create_and_process_template_response(template)
self.assertInHTML('<script src="script.js">', rendered, count=1)
self.assertInHTML('<script src="script2.js">', rendered, count=1)
self.assertInHTML('<link href="style.css" type="text/css" media="all" rel="stylesheet"/>', rendered, count=1)
self.assertInHTML('<link href="style2.css" type="text/css" media="all" rel="stylesheet"/>', rendered, count=1)
def test_placeholder_removed_when_multiple_component_rendered(self):
component.registry.register(name="test1", component=SimpleComponent)
component.registry.register(name="test2", component=SimpleComponentAlternate)
component.registry.register(name="test3", component=SimpleComponentWithSharedDependency)
template = Template("{% load component_tags %}{% component_dependencies %}"
"{% component 'test1' variable='variable' %}{% component 'test2' variable='variable' %}"
"{% component 'test1' variable='variable' %}")
rendered = create_and_process_template_response(template)
self.assertNotIn('_RENDERED', rendered)

View file

@ -64,37 +64,6 @@ class ComponentTemplateTagTest(SimpleTestCase):
# NOTE: component.registry is global, so need to clear before each test # NOTE: component.registry is global, so need to clear before each test
component.registry.clear() component.registry.clear()
def test_single_component_dependencies(self):
component.registry.register(name="test", component=SimpleComponent)
template = Template("{% load component_tags %}{% component_dependencies %}")
rendered = template.render(Context())
expected_outcome = (
"""<link href="style.css" type="text/css" media="all" rel="stylesheet">\n"""
"""<script src="script.js"></script>"""
)
self.assertHTMLEqual(rendered, dedent(expected_outcome))
def test_single_component_css_dependencies(self):
component.registry.register(name="test", component=SimpleComponent)
template = Template("{% load component_tags %}{% component_css_dependencies %}")
rendered = template.render(Context())
expected_outcome = (
"""<link href="style.css" type="text/css" media="all" rel="stylesheet">"""
)
self.assertHTMLEqual(rendered, dedent(expected_outcome))
def test_single_component_js_dependencies(self):
component.registry.register(name="test", component=SimpleComponent)
template = Template("{% load component_tags %}{% component_js_dependencies %}")
rendered = template.render(Context())
expected_outcome = (
"""<script src="script.js"></script>"""
)
self.assertHTMLEqual(rendered, dedent(expected_outcome))
def test_single_component(self): def test_single_component(self):
component.registry.register(name="test", component=SimpleComponent) component.registry.register(name="test", component=SimpleComponent)
@ -145,40 +114,6 @@ class ComponentTemplateTagTest(SimpleTestCase):
rendered = template.render(Context({})) rendered = template.render(Context({}))
self.assertHTMLEqual(rendered, "Variable: <strong>variable</strong>\n") self.assertHTMLEqual(rendered, "Variable: <strong>variable</strong>\n")
def test_multiple_component_dependencies(self):
component.registry.register(name="test1", component=SimpleComponent)
component.registry.register(name="test2", component=SimpleComponent)
template = Template("{% load component_tags %}{% component_dependencies %}")
rendered = template.render(Context())
expected_outcome = (
"""<link href="style.css" type="text/css" media="all" rel="stylesheet">\n"""
"""<script src="script.js"></script>"""
)
self.assertHTMLEqual(rendered, dedent(expected_outcome))
def test_multiple_component_css_dependencies(self):
component.registry.register(name="test1", component=SimpleComponent)
component.registry.register(name="test2", component=SimpleComponent)
template = Template("{% load component_tags %}{% component_css_dependencies %}")
rendered = template.render(Context())
expected_outcome = (
"""<link href="style.css" type="text/css" media="all" rel="stylesheet">"""
)
self.assertHTMLEqual(rendered, dedent(expected_outcome))
def test_multiple_component_js_dependencies(self):
component.registry.register(name="test1", component=SimpleComponent)
component.registry.register(name="test2", component=SimpleComponent)
template = Template("{% load component_tags %}{% component_js_dependencies %}")
rendered = template.render(Context())
expected_outcome = (
"""<script src="script.js"></script>"""
)
self.assertHTMLEqual(rendered, dedent(expected_outcome))
class ComponentSlottedTemplateTagTest(SimpleTestCase): class ComponentSlottedTemplateTagTest(SimpleTestCase):
def setUp(self): def setUp(self):

View file

@ -1,7 +1,43 @@
from django.test import SimpleTestCase from unittest.mock import Mock
from django.template import Context
from django.template.response import TemplateResponse
from django.test import SimpleTestCase, TestCase
from django_components.middleware import ComponentDependencyMiddleware
# Create middleware instance
response_stash = None
middleware = ComponentDependencyMiddleware(get_response=lambda _: response_stash)
class Django30CompatibleSimpleTestCase(SimpleTestCase): class Django30CompatibleSimpleTestCase(SimpleTestCase):
def assertHTMLEqual(self, left, right): def assertHTMLEqual(self, left, right):
left = left.replace(' type="text/javascript"', '') left = left.replace(' type="text/javascript"', '')
super(Django30CompatibleSimpleTestCase, self).assertHTMLEqual(left, right) super(Django30CompatibleSimpleTestCase, self).assertHTMLEqual(left, right)
def assertInHTML(self, needle, haystack, count=None, msg_prefix=''):
haystack = haystack.replace(' type="text/javascript"', '')
super().assertInHTML(needle, haystack, count, msg_prefix)
class Django30CompatibleTestCase(Django30CompatibleSimpleTestCase, TestCase):
pass
request = Mock()
mock_template = Mock()
def create_and_process_template_response(template, context=None, use_middleware=True):
context = context if context is not None else Context({})
mock_template.render = lambda context, _: template.render(context)
response = TemplateResponse(request, mock_template, context)
if use_middleware:
response.render()
global response_stash
response_stash = response
response = middleware(request)
else:
response.render()
return response.content.decode('utf-8')