diff --git a/includes/SkinVector.php b/includes/SkinVector.php index 54a155c..d3ee83a 100644 --- a/includes/SkinVector.php +++ b/includes/SkinVector.php @@ -71,6 +71,18 @@ class SkinVector extends SkinTemplate { return $modules; } + /** + * Set up the VectorTemplate + * + * @param string $classname + * @return VectorTemplate + */ + public function setupTemplate( $classname ) { + $template = new VectorTemplate( $this->getConfig() ); + $template->setTemplateParser( new TemplateParser( __DIR__ . '/templates' ) ); + return $template; + } + /** * Whether the logo should be preloaded with an HTTP link header or not * @since 1.29 diff --git a/includes/VectorTemplate.php b/includes/VectorTemplate.php index 4cd9fac..1d9e906 100644 --- a/includes/VectorTemplate.php +++ b/includes/VectorTemplate.php @@ -34,11 +34,24 @@ class VectorTemplate extends BaseTemplate { private $templateParser; /** - * @param Config|null $config + * @param TemplateParser $parser */ - public function __construct( Config $config = null ) { - parent::__construct( $config ); - $this->templateParser = new TemplateParser( __DIR__ . '/templates' ); + public function setTemplateParser( TemplateParser $parser ) { + $this->templateParser = $parser; + } + + /** + * The template parser might be undefined. This function will check if it set first + * + * @return TemplateParser + */ + protected function getTemplateParser() { + if ( $this->templateParser === null ) { + throw new \LogicException( + 'TemplateParser has to be set first via setTemplateParser method' + ); + } + return $this->templateParser; } /** @@ -121,12 +134,12 @@ class VectorTemplate extends BaseTemplate { 'html-debuglog' => $this->get( 'debughtml', '' ), // From BaseTemplate::getTrail (handles bottom JavaScript) 'html-printtail' => $this->getTrail() . '', - 'html-footer' => $this->templateParser->processTemplate( 'Footer', [ + 'html-footer' => $this->getTemplateParser()->processTemplate( 'Footer', [ 'html-userlangattributes' => $this->get( 'userlangattributes', '' ), 'html-hook-vector-before-footer' => $htmlHookVectorBeforeFooter, 'array-footer-rows' => $this->getTemplateFooterRows(), ] ), - 'html-navigation' => $this->templateParser->processTemplate( 'Navigation', [ + 'html-navigation' => $this->getTemplateParser()->processTemplate( 'Navigation', [ 'html-navigation-heading' => $this->getMsg( 'navigation-heading' ), 'html-personal-menu' => $this->renderNavigation( [ 'PERSONAL' ] ), 'html-navigation-left-tabs' => $this->renderNavigation( [ 'NAMESPACES', 'VARIANTS' ] ), @@ -142,7 +155,7 @@ class VectorTemplate extends BaseTemplate { ]; // Prepare and output the HTML response - echo $this->templateParser->processTemplate( 'index', $params ); + echo $this->getTemplateParser()->processTemplate( 'index', $params ); } /** @@ -285,7 +298,7 @@ class VectorTemplate extends BaseTemplate { $props['html-portal-content'] = $content; } - return $this->templateParser->processTemplate( 'Portal', $props ); + return $this->getTemplateParser()->processTemplate( 'Portal', $props ); } /** @@ -368,7 +381,7 @@ class VectorTemplate extends BaseTemplate { $props[ 'html-items' ] .= $this->makeListItem( $key, $item ); } - return $this->templateParser->processTemplate( 'VectorTabs', $props ); + return $this->getTemplateParser()->processTemplate( 'VectorTabs', $props ); } /** @@ -395,7 +408,7 @@ class VectorTemplate extends BaseTemplate { $props['html-items'] .= $this->makeListItem( $key, $item ); } - return $this->templateParser->processTemplate( 'VectorMenu', $props ); + return $this->getTemplateParser()->processTemplate( 'VectorMenu', $props ); } /** @@ -417,7 +430,7 @@ class VectorTemplate extends BaseTemplate { ] ); } - return $this->templateParser->processTemplate( 'VectorTabs', $props ); + return $this->getTemplateParser()->processTemplate( 'VectorTabs', $props ); } /** @@ -437,7 +450,7 @@ class VectorTemplate extends BaseTemplate { $props['html-items'] .= $this->makeListItem( $key, $item ); } - return $this->templateParser->processTemplate( 'VectorMenu', $props ); + return $this->getTemplateParser()->processTemplate( 'VectorMenu', $props ); } /** @@ -472,7 +485,7 @@ class VectorTemplate extends BaseTemplate { $props['html-personal-tools'] .= $this->makeListItem( $key, $item ); } - return $this->templateParser->processTemplate( 'PersonalMenu', $props ); + return $this->getTemplateParser()->processTemplate( 'PersonalMenu', $props ); } private function renderSearchComponent() { @@ -492,6 +505,6 @@ class VectorTemplate extends BaseTemplate { ), 'searchInputLabel' => $this->getMsg( 'search' ) ]; - return $this->templateParser->processTemplate( 'SearchBox', $props ); + return $this->getTemplateParser()->processTemplate( 'SearchBox', $props ); } } diff --git a/tests/phpunit/integration/VectorTemplateTest.php b/tests/phpunit/integration/VectorTemplateTest.php new file mode 100644 index 0000000..936918e --- /dev/null +++ b/tests/phpunit/integration/VectorTemplateTest.php @@ -0,0 +1,152 @@ +setTemplateParser( new \TemplateParser() ); + return $template; + } + + /** + * @param string $nodeString an HTML of the node we want to verify + * @param string $tag Tag of the element we want to check + * @param string $attribute Attribute of the element we want to check + * @param string $search Value of the attribute we want to verify + * @return bool + */ + private function expectNodeAttribute( $nodeString, $tag, $attribute, $search ) { + $node = new \DOMDocument(); + $node->loadHTML( $nodeString ); + $element = $node->getElementsByTagName( $tag )->item( 0 ); + if ( !$element ) { + return false; + } + + $values = explode( ' ', $element->getAttribute( $attribute ) ); + return in_array( $search, $values ); + } + + /** + * @covers ::makeListItem + */ + public function testMakeListItemRespectsCollapsibleOption() { + $template = $this->provideVectorTemplateObject(); + $listItemClass = 'my_test_class'; + $options = [ 'vector-collapsible' => true ]; + $item = [ 'class' => $listItemClass ]; + $nonCollapsible = $template->makeListItem( 'key', $item, [] ); + $collapsible = $template->makeListItem( 'key', [], $options ); + + $this->assertTrue( + $this->expectNodeAttribute( $collapsible, 'li', 'class', 'collapsible' ), + 'The collapsible element has to have `collapsible` class' + ); + $this->assertFalse( + $this->expectNodeAttribute( $nonCollapsible, 'li', 'class', 'collapsible' ), + 'The non-collapsible element should not have `collapsible` class' + ); + $this->assertTrue( + $this->expectNodeAttribute( $nonCollapsible, 'li', 'class', $listItemClass ), + 'The non-collapsible element should preserve item class' + ); + } + + /** + * @covers ::makeListItem + */ + public function testWatcAndUnwatchHasIconClass() { + $template = $this->provideVectorTemplateObject(); + $this->setMwGlobals( [ + 'wgVectorUseIconWatch' => true + ] ); + $listItemClass = 'my_test_class'; + $options = []; + $item = [ 'class' => $listItemClass ]; + + $watchListItem = $template->makeListItem( 'watch', $item, [] ); + $unwatchListItem = $template->makeListItem( 'unwatch', [], $options ); + $regularListItem = $template->makeListItem( 'whatever', $item, $options ); + + $this->assertTrue( + $this->expectNodeAttribute( $watchListItem, 'li', 'class', 'icon' ), + 'Watch list items require an "icon" class' + ); + $this->assertTrue( + $this->expectNodeAttribute( $unwatchListItem, 'li', 'class', 'icon' ), + 'Unwatch list items require an "icon" class' + ); + $this->assertFalse( + $this->expectNodeAttribute( $regularListItem, 'li', 'class', 'icon' ), + 'List item other than watch or unwatch should not have an "icon" class' + ); + $this->assertTrue( + $this->expectNodeAttribute( $watchListItem, 'li', 'class', $listItemClass ), + 'Watch list items require an item class' + ); + } + + /** + * @covers ::makeListItem + */ + public function testWatchAndUnwatchHasIconClassOnlyIfVectorUseIconWatchIsSet() { + $template = $this->provideVectorTemplateObject(); + $this->setMwGlobals( [ + 'wgVectorUseIconWatch' => false + ] ); + $listItemClass = 'my_test_class'; + $item = [ 'class' => $listItemClass ]; + + $watchListItem = $template->makeListItem( 'watch', $item, [] ); + + $this->assertFalse( + $this->expectNodeAttribute( $watchListItem, 'li', 'class', 'icon' ), + 'Watch list should not have an "icon" class when VectorUserIconWatch is disabled' + ); + $this->assertTrue( + $this->expectNodeAttribute( $watchListItem, 'li', 'class', $listItemClass ), + 'Watch list items require an item class' + ); + } + + /** + * @covers ::renderViewsComponent + */ + public function testRenderViewsComponent() { + $langAttrs = 'LANG_ATTRIBUTES'; + $templateParserMock = $this->createMock( \TemplateParser::class ); + $templateParserMock->expects( $this->once() ) + ->method( 'processTemplate' ) + ->with( 'VectorTabs', $this->callback( function ( $data ) use ( $langAttrs ){ + if ( !array_key_exists( 'empty-portlet', $data ) ) { + return false; + } + return $data['empty-portlet'] == 'emptyPortlet' && + $data['html-userlangattributes'] == $langAttrs; + } ) ); + + $vectorTemplate = new \VectorTemplate( \GlobalVarConfig::newInstance() ); + $vectorTemplate->setTemplateParser( $templateParserMock ); + $vectorTemplate->set( 'view_urls', [] ); + $vectorTemplate->set( 'skin', new \SkinVector() ); + $vectorTemplate->set( 'userlangattributes', $langAttrs ); + $openVectorTemplate = TestingAccessWrapper::newFromObject( $vectorTemplate ); + + $openVectorTemplate->renderViewsComponent(); + } + +}