2 # Copyright (c) Twisted Matrix Laboratories.
3 # See LICENSE for details.
7 Tests for L{twisted.web.template}
10 from cStringIO import StringIO
12 from zope.interface.verify import verifyObject
14 from twisted.internet.defer import succeed, gatherResults
15 from twisted.python.filepath import FilePath
16 from twisted.trial.unittest import TestCase
17 from twisted.web.template import (
18 Element, TagLoader, renderer, tags, XMLFile, XMLString)
19 from twisted.web.iweb import ITemplateLoader
21 from twisted.web.error import (FlattenerError, MissingTemplateLoader,
24 from twisted.web.template import renderElement
25 from twisted.web._element import UnexposedMethodError
26 from twisted.web.test._util import FlattenTestCase
27 from twisted.web.test.test_web import DummyRequest
28 from twisted.web.server import NOT_DONE_YET
30 class TagFactoryTests(TestCase):
32 Tests for L{_TagFactory} through the publicly-exposed L{tags} object.
34 def test_lookupTag(self):
36 HTML tags can be retrieved through C{tags}.
39 self.assertEqual(tag.tagName, "a")
42 def test_lookupHTML5Tag(self):
44 Twisted supports the latest and greatest HTML tags from the HTML5
48 self.assertEqual(tag.tagName, "video")
51 def test_lookupTransparentTag(self):
53 To support transparent inclusion in templates, there is a special tag,
54 the transparent tag, which has no name of its own but is accessed
55 through the "transparent" attribute.
57 tag = tags.transparent
58 self.assertEqual(tag.tagName, "")
61 def test_lookupInvalidTag(self):
63 Invalid tags which are not part of HTML cause AttributeErrors when
64 accessed through C{tags}.
66 self.assertRaises(AttributeError, getattr, tags, "invalid")
69 def test_lookupXMP(self):
71 As a special case, the <xmp> tag is simply not available through
72 C{tags} or any other part of the templating machinery.
74 self.assertRaises(AttributeError, getattr, tags, "xmp")
78 class ElementTests(TestCase):
80 Tests for the awesome new L{Element} class.
82 def test_missingTemplateLoader(self):
84 L{Element.render} raises L{MissingTemplateLoader} if the C{loader}
88 err = self.assertRaises(MissingTemplateLoader, element.render, None)
89 self.assertIdentical(err.element, element)
92 def test_missingTemplateLoaderRepr(self):
94 A L{MissingTemplateLoader} instance can be repr()'d without error.
96 class PrettyReprElement(Element):
98 return 'Pretty Repr Element'
99 self.assertIn('Pretty Repr Element',
100 repr(MissingTemplateLoader(PrettyReprElement())))
103 def test_missingRendererMethod(self):
105 When called with the name which is not associated with a render method,
106 L{Element.lookupRenderMethod} raises L{MissingRenderMethod}.
109 err = self.assertRaises(
110 MissingRenderMethod, element.lookupRenderMethod, "foo")
111 self.assertIdentical(err.element, element)
112 self.assertEqual(err.renderName, "foo")
115 def test_missingRenderMethodRepr(self):
117 A L{MissingRenderMethod} instance can be repr()'d without error.
119 class PrettyReprElement(Element):
121 return 'Pretty Repr Element'
122 s = repr(MissingRenderMethod(PrettyReprElement(),
124 self.assertIn('Pretty Repr Element', s)
125 self.assertIn('expectedMethod', s)
128 def test_definedRenderer(self):
130 When called with the name of a defined render method,
131 L{Element.lookupRenderMethod} returns that render method.
133 class ElementWithRenderMethod(Element):
135 def foo(self, request, tag):
137 foo = ElementWithRenderMethod().lookupRenderMethod("foo")
138 self.assertEqual(foo(None, None), "bar")
141 def test_render(self):
143 L{Element.render} loads a document from the C{loader} attribute and
146 class TemplateLoader(object):
150 class StubElement(Element):
151 loader = TemplateLoader()
153 element = StubElement()
154 self.assertEqual(element.render(None), "result")
157 def test_misuseRenderer(self):
159 If the L{renderer} decorator is called without any arguments, it will
160 raise a comprehensible exception.
162 te = self.assertRaises(TypeError, renderer)
163 self.assertEqual(str(te),
164 "expose() takes at least 1 argument (0 given)")
167 def test_renderGetDirectlyError(self):
169 Called directly, without a default, L{renderer.get} raises
170 L{UnexposedMethodError} when it cannot find a renderer.
172 self.assertRaises(UnexposedMethodError, renderer.get, None,
177 class XMLFileReprTests(TestCase):
179 Tests for L{twisted.web.template.XMLFile}'s C{__repr__}.
181 def test_filePath(self):
183 An L{XMLFile} with a L{FilePath} returns a useful repr().
185 path = FilePath("/tmp/fake.xml")
186 self.assertEqual('<XMLFile of %r>' % (path,), repr(XMLFile(path)))
189 def test_filename(self):
191 An L{XMLFile} with a filename returns a useful repr().
193 fname = "/tmp/fake.xml"
194 self.assertEqual('<XMLFile of %r>' % (fname,), repr(XMLFile(fname)))
199 An L{XMLFile} with a file object returns a useful repr().
201 fobj = StringIO("not xml")
202 self.assertEqual('<XMLFile of %r>' % (fobj,), repr(XMLFile(fobj)))
206 class XMLLoaderTestsMixin(object):
208 @ivar templateString: Simple template to use to excercise the loaders.
210 @ivar deprecatedUse: C{True} if this use of L{XMLFile} is deprecated and
211 should emit a C{DeprecationWarning}.
215 templateString = '<p>Hello, world.</p>'
218 Verify that the loader returns a tag with the correct children.
220 loader = self.loaderFactory()
223 warnings = self.flushWarnings(offendingFunctions=[self.loaderFactory])
224 if self.deprecatedUse:
225 self.assertEqual(len(warnings), 1)
226 self.assertEqual(warnings[0]['category'], DeprecationWarning)
228 warnings[0]['message'],
229 "Passing filenames or file objects to XMLFile is "
230 "deprecated since Twisted 12.1. Pass a FilePath instead.")
232 self.assertEqual(len(warnings), 0)
234 self.assertEqual(tag.tagName, 'p')
235 self.assertEqual(tag.children, [u'Hello, world.'])
238 def test_loadTwice(self):
240 If {load()} can be called on a loader twice the result should be the
243 loader = self.loaderFactory()
244 tags1 = loader.load()
245 tags2 = loader.load()
246 self.assertEqual(tags1, tags2)
250 class XMLStringLoaderTests(TestCase, XMLLoaderTestsMixin):
252 Tests for L{twisted.web.template.XMLString}
254 deprecatedUse = False
255 def loaderFactory(self):
257 @return: an L{XMLString} constructed with C{self.templateString}.
259 return XMLString(self.templateString)
263 class XMLFileWithFilePathTests(TestCase, XMLLoaderTestsMixin):
265 Tests for L{twisted.web.template.XMLFile}'s L{FilePath} support.
267 deprecatedUse = False
268 def loaderFactory(self):
270 @return: an L{XMLString} constructed with a L{FilePath} pointing to a
271 file that contains C{self.templateString}.
273 fp = FilePath(self.mktemp())
274 fp.setContent(self.templateString)
279 class XMLFileWithFileTests(TestCase, XMLLoaderTestsMixin):
281 Tests for L{twisted.web.template.XMLFile}'s deprecated file object support.
284 def loaderFactory(self):
286 @return: an L{XMLString} constructed with a file object that contains
287 C{self.templateString}.
289 return XMLFile(StringIO(self.templateString))
293 class XMLFileWithFilenameTests(TestCase, XMLLoaderTestsMixin):
295 Tests for L{twisted.web.template.XMLFile}'s deprecated filename support.
298 def loaderFactory(self):
300 @return: an L{XMLString} constructed with a filename that points to a
301 file containing C{self.templateString}.
303 fp = FilePath(self.mktemp())
304 fp.setContent(self.templateString)
305 return XMLFile(fp.path)
309 class FlattenIntegrationTests(FlattenTestCase):
311 Tests for integration between L{Element} and
312 L{twisted.web._flatten.flatten}.
315 def test_roundTrip(self):
317 Given a series of parsable XML strings, verify that
318 L{twisted.web._flatten.flatten} will flatten the L{Element} back to the
319 input when sent on a round trip.
322 "<p>Hello, world.</p>",
323 "<p><!-- hello, world --></p>",
324 "<p><![CDATA[Hello, world.]]></p>",
325 '<test1 xmlns:test2="urn:test2">'
326 '<test2:test3></test2:test3></test1>',
327 '<test1 xmlns="urn:test2"><test3></test3></test1>',
328 '<p>\xe2\x98\x83</p>',
331 self.assertFlattensTo(Element(loader=XMLString(xml)), xml)
332 for xml in fragments]
333 return gatherResults(deferreds)
336 def test_entityConversion(self):
338 When flattening an HTML entity, it should flatten out to the utf-8
339 representation if possible.
341 element = Element(loader=XMLString('<p>☃</p>'))
342 return self.assertFlattensTo(element, '<p>\xe2\x98\x83</p>')
345 def test_missingTemplateLoader(self):
347 Rendering a Element without a loader attribute raises the appropriate
350 return self.assertFlatteningRaises(Element(), MissingTemplateLoader)
353 def test_missingRenderMethod(self):
355 Flattening an L{Element} with a C{loader} which has a tag with a render
356 directive fails with L{FlattenerError} if there is no available render
357 method to satisfy that directive.
359 element = Element(loader=XMLString("""
360 <p xmlns:t="http://twistedmatrix.com/ns/twisted.web.template/0.1"
361 t:render="unknownMethod" />
363 return self.assertFlatteningRaises(element, MissingRenderMethod)
366 def test_transparentRendering(self):
368 A C{transparent} element should be eliminated from the DOM and rendered as
371 element = Element(loader=XMLString(
373 'xmlns:t="http://twistedmatrix.com/ns/twisted.web.template/0.1">'
377 return self.assertFlattensTo(element, "Hello, world.")
380 def test_attrRendering(self):
382 An Element with an attr tag renders the vaule of its attr tag as an
383 attribute of its containing tag.
385 element = Element(loader=XMLString(
386 '<a xmlns:t="http://twistedmatrix.com/ns/twisted.web.template/0.1">'
387 '<t:attr name="href">http://example.com</t:attr>'
391 return self.assertFlattensTo(element,
392 '<a href="http://example.com">Hello, world.</a>')
395 def test_errorToplevelAttr(self):
397 A template with a toplevel C{attr} tag will not load; it will raise
398 L{AssertionError} if you try.
404 xmlns:t='http://twistedmatrix.com/ns/twisted.web.template/0.1'
410 def test_errorUnnamedAttr(self):
412 A template with an C{attr} tag with no C{name} attribute will not load;
413 it will raise L{AssertionError} if you try.
419 xmlns:t='http://twistedmatrix.com/ns/twisted.web.template/0.1'
420 >hello</t:attr></html>""")
423 def test_lenientPrefixBehavior(self):
425 If the parser sees a prefix it doesn't recognize on an attribute, it
426 will pass it on through to serialization.
429 '<hello:world hello:sample="testing" '
430 'xmlns:hello="http://made-up.example.com/ns/not-real">'
431 'This is a made-up tag.</hello:world>')
432 element = Element(loader=XMLString(theInput))
433 self.assertFlattensTo(element, theInput)
436 def test_deferredRendering(self):
438 An Element with a render method which returns a Deferred will render
441 class RenderfulElement(Element):
443 def renderMethod(self, request, tag):
444 return succeed("Hello, world.")
445 element = RenderfulElement(loader=XMLString("""
446 <p xmlns:t="http://twistedmatrix.com/ns/twisted.web.template/0.1"
447 t:render="renderMethod">
451 return self.assertFlattensTo(element, "Hello, world.")
454 def test_loaderClassAttribute(self):
456 If there is a non-None loader attribute on the class of an Element
457 instance but none on the instance itself, the class attribute is used.
459 class SubElement(Element):
460 loader = XMLString("<p>Hello, world.</p>")
461 return self.assertFlattensTo(SubElement(), "<p>Hello, world.</p>")
464 def test_directiveRendering(self):
466 An Element with a valid render directive has that directive invoked and
467 the result added to the output.
470 class RenderfulElement(Element):
472 def renderMethod(self, request, tag):
473 renders.append((self, request))
474 return tag("Hello, world.")
475 element = RenderfulElement(loader=XMLString("""
476 <p xmlns:t="http://twistedmatrix.com/ns/twisted.web.template/0.1"
477 t:render="renderMethod" />
479 return self.assertFlattensTo(element, "<p>Hello, world.</p>")
482 def test_directiveRenderingOmittingTag(self):
484 An Element with a render method which omits the containing tag
485 successfully removes that tag from the output.
487 class RenderfulElement(Element):
489 def renderMethod(self, request, tag):
490 return "Hello, world."
491 element = RenderfulElement(loader=XMLString("""
492 <p xmlns:t="http://twistedmatrix.com/ns/twisted.web.template/0.1"
493 t:render="renderMethod">
497 return self.assertFlattensTo(element, "Hello, world.")
500 def test_elementContainingStaticElement(self):
502 An Element which is returned by the render method of another Element is
505 class RenderfulElement(Element):
507 def renderMethod(self, request, tag):
509 loader=XMLString("<em>Hello, world.</em>")))
510 element = RenderfulElement(loader=XMLString("""
511 <p xmlns:t="http://twistedmatrix.com/ns/twisted.web.template/0.1"
512 t:render="renderMethod" />
514 return self.assertFlattensTo(element, "<p><em>Hello, world.</em></p>")
517 def test_elementUsingSlots(self):
519 An Element which is returned by the render method of another Element is
522 class RenderfulElement(Element):
524 def renderMethod(self, request, tag):
525 return tag.fillSlots(test2='world.')
526 element = RenderfulElement(loader=XMLString(
527 '<p xmlns:t="http://twistedmatrix.com/ns/twisted.web.template/0.1"'
528 ' t:render="renderMethod">'
529 '<t:slot name="test1" default="Hello, " />'
530 '<t:slot name="test2" />'
533 return self.assertFlattensTo(element, "<p>Hello, world.</p>")
536 def test_elementContainingDynamicElement(self):
538 Directives in the document factory of a Element returned from a render
539 method of another Element are satisfied from the correct object: the
542 class OuterElement(Element):
544 def outerMethod(self, request, tag):
545 return tag(InnerElement(loader=XMLString("""
547 xmlns:t="http://twistedmatrix.com/ns/twisted.web.template/0.1"
548 t:render="innerMethod" />
550 class InnerElement(Element):
552 def innerMethod(self, request, tag):
553 return "Hello, world."
554 element = OuterElement(loader=XMLString("""
555 <p xmlns:t="http://twistedmatrix.com/ns/twisted.web.template/0.1"
556 t:render="outerMethod" />
558 return self.assertFlattensTo(element, "<p>Hello, world.</p>")
561 def test_sameLoaderTwice(self):
563 Rendering the output of a loader, or even the same element, should
564 return different output each time.
566 sharedLoader = XMLString(
567 '<p xmlns:t="http://twistedmatrix.com/ns/twisted.web.template/0.1">'
568 '<t:transparent t:render="classCounter" /> '
569 '<t:transparent t:render="instanceCounter" />'
572 class DestructiveElement(Element):
575 loader = sharedLoader
578 def classCounter(self, request, tag):
579 DestructiveElement.count += 1
580 return tag(str(DestructiveElement.count))
582 def instanceCounter(self, request, tag):
583 self.instanceCount += 1
584 return tag(str(self.instanceCount))
586 e1 = DestructiveElement()
587 e2 = DestructiveElement()
588 self.assertFlattensImmediately(e1, "<p>1 1</p>")
589 self.assertFlattensImmediately(e1, "<p>2 2</p>")
590 self.assertFlattensImmediately(e2, "<p>3 1</p>")
594 class TagLoaderTests(FlattenTestCase):
596 Tests for L{TagLoader}.
599 self.loader = TagLoader(tags.i('test'))
602 def test_interface(self):
604 An instance of L{TagLoader} provides L{ITemplateLoader}.
606 self.assertTrue(verifyObject(ITemplateLoader, self.loader))
609 def test_loadsList(self):
611 L{TagLoader.load} returns a list, per L{ITemplateLoader}.
613 self.assertIsInstance(self.loader.load(), list)
616 def test_flatten(self):
618 L{TagLoader} can be used in an L{Element}, and flattens as the tag used
619 to construct the L{TagLoader} would flatten.
621 e = Element(self.loader)
622 self.assertFlattensImmediately(e, '<i>test</i>')
626 class TestElement(Element):
628 An L{Element} that can be rendered successfully.
631 '<p xmlns:t="http://twistedmatrix.com/ns/twisted.web.template/0.1">'
637 class TestFailureElement(Element):
639 An L{Element} that can be used in place of L{FailureElement} to verify
640 that L{renderElement} can render failures properly.
643 '<p xmlns:t="http://twistedmatrix.com/ns/twisted.web.template/0.1">'
647 def __init__(self, failure, loader=None):
648 self.failure = failure
652 class FailingElement(Element):
654 An element that raises an exception when rendered.
656 def render(self, request):
663 class FakeSite(object):
665 A minimal L{Site} object that we can use to test displayTracebacks
667 displayTracebacks = False
671 class TestRenderElement(TestCase):
673 Test L{renderElement}
678 Set up a common L{DummyRequest} and L{FakeSite}.
680 self.request = DummyRequest([""])
681 self.request.site = FakeSite()
684 def test_simpleRender(self):
686 L{renderElement} returns NOT_DONE_YET and eventually
687 writes the rendered L{Element} to the request before finishing the
690 element = TestElement()
692 d = self.request.notifyFinish()
696 "".join(self.request.written),
698 "<p>Hello, world.</p>")
699 self.assertTrue(self.request.finished)
703 self.assertIdentical(NOT_DONE_YET, renderElement(self.request, element))
708 def test_simpleFailure(self):
710 L{renderElement} handles failures by writing a minimal
711 error message to the request and finishing it.
713 element = FailingElement()
715 d = self.request.notifyFinish()
718 flushed = self.flushLoggedErrors(FlattenerError)
719 self.assertEqual(len(flushed), 1)
721 "".join(self.request.written),
723 '<div style="font-size:800%;'
724 'background-color:#FFF;'
726 '">An error occurred while rendering the response.</div>'))
727 self.assertTrue(self.request.finished)
731 self.assertIdentical(NOT_DONE_YET, renderElement(self.request, element))
736 def test_simpleFailureWithTraceback(self):
738 L{renderElement} will render a traceback when rendering of
739 the element fails and our site is configured to display tracebacks.
741 self.request.site.displayTracebacks = True
743 element = FailingElement()
745 d = self.request.notifyFinish()
748 flushed = self.flushLoggedErrors(FlattenerError)
749 self.assertEqual(len(flushed), 1)
751 "".join(self.request.written),
752 "<!DOCTYPE html>\n<p>I failed.</p>")
753 self.assertTrue(self.request.finished)
757 renderElement(self.request, element, _failElement=TestFailureElement)
762 def test_nonDefaultDoctype(self):
764 L{renderElement} will write the doctype string specified by the
765 doctype keyword argument.
768 element = TestElement()
770 d = self.request.notifyFinish()
774 "".join(self.request.written),
775 ('<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"'
776 ' "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">\n'
777 '<p>Hello, world.</p>'))
785 '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"'
786 ' "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">'))
791 def test_noneDoctype(self):
793 L{renderElement} will not write out a doctype if the doctype keyword
797 element = TestElement()
799 d = self.request.notifyFinish()
803 "".join(self.request.written),
804 '<p>Hello, world.</p>')
808 renderElement(self.request, element, doctype=None)