custom_elements-8.x-2.x-dev/tests/src/Kernel/Formatter/ImageCeFieldFormatterTest.php
tests/src/Kernel/Formatter/ImageCeFieldFormatterTest.php
<?php
namespace Drupal\Tests\custom_elements\Kernel\Formatter;
use Drupal\Core\Render\Markup;
use Drupal\KernelTests\KernelTestBase;
use Drupal\custom_elements\CustomElementGeneratorTrait;
use Drupal\custom_elements\Entity\EntityCeDisplay;
use Drupal\node\Entity\Node;
use Drupal\node\Entity\NodeType;
use Drupal\image\Entity\ImageStyle;
use Drupal\Tests\content_translation\Traits\ContentTranslationTestTrait;
use Drupal\Tests\custom_elements\Traits\TestHelperTrait;
/**
* Tests rendering of image fields with custom elements.
*
* @group custom_elements
*/
class ImageCeFieldFormatterTest extends KernelTestBase {
use ContentTranslationTestTrait;
use CustomElementGeneratorTrait;
use TestHelperTrait;
/**
* {@inheritdoc}
*
* @var bool
* @todo Fix config schema for CE-displays and re-enable.
*/
protected $strictConfigSchema = FALSE;
/**
* Modules to install.
*
* @var string[]
*/
protected static $modules = [
'custom_elements',
'user',
'node',
'field',
'text',
'file',
'image',
'path',
'system',
];
/**
* The node used in tests.
*
* @var \Drupal\node\NodeInterface
*/
protected $node;
/**
* The image style used in tests.
*
* @var \Drupal\image\ImageStyleInterface
*/
protected $imageStyle;
/**
* The field name used for the image field.
*
* @var string
*/
protected $fieldName = 'field_image';
/**
* The expected width after applying the image style.
*
* @var int
*/
protected $expectedWidth;
/**
* The expected height after applying the image style.
*
* @var int
*/
protected $expectedHeight;
/**
* Calculates the expected width and height for a given image file URI.
*
* @param string $uri
* The image file URI.
* @param int $max_width
* The max width of the image style.
* @param int $max_height
* The max height of the image style.
*
* @return array
* Array with two elements: [expected_width, expected_height].
*/
protected function calculateExpectedDimensions($uri, $max_width = 120, $max_height = 90) {
$image_factory = \Drupal::service('image.factory');
$image = $image_factory->get($uri);
if (!$image->isValid()) {
$this->fail('Sample image is invalid, cannot determine expected dimensions.');
}
$orig_width = $image->getWidth();
$orig_height = $image->getHeight();
$aspect = $orig_width / $orig_height;
if ($max_width / $max_height > $aspect) {
// Limited by height.
$expected_height = $max_height;
$expected_width = (int) round($max_height * $aspect);
}
else {
// Limited by width.
$expected_width = $max_width;
$expected_height = (int) round($max_width / $aspect);
}
return [$expected_width, $expected_height];
}
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installEntitySchema('user');
$this->installEntitySchema('node');
$this->installEntitySchema('file');
$this->installSchema('file', ['file_usage']);
$this->installSchema('node', ['node_access']);
$this->installConfig(['node', 'image']);
// Create a content type.
$type = NodeType::create([
'type' => 'article',
'name' => 'Article',
]);
$type->save();
// Add an image field to the article content type.
$this->createImageField($this->fieldName, 'article');
// Create an image style with a scale effect to 4:3 aspect ratio (120x90).
$this->imageStyle = ImageStyle::create([
'name' => 'test_style',
'label' => 'Test style',
'effects' => [],
]);
$this->imageStyle->addImageEffect([
'id' => 'image_scale',
'data' => [
'width' => 120,
'height' => 90,
'upscale' => FALSE,
],
'weight' => 0,
]);
$this->imageStyle->save();
// Create a node with the image field set using generateSampleItems().
$node = Node::create([
'type' => 'article',
'title' => 'Test Node',
]);
$node->save();
$node->{$this->fieldName}->generateSampleItems(1);
$node->save();
$this->node = $node;
// Calculate expected dimensions for the styled image.
$image_item = $this->node->{$this->fieldName}->first();
$file = $image_item->entity;
$uri = $file->getFileUri();
[$this->expectedWidth, $this->expectedHeight] = $this->calculateExpectedDimensions($uri);
// Configure CE display for node.
EntityCeDisplay::create([
'targetEntityType' => 'node',
'customElementName' => 'article',
'bundle' => 'article',
'mode' => 'default',
])
->setComponent('image', [
'field_name' => $this->fieldName,
'is_slot' => FALSE,
'formatter' => 'image',
'configuration' => [
'image_style' => 'test_style',
'flatten' => TRUE,
'flatten_skip_prefix' => FALSE,
],
])
->save();
}
/**
* Helper to add an image field to a bundle.
*/
protected function createImageField($field_name, $bundle) {
$this->container->get('entity_type.manager')
->getStorage('field_storage_config')
->create([
'field_name' => $field_name,
'entity_type' => 'node',
'type' => 'image',
'cardinality' => 1,
])
->save();
$this->container->get('entity_type.manager')
->getStorage('field_config')
->create([
'field_name' => $field_name,
'entity_type' => 'node',
'bundle' => $bundle,
'label' => 'Image',
'settings' => [
'alt_field' => TRUE,
'title_field' => TRUE,
],
])
->save();
}
/**
* Changes configuration of the 'image' display component.
*
* @param bool $is_slot
* Value for is_slot property.
* @param bool $flatten
* Whether to flatten output.
* @param bool $flatten_skip_prefix
* Whether to skip prefix in flatten mode.
*/
protected function changeImageDisplayComponent($is_slot, $flatten = TRUE, $flatten_skip_prefix = FALSE) {
EntityCeDisplay::load('node.article.default')
// Remove title component to avoid conflicts.
->removeComponent('title')
->setComponent('image', [
'field_name' => $this->fieldName,
'is_slot' => $is_slot,
'formatter' => 'image',
'configuration' => [
'image_style' => 'test_style',
'flatten' => $flatten,
'flatten_skip_prefix' => $flatten_skip_prefix,
],
])
->save();
}
/**
* Tests output for image field as attribute (flattened, with prefix).
*/
public function testFlattenedAttributeWithPrefix() {
$custom_element = $this->getCustomElementGenerator()
->generate($this->node, 'default');
$image_item = $this->node->{$this->fieldName}->first();
$this->assertEquals($image_item->alt, $custom_element->getAttribute('image-alt'));
$this->assertEquals($image_item->title, $custom_element->getAttribute('image-title'));
$this->assertEquals($this->expectedWidth, $custom_element->getAttribute('image-width'));
$this->assertEquals($this->expectedHeight, $custom_element->getAttribute('image-height'));
$this->assertStringContainsString('/styles/test_style/', $custom_element->getAttribute('image-url'));
$this->assertNull($custom_element->getSlot('image'));
}
/**
* Tests output for image field as attribute (flattened, no prefix).
*/
public function testFlattenedAttributeNoPrefix() {
$this->changeImageDisplayComponent(FALSE, TRUE, TRUE);
$custom_element = $this->getCustomElementGenerator()
->generate($this->node, 'default');
$image_item = $this->node->{$this->fieldName}->first();
$this->assertEquals($image_item->alt, $custom_element->getAttribute('alt'));
$this->assertEquals($image_item->title, $custom_element->getAttribute('title'));
$this->assertEquals($this->expectedWidth, $custom_element->getAttribute('width'));
$this->assertEquals($this->expectedHeight, $custom_element->getAttribute('height'));
$this->assertStringContainsString('/styles/test_style/', $custom_element->getAttribute('url'));
$this->assertNull($custom_element->getSlot('image'));
}
/**
* Tests output for image field as slot (flattened).
*/
public function testFlattenedSlot() {
$this->changeImageDisplayComponent(TRUE, TRUE, FALSE);
$custom_element = $this->getCustomElementGenerator()
->generate($this->node, 'default');
$this->assertNull($custom_element->getAttribute('image'));
$image_item = $this->node->{$this->fieldName}->first();
// In flattened+slot mode, each property is a separate slot with prefix.
foreach (['alt', 'title', 'url'] as $property) {
$slot_name = "image-$property";
$slot_entry = $custom_element->getSlot($slot_name);
$this->assertIsArray($slot_entry, "Slot entry for $slot_name should be an array.");
$this->assertArrayHasKey('content', $slot_entry, "Slot entry for $slot_name should have content.");
if ($property === 'url') {
$this->assertStringContainsString('/styles/test_style/', $slot_entry['content']);
}
else {
$this->assertEquals($image_item->{$property}, (string) $slot_entry['content']);
}
}
foreach (['width', 'height'] as $property) {
$slot_name = "image-$property";
$slot_entry = $custom_element->getSlot($slot_name);
$this->assertIsArray($slot_entry, "Slot entry for $slot_name should be an array.");
$this->assertArrayHasKey('content', $slot_entry, "Slot entry for $slot_name should have content.");
// Fix: Markup objects must be cast to string before int.
$value = $slot_entry['content'];
if ($value instanceof Markup) {
$value = (string) $value;
}
$value = (int) $value;
if ($property === 'width') {
$this->assertEquals($this->expectedWidth, $value);
}
else {
$this->assertEquals($this->expectedHeight, $value);
}
}
}
/**
* Tests output for image field as attribute (not flattened).
*/
public function testNotFlattenedAttribute() {
$this->changeImageDisplayComponent(FALSE, FALSE, FALSE);
$custom_element = $this->getCustomElementGenerator()
->generate($this->node, 'default');
$image_data = $custom_element->getAttribute('image');
$image_item = $this->node->{$this->fieldName}->first();
$this->assertIsArray($image_data);
$this->assertEquals($image_item->alt, $image_data['alt']);
$this->assertEquals($image_item->title, $image_data['title']);
$this->assertEquals($this->expectedWidth, $image_data['width']);
$this->assertEquals($this->expectedHeight, $image_data['height']);
$this->assertStringContainsString('/styles/test_style/', $image_data['url']);
}
/**
* Tests output for image field as slot (not flattened).
*/
public function testNotFlattenedSlot() {
$this->changeImageDisplayComponent(TRUE, FALSE, FALSE);
$custom_element = $this->getCustomElementGenerator()
->generate($this->node, 'default');
$this->assertNull($custom_element->getAttribute('image'));
$slot_entry = $custom_element->getSlot('image');
$image_item = $this->node->{$this->fieldName}->first();
$this->assertIsArray($slot_entry);
$this->assertArrayHasKey('content', $slot_entry);
$content = (string) $slot_entry['content'];
$this->assertStringContainsString($image_item->alt, $content);
$this->assertStringContainsString($image_item->title, $content);
$this->assertStringContainsString('/styles/test_style/', $content);
$this->assertStringContainsString((string) $this->expectedWidth, $content);
$this->assertStringContainsString((string) $this->expectedHeight, $content);
}
/**
* Tests the settings summary for the image field formatter.
*/
public function testSettingsSummary() {
$field_definition = $this->node->getFieldDefinition($this->fieldName);
// Flattened with prefix (default).
$formatter = \Drupal::service('custom_elements.plugin.manager.field.custom_element_formatter')
->createInstance('image', [
'image_style' => 'test_style',
'flatten' => TRUE,
'flatten_skip_prefix' => FALSE,
'field_definition' => $field_definition,
'view_mode' => 'default',
'name' => 'image',
'is_slot' => FALSE,
]);
$summary = array_map('strval', $formatter->settingsSummary());
$summary_string = implode(' ', $summary);
$this->assertStringContainsString('Test style', $summary_string);
$this->assertStringContainsString('Flattened', $summary_string);
// Flattened without prefix.
$formatter->setConfiguration([
'image_style' => 'test_style',
'flatten' => TRUE,
'flatten_skip_prefix' => TRUE,
'field_definition' => $field_definition,
'view_mode' => 'default',
'name' => 'image',
'is_slot' => FALSE,
]);
$summary = array_map('strval', $formatter->settingsSummary());
$summary_string = implode(' ', $summary);
$this->assertStringContainsString('Flattened (no prefix)', $summary_string);
// Not flattened.
$formatter->setConfiguration([
'image_style' => 'test_style',
'flatten' => FALSE,
'flatten_skip_prefix' => FALSE,
'field_definition' => $field_definition,
'view_mode' => 'default',
'name' => 'image',
'is_slot' => FALSE,
]);
$summary = array_map('strval', $formatter->settingsSummary());
$summary_string = implode(' ', $summary);
$this->assertStringNotContainsString('Flattened', $summary_string);
}
/**
* Tests output for a multi-valued image field.
*/
public function testMultiValuedImageField() {
// Create a new image field with cardinality 2.
$multi_field_name = 'field_image_multi';
$this->container->get('entity_type.manager')
->getStorage('field_storage_config')
->create([
'field_name' => $multi_field_name,
'entity_type' => 'node',
'type' => 'image',
'cardinality' => 2,
])
->save();
$this->container->get('entity_type.manager')
->getStorage('field_config')
->create([
'field_name' => $multi_field_name,
'entity_type' => 'node',
'bundle' => 'article',
'label' => 'Image Multi',
'settings' => [
'alt_field' => TRUE,
'title_field' => TRUE,
],
])
->save();
// Create a node with two images.
$node = Node::create([
'type' => 'article',
'title' => 'Multi Image Node',
]);
$node->save();
$node->{$multi_field_name}->generateSampleItems(2);
$node->save();
// Configure CE display for the multi-valued field.
EntityCeDisplay::load('node.article.default')
->setComponent('image_multi', [
'field_name' => $multi_field_name,
'is_slot' => FALSE,
'formatter' => 'image',
'configuration' => [
'image_style' => 'test_style',
'flatten' => FALSE,
'flatten_skip_prefix' => FALSE,
],
])
->save();
// Generate the custom element.
$custom_element = $this->getCustomElementGenerator()
->generate($node, 'default');
// The attribute should be an array of two items.
$image_data = $custom_element->getAttribute('image-multi');
$this->assertIsArray($image_data, 'Multi-valued image attribute is an array.');
$this->assertArrayHasKey('alt', $image_data[0], 'First image has alt property.');
$this->assertArrayHasKey('alt', $image_data[1], 'Second image has alt property.');
$this->assertArrayHasKey('url', $image_data[0], 'First image has url property.');
$this->assertArrayHasKey('url', $image_data[1], 'Second image has url property.');
// Now test as slot.
EntityCeDisplay::load('node.article.default')
->setComponent('image_multi', [
'field_name' => $multi_field_name,
'is_slot' => TRUE,
'formatter' => 'image',
'configuration' => [
'image_style' => 'test_style',
'flatten' => FALSE,
'flatten_skip_prefix' => FALSE,
],
])
->save();
$custom_element = $this->getCustomElementGenerator()
->generate($node, 'default');
// For multi-value, not-flattened slot, each slot entry is a string for each
// field value.
$slot_entry_0 = $custom_element->getSlot('image-multi', 0);
$slot_entry_1 = $custom_element->getSlot('image-multi', 1);
$this->assertIsArray($slot_entry_0, 'Slot entry 0 for multi-valued image is an array.');
$this->assertIsArray($slot_entry_1, 'Slot entry 1 for multi-valued image is an array.');
$this->assertArrayHasKey('content', $slot_entry_0, 'Slot entry 0 has content key.');
$this->assertArrayHasKey('content', $slot_entry_1, 'Slot entry 1 has content key.');
$content_0 = (string) $slot_entry_0['content'];
$content_1 = (string) $slot_entry_1['content'];
$this->assertStringContainsString($node->{$multi_field_name}->get(0)->alt, $content_0);
$this->assertStringContainsString($node->{$multi_field_name}->get(1)->alt, $content_1);
$this->assertStringContainsString('/styles/test_style/', $content_0);
$this->assertStringContainsString('/styles/test_style/', $content_1);
// Now test as flattened attributes.
EntityCeDisplay::load('node.article.default')
->setComponent('image_multi', [
'field_name' => $multi_field_name,
'is_slot' => FALSE,
'formatter' => 'image',
'configuration' => [
'image_style' => 'test_style',
'flatten' => TRUE,
'flatten_skip_prefix' => FALSE,
],
])
->save();
$custom_element = $this->getCustomElementGenerator()
->generate($node, 'default');
// Only the first value is output as attributes.
$image_item = $node->{$multi_field_name}->first();
$this->assertEquals($image_item->alt, $custom_element->getAttribute('image-multi-alt'));
$this->assertEquals($image_item->title, $custom_element->getAttribute('image-multi-title'));
$this->assertStringContainsString('/styles/test_style/', $custom_element->getAttribute('image-multi-url'));
// Calculate expected dimensions for the first image in the multi field.
$file = $image_item->entity;
$uri = $file->getFileUri();
[$expectedWidth, $expectedHeight] = $this->calculateExpectedDimensions($uri);
$width = $custom_element->getAttribute('image-multi-width');
$height = $custom_element->getAttribute('image-multi-height');
if ($width instanceof Markup) {
$width = (string) $width;
}
if ($height instanceof Markup) {
$height = (string) $height;
}
$this->assertEquals($expectedWidth, (int) $width);
$this->assertEquals($expectedHeight, (int) $height);
}
}
