Version 1
[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   public function testTwigAddRemoveClasses($template, $expected, $seed_attributes = []) {
266     $loader = new \Twig_Loader_String();
267     $twig = new \Twig_Environment($loader);
268     $data = ['attributes' => new Attribute($seed_attributes)];
269     $result = $twig->render($template, $data);
270     $this->assertEquals($expected, $result);
271   }
272
273   /**
274    * Provides tests data for testEscaping
275    *
276    * @return array
277    *   An array of test data each containing of a twig template string,
278    *   a resulting string of classes and an optional array of attributes.
279    */
280   public function providerTestAttributeClassHelpers() {
281     return [
282       ["{{ attributes.class }}", ''],
283       ["{{ attributes.addClass('everest').class }}", 'everest'],
284       ["{{ attributes.addClass(['k2', 'kangchenjunga']).class }}", 'k2 kangchenjunga'],
285       ["{{ attributes.addClass('lhotse', 'makalu', 'cho-oyu').class }}", 'lhotse makalu cho-oyu'],
286       [
287         "{{ attributes.addClass('nanga-parbat').class }}",
288         'dhaulagiri manaslu nanga-parbat',
289         ['class' => ['dhaulagiri', 'manaslu']],
290       ],
291       [
292         "{{ attributes.removeClass('annapurna').class }}",
293         'gasherbrum-i',
294         ['class' => ['annapurna', 'gasherbrum-i']],
295       ],
296       [
297         "{{ attributes.removeClass(['broad peak']).class }}",
298         'gasherbrum-ii',
299         ['class' => ['broad peak', 'gasherbrum-ii']],
300       ],
301       [
302         "{{ attributes.removeClass('gyachung-kang', 'shishapangma').class }}",
303         '',
304         ['class' => ['shishapangma', 'gyachung-kang']],
305       ],
306       [
307         "{{ attributes.removeClass('nuptse').addClass('annapurna-ii').class }}",
308         'himalchuli annapurna-ii',
309         ['class' => ['himalchuli', 'nuptse']],
310       ],
311       // Test for the removal of an empty class name.
312       ["{{ attributes.addClass('rakaposhi', '').class }}", 'rakaposhi'],
313     ];
314   }
315
316   /**
317    * Tests iterating on the values of the attribute.
318    */
319   public function testIterate() {
320     $attribute = new Attribute(['class' => ['example-class'], 'id' => 'example-id']);
321
322     $counter = 0;
323     foreach ($attribute as $key => $value) {
324       if ($counter == 0) {
325         $this->assertEquals('class', $key);
326         $this->assertEquals(new AttributeArray('class', ['example-class']), $value);
327       }
328       if ($counter == 1) {
329         $this->assertEquals('id', $key);
330         $this->assertEquals(new AttributeString('id', 'example-id'), $value);
331       }
332       $counter++;
333     }
334   }
335
336   /**
337    * Tests printing of an attribute.
338    */
339   public function testPrint() {
340     $attribute = new Attribute(['class' => ['example-class'], 'id' => 'example-id', 'enabled' => TRUE]);
341
342     $content = $this->randomMachineName();
343     $html = '<div' . (string) $attribute . '>' . $content . '</div>';
344     $this->assertClass('example-class', $html);
345     $this->assertNoClass('example-class2', $html);
346
347     $this->assertID('example-id', $html);
348     $this->assertNoID('example-id2', $html);
349
350     $this->assertTrue(strpos($html, 'enabled') !== FALSE);
351   }
352
353   /**
354    * @covers ::createAttributeValue
355    * @dataProvider providerTestAttributeValues
356    */
357   public function testAttributeValues(array $attributes, $expected) {
358     $this->assertEquals($expected, (new Attribute($attributes))->__toString());
359   }
360
361   public function providerTestAttributeValues() {
362     $data = [];
363
364     $string = '"> <script>alert(123)</script>"';
365     $data['safe-object-xss1'] = [['title' => Markup::create($string)], ' title="&quot;&gt; alert(123)&quot;"'];
366     $data['non-safe-object-xss1'] = [['title' => $string], ' title="' . Html::escape($string) . '"'];
367     $string = '&quot;><script>alert(123)</script>';
368     $data['safe-object-xss2'] = [['title' => Markup::create($string)], ' title="&quot;&gt;alert(123)"'];
369     $data['non-safe-object-xss2'] = [['title' => $string], ' title="' . Html::escape($string) . '"'];
370
371     return $data;
372   }
373
374   /**
375    * Checks that the given CSS class is present in the given HTML snippet.
376    *
377    * @param string $class
378    *   The CSS class to check.
379    * @param string $html
380    *   The HTML snippet to check.
381    */
382   protected function assertClass($class, $html) {
383     $xpath = "//*[@class='$class']";
384     self::assertTrue((bool) $this->getXPathResultCount($xpath, $html));
385   }
386
387   /**
388    * Checks that the given CSS class is not present in the given HTML snippet.
389    *
390    * @param string $class
391    *   The CSS class to check.
392    * @param string $html
393    *   The HTML snippet to check.
394    */
395   protected function assertNoClass($class, $html) {
396     $xpath = "//*[@class='$class']";
397     self::assertFalse((bool) $this->getXPathResultCount($xpath, $html));
398   }
399
400   /**
401    * Checks that the given CSS ID is present in the given HTML snippet.
402    *
403    * @param string $id
404    *   The CSS ID to check.
405    * @param string $html
406    *   The HTML snippet to check.
407    */
408   protected function assertID($id, $html) {
409     $xpath = "//*[@id='$id']";
410     self::assertTrue((bool) $this->getXPathResultCount($xpath, $html));
411   }
412
413   /**
414    * Checks that the given CSS ID is not present in the given HTML snippet.
415    *
416    * @param string $id
417    *   The CSS ID to check.
418    * @param string $html
419    *   The HTML snippet to check.
420    */
421   protected function assertNoID($id, $html) {
422     $xpath = "//*[@id='$id']";
423     self::assertFalse((bool) $this->getXPathResultCount($xpath, $html));
424   }
425
426   /**
427    * Counts the occurrences of the given XPath query in a given HTML snippet.
428    *
429    * @param string $query
430    *   The XPath query to execute.
431    * @param string $html
432    *   The HTML snippet to check.
433    *
434    * @return int
435    *   The number of results that are found.
436    */
437   protected function getXPathResultCount($query, $html) {
438     $document = new \DOMDocument();
439     $document->loadHTML($html);
440     $xpath = new \DOMXPath($document);
441
442     return $xpath->query($query)->length;
443   }
444
445   /**
446    * Tests the storage method.
447    */
448   public function testStorage() {
449     $attribute = new Attribute(['class' => ['example-class']]);
450
451     $this->assertEquals(['class' => new AttributeArray('class', ['example-class'])], $attribute->storage());
452   }
453
454 }