3 namespace Drupal\Tests\Core\Template;
5 use Drupal\Core\GeneratedLink;
6 use Drupal\Core\Render\RenderableInterface;
7 use Drupal\Core\StringTranslation\TranslatableMarkup;
8 use Drupal\Core\Template\Loader\StringLoader;
9 use Drupal\Core\Template\TwigEnvironment;
10 use Drupal\Core\Template\TwigExtension;
12 use Drupal\Tests\UnitTestCase;
15 * Tests the twig extension.
20 * @coversDefaultClass \Drupal\Core\Template\TwigExtension
22 class TwigExtensionTest extends UnitTestCase {
27 * @var \Drupal\Core\Render\RendererInterface|\PHPUnit_Framework_MockObject_MockObject
34 * @var \Drupal\Core\Routing\UrlGeneratorInterface|\PHPUnit_Framework_MockObject_MockObject
36 protected $urlGenerator;
41 * @var \Drupal\Core\Theme\ThemeManagerInterface|\PHPUnit_Framework_MockObject_MockObject
43 protected $themeManager;
48 * @var \Drupal\Core\Datetime\DateFormatterInterface|\PHPUnit_Framework_MockObject_MockObject
50 protected $dateFormatter;
53 * The system under test.
55 * @var \Drupal\Core\Template\TwigExtension
57 protected $systemUnderTest;
62 public function setUp() {
65 $this->renderer = $this->getMock('\Drupal\Core\Render\RendererInterface');
66 $this->urlGenerator = $this->getMock('\Drupal\Core\Routing\UrlGeneratorInterface');
67 $this->themeManager = $this->getMock('\Drupal\Core\Theme\ThemeManagerInterface');
68 $this->dateFormatter = $this->getMock('\Drupal\Core\Datetime\DateFormatterInterface');
70 $this->systemUnderTest = new TwigExtension($this->renderer, $this->urlGenerator, $this->themeManager, $this->dateFormatter);
76 * @dataProvider providerTestEscaping
80 public function testEscaping($template, $expected) {
81 $twig = new \Twig_Environment(NULL, [
84 'autoescape' => 'html',
87 $twig->addExtension($this->systemUnderTest);
89 $nodes = $twig->parse($twig->tokenize($template));
91 $this->assertSame($expected, $nodes->getNode('body')
93 ->getNode('expr') instanceof \Twig_Node_Expression_Filter);
97 * Provides tests data for testEscaping
100 * An array of test data each containing of a twig template string and
101 * a boolean expecting whether the path will be safe.
103 public function providerTestEscaping() {
105 ['{{ path("foo") }}', FALSE],
106 ['{{ path("foo", {}) }}', FALSE],
107 ['{{ path("foo", { foo: "foo" }) }}', FALSE],
108 ['{{ path("foo", foo) }}', TRUE],
109 ['{{ path("foo", { foo: foo }) }}', TRUE],
110 ['{{ path("foo", { foo: ["foo", "bar"] }) }}', TRUE],
111 ['{{ path("foo", { foo: "foo", bar: "bar" }) }}', TRUE],
112 ['{{ path(name = "foo", parameters = {}) }}', FALSE],
113 ['{{ path(name = "foo", parameters = { foo: "foo" }) }}', FALSE],
114 ['{{ path(name = "foo", parameters = foo) }}', TRUE],
116 '{{ path(name = "foo", parameters = { foo: ["foo", "bar"] }) }}',
119 ['{{ path(name = "foo", parameters = { foo: foo }) }}', TRUE],
121 '{{ path(name = "foo", parameters = { foo: "foo", bar: "bar" }) }}',
128 * Tests the active_theme function.
132 public function testActiveTheme() {
133 $active_theme = $this->getMockBuilder('\Drupal\Core\Theme\ActiveTheme')
134 ->disableOriginalConstructor()
136 $active_theme->expects($this->once())
138 ->willReturn('test_theme');
139 $this->themeManager->expects($this->once())
140 ->method('getActiveTheme')
141 ->willReturn($active_theme);
143 $loader = new \Twig_Loader_String();
144 $twig = new \Twig_Environment($loader);
145 $twig->addExtension($this->systemUnderTest);
146 $result = $twig->render('{{ active_theme() }}');
147 $this->assertEquals('test_theme', $result);
151 * Tests the format_date filter.
153 public function testFormatDate() {
154 $this->dateFormatter->expects($this->exactly(2))
156 ->willReturn('1978-11-19');
158 $loader = new StringLoader();
159 $twig = new \Twig_Environment($loader);
160 $twig->addExtension($this->systemUnderTest);
161 $result = $twig->render('{{ time|format_date("html_date") }}');
162 $this->assertEquals($this->dateFormatter->format('html_date'), $result);
166 * Tests the active_theme_path function.
168 public function testActiveThemePath() {
169 $active_theme = $this->getMockBuilder('\Drupal\Core\Theme\ActiveTheme')
170 ->disableOriginalConstructor()
173 ->expects($this->once())
175 ->willReturn('foo/bar');
176 $this->themeManager->expects($this->once())
177 ->method('getActiveTheme')
178 ->willReturn($active_theme);
180 $loader = new \Twig_Loader_String();
181 $twig = new \Twig_Environment($loader);
182 $twig->addExtension($this->systemUnderTest);
183 $result = $twig->render('{{ active_theme_path() }}');
184 $this->assertEquals('foo/bar', $result);
188 * Tests the escaping of objects implementing MarkupInterface.
190 * @covers ::escapeFilter
194 public function testSafeStringEscaping() {
195 $twig = new \Twig_Environment(NULL, [
198 'autoescape' => 'html',
199 'optimizations' => 0,
202 // By default, TwigExtension will attempt to cast objects to strings.
203 // Ensure objects that implement MarkupInterface are unchanged.
204 $safe_string = $this->getMock('\Drupal\Component\Render\MarkupInterface');
205 $this->assertSame($safe_string, $this->systemUnderTest->escapeFilter($twig, $safe_string, 'html', 'UTF-8', TRUE));
207 // Ensure objects that do not implement MarkupInterface are escaped.
208 $string_object = new TwigExtensionTestString("<script>alert('here');</script>");
209 $this->assertSame('<script>alert('here');</script>', $this->systemUnderTest->escapeFilter($twig, $string_object, 'html', 'UTF-8', TRUE));
215 public function testSafeJoin() {
216 $this->renderer->expects($this->any())
218 ->with(['#markup' => '<strong>will be rendered</strong>', '#printed' => FALSE])
219 ->willReturn('<strong>will be rendered</strong>');
221 $twig_environment = $this->prophesize(TwigEnvironment::class)->reveal();
224 $markup = $this->prophesize(TranslatableMarkup::class);
225 $markup->__toString()->willReturn('<em>will be markup</em>');
226 $markup = $markup->reveal();
229 '<em>will be escaped</em>',
231 ['#markup' => '<strong>will be rendered</strong>'],
233 $result = $this->systemUnderTest->safeJoin($twig_environment, $items, '<br/>');
234 $this->assertEquals('<em>will be escaped</em><br/><em>will be markup</em><br/><strong>will be rendered</strong>', $result);
236 // Ensure safe_join Twig filter supports Traversable variables.
237 $items = new \ArrayObject([
238 '<em>will be escaped</em>',
240 ['#markup' => '<strong>will be rendered</strong>'],
242 $result = $this->systemUnderTest->safeJoin($twig_environment, $items, ', ');
243 $this->assertEquals('<em>will be escaped</em>, <em>will be markup</em>, <strong>will be rendered</strong>', $result);
245 // Ensure safe_join Twig filter supports empty variables.
247 $result = $this->systemUnderTest->safeJoin($twig_environment, $items, '<br>');
248 $this->assertEmpty($result);
252 * @dataProvider providerTestRenderVar
254 public function testRenderVar($result, $input) {
255 $this->renderer->expects($this->any())
257 ->with($result += ['#printed' => FALSE])
258 ->willReturn('Rendered output');
260 $this->assertEquals('Rendered output', $this->systemUnderTest->renderVar($input));
263 public function providerTestRenderVar() {
266 $renderable = $this->prophesize(RenderableInterface::class);
267 $render_array = ['#type' => 'test', '#var' => 'giraffe'];
268 $renderable->toRenderable()->willReturn($render_array);
269 $data['renderable'] = [$render_array, $renderable->reveal()];
275 * @covers ::escapeFilter
276 * @covers ::bubbleArgMetadata
280 public function testEscapeWithGeneratedLink() {
281 $twig = new \Twig_Environment(NULL, [
284 'autoescape' => 'html',
285 'optimizations' => 0,
289 $twig->addExtension($this->systemUnderTest);
290 $link = new GeneratedLink();
291 $link->setGeneratedLink('<a href="http://example.com"></a>');
292 $link->addCacheTags(['foo']);
293 $link->addAttachments(['library' => ['system/base']]);
295 $this->renderer->expects($this->atLeastOnce())
303 "#attached" => ['library' => ['system/base']],
305 $result = $this->systemUnderTest->escapeFilter($twig, $link, 'html', NULL, TRUE);
306 $this->assertEquals('<a href="http://example.com"></a>', $result);
310 * @covers ::renderVar
311 * @covers ::bubbleArgMetadata
313 public function testRenderVarWithGeneratedLink() {
314 $link = new GeneratedLink();
315 $link->setGeneratedLink('<a href="http://example.com"></a>');
316 $link->addCacheTags(['foo']);
317 $link->addAttachments(['library' => ['system/base']]);
319 $this->renderer->expects($this->atLeastOnce())
327 "#attached" => ['library' => ['system/base']],
329 $result = $this->systemUnderTest->renderVar($link);
330 $this->assertEquals('<a href="http://example.com"></a>', $result);
334 * Tests creating attributes within a Twig template.
336 * @covers ::createAttribute
338 public function testCreateAttribute() {
339 $loader = new StringLoader();
340 $twig = new \Twig_Environment($loader);
341 $twig->addExtension($this->systemUnderTest);
344 ['class' => ['kittens'], 'data-toggle' => 'modal', 'data-lang' => 'es'],
345 ['id' => 'puppies', 'data-value' => 'foo', 'data-lang' => 'en'],
348 $result = $twig->render("{% for iteration in iterations %}<div{{ create_attribute(iteration) }}></div>{% endfor %}", ['iterations' => $iterations]);
349 $expected = '<div class="kittens" data-toggle="modal" data-lang="es"></div><div id="puppies" data-value="foo" data-lang="en"></div><div></div>';
350 $this->assertEquals($expected, $result);
352 // Test default creation of empty attribute object and using its method.
353 $result = $twig->render("<div{{ create_attribute().addClass('meow') }}></div>");
354 $expected = '<div class="meow"></div>';
355 $this->assertEquals($expected, $result);
361 public function testLinkWithOverriddenAttributes() {
362 $url = Url::fromRoute('<front>', [], ['attributes' => ['class' => ['foo']]]);
364 $build = $this->systemUnderTest->getLink('test', $url, ['class' => ['bar']]);
366 $this->assertEquals(['foo', 'bar'], $build['#url']->getOption('attributes')['class']);
371 class TwigExtensionTestString {
375 public function __construct($string) {
376 $this->string = $string;
379 public function __toString() {
380 return $this->string;