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.
19 * @coversDefaultClass \Drupal\Core\Template\TwigExtension
21 class TwigExtensionTest extends UnitTestCase {
26 * @var \Drupal\Core\Render\RendererInterface|\PHPUnit_Framework_MockObject_MockObject
33 * @var \Drupal\Core\Routing\UrlGeneratorInterface|\PHPUnit_Framework_MockObject_MockObject
35 protected $urlGenerator;
40 * @var \Drupal\Core\Theme\ThemeManagerInterface|\PHPUnit_Framework_MockObject_MockObject
42 protected $themeManager;
47 * @var \Drupal\Core\Datetime\DateFormatterInterface|\PHPUnit_Framework_MockObject_MockObject
49 protected $dateFormatter;
52 * The system under test.
54 * @var \Drupal\Core\Template\TwigExtension
56 protected $systemUnderTest;
61 public function setUp() {
64 $this->renderer = $this->getMock('\Drupal\Core\Render\RendererInterface');
65 $this->urlGenerator = $this->getMock('\Drupal\Core\Routing\UrlGeneratorInterface');
66 $this->themeManager = $this->getMock('\Drupal\Core\Theme\ThemeManagerInterface');
67 $this->dateFormatter = $this->getMock('\Drupal\Core\Datetime\DateFormatterInterface');
69 $this->systemUnderTest = new TwigExtension($this->renderer, $this->urlGenerator, $this->themeManager, $this->dateFormatter);
75 * @dataProvider providerTestEscaping
77 public function testEscaping($template, $expected) {
78 $twig = new \Twig_Environment(NULL, [
81 'autoescape' => 'html',
84 $twig->addExtension($this->systemUnderTest);
86 $nodes = $twig->parse($twig->tokenize($template));
88 $this->assertSame($expected, $nodes->getNode('body')
90 ->getNode('expr') instanceof \Twig_Node_Expression_Filter);
94 * Provides tests data for testEscaping
97 * An array of test data each containing of a twig template string and
98 * a boolean expecting whether the path will be safe.
100 public function providerTestEscaping() {
102 ['{{ path("foo") }}', FALSE],
103 ['{{ path("foo", {}) }}', FALSE],
104 ['{{ path("foo", { foo: "foo" }) }}', FALSE],
105 ['{{ path("foo", foo) }}', TRUE],
106 ['{{ path("foo", { foo: foo }) }}', TRUE],
107 ['{{ path("foo", { foo: ["foo", "bar"] }) }}', TRUE],
108 ['{{ path("foo", { foo: "foo", bar: "bar" }) }}', TRUE],
109 ['{{ path(name = "foo", parameters = {}) }}', FALSE],
110 ['{{ path(name = "foo", parameters = { foo: "foo" }) }}', FALSE],
111 ['{{ path(name = "foo", parameters = foo) }}', TRUE],
113 '{{ path(name = "foo", parameters = { foo: ["foo", "bar"] }) }}',
116 ['{{ path(name = "foo", parameters = { foo: foo }) }}', TRUE],
118 '{{ path(name = "foo", parameters = { foo: "foo", bar: "bar" }) }}',
125 * Tests the active_theme function.
127 public function testActiveTheme() {
128 $active_theme = $this->getMockBuilder('\Drupal\Core\Theme\ActiveTheme')
129 ->disableOriginalConstructor()
131 $active_theme->expects($this->once())
133 ->willReturn('test_theme');
134 $this->themeManager->expects($this->once())
135 ->method('getActiveTheme')
136 ->willReturn($active_theme);
138 $loader = new \Twig_Loader_String();
139 $twig = new \Twig_Environment($loader);
140 $twig->addExtension($this->systemUnderTest);
141 $result = $twig->render('{{ active_theme() }}');
142 $this->assertEquals('test_theme', $result);
146 * Tests the format_date filter.
148 public function testFormatDate() {
149 $this->dateFormatter->expects($this->exactly(2))
151 ->willReturn('1978-11-19');
153 $loader = new StringLoader();
154 $twig = new \Twig_Environment($loader);
155 $twig->addExtension($this->systemUnderTest);
156 $result = $twig->render('{{ time|format_date("html_date") }}');
157 $this->assertEquals($this->dateFormatter->format('html_date'), $result);
161 * Tests the active_theme_path function.
163 public function testActiveThemePath() {
164 $active_theme = $this->getMockBuilder('\Drupal\Core\Theme\ActiveTheme')
165 ->disableOriginalConstructor()
168 ->expects($this->once())
170 ->willReturn('foo/bar');
171 $this->themeManager->expects($this->once())
172 ->method('getActiveTheme')
173 ->willReturn($active_theme);
175 $loader = new \Twig_Loader_String();
176 $twig = new \Twig_Environment($loader);
177 $twig->addExtension($this->systemUnderTest);
178 $result = $twig->render('{{ active_theme_path() }}');
179 $this->assertEquals('foo/bar', $result);
183 * Tests the escaping of objects implementing MarkupInterface.
185 * @covers ::escapeFilter
187 public function testSafeStringEscaping() {
188 $twig = new \Twig_Environment(NULL, [
191 'autoescape' => 'html',
192 'optimizations' => 0,
195 // By default, TwigExtension will attempt to cast objects to strings.
196 // Ensure objects that implement MarkupInterface are unchanged.
197 $safe_string = $this->getMock('\Drupal\Component\Render\MarkupInterface');
198 $this->assertSame($safe_string, $this->systemUnderTest->escapeFilter($twig, $safe_string, 'html', 'UTF-8', TRUE));
200 // Ensure objects that do not implement MarkupInterface are escaped.
201 $string_object = new TwigExtensionTestString("<script>alert('here');</script>");
202 $this->assertSame('<script>alert('here');</script>', $this->systemUnderTest->escapeFilter($twig, $string_object, 'html', 'UTF-8', TRUE));
208 public function testSafeJoin() {
209 $this->renderer->expects($this->any())
211 ->with(['#markup' => '<strong>will be rendered</strong>', '#printed' => FALSE])
212 ->willReturn('<strong>will be rendered</strong>');
214 $twig_environment = $this->prophesize(TwigEnvironment::class)->reveal();
217 $markup = $this->prophesize(TranslatableMarkup::class);
218 $markup->__toString()->willReturn('<em>will be markup</em>');
219 $markup = $markup->reveal();
222 '<em>will be escaped</em>',
224 ['#markup' => '<strong>will be rendered</strong>'],
226 $result = $this->systemUnderTest->safeJoin($twig_environment, $items, '<br/>');
227 $this->assertEquals('<em>will be escaped</em><br/><em>will be markup</em><br/><strong>will be rendered</strong>', $result);
229 // Ensure safe_join Twig filter supports Traversable variables.
230 $items = new \ArrayObject([
231 '<em>will be escaped</em>',
233 ['#markup' => '<strong>will be rendered</strong>'],
235 $result = $this->systemUnderTest->safeJoin($twig_environment, $items, ', ');
236 $this->assertEquals('<em>will be escaped</em>, <em>will be markup</em>, <strong>will be rendered</strong>', $result);
238 // Ensure safe_join Twig filter supports empty variables.
240 $result = $this->systemUnderTest->safeJoin($twig_environment, $items, '<br>');
241 $this->assertEmpty($result);
245 * @dataProvider providerTestRenderVar
247 public function testRenderVar($result, $input) {
248 $this->renderer->expects($this->any())
250 ->with($result += ['#printed' => FALSE])
251 ->willReturn('Rendered output');
253 $this->assertEquals('Rendered output', $this->systemUnderTest->renderVar($input));
256 public function providerTestRenderVar() {
259 $renderable = $this->prophesize(RenderableInterface::class);
260 $render_array = ['#type' => 'test', '#var' => 'giraffe'];
261 $renderable->toRenderable()->willReturn($render_array);
262 $data['renderable'] = [$render_array, $renderable->reveal()];
268 * @covers ::escapeFilter
269 * @covers ::bubbleArgMetadata
271 public function testEscapeWithGeneratedLink() {
272 $twig = new \Twig_Environment(NULL, [
275 'autoescape' => 'html',
276 'optimizations' => 0,
280 $twig->addExtension($this->systemUnderTest);
281 $link = new GeneratedLink();
282 $link->setGeneratedLink('<a href="http://example.com"></a>');
283 $link->addCacheTags(['foo']);
284 $link->addAttachments(['library' => ['system/base']]);
286 $this->renderer->expects($this->atLeastOnce())
294 "#attached" => ['library' => ['system/base']],
296 $result = $this->systemUnderTest->escapeFilter($twig, $link, 'html', NULL, TRUE);
297 $this->assertEquals('<a href="http://example.com"></a>', $result);
301 * @covers ::renderVar
302 * @covers ::bubbleArgMetadata
304 public function testRenderVarWithGeneratedLink() {
305 $link = new GeneratedLink();
306 $link->setGeneratedLink('<a href="http://example.com"></a>');
307 $link->addCacheTags(['foo']);
308 $link->addAttachments(['library' => ['system/base']]);
310 $this->renderer->expects($this->atLeastOnce())
318 "#attached" => ['library' => ['system/base']],
320 $result = $this->systemUnderTest->renderVar($link);
321 $this->assertEquals('<a href="http://example.com"></a>', $result);
325 * Tests creating attributes within a Twig template.
327 * @covers ::createAttribute
329 public function testCreateAttribute() {
330 $loader = new StringLoader();
331 $twig = new \Twig_Environment($loader);
332 $twig->addExtension($this->systemUnderTest);
335 ['class' => ['kittens'], 'data-toggle' => 'modal', 'data-lang' => 'es'],
336 ['id' => 'puppies', 'data-value' => 'foo', 'data-lang' => 'en'],
339 $result = $twig->render("{% for iteration in iterations %}<div{{ create_attribute(iteration) }}></div>{% endfor %}", ['iterations' => $iterations]);
340 $expected = '<div class="kittens" data-toggle="modal" data-lang="es"></div><div id="puppies" data-value="foo" data-lang="en"></div><div></div>';
341 $this->assertEquals($expected, $result);
343 // Test default creation of empty attribute object and using its method.
344 $result = $twig->render("<div{{ create_attribute().addClass('meow') }}></div>");
345 $expected = '<div class="meow"></div>';
346 $this->assertEquals($expected, $result);
352 public function testLinkWithOverriddenAttributes() {
353 $url = Url::fromRoute('<front>', [], ['attributes' => ['class' => ['foo']]]);
355 $build = $this->systemUnderTest->getLink('test', $url, ['class' => ['bar']]);
357 $this->assertEquals(['foo', 'bar'], $build['#url']->getOption('attributes')['class']);
362 class TwigExtensionTestString {
366 public function __construct($string) {
367 $this->string = $string;
370 public function __toString() {
371 return $this->string;