overrideConfigValue( MainConfigNames::UsePigLatinVariant, false ); $langFactory = $this->getServiceContainer()->getLanguageFactory(); $contLangObj = $langFactory->getLanguage( 'en' ); // Hardcode namespaces during test runs, // so that html output based on existing namespaces // can be properly evaluated. $contLangObj->setNamespaces( [ -2 => 'Media', -1 => 'Special', 0 => '', 1 => 'Talk', 2 => 'User', 3 => 'User_talk', 4 => 'MyWiki', 5 => 'MyWiki_Talk', 6 => 'File', 7 => 'File_talk', 8 => 'MediaWiki', 9 => 'MediaWiki_talk', 10 => 'Template', 11 => 'Template_talk', 14 => 'Category', 15 => 'Category_talk', 100 => 'Custom', 101 => 'Custom_talk', ] ); $this->setContentLang( $contLangObj ); $userLangObj = $langFactory->getLanguage( 'es' ); $userLangObj->setNamespaces( [ -2 => "Medio", -1 => "Especial", 0 => "", 1 => "Discusión", 2 => "Usuario", 3 => "Usuario discusión", 4 => "Wiki", 5 => "Wiki discusión", 6 => "Archivo", 7 => "Archivo discusión", 8 => "MediaWiki", 9 => "MediaWiki discusión", 10 => "Plantilla", 11 => "Plantilla discusión", 12 => "Ayuda", 13 => "Ayuda discusión", 14 => "Categoría", 15 => "Categoría discusión", 100 => "Personalizado", 101 => "Personalizado discusión", ] ); $this->setUserLang( $userLangObj ); } public function testOpenElement() { $this->expectPHPError( E_USER_NOTICE, static function () { Html::openElement( 'span id="x"' ); }, 'given element name with space' ); } public function testElementBasics() { $this->assertEquals( '', Html::element( 'img', null, '' ), 'Self-closing tag for short-tag elements' ); $this->assertEquals( '', Html::element( 'element', null, null ), 'Close tag for empty element (null, null)' ); $this->assertEquals( '', Html::element( 'element', [], '' ), 'Close tag for empty element (array, string)' ); } public function dataXmlMimeType() { return [ // ( $mimetype, $isXmlMimeType ) # HTML is not an XML MimeType [ 'text/html', false ], # XML is an XML MimeType [ 'text/xml', true ], [ 'application/xml', true ], # XHTML is an XML MimeType [ 'application/xhtml+xml', true ], # Make sure other +xml MimeTypes are supported # SVG is another random MimeType even though we don't use it [ 'image/svg+xml', true ], # Complete random other MimeTypes are not XML [ 'text/plain', false ], ]; } /** * @dataProvider dataXmlMimeType */ public function testXmlMimeType( $mimetype, $isXmlMimeType ) { $this->assertEquals( $isXmlMimeType, Html::isXmlMimeType( $mimetype ) ); } public static function provideExpandAttributes() { // $expect, $attributes yield 'keep keys with an empty string' => [ ' foo=""', [ 'foo' => '' ] ]; yield 'False bool attribs have no output' => [ '', [ 'selected' => false ] ]; yield 'Null bool attribs have no output' => [ '', [ 'selected' => null ] ]; yield 'True bool attribs have output' => [ ' selected=""', [ 'selected' => true ] ]; yield 'True bool attribs have output (passed as numerical array)' => [ ' selected=""', [ 'selected' ] ]; yield 'integer value is cast to string' => [ ' value="1"', [ 'value' => 1 ] ]; yield 'float value is cast to string' => [ ' value="1.1"', [ 'value' => 1.1 ] ]; yield 'object value is converted to string' => [ ' value="stringValue"', [ 'value' => new HtmlTestValue() ] ]; yield 'Empty string is always quoted' => [ ' empty_string=""', [ 'empty_string' => '' ] ]; yield 'Simple string value needs no quotes' => [ ' key="value"', [ 'key' => 'value' ] ]; yield 'Number 1 value needs no quotes' => [ ' one="1"', [ 'one' => 1 ] ]; yield 'Number 0 value needs no quotes' => [ ' zero="0"', [ 'zero' => 0 ] ]; } /** * @dataProvider provideExpandAttributes */ public function testExpandAttributes( string $expect, array $attribs ) { $this->assertEquals( $expect, Html::expandAttributes( $attribs ) ); } public static function provideExpandAttributesEmpty() { // $attributes yield 'skip keys with null value' => [ [ 'foo' => null ] ]; yield 'skip keys with false value' => [ [ 'foo' => false ] ]; } /** * @dataProvider provideExpandAttributesEmpty */ public function testExpandAttributesEmpty( array $attribs ) { $this->assertSame( '', Html::expandAttributes( $attribs ) ); } public static function provideExpandAttributesClass() { // $expect, $classes // string values yield 'Normalization should strip redundant spaces' => [ ' class="redundant spaces here"', ' redundant spaces here ' ]; yield 'Normalization should remove duplicates in string-lists' => [ ' class="foo bar"', 'foo bar foo bar bar' ]; // array values yield 'Value with an empty array' => [ ' class=""', [] ]; yield 'Array with null, empty string and spaces' => [ ' class=""', [ null, '', ' ', ' ' ] ]; yield 'Normalization should remove duplicates in the array' => [ ' class="foo bar"', [ 'foo', 'bar', 'foo', 'bar', 'bar' ] ]; yield 'Normalization should remove duplicates in string-lists in the array' => [ ' class="foo bar"', [ 'foo bar', 'bar foo', 'foo', 'bar bar' ] ]; // Feature added in r96188 - pass attributes values as a PHP array // only applies to class, rel, and accesskey yield 'Associative array' => [ ' class="booltrue one"', [ 'booltrue' => true, 'one' => 1, # Method use isset() internally, make sure we do discard # attributes values which have been assigned well known values 'emptystring' => '', 'boolfalse' => false, 'zero' => 0, 'null' => null, ] ]; // How do we handle duplicate keys in HTML attributes expansion? // We could pass a "class" the values: 'GREEN' and [ 'GREEN' => false ] // The latter will take precedence yield 'Duplicate keys' => [ ' class=""', [ 'GREEN', 'GREEN' => false, 'GREEN', ] ]; } /** * Html::expandAttributes has special features for HTML * attributes that use space separated lists and also * allows arrays to be used as values. * * @dataProvider provideExpandAttributesClass */ public function testExpandAttributesClass( string $expect, $classes ) { $this->assertEquals( $expect, Html::expandAttributes( [ 'class' => $classes ] ) ); } public function testExpandAttributes_ArrayOnNonListValueAttribute_ThrowsException() { // Real-life test case found in the Popups extension (see Gerrit cf0fd64), // when used with an outdated BetaFeatures extension (see Gerrit deda1e7) $this->expectException( UnexpectedValueException::class ); Html::expandAttributes( [ 'src' => [ 'ltr' => 'ltr.svg', 'rtl' => 'rtl.svg' ] ] ); } public function testNamespaceSelector() { $this->assertEquals( '', Html::namespaceSelector(), 'Basic namespace selector without custom options' ); $this->assertEquals( '' . "\u{00A0}" . '', Html::namespaceSelector( [ 'selected' => '2', 'all' => 'all', 'label' => 'Select a namespace:' ], [ 'name' => 'wpNamespace', 'id' => 'mw-test-namespace' ] ), 'Basic namespace selector with custom values' ); $this->assertEquals( '' . "\u{00A0}" . '', Html::namespaceSelector( [ 'label' => 'Select a namespace:' ] ), 'Basic namespace selector with a custom label but no id attribtue for the ' . "\n" . '' . "\n" . '' . "\n" . '' . "\n" . '' . "\n" . '' . "\n" . '' . "\n" . '' . "\n" . '' . "\n" . '' . "\n" . '' . "\n" . '' . "\n" . '' . "\n" . '' . "\n" . '' . "\n" . '' . "\n" . '' . "\n" . '' . "\n" . '' . "\n" . '', Html::namespaceSelector( [ 'in-user-lang' => true ] ), 'Basic namespace selector in user language' ); } public function testCanFilterOutNamespaces() { $this->assertEquals( '', Html::namespaceSelector( [ 'exclude' => [ 0, 1, 3, 100, 101 ] ] ), 'Namespace selector namespace filtering.' ); $this->assertEquals( '', Html::namespaceSelector( [ 'exclude' => [ 0, 1, 3, 100, 101 ], 'all' => '' ] ), 'Namespace selector namespace filtering with empty custom "all" option.' ); } public function testCanDisableANamespaces() { $this->assertEquals( '', Html::namespaceSelector( [ 'disable' => [ 0, 1, 2, 3, 4 ] ] ), 'Namespace selector namespace disabling' ); } /** * @dataProvider provideHtml5InputTypes */ public function testHtmlElementAcceptsNewHtml5TypesInHtml5Mode( $HTML5InputType ) { $this->assertEquals( '', Html::element( 'input', [ 'type' => $HTML5InputType ] ), 'In HTML5, Html::element() should accept type="' . $HTML5InputType . '"' ); } public function testWarningBox() { $this->assertEquals( '
' . '' . '
warn
', Html::warningBox( 'warn' ) ); } public function testErrorBox() { $this->assertEquals( '
' . '' . '
err
', Html::errorBox( 'err' ) ); $this->assertEquals( '
' . '' . '
' . '

heading

err' . '
', Html::errorBox( 'err', 'heading', 'errorbox-custom-class' ) ); $this->assertEquals( '
' . '' . '
' . '

0

err' . '
', Html::errorBox( 'err', '0', '' ) ); } public function testSuccessBox() { $this->assertEquals( '
' . '' . '
great
', Html::successBox( 'great' ) ); $this->assertEquals( '
' . '' . '
' . '' . '
', Html::successBox( '' ) ); } /** * List of input element types values introduced by HTML5 * Full list at https://www.w3.org/TR/html-markup/input.html */ public static function provideHtml5InputTypes() { $types = [ 'datetime', 'datetime-local', 'date', 'month', 'time', 'week', 'number', 'range', 'email', 'url', 'search', 'tel', 'color', ]; foreach ( $types as $type ) { yield [ $type ]; } } /** * Test out Html::element drops or enforces default value * @dataProvider provideElementsWithAttributesHavingDefaultValues */ public function testDropDefaults( $expected, $element, $attribs, $message = '' ) { $this->assertEquals( $expected, Html::element( $element, $attribs ), $message ); } public static function provideElementsWithAttributesHavingDefaultValues() { # Use cases in a concise format: # , , [, ] # Will be mapped to Html::element() $cases = []; # ## Generic cases, match $attribDefault static array $cases[] = [ '', 'area', [ 'shape' => 'rect' ] ]; $cases[] = [ '', 'button', [ 'formaction' => 'GET' ] ]; $cases[] = [ '', 'button', [ 'formenctype' => 'application/x-www-form-urlencoded' ] ]; $cases[] = [ '', 'canvas', [ 'height' => '150' ] ]; $cases[] = [ '', 'canvas', [ 'width' => '300' ] ]; # Also check with numeric values $cases[] = [ '', 'canvas', [ 'height' => 150 ] ]; $cases[] = [ '', 'canvas', [ 'width' => 300 ] ]; $cases[] = [ '
', 'form', [ 'action' => 'GET' ] ]; $cases[] = [ '
', 'form', [ 'autocomplete' => 'on' ] ]; $cases[] = [ '
', 'form', [ 'enctype' => 'application/x-www-form-urlencoded' ] ]; $cases[] = [ '', 'input', [ 'formaction' => 'GET' ] ]; $cases[] = [ '', 'input', [ 'type' => 'text' ] ]; $cases[] = [ '', 'keygen', [ 'keytype' => 'rsa' ] ]; $cases[] = [ '', 'link', [ 'media' => 'all' ] ]; $cases[] = [ '', 'menu', [ 'type' => 'list' ] ]; $cases[] = [ '', 'script', [ 'type' => 'text/javascript' ] ]; $cases[] = [ '', 'style', [ 'media' => 'all' ] ]; $cases[] = [ '', 'style', [ 'type' => 'text/css' ] ]; $cases[] = [ '', 'textarea', [ 'wrap' => 'soft' ] ]; # ## SPECIFIC CASES # $cases[] = [ '', 'link', [ 'type' => 'text/css' ] ]; # specific handling $cases[] = [ '', 'input', [ 'type' => 'checkbox', 'value' => 'on' ], 'Default value "on" is stripped of checkboxes', ]; $cases[] = [ '', 'input', [ 'type' => 'radio', 'value' => 'on' ], 'Default value "on" is stripped of radio buttons', ]; $cases[] = [ '', 'input', [ 'type' => 'submit', 'value' => 'Submit' ], 'Default value "Submit" is kept on submit buttons (for possible l10n issues)', ]; $cases[] = [ '', 'input', [ 'type' => 'color', 'value' => '' ], ]; $cases[] = [ '', 'input', [ 'type' => 'range', 'value' => '' ], ]; # ', 'button', [ 'type' => 'submit' ], 'According to standard the default type is "submit". ' . 'Depending on compatibility mode IE might use "button", instead.', ]; # ', 'select', [ 'size' => '4', 'multiple' => true ], ]; # .. with numeric value $cases[] = [ '', 'select', [ 'size' => 4, 'multiple' => true ], ]; $cases[] = [ '', 'select', [ 'size' => '1', 'multiple' => false ], ]; # .. with numeric value $cases[] = [ '', 'select', [ 'size' => 1, 'multiple' => false ], ]; # Passing an array as value $cases[] = [ '', 'a', [ 'class' => [ 'css-class-one', 'css-class-two' ] ], "dropDefaults accepts values given as an array" ]; # FIXME: doDropDefault should remove defaults given in an array # Expected should be '' $cases[] = [ '', 'a', [ 'class' => [ '', '' ] ], "dropDefaults accepts values given as an array" ]; return $cases; } public function testWrapperInput() { $this->assertEquals( '', Html::input( 'testname', 'testval', 'radio' ), 'Input wrapper with type and value.' ); $this->assertEquals( '', Html::input( 'testname' ), 'Input wrapper with all default values.' ); } public function testWrapperCheck() { $this->assertEquals( '', Html::check( 'testname' ), 'Checkbox wrapper unchecked.' ); $this->assertEquals( '', Html::check( 'testname', true ), 'Checkbox wrapper checked.' ); $this->assertEquals( '', Html::check( 'testname', false, [ 'value' => 'testval' ] ), 'Checkbox wrapper with a value override.' ); } public function testWrapperRadio() { $this->assertEquals( '', Html::radio( 'testname' ), 'Radio wrapper unchecked.' ); $this->assertEquals( '', Html::radio( 'testname', true ), 'Radio wrapper checked.' ); $this->assertEquals( '', Html::radio( 'testname', false, [ 'value' => 'testval' ] ), 'Radio wrapper with a value override.' ); } public function testWrapperLabel() { $this->assertEquals( '', Html::label( 'testlabel', 'testid' ), 'Label wrapper' ); } public static function provideSrcSetImages() { return [ [ [], '', 'when there are no images, return empty string' ], [ [ '1x' => '1x.png', '1.5x' => '1_5x.png', '2x' => '2x.png' ], '1x.png 1x, 1_5x.png 1.5x, 2x.png 2x', 'pixel depth keys may include a trailing "x"' ], [ [ '1' => '1x.png', '1.5' => '1_5x.png', '2' => '2x.png' ], '1x.png 1x, 1_5x.png 1.5x, 2x.png 2x', 'pixel depth keys may omit a trailing "x"' ], [ [ '1' => 'small.png', '1.5' => 'large.png', '2' => 'large.png' ], 'small.png 1x, large.png 1.5x', 'omit larger duplicates' ], [ [ '1' => 'small.png', '2' => 'large.png', '1.5' => 'large.png' ], 'small.png 1x, large.png 1.5x', 'omit larger duplicates in irregular order' ], ]; } /** * @dataProvider provideSrcSetImages */ public function testSrcSet( $images, $expected, $message ) { $this->assertEquals( $expected, Html::srcSet( $images ), $message ); } public static function provideInlineScript() { return [ 'Empty' => [ '', '' ], 'Simple' => [ 'EXAMPLE.label("foo");', '' ], 'Ampersand' => [ 'EXAMPLE.is(a && b);', '' ], 'HTML' => [ 'EXAMPLE.label("");', '' ], 'Script closing string (lower)' => [ 'EXAMPLE.label("");', '', true, ], 'Script closing with non-standard attributes (mixed)' => [ 'EXAMPLE.label("");', '', true, ], 'HTML-comment-open and script-open' => [ // In HTML, , // there are levels of escaping modes, and the below sequence puts an // HTML parser in a state where would *not* close the script. // https://html.spec.whatwg.org/multipage/parsing.html#script-data-double-escape-end-state 'var a = "