custom_elements-8.x-2.x-dev/tests/src/Kernel/CanvasIntegrationTest.php

tests/src/Kernel/CanvasIntegrationTest.php
<?php

declare(strict_types=1);

namespace Drupal\Tests\custom_elements\Kernel;

use Drupal\canvas\Entity\Page;
use Drupal\canvas\Entity\VersionedConfigEntityBase;
use Drupal\canvas_extjs\Plugin\Canvas\ComponentSource\ExternalJavaScriptComponent;
use Drupal\custom_elements\CustomElement;
use Drupal\custom_elements\Entity\EntityCeDisplay;
use Drupal\canvas\Entity\Component;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\KernelTests\KernelTestBase;
use Drupal\node\Entity\Node;
use Drupal\Tests\custom_elements\Traits\TestDebugHelperTrait;
use Drupal\Tests\canvas\Traits\GenerateComponentConfigTrait;
use Drupal\Tests\node\Traits\ContentTypeCreationTrait;
use Drupal\Tests\node\Traits\NodeCreationTrait;

/**
 * Tests integration between Custom Elements and Canvas.
 */
class CanvasIntegrationTest extends KernelTestBase {

  use ContentTypeCreationTrait;
  use TestDebugHelperTrait;
  use GenerateComponentConfigTrait;
  use NodeCreationTrait;
  use \Drupal\custom_elements\CustomElementGeneratorTrait;

  /**
   * Global debug output killswitch.
   *
   * Set to TRUE to enable debug output, FALSE to disable.
   */
  protected const DEBUG_ENABLED = FALSE;

  /**
   * {@inheritdoc}
   *
   * @var bool
   * @todo Fix config schema for CE-displays and re-enable.
   *
   * phpcs:disable DrupalPractice.Objects.StrictSchemaDisabled.StrictConfigSchema
   */
  protected $strictConfigSchema = FALSE;
  // phpcs:enable

  /**
   * {@inheritdoc}
   */
  protected static $modules = [
    'system',
    'user',
    'field',
    'node',
    'text',
    'filter',
    'editor',
    'ckeditor5',
    'options',
    'image',
    'file',
    'media',
    'link',
    'path',
    'path_alias',
    'layout_discovery',
    'layout_builder',
    'block',
    'sdc',
    'custom_elements',
    'canvas',
    'canvas_extjs',
    'canvas_test_sdc',
    'canvas_test_rendering',
  ];

  /**
   * {@inheritdoc}
   */
  protected function setUp(): void {
    parent::setUp();

    $this->installEntitySchema('user');
    $this->installEntitySchema('media');
    $this->installEntitySchema('node');
    $this->installConfig(['system', 'field', 'node', 'filter', 'editor']);

    // Install Canvas config.
    $this->installConfig(['canvas']);

    // These tests expect legacy format.
    \Drupal::configFactory()
      ->getEditable('custom_elements.settings')
      ->set('json_format', 'legacy')
      ->save();

    // Create a content type.
    $this->createContentType(['type' => 'article', 'name' => 'Article']);

    // Create Canvas component tree field.
    $this->createComponentTreeField('node', 'article');

    // Generate component config for Canvas components.
    $this->generateComponentConfig();

    // Debug: List all available components.
    $components = Component::loadMultiple();
    $this->debug(
          '/tmp/canvas_available_components.json', [
            'component_ids' => array_keys($components),
            'component_details' => array_map(
              function ($c) {
                  return [
                    'id' => $c->id(),
                    'label' => $c->label(),
                    'source' => $c->get('source'),
                    'status' => $c->status(),
                  ];
              }, $components
            ),
          ]
      );
  }

  /**
   * Creates a component tree field for the given entity type and bundle.
   *
   * @param string $entity_type_id
   *   The entity type ID.
   * @param string $bundle
   *   The bundle name.
   */
  protected function createComponentTreeField(string $entity_type_id, string $bundle): void {
    $field_storage = FieldStorageConfig::create(
          [
            'field_name' => 'field_component_tree',
            'entity_type' => $entity_type_id,
            'type' => 'component_tree',
          ]
      );
    $field_storage->save();

    FieldConfig::create(
          [
            'field_storage' => $field_storage,
            'bundle' => $bundle,
          ]
      )->save();
  }

  /**
   * Creates a test ExtJS component entity.
   *
   * @param string $component_name
   *   The component name (e.g., 'TestButton', 'TwoColumnLayout').
   * @param array $config
   *   Component configuration with 'label', 'category', 'props', 'slots'.
   */
  protected function createExtJsComponent(string $component_name, array $config): void {
    $snake_case_name = strtolower(preg_replace('/(?<!^)[A-Z]/', '_$0', $component_name));
    $component_id = 'extjs.' . $snake_case_name;

    // Check if already exists.
    if (Component::load($component_id)) {
      return;
    }

    $settings = [
      'label' => $config['label'],
      'local_source_id' => $component_name,
      'props' => $config['props'],
      'slots' => $config['slots'] ?? [],
      'prop_field_definitions' => [],
    ];

    $component = Component::create([
      'id' => $component_id,
      'label' => $config['label'],
      'category' => $config['category'],
      'source' => 'extjs',
      'provider' => NULL,
      'source_local_id' => $component_name,
      'active_version' => 'v1',
      'versioned_properties' => [
        VersionedConfigEntityBase::ACTIVE_VERSION => ['settings' => $settings],
      ],
      'status' => TRUE,
    ]);

    ExternalJavaScriptComponent::ensurePropFieldDefinitions($component);
    $component->save();
  }

  /**
   * Tests basic Canvas component conversion with multiple components.
   */
  public function testCanvasBasicComponentConversion(): void {
    // Create a node with three Canvas components.
    $node = Node::create([
      'type' => 'article',
      'title' => 'Test Node',
      'field_component_tree' => [
        [
          'uuid' => 'aaaaaaaa-1111-2222-3333-444444444444',
          'component_id' => 'sdc.canvas_test_sdc.props-no-slots',
          'component_version' => 'active',
          'inputs' => [
            'heading' => [
              'sourceType' => 'static:field_item:string',
              'value' => 'First Component',
              'expression' => 'ℹ︎string␟value',
            ],
          ],
        ],
        [
          'uuid' => 'bbbbbbbb-1111-2222-3333-444444444444',
          'component_id' => 'sdc.canvas_test_sdc.props-no-slots',
          'component_version' => 'active',
          'inputs' => [
            'heading' => [
              'sourceType' => 'static:field_item:string',
              'value' => 'Second Component',
              'expression' => 'ℹ︎string␟value',
            ],
          ],
        ],
        [
          'uuid' => 'cccccccc-1111-2222-3333-444444444444',
          'component_id' => 'sdc.canvas_test_sdc.props-no-slots',
          'component_version' => 'active',
          'inputs' => [
            'heading' => [
              'sourceType' => 'static:field_item:string',
              'value' => 'Third Component',
              'expression' => 'ℹ︎string␟value',
            ],
          ],
        ],
      ],
    ]);
    $node->save();

    // Get the Canvas field render array.
    $field_items = $node->get('field_component_tree');
    $canvas_render_array = $field_items->toRenderable($node);
    $this->assertIsArray($canvas_render_array, 'Canvas should return render array');

    /**
     * @var \Drupal\custom_elements\RenderConverter\CanvasRenderConverter $converter
     */
    $converter = $this->container->get('custom_elements.canvas_render_converter');

    // Configure this SDC to be rendered as custom element (not twig).
    $converter->setSdcCustomElementComponents(['sdc.canvas_test_sdc.props-no-slots']);

    $custom_element = $converter->convertRenderArray($canvas_render_array);

    // Verify we get a renderless-container for multiple components.
    $this->assertInstanceOf(CustomElement::class, $custom_element);
    $this->assertEquals('renderless-container', $custom_element->getTag(), 'Multiple components should use renderless-container');

    // Normalize to verify JSON structure.
    $normalizer = $this->container->get('custom_elements.normalizer');
    $normalized = $normalizer->normalize($custom_element);

    // Should be a flat array, not nested with UUIDs.
    $this->assertIsArray($normalized);
    $this->assertCount(3, $normalized, 'Normalized output should have 3 components');

    // Verify each component in the flat array.
    $this->assertEquals('canvas-test-sdc-props-no-slots', $normalized[0]['element']);
    $this->assertEquals('First Component', $normalized[0]['heading']);

    $this->assertEquals('canvas-test-sdc-props-no-slots', $normalized[1]['element']);
    $this->assertEquals('Second Component', $normalized[1]['heading']);

    $this->assertEquals('canvas-test-sdc-props-no-slots', $normalized[2]['element']);
    $this->assertEquals('Third Component', $normalized[2]['heading']);

    $this->debug('/tmp/canvas_conversion_test.json', [
      'render_array' => $canvas_render_array,
      'custom_element' => $custom_element,
      'normalized' => $normalized,
    ]);

    // TEST SDC TWIG RENDERING: Reuse existing node to test twig rendering.
    // Switch all components to twig rendering.
    $converter->setSdcCustomElementComponents(['sdc.canvas_test_sdc.props-no-slots'], FALSE);

    // Convert the same render array with twig rendering.
    $twig_elements = $converter->convertRenderArray($canvas_render_array);

    // Should still be a renderless-container with three components.
    $this->assertEquals('renderless-container', $twig_elements->getTag());

    // Get the slots directly from the container to check structure.
    $container_slots = $twig_elements->getSlots();
    $this->assertArrayHasKey('default', $container_slots, 'Container should have default slot');
    $this->assertCount(3, $container_slots['default'], 'Container should have 3 elements');

    // Verify each component in the container.
    foreach ([0 => 'First', 1 => 'Second', 2 => 'Third'] as $index => $prefix) {
      $component_element = $container_slots['default'][$index]['content'];
      $this->assertInstanceOf(CustomElement::class, $component_element);
      $this->assertEquals('drupal-markup', $component_element->getTag(),
        "Component $index should be drupal-markup for twig rendering");

      // Get the rendered content from the drupal-markup element.
      $slot = $component_element->getSlot('default');
      $this->assertNotEmpty($slot, "Component $index should have default slot");

      $html = (string) $slot['content'];

      // Verify the heading prop is rendered correctly in the twig template.
      $this->assertStringContainsString("{$prefix} Component", $html,
        "Twig-rendered HTML should contain heading prop value: {$prefix} Component");

      // Verify the component ID attribute is present.
      $this->assertStringContainsString('data-component-id="canvas_test_sdc:props-no-slots"', $html,
        'Twig-rendered HTML should have component ID attribute');

      // Verify the heading is rendered in an h1 tag (from the SDC template).
      $this->assertMatchesRegularExpression(
        '/<h1[^>]*>' . preg_quote("{$prefix} Component", '/') . '<\/h1>/',
        $html,
        "The heading prop should be rendered inside an h1 tag"
      );
    }
  }

  /**
   * Tests conversion of nested Canvas components with two column layout.
   */
  public function testCanvasNestedComponentConversion(): void {
    // Create a node with the actual two_column component from
    // Canvas.
    $node = Node::create(
          [
            'type' => 'article',
            'title' => 'Two Column Layout Test',
            'field_component_tree' => [
          // Two-column layout as root component.
          [
            'uuid' => '11111111-2222-3333-4444-555555555555',
            'component_id' => 'sdc.canvas_test_sdc.two_column',
            'component_version' => 'active',
            'inputs' => [
              'width' => [
                'sourceType' => 'static:field_item:integer',
                'value' => 50,
                'expression' => 'ℹ︎integer␟value',
              ],
            ],
          ],
          // First column content - a simple props component.
          [
            'uuid' => '22222222-3333-4444-5555-666666666666',
            'component_id' => 'sdc.canvas_test_sdc.props-no-slots',
            'component_version' => 'active',
            'parent_uuid' => '11111111-2222-3333-4444-555555555555',
            'slot' => 'column_one',
            'inputs' => [
              'heading' => [
                'sourceType' => 'static:field_item:string',
                'value' => 'First Column Heading',
                'expression' => 'ℹ︎string␟value',
              ],
            ],
          ],
          // Second column content - another props component.
          [
            'uuid' => '33333333-4444-5555-6666-777777777777',
            'component_id' => 'sdc.canvas_test_sdc.props-no-slots',
            'component_version' => 'active',
            'parent_uuid' => '11111111-2222-3333-4444-555555555555',
            'slot' => 'column_two',
            'inputs' => [
              'heading' => [
                'sourceType' => 'static:field_item:string',
                'value' => 'Second Column Heading',
                'expression' => 'ℹ︎string␟value',
              ],
            ],
          ],
            ],
          ]
      );
    $node->save();

    // Get the Canvas field render array.
    $field_items = $node->get('field_component_tree');
    $canvas_render_array = $field_items->toRenderable($node);

    $this->debug('/tmp/canvas_debug_full.json', $canvas_render_array);

    // Convert to custom element and see what happens.
    /**
     * @var \Drupal\custom_elements\RenderConverter\CanvasRenderConverter $converter
     */
    $converter = $this->container->get('custom_elements.canvas_render_converter');

    // FIRST TEST: Configure SDCs to be rendered as custom elements (not twig).
    $converter->setSdcCustomElementComponents([
      'sdc.canvas_test_sdc.two_column',
      'sdc.canvas_test_sdc.props-no-slots',
    ]);

    $custom_element = $converter->convertRenderArray($canvas_render_array);

    $this->debug(
          '/tmp/canvas_conversion_debug.json', [
            'render_array' => $canvas_render_array,
            'custom_element' => $custom_element,
          ]
      );

    // Verify the two column component was converted.
    $this->assertInstanceOf(CustomElement::class, $custom_element);
    $this->assertEquals('canvas-test-sdc-two-column', $custom_element->getTag());

    // Verify the width prop was converted.
    $this->assertEquals(50, $custom_element->getAttribute('width'));

    // Verify both column slots are present.
    $slots = $custom_element->getSlots();
    $this->assertArrayHasKey('column-one', $slots, 'Column one slot must exist');
    $this->assertArrayHasKey('column-two', $slots, 'Column two slot must exist');

    // Verify content in column one.
    $this->assertNotEmpty($slots['column-one']);
    $column_one_content = $slots['column-one'][0]['content'];
    $this->assertInstanceOf(CustomElement::class, $column_one_content);
    $this->assertEquals('canvas-test-sdc-props-no-slots', $column_one_content->getTag());
    $this->assertEquals('First Column Heading', $column_one_content->getAttribute('heading'));

    // Verify content in column two.
    $this->assertNotEmpty($slots['column-two']);
    $column_two_content = $slots['column-two'][0]['content'];
    $this->assertInstanceOf(CustomElement::class, $column_two_content);
    $this->assertEquals('canvas-test-sdc-props-no-slots', $column_two_content->getTag());
    $this->assertEquals('Second Column Heading', $column_two_content->getAttribute('heading'));

    // SECOND TEST: Now test with all components rendered via twig.
    // Clear the custom element configuration to render all SDCs via twig.
    $converter->setSdcCustomElementComponents([
      'sdc.canvas_test_sdc.two_column',
      'sdc.canvas_test_sdc.props-no-slots',
    ], FALSE);

    // Convert the same render array with twig rendering for all components.
    $twig_rendered_element = $converter->convertRenderArray($canvas_render_array);

    $this->debug(
          '/tmp/canvas_twig_conversion_debug.json', [
            'render_array' => $canvas_render_array,
            'twig_rendered_element' => $twig_rendered_element,
          ]
      );

    // The root element should be drupal-markup containing the rendered twig.
    $this->assertInstanceOf(CustomElement::class, $twig_rendered_element);
    $this->assertEquals('drupal-markup', $twig_rendered_element->getTag(),
      'Root element should be drupal-markup when rendering via twig');

    // Get the rendered HTML content.
    $slot = $twig_rendered_element->getSlot('default');
    $this->assertNotEmpty($slot, 'drupal-markup should have default slot');

    $html = (string) $slot['content'];

    // Verify the two-column structure is present in the HTML.
    $this->assertStringContainsString('class="column-one width-50"', $html,
      'Twig-rendered HTML should contain column-one with correct width');
    $this->assertStringContainsString('class="column-two width-50"', $html,
      'Twig-rendered HTML should contain column-two with correct width');

    // Verify the headings from the nested components are rendered.
    $this->assertStringContainsString('First Column Heading', $html,
      'First column heading should be present in twig-rendered HTML');
    $this->assertStringContainsString('Second Column Heading', $html,
      'Second column heading should be present in twig-rendered HTML');

    // Verify the nested components are rendered with their h1 tags.
    $this->assertMatchesRegularExpression(
      '/<h1[^>]*>First Column Heading<\/h1>/',
      $html,
      'First column heading should be in an h1 tag'
    );
    $this->assertMatchesRegularExpression(
      '/<h1[^>]*>Second Column Heading<\/h1>/',
      $html,
      'Second column heading should be in an h1 tag'
    );
  }

  /**
   * Tests real Drupal block plugins (dynamic components) in Canvas.
   *
   * Tests that blocks render as drupal-markup elements and cache metadata
   * is preserved.
   */
  public function testCanvasBlockComponent(): void {
    // Create a node with a two-column layout containing the "Powered by
    // Drupal" block.
    $node = Node::create(
          [
            'type' => 'article',
            'title' => 'Dynamic Component (Block) Test',
            'field_component_tree' => [
          // Two-column layout as root component.
          [
            'uuid' => '77777777-8888-9999-aaaa-bbbbbbbbbbbb',
            'component_id' => 'sdc.canvas_test_sdc.two_column',
            'component_version' => 'active',
            'inputs' => [
              'width' => [
                'sourceType' => 'static:field_item:integer',
                'value' => 50,
                'expression' => 'ℹ︎integer␟value',
              ],
            ],
          ],
          // "Powered by Drupal" block in the first column.
          [
            'uuid' => '88888888-9999-aaaa-bbbb-cccccccccccc',
            'component_id' => 'block.system_powered_by_block',
            'component_version' => 'active',
            'parent_uuid' => '77777777-8888-9999-aaaa-bbbbbbbbbbbb',
            'slot' => 'column_one',
            'inputs' => [
              'label' => 'Powered by Drupal',
              'label_display' => 'visible',
            ],
          ],
          // A heading component in the second column.
          [
            'uuid' => '99999999-aaaa-bbbb-cccc-dddddddddddd',
            'component_id' => 'sdc.canvas_test_sdc.props-no-slots',
            'component_version' => 'active',
            'parent_uuid' => '77777777-8888-9999-aaaa-bbbbbbbbbbbb',
            'slot' => 'column_two',
            'inputs' => [
              'heading' => [
                'sourceType' => 'static:field_item:string',
                'value' => 'Content alongside the block',
                'expression' => 'ℹ︎string␟value',
              ],
            ],
          ],
            ],
          ]
      );
    $node->save();

    // Get the Canvas field render array.
    $field_items = $node->get('field_component_tree');
    $canvas_render_array = $field_items->toRenderable($node);

    $this->debug('/tmp/canvas_block_debug.json', $canvas_render_array);

    /**
     * @var \Drupal\custom_elements\RenderConverter\CanvasRenderConverter $converter
     */
    $converter = $this->container->get('custom_elements.canvas_render_converter');

    // Configure SDCs to be rendered as custom elements (not twig).
    // Note: block.system_powered_by_block is not an SDC, so it doesn't
    // need configuration.
    $converter->setSdcCustomElementComponents([
      'sdc.canvas_test_sdc.two_column',
      'sdc.canvas_test_sdc.props-no-slots',
    ]);

    $custom_element = $converter->convertRenderArray($canvas_render_array);

    $this->debug(
          '/tmp/canvas_block_conversion.json', [
            'render_array' => $canvas_render_array,
            'custom_element' => $custom_element,
          ]
      );

    // Verify the root structure.
    $this->assertInstanceOf(CustomElement::class, $custom_element);
    $this->assertEquals('canvas-test-sdc-two-column', $custom_element->getTag());

    // Get the block element from column one.
    $slots = $custom_element->getSlots();
    $this->assertArrayHasKey('column-one', $slots, 'Column one slot should exist');
    $this->assertNotEmpty($slots['column-one'], 'Column one should not be empty');

    $block_element = $slots['column-one'][0]['content'];

    // Blocks should be wrapped in drupal-markup.
    $this->assertInstanceOf(CustomElement::class, $block_element);
    $this->assertEquals('drupal-markup', $block_element->getTag());

    // Verify cache metadata is preserved on the custom element.
    $cache_tags = $block_element->getCacheTags();
    $this->assertContains('config:canvas.component.block.system_powered_by_block', $cache_tags, 'Block cache tags should be preserved');

    // Verify the heading in column two renders normally.
    $heading_element = $slots['column-two'][0]['content'];
    $this->assertEquals('canvas-test-sdc-props-no-slots', $heading_element->getTag());
    $this->assertEquals('Content alongside the block', $heading_element->getAttribute('heading'));
  }

  /**
   * Tests the Canvas field formatter with entity CE display.
   */
  public function testCanvasFormatter(): void {
    // Create a node with Canvas content.
    $node = Node::create([
      'type' => 'article',
      'title' => 'Test Canvas Formatter',
      'field_component_tree' => [
        [
          'uuid' => '12345678-formatter-test',
          'component_id' => 'sdc.canvas_test_sdc.props-no-slots',
          'component_version' => 'active',
          'inputs' => [
            'heading' => [
              'sourceType' => 'static:field_item:string',
              'value' => 'Canvas Component Heading',
              'expression' => 'ℹ︎string␟value',
            ],
          ],
        ],
      ],
    ]);
    $node->save();

    // Configure CE display for node with Canvas field as slot.
    EntityCeDisplay::create([
      'targetEntityType' => 'node',
      'customElementName' => 'article',
      'bundle' => 'article',
      'mode' => 'default',
    ])
      ->setComponent('title', [
        'field_name' => 'title',
        'is_slot' => FALSE,
        'formatter' => 'flattened',
      ])
      ->setComponent('xb_content', [
        'field_name' => 'field_component_tree',
        'is_slot' => TRUE,
        'formatter' => 'canvas',
      ])
      ->save();

    // Configure the converter to render this SDC as custom element for test.
    /** @var \Drupal\custom_elements\RenderConverter\CanvasRenderConverter $converter */
    $converter = $this->container->get('custom_elements.canvas_render_converter');
    $converter->setSdcCustomElementComponents(['sdc.canvas_test_sdc.props-no-slots']);

    // Generate the custom element using the CE display.
    $custom_element = $this->getCustomElementGenerator()
      ->generate($node, 'default');

    // Verify the node is rendered as custom element.
    $this->assertEquals('article', $custom_element->getTag());
    $this->assertEquals('Test Canvas Formatter', $custom_element->getAttribute('title'));

    // Verify Canvas content was added as slot.
    $slots = $custom_element->getSlots();
    $this->assertArrayHasKey('xb-content', $slots);
    $this->assertNotEmpty($slots['xb-content']);

    $canvas_element = $slots['xb-content'][0]['content'];
    $this->assertInstanceOf(CustomElement::class, $canvas_element);
    $this->assertEquals('canvas-test-sdc-props-no-slots', $canvas_element->getTag());
    $this->assertEquals('Canvas Component Heading', $canvas_element->getAttribute('heading'));

    // Test formatter as attribute by updating the CE display.
    EntityCeDisplay::load('node.article.default')
      ->setComponent('xb_data', [
        'field_name' => 'field_component_tree',
        'is_slot' => FALSE,
        'formatter' => 'canvas',
      ])
      ->removeComponent('xb_content')
      ->save();

    // Generate custom element with Canvas as attribute.
    $custom_element2 = $this->getCustomElementGenerator()
      ->generate($node, 'default');

    // Verify normalized data was added as attribute.
    $canvas_data = $custom_element2->getAttribute('xb-data');
    $this->assertNotNull($canvas_data, 'Canvas data attribute should be set');
    $this->assertIsArray($canvas_data);
    $this->assertEquals('canvas-test-sdc-props-no-slots', $canvas_data['element']);
    $this->assertArrayHasKey('heading', $canvas_data);
    $this->assertEquals('Canvas Component Heading', $canvas_data['heading']);

    // Verify the slot was removed.
    $slots2 = $custom_element2->getSlots();
    $this->assertArrayNotHasKey('xb-content', $slots2);
  }

  /**
   * Tests conversion of extjs components with nested slots.
   */
  public function testCanvasExtJsComponentConversion(): void {
    // Create extjs test components.
    $this->createExtJsComponent('TwoColumnLayout', [
      'label' => 'Two Column Layout',
      'category' => 'Test Components',
      'props' => [
        'type' => 'object',
        'properties' => [
          'width' => [
            'type' => 'integer',
            'title' => 'Width',
            'default' => 50,
          ],
        ],
      ],
      'slots' => [
        'column-one' => ['title' => 'Column One'],
        'column-two' => ['title' => 'Column Two'],
      ],
    ]);

    $this->createExtJsComponent('TestButton', [
      'label' => 'Test Button',
      'category' => 'Test Components',
      'props' => [
        'type' => 'object',
        'properties' => [
          'label' => ['type' => 'string', 'default' => 'Click me'],
          'variant' => ['type' => 'string', 'default' => 'primary'],
        ],
      ],
    ]);

    // Create a node with TwoColumnLayout containing TestButton in a slot.
    $node = Node::create([
      'type' => 'article',
      'title' => 'ExtJS Component Test',
      'field_component_tree' => [
        // TwoColumnLayout as root.
        [
          'uuid' => 'eeeeeeee-ffff-0000-1111-222222222222',
          'component_id' => 'extjs.two_column_layout',
          'component_version' => 'active',
          'inputs' => [
            'width' => [
              'sourceType' => 'static:field_item:integer',
              'value' => 50,
              'expression' => 'ℹ︎integer␟value',
            ],
          ],
        ],
        // TestButton in column-one slot.
        [
          'uuid' => 'ffffffff-0000-1111-2222-333333333333',
          'component_id' => 'extjs.test_button',
          'component_version' => 'active',
          'parent_uuid' => 'eeeeeeee-ffff-0000-1111-222222222222',
          'slot' => 'column-one',
          'inputs' => [
            'label' => [
              'sourceType' => 'static:field_item:string',
              'value' => 'Nested Button',
              'expression' => 'ℹ︎string␟value',
            ],
            'variant' => [
              'sourceType' => 'static:field_item:string',
              'value' => 'success',
              'expression' => 'ℹ︎string␟value',
            ],
          ],
        ],
      ],
    ]);
    $node->save();

    // Get the Canvas field render array.
    $field_items = $node->get('field_component_tree');
    $canvas_render_array = $field_items->toRenderable($node);

    /** @var \Drupal\custom_elements\RenderConverter\CanvasRenderConverter $converter */
    $converter = $this->container->get('custom_elements.canvas_render_converter');
    $custom_element = $converter->convertRenderArray($canvas_render_array);

    // Verify the root is TwoColumnLayout.
    $this->assertInstanceOf(CustomElement::class, $custom_element);
    $this->assertEquals('TwoColumnLayout', $custom_element->getTag());
    $this->assertEquals(50, $custom_element->getAttribute('width'));

    // Verify slots contain nested CustomElement objects, not HTML strings.
    $slots = $custom_element->getSlots();
    $this->assertArrayHasKey('column-one', $slots);

    $column_one_entry = $slots['column-one'][0];
    $this->assertArrayHasKey('content', $column_one_entry);
    $nested_element = $column_one_entry['content'];
    $this->assertInstanceOf(CustomElement::class, $nested_element, 'Nested component should be CustomElement, not HTML string');
    $this->assertEquals('TestButton', $nested_element->getTag());
    $this->assertEquals('Nested Button', $nested_element->getAttribute('label'));
    $this->assertEquals('success', $nested_element->getAttribute('variant'));
  }

  /**
   * Tests Canvas Page entity rendering with CE generator.
   */
  public function testCanvasPageEntitySupport(): void {
    // Create extjs test component.
    $this->createExtJsComponent('TestButton', [
      'label' => 'Test Button',
      'category' => 'Test Components',
      'props' => [
        'type' => 'object',
        'properties' => [
          'label' => ['type' => 'string', 'default' => 'Click me'],
          'variant' => ['type' => 'string', 'default' => 'primary'],
        ],
      ],
    ]);

    // Install Page entity schema.
    $this->installEntitySchema('canvas_page');

    // Create a Canvas Page with a component.
    $page = Page::create([
      'title' => 'Test Page',
      'components' => [
        [
          'uuid' => 'page-button-uuid',
          'component_id' => 'extjs.test_button',
          'component_version' => 'active',
          'inputs' => [
            'label' => [
              'sourceType' => 'static:field_item:string',
              'value' => 'Page Button',
              'expression' => 'ℹ︎string␟value',
            ],
            'variant' => [
              'sourceType' => 'static:field_item:string',
              'value' => 'primary',
              'expression' => 'ℹ︎string␟value',
            ],
          ],
        ],
      ],
    ]);
    $page->save();

    // Generate the custom element using the auto-generated CE display.
    $custom_element = $this->getCustomElementGenerator()
      ->generate($page, 'full');

    // The component is wrapped in drupal-markup from view() rendering.
    // Just verify it's a CustomElement and contains our component data.
    $this->assertInstanceOf(CustomElement::class, $custom_element);
    $this->assertEquals('canvas-page', $custom_element->getTag());
    $slot = $custom_element->getSlot('components');
    $this->assertArrayHasKey('content', $slot);
    $nested_element = $slot['content'];
    $this->assertInstanceOf(CustomElement::class, $nested_element, 'Nested component should be CustomElement, not HTML string');
    $this->assertEquals('TestButton', $nested_element->getTag());
  }

}

Главная | Обратная связь

drupal hosting | друпал хостинг | it patrol .inc