3 namespace Drupal\Tests\Core\Template;
5 use Drupal\Component\Utility\Html;
6 use Drupal\Core\Render\Markup;
7 use Drupal\Core\Template\Attribute;
8 use Drupal\Core\Template\AttributeArray;
9 use Drupal\Core\Template\AttributeString;
10 use Drupal\Tests\UnitTestCase;
11 use Drupal\Component\Render\MarkupInterface;
14 * @coversDefaultClass \Drupal\Core\Template\Attribute
17 class AttributeTest extends UnitTestCase {
20 * Tests the constructor of the attribute class.
22 public function testConstructor() {
23 $attribute = new Attribute(['class' => ['example-class']]);
24 $this->assertTrue(isset($attribute['class']));
25 $this->assertEquals(new AttributeArray('class', ['example-class']), $attribute['class']);
27 // Test adding boolean attributes through the constructor.
28 $attribute = new Attribute(['selected' => TRUE, 'checked' => FALSE]);
29 $this->assertTrue($attribute['selected']->value());
30 $this->assertFalse($attribute['checked']->value());
32 // Test that non-array values with name "class" are cast to array.
33 $attribute = new Attribute(['class' => 'example-class']);
34 $this->assertTrue(isset($attribute['class']));
35 $this->assertEquals(new AttributeArray('class', ['example-class']), $attribute['class']);
37 // Test that safe string objects work correctly.
38 $safe_string = $this->prophesize(MarkupInterface::class);
39 $safe_string->__toString()->willReturn('example-class');
40 $attribute = new Attribute(['class' => $safe_string->reveal()]);
41 $this->assertTrue(isset($attribute['class']));
42 $this->assertEquals(new AttributeArray('class', ['example-class']), $attribute['class']);
46 * Tests set of values.
48 public function testSet() {
49 $attribute = new Attribute();
50 $attribute['class'] = ['example-class'];
52 $this->assertTrue(isset($attribute['class']));
53 $this->assertEquals(new AttributeArray('class', ['example-class']), $attribute['class']);
57 * Tests adding new values to an existing part of the attribute.
59 public function testAdd() {
60 $attribute = new Attribute(['class' => ['example-class']]);
62 $attribute['class'][] = 'other-class';
63 $this->assertEquals(new AttributeArray('class', ['example-class', 'other-class']), $attribute['class']);
67 * Tests removing of values.
69 public function testRemove() {
70 $attribute = new Attribute(['class' => ['example-class']]);
71 unset($attribute['class']);
72 $this->assertFalse(isset($attribute['class']));
76 * Tests setting attributes.
77 * @covers ::setAttribute
79 public function testSetAttribute() {
80 $attribute = new Attribute();
82 // Test adding various attributes.
83 $attributes = ['alt', 'id', 'src', 'title', 'value'];
84 foreach ($attributes as $key) {
85 foreach (['kitten', ''] as $value) {
86 $attribute = new Attribute();
87 $attribute->setAttribute($key, $value);
88 $this->assertEquals($value, $attribute[$key]);
92 // Test adding array to class.
93 $attribute = new Attribute();
94 $attribute->setAttribute('class', ['kitten', 'cat']);
95 $this->assertArrayEquals(['kitten', 'cat'], $attribute['class']->value());
97 // Test adding boolean attributes.
98 $attribute = new Attribute();
99 $attribute['checked'] = TRUE;
100 $this->assertTrue($attribute['checked']->value());
104 * Tests removing attributes.
105 * @covers ::removeAttribute
107 public function testRemoveAttribute() {
109 'alt' => 'Alternative text',
112 'style' => 'color: pink;',
114 'value' => 'ostrich',
117 $attribute = new Attribute($attributes);
120 $attribute->removeAttribute('alt');
121 $this->assertEmpty($attribute['alt']);
124 $attribute->removeAttribute('id', 'src');
125 $this->assertEmpty($attribute['id']);
126 $this->assertEmpty($attribute['src']);
128 // Single value in array.
129 $attribute->removeAttribute(['style']);
130 $this->assertEmpty($attribute['style']);
133 $attribute->removeAttribute('checked');
134 $this->assertEmpty($attribute['checked']);
136 // Multiple values in array.
137 $attribute->removeAttribute(['title', 'value']);
138 $this->assertEmpty((string) $attribute);
143 * Tests adding class attributes with the AttributeArray helper method.
146 public function testAddClasses() {
147 // Add empty Attribute object with no classes.
148 $attribute = new Attribute();
150 // Add no class on empty attribute.
151 $attribute->addClass();
152 $this->assertEmpty($attribute['class']);
154 // Test various permutations of adding values to empty Attribute objects.
155 foreach ([NULL, FALSE, '', []] as $value) {
157 $attribute->addClass($value);
158 $this->assertEmpty((string) $attribute);
161 $attribute->addClass($value, $value);
162 $this->assertEmpty((string) $attribute);
164 // Single value in array.
165 $attribute->addClass([$value]);
166 $this->assertEmpty((string) $attribute);
168 // Single value in arrays.
169 $attribute->addClass([$value], [$value]);
170 $this->assertEmpty((string) $attribute);
173 // Add one class on empty attribute.
174 $attribute->addClass('banana');
175 $this->assertArrayEquals(['banana'], $attribute['class']->value());
178 $attribute->addClass('aa');
179 $this->assertArrayEquals(['banana', 'aa'], $attribute['class']->value());
181 // Add multiple classes.
182 $attribute->addClass('xx', 'yy');
183 $this->assertArrayEquals(['banana', 'aa', 'xx', 'yy'], $attribute['class']->value());
185 // Add an array of classes.
186 $attribute->addClass(['red', 'green', 'blue']);
187 $this->assertArrayEquals(['banana', 'aa', 'xx', 'yy', 'red', 'green', 'blue'], $attribute['class']->value());
189 // Add an array of duplicate classes.
190 $attribute->addClass(['red', 'green', 'blue'], ['aa', 'aa', 'banana'], 'yy');
191 $this->assertEquals('banana aa xx yy red green blue', (string) $attribute['class']);
195 * Tests removing class attributes with the AttributeArray helper method.
196 * @covers ::removeClass
198 public function testRemoveClasses() {
199 // Add duplicate class to ensure that both duplicates are removed.
200 $classes = ['example-class', 'aa', 'xx', 'yy', 'red', 'green', 'blue', 'red'];
201 $attribute = new Attribute(['class' => $classes]);
204 $attribute->removeClass('example-class');
205 $this->assertNotContains('example-class', $attribute['class']->value());
207 // Remove multiple classes.
208 $attribute->removeClass('xx', 'yy');
209 $this->assertNotContains(['xx', 'yy'], $attribute['class']->value());
211 // Remove an array of classes.
212 $attribute->removeClass(['red', 'green', 'blue']);
213 $this->assertNotContains(['red', 'green', 'blue'], $attribute['class']->value());
215 // Remove a class that does not exist.
216 $attribute->removeClass('gg');
217 $this->assertNotContains(['gg'], $attribute['class']->value());
218 // Test that the array index remains sequential.
219 $this->assertArrayEquals(['aa'], $attribute['class']->value());
221 $attribute->removeClass('aa');
222 $this->assertEmpty((string) $attribute);
226 * Tests checking for class names with the Attribute method.
229 public function testHasClass() {
230 // Test an attribute without any classes.
231 $attribute = new Attribute();
232 $this->assertFalse($attribute->hasClass('a-class-nowhere-to-be-found'));
234 // Add a class to check for.
235 $attribute->addClass('we-totally-have-this-class');
236 // Check that this class exists.
237 $this->assertTrue($attribute->hasClass('we-totally-have-this-class'));
241 * Tests removing class attributes with the Attribute helper methods.
242 * @covers ::removeClass
245 public function testChainAddRemoveClasses() {
246 $attribute = new Attribute(
247 ['class' => ['example-class', 'red', 'green', 'blue']]
251 ->removeClass(['red', 'green', 'pink'])
252 ->addClass(['apple', 'lime', 'grapefruit'])
253 ->addClass(['banana']);
254 $expected = ['example-class', 'blue', 'apple', 'lime', 'grapefruit', 'banana'];
255 $this->assertArrayEquals($expected, $attribute['class']->value(), 'Attributes chained');
259 * Tests the twig calls to the Attribute.
260 * @dataProvider providerTestAttributeClassHelpers
262 * @covers ::removeClass
267 public function testTwigAddRemoveClasses($template, $expected, $seed_attributes = []) {
268 $loader = new \Twig_Loader_String();
269 $twig = new \Twig_Environment($loader);
270 $data = ['attributes' => new Attribute($seed_attributes)];
271 $result = $twig->render($template, $data);
272 $this->assertEquals($expected, $result);
276 * Provides tests data for testEscaping
279 * An array of test data each containing of a twig template string,
280 * a resulting string of classes and an optional array of attributes.
282 public function providerTestAttributeClassHelpers() {
284 ["{{ attributes.class }}", ''],
285 ["{{ attributes.addClass('everest').class }}", 'everest'],
286 ["{{ attributes.addClass(['k2', 'kangchenjunga']).class }}", 'k2 kangchenjunga'],
287 ["{{ attributes.addClass('lhotse', 'makalu', 'cho-oyu').class }}", 'lhotse makalu cho-oyu'],
289 "{{ attributes.addClass('nanga-parbat').class }}",
290 'dhaulagiri manaslu nanga-parbat',
291 ['class' => ['dhaulagiri', 'manaslu']],
294 "{{ attributes.removeClass('annapurna').class }}",
296 ['class' => ['annapurna', 'gasherbrum-i']],
299 "{{ attributes.removeClass(['broad peak']).class }}",
301 ['class' => ['broad peak', 'gasherbrum-ii']],
304 "{{ attributes.removeClass('gyachung-kang', 'shishapangma').class }}",
306 ['class' => ['shishapangma', 'gyachung-kang']],
309 "{{ attributes.removeClass('nuptse').addClass('annapurna-ii').class }}",
310 'himalchuli annapurna-ii',
311 ['class' => ['himalchuli', 'nuptse']],
313 // Test for the removal of an empty class name.
314 ["{{ attributes.addClass('rakaposhi', '').class }}", 'rakaposhi'],
319 * Tests iterating on the values of the attribute.
321 public function testIterate() {
322 $attribute = new Attribute(['class' => ['example-class'], 'id' => 'example-id']);
325 foreach ($attribute as $key => $value) {
327 $this->assertEquals('class', $key);
328 $this->assertEquals(new AttributeArray('class', ['example-class']), $value);
331 $this->assertEquals('id', $key);
332 $this->assertEquals(new AttributeString('id', 'example-id'), $value);
339 * Tests printing of an attribute.
341 public function testPrint() {
342 $attribute = new Attribute(['class' => ['example-class'], 'id' => 'example-id', 'enabled' => TRUE]);
344 $content = $this->randomMachineName();
345 $html = '<div' . (string) $attribute . '>' . $content . '</div>';
346 $this->assertClass('example-class', $html);
347 $this->assertNoClass('example-class2', $html);
349 $this->assertID('example-id', $html);
350 $this->assertNoID('example-id2', $html);
352 $this->assertTrue(strpos($html, 'enabled') !== FALSE);
356 * @covers ::createAttributeValue
357 * @dataProvider providerTestAttributeValues
359 public function testAttributeValues(array $attributes, $expected) {
360 $this->assertEquals($expected, (new Attribute($attributes))->__toString());
363 public function providerTestAttributeValues() {
366 $string = '"> <script>alert(123)</script>"';
367 $data['safe-object-xss1'] = [['title' => Markup::create($string)], ' title=""> alert(123)""'];
368 $data['non-safe-object-xss1'] = [['title' => $string], ' title="' . Html::escape($string) . '"'];
369 $string = '"><script>alert(123)</script>';
370 $data['safe-object-xss2'] = [['title' => Markup::create($string)], ' title="">alert(123)"'];
371 $data['non-safe-object-xss2'] = [['title' => $string], ' title="' . Html::escape($string) . '"'];
377 * Checks that the given CSS class is present in the given HTML snippet.
379 * @param string $class
380 * The CSS class to check.
381 * @param string $html
382 * The HTML snippet to check.
384 protected function assertClass($class, $html) {
385 $xpath = "//*[@class='$class']";
386 self::assertTrue((bool) $this->getXPathResultCount($xpath, $html));
390 * Checks that the given CSS class is not present in the given HTML snippet.
392 * @param string $class
393 * The CSS class to check.
394 * @param string $html
395 * The HTML snippet to check.
397 protected function assertNoClass($class, $html) {
398 $xpath = "//*[@class='$class']";
399 self::assertFalse((bool) $this->getXPathResultCount($xpath, $html));
403 * Checks that the given CSS ID is present in the given HTML snippet.
406 * The CSS ID to check.
407 * @param string $html
408 * The HTML snippet to check.
410 protected function assertID($id, $html) {
411 $xpath = "//*[@id='$id']";
412 self::assertTrue((bool) $this->getXPathResultCount($xpath, $html));
416 * Checks that the given CSS ID is not present in the given HTML snippet.
419 * The CSS ID to check.
420 * @param string $html
421 * The HTML snippet to check.
423 protected function assertNoID($id, $html) {
424 $xpath = "//*[@id='$id']";
425 self::assertFalse((bool) $this->getXPathResultCount($xpath, $html));
429 * Counts the occurrences of the given XPath query in a given HTML snippet.
431 * @param string $query
432 * The XPath query to execute.
433 * @param string $html
434 * The HTML snippet to check.
437 * The number of results that are found.
439 protected function getXPathResultCount($query, $html) {
440 $document = new \DOMDocument();
441 $document->loadHTML($html);
442 $xpath = new \DOMXPath($document);
444 return $xpath->query($query)->length;
448 * Tests the storage method.
450 public function testStorage() {
451 $attribute = new Attribute(['class' => ['example-class']]);
453 $this->assertEquals(['class' => new AttributeArray('class', ['example-class'])], $attribute->storage());