Skip to content

Instantly share code, notes, and snippets.

@edutrul
Last active April 6, 2026 03:35
Show Gist options
  • Select an option

  • Save edutrul/38312bfac529cd054bf8dd083b1bfc57 to your computer and use it in GitHub Desktop.

Select an option

Save edutrul/38312bfac529cd054bf8dd083b1bfc57 to your computer and use it in GitHub Desktop.
How to deal with DTT with a specific paragraph like "Quote" and the correct parent classes following best practices.
<?php
declare(strict_types=1);
namespace Drupal\Tests\server_general\ExistingSite;
use Drupal\Tests\drupal_test_assertions\Assertions\EntityTrait;
use Drupal\Tests\drupal_test_assertions\Assertions\FieldsTrait;
use weitzman\DrupalTestTraits\ExistingSiteBase;
/**
* Abstract base class for testing entity bundles.
*
* Automatically runs field existence and required/optional assertions
* for every concrete test class via testFields().
*/
abstract class EntityBundleTestBase extends ExistingSiteBase implements RequiredAndOptionalFieldTestInterface {
use EntityTrait;
use FieldsTrait;
/**
* Tests required and optional fields for this entity bundle.
*/
public function testFields(): void {
$entity_type = $this->getEntityType();
$entity_bundle = $this->getEntityBundle();
$this->assertEntityExists($entity_type, $entity_bundle);
foreach ($this->getRequiredFields() as $field_name) {
$this->assertFieldIsRequired($field_name, $entity_type, $entity_bundle);
}
foreach ($this->getOptionalFields() as $field_name) {
$this->assertFieldIsNotRequired($field_name, $entity_type, $entity_bundle);
}
}
}
<?php
declare(strict_types=1);
namespace Drupal\Tests\server_general\Traits;
use Drupal\Core\File\FileExists;
use Drupal\file\FileInterface;
use weitzman\DrupalTestTraits\DrupalTrait;
/**
* Provides file entity creation for tests using committed test assets.
*
* Assets live in tests/assets/ and are copied to public:// with
* FileExists::Replace, so files never accumulate across test runs. The
* physical file persists in public:// intentionally — this is safe for
* parallel tests and avoids teardown race conditions.
*
* Available assets:
* - test-image.png
* - test-image.jpg
* - test-file.pdf
*/
trait FileCreationTrait {
use DrupalTrait;
const ASSETS_PATH = 'modules/custom/server_general/tests/assets/';
/**
* Creates a File entity from a committed test asset.
*
* @param string $filename
* The asset filename (e.g. 'test-image.png'). Must exist in tests/assets/.
*
* @return \Drupal\file\FileInterface
* The saved File entity, marked for automatic cleanup.
*/
protected function createFileEntity(string $filename): FileInterface {
$source = \Drupal::root() . '/' . self::ASSETS_PATH . $filename;
$uri = \Drupal::service('file_system')
->copy($source, 'public://' . $filename, FileExists::Replace);
/** @var \Drupal\file\FileInterface $file */
$file = \Drupal::entityTypeManager()
->getStorage('file')
->create([
'uri' => $uri,
'status' => 1,
]);
$file->setPermanent();
$file->save();
$this->markEntityForCleanup($file);
return $file;
}
}
<?php
declare(strict_types=1);
namespace Drupal\Tests\server_general\Traits;
use Drupal\paragraphs\Entity\Paragraph;
use Drupal\paragraphs\ParagraphInterface;
use weitzman\DrupalTestTraits\DrupalTrait;
/**
* Helps in creation of Paragraph entities in tests.
*/
trait ParagraphCreationTrait {
use DrupalTrait;
/**
* Creates a Paragraph and marks it for automatic cleanup.
*
* @param array $settings
* The settings to pass to Paragraph creation.
*
* @return \Drupal\paragraphs\ParagraphInterface
* The created Paragraph entity.
*/
protected function createParagraph(array $settings = []): ParagraphInterface {
/** @var \Drupal\paragraphs\ParagraphInterface $entity */
$entity = Paragraph::create($settings);
$entity->save();
$this->markEntityForCleanup($entity);
return $entity;
}
/**
* Extract the reference values for a paragraph.
*
* @param \Drupal\paragraphs\ParagraphInterface $paragraph
* The paragraph.
*
* @return array
* Reference values containing target_id and target_revision_id.
*/
protected function getParagraphReferenceValues(ParagraphInterface $paragraph): array {
return [
'target_id' => $paragraph->id(),
'target_revision_id' => $paragraph->getRevisionId(),
];
}
}
<?php
declare(strict_types=1);
namespace Drupal\Tests\server_general\ExistingSite;
use Drupal\Tests\server_general\Traits\FileCreationTrait;
use Drupal\Tests\server_general\Traits\ParagraphCreationTrait;
use Symfony\Component\HttpFoundation\Response;
use weitzman\DrupalTestTraits\Entity\MediaCreationTrait;
/**
* Test 'Quote' paragraph type.
*/
final class QuoteParagraphTest extends EntityBundleTestBase {
use FileCreationTrait;
use MediaCreationTrait;
use ParagraphCreationTrait;
/**
* {@inheritdoc}
*/
public function getEntityType(): string {
return 'paragraph';
}
/**
* {@inheritdoc}
*/
public function getEntityBundle(): string {
return 'quote';
}
/**
* {@inheritdoc}
*/
public function getRequiredFields(): array {
return [
'field_body',
'field_image',
];
}
/**
* {@inheritdoc}
*/
public function getOptionalFields(): array {
return [
'field_subtitle',
];
}
/**
* Test render of the paragraph.
*/
public function testRender(): void {
$file = $this->createFileEntity('test-image.png');
$media = $this->createMedia([
'bundle' => 'image',
'name' => 'Test image',
'field_media_image' => [
'target_id' => $file->id(),
'alt' => 'Test image alt',
],
]);
$body = 'This is the body';
$subtitle = 'This is the subtitle';
$paragraph = $this->createParagraph([
'type' => $this->getEntityBundle(),
'field_body' => $body,
'field_subtitle' => $subtitle,
'field_image' => [
'target_id' => $media->id(),
],
]);
$node = $this->createNode([
'title' => 'Landing Page',
'type' => 'landing_page',
'field_paragraphs' => [
$this->getParagraphReferenceValues($paragraph),
],
'moderation_state' => 'published',
]);
$node->setPublished()->save();
$this->drupalGet($node->toUrl());
$this->assertSession()->statusCodeEquals(Response::HTTP_OK);
$this->assertSession()->elementTextContains('css', '.paragraph--type--quote', $body);
$this->assertSession()->elementTextContains('css', '.paragraph--type--quote', $subtitle);
}
}
<?php
declare(strict_types=1);
namespace Drupal\Tests\server_general\ExistingSite;
/**
* Interface defining the contract for entity bundle field tests.
*/
interface RequiredAndOptionalFieldTestInterface {
/**
* Returns the entity type to test.
*
* @return string
* An entity type name (e.g. 'node', 'paragraph').
*/
public function getEntityType(): string;
/**
* Returns the entity bundle to test.
*
* @return string
* A bundle name (e.g. 'quote', 'landing_page').
*/
public function getEntityBundle(): string;
/**
* Returns the required fields for this entity bundle.
*
* @return string[]
* Array of required field names.
*/
public function getRequiredFields(): array;
/**
* Returns the optional fields for this entity bundle.
*
* @return string[]
* Array of optional field names.
*/
public function getOptionalFields(): array;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment