3 namespace Drupal\Tests\layout_builder\Functional;
5 use Drupal\language\Entity\ConfigurableLanguage;
6 use Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay;
7 use Drupal\layout_builder\Section;
8 use Drupal\layout_builder\SectionComponent;
9 use Drupal\Tests\BrowserTestBase;
12 * Tests the rendering of a layout section field.
14 * @group layout_builder
16 class LayoutSectionTest extends BrowserTestBase {
21 public static $modules = ['field_ui', 'layout_builder', 'node', 'block_test'];
24 * The name of the layout section field.
28 protected $fieldName = 'layout_builder__layout';
33 protected function setUp() {
36 $this->createContentType([
37 'type' => 'bundle_without_section_field',
39 $this->createContentType([
40 'type' => 'bundle_with_section_field',
43 LayoutBuilderEntityViewDisplay::load('node.bundle_with_section_field.default')
47 $this->drupalLogin($this->drupalCreateUser([
48 'configure any layout',
49 'administer node display',
50 'administer node fields',
51 'administer content types',
56 * Provides test data for ::testLayoutSectionFormatter().
58 public function providerTestLayoutSectionFormatter() {
60 $data['block_with_global_context'] = [
63 'section' => new Section('layout_onecol', [], [
64 'baz' => new SectionComponent('baz', 'content', [
65 'id' => 'test_context_aware',
66 'context_mapping' => [
67 'user' => '@user.current_user_context:current_user',
75 '#test_context_aware--username',
84 $data['block_with_entity_context'] = [
87 'section' => new Section('layout_onecol', [], [
88 'baz' => new SectionComponent('baz', 'content', [
89 'id' => 'field_block:node:bundle_with_section_field:body',
90 'context_mapping' => [
91 'entity' => 'layout_builder.entity',
109 $data['single_section_single_block'] = [
112 'section' => new Section('layout_onecol', [], [
113 'baz' => new SectionComponent('baz', 'content', [
114 'id' => 'system_powered_by_block',
125 $data['multiple_sections'] = [
128 'section' => new Section('layout_onecol', [], [
129 'baz' => new SectionComponent('baz', 'content', [
130 'id' => 'system_powered_by_block',
135 'section' => new Section('layout_twocol', [], [
136 'foo' => new SectionComponent('foo', 'first', [
137 'id' => 'test_block_instantiation',
138 'display_message' => 'foo text',
140 'bar' => new SectionComponent('bar', 'second', [
141 'id' => 'test_block_instantiation',
142 'display_message' => 'bar text',
164 * Tests layout_section formatter output.
166 * @dataProvider providerTestLayoutSectionFormatter
168 public function testLayoutSectionFormatter($layout_data, $expected_selector, $expected_content, $expected_cache_contexts, $expected_cache_tags, $expected_dynamic_cache) {
169 $node = $this->createSectionNode($layout_data);
171 $canonical_url = $node->toUrl('canonical');
172 $this->drupalGet($canonical_url);
173 $this->assertLayoutSection($expected_selector, $expected_content, $expected_cache_contexts, $expected_cache_tags, $expected_dynamic_cache);
175 $this->drupalGet($canonical_url->toString() . '/layout');
176 $this->assertLayoutSection($expected_selector, $expected_content, $expected_cache_contexts, $expected_cache_tags, 'UNCACHEABLE');
180 * Tests the access checking of the section formatter.
182 public function testLayoutSectionFormatterAccess() {
183 $node = $this->createSectionNode([
185 'section' => new Section('layout_onecol', [], [
186 'baz' => new SectionComponent('baz', 'content', [
187 'id' => 'test_access',
193 // Restrict access to the block.
194 $this->container->get('state')->set('test_block_access', FALSE);
196 $this->drupalGet($node->toUrl('canonical'));
197 $this->assertLayoutSection('.layout--onecol', NULL, '', '', 'UNCACHEABLE');
198 // Ensure the block was not rendered.
199 $this->assertSession()->pageTextNotContains('Hello test world');
201 // Grant access to the block, and ensure it was rendered.
202 $this->container->get('state')->set('test_block_access', TRUE);
203 $this->drupalGet($node->toUrl('canonical'));
204 $this->assertLayoutSection('.layout--onecol', 'Hello test world', '', '', 'UNCACHEABLE');
208 * Tests the multilingual support of the section formatter.
210 public function testMultilingualLayoutSectionFormatter() {
211 $this->container->get('module_installer')->install(['content_translation']);
212 $this->rebuildContainer();
214 ConfigurableLanguage::createFromLangcode('es')->save();
215 $this->container->get('content_translation.manager')->setEnabled('node', 'bundle_with_section_field', TRUE);
217 $entity = $this->createSectionNode([
219 'section' => new Section('layout_onecol', [], [
220 'baz' => new SectionComponent('baz', 'content', [
221 'id' => 'system_powered_by_block',
226 $entity->addTranslation('es', [
227 'title' => 'Translated node title',
228 $this->fieldName => [
230 'section' => new Section('layout_twocol', [], [
231 'foo' => new SectionComponent('foo', 'first', [
232 'id' => 'test_block_instantiation',
233 'display_message' => 'foo text',
235 'bar' => new SectionComponent('bar', 'second', [
236 'id' => 'test_block_instantiation',
237 'display_message' => 'bar text',
245 $this->drupalGet($entity->toUrl('canonical'));
246 $this->assertLayoutSection('.layout--onecol', 'Powered by');
247 $this->drupalGet($entity->toUrl('canonical')->setOption('prefix', 'es/'));
248 $this->assertLayoutSection('.layout--twocol', ['foo text', 'bar text']);
252 * Ensures that the entity title is displayed.
254 public function testLayoutPageTitle() {
255 $this->drupalPlaceBlock('page_title_block');
256 $node = $this->createSectionNode([]);
258 $this->drupalGet($node->toUrl('canonical')->toString() . '/layout');
259 $this->assertSession()->titleEquals('Edit layout for The node title | Drupal');
260 $this->assertEquals('Edit layout for The node title', $this->cssSelect('h1.page-title')[0]->getText());
264 * Tests that no Layout link shows without a section field.
266 public function testLayoutUrlNoSectionField() {
267 $node = $this->createNode([
268 'type' => 'bundle_without_section_field',
269 'title' => 'The node title',
272 'value' => 'The node body',
278 $this->drupalGet($node->toUrl('canonical')->toString() . '/layout');
279 $this->assertSession()->statusCodeEquals(404);
283 * Tests that deleting a field removes it from the layout.
285 public function testLayoutDeletingField() {
286 $assert_session = $this->assertSession();
288 $this->drupalGet('/admin/structure/types/manage/bundle_with_section_field/display-layout/default');
289 $assert_session->statusCodeEquals(200);
290 $assert_session->elementExists('css', '.field--name-body');
292 // Delete the field from both bundles.
293 $this->drupalGet('/admin/structure/types/manage/bundle_without_section_field/fields/node.bundle_without_section_field.body/delete');
294 $this->submitForm([], 'Delete');
295 $this->drupalGet('/admin/structure/types/manage/bundle_with_section_field/display-layout/default');
296 $assert_session->statusCodeEquals(200);
297 $assert_session->elementExists('css', '.field--name-body');
299 $this->drupalGet('/admin/structure/types/manage/bundle_with_section_field/fields/node.bundle_with_section_field.body/delete');
300 $this->submitForm([], 'Delete');
301 $this->drupalGet('/admin/structure/types/manage/bundle_with_section_field/display-layout/default');
302 $assert_session->statusCodeEquals(200);
303 $assert_session->elementNotExists('css', '.field--name-body');
307 * Tests that deleting a bundle removes the layout.
309 public function testLayoutDeletingBundle() {
310 $assert_session = $this->assertSession();
312 $display = LayoutBuilderEntityViewDisplay::load('node.bundle_with_section_field.default');
313 $this->assertInstanceOf(LayoutBuilderEntityViewDisplay::class, $display);
315 $this->drupalPostForm('/admin/structure/types/manage/bundle_with_section_field/delete', [], 'Delete');
316 $assert_session->statusCodeEquals(200);
318 $display = LayoutBuilderEntityViewDisplay::load('node.bundle_with_section_field.default');
319 $this->assertNull($display);
323 * Asserts the output of a layout section.
325 * @param string|array $expected_selector
326 * A selector or list of CSS selectors to find.
327 * @param string|array $expected_content
328 * A string or list of strings to find.
329 * @param string $expected_cache_contexts
330 * A string of cache contexts to be found in the header.
331 * @param string $expected_cache_tags
332 * A string of cache tags to be found in the header.
333 * @param string $expected_dynamic_cache
334 * The expected dynamic cache header. Either 'HIT', 'MISS' or 'UNCACHEABLE'.
336 protected function assertLayoutSection($expected_selector, $expected_content, $expected_cache_contexts = '', $expected_cache_tags = '', $expected_dynamic_cache = 'MISS') {
337 $assert_session = $this->assertSession();
338 // Find the given selector.
339 foreach ((array) $expected_selector as $selector) {
340 $element = $this->cssSelect($selector);
341 $this->assertNotEmpty($element);
344 // Find the given content.
345 foreach ((array) $expected_content as $content) {
346 $assert_session->pageTextContains($content);
348 if ($expected_cache_contexts) {
349 $assert_session->responseHeaderContains('X-Drupal-Cache-Contexts', $expected_cache_contexts);
351 if ($expected_cache_tags) {
352 $assert_session->responseHeaderContains('X-Drupal-Cache-Tags', $expected_cache_tags);
354 $assert_session->responseHeaderEquals('X-Drupal-Dynamic-Cache', $expected_dynamic_cache);
358 * Creates a node with a section field.
360 * @param array $section_values
361 * An array of values for a section field.
363 * @return \Drupal\node\NodeInterface
366 protected function createSectionNode(array $section_values) {
367 return $this->createNode([
368 'type' => 'bundle_with_section_field',
369 'title' => 'The node title',
372 'value' => 'The node body',
375 $this->fieldName => $section_values,