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')
44 ->enableLayoutBuilder()
48 $this->drupalLogin($this->drupalCreateUser([
49 'configure any layout',
50 'administer node display',
51 'administer node fields',
52 'administer content types',
57 * Provides test data for ::testLayoutSectionFormatter().
59 public function providerTestLayoutSectionFormatter() {
61 $data['block_with_global_context'] = [
64 'section' => new Section('layout_onecol', [], [
65 'baz' => new SectionComponent('baz', 'content', [
66 'id' => 'test_context_aware',
67 'context_mapping' => [
68 'user' => '@user.current_user_context:current_user',
76 '#test_context_aware--username',
85 $data['block_with_entity_context'] = [
88 'section' => new Section('layout_onecol', [], [
89 'baz' => new SectionComponent('baz', 'content', [
90 'id' => 'field_block:node:bundle_with_section_field:body',
91 'context_mapping' => [
92 'entity' => 'layout_builder.entity',
110 $data['single_section_single_block'] = [
113 'section' => new Section('layout_onecol', [], [
114 'baz' => new SectionComponent('baz', 'content', [
115 'id' => 'system_powered_by_block',
126 $data['multiple_sections'] = [
129 'section' => new Section('layout_onecol', [], [
130 'baz' => new SectionComponent('baz', 'content', [
131 'id' => 'system_powered_by_block',
136 'section' => new Section('layout_twocol', [], [
137 'foo' => new SectionComponent('foo', 'first', [
138 'id' => 'test_block_instantiation',
139 'display_message' => 'foo text',
141 'bar' => new SectionComponent('bar', 'second', [
142 'id' => 'test_block_instantiation',
143 'display_message' => 'bar text',
165 * Tests layout_section formatter output.
167 * @dataProvider providerTestLayoutSectionFormatter
169 public function testLayoutSectionFormatter($layout_data, $expected_selector, $expected_content, $expected_cache_contexts, $expected_cache_tags, $expected_dynamic_cache) {
170 $node = $this->createSectionNode($layout_data);
172 $canonical_url = $node->toUrl('canonical');
173 $this->drupalGet($canonical_url);
174 $this->assertLayoutSection($expected_selector, $expected_content, $expected_cache_contexts, $expected_cache_tags, $expected_dynamic_cache);
176 $this->drupalGet($canonical_url->toString() . '/layout');
177 $this->assertLayoutSection($expected_selector, $expected_content, $expected_cache_contexts, $expected_cache_tags, 'UNCACHEABLE');
181 * Tests the access checking of the section formatter.
183 public function testLayoutSectionFormatterAccess() {
184 $node = $this->createSectionNode([
186 'section' => new Section('layout_onecol', [], [
187 'baz' => new SectionComponent('baz', 'content', [
188 'id' => 'test_access',
194 // Restrict access to the block.
195 $this->container->get('state')->set('test_block_access', FALSE);
197 $this->drupalGet($node->toUrl('canonical'));
198 $this->assertLayoutSection('.layout--onecol', NULL, '', '', 'UNCACHEABLE');
199 // Ensure the block was not rendered.
200 $this->assertSession()->pageTextNotContains('Hello test world');
202 // Grant access to the block, and ensure it was rendered.
203 $this->container->get('state')->set('test_block_access', TRUE);
204 $this->drupalGet($node->toUrl('canonical'));
205 $this->assertLayoutSection('.layout--onecol', 'Hello test world', '', '', 'UNCACHEABLE');
209 * Tests the multilingual support of the section formatter.
211 public function testMultilingualLayoutSectionFormatter() {
212 $this->container->get('module_installer')->install(['content_translation']);
213 $this->rebuildContainer();
215 ConfigurableLanguage::createFromLangcode('es')->save();
216 $this->container->get('content_translation.manager')->setEnabled('node', 'bundle_with_section_field', TRUE);
218 $entity = $this->createSectionNode([
220 'section' => new Section('layout_onecol', [], [
221 'baz' => new SectionComponent('baz', 'content', [
222 'id' => 'system_powered_by_block',
227 $entity->addTranslation('es', [
228 'title' => 'Translated node title',
229 $this->fieldName => [
231 'section' => new Section('layout_twocol', [], [
232 'foo' => new SectionComponent('foo', 'first', [
233 'id' => 'test_block_instantiation',
234 'display_message' => 'foo text',
236 'bar' => new SectionComponent('bar', 'second', [
237 'id' => 'test_block_instantiation',
238 'display_message' => 'bar text',
246 $this->drupalGet($entity->toUrl('canonical'));
247 $this->assertLayoutSection('.layout--onecol', 'Powered by');
248 $this->drupalGet($entity->toUrl('canonical')->setOption('prefix', 'es/'));
249 $this->assertLayoutSection('.layout--twocol', ['foo text', 'bar text']);
253 * Ensures that the entity title is displayed.
255 public function testLayoutPageTitle() {
256 $this->drupalPlaceBlock('page_title_block');
257 $node = $this->createSectionNode([]);
259 $this->drupalGet($node->toUrl('canonical')->toString() . '/layout');
260 $this->assertSession()->titleEquals('Edit layout for The node title | Drupal');
261 $this->assertEquals('Edit layout for The node title', $this->cssSelect('h1.page-title')[0]->getText());
265 * Tests that no Layout link shows without a section field.
267 public function testLayoutUrlNoSectionField() {
268 $node = $this->createNode([
269 'type' => 'bundle_without_section_field',
270 'title' => 'The node title',
273 'value' => 'The node body',
279 $this->drupalGet($node->toUrl('canonical')->toString() . '/layout');
280 $this->assertSession()->statusCodeEquals(404);
284 * Tests that deleting a field removes it from the layout.
286 public function testLayoutDeletingField() {
287 $assert_session = $this->assertSession();
289 $this->drupalGet('/admin/structure/types/manage/bundle_with_section_field/display-layout/default');
290 $assert_session->statusCodeEquals(200);
291 $assert_session->elementExists('css', '.field--name-body');
293 // Delete the field from both bundles.
294 $this->drupalGet('/admin/structure/types/manage/bundle_without_section_field/fields/node.bundle_without_section_field.body/delete');
295 $this->submitForm([], 'Delete');
296 $this->drupalGet('/admin/structure/types/manage/bundle_with_section_field/display-layout/default');
297 $assert_session->statusCodeEquals(200);
298 $assert_session->elementExists('css', '.field--name-body');
300 $this->drupalGet('/admin/structure/types/manage/bundle_with_section_field/fields/node.bundle_with_section_field.body/delete');
301 $this->submitForm([], 'Delete');
302 $this->drupalGet('/admin/structure/types/manage/bundle_with_section_field/display-layout/default');
303 $assert_session->statusCodeEquals(200);
304 $assert_session->elementNotExists('css', '.field--name-body');
308 * Tests that deleting a bundle removes the layout.
310 public function testLayoutDeletingBundle() {
311 $assert_session = $this->assertSession();
313 $display = LayoutBuilderEntityViewDisplay::load('node.bundle_with_section_field.default');
314 $this->assertInstanceOf(LayoutBuilderEntityViewDisplay::class, $display);
316 $this->drupalPostForm('/admin/structure/types/manage/bundle_with_section_field/delete', [], 'Delete');
317 $assert_session->statusCodeEquals(200);
319 $display = LayoutBuilderEntityViewDisplay::load('node.bundle_with_section_field.default');
320 $this->assertNull($display);
324 * Asserts the output of a layout section.
326 * @param string|array $expected_selector
327 * A selector or list of CSS selectors to find.
328 * @param string|array $expected_content
329 * A string or list of strings to find.
330 * @param string $expected_cache_contexts
331 * A string of cache contexts to be found in the header.
332 * @param string $expected_cache_tags
333 * A string of cache tags to be found in the header.
334 * @param string $expected_dynamic_cache
335 * The expected dynamic cache header. Either 'HIT', 'MISS' or 'UNCACHEABLE'.
337 protected function assertLayoutSection($expected_selector, $expected_content, $expected_cache_contexts = '', $expected_cache_tags = '', $expected_dynamic_cache = 'MISS') {
338 $assert_session = $this->assertSession();
339 // Find the given selector.
340 foreach ((array) $expected_selector as $selector) {
341 $element = $this->cssSelect($selector);
342 $this->assertNotEmpty($element);
345 // Find the given content.
346 foreach ((array) $expected_content as $content) {
347 $assert_session->pageTextContains($content);
349 if ($expected_cache_contexts) {
350 $assert_session->responseHeaderContains('X-Drupal-Cache-Contexts', $expected_cache_contexts);
352 if ($expected_cache_tags) {
353 $assert_session->responseHeaderContains('X-Drupal-Cache-Tags', $expected_cache_tags);
355 $assert_session->responseHeaderEquals('X-Drupal-Dynamic-Cache', $expected_dynamic_cache);
359 * Creates a node with a section field.
361 * @param array $section_values
362 * An array of values for a section field.
364 * @return \Drupal\node\NodeInterface
367 protected function createSectionNode(array $section_values) {
368 return $this->createNode([
369 'type' => 'bundle_with_section_field',
370 'title' => 'The node title',
373 'value' => 'The node body',
376 $this->fieldName => $section_values,