TwigExtensionTest.php 11.3 KB
Newer Older
1 2 3 4
<?php

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 11 12 13 14 15 16 17 18 19 20 21
use Drupal\Core\Template\TwigExtension;
use Drupal\Tests\UnitTestCase;

/**
 * Tests the twig extension.
 *
 * @group Template
 *
 * @coversDefaultClass \Drupal\Core\Template\TwigExtension
 */
class TwigExtensionTest extends UnitTestCase {

22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70
  /**
   * The renderer.
   *
   * @var \Drupal\Core\Render\RendererInterface|\PHPUnit_Framework_MockObject_MockObject
   */
  protected $renderer;

  /**
   * The url generator.
   *
   * @var \Drupal\Core\Routing\UrlGeneratorInterface|\PHPUnit_Framework_MockObject_MockObject
   */
  protected $urlGenerator;

  /**
   * The theme manager.
   *
   * @var \Drupal\Core\Theme\ThemeManagerInterface|\PHPUnit_Framework_MockObject_MockObject
   */
  protected $themeManager;

  /**
   * The date formatter.
   *
   * @var \Drupal\Core\Datetime\DateFormatterInterface|\PHPUnit_Framework_MockObject_MockObject
   */
  protected $dateFormatter;

  /**
   * The system under test.
   *
   * @var \Drupal\Core\Template\TwigExtension
   */
  protected $systemUnderTest;

  /**
   * {@inheritdoc}
   */
  public function setUp() {
    parent::setUp();

    $this->renderer = $this->getMock('\Drupal\Core\Render\RendererInterface');
    $this->urlGenerator = $this->getMock('\Drupal\Core\Routing\UrlGeneratorInterface');
    $this->themeManager = $this->getMock('\Drupal\Core\Theme\ThemeManagerInterface');
    $this->dateFormatter = $this->getMock('\Drupal\Core\Datetime\DateFormatterInterface');

    $this->systemUnderTest = new TwigExtension($this->renderer, $this->urlGenerator, $this->themeManager, $this->dateFormatter);
  }

71 72 73
  /**
   * Tests the escaping
   *
74
   * @dataProvider providerTestEscaping
75 76 77 78 79
   */
  public function testEscaping($template, $expected) {
    $twig = new \Twig_Environment(NULL, array(
      'debug' => TRUE,
      'cache' => FALSE,
80
      'autoescape' => 'html',
81
      'optimizations' => 0,
82
    ));
83
    $twig->addExtension($this->systemUnderTest);
84 85 86 87

    $nodes = $twig->parse($twig->tokenize($template));

    $this->assertSame($expected, $nodes->getNode('body')
88 89
      ->getNode(0)
      ->getNode('expr') instanceof \Twig_Node_Expression_Filter);
90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122
  }

  /**
   * Provides tests data for testEscaping
   *
   * @return array
   *   An array of test data each containing of a twig template string and
   *   a boolean expecting whether the path will be safe.
   */
  public function providerTestEscaping() {
    return array(
      array('{{ path("foo") }}', FALSE),
      array('{{ path("foo", {}) }}', FALSE),
      array('{{ path("foo", { foo: "foo" }) }}', FALSE),
      array('{{ path("foo", foo) }}', TRUE),
      array('{{ path("foo", { foo: foo }) }}', TRUE),
      array('{{ path("foo", { foo: ["foo", "bar"] }) }}', TRUE),
      array('{{ path("foo", { foo: "foo", bar: "bar" }) }}', TRUE),
      array('{{ path(name = "foo", parameters = {}) }}', FALSE),
      array('{{ path(name = "foo", parameters = { foo: "foo" }) }}', FALSE),
      array('{{ path(name = "foo", parameters = foo) }}', TRUE),
      array(
        '{{ path(name = "foo", parameters = { foo: ["foo", "bar"] }) }}',
        TRUE
      ),
      array('{{ path(name = "foo", parameters = { foo: foo }) }}', TRUE),
      array(
        '{{ path(name = "foo", parameters = { foo: "foo", bar: "bar" }) }}',
        TRUE
      ),
    );
  }

123 124 125 126 127 128 129
  /**
   * Tests the active_theme function.
   */
  public function testActiveTheme() {
    $active_theme = $this->getMockBuilder('\Drupal\Core\Theme\ActiveTheme')
      ->disableOriginalConstructor()
      ->getMock();
130
    $active_theme->expects($this->once())
131 132
      ->method('getName')
      ->willReturn('test_theme');
133
    $this->themeManager->expects($this->once())
134 135 136 137 138
      ->method('getActiveTheme')
      ->willReturn($active_theme);

    $loader = new \Twig_Loader_String();
    $twig = new \Twig_Environment($loader);
139
    $twig->addExtension($this->systemUnderTest);
140 141 142 143
    $result = $twig->render('{{ active_theme() }}');
    $this->assertEquals('test_theme', $result);
  }

144 145 146 147
  /**
   * Tests the format_date filter.
   */
  public function testFormatDate() {
148
    $this->dateFormatter->expects($this->exactly(2))
149 150 151 152 153
      ->method('format')
      ->willReturn('1978-11-19');

    $loader = new StringLoader();
    $twig = new \Twig_Environment($loader);
154
    $twig->addExtension($this->systemUnderTest);
155
    $result = $twig->render('{{ time|format_date("html_date") }}');
156
    $this->assertEquals($this->dateFormatter->format('html_date'), $result);
157 158
  }

159 160 161 162 163 164 165 166 167 168 169
  /**
   * Tests the active_theme_path function.
   */
  public function testActiveThemePath() {
    $active_theme = $this->getMockBuilder('\Drupal\Core\Theme\ActiveTheme')
      ->disableOriginalConstructor()
      ->getMock();
    $active_theme
      ->expects($this->once())
      ->method('getPath')
      ->willReturn('foo/bar');
170
    $this->themeManager->expects($this->once())
171 172 173 174 175
      ->method('getActiveTheme')
      ->willReturn($active_theme);

    $loader = new \Twig_Loader_String();
    $twig = new \Twig_Environment($loader);
176
    $twig->addExtension($this->systemUnderTest);
177 178 179 180
    $result = $twig->render('{{ active_theme_path() }}');
    $this->assertEquals('foo/bar', $result);
  }

181
  /**
182
   * Tests the escaping of objects implementing MarkupInterface.
183 184 185 186 187 188 189
   *
   * @covers ::escapeFilter
   */
  public function testSafeStringEscaping() {
    $twig = new \Twig_Environment(NULL, array(
      'debug' => TRUE,
      'cache' => FALSE,
190
      'autoescape' => 'html',
191
      'optimizations' => 0,
192 193 194
    ));

    // By default, TwigExtension will attempt to cast objects to strings.
195 196
    // Ensure objects that implement MarkupInterface are unchanged.
    $safe_string = $this->getMock('\Drupal\Component\Render\MarkupInterface');
197
    $this->assertSame($safe_string, $this->systemUnderTest->escapeFilter($twig, $safe_string, 'html', 'UTF-8', TRUE));
198

199
    // Ensure objects that do not implement MarkupInterface are escaped.
200
    $string_object = new TwigExtensionTestString("<script>alert('here');</script>");
201
    $this->assertSame('&lt;script&gt;alert(&#039;here&#039;);&lt;/script&gt;', $this->systemUnderTest->escapeFilter($twig, $string_object, 'html', 'UTF-8', TRUE));
202 203
  }

204 205 206 207
  /**
   * @covers ::safeJoin
   */
  public function testSafeJoin() {
208 209 210 211
    $this->renderer->expects($this->any())
      ->method('render')
      ->with(['#markup' => '<strong>will be rendered</strong>', '#printed' => FALSE])
      ->willReturn('<strong>will be rendered</strong>');
212 213 214 215

    $twig_environment = $this->prophesize(TwigEnvironment::class)->reveal();

    // Simulate t().
216 217 218
    $markup = $this->prophesize(TranslatableMarkup::class);
    $markup->__toString()->willReturn('<em>will be markup</em>');
    $markup = $markup->reveal();
219 220 221

    $items = [
      '<em>will be escaped</em>',
222
      $markup,
223
      ['#markup' => '<strong>will be rendered</strong>'],
224
    ];
225
    $result = $this->systemUnderTest->safeJoin($twig_environment, $items, '<br/>');
226
    $this->assertEquals('&lt;em&gt;will be escaped&lt;/em&gt;<br/><em>will be markup</em><br/><strong>will be rendered</strong>', $result);
227 228 229 230 231 232 233

    // Ensure safe_join Twig filter supports Traversable variables.
    $items = new \ArrayObject([
      '<em>will be escaped</em>',
      $markup,
      ['#markup' => '<strong>will be rendered</strong>'],
    ]);
234
    $result = $this->systemUnderTest->safeJoin($twig_environment, $items, ', ');
235 236 237 238
    $this->assertEquals('&lt;em&gt;will be escaped&lt;/em&gt;, <em>will be markup</em>, <strong>will be rendered</strong>', $result);

    // Ensure safe_join Twig filter supports empty variables.
    $items = NULL;
239
    $result = $this->systemUnderTest->safeJoin($twig_environment, $items, '<br>');
240
    $this->assertEmpty($result);
241 242
  }

243 244 245 246
  /**
   * @dataProvider providerTestRenderVar
   */
  public function testRenderVar($result, $input) {
247 248 249 250
    $this->renderer->expects($this->any())
      ->method('render')
      ->with($result += ['#printed' => FALSE])
      ->willReturn('Rendered output');
251

252
    $this->assertEquals('Rendered output', $this->systemUnderTest->renderVar($input));
253 254 255 256 257 258 259 260 261 262 263 264 265
  }

  public function providerTestRenderVar() {
    $data = [];

    $renderable = $this->prophesize(RenderableInterface::class);
    $render_array = ['#type' => 'test', '#var' => 'giraffe'];
    $renderable->toRenderable()->willReturn($render_array);
    $data['renderable'] = [$render_array, $renderable->reveal()];

    return $data;
  }

266 267 268 269 270 271 272 273 274 275 276 277 278
  /**
   * @covers ::escapeFilter
   * @covers ::bubbleArgMetadata
   */
  public function testEscapeWithGeneratedLink() {
    $twig = new \Twig_Environment(NULL, [
        'debug' => TRUE,
        'cache' => FALSE,
        'autoescape' => 'html',
        'optimizations' => 0,
      ]
    );

279
    $twig->addExtension($this->systemUnderTest);
280 281 282 283 284
    $link = new GeneratedLink();
    $link->setGeneratedLink('<a href="http://example.com"></a>');
    $link->addCacheTags(['foo']);
    $link->addAttachments(['library' => ['system/base']]);

285 286 287 288 289 290 291 292 293 294 295
    $this->renderer->expects($this->atLeastOnce())
      ->method('render')
      ->with([
        "#cache" => [
          "contexts" => [],
          "tags" => ["foo"],
          "max-age" => -1,
        ],
        "#attached" => ['library' => ['system/base']],
      ]);
    $result = $this->systemUnderTest->escapeFilter($twig, $link, 'html', NULL, TRUE);
296 297 298 299 300 301 302 303 304 305 306 307 308
    $this->assertEquals('<a href="http://example.com"></a>', $result);
  }

  /**
   * @covers ::renderVar
   * @covers ::bubbleArgMetadata
   */
  public function testRenderVarWithGeneratedLink() {
    $link = new GeneratedLink();
    $link->setGeneratedLink('<a href="http://example.com"></a>');
    $link->addCacheTags(['foo']);
    $link->addAttachments(['library' => ['system/base']]);

309 310 311 312 313 314 315 316 317 318 319
    $this->renderer->expects($this->atLeastOnce())
      ->method('render')
      ->with([
        "#cache" => [
          "contexts" => [],
          "tags" => ["foo"],
          "max-age" => -1,
        ],
        "#attached" => ['library' => ['system/base']],
      ]);
    $result = $this->systemUnderTest->renderVar($link);
320 321 322
    $this->assertEquals('<a href="http://example.com"></a>', $result);
  }

323 324 325 326 327 328 329 330
  /**
   * Tests creating attributes within a Twig template.
   *
   * @covers ::createAttribute
   */
  public function testCreateAttribute() {
    $loader = new StringLoader();
    $twig = new \Twig_Environment($loader);
331
    $twig->addExtension($this->systemUnderTest);
332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347

    $iterations = [
      ['class' => ['kittens'], 'data-toggle' => 'modal', 'data-lang' => 'es'],
      ['id' => 'puppies', 'data-value' => 'foo', 'data-lang' => 'en'],
      [],
    ];
    $result = $twig->render("{% for iteration in iterations %}<div{{ create_attribute(iteration) }}></div>{% endfor %}", ['iterations' => $iterations]);
    $expected = '<div class="kittens" data-toggle="modal" data-lang="es"></div><div id="puppies" data-value="foo" data-lang="en"></div><div></div>';
    $this->assertEquals($expected, $result);

    // Test default creation of empty attribute object and using its method.
    $result = $twig->render("<div{{ create_attribute().addClass('meow') }}></div>");
    $expected = '<div class="meow"></div>';
    $this->assertEquals($expected, $result);
  }

348 349 350 351 352 353 354 355 356 357 358 359 360 361
}

class TwigExtensionTestString {

  protected $string;

  public function __construct($string) {
    $this->string = $string;
  }

  public function __toString() {
    return $this->string;
  }

362
}