3 namespace Drupal\Tests\Core\Layout;
5 use Drupal\Component\Plugin\Derivative\DeriverBase;
6 use Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException;
7 use Drupal\Core\Cache\CacheBackendInterface;
8 use Drupal\Core\DependencyInjection\ContainerBuilder;
9 use Drupal\Core\Extension\Extension;
10 use Drupal\Core\Extension\ModuleHandlerInterface;
11 use Drupal\Core\Extension\ThemeHandlerInterface;
12 use Drupal\Core\Layout\LayoutDefault;
13 use Drupal\Core\Layout\LayoutDefinition;
14 use Drupal\Core\Layout\LayoutPluginManager;
15 use Drupal\Core\StringTranslation\TranslatableMarkup;
16 use Drupal\Tests\UnitTestCase;
17 use org\bovigo\vfs\vfsStream;
18 use Prophecy\Argument;
21 * @coversDefaultClass \Drupal\Core\Layout\LayoutPluginManager
24 class LayoutPluginManagerTest extends UnitTestCase {
29 * @var \Drupal\Core\Extension\ModuleHandlerInterface
31 protected $moduleHandler;
36 * @var \Drupal\Core\Extension\ThemeHandlerInterface
38 protected $themeHandler;
41 * Cache backend instance.
43 * @var \Drupal\Core\Cache\CacheBackendInterface
45 protected $cacheBackend;
48 * The layout plugin manager.
50 * @var \Drupal\Core\Layout\LayoutPluginManagerInterface
52 protected $layoutPluginManager;
57 protected function setUp() {
60 $this->setUpFilesystem();
62 $container = new ContainerBuilder();
63 $container->set('string_translation', $this->getStringTranslationStub());
64 \Drupal::setContainer($container);
66 $this->moduleHandler = $this->prophesize(ModuleHandlerInterface::class);
68 $this->moduleHandler->moduleExists('module_a')->willReturn(TRUE);
69 $this->moduleHandler->moduleExists('theme_a')->willReturn(FALSE);
70 $this->moduleHandler->moduleExists('core')->willReturn(FALSE);
71 $this->moduleHandler->moduleExists('invalid_provider')->willReturn(FALSE);
73 $module_a = new Extension('/', 'module', vfsStream::url('root/modules/module_a/module_a.layouts.yml'));
74 $this->moduleHandler->getModule('module_a')->willReturn($module_a);
75 $this->moduleHandler->getModuleDirectories()->willReturn(['module_a' => vfsStream::url('root/modules/module_a')]);
76 $this->moduleHandler->alter('layout', Argument::type('array'))->shouldBeCalled();
78 $this->themeHandler = $this->prophesize(ThemeHandlerInterface::class);
80 $this->themeHandler->themeExists('theme_a')->willReturn(TRUE);
81 $this->themeHandler->themeExists('core')->willReturn(FALSE);
82 $this->themeHandler->themeExists('invalid_provider')->willReturn(FALSE);
84 $theme_a = new Extension('/', 'theme', vfsStream::url('root/themes/theme_a/theme_a.layouts.yml'));
85 $this->themeHandler->getTheme('theme_a')->willReturn($theme_a);
86 $this->themeHandler->getThemeDirectories()->willReturn(['theme_a' => vfsStream::url('root/themes/theme_a')]);
88 $this->cacheBackend = $this->prophesize(CacheBackendInterface::class);
90 $namespaces = new \ArrayObject(['Drupal\Core' => vfsStream::url('root/core/lib/Drupal/Core')]);
91 $this->layoutPluginManager = new LayoutPluginManager($namespaces, $this->cacheBackend->reveal(), $this->moduleHandler->reveal(), $this->themeHandler->reveal(), $this->getStringTranslationStub());
95 * @covers ::getDefinitions
96 * @covers ::providerExists
98 public function testGetDefinitions() {
100 'module_a_provided_layout',
101 'theme_a_provided_layout',
102 'plugin_provided_layout',
105 $layout_definitions = $this->layoutPluginManager->getDefinitions();
106 $this->assertEquals($expected, array_keys($layout_definitions));
107 $this->assertContainsOnlyInstancesOf(LayoutDefinition::class, $layout_definitions);
111 * @covers ::getDefinition
112 * @covers ::processDefinition
114 public function testGetDefinition() {
115 $theme_a_path = vfsStream::url('root/themes/theme_a');
116 $layout_definition = $this->layoutPluginManager->getDefinition('theme_a_provided_layout');
117 $this->assertSame('theme_a_provided_layout', $layout_definition->id());
118 $this->assertSame('2 column layout', (string) $layout_definition->getLabel());
119 $this->assertSame('Columns: 2', (string) $layout_definition->getCategory());
120 $this->assertSame('A theme provided layout', (string) $layout_definition->getDescription());
121 $this->assertTrue($layout_definition->getLabel() instanceof TranslatableMarkup);
122 $this->assertTrue($layout_definition->getCategory() instanceof TranslatableMarkup);
123 $this->assertTrue($layout_definition->getDescription() instanceof TranslatableMarkup);
124 $this->assertSame('twocol', $layout_definition->getTemplate());
125 $this->assertSame("$theme_a_path/templates", $layout_definition->getPath());
126 $this->assertSame('theme_a/twocol', $layout_definition->getLibrary());
127 $this->assertSame('twocol', $layout_definition->getThemeHook());
128 $this->assertSame("$theme_a_path/templates", $layout_definition->getTemplatePath());
129 $this->assertSame('theme_a', $layout_definition->getProvider());
130 $this->assertSame('right', $layout_definition->getDefaultRegion());
131 $this->assertSame(LayoutDefault::class, $layout_definition->getClass());
132 $expected_regions = [
134 'label' => new TranslatableMarkup('Left region', [], ['context' => 'layout_region']),
137 'label' => new TranslatableMarkup('Right region', [], ['context' => 'layout_region']),
140 $regions = $layout_definition->getRegions();
141 $this->assertEquals($expected_regions, $regions);
142 $this->assertTrue($regions['left']['label'] instanceof TranslatableMarkup);
143 $this->assertTrue($regions['right']['label'] instanceof TranslatableMarkup);
145 $module_a_path = vfsStream::url('root/modules/module_a');
146 $layout_definition = $this->layoutPluginManager->getDefinition('module_a_provided_layout');
147 $this->assertSame('module_a_provided_layout', $layout_definition->id());
148 $this->assertSame('1 column layout', (string) $layout_definition->getLabel());
149 $this->assertSame('Columns: 1', (string) $layout_definition->getCategory());
150 $this->assertSame('A module provided layout', (string) $layout_definition->getDescription());
151 $this->assertTrue($layout_definition->getLabel() instanceof TranslatableMarkup);
152 $this->assertTrue($layout_definition->getCategory() instanceof TranslatableMarkup);
153 $this->assertTrue($layout_definition->getDescription() instanceof TranslatableMarkup);
154 $this->assertSame(NULL, $layout_definition->getTemplate());
155 $this->assertSame("$module_a_path/layouts", $layout_definition->getPath());
156 $this->assertSame('module_a/onecol', $layout_definition->getLibrary());
157 $this->assertSame('onecol', $layout_definition->getThemeHook());
158 $this->assertSame(NULL, $layout_definition->getTemplatePath());
159 $this->assertSame('module_a', $layout_definition->getProvider());
160 $this->assertSame('top', $layout_definition->getDefaultRegion());
161 $this->assertSame(LayoutDefault::class, $layout_definition->getClass());
162 $expected_regions = [
164 'label' => new TranslatableMarkup('Top region', [], ['context' => 'layout_region']),
167 'label' => new TranslatableMarkup('Bottom region', [], ['context' => 'layout_region']),
170 $regions = $layout_definition->getRegions();
171 $this->assertEquals($expected_regions, $regions);
172 $this->assertTrue($regions['top']['label'] instanceof TranslatableMarkup);
173 $this->assertTrue($regions['bottom']['label'] instanceof TranslatableMarkup);
175 $core_path = '/core/lib/Drupal/Core';
176 $layout_definition = $this->layoutPluginManager->getDefinition('plugin_provided_layout');
177 $this->assertSame('plugin_provided_layout', $layout_definition->id());
178 $this->assertEquals('Layout plugin', $layout_definition->getLabel());
179 $this->assertEquals('Columns: 1', $layout_definition->getCategory());
180 $this->assertEquals('Test layout', $layout_definition->getDescription());
181 $this->assertTrue($layout_definition->getLabel() instanceof TranslatableMarkup);
182 $this->assertTrue($layout_definition->getCategory() instanceof TranslatableMarkup);
183 $this->assertTrue($layout_definition->getDescription() instanceof TranslatableMarkup);
184 $this->assertSame('plugin-provided-layout', $layout_definition->getTemplate());
185 $this->assertSame($core_path, $layout_definition->getPath());
186 $this->assertSame(NULL, $layout_definition->getLibrary());
187 $this->assertSame('plugin_provided_layout', $layout_definition->getThemeHook());
188 $this->assertSame("$core_path/templates", $layout_definition->getTemplatePath());
189 $this->assertSame('core', $layout_definition->getProvider());
190 $this->assertSame('main', $layout_definition->getDefaultRegion());
191 $this->assertSame('Drupal\Core\Plugin\Layout\TestLayout', $layout_definition->getClass());
192 $expected_regions = [
194 'label' => new TranslatableMarkup('Main Region', [], ['context' => 'layout_region']),
197 $regions = $layout_definition->getRegions();
198 $this->assertEquals($expected_regions, $regions);
199 $this->assertTrue($regions['main']['label'] instanceof TranslatableMarkup);
203 * @covers ::processDefinition
205 public function testProcessDefinition() {
206 $this->moduleHandler->alter('layout', Argument::type('array'))->shouldNotBeCalled();
207 $this->setExpectedException(InvalidPluginDefinitionException::class, 'The "module_a_derived_layout:array_based" layout definition must extend ' . LayoutDefinition::class);
208 $module_a_provided_layout = <<<'EOS'
209 module_a_derived_layout:
210 deriver: \Drupal\Tests\Core\Layout\LayoutDeriver
216 'module_a.layouts.yml' => $module_a_provided_layout,
220 $this->layoutPluginManager->getDefinitions();
224 * @covers ::getThemeImplementations
226 public function testGetThemeImplementations() {
227 $core_path = '/core/lib/Drupal/Core';
228 $theme_a_path = vfsStream::url('root/themes/theme_a');
231 'render element' => 'content',
234 'render element' => 'content',
235 'base hook' => 'layout',
236 'template' => 'twocol',
237 'path' => "$theme_a_path/templates",
239 'plugin_provided_layout' => [
240 'render element' => 'content',
241 'base hook' => 'layout',
242 'template' => 'plugin-provided-layout',
243 'path' => "$core_path/templates",
246 $theme_implementations = $this->layoutPluginManager->getThemeImplementations();
247 $this->assertEquals($expected, $theme_implementations);
251 * @covers ::getCategories
253 public function testGetCategories() {
258 $categories = $this->layoutPluginManager->getCategories();
259 $this->assertEquals($expected, $categories);
263 * @covers ::getSortedDefinitions
265 public function testGetSortedDefinitions() {
267 'module_a_provided_layout',
268 'plugin_provided_layout',
269 'theme_a_provided_layout',
272 $layout_definitions = $this->layoutPluginManager->getSortedDefinitions();
273 $this->assertEquals($expected, array_keys($layout_definitions));
274 $this->assertContainsOnlyInstancesOf(LayoutDefinition::class, $layout_definitions);
278 * @covers ::getGroupedDefinitions
280 public function testGetGroupedDefinitions() {
281 $category_expected = [
283 'module_a_provided_layout',
284 'plugin_provided_layout',
287 'theme_a_provided_layout',
291 $definitions = $this->layoutPluginManager->getGroupedDefinitions();
292 $this->assertEquals(array_keys($category_expected), array_keys($definitions));
293 foreach ($category_expected as $category => $expected) {
294 $this->assertArrayHasKey($category, $definitions);
295 $this->assertEquals($expected, array_keys($definitions[$category]));
296 $this->assertContainsOnlyInstancesOf(LayoutDefinition::class, $definitions[$category]);
301 * Sets up the filesystem with YAML files and annotated plugins.
303 protected function setUpFilesystem() {
304 $module_a_provided_layout = <<<'EOS'
305 module_a_provided_layout:
306 label: 1 column layout
307 category: 'Columns: 1'
308 description: 'A module provided layout'
311 library: module_a/onecol
317 module_a_derived_layout:
318 deriver: \Drupal\Tests\Core\Layout\LayoutDeriver
319 invalid_provider: true
321 $theme_a_provided_layout = <<<'EOS'
322 theme_a_provided_layout:
323 class: '\Drupal\Core\Layout\LayoutDefault'
324 label: 2 column layout
325 category: 'Columns: 2'
326 description: 'A theme provided layout'
329 library: theme_a/twocol
330 default_region: right
337 $plugin_provided_layout = <<<'EOS'
339 namespace Drupal\Core\Plugin\Layout;
340 use Drupal\Core\Layout\LayoutDefault;
343 * id = "plugin_provided_layout",
344 * label = @Translation("Layout plugin"),
345 * category = @Translation("Columns: 1"),
346 * description = @Translation("Test layout"),
347 * path = "core/lib/Drupal/Core",
348 * template = "templates/plugin-provided-layout",
351 * "label" = @Translation("Main Region", context = "layout_region")
356 class TestLayout extends LayoutDefault {}
358 vfsStream::setup('root');
362 'module_a.layouts.yml' => $module_a_provided_layout,
369 'theme_a.layouts.yml' => $theme_a_provided_layout,
380 'TestLayout.php' => $plugin_provided_layout,
393 * Provides a dynamic layout deriver for the test.
395 class LayoutDeriver extends DeriverBase {
400 public function getDerivativeDefinitions($base_plugin_definition) {
401 if ($base_plugin_definition->get('array_based')) {
402 $this->derivatives['array_based'] = [];
404 if ($base_plugin_definition->get('invalid_provider')) {
405 $this->derivatives['invalid_provider'] = new LayoutDefinition([
406 'id' => 'invalid_provider',
407 'provider' => 'invalid_provider',
410 return $this->derivatives;