diff --git a/core/lib/Drupal/Core/Render/Renderer.php b/core/lib/Drupal/Core/Render/Renderer.php index 81423cf..709f4ca 100644 --- a/core/lib/Drupal/Core/Render/Renderer.php +++ b/core/lib/Drupal/Core/Render/Renderer.php @@ -109,7 +109,7 @@ public function render(&$elements, $is_root_call = FALSE) { */ protected function doRender(&$elements, $is_root_call = FALSE) { if (!isset($elements['#access']) && isset($elements['#access_callback'])) { - if (is_string($elements['#access_callback']) && strpos($elements['#access_callback'], '::') === FALSE) { + if (is_string($elements['#access_callback']) && strpos($elements['#access_callback'], '::') !== FALSE) { $elements['#access_callback'] = $this->controllerResolver->getControllerFromDefinition($elements['#access_callback']); } $elements['#access'] = call_user_func($elements['#access_callback'], $elements); diff --git a/core/modules/system/src/Tests/Common/RenderTest.php b/core/modules/system/src/Tests/Common/RenderTest.php index 6848dc9..6a0f636 100644 --- a/core/modules/system/src/Tests/Common/RenderTest.php +++ b/core/modules/system/src/Tests/Common/RenderTest.php @@ -32,148 +32,7 @@ class RenderTest extends KernelTestBase { */ function testDrupalRenderBasics() { $types = array( - array( - 'name' => 'null', - 'value' => NULL, - 'expected' => '', - ), - array( - 'name' => 'no value', - 'expected' => '', - ), - array( - 'name' => 'empty string', - 'value' => '', - 'expected' => '', - ), - array( - 'name' => 'no access', - 'value' => array( - '#markup' => 'foo', - '#access' => FALSE, - ), - 'expected' => '', - ), - array( - 'name' => 'access denied via callback', - 'value' => array( - '#markup' => 'foo', - '#access_callback' => 'is_bool', - ), - 'expected' => '', - ), - array( - 'name' => 'access granted via callback', - 'value' => array( - '#markup' => 'foo', - '#access_callback' => 'is_array', - ), - 'expected' => 'foo', - ), - array( - 'name' => 'access FALSE is honored', - 'value' => array( - '#markup' => 'foo', - '#access' => FALSE, - '#access_callback' => 'is_array', - ), - 'expected' => '', - ), - array( - 'name' => 'previously printed', - 'value' => array( - '#markup' => 'foo', - '#printed' => TRUE, - ), - 'expected' => '', - ), - array( - 'name' => 'printed in prerender', - 'value' => array( - '#markup' => 'foo', - '#pre_render' => array('common_test_drupal_render_printing_pre_render'), - ), - 'expected' => '', - ), - - // Test that #theme and #theme_wrappers can co-exist on an element. - array( - 'name' => '#theme and #theme_wrappers basic', - 'value' => array( - '#theme' => 'common_test_foo', - '#foo' => 'foo', - '#bar' => 'bar', - '#theme_wrappers' => array('container'), - '#attributes' => array('class' => array('baz')), - ), - 'expected' => '
foobar
' . "\n", - ), - // Test that #theme_wrappers can disambiguate element attributes shared - // with rendering methods that build #children by using the alternate - // #theme_wrappers attribute override syntax. - array( - 'name' => '#theme and #theme_wrappers attribute disambiguation', - 'value' => array( - '#type' => 'link', - '#theme_wrappers' => array( - 'container' => array( - '#attributes' => array('class' => array('baz')), - ), - ), - '#attributes' => array('id' => 'foo'), - '#url' => Url::fromUri('http://drupal.org'), - '#title' => 'bar', - ), - 'expected' => '
bar
' . "\n", - ), - // Test that #theme_wrappers can disambiguate element attributes when the - // "base" attribute is not set for #theme. - array( - 'name' => '#theme_wrappers attribute disambiguation with undefined #theme attribute', - 'value' => array( - '#type' => 'link', - '#url' => Url::fromUri('http://drupal.org'), - '#title' => 'foo', - '#theme_wrappers' => array( - 'container' => array( - '#attributes' => array('class' => array('baz')), - ), - ), - ), - 'expected' => '
foo
' . "\n", - ), - // Two 'container' #theme_wrappers, one using the "base" attributes and - // one using an override. - array( - 'name' => 'Two #theme_wrappers container hooks with different attributes', - 'value' => array( - '#attributes' => array('class' => array('foo')), - '#theme_wrappers' => array( - 'container' => array( - '#attributes' => array('class' => array('bar')), - ), - 'container', - ), - ), - 'expected' => '
' . "\n" . '
' . "\n", - ), - // Array syntax theme hook suggestion in #theme_wrappers. - array( - 'name' => '#theme_wrappers implements an array style theme hook suggestion', - 'value' => array( - '#theme_wrappers' => array(array('container')), - '#attributes' => array('class' => array('foo')), - ), - 'expected' => '
' . "\n", - ), - // Test handling of #markup as a fallback for #theme hooks. - // Simple #markup with no theme. - array( - 'name' => 'basic #markup based renderable array', - 'value' => array('#markup' => 'foo'), - 'expected' => 'foo', - ), // Theme suggestion is not implemented, #markup should be rendered. array( 'name' => '#markup fallback for #theme suggestion not implemented', @@ -218,34 +77,6 @@ function testDrupalRenderBasics() { ), // Test handling of #children and child renderable elements. - // #theme is not set, #children is not set and the array has children. - array( - 'name' => '#theme is not set, #children is not set and array has children', - 'value' => array( - 'child' => array('#markup' => 'bar'), - ), - 'expected' => 'bar', - ), - // #theme is not set, #children is set but empty and the array has - // children. - array( - 'name' => '#theme is not set, #children is an empty string and array has children', - 'value' => array( - '#children' => '', - 'child' => array('#markup' => 'bar'), - ), - 'expected' => 'bar', - ), - // #theme is not set, #children is not empty and will be assumed to be the - // rendered child elements even though the #markup for 'child' differs. - array( - 'name' => '#theme is not set, #children is set and array has children', - 'value' => array( - '#children' => 'foo', - 'child' => array('#markup' => 'bar'), - ), - 'expected' => 'foo', - ), // #theme is implemented so the values of both #children and 'child' will // be ignored - it is the responsibility of the theme hook to render these // if appropriate. @@ -295,57 +126,6 @@ function testDrupalRenderBasics() { } /** - * Tests sorting by weight. - */ - function testDrupalRenderSorting() { - $first = $this->randomMachineName(); - $second = $this->randomMachineName(); - // Build an array with '#weight' set for each element. - $elements = array( - 'second' => array( - '#weight' => 10, - '#markup' => $second, - ), - 'first' => array( - '#weight' => 0, - '#markup' => $first, - ), - ); - $output = drupal_render($elements); - - // The lowest weight element should appear last in $output. - $this->assertTrue(strpos($output, $second) > strpos($output, $first), 'Elements were sorted correctly by weight.'); - - // Confirm that the $elements array has '#sorted' set to TRUE. - $this->assertTrue($elements['#sorted'], "'#sorted' => TRUE was added to the array"); - - // Pass $elements through \Drupal\Core\Render\Element::children() and - // ensure it remains sorted in the correct order. drupal_render() will - // return an empty string if used on the same array in the same request. - $children = Element::children($elements); - $this->assertTrue(array_shift($children) == 'first', 'Child found in the correct order.'); - $this->assertTrue(array_shift($children) == 'second', 'Child found in the correct order.'); - - - // The same array structure again, but with #sorted set to TRUE. - $elements = array( - 'second' => array( - '#weight' => 10, - '#markup' => $second, - ), - 'first' => array( - '#weight' => 0, - '#markup' => $first, - ), - '#sorted' => TRUE, - ); - $output = drupal_render($elements); - - // The elements should appear in output in the same order as the array. - $this->assertTrue(strpos($output, $second) < strpos($output, $first), 'Elements were not sorted.'); - } - - /** * Tests #attached functionality in children elements. */ function testDrupalRenderChildrenAttached() { diff --git a/core/tests/Drupal/Tests/Core/Render/RendererTest.php b/core/tests/Drupal/Tests/Core/Render/RendererTest.php index fb40f45..7f3ba29 100644 --- a/core/tests/Drupal/Tests/Core/Render/RendererTest.php +++ b/core/tests/Drupal/Tests/Core/Render/RendererTest.php @@ -10,6 +10,8 @@ use Drupal\Core\Render\Element; use Drupal\Core\Render\Renderer; +use Drupal\Core\Template\Attribute; +use Drupal\Core\Url; use Drupal\Tests\UnitTestCase; /** @@ -25,6 +27,30 @@ class RendererTest extends UnitTestCase { */ protected $renderer; + /** + * The mocked controller resolver. + * + * @var \Drupal\Core\Controller\ControllerResolverInterface|\PHPUnit_Framework_MockObject_MockObject + */ + protected $controllerResolver; + + /** + * The mocked theme manager. + * + * @var \Drupal\Core\Theme\ThemeManagerInterface|\PHPUnit_Framework_MockObject_MockObject + */ + protected $themeManager; + + /** + * The mocked element info. + * + * @var \Drupal\Core\Render\ElementInfoManagerInterface|\PHPUnit_Framework_MockObject_MockObject + */ + protected $elementInfo; + + /** + * {@inheritdoc} + */ protected function setUp() { parent::setUp(); @@ -34,6 +60,167 @@ protected function setUp() { $this->renderer = new Renderer($this->controllerResolver, $this->themeManager, $this->elementInfo); } + /** + * @dataProvider providerTestRenderBasic + */ + public function testRenderBasic($build, $expected, callable $setup_code = NULL) { + + if (isset($setup_code)) { + $setup_code = $setup_code->bindTo($this); + $setup_code(); + } + + $this->assertSame($expected, $this->renderer->render($build)); + } + + public function providerTestRenderBasic() { + $data = []; + + // Pass a NULL. +// $data[] = [NULL, '']; + // Pass an empty string. +// $data[] = ['', '']; + // Previously printed, see ::renderTwice for a more integration like test. +// $data[] = [[ +// '#markup' => 'foo', +// '#printed' => TRUE, +// ], '']; + // Printed in pre_render. +// $data[] = [[ +// '#markup' => 'foo', +// '#pre_render' => [[new TestCallables(), 'preRenderPrinted']] +// ], '']; + + // Test that #theme and #theme_wrappers can co-exist on an element. + // #theme and #theme_wrappers basic + $build = [ + '#theme' => 'common_test_foo', + '#foo' => 'foo', + '#bar' => 'bar', + '#theme_wrappers' => ['container'], + '#attributes' => ['class' => ['baz']], + ]; + $setup_code_type_link = function() { + $this->setupThemeContainer(); + $this->themeManager->expects($this->at(0)) + ->method('render') + ->with('common_test_foo', $this->anything()) + ->willReturnCallback(function($theme, $vars) { + return $vars['#foo'] . $vars['#bar']; + }); + }; + $data[] = [$build, '
foobar
' . "\n", $setup_code_type_link]; + + // Test that #theme_wrappers can disambiguate element attributes shared + // with rendering methods that build #children by using the alternate + // #theme_wrappers attribute override syntax. + // '#theme and #theme_wrappers attribute disambiguation'. + + $build = [ + '#type' => 'link', + '#theme_wrappers' => [ + 'container' => [ + '#attributes' => ['class' => ['baz']], + ], + ], + '#attributes' => ['id' => 'foo'], + '#url' => 'http://drupal.org', + '#title' => 'bar', + ]; + $setup_code_type_link = function() { + $this->setupThemeContainer(); + $this->themeManager->expects($this->at(0)) + ->method('render') + ->with('link', $this->anything()) + ->willReturnCallback(function($theme, $vars) { + $attributes = new Attribute(['href' => $vars['#url']] + (isset($vars['#attributes']) ? $vars['#attributes'] : [])); + return '' . $vars['#title'] . ''; + }); + $this->elementInfo->expects($this->atLeastOnce()) + ->method('getInfo') + ->with('link') + ->willReturn(['#theme' => 'link']); + }; + $data[] = [$build, '
bar
' . "\n", $setup_code_type_link]; + + // Test that #theme_wrappers can disambiguate element attributes when the + // "base" attribute is not set for #theme. + // #theme_wrappers attribute disambiguation with undefined #theme attribute. + $build = [ + '#type' => 'link', + '#url' => 'http://drupal.org', + '#title' => 'foo', + '#theme_wrappers' => [ + 'container' => [ + '#attributes' => ['class' => ['baz']], + ], + ], + ]; + $data[] = [$build, '
foo
' . "\n", $setup_code_type_link]; + + // Two 'container' #theme_wrappers, one using the "base" attributes and + // one using an override. + $build = [ + '#attributes' => ['class' => ['foo']], + '#theme_wrappers' => [ + 'container' => [ + '#attributes' => ['class' => ['bar']], + ], + 'container', + ], + ]; + $setup_code = function() { + $this->setupThemeContainer($this->any()); + }; + $data[] = [$build, '
' . "\n" . '
' . "\n", $setup_code]; + + // Array syntax theme hook suggestion in #theme_wrappers. + $build = [ + '#theme_wrappers' => [['container']], + '#attributes' => ['class' => ['foo']], + ]; + $setup_code = function() { + $this->setupThemeContainerMultiSuggestion($this->any()); + }; + $data[] = [$build, '
' . "\n", $setup_code]; + + // Test handling of #markup as a fallback for #theme hooks. + // Simple #markup with no theme. + + // Basic #markup based renderable array. +// $data[] = [ +// ['#markup' => 'foo'] +// , 'foo']; + + // Test handling of #children and child renderable elements. + // #theme is not set, #children is not set and the array has children. +// $data[] = [ +// [ +// 'child' => ['#markup' => 'bar'], +// ], 'bar']; + // #theme is not set, #children is set but empty and the array has + // children. +// $data[] = [ +// [ +// '#children' => '', +// 'child' => ['#markup' => 'bar'], +// ], 'bar' +// ]; + // #theme is not set, #children is not empty and will be assumed to be the + // rendered child elements even though the #markup for 'child' differs. +// $data[] = [ +// [ +// '#children' => 'foo', +// 'child' => ['#markup' => 'bar'], +// ], 'foo' +// ]; + + return $data; + } + + protected function setupThemeCommonTestFoo() { + + } /** * @covers ::render @@ -89,4 +276,133 @@ public function testRenderSortingWithSetHashSorted() { $this->assertTrue(strpos($output, $second) < strpos($output, $first), 'Elements were not sorted.'); } + /** + * @dataProvider providerBoolean + */ + public function testRenderWithPresetAccess($access) { + $build = [ + '#markup' => 'test', + '#access' => $access, + ]; + + $this->assertAccess($build, $access); + } + + /** + * @dataProvider providerBoolean + */ + public function testRenderWithAccessCallbackCallable($access) { + $build = [ + '#markup' => 'test', + '#access_callback' => function() use ($access) { + return $access; + } + ]; + + $this->assertAccess($build, $access); + } + + /** + * @dataProvider providerBoolean + * + * Ensure that the #access property wins over the callable. + */ + public function testRenderWithAccessPropertyAndCallback($access) { + $build = [ + '#markup' => 'test', + '#access' => $access, + '#access_callback' => function() { + return TRUE; + } + ]; + + $this->assertAccess($build, $access); + } + + /** + * @dataProvider providerBoolean + */ + public function testRenderWithAccessControllerResolved($access) { + $build = [ + '#markup' => 'test', + '#access_callback' => 'Drupal\Tests\Core\Render\TestAccessClass::' . ($access ? 'accessTrue' : 'accessFalse'), + ]; + + $this->controllerResolver->expects($this->atLeastOnce()) + ->method('getControllerFromDefinition') + ->willReturnMap([ + ['Drupal\Tests\Core\Render\TestAccessClass::accessTrue', [new TestAccessClass(), 'accessTrue']], + ['Drupal\Tests\Core\Render\TestAccessClass::accessFalse', [new TestAccessClass(), 'accessFalse']], + ]); + + $this->assertAccess($build, $access); + } + + public function testRenderTwice() { + $build = [ + '#markup' => 'test', + ]; + + $this->assertEquals('test', $this->renderer->render($build)); + // @todo This behaviour is odd ... + $this->assertEquals('', $this->renderer->render($build)); + } + + + + public function providerBoolean() { + return [ + [FALSE], + [TRUE] + ]; + } + + protected function assertAccess($build, $access) { + if ($access) { + $this->assertSame('test', $this->renderer->render($build)); + } + else { + $this->assertSame('', $this->renderer->render($build)); + } + } + + protected function setupThemeContainer($matcher = NULL) { + $this->themeManager->expects($matcher ?: $this->at(1)) + ->method('render') + ->with('container', $this->anything()) + ->willReturnCallback(function($theme, $vars) { + return '' . $vars['#children'] . "\n"; + }); + } + + protected function setupThemeContainerMultiSuggestion($matcher = NULL) { + $this->themeManager->expects($matcher ?: $this->at(1)) + ->method('render') + ->with(['container'], $this->anything()) + ->willReturnCallback(function($theme, $vars) { + return '' . $vars['#children'] . "\n"; + }); + } + +} + +class TestAccessClass { + + public function accessTrue() { + return TRUE; + } + + public function accessFalse() { + return FALSE; + } + +} + +class TestCallables { + + public function preRenderPrinted($elements) { + $elements['#printed'] = TRUE; + return $elements; + } + } \ No newline at end of file