Merge "resourceloader: Throw exception when config serialization fails"
[lhc/web/wiklou.git] / tests / phpunit / includes / HtmlTest.php
1 <?php
2
3 class HtmlTest extends MediaWikiTestCase {
4 private $restoreWarnings;
5
6 protected function setUp() {
7 parent::setUp();
8
9 $this->setMwGlobals( [
10 'wgUseMediaWikiUIEverywhere' => false,
11 ] );
12
13 $langObj = Language::factory( 'en' );
14
15 // Hardcode namespaces during test runs,
16 // so that html output based on existing namespaces
17 // can be properly evaluated.
18 $langObj->setNamespaces( [
19 -2 => 'Media',
20 -1 => 'Special',
21 0 => '',
22 1 => 'Talk',
23 2 => 'User',
24 3 => 'User_talk',
25 4 => 'MyWiki',
26 5 => 'MyWiki_Talk',
27 6 => 'File',
28 7 => 'File_talk',
29 8 => 'MediaWiki',
30 9 => 'MediaWiki_talk',
31 10 => 'Template',
32 11 => 'Template_talk',
33 14 => 'Category',
34 15 => 'Category_talk',
35 100 => 'Custom',
36 101 => 'Custom_talk',
37 ] );
38 $this->setUserLang( $langObj );
39 $this->setContentLang( $langObj );
40 $this->restoreWarnings = false;
41 }
42
43 protected function tearDown() {
44 Language::factory( 'en' )->resetNamespaces();
45
46 if ( $this->restoreWarnings ) {
47 $this->restoreWarnings = false;
48 Wikimedia\restoreWarnings();
49 }
50
51 parent::tearDown();
52 }
53
54 /**
55 * @covers Html::element
56 * @covers Html::rawElement
57 * @covers Html::openElement
58 * @covers Html::closeElement
59 */
60 public function testElementBasics() {
61 $this->assertEquals(
62 '<img/>',
63 Html::element( 'img', null, '' ),
64 'Self-closing tag for short-tag elements'
65 );
66
67 $this->assertEquals(
68 '<element></element>',
69 Html::element( 'element', null, null ),
70 'Close tag for empty element (null, null)'
71 );
72
73 $this->assertEquals(
74 '<element></element>',
75 Html::element( 'element', [], '' ),
76 'Close tag for empty element (array, string)'
77 );
78 }
79
80 public function dataXmlMimeType() {
81 return [
82 // ( $mimetype, $isXmlMimeType )
83 # HTML is not an XML MimeType
84 [ 'text/html', false ],
85 # XML is an XML MimeType
86 [ 'text/xml', true ],
87 [ 'application/xml', true ],
88 # XHTML is an XML MimeType
89 [ 'application/xhtml+xml', true ],
90 # Make sure other +xml MimeTypes are supported
91 # SVG is another random MimeType even though we don't use it
92 [ 'image/svg+xml', true ],
93 # Complete random other MimeTypes are not XML
94 [ 'text/plain', false ],
95 ];
96 }
97
98 /**
99 * @dataProvider dataXmlMimeType
100 * @covers Html::isXmlMimeType
101 */
102 public function testXmlMimeType( $mimetype, $isXmlMimeType ) {
103 $this->assertEquals( $isXmlMimeType, Html::isXmlMimeType( $mimetype ) );
104 }
105
106 /**
107 * @covers Html::expandAttributes
108 */
109 public function testExpandAttributesSkipsNullAndFalse() {
110 # ## EMPTY ########
111 $this->assertEmpty(
112 Html::expandAttributes( [ 'foo' => null ] ),
113 'skip keys with null value'
114 );
115 $this->assertEmpty(
116 Html::expandAttributes( [ 'foo' => false ] ),
117 'skip keys with false value'
118 );
119 $this->assertEquals(
120 ' foo=""',
121 Html::expandAttributes( [ 'foo' => '' ] ),
122 'keep keys with an empty string'
123 );
124 }
125
126 /**
127 * @covers Html::expandAttributes
128 */
129 public function testExpandAttributesForBooleans() {
130 $this->assertEquals(
131 '',
132 Html::expandAttributes( [ 'selected' => false ] ),
133 'Boolean attributes do not generates output when value is false'
134 );
135 $this->assertEquals(
136 '',
137 Html::expandAttributes( [ 'selected' => null ] ),
138 'Boolean attributes do not generates output when value is null'
139 );
140
141 $this->assertEquals(
142 ' selected=""',
143 Html::expandAttributes( [ 'selected' => true ] ),
144 'Boolean attributes have no value when value is true'
145 );
146 $this->assertEquals(
147 ' selected=""',
148 Html::expandAttributes( [ 'selected' ] ),
149 'Boolean attributes have no value when value is true (passed as numerical array)'
150 );
151 }
152
153 /**
154 * @covers Html::expandAttributes
155 */
156 public function testExpandAttributesForNumbers() {
157 $this->assertEquals(
158 ' value="1"',
159 Html::expandAttributes( [ 'value' => 1 ] ),
160 'Integer value is cast to a string'
161 );
162 $this->assertEquals(
163 ' value="1.1"',
164 Html::expandAttributes( [ 'value' => 1.1 ] ),
165 'Float value is cast to a string'
166 );
167 }
168
169 /**
170 * @covers Html::expandAttributes
171 */
172 public function testExpandAttributesForObjects() {
173 $this->assertEquals(
174 ' value="stringValue"',
175 Html::expandAttributes( [ 'value' => new HtmlTestValue() ] ),
176 'Object value is converted to a string'
177 );
178 }
179
180 /**
181 * Test for Html::expandAttributes()
182 * Please note it output a string prefixed with a space!
183 * @covers Html::expandAttributes
184 */
185 public function testExpandAttributesVariousExpansions() {
186 # ## NOT EMPTY ####
187 $this->assertEquals(
188 ' empty_string=""',
189 Html::expandAttributes( [ 'empty_string' => '' ] ),
190 'Empty string is always quoted'
191 );
192 $this->assertEquals(
193 ' key="value"',
194 Html::expandAttributes( [ 'key' => 'value' ] ),
195 'Simple string value needs no quotes'
196 );
197 $this->assertEquals(
198 ' one="1"',
199 Html::expandAttributes( [ 'one' => 1 ] ),
200 'Number 1 value needs no quotes'
201 );
202 $this->assertEquals(
203 ' zero="0"',
204 Html::expandAttributes( [ 'zero' => 0 ] ),
205 'Number 0 value needs no quotes'
206 );
207 }
208
209 /**
210 * Html::expandAttributes has special features for HTML
211 * attributes that use space separated lists and also
212 * allows arrays to be used as values.
213 * @covers Html::expandAttributes
214 */
215 public function testExpandAttributesListValueAttributes() {
216 # ## STRING VALUES
217 $this->assertEquals(
218 ' class="redundant spaces here"',
219 Html::expandAttributes( [ 'class' => ' redundant spaces here ' ] ),
220 'Normalization should strip redundant spaces'
221 );
222 $this->assertEquals(
223 ' class="foo bar"',
224 Html::expandAttributes( [ 'class' => 'foo bar foo bar bar' ] ),
225 'Normalization should remove duplicates in string-lists'
226 );
227 # ## "EMPTY" ARRAY VALUES
228 $this->assertEquals(
229 ' class=""',
230 Html::expandAttributes( [ 'class' => [] ] ),
231 'Value with an empty array'
232 );
233 $this->assertEquals(
234 ' class=""',
235 Html::expandAttributes( [ 'class' => [ null, '', ' ', ' ' ] ] ),
236 'Array with null, empty string and spaces'
237 );
238 # ## NON-EMPTY ARRAY VALUES
239 $this->assertEquals(
240 ' class="foo bar"',
241 Html::expandAttributes( [ 'class' => [
242 'foo',
243 'bar',
244 'foo',
245 'bar',
246 'bar',
247 ] ] ),
248 'Normalization should remove duplicates in the array'
249 );
250 $this->assertEquals(
251 ' class="foo bar"',
252 Html::expandAttributes( [ 'class' => [
253 'foo bar',
254 'bar foo',
255 'foo',
256 'bar bar',
257 ] ] ),
258 'Normalization should remove duplicates in string-lists in the array'
259 );
260 }
261
262 /**
263 * Test feature added by r96188, let pass attributes values as
264 * a PHP array. Restricted to class,rel, accesskey.
265 * @covers Html::expandAttributes
266 */
267 public function testExpandAttributesSpaceSeparatedAttributesWithBoolean() {
268 $this->assertEquals(
269 ' class="booltrue one"',
270 Html::expandAttributes( [ 'class' => [
271 'booltrue' => true,
272 'one' => 1,
273
274 # Method use isset() internally, make sure we do discard
275 # attributes values which have been assigned well known values
276 'emptystring' => '',
277 'boolfalse' => false,
278 'zero' => 0,
279 'null' => null,
280 ] ] )
281 );
282 }
283
284 /**
285 * How do we handle duplicate keys in HTML attributes expansion?
286 * We could pass a "class" the values: 'GREEN' and array( 'GREEN' => false )
287 * The latter will take precedence.
288 *
289 * Feature added by r96188
290 * @covers Html::expandAttributes
291 */
292 public function testValueIsAuthoritativeInSpaceSeparatedAttributesArrays() {
293 $this->assertEquals(
294 ' class=""',
295 Html::expandAttributes( [ 'class' => [
296 'GREEN',
297 'GREEN' => false,
298 'GREEN',
299 ] ] )
300 );
301 }
302
303 /**
304 * @covers Html::expandAttributes
305 * @expectedException MWException
306 */
307 public function testExpandAttributes_ArrayOnNonListValueAttribute_ThrowsException() {
308 // Real-life test case found in the Popups extension (see Gerrit cf0fd64),
309 // when used with an outdated BetaFeatures extension (see Gerrit deda1e7)
310 Html::expandAttributes( [
311 'src' => [
312 'ltr' => 'ltr.svg',
313 'rtl' => 'rtl.svg'
314 ]
315 ] );
316 }
317
318 /**
319 * @covers Html::namespaceSelector
320 * @covers Html::namespaceSelectorOptions
321 */
322 public function testNamespaceSelector() {
323 $this->assertEquals(
324 '<select id="namespace" name="namespace">' . "\n" .
325 '<option value="0">(Main)</option>' . "\n" .
326 '<option value="1">Talk</option>' . "\n" .
327 '<option value="2">User</option>' . "\n" .
328 '<option value="3">User talk</option>' . "\n" .
329 '<option value="4">MyWiki</option>' . "\n" .
330 '<option value="5">MyWiki Talk</option>' . "\n" .
331 '<option value="6">File</option>' . "\n" .
332 '<option value="7">File talk</option>' . "\n" .
333 '<option value="8">MediaWiki</option>' . "\n" .
334 '<option value="9">MediaWiki talk</option>' . "\n" .
335 '<option value="10">Template</option>' . "\n" .
336 '<option value="11">Template talk</option>' . "\n" .
337 '<option value="14">Category</option>' . "\n" .
338 '<option value="15">Category talk</option>' . "\n" .
339 '<option value="100">Custom</option>' . "\n" .
340 '<option value="101">Custom talk</option>' . "\n" .
341 '</select>',
342 Html::namespaceSelector(),
343 'Basic namespace selector without custom options'
344 );
345
346 $this->assertEquals(
347 '<label for="mw-test-namespace">Select a namespace:</label>' . "\u{00A0}" .
348 '<select id="mw-test-namespace" name="wpNamespace">' . "\n" .
349 '<option value="all">all</option>' . "\n" .
350 '<option value="0">(Main)</option>' . "\n" .
351 '<option value="1">Talk</option>' . "\n" .
352 '<option value="2" selected="">User</option>' . "\n" .
353 '<option value="3">User talk</option>' . "\n" .
354 '<option value="4">MyWiki</option>' . "\n" .
355 '<option value="5">MyWiki Talk</option>' . "\n" .
356 '<option value="6">File</option>' . "\n" .
357 '<option value="7">File talk</option>' . "\n" .
358 '<option value="8">MediaWiki</option>' . "\n" .
359 '<option value="9">MediaWiki talk</option>' . "\n" .
360 '<option value="10">Template</option>' . "\n" .
361 '<option value="11">Template talk</option>' . "\n" .
362 '<option value="14">Category</option>' . "\n" .
363 '<option value="15">Category talk</option>' . "\n" .
364 '<option value="100">Custom</option>' . "\n" .
365 '<option value="101">Custom talk</option>' . "\n" .
366 '</select>',
367 Html::namespaceSelector(
368 [ 'selected' => '2', 'all' => 'all', 'label' => 'Select a namespace:' ],
369 [ 'name' => 'wpNamespace', 'id' => 'mw-test-namespace' ]
370 ),
371 'Basic namespace selector with custom values'
372 );
373
374 $this->assertEquals(
375 '<label for="namespace">Select a namespace:</label>' . "\u{00A0}" .
376 '<select id="namespace" name="namespace">' . "\n" .
377 '<option value="0">(Main)</option>' . "\n" .
378 '<option value="1">Talk</option>' . "\n" .
379 '<option value="2">User</option>' . "\n" .
380 '<option value="3">User talk</option>' . "\n" .
381 '<option value="4">MyWiki</option>' . "\n" .
382 '<option value="5">MyWiki Talk</option>' . "\n" .
383 '<option value="6">File</option>' . "\n" .
384 '<option value="7">File talk</option>' . "\n" .
385 '<option value="8">MediaWiki</option>' . "\n" .
386 '<option value="9">MediaWiki talk</option>' . "\n" .
387 '<option value="10">Template</option>' . "\n" .
388 '<option value="11">Template talk</option>' . "\n" .
389 '<option value="14">Category</option>' . "\n" .
390 '<option value="15">Category talk</option>' . "\n" .
391 '<option value="100">Custom</option>' . "\n" .
392 '<option value="101">Custom talk</option>' . "\n" .
393 '</select>',
394 Html::namespaceSelector(
395 [ 'label' => 'Select a namespace:' ]
396 ),
397 'Basic namespace selector with a custom label but no id attribtue for the <select>'
398 );
399 }
400
401 /**
402 * @covers Html::namespaceSelector
403 */
404 public function testCanFilterOutNamespaces() {
405 $this->assertEquals(
406 '<select id="namespace" name="namespace">' . "\n" .
407 '<option value="2">User</option>' . "\n" .
408 '<option value="4">MyWiki</option>' . "\n" .
409 '<option value="5">MyWiki Talk</option>' . "\n" .
410 '<option value="6">File</option>' . "\n" .
411 '<option value="7">File talk</option>' . "\n" .
412 '<option value="8">MediaWiki</option>' . "\n" .
413 '<option value="9">MediaWiki talk</option>' . "\n" .
414 '<option value="10">Template</option>' . "\n" .
415 '<option value="11">Template talk</option>' . "\n" .
416 '<option value="14">Category</option>' . "\n" .
417 '<option value="15">Category talk</option>' . "\n" .
418 '</select>',
419 Html::namespaceSelector(
420 [ 'exclude' => [ 0, 1, 3, 100, 101 ] ]
421 ),
422 'Namespace selector namespace filtering.'
423 );
424 }
425
426 /**
427 * @covers Html::namespaceSelector
428 */
429 public function testCanDisableANamespaces() {
430 $this->assertEquals(
431 '<select id="namespace" name="namespace">' . "\n" .
432 '<option disabled="" value="0">(Main)</option>' . "\n" .
433 '<option disabled="" value="1">Talk</option>' . "\n" .
434 '<option disabled="" value="2">User</option>' . "\n" .
435 '<option disabled="" value="3">User talk</option>' . "\n" .
436 '<option disabled="" value="4">MyWiki</option>' . "\n" .
437 '<option value="5">MyWiki Talk</option>' . "\n" .
438 '<option value="6">File</option>' . "\n" .
439 '<option value="7">File talk</option>' . "\n" .
440 '<option value="8">MediaWiki</option>' . "\n" .
441 '<option value="9">MediaWiki talk</option>' . "\n" .
442 '<option value="10">Template</option>' . "\n" .
443 '<option value="11">Template talk</option>' . "\n" .
444 '<option value="14">Category</option>' . "\n" .
445 '<option value="15">Category talk</option>' . "\n" .
446 '<option value="100">Custom</option>' . "\n" .
447 '<option value="101">Custom talk</option>' . "\n" .
448 '</select>',
449 Html::namespaceSelector( [
450 'disable' => [ 0, 1, 2, 3, 4 ]
451 ] ),
452 'Namespace selector namespace disabling'
453 );
454 }
455
456 /**
457 * @dataProvider provideHtml5InputTypes
458 * @covers Html::element
459 */
460 public function testHtmlElementAcceptsNewHtml5TypesInHtml5Mode( $HTML5InputType ) {
461 $this->assertEquals(
462 '<input type="' . $HTML5InputType . '"/>',
463 Html::element( 'input', [ 'type' => $HTML5InputType ] ),
464 'In HTML5, Html::element() should accept type="' . $HTML5InputType . '"'
465 );
466 }
467
468 /**
469 * @covers Html::warningBox
470 * @covers Html::messageBox
471 */
472 public function testWarningBox() {
473 $this->assertEquals(
474 Html::warningBox( 'warn' ),
475 '<div class="warningbox">warn</div>'
476 );
477 }
478
479 /**
480 * @covers Html::errorBox
481 * @covers Html::messageBox
482 */
483 public function testErrorBox() {
484 $this->assertEquals(
485 Html::errorBox( 'err' ),
486 '<div class="errorbox">err</div>'
487 );
488 $this->assertEquals(
489 Html::errorBox( 'err', 'heading' ),
490 '<div class="errorbox"><h2>heading</h2>err</div>'
491 );
492 $this->assertEquals(
493 Html::errorBox( 'err', '0' ),
494 '<div class="errorbox"><h2>0</h2>err</div>'
495 );
496 }
497
498 /**
499 * @covers Html::successBox
500 * @covers Html::messageBox
501 */
502 public function testSuccessBox() {
503 $this->assertEquals(
504 Html::successBox( 'great' ),
505 '<div class="successbox">great</div>'
506 );
507 $this->assertEquals(
508 Html::successBox( '<script>beware no escaping!</script>' ),
509 '<div class="successbox"><script>beware no escaping!</script></div>'
510 );
511 }
512
513 /**
514 * List of input element types values introduced by HTML5
515 * Full list at https://www.w3.org/TR/html-markup/input.html
516 */
517 public static function provideHtml5InputTypes() {
518 $types = [
519 'datetime',
520 'datetime-local',
521 'date',
522 'month',
523 'time',
524 'week',
525 'number',
526 'range',
527 'email',
528 'url',
529 'search',
530 'tel',
531 'color',
532 ];
533 $cases = [];
534 foreach ( $types as $type ) {
535 $cases[] = [ $type ];
536 }
537
538 return $cases;
539 }
540
541 /**
542 * Test out Html::element drops or enforces default value
543 * @covers Html::dropDefaults
544 * @dataProvider provideElementsWithAttributesHavingDefaultValues
545 */
546 public function testDropDefaults( $expected, $element, $attribs, $message = '' ) {
547 $this->assertEquals( $expected, Html::element( $element, $attribs ), $message );
548 }
549
550 public static function provideElementsWithAttributesHavingDefaultValues() {
551 # Use cases in a concise format:
552 # <expected>, <element name>, <array of attributes> [, <message>]
553 # Will be mapped to Html::element()
554 $cases = [];
555
556 # ## Generic cases, match $attribDefault static array
557 $cases[] = [ '<area/>',
558 'area', [ 'shape' => 'rect' ]
559 ];
560
561 $cases[] = [ '<button type="submit"></button>',
562 'button', [ 'formaction' => 'GET' ]
563 ];
564 $cases[] = [ '<button type="submit"></button>',
565 'button', [ 'formenctype' => 'application/x-www-form-urlencoded' ]
566 ];
567
568 $cases[] = [ '<canvas></canvas>',
569 'canvas', [ 'height' => '150' ]
570 ];
571 $cases[] = [ '<canvas></canvas>',
572 'canvas', [ 'width' => '300' ]
573 ];
574 # Also check with numeric values
575 $cases[] = [ '<canvas></canvas>',
576 'canvas', [ 'height' => 150 ]
577 ];
578 $cases[] = [ '<canvas></canvas>',
579 'canvas', [ 'width' => 300 ]
580 ];
581
582 $cases[] = [ '<form></form>',
583 'form', [ 'action' => 'GET' ]
584 ];
585 $cases[] = [ '<form></form>',
586 'form', [ 'autocomplete' => 'on' ]
587 ];
588 $cases[] = [ '<form></form>',
589 'form', [ 'enctype' => 'application/x-www-form-urlencoded' ]
590 ];
591
592 $cases[] = [ '<input/>',
593 'input', [ 'formaction' => 'GET' ]
594 ];
595 $cases[] = [ '<input/>',
596 'input', [ 'type' => 'text' ]
597 ];
598
599 $cases[] = [ '<keygen/>',
600 'keygen', [ 'keytype' => 'rsa' ]
601 ];
602
603 $cases[] = [ '<link/>',
604 'link', [ 'media' => 'all' ]
605 ];
606
607 $cases[] = [ '<menu></menu>',
608 'menu', [ 'type' => 'list' ]
609 ];
610
611 $cases[] = [ '<script></script>',
612 'script', [ 'type' => 'text/javascript' ]
613 ];
614
615 $cases[] = [ '<style></style>',
616 'style', [ 'media' => 'all' ]
617 ];
618 $cases[] = [ '<style></style>',
619 'style', [ 'type' => 'text/css' ]
620 ];
621
622 $cases[] = [ '<textarea></textarea>',
623 'textarea', [ 'wrap' => 'soft' ]
624 ];
625
626 # ## SPECIFIC CASES
627
628 # <link type="text/css">
629 $cases[] = [ '<link/>',
630 'link', [ 'type' => 'text/css' ]
631 ];
632
633 # <input> specific handling
634 $cases[] = [ '<input type="checkbox"/>',
635 'input', [ 'type' => 'checkbox', 'value' => 'on' ],
636 'Default value "on" is stripped of checkboxes',
637 ];
638 $cases[] = [ '<input type="radio"/>',
639 'input', [ 'type' => 'radio', 'value' => 'on' ],
640 'Default value "on" is stripped of radio buttons',
641 ];
642 $cases[] = [ '<input type="submit" value="Submit"/>',
643 'input', [ 'type' => 'submit', 'value' => 'Submit' ],
644 'Default value "Submit" is kept on submit buttons (for possible l10n issues)',
645 ];
646 $cases[] = [ '<input type="color"/>',
647 'input', [ 'type' => 'color', 'value' => '' ],
648 ];
649 $cases[] = [ '<input type="range"/>',
650 'input', [ 'type' => 'range', 'value' => '' ],
651 ];
652
653 # <button> specific handling
654 # see remarks on https://msdn.microsoft.com/library/ms535211(v=vs.85).aspx
655 $cases[] = [ '<button type="submit"></button>',
656 'button', [ 'type' => 'submit' ],
657 'According to standard the default type is "submit". '
658 . 'Depending on compatibility mode IE might use "button", instead.',
659 ];
660
661 # <select> specific handling
662 $cases[] = [ '<select multiple=""></select>',
663 'select', [ 'size' => '4', 'multiple' => true ],
664 ];
665 # .. with numeric value
666 $cases[] = [ '<select multiple=""></select>',
667 'select', [ 'size' => 4, 'multiple' => true ],
668 ];
669 $cases[] = [ '<select></select>',
670 'select', [ 'size' => '1', 'multiple' => false ],
671 ];
672 # .. with numeric value
673 $cases[] = [ '<select></select>',
674 'select', [ 'size' => 1, 'multiple' => false ],
675 ];
676
677 # Passing an array as value
678 $cases[] = [ '<a class="css-class-one css-class-two"></a>',
679 'a', [ 'class' => [ 'css-class-one', 'css-class-two' ] ],
680 "dropDefaults accepts values given as an array"
681 ];
682
683 # FIXME: doDropDefault should remove defaults given in an array
684 # Expected should be '<a></a>'
685 $cases[] = [ '<a class=""></a>',
686 'a', [ 'class' => [ '', '' ] ],
687 "dropDefaults accepts values given as an array"
688 ];
689
690 # Craft the Html elements
691 $ret = [];
692 foreach ( $cases as $case ) {
693 $ret[] = [
694 $case[0],
695 $case[1], $case[2],
696 $case[3] ?? ''
697 ];
698 }
699
700 return $ret;
701 }
702
703 /**
704 * @covers Html::input
705 */
706 public function testWrapperInput() {
707 $this->assertEquals(
708 '<input type="radio" value="testval" name="testname"/>',
709 Html::input( 'testname', 'testval', 'radio' ),
710 'Input wrapper with type and value.'
711 );
712 $this->assertEquals(
713 '<input name="testname"/>',
714 Html::input( 'testname' ),
715 'Input wrapper with all default values.'
716 );
717 }
718
719 /**
720 * @covers Html::check
721 */
722 public function testWrapperCheck() {
723 $this->assertEquals(
724 '<input type="checkbox" value="1" name="testname"/>',
725 Html::check( 'testname' ),
726 'Checkbox wrapper unchecked.'
727 );
728 $this->assertEquals(
729 '<input checked="" type="checkbox" value="1" name="testname"/>',
730 Html::check( 'testname', true ),
731 'Checkbox wrapper checked.'
732 );
733 $this->assertEquals(
734 '<input type="checkbox" value="testval" name="testname"/>',
735 Html::check( 'testname', false, [ 'value' => 'testval' ] ),
736 'Checkbox wrapper with a value override.'
737 );
738 }
739
740 /**
741 * @covers Html::radio
742 */
743 public function testWrapperRadio() {
744 $this->assertEquals(
745 '<input type="radio" value="1" name="testname"/>',
746 Html::radio( 'testname' ),
747 'Radio wrapper unchecked.'
748 );
749 $this->assertEquals(
750 '<input checked="" type="radio" value="1" name="testname"/>',
751 Html::radio( 'testname', true ),
752 'Radio wrapper checked.'
753 );
754 $this->assertEquals(
755 '<input type="radio" value="testval" name="testname"/>',
756 Html::radio( 'testname', false, [ 'value' => 'testval' ] ),
757 'Radio wrapper with a value override.'
758 );
759 }
760
761 /**
762 * @covers Html::label
763 */
764 public function testWrapperLabel() {
765 $this->assertEquals(
766 '<label for="testid">testlabel</label>',
767 Html::label( 'testlabel', 'testid' ),
768 'Label wrapper'
769 );
770 }
771
772 public static function provideSrcSetImages() {
773 return [
774 [ [], '', 'when there are no images, return empty string' ],
775 [
776 [ '1x' => '1x.png', '1.5x' => '1_5x.png', '2x' => '2x.png' ],
777 '1x.png 1x, 1_5x.png 1.5x, 2x.png 2x',
778 'pixel depth keys may include a trailing "x"'
779 ],
780 [
781 [ '1' => '1x.png', '1.5' => '1_5x.png', '2' => '2x.png' ],
782 '1x.png 1x, 1_5x.png 1.5x, 2x.png 2x',
783 'pixel depth keys may omit a trailing "x"'
784 ],
785 [
786 [ '1' => 'small.png', '1.5' => 'large.png', '2' => 'large.png' ],
787 'small.png 1x, large.png 1.5x',
788 'omit larger duplicates'
789 ],
790 [
791 [ '1' => 'small.png', '2' => 'large.png', '1.5' => 'large.png' ],
792 'small.png 1x, large.png 1.5x',
793 'omit larger duplicates in irregular order'
794 ],
795 ];
796 }
797
798 /**
799 * @dataProvider provideSrcSetImages
800 * @covers Html::srcSet
801 */
802 public function testSrcSet( $images, $expected, $message ) {
803 $this->assertEquals( Html::srcSet( $images ), $expected, $message );
804 }
805
806 public static function provideInlineScript() {
807 return [
808 'Empty' => [
809 '',
810 '<script></script>'
811 ],
812 'Simple' => [
813 'EXAMPLE.label("foo");',
814 '<script>EXAMPLE.label("foo");</script>'
815 ],
816 'Ampersand' => [
817 'EXAMPLE.is(a && b);',
818 '<script>EXAMPLE.is(a && b);</script>'
819 ],
820 'HTML' => [
821 'EXAMPLE.label("<a>");',
822 '<script>EXAMPLE.label("<a>");</script>'
823 ],
824 'Script closing string (lower)' => [
825 'EXAMPLE.label("</script>");',
826 '<script>/* ERROR: Invalid script */</script>',
827 true,
828 ],
829 'Script closing with non-standard attributes (mixed)' => [
830 'EXAMPLE.label("</SCriPT and STyLE>");',
831 '<script>/* ERROR: Invalid script */</script>',
832 true,
833 ],
834 'HTML-comment-open and script-open' => [
835 // In HTML, <script> contents aren't just plain CDATA until </script>,
836 // there are levels of escaping modes, and the below sequence puts an
837 // HTML parser in a state where </script> would *not* close the script.
838 // https://html.spec.whatwg.org/multipage/parsing.html#script-data-double-escape-end-state
839 'var a = "<!--<script>";',
840 '<script>/* ERROR: Invalid script */</script>',
841 true,
842 ],
843 ];
844 }
845
846 /**
847 * @dataProvider provideInlineScript
848 * @covers Html::inlineScript
849 */
850 public function testInlineScript( $code, $expected, $error = false ) {
851 if ( $error ) {
852 Wikimedia\suppressWarnings();
853 $this->restoreWarnings = true;
854 }
855 $this->assertSame( Html::inlineScript( $code ), $expected );
856 }
857 }
858
859 class HtmlTestValue {
860 function __toString() {
861 return 'stringValue';
862 }
863 }