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\Plugin\SectionStorage\OverridesSectionStorage;
8 use Drupal\layout_builder\Section;
9 use Drupal\layout_builder\SectionComponent;
10 use Drupal\Tests\BrowserTestBase;
13 * Tests the rendering of a layout section field.
15 * @group layout_builder
17 class LayoutSectionTest extends BrowserTestBase {
22 public static $modules = ['field_ui', 'layout_builder', 'node', 'block_test'];
27 protected function setUp() {
30 $this->createContentType([
31 'type' => 'bundle_without_section_field',
33 $this->createContentType([
34 'type' => 'bundle_with_section_field',
37 LayoutBuilderEntityViewDisplay::load('node.bundle_with_section_field.default')
38 ->enableLayoutBuilder()
42 $this->drupalLogin($this->drupalCreateUser([
43 'configure any layout',
44 'administer node display',
45 'administer node fields',
46 'administer content types',
51 * Provides test data for ::testLayoutSectionFormatter().
53 public function providerTestLayoutSectionFormatter() {
55 $data['block_with_global_context'] = [
58 'section' => new Section('layout_onecol', [], [
59 'baz' => new SectionComponent('baz', 'content', [
60 'id' => 'test_context_aware',
61 'context_mapping' => [
62 'user' => '@user.current_user_context:current_user',
70 '#test_context_aware--username',
79 $data['block_with_entity_context'] = [
82 'section' => new Section('layout_onecol', [], [
83 'baz' => new SectionComponent('baz', 'content', [
84 'id' => 'field_block:node:bundle_with_section_field:body',
85 'context_mapping' => [
86 'entity' => 'layout_builder.entity',
104 $data['single_section_single_block'] = [
107 'section' => new Section('layout_onecol', [], [
108 'baz' => new SectionComponent('baz', 'content', [
109 'id' => 'system_powered_by_block',
120 $data['multiple_sections'] = [
123 'section' => new Section('layout_onecol', [], [
124 'baz' => new SectionComponent('baz', 'content', [
125 'id' => 'system_powered_by_block',
130 'section' => new Section('layout_twocol', [], [
131 'foo' => new SectionComponent('foo', 'first', [
132 'id' => 'test_block_instantiation',
133 'display_message' => 'foo text',
135 'bar' => new SectionComponent('bar', 'second', [
136 'id' => 'test_block_instantiation',
137 'display_message' => 'bar text',
159 * Tests layout_section formatter output.
161 * @dataProvider providerTestLayoutSectionFormatter
163 public function testLayoutSectionFormatter($layout_data, $expected_selector, $expected_content, $expected_cache_contexts, $expected_cache_tags, $expected_dynamic_cache) {
164 $node = $this->createSectionNode($layout_data);
166 $canonical_url = $node->toUrl('canonical');
167 $this->drupalGet($canonical_url);
168 $this->assertLayoutSection($expected_selector, $expected_content, $expected_cache_contexts, $expected_cache_tags, $expected_dynamic_cache);
170 $this->drupalGet($canonical_url->toString() . '/layout');
171 $this->assertLayoutSection($expected_selector, $expected_content, $expected_cache_contexts, $expected_cache_tags, 'UNCACHEABLE');
175 * Tests the access checking of the section formatter.
177 public function testLayoutSectionFormatterAccess() {
178 $node = $this->createSectionNode([
180 'section' => new Section('layout_onecol', [], [
181 'baz' => new SectionComponent('baz', 'content', [
182 'id' => 'test_access',
188 // Restrict access to the block.
189 $this->container->get('state')->set('test_block_access', FALSE);
191 $this->drupalGet($node->toUrl('canonical'));
192 $this->assertLayoutSection('.layout--onecol', NULL, '', '', 'UNCACHEABLE');
193 // Ensure the block was not rendered.
194 $this->assertSession()->pageTextNotContains('Hello test world');
196 // Grant access to the block, and ensure it was rendered.
197 $this->container->get('state')->set('test_block_access', TRUE);
198 $this->drupalGet($node->toUrl('canonical'));
199 $this->assertLayoutSection('.layout--onecol', 'Hello test world', '', '', 'UNCACHEABLE');
203 * Tests the multilingual support of the section formatter.
205 public function testMultilingualLayoutSectionFormatter() {
206 $this->container->get('module_installer')->install(['content_translation']);
207 $this->rebuildContainer();
209 ConfigurableLanguage::createFromLangcode('es')->save();
210 $this->container->get('content_translation.manager')->setEnabled('node', 'bundle_with_section_field', TRUE);
212 $entity = $this->createSectionNode([
214 'section' => new Section('layout_onecol', [], [
215 'baz' => new SectionComponent('baz', 'content', [
216 'id' => 'system_powered_by_block',
221 $entity->addTranslation('es', [
222 'title' => 'Translated node title',
223 OverridesSectionStorage::FIELD_NAME => [
225 'section' => new Section('layout_twocol', [], [
226 'foo' => new SectionComponent('foo', 'first', [
227 'id' => 'test_block_instantiation',
228 'display_message' => 'foo text',
230 'bar' => new SectionComponent('bar', 'second', [
231 'id' => 'test_block_instantiation',
232 'display_message' => 'bar text',
240 $this->drupalGet($entity->toUrl('canonical'));
241 $this->assertLayoutSection('.layout--onecol', 'Powered by');
242 $this->drupalGet($entity->toUrl('canonical')->setOption('prefix', 'es/'));
243 $this->assertLayoutSection('.layout--twocol', ['foo text', 'bar text']);
247 * Ensures that the entity title is displayed.
249 public function testLayoutPageTitle() {
250 $this->drupalPlaceBlock('page_title_block');
251 $node = $this->createSectionNode([]);
253 $this->drupalGet($node->toUrl('canonical')->toString() . '/layout');
254 $this->assertSession()->titleEquals('Edit layout for The node title | Drupal');
255 $this->assertEquals('Edit layout for The node title', $this->cssSelect('h1.page-title')[0]->getText());
259 * Tests that no Layout link shows without a section field.
261 public function testLayoutUrlNoSectionField() {
262 $node = $this->createNode([
263 'type' => 'bundle_without_section_field',
264 'title' => 'The node title',
267 'value' => 'The node body',
273 $this->drupalGet($node->toUrl('canonical')->toString() . '/layout');
274 $this->assertSession()->statusCodeEquals(404);
278 * Tests that deleting a field removes it from the layout.
280 public function testLayoutDeletingField() {
281 $assert_session = $this->assertSession();
283 $this->drupalGet('/admin/structure/types/manage/bundle_with_section_field/display-layout/default');
284 $assert_session->statusCodeEquals(200);
285 $assert_session->elementExists('css', '.field--name-body');
287 // Delete the field from both bundles.
288 $this->drupalGet('/admin/structure/types/manage/bundle_without_section_field/fields/node.bundle_without_section_field.body/delete');
289 $this->submitForm([], 'Delete');
290 $this->drupalGet('/admin/structure/types/manage/bundle_with_section_field/display-layout/default');
291 $assert_session->statusCodeEquals(200);
292 $assert_session->elementExists('css', '.field--name-body');
294 $this->drupalGet('/admin/structure/types/manage/bundle_with_section_field/fields/node.bundle_with_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->elementNotExists('css', '.field--name-body');
302 * Tests that deleting a bundle removes the layout.
304 public function testLayoutDeletingBundle() {
305 $assert_session = $this->assertSession();
307 $display = LayoutBuilderEntityViewDisplay::load('node.bundle_with_section_field.default');
308 $this->assertInstanceOf(LayoutBuilderEntityViewDisplay::class, $display);
310 $this->drupalPostForm('/admin/structure/types/manage/bundle_with_section_field/delete', [], 'Delete');
311 $assert_session->statusCodeEquals(200);
313 $display = LayoutBuilderEntityViewDisplay::load('node.bundle_with_section_field.default');
314 $this->assertNull($display);
318 * Asserts the output of a layout section.
320 * @param string|array $expected_selector
321 * A selector or list of CSS selectors to find.
322 * @param string|array $expected_content
323 * A string or list of strings to find.
324 * @param string $expected_cache_contexts
325 * A string of cache contexts to be found in the header.
326 * @param string $expected_cache_tags
327 * A string of cache tags to be found in the header.
328 * @param string $expected_dynamic_cache
329 * The expected dynamic cache header. Either 'HIT', 'MISS' or 'UNCACHEABLE'.
331 protected function assertLayoutSection($expected_selector, $expected_content, $expected_cache_contexts = '', $expected_cache_tags = '', $expected_dynamic_cache = 'MISS') {
332 $assert_session = $this->assertSession();
333 // Find the given selector.
334 foreach ((array) $expected_selector as $selector) {
335 $element = $this->cssSelect($selector);
336 $this->assertNotEmpty($element);
339 // Find the given content.
340 foreach ((array) $expected_content as $content) {
341 $assert_session->pageTextContains($content);
343 if ($expected_cache_contexts) {
344 $assert_session->responseHeaderContains('X-Drupal-Cache-Contexts', $expected_cache_contexts);
346 if ($expected_cache_tags) {
347 $assert_session->responseHeaderContains('X-Drupal-Cache-Tags', $expected_cache_tags);
349 $assert_session->responseHeaderEquals('X-Drupal-Dynamic-Cache', $expected_dynamic_cache);
353 * Creates a node with a section field.
355 * @param array $section_values
356 * An array of values for a section field.
358 * @return \Drupal\node\NodeInterface
361 protected function createSectionNode(array $section_values) {
362 return $this->createNode([
363 'type' => 'bundle_with_section_field',
364 'title' => 'The node title',
367 'value' => 'The node body',
370 OverridesSectionStorage::FIELD_NAME => $section_values,