c9bccceec9ce510caac74661c00fbc345ed47fca
[yaffs-website] / web / core / tests / Drupal / Tests / Core / Template / AttributeTest.php
1 <?php
2
3 namespace Drupal\Tests\Core\Template;
4
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;
12
13 /**
14  * @coversDefaultClass \Drupal\Core\Template\Attribute
15  * @group Template
16  */
17 class AttributeTest extends UnitTestCase {
18
19   /**
20    * Tests the constructor of the attribute class.
21    */
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']);
26
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());
31
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']);
36
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']);
43   }
44
45   /**
46    * Tests set of values.
47    */
48   public function testSet() {
49     $attribute = new Attribute();
50     $attribute['class'] = ['example-class'];
51
52     $this->assertTrue(isset($attribute['class']));
53     $this->assertEquals(new AttributeArray('class', ['example-class']), $attribute['class']);
54   }
55
56   /**
57    * Tests adding new values to an existing part of the attribute.
58    */
59   public function testAdd() {
60     $attribute = new Attribute(['class' => ['example-class']]);
61
62     $attribute['class'][] = 'other-class';
63     $this->assertEquals(new AttributeArray('class', ['example-class', 'other-class']), $attribute['class']);
64   }
65
66   /**
67    * Tests removing of values.
68    */
69   public function testRemove() {
70     $attribute = new Attribute(['class' => ['example-class']]);
71     unset($attribute['class']);
72     $this->assertFalse(isset($attribute['class']));
73   }
74
75   /**
76    * Tests setting attributes.
77    * @covers ::setAttribute
78    */
79   public function testSetAttribute() {
80     $attribute = new Attribute();
81
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]);
89       }
90     }
91
92     // Test adding array to class.
93     $attribute = new Attribute();
94     $attribute->setAttribute('class', ['kitten', 'cat']);
95     $this->assertArrayEquals(['kitten', 'cat'], $attribute['class']->value());
96
97     // Test adding boolean attributes.
98     $attribute = new Attribute();
99     $attribute['checked'] = TRUE;
100     $this->assertTrue($attribute['checked']->value());
101   }
102
103   /**
104    * Tests removing attributes.
105    * @covers ::removeAttribute
106    */
107   public function testRemoveAttribute() {
108     $attributes = [
109       'alt' => 'Alternative text',
110       'id' => 'bunny',
111       'src' => 'zebra',
112       'style' => 'color: pink;',
113       'title' => 'kitten',
114       'value' => 'ostrich',
115       'checked' => TRUE,
116     ];
117     $attribute = new Attribute($attributes);
118
119     // Single value.
120     $attribute->removeAttribute('alt');
121     $this->assertEmpty($attribute['alt']);
122
123     // Multiple values.
124     $attribute->removeAttribute('id', 'src');
125     $this->assertEmpty($attribute['id']);
126     $this->assertEmpty($attribute['src']);
127
128     // Single value in array.
129     $attribute->removeAttribute(['style']);
130     $this->assertEmpty($attribute['style']);
131
132     // Boolean value.
133     $attribute->removeAttribute('checked');
134     $this->assertEmpty($attribute['checked']);
135
136     // Multiple values in array.
137     $attribute->removeAttribute(['title', 'value']);
138     $this->assertEmpty((string) $attribute);
139
140   }
141
142   /**
143    * Tests adding class attributes with the AttributeArray helper method.
144    * @covers ::addClass
145    */
146   public function testAddClasses() {
147     // Add empty Attribute object with no classes.
148     $attribute = new Attribute();
149
150     // Add no class on empty attribute.
151     $attribute->addClass();
152     $this->assertEmpty($attribute['class']);
153
154     // Test various permutations of adding values to empty Attribute objects.
155     foreach ([NULL, FALSE, '', []] as $value) {
156       // Single value.
157       $attribute->addClass($value);
158       $this->assertEmpty((string) $attribute);
159
160       // Multiple values.
161       $attribute->addClass($value, $value);
162       $this->assertEmpty((string) $attribute);
163
164       // Single value in array.
165       $attribute->addClass([$value]);
166       $this->assertEmpty((string) $attribute);
167
168       // Single value in arrays.
169       $attribute->addClass([$value], [$value]);
170       $this->assertEmpty((string) $attribute);
171     }
172
173     // Add one class on empty attribute.
174     $attribute->addClass('banana');
175     $this->assertArrayEquals(['banana'], $attribute['class']->value());
176
177     // Add one class.
178     $attribute->addClass('aa');
179     $this->assertArrayEquals(['banana', 'aa'], $attribute['class']->value());
180
181     // Add multiple classes.
182     $attribute->addClass('xx', 'yy');
183     $this->assertArrayEquals(['banana', 'aa', 'xx', 'yy'], $attribute['class']->value());
184
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());
188
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']);
192   }
193
194   /**
195    * Tests removing class attributes with the AttributeArray helper method.
196    * @covers ::removeClass
197    */
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]);
202
203     // Remove one class.
204     $attribute->removeClass('example-class');
205     $this->assertNotContains('example-class', $attribute['class']->value());
206
207     // Remove multiple classes.
208     $attribute->removeClass('xx', 'yy');
209     $this->assertNotContains(['xx', 'yy'], $attribute['class']->value());
210
211     // Remove an array of classes.
212     $attribute->removeClass(['red', 'green', 'blue']);
213     $this->assertNotContains(['red', 'green', 'blue'], $attribute['class']->value());
214
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());
220
221     $attribute->removeClass('aa');
222     $this->assertEmpty((string) $attribute);
223   }
224
225   /**
226    * Tests checking for class names with the Attribute method.
227    * @covers ::hasClass
228    */
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'));
233
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'));
238   }
239
240   /**
241    * Tests removing class attributes with the Attribute helper methods.
242    * @covers ::removeClass
243    * @covers ::addClass
244    */
245   public function testChainAddRemoveClasses() {
246     $attribute = new Attribute(
247       ['class' => ['example-class', 'red', 'green', 'blue']]
248     );
249
250     $attribute
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');
256   }
257
258   /**
259    * Tests the twig calls to the Attribute.
260    * @dataProvider providerTestAttributeClassHelpers
261    *
262    * @covers ::removeClass
263    * @covers ::addClass
264    *
265    * @group legacy
266    */
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);
273   }
274
275   /**
276    * Provides tests data for testEscaping
277    *
278    * @return array
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.
281    */
282   public function providerTestAttributeClassHelpers() {
283     return [
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'],
288       [
289         "{{ attributes.addClass('nanga-parbat').class }}",
290         'dhaulagiri manaslu nanga-parbat',
291         ['class' => ['dhaulagiri', 'manaslu']],
292       ],
293       [
294         "{{ attributes.removeClass('annapurna').class }}",
295         'gasherbrum-i',
296         ['class' => ['annapurna', 'gasherbrum-i']],
297       ],
298       [
299         "{{ attributes.removeClass(['broad peak']).class }}",
300         'gasherbrum-ii',
301         ['class' => ['broad peak', 'gasherbrum-ii']],
302       ],
303       [
304         "{{ attributes.removeClass('gyachung-kang', 'shishapangma').class }}",
305         '',
306         ['class' => ['shishapangma', 'gyachung-kang']],
307       ],
308       [
309         "{{ attributes.removeClass('nuptse').addClass('annapurna-ii').class }}",
310         'himalchuli annapurna-ii',
311         ['class' => ['himalchuli', 'nuptse']],
312       ],
313       // Test for the removal of an empty class name.
314       ["{{ attributes.addClass('rakaposhi', '').class }}", 'rakaposhi'],
315     ];
316   }
317
318   /**
319    * Tests iterating on the values of the attribute.
320    */
321   public function testIterate() {
322     $attribute = new Attribute(['class' => ['example-class'], 'id' => 'example-id']);
323
324     $counter = 0;
325     foreach ($attribute as $key => $value) {
326       if ($counter == 0) {
327         $this->assertEquals('class', $key);
328         $this->assertEquals(new AttributeArray('class', ['example-class']), $value);
329       }
330       if ($counter == 1) {
331         $this->assertEquals('id', $key);
332         $this->assertEquals(new AttributeString('id', 'example-id'), $value);
333       }
334       $counter++;
335     }
336   }
337
338   /**
339    * Tests printing of an attribute.
340    */
341   public function testPrint() {
342     $attribute = new Attribute(['class' => ['example-class'], 'id' => 'example-id', 'enabled' => TRUE]);
343
344     $content = $this->randomMachineName();
345     $html = '<div' . (string) $attribute . '>' . $content . '</div>';
346     $this->assertClass('example-class', $html);
347     $this->assertNoClass('example-class2', $html);
348
349     $this->assertID('example-id', $html);
350     $this->assertNoID('example-id2', $html);
351
352     $this->assertTrue(strpos($html, 'enabled') !== FALSE);
353   }
354
355   /**
356    * @covers ::createAttributeValue
357    * @dataProvider providerTestAttributeValues
358    */
359   public function testAttributeValues(array $attributes, $expected) {
360     $this->assertEquals($expected, (new Attribute($attributes))->__toString());
361   }
362
363   public function providerTestAttributeValues() {
364     $data = [];
365
366     $string = '"> <script>alert(123)</script>"';
367     $data['safe-object-xss1'] = [['title' => Markup::create($string)], ' title="&quot;&gt; alert(123)&quot;"'];
368     $data['non-safe-object-xss1'] = [['title' => $string], ' title="' . Html::escape($string) . '"'];
369     $string = '&quot;><script>alert(123)</script>';
370     $data['safe-object-xss2'] = [['title' => Markup::create($string)], ' title="&quot;&gt;alert(123)"'];
371     $data['non-safe-object-xss2'] = [['title' => $string], ' title="' . Html::escape($string) . '"'];
372
373     return $data;
374   }
375
376   /**
377    * Checks that the given CSS class is present in the given HTML snippet.
378    *
379    * @param string $class
380    *   The CSS class to check.
381    * @param string $html
382    *   The HTML snippet to check.
383    */
384   protected function assertClass($class, $html) {
385     $xpath = "//*[@class='$class']";
386     self::assertTrue((bool) $this->getXPathResultCount($xpath, $html));
387   }
388
389   /**
390    * Checks that the given CSS class is not present in the given HTML snippet.
391    *
392    * @param string $class
393    *   The CSS class to check.
394    * @param string $html
395    *   The HTML snippet to check.
396    */
397   protected function assertNoClass($class, $html) {
398     $xpath = "//*[@class='$class']";
399     self::assertFalse((bool) $this->getXPathResultCount($xpath, $html));
400   }
401
402   /**
403    * Checks that the given CSS ID is present in the given HTML snippet.
404    *
405    * @param string $id
406    *   The CSS ID to check.
407    * @param string $html
408    *   The HTML snippet to check.
409    */
410   protected function assertID($id, $html) {
411     $xpath = "//*[@id='$id']";
412     self::assertTrue((bool) $this->getXPathResultCount($xpath, $html));
413   }
414
415   /**
416    * Checks that the given CSS ID is not present in the given HTML snippet.
417    *
418    * @param string $id
419    *   The CSS ID to check.
420    * @param string $html
421    *   The HTML snippet to check.
422    */
423   protected function assertNoID($id, $html) {
424     $xpath = "//*[@id='$id']";
425     self::assertFalse((bool) $this->getXPathResultCount($xpath, $html));
426   }
427
428   /**
429    * Counts the occurrences of the given XPath query in a given HTML snippet.
430    *
431    * @param string $query
432    *   The XPath query to execute.
433    * @param string $html
434    *   The HTML snippet to check.
435    *
436    * @return int
437    *   The number of results that are found.
438    */
439   protected function getXPathResultCount($query, $html) {
440     $document = new \DOMDocument();
441     $document->loadHTML($html);
442     $xpath = new \DOMXPath($document);
443
444     return $xpath->query($query)->length;
445   }
446
447   /**
448    * Tests the storage method.
449    */
450   public function testStorage() {
451     $attribute = new Attribute(['class' => ['example-class']]);
452
453     $this->assertEquals(['class' => new AttributeArray('class', ['example-class'])], $attribute->storage());
454   }
455
456 }